easydetect 0.1.2__tar.gz → 0.2.1__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 (63) hide show
  1. {easydetect-0.1.2 → easydetect-0.2.1}/PKG-INFO +43 -20
  2. {easydetect-0.1.2 → easydetect-0.2.1}/README.md +40 -19
  3. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/__init__.py +1 -1
  4. easydetect-0.2.1/easydetect/data/augment.py +207 -0
  5. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/data/dataset.py +7 -7
  6. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/data/labels.py +2 -2
  7. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/downloads.py +16 -0
  8. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/model.py +33 -13
  9. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/predictor.py +169 -58
  10. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/validator.py +8 -5
  11. {easydetect-0.1.2 → easydetect-0.2.1}/pyproject.toml +6 -2
  12. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_augment.py +14 -0
  13. easydetect-0.2.1/tests/test_coco2yolo.py +79 -0
  14. easydetect-0.2.1/tests/test_lab_api.py +31 -0
  15. easydetect-0.2.1/tests/test_onnxruntime.py +106 -0
  16. {easydetect-0.1.2 → easydetect-0.2.1}/tools/build_mirror.py +7 -5
  17. easydetect-0.2.1/tools/coco2yolo.py +108 -0
  18. easydetect-0.2.1/tools/compare_yolo.py +151 -0
  19. {easydetect-0.1.2 → easydetect-0.2.1}/tools/hub_README.md +8 -6
  20. {easydetect-0.1.2 → easydetect-0.2.1}/tools/make_toyset.py +1 -1
  21. easydetect-0.1.2/easydetect/data/augment.py +0 -160
  22. easydetect-0.1.2/platform/README.md +0 -194
  23. easydetect-0.1.2/platform/tests/conftest.py +0 -9
  24. easydetect-0.1.2/platform/tests/test_app.py +0 -1222
  25. easydetect-0.1.2/platform/tests/test_labeling.py +0 -94
  26. {easydetect-0.1.2 → easydetect-0.2.1}/.gitignore +0 -0
  27. {easydetect-0.1.2 → easydetect-0.2.1}/LICENSE +0 -0
  28. {easydetect-0.1.2 → easydetect-0.2.1}/NOTICE +0 -0
  29. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/cli.py +0 -0
  30. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/data/__init__.py +0 -0
  31. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/errors.py +0 -0
  32. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/exporter.py +0 -0
  33. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/metrics.py +0 -0
  34. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/__init__.py +0 -0
  35. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/decoder.py +0 -0
  36. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/denoising.py +0 -0
  37. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/dfine_net.py +0 -0
  38. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/encoder.py +0 -0
  39. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/hgnetv2.py +0 -0
  40. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/ops.py +0 -0
  41. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/plotting.py +0 -0
  42. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/results.py +0 -0
  43. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/sources.py +0 -0
  44. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/tracker.py +0 -0
  45. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/trainer.py +0 -0
  46. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/utils/__init__.py +0 -0
  47. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/utils/loss.py +0 -0
  48. {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/utils/ops.py +0 -0
  49. {easydetect-0.1.2 → easydetect-0.2.1}/tests/__init__.py +0 -0
  50. {easydetect-0.1.2 → easydetect-0.2.1}/tests/conftest.py +0 -0
  51. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_cli.py +0 -0
  52. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_dataset.py +0 -0
  53. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_downloads.py +0 -0
  54. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_model.py +0 -0
  55. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_nn.py +0 -0
  56. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_predictor.py +0 -0
  57. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_results.py +0 -0
  58. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_sources.py +0 -0
  59. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_tracker.py +0 -0
  60. {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_train_export.py +0 -0
  61. {easydetect-0.1.2 → easydetect-0.2.1}/tools/bench.py +0 -0
  62. {easydetect-0.1.2 → easydetect-0.2.1}/tools/convert_dfine.py +0 -0
  63. {easydetect-0.1.2 → easydetect-0.2.1}/tools/make_demo.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: easydetect
3
- Version: 0.1.2
3
+ Version: 0.2.1
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
7
7
  Project-URL: Documentation, https://github.com/themakerrobot/easydetect/blob/main/docs/usage.md
8
8
  Project-URL: Issues, https://github.com/themakerrobot/easydetect/issues
9
+ Project-URL: Changelog, https://github.com/themakerrobot/easydetect/blob/main/CHANGELOG.md
9
10
  Author-email: leeyunjai <leeyunjai1982@gmail.com>
10
11
  License-Expression: Apache-2.0
11
12
  License-File: LICENSE
@@ -23,6 +24,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
24
  Classifier: Topic :: Scientific/Engineering :: Image Recognition
24
25
  Requires-Python: >=3.10
25
26
  Requires-Dist: numpy>=1.23
27
+ Requires-Dist: onnxruntime>=1.16
26
28
  Requires-Dist: opencv-python>=4.6
27
29
  Requires-Dist: openvino>=2024.0
28
30
  Requires-Dist: pyyaml>=5.4
@@ -65,8 +67,9 @@ train and test in a browser. Apache-2.0 from the code to the model you export.
65
67
  * **Accurate for its size.** The detector is
66
68
  [D-FINE](https://github.com/Peterande/D-FINE), a real-time DETR: D-FINE-S
67
69
  scores 48.5 COCO mAP with 10M parameters.
68
- * **Runs on the machine you have.** OpenVINO runs the model on a CPU, an Intel
69
- GPU or an Intel NPU; no CUDA needed to deploy. A GPU makes training quick.
70
+ * **Runs on the machine you have.** OpenVINO runs it on a CPU, an Intel GPU or
71
+ an Intel NPU; ONNX Runtime on any CPU, a Raspberry Pi included. No CUDA
72
+ needed to deploy. A GPU makes training quick.
70
73
  * **No NMS to tune.** D-FINE is trained to give each object one box. The odd
71
74
  second box on the same object — one vehicle as both truck and car — is
72
75
  dropped by a fixed overlap filter (IoU 0.7, any class), so there is no
@@ -75,8 +78,18 @@ train and test in a browser. Apache-2.0 from the code to the model you export.
75
78
  ## Install
76
79
 
77
80
  ```bash
78
- pip install easydetect # inference
79
- pip install "easydetect[train]" # + training
81
+ pip install easydetect # inference: OpenVINO and ONNX Runtime
82
+ pip install "easydetect[train]" # + training (PyTorch)
83
+ ```
84
+
85
+ Inference never needs PyTorch. Both runtimes come with the plain install and
86
+ return the same boxes; `Detector` uses OpenVINO unless you pass
87
+ `backend="onnxruntime"`. OpenVINO is the faster one on an Intel CPU (dfine-s at
88
+ 640 on a 4-core Xeon: 49 ms against 132 ms) and the only way to an Intel GPU or
89
+ NPU; ONNX Runtime is the smaller one, for a box where every megabyte counts:
90
+
91
+ ```bash
92
+ pip install --no-deps easydetect && pip install numpy pyyaml opencv-python onnxruntime
80
93
  ```
81
94
 
82
95
  ## Detect
@@ -132,18 +145,21 @@ faster on a small set, and `resume=True` picks a killed run back up.
132
145
 
133
146
  ## Label, train and watch it in a browser
134
147
 
135
- `platform/` is a web app on top of this package: drop images in, label them (the
136
- model drafts the boxes, you correct them), queue a training run, watch the curve,
137
- then run the result over a folder, a video or your webcam — on one machine, with
138
- nothing leaving it. A finished run hands you copy-ready code and a Hugging Face
139
- folder whose model card is written from the run.
148
+ [easydetect lab](https://github.com/themakerrobot/easydetect-lab) is a web app on top of this package, in its own
149
+ repository: drop images in, label them (the model drafts the boxes, you correct
150
+ them), queue a training run, watch the curve, then run the result over a folder,
151
+ a video or your webcam — on one machine, with nothing leaving it. A finished run
152
+ hands you copy-ready code and a Hugging Face folder whose model card is written
153
+ from the run.
140
154
 
141
155
  ```bash
142
- pip install -r platform/requirements.txt
143
- python platform/run.py # http://<this machine>:8080
156
+ git clone https://github.com/themakerrobot/easydetect-lab
157
+ cd easydetect-lab
158
+ pip install -r requirements.txt # easydetect[train] from PyPI, and the web server
159
+ python run.py # http://<this machine>:8080
144
160
  ```
145
161
 
146
- ![the platform](https://raw.githubusercontent.com/themakerrobot/easydetect/main/docs/assets/platform.jpg)
162
+ ![easydetect lab](https://raw.githubusercontent.com/themakerrobot/easydetect-lab/main/docs/lab.jpg)
147
163
 
148
164
  ## Models
149
165
 
@@ -213,7 +229,7 @@ easydetect export model=best.pt format=openvino half=true
213
229
 
214
230
  * [Using the model](https://github.com/themakerrobot/easydetect/blob/main/docs/usage.md) — sources, results, tracking, saving
215
231
  * [Training](https://github.com/themakerrobot/easydetect/blob/main/docs/training.md) — datasets, validation, export, CLI
216
- * [Platform](https://github.com/themakerrobot/easydetect/blob/main/platform/README.md) — the browser app: labelling, jobs, sharing
232
+ * [easydetect lab](https://github.com/themakerrobot/easydetect-lab) — the browser app: labelling, jobs, sharing
217
233
  * [Weights](https://github.com/themakerrobot/easydetect/blob/main/docs/weights.md) — the mirror, building it, offline use
218
234
  * [Performance](https://github.com/themakerrobot/easydetect/blob/main/docs/performance.md) — measuring speed and improving it
219
235
  * [Design](https://github.com/themakerrobot/easydetect/blob/main/docs/design.md) — architecture and provenance
@@ -222,8 +238,9 @@ easydetect export model=best.pt format=openvino half=true
222
238
  ## What it does not do
223
239
 
224
240
  Boxes only — no segmentation, pose or classification. One training process, one
225
- machine; multi-GPU and distributed training are out of scope. Inference targets
226
- OpenVINO, so a CUDA deployment means exporting to ONNX and taking it from there.
241
+ machine; multi-GPU and distributed training are out of scope. Inference runs on
242
+ OpenVINO or ONNX Runtime; for a CUDA deployment, take the exported ONNX to
243
+ TensorRT from there.
227
244
 
228
245
  ## 한국어
229
246
 
@@ -237,10 +254,16 @@ model.export(format="openvino") # 배포용 IR + labels.txt
237
254
  ```
238
255
 
239
256
  세 줄이면 물체 검출이 됩니다. 모델은 D-FINE(실시간 DETR)이고, 코드와 가중치가
240
- 모두 Apache-2.0이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다. CPU만으로도
241
- 돌고, Intel NPU·GPU에서도 같은 결과가 나옵니다. 라벨링부터 학습·추론까지
242
- 브라우저로 하려면 `python platform/run.py`. 가중치 캐시는 `~/.easydetect/`, 사내
243
- 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
257
+ 모두 Apache-2.0이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다.
258
+
259
+ 설치는 두 가지입니다. `pip install easydetect`는 추론용으로 OpenVINO와 ONNX
260
+ Runtime이 함께 들어가고 PyTorch는 없습니다. `pip install "easydetect[train]"`은
261
+ 학습까지 합니다. 기본 엔진은 OpenVINO(인텔 CPU·GPU·NPU)이고,
262
+ `Detector("dfine-s", backend="onnxruntime")`로 ONNX Runtime(모든 CPU, 라즈베리파이
263
+ 포함)을 쓸 수 있습니다. 두 엔진의 결과는 같습니다.
264
+
265
+ 라벨링부터 학습·추론까지 브라우저로 하려면 [easydetect lab](https://github.com/themakerrobot/easydetect-lab). 가중치 캐시는
266
+ `~/.easydetect/`, 사내 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
244
267
 
245
268
  ## Credits
246
269
 
@@ -25,8 +25,9 @@ train and test in a browser. Apache-2.0 from the code to the model you export.
25
25
  * **Accurate for its size.** The detector is
26
26
  [D-FINE](https://github.com/Peterande/D-FINE), a real-time DETR: D-FINE-S
27
27
  scores 48.5 COCO mAP with 10M parameters.
28
- * **Runs on the machine you have.** OpenVINO runs the model on a CPU, an Intel
29
- GPU or an Intel NPU; no CUDA needed to deploy. A GPU makes training quick.
28
+ * **Runs on the machine you have.** OpenVINO runs it on a CPU, an Intel GPU or
29
+ an Intel NPU; ONNX Runtime on any CPU, a Raspberry Pi included. No CUDA
30
+ needed to deploy. A GPU makes training quick.
30
31
  * **No NMS to tune.** D-FINE is trained to give each object one box. The odd
31
32
  second box on the same object — one vehicle as both truck and car — is
32
33
  dropped by a fixed overlap filter (IoU 0.7, any class), so there is no
@@ -35,8 +36,18 @@ train and test in a browser. Apache-2.0 from the code to the model you export.
35
36
  ## Install
36
37
 
37
38
  ```bash
38
- pip install easydetect # inference
39
- pip install "easydetect[train]" # + training
39
+ pip install easydetect # inference: OpenVINO and ONNX Runtime
40
+ pip install "easydetect[train]" # + training (PyTorch)
41
+ ```
42
+
43
+ Inference never needs PyTorch. Both runtimes come with the plain install and
44
+ return the same boxes; `Detector` uses OpenVINO unless you pass
45
+ `backend="onnxruntime"`. OpenVINO is the faster one on an Intel CPU (dfine-s at
46
+ 640 on a 4-core Xeon: 49 ms against 132 ms) and the only way to an Intel GPU or
47
+ NPU; ONNX Runtime is the smaller one, for a box where every megabyte counts:
48
+
49
+ ```bash
50
+ pip install --no-deps easydetect && pip install numpy pyyaml opencv-python onnxruntime
40
51
  ```
41
52
 
42
53
  ## Detect
@@ -92,18 +103,21 @@ faster on a small set, and `resume=True` picks a killed run back up.
92
103
 
93
104
  ## Label, train and watch it in a browser
94
105
 
95
- `platform/` is a web app on top of this package: drop images in, label them (the
96
- model drafts the boxes, you correct them), queue a training run, watch the curve,
97
- then run the result over a folder, a video or your webcam — on one machine, with
98
- nothing leaving it. A finished run hands you copy-ready code and a Hugging Face
99
- folder whose model card is written from the run.
106
+ [easydetect lab](https://github.com/themakerrobot/easydetect-lab) is a web app on top of this package, in its own
107
+ repository: drop images in, label them (the model drafts the boxes, you correct
108
+ them), queue a training run, watch the curve, then run the result over a folder,
109
+ a video or your webcam — on one machine, with nothing leaving it. A finished run
110
+ hands you copy-ready code and a Hugging Face folder whose model card is written
111
+ from the run.
100
112
 
101
113
  ```bash
102
- pip install -r platform/requirements.txt
103
- python platform/run.py # http://<this machine>:8080
114
+ git clone https://github.com/themakerrobot/easydetect-lab
115
+ cd easydetect-lab
116
+ pip install -r requirements.txt # easydetect[train] from PyPI, and the web server
117
+ python run.py # http://<this machine>:8080
104
118
  ```
105
119
 
106
- ![the platform](https://raw.githubusercontent.com/themakerrobot/easydetect/main/docs/assets/platform.jpg)
120
+ ![easydetect lab](https://raw.githubusercontent.com/themakerrobot/easydetect-lab/main/docs/lab.jpg)
107
121
 
108
122
  ## Models
109
123
 
@@ -173,7 +187,7 @@ easydetect export model=best.pt format=openvino half=true
173
187
 
174
188
  * [Using the model](https://github.com/themakerrobot/easydetect/blob/main/docs/usage.md) — sources, results, tracking, saving
175
189
  * [Training](https://github.com/themakerrobot/easydetect/blob/main/docs/training.md) — datasets, validation, export, CLI
176
- * [Platform](https://github.com/themakerrobot/easydetect/blob/main/platform/README.md) — the browser app: labelling, jobs, sharing
190
+ * [easydetect lab](https://github.com/themakerrobot/easydetect-lab) — the browser app: labelling, jobs, sharing
177
191
  * [Weights](https://github.com/themakerrobot/easydetect/blob/main/docs/weights.md) — the mirror, building it, offline use
178
192
  * [Performance](https://github.com/themakerrobot/easydetect/blob/main/docs/performance.md) — measuring speed and improving it
179
193
  * [Design](https://github.com/themakerrobot/easydetect/blob/main/docs/design.md) — architecture and provenance
@@ -182,8 +196,9 @@ easydetect export model=best.pt format=openvino half=true
182
196
  ## What it does not do
183
197
 
184
198
  Boxes only — no segmentation, pose or classification. One training process, one
185
- machine; multi-GPU and distributed training are out of scope. Inference targets
186
- OpenVINO, so a CUDA deployment means exporting to ONNX and taking it from there.
199
+ machine; multi-GPU and distributed training are out of scope. Inference runs on
200
+ OpenVINO or ONNX Runtime; for a CUDA deployment, take the exported ONNX to
201
+ TensorRT from there.
187
202
 
188
203
  ## 한국어
189
204
 
@@ -197,10 +212,16 @@ model.export(format="openvino") # 배포용 IR + labels.txt
197
212
  ```
198
213
 
199
214
  세 줄이면 물체 검출이 됩니다. 모델은 D-FINE(실시간 DETR)이고, 코드와 가중치가
200
- 모두 Apache-2.0이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다. CPU만으로도
201
- 돌고, Intel NPU·GPU에서도 같은 결과가 나옵니다. 라벨링부터 학습·추론까지
202
- 브라우저로 하려면 `python platform/run.py`. 가중치 캐시는 `~/.easydetect/`, 사내
203
- 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
215
+ 모두 Apache-2.0이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다.
216
+
217
+ 설치는 두 가지입니다. `pip install easydetect`는 추론용으로 OpenVINO와 ONNX
218
+ Runtime이 함께 들어가고 PyTorch는 없습니다. `pip install "easydetect[train]"`은
219
+ 학습까지 합니다. 기본 엔진은 OpenVINO(인텔 CPU·GPU·NPU)이고,
220
+ `Detector("dfine-s", backend="onnxruntime")`로 ONNX Runtime(모든 CPU, 라즈베리파이
221
+ 포함)을 쓸 수 있습니다. 두 엔진의 결과는 같습니다.
222
+
223
+ 라벨링부터 학습·추론까지 브라우저로 하려면 [easydetect lab](https://github.com/themakerrobot/easydetect-lab). 가중치 캐시는
224
+ `~/.easydetect/`, 사내 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
204
225
 
205
226
  ## Credits
206
227
 
@@ -24,7 +24,7 @@ needs numpy/opencv/openvino/pyyaml; training adds
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
- __version__ = "0.1.2"
27
+ __version__ = "0.2.1"
28
28
 
29
29
  from .errors import DownloadError, EasyDetectError, ModelNotFoundError
30
30
  from .metrics import BoxMetrics, DetMetrics
@@ -0,0 +1,207 @@
1
+ # Apache-2.0
2
+ """Training-time augmentation for boxes: the recipe D-FINE trains with.
3
+
4
+ Photometric jitter, a random zoom-out onto a larger canvas, an IoU-constrained
5
+ random crop, and a horizontal flip — the steps of D-FINE's configs with the
6
+ probabilities and ranges they use (torchvision's defaults), written here in
7
+ NumPy/OpenCV. One difference: the zoom-out canvas is the picture's mean colour
8
+ rather than black, so the border is not a cue that says "zoomed".
9
+ Zoom-out makes objects small and crop makes them large, so a model sees each
10
+ class at many sizes and positions instead of only the framing the photos had.
11
+
12
+ Boxes are pixel ``xyxy`` float arrays in the original picture's coordinates;
13
+ the geometry moves a rectangle over the picture and renders it once, at the end.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import random
19
+
20
+ import cv2
21
+ import numpy as np
22
+
23
+ ZOOM_OUT_P, ZOOM_OUT_MAX = 0.5, 4.0 # canvas up to 4× the image, half the time
24
+ CROP_P = 0.8
25
+ CROP_MIN_IOUS = (0.0, 0.1, 0.3, 0.5, 0.7, 0.9, None) # None: keep the whole image
26
+ CROP_SCALE, CROP_ASPECT, CROP_TRIES = (0.3, 1.0), (0.5, 2.0), 40
27
+ PHOTO_P = 0.5
28
+
29
+ DESCRIPTION = [
30
+ f"photometric jitter (brightness, contrast, saturation, hue; p={PHOTO_P} each)",
31
+ f"zoom-out onto a canvas up to {ZOOM_OUT_MAX:g}× (p={ZOOM_OUT_P})",
32
+ f"IoU-constrained random crop, {CROP_SCALE[0]:g}–{CROP_SCALE[1]:g} of each side (p={CROP_P})",
33
+ "horizontal flip (p=0.5)",
34
+ ]
35
+
36
+
37
+ _LEVELS = np.arange(256, dtype=np.float32)
38
+
39
+
40
+ def photometric(img: np.ndarray) -> np.ndarray:
41
+ """Brightness, contrast, saturation and hue, each on its own coin flip.
42
+
43
+ Each is a per-value mapping, so each is a 256-entry table: brightness
44
+ and contrast fold into one table for all three channels, saturation and
45
+ hue into tables for the S and H planes.
46
+ """
47
+ brighten, stretch = random.random() < PHOTO_P, random.random() < PHOTO_P
48
+ if brighten or stretch:
49
+ table = _LEVELS * (random.uniform(0.875, 1.125) if brighten else 1.0) # brightness
50
+ if stretch: # contrast ×0.5–1.5 about the mean grey
51
+ grey = float(np.clip(table[cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)], 0, 255).mean())
52
+ table = grey + (np.clip(table, 0, 255) - grey) * random.uniform(0.5, 1.5)
53
+ img = cv2.LUT(img, np.clip(table, 0, 255).astype(np.uint8))
54
+ saturate, shift = random.random() < PHOTO_P, random.random() < PHOTO_P
55
+ if saturate or shift:
56
+ h, s, v = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2HSV))
57
+ if saturate: # saturation ×0.5–1.5
58
+ s = cv2.LUT(s, np.clip(_LEVELS * random.uniform(0.5, 1.5), 0, 255).astype(np.uint8))
59
+ if shift: # hue ±0.05 of the circle (OpenCV: 0–180)
60
+ h = cv2.LUT(h, ((_LEVELS + random.uniform(-9, 9)) % 180).astype(np.uint8))
61
+ img = cv2.cvtColor(cv2.merge((h, s, v)), cv2.COLOR_HSV2BGR)
62
+ return img
63
+
64
+
65
+ def zoom_out(view: np.ndarray) -> np.ndarray:
66
+ """Widen the view onto a canvas up to 4x the picture, the picture somewhere inside.
67
+
68
+ Nothing is drawn here: ``view`` is the rectangle, in the picture's own
69
+ pixel coordinates, that will become the output. The canvas outside the
70
+ picture is filled when the view is finally rendered.
71
+ """
72
+ if random.random() >= ZOOM_OUT_P:
73
+ return view
74
+ w, h = view[2] - view[0], view[3] - view[1]
75
+ s = random.uniform(1.0, ZOOM_OUT_MAX)
76
+ cw, ch = int(w * s), int(h * s)
77
+ left, top = random.randint(0, int(cw - w)), random.randint(0, int(ch - h))
78
+ return np.array([view[0] - left, view[1] - top, view[0] - left + cw, view[1] - top + ch],
79
+ np.float32)
80
+
81
+
82
+ def _iou(region: np.ndarray, boxes: np.ndarray) -> np.ndarray:
83
+ x1 = np.maximum(region[0], boxes[:, 0])
84
+ y1 = np.maximum(region[1], boxes[:, 1])
85
+ x2 = np.minimum(region[2], boxes[:, 2])
86
+ y2 = np.minimum(region[3], boxes[:, 3])
87
+ inter = np.clip(x2 - x1, 0, None) * np.clip(y2 - y1, 0, None)
88
+ area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
89
+ region_area = (region[2] - region[0]) * (region[3] - region[1])
90
+ return inter / np.maximum(area + region_area - inter, 1e-9)
91
+
92
+
93
+ def iou_crop(view: np.ndarray, boxes: np.ndarray,
94
+ classes: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
95
+ """Narrow the view to a region that overlaps some box by at least a random IoU.
96
+
97
+ Boxes whose centre falls outside the region are dropped; the rest are
98
+ clipped to it. A picture with no boxes (a background frame) takes any
99
+ region. When no region qualifies, the view stays as it was.
100
+ """
101
+ if random.random() >= CROP_P:
102
+ return view, boxes, classes
103
+ w, h = view[2] - view[0], view[3] - view[1]
104
+ min_iou = random.choice(CROP_MIN_IOUS)
105
+ if min_iou is None:
106
+ return view, boxes, classes
107
+ for _ in range(CROP_TRIES):
108
+ cw = w * random.uniform(*CROP_SCALE)
109
+ ch = h * random.uniform(*CROP_SCALE)
110
+ if not CROP_ASPECT[0] <= cw / ch <= CROP_ASPECT[1]:
111
+ continue
112
+ cw, ch = max(int(cw), 1), max(int(ch), 1)
113
+ left = view[0] + random.randint(0, int(w - cw))
114
+ top = view[1] + random.randint(0, int(h - ch))
115
+ region = np.array([left, top, left + cw, top + ch], np.float32)
116
+ if len(boxes):
117
+ if _iou(region, boxes).max() < min_iou:
118
+ continue
119
+ cx, cy = (boxes[:, 0] + boxes[:, 2]) / 2, (boxes[:, 1] + boxes[:, 3]) / 2
120
+ inside = (cx > region[0]) & (cx < region[2]) & (cy > region[1]) & (cy < region[3])
121
+ if not inside.any():
122
+ continue
123
+ kept = boxes[inside].copy()
124
+ kept[:, [0, 2]] = np.clip(kept[:, [0, 2]], region[0], region[2])
125
+ kept[:, [1, 3]] = np.clip(kept[:, [1, 3]], region[1], region[3])
126
+ boxes, classes = kept, classes[inside]
127
+ return region, boxes, classes
128
+ return view, boxes, classes
129
+
130
+
131
+ def render(img: np.ndarray, view: np.ndarray, size: tuple[int, int],
132
+ flip: bool) -> np.ndarray:
133
+ """Draw ``view`` (picture coordinates, may reach past the picture) at ``size``.
134
+
135
+ One affine warp straight to the output, so a 4x zoom-out never builds a
136
+ canvas 4x the photo. Outside the picture is its mean colour. A big
137
+ shrink goes through an area resize first, so fine detail averages
138
+ instead of aliasing.
139
+ """
140
+ h, w = img.shape[:2]
141
+ out_w, out_h = size
142
+ sx, sy = out_w / (view[2] - view[0]), out_h / (view[3] - view[1])
143
+ inside = view[0] >= 0 and view[1] >= 0 and view[2] <= w and view[3] <= h
144
+ if inside and not flip and view[0] == 0 and view[1] == 0 and view[2] == w and view[3] == h:
145
+ return cv2.resize(img, (out_w, out_h))
146
+ src, fx, fy = img, 1.0, 1.0
147
+ if max(sx, sy) < 0.5: # shrinking hard: average first
148
+ nw, nh = max(1, round(w * sx * 2)), max(1, round(h * sy * 2))
149
+ src = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_AREA)
150
+ fx, fy = nw / w, nh / h
151
+ ax, ay = sx / fx, sy / fy
152
+ if flip:
153
+ m = np.array([[-ax, 0, view[2] * sx], [0, ay, -view[1] * sy]], np.float32)
154
+ else:
155
+ m = np.array([[ax, 0, -view[0] * sx], [0, ay, -view[1] * sy]], np.float32)
156
+ fill = cv2.mean(img)[:3]
157
+ return cv2.warpAffine(src, m, (out_w, out_h), flags=cv2.INTER_LINEAR,
158
+ borderMode=cv2.BORDER_CONSTANT, borderValue=fill)
159
+
160
+
161
+ def apply(img: np.ndarray, labels: np.ndarray, strong: bool = True,
162
+ size: int | None = None, min_side: float = 1.0) -> tuple[np.ndarray, np.ndarray]:
163
+ """Augment one picture and its ``cls cx cy w h`` (normalised) labels.
164
+
165
+ ``size`` renders the result square at that many pixels (what training
166
+ wants, and the cheap way); ``None`` keeps the view's own pixel size.
167
+ ``strong=False`` keeps only the flip — what the last epochs train on, as
168
+ in D-FINE, so the model settles on pictures framed like the ones it will
169
+ see. ``min_side`` drops boxes thinner than that many output pixels.
170
+ Colour jitter runs last, on the small output: it does not care where the
171
+ pixels came from, and there are far fewer of them there.
172
+ """
173
+ h, w = img.shape[:2]
174
+ classes = labels[:, 0].copy()
175
+ boxes = np.empty((len(labels), 4), np.float32)
176
+ boxes[:, 0] = (labels[:, 1] - labels[:, 3] / 2) * w
177
+ boxes[:, 1] = (labels[:, 2] - labels[:, 4] / 2) * h
178
+ boxes[:, 2] = (labels[:, 1] + labels[:, 3] / 2) * w
179
+ boxes[:, 3] = (labels[:, 2] + labels[:, 4] / 2) * h
180
+
181
+ view = np.array([0, 0, w, h], np.float32)
182
+ if strong:
183
+ view = zoom_out(view)
184
+ view, boxes, classes = iou_crop(view, boxes, classes)
185
+ flip = random.random() < 0.5
186
+
187
+ vw, vh = view[2] - view[0], view[3] - view[1]
188
+ out_size = (size, size) if size else (max(1, round(float(vw))), max(1, round(float(vh))))
189
+ img = render(img, view, out_size, flip)
190
+ if strong:
191
+ img = photometric(img)
192
+
193
+ # boxes into the output: relative to the view, mirrored if flipped
194
+ rel = boxes - np.array([view[0], view[1], view[0], view[1]], np.float32)
195
+ if flip:
196
+ rel[:, [0, 2]] = vw - rel[:, [2, 0]]
197
+ rel /= np.array([vw, vh, vw, vh], np.float32)
198
+ ow, oh = out_size
199
+ keep = ((rel[:, 2] - rel[:, 0]) * ow >= min_side) & ((rel[:, 3] - rel[:, 1]) * oh >= min_side)
200
+ rel, classes = rel[keep], classes[keep]
201
+ out = np.empty((len(rel), 5), np.float32)
202
+ out[:, 0] = classes
203
+ out[:, 1] = (rel[:, 0] + rel[:, 2]) / 2
204
+ out[:, 2] = (rel[:, 1] + rel[:, 3]) / 2
205
+ out[:, 3] = rel[:, 2] - rel[:, 0]
206
+ out[:, 4] = rel[:, 3] - rel[:, 1]
207
+ return img, out
@@ -72,10 +72,10 @@ def _locate(root: Path, yaml_dir: Path, spec: str) -> Path:
72
72
  )
73
73
 
74
74
 
75
- def _list_images(root: Path, spec, yaml_dir: Path | None = None):
75
+ def list_images(root: Path, spec, yaml_dir: Path | None = None):
76
76
  """Images for one split: a folder, a .txt list, or a list of either."""
77
77
  if isinstance(spec, (list, tuple)):
78
- files = [f for s in spec for f in _list_images(root, s, yaml_dir)]
78
+ files = [f for s in spec for f in list_images(root, s, yaml_dir)]
79
79
  return list(dict.fromkeys(files))
80
80
  p = _locate(root, yaml_dir or root, str(spec))
81
81
  if p.is_dir():
@@ -109,7 +109,7 @@ class DetDataset(Dataset):
109
109
  f"{data_yaml} has no '{split}:' entry. Add one — it may point at the "
110
110
  f"training images, but then mAP only measures memorisation."
111
111
  )
112
- self.files = _list_images(cfg["root"], cfg[split], cfg["yaml_dir"])
112
+ self.files = list_images(cfg["root"], cfg[split], cfg["yaml_dir"])
113
113
  if not self.files:
114
114
  raise FileNotFoundError(f"no images for split '{split}'")
115
115
 
@@ -134,10 +134,10 @@ class DetDataset(Dataset):
134
134
  raise FileNotFoundError(f)
135
135
  labels = self._load_labels(f) # cls, cx, cy, w, h (normalized)
136
136
 
137
- if self.augment:
138
- img, labels = aug.apply(img, labels, strong=self.strong)
139
-
140
- img = cv2.resize(img, (self.imgsz, self.imgsz)) # plain resize, as D-FINE trains
137
+ if self.augment: # rendered straight at imgsz x imgsz
138
+ img, labels = aug.apply(img, labels, strong=self.strong, size=self.imgsz)
139
+ else:
140
+ img = cv2.resize(img, (self.imgsz, self.imgsz)) # plain resize, as D-FINE trains
141
141
  img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
142
142
  tensor = torch.from_numpy(img.transpose(2, 0, 1)).contiguous()
143
143
 
@@ -1,10 +1,10 @@
1
1
  # Apache-2.0
2
2
  """Where a label lives, and how one line of it reads — shared by everything.
3
3
 
4
- The trainer reads labels, the platform writes them, the exporter packs them.
4
+ The trainer reads labels, easydetect lab writes them, the exporter packs them.
5
5
  If any two of those disagree on where ``a.jpg``'s boxes are, training runs on
6
6
  nothing and nobody is told. So there is one rule, here, and no torch import:
7
- the platform uses it at startup.
7
+ the lab uses it at startup.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -123,6 +123,22 @@ def download_ir(name: str) -> Path:
123
123
  return xml
124
124
 
125
125
 
126
+ def download_onnx(name: str) -> Path:
127
+ """Fetch ``<name>.onnx`` (+ ``labels.txt``) — what ONNX Runtime runs."""
128
+ name = normalize_name(name)
129
+ if name not in MODEL_NAMES:
130
+ raise ModelNotFoundError(_unknown_name_message(name))
131
+ try:
132
+ onnx = _asset(name, f"{name}.onnx")
133
+ except DownloadError as exc:
134
+ raise ModelNotFoundError(
135
+ f"'{name}'.onnx is not on the mirror ({exc}). Install openvino to use the IR, "
136
+ f"or export one: Detector('{name}').export(format='onnx')"
137
+ ) from exc
138
+ _asset(name, "labels.txt", required=False)
139
+ return onnx
140
+
141
+
126
142
  def download_checkpoint(name: str) -> Path:
127
143
  """Fetch ``<name>.pt`` — the torch weights used to fine-tune or validate."""
128
144
  name = normalize_name(name)
@@ -63,13 +63,20 @@ class Detector:
63
63
  verbose: bool = True,
64
64
  precision: str | None = None,
65
65
  pretrained: bool = True,
66
+ backend: str | None = None,
66
67
  ) -> None:
67
68
  """``pretrained=False`` starts training from an ImageNet backbone
68
69
  instead of the mirror's COCO weights — for domains COCO says nothing
69
- about, or for a clean baseline."""
70
+ about, or for a clean baseline.
71
+
72
+ ``backend`` picks the runtime: ``"openvino"`` (CPU, Intel GPU, NPU;
73
+ reads ``.xml`` and ``.onnx``) or ``"onnxruntime"`` (CPU; ``.onnx``
74
+ only, the lighter install). Left out, OpenVINO is used when it is
75
+ installed and ONNX Runtime otherwise."""
70
76
  self.model_name = str(model)
71
77
  self.device = device
72
78
  self.precision = precision
79
+ self.backend = backend
73
80
  self.pretrained = pretrained
74
81
  self.verbose = verbose
75
82
  self.task = "detect"
@@ -78,7 +85,7 @@ class Detector:
78
85
  self.ckpt: dict | None = None
79
86
  self.variant: str | None = None
80
87
  self.ckpt_path: Path | None = None
81
- self.predictor = None # OVPredictor (lazy)
88
+ self.predictor = None # OVPredictor or ORTPredictor (lazy)
82
89
  self.ir_path: Path | None = None
83
90
  self.tracker = None
84
91
 
@@ -251,30 +258,43 @@ class Detector:
251
258
  _close_windows()
252
259
 
253
260
  def _ensure_predictor(self, device: str | None = None, imgsz: int | None = None):
254
- """Compile (downloading or exporting first, if needed) the OpenVINO model."""
255
- from .predictor import OVPredictor
261
+ """Load the model on its runtime (downloading or exporting first, if needed)."""
262
+ from .predictor import ORTPredictor, OVPredictor, installed, pick_backend
256
263
 
257
264
  device = device or self.device
265
+ if (self.ir_path is not None and self.ir_path.suffix.lower() == ".xml"
266
+ and self.backend != "openvino" and not installed("openvino")
267
+ and self.ir_path.with_suffix(".onnx").exists()):
268
+ # every export writes the .onnx beside the .xml: without OpenVINO, run that
269
+ self.ir_path = self.ir_path.with_suffix(".onnx")
270
+ backend = pick_backend(self.backend, self.ir_path, device)
258
271
  if (
259
272
  self.predictor is not None
273
+ and self.predictor.backend == backend
260
274
  and self.predictor.device == device
261
275
  and self.predictor.precision == self.precision
262
276
  and (imgsz is None or self.predictor.imgsz == imgsz)
263
277
  ):
264
278
  return self.predictor
265
279
 
280
+ wanted = ".xml" if backend == "openvino" else ".onnx"
281
+ if self.ir_path is not None and self.ir_path.suffix.lower() not in (wanted, ".onnx"):
282
+ self.ir_path = None # an .xml met a runtime that cannot read it
266
283
  if self.ir_path is None:
267
284
  if self.net is not None:
268
- self.ir_path = self.export(out_dir=self._cache_export_dir(), verbose=self.verbose)
269
- else:
285
+ fmt = "openvino" if backend == "openvino" else "onnx"
286
+ self.ir_path = self.export(format=fmt, out_dir=self._cache_export_dir(),
287
+ verbose=self.verbose)
288
+ elif backend == "openvino":
270
289
  self.ir_path = downloads.download_ir(self.model_name)
271
- self.predictor = OVPredictor(
272
- self.ir_path,
273
- device=device,
274
- imgsz=imgsz,
275
- names=self.names or None,
276
- precision=self.precision,
277
- )
290
+ else:
291
+ self.ir_path = downloads.download_onnx(self.model_name)
292
+ if backend == "openvino":
293
+ self.predictor = OVPredictor(self.ir_path, device=device, imgsz=imgsz,
294
+ names=self.names or None, precision=self.precision)
295
+ else:
296
+ self.predictor = ORTPredictor(self.ir_path, device=device, imgsz=imgsz,
297
+ names=self.names or None, precision=self.precision)
278
298
  if not self.names:
279
299
  self.names = self.predictor.names
280
300
  return self.predictor