Spaces:
Running on Zero
Running on Zero
File size: 13,330 Bytes
5e5d775 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | """
Multi-scale (N-channel) power-transform depth encoding, designed -- like
`multiscale_disparity_torch.py` -- to reuse pretrained RGB VAEs without wasting
channel capacity. This is the power-transform counterpart of the multi-scale
disparity codec.
With three channels and the recommended preset (lam = -3, c = (2, 6, 18)):
y_R = 1 - 2 / (1 + d / (|lam| * c_1))^2 c_1 = 2 (near)
y_G = 1 - 2 / (1 + d / (|lam| * c_2))^2 c_2 = 6 (mid)
y_B = 1 - 2 / (1 + d / (|lam| * c_3))^2 c_3 = 18 (far)
Each channel is an independent power-transform bijection at a different
scale. Decoding inverts each channel and blends the three depth estimates,
w_i = (1 - y_i^2)^2, d_hat = sum_i w_i * d_i / sum_i w_i.
Why (1 - y_i^2)^2?
------------------
For the disparity codec, this weight happens to be exactly proportional to
d^2 / Var(d_hat), which minimises AbsRel. For the power transform that
identity is no longer exact -- the AbsRel-optimal weight is
w_i^opt proportional to (1 - y_i)^2 * (sqrt(2) - sqrt(1 - y_i))^2.
In our 5-dataset VAE-noise simulation (sigma_y in [0.01, 0.05]) the two weights
were within 0.5%, so we keep the simpler (1 - y^2)^2 as the default and expose
the optimal form as an opt-in (`weight = "opt"`).
Why c = (2, 6, 18) ?
--------------------
Power transform's tail decays as d^-3 (vs d^-2 for disparity), so each channel
covers a *narrower* band of depth. Empirically, ratio r = 3 between adjacent
c's outperforms r = 4 (which we use for disparity) by ~5% AbsRel; r = 2 over-
laps too much, r = 4 leaves precision dips between channels.
In our 5-dataset experiment (NYU, ETH3D, DIODE, Sintel, KITTI), this preset
gives ~10% lower AbsRel than 3-channel disparity (2, 8, 32) under the same
VAE noise level -- at the cost of a slightly less-clean optimal weight.
"""
from __future__ import annotations
from typing import Literal, Optional, Sequence, Tuple, Union
import torch
from torch import Tensor
from .power_transform import depth_to_signed_unit, signed_unit_to_depth
# ---------------------------------------------------------------------------
# Default preset. Picked from VAE-noise sweeps over NYU + ETH3D + DIODE +
# Sintel + KITTI; see the docstring for the rationale.
# ---------------------------------------------------------------------------
DEFAULT_LAM: float = -3.0
DEFAULT_C_RGB: Tuple[float, float, float] = (2.0, 6.0, 18.0)
WeightKind = Literal["iyy2", "opt"]
# ---------------------------------------------------------------------------
# Functional API
# ---------------------------------------------------------------------------
def multiscale_power_encode(
d: Tensor, # shape (..., 1, H, W) or (..., H, W)
c_values: Sequence[float] = DEFAULT_C_RGB,
lam: float = DEFAULT_LAM,
mask: Optional[Tensor] = None,
invalid_fill: float = 0.0,
return_mask: bool = False,
channel_dim: int = -3,
) -> Union[Tensor, Tuple[Tensor, Tensor]]:
"""Encode depth to an N-channel power-transform tensor.
Args:
d : depth tensor with a singleton channel dim (or no channel
dim); any leading batch dims are fine.
c_values : per-channel scale parameters. len == output channels.
lam : shared shape parameter for all channels; must be < -1.
mask : optional bool mask, broadcastable to `d`.
invalid_fill : value at invalid pixels (in [-1, 1) space).
return_mask : also return the effective valid mask.
channel_dim : where to stack the N channels (default -3, so a (..., 1,
H, W) input becomes (..., N, H, W)).
Returns:
y : same as `d` but with N channels on `channel_dim`, in [-1, 1)^N.
"""
if lam >= -1.0:
raise ValueError(f"lam must be < -1, got {lam}")
if any(c <= 0 for c in c_values):
raise ValueError(f"all c must be > 0, got {tuple(c_values)}")
# Collapse a singleton channel dim on d, if present, so per-channel
# encoders see a clean (..., H, W) tensor.
if d.dim() >= -channel_dim and d.shape[channel_dim] == 1:
d_bare = d.squeeze(channel_dim)
if mask is not None and mask.shape == d.shape:
mask_bare: Optional[Tensor] = mask.squeeze(channel_dim)
else:
mask_bare = mask
else:
d_bare = d
mask_bare = mask
channels = []
valid_per_ch = []
for c in c_values:
y_i, v_i = depth_to_signed_unit(
d_bare, lam=lam, c=float(c),
mask=mask_bare, invalid_fill=invalid_fill, return_mask=True,
)
channels.append(y_i)
valid_per_ch.append(v_i)
y = torch.stack(channels, dim=channel_dim)
valid = torch.stack(valid_per_ch, dim=channel_dim)
if return_mask:
return y, valid
return y
def multiscale_power_decode(
y: Tensor, # shape (..., N, H, W)
c_values: Sequence[float] = DEFAULT_C_RGB,
lam: float = DEFAULT_LAM,
inf_cap: Optional[float] = 1e4,
eps_boundary: float = 1e-4,
weight: WeightKind = "iyy2",
channel_dim: int = -3,
) -> Tensor:
"""Decode an N-channel power-transform tensor back to single-channel depth.
Each channel is inverted independently using `signed_unit_to_depth`
and the final estimate is the confidence-weighted average,
d_hat = sum_i w_i * d_i / sum_i w_i
Args:
y : encoded tensor, shape (..., N, H, W).
c_values : per-channel scale parameters.
lam : shared shape parameter, must match the encoder.
inf_cap : clamp on the final depth (None disables).
eps_boundary : keep |y| <= 1 - eps so the inverse stays finite.
weight : "iyy2" -> w = (1 - y^2)^2 (cheap, near-optimal)
"opt" -> AbsRel-optimal weight derived for lam = -3.
channel_dim : channel axis (default -3).
"""
if y.shape[channel_dim] != len(c_values):
raise ValueError(
f"y has {y.shape[channel_dim]} channels but "
f"c_values has {len(c_values)} entries"
)
y_c = y.clamp(min=-1.0 + eps_boundary, max=1.0 - eps_boundary)
# Per-channel depth estimates, one slice at a time so each call to
# `signed_unit_to_depth` sees the right `c` (lam is shared).
chans = []
for i, c in enumerate(c_values):
y_i = y_c.select(channel_dim, i)
d_i = signed_unit_to_depth(y_i, lam=lam, c=float(c), inf_cap=None)
chans.append(d_i)
d_per_ch = torch.stack(chans, dim=channel_dim) # (..., N, H, W)
# --- confidence weights --------------------------------------------------
if weight == "iyy2":
w = (1.0 - y_c.pow(2)).pow(2)
elif weight == "opt":
# w_i^opt proportional to (1 - y)^2 * (sqrt(2) - sqrt(1 - y))^2
# Derived from w proportional to d^2 / Var(d_hat) with lam = -3.
if lam != -3.0:
raise NotImplementedError(
"weight='opt' is currently only derived for lam = -3"
)
one_my = (1.0 - y_c).clamp(min=eps_boundary)
w = one_my.pow(2) * (2.0 ** 0.5 - one_my.sqrt()).pow(2)
else:
raise ValueError(f"unknown weight kind {weight!r}")
w = w.clamp(min=1e-12)
d_hat = (w * d_per_ch).sum(dim=channel_dim) / w.sum(dim=channel_dim)
if inf_cap is not None:
d_hat = d_hat.clamp(max=inf_cap)
return d_hat
# ---------------------------------------------------------------------------
# Container class (parallels DepthMultiScaleDisparityTransform)
# ---------------------------------------------------------------------------
class DepthMultiScalePowerTransform:
"""N-channel power-transform codec. Plain container, not an nn.Module.
Example
-------
>>> codec = DepthMultiScalePowerTransform() # lam=-3, c=(2,6,18)
>>> y = codec.encode(depth, mask=mask) # (..., 3, H, W)
>>> d_hat = codec.decode(y_pred) # (..., H, W)
"""
def __init__(
self,
c: Sequence[float] = DEFAULT_C_RGB,
lam: float = DEFAULT_LAM,
inf_cap: Optional[float] = 1e4,
invalid_fill: float = 0.0,
weight: WeightKind = "iyy2",
channel_dim: int = -3,
):
if lam >= -1.0:
raise ValueError(f"lam must be < -1, got {lam}")
self.c_values = tuple(float(_c) for _c in c)
if any(_c <= 0 for _c in self.c_values):
raise ValueError(f"all c must be > 0, got {self.c_values}")
self.lam = float(lam)
self.inf_cap = inf_cap
self.invalid_fill = float(invalid_fill)
self.weight: WeightKind = weight
self.channel_dim = channel_dim
@property
def num_channels(self) -> int:
return len(self.c_values)
def encode(self, d, mask=None, return_mask=False):
return multiscale_power_encode(
d, c_values=self.c_values, lam=self.lam, mask=mask,
invalid_fill=self.invalid_fill, return_mask=return_mask,
channel_dim=self.channel_dim,
)
def decode(self, y):
return multiscale_power_decode(
y, c_values=self.c_values, lam=self.lam,
inf_cap=self.inf_cap, weight=self.weight,
channel_dim=self.channel_dim,
)
def __call__(self, d, valid_mask=None, return_mask=False):
return self.encode(d, mask=valid_mask, return_mask=return_mask)
def __repr__(self):
return (
f"DepthMultiScalePowerTransform("
f"lam={self.lam:.4f}, c={self.c_values}, "
f"weight={self.weight!r}, inf_cap={self.inf_cap}, "
f"invalid_fill={self.invalid_fill})"
)
# ---------------------------------------------------------------------------
# Self-test: round-trip + VAE-noise comparison vs single-channel and disparity
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import math
torch.manual_seed(0)
codec = DepthMultiScalePowerTransform()
print(codec)
# Round-trip sanity (no noise) -- weighted blend converges to true depth
# because each channel's inverse is exact.
d = torch.rand(2, 1, 64, 64, dtype=torch.float64) * 200.0
y = codec(d)
d_hat = codec.decode(y)
print(f"round-trip max err : {(d.squeeze(1) - d_hat).abs().max():.2e} m")
print(f"y shape : {tuple(y.shape)}")
print(f"y range per channel : "
f"R [{y[:, 0].min():+.3f}, {y[:, 0].max():+.3f}] "
f"G [{y[:, 1].min():+.3f}, {y[:, 1].max():+.3f}] "
f"B [{y[:, 2].min():+.3f}, {y[:, 2].max():+.3f}]")
# ---- VAE-noise comparison vs single-channel power & 3-ch disparity ------
def lognormal(mean_m, sigma, n=200_000, cap=None):
x = (torch.randn(n) * sigma + math.log(mean_m)).exp()
return x if cap is None else x.clamp(0.1, cap)
datasets = {
"NYU": lognormal(2.5, 0.5, cap=10.0),
"ETH3D": lognormal(5.0, 0.8, cap=70.0),
"DIODE": lognormal(8.0, 1.2, cap=350.0),
"Sintel": lognormal(10.0, 0.6, cap=100.0),
"KITTI": lognormal(15.0, 0.7, cap=80.0),
}
# 3-ch disparity (c = 2, 8, 32) for an apples-to-apples baseline.
from multiscale_disparity_torch import DepthMultiScaleDisparityTransform
disp_codec = DepthMultiScaleDisparityTransform() # (2, 8, 32)
pwr_codec = DepthMultiScalePowerTransform() # (2, 6, 18)
pwr_codec_o = DepthMultiScalePowerTransform(weight="opt") # AbsRel-optimal
print("\nVAE-noise AbsRel (sigma_y = 0.01):")
print(f"{'dataset':<8} {'3-ch disp':>10} {'3-ch pwr (1-y2)2':>18} "
f"{'3-ch pwr opt':>14} {'pwr vs disp':>12}")
print("-" * 72)
sigma = 0.01
for ds_name, d_arr in datasets.items():
d_arr = d_arr.reshape(1, 1, -1, 1)
y_d = disp_codec.encode(d_arr) + torch.randn(
1, 3, d_arr.shape[-2], 1) * sigma
ar_d = ((disp_codec.decode(y_d) - d_arr.squeeze(1)).abs()
/ d_arr.squeeze(1)).mean().item()
y_p = pwr_codec.encode(d_arr) + torch.randn(
1, 3, d_arr.shape[-2], 1) * sigma
ar_p = ((pwr_codec.decode(y_p) - d_arr.squeeze(1)).abs()
/ d_arr.squeeze(1)).mean().item()
y_po = pwr_codec_o.encode(d_arr) + torch.randn(
1, 3, d_arr.shape[-2], 1) * sigma
ar_po = ((pwr_codec_o.decode(y_po) - d_arr.squeeze(1)).abs()
/ d_arr.squeeze(1)).mean().item()
imp = (ar_d - ar_p) / ar_d * 100.0
print(f"{ds_name:<8} {ar_d:>10.4f} {ar_p:>18.4f} {ar_po:>14.4f} "
f"{imp:>11.1f}%")
# ---- Per-channel inspection at one specific depth ------------------------
d_pt = torch.tensor([[[1.0, 6.0, 30.0, 100.0]]]) # (1, 1, 4)
y_pt = pwr_codec.encode(d_pt.unsqueeze(-1)) # (1, 3, 4, 1)
print("\nPer-channel y at d = [1, 6, 30, 100] m (c = (2, 6, 18)):")
for i, c in enumerate(pwr_codec.c_values):
row = y_pt[0, i, :, 0].tolist()
print(f" ch{i} (c={c:>4.1f}) y = "
+ " ".join(f"{v:+.3f}" for v in row))
|