918 lines
32 KiB
Rust
918 lines
32 KiB
Rust
//! TRACES: FR-MRG-4
|
||
//! Filling a composite's uncovered border, tile by tile, with an inpainter.
|
||
//!
|
||
//! A merged panorama has a ragged border where no frame reached. FR-MRG-4
|
||
//! crops it by default; this fills it instead, when the photographer asks,
|
||
//! with pixels a model invents from the picture around them. Everything
|
||
//! here is the geometry of that — which tiles to run, what context to hand
|
||
//! the model, how to put its answers back — and none of it is the model:
|
||
//! that is the [`Inpainter`] trait, with MI-GAN behind it in `migan.rs`
|
||
//! and a fake in the tests.
|
||
//!
|
||
//! # Context across the edge
|
||
//!
|
||
//! An inpainting model is trained on holes *inside* pictures. A panorama's
|
||
//! border is a hole at the picture's *edge*: real content on one side,
|
||
//! nothing on the other, and a model given that invents a structure along
|
||
//! the open side — streaks of road in the sky, on the first try
|
||
//! (2026-09-19). So the known content is mirrored across the coverage
|
||
//! edge, column by column for the top and bottom bands and row by row for
|
||
//! the sides, into the hole and into a padding ring around the picture,
|
||
//! and the ring is presented as *known*. The model then interpolates
|
||
//! between real content and its mirror rather than extrapolating into
|
||
//! nothing. The ring is cut off at the end.
|
||
//!
|
||
//! # Structure from far away, texture from near
|
||
//!
|
||
//! One tiled pass at the working resolution was not enough: a 512-px tile
|
||
//! straddling the coverage edge sees a few hundred pixels of real content
|
||
//! on one side and invents the rest from that, two neighbouring tiles
|
||
//! invent differently, and the seams and the merge's own fringe leak into
|
||
//! the fill. [`fill_border`] therefore runs in two stages. A **coarse**
|
||
//! pass at a quarter of the size, where the whole border and hundreds of
|
||
//! pixels of real context sit inside a handful of tiles, decides the
|
||
//! structure — where the slope goes, where the sky stays sky. Then
|
||
//! **fine** passes regenerate the hole in bands from the real edge
|
||
//! outward: each band is the only unknown, with real content (or the band
|
||
//! before, freshly textured) on its near side and the coarse fill,
|
||
//! upsampled, on its far side — blurry, but the right structure — so the
|
||
//! model generates texture and a transition, never a large hole from
|
||
//! nothing.
|
||
//!
|
||
//! Tiles overlap by a third and are blended under a raised-cosine window,
|
||
//! so the seams between tiles do not show; the model's answer replaces
|
||
//! only the pixels that were unknown, and the picture itself is untouched.
|
||
|
||
use crate::PanoError;
|
||
|
||
/// A model that fills a square hole from its surroundings.
|
||
pub trait Inpainter {
|
||
/// The square tile it takes, in pixels.
|
||
fn tile(&self) -> usize;
|
||
|
||
/// Fill one tile. `rgb` is `tile × tile × 3`, row-major, 0..1, with the
|
||
/// unknown pixels' values meaningless; `known` is `tile × tile`. The
|
||
/// result is `tile × tile × 3`, 0..1, of which only the unknown pixels
|
||
/// are read.
|
||
fn fill(&mut self, rgb: &[f32], known: &[bool]) -> Result<Vec<f32>, PanoError>;
|
||
}
|
||
|
||
/// What a caller hears from [`fill_border`]: progress, for a page's bar,
|
||
/// and — for whoever is looking at why a fill went wrong — each stage's
|
||
/// picture as it lands. A plain `FnMut(usize, usize)` is an observer that
|
||
/// hears only the progress.
|
||
pub trait Observer {
|
||
/// `(done, total)` tiles, the total an estimate until the last band.
|
||
fn progress(&mut self, done: usize, total: usize);
|
||
/// A stage's result, `width × height × 3`: `coarse` (at the coarse
|
||
/// size), `band-N` after each fine band, `feathered` at the end.
|
||
fn stage(&mut self, _name: &str, _rgb: &[f32], _width: usize, _height: usize) {}
|
||
}
|
||
|
||
impl<F: FnMut(usize, usize)> Observer for F {
|
||
fn progress(&mut self, done: usize, total: usize) {
|
||
self(done, total)
|
||
}
|
||
}
|
||
|
||
/// How far the picture is extended with mirrored content before tiling.
|
||
/// Half a tile: enough that a hole at the edge sits well inside a tile.
|
||
pub const RING: usize = 256;
|
||
|
||
/// The fill's knobs, in pixels of the working image. The defaults are
|
||
/// what the fixture panorama looked best with on 2026-09-19; the merge
|
||
/// page exposes every one of them while the fill is experimental, so a
|
||
/// bad corner can be worked on from the picture rather than the code.
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub struct Params {
|
||
/// The coarse pass's reduction: 1 skips it.
|
||
pub coarse: usize,
|
||
/// The fine passes' band width.
|
||
pub band: usize,
|
||
/// How deep into the picture the mirrored context reaches, or **zero
|
||
/// for no mirrored context at all**: the void is then shown to the
|
||
/// model as it is — reaching the picture's edge with nothing beyond,
|
||
/// and, beyond the band being filled, still unknown. That is what the
|
||
/// shipped model was trained on (a fine-tune of MI-GAN on voids cut
|
||
/// from photographs the way a cylindrical merge cuts them, see
|
||
/// `docs/dev/panorama.md` §14); a ring would give it a fold to continue.
|
||
///
|
||
/// Non-zero is the stock model's crutch: a plain reflection of a deep
|
||
/// hole pulls in whatever is that far from the edge — a ridge, a peak —
|
||
/// and the model, told that is what lies beyond, paints it upside down.
|
||
/// Folding the reflection within this band keeps the ring looking like
|
||
/// the edge it continues and nothing further away.
|
||
pub mirror_depth: usize,
|
||
/// How far inside the real edge the fill also regenerates, the two
|
||
/// blended by distance. A hard cut between real pixels and invented
|
||
/// ones is a line whatever the fill's quality; blended over this many
|
||
/// pixels it is not. Zero is the hard cut.
|
||
pub feather: usize,
|
||
/// The step between tiles, at most the tile; two thirds of it usual.
|
||
pub stride: usize,
|
||
}
|
||
|
||
impl Default for Params {
|
||
fn default() -> Self {
|
||
Params {
|
||
coarse: 1,
|
||
band: 192,
|
||
mirror_depth: 0,
|
||
feather: 24,
|
||
stride: 384,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Fill the unknown pixels of `rgb` (`width × height × 3`, 0..1) in place:
|
||
/// the coarse pass, then the fine bands, then the seam feathered over
|
||
/// `feather` pixels inside the real edge. Returns the tiles run.
|
||
///
|
||
/// `known` is `width × height`. `observer` hears the progress and, if it
|
||
/// cares, each stage.
|
||
pub fn fill_border(
|
||
rgb: &mut [f32],
|
||
width: usize,
|
||
height: usize,
|
||
known: &[bool],
|
||
model: &mut dyn Inpainter,
|
||
params: Params,
|
||
observer: &mut dyn Observer,
|
||
) -> Result<usize, PanoError> {
|
||
let Params {
|
||
coarse: q,
|
||
band,
|
||
mirror_depth,
|
||
feather,
|
||
stride,
|
||
} = params;
|
||
let q = q.max(1);
|
||
let band = band.max(8);
|
||
// No ring: the void beyond the band stays unknown, as in the model's
|
||
// training; with a ring the far side is the coarse fill, presented as
|
||
// known, which the stock model needed to see something there.
|
||
let open = mirror_depth == 0;
|
||
if width == 0 || height == 0 || rgb.len() != width * height * 3 || known.len() != width * height
|
||
{
|
||
return Err(PanoError::Input("fill: buffer sizes disagree".into()));
|
||
}
|
||
if known.iter().all(|&k| k) {
|
||
return Ok(0);
|
||
}
|
||
// The fill regenerates a margin inside the real edge too, and the
|
||
// result is blended with the real pixels across it at the end.
|
||
let real = rgb.to_vec();
|
||
let outer = known.to_vec();
|
||
let mut inner = known.to_vec();
|
||
erode(&mut inner, width, height, feather);
|
||
let known = &inner[..];
|
||
let mut done = 0usize;
|
||
|
||
// Coarse: a fraction of the size, unknown where any pixel of the cell was.
|
||
let (cw, ch) = ((width / q).max(1), (height / q).max(1));
|
||
let mut coarse = vec![0.0f32; cw * ch * 3];
|
||
let mut cknown = vec![true; cw * ch];
|
||
for y in 0..ch {
|
||
for x in 0..cw {
|
||
let mut sum = [0.0f32; 3];
|
||
let mut n = 0.0f32;
|
||
let mut all_known = true;
|
||
for dy in 0..q {
|
||
for dx in 0..q {
|
||
let (sx, sy) = ((x * q + dx).min(width - 1), (y * q + dy).min(height - 1));
|
||
let i = sy * width + sx;
|
||
all_known &= known[i];
|
||
for c in 0..3 {
|
||
sum[c] += rgb[i * 3 + c];
|
||
}
|
||
n += 1.0;
|
||
}
|
||
}
|
||
for c in 0..3 {
|
||
coarse[(y * cw + x) * 3 + c] = sum[c] / n;
|
||
}
|
||
cknown[y * cw + x] = all_known;
|
||
}
|
||
}
|
||
let estimate = |tiles: usize| tiles * 4;
|
||
done += fill_once(
|
||
&mut coarse,
|
||
cw,
|
||
ch,
|
||
&cknown,
|
||
model,
|
||
stride,
|
||
mirror_depth,
|
||
|n, t| observer.progress(n, estimate(t)),
|
||
)?;
|
||
observer.stage("coarse", &coarse, cw, ch);
|
||
|
||
// The hole starts as the coarse structure, upsampled.
|
||
for y in 0..height {
|
||
for x in 0..width {
|
||
let i = y * width + x;
|
||
if known[i] {
|
||
continue;
|
||
}
|
||
let fx = ((x as f32 + 0.5) / q as f32 - 0.5).clamp(0.0, (cw - 1) as f32);
|
||
let fy = ((y as f32 + 0.5) / q as f32 - 0.5).clamp(0.0, (ch - 1) as f32);
|
||
let (x0, y0) = (fx as usize, fy as usize);
|
||
let (x1, y1) = ((x0 + 1).min(cw - 1), (y0 + 1).min(ch - 1));
|
||
let (tx, ty) = (fx - x0 as f32, fy - y0 as f32);
|
||
for c in 0..3 {
|
||
let at = |xx: usize, yy: usize| coarse[(yy * cw + xx) * 3 + c];
|
||
rgb[i * 3 + c] = (at(x0, y0) * (1.0 - tx) + at(x1, y0) * tx) * (1.0 - ty)
|
||
+ (at(x0, y1) * (1.0 - tx) + at(x1, y1) * tx) * ty;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fine, in bands from the edge outward.
|
||
let dist = distance_to_known(known, width, height);
|
||
let mut band_known = vec![true; width * height];
|
||
let mut b = 0usize;
|
||
loop {
|
||
let lo = (b * band).saturating_sub(band / 2) as f32;
|
||
let hi = ((b + 1) * band) as f32;
|
||
let mut any = false;
|
||
for i in 0..width * height {
|
||
let in_band = !known[i] && dist[i] > lo && dist[i] <= hi;
|
||
band_known[i] = if open {
|
||
known[i] || dist[i] <= lo
|
||
} else {
|
||
!in_band
|
||
};
|
||
any |= in_band;
|
||
}
|
||
if !any {
|
||
break;
|
||
}
|
||
let before = done;
|
||
done += fill_once(
|
||
rgb,
|
||
width,
|
||
height,
|
||
&band_known,
|
||
model,
|
||
stride,
|
||
mirror_depth,
|
||
|n, t| observer.progress(before + n, before + estimate(t)),
|
||
)?;
|
||
observer.stage(&format!("band-{b}"), rgb, width, height);
|
||
b += 1;
|
||
}
|
||
|
||
// The seam: across the margin, real on the inside, invented on the
|
||
// outside, a smooth ramp between by distance from the true hole.
|
||
if feather > 0 {
|
||
let to_hole =
|
||
distance_to_known(&outer.iter().map(|k| !k).collect::<Vec<_>>(), width, height);
|
||
for i in 0..width * height {
|
||
if !outer[i] || known[i] {
|
||
continue;
|
||
}
|
||
// In the margin: outer says known, inner says not.
|
||
let t = (to_hole[i] / feather as f32).clamp(0.0, 1.0);
|
||
let t = t * t * (3.0 - 2.0 * t);
|
||
for c in 0..3 {
|
||
rgb[i * 3 + c] = rgb[i * 3 + c] * (1.0 - t) + real[i * 3 + c] * t;
|
||
}
|
||
}
|
||
}
|
||
observer.stage("feathered", rgb, width, height);
|
||
observer.progress(done, done);
|
||
Ok(done)
|
||
}
|
||
|
||
/// One tiled pass: every unknown pixel regenerated from the tiles that
|
||
/// touch it, the rest kept. Returns the tiles run.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn fill_once(
|
||
rgb: &mut [f32],
|
||
width: usize,
|
||
height: usize,
|
||
known: &[bool],
|
||
model: &mut dyn Inpainter,
|
||
stride: usize,
|
||
mirror_depth: usize,
|
||
mut progress: impl FnMut(usize, usize),
|
||
) -> Result<usize, PanoError> {
|
||
let t = model.tile();
|
||
if t == 0 || known.iter().all(|&k| k) {
|
||
return Ok(0);
|
||
}
|
||
|
||
// The padded canvas with mirrored context, and the hole within it.
|
||
let ctx = MirroredContext::build(rgb, width, height, known, mirror_depth, t);
|
||
let (pw, ph) = (ctx.width, ctx.height);
|
||
|
||
// Tiles that touch the hole, on a grid that reaches both far edges.
|
||
let starts = |n: usize| -> Vec<usize> {
|
||
if n <= t {
|
||
return vec![0];
|
||
}
|
||
let mut v: Vec<usize> = (0..=n - t).step_by(stride.clamp(1, t)).collect();
|
||
if *v.last().unwrap_or(&0) != n - t {
|
||
v.push(n - t);
|
||
}
|
||
v
|
||
};
|
||
let ys = starts(ph);
|
||
let xs = starts(pw);
|
||
let mut tiles = Vec::new();
|
||
for &y in &ys {
|
||
for &x in &xs {
|
||
if y + t > ph || x + t > pw {
|
||
continue;
|
||
}
|
||
let touches =
|
||
(y..y + t).any(|yy| ctx.hole[yy * pw + x..yy * pw + x + t].iter().any(|&h| h));
|
||
if touches {
|
||
tiles.push((x, y));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Raised-cosine window, so overlapping tiles blend.
|
||
let hann: Vec<f32> = (0..t)
|
||
.map(|i| {
|
||
let s = ((i as f32 + 1.0) / (t as f32 + 1.0) * std::f32::consts::PI).sin();
|
||
s * s + 1e-3
|
||
})
|
||
.collect();
|
||
|
||
let mut acc = vec![0.0f32; pw * ph * 3];
|
||
let mut wsum = vec![0.0f32; pw * ph];
|
||
let mut tile_rgb = vec![0.0f32; t * t * 3];
|
||
let mut tile_known = vec![false; t * t];
|
||
let total = tiles.len();
|
||
for (n, &(x, y)) in tiles.iter().enumerate() {
|
||
progress(n, total);
|
||
for r in 0..t {
|
||
let src = ((y + r) * pw + x) * 3;
|
||
tile_rgb[r * t * 3..(r + 1) * t * 3].copy_from_slice(&ctx.rgb[src..src + t * 3]);
|
||
let ks = (y + r) * pw + x;
|
||
for c in 0..t {
|
||
tile_known[r * t + c] = !ctx.hole[ks + c];
|
||
}
|
||
}
|
||
let out = model.fill(&tile_rgb, &tile_known)?;
|
||
if out.len() != t * t * 3 {
|
||
return Err(PanoError::Model(format!(
|
||
"the inpainter returned {} values for a {t}×{t} tile",
|
||
out.len()
|
||
)));
|
||
}
|
||
for r in 0..t {
|
||
for c in 0..t {
|
||
let w = hann[r] * hann[c];
|
||
let p = (y + r) * pw + (x + c);
|
||
for ch in 0..3 {
|
||
acc[p * 3 + ch] += out[(r * t + c) * 3 + ch] * w;
|
||
}
|
||
wsum[p] += w;
|
||
}
|
||
}
|
||
}
|
||
progress(total, total);
|
||
|
||
// Back into the picture: only the unknown pixels change.
|
||
for yy in 0..height {
|
||
for xx in 0..width {
|
||
let i = yy * width + xx;
|
||
if known[i] {
|
||
continue;
|
||
}
|
||
let p = (yy + ctx.ring) * pw + (xx + ctx.ring);
|
||
if wsum[p] > 0.0 {
|
||
for ch in 0..3 {
|
||
rgb[i * 3 + ch] = (acc[p * 3 + ch] / wsum[p]).clamp(0.0, 1.0);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Ok(total)
|
||
}
|
||
|
||
/// Shrink `known` by `iterations` pixels on every side, in place.
|
||
///
|
||
/// The merge's coverage edge carries a fringe — the last partly-covered
|
||
/// pixels of a frame, and whatever the renderer did at the boundary — and
|
||
/// a fill that stops exactly at the coverage bit leaves it as a dark line
|
||
/// along the seam. Eight pixels at a quarter of the composite's resolution
|
||
/// was what it took on the fixture.
|
||
pub fn erode(known: &mut [bool], width: usize, height: usize, iterations: usize) {
|
||
let mut next = known.to_vec();
|
||
for _ in 0..iterations {
|
||
for y in 0..height {
|
||
for x in 0..width {
|
||
let i = y * width + x;
|
||
if !known[i] {
|
||
continue;
|
||
}
|
||
let edge = x == 0
|
||
|| y == 0
|
||
|| x + 1 == width
|
||
|| y + 1 == height
|
||
|| !known[i - 1]
|
||
|| !known[i + 1]
|
||
|| !known[i - width]
|
||
|| !known[i + width];
|
||
next[i] = !edge;
|
||
}
|
||
}
|
||
known.copy_from_slice(&next);
|
||
}
|
||
}
|
||
|
||
/// Distance from each pixel to the nearest known one, by two chamfer
|
||
/// sweeps — within a few percent of Euclidean, and enough to cut bands.
|
||
fn distance_to_known(known: &[bool], width: usize, height: usize) -> Vec<f32> {
|
||
let inf = (width + height) as f32;
|
||
let mut d: Vec<f32> = known.iter().map(|&k| if k { 0.0 } else { inf }).collect();
|
||
let (a, b) = (1.0f32, std::f32::consts::SQRT_2);
|
||
for y in 0..height {
|
||
for x in 0..width {
|
||
let i = y * width + x;
|
||
let mut v = d[i];
|
||
if x > 0 {
|
||
v = v.min(d[i - 1] + a);
|
||
}
|
||
if y > 0 {
|
||
v = v.min(d[i - width] + a);
|
||
if x > 0 {
|
||
v = v.min(d[i - width - 1] + b);
|
||
}
|
||
if x + 1 < width {
|
||
v = v.min(d[i - width + 1] + b);
|
||
}
|
||
}
|
||
d[i] = v;
|
||
}
|
||
}
|
||
for y in (0..height).rev() {
|
||
for x in (0..width).rev() {
|
||
let i = y * width + x;
|
||
let mut v = d[i];
|
||
if x + 1 < width {
|
||
v = v.min(d[i + 1] + a);
|
||
}
|
||
if y + 1 < height {
|
||
v = v.min(d[i + width] + a);
|
||
if x + 1 < width {
|
||
v = v.min(d[i + width + 1] + b);
|
||
}
|
||
if x > 0 {
|
||
v = v.min(d[i + width - 1] + b);
|
||
}
|
||
}
|
||
d[i] = v;
|
||
}
|
||
}
|
||
d
|
||
}
|
||
|
||
/// Distance beyond the edge to distance inside it, folded within `depth`
|
||
/// ([`Params::mirror_depth`]): a triangle wave, so the band is read
|
||
/// forward and back rather than clamped to one row.
|
||
fn fold(d: usize, depth: usize) -> usize {
|
||
let period = 2 * depth;
|
||
let r = d % period;
|
||
if r <= depth {
|
||
r
|
||
} else {
|
||
period - r
|
||
}
|
||
}
|
||
|
||
/// The picture on a canvas `RING` wider on every side, with the hole and
|
||
/// the ring filled by mirroring the known content across the coverage
|
||
/// edge — the nearest `depth` of it, folded — and the hole, the
|
||
/// original unknown and nothing else, marked.
|
||
struct MirroredContext {
|
||
width: usize,
|
||
height: usize,
|
||
/// The padding on every side: `RING` with mirrored context, 0 without.
|
||
ring: usize,
|
||
rgb: Vec<f32>,
|
||
hole: Vec<bool>,
|
||
}
|
||
|
||
impl MirroredContext {
|
||
fn build(
|
||
rgb: &[f32],
|
||
width: usize,
|
||
height: usize,
|
||
known: &[bool],
|
||
depth: usize,
|
||
tile: usize,
|
||
) -> Self {
|
||
if depth == 0 {
|
||
// Open: the picture as it is, the hole as it is. What the hole
|
||
// holds does not matter — the model masks it out. A picture
|
||
// smaller than a tile (the merge page's preview) sits at the
|
||
// origin of a tile-sized canvas whose rest is hole: still the
|
||
// void as it is, and the only way a tile fits at all.
|
||
let (pw, ph) = (width.max(tile), height.max(tile));
|
||
let mut canvas = vec![0.0f32; pw * ph * 3];
|
||
let mut hole = vec![true; pw * ph];
|
||
for y in 0..height {
|
||
canvas[y * pw * 3..(y * pw + width) * 3]
|
||
.copy_from_slice(&rgb[y * width * 3..(y + 1) * width * 3]);
|
||
for x in 0..width {
|
||
hole[y * pw + x] = !known[y * width + x];
|
||
}
|
||
}
|
||
return MirroredContext {
|
||
width: pw,
|
||
height: ph,
|
||
ring: 0,
|
||
rgb: canvas,
|
||
hole,
|
||
};
|
||
}
|
||
let fold = |d: usize| fold(d, depth);
|
||
let (pw, ph) = (width + 2 * RING, height + 2 * RING);
|
||
let mut canvas = vec![0.0f32; pw * ph * 3];
|
||
let mut kn = vec![false; pw * ph];
|
||
let mut hole = vec![false; pw * ph];
|
||
for y in 0..height {
|
||
for x in 0..width {
|
||
let i = y * width + x;
|
||
let p = (y + RING) * pw + (x + RING);
|
||
canvas[p * 3..p * 3 + 3].copy_from_slice(&rgb[i * 3..i * 3 + 3]);
|
||
kn[p] = known[i];
|
||
hole[p] = !known[i];
|
||
}
|
||
}
|
||
|
||
// Per column: mirror across the first and last known row.
|
||
for x in 0..pw {
|
||
let first = (0..ph).find(|&y| kn[y * pw + x]);
|
||
let Some(first) = first else { continue };
|
||
let last = (0..ph).rev().find(|&y| kn[y * pw + x]).unwrap_or(first);
|
||
for y in 0..first {
|
||
let m = (first + fold(first - y)).min(last);
|
||
let (d, s) = ((y * pw + x) * 3, (m * pw + x) * 3);
|
||
canvas.copy_within(s..s + 3, d);
|
||
}
|
||
for y in last + 1..ph {
|
||
let m = last.saturating_sub(fold(y - last)).max(first);
|
||
let (d, s) = ((y * pw + x) * 3, (m * pw + x) * 3);
|
||
canvas.copy_within(s..s + 3, d);
|
||
}
|
||
}
|
||
// Per row, for the sides, over what is there now.
|
||
for y in 0..ph {
|
||
let first = (0..pw).find(|&x| kn[y * pw + x]);
|
||
let Some(first) = first else { continue };
|
||
let last = (0..pw).rev().find(|&x| kn[y * pw + x]).unwrap_or(first);
|
||
for x in 0..first {
|
||
let m = (first + fold(first - x)).min(last);
|
||
let (d, s) = ((y * pw + x) * 3, (y * pw + m) * 3);
|
||
canvas.copy_within(s..s + 3, d);
|
||
}
|
||
for x in last + 1..pw {
|
||
let m = last.saturating_sub(fold(x - last)).max(first);
|
||
let (d, s) = ((y * pw + x) * 3, (y * pw + m) * 3);
|
||
canvas.copy_within(s..s + 3, d);
|
||
}
|
||
}
|
||
MirroredContext {
|
||
width: pw,
|
||
height: ph,
|
||
ring: RING,
|
||
rgb: canvas,
|
||
hole,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// The tests' small pictures: a 48-px stride, a given feather.
|
||
fn test_params(feather: usize) -> Params {
|
||
Params {
|
||
stride: 48,
|
||
feather,
|
||
..Params::default()
|
||
}
|
||
}
|
||
|
||
/// Paints every unknown pixel a fixed grey and copies the known ones,
|
||
/// and remembers what it was shown.
|
||
struct Flat {
|
||
tile: usize,
|
||
seen: Vec<(Vec<f32>, Vec<bool>)>,
|
||
}
|
||
|
||
impl Inpainter for Flat {
|
||
fn tile(&self) -> usize {
|
||
self.tile
|
||
}
|
||
fn fill(&mut self, rgb: &[f32], known: &[bool]) -> Result<Vec<f32>, PanoError> {
|
||
self.seen.push((rgb.to_vec(), known.to_vec()));
|
||
Ok(rgb
|
||
.chunks_exact(3)
|
||
.zip(known)
|
||
.flat_map(|(p, &k)| if k { [p[0], p[1], p[2]] } else { [0.5; 3] })
|
||
.collect())
|
||
}
|
||
}
|
||
|
||
fn picture(w: usize, h: usize, border: usize) -> (Vec<f32>, Vec<bool>) {
|
||
let mut rgb = vec![0.0; w * h * 3];
|
||
let mut known = vec![false; w * h];
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
let i = y * w + x;
|
||
if y >= border && y < h - border {
|
||
known[i] = true;
|
||
rgb[i * 3] = x as f32 / w as f32;
|
||
rgb[i * 3 + 1] = y as f32 / h as f32;
|
||
rgb[i * 3 + 2] = 0.25;
|
||
}
|
||
}
|
||
}
|
||
(rgb, known)
|
||
}
|
||
|
||
#[test]
|
||
fn unknown_pixels_take_the_model_and_known_ones_do_not_move() {
|
||
let (mut rgb, known) = picture(300, 200, 20);
|
||
let before = rgb.clone();
|
||
let mut model = Flat {
|
||
tile: 64,
|
||
seen: Vec::new(),
|
||
};
|
||
let tiles = fill_border(
|
||
&mut rgb,
|
||
300,
|
||
200,
|
||
&known,
|
||
&mut model,
|
||
test_params(0),
|
||
&mut |_, _| {},
|
||
)
|
||
.unwrap();
|
||
assert!(tiles > 0);
|
||
for i in 0..300 * 200 {
|
||
if known[i] {
|
||
assert_eq!(&rgb[i * 3..i * 3 + 3], &before[i * 3..i * 3 + 3]);
|
||
} else {
|
||
for c in 0..3 {
|
||
assert!((rgb[i * 3 + c] - 0.5).abs() < 1e-4, "pixel {i}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_model_is_shown_mirrored_context_not_black() {
|
||
let (mut rgb, known) = picture(300, 200, 20);
|
||
let mut model = Flat {
|
||
tile: 64,
|
||
seen: Vec::new(),
|
||
};
|
||
fill_border(
|
||
&mut rgb,
|
||
300,
|
||
200,
|
||
&known,
|
||
&mut model,
|
||
Params {
|
||
mirror_depth: 48,
|
||
..test_params(0)
|
||
},
|
||
&mut |_, _| {},
|
||
)
|
||
.unwrap();
|
||
for (tile_rgb, tile_known) in &model.seen {
|
||
let known_non_black = tile_rgb
|
||
.chunks_exact(3)
|
||
.zip(tile_known)
|
||
.filter(|(_, &k)| k)
|
||
.any(|(p, _)| p.iter().any(|v| *v > 0.0));
|
||
assert!(known_non_black);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn an_open_void_reaches_the_tile_edge_and_stays_unknown_beyond_the_band() {
|
||
// A 150-tall hole above and below; bands of 96. With no ring the
|
||
// first band's tiles sit at the picture's edge, so a tile's top
|
||
// row is unknown, and the rows deeper than the band are unknown
|
||
// too — not "known" coarse fill — exactly as the model was trained.
|
||
let (mut rgb, known) = picture(200, 500, 150);
|
||
let mut model = Flat {
|
||
tile: 64,
|
||
seen: Vec::new(),
|
||
};
|
||
fill_border(
|
||
&mut rgb,
|
||
200,
|
||
500,
|
||
&known,
|
||
&mut model,
|
||
Params {
|
||
band: 96,
|
||
..test_params(0)
|
||
},
|
||
&mut |_, _| {},
|
||
)
|
||
.unwrap();
|
||
// The first pass's tile at the picture's top edge is unknown
|
||
// through and through: the hole is 150 deep, the tile 64, and
|
||
// nothing beyond the band was presented as known. With a ring, or
|
||
// with the far side shown as coarse fill, no tile is ever all hole.
|
||
assert!(model.seen.iter().any(|(_, k)| k.iter().all(|&v| !v)));
|
||
for i in 0..200 * 500 {
|
||
if !known[i] {
|
||
assert!((rgb[i * 3] - 0.5).abs() < 1e-4, "pixel {i}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_picture_smaller_than_the_tile_is_still_filled_when_the_void_is_open() {
|
||
// The merge page's preview is 1600 wide and a few hundred tall —
|
||
// shorter than a 512 tile. With no ring the canvas is padded to a
|
||
// tile, the padding hole, and the border is still filled.
|
||
let (mut rgb, known) = picture(300, 40, 8);
|
||
let mut model = Flat {
|
||
tile: 64,
|
||
seen: Vec::new(),
|
||
};
|
||
let tiles = fill_border(
|
||
&mut rgb,
|
||
300,
|
||
40,
|
||
&known,
|
||
&mut model,
|
||
test_params(0),
|
||
&mut |_, _| {},
|
||
)
|
||
.unwrap();
|
||
assert!(tiles > 0, "no tile fitted a picture shorter than the tile");
|
||
for i in 0..300 * 40 {
|
||
if !known[i] {
|
||
assert!((rgb[i * 3] - 0.5).abs() < 1e-4, "pixel {i}");
|
||
}
|
||
}
|
||
// And the model saw the padding as hole, never as black content.
|
||
for (_, k) in &model.seen {
|
||
assert_eq!(k.len(), 64 * 64);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_fine_passes_run_in_bands_after_the_coarse_one() {
|
||
// A 150-tall hole above and below a picture: the coarse pass sees
|
||
// it at a quarter; the fine passes need two bands of BAND pixels.
|
||
let (mut rgb, known) = picture(200, 500, 150);
|
||
let mut model = Flat {
|
||
tile: 64,
|
||
seen: Vec::new(),
|
||
};
|
||
fill_border(
|
||
&mut rgb,
|
||
200,
|
||
500,
|
||
&known,
|
||
&mut model,
|
||
Params {
|
||
coarse: 4,
|
||
band: 96,
|
||
mirror_depth: 48,
|
||
..test_params(0)
|
||
},
|
||
&mut |_, _| {},
|
||
)
|
||
.unwrap();
|
||
assert!(model.seen.len() > 4);
|
||
// Every unknown pixel was reached.
|
||
for i in 0..200 * 500 {
|
||
if !known[i] {
|
||
assert!((rgb[i * 3] - 0.5).abs() < 1e-4, "pixel {i}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_seam_ramps_from_real_to_invented_across_the_feather() {
|
||
let (mut rgb, known) = picture(300, 200, 20);
|
||
let before = rgb.clone();
|
||
let mut model = Flat {
|
||
tile: 64,
|
||
seen: Vec::new(),
|
||
};
|
||
fill_border(
|
||
&mut rgb,
|
||
300,
|
||
200,
|
||
&known,
|
||
&mut model,
|
||
test_params(8),
|
||
&mut |_, _| {},
|
||
)
|
||
.unwrap();
|
||
// Row 20 is the real edge; the margin runs to row 27. At the edge
|
||
// the value is the model's grey, eight rows in it is the picture's.
|
||
let at = |y: usize| rgb[(y * 300 + 150) * 3 + 2];
|
||
assert!((at(20) - 0.5).abs() < 0.05, "{}", at(20));
|
||
assert!((at(29) - before[(29 * 300 + 150) * 3 + 2]).abs() < 1e-4);
|
||
let (lo, hi) = (at(20).min(at(29)), at(20).max(at(29)));
|
||
assert!(
|
||
at(23) > lo + 0.02 && at(23) < hi - 0.02,
|
||
"{} between {lo} and {hi}",
|
||
at(23)
|
||
);
|
||
// The hole itself is the model's.
|
||
assert!((at(5) - 0.5).abs() < 1e-4);
|
||
}
|
||
|
||
#[test]
|
||
fn erosion_shrinks_the_known_region_from_every_edge() {
|
||
let (_, mut known) = picture(20, 20, 4);
|
||
erode(&mut known, 20, 20, 2);
|
||
assert!(known[8 * 20 + 10]);
|
||
assert!(!known[5 * 20 + 10]);
|
||
assert!(!known[8 * 20 + 1]);
|
||
}
|
||
|
||
#[test]
|
||
fn distance_counts_pixels_from_the_known_region() {
|
||
let (_, known) = picture(20, 20, 4);
|
||
let d = distance_to_known(&known, 20, 20);
|
||
assert_eq!(d[4 * 20 + 10], 0.0);
|
||
assert!((d[3 * 20 + 10] - 1.0).abs() < 1e-6);
|
||
assert!((d[10] - 4.0).abs() < 1e-6);
|
||
}
|
||
|
||
#[test]
|
||
fn a_fully_covered_picture_runs_nothing() {
|
||
let (mut rgb, known) = picture(100, 100, 0);
|
||
let mut model = Flat {
|
||
tile: 64,
|
||
seen: Vec::new(),
|
||
};
|
||
assert_eq!(
|
||
fill_border(
|
||
&mut rgb,
|
||
100,
|
||
100,
|
||
&known,
|
||
&mut model,
|
||
test_params(0),
|
||
&mut |_, _| {}
|
||
)
|
||
.unwrap(),
|
||
0
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn the_context_mirrors_the_top_rows_upward() {
|
||
let (rgb, known) = picture(40, 30, 5);
|
||
let ctx = MirroredContext::build(&rgb, 40, 30, &known, 48, 64);
|
||
let x = RING + 10;
|
||
let first = RING + 5;
|
||
for k in 1..=4 {
|
||
let above = ((first - k) * ctx.width + x) * 3;
|
||
let mirror = ((first + k) * ctx.width + x) * 3;
|
||
assert_eq!(&ctx.rgb[above..above + 3], &ctx.rgb[mirror..mirror + 3]);
|
||
}
|
||
assert!(ctx.hole[(RING + 2) * ctx.width + x]);
|
||
assert!(!ctx.hole[(RING - 2) * ctx.width + x]);
|
||
}
|
||
|
||
#[test]
|
||
fn the_mirror_reaches_no_deeper_than_its_band() {
|
||
// A ridge 200 rows in must not appear in the ring: beyond the band
|
||
// the reflection folds back towards the edge rather than on into
|
||
// the picture.
|
||
let (mut rgb, known) = picture(40, 400, 5);
|
||
let ridge = 5 + 200;
|
||
for x in 0..40 {
|
||
rgb[(ridge * 40 + x) * 3..(ridge * 40 + x) * 3 + 3].copy_from_slice(&[0.9, 0.1, 0.1]);
|
||
}
|
||
let ctx = MirroredContext::build(&rgb, 40, 400, &known, 48, 64);
|
||
let x = RING + 10;
|
||
for y in 0..RING + 5 {
|
||
let p = (y * ctx.width + x) * 3;
|
||
assert!(
|
||
ctx.rgb[p] < 0.5,
|
||
"row {y} of the ring shows the ridge ({:?})",
|
||
&ctx.rgb[p..p + 3]
|
||
);
|
||
}
|
||
assert_eq!(fold(0, 48), 0);
|
||
assert_eq!(fold(48, 48), 48);
|
||
assert_eq!(fold(58, 48), 38);
|
||
assert_eq!(fold(96, 48), 0);
|
||
assert_eq!(fold(99, 48), 3);
|
||
}
|
||
}
|