easydetect 0.2.0__tar.gz → 0.2.2__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 (62) hide show
  1. {easydetect-0.2.0 → easydetect-0.2.2}/PKG-INFO +14 -11
  2. {easydetect-0.2.0 → easydetect-0.2.2}/README.md +13 -10
  3. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/__init__.py +1 -1
  4. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/data/dataset.py +3 -3
  5. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/data/labels.py +2 -2
  6. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/exporter.py +19 -1
  7. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/model.py +4 -2
  8. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/predictor.py +75 -1
  9. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/trainer.py +25 -2
  10. {easydetect-0.2.0 → easydetect-0.2.2}/pyproject.toml +2 -2
  11. easydetect-0.2.2/tests/test_lab_api.py +31 -0
  12. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_onnxruntime.py +27 -0
  13. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_train_export.py +27 -0
  14. {easydetect-0.2.0 → easydetect-0.2.2}/tools/hub_README.md +1 -1
  15. {easydetect-0.2.0 → easydetect-0.2.2}/tools/make_toyset.py +1 -1
  16. easydetect-0.2.0/platform/README.md +0 -199
  17. easydetect-0.2.0/platform/tests/conftest.py +0 -9
  18. easydetect-0.2.0/platform/tests/test_app.py +0 -1222
  19. easydetect-0.2.0/platform/tests/test_labeling.py +0 -94
  20. {easydetect-0.2.0 → easydetect-0.2.2}/.gitignore +0 -0
  21. {easydetect-0.2.0 → easydetect-0.2.2}/LICENSE +0 -0
  22. {easydetect-0.2.0 → easydetect-0.2.2}/NOTICE +0 -0
  23. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/cli.py +0 -0
  24. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/data/__init__.py +0 -0
  25. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/data/augment.py +0 -0
  26. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/downloads.py +0 -0
  27. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/errors.py +0 -0
  28. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/metrics.py +0 -0
  29. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/__init__.py +0 -0
  30. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/decoder.py +0 -0
  31. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/denoising.py +0 -0
  32. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/dfine_net.py +0 -0
  33. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/encoder.py +0 -0
  34. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/hgnetv2.py +0 -0
  35. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/ops.py +0 -0
  36. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/plotting.py +0 -0
  37. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/results.py +0 -0
  38. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/sources.py +0 -0
  39. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/tracker.py +0 -0
  40. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/utils/__init__.py +0 -0
  41. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/utils/loss.py +0 -0
  42. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/utils/ops.py +0 -0
  43. {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/validator.py +0 -0
  44. {easydetect-0.2.0 → easydetect-0.2.2}/tests/__init__.py +0 -0
  45. {easydetect-0.2.0 → easydetect-0.2.2}/tests/conftest.py +0 -0
  46. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_augment.py +0 -0
  47. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_cli.py +0 -0
  48. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_coco2yolo.py +0 -0
  49. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_dataset.py +0 -0
  50. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_downloads.py +0 -0
  51. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_model.py +0 -0
  52. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_nn.py +0 -0
  53. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_predictor.py +0 -0
  54. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_results.py +0 -0
  55. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_sources.py +0 -0
  56. {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_tracker.py +0 -0
  57. {easydetect-0.2.0 → easydetect-0.2.2}/tools/bench.py +0 -0
  58. {easydetect-0.2.0 → easydetect-0.2.2}/tools/build_mirror.py +0 -0
  59. {easydetect-0.2.0 → easydetect-0.2.2}/tools/coco2yolo.py +0 -0
  60. {easydetect-0.2.0 → easydetect-0.2.2}/tools/compare_yolo.py +0 -0
  61. {easydetect-0.2.0 → easydetect-0.2.2}/tools/convert_dfine.py +0 -0
  62. {easydetect-0.2.0 → easydetect-0.2.2}/tools/make_demo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: easydetect
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -145,18 +145,21 @@ faster on a small set, and `resume=True` picks a killed run back up.
145
145
 
146
146
  ## Label, train and watch it in a browser
147
147
 
148
- `platform/` is a web app on top of this package: drop images in, label them (the
149
- model drafts the boxes, you correct them), queue a training run, watch the curve,
150
- then run the result over a folder, a video or your webcam — on one machine, with
151
- nothing leaving it. A finished run hands you copy-ready code and a Hugging Face
152
- folder whose model card is written from the run.
148
+ [easydetect lab](https://github.com/themakerrobot/easydetect-lab) is a web app on top of this package, in its own
149
+ repository: drop images in, label them (the model drafts the boxes, you correct
150
+ them), queue a training run, watch the curve, then run the result over a folder,
151
+ a video or your webcam — on one machine, with nothing leaving it. A finished run
152
+ hands you copy-ready code and a Hugging Face folder whose model card is written
153
+ from the run.
153
154
 
154
155
  ```bash
155
- pip install -r platform/requirements.txt
156
- python platform/run.py # http://<this machine>:8080
156
+ git clone https://github.com/themakerrobot/easydetect-lab
157
+ cd easydetect-lab
158
+ pip install -r requirements.txt # easydetect[train] from PyPI, and the web server
159
+ python run.py # http://<this machine>:8080
157
160
  ```
158
161
 
159
- ![the platform](https://raw.githubusercontent.com/themakerrobot/easydetect/main/docs/assets/platform.jpg)
162
+ ![easydetect lab](https://raw.githubusercontent.com/themakerrobot/easydetect-lab/main/docs/lab.jpg)
160
163
 
161
164
  ## Models
162
165
 
@@ -226,7 +229,7 @@ easydetect export model=best.pt format=openvino half=true
226
229
 
227
230
  * [Using the model](https://github.com/themakerrobot/easydetect/blob/main/docs/usage.md) — sources, results, tracking, saving
228
231
  * [Training](https://github.com/themakerrobot/easydetect/blob/main/docs/training.md) — datasets, validation, export, CLI
229
- * [Platform](https://github.com/themakerrobot/easydetect/blob/main/platform/README.md) — the browser app: labelling, jobs, sharing
232
+ * [easydetect lab](https://github.com/themakerrobot/easydetect-lab) — the browser app: labelling, jobs, sharing
230
233
  * [Weights](https://github.com/themakerrobot/easydetect/blob/main/docs/weights.md) — the mirror, building it, offline use
231
234
  * [Performance](https://github.com/themakerrobot/easydetect/blob/main/docs/performance.md) — measuring speed and improving it
232
235
  * [Design](https://github.com/themakerrobot/easydetect/blob/main/docs/design.md) — architecture and provenance
@@ -259,7 +262,7 @@ Runtime이 함께 들어가고 PyTorch는 없습니다. `pip install "easydetect
259
262
  `Detector("dfine-s", backend="onnxruntime")`로 ONNX Runtime(모든 CPU, 라즈베리파이
260
263
  포함)을 쓸 수 있습니다. 두 엔진의 결과는 같습니다.
261
264
 
262
- 라벨링부터 학습·추론까지 브라우저로 하려면 `python platform/run.py`. 가중치 캐시는
265
+ 라벨링부터 학습·추론까지 브라우저로 하려면 [easydetect lab](https://github.com/themakerrobot/easydetect-lab). 가중치 캐시는
263
266
  `~/.easydetect/`, 사내 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
264
267
 
265
268
  ## Credits
@@ -103,18 +103,21 @@ faster on a small set, and `resume=True` picks a killed run back up.
103
103
 
104
104
  ## Label, train and watch it in a browser
105
105
 
106
- `platform/` is a web app on top of this package: drop images in, label them (the
107
- model drafts the boxes, you correct them), queue a training run, watch the curve,
108
- then run the result over a folder, a video or your webcam — on one machine, with
109
- nothing leaving it. A finished run hands you copy-ready code and a Hugging Face
110
- folder whose model card is written from the run.
106
+ [easydetect lab](https://github.com/themakerrobot/easydetect-lab) is a web app on top of this package, in its own
107
+ repository: drop images in, label them (the model drafts the boxes, you correct
108
+ them), queue a training run, watch the curve, then run the result over a folder,
109
+ a video or your webcam — on one machine, with nothing leaving it. A finished run
110
+ hands you copy-ready code and a Hugging Face folder whose model card is written
111
+ from the run.
111
112
 
112
113
  ```bash
113
- pip install -r platform/requirements.txt
114
- python platform/run.py # http://<this machine>:8080
114
+ git clone https://github.com/themakerrobot/easydetect-lab
115
+ cd easydetect-lab
116
+ pip install -r requirements.txt # easydetect[train] from PyPI, and the web server
117
+ python run.py # http://<this machine>:8080
115
118
  ```
116
119
 
117
- ![the platform](https://raw.githubusercontent.com/themakerrobot/easydetect/main/docs/assets/platform.jpg)
120
+ ![easydetect lab](https://raw.githubusercontent.com/themakerrobot/easydetect-lab/main/docs/lab.jpg)
118
121
 
119
122
  ## Models
120
123
 
@@ -184,7 +187,7 @@ easydetect export model=best.pt format=openvino half=true
184
187
 
185
188
  * [Using the model](https://github.com/themakerrobot/easydetect/blob/main/docs/usage.md) — sources, results, tracking, saving
186
189
  * [Training](https://github.com/themakerrobot/easydetect/blob/main/docs/training.md) — datasets, validation, export, CLI
187
- * [Platform](https://github.com/themakerrobot/easydetect/blob/main/platform/README.md) — the browser app: labelling, jobs, sharing
190
+ * [easydetect lab](https://github.com/themakerrobot/easydetect-lab) — the browser app: labelling, jobs, sharing
188
191
  * [Weights](https://github.com/themakerrobot/easydetect/blob/main/docs/weights.md) — the mirror, building it, offline use
189
192
  * [Performance](https://github.com/themakerrobot/easydetect/blob/main/docs/performance.md) — measuring speed and improving it
190
193
  * [Design](https://github.com/themakerrobot/easydetect/blob/main/docs/design.md) — architecture and provenance
@@ -217,7 +220,7 @@ Runtime이 함께 들어가고 PyTorch는 없습니다. `pip install "easydetect
217
220
  `Detector("dfine-s", backend="onnxruntime")`로 ONNX Runtime(모든 CPU, 라즈베리파이
218
221
  포함)을 쓸 수 있습니다. 두 엔진의 결과는 같습니다.
219
222
 
220
- 라벨링부터 학습·추론까지 브라우저로 하려면 `python platform/run.py`. 가중치 캐시는
223
+ 라벨링부터 학습·추론까지 브라우저로 하려면 [easydetect lab](https://github.com/themakerrobot/easydetect-lab). 가중치 캐시는
221
224
  `~/.easydetect/`, 사내 미러는 `EASYDETECT_ASSETS_URL` 환경변수로 지정합니다.
222
225
 
223
226
  ## Credits
@@ -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.0"
27
+ __version__ = "0.2.2"
28
28
 
29
29
  from .errors import DownloadError, EasyDetectError, ModelNotFoundError
30
30
  from .metrics import BoxMetrics, DetMetrics
@@ -72,10 +72,10 @@ def _locate(root: Path, yaml_dir: Path, spec: str) -> Path:
72
72
  )
73
73
 
74
74
 
75
- def _list_images(root: Path, spec, yaml_dir: Path | None = None):
75
+ def list_images(root: Path, spec, yaml_dir: Path | None = None):
76
76
  """Images for one split: a folder, a .txt list, or a list of either."""
77
77
  if isinstance(spec, (list, tuple)):
78
- files = [f for s in spec for f in _list_images(root, s, yaml_dir)]
78
+ files = [f for s in spec for f in list_images(root, s, yaml_dir)]
79
79
  return list(dict.fromkeys(files))
80
80
  p = _locate(root, yaml_dir or root, str(spec))
81
81
  if p.is_dir():
@@ -109,7 +109,7 @@ class DetDataset(Dataset):
109
109
  f"{data_yaml} has no '{split}:' entry. Add one — it may point at the "
110
110
  f"training images, but then mAP only measures memorisation."
111
111
  )
112
- self.files = _list_images(cfg["root"], cfg[split], cfg["yaml_dir"])
112
+ self.files = list_images(cfg["root"], cfg[split], cfg["yaml_dir"])
113
113
  if not self.files:
114
114
  raise FileNotFoundError(f"no images for split '{split}'")
115
115
 
@@ -1,10 +1,10 @@
1
1
  # Apache-2.0
2
2
  """Where a label lives, and how one line of it reads — shared by everything.
3
3
 
4
- The trainer reads labels, the platform writes them, the exporter packs them.
4
+ The trainer reads labels, easydetect lab writes them, the exporter packs them.
5
5
  If any two of those disagree on where ``a.jpg``'s boxes are, training runs on
6
6
  nothing and nobody is told. So there is one rule, here, and no torch import:
7
- the platform uses it at startup.
7
+ the lab uses it at startup.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -62,12 +62,30 @@ def export_onnx(net, names, imgsz=640, out_dir=".", fname="easydetect", half=Fal
62
62
  opset_version=17,
63
63
  dynamo=False,
64
64
  )
65
- _write_labels(out_dir, fname, names)
65
+ table = _write_labels(out_dir, fname, names)
66
+ _embed_names(onnx_path, table)
66
67
  if verbose:
67
68
  print(f"[easydetect] exported: {onnx_path}")
68
69
  return onnx_path
69
70
 
70
71
 
72
+ def _embed_names(onnx_path: Path, table: dict[int, str]) -> None:
73
+ """Keep the class names inside the .onnx too, so the file works on its own —
74
+ one download from a Hugging Face page, no labels.txt to fetch beside it."""
75
+ import onnx
76
+
77
+ from .predictor import ONNX_NAMES_KEY
78
+
79
+ model = onnx.load(str(onnx_path))
80
+ kept = [p for p in model.metadata_props if p.key != ONNX_NAMES_KEY]
81
+ del model.metadata_props[:]
82
+ model.metadata_props.extend(kept)
83
+ entry = model.metadata_props.add()
84
+ entry.key = ONNX_NAMES_KEY
85
+ entry.value = json.dumps({str(k): v for k, v in table.items()}, ensure_ascii=False)
86
+ onnx.save(model, str(onnx_path))
87
+
88
+
71
89
  def export_openvino(net, names, imgsz=640, out_dir=".", fname="easydetect", half=False,
72
90
  verbose=True):
73
91
  """Write ``<out_dir>/<fname>.xml`` (+ .bin, + labels). Returns the .xml path."""
@@ -349,7 +349,7 @@ class Detector:
349
349
  name: str = "train",
350
350
  resume: bool = False,
351
351
  patience: int = 50,
352
- lr0: float = 1e-4,
352
+ lr0: float | None = None,
353
353
  seed: int = 0,
354
354
  val: bool = True,
355
355
  amp: bool = True,
@@ -362,7 +362,9 @@ class Detector:
362
362
  """Train on a data.yaml dataset. Returns the path of ``best.pt``.
363
363
 
364
364
  ``freeze="backbone"`` trains about twice as fast on CPU and helps when
365
- the dataset is small. ``augment=True`` trains on zoomed-out, cropped
365
+ the dataset is small. ``lr0=None`` sets the learning rate from the
366
+ batch size, ``1e-4 × √(batch / 4)``, so a bigger batch is not a run
367
+ that learned less; a number sets it outright. ``augment=True`` trains on zoomed-out, cropped
366
368
  and colour-jittered pictures (D-FINE's recipe) and on plain ones for
367
369
  the last tenth of the epochs; ``False`` keeps only the flip.
368
370
  ``on_epoch_end`` is called with a dict of the epoch's numbers — the
@@ -37,13 +37,87 @@ def _looks_like_logits(scores: np.ndarray) -> bool:
37
37
  return bool(scores.size) and (scores.min() < 0.0 or scores.max() > 1.0)
38
38
 
39
39
 
40
+ #: The ONNX metadata key easydetect keeps the class names under.
41
+ ONNX_NAMES_KEY = "easydetect.names"
42
+
43
+
44
+ def _varint(f) -> int | None:
45
+ shift = value = 0
46
+ while True:
47
+ byte = f.read(1)
48
+ if not byte:
49
+ return None
50
+ value |= (byte[0] & 0x7F) << shift
51
+ if byte[0] < 0x80:
52
+ return value
53
+ shift += 7
54
+
55
+
56
+ def onnx_metadata(path: Path) -> dict[str, str]:
57
+ """The ``metadata_props`` of an ONNX file, read without the ``onnx`` package.
58
+
59
+ ModelProto keeps them as field 14, each a (key = 1, value = 2) pair. Only
60
+ the top level of the file is walked; the graph and its weights are skipped
61
+ over with a seek, so a large model costs no more than a small one.
62
+ """
63
+ props: dict[str, str] = {}
64
+ with open(path, "rb") as f:
65
+ while (tag := _varint(f)) is not None:
66
+ field, wire = tag >> 3, tag & 7
67
+ if wire == 0:
68
+ _varint(f)
69
+ elif wire == 1:
70
+ f.seek(8, 1)
71
+ elif wire == 5:
72
+ f.seek(4, 1)
73
+ elif wire == 2:
74
+ size = _varint(f) or 0
75
+ if field != 14:
76
+ f.seek(size, 1)
77
+ continue
78
+ entry, key, value, i = f.read(size), "", "", 0
79
+ while i < len(entry):
80
+ sub_tag, n = entry[i], 0
81
+ i += 1
82
+ shift = 0
83
+ while True: # the length, a varint
84
+ n |= (entry[i] & 0x7F) << shift
85
+ i += 1
86
+ if entry[i - 1] < 0x80:
87
+ break
88
+ shift += 7
89
+ text = entry[i:i + n].decode("utf-8", "replace")
90
+ i += n
91
+ if sub_tag >> 3 == 1:
92
+ key = text
93
+ elif sub_tag >> 3 == 2:
94
+ value = text
95
+ props[key] = value
96
+ else: # not an ONNX file after all
97
+ break
98
+ return props
99
+
100
+
40
101
  def read_names(model_path: Path) -> dict[int, str]:
41
- """Class names from ``labels.txt`` or ``<stem>.names.json`` beside the model."""
102
+ """Class names: ``<stem>.names.json`` beside the model, then the names an
103
+ ``.onnx`` carries inside it, then ``labels.txt`` in its folder.
104
+
105
+ The names inside the file come before a ``labels.txt`` because they belong
106
+ to that file: a lone ``best.onnx`` downloaded into a folder that holds
107
+ another model's ``labels.txt`` still names its own classes.
108
+ """
42
109
  model_path = Path(model_path)
43
110
  sidecar = model_path.with_suffix(".names.json")
44
111
  if sidecar.exists():
45
112
  table = json.loads(sidecar.read_text(encoding="utf-8"))
46
113
  return {int(k): str(v) for k, v in table.items()}
114
+ if model_path.suffix.lower() == ".onnx" and model_path.is_file():
115
+ try:
116
+ inside = onnx_metadata(model_path).get(ONNX_NAMES_KEY)
117
+ if inside:
118
+ return {int(k): str(v) for k, v in json.loads(inside).items()}
119
+ except (OSError, ValueError, IndexError):
120
+ pass
47
121
  labels = model_path.parent / "labels.txt"
48
122
  if labels.exists():
49
123
  lines = [ln.strip() for ln in labels.read_text(encoding="utf-8").splitlines()]
@@ -1,7 +1,8 @@
1
1
  # Apache-2.0
2
2
  """Training loop (PyTorch). Kept deliberately small and readable.
3
3
 
4
- AdamW with a lower LR on the backbone, linear warmup into a cosine decay, AMP
4
+ AdamW with a lower LR on the backbone, the LR set from the batch size unless
5
+ given, linear warmup into a cosine decay, AMP
5
6
  on CUDA, ``last.pt``/``best.pt`` after every epoch, early stop on ``patience``,
6
7
  and ``resume=True`` to pick a killed run back up where it stopped.
7
8
  """
@@ -31,6 +32,24 @@ def seed_everything(seed: int) -> None:
31
32
  torch.cuda.manual_seed_all(seed)
32
33
 
33
34
 
35
+ #: The learning rate a batch of 4 fine-tunes well at: D-FINE from its COCO
36
+ #: weights reached 0.765 mAP50-95 on a dataset where RT-DETR's own run gave 0.762.
37
+ REF_BATCH, REF_LR = 4, 1e-4
38
+
39
+
40
+ def auto_lr(batch: int) -> float:
41
+ """The learning rate for a batch size: ``1e-4 × √(batch / 4)``.
42
+
43
+ A bigger batch means fewer optimizer steps an epoch, and Adam moves each
44
+ weight by about the learning rate per step whatever the batch, so at one
45
+ fixed rate a batch of 32 ends the same epochs having learned an eighth as
46
+ much. The square root is the usual rule for Adam (four times the batch,
47
+ twice the rate); at 32 it gives 2.8e-4, beside the 2–2.5e-4 D-FINE's own
48
+ configs train with at their batch of 32.
49
+ """
50
+ return REF_LR * math.sqrt(max(int(batch), 1) / REF_BATCH)
51
+
52
+
34
53
  #: The backbone learns slower than the rest — D-FINE's per-size ratios.
35
54
  BACKBONE_LR_MULT = {"n": 0.5, "s": 0.5, "m": 0.1, "l": 0.05, "x": 0.01}
36
55
 
@@ -153,7 +172,7 @@ class Trainer:
153
172
  epochs=100,
154
173
  imgsz=640,
155
174
  batch=8,
156
- lr=1e-4,
175
+ lr=None,
157
176
  lr_backbone_mult=None,
158
177
  weight_decay=1e-4,
159
178
  warmup_epochs=1,
@@ -174,6 +193,8 @@ class Trainer:
174
193
  seed_everything(seed)
175
194
  self.augment = augment
176
195
  self.seed, self.freeze, self.origin = seed, freeze, origin
196
+ self.lr_auto = lr is None
197
+ lr = auto_lr(batch) if lr is None else float(lr)
177
198
  self.lr, self.lr_backbone_mult, self.weight_decay = lr, lr_backbone_mult, weight_decay
178
199
  self.net = net
179
200
  self.frozen = freeze_modules(net, freeze)
@@ -288,6 +309,8 @@ class Trainer:
288
309
  "optimizer": {
289
310
  "name": "AdamW",
290
311
  "lr": self.lr,
312
+ "lr_from": (f"batch {self.batch}: {REF_LR:g} × √({self.batch}/{REF_BATCH})"
313
+ if self.lr_auto else "set"),
291
314
  "lr_backbone": self.lr * self.lr_backbone_mult,
292
315
  "weight_decay": self.weight_decay,
293
316
  "schedule": "linear warmup, then cosine decay to 1%",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "easydetect"
7
- version = "0.2.0"
7
+ version = "0.2.2"
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"
@@ -84,5 +84,5 @@ ignore = ["B008", "E741"]
84
84
  "easydetect/utils/loss.py" = ["E501", "B905", "UP", "B006", "B007"]
85
85
 
86
86
  [tool.pytest.ini_options]
87
- testpaths = ["tests", "platform/tests"]
87
+ testpaths = ["tests"]
88
88
  filterwarnings = ["ignore::DeprecationWarning"]
@@ -0,0 +1,31 @@
1
+ # Apache-2.0
2
+ """The names easydetect lab imports from this package.
3
+
4
+ The lab lives in its own repository (themakerrobot/easydetect-lab) and runs on
5
+ the PyPI release, so renaming or moving one of these breaks it only after a
6
+ release. This test makes such a change visible here, before it ships.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+
13
+ import pytest
14
+
15
+ LAB_IMPORTS = {
16
+ "easydetect": ["Detector", "__version__"],
17
+ "easydetect.data.dataset": ["DetDataset", "list_images", "load_data_yaml"],
18
+ "easydetect.data.labels": ["label_path", "label_row_to_box"],
19
+ "easydetect.downloads": ["MODEL_NAMES", "assets_url"],
20
+ "easydetect.plotting": ["draw_boxes"],
21
+ "easydetect.results": ["Boxes"],
22
+ "easydetect.sources": ["SourceLoader"],
23
+ "easydetect.validator": ["validate_torch"],
24
+ }
25
+
26
+
27
+ @pytest.mark.parametrize("module", sorted(LAB_IMPORTS))
28
+ def test_the_lab_finds_what_it_imports(module):
29
+ mod = importlib.import_module(module)
30
+ missing = [name for name in LAB_IMPORTS[module] if not hasattr(mod, name)]
31
+ assert not missing, f"easydetect lab imports {missing} from {module}"
@@ -104,3 +104,30 @@ def test_a_named_model_downloads_the_onnx_for_onnxruntime(monkeypatch, tmp_path,
104
104
  monkeypatch.setattr(downloads, "_asset", fake_asset)
105
105
  assert downloads.download_onnx("D-FINE-S") == tmp_path / "dfine-s.onnx"
106
106
  assert fetched == ["dfine-s.onnx", "labels.txt"]
107
+
108
+
109
+ def test_an_onnx_carries_its_class_names_so_it_works_alone(tiny_ir, tmp_path):
110
+ """One file from a Hugging Face page: no labels.txt beside it, or someone else's."""
111
+ import shutil
112
+
113
+ lone = tmp_path / "downloads" / "best.onnx"
114
+ lone.parent.mkdir()
115
+ shutil.copy(tiny_ir.with_suffix(".onnx"), lone)
116
+ (lone.parent / "labels.txt").write_text("person\ncar\nbus\n") # another model's
117
+ for backend in ("openvino", "onnxruntime"):
118
+ model = Detector(str(lone), backend=backend, verbose=False)
119
+ assert model(draw(), conf=0.0)[0].names == {0: "can", 1: "bottle"}, backend
120
+
121
+
122
+ def test_the_names_are_read_without_the_onnx_package(tiny_ir, tmp_path):
123
+ onnx = pytest.importorskip("onnx")
124
+ from easydetect.predictor import ONNX_NAMES_KEY, onnx_metadata, read_names
125
+
126
+ path = tiny_ir.with_suffix(".onnx")
127
+ expected = {p.key: p.value for p in onnx.load(str(path)).metadata_props}
128
+ assert onnx_metadata(path) == expected and ONNX_NAMES_KEY in expected
129
+
130
+ # not an ONNX file at all: no names, and no crash
131
+ junk = tmp_path / "junk.onnx"
132
+ junk.write_bytes(b"\xff\xff\xff\xff\x0f not a model")
133
+ assert read_names(junk) == {}
@@ -217,6 +217,9 @@ def test_training_reports_every_epoch_to_a_callback_and_a_csv(dataset, tmp_path,
217
217
  assert setup["variant"] == "n" and setup["start"] == {"kind": "scratch"}
218
218
  assert setup["freeze"] == "backbone" and 0 < setup["trainable_params"] < setup["params"]
219
219
  assert setup["optimizer"]["name"] == "AdamW" and setup["amp"] is False
220
+ # no lr0: the rate came from the batch of 2, and the record says so
221
+ assert setup["optimizer"]["lr"] == pytest.approx(1e-4 * (2 / 4) ** 0.5)
222
+ assert setup["optimizer"]["lr_from"].startswith("batch 2:")
220
223
  train = setup["data"]["train"]
221
224
  assert train["images"] > 0 and sum(train["per_class"]) == train["boxes"] > 0
222
225
  assert setup["versions"]["torch"] and setup["device"] == "cpu" and setup["gpu"] is None
@@ -306,3 +309,27 @@ def test_pretrained_false_never_reaches_for_the_mirror(dataset, tmp_path, monkey
306
309
  data=str(dataset), project=str(tmp_path), epochs=1, imgsz=64, batch=2, workers=0,
307
310
  device="cpu", val=False, amp=False,
308
311
  ).exists()
312
+
313
+
314
+ def test_the_learning_rate_follows_the_batch_unless_it_is_given():
315
+ from easydetect.trainer import auto_lr
316
+
317
+ assert auto_lr(4) == pytest.approx(1e-4)
318
+ assert auto_lr(16) == pytest.approx(2e-4)
319
+ assert auto_lr(32) == pytest.approx(2.83e-4, rel=1e-3)
320
+ assert auto_lr(0) == auto_lr(1)
321
+
322
+
323
+ @needs_torch
324
+ def test_a_learning_rate_that_is_given_is_used_as_it_is(dataset, tmp_path, monkeypatch):
325
+ import json
326
+
327
+ monkeypatch.setattr("easydetect.nn.hgnetv2.HGNetv2.load_imagenet", lambda self, name, url: None)
328
+ model = Detector("dfine-n", verbose=False, pretrained=False)
329
+ best = model.train(data=str(dataset), project=str(tmp_path), epochs=1, imgsz=64, batch=2,
330
+ workers=0, device="cpu", val=False, amp=False, lr0=3e-4,
331
+ warmup_epochs=0, weight_decay=0.0, lr_backbone_mult=0.25)
332
+ opt = json.loads((best.parent.parent / "run.json").read_text())["optimizer"]
333
+ assert opt["lr"] == pytest.approx(3e-4) and opt["lr_from"] == "set"
334
+ assert opt["lr_backbone"] == pytest.approx(0.75e-4)
335
+ assert opt["warmup_epochs"] == 0 and opt["weight_decay"] == 0.0
@@ -105,7 +105,7 @@ model.export(format="openvino") # best.xml + best.bin + label
105
105
 
106
106
  `data.yaml` is the common images/ + labels/ layout — a Roboflow export works as
107
107
  it is. Or do it all in a browser: the
108
- [platform](https://github.com/themakerrobot/easydetect/tree/main/platform) collects
108
+ [easydetect lab](https://github.com/themakerrobot/easydetect-lab) collects
109
109
  and labels images, trains, shows the numbers, and writes the upload folder for
110
110
  `models/` with its README filled in from the run.
111
111
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  Every box is exact, shapes barely overlap, backgrounds vary, so a run that
9
9
  learns nothing here is broken rather than short of data. For trying the loop
10
- (train, val, export, predict, the platform), not for judging accuracy.
10
+ (train, val, export, predict, the lab), not for judging accuracy.
11
11
  """
12
12
  import pathlib
13
13
  import random