Skip to main content

glint_mask_tools/core/
postprocessor.rs

1/// Post-processing abstraction for mask refinement.
2///
3/// This module defines the [`PostProcessor`] trait which provides a uniform
4/// interface for post-processing generated masks. The primary post-processor
5/// is pixel buffering, but the trait allows for composable operations.
6use crate::error::Result;
7use ndarray::Array2;
8
9/// Trait for post-processing glint masks.
10///
11/// Post-processors take binary masks and apply various refinement operations
12/// such as morphological operations, buffering, or filtering.
13pub trait PostProcessor: Send + Sync {
14    /// Apply post-processing to a binary mask
15    ///
16    /// # Arguments
17    ///
18    /// * `mask` - Binary mask with shape (height, width)
19    ///
20    /// # Returns
21    ///
22    /// A processed binary mask with the same shape
23    fn process_mask(&self, mask: &Array2<u8>) -> Result<Array2<u8>>;
24
25    /// Get the name of this post-processor
26    fn name(&self) -> &'static str;
27
28    /// Get a description of this post-processor
29    fn description(&self) -> &'static str;
30
31    /// Validate that the post-processor parameters are valid
32    fn validate_parameters(&self) -> Result<()> {
33        Ok(())
34    }
35}
36
37/// Composite post-processor that applies multiple processors in sequence
38pub struct CompositePostProcessor {
39    processors: Vec<Box<dyn PostProcessor>>,
40}
41
42impl CompositePostProcessor {
43    /// Create a new composite post-processor
44    pub fn new() -> Self {
45        Self {
46            processors: Vec::new(),
47        }
48    }
49
50    /// Add a post-processor to the pipeline
51    pub fn add_processor(mut self, processor: Box<dyn PostProcessor>) -> Self {
52        self.processors.push(processor);
53        self
54    }
55
56    /// Get the number of processors in the pipeline
57    pub fn len(&self) -> usize {
58        self.processors.len()
59    }
60
61    /// Check if the pipeline is empty
62    pub fn is_empty(&self) -> bool {
63        self.processors.is_empty()
64    }
65}
66
67impl Default for CompositePostProcessor {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl PostProcessor for CompositePostProcessor {
74    fn process_mask(&self, mask: &Array2<u8>) -> Result<Array2<u8>> {
75        let mut result = mask.clone();
76
77        for processor in &self.processors {
78            result = processor.process_mask(&result)?;
79        }
80
81        Ok(result)
82    }
83
84    fn name(&self) -> &'static str {
85        "Composite"
86    }
87
88    fn description(&self) -> &'static str {
89        "Applies multiple post-processors in sequence"
90    }
91
92    fn validate_parameters(&self) -> Result<()> {
93        for processor in &self.processors {
94            processor.validate_parameters()?;
95        }
96        Ok(())
97    }
98}
99
100/// Pixel buffer post-processor that dilates the mask by a specified radius
101#[derive(Debug, Clone)]
102pub struct PixelBufferProcessor {
103    radius: usize,
104    kernel: Array2<bool>,
105}
106
107impl PixelBufferProcessor {
108    /// Create a new pixel buffer processor
109    pub fn new(radius: usize) -> Self {
110        let kernel = create_circular_kernel(radius);
111        Self { radius, kernel }
112    }
113
114    /// Get the buffer radius
115    pub fn radius(&self) -> usize {
116        self.radius
117    }
118
119    /// Create a kernel for morphological operations
120    fn create_kernel(&self) -> &Array2<bool> {
121        &self.kernel
122    }
123}
124
125impl PostProcessor for PixelBufferProcessor {
126    fn process_mask(&self, mask: &Array2<u8>) -> Result<Array2<u8>> {
127        if self.radius == 0 {
128            return Ok(mask.clone());
129        }
130
131        let (height, width) = mask.dim();
132        let mut result = Array2::zeros((height, width));
133        let kernel = self.create_kernel();
134        let (kernel_height, kernel_width) = kernel.dim();
135        let kernel_center_y = kernel_height / 2;
136        let kernel_center_x = kernel_width / 2;
137
138        for y in 0..height {
139            for x in 0..width {
140                let mut should_mask = false;
141
142                // Check the kernel area around this pixel
143                for ky in 0..kernel_height {
144                    for kx in 0..kernel_width {
145                        if !kernel[[ky, kx]] {
146                            continue;
147                        }
148
149                        let img_y = y as i32 + ky as i32 - kernel_center_y as i32;
150                        let img_x = x as i32 + kx as i32 - kernel_center_x as i32;
151
152                        if img_y >= 0
153                            && img_y < height as i32
154                            && img_x >= 0
155                            && img_x < width as i32
156                            && mask[[img_y as usize, img_x as usize]] > 0
157                        {
158                            should_mask = true;
159                            break;
160                        }
161                    }
162                    if should_mask {
163                        break;
164                    }
165                }
166
167                result[[y, x]] = if should_mask { 1 } else { 0 };
168            }
169        }
170
171        Ok(result)
172    }
173
174    fn name(&self) -> &'static str {
175        "PixelBuffer"
176    }
177
178    fn description(&self) -> &'static str {
179        "Dilates the mask by a specified pixel radius"
180    }
181
182    fn validate_parameters(&self) -> Result<()> {
183        // Radius validation could be added here if needed
184        Ok(())
185    }
186}
187
188/// Metashape format converter that inverts the mask and converts to the expected format
189#[derive(Debug, Clone)]
190pub struct MetashapeConverter;
191
192impl MetashapeConverter {
193    /// Create a new Metashape converter
194    pub fn new() -> Self {
195        Self
196    }
197}
198
199impl Default for MetashapeConverter {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205impl PostProcessor for MetashapeConverter {
206    fn process_mask(&self, mask: &Array2<u8>) -> Result<Array2<u8>> {
207        // Invert the mask and scale to 0-255 range
208        // In our convention: 1 = mask (glint), 0 = keep
209        // In Metashape: 0 = mask (ignore), 255 = keep
210        let result = mask.map(|&pixel| if pixel > 0 { 0 } else { 255 });
211        Ok(result)
212    }
213
214    fn name(&self) -> &'static str {
215        "Metashape"
216    }
217
218    fn description(&self) -> &'static str {
219        "Converts mask to Metashape format (inverted, 0-255 range)"
220    }
221}
222
223/// Create a circular kernel for morphological operations
224fn create_circular_kernel(radius: usize) -> Array2<bool> {
225    let size = 2 * radius + 1;
226    let mut kernel = Array2::from_elem((size, size), false);
227    let center = radius as i32;
228
229    for y in 0..size {
230        for x in 0..size {
231            let dy = y as i32 - center;
232            let dx = x as i32 - center;
233            let distance_sq = dx * dx + dy * dy;
234
235            if distance_sq <= (radius as i32) * (radius as i32) {
236                kernel[[y, x]] = true;
237            }
238        }
239    }
240
241    kernel
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_circular_kernel() {
250        let kernel = create_circular_kernel(1);
251        assert_eq!(kernel.dim(), (3, 3));
252        // Center should be true
253        assert!(kernel[[1, 1]]);
254        // Adjacent pixels should be true
255        assert!(kernel[[0, 1]]);
256        assert!(kernel[[1, 0]]);
257        assert!(kernel[[2, 1]]);
258        assert!(kernel[[1, 2]]);
259        // Corners should be false for radius 1
260        assert!(!kernel[[0, 0]]);
261        assert!(!kernel[[2, 2]]);
262    }
263
264    #[test]
265    fn test_pixel_buffer_processor() {
266        let mut mask = ndarray::Array2::zeros((5, 5));
267        mask[[2, 2]] = 1; // Single pixel in center
268
269        let processor = PixelBufferProcessor::new(1);
270        let result = processor.process_mask(&mask).unwrap();
271
272        // Should have expanded to adjacent pixels
273        assert_eq!(result[[2, 2]], 1);
274        assert_eq!(result[[1, 2]], 1);
275        assert_eq!(result[[3, 2]], 1);
276        assert_eq!(result[[2, 1]], 1);
277        assert_eq!(result[[2, 3]], 1);
278
279        // Corners should still be 0
280        assert_eq!(result[[0, 0]], 0);
281        assert_eq!(result[[4, 4]], 0);
282    }
283
284    #[test]
285    fn test_metashape_converter() {
286        let mut mask = ndarray::Array2::zeros((3, 3));
287        mask[[1, 1]] = 1; // Single masked pixel
288
289        let converter = MetashapeConverter::new();
290        let result = converter.process_mask(&mask).unwrap();
291
292        // Masked pixel should become 0
293        assert_eq!(result[[1, 1]], 0);
294        // Unmasked pixels should become 255
295        assert_eq!(result[[0, 0]], 255);
296        assert_eq!(result[[2, 2]], 255);
297    }
298
299    #[test]
300    fn test_composite_processor() {
301        let mut mask = ndarray::Array2::zeros((5, 5));
302        mask[[2, 2]] = 1;
303
304        let processor = CompositePostProcessor::new()
305            .add_processor(Box::new(PixelBufferProcessor::new(1)))
306            .add_processor(Box::new(MetashapeConverter::new()));
307
308        let result = processor.process_mask(&mask).unwrap();
309
310        // Should have buffered and then converted to Metashape format
311        assert_eq!(result[[2, 2]], 0); // Center (was masked, buffered, then inverted)
312        assert_eq!(result[[1, 2]], 0); // Adjacent (was buffered, then inverted)
313        assert_eq!(result[[0, 0]], 255); // Corner (was not buffered, inverted to 255)
314    }
315}