1use std::{
2 collections::{HashMap, VecDeque},
3 mem::size_of,
4 path::{Path, PathBuf},
5 sync::{Arc, Mutex},
6};
7
8use crate::{
9 Result,
10 error::Error,
11 image_io::{
12 GrayscaleScaling, grayscale_image_shape, grayscale_tiff_page_shapes, load_grayscale,
13 load_grayscale_tiff_page,
14 },
15};
16
17use super::{
18 FrameMetadata, MeasurementSpec, MeasurementStack, PreprocessingConfig,
19 stack::{load_manifest_image, load_manifest_image_set, resolve_path},
20};
21
22#[derive(Debug)]
38pub struct LazyMeasurementStack {
39 paths: Vec<PathBuf>,
40 page_indices: Vec<Option<usize>>,
41 image_shape: (usize, usize),
42 frame_metadata: Vec<FrameMetadata>,
43 dark_frame: Option<Vec<f64>>,
44 flat_field: Option<Vec<f64>>,
45 background: Option<Vec<f64>>,
46 masks: Option<Vec<u8>>,
47 preprocessing: PreprocessingConfig,
48 cache_capacity: usize,
49 cache_byte_capacity: Option<usize>,
50 cache: Mutex<FrameCache>,
51}
52
53#[derive(Debug, Default)]
54struct FrameCache {
55 frames: HashMap<usize, Arc<Vec<f64>>>,
56 least_recently_used: VecDeque<usize>,
57 bytes: usize,
58}
59
60impl FrameCache {
61 fn touch(&mut self, index: usize) {
62 self.least_recently_used.retain(|&cached| cached != index);
63 self.least_recently_used.push_back(index);
64 }
65
66 fn evict_least_recently_used(&mut self) -> bool {
67 let Some(index) = self.least_recently_used.pop_front() else {
68 return false;
69 };
70 if let Some(frame) = self.frames.remove(&index) {
71 self.bytes = self
72 .bytes
73 .saturating_sub(frame.len().saturating_mul(size_of::<f64>()));
74 }
75 true
76 }
77}
78
79impl Clone for LazyMeasurementStack {
80 fn clone(&self) -> Self {
81 Self {
82 paths: self.paths.clone(),
83 page_indices: self.page_indices.clone(),
84 image_shape: self.image_shape,
85 frame_metadata: self.frame_metadata.clone(),
86 dark_frame: self.dark_frame.clone(),
87 flat_field: self.flat_field.clone(),
88 background: self.background.clone(),
89 masks: self.masks.clone(),
90 preprocessing: self.preprocessing.clone(),
91 cache_capacity: self.cache_capacity,
92 cache_byte_capacity: self.cache_byte_capacity,
93 cache: Mutex::new(FrameCache::default()),
96 }
97 }
98}
99
100impl LazyMeasurementStack {
101 pub fn from_image_files<P: AsRef<Path>>(
102 paths: &[P],
103 frame_metadata: Vec<FrameMetadata>,
104 ) -> Result<Self> {
105 if paths.is_empty() {
106 return Err(Error::InvalidMeasurements(
107 "at least one image path is required".into(),
108 ));
109 }
110 let paths: Vec<PathBuf> = paths.iter().map(|path| path.as_ref().to_owned()).collect();
111 let image_shape = grayscale_image_shape(&paths[0])?;
112 for path in &paths[1..] {
113 let shape = grayscale_image_shape(path)?;
114 if shape != image_shape {
115 return Err(Error::InvalidMeasurements(format!(
116 "image {} has shape {shape:?}, expected {image_shape:?}",
117 path.display()
118 )));
119 }
120 }
121 let frame_metadata =
122 metadata_or_labels(&paths, frame_metadata, |path, _| path.display().to_string());
123 let frame_count = paths.len();
124 Self::new(paths, vec![None; frame_count], image_shape, frame_metadata)
125 }
126
127 pub fn from_tiff_stack(
130 path: impl AsRef<Path>,
131 frame_metadata: Vec<FrameMetadata>,
132 ) -> Result<Self> {
133 let path = path.as_ref().to_owned();
134 let shapes = grayscale_tiff_page_shapes(&path)?;
135 let image_shape = *shapes.first().ok_or_else(|| {
136 Error::InvalidMeasurements("TIFF stack does not contain an image".into())
137 })?;
138 if shapes.iter().any(|&shape| shape != image_shape) {
139 return Err(Error::InvalidMeasurements(
140 "all TIFF pages must have the same dimensions".into(),
141 ));
142 }
143 let paths = vec![path; shapes.len()];
144 let frame_metadata = metadata_or_labels(&paths, frame_metadata, |path, index| {
145 format!("{}#page={index}", path.display())
146 });
147 let page_indices = (0..shapes.len()).map(Some).collect();
148 Self::new(paths, page_indices, image_shape, frame_metadata)
149 }
150
151 pub fn from_manifest(path: impl AsRef<Path>) -> Result<Self> {
155 let path = path.as_ref();
156 let manifest = MeasurementSpec::load(path)?;
157 let base_directory = path.parent().unwrap_or_else(|| Path::new("."));
158 Self::from_manifest_definition(manifest, base_directory)
159 }
160
161 pub fn from_manifest_definition(
162 manifest: MeasurementSpec,
163 base_directory: impl AsRef<Path>,
164 ) -> Result<Self> {
165 let base_directory = base_directory.as_ref();
166 let paths: Vec<_> = manifest
167 .frames
168 .iter()
169 .map(|frame| resolve_path(base_directory, &frame.path))
170 .collect();
171 let metadata = manifest
172 .frames
173 .iter()
174 .enumerate()
175 .map(|(index, frame)| FrameMetadata {
176 frame_index: index,
177 illumination_index: frame.illumination_index,
178 original_frame_index: Some(index),
179 original_illumination_index: frame.illumination_index,
180 exposure_time: frame.exposure_time,
181 weight: frame.weight,
182 label: frame
183 .label
184 .clone()
185 .or_else(|| Some(frame.path.display().to_string())),
186 })
187 .collect();
188 let mut stack = Self::from_image_files(&paths, metadata)?;
189 if let Some(path) = &manifest.dark_frame {
190 stack.dark_frame = Some(load_manifest_image(
191 base_directory,
192 path,
193 stack.image_shape,
194 )?);
195 }
196 if let Some(path) = &manifest.flat_field {
197 stack.flat_field = Some(load_manifest_image(
198 base_directory,
199 path,
200 stack.image_shape,
201 )?);
202 }
203 if let Some(background) = &manifest.background {
204 stack.background = Some(load_manifest_image_set(
205 base_directory,
206 background,
207 stack.image_shape,
208 stack.frame_count(),
209 )?);
210 }
211 if let Some(mask) = &manifest.mask {
212 stack.masks = Some(
213 load_manifest_image_set(
214 base_directory,
215 mask,
216 stack.image_shape,
217 stack.frame_count(),
218 )?
219 .into_iter()
220 .map(|value| u8::from(value != 0.0))
221 .collect(),
222 );
223 }
224 stack.preprocessing = manifest.preprocessing;
225 stack.validate()?;
226 Ok(stack)
227 }
228
229 fn new(
230 paths: Vec<PathBuf>,
231 page_indices: Vec<Option<usize>>,
232 image_shape: (usize, usize),
233 frame_metadata: Vec<FrameMetadata>,
234 ) -> Result<Self> {
235 let stack = Self {
236 paths,
237 page_indices,
238 image_shape,
239 frame_metadata,
240 dark_frame: None,
241 flat_field: None,
242 background: None,
243 masks: None,
244 preprocessing: PreprocessingConfig::default(),
245 cache_capacity: 1,
246 cache_byte_capacity: None,
247 cache: Mutex::new(FrameCache::default()),
248 };
249 stack.validate()?;
250 Ok(stack)
251 }
252
253 pub fn with_dark_frame(mut self, dark: Vec<f64>) -> Result<Self> {
254 self.validate_correction("dark frame", &dark, false)?;
255 self.dark_frame = Some(dark);
256 self.preprocessing.subtract_dark = true;
257 self.reset_cache();
258 Ok(self)
259 }
260
261 pub fn with_flat_field(mut self, flat: Vec<f64>) -> Result<Self> {
262 self.validate_correction("flat field", &flat, false)?;
263 if flat.iter().any(|&value| value <= 0.0) {
264 return Err(Error::InvalidMeasurements(
265 "flat-field values must be positive".into(),
266 ));
267 }
268 self.flat_field = Some(flat);
269 self.preprocessing.divide_flat_field = true;
270 self.reset_cache();
271 Ok(self)
272 }
273
274 pub fn with_background(mut self, background: Vec<f64>) -> Result<Self> {
275 self.validate_correction("background", &background, true)?;
276 self.background = Some(background);
277 self.preprocessing.subtract_background = true;
278 self.reset_cache();
279 Ok(self)
280 }
281
282 pub fn with_masks(mut self, masks: Vec<u8>) -> Result<Self> {
283 let frame_len = self.frame_len();
284 let stack_len = self.stack_len()?;
285 if masks.len() != frame_len && masks.len() != stack_len {
286 return Err(Error::InvalidMeasurements(format!(
287 "mask length {} must be {frame_len} or {stack_len}",
288 masks.len()
289 )));
290 }
291 self.masks = Some(masks);
292 Ok(self)
293 }
294
295 pub fn normalize_exposure(mut self) -> Self {
296 self.preprocessing.normalize_exposure = true;
297 self.reset_cache();
298 self
299 }
300
301 pub fn clamp_negative(mut self) -> Self {
302 self.preprocessing.clamp_negative = true;
303 self.reset_cache();
304 self
305 }
306
307 pub fn with_preprocessing(mut self, preprocessing: PreprocessingConfig) -> Result<Self> {
310 self.preprocessing = preprocessing;
311 self.reset_cache();
312 self.validate()?;
313 Ok(self)
314 }
315
316 pub fn with_cache_capacity(mut self, capacity: usize) -> Result<Self> {
318 if capacity == 0 {
319 return Err(Error::InvalidParameter {
320 name: "cache_capacity",
321 reason: "must be greater than zero".into(),
322 });
323 }
324 self.cache_capacity = capacity;
325 self.reset_cache();
326 Ok(self)
327 }
328
329 pub fn with_cache_byte_capacity(mut self, capacity: usize) -> Result<Self> {
332 let frame_bytes = self.frame_bytes()?;
333 if capacity < frame_bytes {
334 return Err(Error::InvalidParameter {
335 name: "cache_byte_capacity",
336 reason: format!("must be at least one decoded frame ({frame_bytes} bytes)"),
337 });
338 }
339 self.cache_byte_capacity = Some(capacity);
340 self.reset_cache();
341 Ok(self)
342 }
343
344 pub fn frame_count(&self) -> usize {
345 self.paths.len()
346 }
347
348 pub fn image_shape(&self) -> (usize, usize) {
349 self.image_shape
350 }
351
352 pub fn frame_len(&self) -> usize {
353 self.image_shape.0 * self.image_shape.1
354 }
355
356 pub fn frame_metadata(&self) -> &[FrameMetadata] {
357 &self.frame_metadata
358 }
359
360 pub fn preprocessing(&self) -> &PreprocessingConfig {
361 &self.preprocessing
362 }
363
364 pub fn paths(&self) -> &[PathBuf] {
367 &self.paths
368 }
369
370 pub fn cached_frame_count(&self) -> usize {
371 self.cache.lock().map_or(0, |cache| cache.frames.len())
375 }
376
377 pub fn cached_byte_count(&self) -> usize {
378 self.cache.lock().map_or(0, |cache| cache.bytes)
379 }
380
381 pub fn frame(&self, index: usize) -> Result<Arc<Vec<f64>>> {
382 if index >= self.frame_count() {
383 return Err(Error::FrameOutOfRange {
384 index,
385 frames: self.frame_count(),
386 });
387 }
388 {
389 let mut cache = self.lock_cache()?;
390 if let Some(frame) = cache.frames.get(&index).cloned() {
391 cache.touch(index);
392 return Ok(frame);
393 }
394 }
395
396 let path = &self.paths[index];
399 let mut values = match self.page_indices[index] {
400 Some(page_index) => {
401 load_grayscale_tiff_page(path, page_index, GrayscaleScaling::NativeCounts)?
402 }
403 None => load_grayscale(path, GrayscaleScaling::NativeCounts)?,
404 };
405 if values.shape() != self.image_shape {
406 return Err(Error::InvalidMeasurements(format!(
407 "image {} changed shape to {:?}, expected {:?}",
408 path.display(),
409 values.shape(),
410 self.image_shape
411 )));
412 }
413 self.preprocess_frame(index, values.as_mut_slice())?;
414 let loaded = Arc::new(values.into_vec());
415 let loaded_bytes = loaded.len().checked_mul(size_of::<f64>()).ok_or_else(|| {
416 Error::InvalidMeasurements("decoded lazy frame byte length overflows".into())
417 })?;
418
419 let mut cache = self.lock_cache()?;
420 if let Some(existing) = cache.frames.get(&index).cloned() {
421 cache.touch(index);
422 return Ok(existing);
423 }
424 while cache.frames.len() >= self.cache_capacity
425 || self
426 .cache_byte_capacity
427 .is_some_and(|capacity| cache.bytes.saturating_add(loaded_bytes) > capacity)
428 {
429 if !cache.evict_least_recently_used() {
430 break;
431 }
432 }
433 cache.frames.insert(index, loaded.clone());
434 cache.bytes = cache.bytes.checked_add(loaded_bytes).ok_or_else(|| {
435 Error::InvalidMeasurements("lazy cache byte accounting overflows".into())
436 })?;
437 cache.touch(index);
438 Ok(loaded)
439 }
440
441 pub fn frame_weight(&self, index: usize) -> Result<f64> {
442 self.frame_metadata
443 .get(index)
444 .map(|metadata| metadata.weight)
445 .ok_or(Error::FrameOutOfRange {
446 index,
447 frames: self.frame_count(),
448 })
449 }
450
451 pub fn frame_mask(&self, index: usize) -> Result<Option<&[u8]>> {
452 if index >= self.frame_count() {
453 return Err(Error::FrameOutOfRange {
454 index,
455 frames: self.frame_count(),
456 });
457 }
458 let Some(masks) = &self.masks else {
459 return Ok(None);
460 };
461 let frame_len = self.frame_len();
462 if masks.len() == frame_len {
463 Ok(Some(masks))
464 } else {
465 let start = index * frame_len;
466 Ok(Some(&masks[start..start + frame_len]))
467 }
468 }
469
470 pub fn materialize(&self) -> Result<MeasurementStack> {
471 let mut data = Vec::with_capacity(self.stack_len()?);
472 for frame in 0..self.frame_count() {
473 data.extend_from_slice(self.frame(frame)?.as_slice());
474 }
475 let mut stack =
476 MeasurementStack::from_vec(data, self.image_shape, self.frame_metadata.clone())?;
477 if let Some(masks) = &self.masks {
478 stack = stack.with_masks(masks.clone())?;
479 }
480 Ok(stack)
481 }
482
483 pub fn validate(&self) -> Result<()> {
484 if self.paths.is_empty()
485 || self.frame_metadata.len() != self.paths.len()
486 || self.page_indices.len() != self.paths.len()
487 || self.cache_capacity == 0
488 {
489 return Err(Error::InvalidMeasurements(
490 "lazy paths, page indices, and metadata must have equal non-zero counts".into(),
491 ));
492 }
493 let frame_bytes = self.frame_bytes()?;
494 for (index, metadata) in self.frame_metadata.iter().enumerate() {
495 if metadata.frame_index != index
496 || !metadata.exposure_time.is_finite()
497 || metadata.exposure_time <= 0.0
498 || !metadata.weight.is_finite()
499 || metadata.weight < 0.0
500 {
501 return Err(Error::InvalidMeasurements(format!(
502 "lazy frame metadata {index} is invalid"
503 )));
504 }
505 }
506 if let Some(dark) = &self.dark_frame {
507 self.validate_correction("dark frame", dark, false)?;
508 }
509 if let Some(flat) = &self.flat_field {
510 self.validate_correction("flat field", flat, false)?;
511 if flat.iter().any(|&value| value <= 0.0) {
512 return Err(Error::InvalidMeasurements(
513 "flat-field values must be positive".into(),
514 ));
515 }
516 }
517 if let Some(background) = &self.background {
518 self.validate_correction("background", background, true)?;
519 }
520 if let Some(masks) = &self.masks {
521 let frame_len = self.frame_len();
522 let stack_len = self.stack_len()?;
523 if masks.len() != frame_len && masks.len() != stack_len {
524 return Err(Error::InvalidMeasurements(
525 "lazy mask dimensions are invalid".into(),
526 ));
527 }
528 }
529 if self.preprocessing.subtract_dark && self.dark_frame.is_none() {
530 return Err(Error::InvalidMeasurements(
531 "dark subtraction requested without a dark frame".into(),
532 ));
533 }
534 if self.preprocessing.divide_flat_field && self.flat_field.is_none() {
535 return Err(Error::InvalidMeasurements(
536 "flat-field correction requested without a flat field".into(),
537 ));
538 }
539 if self.preprocessing.subtract_background && self.background.is_none() {
540 return Err(Error::InvalidMeasurements(
541 "background subtraction requested without a background".into(),
542 ));
543 }
544 if self
545 .cache_byte_capacity
546 .is_some_and(|capacity| capacity < frame_bytes)
547 {
548 return Err(Error::InvalidMeasurements(
549 "lazy byte cache must fit at least one decoded frame".into(),
550 ));
551 }
552 Ok(())
553 }
554
555 fn preprocess_frame(&self, frame_index: usize, values: &mut [f64]) -> Result<()> {
556 let frame_len = self.frame_len();
557 let exposure = self.frame_metadata[frame_index].exposure_time;
558 let dark = if self.preprocessing.subtract_dark {
559 Some(self.dark_frame.as_deref().ok_or_else(|| {
560 Error::InvalidMeasurements("dark subtraction requested without a dark frame".into())
561 })?)
562 } else {
563 None
564 };
565 let background = if self.preprocessing.subtract_background {
566 Some(self.background.as_deref().ok_or_else(|| {
567 Error::InvalidMeasurements(
568 "background subtraction requested without a background".into(),
569 )
570 })?)
571 } else {
572 None
573 };
574 let flat = if self.preprocessing.divide_flat_field {
575 Some(self.flat_field.as_deref().ok_or_else(|| {
576 Error::InvalidMeasurements(
577 "flat-field correction requested without a flat field".into(),
578 )
579 })?)
580 } else {
581 None
582 };
583 for (pixel, value) in values.iter_mut().enumerate() {
584 if let Some(dark) = dark {
585 *value -= dark[pixel];
586 }
587 if let Some(background) = background {
588 *value -= background[if background.len() == frame_len {
589 pixel
590 } else {
591 frame_index * frame_len + pixel
592 }];
593 }
594 if let Some(flat) = flat {
595 *value /= flat[pixel];
596 }
597 if self.preprocessing.normalize_exposure {
598 *value /= exposure;
599 }
600 if self.preprocessing.clamp_negative {
601 *value = value.max(0.0);
602 }
603 if !value.is_finite() {
604 return Err(Error::InvalidMeasurements(format!(
605 "preprocessing frame {frame_index} produced a non-finite value at pixel {pixel}"
606 )));
607 }
608 }
609 Ok(())
610 }
611
612 fn validate_correction(&self, name: &str, values: &[f64], per_frame: bool) -> Result<()> {
613 let frame_len = self.frame_len();
614 let stack_len = self.stack_len()?;
615 let valid_length = values.len() == frame_len || (per_frame && values.len() == stack_len);
616 if !valid_length || values.iter().any(|value| !value.is_finite()) {
617 return Err(Error::InvalidMeasurements(format!(
618 "{name} must contain finite values and have length {frame_len}{}",
619 if per_frame {
620 format!(" or {stack_len}")
621 } else {
622 String::new()
623 }
624 )));
625 }
626 Ok(())
627 }
628
629 fn frame_bytes(&self) -> Result<usize> {
630 self.image_shape
631 .0
632 .checked_mul(self.image_shape.1)
633 .filter(|&length| length > 0)
634 .and_then(|length| length.checked_mul(size_of::<f64>()))
635 .ok_or_else(|| Error::InvalidShape("lazy measurement shape is invalid".into()))
636 }
637
638 fn stack_len(&self) -> Result<usize> {
639 self.frame_len()
640 .checked_mul(self.frame_count())
641 .ok_or_else(|| {
642 Error::InvalidMeasurements("lazy measurement stack length overflows".into())
643 })
644 }
645
646 fn reset_cache(&mut self) {
647 self.cache = Mutex::new(FrameCache::default());
648 }
649
650 fn lock_cache(&self) -> Result<std::sync::MutexGuard<'_, FrameCache>> {
651 self.cache
652 .lock()
653 .map_err(|_| Error::Numerical("lazy measurement cache lock was poisoned".into()))
654 }
655}
656
657fn metadata_or_labels(
658 paths: &[PathBuf],
659 frame_metadata: Vec<FrameMetadata>,
660 label: impl Fn(&Path, usize) -> String,
661) -> Vec<FrameMetadata> {
662 if frame_metadata.is_empty() {
663 paths
664 .iter()
665 .enumerate()
666 .map(|(index, path)| {
667 let mut metadata = FrameMetadata::new(index);
668 metadata.label = Some(label(path, index));
669 metadata
670 })
671 .collect()
672 } else {
673 frame_metadata
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use std::panic::{AssertUnwindSafe, catch_unwind};
680
681 use super::*;
682
683 #[test]
684 fn poisoned_cache_fails_frame_reads_and_hides_unavailable_statistics() {
685 let stack = LazyMeasurementStack::new(
686 vec![PathBuf::from("unreadable-after-poison.png")],
687 vec![None],
688 (1, 1),
689 vec![FrameMetadata::new(0)],
690 )
691 .unwrap();
692
693 let result = catch_unwind(AssertUnwindSafe(|| {
694 let _guard = stack.cache.lock().unwrap();
695 panic!("intentional cache-lock poison for test");
696 }));
697 assert!(result.is_err());
698
699 assert!(
700 matches!(stack.frame(0), Err(Error::Numerical(message)) if message.contains("poisoned"))
701 );
702 assert_eq!(stack.cached_frame_count(), 0);
703 assert_eq!(stack.cached_byte_count(), 0);
704 }
705}