| |
| """ |
| 全流程 Pipeline: .blend/.glb/.gltf/.ply → 边渲边选 → ERPT Warp |
| |
| 支持六种模式: |
| |
| 1. 单 Blend 场景: |
| python run_full_pipeline.py \ |
| --blender /path/to/blender \ |
| --blend /path/to/scene.blend \ |
| --scene-name my_scene \ |
| --output-root ./dataset |
| |
| 2. 批量 Blend(扫描 input-dir 下所有 .blend): |
| python run_full_pipeline.py \ |
| --blender /path/to/blender \ |
| --input-dir /path/to/blend_files/ \ |
| --output-root ./dataset |
| |
| 3. 单 GLB/GLTF 场景: |
| python run_full_pipeline.py \ |
| --blender /path/to/blender \ |
| --glb /path/to/scene.glb \ |
| --scene-name my_scene \ |
| --output-root ./dataset |
| |
| 4. 批量 GLB(扫描 input-dir 下所有 .glb/.gltf): |
| python run_full_pipeline.py \ |
| --blender /path/to/blender \ |
| --input-dir /path/to/glb_files/ \ |
| --output-root ./dataset |
| |
| 5. 单 PLY 场景(无需 Blender): |
| python run_full_pipeline.py \ |
| --ply /path/to/scene.ply \ |
| --scene-name my_scene \ |
| --output-root ./dataset |
| |
| 6. 批量 PLY(扫描 input-dir 下所有 .ply): |
| python run_full_pipeline.py \ |
| --input-dir /path/to/ply_files/ \ |
| --output-root ./dataset |
| |
| 加 --dry-run 预览要跑哪些场景 |
| 已跑完的场景自动跳过(--no-skip-done 强制重跑) |
| """ |
|
|
| import argparse |
| import json |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import time |
| from pathlib import Path |
|
|
|
|
| def run_step1_blend_pipeline( |
| blender_exe: str, |
| scene_path: str, |
| temp_dir: str, |
| num_frames: int, |
| resolution: str, |
| samples: int, |
| engine: str, |
| exposure: float, |
| grid_spacing: float, |
| camera_height, |
| stop_gain: float, |
| stop_score: float, |
| stop_delta: float, |
| min_frames: int, |
| rotation_type: str = "random_yaw", |
| gain_curve: bool = True, |
| scene_flag: str = "--blend", |
| ) -> int: |
| """步骤 1 (Blend/GLB): 调 run_blend_pipeline.py 边渲边选。 |
| scene_flag: "--blend" 或 "--glb" |
| """ |
| script = Path(__file__).parent / "run_blend_pipeline.py" |
| if not script.exists(): |
| raise FileNotFoundError(f"找不到 run_blend_pipeline.py: {script}") |
|
|
| cmd = [ |
| sys.executable, str(script), |
| "--blender", blender_exe, |
| scene_flag, scene_path, |
| "--output-dir", temp_dir, |
| "--num-frames", str(num_frames), |
| "--render-depth", |
| "--resolution", resolution, |
| "--samples", str(samples), |
| "--engine", engine, |
| "--exposure", str(exposure), |
| "--grid-spacing", str(grid_spacing), |
| "--stop-gain", str(stop_gain), |
| "--stop-score", str(stop_score), |
| "--stop-delta", str(stop_delta), |
| "--min-frames", str(min_frames), |
| "--rotation-type", rotation_type, |
| ] |
| if camera_height is not None: |
| cmd += ["--camera-height", str(camera_height)] |
| if not gain_curve: |
| cmd += ["--no-gain-curve"] |
|
|
| print(f"\n{'='*60}") |
| print("[Step 1] 边渲边选 (Blender Cycles)") |
| print(f"{'='*60}") |
|
|
| proc = subprocess.run(cmd, text=True) |
| if proc.returncode != 0: |
| print(f" [Error] run_blend_pipeline 退出码: {proc.returncode}") |
| return proc.returncode |
|
|
| n = sum(1 for f in Path(temp_dir).glob("panorama_*.png")) |
| print(f" 渲染完成: {n} 帧") |
| return 0 |
|
|
|
|
| def run_step1_ply_pipeline( |
| ply_path: str, |
| temp_dir: str, |
| num_frames: int, |
| resolution: str, |
| grid_spacing: float, |
| camera_height, |
| stop_gain: float, |
| stop_score: float, |
| stop_delta: float, |
| min_frames: int, |
| rotation_type: str = "random_yaw", |
| point_size: float = 2.0, |
| z_up: bool = True, |
| ) -> int: |
| """步骤 1 (PLY): 调 run_ply_pipeline.py 边渲边选(无需 Blender)""" |
| script = Path(__file__).parent / "run_ply_pipeline.py" |
| if not script.exists(): |
| raise FileNotFoundError(f"找不到 run_ply_pipeline.py: {script}") |
|
|
| cmd = [ |
| sys.executable, str(script), |
| "--ply", ply_path, |
| "--output-dir", temp_dir, |
| "--num-frames", str(num_frames), |
| "--resolution", resolution, |
| "--grid-spacing", str(grid_spacing), |
| "--stop-gain", str(stop_gain), |
| "--stop-score", str(stop_score), |
| "--stop-delta", str(stop_delta), |
| "--min-frames", str(min_frames), |
| "--rotation-type", rotation_type, |
| "--point-size", str(point_size), |
| ] |
| if camera_height is not None: |
| cmd += ["--camera-height", str(camera_height)] |
| if not z_up: |
| cmd += ["--no-z-up"] |
|
|
| print(f"\n{'='*60}") |
| print("[Step 1] 边渲边选 (PLY 点云)") |
| print(f"{'='*60}") |
|
|
| proc = subprocess.run(cmd, text=True) |
| if proc.returncode != 0: |
| print(f" [Error] run_ply_pipeline 退出码: {proc.returncode}") |
| return proc.returncode |
|
|
| n = sum(1 for f in Path(temp_dir).glob("panorama_*.png")) |
| print(f" 渲染完成: {n} 帧") |
| return 0 |
|
|
|
|
| def run_step1_hm3d_pipeline( |
| blender_exe: str, |
| scene_path: str, |
| temp_dir: str, |
| num_frames: int, |
| resolution: str, |
| samples: int, |
| engine: str, |
| exposure: float, |
| grid_spacing: float, |
| camera_height, |
| stop_gain: float, |
| stop_score: float, |
| stop_delta: float, |
| min_frames: int, |
| rotation_type: str = "random_yaw", |
| gain_curve: bool = True, |
| ) -> int: |
| """步骤 1 (HM3D GLB): 调 run_hm3d_pipeline.py 边渲边选。""" |
| script = Path(__file__).parent / "run_hm3d_pipeline.py" |
| if not script.exists(): |
| raise FileNotFoundError(f"找不到 run_hm3d_pipeline.py: {script}") |
|
|
| cmd = [ |
| sys.executable, str(script), |
| "--blender", blender_exe, |
| "--glb", scene_path, |
| "--output-dir", temp_dir, |
| "--num-frames", str(num_frames), |
| "--render-depth", |
| "--resolution", resolution, |
| "--samples", str(samples), |
| "--engine", engine, |
| "--exposure", str(exposure), |
| "--grid-spacing", str(grid_spacing), |
| "--stop-gain", str(stop_gain), |
| "--stop-score", str(stop_score), |
| "--stop-delta", str(stop_delta), |
| "--min-frames", str(min_frames), |
| "--rotation-type", rotation_type, |
| "--hm3d", "True", |
| ] |
| if camera_height is not None: |
| cmd += ["--camera-height", str(camera_height)] |
| if not gain_curve: |
| cmd += ["--no-gain-curve"] |
|
|
| print(f"\n{'='*60}") |
| print("[Step 1] 边渲边选 (HM3D GLB)") |
| print(f"{'='*60}") |
|
|
| proc = subprocess.run(cmd, text=True) |
| if proc.returncode != 0: |
| print(f" [Error] run_hm3d_pipeline 退出码: {proc.returncode}") |
| return proc.returncode |
|
|
| n = sum(1 for f in Path(temp_dir).rglob("panorama_*.png")) |
| print(f" 渲染完成: {n} 帧") |
| return 0 |
|
|
|
|
| def run_step2_organize_hm3d(temp_dir: str, scene_dir: str) -> int: |
| """步骤 2 (HM3D): 整理多空间目录结构 |
| |
| temp_dir 里有: |
| frame_selection/ |
| space_00/ (panorama_*.png, *_depth.npy, pose_*.json) |
| space_01/ |
| ... |
| |
| 整理成(每个 space 一个独立目录): |
| scene_dir/space_00/input/ → 中心帧 RGB + depth + 所有 pose |
| scene_dir/space_00/output/ → 所有帧 RGB + depth(GT 真值) |
| scene_dir/space_01/input/ |
| scene_dir/space_01/output/ |
| ... |
| scene_dir/frame_selection/ → 选帧信息 |
| """ |
| temp = Path(temp_dir) |
|
|
| print(f"\n{'='*60}") |
| print("[Step 2] 整理目录结构 (HM3D 多空间)") |
| print(f"{'='*60}") |
|
|
| space_dirs = sorted( |
| [d for d in temp.iterdir() if d.is_dir() and d.name.startswith("space_")] |
| ) |
| if not space_dirs: |
| print(" [Error] 没有找到 space_XX 目录") |
| return 1 |
|
|
| print(f" 共 {len(space_dirs)} 个空间") |
|
|
| for space_d in space_dirs: |
| space_name = space_d.name |
|
|
| rgb_files = sorted(space_d.glob("panorama_*.png")) |
| if not rgb_files: |
| print(f" {space_name}: 无渲染结果,跳过") |
| continue |
|
|
| n_frames = len(rgb_files) |
| out_space_dir = Path(scene_dir) / space_name |
| inp_dir = out_space_dir / "input" |
| out_dir = out_space_dir / "output" |
| inp_dir.mkdir(parents=True, exist_ok=True) |
| out_dir.mkdir(parents=True, exist_ok=True) |
|
|
| for rgb_path in rgb_files: |
| shutil.copy2(str(rgb_path), str(out_dir / rgb_path.name)) |
| depth_path = space_d / rgb_path.name.replace(".png", "_depth.npy") |
| if depth_path.exists(): |
| shutil.copy2(str(depth_path), str(out_dir / depth_path.name)) |
|
|
| center_rgb = space_d / "panorama_0000.png" |
| center_depth = space_d / "panorama_0000_depth.npy" |
| if center_rgb.exists(): |
| shutil.copy2(str(center_rgb), str(inp_dir / center_rgb.name)) |
| if center_depth.exists(): |
| shutil.copy2(str(center_depth), str(inp_dir / center_depth.name)) |
|
|
| n_pose = 0 |
| for pose_path in sorted(space_d.glob("pose_*.json")): |
| shutil.copy2(str(pose_path), str(inp_dir / pose_path.name)) |
| n_pose += 1 |
|
|
| print(f" {space_name}: {n_frames} 帧 → output/, 中心帧 + {n_pose} pose → input/") |
|
|
| sel_dir = Path(scene_dir) / "frame_selection" |
| sel_dir.mkdir(parents=True, exist_ok=True) |
|
|
| sel_json = temp / "frame_selection" / "selected_frames.json" |
| if sel_json.exists(): |
| shutil.copy2(str(sel_json), str(sel_dir / "selected_frames.json")) |
|
|
| cand_npy = temp / "frame_selection" / "candidates_filtered.npy" |
| if cand_npy.exists(): |
| shutil.copy2(str(cand_npy), str(sel_dir / "candidates_filtered.npy")) |
|
|
| return 0 |
|
|
|
|
| def run_step2_organize(temp_dir: str, scene_dir: str) -> int: |
| """步骤 2: 整理目录结构 |
| |
| temp_dir 里有: |
| panorama_0000.png, panorama_0000_depth.npy, pose_0000.json, ... |
| |
| 整理成: |
| scene_dir/input/ → 中心帧 RGB + depth + 所有 pose(供 ERPT warp 使用) |
| scene_dir/output/ → 所有帧 RGB + depth(GT 真值) |
| """ |
| temp = Path(temp_dir) |
| inp_dir = Path(scene_dir) / "input" |
| out_dir = Path(scene_dir) / "output" |
| inp_dir.mkdir(parents=True, exist_ok=True) |
| out_dir.mkdir(parents=True, exist_ok=True) |
|
|
| print(f"\n{'='*60}") |
| print("[Step 2] 整理目录结构") |
| print(f"{'='*60}") |
|
|
| |
| rgb_files = sorted(temp.glob("panorama_*.png")) |
| if not rgb_files: |
| print(" [Error] 没有找到渲染的全景图") |
| return 1 |
|
|
| n_frames = len(rgb_files) |
| print(f" 共 {n_frames} 帧") |
|
|
| |
| for rgb_path in rgb_files: |
| shutil.copy2(str(rgb_path), str(out_dir / rgb_path.name)) |
| |
| depth_path = temp / rgb_path.name.replace(".png", "_depth.npy") |
| if depth_path.exists(): |
| shutil.copy2(str(depth_path), str(out_dir / depth_path.name)) |
|
|
| print(f" output/: {n_frames} 帧 RGB + depth") |
|
|
| |
| center_rgb = temp / "panorama_0000.png" |
| center_depth = temp / "panorama_0000_depth.npy" |
|
|
| if center_rgb.exists(): |
| shutil.copy2(str(center_rgb), str(inp_dir / center_rgb.name)) |
| if center_depth.exists(): |
| shutil.copy2(str(center_depth), str(inp_dir / center_depth.name)) |
|
|
| |
| n_pose = 0 |
| for pose_path in sorted(temp.glob("pose_*.json")): |
| shutil.copy2(str(pose_path), str(inp_dir / pose_path.name)) |
| n_pose += 1 |
|
|
| print(f" input/: 中心帧 + {n_pose} 个 pose") |
|
|
| |
| sel_dir = inp_dir / "frame_selection" |
| sel_dir.mkdir(parents=True, exist_ok=True) |
|
|
| sel_json = temp / "frame_selection" / "selected_frames.json" |
| if sel_json.exists(): |
| shutil.copy2(str(sel_json), str(sel_dir / "selected_frames.json")) |
|
|
| cand_npy = temp / "frame_selection" / "candidates_filtered.npy" |
| if cand_npy.exists(): |
| shutil.copy2(str(cand_npy), str(sel_dir / "candidates_filtered.npy")) |
|
|
| |
| if sel_json.exists(): |
| try: |
| draw_gain_curve(str(sel_dir / "selected_frames.json"), |
| str(sel_dir / "gain_curve.jpg")) |
| print(f" 增益曲线: {sel_dir}/gain_curve.jpg") |
| except Exception as e: |
| print(f" [跳过] 画增益曲线失败: {e}") |
|
|
| return 0 |
|
|
|
|
| def draw_gain_curve(json_path, output_path): |
| """画增益曲线(优先 matplotlib,fallback PIL)""" |
| with open(json_path) as f: |
| data = json.load(f) |
|
|
| frames = [fr for fr in data["frames"] if not fr.get("skipped")] |
| if len(frames) < 2: |
| return |
|
|
| fids = [fr["frame_id"] for fr in frames] |
| pred_gains = [fr["gain"] for fr in frames] |
| actual_gains = [fr["actual_gain"] for fr in frames] |
| scores = [fr["score"] for fr in frames] |
| deltas = [fr["delta_ratio"] for fr in frames] |
|
|
| try: |
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
|
|
| fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), sharex=True) |
|
|
| ax1.plot(fids, pred_gains, 'o-', color='#2196F3', label='predicted', markersize=3, linewidth=1.5) |
| ax1.plot(fids, actual_gains, 'o-', color='#FF9800', label='actual', markersize=3, linewidth=1.5) |
| ax1.axhline(y=0.05, color='red', linestyle='--', alpha=0.5, label='stop_gain=5%') |
| ax1.set_ylabel('gain') |
| ax1.set_ylim(-0.05, 1.05) |
| ax1.legend(loc='upper right', fontsize=9) |
| ax1.set_title(f'Gain Curve ({len(frames)} frames)', fontsize=11) |
| ax1.grid(True, alpha=0.3) |
| |
| ax1.annotate(f'{actual_gains[0]:.0%}', (fids[0], actual_gains[0]), |
| textcoords="offset points", xytext=(5, 5), fontsize=7, color='#FF9800') |
| ax1.annotate(f'{actual_gains[-1]:.0%}', (fids[-1], actual_gains[-1]), |
| textcoords="offset points", xytext=(-25, 5), fontsize=7, color='#FF9800') |
|
|
| ax2.plot(fids, scores, 'D-', color='#4CAF50', label='score', markersize=3, linewidth=1.5) |
| ax2.plot(fids, deltas, 's-', color='#9C27B0', label='delta', markersize=2, linewidth=1.2) |
| ax2.axhline(y=-0.33, color='red', linestyle='--', alpha=0.5, label='stop_score=-0.33') |
| ax2.axhline(y=0.01, color='#9C27B0', linestyle=':', alpha=0.4, label='stop_delta=1%') |
| ax2.set_ylabel('value') |
| ax2.set_xlabel('frame') |
| ax2.legend(loc='upper right', fontsize=9) |
| ax2.grid(True, alpha=0.3) |
| |
| ax2.annotate(f'{deltas[0]:.1%}', (fids[0], deltas[0]), |
| textcoords="offset points", xytext=(5, 5), fontsize=7, color='#9C27B0') |
| ax2.annotate(f'{deltas[-1]:.1%}', (fids[-1], deltas[-1]), |
| textcoords="offset points", xytext=(-25, -10), fontsize=7, color='#9C27B0') |
|
|
| plt.tight_layout() |
| plt.savefig(output_path, dpi=150, bbox_inches='tight') |
| plt.close() |
| return |
|
|
| except ImportError: |
| pass |
|
|
| |
| try: |
| from PIL import Image, ImageDraw, ImageFont |
|
|
| W, H = 800, 500 |
| ML, MR, MT, MB = 50, 20, 30, 25 |
| MID = H // 2 |
| pw = W - ML - MR |
|
|
| img = Image.new("RGB", (W, H), "white") |
| draw = ImageDraw.Draw(img) |
| try: |
| font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 9) |
| except Exception: |
| font = ImageFont.load_default() |
|
|
| n = len(frames) |
|
|
| def px(i, v, y0, y1, vmin, vmax): |
| x = ML + int(i / max(n-1,1) * pw) |
| y = y0 + int((1 - (v-vmin)/(vmax-vmin)) * (y1-y0)) |
| return x, max(y0, min(y1, y)) |
|
|
| def line(pts, color, y0, y1, vmin, vmax): |
| for j in range(len(pts)-1): |
| draw.line([px(j,pts[j],y0,y1,vmin,vmax), px(j+1,pts[j+1],y0,y1,vmin,vmax)], fill=color, width=2) |
|
|
| line(pred_gains, "#2196F3", MT, MID-5, 0, 1.05) |
| line(actual_gains, "#FF9800", MT, MID-5, 0, 1.05) |
| line(scores, "#4CAF50", MID+10, H-MB, -0.6, 1.05) |
| line(deltas, "#9C27B0", MID+10, H-MB, -0.6, 1.05) |
|
|
| draw.text((ML, MT-12), f"Gain ({n} frames)", fill="black", font=font) |
| draw.text((ML, MID+2), "Score / Delta", fill="black", font=font) |
| img.save(output_path, quality=90) |
|
|
| except ImportError: |
| pass |
|
|
|
|
| def run_step3_erpt_warp_hm3d(scene_dir: str, device: str = "cuda") -> int: |
| """步骤 3 (HM3D): 对每个空间调 run_pipeline.py 执行 ERPT warp |
| |
| 遍历 scene_dir/space_XX/input/,对帧数 >= 2 的空间生成 warp 文件 |
| """ |
| script = Path(__file__).parent / "run_pipeline.py" |
| if not script.exists(): |
| raise FileNotFoundError(f"找不到 run_pipeline.py: {script}") |
|
|
| print(f"\n{'='*60}") |
| print("[Step 3] ERPT Warp (HM3D 多空间)") |
| print(f"{'='*60}") |
|
|
| scene_path = Path(scene_dir) |
| space_dirs = sorted( |
| [d for d in scene_path.iterdir() |
| if d.is_dir() and d.name.startswith("space_")] |
| ) |
|
|
| if not space_dirs: |
| print(" [Error] 没有找到 space_XX 目录") |
| return 1 |
|
|
| total_ret = 0 |
| n_warped = 0 |
| n_skipped = 0 |
|
|
| for space_d in space_dirs: |
| inp_dir = space_d / "input" |
| if not inp_dir.exists(): |
| continue |
|
|
| n_poses = len(list(inp_dir.glob("pose_*.json"))) |
| if n_poses < 2: |
| print(f" {space_d.name}: {n_poses} pose,跳过 warp") |
| n_skipped += 1 |
| continue |
|
|
| print(f"\n [{space_d.name}] ERPT Warp ({n_poses} poses)...") |
|
|
| cmd = [ |
| sys.executable, str(script), |
| "--stage", "warp_only", |
| "--data_dir", str(inp_dir), |
| "--output_dir", str(inp_dir), |
| "--device", device, |
| "--center_frame", "0", |
| ] |
|
|
| proc = subprocess.run(cmd, text=True) |
| if proc.returncode != 0: |
| print(f" [Error] {space_d.name} warp 失败 (退出码: {proc.returncode})") |
| total_ret = proc.returncode |
| continue |
|
|
| warp_rgb_dir = inp_dir / "warp_rgb" |
| warp_depth_dir = inp_dir / "warp_depth" |
| keep_suffixes = ("_rgb.png", "_mask.png", "_depth_range.npy") |
|
|
| n_moved = 0 |
| for subdir in [warp_rgb_dir, warp_depth_dir]: |
| if subdir.exists(): |
| for f in subdir.iterdir(): |
| if f.is_file() and any(f.name.endswith(s) for s in keep_suffixes): |
| shutil.move(str(f), str(inp_dir / f.name)) |
| n_moved += 1 |
| shutil.rmtree(str(subdir), ignore_errors=True) |
|
|
| n_warped += 1 |
| print(f" warp 文件: {n_moved} 个") |
|
|
| print(f"\n Warp 完成: {n_warped} 个空间, 跳过 {n_skipped} 个") |
| return total_ret |
|
|
|
|
| def run_step3_erpt_warp(scene_dir: str, device: str = "cuda") -> int: |
| """步骤 3: 调 run_pipeline.py 执行 ERPT warp |
| |
| 读取 scene_dir/input/ 里的中心帧 + pose → 生成 warp 文件 |
| warp 文件直接写到 input/ 目录 |
| """ |
| script = Path(__file__).parent / "run_pipeline.py" |
| if not script.exists(): |
| raise FileNotFoundError(f"找不到 run_pipeline.py: {script}") |
|
|
| inp_dir = Path(scene_dir) / "input" |
|
|
| print(f"\n{'='*60}") |
| print("[Step 3] ERPT Warp") |
| print(f"{'='*60}") |
|
|
| cmd = [ |
| sys.executable, str(script), |
| "--stage", "warp_only", |
| "--data_dir", str(inp_dir), |
| "--output_dir", str(inp_dir), |
| "--device", device, |
| "--center_frame", "0", |
| ] |
|
|
| proc = subprocess.run(cmd, text=True) |
| if proc.returncode != 0: |
| print(f" [Error] run_pipeline 退出码: {proc.returncode}") |
| return proc.returncode |
|
|
| |
| |
| warp_rgb_dir = inp_dir / "warp_rgb" |
| warp_depth_dir = inp_dir / "warp_depth" |
| keep_suffixes = ("_rgb.png", "_mask.png", "_depth_range.npy") |
|
|
| n_moved = 0 |
| for subdir in [warp_rgb_dir, warp_depth_dir]: |
| if subdir.exists(): |
| for f in subdir.iterdir(): |
| if f.is_file() and any(f.name.endswith(s) for s in keep_suffixes): |
| shutil.move(str(f), str(inp_dir / f.name)) |
| n_moved += 1 |
| |
| shutil.rmtree(str(subdir), ignore_errors=True) |
|
|
| print(f" warp 文件已移到 input/: {n_moved} 个") |
| return 0 |
|
|
|
|
| def is_already_done(output_root, scene_name): |
| """检查是否已经跑完""" |
| sel_path = os.path.join( |
| output_root, scene_name, "input", "frame_selection", |
| "selected_frames.json") |
| if not os.path.exists(sel_path): |
| |
| sel_path = os.path.join(output_root, scene_name, "input", |
| "selected_frames.json") |
| if not os.path.exists(sel_path): |
| return False |
| try: |
| with open(sel_path) as f: |
| data = json.load(f) |
| return data.get("total_frames", 0) > 0 |
| except Exception: |
| return False |
|
|
|
|
| def find_glb_files(input_dir): |
| """递归查找所有 .glb / .gltf 文件,返回 [(glb_path, scene_name), ...] |
| |
| scene_name 取文件名(不含扩展名),或第一级子目录名(如有子目录)。 |
| """ |
| input_dir = os.path.abspath(input_dir) |
| glb_files = [] |
| for root, dirs, files in os.walk(input_dir): |
| for f in files: |
| if f.lower().endswith(".glb") or f.lower().endswith(".gltf"): |
| glb_path = os.path.join(root, f) |
| rel = os.path.relpath(root, input_dir) |
| if rel == ".": |
| scene_name = os.path.splitext(f)[0] |
| else: |
| scene_name = rel.split(os.sep)[0] |
| glb_files.append((glb_path, scene_name)) |
| glb_files.sort(key=lambda x: x[1]) |
| return glb_files |
|
|
|
|
| def find_blend_files(input_dir): |
| """递归查找所有 .blend 文件,返回 [(blend_path, scene_name), ...] |
| |
| scene_name 取 input_dir 下的第一级子目录名(scene_indoor_XXXX), |
| 不管 .blend 文件嵌套了几层。 |
| |
| 例如: |
| input_dir = /path/to/dataset/indoor |
| .blend 在 /path/to/dataset/indoor/scene_indoor_0001/1407m1/xxx.blend |
| → scene_name = scene_indoor_0001 |
| """ |
| input_dir = os.path.abspath(input_dir) |
| blend_files = [] |
| for root, dirs, files in os.walk(input_dir): |
| for f in files: |
| if f.endswith(".blend"): |
| blend_path = os.path.join(root, f) |
| rel = os.path.relpath(root, input_dir) |
| scene_name = rel.split(os.sep)[0] |
| blend_files.append((blend_path, scene_name)) |
| blend_files.sort(key=lambda x: x[1]) |
| return blend_files |
|
|
|
|
| def find_ply_files(input_dir): |
| """递归查找所有 .ply 文件,返回 [(ply_path, scene_name), ...] |
| |
| scene_name 取文件名(不含扩展名),或第一级子目录名(如有子目录)。 |
| """ |
| input_dir = os.path.abspath(input_dir) |
| ply_files = [] |
| for root, dirs, files in os.walk(input_dir): |
| for f in files: |
| if f.lower().endswith(".ply"): |
| ply_path = os.path.join(root, f) |
| rel = os.path.relpath(root, input_dir) |
| if rel == ".": |
| scene_name = os.path.splitext(f)[0] |
| else: |
| scene_name = rel.split(os.sep)[0] |
| ply_files.append((ply_path, scene_name)) |
| ply_files.sort(key=lambda x: x[1]) |
| return ply_files |
|
|
|
|
| def run_single_scene(args, scene_path, scene_name, scene_type="blend"): |
| """跑单个场景,返回 0=成功 / 非0=失败 |
| |
| scene_type: "blend" | "glb" | "hm3d" | "ply" |
| """ |
| output_root = str(Path(args.output_root).resolve()) |
| scene_dir = os.path.join(output_root, scene_name) |
| temp_dir = os.path.join(scene_dir, "_render_temp") |
| os.makedirs(scene_dir, exist_ok=True) |
|
|
| type_labels = { |
| "blend": ".blend (Blender Cycles)", |
| "glb": ".glb/.gltf (Blender Cycles)", |
| "hm3d": ".glb/.gltf (HM3D 渲染)", |
| "ply": ".ply (点云渲染)", |
| } |
| type_label = type_labels.get(scene_type, scene_type) |
| print("=" * 60) |
| print(f"全流程 Pipeline: {type_label} → 边渲边选 → ERPT Warp") |
| print("=" * 60) |
| print(f" Scene: {scene_name}") |
| print(f" Input: {scene_path}") |
| print(f" Output: {scene_dir}/") |
| t_start = time.time() |
|
|
| |
| if scene_type == "hm3d": |
| ret = run_step1_hm3d_pipeline( |
| blender_exe=args.blender, |
| scene_path=scene_path, |
| temp_dir=temp_dir, |
| num_frames=args.num_frames, |
| resolution=args.resolution, |
| samples=args.samples, |
| engine=args.engine, |
| exposure=args.exposure, |
| grid_spacing=args.grid_spacing, |
| camera_height=args.camera_height, |
| stop_gain=args.stop_gain, |
| stop_score=args.stop_score, |
| stop_delta=args.stop_delta, |
| min_frames=args.min_frames, |
| rotation_type=args.rotation_type, |
| gain_curve=getattr(args, "gain_curve", True), |
| ) |
| elif scene_type in ("blend", "glb"): |
| scene_flag = "--blend" if scene_type == "blend" else "--glb" |
| ret = run_step1_blend_pipeline( |
| blender_exe=args.blender, |
| scene_path=scene_path, |
| temp_dir=temp_dir, |
| num_frames=args.num_frames, |
| resolution=args.resolution, |
| samples=args.samples, |
| engine=args.engine, |
| exposure=args.exposure, |
| grid_spacing=args.grid_spacing, |
| camera_height=args.camera_height, |
| stop_gain=args.stop_gain, |
| stop_score=args.stop_score, |
| stop_delta=args.stop_delta, |
| min_frames=args.min_frames, |
| rotation_type=args.rotation_type, |
| gain_curve=getattr(args, "gain_curve", True), |
| scene_flag=scene_flag, |
| ) |
| else: |
| ret = run_step1_ply_pipeline( |
| ply_path=scene_path, |
| temp_dir=temp_dir, |
| num_frames=args.num_frames, |
| resolution=args.resolution, |
| grid_spacing=args.grid_spacing, |
| camera_height=args.camera_height, |
| stop_gain=args.stop_gain, |
| stop_score=args.stop_score, |
| stop_delta=args.stop_delta, |
| min_frames=args.min_frames, |
| rotation_type=args.rotation_type, |
| point_size=getattr(args, "point_size", 2.0), |
| z_up=getattr(args, "z_up", True), |
| ) |
|
|
| if ret != 0: |
| print(f"[Error] Step 1 失败") |
| return ret |
|
|
| |
| if scene_type == "hm3d": |
| ret = run_step2_organize_hm3d(temp_dir, scene_dir) |
| else: |
| ret = run_step2_organize(temp_dir, scene_dir) |
| if ret != 0: |
| print(f"[Error] Step 2 失败") |
| return ret |
|
|
| |
| if not args.skip_warp: |
| if scene_type == "hm3d": |
| ret = run_step3_erpt_warp_hm3d(scene_dir, device=args.device) |
| else: |
| ret = run_step3_erpt_warp(scene_dir, device=args.device) |
| if ret != 0: |
| print(f"[Error] Step 3 失败") |
|
|
| |
| if os.path.exists(temp_dir): |
| shutil.rmtree(temp_dir, ignore_errors=True) |
|
|
| dt = time.time() - t_start |
| print(f"\n{'='*60}") |
| print(f"完成! {scene_name}, {dt:.1f}s ({dt/60:.1f}min)") |
| print(f"{'='*60}") |
| return 0 |
|
|
|
|
| def run_single(args): |
| """单场景模式(blend、glb、hm3d 或 ply)""" |
| if args.ply: |
| ply_path = str(Path(args.ply).resolve()) |
| scene_name = args.scene_name or Path(args.ply).stem |
| ret = run_single_scene(args, ply_path, scene_name, scene_type="ply") |
| elif args.hm3d: |
| glb_path = str(Path(args.hm3d).resolve()) |
| scene_name = args.scene_name or Path(args.hm3d).stem |
| ret = run_single_scene(args, glb_path, scene_name, scene_type="hm3d") |
| elif args.glb: |
| glb_path = str(Path(args.glb).resolve()) |
| scene_name = args.scene_name or Path(args.glb).stem |
| ret = run_single_scene(args, glb_path, scene_name, scene_type="glb") |
| else: |
| blend_path = str(Path(args.blend).resolve()) |
| scene_name = args.scene_name or Path(args.blend).stem |
| ret = run_single_scene(args, blend_path, scene_name, scene_type="blend") |
| if ret != 0: |
| sys.exit(1) |
|
|
|
|
| def run_batch(args): |
| """批量模式(自动检测 .blend / .glb / .gltf / .ply)""" |
| input_dir_abs = os.path.abspath(args.input_dir) |
| if not os.path.isdir(input_dir_abs): |
| print(f"[Error] --input-dir 目录不存在: {input_dir_abs}") |
| print(f" (原始参数: {args.input_dir})") |
| sys.exit(1) |
|
|
| |
| if not getattr(args, "blender", None): |
| scene_files = find_ply_files(args.input_dir) |
| scene_type = "ply" |
| ext_label = ".ply" |
| else: |
| |
| scene_files = find_blend_files(args.input_dir) |
| scene_type = "blend" |
| ext_label = ".blend" |
| if not scene_files: |
| scene_files = find_glb_files(args.input_dir) |
| scene_type = "hm3d" |
| ext_label = ".glb/.gltf (HM3D)" |
| if not scene_files: |
| scene_files = find_ply_files(args.input_dir) |
| scene_type = "ply" |
| ext_label = ".ply" |
|
|
| if not scene_files: |
| print(f"[Error] 在 {args.input_dir} 下没找到 {ext_label} 文件") |
| sys.exit(1) |
|
|
| output_root = str(Path(args.output_root).resolve()) |
|
|
| print(f"{'='*60}") |
| print(f"批量处理模式 ({ext_label})") |
| print(f"{'='*60}") |
| print(f" 输入目录: {args.input_dir}") |
| print(f" 输出目录: {output_root}") |
| print(f" 找到 {len(scene_files)} 个 {ext_label} 文件") |
| |
| |
| to_run = [] |
| skipped = [] |
| for scene_path, scene_name in scene_files: |
| if args.skip_done and is_already_done(output_root, scene_name): |
| skipped.append((scene_path, scene_name)) |
| else: |
| to_run.append((scene_path, scene_name)) |
|
|
| if skipped: |
| print(f" 跳过 {len(skipped)} 个已完成:") |
| for _, sn in skipped: |
| print(f" ✓ {sn}") |
|
|
| print(f" 待处理 {len(to_run)} 个:") |
| for bp, sn in to_run: |
| print(f" → {sn} ({os.path.basename(bp)})") |
|
|
| if args.dry_run: |
| print(f"\n[Dry run] 不实际运行") |
| return |
|
|
| if not to_run: |
| print(f"\n全部已完成!") |
| return |
|
|
| t_all = time.time() |
| success = [] |
| failed = [] |
|
|
| for idx, (scene_path, scene_name) in enumerate(to_run): |
| print(f"\n{'='*60}") |
| print(f"[{idx+1}/{len(to_run)}] {scene_name}") |
| print(f"{'='*60}") |
|
|
| t_scene = time.time() |
| try: |
| ret = run_single_scene(args, scene_path, scene_name, scene_type) |
| dt = time.time() - t_scene |
| if ret == 0: |
| success.append((scene_name, dt)) |
| print(f"\n ✓ {scene_name} ({dt:.0f}s)") |
| else: |
| failed.append((scene_name, f"exit code {ret}")) |
| print(f"\n ✗ {scene_name} 失败 ({dt:.0f}s)") |
| except Exception as e: |
| dt = time.time() - t_scene |
| failed.append((scene_name, str(e))) |
| print(f"\n ✗ {scene_name} 异常: {e} ({dt:.0f}s)") |
|
|
| dt_all = time.time() - t_all |
| print(f"\n{'='*60}") |
| print(f"批量处理完成") |
| print(f"{'='*60}") |
| print(f" 总耗时: {dt_all:.0f}s ({dt_all/60:.1f}min = {dt_all/3600:.1f}h)") |
| print(f" 成功: {len(success)} 个") |
| for sn, dt in success: |
| print(f" ✓ {sn} ({dt:.0f}s)") |
| if failed: |
| print(f" 失败: {len(failed)} 个") |
| for sn, reason in failed: |
| print(f" ✗ {sn}: {reason}") |
| if skipped: |
| print(f" 跳过: {len(skipped)} 个 (已完成)") |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="全流程: .blend/.glb/.ply → 边渲边选 → ERPT Warp" |
| ) |
|
|
| |
| input_group = parser.add_mutually_exclusive_group() |
| input_group.add_argument("--blend", type=str, default=None, |
| help=".blend 场景文件路径(单 Blend 场景模式)") |
| input_group.add_argument("--glb", type=str, default=None, |
| help=".glb / .gltf 场景文件路径(单 GLB 场景模式)") |
| input_group.add_argument("--hm3d", type=str, default=None, |
| help=".glb / .gltf 场景文件路径(单 HM3D 场景模式)") |
| input_group.add_argument("--ply", type=str, default=None, |
| help=".ply 场景文件路径(单 PLY 场景模式)") |
|
|
| parser.add_argument("--input-dir", type=str, default=None, |
| help="包含场景文件的根目录(批量模式,自动检测 .blend/.glb/.ply)") |
| parser.add_argument("--scene-name", type=str, default=None, |
| help="场景名(默认从文件名提取)") |
| parser.add_argument("--output-root", type=str, default="./dataset", |
| help="输出根目录(默认 ./dataset)") |
|
|
| |
| parser.add_argument("--blender", type=str, default=None, |
| help="Blender 可执行文件路径(Blend/GLB/HM3D 模式必须)") |
| parser.add_argument("--samples", type=int, default=128) |
| parser.add_argument("--engine", type=str, default="CYCLES") |
| parser.add_argument("--exposure", type=float, default=0.0) |
| parser.add_argument("--gain-curve", action="store_true", default=True, |
| help="画增益曲线 (默认开启)") |
| parser.add_argument("--no-gain-curve", dest="gain_curve", action="store_false") |
|
|
| |
| parser.add_argument("--point-size", type=float, default=2.0, |
| help="点云渲染点径(像素),PLY 模式有效(默认 2.0)") |
| parser.add_argument("--z-up", action="store_true", default=True, |
| help="PLY 坐标系为 Z-up(默认 True)") |
| parser.add_argument("--no-z-up", dest="z_up", action="store_false", |
| help="PLY 坐标系为 Y-up(已是 ERPT_native,不转换)") |
|
|
| |
| parser.add_argument("--num-frames", type=int, default=30) |
| parser.add_argument("--resolution", type=str, default="2048,1024") |
|
|
| |
| parser.add_argument("--grid-spacing", type=float, default=0.5) |
| parser.add_argument("--camera-height", type=float, default=None) |
| parser.add_argument("--stop-gain", type=float, default=0.08) |
| parser.add_argument("--stop-score", type=float, default=-0.3) |
| parser.add_argument("--stop-delta", type=float, default=0.08) |
| parser.add_argument("--min-frames", type=int, default=5) |
| parser.add_argument("--rotation-type", type=str, default="random_yaw", |
| choices=["none", "rotate_x_90", "rotate_x_180", |
| "rotate_z_90", "random_yaw"]) |
|
|
| |
| parser.add_argument("--device", type=str, default="cuda") |
| parser.add_argument("--skip-warp", action="store_true", |
| help="只做步骤 1+2,跳过 ERPT warp") |
|
|
| |
| parser.add_argument("--skip-done", action="store_true", default=True, |
| help="跳过已跑完的场景(默认开启)") |
| parser.add_argument("--no-skip-done", action="store_true", |
| help="强制重跑所有场景") |
| parser.add_argument("--dry-run", action="store_true", |
| help="只列出要跑的场景,不实际运行") |
|
|
| args = parser.parse_args() |
|
|
| if args.no_skip_done: |
| args.skip_done = False |
|
|
| |
| if (args.blend or args.glb or args.hm3d) and not args.blender: |
| parser.error("--blend / --glb / --hm3d 模式必须同时提供 --blender 可执行文件路径") |
|
|
| |
| if args.input_dir: |
| run_batch(args) |
| elif args.blend or args.glb or args.hm3d or args.ply: |
| run_single(args) |
| else: |
| parser.error("必须指定 --blend / --glb / --hm3d / --ply(单场景)或 --input-dir(批量)") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|