| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """ |
| 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) |