image_processor.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. from __future__ import annotations
  2. import base64
  3. import io
  4. import math
  5. from dataclasses import dataclass
  6. from typing import Any
  7. from urllib.request import urlopen
  8. IMAGE_START, IMAGE_PAD, IMAGE, IMAGE_NEW_LINE, IMAGE_END = range(5)
  9. COMPRESS_PAD_TO = 4
  10. @dataclass
  11. class ImageInput:
  12. start: int
  13. patches: Any
  14. n_vit_h: int
  15. n_vit_w: int
  16. types: Any
  17. perm: Any
  18. def grid_tokens(best_height, best_width, patch_size, downsample_ratio):
  19. """Number of LLM tokens the aligner grid occupies (N-layout, incl. row/align padding)."""
  20. n_llm_h = math.ceil((best_height // patch_size) / downsample_ratio)
  21. n_llm_w = math.ceil((best_width // patch_size) / downsample_ratio)
  22. num_tokens = n_llm_h * (n_llm_w + 1) + 2
  23. if n_llm_h % 2 == 1:
  24. num_tokens += n_llm_w + 1
  25. num_tokens += (n_llm_h + 1) // 2 * (n_llm_w + 1) % 2 * 2
  26. return n_llm_h, n_llm_w, num_tokens
  27. def solve_resize_ratio(height, width, patch_size, downsample_ratio, max_n_token):
  28. r = height / width
  29. max_w_float = math.sqrt((max_n_token - 2) / r + 0.25) - 0.5
  30. max_h_float = max_w_float * r
  31. if max_w_float < 1.0:
  32. max_w = 1
  33. max_h = (max_n_token - 2) // (max_w + 1)
  34. if max_h % 2 == 1:
  35. max_h -= 1
  36. best_width = max_w * patch_size * downsample_ratio
  37. best_height = max_h * patch_size * downsample_ratio
  38. elif max_h_float < 2.0:
  39. max_h = 2
  40. max_w = ((max_n_token - 2) // max_h) - 1
  41. assert max_w > 1
  42. best_width = max_w * patch_size * downsample_ratio
  43. best_height = max_h * patch_size * downsample_ratio
  44. else:
  45. max_w = math.floor(max_w_float)
  46. max_h = math.floor(max_h_float)
  47. if max_h % 2 == 1:
  48. max_h -= 1
  49. beta = min(max_w * patch_size * downsample_ratio / width, max_h * patch_size * downsample_ratio / height)
  50. best_width = math.floor(width * beta / patch_size) * patch_size
  51. best_height = math.floor(height * beta / patch_size) * patch_size
  52. n_llm_h, n_llm_w, num_tokens = grid_tokens(best_height, best_width, patch_size, downsample_ratio)
  53. return n_llm_h, n_llm_w, best_height, best_width, num_tokens
  54. def safe_resize(height, width, best_height, best_width, patch_size, downsample_ratio, max_n_token):
  55. max_n_token -= COMPRESS_PAD_TO - 1
  56. n_llm_h, n_llm_w, num_tokens = grid_tokens(best_height, best_width, patch_size, downsample_ratio)
  57. budget = max_n_token
  58. while num_tokens > max_n_token:
  59. n_llm_h, n_llm_w, best_height, best_width, num_tokens = solve_resize_ratio(
  60. height, width, patch_size, downsample_ratio, budget)
  61. budget -= 1
  62. return n_llm_h, n_llm_w, best_height, best_width
  63. def load_image_bytes(record) -> bytes:
  64. """Load image bytes from raw/base64 data, an Anthropic source, URL, or path."""
  65. data = record.get("data")
  66. if isinstance(data, bytes):
  67. return data
  68. if isinstance(data, str):
  69. return base64.b64decode(data)
  70. source = record.get("source")
  71. if isinstance(source, dict):
  72. if source.get("data") is not None:
  73. return base64.b64decode(source["data"])
  74. if source.get("url"):
  75. return load_image_bytes({"url": source["url"]})
  76. url = record.get("url")
  77. if isinstance(url, str) and url:
  78. if url.startswith("data:"):
  79. header, _, payload = url.partition(",")
  80. if ";base64" not in header:
  81. raise ValueError(f"Unsupported data URL encoding: {header}")
  82. return base64.b64decode(payload)
  83. if url.startswith(("http://", "https://")):
  84. with urlopen(url, timeout=30) as response:
  85. return response.read()
  86. with open(url, "rb") as file:
  87. return file.read()
  88. raise ValueError(f"Cannot load image from record: {list(record.keys())}")
  89. def load_image(record, args):
  90. """Load and transform one image record into ViT patches."""
  91. import numpy as np
  92. import torch
  93. from PIL import Image, ImageOps
  94. p = args.vision_patch_size
  95. with Image.open(io.BytesIO(load_image_bytes(record))) as source:
  96. image = source.convert("RGB")
  97. width, height = image.size
  98. if args.vision_max_wh_ratio is not None and width > height * args.vision_max_wh_ratio:
  99. width = height * args.vision_max_wh_ratio
  100. if 0 < width * height < args.vision_min_pixels:
  101. ratio = (args.vision_min_pixels / (width * height)) ** 0.5
  102. width = int(width * ratio)
  103. height = int(height * ratio)
  104. best_width = math.ceil(width / p) * p
  105. best_height = math.ceil(height / p) * p
  106. n_llm_h, n_llm_w, best_height, best_width = safe_resize(
  107. height, width, best_height, best_width, p, args.vision_downsample_ratio, args.vision_max_n_token)
  108. n_vit_h, n_vit_w = best_height // p, best_width // p
  109. if args.vision_max_wh_ratio is not None and image.width >= args.vision_max_wh_ratio * image.height:
  110. image = image.resize((best_width, best_height))
  111. else:
  112. image = ImageOps.pad(image, (best_width, best_height), color=(127, 127, 127))
  113. x = torch.from_numpy(np.asarray(image, dtype=np.float32)).permute(2, 0, 1) / 255
  114. x = ((x - 0.5) / 0.5).to(torch.bfloat16)
  115. patches = x.reshape(3, n_vit_h, p, n_vit_w, p).permute(1, 3, 0, 2, 4).reshape(n_vit_h * n_vit_w, 3, p, p)
  116. return patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w
  117. def build_image_block(n_llm_h: int, n_llm_w: int, start_pos: int):
  118. """Builds the N-layout token types (final order) and the aligner-row order for IMAGE slots."""
  119. import torch
  120. compress_pad = COMPRESS_PAD_TO - 1 - start_pos % COMPRESS_PAD_TO
  121. pad_h = n_llm_h % 2
  122. rows = n_llm_h + pad_h
  123. row_len = n_llm_w + 1
  124. pad_last = rows // 2 * row_len % 2 * 2
  125. types = torch.tensor(([IMAGE] * n_llm_w + [IMAGE_NEW_LINE]) * n_llm_h + [IMAGE_PAD] * (row_len * pad_h), dtype=torch.int64)
  126. order = torch.arange(rows * row_len).view(rows // 2, 2, row_len).transpose(1, 2).reshape(-1)
  127. image_idx = torch.full((rows * row_len,), -1, dtype=torch.int64)
  128. image_idx.view(rows, row_len)[:n_llm_h, :n_llm_w] = torch.arange(n_llm_h * n_llm_w).view(n_llm_h, n_llm_w)
  129. perm = image_idx[order]
  130. perm = perm[perm >= 0]
  131. types = torch.cat([
  132. torch.full((compress_pad,), IMAGE_PAD, dtype=torch.int64),
  133. torch.tensor([IMAGE_START]),
  134. types[order],
  135. torch.full((pad_last,), IMAGE_PAD, dtype=torch.int64),
  136. torch.tensor([IMAGE_END]),
  137. ])
  138. return types, perm
  139. def prepare_vl_inputs(prompt, images, tokenizer, args):
  140. """Expand image placeholder tokens into sentinel blocks and ImageInput values."""
  141. from .encoding_dsv4 import IMAGE_PLACEHOLDER
  142. image_token_id = tokenizer.convert_tokens_to_ids(IMAGE_PLACEHOLDER)
  143. if image_token_id is None or image_token_id == tokenizer.unk_token_id:
  144. raise ValueError(f"Token not found in tokenizer: {IMAGE_PLACEHOLDER}")
  145. prompt_tokens = tokenizer.encode(prompt)
  146. num_placeholders = sum(token == image_token_id for token in prompt_tokens)
  147. if num_placeholders != len(images):
  148. raise ValueError(
  149. f"Found {num_placeholders} image tokens but got {len(images)} images")
  150. tokens, image_inputs = [], []
  151. image_iter = iter(images)
  152. for tok in prompt_tokens:
  153. if tok != image_token_id:
  154. tokens.append(tok)
  155. continue
  156. patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w = load_image(
  157. next(image_iter), args)
  158. types, perm = build_image_block(n_llm_h, n_llm_w, len(tokens))
  159. image_inputs.append(ImageInput(len(tokens), patches, n_vit_h, n_vit_w, types, perm))
  160. tokens += (args.vocab_size + types).tolist()
  161. if not image_inputs:
  162. return tokens, None
  163. return tokens, image_inputs