|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""" |
|
Processor class for Spec-Vision. |
|
""" |
|
|
|
import re |
|
from typing import List, Optional, Union |
|
|
|
import numpy as np |
|
import torch |
|
import torchvision |
|
from PIL import Image |
|
from transformers import AutoImageProcessor |
|
from transformers.feature_extraction_utils import BatchFeature |
|
from transformers.image_processing_utils import BaseImageProcessor |
|
from transformers.image_transforms import convert_to_rgb |
|
from transformers.image_utils import (OPENAI_CLIP_MEAN, OPENAI_CLIP_STD, |
|
ImageInput, make_list_of_images, |
|
valid_images) |
|
from transformers.processing_utils import ProcessorMixin |
|
from transformers.tokenization_utils_base import (PaddingStrategy, TextInput, |
|
TruncationStrategy) |
|
from transformers.utils import TensorType, is_vision_available, logging |
|
|
|
logger = logging.get_logger(__name__) |
|
|
|
def padding_336(image): |
|
"""Apply padding to make height a multiple of 336 while preserving aspect ratio.""" |
|
width, height = image.size |
|
target_height = int(np.ceil(height / 336) * 336) |
|
top_padding = int((target_height - height) / 2) |
|
bottom_padding = target_height - height - top_padding |
|
padded_image = torchvision.transforms.functional.pad( |
|
image, |
|
[0, top_padding, 0, bottom_padding], |
|
fill=[255, 255, 255] |
|
) |
|
return padded_image |
|
|
|
def calc_padded_size(width, height, padding_unit=336): |
|
"""Calculate the padded dimensions for an image.""" |
|
target_height = int(np.ceil(height / padding_unit) * padding_unit) |
|
padded_width = width |
|
padded_height = target_height |
|
return padded_width, padded_height |
|
|
|
def hd_transform(img, hd_num=16): |
|
"""Apply HD transformation with support for Spec-Vision's requirements.""" |
|
width, height = img.size |
|
transposed = False |
|
|
|
|
|
if width < height: |
|
img = img.transpose(Image.TRANSPOSE) |
|
width, height = img.size |
|
transposed = True |
|
|
|
ratio = width / height |
|
scale = 1 |
|
while scale * np.ceil(scale / ratio) <= hd_num: |
|
scale += 1 |
|
scale -= 1 |
|
|
|
new_width = int(scale * 336) |
|
new_height = int(new_width / ratio) |
|
|
|
|
|
img = torchvision.transforms.functional.resize(img, [new_height, new_width]) |
|
img = padding_336(img) |
|
|
|
|
|
if transposed: |
|
img = img.transpose(Image.TRANSPOSE) |
|
|
|
return img |
|
|
|
def pad_to_max_crops(images, max_crops=5): |
|
"""Pad batch of images to have consistent number of crops.""" |
|
B, _, H, W = images.shape |
|
if B < max_crops: |
|
padding = torch.zeros(max_crops - B, 3, H, W, dtype=images.dtype, device=images.device) |
|
images = torch.cat([images, padding], dim=0) |
|
return images |
|
|
|
class SpecVisionImageProcessor(BaseImageProcessor): |
|
""" |
|
Image processor for Spec-Vision model. |
|
|
|
This processor handles the preparation of images for the Spec-Vision model, including: |
|
- HD transformation for high-resolution image processing |
|
- Multi-crop processing with configurable number of crops |
|
- Normalization and padding |
|
""" |
|
|
|
model_input_names = ["pixel_values"] |
|
|
|
def __init__( |
|
self, |
|
num_crops: int = 1, |
|
image_mean: Optional[Union[float, List[float]]] = None, |
|
image_std: Optional[Union[float, List[float]]] = None, |
|
do_convert_rgb: bool = True, |
|
hd_transform_order: str = "sub_glb", |
|
**kwargs, |
|
) -> None: |
|
super().__init__(**kwargs) |
|
self.num_crops = num_crops |
|
self.image_mean = image_mean if image_mean is not None else OPENAI_CLIP_MEAN |
|
self.image_std = image_std if image_std is not None else OPENAI_CLIP_STD |
|
self.do_convert_rgb = do_convert_rgb |
|
self.hd_transform_order = hd_transform_order |
|
|
|
def calc_num_image_tokens(self, images: ImageInput) -> List[int]: |
|
"""Calculate number of image tokens needed for each image.""" |
|
images = make_list_of_images(images) |
|
if not valid_images(images): |
|
raise ValueError("Invalid image type provided") |
|
|
|
images = [image.convert('RGB') for image in images] |
|
transformed_images = [hd_transform(im, hd_num=self.num_crops) for im in images] |
|
shapes = [[im.size[1], im.size[0]] for im in transformed_images] |
|
|
|
|
|
num_img_tokens = [ |
|
int((h//336 * w//336 + 1) * 144 + 1 + (h//336 + 1) * 12) |
|
for h, w in shapes |
|
] |
|
return num_img_tokens |
|
|
|
def preprocess( |
|
self, |
|
images: ImageInput, |
|
image_mean: Optional[Union[float, List[float]]] = None, |
|
image_std: Optional[Union[float, List[float]]] = None, |
|
do_convert_rgb: bool = None, |
|
return_tensors: Optional[Union[str, TensorType]] = None, |
|
) -> BatchFeature: |
|
""" |
|
Preprocess images for the Spec-Vision model. |
|
|
|
Handles HD transformation, normalization, and proper formatting of images |
|
according to Spec-Vision's requirements. |
|
""" |
|
image_mean = image_mean if image_mean is not None else self.image_mean |
|
image_std = image_std if image_std is not None else self.image_std |
|
do_convert_rgb = do_convert_rgb if do_convert_rgb is not None else self.do_convert_rgb |
|
|
|
|
|
images = make_list_of_images(images) |
|
if not valid_images(images): |
|
raise ValueError("Invalid image type provided") |
|
|
|
if do_convert_rgb: |
|
images = [convert_to_rgb(image) for image in images] |
|
|
|
|
|
img_processor = torchvision.transforms.Compose([ |
|
torchvision.transforms.ToTensor(), |
|
torchvision.transforms.Normalize(image_mean, image_std) |
|
]) |
|
|
|
|
|
images = [image.convert('RGB') for image in images] |
|
transformed_images = [hd_transform(im, hd_num=self.num_crops) for im in images] |
|
|
|
|
|
hd_images = [img_processor(im) for im in transformed_images] |
|
|
|
|
|
global_images = [ |
|
torch.nn.functional.interpolate( |
|
im.unsqueeze(0).float(), |
|
size=(336, 336), |
|
mode='bicubic' |
|
).to(im.dtype) |
|
for im in hd_images |
|
] |
|
|
|
|
|
shapes = [[im.size(1), im.size(2)] for im in hd_images] |
|
num_img_tokens = [ |
|
int(((h//336) * (w//336) + 1) * 144 + 1 + (h//336 + 1) * 12) |
|
for h, w in shapes |
|
] |
|
|
|
|
|
hd_images_reshaped = [ |
|
im.reshape(1, 3, h//336, 336, w//336, 336) |
|
.permute(0, 2, 4, 1, 3, 5) |
|
.reshape(-1, 3, 336, 336) |
|
.contiguous() |
|
for im, (h, w) in zip(hd_images, shapes) |
|
] |
|
|
|
|
|
if self.hd_transform_order == "sub_glb": |
|
processed_images = [ |
|
torch.cat([_im, _global_image], dim=0) |
|
for _global_image, _im in zip(global_images, hd_images_reshaped) |
|
] |
|
else: |
|
processed_images = [ |
|
torch.cat([_global_image, _im], dim=0) |
|
for _global_image, _im in zip(global_images, hd_images_reshaped) |
|
] |
|
|
|
|
|
image_batch = [ |
|
pad_to_max_crops(im, self.num_crops + 1) |
|
for im in processed_images |
|
] |
|
image_batch = torch.stack(image_batch, dim=0) |
|
|
|
return BatchFeature( |
|
data={ |
|
"pixel_values": image_batch, |
|
"image_sizes": shapes, |
|
"num_img_tokens": num_img_tokens |
|
}, |
|
tensor_type=return_tensors |
|
) |
|
|
|
class SpecVisionProcessor(ProcessorMixin): |
|
""" |
|
Combined processor for Spec-Vision model, handling both image and text inputs. |
|
|
|
Combines SpecVisionImageProcessor for images and a tokenizer for text, |
|
coordinating their interaction for multi-modal inputs. |
|
""" |
|
|
|
attributes = ["image_processor", "tokenizer"] |
|
image_processor_class = "SpecVisionImageProcessor" |
|
tokenizer_class = ("LlamaTokenizer", "LlamaTokenizerFast") |
|
special_image_token = "<|image|>" |
|
|
|
def __init__(self, image_processor, tokenizer): |
|
self.image_processor = image_processor |
|
self.tokenizer = tokenizer |
|
self.num_img_tokens = image_processor.num_crops |
|
self.img_tokens = [f"<|image_{i+1}|>" for i in range(1000000)] |
|
|
|
def __call__( |
|
self, |
|
text: Union[TextInput, List[TextInput]], |
|
images: ImageInput = None, |
|
padding: Union[bool, str, PaddingStrategy] = False, |
|
truncation: Union[bool, str, TruncationStrategy] = None, |
|
max_length=None, |
|
return_tensors: Optional[Union[str, TensorType]] = TensorType.PYTORCH, |
|
) -> BatchFeature: |
|
"""Process both text and image inputs for the model.""" |
|
if images is not None: |
|
image_features = self.image_processor(images, return_tensors=return_tensors) |
|
else: |
|
image_features = {} |
|
|
|
|
|
inputs = self._process_multimodal_inputs( |
|
image_features, |
|
text, |
|
padding=padding, |
|
truncation=truncation, |
|
max_length=max_length, |
|
return_tensors=return_tensors |
|
) |
|
|
|
return inputs |
|
|
|
def _process_multimodal_inputs(self, images, texts, **kwargs): |
|
"""Process and combine image and text inputs.""" |
|
if not images: |
|
return BatchFeature(data=self.tokenizer( |
|
texts, |
|
return_tensors=kwargs.get('return_tensors'), |
|
padding=kwargs.get('padding'), |
|
truncation=kwargs.get('truncation'), |
|
max_length=kwargs.get('max_length') |
|
)) |
|
|
|
|
|
pattern = r"<\|image_\d+\|>" |
|
text_chunks = [ |
|
self.tokenizer(chunk).input_ids |
|
for chunk in re.split(pattern, texts) |
|
] |
|
|
|
|
|
num_img_tokens = ( |
|
images['num_img_tokens'] |
|
if 'num_img_tokens' in images |
|
else [self.num_img_tokens] * len(images['pixel_values']) |
|
) |
|
|
|
image_tags = re.findall(pattern, texts) |
|
image_ids = [int(tag.split("|")[1].split("_")[-1]) for tag in image_tags] |
|
|
|
|
|
unique_ids = sorted(set(image_ids)) |
|
if unique_ids != list(range(1, len(unique_ids) + 1)): |
|
raise ValueError( |
|
f"Image IDs must be consecutive integers starting from 1, got {unique_ids}" |
|
) |
|
if len(unique_ids) != len(images['pixel_values']): |
|
raise ValueError( |
|
f"Number of image tags ({len(unique_ids)}) doesn't match " |
|
f"number of images ({len(images['pixel_values'])})" |
|
) |
|
|
|
|
|
image_ids_padded = [ |
|
[-iid] * num_img_tokens[iid-1] |
|
for iid in image_ids |
|
] |
|
|
|
|
|
input_ids = [] |
|
for x in self._interleave_sequences(text_chunks, image_ids_padded): |
|
input_ids.extend(x) |
|
|
|
input_ids = torch.tensor(input_ids, dtype=torch.long).unsqueeze(0) |
|
attention_mask = (input_ids > -1000000).to(torch.long) |
|
|
|
return BatchFeature(data={ |
|
"input_ids": input_ids, |
|
"attention_mask": attention_mask, |
|
"pixel_values": images['pixel_values'], |
|
"image_sizes": images['image_sizes'] |
|
}) |
|
|
|
def _interleave_sequences(self, seq1, seq2): |
|
"""Interleave two sequences, padding second sequence if needed.""" |
|
if len(seq1) > len(seq2): |
|
seq2.append([]) |
|
return [item for pair in zip(seq1, seq2) for item in pair] |
|
|
|
def batch_decode(self, *args, **kwargs): |
|
"""Decode a batch of token IDs to text.""" |
|
return self.tokenizer.batch_decode(*args, **kwargs) |
|
|
|
def decode(self, *args, **kwargs): |
|
"""Decode token IDs to text.""" |
|
return self.tokenizer.decode(*args, **kwargs) |
|
|
|
@property |
|
def model_input_names(self): |
|
"""Get combined input names from both processors.""" |
|
return list(dict.fromkeys( |
|
self.tokenizer.model_input_names + |
|
self.image_processor.model_input_names |
|
)) |
|
|
|
|
|
AutoImageProcessor.register("SpecVisionImageProcessor", SpecVisionImageProcessor) |