InternVL-MoonViT-V2-MiniCPM5-2B-HF / image_processing_moonvit.py
PerRing's picture
Add files using upload-large-folder tool
56d2875 verified
Raw History Blame Contribute Delete
5.8 kB
# MoonViT native-resolution image processor (port of the moonshotai/MoonViT-SO-400M and
# AI4Industry/MoonViT-V2 processors, which differ only in their limits).
#
# Output:
# pixel_values (total_patches, 3, patch_size, patch_size)
# image_grid_thws (num_images, 3) as (frames, height, width) in patches; frames is 1 for images
#
# `in_token_limit` counts ViT input tokens (14x14 patches, before the 2x2 merge), so the number of
# tokens the LLM sees is at most in_token_limit / 4.
import math
import numpy as np
import torch
from PIL import Image
from torchvision.transforms import functional as TF
from transformers.image_processing_utils import BaseImageProcessor, BatchFeature
from transformers.image_utils import ImageInput, make_flat_list_of_images
class MoonViTImageProcessor(BaseImageProcessor):
model_input_names = ["pixel_values", "image_grid_thws"]
def __init__(
self,
patch_size: int = 14,
merge_kernel_size: int | list[int] | tuple[int, int] = (2, 2),
in_token_limit: int = 4096,
patch_limit_on_one_side: int | None = None,
max_num_frames: int = 1,
pad_input: bool = True,
image_mean: list[float] | tuple[float, ...] = (0.5, 0.5, 0.5),
image_std: list[float] | tuple[float, ...] = (0.5, 0.5, 0.5),
**kwargs,
):
super().__init__(**kwargs)
self.patch_size = patch_size
if isinstance(merge_kernel_size, int):
merge_kernel_size = (merge_kernel_size, merge_kernel_size)
self.merge_kernel_size = list(merge_kernel_size)
# Max ViT tokens (14x14 patches before the 2x2 merge) per image; the image is downscaled to fit.
self.in_token_limit = in_token_limit
# Max patches along one side (MoonViT-V2: 512, i.e. 7168px); None disables the per-side cap.
self.patch_limit_on_one_side = patch_limit_on_one_side
# Video frames covered by the temporal position embedding (MoonViT-V2: 4).
self.max_num_frames = max_num_frames
# True: pad right/bottom to a multiple of patch_size * merge_kernel_size; False: center-crop to it.
self.pad_input = pad_input
self.image_mean = list(image_mean)
self.image_std = list(image_std)
def resize_and_align(self, image: Image.Image) -> Image.Image:
w, h = image.size
p = self.patch_size
scale = min(1.0, math.sqrt(self.in_token_limit / (max(1, w // p) * max(1, h // p))))
if self.patch_limit_on_one_side is not None:
side = self.patch_limit_on_one_side * p
scale = min(scale, side / w, side / h)
if scale < 1.0:
image = image.resize((max(1, int(w * scale)), max(1, int(h * scale))), Image.Resampling.BICUBIC)
w, h = image.size
unit_h, unit_w = self.merge_kernel_size[0] * p, self.merge_kernel_size[1] * p
if self.pad_input:
image = TF.pad(image, [0, 0, (unit_w - w % unit_w) % unit_w, (unit_h - h % unit_h) % unit_h])
else:
image = TF.center_crop(image, [max(h - h % unit_h, unit_h), max(w - w % unit_w, unit_w)])
return image
def get_grid_thw(self, height: int, width: int, num_frames: int = 1) -> tuple[int, int, int]:
"""Grid (in patches) that `resize_and_align` produces for an image of this size."""
w, h = self.resize_and_align(Image.new("RGB", (width, height))).size
return num_frames, h // self.patch_size, w // self.patch_size
def _to_pil(self, image) -> Image.Image:
if isinstance(image, Image.Image):
return image.convert("RGB")
if isinstance(image, torch.Tensor):
image = image.cpu().numpy()
image = np.asarray(image)
if image.ndim == 3 and image.shape[0] in (1, 3) and image.shape[-1] not in (1, 3):
image = image.transpose(1, 2, 0)
if image.dtype != np.uint8:
image = (image * 255 if image.max() <= 1.0 else image).clip(0, 255).astype(np.uint8)
return Image.fromarray(image).convert("RGB")
def _patchify(self, frames: list[Image.Image]) -> tuple[torch.Tensor, list[int]]:
p = self.patch_size
x = torch.stack([TF.normalize(TF.to_tensor(f), self.image_mean, self.image_std) for f in frames])
t, c, h, w = x.shape
patches = x.reshape(t, c, h // p, p, w // p, p).permute(0, 2, 4, 1, 3, 5).reshape(-1, c, p, p)
return patches, [t, h // p, w // p]
def preprocess(self, images: ImageInput, return_tensors=None, **kwargs) -> BatchFeature:
pixel_values, grid_thws = [], []
for image in make_flat_list_of_images(images):
patches, grid = self._patchify([self.resize_and_align(self._to_pil(image))])
pixel_values.append(patches)
grid_thws.append(grid)
data = {"pixel_values": torch.cat(pixel_values), "image_grid_thws": torch.tensor(grid_thws, dtype=torch.long)}
return BatchFeature(data=data, tensor_type=return_tensors)
def preprocess_video(self, frames, return_tensors=None) -> BatchFeature:
"""One video = one sample with T frames; all frames share the first frame's resize/pad."""
frames = [self._to_pil(f) for f in frames]
if not 1 <= len(frames) <= self.max_num_frames:
raise ValueError(f"Got {len(frames)} frames, but max_num_frames={self.max_num_frames}")
w, h = frames[0].size
target = self.resize_and_align(frames[0]).size
frames = [self.resize_and_align(f) if f.size == (w, h) else f.resize(target) for f in frames]
patches, grid = self._patchify(frames)
data = {"pixel_values": patches, "image_grid_thws": torch.tensor([grid], dtype=torch.long)}
return BatchFeature(data=data, tensor_type=return_tensors)
__all__ = ["MoonViTImageProcessor"]