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/__init__.py +23 -0
- arcsort/association.py +529 -0
- arcsort/cmc.py +169 -0
- arcsort/configs/__init__.py +35 -0
- arcsort/configs/dancetrack.yaml +74 -0
- arcsort/configs/sportsmot.yaml +80 -0
- arcsort/configs/sportsmot_deepeiou_inputs.yaml +68 -0
- arcsort/detection.py +367 -0
- arcsort/imm_kalman_filter.py +977 -0
- arcsort/interop.py +42 -0
- arcsort/kalman_filter.py +347 -0
- arcsort/matching.py +484 -0
- arcsort/ocsort_components.py +160 -0
- arcsort/reid.py +326 -0
- arcsort/track.py +237 -0
- arcsort/tracker.py +1181 -0
- arcsort/utils.py +141 -0
- arcsort/visualizer.py +109 -0
- arcsort-0.1.0.dist-info/METADATA +301 -0
- arcsort-0.1.0.dist-info/RECORD +25 -0
- arcsort-0.1.0.dist-info/WHEEL +5 -0
- arcsort-0.1.0.dist-info/licenses/LICENSE +202 -0
- arcsort-0.1.0.dist-info/licenses/NOTICE +17 -0
- arcsort-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +49 -0
- arcsort-0.1.0.dist-info/top_level.txt +1 -0
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
|