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.
Files changed (87) hide show
  1. {easydetect-0.7.0 → easydetect-0.7.2}/PKG-INFO +27 -5
  2. {easydetect-0.7.0 → easydetect-0.7.2}/README.md +26 -4
  3. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/__init__.py +1 -1
  4. easydetect-0.7.2/easydetect/benchmark.py +147 -0
  5. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/cli.py +18 -1
  6. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/downloads.py +15 -2
  7. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/decoder.py +6 -1
  8. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/denoising.py +1 -1
  9. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/encoder.py +10 -3
  10. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/ops.py +14 -14
  11. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/predictor.py +5 -1
  12. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/trainer.py +28 -6
  13. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/utils/loss.py +85 -42
  14. {easydetect-0.7.0 → easydetect-0.7.2}/pyproject.toml +1 -1
  15. easydetect-0.7.2/tests/test_benchmark.py +20 -0
  16. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_downloads.py +64 -0
  17. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_robustness.py +28 -0
  18. {easydetect-0.7.0 → easydetect-0.7.2}/tools/bench_finetune.py +115 -42
  19. {easydetect-0.7.0 → easydetect-0.7.2}/tools/compare_yolo.py +35 -6
  20. easydetect-0.7.2/tools/pi_kit.py +123 -0
  21. easydetect-0.7.2/tools/profile_train.py +168 -0
  22. {easydetect-0.7.0 → easydetect-0.7.2}/.gitignore +0 -0
  23. {easydetect-0.7.0 → easydetect-0.7.2}/LICENSE +0 -0
  24. {easydetect-0.7.0 → easydetect-0.7.2}/NOTICE +0 -0
  25. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/__init__.py +0 -0
  26. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/augment.py +0 -0
  27. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/dataset.py +0 -0
  28. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/keypoints.py +0 -0
  29. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/data/labels.py +0 -0
  30. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/errors.py +0 -0
  31. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/exporter.py +0 -0
  32. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/metrics.py +0 -0
  33. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/model.py +0 -0
  34. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/__init__.py +0 -0
  35. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/dfine_net.py +0 -0
  36. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/hgnetv2.py +0 -0
  37. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/posenet.py +0 -0
  38. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/nn/sam_decoder.py +0 -0
  39. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/plotting.py +0 -0
  40. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/pose.py +0 -0
  41. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/pose_trainer.py +0 -0
  42. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/results.py +0 -0
  43. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/seg_trainer.py +0 -0
  44. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/segment.py +0 -0
  45. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/serve.py +0 -0
  46. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/sources.py +0 -0
  47. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/tracker.py +0 -0
  48. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/utils/__init__.py +0 -0
  49. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/utils/checkpoint.py +0 -0
  50. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/utils/ops.py +0 -0
  51. {easydetect-0.7.0 → easydetect-0.7.2}/easydetect/validator.py +0 -0
  52. {easydetect-0.7.0 → easydetect-0.7.2}/tests/__init__.py +0 -0
  53. {easydetect-0.7.0 → easydetect-0.7.2}/tests/conftest.py +0 -0
  54. {easydetect-0.7.0 → easydetect-0.7.2}/tests/pose_toy.py +0 -0
  55. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_augment.py +0 -0
  56. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_cli.py +0 -0
  57. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_coco2yolo.py +0 -0
  58. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_custom_pose.py +0 -0
  59. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_custom_seg.py +0 -0
  60. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_dataset.py +0 -0
  61. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_lab_api.py +0 -0
  62. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_model.py +0 -0
  63. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_nn.py +0 -0
  64. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_onnxruntime.py +0 -0
  65. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_plotting.py +0 -0
  66. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_pose.py +0 -0
  67. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_predictor.py +0 -0
  68. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_results.py +0 -0
  69. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_results_summary.py +0 -0
  70. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_segment.py +0 -0
  71. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_segment_lowres.py +0 -0
  72. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_serve.py +0 -0
  73. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_sources.py +0 -0
  74. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_sources_threads.py +0 -0
  75. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_tracker.py +0 -0
  76. {easydetect-0.7.0 → easydetect-0.7.2}/tests/test_train_export.py +0 -0
  77. {easydetect-0.7.0 → easydetect-0.7.2}/tools/bench.py +0 -0
  78. {easydetect-0.7.0 → easydetect-0.7.2}/tools/build_mirror.py +0 -0
  79. {easydetect-0.7.0 → easydetect-0.7.2}/tools/coco2yolo.py +0 -0
  80. {easydetect-0.7.0 → easydetect-0.7.2}/tools/convert_dfine.py +0 -0
  81. {easydetect-0.7.0 → easydetect-0.7.2}/tools/convert_sam.py +0 -0
  82. {easydetect-0.7.0 → easydetect-0.7.2}/tools/eval_exports.py +0 -0
  83. {easydetect-0.7.0 → easydetect-0.7.2}/tools/hub_README.md +0 -0
  84. {easydetect-0.7.0 → easydetect-0.7.2}/tools/make_demo.py +0 -0
  85. {easydetect-0.7.0 → easydetect-0.7.2}/tools/make_toyset.py +0 -0
  86. {easydetect-0.7.0 → easydetect-0.7.2}/tools/train_pose.py +0 -0
  87. {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.0
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: `tools/bench_finetune.py` fine-tunes
234
- dfine-s and YOLO26s on the same public datasets (from 92 to 1,132 pictures)
235
- with each trainer's own defaults and scores both with one evaluator on the
236
- CPU. Its results will be published here.
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: `tools/bench_finetune.py` fine-tunes
191
- dfine-s and YOLO26s on the same public datasets (from 92 to 1,132 pictures)
192
- with each trainer's own defaults and scores both with one evaluator on the
193
- CPU. Its results will be published here.
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.0"
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 urllib.request.urlopen(request, timeout=_TIMEOUT) as response, open(tmp, "wb") as out:
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 = (response.headers.get("X-Linked-Etag") or response.headers.get("ETag") or "")
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
- anchors, valid_mask = self._generate_anchors(spatial_shapes, device=memory.device)
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[input_query_bbox < 0] *= -1
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
- pos_embed = self.build_2d_sincos_position_embedding(
452
- w, h, self.hidden_dim, self.pe_temperature
453
- ).to(src_flatten.device)
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 gives inf / nan results
189
- # so do an early check
190
- assert (boxes1[:, 2:] >= boxes1[:, :2]).all()
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
- weight_right = torch.zeros_like(indices)
326
- weight_left = torch.zeros_like(indices)
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
- valid_indices = indices[valid_idx_mask].long()
328
+ safe = indices.clamp(0, reg_max - 1).long()
330
329
 
331
330
  # Obtain distances
332
- left_values = function_values[valid_indices]
333
- right_values = function_values[valid_indices + 1]
331
+ left_values = function_values[safe]
332
+ right_values = function_values[safe + 1]
334
333
 
335
- left_diffs = torch.abs(gt[valid_idx_mask] - left_values)
336
- right_diffs = torch.abs(right_values - gt[valid_idx_mask])
334
+ left_diffs = torch.abs(gt - left_values)
335
+ right_diffs = torch.abs(right_values - gt)
337
336
 
338
337
  # Valid weights
339
- weight_right[valid_idx_mask] = left_diffs / (left_diffs + right_diffs)
340
- weight_left[valid_idx_mask] = 1.0 - weight_right[valid_idx_mask]
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
- return out.float() if out.is_floating_point() else out
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,