Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ holds reusable library modules.
| 1. Data | [data/README.md](data/README.md) | — |
| 2. Tracking | [docs/2_tracking.md](docs/2_tracking.md) | `tracker`, `gs2` |
| 3. Tracker evaluation _(optional)_ | [docs/3_tracker_eval.md](docs/3_tracker_eval.md) | `tracker`, `gs2` |
| 4. Postprocessing | [docs/4_postprocessing.md](docs/4_postprocessing.md) | `tracker` |
| 4. Postprocessing | [docs/4_postprocessing.md](docs/4_postprocessing.md) | `default` |
| 5. Build dataset | [docs/5_dataset.md](docs/5_dataset.md) | `default` |
| 6. Embeddings | [docs/6_embeddings.md](docs/6_embeddings.md) | `default`, `videoprism` |
| 7. Classification | [docs/7_classification.md](docs/7_classification.md) | `default` |
Expand All @@ -87,8 +87,12 @@ pixi run test_val_rotation # Cross-validation tests
pixi run test_tracking_postprocessing # Tracking postprocessing unit tests
pixi run test_tracker # Tracker unit tests (requires CUDA)
pixi run test_dataset_integrity # Data integrity checks (requires a built dataset)
pixi run test_imports_full # Import every src/ and pipeline/ module with all deps (run before a release)
```

CI runs `pixi run -e ci test` in a lightweight environment without torch, so
modules that import torch are skipped there. `test_imports_full` covers them.

## Citation

```bibtex
Expand Down
2 changes: 1 addition & 1 deletion config/sam3_best.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ max_frames_to_track: 0 # set to 0 to track the entire video

# --- Mode 1: Reuse a previous run's chunk boundaries ---
reuse_chunk_info: false
reuse_run_dir: null # e.g. "data/results/tracking/sam3_best/day_28/20260306_152733_sam3_hf"
reuse_run_dir: null # e.g. "data/results/tracking/sam3_best/day_28/{video_stem}"

# --- Mode 3/4: Adaptive or fixed chunking (active when reuse_chunk_info: false) ---
yolo_scan_only: false
Expand Down
16 changes: 16 additions & 0 deletions docs/2_tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ data/results/tracking/{config_stem}/
| **Manual** | `manual_chunk_frames: [[0,375], ...]` | Explicit `[start, end]` frame pairs. |
| **Reuse** | `reuse_chunk_info: true` + `reuse_run_dir` | Load boundaries from a previous run. |

## Grounding (SAM 3)

At each chunk boundary, a text prompt (`text_prompt: "bird"`) searches
`grounding_frames` frames for the best initialisation frame. IDs are matched
across chunks by mask IoU (`id_match_iou_threshold`). If grounding fails,
falls back to the previous chunk's masks (`fallback_to_prev_chunk`).

Optional keys (off by default in every config):

| Key | Effect |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `text_grounding.fill_grounding_gap: true` | Fill the frames before the chosen grounding frame with the grounding model's own outputs. The day-28 production runs used this. |
| `text_grounding.best_overlap_id_matching: true` | Pick the previous-chunk reference frame by mask overlap instead of the latest frame with enough objects. Legacy; no production run. |
| `text_grounding.id_match_min_ratio` (0.5) | Chunks whose ID match ratio falls below this are flagged with `id_discontinuity` in `chunk_info.json`. |
| `frame_loader` (`torchcodec`) | Frame decoder: `torchcodec`, `cv2_seek` or `cv2_sequential`. The day-28 runs predate torchcodec and need `cv2_seek` to reproduce. |

## Recomputing chunk boundaries

Recompute YOLO scan metrics and chunk boundaries from an existing run without
Expand Down
49 changes: 25 additions & 24 deletions docs/6_embeddings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

| Script | Env | Description |
| ------------------------------------------- | ------------ | -------------------------------------------- |
| `pipeline/extract_embeddings_dinov3.py` | `embeddings` | DINOv3 CLS-token embeddings (image backbone) |
| `pipeline/extract_embeddings_vjepa2.py` | `embeddings` | V-JEPA 2 / 2.1 video embeddings |
| `pipeline/extract_embeddings_dinov3.py` | `default` | DINOv3 CLS-token embeddings (image backbone) |
| `pipeline/extract_embeddings_vjepa2.py` | `default` | V-JEPA 2 / 2.1 video embeddings |
| `pipeline/extract_embeddings_videoprism.py` | `videoprism` | VideoPrism video embeddings (JAX) |

