Let the fill show the model an open void: mirror depth 0 means no ring and nothing known beyond the band

This commit is contained in:
2026-09-20 10:56:38 +02:00
parent 3de109fbd1
commit b1c99b5796
8 changed files with 155 additions and 31 deletions
+86 -14
View File
@@ -89,12 +89,19 @@ pub struct Params {
pub coarse: usize,
/// The fine passes' band width.
pub band: usize,
/// How deep into the picture the mirrored context reaches. A plain
/// reflection of a deep hole pulls in whatever is that far from the
/// edge — a ridge, a peak — and the model, told that is what lies
/// beyond, paints it upside down. Folding the reflection within this
/// band keeps the ring looking like the edge it continues (sky beside
/// sky, grass beside grass) and nothing further away.
/// How deep into the picture the mirrored context reaches, or **zero
/// for no mirrored context at all**: the void is then shown to the
/// model as it is — reaching the picture's edge with nothing beyond,
/// and, beyond the band being filled, still unknown. That is what the
/// shipped model was trained on (a fine-tune of MI-GAN on voids cut
/// from photographs the way a cylindrical merge cuts them, see
/// `docs/panorama.md` §14); a ring would give it a fold to continue.
///
/// Non-zero is the stock model's crutch: a plain reflection of a deep
/// hole pulls in whatever is that far from the edge — a ridge, a peak —
/// and the model, told that is what lies beyond, paints it upside down.
/// Folding the reflection within this band keeps the ring looking like
/// the edge it continues and nothing further away.
pub mirror_depth: usize,
/// How far inside the real edge the fill also regenerates, the two
/// blended by distance. A hard cut between real pixels and invented
@@ -108,9 +115,9 @@ pub struct Params {
impl Default for Params {
fn default() -> Self {
Params {
coarse: 4,
band: 96,
mirror_depth: 48,
coarse: 1,
band: 192,
mirror_depth: 0,
feather: 24,
stride: 384,
}
@@ -141,7 +148,10 @@ pub fn fill_border(
} = params;
let q = q.max(1);
let band = band.max(8);
let mirror_depth = mirror_depth.max(1);
// No ring: the void beyond the band stays unknown, as in the model's
// training; with a ring the far side is the coarse fill, presented as
// known, which the stock model needed to see something there.
let open = mirror_depth == 0;
if width == 0 || height == 0 || rgb.len() != width * height * 3 || known.len() != width * height
{
return Err(PanoError::Input("fill: buffer sizes disagree".into()));
@@ -227,7 +237,11 @@ pub fn fill_border(
let mut any = false;
for i in 0..width * height {
let in_band = !known[i] && dist[i] > lo && dist[i] <= hi;
band_known[i] = !in_band;
band_known[i] = if open {
known[i] || dist[i] <= lo
} else {
!in_band
};
any |= in_band;
}
if !any {
@@ -369,7 +383,7 @@ fn fill_once(
if known[i] {
continue;
}
let p = (yy + RING) * pw + (xx + RING);
let p = (yy + ctx.ring) * pw + (xx + ctx.ring);
if wsum[p] > 0.0 {
for ch in 0..3 {
rgb[i * 3 + ch] = (acc[p * 3 + ch] / wsum[p]).clamp(0.0, 1.0);
@@ -478,12 +492,25 @@ fn fold(d: usize, depth: usize) -> usize {
struct MirroredContext {
width: usize,
height: usize,
/// The padding on every side: `RING` with mirrored context, 0 without.
ring: usize,
rgb: Vec<f32>,
hole: Vec<bool>,
}
impl MirroredContext {
fn build(rgb: &[f32], width: usize, height: usize, known: &[bool], depth: usize) -> Self {
if depth == 0 {
// Open: the picture as it is, the hole as it is. What the hole
// holds does not matter — the model masks it out.
return MirroredContext {
width,
height,
ring: 0,
rgb: rgb.to_vec(),
hole: known.iter().map(|&k| !k).collect(),
};
}
let fold = |d: usize| fold(d, depth);
let (pw, ph) = (width + 2 * RING, height + 2 * RING);
let mut canvas = vec![0.0f32; pw * ph * 3];
@@ -534,6 +561,7 @@ impl MirroredContext {
MirroredContext {
width: pw,
height: ph,
ring: RING,
rgb: canvas,
hole,
}
@@ -634,7 +662,10 @@ mod tests {
200,
&known,
&mut model,
test_params(0),
Params {
mirror_depth: 48,
..test_params(0)
},
&mut |_, _| {},
)
.unwrap();
@@ -648,6 +679,42 @@ mod tests {
}
}
#[test]
fn an_open_void_reaches_the_tile_edge_and_stays_unknown_beyond_the_band() {
// A 150-tall hole above and below; bands of 96. With no ring the
// first band's tiles sit at the picture's edge, so a tile's top
// row is unknown, and the rows deeper than the band are unknown
// too — not "known" coarse fill — exactly as the model was trained.
let (mut rgb, known) = picture(200, 500, 150);
let mut model = Flat {
tile: 64,
seen: Vec::new(),
};
fill_border(
&mut rgb,
200,
500,
&known,
&mut model,
Params {
band: 96,
..test_params(0)
},
&mut |_, _| {},
)
.unwrap();
// The first pass's tile at the picture's top edge is unknown
// through and through: the hole is 150 deep, the tile 64, and
// nothing beyond the band was presented as known. With a ring, or
// with the far side shown as coarse fill, no tile is ever all hole.
assert!(model.seen.iter().any(|(_, k)| k.iter().all(|&v| !v)));
for i in 0..200 * 500 {
if !known[i] {
assert!((rgb[i * 3] - 0.5).abs() < 1e-4, "pixel {i}");
}
}
}
#[test]
fn the_fine_passes_run_in_bands_after_the_coarse_one() {
// A 150-tall hole above and below a picture: the coarse pass sees
@@ -663,7 +730,12 @@ mod tests {
500,
&known,
&mut model,
test_params(0),
Params {
coarse: 4,
band: 96,
mirror_depth: 48,
..test_params(0)
},
&mut |_, _| {},
)
.unwrap();