dschandra's picture
Upload 6 files
2db7738 verified
"""Ball detection and tracking for the DRS application.
This module implements a simple motion‑based tracker to follow the cricket
ball in a video. Professional ball tracking systems use multiple high
frame‑rate cameras and sophisticated object detectors. Here, we rely on
background subtraction combined with circle detection (Hough circles) to
locate the ball in each frame. The tracker keeps the coordinates and
timestamps of the ball's centre so that downstream modules can
estimate its trajectory and predict whether it will hit the stumps.
The detection pipeline makes the following assumptions:
* Only one ball is present in the scene at a time.
* The ball is approximately circular in appearance.
* The camera is static or moves little compared to the ball.
These assumptions hold for many amateur cricket recordings but are
obviously simplified compared to a true DRS system.
"""
from __future__ import annotations
import cv2
import numpy as np
from typing import Dict, List, Tuple
def detect_and_track_ball(video_path: str) -> Dict[str, List]:
"""Detect and track the cricket ball in a video.
Parameters
----------
video_path: str
Path to the trimmed video segment containing the delivery and appeal.
Returns
-------
Dict[str, List]
A dictionary containing:
``centers``: list of (x, y) coordinates of the ball in successive frames.
``timestamps``: list of timestamps (in seconds) corresponding to each centre.
``radii``: list of detected circle radii (in pixels).
"""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise RuntimeError(f"Could not open video {video_path}")
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
# Background subtractor for motion detection
bg_sub = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=32, detectShadows=False)
centers: List[Tuple[int, int]] = []
radii: List[int] = []
timestamps: List[float] = []
previous_center: Tuple[int, int] | None = None
frame_idx = 0
while True:
ret, frame = cap.read()
if not ret:
break
timestamp = frame_idx / fps
# Preprocess: grayscale and blur
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Apply background subtraction to emphasise moving objects
fg_mask = bg_sub.apply(frame)
# Remove noise from mask
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN, kernel)
detected_center: Tuple[int, int] | None = None
detected_radius: int | None = None
# Attempt to detect circles using Hough transform
circles = cv2.HoughCircles(
blurred,
cv2.HOUGH_GRADIENT,
dp=1.2,
minDist=20,
param1=50,
param2=30,
minRadius=3,
maxRadius=30,
)
if circles is not None:
circles = np.round(circles[0, :]).astype("int")
# Choose the circle closest to the previous detection to maintain
# continuity. If no previous detection exists, pick the circle
# with the smallest radius (likely the ball).
if previous_center is not None:
min_dist = float("inf")
chosen = None
for x, y, r in circles:
dist = (x - previous_center[0]) ** 2 + (y - previous_center[1]) ** 2
if dist < min_dist:
min_dist = dist
chosen = (x, y, r)
if chosen is not None:
detected_center = (int(chosen[0]), int(chosen[1]))
detected_radius = int(chosen[2])
else:
# No previous centre: pick the smallest radius circle
chosen = min(circles, key=lambda c: c[2])
detected_center = (int(chosen[0]), int(chosen[1]))
detected_radius = int(chosen[2])
# Fallback: use contours on the foreground mask to find moving blobs
if detected_center is None:
contours, _ = cv2.findContours(fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Filter contours by area to eliminate noise; choose the one
# closest to previous centre or the smallest area blob
candidates = []
for cnt in contours:
area = cv2.contourArea(cnt)
if 10 < area < 800: # adjust thresholds as necessary
x, y, w, h = cv2.boundingRect(cnt)
cx = x + w // 2
cy = y + h // 2
candidates.append((cx, cy, w, h, area))
if candidates:
if previous_center is not None:
chosen = min(candidates, key=lambda c: (c[0] - previous_center[0]) ** 2 + (c[1] - previous_center[1]) ** 2)
else:
chosen = min(candidates, key=lambda c: c[4])
cx, cy, w, h, _ = chosen
detected_center = (int(cx), int(cy))
detected_radius = int(max(w, h) / 2)
if detected_center is not None:
centers.append(detected_center)
radii.append(detected_radius or 5)
timestamps.append(timestamp)
previous_center = detected_center
# Increment frame index regardless of detection
frame_idx += 1
cap.release()
return {"centers": centers, "radii": radii, "timestamps": timestamps}