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.
- {easydetect-0.2.0 → easydetect-0.2.2}/PKG-INFO +14 -11
- {easydetect-0.2.0 → easydetect-0.2.2}/README.md +13 -10
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/__init__.py +1 -1
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/data/dataset.py +3 -3
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/data/labels.py +2 -2
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/exporter.py +19 -1
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/model.py +4 -2
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/predictor.py +75 -1
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/trainer.py +25 -2
- {easydetect-0.2.0 → easydetect-0.2.2}/pyproject.toml +2 -2
- easydetect-0.2.2/tests/test_lab_api.py +31 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_onnxruntime.py +27 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_train_export.py +27 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tools/hub_README.md +1 -1
- {easydetect-0.2.0 → easydetect-0.2.2}/tools/make_toyset.py +1 -1
- easydetect-0.2.0/platform/README.md +0 -199
- easydetect-0.2.0/platform/tests/conftest.py +0 -9
- easydetect-0.2.0/platform/tests/test_app.py +0 -1222
- easydetect-0.2.0/platform/tests/test_labeling.py +0 -94
- {easydetect-0.2.0 → easydetect-0.2.2}/.gitignore +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/LICENSE +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/NOTICE +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/cli.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/data/__init__.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/data/augment.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/downloads.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/errors.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/metrics.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/__init__.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/decoder.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/denoising.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/dfine_net.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/encoder.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/hgnetv2.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/nn/ops.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/plotting.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/results.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/sources.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/tracker.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/utils/__init__.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/utils/loss.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/utils/ops.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/easydetect/validator.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/__init__.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/conftest.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_augment.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_cli.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_coco2yolo.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_dataset.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_downloads.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_model.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_nn.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_predictor.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_results.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_sources.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tests/test_tracker.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tools/bench.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tools/build_mirror.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tools/coco2yolo.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tools/compare_yolo.py +0 -0
- {easydetect-0.2.0 → easydetect-0.2.2}/tools/convert_dfine.py +0 -0
- {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.
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
folder whose model card is written
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-

|
|
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
|
-
* [
|
|
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
|
-
라벨링부터 학습·추론까지 브라우저로 하려면
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
folder whose model card is written
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-

|
|
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
|
-
* [
|
|
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
|
-
라벨링부터 학습·추론까지 브라우저로 하려면
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
|
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 =
|
|
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. ``
|
|
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
|
|
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,
|
|
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=
|
|
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.
|
|
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"
|
|
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
|
-
[
|
|
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
|
|
10
|
+
(train, val, export, predict, the lab), not for judging accuracy.
|
|
11
11
|
"""
|
|
12
12
|
import pathlib
|
|
13
13
|
import random
|