Skip to content

Commit 2644149

Browse files
Address remaining PR review comments across workflows and tensor/model handling
1 parent 56389d3 commit 2644149

9 files changed

Lines changed: 118 additions & 31 deletions

File tree

.github/workflows/auto-release.yml

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,7 @@ jobs:
2323
- name: Read version
2424
id: version
2525
run: |
26-
VERSION=$(python3 -c "
27-
import re
28-
with open('pyproject.toml') as f:
29-
match = re.search(r'^version\s*=\s*\"(.+?)\"', f.read(), re.MULTILINE)
30-
print(match.group(1))
31-
")
26+
VERSION=$(python3 -c "import re; print(re.search(r'^version\s*=\s*\"(.+?)\"', open('pyproject.toml').read(), re.MULTILINE).group(1))")
3227
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
3328
echo "TAG=v$VERSION" >> $GITHUB_OUTPUT
3429
echo "VERSION=$VERSION" >> $GITHUB_ENV
@@ -106,10 +101,7 @@ jobs:
106101

107102
- name: Set VERSION variable
108103
run: |
109-
VERSION=$(python3 -c 'import re
110-
with open("pyproject.toml") as f:
111-
match = re.search(r"^version\s*=\s*\"(.+?)\"", f.read(), re.MULTILINE)
112-
print(match.group(1))')
104+
VERSION=$(python3 -c "import re; print(re.search(r'^version\s*=\s*\"(.+?)\"', open('pyproject.toml').read(), re.MULTILINE).group(1))")
113105
echo "VERSION=$VERSION" >> $GITHUB_ENV
114106
115107
- name: Login to Docker Hub

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ dependencies:
1010
- numpy>=1.18.2
1111
- pip>=20.0.2
1212
- python-json-logger>=2.0.0
13-
- pytorch-cpu>=2.0.0
13+
- pytorch-cpu>=2.3.0
1414
- requests>=2.20.0
1515
- timm>=0.6.0
1616
- torchvision>=0.15.0

facetorch/analyzer/core.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,19 +278,19 @@ def _read_input(
278278
image_tensor = numpy_to_chw_tensor(image_source)
279279
return self.reader.process_tensor(image_tensor, fix_img_size=fix_img_size)
280280

281-
opened_here = False
282281
if isinstance(image_source, bytes):
283-
image_source = Image.open(io.BytesIO(image_source))
284-
opened_here = True
282+
with Image.open(io.BytesIO(image_source)) as img:
283+
pil_image = img.convert("RGB") if img.mode != "RGB" else img.copy()
284+
image_tensor = torchvision.transforms.functional.pil_to_tensor(pil_image)
285+
return self.reader.process_tensor(image_tensor, fix_img_size=fix_img_size)
285286

286287
if isinstance(image_source, Image.Image):
287-
if image_source.mode != "RGB":
288-
image_source = image_source.convert("RGB")
289-
image_tensor = torchvision.transforms.functional.pil_to_tensor(
290-
image_source
288+
pil_image = (
289+
image_source.convert("RGB")
290+
if image_source.mode != "RGB"
291+
else image_source
291292
)
292-
if opened_here:
293-
image_source.close()
293+
image_tensor = torchvision.transforms.functional.pil_to_tensor(pil_image)
294294
return self.reader.process_tensor(image_tensor, fix_img_size=fix_img_size)
295295

296296
raise TypeError(

facetorch/analyzer/detector/core.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,24 @@ def run(self, data: ImageData) -> ImageData:
4646
Returns:
4747
ImageData: Image data object with Detection tensors and detected Face objects.
4848
"""
49-
img_h, img_w = data.tensor.shape[-2], data.tensor.shape[-1]
49+
orig_tensor = data.tensor
50+
img_h, img_w = orig_tensor.shape[-2], orig_tensor.shape[-1]
5051
data = self.preprocessor.run(data)
5152
logits = self.inference(data.tensor)
5253
data = self.postprocessor.run(data, logits)
5354

5455
if data.tensor.shape[-2] != img_h or data.tensor.shape[-1] != img_w:
55-
data.tensor = data.tensor[:, :, :img_h, :img_w]
56+
data.tensor = orig_tensor
5657
data.set_dims()
5758

59+
if hasattr(data.det, "dets") and data.det.dets.numel() > 0:
60+
data.det.dets[:, 0].clamp_(0, img_w)
61+
data.det.dets[:, 2].clamp_(0, img_w)
62+
data.det.dets[:, 1].clamp_(0, img_h)
63+
data.det.dets[:, 3].clamp_(0, img_h)
64+
65+
data.faces = []
66+
if hasattr(self.postprocessor, "_extract_faces"):
67+
data = self.postprocessor._extract_faces(data)
68+
5869
return data

facetorch/base.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,15 @@ def run(self, path: str) -> ImageData:
104104
pass
105105

106106
def process_tensor(self, tensor: torch.Tensor, fix_img_size: bool) -> ImageData:
107-
"""Read a tensor and return a data object containing a tensor of the image with
108-
shape (batch, channels, height, width).
107+
"""Read an input tensor and normalize it to shape (B, C, H, W).
109108
110109
Args:
111-
tensor (torch.Tensor): Image tensor with values between 0-255. Accepted shapes: (H, W), (C, H, W), or (B, C, H, W) where C in {1, 3, 4}. Single-channel inputs are expanded to 3 channels, RGBA inputs have the alpha channel dropped.
112-
fix_img_size (bool): Whether to resize the image to a fixed size. If False, the size_portrait and size_landscape are ignored. Default is False.
110+
tensor (torch.Tensor): Image tensor with values between 0-255. Accepted
111+
shapes are (H, W), (C, H, W), (H, W, C), or (B, C, H, W), where
112+
C is in {1, 3, 4}. Unambiguous HWC tensors are converted to CHW.
113+
Batched tensors currently support only B=1.
114+
fix_img_size (bool): Whether to resize the image to a fixed size. If
115+
False, size_portrait and size_landscape are ignored.
113116
"""
114117

115118
data = ImageData(path_input=None)
@@ -119,11 +122,43 @@ def process_tensor(self, tensor: torch.Tensor, fix_img_size: bool) -> ImageData:
119122
data.tensor = data.tensor.unsqueeze(0)
120123

121124
if data.tensor.dim() == 3:
125+
c0 = data.tensor.shape[0]
126+
c2 = data.tensor.shape[2]
127+
chw_like = c0 in (1, 3, 4)
128+
hwc_like = c2 in (1, 3, 4)
129+
if hwc_like and not chw_like:
130+
data.tensor = data.tensor.permute(2, 0, 1)
131+
elif not chw_like and not hwc_like:
132+
raise ValueError(
133+
"Invalid 3D tensor shape. Expected CHW with C in {1,3,4} or "
134+
"HWC with channels in the last dimension."
135+
)
136+
elif chw_like and hwc_like:
137+
raise ValueError(
138+
"Ambiguous 3D tensor layout: both first and last dimensions "
139+
"look like channel dimensions. Please pass CHW explicitly."
140+
)
122141
data.tensor = data.tensor.unsqueeze(0)
123142

124-
if data.tensor.shape[1] == 1:
143+
if data.tensor.dim() != 4:
144+
raise ValueError(
145+
f"Unsupported tensor rank {data.tensor.dim()}. Expected 2D, 3D, or 4D input."
146+
)
147+
148+
if data.tensor.shape[0] != 1:
149+
raise ValueError(
150+
f"Batched tensor input is not supported yet. Expected B=1, got B={data.tensor.shape[0]}."
151+
)
152+
153+
channels = data.tensor.shape[1]
154+
if channels not in (1, 3, 4):
155+
raise ValueError(
156+
f"Unsupported channel count: {channels}. Expected channels in {{1,3,4}}."
157+
)
158+
159+
if channels == 1:
125160
data.tensor = data.tensor.repeat(1, 3, 1, 1)
126-
elif data.tensor.shape[1] == 4:
161+
elif channels == 4:
127162
data.tensor = data.tensor[:, :3, :, :]
128163

129164
data.tensor = data.tensor.to(self.device)
@@ -265,13 +300,14 @@ def _load_exported_model(self) -> torch.nn.Module:
265300
if any(k in err_msg for k in ("schema version", "serialized version", "example_inputs")):
266301
raise RuntimeError(
267302
f"Cannot load {self.path_local}: the .pt2 model was exported with a "
268-
f"different PyTorch version. The bundled models require torch >=2.3.0,<2.4.0. "
303+
f"different PyTorch version. The bundled models require torch >=2.3.0,<2.5.0. "
269304
f"Current version: {torch.__version__}. Install a compatible version or "
270305
f"re-export the model with your current PyTorch."
271306
) from e
272307
raise
273308
model = ep.module()
274309
model.to(self.device)
310+
model.eval()
275311
return model
276312

277313
def _load_native_model(self) -> torch.nn.Module:

gpu.environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dependencies:
1111
- numpy>=1.18.2
1212
- pip>=20.0.2
1313
- python-json-logger>=2.0.0
14-
- pytorch-gpu>=2.0.0
14+
- pytorch-gpu>=2.3.0
1515
- requests>=2.20.0
1616
- timm>=0.6.0
1717
- torchvision>=0.15.0

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Documentation = "https://tomas-gajarsky.github.io/facetorch/facetorch/index.html
6363

6464
[tool.uv]
6565
extra-index-url = ["https://download.pytorch.org/whl/cpu"]
66-
constraint-dependencies = ["torch<2.4.0"]
66+
constraint-dependencies = ["torch<2.5.0"]
6767

6868
[tool.setuptools.packages.find]
6969
exclude = ["tests*", "model_defs*", "scripts*"]

tests/test_base_model.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,27 @@ def fake_download():
210210

211211
m = ConcreteModel(downloader=dl, device=torch.device("cpu"))
212212
assert m.model is not None
213+
214+
215+
def test_exported_model_is_on_device_and_eval(self, tmp_path):
216+
bad_pt2 = str(tmp_path / "model.pt2")
217+
with open(bad_pt2, "wb") as f:
218+
f.write(b"placeholder")
219+
dl = _make_dummy_downloader(bad_pt2)
220+
221+
class _FakeExported(torch.nn.Module):
222+
def __init__(self):
223+
super().__init__()
224+
self.linear = torch.nn.Linear(2, 2)
225+
226+
fake_model = _FakeExported()
227+
228+
class _EP:
229+
def module(self):
230+
return fake_model
231+
232+
with patch("torch.export.load", return_value=_EP()):
233+
m = ConcreteModel(downloader=dl, device=torch.device("cpu"))
234+
235+
assert m.model.training is False
236+
assert next(m.model.parameters()).device.type == "cpu"

tests/test_reader.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,27 @@ def test_process_tensor_rgba(analyzer):
245245
tensor_input = torch.randn(4, 224, 224)
246246
result = analyzer.reader.process_tensor(tensor_input, fix_img_size=False)
247247
assert result.tensor.size() == torch.Size([1, 3, 224, 224])
248+
249+
250+
@pytest.mark.unit
251+
@pytest.mark.reader
252+
def test_process_tensor_hwc_rgb(analyzer):
253+
tensor_input = torch.randn(224, 224, 3)
254+
result = analyzer.reader.process_tensor(tensor_input, fix_img_size=False)
255+
assert result.tensor.size() == torch.Size([1, 3, 224, 224])
256+
257+
258+
@pytest.mark.unit
259+
@pytest.mark.reader
260+
def test_process_tensor_batched_not_supported(analyzer):
261+
tensor_input = torch.randn(2, 3, 224, 224)
262+
with pytest.raises(ValueError, match="B=1"):
263+
analyzer.reader.process_tensor(tensor_input, fix_img_size=False)
264+
265+
266+
@pytest.mark.unit
267+
@pytest.mark.reader
268+
def test_process_tensor_ambiguous_3d_raises(analyzer):
269+
tensor_input = torch.randn(3, 224, 3)
270+
with pytest.raises(ValueError, match="Ambiguous 3D tensor layout"):
271+
analyzer.reader.process_tensor(tensor_input, fix_img_size=False)

0 commit comments

Comments
 (0)