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 if self.config.auto_offset_scale_pending {
349 self.auto_offset_scale(src);
350 self.config.auto_offset_scale_pending = false;
351 }
352
353 let needs_element_ops = self.config.enable_background
356 || self.config.enable_flat_field
357 || self.config.enable_offset_scale
358 || self.config.enable_low_clip
359 || self.config.enable_high_clip;
360
361 if needs_element_ops {
362 let bg = if self.config.enable_background {
363 self.background.as_ref()
364 } else {
365 None
366 };
367 let (ff, ff_scale) = if self.config.enable_flat_field {
368 if let Some(ref ff) = self.flat_field {
369 let scale = if self.config.scale_flat_field > 0.0 {
370 self.config.scale_flat_field
371 } else {
372 ff.iter().sum::<f64>() / ff.len().max(1) as f64
373 };
374 (Some(ff.as_slice()), scale)
375 } else {
376 (None, 0.0)
377 }
378 } else {
379 (None, 0.0)
380 };
381 let do_offset_scale = self.config.enable_offset_scale;
382 let scale = self.config.scale;
383 let offset = self.config.offset;
384 let do_low_clip = self.config.enable_low_clip;
385 let low_clip_thresh = self.config.low_clip_thresh;
386 let low_clip_value = self.config.low_clip_value;
387 let do_high_clip = self.config.enable_high_clip;
388 let high_clip_thresh = self.config.high_clip_thresh;
389 let high_clip_value = self.config.high_clip_value;
390
391 let apply_stages = |i: usize, v: &mut f64| {
392 if let Some(bg) = bg {
394 if i < bg.len() {
395 *v -= bg[i];
396 }
397 }
398 if let Some(ff) = ff {
400 if i < ff.len() && ff[i] != 0.0 {
401 *v = *v * ff_scale / ff[i];
402 }
403 }
404 if do_offset_scale {
406 *v = (*v + offset) * scale;
407 }
408 if do_low_clip && *v < low_clip_thresh {
410 *v = low_clip_value;
411 }
412 if do_high_clip && *v > high_clip_thresh {
413 *v = high_clip_value;
414 }
415 };
416
417 #[cfg(feature = "parallel")]
418 let use_parallel = par_util::should_parallelize(n);
419 #[cfg(not(feature = "parallel"))]
420 let use_parallel = false;
421
422 if use_parallel {
423 #[cfg(feature = "parallel")]
424 par_util::thread_pool().install(|| {
425 values.par_iter_mut().enumerate().for_each(|(i, v)| {
426 apply_stages(i, v);
427 });
428 });
429 } else {
430 for (i, v) in values.iter_mut().enumerate() {
431 apply_stages(i, v);
432 }
433 }
434 }
435
436 if self.config.enable_filter {
438 let fc = &self.config.filter;
439
440 if let Some(ref f) = self.filter_state {
442 if f.len() != n {
443 self.filter_state = None;
444 }
445 }
446
447 let mut reset_filter = self.filter_state.is_none();
448 if self.num_filtered >= fc.num_filter && fc.auto_reset {
449 reset_filter = true;
450 }
451
452 if self.filter_state.is_none() {
454 self.filter_state = Some(values.clone());
455 }
456
457 let filter = self.filter_state.as_mut().unwrap();
458
459 if reset_filter {
460 let r_offset = fc.r_offset;
462 let rc1 = fc.rc[0];
463 let rc2 = fc.rc[1];
464 for i in 0..n {
465 let new_filter = r_offset + rc1 * filter[i] + rc2 * values[i];
466 filter[i] = new_filter;
467 }
468 self.num_filtered = 0;
469 }
470
471 if self.num_filtered < fc.num_filter {
473 self.num_filtered += 1;
474 }
475
476 let nf = self.num_filtered as f64;
478 let o1 = fc.o_scale * (fc.oc[0] + fc.oc[1] / nf);
479 let o2 = fc.o_scale * (fc.oc[2] + fc.oc[3] / nf);
480 let f1 = fc.f_scale * (fc.fc[0] + fc.fc[1] / nf);
481 let f2 = fc.f_scale * (fc.fc[2] + fc.fc[3] / nf);
482 let o_offset = fc.o_offset;
483 let f_offset = fc.f_offset;
484
485 for i in 0..n {
494 let new_data = o_offset + o1 * filter[i] + o2 * values[i];
495 let new_filter = f_offset + f1 * filter[i] + f2 * values[i];
496 values[i] = new_data;
497 filter[i] = new_filter;
498 }
499
500 if fc.filter_callbacks > 0 && self.num_filtered != fc.num_filter {
505 return None;
506 }
507 }
508
509 let out_type = self.config.output_type.unwrap_or(src.data.data_type());
511 let mut out_data = NDDataBuffer::zeros(out_type, n);
512 for i in 0..n {
513 out_data.set_from_f64(i, values[i]);
514 }
515
516 let mut arr = NDArray::new(src.dims.clone(), out_type);
517 arr.data = out_data;
518 arr.unique_id = src.unique_id;
519 arr.timestamp = src.timestamp;
520 arr.attributes = src.attributes.clone();
521 Some(arr)
522 }
523}
524
525#[derive(Default)]
529struct ProcParamIndices {
530 data_type: Option<usize>,
531 save_background: Option<usize>,
532 enable_background: Option<usize>,
533 valid_background: Option<usize>,
534 save_flat_field: Option<usize>,
535 enable_flat_field: Option<usize>,
536 valid_flat_field: Option<usize>,
537 scale_flat_field: Option<usize>,
538 enable_offset_scale: Option<usize>,
539 auto_offset_scale: Option<usize>,
540 offset: Option<usize>,
541 scale: Option<usize>,
542 enable_low_clip: Option<usize>,
543 low_clip_thresh: Option<usize>,
544 low_clip_value: Option<usize>,
545 enable_high_clip: Option<usize>,
546 high_clip_thresh: Option<usize>,
547 high_clip_value: Option<usize>,
548 enable_filter: Option<usize>,
549 filter_type: Option<usize>,
550 reset_filter: Option<usize>,
551 auto_reset_filter: Option<usize>,
552 filter_callbacks: Option<usize>,
553 num_filter: Option<usize>,
554 num_filtered: Option<usize>,
555 o_offset: Option<usize>,
556 o_scale: Option<usize>,
557 oc: [Option<usize>; 4],
558 f_offset: Option<usize>,
559 f_scale: Option<usize>,
560 fc: [Option<usize>; 4],
561 r_offset: Option<usize>,
562 rc: [Option<usize>; 2],
563}
564
565pub struct ProcessProcessor {
567 state: ProcessState,
568 params: ProcParamIndices,
569}
570
571impl ProcessProcessor {
572 pub fn new(config: ProcessConfig) -> Self {
573 Self {
574 state: ProcessState::new(config),
575 params: ProcParamIndices::default(),
576 }
577 }
578
579 pub fn state(&self) -> &ProcessState {
580 &self.state
581 }
582
583 pub fn state_mut(&mut self) -> &mut ProcessState {
584 &mut self.state
585 }
586}
587
588impl NDPluginProcess for ProcessProcessor {
589 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
590 use ad_core_rs::plugin::runtime::ParamUpdate;
591
592 let out = self.state.process(array);
593 let mut result = match out {
596 Some(arr) => ProcessResult::arrays(vec![Arc::new(arr)]),
597 None => ProcessResult::sink(vec![]),
598 };
599
600 if let Some(idx) = self.params.valid_background {
602 result.param_updates.push(ParamUpdate::int32(
603 idx,
604 if self.state.config.valid_background {
605 1
606 } else {
607 0
608 },
609 ));
610 }
611 if let Some(idx) = self.params.valid_flat_field {
612 result.param_updates.push(ParamUpdate::int32(
613 idx,
614 if self.state.config.valid_flat_field {
615 1
616 } else {
617 0
618 },
619 ));
620 }
621 if let Some(idx) = self.params.num_filtered {
622 result
623 .param_updates
624 .push(ParamUpdate::int32(idx, self.state.num_filtered as i32));
625 }
626 if let Some(idx) = self.params.save_background {
628 result.param_updates.push(ParamUpdate::int32(idx, 0));
629 }
630 if let Some(idx) = self.params.save_flat_field {
631 result.param_updates.push(ParamUpdate::int32(idx, 0));
632 }
633
634 result
635 }
636
637 fn plugin_type(&self) -> &str {
638 "NDPluginProcess"
639 }
640
641 fn register_params(
642 &mut self,
643 base: &mut asyn_rs::port::PortDriverBase,
644 ) -> asyn_rs::error::AsynResult<()> {
645 use asyn_rs::param::ParamType;
646 base.create_param("PROCESS_DATA_TYPE", ParamType::Int32)?;
647 base.create_param("SAVE_BACKGROUND", ParamType::Int32)?;
648 base.create_param("ENABLE_BACKGROUND", ParamType::Int32)?;
649 base.create_param("VALID_BACKGROUND", ParamType::Int32)?;
650 base.create_param("SAVE_FLAT_FIELD", ParamType::Int32)?;
651 base.create_param("ENABLE_FLAT_FIELD", ParamType::Int32)?;
652 base.create_param("VALID_FLAT_FIELD", ParamType::Int32)?;
653 base.create_param("SCALE_FLAT_FIELD", ParamType::Float64)?;
654 base.create_param("ENABLE_OFFSET_SCALE", ParamType::Int32)?;
655 base.create_param("AUTO_OFFSET_SCALE", ParamType::Int32)?;
656 base.create_param("OFFSET", ParamType::Float64)?;
657 base.create_param("SCALE", ParamType::Float64)?;
658 base.create_param("ENABLE_LOW_CLIP", ParamType::Int32)?;
659 base.create_param("LOW_CLIP_THRESH", ParamType::Float64)?;
660 base.create_param("LOW_CLIP_VALUE", ParamType::Float64)?;
661 base.create_param("ENABLE_HIGH_CLIP", ParamType::Int32)?;
662 base.create_param("HIGH_CLIP_THRESH", ParamType::Float64)?;
663 base.create_param("HIGH_CLIP_VALUE", ParamType::Float64)?;
664 base.create_param("ENABLE_FILTER", ParamType::Int32)?;
665 base.create_param("FILTER_TYPE", ParamType::Int32)?;
666 base.create_param("RESET_FILTER", ParamType::Int32)?;
667 base.create_param("AUTO_RESET_FILTER", ParamType::Int32)?;
668 base.create_param("FILTER_CALLBACKS", ParamType::Int32)?;
669 base.create_param("NUM_FILTER", ParamType::Int32)?;
670 base.create_param("NUM_FILTERED", ParamType::Int32)?;
671 base.create_param("FILTER_OOFFSET", ParamType::Float64)?;
672 base.create_param("FILTER_OSCALE", ParamType::Float64)?;
673 base.create_param("FILTER_OC1", ParamType::Float64)?;
674 base.create_param("FILTER_OC2", ParamType::Float64)?;
675 base.create_param("FILTER_OC3", ParamType::Float64)?;
676 base.create_param("FILTER_OC4", ParamType::Float64)?;
677 base.create_param("FILTER_FOFFSET", ParamType::Float64)?;
678 base.create_param("FILTER_FSCALE", ParamType::Float64)?;
679 base.create_param("FILTER_FC1", ParamType::Float64)?;
680 base.create_param("FILTER_FC2", ParamType::Float64)?;
681 base.create_param("FILTER_FC3", ParamType::Float64)?;
682 base.create_param("FILTER_FC4", ParamType::Float64)?;
683 base.create_param("FILTER_ROFFSET", ParamType::Float64)?;
684 base.create_param("FILTER_RC1", ParamType::Float64)?;
685 base.create_param("FILTER_RC2", ParamType::Float64)?;
686
687 self.params.data_type = base.find_param("PROCESS_DATA_TYPE");
689 self.params.save_background = base.find_param("SAVE_BACKGROUND");
690 self.params.enable_background = base.find_param("ENABLE_BACKGROUND");
691 self.params.valid_background = base.find_param("VALID_BACKGROUND");
692 self.params.save_flat_field = base.find_param("SAVE_FLAT_FIELD");
693 self.params.enable_flat_field = base.find_param("ENABLE_FLAT_FIELD");
694 self.params.valid_flat_field = base.find_param("VALID_FLAT_FIELD");
695 self.params.scale_flat_field = base.find_param("SCALE_FLAT_FIELD");
696 self.params.enable_offset_scale = base.find_param("ENABLE_OFFSET_SCALE");
697 self.params.auto_offset_scale = base.find_param("AUTO_OFFSET_SCALE");
698 self.params.offset = base.find_param("OFFSET");
699 self.params.scale = base.find_param("SCALE");
700 self.params.enable_low_clip = base.find_param("ENABLE_LOW_CLIP");
701 self.params.low_clip_thresh = base.find_param("LOW_CLIP_THRESH");
702 self.params.low_clip_value = base.find_param("LOW_CLIP_VALUE");
703 self.params.enable_high_clip = base.find_param("ENABLE_HIGH_CLIP");
704 self.params.high_clip_thresh = base.find_param("HIGH_CLIP_THRESH");
705 self.params.high_clip_value = base.find_param("HIGH_CLIP_VALUE");
706 self.params.enable_filter = base.find_param("ENABLE_FILTER");
707 self.params.filter_type = base.find_param("FILTER_TYPE");
708 self.params.reset_filter = base.find_param("RESET_FILTER");
709 self.params.auto_reset_filter = base.find_param("AUTO_RESET_FILTER");
710 self.params.filter_callbacks = base.find_param("FILTER_CALLBACKS");
711 self.params.num_filter = base.find_param("NUM_FILTER");
712 self.params.num_filtered = base.find_param("NUM_FILTERED");
713 self.params.o_offset = base.find_param("FILTER_OOFFSET");
714 self.params.o_scale = base.find_param("FILTER_OSCALE");
715 self.params.oc[0] = base.find_param("FILTER_OC1");
716 self.params.oc[1] = base.find_param("FILTER_OC2");
717 self.params.oc[2] = base.find_param("FILTER_OC3");
718 self.params.oc[3] = base.find_param("FILTER_OC4");
719 self.params.f_offset = base.find_param("FILTER_FOFFSET");
720 self.params.f_scale = base.find_param("FILTER_FSCALE");
721 self.params.fc[0] = base.find_param("FILTER_FC1");
722 self.params.fc[1] = base.find_param("FILTER_FC2");
723 self.params.fc[2] = base.find_param("FILTER_FC3");
724 self.params.fc[3] = base.find_param("FILTER_FC4");
725 self.params.r_offset = base.find_param("FILTER_ROFFSET");
726 self.params.rc[0] = base.find_param("FILTER_RC1");
727 self.params.rc[1] = base.find_param("FILTER_RC2");
728 Ok(())
729 }
730
731 fn on_param_change(
732 &mut self,
733 reason: usize,
734 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
735 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
736 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamUpdate};
737
738 let s = &mut self.state;
739 let p = &self.params;
740 let mut updates = Vec::new();
741
742 if Some(reason) == p.data_type {
743 let v = params.value.as_i32();
744 s.config.output_type = if v < 0 {
745 None } else {
747 NDDataType::from_ordinal(v as u8)
748 };
749 } else if Some(reason) == p.save_background {
750 if params.value.as_i32() != 0 {
751 s.config.save_background = true;
752 }
753 } else if Some(reason) == p.enable_background {
754 s.config.enable_background = params.value.as_i32() != 0;
755 } else if Some(reason) == p.save_flat_field {
756 if params.value.as_i32() != 0 {
757 s.config.save_flat_field = true;
758 }
759 } else if Some(reason) == p.enable_flat_field {
760 s.config.enable_flat_field = params.value.as_i32() != 0;
761 } else if Some(reason) == p.scale_flat_field {
762 s.config.scale_flat_field = params.value.as_f64();
763 } else if Some(reason) == p.enable_offset_scale {
764 s.config.enable_offset_scale = params.value.as_i32() != 0;
765 } else if Some(reason) == p.auto_offset_scale {
766 if params.value.as_i32() != 0 {
767 s.config.auto_offset_scale_pending = true;
772 if let Some(idx) = p.auto_offset_scale {
773 updates.push(ParamUpdate::int32(idx, 0));
774 }
775 }
776 } else if Some(reason) == p.offset {
777 s.config.offset = params.value.as_f64();
778 } else if Some(reason) == p.scale {
779 s.config.scale = params.value.as_f64();
780 } else if Some(reason) == p.enable_low_clip {
781 s.config.enable_low_clip = params.value.as_i32() != 0;
782 } else if Some(reason) == p.low_clip_thresh {
783 s.config.low_clip_thresh = params.value.as_f64();
784 } else if Some(reason) == p.low_clip_value {
785 s.config.low_clip_value = params.value.as_f64();
786 } else if Some(reason) == p.enable_high_clip {
787 s.config.enable_high_clip = params.value.as_i32() != 0;
788 } else if Some(reason) == p.high_clip_thresh {
789 s.config.high_clip_thresh = params.value.as_f64();
790 } else if Some(reason) == p.high_clip_value {
791 s.config.high_clip_value = params.value.as_f64();
792 } else if Some(reason) == p.enable_filter {
793 s.config.enable_filter = params.value.as_i32() != 0;
794 } else if Some(reason) == p.filter_type {
795 s.apply_filter_type(params.value.as_i32());
796 s.reset_filter();
797 let fc = &s.config.filter;
799 for (i, idx) in p.fc.iter().enumerate() {
800 if let Some(idx) = *idx {
801 updates.push(ParamUpdate::float64(idx, fc.fc[i]));
802 }
803 }
804 for (i, idx) in p.oc.iter().enumerate() {
805 if let Some(idx) = *idx {
806 updates.push(ParamUpdate::float64(idx, fc.oc[i]));
807 }
808 }
809 for (i, idx) in p.rc.iter().enumerate() {
810 if let Some(idx) = *idx {
811 updates.push(ParamUpdate::float64(idx, fc.rc[i]));
812 }
813 }
814 if let Some(idx) = p.f_offset {
815 updates.push(ParamUpdate::float64(idx, fc.f_offset));
816 }
817 if let Some(idx) = p.f_scale {
818 updates.push(ParamUpdate::float64(idx, fc.f_scale));
819 }
820 if let Some(idx) = p.o_offset {
821 updates.push(ParamUpdate::float64(idx, fc.o_offset));
822 }
823 if let Some(idx) = p.o_scale {
824 updates.push(ParamUpdate::float64(idx, fc.o_scale));
825 }
826 } else if Some(reason) == p.reset_filter {
827 if params.value.as_i32() != 0 {
828 s.reset_filter();
829 if let Some(idx) = p.num_filtered {
830 updates.push(ParamUpdate::int32(idx, 0));
831 }
832 }
833 } else if Some(reason) == p.auto_reset_filter {
834 s.config.filter.auto_reset = params.value.as_i32() != 0;
835 } else if Some(reason) == p.filter_callbacks {
836 s.config.filter.filter_callbacks = params.value.as_i32().max(0) as usize;
837 } else if Some(reason) == p.num_filter {
838 s.config.filter.num_filter = params.value.as_i32().max(1) as usize;
839 } else if Some(reason) == p.o_offset {
840 s.config.filter.o_offset = params.value.as_f64();
841 } else if Some(reason) == p.o_scale {
842 s.config.filter.o_scale = params.value.as_f64();
843 } else if Some(reason) == p.f_offset {
844 s.config.filter.f_offset = params.value.as_f64();
845 } else if Some(reason) == p.f_scale {
846 s.config.filter.f_scale = params.value.as_f64();
847 } else if Some(reason) == p.r_offset {
848 s.config.filter.r_offset = params.value.as_f64();
849 } else {
850 for i in 0..4 {
852 if Some(reason) == p.oc[i] {
853 s.config.filter.oc[i] = params.value.as_f64();
854 return ParamChangeResult::updates(vec![]);
855 }
856 if Some(reason) == p.fc[i] {
857 s.config.filter.fc[i] = params.value.as_f64();
858 return ParamChangeResult::updates(vec![]);
859 }
860 }
861 for i in 0..2 {
862 if Some(reason) == p.rc[i] {
863 s.config.filter.rc[i] = params.value.as_f64();
864 return ParamChangeResult::updates(vec![]);
865 }
866 }
867 }
868
869 ParamChangeResult::updates(updates)
870 }
871}
872
873#[cfg(test)]
874mod tests {
875 use super::*;
876 use ad_core_rs::ndarray::{NDDataBuffer, NDDimension};
877
878 fn make_array(vals: &[u8]) -> NDArray {
879 let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::UInt8);
880 if let NDDataBuffer::U8(ref mut v) = arr.data {
881 v.copy_from_slice(vals);
882 }
883 arr
884 }
885
886 fn make_f64_array(vals: &[f64]) -> NDArray {
887 let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::Float64);
888 if let NDDataBuffer::F64(ref mut v) = arr.data {
889 v.copy_from_slice(vals);
890 }
891 arr
892 }
893
894 #[test]
895 fn test_background_subtraction() {
896 let bg_arr = make_array(&[10, 20, 30]);
897 let input = make_array(&[15, 25, 35]);
898
899 let mut state = ProcessState::new(ProcessConfig {
900 enable_background: true,
901 ..Default::default()
902 });
903 state.save_background(&bg_arr);
904
905 let result = state.process(&input).unwrap();
906 if let NDDataBuffer::U8(ref v) = result.data {
907 assert_eq!(v[0], 5);
908 assert_eq!(v[1], 5);
909 assert_eq!(v[2], 5);
910 }
911 }
912
913 #[test]
914 fn test_flat_field() {
915 let ff_arr = make_array(&[100, 200, 50]);
916 let input = make_array(&[100, 200, 50]);
917
918 let mut state = ProcessState::new(ProcessConfig {
919 enable_flat_field: true,
920 scale_flat_field: 0.0, ..Default::default()
922 });
923 state.save_flat_field(&ff_arr);
924
925 let result = state.process(&input).unwrap();
926 if let NDDataBuffer::U8(ref v) = result.data {
928 assert!((v[0] as f64 - 116.67).abs() < 1.0);
930 assert!((v[1] as f64 - 116.67).abs() < 1.0);
931 assert!((v[2] as f64 - 116.67).abs() < 1.0);
932 }
933 }
934
935 #[test]
936 fn test_offset_scale() {
937 let input = make_array(&[10, 20, 30]);
938 let mut state = ProcessState::new(ProcessConfig {
939 enable_offset_scale: true,
940 scale: 2.0,
941 offset: 5.0,
942 ..Default::default()
943 });
944
945 let result = state.process(&input).unwrap();
946 if let NDDataBuffer::U8(ref v) = result.data {
947 assert_eq!(v[0], 30); assert_eq!(v[1], 50); assert_eq!(v[2], 70); }
952 }
953
954 #[test]
955 fn test_clipping() {
956 let input = make_array(&[5, 50, 200]);
957 let mut state = ProcessState::new(ProcessConfig {
958 enable_low_clip: true,
959 low_clip_thresh: 10.0,
960 low_clip_value: 10.0,
961 enable_high_clip: true,
962 high_clip_thresh: 100.0,
963 high_clip_value: 100.0,
964 ..Default::default()
965 });
966
967 let result = state.process(&input).unwrap();
968 if let NDDataBuffer::U8(ref v) = result.data {
969 assert_eq!(v[0], 10); assert_eq!(v[1], 50); assert_eq!(v[2], 100); }
973 }
974
975 #[test]
976 fn test_recursive_filter() {
977 let input1 = make_array(&[100, 100, 100]);
983 let input2 = make_array(&[0, 0, 0]);
984
985 let mut state = ProcessState::new(ProcessConfig {
986 enable_filter: true,
987 filter: FilterConfig {
988 num_filter: 10,
989 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()
993 },
994 ..Default::default()
995 });
996
997 let _ = state.process(&input1);
1008
1009 let result = state.process(&input2).unwrap();
1014 if let NDDataBuffer::U8(ref v) = result.data {
1015 assert_eq!(v[0], 100);
1017 assert_eq!(v[1], 100);
1018 }
1019 }
1020
1021 #[test]
1022 fn test_output_type_conversion() {
1023 let input = make_array(&[10, 20, 30]);
1024 let mut state = ProcessState::new(ProcessConfig {
1025 output_type: Some(NDDataType::Float64),
1026 ..Default::default()
1027 });
1028
1029 let result = state.process(&input).unwrap();
1030 assert_eq!(result.data.data_type(), NDDataType::Float64);
1031 }
1032
1033 #[test]
1036 fn test_process_processor() {
1037 let mut proc = ProcessProcessor::new(ProcessConfig {
1038 enable_offset_scale: true,
1039 scale: 2.0,
1040 offset: 1.0,
1041 ..Default::default()
1042 });
1043 let pool = NDArrayPool::new(1_000_000);
1044
1045 let input = make_array(&[10, 20, 30]);
1046 let result = proc.process_array(&input, &pool);
1047 assert_eq!(result.output_arrays.len(), 1);
1048 if let NDDataBuffer::U8(ref v) = result.output_arrays[0].data {
1049 assert_eq!(v[0], 22); }
1051 }
1052
1053 #[test]
1056 fn test_filter_sum_preset() {
1057 let mut state = ProcessState::new(ProcessConfig {
1060 enable_filter: true,
1061 filter: FilterConfig {
1062 num_filter: 10,
1063 fc: [1.0, 0.0, 1.0, 0.0],
1064 oc: [1.0, 0.0, 0.0, 0.0],
1065 rc: [0.0, 1.0],
1066 ..Default::default()
1067 },
1068 output_type: Some(NDDataType::Float64),
1069 ..Default::default()
1070 });
1071
1072 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1084 let v0 = r0.data.get_as_f64(0).unwrap();
1085 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1086
1087 let r1 = state.process(&make_f64_array(&[100.0])).unwrap();
1094 let v1 = r1.data.get_as_f64(0).unwrap();
1095 assert!((v1 - 200.0).abs() < 1e-9, "frame 1: got {v1}");
1096 }
1097
1098 #[test]
1099 fn test_filter_average_preset() {
1100 let mut state = ProcessState::new(ProcessConfig {
1103 enable_filter: true,
1104 filter: FilterConfig {
1105 num_filter: 10,
1106 fc: [1.0, 0.0, 1.0, 0.0],
1107 oc: [0.0, 1.0, 0.0, 0.0],
1108 rc: [0.0, 1.0],
1109 ..Default::default()
1110 },
1111 output_type: Some(NDDataType::Float64),
1112 ..Default::default()
1113 });
1114
1115 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1124 let v0 = r0.data.get_as_f64(0).unwrap();
1125 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1126
1127 let r1 = state.process(&make_f64_array(&[200.0])).unwrap();
1132 let v1 = r1.data.get_as_f64(0).unwrap();
1133 assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
1134
1135 let r2 = state.process(&make_f64_array(&[300.0])).unwrap();
1140 let v2 = r2.data.get_as_f64(0).unwrap();
1141 let expected = 400.0 / 3.0;
1142 assert!((v2 - expected).abs() < 1e-9, "frame 2: got {v2}");
1143 }
1144
1145 #[test]
1146 fn test_filter_recursive_ave() {
1147 let mut state = ProcessState::new(ProcessConfig {
1152 enable_filter: true,
1153 filter: FilterConfig {
1154 num_filter: 10,
1155 fc: [1.0, -1.0, 0.0, 1.0],
1156 oc: [1.0, 0.0, 0.0, 0.0],
1157 rc: [0.0, 1.0],
1158 ..Default::default()
1159 },
1160 output_type: Some(NDDataType::Float64),
1161 ..Default::default()
1162 });
1163
1164 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1176 let v0 = r0.data.get_as_f64(0).unwrap();
1177 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1178
1179 let r1 = state.process(&make_f64_array(&[200.0])).unwrap();
1184 let v1 = r1.data.get_as_f64(0).unwrap();
1185 assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
1186
1187 let r2 = state.process(&make_f64_array(&[300.0])).unwrap();
1192 let v2 = r2.data.get_as_f64(0).unwrap();
1193 assert!((v2 - 150.0).abs() < 1e-9, "frame 2: got {v2}");
1194 }
1195
1196 #[test]
1197 fn test_save_background_one_shot() {
1198 let mut state = ProcessState::new(ProcessConfig {
1199 save_background: true,
1200 ..Default::default()
1201 });
1202
1203 assert!(!state.config.valid_background);
1204 assert!(state.background.is_none());
1205
1206 let input = make_array(&[10, 20, 30]);
1208 let _ = state.process(&input);
1209
1210 assert!(
1211 !state.config.save_background,
1212 "save_background should be cleared"
1213 );
1214 assert!(
1215 state.config.valid_background,
1216 "valid_background should be set"
1217 );
1218 assert!(state.background.is_some());
1219
1220 let bg = state.background.as_ref().unwrap();
1221 assert_eq!(bg.len(), 3);
1222 assert!((bg[0] - 10.0).abs() < 1e-9);
1223 assert!((bg[1] - 20.0).abs() < 1e-9);
1224 assert!((bg[2] - 30.0).abs() < 1e-9);
1225
1226 let input2 = make_array(&[40, 50, 60]);
1228 let _ = state.process(&input2);
1229
1230 assert!(
1231 !state.config.save_background,
1232 "save_background stays cleared"
1233 );
1234 let bg2 = state.background.as_ref().unwrap();
1236 assert!((bg2[0] - 10.0).abs() < 1e-9);
1237 }
1238
1239 #[test]
1240 fn test_save_flat_field_one_shot() {
1241 let mut state = ProcessState::new(ProcessConfig {
1242 save_flat_field: true,
1243 ..Default::default()
1244 });
1245
1246 assert!(!state.config.valid_flat_field);
1247 assert!(state.flat_field.is_none());
1248
1249 let input = make_array(&[50, 100, 150]);
1250 let _ = state.process(&input);
1251
1252 assert!(
1253 !state.config.save_flat_field,
1254 "save_flat_field should be cleared"
1255 );
1256 assert!(
1257 state.config.valid_flat_field,
1258 "valid_flat_field should be set"
1259 );
1260 assert!(state.flat_field.is_some());
1261
1262 let ff = state.flat_field.as_ref().unwrap();
1263 assert_eq!(ff.len(), 3);
1264 assert!((ff[0] - 50.0).abs() < 1e-9);
1265 assert!((ff[1] - 100.0).abs() < 1e-9);
1266 assert!((ff[2] - 150.0).abs() < 1e-9);
1267 }
1268
1269 #[test]
1270 fn test_auto_reset_when_num_filter_reached() {
1271 let mut state = ProcessState::new(ProcessConfig {
1273 enable_filter: true,
1274 filter: FilterConfig {
1275 num_filter: 3,
1276 auto_reset: true,
1277 fc: [1.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0],
1279 rc: [0.0, 1.0],
1280 ..Default::default()
1281 },
1282 output_type: Some(NDDataType::Float64),
1283 ..Default::default()
1284 });
1285
1286 let _ = state.process(&make_f64_array(&[100.0]));
1288 assert_eq!(state.num_filtered, 1);
1289
1290 let _ = state.process(&make_f64_array(&[100.0]));
1292 assert_eq!(state.num_filtered, 2);
1293
1294 let _ = state.process(&make_f64_array(&[100.0]));
1296 assert_eq!(state.num_filtered, 3);
1297
1298 let _ = state.process(&make_f64_array(&[200.0]));
1300 assert_eq!(state.num_filtered, 1, "fresh start after auto reset");
1302 }
1303
1304 #[test]
1305 fn test_filter_with_offset_scale() {
1306 let mut state = ProcessState::new(ProcessConfig {
1310 enable_filter: true,
1311 filter: FilterConfig {
1312 num_filter: 10,
1313 fc: [0.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0], rc: [0.0, 1.0],
1316 f_offset: 10.0,
1317 f_scale: 2.0,
1318 o_offset: 5.0,
1319 o_scale: 3.0,
1320 ..Default::default()
1321 },
1322 output_type: Some(NDDataType::Float64),
1323 ..Default::default()
1324 });
1325
1326 let r0 = state.process(&make_f64_array(&[50.0])).unwrap();
1336 let v0 = r0.data.get_as_f64(0).unwrap();
1337 assert!((v0 - 155.0).abs() < 1e-9, "frame 0: got {v0}");
1338
1339 let r1 = state.process(&make_f64_array(&[20.0])).unwrap();
1344 let v1 = r1.data.get_as_f64(0).unwrap();
1345 assert!((v1 - 335.0).abs() < 1e-9, "frame 1: got {v1}");
1346 }
1347
1348 #[test]
1349 fn test_reset_filter_manual() {
1350 let mut state = ProcessState::new(ProcessConfig {
1351 enable_filter: true,
1352 filter: FilterConfig {
1353 num_filter: 10,
1354 fc: [1.0, 0.0, 1.0, 0.0],
1355 oc: [1.0, 0.0, 0.0, 0.0],
1356 rc: [0.0, 1.0],
1357 ..Default::default()
1358 },
1359 output_type: Some(NDDataType::Float64),
1360 ..Default::default()
1361 });
1362
1363 let _ = state.process(&make_f64_array(&[100.0]));
1365 let _ = state.process(&make_f64_array(&[100.0]));
1366 assert!(state.filter_state.is_some());
1367 assert_eq!(state.num_filtered, 2);
1368
1369 state.reset_filter();
1371 assert!(state.filter_state.is_none());
1372 assert_eq!(state.num_filtered, 0);
1373
1374 let _ = state.process(&make_f64_array(&[200.0]));
1376 assert_eq!(state.num_filtered, 1);
1377 }
1378
1379 #[test]
1380 fn test_auto_offset_scale_applied() {
1381 let mut state = ProcessState::new(ProcessConfig {
1384 output_type: Some(NDDataType::UInt8),
1385 ..Default::default()
1386 });
1387 state.config.auto_offset_scale_pending = true;
1388
1389 let out = state.process(&make_f64_array(&[10.0, 20.0, 30.0])).unwrap();
1391 assert!(!state.config.auto_offset_scale_pending);
1393 assert!(state.config.enable_offset_scale);
1395 assert!((state.config.offset - (-10.0)).abs() < 1e-9);
1396 assert!((state.config.scale - 255.0 / 20.0).abs() < 1e-9);
1397 if let NDDataBuffer::U8(v) = &out.data {
1399 assert_eq!(v[0], 0); assert_eq!(v[2], 255); } else {
1402 panic!("expected u8 output");
1403 }
1404 }
1405
1406 #[test]
1407 fn test_filter_callbacks_drops_suppressed_frame() {
1408 let mut state = ProcessState::new(ProcessConfig {
1412 enable_filter: true,
1413 filter: FilterConfig {
1414 num_filter: 3,
1415 filter_callbacks: 1,
1416 fc: [1.0, 0.0, 1.0, 0.0],
1417 oc: [0.0, 1.0, 0.0, 0.0],
1418 rc: [0.0, 1.0],
1419 ..Default::default()
1420 },
1421 output_type: Some(NDDataType::Float64),
1422 ..Default::default()
1423 });
1424
1425 assert!(state.process(&make_f64_array(&[100.0])).is_none());
1427 assert!(state.process(&make_f64_array(&[100.0])).is_none());
1428 assert!(state.process(&make_f64_array(&[100.0])).is_some());
1430 }
1431
1432 #[test]
1433 fn test_filter_recurrence_matches_cpp() {
1434 let mut state = ProcessState::new(ProcessConfig {
1462 enable_filter: true,
1463 filter: FilterConfig {
1464 num_filter: 100,
1465 fc: [1.0, 0.0, 1.0, 0.0],
1466 oc: [0.0, 1.0, 0.0, 0.0],
1467 rc: [0.0, 1.0],
1468 ..Default::default()
1469 },
1470 output_type: Some(NDDataType::Float64),
1471 ..Default::default()
1472 });
1473
1474 let inputs = [100.0, 200.0, 300.0, 400.0];
1475 let expected_data = [100.0, 100.0, 400.0 / 3.0, 175.0];
1476 let expected_filter = [200.0, 400.0, 700.0, 1100.0];
1477
1478 for k in 0..inputs.len() {
1479 let r = state.process(&make_f64_array(&[inputs[k]])).unwrap();
1480 let v = r.data.get_as_f64(0).unwrap();
1481 assert!(
1482 (v - expected_data[k]).abs() < 1e-9,
1483 "frame {k}: data got {v}, expected {}",
1484 expected_data[k]
1485 );
1486 let fs = state.filter_state.as_ref().unwrap()[0];
1487 assert!(
1488 (fs - expected_filter[k]).abs() < 1e-9,
1489 "frame {k}: filter got {fs}, expected {}",
1490 expected_filter[k]
1491 );
1492 }
1493 }
1494}