seavision-python 0.1.1__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.
Files changed (73) hide show
  1. seavision/__init__.py +3 -0
  2. seavision/defaults.py +38 -0
  3. seavision/edge/__init__.py +38 -0
  4. seavision/edge/bundle.py +189 -0
  5. seavision/edge/capture.py +107 -0
  6. seavision/edge/config.py +219 -0
  7. seavision/edge/postprocess.py +185 -0
  8. seavision/edge/runtime.py +416 -0
  9. seavision/edge/writer.py +167 -0
  10. seavision/engine/__init__.py +52 -0
  11. seavision/engine/detectors/__init__.py +90 -0
  12. seavision/engine/detectors/base.py +179 -0
  13. seavision/engine/detectors/motion/__init__.py +14 -0
  14. seavision/engine/detectors/motion/background.py +92 -0
  15. seavision/engine/detectors/motion/detector.py +218 -0
  16. seavision/engine/detectors/motion/stabiliser.py +174 -0
  17. seavision/engine/detectors/motion/tracker.py +174 -0
  18. seavision/engine/detectors/sam3/__init__.py +23 -0
  19. seavision/engine/detectors/sam3/config.py +309 -0
  20. seavision/engine/detectors/sam3/detector.py +1316 -0
  21. seavision/engine/detectors/sam3/native.py +490 -0
  22. seavision/engine/detectors/yolo/__init__.py +12 -0
  23. seavision/engine/detectors/yolo/config.py +39 -0
  24. seavision/engine/detectors/yolo/detector.py +236 -0
  25. seavision/engine/export/__init__.py +24 -0
  26. seavision/engine/export/config.py +160 -0
  27. seavision/engine/export/exporter.py +286 -0
  28. seavision/engine/export/validation.py +314 -0
  29. seavision/engine/postprocessor.py +738 -0
  30. seavision/engine/source/__init__.py +16 -0
  31. seavision/engine/source/base.py +87 -0
  32. seavision/engine/source/discovery.py +154 -0
  33. seavision/engine/source/local.py +155 -0
  34. seavision/engine/source/s3.py +231 -0
  35. seavision/engine/visualiser/__init__.py +62 -0
  36. seavision/engine/visualiser/annotator.py +382 -0
  37. seavision/engine/visualiser/config.py +127 -0
  38. seavision/engine/visualiser/loader.py +558 -0
  39. seavision/engine/visualiser/visualiser.py +402 -0
  40. seavision/engine/visualiser/writer.py +245 -0
  41. seavision/gui/__init__.py +0 -0
  42. seavision/gui/app.py +30 -0
  43. seavision/gui/main_window.py +891 -0
  44. seavision/gui/shared/__init__.py +0 -0
  45. seavision/gui/shared/conversion.py +55 -0
  46. seavision/gui/shared/session_utils.py +37 -0
  47. seavision/gui/validation/__init__.py +0 -0
  48. seavision/gui/validation/button_styles.py +144 -0
  49. seavision/gui/validation/detection_detail.py +164 -0
  50. seavision/gui/validation/detection_rect_item.py +486 -0
  51. seavision/gui/validation/detection_table.py +563 -0
  52. seavision/gui/validation/interactive_frame_scene.py +340 -0
  53. seavision/gui/validation/interactive_frame_view.py +252 -0
  54. seavision/gui/validation/s3_browser.py +645 -0
  55. seavision/gui/validation/seekable_source.py +262 -0
  56. seavision/gui/validation/session.py +376 -0
  57. seavision/gui/validation/tab.py +2503 -0
  58. seavision/gui/validation/transport_bar.py +172 -0
  59. seavision/gui/validation/validation_model.py +482 -0
  60. seavision/gui/validation/video_cache.py +215 -0
  61. seavision/gui/validation/video_list.py +140 -0
  62. seavision/gui/validation/video_viewer.py +150 -0
  63. seavision/gui/validation/video_worker.py +422 -0
  64. seavision/pipeline.py +856 -0
  65. seavision/resources/default.yaml +139 -0
  66. seavision/run_edge.py +96 -0
  67. seavision/run_export.py +176 -0
  68. seavision/run_pipeline.py +388 -0
  69. seavision_python-0.1.1.dist-info/METADATA +286 -0
  70. seavision_python-0.1.1.dist-info/RECORD +73 -0
  71. seavision_python-0.1.1.dist-info/WHEEL +5 -0
  72. seavision_python-0.1.1.dist-info/entry_points.txt +5 -0
  73. seavision_python-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,218 @@
