File size: 2,009 Bytes
a2f55b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"""
Image decoding.

In the new architecture, each detector does its own tensor preprocessing
(CLIP needs CLIP's specific resize/normalise; frequency detectors don't
resize at all). This module just turns raw bytes into a validated PIL Image
that all detectors can consume.
"""
from __future__ import annotations

from io import BytesIO

from PIL import Image, ImageOps, UnidentifiedImageError

# Pillow's default decompression-bomb guard is ~89 MP. Tighten it: a 25 MP
# image is more than enough for any consumer photo, and rejecting larger
# inputs caps memory usage on the inference server.
Image.MAX_IMAGE_PIXELS = 25_000_000


class InvalidImageError(ValueError):
    """Raised when the bytes do not decode as a usable image."""


def decode(image_bytes: bytes) -> Image.Image:
    """Decode raw bytes into an RGB PIL image. Raises InvalidImageError on failure."""
    try:
        image = Image.open(BytesIO(image_bytes))
        image.load()  # Force decode now so we catch errors here, not later.
    except (UnidentifiedImageError, OSError) as exc:
        raise InvalidImageError(f"Could not decode image: {exc}") from exc
    except Image.DecompressionBombError as exc:
        raise InvalidImageError(f"Image too large: {exc}") from exc

    # Phone cameras almost always store the sensor readout unrotated and record
    # the intended rotation in the EXIF Orientation tag — 9 of the first 13 real
    # test photos (Galaxy S23 Ultra) came through as orientation 6, i.e. stored
    # sideways. Every viewer applies that tag, so a user who uploads a portrait
    # photo sees it upright and assumes we scanned it upright. Without this call
    # the detector was seeing it rotated 90°, which is both wrong for users and
    # would have silently corrupted the P0 false-positive measurement.
    # exif_transpose is a no-op when the tag is absent or already upright.
    image = ImageOps.exif_transpose(image)

    if image.mode != "RGB":
        image = image.convert("RGB")

    return image