Skip to content

Pipeline API

Object Tracking

body_eye_sync.pipeline.object_tracking

Object tracking using BoxMOT

BoundingBox dataclass

A single tracked detection, in video-pixel coordinates.

Source code in src/body_eye_sync/pipeline/object_tracking.py
@dataclass
class BoundingBox:
    """A single tracked detection, in video-pixel coordinates."""

    x1: float
    y1: float
    x2: float
    y2: float
    track_id: int

boxes_from_tracks(tracks)

Drawable boxes for one frame's BoxMOT tracks array.

Source code in src/body_eye_sync/pipeline/object_tracking.py
def boxes_from_tracks(tracks: np.ndarray) -> list[BoundingBox]:
    """Drawable boxes for one frame's BoxMOT ``tracks`` array."""
    return [
        BoundingBox(
            float(row[0]),
            float(row[1]),
            float(row[2]),
            float(row[3]),
            int(row[_ID]),
        )
        for row in np.asarray(tracks)
    ]

default_device()

Pick the fastest available device as a BoxMOT/Ultralytics device string.

Source code in src/body_eye_sync/pipeline/object_tracking.py
def default_device() -> str:
    """Pick the fastest available device as a BoxMOT/Ultralytics device string."""
    try:
        import torch
    except ImportError:
        return "cpu"
    if torch.cuda.is_available():
        return "0"
    mps = getattr(torch.backends, "mps", None)
    if mps is not None and mps.is_available():
        return "mps"
    return "cpu"

detect_tracklets(video_path, detector='yolov8n', reid='osnet_x0_25_msmt17', tracker='botsort', device=None, object_classes=(0,))

Track objects in a video, yielding BoxMOT's per-frame result.

Runs BoxMOT (object detection + ReID tracking) and yields each frame's :class:~boxmot.engine.tracking.results.FrameResult as soon as it is computed, so callers can display or accumulate it live. Each result exposes frame_idx (1-based) and tracks -- an (n, 8) array of x1, y1, x2, y2, id, conf, cls, det_ind. Every frame is yielded, including frames with no detections (their tracks is empty). Stop iterating to cancel tracking early.

object_classes are the COCO class ids to detect; the default (0,) is people only. device is a BoxMOT/Ultralytics device string (e.g. "0", "mps", "cpu"); when None the fastest available device is chosen automatically. The detections sharing a track id form a tracklet, and conf is the per-frame confidence of the object detector.

Source code in src/body_eye_sync/pipeline/object_tracking.py
def detect_tracklets(
    video_path: str | Path,
    detector: str = "yolov8n",
    reid: str = "osnet_x0_25_msmt17",
    tracker: str = "botsort",
    device: str | None = None,
    object_classes: Sequence[int] = (0,),
) -> Iterator[FrameResult]:
    """Track objects in a video, yielding BoxMOT's per-frame result.

    Runs BoxMOT (object detection + ReID tracking) and yields each frame's
    :class:`~boxmot.engine.tracking.results.FrameResult` as soon as it is
    computed, so callers can display or accumulate it live. Each result exposes
    ``frame_idx`` (1-based) and ``tracks`` -- an ``(n, 8)`` array of
    ``x1, y1, x2, y2, id, conf, cls, det_ind``. Every frame is yielded, including
    frames with no detections (their ``tracks`` is empty). Stop iterating to
    cancel tracking early.

    ``object_classes`` are the COCO class ids to detect; the default ``(0,)`` is
    people only. ``device`` is a BoxMOT/Ultralytics device string (e.g. ``"0"``,
    ``"mps"``, ``"cpu"``); when ``None`` the fastest available device is chosen
    automatically. The detections sharing a track id form a tracklet, and
    ``conf`` is the per-frame confidence of the object detector.
    """
    # To get lazy, per-frame results so the GUI can display tracking live,
    # we can't use ``boxmot.Boxmot.track`` as this directly writes to a ``runs/`` directory.
    # So we use these internal ``build_*_from_spec`` imports and pin the boxmot version.
    from boxmot import track
    from boxmot.engine.workflows.support import (
        build_detector_from_spec,
        build_tracker_from_spec,
        build_tracker_with_reid_spec,
    )

    if device is None:
        device = default_device()

    # Pass absolute cache paths so both the Ultralytics detector weights and the
    # BoxMOT ReID weights download into the shared cache, rather than the current
    # working directory (detector) or BoxMOT's own in-package models dir (ReID).
    detector_runtime = build_detector_from_spec(
        cached_model_path(detector), classes=list(object_classes), device=device
    )
    tracker_runtime = build_tracker_from_spec(tracker, device=device)
    reid_runtime = build_tracker_with_reid_spec(
        tracker, tracker_runtime, cached_model_path(reid), device=device
    )

    yield from track(
        str(video_path), detector_runtime, reid_runtime, tracker_runtime, verbose=False
    )

