chantera commited on
Commit
eae16bc
·
verified ·
1 Parent(s): 407c7e9

Upload processing_heron.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. processing_heron.py +64 -50
processing_heron.py CHANGED
@@ -1,7 +1,9 @@
1
  from collections.abc import Mapping
 
2
  from typing import Unpack, cast
3
 
4
  import numpy as np
 
5
  from PIL.Image import Image
6
  from transformers.feature_extraction_utils import BatchFeature
7
  from transformers.image_processing_utils import BaseImageProcessor
@@ -72,14 +74,16 @@ class HeronProcessor(ProcessorMixin):
72
  images = [to_pil_image(image) for image in images]
73
 
74
  image_inputs: Mapping = {}
 
75
  if images is not None:
76
- image_inputs = self._process_images(images, **output_kwargs["images_kwargs"])
77
- text = self._insert_media_placeholders(cast(list, text), image_inputs["pixel_values"])
 
 
78
 
79
  return_tensors = output_kwargs["text_kwargs"].pop("return_tensors", None)
80
  return_mm_token_type_ids = output_kwargs["text_kwargs"].pop("return_mm_token_type_ids", None)
81
- text_inputs = self._process_text(text, **output_kwargs["text_kwargs"])
82
- self._check_special_mm_tokens(text, text_inputs, modalities=["image"]) # type: ignore[arg-type]
83
 
84
  if return_mm_token_type_ids:
85
  array_ids = np.array(text_inputs["input_ids"])
@@ -89,45 +93,42 @@ class HeronProcessor(ProcessorMixin):
89
 
90
  return BatchFeature(data={**text_inputs, **image_inputs}, tensor_type=return_tensors)
91
 
92
- def _insert_media_placeholders(self, text: list[str], image_pixel_values: list, separator="\n") -> list[str]:
93
- image_feature_placeholder = self.image_token * self.num_image_features
 
 
 
 
 
 
94
 
95
- # NOTE: Original implementation appends a newline after image features.
 
96
  # https://github.com/NVlabs/VILA/blob/36f6adcd11a10be1580caeb7e647e1b6f8517f89/llava/model/encoders/image/basic.py#L38
97
  # Instead of appending a newline character, this implementation reserves 1 extra token to be replaced later.
98
  # This treatment is needed because "\n\n" is tokenized as [271] instead of [198, 198].
99
  assert self.tokenizer.eos_token is not None
100
  image_feature_placeholder += self.tokenizer.eos_token
101
 
 
102
  processed_text = []
103
- image_iter = iter(image_pixel_values)
104
  for prompt in text:
105
  new_prompt = prompt
106
  assert "<placeholder>" not in new_prompt
107
  replace_strings = []
108
  while self.image_token in new_prompt:
109
- try:
110
- image_tiles = next(image_iter)
111
- except StopIteration:
112
- raise ValueError("insufficient number of images provided for the prompts") from None
113
- replace_strings.append(separator.join([image_feature_placeholder] * len(image_tiles)))
114
  new_prompt = new_prompt.replace(self.image_token, "<placeholder>", 1)
115
  for s in replace_strings:
116
  new_prompt = new_prompt.replace("<placeholder>", s, 1)
117
  processed_text.append(new_prompt)
118
 
119
- if next(image_iter, None) is not None:
120
- raise ValueError("excessive number of images provided for the prompts")
121
- return processed_text
122
 
123
- def _process_text(self, text: list[str], **kwargs: Unpack[TextKwargs]) -> BatchEncoding:
124
  token_ids = self.tokenizer.encode("\n")
125
  assert len(token_ids) == 1
126
  newline_token_id = token_ids[0]
127
-
128
- encoding = self.tokenizer(text, **kwargs)
129
-
130
- # Replace the last token of every image tile with the newline token
131
  for input_ids in encoding.input_ids:
132
  i, n = 0, len(input_ids)
133
  while i < n:
@@ -138,13 +139,14 @@ class HeronProcessor(ProcessorMixin):
138
  input_ids[i] = newline_token_id
139
  i += 1
140
 
 
141
  return encoding
142
 
143
- def _process_images(
144
  self,
145
  images: list[Image],
146
- min_tiles: int,
147
- max_tiles: int,
148
  **kwargs: Unpack[ImagesKwargs],
149
  ) -> BatchFeature:
150
  assert isinstance(min_tiles, int) and isinstance(max_tiles, int)
@@ -156,19 +158,38 @@ class HeronProcessor(ProcessorMixin):
156
  pixel_values = []
157
  for image in images:
158
  image_tiles = _dynamic_preprocess(
159
- image.convert("RGB"), min_num=min_tiles, max_num=max_tiles, image_size=crop_size["height"]
160
  )
161
- pixel_values.append(self.image_processor(image_tiles, **kwargs)["pixel_values"])
162
 
163
- return BatchFeature({"pixel_values": pixel_values}, tensor_type=return_tensors)
164
 
