easydetect 0.7.2__tar.gz → 0.7.4__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 (88) hide show
  1. {easydetect-0.7.2 → easydetect-0.7.4}/PKG-INFO +1 -1
  2. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/__init__.py +1 -1
  3. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/benchmark.py +3 -1
  4. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/data/dataset.py +3 -1
  5. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/data/keypoints.py +4 -2
  6. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/exporter.py +4 -4
  7. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/model.py +21 -2
  8. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/results.py +3 -2
  9. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/seg_trainer.py +3 -2
  10. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/sources.py +4 -2
  11. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/trainer.py +4 -0
  12. easydetect-0.7.4/easydetect/utils/imageio.py +64 -0
  13. {easydetect-0.7.2 → easydetect-0.7.4}/pyproject.toml +1 -1
  14. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_robustness.py +34 -0
  15. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_sources_threads.py +4 -2
  16. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_train_export.py +17 -0
  17. {easydetect-0.7.2 → easydetect-0.7.4}/tools/bench_finetune.py +9 -3
  18. {easydetect-0.7.2 → easydetect-0.7.4}/tools/profile_train.py +9 -1
  19. {easydetect-0.7.2 → easydetect-0.7.4}/.gitignore +0 -0
  20. {easydetect-0.7.2 → easydetect-0.7.4}/LICENSE +0 -0
  21. {easydetect-0.7.2 → easydetect-0.7.4}/NOTICE +0 -0
  22. {easydetect-0.7.2 → easydetect-0.7.4}/README.md +0 -0
  23. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/cli.py +0 -0
  24. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/data/__init__.py +0 -0
  25. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/data/augment.py +0 -0
  26. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/data/labels.py +0 -0
  27. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/downloads.py +0 -0
  28. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/errors.py +0 -0
  29. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/metrics.py +0 -0
  30. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/__init__.py +0 -0
  31. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/decoder.py +0 -0
  32. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/denoising.py +0 -0
  33. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/dfine_net.py +0 -0
  34. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/encoder.py +0 -0
  35. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/hgnetv2.py +0 -0
  36. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/ops.py +0 -0
  37. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/posenet.py +0 -0
  38. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/nn/sam_decoder.py +0 -0
  39. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/plotting.py +0 -0
  40. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/pose.py +0 -0
  41. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/pose_trainer.py +0 -0
  42. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/predictor.py +0 -0
  43. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/segment.py +0 -0
  44. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/serve.py +0 -0
  45. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/tracker.py +0 -0
  46. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/utils/__init__.py +0 -0
  47. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/utils/checkpoint.py +0 -0
  48. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/utils/loss.py +0 -0
  49. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/utils/ops.py +0 -0
  50. {easydetect-0.7.2 → easydetect-0.7.4}/easydetect/validator.py +0 -0
  51. {easydetect-0.7.2 → easydetect-0.7.4}/tests/__init__.py +0 -0
  52. {easydetect-0.7.2 → easydetect-0.7.4}/tests/conftest.py +0 -0
  53. {easydetect-0.7.2 → easydetect-0.7.4}/tests/pose_toy.py +0 -0
  54. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_augment.py +0 -0
  55. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_benchmark.py +0 -0
  56. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_cli.py +0 -0
  57. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_coco2yolo.py +0 -0
  58. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_custom_pose.py +0 -0
  59. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_custom_seg.py +0 -0
  60. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_dataset.py +0 -0
  61. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_downloads.py +0 -0
  62. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_lab_api.py +0 -0
  63. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_model.py +0 -0
  64. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_nn.py +0 -0
  65. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_onnxruntime.py +0 -0
  66. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_plotting.py +0 -0
  67. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_pose.py +0 -0
  68. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_predictor.py +0 -0
  69. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_results.py +0 -0
  70. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_results_summary.py +0 -0
  71. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_segment.py +0 -0
  72. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_segment_lowres.py +0 -0
  73. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_serve.py +0 -0
  74. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_sources.py +0 -0
  75. {easydetect-0.7.2 → easydetect-0.7.4}/tests/test_tracker.py +0 -0
  76. {easydetect-0.7.2 → easydetect-0.7.4}/tools/bench.py +0 -0
  77. {easydetect-0.7.2 → easydetect-0.7.4}/tools/build_mirror.py +0 -0
  78. {easydetect-0.7.2 → easydetect-0.7.4}/tools/coco2yolo.py +0 -0
  79. {easydetect-0.7.2 → easydetect-0.7.4}/tools/compare_yolo.py +0 -0
  80. {easydetect-0.7.2 → easydetect-0.7.4}/tools/convert_dfine.py +0 -0
  81. {easydetect-0.7.2 → easydetect-0.7.4}/tools/convert_sam.py +0 -0
  82. {easydetect-0.7.2 → easydetect-0.7.4}/tools/eval_exports.py +0 -0
  83. {easydetect-0.7.2 → easydetect-0.7.4}/tools/hub_README.md +0 -0
  84. {easydetect-0.7.2 → easydetect-0.7.4}/tools/make_demo.py +0 -0
  85. {easydetect-0.7.2 → easydetect-0.7.4}/tools/make_toyset.py +0 -0
  86. {easydetect-0.7.2 → easydetect-0.7.4}/tools/pi_kit.py +0 -0
  87. {easydetect-0.7.2 → easydetect-0.7.4}/tools/train_pose.py +0 -0
  88. {easydetect-0.7.2 → easydetect-0.7.4}/tools/voc2yolo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: easydetect
