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, result) in self.results.iter().enumerate() {
417 let addr = i as i32;
418 updates.push(ParamUpdate::float64_addr(p.min_value, addr, result.min));
419 updates.push(ParamUpdate::float64_addr(p.max_value, addr, result.max));
420 updates.push(ParamUpdate::float64_addr(p.mean_value, addr, result.mean));
421 updates.push(ParamUpdate::float64_addr(p.total, addr, result.total));
422 updates.push(ParamUpdate::float64_addr(p.net, addr, result.net));
423 updates.push(ParamUpdate::int32_addr(
424 p.dim0_max_size,
425 addr,
426 x_size as i32,
427 ));
428 updates.push(ParamUpdate::int32_addr(
429 p.dim1_max_size,
430 addr,
431 y_size as i32,
432 ));
433 }
434 updates.push(ParamUpdate::int32(
435 p.ts_current_point,
436 self.ts_current as i32,
437 ));
438 updates.push(ParamUpdate::int32(
439 p.ts_acquiring,
440 if self.ts_mode == TSMode::Acquiring {
441 1
442 } else {
443 0
444 },
445 ));
446
447 ProcessResult::sink(updates)
448 }
449
450 fn plugin_type(&self) -> &str {
451 "NDPluginROIStat"
452 }
453
454 fn register_params(
455 &mut self,
456 base: &mut PortDriverBase,
457 ) -> Result<(), asyn_rs::error::AsynError> {
458 self.params.reset_all = base.create_param("ROISTAT_RESETALL", ParamType::Int32)?;
460 self.params.ts_control = base.create_param("ROISTAT_TS_CONTROL", ParamType::Int32)?;
461 self.params.ts_num_points = base.create_param("ROISTAT_TS_NUM_POINTS", ParamType::Int32)?;
462 base.set_int32_param(self.params.ts_num_points, 0, self.ts_num_points as i32)?;
463 self.params.ts_current_point =
464 base.create_param("ROISTAT_TS_CURRENT_POINT", ParamType::Int32)?;
465 self.params.ts_acquiring = base.create_param("ROISTAT_TS_ACQUIRING", ParamType::Int32)?;
466
467 self.params.use_ = base.create_param("ROISTAT_USE", ParamType::Int32)?;
469 self.params.name = base.create_param("ROISTAT_NAME", ParamType::Octet)?;
470 self.params.reset = base.create_param("ROISTAT_RESET", ParamType::Int32)?;
471 self.params.bgd_width = base.create_param("ROISTAT_BGD_WIDTH", ParamType::Int32)?;
472 self.params.dim0_min = base.create_param("ROISTAT_DIM0_MIN", ParamType::Int32)?;
473 self.params.dim1_min = base.create_param("ROISTAT_DIM1_MIN", ParamType::Int32)?;
474 self.params.dim0_size = base.create_param("ROISTAT_DIM0_SIZE", ParamType::Int32)?;
475 self.params.dim1_size = base.create_param("ROISTAT_DIM1_SIZE", ParamType::Int32)?;
476 self.params.dim0_max_size = base.create_param("ROISTAT_DIM0_MAX_SIZE", ParamType::Int32)?;
477 self.params.dim1_max_size = base.create_param("ROISTAT_DIM1_MAX_SIZE", ParamType::Int32)?;
478 self.params.min_value = base.create_param("ROISTAT_MIN_VALUE", ParamType::Float64)?;
479 self.params.max_value = base.create_param("ROISTAT_MAX_VALUE", ParamType::Float64)?;
480 self.params.mean_value = base.create_param("ROISTAT_MEAN_VALUE", ParamType::Float64)?;
481 self.params.total = base.create_param("ROISTAT_TOTAL", ParamType::Float64)?;
482 self.params.net = base.create_param("ROISTAT_NET", ParamType::Float64)?;
483
484 for (i, roi) in self.rois.iter().enumerate() {
486 let addr = i as i32;
487 base.set_int32_param(self.params.use_, addr, roi.enabled as i32)?;
488 base.set_int32_param(self.params.bgd_width, addr, roi.bgd_width as i32)?;
489 base.set_int32_param(self.params.dim0_min, addr, roi.offset[0] as i32)?;
490 base.set_int32_param(self.params.dim1_min, addr, roi.offset[1] as i32)?;
491 base.set_int32_param(self.params.dim0_size, addr, roi.size[0] as i32)?;
492 base.set_int32_param(self.params.dim1_size, addr, roi.size[1] as i32)?;
493 }
494
495 *self.params_out.lock() = self.params;
497
498 Ok(())
499 }
500
501 fn on_param_change(
502 &mut self,
503 reason: usize,
504 snapshot: &PluginParamSnapshot,
505 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
506 let addr = snapshot.addr as usize;
507 let p = &self.params;
508
509 if reason == p.use_ && addr < self.rois.len() {
510 self.rois[addr].enabled = snapshot.value.as_i32() != 0;
511 } else if reason == p.dim0_min && addr < self.rois.len() {
512 self.rois[addr].offset[0] = snapshot.value.as_i32().max(0) as usize;
513 } else if reason == p.dim1_min && addr < self.rois.len() {
514 self.rois[addr].offset[1] = snapshot.value.as_i32().max(0) as usize;
515 } else if reason == p.dim0_size && addr < self.rois.len() {
516 self.rois[addr].size[0] = snapshot.value.as_i32().max(0) as usize;
517 } else if reason == p.dim1_size && addr < self.rois.len() {
518 self.rois[addr].size[1] = snapshot.value.as_i32().max(0) as usize;
519 } else if reason == p.bgd_width && addr < self.rois.len() {
520 self.rois[addr].bgd_width = snapshot.value.as_i32().max(0) as usize;
521 } else if reason == p.reset && addr < self.rois.len() {
522 self.results[addr] = ROIStatResult::default();
523 } else if reason == p.reset_all {
524 for r in &mut self.results {
525 *r = ROIStatResult::default();
526 }
527 } else if reason == p.ts_control {
528 let mode = if snapshot.value.as_i32() != 0 {
529 TSMode::Acquiring
530 } else {
531 TSMode::Idle
532 };
533 self.set_ts_mode(mode);
534 } else if reason == p.ts_num_points {
535 self.ts_num_points = snapshot.value.as_i32().max(0) as usize;
536 }
537 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
538 }
539}
540
541pub fn create_roi_stat_runtime(
544 port_name: &str,
545 pool: Arc<NDArrayPool>,
546 queue_size: usize,
547 ndarray_port: &str,
548 wiring: Arc<WiringRegistry>,
549 num_rois: usize,
550 ts_registry: &crate::time_series::TsReceiverRegistry,
551) -> (
552 PluginRuntimeHandle,
553 ROIStatParams,
554 std::thread::JoinHandle<()>,
555) {
556 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
557
558 let rois: Vec<ROIStatROI> = (0..num_rois).map(|_| ROIStatROI::default()).collect();
559 let mut processor = ROIStatProcessor::new(rois, 2048);
560 processor.set_ts_sender(ts_tx);
561 let params_handle = processor.params_handle();
562
563 let (handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime_multi_addr(
564 port_name,
565 processor,
566 pool,
567 queue_size,
568 ndarray_port,
569 wiring,
570 num_rois,
571 );
572
573 let roi_stat_params = *params_handle.lock();
574
575 let channel_names = roi_stat_ts_channel_names(num_rois);
577 ts_registry.store(port_name, ts_rx, channel_names);
578
579 (handle, roi_stat_params, data_jh)
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use ad_core_rs::ndarray::{NDDataType, NDDimension};
586
587 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
588 let mut arr = NDArray::new(
589 vec![NDDimension::new(x), NDDimension::new(y)],
590 NDDataType::Float64,
591 );
592 if let NDDataBuffer::F64(ref mut v) = arr.data {
593 for iy in 0..y {
594 for ix in 0..x {
595 v[iy * x + ix] = fill(ix, iy);
596 }
597 }
598 }
599 arr
600 }
601
602 #[test]
603 fn test_single_roi_full_image() {
604 let arr = make_2d_array(4, 4, |_x, _y| 10.0);
605 let rois = vec![ROIStatROI {
606 enabled: true,
607 offset: [0, 0],
608 size: [4, 4],
609 bgd_width: 0,
610 }];
611
612 let mut proc = ROIStatProcessor::new(rois, 0);
613 let pool = NDArrayPool::new(1_000_000);
614 proc.process_array(&arr, &pool);
615
616 let r = &proc.results()[0];
617 assert!((r.min - 10.0).abs() < 1e-10);
618 assert!((r.max - 10.0).abs() < 1e-10);
619 assert!((r.mean - 10.0).abs() < 1e-10);
620 assert!((r.total - 160.0).abs() < 1e-10);
621 }
622
623 #[test]
624 fn test_single_roi_subregion() {
625 let arr = make_2d_array(8, 8, |x, y| (x + y * 8) as f64);
627
628 let rois = vec![ROIStatROI {
629 enabled: true,
630 offset: [2, 2],
631 size: [3, 3],
632 bgd_width: 0,
633 }];
634
635 let mut proc = ROIStatProcessor::new(rois, 0);
636 let pool = NDArrayPool::new(1_000_000);
637 proc.process_array(&arr, &pool);
638
639 let r = &proc.results()[0];
640 assert!((r.min - 18.0).abs() < 1e-10);
642 assert!((r.max - 36.0).abs() < 1e-10);
643 let expected_total = 18.0 + 19.0 + 20.0 + 26.0 + 27.0 + 28.0 + 34.0 + 35.0 + 36.0;
644 assert!((r.total - expected_total).abs() < 1e-10);
645 assert!((r.mean - expected_total / 9.0).abs() < 1e-10);
646 }
647
648 #[test]
649 fn test_multiple_rois() {
650 let arr = make_2d_array(8, 8, |x, _y| x as f64);
651
652 let rois = vec![
653 ROIStatROI {
654 enabled: true,
655 offset: [0, 0],
656 size: [4, 4],
657 bgd_width: 0,
658 },
659 ROIStatROI {
660 enabled: true,
661 offset: [4, 0],
662 size: [4, 4],
663 bgd_width: 0,
664 },
665 ];
666
667 let mut proc = ROIStatProcessor::new(rois, 0);
668 let pool = NDArrayPool::new(1_000_000);
669 proc.process_array(&arr, &pool);
670
671 let r0 = &proc.results()[0];
672 assert!((r0.min - 0.0).abs() < 1e-10);
673 assert!((r0.max - 3.0).abs() < 1e-10);
674
675 let r1 = &proc.results()[1];
676 assert!((r1.min - 4.0).abs() < 1e-10);
677 assert!((r1.max - 7.0).abs() < 1e-10);
678 }
679
680 #[test]
681 fn test_bgd_width() {
682 let arr = make_2d_array(6, 6, |x, y| {
684 if x >= 2 && x < 4 && y >= 2 && y < 4 {
685 100.0
686 } else {
687 10.0
688 }
689 });
690
691 let rois = vec![ROIStatROI {
692 enabled: true,
693 offset: [1, 1],
694 size: [4, 4],
695 bgd_width: 1,
696 }];
697
698 let mut proc = ROIStatProcessor::new(rois, 0);
699 let pool = NDArrayPool::new(1_000_000);
700 proc.process_array(&arr, &pool);
701
702 let r = &proc.results()[0];
703 assert!(
708 r.net < r.total,
709 "net should be less than total with bgd subtraction"
710 );
711 }
712
713 #[test]
714 fn test_empty_roi() {
715 let arr = make_2d_array(4, 4, |_, _| 10.0);
716 let rois = vec![ROIStatROI {
717 enabled: true,
718 offset: [0, 0],
719 size: [0, 0],
720 bgd_width: 0,
721 }];
722
723 let mut proc = ROIStatProcessor::new(rois, 0);
724 let pool = NDArrayPool::new(1_000_000);
725 proc.process_array(&arr, &pool);
726
727 let r = &proc.results()[0];
728 assert!((r.total - 0.0).abs() < 1e-10);
729 }
730
731 #[test]
732 fn test_disabled_roi() {
733 let arr = make_2d_array(4, 4, |_, _| 10.0);
734 let rois = vec![ROIStatROI {
735 enabled: false,
736 offset: [0, 0],
737 size: [4, 4],
738 bgd_width: 0,
739 }];
740
741 let mut proc = ROIStatProcessor::new(rois, 0);
742 let pool = NDArrayPool::new(1_000_000);
743 proc.process_array(&arr, &pool);
744
745 let r = &proc.results()[0];
746 assert!(
747 (r.total - 0.0).abs() < 1e-10,
748 "disabled ROI should have zero stats"
749 );
750 }
751
752 #[test]
753 fn test_roi_out_of_bounds() {
754 let arr = make_2d_array(4, 4, |_, _| 10.0);
755 let rois = vec![ROIStatROI {
756 enabled: true,
757 offset: [10, 10],
758 size: [4, 4],
759 bgd_width: 0,
760 }];
761
762 let mut proc = ROIStatProcessor::new(rois, 0);
763 let pool = NDArrayPool::new(1_000_000);
764 proc.process_array(&arr, &pool);
765
766 let r = &proc.results()[0];
767 assert!(
768 (r.total - 0.0).abs() < 1e-10,
769 "out-of-bounds ROI should produce zero stats"
770 );
771 }
772
773 #[test]
774 fn test_roi_partially_out_of_bounds() {
775 let arr = make_2d_array(4, 4, |_, _| 5.0);
776 let rois = vec![ROIStatROI {
777 enabled: true,
778 offset: [2, 2],
779 size: [10, 10], bgd_width: 0,
781 }];
782
783 let mut proc = ROIStatProcessor::new(rois, 0);
784 let pool = NDArrayPool::new(1_000_000);
785 proc.process_array(&arr, &pool);
786
787 let r = &proc.results()[0];
788 assert!((r.total - 20.0).abs() < 1e-10);
790 assert!((r.mean - 5.0).abs() < 1e-10);
791 }
792
793 #[test]
794 fn test_time_series() {
795 let rois = vec![ROIStatROI {
796 enabled: true,
797 offset: [0, 0],
798 size: [4, 4],
799 bgd_width: 0,
800 }];
801
802 let mut proc = ROIStatProcessor::new(rois, 100);
803 let pool = NDArrayPool::new(1_000_000);
804 proc.set_ts_mode(TSMode::Acquiring);
805
806 for i in 0..5 {
807 let arr = make_2d_array(4, 4, |_, _| (i + 1) as f64);
808 proc.process_array(&arr, &pool);
809 }
810
811 let ts = proc.ts_buffer(0, 2);
813 assert_eq!(ts.len(), 5);
814 assert!((ts[0] - 1.0).abs() < 1e-10);
815 assert!((ts[4] - 5.0).abs() < 1e-10);
816 }
817
818 #[test]
819 fn test_u8_data() {
820 let mut arr = NDArray::new(
821 vec![NDDimension::new(4), NDDimension::new(4)],
822 NDDataType::UInt8,
823 );
824 if let NDDataBuffer::U8(ref mut v) = arr.data {
825 for (i, val) in v.iter_mut().enumerate() {
826 *val = (i + 1) as u8;
827 }
828 }
829
830 let rois = vec![ROIStatROI {
831 enabled: true,
832 offset: [0, 0],
833 size: [4, 4],
834 bgd_width: 0,
835 }];
836
837 let mut proc = ROIStatProcessor::new(rois, 0);
838 let pool = NDArrayPool::new(1_000_000);
839 proc.process_array(&arr, &pool);
840
841 let r = &proc.results()[0];
842 assert!((r.min - 1.0).abs() < 1e-10);
843 assert!((r.max - 16.0).abs() < 1e-10);
844 }
845
846 #[test]
847 fn test_ts_channel_names() {
848 let names = roi_stat_ts_channel_names(2);
849 assert_eq!(names.len(), 10); assert_eq!(names[0], "TS1:MinValue");
851 assert_eq!(names[1], "TS1:MaxValue");
852 assert_eq!(names[4], "TS1:Net");
853 assert_eq!(names[5], "TS2:MinValue");
854 assert_eq!(names[9], "TS2:Net");
855 }
856
857 #[test]
858 fn test_ts_sender_integration() {
859 let (tx, mut rx) = tokio::sync::mpsc::channel::<TimeSeriesData>(16);
860
861 let rois = vec![
862 ROIStatROI {
863 enabled: true,
864 offset: [0, 0],
865 size: [4, 4],
866 bgd_width: 0,
867 },
868 ROIStatROI {
869 enabled: true,
870 offset: [0, 0],
871 size: [2, 2],
872 bgd_width: 0,
873 },
874 ];
875
876 let mut proc = ROIStatProcessor::new(rois, 0);
877 proc.set_ts_sender(tx);
878
879 let pool = NDArrayPool::new(1_000_000);
880 let arr = make_2d_array(4, 4, |_, _| 7.0);
881 proc.process_array(&arr, &pool);
882
883 let data = rx.try_recv().unwrap();
884 assert_eq!(data.values.len(), 10);
886 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); }
894}