glint_mask_tools/core/
image_loader.rs1use crate::error::{GlintError, Result};
7use ndarray::{Array2, Array3};
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
13pub struct ImageCapture {
14 pub id: String,
16 pub paths: Vec<PathBuf>,
18 pub mask_paths: Vec<PathBuf>,
20}
21
22pub trait ImageLoader: Send + Sync {
28 fn as_any(&self) -> &dyn std::any::Any;
30 fn discover_captures(&self, input_dir: &Path, output_dir: &Path) -> Result<Vec<ImageCapture>>;
36
37 fn load_image(&self, capture: &ImageCapture) -> Result<Array3<f64>>;
46
47 fn band_count(&self) -> usize;
49
50 fn bit_depth(&self) -> u8;
52
53 fn supported_extensions(&self) -> Vec<String>;
55
56 fn validate_capture(&self, capture: &ImageCapture) -> Result<()> {
58 for path in &capture.paths {
60 if !path.exists() {
61 return Err(GlintError::MissingFiles {
62 files: vec![path.clone()],
63 });
64 }
65 }
66
67 if capture.paths.len() != self.expected_file_count() {
69 return Err(GlintError::validation(format!(
70 "Expected {} files, got {}",
71 self.expected_file_count(),
72 capture.paths.len()
73 )));
74 }
75
76 Ok(())
77 }
78
79 fn expected_file_count(&self) -> usize {
81 1 }
83
84 fn save_mask(&self, mask: &Array2<u8>, path: &Path) -> Result<()> {
86 let (height, width) = mask.dim();
87 let mut img_buffer = vec![0u8; height * width];
88
89 for (i, row) in mask.rows().into_iter().enumerate() {
90 for (j, &pixel) in row.iter().enumerate() {
91 img_buffer[i * width + j] = pixel;
92 }
93 }
94
95 let img = image::GrayImage::from_raw(width as u32, height as u32, img_buffer)
96 .ok_or_else(|| GlintError::processing("Failed to create image from mask data"))?;
97
98 if let Some(parent) = path.parent() {
100 std::fs::create_dir_all(parent)?;
101 }
102
103 img.save(path)?;
104 Ok(())
105 }
106
107 fn save_masks(&self, mask: &Array2<u8>, capture: &ImageCapture) -> Result<()> {
109 for mask_path in &capture.mask_paths {
110 self.save_mask(mask, mask_path)?;
111 }
112 Ok(())
113 }
114
115 fn generate_mask_path(&self, capture: &ImageCapture, output_dir: &Path) -> PathBuf {
117 let filename = format!("{}_mask.png", capture.id);
118 output_dir.join(filename)
119 }
120
121 fn generate_mask_paths(&self, capture: &ImageCapture, output_dir: &Path) -> Vec<PathBuf> {
123 capture
124 .paths
125 .iter()
126 .map(|input_path| {
127 let filename = if let Some(stem) = input_path.file_stem() {
128 format!("{}_mask.png", stem.to_string_lossy())
129 } else {
130 format!("{}_mask.png", capture.id)
131 };
132 output_dir.join(filename)
133 })
134 .collect()
135 }
136}
137
138pub fn normalize_image(img: &Array3<f64>, bit_depth: u8) -> Result<Array3<f64>> {
140 let max_value = match bit_depth {
141 8 => 255.0,
142 16 => 65535.0,
143 32 => 4294967295.0,
144 _ => return Err(GlintError::InvalidBitDepth { bit_depth }),
145 };
146
147 Ok(img / max_value)
148}
149
150pub fn list_image_files(dir: &Path, extensions: &[String]) -> Result<Vec<PathBuf>> {
152 let mut files = Vec::new();
153
154 for entry in std::fs::read_dir(dir)? {
155 let entry = entry?;
156 let path = entry.path();
157
158 if path.is_file() {
159 if let Some(ext) = path.extension() {
160 let ext_str = ext.to_string_lossy().to_lowercase();
161 if extensions.iter().any(|e| e.to_lowercase() == ext_str) {
162 files.push(path);
163 }
164 }
165 }
166 }
167
168 files.sort();
169 Ok(files)
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use tempfile::tempdir;
176
177 #[test]
178 fn test_normalize_image() {
179 let img = Array3::from_shape_vec((2, 2, 1), vec![0.0, 127.0, 255.0, 128.0]).unwrap();
180 let normalized = normalize_image(&img, 8).unwrap();
181
182 assert_eq!(normalized[[0, 0, 0]], 0.0);
183 assert!((normalized[[0, 1, 0]] - 0.498).abs() < 0.001);
184 assert_eq!(normalized[[1, 0, 0]], 1.0);
185 }
186
187 #[test]
188 fn test_list_image_files() {
189 let dir = tempdir().unwrap();
190 let img_path = dir.path().join("test.jpg");
191 std::fs::write(&img_path, b"fake image data").unwrap();
192
193 let files = list_image_files(dir.path(), &["jpg".to_string()]).unwrap();
194 assert_eq!(files.len(), 1);
195 assert_eq!(files[0], img_path);
196 }
197}