mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-12 15:16:08 -07:00
Snap gate: admit blends of two neighbouring regions
The mixture gate only tested betweenness against the pixel's OWN region and the flip candidate, so a pixel blending two foreign flanks could never flip: on a blurred low-contrast crack the basin cut leaks a distant region along the crack's blend band as a 1-px filament (the striped synthetic under 1.2 px blur grows a 30 px hairline of the circle's red along a stripe boundary), and the snap visited it every sweep yet was forbidden to fix it. A pixel that reads as a blend of two neighbouring regions now flips to the closer of them; genuine third colors (outline strokes) still fail betweenness against every pair and stay put. Blurred synthetic circle: max boundary error 31.6 -> 1.5 px, rms 5.6 -> 0.7. Crisp images byte-unaffected (fidelity 0.9261 unchanged); all watershed property tests pass unchanged.
This commit is contained in:
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Watershed no longer leaks a region along a blurred low-contrast crack as a 1-px filament (a slightly soft image could grow a hairline of one region's color running tens of px down a neighbouring boundary). The boundary snap's mixture gate now also admits pixels that blend two *neighbouring* regions — a blend band belongs to its closer flank even when the basin cut misattributed it to a distant region. On the blurred striped synthetic the circle's max boundary error drops from 31.6 px to 1.5 px; crisp images are byte-unaffected.
|
||||||
|
|
||||||
## 1.0.0-alpha.2 - 2026-07-27
|
## 1.0.0-alpha.2 - 2026-07-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -533,10 +533,16 @@ const SNAP_SLACK: i32 = 16;
|
|||||||
/// quantization applies, which is why the color-cluster frontend never shows
|
/// quantization applies, which is why the color-cluster frontend never shows
|
||||||
/// this.
|
/// this.
|
||||||
///
|
///
|
||||||
/// Only pixels whose color is a *mixture* of the two region means may flip
|
/// Only pixels whose color is a *mixture* of two adjacent region means may
|
||||||
/// (`d(p,A) + d(p,B) ≤ d(A,B) + slack`): a pixel of a genuine third color —
|
/// flip (`d(p,A) + d(p,B) ≤ d(A,B) + slack`): a pixel of a genuine third
|
||||||
/// say a dark outline stroke absorbed into a lighter region — must stay with
|
/// color — say a dark outline stroke absorbed into a lighter region — must
|
||||||
/// its basin even when some other neighbour's mean happens to sit closer.
|
/// stay with its basin even when some other neighbour's mean happens to sit
|
||||||
|
/// closer. The mixture pair is usually the pixel's own region and the flip
|
||||||
|
/// candidate (the classic AA ramp), but a pair of *neighbouring* regions
|
||||||
|
/// also qualifies: on a blurred low-contrast crack the basin cut can leak a
|
||||||
|
/// distant region along the crack's blend band as a 1-px filament — those
|
||||||
|
/// pixels blend the two flanking regions and are unrelated to their own
|
||||||
|
/// region's color, and they belong to the closer flank.
|
||||||
/// Sweeps are double-buffered (flips apply after scanning) and each moves the
|
/// Sweeps are double-buffered (flips apply after scanning) and each moves the
|
||||||
/// boundary at most 1 px, so total movement stays within the ambiguity band;
|
/// boundary at most 1 px, so total movement stays within the ambiguity band;
|
||||||
/// regions are never emptied. Only the first sweep scans the whole canvas;
|
/// regions are never emptied. Only the first sweep scans the whole canvas;
|
||||||
@@ -579,14 +585,22 @@ fn snap_boundaries(
|
|||||||
(cv[0] - m[0]).abs() + (cv[1] - m[1]).abs() + (cv[2] - m[2]).abs()
|
(cv[0] - m[0]).abs() + (cv[1] - m[1]).abs() + (cv[2] - m[2]).abs()
|
||||||
};
|
};
|
||||||
let da = dist(&mean[a]);
|
let da = dist(&mean[a]);
|
||||||
|
// The pixel qualifies as a blend of regions `p` and `q` when its
|
||||||
|
// color sits between their means (L1 between-ness plus noise slack).
|
||||||
|
let mixture = |p: usize, q: usize| -> bool {
|
||||||
|
let dpq: i32 = (0..3).map(|ch| (mean[p][ch] - mean[q][ch]).abs()).sum();
|
||||||
|
dist(&mean[p]) + dist(&mean[q]) <= dpq + SNAP_SLACK
|
||||||
|
};
|
||||||
let mut best = (da, a);
|
let mut best = (da, a);
|
||||||
for b in nb {
|
for b in nb {
|
||||||
if b == a {
|
if b == a {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let db = dist(&mean[b]);
|
let db = dist(&mean[b]);
|
||||||
let dab: i32 = (0..3).map(|ch| (mean[a][ch] - mean[b][ch]).abs()).sum();
|
if db >= best.0 {
|
||||||
if db < best.0 && da + db <= dab + SNAP_SLACK {
|
continue;
|
||||||
|
}
|
||||||
|
if mixture(a, b) || nb.iter().any(|&c| c != a && c != b && mixture(c, b)) {
|
||||||
best = (db, b);
|
best = (db, b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user