twanghcmut's picture
download
raw
9.23 kB
"""Depth hole-filling and 8-bit encoding, promoted from ``scripts/export_cosmos_input.py``.
These three functions were first written as private helpers in the Cosmos export
script and were battle-tested there against a real production failure (see
:func:`fill_holes_smooth`). They are promoted here verbatim-in-behaviour so the
next consumer -- the datagen pipeline's dense-depth stage -- gets the fix for
free instead of re-discovering it.
"""
from __future__ import annotations
import cv2
import numpy as np
from fpgm.types import GeometryError
def fill_holes_smooth(values: np.ndarray, valid: np.ndarray, levels: int = 7) -> np.ndarray:
"""Fill 2D holes by Gaussian-pyramid push-pull, keeping measured pixels exact.
**Do not replace this with a nearest-valid-pixel fill.** That was the first
implementation and it produced a visible, serious artifact in a generated
video: assigning every hole pixel the value of its nearest valid pixel is
*precisely* a Voronoi partition of the image plane, 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 painted a
tessellation of fake planar facets into the depth control channel -- and a
video diffusion model conditioned on it faithfully rendered those facets as
shattered-glass surfaces hanging in the scene. It is nearly invisible in a
downscaled preview of the depth video and obvious the moment an edge
detector (or a Laplacian) is run 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 -- there is no length scale at which
it can produce a straight seam. 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 (or any other per-pixel scalar) surface.
Args:
values: ``(H, W)`` float array of measurements. Values at invalid
locations are ignored (never read) -- they do not need to be
pre-zeroed by the caller.
valid: ``(H, W)`` bool array, ``True`` where ``values`` is a real
measurement.
levels: Depth of the Gaussian pyramid. 7 halves the resolution 7 times,
which for typical camera resolutions (VGA to 1080p) reaches a coarse
level small enough that even a large contiguous hole has *some*
coverage to pull from.
Returns:
``(H, W)`` float32 array, equal to ``values`` at ``valid`` pixels and a
smooth fill elsewhere.
Raises:
GeometryError: if ``values`` and ``valid`` disagree in shape, if either
is not 2D, or if ``valid`` is all-``False`` (there is nothing to seed
the fill from, so any output would be pure invention rather than an
interpolation of real data).
"""
values = np.asarray(values)
valid = np.asarray(valid, dtype=bool)
if values.ndim != 2:
raise GeometryError(f"values must be (H, W), got shape {values.shape}")
if values.shape != valid.shape:
raise GeometryError(
f"values and valid must share shape, got {values.shape} vs {valid.shape}"
)
if not valid.any():
raise GeometryError(
"valid is all-False: there are no measured pixels to fill from, so any "
"output here would be pure invention rather than an interpolation."
)
if valid.all():
return values.astype(np.float32)
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.astype(np.float32)
def valid_depth(depth_m: np.ndarray, seg: np.ndarray, min_background_m: float) -> np.ndarray:
"""Which depth pixels are trustworthy measurements.
Zero means "no data" by the renderer's convention. On top of that, a
*background* reading nearer than ``min_background_m`` is rejected: on the ZED
stereo capture this was promoted from, the sensor's minimum reported depth is
233 mm, and stereo matching failure saturates disparity -- so it lands at the
sensor's *near* limit rather than anywhere near the truth. A cluster of
background readings pinned just above that minimum, concentrated in dark or
textureless regions (a black curtain, a far wall), is a failure mode, not a
measurement: left in, those pixels become the *brightest* thing in an
inverse-depth control (see :func:`depth_to_inverse_8bit`), drawing a hard
bogus structure across the frame and dragging the normalisation range so
everything real loses contrast.
The foreground is exempt on purpose: a robot arm or manipulated object
genuinely can come within centimetres of a wide-angle camera, and its depth
typically comes from a mesh renderer's z-buffer, which this stereo-failure
filter has no business touching.
Args:
depth_m: ``(H, W)`` float array, metres.
seg: ``(H, W)`` integer array of segmentation ids, ``0`` = background.
min_background_m: Background depth readings nearer than this are treated
as stereo-matching failures rather than measurements. ``0`` disables
the background-specific rejection (only ``depth_m > 0`` still
applies).
Returns:
``(H, W)`` bool array, ``True`` where ``depth_m`` should be trusted.
Raises:
GeometryError: if ``depth_m`` and ``seg`` disagree in shape.
"""
depth_m = np.asarray(depth_m)
seg = np.asarray(seg)
if depth_m.shape != seg.shape:
raise GeometryError(
f"depth_m and seg must share shape, got {depth_m.shape} vs {seg.shape}"
)
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).
This matches the convention used by relative-depth estimators such as
DepthAnything, which is what Cosmos-Transfer2.5's own depth control channel
was trained against -- so a control video built this way looks, to the model,
like the output of its native depth extractor rather than an unfamiliar
signal.
**Normalisation must be computed once, globally, for the whole clip -- never
per frame.** ``lo``/``hi`` are the *clip-wide* robust percentiles of inverse
depth, computed by the caller once and passed in unchanged for every frame.
Recomputing them per frame is the obvious-looking implementation and it is
wrong: as the scene's near/far extremes drift frame to frame, a per-frame
range makes the encoded video's brightness drift with it, and a video
diffusion model conditioned on that control faithfully reproduces the drift
as visible brightness pumping in the generated output. A single range fixed
across the clip costs nothing to compute and removes the failure mode
entirely. Use robust percentiles (not min/max) when deriving ``lo``/``hi``,
so a single stray near/far pixel cannot crush the usable range for every
other frame in the clip.
Args:
depth_m: Metric depth, any shape, metres. Non-positive values are
clamped away from a division singularity (``1e-6`` metres) rather
than propagating ``inf``/``NaN`` -- callers are expected to have
already filled or masked non-measurements (see
:func:`fill_holes_smooth`, :func:`valid_depth`).
lo: Clip-wide low percentile of inverse depth (1/metres), i.e. the *far*
end of the range.
hi: Clip-wide high percentile of inverse depth (1/metres), i.e. the
*near* end of the range. Must be strictly greater than ``lo``.
Returns:
uint8 array, same shape as ``depth_m``, in ``[0, 255]``; ``0`` at
``lo`` (far), ``255`` at ``hi`` (near), clamped outside that range.
Raises:
GeometryError: if ``hi <= lo``, since that range is degenerate -- every
pixel would silently collapse to the same output value instead of
visibly failing.
"""
if not (hi > lo):
raise GeometryError(f"depth_to_inverse_8bit requires hi > lo, got lo={lo} hi={hi}")
depth_m = np.asarray(depth_m, dtype=np.float32)
inv = 1.0 / np.maximum(depth_m, 1e-6)
norm = np.clip((inv - lo) / (hi - lo), 0.0, 1.0)
return (norm * 255.0).astype(np.uint8)

Xet Storage Details

Size:
9.23 kB
·
Xet hash:
1e1230de36edc0af868d50bc8a9979b57d92f49e107075905b0465dd4bc366f7

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