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