417 lines
12 KiB
Python
417 lines
12 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
|
|
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 = "A0" # z.B. A0, A1, A2, A3, A4
|
|
ORIENTATION = "portrait" # "portrait" oder "landscape"
|
|
|
|
NUM_ARUCOS = 60
|
|
ARUCO_SIZE_MM = 25.0
|
|
ARUCO_START_ID = 46 # 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
|
|
TEXT_FONT = "Times-Roman"
|
|
TEXT_SIZE_PT = 8
|
|
TEXT_GAP_MM = 4.0
|
|
|
|
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
|
|
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() |