Skip to content

Baseline Cross-Sucking Detector

This page documents how to use scripts/run_models/baseline/baseline.py to detect cross-sucking behaviour in calf videos using YOLO bounding box overlap, and write structured JSON event metadata for downstream use.


Summary

The baseline detector provides a deliberately naive lower-bound reference for cross-sucking detection. It runs a COCO-pretrained YOLO26 model across each video in a test split CSV, computing pairwise IoU between all detected calf bounding boxes in each frame. Consecutive frames where overlap exceeds a minimum threshold are grouped into events, and events shorter than a minimum duration are discarded as noise.

This detector operates on proximity alone and has no understanding of cross-sucking as a behaviour. Its purpose is to provide a reference point against which fine-tuned model performance can be meaningfully compared. Any improvement from the fine-tuned YOLO26 or Seq-NMS models is meaningful precisely because it demonstrates better performance than simply flagging every sufficiently long calf contact as cross-sucking.

One JSON metadata file is written per video containing event timestamps, confidence scores, and per-frame intersection box coordinates for downstream clipping and analysis.


Environment Setup

Before running the baseline script, ensure your local workspace satisfies the following:

  • .env is populated with ROOT_DIR, LOCAL_DIR, and SOURCE_VIDEOS_DIR (used by config.py).
  • Source videos exist under SOURCE_VIDEOS_DIR.
  • A test split CSV exists under data/processed/<split_name>/test.csv, produced by running scripts/split_data/split_data.py.

Output

Results are saved to results/metadata/<split_name>/baseline/ automatically. The split name is inferred from the parent folder of the input CSV (e.g. day_based).

results/
└── metadata/
    └── <split_name>/
        └── baseline/
            ├── ch02_20250913094601_results.json
            └── ch02_20250913094602_results.json

Videos whose JSON already exists are skipped automatically, making the script safe to re-run after interruptions.


Usage

Tip: You can run this step with make baseline instead of the commands below. See Running the Pipeline (Makefile) for details.

Basic usage

uv run scripts/run_models/baseline/baseline.py --csv data/processed/day_based/test.csv

Run with frame skipping for faster processing

uv run scripts/run_models/baseline/baseline.py \
  --csv data/processed/day_based/test.csv \
  --frame_skip 30

Run with custom thresholds

uv run scripts/run_models/baseline/baseline.py \
  --csv data/processed/day_based/test.csv \
  --iou_threshold 0.05 \
  --conf_threshold 0.4 \
  --min_duration 2.0

Specifying a different split

The split name is inferred from the parent folder of the CSV. To run on a different split, pass the corresponding test CSV:

uv run scripts/run_models/baseline/baseline.py \
  --csv data/processed/pen_based/pen_2/test.csv

Arguments

Argument Type Default Description
--csv str required Path to a split test CSV (e.g. data/processed/day_based/test.csv)
--model str yolo26x.pt YOLO model weights filename
--iou_threshold float 0.1 Minimum IoU overlap to flag a frame
--conf_threshold float 0.5 Minimum YOLO detection confidence to keep a box
--min_duration float 0.5 Minimum event duration in seconds
--frame_skip int 1 Process every Nth frame

Function Reference

scripts.run_models.baseline.baseline

Baseline cross-sucking detector using YOLO bounding box overlap.

compute_iou(box_a, box_b)

Compute Intersection over Union (IoU) between two bounding boxes and return the pixel coordinates of their intersection rectangle.

Parameters:

Name Type Description Default
box_a list

[x1, y1, x2, y2] coordinates of box A.

required
box_b list

[x1, y1, x2, y2] coordinates of box B.

required

Returns:

Name Type Description
iou float

IoU score in [0, 1]. 0.0 means no overlap; 1.0 means identical boxes.

intersection_box list or None

[x1, y1, x2, y2] pixel coordinates of the overlapping region, or None if the boxes do not overlap.

Examples:

.. code-block:: python

iou, box = compute_iou([0, 0, 10, 10], [5, 5, 15, 15])
print(iou)   # 0.142...
print(box)   # [5, 5, 10, 10]

frame_has_overlap(boxes, iou_threshold)

Check whether any two bounding boxes in a single frame overlap above the IoU threshold.

Checks every possible pair of detected calf boxes and tracks the pair with the highest IoU. Returns that pair's intersection box for storage in the event metadata.

Parameters:

Name Type Description Default
boxes list

List of [x1, y1, x2, y2] bounding boxes for all detected calves in this frame.

required
iou_threshold float

Minimum IoU score to consider an overlap.

required

Returns:

Name Type Description
overlap_detected bool

True if any pair of boxes exceeds the IoU threshold.

best_inter_box list or None

[x1, y1, x2, y2] of the intersection region of the highest-IoU pair, or None if no overlap was found.

Examples:

.. code-block:: python

boxes = [[0, 0, 10, 10], [5, 5, 15, 15], [100, 100, 200, 200]]
detected, box = frame_has_overlap(boxes, iou_threshold=0.1)
print(detected)  # True
print(box)       # [5, 5, 10, 10]

