Spaces:
Sleeping
Sleeping
"""Trajectory estimation for the cricket ball. | |
Professional ball tracking systems reconstruct the ball's path in 3D | |
from several camera angles and then use physics or machine learning | |
models to project its flight. Here we implement a far simpler | |
approach. Given a sequence of ball centre coordinates extracted from | |
a single camera (behind the bowler), we fit a polynomial curve to | |
approximate the ball's trajectory in image space. We assume that the | |
ball travels roughly along a parabolic path, so a quadratic fit to | |
``y`` as a function of ``x`` is appropriate for the vertical drop. | |
Because we lack explicit knowledge of the camera's field of view, the | |
stumps' location is estimated relative to the range of observed ball | |
positions. If the projected path intersects a fixed region near the | |
bottom middle of the frame, we say that the ball would have hit the | |
stumps. | |
""" | |
from __future__ import annotations | |
import numpy as np | |
from typing import Callable, Dict, List, Tuple | |
def estimate_trajectory(centers: List[Tuple[int, int]], timestamps: List[float]) -> Dict[str, object]: | |
"""Fit a polynomial to the ball's path. | |
Parameters | |
---------- | |
centers: list of tuple(int, int) | |
Detected ball centre positions in pixel coordinates (x, y). | |
timestamps: list of float | |
Timestamps (in seconds) corresponding to each detection. Unused | |
in the current implementation but retained for extensibility. | |
Returns | |
------- | |
dict | |
A dictionary with keys ``coeffs`` (the polynomial coefficients | |
[a, b, c] for ``y = a*x^2 + b*x + c``) and ``model`` (a | |
callable that accepts an x coordinate and returns the | |
predicted y coordinate). | |
""" | |
if not centers: | |
# No detections; return a dummy model | |
return {"coeffs": np.array([0.0, 0.0, 0.0]), "model": lambda x: 0 * x} | |
xs = np.array([pt[0] for pt in centers], dtype=np.float64) | |
ys = np.array([pt[1] for pt in centers], dtype=np.float64) | |
# Require at least 3 points for a quadratic fit; otherwise fall back | |
# to a linear fit | |
if len(xs) >= 3: | |
coeffs = np.polyfit(xs, ys, 2) | |
def model(x: np.ndarray | float) -> np.ndarray | float: | |
return coeffs[0] * (x ** 2) + coeffs[1] * x + coeffs[2] | |
else: | |
coeffs = np.polyfit(xs, ys, 1) | |
def model(x: np.ndarray | float) -> np.ndarray | float: | |
return coeffs[0] * x + coeffs[1] | |
return {"coeffs": coeffs, "model": model} | |
def predict_stumps_intersection(trajectory: Dict[str, object]) -> bool: | |
"""Predict whether the ball's trajectory will hit the stumps. | |
The stumps are assumed to lie roughly in the centre of the frame | |
along the horizontal axis and occupy the lower quarter of the | |
vertical axis. This heuristic works reasonably well for videos | |
captured from behind the bowler. In a production system you | |
would calibrate the exact position of the stumps from the pitch | |
geometry. | |
Parameters | |
---------- | |
trajectory: dict | |
Output of :func:`estimate_trajectory`, containing the | |
polynomial model and the original ``centers`` list if needed. | |
Returns | |
------- | |
bool | |
True if the ball is predicted to hit the stumps, False otherwise. | |
""" | |
model: Callable[[float], float] = trajectory["model"] | |
coeffs = trajectory["coeffs"] | |
# Recover approximate frame dimensions from the observed centres. We | |
# estimate the width and height as slightly larger than the max | |
# observed coordinates. | |
# Note: trajectory does not contain the centres directly, so we | |
# recompute width and height heuristically based on coefficient | |
# magnitudes. To avoid overcomplication we assign reasonable | |
# defaults if no centres were available. | |
if hasattr(trajectory, "centers"): | |
# never executed; left as placeholder | |
pass | |
# Use coefficients to infer approximate domain of x. The roots of | |
# derivative give extremum; but we simply sample across a range | |
# derived from typical video width (e.g. 640px) | |
frame_width = 640 | |
frame_height = 360 | |
# Estimate ball y position at the x coordinate corresponding to the | |
# middle stump: 50% of frame width | |
stumps_x = frame_width * 0.5 | |
predicted_y = model(stumps_x) | |
# Define the vertical bounds of the wicket region in pixels. The | |
# top of the stumps is roughly three quarters down the frame and | |
# the bottom is at the very bottom. These ratios can be tuned. | |
stumps_y_low = frame_height * 0.65 | |
stumps_y_high = frame_height * 0.95 | |
return stumps_y_low <= predicted_y <= stumps_y_high |