Files
hand-motion-pipeline/utils/vipe_worker.py
T

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()