3
- Version: 0.7.2
3
+ Version: 0.7.4
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.7.2"
27
+ __version__ = "0.7.4"
28
28
 
29
29
  from .errors import DownloadError, EasyDetectError, ModelNotFoundError
30
30
  from .metrics import BoxMetrics, DetMetrics
@@ -23,6 +23,8 @@ from pathlib import Path
23
23
 
24
24
  import numpy as np
25
25
 
26
+ from easydetect.utils.imageio import imread # any path, Hangul included
27
+
26
28
 
27
29
  def _model_files(spec) -> list[Path]:
28
30
  items = spec if isinstance(spec, (list, tuple)) else [spec]
@@ -75,7 +77,7 @@ def _picture(source) -> np.ndarray:
75
77
  return cv2.GaussianBlur(noise, (0, 0), 3)
76
78
  import cv2
77
79
 
78
- img = cv2.imread(str(source))
80
+ img = imread(str(source))
79
81
  if img is None:
80
82
  raise FileNotFoundError(f"cannot read {source}")
81
83
  return img
@@ -19,6 +19,8 @@ import torch
19
19
  import yaml
20
20
  from torch.utils.data import Dataset
21
21
 
22
+ from easydetect.utils.imageio import imread # any path, Hangul included
23
+
22
24
  from . import augment as aug
23
25
  from .labels import label_path, label_row_to_box
24
26
 
@@ -201,7 +203,7 @@ class DetDataset(Dataset):
201
203
 
202
204
  def _raw(self, i):
203
205
  f = self.files[i]
204
- img = cv2.imread(str(f))
206
+ img = imread(str(f))
205
207
  if img is None:
206
208
  raise FileNotFoundError(f)
207
209
  return img, self._load_labels(f) # cls, cx, cy, w, h (normalized)
@@ -22,6 +22,8 @@ from pathlib import Path
22
22
  import cv2
23
23
  import numpy as np
24
24
 
25
+ from easydetect.utils.imageio import imread # any path, Hangul included
26
+
25
27
  from ..pose import COCO, INPUT, KeypointSpec, box_to_crop, crop, crop_matrix
26
28
  from .augment import photometric
27
29
 
@@ -68,7 +70,7 @@ def _image_size(path: Path) -> tuple[int, int]:
68
70
  with Image.open(path) as im:
69
71
  return im.size
70
72
  except Exception: # noqa: BLE001 — any reader trouble: decode it
71
- img = cv2.imread(str(path))
73
+ img = imread(str(path))
72
74
  if img is None:
73
75
  raise FileNotFoundError(path) from None
74
76
  return img.shape[1], img.shape[0]
@@ -178,7 +180,7 @@ class KeypointDataset:
178
180
 
179
181
  def __getitem__(self, i: int):
180
182
  image_id, ann = self.items[i]
181
- img = cv2.imread(str(self.images[image_id]))
183
+ img = imread(str(self.images[image_id]))
182
184
  if img is None:
183
185
  raise FileNotFoundError(self.images[image_id])
184
186
  kpts = np.asarray(ann["keypoints"], np.float64).reshape(-1, 3)
