Skip to main content

fpm_rs/measurements/
stack.rs

1use std::path::Path;
2
3use ndarray::{Array2, Array3, ArrayView2, ArrayView3, ArrayViewMut2, Axis};
4use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
5
6use crate::{
7    Result,
8    array_layout::{StandardArray2, StandardArray3, checked_len_2d, checked_len_3d},
9    error::Error,
10    image_io::{GrayscaleScaling, load_grayscale, load_grayscale_tiff_pages},
11};
12
13use super::{FrameMetadata, ImageSet, MeasurementSpec, PreprocessingConfig};
14
15#[derive(Clone, Debug)]
16enum FrameArray<T> {
17    Shared(StandardArray2<T>),
18    PerFrame(StandardArray3<T>),
19}
20
21impl<T> FrameArray<T> {
22    fn as_slice(&self) -> &[T] {
23        match self {
24            Self::Shared(values) => values.as_slice(),
25            Self::PerFrame(values) => values.as_slice(),
26        }
27    }
28
29    fn frame(&self, index: usize, frame_len: usize) -> &[T] {
30        match self {
31            Self::Shared(values) => values.as_slice(),
32            Self::PerFrame(values) => {
33                let start = index * frame_len;
34                &values.as_slice()[start..start + frame_len]
35            }
36        }
37    }
38}
39
40/// An in-memory stack of image-plane intensity measurements.
41///
42/// Data are owned as a C-contiguous ndarray with dimension order
43/// `(frame, row, column)`. Public ndarray views borrow this allocation without
44/// copying. The [`super::MeasurementRead`] boundary continues to expose flat
45/// frame slices for reconstruction and lazy-stack parity.
46#[derive(Clone, Debug)]
47pub struct MeasurementStack {
48    data: StandardArray3<f64>,
49    frame_metadata: Vec<FrameMetadata>,
50    dark_frame: Option<StandardArray2<f64>>,
51    flat_field: Option<StandardArray2<f64>>,
52    background: Option<FrameArray<f64>>,
53    masks: Option<FrameArray<u8>>,
54    preprocessing: PreprocessingConfig,
55}
56
57impl MeasurementStack {
58    /// Stores an owned row-major `(frame, row, column)` buffer with image shape
59    /// `(height, width)`, deriving default metadata when `frame_metadata` is empty.
60    pub fn from_vec(
61        data: Vec<f64>,
62        image_shape: (usize, usize),
63        frame_metadata: Vec<FrameMetadata>,
64    ) -> Result<Self> {
65        let frame_len = checked_len_2d(image_shape)?;
66        if frame_len == 0 {
67            return Err(Error::InvalidShape(
68                "measurement dimensions must be non-zero".into(),
69            ));
70        }
71        if data.is_empty() || !data.len().is_multiple_of(frame_len) {
72            return Err(Error::InvalidMeasurements(format!(
73                "data length {} is not a positive multiple of frame size {frame_len}",
74                data.len()
75            )));
76        }
77        if data.iter().any(|value| !value.is_finite()) {
78            return Err(Error::InvalidMeasurements(
79                "measurements contain non-finite values".into(),
80            ));
81        }
82        let frames = data.len() / frame_len;
83        let frame_metadata = if frame_metadata.is_empty() {
84            (0..frames).map(FrameMetadata::new).collect()
85        } else {
86            if frame_metadata.len() != frames {
87                return Err(Error::InvalidMeasurements(format!(
88                    "{} metadata entries for {frames} frames",
89                    frame_metadata.len()
90                )));
91            }
92            frame_metadata
93        };
94        let stack = Self {
95            data: StandardArray3::from_shape_vec((frames, image_shape.0, image_shape.1), data)?,
96            frame_metadata,
97            dark_frame: None,
98            flat_field: None,
99            background: None,
100            masks: None,
101            preprocessing: PreprocessingConfig::default(),
102        };
103        stack.validate()?;
104        Ok(stack)
105    }
106
107    /// Stores an owned standard-layout `(frame,row,column)` array without a copy.
108    pub fn new(data: Array3<f64>, frame_metadata: Vec<FrameMetadata>) -> Result<Self> {
109        let data = StandardArray3::try_from(data)?;
110        let shape = data.dim();
111        if shape.0 == 0 || shape.1 == 0 || shape.2 == 0 {
112            return Err(Error::InvalidShape(format!(
113                "measurement dimensions must be non-zero, got {shape:?}"
114            )));
115        }
116        if data.as_slice().iter().any(|value| !value.is_finite()) {
117            return Err(Error::InvalidMeasurements(
118                "measurements contain non-finite values".into(),
119            ));
120        }
121        let frame_metadata = if frame_metadata.is_empty() {
122            (0..shape.0).map(FrameMetadata::new).collect()
123        } else if frame_metadata.len() == shape.0 {
124            frame_metadata
125        } else {
126            return Err(Error::InvalidMeasurements(format!(
127                "{} metadata entries for {} frames",
128                frame_metadata.len(),
129                shape.0
130            )));
131        };
132        let stack = Self {
133            data,
134            frame_metadata,
135            dark_frame: None,
136            flat_field: None,
137            background: None,
138            masks: None,
139            preprocessing: PreprocessingConfig::default(),
140        };
141        stack.validate()?;
142        Ok(stack)
143    }
144
145    /// Copies equally shaped row-major intensity frames into one resident stack.
146    pub fn from_frames(frames: &[Vec<f64>], image_shape: (usize, usize)) -> Result<Self> {
147        let frame_len = checked_len_2d(image_shape)?;
148        if frames.iter().any(|frame| frame.len() != frame_len) {
149            return Err(Error::InvalidMeasurements(
150                "every frame must match the declared image shape".into(),
151            ));
152        }
153        let total = frame_len
154            .checked_mul(frames.len())
155            .ok_or_else(|| Error::ShapeOverflow {
156                shape: vec![frames.len(), image_shape.0, image_shape.1],
157            })?;
158        let mut data = Vec::with_capacity(total);
159        data.extend(frames.iter().flatten().copied());
160        Self::from_vec(data, image_shape, Vec::new())
161    }
162
163    /// Loads grayscale image files in acquisition order, converting native pixel values to `f64`.
164    pub fn from_image_files<P: AsRef<Path>>(
165        paths: &[P],
166        frame_metadata: Vec<FrameMetadata>,
167    ) -> Result<Self> {
168        if paths.is_empty() {
169            return Err(Error::InvalidMeasurements(
170                "at least one image path is required".into(),
171            ));
172        }
173        let mut shape = None;
174        let mut data = Vec::new();
175        for path in paths {
176            let frame = load_grayscale(path, GrayscaleScaling::NativeCounts)?;
177            if let Some(expected) = shape {
178                if frame.dim() != expected {
179                    return Err(Error::InvalidMeasurements(format!(
180                        "image {} has shape {:?}, expected {expected:?}",
181                        path.as_ref().display(),
182                        frame.dim()
183                    )));
184                }
185            } else {
186                shape = Some(frame.dim());
187            }
188            data.extend(frame);
189        }
190        let metadata = if frame_metadata.is_empty() {
191            paths
192                .iter()
193                .enumerate()
194                .map(|(index, path)| {
195                    let mut metadata = FrameMetadata::new(index);
196                    metadata.label = Some(path.as_ref().display().to_string());
197                    metadata
198                })
199                .collect()
200        } else {
201            frame_metadata
202        };
203        let shape = shape.ok_or_else(|| {
204            Error::InvalidMeasurements("image paths did not produce a frame".into())
205        })?;
206        Self::from_vec(data, shape, metadata)
207    }
208
209    /// Loads every page of a same-shaped grayscale TIFF as one acquisition frame.
210    pub fn from_tiff_stack(
211        path: impl AsRef<Path>,
212        frame_metadata: Vec<FrameMetadata>,
213    ) -> Result<Self> {
214        let path = path.as_ref();
215        let pages = load_grayscale_tiff_pages(path, GrayscaleScaling::NativeCounts)?;
216        let first = pages.first().ok_or_else(|| {
217            Error::InvalidMeasurements("TIFF stack does not contain an image".into())
218        })?;
219        let shape = first.dim();
220        if pages.iter().any(|page| page.dim() != shape) {
221            return Err(Error::InvalidMeasurements(
222                "all TIFF pages must have the same dimensions".into(),
223            ));
224        }
225        let metadata = if frame_metadata.is_empty() {
226            (0..pages.len())
227                .map(|index| {
228                    let mut metadata = FrameMetadata::new(index);
229                    metadata.label = Some(format!("{}#page={index}", path.display()));
230                    metadata
231                })
232                .collect()
233        } else {
234            frame_metadata
235        };
236        Self::from_vec(pages.into_iter().flatten().collect(), shape, metadata)
237    }
238
239    /// Loads a JSON [`MeasurementSpec`](super::MeasurementSpec) and all referenced images.
240    pub fn from_manifest(path: impl AsRef<Path>) -> Result<Self> {
241        let path = path.as_ref();
242        let manifest = MeasurementSpec::load(path)?;
243        let base_directory = path.parent().unwrap_or_else(|| Path::new("."));
244        Self::from_manifest_definition(manifest, base_directory)
245    }
246
247    /// Loads a manifest value, resolving relative paths beneath `base_directory`.
248    pub fn from_manifest_definition(
249        manifest: MeasurementSpec,
250        base_directory: impl AsRef<Path>,
251    ) -> Result<Self> {
252        let base_directory = base_directory.as_ref();
253        let paths: Vec<_> = manifest
254            .frames
255            .iter()
256            .map(|frame| resolve_path(base_directory, &frame.path))
257            .collect();
258        let metadata = manifest
259            .frames
260            .iter()
261            .enumerate()
262            .map(|(index, frame)| FrameMetadata {
263                frame_index: index,
264                illumination_index: frame.illumination_index,
265                original_frame_index: Some(index),
266                original_illumination_index: frame.illumination_index,
267                exposure_time: frame.exposure_time,
268                weight: frame.weight,
269                label: frame
270                    .label
271                    .clone()
272                    .or_else(|| Some(frame.path.display().to_string())),
273            })
274            .collect();
275        let mut stack = Self::from_image_files(&paths, metadata)?;
276        if let Some(path) = &manifest.dark_frame {
277            stack.dark_frame = Some(StandardArray2::from_shape_vec(
278                stack.image_shape(),
279                load_manifest_image(base_directory, path, stack.image_shape())?,
280            )?);
281        }
282        if let Some(path) = &manifest.flat_field {
283            stack.flat_field = Some(StandardArray2::from_shape_vec(
284                stack.image_shape(),
285                load_manifest_image(base_directory, path, stack.image_shape())?,
286            )?);
287        }
288        if let Some(background) = &manifest.background {
289            let values = load_manifest_image_set(
290                base_directory,
291                background,
292                stack.image_shape(),
293                stack.frame_count(),
294            )?;
295            stack.background = Some(stack.frame_array_from_flat(values, "background")?);
296        }
297        if let Some(mask) = &manifest.mask {
298            let values = load_manifest_image_set(
299                base_directory,
300                mask,
301                stack.image_shape(),
302                stack.frame_count(),
303            )?
304            .into_iter()
305            .map(|value| u8::from(value != 0.0))
306            .collect();
307            stack.masks = Some(stack.frame_array_from_flat(values, "mask")?);
308        }
309        stack.preprocessing = manifest.preprocessing;
310        stack.validate()?;
311        Ok(stack)
312    }
313
314    /// Borrows intensity data as `(frame, row, column)` without allocating or copying.
315    pub fn data(&self) -> ArrayView3<'_, f64> {
316        self.data.ndarray_view()
317    }
318
319    /// Borrows one `(row, column)` intensity frame without copying.
320    pub fn frame_view(&self, index: usize) -> Result<ArrayView2<'_, f64>> {
321        self.check_frame(index)?;
322        Ok(self.data.ndarray_view().index_axis_move(Axis(0), index))
323    }
324
325    /// Mutably borrows one `(row, column)` intensity frame without copying.
326    pub fn frame_view_mut(&mut self, index: usize) -> Result<ArrayViewMut2<'_, f64>> {
327        self.check_frame(index)?;
328        Ok(self.data.ndarray_view_mut().index_axis_move(Axis(0), index))
329    }
330
331    /// Returns the acquisition-frame count.
332    pub fn frame_count(&self) -> usize {
333        self.data.dim().0
334    }
335
336    /// Returns low-resolution frame shape as `(height, width)`.
337    pub fn image_shape(&self) -> (usize, usize) {
338        let shape = self.data.dim();
339        (shape.1, shape.2)
340    }
341
342    /// Returns the row-major element count of one frame.
343    pub fn frame_len(&self) -> usize {
344        let shape = self.data.dim();
345        shape.1 * shape.2
346    }
347
348    /// Borrows the complete standard-layout `(frame, row, column)` stack as a flat slice.
349    pub fn as_slice(&self) -> &[f64] {
350        self.data.as_slice()
351    }
352
353    /// Borrows metadata in acquisition-frame order.
354    pub fn frame_metadata(&self) -> &[FrameMetadata] {
355        &self.frame_metadata
356    }
357
358    /// Sets a frame's reconstruction weight after validating it.
359    pub fn set_frame_weight(&mut self, index: usize, weight: f64) -> Result<()> {
360        self.check_frame(index)?;
361        if !weight.is_finite() || weight < 0.0 {
362            return Err(Error::InvalidMeasurements(format!(
363                "frame {index} has invalid weight {weight}"
364            )));
365        }
366        self.frame_metadata[index].weight = weight;
367        Ok(())
368    }
369
370    /// Borrows the correction flags associated with this stack.
371    pub fn preprocessing(&self) -> &PreprocessingConfig {
372        &self.preprocessing
373    }
374
375    /// Checks non-empty consistent shapes and counts, standard layout, finite intensities,
376    /// metadata indices/exposures/weights, correction arrays, and binary masks.
377    pub fn validate(&self) -> Result<()> {
378        let shape = self.data.dim();
379        let expected = checked_len_3d(shape)?;
380        if shape.0 == 0 || shape.1 == 0 || shape.2 == 0 || self.data.len() != expected {
381            return Err(Error::InvalidMeasurements(
382                "stored frame count, image shape, and data length are inconsistent".into(),
383            ));
384        }
385        if self.data.as_slice().iter().any(|value| !value.is_finite()) {
386            return Err(Error::InvalidMeasurements(
387                "measurements contain non-finite values".into(),
388            ));
389        }
390        if self.frame_metadata.len() != shape.0 {
391            return Err(Error::InvalidMeasurements(format!(
392                "{} metadata entries for {} frames",
393                self.frame_metadata.len(),
394                shape.0
395            )));
396        }
397        for (index, metadata) in self.frame_metadata.iter().enumerate() {
398            if metadata.frame_index != index {
399                return Err(Error::InvalidMeasurements(format!(
400                    "metadata entry {index} identifies frame {}",
401                    metadata.frame_index
402                )));
403            }
404            if !metadata.exposure_time.is_finite() || metadata.exposure_time <= 0.0 {
405                return Err(Error::InvalidMeasurements(format!(
406                    "frame {index} has invalid exposure {}",
407                    metadata.exposure_time
408                )));
409            }
410            if !metadata.weight.is_finite() || metadata.weight < 0.0 {
411                return Err(Error::InvalidMeasurements(format!(
412                    "frame {index} has invalid weight {}",
413                    metadata.weight
414                )));
415            }
416        }
417        let image_shape = self.image_shape();
418        if self
419            .dark_frame
420            .as_ref()
421            .is_some_and(|values| values.dim() != image_shape || !all_finite(values.as_slice()))
422        {
423            return Err(Error::InvalidMeasurements(
424                "dark frame must be finite and match the image shape".into(),
425            ));
426        }
427        if self.flat_field.as_ref().is_some_and(|values| {
428            values.dim() != image_shape
429                || values
430                    .as_slice()
431                    .iter()
432                    .any(|value| !value.is_finite() || *value <= 0.0)
433        }) {
434            return Err(Error::InvalidMeasurements(
435                "flat field must be positive, finite, and match the image shape".into(),
436            ));
437        }
438        self.validate_frame_array(self.background.as_ref(), "background")?;
439        self.validate_frame_array(self.masks.as_ref(), "mask")?;
440        if self
441            .masks
442            .as_ref()
443            .is_some_and(|values| values.as_slice().iter().any(|&value| value > 1))
444        {
445            return Err(Error::InvalidMeasurements(
446                "mask values must be exactly zero or one".into(),
447            ));
448        }
449        if self.preprocessing.subtract_dark && self.dark_frame.is_none() {
450            return Err(Error::InvalidMeasurements(
451                "dark subtraction requested without a dark frame".into(),
452            ));
453        }
454        if self.preprocessing.divide_flat_field && self.flat_field.is_none() {
455            return Err(Error::InvalidMeasurements(
456                "flat-field correction requested without a flat field".into(),
457            ));
458        }
459        if self.preprocessing.subtract_background && self.background.is_none() {
460            return Err(Error::InvalidMeasurements(
461                "background subtraction requested without a background".into(),
462            ));
463        }
464        Ok(())
465    }
466
467    /// Borrows one row-major intensity frame by acquisition index.
468    pub fn frame(&self, index: usize) -> Result<&[f64]> {
469        self.check_frame(index)?;
470        let frame_len = self.frame_len();
471        let start = index * frame_len;
472        Ok(&self.data.as_slice()[start..start + frame_len])
473    }
474
475    /// Mutably borrows one row-major intensity frame by acquisition index.
476    pub fn frame_mut(&mut self, index: usize) -> Result<&mut [f64]> {
477        self.check_frame(index)?;
478        let frame_len = self.frame_len();
479        let start = index * frame_len;
480        Ok(&mut self.data.as_slice_mut()[start..start + frame_len])
481    }
482
483    /// Returns a frame's non-negative reconstruction weight.
484    pub fn frame_weight(&self, index: usize) -> Result<f64> {
485        self.frame_metadata
486            .get(index)
487            .map(|metadata| metadata.weight)
488            .ok_or(Error::FrameOutOfRange {
489                index,
490                frames: self.frame_count(),
491            })
492    }
493
494    /// Borrows a frame's row-major binary validity mask, including a broadcast mask.
495    pub fn frame_mask(&self, index: usize) -> Result<Option<&[u8]>> {
496        self.check_frame(index)?;
497        Ok(self
498            .masks
499            .as_ref()
500            .map(|values| values.frame(index, self.frame_len())))
501    }
502
503    /// Stores a finite same-shaped detector dark image and enables dark subtraction.
504    pub fn with_dark_frame(mut self, dark: Array2<f64>) -> Result<Self> {
505        let dark = StandardArray2::try_from(dark)?;
506        if dark.dim() != self.image_shape() || !all_finite(dark.as_slice()) {
507            return Err(Error::InvalidMeasurements(
508                "dark frame must be finite and match the image shape".into(),
509            ));
510        }
511        self.dark_frame = Some(dark);
512        self.preprocessing.subtract_dark = true;
513        Ok(self)
514    }
515
516    /// Stores a positive finite same-shaped flat field and enables division by it.
517    pub fn with_flat_field(mut self, flat: Array2<f64>) -> Result<Self> {
518        let flat = StandardArray2::try_from(flat)?;
519        if flat.dim() != self.image_shape()
520            || flat
521                .as_slice()
522                .iter()
523                .any(|value| !value.is_finite() || *value <= 0.0)
524        {
525            return Err(Error::InvalidMeasurements(
526                "flat field must be positive, finite, and match the image shape".into(),
527            ));
528        }
529        self.flat_field = Some(flat);
530        self.preprocessing.divide_flat_field = true;
531        Ok(self)
532    }
533
534    /// Stores one finite background image broadcast to every frame and enables subtraction.
535    pub fn with_background(mut self, background: Array2<f64>) -> Result<Self> {
536        let values = StandardArray2::try_from(background)?;
537        if values.dim() != self.image_shape() || !all_finite(values.as_slice()) {
538            return Err(Error::InvalidMeasurements(
539                "background must be finite and match the image shape".into(),
540            ));
541        }
542        self.background = Some(FrameArray::Shared(values));
543        self.preprocessing.subtract_background = true;
544        Ok(self)
545    }
546
547    /// Stores a finite `(frame, row, column)` background stack and enables subtraction.
548    pub fn with_per_frame_background(mut self, background: Array3<f64>) -> Result<Self> {
549        let values = StandardArray3::try_from(background)?;
550        if values.dim() != self.data.dim() || !all_finite(values.as_slice()) {
551            return Err(Error::InvalidMeasurements(
552                "per-frame background must be finite and match the measurement stack".into(),
553            ));
554        }
555        self.background = Some(FrameArray::PerFrame(values));
556        self.preprocessing.subtract_background = true;
557        Ok(self)
558    }
559
560    /// Stores one same-shaped binary mask broadcast to every frame.
561    pub fn with_masks(mut self, masks: Array2<u8>) -> Result<Self> {
562        let values = StandardArray2::try_from(masks)?;
563        if values.dim() != self.image_shape() || values.as_slice().iter().any(|&value| value > 1) {
564            return Err(Error::InvalidMeasurements(
565                "mask must be binary and match the image shape".into(),
566            ));
567        }
568        self.masks = Some(FrameArray::Shared(values));
569        Ok(self)
570    }
571
572    /// Stores binary validity masks shaped `(frame, row, column)`.
573    pub fn with_per_frame_masks(mut self, masks: Array3<u8>) -> Result<Self> {
574        let values = StandardArray3::try_from(masks)?;
575        if values.dim() != self.data.dim() || values.as_slice().iter().any(|&value| value > 1) {
576            return Err(Error::InvalidMeasurements(
577                "per-frame masks must be binary and match the measurement stack".into(),
578            ));
579        }
580        self.masks = Some(FrameArray::PerFrame(values));
581        Ok(self)
582    }
583
584    /// Enables division by each frame's positive exposure time during preprocessing.
585    pub fn normalize_exposure(mut self) -> Self {
586        self.preprocessing.normalize_exposure = true;
587        self
588    }
589
590    /// Enables clamping corrected negative intensities to zero.
591    pub fn clamp_negative(mut self) -> Self {
592        self.preprocessing.clamp_negative = true;
593        self
594    }
595
596    /// Replaces correction flags, requiring every enabled correction array to be present.
597    pub fn with_preprocessing(mut self, preprocessing: PreprocessingConfig) -> Result<Self> {
598        self.preprocessing = preprocessing;
599        self.validate()?;
600        Ok(self)
601    }
602
603    /// Applies configured corrections in place and returns the owned stack.
604    ///
605    /// Order is dark subtraction, flat-field division, exposure normalization,
606    /// background subtraction, then optional non-negative clamping.
607    pub fn apply_preprocessing(mut self) -> Result<Self> {
608        self.validate()?;
609        let frame_len = self.frame_len();
610        let dark = self.dark_frame.as_ref().map(StandardArray2::as_slice);
611        let flat = self.flat_field.as_ref().map(StandardArray2::as_slice);
612        let preprocessing = self.preprocessing.clone();
613        let metadata = &self.frame_metadata;
614        let background = self.background.as_ref();
615        let data = self.data.as_slice_mut();
616        for (frame_index, frame) in data.chunks_exact_mut(frame_len).enumerate() {
617            let exposure = metadata[frame_index].exposure_time;
618            let frame_background = background.map(|values| values.frame(frame_index, frame_len));
619            for (pixel, value) in frame.iter_mut().enumerate() {
620                if preprocessing.subtract_dark
621                    && let Some(dark) = dark
622                {
623                    *value -= dark[pixel];
624                }
625                if preprocessing.subtract_background
626                    && let Some(background) = frame_background
627                {
628                    *value -= background[pixel];
629                }
630                if preprocessing.divide_flat_field
631                    && let Some(flat) = flat
632                {
633                    *value /= flat[pixel];
634                }
635                if preprocessing.normalize_exposure {
636                    *value /= exposure;
637                }
638                if preprocessing.clamp_negative {
639                    *value = value.max(0.0);
640                }
641            }
642        }
643        Ok(self)
644    }
645
646    pub(crate) fn dark_frame_slice(&self) -> Option<&[f64]> {
647        self.dark_frame.as_ref().map(StandardArray2::as_slice)
648    }
649
650    pub(crate) fn flat_field_slice(&self) -> Option<&[f64]> {
651        self.flat_field.as_ref().map(StandardArray2::as_slice)
652    }
653
654    pub(crate) fn background_slice(&self) -> Option<&[f64]> {
655        self.background.as_ref().map(FrameArray::as_slice)
656    }
657
658    pub(crate) fn masks_slice(&self) -> Option<&[u8]> {
659        self.masks.as_ref().map(FrameArray::as_slice)
660    }
661
662    fn check_frame(&self, index: usize) -> Result<()> {
663        if index >= self.frame_count() {
664            Err(Error::FrameOutOfRange {
665                index,
666                frames: self.frame_count(),
667            })
668        } else {
669            Ok(())
670        }
671    }
672
673    fn validate_frame_array<T>(&self, values: Option<&FrameArray<T>>, name: &str) -> Result<()> {
674        let Some(values) = values else {
675            return Ok(());
676        };
677        let valid = match values {
678            FrameArray::Shared(values) => values.dim() == self.image_shape(),
679            FrameArray::PerFrame(values) => values.dim() == self.data.dim(),
680        };
681        if valid {
682            Ok(())
683        } else {
684            Err(Error::InvalidMeasurements(format!(
685                "{name} dimensions do not match the measurement stack"
686            )))
687        }
688    }
689
690    fn frame_array_from_flat<T>(&self, values: Vec<T>, name: &str) -> Result<FrameArray<T>> {
691        if values.len() == self.frame_len() {
692            Ok(FrameArray::Shared(StandardArray2::from_shape_vec(
693                self.image_shape(),
694                values,
695            )?))
696        } else if values.len() == self.data.len() {
697            Ok(FrameArray::PerFrame(StandardArray3::from_shape_vec(
698                self.data.dim(),
699                values,
700            )?))
701        } else {
702            Err(Error::InvalidMeasurements(format!(
703                "{name} length must match one image or the complete frame stack"
704            )))
705        }
706    }
707}
708
709impl Serialize for MeasurementStack {
710    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
711    where
712        S: Serializer,
713    {
714        #[derive(Serialize)]
715        struct Representation<'a> {
716            data: &'a [f64],
717            image_shape: (usize, usize),
718            frames: usize,
719            frame_metadata: &'a [FrameMetadata],
720            dark_frame: Option<&'a [f64]>,
721            flat_field: Option<&'a [f64]>,
722            background: Option<&'a [f64]>,
723            masks: Option<&'a [u8]>,
724            preprocessing: &'a PreprocessingConfig,
725        }
726
727        Representation {
728            data: self.data.as_slice(),
729            image_shape: self.image_shape(),
730            frames: self.frame_count(),
731            frame_metadata: &self.frame_metadata,
732            dark_frame: self.dark_frame_slice(),
733            flat_field: self.flat_field_slice(),
734            background: self.background_slice(),
735            masks: self.masks_slice(),
736            preprocessing: &self.preprocessing,
737        }
738        .serialize(serializer)
739    }
740}
741
742impl<'de> Deserialize<'de> for MeasurementStack {
743    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
744    where
745        D: Deserializer<'de>,
746    {
747        #[derive(Deserialize)]
748        #[serde(deny_unknown_fields)]
749        struct Representation {
750            data: Vec<f64>,
751            image_shape: (usize, usize),
752            frames: usize,
753            frame_metadata: Vec<FrameMetadata>,
754            dark_frame: Option<Vec<f64>>,
755            flat_field: Option<Vec<f64>>,
756            background: Option<Vec<f64>>,
757            masks: Option<Vec<u8>>,
758            preprocessing: PreprocessingConfig,
759        }
760
761        let representation = Representation::deserialize(deserializer)?;
762        let expected = checked_len_3d((
763            representation.frames,
764            representation.image_shape.0,
765            representation.image_shape.1,
766        ))
767        .map_err(D::Error::custom)?;
768        if representation.data.len() != expected {
769            return Err(D::Error::custom(
770                "measurement data length does not match shape",
771            ));
772        }
773        let mut stack = Self::from_vec(
774            representation.data,
775            representation.image_shape,
776            representation.frame_metadata,
777        )
778        .map_err(D::Error::custom)?;
779        if stack.frame_count() != representation.frames {
780            return Err(D::Error::custom("measurement frame count is inconsistent"));
781        }
782        stack.dark_frame = representation
783            .dark_frame
784            .map(|values| StandardArray2::from_shape_vec(stack.image_shape(), values))
785            .transpose()
786            .map_err(D::Error::custom)?;
787        stack.flat_field = representation
788            .flat_field
789            .map(|values| StandardArray2::from_shape_vec(stack.image_shape(), values))
790            .transpose()
791            .map_err(D::Error::custom)?;
792        stack.background = representation
793            .background
794            .map(|values| stack.frame_array_from_flat(values, "background"))
795            .transpose()
796            .map_err(D::Error::custom)?;
797        stack.masks = representation
798            .masks
799            .map(|values| stack.frame_array_from_flat(values, "mask"))
800            .transpose()
801            .map_err(D::Error::custom)?;
802        stack.preprocessing = representation.preprocessing;
803        stack.validate().map_err(D::Error::custom)?;
804        Ok(stack)
805    }
806}
807
808fn all_finite(values: &[f64]) -> bool {
809    values.iter().all(|value| value.is_finite())
810}
811
812pub(super) fn resolve_path(base_directory: &Path, path: &Path) -> std::path::PathBuf {
813    if path.is_absolute() {
814        path.to_owned()
815    } else {
816        base_directory.join(path)
817    }
818}
819
820pub(super) fn load_manifest_image(
821    base_directory: &Path,
822    path: &Path,
823    expected_shape: (usize, usize),
824) -> Result<Vec<f64>> {
825    let resolved = resolve_path(base_directory, path);
826    let image = load_grayscale(&resolved, GrayscaleScaling::NativeCounts)?;
827    if image.dim() != expected_shape {
828        return Err(Error::InvalidMeasurements(format!(
829            "image {} has shape {:?}, expected {expected_shape:?}",
830            resolved.display(),
831            image.dim()
832        )));
833    }
834    Ok(image.into_iter().collect())
835}
836
837pub(super) fn load_manifest_image_set(
838    base_directory: &Path,
839    images: &ImageSet,
840    expected_shape: (usize, usize),
841    frame_count: usize,
842) -> Result<Vec<f64>> {
843    match images {
844        ImageSet::Single(path) => load_manifest_image(base_directory, path, expected_shape),
845        ImageSet::PerFrame(paths) => {
846            if paths.len() != frame_count {
847                return Err(Error::InvalidMeasurements(format!(
848                    "manifest image set has {} entries for {frame_count} frames",
849                    paths.len()
850                )));
851            }
852            let frame_len = checked_len_2d(expected_shape)?;
853            let total = frame_len
854                .checked_mul(frame_count)
855                .ok_or_else(|| Error::ShapeOverflow {
856                    shape: vec![frame_count, expected_shape.0, expected_shape.1],
857                })?;
858            let mut values = Vec::with_capacity(total);
859            for path in paths {
860                values.extend(load_manifest_image(base_directory, path, expected_shape)?);
861            }
862            Ok(values)
863        }
864    }
865}