easydetect 0.1.2__tar.gz → 0.2.0__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.0}/PKG-INFO +31 -11
- {easydetect-0.1.2 → easydetect-0.2.0}/README.md +28 -10
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/__init__.py +1 -1
- easydetect-0.2.0/easydetect/data/augment.py +207 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/data/dataset.py +4 -4
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/downloads.py +16 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/model.py +33 -13
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/predictor.py +169 -58
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/validator.py +8 -5
- {easydetect-0.1.2 → easydetect-0.2.0}/platform/README.md +9 -4
- {easydetect-0.1.2 → easydetect-0.2.0}/pyproject.toml +5 -1
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_augment.py +14 -0
- easydetect-0.2.0/tests/test_coco2yolo.py +79 -0
- easydetect-0.2.0/tests/test_onnxruntime.py +106 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tools/build_mirror.py +7 -5
- easydetect-0.2.0/tools/coco2yolo.py +108 -0
- easydetect-0.2.0/tools/compare_yolo.py +151 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tools/hub_README.md +7 -5
- easydetect-0.1.2/easydetect/data/augment.py +0 -160
- {easydetect-0.1.2 → easydetect-0.2.0}/.gitignore +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/LICENSE +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/NOTICE +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/cli.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/data/__init__.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/data/labels.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/errors.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/exporter.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/metrics.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/__init__.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/decoder.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/denoising.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/dfine_net.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/encoder.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/hgnetv2.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/ops.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/plotting.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/results.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/sources.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/tracker.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/trainer.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/utils/__init__.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/utils/loss.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/utils/ops.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/platform/tests/conftest.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/platform/tests/test_app.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/platform/tests/test_labeling.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/__init__.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/conftest.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_cli.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_dataset.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_downloads.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_model.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_nn.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_predictor.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_results.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_sources.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_tracker.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_train_export.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tools/bench.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tools/convert_dfine.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tools/make_demo.py +0 -0
- {easydetect-0.1.2 → easydetect-0.2.0}/tools/make_toyset.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: easydetect
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -222,8 +235,9 @@ easydetect export model=best.pt format=openvino half=true
|
|
|
222
235
|
## What it does not do
|
|
223
236
|
|
|
224
237
|
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
|
|
238
|
+
machine; multi-GPU and distributed training are out of scope. Inference runs on
|
|
239
|
+
OpenVINO or ONNX Runtime; for a CUDA deployment, take the exported ONNX to
|
|
240
|
+
TensorRT from there.
|
|
227
241
|
|
|
228
242
|
## 한국어
|
|
229
243
|
|
|
@@ -237,10 +251,16 @@ model.export(format="openvino") # 배포용 IR + labels.txt
|
|
|
237
251
|
```
|
|
238
252
|
|
|
239
253
|
세 줄이면 물체 검출이 됩니다. 모델은 D-FINE(실시간 DETR)이고, 코드와 가중치가
|
|
240
|
-
모두 Apache-2.0이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
254
|
+
모두 Apache-2.0이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다.
|
|
255
|
+
|
|
256
|
+
설치는 두 가지입니다. `pip install easydetect`는 추론용으로 OpenVINO와 ONNX
|
|
257
|
+
Runtime이 함께 들어가고 PyTorch는 없습니다. `pip install "easydetect[train]"`은
|
|
258
|
+
학습까지 합니다. 기본 엔진은 OpenVINO(인텔 CPU·GPU·NPU)이고,
|
|
259
|
+
`Detector("dfine-s", backend="onnxruntime")`로 ONNX Runtime(모든 CPU, 라즈베리파이
|
|
260
|
+
포함)을 쓸 수 있습니다. 두 엔진의 결과는 같습니다.
|
|
261
|
+
|
|
262
|
+
라벨링부터 학습·추론까지 브라우저로 하려면 `python platform/run.py`. 가중치 캐시는
|
|
263
|
+
`~/.easydetect/`, 사내 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
|
|
244
264
|
|
|
245
265
|
## Credits
|
|
246
266
|
|
|
@@ -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
|
|
@@ -182,8 +193,9 @@ easydetect export model=best.pt format=openvino half=true
|
|
|
182
193
|
## What it does not do
|
|
183
194
|
|
|
184
195
|
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
|
|
196
|
+
machine; multi-GPU and distributed training are out of scope. Inference runs on
|
|
197
|
+
OpenVINO or ONNX Runtime; for a CUDA deployment, take the exported ONNX to
|
|
198
|
+
TensorRT from there.
|
|
187
199
|
|
|
188
200
|
## 한국어
|
|
189
201
|
|
|
@@ -197,10 +209,16 @@ model.export(format="openvino") # 배포용 IR + labels.txt
|
|
|
197
209
|
```
|
|
198
210
|
|
|
199
211
|
세 줄이면 물체 검출이 됩니다. 모델은 D-FINE(실시간 DETR)이고, 코드와 가중치가
|
|
200
|
-
모두 Apache-2.0이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
212
|
+
모두 Apache-2.0이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다.
|
|
213
|
+
|
|
214
|
+
설치는 두 가지입니다. `pip install easydetect`는 추론용으로 OpenVINO와 ONNX
|
|
215
|
+
Runtime이 함께 들어가고 PyTorch는 없습니다. `pip install "easydetect[train]"`은
|
|
216
|
+
학습까지 합니다. 기본 엔진은 OpenVINO(인텔 CPU·GPU·NPU)이고,
|
|
217
|
+
`Detector("dfine-s", backend="onnxruntime")`로 ONNX Runtime(모든 CPU, 라즈베리파이
|
|
218
|
+
포함)을 쓸 수 있습니다. 두 엔진의 결과는 같습니다.
|
|
219
|
+
|
|
220
|
+
라벨링부터 학습·추론까지 브라우저로 하려면 `python platform/run.py`. 가중치 캐시는
|
|
221
|
+
`~/.easydetect/`, 사내 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
|
|
204
222
|
|
|
205
223
|
## Credits
|
|
206
224
|
|
|
@@ -24,7 +24,7 @@ needs numpy/opencv/openvino/pyyaml; training adds
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
-
__version__ = "0.
|
|
27
|
+
__version__ = "0.2.0"
|
|
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
|
|
@@ -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
|
|
|
@@ -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
|