#!/usr/bin/env python3 """ 2_estimate_camera_pose_from_aruco_json.py Berechnet die Kameraposition im Maschinen-/Board-Koordinatensystem aus: 1. einer ArUco-Detections-JSON 2. robots.json mit bekannten Marker-Positionen NEU: - Marker-Orientierungen unterstützt - Default: Board-Marker zeigen nach +Z - Qualitätsbewertung erweitert - Speichert ALLE erkannten Marker - Speichert auch fehlgeschlagene Lösungen - Bewertet Kamerageometrie - Bewertet Markerabdeckung - Bewertet Sichtwinkel - Bewertet Markeranzahl - Speichert vollständige Rohdaten Benötigt: pip install opencv-python numpy """ import argparse import json import math from pathlib import Path import cv2 import numpy as np # ============================================================ # Hilfsfunktionen # ============================================================ def normalize(v): n = np.linalg.norm(v) if n < 1e-9: return v return v / n def rotation_matrix_from_axes(x_axis, y_axis, z_axis): R = np.column_stack([ normalize(x_axis), normalize(y_axis), normalize(z_axis) ]) return R.astype(np.float32) def rvec_tvec_to_camera_pose(rvec, tvec): """ OpenCV: X_cam = R * X_world + t Kamera im Weltkoordinatensystem: C = -R^T * t """ R_cw, _ = cv2.Rodrigues(rvec) R_wc = R_cw.T cam_pos = -R_wc @ tvec return R_wc, cam_pos.reshape(3) def rotation_matrix_to_euler_zyx(R): yaw = math.degrees(math.atan2(R[1, 0], R[0, 0])) sp = math.sqrt(R[2, 1] ** 2 + R[2, 2] ** 2) pitch = math.degrees(math.atan2(-R[2, 0], sp)) roll = math.degrees(math.atan2(R[2, 1], R[2, 2])) return roll, pitch, yaw # ============================================================ # Marker-Orientierung # ============================================================ def get_marker_rotation(marker): # Explizite Rotation vorhanden? if "rotation_matrix" in marker: return np.array( marker["rotation_matrix"], dtype=np.float32 ) # Default: # Marker zeigt nach +Z # # x = rechts # y = oben # z = aus Board heraus (+Z) x_axis = np.array([1, 0, 0], dtype=np.float32) y_axis = np.array([0, 1, 0], dtype=np.float32) z_axis = np.array([0, 0, 1], dtype=np.float32) return rotation_matrix_from_axes( x_axis, y_axis, z_axis ) # ============================================================ # Marker Lookup # ============================================================ def build_marker_lookup(robot_data): marker_lookup = {} length_units = str(robot_data.get("units", {}).get("length", "")).strip().lower() length_scale = 1.0 if length_units in ("mm", "millimeter", "millimeters"): length_scale = 1.0 / 1000.0 elif length_units in ("cm", "centimeter", "centimeters"): length_scale = 1.0 / 100.0 markers = [] # Neues Format: links -> Board -> markers links = robot_data.get("links", {}) board = links.get("Board") if board and "markers" in board: markers = board["markers"] # Fallback für altes Format if not markers: markers = robot_data.get("Marker", []) for marker in markers: marker_id = int(marker.get("id", -1)) if marker_id < 0: continue # Nur absolute Marker verwenden if "position" not in marker: continue pos = marker["position"] if pos is None: continue if len(pos) != 3: continue rotation = get_marker_rotation(marker) marker_lookup[marker_id] = { "position": np.array( pos, dtype=np.float32 ) * np.float32(length_scale), "rotation": rotation, "on": marker.get("on", "unknown") } return marker_lookup # ============================================================ # Marker-Eckpunkte # ============================================================ def build_marker_object_points( marker_center_world, marker_rotation, marker_size_m): half = marker_size_m / 2.0 # OpenCV Corner Reihenfolge local = np.array([ [-half, half, 0.0], [ half, half, 0.0], [ half, -half, 0.0], [-half, -half, 0.0], ], dtype=np.float32) rotated = (marker_rotation @ local.T).T return rotated + marker_center_world.reshape(1, 3) # ============================================================ # Qualitätsmetriken # ============================================================ def compute_marker_spread(points_3d): if len(points_3d) < 2: return 0.0 mins = np.min(points_3d, axis=0) maxs = np.max(points_3d, axis=0) diag = np.linalg.norm(maxs - mins) return float(diag) def compute_viewing_angles( camera_position, marker_lookup, used_markers): results = [] for marker_id in used_markers: marker = marker_lookup[marker_id] pos = marker["position"] R = marker["rotation"] normal = R[:, 2] to_camera = normalize(camera_position - pos) dot = np.clip( np.dot(normal, to_camera), -1.0, 1.0 ) angle = math.degrees(math.acos(dot)) results.append(angle) if len(results) == 0: return { "mean": None, "max": None } return { "mean": float(np.mean(results)), "max": float(np.max(results)) } def compute_pose_quality( rms, max_err, num_markers, marker_spread, viewing_angle_mean): score = 100.0 # Reprojection Error score -= rms * 8.0 # Max Error score -= max_err * 2.0 # Wenige Marker if num_markers < 2: score -= 50 elif num_markers < 4: score -= 25 elif num_markers < 6: score -= 10 # Schlechte räumliche Verteilung if marker_spread < 0.10: score -= 30 elif marker_spread < 0.25: score -= 15 # Schlechter Blickwinkel if viewing_angle_mean is not None: if viewing_angle_mean > 70: score -= 25 elif viewing_angle_mean > 50: score -= 10 score = max(0.0, min(100.0, score)) return float(score) # ============================================================ # Main # ============================================================ def main(): parser = argparse.ArgumentParser() parser.add_argument( "--detections", default=None ) parser.add_argument( "--robots", required=True ) parser.add_argument( "--min-confidence", type=float, default=0.5 ) parser.add_argument( "--max-reprojection-error", type=float, default=3.0 ) parser.add_argument( "--outDir", default=None ) args = parser.parse_args() # ============================================================ # JSON laden # ============================================================ with open(args.detections, "r", encoding="utf-8") as f: detection_data = json.load(f) with open(args.robots, "r", encoding="utf-8") as f: robot_data = json.load(f) # ============================================================ # Kamera # ============================================================ K = np.array( detection_data["camera"]["camera_matrix"], dtype=np.float32 ) D = np.array( detection_data["camera"]["distortion_coefficients"], dtype=np.float32 ).reshape(-1, 1) # ============================================================ # Marker laden # ============================================================ known_markers = build_marker_lookup(robot_data) # ============================================================ # Punktlisten # ============================================================ object_points = [] image_points = [] used_markers = [] rejected_markers = [] detections = detection_data["detections"] for det in detections: marker_id = int(det["marker_id"]) confidence = float( det.get("confidence", 1.0) ) reason = None if confidence < args.min_confidence: reason = "low_confidence" elif marker_id not in known_markers: reason = "unknown_marker" if reason is not None: rejected_markers.append({ "marker_id": marker_id, "reason": reason, "confidence": confidence }) continue marker_info = known_markers[marker_id] marker_center_world = marker_info["position"] marker_rotation = marker_info["rotation"] marker_size = float( det["marker_size_m"] ) obj_pts = build_marker_object_points( marker_center_world, marker_rotation, marker_size ) img_pts = np.array( det["image_points_px"], dtype=np.float32 ) object_points.append(obj_pts) image_points.append(img_pts) used_markers.append(marker_id) # ============================================================ # Ausgabe vorbereiten # ============================================================ out = { "success": False, "camera_pose": None, "quality": {}, "used_markers": [], "rejected_markers": rejected_markers, "all_detected_markers": [ int(d["marker_id"]) for d in detections ] } # ============================================================ # Zu wenige Marker # ============================================================ if len(object_points) == 0: out["quality"] = { "error": "no_known_markers", "num_detected_markers": len(detections), "num_used_markers": 0 } if args.outDir: out_dir = Path(args.outDir) out_dir.mkdir(parents=True, exist_ok=True) out_file = out_dir / ( Path(args.detections).stem + ".camera_pose.json" ) else: out_file = Path(args.detections).with_suffix( ".camera_pose.json" ) with open(out_file, "w", encoding="utf-8") as f: json.dump(out, f, indent=2) print("Keine bekannten Marker gefunden.") print(out_file) return # ============================================================ # solvePnP # ============================================================ object_points = np.concatenate( object_points, axis=0 ) image_points = np.concatenate( image_points, axis=0 ) success, rvec, tvec = cv2.solvePnP( object_points, image_points, K, D, flags=cv2.SOLVEPNP_ITERATIVE ) if not success: out["quality"] = { "error": "solvepnp_failed", "num_used_markers": len(set(used_markers)) } out_file = Path(args.detections).with_suffix( ".camera_pose.json" ) with open(out_file, "w", encoding="utf-8") as f: json.dump(out, f, indent=2) print("solvePnP fehlgeschlagen") return # ============================================================ # Kamera Pose # ============================================================ R_wc, cam_pos = rvec_tvec_to_camera_pose( rvec, tvec ) roll, pitch, yaw = rotation_matrix_to_euler_zyx( R_wc ) # ============================================================ # Reprojektion # ============================================================ projected, _ = cv2.projectPoints( object_points, rvec, tvec, K, D ) projected = projected.reshape(-1, 2) reproj_error = np.linalg.norm( projected - image_points, axis=1 ) rms = float( np.sqrt(np.mean(reproj_error ** 2)) ) max_err = float( np.max(reproj_error) ) # ============================================================ # Qualität # ============================================================ marker_positions = np.array([ known_markers[mid]["position"] for mid in set(used_markers) ]) marker_spread = compute_marker_spread( marker_positions ) viewing = compute_viewing_angles( cam_pos, known_markers, set(used_markers) ) quality_score = compute_pose_quality( rms, max_err, len(set(used_markers)), marker_spread, viewing["mean"] ) # ============================================================ # Ausgabe # ============================================================ print() print("==================================================") print("KAMERA-POSE") print("==================================================") print() print("Position [m]") print(f"X = {cam_pos[0]:.6f}") print(f"Y = {cam_pos[1]:.6f}") print(f"Z = {cam_pos[2]:.6f}") print() print("Orientation [deg]") print(f"Roll = {roll:.3f}") print(f"Pitch = {pitch:.3f}") print(f"Yaw = {yaw:.3f}") print() print("Reprojection") print(f"RMS = {rms:.3f}px") print(f"MAX = {max_err:.3f}px") print() print("Quality") print(f"Score = {quality_score:.1f}/100") print(f"Marker Spread = {marker_spread:.3f}m") if viewing["mean"] is not None: print( f"Mean Viewing Angle = " f"{viewing['mean']:.1f}deg" ) # ============================================================ # JSON speichern # ============================================================ out["success"] = True out["camera_pose"] = { "position_m": { "x": float(cam_pos[0]), "y": float(cam_pos[1]), "z": float(cam_pos[2]), }, "orientation_deg": { "roll": float(roll), "pitch": float(pitch), "yaw": float(yaw), }, "rotation_matrix_world_from_camera": ( R_wc.tolist() ) } out["quality"] = { "pose_quality_score": quality_score, "reprojection_rms_px": rms, "reprojection_max_px": max_err, "num_detected_markers": len(detections), "num_used_markers": len(set(used_markers)), "marker_spread_m": marker_spread, "mean_viewing_angle_deg": viewing["mean"], "max_viewing_angle_deg": viewing["max"], "pose_stable": max_err <= args.max_reprojection_error } out["used_markers"] = sorted( list(set(used_markers)) ) out_file = Path(args.detections).with_suffix( ".camera_pose.json" ) with open(out_file, "w", encoding="utf-8") as f: json.dump(out, f, indent=2) print() print("Gespeichert:") print(out_file) if __name__ == "__main__": main()