use crate::error::{IrisError, Result};
use crate::image::Image;
use burn::tensor::{Tensor, TensorData, backend::Backend};
use std::cmp::Reverse;
use std::collections::BinaryHeap;
const INFINITY: f32 = 1.0e10;
#[derive(Clone, Copy, PartialEq)]
struct OrdF32(f32);
impl Eq for OrdF32 {}
impl PartialOrd for OrdF32 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrdF32 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0
.partial_cmp(&other.0)
.unwrap_or(std::cmp::Ordering::Equal)
}
}
pub fn inpaint<B: Backend>(image: &Image<B>, mask: &Image<B>, radius: f32) -> Result<Image<B>> {
let img_dims = image.tensor.dims();
let mask_dims = mask.tensor.dims();
if img_dims.len() != 3 || mask_dims.len() != 3 {
return Err(IrisError::InvalidParameter(
"image must be [C,H,W] and mask must be [1,H,W]".into(),
));
}
if img_dims[1] != mask_dims[1] || img_dims[2] != mask_dims[2] {
return Err(IrisError::DimensionMismatch {
expected: vec![img_dims[0], mask_dims[1], mask_dims[2]],
actual: img_dims.to_vec(),
});
}
if mask_dims[0] != 1 {
return Err(IrisError::InvalidParameter(
"mask must have exactly 1 channel".into(),
));
}
let c = img_dims[0];
let h = img_dims[1];
let w = img_dims[2];
let pixels = h * w;
let img_data = image.tensor.clone().into_data();
let img_vals: Vec<f32> = img_data.iter::<f32>().collect();
let mask_data = mask.tensor.clone().into_data();
let mask_vals: Vec<f32> = mask_data.iter::<f32>().collect();
let mut inpaint_buf: Vec<Vec<f32>> = (0..c)
.map(|ch| img_vals[ch * pixels..(ch + 1) * pixels].to_vec())
.collect();
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
Known,
Band,
Unknown,
}
let mut state = vec![State::Unknown; pixels];
let mut dist = vec![INFINITY; pixels];
let neighbours = |y: usize, x: usize| -> [(isize, isize); 4] {
[
(y as isize - 1, x as isize),
(y as isize + 1, x as isize),
(y as isize, x as isize - 1),
(y as isize, x as isize + 1),
]
};
let mut heap: BinaryHeap<(Reverse<OrdF32>, usize)> = BinaryHeap::new();
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
if mask_vals[idx] == 0.0 {
state[idx] = State::Known;
dist[idx] = 0.0;
for (ny, nx) in neighbours(y, x) {
if ny >= 0 && ny < h as isize && nx >= 0 && nx < w as isize {
let ni = ny as usize * w + nx as usize;
if state[ni] == State::Unknown {
state[ni] = State::Band;
dist[ni] = 1.0;
heap.push((Reverse(OrdF32(1.0)), ni));
}
}
}
}
}
}
let band_neighbours = |y: usize, x: usize, st: &[State], w_: usize, h_: usize| -> Vec<usize> {
let mut result = Vec::with_capacity(4);
for (ny, nx) in neighbours(y, x) {
if ny < 0 || ny >= h_ as isize || nx < 0 || nx >= w_ as isize {
continue;
}
let ni = ny as usize * w_ + nx as usize;
if st[ni] != State::Unknown {
result.push(ni);
}
}
result
};
while let Some((Reverse(OrdF32(d)), idx)) = heap.pop() {
if state[idx] == State::Known {
continue;
}
if d > radius {
break;
}
let y = idx / w;
let x = idx % w;
let known_nbrs = band_neighbours(y, x, &state, w, h);
let mut sum_weight = 0.0f64;
let mut sum_vals: Vec<f64> = vec![0.0; c];
let mut grad_mag: f64 = 0.0;
for &ni in &known_nbrs {
let ny = ni / w;
let nx = ni % w;
let dy_dir = ny as f64 - y as f64;
let dx_dir = nx as f64 - x as f64;
let spatial_dist = (dy_dir * dy_dir + dx_dir * dx_dir).sqrt().max(1.0e-6);
let mut g_magnitude: f64 = 0.0;
let nnbrs = band_neighbours(ny, nx, &state, w, h);
for &nni in &nnbrs {
let nny = nni / w;
let nnx = nni % w;
let ddy = nny as f64 - ny as f64;
let ddx = nnx as f64 - nx as f64;
let ndist = (ddy * ddy + ddx * ddx).sqrt().max(1.0e-6);
for ch in 0..c {
let diff = inpaint_buf[ch][nni] as f64 - inpaint_buf[ch][ni] as f64;
g_magnitude += diff * diff;
}
let _ = ndist;
}
grad_mag = grad_mag.max(g_magnitude.sqrt());
let w_spatial = 1.0 / (spatial_dist * spatial_dist);
let beta = if grad_mag > 1.0e-6 {
let mut gx: f64 = 0.0;
let mut gy: f64 = 0.0;
for &nni in &nnbrs {
let nny = nni / w;
let nnx = nni % w;
let ddy = nny as f64 - ny as f64;
let ddx = nnx as f64 - nx as f64;
for ch in 0..c {
let diff = inpaint_buf[ch][nni] as f64 - inpaint_buf[ch][ni] as f64;
gx += diff * ddx;
gy += diff * ddy;
}
}
let gnorm = (gx * gx + gy * gy).sqrt().max(1.0e-12);
gx /= gnorm;
gy /= gnorm;
let px = x as f64 - nx as f64;
let py = y as f64 - ny as f64;
let pnorm = (px * px + py * py).sqrt().max(1.0e-12);
let pxn = px / pnorm;
let pyn = py / pnorm;
(gx * pyn - gy * pxn).abs()
} else {
0.5 };
let weight = w_spatial * (1.0 + beta);
for ch in 0..c {
sum_vals[ch] += inpaint_buf[ch][ni] as f64 * weight;
}
sum_weight += weight;
}
if sum_weight > 1.0e-12 {
for ch in 0..c {
inpaint_buf[ch][idx] = (sum_vals[ch] / sum_weight) as f32;
}
}
state[idx] = State::Known;
for (ny, nx) in neighbours(y, x) {
if ny < 0 || ny >= h as isize || nx < 0 || nx >= w as isize {
continue;
}
let ni = ny as usize * w + nx as usize;
if state[ni] == State::Known {
continue;
}
let new_dist = dist[idx] + 1.0;
if new_dist < dist[ni] {
dist[ni] = new_dist;
}
if state[ni] == State::Unknown {
state[ni] = State::Band;
}
heap.push((Reverse(OrdF32(dist[ni])), ni));
}
}
for idx in 0..pixels {
if state[idx] != State::Known {
for ch in 0..c {
inpaint_buf[ch][idx] = img_vals[ch * pixels + idx];
}
}
}
let mut flat = vec![0.0f32; c * pixels];
for ch in 0..c {
flat[ch * pixels..(ch + 1) * pixels].copy_from_slice(&inpaint_buf[ch]);
}
let device = image.tensor.device();
let new_data = TensorData::new(flat, [c, h, w]);
let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
Ok(Image::new(new_tensor))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::{TestBackend, test_device};
use burn::tensor::{Tensor, TensorData};
#[test]
fn test_inpaint_no_mask() {
let device = test_device();
let data = vec![0.25f32; 3 * 8 * 8];
let img = Image::new(Tensor::<TestBackend, 3>::from_data(
TensorData::new(data, [3, 8, 8]),
&device,
));
let mask = Image::new(Tensor::<TestBackend, 3>::from_data(
TensorData::new(vec![0.0f32; 8 * 8], [1, 8, 8]),
&device,
));
let result = inpaint(&img, &mask, 5.0).unwrap();
assert_eq!(result.shape(), [3, 8, 8]);
let vals: Vec<f32> = result.tensor.into_data().iter::<f32>().collect();
for v in vals {
assert!((v - 0.25).abs() < 1e-6);
}
}
#[test]
fn test_inpaint_center_region() {
let device = test_device();
let h = 10usize;
let w = 10usize;
let mut img_vals = vec![0.0f32; h * w];
for y in 0..h {
for x in 0..w {
img_vals[y * w + x] = if x < w / 2 { 0.0 } else { 1.0 };
}
}
let img = Image::new(Tensor::<TestBackend, 3>::from_data(
TensorData::new(img_vals.clone(), [1, h, w]),
&device,
));
let mut mask_vals = vec![0.0f32; h * w];
mask_vals[4 * w + 4] = 1.0;
mask_vals[4 * w + 5] = 1.0;
mask_vals[5 * w + 4] = 1.0;
mask_vals[5 * w + 5] = 1.0;
let mask = Image::new(Tensor::<TestBackend, 3>::from_data(
TensorData::new(mask_vals, [1, h, w]),
&device,
));
let result = inpaint(&img, &mask, 5.0).unwrap();
assert_eq!(result.shape(), [1, h, w]);
let out: Vec<f32> = result.tensor.into_data().iter::<f32>().collect();
for y in 4..=5 {
for x in 4..=5 {
let v = out[y * w + x];
assert!((0.0..=1.0).contains(&v), "pixel ({},{}) = {}", x, y, v);
}
}
assert!((out[4 * w + 3] - img_vals[4 * w + 3]).abs() < 1e-6);
assert!((out[5 * w + 6] - img_vals[5 * w + 6]).abs() < 1e-6);
}
#[test]
fn test_inpaint_rgb_channel_independence() {
let device = test_device();
let h = 6usize;
let w = 6usize;
let mut img_vals = vec![0.0f32; 3 * h * w];
for y in 0..h {
for x in 0..w {
img_vals[y * w + x] = 0.1;
img_vals[h * w + y * w + x] = 0.5;
img_vals[2 * h * w + y * w + x] = 0.9;
}
}
let img = Image::new(Tensor::<TestBackend, 3>::from_data(
TensorData::new(img_vals, [3, h, w]),
&device,
));
let mut mask_vals = vec![0.0f32; h * w];
mask_vals[3 * w + 3] = 1.0;
let mask = Image::new(Tensor::<TestBackend, 3>::from_data(
TensorData::new(mask_vals, [1, h, w]),
&device,
));
let result = inpaint(&img, &mask, 10.0).unwrap();
let out: Vec<f32> = result.tensor.into_data().iter::<f32>().collect();
let r = out[3 * w + 3];
let g = out[h * w + 3 * w + 3];
let b = out[2 * h * w + 3 * w + 3];
assert!((r - 0.1).abs() < 0.01, "R={}", r);
assert!((g - 0.5).abs() < 0.01, "G={}", g);
assert!((b - 0.9).abs() < 0.01, "B={}", b);
}
#[test]
fn test_inpaint_dimension_mismatch() {
let device = test_device();
let img = Image::new(Tensor::<TestBackend, 3>::from_data(
TensorData::new(vec![0.5f32; 3 * 8 * 8], [3, 8, 8]),
&device,
));
let mask = Image::new(Tensor::<TestBackend, 3>::from_data(
TensorData::new(vec![0.0f32; 6 * 6], [1, 6, 6]),
&device,
));
let result = inpaint(&img, &mask, 5.0);
assert!(result.is_err());
}
}