twanghcmut's picture
download
raw
41.4 kB
#!/usr/bin/env python
"""Package a rendered conditioning set as Cosmos-Transfer2.5-2B control inputs.
``render_conditioning_set.py`` emits the *source of truth*: 16-bit metric
depth, raw-id segmentation, and a CG render, as PNG sequences. None of those
are directly consumable by Cosmos-Transfer2.5, which wants 1280x720 H.264
control videos at 16 FPS whose length is a multiple of 93 frames. This
converts the former into the latter.
Two decisions here are the difference between a control signal that works and
one that quietly degrades the generation:
**The background is a real photograph, not the point cloud.** The capture
camera in this episode is static to machine precision (verified: the per-frame
world-to-camera matrices are bit-identical), so a per-pixel *temporal median*
over the episode's real video removes the moving arm and leaves a clean,
robot-free photographic plate that is pixel-aligned with the render by
construction. Compositing the CG robot/object over that plate -- instead of
over the unprojected point cloud -- eliminates the point cloud's holes,
dithering and 320x180 blockiness from the appearance control entirely. It also
makes the Canny/edge control usable, since the background then carries real
texture edges rather than point-cloud speckle.
**Implausibly near background depth is rejected, not rendered.** The capture is a
ZED stereo pair, and in this frame its minimum reported depth is 233 mm. Stereo
matching failure saturates disparity, which puts the failed pixels at the sensor's
*near* limit -- so a cluster of background readings pinned just above 233 mm, in
exactly the dark and textureless parts of the frame (the black curtain, the far wall),
is a failure mode and not a measurement. Left in, those readings are the *brightest*
thing in an inverse-depth control: they both draw a hard bogus structure across the
upper frame and drag the normalisation range so everything real loses contrast. They
are dropped and re-filled from valid neighbours. This is applied to the background
only -- the robot genuinely does come within 30 cm of this wide-angle camera, and its
depth comes from the mesh renderer, which is never touched by the filter.
**Depth is normalised once for the whole clip, never per frame.** Cosmos's own
depth pipeline (DepthAnything) produces *relative inverse* depth, so this
converts metric metres to inverse depth and maps it to 8-bit. Doing that
per-frame -- the obvious implementation -- makes the depth video flicker as
the scene's near/far extremes change, and the model faithfully reproduces the
flicker as brightness pumping. A single global range fixed across the clip
costs nothing and removes the failure mode. Robust percentiles rather than
min/max, so one stray pixel cannot crush the usable range.
Usage:
PYTHONPATH=src python scripts/export_cosmos_input.py \\
--render-dir outputs/conditioning_cosmos_720p \\
--episode AUTOLab+0d4edc83+2023-10-21-19h-07m-04s --camera ext1 \\
--start-row 20 \\
--out-dir outputs/cosmos_input
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
import cv2
import numpy as np
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "src"))
from fpgm.utils.logging import get_logger, setup_logging # noqa: E402
logger = get_logger("export_cosmos_input")
_FFMPEG = Path.home() / "miniconda3" / "envs" / "ffmpeg_libs" / "bin" / "ffmpeg"
#: Cosmos-Transfer2.5-2B hard requirements, from its model card. Checked, not
#: assumed: a silently-wrong resolution or frame count is the kind of thing
#: that produces a degraded generation rather than an error.
_REQUIRED_WIDTH, _REQUIRED_HEIGHT = 1280, 720
_REQUIRED_FPS = 16
_PREFERRED_FRAME_MULTIPLE = 93
#: Stable across frames *and* across runs. A palette that changes with the set
#: of ids present in a given frame makes the segmentation control flicker
#: identity, which is worse than having no segmentation control at all.
_SEG_PALETTE: dict[int, tuple[int, int, int]] = {
0: (0, 0, 0),
1: (0, 200, 255),
2: (255, 80, 0),
3: (80, 255, 80),
4: (255, 0, 200),
5: (255, 220, 0),
}
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
p.add_argument("--render-dir", type=Path, required=True,
help="output of render_conditioning_set.py (rgb/, depth/, seg/, meta.json)")
p.add_argument("--out-dir", type=Path, required=True)
p.add_argument("--episode", required=True, help="DROID episode uuid (for the real video)")
p.add_argument("--camera", default="ext1", choices=["ext1", "ext2"])
p.add_argument("--start-row", type=int, default=20,
help="trajectory row 0 corresponds to this real video frame index")
p.add_argument("--background", choices=["plate", "cloud"], default="plate",
help="plate = real temporal-median photograph (default, recommended); "
"cloud = the renderer's own point-cloud background")
p.add_argument("--fps", type=int, default=_REQUIRED_FPS)
p.add_argument("--blur-sigma", type=float, default=18.0,
help="Gaussian sigma for the 'vis' (blurred RGB) control, in pixels at 720p")
p.add_argument("--depth-percentile", type=float, default=1.0,
help="robust percentile for the global inverse-depth range (each tail)")
p.add_argument("--min-background-depth", type=float, default=0.40,
help="metres. BACKGROUND depth readings nearer than this are treated as "
"stereo-matching failures rather than measurements and are re-filled "
"from valid neighbours. Robot/object mesh depth is never touched. "
"0 disables. See the module docstring for the justification.")
p.add_argument("--depth-normalize", choices=["global", "perframe"], default="global",
help="global is strongly recommended; perframe is provided only for A/B")
p.add_argument("--weight-vis-fg", type=float, default=0.40,
help="control_weight for the vis (blur) branch")
p.add_argument("--weight-depth-fg", type=float, default=0.90,
help="control_weight for the depth branch")
p.add_argument("--spec-name", default="pick_place_books",
help="'name' field written into each spec_*.json")
p.add_argument("--guidance", type=float, default=3.0,
help="classifier-free guidance; 3 is what the shipped examples use")
p.add_argument("--crf", type=int, default=12, help="x264 CRF for control videos (low = clean)")
p.add_argument("--prompt-file", type=Path, default=None,
help="override the built-in prompt (must be < 300 words)")
return p.parse_args()
# --------------------------------------------------------------------------- #
# ffmpeg
# --------------------------------------------------------------------------- #
class _H264Writer:
"""Raw RGB24 -> H.264/yuv420p mp4, via an ffmpeg pipe.
cv2.VideoWriter is avoided deliberately: its codec/pixel-format selection
depends on how OpenCV was built, and a control video that silently lands in
a different pixel format or gets chroma-subsampled differently is exactly
the kind of difference that is invisible in a preview and harmful to the
model. Here every parameter is explicit.
"""
def __init__(self, path: Path, width: int, height: int, fps: int, crf: int):
path.parent.mkdir(parents=True, exist_ok=True)
self.path = path
cmd = [
str(_FFMPEG), "-y", "-loglevel", "error",
"-f", "rawvideo", "-pix_fmt", "rgb24",
"-s", f"{width}x{height}", "-r", str(fps), "-i", "pipe:0",
"-c:v", "libx264", "-preset", "slow", "-crf", str(crf),
"-pix_fmt", "yuv420p",
# Closed GOP, no scene-cut re-keying: decoders (and dataloaders that
# seek) then agree exactly on frame indices, which matters when four
# control videos must stay frame-aligned with each other.
"-g", "16", "-sc_threshold", "0",
"-movflags", "+faststart",
str(path),
]
self._p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
def write(self, rgb: np.ndarray) -> None:
self._p.stdin.write(np.ascontiguousarray(rgb, dtype=np.uint8).tobytes())
def close(self) -> None:
self._p.stdin.close()
err = self._p.stderr.read().decode(errors="replace")
rc = self._p.wait()
if rc != 0:
raise RuntimeError(f"ffmpeg failed for {self.path}: {err}")
# --------------------------------------------------------------------------- #
# background plate
# --------------------------------------------------------------------------- #
def _real_video_path(episode: str, camera: str, render_meta_dir: Path) -> tuple[Path, str]:
"""Locate the episode mp4 for `camera`, resolving ext1/ext2 -> serial via the mask meta."""
ep_dir = REPO_ROOT / "data" / "droid_raw" / episode / "recordings" / "MP4"
serial = None
for meta in sorted((REPO_ROOT / "outputs" / episode).glob("objects*/*/objects/1_mask/meta.json")):
m = json.loads(meta.read_text())
if m.get("camera") == camera:
serial = m.get("camera_serial")
break
if serial is None:
raise SystemExit(
f"could not resolve {camera} to a camera serial from any mask meta.json under "
f"outputs/{episode}/ -- pass the right --episode/--camera"
)
path = ep_dir / f"{serial}.mp4"
if not path.exists():
raise SystemExit(f"episode video not found: {path}")
return path, serial
def _temporal_median_plate(video_path: Path, cache: Path) -> np.ndarray:
"""Per-pixel temporal median of the whole episode -> a robot-free RGB plate.
Valid only because the capture camera does not move (asserted by the caller
against the render's own per-frame extrinsics). The arm occupies any given
background pixel for well under half the episode, so the median lands on
the background at every pixel it ever vacates.
"""
if cache.exists():
logger.info("background plate: reusing %s", cache)
return cv2.cvtColor(cv2.imread(str(cache)), cv2.COLOR_BGR2RGB)
cap = cv2.VideoCapture(str(video_path))
frames = []
while True:
ok, bgr = cap.read()
if not ok:
break
frames.append(bgr)
cap.release()
if not frames:
raise SystemExit(f"no frames decoded from {video_path}")
plate_bgr = np.median(np.stack(frames), axis=0).astype(np.uint8)
cache.parent.mkdir(parents=True, exist_ok=True)
cv2.imwrite(str(cache), plate_bgr)
logger.info("background plate: temporal median of %d real frames -> %s", len(frames), cache)
return cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2RGB)
def _read_real_frame(video_path: Path, index: int) -> np.ndarray | None:
cap = cv2.VideoCapture(str(video_path))
cap.set(cv2.CAP_PROP_POS_FRAMES, index)
ok, bgr = cap.read()
cap.release()
return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) if ok else None
# --------------------------------------------------------------------------- #
# channel conversion
# --------------------------------------------------------------------------- #
def _fill_holes_smooth(values: np.ndarray, valid: np.ndarray, levels: int = 7) -> np.ndarray:
"""Fill depth holes by pyramid push-pull, keeping measured pixels exact.
**Do not replace this with a nearest-valid-pixel fill.** That was the first
implementation here and it produced a visible, serious artifact in the
generated video: assigning every hole pixel the value of its nearest valid
pixel is *precisely* a Voronoi partition, so a filled region breaks into
flat polygonal cells meeting along straight seams. With ~17% of pixels
holed and the holes forming large contiguous blobs, that paints a
tessellation of fake planar facets into the depth control -- and the model
faithfully rendered them as shattered-glass surfaces hanging in the scene.
It is nearly invisible in a downscaled preview of the depth video and
obvious the moment you run an edge detector over it, which is how it was
finally caught.
Push-pull instead: decimate value and coverage through a Gaussian pyramid,
then reconstruct coarse-to-fine, letting each level supply only what the
finer level lacks. The interpolant is smooth by construction, so it adds no
edges that were not already in the data. Measured pixels are written back
verbatim afterwards -- the fill only ever invents values where there were
none, and it invents them smoothly, which is the honest representation of
"we don't know" for a depth surface.
"""
if valid.all():
return values
m = valid.astype(np.float32)
vs, ms = [values.astype(np.float32) * m], [m]
for _ in range(levels):
vs.append(cv2.pyrDown(vs[-1]))
ms.append(cv2.pyrDown(ms[-1]))
up = vs[-1] / np.maximum(ms[-1], 1e-6)
for k in range(len(vs) - 2, -1, -1):
up = cv2.resize(up, (vs[k].shape[1], vs[k].shape[0]), interpolation=cv2.INTER_LINEAR)
w = np.clip(ms[k], 0.0, 1.0)
up = vs[k] / np.maximum(ms[k], 1e-6) * w + up * (1.0 - w)
out = up
out[valid] = values[valid]
return out
def _valid_depth(depth_m: np.ndarray, seg: np.ndarray, min_background_m: float) -> np.ndarray:
"""Which depth pixels are measurements.
Zero means "no data" by the renderer's convention. On top of that, a
*background* reading nearer than ``min_background_m`` is rejected: see the
module docstring -- those are the stereo's disparity saturating on dark or
textureless regions, and they land at the sensor's near limit rather than
anywhere near the truth. The foreground is exempt on purpose, because the
arm really does come that close to this camera and its depth is rendered
from the mesh, not measured.
"""
valid = depth_m > 0
if min_background_m > 0:
valid &= ~((seg == 0) & (depth_m < min_background_m))
return valid
def _depth_to_inverse_8bit(depth_m: np.ndarray, lo: float, hi: float) -> np.ndarray:
"""Metric metres -> 8-bit relative inverse depth (near = bright), Cosmos convention."""
inv = 1.0 / np.maximum(depth_m, 1e-6)
return np.clip((inv - lo) / max(hi - lo, 1e-9), 0.0, 1.0).astype(np.float32)
def _colorize_seg(seg: np.ndarray) -> np.ndarray:
out = np.zeros((*seg.shape, 3), dtype=np.uint8)
for sid in np.unique(seg):
out[seg == sid] = _SEG_PALETTE.get(int(sid), (128, 128, 128))
return out
def _feathered_fg(seg: np.ndarray) -> np.ndarray:
"""Foreground coverage in [0,1] with a 1px feather, to kill compositing jaggies."""
fg = (seg != 0).astype(np.float32)
return cv2.GaussianBlur(fg, (3, 3), 0.8)
_DEFAULT_PROMPT = (
"A Franka Panda robot arm with a black two-finger gripper works at a cluttered wooden "
"laboratory workbench. The arm picks up a small blue plastic brick from the bench top, "
"lifts it, carries it smoothly to the right, and sets it down on top of a tall stack of "
"hardcover and paperback books sitting on the bench, then releases the brick and draws "
"back. Behind the bench is a blue-painted pegboard wall with an aluminium extrusion frame "
"and a yellow bar. On the bench are stacked books, a white plastic bin holding black "
"binders, a white mug of pens, a cardboard box, and a black tool case. Below the bench top "
"is a dark steel tool chest with one drawer pulled open. The room is lit by even overhead "
"fluorescent light. The camera is fixed on a tripod and does not move. Photorealistic, "
"sharp focus, natural indoor lighting, real laboratory footage."
)
def _settle_result(render_dir: Path) -> dict | None:
"""Whether this trajectory's release was settled under gravity, and to what.
Returns None if the action sidecar shows no settle step, so the caller keeps
the renderer's (then correct) "no settling" limitation.
"""
path = render_dir / "action.json"
if not path.exists():
return None
try:
action = json.loads(path.read_text())
except json.JSONDecodeError:
return None
sim, final = action.get("simulation"), action.get("final_pose")
if not (isinstance(sim, dict) and sim.get("settled") and isinstance(final, dict)):
return None
return {
"engine": f"MuJoCo {sim.get('mujoco_version', '?')}",
"tilt_release_deg": float(final.get("tilt_from_flat_deg_at_release", float("nan"))),
"tilt_final_deg": float(final.get("tilt_from_flat_deg", float("nan"))),
"settle_time_s": float(sim.get("settle_time_s", float("nan"))),
}
def _readme(args, meta: dict, n: int, w: int, h: int) -> str:
d = meta["depth"]
return f"""\
# Cosmos-Transfer2.5-2B input bundle
{n} frames, {w}x{h}, {meta['fps']} FPS = {meta['duration_s']:.3f} s.
{n} = {n // 93} x 93, the frame multiple the model card reports as best.
## What to feed the model
| File | Role |
|---|---|
| `prompt.txt` | text prompt ({len(meta['prompt'].split())} words; the card requires < 300) |
| `input_video.mp4` | the video being transferred: real background plate + CG robot/brick |
| `depth.mp4` | **depth control** -- relative *inverse* depth, 8-bit, near = bright |
| `fg_mask.mp4` | binary robot+brick mask, for a control's `mask_path` |
| `seg.mp4` | segmentation control (stable palette, see `export_meta.json`) |
| `edge.mp4` | Canny edge control |
| `vis.mp4` | a pre-blurred copy -- **normally unused**, see below |
| `first_frame.png` | **real photograph** of this scene at the trajectory's t=0 |
| `spec_*.json` | ready-to-run parameter files (paths relative to the spec) |
## Run it
python examples/inference.py -i <this dir>/spec_depth_vis.json -o outputs/run1
# multi-GPU:
torchrun --nproc_per_node=4 examples/inference.py -i ... -o ...
| Spec | What it does |
|---|---|
| `spec_depth_vis.json` | **start here.** depth {args.weight_depth_fg} + vis {args.weight_vis_fg} |
| `spec_depth_masked_vis.json` | same, but depth applies only to the robot/brick |
| `spec_depth_only.json` | depth alone at 1.0, matching the shipped example |
| `spec_multicontrol.json` | all four branches |
## Why `vis` has no `control_path`
The repo computes the blur itself from `video_path` when you give `vis` a
`preset_blur_strength` instead of a control video -- and that preset is, by
construction, the blur the branch was trained on. `vis.mp4` here was made with a
hand-picked Gaussian sigma ({args.blur_sigma:g} px), which is a *guess* at that
distribution. The specs therefore use the preset. `vis.mp4` is kept only so you can
A/B it.
Do not raise the `vis` weight much: it is the strongest appearance control, and
pushing it up makes the output a de-blurred CG render rather than a photorealistic
video. `depth` is where our real information is.
## On masks
`mask_path` in this repo is **binary** -- white means "use this control here", black
means "don't". It is not a per-pixel weight map; per-control strength is the scalar
`control_weight`. `spec_depth_masked_vis.json` uses `fg_mask.mp4` to confine depth to
the robot and brick, which is worth trying because foreground depth is exact mesh
z-buffer at full resolution while background depth is upsampled from a 320x180
capture and partly interpolated.
## Two things that are easy to get wrong
**Depth is not metric here.** It is relative inverse depth, matching what
DepthAnything (Cosmos's own extractor) produces, normalised **once for the whole clip**
over `{d['global_range_m'][0]:.3f} .. {d['global_range_m'][1]:.3f} m`. Per-frame
normalisation makes the control flicker and the model turns that into brightness
pumping. The metric 16-bit millimetre PNGs remain in `{meta['source_render_dir']}`
and are the source of truth; regenerate from those, never from `depth.mp4`.
**The background is a real photograph.** The capture camera is static to machine
precision, so a per-pixel temporal median over the real episode footage removes the
moving arm and leaves `background_plate.png`, pixel-aligned with the render by
construction. Only the robot and the brick are CG. This is why `edge.mp4` is usable at
all -- the background carries real texture edges rather than point-cloud speckle.
## Spec schema
Taken from the repo's own shipped examples
(`assets/robot_example/*/*_spec.json`), not guessed: `name`, `prompt_path`,
`video_path`, `guidance`, and per-control `{{control_path, control_weight, mask_path}}`.
Paths are relative to the spec file.
## Known limitations inherited from the render
{chr(10).join(f'- **{k}**: {v}' for k, v in meta['source_render_meta'].items())}
- **background depth resolution**: {d['background_depth_native_resolution_note']}
Holes were filled by nearest valid pixel; mean {d['hole_fraction_mean'] * 100:.2f}%,
max {d['hole_fraction_max'] * 100:.2f}% of pixels per frame.
"""
def main() -> int:
args = parse_args()
setup_logging()
rd = args.render_dir
render_meta = json.loads((rd / "meta.json").read_text())
camera_json = json.loads((rd / "camera.json").read_text())
# render_conditioning_set.py states "no_settling_at_release" unconditionally,
# because it cannot know whether the trajectory it was handed had already
# been through the settle step. If the action sidecar shows it has, that
# limitation is simply false for this bundle, and shipping it would tell a
# reader the opposite of what the data does.
limitations = dict(render_meta.get("limitations", {}))
settle = _settle_result(rd)
if settle is not None:
limitations["no_settling_at_release"] = (
"SUPERSEDED for this bundle: the released object WAS settled under gravity "
f"({settle['engine']}). Its tilt at release was {settle['tilt_release_deg']:.2f} deg "
f"and {settle['tilt_final_deg']:.2f} deg after settling, reached in "
f"{settle['settle_time_s']:.3f} s of simulated time. Mass is not load-bearing: "
"a rigid body's fall-and-settle trajectory is mass-independent, which was verified "
"by re-running at 10x mass. Everything else in no_dynamics still holds -- the "
"transport motion itself is kinematic."
)
rgb_paths = sorted((rd / "rgb").glob("*.png"))
depth_paths = sorted((rd / "depth").glob("*.png"))
seg_paths = sorted((rd / "seg").glob("*.png"))
n = len(rgb_paths)
if not (n == len(depth_paths) == len(seg_paths)):
raise SystemExit(f"channel length mismatch: rgb={n} depth={len(depth_paths)} "
f"seg={len(seg_paths)}")
h, w = cv2.imread(str(rgb_paths[0])).shape[:2]
# --- hard requirement checks, up front ---------------------------------
problems = []
if (w, h) != (_REQUIRED_WIDTH, _REQUIRED_HEIGHT):
problems.append(f"resolution is {w}x{h}, Cosmos-Transfer2.5-2B requires "
f"{_REQUIRED_WIDTH}x{_REQUIRED_HEIGHT}")
if args.fps != _REQUIRED_FPS:
problems.append(f"--fps {args.fps}, the model produces {_REQUIRED_FPS} FPS")
if problems:
raise SystemExit("refusing to export:\n - " + "\n - ".join(problems))
if n % _PREFERRED_FRAME_MULTIPLE != 0:
logger.warning(
"%d frames is not a multiple of %d; the model card reports multiples of %d "
"perform best (re-run resample_trajectory.py with --frames %d)",
n, _PREFERRED_FRAME_MULTIPLE, _PREFERRED_FRAME_MULTIPLE,
_PREFERRED_FRAME_MULTIPLE * max(1, round(n / _PREFERRED_FRAME_MULTIPLE)),
)
# The plate is only valid if the camera really is static. Check it rather
# than trusting the earlier observation.
W = np.asarray(camera_json["world_to_cam_per_frame"])
cam_motion = float(np.abs(W - W[0]).max())
if args.background == "plate" and cam_motion > 1e-9:
raise SystemExit(
f"--background plate requires a static camera, but the per-frame extrinsics vary "
f"by {cam_motion:.3e}. Use --background cloud, which re-renders per frame."
)
video_path, serial = _real_video_path(args.episode, args.camera, rd)
args.out_dir.mkdir(parents=True, exist_ok=True)
plate = None
if args.background == "plate":
plate = _temporal_median_plate(video_path, args.out_dir / "background_plate.png")
# The renderer's wording covers appearance and geometry together; with a
# photographic plate only the geometry half still applies.
limitations["single_viewpoint_capture"] = (
"Applies to the DEPTH channel only in this bundle. Background depth is still a "
"single-viewpoint 2.5D capture, so surfaces the camera never saw have no points "
"and any view far from the capture pose exposes real holes. Background "
"*appearance* no longer has this limitation: rgb/vis/edge take their background "
"from a real photograph (temporal median of the episode), not from the cloud."
)
if plate.shape[:2] != (h, w):
plate = cv2.resize(plate, (w, h), interpolation=cv2.INTER_AREA)
# --- pass 1: global inverse-depth range --------------------------------
logger.info("pass 1/2: scanning %d depth frames for the global inverse-depth range", n)
samples, rejected = [], 0
total_bg = 0
for i, dp in enumerate(depth_paths):
d_m = cv2.imread(str(dp), cv2.IMREAD_UNCHANGED).astype(np.float32) / 1000.0
seg = cv2.imread(str(seg_paths[i]), cv2.IMREAD_UNCHANGED)
valid = _valid_depth(d_m, seg, args.min_background_depth)
bg = seg == 0
total_bg += int(bg.sum())
rejected += int((bg & (d_m > 0) & ~valid).sum())
if valid.any():
samples.append(1.0 / d_m[valid])
allinv = np.concatenate(samples)
if args.min_background_depth > 0:
logger.info(
"rejected %d background depth readings nearer than %.2f m as stereo-matching "
"failures (%.2f%% of all background pixels)",
rejected, args.min_background_depth, 100.0 * rejected / max(total_bg, 1),
)
lo = float(np.percentile(allinv, args.depth_percentile))
hi = float(np.percentile(allinv, 100.0 - args.depth_percentile))
logger.info(
"global inverse-depth range: [%.4f, %.4f] 1/m (= %.3f m far .. %.3f m near) "
"from %.1f/%.1f percentiles",
lo, hi, 1.0 / max(lo, 1e-9), 1.0 / max(hi, 1e-9),
args.depth_percentile, 100 - args.depth_percentile,
)
# --- pass 2: write every video -----------------------------------------
fps, crf = args.fps, args.crf
W_ = dict(width=w, height=h, fps=fps, crf=crf)
writers = {
"input_video": _H264Writer(args.out_dir / "input_video.mp4", **W_),
"vis": _H264Writer(args.out_dir / "vis.mp4", **W_),
"depth": _H264Writer(args.out_dir / "depth.mp4", **W_),
"seg": _H264Writer(args.out_dir / "seg.mp4", **W_),
"edge": _H264Writer(args.out_dir / "edge.mp4", **W_),
# Binary, because that is what cosmos-transfer2.5's `mask_path` actually
# takes: white = apply this control here, black = do not. It is NOT a
# continuous weight map -- per-control strength is the scalar
# `control_weight`, and there is no per-pixel weighting to export.
"fg_mask": _H264Writer(args.out_dir / "fg_mask.mp4", **W_),
# The complement, so a control can be confined to the *background*.
# This is what makes the split configuration possible: our background is
# a real photograph and our foreground is CG with exact geometry, so the
# two regions want opposite treatment -- preserve the background's real
# texture, but let the model re-imagine the robot's appearance under a
# hard geometric constraint.
"bg_mask": _H264Writer(args.out_dir / "bg_mask.mp4", **W_),
}
ksize = int(2 * round(3 * args.blur_sigma) + 1)
hole_fracs, first_composite = [], None
logger.info("pass 2/2: writing %d frames of 8 control videos", n)
try:
for i in range(n):
render_rgb = cv2.cvtColor(cv2.imread(str(rgb_paths[i])), cv2.COLOR_BGR2RGB)
seg = cv2.imread(str(seg_paths[i]), cv2.IMREAD_UNCHANGED)
d_mm = cv2.imread(str(depth_paths[i]), cv2.IMREAD_UNCHANGED).astype(np.float32)
fg = _feathered_fg(seg)[:, :, None]
base = plate if plate is not None else render_rgb
composite = (fg * render_rgb + (1.0 - fg) * base).astype(np.uint8)
if i == 0:
first_composite = composite.copy()
# depth
d_m_raw = d_mm / 1000.0
valid = _valid_depth(d_m_raw, seg, args.min_background_depth)
hole_fracs.append(float(1.0 - valid.mean()))
d_m = _fill_holes_smooth(d_m_raw, valid)
if args.depth_normalize == "perframe":
inv = 1.0 / np.maximum(d_m, 1e-6)
f_lo = float(np.percentile(inv, args.depth_percentile))
f_hi = float(np.percentile(inv, 100 - args.depth_percentile))
dnorm = _depth_to_inverse_8bit(d_m, f_lo, f_hi)
else:
dnorm = _depth_to_inverse_8bit(d_m, lo, hi)
depth_u8 = np.repeat((dnorm * 255).astype(np.uint8)[:, :, None], 3, axis=2)
blurred = cv2.GaussianBlur(composite, (ksize, ksize), args.blur_sigma)
gray = cv2.cvtColor(composite, cv2.COLOR_RGB2GRAY)
edge = np.repeat(cv2.Canny(gray, 100, 200)[:, :, None], 3, axis=2)
hard_fg = (seg != 0)
mask_u8 = np.repeat((hard_fg * 255).astype(np.uint8)[:, :, None], 3, axis=2)
writers["input_video"].write(composite)
writers["vis"].write(blurred)
writers["depth"].write(depth_u8)
writers["seg"].write(_colorize_seg(seg))
writers["edge"].write(edge)
writers["fg_mask"].write(mask_u8)
writers["bg_mask"].write(255 - mask_u8)
if i % 40 == 0:
logger.info(" frame %d/%d", i, n)
finally:
for wr in writers.values():
wr.close()
# --- first frame -------------------------------------------------------
real_first = _read_real_frame(video_path, args.start_row)
if real_first is not None:
if real_first.shape[:2] != (h, w):
real_first = cv2.resize(real_first, (w, h), interpolation=cv2.INTER_AREA)
cv2.imwrite(str(args.out_dir / "first_frame.png"),
cv2.cvtColor(real_first, cv2.COLOR_RGB2BGR))
# Alignment is the whole reason a real photo can be used here: the
# trajectory's frame 0 replays the real recorded joints of this very
# video frame, so the CG arm should land on the real arm. Emit the
# blend so that claim is checkable by eye rather than asserted.
blend = (0.5 * real_first + 0.5 * first_composite).astype(np.uint8)
cv2.imwrite(str(args.out_dir / "first_frame_alignment_check.png"),
cv2.cvtColor(blend, cv2.COLOR_RGB2BGR))
cv2.imwrite(str(args.out_dir / "first_frame_composite.png"),
cv2.cvtColor(first_composite, cv2.COLOR_RGB2BGR))
prompt = args.prompt_file.read_text().strip() if args.prompt_file else _DEFAULT_PROMPT
n_words = len(prompt.split())
if n_words >= 300:
raise SystemExit(f"prompt is {n_words} words; the model card requires fewer than 300")
(args.out_dir / "prompt.txt").write_text(prompt + "\n")
# Spec schema taken from the repo's own examples
# (third_party/cosmos-transfer2.5/assets/robot_example/*/*.json), not guessed.
# Paths are relative to the spec file, which is how the shipped examples do it.
#
# `vis` deliberately carries NO control_path: the repo computes the blur on the
# fly from `video_path` using its own `preset_blur_strength`, which is by
# construction the blur the branch was trained on. Our hand-rolled vis.mp4 is
# still exported, but a hand-chosen Gaussian sigma is a guess at that
# distribution and the preset is not, so the preset is what the specs use.
def _write_spec(name: str, body: dict) -> None:
(args.out_dir / f"spec_{name}.json").write_text(
json.dumps({"name": f"{args.spec_name}_{name}", "prompt_path": "prompt.txt",
"video_path": "input_video.mp4", "guidance": args.guidance, **body},
indent=2) + "\n"
)
_write_spec("depth_vis", {
"depth": {"control_path": "depth.mp4", "control_weight": args.weight_depth_fg},
"vis": {"control_weight": args.weight_vis_fg, "preset_blur_strength": "high"},
})
# Depth restricted to the robot + object. `mask_path` is binary (white = use the
# control here), NOT a weight map. Worth trying because our background depth is
# the weakest channel -- upsampled from a 320x180 capture and part-interpolated --
# while the foreground depth is exact mesh z-buffer at full resolution.
_write_spec("depth_masked_vis", {
"depth": {"control_path": "depth.mp4", "control_weight": 1.0,
"mask_path": "fg_mask.mp4"},
"vis": {"control_weight": args.weight_vis_fg, "preset_blur_strength": "high"},
})
_write_spec("depth_only", {
"depth": {"control_path": "depth.mp4", "control_weight": 1.0},
})
# The one that actually targets photorealism.
#
# `image_context_path` is the style reference, and its default is the trap:
# "If None and context_frame_idx is not provided, use a RANDOM FRAME FROM
# THE INPUT VIDEO". Our input video is a CG composite, so leaving it unset
# silently anchors the output's appearance to CG -- which is exactly the
# failure seen on the first run. first_frame.png is a real photograph of
# this scene at this trajectory's t=0, so it is the right style anchor, and
# it is geometrically consistent with every control channel because the
# capture camera never moves.
#
# `vis` is dropped rather than down-weighted: config.py rejects vis and
# image_context_path together ("both used to transfer style ... please only
# provide one"). Geometry still comes from depth, which is where our real
# information is anyway.
_write_spec("depth_realstyle", {
"image_context_path": "first_frame.png",
"depth": {"control_path": "depth.mp4", "control_weight": args.weight_depth_fg},
})
_write_spec("multicontrol", {
"depth": {"control_path": "depth.mp4", "control_weight": args.weight_depth_fg},
"edge": {"control_path": "edge.mp4", "control_weight": 0.2},
"seg": {"control_path": "seg.mp4", "control_weight": 1.0},
"vis": {"control_weight": args.weight_vis_fg, "preset_blur_strength": "high"},
})
meta = {
"target_model": "nvidia/Cosmos-Transfer2.5-2B",
"prompt": prompt,
"prompt_words": n_words,
"resolution": [w, h],
"frames": n,
"fps": fps,
"duration_s": n / fps,
"frames_is_multiple_of_93": n % 93 == 0,
"source_render_dir": str(rd),
"source_render_meta": limitations,
"episode": args.episode,
"camera": args.camera,
"camera_serial": serial,
"camera_static_max_extrinsic_deviation": cam_motion,
"background": {
"mode": args.background,
"method": (
"per-pixel temporal median of all real frames of the episode video, which "
"removes the moving arm and leaves a robot-free photographic plate. Valid "
"only because the capture camera is static (checked above)."
if args.background == "plate" else
"the renderer's unprojected point-cloud background"
),
},
"depth": {
"convention": "relative INVERSE depth, near = bright, 8-bit -- matching what "
"DepthAnything (Cosmos's own depth extractor) produces, NOT metric "
"depth. The metric 16-bit millimetre PNGs in the render dir remain "
"the source of truth.",
"normalization": args.depth_normalize,
"global_inverse_range_per_m": [lo, hi],
"global_range_m": [1.0 / hi, 1.0 / lo],
"why_global": "Per-frame normalization makes the control video flicker as the "
"scene's near/far extremes change, and the model reproduces that as "
"brightness pumping in the output.",
"hole_fill": "pyramid push-pull (smooth interpolation), measured pixels kept exact, applied before inversion. NOT nearest-neighbour: that is a Voronoi partition and tessellates the filled area into flat polygonal facets, which the model renders as fake glass-like planes.",
"hole_fraction_mean": float(np.mean(hole_fracs)),
"hole_fraction_max": float(np.max(hole_fracs)),
"min_background_depth_m": args.min_background_depth,
"background_readings_rejected": rejected,
"background_readings_rejected_fraction": rejected / max(total_bg, 1),
"why_reject_near_background":
"The ZED's minimum reported depth in this capture is 233 mm. Stereo matching "
"failure saturates disparity and therefore lands at the sensor's NEAR limit, so "
"background readings pinned just above that minimum -- concentrated in the dark "
"curtain and the far wall -- are failures, not measurements. In an inverse-depth "
"control they are the brightest pixels in the frame: they draw a hard bogus "
"structure across the upper frame and compress the range available to everything "
"real. Foreground is exempt: the arm genuinely reaches within 30 cm of this "
"wide-angle camera, and its depth is rendered from the mesh, not measured.",
"background_depth_native_resolution_note":
"Background depth is unprojected from a single ~320x180 capture, so it is "
"smooth/blocky relative to the 1280x720 output. Robot and object depth comes "
"from the mesh renderer at full output resolution and is sharp.",
},
"vis": {
"content": "the composited RGB (real plate + CG robot/object), Gaussian blurred",
"blur_sigma_px": args.blur_sigma,
"kernel_px": ksize,
"not_used_by_default": "The generated specs give the vis branch a "
"preset_blur_strength instead of this file, so the repo blurs "
"video_path itself with the strength the branch was trained "
"on. This file's sigma is a hand-picked guess at that "
"distribution; it is exported only for A/B.",
},
"segmentation": {"id_map": render_meta.get("seg_id_map", {}), "palette": _SEG_PALETTE},
"control_weights": {
"vis": args.weight_vis_fg,
"depth": args.weight_depth_fg,
"guidance": args.guidance,
"rationale": "Depth is where our real information is -- foreground depth is exact "
"mesh z-buffer. vis is the strongest appearance control, so it starts "
"low; raising it turns the output into a de-blurred CG render.",
"mask_semantics": "cosmos-transfer2.5's mask_path is BINARY (white = apply this "
"control here), not a per-pixel weight map. Per-control strength "
"is the scalar control_weight only.",
},
"video_encoding": {"codec": "libx264", "pix_fmt": "yuv420p", "crf": crf,
"gop": 16, "scene_cut_detection": False},
"spec_schema_source":
"third_party/cosmos-transfer2.5/assets/robot_example/*/*_spec.json -- the repo's own "
"shipped examples, read directly rather than inferred from documentation.",
}
(args.out_dir / "export_meta.json").write_text(json.dumps(meta, indent=2, default=str))
(args.out_dir / "README.md").write_text(_readme(args, meta, n, w, h))
print("=" * 78)
print(f"{args.out_dir} {n} frames {w}x{h} @ {fps} fps = {n/fps:.3f} s")
print(f" multiple of 93: {'yes' if n % 93 == 0 else 'NO'} ({n/93:.3f} x 93)")
print(f" depth holes filled: mean {np.mean(hole_fracs)*100:.2f}% "
f"max {np.max(hole_fracs)*100:.2f}%")
print(f" background: {args.background}")
for name in sorted(writers):
f = args.out_dir / f"{name}.mp4"
print(f" {name+'.mp4':22s} {f.stat().st_size/1e6:7.2f} MB")
print("=" * 78)
return 0
if __name__ == "__main__":
raise SystemExit(main())

Xet Storage Details

Size:
41.4 kB
·
Xet hash:
fc6ea390fe28aedea08d5b7cd18bd6289ffae0b52e60da7c8548beb7f354c3cf

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