argus.py: factor padding_mode through DPT blocks; add depth crop_border kwarg; batched correspond. README: align with shipped 3M cofiber detection head, drop FCOS framing, fix file sizes/param counts, add real IN1k val comparison, document qkv-bias choice. eval JSON: strip personal paths.
Browse files- README.md +42 -45
- argus.py +79 -27
- rf100vl_zero_shot_cross_domain_eval.json +4 -3
README.md
CHANGED
|
@@ -26,7 +26,7 @@ metrics:
|
|
| 26 |
|
| 27 |
# Argus
|
| 28 |
|
| 29 |
-
Argus is a multi-task perception system built on a single compact vision backbone. From one forward pass through the encoder, the model produces classification labels, semantic segmentation masks, metric depth maps, object detections with bounding boxes, and dense keypoint correspondences,
|
| 30 |
|
| 31 |
The underlying backbone is [EUPE-ViT-B](https://huggingface.co/facebook/EUPE-ViT-B) (86M parameters), which was introduced in *Efficient Universal Perception Encoder* (Zhu et al., Meta FAIR, [arXiv:2603.22387](https://arxiv.org/abs/2603.22387), March 2026). That paper demonstrates that a small vision encoder can be distilled from a collection of larger specialist teachers, yielding features that transfer well to image understanding, dense prediction, and vision–language tasks simultaneously. Argus takes the released EUPE-ViT-B backbone, leaves its weights frozen, and attaches five lightweight task heads that were trained or constructed independently for this project.
|
| 32 |
|
|
@@ -38,14 +38,14 @@ Image → EUPE-ViT-B (frozen, 86M parameters) → shared features
|
|
| 38 |
├── Classification — trained linear softmax, 1000 ImageNet classes
|
| 39 |
├── Segmentation — linear head, 150 ADE20K classes
|
| 40 |
├── Depth — DPT multi-scale decoder, metric depth in meters (NYU Depth V2)
|
| 41 |
-
├── Detection —
|
| 42 |
└── Correspondence — training-free dense feature matching
|
| 43 |
```
|
| 44 |
|
| 45 |
- **Classification** — trained linear softmax, a single `Linear(768, 1000)` layer with bias applied to the L2-normalized CLS token. 85.53% top-1 and 97.69% top-5 on ImageNet-1k val.
|
| 46 |
- **Segmentation** — BatchNorm layer followed by a single 1×1 convolution, trained with the backbone held frozen throughout.
|
| 47 |
- **Depth** — DPT (Dense Prediction Transformer) decoder hooking into four intermediate ViT layers (blocks 2, 5, 8, 11), fusing their features at four spatial scales via residual conv fusion, producing metric depth in meters via a 256-bin weighted sum over the 0.001 to 10 meter range. Improves RMSE by 8% over a linear probe on the same backbone (0.480 vs 0.520 on NYUv2 test), with abs_rel improving by 28%. Attempts to extend the depth head to outdoor scenes via mixed indoor/outdoor training with scale-and-shift invariant loss degraded indoor accuracy without producing a model that worked reliably across both domains; outdoor depth remains a known limitation.
|
| 48 |
-
- **Detection** —
|
| 49 |
- **Correspondence** — no trained parameters. Source and target features are extracted from two images, upsampled to pixel resolution, and matched by cosine similarity at each source keypoint.
|
| 50 |
|
| 51 |
## Benchmarks
|
|
@@ -63,29 +63,26 @@ All four of the paper's reported benchmarks were reproduced as part of building
|
|
| 63 |
|
| 64 |
The classification evaluation used the full 1.28-million-image ImageNet-1k training set as the kNN reference and the 50,000-image validation set as the query. The segmentation and depth heads were trained using the same linear-probe configurations described in the EUPE repository. Correspondence was evaluated on the SPair-71k test split at 512-pixel resolution across all 12,234 test pairs, for a total of 88,328 keypoints, with no failures during the run.
|
| 65 |
|
| 66 |
-
The classification head reaches 85.53% top-1 and 97.69% top-5 on ImageNet-1k val. The kNN protocol
|
| 67 |
-
|
| 68 |
-
| Classification method | Top-1 | Top-5 |
|
| 69 |
-
|-----------------------|----------|----------|
|
| 70 |
-
| kNN (k=10, retired) | 84.07 % | 93.99 % |
|
| 71 |
-
| Linear softmax | 85.53 % | 97.69 % |
|
| 72 |
|
| 73 |
### Detection
|
| 74 |
|
| 75 |
-
The EUPE paper evaluates its backbone exclusively through minimal decoders
|
| 76 |
|
| 77 |
Evaluation on COCO val2017 (5,000 images) with the standard pycocotools protocol:
|
| 78 |
|
| 79 |
-
| Metric |
|
| 80 |
-
|--------|-------|
|
| 81 |
-
| mAP@[0.5:0.95] | **
|
| 82 |
-
| mAP@0.50
|
| 83 |
-
| mAP@0.75
|
| 84 |
-
| mAP (small objects) |
|
| 85 |
-
| mAP (medium
|
| 86 |
-
| mAP (large
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
|
| 89 |
|
| 90 |
### Depth Decoder Comparison (Development Reference)
|
| 91 |
|
|
@@ -120,15 +117,15 @@ The Cityscapes probe reaches 63.76% mIoU, with road at 96.4%, car at 87.9%, sky
|
|
| 120 |
|
| 121 |
## Comparison with Standard Baselines
|
| 122 |
|
| 123 |
-
|
| 124 |
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|--------------------|------------|-----------|-----------|---------|-----------|
|
| 129 |
-
| Argus (EUPE-ViT-B) | 86 M | 38.5% | 61.0% | 13.1 ms | 0.34 GB |
|
| 130 |
-
| ConvNeXt-Base | 89 M | 25.5% | 48.5% | 10.4 ms | 0.35 GB |
|
| 131 |
-
| ResNet50 | 26 M | 23.0% | 46.0% | 8.4 ms | 0.12 GB |
|
| 132 |
|
| 133 |
**Segmentation**:
|
| 134 |
|
|
@@ -154,12 +151,12 @@ The models chosen for this comparison were selected to match the quality tier of
|
|
| 154 |
|
| 155 |
| Pipeline | Parameters | Latency per image | Tasks |
|
| 156 |
|----------|-----------|-------------------|-------|
|
| 157 |
-
| Argus unified |
|
| 158 |
| Four separate models | 260 M | 68 ms | 4 (classify, segment, depth, detect) |
|
| 159 |
|
| 160 |
-
The per-model breakdown for the separate pipeline is ConvNeXt-Base at 6 ms, SegFormer-B3 at 19 ms, Depth-Anything-V2-Base at 31 ms, and YOLO26l at 12 ms, summing to 68 ms when the tasks are run sequentially on the same image. Argus completes five tasks
|
| 161 |
|
| 162 |
-
The throughput advantage comes from the shared backbone. Each of the four separate models pays the cost of encoding the image through its own network before producing task-specific output. Argus encodes the image once through EUPE-ViT-B and then routes the resulting features to five lightweight heads, each of which adds only a few milliseconds on top of the shared representation. The backbone forward pass is the dominant cost in both pipelines, and running it once rather than four times is where the 1.2x throughput improvement and 2.2x parameter reduction originate. The practical consequence for deployment is that Argus requires a single model download (
|
| 163 |
|
| 164 |
## Usage
|
| 165 |
|
|
@@ -176,7 +173,7 @@ top5 = model.classify(image, top_k=5) # trained linear softma
|
|
| 176 |
seg = model.segment(image) # returns [H, W] class indices
|
| 177 |
depth = model.depth(image) # returns [H, W] metric depth in meters
|
| 178 |
|
| 179 |
-
# Detection runs at
|
| 180 |
dets = model.detect(image, score_thresh=0.3)
|
| 181 |
# returns list of {"box": [x1,y1,x2,y2], "score": float, "label": int, "class_name": str}
|
| 182 |
|
|
@@ -230,7 +227,7 @@ paths = model.export_onnx("/path/to/out_dir", backbone_resolution=224, verify=Tr
|
|
| 230 |
# paths["verification"] — max abs diff per component
|
| 231 |
```
|
| 232 |
|
| 233 |
-
|
| 234 |
|
| 235 |
For reduced VRAM on memory-constrained hardware, INT8 weight-only quantization is available via torchao. This quantizes the Linear weight matrices to INT8 while keeping activations in BF16, avoiding the outlier-channel problems that break naive INT8 quantization of ViT models:
|
| 236 |
|
|
@@ -263,12 +260,11 @@ The backbone is frozen for every task. Only the task heads are trained, and the
|
|
| 263 |
| EUPE-ViT-B backbone | LVD-1689M (approximately 1.7 billion web images) | Meta FAIR (used here frozen) |
|
| 264 |
| Segmentation head | ADE20K (20,210 training images, 2,000 validation images) | This repository, 40,000 iterations of linear-probe training |
|
| 265 |
| Depth head | NYU Depth V2 (24,231 training images) | This repository, 38,400 iterations of linear-probe training |
|
| 266 |
-
| Class prototypes (kNN) | ImageNet-1k (1.28 million training images) | This repository, mean CLS feature per class |
|
| 267 |
| Linear softmax classifier | ImageNet-1k (1.28 million training images) | This repository, SGD over cached frozen features |
|
| 268 |
-
| Detection head | COCO 2017 (117,266 training images, 80 classes) | This repository,
|
| 269 |
| Correspondence | None (training-free) | — |
|
| 270 |
|
| 271 |
-
The trainable heads sum to approximately
|
| 272 |
|
| 273 |
### Precision variants
|
| 274 |
|
|
@@ -276,22 +272,24 @@ Two safetensors files with the same weights at different on-disk precision. Infe
|
|
| 276 |
|
| 277 |
| File | Size | Load |
|
| 278 |
|---|---|---|
|
| 279 |
-
| `model.safetensors` |
|
| 280 |
-
| `model.bf16_backbone.safetensors` |
|
| 281 |
|
| 282 |
Both files load into the same FP32 model in memory; PyTorch automatically upcasts the bfloat16 stored weights at construction time. The smaller variant saves download bandwidth and disk space but does not reduce inference VRAM.
|
| 283 |
|
| 284 |
### Architecture details
|
| 285 |
|
| 286 |
-
**
|
|
|
|
|
|
|
| 287 |
|
| 288 |
**Depth head** is a DPT multi-scale decoder that hooks into backbone blocks [2, 5, 8, 11] via PyTorch forward hooks, capturing intermediate representations without modifying the backbone. A reassemble stage projects each block's output from 768 to 256 channels via LayerNorm + Linear, reshapes to spatial grids, and rescales to four target strides (4, 8, 16, 32) via bilinear interpolation. A bottom-up fusion path combines these four scales through residual conv blocks with skip connections, progressively doubling spatial resolution from stride 32 to stride 2. A final conv head produces 256 depth-bin logits, outputting metric depth in meters via a bin-weighted sum. 13,450,000 parameters, ~51 MB on disk. Trained at 416×416 with SILog loss, AdamW (lr 1e-4, weight decay 1e-3), cosine schedule with 3% warmup, batch size 16, 38,400 iterations.
|
| 289 |
|
| 290 |
-
**
|
| 291 |
|
| 292 |
-
**
|
| 293 |
|
| 294 |
-
|
| 295 |
|
| 296 |
**Correspondence** has no learned parameters. At inference time, dense patch features are extracted from both images, upsampled to 512×512 pixel resolution, and matched by cosine similarity per source keypoint.
|
| 297 |
|
|
@@ -301,9 +299,8 @@ Both files load into the same FP32 model in memory; PyTorch automatically upcast
|
|
| 301 |
|------------------------------|---------------------------|-------------|
|
| 302 |
| Segmentation (ADE20K) | 40,000 | ~5 hours |
|
| 303 |
| Depth (NYU Depth V2) | 38,400 | ~3 hours |
|
| 304 |
-
|
|
| 305 |
-
|
|
| 306 |
-
| Detection (COCO 2017) | 8 epochs × 1,832 batches | ~6 hours at batch 64, 640px, FP32, frozen backbone |
|
| 307 |
| DPT depth decoder (NYUv2) | 38,400 iterations | ~5.3 hours at batch 16, 416px, SILog loss, frozen backbone |
|
| 308 |
| Correspondence (SPair) | training-free | — |
|
| 309 |
|
|
@@ -311,11 +308,11 @@ Training was done on a single 48 GB workstation GPU. Peak VRAM was approximately
|
|
| 311 |
|
| 312 |
### Why minimal heads
|
| 313 |
|
| 314 |
-
The segmentation and classification heads follow the EUPE paper's evaluation principle: a minimal decoder isolates the backbone's contribution from the head's capacity. A Mask2Former-style segmentation head would produce higher mIoU, but those numbers would reflect the decoder as much as the features. The depth and detection heads are heavier. The DPT decoder fuses features from four intermediate ViT layers at multiple spatial scales; the
|
| 315 |
|
| 316 |
## Notes
|
| 317 |
|
| 318 |
-
The segmentation head was trained on ADE20K's 150-class indoor-and-urban label space. The depth head was trained on NYU Depth v2 and is indoor-biased; outdoor metric depth should be treated as approximate. The detection head was trained on COCO 2017's 80-class label space at
|
| 319 |
|
| 320 |
## License
|
| 321 |
|
|
|
|
| 26 |
|
| 27 |
# Argus
|
| 28 |
|
| 29 |
+
Argus is a multi-task perception system built on a single compact vision backbone. From one forward pass through the encoder, the model produces classification labels, semantic segmentation masks, metric depth maps, object detections with bounding boxes, and dense keypoint correspondences, collapsing five domain-specific pipelines into a unified package of roughly 103 million parameters (85.6M frozen backbone + 17.4M trained heads + buffers). The system is named after Argus Panoptes, the many-eyed giant of Greek mythology who was tasked by Hera with watching over everything at once.
|
| 30 |
|
| 31 |
The underlying backbone is [EUPE-ViT-B](https://huggingface.co/facebook/EUPE-ViT-B) (86M parameters), which was introduced in *Efficient Universal Perception Encoder* (Zhu et al., Meta FAIR, [arXiv:2603.22387](https://arxiv.org/abs/2603.22387), March 2026). That paper demonstrates that a small vision encoder can be distilled from a collection of larger specialist teachers, yielding features that transfer well to image understanding, dense prediction, and vision–language tasks simultaneously. Argus takes the released EUPE-ViT-B backbone, leaves its weights frozen, and attaches five lightweight task heads that were trained or constructed independently for this project.
|
| 32 |
|
|
|
|
| 38 |
├── Classification — trained linear softmax, 1000 ImageNet classes
|
| 39 |
├── Segmentation — linear head, 150 ADE20K classes
|
| 40 |
├── Depth — DPT multi-scale decoder, metric depth in meters (NYU Depth V2)
|
| 41 |
+
├── Detection — cofiber pyramid + CLIP-text-aligned cosine head, 80 COCO classes
|
| 42 |
└── Correspondence — training-free dense feature matching
|
| 43 |
```
|
| 44 |
|
| 45 |
- **Classification** — trained linear softmax, a single `Linear(768, 1000)` layer with bias applied to the L2-normalized CLS token. 85.53% top-1 and 97.69% top-5 on ImageNet-1k val.
|
| 46 |
- **Segmentation** — BatchNorm layer followed by a single 1×1 convolution, trained with the backbone held frozen throughout.
|
| 47 |
- **Depth** — DPT (Dense Prediction Transformer) decoder hooking into four intermediate ViT layers (blocks 2, 5, 8, 11), fusing their features at four spatial scales via residual conv fusion, producing metric depth in meters via a 256-bin weighted sum over the 0.001 to 10 meter range. Improves RMSE by 8% over a linear probe on the same backbone (0.480 vs 0.520 on NYUv2 test), with abs_rel improving by 28%. Attempts to extend the depth head to outdoor scenes via mixed indoor/outdoor training with scale-and-shift invariant loss degraded indoor accuracy without producing a model that worked reliably across both domains; outdoor depth remains a known limitation.
|
| 48 |
+
- **Detection** — anchor-free detector built on a parameter-free cofiber decomposition of the backbone's stride-16 spatial features (iterated downsample-then-subtract residuals at four scales, concatenated with a 64-dim sinusoidal positional embedding to give an 832-channel input). A 1x1 stem reduces to a 160-channel hidden representation; shared 9-layer cls/reg towers (5 ConvGN blocks plus 4 depthwise-residual blocks) feed three predictions per pyramid level: a CLIP-text-aligned cosine classifier with 80 frozen COCO text embeddings (`text_embed`), a learned `logit_scale` and per-class `cls_bias`, an LTRB box regressor with per-level learned scale, and a centerness branch. Runs at 768-pixel input with letterbox padding; returns per-image lists of bounding boxes with class labels, confidence scores, and COCO class names. ~3M trainable parameters.
|
| 49 |
- **Correspondence** — no trained parameters. Source and target features are extracted from two images, upsampled to pixel resolution, and matched by cosine similarity at each source keypoint.
|
| 50 |
|
| 51 |
## Benchmarks
|
|
|
|
| 63 |
|
| 64 |
The classification evaluation used the full 1.28-million-image ImageNet-1k training set as the kNN reference and the 50,000-image validation set as the query. The segmentation and depth heads were trained using the same linear-probe configurations described in the EUPE repository. Correspondence was evaluated on the SPair-71k test split at 512-pixel resolution across all 12,234 test pairs, for a total of 88,328 keypoints, with no failures during the run.
|
| 65 |
|
| 66 |
+
The shipped classification head is a trained linear softmax that reaches 85.53% top-1 and 97.69% top-5 on ImageNet-1k val. The kNN protocol shown in the EUPE-reproduction table above was the development baseline and is no longer exposed at the API level.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
### Detection
|
| 69 |
|
| 70 |
+
The EUPE paper evaluates its backbone exclusively through minimal decoders, linear probes, kNN, and training-free matching, so that downstream performance can be attributed to the learned features rather than to the capacity of the head. The paper's three evaluation domains are image understanding, dense prediction, and vision-language modeling; detection is not among them. The same frozen-backbone protocol applies: the detection head is trained on COCO 2017 train (117,266 images) at 768-pixel input while the backbone weights remain fixed.
|
| 71 |
|
| 72 |
Evaluation on COCO val2017 (5,000 images) with the standard pycocotools protocol:
|
| 73 |
|
| 74 |
+
| Metric | Hard NMS | Soft NMS |
|
| 75 |
+
|--------|---------:|---------:|
|
| 76 |
+
| mAP@[0.5:0.95] | **42.64** | **42.71** |
|
| 77 |
+
| mAP@0.50 | 65.70 | 65.67 |
|
| 78 |
+
| mAP@0.75 | 45.10 | 45.29 |
|
| 79 |
+
| mAP (small objects) | 22.3 | 22.3 |
|
| 80 |
+
| mAP (medium) | 48.3 | 48.4 |
|
| 81 |
+
| mAP (large) | 62.9 | 63.1 |
|
| 82 |
+
|
| 83 |
+
The full lineage of the cofiber-based detection head, including the analytical closed-form, evolved-circuit, dim-sweep, and person-specialist variants and the recipe ablations that took the same architecture from 24.6 to 42.64 mAP, lives in [phanerozoic/cofiber-detection](https://huggingface.co/phanerozoic/cofiber-detection).
|
| 84 |
|
| 85 |
+
Cross-domain transfer behavior on a 20-domain Roboflow 100 VL subset is recorded in `rf100vl_zero_shot_cross_domain_eval.json`: class-agnostic AR@100 averages 0.289 across the 20 domains, with the per-domain breakdown stored in the same file.
|
| 86 |
|
| 87 |
### Depth Decoder Comparison (Development Reference)
|
| 88 |
|
|
|
|
| 117 |
|
| 118 |
## Comparison with Standard Baselines
|
| 119 |
|
| 120 |
+
**Classification** on ImageNet-1k val (top-1 / top-5):
|
| 121 |
|
| 122 |
+
| Model | Parameters | Top-1 | Top-5 |
|
| 123 |
+
|--------------------|------------|---------|---------|
|
| 124 |
+
| Argus (EUPE-ViT-B) | 86 M | 85.53% | 97.69% |
|
| 125 |
+
| ConvNeXt-Base | 89 M | 83.85% | 96.74% |
|
| 126 |
+
| ResNet50 | 26 M | 80.86% | 95.43% |
|
| 127 |
|
| 128 |
+
ConvNeXt-Base and ResNet50 numbers are from the [torchvision pretrained-model accuracy table](https://docs.pytorch.org/vision/main/models.html) (`ConvNeXt_Base_Weights.IMAGENET1K_V1`, `ResNet50_Weights.IMAGENET1K_V2`). Argus is evaluated on the same 50,000-image ImageNet-1k validation set with center-crop preprocessing.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
**Segmentation**:
|
| 131 |
|
|
|
|
| 151 |
|
| 152 |
| Pipeline | Parameters | Latency per image | Tasks |
|
| 153 |
|----------|-----------|-------------------|-------|
|
| 154 |
+
| Argus unified | 103 M | 56 ms | 5 (classify, segment, depth, detect, correspond) |
|
| 155 |
| Four separate models | 260 M | 68 ms | 4 (classify, segment, depth, detect) |
|
| 156 |
|
| 157 |
+
The per-model breakdown for the separate pipeline is ConvNeXt-Base at 6 ms, SegFormer-B3 at 19 ms, Depth-Anything-V2-Base at 31 ms, and YOLO26l at 12 ms, summing to 68 ms when the tasks are run sequentially on the same image. Argus completes five tasks, the same four plus keypoint correspondence which the separate pipeline does not attempt, in 56 ms from a single model load. The total parameter count for the separate pipeline is 260M across four independent weight sets, while Argus carries 103M in a single file.
|
| 158 |
|
| 159 |
+
The throughput advantage comes from the shared backbone. Each of the four separate models pays the cost of encoding the image through its own network before producing task-specific output. Argus encodes the image once through EUPE-ViT-B and then routes the resulting features to five lightweight heads, each of which adds only a few milliseconds on top of the shared representation. The backbone forward pass is the dominant cost in both pipelines, and running it once rather than four times is where the 1.2x throughput improvement and 2.2x parameter reduction originate. The practical consequence for deployment is that Argus requires a single model download (334 MB), a single checkpoint load into VRAM (0.53 GB), and a single Python import, where the equivalent separate-model pipeline requires four downloads totaling over a gigabyte, four loads consuming over a gigabyte of VRAM if held concurrently, and four separate dependency trees to manage.
|
| 160 |
|
| 161 |
## Usage
|
| 162 |
|
|
|
|
| 173 |
seg = model.segment(image) # returns [H, W] class indices
|
| 174 |
depth = model.depth(image) # returns [H, W] metric depth in meters
|
| 175 |
|
| 176 |
+
# Detection runs at 768px with letterbox padding:
|
| 177 |
dets = model.detect(image, score_thresh=0.3)
|
| 178 |
# returns list of {"box": [x1,y1,x2,y2], "score": float, "label": int, "class_name": str}
|
| 179 |
|
|
|
|
| 227 |
# paths["verification"] — max abs diff per component
|
| 228 |
```
|
| 229 |
|
| 230 |
+
Correspondence has no learned parameters and runs as cosine-max on the backbone's spatial output, so it needs no separate graph.
|
| 231 |
|
| 232 |
For reduced VRAM on memory-constrained hardware, INT8 weight-only quantization is available via torchao. This quantizes the Linear weight matrices to INT8 while keeping activations in BF16, avoiding the outlier-channel problems that break naive INT8 quantization of ViT models:
|
| 233 |
|
|
|
|
| 260 |
| EUPE-ViT-B backbone | LVD-1689M (approximately 1.7 billion web images) | Meta FAIR (used here frozen) |
|
| 261 |
| Segmentation head | ADE20K (20,210 training images, 2,000 validation images) | This repository, 40,000 iterations of linear-probe training |
|
| 262 |
| Depth head | NYU Depth V2 (24,231 training images) | This repository, 38,400 iterations of linear-probe training |
|
|
|
|
| 263 |
| Linear softmax classifier | ImageNet-1k (1.28 million training images) | This repository, SGD over cached frozen features |
|
| 264 |
+
| Detection head | COCO 2017 (117,266 training images, 80 classes) | This repository, cofiber pyramid + CLIP-text-aligned cosine classifier, frozen text embeddings |
|
| 265 |
| Correspondence | None (training-free) | — |
|
| 266 |
|
| 267 |
+
The trainable heads sum to approximately 17.4M parameters (seg 117K + depth DPT 13.45M + linear classifier 769K + detection 3.04M). The unified `model.safetensors` is 412 MB.
|
| 268 |
|
| 269 |
### Precision variants
|
| 270 |
|
|
|
|
| 272 |
|
| 273 |
| File | Size | Load |
|
| 274 |
|---|---|---|
|
| 275 |
+
| `model.safetensors` | 412 MB | `AutoModel.from_pretrained("phanerozoic/argus", trust_remote_code=True)` |
|
| 276 |
+
| `model.bf16_backbone.safetensors` | 241 MB | `AutoModel.from_pretrained("phanerozoic/argus", trust_remote_code=True, variant="bf16_backbone")` |
|
| 277 |
|
| 278 |
Both files load into the same FP32 model in memory; PyTorch automatically upcasts the bfloat16 stored weights at construction time. The smaller variant saves download bandwidth and disk space but does not reduce inference VRAM.
|
| 279 |
|
| 280 |
### Architecture details
|
| 281 |
|
| 282 |
+
**Backbone** is the 86M-parameter EUPE-ViT-B with a deliberate simplification: `qkv_bias` is set to `False` and `mask_k_bias` is set to `False`. The upstream EUPE-ViT-B release shipped a `qkv.bias_mask` buffer that is identically zero across all attention blocks, which makes the effective qkv bias zero everywhere through `masked_bias = bias * 0 = 0`. Argus drops the bias parameter entirely so the computation is bit-equivalent in fp32. The bf16 output drift this introduces is sub-ULP and is absorbed by every head except the DPT depth decoder, where it surfaces as roughly 2 cm of noise against a 0.39 m RMSE (well below the depth head's own metric floor). The Argus-Lite (ViT-S) variant uses the same `qkv_bias=False` setting; the Argus-Edge (ViT-T) variant restores `qkv_bias=True` and `mask_k_bias=True` because the upstream EUPE-ViT-T release ships a non-trivial bias and bias-mask, neither of which can be folded away.
|
| 283 |
+
|
| 284 |
+
**Segmentation head** is `BatchNorm2d(768) → Conv2d(768, 150, 1×1)`, 116,886 parameters, 1.4 MB on disk. Trained at 512×512 with cross-entropy loss, AdamW (lr 1e-3, weight decay 1e-3), WarmupOneCycleLR with 1500-step warmup, batch size 16.
|
| 285 |
|
| 286 |
**Depth head** is a DPT multi-scale decoder that hooks into backbone blocks [2, 5, 8, 11] via PyTorch forward hooks, capturing intermediate representations without modifying the backbone. A reassemble stage projects each block's output from 768 to 256 channels via LayerNorm + Linear, reshapes to spatial grids, and rescales to four target strides (4, 8, 16, 32) via bilinear interpolation. A bottom-up fusion path combines these four scales through residual conv blocks with skip connections, progressively doubling spatial resolution from stride 32 to stride 2. A final conv head produces 256 depth-bin logits, outputting metric depth in meters via a bin-weighted sum. 13,450,000 parameters, ~51 MB on disk. Trained at 416×416 with SILog loss, AdamW (lr 1e-4, weight decay 1e-3), cosine schedule with 3% warmup, batch size 16, 38,400 iterations.
|
| 287 |
|
| 288 |
+
**Linear softmax classifier** is a single `Linear(768, 1000)` layer with bias, 769,000 parameters, about 3 MB on disk. Trained as a two-pass job: first the frozen backbone is run over the ImageNet-1k training set to cache a per-image CLS feature tensor (1,281,167 × 768, stored once at ~3.9 GB), then the linear layer is trained on the cached features alone. The training pass uses SGD with momentum 0.9, weight decay 0, batch size 4096, cosine schedule, 100 epochs, no augmentation, and the best checkpoint by validation top-1 is restored at the end. A small learning-rate sweep over `{0.5, 1.0, 3.0, 10.0, 30.0}` selects the best configuration; the L2-normalized CLS features and zero-initialized weights demand an unusually large learning rate to grow the weight scale to the point where softmax distributions become sharp. The best run used lr = 30.0 and produced 85.53% top-1 / 97.69% top-5 on ImageNet-1k val.
|
| 289 |
|
| 290 |
+
**Detection head** is an anchor-free per-pixel detector built on a parameter-free cofiber decomposition of the backbone's stride-16 spatial features. The cofiber decomposition iterates an avg-pool then bilinear-upsample-and-subtract residual at four scales (a Rocq/HoTT machine-checked exact decomposition in a semi-additive category, see `phanerozoic/cofiber-detection/CofiberDecomposition.v`), giving frequency-separated bands at strides 16, 32, 64, and 128 with no learned parameters and no FPN. Each band is concatenated with a 64-dim sinusoidal positional embedding (832-channel input per scale), passed through a per-scale GroupNorm and a shared 1x1 stem to 160 hidden channels; top-down lateral fusion combines coarser bands into finer ones, and a single stride-2 transposed convolution synthesizes the stride-8 P3 level from the stride-16 band. Each of the five resulting pyramid levels (strides 8, 16, 32, 64, 128) goes through split classification and regression towers, each tower being five standard 3x3 conv blocks (Conv3x3 + GroupNorm + activation) followed by four depthwise-residual blocks. The classifier projects the 160-dim tower output to a 768-dim space via `cls_project` and scores via cosine similarity against a frozen 80x768 `text_embed` matrix of COCO class names encoded with a CLIP ViT-L/14 8-prompt average; a learned `logit_scale` and per-class `cls_bias` sharpen the scores. The regressor predicts LTRB distances exponentiated with a per-level learned scale, and a centerness branch gates final scores. 2,975,067 parameters, 11.4 MB on disk.
|
| 291 |
|
| 292 |
+
Trained on COCO 2017 at 768x768 with letterbox padding for 16 epochs at batch 64, AdamW (lr 1e-3, weight decay 1e-4) cosine schedule with 3% warmup, ATSS target assignment, horizontal-flip augmentation, EMA decay 0.9998, and a `cls_project` initialized with the principal components of the text embedding (first 80 columns from the SVD of `text_embed`, remaining 80 columns a random orthogonal complement). After the main run, a 3-epoch partial fine-tune updates only `cls_project`, `cls_bias`, and `logit_scale` at lr 1e-4 with the towers and the cofiber path frozen, picking up +0.15 mAP. The 24.6 to 42.64 mAP path was entirely recipe and resolution: hidden width actually went down (192 to 160), and the cofiber decomposition itself never changed.
|
| 293 |
|
| 294 |
**Correspondence** has no learned parameters. At inference time, dense patch features are extracted from both images, upsampled to 512×512 pixel resolution, and matched by cosine similarity per source keypoint.
|
| 295 |
|
|
|
|
| 299 |
|------------------------------|---------------------------|-------------|
|
| 300 |
| Segmentation (ADE20K) | 40,000 | ~5 hours |
|
| 301 |
| Depth (NYU Depth V2) | 38,400 | ~3 hours |
|
| 302 |
+
| Linear classifier (IN1k) | 100 epochs × 313 steps | ~25 seconds on cached features held on GPU; ~45 minutes for the one-shot feature extraction pass over the full 1.28M training set if the cache is not already on disk |
|
| 303 |
+
| Detection (COCO 2017) | 8 epochs × 1,832 batches | ~6 hours at batch 64, 768px, FP32, frozen backbone |
|
|
|
|
| 304 |
| DPT depth decoder (NYUv2) | 38,400 iterations | ~5.3 hours at batch 16, 416px, SILog loss, frozen backbone |
|
| 305 |
| Correspondence (SPair) | training-free | — |
|
| 306 |
|
|
|
|
| 308 |
|
| 309 |
### Why minimal heads
|
| 310 |
|
| 311 |
+
The segmentation and classification heads follow the EUPE paper's evaluation principle: a minimal decoder isolates the backbone's contribution from the head's capacity. A Mask2Former-style segmentation head would produce higher mIoU, but those numbers would reflect the decoder as much as the features. The depth and detection heads are heavier. The DPT decoder fuses features from four intermediate ViT layers at multiple spatial scales; the cofiber detection head builds a five-level pyramid (strides 8 through 128) from the backbone's stride-16 output via a parameter-free decomposition rather than a learned FPN. Depth requires multi-scale fusion to capture spatial gradients across a scene, and detection requires a feature pyramid to resolve objects that range from a dozen pixels to the full image. In both cases the backbone remains frozen and only the head is trained.
|
| 312 |
|
| 313 |
## Notes
|
| 314 |
|
| 315 |
+
The segmentation head was trained on ADE20K's 150-class indoor-and-urban label space. The depth head was trained on NYU Depth v2 and is indoor-biased; outdoor metric depth should be treated as approximate. The detection head was trained on COCO 2017's 80-class label space at 768-pixel input; small-object detection is the expected weakness because the stride-8 P3 level can only resolve objects roughly 14 pixels and larger at that resolution. Classification uses a trained linear softmax classifier that produces calibrated probabilities and reaches 85.53% top-1 on ImageNet-1k val.
|
| 316 |
|
| 317 |
## License
|
| 318 |
|
argus.py
CHANGED
|
@@ -1427,11 +1427,19 @@ N_PREFIX_TOKENS = 5 # 1 CLS + 4 register/storage tokens
|
|
| 1427 |
|
| 1428 |
|
| 1429 |
class _ResidualConvUnit(nn.Module):
|
| 1430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1431 |
super().__init__()
|
| 1432 |
-
self.conv1 = nn.Conv2d(dim, dim, 3, padding=1, padding_mode=
|
| 1433 |
self.bn1 = nn.BatchNorm2d(dim)
|
| 1434 |
-
self.conv2 = nn.Conv2d(dim, dim, 3, padding=1, padding_mode=
|
| 1435 |
self.bn2 = nn.BatchNorm2d(dim)
|
| 1436 |
self.act = nn.GELU()
|
| 1437 |
|
|
@@ -1440,10 +1448,10 @@ class _ResidualConvUnit(nn.Module):
|
|
| 1440 |
|
| 1441 |
|
| 1442 |
class _FeatureFusionBlock(nn.Module):
|
| 1443 |
-
def __init__(self, dim: int, has_skip: bool = True):
|
| 1444 |
super().__init__()
|
| 1445 |
-
self.rcu1 = _ResidualConvUnit(dim)
|
| 1446 |
-
self.rcu2 = _ResidualConvUnit(dim)
|
| 1447 |
self.skip_proj = nn.Conv2d(dim, dim, 1) if has_skip else None
|
| 1448 |
|
| 1449 |
def forward(self, x: Tensor, skip: Optional[Tensor] = None) -> Tensor:
|
|
@@ -1757,7 +1765,18 @@ class Argus(PreTrainedModel):
|
|
| 1757 |
return [seg_maps[i] for i in range(len(images))]
|
| 1758 |
|
| 1759 |
@torch.inference_mode()
|
| 1760 |
-
def depth(self, image_or_images, resolution: int = 416, return_confidence: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1761 |
single, images = _normalize_image_input(image_or_images)
|
| 1762 |
transform = make_eupe_transform(resolution)
|
| 1763 |
batch = torch.stack([transform(img) for img in images]).to(self.device)
|
|
@@ -1790,6 +1809,11 @@ class Argus(PreTrainedModel):
|
|
| 1790 |
depth_b = self.depth_head(inter_list, H, W)
|
| 1791 |
std_b = None
|
| 1792 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1793 |
depth_b = F.interpolate(depth_b, size=(resolution, resolution), mode="bilinear", align_corners=False)
|
| 1794 |
if std_b is not None:
|
| 1795 |
std_b = F.interpolate(std_b, size=(resolution, resolution), mode="bilinear", align_corners=False)
|
|
@@ -1809,34 +1833,62 @@ class Argus(PreTrainedModel):
|
|
| 1809 |
@torch.inference_mode()
|
| 1810 |
def correspond(
|
| 1811 |
self,
|
| 1812 |
-
src_image
|
| 1813 |
-
tgt_image
|
| 1814 |
resolution: int = 512,
|
| 1815 |
):
|
| 1816 |
"""Dense patch correspondence between two images.
|
| 1817 |
|
| 1818 |
-
|
| 1819 |
-
|
| 1820 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1821 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1822 |
transform = make_eupe_transform(resolution)
|
| 1823 |
-
|
| 1824 |
-
|
| 1825 |
|
| 1826 |
with torch.autocast(self.device.type, dtype=torch.bfloat16, enabled=self.device.type == "cuda"):
|
| 1827 |
-
oa = self.backbone.forward_features(
|
| 1828 |
-
ob = self.backbone.forward_features(
|
| 1829 |
-
|
| 1830 |
-
|
| 1831 |
-
|
| 1832 |
-
|
| 1833 |
-
|
| 1834 |
-
|
| 1835 |
-
|
| 1836 |
-
|
| 1837 |
-
|
| 1838 |
-
|
| 1839 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1840 |
|
| 1841 |
@torch.inference_mode()
|
| 1842 |
def detect(
|
|
|
|
| 1427 |
|
| 1428 |
|
| 1429 |
class _ResidualConvUnit(nn.Module):
|
| 1430 |
+
"""Two 3x3 conv + BatchNorm blocks with a residual connection. Padding
|
| 1431 |
+
mode is configurable: the Argus-B DPT depth head trains with reflect
|
| 1432 |
+
padding to avoid edge artifacts; Argus-Lite ships weights that were
|
| 1433 |
+
trained with zero padding (the PyTorch default), and switching pad
|
| 1434 |
+
modes at inference would create a small distribution shift in the
|
| 1435 |
+
edge regions. Variants pass `padding_mode` to keep their inference
|
| 1436 |
+
aligned with their training."""
|
| 1437 |
+
|
| 1438 |
+
def __init__(self, dim: int, padding_mode: str = "reflect"):
|
| 1439 |
super().__init__()
|
| 1440 |
+
self.conv1 = nn.Conv2d(dim, dim, 3, padding=1, padding_mode=padding_mode, bias=False)
|
| 1441 |
self.bn1 = nn.BatchNorm2d(dim)
|
| 1442 |
+
self.conv2 = nn.Conv2d(dim, dim, 3, padding=1, padding_mode=padding_mode, bias=False)
|
| 1443 |
self.bn2 = nn.BatchNorm2d(dim)
|
| 1444 |
self.act = nn.GELU()
|
| 1445 |
|
|
|
|
| 1448 |
|
| 1449 |
|
| 1450 |
class _FeatureFusionBlock(nn.Module):
|
| 1451 |
+
def __init__(self, dim: int, has_skip: bool = True, padding_mode: str = "reflect"):
|
| 1452 |
super().__init__()
|
| 1453 |
+
self.rcu1 = _ResidualConvUnit(dim, padding_mode=padding_mode)
|
| 1454 |
+
self.rcu2 = _ResidualConvUnit(dim, padding_mode=padding_mode)
|
| 1455 |
self.skip_proj = nn.Conv2d(dim, dim, 1) if has_skip else None
|
| 1456 |
|
| 1457 |
def forward(self, x: Tensor, skip: Optional[Tensor] = None) -> Tensor:
|
|
|
|
| 1765 |
return [seg_maps[i] for i in range(len(images))]
|
| 1766 |
|
| 1767 |
@torch.inference_mode()
|
| 1768 |
+
def depth(self, image_or_images, resolution: int = 416, return_confidence: bool = False,
|
| 1769 |
+
crop_border: bool = False):
|
| 1770 |
+
"""Run the DPT depth decoder. Returns metric depth in meters at the
|
| 1771 |
+
input resolution.
|
| 1772 |
+
|
| 1773 |
+
``crop_border=True`` strips a small border (``max(4, H/13)`` pixels per
|
| 1774 |
+
side) from the raw decoder output before bilinear-upsampling to the
|
| 1775 |
+
input resolution. Useful when this model is loaded with a backbone
|
| 1776 |
+
whose DPT decoder was trained with zero padding (the unshipped
|
| 1777 |
+
dev-fork behaviour), which leaves a systematic edge artifact. The
|
| 1778 |
+
canonical checkpoint uses reflect padding inside every DPT conv and
|
| 1779 |
+
does not need this crop, so the option defaults to ``False``."""
|
| 1780 |
single, images = _normalize_image_input(image_or_images)
|
| 1781 |
transform = make_eupe_transform(resolution)
|
| 1782 |
batch = torch.stack([transform(img) for img in images]).to(self.device)
|
|
|
|
| 1809 |
depth_b = self.depth_head(inter_list, H, W)
|
| 1810 |
std_b = None
|
| 1811 |
|
| 1812 |
+
if crop_border:
|
| 1813 |
+
crop = max(4, depth_b.shape[2] // 13)
|
| 1814 |
+
depth_b = depth_b[:, :, crop:-crop, crop:-crop]
|
| 1815 |
+
if std_b is not None:
|
| 1816 |
+
std_b = std_b[:, :, crop:-crop, crop:-crop]
|
| 1817 |
depth_b = F.interpolate(depth_b, size=(resolution, resolution), mode="bilinear", align_corners=False)
|
| 1818 |
if std_b is not None:
|
| 1819 |
std_b = F.interpolate(std_b, size=(resolution, resolution), mode="bilinear", align_corners=False)
|
|
|
|
| 1833 |
@torch.inference_mode()
|
| 1834 |
def correspond(
|
| 1835 |
self,
|
| 1836 |
+
src_image,
|
| 1837 |
+
tgt_image,
|
| 1838 |
resolution: int = 512,
|
| 1839 |
):
|
| 1840 |
"""Dense patch correspondence between two images.
|
| 1841 |
|
| 1842 |
+
Single-pair form: pass two `PIL.Image` instances. Returns a dict with
|
| 1843 |
+
keys `matches` (numpy array of length grid*grid mapping each source
|
| 1844 |
+
patch to its argmax target patch), `scores` (cosine similarity at the
|
| 1845 |
+
match), and `grid` (the patch-grid side length).
|
| 1846 |
+
|
| 1847 |
+
Batched form: pass two equally-sized lists/iterables of images. Returns
|
| 1848 |
+
a list of per-pair dicts in the same shape that a single call would
|
| 1849 |
+
produce. Both lists are forwarded through the backbone in two
|
| 1850 |
+
contiguous batches, so cross-pair throughput on GPU is much higher
|
| 1851 |
+
than calling `correspond` in a loop.
|
| 1852 |
"""
|
| 1853 |
+
single = isinstance(src_image, Image.Image) and isinstance(tgt_image, Image.Image)
|
| 1854 |
+
if single:
|
| 1855 |
+
srcs = [src_image]
|
| 1856 |
+
tgts = [tgt_image]
|
| 1857 |
+
else:
|
| 1858 |
+
srcs = list(src_image)
|
| 1859 |
+
tgts = list(tgt_image)
|
| 1860 |
+
if len(srcs) != len(tgts):
|
| 1861 |
+
raise ValueError(
|
| 1862 |
+
f"src_image and tgt_image must have the same length; "
|
| 1863 |
+
f"got {len(srcs)} and {len(tgts)}")
|
| 1864 |
+
if not srcs:
|
| 1865 |
+
raise ValueError("empty image list")
|
| 1866 |
+
for i, (a, b) in enumerate(zip(srcs, tgts)):
|
| 1867 |
+
if not isinstance(a, Image.Image) or not isinstance(b, Image.Image):
|
| 1868 |
+
raise TypeError(f"pair {i} must contain two PIL.Image instances")
|
| 1869 |
+
|
| 1870 |
transform = make_eupe_transform(resolution)
|
| 1871 |
+
src_batch = torch.stack([transform(img) for img in srcs]).to(self.device)
|
| 1872 |
+
tgt_batch = torch.stack([transform(img) for img in tgts]).to(self.device)
|
| 1873 |
|
| 1874 |
with torch.autocast(self.device.type, dtype=torch.bfloat16, enabled=self.device.type == "cuda"):
|
| 1875 |
+
oa = self.backbone.forward_features(src_batch)
|
| 1876 |
+
ob = self.backbone.forward_features(tgt_batch)
|
| 1877 |
+
pa_batch = F.normalize(oa['x_norm_patchtokens'].float(), dim=-1)
|
| 1878 |
+
pb_batch = F.normalize(ob['x_norm_patchtokens'].float(), dim=-1)
|
| 1879 |
+
|
| 1880 |
+
results = []
|
| 1881 |
+
for pa, pb in zip(pa_batch, pb_batch):
|
| 1882 |
+
sim = pa @ pb.t()
|
| 1883 |
+
m = sim.argmax(dim=-1)
|
| 1884 |
+
s = sim.max(dim=-1).values
|
| 1885 |
+
grid = int(np.sqrt(pa.shape[0]))
|
| 1886 |
+
results.append({
|
| 1887 |
+
"matches": m.cpu().numpy(),
|
| 1888 |
+
"scores": s.cpu().numpy(),
|
| 1889 |
+
"grid": grid,
|
| 1890 |
+
})
|
| 1891 |
+
return results[0] if single else results
|
| 1892 |
|
| 1893 |
@torch.inference_mode()
|
| 1894 |
def detect(
|
rf100vl_zero_shot_cross_domain_eval.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
| 1 |
{
|
| 2 |
"picker": {
|
| 3 |
-
"
|
|
|
|
| 4 |
"n_params": 2975067,
|
| 5 |
"text_embed_dim": 768
|
| 6 |
},
|
| 7 |
"fcos": {
|
| 8 |
-
"
|
| 9 |
"n_params": 16138074
|
| 10 |
},
|
| 11 |
-
"cache_dir": "
|
| 12 |
"resolution": 768,
|
| 13 |
"score_thresh": 0.05,
|
| 14 |
"max_per_image": 100,
|
|
|
|
| 1 |
{
|
| 2 |
"picker": {
|
| 3 |
+
"head": "argus_text_aligned_detection_head",
|
| 4 |
+
"config": "split_tower_5scale_160h_5std_4dw_ema_l14_16ep_768_cls_calib",
|
| 5 |
"n_params": 2975067,
|
| 6 |
"text_embed_dim": 768
|
| 7 |
},
|
| 8 |
"fcos": {
|
| 9 |
+
"head": "argus_fcos_detection_head",
|
| 10 |
"n_params": 16138074
|
| 11 |
},
|
| 12 |
+
"cache_dir": "rf100vl_val_cache_768",
|
| 13 |
"resolution": 768,
|
| 14 |
"score_thresh": 0.05,
|
| 15 |
"max_per_image": 100,
|