phanerozoic commited on
Commit
c8fb309
·
verified ·
1 Parent(s): 8619a22

Argus-3D: discovered class-agnostic 3D detection head on EUPE-ViT-B

Browse files
README.md ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Argus-3D
2
+
3
+ Class-agnostic 3D bounding box detection on a frozen [EUPE-ViT-B](https://huggingface.co/facebook/EUPE-ViT-B) backbone. Given a posed RGB image and camera intrinsics, returns 7-DoF boxes (cx, cy, cz, w, h, d, theta) for the objects in the scene.
4
+
5
+ The head is built by feature-dim discovery and unsupervised clustering, not gradient training. No 2D bounding boxes, no class labels, no segmentation map as final output. Camera-frame 3D boxes only.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ Image (768x768)
11
+ -> EUPE-ViT-B (frozen, reused from phanerozoic/argus)
12
+ -> patch tokens (2304, 768) on a 48x48 grid
13
+ -> instance head: ridge over 768 dims -> per-patch foreground score
14
+ depth head: ridge over 768 dims -> per-patch metric depth (m)
15
+ k-means modes: 8 cluster centers -> per-patch object-type assignment
16
+ -> threshold instance score
17
+ -> upsample mask to 768x768, connected components
18
+ -> for each component:
19
+ unproject pixels to 3D using depth + K
20
+ DBSCAN-split for instance separation
21
+ PCA-on-xz for yaw, percentile extents for (w, h, d)
22
+ blend extents toward the matched cluster's size prior
23
+ -> camera-frame 7-DoF box list
24
+ ```
25
+
26
+ ## Components
27
+
28
+ | Component | Parameters | Discovery method |
29
+ |---|---|---|
30
+ | EUPE-ViT-B backbone (frozen, reused) | not part of this head | reused from phanerozoic/argus |
31
+ | Instance head (ridge over 768 dims) | 769 floats + 1 threshold | random K=20 subset search + hard-negative mining, AUC selection |
32
+ | Depth head (ridge over 768 dims) | 769 floats | random K=20 subset search, RMSE selection |
33
+ | K-means cluster centers | 8 x 768 floats | MiniBatchKMeans on foreground patches |
34
+ | Per-cluster size priors (w, h, d) | 8 x 3 floats | median of observed extents per mode |
35
+ | OBB fitter (PCA + percentile + Tikhonov) | 0 | closed-form |
36
+ | **Total head footprint** | **~7,700 floats / 43 KB** | |
37
+
38
+ ## File layout
39
+
40
+ ```
41
+ instance_head.safetensors # ridge dims + coef + intercept + threshold
42
+ depth_head.safetensors # ridge dims + coef + intercept
43
+ size_priors.safetensors # 8 cluster centers + 8 (w, h, d) priors
44
+ config.json # input_res, patch_grid, prior_weight, etc.
45
+ argus_3d.py # Argus3D class
46
+ infer.py # CLI dispatcher
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```python
52
+ from argus_3d import Argus3D
53
+ import numpy as np
54
+
55
+ model = Argus3D.from_pretrained("phanerozoic/argus-3d", device="cuda")
56
+
57
+ K = np.array([[850, 0, 395], [0, 850, 510], [0, 0, 1]])
58
+ boxes = model.detect("room.jpg", K) # list of Box3D
59
+ boxes = model.detect("room.jpg", K, depth=d) # supply RGBD sensor depth
60
+ out = model.perceive("room.jpg", K) # fg score map + depth map + boxes
61
+
62
+ for b in boxes:
63
+ print(b.cx, b.cy, b.cz, b.w, b.h, b.d, b.theta)
64
+ ```
65
+
66
+ ## Eval
67
+
68
+ CA-1M val sequence `ca1m-val-45662921`. Class-agnostic per-scene 3D IoU after multi-view fusion across 284 frames (stride-4 sampling of 1135 total). The head produces its own instance hypotheses; no ground-truth 2D bounding boxes are used. Sensor depth is supplied; the discovered depth head can be used in its place.
69
+
70
+ | Metric | Value |
71
+ |---|---|
72
+ | Fused boxes per scene | 72 |
73
+ | Mean 3D IoU | 0.063 |
74
+ | Fraction > 0.1 IoU | 27.8% |
75
+ | Fraction > 0.25 IoU | 6.9% |
76
+ | Recall (matched / GT) | 19.8% |
77
+
78
+ Per-stage discovery metrics:
79
+
80
+ | Discovery output | Metric | Value |
81
+ |---|---|---|
82
+ | Instance head, per-patch foreground | AUC | 0.860 |
83
+ | Instance head, per-patch foreground | F1 (tuned threshold) | 0.569 |
84
+ | Depth head, foreground patches in 0.1-3 m | RMSE | 0.190 m |
85
+ | Depth head, foreground patches in 0.1-3 m | delta1 (1.25x ratio) | 0.919 |
86
+
87
+ ## Backbone
88
+
89
+ EUPE-ViT-B from Meta FAIR (arXiv:2603.22387) via [phanerozoic/argus](https://huggingface.co/phanerozoic/argus). The backbone is frozen and not modified by this repo.
90
+
91
+ ## License
92
+
93
+ FAIR Research License (non-commercial), inherited via the EUPE-ViT-B backbone.
argus_3d.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Argus3D: class-agnostic 3D bounding box detection on EUPE-ViT-B.
2
+
3
+ A single class-agnostic 3D detector head built by discovery, not gradient
4
+ training. Given a posed RGB image and camera intrinsics, returns a list of
5
+ 7-DoF bounding boxes (cx, cy, cz, w, h, d, theta) in camera frame.
6
+
7
+ Components (all loaded from safetensors at from_pretrained time):
8
+ - frozen EUPE-ViT-B backbone (reused from phanerozoic/argus)
9
+ - instance_head: per-patch foreground ridge, 769 floats + 1 threshold
10
+ - depth_head: per-patch metric depth ridge, 769 floats
11
+ - size_priors: 8 k-means cluster centers (768 each) plus 8 (w, h, d) priors
12
+ - zero-parameter OBB fitter, DBSCAN instance separation, Tikhonov prior blend
13
+ - optional multi-view fusion utilities
14
+
15
+ Use:
16
+ model = Argus3D.from_pretrained('phanerozoic/argus-3d').cuda().eval()
17
+ boxes = model.detect(image, K) # camera-frame 7-DoF boxes
18
+ boxes = model.detect(image, K, depth=depth) # supply sensor depth (RGBD)
19
+ out = model.perceive(image, K) # foreground mask + depth + boxes
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import math
26
+ import os
27
+ from dataclasses import dataclass
28
+ from typing import Dict, List, Optional, Tuple
29
+
30
+ import numpy as np
31
+ import torch
32
+ import torch.nn as nn
33
+ from PIL import Image
34
+
35
+ DEFAULT_INPUT_RES = 768
36
+ DEFAULT_PATCH_GRID = 48
37
+ DEFAULT_PATCH_SIZE = DEFAULT_INPUT_RES // DEFAULT_PATCH_GRID # 16
38
+
39
+
40
+ @dataclass
41
+ class Box3D:
42
+ cx: float
43
+ cy: float
44
+ cz: float
45
+ w: float
46
+ h: float
47
+ d: float
48
+ theta: float
49
+ score: float = 1.0
50
+ n_inliers: int = 0
51
+
52
+
53
+ class Argus3D(nn.Module):
54
+ """Class-agnostic 3D detector on a frozen EUPE-ViT-B backbone."""
55
+
56
+ def __init__(
57
+ self,
58
+ instance_dims: torch.Tensor,
59
+ instance_coef: torch.Tensor,
60
+ instance_intercept: float,
61
+ instance_threshold: float,
62
+ depth_dims: torch.Tensor,
63
+ depth_coef: torch.Tensor,
64
+ depth_intercept: float,
65
+ cluster_centers: torch.Tensor,
66
+ size_priors: torch.Tensor,
67
+ prior_weight: float = 80.0,
68
+ input_res: int = DEFAULT_INPUT_RES,
69
+ patch_grid: int = DEFAULT_PATCH_GRID,
70
+ ):
71
+ super().__init__()
72
+ self.register_buffer("instance_dims", instance_dims.long())
73
+ self.register_buffer("instance_coef", instance_coef.float())
74
+ self.instance_intercept = float(instance_intercept)
75
+ self.instance_threshold = float(instance_threshold)
76
+ self.register_buffer("depth_dims", depth_dims.long())
77
+ self.register_buffer("depth_coef", depth_coef.float())
78
+ self.depth_intercept = float(depth_intercept)
79
+ self.register_buffer("cluster_centers", cluster_centers.float())
80
+ self.register_buffer("size_priors", size_priors.float())
81
+ self.prior_weight = prior_weight
82
+ self.input_res = input_res
83
+ self.patch_grid = patch_grid
84
+ self.patch_size = input_res // patch_grid
85
+ self.backbone: Optional[nn.Module] = None # set externally
86
+
87
+ def attach_backbone(self, backbone: nn.Module) -> "Argus3D":
88
+ """Attach the frozen EUPE-ViT-B backbone (the trunk is not shipped)."""
89
+ self.backbone = backbone
90
+ for p in self.backbone.parameters():
91
+ p.requires_grad = False
92
+ return self
93
+
94
+ @classmethod
95
+ def from_pretrained(
96
+ cls,
97
+ repo_or_dir: str,
98
+ backbone_repo: str = "phanerozoic/argus",
99
+ device: str = "cpu",
100
+ ) -> "Argus3D":
101
+ """Load heads from safetensors in `repo_or_dir`. Backbone is loaded
102
+ from `backbone_repo`'s frozen EUPE-ViT-B trunk."""
103
+ from safetensors.torch import load_file
104
+
105
+ if os.path.isdir(repo_or_dir):
106
+ base = repo_or_dir
107
+ else:
108
+ from huggingface_hub import snapshot_download
109
+ base = snapshot_download(repo_or_dir)
110
+
111
+ inst = load_file(os.path.join(base, "instance_head.safetensors"))
112
+ depth = load_file(os.path.join(base, "depth_head.safetensors"))
113
+ priors = load_file(os.path.join(base, "size_priors.safetensors"))
114
+ with open(os.path.join(base, "config.json"), "r") as f:
115
+ cfg = json.load(f)
116
+
117
+ model = cls(
118
+ instance_dims=inst["dims"],
119
+ instance_coef=inst["coef"],
120
+ instance_intercept=float(inst["intercept"].item()),
121
+ instance_threshold=float(inst["threshold"].item()),
122
+ depth_dims=depth["dims"],
123
+ depth_coef=depth["coef"],
124
+ depth_intercept=float(depth["intercept"].item()),
125
+ cluster_centers=priors["cluster_centers"],
126
+ size_priors=priors["priors_whd"],
127
+ prior_weight=cfg.get("prior_weight", 80.0),
128
+ input_res=cfg.get("input_res", DEFAULT_INPUT_RES),
129
+ patch_grid=cfg.get("patch_grid", DEFAULT_PATCH_GRID),
130
+ )
131
+
132
+ # Backbone reuse: load the frozen EUPE-ViT-B from argus.
133
+ from transformers import AutoModel
134
+ argus = AutoModel.from_pretrained(backbone_repo, trust_remote_code=True)
135
+ model.attach_backbone(argus.backbone.to(device).eval())
136
+ return model.to(device)
137
+
138
+ @staticmethod
139
+ def _imagenet_normalize(x: torch.Tensor) -> torch.Tensor:
140
+ mean = torch.tensor([0.485, 0.456, 0.406], device=x.device).view(1, 3, 1, 1)
141
+ std = torch.tensor([0.229, 0.224, 0.225], device=x.device).view(1, 3, 1, 1)
142
+ return (x - mean) / std
143
+
144
+ def _prepare_image(self, image) -> torch.Tensor:
145
+ if isinstance(image, str):
146
+ pil = Image.open(image).convert("RGB")
147
+ elif isinstance(image, np.ndarray):
148
+ pil = Image.fromarray(image).convert("RGB")
149
+ elif isinstance(image, Image.Image):
150
+ pil = image.convert("RGB")
151
+ else:
152
+ raise TypeError(f"unsupported image type: {type(image)}")
153
+ pil_in = pil.resize((self.input_res, self.input_res), Image.BILINEAR)
154
+ arr = np.array(pil_in).astype(np.float32) / 255.0
155
+ device = next(self.buffers()).device
156
+ return torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0).to(device), pil.size
157
+
158
+ @torch.no_grad()
159
+ def patch_features(self, image) -> Tuple[torch.Tensor, Tuple[int, int]]:
160
+ if self.backbone is None:
161
+ raise RuntimeError("backbone not attached; call attach_backbone or from_pretrained")
162
+ img_t, orig_size = self._prepare_image(image)
163
+ feats = self.backbone.forward_features(self._imagenet_normalize(img_t))
164
+ return feats["x_norm_patchtokens"][0], orig_size # (N, 768)
165
+
166
+ @torch.no_grad()
167
+ def fg_score_map(self, image) -> Tuple[np.ndarray, Tuple[int, int]]:
168
+ """Per-patch foreground score map at patch_grid x patch_grid."""
169
+ patch_tokens, orig_size = self.patch_features(image)
170
+ f = patch_tokens.index_select(1, self.instance_dims)
171
+ scores = (f @ self.instance_coef + self.instance_intercept).cpu().numpy()
172
+ return scores.reshape(self.patch_grid, self.patch_grid), orig_size
173
+
174
+ @torch.no_grad()
175
+ def depth_map(self, image) -> Tuple[np.ndarray, Tuple[int, int]]:
176
+ """Per-patch metric depth at patch_grid x patch_grid (meters)."""
177
+ patch_tokens, orig_size = self.patch_features(image)
178
+ f = patch_tokens.index_select(1, self.depth_dims)
179
+ depths = (f @ self.depth_coef + self.depth_intercept).cpu().numpy()
180
+ return depths.reshape(self.patch_grid, self.patch_grid), orig_size
181
+
182
+ @torch.no_grad()
183
+ def detect(
184
+ self,
185
+ image,
186
+ K: np.ndarray,
187
+ depth: Optional[np.ndarray] = None,
188
+ return_internals: bool = False,
189
+ ) -> List[Box3D]:
190
+ """Camera-frame 7-DoF box list for the image.
191
+
192
+ K: 3x3 intrinsic matrix matching `image`'s native resolution.
193
+ depth: optional (H, W) sensor depth in meters at image resolution. If
194
+ None, the discovered depth head is used.
195
+ """
196
+ from PIL import Image as PILImage
197
+
198
+ patch_tokens, (orig_W, orig_H) = self.patch_features(image)
199
+ feat_inst = patch_tokens.index_select(1, self.instance_dims)
200
+ scores = (feat_inst @ self.instance_coef + self.instance_intercept).cpu().numpy()
201
+ score_grid = scores.reshape(self.patch_grid, self.patch_grid)
202
+
203
+ # K rescaled to input_res
204
+ sx, sy = self.input_res / orig_W, self.input_res / orig_H
205
+ K_scaled = K.astype(np.float64).copy()
206
+ K_scaled[0, 0] *= sx
207
+ K_scaled[0, 2] *= sx
208
+ K_scaled[1, 1] *= sy
209
+ K_scaled[1, 2] *= sy
210
+ K_inv = np.linalg.inv(K_scaled)
211
+
212
+ # Depth at input_res
213
+ if depth is None:
214
+ feat_depth = patch_tokens.index_select(1, self.depth_dims)
215
+ d_grid = (feat_depth @ self.depth_coef + self.depth_intercept).cpu().numpy()
216
+ d_grid = d_grid.reshape(self.patch_grid, self.patch_grid).astype(np.float32)
217
+ d_pil = PILImage.fromarray(d_grid, mode="F")
218
+ depth_full = np.array(
219
+ d_pil.resize((self.input_res, self.input_res), PILImage.BILINEAR)
220
+ )
221
+ else:
222
+ d_pil = PILImage.fromarray(depth.astype(np.float32), mode="F")
223
+ depth_full = np.array(
224
+ d_pil.resize((self.input_res, self.input_res), PILImage.BILINEAR)
225
+ )
226
+
227
+ # Cluster assignment per patch (nearest center).
228
+ diff = patch_tokens.unsqueeze(1) - self.cluster_centers.unsqueeze(0)
229
+ dist = (diff * diff).sum(dim=2)
230
+ cluster_assign = dist.argmin(dim=1).cpu().numpy()
231
+
232
+ # Foreground mask at full image resolution.
233
+ score_full_pil = PILImage.fromarray(score_grid.astype(np.float32), mode="F")
234
+ score_full = np.array(
235
+ score_full_pil.resize((self.input_res, self.input_res), PILImage.BILINEAR)
236
+ )
237
+ fg_full = score_full > self.instance_threshold
238
+
239
+ try:
240
+ from scipy.ndimage import label
241
+ labeled, n_comp = label(fg_full)
242
+ except ImportError:
243
+ labeled = fg_full.astype(np.int32)
244
+ n_comp = 1
245
+
246
+ boxes: List[Box3D] = []
247
+ stride = 4
248
+ for cid in range(1, n_comp + 1):
249
+ comp = labeled == cid
250
+ if comp.sum() < 100:
251
+ continue
252
+ ys_full, xs_full = np.where(comp)
253
+ ys = ys_full[::stride]
254
+ xs = xs_full[::stride]
255
+ if len(ys) < 20:
256
+ continue
257
+ d_arr = depth_full[ys, xs].astype(np.float64)
258
+ valid = (d_arr > 0.1) & (d_arr < 10.0) & np.isfinite(d_arr)
259
+ if valid.sum() < 20:
260
+ continue
261
+ ys_v, xs_v, d_v = ys[valid], xs[valid], d_arr[valid]
262
+ rays = K_inv @ np.stack([xs_v.astype(np.float64), ys_v.astype(np.float64),
263
+ np.ones_like(xs_v, dtype=np.float64)], axis=0)
264
+ pts = np.stack([rays[0] * d_v, rays[1] * d_v, d_v], axis=-1)
265
+
266
+ try:
267
+ from sklearn.cluster import DBSCAN
268
+ db = DBSCAN(eps=0.15, min_samples=15).fit(pts)
269
+ labels_pts = db.labels_
270
+ except ImportError:
271
+ labels_pts = np.zeros(len(pts), dtype=np.int32)
272
+
273
+ uniq = [u for u in set(labels_pts) if u >= 0] or [-1]
274
+ for u in uniq:
275
+ sel = labels_pts == u if u >= 0 else np.ones(len(pts), dtype=bool)
276
+ if sel.sum() < 20:
277
+ continue
278
+ pts_sub = pts[sel]
279
+ obb = self._fit_obb(pts_sub)
280
+ if obb is None:
281
+ continue
282
+ cx, cy, cz, w, h, d, theta = obb
283
+ if cz <= 0.2 or cz > 10.0:
284
+ continue
285
+
286
+ # Per-cluster size prior blend.
287
+ comp_patch_lin = (ys_v[sel] // self.patch_size) * self.patch_grid + (xs_v[sel] // self.patch_size)
288
+ comp_patch_lin = comp_patch_lin.astype(np.int32)
289
+ clusters_here = cluster_assign[comp_patch_lin]
290
+ if len(clusters_here):
291
+ mode = int(np.bincount(clusters_here).argmax())
292
+ w_p, h_p, d_p = self.size_priors[mode].cpu().numpy()
293
+ n = int(sel.sum())
294
+ denom = n + self.prior_weight
295
+ w = (n * w + self.prior_weight * float(w_p)) / denom
296
+ h = (n * h + self.prior_weight * float(h_p)) / denom
297
+ d = (n * d + self.prior_weight * float(d_p)) / denom
298
+
299
+ if max(w, h, d) > 2.5 or min(w, h, d) < 0.05 or w * h * d > 4.0:
300
+ continue
301
+ boxes.append(Box3D(cx, cy, cz, w, h, d, theta, n_inliers=int(sel.sum())))
302
+ return boxes
303
+
304
+ @torch.no_grad()
305
+ def perceive(self, image, K: np.ndarray, depth: Optional[np.ndarray] = None) -> Dict:
306
+ score_map, _ = self.fg_score_map(image)
307
+ depth_map_pred, _ = self.depth_map(image)
308
+ boxes = self.detect(image, K, depth=depth)
309
+ return {
310
+ "fg_score_map": score_map,
311
+ "depth_map": depth_map_pred,
312
+ "boxes": boxes,
313
+ }
314
+
315
+ @staticmethod
316
+ def _fit_obb(pts: np.ndarray, trim_pct: float = 2.0):
317
+ if len(pts) < 8:
318
+ return None
319
+ xz = pts[:, [0, 2]]
320
+ center_xz = xz.mean(axis=0)
321
+ xz_centered = xz - center_xz
322
+ cov = xz_centered.T @ xz_centered / max(1, len(xz_centered) - 1)
323
+ eigvals, eigvecs = np.linalg.eigh(cov)
324
+ principal = eigvecs[:, np.argmax(eigvals)]
325
+ theta = math.atan2(-principal[1], principal[0])
326
+
327
+ c, s = math.cos(-theta), math.sin(-theta)
328
+ x_loc = c * pts[:, 0] + s * pts[:, 2]
329
+ z_loc = -s * pts[:, 0] + c * pts[:, 2]
330
+ y = pts[:, 1]
331
+ lo, hi = trim_pct, 100.0 - trim_pct
332
+ x_lo, x_hi = float(np.percentile(x_loc, lo)), float(np.percentile(x_loc, hi))
333
+ y_lo, y_hi = float(np.percentile(y, lo)), float(np.percentile(y, hi))
334
+ z_lo, z_hi = float(np.percentile(z_loc, lo)), float(np.percentile(z_loc, hi))
335
+ w = x_hi - x_lo
336
+ h = y_hi - y_lo
337
+ d = z_hi - z_lo
338
+ if w <= 0 or h <= 0 or d <= 0:
339
+ return None
340
+ cx_loc = (x_lo + x_hi) / 2.0
341
+ cz_loc = (z_lo + z_hi) / 2.0
342
+ cy = (y_lo + y_hi) / 2.0
343
+ c2, s2 = math.cos(theta), math.sin(theta)
344
+ cx = c2 * cx_loc + s2 * cz_loc
345
+ cz = -s2 * cx_loc + c2 * cz_loc
346
+ return (cx, cy, cz, w, h, d, theta)
config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "input_res": 768,
3
+ "patch_grid": 48,
4
+ "patch_size": 16,
5
+ "feature_dim": 768,
6
+ "prior_weight": 80.0,
7
+ "n_clusters": 8,
8
+ "depth_range_m": [
9
+ 0.1,
10
+ 10.0
11
+ ],
12
+ "fg_threshold": 0.31326109170913696
13
+ }
depth_head.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f9b589df8de62e4e4a707956e82143c0ec685d4f5a8feaf98934db7f8afebad
3
+ size 9420
infer.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CLI for argus-3d.
2
+
3
+ Usage:
4
+ python infer.py detect <image> --K K.json [--depth depth.png]
5
+ python infer.py perceive <image> --K K.json
6
+ """
7
+ import argparse
8
+ import json
9
+ import sys
10
+
11
+ import numpy as np
12
+ from PIL import Image
13
+
14
+ from argus_3d import Argus3D
15
+
16
+
17
+ def _load_K(path: str) -> np.ndarray:
18
+ with open(path, "r") as f:
19
+ return np.array(json.load(f), dtype=np.float64)
20
+
21
+
22
+ def _load_depth(path: str) -> np.ndarray:
23
+ arr = np.array(Image.open(path))
24
+ if arr.dtype == np.uint16:
25
+ return arr.astype(np.float32) / 1000.0
26
+ return arr.astype(np.float32)
27
+
28
+
29
+ def main():
30
+ ap = argparse.ArgumentParser()
31
+ sub = ap.add_subparsers(dest="cmd", required=True)
32
+ p_det = sub.add_parser("detect")
33
+ p_det.add_argument("image")
34
+ p_det.add_argument("--K", required=True)
35
+ p_det.add_argument("--depth", default=None)
36
+ p_det.add_argument("--repo", default="phanerozoic/argus-3d")
37
+ p_det.add_argument("--device", default="cuda")
38
+ p_per = sub.add_parser("perceive")
39
+ p_per.add_argument("image")
40
+ p_per.add_argument("--K", required=True)
41
+ p_per.add_argument("--depth", default=None)
42
+ p_per.add_argument("--repo", default="phanerozoic/argus-3d")
43
+ p_per.add_argument("--device", default="cuda")
44
+ args = ap.parse_args()
45
+
46
+ model = Argus3D.from_pretrained(args.repo, device=args.device)
47
+ K = _load_K(args.K)
48
+ depth = _load_depth(args.depth) if args.depth else None
49
+
50
+ if args.cmd == "detect":
51
+ boxes = model.detect(args.image, K, depth=depth)
52
+ out = [
53
+ {
54
+ "box": [b.cx, b.cy, b.cz, b.w, b.h, b.d, b.theta],
55
+ "score": b.score,
56
+ "n_inliers": b.n_inliers,
57
+ }
58
+ for b in boxes
59
+ ]
60
+ json.dump(out, sys.stdout, indent=2)
61
+ else:
62
+ out = model.perceive(args.image, K, depth=depth)
63
+ out["fg_score_map"] = out["fg_score_map"].tolist()
64
+ out["depth_map"] = out["depth_map"].tolist()
65
+ out["boxes"] = [
66
+ {
67
+ "box": [b.cx, b.cy, b.cz, b.w, b.h, b.d, b.theta],
68
+ "score": b.score,
69
+ "n_inliers": b.n_inliers,
70
+ }
71
+ for b in out["boxes"]
72
+ ]
73
+ json.dump(out, sys.stdout, indent=2)
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
instance_head.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f71ae2a496a02a1727355073a43946d8eae0ad2ff18d3668955b8b2cd5164ac3
3
+ size 9496
size_priors.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:901ae6958490daf1c8dd6541b6e877d83d9cdb822577993fcd5f17cf94b31394
3
+ size 24832