Merge branch 'refactor-for-aimsight' into 'main'
Refactor for AimSight: modular architecture, S3 storage, and new annotation tools See merge request industry/aimsight/river-annotation-tool!1
This commit is contained in:
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(Get-ChildItem -Recurse -Depth 2)",
|
|
||||||
"Bash(Select-Object FullName)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
S3_ACCESS_KEY=your-access-key-here
|
||||||
|
S3_SECRET_ACCESS_KEY=your-secret-key-here
|
||||||
|
S3_ENDPOINT_URL=https://os.zhdk.cloud.switch.ch
|
||||||
|
AWS_REQUEST_CHECKSUM_CALCULATION="when_required"
|
||||||
|
AWS_RESPONSE_CHECKSUM_VALIDATION="when_required"
|
||||||
38
.github/workflows/tests.yml
vendored
38
.github/workflows/tests.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Run tests
|
|
||||||
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
test:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.12"]
|
|
||||||
env:
|
|
||||||
PIP_ROOT_USER_ACTION: ignore
|
|
||||||
UV_LINK_MODE: copy
|
|
||||||
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install uv
|
|
||||||
uv sync --locked
|
|
||||||
|
|
||||||
- name: Check format with ruff
|
|
||||||
run: |
|
|
||||||
uv run ruff format --check
|
|
||||||
|
|
||||||
- name: Check code linting with ruff
|
|
||||||
run: |
|
|
||||||
uv run ruff check
|
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -2,6 +2,8 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
.ipynb_checkpoints/
|
.ipynb_checkpoints/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
.claude/
|
||||||
|
.github/
|
||||||
|
|
||||||
# IDE settings
|
# IDE settings
|
||||||
.vscode/
|
.vscode/
|
||||||
@@ -12,3 +14,11 @@
|
|||||||
|
|
||||||
# Data
|
# Data
|
||||||
data/**
|
data/**
|
||||||
|
|
||||||
|
# User-specific config (copy from *.example.* files)
|
||||||
|
config/config.yaml
|
||||||
|
config/clips.txt
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Notebooks
|
||||||
|
notebooks/
|
||||||
|
|||||||
355
README.md
355
README.md
@@ -1,95 +1,358 @@
|
|||||||
# River Annotation Tool
|
# Video Annotation Tool
|
||||||
|
|
||||||
A desktop application for manually annotating river video clips as part of the [HydroScan](https://github.com/HydroScan) project. It lets annotators draw pixel-level masks over river regions of interest and answer structured survey questions about flow conditions, lighting, and scene quality.
|
A desktop GUI application for manually annotating video clips. Annotators draw pixel-level segmentation masks over footage and answer structured survey questions defined in a config file.
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Load river video clips from ZIP archives (containing MP4s)
|
|
||||||
- Draw and erase masks with an adjustable brush on video frames
|
|
||||||
- Cycle through all frames with auto-playback at native FPS
|
|
||||||
- Answer structured questions across three categories: **River**, **Scene**, and **Weather**
|
|
||||||
- Resume saved annotation sessions; exports masks, metadata, and overlay GIFs
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.12
|
- Python 3.12
|
||||||
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. Clone and install
|
||||||
|
git clone https://gitlab.datascience.ch/industry/aimsight/river-annotation-tool
|
||||||
|
cd river-annotation-tool
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# 2. Create config and clip list from examples
|
||||||
|
cp config/config.example.yaml config/config.yaml
|
||||||
|
cp config/clips.example.txt config/clips.txt
|
||||||
|
|
||||||
|
# 3. Edit config/config.yaml (set data_dir and out_dir)
|
||||||
|
# Edit config/clips.txt (list clips to annotate)
|
||||||
|
# Edit config/questions.yaml to customise survey questions (optional)
|
||||||
|
|
||||||
|
# 4. Run
|
||||||
|
uv run python -m clip_annotator
|
||||||
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Clone the repository
|
# Install with uv (creates the virtual environment automatically)
|
||||||
git clone <repo-url>
|
|
||||||
cd river-annotation-tool
|
|
||||||
|
|
||||||
# Install dependencies (creates a virtual environment automatically with uv)
|
|
||||||
uv sync
|
uv sync
|
||||||
|
|
||||||
# Or with pip
|
# Or with pip
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
.venv\Scripts\activate # Windows
|
.venv\Scripts\activate # Windows
|
||||||
# source .venv/bin/activate # macOS/Linux
|
source .venv/bin/activate # macOS/Linux
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Before running, create your config and clip list from the provided examples:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp config/config.example.yaml config/config.yaml
|
||||||
|
cp config/clips.example.txt config/clips.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `config/config.yaml` to set your `data_dir` and `out_dir`, then edit `config/clips.txt` to list the clips you want to annotate. Survey questions are defined in `config/questions.yaml` (committed to the repo; edit to customise). See the [Configuration](#configuration) section for all available options.
|
||||||
|
|
||||||
|
### S3 storage (optional)
|
||||||
|
|
||||||
|
By default the tool reads clips from and writes annotations to the local filesystem (`storage: local`). To use an S3-compatible object store instead, set `storage: s3` in `config/config.yaml` and give `data_dir` / `out_dir` as `bucket/prefix` paths:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
storage: s3
|
||||||
|
data_dir: my-bucket/clips
|
||||||
|
out_dir: my-bucket/annotation_results
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and fill in your credentials — the app loads this file automatically at startup:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .env.example .env
|
||||||
|
# edit .env with your credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|---|---|
|
||||||
|
| `S3_ACCESS_KEY` | Access key ID |
|
||||||
|
| `S3_SECRET_ACCESS_KEY` | Secret access key |
|
||||||
|
| `S3_ENDPOINT_URL` | Endpoint URL (defaults to `https://os.zhdk.cloud.switch.ch` if not set) |
|
||||||
|
| `AWS_REQUEST_CHECKSUM_CALCULATION` | Set to `when_required` to avoid checksum errors on SwitchEngines/Ceph |
|
||||||
|
| `AWS_RESPONSE_CHECKSUM_VALIDATION` | Set to `when_required` to avoid checksum errors on SwitchEngines/Ceph |
|
||||||
|
|
||||||
|
The `clips_file` (the list of clip filenames to annotate) is always read from the local filesystem even when `storage: s3`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python -m river_annotation_tool.annotation_script \
|
uv run python -m clip_annotator
|
||||||
--data <path/to/zip/files> \
|
# or, if you have the venv activated:
|
||||||
--out <path/to/output/dir> \
|
python -m clip_annotator
|
||||||
[--clip <clip_name>]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
| Argument | Default | Description |
|
| Argument | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `--data` | `../torrent-flow/data/examples_for_annotations/` | Directory containing ZIP files |
|
| `--config` | `config/config.yaml` | Path to the config YAML file |
|
||||||
| `--out` | `data/annotation_results/` | Output directory for saved annotations |
|
| `--data` | *(from config)* | Override `data_dir` from config |
|
||||||
| `--clip` | *(first clip)* | Specific clip to open (e.g. `left_20230501`) |
|
| `--out` | *(from config)* | Override `out_dir` from config |
|
||||||
|
| `--clips` | *(from config)* | Override `clips_file` from config |
|
||||||
|
| `--clip` | *(first unannotated in list)* | Open a specific clip by its stem name (filename without extension, e.g. `clip_20230501T120000`) |
|
||||||
|
| `--extras` | off | Also save GIFs and extra PNGs (see Output section). **Note: GIF encoding is slow — saving will freeze the UI for several seconds per clip.** |
|
||||||
|
| `--no-skip` | off | Show already-annotated clips instead of skipping them |
|
||||||
|
|
||||||
### Controls
|
### Typical workflows
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Annotate clips listed in config/clips.txt (default)
|
||||||
|
uv run python -m clip_annotator
|
||||||
|
|
||||||
|
# Use a different config file
|
||||||
|
uv run python -m clip_annotator --config config/my_config.yaml
|
||||||
|
|
||||||
|
# Override paths from the command line
|
||||||
|
uv run python -m clip_annotator --data data/clips --out data/out
|
||||||
|
|
||||||
|
# Annotate a single specific clip
|
||||||
|
uv run python -m clip_annotator --clip clip_20230615T120000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Main settings live in `config/config.yaml`. Copy `config/config.example.yaml` to get started.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
storage: local # required: 'local' or 's3'
|
||||||
|
|
||||||
|
data_dir: # required: read-only source of ZIP archives (local path or bucket/prefix for S3)
|
||||||
|
out_dir: # required: write destination for annotations (can be same bucket as data_dir with a different prefix, or a separate location)
|
||||||
|
|
||||||
|
clips_file: config/clips.txt
|
||||||
|
optical_flow_config_file: config/optical_flow_config.yaml
|
||||||
|
questions_config_file: config/questions.yaml
|
||||||
|
|
||||||
|
display_max: 720 # longest side in pixels for display
|
||||||
|
fps_fallback: 25 # FPS to use if the video header is missing
|
||||||
|
max_frames: 100 # max frames to extract per clip
|
||||||
|
|
||||||
|
# Override input filenames only if your ZIP archives differ from the defaults
|
||||||
|
filenames:
|
||||||
|
video_in_zip: left.mp4
|
||||||
|
video_tmp_suffix: .mp4
|
||||||
|
zip_extension: .zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Output filenames (`mask.png`, `metadata.json`, etc.) have sensible defaults and can be overridden in the `filenames:` block — see [`config.py`](src/clip_annotator/config.py) for the full list.
|
||||||
|
|
||||||
|
### Survey questions
|
||||||
|
|
||||||
|
Survey questions are defined in `config/questions.yaml` (committed to the repo). Add, remove, or reorder sections and items — the UI rebuilds automatically. `key` is what gets saved in `metadata.json`; `default` selects the pre-checked option (omit or set to `null` to leave unselected).
|
||||||
|
|
||||||
|
### Optical flow segmentation
|
||||||
|
|
||||||
|
`config/optical_flow_config.yaml` controls the **Auto Segment** button. When pressed, the tool computes a segmentation mask from the loaded frames and replaces the current mask (undoable). The segmentation combines two criteria:
|
||||||
|
|
||||||
|
- **Optical flow magnitude** — pixels where the temporal median of frame-to-frame flow (scaled by FPS) exceeds a fraction of the maximum are considered moving.
|
||||||
|
- **Brightness** — pixels outside a brightness window are excluded (removes sky, saturated glare, etc.).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/optical_flow_config.yaml
|
||||||
|
enabled: true
|
||||||
|
norm_squared_threshold: 0.06 # fraction of max flow² that counts as moving
|
||||||
|
gaussian_kernel: [5, 5] # blur kernel applied to the reference frame before brightness check
|
||||||
|
brightness_range: [2, 253] # [min, max] greyscale brightness to keep
|
||||||
|
```
|
||||||
|
|
||||||
|
`enabled: false` disables the button without removing the config file.
|
||||||
|
|
||||||
|
## Clip list file
|
||||||
|
|
||||||
|
`config/clips.txt` lists bare clip filenames (not full paths) to annotate, one per line — the tool prepends `data_dir` automatically. Each entry must be the exact filename of the ZIP archive as it appears in `data_dir` (e.g. `clip_20230501T120000.zip`). Lines starting with `#` are ignored. Clips are processed in order; a clip is considered already-annotated when `<out_dir>/<clip_stem>/mask.png` exists and is skipped automatically. Pass `--no-skip` to include already-annotated clips. When the last clip is reached, a dialog appears and the app exits.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Example clips.txt
|
||||||
|
clip_20230501T120000.zip
|
||||||
|
clip_20230502T120000.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `config/clips.example.txt` as a starting point.
|
||||||
|
|
||||||
|
## Multi-annotator setup
|
||||||
|
|
||||||
|
To distribute clips across multiple annotators, create one clips file per annotator (e.g. `config/annotator_A.txt`) listing their assigned clip filenames, then pass it via `--clips`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv run python -m clip_annotator --clips config/annotator_A.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Assigning non-overlapping clip lists lets each annotator work independently. Intentionally overlapping a subset of clips across annotators enables inter-annotator agreement checks.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
The window is split into two panels: the **video canvas** on the left (~70% of the width) and the **survey panel** on the right. The video auto-plays as a looping preview. Drawing tools and mask controls are arranged above and beside the canvas; navigation buttons (**Previous / Next / Skip**) sit at the top.
|
||||||
|
|
||||||
|
### Tool modes
|
||||||
|
|
||||||
|
Three drawing tools are available in the tool row. The active tool is highlighted in blue.
|
||||||
|
|
||||||
|
| Tool | How to activate | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| **Brush** | Click **Brush** | Click and drag to paint the mask with a circular brush (default) |
|
||||||
|
| **Polygon** | Click **Polygon** | Click to place vertices and build closed shapes; use **Fill** mode to commit them |
|
||||||
|
| **Fill** | Click **Fill** | Click inside a closed polygon to fill it onto the mask |
|
||||||
|
|
||||||
|
### Brush tool
|
||||||
|
|
||||||
| Action | How |
|
| Action | How |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Draw mask | Click and drag on the canvas |
|
| Draw mask | Click and drag on the video |
|
||||||
| Erase mask | Toggle **Eraser** button, then drag |
|
| Erase mask | Toggle **Eraser** button (turns orange when active), then drag |
|
||||||
| Undo last stroke | **Undo** button |
|
| Brush preview | A white circle follows the cursor showing the current brush size |
|
||||||
| Play/pause frames | **Play / Pause** button |
|
| Adjust brush size | **Brush size** slider (2–50 px, default 5); click **↺** to reset |
|
||||||
| Save annotation | **Save** button |
|
|
||||||
| Change brush size | Slider in the toolbar |
|
### Polygon tool
|
||||||
|
|
||||||
|
Polygons are drawn as overlays and do not affect the mask until you use **Fill** mode.
|
||||||
|
|
||||||
|
| Action | How |
|
||||||
|
|---|---|
|
||||||
|
| Add vertex | Left-click on the canvas |
|
||||||
|
| Remove last vertex | Right-click |
|
||||||
|
| Close a shape | Left-click near the first vertex (red dot) when ≥ 3 vertices are placed; completed shapes turn bold cyan |
|
||||||
|
| Draw multiple shapes | Each closed shape is kept independently; draw as many as needed |
|
||||||
|
| Cancel in-progress polygon | **Cancel Current Poly** — discards the unfinished polygon, keeps completed shapes |
|
||||||
|
| Delete last completed shape | **Del Shape** |
|
||||||
|
|
||||||
|
### Fill tool
|
||||||
|
|
||||||
|
| Action | How |
|
||||||
|
|---|---|
|
||||||
|
| Fill a shape | Left-click anywhere inside a closed polygon; that shape's interior is painted onto the mask |
|
||||||
|
| Nested shapes | If a closed polygon lies entirely inside the target, its interior is left unfilled (acts as a hole) |
|
||||||
|
| Innermost shape | Clicking inside nested shapes always fills the innermost (smallest) polygon containing the click |
|
||||||
|
| Undo fill | **Undo** — each fill is a single undoable step |
|
||||||
|
|
||||||
|
### Mask editing
|
||||||
|
|
||||||
|
| Action | How |
|
||||||
|
|---|---|
|
||||||
|
| Undo last action | **Undo** |
|
||||||
|
| Undo 10 actions | **Undo×10** |
|
||||||
|
| Redo | **Redo** |
|
||||||
|
| Clear entire mask | **Clear** |
|
||||||
|
| Toggle mask overlay | **Hide Mask / Show Mask** — button turns red when hidden; does not affect mask data |
|
||||||
|
| Mask transparency | **Mask Alpha** slider (0–100%, default 15%); click **↺** to reset |
|
||||||
|
|
||||||
|
### Starting-point shortcuts
|
||||||
|
|
||||||
|
| Action | How |
|
||||||
|
|---|---|
|
||||||
|
| Load mask from previous clip | **Load Prev Mask** — copies the saved mask of the previous clip onto the current one; undoable |
|
||||||
|
| Optical flow first guess | **Auto Segment** — replaces the current mask with an automatic segmentation based on motion and brightness; undoable. Disabled when `enabled: false` in `config/optical_flow_config.yaml`. |
|
||||||
|
|
||||||
|
### Image display adjustments
|
||||||
|
|
||||||
|
Three vertical sliders sit to the left of the video and affect display only — they do not change what is saved.
|
||||||
|
|
||||||
|
| Slider | Effect | Range |
|
||||||
|
|---|---|---|
|
||||||
|
| Brightness | Shifts all pixel values up or down | −100 to +100 |
|
||||||
|
| Contrast | Scales pixel values around the midpoint | −100 to +100 |
|
||||||
|
| Gamma | Applies a power-law correction (higher = brighter) | 0.1× to 3.0× |
|
||||||
|
|
||||||
|
Click **↺** below any slider to restore its default value.
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
| Action | How |
|
||||||
|
|---|---|
|
||||||
|
| Save and continue | **Next** — saves current clip and loads the next one. If the clip already has a saved annotation a dialog asks whether to replace it or keep the existing save. The UI freezes during saving; this is normal — wait for it to complete before clicking again. |
|
||||||
|
| Go back | **Previous** — saves current clip and returns to the previously viewed clip. Disabled on the first clip. |
|
||||||
|
| Skip without saving | **Skip** — discards any unsaved changes and loads the next clip without writing anything to disk. |
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
Each clip is saved to `<output_dir>/<clip_stem>/`:
|
Each annotated clip produces a folder `<out_dir>/<clip_stem>/` with:
|
||||||
|
|
||||||
```
|
```
|
||||||
mask.png # Binary mask at full resolution
|
mask.png # Binary segmentation mask at full source resolution (always)
|
||||||
metadata.json # Survey answers
|
metadata.json # Survey answers as JSON (always)
|
||||||
frame.png # Key frame
|
frame.png # Middle frame of the clip (always)
|
||||||
mask_vis.png # Mask visualisation
|
overlay.png # That frame with the mask blended in green (always)
|
||||||
overlay.png # Frame + mask overlay
|
|
||||||
video_original_hires.gif
|
# Only with --extras:
|
||||||
video_original_lowres.gif
|
mask_vis.png # Mask rendered as a greyscale PNG
|
||||||
video_overlay_hires.gif
|
video_original_hires.gif # All frames at display resolution
|
||||||
video_overlay_lowres.gif
|
video_original_lowres.gif # All frames at 50% of display resolution
|
||||||
|
video_overlay_hires.gif # Overlay GIF at display resolution
|
||||||
|
video_overlay_lowres.gif # Overlay GIF at 50% of display resolution
|
||||||
```
|
```
|
||||||
|
|
||||||
## Repository Structure
|
All output filenames can be overridden via the `filenames:` section in `config/config.yaml`.
|
||||||
|
|
||||||
|
### Survey answers (`metadata.json`)
|
||||||
|
|
||||||
|
Keys and values are determined by `config/questions.yaml`. With the default questions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"flow": "Turbulent | Laminar | Uncertain",
|
||||||
|
"shadows": "Yes | No | Uncertain",
|
||||||
|
"artifacts": "Yes | No | Uncertain",
|
||||||
|
"lighting": "Day | Night | Uncertain",
|
||||||
|
"exposure": "Overexposed | Underexposed | Both | Normal | Uncertain",
|
||||||
|
"snowing": "Yes | No | Uncertain",
|
||||||
|
"snow_on_ground":"Yes | No | Uncertain"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internals
|
||||||
|
|
||||||
|
### Clip format
|
||||||
|
|
||||||
|
Each clip is a ZIP archive containing a video file. The video inside the archive must be named `left.mp4` by default; if your archives use a different internal name, set `filenames.video_in_zip` in `config.yaml`. The ZIP filename becomes the output folder name (e.g. `clip_20230501T120000.zip` → `<out_dir>/clip_20230501T120000/`).
|
||||||
|
|
||||||
|
### Frame loading
|
||||||
|
|
||||||
|
Up to `max_frames` frames are extracted from the video and scaled so the longest side is `display_max` px. This display-resolution copy is what the annotator works on; the full-resolution dimensions are remembered separately so the saved mask is upscaled back to the original size on export.
|
||||||
|
|
||||||
|
### Mask drawing
|
||||||
|
|
||||||
|
The mask is a binary array at display resolution. **Brush** strokes stamp a filled circle (draw or erase). **Polygon** shapes are stored as overlays and don't touch the mask until a **Fill** click rasterises them — the innermost polygon containing the click is filled, and any polygon whose centroid falls inside it is punched out as a hole.
|
||||||
|
|
||||||
|
Every mask-changing operation is pushed onto an undo stack before it executes. On save, the mask is upscaled to the original video resolution and written as an 8-bit PNG (0 or 255).
|
||||||
|
|
||||||
|
### Resuming
|
||||||
|
|
||||||
|
When a clip is loaded that already has a saved `mask.png` and `metadata.json`, the mask is restored at display resolution and the survey answers are pre-filled.
|
||||||
|
|
||||||
|
## Repository structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/river_annotation_tool/
|
.env.example # S3 credential template (copy to .env and fill in)
|
||||||
annotation_script.py # Main GUI application
|
config/
|
||||||
|
config.yaml # Your local config (git-ignored, copy from example)
|
||||||
|
config.example.yaml # Example config to copy and edit
|
||||||
|
clips.txt # Your clip list (git-ignored, copy from example)
|
||||||
|
clips.example.txt # Example clip list
|
||||||
|
questions.yaml # Survey question definitions
|
||||||
|
optical_flow_config.yaml # Optical flow parameters (set enabled: false to disable Auto Segment)
|
||||||
|
src/clip_annotator/
|
||||||
|
__main__.py # Entry point — argument parsing and app launch
|
||||||
|
annotator.py # Main QMainWindow — orchestrates all components
|
||||||
|
clip_selector.py # Reads the clip list and picks the next clip
|
||||||
|
filesystem.py # Storage backend — local passthrough or S3 via s3fs
|
||||||
|
mask_canvas.py # Drawing widget — brush, undo, erase, mouse events
|
||||||
|
video_loader.py # ZIP extraction and frame resizing
|
||||||
|
compute_optical_flow.py # Optical flow segmentation (Auto Segment button)
|
||||||
|
config.py # AppConfig dataclass and YAML loader
|
||||||
__init__.py # Package version
|
__init__.py # Package version
|
||||||
pyproject.toml # Project metadata and dependencies
|
pyproject.toml # Project metadata and dependencies
|
||||||
requirements.txt # Pinned dependencies (generated)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Install pre-commit hooks
|
# Install pre-commit hooks
|
||||||
pre-commit install
|
uv run pre-commit install
|
||||||
pre-commit run --all-files # Run hooks manually once
|
uv run pre-commit run --all-files # Run manually once
|
||||||
|
|
||||||
# Add a dependency
|
# Add a dependency
|
||||||
uv add <package>
|
uv add <package>
|
||||||
|
|||||||
94
config/annotator_A.txt
Normal file
94
config/annotator_A.txt
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
GRAMMONT_2025-11-17T11:31:38.546953+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T12:31:39.650554+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T15:32:07.184007+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T15:47:10.070449+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T16:02:09.881377+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T16:17:07.937820+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T16:32:06.019806+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T16:47:05.241264+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T17:02:05.056396+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T17:17:05.186394+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T19:47:09.762766+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T20:02:05.552868+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T23:02:09.394251+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T09:32:01.515556+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T11:17:03.118822+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T15:17:03.043807+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T15:32:04.085232+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T15:47:04.864411+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T16:02:09.554901+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T16:17:07.984380+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T16:32:06.042132+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T16:47:05.425453+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T17:02:05.867456+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T19:17:08.208705+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T19:47:07.924056+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T20:17:04.802945+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T23:17:04.368047+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T00:01:38.617204+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T01:01:28.406165+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T01:16:27.492561+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T04:31:27.576814+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T06:01:28.565510+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T06:16:27.160500+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T06:31:27.071465+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T06:46:26.490815+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T07:01:27.248968+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T07:16:24.975576+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T07:31:22.112561+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T07:46:21.433475+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T08:01:18.720307+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T09:31:19.111375+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T09:46:17.316758+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T10:01:17.837711+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T10:16:18.968668+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T11:16:20.885047+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T11:31:19.000253+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T15:31:21.006934+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T15:46:23.298079+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T16:01:26.959862+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T16:16:26.753510+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T16:31:27.166783+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T16:46:27.263665+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T17:01:27.154872+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T17:46:27.751280+00:00.zip
|
||||||
|
GRAMMONT_2026-01-01T21:31:28.388140+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T00:01:37.412545+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T05:31:25.188144+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T07:31:19.934715+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T07:46:17.007279+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T08:16:17.338747+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T10:31:15.867851+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T18:16:25.202124+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T19:16:26.688542+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T20:01:22.748919+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T23:31:26.962588+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T00:01:38.048234+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T01:01:25.496751+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T04:31:25.749592+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T04:46:26.124855+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T05:31:27.076000+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T05:46:26.426312+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T06:01:26.452689+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T06:16:26.767560+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T06:31:24.624797+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T06:46:23.455019+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T07:01:21.080045+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T07:16:20.533988+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T07:31:19.697932+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T08:01:19.085138+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T09:16:17.805079+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T09:31:17.747859+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T12:31:20.771768+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T15:46:19.819366+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T16:31:20.659090+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T16:46:19.780502+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T17:01:25.098410+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T17:16:25.979320+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T17:31:27.522773+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T17:46:26.400124+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T18:01:25.365899+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T18:16:25.442042+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T21:01:25.956306+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T21:31:26.462852+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T22:31:26.629631+00:00.zip
|
||||||
128
config/annotator_B.txt
Normal file
128
config/annotator_B.txt
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
GRAMMONT_2025-11-18T00:02:16.622295+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T01:02:05.553357+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T01:17:10.391972+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T04:02:09.847002+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T05:47:07.350141+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T06:02:06.960352+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T06:17:11.438801+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T06:32:10.134718+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T06:47:05.176210+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T07:02:05.016401+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T07:17:04.505510+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T07:32:02.052621+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T10:47:04.410566+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T14:47:02.368668+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T15:32:07.687051+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T15:47:04.708858+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T16:02:08.642632+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T16:17:08.155540+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T16:32:08.782723+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T16:47:09.340365+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T17:02:04.224910+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T17:17:06.866509+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T18:32:07.533041+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T00:02:14.666551+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T01:02:05.794759+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T01:17:05.892742+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T04:02:05.727427+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T06:17:08.164092+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T06:32:04.336644+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T06:47:08.004420+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T07:02:05.120746+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T07:17:05.647473+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T07:32:01.392138+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T07:47:01.666820+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T08:02:03.470785+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T08:31:58.765660+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T10:17:01.321795+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T12:01:57.048658+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T15:17:05.187833+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T15:32:03.494960+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T15:47:03.966183+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T16:02:08.393449+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T16:17:04.961643+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T16:32:07.906681+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T16:47:06.887564+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T17:02:06.990948+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T18:02:04.071706+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T18:32:05.013636+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T00:01:37.202493+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T01:01:27.022847+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T01:16:27.094115+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T04:31:26.976344+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T06:16:26.327723+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T06:31:24.587899+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T06:46:27.517903+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T07:01:26.908449+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T07:16:24.967227+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T07:31:21.683307+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T07:46:19.343980+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T08:01:19.342405+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T08:16:18.724209+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T09:31:17.414283+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T09:46:18.170203+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T10:01:18.539805+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T10:16:18.307980+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T10:31:19.164363+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T10:46:19.104227+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T11:16:18.209654+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T11:31:16.324483+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T11:46:17.515591+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T15:46:18.998119+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T16:01:21.931831+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T16:16:24.878420+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T16:31:28.169775+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T16:46:26.848151+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T17:01:27.977881+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T17:16:26.719117+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T17:31:26.921538+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T17:46:26.350830+00:00.zip
|
||||||
|
GRAMMONT_2026-01-06T21:31:27.184279+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T00:01:39.500750+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T04:46:27.068804+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T05:31:26.143011+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T05:46:25.853849+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T06:01:25.057912+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T06:16:25.744052+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T06:31:24.481263+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T06:46:24.932963+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T07:01:21.846587+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T07:16:21.265797+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T07:31:18.457424+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T07:46:20.250231+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T12:31:19.009888+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T15:01:19.953447+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T15:46:19.735032+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T16:31:19.052209+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T16:46:21.632499+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T17:01:25.223457+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T17:16:26.740464+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T17:31:25.908204+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T17:46:26.495323+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T18:01:26.530360+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T21:01:27.495845+00:00.zip
|
||||||
|
GRAMMONT_2026-02-12T22:31:26.172360+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T00:02:00.235433+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T01:16:49.553391+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T03:16:48.829809+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T05:01:49.656932+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T05:16:48.603914+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T05:31:48.072998+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T05:46:48.743481+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T06:01:46.583618+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T06:16:45.405628+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T06:31:43.415579+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T06:46:41.697932+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T07:01:41.696734+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T12:16:42.701851+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T15:31:41.098916+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T16:01:39.517501+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T16:46:42.561834+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T17:01:41.958941+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T17:16:46.511805+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T17:31:48.499316+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T17:46:48.055562+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T18:01:47.851974+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T18:16:48.002798+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T18:31:48.973226+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T21:16:48.406731+00:00.zip
|
||||||
146
config/annotator_C.txt
Normal file
146
config/annotator_C.txt
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
GRAMMONT_2025-11-22T00:02:20.203920+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T01:02:09.028313+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T01:17:07.434190+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T04:02:07.766345+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T05:32:06.253178+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T05:47:05.799143+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T06:02:04.909260+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T06:17:06.901461+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T06:31:15.263893+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T06:47:02.531210+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T07:02:01.143506+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T07:16:59.906656+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T10:47:02.705611+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T14:47:00.096714+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T15:32:01.015469+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T15:47:02.337459+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T16:02:04.420357+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T16:17:04.468696+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T16:32:07.616206+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T16:47:04.224377+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T17:02:08.264697+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T17:17:06.243128+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T18:32:05.405485+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T00:02:16.601750+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T01:02:06.959165+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T01:17:05.555142+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T04:02:05.356836+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T06:02:09.414012+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T06:17:06.397863+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T06:32:06.114230+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T06:47:07.413290+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T07:02:05.817199+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T07:17:05.572966+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T07:32:07.077808+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T07:47:02.952666+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T08:02:01.468729+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T09:32:00.091785+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T10:47:03.611372+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T15:01:24.078486+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T15:16:25.132035+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T15:31:27.076379+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T15:46:29.057871+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T16:01:29.207153+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T16:16:28.383856+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T16:31:29.989159+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T16:46:27.655685+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T17:01:26.768672+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T18:01:28.364073+00:00.zip
|
||||||
|
GRAMMONT_2025-12-12T18:31:28.885962+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T00:01:36.564462+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T01:01:25.835453+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T05:31:25.879211+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T06:01:27.118806+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T06:16:26.590606+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T06:31:11.284211+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T06:46:28.122544+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T07:01:26.812905+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T07:16:21.745008+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T07:31:20.991098+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T07:46:18.360296+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T08:01:18.000617+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T09:31:17.177515+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T09:46:16.591998+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T10:01:17.593665+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T10:16:17.565584+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T10:46:18.688218+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T15:46:20.163005+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T16:01:19.900623+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T16:16:25.483667+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T16:31:26.102407+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T16:46:27.893362+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T17:01:26.885062+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T17:16:26.349277+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T18:31:27.062101+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T18:46:27.409224+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T19:16:27.442506+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T19:31:27.148138+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T19:46:27.178939+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T20:01:26.118404+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T20:16:26.814555+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T20:31:26.777464+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T21:01:28.269021+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T21:16:26.280773+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T22:01:25.272933+00:00.zip
|
||||||
|
GRAMMONT_2026-01-07T22:46:25.828656+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T00:01:49.506731+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T02:01:38.207676+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T05:31:38.138921+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T05:46:39.421285+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T06:01:41.482751+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T06:16:38.604849+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T06:31:38.007478+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T06:46:38.680991+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T07:01:36.215933+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T07:16:32.902087+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T07:31:32.860647+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T07:46:32.874607+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T08:31:30.794666+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T09:16:29.781662+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T09:31:32.218357+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T09:46:28.532516+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T10:01:29.027530+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T10:16:30.952271+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T10:31:30.118460+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T10:46:30.728758+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T11:01:29.613915+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T11:16:27.770965+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T11:31:28.625626+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T11:46:27.599574+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T12:01:28.213671+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T12:16:28.302803+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T12:31:28.683376+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T12:46:29.904051+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T13:01:28.903040+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T13:16:31.554237+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T14:01:29.428221+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T14:16:28.134941+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T14:31:28.642884+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T14:46:31.050308+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T15:01:36.440533+00:00.zip
|
||||||
|
GRAMMONT_2026-02-16T18:31:37.721767+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T00:02:00.115993+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T01:16:47.545369+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T03:16:47.017876+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T05:01:48.172478+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T05:16:48.425996+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T05:31:47.535047+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T05:46:49.230361+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T06:01:48.032458+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T06:16:43.444057+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T06:31:43.572406+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T06:46:42.094886+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T07:01:40.412911+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T10:46:41.016664+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T12:16:43.885012+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T15:46:40.689643+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T16:16:40.142917+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T16:46:42.950814+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T17:01:42.774269+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T17:16:45.482827+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T17:31:48.702675+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T17:46:48.793191+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T18:01:48.301590+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T18:16:47.700676+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T18:31:48.737829+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T21:16:48.709909+00:00.zip
|
||||||
102
config/annotator_D.txt
Normal file
102
config/annotator_D.txt
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
GRAMMONT_2025-11-18T00:02:16.622295+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T01:02:05.553357+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T01:17:10.391972+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T04:02:09.847002+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T05:47:07.350141+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T06:02:06.960352+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T06:17:11.438801+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T06:32:10.134718+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T06:47:05.176210+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T07:02:05.016401+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T07:17:04.505510+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T07:32:02.052621+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T10:47:04.410566+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T14:47:02.368668+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T15:32:07.687051+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T15:47:04.708858+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T16:02:08.642632+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T16:17:08.155540+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T16:32:08.782723+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T16:47:09.340365+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T17:02:04.224910+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T17:17:06.866509+00:00.zip
|
||||||
|
GRAMMONT_2025-11-18T18:32:07.533041+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T00:02:17.220289+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T01:17:06.672762+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T05:32:04.261510+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T05:47:09.391030+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T06:02:08.824582+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T06:17:07.446905+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T06:32:00.494092+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T06:47:07.344360+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T07:02:04.529073+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T07:17:05.544853+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T07:32:01.891046+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T07:47:00.405018+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T10:01:59.266718+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T14:47:01.734141+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T15:02:01.452351+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T15:17:01.907373+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T15:32:04.746603+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T15:47:05.257663+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T16:02:03.851786+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T16:17:08.370715+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T16:32:06.312360+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T18:02:05.411421+00:00.zip
|
||||||
|
GRAMMONT_2025-11-24T19:02:04.658144+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T00:01:41.461226+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T01:16:28.567684+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T06:31:29.022557+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T06:46:29.500063+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T07:01:31.526934+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T07:16:30.177120+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T07:31:27.010355+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T07:46:26.162426+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T08:16:25.350003+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T10:46:24.286299+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T11:16:24.959519+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T15:01:24.622331+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T15:16:25.305782+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T15:31:26.923661+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T15:46:30.091662+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T16:01:30.164684+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T16:16:26.912740+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T16:31:27.805273+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T16:46:28.631751+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T20:16:27.452196+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T21:01:28.663624+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T23:01:30.036410+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T00:01:38.571453+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T01:01:25.664393+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T02:31:25.943832+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T08:16:17.392212+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T08:46:16.720207+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T09:31:15.801732+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T18:16:24.859999+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T19:16:25.609118+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T20:01:24.809889+00:00.zip
|
||||||
|
GRAMMONT_2026-01-08T23:31:24.989446+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T00:02:00.235433+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T01:16:49.553391+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T03:16:48.829809+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T05:01:49.656932+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T05:16:48.603914+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T05:31:48.072998+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T05:46:48.743481+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T06:01:46.583618+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T06:16:45.405628+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T06:31:43.415579+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T06:46:41.697932+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T07:01:41.696734+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T12:16:42.701851+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T15:31:41.098916+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T16:01:39.517501+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T16:46:42.561834+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T17:01:41.958941+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T17:16:46.511805+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T17:31:48.499316+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T17:46:48.055562+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T18:01:47.851974+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T18:16:48.002798+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T18:31:48.973226+00:00.zip
|
||||||
|
GRAMMONT_2026-03-02T21:16:48.406731+00:00.zip
|
||||||
80
config/annotator_E.txt
Normal file
80
config/annotator_E.txt
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
GRAMMONT_2025-11-25T00:02:17.876729+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T01:02:05.646001+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T01:17:09.894834+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T04:02:04.433980+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T05:47:07.666696+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T06:02:11.511745+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T06:17:04.055068+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T06:32:09.079202+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T06:46:29.266484+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T07:02:05.006324+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T07:17:04.431043+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T07:32:00.875367+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T08:32:01.921526+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T14:02:00.055854+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T15:02:00.674240+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T15:32:04.667759+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T15:47:03.201793+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T16:02:06.850816+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T16:17:04.360406+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T16:32:09.562770+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T16:47:04.872758+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T17:02:08.046657+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T09:32:01.515556+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T11:17:03.118822+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T15:17:03.043807+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T15:32:04.085232+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T15:47:04.864411+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T16:02:09.554901+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T16:17:07.984380+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T16:32:06.042132+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T16:47:05.425453+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T17:02:05.867456+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T19:17:08.208705+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T19:47:07.924056+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T20:17:04.802945+00:00.zip
|
||||||
|
GRAMMONT_2025-12-03T23:17:04.368047+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T00:01:37.412545+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T05:31:25.188144+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T07:31:19.934715+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T07:46:17.007279+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T08:16:17.338747+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T10:31:15.867851+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T18:16:25.202124+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T19:16:26.688542+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T20:01:22.748919+00:00.zip
|
||||||
|
GRAMMONT_2026-01-09T23:31:26.962588+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T01:01:26.080207+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T04:01:26.663453+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T08:16:16.249789+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T09:31:16.196442+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T18:16:26.514693+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T18:31:25.430227+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T19:16:26.610193+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T20:01:26.503739+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T23:31:26.591525+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T00:02:00.115993+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T01:16:47.545369+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T03:16:47.017876+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T05:01:48.172478+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T05:16:48.425996+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T05:31:47.535047+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T05:46:49.230361+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T06:01:48.032458+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T06:16:43.444057+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T06:31:43.572406+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T06:46:42.094886+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T07:01:40.412911+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T10:46:41.016664+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T12:16:43.885012+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T15:46:40.689643+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T16:16:40.142917+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T16:46:42.950814+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T17:01:42.774269+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T17:16:45.482827+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T17:31:48.702675+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T17:46:48.793191+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T18:01:48.301590+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T18:16:47.700676+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T18:31:48.737829+00:00.zip
|
||||||
|
GRAMMONT_2026-03-03T21:16:48.709909+00:00.zip
|
||||||
93
config/annotator_F.txt
Normal file
93
config/annotator_F.txt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
GRAMMONT_2025-11-25T00:02:17.876729+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T01:02:05.646001+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T01:17:09.894834+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T04:02:04.433980+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T05:47:07.666696+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T06:02:11.511745+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T06:17:04.055068+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T06:32:09.079202+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T06:46:29.266484+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T07:02:05.006324+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T07:17:04.431043+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T07:32:00.875367+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T08:32:01.921526+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T14:02:00.055854+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T15:02:00.674240+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T15:32:04.667759+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T15:47:03.201793+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T16:02:06.850816+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T16:17:04.360406+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T16:32:09.562770+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T16:47:04.872758+00:00.zip
|
||||||
|
GRAMMONT_2025-11-25T17:02:08.046657+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T00:01:41.461226+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T01:16:28.567684+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T06:31:29.022557+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T06:46:29.500063+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T07:01:31.526934+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T07:16:30.177120+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T07:31:27.010355+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T07:46:26.162426+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T08:16:25.350003+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T10:46:24.286299+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T11:16:24.959519+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T15:01:24.622331+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T15:16:25.305782+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T15:31:26.923661+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T15:46:30.091662+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T16:01:30.164684+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T16:16:26.912740+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T16:31:27.805273+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T16:46:28.631751+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T20:16:27.452196+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T21:01:28.663624+00:00.zip
|
||||||
|
GRAMMONT_2025-12-16T23:01:30.036410+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T00:01:36.641915+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T01:01:28.284170+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T04:31:29.222060+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T08:16:15.986472+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T09:31:17.407612+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T18:16:28.897049+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T19:16:30.647866+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T20:01:30.348871+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T21:31:29.784010+00:00.zip
|
||||||
|
GRAMMONT_2026-01-10T23:31:28.564388+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T00:01:38.910412+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T01:16:26.259757+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T13:31:19.055589+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T15:01:18.349499+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T15:46:18.863149+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T16:01:19.328714+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T16:16:24.076563+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T16:31:26.963254+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T16:46:27.052033+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T17:01:27.425383+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T17:16:27.033257+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T17:31:26.566384+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T20:16:26.283507+00:00.zip
|
||||||
|
GRAMMONT_2026-01-13T23:16:26.953217+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T00:02:00.572803+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T01:16:49.764464+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T03:16:48.638128+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T04:46:48.636321+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T05:01:50.264589+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T05:16:49.061370+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T05:31:50.354757+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T05:46:47.683406+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T06:01:45.221459+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T06:16:44.072001+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T06:31:42.765049+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T06:46:42.012177+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T11:16:40.882550+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T12:01:41.385581+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T14:16:41.864888+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T15:46:40.760608+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T16:46:44.235006+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T17:01:44.107636+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T17:16:47.593339+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T17:31:49.616627+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T17:46:49.957863+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T18:01:48.458727+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T18:16:49.530207+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T18:31:48.654295+00:00.zip
|
||||||
|
GRAMMONT_2026-03-11T23:01:49.104435+00:00.zip
|
||||||
110
config/annotator_G.txt
Normal file
110
config/annotator_G.txt
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
GRAMMONT_2025-11-22T00:02:20.203920+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T01:02:09.028313+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T01:17:07.434190+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T04:02:07.766345+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T05:32:06.253178+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T05:47:05.799143+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T06:02:04.909260+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T06:17:06.901461+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T06:31:15.263893+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T06:47:02.531210+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T07:02:01.143506+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T07:16:59.906656+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T10:47:02.705611+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T14:47:00.096714+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T15:32:01.015469+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T15:47:02.337459+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T16:02:04.420357+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T16:17:04.468696+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T16:32:07.616206+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T16:47:04.224377+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T17:02:08.264697+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T17:17:06.243128+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T18:32:05.405485+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T00:02:14.666551+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T01:02:05.794759+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T01:17:05.892742+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T04:02:05.727427+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T06:17:08.164092+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T06:32:04.336644+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T06:47:08.004420+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T07:02:05.120746+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T07:17:05.647473+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T07:32:01.392138+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T07:47:01.666820+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T08:02:03.470785+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T08:31:58.765660+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T10:17:01.321795+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T12:01:57.048658+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T15:17:05.187833+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T15:32:03.494960+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T15:47:03.966183+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T16:02:08.393449+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T16:17:04.961643+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T16:32:07.906681+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T16:47:06.887564+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T17:02:06.990948+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T18:02:04.071706+00:00.zip
|
||||||
|
GRAMMONT_2025-12-05T18:32:05.013636+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T01:01:26.080207+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T04:01:26.663453+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T08:16:16.249789+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T09:31:16.196442+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T18:16:26.514693+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T18:31:25.430227+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T19:16:26.610193+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T20:01:26.503739+00:00.zip
|
||||||
|
GRAMMONT_2026-01-12T23:31:26.591525+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T00:01:38.048234+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T01:01:25.496751+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T04:31:25.749592+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T04:46:26.124855+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T05:31:27.076000+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T05:46:26.426312+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T06:01:26.452689+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T06:16:26.767560+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T06:31:24.624797+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T06:46:23.455019+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T07:01:21.080045+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T07:16:20.533988+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T07:31:19.697932+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T08:01:19.085138+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T09:16:17.805079+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T09:31:17.747859+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T12:31:20.771768+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T15:46:19.819366+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T16:31:20.659090+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T16:46:19.780502+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T17:01:25.098410+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T17:16:25.979320+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T17:31:27.522773+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T17:46:26.400124+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T18:01:25.365899+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T18:16:25.442042+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T21:01:25.956306+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T21:31:26.462852+00:00.zip
|
||||||
|
GRAMMONT_2026-02-11T22:31:26.629631+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T00:02:00.756259+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T01:16:49.289447+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T05:01:48.035876+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T05:16:48.883482+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T05:31:49.793476+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T05:46:49.641480+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T06:01:46.587244+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T06:16:45.028717+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T06:31:43.245102+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T06:46:42.052574+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T10:46:43.935297+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T12:31:42.025147+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T15:46:41.830340+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T16:16:40.453007+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T16:46:42.917700+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T17:01:43.627300+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T17:16:45.055691+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T17:31:48.034432+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T17:46:31.239038+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T18:01:49.569079+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T18:16:47.537836+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T18:31:49.125861+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T18:46:48.535395+00:00.zip
|
||||||
|
GRAMMONT_2026-03-12T22:01:49.336660+00:00.zip
|
||||||
10
config/clips.example.txt
Normal file
10
config/clips.example.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# List the clip filenames (without path) to annotate, one per line.
|
||||||
|
# Lines starting with # are ignored. Order is preserved.
|
||||||
|
GRAMMONT_2025-11-17T11:31:38.546953+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T12:31:39.650554+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T15:32:07.184007+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T15:32:07.184007+00:00.zip
|
||||||
|
GRAMMONT_2025-11-17T15:47:10.070449+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T10:47:02.705611+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T14:47:00.096714+00:00.zip
|
||||||
|
GRAMMONT_2025-11-22T15:32:01.015469+00:00.zip
|
||||||
21
config/config.example.yaml
Normal file
21
config/config.example.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
storage: local # 'local' or 's3'
|
||||||
|
|
||||||
|
# Required: set these to your actual paths (local path or bucket/prefix for S3)
|
||||||
|
data_dir: # e.g. /data/clips or for S3: hydroscan-data/GRAMMONT/clips
|
||||||
|
out_dir: # e.g. /data/out or for S3: hydroscan-data/annotations/<name>/ # Put your name here
|
||||||
|
# For S3 credentials, copy .env.example to .env and fill in:
|
||||||
|
# S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY, S3_ENDPOINT_URL
|
||||||
|
|
||||||
|
clips_file: config/clips.txt
|
||||||
|
optical_flow_config_file: config/optical_flow_config.yaml
|
||||||
|
questions_config_file: config/questions.yaml
|
||||||
|
|
||||||
|
display_max: 720
|
||||||
|
fps_fallback: 25
|
||||||
|
max_frames: 100
|
||||||
|
|
||||||
|
# Input filenames (override if your ZIP archives differ)
|
||||||
|
filenames:
|
||||||
|
video_in_zip: left.mp4
|
||||||
|
video_tmp_suffix: .mp4
|
||||||
|
zip_extension: .zip
|
||||||
4
config/optical_flow_config.yaml
Normal file
4
config/optical_flow_config.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
enabled: true
|
||||||
|
norm_squared_threshold: 0.06
|
||||||
|
gaussian_kernel: [5, 5]
|
||||||
|
brightness_range: [2, 253]
|
||||||
34
config/questions.yaml
Normal file
34
config/questions.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
- section: River
|
||||||
|
items:
|
||||||
|
- key: flow
|
||||||
|
label: Flow Regime
|
||||||
|
options: [Turbulent, Laminar, Uncertain]
|
||||||
|
default: Laminar
|
||||||
|
- key: shadows
|
||||||
|
label: Strong Shadows
|
||||||
|
options: [Yes, No, Uncertain]
|
||||||
|
default: No
|
||||||
|
- key: artifacts
|
||||||
|
label: Artifacts on River
|
||||||
|
options: [Yes, No, Uncertain]
|
||||||
|
default: No
|
||||||
|
- section: Scene
|
||||||
|
items:
|
||||||
|
- key: lighting
|
||||||
|
label: Lighting
|
||||||
|
options: [Day, Night, Uncertain]
|
||||||
|
default: Day
|
||||||
|
- key: exposure
|
||||||
|
label: Exposure
|
||||||
|
options: [Overexposed, Underexposed, Both, Normal, Uncertain]
|
||||||
|
default: Normal
|
||||||
|
- section: Weather
|
||||||
|
items:
|
||||||
|
- key: snowing
|
||||||
|
label: Snowing
|
||||||
|
options: [Yes, No, Uncertain]
|
||||||
|
default: No
|
||||||
|
- key: snow_on_ground
|
||||||
|
label: Snow on Ground
|
||||||
|
options: [Yes, No, Uncertain]
|
||||||
|
default: No
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
{
|
|
||||||
"cells": [
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"id": "fe0521db",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"from pathlib import Path\n",
|
|
||||||
"import json\n",
|
|
||||||
"import numpy as np\n",
|
|
||||||
"import matplotlib.pyplot as plt\n",
|
|
||||||
"from PIL import Image\n",
|
|
||||||
"from IPython.display import display, Image as IPImage"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"id": "c6d7ebbf",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"out_dir = Path(\"../data/annotation_results/\")"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"id": "75efc15a",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"def show_result(folder):\n",
|
|
||||||
" folder = Path(folder)\n",
|
|
||||||
"\n",
|
|
||||||
" with open(folder / \"metadata.json\") as f:\n",
|
|
||||||
" metadata = json.load(f)\n",
|
|
||||||
"\n",
|
|
||||||
" frame = np.array(Image.open(folder / \"frame.png\"))\n",
|
|
||||||
" mask = np.array(Image.open(folder / \"mask_vis.png\"))\n",
|
|
||||||
" overlay = np.array(Image.open(folder / \"overlay.png\"))\n",
|
|
||||||
"\n",
|
|
||||||
" title = \" | \".join(f\"{k}: {v}\" for k, v in metadata.items())\n",
|
|
||||||
" fig, axs = plt.subplots(1, 3, figsize=(15, 5))\n",
|
|
||||||
" axs[0].imshow(frame)\n",
|
|
||||||
" axs[0].set_title(\"Frame\")\n",
|
|
||||||
" axs[0].axis(\"off\")\n",
|
|
||||||
" axs[1].imshow(mask, cmap=\"gray\")\n",
|
|
||||||
" axs[1].set_title(\"Mask\")\n",
|
|
||||||
" axs[1].axis(\"off\")\n",
|
|
||||||
" axs[2].imshow(overlay)\n",
|
|
||||||
" axs[2].set_title(\"Overlay\")\n",
|
|
||||||
" axs[2].axis(\"off\")\n",
|
|
||||||
" plt.suptitle(f\"{folder.name}\\n{title}\", fontsize=9)\n",
|
|
||||||
" plt.tight_layout()\n",
|
|
||||||
" plt.show()\n",
|
|
||||||
"\n",
|
|
||||||
" for gif_name in [\"video_original_lowres.gif\", \"video_overlay_lowres.gif\"]:\n",
|
|
||||||
" gif_path = folder / gif_name\n",
|
|
||||||
" if gif_path.exists():\n",
|
|
||||||
" display(IPImage(filename=str(gif_path)))"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"id": "02ff1ae9",
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"for folder in sorted(out_dir.iterdir()):\n",
|
|
||||||
" if folder.is_dir() and (folder / \"metadata.json\").exists():\n",
|
|
||||||
" show_result(folder)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"kernelspec": {
|
|
||||||
"display_name": "river-annotation-tool",
|
|
||||||
"language": "python",
|
|
||||||
"name": "python3"
|
|
||||||
},
|
|
||||||
"language_info": {
|
|
||||||
"codemirror_mode": {
|
|
||||||
"name": "ipython",
|
|
||||||
"version": 3
|
|
||||||
},
|
|
||||||
"file_extension": ".py",
|
|
||||||
"mimetype": "text/x-python",
|
|
||||||
"name": "python",
|
|
||||||
"nbconvert_exporter": "python",
|
|
||||||
"pygments_lexer": "ipython3",
|
|
||||||
"version": "3.12.13"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nbformat": 4,
|
|
||||||
"nbformat_minor": 5
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0.0"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "river_annotation_tool"
|
name = "clip_annotator"
|
||||||
authors = [
|
authors = [
|
||||||
# TODO configure authors
|
# TODO configure authors
|
||||||
# { name = "Jane Smith", email = "jane.smith@example.com" },
|
# { name = "Jane Smith", email = "jane.smith@example.com" },
|
||||||
@@ -19,6 +19,9 @@ dependencies = [
|
|||||||
"matplotlib-inline>=0.2.1",
|
"matplotlib-inline>=0.2.1",
|
||||||
"pillow>=12.2.0",
|
"pillow>=12.2.0",
|
||||||
"pyside6>=6.11.0",
|
"pyside6>=6.11.0",
|
||||||
|
"python-dotenv>=1.0",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"s3fs>=2024.0",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
||||||
@@ -30,7 +33,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "river_annotation_tool.__version__"}
|
version = {attr = "clip_annotator.__version__"}
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py312"
|
target-version = "py312"
|
||||||
|
|||||||
972
requirements.txt
972
requirements.txt
@@ -1 +1,971 @@
|
|||||||
# This file will be autogenerated from pyproject.toml
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv export --frozen --output-file=requirements.txt
|
||||||
|
-e .
|
||||||
|
aiobotocore==3.7.0 \
|
||||||
|
--hash=sha256:680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e \
|
||||||
|
--hash=sha256:c64d871ed5491a6571948dd48eabd185b46c6c23b64e3afd0c059fc7593ada30
|
||||||
|
# via s3fs
|
||||||
|
aiohappyeyeballs==2.6.1 \
|
||||||
|
--hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \
|
||||||
|
--hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8
|
||||||
|
# via aiohttp
|
||||||
|
aiohttp==3.13.5 \
|
||||||
|
--hash=sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9 \
|
||||||
|
--hash=sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b \
|
||||||
|
--hash=sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1 \
|
||||||
|
--hash=sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416 \
|
||||||
|
--hash=sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe \
|
||||||
|
--hash=sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9 \
|
||||||
|
--hash=sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286 \
|
||||||
|
--hash=sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9 \
|
||||||
|
--hash=sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88 \
|
||||||
|
--hash=sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14 \
|
||||||
|
--hash=sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3 \
|
||||||
|
--hash=sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1 \
|
||||||
|
--hash=sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4 \
|
||||||
|
--hash=sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2 \
|
||||||
|
--hash=sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1 \
|
||||||
|
--hash=sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e \
|
||||||
|
--hash=sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5 \
|
||||||
|
--hash=sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3
|
||||||
|
# via
|
||||||
|
# aiobotocore
|
||||||
|
# s3fs
|
||||||
|
aioitertools==0.13.0 \
|
||||||
|
--hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \
|
||||||
|
--hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c
|
||||||
|
# via aiobotocore
|
||||||
|
aiosignal==1.4.0 \
|
||||||
|
--hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \
|
||||||
|
--hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7
|
||||||
|
# via aiohttp
|
||||||
|
anyio==4.13.0 \
|
||||||
|
--hash=sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 \
|
||||||
|
--hash=sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc
|
||||||
|
# via
|
||||||
|
# httpx
|
||||||
|
# jupyter-server
|
||||||
|
appnope==0.1.4 ; sys_platform == 'darwin' \
|
||||||
|
--hash=sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee \
|
||||||
|
--hash=sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c
|
||||||
|
# via ipykernel
|
||||||
|
argon2-cffi==25.1.0 \
|
||||||
|
--hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \
|
||||||
|
--hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741
|
||||||
|
# via jupyter-server
|
||||||
|
argon2-cffi-bindings==25.1.0 \
|
||||||
|
--hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \
|
||||||
|
--hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \
|
||||||
|
--hash=sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44 \
|
||||||
|
--hash=sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2 \
|
||||||
|
--hash=sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 \
|
||||||
|
--hash=sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98 \
|
||||||
|
--hash=sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500 \
|
||||||
|
--hash=sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94 \
|
||||||
|
--hash=sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d \
|
||||||
|
--hash=sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d \
|
||||||
|
--hash=sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a
|
||||||
|
# via argon2-cffi
|
||||||
|
arrow==1.4.0 \
|
||||||
|
--hash=sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205 \
|
||||||
|
--hash=sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7
|
||||||
|
# via isoduration
|
||||||
|
asttokens==3.0.1 \
|
||||||
|
--hash=sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a \
|
||||||
|
--hash=sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7
|
||||||
|
# via stack-data
|
||||||
|
async-lru==2.3.0 \
|
||||||
|
--hash=sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6 \
|
||||||
|
--hash=sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315
|
||||||
|
# via jupyterlab
|
||||||
|
attrs==26.1.0 \
|
||||||
|
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
|
||||||
|
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# jsonschema
|
||||||
|
# referencing
|
||||||
|
babel==2.18.0 \
|
||||||
|
--hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \
|
||||||
|
--hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35
|
||||||
|
# via jupyterlab-server
|
||||||
|
beautifulsoup4==4.14.3 \
|
||||||
|
--hash=sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb \
|
||||||
|
--hash=sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86
|
||||||
|
# via nbconvert
|
||||||
|
bleach==6.3.0 \
|
||||||
|
--hash=sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22 \
|
||||||
|
--hash=sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6
|
||||||
|
# via nbconvert
|
||||||
|
botocore==1.43.0 \
|
||||||
|
--hash=sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6 \
|
||||||
|
--hash=sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928
|
||||||
|
# via aiobotocore
|
||||||
|
certifi==2026.4.22 \
|
||||||
|
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \
|
||||||
|
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
cffi==2.0.0 \
|
||||||
|
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
|
||||||
|
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
|
||||||
|
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
|
||||||
|
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
|
||||||
|
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
|
||||||
|
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
|
||||||
|
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
|
||||||
|
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
|
||||||
|
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
|
||||||
|
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
|
||||||
|
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
|
||||||
|
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
|
||||||
|
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18
|
||||||
|
# via
|
||||||
|
# argon2-cffi-bindings
|
||||||
|
# pyzmq
|
||||||
|
cfgv==3.5.0 \
|
||||||
|
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
|
||||||
|
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
|
||||||
|
# via pre-commit
|
||||||
|
charset-normalizer==3.4.7 \
|
||||||
|
--hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \
|
||||||
|
--hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \
|
||||||
|
--hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \
|
||||||
|
--hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \
|
||||||
|
--hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \
|
||||||
|
--hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \
|
||||||
|
--hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \
|
||||||
|
--hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \
|
||||||
|
--hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \
|
||||||
|
--hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \
|
||||||
|
--hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \
|
||||||
|
--hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \
|
||||||
|
--hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \
|
||||||
|
--hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \
|
||||||
|
--hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \
|
||||||
|
--hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \
|
||||||
|
--hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \
|
||||||
|
--hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464
|
||||||
|
# via requests
|
||||||
|
colorama==0.4.6 ; sys_platform == 'win32' \
|
||||||
|
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||||
|
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||||
|
# via ipython
|
||||||
|
comm==0.2.3 \
|
||||||
|
--hash=sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971 \
|
||||||
|
--hash=sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417
|
||||||
|
# via ipykernel
|
||||||
|
contourpy==1.3.3 \
|
||||||
|
--hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \
|
||||||
|
--hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \
|
||||||
|
--hash=sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880 \
|
||||||
|
--hash=sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7 \
|
||||||
|
--hash=sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411 \
|
||||||
|
--hash=sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 \
|
||||||
|
--hash=sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 \
|
||||||
|
--hash=sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea \
|
||||||
|
--hash=sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b \
|
||||||
|
--hash=sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 \
|
||||||
|
--hash=sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb \
|
||||||
|
--hash=sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8
|
||||||
|
# via matplotlib
|
||||||
|
cycler==0.12.1 \
|
||||||
|
--hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \
|
||||||
|
--hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c
|
||||||
|
# via matplotlib
|
||||||
|
debugpy==1.8.20 \
|
||||||
|
--hash=sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390 \
|
||||||
|
--hash=sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d \
|
||||||
|
--hash=sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33 \
|
||||||
|
--hash=sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7 \
|
||||||
|
--hash=sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b \
|
||||||
|
--hash=sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3
|
||||||
|
# via ipykernel
|
||||||
|
decorator==5.2.1 \
|
||||||
|
--hash=sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360 \
|
||||||
|
--hash=sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a
|
||||||
|
# via ipython
|
||||||
|
defusedxml==0.7.1 \
|
||||||
|
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
|
||||||
|
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
|
||||||
|
# via nbconvert
|
||||||
|
distlib==0.4.0 \
|
||||||
|
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||||
|
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||||
|
# via virtualenv
|
||||||
|
executing==2.2.1 \
|
||||||
|
--hash=sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4 \
|
||||||
|
--hash=sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017
|
||||||
|
# via stack-data
|
||||||
|
fastjsonschema==2.21.2 \
|
||||||
|
--hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \
|
||||||
|
--hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de
|
||||||
|
# via nbformat
|
||||||
|
filelock==3.29.0 \
|
||||||
|
--hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \
|
||||||
|
--hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258
|
||||||
|
# via
|
||||||
|
# python-discovery
|
||||||
|
# virtualenv
|
||||||
|
fonttools==4.62.1 \
|
||||||
|
--hash=sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04 \
|
||||||
|
--hash=sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9 \
|
||||||
|
--hash=sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 \
|
||||||
|
--hash=sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d \
|
||||||
|
--hash=sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd \
|
||||||
|
--hash=sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 \
|
||||||
|
--hash=sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936 \
|
||||||
|
--hash=sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 \
|
||||||
|
--hash=sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c \
|
||||||
|
--hash=sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d
|
||||||
|
# via matplotlib
|
||||||
|
fqdn==1.5.1 \
|
||||||
|
--hash=sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f \
|
||||||
|
--hash=sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014
|
||||||
|
# via jsonschema
|
||||||
|
frozenlist==1.8.0 \
|
||||||
|
--hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \
|
||||||
|
--hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \
|
||||||
|
--hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \
|
||||||
|
--hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \
|
||||||
|
--hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \
|
||||||
|
--hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \
|
||||||
|
--hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \
|
||||||
|
--hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \
|
||||||
|
--hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \
|
||||||
|
--hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \
|
||||||
|
--hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \
|
||||||
|
--hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \
|
||||||
|
--hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \
|
||||||
|
--hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \
|
||||||
|
--hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \
|
||||||
|
--hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \
|
||||||
|
--hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \
|
||||||
|
--hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# aiosignal
|
||||||
|
fsspec==2026.4.0 \
|
||||||
|
--hash=sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2 \
|
||||||
|
--hash=sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4
|
||||||
|
# via s3fs
|
||||||
|
h11==0.16.0 \
|
||||||
|
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
||||||
|
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
|
||||||
|
# via httpcore
|
||||||
|
httpcore==1.0.9 \
|
||||||
|
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
|
||||||
|
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
|
||||||
|
# via httpx
|
||||||
|
httpx==0.28.1 \
|
||||||
|
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
|
||||||
|
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
|
||||||
|
# via jupyterlab
|
||||||
|
identify==2.6.19 \
|
||||||
|
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
|
||||||
|
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
|
||||||
|
# via pre-commit
|
||||||
|
idna==3.13 \
|
||||||
|
--hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \
|
||||||
|
--hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# httpx
|
||||||
|
# jsonschema
|
||||||
|
# requests
|
||||||
|
# yarl
|
||||||
|
ipykernel==7.2.0 \
|
||||||
|
--hash=sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e \
|
||||||
|
--hash=sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661
|
||||||
|
# via jupyterlab
|
||||||
|
ipython==9.13.0 \
|
||||||
|
--hash=sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201 \
|
||||||
|
--hash=sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967
|
||||||
|
# via ipykernel
|
||||||
|
ipython-pygments-lexers==1.1.1 \
|
||||||
|
--hash=sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81 \
|
||||||
|
--hash=sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c
|
||||||
|
# via ipython
|
||||||
|
isoduration==20.11.0 \
|
||||||
|
--hash=sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9 \
|
||||||
|
--hash=sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042
|
||||||
|
# via jsonschema
|
||||||
|
jedi==0.19.2 \
|
||||||
|
--hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \
|
||||||
|
--hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9
|
||||||
|
# via ipython
|
||||||
|
jinja2==3.1.6 \
|
||||||
|
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
|
||||||
|
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
|
||||||
|
# via
|
||||||
|
# jupyter-server
|
||||||
|
# jupyterlab
|
||||||
|
# jupyterlab-server
|
||||||
|
# nbconvert
|
||||||
|
jmespath==1.1.0 \
|
||||||
|
--hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \
|
||||||
|
--hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64
|
||||||
|
# via
|
||||||
|
# aiobotocore
|
||||||
|
# botocore
|
||||||
|
json5==0.14.0 \
|
||||||
|
--hash=sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a \
|
||||||
|
--hash=sha256:b3f492fad9f6cdbced8b7d40b28b9b1c9701c5f561bef0d33b81c2ff433fefcb
|
||||||
|
# via jupyterlab-server
|
||||||
|
jsonpointer==3.1.1 \
|
||||||
|
--hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \
|
||||||
|
--hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca
|
||||||
|
# via jsonschema
|
||||||
|
jsonschema==4.26.0 \
|
||||||
|
--hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \
|
||||||
|
--hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
|
||||||
|
# via
|
||||||
|
# jupyter-events
|
||||||
|
# jupyterlab-server
|
||||||
|
# nbformat
|
||||||
|
jsonschema-specifications==2025.9.1 \
|
||||||
|
--hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \
|
||||||
|
--hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d
|
||||||
|
# via jsonschema
|
||||||
|
jupyter-client==8.8.0 \
|
||||||
|
--hash=sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e \
|
||||||
|
--hash=sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a
|
||||||
|
# via
|
||||||
|
# ipykernel
|
||||||
|
# jupyter-server
|
||||||
|
# nbclient
|
||||||
|
jupyter-core==5.9.1 \
|
||||||
|
--hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \
|
||||||
|
--hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407
|
||||||
|
# via
|
||||||
|
# ipykernel
|
||||||
|
# jupyter-client
|
||||||
|
# jupyter-server
|
||||||
|
# jupyterlab
|
||||||
|
# nbclient
|
||||||
|
# nbconvert
|
||||||
|
# nbformat
|
||||||
|
jupyter-events==0.12.1 \
|
||||||
|
--hash=sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf \
|
||||||
|
--hash=sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3
|
||||||
|
# via jupyter-server
|
||||||
|
jupyter-lsp==2.3.1 \
|
||||||
|
--hash=sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81 \
|
||||||
|
--hash=sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6
|
||||||
|
# via jupyterlab
|
||||||
|
jupyter-server==2.17.0 \
|
||||||
|
--hash=sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5 \
|
||||||
|
--hash=sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f
|
||||||
|
# via
|
||||||
|
# jupyter-lsp
|
||||||
|
# jupyterlab
|
||||||
|
# jupyterlab-server
|
||||||
|
# notebook
|
||||||
|
# notebook-shim
|
||||||
|
jupyter-server-terminals==0.5.4 \
|
||||||
|
--hash=sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14 \
|
||||||
|
--hash=sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5
|
||||||
|
# via jupyter-server
|
||||||
|
jupyterlab==4.5.7 \
|
||||||
|
--hash=sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0 \
|
||||||
|
--hash=sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d
|
||||||
|
# via notebook
|
||||||
|
jupyterlab-pygments==0.3.0 \
|
||||||
|
--hash=sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d \
|
||||||
|
--hash=sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780
|
||||||
|
# via nbconvert
|
||||||
|
jupyterlab-server==2.28.0 \
|
||||||
|
--hash=sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c \
|
||||||
|
--hash=sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968
|
||||||
|
# via
|
||||||
|
# jupyterlab
|
||||||
|
# notebook
|
||||||
|
kiwisolver==1.5.0 \
|
||||||
|
--hash=sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15 \
|
||||||
|
--hash=sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9 \
|
||||||
|
--hash=sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57 \
|
||||||
|
--hash=sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9 \
|
||||||
|
--hash=sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314 \
|
||||||
|
--hash=sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797 \
|
||||||
|
--hash=sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083 \
|
||||||
|
--hash=sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588 \
|
||||||
|
--hash=sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1 \
|
||||||
|
--hash=sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d \
|
||||||
|
--hash=sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf \
|
||||||
|
--hash=sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f \
|
||||||
|
--hash=sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7 \
|
||||||
|
--hash=sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6 \
|
||||||
|
--hash=sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a \
|
||||||
|
--hash=sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0 \
|
||||||
|
--hash=sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 \
|
||||||
|
--hash=sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 \
|
||||||
|
--hash=sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203 \
|
||||||
|
--hash=sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7
|
||||||
|
# via matplotlib
|
||||||
|
lark==1.3.1 \
|
||||||
|
--hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \
|
||||||
|
--hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12
|
||||||
|
# via rfc3987-syntax
|
||||||
|
markupsafe==3.0.3 \
|
||||||
|
--hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \
|
||||||
|
--hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \
|
||||||
|
--hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \
|
||||||
|
--hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \
|
||||||
|
--hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
|
||||||
|
--hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \
|
||||||
|
--hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \
|
||||||
|
--hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \
|
||||||
|
--hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \
|
||||||
|
--hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \
|
||||||
|
--hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \
|
||||||
|
--hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d
|
||||||
|
# via
|
||||||
|
# jinja2
|
||||||
|
# nbconvert
|
||||||
|
matplotlib==3.10.9 \
|
||||||
|
--hash=sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42 \
|
||||||
|
--hash=sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320 \
|
||||||
|
--hash=sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2 \
|
||||||
|
--hash=sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285 \
|
||||||
|
--hash=sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6 \
|
||||||
|
--hash=sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf \
|
||||||
|
--hash=sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1 \
|
||||||
|
--hash=sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358
|
||||||
|
# via clip-annotator
|
||||||
|
matplotlib-inline==0.2.1 \
|
||||||
|
--hash=sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76 \
|
||||||
|
--hash=sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe
|
||||||
|
# via
|
||||||
|
# clip-annotator
|
||||||
|
# ipykernel
|
||||||
|
# ipython
|
||||||
|
mistune==3.2.0 \
|
||||||
|
--hash=sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a \
|
||||||
|
--hash=sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1
|
||||||
|
# via nbconvert
|
||||||
|
multidict==6.7.1 \
|
||||||
|
--hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \
|
||||||
|
--hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \
|
||||||
|
--hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \
|
||||||
|
--hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \
|
||||||
|
--hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \
|
||||||
|
--hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \
|
||||||
|
--hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \
|
||||||
|
--hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \
|
||||||
|
--hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \
|
||||||
|
--hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \
|
||||||
|
--hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \
|
||||||
|
--hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \
|
||||||
|
--hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \
|
||||||
|
--hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \
|
||||||
|
--hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \
|
||||||
|
--hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \
|
||||||
|
--hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \
|
||||||
|
--hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \
|
||||||
|
--hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \
|
||||||
|
--hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19
|
||||||
|
# via
|
||||||
|
# aiobotocore
|
||||||
|
# aiohttp
|
||||||
|
# yarl
|
||||||
|
nbclient==0.10.4 \
|
||||||
|
--hash=sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9 \
|
||||||
|
--hash=sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440
|
||||||
|
# via nbconvert
|
||||||
|
nbconvert==7.17.1 \
|
||||||
|
--hash=sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2 \
|
||||||
|
--hash=sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8
|
||||||
|
# via jupyter-server
|
||||||
|
nbformat==5.10.4 \
|
||||||
|
--hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \
|
||||||
|
--hash=sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b
|
||||||
|
# via
|
||||||
|
# jupyter-server
|
||||||
|
# nbclient
|
||||||
|
# nbconvert
|
||||||
|
nest-asyncio==1.6.0 \
|
||||||
|
--hash=sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe \
|
||||||
|
--hash=sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c
|
||||||
|
# via ipykernel
|
||||||
|
nodeenv==1.10.0 \
|
||||||
|
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||||
|
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||||
|
# via pre-commit
|
||||||
|
notebook==7.5.6 \
|
||||||
|
--hash=sha256:4dde3f8fb55fa8fb7946d58c6e869ce9baf46d00fc070664f62604569d0faca0 \
|
||||||
|
--hash=sha256:621174aade80108f0020b0f00738000b215f75fa3cd90771ad7aa0f24536a4e1
|
||||||
|
notebook-shim==0.2.4 \
|
||||||
|
--hash=sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef \
|
||||||
|
--hash=sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb
|
||||||
|
# via
|
||||||
|
# jupyterlab
|
||||||
|
# notebook
|
||||||
|
numpy==2.2.6 \
|
||||||
|
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
|
||||||
|
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
|
||||||
|
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
|
||||||
|
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
|
||||||
|
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
|
||||||
|
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
|
||||||
|
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
|
||||||
|
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
|
||||||
|
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
|
||||||
|
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
|
||||||
|
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de
|
||||||
|
# via
|
||||||
|
# contourpy
|
||||||
|
# matplotlib
|
||||||
|
# opencv-contrib-python-headless
|
||||||
|
# pandas
|
||||||
|
opencv-contrib-python-headless==4.12.0.88 \
|
||||||
|
--hash=sha256:3d8a7b23a5faba4ad34e13f51668c56be791e57ab02d68d9016200fed3c12c77 \
|
||||||
|
--hash=sha256:85b520e527052a85a682f09cdc12e5f156f56d8c277261b4b65b48431abae96f \
|
||||||
|
--hash=sha256:902888b4e1b4826c721840d9107e91d32f146a2c3bc8cb728f0088bf44204e4b \
|
||||||
|
--hash=sha256:a17ebb914f309afe72447c33b9187ff02f23f1483faa5c0ffde7aadc88711e2a \
|
||||||
|
--hash=sha256:b183e2322468c9d3bd9cac4ba44b272d828ec22842395bcfa51df31765224c0a \
|
||||||
|
--hash=sha256:c57e32812fea2a542bb220088fb3ce8a210fe114c9454d1c9e8cd162e1a1fde8 \
|
||||||
|
--hash=sha256:d60a12b915c55a50468c013fcd839e941b49ccc1f37b914b62543382c36bf81d
|
||||||
|
# via clip-annotator
|
||||||
|
packaging==26.2 \
|
||||||
|
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||||
|
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||||
|
# via
|
||||||
|
# ipykernel
|
||||||
|
# jupyter-events
|
||||||
|
# jupyter-server
|
||||||
|
# jupyterlab
|
||||||
|
# jupyterlab-server
|
||||||
|
# matplotlib
|
||||||
|
# nbconvert
|
||||||
|
pandas==3.0.2 \
|
||||||
|
--hash=sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd \
|
||||||
|
--hash=sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18 \
|
||||||
|
--hash=sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d \
|
||||||
|
--hash=sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14 \
|
||||||
|
--hash=sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4 \
|
||||||
|
--hash=sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d \
|
||||||
|
--hash=sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f \
|
||||||
|
--hash=sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043 \
|
||||||
|
--hash=sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab
|
||||||
|
# via clip-annotator
|
||||||
|
pandocfilters==1.5.1 \
|
||||||
|
--hash=sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e \
|
||||||
|
--hash=sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc
|
||||||
|
# via nbconvert
|
||||||
|
parso==0.8.6 \
|
||||||
|
--hash=sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd \
|
||||||
|
--hash=sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff
|
||||||
|
# via jedi
|
||||||
|
pexpect==4.9.0 ; sys_platform != 'emscripten' and sys_platform != 'win32' \
|
||||||
|
--hash=sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 \
|
||||||
|
--hash=sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f
|
||||||
|
# via ipython
|
||||||
|
pillow==12.2.0 \
|
||||||
|
--hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \
|
||||||
|
--hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \
|
||||||
|
--hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \
|
||||||
|
--hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \
|
||||||
|
--hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \
|
||||||
|
--hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \
|
||||||
|
--hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \
|
||||||
|
--hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \
|
||||||
|
--hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \
|
||||||
|
--hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \
|
||||||
|
--hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \
|
||||||
|
--hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5
|
||||||
|
# via
|
||||||
|
# clip-annotator
|
||||||
|
# matplotlib
|
||||||
|
platformdirs==4.9.6 \
|
||||||
|
--hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \
|
||||||
|
--hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917
|
||||||
|
# via
|
||||||
|
# jupyter-core
|
||||||
|
# python-discovery
|
||||||
|
# virtualenv
|
||||||
|
pre-commit==4.6.0 \
|
||||||
|
--hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \
|
||||||
|
--hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b
|
||||||
|
prometheus-client==0.25.0 \
|
||||||
|
--hash=sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28 \
|
||||||
|
--hash=sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1
|
||||||
|
# via jupyter-server
|
||||||
|
prompt-toolkit==3.0.52 \
|
||||||
|
--hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \
|
||||||
|
--hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955
|
||||||
|
# via ipython
|
||||||
|
propcache==0.5.2 \
|
||||||
|
--hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \
|
||||||
|
--hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \
|
||||||
|
--hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \
|
||||||
|
--hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \
|
||||||
|
--hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \
|
||||||
|
--hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \
|
||||||
|
--hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \
|
||||||
|
--hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \
|
||||||
|
--hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \
|
||||||
|
--hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \
|
||||||
|
--hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \
|
||||||
|
--hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \
|
||||||
|
--hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \
|
||||||
|
--hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \
|
||||||
|
--hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \
|
||||||
|
--hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \
|
||||||
|
--hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \
|
||||||
|
--hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \
|
||||||
|
--hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# yarl
|
||||||
|
psutil==7.2.2 \
|
||||||
|
--hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \
|
||||||
|
--hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \
|
||||||
|
--hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \
|
||||||
|
--hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \
|
||||||
|
--hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \
|
||||||
|
--hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \
|
||||||
|
--hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \
|
||||||
|
--hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \
|
||||||
|
--hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8
|
||||||
|
# via
|
||||||
|
# ipykernel
|
||||||
|
# ipython
|
||||||
|
ptyprocess==0.7.0 ; os_name != 'nt' or (sys_platform != 'emscripten' and sys_platform != 'win32') \
|
||||||
|
--hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
|
||||||
|
--hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
|
||||||
|
# via
|
||||||
|
# pexpect
|
||||||
|
# terminado
|
||||||
|
pure-eval==0.2.3 \
|
||||||
|
--hash=sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 \
|
||||||
|
--hash=sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42
|
||||||
|
# via stack-data
|
||||||
|
pycparser==3.0 ; implementation_name != 'PyPy' \
|
||||||
|
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||||
|
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||||
|
# via cffi
|
||||||
|
pygments==2.20.0 \
|
||||||
|
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||||
|
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||||
|
# via
|
||||||
|
# ipython
|
||||||
|
# ipython-pygments-lexers
|
||||||
|
# nbconvert
|
||||||
|
pyparsing==3.3.2 \
|
||||||
|
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
|
||||||
|
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
|
||||||
|
# via matplotlib
|
||||||
|
pyside6==6.11.0 \
|
||||||
|
--hash=sha256:1f2735dc4f2bd4ec452ae50502c8a22128bba0aced35358a2bbc58384b820c6f \
|
||||||
|
--hash=sha256:267b344c73580ac938ca63c611881fb42a3922ebfe043e271005f4f06c372c4e \
|
||||||
|
--hash=sha256:9092cb002ca43c64006afb2e0d0f6f51aef17aa737c33a45e502326a081ddcbc \
|
||||||
|
--hash=sha256:b15f39acc2b8f46251a630acad0d97f9a0a0461f2baffcd66d7adfada8eb641e \
|
||||||
|
--hash=sha256:c642e2d25704ca746fd37f56feacf25c5aecc4cd40bef23d18eec81f87d9dc00
|
||||||
|
# via clip-annotator
|
||||||
|
pyside6-addons==6.11.0 \
|
||||||
|
--hash=sha256:413e6121c24f5ffdce376298059eddecff74aa6d638e94e0f6015b33d29b889e \
|
||||||
|
--hash=sha256:8ffb40222456078930816ebcac2f2511716d2acbc11716dd5acc5c365179a753 \
|
||||||
|
--hash=sha256:aaaee83385977a0fe134b2f4fbfb92b45a880d5b656e4d90a708eef10b1b6de8 \
|
||||||
|
--hash=sha256:ac6fe3d4ef4497dde3efc5e896b0acd53ff6c93be4bf485f045690f919419f35 \
|
||||||
|
--hash=sha256:d5eaa4643302e3a0fa94c5766234bee4073d7d5ab9c2b7fd222692a176faf182
|
||||||
|
# via pyside6
|
||||||
|
pyside6-essentials==6.11.0 \
|
||||||
|
--hash=sha256:3b3362882ad9389357a80504e600180006a957731fec05786fced7b038461fdf \
|
||||||
|
--hash=sha256:4854cb0a1b061e7a576d8fb7bb7cf9f49540d558b1acb7df0742a7afefe61e4e \
|
||||||
|
--hash=sha256:81ca603dbf21bc39f89bb42db215c25ebe0c879a1a4c387625c321d2730ec187 \
|
||||||
|
--hash=sha256:85d6ca87ef35fa6565d385ede72ae48420dd3f63113929d10fc800f6b0360e01 \
|
||||||
|
--hash=sha256:dc20e7afd5fc6fe51297db91cef997ce60844be578f7a49fc61b7ab9657a8849
|
||||||
|
# via
|
||||||
|
# pyside6
|
||||||
|
# pyside6-addons
|
||||||
|
python-dateutil==2.9.0.post0 \
|
||||||
|
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||||
|
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||||
|
# via
|
||||||
|
# aiobotocore
|
||||||
|
# arrow
|
||||||
|
# botocore
|
||||||
|
# jupyter-client
|
||||||
|
# matplotlib
|
||||||
|
# pandas
|
||||||
|
python-discovery==1.2.2 \
|
||||||
|
--hash=sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb \
|
||||||
|
--hash=sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a
|
||||||
|
# via virtualenv
|
||||||
|
python-dotenv==1.2.2 \
|
||||||
|
--hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \
|
||||||
|
--hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3
|
||||||
|
# via clip-annotator
|
||||||
|
python-json-logger==4.1.0 \
|
||||||
|
--hash=sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2 \
|
||||||
|
--hash=sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195
|
||||||
|
# via jupyter-events
|
||||||
|
pywinpty==3.0.3 ; os_name == 'nt' \
|
||||||
|
--hash=sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9 \
|
||||||
|
--hash=sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412 \
|
||||||
|
--hash=sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0
|
||||||
|
# via
|
||||||
|
# jupyter-server
|
||||||
|
# jupyter-server-terminals
|
||||||
|
# terminado
|
||||||
|
pyyaml==6.0.3 \
|
||||||
|
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||||
|
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||||
|
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||||
|
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||||
|
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||||
|
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||||
|
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||||
|
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||||
|
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||||
|
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||||
|
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||||
|
# via
|
||||||
|
# clip-annotator
|
||||||
|
# jupyter-events
|
||||||
|
# pre-commit
|
||||||
|
pyzmq==27.1.0 \
|
||||||
|
--hash=sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28 \
|
||||||
|
--hash=sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113 \
|
||||||
|
--hash=sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd \
|
||||||
|
--hash=sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233 \
|
||||||
|
--hash=sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31 \
|
||||||
|
--hash=sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc \
|
||||||
|
--hash=sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f \
|
||||||
|
--hash=sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf \
|
||||||
|
--hash=sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540 \
|
||||||
|
--hash=sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856 \
|
||||||
|
--hash=sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496
|
||||||
|
# via
|
||||||
|
# ipykernel
|
||||||
|
# jupyter-client
|
||||||
|
# jupyter-server
|
||||||
|
referencing==0.37.0 \
|
||||||
|
--hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \
|
||||||
|
--hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8
|
||||||
|
# via
|
||||||
|
# jsonschema
|
||||||
|
# jsonschema-specifications
|
||||||
|
# jupyter-events
|
||||||
|
requests==2.33.1 \
|
||||||
|
--hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \
|
||||||
|
--hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a
|
||||||
|
# via jupyterlab-server
|
||||||
|
rfc3339-validator==0.1.4 \
|
||||||
|
--hash=sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b \
|
||||||
|
--hash=sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa
|
||||||
|
# via
|
||||||
|
# jsonschema
|
||||||
|
# jupyter-events
|
||||||
|
rfc3986-validator==0.1.1 \
|
||||||
|
--hash=sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9 \
|
||||||
|
--hash=sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055
|
||||||
|
# via
|
||||||
|
# jsonschema
|
||||||
|
# jupyter-events
|
||||||
|
rfc3987-syntax==1.1.0 \
|
||||||
|
--hash=sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f \
|
||||||
|
--hash=sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d
|
||||||
|
# via jsonschema
|
||||||
|
rpds-py==0.30.0 \
|
||||||
|
--hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \
|
||||||
|
--hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \
|
||||||
|
--hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \
|
||||||
|
--hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \
|
||||||
|
--hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \
|
||||||
|
--hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \
|
||||||
|
--hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \
|
||||||
|
--hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \
|
||||||
|
--hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \
|
||||||
|
--hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \
|
||||||
|
--hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \
|
||||||
|
--hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \
|
||||||
|
--hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \
|
||||||
|
--hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \
|
||||||
|
--hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \
|
||||||
|
--hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f
|
||||||
|
# via
|
||||||
|
# jsonschema
|
||||||
|
# referencing
|
||||||
|
ruff==0.15.0 \
|
||||||
|
--hash=sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3 \
|
||||||
|
--hash=sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662 \
|
||||||
|
--hash=sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621 \
|
||||||
|
--hash=sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a \
|
||||||
|
--hash=sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3 \
|
||||||
|
--hash=sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179 \
|
||||||
|
--hash=sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d \
|
||||||
|
--hash=sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16 \
|
||||||
|
--hash=sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78 \
|
||||||
|
--hash=sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e \
|
||||||
|
--hash=sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9 \
|
||||||
|
--hash=sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455 \
|
||||||
|
--hash=sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1 \
|
||||||
|
--hash=sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4 \
|
||||||
|
--hash=sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a \
|
||||||
|
--hash=sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce \
|
||||||
|
--hash=sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d \
|
||||||
|
--hash=sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18
|
||||||
|
s3fs==2026.4.0 \
|
||||||
|
--hash=sha256:5bdce0abb00b0435ee150807a45fea727451dbc22de4cbc116464f8504ab9d37 \
|
||||||
|
--hash=sha256:de0d2a1f33cdf03831fd2382d278c6e4e31fe57c3bf2f703c61f8aec6b703e2a
|
||||||
|
# via clip-annotator
|
||||||
|
send2trash==2.1.0 \
|
||||||
|
--hash=sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c \
|
||||||
|
--hash=sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459
|
||||||
|
# via jupyter-server
|
||||||
|
setuptools==82.0.1 \
|
||||||
|
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
|
||||||
|
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
|
||||||
|
# via jupyterlab
|
||||||
|
shiboken6==6.11.0 \
|
||||||
|
--hash=sha256:3bd76cf56105ab2d62ecaff630366f11264f69b88d488f10f048da9a065781f4 \
|
||||||
|
--hash=sha256:483ff78a73c7b3189ca924abc694318084f078bcfeaffa68e32024ff2d025ee1 \
|
||||||
|
--hash=sha256:a10dc7718104ea2dc15d5b0b96909b77162ce1c76fcc6968e6df692b947a00e9 \
|
||||||
|
--hash=sha256:ad54e64f8192ddbdff0c54ac82b89edcd62ed623f502ea21c960541d19514053 \
|
||||||
|
--hash=sha256:d88e8a1eb705f2b9ad21db08a61ae1dc0c773e5cd86a069de0754c4cf1f9b43b
|
||||||
|
# via
|
||||||
|
# pyside6
|
||||||
|
# pyside6-addons
|
||||||
|
# pyside6-essentials
|
||||||
|
six==1.17.0 \
|
||||||
|
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||||
|
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||||
|
# via
|
||||||
|
# python-dateutil
|
||||||
|
# rfc3339-validator
|
||||||
|
soupsieve==2.8.3 \
|
||||||
|
--hash=sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349 \
|
||||||
|
--hash=sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95
|
||||||
|
# via beautifulsoup4
|
||||||
|
stack-data==0.6.3 \
|
||||||
|
--hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
|
||||||
|
--hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
|
||||||
|
# via ipython
|
||||||
|
terminado==0.18.1 \
|
||||||
|
--hash=sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 \
|
||||||
|
--hash=sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e
|
||||||
|
# via
|
||||||
|
# jupyter-server
|
||||||
|
# jupyter-server-terminals
|
||||||
|
tinycss2==1.4.0 \
|
||||||
|
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||||
|
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
|
||||||
|
# via bleach
|
||||||
|
tornado==6.5.5 \
|
||||||
|
--hash=sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9 \
|
||||||
|
--hash=sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6 \
|
||||||
|
--hash=sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca \
|
||||||
|
--hash=sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e \
|
||||||
|
--hash=sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07 \
|
||||||
|
--hash=sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa \
|
||||||
|
--hash=sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b \
|
||||||
|
--hash=sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521 \
|
||||||
|
--hash=sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7 \
|
||||||
|
--hash=sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5
|
||||||
|
# via
|
||||||
|
# ipykernel
|
||||||
|
# jupyter-client
|
||||||
|
# jupyter-server
|
||||||
|
# jupyterlab
|
||||||
|
# notebook
|
||||||
|
# terminado
|
||||||
|
traitlets==5.14.3 \
|
||||||
|
--hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \
|
||||||
|
--hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f
|
||||||
|
# via
|
||||||
|
# ipykernel
|
||||||
|
# ipython
|
||||||
|
# jupyter-client
|
||||||
|
# jupyter-core
|
||||||
|
# jupyter-events
|
||||||
|
# jupyter-server
|
||||||
|
# jupyterlab
|
||||||
|
# matplotlib-inline
|
||||||
|
# nbclient
|
||||||
|
# nbconvert
|
||||||
|
# nbformat
|
||||||
|
typing-extensions==4.15.0 \
|
||||||
|
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
|
||||||
|
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
||||||
|
# via
|
||||||
|
# aiosignal
|
||||||
|
# anyio
|
||||||
|
# beautifulsoup4
|
||||||
|
# referencing
|
||||||
|
tzdata==2026.2 \
|
||||||
|
--hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \
|
||||||
|
--hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7
|
||||||
|
# via
|
||||||
|
# arrow
|
||||||
|
# pandas
|
||||||
|
uri-template==1.3.0 \
|
||||||
|
--hash=sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7 \
|
||||||
|
--hash=sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363
|
||||||
|
# via jsonschema
|
||||||
|
urllib3==2.6.3 \
|
||||||
|
--hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \
|
||||||
|
--hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4
|
||||||
|
# via
|
||||||
|
# botocore
|
||||||
|
# requests
|
||||||
|
virtualenv==21.3.0 \
|
||||||
|
--hash=sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7 \
|
||||||
|
--hash=sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e
|
||||||
|
# via pre-commit
|
||||||
|
wcwidth==0.6.0 \
|
||||||
|
--hash=sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad \
|
||||||
|
--hash=sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159
|
||||||
|
# via prompt-toolkit
|
||||||
|
webcolors==25.10.0 \
|
||||||
|
--hash=sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d \
|
||||||
|
--hash=sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf
|
||||||
|
# via jsonschema
|
||||||
|
webencodings==0.5.1 \
|
||||||
|
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||||
|
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
|
||||||
|
# via
|
||||||
|
# bleach
|
||||||
|
# tinycss2
|
||||||
|
websocket-client==1.9.0 \
|
||||||
|
--hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \
|
||||||
|
--hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef
|
||||||
|
# via jupyter-server
|
||||||
|
wrapt==2.1.2 \
|
||||||
|
--hash=sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63 \
|
||||||
|
--hash=sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e \
|
||||||
|
--hash=sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748 \
|
||||||
|
--hash=sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1 \
|
||||||
|
--hash=sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8 \
|
||||||
|
--hash=sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2 \
|
||||||
|
--hash=sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8 \
|
||||||
|
--hash=sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e \
|
||||||
|
--hash=sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf \
|
||||||
|
--hash=sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c \
|
||||||
|
--hash=sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0 \
|
||||||
|
--hash=sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c \
|
||||||
|
--hash=sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9
|
||||||
|
# via aiobotocore
|
||||||
|
yarl==1.24.2 \
|
||||||
|
--hash=sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30 \
|
||||||
|
--hash=sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9 \
|
||||||
|
--hash=sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db \
|
||||||
|
--hash=sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2 \
|
||||||
|
--hash=sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d \
|
||||||
|
--hash=sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035 \
|
||||||
|
--hash=sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7 \
|
||||||
|
--hash=sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad \
|
||||||
|
--hash=sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c \
|
||||||
|
--hash=sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d \
|
||||||
|
--hash=sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996 \
|
||||||
|
--hash=sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8 \
|
||||||
|
--hash=sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b \
|
||||||
|
--hash=sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8 \
|
||||||
|
--hash=sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576 \
|
||||||
|
--hash=sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712 \
|
||||||
|
--hash=sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1 \
|
||||||
|
--hash=sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c \
|
||||||
|
--hash=sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8
|
||||||
|
# via aiohttp
|
||||||
|
|||||||
76
src/clip_annotator/__main__.py
Normal file
76
src/clip_annotator/__main__.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from matplotlib import use
|
||||||
|
|
||||||
|
|
||||||
|
use("QtAgg")
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
|
from .annotator import Annotator
|
||||||
|
from .config import load_config
|
||||||
|
from .filesystem import make_fs
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
default="config/config.yaml",
|
||||||
|
help="Path to config YAML file (default: config/config.yaml)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--data", default=None, help="Override data_dir from config")
|
||||||
|
parser.add_argument("--out", default=None, help="Override out_dir from config")
|
||||||
|
parser.add_argument("--clips", default=None, help="Override clips_file from config")
|
||||||
|
parser.add_argument(
|
||||||
|
"--clip", default=None, help="Stem name of a specific clip to load"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--extras",
|
||||||
|
action="store_true",
|
||||||
|
help="Also save GIFs, frame PNG, overlay PNG, and mask_vis PNG alongside the mask.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-skip",
|
||||||
|
action="store_true",
|
||||||
|
help="Show already-annotated clips instead of skipping them.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
cfg = load_config(Path(args.config))
|
||||||
|
if args.data:
|
||||||
|
cfg.data_dir = args.data
|
||||||
|
if args.out:
|
||||||
|
cfg.out_dir = args.out
|
||||||
|
if args.clips:
|
||||||
|
cfg.clips_file = args.clips
|
||||||
|
|
||||||
|
fs = make_fs(cfg.storage)
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
try:
|
||||||
|
win = Annotator(
|
||||||
|
cfg,
|
||||||
|
clip=args.clip,
|
||||||
|
extras=args.extras,
|
||||||
|
skip_annotated=not args.no_skip,
|
||||||
|
fs=fs,
|
||||||
|
)
|
||||||
|
except RuntimeError as e:
|
||||||
|
QMessageBox.information(None, "No clips", str(e))
|
||||||
|
sys.exit(0)
|
||||||
|
win.show()
|
||||||
|
app.exec()
|
||||||
506
src/clip_annotator/annotator.py
Normal file
506
src/clip_annotator/annotator.py
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
from PySide6.QtCore import Qt, QTimer
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QButtonGroup,
|
||||||
|
QGroupBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMainWindow,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QRadioButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .clip_selector import ClipSelector
|
||||||
|
from .compute_optical_flow import compute_optical_flow_mask
|
||||||
|
from .config import AppConfig, load_optical_flow_config
|
||||||
|
from .filesystem import fsjoin, fsname, fsstem
|
||||||
|
from .mask_canvas import MaskCanvas
|
||||||
|
from .video_loader import load_frames
|
||||||
|
|
||||||
|
|
||||||
|
class Annotator(QMainWindow):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: AppConfig,
|
||||||
|
clip: str = None,
|
||||||
|
extras: bool = False,
|
||||||
|
skip_annotated: bool = True,
|
||||||
|
fs=None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.cfg = config
|
||||||
|
self.fs = fs
|
||||||
|
self.out_dir = config.out_dir
|
||||||
|
self.extras = extras
|
||||||
|
self.of_cfg = load_optical_flow_config(Path(config.optical_flow_config_file))
|
||||||
|
|
||||||
|
self.selector = ClipSelector(
|
||||||
|
data_dir=config.data_dir,
|
||||||
|
out_dir=self.out_dir,
|
||||||
|
clips_file=Path(config.clips_file),
|
||||||
|
mask_filename=config.filenames.mask,
|
||||||
|
zip_extension=config.filenames.zip_extension,
|
||||||
|
skip_annotated=skip_annotated,
|
||||||
|
fs=fs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.history: list[str] = []
|
||||||
|
self.history_pos: int = -1
|
||||||
|
|
||||||
|
self.setWindowTitle("Clip Annotator")
|
||||||
|
self._load_clip(specific=clip)
|
||||||
|
self._history_push()
|
||||||
|
self._init_ui()
|
||||||
|
self._init_timer()
|
||||||
|
|
||||||
|
# ── filesystem helpers ─────────────────────────────────────────
|
||||||
|
def _out_path(self, *parts: str) -> str:
|
||||||
|
return fsjoin(self.out_dir, fsstem(self.filename), *parts)
|
||||||
|
|
||||||
|
def _fs_exists(self, path: str) -> bool:
|
||||||
|
if self.fs is None:
|
||||||
|
return Path(path).exists()
|
||||||
|
return self.fs.exists(path)
|
||||||
|
|
||||||
|
def _fs_makedirs(self, path: str):
|
||||||
|
if self.fs is None:
|
||||||
|
Path(path).mkdir(parents=True, exist_ok=True)
|
||||||
|
else:
|
||||||
|
self.fs.makedirs(path, exist_ok=True)
|
||||||
|
|
||||||
|
def _pil_open(self, path: str) -> Image.Image:
|
||||||
|
if self.fs is None:
|
||||||
|
return Image.open(path)
|
||||||
|
with self.fs.open(path, "rb") as f:
|
||||||
|
return Image.open(io.BytesIO(f.read()))
|
||||||
|
|
||||||
|
def _pil_save(self, img: Image.Image, path: str):
|
||||||
|
if self.fs is None:
|
||||||
|
img.save(path)
|
||||||
|
else:
|
||||||
|
ext = str(path).rsplit(".", 1)[-1].lower()
|
||||||
|
fmt = "JPEG" if ext in ("jpg", "jpeg") else ext.upper()
|
||||||
|
buf = io.BytesIO()
|
||||||
|
img.save(buf, format=fmt)
|
||||||
|
self.fs.pipe(path, buf.getvalue())
|
||||||
|
|
||||||
|
def _json_read(self, path: str):
|
||||||
|
if self.fs is None:
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
with self.fs.open(path, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def _json_write(self, data, path: str):
|
||||||
|
if self.fs is None:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
else:
|
||||||
|
self.fs.pipe(path, json.dumps(data, indent=2).encode())
|
||||||
|
|
||||||
|
# ── clip loading ───────────────────────────────────────────────
|
||||||
|
def _load_clip(self, specific: str = None, path: str = None):
|
||||||
|
if path is not None:
|
||||||
|
self.filename = path
|
||||||
|
else:
|
||||||
|
self.filename = self.selector.next(specific=specific)
|
||||||
|
self.frames, self.fps, self.dh, self.dw, self.h, self.w = load_frames(
|
||||||
|
self.filename,
|
||||||
|
self.cfg.max_frames,
|
||||||
|
self.cfg.display_max,
|
||||||
|
self.cfg.fps_fallback,
|
||||||
|
self.cfg.filenames.video_in_zip,
|
||||||
|
self.cfg.filenames.video_tmp_suffix,
|
||||||
|
fs=self.fs,
|
||||||
|
)
|
||||||
|
self._pending_answers = self._read_saved_answers()
|
||||||
|
|
||||||
|
def _history_push(self):
|
||||||
|
del self.history[self.history_pos + 1 :]
|
||||||
|
self.history.append(self.filename)
|
||||||
|
self.history_pos = len(self.history) - 1
|
||||||
|
|
||||||
|
def _read_saved_mask(self):
|
||||||
|
mask_path = self._out_path(self.cfg.filenames.mask)
|
||||||
|
if not self._fs_exists(mask_path):
|
||||||
|
return None
|
||||||
|
mask_full = np.array(self._pil_open(mask_path).convert("L"))
|
||||||
|
return cv2.resize(
|
||||||
|
(mask_full > 127).astype(np.uint8),
|
||||||
|
(self.dw, self.dh),
|
||||||
|
interpolation=cv2.INTER_NEAREST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _read_saved_answers(self):
|
||||||
|
meta_path = self._out_path(self.cfg.filenames.metadata)
|
||||||
|
if not self._fs_exists(meta_path):
|
||||||
|
return None
|
||||||
|
return self._json_read(meta_path)
|
||||||
|
|
||||||
|
# ── UI setup ───────────────────────────────────────────────────
|
||||||
|
def _init_ui(self):
|
||||||
|
self.mc = MaskCanvas(self.frames, self.dh, self.dw)
|
||||||
|
self.mc.set_title(fsname(self.filename))
|
||||||
|
self.mc.reset(self._read_saved_mask())
|
||||||
|
|
||||||
|
self.q_widgets = {}
|
||||||
|
question_panel = self._build_question_panel()
|
||||||
|
|
||||||
|
self.btn_prev = QPushButton("Previous")
|
||||||
|
self.btn_prev.setEnabled(False)
|
||||||
|
self.btn_next = QPushButton("Next")
|
||||||
|
btn_skip = QPushButton("Skip")
|
||||||
|
btn_clear = QPushButton("Clear")
|
||||||
|
btn_undo = QPushButton("Undo")
|
||||||
|
btn_undo10 = QPushButton("Undo×10")
|
||||||
|
btn_redo = QPushButton("Redo")
|
||||||
|
btn_load_prev_mask = QPushButton("Load Prev Mask")
|
||||||
|
btn_auto_segment = QPushButton("Auto Segment")
|
||||||
|
btn_auto_segment.setEnabled(self.of_cfg.enabled)
|
||||||
|
|
||||||
|
row1 = QHBoxLayout()
|
||||||
|
for b in [
|
||||||
|
self.btn_prev,
|
||||||
|
self.btn_next,
|
||||||
|
btn_skip,
|
||||||
|
btn_load_prev_mask,
|
||||||
|
btn_auto_segment,
|
||||||
|
]:
|
||||||
|
row1.addWidget(b)
|
||||||
|
|
||||||
|
row_tools = QHBoxLayout()
|
||||||
|
for b in [
|
||||||
|
self.mc.btn_brush,
|
||||||
|
self.mc.btn_polygon,
|
||||||
|
self.mc.btn_fill,
|
||||||
|
self.mc.btn_del_shape,
|
||||||
|
self.mc.btn_cancel_poly,
|
||||||
|
]:
|
||||||
|
row_tools.addWidget(b)
|
||||||
|
|
||||||
|
row2 = QHBoxLayout()
|
||||||
|
for b in [
|
||||||
|
btn_clear,
|
||||||
|
self.mc.btn_erase,
|
||||||
|
btn_undo,
|
||||||
|
btn_undo10,
|
||||||
|
btn_redo,
|
||||||
|
self.mc.btn_mask,
|
||||||
|
]:
|
||||||
|
row2.addWidget(b)
|
||||||
|
|
||||||
|
row3 = QHBoxLayout()
|
||||||
|
row3.addWidget(QLabel("Brush size"))
|
||||||
|
row3.addWidget(self.mc.brush_slider)
|
||||||
|
row3.addWidget(self.mc.brush_reset)
|
||||||
|
|
||||||
|
row4 = QHBoxLayout()
|
||||||
|
row4.addWidget(QLabel("Mask Alpha"))
|
||||||
|
row4.addWidget(self.mc.alpha_slider)
|
||||||
|
row4.addWidget(self.mc.alpha_reset)
|
||||||
|
|
||||||
|
vert_panel = QHBoxLayout()
|
||||||
|
vert_panel.setContentsMargins(0, 0, 4, 0)
|
||||||
|
for label_text, slider, reset_btn in [
|
||||||
|
("Brightness", self.mc.brightness_slider, self.mc.brightness_reset),
|
||||||
|
("Contrast", self.mc.contrast_slider, self.mc.contrast_reset),
|
||||||
|
("Gamma", self.mc.gamma_slider, self.mc.gamma_reset),
|
||||||
|
]:
|
||||||
|
col = QVBoxLayout()
|
||||||
|
lbl = QLabel(label_text)
|
||||||
|
lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
col.addWidget(lbl)
|
||||||
|
col.addWidget(slider, 1)
|
||||||
|
col.addWidget(reset_btn)
|
||||||
|
vert_panel.addLayout(col)
|
||||||
|
|
||||||
|
canvas_row = QHBoxLayout()
|
||||||
|
canvas_row.addLayout(vert_panel)
|
||||||
|
canvas_row.addWidget(self.mc.canvas, 1)
|
||||||
|
|
||||||
|
left = QVBoxLayout()
|
||||||
|
left.addLayout(canvas_row)
|
||||||
|
left.addLayout(row1)
|
||||||
|
left.addLayout(row_tools)
|
||||||
|
left.addLayout(row2)
|
||||||
|
left.addLayout(row3)
|
||||||
|
left.addLayout(row4)
|
||||||
|
|
||||||
|
left_widget = QWidget()
|
||||||
|
left_widget.setLayout(left)
|
||||||
|
right_widget = QWidget()
|
||||||
|
right_widget.setLayout(question_panel)
|
||||||
|
|
||||||
|
main = QHBoxLayout()
|
||||||
|
main.addWidget(left_widget, 3)
|
||||||
|
main.addWidget(right_widget, 1)
|
||||||
|
|
||||||
|
container = QWidget()
|
||||||
|
container.setLayout(main)
|
||||||
|
self.setCentralWidget(container)
|
||||||
|
|
||||||
|
self.btn_prev.clicked.connect(self.prev_clip)
|
||||||
|
self.btn_next.clicked.connect(self.next_clip)
|
||||||
|
btn_skip.clicked.connect(self.skip_clip)
|
||||||
|
btn_clear.clicked.connect(self.mc.clear)
|
||||||
|
btn_undo.clicked.connect(self.mc.undo)
|
||||||
|
btn_undo10.clicked.connect(self.mc.undo10)
|
||||||
|
btn_redo.clicked.connect(self.mc.redo)
|
||||||
|
btn_load_prev_mask.clicked.connect(self.load_prev_mask)
|
||||||
|
btn_auto_segment.clicked.connect(self.run_optical_flow)
|
||||||
|
|
||||||
|
if self._pending_answers:
|
||||||
|
self._set_answers(self._pending_answers)
|
||||||
|
self._pending_answers = None
|
||||||
|
|
||||||
|
def _build_question_panel(self) -> QVBoxLayout:
|
||||||
|
vbox = QVBoxLayout()
|
||||||
|
for section, qs in self.cfg.get_questions():
|
||||||
|
group = QGroupBox(section)
|
||||||
|
gvbox = QVBoxLayout()
|
||||||
|
for key, label, options, default in qs:
|
||||||
|
gvbox.addWidget(QLabel(label))
|
||||||
|
btn_group = QButtonGroup(self)
|
||||||
|
row = QHBoxLayout()
|
||||||
|
buttons = []
|
||||||
|
for opt in options:
|
||||||
|
btn = QRadioButton(opt)
|
||||||
|
btn_group.addButton(btn)
|
||||||
|
row.addWidget(btn)
|
||||||
|
buttons.append(btn)
|
||||||
|
if default == opt:
|
||||||
|
btn.setChecked(True)
|
||||||
|
if default is None and buttons:
|
||||||
|
buttons[-1].setChecked(True)
|
||||||
|
self.q_widgets[key] = (btn_group, buttons, options)
|
||||||
|
gvbox.addLayout(row)
|
||||||
|
group.setLayout(gvbox)
|
||||||
|
vbox.addWidget(group)
|
||||||
|
return vbox
|
||||||
|
|
||||||
|
def _set_answers(self, answers: dict):
|
||||||
|
for key, value in answers.items():
|
||||||
|
if key not in self.q_widgets:
|
||||||
|
continue
|
||||||
|
_, buttons, options = self.q_widgets[key]
|
||||||
|
for i, btn in enumerate(buttons):
|
||||||
|
btn.setChecked(options[i] == value)
|
||||||
|
|
||||||
|
def _init_timer(self):
|
||||||
|
self.frame_i = 0
|
||||||
|
self.timer = QTimer()
|
||||||
|
self.timer.timeout.connect(self._tick)
|
||||||
|
self.timer.start(int(1000 / self.fps))
|
||||||
|
|
||||||
|
def _tick(self):
|
||||||
|
self.frame_i = (self.frame_i + 1) % len(self.frames)
|
||||||
|
self.mc.set_frame(self.frames[self.frame_i])
|
||||||
|
|
||||||
|
# ── answers ────────────────────────────────────────────────────
|
||||||
|
def get_answers(self) -> dict:
|
||||||
|
out = {}
|
||||||
|
for key, (_, buttons, options) in self.q_widgets.items():
|
||||||
|
for i, btn in enumerate(buttons):
|
||||||
|
if btn.isChecked():
|
||||||
|
out[key] = options[i]
|
||||||
|
return out
|
||||||
|
|
||||||
|
# ── save helpers ───────────────────────────────────────────────
|
||||||
|
def _make_overlay(self, frame, alpha=0.4):
|
||||||
|
overlay = frame.copy()
|
||||||
|
green = np.zeros_like(frame)
|
||||||
|
green[..., 1] = 255
|
||||||
|
m = self.mc.mask.astype(bool)
|
||||||
|
overlay[m] = (1 - alpha) * overlay[m] + alpha * green[m]
|
||||||
|
return overlay.astype(np.uint8)
|
||||||
|
|
||||||
|
def _save_gif(self, frames, out_path: str, scale=1.0):
|
||||||
|
h, w = frames[0].shape[:2]
|
||||||
|
nh, nw = max(1, int(h * scale)), max(1, int(w * scale))
|
||||||
|
pil_frames = [Image.fromarray(cv2.resize(f, (nw, nh))) for f in frames]
|
||||||
|
if self.fs is None:
|
||||||
|
pil_frames[0].save(
|
||||||
|
out_path,
|
||||||
|
save_all=True,
|
||||||
|
append_images=pil_frames[1:],
|
||||||
|
duration=int(1000 / self.fps),
|
||||||
|
loop=0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
buf = io.BytesIO()
|
||||||
|
pil_frames[0].save(
|
||||||
|
buf,
|
||||||
|
format="GIF",
|
||||||
|
save_all=True,
|
||||||
|
append_images=pil_frames[1:],
|
||||||
|
duration=int(1000 / self.fps),
|
||||||
|
loop=0,
|
||||||
|
)
|
||||||
|
self.fs.pipe(out_path, buf.getvalue())
|
||||||
|
|
||||||
|
# ── actions ────────────────────────────────────────────────────
|
||||||
|
def _save_locked(self):
|
||||||
|
self.btn_next.setEnabled(False)
|
||||||
|
self.btn_prev.setEnabled(False)
|
||||||
|
QApplication.processEvents()
|
||||||
|
try:
|
||||||
|
self.save()
|
||||||
|
finally:
|
||||||
|
self.btn_next.setEnabled(True)
|
||||||
|
self.btn_prev.setEnabled(self.history_pos > 0)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
out = fsjoin(self.out_dir, fsstem(self.filename))
|
||||||
|
self._fs_makedirs(out)
|
||||||
|
|
||||||
|
mask_full = cv2.resize(
|
||||||
|
self.mc.mask.astype(np.uint8),
|
||||||
|
(self.w, self.h),
|
||||||
|
interpolation=cv2.INTER_NEAREST,
|
||||||
|
)
|
||||||
|
fn = self.cfg.filenames
|
||||||
|
self._pil_save(Image.fromarray(mask_full * 255), fsjoin(out, fn.mask))
|
||||||
|
self._json_write(self.get_answers(), fsjoin(out, fn.metadata))
|
||||||
|
|
||||||
|
mid = len(self.frames) // 2
|
||||||
|
frame = self.frames[mid]
|
||||||
|
self._pil_save(Image.fromarray(frame), fsjoin(out, fn.frame))
|
||||||
|
self._pil_save(
|
||||||
|
Image.fromarray(self._make_overlay(frame)), fsjoin(out, fn.overlay)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.extras:
|
||||||
|
self._pil_save(
|
||||||
|
Image.fromarray((self.mc.mask * 255).astype(np.uint8)),
|
||||||
|
fsjoin(out, fn.mask_vis),
|
||||||
|
)
|
||||||
|
overlay_frames = [self._make_overlay(f) for f in self.frames]
|
||||||
|
self._save_gif(self.frames, fsjoin(out, fn.gif_original_hires), scale=1.0)
|
||||||
|
self._save_gif(self.frames, fsjoin(out, fn.gif_original_lowres), scale=0.5)
|
||||||
|
self._save_gif(overlay_frames, fsjoin(out, fn.gif_overlay_hires), scale=1.0)
|
||||||
|
self._save_gif(
|
||||||
|
overlay_frames, fsjoin(out, fn.gif_overlay_lowres), scale=0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Saved:", out)
|
||||||
|
|
||||||
|
def _switch_ui_to_clip(self):
|
||||||
|
self.frame_i = 0
|
||||||
|
self.mc.load_clip(
|
||||||
|
self.frames,
|
||||||
|
self.dh,
|
||||||
|
self.dw,
|
||||||
|
mask=self._read_saved_mask(),
|
||||||
|
title=fsname(self.filename),
|
||||||
|
)
|
||||||
|
if self._pending_answers:
|
||||||
|
self._set_answers(self._pending_answers)
|
||||||
|
self._pending_answers = None
|
||||||
|
self.btn_prev.setEnabled(self.history_pos > 0)
|
||||||
|
|
||||||
|
def _advance_clip(self):
|
||||||
|
if self.history_pos < len(self.history) - 1:
|
||||||
|
self.history_pos += 1
|
||||||
|
self._load_clip(path=self.history[self.history_pos])
|
||||||
|
self._switch_ui_to_clip()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._load_clip()
|
||||||
|
except RuntimeError:
|
||||||
|
msg = QMessageBox(self)
|
||||||
|
msg.setWindowTitle("All done!")
|
||||||
|
msg.setText("You have reached the end of all clips.")
|
||||||
|
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||||
|
msg.exec()
|
||||||
|
QApplication.instance().quit()
|
||||||
|
return
|
||||||
|
self._history_push()
|
||||||
|
self._switch_ui_to_clip()
|
||||||
|
|
||||||
|
def prev_clip(self):
|
||||||
|
if self.history_pos <= 0:
|
||||||
|
return
|
||||||
|
self._save_locked()
|
||||||
|
self.history_pos -= 1
|
||||||
|
self._load_clip(path=self.history[self.history_pos])
|
||||||
|
self._switch_ui_to_clip()
|
||||||
|
|
||||||
|
def next_clip(self):
|
||||||
|
mask_path = self._out_path(self.cfg.filenames.mask)
|
||||||
|
if self._fs_exists(mask_path):
|
||||||
|
msg = QMessageBox(self)
|
||||||
|
msg.setWindowTitle("Existing annotation found")
|
||||||
|
msg.setText(
|
||||||
|
f"'{fsstem(self.filename)}' already has a saved annotation.\n"
|
||||||
|
"Replace it with your current work, or keep the existing save?"
|
||||||
|
)
|
||||||
|
btn_replace = msg.addButton(
|
||||||
|
"Replace & Continue", QMessageBox.ButtonRole.AcceptRole
|
||||||
|
)
|
||||||
|
btn_keep = msg.addButton(
|
||||||
|
"Keep Existing & Continue", QMessageBox.ButtonRole.AcceptRole
|
||||||
|
)
|
||||||
|
msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
||||||
|
msg.setDefaultButton(btn_replace)
|
||||||
|
msg.exec()
|
||||||
|
clicked = msg.clickedButton()
|
||||||
|
if clicked == btn_replace:
|
||||||
|
self._save_locked()
|
||||||
|
self._advance_clip()
|
||||||
|
elif clicked == btn_keep:
|
||||||
|
self._advance_clip()
|
||||||
|
# Cancel: do nothing
|
||||||
|
else:
|
||||||
|
self._save_locked()
|
||||||
|
self._advance_clip()
|
||||||
|
|
||||||
|
def skip_clip(self):
|
||||||
|
self._advance_clip()
|
||||||
|
|
||||||
|
def load_prev_mask(self):
|
||||||
|
try:
|
||||||
|
idx = self.selector.clips.index(self.filename)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
if idx == 0:
|
||||||
|
QMessageBox.information(
|
||||||
|
self, "No previous clip", "This is the first clip in the list."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
prev_clip = self.selector.clips[idx - 1]
|
||||||
|
mask_path = fsjoin(self.out_dir, fsstem(prev_clip), self.cfg.filenames.mask)
|
||||||
|
if not self._fs_exists(mask_path):
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"No mask found",
|
||||||
|
f"No saved mask found for '{fsstem(prev_clip)}'.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
mask_full = np.array(self._pil_open(mask_path).convert("L"))
|
||||||
|
mask = cv2.resize(
|
||||||
|
(mask_full > 127).astype(np.uint8),
|
||||||
|
(self.dw, self.dh),
|
||||||
|
interpolation=cv2.INTER_NEAREST,
|
||||||
|
)
|
||||||
|
self.mc.set_mask(mask)
|
||||||
|
|
||||||
|
def run_optical_flow(self):
|
||||||
|
mask = compute_optical_flow_mask(
|
||||||
|
self.frames,
|
||||||
|
self.fps,
|
||||||
|
self.of_cfg.norm_squared_threshold,
|
||||||
|
self.of_cfg.gaussian_kernel,
|
||||||
|
self.of_cfg.brightness_range,
|
||||||
|
)
|
||||||
|
self.mc.set_mask(mask)
|
||||||
75
src/clip_annotator/clip_selector.py
Normal file
75
src/clip_annotator/clip_selector.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .filesystem import fsjoin, fsstem
|
||||||
|
|
||||||
|
|
||||||
|
class ClipSelector:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data_dir,
|
||||||
|
out_dir,
|
||||||
|
clips_file: Path,
|
||||||
|
mask_filename: str = "mask.png",
|
||||||
|
zip_extension: str = ".zip",
|
||||||
|
skip_annotated: bool = True,
|
||||||
|
fs=None,
|
||||||
|
):
|
||||||
|
self.data_dir = str(data_dir)
|
||||||
|
self.out_dir = str(out_dir)
|
||||||
|
self.mask_filename = mask_filename
|
||||||
|
self.zip_extension = zip_extension
|
||||||
|
self.skip_annotated = skip_annotated
|
||||||
|
self.fs = fs
|
||||||
|
self.clips = self._load_clips(clips_file)
|
||||||
|
self.index = 0
|
||||||
|
|
||||||
|
def _load_clips(self, clips_file: Path) -> list:
|
||||||
|
lines = clips_file.read_text().splitlines()
|
||||||
|
return [
|
||||||
|
fsjoin(self.data_dir, name.strip())
|
||||||
|
for name in lines
|
||||||
|
if name.strip() and not name.strip().startswith("#")
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_annotated(self, path) -> bool:
|
||||||
|
mask_path = fsjoin(self.out_dir, fsstem(path), self.mask_filename)
|
||||||
|
if self.fs is None:
|
||||||
|
return Path(mask_path).exists()
|
||||||
|
return self.fs.exists(mask_path)
|
||||||
|
|
||||||
|
def next(self, specific: str = None) -> str:
|
||||||
|
if specific:
|
||||||
|
return self._resolve_specific(specific)
|
||||||
|
return self._pick_next()
|
||||||
|
|
||||||
|
def _resolve_specific(self, specific: str) -> str:
|
||||||
|
if self.fs is None:
|
||||||
|
data_dir = Path(self.data_dir)
|
||||||
|
matches = list(data_dir.glob(f"{specific}{self.zip_extension}"))
|
||||||
|
if not matches:
|
||||||
|
p = data_dir / specific
|
||||||
|
matches = [p] if p.exists() else []
|
||||||
|
if not matches:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Clip '{specific}' not found in {self.data_dir}"
|
||||||
|
)
|
||||||
|
return str(matches[0])
|
||||||
|
else:
|
||||||
|
pattern = fsjoin(self.data_dir, f"{specific}{self.zip_extension}")
|
||||||
|
matches = self.fs.glob(pattern)
|
||||||
|
if not matches:
|
||||||
|
p = fsjoin(self.data_dir, specific)
|
||||||
|
matches = [p] if self.fs.exists(p) else []
|
||||||
|
if not matches:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Clip '{specific}' not found in {self.data_dir}"
|
||||||
|
)
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
def _pick_next(self) -> str:
|
||||||
|
while self.index < len(self.clips):
|
||||||
|
clip = self.clips[self.index]
|
||||||
|
self.index += 1
|
||||||
|
if not self.skip_annotated or not self.is_annotated(clip):
|
||||||
|
return clip
|
||||||
|
raise RuntimeError("No remaining clips to annotate")
|
||||||
49
src/clip_annotator/compute_optical_flow.py
Normal file
49
src/clip_annotator/compute_optical_flow.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def compute_optical_flow_mask(
|
||||||
|
frames: list[np.ndarray],
|
||||||
|
fps: float,
|
||||||
|
norm_squared_threshold: float,
|
||||||
|
gaussian_kernel: tuple[int, int],
|
||||||
|
brightness_range: tuple[int, int],
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Return a binary mask (uint8, values 0/1) from optical flow + brightness."""
|
||||||
|
if len(frames) < 2:
|
||||||
|
return np.zeros(frames[0].shape[:2], dtype=np.uint8)
|
||||||
|
|
||||||
|
frames_arr = np.stack(frames).astype(np.float64)
|
||||||
|
frames_sub_mean = frames_arr - np.mean(frames_arr, axis=0)
|
||||||
|
mn, mx = frames_sub_mean.min(), frames_sub_mean.max()
|
||||||
|
if mx > mn:
|
||||||
|
standardized = ((frames_sub_mean - mn) / (mx - mn) * 255).astype(np.uint8)
|
||||||
|
else:
|
||||||
|
standardized = np.zeros_like(frames_arr, dtype=np.uint8)
|
||||||
|
|
||||||
|
N = len(standardized)
|
||||||
|
gray = np.stack([cv2.cvtColor(f, cv2.COLOR_RGB2GRAY) for f in standardized])
|
||||||
|
|
||||||
|
flow_data = np.zeros((N - 1,) + gray.shape[1:] + (2,))
|
||||||
|
for i in range(N - 1):
|
||||||
|
flow_data[i] = fps * cv2.optflow.calcOpticalFlowSparseToDense(
|
||||||
|
gray[i], gray[i + 1]
|
||||||
|
)
|
||||||
|
|
||||||
|
optical_flow = np.median(flow_data, axis=0)
|
||||||
|
|
||||||
|
flow_norm_sq = np.sum(optical_flow**2, axis=-1)
|
||||||
|
max_norm = np.max(flow_norm_sq)
|
||||||
|
if max_norm > 0:
|
||||||
|
flow_mask = flow_norm_sq >= max_norm * norm_squared_threshold**2
|
||||||
|
else:
|
||||||
|
flow_mask = np.zeros(flow_norm_sq.shape, dtype=bool)
|
||||||
|
|
||||||
|
reference_frame = frames[len(frames) // 2]
|
||||||
|
smoothed = cv2.GaussianBlur(reference_frame, gaussian_kernel, 0)
|
||||||
|
gray_ref = cv2.cvtColor(smoothed, cv2.COLOR_RGB2GRAY)
|
||||||
|
brightness_mask = (gray_ref > brightness_range[0]) & (
|
||||||
|
gray_ref < brightness_range[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
return np.logical_and(brightness_mask, flow_mask).astype(np.uint8)
|
||||||
94
src/clip_annotator/config.py
Normal file
94
src/clip_annotator/config.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FilenameConfig:
|
||||||
|
video_in_zip: str = "left.mp4"
|
||||||
|
video_tmp_suffix: str = ".mp4"
|
||||||
|
zip_extension: str = ".zip"
|
||||||
|
mask: str = "mask.png"
|
||||||
|
metadata: str = "metadata.json"
|
||||||
|
frame: str = "frame.png"
|
||||||
|
overlay: str = "overlay.png"
|
||||||
|
mask_vis: str = "mask_vis.png"
|
||||||
|
gif_original_hires: str = "video_original_hires.gif"
|
||||||
|
gif_original_lowres: str = "video_original_lowres.gif"
|
||||||
|
gif_overlay_hires: str = "video_overlay_hires.gif"
|
||||||
|
gif_overlay_lowres: str = "video_overlay_lowres.gif"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppConfig:
|
||||||
|
storage: str
|
||||||
|
data_dir: str
|
||||||
|
out_dir: str
|
||||||
|
optical_flow_config_file: str
|
||||||
|
questions_config_file: str
|
||||||
|
display_max: int = 480
|
||||||
|
fps_fallback: int = 25
|
||||||
|
max_frames: int = 100
|
||||||
|
clips_file: str = "config/clips.txt"
|
||||||
|
filenames: FilenameConfig = field(default_factory=FilenameConfig)
|
||||||
|
questions: list = field(default_factory=list, init=False)
|
||||||
|
|
||||||
|
def get_questions(self):
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
s["section"],
|
||||||
|
[
|
||||||
|
(
|
||||||
|
item["key"],
|
||||||
|
item["label"],
|
||||||
|
[str(o) for o in item["options"]],
|
||||||
|
str(item["default"])
|
||||||
|
if item.get("default") is not None
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
for item in s["items"]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for s in self.questions
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OpticalFlowConfig:
|
||||||
|
enabled: bool = False
|
||||||
|
norm_squared_threshold: float = 0.3
|
||||||
|
gaussian_kernel: tuple[int, int] = (5, 5)
|
||||||
|
brightness_range: tuple[int, int] = (20, 235)
|
||||||
|
|
||||||
|
|
||||||
|
def load_optical_flow_config(path: Path) -> OpticalFlowConfig:
|
||||||
|
with open(path) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
data["gaussian_kernel"] = tuple(data["gaussian_kernel"])
|
||||||
|
data["brightness_range"] = tuple(data["brightness_range"])
|
||||||
|
return OpticalFlowConfig(**data)
|
||||||
|
|
||||||
|
|
||||||
|
def load_questions_config(path: Path) -> list:
|
||||||
|
with open(path) as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(path: Path) -> AppConfig:
|
||||||
|
with open(path) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
for required in (
|
||||||
|
"storage",
|
||||||
|
"data_dir",
|
||||||
|
"out_dir",
|
||||||
|
"optical_flow_config_file",
|
||||||
|
"questions_config_file",
|
||||||
|
):
|
||||||
|
if not data.get(required):
|
||||||
|
raise ValueError(f"{path}: missing required field '{required}'.")
|
||||||
|
fn_data = data.pop("filenames", {})
|
||||||
|
cfg = AppConfig(**data)
|
||||||
|
cfg.filenames = FilenameConfig(**fn_data)
|
||||||
|
cfg.questions = load_questions_config(Path(cfg.questions_config_file))
|
||||||
|
return cfg
|
||||||
35
src/clip_annotator/filesystem.py
Normal file
35
src/clip_annotator/filesystem.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_ENDPOINT = "https://os.zhdk.cloud.switch.ch"
|
||||||
|
|
||||||
|
|
||||||
|
def make_fs(storage: str):
|
||||||
|
"""Return an S3FileSystem for storage='s3', or None for local."""
|
||||||
|
if storage != "s3":
|
||||||
|
return None
|
||||||
|
import s3fs
|
||||||
|
|
||||||
|
return s3fs.S3FileSystem(
|
||||||
|
key=os.environ["S3_ACCESS_KEY"],
|
||||||
|
secret=os.environ["S3_SECRET_ACCESS_KEY"],
|
||||||
|
client_kwargs={
|
||||||
|
"endpoint_url": os.environ.get("S3_ENDPOINT_URL", _DEFAULT_ENDPOINT)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fsjoin(base, *parts: str) -> str:
|
||||||
|
"""Join path segments with forward slashes (works for both local and S3)."""
|
||||||
|
return "/".join([str(base).rstrip("/"), *[str(p).strip("/") for p in parts if p]])
|
||||||
|
|
||||||
|
|
||||||
|
def fsstem(path) -> str:
|
||||||
|
"""Filename stem (no extension) for local Path or S3 string."""
|
||||||
|
name = str(path).replace("\\", "/").split("/")[-1]
|
||||||
|
return name.rsplit(".", 1)[0] if "." in name else name
|
||||||
|
|
||||||
|
|
||||||
|
def fsname(path) -> str:
|
||||||
|
"""Filename component (with extension) for local Path or S3 string."""
|
||||||
|
return str(path).replace("\\", "/").split("/")[-1]
|
||||||
452
src/clip_annotator/mask_canvas.py
Normal file
452
src/clip_annotator/mask_canvas.py
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
from matplotlib.patches import Circle
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import QPushButton, QSlider
|
||||||
|
|
||||||
|
|
||||||
|
class MaskCanvas:
|
||||||
|
"""Matplotlib canvas with brush/polygon mask drawing, undo/redo, and erase."""
|
||||||
|
|
||||||
|
_BRUSH_DEFAULT = 5
|
||||||
|
_ALPHA_DEFAULT = 15
|
||||||
|
_BRIGHTNESS_DEFAULT = 0
|
||||||
|
_CONTRAST_DEFAULT = 0
|
||||||
|
_GAMMA_DEFAULT = 100
|
||||||
|
_CLOSE_THRESHOLD = 15 # image-pixel distance to first vertex that closes a polygon
|
||||||
|
|
||||||
|
def __init__(self, frames, dh: int, dw: int):
|
||||||
|
self.dh = dh
|
||||||
|
self.dw = dw
|
||||||
|
|
||||||
|
self.mask = np.zeros((dh, dw), dtype=np.uint8)
|
||||||
|
self.history: list[np.ndarray] = []
|
||||||
|
self.redo_stack: list[np.ndarray] = []
|
||||||
|
self.erase_mode = False
|
||||||
|
self.drawing = False
|
||||||
|
self.mask_visible = True
|
||||||
|
self._current_frame = frames[0]
|
||||||
|
|
||||||
|
self.tool_mode = "brush"
|
||||||
|
self._shapes: list[list[tuple]] = []
|
||||||
|
self._current_poly: list[tuple] = []
|
||||||
|
self._poly_artists: list = []
|
||||||
|
self._mouse_pos: tuple | None = None
|
||||||
|
|
||||||
|
self._build_figure(frames)
|
||||||
|
self._build_controls()
|
||||||
|
self._connect_events()
|
||||||
|
|
||||||
|
def _build_figure(self, frames):
|
||||||
|
self.fig = Figure(figsize=(self.dw / 80, self.dh / 80))
|
||||||
|
self.canvas = FigureCanvas(self.fig)
|
||||||
|
self.ax = self.fig.add_subplot(111)
|
||||||
|
self.ax.axis("off")
|
||||||
|
self.img_artist = self.ax.imshow(frames[0])
|
||||||
|
self.mask_artist = self.ax.imshow(np.zeros((self.dh, self.dw, 4)))
|
||||||
|
self.title_text = self.ax.set_title("", fontsize=10, pad=4)
|
||||||
|
self.brush_circle = Circle(
|
||||||
|
(0, 0), radius=5, fill=False, color="white", linewidth=1.5, visible=False
|
||||||
|
)
|
||||||
|
self.ax.add_patch(self.brush_circle)
|
||||||
|
self.ax.autoscale(False) # prevent polygon plot() calls from expanding the view
|
||||||
|
|
||||||
|
def _build_controls(self):
|
||||||
|
self.btn_erase = QPushButton("Eraser")
|
||||||
|
self.btn_mask = QPushButton("Hide Mask")
|
||||||
|
|
||||||
|
self.btn_brush = QPushButton("Brush")
|
||||||
|
self.btn_brush.setStyleSheet("background-color: #4488ff; color: white;")
|
||||||
|
self.btn_polygon = QPushButton("Polygon")
|
||||||
|
self.btn_fill = QPushButton("Fill")
|
||||||
|
self.btn_fill.setEnabled(False)
|
||||||
|
self.btn_del_shape = QPushButton("Del Shape")
|
||||||
|
self.btn_del_shape.setEnabled(False)
|
||||||
|
self.btn_cancel_poly = QPushButton("Cancel Current Poly")
|
||||||
|
|
||||||
|
self.brush_slider = QSlider(Qt.Horizontal)
|
||||||
|
self.brush_slider.setRange(2, 50)
|
||||||
|
self.brush_slider.setValue(self._BRUSH_DEFAULT)
|
||||||
|
self.brush_reset = QPushButton("↺")
|
||||||
|
self.brush_reset.setFixedWidth(28)
|
||||||
|
|
||||||
|
self.alpha_slider = QSlider(Qt.Horizontal)
|
||||||
|
self.alpha_slider.setRange(0, 100)
|
||||||
|
self.alpha_slider.setValue(self._ALPHA_DEFAULT)
|
||||||
|
self.alpha_reset = QPushButton("↺")
|
||||||
|
self.alpha_reset.setFixedWidth(28)
|
||||||
|
|
||||||
|
self.brightness_slider = QSlider(Qt.Vertical)
|
||||||
|
self.brightness_slider.setRange(-100, 100)
|
||||||
|
self.brightness_slider.setValue(self._BRIGHTNESS_DEFAULT)
|
||||||
|
self.brightness_reset = QPushButton("↺")
|
||||||
|
self.brightness_reset.setFixedWidth(28)
|
||||||
|
|
||||||
|
self.contrast_slider = QSlider(Qt.Vertical)
|
||||||
|
self.contrast_slider.setRange(-100, 100)
|
||||||
|
self.contrast_slider.setValue(self._CONTRAST_DEFAULT)
|
||||||
|
self.contrast_reset = QPushButton("↺")
|
||||||
|
self.contrast_reset.setFixedWidth(28)
|
||||||
|
|
||||||
|
self.gamma_slider = QSlider(Qt.Vertical)
|
||||||
|
self.gamma_slider.setRange(10, 300)
|
||||||
|
self.gamma_slider.setValue(self._GAMMA_DEFAULT)
|
||||||
|
self.gamma_reset = QPushButton("↺")
|
||||||
|
self.gamma_reset.setFixedWidth(28)
|
||||||
|
|
||||||
|
def _connect_events(self):
|
||||||
|
self.canvas.mpl_connect("button_press_event", self._on_press)
|
||||||
|
self.canvas.mpl_connect("motion_notify_event", self._on_move)
|
||||||
|
self.canvas.mpl_connect("button_release_event", self._on_release)
|
||||||
|
self.canvas.mpl_connect("axes_leave_event", self._on_axes_leave)
|
||||||
|
self.btn_erase.clicked.connect(self.toggle_erase)
|
||||||
|
self.btn_mask.clicked.connect(self.toggle_mask)
|
||||||
|
self.btn_brush.clicked.connect(lambda: self.set_tool_mode("brush"))
|
||||||
|
self.btn_polygon.clicked.connect(lambda: self.set_tool_mode("polygon"))
|
||||||
|
self.btn_fill.clicked.connect(lambda: self.set_tool_mode("fill"))
|
||||||
|
self.btn_del_shape.clicked.connect(self.delete_last_shape)
|
||||||
|
self.btn_cancel_poly.clicked.connect(self.cancel_polygon)
|
||||||
|
self.alpha_slider.valueChanged.connect(self.redraw)
|
||||||
|
self.brightness_slider.valueChanged.connect(self._refresh_frame)
|
||||||
|
self.contrast_slider.valueChanged.connect(self._refresh_frame)
|
||||||
|
self.gamma_slider.valueChanged.connect(self._refresh_frame)
|
||||||
|
self.brush_reset.clicked.connect(
|
||||||
|
lambda: self.brush_slider.setValue(self._BRUSH_DEFAULT)
|
||||||
|
)
|
||||||
|
self.alpha_reset.clicked.connect(
|
||||||
|
lambda: self.alpha_slider.setValue(self._ALPHA_DEFAULT)
|
||||||
|
)
|
||||||
|
self.brightness_reset.clicked.connect(
|
||||||
|
lambda: self.brightness_slider.setValue(self._BRIGHTNESS_DEFAULT)
|
||||||
|
)
|
||||||
|
self.contrast_reset.clicked.connect(
|
||||||
|
lambda: self.contrast_slider.setValue(self._CONTRAST_DEFAULT)
|
||||||
|
)
|
||||||
|
self.gamma_reset.clicked.connect(
|
||||||
|
lambda: self.gamma_slider.setValue(self._GAMMA_DEFAULT)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── clip transition ────────────────────────────────────────────
|
||||||
|
def load_clip(self, frames, dh: int, dw: int, mask=None, title: str = ""):
|
||||||
|
self.dh = dh
|
||||||
|
self.dw = dw
|
||||||
|
self.mask = mask if mask is not None else np.zeros((dh, dw), dtype=np.uint8)
|
||||||
|
self.history = []
|
||||||
|
self.redo_stack = []
|
||||||
|
self._current_frame = frames[0]
|
||||||
|
self._clear_poly_state()
|
||||||
|
self.img_artist.set_data(self._apply_image_adjustments(frames[0]))
|
||||||
|
self.ax.set_xlim(-0.5, dw - 0.5)
|
||||||
|
self.ax.set_ylim(dh - 0.5, -0.5)
|
||||||
|
self.set_title(title)
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
def _clear_poly_state(self):
|
||||||
|
self._shapes = []
|
||||||
|
self._current_poly = []
|
||||||
|
self._mouse_pos = None
|
||||||
|
for a in self._poly_artists:
|
||||||
|
a.remove()
|
||||||
|
self._poly_artists = []
|
||||||
|
self._update_poly_buttons()
|
||||||
|
|
||||||
|
# ── frame / title ──────────────────────────────────────────────
|
||||||
|
def set_frame(self, frame):
|
||||||
|
self._current_frame = frame
|
||||||
|
self.img_artist.set_data(self._apply_image_adjustments(frame))
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
# ── image adjustments ──────────────────────────────────────────
|
||||||
|
def _apply_image_adjustments(self, frame):
|
||||||
|
img = frame.astype(np.float32)
|
||||||
|
img += self.brightness_slider.value()
|
||||||
|
c = self.contrast_slider.value() / 100.0
|
||||||
|
img = (1.0 + c) * (img - 128.0) + 128.0
|
||||||
|
np.clip(img, 0, 255, out=img)
|
||||||
|
g = self.gamma_slider.value() / 100.0
|
||||||
|
img = (img / 255.0) ** (1.0 / g) * 255.0
|
||||||
|
return np.clip(img, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def _refresh_frame(self):
|
||||||
|
if self._current_frame is not None:
|
||||||
|
self.img_artist.set_data(self._apply_image_adjustments(self._current_frame))
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
def set_title(self, text: str):
|
||||||
|
self.title_text.set_text(text)
|
||||||
|
|
||||||
|
# ── mask ops ───────────────────────────────────────────────────
|
||||||
|
def reset(self, mask=None):
|
||||||
|
self.mask = (
|
||||||
|
mask if mask is not None else np.zeros((self.dh, self.dw), dtype=np.uint8)
|
||||||
|
)
|
||||||
|
self.history = []
|
||||||
|
self.redo_stack = []
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
def set_mask(self, mask):
|
||||||
|
"""Replace the mask and push the previous state onto the undo stack."""
|
||||||
|
self.history.append(self.mask.copy())
|
||||||
|
self.redo_stack.clear()
|
||||||
|
self.mask = mask
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
def redraw(self):
|
||||||
|
if self.mask_visible:
|
||||||
|
alpha = self.alpha_slider.value() / 100.0
|
||||||
|
rgba = np.zeros((self.dh, self.dw, 4))
|
||||||
|
rgba[..., 1] = self.mask * 0.7
|
||||||
|
rgba[..., 3] = self.mask * alpha
|
||||||
|
else:
|
||||||
|
rgba = np.zeros((self.dh, self.dw, 4))
|
||||||
|
self.mask_artist.set_data(rgba)
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.mask[:] = 0
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.history:
|
||||||
|
self.redo_stack.append(self.mask.copy())
|
||||||
|
self.mask = self.history.pop()
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
def undo10(self):
|
||||||
|
for _ in range(10):
|
||||||
|
if not self.history:
|
||||||
|
break
|
||||||
|
self.redo_stack.append(self.mask.copy())
|
||||||
|
self.mask = self.history.pop()
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
if self.redo_stack:
|
||||||
|
self.history.append(self.mask.copy())
|
||||||
|
self.mask = self.redo_stack.pop()
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
def toggle_erase(self):
|
||||||
|
self.erase_mode = not self.erase_mode
|
||||||
|
if self.erase_mode:
|
||||||
|
self.btn_erase.setText("Eraser ON")
|
||||||
|
self.btn_erase.setStyleSheet("background-color: orange; color: black;")
|
||||||
|
else:
|
||||||
|
self.btn_erase.setText("Eraser")
|
||||||
|
self.btn_erase.setStyleSheet("")
|
||||||
|
|
||||||
|
def toggle_mask(self):
|
||||||
|
self.mask_visible = not self.mask_visible
|
||||||
|
if self.mask_visible:
|
||||||
|
self.btn_mask.setText("Hide Mask")
|
||||||
|
self.btn_mask.setStyleSheet("")
|
||||||
|
else:
|
||||||
|
self.btn_mask.setText("Show Mask")
|
||||||
|
self.btn_mask.setStyleSheet("background-color: red; color: white;")
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
def stamp(self, x, y):
|
||||||
|
if x is None or y is None:
|
||||||
|
return
|
||||||
|
self.history.append(self.mask.copy())
|
||||||
|
self.redo_stack.clear()
|
||||||
|
r = self.brush_slider.value()
|
||||||
|
ix, iy = int(x), int(y)
|
||||||
|
y0, y1 = max(0, iy - r), min(self.dh, iy + r + 1)
|
||||||
|
x0, x1 = max(0, ix - r), min(self.dw, ix + r + 1)
|
||||||
|
Y, X = np.ogrid[y0:y1, x0:x1]
|
||||||
|
circle = (X - ix) ** 2 + (Y - iy) ** 2 <= r**2
|
||||||
|
self.mask[y0:y1, x0:x1][circle] = 0 if self.erase_mode else 1
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
# ── tool mode ──────────────────────────────────────────────────
|
||||||
|
def set_tool_mode(self, mode: str):
|
||||||
|
self.tool_mode = mode
|
||||||
|
active = "background-color: #4488ff; color: white;"
|
||||||
|
self.btn_brush.setStyleSheet(active if mode == "brush" else "")
|
||||||
|
self.btn_polygon.setStyleSheet(active if mode == "polygon" else "")
|
||||||
|
self.btn_fill.setStyleSheet(active if mode == "fill" else "")
|
||||||
|
if mode != "brush":
|
||||||
|
self.brush_circle.set_visible(False)
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
# ── polygon ops ────────────────────────────────────────────────
|
||||||
|
def _near_first(self, x: float, y: float) -> bool:
|
||||||
|
if not self._current_poly:
|
||||||
|
return False
|
||||||
|
fx, fy = self._current_poly[0]
|
||||||
|
return (x - fx) ** 2 + (y - fy) ** 2 <= self._CLOSE_THRESHOLD**2
|
||||||
|
|
||||||
|
def _update_poly_buttons(self):
|
||||||
|
has = bool(self._shapes)
|
||||||
|
self.btn_fill.setEnabled(has)
|
||||||
|
self.btn_del_shape.setEnabled(has)
|
||||||
|
|
||||||
|
def _draw_polygon_overlay(self, mouse_pos=None):
|
||||||
|
for a in self._poly_artists:
|
||||||
|
a.remove()
|
||||||
|
self._poly_artists.clear()
|
||||||
|
|
||||||
|
# Completed shapes — thick closed outline
|
||||||
|
for shape in self._shapes:
|
||||||
|
xs = [p[0] for p in shape] + [shape[0][0]]
|
||||||
|
ys = [p[1] for p in shape] + [shape[0][1]]
|
||||||
|
(line,) = self.ax.plot(xs, ys, color="cyan", linewidth=3, zorder=5)
|
||||||
|
(dots,) = self.ax.plot(
|
||||||
|
[p[0] for p in shape],
|
||||||
|
[p[1] for p in shape],
|
||||||
|
"o",
|
||||||
|
color="cyan",
|
||||||
|
markersize=4,
|
||||||
|
zorder=6,
|
||||||
|
)
|
||||||
|
self._poly_artists.extend([line, dots])
|
||||||
|
|
||||||
|
# In-progress polygon
|
||||||
|
if self._current_poly:
|
||||||
|
xs = [p[0] for p in self._current_poly]
|
||||||
|
ys = [p[1] for p in self._current_poly]
|
||||||
|
|
||||||
|
if len(self._current_poly) > 1:
|
||||||
|
(edge,) = self.ax.plot(xs, ys, color="yellow", linewidth=1.5, zorder=5)
|
||||||
|
self._poly_artists.append(edge)
|
||||||
|
|
||||||
|
(verts,) = self.ax.plot(xs, ys, "o", color="yellow", markersize=5, zorder=6)
|
||||||
|
# Red dot on first vertex as close-target indicator
|
||||||
|
(first,) = self.ax.plot(
|
||||||
|
[xs[0]], [ys[0]], "o", color="red", markersize=8, zorder=7
|
||||||
|
)
|
||||||
|
self._poly_artists.extend([verts, first])
|
||||||
|
|
||||||
|
# Rubber-band line from last vertex to cursor
|
||||||
|
if mouse_pos:
|
||||||
|
mx, my = mouse_pos
|
||||||
|
near = len(self._current_poly) >= 3 and self._near_first(mx, my)
|
||||||
|
clr = "lime" if near else "yellow"
|
||||||
|
(rband,) = self.ax.plot(
|
||||||
|
[xs[-1], mx], [ys[-1], my], "--", color=clr, linewidth=1, zorder=5
|
||||||
|
)
|
||||||
|
self._poly_artists.append(rband)
|
||||||
|
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
def cancel_polygon(self):
|
||||||
|
self._current_poly = []
|
||||||
|
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
|
||||||
|
|
||||||
|
def delete_last_shape(self):
|
||||||
|
if self._shapes:
|
||||||
|
self._shapes.pop()
|
||||||
|
self._update_poly_buttons()
|
||||||
|
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
|
||||||
|
|
||||||
|
def _fill_shape_at(self, x: float, y: float):
|
||||||
|
if not self._shapes:
|
||||||
|
return
|
||||||
|
|
||||||
|
polys = [
|
||||||
|
np.array(
|
||||||
|
[(int(round(px)), int(round(py))) for px, py in shape], dtype=np.int32
|
||||||
|
)
|
||||||
|
for shape in self._shapes
|
||||||
|
]
|
||||||
|
|
||||||
|
# Find all shapes that contain the click point
|
||||||
|
containing = []
|
||||||
|
for i, poly in enumerate(polys):
|
||||||
|
poly_f32 = poly.reshape(-1, 1, 2).astype(np.float32)
|
||||||
|
if cv2.pointPolygonTest(poly_f32, (x, y), False) >= 0:
|
||||||
|
containing.append((i, poly))
|
||||||
|
|
||||||
|
if not containing:
|
||||||
|
return # click was outside all shapes
|
||||||
|
|
||||||
|
# Pick the innermost (smallest area) shape that contains the click
|
||||||
|
containing.sort(key=lambda t: cv2.contourArea(t[1]))
|
||||||
|
target_idx, target_poly = containing[0]
|
||||||
|
|
||||||
|
self.history.append(self.mask.copy())
|
||||||
|
self.redo_stack.clear()
|
||||||
|
|
||||||
|
temp = np.zeros((self.dh, self.dw), dtype=np.uint8)
|
||||||
|
cv2.fillPoly(temp, [target_poly], 1)
|
||||||
|
|
||||||
|
# Punch holes for any shapes completely inside the target
|
||||||
|
target_f32 = target_poly.reshape(-1, 1, 2).astype(np.float32)
|
||||||
|
for i, poly in enumerate(polys):
|
||||||
|
if i == target_idx:
|
||||||
|
continue
|
||||||
|
cx = float(np.mean(poly[:, 0]))
|
||||||
|
cy = float(np.mean(poly[:, 1]))
|
||||||
|
if cv2.pointPolygonTest(target_f32, (cx, cy), False) > 0:
|
||||||
|
cv2.fillPoly(temp, [poly], 0)
|
||||||
|
|
||||||
|
self.mask |= temp
|
||||||
|
self.redraw()
|
||||||
|
|
||||||
|
# ── brush preview ──────────────────────────────────────────────
|
||||||
|
def _update_brush_preview(self, e):
|
||||||
|
if e.inaxes == self.ax and e.xdata is not None:
|
||||||
|
self.brush_circle.center = (e.xdata, e.ydata)
|
||||||
|
self.brush_circle.set_radius(self.brush_slider.value())
|
||||||
|
self.brush_circle.set_visible(True)
|
||||||
|
else:
|
||||||
|
self.brush_circle.set_visible(False)
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
def _on_axes_leave(self, _):
|
||||||
|
self.brush_circle.set_visible(False)
|
||||||
|
if self.tool_mode == "polygon":
|
||||||
|
self._mouse_pos = None
|
||||||
|
self._draw_polygon_overlay()
|
||||||
|
else:
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
# ── mouse events ───────────────────────────────────────────────
|
||||||
|
def _on_press(self, e):
|
||||||
|
if e.xdata is None:
|
||||||
|
return
|
||||||
|
if self.tool_mode == "brush":
|
||||||
|
self.drawing = True
|
||||||
|
self.stamp(e.xdata, e.ydata)
|
||||||
|
elif self.tool_mode == "polygon":
|
||||||
|
self._handle_polygon_click(e)
|
||||||
|
elif self.tool_mode == "fill" and e.button == 1:
|
||||||
|
self._fill_shape_at(e.xdata, e.ydata)
|
||||||
|
|
||||||
|
def _handle_polygon_click(self, e):
|
||||||
|
if e.button == 3: # right-click: remove last vertex
|
||||||
|
if self._current_poly:
|
||||||
|
self._current_poly.pop()
|
||||||
|
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
|
||||||
|
return
|
||||||
|
if e.button != 1:
|
||||||
|
return
|
||||||
|
x, y = e.xdata, e.ydata
|
||||||
|
if len(self._current_poly) >= 3 and self._near_first(x, y):
|
||||||
|
self._shapes.append(list(self._current_poly))
|
||||||
|
self._current_poly = []
|
||||||
|
self._update_poly_buttons()
|
||||||
|
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
|
||||||
|
else:
|
||||||
|
self._current_poly.append((x, y))
|
||||||
|
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
|
||||||
|
|
||||||
|
def _on_move(self, e):
|
||||||
|
if self.tool_mode == "brush":
|
||||||
|
self._update_brush_preview(e)
|
||||||
|
if self.drawing:
|
||||||
|
self.stamp(e.xdata, e.ydata)
|
||||||
|
elif self.tool_mode == "polygon":
|
||||||
|
self.brush_circle.set_visible(False)
|
||||||
|
if e.inaxes == self.ax and e.xdata is not None:
|
||||||
|
self._mouse_pos = (e.xdata, e.ydata)
|
||||||
|
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
|
||||||
|
else:
|
||||||
|
self._mouse_pos = None
|
||||||
|
self._draw_polygon_overlay()
|
||||||
|
|
||||||
|
def _on_release(self, _):
|
||||||
|
self.drawing = False
|
||||||
56
src/clip_annotator/video_loader.py
Normal file
56
src/clip_annotator/video_loader.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
|
||||||
|
def load_frames(
|
||||||
|
zip_path,
|
||||||
|
max_frames: int,
|
||||||
|
display_max: int,
|
||||||
|
fps_fallback: int,
|
||||||
|
video_in_zip: str = "left.mp4",
|
||||||
|
video_tmp_suffix: str = ".mp4",
|
||||||
|
fs=None,
|
||||||
|
):
|
||||||
|
if fs is None:
|
||||||
|
video_bytes = zipfile.ZipFile(zip_path).read(video_in_zip)
|
||||||
|
else:
|
||||||
|
with fs.open(str(zip_path), "rb") as f:
|
||||||
|
video_bytes = zipfile.ZipFile(io.BytesIO(f.read())).read(video_in_zip)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=video_tmp_suffix, delete=False) as f:
|
||||||
|
f.write(video_bytes)
|
||||||
|
tmp_path = f.name
|
||||||
|
|
||||||
|
cap = cv2.VideoCapture(tmp_path)
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS) or fps_fallback
|
||||||
|
|
||||||
|
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
step = max(1, total // max_frames)
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
i = 0
|
||||||
|
while len(frames) < max_frames:
|
||||||
|
cap.set(cv2.CAP_PROP_POS_FRAMES, i)
|
||||||
|
ok, frame = cap.read()
|
||||||
|
if not ok:
|
||||||
|
break
|
||||||
|
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
||||||
|
i += step
|
||||||
|
|
||||||
|
cap.release()
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
if not frames:
|
||||||
|
raise RuntimeError(f"No frames found in {zip_path}")
|
||||||
|
|
||||||
|
h, w = frames[0].shape[:2]
|
||||||
|
scale = display_max / max(h, w)
|
||||||
|
dh, dw = int(h * scale), int(w * scale)
|
||||||
|
|
||||||
|
frames = [cv2.resize(f, (dw, dh)) for f in frames]
|
||||||
|
|
||||||
|
return frames, fps, dh, dw, h, w
|
||||||
@@ -1,604 +0,0 @@
|
|||||||
import os
|
|
||||||
import zipfile
|
|
||||||
import tempfile
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from matplotlib import use
|
|
||||||
|
|
||||||
use("QtAgg")
|
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QMainWindow,
|
|
||||||
QWidget,
|
|
||||||
QPushButton,
|
|
||||||
QVBoxLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QRadioButton,
|
|
||||||
QButtonGroup,
|
|
||||||
QGroupBox,
|
|
||||||
QSlider,
|
|
||||||
)
|
|
||||||
from PySide6.QtCore import Qt, QTimer
|
|
||||||
|
|
||||||
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
||||||
from matplotlib.figure import Figure
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# CONFIG
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
class Config:
|
|
||||||
DISPLAY_MAX = 480
|
|
||||||
FPS_FALLBACK = 25
|
|
||||||
MAX_FRAMES = 100
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# QUESTIONS
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
QUESTIONS = [
|
|
||||||
(
|
|
||||||
"River",
|
|
||||||
[
|
|
||||||
("flow", "Flow Regime", ["Turbulent", "Laminar", "Uncertain"]),
|
|
||||||
("shadows", "Strong Shadows", ["Yes", "No", "Uncertain"]),
|
|
||||||
("artifacts", "Artifacts on River", ["Yes", "No", "Uncertain"]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Scene",
|
|
||||||
[
|
|
||||||
("lighting", "Lighting", ["Day", "Night", "Uncertain"]),
|
|
||||||
(
|
|
||||||
"exposure",
|
|
||||||
"Exposure",
|
|
||||||
["Overexposed", "Underexposed", "Both", "Normal", "Uncertain"],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Weather",
|
|
||||||
[
|
|
||||||
("snowing", "Snowing", ["Yes", "No", "Uncertain"]),
|
|
||||||
("snow_on_ground", "Snow on Ground", ["Yes", "No", "Uncertain"]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# DEFAULTS
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
DEFAULTS = {
|
|
||||||
"flow": "Laminar",
|
|
||||||
"shadows": "No",
|
|
||||||
"artifacts": "No",
|
|
||||||
"lighting": "Day",
|
|
||||||
"exposure": "Normal",
|
|
||||||
"snowing": "No",
|
|
||||||
"snow_on_ground": "No",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# VIDEO LOADING
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
def load_frames(zip_path: Path, max_frames: int):
|
|
||||||
video_bytes = zipfile.ZipFile(zip_path).read("left.mp4")
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
|
|
||||||
f.write(video_bytes)
|
|
||||||
tmp_path = f.name
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(tmp_path)
|
|
||||||
fps = cap.get(cv2.CAP_PROP_FPS) or Config.FPS_FALLBACK
|
|
||||||
|
|
||||||
frames = []
|
|
||||||
while len(frames) < max_frames:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
os.unlink(tmp_path)
|
|
||||||
|
|
||||||
if not frames:
|
|
||||||
raise RuntimeError(f"No frames found in {zip_path}")
|
|
||||||
|
|
||||||
h, w = frames[0].shape[:2]
|
|
||||||
scale = Config.DISPLAY_MAX / max(h, w)
|
|
||||||
dh, dw = int(h * scale), int(w * scale)
|
|
||||||
|
|
||||||
frames = [cv2.resize(f, (dw, dh)) for f in frames]
|
|
||||||
|
|
||||||
return frames, fps, dh, dw, h, w
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# MAIN APP
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
class Annotator(QMainWindow):
|
|
||||||
def __init__(self, data_dir: Path, out_dir: Path, clip: str = None, target_time: str = None, daily: bool = False, extras: bool = False, skip_existing_day: bool = False):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.data_dir = Path(data_dir)
|
|
||||||
self.out_dir = Path(out_dir)
|
|
||||||
self.target_time = target_time
|
|
||||||
self.daily = daily
|
|
||||||
self.extras = extras
|
|
||||||
self.skip_existing_day = skip_existing_day
|
|
||||||
self.current_date = None
|
|
||||||
|
|
||||||
self.history = []
|
|
||||||
self.erase_mode = False
|
|
||||||
self.frame_i = 0
|
|
||||||
self.drawing = False
|
|
||||||
self._pending_answers = None
|
|
||||||
|
|
||||||
self.setWindowTitle("River Annotator")
|
|
||||||
|
|
||||||
self.df = self._load_dataset()
|
|
||||||
self._load_clip(specific=clip)
|
|
||||||
|
|
||||||
self._init_canvas()
|
|
||||||
self._init_ui()
|
|
||||||
self._init_timer()
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# DATA
|
|
||||||
# ─────────────────────────────
|
|
||||||
def _load_dataset(self):
|
|
||||||
files = list(self.data_dir.glob("*.zip"))
|
|
||||||
if not files:
|
|
||||||
raise FileNotFoundError(f"No zip files in {self.data_dir}")
|
|
||||||
|
|
||||||
df = pd.DataFrame({"filename": files})
|
|
||||||
df["datetime"] = df["filename"].apply(
|
|
||||||
lambda x: pd.to_datetime(x.stem.split("_")[1], errors="coerce")
|
|
||||||
)
|
|
||||||
# sort by datetime
|
|
||||||
df = df.sort_values("datetime").reset_index(drop=True)
|
|
||||||
return df
|
|
||||||
|
|
||||||
def _load_clip(self, specific: str = None, next_day: bool = False):
|
|
||||||
if specific is not None:
|
|
||||||
matches = list(self.data_dir.glob(f"{specific}.zip"))
|
|
||||||
if not matches:
|
|
||||||
p = self.data_dir / specific
|
|
||||||
matches = [p] if p.exists() else []
|
|
||||||
if not matches:
|
|
||||||
raise FileNotFoundError(f"Clip '{specific}' not found in {self.data_dir}")
|
|
||||||
self.filename = matches[0]
|
|
||||||
else:
|
|
||||||
remaining = [
|
|
||||||
f
|
|
||||||
for f in self.df["filename"]
|
|
||||||
if not (self.out_dir / f.stem / "mask.png").exists()
|
|
||||||
]
|
|
||||||
if not remaining:
|
|
||||||
raise RuntimeError("No remaining clips to annotate")
|
|
||||||
|
|
||||||
if self.target_time or self.daily:
|
|
||||||
# Parse target time (format: HH:MM)
|
|
||||||
if self.target_time:
|
|
||||||
target_hour, target_minute = map(int, self.target_time.split(":"))
|
|
||||||
else:
|
|
||||||
target_hour, target_minute = 12, 0 # Default to noon
|
|
||||||
target_seconds = target_hour * 3600 + target_minute * 60
|
|
||||||
|
|
||||||
# Get datetimes for remaining files
|
|
||||||
remaining_datetimes = [
|
|
||||||
self.df[self.df["filename"] == f]["datetime"].values[0]
|
|
||||||
for f in remaining
|
|
||||||
]
|
|
||||||
|
|
||||||
# Group by day
|
|
||||||
df_remaining = pd.DataFrame({
|
|
||||||
"filename": remaining,
|
|
||||||
"datetime": remaining_datetimes
|
|
||||||
})
|
|
||||||
df_remaining["date"] = df_remaining["datetime"].dt.date
|
|
||||||
|
|
||||||
# In daily mode, filter to next day if needed
|
|
||||||
if self.daily and next_day and self.current_date is not None:
|
|
||||||
import datetime
|
|
||||||
next_date = self.current_date + datetime.timedelta(days=1)
|
|
||||||
df_remaining = df_remaining[df_remaining["date"] >= next_date]
|
|
||||||
|
|
||||||
# In daily mode, skip entire days that already have any annotated clip
|
|
||||||
if self.daily and self.skip_existing_day:
|
|
||||||
annotated_dates = set()
|
|
||||||
for f in self.df["filename"]:
|
|
||||||
if (self.out_dir / f.stem / "mask.png").exists():
|
|
||||||
dt = self.df[self.df["filename"] == f]["datetime"].values[0]
|
|
||||||
annotated_dates.add(pd.Timestamp(dt).date())
|
|
||||||
df_remaining = df_remaining[~df_remaining["date"].isin(annotated_dates)]
|
|
||||||
|
|
||||||
if df_remaining.empty:
|
|
||||||
raise RuntimeError("No remaining clips to annotate")
|
|
||||||
|
|
||||||
# For each day, find the clip closest to target time
|
|
||||||
closest_clips = []
|
|
||||||
dates_list = []
|
|
||||||
for date, group in df_remaining.groupby("date"):
|
|
||||||
group = group.copy()
|
|
||||||
group["time_seconds"] = group["datetime"].dt.hour * 3600 + group["datetime"].dt.minute * 60
|
|
||||||
group["time_diff"] = (group["time_seconds"] - target_seconds).abs()
|
|
||||||
closest = group.loc[group["time_diff"].idxmin()]
|
|
||||||
closest_clips.append(closest["filename"])
|
|
||||||
dates_list.append(date)
|
|
||||||
|
|
||||||
# In daily mode, take only the first day's clip
|
|
||||||
if self.daily:
|
|
||||||
self.filename = closest_clips[0]
|
|
||||||
self.current_date = dates_list[0]
|
|
||||||
else:
|
|
||||||
# Take the first one (earliest by date/time)
|
|
||||||
self.filename = closest_clips[0]
|
|
||||||
self.current_date = dates_list[0]
|
|
||||||
else:
|
|
||||||
# take the earliest one (after sorting by datetime)
|
|
||||||
self.filename = remaining[0]
|
|
||||||
# Extract date from filename
|
|
||||||
import datetime
|
|
||||||
dt = self.df[self.df["filename"] == self.filename]["datetime"].values[0]
|
|
||||||
self.current_date = pd.Timestamp(dt).date()
|
|
||||||
|
|
||||||
self.frames, self.fps, self.dh, self.dw, self.h, self.w = load_frames(
|
|
||||||
self.filename, Config.MAX_FRAMES
|
|
||||||
)
|
|
||||||
|
|
||||||
self.history = []
|
|
||||||
self.mask = np.zeros((self.dh, self.dw), dtype=np.uint8)
|
|
||||||
self._pending_answers = None
|
|
||||||
|
|
||||||
out = self.out_dir / self.filename.stem
|
|
||||||
mask_path = out / "mask.png"
|
|
||||||
meta_path = out / "metadata.json"
|
|
||||||
|
|
||||||
if mask_path.exists():
|
|
||||||
mask_full = np.array(Image.open(mask_path).convert("L"))
|
|
||||||
self.mask = cv2.resize(
|
|
||||||
(mask_full > 127).astype(np.uint8),
|
|
||||||
(self.dw, self.dh),
|
|
||||||
interpolation=cv2.INTER_NEAREST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if meta_path.exists():
|
|
||||||
with open(meta_path) as f:
|
|
||||||
self._pending_answers = json.load(f)
|
|
||||||
|
|
||||||
def _set_answers(self, answers: dict):
|
|
||||||
for key, value in answers.items():
|
|
||||||
if key not in self.q_widgets:
|
|
||||||
continue
|
|
||||||
_, buttons, options = self.q_widgets[key]
|
|
||||||
for i, btn in enumerate(buttons):
|
|
||||||
btn.setChecked(options[i] == value)
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# UI
|
|
||||||
# ─────────────────────────────
|
|
||||||
def _init_canvas(self):
|
|
||||||
self.fig = Figure()
|
|
||||||
self.canvas = FigureCanvas(self.fig)
|
|
||||||
|
|
||||||
self.ax = self.fig.add_subplot(111)
|
|
||||||
self.ax.axis("off")
|
|
||||||
|
|
||||||
self.img = self.ax.imshow(self.frames[0])
|
|
||||||
self.mask_img = self.ax.imshow(np.zeros((self.dh, self.dw, 4)))
|
|
||||||
|
|
||||||
self.title_text = self.ax.set_title(self.filename.name, fontsize=10, pad=4)
|
|
||||||
|
|
||||||
def _init_ui(self):
|
|
||||||
self.q_widgets = {}
|
|
||||||
|
|
||||||
question_box = QVBoxLayout()
|
|
||||||
|
|
||||||
for section, qs in QUESTIONS:
|
|
||||||
group = QGroupBox(section)
|
|
||||||
vbox = QVBoxLayout()
|
|
||||||
|
|
||||||
for key, label, options in qs:
|
|
||||||
vbox.addWidget(QLabel(label))
|
|
||||||
|
|
||||||
btn_group = QButtonGroup(self)
|
|
||||||
row = QHBoxLayout()
|
|
||||||
buttons = []
|
|
||||||
|
|
||||||
default_value = DEFAULTS.get(key)
|
|
||||||
|
|
||||||
for opt in options:
|
|
||||||
btn = QRadioButton(opt)
|
|
||||||
btn_group.addButton(btn)
|
|
||||||
row.addWidget(btn)
|
|
||||||
buttons.append(btn)
|
|
||||||
|
|
||||||
if default_value == opt:
|
|
||||||
btn.setChecked(True)
|
|
||||||
|
|
||||||
if default_value is None and buttons:
|
|
||||||
buttons[-1].setChecked(True)
|
|
||||||
|
|
||||||
self.q_widgets[key] = (btn_group, buttons, options)
|
|
||||||
vbox.addLayout(row)
|
|
||||||
|
|
||||||
group.setLayout(vbox)
|
|
||||||
question_box.addWidget(group)
|
|
||||||
|
|
||||||
# Controls
|
|
||||||
self.btn_save = QPushButton("Save")
|
|
||||||
self.btn_next = QPushButton("Next")
|
|
||||||
self.btn_skip = QPushButton("Skip")
|
|
||||||
self.btn_clear = QPushButton("Clear")
|
|
||||||
self.btn_erase = QPushButton("Eraser")
|
|
||||||
self.btn_undo = QPushButton("Undo")
|
|
||||||
self.btn_reload = QPushButton("Reload Saved")
|
|
||||||
|
|
||||||
self.brush_slider = QSlider(Qt.Horizontal)
|
|
||||||
self.brush_slider.setRange(2, 50)
|
|
||||||
self.brush_slider.setValue(5)
|
|
||||||
|
|
||||||
row1 = QHBoxLayout()
|
|
||||||
for b in [self.btn_save, self.btn_next, self.btn_skip]:
|
|
||||||
row1.addWidget(b)
|
|
||||||
|
|
||||||
row2 = QHBoxLayout()
|
|
||||||
for b in [self.btn_clear, self.btn_erase, self.btn_undo, self.btn_reload]:
|
|
||||||
row2.addWidget(b)
|
|
||||||
row2.addWidget(QLabel("Brush"))
|
|
||||||
row2.addWidget(self.brush_slider)
|
|
||||||
|
|
||||||
left = QVBoxLayout()
|
|
||||||
left.addWidget(self.canvas)
|
|
||||||
left.addLayout(row1)
|
|
||||||
left.addLayout(row2)
|
|
||||||
|
|
||||||
main = QHBoxLayout()
|
|
||||||
|
|
||||||
left_widget = QWidget()
|
|
||||||
left_widget.setLayout(left)
|
|
||||||
|
|
||||||
right_widget = QWidget()
|
|
||||||
right_widget.setLayout(question_box)
|
|
||||||
|
|
||||||
main.addWidget(left_widget, 3)
|
|
||||||
main.addWidget(right_widget, 2)
|
|
||||||
|
|
||||||
container = QWidget()
|
|
||||||
container.setLayout(main)
|
|
||||||
self.setCentralWidget(container)
|
|
||||||
|
|
||||||
# events
|
|
||||||
self.btn_save.clicked.connect(self.save)
|
|
||||||
self.btn_next.clicked.connect(self.next_clip)
|
|
||||||
self.btn_skip.clicked.connect(self.skip_clip)
|
|
||||||
self.btn_clear.clicked.connect(self.clear_mask)
|
|
||||||
self.btn_erase.clicked.connect(self.toggle_eraser)
|
|
||||||
self.btn_undo.clicked.connect(self.undo)
|
|
||||||
self.btn_reload.clicked.connect(self.reload_saved)
|
|
||||||
|
|
||||||
self.canvas.mpl_connect("button_press_event", self.on_press)
|
|
||||||
self.canvas.mpl_connect("motion_notify_event", self.on_move)
|
|
||||||
self.canvas.mpl_connect("button_release_event", self.on_release)
|
|
||||||
|
|
||||||
if self._pending_answers:
|
|
||||||
self._set_answers(self._pending_answers)
|
|
||||||
self._pending_answers = None
|
|
||||||
|
|
||||||
def _init_timer(self):
|
|
||||||
self.timer = QTimer()
|
|
||||||
self.timer.timeout.connect(self.update_frame)
|
|
||||||
self.timer.start(int(1000 / self.fps))
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# ANNOTATION
|
|
||||||
# ─────────────────────────────
|
|
||||||
def get_answers(self):
|
|
||||||
out = {}
|
|
||||||
for key, (group, buttons, options) in self.q_widgets.items():
|
|
||||||
for i, btn in enumerate(buttons):
|
|
||||||
if btn.isChecked():
|
|
||||||
out[key] = options[i]
|
|
||||||
return out
|
|
||||||
|
|
||||||
def stamp(self, x, y):
|
|
||||||
if x is None or y is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.history.append(self.mask.copy())
|
|
||||||
|
|
||||||
r = self.brush_slider.value()
|
|
||||||
ix, iy = int(x), int(y)
|
|
||||||
|
|
||||||
y0, y1 = max(0, iy - r), min(self.dh, iy + r + 1)
|
|
||||||
x0, x1 = max(0, ix - r), min(self.dw, ix + r + 1)
|
|
||||||
|
|
||||||
Y, X = np.ogrid[y0:y1, x0:x1]
|
|
||||||
circle = (X - ix) ** 2 + (Y - iy) ** 2 <= r**2
|
|
||||||
|
|
||||||
self.mask[y0:y1, x0:x1][circle] = 0 if self.erase_mode else 1
|
|
||||||
self.redraw_mask()
|
|
||||||
|
|
||||||
def redraw_mask(self):
|
|
||||||
rgba = np.zeros((self.dh, self.dw, 4))
|
|
||||||
rgba[..., 1] = self.mask * 0.7
|
|
||||||
rgba[..., 3] = self.mask * 0.4
|
|
||||||
|
|
||||||
self.mask_img.set_data(rgba)
|
|
||||||
self.canvas.draw_idle()
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# EVENTS
|
|
||||||
# ─────────────────────────────
|
|
||||||
def on_press(self, e):
|
|
||||||
if e.xdata is None:
|
|
||||||
return
|
|
||||||
self.drawing = True
|
|
||||||
self.stamp(e.xdata, e.ydata)
|
|
||||||
|
|
||||||
def on_move(self, e):
|
|
||||||
if self.drawing:
|
|
||||||
self.stamp(e.xdata, e.ydata)
|
|
||||||
|
|
||||||
def on_release(self, _):
|
|
||||||
self.drawing = False
|
|
||||||
|
|
||||||
def update_frame(self):
|
|
||||||
self.frame_i = (self.frame_i + 1) % len(self.frames)
|
|
||||||
self.img.set_data(self.frames[self.frame_i])
|
|
||||||
self.canvas.draw_idle()
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# HELPERS
|
|
||||||
# ─────────────────────────────
|
|
||||||
def _make_overlay(self, frame, alpha=0.4):
|
|
||||||
overlay = frame.copy()
|
|
||||||
green = np.zeros_like(frame)
|
|
||||||
green[..., 1] = 255
|
|
||||||
m = self.mask.astype(bool)
|
|
||||||
overlay[m] = (1 - alpha) * overlay[m] + alpha * green[m]
|
|
||||||
return overlay.astype(np.uint8)
|
|
||||||
|
|
||||||
def _save_gif(self, frames, out_path, scale=1.0):
|
|
||||||
h, w = frames[0].shape[:2]
|
|
||||||
nh, nw = max(1, int(h * scale)), max(1, int(w * scale))
|
|
||||||
pil_frames = [Image.fromarray(cv2.resize(f, (nw, nh))) for f in frames]
|
|
||||||
pil_frames[0].save(
|
|
||||||
out_path,
|
|
||||||
save_all=True,
|
|
||||||
append_images=pil_frames[1:],
|
|
||||||
duration=int(1000 / self.fps),
|
|
||||||
loop=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# ACTIONS
|
|
||||||
# ─────────────────────────────
|
|
||||||
def reload_saved(self):
|
|
||||||
out = self.out_dir / self.filename.stem
|
|
||||||
mask_path = out / "mask.png"
|
|
||||||
meta_path = out / "metadata.json"
|
|
||||||
|
|
||||||
if not mask_path.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
mask_full = np.array(Image.open(mask_path).convert("L"))
|
|
||||||
self.mask = cv2.resize(
|
|
||||||
(mask_full > 127).astype(np.uint8),
|
|
||||||
(self.dw, self.dh),
|
|
||||||
interpolation=cv2.INTER_NEAREST,
|
|
||||||
)
|
|
||||||
self.history = []
|
|
||||||
self.redraw_mask()
|
|
||||||
|
|
||||||
if meta_path.exists():
|
|
||||||
with open(meta_path) as f:
|
|
||||||
self._set_answers(json.load(f))
|
|
||||||
|
|
||||||
def clear_mask(self):
|
|
||||||
self.mask[:] = 0
|
|
||||||
self.redraw_mask()
|
|
||||||
|
|
||||||
def undo(self):
|
|
||||||
if self.history:
|
|
||||||
self.mask = self.history.pop()
|
|
||||||
self.redraw_mask()
|
|
||||||
|
|
||||||
def toggle_eraser(self):
|
|
||||||
self.erase_mode = not self.erase_mode
|
|
||||||
self.btn_erase.setText("Eraser ON" if self.erase_mode else "Eraser")
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
out = self.out_dir / self.filename.stem
|
|
||||||
out.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
mask_full = cv2.resize(
|
|
||||||
self.mask.astype(np.uint8),
|
|
||||||
(self.w, self.h),
|
|
||||||
interpolation=cv2.INTER_NEAREST,
|
|
||||||
)
|
|
||||||
|
|
||||||
Image.fromarray(mask_full * 255).save(out / "mask.png")
|
|
||||||
|
|
||||||
with open(out / "metadata.json", "w") as f:
|
|
||||||
json.dump(self.get_answers(), f, indent=2)
|
|
||||||
|
|
||||||
mid = len(self.frames) // 2
|
|
||||||
frame = self.frames[mid]
|
|
||||||
overlay_frame = self._make_overlay(frame)
|
|
||||||
Image.fromarray(frame).save(out / "frame.png")
|
|
||||||
Image.fromarray(overlay_frame).save(out / "overlay.png")
|
|
||||||
|
|
||||||
if self.extras:
|
|
||||||
Image.fromarray((self.mask * 255).astype(np.uint8)).save(out / "mask_vis.png")
|
|
||||||
|
|
||||||
overlay_frames = [self._make_overlay(f) for f in self.frames]
|
|
||||||
self._save_gif(self.frames, out / "video_original_hires.gif", scale=1.0)
|
|
||||||
self._save_gif(self.frames, out / "video_original_lowres.gif", scale=0.5)
|
|
||||||
self._save_gif(overlay_frames, out / "video_overlay_hires.gif", scale=1.0)
|
|
||||||
self._save_gif(overlay_frames, out / "video_overlay_lowres.gif", scale=0.5)
|
|
||||||
|
|
||||||
print("Saved:", out)
|
|
||||||
|
|
||||||
def next_clip(self):
|
|
||||||
self.save()
|
|
||||||
self._load_clip(next_day=self.daily)
|
|
||||||
|
|
||||||
self.frame_i = 0
|
|
||||||
self.img.set_data(self.frames[0])
|
|
||||||
self.title_text.set_text(self.filename.name)
|
|
||||||
self.redraw_mask()
|
|
||||||
|
|
||||||
if self._pending_answers:
|
|
||||||
self._set_answers(self._pending_answers)
|
|
||||||
self._pending_answers = None
|
|
||||||
|
|
||||||
def skip_clip(self):
|
|
||||||
self._load_clip(next_day=self.daily)
|
|
||||||
|
|
||||||
self.frame_i = 0
|
|
||||||
self.img.set_data(self.frames[0])
|
|
||||||
self.title_text.set_text(self.filename.name)
|
|
||||||
self.redraw_mask()
|
|
||||||
|
|
||||||
if self._pending_answers:
|
|
||||||
self._set_answers(self._pending_answers)
|
|
||||||
self._pending_answers = None
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# ENTRY POINT
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
def parse_args():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--data", default=r"C:\Users\sieverin\HydroScan\Code\river-annotation-tool\data\filtered_data")
|
|
||||||
parser.add_argument("--out", default="data/annotation_results/")
|
|
||||||
parser.add_argument("--clip", default=None, help="Stem name of a specific clip to load (e.g. 'left_20230501')")
|
|
||||||
parser.add_argument("--time", default=None, help="Target time to filter clips by day (format: HH:MM, e.g. '14:30'). Selects the closest clip to this time for each day.")
|
|
||||||
parser.add_argument("--daily", action="store_true", help="Load only 1 clip per day at the specified time (requires --time).")
|
|
||||||
parser.add_argument("--extras", action="store_true", help="Also save GIFs, frame PNG, overlay PNG, and mask_vis PNG alongside the mask.")
|
|
||||||
parser.add_argument("--skip-existing-day", action="store_true", help="In --daily mode, skip days that already have any annotated clip.")
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
args = parse_args()
|
|
||||||
|
|
||||||
app = QApplication([])
|
|
||||||
|
|
||||||
win = Annotator(Path(args.data), Path(args.out), clip=args.clip, target_time=args.time, daily=args.daily, extras=args.extras, skip_existing_day=args.skip_existing_day)
|
|
||||||
win.show()
|
|
||||||
|
|
||||||
app.exec()
|
|
||||||
345
uv.lock
generated
345
uv.lock
generated
@@ -7,6 +7,89 @@ resolution-markers = [
|
|||||||
"sys_platform != 'emscripten' and sys_platform != 'win32'",
|
"sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiobotocore"
|
||||||
|
version = "3.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiohttp" },
|
||||||
|
{ name = "aioitertools" },
|
||||||
|
{ name = "botocore" },
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "multidict" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "wrapt" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/75/42cce839c2ec263ff74b10b650fe36b066fbb124cbee6f247eac0983e1ab/aiobotocore-3.7.0.tar.gz", hash = "sha256:c64d871ed5491a6571948dd48eabd185b46c6c23b64e3afd0c059fc7593ada30", size = 127054, upload-time = "2026-05-09T10:02:52.332Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl", hash = "sha256:680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e", size = 89539, upload-time = "2026-05-09T10:02:50.389Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiohappyeyeballs"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiohttp"
|
||||||
|
version = "3.13.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiohappyeyeballs" },
|
||||||
|
{ name = "aiosignal" },
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "frozenlist" },
|
||||||
|
{ name = "multidict" },
|
||||||
|
{ name = "propcache" },
|
||||||
|
{ name = "yarl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aioitertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosignal"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "frozenlist" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "4.13.0"
|
version = "4.13.0"
|
||||||
@@ -141,6 +224,20 @@ css = [
|
|||||||
{ name = "tinycss2" },
|
{ name = "tinycss2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "botocore"
|
||||||
|
version = "1.43.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/28/79/2f4be1896db3db7ccf44504253a175d56b6bd6b669619edc5147d1aa21ea/botocore-1.43.0.tar.gz", hash = "sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928", size = 15286817, upload-time = "2026-04-29T22:07:31.723Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl", hash = "sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6", size = 14970102, upload-time = "2026-04-29T22:07:27Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.4.22"
|
version = "2026.4.22"
|
||||||
@@ -207,6 +304,48 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clip-annotator"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "matplotlib" },
|
||||||
|
{ name = "matplotlib-inline" },
|
||||||
|
{ name = "opencv-contrib-python-headless" },
|
||||||
|
{ name = "pandas" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pyside6" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "s3fs" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "notebook" },
|
||||||
|
{ name = "pre-commit" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "matplotlib", specifier = ">=3.10.8" },
|
||||||
|
{ name = "matplotlib-inline", specifier = ">=0.2.1" },
|
||||||
|
{ name = "opencv-contrib-python-headless", specifier = "==4.12.0.88" },
|
||||||
|
{ name = "pandas", specifier = ">=2.3.3" },
|
||||||
|
{ name = "pillow", specifier = ">=12.2.0" },
|
||||||
|
{ name = "pyside6", specifier = ">=6.11.0" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.0" },
|
||||||
|
{ name = "pyyaml", specifier = ">=6.0" },
|
||||||
|
{ name = "s3fs", specifier = ">=2024.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "notebook", specifier = "~=7.5" },
|
||||||
|
{ name = "pre-commit", specifier = "~=4.5" },
|
||||||
|
{ name = "ruff", specifier = "==0.15.0" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -349,6 +488,40 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "frozenlist"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fsspec"
|
||||||
|
version = "2026.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -498,6 +671,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jmespath"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "json5"
|
name = "json5"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -811,6 +993,33 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multidict"
|
||||||
|
version = "6.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nbclient"
|
name = "nbclient"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -1072,6 +1281,32 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "propcache"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psutil"
|
name = "psutil"
|
||||||
version = "7.2.2"
|
version = "7.2.2"
|
||||||
@@ -1206,6 +1441,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-json-logger"
|
name = "python-json-logger"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -1326,42 +1570,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" },
|
{ url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "river-annotation-tool"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "matplotlib" },
|
|
||||||
{ name = "matplotlib-inline" },
|
|
||||||
{ name = "opencv-contrib-python-headless" },
|
|
||||||
{ name = "pandas" },
|
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "pyside6" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "notebook" },
|
|
||||||
{ name = "pre-commit" },
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "matplotlib", specifier = ">=3.10.8" },
|
|
||||||
{ name = "matplotlib-inline", specifier = ">=0.2.1" },
|
|
||||||
{ name = "opencv-contrib-python-headless", specifier = "==4.12.0.88" },
|
|
||||||
{ name = "pandas", specifier = ">=2.3.3" },
|
|
||||||
{ name = "pillow", specifier = ">=12.2.0" },
|
|
||||||
{ name = "pyside6", specifier = ">=6.11.0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [
|
|
||||||
{ name = "notebook", specifier = "~=7.5" },
|
|
||||||
{ name = "pre-commit", specifier = "~=4.5" },
|
|
||||||
{ name = "ruff", specifier = "==0.15.0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpds-py"
|
name = "rpds-py"
|
||||||
version = "0.30.0"
|
version = "0.30.0"
|
||||||
@@ -1410,6 +1618,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
|
{ url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "s3fs"
|
||||||
|
version = "2026.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiobotocore" },
|
||||||
|
{ name = "aiohttp" },
|
||||||
|
{ name = "fsspec" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/d8/76f3dc1558bdf4494b117a9f7a9cc0a5d9d34edadc9e5d7ceabc5a6a7c37/s3fs-2026.4.0.tar.gz", hash = "sha256:5bdce0abb00b0435ee150807a45fea727451dbc22de4cbc116464f8504ab9d37", size = 85986, upload-time = "2026-04-29T20:52:51.748Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/a4/9d1ea10ebc9e028a289a72fec84da170689549a8102c8aacfcad26bc5035/s3fs-2026.4.0-py3-none-any.whl", hash = "sha256:de0d2a1f33cdf03831fd2382d278c6e4e31fe57c3bf2f703c61f8aec6b703e2a", size = 32392, upload-time = "2026-04-29T20:52:50.295Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "send2trash"
|
name = "send2trash"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@@ -1610,3 +1832,54 @@ sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
|
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wrapt"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yarl"
|
||||||
|
version = "1.24.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "multidict" },
|
||||||
|
{ name = "propcache" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user