tracks_to_dataframe(frames)

Stack (frame_idx, tracks) pairs into the stored tracklets DataFrame.

Returns a :class:pandas.DataFrame with columns frame, track_id, x1, y1, x2, y2, conf, frame and track_id as integers -- a compact numeric form to store and analyse. Frames with no detections contribute no rows.

Source code in src/body_eye_sync/pipeline/object_tracking.py
def tracks_to_dataframe(frames: Iterable[tuple[int, np.ndarray]]) -> pd.DataFrame:
    """Stack ``(frame_idx, tracks)`` pairs into the stored tracklets DataFrame.

    Returns a :class:`pandas.DataFrame` with columns
    ``frame, track_id, x1, y1, x2, y2, conf``, ``frame`` and ``track_id`` as
    integers -- a compact numeric form to store and analyse. Frames with no
    detections contribute no rows.
    """
    blocks = []
    for frame_idx, tracks in frames:
        tracks = np.asarray(tracks)
        if len(tracks) == 0:
            continue
        block = np.empty((len(tracks), len(_COLUMNS)))
        block[:, 0] = frame_idx
        block[:, 1] = tracks[:, _ID]
        block[:, 2:6] = tracks[:, _XYXY]
        block[:, 6] = tracks[:, _CONF]
        blocks.append(block)
    data = np.vstack(blocks) if blocks else np.empty((0, len(_COLUMNS)))
    return pd.DataFrame(data, columns=_COLUMNS).astype({"frame": int, "track_id": int})

Face Detection

body_eye_sync.pipeline.face_detection

Face detection inside tracked person boxes using InsightFace

FaceBox dataclass

A single detected face: its bounding box plus face-specific attributes.

box is the face's bounding box, in video-pixel coordinates, carrying the tracked person's track_id. landmarks holds the five (x, y) keypoints in :data:LANDMARK_NAMES order. embedding is the InsightFace ArcFace recognition vector (L2-normalised), kept for later identity clustering; None if the model pack has no recognition head.

Source code in src/body_eye_sync/pipeline/face_detection.py
@dataclass
class FaceBox:
    """A single detected face: its bounding box plus face-specific attributes.

    ``box`` is the face's bounding box, in video-pixel coordinates, carrying the
    tracked person's ``track_id``. ``landmarks`` holds the five ``(x, y)``
    keypoints in :data:`LANDMARK_NAMES` order. ``embedding`` is the InsightFace
    ArcFace recognition vector (L2-normalised), kept for later identity
    clustering; ``None`` if the model pack has no recognition head.
    """

    box: BoundingBox
    score: float
    landmarks: list[tuple[float, float]] = field(default_factory=list)
    embedding: np.ndarray | None = None

FaceFrameResult dataclass

Faces detected in one video frame, with a 0-based frame_idx.

Source code in src/body_eye_sync/pipeline/face_detection.py
@dataclass
class FaceFrameResult:
    """Faces detected in one video frame, with a 0-based ``frame_idx``."""

    frame_idx: int
    faces: list[FaceBox]

default_providers()

Pick onnxruntime execution providers, preferring CUDA when available.

Source code in src/body_eye_sync/pipeline/face_detection.py
def default_providers() -> list[str]:
    """Pick onnxruntime execution providers, preferring CUDA when available."""
    try:
        import onnxruntime as ort
    except ImportError:
        return ["CPUExecutionProvider"]
    if "CUDAExecutionProvider" in ort.get_available_providers():
        return ["CUDAExecutionProvider", "CPUExecutionProvider"]
    return ["CPUExecutionProvider"]

