easydetect 0.2.2__tar.gz → 0.2.3__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.2.3}/PKG-INFO +1 -1
  2. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/__init__.py +1 -1
  3. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/cli.py +1 -1
  4. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/model.py +10 -3
  5. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/predictor.py +56 -2
  6. {easydetect-0.2.2 → easydetect-0.2.3}/pyproject.toml +1 -1
  7. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_predictor.py +54 -0
  8. {easydetect-0.2.2 → easydetect-0.2.3}/.gitignore +0 -0
  9. {easydetect-0.2.2 → easydetect-0.2.3}/LICENSE +0 -0
  10. {easydetect-0.2.2 → easydetect-0.2.3}/NOTICE +0 -0
  11. {easydetect-0.2.2 → easydetect-0.2.3}/README.md +0 -0
  12. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/data/__init__.py +0 -0
  13. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/data/augment.py +0 -0
  14. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/data/dataset.py +0 -0
  15. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/data/labels.py +0 -0
  16. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/downloads.py +0 -0
  17. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/errors.py +0 -0
  18. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/exporter.py +0 -0
  19. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/metrics.py +0 -0
  20. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/__init__.py +0 -0
  21. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/decoder.py +0 -0
  22. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/denoising.py +0 -0
  23. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/dfine_net.py +0 -0
  24. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/encoder.py +0 -0
  25. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/hgnetv2.py +0 -0
  26. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/ops.py +0 -0
  27. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/plotting.py +0 -0
  28. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/results.py +0 -0
  29. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/sources.py +0 -0
  30. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/tracker.py +0 -0
  31. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/trainer.py +0 -0
  32. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/utils/__init__.py +0 -0
  33. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/utils/loss.py +0 -0
  34. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/utils/ops.py +0 -0
  35. {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/validator.py +0 -0
  36. {easydetect-0.2.2 → easydetect-0.2.3}/tests/__init__.py +0 -0
  37. {easydetect-0.2.2 → easydetect-0.2.3}/tests/conftest.py +0 -0
  38. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_augment.py +0 -0
  39. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_cli.py +0 -0
  40. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_coco2yolo.py +0 -0
  41. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_dataset.py +0 -0
  42. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_downloads.py +0 -0
  43. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_lab_api.py +0 -0
  44. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_model.py +0 -0
  45. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_nn.py +0 -0
  46. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_onnxruntime.py +0 -0
  47. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_results.py +0 -0
  48. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_sources.py +0 -0
  49. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_tracker.py +0 -0
  50. {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_train_export.py +0 -0
  51. {easydetect-0.2.2 → easydetect-0.2.3}/tools/bench.py +0 -0
  52. {easydetect-0.2.2 → easydetect-0.2.3}/tools/build_mirror.py +0 -0
  53. {easydetect-0.2.2 → easydetect-0.2.3}/tools/coco2yolo.py +0 -0
  54. {easydetect-0.2.2 → easydetect-0.2.3}/tools/compare_yolo.py +0 -0
  55. {easydetect-0.2.2 → easydetect-0.2.3}/tools/convert_dfine.py +0 -0
  56. {easydetect-0.2.2 → easydetect-0.2.3}/tools/hub_README.md +0 -0
  57. {easydetect-0.2.2 → easydetect-0.2.3}/tools/make_demo.py +0 -0
  58. {easydetect-0.2.2 → easydetect-0.2.3}/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.2.3
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.2.3"
28
28
 
29
29
  from .errors import DownloadError, EasyDetectError, ModelNotFoundError
30
30
  from .metrics import BoxMetrics, DetMetrics
@@ -29,7 +29,7 @@ Modes:
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
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
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
@@ -120,6 +120,7 @@ class Detector:
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,
@@ -136,6 +137,9 @@ class Detector:
136
137
  only sane option for a long video or a live camera). ``iou`` drops a
137
138
  box covering a higher-scoring one by more than that IoU — the same
138
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.
139
143
  """
140
144
  if source is None:
141
145
  raise ValueError("predict() needs a source (image, folder, video, url, camera index)")
@@ -152,6 +156,7 @@ class Detector:
152
156
  max_det=max_det,
153
157
  classes=classes,
154
158
  iou=iou,
159
+ contain=contain,
155
160
  save=save,
156
161
  show=show,
157
162
  project=project,
@@ -168,6 +173,7 @@ class Detector:
168
173
  source: Any = None,
169
174
  conf: float = 0.25,
170
175
  iou: float | None = 0.7,
176
+ contain: float | None = None,
171
177
  match_iou: float = 0.3,
172
178
  max_age: int = 30,
173
179
  persist: bool = False,
@@ -176,7 +182,7 @@ class Detector:
176
182
  ) -> Any:
177
183
  """Predict, and keep an id on each box across frames (``boxes.id``).
178
184
 
179
- ``iou`` is the duplicate filter, as in :meth:`predict`. ``match_iou``
185
+ ``iou`` and ``contain`` filter duplicates, as in :meth:`predict`. ``match_iou``
180
186
  is how much a box must overlap a track in the previous frame to carry
181
187
  its id on; ``max_age`` is how many frames a track waits for its object.
182
188
  """
@@ -185,7 +191,7 @@ class Detector:
185
191
  if self.tracker is None or not persist:
186
192
  self.tracker = IoUTracker(iou=match_iou, max_age=max_age)
187
193
  kwargs.setdefault("name", "track")
188
- gen = self._run(source, conf=conf, iou=iou, tracker=self.tracker,
194
+ gen = self._run(source, conf=conf, iou=iou, contain=contain, tracker=self.tracker,
189
195
  **self._track_kwargs(kwargs))
190
196
  return _Stream(gen, "track") if stream else list(gen)
191
197
 
@@ -209,6 +215,7 @@ class Detector:
209
215
  max_det: int = 300,
210
216
  classes: list[int] | None = None,
211
217
  iou: float | None = 0.7,
218
+ contain: float | None = None,
212
219
  save: bool = False,
213
220
  show: bool = False,
214
221
  project: str = "runs",
@@ -230,7 +237,7 @@ class Detector:
230
237
  try:
231
238
  for frame in loader:
232
239
  det, speed = predictor(frame.img, conf=conf, max_det=max_det, classes=classes,
233
- iou=iou)
240
+ iou=iou, contain=contain)
234
241
  if tracker is not None:
235
242
  det = tracker.update(det)
236
243
  result = Results(
@@ -158,6 +158,52 @@ def drop_duplicates(xyxy: np.ndarray, iou: float = IOU) -> np.ndarray:
158
158
  return np.nonzero(alive)[0]
159
159
 
160
160
 
161
+ #: How much less sure an enclosing box may be than the box inside it and still
162
+ #: count as the whole object rather than a loose box around several.
163
+ CONTAIN_MARGIN = 0.1
164
+
165
+
166
+ def drop_contained(xyxy: np.ndarray, scores: np.ndarray, cls: np.ndarray,
167
+ contain: float) -> np.ndarray:
168
+ """Indices of the boxes to keep when a box lies inside another of its class.
169
+
170
+ IoU cannot see a part inside a whole: a chair half hidden behind a person
171
+ can come back as the whole chair (0.64) plus its two visible pieces (0.67,
172
+ 0.61), and each piece overlaps the whole by an IoU of only its share of the
173
+ area. Measured as the overlap over the *smaller* box instead, a piece sits
174
+ near 1.0, so a pair of one class sharing at least ``contain`` of the
175
+ smaller box is one object, and one box goes:
176
+
177
+ * the inner one, when the enclosing box is about as sure (within
178
+ ``CONTAIN_MARGIN``) — the whole object and a piece of it;
179
+ * the enclosing one, when it is much less sure than the box inside — a
180
+ loose box around a group, which must not erase the members.
181
+
182
+ NMS's "keep the best" would not do here: the best box is a piece, and once
183
+ it has removed the whole, the other piece no longer overlaps anything and
184
+ stays — two half chairs. Same class only: a cup on a table is not part of
185
+ the table. The cost is a real object inside another of its class, a child
186
+ held by an adult, so this is off unless asked for.
187
+ """
188
+ x1, y1, x2, y2 = xyxy.T
189
+ area = np.clip(x2 - x1, 0, None) * np.clip(y2 - y1, 0, None)
190
+ gone = np.zeros(len(xyxy), bool)
191
+ for i in range(len(xyxy)):
192
+ for j in range(i + 1, len(xyxy)):
193
+ if cls[i] != cls[j]:
194
+ continue
195
+ iw = max(0.0, min(x2[i], x2[j]) - max(x1[i], x1[j]))
196
+ ih = max(0.0, min(y2[i], y2[j]) - max(y1[i], y1[j]))
197
+ if iw * ih < contain * max(min(area[i], area[j]), 1e-9):
198
+ continue
199
+ big, small = (i, j) if area[i] >= area[j] else (j, i)
200
+ if scores[big] >= scores[small] - CONTAIN_MARGIN:
201
+ gone[small] = True
202
+ else:
203
+ gone[big] = True
204
+ return np.nonzero(~gone)[0]
205
+
206
+
161
207
  def _split_outputs(arrays: list[np.ndarray]) -> tuple[np.ndarray, np.ndarray]:
162
208
  """``(boxes, scores)`` for the one image in a batch, whatever order they come in."""
163
209
  boxes = next((a for a in arrays if a.shape[-1] == 4), None)
@@ -235,12 +281,15 @@ class Predictor:
235
281
  max_det: int = 300,
236
282
  classes: list[int] | None = None,
237
283
  iou: float | None = IOU,
284
+ contain: float | None = None,
238
285
  ) -> np.ndarray:
239
286
  """-> ``(N, 6)`` array of ``x1 y1 x2 y2 conf cls`` in pixels.
240
287
 
241
288
  ``iou`` drops a box that covers a higher-scoring one by more than that
242
289
  IoU, whatever the two classes (``None`` keeps them all): see
243
- :func:`drop_duplicates`.
290
+ :func:`drop_duplicates`. ``contain`` (off by default) also drops a box
291
+ lying mostly inside another of its class: see
292
+ :func:`drop_contained`.
244
293
  """
245
294
  if _looks_like_logits(scores):
246
295
  scores = 1.0 / (1.0 + np.exp(-scores))
@@ -263,6 +312,9 @@ class Predictor:
263
312
  if iou is not None and len(xyxy) > 1:
264
313
  kept = drop_duplicates(xyxy, iou)
265
314
  xyxy, best, cls = xyxy[kept], best[kept], cls[kept]
315
+ if contain is not None and len(xyxy) > 1:
316
+ kept = drop_contained(xyxy, best, cls, contain)
317
+ xyxy, best, cls = xyxy[kept], best[kept], cls[kept]
266
318
  return np.concatenate(
267
319
  [xyxy, best.astype(np.float32)[:, None], cls.astype(np.float32)[:, None]], axis=1
268
320
  )
@@ -274,6 +326,7 @@ class Predictor:
274
326
  max_det: int = 300,
275
327
  classes: list[int] | None = None,
276
328
  iou: float | None = IOU,
329
+ contain: float | None = None,
277
330
  ) -> tuple[np.ndarray, dict[str, float]]:
278
331
  """Run one image; returns ``(detections (N,6), speed dict in ms)``."""
279
332
  import time
@@ -283,7 +336,8 @@ class Predictor:
283
336
  t1 = time.perf_counter()
284
337
  boxes, scores = self.infer(tensor)
285
338
  t2 = time.perf_counter()
286
- det = self.postprocess(boxes, scores, img.shape[:2], conf, max_det, classes, iou)
339
+ det = self.postprocess(boxes, scores, img.shape[:2], conf, max_det, classes, iou,
340
+ contain)
287
341
  t3 = time.perf_counter()
288
342
  speed = {
289
343
  "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.2.3"
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,57 @@ 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
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