processing_idefics3.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. # coding=utf-8
  2. # Copyright 2024 The HuggingFace Inc. team.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """
  16. Processor class for Idefics3.
  17. """
  18. import re
  19. from itertools import accumulate
  20. from typing import TYPE_CHECKING, Optional, Union
  21. import numpy as np
  22. from ...feature_extraction_utils import BatchFeature
  23. from ...image_utils import ImageInput, is_valid_image, load_image
  24. from ...processing_utils import ImagesKwargs, MultiModalData, ProcessingKwargs, ProcessorMixin, Unpack
  25. from ...tokenization_utils_base import AddedToken, BatchEncoding, TextInput
  26. from ...utils import logging
  27. if TYPE_CHECKING:
  28. from ...tokenization_utils_base import PreTokenizedInput
  29. logger = logging.get_logger(__name__)
  30. def is_url(val) -> bool:
  31. return isinstance(val, str) and val.startswith("http")
  32. def is_image_or_image_url(elem):
  33. return is_url(elem) or is_valid_image(elem)
  34. def _prompt_split_image(image_seq_len, image_rows, image_cols, fake_token_around_image, image_token, global_img_token):
  35. """Prompt with expanded image tokens for when the image is split into patches."""
  36. text_split_images = ""
  37. for n_h in range(image_rows):
  38. for n_w in range(image_cols):
  39. text_split_images += (
  40. f"{fake_token_around_image}" + f"<row_{n_h + 1}_col_{n_w + 1}>" + f"{image_token}" * image_seq_len
  41. )
  42. text_split_images += "\n"
  43. text_split_images += (
  44. f"\n{fake_token_around_image}"
  45. + f"{global_img_token}"
  46. + f"{image_token}" * image_seq_len
  47. + f"{fake_token_around_image}"
  48. )
  49. return text_split_images
  50. def _prompt_single_image(image_seq_len, fake_token_around_image, image_token, global_img_token):
  51. """Prompt with expanded image tokens for a single image."""
  52. return (
  53. f"{fake_token_around_image}"
  54. + f"{global_img_token}"
  55. + f"{image_token}" * image_seq_len
  56. + f"{fake_token_around_image}"
  57. )
  58. def get_image_prompt_string(
  59. image_rows, image_cols, image_seq_len, fake_token_around_image, image_token, global_img_token
  60. ):
  61. if image_rows == 0 and image_cols == 0:
  62. return _prompt_single_image(
  63. image_seq_len,
  64. fake_token_around_image=fake_token_around_image,
  65. image_token=image_token,
  66. global_img_token=global_img_token,
  67. )
  68. return _prompt_split_image(
  69. image_seq_len, image_rows, image_cols, fake_token_around_image, image_token, global_img_token
  70. )
  71. class Idefics3ImagesKwargs(ImagesKwargs, total=False):
  72. return_row_col_info: Optional[bool]
  73. max_image_size: Optional[dict[str, int]]
  74. class Idefics3ProcessorKwargs(ProcessingKwargs, total=False):
  75. images_kwargs: Idefics3ImagesKwargs
  76. _defaults = {
  77. "text_kwargs": {
  78. "add_special_tokens": True,
  79. "padding": False,
  80. "is_split_into_words": False,
  81. "return_mm_token_type_ids": False,
  82. },
  83. "images_kwargs": {
  84. "return_row_col_info": True,
  85. },
  86. }
  87. class Idefics3Processor(ProcessorMixin):
  88. r"""
  89. Constructs a Idefics3 processor which wraps a LLama tokenizer and Idefics3 image processor into a single processor.
  90. [`Idefics3Processor`] offers all the functionalities of [`Idefics3ImageProcessor`] and [`Idefics3TokenizerFast`]. See
  91. the docstring of [`~IdeficsProcessor.__call__`] and [`~IdeficsProcessor.decode`] for more information.
  92. Args:
  93. image_processor (`Idefics3ImageProcessor`):
  94. An instance of [`Idefics3ImageProcessor`]. The image processor is a required input.
  95. tokenizer (`PreTrainedTokenizerBase`, *optional*):
  96. An instance of [`PreTrainedTokenizerBase`]. This should correspond with the model's text model. The tokenizer is a required input.
  97. image_seq_len (`int`, *optional*, defaults to 169):
  98. The length of the image sequence i.e. the number of <image> tokens per image in the input.
  99. This parameter is used to build the string from the input prompt and image tokens and should match the
  100. value the model used. It is computed as: image_seq_len = int(((image_size // patch_size) ** 2) / (scale_factor**2))
  101. chat_template (`str`, *optional*): A Jinja template which will be used to convert lists of messages
  102. in a chat into a tokenizable string.
  103. """
  104. attributes = ["image_processor", "tokenizer"]
  105. image_processor_class = "Idefics3ImageProcessor"
  106. tokenizer_class = "AutoTokenizer"
  107. def __init__(
  108. self, image_processor, tokenizer=None, image_seq_len: int = 169, chat_template: Optional[str] = None, **kwargs
  109. ):
  110. self.fake_image_token = AddedToken("<fake_token_around_image>", normalized=False, special=True).content
  111. self.image_token = AddedToken("<image>", normalized=False, special=True).content
  112. self.end_of_utterance_token = AddedToken("<end_of_utterance>", normalized=False, special=True).content
  113. self.global_image_tag = "<global-img>" # https://github.com/huggingface/transformers/pull/32473/files/8063e5e17362571b693f1db95167f5443a3be1b2#r1734825341
  114. self.image_seq_len = image_seq_len
  115. self.image_token_id = tokenizer.convert_tokens_to_ids(self.image_token)
  116. self.fake_image_token_id = tokenizer.convert_tokens_to_ids(self.fake_image_token)
  117. self.global_image_token_id = tokenizer.convert_tokens_to_ids(self.global_image_tag)
  118. self.row_col_ids = [
  119. tokenizer.convert_tokens_to_ids(f"<row_{i + 1}_col_{j + 1}>") for i in range(6) for j in range(6)
  120. ]
  121. # This regex matches one or more occurrences of <global-img> tags (optionally surrounded by newline characters)
  122. # or <row_x_col_y> tags (where x and y are digits, also optionally surrounded by newline characters).
  123. self._regex_to_remove_extra_special_tokens = re.compile(r"(\n?<global-img>\n?|<row_\d+_col_\d+>\n?)+")
  124. tokens_to_add = {
  125. "additional_special_tokens": [
  126. self.fake_image_token,
  127. self.image_token,
  128. self.end_of_utterance_token,
  129. ]
  130. }
  131. tokenizer.add_special_tokens(tokens_to_add)
  132. self.image_token_id = tokenizer.convert_tokens_to_ids(self.image_token)
  133. super().__init__(image_processor, tokenizer, chat_template=chat_template, **kwargs)
  134. def _extract_images_from_prompts(self, prompts):
  135. prompt_images = []
  136. for prompt in prompts:
  137. images = []
  138. for elem in prompt:
  139. if is_valid_image(elem):
  140. images.append(elem)
  141. elif is_url(elem):
  142. images.append(load_image(elem))
  143. prompt_images.append(images)
  144. return prompt_images
  145. def __call__(
  146. self,
  147. images: Union[ImageInput, list[ImageInput], list[list[ImageInput]]] = None,
  148. text: Union[TextInput, "PreTokenizedInput", list[TextInput], list["PreTokenizedInput"]] = None,
  149. audio=None,
  150. videos=None,
  151. image_seq_len: Optional[int] = None,
  152. **kwargs: Unpack[Idefics3ProcessorKwargs],
  153. ) -> BatchEncoding:
  154. """
  155. Processes the input prompts and returns a BatchEncoding.
  156. Example:
  157. ```python
  158. >>> import requests
  159. >>> from transformers import Idefics3Processor
  160. >>> from transformers.image_utils import load_image
  161. >>> processor = Idefics3Processor.from_pretrained("HuggingFaceM4/Idefics3-8B-Llama3")
  162. >>> processor.image_processor.do_image_splitting = False # Force as False to simplify the example
  163. >>> url1 = "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg"
  164. >>> url2 = "https://cdn.britannica.com/59/94459-050-DBA42467/Skyline-Chicago.jpg"
  165. >>> image1, image2 = load_image(url1), load_image(url2)
  166. >>> images = [[image1], [image2]]
  167. >>> text = [
  168. ... "<image>In this image, we see",
  169. ... "bla bla bla<image>",
  170. ... ]
  171. >>> outputs = processor(images=images, text=text, return_tensors="pt", padding=True)
  172. >>> input_ids = outputs.input_ids
  173. >>> input_tokens = processor.tokenizer.batch_decode(input_ids)
  174. >>> print(input_tokens)
  175. ['<|begin_of_text|><fake_token_around_image><global-img>((<image>)*169)<fake_token_around_image> In this image, we see', '<|reserved_special_token_0|><|reserved_special_token_0|><|reserved_special_token_0|><|begin_of_text|>bla bla bla<fake_token_around_image><global-img>((<image>)*169)<fake_token_around_image>']
  176. ```
  177. Args:
  178. images (`PIL.Image.Image`, `np.ndarray`, `torch.Tensor`, `list[PIL.Image.Image]`, `list[np.ndarray]`, `list[torch.Tensor]`, *optional*):
  179. The image or batch of images to be prepared. Each image can be a PIL image, NumPy array or PyTorch
  180. tensor. If is of type `list[ImageInput]`, it's assumed that this is for a single prompt i.e. of batch size 1.
  181. text (`Union[TextInput, PreTokenizedInput, list[TextInput], list[PreTokenizedInput]]`, *optional*):
  182. The sequence or batch of sequences to be encoded. Each sequence can be a string or a list of strings
  183. (pretokenized string). If the sequences are provided as list of strings (pretokenized), you must set
  184. `is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
  185. Wherever an image token, `<image>` is encountered it is expanded to
  186. `<fake_token_around_image>` + `<row_x_col_y>` + `<image>` * `image_seq_len` * <fake_token_around_image>`.
  187. image_seq_len (`int`, *optional*):
  188. The length of the image sequence. If not provided, the default value of self.image_seq_len is used.
  189. image_seq_len should be equal to int(((image_size // patch_size) ** 2) / (scale_factor**2))
  190. return_tensors (`Union[str, TensorType]`, *optional*):
  191. If set, will return tensors of a particular framework. See [`PreTrainedTokenizerFast.__call__`] for more
  192. information.
  193. """
  194. if text is None and images is None:
  195. raise ValueError("You must provide either `text` or `images`.")
  196. output_kwargs = self._merge_kwargs(
  197. Idefics3ProcessorKwargs,
  198. tokenizer_init_kwargs=self.tokenizer.init_kwargs,
  199. **kwargs,
  200. )
  201. image_seq_len = image_seq_len if image_seq_len is not None else self.image_seq_len
  202. return_mm_token_type_ids = output_kwargs["text_kwargs"].pop("return_mm_token_type_ids", False)
  203. return_tensors = output_kwargs["text_kwargs"].pop("return_tensors", None)
  204. n_images_in_text = []
  205. n_images_in_images = []
  206. inputs = {}
  207. if text is not None:
  208. if isinstance(text, str):
  209. text = [text]
  210. elif not isinstance(text, list) and not isinstance(text[0], str):
  211. raise ValueError("Invalid input text. Please provide a string, or a list of strings")
  212. n_images_in_text = [sample.count(self.image_token) for sample in text]
  213. if images is not None:
  214. if is_image_or_image_url(images):
  215. images = [[images]]
  216. elif isinstance(images, (list, tuple)) and is_image_or_image_url(images[0]):
  217. if text is not None:
  218. if sum(n_images_in_text) != len(images):
  219. raise ValueError(
  220. f"The total number of {self.image_token} tokens in the prompts should be the same as the number of images passed."
  221. f" Found {sum(n_images_in_text)} {self.image_token} tokens and {len(images)} images."
  222. )
  223. # Reorganize the images to match the prompts
  224. cumsum_images_in_text = [0] + list(accumulate(n_images_in_text))
  225. images = [
  226. images[cumsum_images_in_text[i] : cumsum_images_in_text[i + 1]]
  227. for i in range(len(n_images_in_text))
  228. ]
  229. else:
  230. images = [images]
  231. elif (
  232. not isinstance(images, (list, tuple))
  233. and not isinstance(images[0], (list, tuple))
  234. and not is_image_or_image_url(images[0][0])
  235. ):
  236. raise ValueError(
  237. "Invalid input images. Please provide a single image or a list of images or a list of list of images."
  238. )
  239. n_images_in_images = [len(sample) for sample in images]
  240. # Load images if they are URLs
  241. images = [[load_image(im) if is_url(im) else im for im in sample] for sample in images]
  242. image_inputs = self.image_processor(images, **output_kwargs["images_kwargs"])
  243. inputs.update(image_inputs)
  244. if text is not None:
  245. if n_images_in_images != n_images_in_text:
  246. raise ValueError(
  247. f"The number of images in the text {n_images_in_text} and images {n_images_in_images} should be the same."
  248. )
  249. image_rows = inputs.pop("rows", [[0] * len(text)])
  250. image_cols = inputs.pop("cols", [[0] * len(text)])
  251. fake_image_token = self.fake_image_token
  252. image_token = self.image_token
  253. global_img_token = self.global_image_tag
  254. prompt_strings = []
  255. batch_image_seq_lengths = []
  256. for sample, sample_rows, sample_cols in zip(text, image_rows, image_cols):
  257. # Replace the image token with fake tokens around the expanded image token sequence of length `image_seq_len`
  258. image_prompt_strings = []
  259. image_seq_lengths = []
  260. for n_rows, n_cols in zip(sample_rows, sample_cols):
  261. image_prompt_string = get_image_prompt_string(
  262. n_rows,
  263. n_cols,
  264. image_seq_len,
  265. image_token=image_token,
  266. fake_token_around_image=fake_image_token,
  267. global_img_token=global_img_token,
  268. )
  269. # Add +2 and +3 for special BOI/EOI/fake_image_wrapper tokens
  270. row_length = (self.image_seq_len + 2) * n_cols + 1
  271. image_seq_lengths.append((self.image_seq_len + 3) + row_length * n_rows)
  272. image_prompt_strings.append(image_prompt_string)
  273. batch_image_seq_lengths.append(image_seq_lengths)
  274. split_sample = sample.split(image_token)
  275. if len(split_sample) == 0:
  276. raise ValueError("The image token should be present in the text.")
  277. # Place in the image prompt strings where the image tokens are
  278. sample = split_sample[0]
  279. for i, image_prompt_string in enumerate(image_prompt_strings):
  280. sample += image_prompt_string + split_sample[i + 1]
  281. prompt_strings.append(sample)
  282. text_inputs = self.tokenizer(prompt_strings, **output_kwargs["text_kwargs"])
  283. self._check_special_mm_tokens(prompt_strings, text_inputs, modalities=["image"])
  284. inputs.update(text_inputs)
  285. elif text is not None:
  286. if any(n_images_in_text):
  287. raise ValueError(
  288. f"Found {sum(n_images_in_text)} {self.image_token} tokens in the text but no images were passed."
  289. )
  290. text_inputs = self.tokenizer(text=text, **output_kwargs["text_kwargs"])
  291. inputs.update(text_inputs)
  292. if return_mm_token_type_ids:
  293. array_ids = np.array(inputs["input_ids"])
  294. mm_token_type_ids = np.zeros_like(array_ids)
  295. for i, seq_lengths in enumerate(batch_image_seq_lengths):
  296. image_start_positions = np.where(array_ids[i] == self.fake_image_token_id)[0]
  297. j = 0
  298. for seq_len in seq_lengths:
  299. if j >= len(image_start_positions):
  300. break
  301. start = image_start_positions[j]
  302. end = start + seq_len
  303. mm_token_type_ids[i, start:end] = 1
  304. j = np.searchsorted(image_start_positions, end)
  305. inputs["mm_token_type_ids"] = mm_token_type_ids.tolist()
  306. return BatchFeature(data=inputs, tensor_type=return_tensors)
  307. def _get_num_multimodal_tokens(self, image_sizes=None, **kwargs):
  308. """
  309. Computes the number of placeholder tokens needed for multimodal inputs with the given sizes.
  310. Args:
  311. image_sizes (`list[list[int]]`, *optional*):
  312. The input sizes formatted as (height, width) per each image.
  313. Returns:
  314. `MultiModalData`: A `MultiModalData` object holding number of tokens per each of the provided
  315. input modalities, along with other useful data.
  316. """
  317. vision_data = {}
  318. if image_sizes is not None:
  319. images_kwargs = Idefics3ProcessorKwargs._defaults.get("images_kwargs", {})
  320. images_kwargs.update(kwargs)
  321. num_image_row_cols = [
  322. self.image_processor.get_number_of_image_patches(*image_size, images_kwargs)
  323. for image_size in image_sizes
  324. ]
  325. base_image_length = self.image_seq_len + 3
  326. col_length = self.image_seq_len + 2
  327. num_image_tokens = []
  328. num_image_patches = []
  329. for num_patches, num_rows, num_cols in num_image_row_cols:
  330. row_length = col_length * num_cols + 1
  331. num_image_tokens.append(base_image_length + (row_length * num_rows))
  332. num_image_patches.append(num_patches)
  333. vision_data.update({"num_image_tokens": num_image_tokens, "num_image_patches": num_image_patches})
  334. return MultiModalData(**vision_data)
  335. __all__ = ["Idefics3Processor"]