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(
140 data: &NDDataBuffer,
141 dims: &[ad_core_rs::ndarray::NDDimension],
142 bgd_width: usize,
143) -> StatsResult {
144 macro_rules! stats_for {
145 ($vec:expr) => {{
146 let v = $vec;
147 if v.is_empty() {
148 return StatsResult::default();
149 }
150
151 let (min, max, min_idx, max_idx, total, variance);
152
153 #[cfg(feature = "parallel")]
154 {
155 if par_util::should_parallelize(v.len()) {
156 let (pmin, pmax, pmin_idx, pmax_idx, ptotal) =
158 par_util::thread_pool().install(|| {
159 v.par_iter()
160 .enumerate()
161 .fold(
162 || (f64::MAX, f64::MIN, 0usize, 0usize, 0.0f64),
163 |(mn, mx, mn_i, mx_i, s), (i, &elem)| {
164 let f = elem as f64;
165 let (new_mn, new_mn_i) =
166 if f < mn { (f, i) } else { (mn, mn_i) };
167 let (new_mx, new_mx_i) =
168 if f > mx { (f, i) } else { (mx, mx_i) };
169 (new_mn, new_mx, new_mn_i, new_mx_i, s + f)
170 },
171 )
172 .reduce(
173 || (f64::MAX, f64::MIN, 0, 0, 0.0),
174 |(mn1, mx1, mn_i1, mx_i1, s1), (mn2, mx2, mn_i2, mx_i2, s2)| {
175 let (rmn, rmn_i) = if mn1 <= mn2 {
176 (mn1, mn_i1)
177 } else {
178 (mn2, mn_i2)
179 };
180 let (rmx, rmx_i) = if mx1 >= mx2 {
181 (mx1, mx_i1)
182 } else {
183 (mx2, mx_i2)
184 };
185 (rmn, rmx, rmn_i, rmx_i, s1 + s2)
186 },
187 )
188 });
189 min = pmin;
190 max = pmax;
191 min_idx = pmin_idx;
192 max_idx = pmax_idx;
193 total = ptotal;
194 let mean_tmp = total / v.len() as f64;
195 variance = par_util::thread_pool().install(|| {
196 v.par_iter()
197 .map(|&elem| {
198 let d = elem as f64 - mean_tmp;
199 d * d
200 })
201 .sum::<f64>()
202 });
203 } else {
204 let mut lmin = v[0] as f64;
205 let mut lmax = v[0] as f64;
206 let mut lmin_idx: usize = 0;
207 let mut lmax_idx: usize = 0;
208 let mut ltotal = 0.0f64;
209 for (i, &elem) in v.iter().enumerate() {
210 let f = elem as f64;
211 if f < lmin {
212 lmin = f;
213 lmin_idx = i;
214 }
215 if f > lmax {
216 lmax = f;
217 lmax_idx = i;
218 }
219 ltotal += f;
220 }
221 min = lmin;
222 max = lmax;
223 min_idx = lmin_idx;
224 max_idx = lmax_idx;
225 total = ltotal;
226 let mean_tmp = total / v.len() as f64;
227 let mut lvar = 0.0f64;
228 for &elem in v.iter() {
229 let d = elem as f64 - mean_tmp;
230 lvar += d * d;
231 }
232 variance = lvar;
233 }
234 }
235
236 #[cfg(not(feature = "parallel"))]
237 {
238 let mut lmin = v[0] as f64;
239 let mut lmax = v[0] as f64;
240 let mut lmin_idx: usize = 0;
241 let mut lmax_idx: usize = 0;
242 let mut ltotal = 0.0f64;
243 for (i, &elem) in v.iter().enumerate() {
244 let f = elem as f64;
245 if f < lmin {
246 lmin = f;
247 lmin_idx = i;
248 }
249 if f > lmax {
250 lmax = f;
251 lmax_idx = i;
252 }
253 ltotal += f;
254 }
255 min = lmin;
256 max = lmax;
257 min_idx = lmin_idx;
258 max_idx = lmax_idx;
259 total = ltotal;
260 let mean_tmp = total / v.len() as f64;
261 let mut lvar = 0.0f64;
262 for &elem in v.iter() {
263 let d = elem as f64 - mean_tmp;
264 lvar += d * d;
265 }
266 variance = lvar;
267 }
268
269 let mean = total / v.len() as f64;
270 let sigma = (variance / v.len() as f64).sqrt();
271 let x_size = dims.first().map_or(v.len(), |d| d.size);
272
273 let net = if bgd_width > 0 && dims.len() >= 2 {
275 let y_size = dims[1].size;
276 let mut bgd_sum = 0.0f64;
277 let mut bgd_count = 0usize;
278 for iy in 0..y_size {
279 for ix in 0..x_size {
280 let is_edge = ix < bgd_width
281 || ix >= x_size.saturating_sub(bgd_width)
282 || iy < bgd_width
283 || iy >= y_size.saturating_sub(bgd_width);
284 if is_edge {
285 let idx = iy * x_size + ix;
286 if idx < v.len() {
287 bgd_sum += v[idx] as f64;
288 bgd_count += 1;
289 }
290 }
291 }
292 }
293 let bgd_avg = if bgd_count > 0 {
294 bgd_sum / bgd_count as f64
295 } else {
296 0.0
297 };
298 total - bgd_avg * v.len() as f64
299 } else {
300 total
301 };
302
303 StatsResult {
304 min,
305 max,
306 mean,
307 sigma,
308 total,
309 net,
310 num_elements: v.len(),
311 min_x: if x_size > 0 { min_idx % x_size } else { 0 },
312 min_y: if x_size > 0 { min_idx / x_size } else { 0 },
313 max_x: if x_size > 0 { max_idx % x_size } else { 0 },
314 max_y: if x_size > 0 { max_idx / x_size } else { 0 },
315 ..StatsResult::default()
316 }
317 }};
318 }
319
320 match data {
321 NDDataBuffer::I8(v) => stats_for!(v),
322 NDDataBuffer::U8(v) => stats_for!(v),
323 NDDataBuffer::I16(v) => stats_for!(v),
324 NDDataBuffer::U16(v) => stats_for!(v),
325 NDDataBuffer::I32(v) => stats_for!(v),
326 NDDataBuffer::U32(v) => stats_for!(v),
327 NDDataBuffer::I64(v) => stats_for!(v),
328 NDDataBuffer::U64(v) => stats_for!(v),
329 NDDataBuffer::F32(v) => stats_for!(v),
330 NDDataBuffer::F64(v) => stats_for!(v),
331 }
332}
333
334pub fn compute_centroid(
338 data: &NDDataBuffer,
339 x_size: usize,
340 y_size: usize,
341 threshold: f64,
342) -> CentroidResult {
343 let n = x_size * y_size;
344 if n == 0 || data.len() < n {
345 return CentroidResult::default();
346 }
347
348 let vals: Vec<f64> = (0..n).map(|i| data.get_as_f64(i).unwrap_or(0.0)).collect();
350
351 let (m00, m10, m01);
353
354 #[cfg(feature = "parallel")]
355 {
356 if par_util::should_parallelize(n) {
357 let xs = x_size;
358 let thr = threshold;
359 let (pm00, pm10, pm01) = par_util::thread_pool().install(|| {
360 vals.par_iter()
361 .enumerate()
362 .fold(
363 || (0.0f64, 0.0f64, 0.0f64),
364 |(s00, s10, s01), (i, &val)| {
365 if val < thr {
366 return (s00, s10, s01);
367 }
368 let ix = i % xs;
369 let iy = i / xs;
370 (s00 + val, s10 + val * ix as f64, s01 + val * iy as f64)
371 },
372 )
373 .reduce(
374 || (0.0, 0.0, 0.0),
375 |(a0, a1, a2), (b0, b1, b2)| (a0 + b0, a1 + b1, a2 + b2),
376 )
377 });
378 m00 = pm00;
379 m10 = pm10;
380 m01 = pm01;
381 } else {
382 let mut lm00 = 0.0f64;
383 let mut lm10 = 0.0f64;
384 let mut lm01 = 0.0f64;
385 for iy in 0..y_size {
386 for ix in 0..x_size {
387 let val = vals[iy * x_size + ix];
388 if val < threshold {
389 continue;
390 }
391 lm00 += val;
392 lm10 += val * ix as f64;
393 lm01 += val * iy as f64;
394 }
395 }
396 m00 = lm00;
397 m10 = lm10;
398 m01 = lm01;
399 }
400 }
401
402 #[cfg(not(feature = "parallel"))]
403 {
404 let mut lm00 = 0.0f64;
405 let mut lm10 = 0.0f64;
406 let mut lm01 = 0.0f64;
407 for iy in 0..y_size {
408 for ix in 0..x_size {
409 let val = vals[iy * x_size + ix];
410 if val < threshold {
411 continue;
412 }
413 lm00 += val;
414 lm10 += val * ix as f64;
415 lm01 += val * iy as f64;
416 }
417 }
418 m00 = lm00;
419 m10 = lm10;
420 m01 = lm01;
421 }
422
423 if m00 == 0.0 {
424 return CentroidResult::default();
425 }
426
427 let cx = m10 / m00;
428 let cy = m01 / m00;
429
430 let (mu20, mu02, mu11, m30_central, m03_central, m40_central, m04_central);
432
433 #[cfg(feature = "parallel")]
434 {
435 if par_util::should_parallelize(n) {
436 let xs = x_size;
437 let thr = threshold;
438 let (p20, p02, p11, p30, p03, p40, p04) = par_util::thread_pool().install(|| {
439 vals.par_iter()
440 .enumerate()
441 .fold(
442 || (0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64),
443 |(s20, s02, s11, s30, s03, s40, s04), (i, &val)| {
444 if val < thr {
445 return (s20, s02, s11, s30, s03, s40, s04);
446 }
447 let ix = i % xs;
448 let iy = i / xs;
449 let dx = ix as f64 - cx;
450 let dy = iy as f64 - cy;
451 let dx2 = dx * dx;
452 let dy2 = dy * dy;
453 (
454 s20 + val * dx2,
455 s02 + val * dy2,
456 s11 + val * dx * dy,
457 s30 + val * dx2 * dx,
458 s03 + val * dy2 * dy,
459 s40 + val * dx2 * dx2,
460 s04 + val * dy2 * dy2,
461 )
462 },
463 )
464 .reduce(
465 || (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
466 |(a0, a1, a2, a3, a4, a5, a6), (b0, b1, b2, b3, b4, b5, b6)| {
467 (
468 a0 + b0,
469 a1 + b1,
470 a2 + b2,
471 a3 + b3,
472 a4 + b4,
473 a5 + b5,
474 a6 + b6,
475 )
476 },
477 )
478 });
479 mu20 = p20;
480 mu02 = p02;
481 mu11 = p11;
482 m30_central = p30;
483 m03_central = p03;
484 m40_central = p40;
485 m04_central = p04;
486 } else {
487 let mut l20 = 0.0f64;
488 let mut l02 = 0.0f64;
489 let mut l11 = 0.0f64;
490 let mut l30 = 0.0f64;
491 let mut l03 = 0.0f64;
492 let mut l40 = 0.0f64;
493 let mut l04 = 0.0f64;
494 for iy in 0..y_size {
495 for ix in 0..x_size {
496 let val = vals[iy * x_size + ix];
497 if val < threshold {
498 continue;
499 }
500 let dx = ix as f64 - cx;
501 let dy = iy as f64 - cy;
502 let dx2 = dx * dx;
503 let dy2 = dy * dy;
504 l20 += val * dx2;
505 l02 += val * dy2;
506 l11 += val * dx * dy;
507 l30 += val * dx2 * dx;
508 l03 += val * dy2 * dy;
509 l40 += val * dx2 * dx2;
510 l04 += val * dy2 * dy2;
511 }
512 }
513 mu20 = l20;
514 mu02 = l02;
515 mu11 = l11;
516 m30_central = l30;
517 m03_central = l03;
518 m40_central = l40;
519 m04_central = l04;
520 }
521 }
522
523 #[cfg(not(feature = "parallel"))]
524 {
525 let mut l20 = 0.0f64;
526 let mut l02 = 0.0f64;
527 let mut l11 = 0.0f64;
528 let mut l30 = 0.0f64;
529 let mut l03 = 0.0f64;
530 let mut l40 = 0.0f64;
531 let mut l04 = 0.0f64;
532 for iy in 0..y_size {
533 for ix in 0..x_size {
534 let val = vals[iy * x_size + ix];
535 if val < threshold {
536 continue;
537 }
538 let dx = ix as f64 - cx;
539 let dy = iy as f64 - cy;
540 let dx2 = dx * dx;
541 let dy2 = dy * dy;
542 l20 += val * dx2;
543 l02 += val * dy2;
544 l11 += val * dx * dy;
545 l30 += val * dx2 * dx;
546 l03 += val * dy2 * dy;
547 l40 += val * dx2 * dx2;
548 l04 += val * dy2 * dy2;
549 }
550 }
551 mu20 = l20;
552 mu02 = l02;
553 mu11 = l11;
554 m30_central = l30;
555 m03_central = l03;
556 m40_central = l40;
557 m04_central = l04;
558 }
559
560 let sigma_x = (mu20 / m00).sqrt();
561 let sigma_y = (mu02 / m00).sqrt();
562 let sigma_xy = if sigma_x > 0.0 && sigma_y > 0.0 {
563 (mu11 / m00) / (sigma_x * sigma_y)
564 } else {
565 0.0
566 };
567
568 let skewness_x = if sigma_x > 0.0 {
570 m30_central / (m00 * sigma_x.powi(3))
571 } else {
572 0.0
573 };
574 let skewness_y = if sigma_y > 0.0 {
575 m03_central / (m00 * sigma_y.powi(3))
576 } else {
577 0.0
578 };
579
580 let kurtosis_x = if sigma_x > 0.0 {
582 m40_central / (m00 * sigma_x.powi(4)) - 3.0
583 } else {
584 0.0
585 };
586 let kurtosis_y = if sigma_y > 0.0 {
587 m04_central / (m00 * sigma_y.powi(4)) - 3.0
588 } else {
589 0.0
590 };
591
592 let denom = mu20 + mu02;
595 let eccentricity = if denom > 0.0 {
596 ((mu20 - mu02).powi(2) - 4.0 * mu11.powi(2)) / denom.powi(2)
597 } else {
598 0.0
599 };
600
601 let orientation = 0.5 * (2.0 * mu11).atan2(mu20 - mu02) * 180.0 / std::f64::consts::PI;
603
604 CentroidResult {
605 centroid_x: cx,
606 centroid_y: cy,
607 sigma_x,
608 sigma_y,
609 sigma_xy,
610 centroid_total: m00,
611 skewness_x,
612 skewness_y,
613 kurtosis_x,
614 kurtosis_y,
615 eccentricity,
616 orientation,
617 }
618}
619
620pub fn compute_histogram(
629 data: &NDDataBuffer,
630 hist_size: usize,
631 hist_min: f64,
632 hist_max: f64,
633) -> (Vec<f64>, f64, f64, f64) {
634 if hist_size == 0 || hist_max <= hist_min {
635 return (vec![], 0.0, 0.0, 0.0);
636 }
637
638 let mut histogram = vec![0.0f64; hist_size];
639 let mut below = 0.0f64;
640 let mut above = 0.0f64;
641 let range = hist_max - hist_min;
642 let n = data.len();
643
644 #[cfg(feature = "parallel")]
645 let use_parallel = par_util::should_parallelize(n);
646 #[cfg(not(feature = "parallel"))]
647 let use_parallel = false;
648
649 if use_parallel {
650 #[cfg(feature = "parallel")]
651 {
652 let vals: Vec<f64> = (0..n).map(|i| data.get_as_f64(i).unwrap_or(0.0)).collect();
653 let chunk_size = (n / rayon::current_num_threads().max(1)).max(1024);
654 let hs = hist_size;
655 let hmin = hist_min;
656 let hmax = hist_max;
657 let rng = range;
658 let chunk_results: Vec<(Vec<f64>, f64, f64)> = par_util::thread_pool().install(|| {
659 vals.par_chunks(chunk_size)
660 .map(|chunk| {
661 let mut local_hist = vec![0.0f64; hs];
662 let mut local_below = 0.0f64;
663 let mut local_above = 0.0f64;
664 for &val in chunk {
665 if val < hmin {
666 local_below += 1.0;
667 } else if val > hmax {
668 local_above += 1.0;
669 } else {
670 let bin = ((val - hmin) * (hs - 1) as f64 / rng + 0.5) as usize;
671 let bin = bin.min(hs - 1);
672 local_hist[bin] += 1.0;
673 }
674 }
675 (local_hist, local_below, local_above)
676 })
677 .collect()
678 });
679 for (local_hist, local_below, local_above) in chunk_results {
680 below += local_below;
681 above += local_above;
682 for (i, &count) in local_hist.iter().enumerate() {
683 histogram[i] += count;
684 }
685 }
686 }
687 } else {
688 for i in 0..n {
689 let val = data.get_as_f64(i).unwrap_or(0.0);
690 if val < hist_min {
691 below += 1.0;
692 } else if val > hist_max {
693 above += 1.0;
694 } else {
695 let bin = ((val - hist_min) * (hist_size - 1) as f64 / range + 0.5) as usize;
696 let bin = bin.min(hist_size - 1);
697 histogram[bin] += 1.0;
698 }
699 }
700 }
701
702 let n_elements = data.len() as f64;
705 let entropy = if n_elements > 0.0 {
706 let mut ent = 0.0f64;
707 for &count in &histogram {
708 let c = if count <= 0.0 { 1.0 } else { count };
709 ent += c * c.ln();
710 }
711 -ent / n_elements
712 } else {
713 0.0
714 };
715
716 (histogram, below, above, entropy)
717}
718
719pub fn compute_profiles(
726 data: &NDDataBuffer,
727 x_size: usize,
728 y_size: usize,
729 threshold: f64,
730 centroid_x: f64,
731 centroid_y: f64,
732 cursor_x: usize,
733 cursor_y: usize,
734) -> ProfileResult {
735 if x_size == 0 || y_size == 0 || data.len() < x_size * y_size {
736 return ProfileResult::default();
737 }
738
739 let mut avg_x = vec![0.0f64; x_size];
740 let mut avg_y = vec![0.0f64; y_size];
741 let mut thresh_x_sum = vec![0.0f64; x_size];
742 let mut thresh_x_cnt = vec![0usize; x_size];
743 let mut thresh_y_sum = vec![0.0f64; y_size];
744 let mut thresh_y_cnt = vec![0usize; y_size];
745
746 for iy in 0..y_size {
748 for ix in 0..x_size {
749 let val = data.get_as_f64(iy * x_size + ix).unwrap_or(0.0);
750 avg_x[ix] += val;
751 avg_y[iy] += val;
752 if val >= threshold {
753 thresh_x_sum[ix] += val;
754 thresh_x_cnt[ix] += 1;
755 thresh_y_sum[iy] += val;
756 thresh_y_cnt[iy] += 1;
757 }
758 }
759 }
760
761 for ix in 0..x_size {
763 avg_x[ix] /= y_size as f64;
764 }
765 for iy in 0..y_size {
766 avg_y[iy] /= x_size as f64;
767 }
768
769 let threshold_x: Vec<f64> = thresh_x_sum
771 .iter()
772 .zip(thresh_x_cnt.iter())
773 .map(|(&s, &c)| if c > 0 { s / c as f64 } else { 0.0 })
774 .collect();
775 let threshold_y: Vec<f64> = thresh_y_sum
776 .iter()
777 .zip(thresh_y_cnt.iter())
778 .map(|(&s, &c)| if c > 0 { s / c as f64 } else { 0.0 })
779 .collect();
780
781 let cy_row = (centroid_y + 0.5) as usize;
783 let cx_col = (centroid_x + 0.5) as usize;
784
785 let centroid_x_profile = if cy_row < y_size {
786 (0..x_size)
787 .map(|ix| data.get_as_f64(cy_row * x_size + ix).unwrap_or(0.0))
788 .collect()
789 } else {
790 vec![0.0; x_size]
791 };
792
793 let centroid_y_profile = if cx_col < x_size {
794 (0..y_size)
795 .map(|iy| data.get_as_f64(iy * x_size + cx_col).unwrap_or(0.0))
796 .collect()
797 } else {
798 vec![0.0; y_size]
799 };
800
801 let cursor_x_profile = if cursor_y < y_size {
803 (0..x_size)
804 .map(|ix| data.get_as_f64(cursor_y * x_size + ix).unwrap_or(0.0))
805 .collect()
806 } else {
807 vec![0.0; x_size]
808 };
809
810 let cursor_y_profile = if cursor_x < x_size {
811 (0..y_size)
812 .map(|iy| data.get_as_f64(iy * x_size + cursor_x).unwrap_or(0.0))
813 .collect()
814 } else {
815 vec![0.0; y_size]
816 };
817
818 ProfileResult {
819 avg_x,
820 avg_y,
821 threshold_x,
822 threshold_y,
823 centroid_x: centroid_x_profile,
824 centroid_y: centroid_y_profile,
825 cursor_x: cursor_x_profile,
826 cursor_y: cursor_y_profile,
827 }
828}
829
830pub struct StatsProcessor {
832 latest_stats: Arc<Mutex<StatsResult>>,
833 do_compute_statistics: bool,
834 do_compute_centroid: bool,
835 do_compute_histogram: bool,
836 do_compute_profiles: bool,
837 bgd_width: usize,
838 centroid_threshold: f64,
839 cursor_x: usize,
840 cursor_y: usize,
841 hist_size: usize,
842 hist_min: f64,
843 hist_max: f64,
844 params: NDStatsParams,
845 params_out: Arc<Mutex<NDStatsParams>>,
847 ts_sender: Option<crate::time_series::TimeSeriesSender>,
849}
850
851impl StatsProcessor {
852 pub fn new() -> Self {
853 Self {
854 latest_stats: Arc::new(Mutex::new(StatsResult::default())),
855 do_compute_statistics: true,
856 do_compute_centroid: true,
857 do_compute_histogram: false,
858 do_compute_profiles: false,
859 bgd_width: 0,
860 centroid_threshold: 0.0,
861 cursor_x: 0,
862 cursor_y: 0,
863 hist_size: 256,
864 hist_min: 0.0,
865 hist_max: 255.0,
866 params: NDStatsParams::default(),
867 params_out: Arc::new(Mutex::new(NDStatsParams::default())),
868 ts_sender: None,
869 }
870 }
871
872 pub fn stats_handle(&self) -> Arc<Mutex<StatsResult>> {
874 self.latest_stats.clone()
875 }
876
877 pub fn params_handle(&self) -> Arc<Mutex<NDStatsParams>> {
879 self.params_out.clone()
880 }
881
882 pub fn set_ts_sender(&mut self, sender: crate::time_series::TimeSeriesSender) {
884 self.ts_sender = Some(sender);
885 }
886}
887
888impl Default for StatsProcessor {
889 fn default() -> Self {
890 Self::new()
891 }
892}
893
894impl NDPluginProcess for StatsProcessor {
895 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
896 let p = &self.params;
897 let info = array.info();
898
899 let mut result = if self.do_compute_statistics {
900 compute_stats(&array.data, &array.dims, self.bgd_width)
901 } else {
902 StatsResult::default()
903 };
904
905 let mut centroid = CentroidResult::default();
907 if self.do_compute_centroid {
908 if info.color_size == 1 && array.dims.len() >= 2 {
909 centroid = compute_centroid(
910 &array.data,
911 info.x_size,
912 info.y_size,
913 self.centroid_threshold,
914 );
915 }
916 }
917
918 if self.do_compute_histogram {
920 let (histogram, below, above, entropy) =
921 compute_histogram(&array.data, self.hist_size, self.hist_min, self.hist_max);
922 result.histogram = histogram;
923 result.hist_below = below;
924 result.hist_above = above;
925 result.hist_entropy = entropy;
926 }
927
928 if self.do_compute_profiles && info.color_size == 1 && array.dims.len() >= 2 {
930 let profiles = compute_profiles(
931 &array.data,
932 info.x_size,
933 info.y_size,
934 self.centroid_threshold,
935 centroid.centroid_x,
936 centroid.centroid_y,
937 self.cursor_x,
938 self.cursor_y,
939 );
940 result.profile_avg_x = profiles.avg_x;
941 result.profile_avg_y = profiles.avg_y;
942 result.profile_threshold_x = profiles.threshold_x;
943 result.profile_threshold_y = profiles.threshold_y;
944 result.profile_centroid_x = profiles.centroid_x;
945 result.profile_centroid_y = profiles.centroid_y;
946 result.profile_cursor_x = profiles.cursor_x;
947 result.profile_cursor_y = profiles.cursor_y;
948 }
949
950 if info.color_size == 1 && array.dims.len() >= 2 {
952 let cx = self.cursor_x;
953 let cy = self.cursor_y;
954 if cx < info.x_size && cy < info.y_size {
955 result.cursor_value = array.data.get_as_f64(cy * info.x_size + cx).unwrap_or(0.0);
956 }
957 }
958
959 let updates = vec![
960 ParamUpdate::float64(p.min_value, result.min),
961 ParamUpdate::float64(p.max_value, result.max),
962 ParamUpdate::float64(p.mean_value, result.mean),
963 ParamUpdate::float64(p.sigma_value, result.sigma),
964 ParamUpdate::float64(p.total, result.total),
965 ParamUpdate::float64(p.net, result.net),
966 ParamUpdate::float64(p.min_x, result.min_x as f64),
967 ParamUpdate::float64(p.min_y, result.min_y as f64),
968 ParamUpdate::float64(p.max_x, result.max_x as f64),
969 ParamUpdate::float64(p.max_y, result.max_y as f64),
970 ParamUpdate::float64(p.centroid_x, centroid.centroid_x),
971 ParamUpdate::float64(p.centroid_y, centroid.centroid_y),
972 ParamUpdate::float64(p.sigma_x, centroid.sigma_x),
973 ParamUpdate::float64(p.sigma_y, centroid.sigma_y),
974 ParamUpdate::float64(p.sigma_xy, centroid.sigma_xy),
975 ParamUpdate::float64(p.centroid_total, centroid.centroid_total),
976 ParamUpdate::float64(p.skewness_x, centroid.skewness_x),
977 ParamUpdate::float64(p.skewness_y, centroid.skewness_y),
978 ParamUpdate::float64(p.kurtosis_x, centroid.kurtosis_x),
979 ParamUpdate::float64(p.kurtosis_y, centroid.kurtosis_y),
980 ParamUpdate::float64(p.eccentricity, centroid.eccentricity),
981 ParamUpdate::float64(p.orientation, centroid.orientation),
982 ParamUpdate::float64(p.hist_below, result.hist_below),
983 ParamUpdate::float64(p.hist_above, result.hist_above),
984 ParamUpdate::float64(p.hist_entropy, result.hist_entropy),
985 ParamUpdate::float64(p.cursor_val, result.cursor_value),
986 ParamUpdate::int32(p.profile_size_x, info.x_size as i32),
987 ParamUpdate::int32(p.profile_size_y, info.y_size as i32),
988 ];
989
990 if let Some(ref sender) = self.ts_sender {
992 let ts_data = crate::time_series::TimeSeriesData {
993 values: vec![
994 result.min,
995 result.min_x as f64,
996 result.min_y as f64,
997 result.max,
998 result.max_x as f64,
999 result.max_y as f64,
1000 result.mean,
1001 result.sigma,
1002 result.total,
1003 result.net,
1004 centroid.centroid_total,
1005 centroid.centroid_x,
1006 centroid.centroid_y,
1007 centroid.sigma_x,
1008 centroid.sigma_y,
1009 centroid.sigma_xy,
1010 centroid.skewness_x,
1011 centroid.skewness_y,
1012 centroid.kurtosis_x,
1013 centroid.kurtosis_y,
1014 centroid.eccentricity,
1015 centroid.orientation,
1016 array.timestamp.as_f64(),
1017 ],
1018 };
1019 let _ = sender.try_send(ts_data);
1020 }
1021
1022 *self.latest_stats.lock() = result;
1023 ProcessResult {
1025 output_arrays: vec![Arc::new(array.clone())],
1026 param_updates: updates,
1027 scatter_index: None,
1028 }
1029 }
1030
1031 fn plugin_type(&self) -> &str {
1032 "NDPluginStats"
1033 }
1034
1035 fn register_params(
1036 &mut self,
1037 base: &mut PortDriverBase,
1038 ) -> Result<(), asyn_rs::error::AsynError> {
1039 self.params.compute_statistics =
1040 base.create_param("COMPUTE_STATISTICS", ParamType::Int32)?;
1041 base.set_int32_param(self.params.compute_statistics, 0, 1)?;
1042
1043 self.params.bgd_width = base.create_param("BGD_WIDTH", ParamType::Int32)?;
1044 self.params.min_value = base.create_param("MIN_VALUE", ParamType::Float64)?;
1045 self.params.max_value = base.create_param("MAX_VALUE", ParamType::Float64)?;
1046 self.params.mean_value = base.create_param("MEAN_VALUE", ParamType::Float64)?;
1047 self.params.sigma_value = base.create_param("SIGMA_VALUE", ParamType::Float64)?;
1048 self.params.total = base.create_param("TOTAL", ParamType::Float64)?;
1049 self.params.net = base.create_param("NET", ParamType::Float64)?;
1050 self.params.min_x = base.create_param("MIN_X", ParamType::Float64)?;
1051 self.params.min_y = base.create_param("MIN_Y", ParamType::Float64)?;
1052 self.params.max_x = base.create_param("MAX_X", ParamType::Float64)?;
1053 self.params.max_y = base.create_param("MAX_Y", ParamType::Float64)?;
1054
1055 self.params.compute_centroid = base.create_param("COMPUTE_CENTROID", ParamType::Int32)?;
1056 base.set_int32_param(self.params.compute_centroid, 0, 1)?;
1057
1058 self.params.centroid_threshold =
1059 base.create_param("CENTROID_THRESHOLD", ParamType::Float64)?;
1060 self.params.centroid_total = base.create_param("CENTROID_TOTAL", ParamType::Float64)?;
1061 self.params.centroid_x = base.create_param("CENTROIDX_VALUE", ParamType::Float64)?;
1062 self.params.centroid_y = base.create_param("CENTROIDY_VALUE", ParamType::Float64)?;
1063 self.params.sigma_x = base.create_param("SIGMAX_VALUE", ParamType::Float64)?;
1064 self.params.sigma_y = base.create_param("SIGMAY_VALUE", ParamType::Float64)?;
1065 self.params.sigma_xy = base.create_param("SIGMAXY_VALUE", ParamType::Float64)?;
1066 self.params.skewness_x = base.create_param("SKEWNESSX_VALUE", ParamType::Float64)?;
1067 self.params.skewness_y = base.create_param("SKEWNESSY_VALUE", ParamType::Float64)?;
1068 self.params.kurtosis_x = base.create_param("KURTOSISX_VALUE", ParamType::Float64)?;
1069 self.params.kurtosis_y = base.create_param("KURTOSISY_VALUE", ParamType::Float64)?;
1070 self.params.eccentricity = base.create_param("ECCENTRICITY_VALUE", ParamType::Float64)?;
1071 self.params.orientation = base.create_param("ORIENTATION_VALUE", ParamType::Float64)?;
1072
1073 self.params.compute_histogram = base.create_param("COMPUTE_HISTOGRAM", ParamType::Int32)?;
1074 self.params.hist_size = base.create_param("HIST_SIZE", ParamType::Int32)?;
1075 base.set_int32_param(self.params.hist_size, 0, 256)?;
1076 self.params.hist_min = base.create_param("HIST_MIN", ParamType::Float64)?;
1077 self.params.hist_max = base.create_param("HIST_MAX", ParamType::Float64)?;
1078 base.set_float64_param(self.params.hist_max, 0, 255.0)?;
1079 self.params.hist_below = base.create_param("HIST_BELOW", ParamType::Float64)?;
1080 self.params.hist_above = base.create_param("HIST_ABOVE", ParamType::Float64)?;
1081 self.params.hist_entropy = base.create_param("HIST_ENTROPY", ParamType::Float64)?;
1082
1083 self.params.compute_profiles = base.create_param("COMPUTE_PROFILES", ParamType::Int32)?;
1084 self.params.cursor_x = base.create_param("CURSOR_X", ParamType::Int32)?;
1085 base.set_int32_param(self.params.cursor_x, 0, 0)?;
1086 self.params.cursor_y = base.create_param("CURSOR_Y", ParamType::Int32)?;
1087 base.set_int32_param(self.params.cursor_y, 0, 0)?;
1088
1089 self.params.cursor_val = base.create_param("CURSOR_VAL", ParamType::Float64)?;
1090 self.params.profile_size_x = base.create_param("PROFILE_SIZE_X", ParamType::Int32)?;
1091 self.params.profile_size_y = base.create_param("PROFILE_SIZE_Y", ParamType::Int32)?;
1092
1093 self.params.skewx_value = base.create_param("SKEWX_VALUE", ParamType::Float64)?;
1094 self.params.skewy_value = base.create_param("SKEWY_VALUE", ParamType::Float64)?;
1095 self.params.profile_average_x =
1096 base.create_param("PROFILE_AVERAGE_X", ParamType::Float64Array)?;
1097 self.params.profile_average_y =
1098 base.create_param("PROFILE_AVERAGE_Y", ParamType::Float64Array)?;
1099 self.params.profile_threshold_x =
1100 base.create_param("PROFILE_THRESHOLD_X", ParamType::Float64Array)?;
1101 self.params.profile_threshold_y =
1102 base.create_param("PROFILE_THRESHOLD_Y", ParamType::Float64Array)?;
1103 self.params.profile_centroid_x =
1104 base.create_param("PROFILE_CENTROID_X", ParamType::Float64Array)?;
1105 self.params.profile_centroid_y =
1106 base.create_param("PROFILE_CENTROID_Y", ParamType::Float64Array)?;
1107 self.params.profile_cursor_x =
1108 base.create_param("PROFILE_CURSOR_X", ParamType::Float64Array)?;
1109 self.params.profile_cursor_y =
1110 base.create_param("PROFILE_CURSOR_Y", ParamType::Float64Array)?;
1111 self.params.hist_array = base.create_param("HIST_ARRAY", ParamType::Float64Array)?;
1112 self.params.hist_x_array = base.create_param("HIST_X_ARRAY", ParamType::Float64Array)?;
1113
1114 *self.params_out.lock() = self.params;
1116
1117 Ok(())
1118 }
1119
1120 fn on_param_change(
1121 &mut self,
1122 reason: usize,
1123 snapshot: &PluginParamSnapshot,
1124 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
1125 let p = &self.params;
1126 if reason == p.compute_statistics {
1127 self.do_compute_statistics = snapshot.value.as_i32() != 0;
1128 } else if reason == p.compute_centroid {
1129 self.do_compute_centroid = snapshot.value.as_i32() != 0;
1130 } else if reason == p.compute_histogram {
1131 self.do_compute_histogram = snapshot.value.as_i32() != 0;
1132 } else if reason == p.compute_profiles {
1133 self.do_compute_profiles = snapshot.value.as_i32() != 0;
1134 } else if reason == p.bgd_width {
1135 self.bgd_width = snapshot.value.as_i32().max(0) as usize;
1136 } else if reason == p.centroid_threshold {
1137 self.centroid_threshold = snapshot.value.as_f64();
1138 } else if reason == p.cursor_x {
1139 self.cursor_x = snapshot.value.as_i32().max(0) as usize;
1140 } else if reason == p.cursor_y {
1141 self.cursor_y = snapshot.value.as_i32().max(0) as usize;
1142 } else if reason == p.hist_size {
1143 self.hist_size = (snapshot.value.as_i32().max(1)) as usize;
1144 } else if reason == p.hist_min {
1145 self.hist_min = snapshot.value.as_f64();
1146 } else if reason == p.hist_max {
1147 self.hist_max = snapshot.value.as_f64();
1148 }
1149 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
1150 }
1151}
1152
1153pub fn create_stats_runtime(
1159 port_name: &str,
1160 pool: Arc<NDArrayPool>,
1161 queue_size: usize,
1162 ndarray_port: &str,
1163 wiring: Arc<WiringRegistry>,
1164 ts_registry: &crate::time_series::TsReceiverRegistry,
1165) -> (
1166 PluginRuntimeHandle,
1167 Arc<Mutex<StatsResult>>,
1168 NDStatsParams,
1169 std::thread::JoinHandle<()>,
1170) {
1171 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
1172
1173 let mut processor = StatsProcessor::new();
1174 processor.set_ts_sender(ts_tx);
1175 let stats_handle = processor.stats_handle();
1176 let params_handle = processor.params_handle();
1177
1178 let (plugin_handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
1179 port_name,
1180 processor,
1181 pool,
1182 queue_size,
1183 ndarray_port,
1184 wiring,
1185 );
1186
1187 let stats_params = *params_handle.lock();
1188
1189 let channel_names: Vec<String> = crate::time_series::STATS_TS_CHANNEL_NAMES
1191 .iter()
1192 .map(|s| s.to_string())
1193 .collect();
1194 ts_registry.store(port_name, ts_rx, channel_names);
1195
1196 (plugin_handle, stats_handle, stats_params, data_jh)
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201 use super::*;
1202 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1203
1204 #[test]
1205 fn test_compute_stats_u8() {
1206 let dims = vec![NDDimension::new(5)];
1207 let data = NDDataBuffer::U8(vec![10, 20, 30, 40, 50]);
1208 let stats = compute_stats(&data, &dims, 0);
1209 assert_eq!(stats.min, 10.0);
1210 assert_eq!(stats.max, 50.0);
1211 assert_eq!(stats.mean, 30.0);
1212 assert_eq!(stats.total, 150.0);
1213 assert_eq!(stats.num_elements, 5);
1214 }
1215
1216 #[test]
1217 fn test_compute_stats_sigma() {
1218 let dims = vec![NDDimension::new(8)];
1219 let data = NDDataBuffer::F64(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
1220 let stats = compute_stats(&data, &dims, 0);
1221 assert!((stats.mean - 5.0).abs() < 1e-10);
1222 assert!((stats.sigma - 2.0).abs() < 1e-10);
1223 }
1224
1225 #[test]
1226 fn test_compute_stats_u16() {
1227 let dims = vec![NDDimension::new(3)];
1228 let data = NDDataBuffer::U16(vec![100, 200, 300]);
1229 let stats = compute_stats(&data, &dims, 0);
1230 assert_eq!(stats.min, 100.0);
1231 assert_eq!(stats.max, 300.0);
1232 assert_eq!(stats.mean, 200.0);
1233 }
1234
1235 #[test]
1236 fn test_compute_stats_f64() {
1237 let dims = vec![NDDimension::new(3)];
1238 let data = NDDataBuffer::F64(vec![1.5, 2.5, 3.5]);
1239 let stats = compute_stats(&data, &dims, 0);
1240 assert!((stats.min - 1.5).abs() < 1e-10);
1241 assert!((stats.max - 3.5).abs() < 1e-10);
1242 assert!((stats.mean - 2.5).abs() < 1e-10);
1243 }
1244
1245 #[test]
1246 fn test_compute_stats_single_element() {
1247 let dims = vec![NDDimension::new(1)];
1248 let data = NDDataBuffer::I32(vec![42]);
1249 let stats = compute_stats(&data, &dims, 0);
1250 assert_eq!(stats.min, 42.0);
1251 assert_eq!(stats.max, 42.0);
1252 assert_eq!(stats.mean, 42.0);
1253 assert_eq!(stats.sigma, 0.0);
1254 assert_eq!(stats.num_elements, 1);
1255 }
1256
1257 #[test]
1258 fn test_compute_stats_empty() {
1259 let data = NDDataBuffer::U8(vec![]);
1260 let stats = compute_stats(&data, &[], 0);
1261 assert_eq!(stats.num_elements, 0);
1262 }
1263
1264 #[test]
1265 fn test_compute_stats_min_max_position() {
1266 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1267 let data = NDDataBuffer::U8((1..=16).collect());
1269 let stats = compute_stats(&data, &dims, 0);
1270 assert_eq!(stats.min_x, 0); assert_eq!(stats.min_y, 0);
1272 assert_eq!(stats.max_x, 3); assert_eq!(stats.max_y, 3);
1274 }
1275
1276 #[test]
1277 fn test_compute_stats_net_no_bgd() {
1278 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1279 let data = NDDataBuffer::U8((1..=16).collect());
1280 let stats = compute_stats(&data, &dims, 0);
1281 assert_eq!(stats.net, stats.total);
1283 }
1284
1285 #[test]
1286 fn test_compute_stats_bgd_subtraction() {
1287 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1289 let mut pixels = vec![10u16; 16];
1290 pixels[2 * 4 + 2] = 110;
1292 let data = NDDataBuffer::U16(pixels);
1293 let stats = compute_stats(&data, &dims, 1);
1294
1295 assert!((stats.net - 100.0).abs() < 1e-10);
1304 }
1305
1306 #[test]
1307 fn test_centroid_uniform() {
1308 let data = NDDataBuffer::U8(vec![1; 16]);
1309 let c = compute_centroid(&data, 4, 4, 0.0);
1310 assert!((c.centroid_x - 1.5).abs() < 1e-10);
1311 assert!((c.centroid_y - 1.5).abs() < 1e-10);
1312 }
1313
1314 #[test]
1315 fn test_centroid_corner() {
1316 let mut d = vec![0u8; 16];
1317 d[0] = 255;
1318 let data = NDDataBuffer::U8(d);
1319 let c = compute_centroid(&data, 4, 4, 0.0);
1320 assert!((c.centroid_x - 0.0).abs() < 1e-10);
1321 assert!((c.centroid_y - 0.0).abs() < 1e-10);
1322 }
1323
1324 #[test]
1325 fn test_centroid_threshold() {
1326 let mut pixels = vec![5u8; 16];
1328 pixels[2 * 4 + 2] = 100;
1329 let data = NDDataBuffer::U8(pixels);
1330
1331 let c = compute_centroid(&data, 4, 4, 50.0);
1333 assert!((c.centroid_x - 2.0).abs() < 1e-10);
1334 assert!((c.centroid_y - 2.0).abs() < 1e-10);
1335 assert!((c.centroid_total - 100.0).abs() < 1e-10);
1336 }
1337
1338 #[test]
1339 fn test_centroid_higher_moments_symmetric() {
1340 let data = NDDataBuffer::U8(vec![1; 16]);
1342 let c = compute_centroid(&data, 4, 4, 0.0);
1343 assert!(c.skewness_x.abs() < 1e-10);
1345 assert!(c.skewness_y.abs() < 1e-10);
1346 assert!(c.eccentricity.abs() < 1e-10);
1348 }
1349
1350 #[test]
1351 fn test_histogram_basic() {
1352 let data = NDDataBuffer::F64((0..10).map(|x| x as f64).collect());
1354 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 9.0);
1355 assert_eq!(hist.len(), 10);
1356 assert_eq!(below, 0.0);
1357 assert_eq!(above, 0.0);
1358 let total: f64 = hist.iter().sum();
1360 assert!((total - 10.0).abs() < 1e-10);
1361 assert!(entropy.abs() < 1e-10);
1364 }
1365
1366 #[test]
1367 fn test_histogram_below_above() {
1368 let data = NDDataBuffer::F64(vec![-1.0, 0.5, 1.5, 3.0]);
1369 let (hist, below, above, _entropy) = compute_histogram(&data, 2, 0.0, 2.0);
1370 assert_eq!(below, 1.0); assert_eq!(above, 1.0); let total_in_bins: f64 = hist.iter().sum();
1373 assert!((total_in_bins - 2.0).abs() < 1e-10); }
1375
1376 #[test]
1377 fn test_histogram_single_value() {
1378 let data = NDDataBuffer::F64(vec![5.0; 100]);
1379 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 10.0);
1380 assert_eq!(below, 0.0);
1381 assert_eq!(above, 0.0);
1382 let expected = -(100.0f64.ln());
1386 assert!((entropy - expected).abs() < 1e-10);
1387 let total: f64 = hist.iter().sum();
1388 assert!((total - 100.0).abs() < 1e-10);
1389 }
1390
1391 #[test]
1392 fn test_profiles_8x8() {
1393 let mut pixels = vec![0.0f64; 64];
1395 for iy in 0..8 {
1396 for ix in 0..8 {
1397 pixels[iy * 8 + ix] = iy as f64;
1398 }
1399 }
1400 let data = NDDataBuffer::F64(pixels);
1401
1402 let profiles = compute_profiles(
1403 &data, 8, 8, 0.0, 3.5, 3.5, 0, 7, );
1409
1410 assert_eq!(profiles.avg_x.len(), 8);
1412 for &v in &profiles.avg_x {
1413 assert!((v - 3.5).abs() < 1e-10, "avg_x should be 3.5, got {v}");
1414 }
1415
1416 assert_eq!(profiles.avg_y.len(), 8);
1418 for (iy, &v) in profiles.avg_y.iter().enumerate() {
1419 assert!(
1420 (v - iy as f64).abs() < 1e-10,
1421 "avg_y[{iy}] should be {iy}, got {v}"
1422 );
1423 }
1424
1425 assert_eq!(profiles.cursor_x.len(), 8);
1427 for &v in &profiles.cursor_x {
1428 assert!((v - 7.0).abs() < 1e-10);
1429 }
1430
1431 assert_eq!(profiles.cursor_y.len(), 8);
1433 for (iy, &v) in profiles.cursor_y.iter().enumerate() {
1434 assert!((v - iy as f64).abs() < 1e-10);
1435 }
1436
1437 assert_eq!(profiles.centroid_x.len(), 8);
1439 for &v in &profiles.centroid_x {
1440 assert!((v - 4.0).abs() < 1e-10);
1441 }
1442
1443 assert_eq!(profiles.centroid_y.len(), 8);
1445 for (iy, &v) in profiles.centroid_y.iter().enumerate() {
1446 assert!((v - iy as f64).abs() < 1e-10);
1447 }
1448 }
1449
1450 #[test]
1451 fn test_profiles_threshold() {
1452 let mut pixels = vec![1.0f64; 16];
1454 pixels[1 * 4 + 2] = 10.0;
1455 let data = NDDataBuffer::F64(pixels);
1456
1457 let profiles = compute_profiles(
1458 &data, 4, 4, 5.0, 2.0, 1.0, 0, 0,
1460 );
1461
1462 assert_eq!(profiles.threshold_x.len(), 4);
1464 assert!((profiles.threshold_x[2] - 10.0).abs() < 1e-10);
1465 assert!((profiles.threshold_x[0] - 0.0).abs() < 1e-10);
1467 assert!((profiles.threshold_x[1] - 0.0).abs() < 1e-10);
1468 assert!((profiles.threshold_x[3] - 0.0).abs() < 1e-10);
1469
1470 assert_eq!(profiles.threshold_y.len(), 4);
1472 assert!((profiles.threshold_y[1] - 10.0).abs() < 1e-10);
1473 assert!((profiles.threshold_y[0] - 0.0).abs() < 1e-10);
1474 }
1475
1476 #[test]
1477 fn test_stats_processor_direct() {
1478 let mut proc = StatsProcessor::new();
1479 let pool = NDArrayPool::new(1_000_000);
1480
1481 let mut arr = NDArray::new(vec![NDDimension::new(5)], NDDataType::UInt8);
1482 if let NDDataBuffer::U8(ref mut v) = arr.data {
1483 v[0] = 10;
1484 v[1] = 20;
1485 v[2] = 30;
1486 v[3] = 40;
1487 v[4] = 50;
1488 }
1489
1490 let result = proc.process_array(&arr, &pool);
1491 assert_eq!(result.output_arrays.len(), 1, "stats forwards the array");
1493
1494 let stats = proc.stats_handle().lock().clone();
1495 assert_eq!(stats.min, 10.0);
1496 assert_eq!(stats.max, 50.0);
1497 assert_eq!(stats.mean, 30.0);
1498 }
1499
1500 #[test]
1501 fn test_stats_runtime_end_to_end() {
1502 let pool = Arc::new(NDArrayPool::new(1_000_000));
1503 let wiring = Arc::new(WiringRegistry::new());
1504 let ts_registry = crate::time_series::TsReceiverRegistry::new();
1505 let (handle, stats, _params, _jh) =
1506 create_stats_runtime("STATS_RT", pool, 10, "", wiring, &ts_registry);
1507
1508 handle
1510 .port_runtime()
1511 .port_handle()
1512 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
1513 .unwrap();
1514 std::thread::sleep(std::time::Duration::from_millis(10));
1515
1516 let mut arr = NDArray::new(
1517 vec![NDDimension::new(4), NDDimension::new(4)],
1518 NDDataType::UInt8,
1519 );
1520 if let NDDataBuffer::U8(ref mut v) = arr.data {
1521 for (i, val) in v.iter_mut().enumerate() {
1522 *val = (i + 1) as u8;
1523 }
1524 }
1525
1526 let rt = tokio::runtime::Builder::new_current_thread()
1527 .enable_all()
1528 .build()
1529 .unwrap();
1530 rt.block_on(handle.array_sender().publish(Arc::new(arr)));
1531 std::thread::sleep(std::time::Duration::from_millis(100));
1532
1533 let result = stats.lock().clone();
1534 assert_eq!(result.min, 1.0);
1535 assert_eq!(result.max, 16.0);
1536 assert_eq!(result.num_elements, 16);
1537 }
1538}