Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +6 -0
- README.md +41 -7
- app.py +270 -0
- examples/cafe_interior.jpg +3 -0
- examples/girl_with_dog.jpg +3 -0
- examples/living_room_blue_couch.jpg +3 -0
- examples/motorcycle_street.jpg +3 -0
- examples/pizza_board.jpg +3 -0
- examples/skateboarder_rail.jpg +3 -0
- objectmodel_v1.yaml +61 -0
- objectmodel_v1/__init__.py +6 -0
- objectmodel_v1/boxes.py +67 -0
- objectmodel_v1/config.py +38 -0
- objectmodel_v1/model.py +413 -0
- objectmodel_v1/postprocess.py +32 -0
- requirements.txt +3 -0
.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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.24.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
examples/girl_with_dog.jpg
ADDED
|
Git LFS Details
|
examples/living_room_blue_couch.jpg
ADDED
|
Git LFS Details
|
examples/motorcycle_street.jpg
ADDED
|
Git LFS Details
|
examples/pizza_board.jpg
ADDED
|
Git LFS Details
|
examples/skateboarder_rail.jpg
ADDED
|
Git LFS Details
|
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
|