@@ -9,6 +9,8 @@ import threading
9
9
  import warnings
10
10
  from pathlib import Path
11
11
 
12
+ from easydetect.utils.imageio import imread # any path, Hangul included
13
+
12
14
  #: torch.onnx keeps global state while exporting ("in_onnx_export"), so two
13
15
  #: exports at once fail an assertion deep inside it. Serialise them — the
14
16
  #: whole export, names and conversion included, so that two threads exporting
@@ -168,7 +170,6 @@ def _int8_agreement(fp32, int8, images, imgsz) -> tuple[int, int]:
168
170
  """How many of the FP32 model's boxes (conf 0.5) the INT8 one finds again
169
171
  (same class, IoU 0.5) on some of the calibration pictures: NNCF quantizes
170
172
  without checking, and a model that loses objects should say so."""
171
- import cv2
172
173
  import numpy as np
173
174
  import openvino as ov
174
175
 
@@ -181,7 +182,7 @@ def _int8_agreement(fp32, int8, images, imgsz) -> tuple[int, int]:
181
182
  step = max(len(images) // AGREEMENT_PICTURES, 1)
182
183
  found = total = 0
183
184
  for path in images[::step][:AGREEMENT_PICTURES]:
184
- img = cv2.imread(str(path))
185
+ img = imread(str(path))
185
186
  if img is None:
186
187
  continue
187
188
  x = preprocess_image(img, imgsz)
@@ -202,7 +203,6 @@ def _int8_agreement(fp32, int8, images, imgsz) -> tuple[int, int]:
202
203
 
203
204
  def _quantize(model, images, imgsz):
204
205
  """8-bit weights and activations, calibrated on ``images`` (paths)."""
205
- import cv2
206
206
  import nncf
207
207
 
208
208
  from .predictor import preprocess_image
@@ -212,7 +212,7 @@ def _quantize(model, images, imgsz):
212
212
  raise ValueError("int8 needs pictures to calibrate on: give data=")
213
213
 
214
214
  def transform(path):
215
- img = cv2.imread(path)
215
+ img = imread(path)
216
216
  if img is None:
217
217
  raise ValueError(f"cannot read {path}")
218
218
  return preprocess_image(img, imgsz)
@@ -26,6 +26,8 @@ from typing import Any
26
26
 
27
27
  import numpy as np
28
28
 
29
+ from easydetect.utils.imageio import cv2_path, imwrite # any path, Hangul included
30
+
29
31
  from . import downloads
30
32
  from .errors import ModelNotFoundError
31
33
  from .metrics import DetMetrics
@@ -457,6 +459,11 @@ class Detector:
457
459
  self.names = {int(k): str(v) for k, v in ckpt.get("names", {}).items()}
458
460
  self.net = DFINENet(ckpt["variant"], ckpt["num_classes"], pretrained_backbone=False)
459
461
  self.net.load_state_dict(ckpt["model"])
462
+ # trained with fewer queries: runs (and fine-tunes again) with as many
463
+ if ckpt.get("queries") and ckpt["queries"] != self.net.decoder.num_queries:
464
+ self.net.trim(queries=int(ckpt["queries"]))
465
+ if ckpt.get("denoising") is not None:
466
+ self.net.decoder.num_denoising = int(ckpt["denoising"])
460
467
  self.net.eval()
461
468
  self.ir_path = None
462
469
  self.predictor = None
@@ -505,6 +512,8 @@ class Detector:
505
512
  seg_epochs: int = 20,
506
513
  seg_batch: int = 16,
507
514
  seg_lr: float = 1e-4,
515
+ queries: int | None = None,
516
+ denoising: int | None = None,
508
517
  **kwargs: Any,
509
518
  ) -> Path:
