arcsort 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
arcsort/cmc.py ADDED
@@ -0,0 +1,169 @@
1
+ """Camera Motion Compensation via sparse optical flow.
2
+
3
+ Estimates the inter-frame camera motion as a 2D affine transform using
4
+ Lucas-Kanade sparse optical flow and RANSAC-based affine estimation.
5
+ The resulting transform is applied to Kalman filter predictions to
6
+ compensate for camera panning, tilting, and zooming.
7
+
8
+ This module is inspired by BoT-SORT's GMC (Global Motion Compensation)
9
+ but simplified to only support sparse optical flow, which provides the
10
+ best speed-accuracy trade-off for real-time tracking.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import cv2
16
+ import numpy as np
17
+ from loguru import logger
18
+
19
+
20
+ # Default CMC parameters (overridden by config)
21
+ DEFAULT_CMC_CONFIG = {
22
+ "max_corners": 1000,
23
+ "quality_level": 0.01,
24
+ "min_distance": 1,
25
+ "block_size": 3,
26
+ "ransac_threshold": 3.0,
27
+ "min_inliers": 10,
28
+ "downscale": 2,
29
+ }
30
+
31
+
32
+ class CameraMotionCompensator:
33
+ """Estimate inter-frame camera motion using sparse optical flow.
34
+
35
+ Tracks corner features between consecutive frames to estimate a
36
+ 2D similarity transform (rotation, uniform scale, translation).
37
+ Falls back to identity transform when too few correspondences are
38
+ found (e.g., uniform backgrounds, heavy motion blur).
39
+ """
40
+
41
+ def __init__(self, config: dict | None = None) -> None:
42
+ """Initialize CMC module.
43
+
44
+ Args:
45
+ config: CMC configuration dictionary. Uses DEFAULT_CMC_CONFIG
46
+ for any missing keys.
47
+ """
48
+ if config is None:
49
+ config = {}
50
+ cfg = {**DEFAULT_CMC_CONFIG, **config}
51
+
52
+ self.downscale = max(1, int(cfg["downscale"]))
53
+ self.min_inliers = cfg["min_inliers"]
54
+ self.ransac_threshold = cfg["ransac_threshold"]
55
+
56
+ self.feature_params = {
57
+ "maxCorners": cfg["max_corners"],
58
+ "qualityLevel": cfg["quality_level"],
59
+ "minDistance": cfg["min_distance"],
60
+ "blockSize": cfg["block_size"],
61
+ "useHarrisDetector": False,
62
+ "k": 0.04,
63
+ }
64
+
65
+ self._prev_frame: np.ndarray | None = None
66
+ self._prev_keypoints: np.ndarray | None = None
67
+
68
+ def reset(self) -> None:
69
+ """Reset CMC state for a new sequence."""
70
+ self._prev_frame = None
71
+ self._prev_keypoints = None
72
+
73
+ def apply(self, frame: np.ndarray) -> np.ndarray:
74
+ """Compute affine transform between previous and current frame.
75
+
76
+ Args:
77
+ frame: Current frame as BGR image, shape (H, W, 3).
78
+
79
+ Returns:
80
+ 2x3 affine matrix representing camera motion from previous
81
+ frame to current frame. Returns identity if this is the
82
+ first frame or if estimation fails.
83
+ """
84
+ height, width = frame.shape[:2]
85
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
86
+
87
+ if self.downscale > 1:
88
+ gray = cv2.resize(
89
+ gray, (width // self.downscale, height // self.downscale)
90
+ )
91
+
92
+ keypoints = cv2.goodFeaturesToTrack(gray, mask=None, **self.feature_params)
93
+
94
+ # First frame: store and return identity
95
+ if self._prev_frame is None:
96
+ self._prev_frame = gray
97
+ self._prev_keypoints = keypoints
98
+ return np.eye(2, 3, dtype=np.float64)
99
+
100
+ affine = self._estimate_affine(gray, keypoints)
101
+
102
+ # Store for next frame
103
+ self._prev_frame = gray
104
+ self._prev_keypoints = keypoints
105
+
106
+ return affine
107
+
108
+ def _estimate_affine(
109
+ self, curr_frame: np.ndarray, curr_keypoints: np.ndarray | None
110
+ ) -> np.ndarray:
111
+ """Estimate affine transform via optical flow and RANSAC.
112
+
113
+ Args:
114
+ curr_frame: Current grayscale frame (possibly downscaled).
115
+ curr_keypoints: Feature points detected in current frame
116
+ (used for next iteration, not directly in flow).
117
+
118
+ Returns:
119
+ 2x3 affine matrix. Identity if estimation fails.
120
+ """
121
+ identity = np.eye(2, 3, dtype=np.float64)
122
+
123
+ if self._prev_keypoints is None or len(self._prev_keypoints) < 4:
124
+ return identity
125
+
126
+ # Track previous keypoints into current frame
127
+ matched_pts, status, _err = cv2.calcOpticalFlowPyrLK(
128
+ self._prev_frame, curr_frame, self._prev_keypoints, None
129
+ )
130
+
131
+ if matched_pts is None:
132
+ return identity
133
+
134
+ # Keep only successfully tracked points
135
+ mask = status.flatten() == 1
136
+ prev_good = self._prev_keypoints[mask]
137
+ curr_good = matched_pts[mask]
138
+
139
+ if len(prev_good) < self.min_inliers:
140
+ logger.debug(
141
+ f"CMC: too few correspondences ({len(prev_good)}), "
142
+ f"using identity"
143
+ )
144
+ return identity
145
+
146
+ # Estimate similarity transform (4 DOF: rotation, scale, tx, ty)
147
+ affine, inliers = cv2.estimateAffinePartial2D(
148
+ prev_good, curr_good,
149
+ method=cv2.RANSAC,
150
+ ransacReprojThreshold=self.ransac_threshold,
151
+ )
152
+
153
+ if affine is None:
154
+ logger.debug("CMC: RANSAC failed, using identity")
155
+ return identity
156
+
157
+ num_inliers = int(inliers.sum()) if inliers is not None else 0
158
+ if num_inliers < self.min_inliers:
159
+ logger.debug(
160
+ f"CMC: too few inliers ({num_inliers}), using identity"
161
+ )
162
+ return identity
163
+
164
+ # Correct translation for downscaling
165
+ if self.downscale > 1:
166
+ affine[0, 2] *= self.downscale
167
+ affine[1, 2] *= self.downscale
168
+
169
+ return affine
@@ -0,0 +1,35 @@
1
+ """Tracker presets: the exact configurations behind the paper's results.
2
+
3
+ Each preset is a YAML file shipped with the package:
4
+
5
+ - ``sportsmot``: SportsMOT test row (81.82 HOTA); the recommended default for
6
+ sports broadcast footage.
7
+ - ``dancetrack``: DanceTrack test row (62.1 HOTA).
8
+ - ``sportsmot_deepeiou_inputs``: SportsMOT row on Deep-EIoU's released
9
+ detections and embeddings (80.31 HOTA); detector-calibration thresholds differ.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from importlib import resources
15
+
16
+ import yaml
17
+
18
+
19
+ def available() -> list[str]:
20
+ """Names of the shipped presets."""
21
+ files = resources.files(__name__).iterdir()
22
+ return sorted(f.name.removesuffix(".yaml") for f in files if f.name.endswith(".yaml"))
23
+
24
+
25
+ def load_preset(name: str) -> dict:
26
+ """Load a preset's tracker config dict by name.
27
+
28
+ Raises:
29
+ ValueError: If no preset with that name exists.
30
+ """
31
+ path = resources.files(__name__) / f"{name}.yaml"
32
+ if not path.is_file():
33
+ raise ValueError(f"Unknown preset '{name}'. Available: {available()}")
34
+ config = yaml.safe_load(path.read_text())
35
+ return config.get("tracker", config)
@@ -0,0 +1,74 @@
1
+ # ARC-SORT preset for DanceTrack (paper: DanceTrack test 62.1 HOTA).
2
+ # Same association settings as sportsmot.yaml except the EIoU expansion
3
+ # scale (1.0) and the FastReID SBS-S50 appearance model (see README: Weights).
4
+ tracker:
5
+ name: ARC-SORT DanceTrack
6
+ track_high_thresh: 0.6
7
+ track_low_thresh: 0.1
8
+ new_track_thresh: 0.7
9
+ track_buffer: 30
10
+ match_thresh: 0.25
11
+ byte_match_thresh: 0.5
12
+ min_hits: 3
13
+ use_byte: true
14
+ use_cmc: true
15
+ use_imm: true
16
+ use_hybrid_distance: false
17
+ use_reid: true
18
+ use_gated_reid: false
19
+ reid:
20
+ weights: weights/dance_sbs_S50_traced.pt
21
+ crop_size:
22
+ - 128
23
+ - 384
24
+ w_assoc_emb: 1.25
25
+ alpha_fixed_emb: 0.95
26
+ aw_off: false
27
+ aw_param: 1.0
28
+ imm_gate_cv_thresh: 0.7
29
+ imm_gate_young_frames: 5
30
+ uncertainty_weight_scale: 1.0
31
+ imm:
32
+ models:
33
+ - cv
34
+ - ca
35
+ - ct
36
+ transition_prob_diagonal: 0.98
37
+ process_noise_cv: 1.0
38
+ process_noise_ca: 1.0
39
+ process_noise_ct: 1.0
40
+ initial_model_probs:
41
+ - 0.7
42
+ - 0.15
43
+ - 0.15
44
+ hybrid_distance:
45
+ iou_fallback_thresh: 0.1
46
+ shape_penalty_weight: 0.3
47
+ cmc:
48
+ max_corners: 1000
49
+ quality_level: 0.01
50
+ min_distance: 1
51
+ block_size: 3
52
+ ransac_threshold: 3.0
53
+ min_inliers: 10
54
+ downscale: 2
55
+ ocsort:
56
+ use_oru: true
57
+ use_ocm: true
58
+ use_ocr: true
59
+ delta_t: 3
60
+ inertia_weight: 0.2
61
+ use_eiou: true
62
+ eiou:
63
+ adaptive: true
64
+ adaptive_scale: 1.0
65
+ min_expansion: 0.2
66
+ max_expansion: 1.2
67
+ ambiguity:
68
+ enabled: true
69
+ margin_thresh: 0.2
70
+ arbitration:
71
+ enabled: true
72
+ window: 30
73
+ min_frames: 4
74
+ llr_thresh: 0.5
@@ -0,0 +1,80 @@
1
+ # ARC-SORT preset for sports broadcast footage (paper: SportsMOT test 81.82 HOTA).
2
+ # SportsMOT is 25 FPS, 720p footage with fast non-linear athlete motion and a
3
+ # moving camera. All values were chosen on the validation split only.
4
+
5
+ tracker:
6
+ name: "ARC-SORT SportsMOT"
7
+
8
+ track_high_thresh: 0.6
9
+ track_low_thresh: 0.1
10
+ new_track_thresh: 0.7
11
+ track_buffer: 30
12
+ match_thresh: 0.25
13
+ byte_match_thresh: 0.5
14
+ min_hits: 3
15
+
16
+ use_byte: true
17
+ use_cmc: true
18
+ use_imm: true
19
+ # Uncertainty-adaptive Expansion IoU is the stage-1 spatial metric
20
+ # (on val it beat both the hybrid distance and fixed-expansion EIoU)
21
+ use_hybrid_distance: false
22
+ use_eiou: true
23
+ use_reid: true
24
+ use_gated_reid: false
25
+ use_mahalanobis_gate: false
26
+
27
+ eiou:
28
+ adaptive: true
29
+ adaptive_scale: 2.0
30
+ min_expansion: 0.2
31
+ max_expansion: 1.2
32
+
33
+ reid:
34
+ # Sports-domain OSNet from the Deep-EIoU release, converted with
35
+ # tools/convert_osnet_weights.py (see README: Weights)
36
+ weights: "weights/sportsmot/sports_osnet_traced.pt"
37
+ crop_size: [128, 256]
38
+ w_assoc_emb: 1.25
39
+ alpha_fixed_emb: 0.95
40
+ aw_off: false
41
+ aw_param: 1.0
42
+ imm_gate_cv_thresh: 0.7
43
+ imm_gate_young_frames: 5
44
+
45
+ imm:
46
+ models: ["cv", "ca", "ct"]
47
+ transition_prob_diagonal: 0.98
48
+ process_noise_cv: 1.0
49
+ process_noise_ca: 1.0
50
+ process_noise_ct: 1.0
51
+ initial_model_probs: [0.7, 0.15, 0.15]
52
+
53
+ hybrid_distance:
54
+ iou_fallback_thresh: 0.1
55
+ shape_penalty_weight: 0.3
56
+
57
+ cmc:
58
+ max_corners: 1000
59
+ quality_level: 0.01
60
+ min_distance: 1
61
+ block_size: 3
62
+ ransac_threshold: 3.0
63
+ min_inliers: 10
64
+ downscale: 2
65
+
66
+ ocsort:
67
+ use_oru: true
68
+ use_ocm: true
69
+ use_ocr: true
70
+ use_cmc_oru: true
71
+ delta_t: 3
72
+ inertia_weight: 0.2
73
+ ambiguity:
74
+ enabled: true
75
+ margin_thresh: 0.2
76
+ arbitration:
77
+ enabled: true
78
+ window: 30
79
+ min_frames: 4
80
+ llr_thresh: 0.5
@@ -0,0 +1,68 @@
1
+ # ARC-SORT on Deep-EIoU's released SportsMOT detections and embeddings
2
+ # (paper: 80.31 HOTA, +3.1 over Deep-EIoU on identical inputs).
3
+ # Same as sportsmot.yaml except the detector-calibration thresholds, which
4
+ # match Deep-EIoU's detector: track_high 0.5, new_track 0.6, match 0.2.
5
+ tracker:
6
+ name: ARC-SORT SportsMOT
7
+ track_high_thresh: 0.5
8
+ track_low_thresh: 0.1
9
+ new_track_thresh: 0.6
10
+ track_buffer: 30
11
+ match_thresh: 0.2
12
+ byte_match_thresh: 0.5
13
+ min_hits: 3
14
+ use_byte: true
15
+ use_cmc: true
16
+ use_imm: true
17
+ use_hybrid_distance: false
18
+ use_eiou: true
19
+ use_reid: true
20
+ use_gated_reid: false
21
+ use_mahalanobis_gate: false
22
+ eiou:
23
+ adaptive: true
24
+ adaptive_scale: 2.0
25
+ min_expansion: 0.2
26
+ max_expansion: 1.2
27
+ reid:
28
+ weights: weights/sportsmot/sports_osnet_traced.pt
29
+ crop_size:
30
+ - 128
31
+ - 256
32
+ w_assoc_emb: 1.25
33
+ alpha_fixed_emb: 0.95
34
+ aw_off: false
35
+ aw_param: 1.0
36
+ imm_gate_cv_thresh: 0.7
37
+ imm_gate_young_frames: 5
38
+ imm:
39
+ models:
40
+ - cv
41
+ - ca
42
+ - ct
43
+ transition_prob_diagonal: 0.98
44
+ process_noise_cv: 1.0
45
+ process_noise_ca: 1.0
46
+ process_noise_ct: 1.0
47
+ initial_model_probs:
48
+ - 0.7
49
+ - 0.15
50
+ - 0.15
51
+ hybrid_distance:
52
+ iou_fallback_thresh: 0.1
53
+ shape_penalty_weight: 0.3
54
+ cmc:
55
+ max_corners: 1000
56
+ quality_level: 0.01
57
+ min_distance: 1
58
+ block_size: 3
59
+ ransac_threshold: 3.0
60
+ min_inliers: 10
61
+ downscale: 2
62
+ ocsort:
63
+ use_oru: true
64
+ use_ocm: true
65
+ use_ocr: true
66
+ use_cmc_oru: true
67
+ delta_t: 3
68
+ inertia_weight: 0.2