easydetect 0.7.0__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {easydetect-0.7.0 → easydetect-0.7.2}/PKG-INFO +27 -5
- {easydetect-0.7.0 → easydetect-0.7.2}/README.md +26 -4
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/__init__.py +1 -1
- easydetect-0.7.2/easydetect/benchmark.py +147 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/cli.py +18 -1
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/downloads.py +15 -2
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/decoder.py +6 -1
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/denoising.py +1 -1
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/encoder.py +10 -3
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/ops.py +14 -14
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/predictor.py +5 -1
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/trainer.py +28 -6
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/utils/loss.py +85 -42
- {easydetect-0.7.0 → easydetect-0.7.2}/pyproject.toml +1 -1
- easydetect-0.7.2/tests/test_benchmark.py +20 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_downloads.py +64 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_robustness.py +28 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/bench_finetune.py +115 -42
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/compare_yolo.py +35 -6
- easydetect-0.7.2/tools/pi_kit.py +123 -0
- easydetect-0.7.2/tools/profile_train.py +168 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/.gitignore +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/LICENSE +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/NOTICE +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/__init__.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/augment.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/dataset.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/keypoints.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/labels.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/errors.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/exporter.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/metrics.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/model.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/__init__.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/dfine_net.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/hgnetv2.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/posenet.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/sam_decoder.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/plotting.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/pose.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/pose_trainer.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/results.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/seg_trainer.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/segment.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/serve.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/sources.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/tracker.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/utils/__init__.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/utils/checkpoint.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/utils/ops.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/validator.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/__init__.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/conftest.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/pose_toy.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_augment.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_cli.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_coco2yolo.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_custom_pose.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_custom_seg.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_dataset.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_lab_api.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_model.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_nn.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_onnxruntime.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_plotting.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_pose.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_predictor.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_results.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_results_summary.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_segment.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_segment_lowres.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_serve.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_sources.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_sources_threads.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_tracker.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_train_export.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/bench.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/build_mirror.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/coco2yolo.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/convert_dfine.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/convert_sam.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/eval_exports.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/hub_README.md +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/make_demo.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/make_toyset.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/train_pose.py +0 -0
- {easydetect-0.7.0 → easydetect-0.7.2}/tools/voc2yolo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: easydetect
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Easy real-time object detection you can ship: D-FINE with PyTorch training, OpenVINO inference on CPU/GPU/NPU, and a browser app to label and train. Apache-2.0 throughout.
|
|
5
5
|
Project-URL: Homepage, https://github.com/themakerrobot/easydetect
|
|
6
6
|
Project-URL: Source, https://github.com/themakerrobot/easydetect
|
|
@@ -230,10 +230,32 @@ Read it plainly:
|
|
|
230
230
|
tasks (oriented boxes, classification, depth), and a far larger ecosystem.
|
|
231
231
|
|
|
232
232
|
COCO is a guide, not your answer. What matters is the model you get from a
|
|
233
|
-
few hundred pictures of your own
|
|
234
|
-
dfine-s and YOLO26s on the same public
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
few hundred pictures of your own. `tools/bench_finetune.py` fine-tunes
|
|
234
|
+
dfine-s (easydetect 0.7.0) and YOLO26s (Ultralytics 8.4) on the same public
|
|
235
|
+
datasets, each with its own trainer's defaults (100 epochs, 640 px, batch 16,
|
|
236
|
+
one RTX 5090), and scores both with one evaluator on the val split:
|
|
237
|
+
|
|
238
|
+
| dataset | train pictures | YOLO26s mAP50-95 | dfine-s mAP50-95 | YOLO26s, one class a box | dfine-s, one class a box |
|
|
239
|
+
| --- | --- | --- | --- | --- | --- |
|
|
240
|
+
| african-wildlife | 1,052 | 0.830 | **0.866** | 0.828 | **0.855** |
|
|
241
|
+
| construction-ppe | 1,132 | 0.289 | **0.303** | 0.288 | **0.303** |
|
|
242
|
+
| brain-tumor | 893 | 0.357 | **0.466** | 0.264 | **0.379** |
|
|
243
|
+
| signature | 143 | 0.984 | **0.991** | 0.984 | **0.991** |
|
|
244
|
+
| medical-pills | 92 | 0.801 | **0.806** | 0.801 | **0.806** |
|
|
245
|
+
| african-wildlife, 100 at random | 100 | 0.674 | **0.790** | 0.662 | **0.777** |
|
|
246
|
+
|
|
247
|
+
| | YOLO26s | dfine-s |
|
|
248
|
+
| --- | --- | --- |
|
|
249
|
+
| inference, a picture (OpenVINO, Core Ultra 5 250K, 640 px) | **23–25 ms** | 28–32 ms |
|
|
250
|
+
| training, 1,000 pictures × 100 epochs (RTX 5090) | **5 min** | 18 min |
|
|
251
|
+
|
|
252
|
+
* **Accuracy: dfine-s ahead on all six**, by little where both are near the
|
|
253
|
+
ceiling (signature, pills) and by 11–12 points where it is hard: two
|
|
254
|
+
look-alike classes (brain-tumor) and 100 pictures for four classes.
|
|
255
|
+
* **YOLO is about 20% faster to run and 3–4× faster to train.**
|
|
256
|
+
* mAP50-95 counts a box for every class it scores, as each project scores
|
|
257
|
+
itself; "one class a box" is what `predict` hands out. The 100-picture set
|
|
258
|
+
is noisy: the same code trained twice gave 0.790 and 0.815.
|
|
237
259
|
|
|
238
260
|
## Command line
|
|
239
261
|
|
|
@@ -187,10 +187,32 @@ Read it plainly:
|
|
|
187
187
|
tasks (oriented boxes, classification, depth), and a far larger ecosystem.
|
|
188
188
|
|
|
189
189
|
COCO is a guide, not your answer. What matters is the model you get from a
|
|
190
|
-
few hundred pictures of your own
|
|
191
|
-
dfine-s and YOLO26s on the same public
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
few hundred pictures of your own. `tools/bench_finetune.py` fine-tunes
|
|
191
|
+
dfine-s (easydetect 0.7.0) and YOLO26s (Ultralytics 8.4) on the same public
|
|
192
|
+
datasets, each with its own trainer's defaults (100 epochs, 640 px, batch 16,
|
|
193
|
+
one RTX 5090), and scores both with one evaluator on the val split:
|
|
194
|
+
|
|
195
|
+
| dataset | train pictures | YOLO26s mAP50-95 | dfine-s mAP50-95 | YOLO26s, one class a box | dfine-s, one class a box |
|
|
196
|
+
| --- | --- | --- | --- | --- | --- |
|
|
197
|
+
| african-wildlife | 1,052 | 0.830 | **0.866** | 0.828 | **0.855** |
|
|
198
|
+
| construction-ppe | 1,132 | 0.289 | **0.303** | 0.288 | **0.303** |
|
|
199
|
+
| brain-tumor | 893 | 0.357 | **0.466** | 0.264 | **0.379** |
|
|
200
|
+
| signature | 143 | 0.984 | **0.991** | 0.984 | **0.991** |
|
|
201
|
+
| medical-pills | 92 | 0.801 | **0.806** | 0.801 | **0.806** |
|
|
202
|
+
| african-wildlife, 100 at random | 100 | 0.674 | **0.790** | 0.662 | **0.777** |
|
|
203
|
+
|
|
204
|
+
| | YOLO26s | dfine-s |
|
|
205
|
+
| --- | --- | --- |
|
|
206
|
+
| inference, a picture (OpenVINO, Core Ultra 5 250K, 640 px) | **23–25 ms** | 28–32 ms |
|
|
207
|
+
| training, 1,000 pictures × 100 epochs (RTX 5090) | **5 min** | 18 min |
|
|
208
|
+
|
|
209
|
+
* **Accuracy: dfine-s ahead on all six**, by little where both are near the
|
|
210
|
+
ceiling (signature, pills) and by 11–12 points where it is hard: two
|
|
211
|
+
look-alike classes (brain-tumor) and 100 pictures for four classes.
|
|
212
|
+
* **YOLO is about 20% faster to run and 3–4× faster to train.**
|
|
213
|
+
* mAP50-95 counts a box for every class it scores, as each project scores
|
|
214
|
+
itself; "one class a box" is what `predict` hands out. The 100-picture set
|
|
215
|
+
is noisy: the same code trained twice gave 0.790 and 0.815.
|
|
194
216
|
|
|
195
217
|
## Command line
|
|
196
218
|
|
|
@@ -24,7 +24,7 @@ needs numpy/opencv/openvino/pyyaml; training adds
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
-
__version__ = "0.7.
|
|
27
|
+
__version__ = "0.7.2"
|
|
28
28
|
|
|
29
29
|
from .errors import DownloadError, EasyDetectError, ModelNotFoundError
|
|
30
30
|
from .metrics import BoxMetrics, DetMetrics
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Apache-2.0
|
|
2
|
+
"""How fast each model runs here: ``easydetect benchmark model=a.xml,b.onnx``.
|
|
3
|
+
|
|
4
|
+
Made for the machine the model will run on (a Raspberry Pi, a mini PC), so it
|
|
5
|
+
needs only the inference dependencies, no PyTorch. Every model is run on every
|
|
6
|
+
runtime that can read it (OpenVINO reads .xml and .onnx, ONNX Runtime .onnx),
|
|
7
|
+
on one picture, ``runs`` times after ``warmup``; the medians of preprocessing,
|
|
8
|
+
inference and decoding are printed as a table, fastest first.
|
|
9
|
+
|
|
10
|
+
``model=`` takes files, folders (every .xml / .onnx inside, at any depth),
|
|
11
|
+
model names (``dfine-n``: the mirror's ready-made 640 px IR and ONNX), or a
|
|
12
|
+
mix. With no
|
|
13
|
+
``source=`` the picture is a synthetic 640×480 frame, the size a Pi camera
|
|
14
|
+
usually delivers; the time a model takes does not depend on what it sees.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import platform
|
|
21
|
+
import statistics
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _model_files(spec) -> list[Path]:
|
|
28
|
+
items = spec if isinstance(spec, (list, tuple)) else [spec]
|
|
29
|
+
files: list[Path] = []
|
|
30
|
+
for item in items:
|
|
31
|
+
path = Path(str(item)).expanduser()
|
|
32
|
+
if not path.exists() and _is_name(str(item)):
|
|
33
|
+
# a model name (dfine-n): the mirror's ready-made IR and ONNX
|
|
34
|
+
from . import downloads
|
|
35
|
+
|
|
36
|
+
fetched = []
|
|
37
|
+
for fetch in (downloads.download_ir, downloads.download_onnx):
|
|
38
|
+
try:
|
|
39
|
+
fetched.append(fetch(str(item)))
|
|
40
|
+
except Exception as exc: # noqa: BLE001 — one format missing is fine
|
|
41
|
+
print(f"[easydetect] {item}: {exc}")
|
|
42
|
+
if not fetched:
|
|
43
|
+
raise FileNotFoundError(f"could not fetch {item} from the mirror (see above)")
|
|
44
|
+
files += fetched
|
|
45
|
+
continue
|
|
46
|
+
if path.is_dir():
|
|
47
|
+
files += sorted(p for p in path.rglob("*") if p.suffix in (".xml", ".onnx"))
|
|
48
|
+
elif path.exists():
|
|
49
|
+
files.append(path)
|
|
50
|
+
else:
|
|
51
|
+
raise FileNotFoundError(f"{path}: no such file or folder")
|
|
52
|
+
if not files:
|
|
53
|
+
raise FileNotFoundError(f"no .xml or .onnx models in {spec}")
|
|
54
|
+
return files
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_name(text: str) -> bool:
|
|
58
|
+
from . import downloads
|
|
59
|
+
|
|
60
|
+
return downloads.is_model_name(text)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _label(path: Path, spec) -> str:
|
|
64
|
+
"""A model's name in the table: its folder too when the folder says more
|
|
65
|
+
(pi_kit.py writes ``dfine-n-320-q100/dfine-n.xml``)."""
|
|
66
|
+
return f"{path.parent.name}/{path.name}" if path.parent.name not in ("", ".") else path.name
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _picture(source) -> np.ndarray:
|
|
70
|
+
if source is None:
|
|
71
|
+
rng = np.random.default_rng(0)
|
|
72
|
+
import cv2
|
|
73
|
+
|
|
74
|
+
noise = (rng.random((480, 640, 3)) * 255).astype(np.uint8)
|
|
75
|
+
return cv2.GaussianBlur(noise, (0, 0), 3)
|
|
76
|
+
import cv2
|
|
77
|
+
|
|
78
|
+
img = cv2.imread(str(source))
|
|
79
|
+
if img is None:
|
|
80
|
+
raise FileNotFoundError(f"cannot read {source}")
|
|
81
|
+
return img
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _predictor(path: Path, backend: str, device: str, threads: int | None):
|
|
85
|
+
from .predictor import ORTPredictor, OVPredictor
|
|
86
|
+
|
|
87
|
+
if backend == "openvino":
|
|
88
|
+
return OVPredictor(path, device=device, threads=threads)
|
|
89
|
+
return ORTPredictor(path, device=device, threads=threads)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def benchmark(model, source=None, runs: int = 50, warmup: int = 5,
|
|
93
|
+
backend: str | list[str] | None = None, device: str = "CPU",
|
|
94
|
+
threads: int | None = None, verbose: bool = True) -> list[dict]:
|
|
95
|
+
"""Time every model on every runtime; returns one dict a (model, runtime)."""
|
|
96
|
+
from .predictor import installed
|
|
97
|
+
|
|
98
|
+
files = _model_files(model)
|
|
99
|
+
img = _picture(source)
|
|
100
|
+
wanted = backend if isinstance(backend, (list, tuple)) else \
|
|
101
|
+
[backend] if backend else ["openvino", "onnxruntime"]
|
|
102
|
+
rows = []
|
|
103
|
+
for path in files:
|
|
104
|
+
for name in wanted:
|
|
105
|
+
if not installed(name) or (name == "onnxruntime" and path.suffix != ".onnx"):
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
predictor = _predictor(path, name, device, threads)
|
|
109
|
+
except Exception as exc: # noqa: BLE001 — one model failing must not stop the rest
|
|
110
|
+
rows.append({"model": _label(path, model), "backend": name,
|
|
111
|
+
"error": str(exc)[:120]})
|
|
112
|
+
continue
|
|
113
|
+
for _ in range(warmup):
|
|
114
|
+
predictor(img, conf=0.25)
|
|
115
|
+
speeds = [predictor(img, conf=0.25)[1] for _ in range(runs)]
|
|
116
|
+
row = {"model": _label(path, model), "backend": name, "imgsz": predictor.imgsz}
|
|
117
|
+
for key in ("preprocess", "inference", "postprocess"):
|
|
118
|
+
row[key] = statistics.median(s[key] for s in speeds)
|
|
119
|
+
row["total"] = row["preprocess"] + row["inference"] + row["postprocess"]
|
|
120
|
+
row["fps"] = 1000.0 / row["total"]
|
|
121
|
+
rows.append(row)
|
|
122
|
+
rows.sort(key=lambda r: r.get("total", float("inf")))
|
|
123
|
+
if verbose:
|
|
124
|
+
print(report(rows, threads))
|
|
125
|
+
return rows
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def report(rows: list[dict], threads: int | None = None) -> str:
|
|
129
|
+
"""The table ``benchmark`` prints, with the machine it ran on."""
|
|
130
|
+
versions = []
|
|
131
|
+
for module in ("openvino", "onnxruntime"):
|
|
132
|
+
try:
|
|
133
|
+
versions.append(f"{module} {__import__(module).__version__}")
|
|
134
|
+
except ImportError:
|
|
135
|
+
pass
|
|
136
|
+
head = (f"{platform.machine()} {platform.system()}, {os.cpu_count()} cores"
|
|
137
|
+
f"{f', {threads} threads' if threads else ''}; {', '.join(versions)}")
|
|
138
|
+
lines = [head, f"{'model':40s} {'runtime':12s} {'size':>5s} {'pre':>7s} {'infer':>8s} "
|
|
139
|
+
f"{'post':>6s} {'total ms':>9s} {'fps':>6s}"]
|
|
140
|
+
for r in rows:
|
|
141
|
+
if "error" in r:
|
|
142
|
+
lines.append(f"{r['model']:40s} {r['backend']:12s} failed: {r['error']}")
|
|
143
|
+
continue
|
|
144
|
+
lines.append(f"{r['model']:40s} {r['backend']:12s} {r['imgsz']:5d} {r['preprocess']:7.1f} "
|
|
145
|
+
f"{r['inference']:8.1f} {r['postprocess']:6.1f} {r['total']:9.1f} "
|
|
146
|
+
f"{r['fps']:6.1f}")
|
|
147
|
+
return "\n".join(lines)
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import sys
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
|
-
MODES = ("predict", "track", "train", "val", "export", "serve")
|
|
17
|
+
MODES = ("predict", "track", "train", "val", "export", "serve", "benchmark")
|
|
18
18
|
|
|
19
19
|
HELP = """easydetect — real-time object detection (D-FINE), Apache-2.0
|
|
20
20
|
|
|
@@ -27,6 +27,7 @@ Modes:
|
|
|
27
27
|
val COCO-style mAP50 / mAP50-95 on the val split
|
|
28
28
|
export write an OpenVINO IR (or ONNX) next to the checkpoint
|
|
29
29
|
serve answer POST /predict with detections as JSON (an HTTP server)
|
|
30
|
+
benchmark time models on this machine (OpenVINO and ONNX Runtime; no PyTorch needed)
|
|
30
31
|
|
|
31
32
|
Common keys:
|
|
32
33
|
model=dfine-n|s|m|l|x|best.pt|model.xml source=bus.jpg|dir|video.mp4|0|url
|
|
@@ -41,6 +42,7 @@ Examples:
|
|
|
41
42
|
easydetect val model=best.pt data=data.yaml
|
|
42
43
|
easydetect export model=best.pt format=openvino half=true
|
|
43
44
|
easydetect serve model=best.onnx host=0.0.0.0 port=8000
|
|
45
|
+
easydetect benchmark model=pi-kit/ runs=50 threads=4
|
|
44
46
|
"""
|
|
45
47
|
|
|
46
48
|
|
|
@@ -104,6 +106,21 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
104
106
|
print(HELP, file=sys.stderr)
|
|
105
107
|
return 2
|
|
106
108
|
|
|
109
|
+
if mode == "benchmark":
|
|
110
|
+
from .benchmark import benchmark
|
|
111
|
+
|
|
112
|
+
keys = ("model", "source", "runs", "warmup", "backend", "device", "threads")
|
|
113
|
+
unknown = set(overrides) - set(keys)
|
|
114
|
+
if unknown or "model" not in overrides:
|
|
115
|
+
print(f"benchmark takes model= (files or folders) and {', '.join(keys[1:])}",
|
|
116
|
+
file=sys.stderr)
|
|
117
|
+
return 2
|
|
118
|
+
benchmark(overrides["model"], source=overrides.get("source"),
|
|
119
|
+
runs=int(overrides.get("runs", 50)), warmup=int(overrides.get("warmup", 5)),
|
|
120
|
+
backend=overrides.get("backend"), device=str(overrides.get("device", "CPU")),
|
|
121
|
+
threads=overrides.get("threads"))
|
|
122
|
+
return 0
|
|
123
|
+
|
|
107
124
|
if mode == "serve":
|
|
108
125
|
from .serve import serve
|
|
109
126
|
|
|
@@ -80,10 +80,23 @@ def download(url: str, dest: Path, progress: bool = True) -> Path:
|
|
|
80
80
|
tmp = dest.with_suffix(dest.suffix + f".{os.getpid()}.part")
|
|
81
81
|
request = urllib.request.Request(url, headers={"User-Agent": "easydetect"})
|
|
82
82
|
digest = hashlib.sha256()
|
|
83
|
+
linked: dict[str, str] = {}
|
|
84
|
+
|
|
85
|
+
class _Remember(urllib.request.HTTPRedirectHandler):
|
|
86
|
+
"""Hugging Face states a large file's SHA-256 in ``X-Linked-Etag`` on
|
|
87
|
+
the redirect to its storage; the storage's own ``ETag`` is some other
|
|
88
|
+
hash. Keep the first, never trust the second."""
|
|
89
|
+
|
|
90
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
91
|
+
if headers.get("X-Linked-Etag"):
|
|
92
|
+
linked.setdefault("etag", headers["X-Linked-Etag"])
|
|
93
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
94
|
+
|
|
95
|
+
opener = urllib.request.build_opener(_Remember)
|
|
83
96
|
try:
|
|
84
|
-
with
|
|
97
|
+
with opener.open(request, timeout=_TIMEOUT) as response, open(tmp, "wb") as out:
|
|
85
98
|
total = int(response.headers.get("Content-Length") or 0)
|
|
86
|
-
tag =
|
|
99
|
+
tag = linked.get("etag") or response.headers.get("X-Linked-Etag") or ""
|
|
87
100
|
expected = re.fullmatch(r'(?:W/)?"?([0-9a-f]{64})"?', tag.strip())
|
|
88
101
|
done = 0
|
|
89
102
|
while chunk := response.read(1 << 16):
|
|
@@ -757,7 +757,12 @@ class DFINETransformer(nn.Module):
|
|
|
757
757
|
):
|
|
758
758
|
# prepare input for decoder
|
|
759
759
|
if self.training or self.eval_spatial_size is None:
|
|
760
|
-
|
|
760
|
+
# the same for every step at a given size: built once (as above)
|
|
761
|
+
key = (tuple(map(tuple, spatial_shapes)), memory.device)
|
|
762
|
+
cache = self.__dict__.setdefault("_anchor_cache", {})
|
|
763
|
+
if key not in cache:
|
|
764
|
+
cache[key] = self._generate_anchors(spatial_shapes, device=memory.device)
|
|
765
|
+
anchors, valid_mask = cache[key]
|
|
761
766
|
else:
|
|
762
767
|
anchors = self.anchors
|
|
763
768
|
valid_mask = self.valid_mask
|
|
@@ -86,7 +86,7 @@ def get_contrastive_denoising_training_group(
|
|
|
86
86
|
known_bbox += rand_sign * rand_part * diff
|
|
87
87
|
known_bbox = torch.clip(known_bbox, min=0.0, max=1.0)
|
|
88
88
|
input_query_bbox = box_xyxy_to_cxcywh(known_bbox)
|
|
89
|
-
input_query_bbox
|
|
89
|
+
input_query_bbox = input_query_bbox.abs()
|
|
90
90
|
input_query_bbox_unact = inverse_sigmoid(input_query_bbox)
|
|
91
91
|
|
|
92
92
|
input_query_logits = class_embed(input_query_class)
|
|
@@ -448,9 +448,16 @@ class HybridEncoder(nn.Module):
|
|
|
448
448
|
# flatten [B, C, H, W] to [B, HxW, C]
|
|
449
449
|
src_flatten = proj_feats[enc_ind].flatten(2).permute(0, 2, 1)
|
|
450
450
|
if self.training or self.eval_spatial_size is None:
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
)
|
|
451
|
+
# the same for every step at a given size: built once, kept
|
|
452
|
+
# on the device (it was rebuilt on the CPU and copied over
|
|
453
|
+
# every training step)
|
|
454
|
+
key = (w, h, src_flatten.device)
|
|
455
|
+
cache = self.__dict__.setdefault("_pos_cache", {})
|
|
456
|
+
if key not in cache:
|
|
457
|
+
cache[key] = self.build_2d_sincos_position_embedding(
|
|
458
|
+
w, h, self.hidden_dim, self.pe_temperature
|
|
459
|
+
).to(src_flatten.device)
|
|
460
|
+
pos_embed = cache[key]
|
|
454
461
|
else:
|
|
455
462
|
pos_embed = getattr(self, f"pos_embed{enc_ind}", None).to(src_flatten.device)
|
|
456
463
|
|
|
@@ -185,10 +185,9 @@ def generalized_box_iou(boxes1, boxes2):
|
|
|
185
185
|
Returns a [N, M] pairwise matrix, where N = len(boxes1)
|
|
186
186
|
and M = len(boxes2)
|
|
187
187
|
"""
|
|
188
|
-
# degenerate boxes
|
|
189
|
-
# so
|
|
190
|
-
|
|
191
|
-
assert (boxes2[:, 2:] >= boxes2[:, :2]).all()
|
|
188
|
+
# degenerate boxes give inf / nan; the boxes here come from cx cy w h
|
|
189
|
+
# with w, h >= 0, so they never are (the check D-FINE had made every call
|
|
190
|
+
# wait for the GPU)
|
|
192
191
|
iou, union = box_iou(boxes1, boxes2)
|
|
193
192
|
|
|
194
193
|
lt = torch.min(boxes1[:, None, :2], boxes2[:, :2])
|
|
@@ -322,22 +321,23 @@ def translate_gt(gt, reg_max, reg_scale, up):
|
|
|
322
321
|
# Calculate the weights for the interpolation
|
|
323
322
|
indices = closest_left_indices.float()
|
|
324
323
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
324
|
+
# computed for every value and kept where valid (torch.where), rather
|
|
325
|
+
# than gathered through a boolean mask: a mask's size is only known once
|
|
326
|
+
# the GPU has computed it, so selecting with one makes the step wait
|
|
328
327
|
valid_idx_mask = (indices >= 0) & (indices < reg_max)
|
|
329
|
-
|
|
328
|
+
safe = indices.clamp(0, reg_max - 1).long()
|
|
330
329
|
|
|
331
330
|
# Obtain distances
|
|
332
|
-
left_values = function_values[
|
|
333
|
-
right_values = function_values[
|
|
331
|
+
left_values = function_values[safe]
|
|
332
|
+
right_values = function_values[safe + 1]
|
|
334
333
|
|
|
335
|
-
left_diffs = torch.abs(gt
|
|
336
|
-
right_diffs = torch.abs(right_values - gt
|
|
334
|
+
left_diffs = torch.abs(gt - left_values)
|
|
335
|
+
right_diffs = torch.abs(right_values - gt)
|
|
337
336
|
|
|
338
337
|
# Valid weights
|
|
339
|
-
|
|
340
|
-
|
|
338
|
+
right = left_diffs / (left_diffs + right_diffs)
|
|
339
|
+
weight_right = torch.where(valid_idx_mask, right, torch.zeros_like(right))
|
|
340
|
+
weight_left = torch.where(valid_idx_mask, 1.0 - right, torch.zeros_like(right))
|
|
341
341
|
|
|
342
342
|
# Invalid weights (out of range)
|
|
343
343
|
invalid_idx_mask_neg = indices < 0
|
|
@@ -401,8 +401,10 @@ class OVPredictor(Predictor):
|
|
|
401
401
|
imgsz: int | None = None,
|
|
402
402
|
names: dict[int, str] | None = None,
|
|
403
403
|
precision: str | None = None,
|
|
404
|
+
threads: int | None = None,
|
|
404
405
|
) -> None:
|
|
405
|
-
"""``precision`` overrides what OpenVINO runs the graph in
|
|
406
|
+
"""``precision`` overrides what OpenVINO runs the graph in;
|
|
407
|
+
``threads`` caps the CPU threads it uses (default: all cores).
|
|
406
408
|
|
|
407
409
|
Left alone, OpenVINO picks: on a CPU that supports it that means
|
|
408
410
|
bfloat16, which is much faster but shifts scores slightly. Pass
|
|
@@ -414,6 +416,8 @@ class OVPredictor(Predictor):
|
|
|
414
416
|
core = ov_core()
|
|
415
417
|
model = core.read_model(str(self.model_path))
|
|
416
418
|
config = {"INFERENCE_PRECISION_HINT": precision} if precision else {}
|
|
419
|
+
if threads and "CPU" in device.upper():
|
|
420
|
+
config["INFERENCE_NUM_THREADS"] = int(threads)
|
|
417
421
|
self.compiled = core.compile_model(model, device, config)
|
|
418
422
|
self.input = self.compiled.input(0)
|
|
419
423
|
# CompiledModel.__call__ reuses one InferRequest, so a second thread
|
|
@@ -44,14 +44,19 @@ BASE_SIZE_REPEAT = 3 # how often imgsz itself is listed among the multi-scale s
|
|
|
44
44
|
EMA_RESTART_STEP = 0.0001 # the average's decay is lowered this much after the best is reloaded
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def _float32(out):
|
|
48
|
-
"""The network's outputs, every floating tensor in them as float32.
|
|
47
|
+
def _float32(out, _seen=None):
|
|
48
|
+
"""The network's outputs, every floating tensor in them as float32. A
|
|
49
|
+
tensor that appears twice is converted once and stays one object (the loss
|
|
50
|
+
tells a layer that is its own teacher by identity)."""
|
|
51
|
+
seen = {} if _seen is None else _seen
|
|
49
52
|
if isinstance(out, torch.Tensor):
|
|
50
|
-
|
|
53
|
+
if id(out) not in seen:
|
|
54
|
+
seen[id(out)] = (out, out.float() if out.is_floating_point() else out)
|
|
55
|
+
return seen[id(out)][1]
|
|
51
56
|
if isinstance(out, dict):
|
|
52
|
-
return {k: _float32(v) for k, v in out.items()}
|
|
57
|
+
return {k: _float32(v, seen) for k, v in out.items()}
|
|
53
58
|
if isinstance(out, (list, tuple)):
|
|
54
|
-
return type(out)(_float32(v) for v in out)
|
|
59
|
+
return type(out)(_float32(v, seen) for v in out)
|
|
55
60
|
return out
|
|
56
61
|
|
|
57
62
|
|
|
@@ -297,6 +302,13 @@ class Trainer:
|
|
|
297
302
|
self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
|
|
298
303
|
self.workers = workers
|
|
299
304
|
self.amp = amp and self.device.type == "cuda"
|
|
305
|
+
if self.device.type == "cuda":
|
|
306
|
+
# the input size is fixed (or one of a few, multi-scale): let cuDNN
|
|
307
|
+
# time its convolution algorithms once and keep the fastest; and
|
|
308
|
+
# let float32 matmuls (the loss, the parts autocast leaves) use TF32
|
|
309
|
+
torch.backends.cudnn.benchmark = True
|
|
310
|
+
torch.backends.cuda.matmul.allow_tf32 = True
|
|
311
|
+
torch.backends.cudnn.allow_tf32 = True
|
|
300
312
|
# bfloat16 where the GPU has it (RTX 30 and later): float32's range, so
|
|
301
313
|
# no loss scaling and no overflow; float16 with a GradScaler otherwise
|
|
302
314
|
self.amp_dtype = (torch.bfloat16 if self.amp and torch.cuda.is_bf16_supported()
|
|
@@ -318,8 +330,16 @@ class Trainer:
|
|
|
318
330
|
if lr_backbone_mult is None:
|
|
319
331
|
lr_backbone_mult = BACKBONE_LR_MULT.get(getattr(net, "variant", ""), 0.1)
|
|
320
332
|
self.lr_backbone_mult = lr_backbone_mult
|
|
333
|
+
if self.device.type == "cuda":
|
|
334
|
+
# convolutions in channels-last (NHWC): what the GPU's tensor cores
|
|
335
|
+
# want, so cuDNN no longer converts every activation back and forth
|
|
336
|
+
# (an RTX 5090, dfine-s at batch 16: 117 -> 100 ms a step)
|
|
337
|
+
net.to(self.device, memory_format=torch.channels_last)
|
|
338
|
+
self.channels_last = self.device.type == "cuda"
|
|
321
339
|
self.opt = torch.optim.AdamW(
|
|
322
|
-
_param_groups(net, lr, lr * lr_backbone_mult), lr=lr, weight_decay=weight_decay
|
|
340
|
+
_param_groups(net, lr, lr * lr_backbone_mult), lr=lr, weight_decay=weight_decay,
|
|
341
|
+
# one kernel for every parameter on a GPU, instead of a few per tensor
|
|
342
|
+
fused=self.device.type == "cuda",
|
|
323
343
|
)
|
|
324
344
|
self.criterion = DetectionLoss(net.num_classes, reg_max=net.decoder.reg_max)
|
|
325
345
|
self.ema = ModelEMA(net)
|
|
@@ -597,6 +617,8 @@ class Trainer:
|
|
|
597
617
|
if side != imgs.shape[-1]:
|
|
598
618
|
imgs = torch.nn.functional.interpolate(
|
|
599
619
|
imgs, size=(side, side), mode="bilinear", align_corners=False)
|
|
620
|
+
if self.channels_last:
|
|
621
|
+
imgs = imgs.contiguous(memory_format=torch.channels_last)
|
|
600
622
|
targets = [{k: v.to(self.device) for k, v in t.items()} for t in targets]
|
|
601
623
|
|
|
602
624
|
with torch.amp.autocast(self.device.type, dtype=self.amp_dtype,
|