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>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user