510
519
  """Train on a data.yaml dataset. Returns the path of ``best.pt``.
@@ -518,6 +527,12 @@ class Detector:
518
527
  ``on_epoch_end`` is called with a dict of the epoch's numbers — the
519
528
  same row that lands in ``results.csv``.
520
529
 
530
+ ``queries`` (of 300) and ``denoising`` (training-only queries, 100)
531
+ make every step cheaper; on a CPU most of a step is spent on them, not
532
+ on the picture. ``queries=100, denoising=30`` trains dfine-n about
533
+ twice as fast on a laptop and suits scenes with a handful of objects;
534
+ the model keeps ``queries`` (it runs faster too).
535
+
521
536
  A keypoint dataset (``kpt_shape`` in its data.yaml, Ultralytics' pose
522
537
  format) also trains a keypoint network after the detector: on crops of
523
538
  the labelled boxes, from the detector's backbone, for ``pose_epochs``
@@ -561,6 +576,10 @@ class Detector:
561
576
  elif self.net.num_classes != cfg["nc"]:
562
577
  self.net = self._reheaded_net(cfg["nc"])
563
578
 
579
+ if queries is not None:
580
+ self.net.trim(queries=int(queries))
581
+ if denoising is not None:
582
+ self.net.decoder.num_denoising = int(denoising)
564
583
  device = _torch_device(device)
565
584
  trainer = Trainer(
566
585
  self.net,
@@ -898,7 +917,7 @@ class _OutputWriter:
898
917
  out = self.save_dir / f"{stem}.jpg"
899
918
  if out.exists():
900
919
  out = self.save_dir / f"{stem}_{frame.index}.jpg"
901
- cv2.imwrite(str(out), img)
920
+ imwrite(str(out), img)
902
921
  return
903
922
  writer = self.videos.get(frame.path)
904
923
  if writer is None:
@@ -906,7 +925,7 @@ class _OutputWriter:
906
925
  h, w = img.shape[:2]
907
926
  # H.264 plays in browsers; OpenCV's pip wheels often lack it, so fall back
908
927
  for codec in ("avc1", "mp4v"):
909
- writer = cv2.VideoWriter(str(self.save_dir / f"{stem}.mp4"),
928
+ writer = cv2.VideoWriter(cv2_path(self.save_dir / f"{stem}.mp4"),
910
929
  cv2.VideoWriter_fourcc(*codec),
911
930
  frame.fps or 25.0, # keep the source's timing
912
931
  (w, h))
@@ -20,6 +20,8 @@ from typing import Any
20
20
 
21
21
  import numpy as np
22
22
 
23
+ from easydetect.utils.imageio import imwrite # any path, Hangul included
24
+
23
25
 
24
26
  class Boxes:
25
27
  """Detected boxes for one image.
@@ -223,14 +225,13 @@ class Results:
223
225
 
224
226
  def save(self, filename: str | Path | None = None) -> Path:
225
227
  """Write the annotated image; returns the path written."""
226
- import cv2
227
228
 
228
229
  if filename is None:
229
230
  stem = Path(self.path).stem or "image"
230
231
  filename = Path(f"{stem}.jpg")
231
232
  filename = Path(filename)
232
233
  filename.parent.mkdir(parents=True, exist_ok=True)
233
- cv2.imwrite(str(filename), self.plot())
234
+ imwrite(str(filename), self.plot())
234
235
  return filename
235
236
 
236
237
  def show(self, title: str | None = None) -> None:
@@ -25,6 +25,8 @@ from pathlib import Path
25
25
 
26
26
  import numpy as np
27
27
 
28
+ from easydetect.utils.imageio import imread # any path, Hangul included
29
+
28
30
  from .nn.sam_decoder import GRID, IMG_SIZE, LOW_RES, MASK_CHOICES
29
31
 
30
32
  BOX_JITTER = 0.05 # Gaussian sigma of each edge's shift, as a share of the box's side
@@ -103,13 +105,12 @@ class EmbeddingCache:
103
105
  def file(self, path: Path) -> tuple[Path, tuple[int, int]]:
104
106
  """``(the .npy file of the picture's embedding, (height, width))``,
105
107
  encoding the picture first if it has not been."""
106
- import cv2
107
108
 
108
109
  file = self._key(path)
109
110
  meta = file.with_suffix(".json")
110
111
  if file.exists() and meta.exists():
111
112
  return file, tuple(json.loads(meta.read_text()))
112
- img = cv2.imread(str(path))
113
+ img = imread(str(path))
113
114
  if img is None:
114
115
  raise FileNotFoundError(path)
115
116
  tensor, _ = self.segmenter.preprocess(img)
@@ -33,6 +33,8 @@ from typing import Any
33
33
 
34
34
  import numpy as np
35
35
 
36
+ from easydetect.utils.imageio import cv2_path, imread # any path, Hangul included
37
+
36
38
  IMG_FORMATS = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff", ".dng", ".pfm"}
