Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/__init__.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/classifiers.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/depthers.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/dinotxt.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/utils.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/cell_dino/__pycache__/backbones.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/cell_dino/backbones.py +182 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__init__.py +7 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/__init__.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/decode_heads.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/encoder_decoder.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/ops.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/decode_heads.py +747 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/encoder_decoder.py +351 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/ops.py +28 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/dinotxt_model.py +135 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/dinov2_wrapper.py +64 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/text_tower.py +104 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/text_transformer.py +72 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/tokenizer.py +45 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/vision_tower.py +179 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/xray_dino/__pycache__/backbones.cpython-311.pyc +0 -0
- migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/xray_dino/backbones.py +28 -0
- migrator/code/mv-sam3d-for-6d-v2/IMPLEMENTATION.md +208 -0
- migrator/code/mv-sam3d-for-6d-v2/configs/heldout.yaml +70 -0
- migrator/code/mv-sam3d-for-6d-v2/configs/train.yaml +141 -0
- migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated.yaml +127 -0
- migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated_object_ssi.yaml +128 -0
- migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated_scene_ssi.yaml +128 -0
- migrator/code/mv-sam3d-for-6d-v2/docs/TRAINING_FEATURES.md +122 -0
- migrator/code/mv-sam3d-for-6d-v2/docs/dataset_convention_spec.md +44 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__init__.py +9 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__pycache__/__init__.cpython-311.pyc +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__pycache__/paths.cpython-311.pyc +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__init__.py +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/__init__.cpython-311.pyc +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/base_dataset.cpython-311.pyc +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/canonical.cpython-311.pyc +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/dexycb_calib.cpython-311.pyc +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/dexycb_dataset.cpython-311.pyc +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/integrated.cpython-311.pyc +0 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/adapters.py +1402 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/base_dataset.py +272 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/canonical.py +76 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/dexycb_calib.py +75 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/dexycb_dataset.py +501 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/integrated.py +120 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/infer/__init__.py +17 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/infer/predict.py +373 -0
- migrator/code/mv-sam3d-for-6d-v2/mvsam3d/model/__init__.py +0 -0
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (218 Bytes). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/classifiers.cpython-311.pyc
ADDED
|
Binary file (9.57 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/depthers.cpython-311.pyc
ADDED
|
Binary file (10.8 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/dinotxt.cpython-311.pyc
ADDED
|
Binary file (4.12 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (2.81 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/cell_dino/__pycache__/backbones.cpython-311.pyc
ADDED
|
Binary file (5.04 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/cell_dino/backbones.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the CC-by-NC licence,
|
| 4 |
+
# found in the LICENSE_CELL_DINO_CODE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
from enum import Enum
|
| 7 |
+
from typing import Optional, Union
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Weights(Enum):
|
| 13 |
+
CELL_DINO = "CELL-DINO"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _make_cell_dino_model(
|
| 17 |
+
*,
|
| 18 |
+
arch_name: str = "vit_large",
|
| 19 |
+
img_size: int = 518,
|
| 20 |
+
patch_size: int = 14,
|
| 21 |
+
init_values: float = 1.0,
|
| 22 |
+
ffn_layer: str = "mlp",
|
| 23 |
+
block_chunks: int = 0,
|
| 24 |
+
num_register_tokens: int = 0,
|
| 25 |
+
interpolate_antialias: bool = False,
|
| 26 |
+
interpolate_offset: float = 0.1,
|
| 27 |
+
pretrained: bool = True,
|
| 28 |
+
channel_adaptive: bool = False,
|
| 29 |
+
weights: Union[Weights, str] = Weights.CELL_DINO,
|
| 30 |
+
pretrained_url: Optional[str] = None,
|
| 31 |
+
pretrained_path: Optional[str] = None,
|
| 32 |
+
**kwargs,
|
| 33 |
+
):
|
| 34 |
+
from ...models import vision_transformer as vits
|
| 35 |
+
|
| 36 |
+
if isinstance(weights, str):
|
| 37 |
+
try:
|
| 38 |
+
weights = Weights[weights]
|
| 39 |
+
except KeyError:
|
| 40 |
+
raise AssertionError(f"Unsupported weights: {weights}")
|
| 41 |
+
|
| 42 |
+
vit_kwargs = dict(
|
| 43 |
+
img_size=img_size,
|
| 44 |
+
patch_size=patch_size,
|
| 45 |
+
init_values=init_values,
|
| 46 |
+
ffn_layer=ffn_layer,
|
| 47 |
+
block_chunks=block_chunks,
|
| 48 |
+
num_register_tokens=num_register_tokens,
|
| 49 |
+
interpolate_antialias=interpolate_antialias,
|
| 50 |
+
interpolate_offset=interpolate_offset,
|
| 51 |
+
channel_adaptive=channel_adaptive,
|
| 52 |
+
)
|
| 53 |
+
vit_kwargs.update(**kwargs)
|
| 54 |
+
model = vits.__dict__[arch_name](**vit_kwargs)
|
| 55 |
+
|
| 56 |
+
if pretrained:
|
| 57 |
+
if pretrained_path is not None:
|
| 58 |
+
state_dict = torch.load(pretrained_path, map_location="cpu")
|
| 59 |
+
else:
|
| 60 |
+
pretrained_url is not None
|
| 61 |
+
state_dict = torch.hub.load_state_dict_from_url(pretrained_url, map_location="cpu")
|
| 62 |
+
model.load_state_dict(state_dict, strict=True)
|
| 63 |
+
|
| 64 |
+
return model
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def cell_dino_hpa_vitl16(
|
| 68 |
+
*,
|
| 69 |
+
pretrained_url: Optional[str] = None,
|
| 70 |
+
pretrained_path: Optional[str] = None,
|
| 71 |
+
pretrained: bool = True,
|
| 72 |
+
weights: Union[Weights, str] = Weights.CELL_DINO,
|
| 73 |
+
in_channels: int = 4,
|
| 74 |
+
**kwargs,
|
| 75 |
+
):
|
| 76 |
+
"""
|
| 77 |
+
Cell-DINO ViT-L/16 model dataset pretrained on HPA dataset.
|
| 78 |
+
"""
|
| 79 |
+
return _make_cell_dino_model(
|
| 80 |
+
arch_name="vit_large",
|
| 81 |
+
patch_size=16,
|
| 82 |
+
img_size=224,
|
| 83 |
+
num_register_tokens=0,
|
| 84 |
+
interpolate_antialias=False,
|
| 85 |
+
interpolate_offset=0.1,
|
| 86 |
+
block_chunks=4,
|
| 87 |
+
pretrained_url=pretrained_url,
|
| 88 |
+
pretrained_path=pretrained_path,
|
| 89 |
+
pretrained=pretrained,
|
| 90 |
+
weights=weights,
|
| 91 |
+
in_chans=in_channels,
|
| 92 |
+
**kwargs,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def cell_dino_hpa_vitl14(
|
| 97 |
+
*,
|
| 98 |
+
pretrained_url: Optional[str] = None,
|
| 99 |
+
pretrained_path: Optional[str] = None,
|
| 100 |
+
pretrained: bool = True,
|
| 101 |
+
weights: Union[Weights, str] = Weights.CELL_DINO,
|
| 102 |
+
in_channels: int = 4,
|
| 103 |
+
**kwargs,
|
| 104 |
+
):
|
| 105 |
+
"""
|
| 106 |
+
Cell-DINO ViT-L/14 model dataset pretrained on LVD, then on HPA dataset.
|
| 107 |
+
"""
|
| 108 |
+
return _make_cell_dino_model(
|
| 109 |
+
arch_name="vit_large",
|
| 110 |
+
patch_size=14,
|
| 111 |
+
img_size=518,
|
| 112 |
+
num_register_tokens=0,
|
| 113 |
+
interpolate_antialias=False,
|
| 114 |
+
interpolate_offset=0.1,
|
| 115 |
+
block_chunks=4,
|
| 116 |
+
pretrained_url=pretrained_url,
|
| 117 |
+
pretrained_path=pretrained_path,
|
| 118 |
+
pretrained=pretrained,
|
| 119 |
+
weights=weights,
|
| 120 |
+
in_chans=in_channels,
|
| 121 |
+
**kwargs,
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def cell_dino_cp_vits8(
|
| 126 |
+
*,
|
| 127 |
+
pretrained_url: Optional[str] = None,
|
| 128 |
+
pretrained_path: Optional[str] = None,
|
| 129 |
+
pretrained: bool = True,
|
| 130 |
+
weights: Union[Weights, str] = Weights.CELL_DINO,
|
| 131 |
+
in_channels: int = 5,
|
| 132 |
+
**kwargs,
|
| 133 |
+
):
|
| 134 |
+
"""
|
| 135 |
+
Cell-DINO ViT-S/8 model dataset pretrained on the combined cell painting dataset.
|
| 136 |
+
"""
|
| 137 |
+
return _make_cell_dino_model(
|
| 138 |
+
arch_name="vit_small",
|
| 139 |
+
patch_size=8,
|
| 140 |
+
img_size=128,
|
| 141 |
+
num_register_tokens=0,
|
| 142 |
+
interpolate_antialias=False,
|
| 143 |
+
interpolate_offset=0.1,
|
| 144 |
+
block_chunks=4,
|
| 145 |
+
pretrained_url=pretrained_url,
|
| 146 |
+
pretrained_path=pretrained_path,
|
| 147 |
+
pretrained=pretrained,
|
| 148 |
+
weights=weights,
|
| 149 |
+
in_chans=in_channels,
|
| 150 |
+
**kwargs,
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def channel_adaptive_dino_vitl16(
|
| 155 |
+
*,
|
| 156 |
+
pretrained_url: Optional[str] = None,
|
| 157 |
+
pretrained_path: Optional[str] = None,
|
| 158 |
+
pretrained: bool = True,
|
| 159 |
+
weights: Union[Weights, str] = Weights.CELL_DINO,
|
| 160 |
+
in_channels: int = 1,
|
| 161 |
+
channel_adaptive: bool = True,
|
| 162 |
+
**kwargs,
|
| 163 |
+
):
|
| 164 |
+
"""
|
| 165 |
+
Cell-DINO ViT-L/16 model dataset pretrained on HPA dataset.
|
| 166 |
+
"""
|
| 167 |
+
return _make_cell_dino_model(
|
| 168 |
+
arch_name="vit_large",
|
| 169 |
+
patch_size=16,
|
| 170 |
+
img_size=224,
|
| 171 |
+
num_register_tokens=0,
|
| 172 |
+
interpolate_antialias=False,
|
| 173 |
+
interpolate_offset=0.1,
|
| 174 |
+
block_chunks=4,
|
| 175 |
+
pretrained_url=pretrained_url,
|
| 176 |
+
pretrained_path=pretrained_path,
|
| 177 |
+
pretrained=pretrained,
|
| 178 |
+
weights=weights,
|
| 179 |
+
in_chans=in_channels,
|
| 180 |
+
channel_adaptive=channel_adaptive,
|
| 181 |
+
**kwargs,
|
| 182 |
+
)
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
from .decode_heads import BNHead, DPTHead
|
| 7 |
+
from .encoder_decoder import DepthEncoderDecoder
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (382 Bytes). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/decode_heads.cpython-311.pyc
ADDED
|
Binary file (39.2 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/encoder_decoder.cpython-311.pyc
ADDED
|
Binary file (19.9 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/ops.cpython-311.pyc
ADDED
|
Binary file (1.82 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/decode_heads.py
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
import copy
|
| 7 |
+
from functools import partial
|
| 8 |
+
import math
|
| 9 |
+
import warnings
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
|
| 14 |
+
from .ops import resize
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# XXX: (Untested) replacement for mmcv.imdenormalize()
|
| 18 |
+
def _imdenormalize(img, mean, std, to_bgr=True):
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
mean = mean.reshape(1, -1).astype(np.float64)
|
| 22 |
+
std = std.reshape(1, -1).astype(np.float64)
|
| 23 |
+
img = (img * std) + mean
|
| 24 |
+
if to_bgr:
|
| 25 |
+
img = img[::-1]
|
| 26 |
+
return img
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class DepthBaseDecodeHead(nn.Module):
|
| 30 |
+
"""Base class for BaseDecodeHead.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
in_channels (List): Input channels.
|
| 34 |
+
channels (int): Channels after modules, before conv_depth.
|
| 35 |
+
conv_layer (nn.Module): Conv layers. Default: None.
|
| 36 |
+
act_layer (nn.Module): Activation layers. Default: nn.ReLU.
|
| 37 |
+
loss_decode (dict): Config of decode loss.
|
| 38 |
+
Default: ().
|
| 39 |
+
sampler (dict|None): The config of depth map sampler.
|
| 40 |
+
Default: None.
|
| 41 |
+
align_corners (bool): align_corners argument of F.interpolate.
|
| 42 |
+
Default: False.
|
| 43 |
+
min_depth (int): Min depth in dataset setting.
|
| 44 |
+
Default: 1e-3.
|
| 45 |
+
max_depth (int): Max depth in dataset setting.
|
| 46 |
+
Default: None.
|
| 47 |
+
norm_layer (dict|None): Norm layers.
|
| 48 |
+
Default: None.
|
| 49 |
+
classify (bool): Whether predict depth in a cls.-reg. manner.
|
| 50 |
+
Default: False.
|
| 51 |
+
n_bins (int): The number of bins used in cls. step.
|
| 52 |
+
Default: 256.
|
| 53 |
+
bins_strategy (str): The discrete strategy used in cls. step.
|
| 54 |
+
Default: 'UD'.
|
| 55 |
+
norm_strategy (str): The norm strategy on cls. probability
|
| 56 |
+
distribution. Default: 'linear'
|
| 57 |
+
scale_up (str): Whether predict depth in a scale-up manner.
|
| 58 |
+
Default: False.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
in_channels,
|
| 64 |
+
conv_layer=None,
|
| 65 |
+
act_layer=nn.ReLU,
|
| 66 |
+
channels=96,
|
| 67 |
+
loss_decode=(),
|
| 68 |
+
sampler=None,
|
| 69 |
+
align_corners=False,
|
| 70 |
+
min_depth=1e-3,
|
| 71 |
+
max_depth=None,
|
| 72 |
+
norm_layer=None,
|
| 73 |
+
classify=False,
|
| 74 |
+
n_bins=256,
|
| 75 |
+
bins_strategy="UD",
|
| 76 |
+
norm_strategy="linear",
|
| 77 |
+
scale_up=False,
|
| 78 |
+
):
|
| 79 |
+
super(DepthBaseDecodeHead, self).__init__()
|
| 80 |
+
|
| 81 |
+
self.in_channels = in_channels
|
| 82 |
+
self.channels = channels
|
| 83 |
+
self.conf_layer = conv_layer
|
| 84 |
+
self.act_layer = act_layer
|
| 85 |
+
self.loss_decode = loss_decode
|
| 86 |
+
self.align_corners = align_corners
|
| 87 |
+
self.min_depth = min_depth
|
| 88 |
+
self.max_depth = max_depth
|
| 89 |
+
self.norm_layer = norm_layer
|
| 90 |
+
self.classify = classify
|
| 91 |
+
self.n_bins = n_bins
|
| 92 |
+
self.scale_up = scale_up
|
| 93 |
+
|
| 94 |
+
if self.classify:
|
| 95 |
+
assert bins_strategy in ["UD", "SID"], "Support bins_strategy: UD, SID"
|
| 96 |
+
assert norm_strategy in ["linear", "softmax", "sigmoid"], "Support norm_strategy: linear, softmax, sigmoid"
|
| 97 |
+
|
| 98 |
+
self.bins_strategy = bins_strategy
|
| 99 |
+
self.norm_strategy = norm_strategy
|
| 100 |
+
self.softmax = nn.Softmax(dim=1)
|
| 101 |
+
self.conv_depth = nn.Conv2d(channels, n_bins, kernel_size=3, padding=1, stride=1)
|
| 102 |
+
else:
|
| 103 |
+
self.conv_depth = nn.Conv2d(channels, 1, kernel_size=3, padding=1, stride=1)
|
| 104 |
+
|
| 105 |
+
self.relu = nn.ReLU()
|
| 106 |
+
self.sigmoid = nn.Sigmoid()
|
| 107 |
+
|
| 108 |
+
def forward(self, inputs, img_metas):
|
| 109 |
+
"""Placeholder of forward function."""
|
| 110 |
+
pass
|
| 111 |
+
|
| 112 |
+
def forward_train(self, img, inputs, img_metas, depth_gt):
|
| 113 |
+
"""Forward function for training.
|
| 114 |
+
Args:
|
| 115 |
+
inputs (list[Tensor]): List of multi-level img features.
|
| 116 |
+
img_metas (list[dict]): List of image info dict where each dict
|
| 117 |
+
has: 'img_shape', 'scale_factor', 'flip', and may also contain
|
| 118 |
+
'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'.
|
| 119 |
+
For details on the values of these keys see
|
| 120 |
+
`depth/datasets/pipelines/formatting.py:Collect`.
|
| 121 |
+
depth_gt (Tensor): GT depth
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
dict[str, Tensor]: a dictionary of loss components
|
| 125 |
+
"""
|
| 126 |
+
depth_pred = self.forward(inputs, img_metas)
|
| 127 |
+
losses = self.losses(depth_pred, depth_gt)
|
| 128 |
+
|
| 129 |
+
log_imgs = self.log_images(img[0], depth_pred[0], depth_gt[0], img_metas[0])
|
| 130 |
+
losses.update(**log_imgs)
|
| 131 |
+
|
| 132 |
+
return losses
|
| 133 |
+
|
| 134 |
+
def forward_test(self, inputs, img_metas):
|
| 135 |
+
"""Forward function for testing.
|
| 136 |
+
Args:
|
| 137 |
+
inputs (list[Tensor]): List of multi-level img features.
|
| 138 |
+
img_metas (list[dict]): List of image info dict where each dict
|
| 139 |
+
has: 'img_shape', 'scale_factor', 'flip', and may also contain
|
| 140 |
+
'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'.
|
| 141 |
+
For details on the values of these keys see
|
| 142 |
+
`depth/datasets/pipelines/formatting.py:Collect`.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
Tensor: Output depth map.
|
| 146 |
+
"""
|
| 147 |
+
return self.forward(inputs, img_metas)
|
| 148 |
+
|
| 149 |
+
def depth_pred(self, feat):
|
| 150 |
+
"""Prediction each pixel."""
|
| 151 |
+
if self.classify:
|
| 152 |
+
logit = self.conv_depth(feat)
|
| 153 |
+
|
| 154 |
+
if self.bins_strategy == "UD":
|
| 155 |
+
bins = torch.linspace(self.min_depth, self.max_depth, self.n_bins, device=feat.device)
|
| 156 |
+
elif self.bins_strategy == "SID":
|
| 157 |
+
bins = torch.logspace(self.min_depth, self.max_depth, self.n_bins, device=feat.device)
|
| 158 |
+
|
| 159 |
+
# following Adabins, default linear
|
| 160 |
+
if self.norm_strategy == "linear":
|
| 161 |
+
logit = torch.relu(logit)
|
| 162 |
+
eps = 0.1
|
| 163 |
+
logit = logit + eps
|
| 164 |
+
logit = logit / logit.sum(dim=1, keepdim=True)
|
| 165 |
+
elif self.norm_strategy == "softmax":
|
| 166 |
+
logit = torch.softmax(logit, dim=1)
|
| 167 |
+
elif self.norm_strategy == "sigmoid":
|
| 168 |
+
logit = torch.sigmoid(logit)
|
| 169 |
+
logit = logit / logit.sum(dim=1, keepdim=True)
|
| 170 |
+
|
| 171 |
+
output = torch.einsum("ikmn,k->imn", [logit, bins]).unsqueeze(dim=1)
|
| 172 |
+
|
| 173 |
+
else:
|
| 174 |
+
if self.scale_up:
|
| 175 |
+
output = self.sigmoid(self.conv_depth(feat)) * self.max_depth
|
| 176 |
+
else:
|
| 177 |
+
output = self.relu(self.conv_depth(feat)) + self.min_depth
|
| 178 |
+
return output
|
| 179 |
+
|
| 180 |
+
def losses(self, depth_pred, depth_gt):
|
| 181 |
+
"""Compute depth loss."""
|
| 182 |
+
loss = dict()
|
| 183 |
+
depth_pred = resize(
|
| 184 |
+
input=depth_pred, size=depth_gt.shape[2:], mode="bilinear", align_corners=self.align_corners, warning=False
|
| 185 |
+
)
|
| 186 |
+
if not isinstance(self.loss_decode, nn.ModuleList):
|
| 187 |
+
losses_decode = [self.loss_decode]
|
| 188 |
+
else:
|
| 189 |
+
losses_decode = self.loss_decode
|
| 190 |
+
for loss_decode in losses_decode:
|
| 191 |
+
if loss_decode.loss_name not in loss:
|
| 192 |
+
loss[loss_decode.loss_name] = loss_decode(depth_pred, depth_gt)
|
| 193 |
+
else:
|
| 194 |
+
loss[loss_decode.loss_name] += loss_decode(depth_pred, depth_gt)
|
| 195 |
+
return loss
|
| 196 |
+
|
| 197 |
+
def log_images(self, img_path, depth_pred, depth_gt, img_meta):
|
| 198 |
+
import numpy as np
|
| 199 |
+
|
| 200 |
+
show_img = copy.deepcopy(img_path.detach().cpu().permute(1, 2, 0))
|
| 201 |
+
show_img = show_img.numpy().astype(np.float32)
|
| 202 |
+
show_img = _imdenormalize(
|
| 203 |
+
show_img,
|
| 204 |
+
img_meta["img_norm_cfg"]["mean"],
|
| 205 |
+
img_meta["img_norm_cfg"]["std"],
|
| 206 |
+
img_meta["img_norm_cfg"]["to_rgb"],
|
| 207 |
+
)
|
| 208 |
+
show_img = np.clip(show_img, 0, 255)
|
| 209 |
+
show_img = show_img.astype(np.uint8)
|
| 210 |
+
show_img = show_img[:, :, ::-1]
|
| 211 |
+
show_img = show_img.transpose(0, 2, 1)
|
| 212 |
+
show_img = show_img.transpose(1, 0, 2)
|
| 213 |
+
|
| 214 |
+
depth_pred = depth_pred / torch.max(depth_pred)
|
| 215 |
+
depth_gt = depth_gt / torch.max(depth_gt)
|
| 216 |
+
|
| 217 |
+
depth_pred_color = copy.deepcopy(depth_pred.detach().cpu())
|
| 218 |
+
depth_gt_color = copy.deepcopy(depth_gt.detach().cpu())
|
| 219 |
+
|
| 220 |
+
return {"img_rgb": show_img, "img_depth_pred": depth_pred_color, "img_depth_gt": depth_gt_color}
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
class BNHead(DepthBaseDecodeHead):
|
| 224 |
+
"""Just a batchnorm."""
|
| 225 |
+
|
| 226 |
+
def __init__(self, input_transform="resize_concat", in_index=(0, 1, 2, 3), upsample=1, **kwargs):
|
| 227 |
+
super().__init__(**kwargs)
|
| 228 |
+
self.input_transform = input_transform
|
| 229 |
+
self.in_index = in_index
|
| 230 |
+
self.upsample = upsample
|
| 231 |
+
# self.bn = nn.SyncBatchNorm(self.in_channels)
|
| 232 |
+
if self.classify:
|
| 233 |
+
self.conv_depth = nn.Conv2d(self.channels, self.n_bins, kernel_size=1, padding=0, stride=1)
|
| 234 |
+
else:
|
| 235 |
+
self.conv_depth = nn.Conv2d(self.channels, 1, kernel_size=1, padding=0, stride=1)
|
| 236 |
+
|
| 237 |
+
def _transform_inputs(self, inputs):
|
| 238 |
+
"""Transform inputs for decoder.
|
| 239 |
+
Args:
|
| 240 |
+
inputs (list[Tensor]): List of multi-level img features.
|
| 241 |
+
Returns:
|
| 242 |
+
Tensor: The transformed inputs
|
| 243 |
+
"""
|
| 244 |
+
|
| 245 |
+
if "concat" in self.input_transform:
|
| 246 |
+
inputs = [inputs[i] for i in self.in_index]
|
| 247 |
+
if "resize" in self.input_transform:
|
| 248 |
+
inputs = [
|
| 249 |
+
resize(
|
| 250 |
+
input=x,
|
| 251 |
+
size=[s * self.upsample for s in inputs[0].shape[2:]],
|
| 252 |
+
mode="bilinear",
|
| 253 |
+
align_corners=self.align_corners,
|
| 254 |
+
)
|
| 255 |
+
for x in inputs
|
| 256 |
+
]
|
| 257 |
+
inputs = torch.cat(inputs, dim=1)
|
| 258 |
+
elif self.input_transform == "multiple_select":
|
| 259 |
+
inputs = [inputs[i] for i in self.in_index]
|
| 260 |
+
else:
|
| 261 |
+
inputs = inputs[self.in_index]
|
| 262 |
+
|
| 263 |
+
return inputs
|
| 264 |
+
|
| 265 |
+
def _forward_feature(self, inputs, img_metas=None, **kwargs):
|
| 266 |
+
"""Forward function for feature maps before classifying each pixel with
|
| 267 |
+
``self.cls_seg`` fc.
|
| 268 |
+
Args:
|
| 269 |
+
inputs (list[Tensor]): List of multi-level img features.
|
| 270 |
+
Returns:
|
| 271 |
+
feats (Tensor): A tensor of shape (batch_size, self.channels,
|
| 272 |
+
H, W) which is feature map for last layer of decoder head.
|
| 273 |
+
"""
|
| 274 |
+
# accept lists (for cls token)
|
| 275 |
+
inputs = list(inputs)
|
| 276 |
+
for i, x in enumerate(inputs):
|
| 277 |
+
if len(x) == 2:
|
| 278 |
+
x, cls_token = x[0], x[1]
|
| 279 |
+
if len(x.shape) == 2:
|
| 280 |
+
x = x[:, :, None, None]
|
| 281 |
+
cls_token = cls_token[:, :, None, None].expand_as(x)
|
| 282 |
+
inputs[i] = torch.cat((x, cls_token), 1)
|
| 283 |
+
else:
|
| 284 |
+
x = x[0]
|
| 285 |
+
if len(x.shape) == 2:
|
| 286 |
+
x = x[:, :, None, None]
|
| 287 |
+
inputs[i] = x
|
| 288 |
+
x = self._transform_inputs(inputs)
|
| 289 |
+
# feats = self.bn(x)
|
| 290 |
+
return x
|
| 291 |
+
|
| 292 |
+
def forward(self, inputs, img_metas=None, **kwargs):
|
| 293 |
+
"""Forward function."""
|
| 294 |
+
output = self._forward_feature(inputs, img_metas=img_metas, **kwargs)
|
| 295 |
+
output = self.depth_pred(output)
|
| 296 |
+
return output
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
class ConvModule(nn.Module):
|
| 300 |
+
"""A conv block that bundles conv/norm/activation layers.
|
| 301 |
+
|
| 302 |
+
This block simplifies the usage of convolution layers, which are commonly
|
| 303 |
+
used with a norm layer (e.g., BatchNorm) and activation layer (e.g., ReLU).
|
| 304 |
+
It is based upon three build methods: `build_conv_layer()`,
|
| 305 |
+
`build_norm_layer()` and `build_activation_layer()`.
|
| 306 |
+
|
| 307 |
+
Besides, we add some additional features in this module.
|
| 308 |
+
1. Automatically set `bias` of the conv layer.
|
| 309 |
+
2. Spectral norm is supported.
|
| 310 |
+
3. More padding modes are supported. Before PyTorch 1.5, nn.Conv2d only
|
| 311 |
+
supports zero and circular padding, and we add "reflect" padding mode.
|
| 312 |
+
|
| 313 |
+
Args:
|
| 314 |
+
in_channels (int): Number of channels in the input feature map.
|
| 315 |
+
Same as that in ``nn._ConvNd``.
|
| 316 |
+
out_channels (int): Number of channels produced by the convolution.
|
| 317 |
+
Same as that in ``nn._ConvNd``.
|
| 318 |
+
kernel_size (int | tuple[int]): Size of the convolving kernel.
|
| 319 |
+
Same as that in ``nn._ConvNd``.
|
| 320 |
+
stride (int | tuple[int]): Stride of the convolution.
|
| 321 |
+
Same as that in ``nn._ConvNd``.
|
| 322 |
+
padding (int | tuple[int]): Zero-padding added to both sides of
|
| 323 |
+
the input. Same as that in ``nn._ConvNd``.
|
| 324 |
+
dilation (int | tuple[int]): Spacing between kernel elements.
|
| 325 |
+
Same as that in ``nn._ConvNd``.
|
| 326 |
+
groups (int): Number of blocked connections from input channels to
|
| 327 |
+
output channels. Same as that in ``nn._ConvNd``.
|
| 328 |
+
bias (bool | str): If specified as `auto`, it will be decided by the
|
| 329 |
+
norm_layer. Bias will be set as True if `norm_layer` is None, otherwise
|
| 330 |
+
False. Default: "auto".
|
| 331 |
+
conv_layer (nn.Module): Convolution layer. Default: None,
|
| 332 |
+
which means using conv2d.
|
| 333 |
+
norm_layer (nn.Module): Normalization layer. Default: None.
|
| 334 |
+
act_layer (nn.Module): Activation layer. Default: nn.ReLU.
|
| 335 |
+
inplace (bool): Whether to use inplace mode for activation.
|
| 336 |
+
Default: True.
|
| 337 |
+
with_spectral_norm (bool): Whether use spectral norm in conv module.
|
| 338 |
+
Default: False.
|
| 339 |
+
padding_mode (str): If the `padding_mode` has not been supported by
|
| 340 |
+
current `Conv2d` in PyTorch, we will use our own padding layer
|
| 341 |
+
instead. Currently, we support ['zeros', 'circular'] with official
|
| 342 |
+
implementation and ['reflect'] with our own implementation.
|
| 343 |
+
Default: 'zeros'.
|
| 344 |
+
order (tuple[str]): The order of conv/norm/activation layers. It is a
|
| 345 |
+
sequence of "conv", "norm" and "act". Common examples are
|
| 346 |
+
("conv", "norm", "act") and ("act", "conv", "norm").
|
| 347 |
+
Default: ('conv', 'norm', 'act').
|
| 348 |
+
"""
|
| 349 |
+
|
| 350 |
+
_abbr_ = "conv_block"
|
| 351 |
+
|
| 352 |
+
def __init__(
|
| 353 |
+
self,
|
| 354 |
+
in_channels,
|
| 355 |
+
out_channels,
|
| 356 |
+
kernel_size,
|
| 357 |
+
stride=1,
|
| 358 |
+
padding=0,
|
| 359 |
+
dilation=1,
|
| 360 |
+
groups=1,
|
| 361 |
+
bias="auto",
|
| 362 |
+
conv_layer=nn.Conv2d,
|
| 363 |
+
norm_layer=None,
|
| 364 |
+
act_layer=nn.ReLU,
|
| 365 |
+
inplace=True,
|
| 366 |
+
with_spectral_norm=False,
|
| 367 |
+
padding_mode="zeros",
|
| 368 |
+
order=("conv", "norm", "act"),
|
| 369 |
+
):
|
| 370 |
+
super(ConvModule, self).__init__()
|
| 371 |
+
official_padding_mode = ["zeros", "circular"]
|
| 372 |
+
self.conv_layer = conv_layer
|
| 373 |
+
self.norm_layer = norm_layer
|
| 374 |
+
self.act_layer = act_layer
|
| 375 |
+
self.inplace = inplace
|
| 376 |
+
self.with_spectral_norm = with_spectral_norm
|
| 377 |
+
self.with_explicit_padding = padding_mode not in official_padding_mode
|
| 378 |
+
self.order = order
|
| 379 |
+
assert isinstance(self.order, tuple) and len(self.order) == 3
|
| 380 |
+
assert set(order) == set(["conv", "norm", "act"])
|
| 381 |
+
|
| 382 |
+
self.with_norm = norm_layer is not None
|
| 383 |
+
self.with_activation = act_layer is not None
|
| 384 |
+
# if the conv layer is before a norm layer, bias is unnecessary.
|
| 385 |
+
if bias == "auto":
|
| 386 |
+
bias = not self.with_norm
|
| 387 |
+
self.with_bias = bias
|
| 388 |
+
|
| 389 |
+
if self.with_explicit_padding:
|
| 390 |
+
if padding_mode == "zeros":
|
| 391 |
+
padding_layer = nn.ZeroPad2d
|
| 392 |
+
else:
|
| 393 |
+
raise AssertionError(f"Unsupported padding mode: {padding_mode}")
|
| 394 |
+
self.pad = padding_layer(padding)
|
| 395 |
+
|
| 396 |
+
# reset padding to 0 for conv module
|
| 397 |
+
conv_padding = 0 if self.with_explicit_padding else padding
|
| 398 |
+
# build convolution layer
|
| 399 |
+
self.conv = self.conv_layer(
|
| 400 |
+
in_channels,
|
| 401 |
+
out_channels,
|
| 402 |
+
kernel_size,
|
| 403 |
+
stride=stride,
|
| 404 |
+
padding=conv_padding,
|
| 405 |
+
dilation=dilation,
|
| 406 |
+
groups=groups,
|
| 407 |
+
bias=bias,
|
| 408 |
+
)
|
| 409 |
+
# export the attributes of self.conv to a higher level for convenience
|
| 410 |
+
self.in_channels = self.conv.in_channels
|
| 411 |
+
self.out_channels = self.conv.out_channels
|
| 412 |
+
self.kernel_size = self.conv.kernel_size
|
| 413 |
+
self.stride = self.conv.stride
|
| 414 |
+
self.padding = padding
|
| 415 |
+
self.dilation = self.conv.dilation
|
| 416 |
+
self.transposed = self.conv.transposed
|
| 417 |
+
self.output_padding = self.conv.output_padding
|
| 418 |
+
self.groups = self.conv.groups
|
| 419 |
+
|
| 420 |
+
if self.with_spectral_norm:
|
| 421 |
+
self.conv = nn.utils.spectral_norm(self.conv)
|
| 422 |
+
|
| 423 |
+
# build normalization layers
|
| 424 |
+
if self.with_norm:
|
| 425 |
+
# norm layer is after conv layer
|
| 426 |
+
if order.index("norm") > order.index("conv"):
|
| 427 |
+
norm_channels = out_channels
|
| 428 |
+
else:
|
| 429 |
+
norm_channels = in_channels
|
| 430 |
+
norm = partial(norm_layer, num_features=norm_channels)
|
| 431 |
+
self.add_module("norm", norm)
|
| 432 |
+
if self.with_bias:
|
| 433 |
+
from torch.nnModules.batchnorm import _BatchNorm
|
| 434 |
+
from torch.nnModules.instancenorm import _InstanceNorm
|
| 435 |
+
|
| 436 |
+
if isinstance(norm, (_BatchNorm, _InstanceNorm)):
|
| 437 |
+
warnings.warn("Unnecessary conv bias before batch/instance norm")
|
| 438 |
+
else:
|
| 439 |
+
self.norm_name = None
|
| 440 |
+
|
| 441 |
+
# build activation layer
|
| 442 |
+
if self.with_activation:
|
| 443 |
+
# nn.Tanh has no 'inplace' argument
|
| 444 |
+
# (nn.Tanh, nn.PReLU, nn.Sigmoid, nn.HSigmoid, nn.Swish, nn.GELU)
|
| 445 |
+
if not isinstance(act_layer, (nn.Tanh, nn.PReLU, nn.Sigmoid, nn.GELU)):
|
| 446 |
+
act_layer = partial(act_layer, inplace=inplace)
|
| 447 |
+
self.activate = act_layer()
|
| 448 |
+
|
| 449 |
+
# Use msra init by default
|
| 450 |
+
self.init_weights()
|
| 451 |
+
|
| 452 |
+
@property
|
| 453 |
+
def norm(self):
|
| 454 |
+
if self.norm_name:
|
| 455 |
+
return getattr(self, self.norm_name)
|
| 456 |
+
else:
|
| 457 |
+
return None
|
| 458 |
+
|
| 459 |
+
def init_weights(self):
|
| 460 |
+
# 1. It is mainly for customized conv layers with their own
|
| 461 |
+
# initialization manners by calling their own ``init_weights()``,
|
| 462 |
+
# and we do not want ConvModule to override the initialization.
|
| 463 |
+
# 2. For customized conv layers without their own initialization
|
| 464 |
+
# manners (that is, they don't have their own ``init_weights()``)
|
| 465 |
+
# and PyTorch's conv layers, they will be initialized by
|
| 466 |
+
# this method with default ``kaiming_init``.
|
| 467 |
+
# Note: For PyTorch's conv layers, they will be overwritten by our
|
| 468 |
+
# initialization implementation using default ``kaiming_init``.
|
| 469 |
+
if not hasattr(self.conv, "init_weights"):
|
| 470 |
+
if self.with_activation and isinstance(self.act_layer, nn.LeakyReLU):
|
| 471 |
+
nonlinearity = "leaky_relu"
|
| 472 |
+
a = 0.01 # XXX: default negative_slope
|
| 473 |
+
else:
|
| 474 |
+
nonlinearity = "relu"
|
| 475 |
+
a = 0
|
| 476 |
+
if hasattr(self.conv, "weight") and self.conv.weight is not None:
|
| 477 |
+
nn.init.kaiming_normal_(self.conv.weight, a=a, mode="fan_out", nonlinearity=nonlinearity)
|
| 478 |
+
if hasattr(self.conv, "bias") and self.conv.bias is not None:
|
| 479 |
+
nn.init.constant_(self.conv.bias, 0)
|
| 480 |
+
if self.with_norm:
|
| 481 |
+
if hasattr(self.norm, "weight") and self.norm.weight is not None:
|
| 482 |
+
nn.init.constant_(self.norm.weight, 1)
|
| 483 |
+
if hasattr(self.norm, "bias") and self.norm.bias is not None:
|
| 484 |
+
nn.init.constant_(self.norm.bias, 0)
|
| 485 |
+
|
| 486 |
+
def forward(self, x, activate=True, norm=True):
|
| 487 |
+
for layer in self.order:
|
| 488 |
+
if layer == "conv":
|
| 489 |
+
if self.with_explicit_padding:
|
| 490 |
+
x = self.pad(x)
|
| 491 |
+
x = self.conv(x)
|
| 492 |
+
elif layer == "norm" and norm and self.with_norm:
|
| 493 |
+
x = self.norm(x)
|
| 494 |
+
elif layer == "act" and activate and self.with_activation:
|
| 495 |
+
x = self.activate(x)
|
| 496 |
+
return x
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
class Interpolate(nn.Module):
|
| 500 |
+
def __init__(self, scale_factor, mode, align_corners=False):
|
| 501 |
+
super(Interpolate, self).__init__()
|
| 502 |
+
self.interp = nn.functional.interpolate
|
| 503 |
+
self.scale_factor = scale_factor
|
| 504 |
+
self.mode = mode
|
| 505 |
+
self.align_corners = align_corners
|
| 506 |
+
|
| 507 |
+
def forward(self, x):
|
| 508 |
+
x = self.interp(x, scale_factor=self.scale_factor, mode=self.mode, align_corners=self.align_corners)
|
| 509 |
+
return x
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
class HeadDepth(nn.Module):
|
| 513 |
+
def __init__(self, features):
|
| 514 |
+
super(HeadDepth, self).__init__()
|
| 515 |
+
self.head = nn.Sequential(
|
| 516 |
+
nn.Conv2d(features, features // 2, kernel_size=3, stride=1, padding=1),
|
| 517 |
+
Interpolate(scale_factor=2, mode="bilinear", align_corners=True),
|
| 518 |
+
nn.Conv2d(features // 2, 32, kernel_size=3, stride=1, padding=1),
|
| 519 |
+
nn.ReLU(),
|
| 520 |
+
nn.Conv2d(32, 1, kernel_size=1, stride=1, padding=0),
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
def forward(self, x):
|
| 524 |
+
x = self.head(x)
|
| 525 |
+
return x
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
class ReassembleBlocks(nn.Module):
|
| 529 |
+
"""ViTPostProcessBlock, process cls_token in ViT backbone output and
|
| 530 |
+
rearrange the feature vector to feature map.
|
| 531 |
+
Args:
|
| 532 |
+
in_channels (int): ViT feature channels. Default: 768.
|
| 533 |
+
out_channels (List): output channels of each stage.
|
| 534 |
+
Default: [96, 192, 384, 768].
|
| 535 |
+
readout_type (str): Type of readout operation. Default: 'ignore'.
|
| 536 |
+
patch_size (int): The patch size. Default: 16.
|
| 537 |
+
"""
|
| 538 |
+
|
| 539 |
+
def __init__(self, in_channels=768, out_channels=[96, 192, 384, 768], readout_type="ignore", patch_size=16):
|
| 540 |
+
super(ReassembleBlocks, self).__init__()
|
| 541 |
+
|
| 542 |
+
assert readout_type in ["ignore", "add", "project"]
|
| 543 |
+
self.readout_type = readout_type
|
| 544 |
+
self.patch_size = patch_size
|
| 545 |
+
|
| 546 |
+
self.projects = nn.ModuleList(
|
| 547 |
+
[
|
| 548 |
+
ConvModule(
|
| 549 |
+
in_channels=in_channels,
|
| 550 |
+
out_channels=out_channel,
|
| 551 |
+
kernel_size=1,
|
| 552 |
+
act_layer=None,
|
| 553 |
+
)
|
| 554 |
+
for out_channel in out_channels
|
| 555 |
+
]
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
self.resize_layers = nn.ModuleList(
|
| 559 |
+
[
|
| 560 |
+
nn.ConvTranspose2d(
|
| 561 |
+
in_channels=out_channels[0], out_channels=out_channels[0], kernel_size=4, stride=4, padding=0
|
| 562 |
+
),
|
| 563 |
+
nn.ConvTranspose2d(
|
| 564 |
+
in_channels=out_channels[1], out_channels=out_channels[1], kernel_size=2, stride=2, padding=0
|
| 565 |
+
),
|
| 566 |
+
nn.Identity(),
|
| 567 |
+
nn.Conv2d(
|
| 568 |
+
in_channels=out_channels[3], out_channels=out_channels[3], kernel_size=3, stride=2, padding=1
|
| 569 |
+
),
|
| 570 |
+
]
|
| 571 |
+
)
|
| 572 |
+
if self.readout_type == "project":
|
| 573 |
+
self.readout_projects = nn.ModuleList()
|
| 574 |
+
for _ in range(len(self.projects)):
|
| 575 |
+
self.readout_projects.append(nn.Sequential(nn.Linear(2 * in_channels, in_channels), nn.GELU()))
|
| 576 |
+
|
| 577 |
+
def forward(self, inputs):
|
| 578 |
+
assert isinstance(inputs, list)
|
| 579 |
+
out = []
|
| 580 |
+
for i, x in enumerate(inputs):
|
| 581 |
+
assert len(x) == 2
|
| 582 |
+
x, cls_token = x[0], x[1]
|
| 583 |
+
feature_shape = x.shape
|
| 584 |
+
if self.readout_type == "project":
|
| 585 |
+
x = x.flatten(2).permute((0, 2, 1))
|
| 586 |
+
readout = cls_token.unsqueeze(1).expand_as(x)
|
| 587 |
+
x = self.readout_projects[i](torch.cat((x, readout), -1))
|
| 588 |
+
x = x.permute(0, 2, 1).reshape(feature_shape)
|
| 589 |
+
elif self.readout_type == "add":
|
| 590 |
+
x = x.flatten(2) + cls_token.unsqueeze(-1)
|
| 591 |
+
x = x.reshape(feature_shape)
|
| 592 |
+
else:
|
| 593 |
+
pass
|
| 594 |
+
x = self.projects[i](x)
|
| 595 |
+
x = self.resize_layers[i](x)
|
| 596 |
+
out.append(x)
|
| 597 |
+
return out
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
class PreActResidualConvUnit(nn.Module):
|
| 601 |
+
"""ResidualConvUnit, pre-activate residual unit.
|
| 602 |
+
Args:
|
| 603 |
+
in_channels (int): number of channels in the input feature map.
|
| 604 |
+
act_layer (nn.Module): activation layer.
|
| 605 |
+
norm_layer (nn.Module): norm layer.
|
| 606 |
+
stride (int): stride of the first block. Default: 1
|
| 607 |
+
dilation (int): dilation rate for convs layers. Default: 1.
|
| 608 |
+
"""
|
| 609 |
+
|
| 610 |
+
def __init__(self, in_channels, act_layer, norm_layer, stride=1, dilation=1):
|
| 611 |
+
super(PreActResidualConvUnit, self).__init__()
|
| 612 |
+
|
| 613 |
+
self.conv1 = ConvModule(
|
| 614 |
+
in_channels,
|
| 615 |
+
in_channels,
|
| 616 |
+
3,
|
| 617 |
+
stride=stride,
|
| 618 |
+
padding=dilation,
|
| 619 |
+
dilation=dilation,
|
| 620 |
+
norm_layer=norm_layer,
|
| 621 |
+
act_layer=act_layer,
|
| 622 |
+
bias=False,
|
| 623 |
+
order=("act", "conv", "norm"),
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
self.conv2 = ConvModule(
|
| 627 |
+
in_channels,
|
| 628 |
+
in_channels,
|
| 629 |
+
3,
|
| 630 |
+
padding=1,
|
| 631 |
+
norm_layer=norm_layer,
|
| 632 |
+
act_layer=act_layer,
|
| 633 |
+
bias=False,
|
| 634 |
+
order=("act", "conv", "norm"),
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
+
def forward(self, inputs):
|
| 638 |
+
inputs_ = inputs.clone()
|
| 639 |
+
x = self.conv1(inputs)
|
| 640 |
+
x = self.conv2(x)
|
| 641 |
+
return x + inputs_
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
class FeatureFusionBlock(nn.Module):
|
| 645 |
+
"""FeatureFusionBlock, merge feature map from different stages.
|
| 646 |
+
Args:
|
| 647 |
+
in_channels (int): Input channels.
|
| 648 |
+
act_layer (nn.Module): activation layer for ResidualConvUnit.
|
| 649 |
+
norm_layer (nn.Module): normalization layer.
|
| 650 |
+
expand (bool): Whether expand the channels in post process block.
|
| 651 |
+
Default: False.
|
| 652 |
+
align_corners (bool): align_corner setting for bilinear upsample.
|
| 653 |
+
Default: True.
|
| 654 |
+
"""
|
| 655 |
+
|
| 656 |
+
def __init__(self, in_channels, act_layer, norm_layer, expand=False, align_corners=True):
|
| 657 |
+
super(FeatureFusionBlock, self).__init__()
|
| 658 |
+
|
| 659 |
+
self.in_channels = in_channels
|
| 660 |
+
self.expand = expand
|
| 661 |
+
self.align_corners = align_corners
|
| 662 |
+
|
| 663 |
+
self.out_channels = in_channels
|
| 664 |
+
if self.expand:
|
| 665 |
+
self.out_channels = in_channels // 2
|
| 666 |
+
|
| 667 |
+
self.project = ConvModule(self.in_channels, self.out_channels, kernel_size=1, act_layer=None, bias=True)
|
| 668 |
+
|
| 669 |
+
self.res_conv_unit1 = PreActResidualConvUnit(
|
| 670 |
+
in_channels=self.in_channels, act_layer=act_layer, norm_layer=norm_layer
|
| 671 |
+
)
|
| 672 |
+
self.res_conv_unit2 = PreActResidualConvUnit(
|
| 673 |
+
in_channels=self.in_channels, act_layer=act_layer, norm_layer=norm_layer
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
def forward(self, *inputs):
|
| 677 |
+
x = inputs[0]
|
| 678 |
+
if len(inputs) == 2:
|
| 679 |
+
if x.shape != inputs[1].shape:
|
| 680 |
+
res = resize(inputs[1], size=(x.shape[2], x.shape[3]), mode="bilinear", align_corners=False)
|
| 681 |
+
else:
|
| 682 |
+
res = inputs[1]
|
| 683 |
+
x = x + self.res_conv_unit1(res)
|
| 684 |
+
x = self.res_conv_unit2(x)
|
| 685 |
+
x = resize(x, scale_factor=2, mode="bilinear", align_corners=self.align_corners)
|
| 686 |
+
x = self.project(x)
|
| 687 |
+
return x
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
class DPTHead(DepthBaseDecodeHead):
|
| 691 |
+
"""Vision Transformers for Dense Prediction.
|
| 692 |
+
This head is implemented of `DPT <https://arxiv.org/abs/2103.13413>`_.
|
| 693 |
+
Args:
|
| 694 |
+
embed_dims (int): The embed dimension of the ViT backbone.
|
| 695 |
+
Default: 768.
|
| 696 |
+
post_process_channels (List): Out channels of post process conv
|
| 697 |
+
layers. Default: [96, 192, 384, 768].
|
| 698 |
+
readout_type (str): Type of readout operation. Default: 'ignore'.
|
| 699 |
+
patch_size (int): The patch size. Default: 16.
|
| 700 |
+
expand_channels (bool): Whether expand the channels in post process
|
| 701 |
+
block. Default: False.
|
| 702 |
+
"""
|
| 703 |
+
|
| 704 |
+
def __init__(
|
| 705 |
+
self,
|
| 706 |
+
embed_dims=768,
|
| 707 |
+
post_process_channels=[96, 192, 384, 768],
|
| 708 |
+
readout_type="ignore",
|
| 709 |
+
patch_size=16,
|
| 710 |
+
expand_channels=False,
|
| 711 |
+
**kwargs,
|
| 712 |
+
):
|
| 713 |
+
super(DPTHead, self).__init__(**kwargs)
|
| 714 |
+
|
| 715 |
+
self.in_channels = self.in_channels
|
| 716 |
+
self.expand_channels = expand_channels
|
| 717 |
+
self.reassemble_blocks = ReassembleBlocks(embed_dims, post_process_channels, readout_type, patch_size)
|
| 718 |
+
|
| 719 |
+
self.post_process_channels = [
|
| 720 |
+
channel * math.pow(2, i) if expand_channels else channel for i, channel in enumerate(post_process_channels)
|
| 721 |
+
]
|
| 722 |
+
self.convs = nn.ModuleList()
|
| 723 |
+
for channel in self.post_process_channels:
|
| 724 |
+
self.convs.append(ConvModule(channel, self.channels, kernel_size=3, padding=1, act_layer=None, bias=False))
|
| 725 |
+
self.fusion_blocks = nn.ModuleList()
|
| 726 |
+
for _ in range(len(self.convs)):
|
| 727 |
+
self.fusion_blocks.append(FeatureFusionBlock(self.channels, self.act_layer, self.norm_layer))
|
| 728 |
+
self.fusion_blocks[0].res_conv_unit1 = None
|
| 729 |
+
self.project = ConvModule(self.channels, self.channels, kernel_size=3, padding=1, norm_layer=self.norm_layer)
|
| 730 |
+
self.num_fusion_blocks = len(self.fusion_blocks)
|
| 731 |
+
self.num_reassemble_blocks = len(self.reassemble_blocks.resize_layers)
|
| 732 |
+
self.num_post_process_channels = len(self.post_process_channels)
|
| 733 |
+
assert self.num_fusion_blocks == self.num_reassemble_blocks
|
| 734 |
+
assert self.num_reassemble_blocks == self.num_post_process_channels
|
| 735 |
+
self.conv_depth = HeadDepth(self.channels)
|
| 736 |
+
|
| 737 |
+
def forward(self, inputs, img_metas):
|
| 738 |
+
assert len(inputs) == self.num_reassemble_blocks
|
| 739 |
+
x = [inp for inp in inputs]
|
| 740 |
+
x = self.reassemble_blocks(x)
|
| 741 |
+
x = [self.convs[i](feature) for i, feature in enumerate(x)]
|
| 742 |
+
out = self.fusion_blocks[0](x[-1])
|
| 743 |
+
for i in range(1, len(self.fusion_blocks)):
|
| 744 |
+
out = self.fusion_blocks[i](out, x[-(i + 1)])
|
| 745 |
+
out = self.project(out)
|
| 746 |
+
out = self.depth_pred(out)
|
| 747 |
+
return out
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/encoder_decoder.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
from collections import OrderedDict
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
from .ops import resize
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def add_prefix(inputs, prefix):
|
| 16 |
+
"""Add prefix for dict.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
inputs (dict): The input dict with str keys.
|
| 20 |
+
prefix (str): The prefix to add.
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
|
| 24 |
+
dict: The dict with keys updated with ``prefix``.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
outputs = dict()
|
| 28 |
+
for name, value in inputs.items():
|
| 29 |
+
outputs[f"{prefix}.{name}"] = value
|
| 30 |
+
|
| 31 |
+
return outputs
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class DepthEncoderDecoder(nn.Module):
|
| 35 |
+
"""Encoder Decoder depther.
|
| 36 |
+
|
| 37 |
+
EncoderDecoder typically consists of backbone and decode_head.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def __init__(self, backbone, decode_head):
|
| 41 |
+
super(DepthEncoderDecoder, self).__init__()
|
| 42 |
+
|
| 43 |
+
self.backbone = backbone
|
| 44 |
+
self.decode_head = decode_head
|
| 45 |
+
self.align_corners = self.decode_head.align_corners
|
| 46 |
+
|
| 47 |
+
def extract_feat(self, img):
|
| 48 |
+
"""Extract features from images."""
|
| 49 |
+
return self.backbone(img)
|
| 50 |
+
|
| 51 |
+
def encode_decode(self, img, img_metas, rescale=True, size=None):
|
| 52 |
+
"""Encode images with backbone and decode into a depth estimation
|
| 53 |
+
map of the same size as input."""
|
| 54 |
+
x = self.extract_feat(img)
|
| 55 |
+
out = self._decode_head_forward_test(x, img_metas)
|
| 56 |
+
# crop the pred depth to the certain range.
|
| 57 |
+
out = torch.clamp(out, min=self.decode_head.min_depth, max=self.decode_head.max_depth)
|
| 58 |
+
if rescale:
|
| 59 |
+
if size is None:
|
| 60 |
+
if img_metas is not None:
|
| 61 |
+
size = img_metas[0]["ori_shape"][:2]
|
| 62 |
+
else:
|
| 63 |
+
size = img.shape[2:]
|
| 64 |
+
out = resize(input=out, size=size, mode="bilinear", align_corners=self.align_corners)
|
| 65 |
+
return out
|
| 66 |
+
|
| 67 |
+
def _decode_head_forward_train(self, img, x, img_metas, depth_gt, **kwargs):
|
| 68 |
+
"""Run forward function and calculate loss for decode head in
|
| 69 |
+
training."""
|
| 70 |
+
losses = dict()
|
| 71 |
+
loss_decode = self.decode_head.forward_train(img, x, img_metas, depth_gt, **kwargs)
|
| 72 |
+
losses.update(add_prefix(loss_decode, "decode"))
|
| 73 |
+
return losses
|
| 74 |
+
|
| 75 |
+
def _decode_head_forward_test(self, x, img_metas):
|
| 76 |
+
"""Run forward function and calculate loss for decode head in
|
| 77 |
+
inference."""
|
| 78 |
+
depth_pred = self.decode_head.forward_test(x, img_metas)
|
| 79 |
+
return depth_pred
|
| 80 |
+
|
| 81 |
+
def forward_dummy(self, img):
|
| 82 |
+
"""Dummy forward function."""
|
| 83 |
+
depth = self.encode_decode(img, None)
|
| 84 |
+
|
| 85 |
+
return depth
|
| 86 |
+
|
| 87 |
+
def forward_train(self, img, img_metas, depth_gt, **kwargs):
|
| 88 |
+
"""Forward function for training.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
img (Tensor): Input images.
|
| 92 |
+
img_metas (list[dict]): List of image info dict where each dict
|
| 93 |
+
has: 'img_shape', 'scale_factor', 'flip', and may also contain
|
| 94 |
+
'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'.
|
| 95 |
+
For details on the values of these keys see
|
| 96 |
+
`depth/datasets/pipelines/formatting.py:Collect`.
|
| 97 |
+
depth_gt (Tensor): Depth gt
|
| 98 |
+
used if the architecture supports depth estimation task.
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
dict[str, Tensor]: a dictionary of loss components
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
x = self.extract_feat(img)
|
| 105 |
+
|
| 106 |
+
losses = dict()
|
| 107 |
+
|
| 108 |
+
# the last of x saves the info from neck
|
| 109 |
+
loss_decode = self._decode_head_forward_train(img, x, img_metas, depth_gt, **kwargs)
|
| 110 |
+
|
| 111 |
+
losses.update(loss_decode)
|
| 112 |
+
|
| 113 |
+
return losses
|
| 114 |
+
|
| 115 |
+
def whole_inference(self, img, img_meta, rescale, size=None):
|
| 116 |
+
"""Inference with full image."""
|
| 117 |
+
return self.encode_decode(img, img_meta, rescale, size=size)
|
| 118 |
+
|
| 119 |
+
def slide_inference(self, img, img_meta, rescale, stride, crop_size):
|
| 120 |
+
"""Inference by sliding-window with overlap.
|
| 121 |
+
|
| 122 |
+
If h_crop > h_img or w_crop > w_img, the small patch will be used to
|
| 123 |
+
decode without padding.
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
h_stride, w_stride = stride
|
| 127 |
+
h_crop, w_crop = crop_size
|
| 128 |
+
batch_size, _, h_img, w_img = img.size()
|
| 129 |
+
h_grids = max(h_img - h_crop + h_stride - 1, 0) // h_stride + 1
|
| 130 |
+
w_grids = max(w_img - w_crop + w_stride - 1, 0) // w_stride + 1
|
| 131 |
+
preds = img.new_zeros((batch_size, 1, h_img, w_img))
|
| 132 |
+
count_mat = img.new_zeros((batch_size, 1, h_img, w_img))
|
| 133 |
+
for h_idx in range(h_grids):
|
| 134 |
+
for w_idx in range(w_grids):
|
| 135 |
+
y1 = h_idx * h_stride
|
| 136 |
+
x1 = w_idx * w_stride
|
| 137 |
+
y2 = min(y1 + h_crop, h_img)
|
| 138 |
+
x2 = min(x1 + w_crop, w_img)
|
| 139 |
+
y1 = max(y2 - h_crop, 0)
|
| 140 |
+
x1 = max(x2 - w_crop, 0)
|
| 141 |
+
crop_img = img[:, :, y1:y2, x1:x2]
|
| 142 |
+
depth_pred = self.encode_decode(crop_img, img_meta, rescale)
|
| 143 |
+
preds += F.pad(depth_pred, (int(x1), int(preds.shape[3] - x2), int(y1), int(preds.shape[2] - y2)))
|
| 144 |
+
|
| 145 |
+
count_mat[:, :, y1:y2, x1:x2] += 1
|
| 146 |
+
assert (count_mat == 0).sum() == 0
|
| 147 |
+
if torch.onnx.is_in_onnx_export():
|
| 148 |
+
# cast count_mat to constant while exporting to ONNX
|
| 149 |
+
count_mat = torch.from_numpy(count_mat.cpu().detach().numpy()).to(device=img.device)
|
| 150 |
+
preds = preds / count_mat
|
| 151 |
+
return preds
|
| 152 |
+
|
| 153 |
+
def inference(self, img, img_meta, rescale, size=None, mode="whole"):
|
| 154 |
+
"""Inference with slide/whole style.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
img (Tensor): The input image of shape (N, 3, H, W).
|
| 158 |
+
img_meta (dict): Image info dict where each dict has: 'img_shape',
|
| 159 |
+
'scale_factor', 'flip', and may also contain
|
| 160 |
+
'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'.
|
| 161 |
+
For details on the values of these keys see
|
| 162 |
+
`depth/datasets/pipelines/formatting.py:Collect`.
|
| 163 |
+
rescale (bool): Whether rescale back to original shape.
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
Tensor: The output depth map.
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
assert mode in ["slide", "whole"]
|
| 170 |
+
ori_shape = img_meta[0]["ori_shape"]
|
| 171 |
+
assert all(_["ori_shape"] == ori_shape for _ in img_meta)
|
| 172 |
+
if mode == "slide":
|
| 173 |
+
depth_pred = self.slide_inference(img, img_meta, rescale)
|
| 174 |
+
else:
|
| 175 |
+
depth_pred = self.whole_inference(img, img_meta, rescale, size=size)
|
| 176 |
+
output = depth_pred
|
| 177 |
+
flip = img_meta[0]["flip"]
|
| 178 |
+
if flip:
|
| 179 |
+
flip_direction = img_meta[0]["flip_direction"]
|
| 180 |
+
assert flip_direction in ["horizontal", "vertical"]
|
| 181 |
+
if flip_direction == "horizontal":
|
| 182 |
+
output = output.flip(dims=(3,))
|
| 183 |
+
elif flip_direction == "vertical":
|
| 184 |
+
output = output.flip(dims=(2,))
|
| 185 |
+
|
| 186 |
+
return output
|
| 187 |
+
|
| 188 |
+
def simple_test(self, img, img_meta, rescale=True):
|
| 189 |
+
"""Simple test with single image."""
|
| 190 |
+
depth_pred = self.inference(img, img_meta, rescale)
|
| 191 |
+
if torch.onnx.is_in_onnx_export():
|
| 192 |
+
# our inference backend only support 4D output
|
| 193 |
+
depth_pred = depth_pred.unsqueeze(0)
|
| 194 |
+
return depth_pred
|
| 195 |
+
depth_pred = depth_pred.cpu().numpy()
|
| 196 |
+
# unravel batch dim
|
| 197 |
+
depth_pred = list(depth_pred)
|
| 198 |
+
return depth_pred
|
| 199 |
+
|
| 200 |
+
def aug_test(self, imgs, img_metas, rescale=True):
|
| 201 |
+
"""Test with augmentations.
|
| 202 |
+
|
| 203 |
+
Only rescale=True is supported.
|
| 204 |
+
"""
|
| 205 |
+
# aug_test rescale all imgs back to ori_shape for now
|
| 206 |
+
assert rescale
|
| 207 |
+
# to save memory, we get augmented depth logit inplace
|
| 208 |
+
depth_pred = self.inference(imgs[0], img_metas[0], rescale)
|
| 209 |
+
for i in range(1, len(imgs)):
|
| 210 |
+
cur_depth_pred = self.inference(imgs[i], img_metas[i], rescale, size=depth_pred.shape[-2:])
|
| 211 |
+
depth_pred += cur_depth_pred
|
| 212 |
+
depth_pred /= len(imgs)
|
| 213 |
+
depth_pred = depth_pred.cpu().numpy()
|
| 214 |
+
# unravel batch dim
|
| 215 |
+
depth_pred = list(depth_pred)
|
| 216 |
+
return depth_pred
|
| 217 |
+
|
| 218 |
+
def forward_test(self, imgs, img_metas, **kwargs):
|
| 219 |
+
"""
|
| 220 |
+
Args:
|
| 221 |
+
imgs (List[Tensor]): the outer list indicates test-time
|
| 222 |
+
augmentations and inner Tensor should have a shape NxCxHxW,
|
| 223 |
+
which contains all images in the batch.
|
| 224 |
+
img_metas (List[List[dict]]): the outer list indicates test-time
|
| 225 |
+
augs (multiscale, flip, etc.) and the inner list indicates
|
| 226 |
+
images in a batch.
|
| 227 |
+
"""
|
| 228 |
+
for var, name in [(imgs, "imgs"), (img_metas, "img_metas")]:
|
| 229 |
+
if not isinstance(var, list):
|
| 230 |
+
raise TypeError(f"{name} must be a list, but got " f"{type(var)}")
|
| 231 |
+
num_augs = len(imgs)
|
| 232 |
+
if num_augs != len(img_metas):
|
| 233 |
+
raise ValueError(f"num of augmentations ({len(imgs)}) != " f"num of image meta ({len(img_metas)})")
|
| 234 |
+
# all images in the same aug batch all of the same ori_shape and pad
|
| 235 |
+
# shape
|
| 236 |
+
for img_meta in img_metas:
|
| 237 |
+
ori_shapes = [_["ori_shape"] for _ in img_meta]
|
| 238 |
+
assert all(shape == ori_shapes[0] for shape in ori_shapes)
|
| 239 |
+
img_shapes = [_["img_shape"] for _ in img_meta]
|
| 240 |
+
assert all(shape == img_shapes[0] for shape in img_shapes)
|
| 241 |
+
pad_shapes = [_["pad_shape"] for _ in img_meta]
|
| 242 |
+
assert all(shape == pad_shapes[0] for shape in pad_shapes)
|
| 243 |
+
|
| 244 |
+
if num_augs == 1:
|
| 245 |
+
return self.simple_test(imgs[0], img_metas[0], **kwargs)
|
| 246 |
+
else:
|
| 247 |
+
return self.aug_test(imgs, img_metas, **kwargs)
|
| 248 |
+
|
| 249 |
+
def forward(self, img, img_metas, return_loss=True, **kwargs):
|
| 250 |
+
"""Calls either :func:`forward_train` or :func:`forward_test` depending
|
| 251 |
+
on whether ``return_loss`` is ``True``.
|
| 252 |
+
|
| 253 |
+
Note this setting will change the expected inputs. When
|
| 254 |
+
``return_loss=True``, img and img_meta are single-nested (i.e. Tensor
|
| 255 |
+
and List[dict]), and when ``resturn_loss=False``, img and img_meta
|
| 256 |
+
should be double nested (i.e. List[Tensor], List[List[dict]]), with
|
| 257 |
+
the outer list indicating test time augmentations.
|
| 258 |
+
"""
|
| 259 |
+
if return_loss:
|
| 260 |
+
return self.forward_train(img, img_metas, **kwargs)
|
| 261 |
+
else:
|
| 262 |
+
return self.forward_test(img, img_metas, **kwargs)
|
| 263 |
+
|
| 264 |
+
def train_step(self, data_batch, optimizer, **kwargs):
|
| 265 |
+
"""The iteration step during training.
|
| 266 |
+
|
| 267 |
+
This method defines an iteration step during training, except for the
|
| 268 |
+
back propagation and optimizer updating, which are done in an optimizer
|
| 269 |
+
hook. Note that in some complicated cases or models, the whole process
|
| 270 |
+
including back propagation and optimizer updating is also defined in
|
| 271 |
+
this method, such as GAN.
|
| 272 |
+
|
| 273 |
+
Args:
|
| 274 |
+
data (dict): The output of dataloader.
|
| 275 |
+
optimizer (:obj:`torch.optim.Optimizer` | dict): The optimizer of
|
| 276 |
+
runner is passed to ``train_step()``. This argument is unused
|
| 277 |
+
and reserved.
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
dict: It should contain at least 3 keys: ``loss``, ``log_vars``,
|
| 281 |
+
``num_samples``.
|
| 282 |
+
``loss`` is a tensor for back propagation, which can be a
|
| 283 |
+
weighted sum of multiple losses.
|
| 284 |
+
``log_vars`` contains all the variables to be sent to the
|
| 285 |
+
logger.
|
| 286 |
+
``num_samples`` indicates the batch size (when the model is
|
| 287 |
+
DDP, it means the batch size on each GPU), which is used for
|
| 288 |
+
averaging the logs.
|
| 289 |
+
"""
|
| 290 |
+
losses = self(**data_batch)
|
| 291 |
+
|
| 292 |
+
# split losses and images
|
| 293 |
+
real_losses = {}
|
| 294 |
+
log_imgs = {}
|
| 295 |
+
for k, v in losses.items():
|
| 296 |
+
if "img" in k:
|
| 297 |
+
log_imgs[k] = v
|
| 298 |
+
else:
|
| 299 |
+
real_losses[k] = v
|
| 300 |
+
|
| 301 |
+
loss, log_vars = self._parse_losses(real_losses)
|
| 302 |
+
|
| 303 |
+
outputs = dict(loss=loss, log_vars=log_vars, num_samples=len(data_batch["img_metas"]), log_imgs=log_imgs)
|
| 304 |
+
|
| 305 |
+
return outputs
|
| 306 |
+
|
| 307 |
+
def val_step(self, data_batch, **kwargs):
|
| 308 |
+
"""The iteration step during validation.
|
| 309 |
+
|
| 310 |
+
This method shares the same signature as :func:`train_step`, but used
|
| 311 |
+
during val epochs. Note that the evaluation after training epochs is
|
| 312 |
+
not implemented with this method, but an evaluation hook.
|
| 313 |
+
"""
|
| 314 |
+
output = self(**data_batch, **kwargs)
|
| 315 |
+
return output
|
| 316 |
+
|
| 317 |
+
@staticmethod
|
| 318 |
+
def _parse_losses(losses):
|
| 319 |
+
import torch.distributed as dist
|
| 320 |
+
|
| 321 |
+
"""Parse the raw outputs (losses) of the network.
|
| 322 |
+
|
| 323 |
+
Args:
|
| 324 |
+
losses (dict): Raw output of the network, which usually contain
|
| 325 |
+
losses and other necessary information.
|
| 326 |
+
|
| 327 |
+
Returns:
|
| 328 |
+
tuple[Tensor, dict]: (loss, log_vars), loss is the loss tensor
|
| 329 |
+
which may be a weighted sum of all losses, log_vars contains
|
| 330 |
+
all the variables to be sent to the logger.
|
| 331 |
+
"""
|
| 332 |
+
log_vars = OrderedDict()
|
| 333 |
+
for loss_name, loss_value in losses.items():
|
| 334 |
+
if isinstance(loss_value, torch.Tensor):
|
| 335 |
+
log_vars[loss_name] = loss_value.mean()
|
| 336 |
+
elif isinstance(loss_value, list):
|
| 337 |
+
log_vars[loss_name] = sum(_loss.mean() for _loss in loss_value)
|
| 338 |
+
else:
|
| 339 |
+
raise TypeError(f"{loss_name} is not a tensor or list of tensors")
|
| 340 |
+
|
| 341 |
+
loss = sum(_value for _key, _value in log_vars.items() if "loss" in _key)
|
| 342 |
+
|
| 343 |
+
log_vars["loss"] = loss
|
| 344 |
+
for loss_name, loss_value in log_vars.items():
|
| 345 |
+
# reduce loss when distributed training
|
| 346 |
+
if dist.is_available() and dist.is_initialized():
|
| 347 |
+
loss_value = loss_value.data.clone()
|
| 348 |
+
dist.all_reduce(loss_value.div_(dist.get_world_size()))
|
| 349 |
+
log_vars[loss_name] = loss_value.item()
|
| 350 |
+
|
| 351 |
+
return loss, log_vars
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/ops.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
import warnings
|
| 7 |
+
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def resize(input, size=None, scale_factor=None, mode="nearest", align_corners=None, warning=False):
|
| 12 |
+
if warning:
|
| 13 |
+
if size is not None and align_corners:
|
| 14 |
+
input_h, input_w = tuple(int(x) for x in input.shape[2:])
|
| 15 |
+
output_h, output_w = tuple(int(x) for x in size)
|
| 16 |
+
if output_h > input_h or output_w > output_h:
|
| 17 |
+
if (
|
| 18 |
+
(output_h > 1 and output_w > 1 and input_h > 1 and input_w > 1)
|
| 19 |
+
and (output_h - 1) % (input_h - 1)
|
| 20 |
+
and (output_w - 1) % (input_w - 1)
|
| 21 |
+
):
|
| 22 |
+
warnings.warn(
|
| 23 |
+
f"When align_corners={align_corners}, "
|
| 24 |
+
"the output would more aligned if "
|
| 25 |
+
f"input size {(input_h, input_w)} is `x+1` and "
|
| 26 |
+
f"out size {(output_h, output_w)} is `nx+1`"
|
| 27 |
+
)
|
| 28 |
+
return F.interpolate(input, size, scale_factor, mode, align_corners)
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/dinotxt_model.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from typing import Optional, Tuple
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
from torch import nn, Tensor
|
| 13 |
+
|
| 14 |
+
from .vision_tower import VisionTower
|
| 15 |
+
from .text_tower import TextTower
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class DinoTxtConfig:
|
| 20 |
+
embed_dim: int
|
| 21 |
+
vision_model_freeze_backbone: bool = True
|
| 22 |
+
vision_model_train_img_size: int = 224
|
| 23 |
+
vision_model_use_class_token: bool = True
|
| 24 |
+
vision_model_use_patch_tokens: bool = False
|
| 25 |
+
vision_model_num_head_blocks: int = 0
|
| 26 |
+
vision_model_head_blocks_drop_path: float = 0.3
|
| 27 |
+
vision_model_use_linear_projection: bool = False
|
| 28 |
+
vision_model_patch_tokens_pooler_type: str = "mean"
|
| 29 |
+
vision_model_patch_token_layer: int = 1 # which layer to take patch tokens from
|
| 30 |
+
# 1 - last layer, 2 - second last layer, etc.
|
| 31 |
+
text_model_freeze_backbone: bool = False
|
| 32 |
+
text_model_num_head_blocks: int = 0
|
| 33 |
+
text_model_head_blocks_is_causal: bool = False
|
| 34 |
+
text_model_head_blocks_drop_prob: float = 0.0
|
| 35 |
+
text_model_tokens_pooler_type: str = "first"
|
| 36 |
+
text_model_use_linear_projection: bool = False
|
| 37 |
+
init_logit_scale: float = math.log(1 / 0.07)
|
| 38 |
+
init_logit_bias: Optional[float] = None
|
| 39 |
+
freeze_logit_scale: bool = False
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class DinoTxt(nn.Module):
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
model_config: DinoTxtConfig,
|
| 46 |
+
vision_backbone: nn.Module,
|
| 47 |
+
text_backbone: nn.Module,
|
| 48 |
+
):
|
| 49 |
+
super().__init__()
|
| 50 |
+
self.model_config = model_config
|
| 51 |
+
self.visual_model = VisionTower(
|
| 52 |
+
vision_backbone,
|
| 53 |
+
model_config.vision_model_freeze_backbone,
|
| 54 |
+
model_config.embed_dim,
|
| 55 |
+
model_config.vision_model_num_head_blocks,
|
| 56 |
+
model_config.vision_model_head_blocks_drop_path,
|
| 57 |
+
model_config.vision_model_use_class_token,
|
| 58 |
+
model_config.vision_model_use_patch_tokens,
|
| 59 |
+
model_config.vision_model_patch_token_layer,
|
| 60 |
+
model_config.vision_model_patch_tokens_pooler_type,
|
| 61 |
+
model_config.vision_model_use_linear_projection,
|
| 62 |
+
)
|
| 63 |
+
self.text_model = TextTower(
|
| 64 |
+
text_backbone,
|
| 65 |
+
model_config.text_model_freeze_backbone,
|
| 66 |
+
model_config.embed_dim,
|
| 67 |
+
model_config.text_model_num_head_blocks,
|
| 68 |
+
model_config.text_model_head_blocks_is_causal,
|
| 69 |
+
model_config.text_model_head_blocks_drop_prob,
|
| 70 |
+
model_config.text_model_tokens_pooler_type,
|
| 71 |
+
model_config.text_model_use_linear_projection,
|
| 72 |
+
)
|
| 73 |
+
self.logit_scale = nn.Parameter(torch.ones(1) * model_config.init_logit_scale)
|
| 74 |
+
if model_config.freeze_logit_scale:
|
| 75 |
+
self.logit_scale.requires_grad = False
|
| 76 |
+
|
| 77 |
+
def init_weights(self):
|
| 78 |
+
self.visual_model.init_weights()
|
| 79 |
+
self.text_model.init_weights()
|
| 80 |
+
|
| 81 |
+
def get_visual_class_and_patch_tokens(self, image: Tensor) -> Tuple[Tensor, Tensor]:
|
| 82 |
+
return self.visual_model.get_class_and_patch_tokens(image)
|
| 83 |
+
|
| 84 |
+
def encode_image(
|
| 85 |
+
self,
|
| 86 |
+
image: Tensor,
|
| 87 |
+
normalize: bool = False,
|
| 88 |
+
) -> Tensor:
|
| 89 |
+
"""
|
| 90 |
+
Encode an image into a vector descriptor containing both global and local features.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
image (Tensor): Tensor of shape `(batch_size, rgb, height, width)`, normalized using ImageNet mean and std.
|
| 94 |
+
normalize (bool, optional): Whether to normalize the output vectors. Default is False.
|
| 95 |
+
Image features should always be normalized before comparing them with text features:
|
| 96 |
+
Returns:
|
| 97 |
+
Tensor: Tensor of shape `(batch_size, embed_dim)` containing the image features.
|
| 98 |
+
The first half of the vector corresponds to the global features (class token),
|
| 99 |
+
and the second half corresponds to the pooled patch features.
|
| 100 |
+
"""
|
| 101 |
+
features = self.visual_model(image)
|
| 102 |
+
return F.normalize(features, dim=-1) if normalize else features
|
| 103 |
+
|
| 104 |
+
def encode_text(self, text: Tensor, normalize: bool = False) -> Tensor:
|
| 105 |
+
"""
|
| 106 |
+
Encode a text input into a vector descriptor.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
text (Tensor): Tensor of shape `(batch_size, seq_len)` containing token indices.
|
| 110 |
+
normalize (bool, optional): Whether to normalize the output vectors. Default is False.
|
| 111 |
+
Text features should be normalized before comparing them with image features:
|
| 112 |
+
Returns:
|
| 113 |
+
Tensor: Tensor of shape `(batch_size, embed_dim)` containing the text features.
|
| 114 |
+
As a consequence of the training procedure, assume that the first half of the tensor corresponds
|
| 115 |
+
to global image features and the second half to pooled patch features.
|
| 116 |
+
"""
|
| 117 |
+
features = self.text_model(text)
|
| 118 |
+
return F.normalize(features, dim=-1) if normalize else features
|
| 119 |
+
|
| 120 |
+
def get_logits(self, image: Tensor, text: Tensor) -> Tuple[Tensor, Tensor]:
|
| 121 |
+
text_features = self.encode_text(text, normalize=True)
|
| 122 |
+
image_features = self.encode_image(image, normalize=True)
|
| 123 |
+
image_logits = self.logit_scale.exp() * image_features @ text_features.T
|
| 124 |
+
text_logits = image_logits.T
|
| 125 |
+
return image_logits, text_logits
|
| 126 |
+
|
| 127 |
+
def forward(
|
| 128 |
+
self,
|
| 129 |
+
image: Tensor,
|
| 130 |
+
text: Tensor,
|
| 131 |
+
) -> Tuple[Tensor, Tensor, Tensor]:
|
| 132 |
+
|
| 133 |
+
text_features = self.encode_text(text, normalize=True)
|
| 134 |
+
image_features = self.encode_image(image, normalize=True)
|
| 135 |
+
return image_features, text_features, self.logit_scale.exp()
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/dinov2_wrapper.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
from typing import Sequence
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class DINOv2Wrapper(torch.nn.Module):
|
| 12 |
+
def __init__(self, model):
|
| 13 |
+
super().__init__()
|
| 14 |
+
self.model = model
|
| 15 |
+
self.embed_dim = model.embed_dim
|
| 16 |
+
self.num_heads = model.num_heads
|
| 17 |
+
self.num_register_tokens = model.num_register_tokens
|
| 18 |
+
|
| 19 |
+
# Same as the original forward, but assert is_training and rename x_norm_regtokens -> x_storage_tokens
|
| 20 |
+
def forward(self, img, is_training: bool):
|
| 21 |
+
assert is_training
|
| 22 |
+
H, W = img.shape[-2:]
|
| 23 |
+
P = self.model.patch_size
|
| 24 |
+
x_dict = self.model(img, is_training=True)
|
| 25 |
+
x_dict["h"] = h = H // P
|
| 26 |
+
x_dict["w"] = w = W // P
|
| 27 |
+
assert x_dict["x_norm_patchtokens"].shape[-2] == h * w
|
| 28 |
+
return x_dict
|
| 29 |
+
|
| 30 |
+
# Same as the original get_intermediate_layers, but allow returining extra tokens (registers)
|
| 31 |
+
def get_intermediate_layers(
|
| 32 |
+
self,
|
| 33 |
+
x: torch.Tensor,
|
| 34 |
+
n: int | Sequence[int] = 1, # Layers or n last layers to take
|
| 35 |
+
reshape: bool = False,
|
| 36 |
+
return_class_token: bool = False,
|
| 37 |
+
return_register_tokens: bool = False,
|
| 38 |
+
norm=True,
|
| 39 |
+
) -> tuple[torch.Tensor] | tuple[tuple[torch.Tensor, ...], ...]:
|
| 40 |
+
if self.model.chunked_blocks:
|
| 41 |
+
outputs = self.model._get_intermediate_layers_chunked(x, n)
|
| 42 |
+
else:
|
| 43 |
+
outputs = self.model._get_intermediate_layers_not_chunked(x, n)
|
| 44 |
+
if norm:
|
| 45 |
+
outputs = [self.model.norm(out) for out in outputs]
|
| 46 |
+
class_tokens = [out[:, 0] for out in outputs]
|
| 47 |
+
register_tokens = [out[:, 1 : 1 + self.model.num_register_tokens] for out in outputs]
|
| 48 |
+
outputs = [out[:, 1 + self.model.num_register_tokens :] for out in outputs]
|
| 49 |
+
if reshape:
|
| 50 |
+
B, _, h, w = x.shape
|
| 51 |
+
outputs = [
|
| 52 |
+
out.reshape(B, h // self.model.patch_size, w // self.model.patch_size, -1)
|
| 53 |
+
.permute(0, 3, 1, 2)
|
| 54 |
+
.contiguous()
|
| 55 |
+
for out in outputs
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
if not return_class_token and not return_register_tokens:
|
| 59 |
+
return tuple(outputs)
|
| 60 |
+
if return_class_token and not return_register_tokens:
|
| 61 |
+
return tuple(zip(outputs, class_tokens))
|
| 62 |
+
if not return_class_token and return_register_tokens:
|
| 63 |
+
return tuple(zip(outputs, register_tokens))
|
| 64 |
+
return tuple(zip(outputs, class_tokens, register_tokens))
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/text_tower.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch import nn, Tensor
|
| 8 |
+
|
| 9 |
+
from dinov2.layers import (
|
| 10 |
+
CausalAttentionBlock,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TextHead(nn.Module):
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
input_dim: int,
|
| 18 |
+
embed_dim: int,
|
| 19 |
+
num_heads: int,
|
| 20 |
+
num_blocks: int,
|
| 21 |
+
block_drop_prob: float,
|
| 22 |
+
is_causal: bool,
|
| 23 |
+
use_linear_projection: bool,
|
| 24 |
+
):
|
| 25 |
+
super().__init__()
|
| 26 |
+
block_list = [nn.Identity()]
|
| 27 |
+
self.ln_final = nn.Identity()
|
| 28 |
+
if num_blocks > 0:
|
| 29 |
+
block_list = [
|
| 30 |
+
CausalAttentionBlock(
|
| 31 |
+
dim=input_dim,
|
| 32 |
+
num_heads=num_heads,
|
| 33 |
+
is_causal=is_causal,
|
| 34 |
+
dropout_prob=block_drop_prob,
|
| 35 |
+
)
|
| 36 |
+
for _ in range(num_blocks)
|
| 37 |
+
]
|
| 38 |
+
self.ln_final = nn.LayerNorm(input_dim)
|
| 39 |
+
self.block_list = nn.ModuleList(block_list)
|
| 40 |
+
self.num_blocks = num_blocks
|
| 41 |
+
self.linear_projection = nn.Identity()
|
| 42 |
+
if input_dim != embed_dim or use_linear_projection:
|
| 43 |
+
self.linear_projection = nn.Linear(input_dim, embed_dim, bias=False)
|
| 44 |
+
|
| 45 |
+
def init_weights(self):
|
| 46 |
+
if self.num_blocks > 0:
|
| 47 |
+
for i in range(self.num_blocks):
|
| 48 |
+
self.block_list[i].init_weights()
|
| 49 |
+
self.ln_final.reset_parameters()
|
| 50 |
+
if isinstance(self.linear_projection, nn.Linear):
|
| 51 |
+
nn.init.normal_(self.linear_projection.weight, std=self.linear_projection.in_features**-0.5)
|
| 52 |
+
|
| 53 |
+
def forward(self, text_tokens: Tensor) -> Tensor:
|
| 54 |
+
for block in self.block_list:
|
| 55 |
+
text_tokens = block(text_tokens)
|
| 56 |
+
text_tokens = self.ln_final(text_tokens)
|
| 57 |
+
return self.linear_projection(text_tokens)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class TextTower(nn.Module):
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
backbone: nn.Module,
|
| 64 |
+
freeze_backbone: bool,
|
| 65 |
+
embed_dim: int,
|
| 66 |
+
num_head_blocks: int,
|
| 67 |
+
head_blocks_is_causal: bool,
|
| 68 |
+
head_blocks_block_drop_prob: float,
|
| 69 |
+
tokens_pooler_type: str,
|
| 70 |
+
use_linear_projection: bool,
|
| 71 |
+
):
|
| 72 |
+
super().__init__()
|
| 73 |
+
self.backbone = backbone
|
| 74 |
+
self.freeze_backbone = freeze_backbone
|
| 75 |
+
backbone_out_dim = backbone.embed_dim
|
| 76 |
+
self.backbone = backbone
|
| 77 |
+
self.head = TextHead(
|
| 78 |
+
backbone_out_dim,
|
| 79 |
+
embed_dim,
|
| 80 |
+
self.backbone.num_heads,
|
| 81 |
+
num_head_blocks,
|
| 82 |
+
head_blocks_block_drop_prob,
|
| 83 |
+
head_blocks_is_causal,
|
| 84 |
+
use_linear_projection,
|
| 85 |
+
)
|
| 86 |
+
self.tokens_pooler_type = tokens_pooler_type
|
| 87 |
+
|
| 88 |
+
def init_weights(self):
|
| 89 |
+
self.backbone.init_weights()
|
| 90 |
+
self.head.init_weights()
|
| 91 |
+
|
| 92 |
+
def forward(self, token_indices: Tensor) -> Tensor:
|
| 93 |
+
text_tokens = self.backbone(token_indices)
|
| 94 |
+
text_tokens = self.head(text_tokens)
|
| 95 |
+
if self.tokens_pooler_type == "first":
|
| 96 |
+
features = text_tokens[:, 0]
|
| 97 |
+
elif self.tokens_pooler_type == "last":
|
| 98 |
+
features = text_tokens[:, -1]
|
| 99 |
+
elif self.tokens_pooler_type == "argmax":
|
| 100 |
+
assert token_indices is not None
|
| 101 |
+
features = text_tokens[torch.arange(text_tokens.shape[0]), token_indices.argmax(dim=-1)]
|
| 102 |
+
else:
|
| 103 |
+
raise ValueError(f"Unknown text tokens pooler type: {self.pooler_type}")
|
| 104 |
+
return features
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/text_transformer.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
from typing import Callable, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn, Tensor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
from dinov2.layers import CausalAttentionBlock
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TextTransformer(nn.Module):
|
| 16 |
+
def __init__(
|
| 17 |
+
self,
|
| 18 |
+
context_length: int,
|
| 19 |
+
vocab_size: int,
|
| 20 |
+
dim: int,
|
| 21 |
+
num_heads: int,
|
| 22 |
+
num_layers: int,
|
| 23 |
+
ffn_ratio: float,
|
| 24 |
+
is_causal: bool,
|
| 25 |
+
ls_init_value: Optional[float] = None,
|
| 26 |
+
act_layer: Callable = nn.GELU,
|
| 27 |
+
norm_layer: Callable = nn.LayerNorm,
|
| 28 |
+
dropout_prob: float = 0.0,
|
| 29 |
+
):
|
| 30 |
+
super().__init__()
|
| 31 |
+
self.vocab_size = vocab_size
|
| 32 |
+
self.embed_dim = dim
|
| 33 |
+
self.num_heads = num_heads
|
| 34 |
+
|
| 35 |
+
self.token_embedding = nn.Embedding(vocab_size, dim)
|
| 36 |
+
self.positional_embedding = nn.Parameter(torch.empty(context_length, dim))
|
| 37 |
+
self.dropout = nn.Dropout(dropout_prob)
|
| 38 |
+
self.num_layers = num_layers
|
| 39 |
+
block_list = [
|
| 40 |
+
CausalAttentionBlock(
|
| 41 |
+
dim=dim,
|
| 42 |
+
num_heads=num_heads,
|
| 43 |
+
ffn_ratio=ffn_ratio,
|
| 44 |
+
ls_init_value=ls_init_value,
|
| 45 |
+
is_causal=is_causal,
|
| 46 |
+
act_layer=act_layer,
|
| 47 |
+
norm_layer=norm_layer,
|
| 48 |
+
dropout_prob=dropout_prob,
|
| 49 |
+
)
|
| 50 |
+
for _ in range(num_layers)
|
| 51 |
+
]
|
| 52 |
+
self.blocks = nn.ModuleList(block_list)
|
| 53 |
+
self.ln_final = norm_layer(dim)
|
| 54 |
+
|
| 55 |
+
def init_weights(self):
|
| 56 |
+
nn.init.normal_(self.token_embedding.weight, std=0.02)
|
| 57 |
+
nn.init.normal_(self.positional_embedding, std=0.01)
|
| 58 |
+
init_attn_std = self.embed_dim**-0.5
|
| 59 |
+
init_proj_std = (self.embed_dim**-0.5) * ((2 * self.num_layers) ** -0.5)
|
| 60 |
+
init_fc_std = (2 * self.embed_dim) ** -0.5
|
| 61 |
+
for block in self.blocks:
|
| 62 |
+
block.init_weights(init_attn_std, init_proj_std, init_fc_std)
|
| 63 |
+
self.ln_final.reset_parameters()
|
| 64 |
+
|
| 65 |
+
def forward(self, token_indices: Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 66 |
+
_, N = token_indices.size()
|
| 67 |
+
x = self.token_embedding(token_indices) + self.positional_embedding[:N]
|
| 68 |
+
x = self.dropout(x)
|
| 69 |
+
for block in self.blocks:
|
| 70 |
+
x = block(x)
|
| 71 |
+
x = self.ln_final(x)
|
| 72 |
+
return x
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/tokenizer.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from typing import List, Union
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
from dinov2.thirdparty.CLIP.clip.simple_tokenizer import SimpleTokenizer
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Tokenizer(SimpleTokenizer):
|
| 14 |
+
def __init__(self, vocab_path: str):
|
| 15 |
+
SimpleTokenizer.__init__(self, bpe_path=vocab_path)
|
| 16 |
+
|
| 17 |
+
def tokenize(self, texts: Union[str, List[str]], context_length: int = 77) -> torch.LongTensor:
|
| 18 |
+
"""
|
| 19 |
+
Returns the tokenized representation of given input string(s)
|
| 20 |
+
|
| 21 |
+
Parameters
|
| 22 |
+
----------
|
| 23 |
+
texts : Union[str, List[str]]
|
| 24 |
+
An input string or a list of input strings to tokenize
|
| 25 |
+
context_length : int
|
| 26 |
+
The context length to use; all CLIP models use 77 as the context length
|
| 27 |
+
|
| 28 |
+
Returns
|
| 29 |
+
-------
|
| 30 |
+
A two-dimensional tensor containing the resulting tokens, shape = [number of input strings, context_length]
|
| 31 |
+
"""
|
| 32 |
+
if isinstance(texts, str):
|
| 33 |
+
texts = [texts]
|
| 34 |
+
sot_token = self.encoder["<|startoftext|>"]
|
| 35 |
+
eot_token = self.encoder["<|endoftext|>"]
|
| 36 |
+
all_tokens = [[sot_token] + self.encode(text) + [eot_token] for text in texts]
|
| 37 |
+
result = torch.zeros(len(all_tokens), context_length, dtype=torch.long)
|
| 38 |
+
|
| 39 |
+
for i, tokens in enumerate(all_tokens):
|
| 40 |
+
if len(tokens) > context_length:
|
| 41 |
+
tokens = tokens[:context_length] # Truncate
|
| 42 |
+
tokens[-1] = eot_token
|
| 43 |
+
result[i, : len(tokens)] = torch.tensor(tokens)
|
| 44 |
+
|
| 45 |
+
return result
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/vision_tower.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 4 |
+
# found in the LICENSE file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
from functools import partial
|
| 7 |
+
from typing import Callable, Tuple
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torch import nn, Tensor
|
| 11 |
+
|
| 12 |
+
from dinov2.layers import (
|
| 13 |
+
LayerScale,
|
| 14 |
+
NestedTensorBlock as AttentionBlock,
|
| 15 |
+
SwiGLUFFNAligned as SwiGLUFFN,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def init_weights_vit_timm(module: nn.Module, name: str = ""):
|
| 20 |
+
"""ViT weight initialization, original timm impl (for reproducibility)"""
|
| 21 |
+
if isinstance(module, nn.Linear):
|
| 22 |
+
nn.init.trunc_normal_(module.weight, std=0.02)
|
| 23 |
+
if module.bias is not None:
|
| 24 |
+
nn.init.zeros_(module.bias)
|
| 25 |
+
if isinstance(module, nn.LayerNorm):
|
| 26 |
+
module.reset_parameters()
|
| 27 |
+
if isinstance(module, LayerScale):
|
| 28 |
+
module.reset_parameters()
|
| 29 |
+
if isinstance(module, nn.Conv2d):
|
| 30 |
+
module.reset_parameters()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def named_apply(fn: Callable, module: nn.Module, name="", depth_first=True, include_root=False) -> nn.Module:
|
| 34 |
+
if not depth_first and include_root:
|
| 35 |
+
fn(module=module, name=name)
|
| 36 |
+
for child_name, child_module in module.named_children():
|
| 37 |
+
child_name = ".".join((name, child_name)) if name else child_name
|
| 38 |
+
named_apply(
|
| 39 |
+
fn=fn,
|
| 40 |
+
module=child_module,
|
| 41 |
+
name=child_name,
|
| 42 |
+
depth_first=depth_first,
|
| 43 |
+
include_root=True,
|
| 44 |
+
)
|
| 45 |
+
if depth_first and include_root:
|
| 46 |
+
fn(module=module, name=name)
|
| 47 |
+
return module
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class VisionHead(nn.Module):
|
| 51 |
+
def __init__(
|
| 52 |
+
self,
|
| 53 |
+
input_dim: int,
|
| 54 |
+
embed_dim: int,
|
| 55 |
+
num_heads: int,
|
| 56 |
+
num_blocks: int,
|
| 57 |
+
blocks_drop_path: float,
|
| 58 |
+
use_class_token: bool,
|
| 59 |
+
use_patch_tokens: bool,
|
| 60 |
+
use_linear_projection: bool,
|
| 61 |
+
):
|
| 62 |
+
super().__init__()
|
| 63 |
+
block_list = [nn.Identity()]
|
| 64 |
+
self.ln_final = nn.Identity()
|
| 65 |
+
if num_blocks > 0:
|
| 66 |
+
block_list = [
|
| 67 |
+
AttentionBlock(
|
| 68 |
+
input_dim,
|
| 69 |
+
num_heads,
|
| 70 |
+
ffn_layer=partial(SwiGLUFFN, align_to=64),
|
| 71 |
+
init_values=1e-5,
|
| 72 |
+
drop_path=blocks_drop_path,
|
| 73 |
+
)
|
| 74 |
+
for _ in range(num_blocks)
|
| 75 |
+
]
|
| 76 |
+
self.ln_final = nn.LayerNorm(input_dim)
|
| 77 |
+
self.block_list = nn.ModuleList(block_list)
|
| 78 |
+
self.num_blocks = num_blocks
|
| 79 |
+
multiplier = 2 if use_class_token and use_patch_tokens else 1
|
| 80 |
+
self.linear_projection = nn.Identity()
|
| 81 |
+
if multiplier * input_dim != embed_dim or use_linear_projection:
|
| 82 |
+
assert embed_dim % multiplier == 0, f"Expects {embed_dim} to be divisible by {multiplier}"
|
| 83 |
+
self.linear_projection = nn.Linear(input_dim, embed_dim // multiplier, bias=False)
|
| 84 |
+
|
| 85 |
+
def init_weights(self):
|
| 86 |
+
if self.num_blocks > 0:
|
| 87 |
+
for i in range(self.num_blocks):
|
| 88 |
+
block = self.block_list[i]
|
| 89 |
+
named_apply(init_weights_vit_timm, block)
|
| 90 |
+
self.ln_final.reset_parameters()
|
| 91 |
+
if isinstance(self.linear_projection, nn.Linear):
|
| 92 |
+
nn.init.normal_(self.linear_projection.weight, std=self.linear_projection.in_features**-0.5)
|
| 93 |
+
|
| 94 |
+
def forward(self, image_tokens: Tensor) -> Tensor:
|
| 95 |
+
for block in self.block_list:
|
| 96 |
+
image_tokens = block(image_tokens)
|
| 97 |
+
image_tokens = self.ln_final(image_tokens)
|
| 98 |
+
return self.linear_projection(image_tokens)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class VisionTower(nn.Module):
|
| 102 |
+
def __init__(
|
| 103 |
+
self,
|
| 104 |
+
backbone: nn.Module,
|
| 105 |
+
freeze_backbone: bool,
|
| 106 |
+
embed_dim: int,
|
| 107 |
+
num_head_blocks: int,
|
| 108 |
+
head_blocks_block_drop_path: float,
|
| 109 |
+
use_class_token: bool,
|
| 110 |
+
use_patch_tokens: bool,
|
| 111 |
+
patch_token_layer: int,
|
| 112 |
+
patch_tokens_pooler_type: str,
|
| 113 |
+
use_linear_projection: bool,
|
| 114 |
+
):
|
| 115 |
+
super().__init__()
|
| 116 |
+
self.backbone = backbone
|
| 117 |
+
self.freeze_backbone = freeze_backbone
|
| 118 |
+
self.use_class_token = use_class_token
|
| 119 |
+
self.use_patch_tokens = use_patch_tokens
|
| 120 |
+
self.patch_token_layer = patch_token_layer
|
| 121 |
+
self.patch_tokens_pooler_type = patch_tokens_pooler_type
|
| 122 |
+
self.num_register_tokens = 0
|
| 123 |
+
if hasattr(self.backbone, "num_register_tokens"):
|
| 124 |
+
self.num_register_tokens = self.backbone.num_register_tokens
|
| 125 |
+
elif hasattr(self.backbone, "n_storage_tokens"):
|
| 126 |
+
self.num_register_tokens = self.backbone.n_storage_tokens
|
| 127 |
+
backbone_out_dim = self.backbone.embed_dim
|
| 128 |
+
self.head = VisionHead(
|
| 129 |
+
backbone_out_dim,
|
| 130 |
+
embed_dim,
|
| 131 |
+
self.backbone.num_heads,
|
| 132 |
+
num_head_blocks,
|
| 133 |
+
head_blocks_block_drop_path,
|
| 134 |
+
use_class_token,
|
| 135 |
+
use_patch_tokens,
|
| 136 |
+
use_linear_projection,
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
def init_weights(self):
|
| 140 |
+
if not self.freeze_backbone:
|
| 141 |
+
self.backbone.init_weights()
|
| 142 |
+
self.head.init_weights()
|
| 143 |
+
|
| 144 |
+
def get_backbone_features(self, images: Tensor) -> Tuple[Tensor, Tensor, Tensor]:
|
| 145 |
+
tokens = self.backbone.get_intermediate_layers(
|
| 146 |
+
images,
|
| 147 |
+
n=self.patch_token_layer,
|
| 148 |
+
return_class_token=True,
|
| 149 |
+
return_register_tokens=True,
|
| 150 |
+
)
|
| 151 |
+
class_token = tokens[-1][1]
|
| 152 |
+
patch_tokens = tokens[0][0]
|
| 153 |
+
register_tokens = tokens[0][2]
|
| 154 |
+
return class_token, patch_tokens, register_tokens
|
| 155 |
+
|
| 156 |
+
def get_class_and_patch_tokens(self, images: Tensor) -> Tuple[Tensor, Tensor]:
|
| 157 |
+
class_token, patch_tokens, register_tokens = self.get_backbone_features(images)
|
| 158 |
+
image_tokens = self.head(torch.cat([class_token.unsqueeze(1), register_tokens, patch_tokens], dim=1))
|
| 159 |
+
class_token, patch_tokens = image_tokens[:, 0], image_tokens[:, self.num_register_tokens + 1 :]
|
| 160 |
+
return class_token, patch_tokens
|
| 161 |
+
|
| 162 |
+
def forward(self, images: Tensor) -> Tensor:
|
| 163 |
+
class_token, patch_tokens = self.get_class_and_patch_tokens(images)
|
| 164 |
+
features = []
|
| 165 |
+
if self.use_class_token:
|
| 166 |
+
features.append(class_token)
|
| 167 |
+
if self.use_patch_tokens:
|
| 168 |
+
if self.patch_tokens_pooler_type == "mean":
|
| 169 |
+
features.append(torch.mean(patch_tokens, dim=1))
|
| 170 |
+
elif self.patch_tokens_pooler_type == "max":
|
| 171 |
+
features.append(torch.max(patch_tokens, dim=1).values)
|
| 172 |
+
elif self.patch_tokens_pooler_type == "gem":
|
| 173 |
+
power = 3
|
| 174 |
+
eps = 1e-6
|
| 175 |
+
patch_tokens_power = patch_tokens.clamp(min=eps).pow(power)
|
| 176 |
+
features.append(torch.mean(patch_tokens_power, dim=1).pow(1 / power))
|
| 177 |
+
else:
|
| 178 |
+
raise ValueError(f"Unknown patch tokens pooler type: {self.patch_tokens_pooler_type}")
|
| 179 |
+
return torch.cat(features, dim=-1)
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/xray_dino/__pycache__/backbones.cpython-311.pyc
ADDED
|
Binary file (1.07 kB). View file
|
|
|
migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/xray_dino/backbones.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
#
|
| 3 |
+
# This source code is licensed under the licence
|
| 4 |
+
# found in the LICENSE_XRAY_DINO_MODEL file in the root directory of this source tree.
|
| 5 |
+
|
| 6 |
+
from typing import Union
|
| 7 |
+
|
| 8 |
+
from ..backbones import Weights, _make_dinov2_model
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def xray_dino_vitl16(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.XRAY_DINO, **kwargs):
|
| 12 |
+
"""
|
| 13 |
+
XRay-DINO ViT-L/16 model (optionally) pretrained on the XRay-DINO dataset.
|
| 14 |
+
"""
|
| 15 |
+
return _make_dinov2_model(
|
| 16 |
+
arch_name="vit_large",
|
| 17 |
+
patch_size=16,
|
| 18 |
+
img_size=512,
|
| 19 |
+
num_register_tokens=0,
|
| 20 |
+
interpolate_antialias=False,
|
| 21 |
+
interpolate_offset=0.1,
|
| 22 |
+
block_chunks=4,
|
| 23 |
+
pretrained=pretrained,
|
| 24 |
+
weights=weights,
|
| 25 |
+
hash="ad31c2b0",
|
| 26 |
+
check_hash=True,
|
| 27 |
+
**kwargs,
|
| 28 |
+
)
|
migrator/code/mv-sam3d-for-6d-v2/IMPLEMENTATION.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mv-sam3d-for-6d-v2 — Implementation
|
| 2 |
+
|
| 3 |
+
A clean, self-contained rebuild of the multi-view (multi-camera) DexYCB 6D-pose
|
| 4 |
+
flow model. Conceptually: *take SAM-3D (`sam3d-for-6d`), drop the shape-K/V
|
| 5 |
+
forcing, change the conditioning fed to the flow transformer, for multi-view
|
| 6 |
+
DexYCB 6D pose.* Everything that already exists in the fork is **vendored
|
| 7 |
+
verbatim** and warm-started from pretrained weights; only the genuinely-new
|
| 8 |
+
pieces (DexYCB multi-view dataset, calibration, training loop, latent-cache
|
| 9 |
+
builder, and the one per-view embedding) are written from scratch.
|
| 10 |
+
|
| 11 |
+
All paths are absolute. Python env:
|
| 12 |
+
`/NHNHOME/data/jonghoon/miniconda3/envs/sam3d-objects/bin/python`.
|
| 13 |
+
GPU work is **sbatch-only**; nothing sets `CUDA_VISIBLE_DEVICES`. The repo is
|
| 14 |
+
self-contained: `PYTHONPATH=<this repo>` only — no dependency on the fork's path,
|
| 15 |
+
no TRELLIS. Every vendored-package `__init__.py` is empty (the fork's lidra init
|
| 16 |
+
chain is NOT pulled in), so imports are clean.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## Model design (locked)
|
| 21 |
+
|
| 22 |
+
* **Backbone:** `SparseStructureFlowTdfyWrapper` (MoT DiT), `mode="default"`
|
| 23 |
+
(fully-symmetric joint self-attention), **`is_shortcut_model=False`** = plain
|
| 24 |
+
rectified flow, v-prediction, `sigma_min=0`. `model_channels=cond_channels=1024`,
|
| 25 |
+
`num_blocks=24`, `num_heads=16`, `mlp_ratio=4`, `pe_mode=ape`,
|
| 26 |
+
`qk_rms_norm=True`, `time_scale=1000`.
|
| 27 |
+
* **Two MoT streams:**
|
| 28 |
+
* `shape` — SS latent `[B,8,16,16,16]` → `view(B,8,-1).permute(0,2,1)` →
|
| 29 |
+
`[B,4096,8]` (row-major to match `ShapePositionEmbedder` meshgrid `ij`,
|
| 30 |
+
res16/patch1).
|
| 31 |
+
* pose — one shared transformer over `{6drotation_normalized[1,6],
|
| 32 |
+
translation[1,3], scale[1,1]}` via `latent_share_transformer`. The shared
|
| 33 |
+
group is **keyed `6drotation_normalized`** (matches the pretrained block-stream
|
| 34 |
+
name so all per-block weights warm-start; see warm-start note below).
|
| 35 |
+
* **NO shape-K/V injection.**
|
| 36 |
+
* **Loss** = weighted v-pred MSE, all stream weights **1.0** (shape /
|
| 37 |
+
6drotation_normalized / translation / scale) — matches the pretrained backbone's
|
| 38 |
+
training regime (SAM-3D fork uses rot=trans=scale=1.0).
|
| 39 |
+
|
| 40 |
+
## Conditioning / multi-view (the genuinely-new part)
|
| 41 |
+
|
| 42 |
+
One timestamp, multiple synchronized DexYCB cameras sharing one object pose. The
|
| 43 |
+
**anchor** camera defines the reference frame; others are **aux**. Everything is
|
| 44 |
+
in the anchor camera's PyTorch3D frame. The SAME `EmbedderFuser` instance runs
|
| 45 |
+
per view, and a per-view **camera embedding** `mlp_cam(extrinsics_v)` is added to
|
| 46 |
+
that view's tokens (replacing the old content-free `view_emb` slot table). A
|
| 47 |
+
2-layer MLP maps each view's relative aux→anchor extrinsics — rot6d(6) +
|
| 48 |
+
SSI-normalized camera centre(3), 9-D, anchor = identity — to `model_channels`,
|
| 49 |
+
with the output layer zero-initialized so step-0 conditioning equals the no-tag
|
| 50 |
+
baseline. This binds the camera frame to every token of the view (including the
|
| 51 |
+
geometry-free RGB/mask tokens) and is camera-consistent across the random aux
|
| 52 |
+
subset. Anchor
|
| 53 |
+
passes all 6 fuser kwargs (cropped+full of image/mask/pointmap); each aux passes
|
| 54 |
+
the 3 cropped kwargs. All views' tokens are concatenated into one condition
|
| 55 |
+
sequence the DiT cross-attends to. Pointmaps KEEP NaN at invalid pixels so the
|
| 56 |
+
pretrained `invalid_xyz_token` pathway fires; pointmaps/masks use nearest-mode
|
| 57 |
+
(NaN-preserving) interpolation.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## File map
|
| 62 |
+
|
| 63 |
+
```
|
| 64 |
+
mvsam3d/
|
| 65 |
+
data/
|
| 66 |
+
canonical.py # CAD -> canonical voxel (vendored voxelize_mesh) + GT layout math
|
| 67 |
+
dexycb_calib.py # extrinsics/intrinsics; cross-camera P3D transform (aux->anchor)
|
| 68 |
+
dexycb_dataset.py # MultiViewDexYCB + collate_multiview [from scratch]
|
| 69 |
+
model/
|
| 70 |
+
condition.py # MultiViewConditionEmbedder (real EmbedderFuser + per-view view_emb)
|
| 71 |
+
flow_model.py # MVSam3DFlow: MoT joint flow + flow_step + loss + warm-start
|
| 72 |
+
ss_vae.py # REAL SAM-3D SS encoder/decoder loaders (ss_encoder.ckpt / ss_decoder.ckpt)
|
| 73 |
+
train/
|
| 74 |
+
train.py # YAML-driven DDP training loop (grad-accum + periodic val) [from scratch]
|
| 75 |
+
validate.py # sampler-based validation: decode->64^3 IoU + pose metrics [from scratch]
|
| 76 |
+
sam3d_objects/ # VENDORED closure (byte-identical to the fork; empty __init__ chain)
|
| 77 |
+
data/utils.py
|
| 78 |
+
model/layers/llama3/ff.py
|
| 79 |
+
model/backbone/dit/embedder/{dino,embedder_fuser,pointmap,point_remapper}.py
|
| 80 |
+
model/backbone/tdfy_dit/models/{mm_latent,mot_sparse_structure_flow,timestep_embedder,
|
| 81 |
+
sparse_structure_vae}.py
|
| 82 |
+
model/backbone/tdfy_dit/modules/{spatial,utils,norm}.py + transformer/ + attention/
|
| 83 |
+
model/backbone/generator/flow_matching/solver.py # Euler ODE solver (reused for sampling)
|
| 84 |
+
training/stage1_flow_lora/distributed.py # DDP/DeepSpeed primitives (reused)
|
| 85 |
+
geometry/{geometry,rotation_constants}.py # column-convention rot6d utils
|
| 86 |
+
pipeline/inference_utils.py # voxelize_mesh / normalize_mesh_verts / trimesh2o3d_mesh (verbatim)
|
| 87 |
+
tools/
|
| 88 |
+
build_canonical_latents.py # 21 objects -> cache/canonical/*.pt (GPU)
|
| 89 |
+
validate_model.py # pretrained-load + fwd/bwd + grad audit (GPU)
|
| 90 |
+
verify_rotation_roundtrip.py # rot6d encode/decode inverse check (CPU)
|
| 91 |
+
verify_gt_reproject.py # GT modal-reproject IoU vs seg (CPU)
|
| 92 |
+
amodal_reproject.py # GT amodal IoU vs independent mesh render (CPU)
|
| 93 |
+
diagnose_reproject.py # GT reproject recall vs seg (CPU)
|
| 94 |
+
configs/train.yaml
|
| 95 |
+
sbatch/train.sh # training launcher
|
| 96 |
+
cache/canonical/<object>.pt # {latent[8,16,16,16], occupancy[64^3] uint8, max_extent, center_cad}
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## Inherited (vendored, byte-identical to fork) vs from-scratch
|
| 100 |
+
|
| 101 |
+
| Component | Source | Notes |
|
| 102 |
+
|---|---|---|
|
| 103 |
+
| MoT backbone, `Latent`, position embedders | vendored | `mot_sparse_structure_flow.py`, `mm_latent.py` |
|
| 104 |
+
| `EmbedderFuser`, `Dino`, `PointPatchEmbed`, `FeedForward`, `PointRemapper` | vendored | embedder closure |
|
| 105 |
+
| SS VAE `SparseStructureEncoder/Decoder` | vendored | `sparse_structure_vae.py` |
|
| 106 |
+
| rot6d utils (column convention) + constants | vendored | `geometry.py`, `rotation_constants.py` |
|
| 107 |
+
| `voxelize_mesh` / `normalize_mesh_verts` / `trimesh2o3d_mesh` | vendored | verbatim functions only |
|
| 108 |
+
| `Euler` ODE solver (`flow_matching/solver.py`) | vendored | reused by `MVSam3DFlow.sample` |
|
| 109 |
+
| DDP/DeepSpeed primitives (`distributed.py`) | vendored | reused by `train.py` |
|
| 110 |
+
| `MultiViewConditionEmbedder` | from scratch | thin wrapper; only `mlp_cam` (per-view camera embedding from extrinsics) is a new module |
|
| 111 |
+
| `MVSam3DFlow` (config/flow_step/loss/warm-start/sample/predict) | from scratch | plain flow, no shape-K/V |
|
| 112 |
+
| DexYCB dataset / calibration / canonical GT | from scratch | no fork equivalent |
|
| 113 |
+
| latent-cache builder, DDP training loop, validation, config | from scratch | |
|
| 114 |
+
|
| 115 |
+
## Pretrained weights
|
| 116 |
+
|
| 117 |
+
* **SS encoder/decoder = the REAL SAM-3D ones** (`checkpoints/hf/ss_encoder.ckpt`,
|
| 118 |
+
`ss_decoder.ckpt`), loaded with **0 missing / 0 unexpected** keys through the
|
| 119 |
+
vendored `SparseStructureEncoder`/`SparseStructureDecoder`. GT latents use the
|
| 120 |
+
posterior MEAN (`sample_posterior=False`). GT latents are validated by
|
| 121 |
+
roundtrip through the **standalone** `ss_decoder.ckpt` (expect IoU≈1.0). The
|
| 122 |
+
decoder embedded in `ss_generator.ckpt` is TRELLIS-identical and gives IoU≈0 on
|
| 123 |
+
these latents — reported for contrast, never used to validate.
|
| 124 |
+
* **Warm-start from `ss_generator.ckpt`:** the condition embedder
|
| 125 |
+
(`_base_models.condition_embedder.*`, 722 tensors) loads fully into the frozen
|
| 126 |
+
`EmbedderFuser`; the torso (`_base_models.generator.reverse_fn.backbone.*`)
|
| 127 |
+
loads **936 / 936** tensors — `t_embedder`, both `latent_mapping` streams, and
|
| 128 |
+
**all** per-block transformer weights for both MoT streams (`shape` and the
|
| 129 |
+
shared pose stream). **The shared pose group is named `6drotation_normalized`
|
| 130 |
+
(NOT `pose`)** so its per-block keys match the pretrained block-stream naming
|
| 131 |
+
(the fork names a shared MoT group after one of its member latents); naming it
|
| 132 |
+
`pose` previously left all 432 pose-stream block tensors at random init. Only
|
| 133 |
+
the shortcut `d_embedder` (4 tensors) and the unused `translation_scale` stream
|
| 134 |
+
have no home here and are skipped cleanly (never forced). After this, **the
|
| 135 |
+
ONLY from-scratch module in the whole model is `mlp_cam`** (the per-view camera
|
| 136 |
+
embedding; no pretrained source, output layer zero-init), which injects the
|
| 137 |
+
relative camera extrinsics that tag the new multi-view axis.
|
| 138 |
+
|
| 139 |
+
## Data / GT
|
| 140 |
+
|
| 141 |
+
* `dexycb_root=/NHNHOME/data/jonghoon/pose6daug/dataset/dexycb`. `ycb_ids`/`seg`
|
| 142 |
+
are 1-based class indices; object mask = `(seg==ycb_id)`. Depth uint16 mm /1000,
|
| 143 |
+
clip to [0.1,3.0] m, invalid→NaN. Pointmap = unproject(aligned_depth) in OpenCV
|
| 144 |
+
→ PyTorch3D via `*[-1,-1,1]`. Aux pointmaps transformed into the anchor frame
|
| 145 |
+
via DexYCB extrinsics BEFORE one shared SSI on the anchor.
|
| 146 |
+
* **SSI** = base/default isotropic: `shift=[0,0,median_z]`,
|
| 147 |
+
`scale=mean(|p-shift|)` over finite points (matches the fork's BASE
|
| 148 |
+
`ScaleShiftInvariant.get_scale_and_shift`). Isotropic is required because the
|
| 149 |
+
GT pose is a rigid similarity; a per-dim anisotropic SSI would shear the
|
| 150 |
+
rotation cue. One SSI computed on the anchor is applied to all fused views.
|
| 151 |
+
* **GT layout (anchor cam, P3D):** with `M=[[1,0,0],[0,0,-1],[0,1,0]]`,
|
| 152 |
+
`D=diag(-1,-1,1)`: `R_p3d=D·R_cv`, `t_p3d=D·t_cv`; then `R=R_dex·Mᵀ`,
|
| 153 |
+
`t=R_dex·center_cad+t_dex`, `s=max_extent`. `center_cad`/`max_extent` are
|
| 154 |
+
derived from the vendored `voxelize_mesh` output (`max_extent=1/scale`,
|
| 155 |
+
`center_cad=Mᵀ·center_rot`).
|
| 156 |
+
* **Rotation 6D:** encode `normalize_rot6d(matrix_to_rot6d(R))`; decode
|
| 157 |
+
`rot6d_to_matrix(denormalize_rot6d(x))` — the vendored COLUMN-convention
|
| 158 |
+
functions used consistently in BOTH dataset and model. (The prior attempt's
|
| 159 |
+
interleaved `R[:,:2].reshape(6)` encode was inconsistent with the
|
| 160 |
+
column-convention decode — a real bug, fixed here.)
|
| 161 |
+
|
| 162 |
+
## Distributed + validation
|
| 163 |
+
|
| 164 |
+
* **DDP.** `sbatch/train.sh` launches `torch.distributed.run --nproc_per_node=$NPROC`
|
| 165 |
+
(`NPROC` defaults to `SLURM_GPUS_ON_NODE`). The generic DDP/DeepSpeed primitives
|
| 166 |
+
are the VENDORED `distributed.py` (`init_distributed`/`setup_runtime`/`barrier`/
|
| 167 |
+
`reduce_scalar_dict`/`cleanup_distributed`); `train.py` is the only from-scratch
|
| 168 |
+
glue. Device selection is by `LOCAL_RANK` (never `CUDA_VISIBLE_DEVICES`).
|
| 169 |
+
`WORLD_SIZE=1` transparently skips the process group + DDP wrap. Training routes
|
| 170 |
+
through `model(batch)` (== `flow_step`) so DDP all-reduces gradients; supports
|
| 171 |
+
`train.grad_accum_steps` (micro-step `no_sync` until the boundary).
|
| 172 |
+
* **Sampling.** `MVSam3DFlow.sample` integrates the rectified-flow ODE from noise
|
| 173 |
+
(t=0) to data (t=1) with the VENDORED `Euler` solver, exact inverse of `flow_step`
|
| 174 |
+
(same `time_scale`, `mode="default"`). `predict` decodes the streams to metric
|
| 175 |
+
`{shape_latent, rotation[3,3], translation[3], scale}`.
|
| 176 |
+
* **Validation** (`validate.py`, every `train.val_every` steps on a disjoint,
|
| 177 |
+
deterministic held-out split): runs `predict` then reports `shape_iou` (decode
|
| 178 |
+
pred latent through the standalone `ss_decoder.ckpt` vs GT canonical occupancy),
|
| 179 |
+
`rot_deg` (geodesic), `trans_cm`, `scale_rel` — all summed-with-count and
|
| 180 |
+
all-reduced across ranks for an exact mean. Val uses a fixed `max_aux` aux views
|
| 181 |
+
so `view_emb` is never indexed past its trained range.
|
| 182 |
+
* **Verified (job 4331, 2× B200, world=2):** DDP process group + gradient reduce +
|
| 183 |
+
barrier, 4 steps finite loss, validation at steps 2 & 4 (sampler→decode→metrics,
|
| 184 |
+
reduced n=2), checkpoint saved, clean exit (COMPLETED).
|
| 185 |
+
|
| 186 |
+
## How to run
|
| 187 |
+
|
| 188 |
+
```sh
|
| 189 |
+
# multi-GPU: request N GPUs; one process per GPU is launched automatically
|
| 190 |
+
sbatch --gres=gpu:b200:4 sbatch/train.sh --config configs/train.yaml
|
| 191 |
+
# single GPU (default sbatch header) or quick smoke:
|
| 192 |
+
sbatch sbatch/train.sh --config configs/train.yaml --set train.steps=3 --set data.subjects=[subject-01]
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
View selection lives under `data:`: `random_views: true` with
|
| 196 |
+
`min_aux_views`/`max_aux_views` (total = 1 + k aux), or `random_views: false`
|
| 197 |
+
for the first `num_aux_views` cameras. `model.max_views` is derived in `train.py`
|
| 198 |
+
(now API-only; `mlp_cam` has no per-slot table, so the view count is unbounded).
|
| 199 |
+
Camera embedding: `condition.cam_hidden_dim` sets the `mlp_cam` hidden width.
|
| 200 |
+
Validation knobs: `data.val_fraction`/`split_seed`, `train.val_every`/
|
| 201 |
+
`val_num_steps`/`val_max_batches`. Distributed knobs: `train.backend`
|
| 202 |
+
(`ddp`|`deepspeed`), `grad_accum_steps`, `amp`/`amp_dtype`. LR schedule:
|
| 203 |
+
`train.scheduler` (`cosine`|`constant`), `warmup_steps`, `min_lr` — REUSES the
|
| 204 |
+
vendored `train_utils.build_scheduler` (warmup→cosine→min_lr). Resume:
|
| 205 |
+
`train.resume_from=<ckpt>` restores trainables + optimizer + scheduler + scaler +
|
| 206 |
+
step via the vendored `checkpointing.{save,load}_checkpoint` (frozen DINO/fuser are
|
| 207 |
+
reconstructed from `pretrained_ckpt`, not stored). Verified: jobs 4385 (0→8) +
|
| 208 |
+
4386 (resume@3) — LR curve continuous across resume, optimizer state restored.
|
migrator/code/mv-sam3d-for-6d-v2/configs/heldout.yaml
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Held-out manifest for integrated training. Two kinds (distinction matters):
|
| 2 |
+
# ood_datasets : whole-dataset OOD eval; objects MAY overlap training
|
| 3 |
+
# (HO-3D is YCB) -> NO object-disjointness enforced.
|
| 4 |
+
# unseen_object_datasets : whole dataset held out AND its object identities
|
| 5 |
+
# reserved (removed from every train dataset). Objects
|
| 6 |
+
# are non-YCB so this is safe.
|
| 7 |
+
ood_datasets: [ho3d, ycbm, hope]
|
| 8 |
+
unseen_object_datasets: [housecat6d]
|
| 9 |
+
# GraspNet test_novel object holdout (M1 resolved). GraspNet stays a `role:train`
|
| 10 |
+
# dataset built on the full `graspnet` root, but the 42 objects that appear in the
|
| 11 |
+
# official test_novel scenes (160-189) are reserved here so they are removed from
|
| 12 |
+
# EVERY train dataset (leak-proof unseen-object holdout for GraspNet-novel eval).
|
| 13 |
+
# Derivation (authoritative, matches graspnetAPI): test_novel = sceneIds 160-189
|
| 14 |
+
# (graspnetAPI/graspnet.py); getObjIds() = union of each scene's object_id_list.txt.
|
| 15 |
+
# We took that union over scenes 160-189 (42 distinct 0-based object ids). The
|
| 16 |
+
# adapter key is `graspnet/{cls_value-1:03d}` and cls_value (meta.mat cls_indexes)
|
| 17 |
+
# == object_id+1 (verified on scenes 160/165/170/180/189), so cls_value-1 ==
|
| 18 |
+
# object_id_list value; keys below are `graspnet/<id:03d>` for the 42 novel ids.
|
| 19 |
+
# NOTE: 9 of these (017,020,026,027,030,051,062,063,066) also appear in
|
| 20 |
+
# train/seen/similar scenes — the global object-disjointness filter removes them
|
| 21 |
+
# from training everywhere, which is exactly why this reservation is required.
|
| 22 |
+
unseen_object_keys_extra:
|
| 23 |
+
- graspnet/003
|
| 24 |
+
- graspnet/004
|
| 25 |
+
- graspnet/006
|
| 26 |
+
- graspnet/017
|
| 27 |
+
- graspnet/019
|
| 28 |
+
- graspnet/020
|
| 29 |
+
- graspnet/024
|
| 30 |
+
- graspnet/026
|
| 31 |
+
- graspnet/027
|
| 32 |
+
- graspnet/028
|
| 33 |
+
- graspnet/030
|
| 34 |
+
- graspnet/031
|
| 35 |
+
- graspnet/032
|
| 36 |
+
- graspnet/033
|
| 37 |
+
- graspnet/035
|
| 38 |
+
- graspnet/045
|
| 39 |
+
- graspnet/047
|
| 40 |
+
- graspnet/049
|
| 41 |
+
- graspnet/051
|
| 42 |
+
- graspnet/055
|
| 43 |
+
- graspnet/062
|
| 44 |
+
- graspnet/063
|
| 45 |
+
- graspnet/065
|
| 46 |
+
- graspnet/066
|
| 47 |
+
- graspnet/067
|
| 48 |
+
- graspnet/071
|
| 49 |
+
- graspnet/072
|
| 50 |
+
- graspnet/073
|
| 51 |
+
- graspnet/074
|
| 52 |
+
- graspnet/075
|
| 53 |
+
- graspnet/076
|
| 54 |
+
- graspnet/077
|
| 55 |
+
- graspnet/078
|
| 56 |
+
- graspnet/079
|
| 57 |
+
- graspnet/080
|
| 58 |
+
- graspnet/081
|
| 59 |
+
- graspnet/082
|
| 60 |
+
- graspnet/083
|
| 61 |
+
- graspnet/084
|
| 62 |
+
- graspnet/085
|
| 63 |
+
- graspnet/086
|
| 64 |
+
- graspnet/087
|
| 65 |
+
never_train: [ho3d, housecat6d] # adapters refused for split:train
|
| 66 |
+
|
| 67 |
+
roots:
|
| 68 |
+
housecat6d: ${MIGRATOR_DATA_ROOT}/housecat6d/extracted
|
| 69 |
+
adapter_kwargs:
|
| 70 |
+
housecat6d: {split: test}
|
migrator/code/mv-sam3d-for-6d-v2/configs/train.yaml
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Training config for the multi-view 6D-pose flow model (v2).
|
| 2 |
+
# ALL training-related hyperparameters live here; train.py reads them and nothing
|
| 3 |
+
# training-related is hardcoded in Python. Launch via:
|
| 4 |
+
# sbatch sbatch/train.sh --config configs/train.yaml
|
| 5 |
+
# Per-key overrides: --set key.subkey=value (repeatable), e.g. --set train.steps=3
|
| 6 |
+
|
| 7 |
+
# --------------------------------------------------------------------------- #
|
| 8 |
+
# data -> MultiViewDexYCB
|
| 9 |
+
# --------------------------------------------------------------------------- #
|
| 10 |
+
data:
|
| 11 |
+
dexycb_root: ${MIGRATOR_DATA_ROOT}/dexycb
|
| 12 |
+
canonical_cache: ${MIGRATOR_CACHE}/canonical
|
| 13 |
+
ssi_mode: scene # scene | object_centric — SSI convention for pointmaps
|
| 14 |
+
# + t/s targets + cam features. "scene" = base SSI
|
| 15 |
+
# (shift=[0,0,median z], all pre-2026-07-17 ckpts).
|
| 16 |
+
# "object_centric" = vendored ObjectCentricSSI (object-
|
| 17 |
+
# median shift, scene scale; release-pipeline pairing).
|
| 18 |
+
# A ckpt MUST be evaluated with its training mode.
|
| 19 |
+
frame_stride: 5 # sample every Nth frame in each sequence
|
| 20 |
+
anchor_sweep: true # every camera serves as anchor (else only the first)
|
| 21 |
+
subjects: null # null = all subjects; or a list of name substrings
|
| 22 |
+
image_size: 518 # DINOv2 input resolution (= IMG_SIZE)
|
| 23 |
+
# --- view selection ---
|
| 24 |
+
num_aux_views: 7 # legacy fixed count, used only when random_views=false
|
| 25 |
+
random_views: true # draw a random count + random subset of aux cameras per sample
|
| 26 |
+
min_aux_views: 1 # inclusive lower bound on aux count when random_views=true
|
| 27 |
+
max_aux_views: 3 # inclusive upper bound -> total views = 1 + [1..3] = 2..4
|
| 28 |
+
# --- depth preprocessing constants ---
|
| 29 |
+
depth_scale: 1000.0 # DexYCB depth PNG is uint16 millimetres
|
| 30 |
+
depth_near: 0.1 # metres; clip implausibly-near depth
|
| 31 |
+
depth_far: 3.0 # metres; clip implausibly-far depth
|
| 32 |
+
# --- train/val split (deterministic; disjoint) ---
|
| 33 |
+
val_fraction: 0.02 # fraction of samples held out for validation (0 = no val)
|
| 34 |
+
split_seed: 0 # fixed seed for the train/val partition
|
| 35 |
+
|
| 36 |
+
# --------------------------------------------------------------------------- #
|
| 37 |
+
# model -> MVSam3DFlow
|
| 38 |
+
# NOTE: max_views is DERIVED in train.py as
|
| 39 |
+
# (max_aux_views if random_views else num_aux_views) + 1
|
| 40 |
+
# SHAPE_RES (16) and SHAPE_CHANNELS (8) are locked by the SAM-3D SS VAE and
|
| 41 |
+
# remain code constants in flow_model.py.
|
| 42 |
+
# --------------------------------------------------------------------------- #
|
| 43 |
+
model:
|
| 44 |
+
model_channels: 1024
|
| 45 |
+
cond_channels: 1024
|
| 46 |
+
num_blocks: 24
|
| 47 |
+
num_heads: 16
|
| 48 |
+
sigma_min: 0.0 # rectified flow; 0 = straight OT path
|
| 49 |
+
time_scale: 1000.0 # flow time scaling fed to the backbone
|
| 50 |
+
# Timestep sampling for the flow loss. Default follows the ORIGINAL SAM-3D
|
| 51 |
+
# generator pretraining schedule (ss_generator.yaml training_time_sampler_fn =
|
| 52 |
+
# lognorm_sampler mean=-1, std=1). v2's t convention (t=0 noise -> t=1 data) is
|
| 53 |
+
# identical to the FlowMatching module's, so mean maps directly (no sign flip).
|
| 54 |
+
# Set timestep_sampler=uniform to reproduce the stage1_flow_lora harness.
|
| 55 |
+
timestep_sampler: logit_normal # logit_normal | uniform
|
| 56 |
+
timestep_lognorm_mean: -1.0
|
| 57 |
+
timestep_lognorm_std: 1.0
|
| 58 |
+
# Warm-start from SAM-3D's pretrained ss_generator. Loads BOTH the condition
|
| 59 |
+
# embedder (722 tensors -> frozen EmbedderFuser) AND the DiT torso (shape /
|
| 60 |
+
# 6drotation_normalized / translation streams + blocks + t_embedder). Set to
|
| 61 |
+
# null to train from scratch.
|
| 62 |
+
pretrained_ckpt: ${MIGRATOR_CHECKPOINTS}/hf/ss_generator.ckpt
|
| 63 |
+
# Standalone SAM-3D SS decoder (authoritative latent space) — used ONLY by
|
| 64 |
+
# validation to decode predicted shape latents to 64^3 occupancy for IoU.
|
| 65 |
+
ss_decoder_ckpt: ${MIGRATOR_CHECKPOINTS}/hf/ss_decoder.ckpt
|
| 66 |
+
|
| 67 |
+
# --------------------------------------------------------------------------- #
|
| 68 |
+
# condition -> MultiViewConditionEmbedder (thin multi-view wrapper around
|
| 69 |
+
# SAM-3D's real EmbedderFuser). Mirrors ss_generator.yaml's
|
| 70 |
+
# module.condition_embedder.backbone exactly: 3 embedders (2x Dino +
|
| 71 |
+
# PointPatchEmbed), learned pos-embedding, projection mult 4.0, dropout 0.1
|
| 72 |
+
# with the pointmap/rgb_pointmap drop weight, freeze: true.
|
| 73 |
+
# --------------------------------------------------------------------------- #
|
| 74 |
+
condition:
|
| 75 |
+
dino_model: dinov2_vitl14_reg
|
| 76 |
+
dino_input_size: 518
|
| 77 |
+
pointmap_input_size: 256
|
| 78 |
+
pointmap_patch_size: 8
|
| 79 |
+
pointmap_embed_dim: 512
|
| 80 |
+
projection_hidden_mult: 4.0 # = projection_net_hidden_dim_multiplier
|
| 81 |
+
dropout_prob: 0.1 # fuser modality dropout (pointmap/rgb_pointmap)
|
| 82 |
+
# Per-view camera embedding (mlp_cam): maps each view's relative aux->anchor
|
| 83 |
+
# extrinsics (rot6d + SSI-normalized camera centre, 9-D; anchor = identity) to
|
| 84 |
+
# model_channels and adds it to that view's tokens. Replaces the old
|
| 85 |
+
# content-free view_emb index table. Output layer is zero-init (no perturbation
|
| 86 |
+
# at step 0). This is the ONLY from-scratch module in the model.
|
| 87 |
+
cam_hidden_dim: 256 # hidden width of the 2-layer mlp_cam
|
| 88 |
+
# SAM-3D freezes its ENTIRE condition embedder (freeze: true). We mirror that:
|
| 89 |
+
# the whole EmbedderFuser (both DINOs, PointPatchEmbed, all projection_nets,
|
| 90 |
+
# idx_emb) is frozen after warm-start. The per-view camera embedding (mlp_cam,
|
| 91 |
+
# the extrinsics-derived module that replaced view_emb) is always trainable
|
| 92 |
+
# (no pretrained source). freeze: false would fine-tune the entire condition
|
| 93 |
+
# pathway.
|
| 94 |
+
freeze: true
|
| 95 |
+
freeze_dino: true # DINO backbone freeze at construction time
|
| 96 |
+
|
| 97 |
+
# --------------------------------------------------------------------------- #
|
| 98 |
+
# loss_weights -> MVSam3DFlow.loss_weights (per-stream v-prediction MSE weights)
|
| 99 |
+
# --------------------------------------------------------------------------- #
|
| 100 |
+
loss_weights:
|
| 101 |
+
# All 1.0 — matches the pretrained backbone's training regime (SAM-3D fork uses
|
| 102 |
+
# rot=trans=scale=1.0) so warm-started weights are fine-tuned under the same
|
| 103 |
+
# objective balance. Rotation has the largest raw loss (hardest stream), so 1.0
|
| 104 |
+
# gives it the gradient share it needs rather than down-weighting it.
|
| 105 |
+
shape: 1.0
|
| 106 |
+
6drotation_normalized: 1.0
|
| 107 |
+
translation: 1.0
|
| 108 |
+
scale: 1.0
|
| 109 |
+
|
| 110 |
+
# --------------------------------------------------------------------------- #
|
| 111 |
+
# optim + train loop
|
| 112 |
+
# --------------------------------------------------------------------------- #
|
| 113 |
+
optim:
|
| 114 |
+
lr: 1.0e-4
|
| 115 |
+
weight_decay: 0.0 # AdamW
|
| 116 |
+
grad_clip: 1.0 # max grad norm
|
| 117 |
+
|
| 118 |
+
train:
|
| 119 |
+
batch_size: 1 # per-GPU micro-batch
|
| 120 |
+
steps: 100000
|
| 121 |
+
num_workers: 8
|
| 122 |
+
# --- LR schedule (reused fork build_scheduler: warmup -> cosine -> min_lr) ---
|
| 123 |
+
scheduler: cosine # cosine | constant
|
| 124 |
+
warmup_steps: 1000 # linear warmup steps before cosine decay
|
| 125 |
+
min_lr: 1.0e-6 # cosine decay floor
|
| 126 |
+
# --- resume ---
|
| 127 |
+
resume_from: "" # path to a checkpoint to resume (empty = fresh start)
|
| 128 |
+
log_every: 20
|
| 129 |
+
ckpt_every: 2000
|
| 130 |
+
ckpt_dir: ${MIGRATOR_CACHE}/ckpt
|
| 131 |
+
amp: true # autocast on cuda (dtype below)
|
| 132 |
+
amp_dtype: bf16 # bf16 | fp16 (fp16 enables GradScaler)
|
| 133 |
+
seed: 0 # seeds torch/numpy/random at startup (offset by rank)
|
| 134 |
+
# --- distributed (DDP via torchrun; world_size=1 transparently skips it) ---
|
| 135 |
+
backend: ddp # ddp | deepspeed
|
| 136 |
+
deepspeed_config: "" # path to a DeepSpeed JSON (only used when backend=deepspeed)
|
| 137 |
+
grad_accum_steps: 1 # micro-steps per optimizer step
|
| 138 |
+
# --- validation (model's own Euler sampler + decode) ---
|
| 139 |
+
val_every: 2000 # run validation every N optimizer steps (0 = never)
|
| 140 |
+
val_num_steps: 50 # Euler ODE steps for sampling during validation
|
| 141 |
+
val_max_batches: 16 # val batches per rank per validation pass
|
migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated.yaml
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data:
|
| 2 |
+
dexycb_root: ${MIGRATOR_DATA_ROOT}/dexycb
|
| 3 |
+
canonical_cache: ${MIGRATOR_CACHE}/canonical
|
| 4 |
+
frame_stride: 5
|
| 5 |
+
anchor_sweep: true
|
| 6 |
+
subjects: null
|
| 7 |
+
image_size: 518
|
| 8 |
+
num_aux_views: 7
|
| 9 |
+
random_views: true
|
| 10 |
+
min_aux_views: 1
|
| 11 |
+
max_aux_views: 3
|
| 12 |
+
depth_scale: 1000.0
|
| 13 |
+
depth_near: 0.1
|
| 14 |
+
depth_far: 3.0
|
| 15 |
+
val_fraction: 0.02
|
| 16 |
+
split_seed: 0
|
| 17 |
+
heldout_manifest: ${MIGRATOR_MV_REPO}/configs/heldout.yaml
|
| 18 |
+
datasets:
|
| 19 |
+
- name: dexycb
|
| 20 |
+
adapter: dexycb
|
| 21 |
+
root: ${MIGRATOR_DATA_ROOT}/dexycb
|
| 22 |
+
role: train
|
| 23 |
+
weight: 1.0
|
| 24 |
+
- name: pace
|
| 25 |
+
adapter: pace
|
| 26 |
+
root: ${MIGRATOR_DATA_ROOT}/pace
|
| 27 |
+
role: train
|
| 28 |
+
weight: 3.0
|
| 29 |
+
data_split: test
|
| 30 |
+
- name: graspnet
|
| 31 |
+
adapter: graspnet
|
| 32 |
+
root: ${MIGRATOR_DATA_ROOT}/graspnet
|
| 33 |
+
role: train
|
| 34 |
+
weight: 2.0
|
| 35 |
+
- name: ycbv
|
| 36 |
+
adapter: ycbv
|
| 37 |
+
root: ${MIGRATOR_DATA_ROOT}/ycbv
|
| 38 |
+
role: train
|
| 39 |
+
weight: 1.0
|
| 40 |
+
data_split: test
|
| 41 |
+
# Adapters implemented + latent-complete + reprojection-gate >=0.8 (2026-07-17).
|
| 42 |
+
- name: hocap
|
| 43 |
+
adapter: hocap
|
| 44 |
+
root: ${MIGRATOR_DATA_ROOT}/hocap
|
| 45 |
+
role: train
|
| 46 |
+
weight: 1.0
|
| 47 |
+
pseudo_gt: true # FoundationPose+SDF pseudo-GT (train-only; never eval)
|
| 48 |
+
- name: h2o
|
| 49 |
+
adapter: h2o
|
| 50 |
+
root: ${MIGRATOR_DATA_ROOT}/h2o
|
| 51 |
+
role: train
|
| 52 |
+
weight: 0.5 # only 8 objects
|
| 53 |
+
- name: hograspnet
|
| 54 |
+
adapter: hograspnet
|
| 55 |
+
root: ${MIGRATOR_DATA_ROOT}/hograspnet
|
| 56 |
+
role: train
|
| 57 |
+
weight: 1.0
|
| 58 |
+
frame_stride: 15 # grasp-hold is quasi-static; keep enumeration tractable
|
| 59 |
+
- name: dexh2r
|
| 60 |
+
adapter: dexh2r
|
| 61 |
+
root: ${MIGRATOR_DATA_ROOT}/dexh2r
|
| 62 |
+
role: train
|
| 63 |
+
weight: 1.0
|
| 64 |
+
pseudo_gt: true # global-registration+ICP pseudo-GT (train-only); enumerates extracted subjects
|
| 65 |
+
- name: contactpose
|
| 66 |
+
adapter: contactpose
|
| 67 |
+
root: ${MIGRATOR_DATA_ROOT}/contactpose
|
| 68 |
+
role: train
|
| 69 |
+
weight: 0.5 # static grasps; only bowl present on disk so far
|
| 70 |
+
pseudo_gt: true
|
| 71 |
+
- name: housecat6d
|
| 72 |
+
adapter: housecat6d
|
| 73 |
+
root: ${MIGRATOR_DATA_ROOT}/housecat6d/extracted
|
| 74 |
+
role: heldout
|
| 75 |
+
data_split: test
|
| 76 |
+
model:
|
| 77 |
+
model_channels: 1024
|
| 78 |
+
cond_channels: 1024
|
| 79 |
+
num_blocks: 24
|
| 80 |
+
num_heads: 16
|
| 81 |
+
sigma_min: 0.0
|
| 82 |
+
time_scale: 1000.0
|
| 83 |
+
timestep_sampler: logit_normal
|
| 84 |
+
timestep_lognorm_mean: -1.0
|
| 85 |
+
timestep_lognorm_std: 1.0
|
| 86 |
+
pretrained_ckpt: ${MIGRATOR_CHECKPOINTS}/hf/ss_generator.ckpt
|
| 87 |
+
ss_decoder_ckpt: ${MIGRATOR_CHECKPOINTS}/hf/ss_decoder.ckpt
|
| 88 |
+
condition:
|
| 89 |
+
dino_model: dinov2_vitl14_reg
|
| 90 |
+
dino_input_size: 518
|
| 91 |
+
pointmap_input_size: 256
|
| 92 |
+
pointmap_patch_size: 8
|
| 93 |
+
pointmap_embed_dim: 512
|
| 94 |
+
projection_hidden_mult: 4.0
|
| 95 |
+
dropout_prob: 0.1
|
| 96 |
+
cam_hidden_dim: 256
|
| 97 |
+
freeze: true
|
| 98 |
+
freeze_dino: true
|
| 99 |
+
loss_weights:
|
| 100 |
+
shape: 1.0
|
| 101 |
+
6drotation_normalized: 1.0
|
| 102 |
+
translation: 1.0
|
| 103 |
+
scale: 1.0
|
| 104 |
+
optim:
|
| 105 |
+
lr: 0.0001
|
| 106 |
+
weight_decay: 0.0
|
| 107 |
+
grad_clip: 1.0
|
| 108 |
+
train:
|
| 109 |
+
batch_size: 1
|
| 110 |
+
steps: 100000
|
| 111 |
+
num_workers: 8
|
| 112 |
+
scheduler: cosine
|
| 113 |
+
warmup_steps: 1000
|
| 114 |
+
min_lr: 1.0e-06
|
| 115 |
+
resume_from: ''
|
| 116 |
+
log_every: 20
|
| 117 |
+
ckpt_every: 2000
|
| 118 |
+
ckpt_dir: ${MIGRATOR_CACHE}/ckpt
|
| 119 |
+
amp: true
|
| 120 |
+
amp_dtype: bf16
|
| 121 |
+
seed: 0
|
| 122 |
+
backend: ddp
|
| 123 |
+
deepspeed_config: ''
|
| 124 |
+
grad_accum_steps: 1
|
| 125 |
+
val_every: 2000
|
| 126 |
+
val_num_steps: 50
|
| 127 |
+
val_max_batches: 16
|
migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated_object_ssi.yaml
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data:
|
| 2 |
+
ssi_mode: object_centric # SSI ablation
|
| 3 |
+
dexycb_root: ${MIGRATOR_DATA_ROOT}/dexycb
|
| 4 |
+
canonical_cache: ${MIGRATOR_CACHE}/canonical
|
| 5 |
+
frame_stride: 5
|
| 6 |
+
anchor_sweep: true
|
| 7 |
+
subjects: null
|
| 8 |
+
image_size: 518
|
| 9 |
+
num_aux_views: 7
|
| 10 |
+
random_views: true
|
| 11 |
+
min_aux_views: 1
|
| 12 |
+
max_aux_views: 3
|
| 13 |
+
depth_scale: 1000.0
|
| 14 |
+
depth_near: 0.1
|
| 15 |
+
depth_far: 3.0
|
| 16 |
+
val_fraction: 0.02
|
| 17 |
+
split_seed: 0
|
| 18 |
+
heldout_manifest: ${MIGRATOR_MV_REPO}/configs/heldout.yaml
|
| 19 |
+
datasets:
|
| 20 |
+
- name: dexycb
|
| 21 |
+
adapter: dexycb
|
| 22 |
+
root: ${MIGRATOR_DATA_ROOT}/dexycb
|
| 23 |
+
role: train
|
| 24 |
+
weight: 1.0
|
| 25 |
+
- name: pace
|
| 26 |
+
adapter: pace
|
| 27 |
+
root: ${MIGRATOR_DATA_ROOT}/pace
|
| 28 |
+
role: train
|
| 29 |
+
weight: 3.0
|
| 30 |
+
data_split: test
|
| 31 |
+
- name: graspnet
|
| 32 |
+
adapter: graspnet
|
| 33 |
+
root: ${MIGRATOR_DATA_ROOT}/graspnet
|
| 34 |
+
role: train
|
| 35 |
+
weight: 2.0
|
| 36 |
+
- name: ycbv
|
| 37 |
+
adapter: ycbv
|
| 38 |
+
root: ${MIGRATOR_DATA_ROOT}/ycbv
|
| 39 |
+
role: train
|
| 40 |
+
weight: 1.0
|
| 41 |
+
data_split: test
|
| 42 |
+
# Adapters implemented + latent-complete + reprojection-gate >=0.8 (2026-07-17).
|
| 43 |
+
- name: hocap
|
| 44 |
+
adapter: hocap
|
| 45 |
+
root: ${MIGRATOR_DATA_ROOT}/hocap
|
| 46 |
+
role: train
|
| 47 |
+
weight: 1.0
|
| 48 |
+
pseudo_gt: true # FoundationPose+SDF pseudo-GT (train-only; never eval)
|
| 49 |
+
- name: h2o
|
| 50 |
+
adapter: h2o
|
| 51 |
+
root: ${MIGRATOR_DATA_ROOT}/h2o
|
| 52 |
+
role: train
|
| 53 |
+
weight: 0.5 # only 8 objects
|
| 54 |
+
- name: hograspnet
|
| 55 |
+
adapter: hograspnet
|
| 56 |
+
root: ${MIGRATOR_DATA_ROOT}/hograspnet
|
| 57 |
+
role: train
|
| 58 |
+
weight: 1.0
|
| 59 |
+
frame_stride: 15 # grasp-hold is quasi-static; keep enumeration tractable
|
| 60 |
+
- name: dexh2r
|
| 61 |
+
adapter: dexh2r
|
| 62 |
+
root: ${MIGRATOR_DATA_ROOT}/dexh2r
|
| 63 |
+
role: train
|
| 64 |
+
weight: 1.0
|
| 65 |
+
pseudo_gt: true # global-registration+ICP pseudo-GT (train-only); enumerates extracted subjects
|
| 66 |
+
- name: contactpose
|
| 67 |
+
adapter: contactpose
|
| 68 |
+
root: ${MIGRATOR_DATA_ROOT}/contactpose
|
| 69 |
+
role: train
|
| 70 |
+
weight: 0.5 # static grasps; only bowl present on disk so far
|
| 71 |
+
pseudo_gt: true
|
| 72 |
+
- name: housecat6d
|
| 73 |
+
adapter: housecat6d
|
| 74 |
+
root: ${MIGRATOR_DATA_ROOT}/housecat6d/extracted
|
| 75 |
+
role: heldout
|
| 76 |
+
data_split: test
|
| 77 |
+
model:
|
| 78 |
+
model_channels: 1024
|
| 79 |
+
cond_channels: 1024
|
| 80 |
+
num_blocks: 24
|
| 81 |
+
num_heads: 16
|
| 82 |
+
sigma_min: 0.0
|
| 83 |
+
time_scale: 1000.0
|
| 84 |
+
timestep_sampler: logit_normal
|
| 85 |
+
timestep_lognorm_mean: -1.0
|
| 86 |
+
timestep_lognorm_std: 1.0
|
| 87 |
+
pretrained_ckpt: ${MIGRATOR_CHECKPOINTS}/hf/ss_generator.ckpt
|
| 88 |
+
ss_decoder_ckpt: ${MIGRATOR_CHECKPOINTS}/hf/ss_decoder.ckpt
|
| 89 |
+
condition:
|
| 90 |
+
dino_model: dinov2_vitl14_reg
|
| 91 |
+
dino_input_size: 518
|
| 92 |
+
pointmap_input_size: 256
|
| 93 |
+
pointmap_patch_size: 8
|
| 94 |
+
pointmap_embed_dim: 512
|
| 95 |
+
projection_hidden_mult: 4.0
|
| 96 |
+
dropout_prob: 0.1
|
| 97 |
+
cam_hidden_dim: 256
|
| 98 |
+
freeze: true
|
| 99 |
+
freeze_dino: true
|
| 100 |
+
loss_weights:
|
| 101 |
+
shape: 1.0
|
| 102 |
+
6drotation_normalized: 1.0
|
| 103 |
+
translation: 1.0
|
| 104 |
+
scale: 1.0
|
| 105 |
+
optim:
|
| 106 |
+
lr: 0.0001
|
| 107 |
+
weight_decay: 0.0
|
| 108 |
+
grad_clip: 1.0
|
| 109 |
+
train:
|
| 110 |
+
batch_size: 1
|
| 111 |
+
steps: 100000
|
| 112 |
+
num_workers: 8
|
| 113 |
+
scheduler: cosine
|
| 114 |
+
warmup_steps: 1000
|
| 115 |
+
min_lr: 1.0e-06
|
| 116 |
+
resume_from: ''
|
| 117 |
+
log_every: 20
|
| 118 |
+
ckpt_every: 2000
|
| 119 |
+
ckpt_dir: ${MIGRATOR_CACHE}/ckpt_object_ssi
|
| 120 |
+
amp: true
|
| 121 |
+
amp_dtype: bf16
|
| 122 |
+
seed: 0
|
| 123 |
+
backend: ddp
|
| 124 |
+
deepspeed_config: ''
|
| 125 |
+
grad_accum_steps: 1
|
| 126 |
+
val_every: 2000
|
| 127 |
+
val_num_steps: 50
|
| 128 |
+
val_max_batches: 16
|
migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated_scene_ssi.yaml
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data:
|
| 2 |
+
ssi_mode: scene # SSI ablation
|
| 3 |
+
dexycb_root: ${MIGRATOR_DATA_ROOT}/dexycb
|
| 4 |
+
canonical_cache: ${MIGRATOR_CACHE}/canonical
|
| 5 |
+
frame_stride: 5
|
| 6 |
+
anchor_sweep: true
|
| 7 |
+
subjects: null
|
| 8 |
+
image_size: 518
|
| 9 |
+
num_aux_views: 7
|
| 10 |
+
random_views: true
|
| 11 |
+
min_aux_views: 1
|
| 12 |
+
max_aux_views: 3
|
| 13 |
+
depth_scale: 1000.0
|
| 14 |
+
depth_near: 0.1
|
| 15 |
+
depth_far: 3.0
|
| 16 |
+
val_fraction: 0.02
|
| 17 |
+
split_seed: 0
|
| 18 |
+
heldout_manifest: ${MIGRATOR_MV_REPO}/configs/heldout.yaml
|
| 19 |
+
datasets:
|
| 20 |
+
- name: dexycb
|
| 21 |
+
adapter: dexycb
|
| 22 |
+
root: ${MIGRATOR_DATA_ROOT}/dexycb
|
| 23 |
+
role: train
|
| 24 |
+
weight: 1.0
|
| 25 |
+
- name: pace
|
| 26 |
+
adapter: pace
|
| 27 |
+
root: ${MIGRATOR_DATA_ROOT}/pace
|
| 28 |
+
role: train
|
| 29 |
+
weight: 3.0
|
| 30 |
+
data_split: test
|
| 31 |
+
- name: graspnet
|
| 32 |
+
adapter: graspnet
|
| 33 |
+
root: ${MIGRATOR_DATA_ROOT}/graspnet
|
| 34 |
+
role: train
|
| 35 |
+
weight: 2.0
|
| 36 |
+
- name: ycbv
|
| 37 |
+
adapter: ycbv
|
| 38 |
+
root: ${MIGRATOR_DATA_ROOT}/ycbv
|
| 39 |
+
role: train
|
| 40 |
+
weight: 1.0
|
| 41 |
+
data_split: test
|
| 42 |
+
# Adapters implemented + latent-complete + reprojection-gate >=0.8 (2026-07-17).
|
| 43 |
+
- name: hocap
|
| 44 |
+
adapter: hocap
|
| 45 |
+
root: ${MIGRATOR_DATA_ROOT}/hocap
|
| 46 |
+
role: train
|
| 47 |
+
weight: 1.0
|
| 48 |
+
pseudo_gt: true # FoundationPose+SDF pseudo-GT (train-only; never eval)
|
| 49 |
+
- name: h2o
|
| 50 |
+
adapter: h2o
|
| 51 |
+
root: ${MIGRATOR_DATA_ROOT}/h2o
|
| 52 |
+
role: train
|
| 53 |
+
weight: 0.5 # only 8 objects
|
| 54 |
+
- name: hograspnet
|
| 55 |
+
adapter: hograspnet
|
| 56 |
+
root: ${MIGRATOR_DATA_ROOT}/hograspnet
|
| 57 |
+
role: train
|
| 58 |
+
weight: 1.0
|
| 59 |
+
frame_stride: 15 # grasp-hold is quasi-static; keep enumeration tractable
|
| 60 |
+
- name: dexh2r
|
| 61 |
+
adapter: dexh2r
|
| 62 |
+
root: ${MIGRATOR_DATA_ROOT}/dexh2r
|
| 63 |
+
role: train
|
| 64 |
+
weight: 1.0
|
| 65 |
+
pseudo_gt: true # global-registration+ICP pseudo-GT (train-only); enumerates extracted subjects
|
| 66 |
+
- name: contactpose
|
| 67 |
+
adapter: contactpose
|
| 68 |
+
root: ${MIGRATOR_DATA_ROOT}/contactpose
|
| 69 |
+
role: train
|
| 70 |
+
weight: 0.5 # static grasps; only bowl present on disk so far
|
| 71 |
+
pseudo_gt: true
|
| 72 |
+
- name: housecat6d
|
| 73 |
+
adapter: housecat6d
|
| 74 |
+
root: ${MIGRATOR_DATA_ROOT}/housecat6d/extracted
|
| 75 |
+
role: heldout
|
| 76 |
+
data_split: test
|
| 77 |
+
model:
|
| 78 |
+
model_channels: 1024
|
| 79 |
+
cond_channels: 1024
|
| 80 |
+
num_blocks: 24
|
| 81 |
+
num_heads: 16
|
| 82 |
+
sigma_min: 0.0
|
| 83 |
+
time_scale: 1000.0
|
| 84 |
+
timestep_sampler: logit_normal
|
| 85 |
+
timestep_lognorm_mean: -1.0
|
| 86 |
+
timestep_lognorm_std: 1.0
|
| 87 |
+
pretrained_ckpt: ${MIGRATOR_CHECKPOINTS}/hf/ss_generator.ckpt
|
| 88 |
+
ss_decoder_ckpt: ${MIGRATOR_CHECKPOINTS}/hf/ss_decoder.ckpt
|
| 89 |
+
condition:
|
| 90 |
+
dino_model: dinov2_vitl14_reg
|
| 91 |
+
dino_input_size: 518
|
| 92 |
+
pointmap_input_size: 256
|
| 93 |
+
pointmap_patch_size: 8
|
| 94 |
+
pointmap_embed_dim: 512
|
| 95 |
+
projection_hidden_mult: 4.0
|
| 96 |
+
dropout_prob: 0.1
|
| 97 |
+
cam_hidden_dim: 256
|
| 98 |
+
freeze: true
|
| 99 |
+
freeze_dino: true
|
| 100 |
+
loss_weights:
|
| 101 |
+
shape: 1.0
|
| 102 |
+
6drotation_normalized: 1.0
|
| 103 |
+
translation: 1.0
|
| 104 |
+
scale: 1.0
|
| 105 |
+
optim:
|
| 106 |
+
lr: 0.0001
|
| 107 |
+
weight_decay: 0.0
|
| 108 |
+
grad_clip: 1.0
|
| 109 |
+
train:
|
| 110 |
+
batch_size: 1
|
| 111 |
+
steps: 100000
|
| 112 |
+
num_workers: 8
|
| 113 |
+
scheduler: cosine
|
| 114 |
+
warmup_steps: 1000
|
| 115 |
+
min_lr: 1.0e-06
|
| 116 |
+
resume_from: ''
|
| 117 |
+
log_every: 20
|
| 118 |
+
ckpt_every: 2000
|
| 119 |
+
ckpt_dir: ${MIGRATOR_CACHE}/ckpt_scene_ssi
|
| 120 |
+
amp: true
|
| 121 |
+
amp_dtype: bf16
|
| 122 |
+
seed: 0
|
| 123 |
+
backend: ddp
|
| 124 |
+
deepspeed_config: ''
|
| 125 |
+
grad_accum_steps: 1
|
| 126 |
+
val_every: 2000
|
| 127 |
+
val_num_steps: 50
|
| 128 |
+
val_max_batches: 16
|
migrator/code/mv-sam3d-for-6d-v2/docs/TRAINING_FEATURES.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mv-sam3d-for-6d-v2 — Training Features
|
| 2 |
+
|
| 3 |
+
> **Scope.** This will become the single doc that explains every training feature
|
| 4 |
+
> of the multi-view 6D-pose flow model. For now it documents **only the camera
|
| 5 |
+
> embedding (`mlp_cam`)** — the other features (joint flow objective, SSI pose
|
| 6 |
+
> targets, DDP/cosine/resume, held-out validation loss, modality dropout, …) will
|
| 7 |
+
> be added as further sections later. Everything below reflects the code as
|
| 8 |
+
> implemented (verified job 4526); paths are absolute.
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## 1. Camera embedding (`mlp_cam`)
|
| 13 |
+
|
| 14 |
+
### One-line summary
|
| 15 |
+
Each view's tokens are tagged with a learned embedding **derived from that view's
|
| 16 |
+
camera pose** (relative aux→anchor extrinsics), instead of a content-free
|
| 17 |
+
per-slot index. This injects inter-camera geometry into the conditioning so the
|
| 18 |
+
appearance tokens (RGB/mask) know which camera they came from.
|
| 19 |
+
|
| 20 |
+
### The gap it closes
|
| 21 |
+
The conditioning is multi-view: one **anchor** camera defines the reference frame,
|
| 22 |
+
the others are **aux** views. Before this feature, the only thing carrying
|
| 23 |
+
relative camera geometry into the conditions was the anchor-frame **pointmap** —
|
| 24 |
+
RGB and mask tokens carried no geometry, and the old per-view tag (`view_emb`) was
|
| 25 |
+
a learned `[max_views, C]` table indexed by **slot**, which was:
|
| 26 |
+
- **content-free** — it encoded only "which slot," not where the camera is;
|
| 27 |
+
- **order-arbitrary** — with random view sampling, "aux slot 1" is a *different
|
| 28 |
+
physical camera* every sample, so the table could never learn a stable
|
| 29 |
+
per-camera signal.
|
| 30 |
+
|
| 31 |
+
`mlp_cam` replaces that table with a per-view bias computed from the actual
|
| 32 |
+
extrinsics, so the camera frame is bound to **every** token of the view —
|
| 33 |
+
including the otherwise geometry-free RGB/mask tokens — and is consistent across
|
| 34 |
+
the random aux subset.
|
| 35 |
+
|
| 36 |
+
### Why this form (design decision)
|
| 37 |
+
Three options were considered for injecting camera geometry; we chose the first:
|
| 38 |
+
|
| 39 |
+
1. **Additive per-view embedding (chosen).** `mlp_cam(extrinsics_v)` → one vector
|
| 40 |
+
added to all of view *v*'s tokens. It **binds** the camera pose to every
|
| 41 |
+
appearance token automatically (no ambiguity about which tokens belong to which
|
| 42 |
+
camera). Cheap, no token-count growth, condition-side only.
|
| 43 |
+
2. **Concatenated camera token (rejected).** A dedicated camera token per view in
|
| 44 |
+
the sequence has full capacity but *floats* — nothing links it to that view's
|
| 45 |
+
appearance tokens, so the model has to learn the association.
|
| 46 |
+
3. **Per-pixel ray positional encoding (rejected for this setting).** Plücker rays
|
| 47 |
+
through the embedder give per-token geometry, but it is **largely redundant**
|
| 48 |
+
with the anchor-frame pointmap (which already carries per-pixel 3D where depth
|
| 49 |
+
is valid) and cannot cleanly reach the frozen-DINO RGB/mask tokens. It would be
|
| 50 |
+
the right choice only in a depth-sparse / monocular regime.
|
| 51 |
+
|
| 52 |
+
> Note: adopting `mlp_cam` deliberately **reverses** the earlier "no new modules"
|
| 53 |
+
> rule. It is now the *only* from-scratch module in the model.
|
| 54 |
+
|
| 55 |
+
### What exactly is added
|
| 56 |
+
For each view, a **9-D camera feature** is built from the relative transform that
|
| 57 |
+
maps that camera's points into the anchor PyTorch3D frame
|
| 58 |
+
(`X_anchor = Rrel · X_aux + trel`):
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
cam_feat (9) = [ rot6d(Rrel) | (trel − shift) / scale ]
|
| 62 |
+
└── 6 ──┘ └──────── 3 ─────────┘
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
- **`rot6d(Rrel)` (6)** — column-convention 6D of the aux→anchor rotation
|
| 66 |
+
(`matrix_to_rot6d`, the same convention as the object-rotation target). Raw 6D
|
| 67 |
+
columns are already unit-scaled, so **no** mean/std normalization is applied
|
| 68 |
+
here (that normalization is tuned to the *object* rotation distribution, not the
|
| 69 |
+
camera one).
|
| 70 |
+
- **`(trel − shift)/scale` (3)** — the aux camera **centre** in the anchor frame
|
| 71 |
+
(which is exactly `trel`, the image of the aux origin), expressed in the **same
|
| 72 |
+
SSI space as the conditioning pointmaps**. So the camera position lives in the
|
| 73 |
+
identical normalized coordinate system as the 3D points it observes.
|
| 74 |
+
- **Anchor view**: passes `Rrel = I`, `trel = 0`, computed by the *same* path — its
|
| 75 |
+
rot6d is the identity `[1,0,0,0,1,0]` and its centre normalizes to
|
| 76 |
+
`−shift/scale`. No special-casing.
|
| 77 |
+
|
| 78 |
+
There is **no scale component in the transform itself** (camera↔camera is rigid
|
| 79 |
+
SE(3)); the SSI `scale`/`shift` appear only to normalize the camera *position*
|
| 80 |
+
into pointmap-consistent units — they are not a camera scale and not the object
|
| 81 |
+
scale `s`. The rotation never sees them.
|
| 82 |
+
|
| 83 |
+
### Where it is computed and wired
|
| 84 |
+
| Stage | Location | What happens |
|
| 85 |
+
|---|---|---|
|
| 86 |
+
| build feature | `mvsam3d/data/dexycb_dataset.py` → `_cam_feature(Rrel, trel, shift, scale)` (`CAM_FEAT_DIM = 9`) | packs the 9-D vector; attached to every view dict as `view["cam"]` (anchor uses `I, 0`) |
|
| 87 |
+
| batch | `mvsam3d/data/dexycb_dataset.py` → `collate_multiview` | `cam` stacks per view position to `[B, 9]` with no special handling |
|
| 88 |
+
| consume | `mvsam3d/model/condition.py` → `MultiViewConditionEmbedder.forward` | per view: `tok = tok + self.mlp_cam(view["cam"])[:, None, :]` (broadcast over all tokens of the view) |
|
| 89 |
+
|
| 90 |
+
### The module
|
| 91 |
+
- `self.mlp_cam = Sequential(Linear(9, cam_hidden_dim), SiLU, Linear(cam_hidden_dim, model_channels))`.
|
| 92 |
+
- **Output layer is zero-initialized** (weight & bias). At step 0 the per-view bias
|
| 93 |
+
is exactly **0**, so the conditioning equals the no-tag baseline and the
|
| 94 |
+
warm-started fuser/backbone are perturbed only as `mlp_cam` learns — i.e.
|
| 95 |
+
resuming/continuing from a prior run sees no init shock.
|
| 96 |
+
- **Trainable.** The whole `EmbedderFuser` (both DINOs, PointPatchEmbed,
|
| 97 |
+
projection nets, `idx_emb`) stays **frozen** after warm-start; `mlp_cam` is the
|
| 98 |
+
only trainable condition-side module (and the only from-scratch module in the
|
| 99 |
+
whole model).
|
| 100 |
+
|
| 101 |
+
### Config
|
| 102 |
+
- `condition.cam_hidden_dim` (default `256`) — hidden width of the 2-layer MLP.
|
| 103 |
+
- `model.max_views` is now **API-only**: `mlp_cam` derives the per-view bias from
|
| 104 |
+
the extrinsics, so there is no per-slot table to size and **no upper bound on the
|
| 105 |
+
view count**.
|
| 106 |
+
|
| 107 |
+
### Properties / invariants
|
| 108 |
+
- **Camera-consistent under random views**: the same physical camera yields the
|
| 109 |
+
same embedding regardless of which random slot it lands in.
|
| 110 |
+
- **Unbounded view count**: any number of cameras works (no table to overflow).
|
| 111 |
+
- **Warm-start neutral at step 0**: zero-init output ⇒ identical to the no-camera
|
| 112 |
+
baseline initially.
|
| 113 |
+
- **Intrinsics not included** (extrinsics only). Appending normalized
|
| 114 |
+
focal/principal-point would extend the feature to 13-D; deferred as minor for a
|
| 115 |
+
fixed-rig, pointmap-present DexYCB setup.
|
| 116 |
+
|
| 117 |
+
### Verification
|
| 118 |
+
Job 4526 (B200): warm-start intact (backbone 936/936, fuser 722/722 frozen),
|
| 119 |
+
`condition trainable modules == ['mlp_cam']` (265,728 params), grad audit
|
| 120 |
+
`fuser=0 / mlp_cam=6.5e3 / backbone=1.75e5`, finite loss, train + validation
|
| 121 |
+
smoke clean. CPU smoke confirmed anchor `rot6d=[1,0,0,0,1,0]`,
|
| 122 |
+
`pos_ssi=[0,0,−median_z/scale]`, and zero-init forward = exactly 0.
|
migrator/code/mv-sam3d-for-6d-v2/docs/dataset_convention_spec.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dataset Coordinate-Convention Spec (verified 2026-07-16/17)
|
| 2 |
+
|
| 3 |
+
TARGET (what every adapter must output, per view, per object):
|
| 4 |
+
- pose (R_oc, t_oc): `x_cam = R_oc @ p_cad + t_oc`, OpenCV axes (x-right, y-down, z-forward), t METERS, p_cad = mesh verts (m) in mesh frame.
|
| 5 |
+
- K OpenCV pinhole (fx,fy,cx,cy); depth meters registered to RGB; extrinsics world<->cam OpenCV meters; mesh meters in pose frame.
|
| 6 |
+
The base dataloader then applies D=diag(-1,-1,1) (OpenCV->PyTorch3D) and canonical M. Adapters MUST NOT pre-apply D or M.
|
| 7 |
+
|
| 8 |
+
Legend: ✅=empirical reprojection IoU, 🔵=code/cross-check verified.
|
| 9 |
+
|
| 10 |
+
## YCB-V 🔵 (train; YCB objs) — LOCAL(models); need BOP train_real
|
| 11 |
+
R_oc=cam_R_m2c.reshape(3,3); t_oc=cam_t_m2c*1e-3; mesh=models_eval/obj_{id:06d}.ply /1000. depth=raw*depth_scale/1000. K=cam_K. extr cam_R/t_w2c (mm->m) world->cam. No flip (native OpenCV). Masks mask_visib/.
|
| 12 |
+
|
| 13 |
+
## HO-3D 🔵 (HELD-OUT, never train; YCB objs) — LOCAL
|
| 14 |
+
R=Rodrigues(meta['objRot']); t=meta['objTrans'](m); then R=diag(1,-1,-1)@R, t=diag(1,-1,-1)@t (OpenGL->OpenCV, applied to BOTH). mesh dexycb textured_simple.obj (m). depth=(R_ch+G_ch*256)*1.2499e-4. K=meta['camMat']. Eval-split masks rendered.
|
| 15 |
+
|
| 16 |
+
## HOPE-Video 🔵 (HELD-OUT bonus OOD; non-YCB) — LOCAL
|
| 17 |
+
R=P[:3,:3]; t=P[:3,3]*1e-2 (cm->m). mesh meshes/eval/{class}.obj /100. depth raw*1e-3. extrinsics raw-as-meters world->cam (do NOT scale). K=camera.intrinsics. Masks precomputed masks_visib/.
|
| 18 |
+
|
| 19 |
+
## YCB-M 🔵 (HELD-OUT; YCB objs) — LOCAL
|
| 20 |
+
R=quat_xyzw->R (F=I, no flip); t=location*1e-2 (cm->m), object in CAMERA frame. mesh models_aligned/ycb_models_aligned_cm /100 (NOT dexycb). depth raw/10000. camera_data=world->cam (invert->cam->world). ⚠️ rotation handedness taken as-is, calib validated translation only — render-verify rotation before any training use.
|
| 21 |
+
|
| 22 |
+
## HouseCat6D ✅ IoU 0.854 (HELD-OUT #1 benchmark; 194 non-YCB) — LOCAL(sqsh)
|
| 23 |
+
R_oc,t_oc = label pkl rotations[i],translations[i] — ALREADY obj->cam OpenCV meters (identity conversion). mesh obj_models_small_size_final/<cat>/<model>.obj (m, origin-centered; cat=token before '-'). depth /1000. K=intrinsics.txt. extr camera_pose/NNNNNN.txt is CAM->WORLD (invert for world->cam); train/val have NO extrinsics (use pkl directly). ⚠️ masks: bg=255, use `mask!=255` (NOT >0). Index by model_list not class_ids. Do NOT rescale by scales/gt_scales.
|
| 24 |
+
|
| 25 |
+
## GraspNet-1B ✅ IoU 0.981 (train + test_novel HELD-OUT; ~56 non-YCB of 88) — SAMPLE(scene_0017)
|
| 26 |
+
R_oc=meta['poses'][:3,:3,i]; t_oc=meta['poses'][:3,3,i] (m). mesh models/%03d/nontextured.ply (m). ⚠️ OBJECT-ID OFF-BY-ONE: label/cls_indexes value V -> mesh models/(V-1). depth raw/1000. K=camK.npy (Kinect≠RealSense, read per-scene). extr per-view cam->table=cam0_wrt_table@camera_poses[v] (not needed; per-view poses already in cam frame). CC-BY-NC.
|
| 27 |
+
|
| 28 |
+
## PACE ✅ IoU 0.90-0.97 (train; 238 non-YCB) — SAMPLE(scene 000215)
|
| 29 |
+
BOP: R_oc=cam_R_m2c.reshape(3,3); t_oc=cam_t_m2c/1000; mesh models_eval/obj_XXXXXX.ply /1000. depth raw*depth_scale/1000 (real depth_scale=1.0; PBR /10000). K=cam_K. ⚠️ real split has NO cam_R/t_w2c (3-cam rig = separate scene folders; per-view poses self-contained). ⚠️ FILTER ARTICULATED: drop obj_id 545..692 (AKB-48); keep 0..544 rigid. masks {imgid:06d}_{gtidx:06d}.png (gtidx into scene_gt).
|
| 30 |
+
|
| 31 |
+
## HO-Cap ✅ IoU 0.77-0.87 (TRAIN-ONLY pseudo-GT; 64 non-YCB) — SAMPLE(subject_1)
|
| 32 |
+
poses_o.npy [n_obj,n_frame,7]=[qx,qy,qz,qw,tx,ty,tz] scipy xyzw, obj->WORLD (world=tag_1). Per-cam: T_oc(s)=rs_RTs_inv[s]@quat_to_mat(poses_o[o,f]); OR read label_<frame>.npz['obj_poses'] (already per-cam obj->cam, verified 1e-7). rs_RTs[s]=inv(tag_1)@extr2master[s]; rs_RTs_inv=inv. mesh models/<id>/textured_mesh.obj (m, matches poses). depth /1000 with COLOR K. K=color intrinsics. ⚠️ seg_mask ids LOCAL 1..4 objects/5 rh/6 lh. poses/labels in separate zips.
|
| 33 |
+
|
| 34 |
+
## DexH2R 🔵 round-trip 2.4e-16 (TRAIN-ONLY pseudo-GT, 6 of 18 views; 56 non-YCB) — MESHES+CALIB only (frames quota-blocked)
|
| 35 |
+
obj_pose.pt [T,4,4]=T_world<-obj (world=hand_arm base). Kinect i: T_oc=inv(cali0i)@hand_arm_mesh_to_kinect_pcd_0@obj_pose[t]. RealSense i(0,1): time-varying via ShadowHand FK(qpos.pt)@inv(realsense_to_forearm_i). mesh object_model/<name>.obj (m). depth int16->uint16 /1000. K per-cam (Kinect 8-param distort; images undistorted). ⚠️ ONLY kinect0-3+realsense0-1 have depth (drop 12 ZCAM). No 2D masks (project real_obj_pcd or threshold depth). Use per-trial calibration/.
|
| 36 |
+
|
| 37 |
+
## H2O 🔵 code-verified (train; 8 non-YCB) — NO DATA (registration wall)
|
| 38 |
+
obj_pose_RT/{f}.txt = class_id + 4x4 obj->cam OpenCV meters, USED AS-IS (⚠️ NO inversion — the "cam_to_obj" note refers to a DIFFERENT file, the keypoint obj_pose/). mesh object/{class}.ply (m). depth /1000. cam_pose/ = cam->world (invert for world->cam). K from cam_intrinsics.txt (fx fy cx cy w h); images undistorted. Per-camera RT already in that cam's frame. Masks must be rendered. Mesh folder names ≠ display names.
|
| 39 |
+
|
| 40 |
+
## HOGraspNet 🔵 code-verified (train; 22 YCB+8, use scanned meshes; mocap GT) — MESHES only (annotations gated)
|
| 41 |
+
object_mat = 4x4 obj->WORLD (world=mas cam). extrinsic = (3,4) world->cam. T_oc=M4(extrinsic)@object_mat. R_oc=T_oc[:3,:3]; t_oc=T_oc[:3,3]/100 (cm->m). mesh obj_scanned_models/<name>.obj: p_cad=(S*V_raw)/100 where S=_OBJECT_SCALE_FIXED[idx-1] (⚠️ MANDATORY per-obj scale; wine_glass .830, small_marker .1035, spatula .671, golf_ball .430, else 1.0). depth /1000. K OpenCV (full-res 1920x1080; cropped 640x480 -> shift cx,cy by bbox). ⚠️ use scanned meshes NOT dexycb CAD (different frame). obj idx = HOGraspNet 1..30 ordering. masks pseudo.
|
| 42 |
+
|
| 43 |
+
## ContactPose ✅ IoU 0.979 (TRAIN-ONLY static; 25 non-YCB) — SAMPLE(p28/use/bowl)
|
| 44 |
+
cTo=object_pose(cam,frame)=obj->cam OpenCV meters (R_oc=cTo[:3,:3],t_oc=cTo[:3,3]). mesh object_models/<obj>.ply *1e-3 (PLY is mm). depth /1000 (Kinect v2, ~30mm systematic bias). ⚠️ projection needs affine A: pixels = A@K@x_cam (cp.P); left/right cams are PORTRAIT 540x960 while K is 960x540 landscape. extr wTc=cam->world (invert->cTw). ⚠️ static: sample 1-few frames per (subject,obj,intent) — 2306 grasps not 2.9M frames. Masks render from mesh+cTo. quats w-first if parsing raw.
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""mvsam3d: multi-view 6D-pose flow model (migrator package).
|
| 2 |
+
|
| 3 |
+
Importing the package registers the migrator path roots (MIGRATOR_ROOT,
|
| 4 |
+
MIGRATOR_DATA_ROOT, MIGRATOR_CHECKPOINTS, MIGRATOR_CACHE, MIGRATOR_MV_REPO) into
|
| 5 |
+
os.environ so that both YAML configs and helper scripts can reference paths via
|
| 6 |
+
``${MIGRATOR_*}`` placeholders regardless of where the package is installed.
|
| 7 |
+
User-set values are never overridden (setdefault)."""
|
| 8 |
+
from . import paths as paths # noqa: F401
|
| 9 |
+
paths.register_env()
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (749 Bytes). View file
|
|
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__pycache__/paths.cpython-311.pyc
ADDED
|
Binary file (6.59 kB). View file
|
|
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__init__.py
ADDED
|
File without changes
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (199 Bytes). View file
|
|
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/base_dataset.cpython-311.pyc
ADDED
|
Binary file (16.8 kB). View file
|
|
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/canonical.cpython-311.pyc
ADDED
|
Binary file (4.41 kB). View file
|
|
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/dexycb_calib.cpython-311.pyc
ADDED
|
Binary file (5.06 kB). View file
|
|
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/dexycb_dataset.cpython-311.pyc
ADDED
|
Binary file (35.6 kB). View file
|
|
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/integrated.cpython-311.pyc
ADDED
|
Binary file (8.68 kB). View file
|
|
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/adapters.py
ADDED
|
@@ -0,0 +1,1402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Per-dataset adapters for MultiViewPoseDataset.
|
| 2 |
+
|
| 3 |
+
Each adapter converts a dataset's native storage into the UNIFIED contract
|
| 4 |
+
(OpenCV obj->cam meters, depth-m registered to RGB, world/tag->cam extrinsics).
|
| 5 |
+
Conventions are the verified ones in docs/dataset_convention_spec.md.
|
| 6 |
+
|
| 7 |
+
Design note on extrinsics for the two multi-view regimes:
|
| 8 |
+
(a) synced rig (DexYCB): extrinsic_cv returns the constant per-serial rig
|
| 9 |
+
calibration (tag->cam).
|
| 10 |
+
(b) static scene + moving cam (HouseCat6D, GraspNet, YCB-V, HOPE): the SAMPLE
|
| 11 |
+
fixes one object; we take the OBJECT'S canonical frame as the shared
|
| 12 |
+
"world", so extrinsic_cv returns that object's obj->cam pose at the given
|
| 13 |
+
frame. The object is static, so relative_p3d_from_ext(anchor,aux) yields the
|
| 14 |
+
exact relative camera transform with NO dependency on a camera-pose file
|
| 15 |
+
(works for train/val/test uniformly).
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import glob
|
| 20 |
+
import os
|
| 21 |
+
from typing import Any, Dict, List, Tuple
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
import torch
|
| 25 |
+
from PIL import Image
|
| 26 |
+
|
| 27 |
+
from .base_dataset import PoseDatasetAdapter, ViewSample
|
| 28 |
+
from . import dexycb_calib
|
| 29 |
+
from .dexycb_dataset import YCB_ID_TO_NAME, DEPTH_SCALE
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# --------------------------------------------------------------------------- #
|
| 33 |
+
# canonical-latent cache access (shared)
|
| 34 |
+
# --------------------------------------------------------------------------- #
|
| 35 |
+
class _CanonCache:
|
| 36 |
+
def __init__(self, cache_dir: str):
|
| 37 |
+
self.cache_dir = cache_dir
|
| 38 |
+
self._c: Dict[str, dict] = {}
|
| 39 |
+
|
| 40 |
+
def get(self, key: str) -> dict:
|
| 41 |
+
if key not in self._c:
|
| 42 |
+
# key may be namespaced "<dataset>/<obj>"; file is <cache>/<key>.pt
|
| 43 |
+
path = os.path.join(self.cache_dir, f"{key}.pt")
|
| 44 |
+
if not os.path.exists(path):
|
| 45 |
+
ds = key.split("/", 1)[0] if "/" in key else "<dataset>"
|
| 46 |
+
raise FileNotFoundError(
|
| 47 |
+
f"canonical latent not built for key '{key}' (expected {path}). "
|
| 48 |
+
f"Run: python tools/build_canonical_latents_multi.py --dataset {ds} "
|
| 49 |
+
f"--root <dataset_root> before training on this dataset."
|
| 50 |
+
)
|
| 51 |
+
self._c[key] = torch.load(path, map_location="cpu", weights_only=False)
|
| 52 |
+
return self._c[key]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _bop_gt_index(entries, oid, rank=0):
|
| 56 |
+
"""Positional index of the rank-th entry with obj_id==oid in a BOP scene_gt
|
| 57 |
+
frame list; None if that instance is absent. Used so aux views resolve the
|
| 58 |
+
SAME physical object by (obj_id, occurrence-rank) instead of trusting the
|
| 59 |
+
anchor's positional index across frames (which breaks if scene_gt ordering
|
| 60 |
+
differs). Mirrors GraspNet's index-by-class approach, rank-aware for scenes
|
| 61 |
+
with duplicate instances of one obj_id."""
|
| 62 |
+
cnt = 0
|
| 63 |
+
for idx, o in enumerate(entries):
|
| 64 |
+
if int(o["obj_id"]) == oid:
|
| 65 |
+
if cnt == rank:
|
| 66 |
+
return idx
|
| 67 |
+
cnt += 1
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# --------------------------------------------------------------------------- #
|
| 72 |
+
# DexYCB (reference adapter — reproduces MultiViewDexYCB exactly)
|
| 73 |
+
# --------------------------------------------------------------------------- #
|
| 74 |
+
class DexYCBAdapter(PoseDatasetAdapter):
|
| 75 |
+
name = "dexycb"
|
| 76 |
+
|
| 77 |
+
def __init__(self, root: str, canonical_cache: str, *, frame_stride: int = 5,
|
| 78 |
+
anchor_sweep: bool = True, subjects=None, depth_scale: float = DEPTH_SCALE):
|
| 79 |
+
self.root = root
|
| 80 |
+
self.calib_root = os.path.join(root, "calibration")
|
| 81 |
+
self.canon = _CanonCache(canonical_cache)
|
| 82 |
+
self.frame_stride = frame_stride
|
| 83 |
+
self.anchor_sweep = anchor_sweep
|
| 84 |
+
self.subjects = subjects
|
| 85 |
+
self.depth_scale = depth_scale
|
| 86 |
+
self._ext: Dict[str, dict] = {}
|
| 87 |
+
|
| 88 |
+
def _extrinsics(self, tag):
|
| 89 |
+
if tag not in self._ext:
|
| 90 |
+
self._ext[tag] = dexycb_calib.load_extrinsics(self.calib_root, tag)
|
| 91 |
+
return self._ext[tag]
|
| 92 |
+
|
| 93 |
+
def list_samples(self) -> List[ViewSample]:
|
| 94 |
+
seq_dirs = sorted(glob.glob(os.path.join(self.root, "2020*-subject-*", "2020*")))
|
| 95 |
+
if self.subjects is not None:
|
| 96 |
+
seq_dirs = [d for d in seq_dirs if any(s in d for s in self.subjects)]
|
| 97 |
+
out: List[ViewSample] = []
|
| 98 |
+
for seq_dir in seq_dirs:
|
| 99 |
+
meta = dexycb_calib._load_yaml(os.path.join(seq_dir, "meta.yml"))
|
| 100 |
+
serials = [str(s) for s in meta["serials"]]
|
| 101 |
+
ycb_ids = list(meta["ycb_ids"])
|
| 102 |
+
tag = str(meta["extrinsics"])
|
| 103 |
+
n = int(meta["num_frames"])
|
| 104 |
+
anchors = serials if self.anchor_sweep else serials[:1]
|
| 105 |
+
for frame in range(0, n, self.frame_stride):
|
| 106 |
+
for anchor in anchors:
|
| 107 |
+
aux = [s for s in serials if s != anchor]
|
| 108 |
+
for oi, oid in enumerate(ycb_ids):
|
| 109 |
+
if int(oid) not in YCB_ID_TO_NAME:
|
| 110 |
+
continue
|
| 111 |
+
obj_key = YCB_ID_TO_NAME[int(oid)]
|
| 112 |
+
aref = (seq_dir, anchor, frame, int(oid), oi, tag)
|
| 113 |
+
pool = [(seq_dir, s, frame, int(oid), oi, tag) for s in aux]
|
| 114 |
+
out.append(ViewSample(anchor=aref, aux_pool=pool, obj_key=obj_key))
|
| 115 |
+
return out
|
| 116 |
+
|
| 117 |
+
def load_view(self, ref: Any) -> Dict[str, Any]:
|
| 118 |
+
seq_dir, serial, frame, obj_id, obj_index, tag = ref
|
| 119 |
+
rgb = np.asarray(Image.open(os.path.join(seq_dir, serial, f"color_{frame:06d}.jpg"))
|
| 120 |
+
.convert("RGB"), dtype=np.float32) / 255.0
|
| 121 |
+
seg = np.load(os.path.join(seq_dir, serial, f"labels_{frame:06d}.npz"))["seg"]
|
| 122 |
+
mask = (seg == obj_id).astype(np.float32)
|
| 123 |
+
depth = np.asarray(Image.open(os.path.join(
|
| 124 |
+
seq_dir, serial, f"aligned_depth_to_color_{frame:06d}.png")),
|
| 125 |
+
dtype=np.float32) / self.depth_scale
|
| 126 |
+
intr = dexycb_calib.load_intrinsics(self.calib_root, serial)
|
| 127 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": intr}
|
| 128 |
+
|
| 129 |
+
def extrinsic_cv(self, ref: Any) -> Dict[str, np.ndarray]:
|
| 130 |
+
seq_dir, serial, frame, obj_id, obj_index, tag = ref
|
| 131 |
+
e = self._extrinsics(tag)[serial]
|
| 132 |
+
return {"R": e["R"], "t": e["t"]}
|
| 133 |
+
|
| 134 |
+
def object_pose_cv(self, sample: ViewSample) -> Tuple[np.ndarray, np.ndarray]:
|
| 135 |
+
seq_dir, serial, frame, obj_id, obj_index, tag = sample.anchor
|
| 136 |
+
py = np.load(os.path.join(seq_dir, serial, f"labels_{frame:06d}.npz"))["pose_y"][obj_index]
|
| 137 |
+
return py[:, :3].astype(np.float64), py[:, 3].astype(np.float64)
|
| 138 |
+
|
| 139 |
+
def mesh_for(self, sample: ViewSample):
|
| 140 |
+
return _load_mesh(os.path.join(self.root, "models", sample.obj_key, "textured_simple.obj"))
|
| 141 |
+
|
| 142 |
+
def canonical(self, obj_key: str) -> dict:
|
| 143 |
+
return self.canon.get(obj_key)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# --------------------------------------------------------------------------- #
|
| 147 |
+
# shared helpers for the new adapters
|
| 148 |
+
# --------------------------------------------------------------------------- #
|
| 149 |
+
import json as _json
|
| 150 |
+
import trimesh as _trimesh
|
| 151 |
+
|
| 152 |
+
def _K_to_intr(K):
|
| 153 |
+
return {"fx": float(K[0, 0]), "fy": float(K[1, 1]),
|
| 154 |
+
"ppx": float(K[0, 2]), "ppy": float(K[1, 2])}
|
| 155 |
+
|
| 156 |
+
def _load_mesh(path):
|
| 157 |
+
m = _trimesh.load(path, process=False, force="mesh")
|
| 158 |
+
if isinstance(m, _trimesh.Scene):
|
| 159 |
+
m = _trimesh.util.concatenate([g for g in m.geometry.values()])
|
| 160 |
+
return m
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# --------------------------------------------------------------------------- #
|
| 164 |
+
# HouseCat6D (type b, HELD-OUT; obj->cam OpenCV meters == identity conversion)
|
| 165 |
+
# --------------------------------------------------------------------------- #
|
| 166 |
+
class HouseCat6DAdapter(PoseDatasetAdapter):
|
| 167 |
+
name = "housecat6d"
|
| 168 |
+
|
| 169 |
+
def __init__(self, root, canonical_cache, *, split="test", max_aux_frames=8, frame_stride=10):
|
| 170 |
+
# root = .../housecat6d/extracted ; split dir under it (test/train/val)
|
| 171 |
+
self.split_dir = os.path.join(root, split)
|
| 172 |
+
self.canon = _CanonCache(canonical_cache)
|
| 173 |
+
self.max_aux_frames = max_aux_frames
|
| 174 |
+
self.frame_stride = frame_stride
|
| 175 |
+
self._pkl_cache = {}
|
| 176 |
+
|
| 177 |
+
def _pkl(self, scene, frame):
|
| 178 |
+
key = (scene, frame)
|
| 179 |
+
if key not in self._pkl_cache:
|
| 180 |
+
import pickle
|
| 181 |
+
with open(os.path.join(scene, "labels", f"{frame:06d}_label.pkl"), "rb") as f:
|
| 182 |
+
self._pkl_cache[key] = pickle.load(f)
|
| 183 |
+
return self._pkl_cache[key]
|
| 184 |
+
|
| 185 |
+
def _frames(self, scene):
|
| 186 |
+
return sorted(int(os.path.splitext(f)[0]) for f in os.listdir(os.path.join(scene, "rgb")))
|
| 187 |
+
|
| 188 |
+
def list_samples(self):
|
| 189 |
+
out = []
|
| 190 |
+
scenes = sorted(glob.glob(os.path.join(self.split_dir, "*")))
|
| 191 |
+
for scene in scenes:
|
| 192 |
+
if not os.path.isdir(os.path.join(scene, "rgb")):
|
| 193 |
+
continue
|
| 194 |
+
frames = self._frames(scene)
|
| 195 |
+
if len(frames) < 2:
|
| 196 |
+
continue
|
| 197 |
+
models = self._pkl(scene, frames[0])["model_list"]
|
| 198 |
+
for oi, model in enumerate(models):
|
| 199 |
+
anchors = frames[:: self.frame_stride] or [frames[0]]
|
| 200 |
+
for af in anchors:
|
| 201 |
+
pool = [f for f in frames if f != af]
|
| 202 |
+
out.append(ViewSample(
|
| 203 |
+
anchor={"scene": scene, "frame": af, "oi": oi, "model": model},
|
| 204 |
+
aux_pool=[{"scene": scene, "frame": f, "oi": oi, "model": model} for f in pool],
|
| 205 |
+
obj_key=f"housecat6d/{model}",
|
| 206 |
+
meta={"scene": scene, "model": model}))
|
| 207 |
+
return out
|
| 208 |
+
|
| 209 |
+
def load_view(self, ref):
|
| 210 |
+
scene, frame, model = ref["scene"], ref["frame"], ref["model"]
|
| 211 |
+
rgb = np.asarray(Image.open(os.path.join(scene, "rgb", f"{frame:06d}.png")).convert("RGB"),
|
| 212 |
+
np.float32) / 255.0
|
| 213 |
+
inst = np.asarray(Image.open(os.path.join(scene, "instance", f"{frame:06d}_{model}.png")))
|
| 214 |
+
mask = (inst != 255).astype(np.float32) # bg=255
|
| 215 |
+
depth = np.asarray(Image.open(os.path.join(scene, "depth", f"{frame:06d}.png")), np.float32) / 1000.0
|
| 216 |
+
K = np.loadtxt(os.path.join(scene, "intrinsics.txt")).reshape(3, 3)
|
| 217 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": _K_to_intr(K)}
|
| 218 |
+
|
| 219 |
+
def extrinsic_cv(self, ref): # world := object frame -> obj->cam pose
|
| 220 |
+
d = self._pkl(ref["scene"], ref["frame"])
|
| 221 |
+
return {"R": np.asarray(d["rotations"][ref["oi"]], np.float64),
|
| 222 |
+
"t": np.asarray(d["translations"][ref["oi"]], np.float64)}
|
| 223 |
+
|
| 224 |
+
def object_pose_cv(self, sample):
|
| 225 |
+
e = self.extrinsic_cv(sample.anchor)
|
| 226 |
+
return e["R"], e["t"]
|
| 227 |
+
|
| 228 |
+
def mesh_for(self, sample):
|
| 229 |
+
model = sample.meta["model"]; cat = model.split("-")[0]
|
| 230 |
+
base = os.path.dirname(os.path.dirname(self.split_dir)) # .../housecat6d
|
| 231 |
+
return _load_mesh(os.path.join(base, "extracted", "obj_models_small_size_final",
|
| 232 |
+
cat, f"{model}.obj"))
|
| 233 |
+
|
| 234 |
+
def canonical(self, obj_key):
|
| 235 |
+
return self.canon.get(obj_key)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
# --------------------------------------------------------------------------- #
|
| 239 |
+
# PACE (BOP; real split; obj->cam OpenCV mm). rigid-only (drop obj_id 545..692)
|
| 240 |
+
# NOTE: real multi-view = the 3 rig cams (separate scene folders per scene/video);
|
| 241 |
+
# a single-cam scene validates convention but not cross-cam grouping.
|
| 242 |
+
# --------------------------------------------------------------------------- #
|
| 243 |
+
class PACEAdapter(PoseDatasetAdapter):
|
| 244 |
+
name = "pace"
|
| 245 |
+
|
| 246 |
+
def __init__(self, root, canonical_cache, *, split="test", depth_scale=1.0, frame_stride=10, max_aux_frames=8):
|
| 247 |
+
self.split_dir = os.path.join(root, split)
|
| 248 |
+
self.models_dir = os.path.join(root, "models_eval")
|
| 249 |
+
self.canon = _CanonCache(canonical_cache)
|
| 250 |
+
self.depth_scale = depth_scale
|
| 251 |
+
self.frame_stride = frame_stride
|
| 252 |
+
self._gt = {}; self._cam = {}
|
| 253 |
+
|
| 254 |
+
def _scene_json(self, scene, which):
|
| 255 |
+
cache = self._gt if which == "gt" else self._cam
|
| 256 |
+
if scene not in cache:
|
| 257 |
+
cache[scene] = _json.load(open(os.path.join(scene, f"scene_{'gt' if which=='gt' else 'camera'}.json")))
|
| 258 |
+
return cache[scene]
|
| 259 |
+
|
| 260 |
+
def list_samples(self):
|
| 261 |
+
out = []
|
| 262 |
+
for scene in sorted(glob.glob(os.path.join(self.split_dir, "*"))):
|
| 263 |
+
if not os.path.isdir(os.path.join(scene, "rgb")):
|
| 264 |
+
continue
|
| 265 |
+
gt = self._scene_json(scene, "gt")
|
| 266 |
+
imids = sorted(int(k) for k in gt.keys())
|
| 267 |
+
for imid in imids[:: self.frame_stride] or imids[:1]:
|
| 268 |
+
entries = gt[str(imid)]
|
| 269 |
+
for gi, o in enumerate(entries):
|
| 270 |
+
oid = int(o["obj_id"])
|
| 271 |
+
if 545 <= oid <= 692: # articulated AKB-48 -> drop
|
| 272 |
+
continue
|
| 273 |
+
# occurrence-rank of this instance within its frame
|
| 274 |
+
rank = sum(1 for o2 in entries[:gi] if int(o2["obj_id"]) == oid)
|
| 275 |
+
# aux = only frames that actually contain this (oid,rank) instance
|
| 276 |
+
pool = [{"scene": scene, "imid": j, "oid": oid, "rank": rank}
|
| 277 |
+
for j in imids if j != imid
|
| 278 |
+
and _bop_gt_index(gt[str(j)], oid, rank) is not None]
|
| 279 |
+
ref = {"scene": scene, "imid": imid, "oid": oid, "rank": rank}
|
| 280 |
+
out.append(ViewSample(anchor=ref, aux_pool=pool, obj_key=f"pace/{oid:06d}",
|
| 281 |
+
meta={"oid": oid}))
|
| 282 |
+
return out
|
| 283 |
+
|
| 284 |
+
def _gi(self, ref):
|
| 285 |
+
idx = _bop_gt_index(self._scene_json(ref["scene"], "gt")[str(ref["imid"])],
|
| 286 |
+
ref["oid"], ref.get("rank", 0))
|
| 287 |
+
if idx is None:
|
| 288 |
+
raise KeyError(f"pace: obj {ref['oid']} rank {ref.get('rank',0)} not in "
|
| 289 |
+
f"{ref['scene']} frame {ref['imid']}")
|
| 290 |
+
return idx
|
| 291 |
+
|
| 292 |
+
def load_view(self, ref):
|
| 293 |
+
scene, imid = ref["scene"], ref["imid"]
|
| 294 |
+
rgb = np.asarray(Image.open(os.path.join(scene, "rgb", f"{imid:06d}.png")).convert("RGB"), np.float32) / 255.0
|
| 295 |
+
depth = np.asarray(Image.open(os.path.join(scene, "depth", f"{imid:06d}.png")), np.float32)
|
| 296 |
+
cam = self._scene_json(scene, "camera")[str(imid)]
|
| 297 |
+
ds = float(cam.get("depth_scale", self.depth_scale))
|
| 298 |
+
depth_m = depth * ds / 1000.0
|
| 299 |
+
gi = self._gi(ref)
|
| 300 |
+
mp = os.path.join(scene, "mask_visib", f"{imid:06d}_{gi:06d}.png")
|
| 301 |
+
mask = (np.asarray(Image.open(mp)) > 0).astype(np.float32) if os.path.exists(mp) else np.zeros(depth.shape, np.float32)
|
| 302 |
+
K = np.asarray(cam["cam_K"], np.float64).reshape(3, 3)
|
| 303 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth_m, "intr": _K_to_intr(K)}
|
| 304 |
+
|
| 305 |
+
def _pose(self, ref):
|
| 306 |
+
o = self._scene_json(ref["scene"], "gt")[str(ref["imid"])][self._gi(ref)]
|
| 307 |
+
R = np.asarray(o["cam_R_m2c"], np.float64).reshape(3, 3)
|
| 308 |
+
t = np.asarray(o["cam_t_m2c"], np.float64) / 1000.0
|
| 309 |
+
return R, t
|
| 310 |
+
|
| 311 |
+
def extrinsic_cv(self, ref):
|
| 312 |
+
R, t = self._pose(ref); return {"R": R, "t": t}
|
| 313 |
+
|
| 314 |
+
def object_pose_cv(self, sample):
|
| 315 |
+
return self._pose(sample.anchor)
|
| 316 |
+
|
| 317 |
+
def mesh_for(self, sample):
|
| 318 |
+
m = _load_mesh(os.path.join(self.models_dir, f"obj_{sample.meta['oid']:06d}.ply"))
|
| 319 |
+
m.apply_scale(1e-3) # mm -> m
|
| 320 |
+
return m
|
| 321 |
+
|
| 322 |
+
def canonical(self, obj_key):
|
| 323 |
+
return self.canon.get(obj_key)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# --------------------------------------------------------------------------- #
|
| 327 |
+
# GraspNet-1B (type b; obj->cam OpenCV meters; mesh id = cls_value - 1)
|
| 328 |
+
# --------------------------------------------------------------------------- #
|
| 329 |
+
class GraspNetAdapter(PoseDatasetAdapter):
|
| 330 |
+
name = "graspnet"
|
| 331 |
+
|
| 332 |
+
def __init__(self, root, canonical_cache, *, camera="kinect", frame_stride=20, max_aux_frames=8):
|
| 333 |
+
self.scenes_dir = os.path.join(root, "scenes")
|
| 334 |
+
self.models_dir = os.path.join(root, "models")
|
| 335 |
+
self.canon = _CanonCache(canonical_cache)
|
| 336 |
+
self.camera = camera
|
| 337 |
+
self.frame_stride = frame_stride
|
| 338 |
+
self._meta = {}
|
| 339 |
+
|
| 340 |
+
def _load_meta(self, cdir, v):
|
| 341 |
+
key = (cdir, v)
|
| 342 |
+
if key not in self._meta:
|
| 343 |
+
import scipy.io as sio
|
| 344 |
+
self._meta[key] = sio.loadmat(os.path.join(cdir, "meta", f"{v:04d}.mat"))
|
| 345 |
+
return self._meta[key]
|
| 346 |
+
|
| 347 |
+
def list_samples(self):
|
| 348 |
+
out = []
|
| 349 |
+
for scene in sorted(glob.glob(os.path.join(self.scenes_dir, "scene_*"))):
|
| 350 |
+
cdir = os.path.join(scene, self.camera)
|
| 351 |
+
if not os.path.isdir(os.path.join(cdir, "rgb")):
|
| 352 |
+
continue
|
| 353 |
+
views = sorted(int(os.path.splitext(f)[0]) for f in os.listdir(os.path.join(cdir, "rgb")))
|
| 354 |
+
m0 = self._load_meta(cdir, views[0])
|
| 355 |
+
cls = [int(c) for c in m0["cls_indexes"].ravel()]
|
| 356 |
+
anchors = views[:: self.frame_stride] or [views[0]]
|
| 357 |
+
for av in anchors:
|
| 358 |
+
mv = self._load_meta(cdir, av)
|
| 359 |
+
clsv = [int(c) for c in mv["cls_indexes"].ravel()]
|
| 360 |
+
for i, V in enumerate(clsv):
|
| 361 |
+
pool = [{"cdir": cdir, "v": w, "cls": V} for w in views if w != av]
|
| 362 |
+
out.append(ViewSample(
|
| 363 |
+
anchor={"cdir": cdir, "v": av, "cls": V, "i": i},
|
| 364 |
+
aux_pool=pool, obj_key=f"graspnet/{V-1:03d}", meta={"V": V}))
|
| 365 |
+
return out
|
| 366 |
+
|
| 367 |
+
def load_view(self, ref):
|
| 368 |
+
cdir, v = ref["cdir"], ref["v"]
|
| 369 |
+
rgb = np.asarray(Image.open(os.path.join(cdir, "rgb", f"{v:04d}.png")).convert("RGB"), np.float32) / 255.0
|
| 370 |
+
depth = np.asarray(Image.open(os.path.join(cdir, "depth", f"{v:04d}.png")), np.float32) / 1000.0
|
| 371 |
+
label = np.asarray(Image.open(os.path.join(cdir, "label", f"{v:04d}.png")))
|
| 372 |
+
mask = (label == ref["cls"]).astype(np.float32)
|
| 373 |
+
m = self._load_meta(cdir, v)
|
| 374 |
+
K = np.asarray(m["intrinsic_matrix"], np.float64).reshape(3, 3)
|
| 375 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": _K_to_intr(K)}
|
| 376 |
+
|
| 377 |
+
def _pose(self, cdir, v, cls):
|
| 378 |
+
m = self._load_meta(cdir, v)
|
| 379 |
+
clsv = [int(c) for c in m["cls_indexes"].ravel()]
|
| 380 |
+
i = clsv.index(cls)
|
| 381 |
+
P = m["poses"][:, :, i]
|
| 382 |
+
return P[:, :3].astype(np.float64), P[:, 3].astype(np.float64)
|
| 383 |
+
|
| 384 |
+
def extrinsic_cv(self, ref):
|
| 385 |
+
R, t = self._pose(ref["cdir"], ref["v"], ref["cls"]); return {"R": R, "t": t}
|
| 386 |
+
|
| 387 |
+
def object_pose_cv(self, sample):
|
| 388 |
+
a = sample.anchor
|
| 389 |
+
return self._pose(a["cdir"], a["v"], a["cls"])
|
| 390 |
+
|
| 391 |
+
def mesh_for(self, sample):
|
| 392 |
+
return _load_mesh(os.path.join(self.models_dir, f"{sample.meta['V']-1:03d}", "nontextured.ply"))
|
| 393 |
+
|
| 394 |
+
def canonical(self, obj_key):
|
| 395 |
+
return self.canon.get(obj_key)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
# --------------------------------------------------------------------------- #
|
| 399 |
+
# YCB-V (BOP; obj->cam OpenCV mm; frames-as-views static; YCB objs reuse latents)
|
| 400 |
+
# --------------------------------------------------------------------------- #
|
| 401 |
+
class YCBVAdapter(PoseDatasetAdapter):
|
| 402 |
+
name = "ycbv"
|
| 403 |
+
_BOP2NAME = None
|
| 404 |
+
|
| 405 |
+
def __init__(self, root, canonical_cache, *, split="test", frame_stride=15, max_aux_frames=8):
|
| 406 |
+
self.split_dir = os.path.join(root, split)
|
| 407 |
+
self.models_dir = os.path.join(root, "models_eval")
|
| 408 |
+
self.canon = _CanonCache(canonical_cache)
|
| 409 |
+
self.frame_stride = frame_stride
|
| 410 |
+
self._gt = {}; self._cam = {}
|
| 411 |
+
if YCBVAdapter._BOP2NAME is None:
|
| 412 |
+
YCBVAdapter._BOP2NAME = {i + 1: n for i, n in enumerate([
|
| 413 |
+
"002_master_chef_can","003_cracker_box","004_sugar_box","005_tomato_soup_can",
|
| 414 |
+
"006_mustard_bottle","007_tuna_fish_can","008_pudding_box","009_gelatin_box",
|
| 415 |
+
"010_potted_meat_can","011_banana","019_pitcher_base","021_bleach_cleanser",
|
| 416 |
+
"024_bowl","025_mug","035_power_drill","036_wood_block","037_scissors",
|
| 417 |
+
"040_large_marker","051_large_clamp","052_extra_large_clamp","061_foam_brick"])}
|
| 418 |
+
|
| 419 |
+
def _sj(self, scene, which):
|
| 420 |
+
c = self._gt if which == "gt" else self._cam
|
| 421 |
+
if scene not in c:
|
| 422 |
+
c[scene] = _json.load(open(os.path.join(scene, f"scene_{'gt' if which=='gt' else 'camera'}.json")))
|
| 423 |
+
return c[scene]
|
| 424 |
+
|
| 425 |
+
def list_samples(self):
|
| 426 |
+
out = []
|
| 427 |
+
for scene in sorted(glob.glob(os.path.join(self.split_dir, "*"))):
|
| 428 |
+
if not os.path.isdir(os.path.join(scene, "rgb")):
|
| 429 |
+
continue
|
| 430 |
+
gt = self._sj(scene, "gt")
|
| 431 |
+
imids = sorted(int(k) for k in gt.keys())
|
| 432 |
+
for imid in imids[:: self.frame_stride] or imids[:1]:
|
| 433 |
+
entries = gt[str(imid)]
|
| 434 |
+
for gi, o in enumerate(entries):
|
| 435 |
+
oid = int(o["obj_id"])
|
| 436 |
+
name = self._BOP2NAME.get(oid)
|
| 437 |
+
if name is None:
|
| 438 |
+
continue
|
| 439 |
+
rank = sum(1 for o2 in entries[:gi] if int(o2["obj_id"]) == oid)
|
| 440 |
+
pool = [{"scene": scene, "imid": j, "oid": oid, "rank": rank}
|
| 441 |
+
for j in imids if j != imid
|
| 442 |
+
and _bop_gt_index(gt[str(j)], oid, rank) is not None]
|
| 443 |
+
out.append(ViewSample(
|
| 444 |
+
anchor={"scene": scene, "imid": imid, "oid": oid, "rank": rank},
|
| 445 |
+
aux_pool=pool, obj_key=name, meta={"oid": oid}))
|
| 446 |
+
return out
|
| 447 |
+
|
| 448 |
+
def _gi(self, ref):
|
| 449 |
+
idx = _bop_gt_index(self._sj(ref["scene"], "gt")[str(ref["imid"])],
|
| 450 |
+
ref["oid"], ref.get("rank", 0))
|
| 451 |
+
if idx is None:
|
| 452 |
+
raise KeyError(f"ycbv: obj {ref['oid']} rank {ref.get('rank',0)} not in "
|
| 453 |
+
f"{ref['scene']} frame {ref['imid']}")
|
| 454 |
+
return idx
|
| 455 |
+
|
| 456 |
+
def load_view(self, ref):
|
| 457 |
+
scene, imid = ref["scene"], ref["imid"]
|
| 458 |
+
rgb = np.asarray(Image.open(os.path.join(scene, "rgb", f"{imid:06d}.png")).convert("RGB"), np.float32) / 255.0
|
| 459 |
+
cam = self._sj(scene, "camera")[str(imid)]
|
| 460 |
+
ds = float(cam.get("depth_scale", 1.0))
|
| 461 |
+
depth = np.asarray(Image.open(os.path.join(scene, "depth", f"{imid:06d}.png")), np.float32) * ds / 1000.0
|
| 462 |
+
gi = self._gi(ref)
|
| 463 |
+
mp = os.path.join(scene, "mask_visib", f"{imid:06d}_{gi:06d}.png")
|
| 464 |
+
mask = (np.asarray(Image.open(mp)) > 0).astype(np.float32) if os.path.exists(mp) else np.zeros(depth.shape, np.float32)
|
| 465 |
+
K = np.asarray(cam["cam_K"], np.float64).reshape(3, 3)
|
| 466 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": _K_to_intr(K)}
|
| 467 |
+
|
| 468 |
+
def _pose(self, ref):
|
| 469 |
+
o = self._sj(ref["scene"], "gt")[str(ref["imid"])][self._gi(ref)]
|
| 470 |
+
return (np.asarray(o["cam_R_m2c"], np.float64).reshape(3, 3),
|
| 471 |
+
np.asarray(o["cam_t_m2c"], np.float64) / 1000.0)
|
| 472 |
+
|
| 473 |
+
def extrinsic_cv(self, ref):
|
| 474 |
+
R, t = self._pose(ref); return {"R": R, "t": t}
|
| 475 |
+
|
| 476 |
+
def object_pose_cv(self, sample):
|
| 477 |
+
return self._pose(sample.anchor)
|
| 478 |
+
|
| 479 |
+
def mesh_for(self, sample):
|
| 480 |
+
m = _load_mesh(os.path.join(self.models_dir, f"obj_{sample.meta['oid']:06d}.ply"))
|
| 481 |
+
m.apply_scale(1e-3)
|
| 482 |
+
return m
|
| 483 |
+
|
| 484 |
+
def canonical(self, obj_key):
|
| 485 |
+
return self.canon.get(obj_key)
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
# --------------------------------------------------------------------------- #
|
| 489 |
+
# STUBS / notes for adapters whose frame data is blocked or needs extra handling
|
| 490 |
+
# --------------------------------------------------------------------------- #
|
| 491 |
+
class _NotReadyAdapter(PoseDatasetAdapter):
|
| 492 |
+
"""Placeholder: convention verified, adapter deferred. See reason."""
|
| 493 |
+
def __init__(self, *a, reason="", **k): self._reason = reason
|
| 494 |
+
def list_samples(self): raise NotImplementedError(self._reason)
|
| 495 |
+
def load_view(self, ref): raise NotImplementedError(self._reason)
|
| 496 |
+
def extrinsic_cv(self, ref): raise NotImplementedError(self._reason)
|
| 497 |
+
def object_pose_cv(self, sample): raise NotImplementedError(self._reason)
|
| 498 |
+
def canonical(self, obj_key): raise NotImplementedError(self._reason)
|
| 499 |
+
|
| 500 |
+
# --------------------------------------------------------------------------- #
|
| 501 |
+
# HO-Cap (type a, 8 synced RealSense; TRAIN-ONLY pseudo-GT). world := object
|
| 502 |
+
# frame (same pattern as the type-b adapters): each view's obj->cam comes from
|
| 503 |
+
# that camera's per-frame label npz (verified == official ope_gt to 1e-7), so no
|
| 504 |
+
# calibration-file compose is needed and relative_p3d_from_ext(anchor,aux) yields
|
| 505 |
+
# the exact cross-camera transform. object_ids[oi] <-> obj_poses[oi] <-> seg_mask
|
| 506 |
+
# id (oi+1), confirmed on disk.
|
| 507 |
+
# --------------------------------------------------------------------------- #
|
| 508 |
+
class HOCapAdapter(PoseDatasetAdapter):
|
| 509 |
+
name = "hocap"
|
| 510 |
+
|
| 511 |
+
def __init__(self, root, canonical_cache, *, frame_stride=15, subjects=None):
|
| 512 |
+
self.root = root # dataset/hocap
|
| 513 |
+
self.canon = _CanonCache(canonical_cache)
|
| 514 |
+
self.frame_stride = frame_stride
|
| 515 |
+
self.subjects = subjects
|
| 516 |
+
self._lab = {}
|
| 517 |
+
|
| 518 |
+
def _seqs(self):
|
| 519 |
+
out = []
|
| 520 |
+
for subj in sorted(glob.glob(os.path.join(self.root, "subject_*"))):
|
| 521 |
+
if os.path.isfile(subj):
|
| 522 |
+
continue
|
| 523 |
+
if self.subjects and os.path.basename(subj) not in self.subjects:
|
| 524 |
+
continue
|
| 525 |
+
for seq in sorted(glob.glob(os.path.join(subj, "2*"))):
|
| 526 |
+
if os.path.exists(os.path.join(seq, "meta.yaml")):
|
| 527 |
+
out.append(seq)
|
| 528 |
+
return out
|
| 529 |
+
|
| 530 |
+
def _meta(self, seq):
|
| 531 |
+
import yaml
|
| 532 |
+
with open(os.path.join(seq, "meta.yaml")) as f:
|
| 533 |
+
return yaml.safe_load(f)
|
| 534 |
+
|
| 535 |
+
def _label(self, seq, serial, frame):
|
| 536 |
+
k = (seq, serial, frame)
|
| 537 |
+
if k not in self._lab:
|
| 538 |
+
self._lab[k] = np.load(os.path.join(seq, serial, f"label_{frame:06d}.npz"))
|
| 539 |
+
return self._lab[k]
|
| 540 |
+
|
| 541 |
+
@staticmethod
|
| 542 |
+
def _present_frames(d):
|
| 543 |
+
"""Integer frames under serial dir `d` that have color+depth+label ALL
|
| 544 |
+
present (so every needed file loads). Robust to sparse/non-contiguous
|
| 545 |
+
frame ids and partially-extracted data."""
|
| 546 |
+
fr = None
|
| 547 |
+
try:
|
| 548 |
+
names = os.listdir(d)
|
| 549 |
+
except FileNotFoundError:
|
| 550 |
+
return set()
|
| 551 |
+
for pre, suf in (("color_", ".jpg"), ("depth_", ".png"), ("label_", ".npz")):
|
| 552 |
+
s = set()
|
| 553 |
+
for f in names:
|
| 554 |
+
if f.startswith(pre) and f.endswith(suf):
|
| 555 |
+
try:
|
| 556 |
+
s.add(int(f[len(pre):-len(suf)]))
|
| 557 |
+
except ValueError:
|
| 558 |
+
pass
|
| 559 |
+
fr = s if fr is None else (fr & s)
|
| 560 |
+
return fr or set()
|
| 561 |
+
|
| 562 |
+
def list_samples(self):
|
| 563 |
+
out = []
|
| 564 |
+
for seq in self._seqs():
|
| 565 |
+
m = self._meta(seq)
|
| 566 |
+
serials = [str(s) for s in m["realsense"]["serials"]]
|
| 567 |
+
obj_ids = list(m["object_ids"])
|
| 568 |
+
# Frames usable by EVERY serial (each view's color/depth/label exist).
|
| 569 |
+
common = None
|
| 570 |
+
for s in serials:
|
| 571 |
+
fs = self._present_frames(os.path.join(seq, s))
|
| 572 |
+
common = fs if common is None else (common & fs)
|
| 573 |
+
frames = sorted(common or set())[:: self.frame_stride]
|
| 574 |
+
for frame in frames:
|
| 575 |
+
for oi, obj in enumerate(obj_ids):
|
| 576 |
+
for anchor in serials:
|
| 577 |
+
aux = [s for s in serials if s != anchor]
|
| 578 |
+
aref = (seq, anchor, frame, oi, obj)
|
| 579 |
+
pool = [(seq, s, frame, oi, obj) for s in aux]
|
| 580 |
+
out.append(ViewSample(anchor=aref, aux_pool=pool,
|
| 581 |
+
obj_key=f"hocap/{obj}", meta={"obj": obj, "oi": oi}))
|
| 582 |
+
return out
|
| 583 |
+
|
| 584 |
+
def load_view(self, ref):
|
| 585 |
+
seq, serial, frame, oi, obj = ref
|
| 586 |
+
rgb = np.asarray(Image.open(os.path.join(seq, serial, f"color_{frame:06d}.jpg")).convert("RGB"),
|
| 587 |
+
np.float32) / 255.0
|
| 588 |
+
depth = np.asarray(Image.open(os.path.join(seq, serial, f"depth_{frame:06d}.png")), np.float32) / 1000.0
|
| 589 |
+
lab = self._label(seq, serial, frame)
|
| 590 |
+
mask = (lab["seg_mask"] == (oi + 1)).astype(np.float32) # local ids 1..n
|
| 591 |
+
K = np.asarray(lab["cam_K"], np.float64).reshape(3, 3) # color K (depth aligned to color)
|
| 592 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": _K_to_intr(K)}
|
| 593 |
+
|
| 594 |
+
def _pose(self, seq, serial, frame, oi):
|
| 595 |
+
T = np.asarray(self._label(seq, serial, frame)["obj_poses"][oi], np.float64) # obj->cam
|
| 596 |
+
return T[:3, :3], T[:3, 3]
|
| 597 |
+
|
| 598 |
+
def extrinsic_cv(self, ref):
|
| 599 |
+
seq, serial, frame, oi, obj = ref
|
| 600 |
+
R, t = self._pose(seq, serial, frame, oi)
|
| 601 |
+
return {"R": R, "t": t}
|
| 602 |
+
|
| 603 |
+
def object_pose_cv(self, sample):
|
| 604 |
+
seq, serial, frame, oi, obj = sample.anchor
|
| 605 |
+
return self._pose(seq, serial, frame, oi)
|
| 606 |
+
|
| 607 |
+
def mesh_for(self, sample):
|
| 608 |
+
return _load_mesh(os.path.join(self.root, "models", sample.meta["obj"], "textured_mesh.obj"))
|
| 609 |
+
|
| 610 |
+
def canonical(self, obj_key):
|
| 611 |
+
return self.canon.get(obj_key)
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
# --------------------------------------------------------------------------- #
|
| 615 |
+
# ContactPose (type a, 3 synced Kinect v2; TRAIN-ONLY, static grasps). cTo is
|
| 616 |
+
# obj->cam OpenCV meters; mesh PLY is mm (*1e-3). The stored images are in a
|
| 617 |
+
# per-camera orientation (left/right PORTRAIT 540x960, middle rot180) while the
|
| 618 |
+
# intrinsics K are always in the LANDSCAPE 960x540 frame with the affine A
|
| 619 |
+
# mapping landscape->stored (utilities/misc.get_A). We inverse-warp each stored
|
| 620 |
+
# rgb/depth back to the landscape frame so the pipeline's diagonal-K unprojection
|
| 621 |
+
# is exact; masks are rendered from mesh+cTo+K in that same landscape frame.
|
| 622 |
+
# world := object frame (per-view extrinsic = that view's cTo). Static -> sample
|
| 623 |
+
# a few frames per grasp (default), not all ~558.
|
| 624 |
+
# --------------------------------------------------------------------------- #
|
| 625 |
+
class ContactPoseAdapter(PoseDatasetAdapter):
|
| 626 |
+
name = "contactpose"
|
| 627 |
+
|
| 628 |
+
def __init__(self, root, canonical_cache, *, frames_per_grasp=4, frame_stride=None):
|
| 629 |
+
self.root = root # dataset/contactpose
|
| 630 |
+
self.data_dir = os.path.join(root, "contactpose_data")
|
| 631 |
+
self.models_dir = os.path.join(root, "object_models")
|
| 632 |
+
self.canon = _CanonCache(canonical_cache)
|
| 633 |
+
self.frames_per_grasp = frames_per_grasp
|
| 634 |
+
self._ann = {}
|
| 635 |
+
self._meshc = {}
|
| 636 |
+
|
| 637 |
+
def _annot(self, gdir):
|
| 638 |
+
if gdir not in self._ann:
|
| 639 |
+
self._ann[gdir] = _json.load(open(os.path.join(gdir, "annotations.json")))
|
| 640 |
+
return self._ann[gdir]
|
| 641 |
+
|
| 642 |
+
@staticmethod
|
| 643 |
+
def _q2m(q): # transforms3d wxyz -> R
|
| 644 |
+
from scipy.spatial.transform import Rotation
|
| 645 |
+
return Rotation.from_quat([q[1], q[2], q[3], q[0]]).as_matrix()
|
| 646 |
+
|
| 647 |
+
@classmethod
|
| 648 |
+
def _pm(cls, p):
|
| 649 |
+
T = np.eye(4); T[:3, :3] = cls._q2m(p["rotation"]); T[:3, 3] = p["translation"]; return T
|
| 650 |
+
|
| 651 |
+
@staticmethod
|
| 652 |
+
def _get_A(cam): # landscape(960x540) -> stored
|
| 653 |
+
def flipud(H): return np.array([[1, 0, 0], [0, -1, H], [0, 0, 1]], float)
|
| 654 |
+
def fliplr(W): return np.array([[-1, 0, W], [0, 1, 0], [0, 0, 1]], float)
|
| 655 |
+
def transpose(): return np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]], float)
|
| 656 |
+
if cam == "kinect2_left": return fliplr(540) @ transpose()
|
| 657 |
+
if cam == "kinect2_right": return flipud(960) @ transpose()
|
| 658 |
+
if cam == "kinect2_middle": return fliplr(960) @ flipud(540)
|
| 659 |
+
raise NotImplementedError(cam)
|
| 660 |
+
|
| 661 |
+
def list_samples(self):
|
| 662 |
+
out = []
|
| 663 |
+
for gdir in sorted(glob.glob(os.path.join(self.data_dir, "*", "*"))):
|
| 664 |
+
if not os.path.exists(os.path.join(gdir, "annotations.json")):
|
| 665 |
+
continue
|
| 666 |
+
ann = self._annot(gdir); obj = ann["object"]
|
| 667 |
+
cams = [c for c, cv in ann["cameras"].items() if cv.get("valid", True)]
|
| 668 |
+
if len(cams) < 2:
|
| 669 |
+
continue
|
| 670 |
+
nfr = len(ann["frames"])
|
| 671 |
+
if nfr == 0:
|
| 672 |
+
continue
|
| 673 |
+
idxs = np.unique(np.linspace(0, nfr - 1, self.frames_per_grasp).round().astype(int))
|
| 674 |
+
for fi in idxs:
|
| 675 |
+
for anchor in cams:
|
| 676 |
+
aux = [c for c in cams if c != anchor]
|
| 677 |
+
aref = (gdir, anchor, int(fi), obj)
|
| 678 |
+
pool = [(gdir, c, int(fi), obj) for c in aux]
|
| 679 |
+
out.append(ViewSample(anchor=aref, aux_pool=pool,
|
| 680 |
+
obj_key=f"contactpose/{obj}", meta={"obj": obj}))
|
| 681 |
+
return out
|
| 682 |
+
|
| 683 |
+
def _K(self, gdir, cam):
|
| 684 |
+
k = self._annot(gdir)["cameras"][cam]["K"]
|
| 685 |
+
return np.array([[k["fx"], 0, k["cx"]], [0, k["fy"], k["cy"]], [0, 0, 1]], float)
|
| 686 |
+
|
| 687 |
+
def _cTo(self, gdir, cam, fi):
|
| 688 |
+
ann = self._annot(gdir)
|
| 689 |
+
wTc = self._pm(ann["cameras"][cam]["wTc"])
|
| 690 |
+
oTw = self._pm(ann["frames"][fi]["oTw"])
|
| 691 |
+
return np.linalg.inv(oTw @ wTc) # obj->cam
|
| 692 |
+
|
| 693 |
+
@staticmethod
|
| 694 |
+
def _unrotate(img, A, Wl=960, Hl=540):
|
| 695 |
+
"""Resample the stored image into the landscape (Wl x Hl) frame:
|
| 696 |
+
landscape[v_l,u_l] = stored[ round(A @ [u_l,v_l,1]) ]. Correct for any A
|
| 697 |
+
(rot180 / transpose+flip) by construction; nearest-neighbour (NaN/edge safe)."""
|
| 698 |
+
us, vs = np.meshgrid(np.arange(Wl), np.arange(Hl))
|
| 699 |
+
land = np.stack([us.ravel(), vs.ravel(), np.ones(Wl * Hl)], 0) # 3xN
|
| 700 |
+
st = A @ land
|
| 701 |
+
su = np.round(st[0]).astype(int); sv = np.round(st[1]).astype(int)
|
| 702 |
+
Hs, Ws = img.shape[:2]
|
| 703 |
+
ok = (su >= 0) & (su < Ws) & (sv >= 0) & (sv < Hs)
|
| 704 |
+
out = np.zeros((Hl * Wl,) + img.shape[2:], img.dtype)
|
| 705 |
+
src = img.reshape((Hs * Ws,) + img.shape[2:])
|
| 706 |
+
out[ok] = src[sv[ok] * Ws + su[ok]]
|
| 707 |
+
return out.reshape((Hl, Wl) + img.shape[2:])
|
| 708 |
+
|
| 709 |
+
def _mesh(self, obj):
|
| 710 |
+
if obj not in self._meshc:
|
| 711 |
+
m = _load_mesh(os.path.join(self.models_dir, f"{obj}.ply"))
|
| 712 |
+
m.apply_scale(1e-3) # mm -> m
|
| 713 |
+
self._meshc[obj] = m
|
| 714 |
+
return self._meshc[obj]
|
| 715 |
+
|
| 716 |
+
def load_view(self, ref):
|
| 717 |
+
gdir, cam, fi, obj = ref
|
| 718 |
+
A = self._get_A(cam)
|
| 719 |
+
cdir = os.path.join(gdir, "images_full", cam)
|
| 720 |
+
rgb_s = np.asarray(Image.open(os.path.join(cdir, "color", f"frame{fi:03d}.png")).convert("RGB"),
|
| 721 |
+
np.float32) / 255.0
|
| 722 |
+
dep_s = np.asarray(Image.open(os.path.join(cdir, "depth", f"frame{fi:03d}.png")), np.float32) / 1000.0
|
| 723 |
+
rgb = self._unrotate(rgb_s, A); depth = self._unrotate(dep_s, A)
|
| 724 |
+
K = self._K(gdir, cam)
|
| 725 |
+
H, W = rgb.shape[:2]
|
| 726 |
+
# render object silhouette (landscape frame) from mesh + cTo + K
|
| 727 |
+
cTo = self._cTo(gdir, cam, fi)
|
| 728 |
+
V = np.asarray(self._mesh(obj).vertices)
|
| 729 |
+
Xc = (cTo[:3, :3] @ V.T + cTo[:3, 3:4]).T
|
| 730 |
+
z = Xc[:, 2]; f = z > 1e-3
|
| 731 |
+
uv = (K @ Xc[f].T).T
|
| 732 |
+
u = np.round(uv[:, 0] / uv[:, 2]).astype(int); v = np.round(uv[:, 1] / uv[:, 2]).astype(int)
|
| 733 |
+
mask = np.zeros((H, W), np.float32)
|
| 734 |
+
ok = (u >= 0) & (u < W) & (v >= 0) & (v < H)
|
| 735 |
+
mask[v[ok], u[ok]] = 1.0
|
| 736 |
+
try:
|
| 737 |
+
import scipy.ndimage as _ndi
|
| 738 |
+
mask = _ndi.binary_fill_holes(_ndi.binary_dilation(mask > 0, iterations=3)).astype(np.float32)
|
| 739 |
+
except Exception:
|
| 740 |
+
pass
|
| 741 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": _K_to_intr(K)}
|
| 742 |
+
|
| 743 |
+
def extrinsic_cv(self, ref):
|
| 744 |
+
gdir, cam, fi, obj = ref
|
| 745 |
+
cTo = self._cTo(gdir, cam, fi)
|
| 746 |
+
return {"R": cTo[:3, :3], "t": cTo[:3, 3]}
|
| 747 |
+
|
| 748 |
+
def object_pose_cv(self, sample):
|
| 749 |
+
gdir, cam, fi, obj = sample.anchor
|
| 750 |
+
cTo = self._cTo(gdir, cam, fi)
|
| 751 |
+
return cTo[:3, :3], cTo[:3, 3]
|
| 752 |
+
|
| 753 |
+
def mesh_for(self, sample):
|
| 754 |
+
return self._mesh(sample.meta["obj"])
|
| 755 |
+
|
| 756 |
+
def canonical(self, obj_key):
|
| 757 |
+
return self.canon.get(obj_key)
|
| 758 |
+
# --------------------------------------------------------------------------- #
|
| 759 |
+
# H2O (type a-like: 4 static synced RGB-D cams cam0..cam3 + 1 ego cam4 [skipped];
|
| 760 |
+
# TRAIN, 8 non-YCB objects). Per README+h2oplayer:
|
| 761 |
+
# camN/obj_pose_RT/{f:06d}.txt = class_id + 4x4 obj->cam OpenCV METERS, USED AS-IS
|
| 762 |
+
# (NO inversion; the "cam_to_obj" note in README refers to the DIFFERENT
|
| 763 |
+
# keypoint file obj_pose/, not obj_pose_RT/).
|
| 764 |
+
# camN/cam_pose/{f:06d}.txt = 4x4 cam->world (invert -> world->cam extrinsic)
|
| 765 |
+
# camN/cam_intrinsics.txt = fx fy cx cy w h (images undistorted)
|
| 766 |
+
# camN/depth/{f:06d}.png = uint16 mm (/1000 -> meters, registered to rgb)
|
| 767 |
+
# mesh object/<name>/<*.obj> = meters (single .obj per folder; 'spray' folder's
|
| 768 |
+
# obj is 'lotion_spray.obj' -> glob, don't assume).
|
| 769 |
+
# Multi-view set = the 4 static synced cams at one frame f (anchor-sweep); each cam
|
| 770 |
+
# supplies its OWN obj->cam pose and its world->cam extrinsic, so the base's
|
| 771 |
+
# relative_p3d_from_ext gives the exact cross-camera transform (static world at
|
| 772 |
+
# instant f). Masks are NOT shipped -> rendered from mesh+pose (CPU vertex splat).
|
| 773 |
+
# canonical_key = h2o/<name>. Mark TRAIN (not held-out).
|
| 774 |
+
# --------------------------------------------------------------------------- #
|
| 775 |
+
_H2O_CLASSES = {1: "book", 2: "espresso", 3: "lotion", 4: "spray",
|
| 776 |
+
5: "milk", 6: "cocoa", 7: "chips", 8: "cappuccino"}
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
class H2OAdapter(PoseDatasetAdapter):
|
| 780 |
+
name = "h2o"
|
| 781 |
+
|
| 782 |
+
def __init__(self, root, canonical_cache, *, frame_stride=15, num_static_cams=4, subjects=None):
|
| 783 |
+
self.root = root # dataset/h2o
|
| 784 |
+
self.obj_root = os.path.join(root, "object_extracted", "object")
|
| 785 |
+
self.canon = _CanonCache(canonical_cache)
|
| 786 |
+
self.frame_stride = frame_stride
|
| 787 |
+
self.ncam = int(num_static_cams) # cam0..cam3 (skip cam4 ego)
|
| 788 |
+
self.subjects = subjects
|
| 789 |
+
self._intr = {}
|
| 790 |
+
self._meshc = {}
|
| 791 |
+
|
| 792 |
+
# --- layout helpers ------------------------------------------------------ #
|
| 793 |
+
@staticmethod
|
| 794 |
+
def _pose_dir(camdir):
|
| 795 |
+
"""H2O ships 'obj_pose_RT' (README); some mirrors use lowercase. Detect."""
|
| 796 |
+
for nm in ("obj_pose_RT", "obj_pose_rt"):
|
| 797 |
+
p = os.path.join(camdir, nm)
|
| 798 |
+
if os.path.isdir(p):
|
| 799 |
+
return p
|
| 800 |
+
return os.path.join(camdir, "obj_pose_RT")
|
| 801 |
+
|
| 802 |
+
def _seq_dirs(self):
|
| 803 |
+
"""A 'sequence' dir contains cam0..cam{ncam-1}. Native layout is
|
| 804 |
+
<root>/subject{1..4}/{h1,h2,k1,k2,o1,o2}/<idx>/camN. Discover robustly by
|
| 805 |
+
finding cam0 dirs that carry an obj_pose_RT folder."""
|
| 806 |
+
out = []
|
| 807 |
+
for cam0 in sorted(glob.glob(os.path.join(self.root, "subject*", "*", "*", "cam0"))):
|
| 808 |
+
seq = os.path.dirname(cam0)
|
| 809 |
+
if not os.path.isdir(os.path.join(cam0, "rgb")):
|
| 810 |
+
continue
|
| 811 |
+
if self.subjects is not None:
|
| 812 |
+
if not any(f"subject{s}" == seq.split(os.sep)[-3] or str(s) in seq.split(os.sep)[-3]
|
| 813 |
+
for s in self.subjects):
|
| 814 |
+
continue
|
| 815 |
+
out.append(seq)
|
| 816 |
+
return out
|
| 817 |
+
|
| 818 |
+
def _frames(self, seq):
|
| 819 |
+
rgbdir = os.path.join(seq, "cam0", "rgb")
|
| 820 |
+
return sorted(int(os.path.splitext(f)[0]) for f in os.listdir(rgbdir) if f.endswith(".png"))
|
| 821 |
+
|
| 822 |
+
def _common_frames(self, seq):
|
| 823 |
+
"""Integer frames present (rgb + depth + obj_pose_RT) in EVERY of the
|
| 824 |
+
ncam static cams, so anchor and all aux views load. Robust to in-flight
|
| 825 |
+
extraction where some cams lag."""
|
| 826 |
+
common = None
|
| 827 |
+
for c in range(self.ncam):
|
| 828 |
+
camdir = os.path.join(seq, f"cam{c}")
|
| 829 |
+
triples = (("rgb", ".png"), ("depth", ".png"), (self._pose_dir(camdir), ".txt"))
|
| 830 |
+
perc = None
|
| 831 |
+
for sub, suf in triples:
|
| 832 |
+
d = sub if os.path.isabs(sub) else os.path.join(camdir, sub)
|
| 833 |
+
s = set()
|
| 834 |
+
try:
|
| 835 |
+
for f in os.listdir(d):
|
| 836 |
+
if f.endswith(suf):
|
| 837 |
+
try:
|
| 838 |
+
s.add(int(os.path.splitext(f)[0]))
|
| 839 |
+
except ValueError:
|
| 840 |
+
pass
|
| 841 |
+
except FileNotFoundError:
|
| 842 |
+
s = set()
|
| 843 |
+
perc = s if perc is None else (perc & s)
|
| 844 |
+
common = (perc or set()) if common is None else (common & (perc or set()))
|
| 845 |
+
return sorted(common or set())
|
| 846 |
+
|
| 847 |
+
def _load_RT(self, seq, cam, frame):
|
| 848 |
+
"""camN/obj_pose_RT/{f:06d}.txt -> (class_id, 4x4 obj->cam)."""
|
| 849 |
+
camdir = os.path.join(seq, f"cam{cam}")
|
| 850 |
+
arr = np.loadtxt(os.path.join(self._pose_dir(camdir), f"{frame:06d}.txt"))
|
| 851 |
+
cls = int(round(float(arr[0])))
|
| 852 |
+
M = np.asarray(arr[1:], np.float64).reshape(4, 4)
|
| 853 |
+
return cls, M
|
| 854 |
+
|
| 855 |
+
def _intrinsics(self, seq, cam):
|
| 856 |
+
key = (seq, cam)
|
| 857 |
+
if key not in self._intr:
|
| 858 |
+
p = os.path.join(seq, f"cam{cam}", "cam_intrinsics.txt")
|
| 859 |
+
fx, fy, cx, cy, w, h = np.loadtxt(p).reshape(6)
|
| 860 |
+
self._intr[key] = {"fx": float(fx), "fy": float(fy), "ppx": float(cx), "ppy": float(cy),
|
| 861 |
+
"w": int(w), "h": int(h)}
|
| 862 |
+
return self._intr[key]
|
| 863 |
+
|
| 864 |
+
def _mesh(self, name):
|
| 865 |
+
if name not in self._meshc:
|
| 866 |
+
cand = sorted(glob.glob(os.path.join(self.obj_root, name, "*.obj")))
|
| 867 |
+
if not cand:
|
| 868 |
+
raise FileNotFoundError(f"h2o: no .obj in {os.path.join(self.obj_root, name)}")
|
| 869 |
+
self._meshc[name] = _load_mesh(cand[0]) # meters, no scaling
|
| 870 |
+
return self._meshc[name]
|
| 871 |
+
|
| 872 |
+
@staticmethod
|
| 873 |
+
def _zbuffer_cpu(V, faces, R, t, K, H, W):
|
| 874 |
+
"""Exact CPU z-buffer of the mesh at obj->cam pose (R,t). Returns a HxW
|
| 875 |
+
float depth map (0 where empty) = the FRONT-surface depth. Per-face
|
| 876 |
+
barycentric fill over each face's 2D bbox, keeping the nearest z. Used to
|
| 877 |
+
(i) get the true object silhouette and (ii) derive an occlusion-aware
|
| 878 |
+
VISIBLE mask by comparing to the sensor depth."""
|
| 879 |
+
Xc = (R @ V.T + t.reshape(3, 1)).T
|
| 880 |
+
z = Xc[:, 2]
|
| 881 |
+
uv = (K @ Xc.T).T
|
| 882 |
+
u = uv[:, 0] / np.maximum(uv[:, 2], 1e-9)
|
| 883 |
+
v = uv[:, 1] / np.maximum(uv[:, 2], 1e-9)
|
| 884 |
+
P = np.stack([u, v], 1)
|
| 885 |
+
zbuf = np.full((H, W), np.inf, np.float64)
|
| 886 |
+
tri = P[faces] # (m,3,2)
|
| 887 |
+
triz = z[faces] # (m,3)
|
| 888 |
+
keep = (triz > 1e-3).all(1)
|
| 889 |
+
tri = tri[keep]; triz = triz[keep]
|
| 890 |
+
for (p0, p1, p2), (z0, z1, z2) in zip(tri, triz):
|
| 891 |
+
x0, y0 = p0; x1, y1 = p1; x2, y2 = p2
|
| 892 |
+
minx = int(max(np.floor(min(x0, x1, x2)), 0)); maxx = int(min(np.ceil(max(x0, x1, x2)), W - 1))
|
| 893 |
+
miny = int(max(np.floor(min(y0, y1, y2)), 0)); maxy = int(min(np.ceil(max(y0, y1, y2)), H - 1))
|
| 894 |
+
if maxx < minx or maxy < miny:
|
| 895 |
+
continue
|
| 896 |
+
xs, ys = np.meshgrid(np.arange(minx, maxx + 1), np.arange(miny, maxy + 1))
|
| 897 |
+
det = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2)
|
| 898 |
+
if abs(det) < 1e-9:
|
| 899 |
+
continue
|
| 900 |
+
a = ((y1 - y2) * (xs - x2) + (x2 - x1) * (ys - y2)) / det
|
| 901 |
+
b = ((y2 - y0) * (xs - x2) + (x0 - x2) * (ys - y2)) / det
|
| 902 |
+
c = 1.0 - a - b
|
| 903 |
+
ins = (a >= 0) & (b >= 0) & (c >= 0)
|
| 904 |
+
if not ins.any():
|
| 905 |
+
continue
|
| 906 |
+
zpix = a * z0 + b * z1 + c * z2
|
| 907 |
+
yy = ys[ins]; xx = xs[ins]; zz = zpix[ins]
|
| 908 |
+
cur = zbuf[yy, xx]
|
| 909 |
+
upd = zz < cur
|
| 910 |
+
zbuf[yy[upd], xx[upd]] = zz[upd]
|
| 911 |
+
zbuf[~np.isfinite(zbuf)] = 0.0
|
| 912 |
+
return zbuf
|
| 913 |
+
|
| 914 |
+
# --- interface ----------------------------------------------------------- #
|
| 915 |
+
def list_samples(self):
|
| 916 |
+
out = []
|
| 917 |
+
for seq in self._seq_dirs():
|
| 918 |
+
frames = self._common_frames(seq) # present in ALL ncam cams
|
| 919 |
+
if not frames:
|
| 920 |
+
continue
|
| 921 |
+
for f in frames[:: self.frame_stride] or frames[:1]:
|
| 922 |
+
try:
|
| 923 |
+
cls, _ = self._load_RT(seq, 0, f) # scene-global object at frame f
|
| 924 |
+
except Exception:
|
| 925 |
+
continue
|
| 926 |
+
if cls <= 0 or cls not in _H2O_CLASSES:
|
| 927 |
+
continue # background / no object
|
| 928 |
+
obj = _H2O_CLASSES[cls]
|
| 929 |
+
cams = list(range(self.ncam))
|
| 930 |
+
for anchor_ci in cams:
|
| 931 |
+
aux = [c for c in cams if c != anchor_ci]
|
| 932 |
+
aref = {"seq": seq, "cam": anchor_ci, "frame": f}
|
| 933 |
+
pool = [{"seq": seq, "cam": c, "frame": f} for c in aux]
|
| 934 |
+
out.append(ViewSample(anchor=aref, aux_pool=pool,
|
| 935 |
+
obj_key=f"h2o/{obj}", meta={"cls": cls, "obj": obj}))
|
| 936 |
+
return out
|
| 937 |
+
|
| 938 |
+
def load_view(self, ref):
|
| 939 |
+
seq, cam, frame = ref["seq"], ref["cam"], ref["frame"]
|
| 940 |
+
camdir = os.path.join(seq, f"cam{cam}")
|
| 941 |
+
rgb = np.asarray(Image.open(os.path.join(camdir, "rgb", f"{frame:06d}.png")).convert("RGB"),
|
| 942 |
+
np.float32) / 255.0
|
| 943 |
+
depth = np.asarray(Image.open(os.path.join(camdir, "depth", f"{frame:06d}.png")),
|
| 944 |
+
np.float32) / 1000.0
|
| 945 |
+
intr = self._intrinsics(seq, cam)
|
| 946 |
+
K = np.array([[intr["fx"], 0, intr["ppx"]], [0, intr["fy"], intr["ppy"]], [0, 0, 1]], np.float64)
|
| 947 |
+
H, W = rgb.shape[:2]
|
| 948 |
+
# masks not shipped -> render the object silhouette (exact CPU z-buffer)
|
| 949 |
+
# from THIS cam's mesh + obj->cam pose, then derive an OCCLUSION-AWARE
|
| 950 |
+
# VISIBLE mask (consistent with the other adapters, which all supply
|
| 951 |
+
# occlusion-aware visible masks): drop pixels where the sensor sees a
|
| 952 |
+
# surface clearly IN FRONT of the object (a hand occluder). This is a
|
| 953 |
+
# physical visibility test (nearer surface only); pixels where the sensor
|
| 954 |
+
# is behind/absent are KEPT so a wrong pose cannot hide there.
|
| 955 |
+
cls, M = self._load_RT(seq, cam, frame)
|
| 956 |
+
mask = np.zeros((H, W), np.float32)
|
| 957 |
+
if cls in _H2O_CLASSES:
|
| 958 |
+
mesh = self._mesh(_H2O_CLASSES[cls])
|
| 959 |
+
V = np.asarray(mesh.vertices, np.float64)
|
| 960 |
+
F = np.asarray(mesh.faces, np.int64)
|
| 961 |
+
zbuf = self._zbuffer_cpu(V, F, M[:3, :3], M[:3, 3], K, H, W)
|
| 962 |
+
sil = zbuf > 1e-6
|
| 963 |
+
occ = sil & (depth > 1e-3) & (depth < zbuf - 0.02) # nearer surface => occluder
|
| 964 |
+
mask = (sil & ~occ).astype(np.float32)
|
| 965 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": _K_to_intr(K)}
|
| 966 |
+
|
| 967 |
+
def extrinsic_cv(self, ref): # world->cam (invert cam->world)
|
| 968 |
+
seq, cam, frame = ref["seq"], ref["cam"], ref["frame"]
|
| 969 |
+
c2w = np.loadtxt(os.path.join(seq, f"cam{cam}", "cam_pose", f"{frame:06d}.txt")).reshape(4, 4)
|
| 970 |
+
w2c = np.linalg.inv(c2w)
|
| 971 |
+
return {"R": w2c[:3, :3].astype(np.float64), "t": w2c[:3, 3].astype(np.float64)}
|
| 972 |
+
|
| 973 |
+
def object_pose_cv(self, sample): # anchor cam's obj->cam, AS-IS
|
| 974 |
+
a = sample.anchor
|
| 975 |
+
_, M = self._load_RT(a["seq"], a["cam"], a["frame"])
|
| 976 |
+
return M[:3, :3].astype(np.float64), M[:3, 3].astype(np.float64)
|
| 977 |
+
|
| 978 |
+
def mesh_for(self, sample):
|
| 979 |
+
return self._mesh(sample.meta["obj"])
|
| 980 |
+
|
| 981 |
+
def canonical(self, obj_key):
|
| 982 |
+
return self.canon.get(obj_key)
|
| 983 |
+
|
| 984 |
+
|
| 985 |
+
# --------------------------------------------------------------------------- #
|
| 986 |
+
# HOGraspNet (type a, 4 synced Kinect cams; TRAIN; mocap GT; grasp = hand
|
| 987 |
+
# occlusion). world := the master ('mas') camera frame -> object_mat is
|
| 988 |
+
# obj->WORLD and each cam's calibration.extrinsic is WORLD->cam (OpenCV), so
|
| 989 |
+
# T_oc = M4(extrinsic) @ object_mat is obj->cam directly and
|
| 990 |
+
# relative_p3d_from_ext(anchor,aux) yields the exact cross-camera transform
|
| 991 |
+
# (same synced-rig pattern as DexYCB/HOCap). Translations are stored in cm
|
| 992 |
+
# (-> /100 for meters). The 4 cams at one frame-instant are the views
|
| 993 |
+
# (anchor-sweep). We use the SCANNED meshes (NOT dexycb CAD; different frame)
|
| 994 |
+
# with the MANDATORY per-object scale cfg._OBJECT_SCALE_FIXED[obj_idx-1] and
|
| 995 |
+
# obj_idx = HOGraspNet's own 1..30 ordering (config.OBJType), read from the
|
| 996 |
+
# sequence name (231023_S01_obj_<idx>_grasp_<g>). We consume the cropped
|
| 997 |
+
# 640x480 source_augmented RGB/depth, shifting cx,cy by the (hand-centred)
|
| 998 |
+
# crop bbox offset. obj_mask (pseudo GT) comes from extra_data.
|
| 999 |
+
# --------------------------------------------------------------------------- #
|
| 1000 |
+
# HOGraspNet's own object ordering (config.OBJType, 1..30) -> mandatory per-object
|
| 1001 |
+
# scale (config.cfg._OBJECT_SCALE_FIXED); index 0 == obj_idx 1.
|
| 1002 |
+
_HOG_OBJECT_SCALE_FIXED = [
|
| 1003 |
+
1.0, 1.0, 1.0, 1.0, 0.8296698468, 1.0, 1.0, 1.0, 1.0, 1.0,
|
| 1004 |
+
0.1035083229, 1.0, 0.6706711338, 1.0, 1.0, 0.43, 1.0, 1.0, 1.0, 1.0,
|
| 1005 |
+
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
| 1006 |
+
]
|
| 1007 |
+
|
| 1008 |
+
|
| 1009 |
+
class HOGraspNetAdapter(PoseDatasetAdapter):
|
| 1010 |
+
name = "hograspnet"
|
| 1011 |
+
CAMS = ("mas", "sub1", "sub2", "sub3") # mas == world origin
|
| 1012 |
+
_CROP_W, _CROP_H = 640, 480 # source_augmented crop size
|
| 1013 |
+
_FULL_W, _FULL_H = 1920, 1080 # full-res intrinsics frame
|
| 1014 |
+
|
| 1015 |
+
def __init__(self, root, canonical_cache, *, frame_stride=1, subjects=None):
|
| 1016 |
+
self.root = root # dataset/hograspnet
|
| 1017 |
+
self.anno_root = os.path.join(root, "labeling_data")
|
| 1018 |
+
self.src_root = os.path.join(root, "source_augmented")
|
| 1019 |
+
self.extra_root = os.path.join(root, "extra_data")
|
| 1020 |
+
self.models_root = os.path.join(root, "obj_scanned_models")
|
| 1021 |
+
self.canon = _CanonCache(canonical_cache)
|
| 1022 |
+
self.frame_stride = frame_stride
|
| 1023 |
+
self.subjects = set(subjects) if subjects is not None else None # e.g. {"S01",...}
|
| 1024 |
+
self._anno = {}
|
| 1025 |
+
self._objdir_map = None
|
| 1026 |
+
|
| 1027 |
+
# -- object index (1..30) -> scanned-model dir name ("01_cracker_box") -- #
|
| 1028 |
+
def _objdir(self):
|
| 1029 |
+
if self._objdir_map is None:
|
| 1030 |
+
m = {}
|
| 1031 |
+
for d in os.listdir(self.models_root):
|
| 1032 |
+
if os.path.isdir(os.path.join(self.models_root, d)):
|
| 1033 |
+
try:
|
| 1034 |
+
m[int(d.split("_")[0])] = d
|
| 1035 |
+
except ValueError:
|
| 1036 |
+
continue
|
| 1037 |
+
self._objdir_map = m
|
| 1038 |
+
return self._objdir_map
|
| 1039 |
+
|
| 1040 |
+
def _anno_path(self, ref):
|
| 1041 |
+
return os.path.join(self.anno_root, ref["seq"], ref["trial"], "annotation",
|
| 1042 |
+
ref["cam"], f'{ref["cam"]}_{ref["frame"]}.json')
|
| 1043 |
+
|
| 1044 |
+
def _read_anno(self, ref):
|
| 1045 |
+
p = self._anno_path(ref)
|
| 1046 |
+
if p not in self._anno:
|
| 1047 |
+
with open(p, "r", encoding="UTF-8-SIG") as f:
|
| 1048 |
+
self._anno[p] = _json.load(f)
|
| 1049 |
+
return self._anno[p]
|
| 1050 |
+
|
| 1051 |
+
def _crop_offset(self, hand_2d):
|
| 1052 |
+
"""Reproduce HOGraspNet util.utils.extractBbox: a fixed 640x480 window
|
| 1053 |
+
centred on the hand 2D keypoints (full-res coords), clamped to image
|
| 1054 |
+
bounds. Returns the integer (x0, y0) top-left offset used to crop the
|
| 1055 |
+
source_augmented RGB/depth (so cx,cy shift by -x0,-y0)."""
|
| 1056 |
+
x_min, x_max = float(hand_2d[:, 0].min()), float(hand_2d[:, 0].max())
|
| 1057 |
+
y_min, y_max = float(hand_2d[:, 1].min()), float(hand_2d[:, 1].max())
|
| 1058 |
+
x0 = max(0.0, (x_min + x_max) / 2 - self._CROP_W / 2)
|
| 1059 |
+
y0 = max(0.0, (y_min + y_max) / 2 - self._CROP_H / 2)
|
| 1060 |
+
if x0 + self._CROP_W > self._FULL_W:
|
| 1061 |
+
x0 = self._FULL_W - self._CROP_W
|
| 1062 |
+
if y0 + self._CROP_H > self._FULL_H:
|
| 1063 |
+
y0 = self._FULL_H - self._CROP_H
|
| 1064 |
+
return int(x0), int(y0)
|
| 1065 |
+
|
| 1066 |
+
def list_samples(self):
|
| 1067 |
+
out = []
|
| 1068 |
+
if not os.path.isdir(self.anno_root):
|
| 1069 |
+
return out
|
| 1070 |
+
objmap = self._objdir()
|
| 1071 |
+
for seq in sorted(os.listdir(self.anno_root)):
|
| 1072 |
+
seqdir = os.path.join(self.anno_root, seq)
|
| 1073 |
+
if not os.path.isdir(seqdir):
|
| 1074 |
+
continue
|
| 1075 |
+
parts = seq.split("_")
|
| 1076 |
+
if len(parts) < 6:
|
| 1077 |
+
continue
|
| 1078 |
+
subject = parts[1] # 'S01'
|
| 1079 |
+
if self.subjects is not None and subject not in self.subjects:
|
| 1080 |
+
continue
|
| 1081 |
+
try:
|
| 1082 |
+
obj_idx = int(parts[3])
|
| 1083 |
+
except ValueError:
|
| 1084 |
+
continue
|
| 1085 |
+
objdir = objmap.get(obj_idx)
|
| 1086 |
+
if objdir is None:
|
| 1087 |
+
continue
|
| 1088 |
+
obj_key = f"hograspnet/{objdir}"
|
| 1089 |
+
for trial in sorted(os.listdir(seqdir)):
|
| 1090 |
+
annob = os.path.join(seqdir, trial, "annotation")
|
| 1091 |
+
srcb = os.path.join(self.src_root, seq, trial, "rgb_crop")
|
| 1092 |
+
if not os.path.isdir(annob) or not os.path.isdir(srcb):
|
| 1093 |
+
continue # need cropped frames to load
|
| 1094 |
+
cams = [c for c in self.CAMS if os.path.isdir(os.path.join(annob, c))
|
| 1095 |
+
and os.path.isdir(os.path.join(srcb, c))]
|
| 1096 |
+
if not cams:
|
| 1097 |
+
continue
|
| 1098 |
+
framesets = {}
|
| 1099 |
+
for c in cams:
|
| 1100 |
+
anno_fs = set()
|
| 1101 |
+
for fn in os.listdir(os.path.join(annob, c)):
|
| 1102 |
+
if fn.endswith(".json"):
|
| 1103 |
+
anno_fs.add(fn[:-5].split("_")[-1])
|
| 1104 |
+
|
| 1105 |
+
def _crop_frames(cropdir, suf, _c=c):
|
| 1106 |
+
s = set()
|
| 1107 |
+
cd = os.path.join(self.src_root, seq, trial, cropdir, _c)
|
| 1108 |
+
try:
|
| 1109 |
+
for fn in os.listdir(cd):
|
| 1110 |
+
if fn.endswith(suf):
|
| 1111 |
+
s.add(fn[:-len(suf)].split("_")[-1])
|
| 1112 |
+
except FileNotFoundError:
|
| 1113 |
+
pass
|
| 1114 |
+
return s
|
| 1115 |
+
# frame usable only if anno json AND rgb_crop AND depth_crop exist
|
| 1116 |
+
framesets[c] = anno_fs & _crop_frames("rgb_crop", ".jpg") & _crop_frames("depth_crop", ".png")
|
| 1117 |
+
allframes = sorted(set().union(*framesets.values()), key=lambda x: int(x))
|
| 1118 |
+
for fi, frame in enumerate(allframes):
|
| 1119 |
+
if self.frame_stride > 1 and (fi % self.frame_stride):
|
| 1120 |
+
continue
|
| 1121 |
+
present = [c for c in cams if frame in framesets[c]]
|
| 1122 |
+
if not present:
|
| 1123 |
+
continue
|
| 1124 |
+
for anchor in present:
|
| 1125 |
+
aux = [c for c in present if c != anchor]
|
| 1126 |
+
aref = {"seq": seq, "trial": trial, "cam": anchor, "frame": frame}
|
| 1127 |
+
pool = [{"seq": seq, "trial": trial, "cam": c, "frame": frame} for c in aux]
|
| 1128 |
+
out.append(ViewSample(anchor=aref, aux_pool=pool, obj_key=obj_key,
|
| 1129 |
+
meta={"obj_idx": obj_idx, "objdir": objdir}))
|
| 1130 |
+
return out
|
| 1131 |
+
|
| 1132 |
+
def load_view(self, ref):
|
| 1133 |
+
anno = self._read_anno(ref)
|
| 1134 |
+
K = np.squeeze(np.asarray(anno["calibration"]["intrinsic"], np.float64)).reshape(3, 3)
|
| 1135 |
+
hand_2d = np.squeeze(np.asarray(anno["hand"]["projected_2D_pose_per_cam"], np.float64))
|
| 1136 |
+
x0, y0 = self._crop_offset(hand_2d)
|
| 1137 |
+
Kc = K.copy()
|
| 1138 |
+
Kc[0, 2] -= x0 # cropped-frame principal point
|
| 1139 |
+
Kc[1, 2] -= y0
|
| 1140 |
+
base = f'{ref["cam"]}_{ref["frame"]}'
|
| 1141 |
+
rgb_p = os.path.join(self.src_root, ref["seq"], ref["trial"], "rgb_crop", ref["cam"], base + ".jpg")
|
| 1142 |
+
dep_p = os.path.join(self.src_root, ref["seq"], ref["trial"], "depth_crop", ref["cam"], base + ".png")
|
| 1143 |
+
msk_p = os.path.join(self.extra_root, ref["seq"], ref["trial"], "obj_mask", ref["cam"], base + ".png")
|
| 1144 |
+
rgb = np.asarray(Image.open(rgb_p).convert("RGB"), np.float32) / 255.0
|
| 1145 |
+
depth = np.asarray(Image.open(dep_p), np.float32) / 1000.0 # mm -> m
|
| 1146 |
+
if os.path.exists(msk_p):
|
| 1147 |
+
ma = np.asarray(Image.open(msk_p))
|
| 1148 |
+
if ma.ndim == 3:
|
| 1149 |
+
ma = ma[..., 0]
|
| 1150 |
+
mask = (ma >= 128).astype(np.float32)
|
| 1151 |
+
else:
|
| 1152 |
+
mask = np.zeros(depth.shape, np.float32)
|
| 1153 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": _K_to_intr(Kc)}
|
| 1154 |
+
|
| 1155 |
+
def _extrinsic4(self, ref):
|
| 1156 |
+
M = np.squeeze(np.asarray(self._read_anno(ref)["calibration"]["extrinsic"], np.float64)).reshape(3, 4)
|
| 1157 |
+
M4 = np.eye(4, dtype=np.float64)
|
| 1158 |
+
M4[:3, :] = M
|
| 1159 |
+
return M4
|
| 1160 |
+
|
| 1161 |
+
def extrinsic_cv(self, ref): # world(mas)->cam, meters
|
| 1162 |
+
M4 = self._extrinsic4(ref)
|
| 1163 |
+
return {"R": M4[:3, :3], "t": M4[:3, 3] / 100.0}
|
| 1164 |
+
|
| 1165 |
+
def object_pose_cv(self, sample): # obj->cam, anchor view, meters
|
| 1166 |
+
a = sample.anchor
|
| 1167 |
+
M4 = self._extrinsic4(a)
|
| 1168 |
+
om = np.squeeze(np.asarray(self._read_anno(a)["Mesh"][0]["object_mat"], np.float64)).reshape(4, 4)
|
| 1169 |
+
T = M4 @ om # obj->cam (translation in cm)
|
| 1170 |
+
return T[:3, :3], T[:3, 3] / 100.0
|
| 1171 |
+
|
| 1172 |
+
def mesh_for(self, sample):
|
| 1173 |
+
objdir = sample.meta["objdir"]
|
| 1174 |
+
S = _HOG_OBJECT_SCALE_FIXED[sample.meta["obj_idx"] - 1] # mandatory per-object scale
|
| 1175 |
+
m = _load_mesh(os.path.join(self.models_root, objdir, f"{objdir}.obj"))
|
| 1176 |
+
m.apply_scale(S * 1e-2) # (S * V_raw) / 100 -> meters
|
| 1177 |
+
return m
|
| 1178 |
+
|
| 1179 |
+
def canonical(self, obj_key):
|
| 1180 |
+
return self.canon.get(obj_key)
|
| 1181 |
+
|
| 1182 |
+
|
| 1183 |
+
# --------------------------------------------------------------------------- #
|
| 1184 |
+
# DexH2R (type a, 4 static synced Kinect RGB-D cams; TRAIN-ONLY pseudo-GT;
|
| 1185 |
+
# 56 non-YCB objects; human->robot handover => heavy hand/robot occlusion).
|
| 1186 |
+
# Per docs/dataset_convention_spec.md (DexH2R) + camera_calibration:
|
| 1187 |
+
# obj_pose.pt = [T,4,4] = T_world<-obj (world == hand_arm base frame).
|
| 1188 |
+
# Per Kinect i in {0,1,2,3} (STATIC extrinsics):
|
| 1189 |
+
# world->cam_i W2C_i = inv(cali0i) @ hand_arm_mesh_to_kinect_pcd_0
|
| 1190 |
+
# obj->cam_i T_oc = W2C_i @ obj_pose[t] (OpenCV, meters)
|
| 1191 |
+
# (equivalently T_oc = inv(cali0i) @ hand_arm @ obj_pose[t]; round-trips the
|
| 1192 |
+
# repo's cam->world = inv(hand_arm) @ cali0i, verified in verify_conventions.py.)
|
| 1193 |
+
# Extrinsics come from the PER-TRIAL calibration/ folder (kinect/cali0i.json +
|
| 1194 |
+
# hand_arm_mesh_to_kinect_pcd_0.json). Intrinsics are NOT shipped per-trial ->
|
| 1195 |
+
# read from the reference camera_calibration/ (Kinect 8-param distortion; the
|
| 1196 |
+
# stored RGB/depth are already undistorted so K is used as a plain pinhole).
|
| 1197 |
+
# depth = uint16 PNG in mm (/1000 -> meters), same res (1080x1920) & K as RGB
|
| 1198 |
+
# (registered). Meshes object_model/<name>.obj are meters in the pose frame.
|
| 1199 |
+
# The 4 Kinect cams at one frame t are the multi-view set (anchor-sweep); each
|
| 1200 |
+
# supplies its own W2C_i so the base's relative_p3d_from_ext gives the exact
|
| 1201 |
+
# cross-camera transform (shared static world = hand_arm base at instant t).
|
| 1202 |
+
# No 2D masks are shipped -> render an OCCLUSION-AWARE visible silhouette from
|
| 1203 |
+
# mesh+pose (same CPU z-buffer + sensor-occluder test as the H2O adapter).
|
| 1204 |
+
# RealSense 0/1 also carry depth but their world pose is time-varying via
|
| 1205 |
+
# ShadowHand FK on qpos.pt (needs the shadow_asserts URDF) -> DEFERRED; the 4
|
| 1206 |
+
# Kinect views are sufficient for multi-view. ZCAM (12 RGB-only) dropped.
|
| 1207 |
+
# canonical_key = dexh2r/<name>. TRAIN, pseudo-GT.
|
| 1208 |
+
# Layout (after extract, prefix stripped): <root>/extracted/<subj>/<obj>/<seq>/
|
| 1209 |
+
# {obj_pose.pt, calibration/, kinect/<i>/{rgb,depth}/<frame>.{jpg,png}, ...}
|
| 1210 |
+
# --------------------------------------------------------------------------- #
|
| 1211 |
+
class DexH2RAdapter(PoseDatasetAdapter):
|
| 1212 |
+
name = "dexh2r"
|
| 1213 |
+
KINECTS = (0, 1, 2, 3) # the 4 static Kinect RGB-D cams
|
| 1214 |
+
|
| 1215 |
+
def __init__(self, root, canonical_cache, *, frame_stride=15, subjects=None,
|
| 1216 |
+
split=None, extracted_subdir="extracted"):
|
| 1217 |
+
self.root = root # dataset/dexh2r
|
| 1218 |
+
self.ext_root = os.path.join(root, extracted_subdir) # extracted subjects live here
|
| 1219 |
+
self.mesh_dir = os.path.join(root, "object_model", "object_model")
|
| 1220 |
+
# reference intrinsics (per-trial calibration ships extrinsics only)
|
| 1221 |
+
self.ref_calib = os.path.join(root, "camera_calibration", "camera_calibration")
|
| 1222 |
+
self.canon = _CanonCache(canonical_cache)
|
| 1223 |
+
self.frame_stride = int(frame_stride)
|
| 1224 |
+
# `subjects`/`split`: restrict to a subset of subject-id strings; split reads
|
| 1225 |
+
# dataset_split.json (<split>_sub_names) when subjects is None.
|
| 1226 |
+
if subjects is None and split is not None:
|
| 1227 |
+
sp = _json.load(open(os.path.join(root, "dataset_split.json")))
|
| 1228 |
+
subjects = [str(s) for s in sp[f"{split}_sub_names"]]
|
| 1229 |
+
self.subjects = set(str(s) for s in subjects) if subjects is not None else None
|
| 1230 |
+
self._intr = {}
|
| 1231 |
+
self._ext = {}
|
| 1232 |
+
self._meshc = {}
|
| 1233 |
+
self._nframe = {}
|
| 1234 |
+
|
| 1235 |
+
# -- calibration ---------------------------------------------------------- #
|
| 1236 |
+
@staticmethod
|
| 1237 |
+
def _seven2mat(js):
|
| 1238 |
+
from scipy.spatial.transform import Rotation
|
| 1239 |
+
v = js["value0"]; r = v["rotation"]; t = v["translation"]
|
| 1240 |
+
M = np.eye(4, dtype=np.float64)
|
| 1241 |
+
M[:3, :3] = Rotation.from_quat([r["x"], r["y"], r["z"], r["w"]]).as_matrix()
|
| 1242 |
+
M[:3, 3] = [t["x"], t["y"], t["z"]]
|
| 1243 |
+
return M
|
| 1244 |
+
|
| 1245 |
+
def _w2c(self, trial_dir, i):
|
| 1246 |
+
"""world(hand_arm)->cam_i (OpenCV, meters), from the PER-TRIAL calibration."""
|
| 1247 |
+
key = (trial_dir, i)
|
| 1248 |
+
if key not in self._ext:
|
| 1249 |
+
cdir = os.path.join(trial_dir, "calibration")
|
| 1250 |
+
hand_arm = self._seven2mat(_json.load(open(
|
| 1251 |
+
os.path.join(cdir, "hand_arm_mesh_to_kinect_pcd_0.json"))))
|
| 1252 |
+
cali0i = self._seven2mat(_json.load(open(
|
| 1253 |
+
os.path.join(cdir, "kinect", f"cali0{i}.json"))))
|
| 1254 |
+
self._ext[key] = np.linalg.inv(cali0i) @ hand_arm # world->cam_i
|
| 1255 |
+
return self._ext[key]
|
| 1256 |
+
|
| 1257 |
+
def _K(self, i):
|
| 1258 |
+
if i not in self._intr:
|
| 1259 |
+
d = _json.load(open(os.path.join(
|
| 1260 |
+
self.ref_calib, "camera_intrinsics", "kinect", f"camera_intrinsics_{i}.json")))
|
| 1261 |
+
self._intr[i] = np.asarray(d["camera_intrinsics"], np.float64).reshape(3, 3)
|
| 1262 |
+
return self._intr[i]
|
| 1263 |
+
|
| 1264 |
+
def _mesh(self, name):
|
| 1265 |
+
if name not in self._meshc:
|
| 1266 |
+
self._meshc[name] = _load_mesh(os.path.join(self.mesh_dir, f"{name}.obj"))
|
| 1267 |
+
return self._meshc[name]
|
| 1268 |
+
|
| 1269 |
+
def _obj_pose(self, trial_dir, frame):
|
| 1270 |
+
"""T_world<-obj at frame (4x4)."""
|
| 1271 |
+
op = torch.load(os.path.join(trial_dir, "obj_pose.pt"), map_location="cpu")
|
| 1272 |
+
return np.asarray(op[frame], np.float64)
|
| 1273 |
+
|
| 1274 |
+
def _T_oc(self, trial_dir, i, frame):
|
| 1275 |
+
return self._w2c(trial_dir, i) @ self._obj_pose(trial_dir, frame) # obj->cam_i
|
| 1276 |
+
|
| 1277 |
+
def _oplen(self, trial_dir):
|
| 1278 |
+
"""Number of obj_pose timesteps (upper bound on valid frame index)."""
|
| 1279 |
+
if trial_dir not in self._nframe:
|
| 1280 |
+
try:
|
| 1281 |
+
op = torch.load(os.path.join(trial_dir, "obj_pose.pt"), map_location="cpu")
|
| 1282 |
+
self._nframe[trial_dir] = int(len(op))
|
| 1283 |
+
except Exception:
|
| 1284 |
+
self._nframe[trial_dir] = 0
|
| 1285 |
+
return self._nframe[trial_dir]
|
| 1286 |
+
|
| 1287 |
+
@staticmethod
|
| 1288 |
+
def _cam_frames(trial_dir, i):
|
| 1289 |
+
"""Integer frames where kinect cam i has BOTH rgb .jpg and depth .png
|
| 1290 |
+
(files are named '<frame>.jpg'/'<frame>.png'; robust to sparse ids)."""
|
| 1291 |
+
cd = os.path.join(trial_dir, "kinect", str(i))
|
| 1292 |
+
out = None
|
| 1293 |
+
for sub, suf in (("rgb", ".jpg"), ("depth", ".png")):
|
| 1294 |
+
s = set()
|
| 1295 |
+
try:
|
| 1296 |
+
for f in os.listdir(os.path.join(cd, sub)):
|
| 1297 |
+
if f.endswith(suf):
|
| 1298 |
+
try:
|
| 1299 |
+
s.add(int(f[:-len(suf)]))
|
| 1300 |
+
except ValueError:
|
| 1301 |
+
pass
|
| 1302 |
+
except FileNotFoundError:
|
| 1303 |
+
return set()
|
| 1304 |
+
out = s if out is None else (out & s)
|
| 1305 |
+
return out or set()
|
| 1306 |
+
|
| 1307 |
+
# -- interface ------------------------------------------------------------ #
|
| 1308 |
+
def list_samples(self):
|
| 1309 |
+
out = []
|
| 1310 |
+
if not os.path.isdir(self.ext_root):
|
| 1311 |
+
return out
|
| 1312 |
+
for subj in sorted(os.listdir(self.ext_root)):
|
| 1313 |
+
subj_dir = os.path.join(self.ext_root, subj)
|
| 1314 |
+
if not os.path.isdir(subj_dir):
|
| 1315 |
+
continue
|
| 1316 |
+
if self.subjects is not None and subj not in self.subjects:
|
| 1317 |
+
continue
|
| 1318 |
+
for name in sorted(os.listdir(subj_dir)):
|
| 1319 |
+
obj_dir = os.path.join(subj_dir, name)
|
| 1320 |
+
if not os.path.isdir(obj_dir) or not os.path.exists(
|
| 1321 |
+
os.path.join(self.mesh_dir, f"{name}.obj")):
|
| 1322 |
+
continue
|
| 1323 |
+
for seq in sorted(os.listdir(obj_dir)):
|
| 1324 |
+
trial_dir = os.path.join(obj_dir, seq)
|
| 1325 |
+
if not os.path.exists(os.path.join(trial_dir, "obj_pose.pt")):
|
| 1326 |
+
continue
|
| 1327 |
+
if not os.path.exists(os.path.join(trial_dir, "calibration",
|
| 1328 |
+
"hand_arm_mesh_to_kinect_pcd_0.json")):
|
| 1329 |
+
continue
|
| 1330 |
+
cams = [i for i in self.KINECTS
|
| 1331 |
+
if os.path.isdir(os.path.join(trial_dir, "kinect", str(i), "depth"))]
|
| 1332 |
+
if len(cams) < 1:
|
| 1333 |
+
continue
|
| 1334 |
+
op_n = self._oplen(trial_dir)
|
| 1335 |
+
if op_n < 1:
|
| 1336 |
+
continue
|
| 1337 |
+
# frames actually present (rgb+depth) per cam, bounded by obj_pose
|
| 1338 |
+
per = {i: {f for f in self._cam_frames(trial_dir, i) if 0 <= f < op_n}
|
| 1339 |
+
for i in cams}
|
| 1340 |
+
allframes = sorted(set().union(*per.values()) if per else set())
|
| 1341 |
+
for fi, frame in enumerate(allframes):
|
| 1342 |
+
if self.frame_stride > 1 and (fi % self.frame_stride):
|
| 1343 |
+
continue
|
| 1344 |
+
present = [i for i in cams if frame in per[i]] # cams with this frame
|
| 1345 |
+
if not present:
|
| 1346 |
+
continue
|
| 1347 |
+
for anchor in present:
|
| 1348 |
+
aux = [c for c in present if c != anchor]
|
| 1349 |
+
aref = {"trial": trial_dir, "cam": anchor, "frame": frame, "name": name}
|
| 1350 |
+
pool = [{"trial": trial_dir, "cam": c, "frame": frame, "name": name}
|
| 1351 |
+
for c in aux]
|
| 1352 |
+
out.append(ViewSample(anchor=aref, aux_pool=pool,
|
| 1353 |
+
obj_key=f"dexh2r/{name}",
|
| 1354 |
+
meta={"obj": name, "subject": subj,
|
| 1355 |
+
"pseudo_gt": True, "train": True}))
|
| 1356 |
+
return out
|
| 1357 |
+
|
| 1358 |
+
def load_view(self, ref):
|
| 1359 |
+
trial, i, frame, name = ref["trial"], ref["cam"], ref["frame"], ref["name"]
|
| 1360 |
+
cdir = os.path.join(trial, "kinect", str(i))
|
| 1361 |
+
rgb = np.asarray(Image.open(os.path.join(cdir, "rgb", f"{frame}.jpg")).convert("RGB"),
|
| 1362 |
+
np.float32) / 255.0
|
| 1363 |
+
depth = np.asarray(Image.open(os.path.join(cdir, "depth", f"{frame}.png")),
|
| 1364 |
+
np.float32) / 1000.0 # uint16 mm -> m
|
| 1365 |
+
K = self._K(i)
|
| 1366 |
+
H, W = depth.shape[:2]
|
| 1367 |
+
# No 2D masks shipped -> render the object silhouette (exact CPU z-buffer)
|
| 1368 |
+
# at THIS cam's obj->cam pose, then derive an OCCLUSION-AWARE VISIBLE mask
|
| 1369 |
+
# (identical policy to the H2O adapter): drop pixels where the sensor sees
|
| 1370 |
+
# a surface clearly IN FRONT of the object (hand/robot occluder); KEEP
|
| 1371 |
+
# pixels where the sensor is behind/absent so a wrong pose cannot hide.
|
| 1372 |
+
T = self._T_oc(trial, i, frame)
|
| 1373 |
+
mesh = self._mesh(name)
|
| 1374 |
+
V = np.asarray(mesh.vertices, np.float64)
|
| 1375 |
+
F = np.asarray(mesh.faces, np.int64)
|
| 1376 |
+
zbuf = H2OAdapter._zbuffer_cpu(V, F, T[:3, :3], T[:3, 3], K, H, W)
|
| 1377 |
+
sil = zbuf > 1e-6
|
| 1378 |
+
occ = sil & (depth > 1e-3) & (depth < zbuf - 0.02) # nearer surface => occluder
|
| 1379 |
+
mask = (sil & ~occ).astype(np.float32)
|
| 1380 |
+
return {"rgb": rgb, "mask": mask, "depth_m": depth, "intr": _K_to_intr(K)}
|
| 1381 |
+
|
| 1382 |
+
def extrinsic_cv(self, ref): # world(hand_arm)->cam_i
|
| 1383 |
+
W2C = self._w2c(ref["trial"], ref["cam"])
|
| 1384 |
+
return {"R": W2C[:3, :3], "t": W2C[:3, 3]}
|
| 1385 |
+
|
| 1386 |
+
def object_pose_cv(self, sample): # obj->cam, anchor view
|
| 1387 |
+
a = sample.anchor
|
| 1388 |
+
T = self._T_oc(a["trial"], a["cam"], a["frame"])
|
| 1389 |
+
return T[:3, :3], T[:3, 3]
|
| 1390 |
+
|
| 1391 |
+
def mesh_for(self, sample):
|
| 1392 |
+
return self._mesh(sample.meta["obj"])
|
| 1393 |
+
|
| 1394 |
+
def canonical(self, obj_key):
|
| 1395 |
+
return self.canon.get(obj_key)
|
| 1396 |
+
|
| 1397 |
+
|
| 1398 |
+
ADAPTERS = {
|
| 1399 |
+
"dexycb": DexYCBAdapter, "housecat6d": HouseCat6DAdapter, "pace": PACEAdapter,
|
| 1400 |
+
"graspnet": GraspNetAdapter, "ycbv": YCBVAdapter, "contactpose": ContactPoseAdapter,
|
| 1401 |
+
"hocap": HOCapAdapter, "h2o": H2OAdapter, "hograspnet": HOGraspNetAdapter, "dexh2r": DexH2RAdapter,
|
| 1402 |
+
}
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/base_dataset.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dataset-agnostic multi-view 6D-pose dataset (base + adapter protocol).
|
| 2 |
+
|
| 3 |
+
This is the generalization of ``MultiViewDexYCB``: ALL geometry / SSI / GT-layout
|
| 4 |
+
/ view-construction math is reused VERBATIM from ``dexycb_dataset`` (the helper
|
| 5 |
+
functions are imported, not re-implemented), and the ONLY dataset-specific logic
|
| 6 |
+
lives behind a thin ``PoseDatasetAdapter``.
|
| 7 |
+
|
| 8 |
+
One sample = one (object-state, anchor-view) with >=1 aux views. The two
|
| 9 |
+
multi-view regimes both funnel into the same anchor+aux construction:
|
| 10 |
+
(a) synced multi-cam rig -> aux views are OTHER cameras at the same instant;
|
| 11 |
+
per-view extrinsic = the constant rig calibration (tag->cam, OpenCV).
|
| 12 |
+
(b) static scene + moving cam -> aux views are OTHER FRAMES of the same static
|
| 13 |
+
sequence; per-view extrinsic = that frame's world->cam pose (OpenCV).
|
| 14 |
+
In both cases the adapter yields, per view ref, an OpenCV tag/world->cam
|
| 15 |
+
extrinsic ``{R,t}``; the base computes the anchor-relative rigid transform with
|
| 16 |
+
the SAME formula as ``dexycb_calib.relative_p3d`` and re-expresses every aux
|
| 17 |
+
pointmap in the anchor PyTorch3D frame under one shared anchor SSI.
|
| 18 |
+
|
| 19 |
+
The adapter delivers the object pose as OpenCV **obj->cam, meters** for the
|
| 20 |
+
anchor view (== DexYCB pose_y semantics); the base applies D=diag(-1,-1,1) and
|
| 21 |
+
the canonical M-mapping. Adapters MUST NOT pre-apply D or M.
|
| 22 |
+
|
| 23 |
+
Output dict is byte-identical in structure to ``MultiViewDexYCB.__getitem__`` and
|
| 24 |
+
feeds ``collate_multiview`` unchanged.
|
| 25 |
+
"""
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import random
|
| 29 |
+
from abc import ABC, abstractmethod
|
| 30 |
+
from dataclasses import dataclass, field
|
| 31 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 32 |
+
|
| 33 |
+
import numpy as np
|
| 34 |
+
import torch
|
| 35 |
+
|
| 36 |
+
from sam3d_objects.geometry.geometry import matrix_to_rot6d, normalize_rot6d
|
| 37 |
+
|
| 38 |
+
from . import canonical
|
| 39 |
+
# Reuse the proven geometry helpers VERBATIM (imported, not re-implemented).
|
| 40 |
+
from .dexycb_dataset import (
|
| 41 |
+
IMG_SIZE,
|
| 42 |
+
_apply_ssi,
|
| 43 |
+
_cam_feature,
|
| 44 |
+
_ssi_params,
|
| 45 |
+
_to_p3d,
|
| 46 |
+
_unproject,
|
| 47 |
+
_build_view_fn,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# --- Robust data loading: a large multi-dataset loader WILL hit corrupt /
|
| 51 |
+
# truncated / missing files (e.g. a PNG half-written during a disk-full). One
|
| 52 |
+
# bad file must NEVER kill a run. We (1) tolerate mildly-truncated images,
|
| 53 |
+
# (2) force-decode on open so a hard-broken file raises WITH its path (logged
|
| 54 |
+
# once to _bad_files.log), and (3) retry another random sample in __getitem__. ---
|
| 55 |
+
import os as _os
|
| 56 |
+
from PIL import Image as _PILImage, ImageFile as _PILImageFile
|
| 57 |
+
|
| 58 |
+
_PILImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 59 |
+
from mvsam3d import paths as _mv_paths
|
| 60 |
+
_BAD_FILES_LOG = _mv_paths.BAD_FILES_LOG
|
| 61 |
+
_bad_seen: set = set()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _log_bad_file(path, err):
|
| 65 |
+
k = str(path)
|
| 66 |
+
if k in _bad_seen:
|
| 67 |
+
return
|
| 68 |
+
_bad_seen.add(k)
|
| 69 |
+
try:
|
| 70 |
+
with open(_BAD_FILES_LOG, "a") as _fh:
|
| 71 |
+
_fh.write(f"{k}\t{type(err).__name__}: {err}\n")
|
| 72 |
+
except Exception:
|
| 73 |
+
pass
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
_orig_pil_open = _PILImage.open
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _safe_pil_open(fp, *a, **k):
|
| 80 |
+
"""Force-decode on open so a broken/truncated image raises here (with its
|
| 81 |
+
path) rather than deep inside np.asarray/convert with no context."""
|
| 82 |
+
try:
|
| 83 |
+
im = _orig_pil_open(fp, *a, **k)
|
| 84 |
+
im.load()
|
| 85 |
+
return im
|
| 86 |
+
except Exception as _e:
|
| 87 |
+
_log_bad_file(getattr(fp, "name", fp), _e)
|
| 88 |
+
raise
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
_PILImage.open = _safe_pil_open # every adapter uses the shared PIL.Image.open
|
| 92 |
+
|
| 93 |
+
_D = np.diag([-1.0, -1.0, 1.0]).astype(np.float64)
|
| 94 |
+
MIN_VALID_PM_SCALE = 1e-6
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@dataclass
|
| 98 |
+
class ViewSample:
|
| 99 |
+
"""One training unit. ``anchor`` and ``aux_pool`` are adapter-defined view
|
| 100 |
+
refs; ``obj_key`` is the canonical-latent cache key."""
|
| 101 |
+
anchor: Any
|
| 102 |
+
aux_pool: List[Any]
|
| 103 |
+
obj_key: str
|
| 104 |
+
meta: dict = field(default_factory=dict)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class PoseDatasetAdapter(ABC):
|
| 108 |
+
"""Minimal dataset-specific interface. Everything returned is in the UNIFIED
|
| 109 |
+
contract: OpenCV, meters, depth registered to RGB."""
|
| 110 |
+
|
| 111 |
+
name: str = "base"
|
| 112 |
+
|
| 113 |
+
@abstractmethod
|
| 114 |
+
def list_samples(self) -> List[ViewSample]:
|
| 115 |
+
...
|
| 116 |
+
|
| 117 |
+
@abstractmethod
|
| 118 |
+
def load_view(self, ref: Any) -> Dict[str, Any]:
|
| 119 |
+
"""Return {'rgb':[H,W,3] float[0,1], 'mask':[H,W] float{0,1},
|
| 120 |
+
'depth_m':[H,W] float meters (registered to rgb), 'intr':{fx,fy,ppx,ppy}}."""
|
| 121 |
+
...
|
| 122 |
+
|
| 123 |
+
@abstractmethod
|
| 124 |
+
def extrinsic_cv(self, ref: Any) -> Dict[str, np.ndarray]:
|
| 125 |
+
"""Return {'R':(3,3),'t':(3,)} tag/world->cam, OpenCV, meters."""
|
| 126 |
+
...
|
| 127 |
+
|
| 128 |
+
@abstractmethod
|
| 129 |
+
def object_pose_cv(self, sample: ViewSample) -> Tuple[np.ndarray, np.ndarray]:
|
| 130 |
+
"""Return (R_cv, t_cv): object(CAD)->camera in the ANCHOR view, OpenCV,
|
| 131 |
+
meters. x_cam = R_cv @ p_cad + t_cv."""
|
| 132 |
+
...
|
| 133 |
+
|
| 134 |
+
@abstractmethod
|
| 135 |
+
def canonical(self, obj_key: str) -> dict:
|
| 136 |
+
"""Return the cached canonical dict: {latent[8,16,16,16],
|
| 137 |
+
occupancy[64,64,64] or [1,...], max_extent float, center_cad (3,)}."""
|
| 138 |
+
...
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def relative_p3d_from_ext(anchor_ext: Dict[str, np.ndarray],
|
| 142 |
+
aux_ext: Dict[str, np.ndarray]):
|
| 143 |
+
"""(Rrel, trel) mapping a PyTorch3D point in aux cam -> anchor cam.
|
| 144 |
+
|
| 145 |
+
IDENTICAL formula to dexycb_calib.relative_p3d, but taking the two OpenCV
|
| 146 |
+
tag/world->cam extrinsics directly (works for both synced-rig and
|
| 147 |
+
static-scene-moving-cam regimes)."""
|
| 148 |
+
Ra, ta = anchor_ext["R"], anchor_ext["t"]
|
| 149 |
+
Rb, tb = aux_ext["R"], aux_ext["t"]
|
| 150 |
+
RaRbT = Ra @ Rb.T
|
| 151 |
+
Rrel = _D @ RaRbT @ _D
|
| 152 |
+
trel = _D @ ta - _D @ (RaRbT @ tb)
|
| 153 |
+
return Rrel.astype(np.float64), trel.astype(np.float64)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def _transform_pointmap(pm: np.ndarray, Rrel: np.ndarray, trel: np.ndarray) -> np.ndarray:
|
| 157 |
+
H, W, _ = pm.shape
|
| 158 |
+
flat = pm.reshape(-1, 3)
|
| 159 |
+
valid = np.isfinite(flat).all(axis=1)
|
| 160 |
+
out = flat.copy()
|
| 161 |
+
out[valid] = flat[valid] @ Rrel.T + trel
|
| 162 |
+
return out.reshape(H, W, 3)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class MultiViewPoseDataset(torch.utils.data.Dataset):
|
| 166 |
+
"""Adapter-driven multi-view 6D-pose dataset. Behaviourally identical to
|
| 167 |
+
MultiViewDexYCB when driven by DexYCBAdapter."""
|
| 168 |
+
|
| 169 |
+
def __init__(self, adapter: PoseDatasetAdapter, *,
|
| 170 |
+
image_size: int = IMG_SIZE,
|
| 171 |
+
random_views: bool = False,
|
| 172 |
+
num_aux_views: int = 7,
|
| 173 |
+
min_aux_views: int = 1,
|
| 174 |
+
max_aux_views: int = 3,
|
| 175 |
+
depth_near: float = 0.1,
|
| 176 |
+
depth_far: float = 3.0,
|
| 177 |
+
ssi_mode: str = "scene"):
|
| 178 |
+
self.adapter = adapter
|
| 179 |
+
self.image_size = image_size
|
| 180 |
+
self.random_views = random_views
|
| 181 |
+
self.num_aux = num_aux_views
|
| 182 |
+
self.min_aux = min_aux_views
|
| 183 |
+
self.max_aux = max_aux_views
|
| 184 |
+
self.depth_near = depth_near
|
| 185 |
+
self.depth_far = depth_far
|
| 186 |
+
if ssi_mode not in ("scene", "object_centric"):
|
| 187 |
+
raise ValueError(f"ssi_mode must be 'scene' or 'object_centric', got {ssi_mode!r}")
|
| 188 |
+
self.ssi_mode = ssi_mode
|
| 189 |
+
if random_views and not (1 <= min_aux_views <= max_aux_views):
|
| 190 |
+
raise ValueError(f"random_views requires 1<=min_aux<=max_aux, got "
|
| 191 |
+
f"{min_aux_views}..{max_aux_views}")
|
| 192 |
+
self.samples: List[ViewSample] = adapter.list_samples()
|
| 193 |
+
|
| 194 |
+
def __len__(self):
|
| 195 |
+
return len(self.samples)
|
| 196 |
+
|
| 197 |
+
def _anchor_pm(self, ref):
|
| 198 |
+
v = self.adapter.load_view(ref)
|
| 199 |
+
pm = _to_p3d(_unproject(v["depth_m"], v["intr"], self.depth_near, self.depth_far))
|
| 200 |
+
return v, pm
|
| 201 |
+
|
| 202 |
+
def __getitem__(self, idx: int) -> dict:
|
| 203 |
+
# Robust wrapper: if a sample references a corrupt/truncated/missing file,
|
| 204 |
+
# log it once and fall back to another random sample — never crash the run.
|
| 205 |
+
n = len(self.samples)
|
| 206 |
+
for _try in range(24):
|
| 207 |
+
j = idx if _try == 0 else random.randrange(n)
|
| 208 |
+
try:
|
| 209 |
+
return self._getitem_impl(j)
|
| 210 |
+
except Exception as _e: # noqa: BLE001 - any decode/IO failure -> skip sample
|
| 211 |
+
_log_bad_file(f"[sample idx={j} adapter={getattr(self.adapter, 'name', '?')}]", _e)
|
| 212 |
+
# Exhausted retries: surface the error (don't hide a genuine bug forever).
|
| 213 |
+
return self._getitem_impl(random.randrange(n))
|
| 214 |
+
|
| 215 |
+
def _getitem_impl(self, idx: int) -> dict:
|
| 216 |
+
s = self.samples[idx]
|
| 217 |
+
|
| 218 |
+
# ---- anchor view + shared SSI from anchor pointmap ----
|
| 219 |
+
a_view, a_pm = self._anchor_pm(s.anchor)
|
| 220 |
+
shift, scale = _ssi_params(a_pm, mask=a_view["mask"], mode=self.ssi_mode)
|
| 221 |
+
anchor_ext = self.adapter.extrinsic_cv(s.anchor)
|
| 222 |
+
|
| 223 |
+
views = [_build_view_fn(a_view["rgb"], a_view["mask"],
|
| 224 |
+
_apply_ssi(a_pm, shift, scale), True, self.image_size)]
|
| 225 |
+
views[0]["cam"] = _cam_feature(np.eye(3), np.zeros(3), shift, scale)
|
| 226 |
+
|
| 227 |
+
# ---- choose aux views ----
|
| 228 |
+
pool = s.aux_pool
|
| 229 |
+
if not pool:
|
| 230 |
+
aux_refs = [] # anchor-only sample (no other views available)
|
| 231 |
+
elif self.random_views:
|
| 232 |
+
k = random.randint(min(self.min_aux, len(pool)), min(self.max_aux, len(pool)))
|
| 233 |
+
aux_refs = random.sample(pool, k)
|
| 234 |
+
else:
|
| 235 |
+
aux_refs = pool[: self.num_aux]
|
| 236 |
+
|
| 237 |
+
for aux in aux_refs:
|
| 238 |
+
x = self.adapter.load_view(aux)
|
| 239 |
+
x_pm = _to_p3d(_unproject(x["depth_m"], x["intr"], self.depth_near, self.depth_far))
|
| 240 |
+
Rrel, trel = relative_p3d_from_ext(anchor_ext, self.adapter.extrinsic_cv(aux))
|
| 241 |
+
x_pm = _transform_pointmap(x_pm, Rrel, trel)
|
| 242 |
+
av = _build_view_fn(x["rgb"], x["mask"], _apply_ssi(x_pm, shift, scale),
|
| 243 |
+
False, self.image_size)
|
| 244 |
+
av["cam"] = _cam_feature(Rrel, trel, shift, scale)
|
| 245 |
+
views.append(av)
|
| 246 |
+
|
| 247 |
+
# ---- GT layout (anchor cam, PyTorch3D) ----
|
| 248 |
+
R_cv, t_cv = self.adapter.object_pose_cv(s)
|
| 249 |
+
R_p3d, t_p3d = _D @ np.asarray(R_cv, np.float64), _D @ np.asarray(t_cv, np.float64)
|
| 250 |
+
canon = self.adapter.canonical(s.obj_key)
|
| 251 |
+
R, t, sc = canonical.gt_layout_anchor_frame(
|
| 252 |
+
R_p3d, t_p3d, np.asarray(canon["center_cad"]), float(canon["max_extent"]))
|
| 253 |
+
rot6d = normalize_rot6d(matrix_to_rot6d(torch.from_numpy(R).float())).float()
|
| 254 |
+
|
| 255 |
+
# ---- SSI-consistent pose targets (mirror dexycb_dataset exactly) ----
|
| 256 |
+
shift_t = torch.from_numpy(np.asarray(shift, np.float64)).float()
|
| 257 |
+
pm_scale_safe = torch.tensor(float(scale), dtype=torch.float32).clamp_min(MIN_VALID_PM_SCALE)
|
| 258 |
+
t_ssi = (torch.from_numpy(t).float() - shift_t) / pm_scale_safe
|
| 259 |
+
gt_scale_m = torch.tensor(float(sc), dtype=torch.float32)
|
| 260 |
+
gt_scale_ratio = (gt_scale_m / pm_scale_safe).clamp_min(MIN_VALID_PM_SCALE)
|
| 261 |
+
s_log3 = torch.log(gt_scale_ratio).expand(3).contiguous()
|
| 262 |
+
|
| 263 |
+
return {
|
| 264 |
+
"shape_latent": canon["latent"].float(),
|
| 265 |
+
"rot6d": rot6d,
|
| 266 |
+
"translation": t_ssi,
|
| 267 |
+
"scale": s_log3,
|
| 268 |
+
"pm_shift": shift_t,
|
| 269 |
+
"pm_scale": pm_scale_safe.reshape(1),
|
| 270 |
+
"views": views,
|
| 271 |
+
"object_name": s.obj_key,
|
| 272 |
+
}
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/canonical.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical-voxel construction and anchor-frame GT layout math.
|
| 2 |
+
|
| 3 |
+
The occupancy is produced by the VENDORED ``voxelize_mesh`` (byte-identical to
|
| 4 |
+
SAM-3D's ``inference_utils.voxelize_mesh``), so the SS encoder sees exactly the
|
| 5 |
+
distribution it was trained on. ``voxelize_mesh`` operates in the ROTATED
|
| 6 |
+
(z-up→y-up) frame and returns that frame's ``scale = 1/max_extent`` and
|
| 7 |
+
``center`` (the rotated-frame AABB center). Per the locked design we recover
|
| 8 |
+
the CAD-frame quantities the GT layout consumes:
|
| 9 |
+
|
| 10 |
+
max_extent = 1 / scale (rotation-invariant: M is axis-aligned)
|
| 11 |
+
center_cad = M^T @ center_rot (rotated-frame center → CAD frame)
|
| 12 |
+
|
| 13 |
+
GT layout (anchor camera, PyTorch3D). A canonical voxel point ``v_canon`` maps
|
| 14 |
+
to the camera as ``X_cam = R @ (s · v_canon) + t``. Composing the voxelization
|
| 15 |
+
with the DexYCB object pose ``X_cam = R_dex @ p_cad + t_dex`` gives:
|
| 16 |
+
|
| 17 |
+
s = max_extent (isotropic scalar)
|
| 18 |
+
R = R_dex @ M^T (absorbs the z-up→y-up grid rotation)
|
| 19 |
+
t = R_dex @ center_cad + t_dex
|
| 20 |
+
|
| 21 |
+
with ``M = [[1,0,0],[0,0,-1],[0,1,0]]`` (the grid rotation baked into
|
| 22 |
+
``voxelize_mesh``) and the OpenCV→PyTorch3D flip ``D = diag(-1,-1,1)`` applied to
|
| 23 |
+
``(R_dex, t_dex)`` upstream in the dataset.
|
| 24 |
+
"""
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
from sam3d_objects.pipeline.inference_utils import trimesh2o3d_mesh, voxelize_mesh
|
| 30 |
+
|
| 31 |
+
# Fixed z-up → y-up rotation applied inside voxelize_mesh:
|
| 32 |
+
# verts are right-multiplied by M.T, i.e. v_rot = v_cad @ M.T <=> v_rot = M @ v_cad.
|
| 33 |
+
M_ZUP_TO_YUP = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]], dtype=np.float64)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def voxelize_canonical(mesh, resolution: int = 64):
|
| 37 |
+
"""CAD trimesh → (occupancy ``[1,R,R,R]`` long, max_extent float, center_cad (3,)).
|
| 38 |
+
|
| 39 |
+
``occupancy`` is exactly the vendored ``voxelize_mesh`` output. ``max_extent``
|
| 40 |
+
and ``center_cad`` are recovered in the ORIGINAL CAD frame from the rotated-
|
| 41 |
+
frame ``scale``/``center`` ``voxelize_mesh`` returns, and are what the GT
|
| 42 |
+
layout math consumes.
|
| 43 |
+
"""
|
| 44 |
+
# voxelize_mesh consumes an open3d TriangleMesh (it assigns
|
| 45 |
+
# o3d.utility.Vector3dVector to mesh.vertices and calls the o3d voxelizer),
|
| 46 |
+
# so convert via the vendored trimesh2o3d_mesh first — exactly as the fork's
|
| 47 |
+
# pipeline does. trimesh2o3d_mesh builds a fresh o3d mesh, so the input
|
| 48 |
+
# trimesh is not mutated.
|
| 49 |
+
o3d_mesh = trimesh2o3d_mesh(mesh)
|
| 50 |
+
ss, scale, center_rot = voxelize_mesh(o3d_mesh, resolution=resolution)
|
| 51 |
+
if scale is None or center_rot is None:
|
| 52 |
+
# Mesh was already normalized to [-0.5, 0.5]; recover stats from raw verts.
|
| 53 |
+
verts = np.asarray(mesh.vertices, dtype=np.float64)
|
| 54 |
+
verts = verts @ M_ZUP_TO_YUP.T # voxelize_mesh works in the rotated frame
|
| 55 |
+
vmin, vmax = verts.min(axis=0), verts.max(axis=0)
|
| 56 |
+
center_rot = (vmax + vmin) / 2.0
|
| 57 |
+
max_extent = float(np.max(vmax - vmin))
|
| 58 |
+
else:
|
| 59 |
+
max_extent = 1.0 / float(scale)
|
| 60 |
+
center_rot = np.asarray(center_rot, dtype=np.float64)
|
| 61 |
+
# center_rot is in the rotated (y-up) frame; bring it back to the CAD frame.
|
| 62 |
+
center_cad = M_ZUP_TO_YUP.T @ center_rot
|
| 63 |
+
return ss, max_extent, center_cad.astype(np.float64)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def gt_layout_anchor_frame(R_dex: np.ndarray, t_dex: np.ndarray,
|
| 67 |
+
center_cad: np.ndarray, max_extent: float):
|
| 68 |
+
"""DexYCB object pose (anchor cam frame) → canonical (R, t, s).
|
| 69 |
+
|
| 70 |
+
R_dex (3,3), t_dex (3,): object→camera pose in the anchor camera frame
|
| 71 |
+
(already in the desired PyTorch3D convention). Returns R (3,3), t (3,),
|
| 72 |
+
s (float).
|
| 73 |
+
"""
|
| 74 |
+
R = R_dex @ M_ZUP_TO_YUP.T
|
| 75 |
+
t = R_dex @ np.asarray(center_cad, np.float64) + np.asarray(t_dex, np.float64)
|
| 76 |
+
return R.astype(np.float64), t.astype(np.float64), float(max_extent)
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/dexycb_calib.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DexYCB camera calibration: per-camera extrinsics/intrinsics and the
|
| 2 |
+
cross-camera transform that re-expresses an aux camera's pointmap and pose in
|
| 3 |
+
the ANCHOR camera frame.
|
| 4 |
+
|
| 5 |
+
DexYCB stores, per extrinsics group, a 3x4 ``[R_c | t_c]`` (row-major, 12
|
| 6 |
+
floats) mapping the shared calibration/tag frame -> camera ``c`` in the
|
| 7 |
+
**OpenCV** convention (x-right, y-down, z-fwd). Exactly one camera is identity
|
| 8 |
+
(the master/tag). The whole training pipeline works in **PyTorch3D** frame
|
| 9 |
+
(x-left, y-up, z-fwd), related by ``D = diag(-1,-1,1)`` (``D == D^{-1}``).
|
| 10 |
+
|
| 11 |
+
For a 3D point in aux cam B (PyTorch3D) the anchor-frame point is:
|
| 12 |
+
|
| 13 |
+
X_tag = R_B^T (D X_B^p3d - t_B)
|
| 14 |
+
X_A^p3d = D (R_A X_tag + t_A)
|
| 15 |
+
= (D R_A R_B^T D) X_B^p3d + (D t_A - D R_A R_B^T t_B)
|
| 16 |
+
|
| 17 |
+
i.e. a rigid transform ``X_A = Rrel @ X_B + trel`` with
|
| 18 |
+
``Rrel = D R_A R_B^T D`` and ``trel = D t_A - D R_A R_B^T t_B``.
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import os
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import yaml
|
| 26 |
+
|
| 27 |
+
_D = np.diag([-1.0, -1.0, 1.0]).astype(np.float64)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _load_yaml(path):
|
| 31 |
+
with open(path) as f:
|
| 32 |
+
return yaml.load(f, Loader=yaml.UnsafeLoader) # files contain python/tuple tags
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_extrinsics(calib_root: str, extrinsics_tag: str) -> dict[str, dict]:
|
| 36 |
+
"""serial -> {"R": (3,3), "t": (3,)} (tag->cam, OpenCV)."""
|
| 37 |
+
path = os.path.join(calib_root, f"extrinsics_{extrinsics_tag}", "extrinsics.yml")
|
| 38 |
+
raw = _load_yaml(path)["extrinsics"]
|
| 39 |
+
out = {}
|
| 40 |
+
for serial, vals in raw.items():
|
| 41 |
+
m = np.asarray(vals, dtype=np.float64).reshape(3, 4)
|
| 42 |
+
out[str(serial)] = {"R": m[:, :3], "t": m[:, 3]}
|
| 43 |
+
return out
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def load_intrinsics(calib_root: str, serial: str) -> dict:
|
| 47 |
+
"""color intrinsics fx,fy,ppx,ppy for a serial (640x480)."""
|
| 48 |
+
path = os.path.join(calib_root, "intrinsics", f"{serial}_640x480.yml")
|
| 49 |
+
return _load_yaml(path)["color"]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def relative_p3d(ext: dict, anchor_serial: str, aux_serial: str):
|
| 53 |
+
"""Return (Rrel, trel) mapping a PyTorch3D point in ``aux`` cam -> anchor cam.
|
| 54 |
+
|
| 55 |
+
X_anchor = Rrel @ X_aux + trel.
|
| 56 |
+
"""
|
| 57 |
+
Ra, ta = ext[anchor_serial]["R"], ext[anchor_serial]["t"]
|
| 58 |
+
Rb, tb = ext[aux_serial]["R"], ext[aux_serial]["t"]
|
| 59 |
+
RaRbT = Ra @ Rb.T
|
| 60 |
+
Rrel = _D @ RaRbT @ _D
|
| 61 |
+
trel = _D @ ta - _D @ (RaRbT @ tb)
|
| 62 |
+
return Rrel.astype(np.float64), trel.astype(np.float64)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def transform_pointmap_to_anchor(pm: np.ndarray, Rrel: np.ndarray, trel: np.ndarray) -> np.ndarray:
|
| 66 |
+
"""Apply the rigid transform to an (H,W,3) pointmap, preserving NaN/invalid.
|
| 67 |
+
|
| 68 |
+
pm is in the aux camera's PyTorch3D frame; returns it in the anchor frame.
|
| 69 |
+
"""
|
| 70 |
+
H, W, _ = pm.shape
|
| 71 |
+
flat = pm.reshape(-1, 3)
|
| 72 |
+
valid = np.isfinite(flat).all(axis=1)
|
| 73 |
+
out = flat.copy()
|
| 74 |
+
out[valid] = flat[valid] @ Rrel.T + trel
|
| 75 |
+
return out.reshape(H, W, 3)
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/dexycb_dataset.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Multi-view (multi-camera) DexYCB dataset for the 6D-pose flow model.
|
| 2 |
+
|
| 3 |
+
One sample = one (sequence, timestamp, object, anchor-camera). The anchor
|
| 4 |
+
camera defines the reference frame; the other synchronized cameras are aux
|
| 5 |
+
views. Everything is expressed in the **anchor camera's PyTorch3D frame**:
|
| 6 |
+
|
| 7 |
+
* Pointmaps are unprojected from each camera's aligned depth (metric, OpenCV),
|
| 8 |
+
converted to PyTorch3D (X' = diag(-1,-1,1) X), and aux cameras are rigidly
|
| 9 |
+
transformed into the anchor frame via DexYCB calibration extrinsics BEFORE a
|
| 10 |
+
single shared SSI (scale-shift invariant) normalization computed on the
|
| 11 |
+
anchor view. Pointmaps deliberately KEEP NaN at invalid/background pixels.
|
| 12 |
+
* GT (R, t, s) is the anchor camera's object pose, OpenCV→PyTorch3D, mapped to
|
| 13 |
+
the canonical voxel frame (R_dex·Mᵀ, R_dex·c_cad + t_dex, max_extent) — see
|
| 14 |
+
canonical.gt_layout_anchor_frame.
|
| 15 |
+
* The 6D rotation is ``normalize_rot6d(matrix_to_rot6d(R))`` using the
|
| 16 |
+
VENDORED column-convention rotation utils (consistent with the model's
|
| 17 |
+
decode ``rot6d_to_matrix(denormalize_rot6d(·))``).
|
| 18 |
+
* The shape target is the precomputed canonical SS latent for the object
|
| 19 |
+
(cache/canonical/<object>.pt).
|
| 20 |
+
|
| 21 |
+
There is NO shape-K/V; this dataset only emits plain conditioning tokens + flow
|
| 22 |
+
targets. Output dict feeds MVSam3DFlow.flow_step directly.
|
| 23 |
+
"""
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import glob
|
| 27 |
+
import os
|
| 28 |
+
import random
|
| 29 |
+
from dataclasses import dataclass
|
| 30 |
+
from typing import Dict, List, Optional
|
| 31 |
+
|
| 32 |
+
import numpy as np
|
| 33 |
+
import torch
|
| 34 |
+
import torch.nn.functional as F
|
| 35 |
+
from PIL import Image
|
| 36 |
+
from torch.utils.data import Dataset
|
| 37 |
+
|
| 38 |
+
from sam3d_objects.geometry.geometry import (
|
| 39 |
+
matrix_to_rot6d,
|
| 40 |
+
normalize_rot6d,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
from . import canonical, dexycb_calib
|
| 44 |
+
|
| 45 |
+
_D = np.diag([-1.0, -1.0, 1.0]).astype(np.float64)
|
| 46 |
+
DEPTH_SCALE = 1000.0 # DexYCB depth PNG is uint16 millimetres
|
| 47 |
+
IMG_SIZE = 518
|
| 48 |
+
DEPTH_NEAR, DEPTH_FAR = 0.1, 3.0 # metres; clip implausible depth
|
| 49 |
+
|
| 50 |
+
# DexYCB ycb_ids / seg use 1-based class indices (NOT the YCB numeric prefix).
|
| 51 |
+
# This is the canonical DexYCB _YCB_CLASSES ordering; seg == ycb_id for the object.
|
| 52 |
+
YCB_ID_TO_NAME = {
|
| 53 |
+
1: "002_master_chef_can", 2: "003_cracker_box", 3: "004_sugar_box",
|
| 54 |
+
4: "005_tomato_soup_can", 5: "006_mustard_bottle", 6: "007_tuna_fish_can",
|
| 55 |
+
7: "008_pudding_box", 8: "009_gelatin_box", 9: "010_potted_meat_can",
|
| 56 |
+
10: "011_banana", 11: "019_pitcher_base", 12: "021_bleach_cleanser",
|
| 57 |
+
13: "024_bowl", 14: "025_mug", 15: "035_power_drill", 16: "036_wood_block",
|
| 58 |
+
17: "037_scissors", 18: "040_large_marker", 19: "051_large_clamp",
|
| 59 |
+
20: "052_extra_large_clamp", 21: "061_foam_brick",
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@dataclass
|
| 64 |
+
class Sample:
|
| 65 |
+
seq_dir: str
|
| 66 |
+
frame: int
|
| 67 |
+
obj_id: int
|
| 68 |
+
obj_index: int # index into the sequence's ycb_ids / pose_y
|
| 69 |
+
anchor_serial: str
|
| 70 |
+
aux_serials: List[str] # FULL pool of non-anchor cameras; subset chosen per __getitem__
|
| 71 |
+
extrinsics_tag: str
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# --------------------------------------------------------------------------- #
|
| 75 |
+
# geometry helpers
|
| 76 |
+
# --------------------------------------------------------------------------- #
|
| 77 |
+
def _unproject(depth_m: np.ndarray, intr: dict,
|
| 78 |
+
depth_near: float = DEPTH_NEAR, depth_far: float = DEPTH_FAR) -> np.ndarray:
|
| 79 |
+
"""aligned depth (H,W) metres → (H,W,3) pointmap in OpenCV cam frame, NaN invalid."""
|
| 80 |
+
H, W = depth_m.shape
|
| 81 |
+
fx, fy, cx, cy = intr["fx"], intr["fy"], intr["ppx"], intr["ppy"]
|
| 82 |
+
us, vs = np.meshgrid(np.arange(W), np.arange(H))
|
| 83 |
+
z = depth_m
|
| 84 |
+
x = (us - cx) / fx * z
|
| 85 |
+
y = (vs - cy) / fy * z
|
| 86 |
+
pm = np.stack([x, y, z], axis=-1).astype(np.float64)
|
| 87 |
+
invalid = (z <= depth_near) | (z >= depth_far)
|
| 88 |
+
pm[invalid] = np.nan
|
| 89 |
+
return pm
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _to_p3d(pm_cv: np.ndarray) -> np.ndarray:
|
| 93 |
+
"""OpenCV pointmap → PyTorch3D (flip x,y)."""
|
| 94 |
+
return pm_cv * np.array([-1.0, -1.0, 1.0])
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
from mvsam3d import paths as _mv_paths
|
| 98 |
+
_FORK_ROOT = _mv_paths.FORK_ROOT
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _load_object_centric_ssi():
|
| 102 |
+
"""[raw SAM-3D] import the vendored ObjectCentricSSI from the fork.
|
| 103 |
+
|
| 104 |
+
This repo vendors a PARTIAL ``sam3d_objects`` subset that shadows the fork's
|
| 105 |
+
full package on sys.path, so ``sam3d_objects.data.dataset`` fails to import.
|
| 106 |
+
Fix: extend the partial packages' ``__path__`` with the fork's directories so
|
| 107 |
+
the missing subpackages resolve there (pure reuse — never reimplement).
|
| 108 |
+
Additive only: names that already resolve keep resolving to this repo."""
|
| 109 |
+
try:
|
| 110 |
+
from sam3d_objects.data.dataset.tdfy.img_and_mask_transforms import ObjectCentricSSI
|
| 111 |
+
return ObjectCentricSSI
|
| 112 |
+
except ModuleNotFoundError:
|
| 113 |
+
import sam3d_objects
|
| 114 |
+
import sam3d_objects.data
|
| 115 |
+
for pkg, sub in ((sam3d_objects, "sam3d_objects"),
|
| 116 |
+
(sam3d_objects.data, os.path.join("sam3d_objects", "data"))):
|
| 117 |
+
p = os.path.join(_FORK_ROOT, sub)
|
| 118 |
+
if p not in pkg.__path__:
|
| 119 |
+
pkg.__path__.append(p)
|
| 120 |
+
from sam3d_objects.data.dataset.tdfy.img_and_mask_transforms import ObjectCentricSSI
|
| 121 |
+
return ObjectCentricSSI
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _ssi_params(pm: np.ndarray, mask: np.ndarray | None = None, mode: str = "scene"):
|
| 125 |
+
"""SSI params from a pointmap. Two selectable conventions (``mode``):
|
| 126 |
+
|
| 127 |
+
* ``"scene"`` (default, used by all pre-2026-07-17 checkpoints):
|
| 128 |
+
shift=[0,0,median_z], scale=mean|p-shift| over the whole scene. Matches the
|
| 129 |
+
fork's BASE ``ScaleShiftInvariant.get_scale_and_shift`` default
|
| 130 |
+
(NOT ObjectCentricSSI).
|
| 131 |
+
* ``"object_centric"``: [raw SAM-3D] vendored ``ObjectCentricSSI``
|
| 132 |
+
(use_scene_scale=True) — shift = 3D median of the points INSIDE the object
|
| 133 |
+
``mask`` (object recentred to the origin), scale = scene scale. This is the
|
| 134 |
+
convention the release HF checkpoint's inference pipeline.yaml pairs with
|
| 135 |
+
the frozen condition encoder. Requires ``mask``.
|
| 136 |
+
|
| 137 |
+
A checkpoint must always be evaluated with the mode it was trained with —
|
| 138 |
+
t/s targets and the cam features live in this SSI space.
|
| 139 |
+
"""
|
| 140 |
+
if mode == "object_centric":
|
| 141 |
+
if mask is None:
|
| 142 |
+
raise ValueError("_ssi_params(mode='object_centric') requires the object mask")
|
| 143 |
+
ObjectCentricSSI = _load_object_centric_ssi()
|
| 144 |
+
pm_t = torch.from_numpy(np.ascontiguousarray(pm)).float().permute(2, 0, 1) # [3,H,W]
|
| 145 |
+
mask_t = torch.from_numpy(np.ascontiguousarray(mask)).float()[None] # [1,H,W]
|
| 146 |
+
scale_t, shift_t = ObjectCentricSSI(use_scene_scale=True)._compute_scale_and_shift(pm_t, mask_t)
|
| 147 |
+
shift = shift_t.numpy().astype(np.float64) # [3]
|
| 148 |
+
scale = max(float(scale_t.reshape(-1)[0]), 1e-6) # isotropic
|
| 149 |
+
return shift, scale
|
| 150 |
+
if mode != "scene":
|
| 151 |
+
raise ValueError(f"unknown ssi mode: {mode!r} (expected 'scene' or 'object_centric')")
|
| 152 |
+
flat = pm.reshape(-1, 3)
|
| 153 |
+
valid = np.isfinite(flat).all(axis=1)
|
| 154 |
+
if valid.sum() < 16:
|
| 155 |
+
return np.zeros(3), 1.0
|
| 156 |
+
pts = flat[valid]
|
| 157 |
+
shift = np.array([0.0, 0.0, np.median(pts[:, 2])])
|
| 158 |
+
scale = float(np.mean(np.abs(pts - shift)))
|
| 159 |
+
scale = max(scale, 1e-6)
|
| 160 |
+
return shift, scale
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _apply_ssi(pm: np.ndarray, shift, scale) -> np.ndarray:
|
| 164 |
+
return (pm - shift) / scale
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
# Per-view camera feature width fed to the model's mlp_cam: rot6d(6) + camera
|
| 168 |
+
# centre in the anchor frame, SSI-normalized (3).
|
| 169 |
+
CAM_FEAT_DIM = 9
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _cam_feature(Rrel: np.ndarray, trel: np.ndarray, shift, scale) -> torch.Tensor:
|
| 173 |
+
"""Pack a view's camera->anchor extrinsics into a 9-vector for ``mlp_cam``.
|
| 174 |
+
|
| 175 |
+
``[0:6]`` = column-convention rot6d of ``Rrel`` (the aux->anchor rotation),
|
| 176 |
+
using the SAME vendored ``matrix_to_rot6d`` as the object-rotation target so
|
| 177 |
+
the convention is consistent (NOT ``normalize_rot6d`` — that's tuned to the
|
| 178 |
+
object distribution; raw rot6d columns are already unit-scaled).
|
| 179 |
+
``[6:9]`` = the camera centre in the anchor frame (which is exactly ``trel``,
|
| 180 |
+
the image of the aux origin under ``X_anchor = Rrel X_aux + trel``), expressed
|
| 181 |
+
in the SAME SSI space as the conditioning pointmaps -> ``(trel - shift)/scale``.
|
| 182 |
+
|
| 183 |
+
Computed uniformly for every view: the anchor passes ``Rrel=I, trel=0`` (its
|
| 184 |
+
centre normalizes to ``-shift/scale``), so there is no special-casing.
|
| 185 |
+
"""
|
| 186 |
+
rot6d = matrix_to_rot6d(torch.from_numpy(np.asarray(Rrel, np.float64)).float()) # [6]
|
| 187 |
+
centre = torch.from_numpy(np.asarray(trel, np.float64)).float() # [3]
|
| 188 |
+
shift_t = torch.from_numpy(np.asarray(shift, np.float64)).float() # [3]
|
| 189 |
+
pos = (centre - shift_t) / float(scale) # [3]
|
| 190 |
+
return torch.cat([rot6d, pos]) # [9]
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _mask_bbox(mask: np.ndarray, pad: float = 0.1):
|
| 194 |
+
"""Square bbox around mask with padding → (r0,c0,r1,c1). Full frame if empty."""
|
| 195 |
+
H, W = mask.shape
|
| 196 |
+
ys, xs = np.where(mask > 0)
|
| 197 |
+
if ys.size == 0:
|
| 198 |
+
return 0, 0, H, W
|
| 199 |
+
r0, r1 = ys.min(), ys.max() + 1
|
| 200 |
+
c0, c1 = xs.min(), xs.max() + 1
|
| 201 |
+
h, w = r1 - r0, c1 - c0
|
| 202 |
+
side = max(h, w) * (1.0 + pad)
|
| 203 |
+
cy, cx = (r0 + r1) / 2.0, (c0 + c1) / 2.0
|
| 204 |
+
r0 = int(round(cy - side / 2)); r1 = int(round(cy + side / 2))
|
| 205 |
+
c0 = int(round(cx - side / 2)); c1 = int(round(cx + side / 2))
|
| 206 |
+
return r0, c0, r1, c1
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _crop_resize(arr: torch.Tensor, box, size: int, mode: str, fill: float):
|
| 210 |
+
"""arr [C,H,W] → [C,size,size]; crop to (possibly out-of-bounds) box then resize.
|
| 211 |
+
|
| 212 |
+
Out-of-bounds region is filled with `fill` (NaN-safe for pointmaps). Nearest
|
| 213 |
+
interpolation is used for pointmaps/masks so NaN is preserved (never averaged).
|
| 214 |
+
"""
|
| 215 |
+
C, H, W = arr.shape
|
| 216 |
+
r0, c0, r1, c1 = box
|
| 217 |
+
bh, bw = r1 - r0, c1 - c0
|
| 218 |
+
canvas = arr.new_full((C, bh, bw), fill)
|
| 219 |
+
sr0, sc0 = max(r0, 0), max(c0, 0)
|
| 220 |
+
sr1, sc1 = min(r1, H), min(c1, W)
|
| 221 |
+
if sr1 > sr0 and sc1 > sc0:
|
| 222 |
+
canvas[:, sr0 - r0:sr1 - r0, sc0 - c0:sc1 - c0] = arr[:, sr0:sr1, sc0:sc1]
|
| 223 |
+
interp = {"bilinear": "bilinear", "nearest": "nearest"}[mode]
|
| 224 |
+
kw = {"align_corners": False} if interp == "bilinear" else {}
|
| 225 |
+
out = F.interpolate(canvas[None], size=(size, size), mode=interp, **kw)[0]
|
| 226 |
+
return out
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def _full_resize(arr: torch.Tensor, size: int, mode: str, fill: float):
|
| 230 |
+
"""Pad [C,H,W] to square (centered) then resize to size."""
|
| 231 |
+
C, H, W = arr.shape
|
| 232 |
+
s = max(H, W)
|
| 233 |
+
canvas = arr.new_full((C, s, s), fill)
|
| 234 |
+
r0, c0 = (s - H) // 2, (s - W) // 2
|
| 235 |
+
canvas[:, r0:r0 + H, c0:c0 + W] = arr
|
| 236 |
+
interp = {"bilinear": "bilinear", "nearest": "nearest"}[mode]
|
| 237 |
+
kw = {"align_corners": False} if interp == "bilinear" else {}
|
| 238 |
+
return F.interpolate(canvas[None], size=(size, size), mode=interp, **kw)[0]
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# --------------------------------------------------------------------------- #
|
| 242 |
+
# dataset
|
| 243 |
+
# --------------------------------------------------------------------------- #
|
| 244 |
+
def _build_view_fn(rgb, mask, pm_anchor_frame, anchor: bool, image_size: int):
|
| 245 |
+
"""Module-level view construction (shared by MultiViewDexYCB._build_view and
|
| 246 |
+
the dataset-agnostic MultiViewPoseDataset). Byte-identical logic."""
|
| 247 |
+
img_t = torch.from_numpy(rgb).permute(2, 0, 1).float()
|
| 248 |
+
msk_t = torch.from_numpy(mask)[None].float()
|
| 249 |
+
pm_t = torch.from_numpy(pm_anchor_frame).permute(2, 0, 1).float()
|
| 250 |
+
box = _mask_bbox(mask)
|
| 251 |
+
out = {
|
| 252 |
+
"image_cropped": _crop_resize(img_t, box, image_size, "bilinear", 0.0),
|
| 253 |
+
"mask_cropped": _crop_resize(msk_t, box, image_size, "nearest", 0.0),
|
| 254 |
+
"pointmap_cropped": _crop_resize(pm_t, box, image_size, "nearest", float("nan")),
|
| 255 |
+
}
|
| 256 |
+
if anchor:
|
| 257 |
+
out["image_full"] = _full_resize(img_t, image_size, "bilinear", 0.0)
|
| 258 |
+
out["mask_full"] = _full_resize(msk_t, image_size, "nearest", 0.0)
|
| 259 |
+
out["pointmap_full"] = _full_resize(pm_t, image_size, "nearest", float("nan"))
|
| 260 |
+
return out
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
class MultiViewDexYCB(Dataset):
|
| 264 |
+
def __init__(
|
| 265 |
+
self,
|
| 266 |
+
dexycb_root: str = os.path.join(_mv_paths.DATA_ROOT, "dexycb"),
|
| 267 |
+
canonical_cache: str = _mv_paths.CANONICAL_CACHE,
|
| 268 |
+
num_aux_views: int = 7,
|
| 269 |
+
frame_stride: int = 5,
|
| 270 |
+
anchor_sweep: bool = True,
|
| 271 |
+
subjects: Optional[List[str]] = None,
|
| 272 |
+
image_size: int = IMG_SIZE,
|
| 273 |
+
random_views: bool = False,
|
| 274 |
+
min_aux_views: int = 1,
|
| 275 |
+
max_aux_views: int = 3,
|
| 276 |
+
depth_scale: float = DEPTH_SCALE,
|
| 277 |
+
depth_near: float = DEPTH_NEAR,
|
| 278 |
+
depth_far: float = DEPTH_FAR,
|
| 279 |
+
split: str = "train",
|
| 280 |
+
val_fraction: float = 0.0,
|
| 281 |
+
split_seed: int = 0,
|
| 282 |
+
ssi_mode: str = "scene",
|
| 283 |
+
):
|
| 284 |
+
"""When ``random_views`` is True, each __getitem__ draws a random aux count
|
| 285 |
+
``k ~ U[min_aux_views, max_aux_views]`` and a random subset of that size
|
| 286 |
+
from the non-anchor cameras (total views = 1 + k, i.e. 2..4 by default).
|
| 287 |
+
When False, the first ``num_aux_views`` aux cameras are used."""
|
| 288 |
+
self.root = dexycb_root
|
| 289 |
+
self.calib_root = os.path.join(dexycb_root, "calibration")
|
| 290 |
+
self.cache = canonical_cache
|
| 291 |
+
self.num_aux = num_aux_views
|
| 292 |
+
self.image_size = image_size
|
| 293 |
+
self.random_views = random_views
|
| 294 |
+
self.min_aux = min_aux_views
|
| 295 |
+
self.max_aux = max_aux_views
|
| 296 |
+
self.depth_scale = depth_scale
|
| 297 |
+
self.depth_near = depth_near
|
| 298 |
+
self.depth_far = depth_far
|
| 299 |
+
# SSI convention for pointmaps + t/s targets + cam features ("scene" |
|
| 300 |
+
# "object_centric"); validated eagerly so a config typo fails at startup.
|
| 301 |
+
if ssi_mode not in ("scene", "object_centric"):
|
| 302 |
+
raise ValueError(f"ssi_mode must be 'scene' or 'object_centric', got {ssi_mode!r}")
|
| 303 |
+
self.ssi_mode = ssi_mode
|
| 304 |
+
if random_views and not (1 <= min_aux_views <= max_aux_views):
|
| 305 |
+
raise ValueError(
|
| 306 |
+
f"random_views requires 1 <= min_aux_views ({min_aux_views}) "
|
| 307 |
+
f"<= max_aux_views ({max_aux_views})"
|
| 308 |
+
)
|
| 309 |
+
self._canon: Dict[str, dict] = {}
|
| 310 |
+
|
| 311 |
+
seq_dirs = sorted(glob.glob(os.path.join(dexycb_root, "2020*-subject-*", "2020*")))
|
| 312 |
+
if subjects is not None:
|
| 313 |
+
seq_dirs = [d for d in seq_dirs if any(s in d for s in subjects)]
|
| 314 |
+
|
| 315 |
+
self.samples: List[Sample] = []
|
| 316 |
+
for seq_dir in seq_dirs:
|
| 317 |
+
meta = dexycb_calib._load_yaml(os.path.join(seq_dir, "meta.yml"))
|
| 318 |
+
serials = [str(s) for s in meta["serials"]]
|
| 319 |
+
ycb_ids = list(meta["ycb_ids"])
|
| 320 |
+
tag = str(meta["extrinsics"])
|
| 321 |
+
n = int(meta["num_frames"])
|
| 322 |
+
anchors = serials if anchor_sweep else serials[:1]
|
| 323 |
+
for frame in range(0, n, frame_stride):
|
| 324 |
+
for anchor in anchors:
|
| 325 |
+
aux = [s for s in serials if s != anchor] # full pool
|
| 326 |
+
for oi, oid in enumerate(ycb_ids):
|
| 327 |
+
if int(oid) not in YCB_ID_TO_NAME:
|
| 328 |
+
continue
|
| 329 |
+
self.samples.append(
|
| 330 |
+
Sample(seq_dir, frame, int(oid), oi, anchor, aux, tag)
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
# ---- deterministic train/val split -------------------------------
|
| 334 |
+
# A fixed-seed shuffle partitions the full sample list once; the same
|
| 335 |
+
# (val_fraction, split_seed) yields the same disjoint train/val sets, so
|
| 336 |
+
# validation never touches a training sample. val_fraction=0 keeps all
|
| 337 |
+
# samples in "train" (no split).
|
| 338 |
+
if val_fraction > 0.0:
|
| 339 |
+
if not 0.0 < val_fraction < 1.0:
|
| 340 |
+
raise ValueError(f"val_fraction must be in (0,1), got {val_fraction}")
|
| 341 |
+
order = list(range(len(self.samples)))
|
| 342 |
+
random.Random(split_seed).shuffle(order)
|
| 343 |
+
n_val = max(1, int(round(len(order) * val_fraction)))
|
| 344 |
+
val_idx = set(order[:n_val])
|
| 345 |
+
if split == "val":
|
| 346 |
+
self.samples = [self.samples[i] for i in sorted(val_idx)]
|
| 347 |
+
elif split == "train":
|
| 348 |
+
self.samples = [self.samples[i] for i in range(len(self.samples)) if i not in val_idx]
|
| 349 |
+
else:
|
| 350 |
+
raise ValueError(f"split must be 'train' or 'val', got {split!r}")
|
| 351 |
+
|
| 352 |
+
def __len__(self):
|
| 353 |
+
return len(self.samples)
|
| 354 |
+
|
| 355 |
+
# ---- per-object canonical cache ----
|
| 356 |
+
def _canonical(self, obj_id: int) -> dict:
|
| 357 |
+
name = YCB_ID_TO_NAME[obj_id]
|
| 358 |
+
if name not in self._canon:
|
| 359 |
+
d = torch.load(os.path.join(self.cache, f"{name}.pt"), map_location="cpu",
|
| 360 |
+
weights_only=False)
|
| 361 |
+
self._canon[name] = d
|
| 362 |
+
return self._canon[name]
|
| 363 |
+
|
| 364 |
+
# ---- raw IO ----
|
| 365 |
+
def _load_color(self, seq_dir, serial, frame) -> np.ndarray:
|
| 366 |
+
p = os.path.join(seq_dir, serial, f"color_{frame:06d}.jpg")
|
| 367 |
+
return np.asarray(Image.open(p).convert("RGB"), dtype=np.float32) / 255.0
|
| 368 |
+
|
| 369 |
+
def _load_depth(self, seq_dir, serial, frame) -> np.ndarray:
|
| 370 |
+
p = os.path.join(seq_dir, serial, f"aligned_depth_to_color_{frame:06d}.png")
|
| 371 |
+
return np.asarray(Image.open(p), dtype=np.float32) / self.depth_scale
|
| 372 |
+
|
| 373 |
+
def _load_seg(self, seq_dir, serial, frame, obj_id) -> np.ndarray:
|
| 374 |
+
p = os.path.join(seq_dir, serial, f"labels_{frame:06d}.npz")
|
| 375 |
+
seg = np.load(p)["seg"]
|
| 376 |
+
return (seg == obj_id).astype(np.float32)
|
| 377 |
+
|
| 378 |
+
def _pose_y(self, seq_dir, serial, frame, obj_index):
|
| 379 |
+
p = os.path.join(seq_dir, serial, f"labels_{frame:06d}.npz")
|
| 380 |
+
py = np.load(p)["pose_y"][obj_index] # (3,4) OpenCV CAD->cam
|
| 381 |
+
return py[:, :3].astype(np.float64), py[:, 3].astype(np.float64)
|
| 382 |
+
|
| 383 |
+
# ---- view tensor construction ----
|
| 384 |
+
def _build_view(self, rgb, mask, pm_anchor_frame, anchor: bool):
|
| 385 |
+
"""rgb [H,W,3] in [0,1]; mask [H,W]; pm [H,W,3] (anchor frame, already SSI).
|
| 386 |
+
|
| 387 |
+
Returns a dict {image_cropped, mask_cropped, pointmap_cropped, [+ *_full]}.
|
| 388 |
+
"""
|
| 389 |
+
# Delegates to the module-level _build_view_fn (byte-identical). Pointmaps
|
| 390 |
+
# intentionally KEEP NaN at invalid/background pixels so the pretrained
|
| 391 |
+
# PointPatchEmbed's isfinite() valid_mask + invalid_xyz_token path works.
|
| 392 |
+
return _build_view_fn(rgb, mask, pm_anchor_frame, anchor, self.image_size)
|
| 393 |
+
|
| 394 |
+
def __getitem__(self, idx: int) -> dict:
|
| 395 |
+
s = self.samples[idx]
|
| 396 |
+
ext = dexycb_calib.load_extrinsics(self.calib_root, s.extrinsics_tag)
|
| 397 |
+
|
| 398 |
+
# ---- anchor view + shared SSI from anchor pointmap ----
|
| 399 |
+
a_rgb = self._load_color(s.seq_dir, s.anchor_serial, s.frame)
|
| 400 |
+
a_mask = self._load_seg(s.seq_dir, s.anchor_serial, s.frame, s.obj_id)
|
| 401 |
+
a_intr = dexycb_calib.load_intrinsics(self.calib_root, s.anchor_serial)
|
| 402 |
+
a_pm = _to_p3d(_unproject(self._load_depth(s.seq_dir, s.anchor_serial, s.frame), a_intr,
|
| 403 |
+
self.depth_near, self.depth_far))
|
| 404 |
+
shift, scale = _ssi_params(a_pm, mask=a_mask, mode=self.ssi_mode)
|
| 405 |
+
|
| 406 |
+
views = [self._build_view(a_rgb, a_mask, _apply_ssi(a_pm, shift, scale), anchor=True)]
|
| 407 |
+
# Anchor camera == the reference frame: relative transform is identity.
|
| 408 |
+
views[0]["cam"] = _cam_feature(np.eye(3), np.zeros(3), shift, scale)
|
| 409 |
+
|
| 410 |
+
# ---- choose which aux cameras to use (random count + subset, or fixed) ----
|
| 411 |
+
pool = s.aux_serials
|
| 412 |
+
if self.random_views:
|
| 413 |
+
k = random.randint(self.min_aux, min(self.max_aux, len(pool)))
|
| 414 |
+
aux_serials = random.sample(pool, k)
|
| 415 |
+
else:
|
| 416 |
+
aux_serials = pool[: self.num_aux]
|
| 417 |
+
|
| 418 |
+
# ---- aux views: transform pointmap into anchor frame, share SSI ----
|
| 419 |
+
for aux in aux_serials:
|
| 420 |
+
x_rgb = self._load_color(s.seq_dir, aux, s.frame)
|
| 421 |
+
x_mask = self._load_seg(s.seq_dir, aux, s.frame, s.obj_id)
|
| 422 |
+
x_intr = dexycb_calib.load_intrinsics(self.calib_root, aux)
|
| 423 |
+
x_pm = _to_p3d(_unproject(self._load_depth(s.seq_dir, aux, s.frame), x_intr,
|
| 424 |
+
self.depth_near, self.depth_far))
|
| 425 |
+
Rrel, trel = dexycb_calib.relative_p3d(ext, s.anchor_serial, aux)
|
| 426 |
+
x_pm = dexycb_calib.transform_pointmap_to_anchor(x_pm, Rrel, trel)
|
| 427 |
+
av = self._build_view(x_rgb, x_mask, _apply_ssi(x_pm, shift, scale), anchor=False)
|
| 428 |
+
# Bind this aux view to its camera pose (aux->anchor extrinsics).
|
| 429 |
+
av["cam"] = _cam_feature(Rrel, trel, shift, scale)
|
| 430 |
+
views.append(av)
|
| 431 |
+
|
| 432 |
+
# ---- GT layout (anchor cam, PyTorch3D) ----
|
| 433 |
+
R_cv, t_cv = self._pose_y(s.seq_dir, s.anchor_serial, s.frame, s.obj_index)
|
| 434 |
+
R_p3d, t_p3d = _D @ R_cv, _D @ t_cv # OpenCV → PyTorch3D
|
| 435 |
+
canon = self._canonical(s.obj_id)
|
| 436 |
+
R, t, sc = canonical.gt_layout_anchor_frame(
|
| 437 |
+
R_p3d, t_p3d, np.asarray(canon["center_cad"]), float(canon["max_extent"])
|
| 438 |
+
)
|
| 439 |
+
# 6D rotation: VENDORED column-convention encode, normalized to the
|
| 440 |
+
# pretrained backbone's distribution. Consistent with the model's decode
|
| 441 |
+
# rot6d_to_matrix(denormalize_rot6d(·)).
|
| 442 |
+
R_t = torch.from_numpy(R).float()
|
| 443 |
+
rot6d = normalize_rot6d(matrix_to_rot6d(R_t)).float() # [6]
|
| 444 |
+
|
| 445 |
+
# ---- SSI-consistent pose targets ----------------------------------
|
| 446 |
+
# Mirror the fork stage1 model_wrapper._build_pose_latents EXACTLY
|
| 447 |
+
# (sam3d-for-6d stage1 model_wrapper.py lines 560-581);
|
| 448 |
+
# the conditioning pointmaps were normalized by the shared anchor SSI
|
| 449 |
+
# (shift [3], scale scalar), so the t/s targets share that SSI space.
|
| 450 |
+
# Guards use the same constants as the fork:
|
| 451 |
+
# min_valid_pointmap_scale = 1e-6 (clamps pm_scale and the scale ratio)
|
| 452 |
+
# No mean/std on t or s (fork normalize=False; NOT the ScaleShiftInvariant
|
| 453 |
+
# pose-target/inference decoder). 3-channel isotropic scale latent.
|
| 454 |
+
MIN_VALID_PM_SCALE = 1e-6
|
| 455 |
+
shift_t = torch.from_numpy(np.asarray(shift, dtype=np.float64)).float() # [3]
|
| 456 |
+
# fork line 560: pm_scale_safe = pm_scale.float().clamp_min(min_valid_pointmap_scale)
|
| 457 |
+
pm_scale_safe = torch.tensor(float(scale), dtype=torch.float32).clamp_min(MIN_VALID_PM_SCALE)
|
| 458 |
+
# fork line 561: gt_trans_ssi = (gt_trans_metric - pm_shift) / pm_scale_safe
|
| 459 |
+
t_ssi = (torch.from_numpy(t).float() - shift_t) / pm_scale_safe # [3]
|
| 460 |
+
# fork lines 580-581: gt_scale_ratio = (gt_scale_m / pm_scale_scalar).clamp_min(min_valid_pointmap_scale)
|
| 461 |
+
# gt_scale_log = log(gt_scale_ratio).expand(...,3)
|
| 462 |
+
gt_scale_m = torch.tensor(float(sc), dtype=torch.float32) # scalar metric extent
|
| 463 |
+
gt_scale_ratio = (gt_scale_m / pm_scale_safe).clamp_min(MIN_VALID_PM_SCALE)
|
| 464 |
+
s_log3 = torch.log(gt_scale_ratio).expand(3).contiguous() # [3] isotropic
|
| 465 |
+
|
| 466 |
+
return {
|
| 467 |
+
"shape_latent": canon["latent"].float(), # [8,16,16,16]
|
| 468 |
+
"rot6d": rot6d, # [6]
|
| 469 |
+
"translation": t_ssi, # [3] SSI space
|
| 470 |
+
"scale": s_log3, # [3] log-ratio, isotropic
|
| 471 |
+
"pm_shift": shift_t, # [3] SSI shift
|
| 472 |
+
"pm_scale": pm_scale_safe.reshape(1), # [1] SSI scale (clamped, fork-safe)
|
| 473 |
+
"views": views,
|
| 474 |
+
"object_name": YCB_ID_TO_NAME[s.obj_id],
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
def collate_multiview(batch: List[dict]) -> dict:
|
| 479 |
+
"""Stack a list of samples. ``views`` becomes a list (len=n_views) of dicts of
|
| 480 |
+
batched [B,C,H,W] tensors; the rest stack on dim 0.
|
| 481 |
+
|
| 482 |
+
Samples may carry different view counts (random-view training). All views at
|
| 483 |
+
a given position share the same group structure (position 0 = anchor with 6
|
| 484 |
+
groups, positions >=1 = aux with 3 cropped groups), so the batch is truncated
|
| 485 |
+
to the minimum view count present. At batch_size=1 this is a no-op."""
|
| 486 |
+
n_views = min(len(b["views"]) for b in batch)
|
| 487 |
+
out = {
|
| 488 |
+
"shape_latent": torch.stack([b["shape_latent"] for b in batch]),
|
| 489 |
+
"rot6d": torch.stack([b["rot6d"] for b in batch]),
|
| 490 |
+
"translation": torch.stack([b["translation"] for b in batch]),
|
| 491 |
+
"scale": torch.stack([b["scale"] for b in batch]),
|
| 492 |
+
"pm_shift": torch.stack([b["pm_shift"] for b in batch]),
|
| 493 |
+
"pm_scale": torch.stack([b["pm_scale"] for b in batch]),
|
| 494 |
+
"object_name": [b["object_name"] for b in batch],
|
| 495 |
+
}
|
| 496 |
+
views = []
|
| 497 |
+
for v in range(n_views):
|
| 498 |
+
keys = batch[0]["views"][v].keys()
|
| 499 |
+
views.append({k: torch.stack([b["views"][v][k] for b in batch]) for k in keys})
|
| 500 |
+
out["views"] = views
|
| 501 |
+
return out
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/integrated.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Integrated multi-dataset training set builder + held-out enforcement.
|
| 2 |
+
|
| 3 |
+
Reads a ``data.datasets`` list from the training config, instantiates one
|
| 4 |
+
MultiViewPoseDataset per entry (via its adapter), enforces the held-out manifest
|
| 5 |
+
(configs/heldout.yaml), concatenates, and provides a diversity-weighted sampler.
|
| 6 |
+
|
| 7 |
+
HELD-OUT SEMANTICS (two distinct kinds — the distinction is load-bearing):
|
| 8 |
+
* OOD datasets (ho3d, ycbm, hope): whole-dataset held-out for evaluation. Their
|
| 9 |
+
objects MAY overlap training objects (e.g. HO-3D is YCB). We do NOT enforce
|
| 10 |
+
object-disjointness for these — doing so would delete every YCB object from
|
| 11 |
+
DexYCB/YCB-V training. They are simply never built with split:train.
|
| 12 |
+
* unseen-object datasets (housecat6d, graspnet test_novel): held out AND their
|
| 13 |
+
object identities reserved — removed from EVERY train dataset (global
|
| 14 |
+
object-disjointness). Their objects are non-YCB, so this is safe.
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
from typing import Dict, List
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
import torch
|
| 23 |
+
import yaml
|
| 24 |
+
from torch.utils.data import ConcatDataset, WeightedRandomSampler
|
| 25 |
+
|
| 26 |
+
from .adapters import ADAPTERS
|
| 27 |
+
from .base_dataset import MultiViewPoseDataset
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
import inspect
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _adapter_kwargs(adapter_cls, entry: dict) -> dict:
|
| 34 |
+
"""Pass through only the entry keys the adapter's __init__ actually accepts.
|
| 35 |
+
`role` is ROUTING (train vs heldout) and is never passed to the adapter;
|
| 36 |
+
`data_split` maps to the adapter's on-disk `split` (test/train/val)."""
|
| 37 |
+
accepted = set(inspect.signature(adapter_cls.__init__).parameters) - {"self", "root", "canonical_cache"}
|
| 38 |
+
kw = {k: entry[k] for k in accepted if k in entry and k not in ("role", "data_split")}
|
| 39 |
+
if "split" in accepted and "data_split" in entry:
|
| 40 |
+
kw["split"] = entry["data_split"]
|
| 41 |
+
return kw
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def load_heldout(path: str | None) -> dict:
|
| 45 |
+
if not path or not os.path.exists(path):
|
| 46 |
+
return {"ood_datasets": [], "unseen_object_datasets": [],
|
| 47 |
+
"unseen_object_keys_extra": [], "never_train": []}
|
| 48 |
+
with open(path) as f:
|
| 49 |
+
return yaml.safe_load(f) or {}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def reserved_object_keys(heldout: dict, canonical_cache: str) -> set:
|
| 53 |
+
"""Union of obj_keys from unseen-object datasets + explicit extras.
|
| 54 |
+
These keys are removed from ALL training datasets."""
|
| 55 |
+
keys = set(heldout.get("unseen_object_keys_extra", []) or [])
|
| 56 |
+
for name in heldout.get("unseen_object_datasets", []) or []:
|
| 57 |
+
ds_root = heldout["roots"][name]
|
| 58 |
+
ad = ADAPTERS[name](ds_root, canonical_cache, **heldout.get("adapter_kwargs", {}).get(name, {}))
|
| 59 |
+
for s in ad.list_samples():
|
| 60 |
+
keys.add(s.obj_key)
|
| 61 |
+
return keys
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def build_integrated_dataset(data_cfg: dict, split: str, *, image_size: int,
|
| 65 |
+
random_views: bool, min_aux: int, max_aux: int,
|
| 66 |
+
num_aux: int, depth_near: float, depth_far: float,
|
| 67 |
+
ssi_mode: str = "scene",
|
| 68 |
+
heldout_path: str | None = None):
|
| 69 |
+
"""Build a ConcatDataset over the configured datasets for `split`, applying
|
| 70 |
+
held-out rules. Returns (dataset, per_sample_weights or None)."""
|
| 71 |
+
cache = data_cfg["canonical_cache"]
|
| 72 |
+
heldout = load_heldout(heldout_path)
|
| 73 |
+
never_train = set(heldout.get("never_train", []) or [])
|
| 74 |
+
reserved = reserved_object_keys(heldout, cache) if split == "train" else set()
|
| 75 |
+
|
| 76 |
+
parts: List[MultiViewPoseDataset] = []
|
| 77 |
+
weights: List[float] = []
|
| 78 |
+
report = []
|
| 79 |
+
for entry in data_cfg["datasets"]:
|
| 80 |
+
name = entry["name"]
|
| 81 |
+
adapter_name = entry.get("adapter", name)
|
| 82 |
+
role = entry.get("role", "train") # routing: "train" | "heldout"
|
| 83 |
+
# only build datasets whose role matches the request
|
| 84 |
+
if split == "train":
|
| 85 |
+
if role != "train" or name in never_train:
|
| 86 |
+
continue
|
| 87 |
+
else: # val / held-out
|
| 88 |
+
if role == "train":
|
| 89 |
+
continue
|
| 90 |
+
ad = ADAPTERS[adapter_name](entry["root"], cache, **_adapter_kwargs(ADAPTERS[adapter_name], entry))
|
| 91 |
+
ds = MultiViewPoseDataset(ad, image_size=image_size,
|
| 92 |
+
random_views=random_views if split == "train" else False,
|
| 93 |
+
num_aux_views=num_aux, min_aux_views=min_aux,
|
| 94 |
+
max_aux_views=max_aux, depth_near=depth_near, depth_far=depth_far,
|
| 95 |
+
ssi_mode=ssi_mode)
|
| 96 |
+
# object-disjointness filter (train only)
|
| 97 |
+
if split == "train" and reserved:
|
| 98 |
+
kept = [i for i, s in enumerate(ds.samples) if s.obj_key not in reserved]
|
| 99 |
+
ds.samples = [ds.samples[i] for i in kept]
|
| 100 |
+
if len(ds) == 0:
|
| 101 |
+
continue
|
| 102 |
+
parts.append(ds)
|
| 103 |
+
w = float(entry.get("weight", 1.0))
|
| 104 |
+
weights.extend([w / len(ds)] * len(ds)) # diversity weight: per-dataset mixing ratio
|
| 105 |
+
report.append(f"{name}:{len(ds)}(w={w})")
|
| 106 |
+
|
| 107 |
+
if not parts:
|
| 108 |
+
raise RuntimeError(f"No datasets for split={split}")
|
| 109 |
+
concat = ConcatDataset(parts)
|
| 110 |
+
print(f"[integrated] {split}: " + " ".join(report) + f" -> total {len(concat)}", flush=True)
|
| 111 |
+
sample_weights = torch.tensor(weights, dtype=torch.double) if split == "train" else None
|
| 112 |
+
return concat, sample_weights
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def make_weighted_sampler(sample_weights, *, world_size: int, rank: int, seed: int = 0):
|
| 116 |
+
"""Diversity-weighted sampler. For DDP, each rank draws an independent
|
| 117 |
+
weighted stream (seeded by rank) — the mixing ratio is preserved per rank."""
|
| 118 |
+
g = torch.Generator().manual_seed(seed + rank)
|
| 119 |
+
return WeightedRandomSampler(sample_weights, num_samples=len(sample_weights),
|
| 120 |
+
replacement=True, generator=g)
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/infer/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inference package for the multi-view 6D-pose flow model (v2).
|
| 2 |
+
|
| 3 |
+
Reusable, data-source-decoupled inference that mirrors the EXACT inference path
|
| 4 |
+
used by ``mvsam3d/train/validate.run_validation``:
|
| 5 |
+
|
| 6 |
+
* build the unwrapped ``MVSam3DFlow`` with the pretrained warm-start so the
|
| 7 |
+
FROZEN condition fuser + warm-started backbone are reconstructed, THEN
|
| 8 |
+
* restore the trained tensors (backbone + mlp_cam) with the vendored
|
| 9 |
+
``load_checkpoint`` (v2 checkpoints store ONLY trainable params), THEN
|
| 10 |
+
* run ``inner.predict(batch, num_steps=...)`` under ``eval()`` + AMP autocast
|
| 11 |
+
+ ``no_grad`` — byte-for-byte the same sampler/decoder validation uses.
|
| 12 |
+
"""
|
| 13 |
+
from .predict import ( # noqa: F401
|
| 14 |
+
build_inference_model,
|
| 15 |
+
load_config,
|
| 16 |
+
predict_pose,
|
| 17 |
+
)
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/infer/predict.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reusable eval-time inference for the multi-view 6D-pose flow model (v2).
|
| 2 |
+
|
| 3 |
+
This module is the single source of truth for "load a trained v2 checkpoint and
|
| 4 |
+
predict (R, t, s, shape) for a batch". It reproduces the inference path of
|
| 5 |
+
``mvsam3d/train/validate.run_validation`` EXACTLY:
|
| 6 |
+
|
| 7 |
+
* model construction mirrors ``mvsam3d/train/train.main`` —
|
| 8 |
+
``MVSam3DFlow(... pretrained_ckpt=<ss_generator.ckpt> ...)`` first (which
|
| 9 |
+
reconstructs the FROZEN condition fuser + warm-starts the backbone), then the
|
| 10 |
+
vendored ``load_checkpoint`` overwrites ONLY the trained tensors (backbone +
|
| 11 |
+
mlp_cam) on top of that warm-start. v2 checkpoints under cache/ckpt store
|
| 12 |
+
trainable params only, so the warm-start MUST happen first or the frozen
|
| 13 |
+
condition pathway would be left uninitialized.
|
| 14 |
+
* prediction mirrors ``run_validation``: ``inner.eval()``, the SAME AMP
|
| 15 |
+
autocast (bf16 on cuda by default), ``torch.no_grad()``, and a single
|
| 16 |
+
``inner.predict(batch, num_steps=...)`` call — same vendored Euler sampler,
|
| 17 |
+
same ``num_steps`` semantics, same ``decode_pose_*`` helpers.
|
| 18 |
+
|
| 19 |
+
The pose-prediction entrypoint ``predict_pose(model, batch, num_steps)`` is
|
| 20 |
+
deliberately DECOUPLED from any data source: it accepts an already-collated batch
|
| 21 |
+
dict (``views`` + ``pm_shift`` / ``pm_scale`` + ...), so an HO3D relative-pose
|
| 22 |
+
eval harness can import and call it with its own batches. The CLI below wires a
|
| 23 |
+
DexYCB batch source around it purely for the smoke test / standalone runs.
|
| 24 |
+
"""
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import argparse
|
| 28 |
+
import json
|
| 29 |
+
import os
|
| 30 |
+
from typing import Dict, List, Optional, Tuple
|
| 31 |
+
|
| 32 |
+
import torch
|
| 33 |
+
import yaml
|
| 34 |
+
|
| 35 |
+
from sam3d_objects.training.stage1_flow_lora.checkpointing import load_checkpoint
|
| 36 |
+
from mvsam3d.model.flow_model import MVSam3DFlow
|
| 37 |
+
from mvsam3d import paths as _paths
|
| 38 |
+
|
| 39 |
+
DEFAULT_CONFIG = _paths.DEFAULT_CONFIG
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# --------------------------------------------------------------------------- #
|
| 43 |
+
# config + small utils
|
| 44 |
+
# --------------------------------------------------------------------------- #
|
| 45 |
+
def load_config(path: str) -> dict:
|
| 46 |
+
"""Load the training YAML (same keys train.py reads)."""
|
| 47 |
+
with open(path) as f:
|
| 48 |
+
return _paths.expand(yaml.safe_load(f))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _move(obj, device):
|
| 52 |
+
"""Recursively move tensors in a (possibly nested) batch to ``device``."""
|
| 53 |
+
if torch.is_tensor(obj):
|
| 54 |
+
return obj.to(device, non_blocking=True)
|
| 55 |
+
if isinstance(obj, dict):
|
| 56 |
+
return {k: _move(v, device) for k, v in obj.items()}
|
| 57 |
+
if isinstance(obj, list):
|
| 58 |
+
return [_move(v, device) for v in obj]
|
| 59 |
+
return obj
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# --------------------------------------------------------------------------- #
|
| 63 |
+
# model construction + checkpoint restore (mirrors train.main order)
|
| 64 |
+
# --------------------------------------------------------------------------- #
|
| 65 |
+
def build_inference_model(
|
| 66 |
+
cfg: dict,
|
| 67 |
+
checkpoint_path: str,
|
| 68 |
+
device,
|
| 69 |
+
*,
|
| 70 |
+
verbose: bool = True,
|
| 71 |
+
) -> Tuple[MVSam3DFlow, Dict]:
|
| 72 |
+
"""Build ``MVSam3DFlow`` with the pretrained warm-start, then restore the
|
| 73 |
+
trained tensors from ``checkpoint_path`` with the vendored ``load_checkpoint``.
|
| 74 |
+
|
| 75 |
+
Returns ``(model_in_eval_mode, load_report)`` where ``load_report`` carries the
|
| 76 |
+
restored ``step`` plus the ``load_state_dict`` missing/unexpected key report.
|
| 77 |
+
|
| 78 |
+
Construction order is IDENTICAL to ``train.main``:
|
| 79 |
+
1. ``MVSam3DFlow(..., pretrained_ckpt=model_cfg['pretrained_ckpt'], ...)``
|
| 80 |
+
— reconstructs the frozen fuser + warm-starts the backbone.
|
| 81 |
+
2. vendored ``load_checkpoint(checkpoint_path, model)`` — overwrites the
|
| 82 |
+
trainable tensors (backbone + mlp_cam) on top of the warm-start.
|
| 83 |
+
"""
|
| 84 |
+
model_cfg = cfg["model"]
|
| 85 |
+
cond_cfg = cfg["condition"]
|
| 86 |
+
loss_cfg = cfg["loss_weights"]
|
| 87 |
+
data_cfg = cfg["data"]
|
| 88 |
+
|
| 89 |
+
# max_views derivation copied verbatim from train.main (API-compat only; the
|
| 90 |
+
# per-view bias now comes from mlp_cam, so this is not a hard upper bound).
|
| 91 |
+
max_aux = (data_cfg["max_aux_views"] if data_cfg["random_views"]
|
| 92 |
+
else data_cfg["num_aux_views"])
|
| 93 |
+
max_views = max_aux + 1
|
| 94 |
+
|
| 95 |
+
# (1) warm-start: pretrained_ckpt reconstructs frozen fuser + backbone torso.
|
| 96 |
+
model = MVSam3DFlow(
|
| 97 |
+
model_channels=model_cfg["model_channels"],
|
| 98 |
+
cond_channels=model_cfg["cond_channels"],
|
| 99 |
+
num_blocks=model_cfg["num_blocks"],
|
| 100 |
+
num_heads=model_cfg["num_heads"],
|
| 101 |
+
max_views=max_views,
|
| 102 |
+
loss_weights=loss_cfg,
|
| 103 |
+
sigma_min=model_cfg["sigma_min"],
|
| 104 |
+
time_scale=model_cfg["time_scale"],
|
| 105 |
+
timestep_sampler=model_cfg["timestep_sampler"],
|
| 106 |
+
timestep_lognorm_mean=model_cfg["timestep_lognorm_mean"],
|
| 107 |
+
timestep_lognorm_std=model_cfg["timestep_lognorm_std"],
|
| 108 |
+
pretrained_ckpt=model_cfg["pretrained_ckpt"],
|
| 109 |
+
condition_kwargs=cond_cfg,
|
| 110 |
+
).to(device)
|
| 111 |
+
|
| 112 |
+
# (2) restore TRAINABLES on top of the warm-start, via the vendored loader.
|
| 113 |
+
# We transiently wrap the model's load_state_dict to capture the
|
| 114 |
+
# IncompatibleKeys (missing/unexpected) that load_checkpoint discards, so we
|
| 115 |
+
# can report them. The actual restore is still done by the vendored
|
| 116 |
+
# load_checkpoint (single 1x checkpoint read; its m.load_state_dict call picks
|
| 117 |
+
# up the instance-level override below).
|
| 118 |
+
inner = model.module if hasattr(model, "module") else model
|
| 119 |
+
_orig_load = inner.load_state_dict
|
| 120 |
+
captured: Dict = {}
|
| 121 |
+
|
| 122 |
+
def _capturing_load(state_dict, strict=False, **kw):
|
| 123 |
+
res = _orig_load(state_dict, strict=strict, **kw)
|
| 124 |
+
captured["restored_tensors"] = len(state_dict)
|
| 125 |
+
captured["missing_keys"] = list(getattr(res, "missing_keys", []))
|
| 126 |
+
captured["unexpected_keys"] = list(getattr(res, "unexpected_keys", []))
|
| 127 |
+
return res
|
| 128 |
+
|
| 129 |
+
inner.load_state_dict = _capturing_load # instance attr shadows bound method
|
| 130 |
+
try:
|
| 131 |
+
step, history = load_checkpoint(
|
| 132 |
+
checkpoint_path=checkpoint_path, model=model, map_location="cpu",
|
| 133 |
+
)
|
| 134 |
+
finally:
|
| 135 |
+
inner.load_state_dict = _orig_load
|
| 136 |
+
|
| 137 |
+
model.eval() # same eval() mode validation uses for the sampler
|
| 138 |
+
|
| 139 |
+
# Sanity: every trainable param name should be present in the restored set
|
| 140 |
+
# (i.e. NOT in missing_keys). missing_keys here are the frozen fuser / DINO
|
| 141 |
+
# tensors that the trainable-only checkpoint legitimately does not carry.
|
| 142 |
+
trainable_names = {n for n, p in inner.named_parameters() if p.requires_grad}
|
| 143 |
+
missing_trainable = sorted(trainable_names & set(captured.get("missing_keys", [])))
|
| 144 |
+
|
| 145 |
+
report = {
|
| 146 |
+
"step": int(step),
|
| 147 |
+
"restored_tensors": captured.get("restored_tensors", 0),
|
| 148 |
+
"n_missing_keys": len(captured.get("missing_keys", [])),
|
| 149 |
+
"n_unexpected_keys": len(captured.get("unexpected_keys", [])),
|
| 150 |
+
"unexpected_keys": captured.get("unexpected_keys", []),
|
| 151 |
+
"n_trainable_params": len(trainable_names),
|
| 152 |
+
"missing_trainable_params": missing_trainable,
|
| 153 |
+
}
|
| 154 |
+
if verbose:
|
| 155 |
+
print(f"[infer] built MVSam3DFlow + warm-start from "
|
| 156 |
+
f"{model_cfg['pretrained_ckpt']}")
|
| 157 |
+
print(f"[infer] load_checkpoint({checkpoint_path}) -> step={report['step']}; "
|
| 158 |
+
f"restored {report['restored_tensors']} trainable tensors; "
|
| 159 |
+
f"{report['n_missing_keys']} keys not in ckpt (frozen fuser/DINO, "
|
| 160 |
+
f"expected); {report['n_unexpected_keys']} unexpected.")
|
| 161 |
+
if missing_trainable:
|
| 162 |
+
print(f"[infer][WARN] {len(missing_trainable)} TRAINABLE params were "
|
| 163 |
+
f"NOT restored (left at warm-start/init): {missing_trainable[:8]}"
|
| 164 |
+
+ (" ..." if len(missing_trainable) > 8 else ""))
|
| 165 |
+
else:
|
| 166 |
+
print("[infer] all trainable params restored from the checkpoint: OK")
|
| 167 |
+
return model, report
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# --------------------------------------------------------------------------- #
|
| 171 |
+
# the decoupled pose-prediction entrypoint
|
| 172 |
+
# --------------------------------------------------------------------------- #
|
| 173 |
+
@torch.no_grad()
|
| 174 |
+
def predict_pose(
|
| 175 |
+
model,
|
| 176 |
+
batch: Dict,
|
| 177 |
+
num_steps: int,
|
| 178 |
+
*,
|
| 179 |
+
amp_dtype: str = "bf16",
|
| 180 |
+
device=None,
|
| 181 |
+
) -> Dict[str, torch.Tensor]:
|
| 182 |
+
"""Run the model's own rectified-flow sampler on one collated ``batch`` and
|
| 183 |
+
return the decoded metric-space pose + shape latent.
|
| 184 |
+
|
| 185 |
+
Wraps ``inner.predict(batch, num_steps=...)`` EXACTLY as
|
| 186 |
+
``validate.run_validation`` does: unwrap to ``inner``, ``inner.eval()``, the
|
| 187 |
+
same AMP autocast (bf16 on cuda by default; matches train/val ``amp_dtype``),
|
| 188 |
+
and ``no_grad``. Outputs are cast to float32 to match the metric space the
|
| 189 |
+
validation metrics are computed in (``pred[...].float()``).
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
model: an ``MVSam3DFlow`` (or DDP-wrapped); unwrapped internally.
|
| 193 |
+
batch: an already-collated batch dict with at least ``views``, ``pm_shift``,
|
| 194 |
+
``pm_scale`` (the data source is irrelevant — HO3D, DexYCB, ...).
|
| 195 |
+
num_steps: Euler ODE steps (same semantics as ``val_num_steps``).
|
| 196 |
+
amp_dtype: "bf16" | "fp16" | "float16" (autocast dtype on cuda).
|
| 197 |
+
|
| 198 |
+
Returns dict:
|
| 199 |
+
rotation [B,3,3] float32 (decode_pose_rotation; orthonormal, det +1),
|
| 200 |
+
translation [B,3] float32 (metric, decode_pose_translation),
|
| 201 |
+
scale [B] float32 (metric, decode_pose_scale),
|
| 202 |
+
shape_latent [B,8,16,16,16] float32.
|
| 203 |
+
"""
|
| 204 |
+
inner = model.module if hasattr(model, "module") else model
|
| 205 |
+
was_training = inner.training
|
| 206 |
+
inner.eval()
|
| 207 |
+
if device is None:
|
| 208 |
+
device = next(inner.parameters()).device
|
| 209 |
+
batch = _move(batch, device)
|
| 210 |
+
|
| 211 |
+
use_amp = (device.type == "cuda"
|
| 212 |
+
and amp_dtype.lower() in {"bf16", "fp16", "float16"})
|
| 213 |
+
amp_dt = torch.bfloat16 if amp_dtype.lower() == "bf16" else torch.float16
|
| 214 |
+
autocast = (torch.autocast(device_type="cuda", dtype=amp_dt)
|
| 215 |
+
if use_amp else torch.autocast(device_type="cpu", enabled=False))
|
| 216 |
+
|
| 217 |
+
try:
|
| 218 |
+
with autocast:
|
| 219 |
+
pred = inner.predict(batch, num_steps=num_steps)
|
| 220 |
+
out = {
|
| 221 |
+
"rotation": pred["rotation"].float(), # [B,3,3]
|
| 222 |
+
"translation": pred["translation"].float(), # [B,3] metric
|
| 223 |
+
"scale": pred["scale"].float(), # [B] metric
|
| 224 |
+
"shape_latent": pred["shape_latent"].float(), # [B,8,16,16,16]
|
| 225 |
+
}
|
| 226 |
+
finally:
|
| 227 |
+
if was_training:
|
| 228 |
+
inner.train()
|
| 229 |
+
return out
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# --------------------------------------------------------------------------- #
|
| 233 |
+
# CLI batch source (DexYCB) — used for the smoke / standalone runs only.
|
| 234 |
+
# The pose entrypoint above does NOT depend on any of this.
|
| 235 |
+
# --------------------------------------------------------------------------- #
|
| 236 |
+
def _build_dexycb(data_cfg: dict, *, subjects, frame_stride, num_aux_views,
|
| 237 |
+
split: str = "val"):
|
| 238 |
+
"""Construct MultiViewDexYCB with deterministic views (random_views=False),
|
| 239 |
+
mirroring train.build_dataset for the val split. ``subjects`` / ``frame_stride``
|
| 240 |
+
/ ``num_aux_views`` may be overridden for a fast smoke."""
|
| 241 |
+
from mvsam3d.data.dexycb_dataset import MultiViewDexYCB
|
| 242 |
+
return MultiViewDexYCB(
|
| 243 |
+
dexycb_root=data_cfg["dexycb_root"],
|
| 244 |
+
canonical_cache=data_cfg["canonical_cache"],
|
| 245 |
+
num_aux_views=num_aux_views,
|
| 246 |
+
frame_stride=frame_stride,
|
| 247 |
+
anchor_sweep=data_cfg["anchor_sweep"],
|
| 248 |
+
subjects=subjects,
|
| 249 |
+
image_size=data_cfg["image_size"],
|
| 250 |
+
random_views=False, # deterministic views (like val)
|
| 251 |
+
min_aux_views=data_cfg["min_aux_views"],
|
| 252 |
+
max_aux_views=data_cfg["max_aux_views"],
|
| 253 |
+
depth_scale=data_cfg["depth_scale"],
|
| 254 |
+
depth_near=data_cfg["depth_near"],
|
| 255 |
+
depth_far=data_cfg["depth_far"],
|
| 256 |
+
split=split,
|
| 257 |
+
val_fraction=data_cfg["val_fraction"],
|
| 258 |
+
split_seed=data_cfg["split_seed"],
|
| 259 |
+
ssi_mode=data_cfg.get("ssi_mode", "scene"),
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def _orthonormality(R: torch.Tensor) -> Tuple[float, float]:
|
| 264 |
+
"""(det, ||RᵀR - I||) for a [3,3] rotation, for a finite/orthonormal check."""
|
| 265 |
+
det = float(torch.det(R))
|
| 266 |
+
err = float((R.transpose(-1, -2) @ R - torch.eye(3, device=R.device, dtype=R.dtype)).abs().max())
|
| 267 |
+
return det, err
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def main():
|
| 271 |
+
ap = argparse.ArgumentParser(
|
| 272 |
+
description="Predict (R,t,s,shape) for DexYCB samples and write to disk.")
|
| 273 |
+
ap.add_argument("--config", default=DEFAULT_CONFIG, help="training YAML config")
|
| 274 |
+
ap.add_argument("--checkpoint", required=True, help="v2 checkpoint (cache/ckpt/step_*.pt)")
|
| 275 |
+
ap.add_argument("--out", required=True, help="output .pt path (a .json sibling is also written)")
|
| 276 |
+
ap.add_argument("--num-steps", type=int, default=None,
|
| 277 |
+
help="Euler ODE steps (default: config train.val_num_steps)")
|
| 278 |
+
ap.add_argument("--num-samples", type=int, default=4, help="number of samples to predict")
|
| 279 |
+
ap.add_argument("--idx", type=int, nargs="*", default=None,
|
| 280 |
+
help="explicit sample indices (overrides --num-samples)")
|
| 281 |
+
ap.add_argument("--subjects", type=str, nargs="*", default=None,
|
| 282 |
+
help="subject substrings to filter (e.g. subject-01); default = config")
|
| 283 |
+
ap.add_argument("--frame-stride", type=int, default=None,
|
| 284 |
+
help="override data.frame_stride (larger = fewer/faster samples)")
|
| 285 |
+
ap.add_argument("--num-aux-views", type=int, default=None,
|
| 286 |
+
help="override fixed aux-view count (smaller = faster)")
|
| 287 |
+
ap.add_argument("--split", default="val", choices=["train", "val"])
|
| 288 |
+
ap.add_argument("--seed", type=int, default=1234, help="RNG seed for the sampler noise z0")
|
| 289 |
+
args = ap.parse_args()
|
| 290 |
+
|
| 291 |
+
cfg = load_config(args.config)
|
| 292 |
+
data_cfg = cfg["data"]
|
| 293 |
+
num_steps = args.num_steps if args.num_steps is not None else cfg["train"]["val_num_steps"]
|
| 294 |
+
amp_dtype = cfg["train"]["amp_dtype"]
|
| 295 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 296 |
+
|
| 297 |
+
# Deterministic sampler noise (validate.py seeds VAL_SEED before sampling).
|
| 298 |
+
torch.manual_seed(args.seed)
|
| 299 |
+
if device.type == "cuda":
|
| 300 |
+
torch.cuda.manual_seed_all(args.seed)
|
| 301 |
+
|
| 302 |
+
model, report = build_inference_model(cfg, args.checkpoint, device, verbose=True)
|
| 303 |
+
|
| 304 |
+
from mvsam3d.data.dexycb_dataset import collate_multiview
|
| 305 |
+
subjects = args.subjects if args.subjects else data_cfg["subjects"]
|
| 306 |
+
frame_stride = args.frame_stride if args.frame_stride is not None else data_cfg["frame_stride"]
|
| 307 |
+
num_aux = (args.num_aux_views if args.num_aux_views is not None
|
| 308 |
+
else (data_cfg["max_aux_views"] if data_cfg["random_views"]
|
| 309 |
+
else data_cfg["num_aux_views"]))
|
| 310 |
+
ds = _build_dexycb(data_cfg, subjects=subjects, frame_stride=frame_stride,
|
| 311 |
+
num_aux_views=num_aux, split=args.split)
|
| 312 |
+
print(f"[infer] dataset: {len(ds)} samples (subjects={subjects} "
|
| 313 |
+
f"frame_stride={frame_stride} num_aux_views={num_aux} split={args.split})")
|
| 314 |
+
if len(ds) == 0:
|
| 315 |
+
raise RuntimeError("dataset is empty for the given filters")
|
| 316 |
+
|
| 317 |
+
idxs: List[int] = (args.idx if args.idx is not None
|
| 318 |
+
else list(range(min(args.num_samples, len(ds)))))
|
| 319 |
+
|
| 320 |
+
records = []
|
| 321 |
+
for i in idxs:
|
| 322 |
+
batch = collate_multiview([ds[i]]) # B=1
|
| 323 |
+
pred = predict_pose(model, batch, num_steps=num_steps, amp_dtype=amp_dtype,
|
| 324 |
+
device=device)
|
| 325 |
+
R = pred["rotation"][0]
|
| 326 |
+
det, ortho_err = _orthonormality(R)
|
| 327 |
+
finite = bool(torch.isfinite(R).all() and torch.isfinite(pred["translation"]).all()
|
| 328 |
+
and torch.isfinite(pred["scale"]).all())
|
| 329 |
+
rec = {
|
| 330 |
+
"idx": int(i),
|
| 331 |
+
"object_name": batch["object_name"][0],
|
| 332 |
+
"rotation": R.cpu(),
|
| 333 |
+
"translation": pred["translation"][0].cpu(),
|
| 334 |
+
"scale": float(pred["scale"][0]),
|
| 335 |
+
"shape_latent_shape": list(pred["shape_latent"].shape),
|
| 336 |
+
"det": det,
|
| 337 |
+
"ortho_err": ortho_err,
|
| 338 |
+
"finite": finite,
|
| 339 |
+
}
|
| 340 |
+
records.append(rec)
|
| 341 |
+
print(f"[infer] idx={i:>5d} obj={rec['object_name']:<22s} "
|
| 342 |
+
f"det={det:+.4f} ortho_err={ortho_err:.2e} "
|
| 343 |
+
f"t={rec['translation'].tolist()} s={rec['scale']:.4f} finite={finite}")
|
| 344 |
+
|
| 345 |
+
# ---- write .pt (full tensors) + .json (human-readable summary) ----
|
| 346 |
+
os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
|
| 347 |
+
torch.save({"load_report": report, "num_steps": num_steps,
|
| 348 |
+
"checkpoint": os.path.abspath(args.checkpoint),
|
| 349 |
+
"records": records}, args.out)
|
| 350 |
+
json_path = os.path.splitext(args.out)[0] + ".json"
|
| 351 |
+
with open(json_path, "w") as f:
|
| 352 |
+
json.dump({
|
| 353 |
+
"checkpoint": os.path.abspath(args.checkpoint),
|
| 354 |
+
"step": report["step"],
|
| 355 |
+
"num_steps": num_steps,
|
| 356 |
+
"load_report": {k: report[k] for k in
|
| 357 |
+
("step", "restored_tensors", "n_missing_keys",
|
| 358 |
+
"n_unexpected_keys", "n_trainable_params",
|
| 359 |
+
"missing_trainable_params")},
|
| 360 |
+
"predictions": [{
|
| 361 |
+
"idx": r["idx"], "object_name": r["object_name"],
|
| 362 |
+
"rotation": r["rotation"].tolist(),
|
| 363 |
+
"translation": r["translation"].tolist(),
|
| 364 |
+
"scale": r["scale"], "det": r["det"], "ortho_err": r["ortho_err"],
|
| 365 |
+
"finite": r["finite"], "shape_latent_shape": r["shape_latent_shape"],
|
| 366 |
+
} for r in records],
|
| 367 |
+
}, f, indent=2)
|
| 368 |
+
print(f"[infer] wrote {os.path.abspath(args.out)}")
|
| 369 |
+
print(f"[infer] wrote {os.path.abspath(json_path)}")
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
if __name__ == "__main__":
|
| 373 |
+
main()
|
migrator/code/mv-sam3d-for-6d-v2/mvsam3d/model/__init__.py
ADDED
|
File without changes
|