easydetect 0.2.2__tar.gz → 0.3.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 (58) hide show
  1. {easydetect-0.2.2 → easydetect-0.3.0}/PKG-INFO +1 -1
  2. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/__init__.py +1 -1
  3. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/cli.py +2 -2
  4. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/model.py +18 -9
  5. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/predictor.py +64 -4
  6. {easydetect-0.2.2 → easydetect-0.3.0}/pyproject.toml +1 -1
  7. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_predictor.py +66 -0
  8. {easydetect-0.2.2 → easydetect-0.3.0}/tools/hub_README.md +1 -1
  9. {easydetect-0.2.2 → easydetect-0.3.0}/.gitignore +0 -0
  10. {easydetect-0.2.2 → easydetect-0.3.0}/LICENSE +0 -0
  11. {easydetect-0.2.2 → easydetect-0.3.0}/NOTICE +0 -0
  12. {easydetect-0.2.2 → easydetect-0.3.0}/README.md +0 -0
  13. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/data/__init__.py +0 -0
  14. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/data/augment.py +0 -0
  15. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/data/dataset.py +0 -0
  16. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/data/labels.py +0 -0
  17. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/downloads.py +0 -0
  18. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/errors.py +0 -0
  19. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/exporter.py +0 -0
  20. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/metrics.py +0 -0
  21. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/nn/__init__.py +0 -0
  22. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/nn/decoder.py +0 -0
  23. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/nn/denoising.py +0 -0
  24. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/nn/dfine_net.py +0 -0
  25. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/nn/encoder.py +0 -0
  26. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/nn/hgnetv2.py +0 -0
  27. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/nn/ops.py +0 -0
  28. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/plotting.py +0 -0
  29. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/results.py +0 -0
  30. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/sources.py +0 -0
  31. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/tracker.py +0 -0
  32. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/trainer.py +0 -0
  33. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/utils/__init__.py +0 -0
  34. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/utils/loss.py +0 -0
  35. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/utils/ops.py +0 -0
  36. {easydetect-0.2.2 → easydetect-0.3.0}/easydetect/validator.py +0 -0
  37. {easydetect-0.2.2 → easydetect-0.3.0}/tests/__init__.py +0 -0
  38. {easydetect-0.2.2 → easydetect-0.3.0}/tests/conftest.py +0 -0
  39. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_augment.py +0 -0
  40. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_cli.py +0 -0
  41. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_coco2yolo.py +0 -0
  42. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_dataset.py +0 -0
  43. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_downloads.py +0 -0
  44. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_lab_api.py +0 -0
  45. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_model.py +0 -0
  46. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_nn.py +0 -0
  47. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_onnxruntime.py +0 -0
  48. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_results.py +0 -0
  49. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_sources.py +0 -0
  50. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_tracker.py +0 -0
  51. {easydetect-0.2.2 → easydetect-0.3.0}/tests/test_train_export.py +0 -0
  52. {easydetect-0.2.2 → easydetect-0.3.0}/tools/bench.py +0 -0
  53. {easydetect-0.2.2 → easydetect-0.3.0}/tools/build_mirror.py +0 -0
  54. {easydetect-0.2.2 → easydetect-0.3.0}/tools/coco2yolo.py +0 -0
  55. {easydetect-0.2.2 → easydetect-0.3.0}/tools/compare_yolo.py +0 -0
  56. {easydetect-0.2.2 → easydetect-0.3.0}/tools/convert_dfine.py +0 -0
  57. {easydetect-0.2.2 → easydetect-0.3.0}/tools/make_demo.py +0 -0
  58. {easydetect-0.2.2 → easydetect-0.3.0}/tools/make_toyset.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: easydetect
3
- Version: 0.2.2
3
+ Version: 0.3.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
@@ -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.2"
27
+ __version__ = "0.3.0"
28
28
 
29
29
  from .errors import DownloadError, EasyDetectError, ModelNotFoundError
30
30
  from .metrics import BoxMetrics, DetMetrics
@@ -28,8 +28,8 @@ 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
32
- project=runs name=predict save=true show=false format=openvino half=true
31
+ data=data.yaml epochs=100 imgsz=640 batch=8 conf=0.5 iou=0.7|none device=0|cpu|AUTO
32
+ contain=0.8 project=runs name=predict save=true show=false format=openvino half=true
33
33
 
34
34
  Examples:
35
35
  easydetect predict model=dfine-s source=bus.jpg conf=0.5
