twanghcmut/backup-foundation-physics / scripts /export_depth_gt_video.py
twanghcmut's picture
download
raw
6.34 kB
#!/usr/bin/env python
"""Whole-episode ground-truth depth as viewable video: inverse-depth + provenance.
Two things this does that ``master/depth_preview.mp4`` does not, both because it
reads ``master/depth_dense.h5`` (the metric ``uint16`` millimetres) rather than a
pre-rendered preview:
**One global normalisation across the whole episode, never per frame.** Per-frame
normalisation makes a depth video flicker as the scene's near/far extremes move,
and this project has already recorded the consequence downstream: a model trained
on it reproduces the flicker as brightness pumping (see
``scripts/export_cosmos_input.py``'s "Depth is normalised once for the whole clip"
note and ``export_vace``'s ``_INVERSE_DEPTH_PERCENTILES``). It also matters for
*looking* at the data: with per-frame ranges, the eye cannot tell a surface that
moved from a normalisation that shifted. Robust percentiles, not min/max, so a
single stray pixel cannot crush the usable range.
**A provenance track.** ``depth_dense.h5``'s ``source`` array records, per pixel
per frame, why that depth has the value it has (:class:`DepthSourceCode`):
``SPLAT`` = a real projected scene_flows point *this frame*, ``ANCHOR`` = the
clip's dense initial-depth anchor, ``FILLED`` = push-pull interpolated from
neighbouring support, ``INVALID`` = no support in range and ``depth_mm`` is 0.
A depth video alone cannot show the difference between measured and interpolated
geometry -- they are both just grey -- so eyeballing depth quality without this
track systematically overrates it.
Usage:
PYTHONPATH=src python scripts/export_depth_gt_video.py \\
--master outputs/datagen/<uuid>/<cam>/master \\
--out-dir outputs/depth_gt [--scale 3]
"""
from __future__ import annotations
import argparse
import subprocess
from pathlib import Path
import cv2
import h5py
import numpy as np
from fpgm.datagen.types import DepthSourceCode
from fpgm.viz.video import _resolve_ffmpeg
#: Robust range for the single global inverse-depth normalisation.
_PERCENTILES = (1.0, 99.0)
#: Provenance palette, BGR. Chosen so the two that mean "measured" (SPLAT,
#: ANCHOR) read as one family and the two that mean "not measured" (FILLED,
#: INVALID) as another -- the distinction that actually matters when judging
#: whether a region of the depth map is evidence or inference.
_SOURCE_BGR = {
DepthSourceCode.SPLAT: (80, 220, 80), # green -- measured this frame
DepthSourceCode.ANCHOR: (220, 200, 60), # cyan -- measured at the anchor
DepthSourceCode.FILLED: (60, 140, 240), # orange -- interpolated
DepthSourceCode.INVALID: (40, 40, 220), # red -- nothing at all
}
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("--master", required=True, type=Path)
ap.add_argument("--out-dir", required=True, type=Path)
ap.add_argument("--scale", type=int, default=3, help="nearest-neighbour upscale for viewing")
ap.add_argument("--fps", type=int, default=16)
args = ap.parse_args()
with h5py.File(args.master / "depth_dense.h5", "r") as f:
depth_mm = np.asarray(f["depth_mm"]) # (T, H, W) uint16
source = np.asarray(f["source"]) # (T, H, W) uint8
support_frac = np.asarray(f["support_frac"]) # (T,)
valid = depth_mm > 0
depth_m = depth_mm.astype(np.float32) / 1000.0
inv = np.zeros_like(depth_m)
inv[valid] = 1.0 / np.maximum(depth_m[valid], 1e-6)
lo, hi = np.percentile(inv[valid], _PERCENTILES)
if not hi > lo:
hi = lo + 1e-6
gray = np.clip((inv - lo) / (hi - lo), 0.0, 1.0)
gray[~valid] = 0.0
gray8 = (gray * 255.0).astype(np.uint8)
t, h, w = gray8.shape
s = max(1, args.scale)
out_h, out_w = h * s, w * s
depth_frames = np.empty((t, out_h, out_w, 3), dtype=np.uint8)
source_frames = np.empty((t, out_h, out_w, 3), dtype=np.uint8)
for i in range(t):
up = cv2.resize(gray8[i], (out_w, out_h), interpolation=cv2.INTER_NEAREST)
depth_frames[i] = cv2.applyColorMap(up, cv2.COLORMAP_TURBO)
rgb = np.zeros((h, w, 3), dtype=np.uint8)
for code, bgr in _SOURCE_BGR.items():
rgb[source[i] == int(code)] = bgr
source_frames[i] = cv2.resize(rgb, (out_w, out_h), interpolation=cv2.INTER_NEAREST)
args.out_dir.mkdir(parents=True, exist_ok=True)
write_h264(depth_frames, args.out_dir / "depth_gt_full.mp4", args.fps)
write_h264(source_frames, args.out_dir / "depth_source_full.mp4", args.fps)
write_h264(
np.concatenate([depth_frames, source_frames], axis=2),
args.out_dir / "depth_gt_and_source.mp4", args.fps,
)
counts = {c.name: float((source == int(c)).mean()) for c in DepthSourceCode}
print(f"frames: {t} ({h}x{w} native, upscaled x{s})")
print(f"valid depth px: {valid.mean():.2%}")
dmin = depth_m[valid].min() if valid.any() else float("nan")
dmax = depth_m[valid].max() if valid.any() else float("nan")
print(f"metric range: {dmin:.3f} .. {dmax:.3f} m")
print(f"global inv range: {lo:.4f} .. {hi:.4f} per-m (p{_PERCENTILES[0]}..p{_PERCENTILES[1]})")
print("provenance (fraction of all pixels):")
for name, frac in counts.items():
print(f" {name:<8} {frac:7.2%}")
print(f"support_frac (non-FILLED) per frame: min {support_frac.min():.3f} "
f"mean {support_frac.mean():.3f} max {support_frac.max():.3f}")
print(f"wrote: {args.out_dir}/depth_gt_full.mp4, depth_source_full.mp4, depth_gt_and_source.mp4")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
6.34 kB
·
Xet hash:
db32f5ef73b32b95b618f05079273eeb9610d6f216431b7a7f7263159fd8eb72

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