FEnigma/tests/test_map_vision_marker_id.py
Dominik Roth 086b871e3a Add marker id detection; log unit_score/margin; use full-res captures
read_marker_id (map_vision.py) reads each marker's own small "#<N>" id
label via template correlation, same approach as read_cell_label and
for the same documented reason (this text sits over the same aerial-
photo backdrop that defeated detection-based approaches for grid
labels). Wired end-to-end: find_markers -> Proposal.detected_id ->
save_marker_ground_truth's JSON. Reads against ScreenshotImport's
full_image when available, since the id text is tiny. Crop region and
threshold are a single-screenshot calibration, not yet validated
against real ground truth (documented as such).

Also switches save_marker_ground_truth/save_grid_correction to use
full_image over the WORK_W-downscaled image, so a human reviewing a
capture can actually read the small id text well enough to judge it.

Logs unit_score/unit_margin on every Proposal too (previously only
pass/fail `unit` was recorded), and measured current type-detection
reliability against the 6 existing ground-truth captures: 0/72 (0%)
accepted proposals had any confident detected_unit at all, not just
wrong guesses -- classify_marker never clears its own confidence floor
against real screenshots. Findings and next steps in TODO.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 18:54:53 +02:00

55 lines
2.1 KiB
Python

"""read_marker_id: template-correlation read of a marker's own small
"#<N>" id label (see map_vision.read_marker_id's own docstring for why
this is template correlation, not OCR -- same reasoning as
read_cell_label). Synthetic image, real font, no fixture screenshot or
the (slow) detection pipeline needed -- just render the label the way
the game does and check it round-trips.
"""
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from fenigma import map_vision
def _render_label(text: str, height: int) -> Image.Image:
"""Cream glyph, heavy dark outline, same style glyph_template expects
to correlate against -- see glyph_template's own docstring."""
font = ImageFont.truetype(str(map_vision.FONT_PATH), height)
pad = height
im = Image.new("L", (height * 4 + pad, height * 2 + pad), 30) # dark "photo" background
ImageDraw.Draw(im).text((pad // 2, pad // 4), text, font=font, fill=230,
stroke_width=max(1, int(height * 0.10)), stroke_fill=0)
return im
def test_reads_a_clean_id_label():
# A marker box roughly where a real one measures (see read_marker_id's
# own calibration note), with a rendered "#8" sitting where the game
# draws it: above-left of the box.
box_w, box_h = 40, 40
label_h = int(0.45 * box_h)
label_im = _render_label("#8", label_h)
canvas = Image.new("L", (200, 200), 60)
label_x, label_y = 60, 60
canvas.paste(label_im, (label_x, label_y))
gray = np.array(canvas)
box_x = label_x + int(1.0 * box_w) - 5 # box sits to the right of/below the label
box_y = label_y + int(0.45 * box_h)
box = (box_x, box_y, box_w, box_h)
assert map_vision.read_marker_id(gray, box) == "8"
def test_returns_none_on_a_blank_patch():
gray = np.full((200, 200), 60, dtype=np.uint8)
box = (100, 100, 40, 40)
assert map_vision.read_marker_id(gray, box) is None
def test_returns_none_on_a_degenerate_box_at_the_image_edge():
gray = np.full((200, 200), 60, dtype=np.uint8)
box = (0, 0, 2, 2) # crop region collapses to nothing usable
assert map_vision.read_marker_id(gray, box) is None