import json import os import subprocess import shutil from pathlib import Path import argparse STATE_KEYS = ("x", "y", "z", "a", "b", "c", "e") def update_robot_json(robot_json_file, camera_position, camera_target, default_position, width, height, fstop): """Aktualisiert cameraPosition/Target, Auflösung und defaultPosition in robot.json. Hinweis: render_robot.py liest robot.json bei jedem Blender-Start frisch ein, deshalb wird hier pro Kamera in die Datei geschrieben. """ try: with open(robot_json_file, 'r', encoding='utf-8') as f: data = json.load(f) data['renderingInfo']['cameraPosition'] = camera_position data['renderingInfo']['cameraTarget'] = camera_target data['renderingInfo']['width'] = width data['renderingInfo']['height'] = height data['renderingInfo']['dofFStop'] = fstop data['defaultPosition'] = default_position with open(robot_json_file, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2) except FileNotFoundError: print(f"Fehler: Datei {robot_json_file} nicht gefunden.") return False except json.JSONDecodeError: print(f"Fehler: JSON-Datei {robot_json_file} ist ungültig.") return False return True def run_blender(blender_executable, script_path, log_level): """Führt Blender mit dem angegebenen Skript aus.""" try: command = [ blender_executable, "-b", "--python", script_path, "--log-level", str(log_level) ] subprocess.run(command, check=True, capture_output=True, text=True) return True except subprocess.CalledProcessError as e: print(f"Blender-Skript fehlgeschlagen:\n{e.stderr}") return False def copy_and_rename_file(source_file, destination_dir, new_filename): """Kopiert die erstellte Datei in den Zielordner und benennt sie um.""" destination_path = os.path.join(destination_dir, new_filename) try: shutil.copy2(source_file, destination_path) # copy2 behält Metadaten return True except FileNotFoundError: print(f"Fehler: Quelldatei {source_file} nicht gefunden.") return False except Exception as e: print(f"Fehler beim Kopieren/Umbenennen der Datei: {e}") return False def load_render_config(robot_json_file): """Liest Posen, Kamera-Setup und Render-Defaults aus robot.json.""" with open(robot_json_file, 'r', encoding='utf-8') as f: data = json.load(f) poses = data.get("robot_test_poses", {}) or {} cam_positions = data.get("test_camera_positions", {}) or {} cam_targets = data.get("test_camera_targets", {}) or {} rinfo = data.get("renderingInfo", {}) or {} # WICHTIG: renderingInfo.width/height/dofFStop sind TRANSIENT — update_robot_json # überschreibt sie bei jedem Render. Als Default für Posen ohne eigenes "rendering" # daher das STABILE Feld 'renderDefaults' verwenden, sonst "vergiftet" ein # Override-Render (z.B. 9b mit 4896x3264) den Default für alle folgenden Läufe. rd = rinfo.get("renderDefaults", {}) or {} default_rendering = { "width": int(rd.get("width", 1280)), "height": int(rd.get("height", 720)), "dofFStop": float(rd.get("dofFStop", 11)), } return poses, cam_positions, cam_targets, default_rendering def split_pose(pose_entry): """Trennt eine robot_test_poses-Definition in (joint_state, rendering_override).""" joint_state = {k: pose_entry[k] for k in STATE_KEYS if k in pose_entry} rendering_override = pose_entry.get("rendering", {}) or {} return joint_state, rendering_override def main(): USER_HOME = Path.home() BASE = Path(__file__).resolve().parents[2] BLENDER_EXE = os.environ.get("BLENDER_EXE", "C:/Program Files/Blender Foundation/Blender 4.5/blender.exe") ROBOT_JSON_FILE = str(BASE / "data" / "robot" / "robot.json") OUTPUT_DIR = str(BASE / "data" / "simulation" / "debug") RENDER_PY = str(BASE / "setup" / "generateSets" / "render_robot.py") RENDER_PNG = str(BASE / "data" / "simulation" / "debug" / "render.png") OUTPUT_SET = str(BASE / "data" / "simulation") parser = argparse.ArgumentParser(description="Automatisiert die Roboter-Rendering-Pipeline.") parser.add_argument("robot_json", nargs="?", default=ROBOT_JSON_FILE, help="Pfad zur robot.json-Datei.") parser.add_argument("blender_executable", nargs="?", default=BLENDER_EXE, help="Pfad zur Blender-Executable.") parser.add_argument("render_script", nargs="?", default=RENDER_PY, help="Pfad zum render_robot.py-Skript.") parser.add_argument("render_png", nargs="?", default=RENDER_PNG, help="vom Skript erzeugte png") parser.add_argument("output_dir", nargs="?", default=OUTPUT_DIR, help="Zielordner für die gerenderten Blender Bilder.") parser.add_argument("output_set", nargs="?", default=OUTPUT_SET, help="Zielordner in dem die Sets abgelegt werden") parser.add_argument("--poses", nargs="*", default=None, help="Nur diese Pose-Keys rendern (z.B. --poses 8 9). Default: alle aus robot_test_poses.") parser.add_argument("--scene_prefix", default="Scene", help="Präfix für die Scene-Ordnernamen (Default: Scene).") parser.add_argument("--log_level", type=int, default=2, help="Log-Level für Blender (Standard: 2).") args = parser.parse_args() # Konfiguration aus robot.json robot_poses, camera_positions, camera_targets, default_rendering = load_render_config(args.robot_json) if not robot_poses: print("Keine 'robot_test_poses' in robot.json gefunden.") return if not camera_positions: print("Keine 'test_camera_positions' in robot.json gefunden.") return # Optionaler Pose-Filter if args.poses: missing = [p for p in args.poses if p not in robot_poses] for p in missing: print(f"[WARN] Pose '{p}' nicht in robot_test_poses – wird übersprungen.") selected = {p: robot_poses[p] for p in args.poses if p in robot_poses} else: selected = robot_poses print(f"Zu rendernde Posen: {list(selected.keys())}") print(f"Kameras pro Pose: {list(camera_positions.keys())}") for pose_name, pose_entry in selected.items(): joint_state, rendering_override = split_pose(pose_entry) # Render-Settings: globaler Default, pro Pose überschreibbar rendering = dict(default_rendering) rendering.update({k: rendering_override[k] for k in ("width", "height", "dofFStop") if k in rendering_override}) new_folder = Path(args.output_set) / f"{args.scene_prefix}{pose_name}" os.makedirs(new_folder, exist_ok=True) print(f"\n=== Scene {pose_name} | {rendering['width']}x{rendering['height']} f/{rendering['dofFStop']} ===") # Ground-Truth-Record: Joint-Pose + Kamera-Setup (für spätere Messung/GT-Posen) pose_file = new_folder / "pose.json" pose_data = { "name": pose_name, "position": joint_state, "rendering": rendering, "camera_positions": camera_positions, "camera_targets": camera_targets, } with open(pose_file, "w", encoding="utf-8") as f: json.dump(pose_data, f, indent=2) for frame_name, camera_position in camera_positions.items(): camera_target = camera_targets.get(frame_name) if camera_target is None: print(f"[WARN] Kein Target für Kamera '{frame_name}' – übersprungen.") continue # 1. robot.json für diesen Render aktualisieren if not update_robot_json(args.robot_json, camera_position, camera_target, joint_state, rendering["width"], rendering["height"], rendering["dofFStop"]): continue # 2. Blender ausführen if not run_blender(args.blender_executable, args.render_script, args.log_level): continue # 3. Ergebnisse kopieren und umbenennen (png, npz, markers.json) new_filename = f"render_{frame_name}.png" if not copy_and_rename_file(args.render_png, new_folder, new_filename): continue if not copy_and_rename_file(args.render_png.replace(".png", ".npz"), new_folder, new_filename.replace(".png", ".npz")): continue if not copy_and_rename_file(args.render_png.replace("render.png", "markers.json"), new_folder, new_filename.replace(".png", ".json")): continue print(f" Frame {frame_name}: OK") print(f"Scene {pose_name} fertig -> {new_folder}") if __name__ == "__main__": main()