1
+ """Motion detector combining stabilisation and background modelling."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, Iterator, Optional
5
+ import cv2
6
+ import numpy as np
7
+
8
+ from ...source import FrameContext
9
+ from ..base import Detection, DetectorBase
10
+ from .stabiliser import FrameStabiliser, StabiliserConfig
11
+ from .background import BackgroundModel, BackgroundConfig
12
+
13
+
14
+ @dataclass
15
+ class MotionDetectorConfig:
16
+ """
17
+ Configuration for the motion detector.
18
+
19
+ Attributes:
20
+ stabiliser: Configuration for the frame stabiliser.
21
+ background: Confiuguration for the background subtraction.
22
+ stabilisation_enabled: Whether to enable frame stabilisation.
23
+ min_area: Minimum area (pixels) for motion contours to be considered.
24
+ max_area: Maximum area (pixels) for motion contours to be considered.
25
+ morph_kernel_size: Kernel size for morphological operations.
26
+ morph_iterations: Number of iterations for morphological operations.
27
+ """
28
+
29
+ stabiliser: StabiliserConfig = field(
30
+ default_factory=StabiliserConfig
31
+ )
32
+ background: BackgroundConfig = field(
33
+ default_factory=BackgroundConfig
34
+ )
35
+ stabilisation_enabled: bool = True
36
+ min_area: int = 100 # pixels
37
+ max_area: int = 50000 # pixels
38
+ morph_kernel_size: int = 5
39
+ morph_iterations: int = 2
40
+
41
+ # Persistence filtering
42
+ persistence_enabled: bool = True
43
+ min_persistence: int = 3 # Frames before emitting detection
44
+ max_frames_missing: int = 5 # Frames before dropping track
45
+ iou_threshold: float = 0.3 # Minimum IoU to match detections
46
+
47
+
48
+ class MotionDetector(DetectorBase):
49
+ """
50
+ Detects motion in video frames using background subtraction in the
51
+ following pipeline:
52
+ 1. (Optional) Stabilise frame to compensate for camera motion.
53
+ 2. Apply background subtraction to get a foreground mask.
54
+ 3. Clean mask with morphological operations.
55
+ 4. Find countours and filter by area.
56
+ 5. Emit Detection objects for valid contours.
57
+
58
+ Example:
59
+ config = MotionDetectorConfig(min_area=200)
60
+
61
+ with MotionDetector(config) as detector:
62
+ for frame, context in video_source.iter_frames():
63
+ for detection in detector.process_frame(frame, context):
64
+ # process detection
65
+ pass
66
+ """
67
+
68
+ def __init__(self, config: Optional[MotionDetectorConfig] = None):
69
+ """
70
+ Initialise the motion detector.
71
+
72
+ Args:
73
+ config: Configuration for the motion detector. If None, defaults
74
+ are used.
75
+ """
76
+
77
+ self.config = config or MotionDetectorConfig()
78
+
79
+ self._stabiliser = FrameStabiliser(self.config.stabiliser)
80
+ self._background = BackgroundModel(self.config.background)
81
+
82
+ # Morphological operation kernel
83
+ self._morph_kernel = cv2.getStructuringElement(
84
+ cv2.MORPH_ELLIPSE,
85
+ (self.config.morph_kernel_size, self.config.morph_kernel_size)
86
+ )
87
+
88
+ # Persistence tracker
89
+ if self.config.persistence_enabled:
90
+ from .tracker import PersistenceTracker, TrackerConfig
91
+ tracker_config = TrackerConfig(
92
+ min_persistence=self.config.min_persistence,
93
+ max_frames_missing=self.config.max_frames_missing,
94
+ iou_threshold=self.config.iou_threshold
95
+ )
96
+ self._tracker = PersistenceTracker(tracker_config)
97
+ else:
98
+ self._tracker = None
99
+
100
+
101
+ def process_frame(
102
+ self,
103
+ frame: np.ndarray,
104
+ context: FrameContext
105
+ ) -> Iterator[Detection]:
106
+ """
107
+ Process a single video frame and yield motion detections.
108
+
109
+ Args:
110
+ frame: BGR image as a numpy array (H x W x 3).
111
+ context: Metadata about the current frame (FrameContext class).
112
+
113
+ Yields:
114
+ Detection objects for each motion contour found in the frame.
115
+ """
116
+
117
+ # Stage 1: Stabilisation
118
+ if self.config.stabilisation_enabled:
119
+ stabilised, _ = self._stabiliser.stabilise(frame)
120
+ else:
121
+ stabilised = frame
122
+
123
+ # Stage 2: Background subtraction
124
+ mask = self._background.apply(stabilised)
125
+
126
+ # Stage 3: Morphological cleaning
127
+ mask = self._clean_mask(mask)
128
+
129
+ # Stage 4: Extract raw detections
130
+ raw_detections = list(self._extract_detections(mask, context))
131
+
132
+ # Stage 5: Persistence filtering
133
+ if self._tracker is not None:
134
+ confirmed_tracks = self._tracker.update(raw_detections)
135
+
136
+ for track in confirmed_tracks:
137
+ yield Detection(
138
+ source_file=context.source_file,
139
+ timestamp=context.timestamp,
140
+ frame_number=context.frame_number,
141
+ xc=track.xc,
142
+ yc=track.yc,
143
+ width=track.width,
144
+ height=track.height,
145
+ confidence=None # No confidence score for motion detection
146
+ )
147
+ else:
148
+ # No tracking - emit all detections
149
+ for det in raw_detections:
150
+ yield Detection(
151
+ source_file=context.source_file,
152
+ timestamp=context.timestamp,
153
+ frame_number=context.frame_number,
154
+ **det
155
+ )
156
+
157
+ def _clean_mask(self, mask: np.ndarray) -> np.ndarray:
158
+ """
159
+ Apply morphological operations to clean the foreground mask.
160
+ """
161
+
162
+ # Opening (erosion then dilation) removes small noise
163
+ mask = cv2.morphologyEx(
164
+ mask,
165
+ cv2.MORPH_OPEN,
166
+ self._morph_kernel,
167
+ iterations=self.config.morph_iterations
168
+ )
169
+
170
+ # Closing (dilation then erosion) fills small holes
171
+ mask = cv2.morphologyEx(
172
+ mask,
173
+ cv2.MORPH_CLOSE,
174
+ self._morph_kernel,
175
+ iterations=self.config.morph_iterations
176
+ )
177
+
178
+ return mask
179
+
180
+
181
+ def _extract_detections(
182
+ self,
183
+ mask: np.ndarray,
184
+ context: FrameContext
185
+ ) -> Iterator[Dict]:
186
+ """
187
+ Find contours in the mask and yield Detection dicts (for tracker input).
188
+ """
189
+
190
+ contours, _ = cv2.findContours(
191
+ mask,
192
+ cv2.RETR_EXTERNAL,
193
+ cv2.CHAIN_APPROX_SIMPLE
194
+ )
195
+
196
+ for contour in contours:
197
+ area = cv2.contourArea(contour)
198
+
199
+ # Filter by size
200
+ if area < self.config.min_area or area > self.config.max_area:
201
+ continue
202
+
203
+ # Get bounding box
204
+ x, y, w, h = cv2.boundingRect(contour)
205
+
206
+ yield {
207
+ "xc": x + w / 2,
208
+ "yc": y + h / 2,
209
+ "width": w,
210
+ "height": h,
211
+ }
212
+
213
+ def reset(self) -> None:
214
+ """Reset internal state between videos."""
215
+ self._stabiliser.reset()
216
+ self._background.reset()
217
+ if self._tracker is not None:
218
+ self._tracker.reset()
@@ -0,0 +1,174 @@
1
+ """Frame stabilisation using feature-based homography."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Tuple
5
+ import cv2
6
+ import numpy as np
7
+
8
+
9
+ @dataclass
10
+ class StabiliserConfig:
11
+ """
12
+ Configuration for frame stabilisation.
13
+
14
+ # TODO: Glossary document which defines these things?
15
+ Attributes:
16
+ feature_detector: Feature detector type (e.g. 'ORB', 'AKAZE').
17
+ max_features: Maximum number of features to detect.
18
+ match_ratio: Lowe's ratio test threshold for feature matching.
19
+ min_matches: Minimum number of matches to compute homography.
20
+ ransac_threshold: RANSAC reprojection threshold for homography.
21
+ """
22
+
23
+ feature_detector: str = "ORB"
24
+ max_features: int = 500
25
+ match_ratio: float = 0.75
26
+ min_matches: int = 10
27
+ ransac_threshold: float = 5.0
28
+
29
+
30
+ class FrameStabiliser:
31
+ """
32
+ Stabilises frames by aligning to the previous frame.
33
+
34
+ Uses feature-based homography estimation to compensate for camera motion.
35
+
36
+ Example:
37
+ stabiliser = FrameStabiliser()
38
+
39
+ for frame in video_frames:
40
+ stable_frame = stabiliser.stabilise(frame)
41
+ # process stable_frame
42
+ """
43
+
44
+ def __init__(self, config: Optional[StabiliserConfig] = None):
45
+ """
46
+ Initialise the frame stabiliser.
47
+
48
+ Args:
49
+ config: Configuration for the stabiliser. If None, defaults are used.
50
+ """
51
+
52
+ self.config = config or StabiliserConfig()
53
+
54
+ # Initialise the feature detector
55
+ # TODO: Are there any more obvious detector options that should be implemented?
56
+ if self.config.feature_detector == "ORB":
57
+ self._detector = cv2.ORB_create(nfeatures=self.config.max_features)
58
+ elif self.config.feature_detector == "AKAZE":
59
+ self._detector = cv2.AKAZE_create()
60
+ else:
61
+ raise ValueError(f"Unknown detector: {self.config.feature_detector}")
62
+
63
+ # Brute-force matcher with Hamming distance (for binary descriptors)
64
+ # TODO: What is a matcher? this should be in the glossary
65
+ self._matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
66
+
67
+ # State
68
+ self._prev_grey: Optional[np.ndarray] = None
69
+ self._prev_keypoints: Optional[list] = None
70
+ self._prev_descriptors: Optional[np.ndarray] = None
71
+
72
+ def stabilise(self, frame: np.ndarray) -> Tuple[np.ndarray, bool]:
73
+ """
74
+ Stabilise a frame by aligning it to the previous frame.
75
+
76
+ Args:
77
+ frame: Input BGR frame as a numpy array.
78
+
79
+ Returns:
80
+ Tuple of (stabilised_frame, is_stabilised) where stabilised_frame
81
+ is the aligned frame and is_stabilised indicates if alignment was
82
+ applied. Returns the original frame if is_stabilised is False.
83
+ """
84
+
85
+ # Convert to greyscale - feature detection works on intensity values
86
+ grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
87
+
88
+ # Detect distinctive visual features (corners, edges) and compute
89
+ # binary descriptors that summarise the local appearance around each
90
+ keypoints, descriptors = self._detector.detectAndCompute(grey, None)
91
+
92
+ # First frame - nothing to align to
93
+ if self._prev_grey is None:
94
+ self._update_reference(grey, keypoints, descriptors)
95
+ return frame, True
96
+
97
+ # Not enough features in current frame
98
+ if descriptors is None or len(keypoints) < self.config.min_matches:
99
+ self._update_reference(grey, keypoints, descriptors)
100
+ return frame, False
101
+
102
+ # Match features to previous frame. For each feature in the previous
103
+ # frame, find the 2 most similar features in the current frame (k=2).
104
+ # Similarity is measured by Hamming distance between binary descriptors.
105
+ matches = self._matcher.knnMatch(
106
+ self._prev_descriptors, descriptors, k=2
107
+ )
108
+
109
+ # Apply Lowe's ratio test to filter ambiguous matches. A match is
110
+ # considered good if the best match (m) is significantly better than
111
+ # the second-best match (n). This rejects features that could match
112
+ # multiple locations (e.g., repetitive patterns).
113
+ good_matches = []
114
+ for m, n in matches:
115
+ if m.distance < self.config.match_ratio * n.distance:
116
+ good_matches.append(m)
117
+
118
+ # Not enough good matches
119
+ if len(good_matches) < self.config.min_matches:
120
+ self._update_reference(grey, keypoints, descriptors)
121
+ return frame, False
122
+
123
+ # Extract matched point coordinates:
124
+ # - src_pts: where features were located in the PREVIOUS frame
125
+ # - dst_pts: where those same features are in the CURRENT frame
126
+ # The difference between these tells us how the camera moved.
127
+ src_pts = np.float32([
128
+ self._prev_keypoints[m.queryIdx].pt for m in good_matches
129
+ ]).reshape(-1, 1, 2)
130
+ dst_pts = np.float32([
131
+ keypoints[m.trainIdx].pt for m in good_matches
132
+ ]).reshape(-1, 1, 2)
133
+
134
+ # Compute homography matrix H that maps dst_pts -> src_pts.
135
+ # H is a 3x3 transformation matrix that describes the camera motion
136
+ # (rotation, translation, perspective change) between frames.
137
+ # RANSAC robustly estimates H by ignoring outlier matches (e.g., from
138
+ # moving objects or mismatches).
139
+ H, mask = cv2.findHomography(
140
+ dst_pts, src_pts,
141
+ cv2.RANSAC,
142
+ self.config.ransac_threshold
143
+ )
144
+
145
+ if H is None:
146
+ self._update_reference(grey, keypoints, descriptors)
147
+ return frame, False
148
+
149
+ # Warp current frame to align with previous frame.
150
+ # This applies the inverse camera motion, so stationary background
151
+ # objects appear in the same position as the previous frame.
152
+ height, width = frame.shape[:2]
153
+ stabilised_frame = cv2.warpPerspective(frame, H, (width, height))
154
+
155
+ # Update reference frame
156
+ self._update_reference(grey, keypoints, descriptors)
157
+ return stabilised_frame, True
158
+
159
+ def _update_reference(
160
+ self,
161
+ grey: np.ndarray,
162
+ keypoints: Tuple,
163
+ descriptors: Optional[np.ndarray]
164
+ ) -> None:
165
+ """Update the reference frame and features."""
166
+ self._prev_grey = grey
167
+ self._prev_keypoints = keypoints
168
+ self._prev_descriptors = descriptors
169
+
170
+ def reset(self) -> None:
171
+ """Reset the stabiliser state."""
172
+ self._prev_grey = None
173
+ self._prev_keypoints = None
174
+ self._prev_descriptors = None
@@ -0,0 +1,174 @@
1
+ """Simple multi-object tracker for persistence filtering."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, List, Optional, Tuple
5
+ import numpy as np
6
+
7
+
8
+ @dataclass
9
+ class TrackedObject:
10
+ """A tracked detection across multiple frames."""
11
+
12
+ track_id: int
13
+ xc: float
14
+ yc: float
15
+ width: float
16
+ height: float
17
+ age: int = 1 # Frames since first seen
18
+ frames_since_update: int = 0 # Frames since last matched
19
+
20
+
21
+ def update(self, xc: float, yc: float, width: float, height: float) -> None:
22
+ """Update the tracked object with new detection data."""
23
+ self.xc = xc
24
+ self.yc = yc
25
+ self.width = width
26
+ self.height = height
27
+ self.age += 1
28
+ self.frames_since_update = 0
29
+
30
+ def mark_missed(self) -> None:
31
+ """Mark that no detection matched this frame."""
32
+ self.frames_since_update += 1
33
+
34
+ @property
35
+ def bbox(self) -> Tuple[float, float, float, float]:
36
+ """Get bounding box as (x1, y1, x2, y2)."""
37
+ x1 = self.xc - self.width / 2
38
+ y1 = self.yc - self.height / 2
39
+ x2 = self.xc + self.width / 2
40
+ y2 = self.yc + self.height / 2
41
+ return (x1, y1, x2, y2)
42
+
43
+
44
+ @dataclass
45
+ class TrackerConfig:
46
+ """Configuration for the persistence tracker."""
47
+
48
+ min_persistence: int = 3 # Frames before emitting detection
49
+ max_frames_missing: int = 5 # Frames before dropping track
50
+ iou_threshold: float = 0.3 # Minimum IoU to match detections
51
+
52
+
53
+ def compute_iou(box1: Tuple[float, float, float, float],
54
+ box2: Tuple[float, float, float, float]) -> float:
55
+ """Compute Intersection over Union between two boxes (x1,y1,x2,y2)."""
56
+ x1 = max(box1[0], box2[0])
57
+ y1 = max(box1[1], box2[1])
58
+ x2 = min(box1[2], box2[2])
59
+ y2 = min(box1[3], box2[3])
60
+
61
+ intersection = max(0, x2 - x1) * max(0, y2 - y1)
62
+
63
+ area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
64
+ area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
65
+ union = area1 + area2 - intersection
66
+
67
+ return intersection / union if union > 0 else 0
68
+
69
+
70
+ class PersistenceTracker:
71
+ """
72
+ tracks detections across frames and filters out transient noise.
73
+
74
+ Only emits detections that have persisted for min_persistence frames.
75
+
76
+ Example:
77
+ tracker = PersistenceTracker(TrackerConfig(min_persistence=3))
78
+
79
+ for frame_detections in all_detections:
80
+ confirmed = tracker.update(frame_detections)
81
+ # confirmed only contains detections seen for 3+ frames
82
+ """
83
+
84
+ def __init__(self, config: Optional[TrackerConfig] = None):
85
+ self.config = config or TrackerConfig()
86
+ self._tracks: Dict[int, TrackedObject] = {}
87
+ self._next_id: int = 0
88
+
89
+ def update(self, detections: List[dict]) -> List[TrackedObject]:
90
+ """
91
+ Update tracker with new frame detections.
92
+
93
+ Args:
94
+ detections: List of detection dicts with keys:
95
+ 'xc', 'yc', 'width', 'height'.
96
+
97
+ Returns:
98
+ A list of tracked objects that have a persistence >=
99
+ min_persistence frames.
100
+ """
101
+
102
+ # Convert detections to bbox format for matching
103
+ det_boxes = []
104
+ for det in detections:
105
+ x1 = det["xc"] - det["width"] / 2
106
+ y1 = det["yc"] - det["height"] / 2
107
+ x2 = det["xc"] + det["width"] / 2
108
+ y2 = det["yc"] + det["height"] / 2
109
+ det_boxes.append((x1, y1, x2, y2))
110
+
111
+ # Match detections to existing tracks using IoU
112
+ matched_tracks = set()
113
+ matched_dets = set()
114
+
115
+ # Greedy matching, highest IoU first
116
+ if self._tracks and det_boxes:
117
+ iou_matrix = []
118
+
119
+ for track_id, track in self._tracks.items():
120
+ for det_idx, det_box in enumerate(det_boxes):
121
+ iou = compute_iou(track.bbox, det_box)
122
+ if iou >= self.config.iou_threshold:
123
+ iou_matrix.append((iou, track_id, det_idx))
124
+
125
+ # Sort by IoU descending
126
+ iou_matrix.sort(reverse=True, key=lambda x: x[0])
127
+
128
+ for iou, track_id, det_idx in iou_matrix:
129
+ if track_id not in matched_tracks and det_idx not in matched_dets:
130
+ # Update track with matched detection
131
+ det = detections[det_idx]
132
+ self._tracks[track_id].update(
133
+ det["xc"], det["yc"], det["width"], det["height"]
134
+ )
135
+ matched_tracks.add(track_id)
136
+ matched_dets.add(det_idx)
137
+
138
+ # Mark unmatched tracks as missed
139
+ for track_id in self._tracks:
140
+ if track_id not in matched_tracks:
141
+ self._tracks[track_id].mark_missed()
142
+
143
+ # Create new tracks for unmatched detections
144
+ for det_idx, det in enumerate(detections):
145
+ if det_idx not in matched_dets:
146
+ self._tracks[self._next_id] = TrackedObject(
147
+ track_id=self._next_id,
148
+ xc=det["xc"],
149
+ yc=det["yc"],
150
+ width=det["width"],
151
+ height=det["height"],
152
+ )
153
+ self._next_id += 1
154
+
155
+ # Remove stale tracks
156
+ stale_ids = [
157
+ track_id for track_id, track in self._tracks.items()
158
+ if track.frames_since_update > self.config.max_frames_missing
159
+ ]
160
+ for track_id in stale_ids:
161
+ del self._tracks[track_id]
162
+
163
+ # Return only tracks that have persisted long enough
164
+ confirmed = [
165
+ track for track in self._tracks.values()
166
+ if track.age >= self.config.min_persistence
167
+ ]
168
+
169
+ return confirmed
170
+
171
+ def reset(self) -> None:
172
+ """Reset the tracker state for a new video."""
173
+ self._tracks.clear()
174
+ self._next_id = 0
@@ -0,0 +1,23 @@
1
+ """SAM 3 (Segment Anything Model 3) detector module."""
2
+
3
+ from .config import (
4
+ PromptType,
5
+ HybridStrategy,
6
+ PrompterConfig,
7
+ PromptConfig,
8
+ SAM3DetectorConfig,
9
+ )
10
+ from .detector import SAM3Detector
11
+ from .native import SAM3NativeDetector
12
+
13
+ __all__ = [
14
+ # Config
15
+ "PromptType",
16
+ "HybridStrategy",
17
+ "PrompterConfig",
18
+ "PromptConfig",
19
+ "SAM3DetectorConfig",
20
+ # Detector
21
+ "SAM3Detector",
22
+ "SAM3NativeDetector",
23
+ ]