All three scripts read `tracks.parquet` from the dataset dir, load video frames,
Expand Down Expand Up @@ -52,9 +52,8 @@ loaded via `torch.hub`. The setup script also patches a namespace collision
between the hub repo's `src/` directory and this project's own `src/` package.

```sh
# Download checkpoint + patch hub cache (run once)
bash scripts/setup_vjepa2.1.sh # default: vjepa2_1_vit_large_384
bash scripts/setup_vjepa2.1.sh vjepa2_1_vit_base_384 # ViT-B variant
# Download ViT-B + ViT-L checkpoints and patch hub cache (run once)
bash scripts/setup_vjepa2.1.sh
```

Available models:
Expand All @@ -81,8 +80,8 @@ Output: `embeddings_vjepa21_vitl_temporal.pt` (name auto-generated from args).

## VideoPrism (JAX)

Runs in its own `videoprism` pixi env (JAX + TensorFlow, separate from the
PyTorch `embeddings` env). TF is imported only to be blocked from grabbing the
Runs in its own `videoprism` pixi env (JAX + TensorFlow, solved separately
from the PyTorch envs). TF is imported only to be blocked from grabbing the
Comment on lines +83 to +84
GPU — the actual inference runs in JAX.

No extra setup: model weights download automatically on first run via the
Expand All @@ -104,20 +103,22 @@ Output: `embeddings_videoprism_temporal.pt` or `embeddings_videoprism_raw.pt`.

## Crop modes

All three scripts support `--crop-mode` to control what region of the frame is
extracted:

| Mode | Description |
| ----------------------- | --------------------------------------------------------------- |
| `bbox` | Per-frame bounding box (default) |
| `plain256` / `plain384` | Fixed square around the bbox centroid |
| `union512` / `union384` | Fixed square around the centroid of all three birds' union bbox |
| `maskbbox` | Bbox crop with non-bird pixels blacked out |
| `dimbbox` | Same, but dimmed to 40% |
| `silbbox` | Flat silhouette (shape only, no texture) |

Mask-based modes (`maskbbox`, `dimbbox`, `silbbox`) require the RLE mask columns
in `tracks.parquet` and are matched controls against the default `bbox` crop.

The crop mode is appended to the output filename: e.g.
`embeddings_vjepa21_vitl_maskbbox_temporal.pt`.
`--crop-mode` controls which part of the frame is fed to the backbone. Not every
script accepts every mode:

| Mode | Description | DINOv3 | V-JEPA 2 | VideoPrism |
| ----------- | ------------------------------------------------------------------------------------------- | :----: | :------: | :--------: |
| `bbox` | Per-frame bounding box crop (default) | ✓ | ✓ | ✓ |
| `plain256` | Fixed 256×256 square around the bird's bbox centroid, per frame | ✓ | ✓ | ✓ |
| `plain384` | Same, 384×384 | | ✓ | ✓ |
| `union512` | Fixed 512×512 square around the centre of the bird's bboxes, same for the whole window | ✓ | ✓ | ✓ |
| `union384` | Same, 384×384 | | ✓ | ✓ |
| `union` | Union of the bird's bboxes in the window, resized to `--frame-size` | | | ✓ |
| `darken512` | `union512` with everything outside the bird's bbox dimmed to 40% _(untested)_ | ✓ | ✓ | ✓ |
| `roi512` | `union512`; VideoPrism then pools only the patch tokens inside the bird's bbox _(untested)_ | ✓ | ✓ | ✓ |

`roi512` only differs from `union512` in VideoPrism; DINOv3 and V-JEPA 2 ignore
the ROI patch indices.

Any mode other than `bbox` is appended to the output filename, e.g.
`embeddings_vjepa21_vitl_union512_temporal.pt`.
5 changes: 3 additions & 2 deletions pipeline/build_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
Typical workflow::

# First run: generates JSONs in data/postprocessing/
pixi run -e tracker python -m pipeline.build_dataset
pixi run build_dataset

