twanghcmut's picture
download
raw
10.1 kB
"""Depth from PointWorld-DROID ``scene_flows`` tracked 3D points, via local interpolation.
There is no downloadable per-pixel depth map for this dataset (the depth archive is
1.23 TB and not cherry-pickable): ``scene_flows`` -- sparse tracked 3D point
positions -- IS the depth source. This module turns those sparse, per-frame 3D
points into dense-enough queryable depth at arbitrary pixel locations via projected
nearest-neighbour interpolation.
Delaunay/barycentric interpolation was considered and rejected: it silently
interpolates *across* occlusion boundaries (e.g. bridging a gripper edge to the
background behind it), producing smoothly-wrong depth with no signal that anything
went wrong. k-NN with an explicit discontinuity-ratio rejection at least produces a
visible ``REJECTED_DISCONTINUITY`` instead of a plausible-looking lie.
"""
from __future__ import annotations
import numpy as np
from scipy.spatial import cKDTree
from fpgm.config import DepthConfig
from fpgm.depth.base import DepthSource
from fpgm.geometry.camera import Camera
from fpgm.types import DepthMethod, DepthQuery, DepthResult, NoValidDepthAnnotationsError
_EPS = 1e-6
def _points_in_mask(uv: np.ndarray, mask: np.ndarray) -> np.ndarray:
"""Return a boolean ``(N,)`` array of whether each ``uv`` falls inside ``mask``.
Pixel membership **floors**: pixel ``i`` covers the half-open span
``[i, i + 1)``. Rounding instead would admit points up to half a pixel *outside*
the silhouette -- and since this test is what keeps background geometry out of
the depth interpolation, that half-pixel leak is precisely the contamination the
mask restriction exists to prevent.
"""
h, w = mask.shape
u = np.floor(uv[:, 0]).astype(np.int64)
v = np.floor(uv[:, 1]).astype(np.int64)
in_bounds = (u >= 0) & (u < w) & (v >= 0) & (v < h)
out = np.zeros(uv.shape[0], dtype=bool)
safe_u = np.where(in_bounds, u, 0)
safe_v = np.where(in_bounds, v, 0)
out[in_bounds] = mask[safe_v[in_bounds], safe_u[in_bounds]]
return out
def _masked_median_rows(values: np.ndarray, mask: np.ndarray) -> np.ndarray:
"""Row-wise median of ``values`` (Q, k), ignoring entries where ``mask`` is False."""
masked = np.ma.array(values, mask=~mask)
return np.ma.median(masked, axis=1).filled(np.nan)
def _weighted_median_rows(
values: np.ndarray, weights: np.ndarray, mask: np.ndarray
) -> np.ndarray:
"""Row-wise weighted median of ``values`` (Q, k) using ``weights``, masked entries excluded.
A proper weighted median (not a weighted mean): sort each row, walk the
cumulative weight, and take the value at which cumulative weight first reaches
half the row's total weight. Robust to a single bad annotation dominating a
weighted mean.
"""
w = np.where(mask, weights, 0.0)
order = np.argsort(values, axis=1)
sorted_vals = np.take_along_axis(values, order, axis=1)
sorted_w = np.take_along_axis(w, order, axis=1)
cum_w = np.cumsum(sorted_w, axis=1)
total_w = cum_w[:, -1:]
threshold = total_w / 2.0
reached = cum_w >= threshold
first_reached = np.argmax(reached, axis=1)
return sorted_vals[np.arange(values.shape[0]), first_reached]
class SceneFlowDepthSource(DepthSource):
"""Provides metric depth by projecting ``scene_flows`` and interpolating in image space.
All HDF5 I/O happens elsewhere; this class only holds already-loaded numpy
arrays plus the resolved frame convention.
"""
def __init__(
self,
camera: Camera,
scene_flows: np.ndarray,
scene_visibility: np.ndarray,
scene_depth_valid: np.ndarray,
frame_is_world: bool,
cfg: DepthConfig,
) -> None:
"""Build a scene-flow-backed depth source.
Args:
camera: Camera at the scene-flow annotation resolution (rescaled
internally per query to ``q.query_resolution``).
scene_flows: ``(T, N, 3)`` tracked 3D point positions.
scene_visibility: ``(T, N)`` bool, whether each point is visible.
scene_depth_valid: ``(T, N)`` bool, whether each point's depth annotation
is trustworthy.
frame_is_world: Whether ``scene_flows`` positions are WORLD-frame
(project via ``camera.project``) or CAMERA-frame (``camera.project_cam``).
Required, not optional -- it must come from
:func:`fpgm.geometry.convention.detect_scene_flow_convention`, never
assumed.
cfg: Interpolation tunables.
"""
self._camera = camera
self._scene_flows = np.asarray(scene_flows, dtype=np.float64)
self._scene_visibility = np.asarray(scene_visibility, dtype=bool)
self._scene_depth_valid = np.asarray(scene_depth_valid, dtype=bool)
self._frame_is_world = frame_is_world
self._cfg = cfg
def query(self, q: DepthQuery) -> DepthResult:
"""Return interpolated metric depth at ``q.uv`` for frame ``q.frame_idx``.
Algorithm:
1. Project that frame's ``scene_flows`` reference points; the support
set is visible & depth-valid & in-front-of-camera points.
2. If ``q.object_mask`` is given, restrict support to reference points
that themselves project inside the mask -- the key defence against a
background point near in image space but on a different surface
corrupting the interpolation of a *moving* object's depth. Falls
back to unrestricted support (with a confidence penalty) if too few
masked reference points remain.
3. One ``cKDTree`` built once over the support set, queried in a single
batched k-NN call -- O((N + Q) log N), never O(Q * N).
4. Per query point: k-NN within ``max_image_radius_px``; reject for too
little support, then reject neighbours off the local depth median by
more than ``depth_discontinuity_ratio`` (occlusion-boundary defence).
5. Depth = inverse-distance-weighted **median** (not mean) of survivors.
Raises:
NoValidDepthAnnotationsError: if the frame has zero usable support
points at all (distinct from a single query point being rejected).
"""
cfg = self._cfg
frame_idx = q.frame_idx
cam = self._camera.rescaled(*q.query_resolution)
ref_points = self._scene_flows[frame_idx]
if self._frame_is_world:
ref_uv, ref_depth = cam.project(ref_points)
else:
ref_uv, ref_depth = cam.project_cam(ref_points)
base_support = (
self._scene_visibility[frame_idx]
& self._scene_depth_valid[frame_idx]
& (ref_depth > 0)
)
if not np.any(base_support):
raise NoValidDepthAnnotationsError(
f"frame {frame_idx} has zero scene_flows points with valid "
"visibility/depth_valid/in-front-of-camera support."
)
used_fallback = False
used_support = base_support
if q.object_mask is not None:
in_mask = _points_in_mask(ref_uv, q.object_mask)
restricted = base_support & in_mask
if int(np.count_nonzero(restricted)) < cfg.min_support:
used_fallback = True
used_support = base_support
else:
used_support = restricted
support_idx = np.flatnonzero(used_support)
support_uv = ref_uv[support_idx]
support_depth = ref_depth[support_idx]
n_query = q.uv.shape[0]
tree = cKDTree(support_uv)
k = min(cfg.k_neighbors, support_idx.size)
dist, nn_idx = tree.query(
q.uv, k=k, distance_upper_bound=cfg.max_image_radius_px
)
if k == 1:
# cKDTree.query collapses the k-axis for k=1; restore it for uniform
# downstream shape handling.
dist = dist.reshape(n_query, 1)
nn_idx = nn_idx.reshape(n_query, 1)
valid0 = np.isfinite(dist) & (nn_idx < support_idx.size)
n_support0 = valid0.sum(axis=1)
reject_no_support = n_support0 < cfg.min_support
safe_idx = np.where(valid0, nn_idx, 0)
depths0 = support_depth[safe_idx]
median0 = _masked_median_rows(depths0, valid0)
rel_dev = np.abs(depths0 - median0[:, None]) / np.where(
median0[:, None] != 0, np.abs(median0[:, None]), _EPS
)
valid1 = valid0 & (rel_dev <= cfg.depth_discontinuity_ratio)
n_support1 = valid1.sum(axis=1)
reject_discontinuity = (~reject_no_support) & (n_support1 < cfg.min_support)
ok = ~reject_no_support & ~reject_discontinuity
weights = 1.0 / (dist + _EPS)
depth_est = _weighted_median_rows(depths0, weights, valid1)
mad_rows = np.abs(depths0 - depth_est[:, None])
mad = _masked_median_rows(mad_rows, valid1)
normalised_spread = mad / np.where(depth_est != 0, np.abs(depth_est), _EPS)
confidence = np.clip(
np.minimum(1.0, n_support1 / max(k, 1)) * (1.0 - normalised_spread), 0.0, 1.0
)
out_depth = np.full(n_query, np.nan, dtype=np.float32)
out_depth[ok] = depth_est[ok].astype(np.float32)
out_conf = np.zeros(n_query, dtype=np.float32)
out_conf[ok] = confidence[ok].astype(np.float32)
method = np.empty(n_query, dtype=object)
method[:] = DepthMethod.REJECTED_NO_SUPPORT
method[reject_discontinuity] = DepthMethod.REJECTED_DISCONTINUITY
method[ok] = (
DepthMethod.INTERP_UNMASKED_FALLBACK if used_fallback else DepthMethod.INTERP
)
if used_fallback:
out_conf[ok] *= cfg.unmasked_fallback_penalty
return DepthResult(
depth=out_depth,
valid=ok,
confidence=out_conf,
method=method,
n_support=n_support1.astype(np.int32),
)

Xet Storage Details

Size:
10.1 kB
·
Xet hash:
53b74c577051063dfdbc998ee9262d6be711409930cc639bb3e01799d7113b34

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