twanghcmut's picture
download
raw
7.65 kB
#!/usr/bin/env python
"""Re-render the MoGe preview videos from the cached ``per_frame/*.npz`` — no GPU.
``moge_run_clips.py`` writes a three-panel RGB|depth|normal video, and on this
project's clips that video *understates* the model: the global depth range is
~1.1 m spread over 255 colour levels, i.e. **4.3 mm per level**, so the 17 mm step
of the manipulated LEGO brick occupies about four levels of turbo and reads as flat
by eye. It is not flat -- measured at frame 90 of the demo episode, the mean depth
inside the brick mask sits 17.3 mm in front of the surrounding ring.
So this adds a fourth panel: the **same depth, near-field colour range**, clipped to
the clip's [p25, p60] depth percentiles -- roughly the manipulation working volume,
~1 mm per colour level, where that 17 mm step spans ~16 levels and is obvious.
This is deliberately NOT a high-pass / unsharp filter. A ``depth - blur(depth)``
panel was tried first and rejected: with any sigma, real depth discontinuities
(arm against background) dwarf object relief -- the p99 of |high-pass| is ~520 mm
against the ~17 mm being looked for -- so the panel saturates into blobs and stops
being readable as depth at all. A narrower colour window applies no filter, keeps
every pixel's value literal, and prints its own mm-per-level in the label, so what
is on screen can be checked against a number.
Layout is 2x2 (2560x1440) rather than 1x4: a 5120-px-wide video gets downscaled by
every player and the detail this exists to show is the first thing lost.
Env: anything with numpy/cv2 (``fpgm`` is fine); imports no torch and no ``fpgm``.
Usage:
/home/quang/miniconda3/envs/fpgm/bin/python scripts/moge_render_panels.py \\
[--clips-root outputs/moge/published_clips] [--near-percentiles 25 60]
"""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
from pathlib import Path
import cv2
import numpy as np
REPO_ROOT = Path(__file__).resolve().parents[1]
_FFMPEG_CANDIDATE = Path("/home/quang/miniconda3/envs/ffmpeg_libs/bin/ffmpeg")
_PERCENTILES = (2.0, 98.0)
#: See moge_run_clips.TRAJECTORY_FPS -- DROID containers say 60/1, content is 15 Hz.
TRAJECTORY_FPS = 15.0
def resolve_ffmpeg() -> str:
if _FFMPEG_CANDIDATE.exists():
return str(_FFMPEG_CANDIDATE)
on_path = shutil.which("ffmpeg")
if on_path:
return on_path
raise RuntimeError("no ffmpeg found")
def write_h264(frames_bgr, out: Path, fps: float, crf: int = 16) -> None:
it = iter(frames_bgr)
first = next(it)
h, w = first.shape[:2]
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", f"{fps:.4f}",
"-i", "pipe:0", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", str(crf),
str(out)]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
assert p.stdin is not None
try:
p.stdin.write(np.ascontiguousarray(first).tobytes())
for f in it:
p.stdin.write(np.ascontiguousarray(f).tobytes())
finally:
p.stdin.close()
if p.wait() != 0:
raise RuntimeError(f"ffmpeg failed writing {out}")
def label(img: np.ndarray, text: str) -> np.ndarray:
out = img.copy()
for colour, thick in ((0, 0, 0), 5), ((255, 255, 255), 2):
cv2.putText(out, text, (14, 36), cv2.FONT_HERSHEY_SIMPLEX, 0.95, colour,
thick, cv2.LINE_AA)
return out
def colorize_depth(depth: np.ndarray, lo: float, hi: float) -> np.ndarray:
valid = np.isfinite(depth)
norm = np.zeros_like(depth, dtype=np.float32)
norm[valid] = np.clip((depth[valid] - lo) / max(hi - lo, 1e-6), 0.0, 1.0)
col = cv2.applyColorMap(((1.0 - norm) * 255).astype(np.uint8), cv2.COLORMAP_TURBO)
col[~valid] = 0
return col
def colorize_normal(normal: np.ndarray, valid: np.ndarray) -> np.ndarray:
bgr = (((normal.astype(np.float32) * 0.5 + 0.5) * 255).clip(0, 255)
.astype(np.uint8))[..., ::-1].copy()
bgr[~valid] = 0
return bgr
def render_clip(clip_dir: Path, near_pcts: tuple[float, float], fps: float) -> dict:
npz_dir = clip_dir / "per_frame"
files = sorted(npz_dir.glob("frame_*.npz"))
if not files:
raise FileNotFoundError(f"no npz under {npz_dir}")
stats = json.loads((clip_dir / "stats.json").read_text())
video = REPO_ROOT / stats["video"]
cap = cv2.VideoCapture(str(video))
rgbs = []
for _ in range(len(files)):
ok, fr = cap.read()
if not ok:
break
rgbs.append(fr)
cap.release()
n = min(len(rgbs), len(files))
print(f" {clip_dir.name}: {n} frames")
depths, masks, normals = [], [], []
for f in files[:n]:
d = np.load(f)
depths.append(d["depth"])
masks.append(d["mask"].astype(bool))
normals.append(d["normal"].astype(np.float32) if "normal" in d else None)
pool = np.concatenate([d[np.isfinite(d)].ravel()[::17] for d in depths])
lo, hi = np.percentile(pool, _PERCENTILES)
nlo, nhi = np.percentile(pool, near_pcts)
mmpl, near_mmpl = (hi - lo) * 1000 / 255, (nhi - nlo) * 1000 / 255
print(f" depth global {lo:.3f}-{hi:.3f} m ({mmpl:.1f} mm/level) | "
f"near-field p{near_pcts[0]:g}-p{near_pcts[1]:g} = {nlo:.3f}-{nhi:.3f} m "
f"({near_mmpl:.1f} mm/level)")
def frames():
for i in range(n):
valid = masks[i] & np.isfinite(depths[i])
tl = label(rgbs[i], "RGB")
tr = label(colorize_depth(depths[i], lo, hi),
f"MoGe-2 depth {lo:.2f}-{hi:.2f} m ({mmpl:.1f} mm/level)")
bl = label(colorize_depth(depths[i], nlo, nhi),
f"same depth, near-field range {nlo:.2f}-{nhi:.2f} m "
f"({near_mmpl:.1f} mm/level)")
br = label(colorize_normal(normals[i], valid) if normals[i] is not None
else np.zeros_like(rgbs[i]), "MoGe-2 normal")
yield np.vstack([np.hstack([tl, tr]), np.hstack([bl, br])])
out = clip_dir / "panels_2x2.mp4"
write_h264(frames(), out, fps)
print(f" wrote {out}")
stats.update({"panels_video": str(out.relative_to(REPO_ROOT)),
"near_field_percentiles": list(near_pcts),
"near_field_range_m": [float(nlo), float(nhi)],
"near_field_mm_per_colour_level": round(near_mmpl, 2),
"depth_mm_per_colour_level": round(mmpl, 2),
"output_fps": fps})
(clip_dir / "stats.json").write_text(json.dumps(stats, indent=2))
return stats
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--clips-root", type=Path,
default=REPO_ROOT / "outputs/moge/published_clips")
ap.add_argument("--near-percentiles", type=float, nargs=2, default=(25.0, 60.0),
metavar=("LO", "HI"),
help="depth percentiles bounding the near-field panel's colour range")
ap.add_argument("--fps", type=float, default=TRAJECTORY_FPS)
args = ap.parse_args()
out = []
for d in sorted(p for p in args.clips_root.iterdir() if p.is_dir()):
if not (d / "stats.json").exists():
print(f" skip {d.name}: no stats.json")
continue
out.append(render_clip(d, tuple(args.near_percentiles), args.fps))
print(f"\nrendered {len(out)} clip(s)")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
7.65 kB
·
Xet hash:
37c21d6bc4cb9ea95a5e8f84a7d0b39f58469b457ae6b4f1c23f70c2520bdbfe

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