Spaces:
Running on Zero
Running on Zero
File size: 16,101 Bytes
87608ea | 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 | """Robust scalar, affine-depth, and point-cloud alignment primitives.
These routines estimate scale and shift parameters under missing data and
outliers, using low-resolution robust solvers where required by the evaluation
protocol. Returned parameters are batched tensors that callers apply to
full-resolution depth maps or point clouds.
"""
import math
from typing import Callable, Optional, Tuple, Union
import torch
def scatter_min(size: int, dim: int, index: torch.LongTensor, src: torch.Tensor) -> torch.return_types.min:
"""Scatter-reduce values by minimum and recover source indices.
Args:
size: Length of the reduced output dimension.
dim: Dimension of ``src`` along which group indices apply.
index: Long tensor broadcast-compatible with ``src`` assigning each
source value to an output group.
src: Floating source tensor of arbitrary shape.
Returns:
``torch.return_types.min`` containing grouped minimum values and source
indices, each shaped like ``src`` with dimension ``dim`` replaced by
``size``.
"""
shape = src.shape[:dim] + (size,) + src.shape[dim + 1:]
minimum = torch.full(shape, float('inf'), dtype=src.dtype, device=src.device).scatter_reduce(dim=dim, index=index, src=src, reduce='amin', include_self=False)
minimum_where = torch.where(src == torch.gather(minimum, dim=dim, index=index))
indices = torch.full(shape, -1, dtype=torch.long, device=src.device)
indices[(*minimum_where[:dim], index[minimum_where], *minimum_where[dim + 1:])] = minimum_where[dim]
return torch.return_types.min((minimum, indices))
def split_batch_fwd(fn: Callable, chunk_size: int, *args, **kwargs):
"""Evaluate a tensor function in chunks along its leading batch axis.
Args:
fn: Callable accepting the supplied positional and keyword arguments.
chunk_size: Maximum leading-axis tensor length per invocation.
*args: Tensor arguments sharing leading batch length, or constants
repeated for every chunk.
**kwargs: Keyword equivalents of ``args``.
Returns:
Concatenated tensor result, or tuple of concatenated tensors when ``fn``
returns a tuple.
"""
batch_size = next(x for x in (*args, *kwargs.values()) if isinstance(x, torch.Tensor)).shape[0]
n_chunks = batch_size // chunk_size + (batch_size % chunk_size > 0)
splited_args = tuple(arg.split(chunk_size, dim=0) if isinstance(arg, torch.Tensor) else [arg] * n_chunks for arg in args)
splited_kwargs = {k: [v.split(chunk_size, dim=0) if isinstance(v, torch.Tensor) else [v] * n_chunks] for k, v in kwargs.items()}
results = []
for i in range(n_chunks):
chunk_args = tuple(arg[i] for arg in splited_args)
chunk_kwargs = {k: v[i] for k, v in splited_kwargs.items()}
results.append(fn(*chunk_args, **chunk_kwargs))
if isinstance(results[0], tuple):
return tuple(torch.cat(r, dim=0) for r in zip(*results))
else:
return torch.cat(results, dim=0)
def _pad_inf(x_: torch.Tensor):
"""Pad a sorted sequence with negative and positive infinity.
Args:
x_: Tensor ``[...,N]``.
Returns:
Tensor ``[...,N+2]`` with sentinels at both ends.
"""
return torch.cat([torch.full_like(x_[..., :1], -torch.inf), x_, torch.full_like(x_[..., :1], torch.inf)], dim=-1)
def _pad_cumsum(cumsum: torch.Tensor):
"""Pad a cumulative sum with zero and its final total.
Args:
cumsum: Cumulative values ``[...,N]``.
Returns:
Tensor ``[...,N+2]`` equal to ``[0,cumsum,total]``.
"""
return torch.cat([torch.zeros_like(cumsum[..., :1]), cumsum, cumsum[..., -1:]], dim=-1)
def _compute_residual(a: torch.Tensor, xyw: torch.Tensor, trunc: float):
"""Evaluate a truncated weighted absolute residual for candidate scales.
Args:
a: Candidate scales ``[K,1]``.
xyw: Stacked source, target, and weight values ``[K,N,3]``.
trunc: Scalar upper bound applied to every weighted residual.
Returns:
Objective values ``[K]``.
"""
return a.mul(xyw[..., 0]).sub_(xyw[..., 1]).abs_().mul_(xyw[..., 2]).clamp_max_(trunc).sum(dim=-1)
def align(x: torch.Tensor, y: torch.Tensor, w: torch.Tensor, trunc: Optional[Union[float, torch.Tensor]] = None, eps: float = 1e-7) -> Tuple[torch.Tensor, torch.Tensor, torch.LongTensor]:
"""Solve robust weighted L1 scale alignment without iterative optimization.
Args:
x: Source values ``[...,N]``.
y: Target values ``[...,N]``.
w: Nonnegative correspondence weights ``[...,N]``.
trunc: Optional scalar/tensor cap for each weighted absolute residual.
eps: Lower bound protecting divisions by near-zero source values.
Returns:
scale: Differentiable optimal scale tensor ``[...]``.
loss: Detached objective value tensor ``[...]``.
index: Long tensor ``[...]`` identifying the correspondence whose ratio
reproduces each selected optimum.
"""
if trunc is None:
x, y, w = torch.broadcast_tensors(x, y, w)
sign = torch.sign(x)
x, y = x * sign, y * sign
y_div_x = y / x.clamp_min(eps)
y_div_x, argsort = y_div_x.sort(dim=-1)
wx = torch.gather(x * w, dim=-1, index=argsort)
derivatives = 2 * wx.cumsum(dim=-1) - wx.sum(dim=-1, keepdim=True)
search = torch.searchsorted(derivatives, torch.zeros_like(derivatives[..., :1]), side='left').clamp_max(derivatives.shape[-1] - 1)
a = y_div_x.gather(dim=-1, index=search).squeeze(-1)
index = argsort.gather(dim=-1, index=search).squeeze(-1)
loss = (w * (a[..., None] * x - y).abs()).sum(dim=-1)
else:
# Reshape to (batch_size, n) for simplicity
x, y, w = torch.broadcast_tensors(x, y, w)
batch_shape = x.shape[:-1]
batch_size = math.prod(batch_shape)
x, y, w = x.reshape(-1, x.shape[-1]), y.reshape(-1, y.shape[-1]), w.reshape(-1, w.shape[-1])
sign = torch.sign(x)
x, y = x * sign, y * sign
wx, wy = w * x, w * y
xyw = torch.stack([x, y, w], dim=-1) # Stacked for convenient gathering
y_div_x = A = y / x.clamp_min(eps)
B = (wy - trunc) / wx.clamp_min(eps)
C = (wy + trunc) / wx.clamp_min(eps)
with torch.no_grad():
# Caculate prefix sum by orders of A, B, C
A, A_argsort = A.sort(dim=-1)
Q_A = torch.cumsum(torch.gather(wx, dim=-1, index=A_argsort), dim=-1)
A, Q_A = _pad_inf(A), _pad_cumsum(Q_A) # Pad [-inf, A1, ..., An, inf] and [0, Q1, ..., Qn, Qn] to handle edge cases.
B, B_argsort = B.sort(dim=-1)
Q_B = torch.cumsum(torch.gather(wx, dim=-1, index=B_argsort), dim=-1)
B, Q_B = _pad_inf(B), _pad_cumsum(Q_B)
C, C_argsort = C.sort(dim=-1)
Q_C = torch.cumsum(torch.gather(wx, dim=-1, index=C_argsort), dim=-1)
C, Q_C = _pad_inf(C), _pad_cumsum(Q_C)
# Caculate left and right derivative of A
j_A = torch.searchsorted(A, y_div_x, side='left').sub_(1)
j_B = torch.searchsorted(B, y_div_x, side='left').sub_(1)
j_C = torch.searchsorted(C, y_div_x, side='left').sub_(1)
left_derivative = 2 * torch.gather(Q_A, dim=-1, index=j_A) - torch.gather(Q_B, dim=-1, index=j_B) - torch.gather(Q_C, dim=-1, index=j_C)
j_A = torch.searchsorted(A, y_div_x, side='right').sub_(1)
j_B = torch.searchsorted(B, y_div_x, side='right').sub_(1)
j_C = torch.searchsorted(C, y_div_x, side='right').sub_(1)
right_derivative = 2 * torch.gather(Q_A, dim=-1, index=j_A) - torch.gather(Q_B, dim=-1, index=j_B) - torch.gather(Q_C, dim=-1, index=j_C)
# Find extrema
is_extrema = (left_derivative < 0) & (right_derivative >= 0)
is_extrema[..., 0] |= ~is_extrema.any(dim=-1) # In case all derivatives are zero, take the first one as extrema.
where_extrema_batch, where_extrema_index = torch.where(is_extrema)
# Calculate objective value at extrema
extrema_a = y_div_x[where_extrema_batch, where_extrema_index] # (num_extrema,)
MAX_ELEMENTS = 4096 ** 2 # Split into small batches to avoid OOM in case there are too many extrema.(~1G)
SPLIT_SIZE = MAX_ELEMENTS // x.shape[-1]
extrema_value = torch.cat([
_compute_residual(extrema_a_split[:, None], xyw[extrema_i_split, :, :], trunc)
for extrema_a_split, extrema_i_split in zip(extrema_a.split(SPLIT_SIZE), where_extrema_batch.split(SPLIT_SIZE))
]) # (num_extrema,)
# Find minima among corresponding extrema
minima, indices = scatter_min(size=batch_size, dim=0, index=where_extrema_batch, src=extrema_value) # (batch_size,)
index = where_extrema_index[indices]
a = torch.gather(y, dim=-1, index=index[..., None]) / torch.gather(x, dim=-1, index=index[..., None]).clamp_min(eps)
a = a.reshape(batch_shape)
loss = minima.reshape(batch_shape)
index = index.reshape(batch_shape)
return a, loss, index
def align_depth_affine(depth_src: torch.Tensor, depth_tgt: torch.Tensor, weight: Optional[torch.Tensor], trunc: Optional[Union[float, torch.Tensor]] = None):
"""Fit robust affine scale and shift between paired scalar values.
Args:
depth_src: Source depth/log-depth values ``[...,N]``.
depth_tgt: Target values ``[...,N]``.
weight: Nonnegative correspondence weights ``[...,N]``.
trunc: Optional robust residual cap forwarded to :func:`align`.
Returns:
scale: Scalar multiplier tensor ``[...]``.
shift: Additive offset tensor ``[...]``.
"""
# Flatten batch dimensions for simplicity
batch_shape, n = depth_src.shape[:-1], depth_src.shape[-1]
batch_size = math.prod(batch_shape)
depth_src, depth_tgt, weight = depth_src.reshape(batch_size, n), depth_tgt.reshape(batch_size, n), weight.reshape(batch_size, n)
# Here, we take anchors only for non-zero weights.
# Although the results will be still correct even anchor points have zero weight,
# it is wasting computation and may cause instability in some cases, e.g. too many extrema.
anchors_where_batch, anchors_where_n = torch.where(weight > 0)
# Stop gradient when solving optimal anchors
with torch.no_grad():
depth_src_anchor = depth_src[anchors_where_batch, anchors_where_n] # (anchors)
depth_tgt_anchor = depth_tgt[anchors_where_batch, anchors_where_n] # (anchors)
depth_src_anchored = depth_src[anchors_where_batch, :] - depth_src_anchor[..., None] # (anchors, n)
depth_tgt_anchored = depth_tgt[anchors_where_batch, :] - depth_tgt_anchor[..., None] # (anchors, n)
weight_anchored = weight[anchors_where_batch, :] # (anchors, n)
scale, loss, index = align(depth_src_anchored, depth_tgt_anchored, weight_anchored, trunc) # (anchors)
loss, index_anchor = scatter_min(size=batch_size, dim=0, index=anchors_where_batch, src=loss) # (batch_size,)
# Reproduce by indexing for shorter compute graph
index_1 = anchors_where_n[index_anchor] # (batch_size,)
index_2 = index[index_anchor] # (batch_size,)
tgt_1, src_1 = torch.gather(depth_tgt, dim=1, index=index_1[..., None]).squeeze(-1), torch.gather(depth_src, dim=1, index=index_1[..., None]).squeeze(-1)
tgt_2, src_2 = torch.gather(depth_tgt, dim=1, index=index_2[..., None]).squeeze(-1), torch.gather(depth_src, dim=1, index=index_2[..., None]).squeeze(-1)
scale = (tgt_2 - tgt_1) / torch.where(src_2 != src_1, src_2 - src_1, 1e-7)
shift = tgt_1 - scale * src_1
scale, shift = scale.reshape(batch_shape), shift.reshape(batch_shape)
return scale, shift
def align_points_scale_xyz_shift(points_src: torch.Tensor, points_tgt: torch.Tensor, weight: Optional[torch.Tensor], trunc: Optional[Union[float, torch.Tensor]] = None, max_iters: int = 30, eps: float = 1e-6):
"""Fit one isotropic scale and a three-axis translation to point pairs.
Args:
points_src: Source camera-space points ``[...,N,3]``.
points_tgt: Target points ``[...,N,3]`` with one-to-one correspondence.
weight: Nonnegative correspondence weights ``[...,N]``.
trunc: Optional robust residual cap.
max_iters: Compatibility argument retained from the research API.
eps: Compatibility stabilizer retained from the research API.
Returns:
scale: Isotropic scale tensor ``[...]``.
shift: XYZ translation tensor ``[...,3]``.
"""
# Flatten batch dimensions for simplicity
batch_shape, n = points_src.shape[:-2], points_src.shape[-2]
batch_size = math.prod(batch_shape)
points_src, points_tgt, weight = points_src.reshape(batch_size, n, 3), points_tgt.reshape(batch_size, n, 3), weight.reshape(batch_size, n)
# Take anchors
anchor_where_batch, anchor_where_n = torch.where(weight > 0)
with torch.no_grad():
points_src_anchor = points_src[anchor_where_batch, anchor_where_n] # (anchors, 3)
points_tgt_anchor = points_tgt[anchor_where_batch, anchor_where_n] # (anchors, 3)
points_src_anchored = points_src[anchor_where_batch, :, :] - points_src_anchor[..., None, :] # (anchors, n, 3)
points_tgt_anchored = points_tgt[anchor_where_batch, :, :] - points_tgt_anchor[..., None, :] # (anchors, n, 3)
weight_anchored = weight[anchor_where_batch, :, None].expand(-1, -1, 3) # (anchors, n, 3)
# Solve optimal scale and shift for each anchor
MAX_ELEMENTS = 2 ** 20
scale, loss, index = split_batch_fwd(align, MAX_ELEMENTS // 2, points_src_anchored.flatten(-2), points_tgt_anchored.flatten(-2), weight_anchored.flatten(-2), trunc) # (anchors,)
# Get optimal scale and shift for each batch element
loss, index_anchor = scatter_min(size=batch_size, dim=0, index=anchor_where_batch, src=loss) # (batch_size,)
index_2 = index[index_anchor] # (batch_size,) [0, 3n)
index_1 = anchor_where_n[index_anchor] * 3 + index_2 % 3 # (batch_size,) [0, 3n)
src_1, tgt_1 = torch.gather(points_src.flatten(-2), dim=1, index=index_1[..., None]).squeeze(-1), torch.gather(points_tgt.flatten(-2), dim=1, index=index_1[..., None]).squeeze(-1)
src_2, tgt_2 = torch.gather(points_src.flatten(-2), dim=1, index=index_2[..., None]).squeeze(-1), torch.gather(points_tgt.flatten(-2), dim=1, index=index_2[..., None]).squeeze(-1)
scale = (tgt_2 - tgt_1) / torch.where(src_2 != src_1, src_2 - src_1, 1.0)
shift = torch.gather(points_tgt, dim=1, index=(index_1 // 3)[..., None, None].expand(-1, -1, 3)).squeeze(-2) - scale[..., None] * torch.gather(points_src, dim=1, index=(index_1 // 3)[..., None, None].expand(-1, -1, 3)).squeeze(-2)
scale, shift = scale.reshape(batch_shape), shift.reshape(*batch_shape, 3)
return scale, shift
def align_affine_lstsq(x: torch.Tensor, y: torch.Tensor, w: torch.Tensor = None) -> Tuple[torch.Tensor, torch.Tensor]:
"""Fit weighted affine scale and shift by linear least squares.
Args:
x: Source scalar values ``[...,N]``.
y: Target scalar values ``[...,N]``.
w: Optional nonnegative least-squares weights ``[...,N]``. ``None``
assigns unit weight.
Returns:
scale: Least-squares multiplier tensor ``[...]``.
shift: Least-squares additive offset tensor ``[...]``.
"""
w_sqrt = torch.ones_like(x) if w is None else w.sqrt()
A = torch.stack([w_sqrt * x, torch.ones_like(x)], dim=-1)
B = (w_sqrt * y)[..., None]
a, b = torch.linalg.lstsq(A, B)[0].squeeze(-1).unbind(-1)
return a, b
|