Scene Roadmap
This commit is contained in:
417
setup/generateTabletopPDF/a3_aruco.py
Normal file
417
setup/generateTabletopPDF/a3_aruco.py
Normal file
@@ -0,0 +1,417 @@
|
||||
#!/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
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
|
||||
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 = "A3" # z.B. A0, A1, A2, A3, A4
|
||||
ORIENTATION = "portrait" # "portrait" oder "landscape"
|
||||
|
||||
NUM_ARUCOS = 10
|
||||
ARUCO_SIZE_MM = 50.0
|
||||
ARUCO_START_ID = 46 # erster Marker aus DICT_4X4_250
|
||||
SEED = 224 # Zufalls-Seed für reproduzierbare Verteilung
|
||||
|
||||
PAGE_BORDER_MARGIN_MM = 10.0 # Abstand aller Marker vom Seitenrand
|
||||
|
||||
FORBIDDEN_RECT_W_MM = 10.0
|
||||
FORBIDDEN_RECT_H_MM = 10.0
|
||||
FORBIDDEN_RECT_MARGIN_MM = 30.0 # keine ArUcos innerhalb dieses Abstands
|
||||
|
||||
LINE_WIDTH_MM = 1.0 # Linienstärke des Rechtecks
|
||||
TEXT_FONT = "Times-Roman"
|
||||
TEXT_SIZE_PT = 8
|
||||
TEXT_GAP_MM = 4.0
|
||||
|
||||
OUTPUT_BASENAME = f"{PAGE_FORMAT}_{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
|
||||
cell_y_mm = y_mm + 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 draw_aruco_label(
|
||||
c: canvas.Canvas,
|
||||
x_mm: float,
|
||||
y_mm: float,
|
||||
size_mm: float,
|
||||
page_h_mm: float,
|
||||
marker_id: int,
|
||||
) -> None:
|
||||
c.setFont(TEXT_FONT, TEXT_SIZE_PT)
|
||||
text = str(marker_id)
|
||||
text_width_pt = pdfmetrics.stringWidth(text, TEXT_FONT, TEXT_SIZE_PT)
|
||||
text_x_pt = mm_to_pt(x_mm + size_mm / 2.0) - text_width_pt / 2.0
|
||||
font = pdfmetrics.getFont(TEXT_FONT)
|
||||
ascent_mm = (font.face.ascent / 1000.0) * TEXT_SIZE_PT * 0.352777777777778
|
||||
text_baseline_y_mm = y_mm + size_mm + TEXT_GAP_MM + ascent_mm
|
||||
c.drawString(text_x_pt, mm_to_pt(page_h_mm - text_baseline_y_mm), text)
|
||||
|
||||
|
||||
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 rechts)
|
||||
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,
|
||||
)
|
||||
draw_aruco_label(
|
||||
c=c,
|
||||
x_mm=item["x_mm"],
|
||||
y_mm=item["y_mm"],
|
||||
size_mm=item["size_mm"],
|
||||
page_h_mm=page_h_mm,
|
||||
marker_id=item["id"],
|
||||
)
|
||||
|
||||
c.showPage()
|
||||
c.save()
|
||||
|
||||
# JSON mit Positionen
|
||||
with json_path.open("w", encoding="utf-8") as f:
|
||||
meta = {
|
||||
"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,
|
||||
}
|
||||
|
||||
f.write(json.dumps(meta, indent=2, ensure_ascii=False)[:-2])
|
||||
f.write(',\n "placements": [\n')
|
||||
|
||||
for index, p in enumerate(placements):
|
||||
item = {
|
||||
"id": p["id"],
|
||||
"position": [
|
||||
round((p["y_mm"] + ARUCO_SIZE_MM / 2) - origin_y_mm, 2),
|
||||
-1*round(origin_x_mm - (p["x_mm"] + ARUCO_SIZE_MM / 2), 2),
|
||||
-27.3,
|
||||
],
|
||||
"normal": [0, 0, 1],
|
||||
"spin": 90,
|
||||
}
|
||||
line = json.dumps(item, ensure_ascii=False)
|
||||
if index < len(placements) - 1:
|
||||
line += ","
|
||||
f.write(f" {line}\n")
|
||||
|
||||
f.write(" ]\n}\n")
|
||||
|
||||
print(f"PDF geschrieben: {pdf_path.resolve()}")
|
||||
print(f"JSON geschrieben: {json_path.resolve()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user