Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ logs/

# Scratch
scratch/

#Yolo-Trcker Models
train_ball_detector.pt
train_pitch_keypoint_detector.pt
train_player_detector.pt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

# Model file names (relative to the models/ directory)
models:
player_detection: "football-player-detection-v3.pt"
ball_detection: "football-ball-detection-v2.pt"
pitch_detection: "football-pitch-detection-v1.pt"
player_detection: "train_player_detector.pt"
ball_detection: "train_ball_detector.pt"
pitch_detection: "train_pitch_keypoint_detector.pt"

# Confidence thresholds for detection
confidence:
Expand All @@ -14,11 +14,11 @@ confidence:
pitch: 0.25

# Default device for inference
device: "cuda"
device: "cpu"

# Default detections to run in analyze()
# Enable/disable each detection type. Set to true/false to control which run.
default_detections:
players: true
ball: true
pitch: true
pitch: true
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"id": "yolo-tracker",
"name": "YOLO Tracker",
"version": "1.0.0",
"description": "ForgeSyte plugin for football player, ball, pitch, and radar analysis using YOLO + Supervision.",
"version": "0.10.0",
"description": "YOLO + Supervision plugin with streaming video analysis.",
"author": "Roger",
"license": "MIT",
"entrypoint": "forgesyte_yolo_tracker.plugin",
Expand All @@ -11,7 +11,8 @@
"player_detection",
"ball_detection",
"pitch_detection",
"radar"
"radar",
"streaming_video_analysis"
],
"tools": [
{
Expand Down Expand Up @@ -88,7 +89,7 @@
"description": "Run ball detection on video frames.",
"input_types": ["video"],
"output_types": ["frames"],
"capabilities": ["ball_detection"],
"capabilities": ["ball_detection", "streaming_video_analysis"],
"inputs": {
"video_path": "string",
"device": "string"
Expand All @@ -104,7 +105,7 @@
"description": "Run pitch detection on video frames.",
"input_types": ["video"],
"output_types": ["frames"],
"capabilities": ["pitch_detection"],
"capabilities": ["pitch_detection", "streaming_video_analysis"],
"inputs": {
"video_path": "string",
"device": "string"
Expand All @@ -120,7 +121,7 @@
"description": "Generate radar view from video frames.",
"input_types": ["video"],
"output_types": ["frames"],
"capabilities": ["radar"],
"capabilities": ["radar", "streaming_video_analysis"],
"inputs": {
"video_path": "string",
"device": "string"
Expand All @@ -136,7 +137,7 @@
"description": "Track players across video frames.",
"input_types": ["video"],
"output_types": ["frames"],
"capabilities": ["player_detection"],
"capabilities": ["player_detection", "streaming_video_analysis"],
"inputs": {
"video_path": "string",
"device": "string"
Expand All @@ -147,4 +148,4 @@
}
}
]
}
}
44 changes: 25 additions & 19 deletions plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def on_unload(self) -> None: # pragma: no cover # noqa: B027
from forgesyte_yolo_tracker.tracking import ByteTrackFactory, get_tracker_ids
from forgesyte_yolo_tracker.inference.radar import radar_json_with_annotated_frame
from forgesyte_yolo_tracker.configs import load_model_config
from forgesyte_yolo_tracker.utils.json_sanitize import sanitize_json

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -121,10 +122,10 @@ def _tool_player_detection(
if error:
return error
if annotated and frame is not None:
return detect_players_json_with_annotated_frame(frame, device=device)
return {"success": True, "result": detect_players_json_with_annotated_frame(frame, device=device)}
if frame is not None:
return detect_players_json(frame, device=device)
return {"error": "image_decode_failed"}
return {"success": True, "result": detect_players_json(frame, device=device)}
return {"success": False, "error": "image_decode_failed"}



Expand All @@ -136,10 +137,10 @@ def _tool_player_tracking(
if error:
return error
if annotated and frame is not None:
return track_players_json_with_annotated_frame(frame, device=device)
return {"success": True, "result": track_players_json_with_annotated_frame(frame, device=device)}
if frame is not None:
return track_players_json(frame, device=device)
return {"error": "image_decode_failed"}
return {"success": True, "result": track_players_json(frame, device=device)}
return {"success": False, "error": "image_decode_failed"}


def _tool_video_player_tracking(
Expand Down Expand Up @@ -220,10 +221,10 @@ def _tool_ball_detection(
if error:
return error
if annotated and frame is not None:
return detect_ball_json_with_annotated_frame(frame, device=device)
return {"success": True, "result": detect_ball_json_with_annotated_frame(frame, device=device)}
if frame is not None:
return detect_ball_json(frame, device=device)
return {"error": "image_decode_failed"}
return {"success": True, "result": detect_ball_json(frame, device=device)}
return {"success": False, "error": "image_decode_failed"}


def _tool_pitch_detection(
Expand All @@ -233,10 +234,10 @@ def _tool_pitch_detection(
if error:
return error
if annotated and frame is not None:
return detect_pitch_json_with_annotated_frame(frame, device=device)
return {"success": True, "result": detect_pitch_json_with_annotated_frame(frame, device=device)}
if frame is not None:
return detect_pitch_json(frame, device=device)
return {"error": "image_decode_failed"}
return {"success": True, "result": detect_pitch_json(frame, device=device)}
return {"success": False, "error": "image_decode_failed"}


def _tool_radar(
Expand All @@ -246,10 +247,10 @@ def _tool_radar(
if error:
return error
if annotated and frame is not None:
return radar_json_with_annotated_frame(frame, device=device)
return {"success": True, "result": radar_json_with_annotated_frame(frame, device=device)}
if frame is not None:
return radar_json(frame, device=device)
return {"error": "image_decode_failed"}
return {"success": True, "result": radar_json(frame, device=device)}
return {"success": False, "error": "image_decode_failed"}


# ---------------------------------------------------------
Expand Down Expand Up @@ -304,9 +305,13 @@ def _run_video_tool(

logger.info(f"Completed: {frame_index} frames")

# v0.10.0: Sanitize output for JSON serialization
return {
"total_frames": frame_index,
"frames": frame_results,
"success": True,
"result": sanitize_json({
"total_frames": frame_index,
"frames": frame_results,
})
}


Expand Down Expand Up @@ -475,10 +480,11 @@ def _tool_video_player_tracking(

frame_index += 1

return {
# v0.10.0: Sanitize output for JSON serialization
return sanitize_json({
"total_frames": frame_index,
"frames": frame_results,
}
})


class Plugin(BasePlugin): # type: ignore[misc]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@
"""

from . import ball, soccer_pitch
from .team import TeamClassifier, create_batches
from .view import ViewTransformer

# Lazy import to avoid torch/transformers at module load time
def __getattr__(name: str):
"""Lazy load heavy dependencies to avoid import errors in tests."""
if name == "TeamClassifier":
from .team import TeamClassifier
return TeamClassifier
elif name == "create_batches":
from .team import create_batches
return create_batches
elif name == "ViewTransformer":
from .view import ViewTransformer
return ViewTransformer
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

__all__ = [
# From local implementations
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""JSON Sanitizer for NumPy types.

v0.10.0: Ensures all plugin outputs are JSON-serializable.

Converts NumPy types to Python primitives:
- np.float16, np.float32, np.float64 → float
- np.int8, np.int16, np.int32, np.int64 → int
- np.uint8, np.uint16, np.uint32, np.uint64 → int
- np.ndarray → list
- np.bool_ → bool
"""

import numpy as np
from typing import Any


def sanitize_json(obj: Any) -> Any:
"""
Recursively convert NumPy types into JSON-safe Python types.

Ensures no np.float32, np.int64, np.ndarray, or other non-serializable
objects appear in the final plugin output.

Args:
obj: Any Python object, potentially containing NumPy types

Returns:
Object with all NumPy types converted to Python primitives

Example:
>>> sanitize_json({
... "track_id": np.int64(1),
... "center": np.array([np.float32(100.5), np.float32(200.5)])
... })
{'track_id': 1, 'center': [100.5, 200.5]}
"""
# Dict → sanitize values
if isinstance(obj, dict):
return {k: sanitize_json(v) for k, v in obj.items()}

# List / tuple → sanitize each element
if isinstance(obj, (list, tuple)):
return [sanitize_json(v) for v in obj]

# NumPy array → convert to list, then sanitize elements
if isinstance(obj, np.ndarray):
return sanitize_json(obj.tolist())

# NumPy floats → Python float
if isinstance(obj, (np.float16, np.float32, np.float64)):
return float(obj)

# NumPy ints → Python int
if isinstance(obj, (np.int8, np.int16, np.int32, np.int64)):
return int(obj)

# NumPy unsigned ints → Python int
if isinstance(obj, (np.uint8, np.uint16, np.uint32, np.uint64)):
return int(obj)

# NumPy bool → Python bool
if isinstance(obj, np.bool_):
return bool(obj)

# Pass through Python primitives and None
return obj
Loading