Ronaldo-GOAT commited on
Commit
2f18e9d
·
verified ·
1 Parent(s): e3ffa01

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/__init__.cpython-311.pyc +0 -0
  2. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/classifiers.cpython-311.pyc +0 -0
  3. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/depthers.cpython-311.pyc +0 -0
  4. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/dinotxt.cpython-311.pyc +0 -0
  5. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/__pycache__/utils.cpython-311.pyc +0 -0
  6. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/cell_dino/__pycache__/backbones.cpython-311.pyc +0 -0
  7. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/cell_dino/backbones.py +182 -0
  8. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__init__.py +7 -0
  9. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/__init__.cpython-311.pyc +0 -0
  10. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/decode_heads.cpython-311.pyc +0 -0
  11. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/encoder_decoder.cpython-311.pyc +0 -0
  12. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/__pycache__/ops.cpython-311.pyc +0 -0
  13. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/decode_heads.py +747 -0
  14. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/encoder_decoder.py +351 -0
  15. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/depth/ops.py +28 -0
  16. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/dinotxt_model.py +135 -0
  17. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/dinov2_wrapper.py +64 -0
  18. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/text_tower.py +104 -0
  19. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/text_transformer.py +72 -0
  20. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/tokenizer.py +45 -0
  21. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/text/vision_tower.py +179 -0
  22. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/xray_dino/__pycache__/backbones.cpython-311.pyc +0 -0
  23. migrator/checkpoints/hub/facebookresearch_dinov2_main/dinov2/hub/xray_dino/backbones.py +28 -0
  24. migrator/code/mv-sam3d-for-6d-v2/IMPLEMENTATION.md +208 -0
  25. migrator/code/mv-sam3d-for-6d-v2/configs/heldout.yaml +70 -0
  26. migrator/code/mv-sam3d-for-6d-v2/configs/train.yaml +141 -0
  27. migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated.yaml +127 -0
  28. migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated_object_ssi.yaml +128 -0
  29. migrator/code/mv-sam3d-for-6d-v2/configs/train_integrated_scene_ssi.yaml +128 -0
  30. migrator/code/mv-sam3d-for-6d-v2/docs/TRAINING_FEATURES.md +122 -0
  31. migrator/code/mv-sam3d-for-6d-v2/docs/dataset_convention_spec.md +44 -0
  32. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__init__.py +9 -0
  33. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__pycache__/__init__.cpython-311.pyc +0 -0
  34. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/__pycache__/paths.cpython-311.pyc +0 -0
  35. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__init__.py +0 -0
  36. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/__init__.cpython-311.pyc +0 -0
  37. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/base_dataset.cpython-311.pyc +0 -0
  38. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/canonical.cpython-311.pyc +0 -0
  39. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/dexycb_calib.cpython-311.pyc +0 -0
  40. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/dexycb_dataset.cpython-311.pyc +0 -0
  41. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/__pycache__/integrated.cpython-311.pyc +0 -0
  42. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/adapters.py +1402 -0
  43. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/base_dataset.py +272 -0
  44. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/canonical.py +76 -0
  45. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/dexycb_calib.py +75 -0
  46. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/dexycb_dataset.py +501 -0
  47. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/data/integrated.py +120 -0
  48. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/infer/__init__.py +17 -0
  49. migrator/code/mv-sam3d-for-6d-v2/mvsam3d/infer/predict.py +373 -0
  50. 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