multimodalart HF Staff commited on
Commit
d6ea92f
·
verified ·
1 Parent(s): 95a86f7

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/cafe_interior.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/girl_with_dog.jpg filter=lfs diff=lfs merge=lfs -text
38
+ examples/living_room_blue_couch.jpg filter=lfs diff=lfs merge=lfs -text
39
+ examples/motorcycle_street.jpg filter=lfs diff=lfs merge=lfs -text
40
+ examples/pizza_board.jpg filter=lfs diff=lfs merge=lfs -text
41
+ examples/skateboarder_rail.jpg filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,47 @@
1
  ---
2
- title: Objectmodel V1 Detection
3
- emoji: 🌍
4
- colorFrom: yellow
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.24.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ObjectModel-v1 Detection
3
+ emoji: 🔍
4
+ colorFrom: red
5
+ colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.24.0
 
8
  app_file: app.py
9
+ short_description: NMS-free COCO object detection with ObjectModel-v1
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 30m
12
  ---
13
 
14
+ # ObjectModel-v1 compact NMS-free object detection
15
+
16
+ Demo of [`bench-labs/objectmodel-v1`](https://huggingface.co/bench-labs/objectmodel-v1), a
17
+ 40.8M-parameter clean-room object detector trained from scratch on COCO 2017
18
+ (AP 0.358 / AP50 0.544 at epoch 95).
19
+
20
+ The architecture compresses multi-scale pyramid features into a fixed 64-slot latent memory
21
+ for global semantics, then recovers geometry with query-conditioned local sampling whose
22
+ radius scales with each query's current box. Prediction is a fixed set of 300 object
23
+ queries trained with Hungarian matching — no anchors and no NMS.
24
+
25
+ ## What this Space does
26
+
27
+ - Letterboxes the uploaded image to 640×640 (grey padding, ImageNet normalisation), exactly
28
+ as the upstream evaluation pipeline does.
29
+ - Runs a single forward pass of the released EMA checkpoint (`objectmodel_v1_best.pt`) on a
30
+ ZeroGPU worker.
31
+ - Takes the per-query max-sigmoid score, keeps the top-k above the confidence threshold, and
32
+ maps boxes back into original image coordinates.
33
+ - Renders boxes plus a table of `label / score / x0 y0 x1 y1`.
34
+
35
+ ## Notes
36
+
37
+ - Small objects are the model's known weak spot (AP_small 0.188 vs AP_large 0.493), and
38
+ out-of-domain footage degrades faster than COCO-style photography.
39
+ - Because there is no NMS, overlapping duplicate boxes are suppressed by the set-prediction
40
+ training rather than post-processing; raising the confidence threshold is the intended way
41
+ to clean up marginal detections.
42
+
43
+ ## Credits
44
+
45
+ Model and inference code © Bench Labs, Apache-2.0. The `objectmodel_v1/` package in this
46
+ Space is vendored unchanged from the model repository. Example images come from
47
+ [`linoyts/repo-to-space-example-inputs`](https://huggingface.co/datasets/linoyts/repo-to-space-example-inputs).
app.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio demo for ObjectModel-v1 — a compact, NMS-free COCO object detector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+
8
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
9
+
10
+ import spaces # noqa: E402 — must precede torch / any CUDA-touching import
11
+
12
+ import numpy as np # noqa: E402
13
+ import torch # noqa: E402
14
+ import gradio as gr # noqa: E402
15
+ from PIL import Image, ImageDraw, ImageFont # noqa: E402
16
+ from huggingface_hub import hf_hub_download # noqa: E402
17
+
18
+ from objectmodel_v1.boxes import box_cxcywh_to_xyxy # noqa: E402
19
+ from objectmodel_v1.config import load_config # noqa: E402
20
+ from objectmodel_v1.model import build_model # noqa: E402
21
+
22
+ MODEL_ID = "bench-labs/objectmodel-v1"
23
+ CHECKPOINT_FILE = "objectmodel_v1_best.pt"
24
+ CONFIG_FILE = "objectmodel_v1.yaml"
25
+
26
+ # The training dataloader maps COCO category ids -> contiguous labels by ascending
27
+ # category id, so label i is the i-th COCO category in id order.
28
+ COCO_CLASSES = [
29
+ "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck",
30
+ "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench",
31
+ "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra",
32
+ "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
33
+ "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove",
34
+ "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup",
35
+ "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
36
+ "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
37
+ "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse",
38
+ "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",
39
+ "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier",
40
+ "toothbrush",
41
+ ]
42
+
43
+ # ImageNet statistics, matching the training-time preprocessing.
44
+ MEAN = torch.tensor([0.485, 0.456, 0.406], dtype=torch.float32)[:, None, None]
45
+ STD = torch.tensor([0.229, 0.224, 0.225], dtype=torch.float32)[:, None, None]
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Model — loaded once at module scope and moved to CUDA eagerly (ZeroGPU rule 2).
49
+ # ---------------------------------------------------------------------------
50
+ _config = load_config(CONFIG_FILE)
51
+ _checkpoint_path = hf_hub_download(MODEL_ID, CHECKPOINT_FILE)
52
+
53
+ model = build_model(_config)
54
+ _state = torch.load(_checkpoint_path, map_location="cpu", weights_only=False)
55
+ model.load_state_dict(_state.get("ema", _state.get("model", _state)))
56
+ del _state
57
+ model.eval().to("cuda")
58
+
59
+ INPUT_SIZE = int(model.spec.input_size)
60
+ NUM_PARAMS = sum(p.numel() for p in model.parameters())
61
+ print(f"ObjectModel-v1 loaded: {NUM_PARAMS/1e6:.1f}M params, input {INPUT_SIZE}px", flush=True)
62
+
63
+
64
+ def letterbox(image: Image.Image, size: int) -> tuple[torch.Tensor, float, int, int]:
65
+ """Resize keeping aspect ratio and pad to a square canvas (eval-time transform)."""
66
+ width, height = image.size
67
+ ratio = min(size / width, size / height)
68
+ resized_width = max(1, round(width * ratio))
69
+ resized_height = max(1, round(height * ratio))
70
+ resized = image.resize((resized_width, resized_height), Image.Resampling.BILINEAR)
71
+ offset_x = (size - resized_width) // 2
72
+ offset_y = (size - resized_height) // 2
73
+ canvas = Image.new("RGB", (size, size), (114, 114, 114))
74
+ canvas.paste(resized, (offset_x, offset_y))
75
+ array = np.asarray(canvas, dtype=np.float32).copy() / 255.0
76
+ tensor = torch.from_numpy(array).permute(2, 0, 1)
77
+ tensor = (tensor - MEAN) / STD
78
+ return tensor, ratio, offset_x, offset_y
79
+
80
+
81
+ def palette(index: int) -> tuple[int, int, int]:
82
+ """Stable, well-spread colour per class index."""
83
+ hue = (index * 0.6180339887) % 1.0
84
+ i = int(hue * 6)
85
+ f = hue * 6 - i
86
+ q, t = 1 - f, f
87
+ table = [(1, t, 0), (q, 1, 0), (0, 1, t), (0, q, 1), (t, 0, 1), (1, 0, q)]
88
+ r, g, b = table[i % 6]
89
+ return (int(70 + 185 * r), int(70 + 185 * g), int(70 + 185 * b))
90
+
91
+
92
+ def _font(size: int):
93
+ for name in ("DejaVuSans-Bold.ttf", "DejaVuSans.ttf"):
94
+ try:
95
+ return ImageFont.truetype(name, size)
96
+ except Exception:
97
+ continue
98
+ return ImageFont.load_default()
99
+
100
+
101
+ def draw_detections(image: Image.Image, detections: list[dict]) -> Image.Image:
102
+ canvas = image.convert("RGB").copy()
103
+ draw = ImageDraw.Draw(canvas)
104
+ scale = max(canvas.width, canvas.height) / 640.0
105
+ thickness = max(2, round(3 * scale))
106
+ font = _font(max(13, round(16 * scale)))
107
+ for det in detections:
108
+ x0, y0, x1, y1 = det["box"]
109
+ colour = palette(det["label_index"])
110
+ draw.rectangle([x0, y0, x1, y1], outline=colour, width=thickness)
111
+ caption = f"{det['label']} {det['score']:.2f}"
112
+ left, top, right, bottom = draw.textbbox((0, 0), caption, font=font)
113
+ text_w, text_h = right - left, bottom - top
114
+ pad = max(2, round(3 * scale))
115
+ box_h = text_h + 2 * pad
116
+ text_y = y0 - box_h if y0 - box_h >= 0 else y0
117
+ draw.rectangle([x0, text_y, x0 + text_w + 2 * pad, text_y + box_h], fill=colour)
118
+ draw.text((x0 + pad - left, text_y + pad - top), caption, fill=(20, 20, 20), font=font)
119
+ return canvas
120
+
121
+
122
+ @spaces.GPU(duration=45)
123
+ def detect(
124
+ image: Image.Image,
125
+ confidence: float = 0.35,
126
+ max_detections: int = 100,
127
+ ) -> tuple[Image.Image, list[list], str]:
128
+ """Detect COCO objects in an image with ObjectModel-v1.
129
+
130
+ Args:
131
+ image: input photograph to run detection on.
132
+ confidence: minimum score (0-1) a detection must reach to be kept.
133
+ max_detections: hard cap on how many boxes are returned.
134
+
135
+ Returns:
136
+ The image with boxes drawn, a table of detections, and a short summary.
137
+ """
138
+ if image is None:
139
+ raise gr.Error("Please upload an image first.")
140
+
141
+ image = image.convert("RGB")
142
+ original_width, original_height = image.size
143
+ tensor, ratio, offset_x, offset_y = letterbox(image, INPUT_SIZE)
144
+
145
+ started = time.perf_counter()
146
+ with torch.inference_mode():
147
+ outputs = model(tensor[None].to("cuda"))
148
+ probabilities = outputs["pred_logits"].sigmoid()[0]
149
+ boxes = box_cxcywh_to_xyxy(outputs["pred_boxes"][0]).clamp(0.0, 1.0) * INPUT_SIZE
150
+ scores, labels = probabilities.max(dim=-1)
151
+ count = min(int(max_detections), scores.numel())
152
+ scores, indices = scores.topk(count)
153
+ keep = scores >= float(confidence)
154
+ scores, indices = scores[keep], indices[keep]
155
+ labels = labels[indices]
156
+ boxes = boxes[indices]
157
+ # Undo the letterbox transform back into original image coordinates.
158
+ boxes[:, [0, 2]] = (boxes[:, [0, 2]] - offset_x) / ratio
159
+ boxes[:, [1, 3]] = (boxes[:, [1, 3]] - offset_y) / ratio
160
+ boxes[:, [0, 2]] = boxes[:, [0, 2]].clamp(0, original_width)
161
+ boxes[:, [1, 3]] = boxes[:, [1, 3]].clamp(0, original_height)
162
+ scores = scores.float().cpu().tolist()
163
+ labels = labels.cpu().tolist()
164
+ boxes = boxes.float().cpu().tolist()
165
+ elapsed = time.perf_counter() - started
166
+
167
+ detections = [
168
+ {
169
+ "label": COCO_CLASSES[label] if label < len(COCO_CLASSES) else str(label),
170
+ "label_index": int(label),
171
+ "score": float(score),
172
+ "box": [float(v) for v in box],
173
+ }
174
+ for score, label, box in zip(scores, labels, boxes)
175
+ ]
176
+
177
+ annotated = draw_detections(image, detections)
178
+ table = [
179
+ [
180
+ det["label"],
181
+ round(det["score"], 3),
182
+ round(det["box"][0]),
183
+ round(det["box"][1]),
184
+ round(det["box"][2]),
185
+ round(det["box"][3]),
186
+ ]
187
+ for det in detections
188
+ ]
189
+ if not table:
190
+ table = [["(nothing above threshold)", 0.0, 0, 0, 0, 0]]
191
+ summary = (
192
+ f"**{len(detections)} object(s)** above {float(confidence):.2f} confidence · "
193
+ f"forward pass {elapsed * 1000:.0f} ms · NMS-free (300 queries)"
194
+ )
195
+ return annotated, table, summary
196
+
197
+
198
+ CSS = """
199
+ #col-container { max-width: 1200px; margin: 0 auto; }
200
+ .dark .gradio-container { color: var(--body-text-color); }
201
+ """
202
+
203
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="ObjectModel-v1 Detection") as demo:
204
+ with gr.Column(elem_id="col-container"):
205
+ gr.Markdown(
206
+ """
207
+ # ObjectModel-v1 — compact NMS-free object detection
208
+
209
+ A 40.8M-parameter clean-room detector from Bench Labs: compressed global latent
210
+ memory + box-scaled local pyramid sampling, trained from scratch on COCO
211
+ (AP 0.358). No anchors, no NMS — a fixed set of 300 queries.
212
+
213
+ [Model card](https://huggingface.co/bench-labs/objectmodel-v1)
214
+ """
215
+ )
216
+ with gr.Row():
217
+ with gr.Column(scale=1):
218
+ image_input = gr.Image(label="Input image", type="pil", height=380)
219
+ confidence = gr.Slider(
220
+ label="Confidence threshold",
221
+ minimum=0.05,
222
+ maximum=0.95,
223
+ step=0.01,
224
+ value=0.35,
225
+ )
226
+ run_button = gr.Button("Detect objects", variant="primary")
227
+ with gr.Accordion("Advanced settings", open=False):
228
+ max_detections = gr.Slider(
229
+ label="Max detections",
230
+ minimum=1,
231
+ maximum=300,
232
+ step=1,
233
+ value=100,
234
+ info="Top-k queries kept before the confidence filter.",
235
+ )
236
+ with gr.Column(scale=1):
237
+ image_output = gr.Image(label="Detections", type="pil", height=380)
238
+ summary_output = gr.Markdown()
239
+ detections_output = gr.Dataframe(
240
+ headers=["label", "score", "x0", "y0", "x1", "y1"],
241
+ label="Detections",
242
+ wrap=True,
243
+ row_count=(1, "dynamic"),
244
+ )
245
+
246
+ gr.Examples(
247
+ examples=[
248
+ ["examples/skateboarder_rail.jpg"],
249
+ ["examples/motorcycle_street.jpg"],
250
+ ["examples/cafe_interior.jpg"],
251
+ ["examples/living_room_blue_couch.jpg"],
252
+ ["examples/girl_with_dog.jpg"],
253
+ ["examples/pizza_board.jpg"],
254
+ ],
255
+ inputs=[image_input],
256
+ outputs=[image_output, detections_output, summary_output],
257
+ fn=detect,
258
+ cache_examples=True,
259
+ cache_mode="lazy",
260
+ )
261
+
262
+ run_button.click(
263
+ fn=detect,
264
+ inputs=[image_input, confidence, max_detections],
265
+ outputs=[image_output, detections_output, summary_output],
266
+ api_name="detect",
267
+ )
268
+
269
+ if __name__ == "__main__":
270
+ demo.launch(mcp_server=True)
examples/cafe_interior.jpg ADDED

Git LFS Details

  • SHA256: 3110d62ff20ae4ca98b3553e12e8e11ae53ffe1c4de6ac0987bb8e6ae68d337e
  • Pointer size: 131 Bytes
  • Size of remote file: 177 kB
examples/girl_with_dog.jpg ADDED

Git LFS Details

  • SHA256: cd89ec7a09a3039ca34f4c8d7a831fafd4e7a23ac8af8d2b2a330126c0e87471
  • Pointer size: 131 Bytes
  • Size of remote file: 171 kB
examples/living_room_blue_couch.jpg ADDED

Git LFS Details

  • SHA256: 31509acfb170d47fc181d7a1ead26c35081b53a7dc7ed14b4aedf02dea866dea
  • Pointer size: 131 Bytes
  • Size of remote file: 112 kB
examples/motorcycle_street.jpg ADDED

Git LFS Details

  • SHA256: 03233a365e2b92d1c557bfdd5c3e3bb0b578875ba699394895899f291a6a4b13
  • Pointer size: 131 Bytes
  • Size of remote file: 182 kB
examples/pizza_board.jpg ADDED

Git LFS Details

  • SHA256: 527a1af77249c162cbd9cc58da1feeac551ee7fcd0ab15e3b8fe6c9c7a233271
  • Pointer size: 131 Bytes
  • Size of remote file: 327 kB
examples/skateboarder_rail.jpg ADDED

Git LFS Details

  • SHA256: 60d969bc469e6a2fa41ff5cabe3642d8e99da0e524d77851ab78858f0a5d59c9
  • Pointer size: 131 Bytes
  • Size of remote file: 119 kB
objectmodel_v1.yaml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model:
2
+ num_classes: 80
3
+ input_size: 640
4
+ stem_channels: 64
5
+ backbone_channels: [96, 192, 384, 512]
6
+ backbone_depths: [2, 3, 6, 3]
7
+ hidden_dim: 384
8
+ fpn_depth: 2
9
+ latent_count: 64
10
+ latent_pool_sizes: [12, 6, 3]
11
+ latent_layers: 2
12
+ decoder_layers: 8
13
+ num_queries: 300
14
+ num_heads: 8
15
+ local_points: 4
16
+ dropout: 0.0
17
+ dense_aux: true
18
+
19
+ loss:
20
+ cost_class: 2.0
21
+ cost_bbox: 5.0
22
+ cost_giou: 2.0
23
+ weight_class: 2.0
24
+ weight_bbox: 5.0
25
+ weight_giou: 2.0
26
+ weight_dense: 1.0
27
+ focal_alpha: 0.25
28
+ focal_gamma: 2.0
29
+ aux_weight: 1.0
30
+ dense_topk: 5
31
+
32
+ train:
33
+ epochs: 150
34
+ batch_size: 16
35
+ eval_batch_size: 8
36
+ workers: 8
37
+ prefetch_factor: 4
38
+ lr: 0.0002
39
+ backbone_lr: 0.0001
40
+ min_lr_ratio: 0.05
41
+ weight_decay: 0.05
42
+ warmup_steps: 1500
43
+ clip_grad_norm: 0.1
44
+ amp: true
45
+ amp_dtype: bfloat16
46
+ channels_last: false
47
+ compile: false
48
+ ema_decay: 0.9998
49
+ seed: 42
50
+ eval_every: 1
51
+ print_freq: 50
52
+
53
+ data:
54
+ train_image_dir: train2017
55
+ train_annotations: annotations/instances_train2017.json
56
+ val_image_dir: val2017
57
+ val_annotations: annotations/instances_val2017.json
58
+ hflip_prob: 0.5
59
+ scale_range: [0.65, 1.0]
60
+ mean: [0.485, 0.456, 0.406]
61
+ std: [0.229, 0.224, 0.225]
objectmodel_v1/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """ObjectModel-v1 compact object detection research package."""
2
+
3
+ from .model import ObjectModelV1, ObjectModelV1Spec, build_model
4
+
5
+ __all__ = ["ObjectModelV1", "ObjectModelV1Spec", "build_model"]
6
+ __version__ = "0.1.0"
objectmodel_v1/boxes.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ from torch import Tensor
5
+
6
+
7
+ def box_cxcywh_to_xyxy(boxes: Tensor) -> Tensor:
8
+ cx, cy, width, height = boxes.unbind(-1)
9
+ return torch.stack(
10
+ (cx - 0.5 * width, cy - 0.5 * height, cx + 0.5 * width, cy + 0.5 * height),
11
+ dim=-1,
12
+ )
13
+
14
+
15
+ def box_xyxy_to_cxcywh(boxes: Tensor) -> Tensor:
16
+ x0, y0, x1, y1 = boxes.unbind(-1)
17
+ return torch.stack(
18
+ ((x0 + x1) * 0.5, (y0 + y1) * 0.5, x1 - x0, y1 - y0), dim=-1
19
+ )
20
+
21
+
22
+ def box_area(boxes: Tensor) -> Tensor:
23
+ return (boxes[..., 2] - boxes[..., 0]).clamp(min=0) * (
24
+ boxes[..., 3] - boxes[..., 1]
25
+ ).clamp(min=0)
26
+
27
+
28
+ def box_iou(boxes1: Tensor, boxes2: Tensor) -> tuple[Tensor, Tensor]:
29
+ area1 = box_area(boxes1)
30
+ area2 = box_area(boxes2)
31
+ top_left = torch.maximum(boxes1[:, None, :2], boxes2[:, :2])
32
+ bottom_right = torch.minimum(boxes1[:, None, 2:], boxes2[:, 2:])
33
+ intersection = (bottom_right - top_left).clamp(min=0).prod(dim=-1)
34
+ union = area1[:, None] + area2 - intersection
35
+ return intersection / union.clamp(min=1e-7), union
36
+
37
+
38
+ def generalized_box_iou(boxes1: Tensor, boxes2: Tensor) -> Tensor:
39
+ """Pairwise generalized IoU for boxes in x0, y0, x1, y1 format."""
40
+ iou, union = box_iou(boxes1, boxes2)
41
+ top_left = torch.minimum(boxes1[:, None, :2], boxes2[:, :2])
42
+ bottom_right = torch.maximum(boxes1[:, None, 2:], boxes2[:, 2:])
43
+ enclosing = (bottom_right - top_left).clamp(min=0).prod(dim=-1)
44
+ return iou - (enclosing - union) / enclosing.clamp(min=1e-7)
45
+
46
+
47
+ def generalized_box_iou_batched(boxes1: Tensor, boxes2: Tensor) -> Tensor:
48
+ """Batched pairwise generalized IoU, boxes in x0, y0, x1, y1 format.
49
+
50
+ boxes1: [B, N, 4], boxes2: [B, M, 4] -> [B, N, M]
51
+ """
52
+ area1 = box_area(boxes1)
53
+ area2 = box_area(boxes2)
54
+ top_left = torch.maximum(boxes1[:, :, None, :2], boxes2[:, None, :, :2])
55
+ bottom_right = torch.minimum(boxes1[:, :, None, 2:], boxes2[:, None, :, 2:])
56
+ intersection = (bottom_right - top_left).clamp(min=0).prod(dim=-1)
57
+ union = area1[:, :, None] + area2[:, None, :] - intersection
58
+ iou = intersection / union.clamp(min=1e-7)
59
+ enc_top_left = torch.minimum(boxes1[:, :, None, :2], boxes2[:, None, :, :2])
60
+ enc_bottom_right = torch.maximum(boxes1[:, :, None, 2:], boxes2[:, None, :, 2:])
61
+ enclosing = (enc_bottom_right - enc_top_left).clamp(min=0).prod(dim=-1)
62
+ return iou - (enclosing - union) / enclosing.clamp(min=1e-7)
63
+
64
+
65
+ def inverse_sigmoid(value: Tensor, eps: float = 1e-5) -> Tensor:
66
+ value = value.clamp(min=0.0, max=1.0)
67
+ return torch.log(value.clamp(min=eps) / (1.0 - value).clamp(min=eps))
objectmodel_v1/config.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+
10
+ def load_config(path: str | Path) -> dict[str, Any]:
11
+ with Path(path).open("r", encoding="utf-8") as handle:
12
+ config = yaml.safe_load(handle)
13
+ if not isinstance(config, dict):
14
+ raise ValueError(f"Configuration must be a mapping: {path}")
15
+ return config
16
+
17
+
18
+ def apply_overrides(config: dict[str, Any], overrides: list[str]) -> dict[str, Any]:
19
+ result = deepcopy(config)
20
+ for item in overrides:
21
+ if "=" not in item:
22
+ raise ValueError(f"Override must have key=value form: {item}")
23
+ dotted_key, raw_value = item.split("=", 1)
24
+ keys = dotted_key.split(".")
25
+ node = result
26
+ for key in keys[:-1]:
27
+ if key not in node or not isinstance(node[key], dict):
28
+ raise KeyError(f"Unknown configuration path: {dotted_key}")
29
+ node = node[key]
30
+ if keys[-1] not in node:
31
+ raise KeyError(f"Unknown configuration key: {dotted_key}")
32
+ node[keys[-1]] = yaml.safe_load(raw_value)
33
+ return result
34
+
35
+
36
+ def save_config(config: dict[str, Any], path: str | Path) -> None:
37
+ with Path(path).open("w", encoding="utf-8") as handle:
38
+ yaml.safe_dump(config, handle, sort_keys=False)
objectmodel_v1/model.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import torch
8
+ from torch import Tensor, nn
9
+ import torch.nn.functional as F
10
+
11
+ from .boxes import inverse_sigmoid
12
+
13
+
14
+ class ConvNormAct(nn.Sequential):
15
+ def __init__(
16
+ self,
17
+ in_channels: int,
18
+ out_channels: int,
19
+ kernel_size: int = 1,
20
+ stride: int = 1,
21
+ groups: int = 1,
22
+ activation: bool = True,
23
+ ) -> None:
24
+ padding = kernel_size // 2
25
+ layers: list[nn.Module] = [
26
+ nn.Conv2d(
27
+ in_channels,
28
+ out_channels,
29
+ kernel_size,
30
+ stride,
31
+ padding,
32
+ groups=groups,
33
+ bias=False,
34
+ ),
35
+ nn.BatchNorm2d(out_channels),
36
+ ]
37
+ if activation:
38
+ layers.append(nn.SiLU(inplace=True))
39
+ super().__init__(*layers)
40
+
41
+
42
+ class GatedConvBlock(nn.Module):
43
+ """Inverted residual block with a cheap learned residual gate."""
44
+
45
+ def __init__(self, channels: int, expansion: float = 2.0) -> None:
46
+ super().__init__()
47
+ hidden = int(channels * expansion)
48
+ self.expand = ConvNormAct(channels, hidden)
49
+ self.depthwise = ConvNormAct(hidden, hidden, 3, groups=hidden)
50
+ self.project = ConvNormAct(hidden, channels, activation=False)
51
+ self.gate = nn.Parameter(torch.zeros(1))
52
+
53
+ def forward(self, inputs: Tensor) -> Tensor:
54
+ return inputs + torch.tanh(self.gate) * self.project(self.depthwise(self.expand(inputs)))
55
+
56
+
57
+ class BackboneStage(nn.Sequential):
58
+ def __init__(self, in_channels: int, out_channels: int, depth: int, stride: int) -> None:
59
+ super().__init__(
60
+ ConvNormAct(in_channels, out_channels, 3, stride=stride),
61
+ *(GatedConvBlock(out_channels) for _ in range(depth)),
62
+ )
63
+
64
+
65
+ class CompactBackbone(nn.Module):
66
+ def __init__(
67
+ self, stem_channels: int, channels: list[int], depths: list[int]
68
+ ) -> None:
69
+ super().__init__()
70
+ if len(channels) != 4 or len(depths) != 4:
71
+ raise ValueError("Backbone requires four channel and depth values")
72
+ self.stem = nn.Sequential(
73
+ ConvNormAct(3, stem_channels, 3, stride=2),
74
+ ConvNormAct(stem_channels, stem_channels, 3, stride=2),
75
+ )
76
+ stages: list[nn.Module] = []
77
+ in_channels = stem_channels
78
+ for index, (out_channels, depth) in enumerate(zip(channels, depths, strict=True)):
79
+ stages.append(
80
+ BackboneStage(in_channels, out_channels, depth, stride=1 if index == 0 else 2)
81
+ )
82
+ in_channels = out_channels
83
+ self.stages = nn.ModuleList(stages)
84
+ self.out_channels = channels[1:]
85
+
86
+ def forward(self, images: Tensor) -> list[Tensor]:
87
+ features = self.stem(images)
88
+ outputs = []
89
+ for index, stage in enumerate(self.stages):
90
+ features = stage(features)
91
+ if index > 0:
92
+ outputs.append(features)
93
+ return outputs
94
+
95
+
96
+ class PyramidFusion(nn.Module):
97
+ def __init__(self, in_channels: list[int], hidden_dim: int, depth: int) -> None:
98
+ super().__init__()
99
+ self.lateral = nn.ModuleList(ConvNormAct(c, hidden_dim) for c in in_channels)
100
+ self.refine = nn.ModuleList(
101
+ nn.Sequential(*(GatedConvBlock(hidden_dim, expansion=1.5) for _ in range(depth)))
102
+ for _ in in_channels
103
+ )
104
+
105
+ def forward(self, inputs: list[Tensor]) -> list[Tensor]:
106
+ projected = [layer(x) for layer, x in zip(self.lateral, inputs, strict=True)]
107
+ outputs = list(projected)
108
+ for index in range(len(outputs) - 2, -1, -1):
109
+ outputs[index] = outputs[index] + F.interpolate(
110
+ outputs[index + 1], size=outputs[index].shape[-2:], mode="nearest"
111
+ )
112
+ return [block(x) for block, x in zip(self.refine, outputs, strict=True)]
113
+
114
+
115
+ def sine_position_encoding(
116
+ height: int, width: int, dim: int, device: torch.device, dtype: torch.dtype
117
+ ) -> Tensor:
118
+ if dim % 4 != 0:
119
+ raise ValueError("Position encoding dimension must be divisible by four")
120
+ y, x = torch.meshgrid(
121
+ torch.linspace(0, 1, height, device=device, dtype=dtype),
122
+ torch.linspace(0, 1, width, device=device, dtype=dtype),
123
+ indexing="ij",
124
+ )
125
+ frequencies = torch.arange(dim // 4, device=device, dtype=dtype)
126
+ frequencies = 2.0 * torch.pi * (10000.0 ** (-frequencies / max(dim // 4, 1)))
127
+ x = x.flatten()[:, None] * frequencies[None]
128
+ y = y.flatten()[:, None] * frequencies[None]
129
+ return torch.cat((x.sin(), x.cos(), y.sin(), y.cos()), dim=-1)
130
+
131
+
132
+ class FeedForward(nn.Sequential):
133
+ def __init__(self, dim: int, expansion: int = 4, dropout: float = 0.0) -> None:
134
+ super().__init__(
135
+ nn.Linear(dim, dim * expansion),
136
+ nn.GELU(),
137
+ nn.Dropout(dropout),
138
+ nn.Linear(dim * expansion, dim),
139
+ nn.Dropout(dropout),
140
+ )
141
+
142
+
143
+ class LatentLayer(nn.Module):
144
+ def __init__(self, dim: int, num_heads: int, dropout: float) -> None:
145
+ super().__init__()
146
+ self.norm1 = nn.LayerNorm(dim)
147
+ self.attention = nn.MultiheadAttention(dim, num_heads, dropout, batch_first=True)
148
+ self.norm2 = nn.LayerNorm(dim)
149
+ self.ffn = FeedForward(dim, dropout=dropout)
150
+
151
+ def forward(self, inputs: Tensor) -> Tensor:
152
+ normalized = self.norm1(inputs)
153
+ inputs = inputs + self.attention(normalized, normalized, normalized, need_weights=False)[0]
154
+ return inputs + self.ffn(self.norm2(inputs))
155
+
156
+
157
+ class LatentMemory(nn.Module):
158
+ """Compresses multi-scale maps into a fixed-size global reasoning memory."""
159
+
160
+ def __init__(
161
+ self,
162
+ dim: int,
163
+ latent_count: int,
164
+ pool_sizes: list[int],
165
+ layers: int,
166
+ num_heads: int,
167
+ dropout: float,
168
+ ) -> None:
169
+ super().__init__()
170
+ if len(pool_sizes) != 3:
171
+ raise ValueError("One latent pool size is required for each pyramid level")
172
+ self.pool_sizes = pool_sizes
173
+ self.latents = nn.Parameter(torch.empty(latent_count, dim))
174
+ self.level_embedding = nn.Parameter(torch.empty(len(pool_sizes), dim))
175
+ self.query_norm = nn.LayerNorm(dim)
176
+ self.token_norm = nn.LayerNorm(dim)
177
+ self.compress = nn.MultiheadAttention(dim, num_heads, dropout, batch_first=True)
178
+ self.layers = nn.ModuleList(LatentLayer(dim, num_heads, dropout) for _ in range(layers))
179
+ nn.init.normal_(self.latents, std=0.02)
180
+ nn.init.normal_(self.level_embedding, std=0.02)
181
+
182
+ def forward(self, features: list[Tensor]) -> Tensor:
183
+ tokens = []
184
+ for level, (feature, size) in enumerate(zip(features, self.pool_sizes, strict=True)):
185
+ pooled = F.adaptive_avg_pool2d(feature, (size, size)).flatten(2).transpose(1, 2)
186
+ position = sine_position_encoding(
187
+ size, size, feature.shape[1], feature.device, feature.dtype
188
+ )
189
+ tokens.append(pooled + position[None] + self.level_embedding[level][None, None])
190
+ token_memory = self.token_norm(torch.cat(tokens, dim=1))
191
+ latents = self.latents[None].expand(features[0].shape[0], -1, -1)
192
+ latents = latents + self.compress(
193
+ self.query_norm(latents), token_memory, token_memory, need_weights=False
194
+ )[0]
195
+ for layer in self.layers:
196
+ latents = layer(latents)
197
+ return latents
198
+
199
+
200
+ class QueryLocalSampler(nn.Module):
201
+ """Samples high-resolution pyramid evidence around each evolving query box."""
202
+
203
+ def __init__(self, dim: int, num_levels: int, points: int) -> None:
204
+ super().__init__()
205
+ self.num_levels = num_levels
206
+ self.points = points
207
+ self.offsets = nn.Linear(dim, num_levels * points * 2)
208
+ self.weights = nn.Linear(dim, num_levels * points)
209
+ self.output = nn.Linear(dim, dim)
210
+ nn.init.zeros_(self.offsets.weight)
211
+ nn.init.zeros_(self.offsets.bias)
212
+ nn.init.zeros_(self.weights.weight)
213
+ nn.init.zeros_(self.weights.bias)
214
+
215
+ def forward(self, queries: Tensor, boxes: Tensor, features: list[Tensor]) -> Tensor:
216
+ batch, query_count, _ = queries.shape
217
+ offsets = self.offsets(queries).view(
218
+ batch, query_count, self.num_levels, self.points, 2
219
+ )
220
+ offsets = offsets.tanh() * boxes[..., None, None, 2:] * 0.5
221
+ centers = boxes[..., None, None, :2]
222
+ sample_points = (centers + offsets).clamp(0.0, 1.0)
223
+ weights = self.weights(queries).view(
224
+ batch, query_count, self.num_levels * self.points
225
+ )
226
+ weights = weights.softmax(dim=-1).view(
227
+ batch, query_count, self.num_levels, self.points
228
+ )
229
+
230
+ sampled_levels = []
231
+ for level, feature in enumerate(features):
232
+ grid = sample_points[:, :, level] * 2.0 - 1.0
233
+ sampled = F.grid_sample(
234
+ feature,
235
+ grid,
236
+ mode="bilinear",
237
+ padding_mode="zeros",
238
+ align_corners=False,
239
+ )
240
+ sampled = sampled.permute(0, 2, 3, 1)
241
+ sampled_levels.append(sampled)
242
+ sampled_features = torch.stack(sampled_levels, dim=2)
243
+ fused = (sampled_features * weights[..., None]).sum(dim=(2, 3))
244
+ return self.output(fused)
245
+
246
+
247
+ class DecoderLayer(nn.Module):
248
+ def __init__(
249
+ self, dim: int, num_heads: int, num_levels: int, local_points: int, dropout: float
250
+ ) -> None:
251
+ super().__init__()
252
+ self.norm1 = nn.LayerNorm(dim)
253
+ self.self_attention = nn.MultiheadAttention(dim, num_heads, dropout, batch_first=True)
254
+ self.norm2 = nn.LayerNorm(dim)
255
+ self.global_attention = nn.MultiheadAttention(dim, num_heads, dropout, batch_first=True)
256
+ self.norm3 = nn.LayerNorm(dim)
257
+ self.local_sampler = QueryLocalSampler(dim, num_levels, local_points)
258
+ self.norm4 = nn.LayerNorm(dim)
259
+ self.ffn = FeedForward(dim, dropout=dropout)
260
+
261
+ def forward(
262
+ self, queries: Tensor, memory: Tensor, boxes: Tensor, features: list[Tensor]
263
+ ) -> Tensor:
264
+ normalized = self.norm1(queries)
265
+ queries = queries + self.self_attention(
266
+ normalized, normalized, normalized, need_weights=False
267
+ )[0]
268
+ queries = queries + self.global_attention(
269
+ self.norm2(queries), memory, memory, need_weights=False
270
+ )[0]
271
+ queries = queries + self.local_sampler(self.norm3(queries), boxes, features)
272
+ return queries + self.ffn(self.norm4(queries))
273
+
274
+
275
+ class MLP(nn.Sequential):
276
+ def __init__(self, input_dim: int, hidden_dim: int, output_dim: int, layers: int) -> None:
277
+ modules: list[nn.Module] = []
278
+ for index in range(layers):
279
+ in_dim = input_dim if index == 0 else hidden_dim
280
+ out_dim = output_dim if index == layers - 1 else hidden_dim
281
+ modules.append(nn.Linear(in_dim, out_dim))
282
+ if index < layers - 1:
283
+ modules.append(nn.ReLU(inplace=True))
284
+ super().__init__(*modules)
285
+
286
+
287
+ class DenseAuxiliaryHead(nn.Module):
288
+ def __init__(self, dim: int, num_classes: int) -> None:
289
+ super().__init__()
290
+ self.shared = nn.ModuleList(
291
+ nn.Sequential(ConvNormAct(dim, dim, 3, groups=dim), ConvNormAct(dim, dim))
292
+ for _ in range(3)
293
+ )
294
+ self.classification = nn.Conv2d(dim, num_classes, 1)
295
+ self.regression = nn.Conv2d(dim, 4, 1)
296
+
297
+ def forward(self, features: list[Tensor]) -> list[dict[str, Tensor]]:
298
+ outputs = []
299
+ for feature, tower in zip(features, self.shared, strict=True):
300
+ hidden = tower(feature)
301
+ outputs.append(
302
+ {
303
+ "logits": self.classification(hidden),
304
+ "distances": F.softplus(self.regression(hidden)),
305
+ }
306
+ )
307
+ return outputs
308
+
309
+
310
+ @dataclass(frozen=True)
311
+ class ObjectModelV1Spec:
312
+ num_classes: int = 80
313
+ input_size: int = 640
314
+ stem_channels: int = 48
315
+ backbone_channels: tuple[int, int, int, int] = (64, 128, 256, 384)
316
+ backbone_depths: tuple[int, int, int, int] = (2, 3, 6, 3)
317
+ hidden_dim: int = 256
318
+ fpn_depth: int = 2
319
+ latent_count: int = 64
320
+ latent_pool_sizes: tuple[int, int, int] = (12, 6, 3)
321
+ latent_layers: int = 2
322
+ decoder_layers: int = 6
323
+ num_queries: int = 300
324
+ num_heads: int = 8
325
+ local_points: int = 4
326
+ dropout: float = 0.0
327
+ dense_aux: bool = True
328
+
329
+
330
+ class ObjectModelV1(nn.Module):
331
+ """NMS-free detector with compressed global memory and local geometric sampling."""
332
+
333
+ def __init__(self, spec: ObjectModelV1Spec) -> None:
334
+ super().__init__()
335
+ self.spec = spec
336
+ self.backbone = CompactBackbone(
337
+ spec.stem_channels, list(spec.backbone_channels), list(spec.backbone_depths)
338
+ )
339
+ self.neck = PyramidFusion(self.backbone.out_channels, spec.hidden_dim, spec.fpn_depth)
340
+ self.memory = LatentMemory(
341
+ spec.hidden_dim,
342
+ spec.latent_count,
343
+ list(spec.latent_pool_sizes),
344
+ spec.latent_layers,
345
+ spec.num_heads,
346
+ spec.dropout,
347
+ )
348
+ decoder_template = DecoderLayer(
349
+ spec.hidden_dim, spec.num_heads, 3, spec.local_points, spec.dropout
350
+ )
351
+ self.decoder = nn.ModuleList(deepcopy(decoder_template) for _ in range(spec.decoder_layers))
352
+ self.query_embedding = nn.Embedding(spec.num_queries, spec.hidden_dim)
353
+ self.reference_points = nn.Embedding(spec.num_queries, 4)
354
+ self.class_heads = nn.ModuleList(
355
+ nn.Linear(spec.hidden_dim, spec.num_classes) for _ in range(spec.decoder_layers)
356
+ )
357
+ self.box_heads = nn.ModuleList(
358
+ MLP(spec.hidden_dim, spec.hidden_dim, 4, 3) for _ in range(spec.decoder_layers)
359
+ )
360
+ self.dense_head = (
361
+ DenseAuxiliaryHead(spec.hidden_dim, spec.num_classes) if spec.dense_aux else None
362
+ )
363
+ self._reset_parameters()
364
+
365
+ def _reset_parameters(self) -> None:
366
+ prior_probability = 0.01
367
+ class_bias = -torch.log(torch.tensor((1.0 - prior_probability) / prior_probability))
368
+ for head in self.class_heads:
369
+ nn.init.constant_(head.bias, class_bias)
370
+ nn.init.zeros_(self.reference_points.weight)
371
+ with torch.no_grad():
372
+ self.reference_points.weight[:, 2:] = -2.0
373
+ for head in self.box_heads:
374
+ nn.init.zeros_(head[-1].weight)
375
+ nn.init.zeros_(head[-1].bias)
376
+ if self.dense_head is not None:
377
+ nn.init.constant_(self.dense_head.classification.bias, class_bias)
378
+ nn.init.zeros_(self.dense_head.regression.weight)
379
+ nn.init.constant_(self.dense_head.regression.bias, 1.0)
380
+
381
+ def forward(self, images: Tensor) -> dict[str, Any]:
382
+ features = self.neck(self.backbone(images))
383
+ memory = self.memory(features)
384
+ batch = images.shape[0]
385
+ queries = self.query_embedding.weight[None].expand(batch, -1, -1)
386
+ boxes = self.reference_points.weight.sigmoid()[None].expand(batch, -1, -1)
387
+ layer_outputs: list[dict[str, Tensor]] = []
388
+ for layer, class_head, box_head in zip(
389
+ self.decoder, self.class_heads, self.box_heads, strict=True
390
+ ):
391
+ queries = layer(queries, memory, boxes, features)
392
+ boxes = (inverse_sigmoid(boxes) + box_head(queries)).sigmoid()
393
+ layer_outputs.append({"pred_logits": class_head(queries), "pred_boxes": boxes})
394
+ boxes = boxes.detach() if self.training else boxes
395
+
396
+ output: dict[str, Any] = dict(layer_outputs[-1])
397
+ output["aux_outputs"] = layer_outputs[:-1]
398
+ if self.training and self.dense_head is not None:
399
+ output["dense_outputs"] = self.dense_head(features)
400
+ return output
401
+
402
+
403
+ def build_model(config: dict[str, Any]) -> ObjectModelV1:
404
+ model_config = config.get("model", config)
405
+ fields = ObjectModelV1Spec.__dataclass_fields__
406
+ unknown = set(model_config) - set(fields)
407
+ if unknown:
408
+ raise ValueError(f"Unknown model configuration keys: {sorted(unknown)}")
409
+ values = dict(model_config)
410
+ for key in ("backbone_channels", "backbone_depths", "latent_pool_sizes"):
411
+ if key in values:
412
+ values[key] = tuple(values[key])
413
+ return ObjectModelV1(ObjectModelV1Spec(**values))
objectmodel_v1/postprocess.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ from torch import Tensor
5
+
6
+ from .boxes import box_cxcywh_to_xyxy
7
+
8
+
9
+ @torch.no_grad()
10
+ def decode_predictions(
11
+ outputs: dict[str, Tensor],
12
+ image_sizes: list[tuple[int, int]],
13
+ confidence: float = 0.25,
14
+ top_k: int = 300,
15
+ ) -> list[dict[str, Tensor]]:
16
+ logits = outputs["pred_logits"].sigmoid()
17
+ boxes = box_cxcywh_to_xyxy(outputs["pred_boxes"]).clamp(0.0, 1.0)
18
+ results = []
19
+ for index, (height, width) in enumerate(image_sizes):
20
+ scores, labels = logits[index].max(dim=-1)
21
+ keep = scores >= confidence
22
+ if keep.sum() > top_k:
23
+ selected = scores.masked_fill(~keep, -1).topk(top_k).indices
24
+ else:
25
+ selected = torch.where(keep)[0]
26
+ selected_boxes = boxes[index, selected].clone()
27
+ selected_boxes[:, [0, 2]] *= width
28
+ selected_boxes[:, [1, 3]] *= height
29
+ results.append(
30
+ {"scores": scores[selected], "labels": labels[selected], "boxes": selected_boxes}
31
+ )
32
+ return results
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ numpy
2
+ pillow
3
+ pyyaml