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 pub ts_total: usize,
113 pub ts_net: usize,
114 pub ts_mean_value: usize,
115 pub ts_min_value: usize,
116 pub ts_max_value: usize,
117 pub ts_timestamp: usize,
118}
119
120pub struct ROIStatProcessor {
122 rois: Vec<ROIStatROI>,
123 results: Vec<ROIStatResult>,
124 ts_mode: TSMode,
126 ts_buffers: Vec<Vec<Vec<f64>>>,
127 ts_num_points: usize,
128 ts_current: usize,
129 ts_sender: Option<TimeSeriesSender>,
131 params: ROIStatParams,
133 params_out: Arc<Mutex<ROIStatParams>>,
135}
136
137impl ROIStatProcessor {
138 pub fn new(rois: Vec<ROIStatROI>, ts_num_points: usize) -> Self {
140 let n = rois.len();
141 let results = vec![ROIStatResult::default(); n];
142 let ts_buffers = vec![vec![Vec::new(); NUM_STATS]; n];
143 Self {
144 rois,
145 results,
146 ts_mode: TSMode::Idle,
147 ts_buffers,
148 ts_num_points,
149 ts_current: 0,
150 ts_sender: None,
151 params: ROIStatParams::default(),
152 params_out: Arc::new(Mutex::new(ROIStatParams::default())),
153 }
154 }
155
156 pub fn params_handle(&self) -> Arc<Mutex<ROIStatParams>> {
158 self.params_out.clone()
159 }
160
161 pub fn results(&self) -> &[ROIStatResult] {
163 &self.results
164 }
165
166 pub fn rois(&self) -> &[ROIStatROI] {
168 &self.rois
169 }
170
171 pub fn rois_mut(&mut self) -> &mut Vec<ROIStatROI> {
173 &mut self.rois
174 }
175
176 pub fn set_ts_mode(&mut self, mode: TSMode) {
178 if mode == TSMode::Acquiring && self.ts_mode != TSMode::Acquiring {
179 for roi_bufs in &mut self.ts_buffers {
181 for stat_buf in roi_bufs.iter_mut() {
182 stat_buf.clear();
183 }
184 }
185 self.ts_current = 0;
186 }
187 self.ts_mode = mode;
188 }
189
190 pub fn ts_buffer(&self, roi_index: usize, stat_index: usize) -> &[f64] {
193 if roi_index < self.ts_buffers.len() && stat_index < NUM_STATS {
194 &self.ts_buffers[roi_index][stat_index]
195 } else {
196 &[]
197 }
198 }
199
200 pub fn set_ts_sender(&mut self, sender: TimeSeriesSender) {
202 self.ts_sender = Some(sender);
203 }
204
205 fn clamp_roi_geometry(
216 roi: &ROIStatROI,
217 ndims: usize,
218 dims: [usize; 2],
219 ) -> Option<([usize; 2], [usize; 2])> {
220 let mut offset = roi.offset;
221 let mut size = roi.size;
222 for d in 0..ndims.min(2) {
223 let dim = dims[d];
224 if dim == 0 {
225 return None;
226 }
227 offset[d] = offset[d].min(dim - 1);
228 size[d] = size[d].max(1).min(dim - offset[d]);
229 }
230 Some((offset, size))
231 }
232
233 pub fn compute_roi_stats(
241 data: &NDDataBuffer,
242 ndims: usize,
243 array_size_x: usize,
244 offset: [usize; 2],
245 size: [usize; 2],
246 bgd_width: usize,
247 ) -> ROIStatResult {
248 let offset_x = offset[0];
249 let size_x = size[0];
250
251 let mut min = f64::MAX;
252 let mut max = f64::MIN;
253 let mut total = 0.0f64;
254 let mut bgd = 0.0f64;
255 let mut n_bgd = 0usize;
256 let n_elements;
257
258 if ndims == 1 {
259 if size_x == 0 {
260 return ROIStatResult::default();
261 }
262 n_elements = size_x;
263 for x in offset_x..offset_x + size_x {
264 let v = data.get_as_f64(x).unwrap_or(0.0);
265 min = min.min(v);
266 max = max.max(v);
267 total += v;
268 }
269 if bgd_width > 0 {
270 let bw_x = bgd_width.min(size_x);
271 for x in offset_x..offset_x + bw_x {
272 n_bgd += 1;
273 bgd += data.get_as_f64(x).unwrap_or(0.0);
274 }
275 for x in (offset_x + size_x - bw_x)..(offset_x + size_x) {
276 n_bgd += 1;
277 bgd += data.get_as_f64(x).unwrap_or(0.0);
278 }
279 }
280 } else if ndims == 2 {
281 let offset_y = offset[1];
282 let size_y = size[1];
283 if size_x == 0 || size_y == 0 {
284 return ROIStatResult::default();
285 }
286 n_elements = size_x * size_y;
287 for y in offset_y..offset_y + size_y {
288 let row = y * array_size_x;
289 for x in offset_x..offset_x + size_x {
290 let v = data.get_as_f64(row + x).unwrap_or(0.0);
291 min = min.min(v);
292 max = max.max(v);
293 total += v;
294 }
295 }
296 if bgd_width > 0 {
297 let bw_x = bgd_width.min(size_x);
298 let bw_y = bgd_width.min(size_y);
299 for y in offset_y..offset_y + bw_y {
301 let row = y * array_size_x;
302 for x in offset_x..offset_x + size_x {
303 n_bgd += 1;
304 bgd += data.get_as_f64(row + x).unwrap_or(0.0);
305 }
306 }
307 for y in (offset_y + size_y - bw_y)..(offset_y + size_y) {
308 let row = y * array_size_x;
309 for x in offset_x..offset_x + size_x {
310 n_bgd += 1;
311 bgd += data.get_as_f64(row + x).unwrap_or(0.0);
312 }
313 }
314 for y in (offset_y + bw_y)..(offset_y + size_y - bw_y) {
316 let row = y * array_size_x;
317 for x in offset_x..offset_x + bw_x {
318 n_bgd += 1;
319 bgd += data.get_as_f64(row + x).unwrap_or(0.0);
320 }
321 for x in (offset_x + size_x - bw_x)..(offset_x + size_x) {
322 n_bgd += 1;
323 bgd += data.get_as_f64(row + x).unwrap_or(0.0);
324 }
325 }
326 }
327 } else {
328 return ROIStatResult::default();
329 }
330
331 if n_elements == 0 {
332 return ROIStatResult::default();
333 }
334
335 let bgd_scaled = if n_bgd > 0 {
340 bgd / n_bgd as f64 * n_elements as f64
341 } else {
342 0.0
343 };
344 ROIStatResult {
345 min,
346 max,
347 mean: total / n_elements as f64,
348 total,
349 net: total - bgd_scaled,
350 }
351 }
352}
353
354impl NDPluginProcess for ROIStatProcessor {
355 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
356 let ndims = array.dims.len();
361 let dims = [
362 array.dims.first().map(|d| d.size).unwrap_or(0),
363 array.dims.get(1).map(|d| d.size).unwrap_or(0),
364 ];
365 let array_size_x = dims[0];
366 let supported = ndims == 1 || ndims == 2;
367
368 self.results
370 .resize(self.rois.len(), ROIStatResult::default());
371
372 let clamped: Vec<Option<([usize; 2], [usize; 2])>> = self
376 .rois
377 .iter()
378 .map(|roi| {
379 if roi.enabled && supported {
380 Self::clamp_roi_geometry(roi, ndims, dims)
381 } else {
382 None
383 }
384 })
385 .collect();
386
387 #[cfg(feature = "parallel")]
388 {
389 let total_elements: usize = clamped
390 .iter()
391 .flatten()
392 .map(|(_, size)| {
393 if ndims == 1 {
394 size[0]
395 } else {
396 size[0] * size[1]
397 }
398 })
399 .sum();
400
401 if par_util::should_parallelize(total_elements) {
402 let data = &array.data;
403 let rois = &self.rois;
404 let new_results: Vec<ROIStatResult> = par_util::thread_pool().install(|| {
405 rois.par_iter()
406 .zip(clamped.par_iter())
407 .map(|(roi, clamp)| match clamp {
408 Some((offset, size)) => Self::compute_roi_stats(
409 data,
410 ndims,
411 array_size_x,
412 *offset,
413 *size,
414 roi.bgd_width,
415 ),
416 None => ROIStatResult::default(),
417 })
418 .collect()
419 });
420 self.results = new_results;
421 } else {
422 for (i, (roi, clamp)) in self.rois.iter().zip(clamped.iter()).enumerate() {
423 self.results[i] = match clamp {
424 Some((offset, size)) => Self::compute_roi_stats(
425 &array.data,
426 ndims,
427 array_size_x,
428 *offset,
429 *size,
430 roi.bgd_width,
431 ),
432 None => ROIStatResult::default(),
433 };
434 }
435 }
436 }
437
438 #[cfg(not(feature = "parallel"))]
439 for (i, (roi, clamp)) in self.rois.iter().zip(clamped.iter()).enumerate() {
440 self.results[i] = match clamp {
441 Some((offset, size)) => Self::compute_roi_stats(
442 &array.data,
443 ndims,
444 array_size_x,
445 *offset,
446 *size,
447 roi.bgd_width,
448 ),
449 None => ROIStatResult::default(),
450 };
451 }
452
453 if self.ts_mode == TSMode::Acquiring {
455 if self.ts_num_points > 0 && self.ts_current >= self.ts_num_points {
456 self.ts_mode = TSMode::Idle;
458 } else {
459 while self.ts_buffers.len() < self.rois.len() {
461 self.ts_buffers.push(vec![Vec::new(); NUM_STATS]);
462 }
463
464 for (i, result) in self.results.iter().enumerate() {
465 if i >= self.ts_buffers.len() {
466 break;
467 }
468 let stats = [
469 result.min,
470 result.max,
471 result.mean,
472 result.total,
473 result.net,
474 ];
475 for (s, &val) in stats.iter().enumerate() {
476 let buf = &mut self.ts_buffers[i][s];
477 buf.push(val);
478 }
479 }
480 self.ts_current += 1;
481 }
482 }
483
484 if let Some(ref sender) = self.ts_sender {
486 let mut values = Vec::with_capacity(self.results.len() * NUM_STATS);
487 for result in &self.results {
488 values.push(result.min);
489 values.push(result.max);
490 values.push(result.mean);
491 values.push(result.total);
492 values.push(result.net);
493 }
494 let _ = sender.try_send(TimeSeriesData { values });
495 }
496
497 let p = &self.params;
499 let mut updates = Vec::new();
500 for (i, roi) in self.rois.iter().enumerate() {
501 if !roi.enabled {
502 continue;
503 }
504 let result = &self.results[i];
505 let addr = i as i32;
506 updates.push(ParamUpdate::float64_addr(p.min_value, addr, result.min));
507 updates.push(ParamUpdate::float64_addr(p.max_value, addr, result.max));
508 updates.push(ParamUpdate::float64_addr(p.mean_value, addr, result.mean));
509 updates.push(ParamUpdate::float64_addr(p.total, addr, result.total));
510 updates.push(ParamUpdate::float64_addr(p.net, addr, result.net));
511
512 updates.push(ParamUpdate::int32_addr(
517 p.dim0_max_size,
518 addr,
519 if ndims >= 1 { dims[0] as i32 } else { 0 },
520 ));
521 updates.push(ParamUpdate::int32_addr(
522 p.dim1_max_size,
523 addr,
524 if ndims >= 2 { dims[1] as i32 } else { 0 },
525 ));
526 if let Some((offset, size)) = clamped[i] {
527 if ndims >= 1 {
528 updates.push(ParamUpdate::int32_addr(p.dim0_min, addr, offset[0] as i32));
529 updates.push(ParamUpdate::int32_addr(p.dim0_size, addr, size[0] as i32));
530 }
531 if ndims >= 2 {
532 updates.push(ParamUpdate::int32_addr(p.dim1_min, addr, offset[1] as i32));
533 updates.push(ParamUpdate::int32_addr(p.dim1_size, addr, size[1] as i32));
534 }
535 }
536 }
537 updates.push(ParamUpdate::int32(
538 p.ts_current_point,
539 self.ts_current as i32,
540 ));
541 updates.push(ParamUpdate::int32(
542 p.ts_acquiring,
543 if self.ts_mode == TSMode::Acquiring {
544 1
545 } else {
546 0
547 },
548 ));
549
550 for (i, roi) in self.rois.iter().enumerate() {
552 if !roi.enabled || i >= self.ts_buffers.len() {
553 continue;
554 }
555 let addr = i as i32;
556 let bufs = &self.ts_buffers[i];
557 if !bufs.is_empty() {
559 updates.push(ParamUpdate::float64_array_addr(
560 p.ts_min_value,
561 addr,
562 bufs[0].clone(),
563 ));
564 }
565 if bufs.len() > 1 {
566 updates.push(ParamUpdate::float64_array_addr(
567 p.ts_max_value,
568 addr,
569 bufs[1].clone(),
570 ));
571 }
572 if bufs.len() > 2 {
573 updates.push(ParamUpdate::float64_array_addr(
574 p.ts_mean_value,
575 addr,
576 bufs[2].clone(),
577 ));
578 }
579 if bufs.len() > 3 {
580 updates.push(ParamUpdate::float64_array_addr(
581 p.ts_total,
582 addr,
583 bufs[3].clone(),
584 ));
585 }
586 if bufs.len() > 4 {
587 updates.push(ParamUpdate::float64_array_addr(
588 p.ts_net,
589 addr,
590 bufs[4].clone(),
591 ));
592 }
593 }
594
595 ProcessResult::sink(updates)
596 }
597
598 fn plugin_type(&self) -> &str {
599 "NDPluginROIStat"
600 }
601
602 fn register_params(
603 &mut self,
604 base: &mut PortDriverBase,
605 ) -> Result<(), asyn_rs::error::AsynError> {
606 self.params.reset_all = base.create_param("ROISTAT_RESETALL", ParamType::Int32)?;
608 self.params.ts_control = base.create_param("ROISTAT_TS_CONTROL", ParamType::Int32)?;
609 self.params.ts_num_points = base.create_param("ROISTAT_TS_NUM_POINTS", ParamType::Int32)?;
610 base.set_int32_param(self.params.ts_num_points, 0, self.ts_num_points as i32)?;
611 self.params.ts_current_point =
612 base.create_param("ROISTAT_TS_CURRENT_POINT", ParamType::Int32)?;
613 self.params.ts_acquiring = base.create_param("ROISTAT_TS_ACQUIRING", ParamType::Int32)?;
614
615 self.params.use_ = base.create_param("ROISTAT_USE", ParamType::Int32)?;
617 self.params.name = base.create_param("ROISTAT_NAME", ParamType::Octet)?;
618 self.params.reset = base.create_param("ROISTAT_RESET", ParamType::Int32)?;
619 self.params.bgd_width = base.create_param("ROISTAT_BGD_WIDTH", ParamType::Int32)?;
620 self.params.dim0_min = base.create_param("ROISTAT_DIM0_MIN", ParamType::Int32)?;
621 self.params.dim1_min = base.create_param("ROISTAT_DIM1_MIN", ParamType::Int32)?;
622 self.params.dim0_size = base.create_param("ROISTAT_DIM0_SIZE", ParamType::Int32)?;
623 self.params.dim1_size = base.create_param("ROISTAT_DIM1_SIZE", ParamType::Int32)?;
624 self.params.dim0_max_size = base.create_param("ROISTAT_DIM0_MAX_SIZE", ParamType::Int32)?;
625 self.params.dim1_max_size = base.create_param("ROISTAT_DIM1_MAX_SIZE", ParamType::Int32)?;
626 self.params.min_value = base.create_param("ROISTAT_MIN_VALUE", ParamType::Float64)?;
627 self.params.max_value = base.create_param("ROISTAT_MAX_VALUE", ParamType::Float64)?;
628 self.params.mean_value = base.create_param("ROISTAT_MEAN_VALUE", ParamType::Float64)?;
629 self.params.total = base.create_param("ROISTAT_TOTAL", ParamType::Float64)?;
630 self.params.net = base.create_param("ROISTAT_NET", ParamType::Float64)?;
631
632 self.params.ts_total = base.create_param("ROISTAT_TS_TOTAL", ParamType::Float64Array)?;
634 self.params.ts_net = base.create_param("ROISTAT_TS_NET", ParamType::Float64Array)?;
635 self.params.ts_mean_value =
636 base.create_param("ROISTAT_TS_MEAN_VALUE", ParamType::Float64Array)?;
637 self.params.ts_min_value =
638 base.create_param("ROISTAT_TS_MIN_VALUE", ParamType::Float64Array)?;
639 self.params.ts_max_value =
640 base.create_param("ROISTAT_TS_MAX_VALUE", ParamType::Float64Array)?;
641 self.params.ts_timestamp =
642 base.create_param("ROISTAT_TS_TIMESTAMP", ParamType::Float64Array)?;
643
644 for (i, roi) in self.rois.iter().enumerate() {
646 let addr = i as i32;
647 base.set_int32_param(self.params.use_, addr, roi.enabled as i32)?;
648 base.set_int32_param(self.params.bgd_width, addr, roi.bgd_width as i32)?;
649 base.set_int32_param(self.params.dim0_min, addr, roi.offset[0] as i32)?;
650 base.set_int32_param(self.params.dim1_min, addr, roi.offset[1] as i32)?;
651 base.set_int32_param(self.params.dim0_size, addr, roi.size[0] as i32)?;
652 base.set_int32_param(self.params.dim1_size, addr, roi.size[1] as i32)?;
653 }
654
655 *self.params_out.lock() = self.params;
657
658 Ok(())
659 }
660
661 fn on_param_change(
662 &mut self,
663 reason: usize,
664 snapshot: &PluginParamSnapshot,
665 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
666 let addr = snapshot.addr as usize;
667 let p = &self.params;
668
669 if reason == p.use_ && addr < self.rois.len() {
670 self.rois[addr].enabled = snapshot.value.as_i32() != 0;
671 } else if reason == p.dim0_min && addr < self.rois.len() {
672 self.rois[addr].offset[0] = snapshot.value.as_i32().max(0) as usize;
673 } else if reason == p.dim1_min && addr < self.rois.len() {
674 self.rois[addr].offset[1] = snapshot.value.as_i32().max(0) as usize;
675 } else if reason == p.dim0_size && addr < self.rois.len() {
676 self.rois[addr].size[0] = snapshot.value.as_i32().max(0) as usize;
677 } else if reason == p.dim1_size && addr < self.rois.len() {
678 self.rois[addr].size[1] = snapshot.value.as_i32().max(0) as usize;
679 } else if reason == p.bgd_width && addr < self.rois.len() {
680 self.rois[addr].bgd_width = snapshot.value.as_i32().max(0) as usize;
681 } else if reason == p.reset && addr < self.rois.len() {
682 self.results[addr] = ROIStatResult::default();
683 } else if reason == p.reset_all {
684 for r in &mut self.results {
685 *r = ROIStatResult::default();
686 }
687 } else if reason == p.ts_control {
688 match snapshot.value.as_i32() {
690 0 => {
691 for roi_bufs in &mut self.ts_buffers {
693 for stat_buf in roi_bufs.iter_mut() {
694 stat_buf.clear();
695 }
696 }
697 self.ts_current = 0;
698 self.ts_mode = TSMode::Acquiring;
699 }
700 1 => {
701 self.ts_mode = TSMode::Acquiring;
703 }
704 2 => {
705 self.ts_mode = TSMode::Idle;
707 }
708 3 => {
709 }
711 4 => {
712 for roi_bufs in &mut self.ts_buffers {
714 for stat_buf in roi_bufs.iter_mut() {
715 stat_buf.clear();
716 }
717 }
718 self.ts_current = 0;
719 }
720 _ => {}
721 }
722 } else if reason == p.ts_num_points {
723 self.ts_num_points = snapshot.value.as_i32().max(0) as usize;
724 }
725 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
726 }
727}
728
729pub fn create_roi_stat_runtime(
732 port_name: &str,
733 pool: Arc<NDArrayPool>,
734 queue_size: usize,
735 ndarray_port: &str,
736 wiring: Arc<WiringRegistry>,
737 num_rois: usize,
738 ts_registry: &crate::time_series::TsReceiverRegistry,
739) -> (
740 PluginRuntimeHandle,
741 ROIStatParams,
742 std::thread::JoinHandle<()>,
743) {
744 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
745
746 let rois: Vec<ROIStatROI> = (0..num_rois).map(|_| ROIStatROI::default()).collect();
747 let mut processor = ROIStatProcessor::new(rois, 2048);
748 processor.set_ts_sender(ts_tx);
749 let params_handle = processor.params_handle();
750
751 let (handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime_multi_addr(
752 port_name,
753 processor,
754 pool,
755 queue_size,
756 ndarray_port,
757 wiring,
758 num_rois,
759 );
760
761 let roi_stat_params = *params_handle.lock();
762
763 let channel_names = roi_stat_ts_channel_names(num_rois);
765 ts_registry.store(port_name, ts_rx, channel_names);
766
767 (handle, roi_stat_params, data_jh)
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773 use ad_core_rs::ndarray::{NDDataType, NDDimension};
774
775 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
776 let mut arr = NDArray::new(
777 vec![NDDimension::new(x), NDDimension::new(y)],
778 NDDataType::Float64,
779 );
780 if let NDDataBuffer::F64(ref mut v) = arr.data {
781 for iy in 0..y {
782 for ix in 0..x {
783 v[iy * x + ix] = fill(ix, iy);
784 }
785 }
786 }
787 arr
788 }
789
790 #[test]
791 fn test_single_roi_full_image() {
792 let arr = make_2d_array(4, 4, |_x, _y| 10.0);
793 let rois = vec![ROIStatROI {
794 enabled: true,
795 offset: [0, 0],
796 size: [4, 4],
797 bgd_width: 0,
798 }];
799
800 let mut proc = ROIStatProcessor::new(rois, 0);
801 let pool = NDArrayPool::new(1_000_000);
802 proc.process_array(&arr, &pool);
803
804 let r = &proc.results()[0];
805 assert!((r.min - 10.0).abs() < 1e-10);
806 assert!((r.max - 10.0).abs() < 1e-10);
807 assert!((r.mean - 10.0).abs() < 1e-10);
808 assert!((r.total - 160.0).abs() < 1e-10);
809 }
810
811 #[test]
812 fn test_single_roi_subregion() {
813 let arr = make_2d_array(8, 8, |x, y| (x + y * 8) as f64);
815
816 let rois = vec![ROIStatROI {
817 enabled: true,
818 offset: [2, 2],
819 size: [3, 3],
820 bgd_width: 0,
821 }];
822
823 let mut proc = ROIStatProcessor::new(rois, 0);
824 let pool = NDArrayPool::new(1_000_000);
825 proc.process_array(&arr, &pool);
826
827 let r = &proc.results()[0];
828 assert!((r.min - 18.0).abs() < 1e-10);
830 assert!((r.max - 36.0).abs() < 1e-10);
831 let expected_total = 18.0 + 19.0 + 20.0 + 26.0 + 27.0 + 28.0 + 34.0 + 35.0 + 36.0;
832 assert!((r.total - expected_total).abs() < 1e-10);
833 assert!((r.mean - expected_total / 9.0).abs() < 1e-10);
834 }
835
836 #[test]
837 fn test_multiple_rois() {
838 let arr = make_2d_array(8, 8, |x, _y| x as f64);
839
840 let rois = vec![
841 ROIStatROI {
842 enabled: true,
843 offset: [0, 0],
844 size: [4, 4],
845 bgd_width: 0,
846 },
847 ROIStatROI {
848 enabled: true,
849 offset: [4, 0],
850 size: [4, 4],
851 bgd_width: 0,
852 },
853 ];
854
855 let mut proc = ROIStatProcessor::new(rois, 0);
856 let pool = NDArrayPool::new(1_000_000);
857 proc.process_array(&arr, &pool);
858
859 let r0 = &proc.results()[0];
860 assert!((r0.min - 0.0).abs() < 1e-10);
861 assert!((r0.max - 3.0).abs() < 1e-10);
862
863 let r1 = &proc.results()[1];
864 assert!((r1.min - 4.0).abs() < 1e-10);
865 assert!((r1.max - 7.0).abs() < 1e-10);
866 }
867
868 #[test]
869 fn test_bgd_width() {
870 let arr = make_2d_array(6, 6, |x, y| {
872 if x >= 2 && x < 4 && y >= 2 && y < 4 {
873 100.0
874 } else {
875 10.0
876 }
877 });
878
879 let rois = vec![ROIStatROI {
880 enabled: true,
881 offset: [1, 1],
882 size: [4, 4],
883 bgd_width: 1,
884 }];
885
886 let mut proc = ROIStatProcessor::new(rois, 0);
887 let pool = NDArrayPool::new(1_000_000);
888 proc.process_array(&arr, &pool);
889
890 let r = &proc.results()[0];
891 assert!(
896 r.net < r.total,
897 "net should be less than total with bgd subtraction"
898 );
899 }
900
901 #[test]
902 fn test_empty_roi() {
903 let arr = make_2d_array(4, 4, |_, _| 10.0);
904 let rois = vec![ROIStatROI {
905 enabled: true,
906 offset: [0, 0],
907 size: [0, 0],
908 bgd_width: 0,
909 }];
910
911 let mut proc = ROIStatProcessor::new(rois, 0);
912 let pool = NDArrayPool::new(1_000_000);
913 proc.process_array(&arr, &pool);
914
915 let r = &proc.results()[0];
918 assert!((r.total - 10.0).abs() < 1e-10);
919 assert!((r.mean - 10.0).abs() < 1e-10);
920 }
921
922 #[test]
923 fn test_disabled_roi() {
924 let arr = make_2d_array(4, 4, |_, _| 10.0);
925 let rois = vec![ROIStatROI {
926 enabled: false,
927 offset: [0, 0],
928 size: [4, 4],
929 bgd_width: 0,
930 }];
931
932 let mut proc = ROIStatProcessor::new(rois, 0);
933 let pool = NDArrayPool::new(1_000_000);
934 proc.process_array(&arr, &pool);
935
936 let r = &proc.results()[0];
937 assert!(
938 (r.total - 0.0).abs() < 1e-10,
939 "disabled ROI should have zero stats"
940 );
941 }
942
943 #[test]
944 fn test_roi_out_of_bounds() {
945 let arr = make_2d_array(4, 4, |_, _| 10.0);
946 let rois = vec![ROIStatROI {
947 enabled: true,
948 offset: [10, 10],
949 size: [4, 4],
950 bgd_width: 0,
951 }];
952
953 let mut proc = ROIStatProcessor::new(rois, 0);
954 let pool = NDArrayPool::new(1_000_000);
955 proc.process_array(&arr, &pool);
956
957 let r = &proc.results()[0];
960 assert!(
961 (r.total - 10.0).abs() < 1e-10,
962 "out-of-bounds ROI clamps to one edge pixel, not zero"
963 );
964 assert!((r.mean - 10.0).abs() < 1e-10);
965 }
966
967 #[test]
968 fn test_roi_partially_out_of_bounds() {
969 let arr = make_2d_array(4, 4, |_, _| 5.0);
970 let rois = vec![ROIStatROI {
971 enabled: true,
972 offset: [2, 2],
973 size: [10, 10], bgd_width: 0,
975 }];
976
977 let mut proc = ROIStatProcessor::new(rois, 0);
978 let pool = NDArrayPool::new(1_000_000);
979 proc.process_array(&arr, &pool);
980
981 let r = &proc.results()[0];
982 assert!((r.total - 20.0).abs() < 1e-10);
984 assert!((r.mean - 5.0).abs() < 1e-10);
985 }
986
987 #[test]
988 fn test_time_series() {
989 let rois = vec![ROIStatROI {
990 enabled: true,
991 offset: [0, 0],
992 size: [4, 4],
993 bgd_width: 0,
994 }];
995
996 let mut proc = ROIStatProcessor::new(rois, 100);
997 let pool = NDArrayPool::new(1_000_000);
998 proc.set_ts_mode(TSMode::Acquiring);
999
1000 for i in 0..5 {
1001 let arr = make_2d_array(4, 4, |_, _| (i + 1) as f64);
1002 proc.process_array(&arr, &pool);
1003 }
1004
1005 let ts = proc.ts_buffer(0, 2);
1007 assert_eq!(ts.len(), 5);
1008 assert!((ts[0] - 1.0).abs() < 1e-10);
1009 assert!((ts[4] - 5.0).abs() < 1e-10);
1010 }
1011
1012 #[test]
1013 fn test_u8_data() {
1014 let mut arr = NDArray::new(
1015 vec![NDDimension::new(4), NDDimension::new(4)],
1016 NDDataType::UInt8,
1017 );
1018 if let NDDataBuffer::U8(ref mut v) = arr.data {
1019 for (i, val) in v.iter_mut().enumerate() {
1020 *val = (i + 1) as u8;
1021 }
1022 }
1023
1024 let rois = vec![ROIStatROI {
1025 enabled: true,
1026 offset: [0, 0],
1027 size: [4, 4],
1028 bgd_width: 0,
1029 }];
1030
1031 let mut proc = ROIStatProcessor::new(rois, 0);
1032 let pool = NDArrayPool::new(1_000_000);
1033 proc.process_array(&arr, &pool);
1034
1035 let r = &proc.results()[0];
1036 assert!((r.min - 1.0).abs() < 1e-10);
1037 assert!((r.max - 16.0).abs() < 1e-10);
1038 }
1039
1040 #[test]
1041 fn test_ts_channel_names() {
1042 let names = roi_stat_ts_channel_names(2);
1043 assert_eq!(names.len(), 10); assert_eq!(names[0], "TS1:MinValue");
1045 assert_eq!(names[1], "TS1:MaxValue");
1046 assert_eq!(names[4], "TS1:Net");
1047 assert_eq!(names[5], "TS2:MinValue");
1048 assert_eq!(names[9], "TS2:Net");
1049 }
1050
1051 #[test]
1052 fn test_ts_sender_integration() {
1053 let (tx, mut rx) = tokio::sync::mpsc::channel::<TimeSeriesData>(16);
1054
1055 let rois = vec![
1056 ROIStatROI {
1057 enabled: true,
1058 offset: [0, 0],
1059 size: [4, 4],
1060 bgd_width: 0,
1061 },
1062 ROIStatROI {
1063 enabled: true,
1064 offset: [0, 0],
1065 size: [2, 2],
1066 bgd_width: 0,
1067 },
1068 ];
1069
1070 let mut proc = ROIStatProcessor::new(rois, 0);
1071 proc.set_ts_sender(tx);
1072
1073 let pool = NDArrayPool::new(1_000_000);
1074 let arr = make_2d_array(4, 4, |_, _| 7.0);
1075 proc.process_array(&arr, &pool);
1076
1077 let data = rx.try_recv().unwrap();
1078 assert_eq!(data.values.len(), 10);
1080 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); }
1088
1089 fn make_1d_array(n: usize, fill: impl Fn(usize) -> f64) -> NDArray {
1090 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1091 if let NDDataBuffer::F64(ref mut v) = arr.data {
1092 for (i, slot) in v.iter_mut().enumerate() {
1093 *slot = fill(i);
1094 }
1095 }
1096 arr
1097 }
1098
1099 #[test]
1100 fn test_adp16_clamp_out_of_range_offset_to_one_pixel() {
1101 let roi = ROIStatROI {
1104 enabled: true,
1105 offset: [10, 1],
1106 size: [4, 9],
1107 bgd_width: 0,
1108 };
1109 let (offset, size) = ROIStatProcessor::clamp_roi_geometry(&roi, 2, [4, 4]).unwrap();
1110 assert_eq!(offset, [3, 1]); assert_eq!(size, [1, 3]); }
1113
1114 #[test]
1115 fn test_adp16_clamp_zero_size_to_one_pixel() {
1116 let roi = ROIStatROI {
1117 enabled: true,
1118 offset: [0, 0],
1119 size: [0, 0],
1120 bgd_width: 0,
1121 };
1122 let (offset, size) = ROIStatProcessor::clamp_roi_geometry(&roi, 2, [4, 4]).unwrap();
1123 assert_eq!(offset, [0, 0]);
1124 assert_eq!(size, [1, 1]); }
1126
1127 #[test]
1128 fn test_adp16_geometry_writeback_uses_clamped_values() {
1129 use asyn_rs::port::{PortDriverBase, PortFlags};
1130
1131 let arr = make_2d_array(4, 4, |_, _| 10.0);
1132 let rois = vec![ROIStatROI {
1133 enabled: true,
1134 offset: [10, 1],
1135 size: [4, 9],
1136 bgd_width: 0,
1137 }];
1138 let mut proc = ROIStatProcessor::new(rois, 0);
1139 let mut base = PortDriverBase::new("roistat_adp16", 1, PortFlags::default());
1140 proc.register_params(&mut base).unwrap();
1141 let p = *proc.params_handle().lock();
1142
1143 let pool = NDArrayPool::new(1_000_000);
1144 let res = proc.process_array(&arr, &pool);
1145
1146 let find = |reason: usize, addr: i32| {
1147 res.param_updates.iter().find_map(|u| match u {
1148 ParamUpdate::Int32 {
1149 reason: r,
1150 addr: a,
1151 value,
1152 } if *r == reason && *a == addr => Some(*value),
1153 _ => None,
1154 })
1155 };
1156 assert_eq!(find(p.dim0_min, 0), Some(3));
1158 assert_eq!(find(p.dim0_size, 0), Some(1));
1159 assert_eq!(find(p.dim1_min, 0), Some(1));
1160 assert_eq!(find(p.dim1_size, 0), Some(3));
1161 assert_eq!(find(p.dim0_max_size, 0), Some(4));
1162 assert_eq!(find(p.dim1_max_size, 0), Some(4));
1163 }
1164
1165 #[test]
1166 fn test_adp17_1d_background_uses_x_strips_only() {
1167 let arr = make_1d_array(6, |x| if x == 0 || x == 5 { 10.0 } else { 0.0 });
1173 let rois = vec![ROIStatROI {
1174 enabled: true,
1175 offset: [0, 0],
1176 size: [6, 0],
1177 bgd_width: 1,
1178 }];
1179 let mut proc = ROIStatProcessor::new(rois, 0);
1180 let pool = NDArrayPool::new(1_000_000);
1181 proc.process_array(&arr, &pool);
1182
1183 let r = &proc.results()[0];
1184 assert!((r.total - 20.0).abs() < 1e-10, "total={}", r.total);
1185 assert!((r.min - 0.0).abs() < 1e-10);
1186 assert!((r.max - 10.0).abs() < 1e-10);
1187 assert!((r.net + 40.0).abs() < 1e-10, "net={}", r.net);
1188 }
1189}