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