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.
Files changed (62) hide show
  1. {easydetect-0.1.2 → easydetect-0.2.0}/PKG-INFO +31 -11
  2. {easydetect-0.1.2 → easydetect-0.2.0}/README.md +28 -10
  3. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/__init__.py +1 -1
  4. easydetect-0.2.0/easydetect/data/augment.py +207 -0
  5. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/data/dataset.py +4 -4
  6. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/downloads.py +16 -0
  7. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/model.py +33 -13
  8. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/predictor.py +169 -58
  9. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/validator.py +8 -5
  10. {easydetect-0.1.2 → easydetect-0.2.0}/platform/README.md +9 -4
  11. {easydetect-0.1.2 → easydetect-0.2.0}/pyproject.toml +5 -1
  12. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_augment.py +14 -0
  13. easydetect-0.2.0/tests/test_coco2yolo.py +79 -0
  14. easydetect-0.2.0/tests/test_onnxruntime.py +106 -0
  15. {easydetect-0.1.2 → easydetect-0.2.0}/tools/build_mirror.py +7 -5
  16. easydetect-0.2.0/tools/coco2yolo.py +108 -0
  17. easydetect-0.2.0/tools/compare_yolo.py +151 -0
  18. {easydetect-0.1.2 → easydetect-0.2.0}/tools/hub_README.md +7 -5
  19. easydetect-0.1.2/easydetect/data/augment.py +0 -160
  20. {easydetect-0.1.2 → easydetect-0.2.0}/.gitignore +0 -0
  21. {easydetect-0.1.2 → easydetect-0.2.0}/LICENSE +0 -0
  22. {easydetect-0.1.2 → easydetect-0.2.0}/NOTICE +0 -0
  23. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/cli.py +0 -0
  24. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/data/__init__.py +0 -0
  25. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/data/labels.py +0 -0
  26. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/errors.py +0 -0
  27. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/exporter.py +0 -0
  28. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/metrics.py +0 -0
  29. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/__init__.py +0 -0
  30. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/decoder.py +0 -0
  31. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/denoising.py +0 -0
  32. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/dfine_net.py +0 -0
  33. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/encoder.py +0 -0
  34. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/hgnetv2.py +0 -0
  35. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/nn/ops.py +0 -0
  36. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/plotting.py +0 -0
  37. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/results.py +0 -0
  38. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/sources.py +0 -0
  39. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/tracker.py +0 -0
  40. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/trainer.py +0 -0
  41. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/utils/__init__.py +0 -0
  42. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/utils/loss.py +0 -0
  43. {easydetect-0.1.2 → easydetect-0.2.0}/easydetect/utils/ops.py +0 -0
  44. {easydetect-0.1.2 → easydetect-0.2.0}/platform/tests/conftest.py +0 -0
  45. {easydetect-0.1.2 → easydetect-0.2.0}/platform/tests/test_app.py +0 -0
  46. {easydetect-0.1.2 → easydetect-0.2.0}/platform/tests/test_labeling.py +0 -0
  47. {easydetect-0.1.2 → easydetect-0.2.0}/tests/__init__.py +0 -0
  48. {easydetect-0.1.2 → easydetect-0.2.0}/tests/conftest.py +0 -0
  49. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_cli.py +0 -0
  50. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_dataset.py +0 -0
  51. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_downloads.py +0 -0
  52. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_model.py +0 -0
  53. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_nn.py +0 -0
  54. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_predictor.py +0 -0
  55. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_results.py +0 -0
  56. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_sources.py +0 -0
  57. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_tracker.py +0 -0
  58. {easydetect-0.1.2 → easydetect-0.2.0}/tests/test_train_export.py +0 -0
  59. {easydetect-0.1.2 → easydetect-0.2.0}/tools/bench.py +0 -0
  60. {easydetect-0.1.2 → easydetect-0.2.0}/tools/convert_dfine.py +0 -0
  61. {easydetect-0.1.2 → easydetect-0.2.0}/tools/make_demo.py +0 -0
  62. {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.1.2
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 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
@@ -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 targets
226
- OpenVINO, so a CUDA deployment means exporting to ONNX and taking it from there.
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이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다. CPU만으로도
241
- 돌고, Intel NPU·GPU에서도 같은 결과가 나옵니다. 라벨링부터 학습·추론까지
242
- 브라우저로 하려면 `python platform/run.py`. 가중치 캐시는 `~/.easydetect/`, 사내
243
- 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
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 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
@@ -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 targets
186
- OpenVINO, so a CUDA deployment means exporting to ONNX and taking it from there.
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이라 상용 제품이나 교육 현장에 그대로 쓸 수 있습니다. CPU만으로도
201
- 돌고, Intel NPU·GPU에서도 같은 결과가 나옵니다. 라벨링부터 학습·추론까지
202
- 브라우저로 하려면 `python platform/run.py`. 가중치 캐시는 `~/.easydetect/`, 사내
203
- 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
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.1.2"
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
- 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
 
@@ -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