detect_faces(video_path, boxes_by_frame, model_name='buffalo_l', det_size=640, det_thresh=0.5, providers=None)

Detect one face per tracked person box, yielding a result per frame.

boxes_by_frame maps a 0-based frame index to that frame's tracked person :class:BoundingBoxes, as produced by object tracking. For each such frame the person crop is run through InsightFace; the highest scoring face above det_thresh is kept, its bounding box and five landmarks offset back into full-frame coordinates and tagged with the box's track_id. Every requested frame is yielded -- including frames where no face cleared the threshold (their faces is empty) -- so callers can show progress and cancel responsively. Stop iterating to cancel early.

Source code in src/body_eye_sync/pipeline/face_detection.py
def detect_faces(
    video_path: str | Path,
    boxes_by_frame: Mapping[int, Sequence[BoundingBox]],
    model_name: str = "buffalo_l",
    det_size: int = 640,
    det_thresh: float = 0.5,
    providers: list[str] | None = None,
) -> Iterator[FaceFrameResult]:
    """Detect one face per tracked person box, yielding a result per frame.

    ``boxes_by_frame`` maps a 0-based frame index to that frame's tracked person
    :class:`BoundingBox`es, as produced by object tracking. For each such frame
    the person crop is run through InsightFace; the highest scoring face above
    ``det_thresh`` is kept, its bounding box and five landmarks offset back into
    full-frame coordinates and tagged with the box's ``track_id``. Every
    requested frame is yielded -- including frames where no face cleared the
    threshold (their ``faces`` is empty) -- so callers can show progress and
    cancel responsively. Stop iterating to cancel early.
    """
    import cv2
    from insightface.app import FaceAnalysis

    if providers is None:
        providers = default_providers()

    root = _insightface_root()
    _ensure_model_available(model_name, root)
    app = FaceAnalysis(name=model_name, providers=providers, root=root)
    ctx_id = 0 if providers[0].startswith("CUDA") else -1
    app.prepare(ctx_id=ctx_id, det_size=(det_size, det_size))

    capture = cv2.VideoCapture(str(video_path))
    if not capture.isOpened():
        raise OSError(f"Could not open video: {video_path}")
    width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))

    try:
        if not boxes_by_frame:
            return
        wanted = set(boxes_by_frame)
        last_frame = max(wanted)
        # Walk frames in order, only decoding (retrieve) the ones we need; this
        # avoids the per-frame seeks that random access would cost.
        frame_idx = -1
        while frame_idx < last_frame:
            if not capture.grab():
                break
            frame_idx += 1
            if frame_idx not in wanted:
                continue
            ok, image = capture.retrieve()
            if not ok:
                break
            faces = _detect_in_boxes(
                app, image, boxes_by_frame[frame_idx], width, height, det_thresh
            )
            yield FaceFrameResult(frame_idx, faces)
    finally:
        capture.release()

face_box_from_row(row)

Rebuild a drawable :class:FaceBox from a merged DataFrame row.

Source code in src/body_eye_sync/pipeline/face_detection.py
def face_box_from_row(row) -> FaceBox:
    """Rebuild a drawable :class:`FaceBox` from a merged DataFrame row."""
    landmarks = [
        (float(getattr(row, f"{name}_x")), float(getattr(row, f"{name}_y")))
        for name in LANDMARK_NAMES
    ]
    box = BoundingBox(
        float(row.face_x1),
        float(row.face_y1),
        float(row.face_x2),
        float(row.face_y2),
        int(row.track_id),
    )
    return FaceBox(box, float(row.face_score), landmarks)

faces_to_dataframe(frames)

Stack per-frame face results into a DataFrame keyed on (frame, track_id).

Returns a :class:pandas.DataFrame with columns frame, track_id plus :data:FACE_COLUMNS, ready to left-merge onto the stored tracks. Frames with no detected faces contribute no rows.

