crack-detection / make_marker.py
calcent's picture
Deploy crack detection app
cd0d068 verified
Raw
History Blame Contribute Delete
4.38 kB
"""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()