@camera.ui/rust-postprocessor 0.0.1

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/src/nms.rs ADDED
@@ -0,0 +1,296 @@
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
+ }