mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
* security(algo): bound working sets sized by caller-controlled extents Zero-width inpaint grids, content-aware fill mosaics of turned thin images, Liquify fields over huge canvases, tiny-scale reduction weights, warp tile bookkeeping and per-tile source footprints, and windowed filters whose halo follows an unclamped radius or a stacked Filter Gallery reach all asked for memory sized by file or parameter input. Each is now bounded; halo arithmetic saturates instead of overflowing. * security(algo): budget variants by dimensions, size-check footprints first, widen the tile-halo maths CodeRabbit follow-up: - fill_cow settles the mosaic budget on variant dimensions only, so a variant's image and mask buffers are not built when the mosaic would exceed MAX_MOSAIC_PIXELS (complete falls back to the image itself). - warp validates the footprint's pixel area against MAX_FOOTPRINT_PIXELS before src.has_tiles_in, so an oversized footprint returns without the tile-grid scan. - auto_tile widens the input-derived halo arithmetic to i64 and clamps before the cast, so an i32::MAX radius cannot overflow in debug builds and the 2048 tile cap stands. * fix(algo): drop the duplicate mosaic_size call left behind in fill_cow The variant-budget rework kept both the new dimensions-based budget and the old variants-based one; the old call no longer compiled after mosaic_size started taking dimensions. Keep the comment that explains the fallback.
2505 lines
107 KiB
Rust
2505 lines
107 KiB
Rust
//! Hole filling for Content-Aware Fill and Spot Healing: patch-based completion, proximity match
|
||
//! and texture synthesis.
|
||
//!
|
||
//! * [`complete`], content-aware fill: the multi-scale "space-time completion" EM scheme of
|
||
//! Y. Wexler, E. Shechtman, M. Irani, *Space-Time Completion of Video*, IEEE TPAMI 2007, with the
|
||
//! nearest-neighbour field computed by C. Barnes, E. Shechtman, A. Finkelstein, D. B. Goldman,
|
||
//! *PatchMatch: A Randomized Correspondence Algorithm for Structural Image Editing*, SIGGRAPH 2009
|
||
//! (random init, propagation, exponentially shrinking random search). Each EM iteration finds, for
|
||
//! every patch overlapping the hole, its most similar source patch, then re-estimates hole pixels
|
||
//! by a similarity-weighted vote of the overlapping matches. Coarse to fine over an image pyramid
|
||
//! (down to where the hole is about four patches across): the coarsest level starts by filling
|
||
//! the hole from its edge inwards, ring by ring, each pixel from the patch that best matches what
|
||
//! is known around it (rather than from a smooth membrane, which draws the fill towards flat
|
||
//! areas); every finer level starts from the coarser level's matches scaled up and voted with the
|
||
//! finer level's own pixels, so the texture stays sharp. Where neighbouring patches agree on their
|
||
//! source (the usual case), the vote copies the source pixels exactly; only along the seams
|
||
//! between copied pieces does it blend.
|
||
//!
|
||
//! A thin hole (a selected wire, string or hair: long, but only a few patches thick) is
|
||
//! measured by its thickness, not its bounds (#1366): the pyramid stops where it is still about
|
||
//! two patches thick. That level is big, so the onion peel starts it only where its exhaustive
|
||
//! search stays affordable; otherwise a membrane across the hole does, which over so short a
|
||
//! distance is the surroundings themselves. Shrunk to where its bounds are four patches across,
|
||
//! a long thin hole swallows its surroundings, the matches found there copy whatever happens to
|
||
//! look like the blur (a dark blotch, or a piece of another string), and the finer levels,
|
||
//! which only refine those matches, keep them: the removed object's outline stays visible.
|
||
//!
|
||
//! This is what Photoshop's Content-Aware Fill does, measured as a black box on Photoshop 25.4
|
||
//! (see `content_aware`): with colour adaptation off its fill is pieces of the image copied pixel
|
||
//! for pixel, typically 15–40 px across, joined by seams a few pixels wide; with colour
|
||
//! adaptation the copies come with a gain and bias. That is the patch transform of S. Darabi,
|
||
//! E. Shechtman, C. Barnes, D. B. Goldman, P. Sen, *Image Melding: Combining Inconsistent Images
|
||
//! using Patch-based Synthesis*, SIGGRAPH 2012: each source patch may be scaled and offset per
|
||
//! channel to match its target, within limits ([`Adapt`]), and is voted in adjusted.
|
||
//! * [`best_offset`], proximity match: the single displacement whose surrounding ring best matches the
|
||
//! ring around the hole (sum of squared differences), i.e. an automatic Healing Brush source.
|
||
//! * [`synthesize`], create texture: patch-based texture synthesis in the style of A. Efros,
|
||
//! W. Freeman, *Image Quilting for Texture Synthesis and Transfer*, SIGGRAPH 2001 (blocks picked from
|
||
//! random candidates by overlap error, joined along minimum-error boundary cuts, so the texture is
|
||
//! copied, never averaged).
|
||
//!
|
||
//! All functions take interleaved `w × h × ch` normalised floats (any colour model/depth) and a
|
||
//! `w × h` hole mask, and return a full buffer equal to the input outside the hole. They are
|
||
//! deterministic (fixed-seed hash RNG, results independent of thread count).
|
||
|
||
use std::borrow::Cow;
|
||
|
||
use crate::par;
|
||
use crate::poisson::membrane_fill;
|
||
|
||
mod proximity_integral;
|
||
use proximity_integral::HoleIntegral;
|
||
|
||
/// Below this many cells a pass runs on the calling thread (thread hand-off costs more than it
|
||
/// saves). Every pass gives the same result either way.
|
||
const PAR_MIN: usize = 64 * 64;
|
||
|
||
/// [`PAR_MIN`] for the passes that match or vote a patch per cell.
|
||
const PAR_MIN_PATCHES: usize = 16 * 16;
|
||
|
||
/// Largest level (in cells) whose source patches' moments [`Solver::prepare`] keeps.
|
||
const SOURCE_MOMENTS_MAX: usize = 1 << 20;
|
||
|
||
/// SplitMix64: tiny deterministic RNG.
|
||
#[derive(Clone)]
|
||
pub(crate) struct Rng(u64);
|
||
impl Rng {
|
||
/// A generator whose sequence is fixed by `seed`.
|
||
pub(crate) fn new(seed: u64) -> Self {
|
||
Rng(seed ^ 0x9E37_79B9_7F4A_7C15)
|
||
}
|
||
/// The next value of the SplitMix64 sequence.
|
||
pub(crate) fn next(&mut self) -> u64 {
|
||
crate::photo_util::splitmix64(&mut self.0)
|
||
}
|
||
/// Skip `n` numbers (the same state as after `n` calls of [`Rng::next`]).
|
||
pub(crate) fn skip(&mut self, n: u64) {
|
||
self.0 = self.0.wrapping_add(n.wrapping_mul(0x9E37_79B9_7F4A_7C15));
|
||
}
|
||
/// Uniform integer in `lo..=hi`.
|
||
pub(crate) fn range(&mut self, lo: i32, hi: i32) -> i32 {
|
||
if hi <= lo {
|
||
return lo;
|
||
}
|
||
lo + (self.next() % (hi - lo + 1) as u64) as i32
|
||
}
|
||
}
|
||
|
||
/// How far a copied patch may be adjusted to its target, per channel: `gain · source + bias`, the
|
||
/// gain within `gain` and the bias within `±bias` (Image Melding's photometric transform). The
|
||
/// gain is the ratio of the patches' standard deviations, the bias what then matches their means.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
pub struct Adapt {
|
||
pub gain: (f32, f32),
|
||
pub bias: f32,
|
||
}
|
||
|
||
impl Adapt {
|
||
/// Copies are taken as they are.
|
||
pub const NONE: Adapt = Adapt { gain: (1.0, 1.0), bias: 0.0 };
|
||
|
||
pub fn is_none(&self) -> bool {
|
||
self.gain == (1.0, 1.0) && self.bias == 0.0
|
||
}
|
||
}
|
||
|
||
impl Default for Adapt {
|
||
fn default() -> Self {
|
||
Self::NONE
|
||
}
|
||
}
|
||
|
||
/// Parameters for [`complete`].
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct CompleteParams {
|
||
/// Patch radius (patches are `2r+1` square).
|
||
pub patch_radius: usize,
|
||
/// EM iterations at the coarsest level (finer levels use fewer, down to `min_em_iters`).
|
||
pub em_iters: usize,
|
||
pub min_em_iters: usize,
|
||
/// PatchMatch passes per EM iteration.
|
||
pub pm_iters: usize,
|
||
/// Levels where the hole is wider than this (in that level's pixels) only *refine* the
|
||
/// scaled-up match field (one local PatchMatch pass and one vote), which keeps big holes
|
||
/// tractable: the structure is decided at coarse scale, fine levels just add detail.
|
||
pub refine_extent: usize,
|
||
/// Colour adaptation of the copies.
|
||
pub adapt: Adapt,
|
||
pub seed: u64,
|
||
}
|
||
|
||
impl Default for CompleteParams {
|
||
fn default() -> Self {
|
||
Self { patch_radius: 3, em_iters: 10, min_em_iters: 3, pm_iters: 2, refine_extent: 192, adapt: Adapt::NONE, seed: 1 }
|
||
}
|
||
}
|
||
|
||
/// What a pixel is to [`complete_masked`].
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Px {
|
||
/// Not part of the image (a gap, or outside a transformed copy): ignored.
|
||
Void,
|
||
/// Known, part of the context the fill must agree with, but not copied from.
|
||
Known,
|
||
/// Known and may be copied from.
|
||
Source,
|
||
/// To be filled.
|
||
Hole,
|
||
}
|
||
|
||
/// One pyramid level. Level 0 borrows the caller's buffers until it is solved.
|
||
struct Level<'a> {
|
||
w: usize,
|
||
h: usize,
|
||
img: Cow<'a, [f32]>,
|
||
px: Cow<'a, [Px]>,
|
||
}
|
||
|
||
impl Level<'_> {
|
||
const EMPTY: Level<'static> = Level { w: 0, h: 0, img: Cow::Borrowed(&[]), px: Cow::Borrowed(&[]) };
|
||
}
|
||
|
||
/// The half-open box `[x0, x1) × [y0, y1)` of a level's grid.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
struct Bounds {
|
||
x0: usize,
|
||
y0: usize,
|
||
x1: usize,
|
||
y1: usize,
|
||
}
|
||
|
||
impl Bounds {
|
||
/// Grown by `r` on every side, within a `w × h` grid.
|
||
fn grow(self, r: usize, w: usize, h: usize) -> Bounds {
|
||
Bounds { x0: self.x0.saturating_sub(r), y0: self.y0.saturating_sub(r), x1: self.x1.saturating_add(r).min(w), y1: self.y1.saturating_add(r).min(h) }
|
||
}
|
||
|
||
/// The same box on the next coarser level (whose cells are 2 × 2 of these).
|
||
fn half(self) -> Bounds {
|
||
Bounds { x0: self.x0 / 2, y0: self.y0 / 2, x1: self.x1.div_ceil(2), y1: self.y1.div_ceil(2) }
|
||
}
|
||
|
||
fn width(&self) -> usize {
|
||
self.x1.saturating_sub(self.x0)
|
||
}
|
||
|
||
fn height(&self) -> usize {
|
||
self.y1.saturating_sub(self.y0)
|
||
}
|
||
|
||
fn area(&self) -> usize {
|
||
self.width() * self.height()
|
||
}
|
||
}
|
||
|
||
/// Half-size level: a cell is a hole if any of its pixels is; else it averages its non-void
|
||
/// pixels, and may be copied from only if all of them may.
|
||
fn downsample(l: &Level, ch: usize) -> Level<'static> {
|
||
let (w, h) = (l.w.div_ceil(2), l.h.div_ceil(2));
|
||
let mut img = vec![0.0f32; w * h * ch];
|
||
let mut px = vec![Px::Void; w * h];
|
||
let (lw, lh, limg, lpx) = (l.w, l.h, &*l.img, &*l.px);
|
||
let row = |y: usize, irow: &mut [f32], prow: &mut [Px]| {
|
||
for (x, p) in prow.iter_mut().enumerate() {
|
||
let mut n = 0.0f32;
|
||
let o = x * ch;
|
||
let (mut hole, mut all_source, mut any) = (false, true, false);
|
||
for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
|
||
let (sx, sy) = (x * 2 + dx, y * 2 + dy);
|
||
if sx >= lw || sy >= lh {
|
||
continue;
|
||
}
|
||
let si = sy * lw + sx;
|
||
match lpx[si] {
|
||
Px::Hole => hole = true,
|
||
Px::Void => all_source = false,
|
||
k => {
|
||
any = true;
|
||
all_source &= k == Px::Source;
|
||
for c in 0..ch {
|
||
irow[o + c] += limg[si * ch + c];
|
||
}
|
||
n += 1.0;
|
||
}
|
||
}
|
||
}
|
||
if n > 0.0 {
|
||
for c in 0..ch {
|
||
irow[o + c] /= n;
|
||
}
|
||
}
|
||
*p = if hole {
|
||
Px::Hole
|
||
} else if !any {
|
||
Px::Void
|
||
} else if all_source {
|
||
Px::Source
|
||
} else {
|
||
Px::Known
|
||
};
|
||
}
|
||
};
|
||
par::chunks2_mut(&mut img, w * ch, &mut px, w, w * h >= PAR_MIN, row);
|
||
Level { w, h, img: Cow::Owned(img), px: Cow::Owned(px) }
|
||
}
|
||
|
||
/// Summed-area table of a boolean grid (`(w+1) × (h+1)`). Not inlined: in [`synthesize`] it made
|
||
/// Create Texture's candidate loop slower (measured).
|
||
#[inline(never)]
|
||
fn integral(w: usize, h: usize, m: &[bool]) -> Vec<u32> {
|
||
let mut s = vec![0u32; (w + 1) * (h + 1)];
|
||
for y in 0..h {
|
||
let mut row = 0u32;
|
||
for x in 0..w {
|
||
row += u32::from(m[y * w + x]);
|
||
s[(y + 1) * (w + 1) + x + 1] = s[y * (w + 1) + x + 1] + row;
|
||
}
|
||
}
|
||
s
|
||
}
|
||
|
||
/// Count of set cells in the clipped window `[x0, x1) × [y0, y1)`.
|
||
fn window_count(s: &[u32], gw: usize, gh: usize, x0: i32, y0: i32, x1: i32, y1: i32) -> u32 {
|
||
let (x0, y0) = (x0.max(0) as usize, y0.max(0) as usize);
|
||
let (x1, y1) = ((x1.max(0) as usize).min(gw), (y1.max(0) as usize).min(gh));
|
||
if x1 <= x0 || y1 <= y0 {
|
||
return 0;
|
||
}
|
||
let w1 = gw + 1;
|
||
s[y1 * w1 + x1] + s[y0 * w1 + x0] - s[y0 * w1 + x1] - s[y1 * w1 + x0]
|
||
}
|
||
|
||
fn hole_bbox(w: usize, h: usize, hole: &[bool]) -> Option<(usize, usize, usize, usize)> {
|
||
let mut b: Option<(usize, usize, usize, usize)> = None;
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
if hole[y * w + x] {
|
||
b = Some(match b {
|
||
None => (x, y, x + 1, y + 1),
|
||
Some((x0, y0, x1, y1)) => (x0.min(x), y0.min(y), x1.max(x + 1), y1.max(y + 1)),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
b
|
||
}
|
||
|
||
/// A hole at most `1 / THIN_RATIO` as thick as its bounds are long counts as thin (a wire, a
|
||
/// string, a hair, a long brush stroke): see the module docs.
|
||
const THIN_RATIO: usize = 4;
|
||
|
||
/// How thick the hole within bounds `hb` of a `w`-wide role grid is: twice the largest distance
|
||
/// from a hole pixel to the nearest pixel that is not one (a long, thin hole is thin however long
|
||
/// its bounds).
|
||
fn hole_thickness(w: usize, hb: Bounds, px: &[Px]) -> usize {
|
||
// The bounds and a ring of non-hole cells around them (also past the grid's edge).
|
||
let (gw, gh) = (hb.width() + 2, hb.height() + 2);
|
||
let outside: Vec<bool> = par::map(gw * gh, gw * gh >= PAR_MIN, |i| {
|
||
let (gx, gy) = (i % gw, i / gw);
|
||
if gx == 0 || gy == 0 || gx > hb.width() || gy > hb.height() {
|
||
return true;
|
||
}
|
||
px.get((hb.y0 + gy - 1) * w + hb.x0 + gx - 1) != Some(&Px::Hole)
|
||
});
|
||
let deepest = crate::selection::edt(&outside, gw, gh).into_iter().fold(0.0f32, f32::max);
|
||
(2.0 * deepest).ceil() as usize
|
||
}
|
||
|
||
/// Most target pixels × sources the onion peel of a thin hole may compare (its search is
|
||
/// exhaustive, though most comparisons stop early: about a second at this many).
|
||
const PEEL_BUDGET: usize = 1 << 28;
|
||
|
||
/// Whether the onion peel of the hole cells within `hb` of a `w`-wide role grid from `sources`
|
||
/// source centres stays within [`PEEL_BUDGET`].
|
||
fn peel_affordable(w: usize, px: &[Px], hb: Bounds, sources: usize) -> bool {
|
||
let holes: usize = (hb.y0..hb.y1).map(|y| px.get(y * w + hb.x0..y * w + hb.x1).map_or(0, |row| row.iter().filter(|k| **k == Px::Hole).count())).sum();
|
||
holes.saturating_mul(sources) <= PEEL_BUDGET
|
||
}
|
||
|
||
/// Bounds of the hole pixels of a role grid.
|
||
fn hole_bounds(w: usize, h: usize, px: &[Px]) -> Option<Bounds> {
|
||
// A zero-width grid holds no pixels whatever its height; don't size a row table by it.
|
||
if w == 0 || h == 0 {
|
||
return None;
|
||
}
|
||
// Per row the first and last hole column; the union of the rows' spans.
|
||
let rows: Vec<Option<(usize, usize)>> = par::map(h, w * h >= PAR_MIN, |y| {
|
||
let row = px.get(y * w..(y + 1) * w)?;
|
||
let x0 = row.iter().position(|k| *k == Px::Hole)?;
|
||
let x1 = row.iter().rposition(|k| *k == Px::Hole).unwrap_or(x0) + 1;
|
||
Some((x0, x1))
|
||
});
|
||
let mut b: Option<Bounds> = None;
|
||
for (y, span) in rows.into_iter().enumerate() {
|
||
if let Some((x0, x1)) = span {
|
||
b = Some(match b {
|
||
None => Bounds { x0, y0: y, x1, y1: y + 1 },
|
||
Some(b) => Bounds { x0: b.x0.min(x0), y0: b.y0, x1: b.x1.max(x1), y1: y + 1 },
|
||
});
|
||
}
|
||
}
|
||
b
|
||
}
|
||
|
||
/// Valid source centres: the whole `2r+1` patch inside the grid and copyable. Found per row
|
||
/// (each pixel's horizontal run), then per column of runs. `None` when cancelled.
|
||
fn valid_sources(w: usize, h: usize, r: usize, px: &[Px], ctl: &photocraft_raster::Interrupt) -> Option<Vec<bool>> {
|
||
const BAND: usize = 64;
|
||
let side = 2 * r + 1;
|
||
let par = w * h >= PAR_MIN;
|
||
let mut run = vec![false; w * h];
|
||
if w < side || h < side {
|
||
return Some(run);
|
||
}
|
||
// Per row: is the run of `side` pixels centred on each one all copyable (a sliding count)?
|
||
par::chunks_mut(&mut run, w, par, |y, out| {
|
||
if y % BAND == 0 && ctl.cancelled() {
|
||
return;
|
||
}
|
||
let row = &px[y * w..(y + 1) * w];
|
||
let copyable = |x: usize| usize::from(row[x] == Px::Source);
|
||
let mut count: usize = (0..side).map(copyable).sum();
|
||
out[r] = count == side;
|
||
for (x, o) in out.iter_mut().enumerate().take(w - r).skip(r + 1) {
|
||
count = count + copyable(x + r) - copyable(x - r - 1);
|
||
*o = count == side;
|
||
}
|
||
});
|
||
if ctl.cancelled() {
|
||
return None;
|
||
}
|
||
// Per column, in bands of rows: how many such runs end each row in a row; a centre is valid
|
||
// where `side` of them stack up to `r` rows below it.
|
||
let mut valid = vec![false; w * h];
|
||
par::chunks_mut(&mut valid, BAND * w, par, |band, out| {
|
||
if ctl.cancelled() {
|
||
return;
|
||
}
|
||
let (y0, rows) = (band * BAND, out.len() / w);
|
||
let mut stack = vec![0usize; w];
|
||
for yy in y0.saturating_sub(r)..(y0 + rows + r).min(h) {
|
||
for (x, n) in stack.iter_mut().enumerate() {
|
||
*n = if run[yy * w + x] { *n + 1 } else { 0 };
|
||
}
|
||
// `yy` is the bottom row of centre `y`'s patch.
|
||
if yy < y0 + r || yy < 2 * r {
|
||
continue;
|
||
}
|
||
let y = yy - r;
|
||
for (v, n) in out[(y - y0) * w..(y - y0 + 1) * w].iter_mut().zip(&stack).take(w - r).skip(r) {
|
||
*v = *n >= side;
|
||
}
|
||
}
|
||
});
|
||
if ctl.cancelled() {
|
||
return None;
|
||
}
|
||
Some(valid)
|
||
}
|
||
|
||
/// Columns per block of [`Sources`]' index.
|
||
const SOURCE_BLOCK: usize = 64;
|
||
|
||
/// The valid source centres in row-major order, indexed by counts per row and per block of
|
||
/// [`SOURCE_BLOCK`] columns (so the `k`-th is found without listing them all).
|
||
struct Sources {
|
||
/// Valid centres before each row (`h + 1` entries).
|
||
row_start: Vec<usize>,
|
||
/// Per row, the valid centres in it before each block.
|
||
block_start: Vec<u32>,
|
||
blocks: usize,
|
||
}
|
||
|
||
impl Sources {
|
||
fn new(valid: &[bool], w: usize) -> Sources {
|
||
let h = valid.len().checked_div(w).unwrap_or(0);
|
||
let blocks = w.div_ceil(SOURCE_BLOCK);
|
||
let mut block_start = vec![0u32; h * blocks];
|
||
// Per row: the block prefix, and the row's count.
|
||
let counts: Vec<usize> = par::map(h, valid.len() >= PAR_MIN, |y| valid[y * w..(y + 1) * w].iter().filter(|v| **v).count());
|
||
par::chunks_mut(&mut block_start, blocks, valid.len() >= PAR_MIN, |y, out| {
|
||
let mut n = 0u32;
|
||
for (b, o) in out.iter_mut().enumerate() {
|
||
*o = n;
|
||
n += valid[y * w + b * SOURCE_BLOCK..y * w + ((b + 1) * SOURCE_BLOCK).min(w)].iter().filter(|v| **v).count() as u32;
|
||
}
|
||
});
|
||
let mut row_start = Vec::with_capacity(counts.len() + 1);
|
||
let mut total = 0usize;
|
||
row_start.push(0);
|
||
for c in counts {
|
||
total += c;
|
||
row_start.push(total);
|
||
}
|
||
Sources { row_start, block_start, blocks }
|
||
}
|
||
|
||
fn len(&self) -> usize {
|
||
self.row_start.last().copied().unwrap_or(0)
|
||
}
|
||
|
||
/// The `k`-th valid centre in row-major order (`k < len`).
|
||
fn nth(&self, k: usize, valid: &[bool], w: usize) -> (i32, i32) {
|
||
let y = self.row_start.partition_point(|&s| s <= k).saturating_sub(1);
|
||
let left = k.saturating_sub(self.row_start.get(y).copied().unwrap_or(0));
|
||
let row_blocks = self.block_start.get(y * self.blocks..(y + 1) * self.blocks).unwrap_or(&[]);
|
||
let b = row_blocks.partition_point(|&s| s as usize <= left).saturating_sub(1);
|
||
let mut left = left.saturating_sub(row_blocks.get(b).copied().unwrap_or(0) as usize);
|
||
let x0 = b * SOURCE_BLOCK;
|
||
for (x, v) in valid.iter().skip(y * w + x0).take(w.saturating_sub(x0)).enumerate() {
|
||
if *v {
|
||
if left == 0 {
|
||
return ((x0 + x) as i32, y as i32);
|
||
}
|
||
left -= 1;
|
||
}
|
||
}
|
||
(0, 0)
|
||
}
|
||
|
||
/// All of them, in row-major order.
|
||
fn all(&self, valid: &[bool], w: usize) -> Vec<(i32, i32)> {
|
||
let mut out = Vec::with_capacity(self.len());
|
||
for (i, v) in valid.iter().enumerate() {
|
||
if *v {
|
||
out.push(((i % w) as i32, (i / w) as i32));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
}
|
||
|
||
/// Per cell of `tb` (the hole's bounds `hb` grown by the patch radius, where all of them lie): is
|
||
/// it a target centre (its patch overlaps the hole)?
|
||
fn targets_around(w: usize, r: usize, px: &[Px], hb: Bounds, tb: Bounds) -> Vec<bool> {
|
||
// Hole counts summed over the hole's bounds: every hole pixel lies inside them.
|
||
let (bw, bh) = (hb.width(), hb.height());
|
||
let mut s = vec![0u32; (bw + 1) * (bh + 1)];
|
||
for y in 0..bh {
|
||
let mut row = 0u32;
|
||
for x in 0..bw {
|
||
row += u32::from(px[(hb.y0 + y) * w + hb.x0 + x] == Px::Hole);
|
||
s[(y + 1) * (bw + 1) + x + 1] = s[y * (bw + 1) + x + 1] + row;
|
||
}
|
||
}
|
||
let mut targets = vec![false; tb.area()];
|
||
let ri = r as i32;
|
||
let (ox, oy) = (hb.x0 as i32, hb.y0 as i32);
|
||
for y in tb.y0..tb.y1 {
|
||
for x in tb.x0..tb.x1 {
|
||
let k = (y - tb.y0) * tb.width() + (x - tb.x0);
|
||
let (x, y) = (x as i32, y as i32);
|
||
targets[k] = window_count(&s, bw, bh, x - ri - ox, y - ri - oy, x + ri + 1 - ox, y + ri + 1 - oy) > 0;
|
||
}
|
||
}
|
||
targets
|
||
}
|
||
|
||
/// A finished coarser level: its size, pixels and match field (the source centre per cell of
|
||
/// its target box, `i32::MIN` where there is none; empty when it had nothing to copy).
|
||
struct Coarser {
|
||
w: usize,
|
||
h: usize,
|
||
img: Vec<f32>,
|
||
field: Vec<(i32, i32)>,
|
||
tbox: Bounds,
|
||
}
|
||
|
||
impl Coarser {
|
||
/// The match of cell `(x, y)` (`i32::MIN` outside the target box).
|
||
fn field_at(&self, x: usize, y: usize) -> (i32, i32) {
|
||
let t = self.tbox;
|
||
if x < t.x0 || x >= t.x1 || y < t.y0 || y >= t.y1 {
|
||
return (i32::MIN, i32::MIN);
|
||
}
|
||
self.field.get((y - t.y0) * t.width() + (x - t.x0)).copied().unwrap_or((i32::MIN, i32::MIN))
|
||
}
|
||
}
|
||
|
||
/// Most channels whose moments the colour adaptation fits (the rest are copied as they are).
|
||
const MOM_CH: usize = 8;
|
||
|
||
/// Per-level PatchMatch/EM state.
|
||
struct Solver<'a> {
|
||
w: usize,
|
||
h: usize,
|
||
ch: usize,
|
||
r: i32,
|
||
img: &'a mut Vec<f32>,
|
||
px: &'a [Px],
|
||
/// Valid source centres: the whole patch is inside the grid and may be copied from.
|
||
valid: Vec<bool>,
|
||
/// The hole's bounds and the targets' (the hole's grown by the patch radius): nothing outside
|
||
/// them changes or is matched. The match field, its costs and these per-target tables are
|
||
/// kept over `tbox`, row-major.
|
||
hole_box: Bounds,
|
||
tbox: Bounds,
|
||
/// Target centres (the patch overlaps the hole).
|
||
targets: Vec<bool>,
|
||
/// Per cell of `tbox`: the whole patch is inside the grid and not void (so its distances
|
||
/// need no bounds checks and its own moments can be kept).
|
||
full: Vec<bool>,
|
||
/// With colour adaptation, per cell of `tbox` and channel (at most [`MOM_CH`]): `Σt, Σt²` of
|
||
/// the patch, for full patches, as of the last [`Solver::costs`].
|
||
tmom: Vec<[f32; 2]>,
|
||
/// With colour adaptation where it pays: per source centre and channel, `Σs, Σs²` of its
|
||
/// patch (source pixels never change within a level). Empty otherwise.
|
||
smom: Vec<[f32; 2]>,
|
||
adapt: Adapt,
|
||
/// Cancellation: checked per row band; a cancelled solve's output is discarded.
|
||
ctl: photocraft_raster::Interrupt<'a>,
|
||
}
|
||
|
||
/// A channel deviation under which a patch counts as flat for [`fit`] (about 5 levels of 255).
|
||
const FLAT: f32 = 0.02;
|
||
|
||
/// Gain and bias of one channel for target moments `(Σt, Σt²)` and source moments `(Σs, Σs²)`
|
||
/// over `n` samples, within the limits.
|
||
#[inline]
|
||
pub(crate) fn fit(a: Adapt, n: f32, st: f32, stt: f32, ss: f32, sss: f32) -> (f32, f32) {
|
||
let (mt, ms) = (st / n, ss / n);
|
||
let vt = (stt / n - mt * mt).max(0.0);
|
||
let vs = (sss / n - ms * ms).max(0.0);
|
||
let g = if vs > 1e-10 { (vt / vs).sqrt() } else { 1.0 };
|
||
let (mut lo, mut hi) = a.gain;
|
||
// Nearly flat patches of about the same colour (their means within the bias of each other):
|
||
// the ratio of their deviations is noise, so match the mean first, keeping the gain where the
|
||
// bias can still bring the source's mean to the target's. Gained about zero, a gain at its
|
||
// limit moved bright means further than the bias could bring back, and each channel by a
|
||
// different amount: a tinted blotch in a smooth gradient. Textured patches, and flat ones
|
||
// whose colours differ more (Content-Aware Move's Color), are fitted as before.
|
||
if vt.max(vs) < FLAT * FLAT && (mt - ms).abs() <= a.bias && ms > 1e-6 {
|
||
let (glo, ghi) = ((mt - a.bias) / ms, (mt + a.bias) / ms);
|
||
if glo.max(lo) <= ghi.min(hi) {
|
||
(lo, hi) = (glo.max(lo), ghi.min(hi));
|
||
}
|
||
}
|
||
let g = g.clamp(lo, hi);
|
||
let b = (mt - g * ms).clamp(-a.bias, a.bias);
|
||
(g, b)
|
||
}
|
||
|
||
impl Solver<'_> {
|
||
#[inline]
|
||
fn is_valid(&self, x: i32, y: i32) -> bool {
|
||
x >= 0 && y >= 0 && (x as usize) < self.w && (y as usize) < self.h && self.valid[y as usize * self.w + x as usize]
|
||
}
|
||
|
||
/// Mark the full target patches (`full`), and keep the source moments where they pay off.
|
||
fn prepare(&mut self, keep_source_moments: bool) {
|
||
let (w, h, r, tb) = (self.w, self.h, self.r as usize, self.tbox);
|
||
let side = 2 * r + 1;
|
||
let mut full = vec![false; tb.area()];
|
||
let px = self.px;
|
||
par::chunks_mut(&mut full, tb.width(), tb.area() >= PAR_MIN_PATCHES, |ty, out| {
|
||
let y = tb.y0 + ty;
|
||
if y < r || y + r >= h {
|
||
return;
|
||
}
|
||
for (tx, f) in out.iter_mut().enumerate() {
|
||
let x = tb.x0 + tx;
|
||
if x < r || x + r >= w {
|
||
continue;
|
||
}
|
||
*f = (y - r..=y + r).all(|yy| px[yy * w + x - r..yy * w + x - r + side].iter().all(|k| *k != Px::Void));
|
||
}
|
||
});
|
||
self.full = full;
|
||
if self.adapt.is_none() {
|
||
return;
|
||
}
|
||
let c8 = self.ch.min(MOM_CH);
|
||
self.tmom = vec![[0.0; 2]; tb.area() * c8];
|
||
if !keep_source_moments {
|
||
return;
|
||
}
|
||
let (ch, img, valid, ctl) = (self.ch, &*self.img, &self.valid, &self.ctl);
|
||
let mut smom = vec![[0.0f32; 2]; w * h * c8];
|
||
par::chunks_mut(&mut smom, w * c8, w * h >= PAR_MIN_PATCHES, |y, out| {
|
||
if ctl.cancelled() {
|
||
return;
|
||
}
|
||
for x in 0..w {
|
||
if valid[y * w + x] {
|
||
patch_moments(img, w, ch, c8, side, x - r, y - r, &mut out[x * c8..(x + 1) * c8]);
|
||
}
|
||
}
|
||
});
|
||
self.smom = smom;
|
||
}
|
||
|
||
/// Distance between the target patch at `t` (its pixels inside the grid and not void; `k` its
|
||
/// index in the `tbox` tables) and the source patch at `s`, per sample; with colour adaptation
|
||
/// the source is fitted first.
|
||
#[inline]
|
||
fn dist(&self, t: (i32, i32), k: usize, s: (i32, i32), cutoff: f32) -> f32 {
|
||
let full = self.full[k];
|
||
if !self.adapt.is_none() {
|
||
return if full { self.dist_adapted_full(k, t, s) } else { self.dist_adapted(t, s) };
|
||
}
|
||
if full { self.dist_full(t, s, cutoff) } else { self.dist_clipped(t, s, cutoff) }
|
||
}
|
||
|
||
/// [`Solver::dist`] without adaptation, of a patch that may be clipped.
|
||
fn dist_clipped(&self, t: (i32, i32), s: (i32, i32), cutoff: f32) -> f32 {
|
||
let (w, h, ch, r) = (self.w as i32, self.h as i32, self.ch, self.r);
|
||
let full = ((2 * r + 1) * (2 * r + 1)) as f32 * ch as f32;
|
||
let mut sum = 0.0f32;
|
||
let mut n = 0usize;
|
||
for dy in -r..=r {
|
||
let ty = t.1 + dy;
|
||
if ty < 0 || ty >= h {
|
||
continue;
|
||
}
|
||
let sy = s.1 + dy;
|
||
for dx in -r..=r {
|
||
let tx = t.0 + dx;
|
||
if tx < 0 || tx >= w || self.px[ty as usize * self.w + tx as usize] == Px::Void {
|
||
continue;
|
||
}
|
||
let ti = (ty as usize * self.w + tx as usize) * ch;
|
||
let si = (sy as usize * self.w + (s.0 + dx) as usize) * ch;
|
||
for c in 0..ch {
|
||
let d = self.img[ti + c] - self.img[si + c];
|
||
sum += d * d;
|
||
}
|
||
n += ch;
|
||
}
|
||
// Early out: the mean can only be ≥ sum / (full patch size).
|
||
if sum > cutoff * full {
|
||
return f32::INFINITY;
|
||
}
|
||
}
|
||
if n == 0 { f32::INFINITY } else { sum / n as f32 }
|
||
}
|
||
|
||
/// [`Solver::dist_clipped`] of a full patch: the same sums, in the same order. It gives up
|
||
/// (infinity) as soon as the sum shows the patch is worse than `cutoff`, which the sum, only
|
||
/// growing, can show after any pixel: the caller only asks whether it is better.
|
||
fn dist_full(&self, t: (i32, i32), s: (i32, i32), cutoff: f32) -> f32 {
|
||
let (w, ch, r) = (self.w, self.ch, self.r);
|
||
let side = (2 * r + 1) as usize;
|
||
let full = ((2 * r + 1) * (2 * r + 1)) as f32 * ch as f32;
|
||
let (span, limit) = (side * ch, cutoff * full);
|
||
let mut sum = 0.0f32;
|
||
for dy in -r..=r {
|
||
let ti = ((t.1 + dy) as usize * w + (t.0 - r) as usize) * ch;
|
||
let si = ((s.1 + dy) as usize * w + (s.0 - r) as usize) * ch;
|
||
for (tp, sp) in self.img[ti..ti + span].chunks_exact(ch).zip(self.img[si..si + span].chunks_exact(ch)) {
|
||
for (a, b) in tp.iter().zip(sp) {
|
||
let d = a - b;
|
||
sum += d * d;
|
||
}
|
||
if sum > limit {
|
||
return f32::INFINITY;
|
||
}
|
||
}
|
||
}
|
||
sum / (side * side * ch) as f32
|
||
}
|
||
|
||
/// Moments of the target patch at `t` and the source patch at `s` per channel:
|
||
/// `[Σt, Σt², Σs, Σs², Σts]`, and the sample count.
|
||
fn moments(&self, t: (i32, i32), s: (i32, i32), m: &mut [[f32; 5]]) -> usize {
|
||
let (w, h, ch, r) = (self.w as i32, self.h as i32, self.ch, self.r);
|
||
m.iter_mut().for_each(|v| *v = [0.0; 5]);
|
||
let mut n = 0usize;
|
||
for dy in -r..=r {
|
||
let ty = t.1 + dy;
|
||
if ty < 0 || ty >= h {
|
||
continue;
|
||
}
|
||
let sy = s.1 + dy;
|
||
for dx in -r..=r {
|
||
let tx = t.0 + dx;
|
||
if tx < 0 || tx >= w || self.px[ty as usize * self.w + tx as usize] == Px::Void {
|
||
continue;
|
||
}
|
||
let ti = (ty as usize * self.w + tx as usize) * ch;
|
||
let si = (sy as usize * self.w + (s.0 + dx) as usize) * ch;
|
||
for (c, mc) in m.iter_mut().enumerate().take(ch) {
|
||
let (a, b) = (self.img[ti + c], self.img[si + c]);
|
||
mc[0] += a;
|
||
mc[1] += a * a;
|
||
mc[2] += b;
|
||
mc[3] += b * b;
|
||
mc[4] += a * b;
|
||
}
|
||
n += 1;
|
||
}
|
||
}
|
||
n
|
||
}
|
||
|
||
/// [`Solver::moments`] of a full target patch (`k` in the `tbox` tables): the target's own
|
||
/// sums kept from [`Solver::costs`], the source's from [`Solver::prepare`] when kept, each
|
||
/// summed in the same order as there.
|
||
fn moments_full(&self, k: usize, t: (i32, i32), s: (i32, i32), m: &mut [[f32; 5]]) {
|
||
let (w, ch, r) = (self.w, self.ch, self.r);
|
||
let c8 = m.len();
|
||
let side = (2 * r + 1) as usize;
|
||
let tm = &self.tmom[k * c8..(k + 1) * c8];
|
||
let sm = self.smom.get((s.1 as usize * w + s.0 as usize) * c8..(s.1 as usize * w + s.0 as usize + 1) * c8);
|
||
let mut acc = [[0.0f32; 3]; MOM_CH];
|
||
for dy in -r..=r {
|
||
let ti = ((t.1 + dy) as usize * w + (t.0 - r) as usize) * ch;
|
||
let si = ((s.1 + dy) as usize * w + (s.0 - r) as usize) * ch;
|
||
let (trow, srow) = (&self.img[ti..ti + side * ch], &self.img[si..si + side * ch]);
|
||
if sm.is_some() {
|
||
for (tp, sp) in trow.chunks_exact(ch).zip(srow.chunks_exact(ch)) {
|
||
for c in 0..c8 {
|
||
acc[c][2] += tp[c] * sp[c];
|
||
}
|
||
}
|
||
} else {
|
||
for (tp, sp) in trow.chunks_exact(ch).zip(srow.chunks_exact(ch)) {
|
||
for c in 0..c8 {
|
||
let (a, b) = (tp[c], sp[c]);
|
||
acc[c][0] += b;
|
||
acc[c][1] += b * b;
|
||
acc[c][2] += a * b;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (c, mc) in m.iter_mut().enumerate() {
|
||
let [ss, sss] = match sm {
|
||
Some(sm) => sm[c],
|
||
None => [acc[c][0], acc[c][1]],
|
||
};
|
||
*mc = [tm[c][0], tm[c][1], ss, sss, acc[c][2]];
|
||
}
|
||
}
|
||
|
||
fn dist_adapted(&self, t: (i32, i32), s: (i32, i32)) -> f32 {
|
||
let mut m = [[0.0f32; 5]; MOM_CH];
|
||
let ch = self.ch.min(MOM_CH);
|
||
let n = self.moments(t, s, &mut m[..ch]);
|
||
if n == 0 {
|
||
return f32::INFINITY;
|
||
}
|
||
adapted_cost(self.adapt, n as f32, &m[..ch])
|
||
}
|
||
|
||
fn dist_adapted_full(&self, k: usize, t: (i32, i32), s: (i32, i32)) -> f32 {
|
||
let mut m = [[0.0f32; 5]; MOM_CH];
|
||
let ch = self.ch.min(MOM_CH);
|
||
self.moments_full(k, t, s, &mut m[..ch]);
|
||
let side = (2 * self.r + 1) as usize;
|
||
adapted_cost(self.adapt, (side * side) as f32, &m[..ch])
|
||
}
|
||
|
||
/// Gain and bias per channel for the match `t → s` (identity without adaptation).
|
||
fn transform(&self, t: (i32, i32), k: usize, s: (i32, i32), out: &mut [(f32, f32)]) {
|
||
if self.adapt.is_none() {
|
||
out.iter_mut().for_each(|v| *v = (1.0, 0.0));
|
||
return;
|
||
}
|
||
let mut m = [[0.0f32; 5]; MOM_CH];
|
||
let ch = self.ch.min(MOM_CH);
|
||
let n = if self.full[k] {
|
||
self.moments_full(k, t, s, &mut m[..ch]);
|
||
let side = (2 * self.r + 1) as usize;
|
||
side * side
|
||
} else {
|
||
self.moments(t, s, &mut m[..ch])
|
||
};
|
||
for (c, o) in out.iter_mut().enumerate() {
|
||
*o = if n == 0 || c >= ch { (1.0, 0.0) } else { fit(self.adapt, n as f32, m[c][0], m[c][1], m[c][2], m[c][3]) };
|
||
}
|
||
}
|
||
|
||
/// One PatchMatch step at target `t` (`k` in the target box) whose match is `cur`: propagation
|
||
/// from the matches of the neighbours visited just before it (`left` in its row, `up` in its
|
||
/// column, in this pass's direction `step`), then random search in exponentially shrinking
|
||
/// windows around the best (two numbers from `rng` per window).
|
||
#[allow(clippy::too_many_arguments)]
|
||
#[inline]
|
||
fn pm_step(
|
||
&self,
|
||
t: (i32, i32),
|
||
k: usize,
|
||
cur: ((i32, i32), f32),
|
||
left: Option<(i32, i32)>,
|
||
up: Option<(i32, i32)>,
|
||
step: i32,
|
||
rng: &mut Rng,
|
||
search_radius: i32,
|
||
) -> ((i32, i32), f32) {
|
||
let (mut best, mut bc) = cur;
|
||
if let Some(c) = left {
|
||
let cand = (c.0 + step, c.1);
|
||
if self.is_valid(cand.0, cand.1) && cand != best {
|
||
let d = self.dist(t, k, cand, bc);
|
||
if d < bc {
|
||
(best, bc) = (cand, d);
|
||
}
|
||
}
|
||
}
|
||
if let Some(c) = up {
|
||
let cand = (c.0, c.1 + step);
|
||
if self.is_valid(cand.0, cand.1) && cand != best {
|
||
let d = self.dist(t, k, cand, bc);
|
||
if d < bc {
|
||
(best, bc) = (cand, d);
|
||
}
|
||
}
|
||
}
|
||
let mut rad = search_radius;
|
||
while rad >= 1 {
|
||
let cand = (best.0 + rng.range(-rad, rad), best.1 + rng.range(-rad, rad));
|
||
if self.is_valid(cand.0, cand.1) && cand != best {
|
||
let d = self.dist(t, k, cand, bc);
|
||
if d < bc {
|
||
(best, bc) = (cand, d);
|
||
}
|
||
}
|
||
rad /= 2;
|
||
}
|
||
(best, bc)
|
||
}
|
||
|
||
/// One PatchMatch pass over all target rows, in bands of the grid's rows: each band is
|
||
/// visited in raster order (backwards on odd passes), propagating within itself, its random
|
||
/// numbers seeded by its index. `nnf` and `cost` are over the target box. The bands run in
|
||
/// parallel; when there are fewer of them than threads, each is also swept in wavefronts of
|
||
/// blocks (a cell depends only on its neighbours before it in its row and its column), which
|
||
/// visits every cell with the same neighbours and random numbers as the raster order.
|
||
fn patchmatch(&self, nnf: &mut [(i32, i32)], cost: &mut [f32], pass: usize, seed: u64, search_radius: i32) {
|
||
const BAND: usize = 16;
|
||
let tb = self.tbox;
|
||
let tw = tb.width();
|
||
if tw == 0 || tb.height() == 0 || nnf.len() < tb.area() || cost.len() < tb.area() {
|
||
return;
|
||
}
|
||
let reverse = pass % 2 == 1;
|
||
let step: i32 = if reverse { -1 } else { 1 };
|
||
let band_seed = |bi: usize| seed ^ (bi as u64).wrapping_mul(0x2545_F491_4F6C_DD1D) ^ ((pass as u64) << 40);
|
||
// The bands crossing the target box: index, first row, rows in the box.
|
||
let mut bands = Vec::new();
|
||
let mut y = tb.y0;
|
||
while y < tb.y1 {
|
||
let bi = y / BAND;
|
||
let end = ((bi + 1) * BAND).min(tb.y1);
|
||
bands.push((bi, y, end - y));
|
||
y = end;
|
||
}
|
||
let threads = par::threads();
|
||
if tb.area() >= PAR_MIN_PATCHES * 8 && !bands.is_empty() && bands.len() < threads {
|
||
self.patchmatch_wavefront(nnf, cost, &bands, reverse, &band_seed, search_radius);
|
||
return;
|
||
}
|
||
let band_fn = |bi: usize, ys: usize, nn: &mut [(i32, i32)], co: &mut [f32]| {
|
||
if self.ctl.cancelled() {
|
||
return;
|
||
}
|
||
let rows = nn.len() / tw;
|
||
let mut rng = Rng::new(band_seed(bi));
|
||
for ry in 0..rows {
|
||
if self.ctl.cancelled() {
|
||
return;
|
||
}
|
||
let ly = if reverse { rows - 1 - ry } else { ry };
|
||
let y = ys + ly;
|
||
for rx in 0..tw {
|
||
let lx = if reverse { tw - 1 - rx } else { rx };
|
||
let li = ly * tw + lx;
|
||
let k = (y - tb.y0) * tw + lx;
|
||
if !self.targets[k] {
|
||
continue;
|
||
}
|
||
// Neighbours outside the target box hold no match: nothing to try.
|
||
let nx = lx as i32 - step;
|
||
let left = (nx >= 0 && (nx as usize) < tw).then(|| nn[ly * tw + nx as usize]);
|
||
let nly = ly as i32 - step;
|
||
let up = (nly >= 0 && (nly as usize) < rows).then(|| nn[nly as usize * tw + lx]);
|
||
let t = ((tb.x0 + lx) as i32, y as i32);
|
||
(nn[li], co[li]) = self.pm_step(t, k, (nn[li], co[li]), left, up, step, &mut rng, search_radius);
|
||
}
|
||
}
|
||
};
|
||
// The target box's rows, band by band.
|
||
let mut parts = Vec::with_capacity(bands.len());
|
||
let (mut nn_rest, mut co_rest) = (&mut nnf[..tb.area()], &mut cost[..tb.area()]);
|
||
for &(bi, ys, rows) in &bands {
|
||
let (nn, nr) = std::mem::take(&mut nn_rest).split_at_mut(rows * tw);
|
||
let (co, cr) = std::mem::take(&mut co_rest).split_at_mut(rows * tw);
|
||
parts.push((bi, ys, nn, co));
|
||
(nn_rest, co_rest) = (nr, cr);
|
||
}
|
||
let par = tb.area() >= PAR_MIN_PATCHES && parts.len() > 1;
|
||
par::each(parts, par, |(bi, ys, nn, co)| band_fn(bi, ys, nn, co));
|
||
}
|
||
|
||
/// [`Solver::patchmatch`] swept in wavefronts: the cells of each band in blocks of a row, a
|
||
/// block once the block before it in its row and the one above it (in the pass's direction)
|
||
/// are done. A target's random numbers are those it would draw in raster order (each target
|
||
/// draws the same count).
|
||
fn patchmatch_wavefront(
|
||
&self,
|
||
nnf: &mut [(i32, i32)],
|
||
cost: &mut [f32],
|
||
bands: &[(usize, usize, usize)],
|
||
reverse: bool,
|
||
band_seed: &(dyn Fn(usize) -> u64 + Sync),
|
||
search_radius: i32,
|
||
) {
|
||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering::Relaxed};
|
||
|
||
let tb = self.tbox;
|
||
let tw = tb.width();
|
||
let step: i32 = if reverse { -1 } else { 1 };
|
||
let pack = |(x, y): (i32, i32)| (u64::from(x as u32) << 32) | u64::from(y as u32);
|
||
let unpack = |v: u64| ((v >> 32) as u32 as i32, v as u32 as i32);
|
||
// Random numbers each target draws.
|
||
let mut draws = 0u64;
|
||
let mut rad = search_radius;
|
||
while rad >= 1 {
|
||
draws += 2;
|
||
rad /= 2;
|
||
}
|
||
// Targets before each column of each row of the box.
|
||
let mut before = vec![0u32; tb.height() * (tw + 1)];
|
||
for (ry, row) in before.chunks_mut(tw + 1).enumerate() {
|
||
for lx in 0..tw {
|
||
row[lx + 1] = row[lx] + u32::from(self.targets[ry * tw + lx]);
|
||
}
|
||
}
|
||
// Targets visited before each of a band's rows (in the pass's order).
|
||
let row_base: Vec<Vec<u64>> = bands
|
||
.iter()
|
||
.map(|&(_, ys, rows)| {
|
||
let mut base = Vec::with_capacity(rows);
|
||
let mut n = 0u64;
|
||
for pr in 0..rows {
|
||
base.push(n);
|
||
let ly = if reverse { rows - 1 - pr } else { pr };
|
||
n += u64::from(before[(ys + ly - tb.y0) * (tw + 1) + tw]);
|
||
}
|
||
base
|
||
})
|
||
.collect();
|
||
let block = (tw / 8).clamp(4, 32);
|
||
let blocks = tw.div_ceil(block);
|
||
let nn: Vec<AtomicU64> = nnf[..tb.area()].iter().map(|m| AtomicU64::new(pack(*m))).collect();
|
||
let co: Vec<AtomicU32> = cost[..tb.area()].iter().map(|c| AtomicU32::new(c.to_bits())).collect();
|
||
let sweep = |(j, pr, b): (usize, usize, usize)| {
|
||
let (bi, ys, rows) = bands[j];
|
||
let ly = if reverse { rows - 1 - pr } else { pr };
|
||
let y = ys + ly;
|
||
let ry = y - tb.y0;
|
||
let (pc0, pc1) = (b * block, ((b + 1) * block).min(tw));
|
||
// The targets visited before this block, in raster order.
|
||
let first = if reverse { tw - 1 - pc0 } else { pc0 };
|
||
let row = &before[ry * (tw + 1)..(ry + 1) * (tw + 1)];
|
||
let in_row = if reverse { row[tw] - row[first + 1] } else { row[first] };
|
||
let mut rng = Rng::new(band_seed(bi));
|
||
rng.skip((row_base[j][pr] + u64::from(in_row)) * draws);
|
||
for pc in pc0..pc1 {
|
||
let lx = if reverse { tw - 1 - pc } else { pc };
|
||
let k = ry * tw + lx;
|
||
if !self.targets[k] {
|
||
continue;
|
||
}
|
||
let nx = lx as i32 - step;
|
||
let left = (nx >= 0 && (nx as usize) < tw).then(|| unpack(nn[ry * tw + nx as usize].load(Relaxed)));
|
||
let nly = ly as i32 - step;
|
||
let up = (nly >= 0 && (nly as usize) < rows).then(|| unpack(nn[(ys + nly as usize - tb.y0) * tw + lx].load(Relaxed)));
|
||
let cur = (unpack(nn[k].load(Relaxed)), f32::from_bits(co[k].load(Relaxed)));
|
||
let t = ((tb.x0 + lx) as i32, y as i32);
|
||
let (m, c) = self.pm_step(t, k, cur, left, up, step, &mut rng, search_radius);
|
||
nn[k].store(pack(m), Relaxed);
|
||
co[k].store(c.to_bits(), Relaxed);
|
||
}
|
||
};
|
||
// Wavefront `d`: block `b` of (visited) row `pr` with `pr + b = d`, in every band. Each
|
||
// wave ends before the next starts.
|
||
let longest = bands.iter().map(|b| b.2).max().unwrap_or(0);
|
||
for d in 0..longest + blocks - 1 {
|
||
if self.ctl.cancelled() {
|
||
return;
|
||
}
|
||
let wave: Vec<(usize, usize, usize)> = bands
|
||
.iter()
|
||
.enumerate()
|
||
.flat_map(|(j, &(_, _, rows))| (0..rows).filter(move |&pr| pr <= d && d - pr < blocks).map(move |pr| (j, pr, d - pr)))
|
||
.collect();
|
||
par::each(wave, true, sweep);
|
||
}
|
||
for (m, a) in nnf.iter_mut().zip(&nn) {
|
||
*m = unpack(a.load(Relaxed));
|
||
}
|
||
for (c, a) in cost.iter_mut().zip(&co) {
|
||
*c = f32::from_bits(a.load(Relaxed));
|
||
}
|
||
}
|
||
|
||
/// Recompute every target's match cost (hole pixels change between iterations), and with
|
||
/// colour adaptation the full target patches' own moments.
|
||
fn costs(&mut self, nnf: &[(i32, i32)], cost: &mut [f32]) -> Result<(), photocraft_raster::Cancelled> {
|
||
let (w, ch, r, tb) = (self.w, self.ch, self.r as usize, self.tbox);
|
||
let par = tb.area() >= PAR_MIN_PATCHES;
|
||
self.ctl.check()?;
|
||
if !self.adapt.is_none() {
|
||
let c8 = ch.min(MOM_CH);
|
||
let side = 2 * r + 1;
|
||
let mut tmom = std::mem::take(&mut self.tmom);
|
||
let (img, full, ctl) = (&*self.img, &self.full, &self.ctl);
|
||
par::chunks_mut(&mut tmom, tb.width() * c8, par, |ty, out| {
|
||
if ctl.cancelled() {
|
||
return;
|
||
}
|
||
for tx in 0..tb.width() {
|
||
if full[ty * tb.width() + tx] {
|
||
patch_moments(img, w, ch, c8, side, tb.x0 + tx - r, tb.y0 + ty - r, &mut out[tx * c8..(tx + 1) * c8]);
|
||
}
|
||
}
|
||
});
|
||
self.tmom = tmom;
|
||
}
|
||
let this = &*self;
|
||
par::chunks_mut(cost, tb.width(), par, |ty, co| {
|
||
if this.ctl.cancelled() {
|
||
return;
|
||
}
|
||
let y = tb.y0 + ty;
|
||
for (tx, c) in co.iter_mut().enumerate() {
|
||
let k = ty * tb.width() + tx;
|
||
if this.targets[k] {
|
||
*c = this.dist(((tb.x0 + tx) as i32, y as i32), k, nnf[k], f32::INFINITY);
|
||
}
|
||
}
|
||
});
|
||
self.ctl.check()
|
||
}
|
||
|
||
/// Re-estimate hole pixels by a similarity-weighted vote of all overlapping patch matches
|
||
/// (each copy adjusted by its gain and bias).
|
||
fn vote(&mut self, nnf: &[(i32, i32)], cost: &[f32]) {
|
||
let (w, h, ch, r) = (self.w, self.h, self.ch, self.r);
|
||
let (tb, hb) = (self.tbox, self.hole_box);
|
||
// A cancelled solve is thrown away.
|
||
if self.ctl.cancelled() {
|
||
return;
|
||
}
|
||
let used = |k: usize| self.targets[k] && cost[k].is_finite();
|
||
// σ² from the 75th percentile of match costs (Wexler et al. use a robust scale like this).
|
||
let cs: Vec<f32> = (0..tb.area()).filter(|&k| used(k)).map(|k| cost[k]).collect();
|
||
let mut cs = cs;
|
||
let sigma2 = if cs.is_empty() {
|
||
1.0
|
||
} else {
|
||
let k = (cs.len() * 3 / 4).min(cs.len() - 1);
|
||
let (_, v, _) = cs.select_nth_unstable_by(k, |a, b| a.total_cmp(b));
|
||
v.max(1e-6)
|
||
};
|
||
drop(cs);
|
||
// Each target's weight and photometric transform (computed once, used by every pixel it
|
||
// covers).
|
||
let adapted = !self.adapt.is_none();
|
||
let par = tb.area() >= PAR_MIN_PATCHES;
|
||
// A weight (zero for a target that doesn't vote) and its match's centre (as a pixel
|
||
// index) per target.
|
||
let mut wt = vec![0.0f32; tb.area()];
|
||
let mut src = vec![0isize; tb.area()];
|
||
par::chunks2_mut(&mut wt, tb.width(), &mut src, tb.width(), par, |ty, wrow, srow| {
|
||
for (tx, (o, sc)) in wrow.iter_mut().zip(srow.iter_mut()).enumerate() {
|
||
let k = ty * tb.width() + tx;
|
||
if used(k) {
|
||
*o = (-cost[k] / (2.0 * sigma2)).exp().max(1e-8);
|
||
*sc = nnf[k].1 as isize * w as isize + nnf[k].0 as isize;
|
||
}
|
||
}
|
||
});
|
||
let mut gb: Vec<(f32, f32)> = Vec::new();
|
||
if adapted {
|
||
gb = vec![(1.0f32, 0.0f32); tb.area() * ch];
|
||
let this = &*self;
|
||
par::chunks_mut(&mut gb, tb.width() * ch, par, |ty, out| {
|
||
if this.ctl.cancelled() {
|
||
return;
|
||
}
|
||
let y = tb.y0 + ty;
|
||
for tx in 0..tb.width() {
|
||
let k = ty * tb.width() + tx;
|
||
if used(k) {
|
||
this.transform(((tb.x0 + tx) as i32, y as i32), k, nnf[k], &mut out[tx * ch..(tx + 1) * ch]);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
// The vote, into a buffer over the hole's bounds (every pixel reads the old pixels).
|
||
let votes = Votes { w, h, ch, r: r as usize, tbox: tb, img: &*self.img, wt: &wt, src: &src, gb: &gb };
|
||
let (px, ctl) = (self.px, &self.ctl);
|
||
let mut out = vec![0.0f32; hb.area() * ch];
|
||
let mut got = vec![false; hb.area()];
|
||
let row = |yy: usize, orow: &mut [f32], grow: &mut [bool]| {
|
||
if ctl.cancelled() {
|
||
return;
|
||
}
|
||
let y = hb.y0 + yy;
|
||
for x in hb.x0..hb.x1 {
|
||
if px[y * w + x] != Px::Hole {
|
||
continue;
|
||
}
|
||
let o = (x - hb.x0) * ch;
|
||
let dst = &mut orow[o..o + ch];
|
||
grow[x - hb.x0] = match (ch, adapted) {
|
||
(1, false) => votes.pixel::<1, false>(x, y, dst),
|
||
(3, false) => votes.pixel::<3, false>(x, y, dst),
|
||
(4, false) => votes.pixel::<4, false>(x, y, dst),
|
||
(1, true) => votes.pixel::<1, true>(x, y, dst),
|
||
(3, true) => votes.pixel::<3, true>(x, y, dst),
|
||
(4, true) => votes.pixel::<4, true>(x, y, dst),
|
||
_ => votes.pixel_any(x, y, adapted, dst),
|
||
};
|
||
}
|
||
};
|
||
par::chunks2_mut(&mut out, hb.width() * ch, &mut got, hb.width(), hb.area() >= PAR_MIN_PATCHES, row);
|
||
for yy in 0..hb.height() {
|
||
for xx in 0..hb.width() {
|
||
if got[yy * hb.width() + xx] {
|
||
let (d, s) = (((hb.y0 + yy) * w + hb.x0 + xx) * ch, (yy * hb.width() + xx) * ch);
|
||
self.img[d..d + ch].copy_from_slice(&out[s..s + ch]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// What [`Solver::vote`] votes with: per target (over the target box) its weight (zero when it
|
||
/// doesn't vote), its match's centre as a pixel index, and with colour adaptation its gain and
|
||
/// bias per channel.
|
||
struct Votes<'a> {
|
||
w: usize,
|
||
h: usize,
|
||
ch: usize,
|
||
r: usize,
|
||
tbox: Bounds,
|
||
img: &'a [f32],
|
||
wt: &'a [f32],
|
||
src: &'a [isize],
|
||
gb: &'a [(f32, f32)],
|
||
}
|
||
|
||
impl Votes<'_> {
|
||
/// Hole pixel `(x, y)`'s vote into `out` (`CH` channels): the weighted mean of what the
|
||
/// matches of the patches covering it put there, summed patch by patch in raster order of
|
||
/// their centres. False when no patch votes.
|
||
#[inline(always)]
|
||
fn pixel<const CH: usize, const ADAPT: bool>(&self, x: usize, y: usize, out: &mut [f32]) -> bool {
|
||
let (w, r, tb) = (self.w, self.r, self.tbox);
|
||
let mut acc = [0.0f32; CH];
|
||
let mut wsum = 0.0f32;
|
||
for ty in y.saturating_sub(r)..=(y + r).min(self.h - 1) {
|
||
let dy = ty as isize - y as isize;
|
||
let krow = (ty - tb.y0) * tb.width();
|
||
for tx in x.saturating_sub(r)..=(x + r).min(w - 1) {
|
||
let k = krow + tx - tb.x0;
|
||
let wt = self.wt[k];
|
||
if wt == 0.0 {
|
||
continue;
|
||
}
|
||
// The pixel of the match's patch that lands here.
|
||
let si = (self.src[k] - dy * w as isize - (tx as isize - x as isize)) as usize * CH;
|
||
let s = &self.img[si..si + CH];
|
||
for c in 0..CH {
|
||
let v = if ADAPT {
|
||
let (g, b) = self.gb[k * CH + c];
|
||
g * s[c] + b
|
||
} else {
|
||
s[c]
|
||
};
|
||
acc[c] += v * wt;
|
||
}
|
||
wsum += wt;
|
||
}
|
||
}
|
||
if wsum > 0.0 {
|
||
for (d, a) in out.iter_mut().zip(&acc) {
|
||
*d = a / wsum;
|
||
}
|
||
return true;
|
||
}
|
||
false
|
||
}
|
||
|
||
/// [`Votes::pixel`] for any number of channels.
|
||
fn pixel_any(&self, x: usize, y: usize, adapted: bool, out: &mut [f32]) -> bool {
|
||
let (w, r, tb, ch) = (self.w, self.r, self.tbox, self.ch);
|
||
let mut acc = vec![0.0f32; ch];
|
||
let mut wsum = 0.0f32;
|
||
for ty in y.saturating_sub(r)..=(y + r).min(self.h - 1) {
|
||
let dy = ty as isize - y as isize;
|
||
let krow = (ty - tb.y0) * tb.width();
|
||
for tx in x.saturating_sub(r)..=(x + r).min(w - 1) {
|
||
let k = krow + tx - tb.x0;
|
||
let wt = self.wt[k];
|
||
if wt == 0.0 {
|
||
continue;
|
||
}
|
||
let si = (self.src[k] - dy * w as isize - (tx as isize - x as isize)) as usize * ch;
|
||
for (c, a) in acc.iter_mut().enumerate() {
|
||
let v = if adapted {
|
||
let (g, b) = self.gb[k * ch + c];
|
||
g * self.img[si + c] + b
|
||
} else {
|
||
self.img[si + c]
|
||
};
|
||
*a += v * wt;
|
||
}
|
||
wsum += wt;
|
||
}
|
||
}
|
||
if wsum > 0.0 {
|
||
for (d, a) in out.iter_mut().zip(&acc) {
|
||
*d = a / wsum;
|
||
}
|
||
return true;
|
||
}
|
||
false
|
||
}
|
||
}
|
||
|
||
/// `Σv, Σv²` per channel (the first `out.len()`) of the `side × side` patch with top-left
|
||
/// `(x0, y0)`, summed row by row, pixel by pixel, as [`Solver::moments`] does.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn patch_moments(img: &[f32], w: usize, ch: usize, c8: usize, side: usize, x0: usize, y0: usize, out: &mut [[f32; 2]]) {
|
||
out.iter_mut().for_each(|v| *v = [0.0; 2]);
|
||
for y in y0..y0 + side {
|
||
let row = &img[(y * w + x0) * ch..(y * w + x0 + side) * ch];
|
||
for p in row.chunks_exact(ch) {
|
||
for (c, o) in out.iter_mut().enumerate().take(c8) {
|
||
let a = p[c];
|
||
o[0] += a;
|
||
o[1] += a * a;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The adapted distance from the moments `[Σt, Σt², Σs, Σs², Σts]` of each channel over `n`
|
||
/// samples: what is left of the target after the source is fitted to it, per sample.
|
||
#[inline]
|
||
fn adapted_cost(a: Adapt, nf: f32, m: &[[f32; 5]]) -> f32 {
|
||
let mut sum = 0.0f32;
|
||
for mc in m {
|
||
let (g, b) = fit(a, nf, mc[0], mc[1], mc[2], mc[3]);
|
||
// Σ (t − g s − b)², expanded.
|
||
sum += mc[1] + g * g * mc[3] + nf * b * b - 2.0 * g * mc[4] - 2.0 * b * mc[0] + 2.0 * g * b * mc[2];
|
||
}
|
||
sum.max(0.0) / (nf * m.len() as f32)
|
||
}
|
||
|
||
/// `Σ (t − s)²` over a target patch's known pixels (offsets `offs` from its centre, samples
|
||
/// `tv`, `CH` per pixel) against the source patch centred at pixel `base`, summed in that order;
|
||
/// `None` once the sum reaches `limit` (it only grows).
|
||
#[inline(always)]
|
||
fn known_sse<const CH: usize>(image: &[f32], offs: &[isize], tv: &[f32], base: isize, limit: f32) -> Option<f32> {
|
||
let mut e = 0.0f32;
|
||
for (o, t) in offs.iter().zip(tv.as_chunks::<CH>().0) {
|
||
let si = (base + o) as usize * CH;
|
||
let s = &image[si..si + CH];
|
||
for c in 0..CH {
|
||
let d = t[c] - s[c];
|
||
e += d * d;
|
||
}
|
||
if e >= limit {
|
||
return None;
|
||
}
|
||
}
|
||
Some(e)
|
||
}
|
||
|
||
/// [`known_sse`] of [`LANES`] sources at once, each against its own limit (summed in step: the
|
||
/// sums are independent, so the processor overlaps them).
|
||
#[inline(always)]
|
||
fn known_sse_lanes<const CH: usize>(image: &[f32], offs: &[isize], tv: &[f32], bases: [isize; LANES], limits: [f32; LANES]) -> [Option<f32>; LANES] {
|
||
let mut e = [0.0f32; LANES];
|
||
for (o, t) in offs.iter().zip(tv.as_chunks::<CH>().0) {
|
||
for l in 0..LANES {
|
||
let si = (bases[l] + o) as usize * CH;
|
||
let s = &image[si..si + CH];
|
||
for c in 0..CH {
|
||
let d = t[c] - s[c];
|
||
e[l] += d * d;
|
||
}
|
||
}
|
||
// Sums only grow: once all of them reached their limits, none can win.
|
||
if (0..LANES).all(|l| e[l] >= limits[l]) {
|
||
break;
|
||
}
|
||
}
|
||
std::array::from_fn(|l| (e[l] < limits[l]).then_some(e[l]))
|
||
}
|
||
|
||
/// Sources [`known_sse_lanes`] sums at once.
|
||
const LANES: usize = 4;
|
||
|
||
/// [`known_sse`] for any number of channels.
|
||
fn known_sse_any(image: &[f32], ch: usize, offs: &[isize], tv: &[f32], base: isize, limit: f32) -> Option<f32> {
|
||
let mut e = 0.0f32;
|
||
for (o, t) in offs.iter().zip(tv.chunks_exact(ch.max(1))) {
|
||
let si = (base + o) as usize * ch;
|
||
for (tc, sc) in t.iter().zip(&image[si..si + ch]) {
|
||
let d = tc - sc;
|
||
e += d * d;
|
||
}
|
||
if e >= limit {
|
||
return None;
|
||
}
|
||
}
|
||
Some(e)
|
||
}
|
||
|
||
/// The best source so far of [`onion_peel`]'s search.
|
||
#[derive(Clone, Copy)]
|
||
struct Best {
|
||
/// Error per sample, and the place in the sources.
|
||
v: f32,
|
||
j: usize,
|
||
/// Sums of squared errors from which `sum / n` is above `v`, and at least `v`.
|
||
worse: f32,
|
||
equal: f32,
|
||
}
|
||
|
||
/// The least sum `e ≥ 0` with `e / n > v` (`strict`) or `e / n ≥ v` in float arithmetic (the
|
||
/// quotient only grows with `e`); infinity for an infinite `v`.
|
||
fn least_sum(v: f32, n: f32, strict: bool) -> f32 {
|
||
let holds = |e: f32| if strict { e / n > v } else { e / n >= v };
|
||
if !v.is_finite() || !n.is_finite() || n <= 0.0 {
|
||
return f32::INFINITY;
|
||
}
|
||
let mut e = (v * n).max(0.0);
|
||
while e > 0.0 && holds(e.next_down()) {
|
||
e = e.next_down();
|
||
}
|
||
while !holds(e) && e < f32::INFINITY {
|
||
e = e.next_up();
|
||
}
|
||
e
|
||
}
|
||
|
||
/// Fill the hole from its edge inwards, one ring at a time: each pixel takes the centre of the
|
||
/// source patch that best matches what is already known around it (the first in `sources` of
|
||
/// those that match best). `hb` bounds the hole.
|
||
///
|
||
/// The search is exhaustive, but most sources are given up early (#2771, #2805): the target's
|
||
/// known pixels are gathered once, the search starts from where the pixel's filled neighbours
|
||
/// took their sources (or every 16th source at the hole's edge), and a source's error stops being
|
||
/// added up once it can only lose (it only grows). So it finds the same source as a plain scan in
|
||
/// order, sooner.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn onion_peel(
|
||
w: usize,
|
||
h: usize,
|
||
ch: usize,
|
||
r: i32,
|
||
img: &mut [f32],
|
||
px: &[Px],
|
||
hb: Bounds,
|
||
sources: &[(i32, i32)],
|
||
ctl: &photocraft_raster::Interrupt,
|
||
) -> Result<(), photocraft_raster::Cancelled> {
|
||
const NONE: u32 = u32::MAX;
|
||
let mut known: Vec<bool> = px.iter().map(|k| matches!(k, Px::Known | Px::Source)).collect();
|
||
// Each source centre's place in `sources`, and the source each filled pixel took.
|
||
let mut index = vec![NONE; w * h];
|
||
for (j, &(x, y)) in sources.iter().enumerate() {
|
||
index[y as usize * w + x as usize] = j as u32;
|
||
}
|
||
let mut took = vec![NONE; w * h];
|
||
let neighbours = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)];
|
||
loop {
|
||
ctl.check()?;
|
||
let mut front: Vec<usize> = Vec::new();
|
||
for y in hb.y0..hb.y1 {
|
||
for x in hb.x0..hb.x1 {
|
||
let i = y * w + x;
|
||
if known[i] || px[i] != Px::Hole {
|
||
continue;
|
||
}
|
||
let (x, y) = (x as i32, y as i32);
|
||
let edge = (-1..=1).any(|dy: i32| {
|
||
(-1..=1).any(|dx: i32| {
|
||
let (nx, ny) = (x + dx, y + dy);
|
||
nx >= 0 && ny >= 0 && (nx as usize) < w && (ny as usize) < h && known[ny as usize * w + nx as usize]
|
||
})
|
||
});
|
||
if edge {
|
||
front.push(i);
|
||
}
|
||
}
|
||
}
|
||
if front.is_empty() {
|
||
break;
|
||
}
|
||
let (known_now, took_now) = (&known, &took);
|
||
let image = &*img;
|
||
let best = |&i: &usize| -> (Vec<f32>, u32) {
|
||
let (x, y) = ((i % w) as i32, (i / w) as i32);
|
||
// The known pixels of the target patch, in the plain scan's order: each one's offset
|
||
// from the patch centre (in pixels) and its samples (gathered once, #2805).
|
||
let side = (2 * r + 1) as usize;
|
||
let mut offs: Vec<isize> = Vec::with_capacity(side * side);
|
||
let mut tv: Vec<f32> = Vec::with_capacity(side * side * ch);
|
||
for dy in -r..=r {
|
||
for dx in -r..=r {
|
||
let (tx, ty) = (x + dx, y + dy);
|
||
if tx >= 0 && ty >= 0 && (tx as usize) < w && (ty as usize) < h && known_now[ty as usize * w + tx as usize] {
|
||
let ti = (ty as usize * w + tx as usize) * ch;
|
||
offs.push(dy as isize * w as isize + dx as isize);
|
||
tv.extend_from_slice(&image[ti..ti + ch]);
|
||
}
|
||
}
|
||
}
|
||
// The best so far: its error per sample, its place in `sources` (the first of equals
|
||
// wins, as in a scan in order), and the sums of squares at which a source surely loses
|
||
// to it (`e / n` above the best) or ties it (at least the best).
|
||
let mut best = Best { v: f32::INFINITY, j: usize::MAX, worse: f32::INFINITY, equal: f32::INFINITY };
|
||
if !offs.is_empty() {
|
||
let nf = offs.len() as f32;
|
||
let base_of = |j: usize| sources.get(j).map_or(0, |&(sx, sy)| sy as isize * w as isize + sx as isize);
|
||
// The sum at which source `j` can't win: a tie loses to an earlier source.
|
||
let limit_of = |j: usize, best: &Best| if j > best.j { best.equal } else { best.worse };
|
||
// Source `j`'s whole sum `e` (it did not reach its limit): does it win?
|
||
let adopt = |j: usize, e: f32, best: &mut Best| {
|
||
let v = e / nf;
|
||
if v < f32::INFINITY && (v < best.v || (v == best.v && j < best.j)) {
|
||
*best = Best { v, j, worse: least_sum(v, nf, true), equal: least_sum(v, nf, false) };
|
||
}
|
||
};
|
||
let consider = |j: usize, best: &mut Best| {
|
||
if j >= sources.len() {
|
||
return;
|
||
}
|
||
let (base, limit) = (base_of(j), limit_of(j, best));
|
||
let e = match ch {
|
||
1 => known_sse::<1>(image, &offs, &tv, base, limit),
|
||
3 => known_sse::<3>(image, &offs, &tv, base, limit),
|
||
4 => known_sse::<4>(image, &offs, &tv, base, limit),
|
||
_ => known_sse_any(image, ch, &offs, &tv, base, limit),
|
||
};
|
||
if let Some(e) = e {
|
||
adopt(j, e, best);
|
||
}
|
||
};
|
||
// A close bound early: where the filled neighbours' sources point (shifted as in
|
||
// PatchMatch's propagation), or at the hole's edge, where none is filled yet,
|
||
// every 16th source.
|
||
for (dx, dy) in neighbours {
|
||
let (qx, qy) = (x + dx, y + dy);
|
||
if qx < 0 || qy < 0 || qx as usize >= w || qy as usize >= h {
|
||
continue;
|
||
}
|
||
let Some(&(sx, sy)) = sources.get(took_now[qy as usize * w + qx as usize] as usize) else { continue };
|
||
let (cx, cy) = (sx - dx, sy - dy);
|
||
if cx >= 0 && cy >= 0 && (cx as usize) < w && (cy as usize) < h && index[cy as usize * w + cx as usize] != NONE {
|
||
consider(index[cy as usize * w + cx as usize] as usize, &mut best);
|
||
}
|
||
}
|
||
if best.j == usize::MAX {
|
||
for j in (0..sources.len()).step_by(16) {
|
||
consider(j, &mut best);
|
||
}
|
||
}
|
||
// The scan in order, `LANES` sources at a time (their sums are independent, so they
|
||
// overlap), each against its limit from the best before them: the best only gets
|
||
// better, so a source given up would have lost to it anyway.
|
||
let mut j = 0;
|
||
while j + LANES <= sources.len() {
|
||
if j.is_multiple_of(4096) && j > 0 && ctl.cancelled() {
|
||
break;
|
||
}
|
||
let bases: [isize; LANES] = std::array::from_fn(|l| base_of(j + l));
|
||
let limits: [f32; LANES] = std::array::from_fn(|l| limit_of(j + l, &best));
|
||
let sums = match ch {
|
||
1 => known_sse_lanes::<1>(image, &offs, &tv, bases, limits),
|
||
3 => known_sse_lanes::<3>(image, &offs, &tv, bases, limits),
|
||
4 => known_sse_lanes::<4>(image, &offs, &tv, bases, limits),
|
||
_ => std::array::from_fn(|l| known_sse_any(image, ch, &offs, &tv, bases[l], limits[l])),
|
||
};
|
||
for (l, e) in sums.into_iter().enumerate() {
|
||
if let Some(e) = e {
|
||
adopt(j + l, e, &mut best);
|
||
}
|
||
}
|
||
j += LANES;
|
||
}
|
||
for j in j..sources.len() {
|
||
consider(j, &mut best);
|
||
}
|
||
}
|
||
let (sx, sy) = sources.get(best.j).copied().unwrap_or((0, 0));
|
||
let si = (sy as usize * w + sx as usize) * ch;
|
||
(image[si..si + ch].to_vec(), if best.j == usize::MAX { NONE } else { best.j as u32 })
|
||
};
|
||
let vals: Vec<(Vec<f32>, u32)> = par::map(front.len(), front.len() * sources.len() >= 4096, |k| best(&front[k]));
|
||
ctl.check()?;
|
||
for (&i, (v, j)) in front.iter().zip(vals) {
|
||
img[i * ch..(i + 1) * ch].copy_from_slice(&v);
|
||
known[i] = true;
|
||
took[i] = j;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Content-aware completion of `hole` (see module docs), copying from any pixel outside it. Returns
|
||
/// `None` when there is no fully-known patch to copy from (the caller can fall back to
|
||
/// [`membrane_fill`]).
|
||
pub fn complete(w: usize, h: usize, ch: usize, img: &[f32], hole: &[bool], p: &CompleteParams) -> Option<Vec<f32>> {
|
||
// Never cancelled, so never `Err`.
|
||
complete_with(w, h, ch, img, hole, p, &photocraft_raster::Interrupt::NONE).unwrap_or(None)
|
||
}
|
||
|
||
/// [`complete`] that can be cancelled (checked per EM iteration, PatchMatch pass and row band)
|
||
/// and reports progress (by pyramid level). `Err(Cancelled)` when `ctl` was cancelled.
|
||
pub fn complete_with(
|
||
w: usize,
|
||
h: usize,
|
||
ch: usize,
|
||
img: &[f32],
|
||
hole: &[bool],
|
||
p: &CompleteParams,
|
||
ctl: &photocraft_raster::Interrupt,
|
||
) -> Result<Option<Vec<f32>>, photocraft_raster::Cancelled> {
|
||
assert_eq!(hole.len(), w * h);
|
||
let px: Vec<Px> = hole.iter().map(|h| if *h { Px::Hole } else { Px::Source }).collect();
|
||
complete_masked(w, h, ch, img, &px, p, ctl)
|
||
}
|
||
|
||
/// [`complete_with`] with every pixel's role given ([`Px`]): the hole, the context, what may be
|
||
/// copied, and what isn't image at all.
|
||
pub fn complete_masked(
|
||
w: usize,
|
||
h: usize,
|
||
ch: usize,
|
||
img: &[f32],
|
||
px: &[Px],
|
||
p: &CompleteParams,
|
||
ctl: &photocraft_raster::Interrupt,
|
||
) -> Result<Option<Vec<f32>>, photocraft_raster::Cancelled> {
|
||
Ok(complete_cow(w, h, ch, Cow::Borrowed(img), px, p, ctl)?.ok())
|
||
}
|
||
|
||
/// [`complete_masked`] that fills `img` itself instead of a copy of it (only the hole changes):
|
||
/// `Err(img)`, untouched, when there is nothing to copy from.
|
||
pub fn complete_masked_owned(
|
||
w: usize,
|
||
h: usize,
|
||
ch: usize,
|
||
img: Vec<f32>,
|
||
px: &[Px],
|
||
p: &CompleteParams,
|
||
ctl: &photocraft_raster::Interrupt,
|
||
) -> Result<Result<Vec<f32>, Vec<f32>>, photocraft_raster::Cancelled> {
|
||
Ok(complete_cow(w, h, ch, Cow::Owned(img), px, p, ctl)?.map_err(Cow::into_owned))
|
||
}
|
||
|
||
/// The completion, on the caller's pixels or a copy of them: `Err` gives them back when there is
|
||
/// nothing to copy from.
|
||
fn complete_cow<'a>(
|
||
w: usize,
|
||
h: usize,
|
||
ch: usize,
|
||
img: Cow<'a, [f32]>,
|
||
px: &'a [Px],
|
||
p: &CompleteParams,
|
||
ctl: &photocraft_raster::Interrupt,
|
||
) -> Result<Result<Vec<f32>, Cow<'a, [f32]>>, photocraft_raster::Cancelled> {
|
||
assert_eq!(img.len(), w * h * ch);
|
||
assert_eq!(px.len(), w * h);
|
||
let Some(hb0) = hole_bounds(w, h, px) else { return Ok(Ok(img.into_owned())) };
|
||
if ch == 0 {
|
||
return Ok(Ok(Vec::new()));
|
||
}
|
||
let r = p.patch_radius.max(1);
|
||
let psz = 2 * r + 1;
|
||
// Pyramid: shrink until the hole is about four patches across (a deeper pyramid decides the
|
||
// structure where the texture has averaged out, and fills flat). A thin hole stops while it
|
||
// is still about two patches thick: through it, not along it, is what counts (#1366).
|
||
let mut levels = vec![Level { w, h, img, px: Cow::Borrowed(px) }];
|
||
let mut boxes = vec![hb0];
|
||
let mut ext = hb0.width().max(hb0.height());
|
||
let thickness = hole_thickness(w, hb0, px);
|
||
let thin = thickness.saturating_mul(THIN_RATIO) <= ext;
|
||
if thin {
|
||
ext = thickness;
|
||
}
|
||
let mut exts = vec![ext];
|
||
while let Some(l) = levels.last() {
|
||
ctl.check()?;
|
||
if ext <= if thin { 2 * psz } else { 4 * psz } || l.w / 2 < 3 * psz || l.h / 2 < 3 * psz {
|
||
break;
|
||
}
|
||
let d = downsample(l, ch);
|
||
levels.push(d);
|
||
boxes.push(boxes[boxes.len() - 1].half());
|
||
ext = ext.div_ceil(2);
|
||
exts.push(ext);
|
||
}
|
||
let nlev = levels.len();
|
||
// The coarser level's result.
|
||
let mut prev: Option<Coarser> = None;
|
||
let total_px: usize = levels.iter().map(|l| l.w * l.h).sum();
|
||
let mut done_px = 0usize;
|
||
let ri = r as i32;
|
||
for li in (0..nlev).rev() {
|
||
ctl.check()?;
|
||
let Level { w: lw, h: lh, img: limg, px: lpx } = std::mem::replace(&mut levels[li], Level::EMPTY);
|
||
let hb = boxes[li];
|
||
let tb = hb.grow(r, lw, lh);
|
||
let valid = valid_sources(lw, lh, r, &lpx, ctl).ok_or(photocraft_raster::Cancelled)?;
|
||
let sources = Sources::new(&valid, lw);
|
||
// Nothing to copy from at the finest level: the caller falls back (and gets its pixels
|
||
// back as they were).
|
||
if li == 0 && sources.len() == 0 {
|
||
return Ok(Err(limg));
|
||
}
|
||
// The level's own pixels to fill (at level 0 the caller's, or a copy made in parallel).
|
||
let mut limg = match limg {
|
||
Cow::Owned(v) => v,
|
||
Cow::Borrowed(b) => {
|
||
const CHUNK: usize = 1 << 16;
|
||
let mut v = vec![0.0f32; b.len()];
|
||
par::chunks_mut(&mut v, CHUNK, true, |i, d| d.copy_from_slice(&b[i * CHUNK..i * CHUNK + d.len()]));
|
||
v
|
||
}
|
||
};
|
||
let targets = targets_around(lw, r, &lpx, hb, tb);
|
||
ctl.check()?;
|
||
// Initialise the hole: at the coarsest level peeled from its edge inwards (or a membrane
|
||
// where nothing can be copied), else the coarser level's pixels, refined below by a vote of
|
||
// its scaled-up matches.
|
||
let coarser = prev.take();
|
||
match &coarser {
|
||
Some(c) => {
|
||
let (pw, ph) = (c.w, c.h);
|
||
for y in hb.y0..hb.y1 {
|
||
for x in hb.x0..hb.x1 {
|
||
if lpx[y * lw + x] == Px::Hole {
|
||
let (sx, sy) = ((x / 2).min(pw - 1), (y / 2).min(ph - 1));
|
||
let (d, s) = ((y * lw + x) * ch, (sy * pw + sx) * ch);
|
||
limg[d..d + ch].copy_from_slice(&c.img[s..s + ch]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// A thin hole, when peeling it would take too long, takes the membrane below: a few
|
||
// patches thick here, a membrane across it is its surroundings.
|
||
None if sources.len() > 0 && (!thin || peel_affordable(lw, &lpx, hb, sources.len())) => {
|
||
onion_peel(lw, lh, ch, ri, &mut limg, &lpx, hb, &sources.all(&valid, lw), ctl)?
|
||
}
|
||
None => {
|
||
let mut unknown: Vec<bool> = lpx.iter().map(|k| matches!(k, Px::Hole | Px::Void)).collect();
|
||
// Across a thin hole the membrane starts a patch radius out, past the
|
||
// anti-aliased fringe or halo that a selection drawn close to the object leaves.
|
||
if thin {
|
||
let hole: Vec<bool> = lpx.iter().map(|k| *k == Px::Hole).collect();
|
||
for (u, d) in unknown.iter_mut().zip(crate::selection::edt(&hole, lw, lh)) {
|
||
*u |= d <= r as f32;
|
||
}
|
||
}
|
||
let m = membrane_fill(lw, lh, ch, &limg, &unknown);
|
||
for (i, k) in lpx.iter().enumerate() {
|
||
if *k == Px::Hole {
|
||
limg[i * ch..(i + 1) * ch].copy_from_slice(&m[i * ch..(i + 1) * ch]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if sources.len() == 0 {
|
||
prev = Some(Coarser { w: lw, h: lh, img: limg, field: Vec::new(), tbox: tb });
|
||
continue;
|
||
}
|
||
let refine_only = li + 1 < nlev && exts[li] > p.refine_extent;
|
||
let n_targets = targets.iter().filter(|t| **t).count();
|
||
// Source moments are worth keeping when there are few sources for the matches tried.
|
||
let keep_source_moments = lw * lh <= SOURCE_MOMENTS_MAX && sources.len() <= n_targets.saturating_mul(if refine_only { 4 } else { 64 });
|
||
let mut solver = Solver {
|
||
w: lw,
|
||
h: lh,
|
||
ch,
|
||
r: ri,
|
||
img: &mut limg,
|
||
px: &lpx,
|
||
valid,
|
||
targets,
|
||
hole_box: hb,
|
||
tbox: tb,
|
||
full: Vec::new(),
|
||
tmom: Vec::new(),
|
||
smom: Vec::new(),
|
||
adapt: p.adapt,
|
||
ctl: *ctl,
|
||
};
|
||
solver.prepare(keep_source_moments);
|
||
ctl.check()?;
|
||
// NNF init: the coarser level's matches scaled up where they land on a valid source,
|
||
// random otherwise.
|
||
let mut rng = Rng::new(p.seed ^ (li as u64) << 20);
|
||
let tw = tb.width();
|
||
let mut nnf = vec![(0i32, 0i32); tb.area()];
|
||
let mut upsampled = false;
|
||
for y in tb.y0..tb.y1 {
|
||
if y % 32 == 0 {
|
||
ctl.check()?;
|
||
}
|
||
for x in tb.x0..tb.x1 {
|
||
let k = (y - tb.y0) * tw + (x - tb.x0);
|
||
if !solver.targets[k] {
|
||
continue;
|
||
}
|
||
let mut cand = None;
|
||
if let Some(c) = &coarser
|
||
&& !c.field.is_empty()
|
||
{
|
||
let (cx, cy) = ((x / 2).min(c.w - 1), (y / 2).min(c.h - 1));
|
||
let (sx, sy) = c.field_at(cx, cy);
|
||
if sx != i32::MIN {
|
||
// The same displacement, twice as long.
|
||
let c = (x as i32 + 2 * (sx - cx as i32), y as i32 + 2 * (sy - cy as i32));
|
||
if solver.is_valid(c.0, c.1) {
|
||
cand = Some(c);
|
||
}
|
||
}
|
||
}
|
||
upsampled |= cand.is_some();
|
||
nnf[k] = cand.unwrap_or_else(|| sources.nth((rng.next() % sources.len() as u64) as usize, &solver.valid, lw));
|
||
}
|
||
}
|
||
// The coarser level has given all it had.
|
||
drop(coarser);
|
||
let mut cost = vec![f32::INFINITY; tb.area()];
|
||
// A finer level starts from the coarse matches voted with its own (sharp) pixels.
|
||
if upsampled {
|
||
solver.costs(&nnf, &mut cost)?;
|
||
solver.vote(&nnf, &cost);
|
||
}
|
||
let em = if li + 1 == nlev {
|
||
p.em_iters
|
||
} else if refine_only {
|
||
1
|
||
} else {
|
||
(p.em_iters >> (nlev - 1 - li)).max(p.min_em_iters)
|
||
};
|
||
let pm_iters = if refine_only { 1 } else { p.pm_iters };
|
||
let full_search = (lw.max(lh)) as i32;
|
||
for it in 0..em {
|
||
// Costs change as hole pixels are re-estimated: refresh before each PatchMatch.
|
||
solver.costs(&nnf, &mut cost)?;
|
||
// Wide random search on the coarsest level and on each level's first iteration,
|
||
// local refinement afterwards.
|
||
let radius = if refine_only {
|
||
2
|
||
} else if li + 1 == nlev || it == 0 {
|
||
full_search
|
||
} else {
|
||
(4 * psz as i32).min(full_search)
|
||
};
|
||
for pass in 0..pm_iters {
|
||
ctl.check()?;
|
||
solver.patchmatch(&mut nnf, &mut cost, pass + it * pm_iters, p.seed.wrapping_add((li * 1000 + it) as u64), radius);
|
||
}
|
||
ctl.check()?;
|
||
solver.vote(&nnf, &cost);
|
||
ctl.check()?;
|
||
}
|
||
// A vote that saw the cancellation stopped part-way: never hand its pixels on.
|
||
ctl.check()?;
|
||
// Keep the match field (unmatched cells marked) for the next level (none after the last).
|
||
let field: Vec<(i32, i32)> =
|
||
if li == 0 { Vec::new() } else { nnf.iter().zip(&solver.targets).map(|(n, t)| if *t { *n } else { (i32::MIN, i32::MIN) }).collect() };
|
||
// Finer levels cost ~4× the previous one: weight progress by pixel count.
|
||
done_px += lw * lh;
|
||
ctl.progress(done_px as f32 / total_px.max(1) as f32);
|
||
prev = Some(Coarser { w: lw, h: lh, img: limg, field, tbox: tb });
|
||
}
|
||
Ok(prev.map(|c| c.img).ok_or(Cow::Owned(Vec::new())))
|
||
}
|
||
|
||
/// Proximity match: the displacement `(dx, dy)` (within `max_radius`) whose surroundings best match the
|
||
/// ring of `ring` pixels around the hole, with the displaced hole fully inside the grid and outside the
|
||
/// hole. `None` if no displacement fits.
|
||
pub fn best_offset(w: usize, h: usize, ch: usize, img: &[f32], hole: &[bool], ring: usize, max_radius: i32) -> Option<(i32, i32)> {
|
||
let (bx0, by0, bx1, by1) = hole_bbox(w, h, hole)?;
|
||
// No valid translation can move farther than the source image's extent.
|
||
// Bound arbitrary public API inputs before constructing i32 search ranges:
|
||
// usize -> i32 truncation and signed increments otherwise overflow or hang.
|
||
let extent = w.max(h).min((i32::MAX / 4) as usize) as i32;
|
||
let max_radius = max_radius.max(0).min(extent);
|
||
if max_radius == 0 {
|
||
return None;
|
||
}
|
||
let ring = ring.max(1).min(extent as usize) as i32;
|
||
// Every set cell lies in the hole bounds. Counts outside that rectangle are zero,
|
||
// so source rejection needs a table for the hole, not for the whole photo.
|
||
let hs = HoleIntegral::new(w, hole, (bx0, by0, bx1, by1))?;
|
||
// Sample points: the hole's ring (dilated minus hole), subsampled for speed.
|
||
let mut pts = Vec::new();
|
||
let (x0, y0) = (bx0 as i32 - ring, by0 as i32 - ring);
|
||
let (x1, y1) = (bx1 as i32 + ring, by1 as i32 + ring);
|
||
for y in y0.max(0)..y1.min(h as i32) {
|
||
for x in x0.max(0)..x1.min(w as i32) {
|
||
if hole[y as usize * w + x as usize] {
|
||
continue;
|
||
}
|
||
if hs.count(x - ring, y - ring, x + ring + 1, y + ring + 1) > 0 {
|
||
pts.push((x, y));
|
||
}
|
||
}
|
||
}
|
||
let stride = (pts.len() / 2000).max(1);
|
||
let pts: Vec<(i32, i32)> = pts.into_iter().step_by(stride).collect();
|
||
let eval = |dx: i32, dy: i32, cutoff: f32| -> Option<f32> {
|
||
let (hx0, hy0, hx1, hy1) = (x0 + dx, y0 + dy, x1 + dx, y1 + dy);
|
||
let inside = hx0 >= 0 && hy0 >= 0 && hx1 <= w as i32 && hy1 <= h as i32;
|
||
if (dx, dy) == (0, 0) || !inside || hs.count(hx0, hy0, hx1, hy1) != 0 {
|
||
return None;
|
||
}
|
||
let mut e = 0.0f32;
|
||
let n = pts.len().max(1) as f32;
|
||
let penalty = 1.0 + 1e-3 * ((dx as f32).hypot(dy as f32) / max_radius as f32);
|
||
for (i, &(x, y)) in pts.iter().enumerate() {
|
||
let (a, b) = ((y as usize * w + x as usize) * ch, ((y + dy) as usize * w + (x + dx) as usize) * ch);
|
||
for c in 0..ch {
|
||
let d = img[a + c] - img[b + c];
|
||
e += d * d;
|
||
}
|
||
// SSD terms are nonnegative. The partial score is a lower bound on the
|
||
// final score; use the same division and penalty to keep float rounding
|
||
// and strict tie ordering unchanged. Check in batches to keep this cheap.
|
||
if i % 32 == 31 && cutoff.is_finite() && e / n * penalty >= cutoff {
|
||
return None;
|
||
}
|
||
}
|
||
// Mild preference for nearer sources on ties.
|
||
Some(e / n * penalty)
|
||
};
|
||
// Coarse grid over the search window, then a full-resolution refinement around the winner.
|
||
let step = (max_radius / 24).max(1);
|
||
let mut best: Option<((i32, i32), f32)> = None;
|
||
let consider = |dx: i32, dy: i32, best: &mut Option<((i32, i32), f32)>| {
|
||
if let Some(e) = eval(dx, dy, best.map_or(f32::INFINITY, |(_, error)| error))
|
||
&& best.is_none_or(|(_, be)| e < be)
|
||
{
|
||
*best = Some(((dx, dy), e));
|
||
}
|
||
};
|
||
let mut dy = -max_radius;
|
||
while dy <= max_radius {
|
||
let mut dx = -max_radius;
|
||
while dx <= max_radius {
|
||
consider(dx, dy, &mut best);
|
||
dx += step;
|
||
}
|
||
dy += step;
|
||
}
|
||
if step > 1
|
||
&& let Some(((bx, by), _)) = best
|
||
{
|
||
for dy in (by - step)..=(by + step) {
|
||
for dx in (bx - step)..=(bx + step) {
|
||
consider(dx, dy, &mut best);
|
||
}
|
||
}
|
||
}
|
||
best.map(|b| b.0)
|
||
}
|
||
|
||
/// Create texture: fill the hole with blocks of surrounding texture (see module docs).
|
||
pub fn synthesize(w: usize, h: usize, ch: usize, img: &[f32], hole: &[bool], block: usize, seed: u64) -> Vec<f32> {
|
||
let mut out = img.to_vec();
|
||
let Some((bx0, by0, bx1, by1)) = hole_bbox(w, h, hole) else { return out };
|
||
let b = block.max(4) as i32;
|
||
let ov = (b / 3).max(1);
|
||
let hs = integral(w, h, hole);
|
||
// Candidate block origins: whole block (with overlap) inside the grid and outside the hole.
|
||
let mut cands = Vec::new();
|
||
for y in ov..(h as i32 - b - ov) {
|
||
for x in ov..(w as i32 - b - ov) {
|
||
if window_count(&hs, w, h, x - ov, y - ov, x + b + ov, y + b + ov) == 0 {
|
||
cands.push((x, y));
|
||
}
|
||
}
|
||
}
|
||
if cands.is_empty() {
|
||
return membrane_fill(w, h, ch, img, hole);
|
||
}
|
||
let mut filled: Vec<bool> = hole.iter().map(|x| !x).collect();
|
||
let mut rng = Rng::new(seed);
|
||
let mut by = by0 as i32;
|
||
while by < by1 as i32 {
|
||
let mut bx = bx0 as i32;
|
||
while bx < bx1 as i32 {
|
||
if window_count(&hs, w, h, bx, by, bx + b, by + b) > 0 {
|
||
// Pick the candidate whose block best agrees with already-known pixels around/inside.
|
||
let mut best = (cands[0], f32::INFINITY);
|
||
for _ in 0..48 {
|
||
let c = cands[(rng.next() % cands.len() as u64) as usize];
|
||
let (mut e, mut n) = (0.0f32, 0u32);
|
||
for y in (by - ov)..(by + b + ov) {
|
||
for x in (bx - ov)..(bx + b + ov) {
|
||
if x < 0 || y < 0 || x >= w as i32 || y >= h as i32 || !filled[y as usize * w + x as usize] {
|
||
continue;
|
||
}
|
||
let (a, s) = ((y as usize * w + x as usize) * ch, ((y - by + c.1) as usize * w + (x - bx + c.0) as usize) * ch);
|
||
for k in 0..ch {
|
||
let d = out[a + k] - img[s + k];
|
||
e += d * d;
|
||
}
|
||
n += 1;
|
||
}
|
||
}
|
||
let e = if n == 0 { 0.0 } else { e / n as f32 };
|
||
if e < best.1 {
|
||
best = (c, e);
|
||
}
|
||
}
|
||
let c = best.0;
|
||
let (x0, y0) = ((bx - ov).max(0), (by - ov).max(0));
|
||
let (x1, y1) = ((bx + b + ov).min(w as i32), (by + b + ov).min(h as i32));
|
||
let src = |x: i32, y: i32| ((y - by + c.1) as usize * w + (x - bx + c.0) as usize) * ch;
|
||
// Squared difference between what is there and the block, where both exist.
|
||
let err = |x: i32, y: i32| {
|
||
let i = y as usize * w + x as usize;
|
||
if !(hole[i] && filled[i]) {
|
||
return 0.0;
|
||
}
|
||
let (a, s) = (i * ch, src(x, y));
|
||
(0..ch).map(|q| (out[a + q] - img[s + q]).powi(2)).sum::<f32>()
|
||
};
|
||
// Minimum-error boundary cuts through the overlaps with the blocks to the left and
|
||
// above: the block is copied right of and below them, untouched pixels are kept
|
||
// on the other side. Copying (rather than averaging) keeps the texture's grain.
|
||
let left = min_cut((x0..(bx + ov).min(x1)).len(), (y0..y1).len(), |u, v| err(x0 + u as i32, y0 + v as i32));
|
||
let top = min_cut((y0..(by + ov).min(y1)).len(), (x0..x1).len(), |u, v| err(x0 + v as i32, y0 + u as i32));
|
||
for y in y0..y1 {
|
||
for x in x0..x1 {
|
||
let i = y as usize * w + x as usize;
|
||
if !hole[i] {
|
||
continue;
|
||
}
|
||
let (u, v) = ((x - x0) as usize, (y - y0) as usize);
|
||
let keep = filled[i] && (left.get(v).is_some_and(|s| u < *s) || top.get(u).is_some_and(|s| v < *s));
|
||
if !keep {
|
||
let (a, s) = (i * ch, src(x, y));
|
||
out[a..a + ch].copy_from_slice(&img[s..s + ch]);
|
||
}
|
||
filled[i] = true;
|
||
}
|
||
}
|
||
}
|
||
bx += b;
|
||
}
|
||
by += b;
|
||
}
|
||
out
|
||
}
|
||
|
||
/// The minimum-error cut through a band `across` wide and `along` long (Efros & Freeman's
|
||
/// boundary cut, by dynamic programming): for each step along the band, the offset across it
|
||
/// where the cut runs (`err(across, along)` is the cost of a pixel). Pixels before the cut keep
|
||
/// what is there. Empty when there is no cost anywhere (nothing to cut through).
|
||
fn min_cut(across: usize, along: usize, err: impl Fn(usize, usize) -> f32) -> Vec<usize> {
|
||
if across == 0 || along == 0 {
|
||
return Vec::new();
|
||
}
|
||
// Cumulative cost, step by step along the band; the cut may move by one pixel per step.
|
||
let mut acc: Vec<f32> = (0..along).flat_map(|v| (0..across).map(move |u| (u, v))).map(|(u, v)| err(u, v)).collect();
|
||
if acc.iter().all(|x| *x == 0.0) {
|
||
return Vec::new();
|
||
}
|
||
for v in 1..along {
|
||
for u in 0..across {
|
||
let lo = u.saturating_sub(1);
|
||
let hi = (u + 1).min(across - 1);
|
||
let m = (lo..=hi).map(|k| acc[(v - 1) * across + k]).fold(f32::INFINITY, f32::min);
|
||
acc[v * across + u] += m;
|
||
}
|
||
}
|
||
let argmin = |v: usize, lo: usize, hi: usize| (lo..=hi).min_by(|a, b| acc[v * across + a].total_cmp(&acc[v * across + b])).unwrap_or(lo);
|
||
let mut cut = vec![0; along];
|
||
let mut u = argmin(along - 1, 0, across - 1);
|
||
for v in (0..along).rev() {
|
||
if v + 1 < along {
|
||
u = argmin(v, u.saturating_sub(1), (u + 1).min(across - 1));
|
||
}
|
||
cut[v] = u;
|
||
}
|
||
cut
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// The mean is matched first: on a nearly flat bright patch with a noisy deviation ratio the
|
||
/// gain stays where the bias can still reach the target's mean.
|
||
#[test]
|
||
fn fit_matches_the_mean_before_the_contrast() {
|
||
let a = Adapt { gain: (0.9, 1.1), bias: 0.04 };
|
||
// Target mean 0.70, source mean 0.70; deviations 0.01 vs 0.004 (ratio 2.5).
|
||
let n = 25.0;
|
||
let (mt, ms, sdt, sds) = (0.70f32, 0.70f32, 0.01f32, 0.004f32);
|
||
let (g, b) = fit(a, n, mt * n, (sdt * sdt + mt * mt) * n, ms * n, (sds * sds + ms * ms) * n);
|
||
assert!((g * ms + b - mt).abs() < 1e-5, "mean {} vs {mt} (g {g}, b {b})", g * ms + b);
|
||
// Where the mean can't be reached anyway, the limits hold as before.
|
||
let (g, b) = fit(a, n, 0.2 * n, 0.04 * n, 0.9 * n, 0.81 * n);
|
||
assert!((a.gain.0..=a.gain.1).contains(&g) && b.abs() <= a.bias + 1e-6);
|
||
}
|
||
|
||
/// A zero-width image has no pixels however tall it is claimed to be: no per-row table.
|
||
#[test]
|
||
fn zero_width_images_complete_without_sizing_by_height() {
|
||
let p = CompleteParams::default();
|
||
assert_eq!(complete(0, 1 << 40, 3, &[], &[], &p), Some(Vec::new()));
|
||
assert_eq!(complete(1 << 40, 0, 3, &[], &[], &p), Some(Vec::new()));
|
||
}
|
||
|
||
/// Smooth gradient plus deterministic fine noise.
|
||
fn texture(w: usize, h: usize) -> Vec<f32> {
|
||
let mut rng = Rng::new(7);
|
||
(0..w * h)
|
||
.flat_map(|i| {
|
||
let (x, y) = ((i % w) as f32, (i / w) as f32);
|
||
let n = (rng.next() % 1000) as f32 / 1000.0 * 0.04 - 0.02;
|
||
[0.3 + 0.004 * x + n, 0.5 + 0.002 * y + n, 0.4 + n]
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn disc(w: usize, h: usize, cx: f32, cy: f32, r: f32) -> Vec<bool> {
|
||
(0..w * h).map(|i| ((i % w) as f32 - cx).hypot((i / w) as f32 - cy) < r).collect()
|
||
}
|
||
|
||
fn hole_rmse(a: &[f32], b: &[f32], hole: &[bool], ch: usize) -> f32 {
|
||
let (mut e, mut n) = (0.0f32, 0);
|
||
for (i, hh) in hole.iter().enumerate() {
|
||
if *hh {
|
||
for c in 0..ch {
|
||
e += (a[i * ch + c] - b[i * ch + c]).powi(2);
|
||
n += 1;
|
||
}
|
||
}
|
||
}
|
||
(e / n as f32).sqrt()
|
||
}
|
||
|
||
#[test]
|
||
fn completion_fills_blemish_from_texture() {
|
||
let (w, h, ch) = (96, 96, 3);
|
||
let clean = texture(w, h);
|
||
let hole = disc(w, h, 48.0, 48.0, 9.0);
|
||
let mut dirty = clean.clone();
|
||
for (i, hh) in hole.iter().enumerate() {
|
||
if *hh {
|
||
dirty[i * ch..i * ch + ch].copy_from_slice(&[0.02, 0.02, 0.02]);
|
||
}
|
||
}
|
||
let out = complete(w, h, ch, &dirty, &hole, &CompleteParams::default()).unwrap();
|
||
let before = hole_rmse(&dirty, &clean, &hole, ch);
|
||
let after = hole_rmse(&out, &clean, &hole, ch);
|
||
assert!(after < 0.06 && after < before * 0.15, "rmse before {before} after {after}");
|
||
// Outside the hole untouched.
|
||
assert!(out.iter().zip(&dirty).zip(hole.iter().flat_map(|h| [*h; 3])).all(|((a, b), h)| h || a == b));
|
||
// Deterministic.
|
||
assert_eq!(out, complete(w, h, ch, &dirty, &hole, &CompleteParams::default()).unwrap());
|
||
}
|
||
|
||
#[test]
|
||
fn completion_continues_structure() {
|
||
// Vertical stripes: the filled hole should keep the stripes (not a blur).
|
||
let (w, h, ch) = (80, 80, 1);
|
||
let clean: Vec<f32> = (0..w * h).map(|i| if (i % w / 4) % 2 == 0 { 0.2 } else { 0.8 }).collect();
|
||
let hole = disc(w, h, 40.0, 40.0, 7.0);
|
||
let dirty: Vec<f32> = clean.iter().zip(&hole).map(|(c, h)| if *h { 0.5 } else { *c }).collect();
|
||
let out = complete(w, h, ch, &dirty, &hole, &CompleteParams::default()).unwrap();
|
||
let err = hole_rmse(&out, &clean, &hole, ch);
|
||
assert!(err < 0.15, "{err}");
|
||
}
|
||
|
||
#[test]
|
||
fn proximity_finds_matching_offset() {
|
||
let (w, h, ch) = (80, 60, 1);
|
||
// Periodic pattern with period 10 in x: a match is any multiple-of-10 displacement.
|
||
let img: Vec<f32> = (0..w * h).map(|i| ((i % w) as f32 * std::f32::consts::TAU / 10.0).sin() * 0.4 + 0.5).collect();
|
||
let hole = disc(w, h, 40.0, 30.0, 5.0);
|
||
let (dx, dy) = best_offset(w, h, ch, &img, &hole, 3, 20).unwrap();
|
||
assert_eq!(dx.rem_euclid(10), 0, "dx {dx} dy {dy}");
|
||
assert!((dx, dy) != (0, 0));
|
||
}
|
||
|
||
#[test]
|
||
fn proximity_search_clamps_hostile_radii_without_overflow_or_hanging() {
|
||
let (w, h, ch) = (24, 20, 1);
|
||
let img: Vec<f32> = (0..w * h).map(|i| (i % w) as f32 / w as f32).collect();
|
||
let hole = disc(w, h, 12.0, 10.0, 2.0);
|
||
// User-supplied values used to overflow i32 loops, cast a huge usize
|
||
// ring into a negative number, or take billions of search iterations.
|
||
let result = best_offset(w, h, ch, &img, &hole, usize::MAX, i32::MAX);
|
||
if let Some((dx, dy)) = result {
|
||
assert!(dx.abs() <= w as i32 && dy.abs() <= h as i32);
|
||
}
|
||
assert_eq!(best_offset(w, h, ch, &img, &hole, 2, -1), None);
|
||
assert_eq!(best_offset(w, h, ch, &img, &hole, 2, 0), None);
|
||
}
|
||
|
||
/// Structure, fine noise and flat patches (so that some matches tie exactly), `ch` channels.
|
||
fn busy(w: usize, h: usize, ch: usize, seed: u64) -> Vec<f32> {
|
||
let mut rng = Rng::new(seed);
|
||
(0..w * h)
|
||
.flat_map(|i| {
|
||
let (x, y) = (i % w, i / w);
|
||
let flat = (x / 9 + y / 7) % 5 == 0;
|
||
let n = (rng.next() % 1000) as f32 / 1000.0;
|
||
(0..ch)
|
||
.map(move |c| if flat { 0.25 + 0.1 * c as f32 } else { (0.2 + 0.004 * x as f32 + 0.1 * ((x + 2 * y + c) % 6) as f32 + 0.05 * n).min(1.0) })
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn bits(v: &[f32]) -> Vec<u32> {
|
||
v.iter().map(|x| x.to_bits()).collect()
|
||
}
|
||
|
||
/// Roles: a hole, a band that is context only, a corner that is no image.
|
||
fn roles(w: usize, h: usize, hole: &[bool]) -> Vec<Px> {
|
||
(0..w * h)
|
||
.map(|i| {
|
||
let (x, y) = (i % w, i / w);
|
||
if hole[i] {
|
||
Px::Hole
|
||
} else if x < 4 && y < 6 {
|
||
Px::Void
|
||
} else if (w * 3 / 4..w * 3 / 4 + 3).contains(&x) {
|
||
Px::Known
|
||
} else {
|
||
Px::Source
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
fn a_hole_is_as_thick_as_it_is_through_not_along() {
|
||
let (w, h) = (240, 120);
|
||
let all = Bounds { x0: 0, y0: 0, x1: w, y1: h };
|
||
let thickness = |hole: &[bool]| {
|
||
let px: Vec<Px> = hole.iter().map(|&o| if o { Px::Hole } else { Px::Source }).collect();
|
||
hole_thickness(w, hole_bounds(w, h, &px).unwrap_or(all), &px)
|
||
};
|
||
// A band 200 px long and 9 px thick; a disc of radius 20; the band touching the edge
|
||
// counts the outside of the image as not hole.
|
||
let band: Vec<bool> = (0..w * h).map(|i| (20..220).contains(&(i % w)) && (50..59).contains(&(i / w))).collect();
|
||
assert!((9..=11).contains(&thickness(&band)), "{}", thickness(&band));
|
||
assert!((39..=42).contains(&thickness(&disc(w, h, 120.0, 60.0, 20.0))));
|
||
let edge: Vec<bool> = (0..w * h).map(|i| i / w < 9).collect();
|
||
assert!((9..=11).contains(&thickness(&edge)), "{}", thickness(&edge));
|
||
}
|
||
|
||
/// A long thin hole goes the thin way (a shallow pyramid that starts from a membrane): it is
|
||
/// filled, only it changes, and the result does not depend on the thread count either.
|
||
#[test]
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn thin_holes_are_filled_the_same_on_any_number_of_threads() {
|
||
let (w, h) = (300, 200);
|
||
let img = busy(w, h, 3, 5);
|
||
// A wavy line 6 px thick across the image.
|
||
let hole: Vec<bool> = (0..w * h).map(|i| (((i % w) as f32 * 0.05).sin() * 30.0 + 100.0 - (i / w) as f32).abs() < 3.0).collect();
|
||
let px = roles(w, h, &hole);
|
||
let run = |threads: usize| {
|
||
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap();
|
||
pool.install(|| complete_masked(w, h, 3, &img, &px, &CompleteParams::default(), &photocraft_raster::Interrupt::NONE)).unwrap().unwrap()
|
||
};
|
||
let one = run(1);
|
||
assert_eq!(bits(&one), bits(&run(7)));
|
||
assert!((0..w * h).all(|i| hole[i] || (0..3).all(|c| one[i * 3 + c].to_bits() == img[i * 3 + c].to_bits())));
|
||
assert!((0..w * h).filter(|&i| hole[i]).all(|i| (0..3).all(|c| one[i * 3 + c].is_finite())));
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn results_do_not_depend_on_the_thread_count() {
|
||
// Big enough for the parallel passes, and for PatchMatch to sweep in wavefronts where
|
||
// there are fewer bands than threads (never with one thread).
|
||
let (w, h) = (300, 260);
|
||
for (ch, adapt) in [(3, Adapt::NONE), (4, Adapt { gain: (0.9, 1.1), bias: 0.04 })] {
|
||
let img = busy(w, h, ch, 3);
|
||
let hole = disc(w, h, 140.0, 130.0, 40.0);
|
||
let px = roles(w, h, &hole);
|
||
let p = CompleteParams { adapt, ..Default::default() };
|
||
let run = |threads: usize| {
|
||
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap();
|
||
bits(&pool.install(|| complete_masked(w, h, ch, &img, &px, &p, &photocraft_raster::Interrupt::NONE)).unwrap().unwrap())
|
||
};
|
||
let one = run(1);
|
||
assert_eq!(one, run(3), "{ch} channels, 3 threads");
|
||
assert_eq!(one, run(16), "{ch} channels, 16 threads");
|
||
}
|
||
}
|
||
|
||
/// The onion peel as first written: every source in order, every known pixel summed.
|
||
fn onion_peel_plain(w: usize, h: usize, ch: usize, r: i32, img: &mut [f32], px: &[Px], sources: &[(i32, i32)]) {
|
||
let mut known: Vec<bool> = px.iter().map(|k| matches!(k, Px::Known | Px::Source)).collect();
|
||
loop {
|
||
let front: Vec<usize> = (0..w * h)
|
||
.filter(|&i| {
|
||
let (x, y) = ((i % w) as i32, (i / w) as i32);
|
||
!known[i]
|
||
&& px[i] == Px::Hole
|
||
&& (-1..=1).any(|dy: i32| {
|
||
(-1..=1).any(|dx: i32| {
|
||
let (nx, ny) = (x + dx, y + dy);
|
||
nx >= 0 && ny >= 0 && (nx as usize) < w && (ny as usize) < h && known[ny as usize * w + nx as usize]
|
||
})
|
||
})
|
||
})
|
||
.collect();
|
||
if front.is_empty() {
|
||
break;
|
||
}
|
||
let vals: Vec<Vec<f32>> = front
|
||
.iter()
|
||
.map(|&i| {
|
||
let (x, y) = ((i % w) as i32, (i / w) as i32);
|
||
let mut best = (f32::INFINITY, (0i32, 0i32));
|
||
for &(sx, sy) in sources {
|
||
let (mut e, mut n) = (0.0f32, 0usize);
|
||
for dy in -r..=r {
|
||
for dx in -r..=r {
|
||
let (tx, ty) = (x + dx, y + dy);
|
||
if tx < 0 || ty < 0 || tx as usize >= w || ty as usize >= h || !known[ty as usize * w + tx as usize] {
|
||
continue;
|
||
}
|
||
let (ti, si) = ((ty as usize * w + tx as usize) * ch, ((sy + dy) as usize * w + (sx + dx) as usize) * ch);
|
||
for c in 0..ch {
|
||
let d = img[ti + c] - img[si + c];
|
||
e += d * d;
|
||
}
|
||
n += 1;
|
||
}
|
||
}
|
||
if n > 0 && e / (n as f32) < best.0 {
|
||
best = (e / n as f32, (sx, sy));
|
||
}
|
||
}
|
||
let si = (best.1.1 as usize * w + best.1.0 as usize) * ch;
|
||
img[si..si + ch].to_vec()
|
||
})
|
||
.collect();
|
||
for (&i, v) in front.iter().zip(vals) {
|
||
img[i * ch..(i + 1) * ch].copy_from_slice(&v);
|
||
known[i] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// #2805's case (upstream's own speed-up of the onion peel, superseded here): repeating
|
||
/// texture with many exact ties, two holes; the result must be the plain scan's to the bit.
|
||
#[test]
|
||
fn onion_peel_picks_exactly_what_the_plain_scan_picks() {
|
||
let (w, h, ch, r) = (48, 40, 3, 3);
|
||
let mut img = texture(w, h);
|
||
for y in 0..h {
|
||
for x in 6..w / 2 {
|
||
for c in 0..ch {
|
||
img[(y * w + x) * ch + c] = img[(y * w + x % 6) * ch + c];
|
||
}
|
||
}
|
||
}
|
||
let none = photocraft_raster::Interrupt::NONE;
|
||
for hole in [disc(w, h, 20.0, 18.0, 7.5), disc(w, h, 34.0, 22.0, 5.0)] {
|
||
let px: Vec<Px> = hole.iter().map(|h| if *h { Px::Hole } else { Px::Source }).collect();
|
||
let near = integral(w, h, &hole);
|
||
let sources: Vec<(i32, i32)> = (r..h as i32 - r)
|
||
.flat_map(|y| (r..w as i32 - r).map(move |x| (x, y)))
|
||
.filter(|&(x, y)| window_count(&near, w, h, x - r, y - r, x + r + 1, y + r + 1) == 0)
|
||
.collect();
|
||
let (mut fast, mut plain) = (img.clone(), img.clone());
|
||
onion_peel(w, h, ch, r, &mut fast, &px, hole_bounds(w, h, &px).unwrap(), &sources, &none).unwrap();
|
||
onion_peel_plain(w, h, ch, r, &mut plain, &px, &sources);
|
||
assert!(fast.iter().zip(&plain).all(|(a, b)| a.to_bits() == b.to_bits()));
|
||
assert_ne!(fast, img);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn onion_peel_takes_the_plain_scans_sources() {
|
||
let none = photocraft_raster::Interrupt::NONE;
|
||
for (w, h, ch, seed) in [(44, 36, 1, 1), (50, 40, 3, 2), (40, 40, 4, 3)] {
|
||
let mut img = busy(w, h, ch, seed);
|
||
// Also an image of a few exact values, where many sources tie.
|
||
if seed == 3 {
|
||
img.iter_mut().for_each(|v| *v = (*v * 3.0).round() / 3.0);
|
||
}
|
||
let hole = disc(w, h, w as f32 * 0.45, h as f32 * 0.5, 7.5);
|
||
let px = roles(w, h, &hole);
|
||
let valid = valid_sources(w, h, 3, &px, &none).unwrap();
|
||
let sources = Sources::new(&valid, w).all(&valid, w);
|
||
let (mut a, mut b) = (img.clone(), img.clone());
|
||
onion_peel(w, h, ch, 3, &mut a, &px, hole_bounds(w, h, &px).unwrap(), &sources, &none).unwrap();
|
||
onion_peel_plain(w, h, ch, 3, &mut b, &px, &sources);
|
||
assert_eq!(bits(&a), bits(&b), "{w}×{h}×{ch}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn least_sum_is_where_the_quotient_crosses() {
|
||
let mut rng = Rng::new(5);
|
||
for i in 0..4000 {
|
||
let n = (1 + rng.next() % 49) as f32;
|
||
let v = match i % 4 {
|
||
0 => 0.0,
|
||
1 => (rng.next() % 1000) as f32 * 1e-9,
|
||
_ => (rng.next() % 1_000_000) as f32 / 1_000_000.0,
|
||
};
|
||
for strict in [true, false] {
|
||
let holds = |e: f32| if strict { e / n > v } else { e / n >= v };
|
||
let e = least_sum(v, n, strict);
|
||
assert!(holds(e), "{v} {n} {strict}: {e}");
|
||
assert!(e == 0.0 || !holds(e.next_down()), "{v} {n} {strict}: {e} is not the least");
|
||
}
|
||
}
|
||
assert_eq!(least_sum(f32::INFINITY, 4.0, true), f32::INFINITY);
|
||
}
|
||
|
||
#[test]
|
||
fn valid_sources_are_the_wholly_copyable_patches() {
|
||
let (w, h, r) = (37, 29, 2);
|
||
let mut rng = Rng::new(9);
|
||
let px: Vec<Px> = (0..w * h)
|
||
.map(|_| match rng.next() % 23 {
|
||
0 => Px::Hole,
|
||
1 => Px::Void,
|
||
2 => Px::Known,
|
||
_ => Px::Source,
|
||
})
|
||
.collect();
|
||
let valid = valid_sources(w, h, r, &px, &photocraft_raster::Interrupt::NONE).unwrap();
|
||
let source: Vec<bool> = px.iter().map(|k| *k == Px::Source).collect();
|
||
let ss = integral(w, h, &source);
|
||
let (ri, full) = (r as i32, ((2 * r + 1) * (2 * r + 1)) as u32);
|
||
for y in 0..h as i32 {
|
||
for x in 0..w as i32 {
|
||
let inside = x >= ri && y >= ri && x + ri < w as i32 && y + ri < h as i32;
|
||
let want = inside && window_count(&ss, w, h, x - ri, y - ri, x + ri + 1, y + ri + 1) == full;
|
||
assert_eq!(valid[y as usize * w + x as usize], want, "({x}, {y})");
|
||
}
|
||
}
|
||
// Counting per row and block finds the centres in order.
|
||
let sources = Sources::new(&valid, w);
|
||
let list = sources.all(&valid, w);
|
||
assert!(list.len() > 10 && list.len() == sources.len());
|
||
for (k, p) in list.iter().enumerate() {
|
||
assert_eq!(sources.nth(k, &valid, w), *p);
|
||
}
|
||
// And on a grid wider than a block, with empty blocks and rows.
|
||
let (w, h) = (300, 9);
|
||
let wide: Vec<bool> = (0..w * h).map(|i| (i % w) % 7 == 3 && (i % w) / 64 != 2 && i / w != 4).collect();
|
||
let sources = Sources::new(&wide, w);
|
||
let list = sources.all(&wide, w);
|
||
for (k, p) in list.iter().enumerate() {
|
||
assert_eq!(sources.nth(k, &wide, w), *p);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn full_patches_measure_as_clipped_ones() {
|
||
let (w, h, ch, r) = (46, 38, 4, 3);
|
||
let mut img = busy(w, h, ch, 4);
|
||
let hole = disc(w, h, 20.0, 18.0, 6.0);
|
||
let px = roles(w, h, &hole);
|
||
let none = photocraft_raster::Interrupt::NONE;
|
||
let hb = hole_bounds(w, h, &px).unwrap();
|
||
let tb = hb.grow(r, w, h);
|
||
let valid = valid_sources(w, h, r, &px, &none).unwrap();
|
||
let picks: Vec<(i32, i32)> = Sources::new(&valid, w).all(&valid, w).iter().step_by(37).copied().collect();
|
||
assert!(picks.len() > 5);
|
||
for adapt in [Adapt::NONE, Adapt { gain: (0.8, 1.25), bias: 0.08 }] {
|
||
for keep in [false, true] {
|
||
let targets = targets_around(w, r, &px, hb, tb);
|
||
let mut s = Solver {
|
||
w,
|
||
h,
|
||
ch,
|
||
r: r as i32,
|
||
img: &mut img,
|
||
px: &px,
|
||
valid: valid.clone(),
|
||
targets,
|
||
hole_box: hb,
|
||
tbox: tb,
|
||
full: Vec::new(),
|
||
tmom: Vec::new(),
|
||
smom: Vec::new(),
|
||
adapt,
|
||
ctl: none,
|
||
};
|
||
s.prepare(keep);
|
||
let nnf = vec![picks[0]; tb.area()];
|
||
let mut cost = vec![f32::INFINITY; tb.area()];
|
||
s.costs(&nnf, &mut cost).unwrap();
|
||
let mut checked = 0;
|
||
for k in 0..tb.area() {
|
||
if !s.targets[k] || !s.full[k] {
|
||
continue;
|
||
}
|
||
let t = ((tb.x0 + k % tb.width()) as i32, (tb.y0 + k / tb.width()) as i32);
|
||
for &src in &picks {
|
||
if adapt.is_none() {
|
||
let exact = s.dist_clipped(t, src, f32::INFINITY);
|
||
assert_eq!(s.dist_full(t, src, f32::INFINITY).to_bits(), exact.to_bits());
|
||
for cutoff in [exact * 0.5, exact, exact * 2.0] {
|
||
let (a, b) = (s.dist_full(t, src, cutoff), s.dist_clipped(t, src, cutoff));
|
||
assert_eq!(a < cutoff, b < cutoff);
|
||
}
|
||
} else {
|
||
let (mut m1, mut m2) = ([[0.0f32; 5]; 4], [[0.0f32; 5]; 4]);
|
||
s.moments_full(k, t, src, &mut m1);
|
||
assert_eq!(s.moments(t, src, &mut m2), (2 * r + 1) * (2 * r + 1));
|
||
assert_eq!(m1.map(|c| c.map(f32::to_bits)), m2.map(|c| c.map(f32::to_bits)));
|
||
assert_eq!(s.dist_adapted_full(k, t, src).to_bits(), s.dist_adapted(t, src).to_bits());
|
||
}
|
||
checked += 1;
|
||
}
|
||
}
|
||
assert!(checked > 100, "{checked}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn owned_completion_fills_in_place_or_gives_the_pixels_back() {
|
||
let (w, h, ch) = (64, 52, 3);
|
||
let img = busy(w, h, ch, 6);
|
||
let hole = disc(w, h, 30.0, 26.0, 8.0);
|
||
let px = roles(w, h, &hole);
|
||
let (p, none) = (CompleteParams::default(), photocraft_raster::Interrupt::NONE);
|
||
let borrowed = complete_masked(w, h, ch, &img, &px, &p, &none).unwrap().unwrap();
|
||
let owned = complete_masked_owned(w, h, ch, img.clone(), &px, &p, &none).unwrap().unwrap();
|
||
assert_eq!(bits(&owned), bits(&borrowed));
|
||
// Nothing may be copied: no completion, and the pixels come back as they were.
|
||
let known: Vec<Px> = px.iter().map(|k| if *k == Px::Source { Px::Known } else { *k }).collect();
|
||
assert!(complete_masked(w, h, ch, &img, &known, &p, &none).unwrap().is_none());
|
||
let back = complete_masked_owned(w, h, ch, img.clone(), &known, &p, &none).unwrap().unwrap_err();
|
||
assert_eq!(bits(&back), bits(&img));
|
||
// Cancelled, it stops.
|
||
let yes = || true;
|
||
assert!(complete_masked_owned(w, h, ch, img, &px, &p, &photocraft_raster::Interrupt::cancel_only(&yes)).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn synthesis_fills_hole_with_plausible_values() {
|
||
let (w, h, ch) = (64, 64, 3);
|
||
let clean = texture(w, h);
|
||
let hole = disc(w, h, 32.0, 32.0, 6.0);
|
||
let out = synthesize(w, h, ch, &clean, &hole, 8, 3);
|
||
let err = hole_rmse(&out, &clean, &hole, ch);
|
||
assert!(err < 0.15, "{err}");
|
||
assert!(out.iter().all(|v| v.is_finite()));
|
||
}
|
||
}
|