extract_events(frame_flags, fps, min_duration, frame_skip, confidences, frame_boxes, frame_indices)

Group consecutive flagged frames into discrete cross-sucking events.

A frame is flagged when its IoU exceeds the overlap threshold. Consecutive flagged frames are merged into a single event. Events shorter than min_duration seconds are discarded as noise.

Parameters:

Name Type Description Default
frame_flags list of bool

Per-frame overlap flag. True when bounding-box overlap was detected.

required
fps float

Frames per second of the source video, used to convert frame numbers to timestamps in seconds.

required
min_duration float

Minimum event length in seconds. Events below this threshold are dropped.

required
frame_skip int

Frame-skip factor used during inference, needed to compute the minimum frame count threshold correctly.

required
confidences list of float

Per-frame maximum YOLO detection confidence (0.0 when no detections).

required
frame_boxes list

Per-frame intersection box [x1, y1, x2, y2], or None for frames where no overlap was detected. Must be the same length as frame_flags.

required
frame_indices list of int

Actual frame indices in the source video, accounting for frame skip.

required

Returns:

Type Description
list of dict

Each dict contains:

  • start_sec (float): Event start time in seconds.
  • end_sec (float): Event end time in seconds.
  • duration_sec (float): Total duration of the event.
  • avg_confidence (float): Mean YOLO confidence across event frames.
  • intersection_box (list): Per-frame dicts with keys frame, x1, y1, x2, y2.
Notes

An event that runs to the very last frame of the video is flushed and saved even if no explicit end flag is encountered.

load_split_from_csv(csv_path)

Load video paths from a single split CSV.

Parameters:

Name Type Description Default
csv_path Path

Path to a test.csv file (e.g. data/processed/day_based/test.csv).

required

Returns:

Type Description
list of Path

Resolved absolute video paths, deduplicated.

Raises:

Type Description
ValueError

If the CSV is empty after deduplication.

Examples:

.. code-block:: python

from pathlib import Path
from scripts.run_models.baseline.baseline import load_split_from_csv

paths = load_split_from_csv(Path("data/processed/day_based/test.csv"))
print(paths[0])  # /path/to/source/video.mp4

detect_video(video_path, model, target_ids, iou_threshold, conf_threshold, min_duration, frame_skip)

Run the full baseline detection pipeline on a single video file.

Opens the video, detects calves per frame using YOLO, computes pairwise IoU overlap, groups consecutive flagged frames into events, and returns a metadata dict ready to be written to JSON.

Parameters:

Name Type Description Default
video_path Path

Absolute path to the input video file.

required
model YOLO

Loaded YOLO model instance.

required
target_ids set

Set of YOLO class IDs corresponding to the target animal class.

required
iou_threshold float

Minimum IoU to flag a frame as containing an overlap event.

required
conf_threshold float

Minimum YOLO detection confidence to keep a bounding box.

required
min_duration float

Minimum event duration in seconds; shorter events are discarded.

required
frame_skip int

Process every Nth frame. Frame indices in output reflect actual source video frame numbers.

required

Returns:

Type Description
dict

Metadata dict containing detection parameters and per-event results. Written directly to JSON by run_all. Fields include identifier, video_path, model, iou_threshold, conf_threshold, min_duration_sec, fps, total_frames, total_duration_sec, frame_skip, cross_sucking_detected, num_events, and events.

Raises:

Type Description
FileNotFoundError

If the video file cannot be opened by OpenCV.

run_all(csv_path, model_path, iou_threshold, conf_threshold, min_duration, frame_skip)

End-to-end pipeline: load split CSV → run detection → save JSON metadata.

Iterates over every unique video in the split CSV, runs detect_video on each, and writes one JSON metadata file per video to the baseline metadata directory. Videos whose output JSON already exists are skipped automatically, making this function safe to re-run after interruptions.

Parameters:

Name Type Description Default
csv_path Path

Path to a split test CSV (e.g. data/processed/day_based/test.csv).

required
model_path str

YOLO weights filename (e.g. 'yolo26x.pt'). Downloaded automatically on first run if not found locally.

required
iou_threshold float

IoU threshold to flag a frame as containing an overlap event.

required
conf_threshold float

Minimum YOLO detection confidence to keep a bounding box.

required
min_duration float

Minimum event duration in seconds.

required
frame_skip int

Process every Nth frame (1 = every frame).

required

Returns:

Type Description
None

Writes JSON metadata files to disk and does not return anything.

Raises:

Type Description
ValueError

If the target class is not present in the loaded model.

Examples:

.. code-block:: python

from pathlib import Path
from scripts.run_models.baseline.baseline import run_all

run_all(
    csv_path=Path("data/processed/day_based/test.csv"),
    model_path="yolo26x.pt",
    iou_threshold=0.1,
    conf_threshold=0.5,
    min_duration=1.0,
    frame_skip=30,
)