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>
55 lines
2.1 KiB
Python
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
|