attr-eomt 1.2.0__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 (75) hide show
  1. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/PKG-INFO +1 -1
  2. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/preprocess.py +52 -7
  3. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/pyproject.toml +1 -1
  4. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_smoke.py +13 -8
  5. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_tta.py +1 -1
  6. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/.github/workflows/pages.yml +0 -0
  7. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/.github/workflows/publish.yml +0 -0
  8. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/.github/workflows/release.yml +0 -0
  9. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/.github/workflows/tests.yml +0 -0
  10. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/.gitignore +0 -0
  11. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/LICENSE +0 -0
  12. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/README.md +0 -0
  13. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/configs/coco.yaml +0 -0
  14. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/docs/assets/00-hero-banner.png +0 -0
  15. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/docs/assets/00-hero-banner.svg +0 -0
  16. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/docs/examples/billie_eilish.jpg +0 -0
  17. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/docs/examples/rosalia.jpg +0 -0
  18. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/docs/examples/sample.jpg +0 -0
  19. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/docs/examples/taylor_swift.jpg +0 -0
  20. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/__init__.py +0 -0
  21. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/api.py +0 -0
  22. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/aux_cls.py +0 -0
  23. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/box_loss.py +0 -0
  24. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/compress.py +0 -0
  25. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/config.py +0 -0
  26. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/data/__init__.py +0 -0
  27. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/data/coco.py +0 -0
  28. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/data/config.py +0 -0
  29. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/data/download.py +0 -0
  30. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/data/transforms.py +0 -0
  31. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/device.py +0 -0
  32. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/ema.py +0 -0
  33. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/engine/__init__.py +0 -0
  34. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/engine/distributed.py +0 -0
  35. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/engine/match.py +0 -0
  36. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/engine/predict.py +0 -0
  37. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/engine/track.py +0 -0
  38. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/engine/train.py +0 -0
  39. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/engine/validate.py +0 -0
  40. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/loss.py +0 -0
  41. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/model.py +0 -0
  42. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/plotting.py +0 -0
  43. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/postprocess.py +0 -0
  44. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/reid.py +0 -0
  45. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/serialization.py +0 -0
  46. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/tta.py +0 -0
  47. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/eomt/visualize.py +0 -0
  48. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/sample_data/README.md +0 -0
  49. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/sample_data/annotations/instances_train.json +0 -0
  50. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/sample_data/annotations/instances_val.json +0 -0
  51. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/sample_data/data.yaml +0 -0
  52. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/sample_data/images/train/.gitkeep +0 -0
  53. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/sample_data/images/val/.gitkeep +0 -0
  54. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/scripts/info.py +0 -0
  55. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/scripts/match.py +0 -0
  56. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/scripts/predict.py +0 -0
  57. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/scripts/push_to_hub.py +0 -0
  58. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/scripts/track.py +0 -0
  59. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/scripts/train.py +0 -0
  60. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/scripts/train_detect_coco.sh +0 -0
  61. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/scripts/val.py +0 -0
  62. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/site/index.html +0 -0
  63. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_api.py +0 -0
  64. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_architecture.py +0 -0
  65. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_augment.py +0 -0
  66. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_compress.py +0 -0
  67. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_ddp.py +0 -0
  68. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_parity.py +0 -0
  69. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_recipe.py +0 -0
  70. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_regression_attr.py +0 -0
  71. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_reid.py +0 -0
  72. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tests/test_sweep.py +0 -0
  73. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tools/attr_tagger/README.md +0 -0
  74. {attr_eomt-1.2.0 → attr_eomt-1.2.1}/tools/attr_tagger/tag.py +0 -0
  75. {attr_eomt-1.2.0 → 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.2.0
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
@@ -13,10 +13,14 @@ needs to map masks back to the original image (see :func:`make_preprocess_meta`)
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import functools
17
+
16
18
  import numpy as np
17
19
 
18
20
  IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
19
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
20
24
 
21
25
 
22
26
  def make_preprocess_meta(letterbox: bool, content_hw: tuple[int, int], input_size: int) -> dict:
@@ -67,6 +71,47 @@ def preprocess_numpy(
67
71
  return chw, make_preprocess_meta(True, (nh, nw), input_size)
68
72
 
69
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
+
70
115
  def preprocess_torch(
71
116
  img_rgb_hwc: np.ndarray,
72
117
  input_size: int,
@@ -76,13 +121,12 @@ def preprocess_torch(
76
121
  std=None,
77
122
  device="cpu",
78
123
  ):
79
- """:func:`preprocess_numpy` run with torch on ``device`` (e.g. the model's GPU).
124
+ """:func:`preprocess_numpy` run with torch on ``device`` (e.g. the model's GPU), with the same result.
80
125
 
81
- The resize is an antialiased bilinear one rounded to uint8, as PIL's, so the result matches
82
- :func:`preprocess_numpy` within one grey level. Returns ``(chw_float32_tensor, meta)``.
126
+ The resize reproduces PIL's fixed-point bilinear one, so the output equals :func:`preprocess_numpy`'s.
127
+ Returns ``(chw_float32_tensor, meta)``.
83
128
  """
84
129
  import torch
85
- import torch.nn.functional as F # noqa: N812
86
130
 
87
131
  h, w = img_rgb_hwc.shape[:2]
88
132
  if letterbox:
@@ -90,10 +134,11 @@ def preprocess_torch(
90
134
  nh, nw = max(1, round(h * scale)), max(1, round(w * scale))
91
135
  else:
92
136
  nh, nw = input_size, input_size
93
- x = torch.from_numpy(np.ascontiguousarray(img_rgb_hwc)).to(device).permute(2, 0, 1)[None].float()
94
- x = F.interpolate(x, size=(nh, nw), mode="bilinear", antialias=True, align_corners=False).round_()
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()
95
139
  mean = torch.as_tensor(IMAGENET_MEAN if mean is None else mean, dtype=torch.float32, device=device).view(3, 1, 1)
96
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)
97
142
  chw = x.new_zeros((3, input_size, input_size)) # 0 == mean post-normalize
98
- chw[:, :nh, :nw] = (x[0] / 255.0 - mean) / std
143
+ chw[:, :nh, :nw] = (x / levels - mean) / std
99
144
  return chw, make_preprocess_meta(letterbox, (nh, nw), input_size)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "attr-eomt"
7
- version = "1.2.0"
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,18 +514,23 @@ 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"))])
517
519
  @pytest.mark.parametrize("letterbox", [True, False])
518
- def test_preprocess_torch_matches_preprocess_numpy_within_one_grey_level(letterbox):
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."""
519
523
  import numpy as np
520
524
 
521
- from eomt.preprocess import IMAGENET_STD, preprocess_numpy, preprocess_torch
525
+ from eomt.preprocess import preprocess_numpy, preprocess_torch
522
526
 
523
- img = np.random.default_rng(0).integers(0, 255, (97, 151, 3), dtype=np.uint8)
524
- chw, meta = preprocess_torch(img, 64, letterbox=letterbox)
525
- expected, expected_meta = preprocess_numpy(img, 64, letterbox=letterbox)
526
- assert chw.shape == (3, 64, 64) and chw.dtype == torch.float32 and meta == expected_meta
527
- diff = np.abs(chw.numpy() - expected) * IMAGENET_STD.reshape(3, 1, 1) * 255 # in grey levels
528
- assert diff.max() <= 1.01 and diff.mean() < 0.25
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)
529
534
 
530
535
 
531
536
  def test_letterbox_inverse_crops_content_not_padding():
@@ -178,7 +178,7 @@ def test_device_preprocessed_views_find_the_same_instances():
178
178
  cpu = predict_views(_Blobs(), img, cfg, imgsz=IMGSZ, conf_thres=0.3)
179
179
  dev = predict_views(_Blobs(), img, cfg, imgsz=IMGSZ, conf_thres=0.3, device_preprocess=True)
180
180
  assert dev["num_detections"] == cpu["num_detections"] == 1
181
- assert torch.allclose(dev["scores"], cpu["scores"]) and _same(dev["masks"], cpu["masks"])
181
+ assert torch.equal(dev["scores"], cpu["scores"]) and torch.equal(dev["masks"], cpu["masks"])
182
182
 
183
183
 
184
184
  def test_predict_image_with_tta_keeps_the_result_contract():
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