bpHigh's picture
Upload via Autoresearch export_data
287ca83 verified
Raw History Blame Contribute Delete
3.39 kB
"""Image inputs.
Qwen3.5 reads images natively: its processor cuts each image into 16-px patches, the vision
encoder merges 2×2 patches into one token, and the language model sees those tokens between
``<|vision_start|>`` and ``<|vision_end|>`` at ``<|image_pad|>`` placeholders, with 3-D (M-RoPE)
positions. ``ImageEncoder`` turns one decision image into the placeholder ids for the layout and
the pixel tensors for ``collate``; ``DecisionModel.hidden`` scatters the encoder's features into
the placeholders.
Glance 0.1 was trained and evaluated on images at most 640 px on the long side;
``encode_image`` prepares an image the same way (downscaled, JPEG at quality 88, as a ``data:`` URL).
"""
from __future__ import annotations
import base64
import io
from pathlib import Path
from typing import Any
from PIL import Image
MAX_SIDE = 640
JPEG_QUALITY = 88
def load_image(entry: dict[str, Any]) -> Image.Image:
"""The image an entry refers to (``image``, ``path`` or ``data_url``), as RGB."""
if "image" in entry:
return entry["image"].convert("RGB")
if "path" in entry:
with Image.open(Path(entry["path"])) as im:
return im.convert("RGB")
if "data_url" in entry:
data = base64.b64decode(entry["data_url"].split(",", 1)[1])
with Image.open(io.BytesIO(data)) as im:
return im.convert("RGB")
raise ValueError("image entry needs image, path or data_url")
def to_data_url(img: Image.Image, max_side: int | None = MAX_SIDE, quality: int = JPEG_QUALITY) -> str:
img = img.convert("RGB")
if max_side and max(img.size) > max_side:
scale = max_side / max(img.size)
img = img.resize((max(1, round(img.width * scale)), max(1, round(img.height * scale))), Image.BILINEAR)
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality)
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
def encode_image(image: "str | Path | Image.Image", max_side: int | None = MAX_SIDE, quality: int = JPEG_QUALITY) -> str:
"""A file path, PIL image or ``data:`` URL as the ``data:`` URL a request carries."""
if isinstance(image, Image.Image):
return to_data_url(image, max_side, quality)
if isinstance(image, str) and image.startswith("data:"):
return image
with Image.open(Path(image)) as im:
return to_data_url(im, max_side, quality)
class ImageEncoder:
def __init__(self, image_processor: Any, *, image_token_id: int, start_id: int | None = None,
end_id: int | None = None, merge_size: int = 2):
self.image_processor = image_processor
self.image_token_id = image_token_id
self.start_id, self.end_id = start_id, end_id
self.merge = merge_size
def __call__(self, entry: dict[str, Any]) -> tuple[list[int], dict[str, Any]]:
"""Placeholder ids for one image, and its ``pixel_values`` / ``image_grid_thw``."""
out = self.image_processor(images=[load_image(entry)], return_tensors="pt")
grid = out["image_grid_thw"]
n = int(grid.prod()) // self.merge ** 2
ids = [self.image_token_id] * n
if self.start_id is not None:
ids = [self.start_id, *ids]
if self.end_id is not None:
ids = [*ids, self.end_id]
return ids, {"pixel_values": out["pixel_values"], "image_grid_thw": grid}