Source code in src/body_eye_sync/pipeline/face_detection.py
def faces_to_dataframe(frames: Iterable[FaceFrameResult]) -> pd.DataFrame:
    """Stack per-frame face results into a DataFrame keyed on ``(frame, track_id)``.

    Returns a :class:`pandas.DataFrame` with columns ``frame, track_id`` plus
    :data:`FACE_COLUMNS`, ready to left-merge onto the stored tracks. Frames with
    no detected faces contribute no rows.
    """
    rows = []
    for result in frames:
        for face in result.faces:
            row = [
                result.frame_idx,
                face.box.track_id,
                face.score,
                face.box.x1,
                face.box.y1,
                face.box.x2,
                face.box.y2,
            ]
            for px, py in face.landmarks:
                row.extend((px, py))
            rows.append(row)
    data = np.asarray(rows, dtype=float) if rows else np.empty((0, len(_COLUMNS)))
    return pd.DataFrame(data, columns=_COLUMNS).astype({"frame": int, "track_id": int})

Body Pose

body_eye_sync.pipeline.body_pose

Body pose detection inside tracked person boxes using Ultralytics YOLO pose.

BodyPose dataclass

A single detected body pose for one tracked person box.

box is the YOLO pose bounding box, in video-pixel coordinates, carrying the tracked person's track_id. keypoints holds (x, y, score) triples in :data:KEYPOINT_NAMES order.

Source code in src/body_eye_sync/pipeline/body_pose.py
@dataclass
class BodyPose:
    """A single detected body pose for one tracked person box.

    ``box`` is the YOLO pose bounding box, in video-pixel coordinates, carrying
    the tracked person's ``track_id``. ``keypoints`` holds ``(x, y, score)``
    triples in :data:`KEYPOINT_NAMES` order.
    """

    box: BoundingBox
    score: float
    keypoints: list[tuple[float, float, float]] = field(default_factory=list)

PoseFrameResult dataclass

Body poses detected in one video frame, with a 0-based frame_idx.

Source code in src/body_eye_sync/pipeline/body_pose.py
@dataclass
class PoseFrameResult:
    """Body poses detected in one video frame, with a 0-based ``frame_idx``."""

    frame_idx: int
    poses: list[BodyPose]

detect_body_poses(video_path, boxes_by_frame, model_name=DEFAULT_MODEL_NAME, conf=0.25, device=None)

Detect one body pose per tracked person box, yielding a result per frame.

boxes_by_frame maps a 0-based frame index to that frame's tracked person :class:BoundingBoxes. For each requested frame, each person crop is passed through an Ultralytics YOLO pose model; the highest scoring pose in the crop is kept, its bounding box and COCO keypoints are offset back into full-frame coordinates and tagged with the person's track_id. Every requested frame is yielded, including frames where no pose cleared conf.

Source code in src/body_eye_sync/pipeline/body_pose.py
def detect_body_poses(
    video_path: str | Path,
    boxes_by_frame: Mapping[int, Sequence[BoundingBox]],
    model_name: str | Path = DEFAULT_MODEL_NAME,
    conf: float = 0.25,
    device: str | None = None,
) -> Iterator[PoseFrameResult]:
    """Detect one body pose per tracked person box, yielding a result per frame.

    ``boxes_by_frame`` maps a 0-based frame index to that frame's tracked person
    :class:`BoundingBox`es. For each requested frame, each person crop is passed
    through an Ultralytics YOLO pose model; the highest scoring pose in the crop
    is kept, its bounding box and COCO keypoints are offset back into full-frame
    coordinates and tagged with the person's ``track_id``. Every requested frame
    is yielded, including frames where no pose cleared ``conf``.
    """
    import cv2
    from ultralytics import YOLO

    if device is None:
        device = default_device()

    model = YOLO(cached_model_path(model_name))

    capture = cv2.VideoCapture(str(video_path))
    if not capture.isOpened():
        raise OSError(f"Could not open video: {video_path}")
    width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))

    try:
        if not boxes_by_frame:
            return
        wanted = set(boxes_by_frame)
        last_frame = max(wanted)
        frame_idx = -1
        while frame_idx < last_frame:
            if not capture.grab():
                break
            frame_idx += 1
            if frame_idx not in wanted:
                continue
            ok, image = capture.retrieve()
            if not ok:
                break
            poses = _detect_in_boxes(
                model, image, boxes_by_frame[frame_idx], width, height, conf, device
            )
            yield PoseFrameResult(frame_idx, poses)
    finally:
        capture.release()

