1use std::sync::Arc;
8
9use ad_core_rs::ndarray::{NDArray, NDDataBuffer};
10use ad_core_rs::ndarray_pool::NDArrayPool;
11use ad_core_rs::plugin::runtime::{
12 NDPluginProcess, ParamUpdate, PluginParamSnapshot, PluginRuntimeHandle, ProcessResult,
13};
14use ad_core_rs::plugin::wiring::WiringRegistry;
15use asyn_rs::param::ParamType;
16use asyn_rs::port::PortDriverBase;
17use parking_lot::Mutex;
18
19#[cfg(feature = "parallel")]
20use crate::par_util;
21use crate::time_series::{TimeSeriesData, TimeSeriesSender};
22#[cfg(feature = "parallel")]
23use rayon::prelude::*;
24
25#[derive(Debug, Clone)]
27pub struct ROIStatROI {
28 pub enabled: bool,
29 pub offset: [usize; 2],
31 pub size: [usize; 2],
33 pub bgd_width: usize,
35}
36
37impl Default for ROIStatROI {
38 fn default() -> Self {
39 Self {
40 enabled: true,
41 offset: [0, 0],
42 size: [0, 0],
43 bgd_width: 0,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct ROIStatResult {
51 pub min: f64,
52 pub max: f64,
53 pub mean: f64,
54 pub total: f64,
55 pub net: f64,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum TSMode {
62 Idle,
63 Acquiring,
64}
65
66const NUM_STATS: usize = 5;
68
69const ROI_STAT_NAMES: [&str; NUM_STATS] = ["MinValue", "MaxValue", "MeanValue", "Total", "Net"];
71
72pub fn roi_stat_ts_channel_names(num_rois: usize) -> Vec<String> {
75 let mut names = Vec::with_capacity(num_rois * NUM_STATS);
76 for roi_idx in 0..num_rois {
77 for stat_name in &ROI_STAT_NAMES {
78 names.push(format!("TS{}:{}", roi_idx + 1, stat_name));
79 }
80 }
81 names
82}
83
84#[derive(Clone, Copy, Default)]
88pub struct ROIStatParams {
89 pub reset_all: usize,
91 pub ts_control: usize,
92 pub ts_num_points: usize,
93 pub ts_current_point: usize,
94 pub ts_acquiring: usize,
95 pub use_: usize,
97 pub name: usize,
98 pub reset: usize,
99 pub bgd_width: usize,
100 pub dim0_min: usize,
101 pub dim1_min: usize,
102 pub dim0_size: usize,
103 pub dim1_size: usize,
104 pub dim0_max_size: usize,
105 pub dim1_max_size: usize,
106 pub min_value: usize,
107 pub max_value: usize,
108 pub mean_value: usize,
109 pub total: usize,
110 pub net: usize,
111}
112
113pub struct ROIStatProcessor {
115 rois: Vec<ROIStatROI>,
116 results: Vec<ROIStatResult>,
117 ts_mode: TSMode,
119 ts_buffers: Vec<Vec<Vec<f64>>>,
120 ts_num_points: usize,
121 ts_current: usize,
122 ts_sender: Option<TimeSeriesSender>,
124 params: ROIStatParams,
126 params_out: Arc<Mutex<ROIStatParams>>,
128}
129
130impl ROIStatProcessor {
131 pub fn new(rois: Vec<ROIStatROI>, ts_num_points: usize) -> Self {
133 let n = rois.len();
134 let results = vec![ROIStatResult::default(); n];
135 let ts_buffers = vec![vec![Vec::new(); NUM_STATS]; n];
136 Self {
137 rois,
138 results,
139 ts_mode: TSMode::Idle,
140 ts_buffers,
141 ts_num_points,
142 ts_current: 0,
143 ts_sender: None,
144 params: ROIStatParams::default(),
145 params_out: Arc::new(Mutex::new(ROIStatParams::default())),
146 }
147 }
148
149 pub fn params_handle(&self) -> Arc<Mutex<ROIStatParams>> {
151 self.params_out.clone()
152 }
153
154 pub fn results(&self) -> &[ROIStatResult] {
156 &self.results
157 }
158
159 pub fn rois(&self) -> &[ROIStatROI] {
161 &self.rois
162 }
163
164 pub fn rois_mut(&mut self) -> &mut Vec<ROIStatROI> {
166 &mut self.rois
167 }
168
169 pub fn set_ts_mode(&mut self, mode: TSMode) {
171 if mode == TSMode::Acquiring && self.ts_mode != TSMode::Acquiring {
172 for roi_bufs in &mut self.ts_buffers {
174 for stat_buf in roi_bufs.iter_mut() {
175 stat_buf.clear();
176 }
177 }
178 self.ts_current = 0;
179 }
180 self.ts_mode = mode;
181 }
182
183 pub fn ts_buffer(&self, roi_index: usize, stat_index: usize) -> &[f64] {
186 if roi_index < self.ts_buffers.len() && stat_index < NUM_STATS {
187 &self.ts_buffers[roi_index][stat_index]
188 } else {
189 &[]
190 }
191 }
192
193 pub fn set_ts_sender(&mut self, sender: TimeSeriesSender) {
195 self.ts_sender = Some(sender);
196 }
197
198 pub fn compute_roi_stats(
200 data: &NDDataBuffer,
201 x_size: usize,
202 y_size: usize,
203 roi: &ROIStatROI,
204 ) -> ROIStatResult {
205 let roi_x = roi.offset[0];
206 let roi_y = roi.offset[1];
207 let roi_w = roi.size[0];
208 let roi_h = roi.size[1];
209
210 if roi_x >= x_size || roi_y >= y_size || roi_w == 0 || roi_h == 0 {
212 return ROIStatResult::default();
213 }
214 let roi_w = roi_w.min(x_size - roi_x);
215 let roi_h = roi_h.min(y_size - roi_y);
216
217 let mut min = f64::MAX;
218 let mut max = f64::MIN;
219 let mut total = 0.0f64;
220 let mut count = 0usize;
221
222 for iy in roi_y..(roi_y + roi_h) {
223 for ix in roi_x..(roi_x + roi_w) {
224 let idx = iy * x_size + ix;
225 if let Some(val) = data.get_as_f64(idx) {
226 if val < min {
227 min = val;
228 }
229 if val > max {
230 max = val;
231 }
232 total += val;
233 count += 1;
234 }
235 }
236 }
237
238 if count == 0 {
239 return ROIStatResult::default();
240 }
241
242 let mean = total / count as f64;
243
244 let net = if roi.bgd_width > 0 {
246 let bgd = Self::compute_background(data, x_size, y_size, roi);
247 total - bgd * count as f64
248 } else {
249 total
250 };
251
252 ROIStatResult {
253 min,
254 max,
255 mean,
256 total,
257 net,
258 }
259 }
260
261 fn compute_background(
263 data: &NDDataBuffer,
264 x_size: usize,
265 y_size: usize,
266 roi: &ROIStatROI,
267 ) -> f64 {
268 let roi_x = roi.offset[0];
269 let roi_y = roi.offset[1];
270 let roi_w = roi.size[0].min(x_size.saturating_sub(roi_x));
271 let roi_h = roi.size[1].min(y_size.saturating_sub(roi_y));
272 let bw = roi.bgd_width;
273
274 if bw == 0 || roi_w == 0 || roi_h == 0 {
275 return 0.0;
276 }
277
278 let mut bgd_total = 0.0f64;
279 let mut bgd_count = 0usize;
280
281 for iy in roi_y..(roi_y + roi_h) {
282 for ix in roi_x..(roi_x + roi_w) {
283 let dx_from_left = ix - roi_x;
285 let dx_from_right = (roi_x + roi_w - 1) - ix;
286 let dy_from_top = iy - roi_y;
287 let dy_from_bottom = (roi_y + roi_h - 1) - iy;
288
289 let in_border = dx_from_left < bw
290 || dx_from_right < bw
291 || dy_from_top < bw
292 || dy_from_bottom < bw;
293
294 if in_border {
295 let idx = iy * x_size + ix;
296 if let Some(val) = data.get_as_f64(idx) {
297 bgd_total += val;
298 bgd_count += 1;
299 }
300 }
301 }
302 }
303
304 if bgd_count == 0 {
305 0.0
306 } else {
307 bgd_total / bgd_count as f64
308 }
309 }
310}
311
312impl NDPluginProcess for ROIStatProcessor {
313 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
314 let info = array.info();
315 let x_size = info.x_size;
316 let y_size = info.y_size;
317
318 self.results
320 .resize(self.rois.len(), ROIStatResult::default());
321
322 #[cfg(feature = "parallel")]
323 {
324 let total_elements: usize = self
325 .rois
326 .iter()
327 .filter(|r| r.enabled)
328 .map(|r| r.size[0] * r.size[1])
329 .sum();
330
331 if par_util::should_parallelize(total_elements) {
332 let data = &array.data;
333 let rois = &self.rois;
334 let new_results: Vec<ROIStatResult> = par_util::thread_pool().install(|| {
335 rois.par_iter()
336 .map(|roi| {
337 if roi.enabled {
338 Self::compute_roi_stats(data, x_size, y_size, roi)
339 } else {
340 ROIStatResult::default()
341 }
342 })
343 .collect()
344 });
345 self.results = new_results;
346 } else {
347 for (i, roi) in self.rois.iter().enumerate() {
348 if !roi.enabled {
349 self.results[i] = ROIStatResult::default();
350 continue;
351 }
352 self.results[i] = Self::compute_roi_stats(&array.data, x_size, y_size, roi);
353 }
354 }
355 }
356
357 #[cfg(not(feature = "parallel"))]
358 for (i, roi) in self.rois.iter().enumerate() {
359 if !roi.enabled {
360 self.results[i] = ROIStatResult::default();
361 continue;
362 }
363 self.results[i] = Self::compute_roi_stats(&array.data, x_size, y_size, roi);
364 }
365
366 if self.ts_mode == TSMode::Acquiring {
368 while self.ts_buffers.len() < self.rois.len() {
370 self.ts_buffers.push(vec![Vec::new(); NUM_STATS]);
371 }
372
373 for (i, result) in self.results.iter().enumerate() {
374 if i >= self.ts_buffers.len() {
375 break;
376 }
377 let stats = [
378 result.min,
379 result.max,
380 result.mean,
381 result.total,
382 result.net,
383 ];
384 for (s, &val) in stats.iter().enumerate() {
385 let buf = &mut self.ts_buffers[i][s];
386 if buf.len() >= self.ts_num_points && self.ts_num_points > 0 {
387 let idx = self.ts_current % self.ts_num_points;
389 if idx < buf.len() {
390 buf[idx] = val;
391 }
392 } else {
393 buf.push(val);
394 }
395 }
396 }
397 self.ts_current += 1;
398 }
399
400 if let Some(ref sender) = self.ts_sender {
402 let mut values = Vec::with_capacity(self.results.len() * NUM_STATS);
403 for result in &self.results {
404 values.push(result.min);
405 values.push(result.max);
406 values.push(result.mean);
407 values.push(result.total);
408 values.push(result.net);
409 }
410 let _ = sender.try_send(TimeSeriesData { values });
411 }
412
413 let p = &self.params;
415 let mut updates = Vec::new();
416 for (i, roi) in self.rois.iter().enumerate() {
417 if !roi.enabled {
418 continue;
419 }
420 let result = &self.results[i];
421 let addr = i as i32;
422 updates.push(ParamUpdate::float64_addr(p.min_value, addr, result.min));
423 updates.push(ParamUpdate::float64_addr(p.max_value, addr, result.max));
424 updates.push(ParamUpdate::float64_addr(p.mean_value, addr, result.mean));
425 updates.push(ParamUpdate::float64_addr(p.total, addr, result.total));
426 updates.push(ParamUpdate::float64_addr(p.net, addr, result.net));
427 updates.push(ParamUpdate::int32_addr(
428 p.dim0_max_size,
429 addr,
430 x_size as i32,
431 ));
432 updates.push(ParamUpdate::int32_addr(
433 p.dim1_max_size,
434 addr,
435 y_size as i32,
436 ));
437 }
438 updates.push(ParamUpdate::int32(
439 p.ts_current_point,
440 self.ts_current as i32,
441 ));
442 updates.push(ParamUpdate::int32(
443 p.ts_acquiring,
444 if self.ts_mode == TSMode::Acquiring {
445 1
446 } else {
447 0
448 },
449 ));
450
451 ProcessResult::sink(updates)
452 }
453
454 fn plugin_type(&self) -> &str {
455 "NDPluginROIStat"
456 }
457
458 fn register_params(
459 &mut self,
460 base: &mut PortDriverBase,
461 ) -> Result<(), asyn_rs::error::AsynError> {
462 self.params.reset_all = base.create_param("ROISTAT_RESETALL", ParamType::Int32)?;
464 self.params.ts_control = base.create_param("ROISTAT_TS_CONTROL", ParamType::Int32)?;
465 self.params.ts_num_points = base.create_param("ROISTAT_TS_NUM_POINTS", ParamType::Int32)?;
466 base.set_int32_param(self.params.ts_num_points, 0, self.ts_num_points as i32)?;
467 self.params.ts_current_point =
468 base.create_param("ROISTAT_TS_CURRENT_POINT", ParamType::Int32)?;
469 self.params.ts_acquiring = base.create_param("ROISTAT_TS_ACQUIRING", ParamType::Int32)?;
470
471 self.params.use_ = base.create_param("ROISTAT_USE", ParamType::Int32)?;
473 self.params.name = base.create_param("ROISTAT_NAME", ParamType::Octet)?;
474 self.params.reset = base.create_param("ROISTAT_RESET", ParamType::Int32)?;
475 self.params.bgd_width = base.create_param("ROISTAT_BGD_WIDTH", ParamType::Int32)?;
476 self.params.dim0_min = base.create_param("ROISTAT_DIM0_MIN", ParamType::Int32)?;
477 self.params.dim1_min = base.create_param("ROISTAT_DIM1_MIN", ParamType::Int32)?;
478 self.params.dim0_size = base.create_param("ROISTAT_DIM0_SIZE", ParamType::Int32)?;
479 self.params.dim1_size = base.create_param("ROISTAT_DIM1_SIZE", ParamType::Int32)?;
480 self.params.dim0_max_size = base.create_param("ROISTAT_DIM0_MAX_SIZE", ParamType::Int32)?;
481 self.params.dim1_max_size = base.create_param("ROISTAT_DIM1_MAX_SIZE", ParamType::Int32)?;
482 self.params.min_value = base.create_param("ROISTAT_MIN_VALUE", ParamType::Float64)?;
483 self.params.max_value = base.create_param("ROISTAT_MAX_VALUE", ParamType::Float64)?;
484 self.params.mean_value = base.create_param("ROISTAT_MEAN_VALUE", ParamType::Float64)?;
485 self.params.total = base.create_param("ROISTAT_TOTAL", ParamType::Float64)?;
486 self.params.net = base.create_param("ROISTAT_NET", ParamType::Float64)?;
487
488 for (i, roi) in self.rois.iter().enumerate() {
490 let addr = i as i32;
491 base.set_int32_param(self.params.use_, addr, roi.enabled as i32)?;
492 base.set_int32_param(self.params.bgd_width, addr, roi.bgd_width as i32)?;
493 base.set_int32_param(self.params.dim0_min, addr, roi.offset[0] as i32)?;
494 base.set_int32_param(self.params.dim1_min, addr, roi.offset[1] as i32)?;
495 base.set_int32_param(self.params.dim0_size, addr, roi.size[0] as i32)?;
496 base.set_int32_param(self.params.dim1_size, addr, roi.size[1] as i32)?;
497 }
498
499 *self.params_out.lock() = self.params;
501
502 Ok(())
503 }
504
505 fn on_param_change(
506 &mut self,
507 reason: usize,
508 snapshot: &PluginParamSnapshot,
509 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
510 let addr = snapshot.addr as usize;
511 let p = &self.params;
512
513 if reason == p.use_ && addr < self.rois.len() {
514 self.rois[addr].enabled = snapshot.value.as_i32() != 0;
515 } else if reason == p.dim0_min && addr < self.rois.len() {
516 self.rois[addr].offset[0] = snapshot.value.as_i32().max(0) as usize;
517 } else if reason == p.dim1_min && addr < self.rois.len() {
518 self.rois[addr].offset[1] = snapshot.value.as_i32().max(0) as usize;
519 } else if reason == p.dim0_size && addr < self.rois.len() {
520 self.rois[addr].size[0] = snapshot.value.as_i32().max(0) as usize;
521 } else if reason == p.dim1_size && addr < self.rois.len() {
522 self.rois[addr].size[1] = snapshot.value.as_i32().max(0) as usize;
523 } else if reason == p.bgd_width && addr < self.rois.len() {
524 self.rois[addr].bgd_width = snapshot.value.as_i32().max(0) as usize;
525 } else if reason == p.reset && addr < self.rois.len() {
526 self.results[addr] = ROIStatResult::default();
527 } else if reason == p.reset_all {
528 for r in &mut self.results {
529 *r = ROIStatResult::default();
530 }
531 } else if reason == p.ts_control {
532 let mode = if snapshot.value.as_i32() != 0 {
533 TSMode::Acquiring
534 } else {
535 TSMode::Idle
536 };
537 self.set_ts_mode(mode);
538 } else if reason == p.ts_num_points {
539 self.ts_num_points = snapshot.value.as_i32().max(0) as usize;
540 }
541 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
542 }
543}
544
545pub fn create_roi_stat_runtime(
548 port_name: &str,
549 pool: Arc<NDArrayPool>,
550 queue_size: usize,
551 ndarray_port: &str,
552 wiring: Arc<WiringRegistry>,
553 num_rois: usize,
554 ts_registry: &crate::time_series::TsReceiverRegistry,
555) -> (
556 PluginRuntimeHandle,
557 ROIStatParams,
558 std::thread::JoinHandle<()>,
559) {
560 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
561
562 let rois: Vec<ROIStatROI> = (0..num_rois).map(|_| ROIStatROI::default()).collect();
563 let mut processor = ROIStatProcessor::new(rois, 2048);
564 processor.set_ts_sender(ts_tx);
565 let params_handle = processor.params_handle();
566
567 let (handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime_multi_addr(
568 port_name,
569 processor,
570 pool,
571 queue_size,
572 ndarray_port,
573 wiring,
574 num_rois,
575 );
576
577 let roi_stat_params = *params_handle.lock();
578
579 let channel_names = roi_stat_ts_channel_names(num_rois);
581 ts_registry.store(port_name, ts_rx, channel_names);
582
583 (handle, roi_stat_params, data_jh)
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589 use ad_core_rs::ndarray::{NDDataType, NDDimension};
590
591 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
592 let mut arr = NDArray::new(
593 vec![NDDimension::new(x), NDDimension::new(y)],
594 NDDataType::Float64,
595 );
596 if let NDDataBuffer::F64(ref mut v) = arr.data {
597 for iy in 0..y {
598 for ix in 0..x {
599 v[iy * x + ix] = fill(ix, iy);
600 }
601 }
602 }
603 arr
604 }
605
606 #[test]
607 fn test_single_roi_full_image() {
608 let arr = make_2d_array(4, 4, |_x, _y| 10.0);
609 let rois = vec![ROIStatROI {
610 enabled: true,
611 offset: [0, 0],
612 size: [4, 4],
613 bgd_width: 0,
614 }];
615
616 let mut proc = ROIStatProcessor::new(rois, 0);
617 let pool = NDArrayPool::new(1_000_000);
618 proc.process_array(&arr, &pool);
619
620 let r = &proc.results()[0];
621 assert!((r.min - 10.0).abs() < 1e-10);
622 assert!((r.max - 10.0).abs() < 1e-10);
623 assert!((r.mean - 10.0).abs() < 1e-10);
624 assert!((r.total - 160.0).abs() < 1e-10);
625 }
626
627 #[test]
628 fn test_single_roi_subregion() {
629 let arr = make_2d_array(8, 8, |x, y| (x + y * 8) as f64);
631
632 let rois = vec![ROIStatROI {
633 enabled: true,
634 offset: [2, 2],
635 size: [3, 3],
636 bgd_width: 0,
637 }];
638
639 let mut proc = ROIStatProcessor::new(rois, 0);
640 let pool = NDArrayPool::new(1_000_000);
641 proc.process_array(&arr, &pool);
642
643 let r = &proc.results()[0];
644 assert!((r.min - 18.0).abs() < 1e-10);
646 assert!((r.max - 36.0).abs() < 1e-10);
647 let expected_total = 18.0 + 19.0 + 20.0 + 26.0 + 27.0 + 28.0 + 34.0 + 35.0 + 36.0;
648 assert!((r.total - expected_total).abs() < 1e-10);
649 assert!((r.mean - expected_total / 9.0).abs() < 1e-10);
650 }
651
652 #[test]
653 fn test_multiple_rois() {
654 let arr = make_2d_array(8, 8, |x, _y| x as f64);
655
656 let rois = vec![
657 ROIStatROI {
658 enabled: true,
659 offset: [0, 0],
660 size: [4, 4],
661 bgd_width: 0,
662 },
663 ROIStatROI {
664 enabled: true,
665 offset: [4, 0],
666 size: [4, 4],
667 bgd_width: 0,
668 },
669 ];
670
671 let mut proc = ROIStatProcessor::new(rois, 0);
672 let pool = NDArrayPool::new(1_000_000);
673 proc.process_array(&arr, &pool);
674
675 let r0 = &proc.results()[0];
676 assert!((r0.min - 0.0).abs() < 1e-10);
677 assert!((r0.max - 3.0).abs() < 1e-10);
678
679 let r1 = &proc.results()[1];
680 assert!((r1.min - 4.0).abs() < 1e-10);
681 assert!((r1.max - 7.0).abs() < 1e-10);
682 }
683
684 #[test]
685 fn test_bgd_width() {
686 let arr = make_2d_array(6, 6, |x, y| {
688 if x >= 2 && x < 4 && y >= 2 && y < 4 {
689 100.0
690 } else {
691 10.0
692 }
693 });
694
695 let rois = vec![ROIStatROI {
696 enabled: true,
697 offset: [1, 1],
698 size: [4, 4],
699 bgd_width: 1,
700 }];
701
702 let mut proc = ROIStatProcessor::new(rois, 0);
703 let pool = NDArrayPool::new(1_000_000);
704 proc.process_array(&arr, &pool);
705
706 let r = &proc.results()[0];
707 assert!(
712 r.net < r.total,
713 "net should be less than total with bgd subtraction"
714 );
715 }
716
717 #[test]
718 fn test_empty_roi() {
719 let arr = make_2d_array(4, 4, |_, _| 10.0);
720 let rois = vec![ROIStatROI {
721 enabled: true,
722 offset: [0, 0],
723 size: [0, 0],
724 bgd_width: 0,
725 }];
726
727 let mut proc = ROIStatProcessor::new(rois, 0);
728 let pool = NDArrayPool::new(1_000_000);
729 proc.process_array(&arr, &pool);
730
731 let r = &proc.results()[0];
732 assert!((r.total - 0.0).abs() < 1e-10);
733 }
734
735 #[test]
736 fn test_disabled_roi() {
737 let arr = make_2d_array(4, 4, |_, _| 10.0);
738 let rois = vec![ROIStatROI {
739 enabled: false,
740 offset: [0, 0],
741 size: [4, 4],
742 bgd_width: 0,
743 }];
744
745 let mut proc = ROIStatProcessor::new(rois, 0);
746 let pool = NDArrayPool::new(1_000_000);
747 proc.process_array(&arr, &pool);
748
749 let r = &proc.results()[0];
750 assert!(
751 (r.total - 0.0).abs() < 1e-10,
752 "disabled ROI should have zero stats"
753 );
754 }
755
756 #[test]
757 fn test_roi_out_of_bounds() {
758 let arr = make_2d_array(4, 4, |_, _| 10.0);
759 let rois = vec![ROIStatROI {
760 enabled: true,
761 offset: [10, 10],
762 size: [4, 4],
763 bgd_width: 0,
764 }];
765
766 let mut proc = ROIStatProcessor::new(rois, 0);
767 let pool = NDArrayPool::new(1_000_000);
768 proc.process_array(&arr, &pool);
769
770 let r = &proc.results()[0];
771 assert!(
772 (r.total - 0.0).abs() < 1e-10,
773 "out-of-bounds ROI should produce zero stats"
774 );
775 }
776
777 #[test]
778 fn test_roi_partially_out_of_bounds() {
779 let arr = make_2d_array(4, 4, |_, _| 5.0);
780 let rois = vec![ROIStatROI {
781 enabled: true,
782 offset: [2, 2],
783 size: [10, 10], bgd_width: 0,
785 }];
786
787 let mut proc = ROIStatProcessor::new(rois, 0);
788 let pool = NDArrayPool::new(1_000_000);
789 proc.process_array(&arr, &pool);
790
791 let r = &proc.results()[0];
792 assert!((r.total - 20.0).abs() < 1e-10);
794 assert!((r.mean - 5.0).abs() < 1e-10);
795 }
796
797 #[test]
798 fn test_time_series() {
799 let rois = vec![ROIStatROI {
800 enabled: true,
801 offset: [0, 0],
802 size: [4, 4],
803 bgd_width: 0,
804 }];
805
806 let mut proc = ROIStatProcessor::new(rois, 100);
807 let pool = NDArrayPool::new(1_000_000);
808 proc.set_ts_mode(TSMode::Acquiring);
809
810 for i in 0..5 {
811 let arr = make_2d_array(4, 4, |_, _| (i + 1) as f64);
812 proc.process_array(&arr, &pool);
813 }
814
815 let ts = proc.ts_buffer(0, 2);
817 assert_eq!(ts.len(), 5);
818 assert!((ts[0] - 1.0).abs() < 1e-10);
819 assert!((ts[4] - 5.0).abs() < 1e-10);
820 }
821
822 #[test]
823 fn test_u8_data() {
824 let mut arr = NDArray::new(
825 vec![NDDimension::new(4), NDDimension::new(4)],
826 NDDataType::UInt8,
827 );
828 if let NDDataBuffer::U8(ref mut v) = arr.data {
829 for (i, val) in v.iter_mut().enumerate() {
830 *val = (i + 1) as u8;
831 }
832 }
833
834 let rois = vec![ROIStatROI {
835 enabled: true,
836 offset: [0, 0],
837 size: [4, 4],
838 bgd_width: 0,
839 }];
840
841 let mut proc = ROIStatProcessor::new(rois, 0);
842 let pool = NDArrayPool::new(1_000_000);
843 proc.process_array(&arr, &pool);
844
845 let r = &proc.results()[0];
846 assert!((r.min - 1.0).abs() < 1e-10);
847 assert!((r.max - 16.0).abs() < 1e-10);
848 }
849
850 #[test]
851 fn test_ts_channel_names() {
852 let names = roi_stat_ts_channel_names(2);
853 assert_eq!(names.len(), 10); assert_eq!(names[0], "TS1:MinValue");
855 assert_eq!(names[1], "TS1:MaxValue");
856 assert_eq!(names[4], "TS1:Net");
857 assert_eq!(names[5], "TS2:MinValue");
858 assert_eq!(names[9], "TS2:Net");
859 }
860
861 #[test]
862 fn test_ts_sender_integration() {
863 let (tx, mut rx) = tokio::sync::mpsc::channel::<TimeSeriesData>(16);
864
865 let rois = vec![
866 ROIStatROI {
867 enabled: true,
868 offset: [0, 0],
869 size: [4, 4],
870 bgd_width: 0,
871 },
872 ROIStatROI {
873 enabled: true,
874 offset: [0, 0],
875 size: [2, 2],
876 bgd_width: 0,
877 },
878 ];
879
880 let mut proc = ROIStatProcessor::new(rois, 0);
881 proc.set_ts_sender(tx);
882
883 let pool = NDArrayPool::new(1_000_000);
884 let arr = make_2d_array(4, 4, |_, _| 7.0);
885 proc.process_array(&arr, &pool);
886
887 let data = rx.try_recv().unwrap();
888 assert_eq!(data.values.len(), 10);
890 assert!((data.values[0] - 7.0).abs() < 1e-10); assert!((data.values[1] - 7.0).abs() < 1e-10); assert!((data.values[2] - 7.0).abs() < 1e-10); assert!((data.values[3] - 112.0).abs() < 1e-10); assert!((data.values[8] - 28.0).abs() < 1e-10); }
898}