From 4b5e55bc50a856e727f26691dabf83607068dcf2 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 14 Oct 2026 17:35:27 +0700 Subject: [PATCH] fix(heal): eliminate boundary seams by windowing inpaint clusters to 1:1 scale and smoothing feather transition --- docker/frontend/shared/utils/heal.ts | 6 ++-- docker/frontend/src/engine/exportEngine.ts | 33 ++++++++++++++++++---- docker/frontend/src/engine/inpaint.ts | 5 ++-- docker/recipescam-images.tar.gz.sha256 | 2 +- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/docker/frontend/shared/utils/heal.ts b/docker/frontend/shared/utils/heal.ts index cd577e0..96f772d 100644 --- a/docker/frontend/shared/utils/heal.ts +++ b/docker/frontend/shared/utils/heal.ts @@ -25,9 +25,9 @@ import type { HealSpot } from '../types'; export const HEAL_DEFAULT_R = 0.012; // How much of a spot is painted at full strength, as a fraction of its radius: -// out to 95% the mask is solid to completely eliminate defects without leaving -// residual dirt/shadows, and the last 5% is a smooth Hermite fade to blend into the photo. -export const HEAL_FEATHER = 0.95; +// out to 75% the mask is solid to completely eliminate defects, and the outer 25% +// is a smooth Hermite fade (smoothstep) to blend invisibly into the surrounding photo. +export const HEAL_FEATHER = 0.75; // The finest of the fill's blurs, as a multiple of the widest brush in the // stroke. The light of the rim has to reach the middle of the hole, so the diff --git a/docker/frontend/src/engine/exportEngine.ts b/docker/frontend/src/engine/exportEngine.ts index dffeb76..c9effa9 100644 --- a/docker/frontend/src/engine/exportEngine.ts +++ b/docker/frontend/src/engine/exportEngine.ts @@ -1326,20 +1326,43 @@ function clusterHealSpots( } } } + + // Maximum dimension of a cluster's bounding box so that the inpaint crop + // runs at ~1:1 native resolution (avoiding heavy downscale/upscale blur & edge seams) + const isMob = typeof navigator !== 'undefined' && /iPad|iPhone|iPod|Android|Mobile/i.test(navigator.userAgent || ''); + const maxSpan = isMob ? 240 : 380; const visited = new Uint8Array(n); const clusters: { x: number; y: number; r: number }[][] = []; + for (let i = 0; i < n; i++) { if (visited[i]) continue; visited[i] = 1; const cluster: { x: number; y: number; r: number }[] = [spots[i]]; + let minX = spots[i].x * width; + let maxX = minX; + let minY = spots[i].y * height; + let maxY = minY; const queue = [i]; + while (queue.length > 0) { - const curr = queue.pop()!; + const curr = queue.shift()!; for (const neighbor of adj[curr]) { if (!visited[neighbor]) { - visited[neighbor] = 1; - cluster.push(spots[neighbor]); - queue.push(neighbor); + const nx = spots[neighbor].x * width; + const ny = spots[neighbor].y * height; + const newMinX = Math.min(minX, nx); + const newMaxX = Math.max(maxX, nx); + const newMinY = Math.min(minY, ny); + const newMaxY = Math.max(maxY, ny); + if (newMaxX - newMinX <= maxSpan && newMaxY - newMinY <= maxSpan) { + visited[neighbor] = 1; + cluster.push(spots[neighbor]); + minX = newMinX; + maxX = newMaxX; + minY = newMinY; + maxY = newMaxY; + queue.push(neighbor); + } } } } @@ -1503,7 +1526,7 @@ function clusterHealSpots( const my1 = Math.max(...cYs) + cWidest; const margin = Math.max( 16, - Math.min(64, Math.round(Math.max(cWidest * 1.5, 0.25 * Math.max(mx1 - mx0, my1 - my0)))) + Math.min(64, Math.round(cWidest * 1.5)) ); const srcLeft = Math.max(boxLeft, Math.floor(mx0 - margin)); const srcTop = Math.max(boxTop, Math.floor(my0 - margin)); diff --git a/docker/frontend/src/engine/inpaint.ts b/docker/frontend/src/engine/inpaint.ts index 035ca27..1399326 100644 --- a/docker/frontend/src/engine/inpaint.ts +++ b/docker/frontend/src/engine/inpaint.ts @@ -253,7 +253,7 @@ export function fastLocalInpaint( const fG = new Float32Array(w * h); const fB = new Float32Array(w * h); - const bStep = Math.max(1, Math.floor(bLen / 32)); + const bStep = bLen <= 96 ? 1 : Math.max(1, Math.floor(bLen / 64)); for (let y = 0; y < h; y++) { const row = y * w; @@ -329,7 +329,8 @@ export function fastLocalInpaint( } noiseVariance /= Math.max(1, count); // High-frequency noise standard deviation (adjacent difference / sqrt(2)) - const noiseAmp = Math.min(5, Math.max(0.6, Math.sqrt(noiseVariance * 0.5))); + const rawAmp = Math.sqrt(noiseVariance * 0.5); + const noiseAmp = rawAmp < 0.3 ? 0 : Math.min(4, rawAmp); let seed = (w * 31 + h * 17 + holeCount) | 0; function prng(): number { diff --git a/docker/recipescam-images.tar.gz.sha256 b/docker/recipescam-images.tar.gz.sha256 index d08b37b..23353ae 100644 --- a/docker/recipescam-images.tar.gz.sha256 +++ b/docker/recipescam-images.tar.gz.sha256 @@ -1 +1 @@ -11bf4442bb348f4ac3ed55da345805502351bcadddbe11a1edb2b819a864a611 recipescam-images.tar.gz +ca55c46c0374f76a007f5309759bf25e07fcee625483a87fe96cce5c34a93645 recipescam-images.tar.gz