165
 
166
- # Copied from https://github.com/NVlabs/VILA/blob/36f6adcd11a10be1580caeb7e647e1b6f8517f89/llava/mm_utils.py#L296
167
  def _dynamic_preprocess(
168
  image: Image, min_num: int, max_num: int, image_size: int, use_thumbnail: bool = True
169
  ) -> list[Image]:
 
 
 
170
  orig_width, orig_height = image.size
171
- aspect_ratio = orig_width / orig_height
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  # calculate the existing image aspect ratio
174
  target_ratio_set = {
@@ -176,7 +197,7 @@ def _dynamic_preprocess(
176
  for n in range(min_num, max_num + 1)
177
  for i in range(1, n + 1)
178
  for j in range(1, n + 1)
179
- if i * j <= max_num and i * j >= min_num
180
  }
181
  target_ratios = sorted(target_ratio_set, key=lambda x: x[0] * x[1])
182
 
@@ -184,34 +205,27 @@ def _dynamic_preprocess(
184
  target_aspect_ratio = _find_closest_aspect_ratio(
185
  aspect_ratio,
186
  target_ratios,
187
- orig_width,
188
- orig_height,
189
- image_size,
190
  )
191
 
192
  # calculate the target width and height
193
- target_width = image_size * target_aspect_ratio[0]
194
- target_height = image_size * target_aspect_ratio[1]
195
  blocks = target_aspect_ratio[0] * target_aspect_ratio[1]
196
 
197
- # resize the image
198
- resized_img = image.resize((target_width, target_height))
199
- processed_images = []
200
  for i in range(blocks):
201
  box = (
202
- (i % (target_width // image_size)) * image_size,
203
- (i // (target_width // image_size)) * image_size,
204
- ((i % (target_width // image_size)) + 1) * image_size,
205
- ((i // (target_width // image_size)) + 1) * image_size,
206
  )
207
- # split the image
208
- split_img = resized_img.crop(box)
209
- processed_images.append(split_img)
210
- assert len(processed_images) == blocks
211
- if use_thumbnail and len(processed_images) != 1:
212
- thumbnail_img = image.resize((image_size, image_size))
213
- processed_images.append(thumbnail_img)
214
- return processed_images
215
 
216
 
217
  # Copied from https://github.com/NVlabs/VILA/blob/36f6adcd11a10be1580caeb7e647e1b6f8517f89/llava/mm_utils.py#L280
 
1
  from collections.abc import Mapping
2
+ from functools import lru_cache
3
  from typing import Unpack, cast
4
 
5
  import numpy as np
6
+ import torch
7
  from PIL.Image import Image
8
  from transformers.feature_extraction_utils import BatchFeature
9
  from transformers.image_processing_utils import BaseImageProcessor
 
74
  images = [to_pil_image(image) for image in images]
75
 
76
  image_inputs: Mapping = {}
77
+ num_image_tiles = None
78
  if images is not None:
79
+ if sum(s.count(self.image_token) for s in text) != len(images):
80
+ raise ValueError("the number of images does not match the number of image tokens in the text")
81
+ image_inputs = self.process_images(images, **output_kwargs["images_kwargs"])
82
+ num_image_tiles = image_inputs["pixel_values"].shape[1]
83
 
84
  return_tensors = output_kwargs["text_kwargs"].pop("return_tensors", None)
85
  return_mm_token_type_ids = output_kwargs["text_kwargs"].pop("return_mm_token_type_ids", None)
86
+ text_inputs = self.process_text(text, num_image_tiles, **output_kwargs["text_kwargs"])
 
87
 
88
  if return_mm_token_type_ids:
89
  array_ids = np.array(text_inputs["input_ids"])
 
93
 
94
  return BatchFeature(data={**text_inputs, **image_inputs}, tensor_type=return_tensors)
95
 
96
+ def process_text(
97
+ self, text: list[str], num_image_tiles: int | None = None, **kwargs: Unpack[TextKwargs]
98
+ ) -> BatchEncoding:
99
+ if all(self.image_token not in prompt for prompt in text):
100
+ return self.tokenizer(text, **kwargs)
101
+
102
+ if num_image_tiles is None:
103
+ raise ValueError("num_image_tiles must be specified when processing image tokens")
104
 
105
+ image_feature_placeholder = self.image_token * self.num_image_features
106
+ # NOTE: Original implementation appends an extra newline after image features.
107
  # https://github.com/NVlabs/VILA/blob/36f6adcd11a10be1580caeb7e647e1b6f8517f89/llava/model/encoders/image/basic.py#L38
108
  # Instead of appending a newline character, this implementation reserves 1 extra token to be replaced later.
109
  # This treatment is needed because "\n\n" is tokenized as [271] instead of [198, 198].
110
  assert self.tokenizer.eos_token is not None
111
  image_feature_placeholder += self.tokenizer.eos_token
112
 
113
+ # Expand image tokens according to the number of image features and tiles
114
  processed_text = []
 
115
  for prompt in text:
116
  new_prompt = prompt
117
  assert "<placeholder>" not in new_prompt
118
  replace_strings = []
119
  while self.image_token in new_prompt:
120
+ replace_strings.append("\n".join([image_feature_placeholder] * num_image_tiles))
 
 
 
 
121
  new_prompt = new_prompt.replace(self.image_token, "<placeholder>", 1)
122
  for s in replace_strings:
123
  new_prompt = new_prompt.replace("<placeholder>", s, 1)
124
  processed_text.append(new_prompt)
125
 
126
+ encoding = self.tokenizer(processed_text, **kwargs)
 
 
127
 
128
+ # Replace the last token of every image tile with the newline token
129
  token_ids = self.tokenizer.encode("\n")
130
  assert len(token_ids) == 1
131
  newline_token_id = token_ids[0]
 
 
 
 
132
  for input_ids in encoding.input_ids:
133
  i, n = 0, len(input_ids)
134
  while i < n:
 
139
  input_ids[i] = newline_token_id
140
  i += 1
141
 
142
+ self._check_special_mm_tokens(processed_text, encoding, modalities=["image"]) # type: ignore[arg-type]
143
  return encoding
144
 
145
+ def process_images(
146
  self,
147
  images: list[Image],
148
+ min_tiles: int = 1,
149
+ max_tiles: int = 12,
150
  **kwargs: Unpack[ImagesKwargs],
151
  ) -> BatchFeature:
152
  assert isinstance(min_tiles, int) and isinstance(max_tiles, int)
 
158
  pixel_values = []
159
  for image in images:
160
  image_tiles = _dynamic_preprocess(
161
+ image, min_num=min_tiles, max_num=max_tiles, image_size=crop_size["height"]
162
  )
163
+ pixel_values.append(self.image_processor(image_tiles, **kwargs, return_tensors="pt")["pixel_values"])
164
 
165
+ return BatchFeature({"pixel_values": torch.stack(pixel_values)}, tensor_type=return_tensors)
166
 
167
 
168
+ # Adapted from https://github.com/NVlabs/VILA/blob/36f6adcd11a10be1580caeb7e647e1b6f8517f89/llava/mm_utils.py#L296
169
  def _dynamic_preprocess(
170
  image: Image, min_num: int, max_num: int, image_size: int, use_thumbnail: bool = True
171
  ) -> list[Image]:
172
+ if image.mode != "RGB":
173
+ image = image.convert("RGB")
174
+
175
  orig_width, orig_height = image.size
176
+ (target_width, target_height), crop_boxes = _calculate_crops(orig_width, orig_height, image_size, min_num, max_num)
177
+
178
+ resized_img = image.resize((target_width, target_height))
179
+ processed_images = [resized_img.crop(box) for box in crop_boxes]
180
+
181
+ if use_thumbnail and len(processed_images) != 1:
182
+ thumbnail_img = image.resize((image_size, image_size))
183
+ processed_images.append(thumbnail_img)
184
+
185
+ return processed_images
186
+
187
+
188
+ @lru_cache(maxsize=32)
189
+ def _calculate_crops(
190
+ width: int, height: int, crop_size: int, min_num: int, max_num: int
191
+ ) -> tuple[tuple[int, int], list[tuple[int, int, int, int]]]:
192
+ aspect_ratio = width / height
193
 
194
  # calculate the existing image aspect ratio
195
  target_ratio_set = {
 
197
  for n in range(min_num, max_num + 1)
198
  for i in range(1, n + 1)
199
  for j in range(1, n + 1)
200
+ if min_num <= i * j <= max_num
201
  }
202
  target_ratios = sorted(target_ratio_set, key=lambda x: x[0] * x[1])
203
 
 
205
  target_aspect_ratio = _find_closest_aspect_ratio(
206
  aspect_ratio,
207
  target_ratios,
208
+ width,
209
+ height,
210
+ crop_size,
211
  )
212
 
213
  # calculate the target width and height
214
+ target_width = crop_size * target_aspect_ratio[0]
215
+ target_height = crop_size * target_aspect_ratio[1]
216
  blocks = target_aspect_ratio[0] * target_aspect_ratio[1]
217
 
218
+ crop_boxes = []
 
 
219
  for i in range(blocks):
220
  box = (
221
+ (i % (target_width // crop_size)) * crop_size,
222
+ (i // (target_width // crop_size)) * crop_size,
223
+ ((i % (target_width // crop_size)) + 1) * crop_size,
224
+ ((i // (target_width // crop_size)) + 1) * crop_size,
225
  )
226
+ crop_boxes.append(box)
227
+
228
+ return (target_width, target_height), crop_boxes
 
 
 
 
 
229
 
230
 
231
  # Copied from https://github.com/NVlabs/VILA/blob/36f6adcd11a10be1580caeb7e647e1b6f8517f89/llava/mm_utils.py#L280