twanghcmut/backup-foundation-physics / scripts /export_real_depth_video.py
twanghcmut's picture
download
raw
9.28 kB
#!/usr/bin/env python
"""Whole-episode depth built ONLY from real per-frame measurements -- nothing filled.
Answers "what does the depth actually look like before any interpolation?", which
neither existing artifact does: ``master/depth_dense.h5`` is 90.5% ANCHOR + 9.5%
push-pull FILLED with `support_frac` bit-identical across every frame (i.e. one
static map repeated), and the exported ``control_depth.mkv`` is a mesh z-buffer
composited over that static background plate. Both are useful; neither shows the
raw evidence.
The raw evidence is PointWorld's ``scene_flows``: ``(T, N, 3)`` metric 3D point
tracks, per clip, gated by ``scene_visibility`` and ``scene_depth_valid_mask``.
This projects them into the camera with a z-buffer and leaves every unmeasured
pixel black. **The gaps are the point of the output**, so they are never filled.
Two things this surfaces that a filled depth map hides:
* **PointWorld does not cover every frame.** Its clips are short overlapping
windows (e.g. ``0:11``, ``5:16``, ``20:31``...) and an episode is typically
covered only in part; the uncovered frames have no measured depth *at all*.
Coverage is printed and written into the sidecar JSON.
* **Even a covered frame is sparse.** ~27k tracked points against 320x180 = 57,600
pixels is under half the frame before visibility/validity gating.
Frame convention (WORLD vs CAMERA ``scene_flows``) is not assumed: it is decided by
``fpgm.geometry.convention.detect_scene_flow_convention``, the same detector the
pipeline itself uses, against the clip's own ``initial_rgb``/``scene_colors``.
Usage:
PYTHONPATH=src python scripts/export_real_depth_video.py \\
--flows data/pointworld/droid/flows-fs-optimized/<uuid>_flows.h5 \\
--camera-serial 22008760 --out-dir outputs/depth_real [--scale 3]
"""
from __future__ import annotations
import argparse
import io
import json
import re
import subprocess
from pathlib import Path
import cv2
import h5py
import numpy as np
from PIL import Image
from fpgm.config import ConventionConfig
from fpgm.geometry.camera import Camera, CameraIntrinsics
from fpgm.geometry.convention import detect_scene_flow_convention
from fpgm.types import FrameConvention
from fpgm.viz.video import _resolve_ffmpeg
_CLIP_RE = re.compile(r"^(\d+):(\d+)$")
def _decode_initial_rgb(raw: object) -> np.ndarray:
"""``initial_rgb`` -> ``(H, W, 3)`` uint8.
The dataset stores it as a 1-element object array holding **JPEG bytes**, and
h5py hands those back as a 1-D ``uint8`` ndarray rather than a ``bytes`` object,
so an ``isinstance(..., bytes)`` check alone silently returns the raw byte
string as if it were an image.
"""
data = raw[0] if isinstance(raw, np.ndarray) and raw.dtype == object else raw
if isinstance(data, bytes | bytearray | np.bytes_):
return np.asarray(Image.open(io.BytesIO(bytes(data))).convert("RGB"))
arr = np.asarray(data)
if arr.ndim == 1: # encoded image bytes
return np.asarray(Image.open(io.BytesIO(arr.tobytes())).convert("RGB"))
return arr
def _camera_from(intrinsic: np.ndarray, extrinsic: np.ndarray, w: int, h: int) -> Camera:
K = CameraIntrinsics(
fx=float(intrinsic[0, 0]), fy=float(intrinsic[1, 1]),
cx=float(intrinsic[0, 2]), cy=float(intrinsic[1, 2]), width=w, height=h,
)
return Camera(K, np.asarray(extrinsic, dtype=np.float64))
def _zbuffer(uv: np.ndarray, z: np.ndarray, h: int, w: int) -> np.ndarray:
"""Nearest-wins splat of projected points into an (H, W) float32 depth map."""
out = np.zeros((h, w), dtype=np.float32)
u = np.round(uv[:, 0]).astype(np.int64)
v = np.round(uv[:, 1]).astype(np.int64)
keep = (u >= 0) & (u < w) & (v >= 0) & (v < h) & (z > 0)
u, v, z = u[keep], v[keep], z[keep]
order = np.argsort(-z) # far first, so nearer points overwrite
np.maximum.at(out, (v[order], u[order]), 0) # touch, keeps dtype/shape semantics
out[v[order], u[order]] = z[order]
return out
def write_h264(frames_bgr: np.ndarray, out: Path, fps: int) -> None:
t, h, w, _ = frames_bgr.shape
out.parent.mkdir(parents=True, exist_ok=True)
cmd = [
_resolve_ffmpeg(), "-y", "-hide_banner", "-loglevel", "error",
"-f", "rawvideo", "-pix_fmt", "bgr24", "-s", f"{w}x{h}", "-r", str(fps),
"-i", "pipe:0", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "16", str(out),
]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
assert p.stdin is not None
try:
for i in range(t):
p.stdin.write(np.ascontiguousarray(frames_bgr[i]).tobytes())
finally:
p.stdin.close()
if p.wait() != 0:
raise RuntimeError(f"ffmpeg failed writing {out}")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--flows", required=True, type=Path)
ap.add_argument("--camera-serial", required=True)
ap.add_argument("--out-dir", required=True, type=Path)
ap.add_argument("--scale", type=int, default=3)
ap.add_argument("--fps", type=int, default=16)
args = ap.parse_args()
with h5py.File(args.flows, "r") as f:
clips = sorted(
(k for k in f if _CLIP_RE.fullmatch(k)),
key=lambda s: int(s.split(":")[0]),
)
if not clips:
raise SystemExit(f"no clip groups in {args.flows}")
n_frames = int(f.attrs.get("trajectory_length", 0)) or (
max(int(c.split(":")[1]) for c in clips)
)
cam_key = f"camera_{args.camera_serial}_ext"
first = f[clips[0]][cam_key]
h, w = np.asarray(first["initial_depth"]).shape
rgb0 = _decode_initial_rgb(first["initial_rgb"][()])
cam_native = _camera_from(first["intrinsic"], first["extrinsic"], w, h)
det = detect_scene_flow_convention(
cam_native.rescaled(rgb0.shape[1], rgb0.shape[0]),
np.asarray(first["scene_flows"][0], dtype=np.float64),
np.asarray(first["scene_colors"][0]),
rgb0,
ConventionConfig(),
)
is_world = det.convention is FrameConvention.WORLD
print(f"frame convention: {det.convention.name}")
depth = np.zeros((n_frames, h, w), dtype=np.float32)
measured = np.zeros(n_frames, dtype=bool)
for clip in clips:
a, b = (int(x) for x in clip.split(":"))
g = f[clip][cam_key]
cam = _camera_from(g["intrinsic"], g["extrinsic"], w, h)
pts = np.asarray(g["scene_flows"], dtype=np.float64)
vis = np.asarray(g["scene_visibility"])
ok = np.asarray(g["scene_depth_valid_mask"])
for i, t in enumerate(range(a, min(b, n_frames))):
sel = vis[i] & ok[i]
if not sel.any():
continue
p = pts[i][sel]
uv, z = (cam.project(p) if is_world else cam.project_cam(p))
depth[t] = _zbuffer(np.asarray(uv), np.asarray(z), h, w)
measured[t] = True
valid = depth > 0
if not valid.any():
raise SystemExit("no measured depth recovered -- check --camera-serial")
lo, hi = np.percentile(1.0 / depth[valid], (1.0, 99.0))
if not hi > lo:
hi = lo + 1e-6
s = max(1, args.scale)
out_h, out_w = h * s, w * s
frames = np.zeros((n_frames, out_h, out_w, 3), dtype=np.uint8)
for t in range(n_frames):
g = np.zeros((h, w), dtype=np.float32)
m = depth[t] > 0
if m.any():
g[m] = np.clip((1.0 / depth[t][m] - lo) / (hi - lo), 0.0, 1.0)
up = cv2.resize((g * 255).astype(np.uint8), (out_w, out_h), interpolation=cv2.INTER_NEAREST)
col = cv2.applyColorMap(up, cv2.COLORMAP_TURBO)
col[cv2.resize(m.astype(np.uint8), (out_w, out_h), interpolation=cv2.INTER_NEAREST) == 0] = 0
tag = f"f{t:03d} " + ("MEASURED" if measured[t] else "NO POINTWORLD DATA")
cv2.rectangle(col, (0, 0), (len(tag) * 11 + 10, 26), (0, 0, 0), -1)
cv2.putText(col, tag, (5, 19), cv2.FONT_HERSHEY_SIMPLEX, 0.6,
(255, 255, 255) if measured[t] else (60, 60, 255), 1, cv2.LINE_AA)
frames[t] = col
args.out_dir.mkdir(parents=True, exist_ok=True)
write_h264(frames, args.out_dir / "depth_real_full.mp4", args.fps)
per_frame_cov = valid.reshape(n_frames, -1).mean(axis=1)
stats = {
"n_frames": int(n_frames),
"frames_with_any_measurement": int(measured.sum()),
"frame_coverage": float(measured.mean()),
"pixel_coverage_on_measured_frames": float(per_frame_cov[measured].mean()) if measured.any() else 0.0,
"clips": clips,
"convention": det.convention.name,
"metric_range_m": [float(depth[valid].min()), float(depth[valid].max())],
}
(args.out_dir / "depth_real_stats.json").write_text(json.dumps(stats, indent=2))
print(f"frames with real measurement: {measured.sum()}/{n_frames} = {measured.mean():.1%}")
print(f"pixel coverage on those frames: {stats['pixel_coverage_on_measured_frames']:.1%}")
print(f"metric range: {stats['metric_range_m'][0]:.3f} .. {stats['metric_range_m'][1]:.3f} m")
print(f"wrote {args.out_dir}/depth_real_full.mp4")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
9.28 kB
·
Xet hash:
7696d05aec390700d40e2f25c52b3d993ed6e26c0167680ad5f94521baecb571

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.