1use std::sync::Arc;
2
3#[cfg(feature = "parallel")]
4use crate::par_util;
5#[cfg(feature = "parallel")]
6use rayon::prelude::*;
7
8use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType};
9use ad_core_rs::ndarray_pool::NDArrayPool;
10use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
11
12#[derive(Debug, Clone)]
27pub struct FilterConfig {
28 pub num_filter: usize,
30 pub auto_reset: bool,
32 pub filter_callbacks: usize,
34 pub oc: [f64; 4],
36 pub fc: [f64; 4],
38 pub rc: [f64; 2],
40 pub r_offset: f64,
42 pub o_offset: f64,
44 pub o_scale: f64,
46 pub f_offset: f64,
48 pub f_scale: f64,
50}
51
52impl Default for FilterConfig {
53 fn default() -> Self {
54 Self {
55 num_filter: 1,
56 auto_reset: false,
57 filter_callbacks: 0,
58 oc: [1.0, 0.0, 0.0, 0.0], fc: [1.0, 0.0, 0.0, 0.0],
60 rc: [1.0, 0.0],
61 r_offset: 0.0,
62 o_offset: 0.0,
63 o_scale: 1.0,
64 f_offset: 0.0,
65 f_scale: 1.0,
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct ProcessConfig {
73 pub enable_background: bool,
74 pub enable_flat_field: bool,
75 pub enable_offset_scale: bool,
76 pub offset: f64,
77 pub scale: f64,
78 pub enable_low_clip: bool,
79 pub low_clip_thresh: f64,
80 pub low_clip_value: f64,
81 pub enable_high_clip: bool,
82 pub high_clip_thresh: f64,
83 pub high_clip_value: f64,
84 pub scale_flat_field: f64,
85 pub enable_filter: bool,
86 pub filter: FilterConfig,
87 pub output_type: Option<NDDataType>,
88 pub save_background: bool,
90 pub save_flat_field: bool,
92 pub auto_offset_scale_pending: bool,
95 pub valid_background: bool,
97 pub valid_flat_field: bool,
99}
100
101impl Default for ProcessConfig {
102 fn default() -> Self {
103 Self {
104 enable_background: false,
105 enable_flat_field: false,
106 enable_offset_scale: false,
107 offset: 0.0,
108 scale: 1.0,
109 enable_low_clip: false,
110 low_clip_thresh: 0.0,
111 low_clip_value: 0.0,
112 enable_high_clip: false,
113 high_clip_thresh: 100.0,
114 high_clip_value: 100.0,
115 scale_flat_field: 255.0,
116 enable_filter: false,
117 filter: FilterConfig::default(),
118 output_type: None,
119 save_background: false,
120 save_flat_field: false,
121 auto_offset_scale_pending: false,
122 valid_background: false,
123 valid_flat_field: false,
124 }
125 }
126}
127
128pub struct ProcessState {
132 pub config: ProcessConfig,
133 pub background: Option<Vec<f64>>,
134 pub flat_field: Option<Vec<f64>>,
135 pub filter_state: Option<Vec<f64>>,
137 pub num_filtered: usize,
139}
140
141impl ProcessState {
142 pub fn new(config: ProcessConfig) -> Self {
143 Self {
144 config,
145 background: None,
146 flat_field: None,
147 filter_state: None,
148 num_filtered: 0,
149 }
150 }
151
152 pub fn save_background(&mut self, array: &NDArray) {
154 let n = array.data.len();
155 let mut bg = vec![0.0f64; n];
156 for i in 0..n {
157 bg[i] = array.data.get_as_f64(i).unwrap_or(0.0);
158 }
159 self.background = Some(bg);
160 self.config.valid_background = true;
161 }
162
163 pub fn save_flat_field(&mut self, array: &NDArray) {
165 let n = array.data.len();
166 let mut ff = vec![0.0f64; n];
167 for i in 0..n {
168 ff[i] = array.data.get_as_f64(i).unwrap_or(0.0);
169 }
170 self.flat_field = Some(ff);
171 self.config.valid_flat_field = true;
172 }
173
174 pub fn auto_offset_scale(&mut self, array: &NDArray) {
179 let n = array.data.len();
180 if n == 0 {
181 return;
182 }
183 let mut min_val = f64::MAX;
184 let mut max_val = f64::MIN;
185 for i in 0..n {
186 let v = array.data.get_as_f64(i).unwrap_or(0.0);
187 if v < min_val {
188 min_val = v;
189 }
190 if v > max_val {
191 max_val = v;
192 }
193 }
194 let range = max_val - min_val;
195 if range > 0.0 {
196 let bytes_per_elem = match self.config.output_type.unwrap_or(array.data.data_type()) {
198 NDDataType::Int8 | NDDataType::UInt8 => 1,
199 NDDataType::Int16 | NDDataType::UInt16 => 2,
200 NDDataType::Int32 | NDDataType::UInt32 => 4,
201 NDDataType::Int64 | NDDataType::UInt64 => 8,
202 NDDataType::Float32 => 4,
203 NDDataType::Float64 => 8,
204 };
205 let max_scale = 2.0f64.powi(bytes_per_elem * 8) - 1.0;
206 self.config.scale = max_scale / range;
208 self.config.offset = -min_val;
209 self.config.enable_offset_scale = true;
211 self.config.enable_low_clip = true;
212 self.config.low_clip_thresh = 0.0;
213 self.config.enable_high_clip = true;
214 self.config.high_clip_thresh = max_scale;
215 }
216 }
217
218 pub fn apply_filter_type(&mut self, filter_type: i32) {
226 let fc = &mut self.config.filter;
227 match filter_type {
228 0 => {
229 fc.fc = [1.0, -1.0, 0.0, 1.0];
243 fc.oc = [1.0, 0.0, 0.0, 0.0];
244 fc.rc = [0.0, 1.0]; fc.r_offset = 0.0;
246 fc.f_offset = 0.0;
247 fc.f_scale = 1.0;
248 fc.o_offset = 0.0;
249 fc.o_scale = 1.0;
250 }
251 1 => {
252 fc.fc = [1.0, 0.0, 1.0, 0.0];
258 fc.oc = [0.0, 1.0, 0.0, 0.0];
259 fc.rc = [0.0, 1.0]; fc.r_offset = 0.0;
261 fc.f_offset = 0.0;
262 fc.f_scale = 1.0;
263 fc.o_offset = 0.0;
264 fc.o_scale = 1.0;
265 }
266 2 => {
267 fc.fc = [1.0, 0.0, 1.0, 0.0];
269 fc.oc = [1.0, 0.0, 0.0, 0.0];
270 fc.rc = [0.0, 1.0];
271 fc.r_offset = 0.0;
272 fc.f_offset = 0.0;
273 fc.f_scale = 1.0;
274 fc.o_offset = 0.0;
275 fc.o_scale = 1.0;
276 }
277 3 => {
278 fc.fc = [0.0, 0.0, 1.0, 0.0];
282 fc.oc = [-1.0, 0.0, 1.0, 0.0];
283 fc.rc = [0.0, 1.0];
284 fc.r_offset = 0.0;
285 fc.f_offset = 0.0;
286 fc.f_scale = 1.0;
287 fc.o_offset = 0.0;
288 fc.o_scale = 1.0;
289 }
290 4 => {
291 fc.fc = [1.0, -1.0, 0.0, 1.0];
294 fc.oc = [-1.0, 0.0, 1.0, 0.0];
295 fc.rc = [0.0, 1.0];
296 fc.r_offset = 0.0;
297 fc.f_offset = 0.0;
298 fc.f_scale = 1.0;
299 fc.o_offset = 0.0;
300 fc.o_scale = 1.0;
301 }
302 5 => {
303 fc.fc = [0.0, 0.0, 1.0, 0.0];
305 fc.oc = [1.0, 0.0, 0.0, 0.0];
306 fc.rc = [0.0, 1.0];
307 fc.r_offset = 0.0;
308 fc.f_offset = 0.0;
309 fc.f_scale = 1.0;
310 fc.o_offset = 0.0;
311 fc.o_scale = 1.0;
312 }
313 _ => {} }
315 }
316
317 pub fn reset_filter(&mut self) {
319 self.filter_state = None;
320 self.num_filtered = 0;
321 }
322
323 pub fn process(&mut self, src: &NDArray) -> Option<NDArray> {
330 let n = src.data.len();
331 let mut values = vec![0.0f64; n];
332 for i in 0..n {
333 values[i] = src.data.get_as_f64(i).unwrap_or(0.0);
334 }
335
336 if self.config.save_background {
338 self.save_background(src);
339 self.config.save_background = false;
340 }
341 if self.config.save_flat_field {
342 self.save_flat_field(src);
343 self.config.save_flat_field = false;
344 }
345 let auto_offset_scale_now = self.config.auto_offset_scale_pending;
352 self.config.auto_offset_scale_pending = false;
353
354 self.config.valid_background = self.background.as_ref().is_some_and(|b| b.len() == n);
360 self.config.valid_flat_field = self.flat_field.as_ref().is_some_and(|f| f.len() == n);
361
362 let needs_element_ops = self.config.enable_background
365 || self.config.enable_flat_field
366 || self.config.enable_offset_scale
367 || self.config.enable_low_clip
368 || self.config.enable_high_clip;
369
370 if needs_element_ops {
371 let bg = if self.config.enable_background && self.config.valid_background {
374 self.background.as_ref()
375 } else {
376 None
377 };
378 let (ff, ff_scale) = if self.config.enable_flat_field && self.config.valid_flat_field {
379 if let Some(ref ff) = self.flat_field {
380 (Some(ff.as_slice()), self.config.scale_flat_field)
384 } else {
385 (None, 0.0)
386 }
387 } else {
388 (None, 0.0)
389 };
390 let do_offset_scale = self.config.enable_offset_scale;
391 let scale = self.config.scale;
392 let offset = self.config.offset;
393 let do_low_clip = self.config.enable_low_clip;
394 let low_clip_thresh = self.config.low_clip_thresh;
395 let low_clip_value = self.config.low_clip_value;
396 let do_high_clip = self.config.enable_high_clip;
397 let high_clip_thresh = self.config.high_clip_thresh;
398 let high_clip_value = self.config.high_clip_value;
399
400 let apply_stages = |i: usize, v: &mut f64| {
401 if let Some(bg) = bg {
405 *v -= bg[i];
406 }
407 if let Some(ff) = ff {
409 if ff[i] != 0.0 {
410 *v = *v * ff_scale / ff[i];
411 }
412 }
413 if do_offset_scale {
415 *v = (*v + offset) * scale;
416 }
417 if do_high_clip && *v > high_clip_thresh {
421 *v = high_clip_value;
422 }
423 if do_low_clip && *v < low_clip_thresh {
424 *v = low_clip_value;
425 }
426 };
427
428 #[cfg(feature = "parallel")]
429 let use_parallel = par_util::should_parallelize(n);
430 #[cfg(not(feature = "parallel"))]
431 let use_parallel = false;
432
433 if use_parallel {
434 #[cfg(feature = "parallel")]
435 par_util::thread_pool().install(|| {
436 values.par_iter_mut().enumerate().for_each(|(i, v)| {
437 apply_stages(i, v);
438 });
439 });
440 } else {
441 for (i, v) in values.iter_mut().enumerate() {
442 apply_stages(i, v);
443 }
444 }
445 }
446
447 if self.config.enable_filter {
449 let fc = &self.config.filter;
450
451 if let Some(ref f) = self.filter_state {
453 if f.len() != n {
454 self.filter_state = None;
455 }
456 }
457
458 let mut reset_filter = self.filter_state.is_none();
459 if self.num_filtered >= fc.num_filter && fc.auto_reset {
460 reset_filter = true;
461 }
462
463 if self.filter_state.is_none() {
465 self.filter_state = Some(values.clone());
466 }
467
468 let filter = self.filter_state.as_mut().unwrap();
469
470 if reset_filter {
471 let r_offset = fc.r_offset;
473 let rc1 = fc.rc[0];
474 let rc2 = fc.rc[1];
475 for i in 0..n {
476 let new_filter = r_offset + rc1 * filter[i] + rc2 * values[i];
477 filter[i] = new_filter;
478 }
479 self.num_filtered = 0;
480 }
481
482 if self.num_filtered < fc.num_filter {
484 self.num_filtered += 1;
485 }
486
487 let nf = self.num_filtered as f64;
489 let o1 = fc.o_scale * (fc.oc[0] + fc.oc[1] / nf);
490 let o2 = fc.o_scale * (fc.oc[2] + fc.oc[3] / nf);
491 let f1 = fc.f_scale * (fc.fc[0] + fc.fc[1] / nf);
492 let f2 = fc.f_scale * (fc.fc[2] + fc.fc[3] / nf);
493 let o_offset = fc.o_offset;
494 let f_offset = fc.f_offset;
495
496 for i in 0..n {
505 let new_data = o_offset + o1 * filter[i] + o2 * values[i];
506 let new_filter = f_offset + f1 * filter[i] + f2 * values[i];
507 values[i] = new_data;
508 filter[i] = new_filter;
509 }
510
511 if fc.filter_callbacks > 0 && self.num_filtered != fc.num_filter {
516 return None;
517 }
518 }
519
520 let out_type = self.config.output_type.unwrap_or(src.data.data_type());
522 let mut out_data = NDDataBuffer::zeros(out_type, n);
523 for i in 0..n {
524 out_data.set_from_f64(i, values[i]);
525 }
526
527 let mut arr = NDArray::new(src.dims.clone(), out_type);
528 arr.data = out_data;
529 arr.unique_id = src.unique_id;
530 arr.timestamp = src.timestamp;
531 arr.attributes = src.attributes.clone();
532
533 if auto_offset_scale_now {
538 self.auto_offset_scale(src);
539 }
540
541 Some(arr)
542 }
543}
544
545#[derive(Default)]
549struct ProcParamIndices {
550 data_type: Option<usize>,
551 save_background: Option<usize>,
552 enable_background: Option<usize>,
553 valid_background: Option<usize>,
554 save_flat_field: Option<usize>,
555 enable_flat_field: Option<usize>,
556 valid_flat_field: Option<usize>,
557 scale_flat_field: Option<usize>,
558 enable_offset_scale: Option<usize>,
559 auto_offset_scale: Option<usize>,
560 offset: Option<usize>,
561 scale: Option<usize>,
562 enable_low_clip: Option<usize>,
563 low_clip_thresh: Option<usize>,
564 low_clip_value: Option<usize>,
565 enable_high_clip: Option<usize>,
566 high_clip_thresh: Option<usize>,
567 high_clip_value: Option<usize>,
568 enable_filter: Option<usize>,
569 filter_type: Option<usize>,
570 reset_filter: Option<usize>,
571 auto_reset_filter: Option<usize>,
572 filter_callbacks: Option<usize>,
573 num_filter: Option<usize>,
574 num_filtered: Option<usize>,
575 o_offset: Option<usize>,
576 o_scale: Option<usize>,
577 oc: [Option<usize>; 4],
578 f_offset: Option<usize>,
579 f_scale: Option<usize>,
580 fc: [Option<usize>; 4],
581 r_offset: Option<usize>,
582 rc: [Option<usize>; 2],
583}
584
585pub struct ProcessProcessor {
587 state: ProcessState,
588 params: ProcParamIndices,
589}
590
591impl ProcessProcessor {
592 pub fn new(config: ProcessConfig) -> Self {
593 Self {
594 state: ProcessState::new(config),
595 params: ProcParamIndices::default(),
596 }
597 }
598
599 pub fn state(&self) -> &ProcessState {
600 &self.state
601 }
602
603 pub fn state_mut(&mut self) -> &mut ProcessState {
604 &mut self.state
605 }
606}
607
608impl NDPluginProcess for ProcessProcessor {
609 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
610 use ad_core_rs::plugin::runtime::ParamUpdate;
611
612 let out = self.state.process(array);
613 let mut result = match out {
616 Some(arr) => ProcessResult::arrays(vec![Arc::new(arr)]),
617 None => ProcessResult::sink(vec![]),
618 };
619
620 if let Some(idx) = self.params.valid_background {
622 result.param_updates.push(ParamUpdate::int32(
623 idx,
624 if self.state.config.valid_background {
625 1
626 } else {
627 0
628 },
629 ));
630 }
631 if let Some(idx) = self.params.valid_flat_field {
632 result.param_updates.push(ParamUpdate::int32(
633 idx,
634 if self.state.config.valid_flat_field {
635 1
636 } else {
637 0
638 },
639 ));
640 }
641 if let Some(idx) = self.params.num_filtered {
642 result
643 .param_updates
644 .push(ParamUpdate::int32(idx, self.state.num_filtered as i32));
645 }
646 if let Some(idx) = self.params.save_background {
648 result.param_updates.push(ParamUpdate::int32(idx, 0));
649 }
650 if let Some(idx) = self.params.save_flat_field {
651 result.param_updates.push(ParamUpdate::int32(idx, 0));
652 }
653
654 result
655 }
656
657 fn plugin_type(&self) -> &str {
658 "NDPluginProcess"
659 }
660
661 fn register_params(
662 &mut self,
663 base: &mut asyn_rs::port::PortDriverBase,
664 ) -> asyn_rs::error::AsynResult<()> {
665 use asyn_rs::param::ParamType;
666 base.create_param("PROCESS_DATA_TYPE", ParamType::Int32)?;
667 base.create_param("SAVE_BACKGROUND", ParamType::Int32)?;
668 base.create_param("ENABLE_BACKGROUND", ParamType::Int32)?;
669 base.create_param("VALID_BACKGROUND", ParamType::Int32)?;
670 base.create_param("SAVE_FLAT_FIELD", ParamType::Int32)?;
671 base.create_param("ENABLE_FLAT_FIELD", ParamType::Int32)?;
672 base.create_param("VALID_FLAT_FIELD", ParamType::Int32)?;
673 base.create_param("SCALE_FLAT_FIELD", ParamType::Float64)?;
674 base.create_param("ENABLE_OFFSET_SCALE", ParamType::Int32)?;
675 base.create_param("AUTO_OFFSET_SCALE", ParamType::Int32)?;
676 base.create_param("OFFSET", ParamType::Float64)?;
677 base.create_param("SCALE", ParamType::Float64)?;
678 base.create_param("ENABLE_LOW_CLIP", ParamType::Int32)?;
679 base.create_param("LOW_CLIP_THRESH", ParamType::Float64)?;
680 base.create_param("LOW_CLIP_VALUE", ParamType::Float64)?;
681 base.create_param("ENABLE_HIGH_CLIP", ParamType::Int32)?;
682 base.create_param("HIGH_CLIP_THRESH", ParamType::Float64)?;
683 base.create_param("HIGH_CLIP_VALUE", ParamType::Float64)?;
684 base.create_param("ENABLE_FILTER", ParamType::Int32)?;
685 base.create_param("FILTER_TYPE", ParamType::Int32)?;
686 base.create_param("RESET_FILTER", ParamType::Int32)?;
687 base.create_param("AUTO_RESET_FILTER", ParamType::Int32)?;
688 base.create_param("FILTER_CALLBACKS", ParamType::Int32)?;
689 base.create_param("NUM_FILTER", ParamType::Int32)?;
690 base.create_param("NUM_FILTERED", ParamType::Int32)?;
691 base.create_param("FILTER_OOFFSET", ParamType::Float64)?;
692 base.create_param("FILTER_OSCALE", ParamType::Float64)?;
693 base.create_param("FILTER_OC1", ParamType::Float64)?;
694 base.create_param("FILTER_OC2", ParamType::Float64)?;
695 base.create_param("FILTER_OC3", ParamType::Float64)?;
696 base.create_param("FILTER_OC4", ParamType::Float64)?;
697 base.create_param("FILTER_FOFFSET", ParamType::Float64)?;
698 base.create_param("FILTER_FSCALE", ParamType::Float64)?;
699 base.create_param("FILTER_FC1", ParamType::Float64)?;
700 base.create_param("FILTER_FC2", ParamType::Float64)?;
701 base.create_param("FILTER_FC3", ParamType::Float64)?;
702 base.create_param("FILTER_FC4", ParamType::Float64)?;
703 base.create_param("FILTER_ROFFSET", ParamType::Float64)?;
704 base.create_param("FILTER_RC1", ParamType::Float64)?;
705 base.create_param("FILTER_RC2", ParamType::Float64)?;
706
707 self.params.data_type = base.find_param("PROCESS_DATA_TYPE");
709 self.params.save_background = base.find_param("SAVE_BACKGROUND");
710 self.params.enable_background = base.find_param("ENABLE_BACKGROUND");
711 self.params.valid_background = base.find_param("VALID_BACKGROUND");
712 self.params.save_flat_field = base.find_param("SAVE_FLAT_FIELD");
713 self.params.enable_flat_field = base.find_param("ENABLE_FLAT_FIELD");
714 self.params.valid_flat_field = base.find_param("VALID_FLAT_FIELD");
715 self.params.scale_flat_field = base.find_param("SCALE_FLAT_FIELD");
716 self.params.enable_offset_scale = base.find_param("ENABLE_OFFSET_SCALE");
717 self.params.auto_offset_scale = base.find_param("AUTO_OFFSET_SCALE");
718 self.params.offset = base.find_param("OFFSET");
719 self.params.scale = base.find_param("SCALE");
720 self.params.enable_low_clip = base.find_param("ENABLE_LOW_CLIP");
721 self.params.low_clip_thresh = base.find_param("LOW_CLIP_THRESH");
722 self.params.low_clip_value = base.find_param("LOW_CLIP_VALUE");
723 self.params.enable_high_clip = base.find_param("ENABLE_HIGH_CLIP");
724 self.params.high_clip_thresh = base.find_param("HIGH_CLIP_THRESH");
725 self.params.high_clip_value = base.find_param("HIGH_CLIP_VALUE");
726 self.params.enable_filter = base.find_param("ENABLE_FILTER");
727 self.params.filter_type = base.find_param("FILTER_TYPE");
728 self.params.reset_filter = base.find_param("RESET_FILTER");
729 self.params.auto_reset_filter = base.find_param("AUTO_RESET_FILTER");
730 self.params.filter_callbacks = base.find_param("FILTER_CALLBACKS");
731 self.params.num_filter = base.find_param("NUM_FILTER");
732 self.params.num_filtered = base.find_param("NUM_FILTERED");
733 self.params.o_offset = base.find_param("FILTER_OOFFSET");
734 self.params.o_scale = base.find_param("FILTER_OSCALE");
735 self.params.oc[0] = base.find_param("FILTER_OC1");
736 self.params.oc[1] = base.find_param("FILTER_OC2");
737 self.params.oc[2] = base.find_param("FILTER_OC3");
738 self.params.oc[3] = base.find_param("FILTER_OC4");
739 self.params.f_offset = base.find_param("FILTER_FOFFSET");
740 self.params.f_scale = base.find_param("FILTER_FSCALE");
741 self.params.fc[0] = base.find_param("FILTER_FC1");
742 self.params.fc[1] = base.find_param("FILTER_FC2");
743 self.params.fc[2] = base.find_param("FILTER_FC3");
744 self.params.fc[3] = base.find_param("FILTER_FC4");
745 self.params.r_offset = base.find_param("FILTER_ROFFSET");
746 self.params.rc[0] = base.find_param("FILTER_RC1");
747 self.params.rc[1] = base.find_param("FILTER_RC2");
748 Ok(())
749 }
750
751 fn on_param_change(
752 &mut self,
753 reason: usize,
754 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
755 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
756 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamUpdate};
757
758 let s = &mut self.state;
759 let p = &self.params;
760 let mut updates = Vec::new();
761
762 if Some(reason) == p.data_type {
763 let v = params.value.as_i32();
764 s.config.output_type = if v < 0 {
765 None } else {
767 NDDataType::from_ordinal(v as u8)
768 };
769 } else if Some(reason) == p.save_background {
770 if params.value.as_i32() != 0 {
771 s.config.save_background = true;
772 }
773 } else if Some(reason) == p.enable_background {
774 s.config.enable_background = params.value.as_i32() != 0;
775 } else if Some(reason) == p.save_flat_field {
776 if params.value.as_i32() != 0 {
777 s.config.save_flat_field = true;
778 }
779 } else if Some(reason) == p.enable_flat_field {
780 s.config.enable_flat_field = params.value.as_i32() != 0;
781 } else if Some(reason) == p.scale_flat_field {
782 s.config.scale_flat_field = params.value.as_f64();
783 } else if Some(reason) == p.enable_offset_scale {
784 s.config.enable_offset_scale = params.value.as_i32() != 0;
785 } else if Some(reason) == p.auto_offset_scale {
786 if params.value.as_i32() != 0 {
787 s.config.auto_offset_scale_pending = true;
792 if let Some(idx) = p.auto_offset_scale {
793 updates.push(ParamUpdate::int32(idx, 0));
794 }
795 }
796 } else if Some(reason) == p.offset {
797 s.config.offset = params.value.as_f64();
798 } else if Some(reason) == p.scale {
799 s.config.scale = params.value.as_f64();
800 } else if Some(reason) == p.enable_low_clip {
801 s.config.enable_low_clip = params.value.as_i32() != 0;
802 } else if Some(reason) == p.low_clip_thresh {
803 s.config.low_clip_thresh = params.value.as_f64();
804 } else if Some(reason) == p.low_clip_value {
805 s.config.low_clip_value = params.value.as_f64();
806 } else if Some(reason) == p.enable_high_clip {
807 s.config.enable_high_clip = params.value.as_i32() != 0;
808 } else if Some(reason) == p.high_clip_thresh {
809 s.config.high_clip_thresh = params.value.as_f64();
810 } else if Some(reason) == p.high_clip_value {
811 s.config.high_clip_value = params.value.as_f64();
812 } else if Some(reason) == p.enable_filter {
813 s.config.enable_filter = params.value.as_i32() != 0;
814 } else if Some(reason) == p.filter_type {
815 s.apply_filter_type(params.value.as_i32());
816 s.reset_filter();
817 let fc = &s.config.filter;
819 for (i, idx) in p.fc.iter().enumerate() {
820 if let Some(idx) = *idx {
821 updates.push(ParamUpdate::float64(idx, fc.fc[i]));
822 }
823 }
824 for (i, idx) in p.oc.iter().enumerate() {
825 if let Some(idx) = *idx {
826 updates.push(ParamUpdate::float64(idx, fc.oc[i]));
827 }
828 }
829 for (i, idx) in p.rc.iter().enumerate() {
830 if let Some(idx) = *idx {
831 updates.push(ParamUpdate::float64(idx, fc.rc[i]));
832 }
833 }
834 if let Some(idx) = p.f_offset {
835 updates.push(ParamUpdate::float64(idx, fc.f_offset));
836 }
837 if let Some(idx) = p.f_scale {
838 updates.push(ParamUpdate::float64(idx, fc.f_scale));
839 }
840 if let Some(idx) = p.o_offset {
841 updates.push(ParamUpdate::float64(idx, fc.o_offset));
842 }
843 if let Some(idx) = p.o_scale {
844 updates.push(ParamUpdate::float64(idx, fc.o_scale));
845 }
846 } else if Some(reason) == p.reset_filter {
847 if params.value.as_i32() != 0 {
848 s.reset_filter();
849 if let Some(idx) = p.num_filtered {
850 updates.push(ParamUpdate::int32(idx, 0));
851 }
852 }
853 } else if Some(reason) == p.auto_reset_filter {
854 s.config.filter.auto_reset = params.value.as_i32() != 0;
855 } else if Some(reason) == p.filter_callbacks {
856 s.config.filter.filter_callbacks = params.value.as_i32().max(0) as usize;
857 } else if Some(reason) == p.num_filter {
858 s.config.filter.num_filter = params.value.as_i32().max(1) as usize;
859 } else if Some(reason) == p.o_offset {
860 s.config.filter.o_offset = params.value.as_f64();
861 } else if Some(reason) == p.o_scale {
862 s.config.filter.o_scale = params.value.as_f64();
863 } else if Some(reason) == p.f_offset {
864 s.config.filter.f_offset = params.value.as_f64();
865 } else if Some(reason) == p.f_scale {
866 s.config.filter.f_scale = params.value.as_f64();
867 } else if Some(reason) == p.r_offset {
868 s.config.filter.r_offset = params.value.as_f64();
869 } else {
870 for i in 0..4 {
872 if Some(reason) == p.oc[i] {
873 s.config.filter.oc[i] = params.value.as_f64();
874 return ParamChangeResult::updates(vec![]);
875 }
876 if Some(reason) == p.fc[i] {
877 s.config.filter.fc[i] = params.value.as_f64();
878 return ParamChangeResult::updates(vec![]);
879 }
880 }
881 for i in 0..2 {
882 if Some(reason) == p.rc[i] {
883 s.config.filter.rc[i] = params.value.as_f64();
884 return ParamChangeResult::updates(vec![]);
885 }
886 }
887 }
888
889 ParamChangeResult::updates(updates)
890 }
891}
892
893#[cfg(test)]
894mod tests {
895 use super::*;
896 use ad_core_rs::ndarray::{NDDataBuffer, NDDimension};
897
898 fn make_array(vals: &[u8]) -> NDArray {
899 let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::UInt8);
900 if let NDDataBuffer::U8(ref mut v) = arr.data {
901 v.copy_from_slice(vals);
902 }
903 arr
904 }
905
906 fn make_f64_array(vals: &[f64]) -> NDArray {
907 let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::Float64);
908 if let NDDataBuffer::F64(ref mut v) = arr.data {
909 v.copy_from_slice(vals);
910 }
911 arr
912 }
913
914 #[test]
915 fn test_background_subtraction() {
916 let bg_arr = make_array(&[10, 20, 30]);
917 let input = make_array(&[15, 25, 35]);
918
919 let mut state = ProcessState::new(ProcessConfig {
920 enable_background: true,
921 ..Default::default()
922 });
923 state.save_background(&bg_arr);
924
925 let result = state.process(&input).unwrap();
926 if let NDDataBuffer::U8(ref v) = result.data {
927 assert_eq!(v[0], 5);
928 assert_eq!(v[1], 5);
929 assert_eq!(v[2], 5);
930 }
931 }
932
933 #[test]
934 fn test_adp7_size_mismatched_background_invalidated_not_partial() {
935 let bg_arr = make_array(&[10, 20]); let input = make_array(&[15, 25, 35]); let mut state = ProcessState::new(ProcessConfig {
942 enable_background: true,
943 ..Default::default()
944 });
945 state.save_background(&bg_arr);
946 assert!(state.config.valid_background); let result = state.process(&input).unwrap();
949 assert!(!state.config.valid_background);
952 if let NDDataBuffer::U8(ref v) = result.data {
953 assert_eq!(v, &[15, 25, 35]);
954 } else {
955 panic!("expected U8 output");
956 }
957 }
958
959 #[test]
960 fn test_flat_field() {
961 let ff_arr = make_array(&[100, 200, 50]);
964 let input = make_array(&[100, 100, 100]);
965
966 let mut state = ProcessState::new(ProcessConfig {
967 enable_flat_field: true,
968 scale_flat_field: 100.0,
969 ..Default::default()
970 });
971 state.save_flat_field(&ff_arr);
972
973 let result = state.process(&input).unwrap();
974 if let NDDataBuffer::U8(ref v) = result.data {
975 assert_eq!(v[0], 100); assert_eq!(v[1], 50); assert_eq!(v[2], 200); } else {
979 panic!("expected U8 output");
980 }
981 }
982
983 #[test]
984 fn test_adp24_scale_flat_field_zero_zeroes_output() {
985 let ff_arr = make_array(&[100, 200, 50]);
989 let input = make_array(&[100, 100, 100]);
990 let mut state = ProcessState::new(ProcessConfig {
991 enable_flat_field: true,
992 scale_flat_field: 0.0,
993 ..Default::default()
994 });
995 state.save_flat_field(&ff_arr);
996 let result = state.process(&input).unwrap();
997 if let NDDataBuffer::U8(ref v) = result.data {
998 assert_eq!(v, &[0, 0, 0]);
999 } else {
1000 panic!("expected U8 output");
1001 }
1002 }
1003
1004 #[test]
1005 fn test_offset_scale() {
1006 let input = make_array(&[10, 20, 30]);
1007 let mut state = ProcessState::new(ProcessConfig {
1008 enable_offset_scale: true,
1009 scale: 2.0,
1010 offset: 5.0,
1011 ..Default::default()
1012 });
1013
1014 let result = state.process(&input).unwrap();
1015 if let NDDataBuffer::U8(ref v) = result.data {
1016 assert_eq!(v[0], 30); assert_eq!(v[1], 50); assert_eq!(v[2], 70); }
1021 }
1022
1023 #[test]
1024 fn test_clipping() {
1025 let input = make_array(&[5, 50, 200]);
1026 let mut state = ProcessState::new(ProcessConfig {
1027 enable_low_clip: true,
1028 low_clip_thresh: 10.0,
1029 low_clip_value: 10.0,
1030 enable_high_clip: true,
1031 high_clip_thresh: 100.0,
1032 high_clip_value: 100.0,
1033 ..Default::default()
1034 });
1035
1036 let result = state.process(&input).unwrap();
1037 if let NDDataBuffer::U8(ref v) = result.data {
1038 assert_eq!(v[0], 10); assert_eq!(v[1], 50); assert_eq!(v[2], 100); }
1042 }
1043
1044 #[test]
1045 fn test_adp5_clip_order_high_before_low() {
1046 let input = make_f64_array(&[200.0]);
1051 let mut state = ProcessState::new(ProcessConfig {
1052 enable_high_clip: true,
1053 high_clip_thresh: 100.0,
1054 high_clip_value: 10.0,
1055 enable_low_clip: true,
1056 low_clip_thresh: 50.0,
1057 low_clip_value: 999.0,
1058 ..Default::default()
1059 });
1060 let result = state.process(&input).unwrap();
1061 if let NDDataBuffer::F64(ref v) = result.data {
1062 assert_eq!(v[0], 999.0);
1063 } else {
1064 panic!("expected F64 output");
1065 }
1066 }
1067
1068 #[test]
1069 fn test_recursive_filter() {
1070 let input1 = make_array(&[100, 100, 100]);
1076 let input2 = make_array(&[0, 0, 0]);
1077
1078 let mut state = ProcessState::new(ProcessConfig {
1079 enable_filter: true,
1080 filter: FilterConfig {
1081 num_filter: 10,
1082 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()
1086 },
1087 ..Default::default()
1088 });
1089
1090 let _ = state.process(&input1);
1101
1102 let result = state.process(&input2).unwrap();
1107 if let NDDataBuffer::U8(ref v) = result.data {
1108 assert_eq!(v[0], 100);
1110 assert_eq!(v[1], 100);
1111 }
1112 }
1113
1114 #[test]
1115 fn test_output_type_conversion() {
1116 let input = make_array(&[10, 20, 30]);
1117 let mut state = ProcessState::new(ProcessConfig {
1118 output_type: Some(NDDataType::Float64),
1119 ..Default::default()
1120 });
1121
1122 let result = state.process(&input).unwrap();
1123 assert_eq!(result.data.data_type(), NDDataType::Float64);
1124 }
1125
1126 #[test]
1129 fn test_process_processor() {
1130 let mut proc = ProcessProcessor::new(ProcessConfig {
1131 enable_offset_scale: true,
1132 scale: 2.0,
1133 offset: 1.0,
1134 ..Default::default()
1135 });
1136 let pool = NDArrayPool::new(1_000_000);
1137
1138 let input = make_array(&[10, 20, 30]);
1139 let result = proc.process_array(&input, &pool);
1140 assert_eq!(result.output_arrays.len(), 1);
1141 if let NDDataBuffer::U8(ref v) = result.output_arrays[0].data {
1142 assert_eq!(v[0], 22); }
1144 }
1145
1146 #[test]
1149 fn test_filter_sum_preset() {
1150 let mut state = ProcessState::new(ProcessConfig {
1153 enable_filter: true,
1154 filter: FilterConfig {
1155 num_filter: 10,
1156 fc: [1.0, 0.0, 1.0, 0.0],
1157 oc: [1.0, 0.0, 0.0, 0.0],
1158 rc: [0.0, 1.0],
1159 ..Default::default()
1160 },
1161 output_type: Some(NDDataType::Float64),
1162 ..Default::default()
1163 });
1164
1165 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1177 let v0 = r0.data.get_as_f64(0).unwrap();
1178 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1179
1180 let r1 = state.process(&make_f64_array(&[100.0])).unwrap();
1187 let v1 = r1.data.get_as_f64(0).unwrap();
1188 assert!((v1 - 200.0).abs() < 1e-9, "frame 1: got {v1}");
1189 }
1190
1191 #[test]
1192 fn test_filter_average_preset() {
1193 let mut state = ProcessState::new(ProcessConfig {
1196 enable_filter: true,
1197 filter: FilterConfig {
1198 num_filter: 10,
1199 fc: [1.0, 0.0, 1.0, 0.0],
1200 oc: [0.0, 1.0, 0.0, 0.0],
1201 rc: [0.0, 1.0],
1202 ..Default::default()
1203 },
1204 output_type: Some(NDDataType::Float64),
1205 ..Default::default()
1206 });
1207
1208 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1217 let v0 = r0.data.get_as_f64(0).unwrap();
1218 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1219
1220 let r1 = state.process(&make_f64_array(&[200.0])).unwrap();
1225 let v1 = r1.data.get_as_f64(0).unwrap();
1226 assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
1227
1228 let r2 = state.process(&make_f64_array(&[300.0])).unwrap();
1233 let v2 = r2.data.get_as_f64(0).unwrap();
1234 let expected = 400.0 / 3.0;
1235 assert!((v2 - expected).abs() < 1e-9, "frame 2: got {v2}");
1236 }
1237
1238 #[test]
1239 fn test_filter_recursive_ave() {
1240 let mut state = ProcessState::new(ProcessConfig {
1245 enable_filter: true,
1246 filter: FilterConfig {
1247 num_filter: 10,
1248 fc: [1.0, -1.0, 0.0, 1.0],
1249 oc: [1.0, 0.0, 0.0, 0.0],
1250 rc: [0.0, 1.0],
1251 ..Default::default()
1252 },
1253 output_type: Some(NDDataType::Float64),
1254 ..Default::default()
1255 });
1256
1257 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1269 let v0 = r0.data.get_as_f64(0).unwrap();
1270 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1271
1272 let r1 = state.process(&make_f64_array(&[200.0])).unwrap();
1277 let v1 = r1.data.get_as_f64(0).unwrap();
1278 assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
1279
1280 let r2 = state.process(&make_f64_array(&[300.0])).unwrap();
1285 let v2 = r2.data.get_as_f64(0).unwrap();
1286 assert!((v2 - 150.0).abs() < 1e-9, "frame 2: got {v2}");
1287 }
1288
1289 #[test]
1290 fn test_save_background_one_shot() {
1291 let mut state = ProcessState::new(ProcessConfig {
1292 save_background: true,
1293 ..Default::default()
1294 });
1295
1296 assert!(!state.config.valid_background);
1297 assert!(state.background.is_none());
1298
1299 let input = make_array(&[10, 20, 30]);
1301 let _ = state.process(&input);
1302
1303 assert!(
1304 !state.config.save_background,
1305 "save_background should be cleared"
1306 );
1307 assert!(
1308 state.config.valid_background,
1309 "valid_background should be set"
1310 );
1311 assert!(state.background.is_some());
1312
1313 let bg = state.background.as_ref().unwrap();
1314 assert_eq!(bg.len(), 3);
1315 assert!((bg[0] - 10.0).abs() < 1e-9);
1316 assert!((bg[1] - 20.0).abs() < 1e-9);
1317 assert!((bg[2] - 30.0).abs() < 1e-9);
1318
1319 let input2 = make_array(&[40, 50, 60]);
1321 let _ = state.process(&input2);
1322
1323 assert!(
1324 !state.config.save_background,
1325 "save_background stays cleared"
1326 );
1327 let bg2 = state.background.as_ref().unwrap();
1329 assert!((bg2[0] - 10.0).abs() < 1e-9);
1330 }
1331
1332 #[test]
1333 fn test_save_flat_field_one_shot() {
1334 let mut state = ProcessState::new(ProcessConfig {
1335 save_flat_field: true,
1336 ..Default::default()
1337 });
1338
1339 assert!(!state.config.valid_flat_field);
1340 assert!(state.flat_field.is_none());
1341
1342 let input = make_array(&[50, 100, 150]);
1343 let _ = state.process(&input);
1344
1345 assert!(
1346 !state.config.save_flat_field,
1347 "save_flat_field should be cleared"
1348 );
1349 assert!(
1350 state.config.valid_flat_field,
1351 "valid_flat_field should be set"
1352 );
1353 assert!(state.flat_field.is_some());
1354
1355 let ff = state.flat_field.as_ref().unwrap();
1356 assert_eq!(ff.len(), 3);
1357 assert!((ff[0] - 50.0).abs() < 1e-9);
1358 assert!((ff[1] - 100.0).abs() < 1e-9);
1359 assert!((ff[2] - 150.0).abs() < 1e-9);
1360 }
1361
1362 #[test]
1363 fn test_auto_reset_when_num_filter_reached() {
1364 let mut state = ProcessState::new(ProcessConfig {
1366 enable_filter: true,
1367 filter: FilterConfig {
1368 num_filter: 3,
1369 auto_reset: true,
1370 fc: [1.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0],
1372 rc: [0.0, 1.0],
1373 ..Default::default()
1374 },
1375 output_type: Some(NDDataType::Float64),
1376 ..Default::default()
1377 });
1378
1379 let _ = state.process(&make_f64_array(&[100.0]));
1381 assert_eq!(state.num_filtered, 1);
1382
1383 let _ = state.process(&make_f64_array(&[100.0]));
1385 assert_eq!(state.num_filtered, 2);
1386
1387 let _ = state.process(&make_f64_array(&[100.0]));
1389 assert_eq!(state.num_filtered, 3);
1390
1391 let _ = state.process(&make_f64_array(&[200.0]));
1393 assert_eq!(state.num_filtered, 1, "fresh start after auto reset");
1395 }
1396
1397 #[test]
1398 fn test_filter_with_offset_scale() {
1399 let mut state = ProcessState::new(ProcessConfig {
1403 enable_filter: true,
1404 filter: FilterConfig {
1405 num_filter: 10,
1406 fc: [0.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0], rc: [0.0, 1.0],
1409 f_offset: 10.0,
1410 f_scale: 2.0,
1411 o_offset: 5.0,
1412 o_scale: 3.0,
1413 ..Default::default()
1414 },
1415 output_type: Some(NDDataType::Float64),
1416 ..Default::default()
1417 });
1418
1419 let r0 = state.process(&make_f64_array(&[50.0])).unwrap();
1429 let v0 = r0.data.get_as_f64(0).unwrap();
1430 assert!((v0 - 155.0).abs() < 1e-9, "frame 0: got {v0}");
1431
1432 let r1 = state.process(&make_f64_array(&[20.0])).unwrap();
1437 let v1 = r1.data.get_as_f64(0).unwrap();
1438 assert!((v1 - 335.0).abs() < 1e-9, "frame 1: got {v1}");
1439 }
1440
1441 #[test]
1442 fn test_reset_filter_manual() {
1443 let mut state = ProcessState::new(ProcessConfig {
1444 enable_filter: true,
1445 filter: FilterConfig {
1446 num_filter: 10,
1447 fc: [1.0, 0.0, 1.0, 0.0],
1448 oc: [1.0, 0.0, 0.0, 0.0],
1449 rc: [0.0, 1.0],
1450 ..Default::default()
1451 },
1452 output_type: Some(NDDataType::Float64),
1453 ..Default::default()
1454 });
1455
1456 let _ = state.process(&make_f64_array(&[100.0]));
1458 let _ = state.process(&make_f64_array(&[100.0]));
1459 assert!(state.filter_state.is_some());
1460 assert_eq!(state.num_filtered, 2);
1461
1462 state.reset_filter();
1464 assert!(state.filter_state.is_none());
1465 assert_eq!(state.num_filtered, 0);
1466
1467 let _ = state.process(&make_f64_array(&[200.0]));
1469 assert_eq!(state.num_filtered, 1);
1470 }
1471
1472 #[test]
1473 fn test_adp6_auto_offset_scale_arms_next_frame_not_trigger() {
1474 let mut state = ProcessState::new(ProcessConfig {
1479 output_type: Some(NDDataType::UInt8),
1480 ..Default::default()
1481 });
1482 state.config.auto_offset_scale_pending = true;
1483
1484 let out1 = state.process(&make_f64_array(&[10.0, 20.0, 30.0])).unwrap();
1487 assert!(!state.config.auto_offset_scale_pending); if let NDDataBuffer::U8(v) = &out1.data {
1489 assert_eq!(v, &[10, 20, 30]); } else {
1491 panic!("expected u8 output");
1492 }
1493 assert!(state.config.enable_offset_scale);
1496 assert!((state.config.offset - (-10.0)).abs() < 1e-9);
1497 assert!((state.config.scale - 255.0 / 20.0).abs() < 1e-9);
1498
1499 let out2 = state.process(&make_f64_array(&[10.0, 20.0, 30.0])).unwrap();
1501 if let NDDataBuffer::U8(v) = &out2.data {
1502 assert_eq!(v[0], 0); assert_eq!(v[2], 255); } else {
1505 panic!("expected u8 output");
1506 }
1507 }
1508
1509 #[test]
1510 fn test_filter_callbacks_drops_suppressed_frame() {
1511 let mut state = ProcessState::new(ProcessConfig {
1515 enable_filter: true,
1516 filter: FilterConfig {
1517 num_filter: 3,
1518 filter_callbacks: 1,
1519 fc: [1.0, 0.0, 1.0, 0.0],
1520 oc: [0.0, 1.0, 0.0, 0.0],
1521 rc: [0.0, 1.0],
1522 ..Default::default()
1523 },
1524 output_type: Some(NDDataType::Float64),
1525 ..Default::default()
1526 });
1527
1528 assert!(state.process(&make_f64_array(&[100.0])).is_none());
1530 assert!(state.process(&make_f64_array(&[100.0])).is_none());
1531 assert!(state.process(&make_f64_array(&[100.0])).is_some());
1533 }
1534
1535 #[test]
1536 fn test_filter_recurrence_matches_cpp() {
1537 let mut state = ProcessState::new(ProcessConfig {
1565 enable_filter: true,
1566 filter: FilterConfig {
1567 num_filter: 100,
1568 fc: [1.0, 0.0, 1.0, 0.0],
1569 oc: [0.0, 1.0, 0.0, 0.0],
1570 rc: [0.0, 1.0],
1571 ..Default::default()
1572 },
1573 output_type: Some(NDDataType::Float64),
1574 ..Default::default()
1575 });
1576
1577 let inputs = [100.0, 200.0, 300.0, 400.0];
1578 let expected_data = [100.0, 100.0, 400.0 / 3.0, 175.0];
1579 let expected_filter = [200.0, 400.0, 700.0, 1100.0];
1580
1581 for k in 0..inputs.len() {
1582 let r = state.process(&make_f64_array(&[inputs[k]])).unwrap();
1583 let v = r.data.get_as_f64(0).unwrap();
1584 assert!(
1585 (v - expected_data[k]).abs() < 1e-9,
1586 "frame {k}: data got {v}, expected {}",
1587 expected_data[k]
1588 );
1589 let fs = state.filter_state.as_ref().unwrap()[0];
1590 assert!(
1591 (fs - expected_filter[k]).abs() < 1e-9,
1592 "frame {k}: filter got {fs}, expected {}",
1593 expected_filter[k]
1594 );
1595 }
1596 }
1597}