Files
appRobotRender/pipeline/marker_sets.py
2026-06-10 07:13:19 +02:00

262 lines
11 KiB
Python

#!/usr/bin/env python3
"""
marker_sets.py
==============
Klassifiziert die Marker aus robot.json in drei Rollen für die Szenen-Kalibrierung
(siehe doc/callibrate_scene_roadmap.md, Phase P0):
arm : Marker an BEWEGLICHEN Roboter-Links (Arm1, Ellbow, Arm2, Finger, ...).
Ihre Lage je Link ist bekannt -> Welt-Referenz, wird NICHT kalibriert.
set : Marker an einem STATISCHEN Link (Welt-Wurzel) MIT "set"-Feld. Gleiches
"set" = ein starres Objekt mit fixen relativen Bezügen -> es wird nur die
6-DoF-Platzierung des ganzen Sets kalibriert.
loose : Marker an einem statischen Link OHNE "set"-Feld. Lose, einzeln zu vermessen.
Die Set-Zugehörigkeit steht ausschließlich in robot.json (optionales Feld "set" am
Marker). Hier wird nichts hartkodiert — die Sets ergeben sich per Auto-Discovery aus
den vorhandenen "set"-Werten.
"Beweglich" = es liegt irgendwo auf der Kette bis zur Wurzel ein revolute/linear-Joint.
Damit zählt die Wurzel (Board) und jeder rein über "fixed"-Joints angebundene Link als
statisch (Welt), alles ab dem ersten Gelenk als Arm.
Public API
----------
data = load_robot("data/robot/robot.json")
cls = classify_markers(data) # id -> MarkerInfo
sets = get_sets(data) # set_name -> [MarkerInfo, ...]
loose = get_loose_markers(data) # [MarkerInfo, ...]
arm = get_arm_markers(data) # id -> MarkerInfo
rep = set_summary(data) # serialisierbarer Report (für QA / --json)
CLI
---
python pipeline/marker_sets.py -robot data/robot/robot.json
python pipeline/marker_sets.py -robot data/robot/robot.json --json
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, asdict
from typing import Any, Dict, List, Optional
# ──────────────────────────────────────────────────────────────
# Datentyp
# ──────────────────────────────────────────────────────────────
@dataclass
class MarkerInfo:
id: int
link: str
role: str # "arm" | "set" | "loose"
set_name: Optional[str] # nur bei role == "set"
position: List[float] # im Link-Frame (mm), wie in robot.json
normal: Optional[List[float]]
spin: Optional[float]
# ──────────────────────────────────────────────────────────────
# Laden
# ──────────────────────────────────────────────────────────────
def load_robot(path: str) -> Dict[str, Any]:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
# ──────────────────────────────────────────────────────────────
# Link-Topologie: statisch (Welt) vs. beweglich (Arm)
# ──────────────────────────────────────────────────────────────
_MOVABLE_JOINTS = ("revolute", "linear")
def link_is_movable(links: Dict[str, Any], name: str) -> bool:
"""
True, wenn auf der Kette von `name` bis zur Wurzel mindestens ein
revolute/linear-Joint liegt (Marker dort gehören zum beweglichen Roboter).
"""
seen = set()
cur: Optional[str] = name
while cur and cur in links and cur not in seen:
seen.add(cur)
joint = links[cur].get("jointToParent") or {}
if str(joint.get("type", "")).lower() in _MOVABLE_JOINTS:
return True
cur = links[cur].get("parent")
return False
def root_links(links: Dict[str, Any]) -> List[str]:
return [n for n, l in links.items()
if not l.get("parent") or l.get("parent") not in links]
# ──────────────────────────────────────────────────────────────
# Klassifizierung
# ──────────────────────────────────────────────────────────────
def _set_name_of(marker: Dict[str, Any]) -> Optional[str]:
val = marker.get("set")
if val is None:
return None
s = str(val).strip()
return s or None
def classify_markers(robot_data: Dict[str, Any]) -> Dict[int, MarkerInfo]:
"""
Liefert id -> MarkerInfo über alle Links. Bei doppelten IDs gewinnt das erste
Vorkommen (zusätzlich als Warnung in set_summary sichtbar).
"""
links = robot_data.get("links", {}) or {}
out: Dict[int, MarkerInfo] = {}
for link_name, link in links.items():
movable = link_is_movable(links, link_name)
for mk in link.get("markers", []) or []:
if "id" not in mk or "position" not in mk:
continue
mid = int(mk["id"])
if mid in out:
continue # erstes Vorkommen behalten
set_name = _set_name_of(mk)
if movable:
role = "arm"
set_name = None
elif set_name is not None:
role = "set"
else:
role = "loose"
normal = mk.get("normal")
spin = mk.get("spin")
out[mid] = MarkerInfo(
id=mid,
link=link_name,
role=role,
set_name=set_name,
position=[float(v) for v in mk["position"]],
normal=[float(v) for v in normal] if normal is not None else None,
spin=float(spin) if spin is not None else None,
)
return out
def get_arm_markers(robot_data: Dict[str, Any]) -> Dict[int, MarkerInfo]:
return {m.id: m for m in classify_markers(robot_data).values() if m.role == "arm"}
def get_sets(robot_data: Dict[str, Any]) -> Dict[str, List[MarkerInfo]]:
"""set_name -> Liste der Marker (role == 'set'), gruppiert nach 'set'-Wert."""
sets: Dict[str, List[MarkerInfo]] = {}
for m in classify_markers(robot_data).values():
if m.role == "set" and m.set_name is not None:
sets.setdefault(m.set_name, []).append(m)
return sets
def get_loose_markers(robot_data: Dict[str, Any]) -> List[MarkerInfo]:
return [m for m in classify_markers(robot_data).values() if m.role == "loose"]
# ──────────────────────────────────────────────────────────────
# QA / Report
# ──────────────────────────────────────────────────────────────
def _duplicate_ids(robot_data: Dict[str, Any]) -> List[int]:
links = robot_data.get("links", {}) or {}
seen, dups = set(), set()
for link in links.values():
for mk in link.get("markers", []) or []:
if "id" not in mk:
continue
mid = int(mk["id"])
(dups if mid in seen else seen).add(mid)
return sorted(dups)
def set_summary(robot_data: Dict[str, Any]) -> Dict[str, Any]:
cls = classify_markers(robot_data)
sets = get_sets(robot_data)
loose = get_loose_markers(robot_data)
arm = [m for m in cls.values() if m.role == "arm"]
warnings: List[str] = []
for mid in _duplicate_ids(robot_data):
warnings.append(f"Marker-ID {mid} kommt mehrfach vor (erstes Vorkommen gewertet)")
for name, members in sets.items():
links_used = sorted({m.link for m in members})
if len(members) < 3:
warnings.append(
f"Set '{name}' hat nur {len(members)} Marker — 6-DoF-Platzierung "
f"nicht voll bestimmbar (>=3 nicht-kollineare nötig)")
if len(links_used) > 1:
warnings.append(f"Set '{name}' verteilt sich über mehrere Links {links_used} "
f"— ein Set sollte ein physisches Objekt sein")
return {
"counts": {
"total": len(cls),
"arm": len(arm),
"set": sum(len(v) for v in sets.values()),
"loose": len(loose),
"num_sets": len(sets),
},
"root_links": root_links(robot_data.get("links", {}) or {}),
"sets": {name: sorted(m.id for m in members) for name, members in sorted(sets.items())},
"loose_ids": sorted(m.id for m in loose),
"arm_ids": sorted(m.id for m in arm),
"warnings": warnings,
}
# ──────────────────────────────────────────────────────────────
# CLI
# ──────────────────────────────────────────────────────────────
def main() -> None:
ap = argparse.ArgumentParser(description="Marker aus robot.json in arm/set/loose klassifizieren")
ap.add_argument("-robot", "--robot", required=True, help="Pfad zu robot.json")
ap.add_argument("--json", action="store_true", help="Report als JSON ausgeben")
args = ap.parse_args()
data = load_robot(args.robot)
rep = set_summary(data)
if args.json:
print(json.dumps(rep, indent=2, ensure_ascii=False))
return
c = rep["counts"]
print(f"robot.json: {args.robot}")
print(f"Wurzel-Link(s): {rep['root_links']}")
print(f"\nMarker gesamt: {c['total']} | arm: {c['arm']} set: {c['set']} "
f"loose: {c['loose']} (Sets: {c['num_sets']})")
print("\nSets (starr, fixe interne Lage -> 6-DoF kalibrieren):")
if rep["sets"]:
for name, ids in rep["sets"].items():
print(f" {name:10s} ({len(ids):3d}): {ids}")
else:
print(" (keine)")
print(f"\nLose Marker (einzeln zu vermessen): {rep['loose_ids'] or '(keine)'}")
print(f"Arm-Marker (Welt-Referenz, nicht kalibriert): {rep['arm_ids']}")
if rep["warnings"]:
print("\n[WARN]")
for w in rep["warnings"]:
print(f" - {w}")
if __name__ == "__main__":
try:
main()
except Exception as exc: # pragma: no cover
print(f"[ERROR] {exc}", file=sys.stderr)
sys.exit(1)