Auto-select digit template font

This commit is contained in:
scootz 2026-05-01 13:25:01 +01:00
parent 3216f4a7a2
commit 53d1077a22
2 changed files with 94 additions and 15 deletions

View file

@ -29,6 +29,8 @@ make
No sudo install is required for the default Portal/PipeWire capture path.
The digit matcher uses ImageMagick to render reference digits. By default it auto-selects an installed font, preferring Roboto Condensed when present and falling back to common system fonts such as DejaVu Sans.
## Use
Preferred command:
@ -61,6 +63,12 @@ Save the captured input image while debugging:
uv run reforger_queue_read.py --portal-window --save-input /tmp/apt-portal-window.png --show-crop --debug
```
If font matching is weird on a specific machine, pass an installed ImageMagick font explicitly:
```bash
uv run reforger_queue_read.py --portal-window --font DejaVu-Sans-Condensed --show-crop --debug
```
## Optional KDE/KWin Mode
KWin mode can automatically find the Arma window by KWin metadata and can rebind after the game restarts. It is KDE-specific and requires installing an authorized helper.

View file

@ -16,8 +16,20 @@ import uuid
DEFAULT_REFERENCE_SIZE = (2560, 1440)
DEFAULT_REFERENCE_CROP = (1050, 620, 100, 60)
DEFAULT_FONT = "Roboto-Condensed"
DEFAULT_FONT = "auto"
DEFAULT_POINTSIZE = 43
AUTO_FONT_CANDIDATES = (
"Roboto-Condensed",
"RobotoCondensed-Regular",
"Roboto-Condensed-Regular",
"Noto-Sans-Condensed",
"NotoSans-Condensed",
"Liberation-Sans-Narrow",
"DejaVu-Sans-Condensed",
"DejaVu-Sans",
"Arial",
"Helvetica",
)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SYSTEM_KWIN_HELPER = "/usr/local/bin/reforger-kwin-capture"
DEFAULT_PORTAL_RESTORE_TOKEN = os.path.expanduser(
@ -507,8 +519,7 @@ def save_obs_scene_screenshot_with_setup(
time.sleep(0.5)
def ppm_from_magick(args):
data = run_bytes(["magick", *args, "-alpha", "off", "-depth", "8", "ppm:-"])
def parse_ppm(data):
idx = 0
def next_token():
@ -540,6 +551,44 @@ def ppm_from_magick(args):
return width, height, data[idx:]
def ppm_from_magick(args):
return parse_ppm(run_bytes(["magick", *args, "-alpha", "off", "-depth", "8", "ppm:-"]))
def try_ppm_from_magick(args):
command = ["magick", *args, "-alpha", "off", "-depth", "8", "ppm:-"]
try:
result = subprocess.run(command, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except FileNotFoundError:
sys.exit("missing required command: magick")
if result.returncode != 0:
return None, result.stderr.decode("utf-8", "replace").strip()
try:
return parse_ppm(result.stdout), ""
except Exception as exc:
return None, str(exc)
def magick_fonts():
fonts = set()
for line in run_text(["magick", "-list", "font"]).splitlines():
match = re.match(r"\s*Font:\s*(\S+)", line)
if match:
fonts.add(match.group(1))
return fonts
def template_font_candidates(font):
if font != "auto":
return [font]
available = magick_fonts()
candidates = [candidate for candidate in AUTO_FONT_CANDIDATES if candidate in available]
return [*candidates, None]
def image_size(image_path):
output = run_bytes(["magick", "identify", "-format", "%w %h", image_path]).decode("ascii")
width, height = output.split()
@ -657,24 +706,46 @@ def template_distance(left, right):
return hamming, iou
def build_templates(font, pointsize):
width, height, raw = ppm_from_magick(
def build_template_args(font, pointsize):
args = [
"-background",
"black",
"-fill",
"white",
]
if font:
args.extend(["-font", font])
args.extend(
[
"-background",
"black",
"-fill",
"white",
"-font",
font,
"-pointsize",
str(pointsize),
"label:0123456789",
]
)
components = connected_components(white_mask(width, height, raw))
if len(components) < 10:
sys.exit(f"template rendering produced only {len(components)} digit components")
return {str(index): normalize(component) for index, component in enumerate(components[:10])}
return args
def build_templates(font, pointsize):
errors = []
for candidate in template_font_candidates(font):
ppm, error = try_ppm_from_magick(build_template_args(candidate, pointsize))
if ppm is None:
errors.append(f"{candidate or 'ImageMagick default'}: {error}")
continue
width, height, raw = ppm
components = connected_components(white_mask(width, height, raw))
if len(components) < 10:
errors.append(f"{candidate or 'ImageMagick default'}: rendered only {len(components)} digit components")
continue
return {str(index): normalize(component) for index, component in enumerate(components[:10])}
if font == "auto":
sys.exit("could not render digit templates with any available ImageMagick font:\n " + "\n ".join(errors))
sys.exit(
f"could not render digit templates with font {font!r}. "
"Install that font, use --font auto, or pass another ImageMagick font name."
)
def classify(component, templates):