This project is a high-performance machine learning pipeline designed to ingest broadcast NHL video feeds, segment rink advertising boards, robustly track players, and seamlessly composite new neutral board textures without occluding foreground action, sticks, or goalie nets.
Previously, the board detector predicted a razor-thin line floating high up in the air. By moving away from mathematical camera warping and retraining a lightweight U-Net model on raw screen-space broadcast frames, the model now segments the full physical boards end-to-end at their actual physical thickness.
We upgraded the player detection model from yolov8n-seg.pt (which consistently missed players in motion/shadows, causing severe clipping) to yolov8m-seg.pt (Medium). The medium model catches 100% of players, sticks, and skates under native Apple Silicon Metal Performance Shaders (MPS) acceleration.
To solve the issue of digital jaggies and colored board halos around players overlapping the boards, we implemented an edge-preserving Guided Filter (r=4, eps=0.01 regularization). By calculating the local covariance between raw player masks and grayscale frame textures, the compositor snaps the player boundary perfectly to physical high-contrast edges (such as jersey borders, sticks, and hair), eliminating background leakage.
The replacement dasher-board panel is drawn using a customized HD white matte texture (images/boards.jpeg) which is tiled in 2D space and dimmed to 80% brightness to blend naturally under arena lighting. Additionally, the red top line and yellow bottom kickplate are dynamically sampled from clean, player-free sections of the original frame to ensure a perfect color match.
Below are side-by-side examples from successful U-Net model validation, demonstrating complete coverage of curved boards and stands rejection:
Figure 1: Game Action neutral zone panning broadcast validation.
Figure 2: Game Action fast transition play validation.
Note
Why do imperfect frames show a Confidence of 1.0000?
The U-Net detector calculates its scalar confidence score using the 99.5th percentile of raw predicted probabilities in the output map:
0.9999 or higher. This rounds to exactly 1.000 in the visualization.
Run scratch/visualize_purple.py to examine player silhouettes and ad subtraction boundaries:
Figure 3: Left side highlights detected players in purple and boards in green. Right side displays the final subtracted area that will be replaced with new ads.
Extracted frame 100 of the final composited video. Notice how the neutral off-white tiled boards sit perfectly behind the players and sticks:
Figure 4: Frame 100 demonstrating the Guided Filter edge-snapping players and equipment seamlessly onto the white matte replacement boards.
The Guided Filter tracks and snaps boundaries dynamically across high-speed horizontal camera panning:
Figure 8: Composite Frame 130.
To compile a final edge-snapped video from a raw broadcast hockey clip, follow these steps:
The source frame is fed into our trained screen-space U-Net board detector:
# src/calibration/ml_board_detector.py
success = board_detector.detect(frame)
board_mask = board_detector.get_board_mask()We run instance segmentation to locate all players. Dilation is disabled (dilation_kernel_size=0) to keep the mask tight:
# src/inference/model_runner.py
player_mask = runner.get_player_mask(frame, dilation_kernel_size=0)The original frame's grayscale channel is used as a guidance image
# src/compositing/homography.py
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
p = player_mask.astype(np.float32) / 255.0
P = self._guided_filter(gray_frame, p, r=4, eps=0.01)The final compositing weight is calculated as
W = B * (1.0 - P)
W3 = np.expand_dims(W, axis=-1)
composited = blended_layer * W3 + frame * (1.0 - W3)To run the full pipeline on a raw video feed and save the composited output:
PYTHONPATH=. python scripts/replace_boards_ml.py \
--video "data/videos/2026-05-19 23-34-03.mp4" \
--ad images/boards.jpeg \
--model src/calibration/board_segmentation_model_unet.pth \
--output output/ml_composited.mp4To export BGR overlay comparisons of YOLO detections vs U-Net boards:
PYTHONPATH=. python scratch/visualize_purple.pyTo run the real-time HUD and compare blocking dynamically with keypresses:
PYTHONPATH=. python scripts/realtime_ad_blocker.py --source "data/videos/2026-05-19 23-34-03.mp4"- Controls: Press
sto instantly toggle the digital ad-blocker ON/OFF, andqto safely quit.