@@ -114,12 +114,13 @@ 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,
121
121
  classes: list[int] | None = None,
122
122
  iou: float | None = 0.7,
123
+ contain: float | None = None,
123
124
  stream: bool = False,
124
125
  save: bool = False,
125
126
  show: bool = False,
@@ -133,9 +134,14 @@ class Detector:
133
134
  """Run detection on any supported source.
134
135
 
135
136
  Returns a ``list[Results]``, or a generator when ``stream=True`` (the
136
- only sane option for a long video or a live camera). ``iou`` drops a
137
- box covering a higher-scoring one by more than that IoU — the same
138
- object found twice, sometimes under two classes; ``None`` keeps all.
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.
139
145
  """
140
146
  if source is None:
141
147
  raise ValueError("predict() needs a source (image, folder, video, url, camera index)")
@@ -152,6 +158,7 @@ class Detector:
152
158
  max_det=max_det,
153
159
  classes=classes,
154
160
  iou=iou,
161
+ contain=contain,
155
162
  save=save,
156
163
  show=show,
157
164
  project=project,
@@ -166,8 +173,9 @@ class Detector:
166
173
  def track(
167
174
  self,
168
175
  source: Any = None,
169
- conf: float = 0.25,
176
+ conf: float = 0.5,
170
177
  iou: float | None = 0.7,
178
+ contain: float | None = None,
171
179
  match_iou: float = 0.3,
172
180
  max_age: int = 30,
173
181
  persist: bool = False,
@@ -176,7 +184,7 @@ class Detector:
176
184
  ) -> Any:
177
185
  """Predict, and keep an id on each box across frames (``boxes.id``).
178
186
 
179
- ``iou`` is the duplicate filter, as in :meth:`predict`. ``match_iou``
187
+ ``iou`` and ``contain`` filter duplicates, as in :meth:`predict`. ``match_iou``
180
188
  is how much a box must overlap a track in the previous frame to carry
181
189
  its id on; ``max_age`` is how many frames a track waits for its object.
182
190
  """
@@ -185,7 +193,7 @@ class Detector:
185
193
  if self.tracker is None or not persist:
186
194
  self.tracker = IoUTracker(iou=match_iou, max_age=max_age)
187
195
  kwargs.setdefault("name", "track")
