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.
Files changed (59) hide show
  1. {easydetect-0.2.3 → easydetect-0.3.1}/PKG-INFO +2 -1
  2. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/__init__.py +1 -1
  3. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/cli.py +1 -1
  4. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/exporter.py +44 -7
  5. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/model.py +58 -11
  6. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/dfine_net.py +24 -0
  7. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/predictor.py +8 -2
  8. {easydetect-0.2.3 → easydetect-0.3.1}/pyproject.toml +2 -1
  9. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_predictor.py +12 -0
  10. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_train_export.py +47 -0
  11. easydetect-0.3.1/tools/eval_exports.py +123 -0
  12. {easydetect-0.2.3 → easydetect-0.3.1}/tools/hub_README.md +1 -1
  13. {easydetect-0.2.3 → easydetect-0.3.1}/.gitignore +0 -0
  14. {easydetect-0.2.3 → easydetect-0.3.1}/LICENSE +0 -0
  15. {easydetect-0.2.3 → easydetect-0.3.1}/NOTICE +0 -0
  16. {easydetect-0.2.3 → easydetect-0.3.1}/README.md +0 -0
  17. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/data/__init__.py +0 -0
  18. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/data/augment.py +0 -0
  19. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/data/dataset.py +0 -0
  20. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/data/labels.py +0 -0
  21. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/downloads.py +0 -0
  22. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/errors.py +0 -0
  23. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/metrics.py +0 -0
  24. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/__init__.py +0 -0
  25. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/decoder.py +0 -0
  26. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/denoising.py +0 -0
  27. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/encoder.py +0 -0
  28. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/hgnetv2.py +0 -0
  29. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/nn/ops.py +0 -0
  30. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/plotting.py +0 -0
  31. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/results.py +0 -0
  32. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/sources.py +0 -0
  33. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/tracker.py +0 -0
  34. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/trainer.py +0 -0
  35. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/utils/__init__.py +0 -0
  36. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/utils/loss.py +0 -0
  37. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/utils/ops.py +0 -0
  38. {easydetect-0.2.3 → easydetect-0.3.1}/easydetect/validator.py +0 -0
  39. {easydetect-0.2.3 → easydetect-0.3.1}/tests/__init__.py +0 -0
  40. {easydetect-0.2.3 → easydetect-0.3.1}/tests/conftest.py +0 -0
  41. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_augment.py +0 -0
  42. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_cli.py +0 -0
  43. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_coco2yolo.py +0 -0
  44. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_dataset.py +0 -0
  45. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_downloads.py +0 -0
  46. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_lab_api.py +0 -0
  47. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_model.py +0 -0
  48. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_nn.py +0 -0
  49. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_onnxruntime.py +0 -0
  50. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_results.py +0 -0
  51. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_sources.py +0 -0
  52. {easydetect-0.2.3 → easydetect-0.3.1}/tests/test_tracker.py +0 -0
  53. {easydetect-0.2.3 → easydetect-0.3.1}/tools/bench.py +0 -0
  54. {easydetect-0.2.3 → easydetect-0.3.1}/tools/build_mirror.py +0 -0
  55. {easydetect-0.2.3 → easydetect-0.3.1}/tools/coco2yolo.py +0 -0
  56. {easydetect-0.2.3 → easydetect-0.3.1}/tools/compare_yolo.py +0 -0
  57. {easydetect-0.2.3 → easydetect-0.3.1}/tools/convert_dfine.py +0 -0
  58. {easydetect-0.2.3 → easydetect-0.3.1}/tools/make_demo.py +0 -0
  59. {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.2.3
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.2.3"
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.25 iou=0.7|none device=0|cpu|AUTO
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
- """Write ``<out_dir>/<fname>.onnx`` (plus labels). Returns the .onnx path."""
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
- ov.save_model(model, str(xml_path), compress_to_fp16=half)
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
- print(f"[easydetect] exported: {xml_path} ({'FP16' if half else 'FP32'})")
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.25,
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). ``iou`` drops a
138
- box covering a higher-scoring one by more than that IoU — the same
139
- object found twice, sometimes under two classes; ``None`` keeps all.
140
- ``contain=0.8`` also merges a box with 80% of its area inside another
141
- of its class — the visible pieces of a half hidden object reported
142
- beside the whole of it — into one box. Off by default.
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.25,
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.25,
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 = 0.25,
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 = 0.25,
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.2.3"
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.25)
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