7e4ef6f98b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
180 lines
6.9 KiB
Python
180 lines
6.9 KiB
Python
"""ViPE SLAM worker — invoked via subprocess + conda run -n vipe.
|
|
|
|
Provides:
|
|
- run_vipe_slam(): caller side (runs in the handflow env); launches a separate vipe env via subprocess
|
|
- main(): worker side (runs in the vipe env); actually loads ViPE, runs SLAM, and writes JSON output
|
|
|
|
demo.py calls run_vipe_slam only in moving-camera mode (i.e. without --fix_camera): with
|
|
explicit/default intrinsics it uses the gt_intr variant (c2w only), and with --intrinsics auto
|
|
it uses the default variant (intrinsics + c2w).
|
|
The worker is triggered by `conda run -n vipe python utils/vipe_worker.py --worker ...`,
|
|
so vipe imports are deliberately deferred into main() (top-level imports would fail in the handflow env).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import time
|
|
import traceback
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import numpy as np
|
|
|
|
# ViPE submodule root directory (can be overridden by the VIPE_ROOT environment variable)
|
|
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
VIPE_ROOT = Path(os.environ.get("VIPE_ROOT", str(_PROJECT_ROOT / "third_party" / "vipe")))
|
|
|
|
|
|
def run_vipe_slam(
|
|
images_dir: str,
|
|
output_dir: str,
|
|
variant: str = "default",
|
|
gt_intrinsics_4d: Optional[np.ndarray] = None,
|
|
vipe_env: str = "vipe",
|
|
seq_name: Optional[str] = None,
|
|
timeout: int = 3600,
|
|
) -> dict:
|
|
"""Caller side: run ViPE SLAM via `conda run -n <vipe_env>`.
|
|
|
|
Args:
|
|
images_dir: directory of frame images (already extracted into images)
|
|
output_dir: output directory (slam_result.json is written here)
|
|
variant: "default" (ViPE estimates intrinsics + c2w)
|
|
| "gt_intr" (given intrinsics, estimate c2w only)
|
|
gt_intrinsics_4d: [fx, fy, cx, cy] used when variant=gt_intr
|
|
vipe_env: name of the conda environment containing ViPE (default "vipe")
|
|
Returns:
|
|
{"poses": (T,4,4) c2w, "intrinsics": (T,4) or (4,), "slam_time_s": float}
|
|
"""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
output_json = str(Path(output_dir) / "slam_result.json")
|
|
seq_name = seq_name or Path(images_dir).name
|
|
|
|
vipe_python = os.environ.get("VIPE_PYTHON")
|
|
launcher = [vipe_python] if vipe_python else [
|
|
"conda", "run", "-n", vipe_env, "--no-capture-output", "python",
|
|
]
|
|
cmd = launcher + [
|
|
str(Path(__file__).resolve()), "--worker",
|
|
"--img_dir", images_dir,
|
|
"--seq_name", seq_name,
|
|
"--variant", variant,
|
|
"--output", output_json,
|
|
]
|
|
if variant == "gt_intr" and gt_intrinsics_4d is not None:
|
|
intr = np.asarray(gt_intrinsics_4d).ravel()[:4]
|
|
cmd += ["--gt_intr_str", ",".join(str(float(x)) for x in intr)]
|
|
|
|
result = subprocess.run(
|
|
cmd, capture_output=True, text=True, timeout=timeout, cwd=str(_PROJECT_ROOT)
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(
|
|
f"ViPE SLAM failed (returncode={result.returncode}):\n"
|
|
f"{result.stderr.strip()[-800:]}"
|
|
)
|
|
if not os.path.exists(output_json):
|
|
raise RuntimeError(f"ViPE worker produced no output: {result.stderr[-400:]}")
|
|
|
|
with open(output_json) as f:
|
|
data = json.load(f)
|
|
if not data.get("ok"):
|
|
raise RuntimeError(f"ViPE worker error: {data.get('error')}")
|
|
|
|
return {
|
|
"poses": np.array(data["poses"], dtype=np.float32), # (T, 4, 4) c2w
|
|
"intrinsics": np.array(data["intrinsics"], dtype=np.float32), # (T, 4) or (4,)
|
|
"slam_time_s": float(data.get("slam_time_s", 0.0)),
|
|
}
|
|
|
|
|
|
# ── Worker (executed inside the vipe environment) ──────────────────────────────────────────
|
|
|
|
def _worker_main() -> None:
|
|
"""Actually loads ViPE, runs SLAM, and writes results to JSON. Only runnable in the vipe environment."""
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--worker", action="store_true")
|
|
parser.add_argument("--img_dir", required=True)
|
|
parser.add_argument("--seq_name", default="seq")
|
|
parser.add_argument("--variant", default="default", choices=["default", "gt_intr"])
|
|
parser.add_argument("--gt_intr_str", default="")
|
|
parser.add_argument("--output", required=True)
|
|
args = parser.parse_args()
|
|
|
|
from omegaconf import OmegaConf
|
|
import torch
|
|
from vipe.streams.frame_dir_stream import FrameDirStream
|
|
from vipe.slam.system import SLAMSystem
|
|
from vipe.streams.base import ProcessedVideoStream, StreamProcessor, FrameAttribute
|
|
from vipe.utils.cameras import CameraType
|
|
|
|
slam_cfg = OmegaConf.load(str(VIPE_ROOT / "configs" / "slam" / "default.yaml"))
|
|
slam_cfg.optimize_intrinsics = (args.variant != "gt_intr")
|
|
|
|
try:
|
|
stream = FrameDirStream(path=Path(args.img_dir),
|
|
name=args.seq_name.replace("/", "_"))
|
|
|
|
if args.variant == "gt_intr":
|
|
fx, fy, cx, cy = [float(x) for x in args.gt_intr_str.split(",")]
|
|
|
|
class GTIntrinsicsProcessor(StreamProcessor):
|
|
def update_attributes(self, prev):
|
|
return prev | {FrameAttribute.INTRINSICS}
|
|
|
|
def __call__(self, frame_idx, frame):
|
|
frame.intrinsics = torch.as_tensor([fx, fy, cx, cy]).float()
|
|
frame.camera_type = CameraType.PINHOLE
|
|
return frame
|
|
|
|
stream = ProcessedVideoStream(stream, [GTIntrinsicsProcessor()])
|
|
else:
|
|
from vipe.pipeline.processors import GeoCalibIntrinsicsProcessor
|
|
stream = ProcessedVideoStream(
|
|
stream, [GeoCalibIntrinsicsProcessor(stream, camera_type=CameraType.PINHOLE)],
|
|
)
|
|
|
|
device = torch.device("cuda")
|
|
slam_system = SLAMSystem(device=device, config=slam_cfg)
|
|
t0 = time.perf_counter()
|
|
slam_output = slam_system.run([stream], camera_type=CameraType.PINHOLE)
|
|
slam_time = time.perf_counter() - t0
|
|
|
|
traj_mat = slam_output.trajectory.matrix()
|
|
if hasattr(traj_mat, "detach"):
|
|
traj_mat = traj_mat.detach().cpu().numpy()
|
|
intr_tensor = slam_output.intrinsics
|
|
if hasattr(intr_tensor, "detach"):
|
|
intr_tensor = intr_tensor.detach().cpu().numpy()
|
|
|
|
n_frames = len(traj_mat)
|
|
if len(intr_tensor) == 1 and n_frames > 1:
|
|
intr_tensor = np.tile(intr_tensor, (n_frames, 1))
|
|
N = min(len(traj_mat), len(intr_tensor))
|
|
|
|
result = {
|
|
"poses": traj_mat[:N].tolist(),
|
|
"intrinsics": intr_tensor[:N].tolist(),
|
|
"slam_time_s": slam_time,
|
|
"ok": True,
|
|
}
|
|
except Exception as e:
|
|
result = {
|
|
"ok": False,
|
|
"error": str(e),
|
|
"traceback": traceback.format_exc()[-500:],
|
|
"slam_time_s": 0.0,
|
|
}
|
|
|
|
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
|
with open(args.output, "w") as f:
|
|
json.dump(result, f)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_worker_main()
|