twanghcmut/backup-foundation-physics / scripts /export_moge_pointcloud_control.py
twanghcmut's picture
download
raw
6.81 kB
#!/usr/bin/env python
"""Build a VACE control video by re-rendering a clip's MoGe-2 point cloud.
Env + objects come entirely from MoGe: every pixel is unprojected with MoGe's own
per-frame estimated intrinsics, then z-buffer-splatted back into the same camera.
Colour is the real frame's RGB carried on the points, so what reaches VACE is
photograph-like rather than a false-colour geometry buffer.
That last point is the whole design constraint, and it is measured, not stylistic:
``wan/vace.py::vace_encode_frames`` runs ``vae.encode()`` on the control, so the
control passes through Wan's natural-video VAE before the DiT sees it. Feeding this
project's geometry buffers straight in scored PSNR 9.2-9.9 with the generated clip's
temporal activity pinned at the *control's* (0.0056 vs the real 0.0131) -- i.e. pure
passthrough. Compositing something photograph-like instead scored 17.0-18.0.
What the splat gaps mean: they are the honest signature of a point cloud rendered
from a single view -- disocclusions and depth-discontinuity thinning. They are NOT
filled, because filling them would hide exactly the thing this probe is meant to
show (how complete MoGe's geometry is).
Usage:
/home/quang/miniconda3/envs/fpgm/bin/python scripts/export_moge_pointcloud_control.py \\
--clip-dir outputs/moge/sample_bucket/<uuid> --out outputs/zeroshot_moge/<uuid>
"""
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 = Path("/home/quang/miniconda3/envs/ffmpeg_libs/bin/ffmpeg")
def ffmpeg() -> str:
return str(_FFMPEG) if _FFMPEG.exists() else (shutil.which("ffmpeg") or "ffmpeg")
def write_h264(frames_bgr, out: Path, fps: float, crf: int = 14) -> None:
it = iter(frames_bgr)
first = next(it)
h, w = first.shape[:2]
out.parent.mkdir(parents=True, exist_ok=True)
p = subprocess.Popen(
[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)],
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 splat(depth: np.ndarray, rgb: np.ndarray, K: np.ndarray, radius: int) -> tuple[np.ndarray, np.ndarray]:
"""Unproject every valid pixel and z-buffer-splat it back through the same K.
Returns ``(rendered_bgr, filled_mask)``. Nearest point wins per output pixel;
``radius`` dilates each point into a square so the cloud reads as a surface
rather than a stipple, without inventing geometry between disconnected points.
"""
h, w = depth.shape
valid = np.isfinite(depth) & (depth > 0)
ys, xs = np.nonzero(valid)
z = depth[ys, xs].astype(np.float32)
fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2]
X = (xs - cx) / fx * z
Y = (ys - cy) / fy * z
u = np.round(fx * X / z + cx).astype(np.int32)
v = np.round(fy * Y / z + cy).astype(np.int32)
out = np.zeros((h, w, 3), np.uint8)
zbuf = np.full((h, w), np.inf, np.float32)
cols = rgb[ys, xs]
order = np.argsort(-z) # far first, so near overwrite
u, v, z, cols = u[order], v[order], z[order], cols[order]
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
uu, vv = u + dx, v + dy
ok = (uu >= 0) & (uu < w) & (vv >= 0) & (vv < h)
uu, vv, zz, cc = uu[ok], vv[ok], z[ok], cols[ok]
closer = zz < zbuf[vv, uu]
zbuf[vv[closer], uu[closer]] = zz[closer]
out[vv[closer], uu[closer]] = cc[closer]
return out[:, :, ::-1].copy(), np.isfinite(zbuf)
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--clip-dir", type=Path, required=True,
help="outputs/moge/sample_bucket/<uuid> (needs per_frame/*.npz + stats.json)")
ap.add_argument("--out", type=Path, required=True)
ap.add_argument("--radius", type=int, default=1, help="splat half-size in px")
ap.add_argument("--fps", type=float, default=15.0)
args = ap.parse_args()
stats = json.loads((args.clip_dir / "stats.json").read_text())
video = REPO_ROOT / stats["video"]
files = sorted((args.clip_dir / "per_frame").glob("frame_*.npz"))
if not files:
raise SystemExit(f"no npz under {args.clip_dir/'per_frame'}")
cap = cv2.VideoCapture(str(video))
rgbs = []
while True:
ok, f = cap.read()
if not ok:
break
rgbs.append(cv2.cvtColor(f, cv2.COLOR_BGR2RGB))
cap.release()
n = min(len(rgbs), len(files))
print(f"{args.clip_dir.name}: {n} frames, {stats['width']}x{stats['height']}")
args.out.mkdir(parents=True, exist_ok=True)
fill_fracs = []
def frames():
for i in range(n):
d = np.load(files[i])
depth = d["depth"].astype(np.float32)
depth = np.where(d["mask"].astype(bool), depth, np.nan)
K = d["intrinsics"].astype(np.float64).copy()
# MoGe returns NORMALISED intrinsics (fx, fy, cx, cy in [0,1] units of
# image size). Denormalise before any pixel arithmetic -- using them raw
# would put every point within one pixel of the origin.
h, w = depth.shape
K[0, :] *= w
K[1, :] *= h
img, filled = splat(depth, rgbs[i], K, args.radius)
fill_fracs.append(float(filled.mean()))
yield img
out_video = args.out / "control_moge_pointcloud.mp4"
write_h264(frames(), out_video, args.fps)
ref = args.out / "ref_frame0.png"
cv2.imwrite(str(ref), rgbs[0][:, :, ::-1])
meta = {"uuid": args.clip_dir.name, "source_video": stats["video"],
"n_frames": n, "splat_radius": args.radius,
"fill_fraction_mean": round(float(np.mean(fill_fracs)), 4),
"fill_fraction_min": round(float(np.min(fill_fracs)), 4),
"control": str(out_video), "ref_image": str(ref),
"instruction": stats.get("instruction")}
(args.out / "control_meta.json").write_text(json.dumps(meta, indent=2))
print(f"fill fraction mean {meta['fill_fraction_mean']:.4f} "
f"min {meta['fill_fraction_min']:.4f}")
print(f"wrote {out_video}\nwrote {ref}")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
6.81 kB
·
Xet hash:
f7ee6d38a7205e248c43323f3f8ab7dd93628abae8d41762ce28b387c25d1d9d

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