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:
.envis populated withROOT_DIR,LOCAL_DIR, andSOURCE_VIDEOS_DIR(used byconfig.py).- Source videos exist under
SOURCE_VIDEOS_DIR. - A test split CSV exists under
data/processed/<split_name>/test.csv, produced by runningscripts/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 baselineinstead 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 |
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:
|
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. |
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 |
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. |
required |
model_path
|
str
|
YOLO weights filename (e.g. |
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,
)