fix(da2): normalize RGB input and depth prior for ApDepth
Browse files- DA2/depth_anything_v2/dpt.py +91 -16
DA2/depth_anything_v2/dpt.py
CHANGED
|
@@ -192,43 +192,118 @@ class DepthAnythingV2(nn.Module):
|
|
| 192 |
depth = F.interpolate(depth[:, None], (h, w), mode="bilinear", align_corners=True)[0, 0]
|
| 193 |
|
| 194 |
return depth.cpu().numpy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
@torch.no_grad()
|
| 197 |
def infer_batch(self, images: torch.Tensor):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
assert images.ndim == 4 and images.size(1) == 3, \
|
| 199 |
f"Expect input shape [B,3,H,W], but got {images.shape}"
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
-
|
| 205 |
|
| 206 |
new_H = int((H + 13) // 14 * 14)
|
| 207 |
new_W = int((W + 13) // 14 * 14)
|
| 208 |
|
| 209 |
if new_H != H or new_W != W:
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
patch_h, patch_w = new_H // 14, new_W // 14
|
| 215 |
|
| 216 |
features = self.pretrained.get_intermediate_layers(
|
| 217 |
-
|
| 218 |
self.intermediate_layer_idx[self.encoder],
|
| 219 |
-
return_class_token=True
|
| 220 |
)
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
# return depth # [B,1,H,W]
|
| 228 |
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
-
return
|
| 232 |
|
| 233 |
|
| 234 |
|
|
|
|
| 192 |
depth = F.interpolate(depth[:, None], (h, w), mode="bilinear", align_corners=True)[0, 0]
|
| 193 |
|
| 194 |
return depth.cpu().numpy()
|
| 195 |
+
|
| 196 |
+
@staticmethod
|
| 197 |
+
def _prepare_project_rgb_for_dinov2(
|
| 198 |
+
images: torch.Tensor, model_dtype: torch.dtype
|
| 199 |
+
) -> torch.Tensor:
|
| 200 |
+
"""Convert ApDepth RGB tensors to the input domain expected by DINOv2.
|
| 201 |
+
|
| 202 |
+
ApDepth creates ``rgb_norm`` in ``src/dataset/base_depth_dataset.py`` as
|
| 203 |
+
``rgb / 255 * 2 - 1``, so this method receives RGB in [-1, 1]. The
|
| 204 |
+
official DA2 ``image2tensor`` path below instead starts from RGB in
|
| 205 |
+
[0, 1] and applies ImageNet mean/std normalization. Passing ApDepth's
|
| 206 |
+
[-1, 1] tensor directly to DINOv2 skips both conversions and is the
|
| 207 |
+
input-range mismatch fixed here.
|
| 208 |
+
"""
|
| 209 |
+
if not images.is_floating_point():
|
| 210 |
+
raise TypeError(f"Expected floating-point RGB, got {images.dtype}")
|
| 211 |
+
|
| 212 |
+
images_01 = ((images.to(dtype=model_dtype) + 1.0) * 0.5).clamp_(0.0, 1.0)
|
| 213 |
+
mean = images_01.new_tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
|
| 214 |
+
std = images_01.new_tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)
|
| 215 |
+
return (images_01 - mean) / std
|
| 216 |
+
|
| 217 |
+
@staticmethod
|
| 218 |
+
def _relative_disparity_to_apdepth_range(
|
| 219 |
+
disparity: torch.Tensor, output_dtype: torch.dtype
|
| 220 |
+
) -> torch.Tensor:
|
| 221 |
+
"""Map DA2 relative disparity to ApDepth's depth/VAE convention.
|
| 222 |
+
|
| 223 |
+
DA2's relative model emits an unbounded disparity-like value: larger is
|
| 224 |
+
nearer. ApDepth's ``ScaleShiftDepthNormalizer`` uses [-1, 1] with
|
| 225 |
+
``far_plane_at_max=True``: -1 is near and +1 is far. SD2's VAE also
|
| 226 |
+
expects an image-like tensor in [-1, 1]. Returning raw DA2 output from
|
| 227 |
+
``infer_batch`` therefore mismatched both its direction and range.
|
| 228 |
+
|
| 229 |
+
The same 2%/98% robust per-image normalization used for the training GT
|
| 230 |
+
removes DA2's arbitrary relative scale. The subtraction from one then
|
| 231 |
+
changes disparity (near-is-large) into depth (far-is-large).
|
| 232 |
+
"""
|
| 233 |
+
disparity_fp32 = disparity.float()
|
| 234 |
+
flat = disparity_fp32.flatten(1)
|
| 235 |
+
low_q = torch.quantile(flat, 0.02, dim=1).view(-1, 1, 1, 1)
|
| 236 |
+
high_q = torch.quantile(flat, 0.98, dim=1).view(-1, 1, 1, 1)
|
| 237 |
+
disparity_01 = (disparity_fp32 - low_q) / (
|
| 238 |
+
high_q - low_q
|
| 239 |
+
).clamp_min(1e-6)
|
| 240 |
+
disparity_01 = disparity_01.clamp_(0.0, 1.0)
|
| 241 |
+
|
| 242 |
+
depth_01 = 1.0 - disparity_01
|
| 243 |
+
depth_m11 = depth_01.mul(2.0).sub(1.0)
|
| 244 |
+
return depth_m11.to(dtype=output_dtype)
|
| 245 |
|
| 246 |
@torch.no_grad()
|
| 247 |
def infer_batch(self, images: torch.Tensor):
|
| 248 |
+
"""Build a three-channel DA2 prior compatible with ApDepth's SD2 VAE.
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
images: ApDepth RGB tensor [B, 3, H, W] in [-1, 1].
|
| 252 |
+
|
| 253 |
+
Returns:
|
| 254 |
+
Relative depth [B, 3, H, W] in [-1, 1], where -1 is near and +1
|
| 255 |
+
is far. This is a relative prior; it is not metric depth in metres.
|
| 256 |
+
"""
|
| 257 |
assert images.ndim == 4 and images.size(1) == 3, \
|
| 258 |
f"Expect input shape [B,3,H,W], but got {images.shape}"
|
| 259 |
|
| 260 |
+
model_parameter = next(self.parameters())
|
| 261 |
+
device = model_parameter.device
|
| 262 |
+
output_dtype = images.dtype
|
| 263 |
+
# Resize in fp32 because CUDA antialiased bicubic kernels are not
|
| 264 |
+
# available for every fp16/bf16 PyTorch build. DINO input is cast to
|
| 265 |
+
# the actual DA2 weight dtype after resizing.
|
| 266 |
+
images = images.to(device=device, dtype=torch.float32)
|
| 267 |
|
| 268 |
+
_, _, H, W = images.shape
|
| 269 |
|
| 270 |
new_H = int((H + 13) // 14 * 14)
|
| 271 |
new_W = int((W + 13) // 14 * 14)
|
| 272 |
|
| 273 |
if new_H != H or new_W != W:
|
| 274 |
+
images = F.interpolate(
|
| 275 |
+
images,
|
| 276 |
+
size=(new_H, new_W),
|
| 277 |
+
mode="bicubic",
|
| 278 |
+
align_corners=False,
|
| 279 |
+
antialias=True,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# RANGE CONTRACT 1: ApDepth [-1, 1] RGB -> DA2/DINOv2 ImageNet input.
|
| 283 |
+
images_dinov2 = self._prepare_project_rgb_for_dinov2(
|
| 284 |
+
images, model_dtype=model_parameter.dtype
|
| 285 |
+
)
|
| 286 |
|
| 287 |
patch_h, patch_w = new_H // 14, new_W // 14
|
| 288 |
|
| 289 |
features = self.pretrained.get_intermediate_layers(
|
| 290 |
+
images_dinov2,
|
| 291 |
self.intermediate_layer_idx[self.encoder],
|
| 292 |
+
return_class_token=True,
|
| 293 |
)
|
| 294 |
|
| 295 |
+
# DA2 relative output is disparity-like (near is large), not metric depth.
|
| 296 |
+
disparity = F.relu(self.depth_head(features, patch_h, patch_w))
|
| 297 |
+
disparity = F.interpolate(
|
| 298 |
+
disparity, size=(H, W), mode="bilinear", align_corners=False
|
| 299 |
+
)
|
|
|
|
| 300 |
|
| 301 |
+
# RANGE CONTRACT 2: raw DA2 disparity -> ApDepth/VAE depth in [-1, 1].
|
| 302 |
+
depth_m11 = self._relative_disparity_to_apdepth_range(
|
| 303 |
+
disparity, output_dtype=output_dtype
|
| 304 |
+
)
|
| 305 |
|
| 306 |
+
return depth_m11.repeat(1, 3, 1, 1)
|
| 307 |
|
| 308 |
|
| 309 |
|