#!/usr/bin/env python3 from __future__ import annotations import json import random from dataclasses import dataclass from pathlib import Path from typing import List, Tuple from reportlab.pdfgen import canvas from reportlab.lib.units import mm try: import cv2 except ImportError as exc: raise SystemExit( "OpenCV ist erforderlich. Installiere es mit: pip install opencv-contrib-python" ) from exc # ========================= # Header / Parameter # ========================= PAGE_FORMAT = "A0" # z.B. A0, A1, A2, A3, A4 ORIENTATION = "portrait" # "portrait" oder "landscape" NUM_ARUCOS = 60 ARUCO_SIZE_MM = 25.0 ARUCO_START_ID = 103 # erster Marker aus DICT_4X4_250 SEED = 223 # Zufalls-Seed für reproduzierbare Verteilung PAGE_BORDER_MARGIN_MM = 50.0 # Abstand aller Marker vom Seitenrand FORBIDDEN_RECT_W_MM = 204.0 FORBIDDEN_RECT_H_MM = 1000.0 FORBIDDEN_RECT_MARGIN_MM = 30.0 # keine ArUcos innerhalb dieses Abstands LINE_WIDTH_MM = 1.0 # Linienstärke des Rechtecks OUTPUT_BASENAME = f"A0_{NUM_ARUCOS}Arucos_{int(ARUCO_SIZE_MM)}mm_Seet{SEED}" # ========================= # DIN-Formate # ========================= DIN_SIZES_MM = { "A0": (841.0, 1189.0), "A1": (594.0, 841.0), "A2": (420.0, 594.0), "A3": (297.0, 420.0), "A4": (210.0, 297.0), } @dataclass(frozen=True) class RectMM: x: float y: float w: float h: float def intersects(self, other: "RectMM") -> bool: return not ( self.x + self.w <= other.x or other.x + other.w <= self.x or self.y + self.h <= other.y or other.y + other.h <= self.y ) def mm_to_pt(value_mm: float) -> float: return value_mm * mm def get_page_size_mm(page_format: str, orientation: str) -> Tuple[float, float]: if page_format not in DIN_SIZES_MM: raise ValueError(f"Unbekanntes Format: {page_format}. Unterstützt: {sorted(DIN_SIZES_MM)}") w_mm, h_mm = DIN_SIZES_MM[page_format] if orientation.lower() == "portrait": return w_mm, h_mm if orientation.lower() == "landscape": return h_mm, w_mm raise ValueError("ORIENTATION muss 'portrait' oder 'landscape' sein.") def centered_rect(page_w_mm: float, page_h_mm: float, rect_w_mm: float, rect_h_mm: float) -> RectMM: return RectMM( x=(page_w_mm - rect_w_mm) / 2.0, y=(page_h_mm - rect_h_mm) / 2.0, w=rect_w_mm, h=rect_h_mm, ) def expand_rect(rect: RectMM, margin_mm: float) -> RectMM: return RectMM( x=rect.x - margin_mm, y=rect.y - margin_mm, w=rect.w + 2.0 * margin_mm, h=rect.h + 2.0 * margin_mm, ) def get_aruco_dictionary(): # DICT_4X4_250 hat IDs 0..249 return cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250) def marker_module_pattern(marker_id: int) -> List[List[int]]: """ Liefert ein 6x6-Raster (inkl. schwarzem Rand). 1 = schwarz, 0 = weiss """ aruco_dict = get_aruco_dictionary() img = cv2.aruco.generateImageMarker(aruco_dict, marker_id, 600) # nur zum Abtasten modules = 6 cell = img.shape[0] // modules pattern: List[List[int]] = [] for r in range(modules): row: List[int] = [] for c in range(modules): cy = int((r + 0.5) * cell) cx = int((c + 0.5) * cell) pixel = int(img[cy, cx]) # 0 = schwarz, 255 = weiss row.append(1 if pixel < 128 else 0) pattern.append(row) return pattern def draw_aruco_vector( c: canvas.Canvas, x_mm: float, y_mm: float, size_mm: float, marker_id: int, page_h_mm: float, ) -> None: """ Zeichnet den Marker als Vektor-Rechtecke. x_mm, y_mm = linke obere Ecke in mm. """ pattern = marker_module_pattern(marker_id) modules = len(pattern) cell_mm = size_mm / modules for r in range(modules): for col in range(modules): if pattern[r][col] == 1: cell_x_mm = x_mm + col * cell_mm cell_y_mm = y_mm + (modules - 1 - r) * cell_mm c.rect( mm_to_pt(cell_x_mm), mm_to_pt(page_h_mm - cell_y_mm - cell_mm), mm_to_pt(cell_mm), mm_to_pt(cell_mm), stroke=0, fill=1, ) def place_markers( page_w_mm: float, page_h_mm: float, num_markers: int, marker_size_mm: float, border_margin_mm: float, forbidden_area: RectMM, forbidden_margin_mm: float, seed: int, ) -> List[dict]: rng = random.Random(seed) placed: List[RectMM] = [] result: List[dict] = [] allowed_x_min = border_margin_mm allowed_y_min = border_margin_mm allowed_x_max = page_w_mm - border_margin_mm - marker_size_mm allowed_y_max = page_h_mm - border_margin_mm - marker_size_mm if allowed_x_max < allowed_x_min or allowed_y_max < allowed_y_min: raise RuntimeError("Das Poster ist zu klein für Randabstand und Markergrösse.") excluded = expand_rect(forbidden_area, forbidden_margin_mm) attempts = 0 max_attempts = 200000 while len(result) < num_markers and attempts < max_attempts: attempts += 1 x = rng.uniform(allowed_x_min, allowed_x_max) y = rng.uniform(allowed_y_min, allowed_y_max) candidate = RectMM(x=x, y=y, w=marker_size_mm, h=marker_size_mm) if any(candidate.intersects(other) for other in placed): continue if candidate.intersects(excluded): continue placed.append(candidate) result.append( { "id": ARUCO_START_ID + len(result), "x_mm": round(x, 2), "y_mm": round(y, 2), "size_mm": marker_size_mm, } ) if len(result) < num_markers: raise RuntimeError( f"Nicht alle Marker konnten platziert werden: {len(result)} von {num_markers} " f"(nach {attempts} Versuchen)." ) return result def main() -> None: page_w_mm, page_h_mm = get_page_size_mm(PAGE_FORMAT, ORIENTATION) if ARUCO_START_ID < 0 or ARUCO_START_ID + NUM_ARUCOS > 250: raise ValueError("ARUCO_START_ID + NUM_ARUCOS muss in den Bereich 0..249 von DICT_4X4_250 fallen.") forbidden_rect = centered_rect(page_w_mm, page_h_mm, FORBIDDEN_RECT_W_MM, FORBIDDEN_RECT_H_MM) # Neues lokales Koordinatensystem origin_x_mm = forbidden_rect.x + forbidden_rect.w - 90.0 origin_y_mm = forbidden_rect.y placements = place_markers( page_w_mm=page_w_mm, page_h_mm=page_h_mm, num_markers=NUM_ARUCOS, marker_size_mm=ARUCO_SIZE_MM, border_margin_mm=PAGE_BORDER_MARGIN_MM, forbidden_area=forbidden_rect, forbidden_margin_mm=FORBIDDEN_RECT_MARGIN_MM, seed=SEED, ) pdf_path = Path(f"{OUTPUT_BASENAME}.pdf") json_path = Path(f"{OUTPUT_BASENAME}.json") c = canvas.Canvas(str(pdf_path), pagesize=(mm_to_pt(page_w_mm), mm_to_pt(page_h_mm))) c.setTitle(pdf_path.stem) c.setAuthor("OpenAI") c.setSubject("A0 poster with random ArUco placement") # Weisser Hintergrund c.setFillColorRGB(1, 1, 1) c.rect(0, 0, mm_to_pt(page_w_mm), mm_to_pt(page_h_mm), stroke=0, fill=1) # Rechteck mit 1 mm schwarzer Linie c.setStrokeColorRGB(0, 0, 0) c.setLineWidth(mm_to_pt(LINE_WIDTH_MM)) c.rect( mm_to_pt(forbidden_rect.x), mm_to_pt(page_h_mm - forbidden_rect.y - forbidden_rect.h), mm_to_pt(forbidden_rect.w), mm_to_pt(forbidden_rect.h), stroke=1, fill=0, ) # Koordinatensystem ARROW_LEN_MM = 50.0 # X-Achse (rot, nach unten) c.setStrokeColorRGB(1, 0, 0) c.setLineWidth(mm_to_pt(1.0)) c.line( mm_to_pt(origin_x_mm), mm_to_pt(page_h_mm - origin_y_mm), mm_to_pt(origin_x_mm), mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM)), ) # Pfeilspitze c.line( mm_to_pt(origin_x_mm), mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM)), mm_to_pt(origin_x_mm - 4), mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM - 4)), ) c.line( mm_to_pt(origin_x_mm), mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM)), mm_to_pt(origin_x_mm + 4), mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM - 4)), ) # Y-Achse (grün, nach links) c.setStrokeColorRGB(0, 0.7, 0) c.line( mm_to_pt(origin_x_mm), mm_to_pt(page_h_mm - origin_y_mm), mm_to_pt(origin_x_mm - ARROW_LEN_MM), mm_to_pt(page_h_mm - origin_y_mm), ) # Pfeilspitze c.line( mm_to_pt(origin_x_mm - ARROW_LEN_MM), mm_to_pt(page_h_mm - origin_y_mm), mm_to_pt(origin_x_mm - ARROW_LEN_MM + 4), mm_to_pt(page_h_mm - origin_y_mm - 4), ) c.line( mm_to_pt(origin_x_mm - ARROW_LEN_MM), mm_to_pt(page_h_mm - origin_y_mm), mm_to_pt(origin_x_mm - ARROW_LEN_MM + 4), mm_to_pt(page_h_mm - origin_y_mm + 4), ) c.setStrokeColorRGB(0, 0, 0) # ArUcos zeichnen c.setFillColorRGB(0, 0, 0) for item in placements: draw_aruco_vector( c=c, x_mm=item["x_mm"], y_mm=item["y_mm"], size_mm=item["size_mm"], marker_id=item["id"], page_h_mm=page_h_mm, ) c.showPage() c.save() # JSON mit Positionen with json_path.open("w", encoding="utf-8") as f: json.dump( { "page_format": PAGE_FORMAT, "orientation": ORIENTATION, "page_size_mm": {"width": page_w_mm, "height": page_h_mm}, "seed": SEED, "num_arucos": NUM_ARUCOS, "aruco_size_mm": ARUCO_SIZE_MM, "aruco_dictionary": "DICT_4X4_250", "aruco_start_id": ARUCO_START_ID, "page_border_margin_mm": PAGE_BORDER_MARGIN_MM, "forbidden_rectangle_mm": { "x": round(forbidden_rect.x, 2), "y": round(forbidden_rect.y, 2), "w": forbidden_rect.w, "h": forbidden_rect.h, }, "forbidden_rectangle_margin_mm": FORBIDDEN_RECT_MARGIN_MM, "placements": [ {"id": p["id"],"position": [-1*round((p["y_mm"] + ARUCO_SIZE_MM / 2) - origin_y_mm, 2), round(origin_x_mm - (p["x_mm"] + ARUCO_SIZE_MM / 2), 2),-19],"normal": [0, 0, 1],"spin": 90} for p in placements ], }, f, indent=2, ensure_ascii=False, ) print(f"PDF geschrieben: {pdf_path.resolve()}") print(f"JSON geschrieben: {json_path.resolve()}") if __name__ == "__main__": main()