188
- gen = self._run(source, conf=conf, iou=iou, tracker=self.tracker,
196
+ gen = self._run(source, conf=conf, iou=iou, contain=contain, tracker=self.tracker,
189
197
  **self._track_kwargs(kwargs))
190
198
  return _Stream(gen, "track") if stream else list(gen)
191
199
 
@@ -203,12 +211,13 @@ class Detector:
203
211
  def _run(
204
212
  self,
205
213
  source: Any,
206
- conf: float = 0.25,
214
+ conf: float = 0.5,
207
215
  imgsz: int | None = None,
208
216
  device: str | None = None,
209
217
  max_det: int = 300,
210
218
  classes: list[int] | None = None,
211
219
  iou: float | None = 0.7,
220
+ contain: float | None = None,
212
221
  save: bool = False,
213
222
  show: bool = False,
214
223
  project: str = "runs",
@@ -230,7 +239,7 @@ class Detector:
230
239
  try:
231
240
  for frame in loader:
232
241
  det, speed = predictor(frame.img, conf=conf, max_det=max_det, classes=classes,
233
- iou=iou)
242
+ iou=iou, contain=contain)
234
243
  if tracker is not None:
235
244
  det = tracker.update(det)
236
245
  result = Results(
@@ -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.
@@ -158,6 +164,52 @@ def drop_duplicates(xyxy: np.ndarray, iou: float = IOU) -> np.ndarray:
158
164
  return np.nonzero(alive)[0]
159
165
 
160
166
 
167
+ #: How much less sure an enclosing box may be than the box inside it and still
168
+ #: count as the whole object rather than a loose box around several.
169
+ CONTAIN_MARGIN = 0.1
170
+
171
+
172
+ def drop_contained(xyxy: np.ndarray, scores: np.ndarray, cls: np.ndarray,
173
+ contain: float) -> np.ndarray:
174
+ """Indices of the boxes to keep when a box lies inside another of its class.
175
+
176
+ IoU cannot see a part inside a whole: a chair half hidden behind a person
177
+ can come back as the whole chair (0.64) plus its two visible pieces (0.67,
178
+ 0.61), and each piece overlaps the whole by an IoU of only its share of the
179
+ area. Measured as the overlap over the *smaller* box instead, a piece sits
180
+ near 1.0, so a pair of one class sharing at least ``contain`` of the
181
+ smaller box is one object, and one box goes:
182
+
183
+ * the inner one, when the enclosing box is about as sure (within
184
+ ``CONTAIN_MARGIN``) — the whole object and a piece of it;
185
+ * the enclosing one, when it is much less sure than the box inside — a
186
+ loose box around a group, which must not erase the members.
187
+
188
+ NMS's "keep the best" would not do here: the best box is a piece, and once
189
+ it has removed the whole, the other piece no longer overlaps anything and
190
+ stays — two half chairs. Same class only: a cup on a table is not part of
191
+ the table. The cost is a real object inside another of its class, a child
192
+ held by an adult, so this is off unless asked for.
193
+ """
194
+ x1, y1, x2, y2 = xyxy.T
195
+ area = np.clip(x2 - x1, 0, None) * np.clip(y2 - y1, 0, None)
196
+ gone = np.zeros(len(xyxy), bool)
197
+ for i in range(len(xyxy)):
198
+ for j in range(i + 1, len(xyxy)):
199
+ if cls[i] != cls[j]:
200
+ continue
201
+ iw = max(0.0, min(x2[i], x2[j]) - max(x1[i], x1[j]))
202
+ ih = max(0.0, min(y2[i], y2[j]) - max(y1[i], y1[j]))
203
+ if iw * ih < contain * max(min(area[i], area[j]), 1e-9):
204
+ continue
205
+ big, small = (i, j) if area[i] >= area[j] else (j, i)
206
+ if scores[big] >= scores[small] - CONTAIN_MARGIN:
207
+ gone[small] = True
208
+ else:
209
+ gone[big] = True
210
+ return np.nonzero(~gone)[0]
211
+
212
+
161
213
  def _split_outputs(arrays: list[np.ndarray]) -> tuple[np.ndarray, np.ndarray]:
162
214
  """``(boxes, scores)`` for the one image in a batch, whatever order they come in."""
163
215
  boxes = next((a for a in arrays if a.shape[-1] == 4), None)
@@ -231,16 +283,19 @@ class Predictor:
231
283
  boxes: np.ndarray,
232
284
  scores: np.ndarray,
233
285
  orig_shape: tuple[int, int],
234
- conf: float = 0.25,
286
+ conf: float = CONF,
235
287
  max_det: int = 300,
236
288
  classes: list[int] | None = None,
237
289
  iou: float | None = IOU,
290
+ contain: float | None = None,
238
291
  ) -> np.ndarray:
239
292
  """-> ``(N, 6)`` array of ``x1 y1 x2 y2 conf cls`` in pixels.
240
293
 
241
294
  ``iou`` drops a box that covers a higher-scoring one by more than that
242
295
  IoU, whatever the two classes (``None`` keeps them all): see
243
- :func:`drop_duplicates`.
296
+ :func:`drop_duplicates`. ``contain`` (off by default) also drops a box
297
+ lying mostly inside another of its class: see
298
+ :func:`drop_contained`.
244
299
  """
245
300
  if _looks_like_logits(scores):
246
301
  scores = 1.0 / (1.0 + np.exp(-scores))
@@ -263,6 +318,9 @@ class Predictor:
263
318
  if iou is not None and len(xyxy) > 1:
264
319
  kept = drop_duplicates(xyxy, iou)
265
320
  xyxy, best, cls = xyxy[kept], best[kept], cls[kept]
321
+ if contain is not None and len(xyxy) > 1:
322
+ kept = drop_contained(xyxy, best, cls, contain)
323
+ xyxy, best, cls = xyxy[kept], best[kept], cls[kept]
266
324
  return np.concatenate(
267
325
  [xyxy, best.astype(np.float32)[:, None], cls.astype(np.float32)[:, None]], axis=1
268
326
  )
@@ -270,10 +328,11 @@ class Predictor:
270
328
  def __call__(
271
329
  self,
272
330
  img: np.ndarray,
273
- conf: float = 0.25,
331
+ conf: float = CONF,
274
332
  max_det: int = 300,
275
333
  classes: list[int] | None = None,
276
334
  iou: float | None = IOU,
335
+ contain: float | None = None,
277
336
  ) -> tuple[np.ndarray, dict[str, float]]:
278
337
  """Run one image; returns ``(detections (N,6), speed dict in ms)``."""
279
338
  import time
@@ -283,7 +342,8 @@ class Predictor:
283
342
  t1 = time.perf_counter()
284
343
  boxes, scores = self.infer(tensor)
285
344
  t2 = time.perf_counter()
286
- det = self.postprocess(boxes, scores, img.shape[:2], conf, max_det, classes, iou)
345
+ det = self.postprocess(boxes, scores, img.shape[:2], conf, max_det, classes, iou,
346
+ contain)
287
347
  t3 = time.perf_counter()
288
348
  speed = {
289
349
  "preprocess": (t1 - t0) * 1e3,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "easydetect"
7
- version = "0.2.2"
7
+ version = "0.3.0"
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"
@@ -147,3 +147,69 @@ def test_one_predictor_can_be_shared_between_threads(tiny_ir):
147
147
  for thread in threads:
148
148
  thread.join()
149
149
  assert failures == []
150
+
151
+
152
+ def test_a_half_hidden_object_and_its_pieces_become_one_box():
153
+ from easydetect.predictor import drop_contained, drop_duplicates
154
+
155
+ # best first: a piece (0.67), the whole chair (0.64), another piece (0.61),
156
+ # and a second chair beside it that only touches the first
157
+ xyxy = np.array([[320, 520, 372, 640], # lower right piece
158
+ [285, 455, 373, 640], # the whole chair
159
+ [285, 470, 330, 640], # left piece
160
+ [380, 455, 470, 640]], # another chair
161
+ np.float32)
162
+ scores = np.array([0.67, 0.64, 0.61, 0.60])
163
+ cls = np.array([56, 56, 56, 56])
164
+ assert list(drop_duplicates(xyxy, 0.7)) == [0, 1, 2, 3] # IoU sees four chairs
165
+ # the whole chair stays, its pieces go; the chair beside it is left alone
166
+ assert list(drop_contained(xyxy, scores, cls, 0.8)) == [1, 3]
167
+
168
+
169
+ def test_a_loose_box_around_a_group_does_not_erase_its_members():
170
+ from easydetect.predictor import drop_contained
171
+
172
+ xyxy = np.array([[0, 0, 50, 100], [50, 0, 100, 100], [0, 0, 100, 100]], np.float32)
173
+ scores = np.array([0.9, 0.85, 0.3]) # two people, and a box around both
174
+ assert list(drop_contained(xyxy, scores, np.zeros(3), 0.8)) == [0, 1]
175
+
176
+
177
+ def test_containment_never_crosses_classes():
178
+ from easydetect.predictor import drop_contained
179
+
180
+ xyxy = np.array([[0, 0, 100, 100], [10, 10, 40, 40]], np.float32) # a cup on a table
181
+ scores = np.array([0.8, 0.7])
182
+ assert list(drop_contained(xyxy, scores, np.array([60, 41]), 0.8)) == [0, 1]
183
+ assert list(drop_contained(xyxy, scores, np.array([60, 60]), 0.8)) == [0]
184
+
185
+
186
+ def test_the_whole_path_merges_pieces_only_when_asked():
187
+ """postprocess as predict() runs it: model outputs in, boxes out, at 640 x 720."""
188
+ from easydetect.predictor import Predictor
189
+
190
+ pieces = [(320, 520, 372, 640, 0.67), (285, 455, 373, 640, 0.64), (285, 470, 330, 640, 0.61),
191
+ (40, 100, 340, 640, 0.97)] # three chair boxes and a person
192
+ boxes = np.array([[(x1 + x2) / 2 / 640, (y1 + y2) / 2 / 720, (x2 - x1) / 640,
193
+ (y2 - y1) / 720] for x1, y1, x2, y2, _ in pieces], np.float32)
194
+ scores = np.zeros((4, 80), np.float32)
195
+ for i, (*_, s) in enumerate(pieces):
196
+ scores[i, 0 if i == 3 else 56] = s
197
+ post = Predictor.postprocess.__get__(object.__new__(Predictor))
198
+ default = post(boxes, scores, (720, 640), conf=0.5)
199
+ merged = post(boxes, scores, (720, 640), conf=0.5, contain=0.8)
200
+ assert len(default) == 4 # NMS at 0.7 keeps every piece
201
+ assert sorted(merged[:, 5].tolist()) == [0, 56] # one person, one chair
202
+ chair = merged[merged[:, 5] == 56][0]
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
@@ -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