Spaces:
Sleeping
Sleeping
| 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 | |