use std::sync::Arc;
use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
use ad_core_rs::ndarray_pool::NDArrayPool;
use ad_core_rs::plugin::runtime::{
NDPluginProcess, ParamUpdate, PluginParamSnapshot, ProcessResult,
};
use asyn_rs::param::ParamType;
use asyn_rs::port::PortDriverBase;
#[derive(Debug, Clone)]
pub struct ROIDimConfig {
pub min: usize,
pub size: usize,
pub bin: usize,
pub reverse: bool,
pub enable: bool,
pub auto_size: bool,
}
impl Default for ROIDimConfig {
fn default() -> Self {
Self {
min: 0,
size: 0,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutoCenter {
None,
CenterOfMass,
PeakPosition,
}
#[derive(Debug, Clone)]
pub struct ROIConfig {
pub dims: [ROIDimConfig; 3],
pub data_type: Option<NDDataType>,
pub enable_scale: bool,
pub scale: f64,
pub collapse_dims: bool,
pub autocenter: AutoCenter,
}
impl Default for ROIConfig {
fn default() -> Self {
Self {
dims: [
ROIDimConfig::default(),
ROIDimConfig::default(),
ROIDimConfig::default(),
],
data_type: None,
enable_scale: false,
scale: 1.0,
collapse_dims: false,
autocenter: AutoCenter::None,
}
}
}
fn find_centroid_2d(data: &NDDataBuffer, x_size: usize, y_size: usize) -> (usize, usize) {
let mut cx = 0.0f64;
let mut cy = 0.0f64;
let mut total = 0.0f64;
for iy in 0..y_size {
for ix in 0..x_size {
let val = data.get_as_f64(iy * x_size + ix).unwrap_or(0.0);
total += val;
cx += val * ix as f64;
cy += val * iy as f64;
}
}
if total > 0.0 {
((cx / total) as usize, (cy / total) as usize)
} else {
(x_size / 2, y_size / 2)
}
}
fn find_peak_2d(data: &NDDataBuffer, x_size: usize, y_size: usize) -> (usize, usize) {
let mut max_val = f64::NEG_INFINITY;
let mut max_x = 0;
let mut max_y = 0;
for iy in 0..y_size {
for ix in 0..x_size {
let val = data.get_as_f64(iy * x_size + ix).unwrap_or(0.0);
if val > max_val {
max_val = val;
max_x = ix;
max_y = iy;
}
}
}
(max_x, max_y)
}
pub fn extract_roi(src: &NDArray, config: &ROIConfig) -> Option<NDArray> {
use ad_core_rs::color::NDColorMode;
if src.dims.len() >= 3 {
let info = src.info();
if matches!(
info.color_mode,
NDColorMode::RGB1 | NDColorMode::RGB2 | NDColorMode::RGB3
) {
return extract_roi_3d(src, config);
}
}
extract_roi_2d(src, config)
}
fn resolve_axis(cfg: &ROIDimConfig, dim_size: usize) -> (usize, usize, usize) {
if !cfg.enable || dim_size == 0 {
return (0, dim_size, 1);
}
let offset = cfg.min.min(dim_size - 1);
let size = if cfg.auto_size { dim_size } else { cfg.size };
let size = size.max(1).min(dim_size - offset);
let binning = cfg.bin.max(1).min(size);
(offset, size, binning)
}
fn convert_roi(src: &NDArray, dims_out: &[NDDimension], config: &ROIConfig) -> Option<NDArray> {
use ad_core_rs::convert::{convert_dims, convert_type};
let target_type = config.data_type.unwrap_or(src.data.data_type());
let scaled = config.enable_scale && config.scale != 0.0 && config.scale != 1.0;
let result = if scaled {
convert_dims(src, dims_out, NDDataType::Float64).and_then(|mut scratch| {
if let NDDataBuffer::F64(v) = &mut scratch.data {
for x in v.iter_mut() {
*x /= config.scale;
}
}
convert_type(&scratch, target_type)
})
} else {
convert_dims(src, dims_out, target_type)
};
match result {
Ok(arr) => Some(arr),
Err(e) => {
tracing::warn!(
error = %e,
from = ?src.data.data_type(),
to = ?target_type,
"ROI extraction failed; dropping frame"
);
None
}
}
}
pub fn extract_roi_3d(src: &NDArray, config: &ROIConfig) -> Option<NDArray> {
let info = src.info();
let (src_x, src_y, src_c) = (info.x_size, info.y_size, info.color_size.max(1));
if src_x == 0 || src_y == 0 || src_c == 0 {
return None;
}
let (x_min, x_roi, bin_x) = resolve_axis(&config.dims[0], src_x);
let (y_min, y_roi, bin_y) = resolve_axis(&config.dims[1], src_y);
let (c_min, c_roi, bin_c) = resolve_axis(&config.dims[2], src_c);
let (out_x, out_y, out_c) = (x_roi / bin_x, y_roi / bin_y, c_roi / bin_c);
if out_x == 0 || out_y == 0 || out_c == 0 {
return None;
}
let user_dims = info.user_dims();
let mut dims_out: Vec<NDDimension> = src.dims.clone();
for d in dims_out.iter_mut() {
d.offset = 0;
d.binning = 1;
d.reverse = false;
}
for (roi_dim, (min, size, bin)) in [
(x_min, x_roi, bin_x),
(y_min, y_roi, bin_y),
(c_min, c_roi, bin_c),
]
.into_iter()
.enumerate()
{
let d = dims_out.get_mut(user_dims[roi_dim])?;
d.offset = min;
d.size = size;
d.binning = bin;
d.reverse = config.dims[roi_dim].reverse;
}
let mut arr = convert_roi(src, &dims_out, config)?;
let single_color = out_c == 1
&& matches!(
info.color_mode,
ad_core_rs::color::NDColorMode::RGB1
| ad_core_rs::color::NDColorMode::RGB2
| ad_core_rs::color::NDColorMode::RGB3
);
if single_color || config.collapse_dims {
let collapsed: Vec<NDDimension> = arr.dims.iter().filter(|d| d.size > 1).cloned().collect();
arr.dims = if collapsed.is_empty() {
vec![NDDimension::new(1)]
} else {
collapsed
};
}
arr.unique_id = src.unique_id;
if single_color {
use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
arr.attributes.add(NDAttribute::new_static(
"ColorMode",
"Color mode",
NDAttrSource::Driver,
NDAttrValue::Int32(ad_core_rs::color::NDColorMode::Mono as i32),
));
}
Some(arr)
}
pub fn extract_roi_2d(src: &NDArray, config: &ROIConfig) -> Option<NDArray> {
if src.dims.len() < 2 {
return None;
}
let src_x = src.dims[0].size;
let src_y = src.dims[1].size;
let (eff_x_min, eff_x_size, bin_x) = resolve_axis(&config.dims[0], src_x);
let (eff_y_min, eff_y_size, bin_y) = resolve_axis(&config.dims[1], src_y);
let (roi_x_min, roi_y_min) = match config.autocenter {
AutoCenter::None => (eff_x_min, eff_y_min),
AutoCenter::CenterOfMass => {
let (cx, cy) = find_centroid_2d(&src.data, src_x, src_y);
let mx = cx
.saturating_sub(eff_x_size / 2)
.min(src_x.saturating_sub(eff_x_size));
let my = cy
.saturating_sub(eff_y_size / 2)
.min(src_y.saturating_sub(eff_y_size));
(mx, my)
}
AutoCenter::PeakPosition => {
let (px, py) = find_peak_2d(&src.data, src_x, src_y);
let mx = px
.saturating_sub(eff_x_size / 2)
.min(src_x.saturating_sub(eff_x_size));
let my = py
.saturating_sub(eff_y_size / 2)
.min(src_y.saturating_sub(eff_y_size));
(mx, my)
}
};
if eff_x_size == 0 || eff_y_size == 0 {
return None;
}
if eff_x_size / bin_x == 0 || eff_y_size / bin_y == 0 {
return None;
}
let mut dims_out: Vec<NDDimension> = src.dims.clone();
for d in dims_out.iter_mut() {
d.offset = 0;
d.binning = 1;
d.reverse = false;
}
dims_out[0] = NDDimension {
size: eff_x_size,
offset: roi_x_min,
binning: bin_x,
reverse: config.dims[0].reverse,
};
dims_out[1] = NDDimension {
size: eff_y_size,
offset: roi_y_min,
binning: bin_y,
reverse: config.dims[1].reverse,
};
let mut arr = convert_roi(src, &dims_out, config)?;
if config.collapse_dims {
let collapsed: Vec<NDDimension> = arr.dims.iter().filter(|d| d.size > 1).cloned().collect();
arr.dims = if collapsed.is_empty() {
vec![NDDimension::new(arr.dims[0].size)]
} else {
collapsed
};
}
arr.unique_id = src.unique_id;
Some(arr)
}
#[derive(Default, Clone, Copy)]
pub struct ROIDimParams {
pub min: usize,
pub size: usize,
pub bin: usize,
pub reverse: usize,
pub enable: usize,
pub auto_size: usize,
pub max_size: usize,
}
#[derive(Default)]
pub struct ROIParams {
pub dims: [ROIDimParams; 3],
pub enable_scale: usize,
pub scale: usize,
pub data_type: usize,
pub collapse_dims: usize,
pub name: usize,
}
pub struct ROIProcessor {
config: ROIConfig,
params: ROIParams,
}
impl ROIProcessor {
pub fn new(config: ROIConfig) -> Self {
Self {
config,
params: ROIParams::default(),
}
}
pub fn params(&self) -> &ROIParams {
&self.params
}
}
impl NDPluginProcess for ROIProcessor {
fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
let user_dims = array.info().user_dims();
let mut updates = Vec::new();
for (i, dim_params) in self.params.dims.iter().enumerate() {
let dim_size = if i < array.dims.len() {
array.dims[user_dims[i]].size as i32
} else {
0
};
updates.push(ParamUpdate::int32(dim_params.max_size, dim_size));
}
match extract_roi(array, &self.config) {
Some(roi_arr) => ProcessResult {
output_arrays: vec![Arc::new(roi_arr)],
param_updates: updates,
scatter: false,
},
None => ProcessResult::sink(updates),
}
}
fn plugin_type(&self) -> &str {
"NDPluginROI"
}
fn register_params(
&mut self,
base: &mut PortDriverBase,
) -> Result<(), asyn_rs::error::AsynError> {
let dim_names = ["DIM0", "DIM1", "DIM2"];
for (i, prefix) in dim_names.iter().enumerate() {
self.params.dims[i].min =
base.create_param(&format!("{prefix}_MIN"), ParamType::Int32)?;
self.params.dims[i].size =
base.create_param(&format!("{prefix}_SIZE"), ParamType::Int32)?;
self.params.dims[i].bin =
base.create_param(&format!("{prefix}_BIN"), ParamType::Int32)?;
self.params.dims[i].reverse =
base.create_param(&format!("{prefix}_REVERSE"), ParamType::Int32)?;
self.params.dims[i].enable =
base.create_param(&format!("{prefix}_ENABLE"), ParamType::Int32)?;
self.params.dims[i].auto_size =
base.create_param(&format!("{prefix}_AUTO_SIZE"), ParamType::Int32)?;
self.params.dims[i].max_size =
base.create_param(&format!("{prefix}_MAX_SIZE"), ParamType::Int32)?;
base.set_int32_param(self.params.dims[i].min, 0, self.config.dims[i].min as i32)?;
base.set_int32_param(self.params.dims[i].size, 0, self.config.dims[i].size as i32)?;
base.set_int32_param(self.params.dims[i].bin, 0, self.config.dims[i].bin as i32)?;
base.set_int32_param(
self.params.dims[i].reverse,
0,
self.config.dims[i].reverse as i32,
)?;
base.set_int32_param(
self.params.dims[i].enable,
0,
self.config.dims[i].enable as i32,
)?;
base.set_int32_param(
self.params.dims[i].auto_size,
0,
self.config.dims[i].auto_size as i32,
)?;
}
self.params.enable_scale = base.create_param("ENABLE_SCALE", ParamType::Int32)?;
self.params.scale = base.create_param("SCALE_VALUE", ParamType::Float64)?;
self.params.data_type = base.create_param("ROI_DATA_TYPE", ParamType::Int32)?;
self.params.collapse_dims = base.create_param("COLLAPSE_DIMS", ParamType::Int32)?;
self.params.name = base.create_param("NAME", ParamType::Octet)?;
base.set_int32_param(self.params.enable_scale, 0, self.config.enable_scale as i32)?;
base.set_float64_param(self.params.scale, 0, self.config.scale)?;
base.set_int32_param(self.params.data_type, 0, -1)?; base.set_int32_param(
self.params.collapse_dims,
0,
self.config.collapse_dims as i32,
)?;
Ok(())
}
fn on_param_change(
&mut self,
reason: usize,
snapshot: &PluginParamSnapshot,
) -> ad_core_rs::plugin::runtime::ParamChangeResult {
let p = &self.params;
for i in 0..3 {
if reason == p.dims[i].min {
self.config.dims[i].min = snapshot.value.as_i32().max(0) as usize;
return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
}
if reason == p.dims[i].size {
self.config.dims[i].size = snapshot.value.as_i32().max(0) as usize;
return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
}
if reason == p.dims[i].bin {
self.config.dims[i].bin = snapshot.value.as_i32().max(1) as usize;
return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
}
if reason == p.dims[i].reverse {
self.config.dims[i].reverse = snapshot.value.as_i32() != 0;
return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
}
if reason == p.dims[i].enable {
self.config.dims[i].enable = snapshot.value.as_i32() != 0;
return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
}
if reason == p.dims[i].auto_size {
self.config.dims[i].auto_size = snapshot.value.as_i32() != 0;
return ad_core_rs::plugin::runtime::ParamChangeResult::empty();
}
}
if reason == p.enable_scale {
self.config.enable_scale = snapshot.value.as_i32() != 0;
} else if reason == p.scale {
self.config.scale = snapshot.value.as_f64();
} else if reason == p.data_type {
let v = snapshot.value.as_i32();
self.config.data_type = if v < 0 {
None
} else {
NDDataType::from_ordinal(v as u8)
};
} else if reason == p.collapse_dims {
self.config.collapse_dims = snapshot.value.as_i32() != 0;
}
ad_core_rs::plugin::runtime::ParamChangeResult::empty()
}
}
pub fn create_roi_runtime(
port_name: &str,
pool: Arc<NDArrayPool>,
queue_size: usize,
ndarray_port: &str,
wiring: Arc<ad_core_rs::plugin::wiring::WiringRegistry>,
) -> (
ad_core_rs::plugin::runtime::PluginRuntimeHandle,
ROIParams,
std::thread::JoinHandle<()>,
) {
let processor = ROIProcessor::new(ROIConfig::default());
let (handle, jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
port_name,
processor,
pool,
queue_size,
ndarray_port,
wiring,
);
let params = {
let mut base =
asyn_rs::port::PortDriverBase::new("_scratch_", 1, asyn_rs::port::PortFlags::default());
let _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
let mut p = ROIParams::default();
let dim_names = ["DIM0", "DIM1", "DIM2"];
for (i, prefix) in dim_names.iter().enumerate() {
p.dims[i].min = base
.create_param(&format!("{prefix}_MIN"), asyn_rs::param::ParamType::Int32)
.unwrap();
p.dims[i].size = base
.create_param(&format!("{prefix}_SIZE"), asyn_rs::param::ParamType::Int32)
.unwrap();
p.dims[i].bin = base
.create_param(&format!("{prefix}_BIN"), asyn_rs::param::ParamType::Int32)
.unwrap();
p.dims[i].reverse = base
.create_param(
&format!("{prefix}_REVERSE"),
asyn_rs::param::ParamType::Int32,
)
.unwrap();
p.dims[i].enable = base
.create_param(
&format!("{prefix}_ENABLE"),
asyn_rs::param::ParamType::Int32,
)
.unwrap();
p.dims[i].auto_size = base
.create_param(
&format!("{prefix}_AUTO_SIZE"),
asyn_rs::param::ParamType::Int32,
)
.unwrap();
p.dims[i].max_size = base
.create_param(
&format!("{prefix}_MAX_SIZE"),
asyn_rs::param::ParamType::Int32,
)
.unwrap();
}
p.enable_scale = base
.create_param("ENABLE_SCALE", asyn_rs::param::ParamType::Int32)
.unwrap();
p.scale = base
.create_param("SCALE_VALUE", asyn_rs::param::ParamType::Float64)
.unwrap();
p.data_type = base
.create_param("ROI_DATA_TYPE", asyn_rs::param::ParamType::Int32)
.unwrap();
p.collapse_dims = base
.create_param("COLLAPSE_DIMS", asyn_rs::param::ParamType::Int32)
.unwrap();
p.name = base
.create_param("NAME", asyn_rs::param::ParamType::Octet)
.unwrap();
p
};
(handle, params, jh)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_4x4_u8() -> NDArray {
let mut arr = NDArray::new(
vec![NDDimension::new(4), NDDimension::new(4)],
NDDataType::UInt8,
);
if let NDDataBuffer::U8(ref mut v) = arr.data {
for i in 0..16 {
v[i] = i as u8;
}
}
arr
}
#[test]
fn test_r9_66_disabled_dimension_is_not_binned() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 2,
size: 1,
bin: 2,
reverse: false,
enable: false,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 4,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 4, "disabled axis keeps its full size");
assert_eq!(roi.dims[1].size, 4);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(&v[0..4], &[0, 1, 2, 3], "disabled axis must not bin-sum");
} else {
panic!("expected U8");
}
config.dims[0].enable = true;
config.dims[0].min = 0;
config.dims[0].auto_size = true; let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(
roi.dims[0].size, 2,
"enabled axis with bin=2 halves the axis"
);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(&v[0..2], &[1, 5], "enabled axis bin-sums");
} else {
panic!("expected U8");
}
}
#[test]
fn test_r9_66_disabled_color_axis_is_not_binned() {
use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
use ad_core_rs::color::NDColorMode;
let mut arr = NDArray::new(
vec![
NDDimension::new(3),
NDDimension::new(4),
NDDimension::new(2),
],
NDDataType::UInt8,
);
arr.attributes.add(NDAttribute::new_static(
"ColorMode",
"",
NDAttrSource::Driver,
NDAttrValue::Int32(NDColorMode::RGB1 as i32),
));
let mut config = ROIConfig::default();
config.dims[2] = ROIDimConfig {
min: 0,
size: 3,
bin: 3,
reverse: false,
enable: false,
auto_size: false,
};
for i in 0..2 {
config.dims[i] = ROIDimConfig {
min: 0,
size: 0,
bin: 1,
reverse: false,
enable: true,
auto_size: true,
};
}
let roi = extract_roi_3d(&arr, &config).unwrap();
assert_eq!(
roi.dims[0].size, 3,
"disabled colour axis keeps all 3 planes"
);
assert_eq!(roi.dims[1].size, 4);
assert_eq!(roi.dims[2].size, 2);
}
#[test]
fn test_extract_sub_region() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 1,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 1,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 2);
assert_eq!(roi.dims[1].size, 2);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 5);
assert_eq!(v[1], 6);
assert_eq!(v[2], 9);
assert_eq!(v[3], 10);
}
}
#[test]
fn test_binning_2x2() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 4,
bin: 2,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 4,
bin: 2,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 2);
assert_eq!(roi.dims[1].size, 2);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 10);
}
}
#[test]
fn test_reverse() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 4,
bin: 1,
reverse: true,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 3);
assert_eq!(v[1], 2);
assert_eq!(v[2], 1);
assert_eq!(v[3], 0);
}
}
#[test]
fn test_collapse_dims() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 4,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.collapse_dims = true;
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims.len(), 1);
assert_eq!(roi.dims[0].size, 4);
}
#[test]
fn test_scale() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.enable_scale = true;
config.scale = 2.0;
let roi = extract_roi_2d(&arr, &config).unwrap();
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 0); assert_eq!(v[1], 0); }
}
#[test]
fn test_type_convert() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.data_type = Some(NDDataType::UInt16);
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.data.data_type(), NDDataType::UInt16);
}
#[test]
fn test_roi_processor() {
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 1,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 1,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let mut proc = ROIProcessor::new(config);
let pool = NDArrayPool::new(1_000_000);
let arr = make_4x4_u8();
let result = proc.process_array(&arr, &pool);
assert_eq!(result.output_arrays.len(), 1);
assert_eq!(result.output_arrays[0].dims[0].size, 2);
assert_eq!(result.output_arrays[0].dims[1].size, 2);
}
#[test]
fn test_r9_67_max_size_uses_the_user_dims_mapping() {
use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
use ad_core_rs::color::NDColorMode;
use asyn_rs::port::{PortDriverBase, PortFlags};
let mut arr = NDArray::new(
vec![
NDDimension::new(3),
NDDimension::new(8),
NDDimension::new(5),
],
NDDataType::UInt8,
);
arr.attributes.add(NDAttribute::new_static(
"ColorMode",
"",
NDAttrSource::Driver,
NDAttrValue::Int32(NDColorMode::RGB1 as i32),
));
let mut proc = ROIProcessor::new(ROIConfig::default());
let mut base = PortDriverBase::new("R9_67", 1, PortFlags::default());
proc.register_params(&mut base).unwrap();
let reasons = [
proc.params().dims[0].max_size,
proc.params().dims[1].max_size,
proc.params().dims[2].max_size,
];
let pool = NDArrayPool::new(1_000_000);
let result = proc.process_array(&arr, &pool);
let max_size = |reason: usize| {
result
.param_updates
.iter()
.find_map(|u| match u {
ParamUpdate::Int32 {
reason: r, value, ..
} if *r == reason => Some(*value),
_ => None,
})
.expect("MaxSize update")
};
assert_eq!(
max_size(reasons[0]),
8,
"Dim0MaxSize is the X axis (dims[1])"
);
assert_eq!(
max_size(reasons[1]),
5,
"Dim1MaxSize is the Y axis (dims[2])"
);
assert_eq!(
max_size(reasons[2]),
3,
"Dim2MaxSize is the colour axis (dims[0])"
);
let arr2d = NDArray::new(
vec![NDDimension::new(6), NDDimension::new(4)],
NDDataType::UInt8,
);
let result = proc.process_array(&arr2d, &pool);
let max_size = |reason: usize| {
result
.param_updates
.iter()
.find_map(|u| match u {
ParamUpdate::Int32 {
reason: r, value, ..
} if *r == reason => Some(*value),
_ => None,
})
.expect("MaxSize update")
};
assert_eq!(max_size(reasons[0]), 6);
assert_eq!(max_size(reasons[1]), 4);
assert_eq!(max_size(reasons[2]), 0);
}
#[test]
fn test_auto_size() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 1,
size: 0,
bin: 1,
reverse: false,
enable: true,
auto_size: true,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 0,
bin: 1,
reverse: false,
enable: true,
auto_size: true,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 3);
assert_eq!(roi.dims[1].size, 4);
}
#[test]
fn test_dim_disable() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 2,
size: 1,
bin: 1,
reverse: false,
enable: false,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 4,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 4);
assert_eq!(roi.dims[1].size, 4);
}
#[test]
fn test_autocenter_peak() {
let mut arr = NDArray::new(
vec![NDDimension::new(8), NDDimension::new(8)],
NDDataType::UInt8,
);
if let NDDataBuffer::U8(ref mut v) = arr.data {
for i in 0..64 {
v[i] = 1;
}
v[5 * 8 + 6] = 255;
}
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 4,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 4,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.autocenter = AutoCenter::PeakPosition;
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 4);
assert_eq!(roi.dims[1].size, 4);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[2 * 4 + 2], 255); }
}
#[test]
fn test_offset_clamp_to_last_column() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 4,
size: 10,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 1);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 3); }
}
#[test]
fn test_bin_larger_than_roi_clamps() {
let arr = make_4x4_u8();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 2,
bin: 99, reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 1);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 1);
}
}
fn make_rgb1_2x2() -> NDArray {
use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
let mut arr = NDArray::new(
vec![
NDDimension::new(3),
NDDimension::new(2),
NDDimension::new(2),
],
NDDataType::UInt8,
);
arr.attributes.add(NDAttribute::new_static(
"ColorMode",
"",
NDAttrSource::Driver,
NDAttrValue::Int32(ad_core_rs::color::NDColorMode::RGB1 as i32),
));
if let NDDataBuffer::U8(ref mut v) = arr.data {
for y in 0..2 {
for x in 0..2 {
for c in 0..3 {
v[y * 6 + x * 3 + c] = (100 * y + 10 * x + c) as u8;
}
}
}
}
arr
}
#[test]
fn test_roi_3d_rgb1_x_subregion() {
let arr = make_rgb1_2x2();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 1,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[2] = ROIDimConfig {
min: 0,
size: 3,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi(&arr, &config).unwrap();
assert_eq!(roi.dims[0].size, 3);
assert_eq!(roi.dims[1].size, 1);
assert_eq!(roi.dims[2].size, 2);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(&v[0..3], &[10, 11, 12]);
assert_eq!(&v[3..6], &[110, 111, 112]);
} else {
panic!("not u8");
}
}
#[test]
fn test_adp18_3d_rgb_honors_output_data_type() {
let arr = make_rgb1_2x2();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[2] = ROIDimConfig {
min: 0,
size: 3,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.data_type = Some(NDDataType::UInt16);
let roi = extract_roi(&arr, &config).unwrap();
assert_eq!(roi.data.data_type(), NDDataType::UInt16);
assert_eq!(roi.dims[0].size, 3);
assert_eq!(roi.dims[1].size, 2);
assert_eq!(roi.dims[2].size, 2);
if let NDDataBuffer::U16(ref v) = roi.data {
assert_eq!(&v[0..3], &[0, 1, 2]);
} else {
panic!("not u16");
}
}
#[test]
fn test_adp19_single_color_collapses_to_2d_mono() {
use ad_core_rs::color::NDColorMode;
let arr = make_rgb1_2x2();
let mut config = ROIConfig::default();
config.dims[0] = ROIDimConfig {
min: 0,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[2] = ROIDimConfig {
min: 1,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi(&arr, &config).unwrap();
assert_eq!(roi.dims.len(), 2);
assert_eq!(roi.dims[0].size, 2);
assert_eq!(roi.dims[1].size, 2);
let cm = roi
.attributes
.get("ColorMode")
.and_then(|a| a.value.as_i64())
.unwrap();
assert_eq!(cm, NDColorMode::Mono as i64);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v, &[1, 11, 101, 111]);
} else {
panic!("not u8");
}
}
#[test]
fn test_bin_sum_wraps_modulo_output_type() {
let mut arr = NDArray::new(
vec![NDDimension::new(3), NDDimension::new(3)],
NDDataType::UInt8,
);
if let NDDataBuffer::U8(ref mut v) = arr.data {
v.iter_mut().for_each(|p| *p = 100);
}
let mut config = ROIConfig {
enable_scale: false,
..Default::default()
};
for d in 0..2 {
config.dims[d] = ROIDimConfig {
min: 0,
size: 3,
bin: 3,
reverse: false,
enable: true,
auto_size: false,
};
}
let roi = extract_roi_2d(&arr, &config).unwrap();
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 132, "900 % 256 == 132 (C wraps), not 255");
} else {
panic!("not u8");
}
}
#[test]
fn test_bin_sum_with_scale_uses_float64_intermediate() {
let mut arr = NDArray::new(
vec![NDDimension::new(3), NDDimension::new(3)],
NDDataType::UInt8,
);
if let NDDataBuffer::U8(ref mut v) = arr.data {
v.iter_mut().for_each(|p| *p = 100);
}
let mut config = ROIConfig {
enable_scale: true,
scale: 9.0,
..Default::default()
};
for d in 0..2 {
config.dims[d] = ROIDimConfig {
min: 0,
size: 3,
bin: 3,
reverse: false,
enable: true,
auto_size: false,
};
}
let roi = extract_roi_2d(&arr, &config).unwrap();
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 100, "900/9 via the Float64 path");
} else {
panic!("not u8");
}
}
#[test]
fn test_narrowing_output_type_wraps() {
let mut arr = NDArray::new(
vec![NDDimension::new(2), NDDimension::new(1)],
NDDataType::UInt16,
);
if let NDDataBuffer::U16(ref mut v) = arr.data {
v[0] = 300;
v[1] = 70;
}
let mut config = ROIConfig {
data_type: Some(NDDataType::UInt8),
..Default::default()
};
config.dims[0] = ROIDimConfig {
min: 0,
size: 2,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
config.dims[1] = ROIDimConfig {
min: 0,
size: 1,
bin: 1,
reverse: false,
enable: true,
auto_size: false,
};
let roi = extract_roi_2d(&arr, &config).unwrap();
assert_eq!(roi.data.data_type(), NDDataType::UInt8);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 44, "(epicsUInt8)300 == 44");
assert_eq!(v[1], 70);
} else {
panic!("not u8");
}
}
#[test]
fn test_bin_sum_accumulates_in_the_output_type() {
let mut arr = NDArray::new(
vec![NDDimension::new(3), NDDimension::new(3)],
NDDataType::UInt8,
);
if let NDDataBuffer::U8(ref mut v) = arr.data {
v.iter_mut().for_each(|p| *p = 100);
}
let mut config = ROIConfig {
data_type: Some(NDDataType::UInt16),
..Default::default()
};
for d in 0..2 {
config.dims[d] = ROIDimConfig {
min: 0,
size: 3,
bin: 3,
reverse: false,
enable: true,
auto_size: false,
};
}
let roi = extract_roi_2d(&arr, &config).unwrap();
if let NDDataBuffer::U16(ref v) = roi.data {
assert_eq!(v[0], 900, "the bin sum accumulates in UInt16");
} else {
panic!("not u16");
}
}
}