@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/merge.rs
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
//! Per-class union-find clustering of nearby/overlapping detections.
|
|
2
|
-
//!
|
|
3
|
-
//! This is a separate dedup pass from NMS: where NMS keeps the highest-
|
|
4
|
-
//! confidence box and discards its rivals, this function **merges** every
|
|
5
|
-
//! cluster of close-or-overlapping same-label boxes into a single union
|
|
6
|
-
//! bounding box covering all members. Useful when an object detector
|
|
7
|
-
//! consistently splits a single physical object into several adjacent
|
|
8
|
-
//! detections (e.g. a person torso + legs becoming two boxes).
|
|
9
|
-
//!
|
|
10
|
-
//! Two boxes are clustered together if either condition holds:
|
|
11
|
-
//! 1. Their top-left corners are within `close_threshold` along both
|
|
12
|
-
//! axes (cheap proximity check that survives zero IoU), OR
|
|
13
|
-
//! 2. Their IoU exceeds `iou_threshold`.
|
|
14
|
-
//!
|
|
15
|
-
//! Output: one Detection per connected component, with `box` set to the
|
|
16
|
-
//! axis-aligned union of all member boxes (clamped to `[0, 1]`) and
|
|
17
|
-
//! `confidence` set to the maximum confidence in the cluster. The cluster
|
|
18
|
-
//! inherits the label and remaining metadata from the first member of the
|
|
19
|
-
//! group (all members share the same label since we group by label first).
|
|
20
|
-
|
|
21
|
-
use std::collections::HashMap;
|
|
22
|
-
|
|
23
|
-
use crate::types::Detection;
|
|
24
|
-
|
|
25
|
-
/// Merge nearby/overlapping same-label detections via union-find clustering.
|
|
26
|
-
pub fn merge_detections(
|
|
27
|
-
detections: Vec<Detection>,
|
|
28
|
-
iou_threshold: f32,
|
|
29
|
-
close_threshold: f32,
|
|
30
|
-
) -> Vec<Detection> {
|
|
31
|
-
if detections.is_empty() {
|
|
32
|
-
return Vec::new();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Fast path: when all detections share the same label (common after NMS
|
|
36
|
-
// reduces to 1-2 objects of the same class), skip HashMap grouping.
|
|
37
|
-
let all_same_label = detections.windows(2).all(|w| w[0].label == w[1].label);
|
|
38
|
-
|
|
39
|
-
let mut result: Vec<Detection> = Vec::with_capacity(detections.len());
|
|
40
|
-
|
|
41
|
-
if all_same_label {
|
|
42
|
-
let indices: Vec<usize> = (0..detections.len()).collect();
|
|
43
|
-
merge_cluster(
|
|
44
|
-
&detections,
|
|
45
|
-
&indices,
|
|
46
|
-
iou_threshold,
|
|
47
|
-
close_threshold,
|
|
48
|
-
&mut result,
|
|
49
|
-
);
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Group by label so clustering only happens within a class.
|
|
54
|
-
let mut by_label: HashMap<String, Vec<usize>> = HashMap::new();
|
|
55
|
-
for (i, det) in detections.iter().enumerate() {
|
|
56
|
-
by_label.entry(det.label.clone()).or_default().push(i);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for (_label, indices) in by_label {
|
|
60
|
-
merge_cluster(
|
|
61
|
-
&detections,
|
|
62
|
-
&indices,
|
|
63
|
-
iou_threshold,
|
|
64
|
-
close_threshold,
|
|
65
|
-
&mut result,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
result
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/// Cluster a single-label group of detections and append results to `out`.
|
|
73
|
-
fn merge_cluster(
|
|
74
|
-
detections: &[Detection],
|
|
75
|
-
indices: &[usize],
|
|
76
|
-
iou_threshold: f32,
|
|
77
|
-
close_threshold: f32,
|
|
78
|
-
out: &mut Vec<Detection>,
|
|
79
|
-
) {
|
|
80
|
-
let n = indices.len();
|
|
81
|
-
|
|
82
|
-
if n == 1 {
|
|
83
|
-
out.push(detections[indices[0]].clone());
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Pack box corners + areas into a SoA layout for cache-friendly
|
|
88
|
-
// pairwise comparison: [x1, y1, x2, y2, area] per detection.
|
|
89
|
-
let mut boxes = vec![0.0f32; n * 5];
|
|
90
|
-
for (i, &orig_idx) in indices.iter().enumerate() {
|
|
91
|
-
let det = &detections[orig_idx];
|
|
92
|
-
let off = i * 5;
|
|
93
|
-
boxes[off] = det.x;
|
|
94
|
-
boxes[off + 1] = det.y;
|
|
95
|
-
boxes[off + 2] = det.x + det.width;
|
|
96
|
-
boxes[off + 3] = det.y + det.height;
|
|
97
|
-
boxes[off + 4] = det.width * det.height;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Union-find with path-compression. `parent[i]` points to the
|
|
101
|
-
// representative of i's cluster.
|
|
102
|
-
let mut parent: Vec<usize> = (0..n).collect();
|
|
103
|
-
fn find(parent: &mut [usize], mut x: usize) -> usize {
|
|
104
|
-
while parent[x] != x {
|
|
105
|
-
parent[x] = parent[parent[x]];
|
|
106
|
-
x = parent[x];
|
|
107
|
-
}
|
|
108
|
-
x
|
|
109
|
-
}
|
|
110
|
-
fn union(parent: &mut [usize], a: usize, b: usize) {
|
|
111
|
-
let ra = find(parent, a);
|
|
112
|
-
let rb = find(parent, b);
|
|
113
|
-
parent[ra] = rb;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
for i in 0..n {
|
|
117
|
-
let off_i = i * 5;
|
|
118
|
-
let ix1 = boxes[off_i];
|
|
119
|
-
let iy1 = boxes[off_i + 1];
|
|
120
|
-
let ix2 = boxes[off_i + 2];
|
|
121
|
-
let iy2 = boxes[off_i + 3];
|
|
122
|
-
let i_area = boxes[off_i + 4];
|
|
123
|
-
|
|
124
|
-
for j in (i + 1)..n {
|
|
125
|
-
let off_j = j * 5;
|
|
126
|
-
|
|
127
|
-
let close = (ix1 - boxes[off_j]).abs() <= close_threshold
|
|
128
|
-
&& (iy1 - boxes[off_j + 1]).abs() <= close_threshold;
|
|
129
|
-
if close {
|
|
130
|
-
union(&mut parent, i, j);
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let inter_x1 = ix1.max(boxes[off_j]);
|
|
135
|
-
let inter_y1 = iy1.max(boxes[off_j + 1]);
|
|
136
|
-
let inter_x2 = ix2.min(boxes[off_j + 2]);
|
|
137
|
-
let inter_y2 = iy2.min(boxes[off_j + 3]);
|
|
138
|
-
if inter_x2 <= inter_x1 || inter_y2 <= inter_y1 {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
let inter_area = (inter_x2 - inter_x1) * (inter_y2 - inter_y1);
|
|
142
|
-
let union_area = i_area + boxes[off_j + 4] - inter_area;
|
|
143
|
-
if union_area <= 0.0 {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
let iou = inter_area / union_area;
|
|
147
|
-
if iou > iou_threshold {
|
|
148
|
-
union(&mut parent, i, j);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Collect cluster members keyed by their representative.
|
|
154
|
-
let mut clusters: HashMap<usize, Vec<usize>> = HashMap::new();
|
|
155
|
-
for i in 0..n {
|
|
156
|
-
let root = find(&mut parent, i);
|
|
157
|
-
clusters.entry(root).or_default().push(i);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
for (_root, members) in clusters {
|
|
161
|
-
if members.len() == 1 {
|
|
162
|
-
out.push(detections[indices[members[0]]].clone());
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let mut min_x = 1.0f32;
|
|
167
|
-
let mut min_y = 1.0f32;
|
|
168
|
-
let mut max_x = 0.0f32;
|
|
169
|
-
let mut max_y = 0.0f32;
|
|
170
|
-
let mut max_conf = 0.0f32;
|
|
171
|
-
|
|
172
|
-
for &m in &members {
|
|
173
|
-
let off = m * 5;
|
|
174
|
-
if boxes[off] < min_x {
|
|
175
|
-
min_x = boxes[off];
|
|
176
|
-
}
|
|
177
|
-
if boxes[off + 1] < min_y {
|
|
178
|
-
min_y = boxes[off + 1];
|
|
179
|
-
}
|
|
180
|
-
if boxes[off + 2] > max_x {
|
|
181
|
-
max_x = boxes[off + 2];
|
|
182
|
-
}
|
|
183
|
-
if boxes[off + 3] > max_y {
|
|
184
|
-
max_y = boxes[off + 3];
|
|
185
|
-
}
|
|
186
|
-
let conf = detections[indices[m]].confidence;
|
|
187
|
-
if conf > max_conf {
|
|
188
|
-
max_conf = conf;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
min_x = min_x.max(0.0);
|
|
193
|
-
min_y = min_y.max(0.0);
|
|
194
|
-
max_x = max_x.min(1.0);
|
|
195
|
-
max_y = max_y.min(1.0);
|
|
196
|
-
|
|
197
|
-
let w = max_x - min_x;
|
|
198
|
-
let h = max_y - min_y;
|
|
199
|
-
if w <= 0.0 || h <= 0.0 {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
let template = &detections[indices[members[0]]];
|
|
204
|
-
out.push(Detection {
|
|
205
|
-
x: min_x,
|
|
206
|
-
y: min_y,
|
|
207
|
-
width: w,
|
|
208
|
-
height: h,
|
|
209
|
-
confidence: max_conf,
|
|
210
|
-
label: template.label.clone(),
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
#[cfg(test)]
|
|
216
|
-
mod tests {
|
|
217
|
-
use super::*;
|
|
218
|
-
|
|
219
|
-
fn det(x: f32, y: f32, w: f32, h: f32, conf: f32, label: &str) -> Detection {
|
|
220
|
-
Detection {
|
|
221
|
-
x,
|
|
222
|
-
y,
|
|
223
|
-
width: w,
|
|
224
|
-
height: h,
|
|
225
|
-
confidence: conf,
|
|
226
|
-
label: label.to_string(),
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
#[test]
|
|
231
|
-
fn empty_input() {
|
|
232
|
-
assert!(merge_detections(Vec::new(), 0.5, 0.1).is_empty());
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
#[test]
|
|
236
|
-
fn single_detection_unchanged() {
|
|
237
|
-
let input = vec![det(0.1, 0.1, 0.2, 0.2, 0.9, "person")];
|
|
238
|
-
let out = merge_detections(input, 0.5, 0.1);
|
|
239
|
-
assert_eq!(out.len(), 1);
|
|
240
|
-
assert!((out[0].x - 0.1).abs() < 1e-6);
|
|
241
|
-
assert!((out[0].confidence - 0.9).abs() < 1e-6);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
#[test]
|
|
245
|
-
fn overlapping_boxes_merged_to_union() {
|
|
246
|
-
// Two boxes with significant overlap should collapse into one
|
|
247
|
-
// covering the full extent.
|
|
248
|
-
let input = vec![
|
|
249
|
-
det(0.1, 0.1, 0.2, 0.2, 0.7, "person"),
|
|
250
|
-
det(0.15, 0.15, 0.2, 0.2, 0.9, "person"),
|
|
251
|
-
];
|
|
252
|
-
let out = merge_detections(input, 0.01, 0.001);
|
|
253
|
-
assert_eq!(out.len(), 1);
|
|
254
|
-
let m = &out[0];
|
|
255
|
-
// Union covers [0.1..0.35] x [0.1..0.35]
|
|
256
|
-
assert!((m.x - 0.1).abs() < 1e-6);
|
|
257
|
-
assert!((m.y - 0.1).abs() < 1e-6);
|
|
258
|
-
assert!((m.width - 0.25).abs() < 1e-6);
|
|
259
|
-
assert!((m.height - 0.25).abs() < 1e-6);
|
|
260
|
-
// Max confidence in the cluster
|
|
261
|
-
assert!((m.confidence - 0.9).abs() < 1e-6);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
#[test]
|
|
265
|
-
fn close_corners_merged_even_without_iou() {
|
|
266
|
-
// Two non-overlapping boxes whose top-left corners are within
|
|
267
|
-
// close_threshold should still cluster (the cheap proximity rule).
|
|
268
|
-
let input = vec![
|
|
269
|
-
det(0.10, 0.10, 0.05, 0.05, 0.8, "person"),
|
|
270
|
-
det(0.12, 0.12, 0.05, 0.05, 0.7, "person"),
|
|
271
|
-
];
|
|
272
|
-
let out = merge_detections(input, 0.5, 0.05);
|
|
273
|
-
assert_eq!(out.len(), 1);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
#[test]
|
|
277
|
-
fn different_labels_not_merged() {
|
|
278
|
-
let input = vec![
|
|
279
|
-
det(0.1, 0.1, 0.2, 0.2, 0.7, "person"),
|
|
280
|
-
det(0.1, 0.1, 0.2, 0.2, 0.9, "car"),
|
|
281
|
-
];
|
|
282
|
-
let out = merge_detections(input, 0.01, 0.001);
|
|
283
|
-
assert_eq!(out.len(), 2);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
#[test]
|
|
287
|
-
fn distant_boxes_not_merged() {
|
|
288
|
-
let input = vec![
|
|
289
|
-
det(0.1, 0.1, 0.05, 0.05, 0.7, "person"),
|
|
290
|
-
det(0.8, 0.8, 0.05, 0.05, 0.9, "person"),
|
|
291
|
-
];
|
|
292
|
-
let out = merge_detections(input, 0.5, 0.01);
|
|
293
|
-
assert_eq!(out.len(), 2);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
#[test]
|
|
297
|
-
fn three_box_chain() {
|
|
298
|
-
// a–b overlap, b–c overlap, a–c don't — union-find should still
|
|
299
|
-
// collapse all three into one cluster via the chain.
|
|
300
|
-
let input = vec![
|
|
301
|
-
det(0.10, 0.10, 0.10, 0.10, 0.5, "person"),
|
|
302
|
-
det(0.15, 0.15, 0.10, 0.10, 0.6, "person"),
|
|
303
|
-
det(0.20, 0.20, 0.10, 0.10, 0.7, "person"),
|
|
304
|
-
];
|
|
305
|
-
let out = merge_detections(input, 0.01, 0.001);
|
|
306
|
-
assert_eq!(out.len(), 1);
|
|
307
|
-
// Union covers [0.10..0.30] x [0.10..0.30]
|
|
308
|
-
let m = &out[0];
|
|
309
|
-
assert!((m.x - 0.1).abs() < 1e-6);
|
|
310
|
-
assert!((m.width - 0.2).abs() < 1e-6);
|
|
311
|
-
}
|
|
312
|
-
}
|
package/src/nms.rs
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
//! Non-Maximum Suppression with SIMD acceleration.
|
|
2
|
-
//!
|
|
3
|
-
//! Greedy NMS over a list of detections, suppressing within the same class
|
|
4
|
-
//! label only. Uses a Structure-of-Arrays layout so the inner IoU loop can
|
|
5
|
-
//! process eight candidate boxes at once via `wide::f32x8`.
|
|
6
|
-
//!
|
|
7
|
-
//! Algorithm steps:
|
|
8
|
-
//! 1. Optional top-k pre-filter — bounds the work for pathological inputs
|
|
9
|
-
//! with thousands of overlapping detections.
|
|
10
|
-
//! 2. Sort surviving candidates by descending confidence.
|
|
11
|
-
//! 3. Greedy keep-then-suppress: for each kept box compare in `f32x8`
|
|
12
|
-
//! chunks against all later boxes of the same class, suppressing those
|
|
13
|
-
//! whose IoU exceeds the threshold.
|
|
14
|
-
//!
|
|
15
|
-
//! Output preserves the original `Detection` order by confidence (descending).
|
|
16
|
-
|
|
17
|
-
use wide::{f32x8, CmpGt};
|
|
18
|
-
// CmpGt is the trait that provides `simd_gt` on f32x8.
|
|
19
|
-
|
|
20
|
-
use crate::types::Detection;
|
|
21
|
-
|
|
22
|
-
/// Run greedy NMS on `detections`.
|
|
23
|
-
///
|
|
24
|
-
/// * `iou_threshold` — boxes with IoU strictly greater than this value
|
|
25
|
-
/// compared to a higher-scoring box of the same class are removed.
|
|
26
|
-
/// * `max_detections` — hard cap on the output length. `None` for no cap.
|
|
27
|
-
///
|
|
28
|
-
/// Detections without overlapping rivals pass through unchanged. Per-class
|
|
29
|
-
/// semantics ensure a `person` and a `car` with overlapping bboxes are both
|
|
30
|
-
/// retained.
|
|
31
|
-
pub fn nms(
|
|
32
|
-
detections: Vec<Detection>,
|
|
33
|
-
iou_threshold: f32,
|
|
34
|
-
max_detections: Option<usize>,
|
|
35
|
-
) -> Vec<Detection> {
|
|
36
|
-
if detections.is_empty() {
|
|
37
|
-
return Vec::new();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Pre-filter: keep at most max_detections * 10 by confidence to bound the
|
|
41
|
-
// O(n²) inner loop on adversarial inputs. The factor 10 leaves headroom for
|
|
42
|
-
// suppression to still hit the requested final count.
|
|
43
|
-
let mut candidates = detections;
|
|
44
|
-
let max_det = max_detections.unwrap_or(candidates.len());
|
|
45
|
-
let prefilter_cap = max_det.saturating_mul(10).max(64);
|
|
46
|
-
if candidates.len() > prefilter_cap {
|
|
47
|
-
candidates.select_nth_unstable_by(prefilter_cap, |a, b| {
|
|
48
|
-
b.confidence
|
|
49
|
-
.partial_cmp(&a.confidence)
|
|
50
|
-
.unwrap_or(std::cmp::Ordering::Equal)
|
|
51
|
-
});
|
|
52
|
-
candidates.truncate(prefilter_cap);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Sort by confidence descending so the highest-score box is always picked
|
|
56
|
-
// first in the greedy pass.
|
|
57
|
-
candidates.sort_unstable_by(|a, b| {
|
|
58
|
-
b.confidence
|
|
59
|
-
.partial_cmp(&a.confidence)
|
|
60
|
-
.unwrap_or(std::cmp::Ordering::Equal)
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
let n = candidates.len();
|
|
64
|
-
|
|
65
|
-
// Structure-of-Arrays for SIMD-friendly access. We store xyxy (corners)
|
|
66
|
-
// because the IoU intersection math uses min/max over corners directly.
|
|
67
|
-
let mut x1 = vec![0.0f32; n];
|
|
68
|
-
let mut y1 = vec![0.0f32; n];
|
|
69
|
-
let mut x2 = vec![0.0f32; n];
|
|
70
|
-
let mut y2 = vec![0.0f32; n];
|
|
71
|
-
let mut areas = vec![0.0f32; n];
|
|
72
|
-
|
|
73
|
-
for (i, det) in candidates.iter().enumerate() {
|
|
74
|
-
x1[i] = det.x;
|
|
75
|
-
y1[i] = det.y;
|
|
76
|
-
x2[i] = det.x + det.width;
|
|
77
|
-
y2[i] = det.y + det.height;
|
|
78
|
-
areas[i] = det.width * det.height;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Per-class label dispatch: convert label strings to small integer ids so
|
|
82
|
-
// the inner loop can compare classes via cheap integer equality.
|
|
83
|
-
let mut label_ids = vec![0u32; n];
|
|
84
|
-
let mut label_table: Vec<&str> = Vec::new();
|
|
85
|
-
for (i, det) in candidates.iter().enumerate() {
|
|
86
|
-
let id = match label_table.iter().position(|l| *l == det.label.as_str()) {
|
|
87
|
-
Some(idx) => idx as u32,
|
|
88
|
-
None => {
|
|
89
|
-
label_table.push(det.label.as_str());
|
|
90
|
-
(label_table.len() - 1) as u32
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
label_ids[i] = id;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
let mut suppressed = vec![false; n];
|
|
97
|
-
let mut keep_indices: Vec<usize> = Vec::with_capacity(max_det.min(n));
|
|
98
|
-
|
|
99
|
-
let iou_v = f32x8::splat(iou_threshold);
|
|
100
|
-
let zero_v = f32x8::splat(0.0);
|
|
101
|
-
|
|
102
|
-
for i in 0..n {
|
|
103
|
-
if suppressed[i] {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
keep_indices.push(i);
|
|
107
|
-
if keep_indices.len() >= max_det {
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let ax1 = f32x8::splat(x1[i]);
|
|
112
|
-
let ay1 = f32x8::splat(y1[i]);
|
|
113
|
-
let ax2 = f32x8::splat(x2[i]);
|
|
114
|
-
let ay2 = f32x8::splat(y2[i]);
|
|
115
|
-
let aa = f32x8::splat(areas[i]);
|
|
116
|
-
let a_label = label_ids[i];
|
|
117
|
-
|
|
118
|
-
let mut j = i + 1;
|
|
119
|
-
// SIMD chunks of 8 — load 8 boxes at a time, compute IoU vector, build
|
|
120
|
-
// a suppression mask, then apply it back per-element with class check.
|
|
121
|
-
while j + 8 <= n {
|
|
122
|
-
// Skip the chunk entirely if no candidate inside it can be suppressed
|
|
123
|
-
// (all already suppressed OR all of a different class).
|
|
124
|
-
let mut chunk_active = false;
|
|
125
|
-
for k in 0..8 {
|
|
126
|
-
if !suppressed[j + k] && label_ids[j + k] == a_label {
|
|
127
|
-
chunk_active = true;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if chunk_active {
|
|
133
|
-
// Safe unaligned reads — Vec<f32> is contiguous and we never index
|
|
134
|
-
// past the end (the `while` condition guarantees j + 8 <= n).
|
|
135
|
-
let bx1: f32x8 = unsafe { (x1.as_ptr().add(j) as *const f32x8).read_unaligned() };
|
|
136
|
-
let by1: f32x8 = unsafe { (y1.as_ptr().add(j) as *const f32x8).read_unaligned() };
|
|
137
|
-
let bx2: f32x8 = unsafe { (x2.as_ptr().add(j) as *const f32x8).read_unaligned() };
|
|
138
|
-
let by2: f32x8 = unsafe { (y2.as_ptr().add(j) as *const f32x8).read_unaligned() };
|
|
139
|
-
let ba: f32x8 = unsafe { (areas.as_ptr().add(j) as *const f32x8).read_unaligned() };
|
|
140
|
-
|
|
141
|
-
let ix1 = ax1.fast_max(bx1);
|
|
142
|
-
let iy1 = ay1.fast_max(by1);
|
|
143
|
-
let ix2 = ax2.fast_min(bx2);
|
|
144
|
-
let iy2 = ay2.fast_min(by2);
|
|
145
|
-
|
|
146
|
-
let iw = (ix2 - ix1).fast_max(zero_v);
|
|
147
|
-
let ih = (iy2 - iy1).fast_max(zero_v);
|
|
148
|
-
let inter = iw * ih;
|
|
149
|
-
let union = aa + ba - inter;
|
|
150
|
-
// Avoid divide-by-zero — when union is 0 the IoU is irrelevant
|
|
151
|
-
// (both boxes have zero area, the candidate would be filtered
|
|
152
|
-
// elsewhere). Use a sentinel large enough to fail the threshold
|
|
153
|
-
// check below.
|
|
154
|
-
let safe_union = union.fast_max(f32x8::splat(f32::MIN_POSITIVE));
|
|
155
|
-
let iou = inter / safe_union;
|
|
156
|
-
|
|
157
|
-
let mask = iou.simd_gt(iou_v);
|
|
158
|
-
// `to_bitmask` packs the SIMD comparison result into a bitmap where
|
|
159
|
-
// bit k is set when lane k of `iou` exceeds the threshold.
|
|
160
|
-
let bits = mask.to_bitmask();
|
|
161
|
-
if bits != 0 {
|
|
162
|
-
for k in 0..8 {
|
|
163
|
-
if (bits & (1 << k)) != 0 && !suppressed[j + k] && label_ids[j + k] == a_label {
|
|
164
|
-
suppressed[j + k] = true;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
j += 8;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Scalar tail — handle the remaining < 8 elements without SIMD.
|
|
173
|
-
while j < n {
|
|
174
|
-
if !suppressed[j] && label_ids[j] == a_label {
|
|
175
|
-
let ix1 = x1[i].max(x1[j]);
|
|
176
|
-
let iy1 = y1[i].max(y1[j]);
|
|
177
|
-
let ix2 = x2[i].min(x2[j]);
|
|
178
|
-
let iy2 = y2[i].min(y2[j]);
|
|
179
|
-
let iw = (ix2 - ix1).max(0.0);
|
|
180
|
-
let ih = (iy2 - iy1).max(0.0);
|
|
181
|
-
let inter = iw * ih;
|
|
182
|
-
let union = areas[i] + areas[j] - inter;
|
|
183
|
-
if union > 0.0 && inter / union > iou_threshold {
|
|
184
|
-
suppressed[j] = true;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
j += 1;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Move kept detections out of the candidates Vec — no clones, ownership
|
|
192
|
-
// transfer via take() over a Vec<Option<Detection>> would also work but
|
|
193
|
-
// an indexed swap_remove pattern is simpler since keep_indices is sorted.
|
|
194
|
-
let mut kept: Vec<Detection> = Vec::with_capacity(keep_indices.len());
|
|
195
|
-
// Build a flag vector so we can drain in original order with one pass.
|
|
196
|
-
let mut keep_flag = vec![false; n];
|
|
197
|
-
for &idx in &keep_indices {
|
|
198
|
-
keep_flag[idx] = true;
|
|
199
|
-
}
|
|
200
|
-
for (i, det) in candidates.into_iter().enumerate() {
|
|
201
|
-
if keep_flag[i] {
|
|
202
|
-
kept.push(det);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
kept
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
#[cfg(test)]
|
|
209
|
-
mod tests {
|
|
210
|
-
use super::*;
|
|
211
|
-
|
|
212
|
-
fn det(x: f32, y: f32, w: f32, h: f32, conf: f32, label: &str) -> Detection {
|
|
213
|
-
Detection {
|
|
214
|
-
x,
|
|
215
|
-
y,
|
|
216
|
-
width: w,
|
|
217
|
-
height: h,
|
|
218
|
-
confidence: conf,
|
|
219
|
-
label: label.to_string(),
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
#[test]
|
|
224
|
-
fn empty_input() {
|
|
225
|
-
assert!(nms(Vec::new(), 0.5, None).is_empty());
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
#[test]
|
|
229
|
-
fn single_box_kept() {
|
|
230
|
-
let input = vec![det(0.1, 0.1, 0.2, 0.2, 0.9, "person")];
|
|
231
|
-
let out = nms(input, 0.5, None);
|
|
232
|
-
assert_eq!(out.len(), 1);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
#[test]
|
|
236
|
-
fn duplicate_boxes_suppressed() {
|
|
237
|
-
let input = vec![
|
|
238
|
-
det(0.1, 0.1, 0.2, 0.2, 0.9, "person"),
|
|
239
|
-
det(0.1, 0.1, 0.2, 0.2, 0.8, "person"),
|
|
240
|
-
det(0.1, 0.1, 0.2, 0.2, 0.7, "person"),
|
|
241
|
-
];
|
|
242
|
-
let out = nms(input, 0.5, None);
|
|
243
|
-
assert_eq!(out.len(), 1);
|
|
244
|
-
assert!((out[0].confidence - 0.9).abs() < 1e-6);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
#[test]
|
|
248
|
-
fn different_classes_kept() {
|
|
249
|
-
let input = vec![
|
|
250
|
-
det(0.1, 0.1, 0.2, 0.2, 0.9, "person"),
|
|
251
|
-
det(0.1, 0.1, 0.2, 0.2, 0.8, "car"),
|
|
252
|
-
];
|
|
253
|
-
let out = nms(input, 0.5, None);
|
|
254
|
-
assert_eq!(out.len(), 2);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
#[test]
|
|
258
|
-
fn many_overlapping_simd_path() {
|
|
259
|
-
// Generate 20 nearly-identical person boxes — exercises the SIMD
|
|
260
|
-
// chunked loop (n - 1 = 19, so multiple f32x8 iterations).
|
|
261
|
-
let mut input = Vec::new();
|
|
262
|
-
for i in 0..20 {
|
|
263
|
-
input.push(det(0.1, 0.1, 0.2, 0.2, 0.9 - i as f32 * 0.001, "person"));
|
|
264
|
-
}
|
|
265
|
-
let out = nms(input, 0.5, None);
|
|
266
|
-
assert_eq!(out.len(), 1);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
#[test]
|
|
270
|
-
fn max_detections_cap() {
|
|
271
|
-
let input = vec![
|
|
272
|
-
det(0.0, 0.0, 0.1, 0.1, 0.9, "a"),
|
|
273
|
-
det(0.2, 0.2, 0.1, 0.1, 0.85, "b"),
|
|
274
|
-
det(0.4, 0.4, 0.1, 0.1, 0.8, "c"),
|
|
275
|
-
det(0.6, 0.6, 0.1, 0.1, 0.75, "d"),
|
|
276
|
-
];
|
|
277
|
-
let out = nms(input, 0.5, Some(2));
|
|
278
|
-
assert_eq!(out.len(), 2);
|
|
279
|
-
// Top two by confidence
|
|
280
|
-
assert!((out[0].confidence - 0.9).abs() < 1e-6);
|
|
281
|
-
assert!((out[1].confidence - 0.85).abs() < 1e-6);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
#[test]
|
|
285
|
-
fn output_sorted_by_confidence() {
|
|
286
|
-
let input = vec![
|
|
287
|
-
det(0.0, 0.0, 0.1, 0.1, 0.5, "a"),
|
|
288
|
-
det(0.2, 0.2, 0.1, 0.1, 0.9, "b"),
|
|
289
|
-
det(0.4, 0.4, 0.1, 0.1, 0.7, "c"),
|
|
290
|
-
];
|
|
291
|
-
let out = nms(input, 0.5, None);
|
|
292
|
-
assert_eq!(out.len(), 3);
|
|
293
|
-
assert!(out[0].confidence > out[1].confidence);
|
|
294
|
-
assert!(out[1].confidence > out[2].confidence);
|
|
295
|
-
}
|
|
296
|
-
}
|