Spaces:
Sleeping
Sleeping
| """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() | |