From b9fde4efa27583676ddb29792a9a9689c267cdba Mon Sep 17 00:00:00 2001 From: Rasmus Ros Date: Mon, 17 Aug 2026 18:00:24 +0200 Subject: [PATCH] fix: measure the target's detail at the size candidates are rendered at --- src/vectrify/vector/runner.py | 13 ++++++++----- tests/score/test_complexity.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/vectrify/vector/runner.py b/src/vectrify/vector/runner.py index 6447d31..959d3c8 100644 --- a/src/vectrify/vector/runner.py +++ b/src/vectrify/vector/runner.py @@ -167,11 +167,14 @@ def run_vector_search( scoring_img = resize_long_side(original_img, DEFAULT_CONFIG.target_long_side) pixel_ref = prepare(scoring_img) - # The target's own detail, measured once. Candidates are charged for the - # distance from it, so this has to come from the same render size they do. - _ref_buf = io.BytesIO() - scoring_img.save(_ref_buf, format="PNG") - reference_detail = detail(_ref_buf.getvalue()) + # The target's own detail, measured once, at the size candidates are + # rasterized at -- original_png_bytes, not the smaller image the pixel + # comparison resizes to. Compressed size grows with pixel count, so reading + # the reference at scoring resolution and candidates at render resolution + # charges every candidate for the difference between the two: measured on + # the duck, the same image reads 7,607 at 256 and 41,602 at 700, and every + # candidate in a run came out 58-142% "busier" than a target it matched. + reference_detail = detail(original_png_bytes) log.info( "Round scoring: edge overlap, colour distance and a detail budget, no model. " f"Front evaluator: {ScorerType(scorer_type).value} ({vision_model})." diff --git a/tests/score/test_complexity.py b/tests/score/test_complexity.py index da9b1aa..f2c6fb4 100644 --- a/tests/score/test_complexity.py +++ b/tests/score/test_complexity.py @@ -74,3 +74,32 @@ def test_an_empty_candidate_wins_this_axis_and_loses_the_rest(): def test_survives_a_blank_reference(): assert detail_excess(0.0, _noisy()) == 0.0 + + +def test_detail_grows_with_pixel_count_at_equal_busyness(): + """Why the reference has to be measured at the size candidates render at. + Compressed size counts bytes, so the same picture reads larger when there + is more of it -- reading the reference at scoring resolution and candidates + at render resolution charges every candidate for the gap between the two. + """ + from vectrify.image_utils import resize_long_side + + rng = random.Random(5) + big = Image.new("RGB", (256, 256)) + big.putdata( + [ + (rng.randrange(256), rng.randrange(256), rng.randrange(256)) + for _ in range(256 * 256) + ] + ) + small = resize_long_side(big, 96) + + def as_png(img): + buf = io.BytesIO() + img.save(buf, format="PNG") + return buf.getvalue() + + assert detail(as_png(big)) > detail(as_png(small)) * 2 + # Which is exactly the false charge: the same image against itself at the + # wrong scale reads as substantially busier than the reference. + assert detail_excess(detail(as_png(small)), as_png(big)) > 1.0