1use std::sync::Arc;
2
3#[cfg(feature = "parallel")]
4use crate::par_util;
5#[cfg(feature = "parallel")]
6use rayon::prelude::*;
7
8use ad_core_rs::ndarray::{NDArray, NDDataBuffer};
9use ad_core_rs::ndarray_pool::NDArrayPool;
10use ad_core_rs::plugin::runtime::{
11 NDPluginProcess, ParamUpdate, PluginParamSnapshot, PluginRuntimeHandle, ProcessResult,
12};
13use ad_core_rs::plugin::wiring::WiringRegistry;
14use asyn_rs::param::ParamType;
15use asyn_rs::port::PortDriverBase;
16use parking_lot::Mutex;
17
18#[derive(Clone, Copy, Default)]
20pub struct NDStatsParams {
21 pub compute_statistics: usize,
22 pub bgd_width: usize,
23 pub min_value: usize,
24 pub max_value: usize,
25 pub mean_value: usize,
26 pub sigma_value: usize,
27 pub total: usize,
28 pub net: usize,
29 pub min_x: usize,
30 pub min_y: usize,
31 pub max_x: usize,
32 pub max_y: usize,
33 pub compute_centroid: usize,
34 pub centroid_threshold: usize,
35 pub centroid_total: usize,
36 pub centroid_x: usize,
37 pub centroid_y: usize,
38 pub sigma_x: usize,
39 pub sigma_y: usize,
40 pub sigma_xy: usize,
41 pub skewness_x: usize,
42 pub skewness_y: usize,
43 pub kurtosis_x: usize,
44 pub kurtosis_y: usize,
45 pub eccentricity: usize,
46 pub orientation: usize,
47 pub compute_histogram: usize,
48 pub hist_size: usize,
49 pub hist_min: usize,
50 pub hist_max: usize,
51 pub hist_below: usize,
52 pub hist_above: usize,
53 pub hist_entropy: usize,
54 pub compute_profiles: usize,
55 pub cursor_x: usize,
56 pub cursor_y: usize,
57 pub cursor_val: usize,
58 pub profile_size_x: usize,
59 pub profile_size_y: usize,
60 pub skewx_value: usize,
61 pub skewy_value: usize,
62 pub profile_average_x: usize,
63 pub profile_average_y: usize,
64 pub profile_threshold_x: usize,
65 pub profile_threshold_y: usize,
66 pub profile_centroid_x: usize,
67 pub profile_centroid_y: usize,
68 pub profile_cursor_x: usize,
69 pub profile_cursor_y: usize,
70 pub hist_array: usize,
71 pub hist_x_array: usize,
72}
73
74#[derive(Debug, Clone, Default)]
76pub struct StatsResult {
77 pub min: f64,
78 pub max: f64,
79 pub mean: f64,
80 pub sigma: f64,
81 pub total: f64,
82 pub net: f64,
83 pub num_elements: usize,
84 pub min_x: usize,
85 pub min_y: usize,
86 pub max_x: usize,
87 pub max_y: usize,
88 pub histogram: Vec<f64>,
89 pub hist_below: f64,
90 pub hist_above: f64,
91 pub hist_entropy: f64,
92 pub profile_avg_x: Vec<f64>,
93 pub profile_avg_y: Vec<f64>,
94 pub profile_threshold_x: Vec<f64>,
95 pub profile_threshold_y: Vec<f64>,
96 pub profile_centroid_x: Vec<f64>,
97 pub profile_centroid_y: Vec<f64>,
98 pub profile_cursor_x: Vec<f64>,
99 pub profile_cursor_y: Vec<f64>,
100 pub cursor_value: f64,
101}
102
103#[derive(Debug, Clone, Default)]
105pub struct CentroidResult {
106 pub centroid_x: f64,
107 pub centroid_y: f64,
108 pub sigma_x: f64,
109 pub sigma_y: f64,
110 pub sigma_xy: f64,
111 pub centroid_total: f64,
112 pub skewness_x: f64,
113 pub skewness_y: f64,
114 pub kurtosis_x: f64,
115 pub kurtosis_y: f64,
116 pub eccentricity: f64,
117 pub orientation: f64,
118}
119
120#[derive(Debug, Clone, Default)]
122pub struct ProfileResult {
123 pub avg_x: Vec<f64>,
124 pub avg_y: Vec<f64>,
125 pub threshold_x: Vec<f64>,
126 pub threshold_y: Vec<f64>,
127 pub centroid_x: Vec<f64>,
128 pub centroid_y: Vec<f64>,
129 pub cursor_x: Vec<f64>,
130 pub cursor_y: Vec<f64>,
131}
132
133pub fn compute_stats(
144 data: &NDDataBuffer,
145 dims: &[ad_core_rs::ndarray::NDDimension],
146 bgd_width: usize,
147) -> StatsResult {
148 macro_rules! stats_for {
149 ($vec:expr) => {{
150 let v = $vec;
151 if v.is_empty() {
152 return StatsResult::default();
153 }
154
155 let (min, max, min_idx, max_idx, total, variance);
156
157 #[cfg(feature = "parallel")]
158 {
159 if par_util::should_parallelize(v.len()) {
160 let (pmin, pmax, pmin_idx, pmax_idx, ptotal) =
162 par_util::thread_pool().install(|| {
163 v.par_iter()
164 .enumerate()
165 .fold(
166 || (f64::MAX, f64::MIN, 0usize, 0usize, 0.0f64),
167 |(mn, mx, mn_i, mx_i, s), (i, &elem)| {
168 let f = elem as f64;
169 let (new_mn, new_mn_i) =
170 if f < mn { (f, i) } else { (mn, mn_i) };
171 let (new_mx, new_mx_i) =
172 if f > mx { (f, i) } else { (mx, mx_i) };
173 (new_mn, new_mx, new_mn_i, new_mx_i, s + f)
174 },
175 )
176 .reduce(
177 || (f64::MAX, f64::MIN, 0, 0, 0.0),
178 |(mn1, mx1, mn_i1, mx_i1, s1), (mn2, mx2, mn_i2, mx_i2, s2)| {
179 let (rmn, rmn_i) = if mn1 <= mn2 {
180 (mn1, mn_i1)
181 } else {
182 (mn2, mn_i2)
183 };
184 let (rmx, rmx_i) = if mx1 >= mx2 {
185 (mx1, mx_i1)
186 } else {
187 (mx2, mx_i2)
188 };
189 (rmn, rmx, rmn_i, rmx_i, s1 + s2)
190 },
191 )
192 });
193 min = pmin;
194 max = pmax;
195 min_idx = pmin_idx;
196 max_idx = pmax_idx;
197 total = ptotal;
198 let mean_tmp = total / v.len() as f64;
199 variance = par_util::thread_pool().install(|| {
200 v.par_iter()
201 .map(|&elem| {
202 let d = elem as f64 - mean_tmp;
203 d * d
204 })
205 .sum::<f64>()
206 });
207 } else {
208 let mut lmin = v[0] as f64;
209 let mut lmax = v[0] as f64;
210 let mut lmin_idx: usize = 0;
211 let mut lmax_idx: usize = 0;
212 let mut ltotal = 0.0f64;
213 for (i, &elem) in v.iter().enumerate() {
214 let f = elem as f64;
215 if f < lmin {
216 lmin = f;
217 lmin_idx = i;
218 }
219 if f > lmax {
220 lmax = f;
221 lmax_idx = i;
222 }
223 ltotal += f;
224 }
225 min = lmin;
226 max = lmax;
227 min_idx = lmin_idx;
228 max_idx = lmax_idx;
229 total = ltotal;
230 let mean_tmp = total / v.len() as f64;
231 let mut lvar = 0.0f64;
232 for &elem in v.iter() {
233 let d = elem as f64 - mean_tmp;
234 lvar += d * d;
235 }
236 variance = lvar;
237 }
238 }
239
240 #[cfg(not(feature = "parallel"))]
241 {
242 let mut lmin = v[0] as f64;
243 let mut lmax = v[0] as f64;
244 let mut lmin_idx: usize = 0;
245 let mut lmax_idx: usize = 0;
246 let mut ltotal = 0.0f64;
247 for (i, &elem) in v.iter().enumerate() {
248 let f = elem as f64;
249 if f < lmin {
250 lmin = f;
251 lmin_idx = i;
252 }
253 if f > lmax {
254 lmax = f;
255 lmax_idx = i;
256 }
257 ltotal += f;
258 }
259 min = lmin;
260 max = lmax;
261 min_idx = lmin_idx;
262 max_idx = lmax_idx;
263 total = ltotal;
264 let mean_tmp = total / v.len() as f64;
265 let mut lvar = 0.0f64;
266 for &elem in v.iter() {
267 let d = elem as f64 - mean_tmp;
268 lvar += d * d;
269 }
270 variance = lvar;
271 }
272
273 let mean = total / v.len() as f64;
274 let sigma = (variance / v.len() as f64).sqrt();
275 let x_size = dims.first().map_or(v.len(), |d| d.size);
276
277 let net = if bgd_width > 0 && !dims.is_empty() {
288 let sizes: Vec<usize> = dims.iter().map(|d| d.size).collect();
289 let ndims = sizes.len();
292 let mut strides = vec![1usize; ndims];
293 for i in 1..ndims {
294 strides[i] = strides[i - 1] * sizes[i - 1];
295 }
296
297 let strip = |sd: usize, s_off: usize, s_len: usize| -> (f64, usize) {
301 if s_len == 0 {
302 return (0.0, 0);
303 }
304 let mut count = s_len;
306 for (d, &sz) in sizes.iter().enumerate() {
307 if d != sd {
308 count *= sz;
309 }
310 }
311 let mut sum = 0.0f64;
312 let mut coords = vec![0usize; ndims];
315 for _ in 0..count {
316 let mut flat = 0usize;
317 for d in 0..ndims {
318 let c = if d == sd {
319 coords[d] + s_off
320 } else {
321 coords[d]
322 };
323 flat += c * strides[d];
324 }
325 if flat < v.len() {
326 sum += v[flat] as f64;
327 }
328 for d in 0..ndims {
332 let radix = if d == sd { s_len } else { sizes[d] };
333 coords[d] += 1;
334 if coords[d] < radix {
335 break;
336 }
337 coords[d] = 0;
338 }
339 }
340 (sum, count)
341 };
342
343 let mut bgd_counts = 0.0f64;
344 let mut bgd_pixels = 0usize;
345 for (d, &dim_size) in sizes.iter().enumerate() {
346 let low_len = bgd_width.min(dim_size);
348 let (low_sum, low_n) = strip(d, 0, low_len);
349 bgd_counts += low_sum;
350 bgd_pixels += low_n;
351 let high_off = dim_size.saturating_sub(bgd_width);
354 let high_len = bgd_width.min(dim_size - high_off);
355 let (high_sum, high_n) = strip(d, high_off, high_len);
356 bgd_counts += high_sum;
357 bgd_pixels += high_n;
358 }
359 let bgd_avg = bgd_counts / bgd_pixels.max(1) as f64;
361 total - bgd_avg * v.len() as f64
362 } else {
363 total
364 };
365
366 StatsResult {
367 min,
368 max,
369 mean,
370 sigma,
371 total,
372 net,
373 num_elements: v.len(),
374 min_x: if x_size > 0 { min_idx % x_size } else { 0 },
375 min_y: if x_size > 0 { min_idx / x_size } else { 0 },
376 max_x: if x_size > 0 { max_idx % x_size } else { 0 },
377 max_y: if x_size > 0 { max_idx / x_size } else { 0 },
378 ..StatsResult::default()
379 }
380 }};
381 }
382
383 match data {
384 NDDataBuffer::I8(v) => stats_for!(v),
385 NDDataBuffer::U8(v) => stats_for!(v),
386 NDDataBuffer::I16(v) => stats_for!(v),
387 NDDataBuffer::U16(v) => stats_for!(v),
388 NDDataBuffer::I32(v) => stats_for!(v),
389 NDDataBuffer::U32(v) => stats_for!(v),
390 NDDataBuffer::I64(v) => stats_for!(v),
391 NDDataBuffer::U64(v) => stats_for!(v),
392 NDDataBuffer::F32(v) => stats_for!(v),
393 NDDataBuffer::F64(v) => stats_for!(v),
394 }
395}
396
397pub fn compute_centroid(
401 data: &NDDataBuffer,
402 x_size: usize,
403 y_size: usize,
404 threshold: f64,
405) -> CentroidResult {
406 let n = x_size * y_size;
407 if n == 0 || data.len() < n {
408 return CentroidResult::default();
409 }
410
411 let vals: Vec<f64> = (0..n).map(|i| data.get_as_f64(i).unwrap_or(0.0)).collect();
413
414 let (m00, m10, m01);
416
417 #[cfg(feature = "parallel")]
418 {
419 if par_util::should_parallelize(n) {
420 let xs = x_size;
421 let thr = threshold;
422 let (pm00, pm10, pm01) = par_util::thread_pool().install(|| {
423 vals.par_iter()
424 .enumerate()
425 .fold(
426 || (0.0f64, 0.0f64, 0.0f64),
427 |(s00, s10, s01), (i, &val)| {
428 if val < thr {
429 return (s00, s10, s01);
430 }
431 let ix = i % xs;
432 let iy = i / xs;
433 (s00 + val, s10 + val * ix as f64, s01 + val * iy as f64)
434 },
435 )
436 .reduce(
437 || (0.0, 0.0, 0.0),
438 |(a0, a1, a2), (b0, b1, b2)| (a0 + b0, a1 + b1, a2 + b2),
439 )
440 });
441 m00 = pm00;
442 m10 = pm10;
443 m01 = pm01;
444 } else {
445 let mut lm00 = 0.0f64;
446 let mut lm10 = 0.0f64;
447 let mut lm01 = 0.0f64;
448 for iy in 0..y_size {
449 for ix in 0..x_size {
450 let val = vals[iy * x_size + ix];
451 if val < threshold {
452 continue;
453 }
454 lm00 += val;
455 lm10 += val * ix as f64;
456 lm01 += val * iy as f64;
457 }
458 }
459 m00 = lm00;
460 m10 = lm10;
461 m01 = lm01;
462 }
463 }
464
465 #[cfg(not(feature = "parallel"))]
466 {
467 let mut lm00 = 0.0f64;
468 let mut lm10 = 0.0f64;
469 let mut lm01 = 0.0f64;
470 for iy in 0..y_size {
471 for ix in 0..x_size {
472 let val = vals[iy * x_size + ix];
473 if val < threshold {
474 continue;
475 }
476 lm00 += val;
477 lm10 += val * ix as f64;
478 lm01 += val * iy as f64;
479 }
480 }
481 m00 = lm00;
482 m10 = lm10;
483 m01 = lm01;
484 }
485
486 if m00 == 0.0 {
487 return CentroidResult::default();
488 }
489
490 let cx = m10 / m00;
491 let cy = m01 / m00;
492
493 let (mu20, mu02, mu11, m30_central, m03_central, m40_central, m04_central);
495
496 #[cfg(feature = "parallel")]
497 {
498 if par_util::should_parallelize(n) {
499 let xs = x_size;
500 let thr = threshold;
501 let (p20, p02, p11, p30, p03, p40, p04) = par_util::thread_pool().install(|| {
502 vals.par_iter()
503 .enumerate()
504 .fold(
505 || (0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64),
506 |(s20, s02, s11, s30, s03, s40, s04), (i, &val)| {
507 if val < thr {
508 return (s20, s02, s11, s30, s03, s40, s04);
509 }
510 let ix = i % xs;
511 let iy = i / xs;
512 let dx = ix as f64 - cx;
513 let dy = iy as f64 - cy;
514 let dx2 = dx * dx;
515 let dy2 = dy * dy;
516 (
517 s20 + val * dx2,
518 s02 + val * dy2,
519 s11 + val * dx * dy,
520 s30 + val * dx2 * dx,
521 s03 + val * dy2 * dy,
522 s40 + val * dx2 * dx2,
523 s04 + val * dy2 * dy2,
524 )
525 },
526 )
527 .reduce(
528 || (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
529 |(a0, a1, a2, a3, a4, a5, a6), (b0, b1, b2, b3, b4, b5, b6)| {
530 (
531 a0 + b0,
532 a1 + b1,
533 a2 + b2,
534 a3 + b3,
535 a4 + b4,
536 a5 + b5,
537 a6 + b6,
538 )
539 },
540 )
541 });
542 mu20 = p20;
543 mu02 = p02;
544 mu11 = p11;
545 m30_central = p30;
546 m03_central = p03;
547 m40_central = p40;
548 m04_central = p04;
549 } else {
550 let mut l20 = 0.0f64;
551 let mut l02 = 0.0f64;
552 let mut l11 = 0.0f64;
553 let mut l30 = 0.0f64;
554 let mut l03 = 0.0f64;
555 let mut l40 = 0.0f64;
556 let mut l04 = 0.0f64;
557 for iy in 0..y_size {
558 for ix in 0..x_size {
559 let val = vals[iy * x_size + ix];
560 if val < threshold {
561 continue;
562 }
563 let dx = ix as f64 - cx;
564 let dy = iy as f64 - cy;
565 let dx2 = dx * dx;
566 let dy2 = dy * dy;
567 l20 += val * dx2;
568 l02 += val * dy2;
569 l11 += val * dx * dy;
570 l30 += val * dx2 * dx;
571 l03 += val * dy2 * dy;
572 l40 += val * dx2 * dx2;
573 l04 += val * dy2 * dy2;
574 }
575 }
576 mu20 = l20;
577 mu02 = l02;
578 mu11 = l11;
579 m30_central = l30;
580 m03_central = l03;
581 m40_central = l40;
582 m04_central = l04;
583 }
584 }
585
586 #[cfg(not(feature = "parallel"))]
587 {
588 let mut l20 = 0.0f64;
589 let mut l02 = 0.0f64;
590 let mut l11 = 0.0f64;
591 let mut l30 = 0.0f64;
592 let mut l03 = 0.0f64;
593 let mut l40 = 0.0f64;
594 let mut l04 = 0.0f64;
595 for iy in 0..y_size {
596 for ix in 0..x_size {
597 let val = vals[iy * x_size + ix];
598 if val < threshold {
599 continue;
600 }
601 let dx = ix as f64 - cx;
602 let dy = iy as f64 - cy;
603 let dx2 = dx * dx;
604 let dy2 = dy * dy;
605 l20 += val * dx2;
606 l02 += val * dy2;
607 l11 += val * dx * dy;
608 l30 += val * dx2 * dx;
609 l03 += val * dy2 * dy;
610 l40 += val * dx2 * dx2;
611 l04 += val * dy2 * dy2;
612 }
613 }
614 mu20 = l20;
615 mu02 = l02;
616 mu11 = l11;
617 m30_central = l30;
618 m03_central = l03;
619 m40_central = l40;
620 m04_central = l04;
621 }
622
623 let sigma_x = (mu20 / m00).sqrt();
624 let sigma_y = (mu02 / m00).sqrt();
625 let sigma_xy = if sigma_x > 0.0 && sigma_y > 0.0 {
626 (mu11 / m00) / (sigma_x * sigma_y)
627 } else {
628 0.0
629 };
630
631 let skewness_x = if sigma_x > 0.0 {
633 m30_central / (m00 * sigma_x.powi(3))
634 } else {
635 0.0
636 };
637 let skewness_y = if sigma_y > 0.0 {
638 m03_central / (m00 * sigma_y.powi(3))
639 } else {
640 0.0
641 };
642
643 let kurtosis_x = if sigma_x > 0.0 {
645 m40_central / (m00 * sigma_x.powi(4)) - 3.0
646 } else {
647 0.0
648 };
649 let kurtosis_y = if sigma_y > 0.0 {
650 m04_central / (m00 * sigma_y.powi(4)) - 3.0
651 } else {
652 0.0
653 };
654
655 let denom = mu20 + mu02;
658 let eccentricity = if denom > 0.0 {
659 ((mu20 - mu02).powi(2) - 4.0 * mu11.powi(2)) / denom.powi(2)
660 } else {
661 0.0
662 };
663
664 let orientation = 0.5 * (2.0 * mu11).atan2(mu20 - mu02) * 180.0 / std::f64::consts::PI;
666
667 CentroidResult {
668 centroid_x: cx,
669 centroid_y: cy,
670 sigma_x,
671 sigma_y,
672 sigma_xy,
673 centroid_total: m00,
674 skewness_x,
675 skewness_y,
676 kurtosis_x,
677 kurtosis_y,
678 eccentricity,
679 orientation,
680 }
681}
682
683pub fn compute_histogram(
692 data: &NDDataBuffer,
693 hist_size: usize,
694 hist_min: f64,
695 hist_max: f64,
696) -> (Vec<f64>, f64, f64, f64) {
697 if hist_size == 0 || hist_max <= hist_min {
698 return (vec![], 0.0, 0.0, 0.0);
699 }
700
701 let mut histogram = vec![0.0f64; hist_size];
702 let mut below = 0.0f64;
703 let mut above = 0.0f64;
704 let range = hist_max - hist_min;
705 let n = data.len();
706
707 #[cfg(feature = "parallel")]
708 let use_parallel = par_util::should_parallelize(n);
709 #[cfg(not(feature = "parallel"))]
710 let use_parallel = false;
711
712 if use_parallel {
713 #[cfg(feature = "parallel")]
714 {
715 let vals: Vec<f64> = (0..n).map(|i| data.get_as_f64(i).unwrap_or(0.0)).collect();
716 let chunk_size = (n / rayon::current_num_threads().max(1)).max(1024);
717 let hs = hist_size;
718 let hmin = hist_min;
719 let hmax = hist_max;
720 let rng = range;
721 let chunk_results: Vec<(Vec<f64>, f64, f64)> = par_util::thread_pool().install(|| {
722 vals.par_chunks(chunk_size)
723 .map(|chunk| {
724 let mut local_hist = vec![0.0f64; hs];
725 let mut local_below = 0.0f64;
726 let mut local_above = 0.0f64;
727 for &val in chunk {
728 if val < hmin {
729 local_below += 1.0;
730 } else if val > hmax {
731 local_above += 1.0;
732 } else {
733 let bin = ((val - hmin) * (hs - 1) as f64 / rng + 0.5) as usize;
734 let bin = bin.min(hs - 1);
735 local_hist[bin] += 1.0;
736 }
737 }
738 (local_hist, local_below, local_above)
739 })
740 .collect()
741 });
742 for (local_hist, local_below, local_above) in chunk_results {
743 below += local_below;
744 above += local_above;
745 for (i, &count) in local_hist.iter().enumerate() {
746 histogram[i] += count;
747 }
748 }
749 }
750 } else {
751 for i in 0..n {
752 let val = data.get_as_f64(i).unwrap_or(0.0);
753 if val < hist_min {
754 below += 1.0;
755 } else if val > hist_max {
756 above += 1.0;
757 } else {
758 let bin = ((val - hist_min) * (hist_size - 1) as f64 / range + 0.5) as usize;
759 let bin = bin.min(hist_size - 1);
760 histogram[bin] += 1.0;
761 }
762 }
763 }
764
765 let n_elements = data.len() as f64;
768 let entropy = if n_elements > 0.0 {
769 let mut ent = 0.0f64;
770 for &count in &histogram {
771 let c = if count <= 0.0 { 1.0 } else { count };
772 ent += c * c.ln();
773 }
774 -ent / n_elements
775 } else {
776 0.0
777 };
778
779 (histogram, below, above, entropy)
780}
781
782pub fn compute_profiles(
789 data: &NDDataBuffer,
790 x_size: usize,
791 y_size: usize,
792 threshold: f64,
793 centroid_x: f64,
794 centroid_y: f64,
795 cursor_x: usize,
796 cursor_y: usize,
797) -> ProfileResult {
798 if x_size == 0 || y_size == 0 || data.len() < x_size * y_size {
799 return ProfileResult::default();
800 }
801
802 let mut avg_x = vec![0.0f64; x_size];
803 let mut avg_y = vec![0.0f64; y_size];
804 let mut thresh_x_sum = vec![0.0f64; x_size];
805 let mut thresh_x_cnt = vec![0usize; x_size];
806 let mut thresh_y_sum = vec![0.0f64; y_size];
807 let mut thresh_y_cnt = vec![0usize; y_size];
808
809 for iy in 0..y_size {
811 for ix in 0..x_size {
812 let val = data.get_as_f64(iy * x_size + ix).unwrap_or(0.0);
813 avg_x[ix] += val;
814 avg_y[iy] += val;
815 if val >= threshold {
816 thresh_x_sum[ix] += val;
817 thresh_x_cnt[ix] += 1;
818 thresh_y_sum[iy] += val;
819 thresh_y_cnt[iy] += 1;
820 }
821 }
822 }
823
824 for ix in 0..x_size {
826 avg_x[ix] /= y_size as f64;
827 }
828 for iy in 0..y_size {
829 avg_y[iy] /= x_size as f64;
830 }
831
832 let threshold_x: Vec<f64> = thresh_x_sum
834 .iter()
835 .zip(thresh_x_cnt.iter())
836 .map(|(&s, &c)| if c > 0 { s / c as f64 } else { 0.0 })
837 .collect();
838 let threshold_y: Vec<f64> = thresh_y_sum
839 .iter()
840 .zip(thresh_y_cnt.iter())
841 .map(|(&s, &c)| if c > 0 { s / c as f64 } else { 0.0 })
842 .collect();
843
844 let cy_row = ((centroid_y + 0.5).max(0.0) as usize).min(y_size - 1);
850 let cx_col = ((centroid_x + 0.5).max(0.0) as usize).min(x_size - 1);
851 let cur_y = cursor_y.min(y_size - 1);
852 let cur_x = cursor_x.min(x_size - 1);
853
854 let centroid_x_profile: Vec<f64> = (0..x_size)
855 .map(|ix| data.get_as_f64(cy_row * x_size + ix).unwrap_or(0.0))
856 .collect();
857 let centroid_y_profile: Vec<f64> = (0..y_size)
858 .map(|iy| data.get_as_f64(iy * x_size + cx_col).unwrap_or(0.0))
859 .collect();
860 let cursor_x_profile: Vec<f64> = (0..x_size)
861 .map(|ix| data.get_as_f64(cur_y * x_size + ix).unwrap_or(0.0))
862 .collect();
863 let cursor_y_profile: Vec<f64> = (0..y_size)
864 .map(|iy| data.get_as_f64(iy * x_size + cur_x).unwrap_or(0.0))
865 .collect();
866
867 ProfileResult {
868 avg_x,
869 avg_y,
870 threshold_x,
871 threshold_y,
872 centroid_x: centroid_x_profile,
873 centroid_y: centroid_y_profile,
874 cursor_x: cursor_x_profile,
875 cursor_y: cursor_y_profile,
876 }
877}
878
879pub struct StatsProcessor {
881 latest_stats: Arc<Mutex<StatsResult>>,
882 do_compute_statistics: bool,
883 do_compute_centroid: bool,
884 do_compute_histogram: bool,
885 do_compute_profiles: bool,
886 bgd_width: usize,
887 centroid_threshold: f64,
888 cursor_x: usize,
889 cursor_y: usize,
890 hist_size: usize,
891 hist_min: f64,
892 hist_max: f64,
893 params: NDStatsParams,
894 params_out: Arc<Mutex<NDStatsParams>>,
896 ts_sender: Option<crate::time_series::TimeSeriesSender>,
898}
899
900impl StatsProcessor {
901 pub fn new() -> Self {
902 Self {
903 latest_stats: Arc::new(Mutex::new(StatsResult::default())),
904 do_compute_statistics: true,
905 do_compute_centroid: true,
906 do_compute_histogram: false,
907 do_compute_profiles: false,
908 bgd_width: 0,
909 centroid_threshold: 0.0,
910 cursor_x: 0,
911 cursor_y: 0,
912 hist_size: 256,
913 hist_min: 0.0,
914 hist_max: 255.0,
915 params: NDStatsParams::default(),
916 params_out: Arc::new(Mutex::new(NDStatsParams::default())),
917 ts_sender: None,
918 }
919 }
920
921 pub fn stats_handle(&self) -> Arc<Mutex<StatsResult>> {
923 self.latest_stats.clone()
924 }
925
926 pub fn params_handle(&self) -> Arc<Mutex<NDStatsParams>> {
928 self.params_out.clone()
929 }
930
931 pub fn set_ts_sender(&mut self, sender: crate::time_series::TimeSeriesSender) {
933 self.ts_sender = Some(sender);
934 }
935}
936
937impl Default for StatsProcessor {
938 fn default() -> Self {
939 Self::new()
940 }
941}
942
943impl NDPluginProcess for StatsProcessor {
944 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
945 let p = &self.params;
946 let info = array.info();
947
948 let mut result = if self.do_compute_statistics {
949 compute_stats(&array.data, &array.dims, self.bgd_width)
950 } else {
951 StatsResult::default()
952 };
953
954 let mut centroid = CentroidResult::default();
956 if self.do_compute_centroid {
957 if info.color_size <= 1 && array.dims.len() == 2 {
963 centroid = compute_centroid(
964 &array.data,
965 info.x_size,
966 info.y_size,
967 self.centroid_threshold,
968 );
969 }
970 }
971
972 if self.do_compute_histogram {
974 let (histogram, below, above, entropy) =
975 compute_histogram(&array.data, self.hist_size, self.hist_min, self.hist_max);
976 result.histogram = histogram;
977 result.hist_below = below;
978 result.hist_above = above;
979 result.hist_entropy = entropy;
980 }
981
982 if self.do_compute_profiles && info.color_size <= 1 && array.dims.len() == 2 {
985 let profiles = compute_profiles(
986 &array.data,
987 info.x_size,
988 info.y_size,
989 self.centroid_threshold,
990 centroid.centroid_x,
991 centroid.centroid_y,
992 self.cursor_x,
993 self.cursor_y,
994 );
995 result.profile_avg_x = profiles.avg_x;
996 result.profile_avg_y = profiles.avg_y;
997 result.profile_threshold_x = profiles.threshold_x;
998 result.profile_threshold_y = profiles.threshold_y;
999 result.profile_centroid_x = profiles.centroid_x;
1000 result.profile_centroid_y = profiles.centroid_y;
1001 result.profile_cursor_x = profiles.cursor_x;
1002 result.profile_cursor_y = profiles.cursor_y;
1003 }
1004
1005 if info.color_size <= 1 && array.dims.len() == 2 && info.x_size > 0 && info.y_size > 0 {
1009 let cx = self.cursor_x.min(info.x_size - 1);
1010 let cy = self.cursor_y.min(info.y_size - 1);
1011 result.cursor_value = array.data.get_as_f64(cy * info.x_size + cx).unwrap_or(0.0);
1012 }
1013
1014 let mut updates = vec![
1015 ParamUpdate::float64(p.min_value, result.min),
1016 ParamUpdate::float64(p.max_value, result.max),
1017 ParamUpdate::float64(p.mean_value, result.mean),
1018 ParamUpdate::float64(p.sigma_value, result.sigma),
1019 ParamUpdate::float64(p.total, result.total),
1020 ParamUpdate::float64(p.net, result.net),
1021 ParamUpdate::float64(p.min_x, result.min_x as f64),
1022 ParamUpdate::float64(p.min_y, result.min_y as f64),
1023 ParamUpdate::float64(p.max_x, result.max_x as f64),
1024 ParamUpdate::float64(p.max_y, result.max_y as f64),
1025 ParamUpdate::float64(p.centroid_x, centroid.centroid_x),
1026 ParamUpdate::float64(p.centroid_y, centroid.centroid_y),
1027 ParamUpdate::float64(p.sigma_x, centroid.sigma_x),
1028 ParamUpdate::float64(p.sigma_y, centroid.sigma_y),
1029 ParamUpdate::float64(p.sigma_xy, centroid.sigma_xy),
1030 ParamUpdate::float64(p.centroid_total, centroid.centroid_total),
1031 ParamUpdate::float64(p.skewness_x, centroid.skewness_x),
1032 ParamUpdate::float64(p.skewness_y, centroid.skewness_y),
1033 ParamUpdate::float64(p.kurtosis_x, centroid.kurtosis_x),
1034 ParamUpdate::float64(p.kurtosis_y, centroid.kurtosis_y),
1035 ParamUpdate::float64(p.eccentricity, centroid.eccentricity),
1036 ParamUpdate::float64(p.orientation, centroid.orientation),
1037 ParamUpdate::int32(p.hist_below, result.hist_below as i32),
1038 ParamUpdate::int32(p.hist_above, result.hist_above as i32),
1039 ParamUpdate::float64(p.hist_entropy, result.hist_entropy),
1040 ParamUpdate::float64(p.cursor_val, result.cursor_value),
1041 ParamUpdate::int32(p.profile_size_x, info.x_size as i32),
1042 ParamUpdate::int32(p.profile_size_y, info.y_size as i32),
1043 ];
1044
1045 if self.do_compute_histogram && !result.histogram.is_empty() {
1052 updates.push(ParamUpdate::float64_array(
1053 p.hist_array,
1054 result.histogram.clone(),
1055 ));
1056 let n = result.histogram.len();
1057 let step = (self.hist_max - self.hist_min) / n as f64;
1058 let hist_x: Vec<f64> = (0..n).map(|i| self.hist_min + i as f64 * step).collect();
1059 updates.push(ParamUpdate::float64_array(p.hist_x_array, hist_x));
1060 }
1061
1062 if self.do_compute_profiles && !result.profile_avg_x.is_empty() {
1065 updates.push(ParamUpdate::float64_array(
1066 p.profile_average_x,
1067 result.profile_avg_x.clone(),
1068 ));
1069 updates.push(ParamUpdate::float64_array(
1070 p.profile_average_y,
1071 result.profile_avg_y.clone(),
1072 ));
1073 updates.push(ParamUpdate::float64_array(
1074 p.profile_threshold_x,
1075 result.profile_threshold_x.clone(),
1076 ));
1077 updates.push(ParamUpdate::float64_array(
1078 p.profile_threshold_y,
1079 result.profile_threshold_y.clone(),
1080 ));
1081 updates.push(ParamUpdate::float64_array(
1082 p.profile_centroid_x,
1083 result.profile_centroid_x.clone(),
1084 ));
1085 updates.push(ParamUpdate::float64_array(
1086 p.profile_centroid_y,
1087 result.profile_centroid_y.clone(),
1088 ));
1089 updates.push(ParamUpdate::float64_array(
1090 p.profile_cursor_x,
1091 result.profile_cursor_x.clone(),
1092 ));
1093 updates.push(ParamUpdate::float64_array(
1094 p.profile_cursor_y,
1095 result.profile_cursor_y.clone(),
1096 ));
1097 }
1098
1099 if let Some(ref sender) = self.ts_sender {
1101 let ts_data = crate::time_series::TimeSeriesData {
1102 values: vec![
1103 result.min,
1104 result.min_x as f64,
1105 result.min_y as f64,
1106 result.max,
1107 result.max_x as f64,
1108 result.max_y as f64,
1109 result.mean,
1110 result.sigma,
1111 result.total,
1112 result.net,
1113 centroid.centroid_total,
1114 centroid.centroid_x,
1115 centroid.centroid_y,
1116 centroid.sigma_x,
1117 centroid.sigma_y,
1118 centroid.sigma_xy,
1119 centroid.skewness_x,
1120 centroid.skewness_y,
1121 centroid.kurtosis_x,
1122 centroid.kurtosis_y,
1123 centroid.eccentricity,
1124 centroid.orientation,
1125 array.timestamp.as_f64(),
1126 ],
1127 };
1128 let _ = sender.try_send(ts_data);
1129 }
1130
1131 *self.latest_stats.lock() = result;
1132 ProcessResult {
1134 output_arrays: vec![Arc::new(array.clone())],
1135 param_updates: updates,
1136 scatter: false,
1137 }
1138 }
1139
1140 fn plugin_type(&self) -> &str {
1141 "NDPluginStats"
1142 }
1143
1144 fn register_params(
1145 &mut self,
1146 base: &mut PortDriverBase,
1147 ) -> Result<(), asyn_rs::error::AsynError> {
1148 self.params.compute_statistics =
1149 base.create_param("COMPUTE_STATISTICS", ParamType::Int32)?;
1150 base.set_int32_param(self.params.compute_statistics, 0, 1)?;
1151
1152 self.params.bgd_width = base.create_param("BGD_WIDTH", ParamType::Int32)?;
1153 self.params.min_value = base.create_param("MIN_VALUE", ParamType::Float64)?;
1154 self.params.max_value = base.create_param("MAX_VALUE", ParamType::Float64)?;
1155 self.params.mean_value = base.create_param("MEAN_VALUE", ParamType::Float64)?;
1156 self.params.sigma_value = base.create_param("SIGMA_VALUE", ParamType::Float64)?;
1157 self.params.total = base.create_param("TOTAL", ParamType::Float64)?;
1158 self.params.net = base.create_param("NET", ParamType::Float64)?;
1159 self.params.min_x = base.create_param("MIN_X", ParamType::Float64)?;
1160 self.params.min_y = base.create_param("MIN_Y", ParamType::Float64)?;
1161 self.params.max_x = base.create_param("MAX_X", ParamType::Float64)?;
1162 self.params.max_y = base.create_param("MAX_Y", ParamType::Float64)?;
1163
1164 self.params.compute_centroid = base.create_param("COMPUTE_CENTROID", ParamType::Int32)?;
1165 base.set_int32_param(self.params.compute_centroid, 0, 1)?;
1166
1167 self.params.centroid_threshold =
1168 base.create_param("CENTROID_THRESHOLD", ParamType::Float64)?;
1169 self.params.centroid_total = base.create_param("CENTROID_TOTAL", ParamType::Float64)?;
1170 self.params.centroid_x = base.create_param("CENTROIDX_VALUE", ParamType::Float64)?;
1171 self.params.centroid_y = base.create_param("CENTROIDY_VALUE", ParamType::Float64)?;
1172 self.params.sigma_x = base.create_param("SIGMAX_VALUE", ParamType::Float64)?;
1173 self.params.sigma_y = base.create_param("SIGMAY_VALUE", ParamType::Float64)?;
1174 self.params.sigma_xy = base.create_param("SIGMAXY_VALUE", ParamType::Float64)?;
1175 self.params.skewness_x = base.create_param("SKEWNESSX_VALUE", ParamType::Float64)?;
1176 self.params.skewness_y = base.create_param("SKEWNESSY_VALUE", ParamType::Float64)?;
1177 self.params.kurtosis_x = base.create_param("KURTOSISX_VALUE", ParamType::Float64)?;
1178 self.params.kurtosis_y = base.create_param("KURTOSISY_VALUE", ParamType::Float64)?;
1179 self.params.eccentricity = base.create_param("ECCENTRICITY_VALUE", ParamType::Float64)?;
1180 self.params.orientation = base.create_param("ORIENTATION_VALUE", ParamType::Float64)?;
1181
1182 self.params.compute_histogram = base.create_param("COMPUTE_HISTOGRAM", ParamType::Int32)?;
1183 self.params.hist_size = base.create_param("HIST_SIZE", ParamType::Int32)?;
1184 base.set_int32_param(self.params.hist_size, 0, 256)?;
1185 self.params.hist_min = base.create_param("HIST_MIN", ParamType::Float64)?;
1186 self.params.hist_max = base.create_param("HIST_MAX", ParamType::Float64)?;
1187 base.set_float64_param(self.params.hist_max, 0, 255.0)?;
1188 self.params.hist_below = base.create_param("HIST_BELOW", ParamType::Int32)?;
1193 self.params.hist_above = base.create_param("HIST_ABOVE", ParamType::Int32)?;
1194 self.params.hist_entropy = base.create_param("HIST_ENTROPY", ParamType::Float64)?;
1195
1196 self.params.compute_profiles = base.create_param("COMPUTE_PROFILES", ParamType::Int32)?;
1197 self.params.cursor_x = base.create_param("CURSOR_X", ParamType::Int32)?;
1198 base.set_int32_param(self.params.cursor_x, 0, 0)?;
1199 self.params.cursor_y = base.create_param("CURSOR_Y", ParamType::Int32)?;
1200 base.set_int32_param(self.params.cursor_y, 0, 0)?;
1201
1202 self.params.cursor_val = base.create_param("CURSOR_VAL", ParamType::Float64)?;
1203 self.params.profile_size_x = base.create_param("PROFILE_SIZE_X", ParamType::Int32)?;
1204 self.params.profile_size_y = base.create_param("PROFILE_SIZE_Y", ParamType::Int32)?;
1205
1206 self.params.skewx_value = base.create_param("SKEWX_VALUE", ParamType::Float64)?;
1207 self.params.skewy_value = base.create_param("SKEWY_VALUE", ParamType::Float64)?;
1208 self.params.profile_average_x =
1209 base.create_param("PROFILE_AVERAGE_X", ParamType::Float64Array)?;
1210 self.params.profile_average_y =
1211 base.create_param("PROFILE_AVERAGE_Y", ParamType::Float64Array)?;
1212 self.params.profile_threshold_x =
1213 base.create_param("PROFILE_THRESHOLD_X", ParamType::Float64Array)?;
1214 self.params.profile_threshold_y =
1215 base.create_param("PROFILE_THRESHOLD_Y", ParamType::Float64Array)?;
1216 self.params.profile_centroid_x =
1217 base.create_param("PROFILE_CENTROID_X", ParamType::Float64Array)?;
1218 self.params.profile_centroid_y =
1219 base.create_param("PROFILE_CENTROID_Y", ParamType::Float64Array)?;
1220 self.params.profile_cursor_x =
1221 base.create_param("PROFILE_CURSOR_X", ParamType::Float64Array)?;
1222 self.params.profile_cursor_y =
1223 base.create_param("PROFILE_CURSOR_Y", ParamType::Float64Array)?;
1224 self.params.hist_array = base.create_param("HIST_ARRAY", ParamType::Float64Array)?;
1225 self.params.hist_x_array = base.create_param("HIST_X_ARRAY", ParamType::Float64Array)?;
1226
1227 *self.params_out.lock() = self.params;
1229
1230 Ok(())
1231 }
1232
1233 fn on_param_change(
1234 &mut self,
1235 reason: usize,
1236 snapshot: &PluginParamSnapshot,
1237 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
1238 let p = &self.params;
1239 if reason == p.compute_statistics {
1240 self.do_compute_statistics = snapshot.value.as_i32() != 0;
1241 } else if reason == p.compute_centroid {
1242 self.do_compute_centroid = snapshot.value.as_i32() != 0;
1243 } else if reason == p.compute_histogram {
1244 self.do_compute_histogram = snapshot.value.as_i32() != 0;
1245 } else if reason == p.compute_profiles {
1246 self.do_compute_profiles = snapshot.value.as_i32() != 0;
1247 } else if reason == p.bgd_width {
1248 self.bgd_width = snapshot.value.as_i32().max(0) as usize;
1249 } else if reason == p.centroid_threshold {
1250 self.centroid_threshold = snapshot.value.as_f64();
1251 } else if reason == p.cursor_x {
1252 self.cursor_x = snapshot.value.as_i32().max(0) as usize;
1253 } else if reason == p.cursor_y {
1254 self.cursor_y = snapshot.value.as_i32().max(0) as usize;
1255 } else if reason == p.hist_size {
1256 self.hist_size = (snapshot.value.as_i32().max(1)) as usize;
1257 } else if reason == p.hist_min {
1258 self.hist_min = snapshot.value.as_f64();
1259 } else if reason == p.hist_max {
1260 self.hist_max = snapshot.value.as_f64();
1261 }
1262 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
1263 }
1264}
1265
1266pub fn create_stats_runtime(
1272 port_name: &str,
1273 pool: Arc<NDArrayPool>,
1274 queue_size: usize,
1275 ndarray_port: &str,
1276 wiring: Arc<WiringRegistry>,
1277 ts_registry: &crate::time_series::TsReceiverRegistry,
1278) -> (
1279 PluginRuntimeHandle,
1280 Arc<Mutex<StatsResult>>,
1281 NDStatsParams,
1282 std::thread::JoinHandle<()>,
1283) {
1284 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
1285
1286 let mut processor = StatsProcessor::new();
1287 processor.set_ts_sender(ts_tx);
1288 let stats_handle = processor.stats_handle();
1289 let params_handle = processor.params_handle();
1290
1291 let (plugin_handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
1292 port_name,
1293 processor,
1294 pool,
1295 queue_size,
1296 ndarray_port,
1297 wiring,
1298 );
1299
1300 let stats_params = *params_handle.lock();
1301
1302 let channel_names: Vec<String> = crate::time_series::STATS_TS_CHANNEL_NAMES
1304 .iter()
1305 .map(|s| s.to_string())
1306 .collect();
1307 ts_registry.store(port_name, ts_rx, channel_names);
1308
1309 (plugin_handle, stats_handle, stats_params, data_jh)
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314 use super::*;
1315 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1316
1317 #[test]
1318 fn test_compute_stats_u8() {
1319 let dims = vec![NDDimension::new(5)];
1320 let data = NDDataBuffer::U8(vec![10, 20, 30, 40, 50]);
1321 let stats = compute_stats(&data, &dims, 0);
1322 assert_eq!(stats.min, 10.0);
1323 assert_eq!(stats.max, 50.0);
1324 assert_eq!(stats.mean, 30.0);
1325 assert_eq!(stats.total, 150.0);
1326 assert_eq!(stats.num_elements, 5);
1327 }
1328
1329 #[test]
1330 fn test_compute_stats_sigma() {
1331 let dims = vec![NDDimension::new(8)];
1332 let data = NDDataBuffer::F64(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
1333 let stats = compute_stats(&data, &dims, 0);
1334 assert!((stats.mean - 5.0).abs() < 1e-10);
1335 assert!((stats.sigma - 2.0).abs() < 1e-10);
1336 }
1337
1338 #[test]
1339 fn test_compute_stats_u16() {
1340 let dims = vec![NDDimension::new(3)];
1341 let data = NDDataBuffer::U16(vec![100, 200, 300]);
1342 let stats = compute_stats(&data, &dims, 0);
1343 assert_eq!(stats.min, 100.0);
1344 assert_eq!(stats.max, 300.0);
1345 assert_eq!(stats.mean, 200.0);
1346 }
1347
1348 #[test]
1349 fn test_compute_stats_f64() {
1350 let dims = vec![NDDimension::new(3)];
1351 let data = NDDataBuffer::F64(vec![1.5, 2.5, 3.5]);
1352 let stats = compute_stats(&data, &dims, 0);
1353 assert!((stats.min - 1.5).abs() < 1e-10);
1354 assert!((stats.max - 3.5).abs() < 1e-10);
1355 assert!((stats.mean - 2.5).abs() < 1e-10);
1356 }
1357
1358 #[test]
1359 fn test_compute_stats_single_element() {
1360 let dims = vec![NDDimension::new(1)];
1361 let data = NDDataBuffer::I32(vec![42]);
1362 let stats = compute_stats(&data, &dims, 0);
1363 assert_eq!(stats.min, 42.0);
1364 assert_eq!(stats.max, 42.0);
1365 assert_eq!(stats.mean, 42.0);
1366 assert_eq!(stats.sigma, 0.0);
1367 assert_eq!(stats.num_elements, 1);
1368 }
1369
1370 #[test]
1371 fn test_compute_stats_empty() {
1372 let data = NDDataBuffer::U8(vec![]);
1373 let stats = compute_stats(&data, &[], 0);
1374 assert_eq!(stats.num_elements, 0);
1375 }
1376
1377 #[test]
1378 fn test_compute_stats_min_max_position() {
1379 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1380 let data = NDDataBuffer::U8((1..=16).collect());
1382 let stats = compute_stats(&data, &dims, 0);
1383 assert_eq!(stats.min_x, 0); assert_eq!(stats.min_y, 0);
1385 assert_eq!(stats.max_x, 3); assert_eq!(stats.max_y, 3);
1387 }
1388
1389 #[test]
1390 fn test_compute_stats_net_no_bgd() {
1391 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1392 let data = NDDataBuffer::U8((1..=16).collect());
1393 let stats = compute_stats(&data, &dims, 0);
1394 assert_eq!(stats.net, stats.total);
1396 }
1397
1398 #[test]
1399 fn test_compute_stats_bgd_subtraction() {
1400 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1402 let mut pixels = vec![10u16; 16];
1403 pixels[2 * 4 + 2] = 110;
1405 let data = NDDataBuffer::U16(pixels);
1406 let stats = compute_stats(&data, &dims, 1);
1407
1408 assert!((stats.net - 100.0).abs() < 1e-10);
1417 }
1418
1419 #[test]
1429 fn test_compute_stats_bgd_2d_corner_double_count() {
1430 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1441 let mut pixels = vec![1u16; 16];
1442 for &i in &[1usize, 2, 4, 7, 8, 11, 13, 14] {
1443 pixels[i] = 10;
1444 }
1445 for &i in &[0usize, 3, 12, 15] {
1446 pixels[i] = 100;
1447 }
1448 let total_expected: f64 = pixels.iter().map(|&p| p as f64).sum();
1449 let data = NDDataBuffer::U16(pixels);
1450 let stats = compute_stats(&data, &dims, 1);
1451
1452 let bgd_counts = 220 + 220 + 220 + 220; let bgd_pixels = 16; let bgd_avg = bgd_counts as f64 / bgd_pixels as f64; let expected_net = total_expected - bgd_avg * 16.0;
1462 assert!(
1463 (stats.net - expected_net).abs() < 1e-9,
1464 "net {} != expected {}",
1465 stats.net,
1466 expected_net
1467 );
1468
1469 let perimeter_avg = (4.0 * 100.0 + 8.0 * 10.0) / 12.0;
1473 assert!(
1474 (bgd_avg - perimeter_avg).abs() > 1e-9,
1475 "corner double-count must change bgd_avg vs a once-each perimeter"
1476 );
1477 assert!((bgd_avg - 55.0).abs() < 1e-9);
1478 }
1479
1480 #[test]
1483 fn test_compute_stats_bgd_1d() {
1484 let dims = vec![NDDimension::new(8)];
1486 let data = NDDataBuffer::U16(vec![10, 20, 30, 40, 50, 60, 70, 80]);
1487 let stats = compute_stats(&data, &dims, 2);
1488
1489 let bgd_counts = 10 + 20 + 70 + 80;
1493 let bgd_pixels = 4;
1494 let bgd_avg = bgd_counts as f64 / bgd_pixels as f64; let total = (10 + 20 + 30 + 40 + 50 + 60 + 70 + 80) as f64;
1496 let expected_net = total - bgd_avg * 8.0;
1497 assert!(
1498 (stats.net - expected_net).abs() < 1e-9,
1499 "1-D net {} != expected {}",
1500 stats.net,
1501 expected_net
1502 );
1503 }
1504
1505 #[test]
1507 fn test_compute_stats_bgd_3d() {
1508 let dims = vec![
1513 NDDimension::new(2),
1514 NDDimension::new(2),
1515 NDDimension::new(2),
1516 ];
1517 let data = NDDataBuffer::U8(vec![1u8; 8]);
1518 let stats = compute_stats(&data, &dims, 1);
1519 assert!(
1520 stats.net.abs() < 1e-9,
1521 "3-D uniform net should be 0, got {}",
1522 stats.net
1523 );
1524 assert_eq!(stats.total, 8.0);
1525 }
1526
1527 #[test]
1528 fn test_centroid_uniform() {
1529 let data = NDDataBuffer::U8(vec![1; 16]);
1530 let c = compute_centroid(&data, 4, 4, 0.0);
1531 assert!((c.centroid_x - 1.5).abs() < 1e-10);
1532 assert!((c.centroid_y - 1.5).abs() < 1e-10);
1533 }
1534
1535 #[test]
1536 fn test_centroid_corner() {
1537 let mut d = vec![0u8; 16];
1538 d[0] = 255;
1539 let data = NDDataBuffer::U8(d);
1540 let c = compute_centroid(&data, 4, 4, 0.0);
1541 assert!((c.centroid_x - 0.0).abs() < 1e-10);
1542 assert!((c.centroid_y - 0.0).abs() < 1e-10);
1543 }
1544
1545 #[test]
1546 fn test_centroid_threshold() {
1547 let mut pixels = vec![5u8; 16];
1549 pixels[2 * 4 + 2] = 100;
1550 let data = NDDataBuffer::U8(pixels);
1551
1552 let c = compute_centroid(&data, 4, 4, 50.0);
1554 assert!((c.centroid_x - 2.0).abs() < 1e-10);
1555 assert!((c.centroid_y - 2.0).abs() < 1e-10);
1556 assert!((c.centroid_total - 100.0).abs() < 1e-10);
1557 }
1558
1559 #[test]
1560 fn test_centroid_higher_moments_symmetric() {
1561 let data = NDDataBuffer::U8(vec![1; 16]);
1563 let c = compute_centroid(&data, 4, 4, 0.0);
1564 assert!(c.skewness_x.abs() < 1e-10);
1566 assert!(c.skewness_y.abs() < 1e-10);
1567 assert!(c.eccentricity.abs() < 1e-10);
1569 }
1570
1571 #[test]
1572 fn test_histogram_basic() {
1573 let data = NDDataBuffer::F64((0..10).map(|x| x as f64).collect());
1575 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 9.0);
1576 assert_eq!(hist.len(), 10);
1577 assert_eq!(below, 0.0);
1578 assert_eq!(above, 0.0);
1579 let total: f64 = hist.iter().sum();
1581 assert!((total - 10.0).abs() < 1e-10);
1582 assert!(entropy.abs() < 1e-10);
1585 }
1586
1587 #[test]
1588 fn test_histogram_below_above() {
1589 let data = NDDataBuffer::F64(vec![-1.0, 0.5, 1.5, 3.0]);
1590 let (hist, below, above, _entropy) = compute_histogram(&data, 2, 0.0, 2.0);
1591 assert_eq!(below, 1.0); assert_eq!(above, 1.0); let total_in_bins: f64 = hist.iter().sum();
1594 assert!((total_in_bins - 2.0).abs() < 1e-10); }
1596
1597 #[test]
1598 fn test_histogram_single_value() {
1599 let data = NDDataBuffer::F64(vec![5.0; 100]);
1600 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 10.0);
1601 assert_eq!(below, 0.0);
1602 assert_eq!(above, 0.0);
1603 let expected = -(100.0f64.ln());
1607 assert!((entropy - expected).abs() < 1e-10);
1608 let total: f64 = hist.iter().sum();
1609 assert!((total - 100.0).abs() < 1e-10);
1610 }
1611
1612 #[test]
1613 fn test_profiles_8x8() {
1614 let mut pixels = vec![0.0f64; 64];
1616 for iy in 0..8 {
1617 for ix in 0..8 {
1618 pixels[iy * 8 + ix] = iy as f64;
1619 }
1620 }
1621 let data = NDDataBuffer::F64(pixels);
1622
1623 let profiles = compute_profiles(
1624 &data, 8, 8, 0.0, 3.5, 3.5, 0, 7, );
1630
1631 assert_eq!(profiles.avg_x.len(), 8);
1633 for &v in &profiles.avg_x {
1634 assert!((v - 3.5).abs() < 1e-10, "avg_x should be 3.5, got {v}");
1635 }
1636
1637 assert_eq!(profiles.avg_y.len(), 8);
1639 for (iy, &v) in profiles.avg_y.iter().enumerate() {
1640 assert!(
1641 (v - iy as f64).abs() < 1e-10,
1642 "avg_y[{iy}] should be {iy}, got {v}"
1643 );
1644 }
1645
1646 assert_eq!(profiles.cursor_x.len(), 8);
1648 for &v in &profiles.cursor_x {
1649 assert!((v - 7.0).abs() < 1e-10);
1650 }
1651
1652 assert_eq!(profiles.cursor_y.len(), 8);
1654 for (iy, &v) in profiles.cursor_y.iter().enumerate() {
1655 assert!((v - iy as f64).abs() < 1e-10);
1656 }
1657
1658 assert_eq!(profiles.centroid_x.len(), 8);
1660 for &v in &profiles.centroid_x {
1661 assert!((v - 4.0).abs() < 1e-10);
1662 }
1663
1664 assert_eq!(profiles.centroid_y.len(), 8);
1666 for (iy, &v) in profiles.centroid_y.iter().enumerate() {
1667 assert!((v - iy as f64).abs() < 1e-10);
1668 }
1669 }
1670
1671 #[test]
1672 fn test_adp14_out_of_range_cursor_clamps_to_edge_not_zeros() {
1673 let mut pixels = vec![0.0f64; 64];
1677 for iy in 0..8 {
1678 for ix in 0..8 {
1679 pixels[iy * 8 + ix] = iy as f64;
1680 }
1681 }
1682 let data = NDDataBuffer::F64(pixels);
1683
1684 let profiles = compute_profiles(
1685 &data, 8, 8, 0.0, 100.0, 100.0, 50, 50, );
1691
1692 assert_eq!(profiles.cursor_x.len(), 8);
1694 for &v in &profiles.cursor_x {
1695 assert!((v - 7.0).abs() < 1e-10, "cursor_x should clamp to row 7");
1696 }
1697 for (iy, &v) in profiles.cursor_y.iter().enumerate() {
1699 assert!(
1700 (v - iy as f64).abs() < 1e-10,
1701 "cursor_y should clamp to col 7"
1702 );
1703 }
1704 for &v in &profiles.centroid_x {
1706 assert!((v - 7.0).abs() < 1e-10, "centroid_x should clamp to row 7");
1707 }
1708 for (iy, &v) in profiles.centroid_y.iter().enumerate() {
1710 assert!(
1711 (v - iy as f64).abs() < 1e-10,
1712 "centroid_y should clamp to col 7"
1713 );
1714 }
1715 }
1716
1717 #[test]
1718 fn test_profiles_threshold() {
1719 let mut pixels = vec![1.0f64; 16];
1721 pixels[1 * 4 + 2] = 10.0;
1722 let data = NDDataBuffer::F64(pixels);
1723
1724 let profiles = compute_profiles(
1725 &data, 4, 4, 5.0, 2.0, 1.0, 0, 0,
1727 );
1728
1729 assert_eq!(profiles.threshold_x.len(), 4);
1731 assert!((profiles.threshold_x[2] - 10.0).abs() < 1e-10);
1732 assert!((profiles.threshold_x[0] - 0.0).abs() < 1e-10);
1734 assert!((profiles.threshold_x[1] - 0.0).abs() < 1e-10);
1735 assert!((profiles.threshold_x[3] - 0.0).abs() < 1e-10);
1736
1737 assert_eq!(profiles.threshold_y.len(), 4);
1739 assert!((profiles.threshold_y[1] - 10.0).abs() < 1e-10);
1740 assert!((profiles.threshold_y[0] - 0.0).abs() < 1e-10);
1741 }
1742
1743 #[test]
1744 fn test_stats_processor_direct() {
1745 let mut proc = StatsProcessor::new();
1746 let pool = NDArrayPool::new(1_000_000);
1747
1748 let mut arr = NDArray::new(vec![NDDimension::new(5)], NDDataType::UInt8);
1749 if let NDDataBuffer::U8(ref mut v) = arr.data {
1750 v[0] = 10;
1751 v[1] = 20;
1752 v[2] = 30;
1753 v[3] = 40;
1754 v[4] = 50;
1755 }
1756
1757 let result = proc.process_array(&arr, &pool);
1758 assert_eq!(result.output_arrays.len(), 1, "stats forwards the array");
1760
1761 let stats = proc.stats_handle().lock().clone();
1762 assert_eq!(stats.min, 10.0);
1763 assert_eq!(stats.max, 50.0);
1764 assert_eq!(stats.mean, 30.0);
1765 }
1766
1767 #[test]
1768 fn test_stats_emits_histogram_and_profile_arrays() {
1769 use ad_core_rs::plugin::runtime::ParamUpdate;
1770 let mut proc = StatsProcessor::new();
1771 let mut base = asyn_rs::port::PortDriverBase::new(
1773 "_stats_scratch_",
1774 1,
1775 asyn_rs::port::PortFlags::default(),
1776 );
1777 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1778 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1779 proc.register_params(&mut base).unwrap();
1780
1781 proc.do_compute_histogram = true;
1782 proc.do_compute_profiles = true;
1783 proc.hist_size = 8;
1784 proc.hist_min = 0.0;
1785 proc.hist_max = 7.0;
1786 let pool = NDArrayPool::new(1_000_000);
1787
1788 let mut arr = NDArray::new(
1789 vec![NDDimension::new(4), NDDimension::new(4)],
1790 NDDataType::UInt8,
1791 );
1792 if let NDDataBuffer::U8(ref mut v) = arr.data {
1793 for (i, val) in v.iter_mut().enumerate() {
1794 *val = (i % 8) as u8;
1795 }
1796 }
1797
1798 let result = proc.process_array(&arr, &pool);
1799 let p = proc.params;
1800 let array_reasons: Vec<usize> = result
1803 .param_updates
1804 .iter()
1805 .filter_map(|u| match u {
1806 ParamUpdate::Float64Array { reason, value, .. } => {
1807 assert!(!value.is_empty(), "array waveform must not be empty");
1808 Some(*reason)
1809 }
1810 _ => None,
1811 })
1812 .collect();
1813 for reason in [
1814 p.hist_array,
1815 p.hist_x_array,
1816 p.profile_average_x,
1817 p.profile_average_y,
1818 p.profile_threshold_x,
1819 p.profile_threshold_y,
1820 p.profile_centroid_x,
1821 p.profile_centroid_y,
1822 p.profile_cursor_x,
1823 p.profile_cursor_y,
1824 ] {
1825 assert!(
1826 array_reasons.contains(&reason),
1827 "missing array update for reason {reason}"
1828 );
1829 }
1830 }
1831
1832 #[test]
1833 fn test_adp13_ndims_gt_2_skips_centroid_and_profiles() {
1834 use ad_core_rs::plugin::runtime::ParamUpdate;
1835 let mut proc = StatsProcessor::new();
1839 let mut base = asyn_rs::port::PortDriverBase::new(
1840 "_stats_adp13_",
1841 1,
1842 asyn_rs::port::PortFlags::default(),
1843 );
1844 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1845 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1846 proc.register_params(&mut base).unwrap();
1847 proc.do_compute_centroid = true;
1848 proc.do_compute_profiles = true;
1849 let pool = NDArrayPool::new(1_000_000);
1850
1851 let mut arr = NDArray::new(
1854 vec![
1855 NDDimension::new(4),
1856 NDDimension::new(4),
1857 NDDimension::new(2),
1858 NDDimension::new(2),
1859 ],
1860 NDDataType::UInt8,
1861 );
1862 if let NDDataBuffer::U8(ref mut v) = arr.data {
1863 for (i, val) in v.iter_mut().enumerate() {
1864 *val = if i % 4 == 3 { 100 } else { 0 };
1865 }
1866 }
1867
1868 let result = proc.process_array(&arr, &pool);
1869 let p = proc.params;
1870
1871 let centroid_x = result.param_updates.iter().find_map(|u| match u {
1873 ParamUpdate::Float64 { reason, value, .. } if *reason == p.centroid_x => Some(*value),
1874 _ => None,
1875 });
1876 assert_eq!(
1877 centroid_x,
1878 Some(0.0),
1879 "centroid_x must not be computed for ndims>2"
1880 );
1881
1882 let profile_reasons = [
1884 p.profile_average_x,
1885 p.profile_average_y,
1886 p.profile_threshold_x,
1887 p.profile_threshold_y,
1888 p.profile_centroid_x,
1889 p.profile_centroid_y,
1890 p.profile_cursor_x,
1891 p.profile_cursor_y,
1892 ];
1893 for u in &result.param_updates {
1894 if let ParamUpdate::Float64Array { reason, .. } = u {
1895 assert!(
1896 !profile_reasons.contains(reason),
1897 "no profile waveform may be emitted for ndims>2"
1898 );
1899 }
1900 }
1901 }
1902
1903 #[test]
1904 fn test_adp30_hist_below_above_emitted_as_int32() {
1905 use ad_core_rs::plugin::runtime::ParamUpdate;
1906 let mut proc = StatsProcessor::new();
1910 let mut base = asyn_rs::port::PortDriverBase::new(
1911 "_stats_adp30_",
1912 1,
1913 asyn_rs::port::PortFlags::default(),
1914 );
1915 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1916 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1917 proc.register_params(&mut base).unwrap();
1918 proc.do_compute_histogram = true;
1919 proc.hist_size = 4;
1920 proc.hist_min = 2.0;
1921 proc.hist_max = 5.0;
1922 let pool = NDArrayPool::new(1_000_000);
1923
1924 let mut arr = NDArray::new(
1927 vec![NDDimension::new(4), NDDimension::new(2)],
1928 NDDataType::UInt8,
1929 );
1930 if let NDDataBuffer::U8(ref mut v) = arr.data {
1931 v.copy_from_slice(&[0, 1, 3, 3, 9, 9, 9, 4]);
1932 }
1933
1934 let result = proc.process_array(&arr, &pool);
1935 let p = proc.params;
1936
1937 let below = result.param_updates.iter().find_map(|u| match u {
1938 ParamUpdate::Int32 { reason, value, .. } if *reason == p.hist_below => Some(*value),
1939 _ => None,
1940 });
1941 let above = result.param_updates.iter().find_map(|u| match u {
1942 ParamUpdate::Int32 { reason, value, .. } if *reason == p.hist_above => Some(*value),
1943 _ => None,
1944 });
1945 assert_eq!(
1946 below,
1947 Some(2),
1948 "HIST_BELOW must be emitted as an Int32 count"
1949 );
1950 assert_eq!(
1951 above,
1952 Some(3),
1953 "HIST_ABOVE must be emitted as an Int32 count"
1954 );
1955
1956 for u in &result.param_updates {
1958 if let ParamUpdate::Float64 { reason, .. } = u {
1959 assert!(
1960 *reason != p.hist_below && *reason != p.hist_above,
1961 "HIST_BELOW/HIST_ABOVE must not be emitted as Float64"
1962 );
1963 }
1964 }
1965 }
1966
1967 #[test]
1968 fn test_hist_x_array_uses_bin_count_divisor() {
1969 use ad_core_rs::plugin::runtime::ParamUpdate;
1970 let mut proc = StatsProcessor::new();
1974 let mut base = asyn_rs::port::PortDriverBase::new(
1975 "_stats_histx_",
1976 1,
1977 asyn_rs::port::PortFlags::default(),
1978 );
1979 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1980 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1981 proc.register_params(&mut base).unwrap();
1982
1983 proc.do_compute_histogram = true;
1984 proc.hist_size = 256;
1985 proc.hist_min = 0.0;
1986 proc.hist_max = 255.0;
1987 let pool = NDArrayPool::new(1_000_000);
1988
1989 let mut arr = NDArray::new(
1990 vec![NDDimension::new(4), NDDimension::new(4)],
1991 NDDataType::UInt8,
1992 );
1993 if let NDDataBuffer::U8(ref mut v) = arr.data {
1994 for (i, val) in v.iter_mut().enumerate() {
1995 *val = (i * 16) as u8;
1996 }
1997 }
1998
1999 let result = proc.process_array(&arr, &pool);
2000 let hist_x = result
2001 .param_updates
2002 .iter()
2003 .find_map(|u| match u {
2004 ParamUpdate::Float64Array { reason, value, .. }
2005 if *reason == proc.params.hist_x_array =>
2006 {
2007 Some(value.clone())
2008 }
2009 _ => None,
2010 })
2011 .expect("HIST_X_ARRAY must be emitted");
2012
2013 assert_eq!(hist_x.len(), 256, "256 bins");
2014 let scale = 255.0 / 256.0;
2015 assert!(
2016 (hist_x[0] - 0.0).abs() < 1e-9,
2017 "bin 0 X must be histMin (0.0), got {}",
2018 hist_x[0]
2019 );
2020 assert!(
2021 (hist_x[1] - scale).abs() < 1e-9,
2022 "bin 1 X must be {scale}, got {}",
2023 hist_x[1]
2024 );
2025 assert!(
2026 (hist_x[255] - 255.0 * scale).abs() < 1e-9,
2027 "last bin X must be ~254.004 (255*255/256), got {}",
2028 hist_x[255]
2029 );
2030 assert!(
2031 hist_x[255] < 255.0,
2032 "last bin X must be strictly below histMax, got {}",
2033 hist_x[255]
2034 );
2035 }
2036
2037 #[test]
2038 fn test_stats_runtime_end_to_end() {
2039 let pool = Arc::new(NDArrayPool::new(1_000_000));
2040 let wiring = Arc::new(WiringRegistry::new());
2041 let ts_registry = crate::time_series::TsReceiverRegistry::new();
2042 let (handle, stats, _params, _jh) =
2043 create_stats_runtime("STATS_RT", pool, 10, "", wiring, &ts_registry);
2044
2045 handle
2047 .port_runtime()
2048 .port_handle()
2049 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2050 .unwrap();
2051 std::thread::sleep(std::time::Duration::from_millis(10));
2052
2053 let mut arr = NDArray::new(
2054 vec![NDDimension::new(4), NDDimension::new(4)],
2055 NDDataType::UInt8,
2056 );
2057 if let NDDataBuffer::U8(ref mut v) = arr.data {
2058 for (i, val) in v.iter_mut().enumerate() {
2059 *val = (i + 1) as u8;
2060 }
2061 }
2062
2063 let rt = tokio::runtime::Builder::new_current_thread()
2064 .enable_all()
2065 .build()
2066 .unwrap();
2067 rt.block_on(handle.array_sender().publish(Arc::new(arr)));
2068 std::thread::sleep(std::time::Duration::from_millis(100));
2069
2070 let result = stats.lock().clone();
2071 assert_eq!(result.min, 1.0);
2072 assert_eq!(result.max, 16.0);
2073 assert_eq!(result.num_elements, 16);
2074 }
2075}