From c8a33ba9e83f72ea50fb72384b3af37dfcee7de8 Mon Sep 17 00:00:00 2001 From: Ozan Sozuoz Date: Wed, 15 Jul 2026 19:32:00 -0400 Subject: [PATCH 1/2] fix: close the [288,512) auto-enhance dead zone Real-world small web rasters (300-511px icons/logos, AA + JPEG) got no enhancement at all: below the 512 auto-upscale floor, not blurry enough for restore. A reported 500x500 JPEG app icon traced its raw JPEG noise into splotchy gradient regions and wobbly edges. Lower DEFAULT_MIN_UPSCALE_EDGE 512 -> 288 so the SR-before-trace recipe covers the band. Size-only gating is deliberate: measured content signals (soft-edge fraction, unique-color ratio, 8px blockiness) do not separate clean synthetic fixtures from degraded rasters, while 288 separates by construction (every synthetic golden/corpus fixture is <= 256px, so goldens stay byte-identical -- verified, full suite green). [64, 288) stays unenhanced pending real fixtures: SR from <288px to 1600 is a >5.5x jump with real hallucination risk. E2E on a 500px icon-proxy JPEG (auto -> illustration): enhancement now fires (working res 1600); JPEG speckle patches gone, donut arcs smooth, rounded corners clean vs the ragged/staircased old output. Co-Authored-By: Claude Fable 5 --- engine/enhance/assess.py | 31 ++++++++++++++++++++++--------- tests/test_enhance.py | 29 +++++++++++++++++++++++++++++ tests/test_enhance_assess.py | 17 +++++++++++++++++ 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/engine/enhance/assess.py b/engine/enhance/assess.py index a7eeacc..905e66e 100644 --- a/engine/enhance/assess.py +++ b/engine/enhance/assess.py @@ -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) diff --git a/tests/test_enhance.py b/tests/test_enhance.py index dbefe06..4a683b4 100644 --- a/tests/test_enhance.py +++ b/tests/test_enhance.py @@ -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 diff --git a/tests/test_enhance_assess.py b/tests/test_enhance_assess.py index 5c45c0f..8a40f94 100644 --- a/tests/test_enhance_assess.py +++ b/tests/test_enhance_assess.py @@ -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) From d5127bfe1a13045410f9cad9ee00f089ac2071d0 Mon Sep 17 00:00:00 2001 From: Ozan Sozuoz Date: Wed, 15 Jul 2026 19:34:22 -0400 Subject: [PATCH 2/2] chore: sync uv.lock with 0.2.0 version bump Co-Authored-By: Claude Fable 5 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index be81b0b..b52dee5 100644 --- a/uv.lock +++ b/uv.lock @@ -1546,7 +1546,7 @@ wheels = [ [[package]] name = "shapeform" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "cairosvg" },