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.
- {easydetect-0.1.2 → easydetect-0.2.1}/PKG-INFO +43 -20
- {easydetect-0.1.2 → easydetect-0.2.1}/README.md +40 -19
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/__init__.py +1 -1
- easydetect-0.2.1/easydetect/data/augment.py +207 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/data/dataset.py +7 -7
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/data/labels.py +2 -2
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/downloads.py +16 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/model.py +33 -13
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/predictor.py +169 -58
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/validator.py +8 -5
- {easydetect-0.1.2 → easydetect-0.2.1}/pyproject.toml +6 -2
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_augment.py +14 -0
- easydetect-0.2.1/tests/test_coco2yolo.py +79 -0
- easydetect-0.2.1/tests/test_lab_api.py +31 -0
- easydetect-0.2.1/tests/test_onnxruntime.py +106 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tools/build_mirror.py +7 -5
- easydetect-0.2.1/tools/coco2yolo.py +108 -0
- easydetect-0.2.1/tools/compare_yolo.py +151 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tools/hub_README.md +8 -6
- {easydetect-0.1.2 → easydetect-0.2.1}/tools/make_toyset.py +1 -1
- easydetect-0.1.2/easydetect/data/augment.py +0 -160
- easydetect-0.1.2/platform/README.md +0 -194
- easydetect-0.1.2/platform/tests/conftest.py +0 -9
- easydetect-0.1.2/platform/tests/test_app.py +0 -1222
- easydetect-0.1.2/platform/tests/test_labeling.py +0 -94
- {easydetect-0.1.2 → easydetect-0.2.1}/.gitignore +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/LICENSE +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/NOTICE +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/cli.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/data/__init__.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/errors.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/exporter.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/metrics.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/__init__.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/decoder.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/denoising.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/dfine_net.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/encoder.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/hgnetv2.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/nn/ops.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/plotting.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/results.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/sources.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/tracker.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/trainer.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/utils/__init__.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/utils/loss.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/easydetect/utils/ops.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/__init__.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/conftest.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_cli.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_dataset.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_downloads.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_model.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_nn.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_predictor.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_results.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_sources.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_tracker.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tests/test_train_export.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tools/bench.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.1}/tools/convert_dfine.py +0 -0
- {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
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
folder whose model card is written
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-

|
|
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
|
-
* [
|
|
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
|
|
226
|
-
OpenVINO
|
|
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이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
29
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
folder whose model card is written
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-

|
|
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
|
-
* [
|
|
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
|
|
186
|
-
OpenVINO
|
|
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이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
imgsz=imgsz,
|
|
275
|
-
|
|
276
|
-
|
|
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
|