@camera.ui/rust-postprocessor 0.0.1 → 0.0.3
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 +50 -5
- package/index.js +53 -52
- package/package.json +17 -17
- package/.yarnrc.yml +0 -1
- package/Cargo.toml +0 -29
- package/build.rs +0 -5
- package/rust-toolchain.toml +0 -2
- package/src/iou.rs +0 -83
- package/src/lib.rs +0 -471
- package/src/line_crossing.rs +0 -214
- package/src/merge.rs +0 -312
- package/src/nms.rs +0 -296
- package/src/tracker.rs +0 -1064
- package/src/types.rs +0 -40
- package/src/zone_filter.rs +0 -567
package/src/types.rs
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
//! Shared internal types used by NMS and the tracker wrapper.
|
|
2
|
-
//!
|
|
3
|
-
//! These mirror the JS SDK shapes (`BoundingBox`, `Detection`,
|
|
4
|
-
//! `TrackedDetection`) but live entirely on the Rust side. The napi
|
|
5
|
-
//! bindings in `lib.rs` convert to/from these on the boundary.
|
|
6
|
-
|
|
7
|
-
/// Detection in normalized image coordinates `[0.0, 1.0]`.
|
|
8
|
-
#[derive(Debug, Clone)]
|
|
9
|
-
pub struct Detection {
|
|
10
|
-
pub x: f32,
|
|
11
|
-
pub y: f32,
|
|
12
|
-
pub width: f32,
|
|
13
|
-
pub height: f32,
|
|
14
|
-
pub confidence: f32,
|
|
15
|
-
pub label: String,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/// Tracked detection with stable identity across frames.
|
|
19
|
-
#[derive(Debug, Clone)]
|
|
20
|
-
pub struct TrackedDetection {
|
|
21
|
-
pub x: f32,
|
|
22
|
-
pub y: f32,
|
|
23
|
-
pub width: f32,
|
|
24
|
-
pub height: f32,
|
|
25
|
-
pub confidence: f32,
|
|
26
|
-
pub label: String,
|
|
27
|
-
/// Globally-unique track id assigned by the tracker. Stable across
|
|
28
|
-
/// matched frames; resets only on `ObjectTracker::reset()`.
|
|
29
|
-
pub track_id: u32,
|
|
30
|
-
/// Number of frames this track has existed (1 on first appearance).
|
|
31
|
-
pub track_age: u32,
|
|
32
|
-
/// True when this output frame did not actually match a fresh detection
|
|
33
|
-
/// — the tracker is keeping the box alive via Kalman extrapolation.
|
|
34
|
-
pub track_lost: bool,
|
|
35
|
-
/// Average centroid speed in normalized units/second over a short
|
|
36
|
-
/// sliding window (~1s of past positions). 0 means the track is
|
|
37
|
-
/// effectively stationary. Used by consumers to prefer moving tracks
|
|
38
|
-
/// over static ones (e.g. thumbnail selection).
|
|
39
|
-
pub track_speed: f32,
|
|
40
|
-
}
|
package/src/zone_filter.rs
DELETED
|
@@ -1,567 +0,0 @@
|
|
|
1
|
-
//! Detection zone filter — drops detections based on:
|
|
2
|
-
//! 1. Confidence threshold (`min_confidence`).
|
|
3
|
-
//! 2. Whether the detection's label is allowed by any configured zone
|
|
4
|
-
//! (when at least one zone declares a non-empty `labels` list).
|
|
5
|
-
//! 3. Privacy masks: detections fully contained inside a privacy mask
|
|
6
|
-
//! are removed.
|
|
7
|
-
//! 4. Active include/exclude zones with intersect/contain semantics.
|
|
8
|
-
//!
|
|
9
|
-
//! Zones are stored normalized to `[0.0, 1.0]` (input coordinates from the
|
|
10
|
-
//! UI come in `[0, 100]` and are scaled by [`prepare_zones`]). Polygons
|
|
11
|
-
//! are auto-closed if the caller didn't repeat the first vertex at the
|
|
12
|
-
//! end.
|
|
13
|
-
//!
|
|
14
|
-
//! Polygon math: the in-polygon test uses the standard ray-casting
|
|
15
|
-
//! algorithm with an extra "point exactly on edge" branch so detections
|
|
16
|
-
//! whose corner sits precisely on a zone boundary still count as inside.
|
|
17
|
-
|
|
18
|
-
use std::collections::HashSet;
|
|
19
|
-
|
|
20
|
-
use crate::types::Detection;
|
|
21
|
-
|
|
22
|
-
/// Where in the polygon the box must sit for the zone to apply.
|
|
23
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
24
|
-
pub enum ZoneMatchType {
|
|
25
|
-
/// Any overlap with the zone counts.
|
|
26
|
-
Intersect,
|
|
27
|
-
/// All four corners of the box must be inside the zone.
|
|
28
|
-
Contain,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/// How a zone affects matching detections.
|
|
32
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
33
|
-
pub enum ZoneFilterMode {
|
|
34
|
-
/// Detection passes only if it sits in at least one Include zone.
|
|
35
|
-
Include,
|
|
36
|
-
/// Detection is dropped if it sits in any Exclude zone.
|
|
37
|
-
Exclude,
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// User-supplied zone definition (matches the SDK shape).
|
|
41
|
-
#[derive(Debug, Clone)]
|
|
42
|
-
pub struct ZoneInput {
|
|
43
|
-
/// Optional label filter — empty means "all labels".
|
|
44
|
-
pub labels: Vec<String>,
|
|
45
|
-
pub filter: ZoneFilterMode,
|
|
46
|
-
pub match_type: ZoneMatchType,
|
|
47
|
-
pub is_privacy_mask: bool,
|
|
48
|
-
/// Polygon vertices in `[0, 100]` UI coordinates. Doesn't need to be
|
|
49
|
-
/// closed — `prepare_zones` will close it automatically.
|
|
50
|
-
pub points: Vec<[f64; 2]>,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/// Pre-computed zone ready for fast per-frame filtering.
|
|
54
|
-
///
|
|
55
|
-
/// Note: there is no `is_privacy_mask` field — privacy masks live in
|
|
56
|
-
/// `PreparedZones.privacy_masks`, active zones in
|
|
57
|
-
/// `PreparedZones.active_zones`. The bucket the zone sits in tells the
|
|
58
|
-
/// filter how to treat it.
|
|
59
|
-
#[derive(Debug, Clone)]
|
|
60
|
-
pub struct PreparedZone {
|
|
61
|
-
pub labels: HashSet<String>,
|
|
62
|
-
pub filter: ZoneFilterMode,
|
|
63
|
-
pub match_type: ZoneMatchType,
|
|
64
|
-
/// Closed polygon in normalized `[0.0, 1.0]` coordinates.
|
|
65
|
-
pub points: Vec<[f32; 2]>,
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/// Sorted-into-buckets prepared zones plus the union of all label
|
|
69
|
-
/// restrictions across active zones.
|
|
70
|
-
#[derive(Debug, Clone, Default)]
|
|
71
|
-
pub struct PreparedZones {
|
|
72
|
-
pub privacy_masks: Vec<PreparedZone>,
|
|
73
|
-
pub active_zones: Vec<PreparedZone>,
|
|
74
|
-
/// Union of every active zone's allowed labels (lowercased). Empty
|
|
75
|
-
/// means "no per-zone label restriction is in effect".
|
|
76
|
-
pub all_labels: HashSet<String>,
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/// Convert UI 0-100 zones into normalized prepared zones bucketed by type.
|
|
80
|
-
pub fn prepare_zones(zones: &[ZoneInput]) -> PreparedZones {
|
|
81
|
-
let mut privacy_masks = Vec::new();
|
|
82
|
-
let mut active_zones = Vec::new();
|
|
83
|
-
let mut all_labels: HashSet<String> = HashSet::new();
|
|
84
|
-
|
|
85
|
-
for zone in zones {
|
|
86
|
-
// Normalize 0..100 to 0..1
|
|
87
|
-
let mut points: Vec<[f32; 2]> = zone
|
|
88
|
-
.points
|
|
89
|
-
.iter()
|
|
90
|
-
.map(|p| [(p[0] / 100.0) as f32, (p[1] / 100.0) as f32])
|
|
91
|
-
.collect();
|
|
92
|
-
|
|
93
|
-
// Auto-close: append the first vertex if it's not already at the end
|
|
94
|
-
if points.len() > 1 {
|
|
95
|
-
let first = points[0];
|
|
96
|
-
let last = points[points.len() - 1];
|
|
97
|
-
if (first[0] - last[0]).abs() > 1e-9 || (first[1] - last[1]).abs() > 1e-9 {
|
|
98
|
-
points.push(first);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
let mut labels: HashSet<String> = HashSet::new();
|
|
103
|
-
for label in &zone.labels {
|
|
104
|
-
let lc = label.to_lowercase();
|
|
105
|
-
labels.insert(lc.clone());
|
|
106
|
-
if !zone.is_privacy_mask {
|
|
107
|
-
all_labels.insert(lc);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let prepared = PreparedZone {
|
|
112
|
-
labels,
|
|
113
|
-
filter: zone.filter,
|
|
114
|
-
match_type: zone.match_type,
|
|
115
|
-
points,
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
if zone.is_privacy_mask {
|
|
119
|
-
privacy_masks.push(prepared);
|
|
120
|
-
} else {
|
|
121
|
-
active_zones.push(prepared);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
PreparedZones {
|
|
126
|
-
privacy_masks,
|
|
127
|
-
active_zones,
|
|
128
|
-
all_labels,
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/// Standard ray-cast point-in-polygon test, augmented with a "point lies
|
|
133
|
-
/// exactly on an edge" check that returns true when applicable. The
|
|
134
|
-
/// polygon must be closed (first vertex == last vertex).
|
|
135
|
-
fn is_point_in_polygon(px: f32, py: f32, polygon: &[[f32; 2]]) -> bool {
|
|
136
|
-
if polygon.len() < 3 {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
let mut inside = false;
|
|
140
|
-
let n = polygon.len();
|
|
141
|
-
let mut j = n - 1;
|
|
142
|
-
for i in 0..n {
|
|
143
|
-
let xi = polygon[i][0];
|
|
144
|
-
let yi = polygon[i][1];
|
|
145
|
-
let xj = polygon[j][0];
|
|
146
|
-
let yj = polygon[j][1];
|
|
147
|
-
|
|
148
|
-
// Edge containment: point lies exactly on (or very near) the edge.
|
|
149
|
-
let min_x = xi.min(xj);
|
|
150
|
-
let max_x = xi.max(xj);
|
|
151
|
-
let min_y = yi.min(yj);
|
|
152
|
-
let max_y = yi.max(yj);
|
|
153
|
-
if px >= min_x && px <= max_x && py >= min_y && py <= max_y {
|
|
154
|
-
if (xi - xj).abs() < 1e-9 {
|
|
155
|
-
// Vertical edge
|
|
156
|
-
if (px - xi).abs() < 1e-9 {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
let m = (yj - yi) / (xj - xi);
|
|
161
|
-
if (py - (m * px + (yi - m * xi))).abs() < 1e-9 {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Standard ray-cast: count intersections with horizontal ray to the
|
|
168
|
-
// left of the test point.
|
|
169
|
-
let yi_above = yi > py;
|
|
170
|
-
let yj_above = yj > py;
|
|
171
|
-
if yi_above != yj_above {
|
|
172
|
-
let x_intersect = (xj - xi) * (py - yi) / (yj - yi) + xi;
|
|
173
|
-
if px < x_intersect {
|
|
174
|
-
inside = !inside;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
j = i;
|
|
179
|
-
}
|
|
180
|
-
inside
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/// Parametric segment-segment intersection check (does not return the
|
|
184
|
-
/// crossing point — only whether the segments cross).
|
|
185
|
-
fn do_lines_intersect(a1: [f32; 2], a2: [f32; 2], b1: [f32; 2], b2: [f32; 2]) -> bool {
|
|
186
|
-
let denom = (b2[1] - b1[1]) * (a2[0] - a1[0]) - (b2[0] - b1[0]) * (a2[1] - a1[1]);
|
|
187
|
-
if denom.abs() < 1e-12 {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
let ua = ((b2[0] - b1[0]) * (a1[1] - b1[1]) - (b2[1] - b1[1]) * (a1[0] - b1[0])) / denom;
|
|
191
|
-
let ub = ((a2[0] - a1[0]) * (a1[1] - b1[1]) - (a2[1] - a1[1]) * (a1[0] - b1[0])) / denom;
|
|
192
|
-
(0.0..=1.0).contains(&ua) && (0.0..=1.0).contains(&ub)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
#[inline]
|
|
196
|
-
fn box_corners(det: &Detection) -> [[f32; 2]; 4] {
|
|
197
|
-
let x2 = det.x + det.width;
|
|
198
|
-
let y2 = det.y + det.height;
|
|
199
|
-
[[det.x, det.y], [x2, det.y], [x2, y2], [det.x, y2]]
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/// True if the detection box has any overlap with the polygon, or if any
|
|
203
|
-
/// polygon vertex sits inside the box, or if any edges cross.
|
|
204
|
-
fn box_intersects_polygon(det: &Detection, polygon: &[[f32; 2]]) -> bool {
|
|
205
|
-
let corners = box_corners(det);
|
|
206
|
-
let x2 = det.x + det.width;
|
|
207
|
-
let y2 = det.y + det.height;
|
|
208
|
-
|
|
209
|
-
// 1) Any corner of the box inside the polygon → overlap.
|
|
210
|
-
for &[cx, cy] in &corners {
|
|
211
|
-
if is_point_in_polygon(cx, cy, polygon) {
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
// 2) Any vertex of the polygon inside the box → overlap.
|
|
216
|
-
for &[px, py] in polygon {
|
|
217
|
-
if px >= det.x && px <= x2 && py >= det.y && py <= y2 {
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// 3) Any edge of the box crosses any edge of the polygon → overlap.
|
|
222
|
-
let edges = [
|
|
223
|
-
(corners[0], corners[1]),
|
|
224
|
-
(corners[1], corners[2]),
|
|
225
|
-
(corners[2], corners[3]),
|
|
226
|
-
(corners[3], corners[0]),
|
|
227
|
-
];
|
|
228
|
-
if polygon.len() < 2 {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
for i in 0..(polygon.len() - 1) {
|
|
232
|
-
for &(ea, eb) in &edges {
|
|
233
|
-
if do_lines_intersect(ea, eb, polygon[i], polygon[i + 1]) {
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
false
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/// True if all four corners of the box are inside the polygon.
|
|
242
|
-
fn box_contained_in_polygon(det: &Detection, polygon: &[[f32; 2]]) -> bool {
|
|
243
|
-
for &[cx, cy] in &box_corners(det) {
|
|
244
|
-
if !is_point_in_polygon(cx, cy, polygon) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
true
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
#[inline]
|
|
252
|
-
fn zone_accepts_label(zone: &PreparedZone, lc_label: &str) -> bool {
|
|
253
|
-
zone.labels.is_empty() || zone.labels.contains(lc_label)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/// Same predicate as [`filter_detections`] but operates on a borrowed
|
|
257
|
-
/// slice and returns the indices of detections that pass the filter
|
|
258
|
-
/// instead of new owned values. Used by callers that need to project the
|
|
259
|
-
/// filter result back onto a parallel array of richer types (face/lpd
|
|
260
|
-
/// items wrapped as Detection with a substitute label, etc.).
|
|
261
|
-
pub fn filter_indices(
|
|
262
|
-
detections: &[Detection],
|
|
263
|
-
zones: &PreparedZones,
|
|
264
|
-
min_confidence: f32,
|
|
265
|
-
) -> Vec<u32> {
|
|
266
|
-
let PreparedZones {
|
|
267
|
-
privacy_masks,
|
|
268
|
-
active_zones,
|
|
269
|
-
all_labels,
|
|
270
|
-
} = zones;
|
|
271
|
-
|
|
272
|
-
let mut out: Vec<u32> = Vec::with_capacity(detections.len());
|
|
273
|
-
for (i, det) in detections.iter().enumerate() {
|
|
274
|
-
if det.confidence < min_confidence {
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Fast path: no zones at all → confidence threshold only.
|
|
279
|
-
if active_zones.is_empty() && privacy_masks.is_empty() {
|
|
280
|
-
out.push(i as u32);
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
let label_lc = det.label.to_lowercase();
|
|
285
|
-
if !all_labels.is_empty() && !all_labels.contains(&label_lc) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
let mut dropped = false;
|
|
290
|
-
for mask in privacy_masks {
|
|
291
|
-
if box_contained_in_polygon(det, &mask.points) {
|
|
292
|
-
dropped = true;
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if dropped {
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
let mut has_include_zone = false;
|
|
301
|
-
let mut satisfies_include = false;
|
|
302
|
-
|
|
303
|
-
for zone in active_zones {
|
|
304
|
-
if !zone_accepts_label(zone, &label_lc) {
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
let intersects = box_intersects_polygon(det, &zone.points);
|
|
308
|
-
let contained = intersects && box_contained_in_polygon(det, &zone.points);
|
|
309
|
-
|
|
310
|
-
match zone.filter {
|
|
311
|
-
ZoneFilterMode::Exclude => match zone.match_type {
|
|
312
|
-
ZoneMatchType::Contain => {
|
|
313
|
-
if intersects {
|
|
314
|
-
dropped = true;
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
ZoneMatchType::Intersect => {
|
|
319
|
-
if intersects || contained {
|
|
320
|
-
dropped = true;
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
|
-
ZoneFilterMode::Include => {
|
|
326
|
-
has_include_zone = true;
|
|
327
|
-
if !satisfies_include {
|
|
328
|
-
match zone.match_type {
|
|
329
|
-
ZoneMatchType::Contain => {
|
|
330
|
-
if contained {
|
|
331
|
-
satisfies_include = true;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
ZoneMatchType::Intersect => {
|
|
335
|
-
if intersects {
|
|
336
|
-
satisfies_include = true;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if dropped {
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
if has_include_zone && !satisfies_include {
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
out.push(i as u32);
|
|
352
|
-
}
|
|
353
|
-
out
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/// Apply confidence threshold + zone filter to a list of detections.
|
|
357
|
-
///
|
|
358
|
-
/// Delegates to [`filter_indices`] and extracts the kept detections by
|
|
359
|
-
/// index — single implementation, no duplicated logic.
|
|
360
|
-
#[cfg(test)]
|
|
361
|
-
fn filter_detections(
|
|
362
|
-
detections: Vec<Detection>,
|
|
363
|
-
zones: &PreparedZones,
|
|
364
|
-
min_confidence: f32,
|
|
365
|
-
) -> Vec<Detection> {
|
|
366
|
-
let indices = filter_indices(&detections, zones, min_confidence);
|
|
367
|
-
if indices.len() == detections.len() {
|
|
368
|
-
return detections; // all kept — avoid extra work
|
|
369
|
-
}
|
|
370
|
-
let mut slots: Vec<Option<Detection>> = detections.into_iter().map(Some).collect();
|
|
371
|
-
indices
|
|
372
|
-
.iter()
|
|
373
|
-
.filter_map(|&i| slots[i as usize].take())
|
|
374
|
-
.collect()
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
#[cfg(test)]
|
|
378
|
-
mod tests {
|
|
379
|
-
use super::*;
|
|
380
|
-
|
|
381
|
-
fn det(x: f32, y: f32, w: f32, h: f32, label: &str) -> Detection {
|
|
382
|
-
Detection {
|
|
383
|
-
x,
|
|
384
|
-
y,
|
|
385
|
-
width: w,
|
|
386
|
-
height: h,
|
|
387
|
-
confidence: 0.9,
|
|
388
|
-
label: label.to_string(),
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
fn rect_zone(
|
|
393
|
-
x1: f64,
|
|
394
|
-
y1: f64,
|
|
395
|
-
x2: f64,
|
|
396
|
-
y2: f64,
|
|
397
|
-
mode: ZoneFilterMode,
|
|
398
|
-
mt: ZoneMatchType,
|
|
399
|
-
labels: Vec<String>,
|
|
400
|
-
) -> ZoneInput {
|
|
401
|
-
ZoneInput {
|
|
402
|
-
labels,
|
|
403
|
-
filter: mode,
|
|
404
|
-
match_type: mt,
|
|
405
|
-
is_privacy_mask: false,
|
|
406
|
-
points: vec![[x1, y1], [x2, y1], [x2, y2], [x1, y2]],
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
#[test]
|
|
411
|
-
fn confidence_filter_only() {
|
|
412
|
-
let zones = prepare_zones(&[]);
|
|
413
|
-
let mut a = det(0.1, 0.1, 0.2, 0.2, "person");
|
|
414
|
-
a.confidence = 0.4;
|
|
415
|
-
let mut b = det(0.3, 0.3, 0.2, 0.2, "person");
|
|
416
|
-
b.confidence = 0.8;
|
|
417
|
-
let out = filter_detections(vec![a, b], &zones, 0.5);
|
|
418
|
-
assert_eq!(out.len(), 1);
|
|
419
|
-
assert!(out[0].confidence >= 0.5);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
#[test]
|
|
423
|
-
fn include_zone_contains_box() {
|
|
424
|
-
// Zone covers the middle 50% of the image
|
|
425
|
-
let zones = prepare_zones(&[rect_zone(
|
|
426
|
-
25.0,
|
|
427
|
-
25.0,
|
|
428
|
-
75.0,
|
|
429
|
-
75.0,
|
|
430
|
-
ZoneFilterMode::Include,
|
|
431
|
-
ZoneMatchType::Contain,
|
|
432
|
-
vec![],
|
|
433
|
-
)]);
|
|
434
|
-
// Box fully inside
|
|
435
|
-
let inside = det(0.30, 0.30, 0.20, 0.20, "person");
|
|
436
|
-
// Box outside the zone
|
|
437
|
-
let outside = det(0.85, 0.85, 0.10, 0.10, "person");
|
|
438
|
-
let out = filter_detections(vec![inside, outside], &zones, 0.0);
|
|
439
|
-
assert_eq!(out.len(), 1);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
#[test]
|
|
443
|
-
fn include_intersect_keeps_partial_overlap() {
|
|
444
|
-
let zones = prepare_zones(&[rect_zone(
|
|
445
|
-
25.0,
|
|
446
|
-
25.0,
|
|
447
|
-
75.0,
|
|
448
|
-
75.0,
|
|
449
|
-
ZoneFilterMode::Include,
|
|
450
|
-
ZoneMatchType::Intersect,
|
|
451
|
-
vec![],
|
|
452
|
-
)]);
|
|
453
|
-
// Box overlaps the zone but isn't fully contained
|
|
454
|
-
let partial = det(0.70, 0.30, 0.20, 0.20, "person");
|
|
455
|
-
let out = filter_detections(vec![partial], &zones, 0.0);
|
|
456
|
-
assert_eq!(out.len(), 1);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
#[test]
|
|
460
|
-
fn exclude_zone_drops_box() {
|
|
461
|
-
let zones = prepare_zones(&[rect_zone(
|
|
462
|
-
25.0,
|
|
463
|
-
25.0,
|
|
464
|
-
75.0,
|
|
465
|
-
75.0,
|
|
466
|
-
ZoneFilterMode::Exclude,
|
|
467
|
-
ZoneMatchType::Intersect,
|
|
468
|
-
vec![],
|
|
469
|
-
)]);
|
|
470
|
-
let inside = det(0.30, 0.30, 0.20, 0.20, "person");
|
|
471
|
-
let outside = det(0.05, 0.05, 0.10, 0.10, "person");
|
|
472
|
-
let out = filter_detections(vec![inside, outside], &zones, 0.0);
|
|
473
|
-
assert_eq!(out.len(), 1);
|
|
474
|
-
assert!((out[0].x - 0.05).abs() < 1e-6);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
#[test]
|
|
478
|
-
fn privacy_mask_drops_contained_box() {
|
|
479
|
-
let mut mask = rect_zone(
|
|
480
|
-
0.0,
|
|
481
|
-
0.0,
|
|
482
|
-
50.0,
|
|
483
|
-
50.0,
|
|
484
|
-
ZoneFilterMode::Include,
|
|
485
|
-
ZoneMatchType::Intersect,
|
|
486
|
-
vec![],
|
|
487
|
-
);
|
|
488
|
-
mask.is_privacy_mask = true;
|
|
489
|
-
let zones = prepare_zones(&[mask]);
|
|
490
|
-
let inside = det(0.10, 0.10, 0.20, 0.20, "person");
|
|
491
|
-
let outside = det(0.60, 0.60, 0.20, 0.20, "person");
|
|
492
|
-
let out = filter_detections(vec![inside, outside], &zones, 0.0);
|
|
493
|
-
assert_eq!(out.len(), 1);
|
|
494
|
-
assert!((out[0].x - 0.60).abs() < 1e-6);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
#[test]
|
|
498
|
-
fn label_filter_restricts_globally() {
|
|
499
|
-
// Zone is configured for cars only — persons should be dropped even
|
|
500
|
-
// outside the zone (the union of all zone labels becomes a global
|
|
501
|
-
// allow-list).
|
|
502
|
-
let zones = prepare_zones(&[rect_zone(
|
|
503
|
-
0.0,
|
|
504
|
-
0.0,
|
|
505
|
-
100.0,
|
|
506
|
-
100.0,
|
|
507
|
-
ZoneFilterMode::Include,
|
|
508
|
-
ZoneMatchType::Intersect,
|
|
509
|
-
vec!["car".to_string()],
|
|
510
|
-
)]);
|
|
511
|
-
let person = det(0.30, 0.30, 0.20, 0.20, "person");
|
|
512
|
-
let car = det(0.30, 0.30, 0.20, 0.20, "car");
|
|
513
|
-
let out = filter_detections(vec![person, car], &zones, 0.0);
|
|
514
|
-
assert_eq!(out.len(), 1);
|
|
515
|
-
assert_eq!(out[0].label, "car");
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
#[test]
|
|
519
|
-
fn label_zone_with_other_label_zone_combined() {
|
|
520
|
-
// Two zones: one for "person", one for "car". Detections of either
|
|
521
|
-
// class are allowed; cats get dropped.
|
|
522
|
-
let zones = prepare_zones(&[
|
|
523
|
-
rect_zone(
|
|
524
|
-
0.0,
|
|
525
|
-
0.0,
|
|
526
|
-
100.0,
|
|
527
|
-
100.0,
|
|
528
|
-
ZoneFilterMode::Include,
|
|
529
|
-
ZoneMatchType::Intersect,
|
|
530
|
-
vec!["person".to_string()],
|
|
531
|
-
),
|
|
532
|
-
rect_zone(
|
|
533
|
-
0.0,
|
|
534
|
-
0.0,
|
|
535
|
-
100.0,
|
|
536
|
-
100.0,
|
|
537
|
-
ZoneFilterMode::Include,
|
|
538
|
-
ZoneMatchType::Intersect,
|
|
539
|
-
vec!["car".to_string()],
|
|
540
|
-
),
|
|
541
|
-
]);
|
|
542
|
-
let person = det(0.30, 0.30, 0.20, 0.20, "person");
|
|
543
|
-
let car = det(0.30, 0.30, 0.20, 0.20, "car");
|
|
544
|
-
let cat = det(0.30, 0.30, 0.20, 0.20, "cat");
|
|
545
|
-
let out = filter_detections(vec![person, car, cat], &zones, 0.0);
|
|
546
|
-
assert_eq!(out.len(), 2);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
#[test]
|
|
550
|
-
fn auto_close_polygon() {
|
|
551
|
-
// Pass a polygon that doesn't end with the start vertex — prepare
|
|
552
|
-
// should close it for us.
|
|
553
|
-
let zone = ZoneInput {
|
|
554
|
-
labels: vec![],
|
|
555
|
-
filter: ZoneFilterMode::Include,
|
|
556
|
-
match_type: ZoneMatchType::Intersect,
|
|
557
|
-
is_privacy_mask: false,
|
|
558
|
-
points: vec![[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [0.0, 100.0]],
|
|
559
|
-
};
|
|
560
|
-
let zones = prepare_zones(&[zone]);
|
|
561
|
-
assert_eq!(zones.active_zones[0].points.len(), 5);
|
|
562
|
-
let first = zones.active_zones[0].points[0];
|
|
563
|
-
let last = zones.active_zones[0].points[4];
|
|
564
|
-
assert!((first[0] - last[0]).abs() < 1e-6);
|
|
565
|
-
assert!((first[1] - last[1]).abs() < 1e-6);
|
|
566
|
-
}
|
|
567
|
-
}
|