Laguna-XS-2.1-Vision-NVFP4 / image_processing_laguna.py
webbrain-one's picture
Wire up multimodal integration: vision tower, projector, processor, vLLM path
2e40c5a verified
Raw
History Blame Contribute Delete
5.35 kB
# Copyright 2026 WebBrain and the HuggingFace Inc. team. All rights reserved.
#
# Adapted from moonshotai/Kimi-VL-A3B-Instruct's image_processing_kimi_vl.py
# (Apache-2.0), which this repository's MoonViT-3d tower is itself a frozen,
# fingerprint-verified copy of (see VISION_ADAPTER_MANIFEST.json).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Image processor for Laguna XS 2.1 Vision."""
import math
from typing import Optional, Union
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_list_of_images, valid_images
from transformers.utils import TensorType
# MoonViT-3d was trained with plain (0.5, 0.5, 0.5) normalization, not the
# OpenAI CLIP statistics — matches moonshotai/Kimi-K2.6's own preprocessing.
LAGUNA_VISION_MEAN = (0.5, 0.5, 0.5)
LAGUNA_VISION_STD = (0.5, 0.5, 0.5)
class LagunaImageProcessor(BaseImageProcessor):
model_type = "laguna"
def __init__(
self,
patch_size: int = 14,
pad_input: bool = True,
image_mean: tuple[float, float, float] = LAGUNA_VISION_MEAN,
image_std: tuple[float, float, float] = LAGUNA_VISION_STD,
in_token_limit: int = 2048,
merge_kernel_size: tuple[int, int] = (2, 2),
**kwargs,
):
super().__init__(**kwargs)
self.patch_size = patch_size
self.pad_input = pad_input
self.image_mean = image_mean
self.image_std = image_std
self.in_token_limit = in_token_limit
self.merge_kernel_size = merge_kernel_size
def rescale(self, image: Image.Image) -> Image.Image:
w, h = image.size
patch_size = self.patch_size
merge_h, merge_w = self.merge_kernel_size
if (w // patch_size) * (h // patch_size) > self.in_token_limit:
scale = math.sqrt(self.in_token_limit / ((w // patch_size) * (h // patch_size)))
new_w, new_h = max(1, int(w * scale)), max(1, int(h * scale))
image = image.resize((new_w, new_h), Image.Resampling.BICUBIC)
if self.pad_input:
new_w, new_h = image.size
pad_size_h = merge_h * patch_size
pad_size_w = merge_w * patch_size
pad_h = (pad_size_h - new_h % pad_size_h) % pad_size_h
pad_w = (pad_size_w - new_w % pad_size_w) % pad_size_w
image = TF.pad(image, (0, 0, pad_w, pad_h))
else:
new_w, new_h = image.size
new_w = new_w - new_w % patch_size
new_h = new_h - new_h % patch_size
image = TF.center_crop(image, (new_h, new_w))
w, h = image.size
if w // patch_size >= 512 or h // patch_size >= 512:
raise ValueError(
f"Image too large after rescale: {w}x{h} exceeds the vision tower's "
"512x512 patch-grid positional embedding range."
)
return image
def to_tensor(self, image: Image.Image) -> torch.Tensor:
return TF.to_tensor(image.convert("RGB"))
def normalize(self, image: torch.Tensor) -> torch.Tensor:
return TF.normalize(image, self.image_mean, self.image_std)
def patchify(self, image: torch.Tensor) -> tuple[torch.Tensor, tuple[int, int]]:
patch_size = self.patch_size
c, h, w = image.shape
patches = image.reshape(c, h // patch_size, patch_size, w // patch_size, patch_size)
patches = patches.permute(1, 3, 0, 2, 4).contiguous().view(-1, c, patch_size, patch_size)
return patches, (h // patch_size, w // patch_size)
def _preprocess(self, image: ImageInput) -> tuple[torch.Tensor, tuple[int, int]]:
image = self.rescale(image)
image = self.to_tensor(image)
image = self.normalize(image)
return self.patchify(image)
def preprocess(
self,
images: ImageInput,
return_tensors: Optional[Union[str, TensorType]] = None,
**kwargs,
) -> BatchFeature:
images = make_list_of_images(images)
if not valid_images(images):
raise ValueError(
"Invalid image type. Must be of type PIL.Image.Image, numpy.ndarray, "
"torch.Tensor, tf.Tensor or jax.ndarray."
)
pixel_values, image_grid_hws = [], []
for image in images:
patches, grid_hw = self._preprocess(image)
pixel_values.append(patches)
image_grid_hws.append(grid_hw)
pixel_values = torch.cat(pixel_values, dim=0)
image_grid_hws = np.array(image_grid_hws)
data = {"pixel_values": pixel_values, "image_grid_hws": image_grid_hws}
return BatchFeature(data=data, tensor_type=return_tensors)
__all__ = ["LagunaImageProcessor"]