pose_from_row(row)

Rebuild a drawable :class:BodyPose from a merged DataFrame row.

Source code in src/body_eye_sync/pipeline/body_pose.py
def pose_from_row(row) -> BodyPose:
    """Rebuild a drawable :class:`BodyPose` from a merged DataFrame row."""
    keypoints = [
        (
            float(getattr(row, f"pose_{name}_x")),
            float(getattr(row, f"pose_{name}_y")),
            float(getattr(row, f"pose_{name}_score")),
        )
        for name in KEYPOINT_NAMES
    ]
    box = BoundingBox(
        float(row.pose_x1),
        float(row.pose_y1),
        float(row.pose_x2),
        float(row.pose_y2),
        int(row.track_id),
    )
    return BodyPose(box, float(row.pose_score), keypoints)

poses_to_dataframe(frames)

Stack per-frame pose results into a DataFrame keyed on (frame, track_id).

Returns a :class:pandas.DataFrame with columns frame, track_id plus :data:POSE_COLUMNS, ready to left-merge onto the stored tracks. Frames with no detected poses contribute no rows.

Source code in src/body_eye_sync/pipeline/body_pose.py
def poses_to_dataframe(frames: Iterable[PoseFrameResult]) -> pd.DataFrame:
    """Stack per-frame pose results into a DataFrame keyed on ``(frame, track_id)``.

    Returns a :class:`pandas.DataFrame` with columns ``frame, track_id`` plus
    :data:`POSE_COLUMNS`, ready to left-merge onto the stored tracks. Frames with
    no detected poses contribute no rows.
    """
    rows = []
    for result in frames:
        for pose in result.poses:
            row = [
                result.frame_idx,
                pose.box.track_id,
                pose.score,
                pose.box.x1,
                pose.box.y1,
                pose.box.x2,
                pose.box.y2,
            ]
            for px, py, score in pose.keypoints:
                row.extend((px, py, score))
            rows.append(row)
    data = np.asarray(rows, dtype=float) if rows else np.empty((0, len(_COLUMNS)))
    return pd.DataFrame(data, columns=_COLUMNS).astype({"frame": int, "track_id": int})

Model Cache

body_eye_sync.pipeline.model_cache

Shared on-disk cache for downloaded model weights.

A single cross-platform cache directory that every pipeline step routes its model downloads into, rather than scattering them across per-library defaults (Ultralytics' working directory, InsightFace's ~/.insightface).

cached_model_path(model_ref)

Route a bare weights filename into the shared model cache.

Given only a bare name (e.g. yolo26m.pt), Ultralytics downloads the weights into the current working directory. Rewriting it to an absolute path under :func:model_cache_dir makes the download land in the shared cache instead. An explicit path (one with a directory part) or an already-existing file is returned unchanged.

Source code in src/body_eye_sync/pipeline/model_cache.py
def cached_model_path(model_ref: str | Path) -> str:
    """Route a bare weights filename into the shared model cache.

    Given only a bare name (e.g. ``yolo26m.pt``), Ultralytics downloads the
    weights into the current working directory. Rewriting it to an absolute path
    under :func:`model_cache_dir` makes the download land in the shared cache
    instead. An explicit path (one with a directory part) or an already-existing
    file is returned unchanged.
    """
    path = Path(model_ref)
    if path.parent != Path(".") or path.exists():
        return str(model_ref)
    cache_dir = model_cache_dir()
    cache_dir.mkdir(parents=True, exist_ok=True)
    return str(cache_dir / path.name)

model_cache_dir()

Directory all downloaded model weights are cached in (cross-platform).

Source code in src/body_eye_sync/pipeline/model_cache.py
def model_cache_dir() -> Path:
    """Directory all downloaded model weights are cached in (cross-platform)."""
    return user_cache_path("body-eye-sync", "SSC") / "models"