@camera.ui/rust-postprocessor 0.0.1 → 0.0.2

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.
package/index.d.ts CHANGED
@@ -179,6 +179,15 @@ export declare function merge(detections: Array<Detection>, iouThreshold: number
179
179
  */
180
180
  export declare function nms(detections: Array<Detection>, iouThreshold: number, maxDetections?: number | undefined | null): Array<Detection>
181
181
 
182
+ /**
183
+ * Run greedy NMS and return the **indices** (positions in the input
184
+ * array) of the surviving detections, sorted by confidence descending.
185
+ * Use this instead of [`nms`] when the caller needs to preserve extra
186
+ * fields on the original detection objects (e.g. face identity, plate
187
+ * text) that would be lost by the Detection → RustDetection round-trip.
188
+ */
189
+ export declare function nmsIndices(detections: Array<Detection>, iouThreshold: number): Array<number>
190
+
182
191
  /** Constructor options for [`ObjectTracker`]. */
183
192
  export interface ObjectTrackerOptions {
184
193
  /**
package/index.js CHANGED
@@ -581,5 +581,6 @@ module.exports.boxIou = nativeBinding.boxIou
581
581
  module.exports.LineDirection = nativeBinding.LineDirection
582
582
  module.exports.merge = nativeBinding.merge
583
583
  module.exports.nms = nativeBinding.nms
584
+ module.exports.nmsIndices = nativeBinding.nmsIndices
584
585
  module.exports.ZoneFilterMode = nativeBinding.ZoneFilterMode
585
586
  module.exports.ZoneMatchType = nativeBinding.ZoneMatchType
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camera.ui/rust-postprocessor",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "camera.ui rust postprocessor",
5
5
  "author": "seydx (https://github.com/seydx/camera.ui)",
6
6
  "main": "index.js",
@@ -72,15 +72,15 @@
72
72
  "node": ">= 10"
73
73
  },
74
74
  "optionalDependencies": {
75
- "@camera.ui/rust-postprocessor-darwin-x64": "0.0.1",
76
- "@camera.ui/rust-postprocessor-win32-x64-msvc": "0.0.1",
77
- "@camera.ui/rust-postprocessor-linux-x64-gnu": "0.0.1",
78
- "@camera.ui/rust-postprocessor-linux-x64-musl": "0.0.1",
79
- "@camera.ui/rust-postprocessor-freebsd-x64": "0.0.1",
80
- "@camera.ui/rust-postprocessor-darwin-arm64": "0.0.1",
81
- "@camera.ui/rust-postprocessor-win32-arm64-msvc": "0.0.1",
82
- "@camera.ui/rust-postprocessor-linux-arm64-gnu": "0.0.1",
83
- "@camera.ui/rust-postprocessor-linux-arm64-musl": "0.0.1",
84
- "@camera.ui/rust-postprocessor-linux-riscv64-gnu": "0.0.1"
75
+ "@camera.ui/rust-postprocessor-darwin-x64": "0.0.2",
76
+ "@camera.ui/rust-postprocessor-win32-x64-msvc": "0.0.2",
77
+ "@camera.ui/rust-postprocessor-linux-x64-gnu": "0.0.2",
78
+ "@camera.ui/rust-postprocessor-linux-x64-musl": "0.0.2",
79
+ "@camera.ui/rust-postprocessor-freebsd-x64": "0.0.2",
80
+ "@camera.ui/rust-postprocessor-darwin-arm64": "0.0.2",
81
+ "@camera.ui/rust-postprocessor-win32-arm64-msvc": "0.0.2",
82
+ "@camera.ui/rust-postprocessor-linux-arm64-gnu": "0.0.2",
83
+ "@camera.ui/rust-postprocessor-linux-arm64-musl": "0.0.2",
84
+ "@camera.ui/rust-postprocessor-linux-riscv64-gnu": "0.0.2"
85
85
  }
86
86
  }
package/.yarnrc.yml DELETED
@@ -1 +0,0 @@
1
- nodeLinker: node-modules
package/Cargo.toml DELETED
@@ -1,29 +0,0 @@
1
- [package]
2
- edition = "2021"
3
- name = "cameraui-rust-postprocessor"
4
- version = "0.0.0"
5
-
6
- [lib]
7
- crate-type = ["cdylib"]
8
-
9
- [dependencies]
10
- napi = { version = "3.8.4", default-features = false, features = ["napi9"] }
11
- napi-derive = "3.5.3"
12
-
13
- # Tracker engine — Rust port of the Python `norfair` library used by Frigate.
14
- # Same battle-tested algorithm: Kalman filter, IoU/Euclidean distance, ReID lifecycle.
15
- norfair-rs = { version = "0.4.1", default-features = false }
16
-
17
- # Stable nalgebra for the few places we construct DMatrix ourselves
18
- nalgebra = "0.34.2"
19
-
20
- # SIMD primitives for our NMS implementation (f32x8 = 8 floats parallel).
21
- wide = "1.2.0"
22
-
23
- [build-dependencies]
24
- napi-build = "2.3.1"
25
-
26
- [profile.release]
27
- lto = true
28
- strip = "symbols"
29
- codegen-units = 1
package/build.rs DELETED
@@ -1,5 +0,0 @@
1
- extern crate napi_build;
2
-
3
- fn main() {
4
- napi_build::setup();
5
- }
@@ -1,2 +0,0 @@
1
- [toolchain]
2
- channel = "nightly-2025-09-30"
package/src/iou.rs DELETED
@@ -1,83 +0,0 @@
1
- //! Intersection-over-Union helpers.
2
- //!
3
- //! Boxes are in normalized `[x, y, width, height]` format with coordinates in
4
- //! `[0.0, 1.0]`. This matches the convention used by the Node SDK
5
- //! (`BoundingBox`).
6
-
7
- /// Compute IoU between two `[x, y, width, height]` boxes.
8
- ///
9
- /// Returns 0 if either box has non-positive area or there is no overlap.
10
- #[inline]
11
- pub fn box_iou(a: &[f32; 4], b: &[f32; 4]) -> f32 {
12
- let ax2 = a[0] + a[2];
13
- let ay2 = a[1] + a[3];
14
- let bx2 = b[0] + b[2];
15
- let by2 = b[1] + b[3];
16
-
17
- let ix1 = a[0].max(b[0]);
18
- let iy1 = a[1].max(b[1]);
19
- let ix2 = ax2.min(bx2);
20
- let iy2 = ay2.min(by2);
21
-
22
- if ix2 <= ix1 || iy2 <= iy1 {
23
- return 0.0;
24
- }
25
-
26
- let inter = (ix2 - ix1) * (iy2 - iy1);
27
- let a_area = a[2] * a[3];
28
- let b_area = b[2] * b[3];
29
- let union = a_area + b_area - inter;
30
-
31
- if union > 0.0 {
32
- inter / union
33
- } else {
34
- 0.0
35
- }
36
- }
37
-
38
- #[cfg(test)]
39
- mod tests {
40
- use super::*;
41
-
42
- #[test]
43
- fn perfect_overlap() {
44
- let a = [0.1, 0.1, 0.2, 0.2];
45
- let b = [0.1, 0.1, 0.2, 0.2];
46
- assert!((box_iou(&a, &b) - 1.0).abs() < 1e-6);
47
- }
48
-
49
- #[test]
50
- fn no_overlap() {
51
- let a = [0.0, 0.0, 0.1, 0.1];
52
- let b = [0.5, 0.5, 0.1, 0.1];
53
- assert_eq!(box_iou(&a, &b), 0.0);
54
- }
55
-
56
- #[test]
57
- fn half_overlap_horizontal() {
58
- // a = [0..0.2] x [0..0.2], b = [0.1..0.3] x [0..0.2]
59
- // intersection = 0.1 * 0.2 = 0.02
60
- // union = 0.04 + 0.04 - 0.02 = 0.06
61
- // iou = 1/3
62
- let a = [0.0, 0.0, 0.2, 0.2];
63
- let b = [0.1, 0.0, 0.2, 0.2];
64
- let v = box_iou(&a, &b);
65
- assert!((v - 1.0 / 3.0).abs() < 1e-6, "got {v}");
66
- }
67
-
68
- #[test]
69
- fn contained() {
70
- let a = [0.0, 0.0, 1.0, 1.0];
71
- let b = [0.25, 0.25, 0.5, 0.5];
72
- // inter = 0.25, union = 1.0
73
- let v = box_iou(&a, &b);
74
- assert!((v - 0.25).abs() < 1e-6, "got {v}");
75
- }
76
-
77
- #[test]
78
- fn zero_area() {
79
- let a = [0.0, 0.0, 0.0, 0.0];
80
- let b = [0.0, 0.0, 0.5, 0.5];
81
- assert_eq!(box_iou(&a, &b), 0.0);
82
- }
83
- }
package/src/lib.rs DELETED
@@ -1,471 +0,0 @@
1
- //! camera.ui rust-postprocessor — high-performance NMS, IoU and object
2
- //! tracking exposed to Node.js via napi-rs.
3
- //!
4
- //! - [`nms`] — SIMD greedy NMS over a list of detections (per-class).
5
- //! - [`box_iou`] — single-pair IoU helper for normalized boxes.
6
- //! - [`ObjectTracker`] — multi-class IoU+Kalman tracker built on
7
- //! `norfair-rs` (Rust port of the Python norfair library used by Frigate).
8
- //!
9
- //! All coordinates are normalized to `[0.0, 1.0]` so the tracker is
10
- //! resolution-independent.
11
-
12
- mod iou;
13
- mod line_crossing;
14
- mod merge;
15
- mod nms;
16
- mod tracker;
17
- mod types;
18
- mod zone_filter;
19
-
20
- use napi_derive::napi;
21
-
22
- use crate::line_crossing::{
23
- CrossingDirection as InnerCrossingDirection, DetectionLineInput as InnerDetectionLineInput,
24
- LineDirectionFilter as InnerLineDirectionFilter,
25
- };
26
- use crate::tracker::{ObjectTracker as InnerObjectTracker, ObjectTrackerConfig};
27
- use crate::zone_filter::{
28
- ZoneFilterMode as InnerZoneFilterMode, ZoneInput as InnerZoneInput,
29
- ZoneMatchType as InnerZoneMatchType,
30
- };
31
-
32
- /// Detection in normalized image coordinates `[0.0, 1.0]`.
33
- #[napi(object)]
34
- pub struct Detection {
35
- pub x: f64,
36
- pub y: f64,
37
- pub width: f64,
38
- pub height: f64,
39
- pub confidence: f64,
40
- pub label: String,
41
- }
42
-
43
- /// Detection with stable identity assigned by [`ObjectTracker`].
44
- #[napi(object)]
45
- pub struct TrackedDetection {
46
- pub x: f64,
47
- pub y: f64,
48
- pub width: f64,
49
- pub height: f64,
50
- pub confidence: f64,
51
- pub label: String,
52
- /// Stable id across frames. Resets only on `ObjectTracker::reset()`.
53
- pub track_id: u32,
54
- /// Number of frames this track has existed (1 on first emit).
55
- pub track_age: u32,
56
- /// True when the box is being kept alive by Kalman extrapolation
57
- /// instead of a fresh detection match this frame.
58
- pub track_lost: bool,
59
- /// Average centroid speed in normalized units/second over a sliding
60
- /// ~1 s window of past positions. Consumers can compare against a
61
- /// threshold (typical: 0.05) to distinguish moving from stationary
62
- /// tracks. 0 when the track has only one sample so far.
63
- pub track_speed: f64,
64
- }
65
-
66
- /// Bounding box used by [`box_iou`].
67
- #[napi(object)]
68
- pub struct BoundingBox {
69
- pub x: f64,
70
- pub y: f64,
71
- pub width: f64,
72
- pub height: f64,
73
- }
74
-
75
- /// How an active zone affects matching detections.
76
- #[napi(string_enum = "kebab-case")]
77
- pub enum ZoneFilterMode {
78
- Include,
79
- Exclude,
80
- }
81
-
82
- /// Where in the polygon the box must sit for the zone to apply.
83
- #[napi(string_enum = "kebab-case")]
84
- pub enum ZoneMatchType {
85
- /// Any overlap with the polygon counts.
86
- Intersect,
87
- /// All four corners of the box must be inside the polygon.
88
- Contain,
89
- }
90
-
91
- /// User-facing detection zone definition (matches the SDK shape).
92
- ///
93
- /// `points` are polygon vertices in `[0, 100]` UI coordinates. The
94
- /// polygon is auto-closed so the caller does not need to repeat the
95
- /// first vertex at the end.
96
- #[napi(object)]
97
- pub struct DetectionZone {
98
- pub labels: Vec<String>,
99
- pub filter: ZoneFilterMode,
100
- /// Match mode (intersect vs contain). Mapped to `type` on the JS side
101
- /// of the SDK — the consumer is responsible for translating that field.
102
- pub match_type: ZoneMatchType,
103
- pub is_privacy_mask: bool,
104
- pub points: Vec<Vec<f64>>,
105
- }
106
-
107
- /// Crossing line direction filter.
108
- ///
109
- /// `"both"` accepts either direction. `"a-to-b"` and `"b-to-a"` only fire
110
- /// when the track moves from one specific side to the other.
111
- #[napi(string_enum = "kebab-case")]
112
- pub enum LineDirection {
113
- Both,
114
- AToB,
115
- BToA,
116
- }
117
-
118
- /// User-facing crossing line definition.
119
- ///
120
- /// `points` are the two handle endpoints in `[0, 100]` UI coordinates.
121
- /// `labels` is the set of allowed detection labels — empty means "any label".
122
- #[napi(object)]
123
- pub struct DetectionLine {
124
- pub name: String,
125
- pub direction: LineDirection,
126
- pub labels: Vec<String>,
127
- pub points: Vec<Vec<f64>>,
128
- }
129
-
130
- /// Crossing event emitted by [`ObjectTracker::update`].
131
- #[napi(object)]
132
- pub struct LineCrossingEvent {
133
- pub line_name: String,
134
- pub direction: LineDirection,
135
- pub track_id: u32,
136
- pub label: String,
137
- pub confidence: f64,
138
- /// Frame timestamp in milliseconds (forwarded from the `update()` call).
139
- pub timestamp_ms: f64,
140
- pub prev_x: f64,
141
- pub prev_y: f64,
142
- pub curr_x: f64,
143
- pub curr_y: f64,
144
- }
145
-
146
- /// Result of [`ObjectTracker::update`] — active tracks plus any crossings
147
- /// fired this frame.
148
- #[napi(object)]
149
- pub struct UpdateResult {
150
- pub tracked: Vec<TrackedDetection>,
151
- pub crossings: Vec<LineCrossingEvent>,
152
- }
153
-
154
- /// Constructor options for [`ObjectTracker`].
155
- #[napi(object)]
156
- pub struct ObjectTrackerOptions {
157
- /// IoU threshold above which a candidate is matched to an existing track.
158
- /// Higher = stricter matching. Default 0.3.
159
- pub iou_threshold: Option<f64>,
160
- /// Frames a track survives without a fresh detection (Kalman extrapolation
161
- /// continues during this window). Default 15.
162
- pub hit_counter_max: Option<i32>,
163
- /// Frames a new track must be matched before getting a permanent id —
164
- /// filters one-frame false positives. Default 3.
165
- pub initialization_delay: Option<i32>,
166
- /// Frames a dead track stays available for ReID re-matching. When a new
167
- /// detection appears near a dead track, norfair merges them — the old
168
- /// track ID is preserved. Default: disabled.
169
- pub reid_hit_counter_max: Option<i32>,
170
- }
171
-
172
- fn to_internal(d: Detection) -> crate::types::Detection {
173
- crate::types::Detection {
174
- x: d.x as f32,
175
- y: d.y as f32,
176
- width: d.width as f32,
177
- height: d.height as f32,
178
- confidence: d.confidence as f32,
179
- label: d.label,
180
- }
181
- }
182
-
183
- fn from_internal(d: crate::types::Detection) -> Detection {
184
- Detection {
185
- x: d.x as f64,
186
- y: d.y as f64,
187
- width: d.width as f64,
188
- height: d.height as f64,
189
- confidence: d.confidence as f64,
190
- label: d.label,
191
- }
192
- }
193
-
194
- fn zone_filter_to_internal(m: ZoneFilterMode) -> InnerZoneFilterMode {
195
- match m {
196
- ZoneFilterMode::Include => InnerZoneFilterMode::Include,
197
- ZoneFilterMode::Exclude => InnerZoneFilterMode::Exclude,
198
- }
199
- }
200
-
201
- fn zone_match_to_internal(m: ZoneMatchType) -> InnerZoneMatchType {
202
- match m {
203
- ZoneMatchType::Intersect => InnerZoneMatchType::Intersect,
204
- ZoneMatchType::Contain => InnerZoneMatchType::Contain,
205
- }
206
- }
207
-
208
- fn detection_zone_to_internal(zone: DetectionZone) -> Option<InnerZoneInput> {
209
- let mut points: Vec<[f64; 2]> = Vec::with_capacity(zone.points.len());
210
- for p in zone.points {
211
- if p.len() != 2 {
212
- return None;
213
- }
214
- points.push([p[0], p[1]]);
215
- }
216
- Some(InnerZoneInput {
217
- labels: zone.labels,
218
- filter: zone_filter_to_internal(zone.filter),
219
- match_type: zone_match_to_internal(zone.match_type),
220
- is_privacy_mask: zone.is_privacy_mask,
221
- points,
222
- })
223
- }
224
-
225
- fn line_direction_to_internal(d: LineDirection) -> InnerLineDirectionFilter {
226
- match d {
227
- LineDirection::Both => InnerLineDirectionFilter::Both,
228
- LineDirection::AToB => InnerLineDirectionFilter::AToB,
229
- LineDirection::BToA => InnerLineDirectionFilter::BToA,
230
- }
231
- }
232
-
233
- fn line_direction_from_internal(d: InnerCrossingDirection) -> LineDirection {
234
- match d {
235
- InnerCrossingDirection::AToB => LineDirection::AToB,
236
- InnerCrossingDirection::BToA => LineDirection::BToA,
237
- }
238
- }
239
-
240
- fn detection_line_to_internal(line: DetectionLine) -> Option<InnerDetectionLineInput> {
241
- // Validate `points` is exactly two `[x, y]` pairs.
242
- if line.points.len() != 2 {
243
- return None;
244
- }
245
- let p1 = &line.points[0];
246
- let p2 = &line.points[1];
247
- if p1.len() != 2 || p2.len() != 2 {
248
- return None;
249
- }
250
- Some(InnerDetectionLineInput {
251
- name: line.name,
252
- direction: line_direction_to_internal(line.direction),
253
- labels: line.labels,
254
- points: [[p1[0], p1[1]], [p2[0], p2[1]]],
255
- })
256
- }
257
-
258
- fn from_internal_tracked(d: crate::types::TrackedDetection) -> TrackedDetection {
259
- TrackedDetection {
260
- x: d.x as f64,
261
- y: d.y as f64,
262
- width: d.width as f64,
263
- height: d.height as f64,
264
- confidence: d.confidence as f64,
265
- label: d.label,
266
- track_id: d.track_id,
267
- track_age: d.track_age,
268
- track_lost: d.track_lost,
269
- track_speed: d.track_speed as f64,
270
- }
271
- }
272
-
273
- /// Run greedy non-maximum suppression on a list of detections.
274
- ///
275
- /// Detections are suppressed only against higher-confidence boxes of the
276
- /// same `label`. Output is sorted by confidence descending. Pass
277
- /// `maxDetections` to cap the result length.
278
- #[napi]
279
- pub fn nms(
280
- detections: Vec<Detection>,
281
- iou_threshold: f64,
282
- max_detections: Option<u32>,
283
- ) -> Vec<Detection> {
284
- let internal: Vec<crate::types::Detection> = detections.into_iter().map(to_internal).collect();
285
- let max = max_detections.map(|n| n as usize);
286
- let kept = crate::nms::nms(internal, iou_threshold as f32, max);
287
- kept.into_iter().map(from_internal).collect()
288
- }
289
-
290
- /// Cluster nearby/overlapping same-label detections into single union
291
- /// boxes via union-find. Two boxes join the same cluster if their
292
- /// top-left corners are within `closeThreshold` along both axes OR if
293
- /// their IoU exceeds `iouThreshold`. Each cluster collapses to one
294
- /// `Detection` whose box covers the axis-aligned union of all members
295
- /// (clamped to `[0, 1]`) and whose confidence is the maximum in the
296
- /// cluster. Different labels are never clustered together.
297
- #[napi]
298
- pub fn merge(
299
- detections: Vec<Detection>,
300
- iou_threshold: f64,
301
- close_threshold: f64,
302
- ) -> Vec<Detection> {
303
- let internal: Vec<crate::types::Detection> = detections.into_iter().map(to_internal).collect();
304
- let merged =
305
- crate::merge::merge_detections(internal, iou_threshold as f32, close_threshold as f32);
306
- merged.into_iter().map(from_internal).collect()
307
- }
308
-
309
- /// Compute IoU between two normalized `[x, y, width, height]` boxes.
310
- #[napi(js_name = "boxIou")]
311
- pub fn box_iou(a: BoundingBox, b: BoundingBox) -> f64 {
312
- let aa = [a.x as f32, a.y as f32, a.width as f32, a.height as f32];
313
- let bb = [b.x as f32, b.y as f32, b.width as f32, b.height as f32];
314
- crate::iou::box_iou(&aa, &bb) as f64
315
- }
316
-
317
- /// Multi-class IoU + Kalman object tracker.
318
- ///
319
- /// Wraps `norfair-rs` with one sub-tracker per class label. Track ids are
320
- /// stable across frames and globally unique across classes. The tracker is
321
- /// resolution-independent — feed normalized `[0.0, 1.0]` coordinates.
322
- #[napi]
323
- pub struct ObjectTracker {
324
- inner: InnerObjectTracker,
325
- }
326
-
327
- #[napi]
328
- impl ObjectTracker {
329
- #[napi(constructor)]
330
- pub fn new(options: Option<ObjectTrackerOptions>) -> Self {
331
- let mut config = ObjectTrackerConfig::default();
332
- if let Some(opts) = options {
333
- if let Some(t) = opts.iou_threshold {
334
- config.iou_threshold = t as f32;
335
- }
336
- if let Some(h) = opts.hit_counter_max {
337
- config.hit_counter_max = h;
338
- }
339
- if let Some(i) = opts.initialization_delay {
340
- config.initialization_delay = i;
341
- }
342
- if let Some(r) = opts.reid_hit_counter_max {
343
- config.reid_hit_counter_max = if r > 0 { Some(r) } else { None };
344
- }
345
- }
346
- Self {
347
- inner: InnerObjectTracker::new(config),
348
- }
349
- }
350
-
351
- /// Process one frame's detections and return active tracks plus any
352
- /// line-crossing events that fired this frame. Detections without
353
- /// overlap to existing tracks spawn new ones; existing tracks without a
354
- /// matching detection are kept alive via Kalman extrapolation until
355
- /// `hitCounterMax` frames have elapsed.
356
- ///
357
- /// `timestampMs` is forwarded onto emitted crossing events for sequencing
358
- /// — pass `Date.now()` (or any monotonic millisecond clock).
359
- #[napi]
360
- pub fn update(&mut self, detections: Vec<Detection>, timestamp_ms: f64) -> UpdateResult {
361
- let internal: Vec<crate::types::Detection> = detections.into_iter().map(to_internal).collect();
362
- let result = self.inner.update(internal, timestamp_ms);
363
- UpdateResult {
364
- tracked: result
365
- .tracked
366
- .into_iter()
367
- .map(from_internal_tracked)
368
- .collect(),
369
- crossings: result
370
- .crossings
371
- .into_iter()
372
- .map(|c| LineCrossingEvent {
373
- line_name: c.line_name,
374
- direction: line_direction_from_internal(c.direction),
375
- track_id: c.track_id,
376
- label: c.label,
377
- confidence: c.confidence as f64,
378
- timestamp_ms: c.timestamp_ms,
379
- prev_x: c.prev_pos[0] as f64,
380
- prev_y: c.prev_pos[1] as f64,
381
- curr_x: c.curr_pos[0] as f64,
382
- curr_y: c.curr_pos[1] as f64,
383
- })
384
- .collect(),
385
- }
386
- }
387
-
388
- /// Replace the configured crossing lines. Pass an empty array to disable
389
- /// line crossings entirely. The `aspectRatio` is the camera's
390
- /// `width / height` so the perpendicular crossing line ends up visually
391
- /// perpendicular to the handle the user drew.
392
- ///
393
- /// Crossing memory is cleared on every reconfigure so existing tracks
394
- /// will fire again the moment they cross a freshly-edited line.
395
- #[napi]
396
- pub fn set_lines(&mut self, lines: Vec<DetectionLine>, aspect_ratio: f64) {
397
- let internal: Vec<InnerDetectionLineInput> = lines
398
- .into_iter()
399
- .filter_map(detection_line_to_internal)
400
- .collect();
401
- self.inner.set_lines(internal, aspect_ratio as f32);
402
- }
403
-
404
- /// Replace the configured detection zones (privacy masks + active
405
- /// include/exclude regions). Pass an empty array to disable zone
406
- /// filtering entirely. Coordinates are in `[0, 100]` UI space — the
407
- /// tracker normalizes them internally and auto-closes polygons.
408
- ///
409
- /// The filter runs at the start of every `update()` call before
410
- /// detections reach the underlying tracker.
411
- #[napi]
412
- pub fn set_zones(&mut self, zones: Vec<DetectionZone>) {
413
- let internal: Vec<InnerZoneInput> = zones
414
- .into_iter()
415
- .filter_map(detection_zone_to_internal)
416
- .collect();
417
- self.inner.set_zones(internal);
418
- }
419
-
420
- /// Set the minimum detection confidence threshold. Detections below
421
- /// this score are dropped at the start of every `update()` call before
422
- /// they reach the zone filter or tracker. Default 0.0 (no threshold).
423
- #[napi]
424
- pub fn set_min_confidence(&mut self, min_confidence: f64) {
425
- self.inner.set_min_confidence(min_confidence as f32);
426
- }
427
-
428
- /// Set how many frames a dead track stays available for ReID re-matching.
429
- /// When a new detection appears near a dead track's last position,
430
- /// norfair merges them — the old track ID is preserved with fresh
431
- /// Kalman state. Pass 0 to disable ReID. Use to tie track re-ID
432
- /// window to an external cascade timeout (e.g. `cascadeTimeout * fps`).
433
- #[napi]
434
- pub fn set_reid_hit_counter_max(&mut self, frames: i32) {
435
- self.inner.set_reid_hit_counter_max(frames);
436
- }
437
-
438
- /// Refresh the ReID counter for all dead tracks back to max. Call every
439
- /// frame while a cascade is active so dead tracks never expire during
440
- /// the cascade window. Stop calling when the cascade ends.
441
- #[napi]
442
- pub fn refresh_reid(&mut self) {
443
- self.inner.refresh_reid();
444
- }
445
-
446
- /// Apply the configured zones + confidence threshold to a list of
447
- /// detections WITHOUT advancing the tracker state. Returns the indices
448
- /// (positions in the input array) of the detections that pass the
449
- /// filter. Used for the external-sensor-write path where a plugin
450
- /// reports detections directly and we want to apply zone filtering
451
- /// without running the full tracker.
452
- #[napi]
453
- pub fn filter_indices(&self, detections: Vec<Detection>) -> Vec<u32> {
454
- let internal: Vec<crate::types::Detection> = detections.into_iter().map(to_internal).collect();
455
- self.inner.filter_indices(&internal)
456
- }
457
-
458
- /// Drop every active track. The next `update()` starts from a clean
459
- /// slate and track ids restart from 1. Use this to align tracker state
460
- /// with external segment boundaries (e.g. cascade activate/deactivate).
461
- #[napi]
462
- pub fn reset(&mut self) {
463
- self.inner.reset();
464
- }
465
-
466
- /// Total number of active tracks across all class buckets.
467
- #[napi(getter)]
468
- pub fn track_count(&self) -> u32 {
469
- self.inner.track_count() as u32
470
- }
471
- }