"""Generate a printable ArUco marker for wall measurement. Run: ./venv/bin/python make_marker.py Output: aruco_marker_60mm_A4.pdf (and a .png preview) Print the PDF at 100% / "Actual size" (no scaling, no "fit to page"). Then verify with a ruler that the printed black square is exactly 6.0 cm across. If it is off, scaling was applied — reprint, or pass the real measured size to the app's "marker size" field. """ from __future__ import annotations import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont import aruco_scale DPI = 300 MM_PER_INCH = 25.4 MARKER_MM = aruco_scale.DEFAULT_MARKER_LENGTH_MM # 120 mm MARKER_ID = aruco_scale.DEFAULT_MARKER_ID # 0 A4_W_MM, A4_H_MM = 210.0, 297.0 def mm_to_px(mm: float) -> int: return int(round(mm / MM_PER_INCH * DPI)) def _font(size_px: int) -> ImageFont.FreeTypeFont: for path in ("/System/Library/Fonts/Supplemental/Arial.ttf", "/System/Library/Fonts/Helvetica.ttc", "/Library/Fonts/Arial.ttf"): try: return ImageFont.truetype(path, size_px) except OSError: continue return ImageFont.load_default() def build_marker_image(side_px: int) -> Image.Image: """Render the ArUco marker (6x6 modules) crisply at `side_px`.""" modules = 6 # 4x4 data + 1-module black border each side hires = modules * 200 raw = cv2.aruco.generateImageMarker(aruco_scale.get_dictionary(), MARKER_ID, hires) return Image.fromarray(raw).resize((side_px, side_px), Image.NEAREST) def main() -> None: page = Image.new("RGB", (mm_to_px(A4_W_MM), mm_to_px(A4_H_MM)), "white") draw = ImageDraw.Draw(page) cx = page.width // 2 title = _font(mm_to_px(7)) body = _font(mm_to_px(4.2)) small = _font(mm_to_px(3.4)) def centered(y, text, font, fill="black"): w = draw.textlength(text, font=font) draw.text((cx - w / 2, y), text, font=font, fill=fill) y = mm_to_px(15) centered(y, "Crack Measurement - ArUco Scale Marker", title) y += mm_to_px(11) centered(y, "1. Print this page at 100% / Actual Size (no scaling).", body) y += mm_to_px(6) centered(y, "2. Tape it FLAT on the wall, beside the crack, facing the camera.", body) y += mm_to_px(6) centered(y, "3. Photograph wall + marker together, then upload to the app.", body) y += mm_to_px(10) # The marker, with a white quiet zone the printed page already provides. side_px = mm_to_px(MARKER_MM) marker = build_marker_image(side_px) mx, my = cx - side_px // 2, y page.paste(marker, (mx, my)) # Dimension callout under the marker. yb = my + side_px + mm_to_px(4) draw.line([(mx, yb), (mx + side_px, yb)], fill="black", width=3) for ex in (mx, mx + side_px): draw.line([(ex, yb - mm_to_px(2)), (ex, yb + mm_to_px(2))], fill="black", width=3) centered(yb + mm_to_px(3), f"Black square = {MARKER_MM:.0f} mm ({MARKER_MM/10:.1f} cm)", body) y = yb + mm_to_px(14) centered(y, f"Dictionary: DICT_4X4_50 Marker ID: {MARKER_ID}", small) y += mm_to_px(10) # Independent 100 mm print-scale check ruler. centered(y, "Print check - this line must measure exactly 10.0 cm:", small) y += mm_to_px(6) ruler_px = mm_to_px(100.0) rx = cx - ruler_px // 2 draw.line([(rx, y), (rx + ruler_px, y)], fill="black", width=3) for i in range(11): # cm ticks tx = rx + mm_to_px(10.0 * i) th = mm_to_px(3.5 if i % 5 == 0 else 2.0) draw.line([(tx, y - th), (tx, y + th)], fill="black", width=2) y += mm_to_px(8) centered(y, "If it is not 10.0 cm, your printer rescaled the page - " "reprint without scaling.", small) # Save the PDF as a 1-bit (black/white) page: lossless CCITT compression, # crisp marker edges, small file. The page is pure line art so nothing # is lost. dither=NONE keeps text edges clean instead of speckled. stem = f"aruco_marker_{MARKER_MM:.0f}mm_A4" page.save(f"{stem}.png", dpi=(DPI, DPI)) page.convert("1", dither=Image.NONE).save( f"{stem}.pdf", resolution=float(DPI)) print(f"Wrote {stem}.pdf and {stem}.png") print(f"Marker: DICT_4X4_50 id={MARKER_ID} side={MARKER_MM:.0f} mm") if __name__ == "__main__": main()