Skip to main content

fpm_rs/measurements/
manifest.rs

1use std::{
2    fs::File,
3    io::{BufReader, BufWriter},
4    path::{Path, PathBuf},
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::Result;
10
11use super::PreprocessingConfig;
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15/// Serializable specification for loading a measurement stack.
16pub struct MeasurementSpec {
17    /// Measurement files in acquisition-frame order.
18    pub frames: Vec<FrameSpec>,
19    #[serde(default)]
20    /// Optional shared detector dark image, resolved relative to the manifest.
21    pub dark_frame: Option<PathBuf>,
22    #[serde(default)]
23    /// Optional shared positive flat-field image, resolved relative to the manifest.
24    pub flat_field: Option<PathBuf>,
25    #[serde(default)]
26    /// Optional shared or per-frame additive background images.
27    pub background: Option<ImageSet>,
28    #[serde(default)]
29    /// Optional shared or per-frame binary mask images; non-zero pixels are valid.
30    pub mask: Option<ImageSet>,
31    #[serde(default)]
32    /// Corrections applied when the resident stack is built or a lazy frame is decoded.
33    pub preprocessing: PreprocessingConfig,
34}
35
36impl MeasurementSpec {
37    /// Creates a specification with no correction images and preprocessing disabled.
38    pub fn new(frames: Vec<FrameSpec>) -> Self {
39        Self {
40            frames,
41            dark_frame: None,
42            flat_field: None,
43            background: None,
44            mask: None,
45            preprocessing: PreprocessingConfig::default(),
46        }
47    }
48
49    /// Serializes this specification as pretty-printed JSON at `path`.
50    pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
51        let writer = BufWriter::new(File::create(path)?);
52        serde_json::to_writer_pretty(writer, self)?;
53        Ok(())
54    }
55
56    /// Deserializes a JSON measurement specification without loading its image files.
57    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
58        let reader = BufReader::new(File::open(path)?);
59        Ok(serde_json::from_reader(reader)?)
60    }
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65/// Path and acquisition metadata for one measurement frame.
66pub struct FrameSpec {
67    /// Image path, interpreted relative to the manifest directory when not absolute.
68    pub path: PathBuf,
69    #[serde(default)]
70    /// Optional individual source index associated with this acquisition frame.
71    pub illumination_index: Option<usize>,
72    #[serde(default = "unit_value")]
73    /// Positive exposure time in caller-defined units; defaults to `1.0`.
74    pub exposure_time: f64,
75    #[serde(default = "unit_value")]
76    /// Non-negative reconstruction weight; defaults to `1.0`.
77    pub weight: f64,
78    #[serde(default)]
79    /// Optional human-readable acquisition label.
80    pub label: Option<String>,
81}
82
83impl FrameSpec {
84    /// Creates a unit-exposure, unit-weight frame specification for `path`.
85    pub fn new(path: impl Into<PathBuf>) -> Self {
86        Self {
87            path: path.into(),
88            illumination_index: None,
89            exposure_time: 1.0,
90            weight: 1.0,
91            label: None,
92        }
93    }
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize)]
97#[serde(untagged)]
98/// A single image shared by all frames or one image per frame.
99pub enum ImageSet {
100    /// One image broadcast to every acquisition frame.
101    Single(PathBuf),
102    /// One image per acquisition frame, in matching order.
103    PerFrame(Vec<PathBuf>),
104}
105
106fn unit_value() -> f64 {
107    1.0
108}