Files
appRobotRender/setup/generateTabletopPDF/a0_aruco.py
2026-05-30 15:47:58 +02:00

374 lines
11 KiB
Python

#!/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()