37
39
  VID_FORMATS = {".mp4", ".avi", ".mov", ".mkv", ".mpg", ".mpeg", ".m4v", ".wmv", ".webm", ".gif"}
38
40
  _URL_RE = re.compile(r"^(https?|rtsp|rtmp|tcp|udp)://", re.IGNORECASE)
@@ -252,12 +254,12 @@ class SourceLoader:
252
254
  elif kind == "url-image":
253
255
  yield Frame(_read_url_image(payload), payload, i, self.total, "image")
254
256
  elif kind == "image":
255
- img = cv2.imread(str(payload))
257
+ img = imread(str(payload))
256
258
  if img is None:
257
259
  raise ValueError(f"could not read image: {payload}")
258
260
  yield Frame(img, payload, i, self.total, "image")
259
261
  else: # video file, webcam index, or network stream
260
- cap = cv2.VideoCapture(payload if isinstance(payload, int) else str(payload))
262
+ cap = cv2.VideoCapture(payload if isinstance(payload, int) else cv2_path(payload))
261
263
  if not cap.isOpened():
262
264
  raise ValueError(f"could not open {payload!r}")
263
265
  frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) if kind == "video" else 0
@@ -436,6 +436,8 @@ class Trainer:
436
436
  # a resumed run started where its first attempt did
437
437
  "start": old.get("start") or self.origin,
438
438
  "imgsz": self.imgsz,
439
+ "queries": self.net.decoder.num_queries,
440
+ "denoising": self.net.decoder.num_denoising,
439
441
  "batch": self.batch,
440
442
  "epochs": self.epochs,
441
443
  "patience": self.patience,
@@ -736,6 +738,8 @@ class Trainer:
736
738
  "scaler": self.scaler.state_dict() if self.amp else None,
737
739
  "variant": net.variant,
738
740
  "num_classes": net.num_classes,
741
+ "queries": net.decoder.num_queries,
742
+ "denoising": net.decoder.num_denoising,
739
743
  "names": self.cfg["names"],
740
744
  "epoch": epoch,
741
745
  "best": self.best,
@@ -0,0 +1,64 @@
1
+ # Apache-2.0
2
+ """Pictures in and out by any path, Hangul included.
3
+
4
+ OpenCV's ``imread``/``imwrite`` take the path as bytes in the system code
5
+ page; on Windows a path with characters outside it (a folder called
6
+ ``바탕 화면``, a OneDrive in Korean) reads as nothing. Reading the file's
7
+ bytes with Python and decoding them in memory works for every path.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ import cv2
15
+ import numpy as np
16
+
17
+
18
+ def imread(path, flags: int = cv2.IMREAD_COLOR):
19
+ """``cv2.imread`` for any path: the picture, or ``None`` when it cannot be read."""
20
+ try:
21
+ data = np.fromfile(str(path), dtype=np.uint8)
22
+ except OSError:
23
+ return None
24
+ if data.size == 0:
25
+ return None
26
+ return cv2.imdecode(data, flags)
27
+
28
+
29
+ def imwrite(path, img, params=None) -> bool:
30
+ """``cv2.imwrite`` for any path; the format follows the file's extension."""
31
+ ext = Path(str(path)).suffix or ".jpg"
32
+ ok, buf = cv2.imencode(ext, img, params or [])
33
+ if not ok:
34
+ return False
35
+ buf.tofile(str(path))
36
+ return True
37
+
38
+
39
+ def cv2_path(path) -> str:
40
+ """A path OpenCV's video reader and writer can open. On Windows a path
41
+ outside the code page becomes its short (8.3) form, which is plain ASCII;
42
+ elsewhere, or when it is ASCII already, the path as it is. A file that
43
+ does not exist yet (a video about to be written) keeps its name and gets
44
+ its folder's short form."""
45
+ text = str(path)
46
+ import os
47
+ import sys
48
+
49
+ if sys.platform != "win32" or text.isascii():
50
+ return text
51
+ import ctypes
52
+
53
+ def short(p: str) -> str:
54
+ size = ctypes.windll.kernel32.GetShortPathNameW(p, None, 0)
55
+ if not size:
56
+ return p
57
+ buf = ctypes.create_unicode_buffer(size)
58
+ ctypes.windll.kernel32.GetShortPathNameW(p, buf, size)
59
+ return buf.value
60
+
61
+ if os.path.exists(text):
62
+ return short(text)
63
+ folder, name = os.path.split(text)
64
+ return os.path.join(short(folder), name)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "easydetect"
7
- version = "0.7.2"
7
+ version = "0.7.4"
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"
@@ -236,3 +236,37 @@ def test_matching_all_layers_at_once_gives_the_same_loss():
236
236
  apart, _ = crit(out, targets)
