trifecta_backend / app /preprocess.py
AmithAdiraju1694
Added better rounding off of patching.
1276079
Raw
History Blame Contribute Delete
5.35 kB
from __future__ import annotations
import gzip
import io
import tarfile
from typing import Iterable, List, Tuple
import cv2
import numpy as np
class ImageDecodeError(ValueError):
"""Raised when raw bytes cannot be decoded into a valid image."""
pass
def maybe_decompress(data: bytes, content_encoding: str | None) -> bytes:
"""Decompress request bytes when `Content-Encoding` indicates gzip."""
if not content_encoding:
return data
encoding = content_encoding.lower().strip()
if encoding == "gzip":
# gzip.decompress returns the original raw image/tar bytes.
return gzip.decompress(data)
return data
def decode_image_bytes(data: bytes) -> np.ndarray:
"""Decode encoded image bytes into an RGB `numpy.ndarray`."""
# Build a uint8 view over bytes for OpenCV decoding.
arr = np.frombuffer(data, dtype=np.uint8)
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
if img is None:
raise ImageDecodeError("Failed to decode image bytes")
# OpenCV decodes BGR by default; convert to RGB for model preprocessing.
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
return img
def load_webdataset_images(data: bytes) -> List[np.ndarray]:
"""Read a TAR archive and decode every file that looks like an image."""
images: List[np.ndarray] = []
with tarfile.open(fileobj=io.BytesIO(data), mode="r:*") as tar:
for member in tar.getmembers():
if not member.isfile():
continue
fileobj = tar.extractfile(member)
if fileobj is None:
continue
content = fileobj.read()
try:
images.append(decode_image_bytes(content))
# Skip non-image files in the TAR instead of failing all inputs.
except ImageDecodeError:
continue
return images
def resize_and_normalize(
img: np.ndarray,
input_size: int,
mean: Iterable[float],
std: Iterable[float],
) -> np.ndarray:
"""Resize an RGB image and normalize it into CHW float tensor format."""
# Resize each image to the fixed square resolution expected by the model.
resized = cv2.resize(img, (input_size, input_size), interpolation=cv2.INTER_LINEAR)
# Convert to [0,1] float values before channel-wise normalization.
tensor = resized.astype(np.float32) / 255.0
mean_arr = np.array(mean, dtype=np.float32)
std_arr = np.array(std, dtype=np.float32)
tensor = (tensor - mean_arr) / std_arr
# Rearrange from height-width-channel to channel-height-width.
tensor = np.transpose(tensor, (2, 0, 1))
return tensor
def prepare_batch(
images: List[np.ndarray],
input_size: int,
mean: Iterable[float],
std: Iterable[float],
) -> np.ndarray:
"""Convert a list of RGB images into one batched float32 tensor."""
# Preprocess each image independently, then stack into shape [B, C, H, W].
batch = [resize_and_normalize(img, input_size, mean, std) for img in images]
return np.stack(batch, axis=0).astype(np.float32)
def normalize_only(
img: np.ndarray,
mean: Iterable[float],
std: Iterable[float],
) -> np.ndarray:
"""Normalize an RGB image without resizing; returns CHW float32."""
tensor = img.astype(np.float32) / 255.0
mean_arr = np.array(mean, dtype=np.float32)
std_arr = np.array(std, dtype=np.float32)
tensor = (tensor - mean_arr) / std_arr
return np.transpose(tensor, (2, 0, 1)).astype(np.float32, copy=False)
def maybe_downscale_max_side(img: np.ndarray, max_side: int) -> tuple[np.ndarray, tuple[int, int]]:
"""Downscale an RGB image so max(H, W) <= max_side, preserving aspect ratio.
Returns (resized_img, (new_h, new_w)). If no resize is needed, returns the input image.
"""
if max_side <= 0:
return img, (int(img.shape[0]), int(img.shape[1]))
h, w = int(img.shape[0]), int(img.shape[1])
long_side = max(h, w)
if long_side <= max_side:
return img, (h, w)
scale = float(max_side) / float(long_side)
new_w = max(1, int(round(w * scale)))
new_h = max(1, int(round(h * scale)))
resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
return resized, (new_h, new_w)
def prepare_batch_dynamic(
images: List[np.ndarray],
mean: Iterable[float],
std: Iterable[float],
pad_to: int = 1,
) -> Tuple[np.ndarray, List[Tuple[int, int]]]:
"""Prepare a batch without forcing a fixed resize.
If images have different H/W, they are padded (bottom/right) to the max H/W in the batch.
Returns (batch, original_sizes) where sizes are (h, w) in pixels.
"""
if not images:
raise ValueError("images must be non-empty")
orig_sizes: List[Tuple[int, int]] = [(int(im.shape[0]), int(im.shape[1])) for im in images]
chw = [normalize_only(im, mean, std) for im in images]
max_h = max(x.shape[1] for x in chw)
max_w = max(x.shape[2] for x in chw)
if pad_to and pad_to > 1:
max_h = ((int(max_h) + int(pad_to) - 1) // int(pad_to)) * int(pad_to)
max_w = ((int(max_w) + int(pad_to) - 1) // int(pad_to)) * int(pad_to)
batch = np.zeros((len(chw), 3, max_h, max_w), dtype=np.float32)
for i, x in enumerate(chw):
h, w = x.shape[1], x.shape[2]
batch[i, :, :h, :w] = x
return batch, orig_sizes