attr-eomt 1.2.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/.gitignore +4 -1
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/PKG-INFO +208 -199
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/README.md +207 -198
- attr_eomt-2.0.0/configs/xview.yaml +26 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/api.py +47 -12
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/aux_cls.py +30 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/compress.py +1 -2
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/config.py +11 -7
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/data/__init__.py +4 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/data/coco.py +35 -4
- attr_eomt-2.0.0/eomt/data/config.py +128 -0
- attr_eomt-2.0.0/eomt/data/sets.py +272 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/data/transforms.py +5 -4
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/ema.py +2 -1
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/engine/match.py +49 -19
- attr_eomt-2.0.0/eomt/engine/predict.py +411 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/engine/track.py +4 -4
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/engine/train.py +552 -168
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/engine/validate.py +207 -11
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/loss.py +1 -2
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/model.py +187 -2
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/postprocess.py +9 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/preprocess.py +52 -7
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/reid.py +1 -3
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/serialization.py +14 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/tta.py +3 -4
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/pyproject.toml +1 -1
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/sample_data/README.md +15 -3
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/sample_data/data.yaml +5 -10
- attr_eomt-2.0.0/sample_data/run.yaml +55 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/scripts/match.py +10 -6
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/scripts/predict.py +10 -2
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/scripts/train.py +38 -33
- attr_eomt-2.0.0/scripts/train_multiview.py +47 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/site/index.html +2 -2
- attr_eomt-2.0.0/tests/test_hires.py +70 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_regression_attr.py +25 -23
- attr_eomt-2.0.0/tests/test_run_config.py +102 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_smoke.py +13 -8
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_tta.py +1 -1
- attr_eomt-2.0.0/tests/test_xview.py +337 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/uv.lock +1 -1
- attr_eomt-1.2.0/eomt/data/config.py +0 -83
- attr_eomt-1.2.0/eomt/engine/predict.py +0 -242
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/.github/workflows/pages.yml +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/.github/workflows/publish.yml +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/.github/workflows/release.yml +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/.github/workflows/tests.yml +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/LICENSE +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/configs/coco.yaml +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/docs/assets/00-hero-banner.png +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/docs/assets/00-hero-banner.svg +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/docs/examples/billie_eilish.jpg +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/docs/examples/rosalia.jpg +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/docs/examples/sample.jpg +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/docs/examples/taylor_swift.jpg +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/__init__.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/box_loss.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/data/download.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/device.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/engine/__init__.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/engine/distributed.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/plotting.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/eomt/visualize.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/sample_data/annotations/instances_train.json +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/sample_data/annotations/instances_val.json +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/sample_data/images/train/.gitkeep +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/sample_data/images/val/.gitkeep +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/scripts/info.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/scripts/push_to_hub.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/scripts/track.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/scripts/train_detect_coco.sh +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/scripts/val.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_api.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_architecture.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_augment.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_compress.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_ddp.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_parity.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_recipe.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_reid.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tests/test_sweep.py +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tools/attr_tagger/README.md +0 -0
- {attr_eomt-1.2.0 → attr_eomt-2.0.0}/tools/attr_tagger/tag.py +0 -0
|
@@ -20,11 +20,14 @@ weights/
|
|
|
20
20
|
*.pt
|
|
21
21
|
*.pth
|
|
22
22
|
|
|
23
|
-
# Datasets (auto-downloaded)
|
|
23
|
+
# Datasets (auto-downloaded) and their local converters
|
|
24
24
|
datasets/
|
|
25
25
|
/data/coco/
|
|
26
26
|
scripts/sample_images/
|
|
27
27
|
|
|
28
|
+
# Local training: per-dataset train_*.py, their configs/ and converter tests
|
|
29
|
+
local_training_scripts/
|
|
30
|
+
|
|
28
31
|
# Editor / OS
|
|
29
32
|
.idea/
|
|
30
33
|
.vscode/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: attr-eomt
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Standalone EoMT (Encoder-only Mask Transformer) for instance segmentation, with DINOv2 init, COCO training/validation and inference.
|
|
5
5
|
Project-URL: Homepage, https://github.com/imagra93/attr-eomt
|
|
6
6
|
Project-URL: Documentation, https://imagra93.github.io/attr-eomt
|
|
@@ -121,40 +121,14 @@ post-processing automatically. Everything below applies identically to both.
|
|
|
121
121
|
|
|
122
122
|
### Models & sizes
|
|
123
123
|
|
|
124
|
-
| size | backbone | hidden | layers | heads | queries |
|
|
125
|
-
|
|
126
|
-
| `s` | DINOv2-small | 384 | 12 | 6 | 100 |
|
|
127
|
-
| `b` | DINOv2-base | 768 | 12 | 12 | 200 |
|
|
128
|
-
| `l` | DINOv2-large | 1024 | 24 | 16 | 200 |
|
|
124
|
+
| size | backbone | hidden | layers | heads | queries | params |
|
|
125
|
+
|------|-----------------|--------|--------|-------|---------|--------|
|
|
126
|
+
| `s` | DINOv2-small | 384 | 12 | 6 | 100 | 24 M |
|
|
127
|
+
| `b` | DINOv2-base | 768 | 12 | 12 | 200 | 94 M |
|
|
128
|
+
| `l` | DINOv2-large | 1024 | 24 | 16 | 200 | 317 M |
|
|
129
129
|
|
|
130
|
-
Default input is a patch-14-aligned square (`644 = 14 × 46`) so DINOv2 weights load 1:1.
|
|
131
|
-
|
|
132
|
-
### Compute & inference speed
|
|
133
|
-
|
|
134
|
-
Measured on a single **NVIDIA GeForce RTX 5090**, `644 × 644` input, batch size 1.
|
|
135
|
-
GFLOPs are multiply-accumulates at that resolution (attention included); latency /
|
|
136
|
-
throughput are the median over 50 runs after warm-up, under `torch.amp.autocast`
|
|
137
|
-
(fp16) — the package's own inference path.
|
|
138
|
-
|
|
139
|
-
**`instance` family** (masks + boxes + class):
|
|
140
|
-
|
|
141
|
-
| size | params | GFLOPs | latency (fp16) | throughput (fp16) | throughput (fp32) |
|
|
142
|
-
|------|--------|--------|----------------|-------------------|-------------------|
|
|
143
|
-
| `s` | 24.0 M | 128 | 8.4 ms | 119 img/s | 70 img/s |
|
|
144
|
-
| `b` | 93.9 M | 430 | 17.4 ms | 58 img/s | 32 img/s |
|
|
145
|
-
| `l` | 317 M | 1144 | 30.2 ms | 33 img/s | 15 img/s |
|
|
146
|
-
|
|
147
|
-
**`detect` family** (boxes + class, no mask head):
|
|
148
|
-
|
|
149
|
-
| size | params | GFLOPs | latency (fp16) | throughput (fp16) | throughput (fp32) |
|
|
150
|
-
|------|--------|--------|----------------|-------------------|-------------------|
|
|
151
|
-
| `s` | 22.7 M | 89 | 2.9 ms | 348 img/s | 120 img/s |
|
|
152
|
-
| `b` | 88.6 M | 276 | 5.3 ms | 190 img/s | 60 img/s |
|
|
153
|
-
| `l` | 308 M | 881 | 13.6 ms | 74 img/s | 21 img/s |
|
|
154
|
-
|
|
155
|
-
Dropping the mask-upsampling head makes `detect` substantially lighter and ~1.3–3×
|
|
156
|
-
faster. Figures are for the detector itself (backbone + queries + heads); the
|
|
157
|
-
attribute heads add a thin linear/MLP per head and are negligible by design.
|
|
130
|
+
Default input is a patch-14-aligned square (`644 = 14 × 46`) so DINOv2 weights load 1:1. The `detect` family drops the
|
|
131
|
+
mask-upsampling head and is lighter; each attribute head adds only a thin linear / MLP layer.
|
|
158
132
|
|
|
159
133
|
---
|
|
160
134
|
|
|
@@ -230,8 +204,10 @@ simply reuse that same query→GT assignment and read the answer off the matched
|
|
|
230
204
|
|
|
231
205
|
Attributes live **inside the COCO annotations** — each annotation is already a
|
|
232
206
|
per-instance object, so alignment is automatic and `pycocotools` still parses it. Just
|
|
233
|
-
two additions to a standard COCO file
|
|
234
|
-
|
|
207
|
+
two additions to a standard COCO file — heads (count, classes, names) are discovered from the
|
|
208
|
+
JSON, the same as `nc`. The dataset holds **only data** (each attribute's name, type and
|
|
209
|
+
categories); which attributes get a head and how each one learns (`applies_to`, `flip_aware`,
|
|
210
|
+
`log`, `range`, `crop`) is the **run config**'s `attributes` section — see *Run config* below.
|
|
235
211
|
|
|
236
212
|
**1. A top-level `attributes` list** — one entry per head, defining its vocabulary:
|
|
237
213
|
|
|
@@ -275,30 +251,36 @@ Notes:
|
|
|
275
251
|
instances that actually carry its value. A JSON with **no** `attributes` ⇒ detection-only,
|
|
276
252
|
exactly as before.
|
|
277
253
|
|
|
278
|
-
**Class-conditional heads.** Give an attribute
|
|
279
|
-
|
|
254
|
+
**Class-conditional heads.** Give an attribute an optional `applies_to` list of primary-class
|
|
255
|
+
names or ids in the run config, and that head is only trained on — and only emitted for —
|
|
280
256
|
instances of those classes (hard routing on the primary class). Omit it and the head applies
|
|
281
257
|
to every class. So different attributes can attach to different classes, each with its own
|
|
282
258
|
label set, in one model:
|
|
283
259
|
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
]
|
|
260
|
+
```yaml
|
|
261
|
+
# run config
|
|
262
|
+
attributes:
|
|
263
|
+
posture: {applies_to: [cat, dog]}
|
|
288
264
|
```
|
|
289
265
|
|
|
290
266
|
At inference a scoped head reports `ids = -1` ("not applicable") for detections whose class it
|
|
291
267
|
does not cover. The scope is stored in the checkpoint, so it survives reload.
|
|
292
268
|
|
|
293
269
|
**Flip-aware attributes.** A horizontal flip mirrors the photo but not the labels, so an attribute
|
|
294
|
-
that encodes a side (a left
|
|
295
|
-
|
|
270
|
+
that encodes a side (a left shoe shows up as a right one) is corrupted by `flip_prob > 0`. Give
|
|
271
|
+
it `flip_aware` in the run config, a map of category names to the ones they become on a horizontal
|
|
296
272
|
flip, and the training augmentation swaps them for every instance it flips (each pair swaps both
|
|
297
|
-
ways, so `{
|
|
273
|
+
ways, so `{left: right}` is enough; unlisted categories stay as they are):
|
|
298
274
|
|
|
299
275
|
```jsonc
|
|
300
|
-
|
|
301
|
-
"
|
|
276
|
+
// data: the attribute's categories
|
|
277
|
+
{"name": "side", "categories": [{"id": 0, "name": "left"}, {"id": 1, "name": "center"}, {"id": 2, "name": "right"}]}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
# run config: how it learns
|
|
282
|
+
attributes:
|
|
283
|
+
side: {flip_aware: {left: right}}
|
|
302
284
|
```
|
|
303
285
|
|
|
304
286
|
Mosaic, mixup and copy-paste flip each source image independently, and the swap follows each
|
|
@@ -308,18 +290,26 @@ attribute. It is categorical only. The mapping is stored in the checkpoint, and
|
|
|
308
290
|
mapping was stored takes it from the val JSON, with a warning). The training banner lists each head's swapped pairs. A
|
|
309
291
|
regression head needs no mapping: its value must not change under a flip (see below).
|
|
310
292
|
|
|
311
|
-
**Regression attributes.** `"type": "regression"` makes a head regress one real number
|
|
312
|
-
instance instead of choosing a category; categorical and regression heads mix freely in one
|
|
313
|
-
dataset. `log` (regress `log1p` of the value, for heavy-tailed positives such as
|
|
314
|
-
`range` (valid values; predictions are clipped to it) are optional, and `applies_to` works as above:
|
|
293
|
+
**Regression attributes.** `"type": "regression"` in the data makes a head regress one real number
|
|
294
|
+
per instance instead of choosing a category; categorical and regression heads mix freely in one
|
|
295
|
+
dataset. In the run config, `log` (regress `log1p` of the value, for heavy-tailed positives such as
|
|
296
|
+
costs) and `range` (valid values; predictions are clipped to it) are optional, and `applies_to` works as above:
|
|
315
297
|
|
|
316
298
|
```jsonc
|
|
299
|
+
// data
|
|
317
300
|
"attributes": [
|
|
318
|
-
{"name": "
|
|
319
|
-
{"name": "
|
|
301
|
+
{"name": "grade", "categories": [...]},
|
|
302
|
+
{"name": "price", "type": "regression", "unit": "EUR"}
|
|
320
303
|
]
|
|
321
304
|
// per annotation:
|
|
322
|
-
"attributes": {"
|
|
305
|
+
"attributes": {"grade": 3, "price": 812.5}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```yaml
|
|
309
|
+
# run config
|
|
310
|
+
attributes:
|
|
311
|
+
grade:
|
|
312
|
+
price: {log: true, range: [0, 20000], applies_to: [sofa]}
|
|
323
313
|
```
|
|
324
314
|
|
|
325
315
|
A missing, non-numeric or out-of-range value is ignored. The head is trained on the value
|
|
@@ -334,29 +324,21 @@ A value that depends on how much of the instance is in view can say so with `cro
|
|
|
334
324
|
training augmentation measures, per instance, the fraction it keeps in view (mask mass, or box
|
|
335
325
|
area, inside the crop window, after copy-paste occlusion), and the value follows it.
|
|
336
326
|
`"crop": "scale"` multiplies the value by that fraction — for an extensive quantity such as the
|
|
337
|
-
object's
|
|
327
|
+
object's surface in cm² (half the object cropped away ⇒ half the surface). `"crop": "ignore"` drops the
|
|
338
328
|
value when less than `min_visible` (default `0.9`) is kept — for a property of the whole object
|
|
339
329
|
that a cut view no longer shows. Validation and inference see whole images, so the head predicts
|
|
340
330
|
the full value there:
|
|
341
331
|
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
|
|
332
|
+
```yaml
|
|
333
|
+
# run config
|
|
334
|
+
attributes:
|
|
335
|
+
surface_cm2: {log: true, crop: scale}
|
|
336
|
+
length_cm: {crop: ignore, min_visible: 0.95}
|
|
345
337
|
```
|
|
346
338
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
the
|
|
350
|
-
hidden, and instances brought in by mosaic, mixup or copy-paste are measured against their own crops. For boxes (detect family), the ratio of
|
|
351
|
-
the box areas: exact for a crop, approximate under rotation. Each call of the built-in
|
|
352
|
-
augmentation exposes it as `TrainAugment.last_info["visible"]`.
|
|
353
|
-
|
|
354
|
-
Limits: the head's mean / std and its `aux_class_weights` density come from the unscaled values,
|
|
355
|
-
so with `"scale"` the values trained on sit somewhat lower than those statistics. Only the visible
|
|
356
|
-
fraction is tracked, not the scale jitter, so a pixel area is still not supported. A custom
|
|
357
|
-
`transform=` gets no adjustment. `crop` is rejected on a categorical attribute, and it is a
|
|
358
|
-
training-only setting (not stored in the checkpoint). The training banner prints each head's crop
|
|
359
|
-
mode.
|
|
339
|
+
The fraction is the share of the instance's mask mass kept inside the window (box overlap for the detect family),
|
|
340
|
+
exposed as `TrainAugment.last_info["visible"]`. `crop` applies to regression heads only, is training-only (not stored in
|
|
341
|
+
the checkpoint), and is not applied to a custom `transform=`.
|
|
360
342
|
|
|
361
343
|
**Sidecar format (optional).** You can keep the COCO JSON as plain, standard COCO and put the
|
|
362
344
|
attributes beside it instead of inside it: an `attributes.yaml` schema (same keys, regression heads included) in the dataset root plus
|
|
@@ -366,101 +348,140 @@ dataset always works and the sidecar is picked up automatically when you add it.
|
|
|
366
348
|
`attributes` in the JSON take precedence.
|
|
367
349
|
|
|
368
350
|
A tiny, self-contained example (two heads, including a non-contiguous id set) lives in
|
|
369
|
-
[sample_data/](sample_data/).
|
|
351
|
+
[sample_data/](sample_data/), with its run config [sample_data/run.yaml](sample_data/run.yaml).
|
|
352
|
+
|
|
353
|
+
Datasets built before the run config may still carry `applies_to` / `flip_aware` / `log` / `range` / `crop` in their
|
|
354
|
+
attribute definitions: they are still read, and the run config's `attributes` win over them.
|
|
370
355
|
|
|
371
356
|
---
|
|
372
357
|
|
|
358
|
+
## Run config (one YAML per run)
|
|
359
|
+
|
|
360
|
+
**The dataset holds the data; one YAML holds the run.** The COCO JSONs carry the images, the instances and each
|
|
361
|
+
attribute's name, type and categories — nothing about training. Everything else is written out in the YAML that
|
|
362
|
+
`--data` points at, the **run config**: the data paths, which attributes get a head and how each learns, the
|
|
363
|
+
augmentation, the `train()` settings and the inference settings saved with the model. One file says how a model was
|
|
364
|
+
made, so a run can be read, repeated and compared from it. Start from the commented template
|
|
365
|
+
[`sample_data/run.yaml`](sample_data/run.yaml):
|
|
366
|
+
|
|
367
|
+
```yaml
|
|
368
|
+
# --- data
|
|
369
|
+
path: .
|
|
370
|
+
train_images: images/train
|
|
371
|
+
train_json: annotations/instances_train.json
|
|
372
|
+
val_images: images/val
|
|
373
|
+
val_json: annotations/instances_val.json
|
|
374
|
+
|
|
375
|
+
# --- the heads, and how each learns: flip_aware, crop, min_visible, log, range, applies_to
|
|
376
|
+
# (an attribute of the data left out gets no head; no section: every attribute as the data declares it)
|
|
377
|
+
attributes:
|
|
378
|
+
scale:
|
|
379
|
+
viewpoint:
|
|
380
|
+
# side: {flip_aware: {left: right}}
|
|
381
|
+
# price: {log: true, range: [0, 20000], applies_to: [sofa]}
|
|
382
|
+
# surface_cm2: {log: true, crop: scale}
|
|
383
|
+
|
|
384
|
+
# --- augmentation: AugConfig fields over its defaults (`train_aug`, the older name, is the same section)
|
|
385
|
+
aug:
|
|
386
|
+
flip_prob: 0.5
|
|
387
|
+
min_scale: 0.4
|
|
388
|
+
max_scale: 1.6
|
|
389
|
+
|
|
390
|
+
# --- train() keywords: anything train() takes but the data paths (the size: --size, the device: --device)
|
|
391
|
+
train:
|
|
392
|
+
project: ../runs/train
|
|
393
|
+
name: sample
|
|
394
|
+
epochs: 12
|
|
395
|
+
imgsz: 644
|
|
396
|
+
batch: 4
|
|
397
|
+
# init_weights: ../runs/train/eomt-l/weights/best.pt # warm start from a checkpoint
|
|
398
|
+
|
|
399
|
+
# --- saved in every checkpoint: the defaults of predict() / track() with that model
|
|
400
|
+
inference:
|
|
401
|
+
conf_thres: 0.3
|
|
402
|
+
max_det: 100
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
python scripts/train.py --data sample_data/run.yaml --size s --device 0,1 # the whole run from the file
|
|
407
|
+
python scripts/train.py --data sample_data/run.yaml --size s --epochs 1 # a flag beats the file
|
|
408
|
+
python scripts/predict.py runs/train/sample/weights/best.pt images/ # with the conf_thres it was saved with
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
- **Precedence:** explicit arguments (command line, `train()` / `EoMT.train()` keywords) beat the YAML, which beats the
|
|
412
|
+
library's defaults. Relative paths (`path`, `train.init_weights`, `train.project`) are relative to the YAML.
|
|
413
|
+
- **Recorded:** `args.yaml` in the run folder and every checkpoint (`run_config`) hold the resolved run — every
|
|
414
|
+
hyper-parameter, the augmentation and how each head learned (with its regression statistics); the checkpoint also
|
|
415
|
+
keeps `inference`, which `predict()` / `track()` (and `scripts/predict.py`) use for any setting the call leaves out.
|
|
416
|
+
- **Errors early:** an attribute the YAML configures but the data does not define, an unknown behavior or inference key,
|
|
417
|
+
or a data path / size / device inside `train` stops the run before it starts.
|
|
418
|
+
|
|
373
419
|
## Cross-photo re-identification
|
|
374
420
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
421
|
+
Given several photos of one subject, `infer_match()` decides which detections are the same physical instance, with no
|
|
422
|
+
second model and nothing retrained: the per-query embedding that feeds the class head is the fingerprint. Photos are
|
|
423
|
+
matched pairwise with the Hungarian matcher and linked into identities with union-find (at most one instance per photo,
|
|
424
|
+
plus a merge guard against transitive chaining). `group_by` limits which pairs may compete (same class by default; add
|
|
425
|
+
attribute heads to tighten it) and `sim_thres` (cosine, default `0.7`) is the floor for "same instance". Each run
|
|
426
|
+
writes its candidate pairs and similarity diagnostics to JSON, so the threshold can be retuned without re-running
|
|
427
|
+
inference.
|
|
381
428
|
|
|
382
429
|
```python
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
model = EoMT("runs/train/eomt-l/weights/best.pt")
|
|
386
|
-
result = model.infer_match("photos/subject_42/", group_by=("class",))
|
|
430
|
+
result = EoMT("runs/train/eomt-l/weights/best.pt").infer_match("photos/subject_42/", group_by=("class",))
|
|
431
|
+
```
|
|
387
432
|
|
|
388
|
-
|
|
389
|
-
|
|
433
|
+
```bash
|
|
434
|
+
python scripts/match.py weights/best.pt photos/ --each-subdir # one folder per subject
|
|
390
435
|
```
|
|
391
436
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
437
|
+
Every option is documented in [`eomt/engine/match.py`](eomt/engine/match.py) and `python scripts/match.py --help`.
|
|
438
|
+
|
|
439
|
+
### Predicting the photos together (cross-view)
|
|
440
|
+
|
|
441
|
+
Re-identification links what each photo found on its own. A **cross-view** model predicts
|
|
442
|
+
the photos of one subject (1–4) in **one** forward pass: after each of the four query
|
|
443
|
+
blocks, the queries of every photo attend to the patches and queries of the *other*
|
|
444
|
+
photos (plus a null token to abstain). An instance that is tiny or covered in one photo
|
|
445
|
+
can be found because another shows it clearly, and an ambiguous close-up can take its
|
|
446
|
+
class and attributes from the wide shot. Each photo's masks still cover only what that
|
|
447
|
+
photo shows, and a photo alone is predicted bit for bit as before. A small matching head
|
|
448
|
+
embeds every instance for the cross-photo links.
|
|
449
|
+
|
|
450
|
+
It is a post-training of an existing checkpoint, and it needs no multi-photo annotations:
|
|
451
|
+
each set is made from one annotated photo — a hard view (shrunken, degraded, occluded or a
|
|
452
|
+
close-up), a view that clears it up, and optionally a distractor photo and a mirrored copy
|
|
453
|
+
— and every instance keeps its annotation id across the views:
|
|
399
454
|
|
|
400
455
|
```bash
|
|
401
|
-
|
|
402
|
-
python scripts/
|
|
456
|
+
# post-train a trained model -> runs/train/multiview/weights/best.pt
|
|
457
|
+
python scripts/train_multiview.py --data my/data.yaml --weights runs/train/eomt-l/weights/best.pt --device 0,1
|
|
458
|
+
|
|
459
|
+
# predict the photos of each subject together: photos/ holds one subject's photos, or a subfolder per subject
|
|
460
|
+
python scripts/predict.py runs/train/multiview photos/ --subject
|
|
403
461
|
```
|
|
404
462
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
covers instances that sit at different places on the subject, an attribute head that
|
|
428
|
-
separates them — `("class", "position")`, say — stops an instance in one location from
|
|
429
|
-
ever matching one in another, however alike they look. Any head in the checkpoint can
|
|
430
|
-
be named; unknown names raise before the first forward pass. `group_by=None` disables
|
|
431
|
-
gating entirely and needs a much higher `sim_thres`, since far more pairs then compete
|
|
432
|
-
with no structural prior ruling any of them out.
|
|
433
|
-
|
|
434
|
-
`sim_thres` (default `0.7`) is the cosine floor for "same instance". Hungarian always
|
|
435
|
-
returns a full assignment, so the threshold is what turns *best available partner* into
|
|
436
|
-
*no partner — this is a new instance*. Measured on one checkpoint across four real
|
|
437
|
-
photo sets, with the gate on:
|
|
438
|
-
|
|
439
|
-
| | non-candidate pairs | true-match pairs |
|
|
440
|
-
|---|---|---|
|
|
441
|
-
| mean | 0.11 – 0.16 | 0.85 – 0.98 (the true-match mode) |
|
|
442
|
-
| p99 / max | 0.40 – 0.67 / 0.49 – 0.79 | — |
|
|
443
|
-
|
|
444
|
-
The default was raised from `0.6` to `0.7` after a 30-case run showed non-candidate
|
|
445
|
-
pairs reaching `0.73` and the two distributions overlapping in over half the cases —
|
|
446
|
-
`0.6` was admitting matches with no margin at all. Every run
|
|
447
|
-
writes its own `diagnostics` (within-identity vs across-identity percentiles) into
|
|
448
|
-
`<subject>_identities.json`; if those two distributions overlap, no threshold will
|
|
449
|
-
save the run.
|
|
450
|
-
|
|
451
|
-
### Limitations
|
|
452
|
-
|
|
453
|
-
- **A gate group with one instance per photo gets no benefit from the embedding.**
|
|
454
|
-
Hungarian has no choice to make, and only `sim_thres` can veto the pairing. The
|
|
455
|
-
fingerprint earns its keep where several instances of one group compete in the same
|
|
456
|
-
photo.
|
|
457
|
-
- **Small-instance recall caps what can be matched.** An instance that is never
|
|
458
|
-
detected in a view cannot be linked to it.
|
|
459
|
-
- **One folder must be one subject.** A folder holding photos of more than one subject
|
|
460
|
-
will happily link generic-looking instances across them; that is a data problem, not
|
|
461
|
-
a matching one.
|
|
462
|
-
- **Letterboxing shifts scale with orientation**, so a landscape and a portrait shot of
|
|
463
|
-
the same subject are not on quite equal footing.
|
|
463
|
+
`scripts/train_multiview.py` is `scripts/train.py` with the recipe as defaults (`--multiview configs/xview.yaml
|
|
464
|
+
--batch 1 --epochs 2 --val-interval 1 --name multiview`); any other flag of `scripts/train.py` can be added and wins.
|
|
465
|
+
In Python, `EoMT(path).predict("photos/", subject=True)`.
|
|
466
|
+
|
|
467
|
+
The default config is stage A: only the new modules train (about 18 M parameters on `l`),
|
|
468
|
+
the checkpoint stays frozen; stage B adds `query_blocks: 0.1` to `trainable`. `best.pt` is
|
|
469
|
+
chosen on fixed synthetic validation sets: the hard photo's segm mAP predicted with its set
|
|
470
|
+
(`val/sets/segm/mAP_set`, next to `_alone`), its attributes, and the link F1 of the
|
|
471
|
+
matching head against plain re-ID (`val/sets/match/*` vs `val/sets/reid/*`). Synthetic sets
|
|
472
|
+
show neither real viewpoint changes nor real reflections: measure on real multi-photo sets
|
|
473
|
+
before trusting a gain. `infer_match()` and `scripts/match.py` predict a cross-view
|
|
474
|
+
checkpoint jointly (`joint=False` for one pass per photo), with its calibrated threshold
|
|
475
|
+
and no class gate; `eomt.engine.predict.predict_set` returns the per-photo results alone.
|
|
476
|
+
|
|
477
|
+
For a subject with more photos than a set holds, `eomt.engine.predict.predict_subject` groups
|
|
478
|
+
them (`subject_sets`) and returns one result per photo: `mode="order"` (the default) predicts
|
|
479
|
+
them `max_views` at a time in the given order, every photo once (about the cost of predicting
|
|
480
|
+
them one by one); `"partition"` groups similar photos (mean patch token of a small pass);
|
|
481
|
+
`"anchor"` predicts each photo with its `k` most similar others. On real multi-photo sets
|
|
482
|
+
(close-ups plus overviews of each subject) the three gave the same gain: what helps is seeing
|
|
483
|
+
other photos of the subject, not a particular one. `scripts/predict.py --subject [MODE]` and
|
|
484
|
+
`predict(subject=MODE)` take the mode (`--subject` alone is `"order"`).
|
|
464
485
|
|
|
465
486
|
---
|
|
466
487
|
|
|
@@ -484,8 +505,9 @@ from eomt import EoMT
|
|
|
484
505
|
# Train on COCO 2017 (auto-downloaded on first run):
|
|
485
506
|
EoMT("l").train(data="coco", batch=4) # 12 epochs, validated every 2: the official recipe
|
|
486
507
|
|
|
487
|
-
# ...or any COCO-format dataset
|
|
488
|
-
EoMT("s").train(data="sample_data/
|
|
508
|
+
# ...or any COCO-format dataset, the run written out in its run config (see "Run config"):
|
|
509
|
+
EoMT("s").train(data="sample_data/run.yaml")
|
|
510
|
+
EoMT("s").train(data="sample_data/run.yaml", epochs=1) # a keyword beats the file
|
|
489
511
|
|
|
490
512
|
# Validate and predict from a trained run:
|
|
491
513
|
EoMT("runs/train/eomt-l").val(data="coco")
|
|
@@ -494,9 +516,7 @@ EoMT("runs/train/eomt-l").predict("images/", plot=True) # writes annotated ima
|
|
|
494
516
|
|
|
495
517
|
**Several GPUs.** `device="0,1"` trains on both GPUs, one process each (DistributedDataParallel); `"auto"` is GPU 0.
|
|
496
518
|
`batch` is per GPU and `nominal_batch` (16) stays the global effective batch, so the optimizer steps, LR schedule and
|
|
497
|
-
masked-attention annealing are exactly those of one GPU
|
|
498
|
-
validation and checkpoints). Validation splits the val images between the
|
|
499
|
-
GPUs; the main process scores them and alone logs and writes checkpoints. The processes are spawned, so every `train()`
|
|
519
|
+
masked-attention annealing are exactly those of one GPU. Validation splits the val images between the GPUs; the main process scores them and alone logs and writes checkpoints. The processes are spawned, so every `train()`
|
|
500
520
|
argument must be picklable.
|
|
501
521
|
|
|
502
522
|
```python
|
|
@@ -521,9 +541,7 @@ m.predict("images/", augment={"flip": True}, tiles=1024) # 1x / 1.5x
|
|
|
521
541
|
```
|
|
522
542
|
|
|
523
543
|
`scripts/val.py` and `scripts/predict.py` take `--augment` and `--tiles [SIZE]` (`scripts/val.py` also `--flip`; its arguments are
|
|
524
|
-
in [Command-line scripts](#command-line-scripts-arguments-and-things-to-take-into-account)). Each view is
|
|
525
|
-
costs ~3.5x one prediction (the 1.5x view has 2.25x the pixels); a 1280 x 960 photo at `imgsz` 644 makes 6 tiles + the
|
|
526
|
-
whole image.
|
|
544
|
+
in [Command-line scripts](#command-line-scripts-arguments-and-things-to-take-into-account)). Each view is one more forward pass.
|
|
527
545
|
|
|
528
546
|
For the full training recipe, every `train()` knob, and int8 compression, see the
|
|
529
547
|
**[annotated explainer →](https://imagra93.github.io/attr-eomt)** — it's the deep dive.
|
|
@@ -547,9 +565,8 @@ close to the head's speed: scaling the whole encoder by 0.1 (as detector-style r
|
|
|
547
565
|
LR decay and the annealing span `epochs`.
|
|
548
566
|
|
|
549
567
|
**Validation follows the training regime**, as in the official EoMT: masked attention at the annealing probabilities of
|
|
550
|
-
the epoch's end. A model that still masks is far worse without its masks
|
|
551
|
-
|
|
552
|
-
block finishes it, not the model. `best.pt` is deployed mask-free, so it is only chosen once every block's annealing is
|
|
568
|
+
the epoch's end. A model that still masks is far worse without its masks, so a mask-free curve before the last sixth follows the
|
|
569
|
+
annealing and jumps as each block finishes it, not the model. `best.pt` is deployed mask-free, so it is only chosen once every block's annealing is
|
|
553
570
|
over (a run stopped earlier, e.g. with `stop_after_epochs`, has `last.pt` alone), and every checkpoint is saved mask-free.
|
|
554
571
|
Training prints the windows in epochs when it starts.
|
|
555
572
|
|
|
@@ -587,9 +604,10 @@ Things to take into account when training:
|
|
|
587
604
|
every validation is eligible.
|
|
588
605
|
- **Validated epochs only.** With `--val-interval N` the other epochs have empty validation cells in `metrics.csv`; the curves in
|
|
589
606
|
`metrics.png` join the validated points.
|
|
590
|
-
- **Attribute heads follow the
|
|
591
|
-
from the
|
|
592
|
-
|
|
607
|
+
- **Attribute heads follow the run config.** Which attributes train and their `applies_to`, `flip_aware`, `crop`, `log` and
|
|
608
|
+
`range` come from the YAML's `attributes` section (see *Run config*), else from the dataset's definitions; the training
|
|
609
|
+
banner lists them. The flip mapping, scope and regression statistics are stored in the checkpoint, and the whole run
|
|
610
|
+
config in `args.yaml` and the checkpoint's `run_config`.
|
|
593
611
|
|
|
594
612
|
**`scripts/val.py`.**
|
|
595
613
|
|
|
@@ -629,41 +647,32 @@ Things to take into account when validating:
|
|
|
629
647
|
All are keyword arguments of `train()` (and flags of `scripts/train.py`); checkpoints record them, so `EoMT(path)` rebuilds
|
|
630
648
|
the model with the same modules, and checkpoints without them load as before.
|
|
631
649
|
|
|
632
|
-
| option | what it does |
|
|
633
|
-
|
|
634
|
-
| `fpn_scales=None` | drop the multi-scale FPN (default `(2, 1, 0.5)`) |
|
|
635
|
-
| `box_head=True` | auxiliary box head: L1 + GIoU loss on the matched queries, and the same terms in the matching cost (a localisation signal that does not depend on the mask grid). Predicted boxes are returned as `aux_boxes` / `head_boxes` |
|
|
636
|
-
| `deep_supervision=False` | stop supervising a query block's prediction once that block's masked attention is annealed away (while it masks, the prediction builds its attention mask and stays supervised
|
|
637
|
-
| `num_upscale_blocks=3` |
|
|
638
|
-
| `iou_aware_cls=True` | train a matched query's class probability towards `(1 + mask IoU) / 2` instead of 1 (IoU-aware targets, as in VarifocalNet / Stable-DINO), so the score ranks loose masks below tight ones; above 0.5, so matched queries keep their class for the attribute gate. Hard targets reduce exactly to the weighted CE
|
|
639
|
-
| `quality_weight=2` | mask-quality head: a small MLP on the query embedding predicts each matched query's mask IoU (BCE against the measured IoU, final prediction only; Mask Scoring R-CNN), and postprocess scores class probability x predicted IoU instead of the mean mask probability
|
|
640
|
-
| `val_imgsz=N` | validate (and pick `best.pt`) at another input size than `imgsz`. Scale jitter shows the objects larger, on average, than a val image resized to `imgsz`: validate at `imgsz` × the mean training scale
|
|
641
|
-
| `stop_after_epochs=N` | stop after N epochs while the LR schedule and mask annealing still span `epochs` (truncated A/B runs) |
|
|
650
|
+
| option | what it does |
|
|
651
|
+
|---|---|
|
|
652
|
+
| `fpn_scales=None` | drop the multi-scale FPN (default `(2, 1, 0.5)`) |
|
|
653
|
+
| `box_head=True` | auxiliary box head: L1 + GIoU loss on the matched queries, and the same terms in the matching cost (a localisation signal that does not depend on the mask grid). Predicted boxes are returned as `aux_boxes` / `head_boxes` |
|
|
654
|
+
| `deep_supervision=False` | stop supervising a query block's prediction once that block's masked attention is annealed away (while it masks, the prediction builds its attention mask and stays supervised). Blocks drop out at 2/6, 3/6, 4/6, 5/6 of training |
|
|
655
|
+
| `num_upscale_blocks=3` | finer mask logits (368² at 644 px) for small objects; cannot warm-start a 2-block checkpoint |
|
|
656
|
+
| `iou_aware_cls=True` | train a matched query's class probability towards `(1 + mask IoU) / 2` instead of 1 (IoU-aware targets, as in VarifocalNet / Stable-DINO), so the score ranks loose masks below tight ones; above 0.5, so matched queries keep their class for the attribute gate. Hard targets reduce exactly to the weighted CE |
|
|
657
|
+
| `quality_weight=2` | mask-quality head: a small MLP on the query embedding predicts each matched query's mask IoU (BCE against the measured IoU, final prediction only; Mask Scoring R-CNN), and postprocess scores class probability x predicted IoU instead of the mean mask probability |
|
|
658
|
+
| `val_imgsz=N` | validate (and pick `best.pt`) at another input size than `imgsz`. Scale jitter shows the objects larger, on average, than a val image resized to `imgsz`: validate at `imgsz` × the mean training scale |
|
|
659
|
+
| `stop_after_epochs=N` | stop after N epochs while the LR schedule and mask annealing still span `epochs` (truncated A/B runs) |
|
|
642
660
|
|
|
643
661
|
Always on (training-side; inference of existing checkpoints is unchanged):
|
|
644
662
|
|
|
645
|
-
- **Masked attention keeps small objects in view.** A query
|
|
663
|
+
- **Masked attention keeps small objects in view.** A query attends to every patch its predicted mask touches (max-pool of
|
|
646
664
|
the mask logits onto the patch grid), and a query whose mask touches no patch attends to all of them, as in Mask2Former. The
|
|
647
|
-
reference EoMT
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
area-averaged onto it, instead of on 12,544 random points (one per ~33 px² at 644 px, so an object of a few dozen pixels often
|
|
652
|
-
got no point and its assigned query changed from step to step). The mask loss keeps the reference's BCE on 12,544 points
|
|
653
|
-
concentrated where the prediction is uncertain (the boundaries), and computes dice on every GT pixel against the upsampled
|
|
654
|
-
logits, so every instance is supervised however small. A fully dense loss gave coarser masks (AP75 down) on thin objects;
|
|
655
|
-
dropping the points altogether lost the boundaries. The `train_num_points` / `oversample_ratio` / `importance_sample_ratio`
|
|
656
|
-
settings are gone (fixed at the reference values; checkpoints that carry them still load).
|
|
665
|
+
reference EoMT samples the mask bilinearly at patch centres and leaves an empty-mask query blind.
|
|
666
|
+
- **Mask terms that see small objects.** Matching scores every query on every cell of the mask-logit grid instead of on
|
|
667
|
+
12,544 random points, and dice is computed on every GT pixel, so every instance is supervised however small. BCE keeps the
|
|
668
|
+
reference's 12,544 points concentrated on the uncertain boundaries.
|
|
657
669
|
- **Attribute IoU gate at full resolution.** The gate that decides which matched queries train the attribute heads compares the
|
|
658
|
-
upsampled prediction with the GT,
|
|
659
|
-
few pixels, so they never trained the attribute heads).
|
|
670
|
+
upsampled prediction with the GT, so objects of a few pixels are not erased by a "nearest" downscale.
|
|
660
671
|
|
|
661
|
-
Inference: `predict_image(..., amp=True)` (and `predict(..., amp=True)`) runs the network under bf16 autocast
|
|
662
|
-
on an RTX 5090 with the same accuracy on a trained ViT-L model (AP within 0.002 in every size bucket). Off by default.
|
|
672
|
+
Inference: `predict_image(..., amp=True)` (and `predict(..., amp=True)`) runs the network under bf16 autocast. Off by default.
|
|
663
673
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
epochs.
|
|
674
|
+
The accuracy effect of these options depends on the data. Measure it with truncated runs (`stop_after_epochs`), which
|
|
675
|
+
follow the schedule of a full run, against a reference run at equal epochs.
|
|
667
676
|
|
|
668
677
|
---
|
|
669
678
|
|
|
@@ -685,7 +694,7 @@ mass-preserving), so thin objects are not shredded into dots by nearest-neighbou
|
|
|
685
694
|
| masks | `mask_resize="area"` (or `"nearest"`) |
|
|
686
695
|
|
|
687
696
|
Override per run, per dataset, or from the CLI — precedence is explicit keywords (`flip_prob`, `min_scale`,
|
|
688
|
-
`max_scale`) **>** `aug=` **>** the
|
|
697
|
+
`max_scale`) **>** `aug=` **>** the YAML's `aug` (or `train_aug`) block **>** defaults:
|
|
689
698
|
|
|
690
699
|
```python
|
|
691
700
|
model.train(data="coco", aug={"rotate_prob": 0.5, "blur_prob": 0}) # in code
|
|
@@ -712,16 +721,16 @@ Things worth knowing:
|
|
|
712
721
|
orientation (e.g. a `viewpoint` head) set `flip_prob=0` and `rotate_prob=0`, or, for a left / right attribute,
|
|
713
722
|
mark it `flip_aware` (see *Flip-aware attributes*) and keep the horizontal flip; `vflip_prob` / `rot90_prob` stay
|
|
714
723
|
off unless your scenes have no canonical "up".
|
|
715
|
-
- **Thin or tiny objects.** Large-Scale Jitter at 0.1 shrinks a 5 px
|
|
724
|
+
- **Thin or tiny objects.** Large-Scale Jitter at 0.1 shrinks a 5 px wide line to under a pixel; keep the effective
|
|
716
725
|
scale within ~[0.4, 1.0] (`min_scale = 0.4·long_side/imgsz`, `max_scale = 1.0·long_side/imgsz`). The instance-aware
|
|
717
726
|
crop pays off when the crop is much smaller than the image (high zoom, small `imgsz`), and
|
|
718
727
|
`instance_crop_rarity_attr` makes rare attribute values the crop's focus more often.
|
|
719
728
|
- **Close-ups.** `zoom_prob` makes an occasional sample a close-up: scaled by `s ~ U(zoom_scale)` (default 4–5x
|
|
720
729
|
the whole-image view, past the photo's own resolution) instead of the LSJ range, with the window on one instance
|
|
721
730
|
(whole when it fits, else a window inside it). `zoom_attr` restricts it to instances with a value of that
|
|
722
|
-
attribute and to the samples that have one, e.g. `{"zoom_prob": 0.1, "zoom_attr": "
|
|
723
|
-
head also learns that
|
|
724
|
-
It resizes the whole photo first
|
|
731
|
+
attribute and to the samples that have one, e.g. `{"zoom_prob": 0.1, "zoom_attr": "surface_cm2"}` so a crop-scaled
|
|
732
|
+
head also learns that an object filling the frame keeps its real surface, and that a cut object keeps the share in view.
|
|
733
|
+
It resizes the whole photo first, so keep it occasional.
|
|
725
734
|
- **Multi-image ops** (`mosaic`, `mixup`, `copy_paste`) add instances from other samples and are *off*: they splice or
|
|
726
735
|
blend long thin structures, so try them deliberately and look at the result.
|
|
727
736
|
- **Negatives.** Images with no annotations are dropped from the train set by default. `train(keep_empty=True)` keeps
|