1#![allow(clippy::cast_precision_loss, clippy::needless_range_loop)]
3
4use core::cmp::Ordering;
5
6use crate::{CoreError, Detection, ios, iou};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum OverlapMetric {
11 IoU,
13 IoS,
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum NmsMode {
20 ClassAware,
22 ClassAgnostic,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct NmsConfig {
29 pub threshold: f32,
32 pub mode: NmsMode,
34 pub metric: OverlapMetric,
36}
37
38pub fn nms_in_place(
51 detections: &mut [Detection],
52 order_scratch: &mut [usize],
53 suppressed_scratch: &mut [bool],
54 config: NmsConfig,
55) -> Result<usize, CoreError> {
56 if !config.threshold.is_finite() || !(0.0..=1.0).contains(&config.threshold) {
57 return Err(CoreError::InvalidThreshold);
58 }
59
60 let len = detections.len();
61 if order_scratch.len() < len || suppressed_scratch.len() < len {
62 return Err(CoreError::InsufficientScratch);
63 }
64
65 let order = &mut order_scratch[..len];
66 let suppressed = &mut suppressed_scratch[..len];
67 for (index, slot) in order.iter_mut().enumerate() {
68 *slot = index;
69 }
70 for value in suppressed.iter_mut() {
71 *value = false;
72 }
73
74 order.sort_unstable_by(|left, right| {
75 detections[*right]
76 .score()
77 .partial_cmp(&detections[*left].score())
78 .unwrap_or(Ordering::Equal)
79 .then_with(|| left.cmp(right))
80 });
81
82 for candidate_position in 0..len {
83 let candidate_index = order[candidate_position];
84 if suppressed[candidate_index] {
85 continue;
86 }
87
88 for contender_index in order.iter().skip(candidate_position + 1).copied() {
89 if suppressed[contender_index]
90 || (config.mode == NmsMode::ClassAware
91 && detections[candidate_index].class_id()
92 != detections[contender_index].class_id())
93 {
94 continue;
95 }
96
97 let overlap = match config.metric {
98 OverlapMetric::IoU => iou(
99 detections[candidate_index].bbox(),
100 detections[contender_index].bbox(),
101 ),
102 OverlapMetric::IoS => ios(
103 detections[candidate_index].bbox(),
104 detections[contender_index].bbox(),
105 ),
106 };
107 if overlap > config.threshold {
108 suppressed[contender_index] = true;
109 }
110 }
111 }
112
113 let mut kept = 0;
114 for index in 0..len {
115 if !suppressed[index] {
116 let detection = detections[index];
117 detections[kept] = detection;
118 kept += 1;
119 }
120 }
121
122 Ok(kept)
123}
124
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub enum SoftNmsMethod {
128 Linear,
130 Gaussian,
132}
133
134#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct SoftNmsConfig {
137 pub threshold: f32,
139 pub sigma: f32,
141 pub score_threshold: f32,
143 pub method: SoftNmsMethod,
145 pub mode: NmsMode,
147 pub metric: OverlapMetric,
149}
150
151impl Default for SoftNmsConfig {
152 fn default() -> Self {
153 Self {
154 threshold: 0.5,
155 sigma: 0.5,
156 score_threshold: 0.001,
157 method: SoftNmsMethod::Gaussian,
158 mode: NmsMode::ClassAgnostic,
159 metric: OverlapMetric::IoU,
160 }
161 }
162}
163
164pub fn soft_nms_in_place(
173 detections: &mut [Detection],
174 order_scratch: &mut [usize],
175 scores_scratch: &mut [f32],
176 config: SoftNmsConfig,
177) -> Result<usize, CoreError> {
178 if !config.threshold.is_finite()
179 || !(0.0..=1.0).contains(&config.threshold)
180 || !config.sigma.is_finite()
181 || config.sigma <= 0.0
182 || !config.score_threshold.is_finite()
183 {
184 return Err(CoreError::InvalidThreshold);
185 }
186 let len = detections.len();
187 if order_scratch.len() < len || scores_scratch.len() < len {
188 return Err(CoreError::InsufficientScratch);
189 }
190 let order = &mut order_scratch[..len];
191 let scores = &mut scores_scratch[..len];
192 for (i, d) in detections.iter().enumerate() {
193 order[i] = i;
194 scores[i] = d.score();
195 }
196
197 let mut remaining = len;
199 while remaining > 0 {
200 let mut best_pos = 0_usize;
201 for p in 1..remaining {
202 let cmp = scores[order[p]]
203 .partial_cmp(&scores[order[best_pos]])
204 .unwrap_or(Ordering::Equal);
205 if cmp == Ordering::Greater || (cmp == Ordering::Equal && order[p] < order[best_pos]) {
206 best_pos = p;
207 }
208 }
209 order.swap(best_pos, remaining - 1);
211 let cand = order[remaining - 1];
212 remaining -= 1;
213 if scores[cand] < config.score_threshold {
214 scores[cand] = 0.0;
215 continue;
216 }
217 for p in 0..remaining {
218 let other = order[p];
219 if config.mode == NmsMode::ClassAware
220 && detections[cand].class_id() != detections[other].class_id()
221 {
222 continue;
223 }
224 let overlap = match config.metric {
225 OverlapMetric::IoU => iou(detections[cand].bbox(), detections[other].bbox()),
226 OverlapMetric::IoS => ios(detections[cand].bbox(), detections[other].bbox()),
227 };
228 let weight = match config.method {
229 SoftNmsMethod::Linear => {
230 if overlap > config.threshold {
231 1.0 - overlap
232 } else {
233 1.0
234 }
235 }
236 SoftNmsMethod::Gaussian => exp_neg_approx((overlap * overlap) / config.sigma),
237 };
238 scores[other] *= weight;
239 }
240 }
241
242 for (i, slot) in order.iter_mut().enumerate() {
243 *slot = i;
244 }
245 order.sort_unstable_by(|a, b| {
246 scores[*b]
247 .partial_cmp(&scores[*a])
248 .unwrap_or(Ordering::Equal)
249 .then_with(|| a.cmp(b))
250 });
251 let mut out = 0_usize;
252 for &i in order.iter() {
253 if scores[i] < config.score_threshold {
254 break;
255 }
256 let d = detections[i];
257 detections[out] = Detection::new(d.bbox(), scores[i], d.class_id(), d.track_id())
258 .map_err(|_| CoreError::NonFinite)?;
259 out += 1;
260 }
261 Ok(out)
262}
263
264fn union_find_root(parent: &mut [usize], mut x: usize) -> usize {
265 while parent[x] != x {
266 parent[x] = parent[parent[x]];
267 x = parent[x];
268 }
269 x
270}
271
272pub fn merge_nms_in_place(
280 detections: &mut [Detection],
281 order_scratch: &mut [usize],
282 parent_scratch: &mut [usize],
283 config: NmsConfig,
284) -> Result<usize, CoreError> {
285 if !config.threshold.is_finite() || !(0.0..=1.0).contains(&config.threshold) {
286 return Err(CoreError::InvalidThreshold);
287 }
288 let len = detections.len();
289 if order_scratch.len() < len || parent_scratch.len() < len {
290 return Err(CoreError::InsufficientScratch);
291 }
292 let parent = &mut parent_scratch[..len];
293 for (i, p) in parent.iter_mut().enumerate() {
294 *p = i;
295 }
296 for i in 0..len {
297 for j in (i + 1)..len {
298 if config.mode == NmsMode::ClassAware
299 && detections[i].class_id() != detections[j].class_id()
300 {
301 continue;
302 }
303 let overlap = match config.metric {
304 OverlapMetric::IoU => iou(detections[i].bbox(), detections[j].bbox()),
305 OverlapMetric::IoS => ios(detections[i].bbox(), detections[j].bbox()),
306 };
307 if overlap > config.threshold {
308 let a = union_find_root(parent, i);
309 let b = union_find_root(parent, j);
310 if a != b {
311 if detections[a].score() >= detections[b].score() {
313 parent[b] = a;
314 } else {
315 parent[a] = b;
316 }
317 }
318 }
319 }
320 }
321 let used = order_scratch;
323 for u in used.iter_mut().take(len) {
324 *u = 0;
325 }
326 let mut out = 0_usize;
327 for i in 0..len {
328 let r = union_find_root(parent, i);
329 if used[r] != 0 {
330 continue;
331 }
332 used[r] = 1;
333 let mut sum_l = 0.0_f32;
334 let mut sum_t = 0.0_f32;
335 let mut sum_r = 0.0_f32;
336 let mut sum_b = 0.0_f32;
337 let mut max_score = 0.0_f32;
338 let mut n = 0_f32;
339 let mut class_id = detections[r].class_id();
340 let mut track_id = detections[r].track_id();
341 for j in 0..len {
342 if union_find_root(parent, j) != r {
343 continue;
344 }
345 let b = detections[j].bbox();
346 sum_l += b.left();
347 sum_t += b.top();
348 sum_r += b.right();
349 sum_b += b.bottom();
350 max_score = max_score.max(detections[j].score());
351 n += 1.0;
352 if detections[j].class_id().is_some() {
353 class_id = detections[j].class_id();
354 }
355 if detections[j].track_id().is_some() {
356 track_id = detections[j].track_id();
357 }
358 }
359 if n <= 0.0 {
360 continue;
361 }
362 let rect = crate::Rect::new(sum_l / n, sum_t / n, sum_r / n, sum_b / n)
363 .map_err(|_| CoreError::NonFinite)?;
364 detections[out] = Detection::new(rect, max_score, class_id, track_id)
365 .map_err(|_| CoreError::NonFinite)?;
366 out += 1;
367 }
368 Ok(out)
369}
370
371fn exp_neg_approx(x: f32) -> f32 {
373 if !x.is_finite() || x < 0.0 {
374 return 1.0;
375 }
376 if x > 20.0 {
377 return 0.0;
378 }
379 let mut term = 1.0_f32;
381 let mut sum = 1.0_f32;
382 for k in 1..12 {
383 term *= -x / k as f32;
384 sum += term;
385 }
386 sum.clamp(0.0, 1.0)
387}