237
237
  assert calls and calls[0] > 2 # the last layer, the others, the encoder
238
238
  assert torch.allclose(together, apart)
239
+
240
+
241
+ def test_pictures_in_a_hangul_folder_read_and_write(tmp_path):
242
+ """Windows' OpenCV cannot open such a path itself (바탕 화면, a Korean OneDrive)."""
243
+ import numpy as np
244
+
245
+ from easydetect.utils.imageio import cv2_path, imread, imwrite
246
+
247
+ folder = tmp_path / "바탕 화면" / "사진"
248
+ folder.mkdir(parents=True)
249
+ img = np.full((20, 30, 3), 120, np.uint8)
250
+ assert imwrite(folder / "고양이.png", img)
251
+ back = imread(folder / "고양이.png")
252
+ assert back is not None and back.shape == img.shape and (back == img).all()
253
+ assert imread(folder / "없음.jpg") is None
254
+ assert imread(folder / "고양이.png").dtype == np.uint8
255
+ assert cv2_path(folder / "a.mp4").endswith("a.mp4")
256
+
257
+
258
+ @needs_torch
259
+ def test_a_dataset_in_a_hangul_folder_trains(dataset, tmp_path):
260
+ import shutil
261
+
262
+ import yaml
263
+
264
+ from easydetect.data.dataset import DetDataset
265
+
266
+ root = tmp_path / "바탕 화면" / "데이터"
267
+ shutil.copytree(dataset.parent, root)
268
+ cfg = yaml.safe_load(dataset.read_text())
269
+ (root / "data.yaml").write_text(yaml.safe_dump(dict(cfg, path=str(root))), encoding="utf-8")
270
+ ds = DetDataset(root / "data.yaml", "train", 64, augment=True)
271
+ img, labels = ds[0]
272
+ assert img.shape[-2:] == (64, 64)
@@ -114,9 +114,11 @@ def pictures(tmp_path):
114
114
 
115
115
 
116
116
  def test_pictures_are_read_ahead_of_the_model(pictures, monkeypatch):
117
+ from easydetect import sources
118
+
117
119
  read = []
118
- real = cv2.imread
119
- monkeypatch.setattr(cv2, "imread", lambda p, *a: read.append(p) or real(p, *a))
120
+ real = sources.imread
121
+ monkeypatch.setattr(sources, "imread", lambda p, *a: read.append(p) or real(p, *a))
120
122
  frames = iter(SourceLoader(pictures))
121
123
  first = next(frames)
122
124
  deadline = time.time() + 3
