The 2024 video set in all_video_info_merged.xlsx covers 63 (date, machine)
sessions — 129 video instances — that have no auto-detectable targets, so
ROI placement requires manual reference-point selection. This commit adds
the three-stage pipeline that lets a user click for an hour, then walk
away while the tracker grinds overnight:
1. build_video_inventory.py — scan /mnt/ethoscope_data/videos/ and join
against the xlsx, producing data/metadata/video_inventory.csv
2. pick_targets.py — interactive matplotlib/Tk picker. User clicks
TOP/CORNER/LEFT (the L-shape ethoscope expects); after the third
click the 6 ROI rectangles are drawn on top of the frame so geometry
can be verified before saving. Also supports marking a video
'unusable' (FOV wrong) so it's permanently skipped, frame stepping
by ±1s/±5%/midpoint, point editing in --redo mode, and a crosshair
cursor that survives matplotlib's per-motion cursor reset.
3. track_videos.py — headless batch tracker. Reads the JSON sidecars,
builds 6 ROIs from the HD-mating-arena geometry, runs MultiFlyTracker
against the merged.mp4 via MovieVirtualCamera, writes SQLite DBs to
data/tracked/. Idempotent (skips done DBs), parallel via --jobs,
subclasses MovieVirtualCamera so frames stay BGR (MultiFlyTracker
calls cvtColor(BGR2GRAY) without checking channel count).
Plus auto_detect_targets.py (fallback that runs ethoscope's auto-detector
in case any videos do have visible target dots), monitor_tracking.py
(progress + ETA from data/tracked/ ground truth, --watch for live view),
and tracking_geometry.py (single source of truth for the affine math
shared by picker and tracker).
requirements-tracking.txt pins the extra deps (opencv-python, openpyxl,
gitpython, netifaces, mysql-connector-python) — these are only needed
for the tracking pipeline, not the existing analysis notebooks.
Verified end-to-end on one of the user-picked videos: ~4000 rows/ROI in
a 120s slice, fly bounding boxes in the expected 800-2000 px² band.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
71 lines
2.2 KiB
Python
71 lines
2.2 KiB
Python
"""Shared HD-mating-arena ROI geometry, used by both pick_targets.py
|
|
(for live overlay) and track_videos.py (for actual tracking).
|
|
|
|
Pure numpy + cv2; no ethoscope dependency.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import itertools
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
# Layout from
|
|
# ethoscope/.../roi_builders/roi_templates/builtin/HD_Mating_Arena_6_ROIS.json
|
|
HD_MATING_ARENA = {
|
|
"n_rows": 2,
|
|
"n_cols": 3,
|
|
"top_margin": -0.21,
|
|
"bottom_margin": -0.13,
|
|
"left_margin": 0.05,
|
|
"right_margin": 0.05,
|
|
"horizontal_fill": 0.85,
|
|
"vertical_fill": 1.3,
|
|
}
|
|
|
|
HD_FG_DATA = {
|
|
"sample_size": 400,
|
|
"normal_limits": [800, 2000],
|
|
"tolerance": 0.8,
|
|
}
|
|
|
|
|
|
def compute_roi_polygons(reference_points, layout=HD_MATING_ARENA):
|
|
"""Map 3 L-shape reference points to 6 ROI polygons, in the order ROI 1..6.
|
|
|
|
Reference points must be ordered:
|
|
[TOP, CORNER, LEFT]
|
|
matching ethoscope's dst_points = [(0, -1), (0, 0), (-1, 0)].
|
|
|
|
Returns:
|
|
list[np.ndarray] # 6 arrays, each shape (4, 2), int32, in image coords
|
|
"""
|
|
ref = np.asarray(reference_points, dtype=np.float32)
|
|
if ref.shape != (3, 2):
|
|
raise ValueError(f"reference_points must be 3x2, got shape {ref.shape}")
|
|
|
|
dst_points = np.array([(0, -1), (0, 0), (-1, 0)], dtype=np.float32)
|
|
wrap_mat = cv2.getAffineTransform(dst_points, ref)
|
|
|
|
n_col = layout["n_cols"]
|
|
n_row = layout["n_rows"]
|
|
tm, bm = layout["top_margin"], layout["bottom_margin"]
|
|
lm, rm = layout["left_margin"], layout["right_margin"]
|
|
hf, vf = layout["horizontal_fill"], layout["vertical_fill"]
|
|
|
|
y_positions = (np.arange(n_row) * 2.0 + 1) * (1 - tm - bm) / (2 * n_row) + tm
|
|
x_positions = (np.arange(n_col) * 2.0 + 1) * (1 - lm - rm) / (2 * n_col) + lm
|
|
centres = [np.array([x, y]) for x, y in itertools.product(x_positions, y_positions)]
|
|
sign_mat = np.array([[-1, -1], [+1, -1], [+1, +1], [-1, +1]])
|
|
xy_size = np.array([hf / float(n_col), vf / float(n_row)]) / 2.0
|
|
rectangles = [sign_mat * xy_size + c for c in centres]
|
|
|
|
shift = np.dot(wrap_mat, [1, 1, 0]) - ref[1]
|
|
|
|
polys = []
|
|
for r in rectangles:
|
|
r3 = np.append(r, np.zeros((4, 1)), axis=1)
|
|
mapped = np.dot(wrap_mat, r3.T).T - shift
|
|
polys.append(mapped.astype(np.int32))
|
|
return polys
|