Issue #3272: AI masks are enlarged along the photo's edges, no outline under a raised Exposure

An Object / Describe mask is stored as a model's logit grid (288² over the whole photo,
~20 px cells on a 24 MP export) and was enlarged by bilinear sampling alone, so its edge
spilled 1–2 px onto the background; raising the mask's Exposure turned that strip into a
bright outline in previews and exports (CPU and GPU: the GPU renderer evaluates these
shapes with the same CPU `shape_alpha`).

`sample_seg` now guided-filters the sampled logits by the photo's log luminance where the
grid is coarser than the output: the fast (subsampled) guided filter, a window of 1.5 grid
cells, so a preview and an export put the edge in the same place. Filtering logits, not
coverage, lets the confident side win (the spill goes to ≈ 0, not to a mean of ≈ 0.1).
Pixels whose window is all inside or all outside, and edges the photo has no contrast
across, keep the plain sample exactly. Refine Edges still runs afterwards on the combined
mask and finds the edges in place: no second halo, no tightening.

One implementation: `guided_cross` (Refine Edges, Auto Mask) and the new
`guided_cross_fast` share `guided_coeffs`; the subsampled form uses
`local::guided_fast_step` and a box `resize`, like `local::guided_fast`. `guided_cross`'s
output is unchanged.

Existing AI masks become 1–2 px tighter at a subject's edges, on every process; their
inside, outside and contrast-less edges are unchanged. This lands in place under the
bug-fix rule of docs/process-versions.md (output that is in neither the photo nor the
edit), noted there.

Cost per AI selection (M2 Pro, 12 cores; one shape_alpha, before → after):
24 MP 25.7 → 48.2 ms, heap peak 96.5 → 108.8 MB; 60 MP 64.1 → 109.9 ms, 239.9 → 256.7 MB;
2.7 MP preview 3.1 → 10.6 ms, 11.2 → 26.5 MB.

Tests (a dark disc on a light background, its mask on a 32² grid): ≈ 0 one and two px
outside and ≈ 1 inside, all around (1 px outside was 0.66), bit-identical away from the
edge; with Refine Edges 50 / 100 (was 0.29 outside) and the edge within 1 px of the
unrefined one; the same edge position at 256, 512 and 1024 px; a Prompt mask's window
matches the whole render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brain Wang
2026-10-29 11:13:04 +08:00
co-authored by Claude Opus 5.5
parent 77c0a6adfa
commit f14d166fdf
7 changed files with 247 additions and 12 deletions
+7
View File
@@ -27,6 +27,13 @@ Blacks at 0) is the identity up to white: the SDR shoulder no longer pulls 1.0 d
255 to 254, issue #1945). It landed in place as a bug fix, since the tone map promised identity at
neutral settings; edited curves keep the shoulder and their V1 look.
**AI mask edges** (Object and Describe selections stored as a model's grid) follow the photo's edges
when they are enlarged to the output (issue #3272): the plain enlargement spilled 1–2 px onto the
background, which a raised Exposure turned into a bright outline around the subject in previews and
exports. Existing AI masks become 1–2 px tighter at a subject's edges, on every process; their
inside, their outside and edges without contrast in the photo are unchanged. It landed in place as
a bug fix: the outline is output that isn't in the photo nor in the edit, not a look anyone chose.
## Adding a process
1. Add a variant to `Process` (`V2`), append it to `Process::ALL` (oldest first), give it its number in