From 5e89efba646f5a0966679b3d888a3ba0d102ab43 Mon Sep 17 00:00:00 2001 From: Yosi Taguri Date: Mon, 27 Apr 2026 18:14:34 +0300 Subject: [PATCH] fix(ml): handle empty/corrupt images in face detection (#27391) * fix(ml): handle empty/corrupt images in face detection When a corrupt or degenerate image with zero-dimension (0 width or 0 height) reaches the face detection pipeline, insightface's RetinaFace.detect() calls cv2.resize() with a target size of 0, triggering an OpenCV assertion failure: error: (-215:Assertion failed) inv_scale_x > 0 in function 'resize' This crashes the ML worker and returns a 500 error to the server. Add an early return in FaceDetector._predict() that checks for zero-dimension images after decoding and returns empty detection results instead of passing them to the insightface model. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(ml): move empty image validation to request level Per review feedback, validate image dimensions in the predict endpoint (returning 400) rather than in each model's _predict method. This catches all zero-dimension images before they reach any model task. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(ml): resolve mypy strict type error in predict endpoint Use intermediate `decoded` variable so mypy knows `.width` and `.height` are accessed on `Image`, not on `Image | str`. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- machine-learning/immich_ml/main.py | 5 ++++- machine-learning/test_main.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/machine-learning/immich_ml/main.py b/machine-learning/immich_ml/main.py index e7e3a719bb..4fca7a2e2b 100644 --- a/machine-learning/immich_ml/main.py +++ b/machine-learning/immich_ml/main.py @@ -183,7 +183,10 @@ async def predict( text: str | None = Form(default=None), ) -> Any: if image is not None: - inputs: Image | str = await run(lambda: decode_pil(image)) + decoded = await run(lambda: decode_pil(image)) + if decoded.width == 0 or decoded.height == 0: + raise HTTPException(400, "Image has zero width or height") + inputs: Image | str = decoded elif text is not None: inputs = text else: diff --git a/machine-learning/test_main.py b/machine-learning/test_main.py index 0182c57c67..cce334e40e 100644 --- a/machine-learning/test_main.py +++ b/machine-learning/test_main.py @@ -1198,6 +1198,19 @@ class TestLoad: mock_model.model_format = ModelFormat.ONNX +@pytest.mark.parametrize("size", [(0, 100), (100, 0), (0, 0)]) +def test_predict_rejects_empty_image(size: tuple[int, int], deployed_app: TestClient) -> None: + with mock.patch("immich_ml.main.decode_pil", return_value=Image.new("RGB", size)): + response = deployed_app.post( + "http://localhost:3003/predict", + data={"entries": json.dumps({"clip": {"visual": {"modelName": "ViT-B-32__openai"}}})}, + files={"image": b"fake image bytes"}, + ) + + assert response.status_code == 400 + assert "zero" in response.json()["detail"].lower() + + def test_root_endpoint(deployed_app: TestClient) -> None: response = deployed_app.get("http://localhost:3003")