mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
93 lines
3.1 KiB
Python
93 lines
3.1 KiB
Python
import string
|
|
from io import BytesIO
|
|
|
|
import cv2
|
|
import numpy as np
|
|
import orjson
|
|
from numpy.typing import NDArray
|
|
from PIL import Image
|
|
|
|
_PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling}
|
|
_PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation)
|
|
|
|
|
|
def resize_pil(img: Image.Image, size: int, resample: Image.Resampling = Image.Resampling.BICUBIC) -> Image.Image:
|
|
if img.width < img.height:
|
|
return img.resize((size, int((img.height / img.width) * size)), resample=resample)
|
|
return img.resize((int((img.width / img.height) * size), size), resample=resample)
|
|
|
|
|
|
# https://stackoverflow.com/a/60883103
|
|
def crop_pil(img: Image.Image, size: int) -> Image.Image:
|
|
left = int((img.size[0] / 2) - (size / 2))
|
|
upper = int((img.size[1] / 2) - (size / 2))
|
|
right = left + size
|
|
lower = upper + size
|
|
|
|
return img.crop((left, upper, right, lower))
|
|
|
|
|
|
def to_numpy(img: Image.Image) -> NDArray[np.float32]:
|
|
return np.asarray(img if img.mode == "RGB" else img.convert("RGB"), dtype=np.float32) / 255.0
|
|
|
|
|
|
def normalize(
|
|
img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32]
|
|
) -> NDArray[np.float32]:
|
|
img *= 1.0 / std
|
|
img -= mean / std
|
|
return img
|
|
|
|
|
|
def get_pil_resampling(resample: str) -> Image.Resampling:
|
|
return _PIL_RESAMPLING_METHODS[resample.lower()]
|
|
|
|
|
|
def pil_to_cv2(image: Image.Image) -> NDArray[np.uint8]:
|
|
return cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) # type: ignore
|
|
|
|
|
|
def decode_pil(image_bytes: bytes | Image.Image | NDArray[np.uint8]) -> Image.Image:
|
|
image: Image.Image
|
|
match image_bytes:
|
|
case Image.Image():
|
|
image = image_bytes
|
|
case np.ndarray():
|
|
image = Image.fromarray(image_bytes)
|
|
case bytes():
|
|
image = Image.open(BytesIO(image_bytes))
|
|
image.load()
|
|
if not image.mode == "RGB":
|
|
image = image.convert("RGB")
|
|
return image
|
|
|
|
|
|
def clean_text(text: str, canonicalize: bool = False) -> str:
|
|
text = " ".join(text.split())
|
|
if canonicalize:
|
|
text = text.translate(_PUNCTUATION_TRANS).lower()
|
|
return text
|
|
|
|
|
|
# this allows the client to use the array as a string without deserializing only to serialize back to a string
|
|
# TODO: use this in a less invasive way
|
|
def serialize_np_array(arr: NDArray[np.float32]) -> str:
|
|
return orjson.dumps(arr, option=orjson.OPT_SERIALIZE_NUMPY).decode()
|
|
|
|
|
|
def letterbox(image: NDArray[np.uint8] | Image.Image, size: int) -> tuple[NDArray[np.uint8], float]:
|
|
if isinstance(image, Image.Image):
|
|
image = np.asarray(image)
|
|
height, width = image.shape[:2]
|
|
if height > width:
|
|
new_height, new_width = size, int(size * width / height)
|
|
else:
|
|
new_width, new_height = size, int(size * height / width)
|
|
|
|
canvas = np.zeros((size, size, 3), dtype=np.uint8)
|
|
cv2.resize(image, (new_width, new_height), dst=canvas[:new_height, :new_width])
|
|
return canvas, new_height / height
|
|
|
|
|
|
def ensure_dims(array: NDArray[np.float32], ndim: int) -> NDArray[np.float32]:
|
|
return array if array.ndim >= ndim else np.expand_dims(array, axis=tuple(range(ndim - array.ndim)))
|