attr-eomt 1.1.4__tar.gz → 1.2.1__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.1.4 → attr_eomt-1.2.1}/PKG-INFO +1 -1
- attr_eomt-1.2.1/eomt/preprocess.py +144 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/tta.py +9 -5
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/pyproject.toml +1 -1
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_smoke.py +19 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_tta.py +9 -0
- attr_eomt-1.1.4/eomt/preprocess.py +0 -67
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.github/workflows/pages.yml +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.github/workflows/publish.yml +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.github/workflows/release.yml +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.github/workflows/tests.yml +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.gitignore +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/LICENSE +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/README.md +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/configs/coco.yaml +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/assets/00-hero-banner.png +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/assets/00-hero-banner.svg +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/examples/billie_eilish.jpg +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/examples/rosalia.jpg +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/examples/sample.jpg +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/examples/taylor_swift.jpg +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/__init__.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/api.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/aux_cls.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/box_loss.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/compress.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/config.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/__init__.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/coco.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/config.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/download.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/transforms.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/device.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/ema.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/__init__.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/distributed.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/match.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/predict.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/track.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/train.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/validate.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/loss.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/model.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/plotting.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/postprocess.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/reid.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/serialization.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/visualize.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/README.md +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/annotations/instances_train.json +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/annotations/instances_val.json +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/data.yaml +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/images/train/.gitkeep +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/images/val/.gitkeep +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/info.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/match.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/predict.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/push_to_hub.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/track.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/train.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/train_detect_coco.sh +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/val.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/site/index.html +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_api.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_architecture.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_augment.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_compress.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_ddp.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_parity.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_recipe.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_regression_attr.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_reid.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_sweep.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tools/attr_tagger/README.md +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tools/attr_tagger/tag.py +0 -0
- {attr_eomt-1.1.4 → attr_eomt-1.2.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: attr-eomt
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
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
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Preprocessing helpers for EoMT.
|
|
2
|
+
|
|
3
|
+
EoMT / DINOv2 expect a square, ImageNet-normalized RGB tensor. Two modes:
|
|
4
|
+
|
|
5
|
+
* **letterbox** (default): resize the long side to ``input_size`` preserving
|
|
6
|
+
aspect ratio, then pad bottom/right to a square — object shapes are undistorted.
|
|
7
|
+
* **stretch**: resize straight to a square (DETR-style, legacy) — faster but
|
|
8
|
+
distorts aspect ratio.
|
|
9
|
+
|
|
10
|
+
Both return ``(chw_float32, meta)`` where ``meta`` records what postprocessing
|
|
11
|
+
needs to map masks back to the original image (see :func:`make_preprocess_meta`).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import functools
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
|
|
21
|
+
IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
|
|
22
|
+
#: Fixed-point precision of PIL's 8-bit resampling (Pillow's Resample.c).
|
|
23
|
+
_PIL_PRECISION_BITS = 32 - 8 - 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_preprocess_meta(letterbox: bool, content_hw: tuple[int, int], input_size: int) -> dict:
|
|
27
|
+
"""Pack the info postprocessing needs to invert preprocessing.
|
|
28
|
+
|
|
29
|
+
``content_hw`` is the ``(height, width)`` the real image occupies inside the
|
|
30
|
+
square canvas (equal to ``(input_size, input_size)`` for stretch).
|
|
31
|
+
"""
|
|
32
|
+
return {"letterbox": bool(letterbox), "content_hw": tuple(content_hw), "input_size": int(input_size)}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def preprocess_numpy(
|
|
36
|
+
img_rgb_hwc: np.ndarray,
|
|
37
|
+
input_size: int,
|
|
38
|
+
*,
|
|
39
|
+
letterbox: bool = True,
|
|
40
|
+
mean=None,
|
|
41
|
+
std=None,
|
|
42
|
+
):
|
|
43
|
+
"""Resize an RGB HWC uint8 image to ``(C, input_size, input_size)``, normalized.
|
|
44
|
+
|
|
45
|
+
``mean`` / ``std`` default to :data:`IMAGENET_MEAN` / :data:`IMAGENET_STD`; pass
|
|
46
|
+
a model's stored ``pixel_mean`` / ``pixel_std`` to reproduce its exact training
|
|
47
|
+
normalization. Returns ``(chw_float32, meta)``; ``meta`` is from
|
|
48
|
+
:func:`make_preprocess_meta`. The padded border (letterbox) is filled with 0 in
|
|
49
|
+
normalized space, i.e. the mean colour.
|
|
50
|
+
"""
|
|
51
|
+
from PIL import Image
|
|
52
|
+
|
|
53
|
+
mean = IMAGENET_MEAN if mean is None else np.asarray(mean, dtype=np.float32)
|
|
54
|
+
std = IMAGENET_STD if std is None else np.asarray(std, dtype=np.float32)
|
|
55
|
+
|
|
56
|
+
h, w = img_rgb_hwc.shape[:2]
|
|
57
|
+
if not letterbox:
|
|
58
|
+
img = Image.fromarray(img_rgb_hwc).resize((input_size, input_size), Image.BILINEAR)
|
|
59
|
+
arr = np.asarray(img, dtype=np.float32) / 255.0
|
|
60
|
+
arr = (arr - mean) / std
|
|
61
|
+
chw = np.ascontiguousarray(arr.transpose(2, 0, 1))
|
|
62
|
+
return chw, make_preprocess_meta(False, (input_size, input_size), input_size)
|
|
63
|
+
|
|
64
|
+
scale = input_size / max(h, w)
|
|
65
|
+
nh, nw = max(1, round(h * scale)), max(1, round(w * scale))
|
|
66
|
+
img = Image.fromarray(img_rgb_hwc).resize((nw, nh), Image.BILINEAR)
|
|
67
|
+
arr = (np.asarray(img, dtype=np.float32) / 255.0 - mean) / std # (nh, nw, 3)
|
|
68
|
+
canvas = np.zeros((input_size, input_size, 3), dtype=np.float32) # 0 == mean post-normalize
|
|
69
|
+
canvas[:nh, :nw] = arr
|
|
70
|
+
chw = np.ascontiguousarray(canvas.transpose(2, 0, 1))
|
|
71
|
+
return chw, make_preprocess_meta(True, (nh, nw), input_size)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@functools.lru_cache(maxsize=64)
|
|
75
|
+
def _pil_bilinear_taps(in_size: int, out_size: int) -> tuple[np.ndarray, np.ndarray]:
|
|
76
|
+
"""PIL's fixed-point bilinear coefficients for resizing ``in_size`` px to ``out_size``.
|
|
77
|
+
|
|
78
|
+
Mirrors ``precompute_coeffs`` and ``normalize_coeffs_8bpc`` of Pillow's Resample.c. Returns the input index and
|
|
79
|
+
the integer weight of every tap, both ``(out_size, taps)``; padding taps have weight 0.
|
|
80
|
+
"""
|
|
81
|
+
scale = in_size / out_size
|
|
82
|
+
filterscale = max(scale, 1.0)
|
|
83
|
+
support = filterscale # the bilinear filter's support is 1
|
|
84
|
+
center = (np.arange(out_size) + 0.5) * scale
|
|
85
|
+
xmin = np.maximum((center - support + 0.5).astype(np.int64), 0) # C's (int) truncation
|
|
86
|
+
xmax = np.minimum((center + support + 0.5).astype(np.int64), in_size)
|
|
87
|
+
x = xmin[:, None] + np.arange(int(np.ceil(support)) * 2 + 1)
|
|
88
|
+
w = np.maximum(1.0 - np.abs((x - center[:, None] + 0.5) * (1.0 / filterscale)), 0.0) * (x < xmax[:, None])
|
|
89
|
+
total = np.cumsum(w, axis=1)[:, -1:] # summed in order, as PIL does
|
|
90
|
+
w = np.divide(w, total, out=np.zeros_like(w), where=total != 0)
|
|
91
|
+
return np.minimum(x, in_size - 1), np.floor(0.5 + w * (1 << _PIL_PRECISION_BITS)).astype(np.int32)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _pil_resize(x, nh: int, nw: int):
|
|
95
|
+
"""Resize a ``(C, H, W)`` int32 image as PIL's ``Image.resize(..., BILINEAR)`` does an 8-bit one, bit for bit:
|
|
96
|
+
a horizontal then a vertical pass, each rounded to uint8."""
|
|
97
|
+
import torch
|
|
98
|
+
|
|
99
|
+
half = 1 << (_PIL_PRECISION_BITS - 1)
|
|
100
|
+
if nw != x.shape[2]:
|
|
101
|
+
idx, k = (torch.from_numpy(a).to(x.device) for a in _pil_bilinear_taps(x.shape[2], nw))
|
|
102
|
+
acc = torch.full((x.shape[0], x.shape[1], nw), half, dtype=torch.int32, device=x.device)
|
|
103
|
+
for t in range(k.shape[1]):
|
|
104
|
+
acc += x[:, :, idx[:, t]] * k[:, t]
|
|
105
|
+
x = (acc >> _PIL_PRECISION_BITS).clamp_(0, 255)
|
|
106
|
+
if nh != x.shape[1]:
|
|
107
|
+
idx, k = (torch.from_numpy(a).to(x.device) for a in _pil_bilinear_taps(x.shape[1], nh))
|
|
108
|
+
acc = torch.full((x.shape[0], nh, x.shape[2]), half, dtype=torch.int32, device=x.device)
|
|
109
|
+
for t in range(k.shape[1]):
|
|
110
|
+
acc += x[:, idx[:, t], :] * k[:, t, None]
|
|
111
|
+
x = (acc >> _PIL_PRECISION_BITS).clamp_(0, 255)
|
|
112
|
+
return x
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def preprocess_torch(
|
|
116
|
+
img_rgb_hwc: np.ndarray,
|
|
117
|
+
input_size: int,
|
|
118
|
+
*,
|
|
119
|
+
letterbox: bool = True,
|
|
120
|
+
mean=None,
|
|
121
|
+
std=None,
|
|
122
|
+
device="cpu",
|
|
123
|
+
):
|
|
124
|
+
""":func:`preprocess_numpy` run with torch on ``device`` (e.g. the model's GPU), with the same result.
|
|
125
|
+
|
|
126
|
+
The resize reproduces PIL's fixed-point bilinear one, so the output equals :func:`preprocess_numpy`'s.
|
|
127
|
+
Returns ``(chw_float32_tensor, meta)``.
|
|
128
|
+
"""
|
|
129
|
+
import torch
|
|
130
|
+
|
|
131
|
+
h, w = img_rgb_hwc.shape[:2]
|
|
132
|
+
if letterbox:
|
|
133
|
+
scale = input_size / max(h, w)
|
|
134
|
+
nh, nw = max(1, round(h * scale)), max(1, round(w * scale))
|
|
135
|
+
else:
|
|
136
|
+
nh, nw = input_size, input_size
|
|
137
|
+
x = torch.from_numpy(np.ascontiguousarray(img_rgb_hwc)).to(device).permute(2, 0, 1).to(torch.int32)
|
|
138
|
+
x = _pil_resize(x, nh, nw).float()
|
|
139
|
+
mean = torch.as_tensor(IMAGENET_MEAN if mean is None else mean, dtype=torch.float32, device=device).view(3, 1, 1)
|
|
140
|
+
std = torch.as_tensor(IMAGENET_STD if std is None else std, dtype=torch.float32, device=device).view(3, 1, 1)
|
|
141
|
+
levels = torch.full((3, 1, 1), 255.0, device=device) # not a scalar, which torch would divide by as x * (1 / 255)
|
|
142
|
+
chw = x.new_zeros((3, input_size, input_size)) # 0 == mean post-normalize
|
|
143
|
+
chw[:, :nh, :nw] = (x / levels - mean) / std
|
|
144
|
+
return chw, make_preprocess_meta(letterbox, (nh, nw), input_size)
|
|
@@ -38,6 +38,7 @@ repair / replace rules). Attribute probabilities and embeddings are score-weight
|
|
|
38
38
|
from __future__ import annotations
|
|
39
39
|
|
|
40
40
|
import contextlib
|
|
41
|
+
import functools
|
|
41
42
|
import itertools
|
|
42
43
|
import math
|
|
43
44
|
from dataclasses import dataclass
|
|
@@ -47,7 +48,7 @@ import torch
|
|
|
47
48
|
import torch.nn.functional as F # noqa: N812
|
|
48
49
|
|
|
49
50
|
from .postprocess import _masks_to_original, aux_result, aux_value_result, boxes_from_masks, query_scores
|
|
50
|
-
from .preprocess import preprocess_numpy
|
|
51
|
+
from .preprocess import preprocess_numpy, preprocess_torch
|
|
51
52
|
|
|
52
53
|
_AUGMENT_KEYS = {"flip", "scales"}
|
|
53
54
|
_TILES_KEYS = {"size", "overlap", "full"}
|
|
@@ -288,12 +289,16 @@ def predict_views(
|
|
|
288
289
|
amp_dtype: torch.dtype | None = None,
|
|
289
290
|
aux_scopes: dict | None = None,
|
|
290
291
|
embed: bool = False,
|
|
292
|
+
device_preprocess: bool = False,
|
|
291
293
|
) -> dict:
|
|
292
294
|
"""Instances of the RGB ``image`` ``(H, W, 3)`` uint8, merged over the views of ``cfg``.
|
|
293
295
|
|
|
294
296
|
Returns the :func:`~eomt.postprocess.postprocess_instance` dict (``boxes`` / ``scores`` / ``classes`` / ``masks``
|
|
295
297
|
and, with attribute heads, ``aux``), plus ``embed`` ``(N, hidden)`` with ``embed=True``. There is no ``query_idx``:
|
|
296
298
|
a merged instance comes from several queries.
|
|
299
|
+
|
|
300
|
+
With ``device_preprocess=True`` the views are preprocessed on the model's device
|
|
301
|
+
(:func:`~eomt.preprocess.preprocess_torch`) instead of with PIL on the CPU; the inputs match within one grey level.
|
|
297
302
|
"""
|
|
298
303
|
if getattr(model, "family", "instance") != "instance":
|
|
299
304
|
raise NotImplementedError("TTA / tiled inference supports the instance family only.")
|
|
@@ -311,15 +316,14 @@ def predict_views(
|
|
|
311
316
|
view_cells = [_grid_cells(v.rect, g, hg, wg) for v in views]
|
|
312
317
|
|
|
313
318
|
flip_perms = {s.name: s.flip_perm() for s in getattr(model, "aux_specs", []) if s.flip_perm() is not None}
|
|
319
|
+
prep = functools.partial(preprocess_torch, device=dev) if device_preprocess else preprocess_numpy
|
|
314
320
|
found: list[dict] = []
|
|
315
321
|
for size in dict.fromkeys(v.size for v in views):
|
|
316
322
|
idx = [i for i, v in enumerate(views) if v.size == size]
|
|
317
323
|
for start in range(0, len(idx), cfg.batch):
|
|
318
324
|
chunk = idx[start : start + cfg.batch]
|
|
319
|
-
pre = [
|
|
320
|
-
|
|
321
|
-
]
|
|
322
|
-
x = torch.from_numpy(np.stack([chw for chw, _ in pre])).to(dev)
|
|
325
|
+
pre = [prep(_crop(image, views[i]), size, letterbox=letterbox, mean=mean, std=std) for i in chunk]
|
|
326
|
+
x = torch.stack([torch.as_tensor(chw) for chw, _ in pre]).to(dev)
|
|
323
327
|
ctx = torch.autocast(dev.type, dtype=amp_dtype) if amp_dtype is not None else contextlib.nullcontext()
|
|
324
328
|
with ctx:
|
|
325
329
|
out = model(x)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "attr-eomt"
|
|
7
|
-
version = "1.1
|
|
7
|
+
version = "1.2.1"
|
|
8
8
|
description = "Standalone EoMT (Encoder-only Mask Transformer) for instance segmentation, with DINOv2 init, COCO training/validation and inference."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -514,6 +514,25 @@ def test_preprocess_letterbox_meta_and_padding():
|
|
|
514
514
|
assert not meta2["letterbox"] and meta2["content_hw"] == (20, 20)
|
|
515
515
|
|
|
516
516
|
|
|
517
|
+
@pytest.mark.parametrize("device", ["cpu", pytest.param("cuda", marks=pytest.mark.skipif(
|
|
518
|
+
not torch.cuda.is_available(), reason="no GPU"))])
|
|
519
|
+
@pytest.mark.parametrize("letterbox", [True, False])
|
|
520
|
+
@pytest.mark.parametrize("shape, size", [((97, 151), 64), ((720, 1280), 644), ((300, 200), 644), ((5, 900), 140)])
|
|
521
|
+
def test_preprocess_torch_equals_preprocess_numpy(shape, size, letterbox, device):
|
|
522
|
+
"""Downscaled, upscaled and extreme aspect ratios: the same input as PIL's, value for value."""
|
|
523
|
+
import numpy as np
|
|
524
|
+
|
|
525
|
+
from eomt.preprocess import preprocess_numpy, preprocess_torch
|
|
526
|
+
|
|
527
|
+
img = np.random.default_rng(0).integers(0, 256, (*shape, 3), dtype=np.uint8)
|
|
528
|
+
chw, meta = preprocess_torch(img, size, letterbox=letterbox, mean=(0.5, 0.4, 0.3), std=(0.2, 0.3, 0.25),
|
|
529
|
+
device=device)
|
|
530
|
+
expected, expected_meta = preprocess_numpy(img, size, letterbox=letterbox, mean=(0.5, 0.4, 0.3),
|
|
531
|
+
std=(0.2, 0.3, 0.25))
|
|
532
|
+
assert chw.shape == (3, size, size) and chw.dtype == torch.float32 and meta == expected_meta
|
|
533
|
+
assert np.array_equal(chw.cpu().numpy(), expected)
|
|
534
|
+
|
|
535
|
+
|
|
517
536
|
def test_letterbox_inverse_crops_content_not_padding():
|
|
518
537
|
from eomt.postprocess import _masks_to_original
|
|
519
538
|
from eomt.preprocess import make_preprocess_meta
|
|
@@ -172,6 +172,15 @@ def test_flip_tta_merges_the_two_views_and_scores_their_agreement():
|
|
|
172
172
|
assert _iou(mean["masks"][0], 14, 35, 56, 91) > 0.95
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
def test_device_preprocessed_views_find_the_same_instances():
|
|
176
|
+
img = _image(280, 420, 70, 42, 315, 98)
|
|
177
|
+
cfg = TTAConfig(flip=True, scales=(1.0, 1.5), tiles=True, tile_size=140)
|
|
178
|
+
cpu = predict_views(_Blobs(), img, cfg, imgsz=IMGSZ, conf_thres=0.3)
|
|
179
|
+
dev = predict_views(_Blobs(), img, cfg, imgsz=IMGSZ, conf_thres=0.3, device_preprocess=True)
|
|
180
|
+
assert dev["num_detections"] == cpu["num_detections"] == 1
|
|
181
|
+
assert torch.equal(dev["scores"], cpu["scores"]) and torch.equal(dev["masks"], cpu["masks"])
|
|
182
|
+
|
|
183
|
+
|
|
175
184
|
def test_predict_image_with_tta_keeps_the_result_contract():
|
|
176
185
|
from eomt.engine.predict import predict_image
|
|
177
186
|
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
"""Preprocessing helpers for EoMT.
|
|
2
|
-
|
|
3
|
-
EoMT / DINOv2 expect a square, ImageNet-normalized RGB tensor. Two modes:
|
|
4
|
-
|
|
5
|
-
* **letterbox** (default): resize the long side to ``input_size`` preserving
|
|
6
|
-
aspect ratio, then pad bottom/right to a square — object shapes are undistorted.
|
|
7
|
-
* **stretch**: resize straight to a square (DETR-style, legacy) — faster but
|
|
8
|
-
distorts aspect ratio.
|
|
9
|
-
|
|
10
|
-
Both return ``(chw_float32, meta)`` where ``meta`` records what postprocessing
|
|
11
|
-
needs to map masks back to the original image (see :func:`make_preprocess_meta`).
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import numpy as np
|
|
17
|
-
|
|
18
|
-
IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
|
|
19
|
-
IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def make_preprocess_meta(letterbox: bool, content_hw: tuple[int, int], input_size: int) -> dict:
|
|
23
|
-
"""Pack the info postprocessing needs to invert preprocessing.
|
|
24
|
-
|
|
25
|
-
``content_hw`` is the ``(height, width)`` the real image occupies inside the
|
|
26
|
-
square canvas (equal to ``(input_size, input_size)`` for stretch).
|
|
27
|
-
"""
|
|
28
|
-
return {"letterbox": bool(letterbox), "content_hw": tuple(content_hw), "input_size": int(input_size)}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def preprocess_numpy(
|
|
32
|
-
img_rgb_hwc: np.ndarray,
|
|
33
|
-
input_size: int,
|
|
34
|
-
*,
|
|
35
|
-
letterbox: bool = True,
|
|
36
|
-
mean=None,
|
|
37
|
-
std=None,
|
|
38
|
-
):
|
|
39
|
-
"""Resize an RGB HWC uint8 image to ``(C, input_size, input_size)``, normalized.
|
|
40
|
-
|
|
41
|
-
``mean`` / ``std`` default to :data:`IMAGENET_MEAN` / :data:`IMAGENET_STD`; pass
|
|
42
|
-
a model's stored ``pixel_mean`` / ``pixel_std`` to reproduce its exact training
|
|
43
|
-
normalization. Returns ``(chw_float32, meta)``; ``meta`` is from
|
|
44
|
-
:func:`make_preprocess_meta`. The padded border (letterbox) is filled with 0 in
|
|
45
|
-
normalized space, i.e. the mean colour.
|
|
46
|
-
"""
|
|
47
|
-
from PIL import Image
|
|
48
|
-
|
|
49
|
-
mean = IMAGENET_MEAN if mean is None else np.asarray(mean, dtype=np.float32)
|
|
50
|
-
std = IMAGENET_STD if std is None else np.asarray(std, dtype=np.float32)
|
|
51
|
-
|
|
52
|
-
h, w = img_rgb_hwc.shape[:2]
|
|
53
|
-
if not letterbox:
|
|
54
|
-
img = Image.fromarray(img_rgb_hwc).resize((input_size, input_size), Image.BILINEAR)
|
|
55
|
-
arr = np.asarray(img, dtype=np.float32) / 255.0
|
|
56
|
-
arr = (arr - mean) / std
|
|
57
|
-
chw = np.ascontiguousarray(arr.transpose(2, 0, 1))
|
|
58
|
-
return chw, make_preprocess_meta(False, (input_size, input_size), input_size)
|
|
59
|
-
|
|
60
|
-
scale = input_size / max(h, w)
|
|
61
|
-
nh, nw = max(1, round(h * scale)), max(1, round(w * scale))
|
|
62
|
-
img = Image.fromarray(img_rgb_hwc).resize((nw, nh), Image.BILINEAR)
|
|
63
|
-
arr = (np.asarray(img, dtype=np.float32) / 255.0 - mean) / std # (nh, nw, 3)
|
|
64
|
-
canvas = np.zeros((input_size, input_size, 3), dtype=np.float32) # 0 == mean post-normalize
|
|
65
|
-
canvas[:nh, :nw] = arr
|
|
66
|
-
chw = np.ascontiguousarray(canvas.transpose(2, 0, 1))
|
|
67
|
-
return chw, make_preprocess_meta(True, (nh, nw), input_size)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|