Skip to main content

iris/inpaint/
mod.rs

1use crate::error::{IrisError, Result};
2use crate::image::Image;
3use burn::tensor::{Tensor, TensorData, backend::Backend};
4use std::cmp::Reverse;
5use std::collections::BinaryHeap;
6
7const INFINITY: f32 = 1.0e10;
8
9/// Wrapper around `f32` that implements `Ord` (panics on NaN).
10#[derive(Clone, Copy, PartialEq)]
11struct OrdF32(f32);
12
13impl Eq for OrdF32 {}
14
15impl PartialOrd for OrdF32 {
16    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
17        Some(self.cmp(other))
18    }
19}
20
21impl Ord for OrdF32 {
22    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
23        self.0
24            .partial_cmp(&other.0)
25            .unwrap_or(std::cmp::Ordering::Equal)
26    }
27}
28
29/// Telea (Fast Marching Method) image inpainting.
30///
31/// Replaces pixels where `mask > 0` by propagating values from the boundary
32/// of the known region inward, weighted by distance and gradient.
33///
34/// * `image` – input image of shape `[C, H, W]`, values in `[0, 1]`.
35/// * `mask`  – single-channel mask of shape `[1, H, W]` where `0` = known, `>0` = to inpaint.
36/// * `radius` – maximum propagation radius in pixels; known pixels farther than this are ignored.
37pub fn inpaint<B: Backend>(image: &Image<B>, mask: &Image<B>, radius: f32) -> Result<Image<B>> {
38    let img_dims = image.tensor.dims();
39    let mask_dims = mask.tensor.dims();
40
41    if img_dims.len() != 3 || mask_dims.len() != 3 {
42        return Err(IrisError::InvalidParameter(
43            "image must be [C,H,W] and mask must be [1,H,W]".into(),
44        ));
45    }
46    if img_dims[1] != mask_dims[1] || img_dims[2] != mask_dims[2] {
47        return Err(IrisError::DimensionMismatch {
48            expected: vec![img_dims[0], mask_dims[1], mask_dims[2]],
49            actual: img_dims.to_vec(),
50        });
51    }
52    if mask_dims[0] != 1 {
53        return Err(IrisError::InvalidParameter(
54            "mask must have exactly 1 channel".into(),
55        ));
56    }
57
58    let c = img_dims[0];
59    let h = img_dims[1];
60    let w = img_dims[2];
61    let pixels = h * w;
62
63    let img_data = image.tensor.clone().into_data();
64    let img_vals: Vec<f32> = img_data.iter::<f32>().collect();
65    let mask_data = mask.tensor.clone().into_data();
66    let mask_vals: Vec<f32> = mask_data.iter::<f32>().collect();
67
68    let mut inpaint_buf: Vec<Vec<f32>> = (0..c)
69        .map(|ch| img_vals[ch * pixels..(ch + 1) * pixels].to_vec())
70        .collect();
71
72    #[derive(Clone, Copy, PartialEq, Eq)]
73    enum State {
74        Known,
75        Band,
76        Unknown,
77    }
78
79    let mut state = vec![State::Unknown; pixels];
80    let mut dist = vec![INFINITY; pixels];
81
82    let neighbours = |y: usize, x: usize| -> [(isize, isize); 4] {
83        [
84            (y as isize - 1, x as isize),
85            (y as isize + 1, x as isize),
86            (y as isize, x as isize - 1),
87            (y as isize, x as isize + 1),
88        ]
89    };
90
91    let mut heap: BinaryHeap<(Reverse<OrdF32>, usize)> = BinaryHeap::new();
92
93    for y in 0..h {
94        for x in 0..w {
95            let idx = y * w + x;
96            if mask_vals[idx] == 0.0 {
97                state[idx] = State::Known;
98                dist[idx] = 0.0;
99                for (ny, nx) in neighbours(y, x) {
100                    if ny >= 0 && ny < h as isize && nx >= 0 && nx < w as isize {
101                        let ni = ny as usize * w + nx as usize;
102                        if state[ni] == State::Unknown {
103                            state[ni] = State::Band;
104                            dist[ni] = 1.0;
105                            heap.push((Reverse(OrdF32(1.0)), ni));
106                        }
107                    }
108                }
109            }
110        }
111    }
112
113    // FMM narrow-band neighbours helper: returns indices of known/band 4-neighbours
114    let band_neighbours = |y: usize, x: usize, st: &[State], w_: usize, h_: usize| -> Vec<usize> {
115        let mut result = Vec::with_capacity(4);
116        for (ny, nx) in neighbours(y, x) {
117            if ny < 0 || ny >= h_ as isize || nx < 0 || nx >= w_ as isize {
118                continue;
119            }
120            let ni = ny as usize * w_ + nx as usize;
121            if st[ni] != State::Unknown {
122                result.push(ni);
123            }
124        }
125        result
126    };
127
128    while let Some((Reverse(OrdF32(d)), idx)) = heap.pop() {
129        if state[idx] == State::Known {
130            continue;
131        }
132        if d > radius {
133            break;
134        }
135
136        let y = idx / w;
137        let x = idx % w;
138
139        // Gather known/band neighbours for weighted interpolation
140        let known_nbrs = band_neighbours(y, x, &state, w, h);
141
142        let mut sum_weight = 0.0f64;
143        let mut sum_vals: Vec<f64> = vec![0.0; c];
144
145        // Pre-compute local gradient magnitude (averaged over known neighbours)
146        // using finite differences of the known/band neighbour values.
147        let mut grad_mag: f64 = 0.0;
148        for &ni in &known_nbrs {
149            let ny = ni / w;
150            let nx = ni % w;
151            let dy_dir = ny as f64 - y as f64;
152            let dx_dir = nx as f64 - x as f64;
153            let spatial_dist = (dy_dir * dy_dir + dx_dir * dx_dir).sqrt().max(1.0e-6);
154
155            // Compute gradient of the image at this known neighbour using its own known neighbours
156            let mut g_magnitude: f64 = 0.0;
157            let nnbrs = band_neighbours(ny, nx, &state, w, h);
158            for &nni in &nnbrs {
159                let nny = nni / w;
160                let nnx = nni % w;
161                let ddy = nny as f64 - ny as f64;
162                let ddx = nnx as f64 - nx as f64;
163                let ndist = (ddy * ddy + ddx * ddx).sqrt().max(1.0e-6);
164                for ch in 0..c {
165                    let diff = inpaint_buf[ch][nni] as f64 - inpaint_buf[ch][ni] as f64;
166                    g_magnitude += diff * diff;
167                }
168                let _ = ndist;
169            }
170            grad_mag = grad_mag.max(g_magnitude.sqrt());
171
172            // Spatial weight (inverse distance squared)
173            let w_spatial = 1.0 / (spatial_dist * spatial_dist);
174
175            // Directional weight: prefer propagation along edges (perpendicular to gradient).
176            // The Telea anisotropic term: if the propagation direction is along the gradient,
177            // reduce weight (we want to propagate across edges, not along them).
178            // beta is high when propagation is perpendicular to the gradient.
179            let beta = if grad_mag > 1.0e-6 {
180                // Compute gradient direction components at this known neighbour
181                let mut gx: f64 = 0.0;
182                let mut gy: f64 = 0.0;
183                for &nni in &nnbrs {
184                    let nny = nni / w;
185                    let nnx = nni % w;
186                    let ddy = nny as f64 - ny as f64;
187                    let ddx = nnx as f64 - nx as f64;
188                    for ch in 0..c {
189                        let diff = inpaint_buf[ch][nni] as f64 - inpaint_buf[ch][ni] as f64;
190                        gx += diff * ddx;
191                        gy += diff * ddy;
192                    }
193                }
194                let gnorm = (gx * gx + gy * gy).sqrt().max(1.0e-12);
195                gx /= gnorm;
196                gy /= gnorm;
197
198                // Propagation direction (from known neighbour toward current pixel)
199                let px = x as f64 - nx as f64;
200                let py = y as f64 - ny as f64;
201                let pnorm = (px * px + py * py).sqrt().max(1.0e-12);
202                let pxn = px / pnorm;
203                let pyn = py / pnorm;
204
205                // Cross product magnitude = sin(angle between gradient and propagation)
206                // High value means propagation is perpendicular to gradient (along edge) -> good
207                (gx * pyn - gy * pxn).abs()
208            } else {
209                0.5 // no gradient info, neutral weight
210            };
211
212            let weight = w_spatial * (1.0 + beta);
213
214            for ch in 0..c {
215                sum_vals[ch] += inpaint_buf[ch][ni] as f64 * weight;
216            }
217            sum_weight += weight;
218        }
219
220        if sum_weight > 1.0e-12 {
221            for ch in 0..c {
222                inpaint_buf[ch][idx] = (sum_vals[ch] / sum_weight) as f32;
223            }
224        }
225
226        state[idx] = State::Known;
227
228        for (ny, nx) in neighbours(y, x) {
229            if ny < 0 || ny >= h as isize || nx < 0 || nx >= w as isize {
230                continue;
231            }
232            let ni = ny as usize * w + nx as usize;
233            if state[ni] == State::Known {
234                continue;
235            }
236            let new_dist = dist[idx] + 1.0;
237            if new_dist < dist[ni] {
238                dist[ni] = new_dist;
239            }
240            if state[ni] == State::Unknown {
241                state[ni] = State::Band;
242            }
243            heap.push((Reverse(OrdF32(dist[ni])), ni));
244        }
245    }
246
247    for idx in 0..pixels {
248        if state[idx] != State::Known {
249            for ch in 0..c {
250                inpaint_buf[ch][idx] = img_vals[ch * pixels + idx];
251            }
252        }
253    }
254
255    let mut flat = vec![0.0f32; c * pixels];
256    for ch in 0..c {
257        flat[ch * pixels..(ch + 1) * pixels].copy_from_slice(&inpaint_buf[ch]);
258    }
259
260    let device = image.tensor.device();
261    let new_data = TensorData::new(flat, [c, h, w]);
262    let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
263    Ok(Image::new(new_tensor))
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::test_helpers::{TestBackend, test_device};
270    use burn::tensor::{Tensor, TensorData};
271
272    #[test]
273    fn test_inpaint_no_mask() {
274        let device = test_device();
275        let data = vec![0.25f32; 3 * 8 * 8];
276        let img = Image::new(Tensor::<TestBackend, 3>::from_data(
277            TensorData::new(data, [3, 8, 8]),
278            &device,
279        ));
280        let mask = Image::new(Tensor::<TestBackend, 3>::from_data(
281            TensorData::new(vec![0.0f32; 8 * 8], [1, 8, 8]),
282            &device,
283        ));
284
285        let result = inpaint(&img, &mask, 5.0).unwrap();
286        assert_eq!(result.shape(), [3, 8, 8]);
287        let vals: Vec<f32> = result.tensor.into_data().iter::<f32>().collect();
288        for v in vals {
289            assert!((v - 0.25).abs() < 1e-6);
290        }
291    }
292
293    #[test]
294    fn test_inpaint_center_region() {
295        let device = test_device();
296        let h = 10usize;
297        let w = 10usize;
298
299        let mut img_vals = vec![0.0f32; h * w];
300        for y in 0..h {
301            for x in 0..w {
302                img_vals[y * w + x] = if x < w / 2 { 0.0 } else { 1.0 };
303            }
304        }
305        let img = Image::new(Tensor::<TestBackend, 3>::from_data(
306            TensorData::new(img_vals.clone(), [1, h, w]),
307            &device,
308        ));
309
310        let mut mask_vals = vec![0.0f32; h * w];
311        mask_vals[4 * w + 4] = 1.0;
312        mask_vals[4 * w + 5] = 1.0;
313        mask_vals[5 * w + 4] = 1.0;
314        mask_vals[5 * w + 5] = 1.0;
315        let mask = Image::new(Tensor::<TestBackend, 3>::from_data(
316            TensorData::new(mask_vals, [1, h, w]),
317            &device,
318        ));
319
320        let result = inpaint(&img, &mask, 5.0).unwrap();
321        assert_eq!(result.shape(), [1, h, w]);
322
323        let out: Vec<f32> = result.tensor.into_data().iter::<f32>().collect();
324
325        for y in 4..=5 {
326            for x in 4..=5 {
327                let v = out[y * w + x];
328                assert!((0.0..=1.0).contains(&v), "pixel ({},{}) = {}", x, y, v);
329            }
330        }
331        assert!((out[4 * w + 3] - img_vals[4 * w + 3]).abs() < 1e-6);
332        assert!((out[5 * w + 6] - img_vals[5 * w + 6]).abs() < 1e-6);
333    }
334
335    #[test]
336    fn test_inpaint_rgb_channel_independence() {
337        let device = test_device();
338        let h = 6usize;
339        let w = 6usize;
340
341        let mut img_vals = vec![0.0f32; 3 * h * w];
342        for y in 0..h {
343            for x in 0..w {
344                img_vals[y * w + x] = 0.1;
345                img_vals[h * w + y * w + x] = 0.5;
346                img_vals[2 * h * w + y * w + x] = 0.9;
347            }
348        }
349        let img = Image::new(Tensor::<TestBackend, 3>::from_data(
350            TensorData::new(img_vals, [3, h, w]),
351            &device,
352        ));
353
354        let mut mask_vals = vec![0.0f32; h * w];
355        mask_vals[3 * w + 3] = 1.0;
356        let mask = Image::new(Tensor::<TestBackend, 3>::from_data(
357            TensorData::new(mask_vals, [1, h, w]),
358            &device,
359        ));
360
361        let result = inpaint(&img, &mask, 10.0).unwrap();
362        let out: Vec<f32> = result.tensor.into_data().iter::<f32>().collect();
363
364        let r = out[3 * w + 3];
365        let g = out[h * w + 3 * w + 3];
366        let b = out[2 * h * w + 3 * w + 3];
367        assert!((r - 0.1).abs() < 0.01, "R={}", r);
368        assert!((g - 0.5).abs() < 0.01, "G={}", g);
369        assert!((b - 0.9).abs() < 0.01, "B={}", b);
370    }
371
372    #[test]
373    fn test_inpaint_dimension_mismatch() {
374        let device = test_device();
375        let img = Image::new(Tensor::<TestBackend, 3>::from_data(
376            TensorData::new(vec![0.5f32; 3 * 8 * 8], [3, 8, 8]),
377            &device,
378        ));
379        let mask = Image::new(Tensor::<TestBackend, 3>::from_data(
380            TensorData::new(vec![0.0f32; 6 * 6], [1, 6, 6]),
381            &device,
382        ));
383        let result = inpaint(&img, &mask, 5.0);
384        assert!(result.is_err());
385    }
386}