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
131pub fn extract_roi_3d(src: &NDArray, config: &ROIConfig) -> Option<NDArray> {
139 let info = src.info();
140 let (src_x, src_y, src_c) = (info.x_size, info.y_size, info.color_size.max(1));
141 if src_x == 0 || src_y == 0 || src_c == 0 {
142 return None;
143 }
144
145 let resolve = |cfg: &ROIDimConfig, dim_size: usize| -> (usize, usize) {
147 if !cfg.enable || dim_size == 0 {
148 return (0, dim_size);
149 }
150 let offset = cfg.min.min(dim_size - 1);
151 let size = if cfg.auto_size { dim_size } else { cfg.size };
152 let size = size.max(1).min(dim_size - offset);
153 (offset, size)
154 };
155 let (x_min, x_roi) = resolve(&config.dims[0], src_x);
156 let (y_min, y_roi) = resolve(&config.dims[1], src_y);
157 let (c_min, c_roi) = resolve(&config.dims[2], src_c);
158
159 let bin_x = config.dims[0].bin.max(1).min(x_roi);
160 let bin_y = config.dims[1].bin.max(1).min(y_roi);
161 let bin_c = config.dims[2].bin.max(1).min(c_roi);
162 let (out_x, out_y, out_c) = (x_roi / bin_x, y_roi / bin_y, c_roi / bin_c);
163 if out_x == 0 || out_y == 0 || out_c == 0 {
164 return None;
165 }
166
167 let (sxs, sys, scs) = (
169 info.x_stride.max(1),
170 info.y_stride.max(1),
171 info.color_stride.max(1),
172 );
173 let (dims, dxs, dys, dcs) = match info.color_mode {
175 ad_core_rs::color::NDColorMode::RGB1 => (
176 vec![
177 NDDimension::new(out_c),
178 NDDimension::new(out_x),
179 NDDimension::new(out_y),
180 ],
181 out_c,
182 out_x * out_c,
183 1usize,
184 ),
185 ad_core_rs::color::NDColorMode::RGB2 => (
186 vec![
187 NDDimension::new(out_x),
188 NDDimension::new(out_c),
189 NDDimension::new(out_y),
190 ],
191 1usize,
192 out_x * out_c,
193 out_x,
194 ),
195 _ => (
196 vec![
197 NDDimension::new(out_x),
198 NDDimension::new(out_y),
199 NDDimension::new(out_c),
200 ],
201 1usize,
202 out_x,
203 out_x * out_y,
204 ),
205 };
206 let total = out_x * out_y * out_c;
207
208 macro_rules! extract3d {
209 ($vec:expr, $T:ty, $zero:expr) => {{
210 let mut out = vec![$zero; total];
211 for oc in 0..out_c {
212 for oy in 0..out_y {
213 for ox in 0..out_x {
214 let mut sum = 0.0f64;
215 for bc in 0..bin_c {
216 for by in 0..bin_y {
217 for bx in 0..bin_x {
218 let sx = x_min + ox * bin_x + bx;
219 let sy = y_min + oy * bin_y + by;
220 let sc = c_min + oc * bin_c + bc;
221 sum += $vec[sy * sys + sx * sxs + sc * scs] as f64;
222 }
223 }
224 }
225 let dx = if config.dims[0].reverse {
226 out_x - 1 - ox
227 } else {
228 ox
229 };
230 let dy = if config.dims[1].reverse {
231 out_y - 1 - oy
232 } else {
233 oy
234 };
235 let dc = if config.dims[2].reverse {
236 out_c - 1 - oc
237 } else {
238 oc
239 };
240 let scaled = if config.enable_scale && config.scale != 0.0 {
241 sum / config.scale
242 } else {
243 sum
244 };
245 out[dy * dys + dx * dxs + dc * dcs] = scaled as $T;
246 }
247 }
248 }
249 out
250 }};
251 }
252
253 let out_data = match &src.data {
254 NDDataBuffer::U8(v) => NDDataBuffer::U8(extract3d!(v, u8, 0)),
255 NDDataBuffer::U16(v) => NDDataBuffer::U16(extract3d!(v, u16, 0)),
256 NDDataBuffer::I8(v) => NDDataBuffer::I8(extract3d!(v, i8, 0)),
257 NDDataBuffer::I16(v) => NDDataBuffer::I16(extract3d!(v, i16, 0)),
258 NDDataBuffer::I32(v) => NDDataBuffer::I32(extract3d!(v, i32, 0)),
259 NDDataBuffer::U32(v) => NDDataBuffer::U32(extract3d!(v, u32, 0)),
260 NDDataBuffer::I64(v) => NDDataBuffer::I64(extract3d!(v, i64, 0)),
261 NDDataBuffer::U64(v) => NDDataBuffer::U64(extract3d!(v, u64, 0)),
262 NDDataBuffer::F32(v) => NDDataBuffer::F32(extract3d!(v, f32, 0.0)),
263 NDDataBuffer::F64(v) => NDDataBuffer::F64(extract3d!(v, f64, 0.0)),
264 };
265
266 let single_color = out_c == 1
273 && matches!(
274 info.color_mode,
275 ad_core_rs::color::NDColorMode::RGB1
276 | ad_core_rs::color::NDColorMode::RGB2
277 | ad_core_rs::color::NDColorMode::RGB3
278 );
279 let dims = if single_color || config.collapse_dims {
280 let collapsed: Vec<NDDimension> = dims.into_iter().filter(|d| d.size > 1).collect();
281 if collapsed.is_empty() {
282 vec![NDDimension::new(1)]
283 } else {
284 collapsed
285 }
286 } else {
287 dims
288 };
289
290 let target_type = config.data_type.unwrap_or(src.data.data_type());
294 let mut arr = NDArray::new(dims, target_type);
295 if target_type == src.data.data_type() {
296 arr.data = out_data;
297 } else {
298 let mut temp = NDArray::new(arr.dims.clone(), src.data.data_type());
299 temp.data = out_data;
300 match ad_core_rs::color::convert_data_type(&temp, target_type) {
301 Ok(converted) => arr.data = converted.data,
302 Err(e) => {
303 tracing::warn!(
304 error = %e,
305 from = ?src.data.data_type(),
306 to = ?target_type,
307 "ROI 3-D output data-type conversion failed; dropping frame"
308 );
309 return None;
310 }
311 }
312 }
313 arr.unique_id = src.unique_id;
314 arr.timestamp = src.timestamp;
315 arr.time_stamp = src.time_stamp;
316 arr.attributes = src.attributes.clone();
317 if single_color {
320 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
321 arr.attributes.add(NDAttribute::new_static(
322 "ColorMode",
323 "Color mode",
324 NDAttrSource::Driver,
325 NDAttrValue::Int32(ad_core_rs::color::NDColorMode::Mono as i32),
326 ));
327 }
328 Some(arr)
329}
330
331pub fn extract_roi_2d(src: &NDArray, config: &ROIConfig) -> Option<NDArray> {
333 if src.dims.len() < 2 {
334 return None;
335 }
336
337 let src_x = src.dims[0].size;
338 let src_y = src.dims[1].size;
339
340 let resolve = |cfg: &ROIDimConfig, dim_size: usize| -> (usize, usize) {
346 if !cfg.enable || dim_size == 0 {
347 return (0, dim_size);
348 }
349 let offset = cfg.min.min(dim_size - 1);
351 let size = if cfg.auto_size { dim_size } else { cfg.size };
352 let size = size.max(1).min(dim_size - offset);
354 (offset, size)
355 };
356 let (eff_x_min, eff_x_size) = resolve(&config.dims[0], src_x);
357 let (eff_y_min, eff_y_size) = resolve(&config.dims[1], src_y);
358
359 let (roi_x_min, roi_y_min) = match config.autocenter {
362 AutoCenter::None => (eff_x_min, eff_y_min),
363 AutoCenter::CenterOfMass => {
364 let (cx, cy) = find_centroid_2d(&src.data, src_x, src_y);
365 let mx = cx
366 .saturating_sub(eff_x_size / 2)
367 .min(src_x.saturating_sub(eff_x_size));
368 let my = cy
369 .saturating_sub(eff_y_size / 2)
370 .min(src_y.saturating_sub(eff_y_size));
371 (mx, my)
372 }
373 AutoCenter::PeakPosition => {
374 let (px, py) = find_peak_2d(&src.data, src_x, src_y);
375 let mx = px
376 .saturating_sub(eff_x_size / 2)
377 .min(src_x.saturating_sub(eff_x_size));
378 let my = py
379 .saturating_sub(eff_y_size / 2)
380 .min(src_y.saturating_sub(eff_y_size));
381 (mx, my)
382 }
383 };
384
385 let roi_x_size = eff_x_size;
386 let roi_y_size = eff_y_size;
387
388 if roi_x_size == 0 || roi_y_size == 0 {
389 return None;
390 }
391
392 let bin_x = config.dims[0].bin.max(1).min(roi_x_size);
396 let bin_y = config.dims[1].bin.max(1).min(roi_y_size);
397 let out_x = roi_x_size / bin_x;
398 let out_y = roi_y_size / bin_y;
399
400 if out_x == 0 || out_y == 0 {
401 return None;
402 }
403
404 macro_rules! extract {
405 ($vec:expr, $T:ty, $zero:expr) => {{
406 let mut out = vec![$zero; out_x * out_y];
407 for oy in 0..out_y {
408 for ox in 0..out_x {
409 let mut sum = 0.0f64;
410 let mut _count = 0usize;
411 for by in 0..bin_y {
412 for bx in 0..bin_x {
413 let sx = roi_x_min + ox * bin_x + bx;
414 let sy = roi_y_min + oy * bin_y + by;
415 if sx < src_x && sy < src_y {
416 sum += $vec[sy * src_x + sx] as f64;
417 _count += 1;
418 }
419 }
420 }
421 let val = sum;
423 let idx = if config.dims[0].reverse {
424 out_x - 1 - ox
425 } else {
426 ox
427 } + if config.dims[1].reverse {
428 out_y - 1 - oy
429 } else {
430 oy
431 } * out_x;
432 let scaled = if config.enable_scale && config.scale != 0.0 {
433 val / config.scale
434 } else {
435 val
436 };
437 out[idx] = scaled as $T;
438 }
439 }
440 out
441 }};
442 }
443
444 let out_data = match &src.data {
445 NDDataBuffer::U8(v) => NDDataBuffer::U8(extract!(v, u8, 0)),
446 NDDataBuffer::U16(v) => NDDataBuffer::U16(extract!(v, u16, 0)),
447 NDDataBuffer::I8(v) => NDDataBuffer::I8(extract!(v, i8, 0)),
448 NDDataBuffer::I16(v) => NDDataBuffer::I16(extract!(v, i16, 0)),
449 NDDataBuffer::I32(v) => NDDataBuffer::I32(extract!(v, i32, 0)),
450 NDDataBuffer::U32(v) => NDDataBuffer::U32(extract!(v, u32, 0)),
451 NDDataBuffer::I64(v) => NDDataBuffer::I64(extract!(v, i64, 0)),
452 NDDataBuffer::U64(v) => NDDataBuffer::U64(extract!(v, u64, 0)),
453 NDDataBuffer::F32(v) => NDDataBuffer::F32(extract!(v, f32, 0.0)),
454 NDDataBuffer::F64(v) => NDDataBuffer::F64(extract!(v, f64, 0.0)),
455 };
456
457 let out_dims = if config.collapse_dims {
458 let all_dims = vec![NDDimension::new(out_x), NDDimension::new(out_y)];
459 let filtered: Vec<NDDimension> = all_dims.into_iter().filter(|d| d.size > 1).collect();
460 if filtered.is_empty() {
461 vec![NDDimension::new(out_x)]
462 } else {
463 filtered
464 }
465 } else {
466 vec![NDDimension::new(out_x), NDDimension::new(out_y)]
467 };
468
469 let target_type = config.data_type.unwrap_or(src.data.data_type());
471
472 let mut arr = NDArray::new(out_dims, target_type);
473 if target_type == src.data.data_type() {
474 arr.data = out_data;
475 } else {
476 let mut temp = NDArray::new(arr.dims.clone(), src.data.data_type());
478 temp.data = out_data;
479 match ad_core_rs::color::convert_data_type(&temp, target_type) {
480 Ok(converted) => arr.data = converted.data,
481 Err(e) => {
482 tracing::warn!(
486 error = %e,
487 from = ?src.data.data_type(),
488 to = ?target_type,
489 "ROI output data-type conversion failed; dropping frame"
490 );
491 return None;
492 }
493 }
494 }
495
496 arr.unique_id = src.unique_id;
497 arr.timestamp = src.timestamp;
498 arr.attributes = src.attributes.clone();
499 Some(arr)
500}
501
502#[derive(Default, Clone, Copy)]
504pub struct ROIDimParams {
505 pub min: usize,
506 pub size: usize,
507 pub bin: usize,
508 pub reverse: usize,
509 pub enable: usize,
510 pub auto_size: usize,
511 pub max_size: usize,
512}
513
514#[derive(Default)]
516pub struct ROIParams {
517 pub dims: [ROIDimParams; 3],
518 pub enable_scale: usize,
519 pub scale: usize,
520 pub data_type: usize,
521 pub collapse_dims: usize,
522 pub name: usize,
523}
524
525pub struct ROIProcessor {
527 config: ROIConfig,
528 params: ROIParams,
529}
530
531impl ROIProcessor {
532 pub fn new(config: ROIConfig) -> Self {
533 Self {
534 config,
535 params: ROIParams::default(),
536 }
537 }
538
539 pub fn params(&self) -> &ROIParams {
541 &self.params
542 }
543}
544
545impl NDPluginProcess for ROIProcessor {
546 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
547 let mut updates = Vec::new();
549 for (i, dim_params) in self.params.dims.iter().enumerate() {
550 let dim_size = array.dims.get(i).map(|d| d.size as i32).unwrap_or(0);
551 updates.push(ParamUpdate::int32(dim_params.max_size, dim_size));
552 }
553
554 match extract_roi(array, &self.config) {
555 Some(roi_arr) => ProcessResult {
556 output_arrays: vec![Arc::new(roi_arr)],
557 param_updates: updates,
558 scatter: false,
559 },
560 None => ProcessResult::sink(updates),
561 }
562 }
563
564 fn plugin_type(&self) -> &str {
565 "NDPluginROI"
566 }
567
568 fn register_params(
569 &mut self,
570 base: &mut PortDriverBase,
571 ) -> Result<(), asyn_rs::error::AsynError> {
572 let dim_names = ["DIM0", "DIM1", "DIM2"];
573 for (i, prefix) in dim_names.iter().enumerate() {
574 self.params.dims[i].min =
575 base.create_param(&format!("{prefix}_MIN"), ParamType::Int32)?;
576 self.params.dims[i].size =
577 base.create_param(&format!("{prefix}_SIZE"), ParamType::Int32)?;
578 self.params.dims[i].bin =
579 base.create_param(&format!("{prefix}_BIN"), ParamType::Int32)?;
580 self.params.dims[i].reverse =
581 base.create_param(&format!("{prefix}_REVERSE"), ParamType::Int32)?;
582 self.params.dims[i].enable =
583 base.create_param(&format!("{prefix}_ENABLE"), ParamType::Int32)?;
584 self.params.dims[i].auto_size =
585 base.create_param(&format!("{prefix}_AUTO_SIZE"), ParamType::Int32)?;
586 self.params.dims[i].max_size =
587 base.create_param(&format!("{prefix}_MAX_SIZE"), ParamType::Int32)?;
588
589 base.set_int32_param(self.params.dims[i].min, 0, self.config.dims[i].min as i32)?;
591 base.set_int32_param(self.params.dims[i].size, 0, self.config.dims[i].size as i32)?;
592 base.set_int32_param(self.params.dims[i].bin, 0, self.config.dims[i].bin as i32)?;
593 base.set_int32_param(
594 self.params.dims[i].reverse,
595 0,
596 self.config.dims[i].reverse as i32,
597 )?;
598 base.set_int32_param(
599 self.params.dims[i].enable,
600 0,
601 self.config.dims[i].enable as i32,
602 )?;
603 base.set_int32_param(
604 self.params.dims[i].auto_size,
605 0,
606 self.config.dims[i].auto_size as i32,
607 )?;
608 }
609 self.params.enable_scale = base.create_param("ENABLE_SCALE", ParamType::Int32)?;
610 self.params.scale = base.create_param("SCALE_VALUE", ParamType::Float64)?;
611 self.params.data_type = base.create_param("ROI_DATA_TYPE", ParamType::Int32)?;
612 self.params.collapse_dims = base.create_param("COLLAPSE_DIMS", ParamType::Int32)?;
613 self.params.name = base.create_param("NAME", ParamType::Octet)?;
614
615 base.set_int32_param(self.params.enable_scale, 0, self.config.enable_scale as i32)?;
616 base.set_float64_param(self.params.scale, 0, self.config.scale)?;
617 base.set_int32_param(self.params.data_type, 0, -1)?; base.set_int32_param(
619 self.params.collapse_dims,
620 0,
621 self.config.collapse_dims as i32,
622 )?;
623
624 Ok(())
625 }
626
627 fn on_param_change(
628 &mut self,
629 reason: usize,
630 snapshot: &PluginParamSnapshot,
631 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
632 let p = &self.params;
633 for i in 0..3 {
634 if reason == p.dims[i].min {
635 self.config.dims[i].min = snapshot.value.as_i32().max(0) as usize;
636 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
637 }
638 if reason == p.dims[i].size {
639 self.config.dims[i].size = snapshot.value.as_i32().max(0) as usize;
640 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
641 }
642 if reason == p.dims[i].bin {
643 self.config.dims[i].bin = snapshot.value.as_i32().max(1) as usize;
644 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
645 }
646 if reason == p.dims[i].reverse {
647 self.config.dims[i].reverse = snapshot.value.as_i32() != 0;
648 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
649 }
650 if reason == p.dims[i].enable {
651 self.config.dims[i].enable = snapshot.value.as_i32() != 0;
652 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
653 }
654 if reason == p.dims[i].auto_size {
655 self.config.dims[i].auto_size = snapshot.value.as_i32() != 0;
656 return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
657 }
658 }
659 if reason == p.enable_scale {
660 self.config.enable_scale = snapshot.value.as_i32() != 0;
661 } else if reason == p.scale {
662 self.config.scale = snapshot.value.as_f64();
663 } else if reason == p.data_type {
664 let v = snapshot.value.as_i32();
665 self.config.data_type = if v < 0 {
666 None
667 } else {
668 NDDataType::from_ordinal(v as u8)
669 };
670 } else if reason == p.collapse_dims {
671 self.config.collapse_dims = snapshot.value.as_i32() != 0;
672 }
673 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
674 }
675}
676
677pub fn create_roi_runtime(
679 port_name: &str,
680 pool: Arc<NDArrayPool>,
681 queue_size: usize,
682 ndarray_port: &str,
683 wiring: Arc<ad_core_rs::plugin::wiring::WiringRegistry>,
684) -> (
685 ad_core_rs::plugin::runtime::PluginRuntimeHandle,
686 ROIParams,
687 std::thread::JoinHandle<()>,
688) {
689 let processor = ROIProcessor::new(ROIConfig::default());
690 let (handle, jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
691 port_name,
692 processor,
693 pool,
694 queue_size,
695 ndarray_port,
696 wiring,
697 );
698 let params = {
700 let mut base =
701 asyn_rs::port::PortDriverBase::new("_scratch_", 1, asyn_rs::port::PortFlags::default());
702 let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
703 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
704 let mut p = ROIParams::default();
705 let dim_names = ["DIM0", "DIM1", "DIM2"];
706 for (i, prefix) in dim_names.iter().enumerate() {
707 p.dims[i].min = base
708 .create_param(&format!("{prefix}_MIN"), asyn_rs::param::ParamType::Int32)
709 .unwrap();
710 p.dims[i].size = base
711 .create_param(&format!("{prefix}_SIZE"), asyn_rs::param::ParamType::Int32)
712 .unwrap();
713 p.dims[i].bin = base
714 .create_param(&format!("{prefix}_BIN"), asyn_rs::param::ParamType::Int32)
715 .unwrap();
716 p.dims[i].reverse = base
717 .create_param(
718 &format!("{prefix}_REVERSE"),
719 asyn_rs::param::ParamType::Int32,
720 )
721 .unwrap();
722 p.dims[i].enable = base
723 .create_param(
724 &format!("{prefix}_ENABLE"),
725 asyn_rs::param::ParamType::Int32,
726 )
727 .unwrap();
728 p.dims[i].auto_size = base
729 .create_param(
730 &format!("{prefix}_AUTO_SIZE"),
731 asyn_rs::param::ParamType::Int32,
732 )
733 .unwrap();
734 p.dims[i].max_size = base
735 .create_param(
736 &format!("{prefix}_MAX_SIZE"),
737 asyn_rs::param::ParamType::Int32,
738 )
739 .unwrap();
740 }
741 p.enable_scale = base
742 .create_param("ENABLE_SCALE", asyn_rs::param::ParamType::Int32)
743 .unwrap();
744 p.scale = base
745 .create_param("SCALE_VALUE", asyn_rs::param::ParamType::Float64)
746 .unwrap();
747 p.data_type = base
748 .create_param("ROI_DATA_TYPE", asyn_rs::param::ParamType::Int32)
749 .unwrap();
750 p.collapse_dims = base
751 .create_param("COLLAPSE_DIMS", asyn_rs::param::ParamType::Int32)
752 .unwrap();
753 p.name = base
754 .create_param("NAME", asyn_rs::param::ParamType::Octet)
755 .unwrap();
756 p
757 };
758 (handle, params, jh)
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 fn make_4x4_u8() -> NDArray {
766 let mut arr = NDArray::new(
767 vec![NDDimension::new(4), NDDimension::new(4)],
768 NDDataType::UInt8,
769 );
770 if let NDDataBuffer::U8(ref mut v) = arr.data {
771 for i in 0..16 {
772 v[i] = i as u8;
773 }
774 }
775 arr
776 }
777
778 #[test]
779 fn test_extract_sub_region() {
780 let arr = make_4x4_u8();
781 let mut config = ROIConfig::default();
782 config.dims[0] = ROIDimConfig {
783 min: 1,
784 size: 2,
785 bin: 1,
786 reverse: false,
787 enable: true,
788 auto_size: false,
789 };
790 config.dims[1] = ROIDimConfig {
791 min: 1,
792 size: 2,
793 bin: 1,
794 reverse: false,
795 enable: true,
796 auto_size: false,
797 };
798
799 let roi = extract_roi_2d(&arr, &config).unwrap();
800 assert_eq!(roi.dims[0].size, 2);
801 assert_eq!(roi.dims[1].size, 2);
802 if let NDDataBuffer::U8(ref v) = roi.data {
803 assert_eq!(v[0], 5);
805 assert_eq!(v[1], 6);
806 assert_eq!(v[2], 9);
807 assert_eq!(v[3], 10);
808 }
809 }
810
811 #[test]
812 fn test_binning_2x2() {
813 let arr = make_4x4_u8();
814 let mut config = ROIConfig::default();
815 config.dims[0] = ROIDimConfig {
816 min: 0,
817 size: 4,
818 bin: 2,
819 reverse: false,
820 enable: true,
821 auto_size: false,
822 };
823 config.dims[1] = ROIDimConfig {
824 min: 0,
825 size: 4,
826 bin: 2,
827 reverse: false,
828 enable: true,
829 auto_size: false,
830 };
831
832 let roi = extract_roi_2d(&arr, &config).unwrap();
833 assert_eq!(roi.dims[0].size, 2);
834 assert_eq!(roi.dims[1].size, 2);
835 if let NDDataBuffer::U8(ref v) = roi.data {
836 assert_eq!(v[0], 10);
838 }
839 }
840
841 #[test]
842 fn test_reverse() {
843 let arr = make_4x4_u8();
844 let mut config = ROIConfig::default();
845 config.dims[0] = ROIDimConfig {
846 min: 0,
847 size: 4,
848 bin: 1,
849 reverse: true,
850 enable: true,
851 auto_size: false,
852 };
853 config.dims[1] = ROIDimConfig {
854 min: 0,
855 size: 1,
856 bin: 1,
857 reverse: false,
858 enable: true,
859 auto_size: false,
860 };
861
862 let roi = extract_roi_2d(&arr, &config).unwrap();
863 if let NDDataBuffer::U8(ref v) = roi.data {
864 assert_eq!(v[0], 3);
865 assert_eq!(v[1], 2);
866 assert_eq!(v[2], 1);
867 assert_eq!(v[3], 0);
868 }
869 }
870
871 #[test]
872 fn test_collapse_dims() {
873 let arr = make_4x4_u8();
874 let mut config = ROIConfig::default();
875 config.dims[0] = ROIDimConfig {
876 min: 0,
877 size: 4,
878 bin: 1,
879 reverse: false,
880 enable: true,
881 auto_size: false,
882 };
883 config.dims[1] = ROIDimConfig {
884 min: 0,
885 size: 1,
886 bin: 1,
887 reverse: false,
888 enable: true,
889 auto_size: false,
890 };
891 config.collapse_dims = true;
892
893 let roi = extract_roi_2d(&arr, &config).unwrap();
894 assert_eq!(roi.dims.len(), 1);
895 assert_eq!(roi.dims[0].size, 4);
896 }
897
898 #[test]
899 fn test_scale() {
900 let arr = make_4x4_u8();
901 let mut config = ROIConfig::default();
902 config.dims[0] = ROIDimConfig {
903 min: 0,
904 size: 2,
905 bin: 1,
906 reverse: false,
907 enable: true,
908 auto_size: false,
909 };
910 config.dims[1] = ROIDimConfig {
911 min: 0,
912 size: 1,
913 bin: 1,
914 reverse: false,
915 enable: true,
916 auto_size: false,
917 };
918 config.enable_scale = true;
919 config.scale = 2.0;
920
921 let roi = extract_roi_2d(&arr, &config).unwrap();
922 if let NDDataBuffer::U8(ref v) = roi.data {
923 assert_eq!(v[0], 0); assert_eq!(v[1], 0); }
927 }
928
929 #[test]
930 fn test_type_convert() {
931 let arr = make_4x4_u8();
932 let mut config = ROIConfig::default();
933 config.dims[0] = ROIDimConfig {
934 min: 0,
935 size: 2,
936 bin: 1,
937 reverse: false,
938 enable: true,
939 auto_size: false,
940 };
941 config.dims[1] = ROIDimConfig {
942 min: 0,
943 size: 1,
944 bin: 1,
945 reverse: false,
946 enable: true,
947 auto_size: false,
948 };
949 config.data_type = Some(NDDataType::UInt16);
950
951 let roi = extract_roi_2d(&arr, &config).unwrap();
952 assert_eq!(roi.data.data_type(), NDDataType::UInt16);
953 }
954
955 #[test]
958 fn test_roi_processor() {
959 let mut config = ROIConfig::default();
960 config.dims[0] = ROIDimConfig {
961 min: 1,
962 size: 2,
963 bin: 1,
964 reverse: false,
965 enable: true,
966 auto_size: false,
967 };
968 config.dims[1] = ROIDimConfig {
969 min: 1,
970 size: 2,
971 bin: 1,
972 reverse: false,
973 enable: true,
974 auto_size: false,
975 };
976
977 let mut proc = ROIProcessor::new(config);
978 let pool = NDArrayPool::new(1_000_000);
979
980 let arr = make_4x4_u8();
981 let result = proc.process_array(&arr, &pool);
982 assert_eq!(result.output_arrays.len(), 1);
983 assert_eq!(result.output_arrays[0].dims[0].size, 2);
984 assert_eq!(result.output_arrays[0].dims[1].size, 2);
985 }
986
987 #[test]
990 fn test_auto_size() {
991 let arr = make_4x4_u8();
993 let mut config = ROIConfig::default();
994 config.dims[0] = ROIDimConfig {
995 min: 1,
996 size: 0,
997 bin: 1,
998 reverse: false,
999 enable: true,
1000 auto_size: true,
1001 };
1002 config.dims[1] = ROIDimConfig {
1003 min: 0,
1004 size: 0,
1005 bin: 1,
1006 reverse: false,
1007 enable: true,
1008 auto_size: true,
1009 };
1010
1011 let roi = extract_roi_2d(&arr, &config).unwrap();
1012 assert_eq!(roi.dims[0].size, 3);
1016 assert_eq!(roi.dims[1].size, 4);
1017 }
1018
1019 #[test]
1020 fn test_dim_disable() {
1021 let arr = make_4x4_u8();
1023 let mut config = ROIConfig::default();
1024 config.dims[0] = ROIDimConfig {
1025 min: 2,
1026 size: 1,
1027 bin: 1,
1028 reverse: false,
1029 enable: false,
1030 auto_size: false,
1031 };
1032 config.dims[1] = ROIDimConfig {
1033 min: 0,
1034 size: 4,
1035 bin: 1,
1036 reverse: false,
1037 enable: true,
1038 auto_size: false,
1039 };
1040
1041 let roi = extract_roi_2d(&arr, &config).unwrap();
1042 assert_eq!(roi.dims[0].size, 4);
1044 assert_eq!(roi.dims[1].size, 4);
1045 }
1046
1047 #[test]
1048 fn test_autocenter_peak() {
1049 let mut arr = NDArray::new(
1051 vec![NDDimension::new(8), NDDimension::new(8)],
1052 NDDataType::UInt8,
1053 );
1054 if let NDDataBuffer::U8(ref mut v) = arr.data {
1055 for i in 0..64 {
1056 v[i] = 1;
1057 }
1058 v[5 * 8 + 6] = 255;
1060 }
1061
1062 let mut config = ROIConfig::default();
1063 config.dims[0] = ROIDimConfig {
1064 min: 0,
1065 size: 4,
1066 bin: 1,
1067 reverse: false,
1068 enable: true,
1069 auto_size: false,
1070 };
1071 config.dims[1] = ROIDimConfig {
1072 min: 0,
1073 size: 4,
1074 bin: 1,
1075 reverse: false,
1076 enable: true,
1077 auto_size: false,
1078 };
1079 config.autocenter = AutoCenter::PeakPosition;
1080
1081 let roi = extract_roi_2d(&arr, &config).unwrap();
1082 assert_eq!(roi.dims[0].size, 4);
1083 assert_eq!(roi.dims[1].size, 4);
1084
1085 if let NDDataBuffer::U8(ref v) = roi.data {
1091 assert_eq!(v[2 * 4 + 2], 255); }
1093 }
1094
1095 #[test]
1096 fn test_offset_clamp_to_last_column() {
1097 let arr = make_4x4_u8();
1101 let mut config = ROIConfig::default();
1102 config.dims[0] = ROIDimConfig {
1104 min: 4,
1105 size: 10,
1106 bin: 1,
1107 reverse: false,
1108 enable: true,
1109 auto_size: false,
1110 };
1111 config.dims[1] = ROIDimConfig {
1112 min: 0,
1113 size: 1,
1114 bin: 1,
1115 reverse: false,
1116 enable: true,
1117 auto_size: false,
1118 };
1119 let roi = extract_roi_2d(&arr, &config).unwrap();
1120 assert_eq!(roi.dims[0].size, 1);
1122 if let NDDataBuffer::U8(ref v) = roi.data {
1123 assert_eq!(v[0], 3); }
1125 }
1126
1127 #[test]
1128 fn test_bin_larger_than_roi_clamps() {
1129 let arr = make_4x4_u8();
1133 let mut config = ROIConfig::default();
1134 config.dims[0] = ROIDimConfig {
1135 min: 0,
1136 size: 2,
1137 bin: 99, reverse: false,
1139 enable: true,
1140 auto_size: false,
1141 };
1142 config.dims[1] = ROIDimConfig {
1143 min: 0,
1144 size: 1,
1145 bin: 1,
1146 reverse: false,
1147 enable: true,
1148 auto_size: false,
1149 };
1150 let roi = extract_roi_2d(&arr, &config).unwrap();
1151 assert_eq!(roi.dims[0].size, 1);
1153 if let NDDataBuffer::U8(ref v) = roi.data {
1154 assert_eq!(v[0], 1);
1156 }
1157 }
1158
1159 fn make_rgb1_2x2() -> NDArray {
1161 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
1162 let mut arr = NDArray::new(
1163 vec![
1164 NDDimension::new(3),
1165 NDDimension::new(2),
1166 NDDimension::new(2),
1167 ],
1168 NDDataType::UInt8,
1169 );
1170 arr.attributes.add(NDAttribute::new_static(
1171 "ColorMode",
1172 "",
1173 NDAttrSource::Driver,
1174 NDAttrValue::Int32(ad_core_rs::color::NDColorMode::RGB1 as i32),
1175 ));
1176 if let NDDataBuffer::U8(ref mut v) = arr.data {
1177 for y in 0..2 {
1178 for x in 0..2 {
1179 for c in 0..3 {
1180 v[y * 6 + x * 3 + c] = (100 * y + 10 * x + c) as u8;
1181 }
1182 }
1183 }
1184 }
1185 arr
1186 }
1187
1188 #[test]
1189 fn test_roi_3d_rgb1_x_subregion() {
1190 let arr = make_rgb1_2x2();
1192 let mut config = ROIConfig::default();
1193 config.dims[0] = ROIDimConfig {
1195 min: 1,
1196 size: 1,
1197 bin: 1,
1198 reverse: false,
1199 enable: true,
1200 auto_size: false,
1201 };
1202 config.dims[1] = ROIDimConfig {
1203 min: 0,
1204 size: 2,
1205 bin: 1,
1206 reverse: false,
1207 enable: true,
1208 auto_size: false,
1209 };
1210 config.dims[2] = ROIDimConfig {
1211 min: 0,
1212 size: 3,
1213 bin: 1,
1214 reverse: false,
1215 enable: true,
1216 auto_size: false,
1217 };
1218 let roi = extract_roi(&arr, &config).unwrap();
1219 assert_eq!(roi.dims[0].size, 3);
1221 assert_eq!(roi.dims[1].size, 1);
1222 assert_eq!(roi.dims[2].size, 2);
1223 if let NDDataBuffer::U8(ref v) = roi.data {
1224 assert_eq!(&v[0..3], &[10, 11, 12]);
1226 assert_eq!(&v[3..6], &[110, 111, 112]);
1228 } else {
1229 panic!("not u8");
1230 }
1231 }
1232
1233 #[test]
1234 fn test_adp18_3d_rgb_honors_output_data_type() {
1235 let arr = make_rgb1_2x2();
1239 let mut config = ROIConfig::default();
1240 config.dims[0] = ROIDimConfig {
1241 min: 0,
1242 size: 2,
1243 bin: 1,
1244 reverse: false,
1245 enable: true,
1246 auto_size: false,
1247 };
1248 config.dims[1] = ROIDimConfig {
1249 min: 0,
1250 size: 2,
1251 bin: 1,
1252 reverse: false,
1253 enable: true,
1254 auto_size: false,
1255 };
1256 config.dims[2] = ROIDimConfig {
1257 min: 0,
1258 size: 3,
1259 bin: 1,
1260 reverse: false,
1261 enable: true,
1262 auto_size: false,
1263 };
1264 config.data_type = Some(NDDataType::UInt16);
1265
1266 let roi = extract_roi(&arr, &config).unwrap();
1267 assert_eq!(roi.data.data_type(), NDDataType::UInt16);
1268 assert_eq!(roi.dims[0].size, 3);
1270 assert_eq!(roi.dims[1].size, 2);
1271 assert_eq!(roi.dims[2].size, 2);
1272 if let NDDataBuffer::U16(ref v) = roi.data {
1274 assert_eq!(&v[0..3], &[0, 1, 2]);
1275 } else {
1276 panic!("not u16");
1277 }
1278 }
1279
1280 #[test]
1281 fn test_adp19_single_color_collapses_to_2d_mono() {
1282 use ad_core_rs::color::NDColorMode;
1283 let arr = make_rgb1_2x2();
1288 let mut config = ROIConfig::default();
1289 config.dims[0] = ROIDimConfig {
1290 min: 0,
1291 size: 2,
1292 bin: 1,
1293 reverse: false,
1294 enable: true,
1295 auto_size: false,
1296 };
1297 config.dims[1] = ROIDimConfig {
1298 min: 0,
1299 size: 2,
1300 bin: 1,
1301 reverse: false,
1302 enable: true,
1303 auto_size: false,
1304 };
1305 config.dims[2] = ROIDimConfig {
1306 min: 1,
1307 size: 1,
1308 bin: 1,
1309 reverse: false,
1310 enable: true,
1311 auto_size: false,
1312 };
1313
1314 let roi = extract_roi(&arr, &config).unwrap();
1315 assert_eq!(roi.dims.len(), 2);
1317 assert_eq!(roi.dims[0].size, 2);
1318 assert_eq!(roi.dims[1].size, 2);
1319 let cm = roi
1321 .attributes
1322 .get("ColorMode")
1323 .and_then(|a| a.value.as_i64())
1324 .unwrap();
1325 assert_eq!(cm, NDColorMode::Mono as i64);
1326 if let NDDataBuffer::U8(ref v) = roi.data {
1328 assert_eq!(v, &[1, 11, 101, 111]);
1329 } else {
1330 panic!("not u8");
1331 }
1332 }
1333}