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) as usize;
846 let cx_col = (centroid_x + 0.5) as usize;
847
848 let centroid_x_profile = if cy_row < y_size {
849 (0..x_size)
850 .map(|ix| data.get_as_f64(cy_row * x_size + ix).unwrap_or(0.0))
851 .collect()
852 } else {
853 vec![0.0; x_size]
854 };
855
856 let centroid_y_profile = if cx_col < x_size {
857 (0..y_size)
858 .map(|iy| data.get_as_f64(iy * x_size + cx_col).unwrap_or(0.0))
859 .collect()
860 } else {
861 vec![0.0; y_size]
862 };
863
864 let cursor_x_profile = if cursor_y < y_size {
866 (0..x_size)
867 .map(|ix| data.get_as_f64(cursor_y * x_size + ix).unwrap_or(0.0))
868 .collect()
869 } else {
870 vec![0.0; x_size]
871 };
872
873 let cursor_y_profile = if cursor_x < x_size {
874 (0..y_size)
875 .map(|iy| data.get_as_f64(iy * x_size + cursor_x).unwrap_or(0.0))
876 .collect()
877 } else {
878 vec![0.0; y_size]
879 };
880
881 ProfileResult {
882 avg_x,
883 avg_y,
884 threshold_x,
885 threshold_y,
886 centroid_x: centroid_x_profile,
887 centroid_y: centroid_y_profile,
888 cursor_x: cursor_x_profile,
889 cursor_y: cursor_y_profile,
890 }
891}
892
893pub struct StatsProcessor {
895 latest_stats: Arc<Mutex<StatsResult>>,
896 do_compute_statistics: bool,
897 do_compute_centroid: bool,
898 do_compute_histogram: bool,
899 do_compute_profiles: bool,
900 bgd_width: usize,
901 centroid_threshold: f64,
902 cursor_x: usize,
903 cursor_y: usize,
904 hist_size: usize,
905 hist_min: f64,
906 hist_max: f64,
907 params: NDStatsParams,
908 params_out: Arc<Mutex<NDStatsParams>>,
910 ts_sender: Option<crate::time_series::TimeSeriesSender>,
912}
913
914impl StatsProcessor {
915 pub fn new() -> Self {
916 Self {
917 latest_stats: Arc::new(Mutex::new(StatsResult::default())),
918 do_compute_statistics: true,
919 do_compute_centroid: true,
920 do_compute_histogram: false,
921 do_compute_profiles: false,
922 bgd_width: 0,
923 centroid_threshold: 0.0,
924 cursor_x: 0,
925 cursor_y: 0,
926 hist_size: 256,
927 hist_min: 0.0,
928 hist_max: 255.0,
929 params: NDStatsParams::default(),
930 params_out: Arc::new(Mutex::new(NDStatsParams::default())),
931 ts_sender: None,
932 }
933 }
934
935 pub fn stats_handle(&self) -> Arc<Mutex<StatsResult>> {
937 self.latest_stats.clone()
938 }
939
940 pub fn params_handle(&self) -> Arc<Mutex<NDStatsParams>> {
942 self.params_out.clone()
943 }
944
945 pub fn set_ts_sender(&mut self, sender: crate::time_series::TimeSeriesSender) {
947 self.ts_sender = Some(sender);
948 }
949}
950
951impl Default for StatsProcessor {
952 fn default() -> Self {
953 Self::new()
954 }
955}
956
957impl NDPluginProcess for StatsProcessor {
958 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
959 let p = &self.params;
960 let info = array.info();
961
962 let mut result = if self.do_compute_statistics {
963 compute_stats(&array.data, &array.dims, self.bgd_width)
964 } else {
965 StatsResult::default()
966 };
967
968 let mut centroid = CentroidResult::default();
970 if self.do_compute_centroid {
971 if info.color_size <= 1 && array.dims.len() >= 2 {
972 centroid = compute_centroid(
973 &array.data,
974 info.x_size,
975 info.y_size,
976 self.centroid_threshold,
977 );
978 }
979 }
980
981 if self.do_compute_histogram {
983 let (histogram, below, above, entropy) =
984 compute_histogram(&array.data, self.hist_size, self.hist_min, self.hist_max);
985 result.histogram = histogram;
986 result.hist_below = below;
987 result.hist_above = above;
988 result.hist_entropy = entropy;
989 }
990
991 if self.do_compute_profiles && info.color_size <= 1 && array.dims.len() >= 2 {
993 let profiles = compute_profiles(
994 &array.data,
995 info.x_size,
996 info.y_size,
997 self.centroid_threshold,
998 centroid.centroid_x,
999 centroid.centroid_y,
1000 self.cursor_x,
1001 self.cursor_y,
1002 );
1003 result.profile_avg_x = profiles.avg_x;
1004 result.profile_avg_y = profiles.avg_y;
1005 result.profile_threshold_x = profiles.threshold_x;
1006 result.profile_threshold_y = profiles.threshold_y;
1007 result.profile_centroid_x = profiles.centroid_x;
1008 result.profile_centroid_y = profiles.centroid_y;
1009 result.profile_cursor_x = profiles.cursor_x;
1010 result.profile_cursor_y = profiles.cursor_y;
1011 }
1012
1013 if info.color_size <= 1 && array.dims.len() >= 2 {
1015 let cx = self.cursor_x;
1016 let cy = self.cursor_y;
1017 if cx < info.x_size && cy < info.y_size {
1018 result.cursor_value = array.data.get_as_f64(cy * info.x_size + cx).unwrap_or(0.0);
1019 }
1020 }
1021
1022 let mut updates = vec![
1023 ParamUpdate::float64(p.min_value, result.min),
1024 ParamUpdate::float64(p.max_value, result.max),
1025 ParamUpdate::float64(p.mean_value, result.mean),
1026 ParamUpdate::float64(p.sigma_value, result.sigma),
1027 ParamUpdate::float64(p.total, result.total),
1028 ParamUpdate::float64(p.net, result.net),
1029 ParamUpdate::float64(p.min_x, result.min_x as f64),
1030 ParamUpdate::float64(p.min_y, result.min_y as f64),
1031 ParamUpdate::float64(p.max_x, result.max_x as f64),
1032 ParamUpdate::float64(p.max_y, result.max_y as f64),
1033 ParamUpdate::float64(p.centroid_x, centroid.centroid_x),
1034 ParamUpdate::float64(p.centroid_y, centroid.centroid_y),
1035 ParamUpdate::float64(p.sigma_x, centroid.sigma_x),
1036 ParamUpdate::float64(p.sigma_y, centroid.sigma_y),
1037 ParamUpdate::float64(p.sigma_xy, centroid.sigma_xy),
1038 ParamUpdate::float64(p.centroid_total, centroid.centroid_total),
1039 ParamUpdate::float64(p.skewness_x, centroid.skewness_x),
1040 ParamUpdate::float64(p.skewness_y, centroid.skewness_y),
1041 ParamUpdate::float64(p.kurtosis_x, centroid.kurtosis_x),
1042 ParamUpdate::float64(p.kurtosis_y, centroid.kurtosis_y),
1043 ParamUpdate::float64(p.eccentricity, centroid.eccentricity),
1044 ParamUpdate::float64(p.orientation, centroid.orientation),
1045 ParamUpdate::float64(p.hist_below, result.hist_below),
1046 ParamUpdate::float64(p.hist_above, result.hist_above),
1047 ParamUpdate::float64(p.hist_entropy, result.hist_entropy),
1048 ParamUpdate::float64(p.cursor_val, result.cursor_value),
1049 ParamUpdate::int32(p.profile_size_x, info.x_size as i32),
1050 ParamUpdate::int32(p.profile_size_y, info.y_size as i32),
1051 ];
1052
1053 if self.do_compute_histogram && !result.histogram.is_empty() {
1060 updates.push(ParamUpdate::float64_array(
1061 p.hist_array,
1062 result.histogram.clone(),
1063 ));
1064 let n = result.histogram.len();
1065 let step = (self.hist_max - self.hist_min) / n as f64;
1066 let hist_x: Vec<f64> = (0..n).map(|i| self.hist_min + i as f64 * step).collect();
1067 updates.push(ParamUpdate::float64_array(p.hist_x_array, hist_x));
1068 }
1069
1070 if self.do_compute_profiles && !result.profile_avg_x.is_empty() {
1073 updates.push(ParamUpdate::float64_array(
1074 p.profile_average_x,
1075 result.profile_avg_x.clone(),
1076 ));
1077 updates.push(ParamUpdate::float64_array(
1078 p.profile_average_y,
1079 result.profile_avg_y.clone(),
1080 ));
1081 updates.push(ParamUpdate::float64_array(
1082 p.profile_threshold_x,
1083 result.profile_threshold_x.clone(),
1084 ));
1085 updates.push(ParamUpdate::float64_array(
1086 p.profile_threshold_y,
1087 result.profile_threshold_y.clone(),
1088 ));
1089 updates.push(ParamUpdate::float64_array(
1090 p.profile_centroid_x,
1091 result.profile_centroid_x.clone(),
1092 ));
1093 updates.push(ParamUpdate::float64_array(
1094 p.profile_centroid_y,
1095 result.profile_centroid_y.clone(),
1096 ));
1097 updates.push(ParamUpdate::float64_array(
1098 p.profile_cursor_x,
1099 result.profile_cursor_x.clone(),
1100 ));
1101 updates.push(ParamUpdate::float64_array(
1102 p.profile_cursor_y,
1103 result.profile_cursor_y.clone(),
1104 ));
1105 }
1106
1107 if let Some(ref sender) = self.ts_sender {
1109 let ts_data = crate::time_series::TimeSeriesData {
1110 values: vec![
1111 result.min,
1112 result.min_x as f64,
1113 result.min_y as f64,
1114 result.max,
1115 result.max_x as f64,
1116 result.max_y as f64,
1117 result.mean,
1118 result.sigma,
1119 result.total,
1120 result.net,
1121 centroid.centroid_total,
1122 centroid.centroid_x,
1123 centroid.centroid_y,
1124 centroid.sigma_x,
1125 centroid.sigma_y,
1126 centroid.sigma_xy,
1127 centroid.skewness_x,
1128 centroid.skewness_y,
1129 centroid.kurtosis_x,
1130 centroid.kurtosis_y,
1131 centroid.eccentricity,
1132 centroid.orientation,
1133 array.timestamp.as_f64(),
1134 ],
1135 };
1136 let _ = sender.try_send(ts_data);
1137 }
1138
1139 *self.latest_stats.lock() = result;
1140 ProcessResult {
1142 output_arrays: vec![Arc::new(array.clone())],
1143 param_updates: updates,
1144 scatter_index: None,
1145 }
1146 }
1147
1148 fn plugin_type(&self) -> &str {
1149 "NDPluginStats"
1150 }
1151
1152 fn register_params(
1153 &mut self,
1154 base: &mut PortDriverBase,
1155 ) -> Result<(), asyn_rs::error::AsynError> {
1156 self.params.compute_statistics =
1157 base.create_param("COMPUTE_STATISTICS", ParamType::Int32)?;
1158 base.set_int32_param(self.params.compute_statistics, 0, 1)?;
1159
1160 self.params.bgd_width = base.create_param("BGD_WIDTH", ParamType::Int32)?;
1161 self.params.min_value = base.create_param("MIN_VALUE", ParamType::Float64)?;
1162 self.params.max_value = base.create_param("MAX_VALUE", ParamType::Float64)?;
1163 self.params.mean_value = base.create_param("MEAN_VALUE", ParamType::Float64)?;
1164 self.params.sigma_value = base.create_param("SIGMA_VALUE", ParamType::Float64)?;
1165 self.params.total = base.create_param("TOTAL", ParamType::Float64)?;
1166 self.params.net = base.create_param("NET", ParamType::Float64)?;
1167 self.params.min_x = base.create_param("MIN_X", ParamType::Float64)?;
1168 self.params.min_y = base.create_param("MIN_Y", ParamType::Float64)?;
1169 self.params.max_x = base.create_param("MAX_X", ParamType::Float64)?;
1170 self.params.max_y = base.create_param("MAX_Y", ParamType::Float64)?;
1171
1172 self.params.compute_centroid = base.create_param("COMPUTE_CENTROID", ParamType::Int32)?;
1173 base.set_int32_param(self.params.compute_centroid, 0, 1)?;
1174
1175 self.params.centroid_threshold =
1176 base.create_param("CENTROID_THRESHOLD", ParamType::Float64)?;
1177 self.params.centroid_total = base.create_param("CENTROID_TOTAL", ParamType::Float64)?;
1178 self.params.centroid_x = base.create_param("CENTROIDX_VALUE", ParamType::Float64)?;
1179 self.params.centroid_y = base.create_param("CENTROIDY_VALUE", ParamType::Float64)?;
1180 self.params.sigma_x = base.create_param("SIGMAX_VALUE", ParamType::Float64)?;
1181 self.params.sigma_y = base.create_param("SIGMAY_VALUE", ParamType::Float64)?;
1182 self.params.sigma_xy = base.create_param("SIGMAXY_VALUE", ParamType::Float64)?;
1183 self.params.skewness_x = base.create_param("SKEWNESSX_VALUE", ParamType::Float64)?;
1184 self.params.skewness_y = base.create_param("SKEWNESSY_VALUE", ParamType::Float64)?;
1185 self.params.kurtosis_x = base.create_param("KURTOSISX_VALUE", ParamType::Float64)?;
1186 self.params.kurtosis_y = base.create_param("KURTOSISY_VALUE", ParamType::Float64)?;
1187 self.params.eccentricity = base.create_param("ECCENTRICITY_VALUE", ParamType::Float64)?;
1188 self.params.orientation = base.create_param("ORIENTATION_VALUE", ParamType::Float64)?;
1189
1190 self.params.compute_histogram = base.create_param("COMPUTE_HISTOGRAM", ParamType::Int32)?;
1191 self.params.hist_size = base.create_param("HIST_SIZE", ParamType::Int32)?;
1192 base.set_int32_param(self.params.hist_size, 0, 256)?;
1193 self.params.hist_min = base.create_param("HIST_MIN", ParamType::Float64)?;
1194 self.params.hist_max = base.create_param("HIST_MAX", ParamType::Float64)?;
1195 base.set_float64_param(self.params.hist_max, 0, 255.0)?;
1196 self.params.hist_below = base.create_param("HIST_BELOW", ParamType::Float64)?;
1197 self.params.hist_above = base.create_param("HIST_ABOVE", ParamType::Float64)?;
1198 self.params.hist_entropy = base.create_param("HIST_ENTROPY", ParamType::Float64)?;
1199
1200 self.params.compute_profiles = base.create_param("COMPUTE_PROFILES", ParamType::Int32)?;
1201 self.params.cursor_x = base.create_param("CURSOR_X", ParamType::Int32)?;
1202 base.set_int32_param(self.params.cursor_x, 0, 0)?;
1203 self.params.cursor_y = base.create_param("CURSOR_Y", ParamType::Int32)?;
1204 base.set_int32_param(self.params.cursor_y, 0, 0)?;
1205
1206 self.params.cursor_val = base.create_param("CURSOR_VAL", ParamType::Float64)?;
1207 self.params.profile_size_x = base.create_param("PROFILE_SIZE_X", ParamType::Int32)?;
1208 self.params.profile_size_y = base.create_param("PROFILE_SIZE_Y", ParamType::Int32)?;
1209
1210 self.params.skewx_value = base.create_param("SKEWX_VALUE", ParamType::Float64)?;
1211 self.params.skewy_value = base.create_param("SKEWY_VALUE", ParamType::Float64)?;
1212 self.params.profile_average_x =
1213 base.create_param("PROFILE_AVERAGE_X", ParamType::Float64Array)?;
1214 self.params.profile_average_y =
1215 base.create_param("PROFILE_AVERAGE_Y", ParamType::Float64Array)?;
1216 self.params.profile_threshold_x =
1217 base.create_param("PROFILE_THRESHOLD_X", ParamType::Float64Array)?;
1218 self.params.profile_threshold_y =
1219 base.create_param("PROFILE_THRESHOLD_Y", ParamType::Float64Array)?;
1220 self.params.profile_centroid_x =
1221 base.create_param("PROFILE_CENTROID_X", ParamType::Float64Array)?;
1222 self.params.profile_centroid_y =
1223 base.create_param("PROFILE_CENTROID_Y", ParamType::Float64Array)?;
1224 self.params.profile_cursor_x =
1225 base.create_param("PROFILE_CURSOR_X", ParamType::Float64Array)?;
1226 self.params.profile_cursor_y =
1227 base.create_param("PROFILE_CURSOR_Y", ParamType::Float64Array)?;
1228 self.params.hist_array = base.create_param("HIST_ARRAY", ParamType::Float64Array)?;
1229 self.params.hist_x_array = base.create_param("HIST_X_ARRAY", ParamType::Float64Array)?;
1230
1231 *self.params_out.lock() = self.params;
1233
1234 Ok(())
1235 }
1236
1237 fn on_param_change(
1238 &mut self,
1239 reason: usize,
1240 snapshot: &PluginParamSnapshot,
1241 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
1242 let p = &self.params;
1243 if reason == p.compute_statistics {
1244 self.do_compute_statistics = snapshot.value.as_i32() != 0;
1245 } else if reason == p.compute_centroid {
1246 self.do_compute_centroid = snapshot.value.as_i32() != 0;
1247 } else if reason == p.compute_histogram {
1248 self.do_compute_histogram = snapshot.value.as_i32() != 0;
1249 } else if reason == p.compute_profiles {
1250 self.do_compute_profiles = snapshot.value.as_i32() != 0;
1251 } else if reason == p.bgd_width {
1252 self.bgd_width = snapshot.value.as_i32().max(0) as usize;
1253 } else if reason == p.centroid_threshold {
1254 self.centroid_threshold = snapshot.value.as_f64();
1255 } else if reason == p.cursor_x {
1256 self.cursor_x = snapshot.value.as_i32().max(0) as usize;
1257 } else if reason == p.cursor_y {
1258 self.cursor_y = snapshot.value.as_i32().max(0) as usize;
1259 } else if reason == p.hist_size {
1260 self.hist_size = (snapshot.value.as_i32().max(1)) as usize;
1261 } else if reason == p.hist_min {
1262 self.hist_min = snapshot.value.as_f64();
1263 } else if reason == p.hist_max {
1264 self.hist_max = snapshot.value.as_f64();
1265 }
1266 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
1267 }
1268}
1269
1270pub fn create_stats_runtime(
1276 port_name: &str,
1277 pool: Arc<NDArrayPool>,
1278 queue_size: usize,
1279 ndarray_port: &str,
1280 wiring: Arc<WiringRegistry>,
1281 ts_registry: &crate::time_series::TsReceiverRegistry,
1282) -> (
1283 PluginRuntimeHandle,
1284 Arc<Mutex<StatsResult>>,
1285 NDStatsParams,
1286 std::thread::JoinHandle<()>,
1287) {
1288 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
1289
1290 let mut processor = StatsProcessor::new();
1291 processor.set_ts_sender(ts_tx);
1292 let stats_handle = processor.stats_handle();
1293 let params_handle = processor.params_handle();
1294
1295 let (plugin_handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
1296 port_name,
1297 processor,
1298 pool,
1299 queue_size,
1300 ndarray_port,
1301 wiring,
1302 );
1303
1304 let stats_params = *params_handle.lock();
1305
1306 let channel_names: Vec<String> = crate::time_series::STATS_TS_CHANNEL_NAMES
1308 .iter()
1309 .map(|s| s.to_string())
1310 .collect();
1311 ts_registry.store(port_name, ts_rx, channel_names);
1312
1313 (plugin_handle, stats_handle, stats_params, data_jh)
1314}
1315
1316#[cfg(test)]
1317mod tests {
1318 use super::*;
1319 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1320
1321 #[test]
1322 fn test_compute_stats_u8() {
1323 let dims = vec![NDDimension::new(5)];
1324 let data = NDDataBuffer::U8(vec![10, 20, 30, 40, 50]);
1325 let stats = compute_stats(&data, &dims, 0);
1326 assert_eq!(stats.min, 10.0);
1327 assert_eq!(stats.max, 50.0);
1328 assert_eq!(stats.mean, 30.0);
1329 assert_eq!(stats.total, 150.0);
1330 assert_eq!(stats.num_elements, 5);
1331 }
1332
1333 #[test]
1334 fn test_compute_stats_sigma() {
1335 let dims = vec![NDDimension::new(8)];
1336 let data = NDDataBuffer::F64(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
1337 let stats = compute_stats(&data, &dims, 0);
1338 assert!((stats.mean - 5.0).abs() < 1e-10);
1339 assert!((stats.sigma - 2.0).abs() < 1e-10);
1340 }
1341
1342 #[test]
1343 fn test_compute_stats_u16() {
1344 let dims = vec![NDDimension::new(3)];
1345 let data = NDDataBuffer::U16(vec![100, 200, 300]);
1346 let stats = compute_stats(&data, &dims, 0);
1347 assert_eq!(stats.min, 100.0);
1348 assert_eq!(stats.max, 300.0);
1349 assert_eq!(stats.mean, 200.0);
1350 }
1351
1352 #[test]
1353 fn test_compute_stats_f64() {
1354 let dims = vec![NDDimension::new(3)];
1355 let data = NDDataBuffer::F64(vec![1.5, 2.5, 3.5]);
1356 let stats = compute_stats(&data, &dims, 0);
1357 assert!((stats.min - 1.5).abs() < 1e-10);
1358 assert!((stats.max - 3.5).abs() < 1e-10);
1359 assert!((stats.mean - 2.5).abs() < 1e-10);
1360 }
1361
1362 #[test]
1363 fn test_compute_stats_single_element() {
1364 let dims = vec![NDDimension::new(1)];
1365 let data = NDDataBuffer::I32(vec![42]);
1366 let stats = compute_stats(&data, &dims, 0);
1367 assert_eq!(stats.min, 42.0);
1368 assert_eq!(stats.max, 42.0);
1369 assert_eq!(stats.mean, 42.0);
1370 assert_eq!(stats.sigma, 0.0);
1371 assert_eq!(stats.num_elements, 1);
1372 }
1373
1374 #[test]
1375 fn test_compute_stats_empty() {
1376 let data = NDDataBuffer::U8(vec![]);
1377 let stats = compute_stats(&data, &[], 0);
1378 assert_eq!(stats.num_elements, 0);
1379 }
1380
1381 #[test]
1382 fn test_compute_stats_min_max_position() {
1383 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1384 let data = NDDataBuffer::U8((1..=16).collect());
1386 let stats = compute_stats(&data, &dims, 0);
1387 assert_eq!(stats.min_x, 0); assert_eq!(stats.min_y, 0);
1389 assert_eq!(stats.max_x, 3); assert_eq!(stats.max_y, 3);
1391 }
1392
1393 #[test]
1394 fn test_compute_stats_net_no_bgd() {
1395 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1396 let data = NDDataBuffer::U8((1..=16).collect());
1397 let stats = compute_stats(&data, &dims, 0);
1398 assert_eq!(stats.net, stats.total);
1400 }
1401
1402 #[test]
1403 fn test_compute_stats_bgd_subtraction() {
1404 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1406 let mut pixels = vec![10u16; 16];
1407 pixels[2 * 4 + 2] = 110;
1409 let data = NDDataBuffer::U16(pixels);
1410 let stats = compute_stats(&data, &dims, 1);
1411
1412 assert!((stats.net - 100.0).abs() < 1e-10);
1421 }
1422
1423 #[test]
1433 fn test_compute_stats_bgd_2d_corner_double_count() {
1434 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1445 let mut pixels = vec![1u16; 16];
1446 for &i in &[1usize, 2, 4, 7, 8, 11, 13, 14] {
1447 pixels[i] = 10;
1448 }
1449 for &i in &[0usize, 3, 12, 15] {
1450 pixels[i] = 100;
1451 }
1452 let total_expected: f64 = pixels.iter().map(|&p| p as f64).sum();
1453 let data = NDDataBuffer::U16(pixels);
1454 let stats = compute_stats(&data, &dims, 1);
1455
1456 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;
1466 assert!(
1467 (stats.net - expected_net).abs() < 1e-9,
1468 "net {} != expected {}",
1469 stats.net,
1470 expected_net
1471 );
1472
1473 let perimeter_avg = (4.0 * 100.0 + 8.0 * 10.0) / 12.0;
1477 assert!(
1478 (bgd_avg - perimeter_avg).abs() > 1e-9,
1479 "corner double-count must change bgd_avg vs a once-each perimeter"
1480 );
1481 assert!((bgd_avg - 55.0).abs() < 1e-9);
1482 }
1483
1484 #[test]
1487 fn test_compute_stats_bgd_1d() {
1488 let dims = vec![NDDimension::new(8)];
1490 let data = NDDataBuffer::U16(vec![10, 20, 30, 40, 50, 60, 70, 80]);
1491 let stats = compute_stats(&data, &dims, 2);
1492
1493 let bgd_counts = 10 + 20 + 70 + 80;
1497 let bgd_pixels = 4;
1498 let bgd_avg = bgd_counts as f64 / bgd_pixels as f64; let total = (10 + 20 + 30 + 40 + 50 + 60 + 70 + 80) as f64;
1500 let expected_net = total - bgd_avg * 8.0;
1501 assert!(
1502 (stats.net - expected_net).abs() < 1e-9,
1503 "1-D net {} != expected {}",
1504 stats.net,
1505 expected_net
1506 );
1507 }
1508
1509 #[test]
1511 fn test_compute_stats_bgd_3d() {
1512 let dims = vec![
1517 NDDimension::new(2),
1518 NDDimension::new(2),
1519 NDDimension::new(2),
1520 ];
1521 let data = NDDataBuffer::U8(vec![1u8; 8]);
1522 let stats = compute_stats(&data, &dims, 1);
1523 assert!(
1524 stats.net.abs() < 1e-9,
1525 "3-D uniform net should be 0, got {}",
1526 stats.net
1527 );
1528 assert_eq!(stats.total, 8.0);
1529 }
1530
1531 #[test]
1532 fn test_centroid_uniform() {
1533 let data = NDDataBuffer::U8(vec![1; 16]);
1534 let c = compute_centroid(&data, 4, 4, 0.0);
1535 assert!((c.centroid_x - 1.5).abs() < 1e-10);
1536 assert!((c.centroid_y - 1.5).abs() < 1e-10);
1537 }
1538
1539 #[test]
1540 fn test_centroid_corner() {
1541 let mut d = vec![0u8; 16];
1542 d[0] = 255;
1543 let data = NDDataBuffer::U8(d);
1544 let c = compute_centroid(&data, 4, 4, 0.0);
1545 assert!((c.centroid_x - 0.0).abs() < 1e-10);
1546 assert!((c.centroid_y - 0.0).abs() < 1e-10);
1547 }
1548
1549 #[test]
1550 fn test_centroid_threshold() {
1551 let mut pixels = vec![5u8; 16];
1553 pixels[2 * 4 + 2] = 100;
1554 let data = NDDataBuffer::U8(pixels);
1555
1556 let c = compute_centroid(&data, 4, 4, 50.0);
1558 assert!((c.centroid_x - 2.0).abs() < 1e-10);
1559 assert!((c.centroid_y - 2.0).abs() < 1e-10);
1560 assert!((c.centroid_total - 100.0).abs() < 1e-10);
1561 }
1562
1563 #[test]
1564 fn test_centroid_higher_moments_symmetric() {
1565 let data = NDDataBuffer::U8(vec![1; 16]);
1567 let c = compute_centroid(&data, 4, 4, 0.0);
1568 assert!(c.skewness_x.abs() < 1e-10);
1570 assert!(c.skewness_y.abs() < 1e-10);
1571 assert!(c.eccentricity.abs() < 1e-10);
1573 }
1574
1575 #[test]
1576 fn test_histogram_basic() {
1577 let data = NDDataBuffer::F64((0..10).map(|x| x as f64).collect());
1579 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 9.0);
1580 assert_eq!(hist.len(), 10);
1581 assert_eq!(below, 0.0);
1582 assert_eq!(above, 0.0);
1583 let total: f64 = hist.iter().sum();
1585 assert!((total - 10.0).abs() < 1e-10);
1586 assert!(entropy.abs() < 1e-10);
1589 }
1590
1591 #[test]
1592 fn test_histogram_below_above() {
1593 let data = NDDataBuffer::F64(vec![-1.0, 0.5, 1.5, 3.0]);
1594 let (hist, below, above, _entropy) = compute_histogram(&data, 2, 0.0, 2.0);
1595 assert_eq!(below, 1.0); assert_eq!(above, 1.0); let total_in_bins: f64 = hist.iter().sum();
1598 assert!((total_in_bins - 2.0).abs() < 1e-10); }
1600
1601 #[test]
1602 fn test_histogram_single_value() {
1603 let data = NDDataBuffer::F64(vec![5.0; 100]);
1604 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 10.0);
1605 assert_eq!(below, 0.0);
1606 assert_eq!(above, 0.0);
1607 let expected = -(100.0f64.ln());
1611 assert!((entropy - expected).abs() < 1e-10);
1612 let total: f64 = hist.iter().sum();
1613 assert!((total - 100.0).abs() < 1e-10);
1614 }
1615
1616 #[test]
1617 fn test_profiles_8x8() {
1618 let mut pixels = vec![0.0f64; 64];
1620 for iy in 0..8 {
1621 for ix in 0..8 {
1622 pixels[iy * 8 + ix] = iy as f64;
1623 }
1624 }
1625 let data = NDDataBuffer::F64(pixels);
1626
1627 let profiles = compute_profiles(
1628 &data, 8, 8, 0.0, 3.5, 3.5, 0, 7, );
1634
1635 assert_eq!(profiles.avg_x.len(), 8);
1637 for &v in &profiles.avg_x {
1638 assert!((v - 3.5).abs() < 1e-10, "avg_x should be 3.5, got {v}");
1639 }
1640
1641 assert_eq!(profiles.avg_y.len(), 8);
1643 for (iy, &v) in profiles.avg_y.iter().enumerate() {
1644 assert!(
1645 (v - iy as f64).abs() < 1e-10,
1646 "avg_y[{iy}] should be {iy}, got {v}"
1647 );
1648 }
1649
1650 assert_eq!(profiles.cursor_x.len(), 8);
1652 for &v in &profiles.cursor_x {
1653 assert!((v - 7.0).abs() < 1e-10);
1654 }
1655
1656 assert_eq!(profiles.cursor_y.len(), 8);
1658 for (iy, &v) in profiles.cursor_y.iter().enumerate() {
1659 assert!((v - iy as f64).abs() < 1e-10);
1660 }
1661
1662 assert_eq!(profiles.centroid_x.len(), 8);
1664 for &v in &profiles.centroid_x {
1665 assert!((v - 4.0).abs() < 1e-10);
1666 }
1667
1668 assert_eq!(profiles.centroid_y.len(), 8);
1670 for (iy, &v) in profiles.centroid_y.iter().enumerate() {
1671 assert!((v - iy as f64).abs() < 1e-10);
1672 }
1673 }
1674
1675 #[test]
1676 fn test_profiles_threshold() {
1677 let mut pixels = vec![1.0f64; 16];
1679 pixels[1 * 4 + 2] = 10.0;
1680 let data = NDDataBuffer::F64(pixels);
1681
1682 let profiles = compute_profiles(
1683 &data, 4, 4, 5.0, 2.0, 1.0, 0, 0,
1685 );
1686
1687 assert_eq!(profiles.threshold_x.len(), 4);
1689 assert!((profiles.threshold_x[2] - 10.0).abs() < 1e-10);
1690 assert!((profiles.threshold_x[0] - 0.0).abs() < 1e-10);
1692 assert!((profiles.threshold_x[1] - 0.0).abs() < 1e-10);
1693 assert!((profiles.threshold_x[3] - 0.0).abs() < 1e-10);
1694
1695 assert_eq!(profiles.threshold_y.len(), 4);
1697 assert!((profiles.threshold_y[1] - 10.0).abs() < 1e-10);
1698 assert!((profiles.threshold_y[0] - 0.0).abs() < 1e-10);
1699 }
1700
1701 #[test]
1702 fn test_stats_processor_direct() {
1703 let mut proc = StatsProcessor::new();
1704 let pool = NDArrayPool::new(1_000_000);
1705
1706 let mut arr = NDArray::new(vec![NDDimension::new(5)], NDDataType::UInt8);
1707 if let NDDataBuffer::U8(ref mut v) = arr.data {
1708 v[0] = 10;
1709 v[1] = 20;
1710 v[2] = 30;
1711 v[3] = 40;
1712 v[4] = 50;
1713 }
1714
1715 let result = proc.process_array(&arr, &pool);
1716 assert_eq!(result.output_arrays.len(), 1, "stats forwards the array");
1718
1719 let stats = proc.stats_handle().lock().clone();
1720 assert_eq!(stats.min, 10.0);
1721 assert_eq!(stats.max, 50.0);
1722 assert_eq!(stats.mean, 30.0);
1723 }
1724
1725 #[test]
1726 fn test_stats_emits_histogram_and_profile_arrays() {
1727 use ad_core_rs::plugin::runtime::ParamUpdate;
1728 let mut proc = StatsProcessor::new();
1729 let mut base = asyn_rs::port::PortDriverBase::new(
1731 "_stats_scratch_",
1732 1,
1733 asyn_rs::port::PortFlags::default(),
1734 );
1735 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1736 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1737 proc.register_params(&mut base).unwrap();
1738
1739 proc.do_compute_histogram = true;
1740 proc.do_compute_profiles = true;
1741 proc.hist_size = 8;
1742 proc.hist_min = 0.0;
1743 proc.hist_max = 7.0;
1744 let pool = NDArrayPool::new(1_000_000);
1745
1746 let mut arr = NDArray::new(
1747 vec![NDDimension::new(4), NDDimension::new(4)],
1748 NDDataType::UInt8,
1749 );
1750 if let NDDataBuffer::U8(ref mut v) = arr.data {
1751 for (i, val) in v.iter_mut().enumerate() {
1752 *val = (i % 8) as u8;
1753 }
1754 }
1755
1756 let result = proc.process_array(&arr, &pool);
1757 let p = proc.params;
1758 let array_reasons: Vec<usize> = result
1761 .param_updates
1762 .iter()
1763 .filter_map(|u| match u {
1764 ParamUpdate::Float64Array { reason, value, .. } => {
1765 assert!(!value.is_empty(), "array waveform must not be empty");
1766 Some(*reason)
1767 }
1768 _ => None,
1769 })
1770 .collect();
1771 for reason in [
1772 p.hist_array,
1773 p.hist_x_array,
1774 p.profile_average_x,
1775 p.profile_average_y,
1776 p.profile_threshold_x,
1777 p.profile_threshold_y,
1778 p.profile_centroid_x,
1779 p.profile_centroid_y,
1780 p.profile_cursor_x,
1781 p.profile_cursor_y,
1782 ] {
1783 assert!(
1784 array_reasons.contains(&reason),
1785 "missing array update for reason {reason}"
1786 );
1787 }
1788 }
1789
1790 #[test]
1791 fn test_hist_x_array_uses_bin_count_divisor() {
1792 use ad_core_rs::plugin::runtime::ParamUpdate;
1793 let mut proc = StatsProcessor::new();
1797 let mut base = asyn_rs::port::PortDriverBase::new(
1798 "_stats_histx_",
1799 1,
1800 asyn_rs::port::PortFlags::default(),
1801 );
1802 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1803 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1804 proc.register_params(&mut base).unwrap();
1805
1806 proc.do_compute_histogram = true;
1807 proc.hist_size = 256;
1808 proc.hist_min = 0.0;
1809 proc.hist_max = 255.0;
1810 let pool = NDArrayPool::new(1_000_000);
1811
1812 let mut arr = NDArray::new(
1813 vec![NDDimension::new(4), NDDimension::new(4)],
1814 NDDataType::UInt8,
1815 );
1816 if let NDDataBuffer::U8(ref mut v) = arr.data {
1817 for (i, val) in v.iter_mut().enumerate() {
1818 *val = (i * 16) as u8;
1819 }
1820 }
1821
1822 let result = proc.process_array(&arr, &pool);
1823 let hist_x = result
1824 .param_updates
1825 .iter()
1826 .find_map(|u| match u {
1827 ParamUpdate::Float64Array { reason, value, .. }
1828 if *reason == proc.params.hist_x_array =>
1829 {
1830 Some(value.clone())
1831 }
1832 _ => None,
1833 })
1834 .expect("HIST_X_ARRAY must be emitted");
1835
1836 assert_eq!(hist_x.len(), 256, "256 bins");
1837 let scale = 255.0 / 256.0;
1838 assert!(
1839 (hist_x[0] - 0.0).abs() < 1e-9,
1840 "bin 0 X must be histMin (0.0), got {}",
1841 hist_x[0]
1842 );
1843 assert!(
1844 (hist_x[1] - scale).abs() < 1e-9,
1845 "bin 1 X must be {scale}, got {}",
1846 hist_x[1]
1847 );
1848 assert!(
1849 (hist_x[255] - 255.0 * scale).abs() < 1e-9,
1850 "last bin X must be ~254.004 (255*255/256), got {}",
1851 hist_x[255]
1852 );
1853 assert!(
1854 hist_x[255] < 255.0,
1855 "last bin X must be strictly below histMax, got {}",
1856 hist_x[255]
1857 );
1858 }
1859
1860 #[test]
1861 fn test_stats_runtime_end_to_end() {
1862 let pool = Arc::new(NDArrayPool::new(1_000_000));
1863 let wiring = Arc::new(WiringRegistry::new());
1864 let ts_registry = crate::time_series::TsReceiverRegistry::new();
1865 let (handle, stats, _params, _jh) =
1866 create_stats_runtime("STATS_RT", pool, 10, "", wiring, &ts_registry);
1867
1868 handle
1870 .port_runtime()
1871 .port_handle()
1872 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
1873 .unwrap();
1874 std::thread::sleep(std::time::Duration::from_millis(10));
1875
1876 let mut arr = NDArray::new(
1877 vec![NDDimension::new(4), NDDimension::new(4)],
1878 NDDataType::UInt8,
1879 );
1880 if let NDDataBuffer::U8(ref mut v) = arr.data {
1881 for (i, val) in v.iter_mut().enumerate() {
1882 *val = (i + 1) as u8;
1883 }
1884 }
1885
1886 let rt = tokio::runtime::Builder::new_current_thread()
1887 .enable_all()
1888 .build()
1889 .unwrap();
1890 rt.block_on(handle.array_sender().publish(Arc::new(arr)));
1891 std::thread::sleep(std::time::Duration::from_millis(100));
1892
1893 let result = stats.lock().clone();
1894 assert_eq!(result.min, 1.0);
1895 assert_eq!(result.max, 16.0);
1896 assert_eq!(result.num_elements, 16);
1897 }
1898}