Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions engine/enhance/assess.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,34 @@
pouchette-icon 2.47, UI screenshots 15-37) while the blurry 2000px crest
scores 0.51. Photos score 0.23-0.32 -- below both thresholds -- which is
why enhance_image additionally exempts photo-classified content from
restore-only triggers. Below the floor the conservative small-image
threshold applies unchanged, keeping the small synthetic goldens
byte-identical."""
restore-only triggers. Inside the [288, 512) slice of the upscale band
this looser threshold can misread sharp content as blurry (a sharp
448px icon scores ~1.1), but the flag is behaviorally inert there:
needs_upscale already fires, and the restore downscale-ladder no-ops
below its own 512px floor. Below the upscale floor the conservative
small-image threshold applies unchanged, keeping the small synthetic
goldens byte-identical."""
DEFAULT_UPSCALE_TARGET = 1600
"""Sweet-spot long-edge target for the pre-trace upscale: super-res to
~1600px cleans anti-aliasing and sharpens edges so tracing yields far
fewer, smoother regions (crest: 2213->756 paths, 435->25 colors, ~30s),
while 2048 is disproportionately slower for little visual gain."""

DEFAULT_MIN_UPSCALE_EDGE = 512
"""Floor for auto-upscaling clean art: images below this are either tiny
(handled by the legacy <64px superres path) or small logos that already
trace cleanly, and upscaling them would only add cost -- and would perturb
the small synthetic golden fixtures (all <= 256px). Mid-res inputs in
[512, target) are the ones a crisper trace genuinely helps."""
DEFAULT_MIN_UPSCALE_EDGE = 288
"""Floor for auto-upscaling: inputs in [288, target) get the super-res
pre-pass. The floor used to be 512, which left [64, 512) with no
enhancement at all -- and ~300-512px is exactly where real-world web
rasters live (a reported 500x500 JPEG app icon traced its raw JPEG
noise into splotchy regions and wobbly edges). Real images that small
essentially always carry AA ramps and/or JPEG artifacts that the
SR-before-trace recipe cleans, so the band is gated on size alone:
content signals were measured across the fixtures (soft-edge fraction,
unique-color ratio, 8px blockiness) and none separates clean synthetic
art from degraded rasters, while 288 separates by construction -- every
synthetic golden/corpus fixture is <= 256px and stays byte-identical.
Below 288 the legacy <64px superres path still covers tiny inputs;
[64, 288) remains unenhanced pending real fixtures (SR from <288 to
1600 is a >5.5x jump with real hallucination risk)."""


@dataclass(frozen=True)
Expand Down
29 changes: 29 additions & 0 deletions tests/test_enhance.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,35 @@ def fake_enhance(image, mode, report=None):
assert calls == ["enhance:auto", "superres"]


def test_enhance_auto_upscales_small_web_raster_band():
# Regression: a 500x500 JPEG app icon fell in the old [64, 512) dead
# zone -- below the 512 upscale floor, not blurry enough for restore --
# so auto silently no-oped and the tracer ate raw JPEG noise + AA
# (splotchy gradient bands, wobbly rounded-square edges). Auto must
# now run the SR-before-trace recipe on the [288, 512) band.
rgb = np.full((500, 500, 3), 1.0, dtype=np.float32)
rgb[60:440, 60:440] = 0.1
rgb[125:375, 125:375] = 0.7
img = _img(cv2.GaussianBlur(rgb, (0, 0), sigmaX=1.0))
calls: list[dict] = []

def upscaler(image, **kwargs):
calls.append({"shape": image.rgb.shape[:2], **kwargs})
return image

result = enhance_image(
img, "auto", classify=lambda image: "illustration", upscaler=upscaler
)

assert result is img
assert len(calls) == 1
assert calls[0]["model_key"] == "lineart_x4"
assert calls[0]["target_edge"] == 1600
# the restore downscale-ladder must no-op below its 512 floor: the
# upscaler receives the image at native 500px, not shrunken
assert max(calls[0]["shape"]) == 500


def _blurry_large(size: int = 1990) -> RasterImage:
rgb = np.full((size, size, 3), 1.0, dtype=np.float32)
rgb[size // 4 : 3 * size // 4, size // 4 : 3 * size // 4] = 0.1
Expand Down
17 changes: 17 additions & 0 deletions tests/test_enhance_assess.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ def test_quality_flags_at_upscale_and_restore_boundaries():
assert flat_report.needs_restore is True


def test_small_web_raster_band_needs_upscale_at_default_floor():
# Regression: the auto-upscale floor was 512, leaving [64, 512) with no
# enhancement at all -- a 500x500 JPEG app icon traced its raw JPEG
# noise into splotchy regions and wobbly edges. The floor is now 288:
# in-band at 288 and 500, out at 287, and every synthetic golden
# fixture (<= 256px) stays below it by construction.
def crisp(size: int) -> RasterImage:
rgb = np.zeros((size, size, 3), dtype=np.float32)
rgb[::2, :] = 1.0
return _img(rgb)

assert assess_quality(crisp(500)).needs_upscale is True
assert assess_quality(crisp(288)).needs_upscale is True
assert assess_quality(crisp(287)).needs_upscale is False
assert assess_quality(crisp(256)).needs_upscale is False


def _large_shapes(size: int) -> np.ndarray:
"""Synthetic flat-art image: solid shapes on white, like a logo/crest."""
rgb = np.full((size, size, 3), 1.0, dtype=np.float32)
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading