use std::sync::Arc;
#[cfg(feature = "parallel")]
use crate::par_util;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType};
use ad_core_rs::ndarray_pool::NDArrayPool;
use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
#[derive(Debug, Clone)]
pub struct FilterConfig {
pub num_filter: usize,
pub auto_reset: bool,
pub filter_callbacks: usize,
pub oc: [f64; 4],
pub fc: [f64; 4],
pub rc: [f64; 2],
pub r_offset: f64,
pub o_offset: f64,
pub o_scale: f64,
pub f_offset: f64,
pub f_scale: f64,
}
impl Default for FilterConfig {
fn default() -> Self {
Self {
num_filter: 1,
auto_reset: false,
filter_callbacks: 0,
oc: [1.0, 0.0, 0.0, 0.0], fc: [1.0, 0.0, 0.0, 0.0],
rc: [1.0, 0.0],
r_offset: 0.0,
o_offset: 0.0,
o_scale: 1.0,
f_offset: 0.0,
f_scale: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessConfig {
pub enable_background: bool,
pub enable_flat_field: bool,
pub enable_offset_scale: bool,
pub offset: f64,
pub scale: f64,
pub enable_low_clip: bool,
pub low_clip_thresh: f64,
pub low_clip_value: f64,
pub enable_high_clip: bool,
pub high_clip_thresh: f64,
pub high_clip_value: f64,
pub scale_flat_field: f64,
pub enable_filter: bool,
pub filter: FilterConfig,
pub output_type: Option<NDDataType>,
pub save_background: bool,
pub save_flat_field: bool,
pub valid_background: bool,
pub valid_flat_field: bool,
}
impl Default for ProcessConfig {
fn default() -> Self {
Self {
enable_background: false,
enable_flat_field: false,
enable_offset_scale: false,
offset: 0.0,
scale: 1.0,
enable_low_clip: false,
low_clip_thresh: 0.0,
low_clip_value: 0.0,
enable_high_clip: false,
high_clip_thresh: 100.0,
high_clip_value: 100.0,
scale_flat_field: 255.0,
enable_filter: false,
filter: FilterConfig::default(),
output_type: None,
save_background: false,
save_flat_field: false,
valid_background: false,
valid_flat_field: false,
}
}
}
pub struct ProcessState {
pub config: ProcessConfig,
pub background: Option<Vec<f64>>,
pub flat_field: Option<Vec<f64>>,
pub filter_state: Option<Vec<f64>>,
pub num_filtered: usize,
}
impl ProcessState {
pub fn new(config: ProcessConfig) -> Self {
Self {
config,
background: None,
flat_field: None,
filter_state: None,
num_filtered: 0,
}
}
pub fn save_background(&mut self, array: &NDArray) {
let n = array.data.len();
let mut bg = vec![0.0f64; n];
for i in 0..n {
bg[i] = array.data.get_as_f64(i).unwrap_or(0.0);
}
self.background = Some(bg);
self.config.valid_background = true;
}
pub fn save_flat_field(&mut self, array: &NDArray) {
let n = array.data.len();
let mut ff = vec![0.0f64; n];
for i in 0..n {
ff[i] = array.data.get_as_f64(i).unwrap_or(0.0);
}
self.flat_field = Some(ff);
self.config.valid_flat_field = true;
}
pub fn auto_offset_scale(&mut self, array: &NDArray) {
let n = array.data.len();
if n == 0 {
return;
}
let mut min_val = f64::MAX;
let mut max_val = f64::MIN;
for i in 0..n {
let v = array.data.get_as_f64(i).unwrap_or(0.0);
if v < min_val {
min_val = v;
}
if v > max_val {
max_val = v;
}
}
let range = max_val - min_val;
if range > 0.0 {
let bytes_per_elem = match self.config.output_type.unwrap_or(array.data.data_type()) {
NDDataType::Int8 | NDDataType::UInt8 => 1,
NDDataType::Int16 | NDDataType::UInt16 => 2,
NDDataType::Int32 | NDDataType::UInt32 => 4,
NDDataType::Int64 | NDDataType::UInt64 => 8,
NDDataType::Float32 => 4,
NDDataType::Float64 => 8,
};
let max_scale = 2.0f64.powi(bytes_per_elem * 8) - 1.0;
self.config.scale = max_scale / range;
self.config.offset = -min_val;
self.config.enable_offset_scale = true;
self.config.enable_low_clip = true;
self.config.low_clip_thresh = 0.0;
self.config.enable_high_clip = true;
self.config.high_clip_thresh = max_scale;
}
}
pub fn apply_filter_type(&mut self, filter_type: i32) {
let fc = &mut self.config.filter;
match filter_type {
0 => {
fc.fc = [1.0, -1.0, 0.0, 1.0];
fc.oc = [1.0, 0.0, 0.0, 0.0];
fc.rc = [0.0, 1.0]; fc.r_offset = 0.0;
fc.f_offset = 0.0;
fc.f_scale = 1.0;
fc.o_offset = 0.0;
fc.o_scale = 1.0;
}
1 => {
fc.fc = [1.0, 0.0, 1.0, 0.0];
fc.oc = [0.0, 1.0, 0.0, 0.0];
fc.rc = [0.0, 1.0]; fc.r_offset = 0.0;
fc.f_offset = 0.0;
fc.f_scale = 1.0;
fc.o_offset = 0.0;
fc.o_scale = 1.0;
}
2 => {
fc.fc = [1.0, 0.0, 1.0, 0.0];
fc.oc = [1.0, 0.0, 0.0, 0.0];
fc.rc = [0.0, 1.0];
fc.r_offset = 0.0;
fc.f_offset = 0.0;
fc.f_scale = 1.0;
fc.o_offset = 0.0;
fc.o_scale = 1.0;
}
3 => {
fc.fc = [0.0, 0.0, 1.0, 0.0];
fc.oc = [-1.0, 0.0, 1.0, 0.0];
fc.rc = [0.0, 1.0];
fc.r_offset = 0.0;
fc.f_offset = 0.0;
fc.f_scale = 1.0;
fc.o_offset = 0.0;
fc.o_scale = 1.0;
}
4 => {
fc.fc = [1.0, -1.0, 0.0, 1.0];
fc.oc = [-1.0, 0.0, 1.0, 0.0];
fc.rc = [0.0, 1.0];
fc.r_offset = 0.0;
fc.f_offset = 0.0;
fc.f_scale = 1.0;
fc.o_offset = 0.0;
fc.o_scale = 1.0;
}
5 => {
fc.fc = [0.0, 0.0, 1.0, 0.0];
fc.oc = [1.0, 0.0, 0.0, 0.0];
fc.rc = [0.0, 1.0];
fc.r_offset = 0.0;
fc.f_offset = 0.0;
fc.f_scale = 1.0;
fc.o_offset = 0.0;
fc.o_scale = 1.0;
}
_ => {} }
}
pub fn reset_filter(&mut self) {
self.filter_state = None;
self.num_filtered = 0;
}
pub fn process(&mut self, src: &NDArray) -> NDArray {
let n = src.data.len();
let mut values = vec![0.0f64; n];
for i in 0..n {
values[i] = src.data.get_as_f64(i).unwrap_or(0.0);
}
if self.config.save_background {
self.save_background(src);
self.config.save_background = false;
}
if self.config.save_flat_field {
self.save_flat_field(src);
self.config.save_flat_field = false;
}
let needs_element_ops = self.config.enable_background
|| self.config.enable_flat_field
|| self.config.enable_offset_scale
|| self.config.enable_low_clip
|| self.config.enable_high_clip;
if needs_element_ops {
let bg = if self.config.enable_background {
self.background.as_ref()
} else {
None
};
let (ff, ff_scale) = if self.config.enable_flat_field {
if let Some(ref ff) = self.flat_field {
let scale = if self.config.scale_flat_field > 0.0 {
self.config.scale_flat_field
} else {
ff.iter().sum::<f64>() / ff.len().max(1) as f64
};
(Some(ff.as_slice()), scale)
} else {
(None, 0.0)
}
} else {
(None, 0.0)
};
let do_offset_scale = self.config.enable_offset_scale;
let scale = self.config.scale;
let offset = self.config.offset;
let do_low_clip = self.config.enable_low_clip;
let low_clip_thresh = self.config.low_clip_thresh;
let low_clip_value = self.config.low_clip_value;
let do_high_clip = self.config.enable_high_clip;
let high_clip_thresh = self.config.high_clip_thresh;
let high_clip_value = self.config.high_clip_value;
let apply_stages = |i: usize, v: &mut f64| {
if let Some(bg) = bg {
if i < bg.len() {
*v -= bg[i];
}
}
if let Some(ff) = ff {
if i < ff.len() && ff[i] != 0.0 {
*v = *v * ff_scale / ff[i];
}
}
if do_offset_scale {
*v = (*v + offset) * scale;
}
if do_low_clip && *v < low_clip_thresh {
*v = low_clip_value;
}
if do_high_clip && *v > high_clip_thresh {
*v = high_clip_value;
}
};
#[cfg(feature = "parallel")]
let use_parallel = par_util::should_parallelize(n);
#[cfg(not(feature = "parallel"))]
let use_parallel = false;
if use_parallel {
#[cfg(feature = "parallel")]
par_util::thread_pool().install(|| {
values.par_iter_mut().enumerate().for_each(|(i, v)| {
apply_stages(i, v);
});
});
} else {
for (i, v) in values.iter_mut().enumerate() {
apply_stages(i, v);
}
}
}
if self.config.enable_filter {
let fc = &self.config.filter;
if let Some(ref f) = self.filter_state {
if f.len() != n {
self.filter_state = None;
}
}
let mut reset_filter = self.filter_state.is_none();
if self.num_filtered >= fc.num_filter && fc.auto_reset {
reset_filter = true;
}
if self.filter_state.is_none() {
self.filter_state = Some(values.clone());
}
let filter = self.filter_state.as_mut().unwrap();
if reset_filter {
let r_offset = fc.r_offset;
let rc1 = fc.rc[0];
let rc2 = fc.rc[1];
for i in 0..n {
let new_filter = r_offset + rc1 * filter[i] + rc2 * values[i];
filter[i] = new_filter;
}
self.num_filtered = 0;
}
if self.num_filtered < fc.num_filter {
self.num_filtered += 1;
}
let nf = self.num_filtered as f64;
let o1 = fc.o_scale * (fc.oc[0] + fc.oc[1] / nf);
let o2 = fc.o_scale * (fc.oc[2] + fc.oc[3] / nf);
let f1 = fc.f_scale * (fc.fc[0] + fc.fc[1] / nf);
let f2 = fc.f_scale * (fc.fc[2] + fc.fc[3] / nf);
let o_offset = fc.o_offset;
let f_offset = fc.f_offset;
for i in 0..n {
let new_data = o_offset + o1 * filter[i] + o2 * values[i];
let new_filter = f_offset + f1 * filter[i] + f2 * values[i];
values[i] = new_data;
filter[i] = new_filter;
}
if fc.filter_callbacks > 0 && self.num_filtered != fc.num_filter {
return src.clone();
}
}
let out_type = self.config.output_type.unwrap_or(src.data.data_type());
let mut out_data = NDDataBuffer::zeros(out_type, n);
for i in 0..n {
out_data.set_from_f64(i, values[i]);
}
let mut arr = NDArray::new(src.dims.clone(), out_type);
arr.data = out_data;
arr.unique_id = src.unique_id;
arr.timestamp = src.timestamp;
arr.attributes = src.attributes.clone();
arr
}
}
#[derive(Default)]
struct ProcParamIndices {
data_type: Option<usize>,
save_background: Option<usize>,
enable_background: Option<usize>,
valid_background: Option<usize>,
save_flat_field: Option<usize>,
enable_flat_field: Option<usize>,
valid_flat_field: Option<usize>,
scale_flat_field: Option<usize>,
enable_offset_scale: Option<usize>,
auto_offset_scale: Option<usize>,
offset: Option<usize>,
scale: Option<usize>,
enable_low_clip: Option<usize>,
low_clip_thresh: Option<usize>,
low_clip_value: Option<usize>,
enable_high_clip: Option<usize>,
high_clip_thresh: Option<usize>,
high_clip_value: Option<usize>,
enable_filter: Option<usize>,
filter_type: Option<usize>,
reset_filter: Option<usize>,
auto_reset_filter: Option<usize>,
filter_callbacks: Option<usize>,
num_filter: Option<usize>,
num_filtered: Option<usize>,
o_offset: Option<usize>,
o_scale: Option<usize>,
oc: [Option<usize>; 4],
f_offset: Option<usize>,
f_scale: Option<usize>,
fc: [Option<usize>; 4],
r_offset: Option<usize>,
rc: [Option<usize>; 2],
}
pub struct ProcessProcessor {
state: ProcessState,
params: ProcParamIndices,
}
impl ProcessProcessor {
pub fn new(config: ProcessConfig) -> Self {
Self {
state: ProcessState::new(config),
params: ProcParamIndices::default(),
}
}
pub fn state(&self) -> &ProcessState {
&self.state
}
pub fn state_mut(&mut self) -> &mut ProcessState {
&mut self.state
}
}
impl NDPluginProcess for ProcessProcessor {
fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
use ad_core_rs::plugin::runtime::ParamUpdate;
let out = self.state.process(array);
let mut result = ProcessResult::arrays(vec![Arc::new(out)]);
if let Some(idx) = self.params.valid_background {
result.param_updates.push(ParamUpdate::int32(
idx,
if self.state.config.valid_background {
1
} else {
0
},
));
}
if let Some(idx) = self.params.valid_flat_field {
result.param_updates.push(ParamUpdate::int32(
idx,
if self.state.config.valid_flat_field {
1
} else {
0
},
));
}
if let Some(idx) = self.params.num_filtered {
result
.param_updates
.push(ParamUpdate::int32(idx, self.state.num_filtered as i32));
}
if let Some(idx) = self.params.save_background {
result.param_updates.push(ParamUpdate::int32(idx, 0));
}
if let Some(idx) = self.params.save_flat_field {
result.param_updates.push(ParamUpdate::int32(idx, 0));
}
result
}
fn plugin_type(&self) -> &str {
"NDPluginProcess"
}
fn register_params(
&mut self,
base: &mut asyn_rs::port::PortDriverBase,
) -> asyn_rs::error::AsynResult<()> {
use asyn_rs::param::ParamType;
base.create_param("PROCESS_DATA_TYPE", ParamType::Int32)?;
base.create_param("SAVE_BACKGROUND", ParamType::Int32)?;
base.create_param("ENABLE_BACKGROUND", ParamType::Int32)?;
base.create_param("VALID_BACKGROUND", ParamType::Int32)?;
base.create_param("SAVE_FLAT_FIELD", ParamType::Int32)?;
base.create_param("ENABLE_FLAT_FIELD", ParamType::Int32)?;
base.create_param("VALID_FLAT_FIELD", ParamType::Int32)?;
base.create_param("SCALE_FLAT_FIELD", ParamType::Float64)?;
base.create_param("ENABLE_OFFSET_SCALE", ParamType::Int32)?;
base.create_param("AUTO_OFFSET_SCALE", ParamType::Int32)?;
base.create_param("OFFSET", ParamType::Float64)?;
base.create_param("SCALE", ParamType::Float64)?;
base.create_param("ENABLE_LOW_CLIP", ParamType::Int32)?;
base.create_param("LOW_CLIP_THRESH", ParamType::Float64)?;
base.create_param("LOW_CLIP_VALUE", ParamType::Float64)?;
base.create_param("ENABLE_HIGH_CLIP", ParamType::Int32)?;
base.create_param("HIGH_CLIP_THRESH", ParamType::Float64)?;
base.create_param("HIGH_CLIP_VALUE", ParamType::Float64)?;
base.create_param("ENABLE_FILTER", ParamType::Int32)?;
base.create_param("FILTER_TYPE", ParamType::Int32)?;
base.create_param("RESET_FILTER", ParamType::Int32)?;
base.create_param("AUTO_RESET_FILTER", ParamType::Int32)?;
base.create_param("FILTER_CALLBACKS", ParamType::Int32)?;
base.create_param("NUM_FILTER", ParamType::Int32)?;
base.create_param("NUM_FILTERED", ParamType::Int32)?;
base.create_param("FILTER_OOFFSET", ParamType::Float64)?;
base.create_param("FILTER_OSCALE", ParamType::Float64)?;
base.create_param("FILTER_OC1", ParamType::Float64)?;
base.create_param("FILTER_OC2", ParamType::Float64)?;
base.create_param("FILTER_OC3", ParamType::Float64)?;
base.create_param("FILTER_OC4", ParamType::Float64)?;
base.create_param("FILTER_FOFFSET", ParamType::Float64)?;
base.create_param("FILTER_FSCALE", ParamType::Float64)?;
base.create_param("FILTER_FC1", ParamType::Float64)?;
base.create_param("FILTER_FC2", ParamType::Float64)?;
base.create_param("FILTER_FC3", ParamType::Float64)?;
base.create_param("FILTER_FC4", ParamType::Float64)?;
base.create_param("FILTER_ROFFSET", ParamType::Float64)?;
base.create_param("FILTER_RC1", ParamType::Float64)?;
base.create_param("FILTER_RC2", ParamType::Float64)?;
self.params.data_type = base.find_param("PROCESS_DATA_TYPE");
self.params.save_background = base.find_param("SAVE_BACKGROUND");
self.params.enable_background = base.find_param("ENABLE_BACKGROUND");
self.params.valid_background = base.find_param("VALID_BACKGROUND");
self.params.save_flat_field = base.find_param("SAVE_FLAT_FIELD");
self.params.enable_flat_field = base.find_param("ENABLE_FLAT_FIELD");
self.params.valid_flat_field = base.find_param("VALID_FLAT_FIELD");
self.params.scale_flat_field = base.find_param("SCALE_FLAT_FIELD");
self.params.enable_offset_scale = base.find_param("ENABLE_OFFSET_SCALE");
self.params.auto_offset_scale = base.find_param("AUTO_OFFSET_SCALE");
self.params.offset = base.find_param("OFFSET");
self.params.scale = base.find_param("SCALE");
self.params.enable_low_clip = base.find_param("ENABLE_LOW_CLIP");
self.params.low_clip_thresh = base.find_param("LOW_CLIP_THRESH");
self.params.low_clip_value = base.find_param("LOW_CLIP_VALUE");
self.params.enable_high_clip = base.find_param("ENABLE_HIGH_CLIP");
self.params.high_clip_thresh = base.find_param("HIGH_CLIP_THRESH");
self.params.high_clip_value = base.find_param("HIGH_CLIP_VALUE");
self.params.enable_filter = base.find_param("ENABLE_FILTER");
self.params.filter_type = base.find_param("FILTER_TYPE");
self.params.reset_filter = base.find_param("RESET_FILTER");
self.params.auto_reset_filter = base.find_param("AUTO_RESET_FILTER");
self.params.filter_callbacks = base.find_param("FILTER_CALLBACKS");
self.params.num_filter = base.find_param("NUM_FILTER");
self.params.num_filtered = base.find_param("NUM_FILTERED");
self.params.o_offset = base.find_param("FILTER_OOFFSET");
self.params.o_scale = base.find_param("FILTER_OSCALE");
self.params.oc[0] = base.find_param("FILTER_OC1");
self.params.oc[1] = base.find_param("FILTER_OC2");
self.params.oc[2] = base.find_param("FILTER_OC3");
self.params.oc[3] = base.find_param("FILTER_OC4");
self.params.f_offset = base.find_param("FILTER_FOFFSET");
self.params.f_scale = base.find_param("FILTER_FSCALE");
self.params.fc[0] = base.find_param("FILTER_FC1");
self.params.fc[1] = base.find_param("FILTER_FC2");
self.params.fc[2] = base.find_param("FILTER_FC3");
self.params.fc[3] = base.find_param("FILTER_FC4");
self.params.r_offset = base.find_param("FILTER_ROFFSET");
self.params.rc[0] = base.find_param("FILTER_RC1");
self.params.rc[1] = base.find_param("FILTER_RC2");
Ok(())
}
fn on_param_change(
&mut self,
reason: usize,
params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
) -> ad_core_rs::plugin::runtime::ParamChangeResult {
use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamUpdate};
let s = &mut self.state;
let p = &self.params;
let mut updates = Vec::new();
if Some(reason) == p.data_type {
let v = params.value.as_i32();
s.config.output_type = if v < 0 {
None } else {
NDDataType::from_ordinal(v as u8)
};
} else if Some(reason) == p.save_background {
if params.value.as_i32() != 0 {
s.config.save_background = true;
}
} else if Some(reason) == p.enable_background {
s.config.enable_background = params.value.as_i32() != 0;
} else if Some(reason) == p.save_flat_field {
if params.value.as_i32() != 0 {
s.config.save_flat_field = true;
}
} else if Some(reason) == p.enable_flat_field {
s.config.enable_flat_field = params.value.as_i32() != 0;
} else if Some(reason) == p.scale_flat_field {
s.config.scale_flat_field = params.value.as_f64();
} else if Some(reason) == p.enable_offset_scale {
s.config.enable_offset_scale = params.value.as_i32() != 0;
} else if Some(reason) == p.auto_offset_scale {
if params.value.as_i32() != 0 {
if let Some(ref arr) = self.state.background {
let _ = arr; }
}
} else if Some(reason) == p.offset {
s.config.offset = params.value.as_f64();
} else if Some(reason) == p.scale {
s.config.scale = params.value.as_f64();
} else if Some(reason) == p.enable_low_clip {
s.config.enable_low_clip = params.value.as_i32() != 0;
} else if Some(reason) == p.low_clip_thresh {
s.config.low_clip_thresh = params.value.as_f64();
} else if Some(reason) == p.low_clip_value {
s.config.low_clip_value = params.value.as_f64();
} else if Some(reason) == p.enable_high_clip {
s.config.enable_high_clip = params.value.as_i32() != 0;
} else if Some(reason) == p.high_clip_thresh {
s.config.high_clip_thresh = params.value.as_f64();
} else if Some(reason) == p.high_clip_value {
s.config.high_clip_value = params.value.as_f64();
} else if Some(reason) == p.enable_filter {
s.config.enable_filter = params.value.as_i32() != 0;
} else if Some(reason) == p.filter_type {
s.apply_filter_type(params.value.as_i32());
s.reset_filter();
let fc = &s.config.filter;
for (i, idx) in p.fc.iter().enumerate() {
if let Some(idx) = *idx {
updates.push(ParamUpdate::float64(idx, fc.fc[i]));
}
}
for (i, idx) in p.oc.iter().enumerate() {
if let Some(idx) = *idx {
updates.push(ParamUpdate::float64(idx, fc.oc[i]));
}
}
for (i, idx) in p.rc.iter().enumerate() {
if let Some(idx) = *idx {
updates.push(ParamUpdate::float64(idx, fc.rc[i]));
}
}
if let Some(idx) = p.f_offset {
updates.push(ParamUpdate::float64(idx, fc.f_offset));
}
if let Some(idx) = p.f_scale {
updates.push(ParamUpdate::float64(idx, fc.f_scale));
}
if let Some(idx) = p.o_offset {
updates.push(ParamUpdate::float64(idx, fc.o_offset));
}
if let Some(idx) = p.o_scale {
updates.push(ParamUpdate::float64(idx, fc.o_scale));
}
} else if Some(reason) == p.reset_filter {
if params.value.as_i32() != 0 {
s.reset_filter();
if let Some(idx) = p.num_filtered {
updates.push(ParamUpdate::int32(idx, 0));
}
}
} else if Some(reason) == p.auto_reset_filter {
s.config.filter.auto_reset = params.value.as_i32() != 0;
} else if Some(reason) == p.filter_callbacks {
s.config.filter.filter_callbacks = params.value.as_i32().max(0) as usize;
} else if Some(reason) == p.num_filter {
s.config.filter.num_filter = params.value.as_i32().max(1) as usize;
} else if Some(reason) == p.o_offset {
s.config.filter.o_offset = params.value.as_f64();
} else if Some(reason) == p.o_scale {
s.config.filter.o_scale = params.value.as_f64();
} else if Some(reason) == p.f_offset {
s.config.filter.f_offset = params.value.as_f64();
} else if Some(reason) == p.f_scale {
s.config.filter.f_scale = params.value.as_f64();
} else if Some(reason) == p.r_offset {
s.config.filter.r_offset = params.value.as_f64();
} else {
for i in 0..4 {
if Some(reason) == p.oc[i] {
s.config.filter.oc[i] = params.value.as_f64();
return ParamChangeResult::updates(vec![]);
}
if Some(reason) == p.fc[i] {
s.config.filter.fc[i] = params.value.as_f64();
return ParamChangeResult::updates(vec![]);
}
}
for i in 0..2 {
if Some(reason) == p.rc[i] {
s.config.filter.rc[i] = params.value.as_f64();
return ParamChangeResult::updates(vec![]);
}
}
}
ParamChangeResult::updates(updates)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ad_core_rs::ndarray::{NDDataBuffer, NDDimension};
fn make_array(vals: &[u8]) -> NDArray {
let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::UInt8);
if let NDDataBuffer::U8(ref mut v) = arr.data {
v.copy_from_slice(vals);
}
arr
}
fn make_f64_array(vals: &[f64]) -> NDArray {
let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::Float64);
if let NDDataBuffer::F64(ref mut v) = arr.data {
v.copy_from_slice(vals);
}
arr
}
#[test]
fn test_background_subtraction() {
let bg_arr = make_array(&[10, 20, 30]);
let input = make_array(&[15, 25, 35]);
let mut state = ProcessState::new(ProcessConfig {
enable_background: true,
..Default::default()
});
state.save_background(&bg_arr);
let result = state.process(&input);
if let NDDataBuffer::U8(ref v) = result.data {
assert_eq!(v[0], 5);
assert_eq!(v[1], 5);
assert_eq!(v[2], 5);
}
}
#[test]
fn test_flat_field() {
let ff_arr = make_array(&[100, 200, 50]);
let input = make_array(&[100, 200, 50]);
let mut state = ProcessState::new(ProcessConfig {
enable_flat_field: true,
scale_flat_field: 0.0, ..Default::default()
});
state.save_flat_field(&ff_arr);
let result = state.process(&input);
if let NDDataBuffer::U8(ref v) = result.data {
assert!((v[0] as f64 - 116.67).abs() < 1.0);
assert!((v[1] as f64 - 116.67).abs() < 1.0);
assert!((v[2] as f64 - 116.67).abs() < 1.0);
}
}
#[test]
fn test_offset_scale() {
let input = make_array(&[10, 20, 30]);
let mut state = ProcessState::new(ProcessConfig {
enable_offset_scale: true,
scale: 2.0,
offset: 5.0,
..Default::default()
});
let result = state.process(&input);
if let NDDataBuffer::U8(ref v) = result.data {
assert_eq!(v[0], 30); assert_eq!(v[1], 50); assert_eq!(v[2], 70); }
}
#[test]
fn test_clipping() {
let input = make_array(&[5, 50, 200]);
let mut state = ProcessState::new(ProcessConfig {
enable_low_clip: true,
low_clip_thresh: 10.0,
low_clip_value: 10.0,
enable_high_clip: true,
high_clip_thresh: 100.0,
high_clip_value: 100.0,
..Default::default()
});
let result = state.process(&input);
if let NDDataBuffer::U8(ref v) = result.data {
assert_eq!(v[0], 10); assert_eq!(v[1], 50); assert_eq!(v[2], 100); }
}
#[test]
fn test_recursive_filter() {
let input1 = make_array(&[100, 100, 100]);
let input2 = make_array(&[0, 0, 0]);
let mut state = ProcessState::new(ProcessConfig {
enable_filter: true,
filter: FilterConfig {
num_filter: 10,
fc: [0.5, 0.0, 0.5, 0.0], oc: [1.0, 0.0, 0.0, 0.0], rc: [0.0, 1.0], ..Default::default()
},
..Default::default()
});
let _ = state.process(&input1);
let result = state.process(&input2);
if let NDDataBuffer::U8(ref v) = result.data {
assert_eq!(v[0], 100);
assert_eq!(v[1], 100);
}
}
#[test]
fn test_output_type_conversion() {
let input = make_array(&[10, 20, 30]);
let mut state = ProcessState::new(ProcessConfig {
output_type: Some(NDDataType::Float64),
..Default::default()
});
let result = state.process(&input);
assert_eq!(result.data.data_type(), NDDataType::Float64);
}
#[test]
fn test_process_processor() {
let mut proc = ProcessProcessor::new(ProcessConfig {
enable_offset_scale: true,
scale: 2.0,
offset: 1.0,
..Default::default()
});
let pool = NDArrayPool::new(1_000_000);
let input = make_array(&[10, 20, 30]);
let result = proc.process_array(&input, &pool);
assert_eq!(result.output_arrays.len(), 1);
if let NDDataBuffer::U8(ref v) = result.output_arrays[0].data {
assert_eq!(v[0], 22); }
}
#[test]
fn test_filter_sum_preset() {
let mut state = ProcessState::new(ProcessConfig {
enable_filter: true,
filter: FilterConfig {
num_filter: 10,
fc: [1.0, 0.0, 1.0, 0.0],
oc: [1.0, 0.0, 0.0, 0.0],
rc: [0.0, 1.0],
..Default::default()
},
output_type: Some(NDDataType::Float64),
..Default::default()
});
let r0 = state.process(&make_f64_array(&[100.0]));
let v0 = r0.data.get_as_f64(0).unwrap();
assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
let r1 = state.process(&make_f64_array(&[100.0]));
let v1 = r1.data.get_as_f64(0).unwrap();
assert!((v1 - 200.0).abs() < 1e-9, "frame 1: got {v1}");
}
#[test]
fn test_filter_average_preset() {
let mut state = ProcessState::new(ProcessConfig {
enable_filter: true,
filter: FilterConfig {
num_filter: 10,
fc: [1.0, 0.0, 1.0, 0.0],
oc: [0.0, 1.0, 0.0, 0.0],
rc: [0.0, 1.0],
..Default::default()
},
output_type: Some(NDDataType::Float64),
..Default::default()
});
let r0 = state.process(&make_f64_array(&[100.0]));
let v0 = r0.data.get_as_f64(0).unwrap();
assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
let r1 = state.process(&make_f64_array(&[200.0]));
let v1 = r1.data.get_as_f64(0).unwrap();
assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
let r2 = state.process(&make_f64_array(&[300.0]));
let v2 = r2.data.get_as_f64(0).unwrap();
let expected = 400.0 / 3.0; assert!((v2 - expected).abs() < 1e-9, "frame 2: got {v2}");
}
#[test]
fn test_filter_recursive_ave() {
let mut state = ProcessState::new(ProcessConfig {
enable_filter: true,
filter: FilterConfig {
num_filter: 10,
fc: [1.0, -1.0, 0.0, 1.0],
oc: [1.0, 0.0, 0.0, 0.0],
rc: [0.0, 1.0],
..Default::default()
},
output_type: Some(NDDataType::Float64),
..Default::default()
});
let r0 = state.process(&make_f64_array(&[100.0]));
let v0 = r0.data.get_as_f64(0).unwrap();
assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
let r1 = state.process(&make_f64_array(&[200.0]));
let v1 = r1.data.get_as_f64(0).unwrap();
assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
let r2 = state.process(&make_f64_array(&[300.0]));
let v2 = r2.data.get_as_f64(0).unwrap();
assert!((v2 - 150.0).abs() < 1e-9, "frame 2: got {v2}");
}
#[test]
fn test_save_background_one_shot() {
let mut state = ProcessState::new(ProcessConfig {
save_background: true,
..Default::default()
});
assert!(!state.config.valid_background);
assert!(state.background.is_none());
let input = make_array(&[10, 20, 30]);
let _ = state.process(&input);
assert!(
!state.config.save_background,
"save_background should be cleared"
);
assert!(
state.config.valid_background,
"valid_background should be set"
);
assert!(state.background.is_some());
let bg = state.background.as_ref().unwrap();
assert_eq!(bg.len(), 3);
assert!((bg[0] - 10.0).abs() < 1e-9);
assert!((bg[1] - 20.0).abs() < 1e-9);
assert!((bg[2] - 30.0).abs() < 1e-9);
let input2 = make_array(&[40, 50, 60]);
let _ = state.process(&input2);
assert!(
!state.config.save_background,
"save_background stays cleared"
);
let bg2 = state.background.as_ref().unwrap();
assert!((bg2[0] - 10.0).abs() < 1e-9);
}
#[test]
fn test_save_flat_field_one_shot() {
let mut state = ProcessState::new(ProcessConfig {
save_flat_field: true,
..Default::default()
});
assert!(!state.config.valid_flat_field);
assert!(state.flat_field.is_none());
let input = make_array(&[50, 100, 150]);
let _ = state.process(&input);
assert!(
!state.config.save_flat_field,
"save_flat_field should be cleared"
);
assert!(
state.config.valid_flat_field,
"valid_flat_field should be set"
);
assert!(state.flat_field.is_some());
let ff = state.flat_field.as_ref().unwrap();
assert_eq!(ff.len(), 3);
assert!((ff[0] - 50.0).abs() < 1e-9);
assert!((ff[1] - 100.0).abs() < 1e-9);
assert!((ff[2] - 150.0).abs() < 1e-9);
}
#[test]
fn test_auto_reset_when_num_filter_reached() {
let mut state = ProcessState::new(ProcessConfig {
enable_filter: true,
filter: FilterConfig {
num_filter: 3,
auto_reset: true,
fc: [1.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0],
rc: [0.0, 1.0],
..Default::default()
},
output_type: Some(NDDataType::Float64),
..Default::default()
});
let _ = state.process(&make_f64_array(&[100.0]));
assert_eq!(state.num_filtered, 1);
let _ = state.process(&make_f64_array(&[100.0]));
assert_eq!(state.num_filtered, 2);
let _ = state.process(&make_f64_array(&[100.0]));
assert_eq!(state.num_filtered, 3);
let _ = state.process(&make_f64_array(&[200.0]));
assert_eq!(state.num_filtered, 1, "fresh start after auto reset");
}
#[test]
fn test_filter_with_offset_scale() {
let mut state = ProcessState::new(ProcessConfig {
enable_filter: true,
filter: FilterConfig {
num_filter: 10,
fc: [0.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0], rc: [0.0, 1.0],
f_offset: 10.0,
f_scale: 2.0,
o_offset: 5.0,
o_scale: 3.0,
..Default::default()
},
output_type: Some(NDDataType::Float64),
..Default::default()
});
let r0 = state.process(&make_f64_array(&[50.0]));
let v0 = r0.data.get_as_f64(0).unwrap();
assert!((v0 - 155.0).abs() < 1e-9, "frame 0: got {v0}");
let r1 = state.process(&make_f64_array(&[20.0]));
let v1 = r1.data.get_as_f64(0).unwrap();
assert!((v1 - 335.0).abs() < 1e-9, "frame 1: got {v1}");
}
#[test]
fn test_reset_filter_manual() {
let mut state = ProcessState::new(ProcessConfig {
enable_filter: true,
filter: FilterConfig {
num_filter: 10,
fc: [1.0, 0.0, 1.0, 0.0],
oc: [1.0, 0.0, 0.0, 0.0],
rc: [0.0, 1.0],
..Default::default()
},
output_type: Some(NDDataType::Float64),
..Default::default()
});
let _ = state.process(&make_f64_array(&[100.0]));
let _ = state.process(&make_f64_array(&[100.0]));
assert!(state.filter_state.is_some());
assert_eq!(state.num_filtered, 2);
state.reset_filter();
assert!(state.filter_state.is_none());
assert_eq!(state.num_filtered, 0);
let _ = state.process(&make_f64_array(&[200.0]));
assert_eq!(state.num_filtered, 1);
}
}