1use std::sync::Arc;
2
3use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
4use ad_core_rs::ndarray_pool::NDArrayPool;
5use ad_core_rs::plugin::runtime::{
6 NDPluginProcess, ParamUpdate, PluginParamSnapshot, ProcessResult,
7};
8use asyn_rs::param::ParamType;
9use asyn_rs::port::PortDriverBase;
10
11#[derive(Debug, Clone)]
13pub struct ROIDimConfig {
14 pub min: usize,
15 pub size: usize,
16 pub bin: usize,
17 pub reverse: bool,
18 pub enable: bool,
19 pub auto_size: bool,
21}
22
23impl Default for ROIDimConfig {
24 fn default() -> Self {
25 Self {
26 min: 0,
27 size: 0,
28 bin: 1,
29 reverse: false,
30 enable: true,
31 auto_size: false,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum AutoCenter {
39 None,
40 CenterOfMass,
41 PeakPosition,
42}
43
44#[derive(Debug, Clone)]
46pub struct ROIConfig {
47 pub dims: [ROIDimConfig; 3],
48 pub data_type: Option<NDDataType>,
49 pub enable_scale: bool,
50 pub scale: f64,
51 pub collapse_dims: bool,
52 pub autocenter: AutoCenter,
53}
54
55impl Default for ROIConfig {
56 fn default() -> Self {
57 Self {
58 dims: [
59 ROIDimConfig::default(),
60 ROIDimConfig::default(),
61 ROIDimConfig::default(),
62 ],
63 data_type: None,
64 enable_scale: false,
65 scale: 1.0,
66 collapse_dims: false,
67 autocenter: AutoCenter::None,
68 }
69 }
70}
71
72fn find_centroid_2d(data: &NDDataBuffer, x_size: usize, y_size: usize) -> (usize, usize) {
74 let mut cx = 0.0f64;
75 let mut cy = 0.0f64;
76 let mut total = 0.0f64;
77 for iy in 0..y_size {
78 for ix in 0..x_size {
79 let val = data.get_as_f64(iy * x_size + ix).unwrap_or(0.0);
80 total += val;
81 cx += val * ix as f64;
82 cy += val * iy as f64;
83 }
84 }
85 if total > 0.0 {
86 ((cx / total) as usize, (cy / total) as usize)
87 } else {
88 (x_size / 2, y_size / 2)
89 }
90}
91
92fn find_peak_2d(data: &NDDataBuffer, x_size: usize, y_size: usize) -> (usize, usize) {
94 let mut max_val = f64::NEG_INFINITY;
95 let mut max_x = 0;
96 let mut max_y = 0;
97 for iy in 0..y_size {
98 for ix in 0..x_size {
99 let val = data.get_as_f64(iy * x_size + ix).unwrap_or(0.0);
100 if val > max_val {
101 max_val = val;
102 max_x = ix;
103 max_y = iy;
104 }
105 }
106 }
107 (max_x, max_y)
108}
109
110pub fn extract_roi(src: &NDArray, config: &ROIConfig) -> Option<NDArray> {
118 use ad_core_rs::color::NDColorMode;
119 if src.dims.len() >= 3 {
120 let info = src.info();
121 if matches!(
122 info.color_mode,
123 NDColorMode::RGB1 | NDColorMode::RGB2 | NDColorMode::RGB3
124 ) {
125 return extract_roi_3d(src, config);
126 }
127 }
128 extract_roi_2d(src, config)
129}
130
131fn resolve_axis(cfg: &ROIDimConfig, dim_size: usize) -> (usize, usize, usize) {
148 if !cfg.enable || dim_size == 0 {
149 return (0, dim_size, 1);
150 }
151 let offset = cfg.min.min(dim_size - 1);
152 let size = if cfg.auto_size { dim_size } else { cfg.size };
153 let size = size.max(1).min(dim_size - offset);
154 let binning = cfg.bin.max(1).min(size);
158 (offset, size, binning)
159}
160
161fn convert_roi(src: &NDArray, dims_out: &[NDDimension], config: &ROIConfig) -> Option<NDArray> {
177 use ad_core_rs::convert::{convert_dims, convert_type};
178
179 let target_type = config.data_type.unwrap_or(src.data.data_type());
180 let scaled = config.enable_scale && config.scale != 0.0 && config.scale != 1.0;
181
182 let result = if scaled {
183 convert_dims(src, dims_out, NDDataType::Float64).and_then(|mut scratch| {
184 if let NDDataBuffer::F64(v) = &mut scratch.data {
185 for x in v.iter_mut() {
186 *x /= config.scale;
187 }
188 }
189 convert_type(&scratch, target_type)
190 })
191 } else {
192 convert_dims(src, dims_out, target_type)
193 };
194
195 match result {
196 Ok(arr) => Some(arr),
197 Err(e) => {
198 tracing::warn!(
202 error = %e,
203 from = ?src.data.data_type(),
204 to = ?target_type,
205 "ROI extraction failed; dropping frame"
206 );
207 None
208 }
209 }
210}
211
212pub fn extract_roi_3d(src: &NDArray, config: &ROIConfig) -> Option<NDArray> {
220 let info = src.info();
221 let (src_x, src_y, src_c) = (info.x_size, info.y_size, info.color_size.max(1));
222 if src_x == 0 || src_y == 0 || src_c == 0 {
223 return None;
224 }
225
226 let (x_min, x_roi, bin_x) = resolve_axis(&config.dims[0], src_x);
227 let (y_min, y_roi, bin_y) = resolve_axis(&config.dims[1], src_y);
228 let (c_min, c_roi, bin_c) = resolve_axis(&config.dims[2], src_c);
229
230 let (out_x, out_y, out_c) = (x_roi / bin_x, y_roi / bin_y, c_roi / bin_c);
231 if out_x == 0 || out_y == 0 || out_c == 0 {
232 return None;
233 }
234
235 let user_dims = info.user_dims();
239 let mut dims_out: Vec<NDDimension> = src.dims.clone();
240 for d in dims_out.iter_mut() {
241 d.offset = 0;
242 d.binning = 1;
243 d.reverse = false;
244 }
245 for (roi_dim, (min, size, bin)) in [
246 (x_min, x_roi, bin_x),
247 (y_min, y_roi, bin_y),
248 (c_min, c_roi, bin_c),
249 ]
250 .into_iter()
251 .enumerate()
252 {
253 let d = dims_out.get_mut(user_dims[roi_dim])?;
254 d.offset = min;
255 d.size = size;
256 d.binning = bin;
257 d.reverse = config.dims[roi_dim].reverse;
258 }
259
260 let mut arr = convert_roi(src, &dims_out, config)?;
261
262 let single_color = out_c == 1
269 && matches!(
270 info.color_mode,
271 ad_core_rs::color::NDColorMode::RGB1
272 | ad_core_rs::color::NDColorMode::RGB2
273 | ad_core_rs::color::NDColorMode::RGB3
274 );
275 if single_color || config.collapse_dims {
276 let collapsed: Vec<NDDimension> = arr.dims.iter().filter(|d| d.size > 1).cloned().collect();
277 arr.dims = if collapsed.is_empty() {
278 vec![NDDimension::new(1)]
279 } else {
280 collapsed
281 };
282 }
283
284 arr.unique_id = src.unique_id;
285 if single_color {
288 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
289 arr.attributes.add(NDAttribute::new_static(
290 "ColorMode",
291 "Color mode",
292 NDAttrSource::Driver,
293 NDAttrValue::Int32(ad_core_rs::color::NDColorMode::Mono as i32),
294 ));
295 }
296 Some(arr)
297}
298
299pub fn extract_roi_2d(src: &NDArray, config: &ROIConfig) -> Option<NDArray> {
301 if src.dims.len() < 2 {
302 return None;
303 }
304
305 let src_x = src.dims[0].size;
306 let src_y = src.dims[1].size;
307
308 let (eff_x_min, eff_x_size, bin_x) = resolve_axis(&config.dims[0], src_x);
309 let (eff_y_min, eff_y_size, bin_y) = resolve_axis(&config.dims[1], src_y);
310
311 let (roi_x_min, roi_y_min) = match config.autocenter {
314 AutoCenter::None => (eff_x_min, eff_y_min),
315 AutoCenter::CenterOfMass => {
316 let (cx, cy) = find_centroid_2d(&src.data, src_x, src_y);
317 let mx = cx
318 .saturating_sub(eff_x_size / 2)
319 .min(src_x.saturating_sub(eff_x_size));
320 let my = cy
321 .saturating_sub(eff_y_size / 2)
322 .min(src_y.saturating_sub(eff_y_size));
323 (mx, my)
324 }
325 AutoCenter::PeakPosition => {
326 let (px, py) = find_peak_2d(&src.data, src_x, src_y);
327 let mx = px
328 .saturating_sub(eff_x_size / 2)
329 .min(src_x.saturating_sub(eff_x_size));
330 let my = py
331 .saturating_sub(eff_y_size / 2)
332 .min(src_y.saturating_sub(eff_y_size));
333 (mx, my)
334 }
335 };
336
337 if eff_x_size == 0 || eff_y_size == 0 {
338 return None;
339 }
340
341 if eff_x_size / bin_x == 0 || eff_y_size / bin_y == 0 {
342 return None;
343 }
344
345 let mut dims_out: Vec<NDDimension> = src.dims.clone();
349 for d in dims_out.iter_mut() {
350 d.offset = 0;
351 d.binning = 1;
352 d.reverse = false;
353 }
354 dims_out[0] = NDDimension {
355 size: eff_x_size,
356 offset: roi_x_min,
357 binning: bin_x,
358 reverse: config.dims[0].reverse,
359 };
360 dims_out[1] = NDDimension {
361 size: eff_y_size,
362 offset: roi_y_min,
363 binning: bin_y,
364 reverse: config.dims[1].reverse,
365 };
366
367 let mut arr = convert_roi(src, &dims_out, config)?;
368
369 if config.collapse_dims {
370 let collapsed: Vec<NDDimension> = arr.dims.iter().filter(|d| d.size > 1).cloned().collect();
371 arr.dims = if collapsed.is_empty() {
372 vec![NDDimension::new(arr.dims[0].size)]
373 } else {
374 collapsed
375 };
376 }
377
378 arr.unique_id = src.unique_id;
379 Some(arr)
380}
381
382#[derive(Default, Clone, Copy)]
384pub struct ROIDimParams {
385 pub min: usize,
386 pub size: usize,
387 pub bin: usize,
388 pub reverse: usize,
389 pub enable: usize,
390 pub auto_size: usize,
391 pub max_size: usize,
392}
393
394#[derive(Default)]
396pub struct ROIParams {
397 pub dims: [ROIDimParams; 3],
398 pub enable_scale: usize,
399 pub scale: usize,
400 pub data_type: usize,
401 pub collapse_dims: usize,
402 pub name: usize,
403}
404
405pub struct ROIProcessor {
407 config: ROIConfig,
408 params: ROIParams,
409}
410
411impl ROIProcessor {
412 pub fn new(config: ROIConfig) -> Self {
413 Self {
414 config,
415 params: ROIParams::default(),
416 }
417 }
418
419 pub fn params(&self) -> &ROIParams {
421 &self.params
422 }
423}
424
425impl NDPluginProcess for ROIProcessor {
426 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
427 let user_dims = array.info().user_dims();
434 let mut updates = Vec::new();
435 for (i, dim_params) in self.params.dims.iter().enumerate() {
436 let dim_size = if i < array.dims.len() {
437 array.dims[user_dims[i]].size as i32
438 } else {
439 0
440 };
441 updates.push(ParamUpdate::int32(dim_params.max_size, dim_size));
442 }
443
444 match extract_roi(array, &self.config) {
445 Some(roi_arr) => ProcessResult {
446 output_arrays: vec![Arc::new(roi_arr)],
447 param_updates: updates,
448 scatter: false,
449 },
450 None => ProcessResult::sink(updates),
451 }
452 }
453
454 fn plugin_type(&self) -> &str {
455 "NDPluginROI"
456 }
457
458 fn register_params(
459 &mut self,
460 base: &mut PortDriverBase,
461 ) -> Result<(), asyn_rs::error::AsynError> {
462 let dim_names = ["DIM0", "DIM1", "DIM2"];
463 for (i, prefix) in dim_names.iter().enumerate() {
464 self.params.dims[i].min =
465 base.create_param(&format!("{prefix}_MIN"), ParamType::Int32)?;
466 self.params.dims[i].size =
467 base.create_param(&format!("{prefix}_SIZE"), ParamType::Int32)?;
468 self.params.dims[i].bin =
469 base.create_param(&format!("{prefix}_BIN"), ParamType::Int32)?;
470 self.params.dims[i].reverse =
471 base.create_param(&format!("{prefix}_REVERSE"), ParamType::Int32)?;
472 self.params.dims[i].enable =
473 base.create_param(&format!("{prefix}_ENABLE"), ParamType::Int32)?;
474 self.params.dims[i].auto_size =
475 base.create_param(&format!("{prefix}_AUTO_SIZE"), ParamType::Int32)?;
476 self.params.dims[i].max_size =
477 base.create_param(&format!("{prefix}_MAX_SIZE"), ParamType::Int32)?;
478
479 base.set_int32_param(self.params.dims[i].min, 0, self.config.dims[i].min as i32)?;
481 base.set_int32_param(self.params.dims[i].size, 0, self.config.dims[i].size as i32)?;
482 base.set_int32_param(self.params.dims[i].bin, 0, self.config.dims[i].bin as i32)?;
483 base.set_int32_param(
484 self.params.dims[i].reverse,
485 0,
486 self.config.dims[i].reverse as i32,
487 )?;
488 base.set_int32_param(
489 self.params.dims[i].enable,
490 0,
491 self.config.dims[i].enable as i32,
492 )?;
493 base.set_int32_param(
494 self.params.dims[i].auto_size,
495 0,
496 self.config.dims[i].auto_size as i32,
497 )?;
498 }
499 self.params.enable_scale = base.create_param("ENABLE_SCALE", ParamType::Int32)?;
500 self.params.scale = base.create_param("SCALE_VALUE", ParamType::Float64)?;
501 self.params.data_type = base.create_param("ROI_DATA_TYPE", ParamType::Int32)?;
502 self.params.collapse_dims = base.create_param("COLLAPSE_DIMS", ParamType::Int32)?;
503 self.params.name = base.create_param("NAME", ParamType::Octet)?;
504
505 base.set_int32_param(self.params.enable_scale, 0, self.config.enable_scale as i32)?;
506 base.set_float64_param(self.params.scale, 0, self.config.scale)?;
507 base.set_int32_param(self.params.data_type, 0, -1)?; base.set_int32_param(
509 self.params.collapse_dims,
510 0,
511 self.config.collapse_dims as i32,
512 )?;
513
514 Ok(())
515 }
516
517 fn on_param_change(
518 &mut self,
519 reason: usize,
520 snapshot: &PluginParamSnapshot,
521 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
522 let p = &self.params;
523 for i in 0..3 {
524 if reason == p.dims[i].min {
525 self.config.dims[i].min = snapshot.value.as_i32().max(0) as usize;
526 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
527 }
528 if reason == p.dims[i].size {
529 self.config.dims[i].size = snapshot.value.as_i32().max(0) as usize;
530 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
531 }
532 if reason == p.dims[i].bin {
533 self.config.dims[i].bin = snapshot.value.as_i32().max(1) as usize;
534 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
535 }
536 if reason == p.dims[i].reverse {
537 self.config.dims[i].reverse = snapshot.value.as_i32() != 0;
538 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
539 }
540 if reason == p.dims[i].enable {
541 self.config.dims[i].enable = snapshot.value.as_i32() != 0;
542 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
543 }
544 if reason == p.dims[i].auto_size {
545 self.config.dims[i].auto_size = snapshot.value.as_i32() != 0;
546 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
547 }
548 }
549 if reason == p.enable_scale {
550 self.config.enable_scale = snapshot.value.as_i32() != 0;
551 } else if reason == p.scale {
552 self.config.scale = snapshot.value.as_f64();
553 } else if reason == p.data_type {
554 let v = snapshot.value.as_i32();
555 self.config.data_type = if v < 0 {
556 None
557 } else {
558 NDDataType::from_ordinal(v as u8)
559 };
560 } else if reason == p.collapse_dims {
561 self.config.collapse_dims = snapshot.value.as_i32() != 0;
562 }
563 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
564 }
565}
566
567pub fn create_roi_runtime(
569 port_name: &str,
570 pool: Arc<NDArrayPool>,
571 queue_size: usize,
572 ndarray_port: &str,
573 wiring: Arc<ad_core_rs::plugin::wiring::WiringRegistry>,
574) -> (
575 ad_core_rs::plugin::runtime::PluginRuntimeHandle,
576 ROIParams,
577 std::thread::JoinHandle<()>,
578) {
579 let processor = ROIProcessor::new(ROIConfig::default());
580 let (handle, jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
581 port_name,
582 processor,
583 pool,
584 queue_size,
585 ndarray_port,
586 wiring,
587 );
588 let params = {
590 let mut base =
591 asyn_rs::port::PortDriverBase::new("_scratch_", 1, asyn_rs::port::PortFlags::default());
592 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
593 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
594 let mut p = ROIParams::default();
595 let dim_names = ["DIM0", "DIM1", "DIM2"];
596 for (i, prefix) in dim_names.iter().enumerate() {
597 p.dims[i].min = base
598 .create_param(&format!("{prefix}_MIN"), asyn_rs::param::ParamType::Int32)
599 .unwrap();
600 p.dims[i].size = base
601 .create_param(&format!("{prefix}_SIZE"), asyn_rs::param::ParamType::Int32)
602 .unwrap();
603 p.dims[i].bin = base
604 .create_param(&format!("{prefix}_BIN"), asyn_rs::param::ParamType::Int32)
605 .unwrap();
606 p.dims[i].reverse = base
607 .create_param(
608 &format!("{prefix}_REVERSE"),
609 asyn_rs::param::ParamType::Int32,
610 )
611 .unwrap();
612 p.dims[i].enable = base
613 .create_param(
614 &format!("{prefix}_ENABLE"),
615 asyn_rs::param::ParamType::Int32,
616 )
617 .unwrap();
618 p.dims[i].auto_size = base
619 .create_param(
620 &format!("{prefix}_AUTO_SIZE"),
621 asyn_rs::param::ParamType::Int32,
622 )
623 .unwrap();
624 p.dims[i].max_size = base
625 .create_param(
626 &format!("{prefix}_MAX_SIZE"),
627 asyn_rs::param::ParamType::Int32,
628 )
629 .unwrap();
630 }
631 p.enable_scale = base
632 .create_param("ENABLE_SCALE", asyn_rs::param::ParamType::Int32)
633 .unwrap();
634 p.scale = base
635 .create_param("SCALE_VALUE", asyn_rs::param::ParamType::Float64)
636 .unwrap();
637 p.data_type = base
638 .create_param("ROI_DATA_TYPE", asyn_rs::param::ParamType::Int32)
639 .unwrap();
640 p.collapse_dims = base
641 .create_param("COLLAPSE_DIMS", asyn_rs::param::ParamType::Int32)
642 .unwrap();
643 p.name = base
644 .create_param("NAME", asyn_rs::param::ParamType::Octet)
645 .unwrap();
646 p
647 };
648 (handle, params, jh)
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 fn make_4x4_u8() -> NDArray {
656 let mut arr = NDArray::new(
657 vec![NDDimension::new(4), NDDimension::new(4)],
658 NDDataType::UInt8,
659 );
660 if let NDDataBuffer::U8(ref mut v) = arr.data {
661 for i in 0..16 {
662 v[i] = i as u8;
663 }
664 }
665 arr
666 }
667
668 #[test]
669 fn test_r9_66_disabled_dimension_is_not_binned() {
670 let arr = make_4x4_u8();
678 let mut config = ROIConfig::default();
679 config.dims[0] = ROIDimConfig {
681 min: 2,
682 size: 1,
683 bin: 2,
684 reverse: false,
685 enable: false,
686 auto_size: false,
687 };
688 config.dims[1] = ROIDimConfig {
689 min: 0,
690 size: 4,
691 bin: 1,
692 reverse: false,
693 enable: true,
694 auto_size: false,
695 };
696
697 let roi = extract_roi_2d(&arr, &config).unwrap();
698 assert_eq!(roi.dims[0].size, 4, "disabled axis keeps its full size");
701 assert_eq!(roi.dims[1].size, 4);
702 if let NDDataBuffer::U8(ref v) = roi.data {
703 assert_eq!(&v[0..4], &[0, 1, 2, 3], "disabled axis must not bin-sum");
705 } else {
706 panic!("expected U8");
707 }
708
709 config.dims[0].enable = true;
712 config.dims[0].min = 0;
713 config.dims[0].auto_size = true; let roi = extract_roi_2d(&arr, &config).unwrap();
715 assert_eq!(
716 roi.dims[0].size, 2,
717 "enabled axis with bin=2 halves the axis"
718 );
719 if let NDDataBuffer::U8(ref v) = roi.data {
720 assert_eq!(&v[0..2], &[1, 5], "enabled axis bin-sums");
722 } else {
723 panic!("expected U8");
724 }
725 }
726
727 #[test]
728 fn test_r9_66_disabled_color_axis_is_not_binned() {
729 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
731 use ad_core_rs::color::NDColorMode;
732
733 let mut arr = NDArray::new(
735 vec![
736 NDDimension::new(3),
737 NDDimension::new(4),
738 NDDimension::new(2),
739 ],
740 NDDataType::UInt8,
741 );
742 arr.attributes.add(NDAttribute::new_static(
743 "ColorMode",
744 "",
745 NDAttrSource::Driver,
746 NDAttrValue::Int32(NDColorMode::RGB1 as i32),
747 ));
748 let mut config = ROIConfig::default();
749 config.dims[2] = ROIDimConfig {
751 min: 0,
752 size: 3,
753 bin: 3,
754 reverse: false,
755 enable: false,
756 auto_size: false,
757 };
758 for i in 0..2 {
759 config.dims[i] = ROIDimConfig {
760 min: 0,
761 size: 0,
762 bin: 1,
763 reverse: false,
764 enable: true,
765 auto_size: true,
766 };
767 }
768
769 let roi = extract_roi_3d(&arr, &config).unwrap();
770 assert_eq!(
773 roi.dims[0].size, 3,
774 "disabled colour axis keeps all 3 planes"
775 );
776 assert_eq!(roi.dims[1].size, 4);
777 assert_eq!(roi.dims[2].size, 2);
778 }
779
780 #[test]
781 fn test_extract_sub_region() {
782 let arr = make_4x4_u8();
783 let mut config = ROIConfig::default();
784 config.dims[0] = ROIDimConfig {
785 min: 1,
786 size: 2,
787 bin: 1,
788 reverse: false,
789 enable: true,
790 auto_size: false,
791 };
792 config.dims[1] = ROIDimConfig {
793 min: 1,
794 size: 2,
795 bin: 1,
796 reverse: false,
797 enable: true,
798 auto_size: false,
799 };
800
801 let roi = extract_roi_2d(&arr, &config).unwrap();
802 assert_eq!(roi.dims[0].size, 2);
803 assert_eq!(roi.dims[1].size, 2);
804 if let NDDataBuffer::U8(ref v) = roi.data {
805 assert_eq!(v[0], 5);
807 assert_eq!(v[1], 6);
808 assert_eq!(v[2], 9);
809 assert_eq!(v[3], 10);
810 }
811 }
812
813 #[test]
814 fn test_binning_2x2() {
815 let arr = make_4x4_u8();
816 let mut config = ROIConfig::default();
817 config.dims[0] = ROIDimConfig {
818 min: 0,
819 size: 4,
820 bin: 2,
821 reverse: false,
822 enable: true,
823 auto_size: false,
824 };
825 config.dims[1] = ROIDimConfig {
826 min: 0,
827 size: 4,
828 bin: 2,
829 reverse: false,
830 enable: true,
831 auto_size: false,
832 };
833
834 let roi = extract_roi_2d(&arr, &config).unwrap();
835 assert_eq!(roi.dims[0].size, 2);
836 assert_eq!(roi.dims[1].size, 2);
837 if let NDDataBuffer::U8(ref v) = roi.data {
838 assert_eq!(v[0], 10);
840 }
841 }
842
843 #[test]
844 fn test_reverse() {
845 let arr = make_4x4_u8();
846 let mut config = ROIConfig::default();
847 config.dims[0] = ROIDimConfig {
848 min: 0,
849 size: 4,
850 bin: 1,
851 reverse: true,
852 enable: true,
853 auto_size: false,
854 };
855 config.dims[1] = ROIDimConfig {
856 min: 0,
857 size: 1,
858 bin: 1,
859 reverse: false,
860 enable: true,
861 auto_size: false,
862 };
863
864 let roi = extract_roi_2d(&arr, &config).unwrap();
865 if let NDDataBuffer::U8(ref v) = roi.data {
866 assert_eq!(v[0], 3);
867 assert_eq!(v[1], 2);
868 assert_eq!(v[2], 1);
869 assert_eq!(v[3], 0);
870 }
871 }
872
873 #[test]
874 fn test_collapse_dims() {
875 let arr = make_4x4_u8();
876 let mut config = ROIConfig::default();
877 config.dims[0] = ROIDimConfig {
878 min: 0,
879 size: 4,
880 bin: 1,
881 reverse: false,
882 enable: true,
883 auto_size: false,
884 };
885 config.dims[1] = ROIDimConfig {
886 min: 0,
887 size: 1,
888 bin: 1,
889 reverse: false,
890 enable: true,
891 auto_size: false,
892 };
893 config.collapse_dims = true;
894
895 let roi = extract_roi_2d(&arr, &config).unwrap();
896 assert_eq!(roi.dims.len(), 1);
897 assert_eq!(roi.dims[0].size, 4);
898 }
899
900 #[test]
901 fn test_scale() {
902 let arr = make_4x4_u8();
903 let mut config = ROIConfig::default();
904 config.dims[0] = ROIDimConfig {
905 min: 0,
906 size: 2,
907 bin: 1,
908 reverse: false,
909 enable: true,
910 auto_size: false,
911 };
912 config.dims[1] = ROIDimConfig {
913 min: 0,
914 size: 1,
915 bin: 1,
916 reverse: false,
917 enable: true,
918 auto_size: false,
919 };
920 config.enable_scale = true;
921 config.scale = 2.0;
922
923 let roi = extract_roi_2d(&arr, &config).unwrap();
924 if let NDDataBuffer::U8(ref v) = roi.data {
925 assert_eq!(v[0], 0); assert_eq!(v[1], 0); }
929 }
930
931 #[test]
932 fn test_type_convert() {
933 let arr = make_4x4_u8();
934 let mut config = ROIConfig::default();
935 config.dims[0] = ROIDimConfig {
936 min: 0,
937 size: 2,
938 bin: 1,
939 reverse: false,
940 enable: true,
941 auto_size: false,
942 };
943 config.dims[1] = ROIDimConfig {
944 min: 0,
945 size: 1,
946 bin: 1,
947 reverse: false,
948 enable: true,
949 auto_size: false,
950 };
951 config.data_type = Some(NDDataType::UInt16);
952
953 let roi = extract_roi_2d(&arr, &config).unwrap();
954 assert_eq!(roi.data.data_type(), NDDataType::UInt16);
955 }
956
957 #[test]
960 fn test_roi_processor() {
961 let mut config = ROIConfig::default();
962 config.dims[0] = ROIDimConfig {
963 min: 1,
964 size: 2,
965 bin: 1,
966 reverse: false,
967 enable: true,
968 auto_size: false,
969 };
970 config.dims[1] = ROIDimConfig {
971 min: 1,
972 size: 2,
973 bin: 1,
974 reverse: false,
975 enable: true,
976 auto_size: false,
977 };
978
979 let mut proc = ROIProcessor::new(config);
980 let pool = NDArrayPool::new(1_000_000);
981
982 let arr = make_4x4_u8();
983 let result = proc.process_array(&arr, &pool);
984 assert_eq!(result.output_arrays.len(), 1);
985 assert_eq!(result.output_arrays[0].dims[0].size, 2);
986 assert_eq!(result.output_arrays[0].dims[1].size, 2);
987 }
988
989 #[test]
990 fn test_r9_67_max_size_uses_the_user_dims_mapping() {
991 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
998 use ad_core_rs::color::NDColorMode;
999 use asyn_rs::port::{PortDriverBase, PortFlags};
1000
1001 let mut arr = NDArray::new(
1003 vec![
1004 NDDimension::new(3),
1005 NDDimension::new(8),
1006 NDDimension::new(5),
1007 ],
1008 NDDataType::UInt8,
1009 );
1010 arr.attributes.add(NDAttribute::new_static(
1011 "ColorMode",
1012 "",
1013 NDAttrSource::Driver,
1014 NDAttrValue::Int32(NDColorMode::RGB1 as i32),
1015 ));
1016
1017 let mut proc = ROIProcessor::new(ROIConfig::default());
1018 let mut base = PortDriverBase::new("R9_67", 1, PortFlags::default());
1019 proc.register_params(&mut base).unwrap();
1020 let reasons = [
1021 proc.params().dims[0].max_size,
1022 proc.params().dims[1].max_size,
1023 proc.params().dims[2].max_size,
1024 ];
1025
1026 let pool = NDArrayPool::new(1_000_000);
1027 let result = proc.process_array(&arr, &pool);
1028 let max_size = |reason: usize| {
1029 result
1030 .param_updates
1031 .iter()
1032 .find_map(|u| match u {
1033 ParamUpdate::Int32 {
1034 reason: r, value, ..
1035 } if *r == reason => Some(*value),
1036 _ => None,
1037 })
1038 .expect("MaxSize update")
1039 };
1040
1041 assert_eq!(
1042 max_size(reasons[0]),
1043 8,
1044 "Dim0MaxSize is the X axis (dims[1])"
1045 );
1046 assert_eq!(
1047 max_size(reasons[1]),
1048 5,
1049 "Dim1MaxSize is the Y axis (dims[2])"
1050 );
1051 assert_eq!(
1052 max_size(reasons[2]),
1053 3,
1054 "Dim2MaxSize is the colour axis (dims[0])"
1055 );
1056
1057 let arr2d = NDArray::new(
1062 vec![NDDimension::new(6), NDDimension::new(4)],
1063 NDDataType::UInt8,
1064 );
1065 let result = proc.process_array(&arr2d, &pool);
1066 let max_size = |reason: usize| {
1067 result
1068 .param_updates
1069 .iter()
1070 .find_map(|u| match u {
1071 ParamUpdate::Int32 {
1072 reason: r, value, ..
1073 } if *r == reason => Some(*value),
1074 _ => None,
1075 })
1076 .expect("MaxSize update")
1077 };
1078 assert_eq!(max_size(reasons[0]), 6);
1079 assert_eq!(max_size(reasons[1]), 4);
1080 assert_eq!(max_size(reasons[2]), 0);
1081 }
1082
1083 #[test]
1086 fn test_auto_size() {
1087 let arr = make_4x4_u8();
1089 let mut config = ROIConfig::default();
1090 config.dims[0] = ROIDimConfig {
1091 min: 1,
1092 size: 0,
1093 bin: 1,
1094 reverse: false,
1095 enable: true,
1096 auto_size: true,
1097 };
1098 config.dims[1] = ROIDimConfig {
1099 min: 0,
1100 size: 0,
1101 bin: 1,
1102 reverse: false,
1103 enable: true,
1104 auto_size: true,
1105 };
1106
1107 let roi = extract_roi_2d(&arr, &config).unwrap();
1108 assert_eq!(roi.dims[0].size, 3);
1112 assert_eq!(roi.dims[1].size, 4);
1113 }
1114
1115 #[test]
1116 fn test_dim_disable() {
1117 let arr = make_4x4_u8();
1119 let mut config = ROIConfig::default();
1120 config.dims[0] = ROIDimConfig {
1121 min: 2,
1122 size: 1,
1123 bin: 1,
1124 reverse: false,
1125 enable: false,
1126 auto_size: false,
1127 };
1128 config.dims[1] = ROIDimConfig {
1129 min: 0,
1130 size: 4,
1131 bin: 1,
1132 reverse: false,
1133 enable: true,
1134 auto_size: false,
1135 };
1136
1137 let roi = extract_roi_2d(&arr, &config).unwrap();
1138 assert_eq!(roi.dims[0].size, 4);
1140 assert_eq!(roi.dims[1].size, 4);
1141 }
1142
1143 #[test]
1144 fn test_autocenter_peak() {
1145 let mut arr = NDArray::new(
1147 vec![NDDimension::new(8), NDDimension::new(8)],
1148 NDDataType::UInt8,
1149 );
1150 if let NDDataBuffer::U8(ref mut v) = arr.data {
1151 for i in 0..64 {
1152 v[i] = 1;
1153 }
1154 v[5 * 8 + 6] = 255;
1156 }
1157
1158 let mut config = ROIConfig::default();
1159 config.dims[0] = ROIDimConfig {
1160 min: 0,
1161 size: 4,
1162 bin: 1,
1163 reverse: false,
1164 enable: true,
1165 auto_size: false,
1166 };
1167 config.dims[1] = ROIDimConfig {
1168 min: 0,
1169 size: 4,
1170 bin: 1,
1171 reverse: false,
1172 enable: true,
1173 auto_size: false,
1174 };
1175 config.autocenter = AutoCenter::PeakPosition;
1176
1177 let roi = extract_roi_2d(&arr, &config).unwrap();
1178 assert_eq!(roi.dims[0].size, 4);
1179 assert_eq!(roi.dims[1].size, 4);
1180
1181 if let NDDataBuffer::U8(ref v) = roi.data {
1187 assert_eq!(v[2 * 4 + 2], 255); }
1189 }
1190
1191 #[test]
1192 fn test_offset_clamp_to_last_column() {
1193 let arr = make_4x4_u8();
1197 let mut config = ROIConfig::default();
1198 config.dims[0] = ROIDimConfig {
1200 min: 4,
1201 size: 10,
1202 bin: 1,
1203 reverse: false,
1204 enable: true,
1205 auto_size: false,
1206 };
1207 config.dims[1] = ROIDimConfig {
1208 min: 0,
1209 size: 1,
1210 bin: 1,
1211 reverse: false,
1212 enable: true,
1213 auto_size: false,
1214 };
1215 let roi = extract_roi_2d(&arr, &config).unwrap();
1216 assert_eq!(roi.dims[0].size, 1);
1218 if let NDDataBuffer::U8(ref v) = roi.data {
1219 assert_eq!(v[0], 3); }
1221 }
1222
1223 #[test]
1224 fn test_bin_larger_than_roi_clamps() {
1225 let arr = make_4x4_u8();
1229 let mut config = ROIConfig::default();
1230 config.dims[0] = ROIDimConfig {
1231 min: 0,
1232 size: 2,
1233 bin: 99, reverse: false,
1235 enable: true,
1236 auto_size: false,
1237 };
1238 config.dims[1] = ROIDimConfig {
1239 min: 0,
1240 size: 1,
1241 bin: 1,
1242 reverse: false,
1243 enable: true,
1244 auto_size: false,
1245 };
1246 let roi = extract_roi_2d(&arr, &config).unwrap();
1247 assert_eq!(roi.dims[0].size, 1);
1249 if let NDDataBuffer::U8(ref v) = roi.data {
1250 assert_eq!(v[0], 1);
1252 }
1253 }
1254
1255 fn make_rgb1_2x2() -> NDArray {
1257 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
1258 let mut arr = NDArray::new(
1259 vec![
1260 NDDimension::new(3),
1261 NDDimension::new(2),
1262 NDDimension::new(2),
1263 ],
1264 NDDataType::UInt8,
1265 );
1266 arr.attributes.add(NDAttribute::new_static(
1267 "ColorMode",
1268 "",
1269 NDAttrSource::Driver,
1270 NDAttrValue::Int32(ad_core_rs::color::NDColorMode::RGB1 as i32),
1271 ));
1272 if let NDDataBuffer::U8(ref mut v) = arr.data {
1273 for y in 0..2 {
1274 for x in 0..2 {
1275 for c in 0..3 {
1276 v[y * 6 + x * 3 + c] = (100 * y + 10 * x + c) as u8;
1277 }
1278 }
1279 }
1280 }
1281 arr
1282 }
1283
1284 #[test]
1285 fn test_roi_3d_rgb1_x_subregion() {
1286 let arr = make_rgb1_2x2();
1288 let mut config = ROIConfig::default();
1289 config.dims[0] = ROIDimConfig {
1291 min: 1,
1292 size: 1,
1293 bin: 1,
1294 reverse: false,
1295 enable: true,
1296 auto_size: false,
1297 };
1298 config.dims[1] = ROIDimConfig {
1299 min: 0,
1300 size: 2,
1301 bin: 1,
1302 reverse: false,
1303 enable: true,
1304 auto_size: false,
1305 };
1306 config.dims[2] = ROIDimConfig {
1307 min: 0,
1308 size: 3,
1309 bin: 1,
1310 reverse: false,
1311 enable: true,
1312 auto_size: false,
1313 };
1314 let roi = extract_roi(&arr, &config).unwrap();
1315 assert_eq!(roi.dims[0].size, 3);
1317 assert_eq!(roi.dims[1].size, 1);
1318 assert_eq!(roi.dims[2].size, 2);
1319 if let NDDataBuffer::U8(ref v) = roi.data {
1320 assert_eq!(&v[0..3], &[10, 11, 12]);
1322 assert_eq!(&v[3..6], &[110, 111, 112]);
1324 } else {
1325 panic!("not u8");
1326 }
1327 }
1328
1329 #[test]
1330 fn test_adp18_3d_rgb_honors_output_data_type() {
1331 let arr = make_rgb1_2x2();
1335 let mut config = ROIConfig::default();
1336 config.dims[0] = ROIDimConfig {
1337 min: 0,
1338 size: 2,
1339 bin: 1,
1340 reverse: false,
1341 enable: true,
1342 auto_size: false,
1343 };
1344 config.dims[1] = ROIDimConfig {
1345 min: 0,
1346 size: 2,
1347 bin: 1,
1348 reverse: false,
1349 enable: true,
1350 auto_size: false,
1351 };
1352 config.dims[2] = ROIDimConfig {
1353 min: 0,
1354 size: 3,
1355 bin: 1,
1356 reverse: false,
1357 enable: true,
1358 auto_size: false,
1359 };
1360 config.data_type = Some(NDDataType::UInt16);
1361
1362 let roi = extract_roi(&arr, &config).unwrap();
1363 assert_eq!(roi.data.data_type(), NDDataType::UInt16);
1364 assert_eq!(roi.dims[0].size, 3);
1366 assert_eq!(roi.dims[1].size, 2);
1367 assert_eq!(roi.dims[2].size, 2);
1368 if let NDDataBuffer::U16(ref v) = roi.data {
1370 assert_eq!(&v[0..3], &[0, 1, 2]);
1371 } else {
1372 panic!("not u16");
1373 }
1374 }
1375
1376 #[test]
1377 fn test_adp19_single_color_collapses_to_2d_mono() {
1378 use ad_core_rs::color::NDColorMode;
1379 let arr = make_rgb1_2x2();
1384 let mut config = ROIConfig::default();
1385 config.dims[0] = ROIDimConfig {
1386 min: 0,
1387 size: 2,
1388 bin: 1,
1389 reverse: false,
1390 enable: true,
1391 auto_size: false,
1392 };
1393 config.dims[1] = ROIDimConfig {
1394 min: 0,
1395 size: 2,
1396 bin: 1,
1397 reverse: false,
1398 enable: true,
1399 auto_size: false,
1400 };
1401 config.dims[2] = ROIDimConfig {
1402 min: 1,
1403 size: 1,
1404 bin: 1,
1405 reverse: false,
1406 enable: true,
1407 auto_size: false,
1408 };
1409
1410 let roi = extract_roi(&arr, &config).unwrap();
1411 assert_eq!(roi.dims.len(), 2);
1413 assert_eq!(roi.dims[0].size, 2);
1414 assert_eq!(roi.dims[1].size, 2);
1415 let cm = roi
1417 .attributes
1418 .get("ColorMode")
1419 .and_then(|a| a.value.as_i64())
1420 .unwrap();
1421 assert_eq!(cm, NDColorMode::Mono as i64);
1422 if let NDDataBuffer::U8(ref v) = roi.data {
1424 assert_eq!(v, &[1, 11, 101, 111]);
1425 } else {
1426 panic!("not u8");
1427 }
1428 }
1429 #[test]
1438 fn test_bin_sum_wraps_modulo_output_type() {
1439 let mut arr = NDArray::new(
1440 vec![NDDimension::new(3), NDDimension::new(3)],
1441 NDDataType::UInt8,
1442 );
1443 if let NDDataBuffer::U8(ref mut v) = arr.data {
1444 v.iter_mut().for_each(|p| *p = 100);
1445 }
1446
1447 let mut config = ROIConfig {
1448 enable_scale: false,
1449 ..Default::default()
1450 };
1451 for d in 0..2 {
1452 config.dims[d] = ROIDimConfig {
1453 min: 0,
1454 size: 3,
1455 bin: 3,
1456 reverse: false,
1457 enable: true,
1458 auto_size: false,
1459 };
1460 }
1461
1462 let roi = extract_roi_2d(&arr, &config).unwrap();
1463 if let NDDataBuffer::U8(ref v) = roi.data {
1464 assert_eq!(v[0], 132, "900 % 256 == 132 (C wraps), not 255");
1465 } else {
1466 panic!("not u8");
1467 }
1468 }
1469
1470 #[test]
1474 fn test_bin_sum_with_scale_uses_float64_intermediate() {
1475 let mut arr = NDArray::new(
1476 vec![NDDimension::new(3), NDDimension::new(3)],
1477 NDDataType::UInt8,
1478 );
1479 if let NDDataBuffer::U8(ref mut v) = arr.data {
1480 v.iter_mut().for_each(|p| *p = 100);
1481 }
1482
1483 let mut config = ROIConfig {
1484 enable_scale: true,
1485 scale: 9.0,
1486 ..Default::default()
1487 };
1488 for d in 0..2 {
1489 config.dims[d] = ROIDimConfig {
1490 min: 0,
1491 size: 3,
1492 bin: 3,
1493 reverse: false,
1494 enable: true,
1495 auto_size: false,
1496 };
1497 }
1498
1499 let roi = extract_roi_2d(&arr, &config).unwrap();
1500 if let NDDataBuffer::U8(ref v) = roi.data {
1501 assert_eq!(v[0], 100, "900/9 via the Float64 path");
1502 } else {
1503 panic!("not u8");
1504 }
1505 }
1506
1507 #[test]
1510 fn test_narrowing_output_type_wraps() {
1511 let mut arr = NDArray::new(
1512 vec![NDDimension::new(2), NDDimension::new(1)],
1513 NDDataType::UInt16,
1514 );
1515 if let NDDataBuffer::U16(ref mut v) = arr.data {
1516 v[0] = 300;
1517 v[1] = 70;
1518 }
1519
1520 let mut config = ROIConfig {
1521 data_type: Some(NDDataType::UInt8),
1522 ..Default::default()
1523 };
1524 config.dims[0] = ROIDimConfig {
1525 min: 0,
1526 size: 2,
1527 bin: 1,
1528 reverse: false,
1529 enable: true,
1530 auto_size: false,
1531 };
1532 config.dims[1] = ROIDimConfig {
1533 min: 0,
1534 size: 1,
1535 bin: 1,
1536 reverse: false,
1537 enable: true,
1538 auto_size: false,
1539 };
1540
1541 let roi = extract_roi_2d(&arr, &config).unwrap();
1542 assert_eq!(roi.data.data_type(), NDDataType::UInt8);
1543 if let NDDataBuffer::U8(ref v) = roi.data {
1544 assert_eq!(v[0], 44, "(epicsUInt8)300 == 44");
1545 assert_eq!(v[1], 70);
1546 } else {
1547 panic!("not u8");
1548 }
1549 }
1550
1551 #[test]
1555 fn test_bin_sum_accumulates_in_the_output_type() {
1556 let mut arr = NDArray::new(
1557 vec![NDDimension::new(3), NDDimension::new(3)],
1558 NDDataType::UInt8,
1559 );
1560 if let NDDataBuffer::U8(ref mut v) = arr.data {
1561 v.iter_mut().for_each(|p| *p = 100);
1562 }
1563
1564 let mut config = ROIConfig {
1565 data_type: Some(NDDataType::UInt16),
1566 ..Default::default()
1567 };
1568 for d in 0..2 {
1569 config.dims[d] = ROIDimConfig {
1570 min: 0,
1571 size: 3,
1572 bin: 3,
1573 reverse: false,
1574 enable: true,
1575 auto_size: false,
1576 };
1577 }
1578
1579 let roi = extract_roi_2d(&arr, &config).unwrap();
1580 if let NDDataBuffer::U16(ref v) = roi.data {
1581 assert_eq!(v[0], 900, "the bin sum accumulates in UInt16");
1582 } else {
1583 panic!("not u16");
1584 }
1585 }
1586}