"""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