# Manually fill in "to" values in each tracking_postprocessing.json

# Second run: validates remaps, builds dataset
pixi run -e tracker python -m pipeline.build_dataset
pixi run build_dataset
"""

import json
Expand Down Expand Up @@ -162,6 +162,7 @@ def process_tracking_subdir(tracking_dir, pp_dir, bird_info):


def save_data(save_dict, output_dir):
"""Save a dict of DataFrames/JSON to an output directory (parquet or JSON by extension)."""
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
for fname, data in save_dict.items():
Expand Down
42 changes: 33 additions & 9 deletions pipeline/compute_chunk_boundaries.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""Recompute chunk boundary metrics from an existing run directory.

Given a run dir that contains ``yolo_tracking.parquet``, recomputes per-frame
metrics, adaptive chunk boundaries, and saves updated outputs in-place.
metrics, adaptive chunk boundaries, and saves updated outputs in-place:

- metrics/yolo_scan_metrics.parquet, metrics/yolo_scan_summary.parquet
- chunk_info.json
- visualizations/yolo_scan_overview.png
- visualizations/chunk_boundaries.png (frame screengrab grid, requires video)

Usage::

Expand Down Expand Up @@ -104,6 +109,16 @@ def parse_args():
default=None,
help="Override YAML config; defaults to the .yaml found in --run-dir",
)
parser.add_argument(
"--video-path",
default=None,
help="Explicit path to source video (for boundary frame grid)",
)
parser.add_argument(
"--video-dir",
default=None,
help="Directory containing source videos (matched by run dir name)",
)
return parser.parse_args()


Expand Down Expand Up @@ -265,16 +280,25 @@ def main():
logger.info(f"Saved: {viz_dir / 'yolo_scan_overview.png'}")

# -------------------------------------------------------------------------
# 11. Generate chunk_boundaries_<run>.png (N rows × 2 cols: start + end)
# 11. Generate chunk_boundaries.png (N rows × 2 cols: start + end)
# -------------------------------------------------------------------------
video_path = cfg.get("video_path")
plot_chunk_boundary_frames(
chunk_info=chunk_info,
video_path=video_path,
fps=fps,
yolo_scan_df=yolo_scan_metrics_df,
save_path=viz_dir / f"chunk_boundaries_{run_dir.stem}.png",
video_path = _resolve_video_path(
run_dir,
video_path_override=Path(args.video_path) if args.video_path else None,
video_dir_override=Path(args.video_dir) if args.video_dir else None,
)
if video_path is not None:
boundary_path = viz_dir / "chunk_boundaries.png"
plot_chunk_boundary_frames(
chunk_info=chunk_info,
video_path=video_path,
fps=fps,
yolo_scan_df=yolo_scan_metrics_df,
save_path=boundary_path,
)
logger.info(f"Saved: {boundary_path}")
else:
logger.warning("Skipping chunk_boundaries.png (no video found)")


if __name__ == "__main__":
Expand Down
6 changes: 1 addition & 5 deletions pipeline/eval_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import json
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from glob import glob
from pathlib import Path

import numpy as np
Expand Down Expand Up @@ -48,12 +47,9 @@ def _load_cfg(run_dir: Path) -> dict:

def _build_dm_and_model_cls(cfg: dict):
"""Reconstruct datamodule and model class from a run config."""
import torch

from src.classification.datamodule import BehaviourDataModule
from src.classification.model_selection import LOCO
from src.classification.models import MODEL_REGISTRY
from src.classification.trainer import BehaviourClassifier
from src.classification.utils import parse_input

use_features, use_embeddings, embeddings_files = parse_input(cfg["input"])
Expand Down Expand Up @@ -113,7 +109,7 @@ def evaluate_run(run_dir: Path, device: str = "cuda:0", predictions: bool = True
import torch
import torch.nn.functional as F

from src.classification.stats import _save_per_fold_cms, _save_recall_txt
from src.classification.stats import _save_recall_txt

run_dir = Path(run_dir)
cfg = _load_cfg(run_dir)
Expand Down
12 changes: 2 additions & 10 deletions pipeline/extract_embeddings_videoprism.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
Usage::

pixi run -e videoprism extract_videoprism \
--video-dir data/video/batch data/video/batch2 --device 0
--video-dir data/videos/day_28 data/videos/day_29 --device 0

# Save raw patch tokens for trainable pooler
pixi run -e videoprism extract_videoprism \
--video-dir data/video/batch data/video/batch2 --device 0 --raw
--video-dir data/videos/day_28 data/videos/day_29 --device 0 --raw
"""

