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.time_stamp,
1128 ],
1129 };
1130 let _ = sender.try_send(ts_data);
1131 }
1132
1133 *self.latest_stats.lock() = result;
1134 ProcessResult {
1136 output_arrays: vec![Arc::new(array.clone())],
1137 param_updates: updates,
1138 scatter: false,
1139 }
1140 }
1141
1142 fn plugin_type(&self) -> &str {
1143 "NDPluginStats"
1144 }
1145
1146 fn register_params(
1147 &mut self,
1148 base: &mut PortDriverBase,
1149 ) -> Result<(), asyn_rs::error::AsynError> {
1150 self.params.compute_statistics =
1151 base.create_param("COMPUTE_STATISTICS", ParamType::Int32)?;
1152 base.set_int32_param(self.params.compute_statistics, 0, 1)?;
1153
1154 self.params.bgd_width = base.create_param("BGD_WIDTH", ParamType::Int32)?;
1155 self.params.min_value = base.create_param("MIN_VALUE", ParamType::Float64)?;
1156 self.params.max_value = base.create_param("MAX_VALUE", ParamType::Float64)?;
1157 self.params.mean_value = base.create_param("MEAN_VALUE", ParamType::Float64)?;
1158 self.params.sigma_value = base.create_param("SIGMA_VALUE", ParamType::Float64)?;
1159 self.params.total = base.create_param("TOTAL", ParamType::Float64)?;
1160 self.params.net = base.create_param("NET", ParamType::Float64)?;
1161 self.params.min_x = base.create_param("MIN_X", ParamType::Float64)?;
1162 self.params.min_y = base.create_param("MIN_Y", ParamType::Float64)?;
1163 self.params.max_x = base.create_param("MAX_X", ParamType::Float64)?;
1164 self.params.max_y = base.create_param("MAX_Y", ParamType::Float64)?;
1165
1166 self.params.compute_centroid = base.create_param("COMPUTE_CENTROID", ParamType::Int32)?;
1167 base.set_int32_param(self.params.compute_centroid, 0, 1)?;
1168
1169 self.params.centroid_threshold =
1170 base.create_param("CENTROID_THRESHOLD", ParamType::Float64)?;
1171 self.params.centroid_total = base.create_param("CENTROID_TOTAL", ParamType::Float64)?;
1172 self.params.centroid_x = base.create_param("CENTROIDX_VALUE", ParamType::Float64)?;
1173 self.params.centroid_y = base.create_param("CENTROIDY_VALUE", ParamType::Float64)?;
1174 self.params.sigma_x = base.create_param("SIGMAX_VALUE", ParamType::Float64)?;
1175 self.params.sigma_y = base.create_param("SIGMAY_VALUE", ParamType::Float64)?;
1176 self.params.sigma_xy = base.create_param("SIGMAXY_VALUE", ParamType::Float64)?;
1177 self.params.skewness_x = base.create_param("SKEWNESSX_VALUE", ParamType::Float64)?;
1178 self.params.skewness_y = base.create_param("SKEWNESSY_VALUE", ParamType::Float64)?;
1179 self.params.kurtosis_x = base.create_param("KURTOSISX_VALUE", ParamType::Float64)?;
1180 self.params.kurtosis_y = base.create_param("KURTOSISY_VALUE", ParamType::Float64)?;
1181 self.params.eccentricity = base.create_param("ECCENTRICITY_VALUE", ParamType::Float64)?;
1182 self.params.orientation = base.create_param("ORIENTATION_VALUE", ParamType::Float64)?;
1183
1184 self.params.compute_histogram = base.create_param("COMPUTE_HISTOGRAM", ParamType::Int32)?;
1185 self.params.hist_size = base.create_param("HIST_SIZE", ParamType::Int32)?;
1186 base.set_int32_param(self.params.hist_size, 0, 256)?;
1187 self.params.hist_min = base.create_param("HIST_MIN", ParamType::Float64)?;
1188 self.params.hist_max = base.create_param("HIST_MAX", ParamType::Float64)?;
1189 base.set_float64_param(self.params.hist_max, 0, 255.0)?;
1190 self.params.hist_below = base.create_param("HIST_BELOW", ParamType::Int32)?;
1195 self.params.hist_above = base.create_param("HIST_ABOVE", ParamType::Int32)?;
1196 self.params.hist_entropy = base.create_param("HIST_ENTROPY", ParamType::Float64)?;
1197
1198 self.params.compute_profiles = base.create_param("COMPUTE_PROFILES", ParamType::Int32)?;
1199 self.params.cursor_x = base.create_param("CURSOR_X", ParamType::Int32)?;
1200 base.set_int32_param(self.params.cursor_x, 0, 0)?;
1201 self.params.cursor_y = base.create_param("CURSOR_Y", ParamType::Int32)?;
1202 base.set_int32_param(self.params.cursor_y, 0, 0)?;
1203
1204 self.params.cursor_val = base.create_param("CURSOR_VAL", ParamType::Float64)?;
1205 self.params.profile_size_x = base.create_param("PROFILE_SIZE_X", ParamType::Int32)?;
1206 self.params.profile_size_y = base.create_param("PROFILE_SIZE_Y", ParamType::Int32)?;
1207
1208 self.params.skewx_value = base.create_param("SKEWX_VALUE", ParamType::Float64)?;
1209 self.params.skewy_value = base.create_param("SKEWY_VALUE", ParamType::Float64)?;
1210 self.params.profile_average_x =
1211 base.create_param("PROFILE_AVERAGE_X", ParamType::Float64Array)?;
1212 self.params.profile_average_y =
1213 base.create_param("PROFILE_AVERAGE_Y", ParamType::Float64Array)?;
1214 self.params.profile_threshold_x =
1215 base.create_param("PROFILE_THRESHOLD_X", ParamType::Float64Array)?;
1216 self.params.profile_threshold_y =
1217 base.create_param("PROFILE_THRESHOLD_Y", ParamType::Float64Array)?;
1218 self.params.profile_centroid_x =
1219 base.create_param("PROFILE_CENTROID_X", ParamType::Float64Array)?;
1220 self.params.profile_centroid_y =
1221 base.create_param("PROFILE_CENTROID_Y", ParamType::Float64Array)?;
1222 self.params.profile_cursor_x =
1223 base.create_param("PROFILE_CURSOR_X", ParamType::Float64Array)?;
1224 self.params.profile_cursor_y =
1225 base.create_param("PROFILE_CURSOR_Y", ParamType::Float64Array)?;
1226 self.params.hist_array = base.create_param("HIST_ARRAY", ParamType::Float64Array)?;
1227 self.params.hist_x_array = base.create_param("HIST_X_ARRAY", ParamType::Float64Array)?;
1228
1229 *self.params_out.lock() = self.params;
1231
1232 Ok(())
1233 }
1234
1235 fn on_param_change(
1236 &mut self,
1237 reason: usize,
1238 snapshot: &PluginParamSnapshot,
1239 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
1240 let p = &self.params;
1241 if reason == p.compute_statistics {
1242 self.do_compute_statistics = snapshot.value.as_i32() != 0;
1243 } else if reason == p.compute_centroid {
1244 self.do_compute_centroid = snapshot.value.as_i32() != 0;
1245 } else if reason == p.compute_histogram {
1246 self.do_compute_histogram = snapshot.value.as_i32() != 0;
1247 } else if reason == p.compute_profiles {
1248 self.do_compute_profiles = snapshot.value.as_i32() != 0;
1249 } else if reason == p.bgd_width {
1250 self.bgd_width = snapshot.value.as_i32().max(0) as usize;
1251 } else if reason == p.centroid_threshold {
1252 self.centroid_threshold = snapshot.value.as_f64();
1253 } else if reason == p.cursor_x {
1254 self.cursor_x = snapshot.value.as_i32().max(0) as usize;
1255 } else if reason == p.cursor_y {
1256 self.cursor_y = snapshot.value.as_i32().max(0) as usize;
1257 } else if reason == p.hist_size {
1258 self.hist_size = (snapshot.value.as_i32().max(1)) as usize;
1259 } else if reason == p.hist_min {
1260 self.hist_min = snapshot.value.as_f64();
1261 } else if reason == p.hist_max {
1262 self.hist_max = snapshot.value.as_f64();
1263 }
1264 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
1265 }
1266}
1267
1268pub fn create_stats_runtime(
1274 port_name: &str,
1275 pool: Arc<NDArrayPool>,
1276 queue_size: usize,
1277 ndarray_port: &str,
1278 wiring: Arc<WiringRegistry>,
1279 ts_registry: &crate::time_series::TsReceiverRegistry,
1280) -> (
1281 PluginRuntimeHandle,
1282 Arc<Mutex<StatsResult>>,
1283 NDStatsParams,
1284 std::thread::JoinHandle<()>,
1285) {
1286 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
1287
1288 let mut processor = StatsProcessor::new();
1289 processor.set_ts_sender(ts_tx);
1290 let stats_handle = processor.stats_handle();
1291 let params_handle = processor.params_handle();
1292
1293 let (plugin_handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
1294 port_name,
1295 processor,
1296 pool,
1297 queue_size,
1298 ndarray_port,
1299 wiring,
1300 );
1301
1302 let stats_params = *params_handle.lock();
1303
1304 let channel_names: Vec<String> = crate::time_series::STATS_TS_CHANNEL_NAMES
1306 .iter()
1307 .map(|s| s.to_string())
1308 .collect();
1309 ts_registry.store(port_name, ts_rx, channel_names);
1310
1311 (plugin_handle, stats_handle, stats_params, data_jh)
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 use super::*;
1317 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1318
1319 #[test]
1320 fn test_ts_timestamp_channel_is_the_standalone_double() {
1321 use crate::time_series::{NUM_STATS_TS_CHANNELS, STATS_TS_CHANNEL_NAMES};
1326 use ad_core_rs::ndarray::NDArray;
1327 use ad_core_rs::ndarray_pool::NDArrayPool;
1328 use ad_core_rs::plugin::runtime::NDPluginProcess;
1329
1330 let (tx, mut rx) = tokio::sync::mpsc::channel(4);
1331 let mut processor = StatsProcessor::new();
1332 processor.set_ts_sender(tx);
1333
1334 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1335 arr.data = NDDataBuffer::U8(vec![1, 2, 3, 4]);
1336 arr.timestamp = ad_core_rs::timestamp::EpicsTimestamp {
1337 sec: 100,
1338 nsec: 500_000_000,
1339 };
1340 arr.time_stamp = 7.25; processor.process_array(&arr, &NDArrayPool::new(1_000_000));
1343
1344 let ts = rx.try_recv().expect("stats pushes a TS sample per frame");
1345 assert_eq!(ts.values.len(), NUM_STATS_TS_CHANNELS);
1346 let idx = STATS_TS_CHANNEL_NAMES
1347 .iter()
1348 .position(|n| *n == "TSTimestamp")
1349 .unwrap();
1350 assert!(
1351 (ts.values[idx] - 7.25).abs() < 1e-9,
1352 "TSTimestamp carries pArray->timeStamp, got {}",
1353 ts.values[idx]
1354 );
1355 }
1356
1357 #[test]
1358 fn test_compute_stats_u8() {
1359 let dims = vec![NDDimension::new(5)];
1360 let data = NDDataBuffer::U8(vec![10, 20, 30, 40, 50]);
1361 let stats = compute_stats(&data, &dims, 0);
1362 assert_eq!(stats.min, 10.0);
1363 assert_eq!(stats.max, 50.0);
1364 assert_eq!(stats.mean, 30.0);
1365 assert_eq!(stats.total, 150.0);
1366 assert_eq!(stats.num_elements, 5);
1367 }
1368
1369 #[test]
1370 fn test_compute_stats_sigma() {
1371 let dims = vec![NDDimension::new(8)];
1372 let data = NDDataBuffer::F64(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
1373 let stats = compute_stats(&data, &dims, 0);
1374 assert!((stats.mean - 5.0).abs() < 1e-10);
1375 assert!((stats.sigma - 2.0).abs() < 1e-10);
1376 }
1377
1378 #[test]
1379 fn test_compute_stats_u16() {
1380 let dims = vec![NDDimension::new(3)];
1381 let data = NDDataBuffer::U16(vec![100, 200, 300]);
1382 let stats = compute_stats(&data, &dims, 0);
1383 assert_eq!(stats.min, 100.0);
1384 assert_eq!(stats.max, 300.0);
1385 assert_eq!(stats.mean, 200.0);
1386 }
1387
1388 #[test]
1389 fn test_compute_stats_f64() {
1390 let dims = vec![NDDimension::new(3)];
1391 let data = NDDataBuffer::F64(vec![1.5, 2.5, 3.5]);
1392 let stats = compute_stats(&data, &dims, 0);
1393 assert!((stats.min - 1.5).abs() < 1e-10);
1394 assert!((stats.max - 3.5).abs() < 1e-10);
1395 assert!((stats.mean - 2.5).abs() < 1e-10);
1396 }
1397
1398 #[test]
1399 fn test_compute_stats_single_element() {
1400 let dims = vec![NDDimension::new(1)];
1401 let data = NDDataBuffer::I32(vec![42]);
1402 let stats = compute_stats(&data, &dims, 0);
1403 assert_eq!(stats.min, 42.0);
1404 assert_eq!(stats.max, 42.0);
1405 assert_eq!(stats.mean, 42.0);
1406 assert_eq!(stats.sigma, 0.0);
1407 assert_eq!(stats.num_elements, 1);
1408 }
1409
1410 #[test]
1411 fn test_compute_stats_empty() {
1412 let data = NDDataBuffer::U8(vec![]);
1413 let stats = compute_stats(&data, &[], 0);
1414 assert_eq!(stats.num_elements, 0);
1415 }
1416
1417 #[test]
1418 fn test_compute_stats_min_max_position() {
1419 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1420 let data = NDDataBuffer::U8((1..=16).collect());
1422 let stats = compute_stats(&data, &dims, 0);
1423 assert_eq!(stats.min_x, 0); assert_eq!(stats.min_y, 0);
1425 assert_eq!(stats.max_x, 3); assert_eq!(stats.max_y, 3);
1427 }
1428
1429 #[test]
1430 fn test_compute_stats_net_no_bgd() {
1431 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1432 let data = NDDataBuffer::U8((1..=16).collect());
1433 let stats = compute_stats(&data, &dims, 0);
1434 assert_eq!(stats.net, stats.total);
1436 }
1437
1438 #[test]
1439 fn test_compute_stats_bgd_subtraction() {
1440 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1442 let mut pixels = vec![10u16; 16];
1443 pixels[2 * 4 + 2] = 110;
1445 let data = NDDataBuffer::U16(pixels);
1446 let stats = compute_stats(&data, &dims, 1);
1447
1448 assert!((stats.net - 100.0).abs() < 1e-10);
1457 }
1458
1459 #[test]
1469 fn test_compute_stats_bgd_2d_corner_double_count() {
1470 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1481 let mut pixels = vec![1u16; 16];
1482 for &i in &[1usize, 2, 4, 7, 8, 11, 13, 14] {
1483 pixels[i] = 10;
1484 }
1485 for &i in &[0usize, 3, 12, 15] {
1486 pixels[i] = 100;
1487 }
1488 let total_expected: f64 = pixels.iter().map(|&p| p as f64).sum();
1489 let data = NDDataBuffer::U16(pixels);
1490 let stats = compute_stats(&data, &dims, 1);
1491
1492 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;
1502 assert!(
1503 (stats.net - expected_net).abs() < 1e-9,
1504 "net {} != expected {}",
1505 stats.net,
1506 expected_net
1507 );
1508
1509 let perimeter_avg = (4.0 * 100.0 + 8.0 * 10.0) / 12.0;
1513 assert!(
1514 (bgd_avg - perimeter_avg).abs() > 1e-9,
1515 "corner double-count must change bgd_avg vs a once-each perimeter"
1516 );
1517 assert!((bgd_avg - 55.0).abs() < 1e-9);
1518 }
1519
1520 #[test]
1523 fn test_compute_stats_bgd_1d() {
1524 let dims = vec![NDDimension::new(8)];
1526 let data = NDDataBuffer::U16(vec![10, 20, 30, 40, 50, 60, 70, 80]);
1527 let stats = compute_stats(&data, &dims, 2);
1528
1529 let bgd_counts = 10 + 20 + 70 + 80;
1533 let bgd_pixels = 4;
1534 let bgd_avg = bgd_counts as f64 / bgd_pixels as f64; let total = (10 + 20 + 30 + 40 + 50 + 60 + 70 + 80) as f64;
1536 let expected_net = total - bgd_avg * 8.0;
1537 assert!(
1538 (stats.net - expected_net).abs() < 1e-9,
1539 "1-D net {} != expected {}",
1540 stats.net,
1541 expected_net
1542 );
1543 }
1544
1545 #[test]
1547 fn test_compute_stats_bgd_3d() {
1548 let dims = vec![
1553 NDDimension::new(2),
1554 NDDimension::new(2),
1555 NDDimension::new(2),
1556 ];
1557 let data = NDDataBuffer::U8(vec![1u8; 8]);
1558 let stats = compute_stats(&data, &dims, 1);
1559 assert!(
1560 stats.net.abs() < 1e-9,
1561 "3-D uniform net should be 0, got {}",
1562 stats.net
1563 );
1564 assert_eq!(stats.total, 8.0);
1565 }
1566
1567 #[test]
1568 fn test_centroid_uniform() {
1569 let data = NDDataBuffer::U8(vec![1; 16]);
1570 let c = compute_centroid(&data, 4, 4, 0.0);
1571 assert!((c.centroid_x - 1.5).abs() < 1e-10);
1572 assert!((c.centroid_y - 1.5).abs() < 1e-10);
1573 }
1574
1575 #[test]
1576 fn test_centroid_corner() {
1577 let mut d = vec![0u8; 16];
1578 d[0] = 255;
1579 let data = NDDataBuffer::U8(d);
1580 let c = compute_centroid(&data, 4, 4, 0.0);
1581 assert!((c.centroid_x - 0.0).abs() < 1e-10);
1582 assert!((c.centroid_y - 0.0).abs() < 1e-10);
1583 }
1584
1585 #[test]
1586 fn test_centroid_threshold() {
1587 let mut pixels = vec![5u8; 16];
1589 pixels[2 * 4 + 2] = 100;
1590 let data = NDDataBuffer::U8(pixels);
1591
1592 let c = compute_centroid(&data, 4, 4, 50.0);
1594 assert!((c.centroid_x - 2.0).abs() < 1e-10);
1595 assert!((c.centroid_y - 2.0).abs() < 1e-10);
1596 assert!((c.centroid_total - 100.0).abs() < 1e-10);
1597 }
1598
1599 #[test]
1600 fn test_centroid_higher_moments_symmetric() {
1601 let data = NDDataBuffer::U8(vec![1; 16]);
1603 let c = compute_centroid(&data, 4, 4, 0.0);
1604 assert!(c.skewness_x.abs() < 1e-10);
1606 assert!(c.skewness_y.abs() < 1e-10);
1607 assert!(c.eccentricity.abs() < 1e-10);
1609 }
1610
1611 #[test]
1612 fn test_histogram_basic() {
1613 let data = NDDataBuffer::F64((0..10).map(|x| x as f64).collect());
1615 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 9.0);
1616 assert_eq!(hist.len(), 10);
1617 assert_eq!(below, 0.0);
1618 assert_eq!(above, 0.0);
1619 let total: f64 = hist.iter().sum();
1621 assert!((total - 10.0).abs() < 1e-10);
1622 assert!(entropy.abs() < 1e-10);
1625 }
1626
1627 #[test]
1628 fn test_histogram_below_above() {
1629 let data = NDDataBuffer::F64(vec![-1.0, 0.5, 1.5, 3.0]);
1630 let (hist, below, above, _entropy) = compute_histogram(&data, 2, 0.0, 2.0);
1631 assert_eq!(below, 1.0); assert_eq!(above, 1.0); let total_in_bins: f64 = hist.iter().sum();
1634 assert!((total_in_bins - 2.0).abs() < 1e-10); }
1636
1637 #[test]
1638 fn test_histogram_single_value() {
1639 let data = NDDataBuffer::F64(vec![5.0; 100]);
1640 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 10.0);
1641 assert_eq!(below, 0.0);
1642 assert_eq!(above, 0.0);
1643 let expected = -(100.0f64.ln());
1647 assert!((entropy - expected).abs() < 1e-10);
1648 let total: f64 = hist.iter().sum();
1649 assert!((total - 100.0).abs() < 1e-10);
1650 }
1651
1652 #[test]
1653 fn test_profiles_8x8() {
1654 let mut pixels = vec![0.0f64; 64];
1656 for iy in 0..8 {
1657 for ix in 0..8 {
1658 pixels[iy * 8 + ix] = iy as f64;
1659 }
1660 }
1661 let data = NDDataBuffer::F64(pixels);
1662
1663 let profiles = compute_profiles(
1664 &data, 8, 8, 0.0, 3.5, 3.5, 0, 7, );
1670
1671 assert_eq!(profiles.avg_x.len(), 8);
1673 for &v in &profiles.avg_x {
1674 assert!((v - 3.5).abs() < 1e-10, "avg_x should be 3.5, got {v}");
1675 }
1676
1677 assert_eq!(profiles.avg_y.len(), 8);
1679 for (iy, &v) in profiles.avg_y.iter().enumerate() {
1680 assert!(
1681 (v - iy as f64).abs() < 1e-10,
1682 "avg_y[{iy}] should be {iy}, got {v}"
1683 );
1684 }
1685
1686 assert_eq!(profiles.cursor_x.len(), 8);
1688 for &v in &profiles.cursor_x {
1689 assert!((v - 7.0).abs() < 1e-10);
1690 }
1691
1692 assert_eq!(profiles.cursor_y.len(), 8);
1694 for (iy, &v) in profiles.cursor_y.iter().enumerate() {
1695 assert!((v - iy as f64).abs() < 1e-10);
1696 }
1697
1698 assert_eq!(profiles.centroid_x.len(), 8);
1700 for &v in &profiles.centroid_x {
1701 assert!((v - 4.0).abs() < 1e-10);
1702 }
1703
1704 assert_eq!(profiles.centroid_y.len(), 8);
1706 for (iy, &v) in profiles.centroid_y.iter().enumerate() {
1707 assert!((v - iy as f64).abs() < 1e-10);
1708 }
1709 }
1710
1711 #[test]
1712 fn test_adp14_out_of_range_cursor_clamps_to_edge_not_zeros() {
1713 let mut pixels = vec![0.0f64; 64];
1717 for iy in 0..8 {
1718 for ix in 0..8 {
1719 pixels[iy * 8 + ix] = iy as f64;
1720 }
1721 }
1722 let data = NDDataBuffer::F64(pixels);
1723
1724 let profiles = compute_profiles(
1725 &data, 8, 8, 0.0, 100.0, 100.0, 50, 50, );
1731
1732 assert_eq!(profiles.cursor_x.len(), 8);
1734 for &v in &profiles.cursor_x {
1735 assert!((v - 7.0).abs() < 1e-10, "cursor_x should clamp to row 7");
1736 }
1737 for (iy, &v) in profiles.cursor_y.iter().enumerate() {
1739 assert!(
1740 (v - iy as f64).abs() < 1e-10,
1741 "cursor_y should clamp to col 7"
1742 );
1743 }
1744 for &v in &profiles.centroid_x {
1746 assert!((v - 7.0).abs() < 1e-10, "centroid_x should clamp to row 7");
1747 }
1748 for (iy, &v) in profiles.centroid_y.iter().enumerate() {
1750 assert!(
1751 (v - iy as f64).abs() < 1e-10,
1752 "centroid_y should clamp to col 7"
1753 );
1754 }
1755 }
1756
1757 #[test]
1758 fn test_profiles_threshold() {
1759 let mut pixels = vec![1.0f64; 16];
1761 pixels[1 * 4 + 2] = 10.0;
1762 let data = NDDataBuffer::F64(pixels);
1763
1764 let profiles = compute_profiles(
1765 &data, 4, 4, 5.0, 2.0, 1.0, 0, 0,
1767 );
1768
1769 assert_eq!(profiles.threshold_x.len(), 4);
1771 assert!((profiles.threshold_x[2] - 10.0).abs() < 1e-10);
1772 assert!((profiles.threshold_x[0] - 0.0).abs() < 1e-10);
1774 assert!((profiles.threshold_x[1] - 0.0).abs() < 1e-10);
1775 assert!((profiles.threshold_x[3] - 0.0).abs() < 1e-10);
1776
1777 assert_eq!(profiles.threshold_y.len(), 4);
1779 assert!((profiles.threshold_y[1] - 10.0).abs() < 1e-10);
1780 assert!((profiles.threshold_y[0] - 0.0).abs() < 1e-10);
1781 }
1782
1783 #[test]
1784 fn test_stats_processor_direct() {
1785 let mut proc = StatsProcessor::new();
1786 let pool = NDArrayPool::new(1_000_000);
1787
1788 let mut arr = NDArray::new(vec![NDDimension::new(5)], NDDataType::UInt8);
1789 if let NDDataBuffer::U8(ref mut v) = arr.data {
1790 v[0] = 10;
1791 v[1] = 20;
1792 v[2] = 30;
1793 v[3] = 40;
1794 v[4] = 50;
1795 }
1796
1797 let result = proc.process_array(&arr, &pool);
1798 assert_eq!(result.output_arrays.len(), 1, "stats forwards the array");
1800
1801 let stats = proc.stats_handle().lock().clone();
1802 assert_eq!(stats.min, 10.0);
1803 assert_eq!(stats.max, 50.0);
1804 assert_eq!(stats.mean, 30.0);
1805 }
1806
1807 #[test]
1808 fn test_stats_emits_histogram_and_profile_arrays() {
1809 use ad_core_rs::plugin::runtime::ParamUpdate;
1810 let mut proc = StatsProcessor::new();
1811 let mut base = asyn_rs::port::PortDriverBase::new(
1813 "_stats_scratch_",
1814 1,
1815 asyn_rs::port::PortFlags::default(),
1816 );
1817 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1818 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1819 proc.register_params(&mut base).unwrap();
1820
1821 proc.do_compute_histogram = true;
1822 proc.do_compute_profiles = true;
1823 proc.hist_size = 8;
1824 proc.hist_min = 0.0;
1825 proc.hist_max = 7.0;
1826 let pool = NDArrayPool::new(1_000_000);
1827
1828 let mut arr = NDArray::new(
1829 vec![NDDimension::new(4), NDDimension::new(4)],
1830 NDDataType::UInt8,
1831 );
1832 if let NDDataBuffer::U8(ref mut v) = arr.data {
1833 for (i, val) in v.iter_mut().enumerate() {
1834 *val = (i % 8) as u8;
1835 }
1836 }
1837
1838 let result = proc.process_array(&arr, &pool);
1839 let p = proc.params;
1840 let array_reasons: Vec<usize> = result
1843 .param_updates
1844 .iter()
1845 .filter_map(|u| match u {
1846 ParamUpdate::Float64Array { reason, value, .. } => {
1847 assert!(!value.is_empty(), "array waveform must not be empty");
1848 Some(*reason)
1849 }
1850 _ => None,
1851 })
1852 .collect();
1853 for reason in [
1854 p.hist_array,
1855 p.hist_x_array,
1856 p.profile_average_x,
1857 p.profile_average_y,
1858 p.profile_threshold_x,
1859 p.profile_threshold_y,
1860 p.profile_centroid_x,
1861 p.profile_centroid_y,
1862 p.profile_cursor_x,
1863 p.profile_cursor_y,
1864 ] {
1865 assert!(
1866 array_reasons.contains(&reason),
1867 "missing array update for reason {reason}"
1868 );
1869 }
1870 }
1871
1872 #[test]
1873 fn test_adp13_ndims_gt_2_skips_centroid_and_profiles() {
1874 use ad_core_rs::plugin::runtime::ParamUpdate;
1875 let mut proc = StatsProcessor::new();
1879 let mut base = asyn_rs::port::PortDriverBase::new(
1880 "_stats_adp13_",
1881 1,
1882 asyn_rs::port::PortFlags::default(),
1883 );
1884 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1885 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1886 proc.register_params(&mut base).unwrap();
1887 proc.do_compute_centroid = true;
1888 proc.do_compute_profiles = true;
1889 let pool = NDArrayPool::new(1_000_000);
1890
1891 let mut arr = NDArray::new(
1894 vec![
1895 NDDimension::new(4),
1896 NDDimension::new(4),
1897 NDDimension::new(2),
1898 NDDimension::new(2),
1899 ],
1900 NDDataType::UInt8,
1901 );
1902 if let NDDataBuffer::U8(ref mut v) = arr.data {
1903 for (i, val) in v.iter_mut().enumerate() {
1904 *val = if i % 4 == 3 { 100 } else { 0 };
1905 }
1906 }
1907
1908 let result = proc.process_array(&arr, &pool);
1909 let p = proc.params;
1910
1911 let centroid_x = result.param_updates.iter().find_map(|u| match u {
1913 ParamUpdate::Float64 { reason, value, .. } if *reason == p.centroid_x => Some(*value),
1914 _ => None,
1915 });
1916 assert_eq!(
1917 centroid_x,
1918 Some(0.0),
1919 "centroid_x must not be computed for ndims>2"
1920 );
1921
1922 let profile_reasons = [
1924 p.profile_average_x,
1925 p.profile_average_y,
1926 p.profile_threshold_x,
1927 p.profile_threshold_y,
1928 p.profile_centroid_x,
1929 p.profile_centroid_y,
1930 p.profile_cursor_x,
1931 p.profile_cursor_y,
1932 ];
1933 for u in &result.param_updates {
1934 if let ParamUpdate::Float64Array { reason, .. } = u {
1935 assert!(
1936 !profile_reasons.contains(reason),
1937 "no profile waveform may be emitted for ndims>2"
1938 );
1939 }
1940 }
1941 }
1942
1943 #[test]
1944 fn test_adp30_hist_below_above_emitted_as_int32() {
1945 use ad_core_rs::plugin::runtime::ParamUpdate;
1946 let mut proc = StatsProcessor::new();
1950 let mut base = asyn_rs::port::PortDriverBase::new(
1951 "_stats_adp30_",
1952 1,
1953 asyn_rs::port::PortFlags::default(),
1954 );
1955 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
1956 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
1957 proc.register_params(&mut base).unwrap();
1958 proc.do_compute_histogram = true;
1959 proc.hist_size = 4;
1960 proc.hist_min = 2.0;
1961 proc.hist_max = 5.0;
1962 let pool = NDArrayPool::new(1_000_000);
1963
1964 let mut arr = NDArray::new(
1967 vec![NDDimension::new(4), NDDimension::new(2)],
1968 NDDataType::UInt8,
1969 );
1970 if let NDDataBuffer::U8(ref mut v) = arr.data {
1971 v.copy_from_slice(&[0, 1, 3, 3, 9, 9, 9, 4]);
1972 }
1973
1974 let result = proc.process_array(&arr, &pool);
1975 let p = proc.params;
1976
1977 let below = result.param_updates.iter().find_map(|u| match u {
1978 ParamUpdate::Int32 { reason, value, .. } if *reason == p.hist_below => Some(*value),
1979 _ => None,
1980 });
1981 let above = result.param_updates.iter().find_map(|u| match u {
1982 ParamUpdate::Int32 { reason, value, .. } if *reason == p.hist_above => Some(*value),
1983 _ => None,
1984 });
1985 assert_eq!(
1986 below,
1987 Some(2),
1988 "HIST_BELOW must be emitted as an Int32 count"
1989 );
1990 assert_eq!(
1991 above,
1992 Some(3),
1993 "HIST_ABOVE must be emitted as an Int32 count"
1994 );
1995
1996 for u in &result.param_updates {
1998 if let ParamUpdate::Float64 { reason, .. } = u {
1999 assert!(
2000 *reason != p.hist_below && *reason != p.hist_above,
2001 "HIST_BELOW/HIST_ABOVE must not be emitted as Float64"
2002 );
2003 }
2004 }
2005 }
2006
2007 #[test]
2008 fn test_hist_x_array_uses_bin_count_divisor() {
2009 use ad_core_rs::plugin::runtime::ParamUpdate;
2010 let mut proc = StatsProcessor::new();
2014 let mut base = asyn_rs::port::PortDriverBase::new(
2015 "_stats_histx_",
2016 1,
2017 asyn_rs::port::PortFlags::default(),
2018 );
2019 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
2020 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
2021 proc.register_params(&mut base).unwrap();
2022
2023 proc.do_compute_histogram = true;
2024 proc.hist_size = 256;
2025 proc.hist_min = 0.0;
2026 proc.hist_max = 255.0;
2027 let pool = NDArrayPool::new(1_000_000);
2028
2029 let mut arr = NDArray::new(
2030 vec![NDDimension::new(4), NDDimension::new(4)],
2031 NDDataType::UInt8,
2032 );
2033 if let NDDataBuffer::U8(ref mut v) = arr.data {
2034 for (i, val) in v.iter_mut().enumerate() {
2035 *val = (i * 16) as u8;
2036 }
2037 }
2038
2039 let result = proc.process_array(&arr, &pool);
2040 let hist_x = result
2041 .param_updates
2042 .iter()
2043 .find_map(|u| match u {
2044 ParamUpdate::Float64Array { reason, value, .. }
2045 if *reason == proc.params.hist_x_array =>
2046 {
2047 Some(value.clone())
2048 }
2049 _ => None,
2050 })
2051 .expect("HIST_X_ARRAY must be emitted");
2052
2053 assert_eq!(hist_x.len(), 256, "256 bins");
2054 let scale = 255.0 / 256.0;
2055 assert!(
2056 (hist_x[0] - 0.0).abs() < 1e-9,
2057 "bin 0 X must be histMin (0.0), got {}",
2058 hist_x[0]
2059 );
2060 assert!(
2061 (hist_x[1] - scale).abs() < 1e-9,
2062 "bin 1 X must be {scale}, got {}",
2063 hist_x[1]
2064 );
2065 assert!(
2066 (hist_x[255] - 255.0 * scale).abs() < 1e-9,
2067 "last bin X must be ~254.004 (255*255/256), got {}",
2068 hist_x[255]
2069 );
2070 assert!(
2071 hist_x[255] < 255.0,
2072 "last bin X must be strictly below histMax, got {}",
2073 hist_x[255]
2074 );
2075 }
2076
2077 #[test]
2078 fn test_stats_runtime_end_to_end() {
2079 let pool = Arc::new(NDArrayPool::new(1_000_000));
2080 let wiring = Arc::new(WiringRegistry::new());
2081 let ts_registry = crate::time_series::TsReceiverRegistry::new();
2082 let (handle, stats, _params, _jh) =
2083 create_stats_runtime("STATS_RT", pool, 10, "", wiring, &ts_registry);
2084
2085 handle
2088 .port_runtime()
2089 .port_handle()
2090 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2091 .unwrap();
2092 assert!(
2093 handle.wait_params_applied(std::time::Duration::from_secs(10)),
2094 "data thread did not apply EnableCallbacks"
2095 );
2096
2097 let mut arr = NDArray::new(
2098 vec![NDDimension::new(4), NDDimension::new(4)],
2099 NDDataType::UInt8,
2100 );
2101 if let NDDataBuffer::U8(ref mut v) = arr.data {
2102 for (i, val) in v.iter_mut().enumerate() {
2103 *val = (i + 1) as u8;
2104 }
2105 }
2106
2107 let rt = tokio::runtime::Builder::new_current_thread()
2108 .enable_all()
2109 .build()
2110 .unwrap();
2111 rt.block_on(handle.array_sender().publish(Arc::new(arr)));
2112
2113 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2116 while stats.lock().num_elements != 16 {
2117 assert!(
2118 std::time::Instant::now() < deadline,
2119 "timed out waiting for stats to be computed"
2120 );
2121 std::thread::sleep(std::time::Duration::from_millis(2));
2122 }
2123
2124 let result = stats.lock().clone();
2125 assert_eq!(result.min, 1.0);
2126 assert_eq!(result.max, 16.0);
2127 assert_eq!(result.num_elements, 16);
2128 }
2129}