1use std::sync::Arc;
2
3#[cfg(feature = "parallel")]
4use rayon::prelude::*;
5#[cfg(feature = "parallel")]
6use crate::par_util;
7
8use ad_core::ndarray::{NDArray, NDDataBuffer};
9use ad_core::ndarray_pool::NDArrayPool;
10use ad_core::plugin::registry::{build_plugin_base_registry, ParamInfo, ParamRegistry};
11use ad_core::plugin::runtime::{NDPluginProcess, ParamUpdate, PluginParamSnapshot, PluginRuntimeHandle, ProcessResult};
12use ad_core::plugin::wiring::WiringRegistry;
13use asyn_rs::param::ParamType;
14use asyn_rs::port::PortDriverBase;
15use parking_lot::Mutex;
16
17#[derive(Clone, Copy, Default)]
19pub struct NDStatsParams {
20 pub compute_statistics: usize,
21 pub bgd_width: usize,
22 pub min_value: usize,
23 pub max_value: usize,
24 pub mean_value: usize,
25 pub sigma_value: usize,
26 pub total: usize,
27 pub net: usize,
28 pub min_x: usize,
29 pub min_y: usize,
30 pub max_x: usize,
31 pub max_y: usize,
32 pub compute_centroid: usize,
33 pub centroid_threshold: usize,
34 pub centroid_total: usize,
35 pub centroid_x: usize,
36 pub centroid_y: usize,
37 pub sigma_x: usize,
38 pub sigma_y: usize,
39 pub sigma_xy: usize,
40 pub skewness_x: usize,
41 pub skewness_y: usize,
42 pub kurtosis_x: usize,
43 pub kurtosis_y: usize,
44 pub eccentricity: usize,
45 pub orientation: usize,
46 pub compute_histogram: usize,
47 pub hist_size: usize,
48 pub hist_min: usize,
49 pub hist_max: usize,
50 pub hist_below: usize,
51 pub hist_above: usize,
52 pub hist_entropy: usize,
53 pub compute_profiles: usize,
54 pub cursor_x: usize,
55 pub cursor_y: usize,
56}
57
58#[derive(Debug, Clone, Default)]
60pub struct StatsResult {
61 pub min: f64,
62 pub max: f64,
63 pub mean: f64,
64 pub sigma: f64,
65 pub total: f64,
66 pub net: f64,
67 pub num_elements: usize,
68 pub min_x: usize,
69 pub min_y: usize,
70 pub max_x: usize,
71 pub max_y: usize,
72 pub histogram: Vec<f64>,
73 pub hist_below: f64,
74 pub hist_above: f64,
75 pub hist_entropy: f64,
76 pub profile_avg_x: Vec<f64>,
77 pub profile_avg_y: Vec<f64>,
78 pub profile_threshold_x: Vec<f64>,
79 pub profile_threshold_y: Vec<f64>,
80 pub profile_centroid_x: Vec<f64>,
81 pub profile_centroid_y: Vec<f64>,
82 pub profile_cursor_x: Vec<f64>,
83 pub profile_cursor_y: Vec<f64>,
84}
85
86#[derive(Debug, Clone, Default)]
88pub struct CentroidResult {
89 pub centroid_x: f64,
90 pub centroid_y: f64,
91 pub sigma_x: f64,
92 pub sigma_y: f64,
93 pub sigma_xy: f64,
94 pub centroid_total: f64,
95 pub skewness_x: f64,
96 pub skewness_y: f64,
97 pub kurtosis_x: f64,
98 pub kurtosis_y: f64,
99 pub eccentricity: f64,
100 pub orientation: f64,
101}
102
103#[derive(Debug, Clone, Default)]
105pub struct ProfileResult {
106 pub avg_x: Vec<f64>,
107 pub avg_y: Vec<f64>,
108 pub threshold_x: Vec<f64>,
109 pub threshold_y: Vec<f64>,
110 pub centroid_x: Vec<f64>,
111 pub centroid_y: Vec<f64>,
112 pub cursor_x: Vec<f64>,
113 pub cursor_y: Vec<f64>,
114}
115
116pub fn compute_stats(
123 data: &NDDataBuffer,
124 dims: &[ad_core::ndarray::NDDimension],
125 bgd_width: usize,
126) -> StatsResult {
127 macro_rules! stats_for {
128 ($vec:expr) => {{
129 let v = $vec;
130 if v.is_empty() {
131 return StatsResult::default();
132 }
133
134 let (min, max, min_idx, max_idx, total, variance);
135
136 #[cfg(feature = "parallel")]
137 {
138 if par_util::should_parallelize(v.len()) {
139 let (pmin, pmax, pmin_idx, pmax_idx, ptotal) = par_util::thread_pool().install(|| {
141 v.par_iter().enumerate()
142 .fold(
143 || (f64::MAX, f64::MIN, 0usize, 0usize, 0.0f64),
144 |(mn, mx, mn_i, mx_i, s), (i, &elem)| {
145 let f = elem as f64;
146 let (new_mn, new_mn_i) = if f < mn { (f, i) } else { (mn, mn_i) };
147 let (new_mx, new_mx_i) = if f > mx { (f, i) } else { (mx, mx_i) };
148 (new_mn, new_mx, new_mn_i, new_mx_i, s + f)
149 },
150 )
151 .reduce(
152 || (f64::MAX, f64::MIN, 0, 0, 0.0),
153 |(mn1, mx1, mn_i1, mx_i1, s1), (mn2, mx2, mn_i2, mx_i2, s2)| {
154 let (rmn, rmn_i) = if mn1 <= mn2 { (mn1, mn_i1) } else { (mn2, mn_i2) };
155 let (rmx, rmx_i) = if mx1 >= mx2 { (mx1, mx_i1) } else { (mx2, mx_i2) };
156 (rmn, rmx, rmn_i, rmx_i, s1 + s2)
157 },
158 )
159 });
160 min = pmin; max = pmax; min_idx = pmin_idx; max_idx = pmax_idx; total = ptotal;
161 let mean_tmp = total / v.len() as f64;
162 variance = par_util::thread_pool().install(|| {
163 v.par_iter()
164 .map(|&elem| { let d = elem as f64 - mean_tmp; d * d })
165 .sum::<f64>()
166 });
167 } else {
168 let mut lmin = v[0] as f64;
169 let mut lmax = v[0] as f64;
170 let mut lmin_idx: usize = 0;
171 let mut lmax_idx: usize = 0;
172 let mut ltotal = 0.0f64;
173 for (i, &elem) in v.iter().enumerate() {
174 let f = elem as f64;
175 if f < lmin { lmin = f; lmin_idx = i; }
176 if f > lmax { lmax = f; lmax_idx = i; }
177 ltotal += f;
178 }
179 min = lmin; max = lmax; min_idx = lmin_idx; max_idx = lmax_idx; total = ltotal;
180 let mean_tmp = total / v.len() as f64;
181 let mut lvar = 0.0f64;
182 for &elem in v.iter() {
183 let d = elem as f64 - mean_tmp;
184 lvar += d * d;
185 }
186 variance = lvar;
187 }
188 }
189
190 #[cfg(not(feature = "parallel"))]
191 {
192 let mut lmin = v[0] as f64;
193 let mut lmax = v[0] as f64;
194 let mut lmin_idx: usize = 0;
195 let mut lmax_idx: usize = 0;
196 let mut ltotal = 0.0f64;
197 for (i, &elem) in v.iter().enumerate() {
198 let f = elem as f64;
199 if f < lmin { lmin = f; lmin_idx = i; }
200 if f > lmax { lmax = f; lmax_idx = i; }
201 ltotal += f;
202 }
203 min = lmin; max = lmax; min_idx = lmin_idx; max_idx = lmax_idx; total = ltotal;
204 let mean_tmp = total / v.len() as f64;
205 let mut lvar = 0.0f64;
206 for &elem in v.iter() {
207 let d = elem as f64 - mean_tmp;
208 lvar += d * d;
209 }
210 variance = lvar;
211 }
212
213 let mean = total / v.len() as f64;
214 let sigma = (variance / v.len() as f64).sqrt();
215 let x_size = dims.first().map_or(v.len(), |d| d.size);
216
217 let net = if bgd_width > 0 && dims.len() >= 2 {
219 let y_size = dims[1].size;
220 let mut bgd_sum = 0.0f64;
221 let mut bgd_count = 0usize;
222 for iy in 0..y_size {
223 for ix in 0..x_size {
224 let is_edge = ix < bgd_width
225 || ix >= x_size.saturating_sub(bgd_width)
226 || iy < bgd_width
227 || iy >= y_size.saturating_sub(bgd_width);
228 if is_edge {
229 let idx = iy * x_size + ix;
230 if idx < v.len() {
231 bgd_sum += v[idx] as f64;
232 bgd_count += 1;
233 }
234 }
235 }
236 }
237 let bgd_avg = if bgd_count > 0 { bgd_sum / bgd_count as f64 } else { 0.0 };
238 total - bgd_avg * v.len() as f64
239 } else {
240 total
241 };
242
243 StatsResult {
244 min,
245 max,
246 mean,
247 sigma,
248 total,
249 net,
250 num_elements: v.len(),
251 min_x: if x_size > 0 { min_idx % x_size } else { 0 },
252 min_y: if x_size > 0 { min_idx / x_size } else { 0 },
253 max_x: if x_size > 0 { max_idx % x_size } else { 0 },
254 max_y: if x_size > 0 { max_idx / x_size } else { 0 },
255 ..StatsResult::default()
256 }
257 }};
258 }
259
260 match data {
261 NDDataBuffer::I8(v) => stats_for!(v),
262 NDDataBuffer::U8(v) => stats_for!(v),
263 NDDataBuffer::I16(v) => stats_for!(v),
264 NDDataBuffer::U16(v) => stats_for!(v),
265 NDDataBuffer::I32(v) => stats_for!(v),
266 NDDataBuffer::U32(v) => stats_for!(v),
267 NDDataBuffer::I64(v) => stats_for!(v),
268 NDDataBuffer::U64(v) => stats_for!(v),
269 NDDataBuffer::F32(v) => stats_for!(v),
270 NDDataBuffer::F64(v) => stats_for!(v),
271 }
272}
273
274pub fn compute_centroid(
278 data: &NDDataBuffer,
279 x_size: usize,
280 y_size: usize,
281 threshold: f64,
282) -> CentroidResult {
283 let n = x_size * y_size;
284 if n == 0 || data.len() < n {
285 return CentroidResult::default();
286 }
287
288 let vals: Vec<f64> = (0..n).map(|i| data.get_as_f64(i).unwrap_or(0.0)).collect();
290
291 let (m00, m10, m01);
293
294 #[cfg(feature = "parallel")]
295 {
296 if par_util::should_parallelize(n) {
297 let xs = x_size;
298 let thr = threshold;
299 let (pm00, pm10, pm01) = par_util::thread_pool().install(|| {
300 vals.par_iter().enumerate()
301 .fold(
302 || (0.0f64, 0.0f64, 0.0f64),
303 |(s00, s10, s01), (i, &val)| {
304 if val < thr { return (s00, s10, s01); }
305 let ix = i % xs;
306 let iy = i / xs;
307 (s00 + val, s10 + val * ix as f64, s01 + val * iy as f64)
308 },
309 )
310 .reduce(
311 || (0.0, 0.0, 0.0),
312 |(a0, a1, a2), (b0, b1, b2)| (a0 + b0, a1 + b1, a2 + b2),
313 )
314 });
315 m00 = pm00; m10 = pm10; m01 = pm01;
316 } else {
317 let mut lm00 = 0.0f64;
318 let mut lm10 = 0.0f64;
319 let mut lm01 = 0.0f64;
320 for iy in 0..y_size {
321 for ix in 0..x_size {
322 let val = vals[iy * x_size + ix];
323 if val < threshold { continue; }
324 lm00 += val;
325 lm10 += val * ix as f64;
326 lm01 += val * iy as f64;
327 }
328 }
329 m00 = lm00; m10 = lm10; m01 = lm01;
330 }
331 }
332
333 #[cfg(not(feature = "parallel"))]
334 {
335 let mut lm00 = 0.0f64;
336 let mut lm10 = 0.0f64;
337 let mut lm01 = 0.0f64;
338 for iy in 0..y_size {
339 for ix in 0..x_size {
340 let val = vals[iy * x_size + ix];
341 if val < threshold { continue; }
342 lm00 += val;
343 lm10 += val * ix as f64;
344 lm01 += val * iy as f64;
345 }
346 }
347 m00 = lm00; m10 = lm10; m01 = lm01;
348 }
349
350 if m00 == 0.0 {
351 return CentroidResult::default();
352 }
353
354 let cx = m10 / m00;
355 let cy = m01 / m00;
356
357 let (mu20, mu02, mu11, m30_central, m03_central, m40_central, m04_central);
359
360 #[cfg(feature = "parallel")]
361 {
362 if par_util::should_parallelize(n) {
363 let xs = x_size;
364 let thr = threshold;
365 let (p20, p02, p11, p30, p03, p40, p04) = par_util::thread_pool().install(|| {
366 vals.par_iter().enumerate()
367 .fold(
368 || (0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64),
369 |(s20, s02, s11, s30, s03, s40, s04), (i, &val)| {
370 if val < thr { return (s20, s02, s11, s30, s03, s40, s04); }
371 let ix = i % xs;
372 let iy = i / xs;
373 let dx = ix as f64 - cx;
374 let dy = iy as f64 - cy;
375 let dx2 = dx * dx;
376 let dy2 = dy * dy;
377 (
378 s20 + val * dx2,
379 s02 + val * dy2,
380 s11 + val * dx * dy,
381 s30 + val * dx2 * dx,
382 s03 + val * dy2 * dy,
383 s40 + val * dx2 * dx2,
384 s04 + val * dy2 * dy2,
385 )
386 },
387 )
388 .reduce(
389 || (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
390 |(a0,a1,a2,a3,a4,a5,a6),(b0,b1,b2,b3,b4,b5,b6)|
391 (a0+b0,a1+b1,a2+b2,a3+b3,a4+b4,a5+b5,a6+b6),
392 )
393 });
394 mu20 = p20; mu02 = p02; mu11 = p11;
395 m30_central = p30; m03_central = p03; m40_central = p40; m04_central = p04;
396 } else {
397 let mut l20 = 0.0f64; let mut l02 = 0.0f64; let mut l11 = 0.0f64;
398 let mut l30 = 0.0f64; let mut l03 = 0.0f64; let mut l40 = 0.0f64; let mut l04 = 0.0f64;
399 for iy in 0..y_size {
400 for ix in 0..x_size {
401 let val = vals[iy * x_size + ix];
402 if val < threshold { continue; }
403 let dx = ix as f64 - cx; let dy = iy as f64 - cy;
404 let dx2 = dx * dx; let dy2 = dy * dy;
405 l20 += val * dx2; l02 += val * dy2; l11 += val * dx * dy;
406 l30 += val * dx2 * dx; l03 += val * dy2 * dy;
407 l40 += val * dx2 * dx2; l04 += val * dy2 * dy2;
408 }
409 }
410 mu20 = l20; mu02 = l02; mu11 = l11;
411 m30_central = l30; m03_central = l03; m40_central = l40; m04_central = l04;
412 }
413 }
414
415 #[cfg(not(feature = "parallel"))]
416 {
417 let mut l20 = 0.0f64; let mut l02 = 0.0f64; let mut l11 = 0.0f64;
418 let mut l30 = 0.0f64; let mut l03 = 0.0f64; let mut l40 = 0.0f64; let mut l04 = 0.0f64;
419 for iy in 0..y_size {
420 for ix in 0..x_size {
421 let val = vals[iy * x_size + ix];
422 if val < threshold { continue; }
423 let dx = ix as f64 - cx; let dy = iy as f64 - cy;
424 let dx2 = dx * dx; let dy2 = dy * dy;
425 l20 += val * dx2; l02 += val * dy2; l11 += val * dx * dy;
426 l30 += val * dx2 * dx; l03 += val * dy2 * dy;
427 l40 += val * dx2 * dx2; l04 += val * dy2 * dy2;
428 }
429 }
430 mu20 = l20; mu02 = l02; mu11 = l11;
431 m30_central = l30; m03_central = l03; m40_central = l40; m04_central = l04;
432 }
433
434 let sigma_x = (mu20 / m00).sqrt();
435 let sigma_y = (mu02 / m00).sqrt();
436 let sigma_xy = mu11 / m00;
437
438 let skewness_x = if sigma_x > 0.0 {
440 m30_central / (m00 * sigma_x.powi(3))
441 } else {
442 0.0
443 };
444 let skewness_y = if sigma_y > 0.0 {
445 m03_central / (m00 * sigma_y.powi(3))
446 } else {
447 0.0
448 };
449
450 let kurtosis_x = if sigma_x > 0.0 {
452 m40_central / (m00 * sigma_x.powi(4)) - 3.0
453 } else {
454 0.0
455 };
456 let kurtosis_y = if sigma_y > 0.0 {
457 m04_central / (m00 * sigma_y.powi(4)) - 3.0
458 } else {
459 0.0
460 };
461
462 let mu20_norm = mu20 / m00;
464 let mu02_norm = mu02 / m00;
465 let mu11_norm = mu11 / m00;
466 let denom = mu20_norm + mu02_norm;
467 let eccentricity = if denom > 0.0 {
468 ((mu20_norm - mu02_norm).powi(2) + 4.0 * mu11_norm.powi(2)) / denom.powi(2)
469 } else {
470 0.0
471 };
472
473 let orientation =
475 0.5 * (2.0 * mu11_norm).atan2(mu20_norm - mu02_norm) * 180.0 / std::f64::consts::PI;
476
477 CentroidResult {
478 centroid_x: cx,
479 centroid_y: cy,
480 sigma_x,
481 sigma_y,
482 sigma_xy,
483 centroid_total: m00,
484 skewness_x,
485 skewness_y,
486 kurtosis_x,
487 kurtosis_y,
488 eccentricity,
489 orientation,
490 }
491}
492
493pub fn compute_histogram(
502 data: &NDDataBuffer,
503 hist_size: usize,
504 hist_min: f64,
505 hist_max: f64,
506) -> (Vec<f64>, f64, f64, f64) {
507 if hist_size == 0 || hist_max <= hist_min {
508 return (vec![], 0.0, 0.0, 0.0);
509 }
510
511 let mut histogram = vec![0.0f64; hist_size];
512 let mut below = 0.0f64;
513 let mut above = 0.0f64;
514 let range = hist_max - hist_min;
515 let n = data.len();
516
517 #[cfg(feature = "parallel")]
518 let use_parallel = par_util::should_parallelize(n);
519 #[cfg(not(feature = "parallel"))]
520 let use_parallel = false;
521
522 if use_parallel {
523 #[cfg(feature = "parallel")]
524 {
525 let vals: Vec<f64> = (0..n).map(|i| data.get_as_f64(i).unwrap_or(0.0)).collect();
526 let chunk_size = (n / rayon::current_num_threads().max(1)).max(1024);
527 let hs = hist_size;
528 let hmin = hist_min;
529 let hmax = hist_max;
530 let rng = range;
531 let chunk_results: Vec<(Vec<f64>, f64, f64)> = par_util::thread_pool().install(|| {
532 vals.par_chunks(chunk_size)
533 .map(|chunk| {
534 let mut local_hist = vec![0.0f64; hs];
535 let mut local_below = 0.0f64;
536 let mut local_above = 0.0f64;
537 for &val in chunk {
538 if val < hmin {
539 local_below += 1.0;
540 } else if val > hmax {
541 local_above += 1.0;
542 } else {
543 let bin = ((val - hmin) * (hs - 1) as f64 / rng + 0.5) as usize;
544 let bin = bin.min(hs - 1);
545 local_hist[bin] += 1.0;
546 }
547 }
548 (local_hist, local_below, local_above)
549 })
550 .collect()
551 });
552 for (local_hist, local_below, local_above) in chunk_results {
553 below += local_below;
554 above += local_above;
555 for (i, &count) in local_hist.iter().enumerate() {
556 histogram[i] += count;
557 }
558 }
559 }
560 } else {
561 for i in 0..n {
562 let val = data.get_as_f64(i).unwrap_or(0.0);
563 if val < hist_min {
564 below += 1.0;
565 } else if val > hist_max {
566 above += 1.0;
567 } else {
568 let bin = ((val - hist_min) * (hist_size - 1) as f64 / range + 0.5) as usize;
569 let bin = bin.min(hist_size - 1);
570 histogram[bin] += 1.0;
571 }
572 }
573 }
574
575 let total_in_bins: f64 = histogram.iter().sum();
577 let entropy = if total_in_bins > 0.0 {
578 let mut ent = 0.0f64;
579 for &count in &histogram {
580 if count > 0.0 {
581 let p = count / total_in_bins;
582 ent -= p * p.ln();
583 }
584 }
585 ent
586 } else {
587 0.0
588 };
589
590 (histogram, below, above, entropy)
591}
592
593pub fn compute_profiles(
600 data: &NDDataBuffer,
601 x_size: usize,
602 y_size: usize,
603 threshold: f64,
604 centroid_x: f64,
605 centroid_y: f64,
606 cursor_x: usize,
607 cursor_y: usize,
608) -> ProfileResult {
609 if x_size == 0 || y_size == 0 || data.len() < x_size * y_size {
610 return ProfileResult::default();
611 }
612
613 let mut avg_x = vec![0.0f64; x_size];
614 let mut avg_y = vec![0.0f64; y_size];
615 let mut thresh_x_sum = vec![0.0f64; x_size];
616 let mut thresh_x_cnt = vec![0usize; x_size];
617 let mut thresh_y_sum = vec![0.0f64; y_size];
618 let mut thresh_y_cnt = vec![0usize; y_size];
619
620 for iy in 0..y_size {
622 for ix in 0..x_size {
623 let val = data.get_as_f64(iy * x_size + ix).unwrap_or(0.0);
624 avg_x[ix] += val;
625 avg_y[iy] += val;
626 if val >= threshold {
627 thresh_x_sum[ix] += val;
628 thresh_x_cnt[ix] += 1;
629 thresh_y_sum[iy] += val;
630 thresh_y_cnt[iy] += 1;
631 }
632 }
633 }
634
635 for ix in 0..x_size {
637 avg_x[ix] /= y_size as f64;
638 }
639 for iy in 0..y_size {
640 avg_y[iy] /= x_size as f64;
641 }
642
643 let threshold_x: Vec<f64> = thresh_x_sum
645 .iter()
646 .zip(thresh_x_cnt.iter())
647 .map(|(&s, &c)| if c > 0 { s / c as f64 } else { 0.0 })
648 .collect();
649 let threshold_y: Vec<f64> = thresh_y_sum
650 .iter()
651 .zip(thresh_y_cnt.iter())
652 .map(|(&s, &c)| if c > 0 { s / c as f64 } else { 0.0 })
653 .collect();
654
655 let cy_row = (centroid_y + 0.5) as usize;
657 let cx_col = (centroid_x + 0.5) as usize;
658
659 let centroid_x_profile = if cy_row < y_size {
660 (0..x_size)
661 .map(|ix| data.get_as_f64(cy_row * x_size + ix).unwrap_or(0.0))
662 .collect()
663 } else {
664 vec![0.0; x_size]
665 };
666
667 let centroid_y_profile = if cx_col < x_size {
668 (0..y_size)
669 .map(|iy| data.get_as_f64(iy * x_size + cx_col).unwrap_or(0.0))
670 .collect()
671 } else {
672 vec![0.0; y_size]
673 };
674
675 let cursor_x_profile = if cursor_y < y_size {
677 (0..x_size)
678 .map(|ix| data.get_as_f64(cursor_y * x_size + ix).unwrap_or(0.0))
679 .collect()
680 } else {
681 vec![0.0; x_size]
682 };
683
684 let cursor_y_profile = if cursor_x < x_size {
685 (0..y_size)
686 .map(|iy| data.get_as_f64(iy * x_size + cursor_x).unwrap_or(0.0))
687 .collect()
688 } else {
689 vec![0.0; y_size]
690 };
691
692 ProfileResult {
693 avg_x,
694 avg_y,
695 threshold_x,
696 threshold_y,
697 centroid_x: centroid_x_profile,
698 centroid_y: centroid_y_profile,
699 cursor_x: cursor_x_profile,
700 cursor_y: cursor_y_profile,
701 }
702}
703
704pub struct StatsProcessor {
706 latest_stats: Arc<Mutex<StatsResult>>,
707 do_compute_centroid: bool,
708 do_compute_histogram: bool,
709 do_compute_profiles: bool,
710 bgd_width: usize,
711 centroid_threshold: f64,
712 cursor_x: usize,
713 cursor_y: usize,
714 hist_size: usize,
715 hist_min: f64,
716 hist_max: f64,
717 params: NDStatsParams,
718 params_out: Arc<Mutex<NDStatsParams>>,
720 ts_sender: Option<crate::time_series::TimeSeriesSender>,
722}
723
724impl StatsProcessor {
725 pub fn new() -> Self {
726 Self {
727 latest_stats: Arc::new(Mutex::new(StatsResult::default())),
728 do_compute_centroid: true,
729 do_compute_histogram: false,
730 do_compute_profiles: false,
731 bgd_width: 0,
732 centroid_threshold: 0.0,
733 cursor_x: 0,
734 cursor_y: 0,
735 hist_size: 256,
736 hist_min: 0.0,
737 hist_max: 255.0,
738 params: NDStatsParams::default(),
739 params_out: Arc::new(Mutex::new(NDStatsParams::default())),
740 ts_sender: None,
741 }
742 }
743
744 pub fn stats_handle(&self) -> Arc<Mutex<StatsResult>> {
746 self.latest_stats.clone()
747 }
748
749 pub fn params_handle(&self) -> Arc<Mutex<NDStatsParams>> {
751 self.params_out.clone()
752 }
753
754 pub fn set_ts_sender(&mut self, sender: crate::time_series::TimeSeriesSender) {
756 self.ts_sender = Some(sender);
757 }
758}
759
760impl Default for StatsProcessor {
761 fn default() -> Self {
762 Self::new()
763 }
764}
765
766impl NDPluginProcess for StatsProcessor {
767 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
768 let p = &self.params;
769 let info = array.info();
770
771 let mut result = compute_stats(&array.data, &array.dims, self.bgd_width);
772
773 let mut centroid = CentroidResult::default();
775 if self.do_compute_centroid {
776 if info.color_size == 1 && array.dims.len() >= 2 {
777 centroid = compute_centroid(
778 &array.data, info.x_size, info.y_size, self.centroid_threshold,
779 );
780 }
781 }
782
783 if self.do_compute_histogram {
785 let (histogram, below, above, entropy) =
786 compute_histogram(&array.data, self.hist_size, self.hist_min, self.hist_max);
787 result.histogram = histogram;
788 result.hist_below = below;
789 result.hist_above = above;
790 result.hist_entropy = entropy;
791 }
792
793 if self.do_compute_profiles && info.color_size == 1 && array.dims.len() >= 2 {
795 let profiles = compute_profiles(
796 &array.data,
797 info.x_size,
798 info.y_size,
799 self.centroid_threshold,
800 centroid.centroid_x,
801 centroid.centroid_y,
802 self.cursor_x,
803 self.cursor_y,
804 );
805 result.profile_avg_x = profiles.avg_x;
806 result.profile_avg_y = profiles.avg_y;
807 result.profile_threshold_x = profiles.threshold_x;
808 result.profile_threshold_y = profiles.threshold_y;
809 result.profile_centroid_x = profiles.centroid_x;
810 result.profile_centroid_y = profiles.centroid_y;
811 result.profile_cursor_x = profiles.cursor_x;
812 result.profile_cursor_y = profiles.cursor_y;
813 }
814
815 let updates = vec![
816 ParamUpdate::float64(p.min_value, result.min),
817 ParamUpdate::float64(p.max_value, result.max),
818 ParamUpdate::float64(p.mean_value, result.mean),
819 ParamUpdate::float64(p.sigma_value, result.sigma),
820 ParamUpdate::float64(p.total, result.total),
821 ParamUpdate::float64(p.net, result.net),
822 ParamUpdate::float64(p.min_x, result.min_x as f64),
823 ParamUpdate::float64(p.min_y, result.min_y as f64),
824 ParamUpdate::float64(p.max_x, result.max_x as f64),
825 ParamUpdate::float64(p.max_y, result.max_y as f64),
826 ParamUpdate::float64(p.centroid_x, centroid.centroid_x),
827 ParamUpdate::float64(p.centroid_y, centroid.centroid_y),
828 ParamUpdate::float64(p.sigma_x, centroid.sigma_x),
829 ParamUpdate::float64(p.sigma_y, centroid.sigma_y),
830 ParamUpdate::float64(p.sigma_xy, centroid.sigma_xy),
831 ParamUpdate::float64(p.centroid_total, centroid.centroid_total),
832 ParamUpdate::float64(p.skewness_x, centroid.skewness_x),
833 ParamUpdate::float64(p.skewness_y, centroid.skewness_y),
834 ParamUpdate::float64(p.kurtosis_x, centroid.kurtosis_x),
835 ParamUpdate::float64(p.kurtosis_y, centroid.kurtosis_y),
836 ParamUpdate::float64(p.eccentricity, centroid.eccentricity),
837 ParamUpdate::float64(p.orientation, centroid.orientation),
838 ParamUpdate::float64(p.hist_below, result.hist_below),
839 ParamUpdate::float64(p.hist_above, result.hist_above),
840 ParamUpdate::float64(p.hist_entropy, result.hist_entropy),
841 ];
842
843 if let Some(ref sender) = self.ts_sender {
845 let ts_data = crate::time_series::TimeSeriesData {
846 values: vec![
847 result.min,
848 result.min_x as f64,
849 result.min_y as f64,
850 result.max,
851 result.max_x as f64,
852 result.max_y as f64,
853 result.mean,
854 result.sigma,
855 result.total,
856 result.net,
857 centroid.centroid_total,
858 centroid.centroid_x,
859 centroid.centroid_y,
860 centroid.sigma_x,
861 centroid.sigma_y,
862 centroid.sigma_xy,
863 centroid.skewness_x,
864 centroid.skewness_y,
865 centroid.kurtosis_x,
866 centroid.kurtosis_y,
867 centroid.eccentricity,
868 centroid.orientation,
869 array.timestamp.as_f64(),
870 ],
871 };
872 let _ = sender.try_send(ts_data);
873 }
874
875 *self.latest_stats.lock() = result;
876 ProcessResult::sink(updates)
877 }
878
879 fn plugin_type(&self) -> &str {
880 "NDPluginStats"
881 }
882
883 fn register_params(&mut self, base: &mut PortDriverBase) -> Result<(), asyn_rs::error::AsynError> {
884 self.params.compute_statistics = base.create_param("COMPUTE_STATISTICS", ParamType::Int32)?;
885 base.set_int32_param(self.params.compute_statistics, 0, 1)?;
886
887 self.params.bgd_width = base.create_param("BGD_WIDTH", ParamType::Int32)?;
888 self.params.min_value = base.create_param("MIN_VALUE", ParamType::Float64)?;
889 self.params.max_value = base.create_param("MAX_VALUE", ParamType::Float64)?;
890 self.params.mean_value = base.create_param("MEAN_VALUE", ParamType::Float64)?;
891 self.params.sigma_value = base.create_param("SIGMA_VALUE", ParamType::Float64)?;
892 self.params.total = base.create_param("TOTAL", ParamType::Float64)?;
893 self.params.net = base.create_param("NET", ParamType::Float64)?;
894 self.params.min_x = base.create_param("MIN_X", ParamType::Float64)?;
895 self.params.min_y = base.create_param("MIN_Y", ParamType::Float64)?;
896 self.params.max_x = base.create_param("MAX_X", ParamType::Float64)?;
897 self.params.max_y = base.create_param("MAX_Y", ParamType::Float64)?;
898
899 self.params.compute_centroid = base.create_param("COMPUTE_CENTROID", ParamType::Int32)?;
900 base.set_int32_param(self.params.compute_centroid, 0, 1)?;
901
902 self.params.centroid_threshold = base.create_param("CENTROID_THRESHOLD", ParamType::Float64)?;
903 self.params.centroid_total = base.create_param("CENTROID_TOTAL", ParamType::Float64)?;
904 self.params.centroid_x = base.create_param("CENTROIDX_VALUE", ParamType::Float64)?;
905 self.params.centroid_y = base.create_param("CENTROIDY_VALUE", ParamType::Float64)?;
906 self.params.sigma_x = base.create_param("SIGMAX_VALUE", ParamType::Float64)?;
907 self.params.sigma_y = base.create_param("SIGMAY_VALUE", ParamType::Float64)?;
908 self.params.sigma_xy = base.create_param("SIGMAXY_VALUE", ParamType::Float64)?;
909 self.params.skewness_x = base.create_param("SKEWNESSX_VALUE", ParamType::Float64)?;
910 self.params.skewness_y = base.create_param("SKEWNESSY_VALUE", ParamType::Float64)?;
911 self.params.kurtosis_x = base.create_param("KURTOSISX_VALUE", ParamType::Float64)?;
912 self.params.kurtosis_y = base.create_param("KURTOSISY_VALUE", ParamType::Float64)?;
913 self.params.eccentricity = base.create_param("ECCENTRICITY_VALUE", ParamType::Float64)?;
914 self.params.orientation = base.create_param("ORIENTATION_VALUE", ParamType::Float64)?;
915
916 self.params.compute_histogram = base.create_param("COMPUTE_HISTOGRAM", ParamType::Int32)?;
917 self.params.hist_size = base.create_param("HIST_SIZE", ParamType::Int32)?;
918 base.set_int32_param(self.params.hist_size, 0, 256)?;
919 self.params.hist_min = base.create_param("HIST_MIN", ParamType::Float64)?;
920 self.params.hist_max = base.create_param("HIST_MAX", ParamType::Float64)?;
921 base.set_float64_param(self.params.hist_max, 0, 255.0)?;
922 self.params.hist_below = base.create_param("HIST_BELOW", ParamType::Float64)?;
923 self.params.hist_above = base.create_param("HIST_ABOVE", ParamType::Float64)?;
924 self.params.hist_entropy = base.create_param("HIST_ENTROPY", ParamType::Float64)?;
925
926 self.params.compute_profiles = base.create_param("COMPUTE_PROFILES", ParamType::Int32)?;
927 self.params.cursor_x = base.create_param("CURSOR_X", ParamType::Int32)?;
928 base.set_int32_param(self.params.cursor_x, 0, 0)?;
929 self.params.cursor_y = base.create_param("CURSOR_Y", ParamType::Int32)?;
930 base.set_int32_param(self.params.cursor_y, 0, 0)?;
931
932 *self.params_out.lock() = self.params;
934
935 Ok(())
936 }
937
938 fn on_param_change(&mut self, reason: usize, snapshot: &PluginParamSnapshot) {
939 let p = &self.params;
940 if reason == p.compute_statistics {
941 } else if reason == p.compute_centroid {
943 self.do_compute_centroid = snapshot.value.as_i32() != 0;
944 } else if reason == p.compute_histogram {
945 self.do_compute_histogram = snapshot.value.as_i32() != 0;
946 } else if reason == p.compute_profiles {
947 self.do_compute_profiles = snapshot.value.as_i32() != 0;
948 } else if reason == p.bgd_width {
949 self.bgd_width = snapshot.value.as_i32().max(0) as usize;
950 } else if reason == p.centroid_threshold {
951 self.centroid_threshold = snapshot.value.as_f64();
952 } else if reason == p.cursor_x {
953 self.cursor_x = snapshot.value.as_i32().max(0) as usize;
954 } else if reason == p.cursor_y {
955 self.cursor_y = snapshot.value.as_i32().max(0) as usize;
956 } else if reason == p.hist_size {
957 self.hist_size = (snapshot.value.as_i32().max(1)) as usize;
958 } else if reason == p.hist_min {
959 self.hist_min = snapshot.value.as_f64();
960 } else if reason == p.hist_max {
961 self.hist_max = snapshot.value.as_f64();
962 }
963 }
964}
965
966pub fn build_stats_registry(h: &PluginRuntimeHandle, sp: &NDStatsParams) -> ParamRegistry {
968 let mut map = build_plugin_base_registry(h);
969
970 map.insert("ComputeStatistics".into(), ParamInfo::int32(sp.compute_statistics, "COMPUTE_STATISTICS"));
972 map.insert("ComputeStatistics_RBV".into(), ParamInfo::int32(sp.compute_statistics, "COMPUTE_STATISTICS"));
973 map.insert("BgdWidth".into(), ParamInfo::int32(sp.bgd_width, "BGD_WIDTH"));
974 map.insert("BgdWidth_RBV".into(), ParamInfo::int32(sp.bgd_width, "BGD_WIDTH"));
975 map.insert("ComputeCentroid".into(), ParamInfo::int32(sp.compute_centroid, "COMPUTE_CENTROID"));
976 map.insert("ComputeCentroid_RBV".into(), ParamInfo::int32(sp.compute_centroid, "COMPUTE_CENTROID"));
977 map.insert("CentroidThreshold".into(), ParamInfo::float64(sp.centroid_threshold, "CENTROID_THRESHOLD"));
978 map.insert("CentroidThreshold_RBV".into(), ParamInfo::float64(sp.centroid_threshold, "CENTROID_THRESHOLD"));
979
980 map.insert("MinValue_RBV".into(), ParamInfo::float64(sp.min_value, "MIN_VALUE"));
982 map.insert("MaxValue_RBV".into(), ParamInfo::float64(sp.max_value, "MAX_VALUE"));
983 map.insert("MeanValue_RBV".into(), ParamInfo::float64(sp.mean_value, "MEAN_VALUE"));
984 map.insert("Sigma_RBV".into(), ParamInfo::float64(sp.sigma_value, "SIGMA_VALUE"));
985 map.insert("Total_RBV".into(), ParamInfo::float64(sp.total, "TOTAL"));
986 map.insert("Net_RBV".into(), ParamInfo::float64(sp.net, "NET"));
987
988 map.insert("MinX_RBV".into(), ParamInfo::float64(sp.min_x, "MIN_X"));
990 map.insert("MinY_RBV".into(), ParamInfo::float64(sp.min_y, "MIN_Y"));
991 map.insert("MaxX_RBV".into(), ParamInfo::float64(sp.max_x, "MAX_X"));
992 map.insert("MaxY_RBV".into(), ParamInfo::float64(sp.max_y, "MAX_Y"));
993
994 map.insert("CentroidTotal_RBV".into(), ParamInfo::float64(sp.centroid_total, "CENTROID_TOTAL"));
996 map.insert("CentroidX_RBV".into(), ParamInfo::float64(sp.centroid_x, "CENTROIDX_VALUE"));
997 map.insert("CentroidY_RBV".into(), ParamInfo::float64(sp.centroid_y, "CENTROIDY_VALUE"));
998 map.insert("SigmaX_RBV".into(), ParamInfo::float64(sp.sigma_x, "SIGMAX_VALUE"));
999 map.insert("SigmaY_RBV".into(), ParamInfo::float64(sp.sigma_y, "SIGMAY_VALUE"));
1000 map.insert("SigmaXY_RBV".into(), ParamInfo::float64(sp.sigma_xy, "SIGMAXY_VALUE"));
1001
1002 map.insert("SkewnessX_RBV".into(), ParamInfo::float64(sp.skewness_x, "SKEWNESSX_VALUE"));
1004 map.insert("SkewnessY_RBV".into(), ParamInfo::float64(sp.skewness_y, "SKEWNESSY_VALUE"));
1005 map.insert("KurtosisX_RBV".into(), ParamInfo::float64(sp.kurtosis_x, "KURTOSISX_VALUE"));
1006 map.insert("KurtosisY_RBV".into(), ParamInfo::float64(sp.kurtosis_y, "KURTOSISY_VALUE"));
1007 map.insert("Eccentricity_RBV".into(), ParamInfo::float64(sp.eccentricity, "ECCENTRICITY_VALUE"));
1008 map.insert("Orientation_RBV".into(), ParamInfo::float64(sp.orientation, "ORIENTATION_VALUE"));
1009
1010 map.insert("ComputeHistogram".into(), ParamInfo::int32(sp.compute_histogram, "COMPUTE_HISTOGRAM"));
1012 map.insert("ComputeHistogram_RBV".into(), ParamInfo::int32(sp.compute_histogram, "COMPUTE_HISTOGRAM"));
1013 map.insert("HistSize".into(), ParamInfo::int32(sp.hist_size, "HIST_SIZE"));
1014 map.insert("HistSize_RBV".into(), ParamInfo::int32(sp.hist_size, "HIST_SIZE"));
1015 map.insert("HistMin".into(), ParamInfo::float64(sp.hist_min, "HIST_MIN"));
1016 map.insert("HistMin_RBV".into(), ParamInfo::float64(sp.hist_min, "HIST_MIN"));
1017 map.insert("HistMax".into(), ParamInfo::float64(sp.hist_max, "HIST_MAX"));
1018 map.insert("HistMax_RBV".into(), ParamInfo::float64(sp.hist_max, "HIST_MAX"));
1019 map.insert("HistBelow_RBV".into(), ParamInfo::float64(sp.hist_below, "HIST_BELOW"));
1020 map.insert("HistAbove_RBV".into(), ParamInfo::float64(sp.hist_above, "HIST_ABOVE"));
1021 map.insert("HistEntropy_RBV".into(), ParamInfo::float64(sp.hist_entropy, "HIST_ENTROPY"));
1022
1023 map.insert("ComputeProfiles".into(), ParamInfo::int32(sp.compute_profiles, "COMPUTE_PROFILES"));
1025 map.insert("ComputeProfiles_RBV".into(), ParamInfo::int32(sp.compute_profiles, "COMPUTE_PROFILES"));
1026 map.insert("CursorX".into(), ParamInfo::int32(sp.cursor_x, "CURSOR_X"));
1027 map.insert("CursorX_RBV".into(), ParamInfo::int32(sp.cursor_x, "CURSOR_X"));
1028 map.insert("CursorY".into(), ParamInfo::int32(sp.cursor_y, "CURSOR_Y"));
1029 map.insert("CursorY_RBV".into(), ParamInfo::int32(sp.cursor_y, "CURSOR_Y"));
1030
1031 map
1032}
1033
1034pub fn create_stats_runtime(
1044 port_name: &str,
1045 pool: Arc<NDArrayPool>,
1046 queue_size: usize,
1047 ndarray_port: &str,
1048 wiring: Arc<WiringRegistry>,
1049) -> (
1050 PluginRuntimeHandle,
1051 Arc<Mutex<StatsResult>>,
1052 NDStatsParams,
1053 asyn_rs::runtime::port::PortRuntimeHandle,
1054 crate::time_series::TSParams,
1055 std::thread::JoinHandle<()>,
1056 std::thread::JoinHandle<()>,
1057 std::thread::JoinHandle<()>,
1058) {
1059 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
1061
1062 let mut processor = StatsProcessor::new();
1063 processor.set_ts_sender(ts_tx);
1064 let stats_handle = processor.stats_handle();
1065 let params_handle = processor.params_handle();
1066
1067 let (plugin_handle, data_jh) = ad_core::plugin::runtime::create_plugin_runtime(
1068 port_name,
1069 processor,
1070 pool,
1071 queue_size,
1072 ndarray_port,
1073 wiring,
1074 );
1075
1076 let stats_params = *params_handle.lock();
1079
1080 let ts_port_name = format!("{port_name}_TS");
1082 let (ts_runtime, ts_params, ts_actor_jh, ts_data_jh) =
1083 crate::time_series::create_ts_port_runtime(
1084 &ts_port_name,
1085 &crate::time_series::STATS_TS_CHANNEL_NAMES,
1086 2048,
1087 ts_rx,
1088 );
1089
1090 (plugin_handle, stats_handle, stats_params, ts_runtime, ts_params, data_jh, ts_actor_jh, ts_data_jh)
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096 use ad_core::ndarray::{NDDataType, NDDimension};
1097
1098 #[test]
1099 fn test_compute_stats_u8() {
1100 let dims = vec![NDDimension::new(5)];
1101 let data = NDDataBuffer::U8(vec![10, 20, 30, 40, 50]);
1102 let stats = compute_stats(&data, &dims, 0);
1103 assert_eq!(stats.min, 10.0);
1104 assert_eq!(stats.max, 50.0);
1105 assert_eq!(stats.mean, 30.0);
1106 assert_eq!(stats.total, 150.0);
1107 assert_eq!(stats.num_elements, 5);
1108 }
1109
1110 #[test]
1111 fn test_compute_stats_sigma() {
1112 let dims = vec![NDDimension::new(8)];
1113 let data = NDDataBuffer::F64(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
1114 let stats = compute_stats(&data, &dims, 0);
1115 assert!((stats.mean - 5.0).abs() < 1e-10);
1116 assert!((stats.sigma - 2.0).abs() < 1e-10);
1117 }
1118
1119 #[test]
1120 fn test_compute_stats_u16() {
1121 let dims = vec![NDDimension::new(3)];
1122 let data = NDDataBuffer::U16(vec![100, 200, 300]);
1123 let stats = compute_stats(&data, &dims, 0);
1124 assert_eq!(stats.min, 100.0);
1125 assert_eq!(stats.max, 300.0);
1126 assert_eq!(stats.mean, 200.0);
1127 }
1128
1129 #[test]
1130 fn test_compute_stats_f64() {
1131 let dims = vec![NDDimension::new(3)];
1132 let data = NDDataBuffer::F64(vec![1.5, 2.5, 3.5]);
1133 let stats = compute_stats(&data, &dims, 0);
1134 assert!((stats.min - 1.5).abs() < 1e-10);
1135 assert!((stats.max - 3.5).abs() < 1e-10);
1136 assert!((stats.mean - 2.5).abs() < 1e-10);
1137 }
1138
1139 #[test]
1140 fn test_compute_stats_single_element() {
1141 let dims = vec![NDDimension::new(1)];
1142 let data = NDDataBuffer::I32(vec![42]);
1143 let stats = compute_stats(&data, &dims, 0);
1144 assert_eq!(stats.min, 42.0);
1145 assert_eq!(stats.max, 42.0);
1146 assert_eq!(stats.mean, 42.0);
1147 assert_eq!(stats.sigma, 0.0);
1148 assert_eq!(stats.num_elements, 1);
1149 }
1150
1151 #[test]
1152 fn test_compute_stats_empty() {
1153 let data = NDDataBuffer::U8(vec![]);
1154 let stats = compute_stats(&data, &[], 0);
1155 assert_eq!(stats.num_elements, 0);
1156 }
1157
1158 #[test]
1159 fn test_compute_stats_min_max_position() {
1160 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1161 let data = NDDataBuffer::U8((1..=16).collect());
1163 let stats = compute_stats(&data, &dims, 0);
1164 assert_eq!(stats.min_x, 0); assert_eq!(stats.min_y, 0);
1166 assert_eq!(stats.max_x, 3); assert_eq!(stats.max_y, 3);
1168 }
1169
1170 #[test]
1171 fn test_compute_stats_net_no_bgd() {
1172 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1173 let data = NDDataBuffer::U8((1..=16).collect());
1174 let stats = compute_stats(&data, &dims, 0);
1175 assert_eq!(stats.net, stats.total);
1177 }
1178
1179 #[test]
1180 fn test_compute_stats_bgd_subtraction() {
1181 let dims = vec![NDDimension::new(4), NDDimension::new(4)];
1183 let mut pixels = vec![10u16; 16];
1184 pixels[2 * 4 + 2] = 110;
1186 let data = NDDataBuffer::U16(pixels);
1187 let stats = compute_stats(&data, &dims, 1);
1188
1189 assert!((stats.net - 100.0).abs() < 1e-10);
1198 }
1199
1200 #[test]
1201 fn test_centroid_uniform() {
1202 let data = NDDataBuffer::U8(vec![1; 16]);
1203 let c = compute_centroid(&data, 4, 4, 0.0);
1204 assert!((c.centroid_x - 1.5).abs() < 1e-10);
1205 assert!((c.centroid_y - 1.5).abs() < 1e-10);
1206 }
1207
1208 #[test]
1209 fn test_centroid_corner() {
1210 let mut d = vec![0u8; 16];
1211 d[0] = 255;
1212 let data = NDDataBuffer::U8(d);
1213 let c = compute_centroid(&data, 4, 4, 0.0);
1214 assert!((c.centroid_x - 0.0).abs() < 1e-10);
1215 assert!((c.centroid_y - 0.0).abs() < 1e-10);
1216 }
1217
1218 #[test]
1219 fn test_centroid_threshold() {
1220 let mut pixels = vec![5u8; 16];
1222 pixels[2 * 4 + 2] = 100;
1223 let data = NDDataBuffer::U8(pixels);
1224
1225 let c = compute_centroid(&data, 4, 4, 50.0);
1227 assert!((c.centroid_x - 2.0).abs() < 1e-10);
1228 assert!((c.centroid_y - 2.0).abs() < 1e-10);
1229 assert!((c.centroid_total - 100.0).abs() < 1e-10);
1230 }
1231
1232 #[test]
1233 fn test_centroid_higher_moments_symmetric() {
1234 let data = NDDataBuffer::U8(vec![1; 16]);
1236 let c = compute_centroid(&data, 4, 4, 0.0);
1237 assert!(c.skewness_x.abs() < 1e-10);
1239 assert!(c.skewness_y.abs() < 1e-10);
1240 assert!(c.eccentricity.abs() < 1e-10);
1242 }
1243
1244 #[test]
1245 fn test_histogram_basic() {
1246 let data = NDDataBuffer::F64((0..10).map(|x| x as f64).collect());
1248 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 9.0);
1249 assert_eq!(hist.len(), 10);
1250 assert_eq!(below, 0.0);
1251 assert_eq!(above, 0.0);
1252 let total: f64 = hist.iter().sum();
1254 assert!((total - 10.0).abs() < 1e-10);
1255 assert!((entropy - 10.0f64.ln()).abs() < 0.1);
1257 }
1258
1259 #[test]
1260 fn test_histogram_below_above() {
1261 let data = NDDataBuffer::F64(vec![-1.0, 0.5, 1.5, 3.0]);
1262 let (hist, below, above, _entropy) = compute_histogram(&data, 2, 0.0, 2.0);
1263 assert_eq!(below, 1.0); assert_eq!(above, 1.0); let total_in_bins: f64 = hist.iter().sum();
1266 assert!((total_in_bins - 2.0).abs() < 1e-10); }
1268
1269 #[test]
1270 fn test_histogram_single_value() {
1271 let data = NDDataBuffer::F64(vec![5.0; 100]);
1272 let (hist, below, above, entropy) = compute_histogram(&data, 10, 0.0, 10.0);
1273 assert_eq!(below, 0.0);
1274 assert_eq!(above, 0.0);
1275 assert!((entropy - 0.0).abs() < 1e-10);
1277 let total: f64 = hist.iter().sum();
1278 assert!((total - 100.0).abs() < 1e-10);
1279 }
1280
1281 #[test]
1282 fn test_profiles_8x8() {
1283 let mut pixels = vec![0.0f64; 64];
1285 for iy in 0..8 {
1286 for ix in 0..8 {
1287 pixels[iy * 8 + ix] = iy as f64;
1288 }
1289 }
1290 let data = NDDataBuffer::F64(pixels);
1291
1292 let profiles = compute_profiles(
1293 &data, 8, 8,
1294 0.0, 3.5, 3.5, 0, 7, );
1300
1301 assert_eq!(profiles.avg_x.len(), 8);
1303 for &v in &profiles.avg_x {
1304 assert!((v - 3.5).abs() < 1e-10, "avg_x should be 3.5, got {v}");
1305 }
1306
1307 assert_eq!(profiles.avg_y.len(), 8);
1309 for (iy, &v) in profiles.avg_y.iter().enumerate() {
1310 assert!((v - iy as f64).abs() < 1e-10, "avg_y[{iy}] should be {iy}, got {v}");
1311 }
1312
1313 assert_eq!(profiles.cursor_x.len(), 8);
1315 for &v in &profiles.cursor_x {
1316 assert!((v - 7.0).abs() < 1e-10);
1317 }
1318
1319 assert_eq!(profiles.cursor_y.len(), 8);
1321 for (iy, &v) in profiles.cursor_y.iter().enumerate() {
1322 assert!((v - iy as f64).abs() < 1e-10);
1323 }
1324
1325 assert_eq!(profiles.centroid_x.len(), 8);
1327 for &v in &profiles.centroid_x {
1328 assert!((v - 4.0).abs() < 1e-10);
1329 }
1330
1331 assert_eq!(profiles.centroid_y.len(), 8);
1333 for (iy, &v) in profiles.centroid_y.iter().enumerate() {
1334 assert!((v - iy as f64).abs() < 1e-10);
1335 }
1336 }
1337
1338 #[test]
1339 fn test_profiles_threshold() {
1340 let mut pixels = vec![1.0f64; 16];
1342 pixels[1 * 4 + 2] = 10.0;
1343 let data = NDDataBuffer::F64(pixels);
1344
1345 let profiles = compute_profiles(
1346 &data, 4, 4,
1347 5.0, 2.0, 1.0,
1349 0, 0,
1350 );
1351
1352 assert_eq!(profiles.threshold_x.len(), 4);
1354 assert!((profiles.threshold_x[2] - 10.0).abs() < 1e-10);
1355 assert!((profiles.threshold_x[0] - 0.0).abs() < 1e-10);
1357 assert!((profiles.threshold_x[1] - 0.0).abs() < 1e-10);
1358 assert!((profiles.threshold_x[3] - 0.0).abs() < 1e-10);
1359
1360 assert_eq!(profiles.threshold_y.len(), 4);
1362 assert!((profiles.threshold_y[1] - 10.0).abs() < 1e-10);
1363 assert!((profiles.threshold_y[0] - 0.0).abs() < 1e-10);
1364 }
1365
1366 #[test]
1367 fn test_stats_processor_direct() {
1368 let mut proc = StatsProcessor::new();
1369 let pool = NDArrayPool::new(1_000_000);
1370
1371 let mut arr = NDArray::new(vec![NDDimension::new(5)], NDDataType::UInt8);
1372 if let NDDataBuffer::U8(ref mut v) = arr.data {
1373 v[0] = 10; v[1] = 20; v[2] = 30; v[3] = 40; v[4] = 50;
1374 }
1375
1376 let result = proc.process_array(&arr, &pool);
1377 assert!(result.output_arrays.is_empty(), "stats is a sink");
1378
1379 let stats = proc.stats_handle().lock().clone();
1380 assert_eq!(stats.min, 10.0);
1381 assert_eq!(stats.max, 50.0);
1382 assert_eq!(stats.mean, 30.0);
1383 }
1384
1385 #[test]
1386 fn test_stats_runtime_end_to_end() {
1387 let pool = Arc::new(NDArrayPool::new(1_000_000));
1388 let wiring = Arc::new(WiringRegistry::new());
1389 let (handle, stats, _params, _ts_runtime, _ts_params, _jh, _ts_actor_jh, _ts_data_jh) =
1390 create_stats_runtime("STATS_RT", pool, 10, "", wiring);
1391
1392 handle.port_runtime().port_handle()
1394 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1).unwrap();
1395 std::thread::sleep(std::time::Duration::from_millis(10));
1396
1397 let mut arr = NDArray::new(
1398 vec![NDDimension::new(4), NDDimension::new(4)],
1399 NDDataType::UInt8,
1400 );
1401 if let NDDataBuffer::U8(ref mut v) = arr.data {
1402 for (i, val) in v.iter_mut().enumerate() {
1403 *val = (i + 1) as u8;
1404 }
1405 }
1406
1407 handle.array_sender().send(Arc::new(arr));
1408 std::thread::sleep(std::time::Duration::from_millis(100));
1409
1410 let result = stats.lock().clone();
1411 assert_eq!(result.min, 1.0);
1412 assert_eq!(result.max, 16.0);
1413 assert_eq!(result.num_elements, 16);
1414 }
1415}