easydetect 0.2.3__tar.gz → 0.3.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.2.3 → easydetect-0.3.1}/PKG-INFO +2 -1
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/__init__.py +1 -1
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/cli.py +1 -1
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/exporter.py +44 -7
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/model.py +58 -11
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/dfine_net.py +24 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/predictor.py +8 -2
- {easydetect-0.2.3 → easydetect-0.3.1}/pyproject.toml +2 -1
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_predictor.py +12 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_train_export.py +47 -0
- easydetect-0.3.1/tools/eval_exports.py +123 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tools/hub_README.md +1 -1
- {easydetect-0.2.3 → easydetect-0.3.1}/.gitignore +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/LICENSE +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/NOTICE +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/README.md +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/data/__init__.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/data/augment.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/data/dataset.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/data/labels.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/downloads.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/errors.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/metrics.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/__init__.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/decoder.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/denoising.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/encoder.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/hgnetv2.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/ops.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/plotting.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/results.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/sources.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/tracker.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/trainer.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/utils/__init__.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/utils/loss.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/utils/ops.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/validator.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/__init__.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/conftest.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_augment.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_cli.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_coco2yolo.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_dataset.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_downloads.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_lab_api.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_model.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_nn.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_onnxruntime.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_results.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_sources.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_tracker.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tools/bench.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tools/build_mirror.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tools/coco2yolo.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tools/compare_yolo.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tools/convert_dfine.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tools/make_demo.py +0 -0
- {easydetect-0.2.3 → easydetect-0.3.1}/tools/make_toyset.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: easydetect
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -34,6 +34,7 @@ Requires-Dist: pillow>=9.0; extra == 'dev'
|
|
|
34
34
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
35
35
|
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
36
36
|
Provides-Extra: train
|
|
37
|
+
Requires-Dist: nncf>=2.10; extra == 'train'
|
|
37
38
|
Requires-Dist: onnx>=1.14; extra == 'train'
|
|
38
39
|
Requires-Dist: scipy>=1.9; extra == 'train'
|
|
39
40
|
Requires-Dist: torch>=2.1; extra == 'train'
|
|
@@ -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.3.1"
|
|
28
28
|
|
|
29
29
|
from .errors import DownloadError, EasyDetectError, ModelNotFoundError
|
|
30
30
|
from .metrics import BoxMetrics, DetMetrics
|
|
@@ -28,7 +28,7 @@ Modes:
|
|
|
28
28
|
|
|
29
29
|
Common keys:
|
|
30
30
|
model=dfine-n|s|m|l|x|best.pt|model.xml source=bus.jpg|dir|video.mp4|0|url
|
|
31
|
-
data=data.yaml epochs=100 imgsz=640 batch=8 conf=0.
|
|
31
|
+
data=data.yaml epochs=100 imgsz=640 batch=8 conf=0.5 iou=0.7|none device=0|cpu|AUTO
|
|
32
32
|
contain=0.8 project=runs name=predict save=true show=false format=openvino half=true
|
|
33
33
|
|
|
34
34
|
Examples:
|
|
@@ -31,8 +31,12 @@ def _write_labels(out_dir: Path, fname: str, names) -> dict[int, str]:
|
|
|
31
31
|
return table
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def export_onnx(net, names, imgsz=640, out_dir=".", fname="easydetect", half=False, verbose=True
|
|
35
|
-
|
|
34
|
+
def export_onnx(net, names, imgsz=640, out_dir=".", fname="easydetect", half=False, verbose=True,
|
|
35
|
+
layers=None, queries=None):
|
|
36
|
+
"""Write ``<out_dir>/<fname>.onnx`` (plus labels). Returns the .onnx path.
|
|
37
|
+
|
|
38
|
+
``layers`` / ``queries`` export a lighter decoder: see ``DFINENet.trim``.
|
|
39
|
+
"""
|
|
36
40
|
import torch
|
|
37
41
|
|
|
38
42
|
from .nn import DeployWrapper
|
|
@@ -44,6 +48,8 @@ def export_onnx(net, names, imgsz=640, out_dir=".", fname="easydetect", half=Fal
|
|
|
44
48
|
# the deploy form fuses the re-parameterised blocks and drops the training
|
|
45
49
|
# heads; a copy, so the caller's network can keep training
|
|
46
50
|
deployed = copy.deepcopy(net).cpu()
|
|
51
|
+
if layers is not None or queries is not None:
|
|
52
|
+
deployed.trim(layers=layers, queries=queries)
|
|
47
53
|
deployed = deployed.deploy() if hasattr(deployed, "deploy") else deployed.eval()
|
|
48
54
|
wrapper = DeployWrapper(deployed).eval()
|
|
49
55
|
dummy = torch.zeros(1, 3, imgsz, imgsz)
|
|
@@ -87,18 +93,49 @@ def _embed_names(onnx_path: Path, table: dict[int, str]) -> None:
|
|
|
87
93
|
|
|
88
94
|
|
|
89
95
|
def export_openvino(net, names, imgsz=640, out_dir=".", fname="easydetect", half=False,
|
|
90
|
-
verbose=True):
|
|
91
|
-
"""Write ``<out_dir>/<fname>.xml`` (+ .bin, + labels). Returns the .xml path.
|
|
96
|
+
verbose=True, layers=None, queries=None, int8=None):
|
|
97
|
+
"""Write ``<out_dir>/<fname>.xml`` (+ .bin, + labels). Returns the .xml path.
|
|
98
|
+
|
|
99
|
+
``int8`` is a list of image paths to calibrate an 8-bit model on (NNCF's
|
|
100
|
+
post-training quantization; a few hundred pictures like the ones it will
|
|
101
|
+
see). It runs about as fast as bfloat16 where the CPU has that, and over
|
|
102
|
+
twice as fast as float32 where it does not — most laptop CPUs.
|
|
103
|
+
"""
|
|
92
104
|
import openvino as ov
|
|
93
105
|
|
|
94
106
|
out_dir = Path(out_dir)
|
|
95
107
|
onnx_path = export_onnx(
|
|
96
|
-
net, names, imgsz=imgsz, out_dir=out_dir, fname=fname, verbose=False
|
|
108
|
+
net, names, imgsz=imgsz, out_dir=out_dir, fname=fname, verbose=False,
|
|
109
|
+
layers=layers, queries=queries,
|
|
97
110
|
)
|
|
98
111
|
xml_path = out_dir / f"{fname}.xml"
|
|
99
112
|
|
|
100
113
|
model = ov.convert_model(str(onnx_path))
|
|
101
|
-
|
|
114
|
+
if int8:
|
|
115
|
+
model = _quantize(model, int8, imgsz)
|
|
116
|
+
ov.save_model(model, str(xml_path), compress_to_fp16=half and not int8)
|
|
102
117
|
if verbose:
|
|
103
|
-
|
|
118
|
+
kind = "INT8" if int8 else "FP16" if half else "FP32"
|
|
119
|
+
print(f"[easydetect] exported: {xml_path} ({kind})")
|
|
104
120
|
return xml_path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _quantize(model, images, imgsz):
|
|
124
|
+
"""8-bit weights and activations, calibrated on ``images`` (paths)."""
|
|
125
|
+
import cv2
|
|
126
|
+
import nncf
|
|
127
|
+
|
|
128
|
+
from .predictor import preprocess_image
|
|
129
|
+
|
|
130
|
+
images = [str(p) for p in images]
|
|
131
|
+
if not images:
|
|
132
|
+
raise ValueError("int8 needs pictures to calibrate on: give data=")
|
|
133
|
+
|
|
134
|
+
def transform(path):
|
|
135
|
+
img = cv2.imread(path)
|
|
136
|
+
if img is None:
|
|
137
|
+
raise ValueError(f"cannot read {path}")
|
|
138
|
+
return preprocess_image(img, imgsz)
|
|
139
|
+
|
|
140
|
+
return nncf.quantize(model, nncf.Dataset(images, transform), subset_size=len(images),
|
|
141
|
+
model_type=nncf.ModelType.TRANSFORMER)
|
|
@@ -114,7 +114,7 @@ class Detector:
|
|
|
114
114
|
def predict(
|
|
115
115
|
self,
|
|
116
116
|
source: Any = None,
|
|
117
|
-
conf: float = 0.
|
|
117
|
+
conf: float = 0.5,
|
|
118
118
|
imgsz: int | None = None,
|
|
119
119
|
device: str | None = None,
|
|
120
120
|
max_det: int = 300,
|
|
@@ -134,12 +134,14 @@ class Detector:
|
|
|
134
134
|
"""Run detection on any supported source.
|
|
135
135
|
|
|
136
136
|
Returns a ``list[Results]``, or a generator when ``stream=True`` (the
|
|
137
|
-
only sane option for a long video or a live camera). ``
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
only sane option for a long video or a live camera). ``conf=0.5``
|
|
138
|
+
is where D-FINE's boxes are mostly right (70% on COCO, against 32% at
|
|
139
|
+
YOLO's usual 0.25). ``iou`` drops a box covering a higher-scoring one
|
|
140
|
+
by more than that IoU — the same object found twice, sometimes under
|
|
141
|
+
two classes; ``None`` keeps all. ``contain=0.8`` also merges a box
|
|
142
|
+
with 80% of its area inside another of its class — the visible pieces
|
|
143
|
+
of a half hidden object beside the whole of it — into one box; off by
|
|
144
|
+
default, as on COCO it removes more real objects than it fixes.
|
|
143
145
|
"""
|
|
144
146
|
if source is None:
|
|
145
147
|
raise ValueError("predict() needs a source (image, folder, video, url, camera index)")
|
|
@@ -171,7 +173,7 @@ class Detector:
|
|
|
171
173
|
def track(
|
|
172
174
|
self,
|
|
173
175
|
source: Any = None,
|
|
174
|
-
conf: float = 0.
|
|
176
|
+
conf: float = 0.5,
|
|
175
177
|
iou: float | None = 0.7,
|
|
176
178
|
contain: float | None = None,
|
|
177
179
|
match_iou: float = 0.3,
|
|
@@ -209,7 +211,7 @@ class Detector:
|
|
|
209
211
|
def _run(
|
|
210
212
|
self,
|
|
211
213
|
source: Any,
|
|
212
|
-
conf: float = 0.
|
|
214
|
+
conf: float = 0.5,
|
|
213
215
|
imgsz: int | None = None,
|
|
214
216
|
device: str | None = None,
|
|
215
217
|
max_det: int = 300,
|
|
@@ -500,8 +502,22 @@ class Detector:
|
|
|
500
502
|
half: bool = False,
|
|
501
503
|
out_dir: str | Path | None = None,
|
|
502
504
|
verbose: bool | None = None,
|
|
505
|
+
layers: int | None = None,
|
|
506
|
+
queries: int | None = None,
|
|
507
|
+
int8: bool = False,
|
|
508
|
+
data: Any = None,
|
|
509
|
+
calib: int = 300,
|
|
503
510
|
) -> Path:
|
|
504
|
-
"""Export to OpenVINO IR (default) or ONNX. Returns the file written.
|
|
511
|
+
"""Export to OpenVINO IR (default) or ONNX. Returns the file written.
|
|
512
|
+
|
|
513
|
+
``int8=True`` (OpenVINO only) quantizes the IR to 8 bits, calibrated
|
|
514
|
+
on ``calib`` pictures from ``data`` — a data.yaml (its train split), a
|
|
515
|
+
folder or a list of files — and writes ``<name>_int8.xml``.
|
|
516
|
+
|
|
517
|
+
``layers`` (decoder layers to run, of 3-6 by size) and ``queries`` (of
|
|
518
|
+
300) export a faster, slightly less accurate model; dfine-n at 320 on
|
|
519
|
+
a 4-core CPU: 19.9 ms, 13.2 ms with ``queries=100``.
|
|
520
|
+
"""
|
|
505
521
|
format = format.lower()
|
|
506
522
|
if format not in ("openvino", "onnx"):
|
|
507
523
|
raise ValueError("supported formats: 'openvino', 'onnx'")
|
|
@@ -513,10 +529,18 @@ class Detector:
|
|
|
513
529
|
imgsz = imgsz or (self.ckpt or {}).get("imgsz", 640)
|
|
514
530
|
stem = self.ckpt_path.stem if self.ckpt_path else f"easydetect-{self.variant}"
|
|
515
531
|
verbose = self.verbose if verbose is None else verbose
|
|
532
|
+
if int8:
|
|
533
|
+
if format != "openvino":
|
|
534
|
+
raise ValueError("int8 is an OpenVINO export: format='openvino'")
|
|
535
|
+
return export_openvino(
|
|
536
|
+
self.net, self.names, imgsz=imgsz, out_dir=out_dir, fname=f"{stem}_int8",
|
|
537
|
+
verbose=verbose, layers=layers, queries=queries,
|
|
538
|
+
int8=_calibration_images(data, calib),
|
|
539
|
+
)
|
|
516
540
|
exporter = export_openvino if format == "openvino" else export_onnx
|
|
517
541
|
return exporter(
|
|
518
542
|
self.net, self.names, imgsz=imgsz, out_dir=out_dir, fname=stem,
|
|
519
|
-
half=half, verbose=verbose,
|
|
543
|
+
half=half, verbose=verbose, layers=layers, queries=queries,
|
|
520
544
|
)
|
|
521
545
|
|
|
522
546
|
# -------------------------------------------------------------------- misc
|
|
@@ -531,6 +555,29 @@ class Detector:
|
|
|
531
555
|
return f"Detector({self.model_name!r}, device={self.device!r})"
|
|
532
556
|
|
|
533
557
|
|
|
558
|
+
def _calibration_images(data: Any, count: int) -> list[Path]:
|
|
559
|
+
"""Up to ``count`` pictures spread over a data.yaml's train split, a folder or a list."""
|
|
560
|
+
from .data.dataset import list_images, load_data_yaml
|
|
561
|
+
|
|
562
|
+
if data is None:
|
|
563
|
+
raise ValueError("int8=True needs pictures to calibrate on: data='data.yaml' or a folder")
|
|
564
|
+
if isinstance(data, (list, tuple)):
|
|
565
|
+
files = [Path(f) for f in data]
|
|
566
|
+
else:
|
|
567
|
+
path = Path(data).expanduser()
|
|
568
|
+
if path.suffix in (".yaml", ".yml"):
|
|
569
|
+
cfg = load_data_yaml(path)
|
|
570
|
+
files = list_images(cfg["root"], cfg["train"], cfg["yaml_dir"])
|
|
571
|
+
elif path.is_dir():
|
|
572
|
+
files = list_images(path, ".")
|
|
573
|
+
else:
|
|
574
|
+
raise ValueError(f"{data}: give a data.yaml, a folder of pictures or a list")
|
|
575
|
+
if not files:
|
|
576
|
+
raise ValueError(f"no pictures found in {data}")
|
|
577
|
+
step = max(len(files) // max(int(count), 1), 1)
|
|
578
|
+
return files[::step][:count]
|
|
579
|
+
|
|
580
|
+
|
|
534
581
|
class _Stream:
|
|
535
582
|
"""The generator ``stream=True`` hands back, plus a nudge when it is dropped.
|
|
536
583
|
|
|
@@ -85,6 +85,30 @@ class DFINENet(nn.Module):
|
|
|
85
85
|
def forward(self, x, targets=None):
|
|
86
86
|
return self.decoder(self.encoder(self.backbone(x)), targets)
|
|
87
87
|
|
|
88
|
+
def trim(self, layers: int | None = None, queries: int | None = None) -> DFINENet:
|
|
89
|
+
"""Run fewer decoder layers or queries (in place, before :meth:`deploy`).
|
|
90
|
+
|
|
91
|
+
Every decoder layer is trained to answer on its own (each has its own
|
|
92
|
+
score and box heads and its own loss), so stopping after ``layers`` of
|
|
93
|
+
them gives a working detector that skips the rest. ``queries`` is how
|
|
94
|
+
many candidate boxes the encoder hands the decoder (300 as trained);
|
|
95
|
+
a scene with a handful of objects needs far fewer. Both cost accuracy
|
|
96
|
+
and buy speed — measure on your data (docs/performance.md).
|
|
97
|
+
"""
|
|
98
|
+
dec = self.decoder
|
|
99
|
+
if hasattr(dec.decoder, "project"): # convert_to_deploy() has run
|
|
100
|
+
raise RuntimeError("trim() before deploy(): a deployed network has dropped its layers")
|
|
101
|
+
if layers is not None:
|
|
102
|
+
if not 1 <= int(layers) <= dec.num_layers:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"layers must be 1..{dec.num_layers} for D-FINE-{self.variant.upper()}")
|
|
105
|
+
dec.eval_idx = dec.decoder.eval_idx = int(layers) - 1
|
|
106
|
+
if queries is not None:
|
|
107
|
+
if not 1 <= int(queries) <= dec.num_queries:
|
|
108
|
+
raise ValueError(f"queries must be 1..{dec.num_queries}")
|
|
109
|
+
dec.num_queries = int(queries)
|
|
110
|
+
return self
|
|
111
|
+
|
|
88
112
|
def deploy(self) -> DFINENet:
|
|
89
113
|
"""Fuse re-parameterisable blocks and drop training-only heads (in place)."""
|
|
90
114
|
self.eval()
|
|
@@ -128,6 +128,12 @@ def read_names(model_path: Path) -> dict[int, str]:
|
|
|
128
128
|
#: Default for ``iou``: above this IoU, two boxes are one object.
|
|
129
129
|
IOU = 0.7
|
|
130
130
|
|
|
131
|
+
#: Default for ``conf``. D-FINE gives an unsure box 0.3-0.5 where YOLO would
|
|
132
|
+
#: give it under 0.25, so YOLO's 0.25 shows mostly mistakes here: dfine-m on
|
|
133
|
+
#: COCO val2017 (NMS 0.7) shows boxes 32% right at 0.25, 70% at 0.5, while
|
|
134
|
+
#: still finding 67% of the objects. See docs/performance.md.
|
|
135
|
+
CONF = 0.5
|
|
136
|
+
|
|
131
137
|
|
|
132
138
|
def drop_duplicates(xyxy: np.ndarray, iou: float = IOU) -> np.ndarray:
|
|
133
139
|
"""Indices of the boxes to keep, from boxes sorted best first.
|
|
@@ -277,7 +283,7 @@ class Predictor:
|
|
|
277
283
|
boxes: np.ndarray,
|
|
278
284
|
scores: np.ndarray,
|
|
279
285
|
orig_shape: tuple[int, int],
|
|
280
|
-
conf: float =
|
|
286
|
+
conf: float = CONF,
|
|
281
287
|
max_det: int = 300,
|
|
282
288
|
classes: list[int] | None = None,
|
|
283
289
|
iou: float | None = IOU,
|
|
@@ -322,7 +328,7 @@ class Predictor:
|
|
|
322
328
|
def __call__(
|
|
323
329
|
self,
|
|
324
330
|
img: np.ndarray,
|
|
325
|
-
conf: float =
|
|
331
|
+
conf: float = CONF,
|
|
326
332
|
max_det: int = 300,
|
|
327
333
|
classes: list[int] | None = None,
|
|
328
334
|
iou: float | None = IOU,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "easydetect"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -45,6 +45,7 @@ train = [
|
|
|
45
45
|
"torchvision>=0.16",
|
|
46
46
|
"scipy>=1.9",
|
|
47
47
|
"onnx>=1.14",
|
|
48
|
+
"nncf>=2.10", # INT8 export
|
|
48
49
|
]
|
|
49
50
|
dev = [
|
|
50
51
|
"pytest>=7.0",
|
|
@@ -201,3 +201,15 @@ def test_the_whole_path_merges_pieces_only_when_asked():
|
|
|
201
201
|
assert sorted(merged[:, 5].tolist()) == [0, 56] # one person, one chair
|
|
202
202
|
chair = merged[merged[:, 5] == 56][0]
|
|
203
203
|
assert chair[:4].round().tolist() == [285, 455, 373, 640] # the whole of it
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_the_defaults_are_the_measured_ones():
|
|
207
|
+
"""conf 0.5, NMS 0.7, contain off: docs/performance.md, 'Confidence and overlap defaults'."""
|
|
208
|
+
import inspect
|
|
209
|
+
|
|
210
|
+
from easydetect import Detector
|
|
211
|
+
|
|
212
|
+
for method in (Detector.predict, Detector.track):
|
|
213
|
+
params = inspect.signature(method).parameters
|
|
214
|
+
assert params["conf"].default == 0.5 and params["iou"].default == 0.7
|
|
215
|
+
assert params["contain"].default is None
|
|
@@ -333,3 +333,50 @@ def test_a_learning_rate_that_is_given_is_used_as_it_is(dataset, tmp_path, monke
|
|
|
333
333
|
assert opt["lr"] == pytest.approx(3e-4) and opt["lr_from"] == "set"
|
|
334
334
|
assert opt["lr_backbone"] == pytest.approx(0.75e-4)
|
|
335
335
|
assert opt["warmup_epochs"] == 0 and opt["weight_decay"] == 0.0
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_a_lighter_decoder_exports_and_predicts(tmp_path, image):
|
|
339
|
+
"""layers / queries: fewer decoder layers and candidate boxes, same outputs otherwise."""
|
|
340
|
+
import onnxruntime as ort
|
|
341
|
+
|
|
342
|
+
from easydetect.exporter import export_onnx
|
|
343
|
+
from easydetect.nn import DFINENet
|
|
344
|
+
|
|
345
|
+
net = DFINENet("n", 2, pretrained_backbone=False)
|
|
346
|
+
names = {0: "a", 1: "b"}
|
|
347
|
+
full = export_onnx(net, names, imgsz=256, out_dir=tmp_path, fname="full", verbose=False)
|
|
348
|
+
light = export_onnx(net, names, imgsz=256, out_dir=tmp_path, fname="light", verbose=False,
|
|
349
|
+
layers=1, queries=50)
|
|
350
|
+
shapes = {} # at 256 px dfine-n has 16x16 + 8x8 places to start a box: over 300
|
|
351
|
+
for path in (full, light):
|
|
352
|
+
session = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"])
|
|
353
|
+
outs = session.run(None, {"images": np.zeros((1, 3, 256, 256), np.float32)})
|
|
354
|
+
shapes[path.stem] = [o.shape for o in outs]
|
|
355
|
+
assert shapes["full"] == [(1, 300, 4), (1, 300, 2)]
|
|
356
|
+
assert shapes["light"] == [(1, 50, 4), (1, 50, 2)]
|
|
357
|
+
assert net.decoder.num_queries == 300 # the caller's network is untouched
|
|
358
|
+
|
|
359
|
+
with pytest.raises(ValueError, match="layers must be 1..3"):
|
|
360
|
+
DFINENet("n", 2, pretrained_backbone=False).trim(layers=4)
|
|
361
|
+
with pytest.raises(RuntimeError, match="before deploy"):
|
|
362
|
+
DFINENet("n", 2, pretrained_backbone=False).deploy().trim(queries=10)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@needs_ov
|
|
366
|
+
def test_an_int8_export_calibrates_on_the_dataset_and_predicts(trained, dataset, tmp_path, image):
|
|
367
|
+
pytest.importorskip("nncf")
|
|
368
|
+
model, _ = trained
|
|
369
|
+
xml = model.export(format="openvino", imgsz=64, out_dir=tmp_path, int8=True,
|
|
370
|
+
data=str(dataset), calib=4, verbose=False)
|
|
371
|
+
assert xml.name.endswith("_int8.xml") and xml.with_suffix(".bin").exists()
|
|
372
|
+
import openvino as ov
|
|
373
|
+
|
|
374
|
+
ops = {op.get_type_name() for op in ov.Core().read_model(str(xml)).get_ops()}
|
|
375
|
+
assert "FakeQuantize" in ops # 8-bit, not a float IR renamed
|
|
376
|
+
results = Detector(str(xml), device="CPU", verbose=False)(image, conf=0.0, max_det=2)
|
|
377
|
+
assert results[0].names == {0: "box"} and results[0].boxes.xyxy.shape == (2, 4)
|
|
378
|
+
|
|
379
|
+
with pytest.raises(ValueError, match="calibrate"):
|
|
380
|
+
model.export(format="openvino", imgsz=64, out_dir=tmp_path, int8=True)
|
|
381
|
+
with pytest.raises(ValueError, match="OpenVINO export"):
|
|
382
|
+
model.export(format="onnx", imgsz=64, out_dir=tmp_path, int8=True, data=str(dataset))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Apache-2.0
|
|
3
|
+
"""Lighter exports side by side: fewer queries or decoder layers, INT8.
|
|
4
|
+
|
|
5
|
+
python tools/eval_exports.py --data ~/datasets/coco/data.yaml --model dfine-n --imgsz 320
|
|
6
|
+
python tools/eval_exports.py --data data.yaml --model runs/train/weights/best.pt
|
|
7
|
+
|
|
8
|
+
Each config (``layers:queries[:int8]``, ``all`` for every layer) is exported
|
|
9
|
+
from the same checkpoint (a mirror name's COCO .pt, or yours) and scored on
|
|
10
|
+
the same val pictures: mAP50-95 / mAP50 with every box (as ``val`` does),
|
|
11
|
+
precision and recall of what ``predict`` shows by default (conf 0.5, NMS 0.7),
|
|
12
|
+
and the median inference time. INT8 calibrates on the first ``--calib`` val
|
|
13
|
+
pictures, which are then left out of the scoring for every config alike.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import cv2
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from easydetect import Detector, downloads
|
|
25
|
+
from easydetect.data.dataset import DetDataset
|
|
26
|
+
from easydetect.predictor import drop_duplicates
|
|
27
|
+
from easydetect.validator import Evaluator
|
|
28
|
+
|
|
29
|
+
DEFAULT_CONFIGS = ["all:300", "all:100", "2:300", "1:300", "1:100", "all:300:int8", "all:100:int8"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def box_iou(x, y):
|
|
33
|
+
tl = np.maximum(x[:, None, :2], y[None, :, :2])
|
|
34
|
+
br = np.minimum(x[:, None, 2:], y[None, :, 2:])
|
|
35
|
+
inter = np.prod(np.clip(br - tl, 0, None), axis=2)
|
|
36
|
+
ax = np.prod(x[:, 2:] - x[:, :2], axis=1)
|
|
37
|
+
ay = np.prod(y[:, 2:] - y[:, :2], axis=1)
|
|
38
|
+
return inter / np.maximum(ax[:, None] + ay[None, :] - inter, 1e-9)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def shown_and_correct(r, gt, gk, w, h):
|
|
42
|
+
"""Boxes predict() shows by default (conf 0.5, NMS 0.7) and how many match a truth box."""
|
|
43
|
+
keep = r.boxes.conf >= 0.5
|
|
44
|
+
xyxy, cls = r.boxes.xyxy[keep].astype(np.float32), r.boxes.cls[keep].astype(int)
|
|
45
|
+
if len(xyxy) > 1:
|
|
46
|
+
kept = drop_duplicates(xyxy, 0.7)
|
|
47
|
+
xyxy, cls = xyxy[kept], cls[kept]
|
|
48
|
+
if not len(xyxy) or not len(gt):
|
|
49
|
+
return len(xyxy), 0
|
|
50
|
+
ious = box_iou(xyxy / [w, h, w, h], gt)
|
|
51
|
+
taken = np.zeros(len(gt), bool)
|
|
52
|
+
correct = 0
|
|
53
|
+
for i in range(len(xyxy)):
|
|
54
|
+
cand = np.where((gk == cls[i]) & ~taken & (ious[i] >= 0.5))[0]
|
|
55
|
+
if len(cand):
|
|
56
|
+
taken[cand[ious[i, cand].argmax()]] = True
|
|
57
|
+
correct += 1
|
|
58
|
+
return len(xyxy), correct
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
63
|
+
parser.add_argument("--data", required=True)
|
|
64
|
+
parser.add_argument("--model", default="dfine-n", help="a mirror name (its COCO .pt) or a .pt")
|
|
65
|
+
parser.add_argument("--imgsz", type=int, default=640)
|
|
66
|
+
parser.add_argument("--configs", nargs="+", default=DEFAULT_CONFIGS,
|
|
67
|
+
help="layers:queries[:int8]; 'all' = every decoder layer")
|
|
68
|
+
parser.add_argument("--calib", type=int, default=300, help="pictures to calibrate INT8 on")
|
|
69
|
+
parser.add_argument("--device", default="CPU")
|
|
70
|
+
parser.add_argument("--limit", type=int, help="score only the first N pictures")
|
|
71
|
+
parser.add_argument("--out", default="exports", help="where the exported variants go")
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
pt = Path(args.model)
|
|
75
|
+
if pt.suffix != ".pt":
|
|
76
|
+
pt = downloads.download_checkpoint(args.model)
|
|
77
|
+
ds = DetDataset(Path(args.data).expanduser(), "val", 64, augment=False)
|
|
78
|
+
calib, files = ds.files[:args.calib], ds.files[args.calib:]
|
|
79
|
+
files = files[:args.limit] if args.limit else files
|
|
80
|
+
truth = []
|
|
81
|
+
for f in files:
|
|
82
|
+
lab = ds._load_labels(f)
|
|
83
|
+
cx, cy, bw, bh = lab[:, 1], lab[:, 2], lab[:, 3], lab[:, 4]
|
|
84
|
+
truth.append((np.stack([cx - bw / 2, cy - bh / 2, cx + bw / 2, cy + bh / 2], 1),
|
|
85
|
+
lab[:, 0].astype(int)))
|
|
86
|
+
n_truth = sum(len(t[1]) for t in truth)
|
|
87
|
+
|
|
88
|
+
rows = []
|
|
89
|
+
for config in args.configs:
|
|
90
|
+
layers, queries, *rest = config.split(":")
|
|
91
|
+
int8 = rest == ["int8"]
|
|
92
|
+
out = Path(args.out) / f"{pt.stem}-{args.imgsz}-{config.replace(':', '-')}"
|
|
93
|
+
xml = Detector(str(pt), verbose=False).export(
|
|
94
|
+
format="openvino", imgsz=args.imgsz, out_dir=out,
|
|
95
|
+
layers=None if layers == "all" else int(layers), queries=int(queries),
|
|
96
|
+
int8=int8, data=calib if int8 else None, calib=args.calib, verbose=False)
|
|
97
|
+
model = Detector(str(xml), device=args.device, verbose=False)
|
|
98
|
+
for _ in range(3): # compile and warm up before the clock runs
|
|
99
|
+
model(cv2.imread(str(files[0])), verbose=False)
|
|
100
|
+
ev, shown, correct, times = Evaluator(ds.nc), 0, 0, []
|
|
101
|
+
for k, (f, (gt, gk)) in enumerate(zip(files, truth, strict=True)):
|
|
102
|
+
img = cv2.imread(str(f))
|
|
103
|
+
h, w = img.shape[:2]
|
|
104
|
+
r = model(img, conf=0.001, iou=None, max_det=300, verbose=False)[0]
|
|
105
|
+
times.append(r.speed["inference"])
|
|
106
|
+
ev.add(r.boxes.xyxy / [w, h, w, h], r.boxes.conf, r.boxes.cls.astype(int), gt, gk)
|
|
107
|
+
s, c = shown_and_correct(r, gt, gk, w, h)
|
|
108
|
+
shown, correct = shown + s, correct + c
|
|
109
|
+
if (k + 1) % 1000 == 0:
|
|
110
|
+
print(f" {config}: {k + 1}/{len(files)}", flush=True)
|
|
111
|
+
m = ev.compute()
|
|
112
|
+
rows.append((config, m["map"], m["map50"], correct / max(shown, 1),
|
|
113
|
+
correct / max(n_truth, 1), float(np.median(times))))
|
|
114
|
+
print(f" {config}: mAP50-95 {m['map']:.4f}, {rows[-1][-1]:.1f} ms", flush=True)
|
|
115
|
+
|
|
116
|
+
print(f"\n{pt.stem} at {args.imgsz}, {len(files)} pictures, {args.device}")
|
|
117
|
+
print(f"{'config':<16}{'mAP50-95':>9}{'mAP50':>8}{'P@0.5':>8}{'R@0.5':>8}{'ms':>8}")
|
|
118
|
+
for config, m, m50, precision, recall, ms in rows:
|
|
119
|
+
print(f"{config:<16}{m:9.4f}{m50:8.4f}{precision:8.1%}{recall:8.1%}{ms:8.1f}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
main()
|
|
@@ -29,7 +29,7 @@ pip install easydetect # OpenVINO and ONNX Runtime, no PyTorch
|
|
|
29
29
|
from easydetect import Detector
|
|
30
30
|
|
|
31
31
|
model = Detector("dfine-s") # downloads dfine-s/ from this repo once
|
|
32
|
-
results = model("photo.jpg", conf=0.
|
|
32
|
+
results = model("photo.jpg", conf=0.5)
|
|
33
33
|
results[0].save("result.jpg")
|
|
34
34
|
|
|
35
35
|
for r in model.predict(0, stream=True, show=True): # webcam; q or Esc quits
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|