@@ -477,3 +477,20 @@ def test_multiscale_mosaic_and_mixup_train_and_are_recorded(dataset, tmp_path, m
477
477
  assert any("mixup" in line for line in setup["augment"])
478
478
  assert any("random size, 32–64" in line for line in setup["augment"])
479
479
  assert (32, 32) in sizes # the batches went through at the drawn size
480
+
481
+
482
+ def test_fewer_queries_train_and_stay_with_the_model(dataset, tmp_path, monkeypatch):
483
+ monkeypatch.setattr("easydetect.nn.hgnetv2.HGNetv2.load_imagenet", lambda self, name, url: None)
484
+ model = Detector("dfine-n", verbose=False, pretrained=False)
485
+ best = model.train(data=str(dataset), project=str(tmp_path), epochs=1, imgsz=64, batch=2,
486
+ workers=0, device="cpu", val=False, amp=False, queries=40, denoising=10)
487
+ again = Detector(str(best), verbose=False)
488
+ again._ensure_net()
489
+ assert again.net.decoder.num_queries == 40 and again.net.decoder.num_denoising == 10
490
+ # at 160 px the encoder has more than 300 candidates: only the trim limits it
491
+ onnx = again.export(format="onnx", imgsz=160, out_dir=tmp_path / "x", verbose=False)
492
+ import onnxruntime as ort
493
+
494
+ out = ort.InferenceSession(str(onnx)).run(None, {"images": np.zeros((1, 3, 160, 160),
495
+ np.float32)})
496
+ assert any(o.shape[1] == 40 for o in out if o.ndim == 3)
@@ -141,14 +141,17 @@ def train_dfine(data: Path, out: Path, args) -> tuple[Path, float]:
141
141
  # in a process of its own, as the yolo command is: a crash in what training
142
142
  # leaves behind (loader workers, CUDA) then takes only that process down
143
143
  started = time.time()
144
- code = ("import sys; from easydetect import Detector; "
144
+ extra = {k: v for k, v in (("queries", args.queries), ("denoising", args.denoising),
145
+ ("freeze", args.freeze)) if v is not None}
146
+ code = ("import json, sys; from easydetect import Detector; "
145
147
  "print('BEST', Detector(sys.argv[1], verbose=False).train(data=sys.argv[2], "
146
148
  "epochs=int(sys.argv[3]), imgsz=int(sys.argv[4]), batch=int(sys.argv[5]), "
147
149
  "patience=int(sys.argv[3]), project=sys.argv[6], name=sys.argv[1], "
148
- "workers=int(sys.argv[7])))")
150
+ "workers=int(sys.argv[7]), **json.loads(sys.argv[8])))")
149
151
  best = None
150
152
  with subprocess.Popen([sys.executable, "-c", code, args.dfine, str(data), str(args.epochs),
151
- str(args.imgsz), str(args.batch), str(out), str(args.workers)],
153
+ str(args.imgsz), str(args.batch), str(out), str(args.workers),
154
+ json.dumps(extra)],
152
155
  env=UNBUFFERED, stdout=subprocess.PIPE, text=True) as proc:
153
156
  for line in proc.stdout: # passed on as it comes
154
157
  print(line, end="", flush=True)
@@ -276,6 +279,9 @@ def main():
276
279
  parser.add_argument("--yolo", default="yolo26s.pt")
277
280
  parser.add_argument("--dfine", default="dfine-s")
278
281
  parser.add_argument("--device", default="CPU", help="OpenVINO device for scoring")
282
+ parser.add_argument("--queries", type=int, help="D-FINE decoder queries (of 300)")
283
+ parser.add_argument("--denoising", type=int, help="D-FINE denoising queries (100)")
284
+ parser.add_argument("--freeze", help="D-FINE parts not trained (backbone)")
279
285
  parser.add_argument("--rescore", action="store_true",
280
286
  help="score finished datasets again (their trained models are reused)")
281
287
  args = parser.parse_args()
@@ -35,6 +35,9 @@ def main():
35
35
  parser.add_argument("--steps", type=int, default=40)
36
36
  parser.add_argument("--warmup", type=int, default=10)
37
37
  parser.add_argument("--no-amp", action="store_true")
38
+ parser.add_argument("--freeze", default=None, help="backbone, encoder, …: parts not trained")
39
+ parser.add_argument("--queries", type=int, help="decoder queries while training (of 300)")
40
+ parser.add_argument("--denoising", type=int, help="denoising queries while training (100)")
38
41
  parser.add_argument("--trace", action="store_true", help="also PyTorch's operator table")
39
42
  parser.add_argument("--syncs", action="store_true",
40
43
  help="list the lines of easydetect where a step waits for the GPU")
@@ -44,9 +47,14 @@ def main():
44
47
 
45
48
  nc = load_data_yaml(args.data)["nc"]
46
49
  net = DFINENet(args.model, nc)
50
+ if args.queries:
51
+ net.trim(queries=args.queries)
52
+ if args.denoising is not None:
53
+ net.decoder.num_denoising = args.denoising
47
54
  with tempfile.TemporaryDirectory() as tmp:
48
55
  tr = Trainer(net, args.data, epochs=1, imgsz=args.imgsz, batch=args.batch,
49
- workers=args.workers, project=tmp, amp=not args.no_amp)
56
+ workers=args.workers, project=tmp, amp=not args.no_amp,
57
+ freeze=args.freeze)
50
58
  ds = DetDataset(args.data, "train", args.imgsz, augment=True)
51
59
  dl = tr._loader(ds)
52
60
  net = tr.net.to(tr.device).train()
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