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.
Files changed (76) hide show
  1. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/PKG-INFO +1 -1
  2. attr_eomt-1.2.1/eomt/preprocess.py +144 -0
  3. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/tta.py +9 -5
  4. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/pyproject.toml +1 -1
  5. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_smoke.py +19 -0
  6. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_tta.py +9 -0
  7. attr_eomt-1.1.4/eomt/preprocess.py +0 -67
  8. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.github/workflows/pages.yml +0 -0
  9. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.github/workflows/publish.yml +0 -0
  10. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.github/workflows/release.yml +0 -0
  11. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.github/workflows/tests.yml +0 -0
  12. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/.gitignore +0 -0
  13. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/LICENSE +0 -0
  14. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/README.md +0 -0
  15. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/configs/coco.yaml +0 -0
  16. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/assets/00-hero-banner.png +0 -0
  17. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/assets/00-hero-banner.svg +0 -0
  18. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/examples/billie_eilish.jpg +0 -0
  19. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/examples/rosalia.jpg +0 -0
  20. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/examples/sample.jpg +0 -0
  21. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/docs/examples/taylor_swift.jpg +0 -0
  22. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/__init__.py +0 -0
  23. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/api.py +0 -0
  24. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/aux_cls.py +0 -0
  25. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/box_loss.py +0 -0
  26. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/compress.py +0 -0
  27. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/config.py +0 -0
  28. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/__init__.py +0 -0
  29. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/coco.py +0 -0
  30. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/config.py +0 -0
  31. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/download.py +0 -0
  32. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/data/transforms.py +0 -0
  33. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/device.py +0 -0
  34. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/ema.py +0 -0
  35. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/__init__.py +0 -0
  36. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/distributed.py +0 -0
  37. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/match.py +0 -0
  38. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/predict.py +0 -0
  39. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/track.py +0 -0
  40. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/train.py +0 -0
  41. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/engine/validate.py +0 -0
  42. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/loss.py +0 -0
  43. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/model.py +0 -0
  44. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/plotting.py +0 -0
  45. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/postprocess.py +0 -0
  46. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/reid.py +0 -0
  47. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/serialization.py +0 -0
  48. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/eomt/visualize.py +0 -0
  49. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/README.md +0 -0
  50. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/annotations/instances_train.json +0 -0
  51. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/annotations/instances_val.json +0 -0
  52. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/data.yaml +0 -0
  53. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/images/train/.gitkeep +0 -0
  54. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/sample_data/images/val/.gitkeep +0 -0
  55. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/info.py +0 -0
  56. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/match.py +0 -0
  57. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/predict.py +0 -0
  58. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/push_to_hub.py +0 -0
  59. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/track.py +0 -0
  60. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/train.py +0 -0
  61. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/train_detect_coco.sh +0 -0
  62. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/scripts/val.py +0 -0
  63. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/site/index.html +0 -0
  64. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_api.py +0 -0
  65. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_architecture.py +0 -0
  66. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_augment.py +0 -0
  67. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_compress.py +0 -0
  68. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_ddp.py +0 -0
  69. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_parity.py +0 -0
  70. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_recipe.py +0 -0
  71. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_regression_attr.py +0 -0
  72. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_reid.py +0 -0
  73. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tests/test_sweep.py +0 -0
  74. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tools/attr_tagger/README.md +0 -0
  75. {attr_eomt-1.1.4 → attr_eomt-1.2.1}/tools/attr_tagger/tag.py +0 -0
  76. {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.4
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
- preprocess_numpy(_crop(image, views[i]), size, letterbox=letterbox, mean=mean, std=std) for i in chunk
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.4"
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