import os
Expand All @@ -36,16 +36,8 @@
import torch

from loguru import logger
from PIL import Image
from tqdm import tqdm
from videoprism import models as vp

# Lazy torch import — only needed for saving .pt files
import torch

# Block TensorFlow from grabbing GPU
import tensorflow as tf

tf.config.set_visible_devices([], "GPU")
tf.config.set_visible_devices([], "TPU")

Expand Down
22 changes: 16 additions & 6 deletions pipeline/extract_embeddings_vjepa2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@

V-JEPA 2.1 setup (one-time)::

bash scripts/setup_vjepa2.1.sh # default: vjepa2_1_vit_large_384
bash scripts/setup_vjepa2.1.sh <model> # other variants
bash scripts/setup_vjepa2.1.sh

This downloads the checkpoint and patches the torch.hub cache to rename
This downloads the ViT-B and ViT-L checkpoints and patches the torch.hub cache to rename
``src/`` to ``vjepa2/`` (avoids collision with this project's ``src/``).
"""

Expand All @@ -30,7 +29,7 @@
DEFAULT_TRACKING_DIR,
DEFAULT_VIDEO_DIR,
)
from src.dataset.crops import CROP_MODES, needs_mask
from src.dataset.crops import CROP_MODES
from src.dataset.embeddings.vjepa2 import (
VJEPA21Wrapper,
extract_video_embeddings,
Expand Down Expand Up @@ -112,6 +111,11 @@ def parse_args():
default=None,
help="Output filename (default: auto-generated from model name)",
)
parser.add_argument(
"--resume",
action="store_true",
help="Reuse an existing output file and skip videos already extracted",
)
parser.add_argument(
"--cid-checkpoint",
type=Path,
Expand Down Expand Up @@ -229,10 +233,16 @@ def main():
output_path = (
Path(output_name) if "/" in str(output_name) else args.dataset_dir / output_name
)
save_incremental = args.raw

all_embeddings = {}
if args.resume and not args.dry_run and output_path.exists():
all_embeddings = torch.load(output_path, map_location="cpu", weights_only=False)
logger.info(f"Resuming from {output_path}: {len(all_embeddings)} embeddings")
done_videos = {k[0] for k in all_embeddings}

for video_id in video_ids:
if video_id in done_videos:
logger.info(f"--- Skipping {video_id} (already extracted) ---")
continue
logger.info(f"--- Processing video: {video_id} ---")
video_path = resolve_video_path(video_id, args.tracking_dir, args.video_dir)
if video_path is None:
Expand Down
9 changes: 8 additions & 1 deletion pipeline/run_tracker.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
"""Unified launcher for tracking pipelines (SAM 3, Grounded-SAM-2)."""
"""Unified launcher for tracking pipelines (SAM 3, Grounded-SAM-2).

Usage:
# Run with default config (config/sam3_best.yaml)
pixi run -e tracker track_best
# Run with custom config
pixi run -e tracker python -m pipeline.run_tracker --config config/sam3_baseline.yaml
"""

import os

Expand Down
2 changes: 1 addition & 1 deletion pipeline/train_ablate_best.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Each run changes one hyperparameter from the reference config.
#
# Usage:
# pixi run ablate-best [-- --device cuda:0 --dry-run]
# pixi run train_ablate_best [--device cuda:0] [--dry-run]
# bash pipeline/train_ablate_best.sh [--device cuda:0] [--dry-run]

set -euo pipefail
Expand Down
2 changes: 1 addition & 1 deletion pipeline/train_sweep_k.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Embeddings-only, TemporalCNNv2, excluding social.
#
# Usage:
# pixi run sweep-k [-- --device cuda:0 --dry-run]
# pixi run train_sweep_k [--device cuda:0] [--dry-run]
# bash pipeline/train_sweep_k.sh [--device cuda:0] [--dry-run]

set -euo pipefail
Expand Down
Loading
Loading