""" Field segmentation — HSV green mask → pitch boundary contour. Pure OpenCV. Called by the inference server endpoint. """ from __future__ import annotations import base64 import cv2 import numpy as np def segment_field( image: np.ndarray, hue_low: int = 30, hue_high: int = 85, sat_low: int = 30, sat_high: int = 255, val_low: int = 30, val_high: int = 255, morph_kernel: int = 15, min_area_ratio: float = 0.05, ) -> dict: """ Detect the pitch area using HSV green thresholding. Returns dict with: boundary: list of [x, y] points coverage: float (fraction of frame) """ hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) lower = np.array([hue_low, sat_low, val_low]) upper = np.array([hue_high, sat_high, val_high]) mask = cv2.inRange(hsv, lower, upper) k = morph_kernel kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) h, w = image.shape[:2] min_area = min_area_ratio * h * w boundary = [] coverage = 0.0 if contours: large = [c for c in contours if cv2.contourArea(c) >= min_area] if large: pitch_contour = max(large, key=cv2.contourArea) boundary = pitch_contour.reshape(-1, 2).tolist() coverage = cv2.contourArea(pitch_contour) / (h * w) refined = np.zeros_like(mask) cv2.drawContours(refined, [pitch_contour], -1, 255, cv2.FILLED) mask = refined return { "boundary": boundary, "coverage": coverage, "mask": mask, } def segment_field_debug( image: np.ndarray, **kwargs, ) -> dict: """Same as segment_field but includes a mask overlay for the editor.""" result = segment_field(image, **kwargs) mask = result["mask"] # RGBA overlay: solid green where mask, fully transparent elsewhere h, w = image.shape[:2] overlay = np.zeros((h, w, 4), dtype=np.uint8) overlay[mask > 0] = [0, 255, 0, 255] _, buf = cv2.imencode(".png", overlay) result["mask_overlay_b64"] = base64.b64encode(buf.tobytes()).decode() # Don't send the raw mask over HTTP del result["mask"] return result