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_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) = if !config.dims[0].enable {
(0, src_x)
} else if config.dims[0].auto_size {
let min = config.dims[0].min.min(src_x);
(min, src_x.saturating_sub(min))
} else {
let min = config.dims[0].min.min(src_x);
let size = config.dims[0].size.min(src_x - min);
(min, size)
};
let (eff_y_min, eff_y_size) = if !config.dims[1].enable {
(0, src_y)
} else if config.dims[1].auto_size {
let min = config.dims[1].min.min(src_y);
(min, src_y.saturating_sub(min))
} else {
let min = config.dims[1].min.min(src_y);
let size = config.dims[1].size.min(src_y - min);
(min, size)
};
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)
}
};
let roi_x_size = eff_x_size;
let roi_y_size = eff_y_size;
if roi_x_size == 0 || roi_y_size == 0 {
return None;
}
let bin_x = config.dims[0].bin.max(1);
let bin_y = config.dims[1].bin.max(1);
let out_x = roi_x_size / bin_x;
let out_y = roi_y_size / bin_y;
if out_x == 0 || out_y == 0 {
return None;
}
macro_rules! extract {
($vec:expr, $T:ty, $zero:expr) => {{
let mut out = vec![$zero; out_x * out_y];
for oy in 0..out_y {
for ox in 0..out_x {
let mut sum = 0.0f64;
let mut count = 0usize;
for by in 0..bin_y {
for bx in 0..bin_x {
let sx = roi_x_min + ox * bin_x + bx;
let sy = roi_y_min + oy * bin_y + by;
if sx < src_x && sy < src_y {
sum += $vec[sy * src_x + sx] as f64;
count += 1;
}
}
}
let val = if count > 0 { sum / count as f64 } else { 0.0 };
let idx = if config.dims[0].reverse {
out_x - 1 - ox
} else {
ox
} + if config.dims[1].reverse {
out_y - 1 - oy
} else {
oy
} * out_x;
let scaled = if config.enable_scale {
val * config.scale
} else {
val
};
out[idx] = scaled as $T;
}
}
out
}};
}
let out_data = match &src.data {
NDDataBuffer::U8(v) => NDDataBuffer::U8(extract!(v, u8, 0)),
NDDataBuffer::U16(v) => NDDataBuffer::U16(extract!(v, u16, 0)),
NDDataBuffer::I8(v) => NDDataBuffer::I8(extract!(v, i8, 0)),
NDDataBuffer::I16(v) => NDDataBuffer::I16(extract!(v, i16, 0)),
NDDataBuffer::I32(v) => NDDataBuffer::I32(extract!(v, i32, 0)),
NDDataBuffer::U32(v) => NDDataBuffer::U32(extract!(v, u32, 0)),
NDDataBuffer::I64(v) => NDDataBuffer::I64(extract!(v, i64, 0)),
NDDataBuffer::U64(v) => NDDataBuffer::U64(extract!(v, u64, 0)),
NDDataBuffer::F32(v) => NDDataBuffer::F32(extract!(v, f32, 0.0)),
NDDataBuffer::F64(v) => NDDataBuffer::F64(extract!(v, f64, 0.0)),
};
let out_dims = if config.collapse_dims && out_y == 1 {
vec![NDDimension::new(out_x)]
} else {
vec![NDDimension::new(out_x), NDDimension::new(out_y)]
};
let target_type = config.data_type.unwrap_or(src.data.data_type());
let mut arr = NDArray::new(out_dims, target_type);
if target_type == src.data.data_type() {
arr.data = out_data;
} else {
let mut temp = NDArray::new(arr.dims.clone(), src.data.data_type());
temp.data = out_data;
if let Ok(converted) = ad_core_rs::color::convert_data_type(&temp, target_type) {
arr.data = converted.data;
} else {
arr.data = out_data_fallback(&temp.data, target_type, temp.data.len());
}
}
arr.unique_id = src.unique_id;
arr.timestamp = src.timestamp;
arr.attributes = src.attributes.clone();
Some(arr)
}
fn out_data_fallback(_src: &NDDataBuffer, target: NDDataType, len: usize) -> NDDataBuffer {
NDDataBuffer::zeros(target, len)
}
#[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 mut updates = Vec::new();
for (i, dim_params) in self.params.dims.iter().enumerate() {
let dim_size = array.dims.get(i).map(|d| d.size as i32).unwrap_or(0);
updates.push(ParamUpdate::int32(dim_params.max_size, dim_size));
}
match extract_roi_2d(array, &self.config) {
Some(roi_arr) => ProcessResult {
output_arrays: vec![Arc::new(roi_arr)],
param_updates: updates,
scatter_index: None,
},
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_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], 2);
}
}
#[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], 2); }
}
#[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_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);
if let NDDataBuffer::U8(ref v) = roi.data {
assert_eq!(v[0], 1);
assert_eq!(v[1], 2);
assert_eq!(v[2], 3);
}
}
#[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); }
}
}