$ teds read --post detect-what-you-can-name-paligemma-2-object-detection-transformers
Detect What You Can Name: PaliGemma 2 Object Detection with Transformers
A practical tutorial for using current PaliGemma 2 mix checkpoints with Transformers 5 to run prompt-driven object detection and draw bounding boxes from generated location tokens.
Detect What You Can Name: PaliGemma 2 Object Detection with Transformers
TL;DR
- PaliGemma 2 can do prompt-driven object detection by generating structured
<loc0000>tokens instead of detector head tensors. - The largest current PaliGemma 2 mix checkpoint for this workflow is
google/paligemma2-28b-mix-448; use10bor3bvariants when hardware is tighter. - With current
transformers, the core loop is compact: load a processor and model, prompt withdetect {label} ; {label}, parse the generated location tokens, and draw boxes.
Abstract
Classic object detectors return boxes and class scores from a dedicated detection head. PaliGemma 2 takes a different route: it treats detection as conditional text generation, so the model emits bounding boxes as location tokens followed by labels. This post shows how to use that behavior with the current Transformers API, turn generated text into pixel boxes, and wrap the result in a reusable function. By the end, you will have a clean baseline for prompt-driven detection that you can extend into a notebook, script, or Gradio demo.
Requirements
- Python 3.10+
- A Hugging Face account with access accepted for the PaliGemma/Gemma license
- A CUDA GPU for the larger checkpoints, or a smaller PaliGemma 2 variant for constrained hardware
transformers>=5.12.0torchacceleratepillowgradioif you want the demo UItorchaoif you want int4 weight-only loading for the 28B checkpoint
pip install -U "transformers>=5.12.0" torch accelerate pillow gradio torchao
hf auth login
Why PaliGemma 2 Mix 448?
The original notebook used google/paligemma2-3b-mix-224. That is still useful, but the 448 checkpoints give the vision encoder more input resolution, which matters for localization. The current PaliGemma 2 mix family on Hugging Face includes:
| Model | Parameters | Why use it |
|---|---|---|
google/paligemma2-28b-mix-448 |
27.7B | Strongest option when you can use quantization or large GPU memory. |
google/paligemma2-10b-mix-448 |
9.7B | Good balance when 28B is too large but you still want the higher-resolution 448 variant. |
google/paligemma2-3b-mix-448 |
3.0B | The most practical local/dev option. |
This tutorial uses google/paligemma2-28b-mix-448 as the main model because it is the largest current PaliGemma 2 mix checkpoint with detection prompt support. If you are experimenting on a smaller GPU, change only MODEL_ID; the rest of the code stays the same.
There is not a single official Hugging Face object-detection leaderboard that directly ranks these PaliGemma 2 mix checkpoints against one another. The choice here is based on current checkpoint availability, input resolution, model size, and explicit detection prompt support in the model cards.
How PaliGemma 2 Represents Detections
PaliGemma detection prompts look like this:
detect person ; bicycle ; dog
The model responds with text shaped like this:
<loc0128><loc0184><loc0660><loc0732> dog ; <loc0201><loc0042><loc0912><loc0408> person
Each detection contains four location tokens in this order:
<loc y_min><loc x_min><loc y_max><loc x_max> label
The location values live on a 0-1024 grid. To draw them on the original image, divide by 1024 and scale y by image height and x by image width.
Load the Model and Processor
For the 28B checkpoint, int4 weight-only loading is the most practical starting point. If you use the 10b or 3b checkpoint and have enough memory, set USE_INT4 = False.
import torch
from transformers import AutoProcessor, PaliGemmaForConditionalGeneration, TorchAoConfig
MODEL_ID = "google/paligemma2-28b-mix-448"
USE_INT4 = True
processor = AutoProcessor.from_pretrained(MODEL_ID)
model_kwargs = {"device_map": "auto"}
if USE_INT4:
model_kwargs["quantization_config"] = TorchAoConfig("int4_weight_only", group_size=128)
else:
model_kwargs["torch_dtype"] = torch.bfloat16
model = PaliGemmaForConditionalGeneration.from_pretrained(
MODEL_ID,
**model_kwargs,
).eval()
If you want the smaller development model:
MODEL_ID = "google/paligemma2-3b-mix-448"
USE_INT4 = False
Parse Generated Location Tokens
Keep the parser separate from model inference. That makes it easy to unit test and easy to replace later if you add segmentation tokens.
from dataclasses import dataclass
import re
@dataclass(frozen=True)
class Detection:
label: str
box_xyxy: tuple[int, int, int, int]
LOC_PATTERN = re.compile(
r"<loc(?P<y0>\d{4})><loc(?P<x0>\d{4})>"
r"<loc(?P<y1>\d{4})><loc(?P<x1>\d{4})>"
r"\s*(?P<label>[^;<>]+?)\s*(?:;|$)"
)
def parse_detections(text: str, image_size: tuple[int, int]) -> list[Detection]:
"""Convert PaliGemma location-token output into pixel-space boxes."""
width, height = image_size
detections: list[Detection] = []
for match in LOC_PATTERN.finditer(text):
y0 = int(match.group("y0")) / 1024 * height
x0 = int(match.group("x0")) / 1024 * width
y1 = int(match.group("y1")) / 1024 * height
x1 = int(match.group("x1")) / 1024 * width
detections.append(
Detection(
label=match.group("label").strip(),
box_xyxy=(round(x0), round(y0), round(x1), round(y1)),
)
)
return detections
Run Prompt-Driven Detection
PaliGemma is not a closed-set detector like YOLO or DETR. You give it the object names you care about, and it tries to localize those objects.
from PIL import Image
from transformers.image_utils import load_image
def make_detection_prompt(labels: list[str]) -> str:
cleaned = [label.strip().lower() for label in labels if label.strip()]
if not cleaned:
raise ValueError("Pass at least one label, for example: ['person', 'dog']")
return "detect " + " ; ".join(cleaned)
def detect_objects(image: Image.Image, labels: list[str], max_new_tokens: int = 256) -> tuple[str, list[Detection]]:
prompt = make_detection_prompt(labels)
rgb_image = image.convert("RGB")
inputs = processor(text=prompt, images=rgb_image, return_tensors="pt").to(model.device)
with torch.inference_mode():
generated = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
cache_implementation="static",
)
input_length = inputs["input_ids"].shape[-1]
generated_tokens = generated[0][input_length:]
decoded = processor.decode(generated_tokens, skip_special_tokens=False).strip()
return decoded, parse_detections(decoded, rgb_image.size)
image = load_image(
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/car.jpg"
)
raw_output, detections = detect_objects(image, ["car", "wheel", "person"])
print(raw_output)
print(detections)
Notice the skip_special_tokens=False setting. For this task, the <loc...> tokens are the result you need, so do not discard them before parsing.
Draw the Boxes
Keep drawing code boring. It should not know anything about Transformers; it only needs an image and parsed detections.
from PIL import ImageDraw, ImageFont
def clamp(value: int, lower: int, upper: int) -> int:
return max(lower, min(value, upper))
def draw_detections(image: Image.Image, detections: list[Detection]) -> Image.Image:
canvas = image.convert("RGB").copy()
draw = ImageDraw.Draw(canvas)
font = ImageFont.load_default()
width, height = canvas.size
for detection in detections:
x0, y0, x1, y1 = detection.box_xyxy
x0 = clamp(x0, 0, width - 1)
x1 = clamp(x1, 0, width - 1)
y0 = clamp(y0, 0, height - 1)
y1 = clamp(y1, 0, height - 1)
if x1 <= x0 or y1 <= y0:
continue
draw.rectangle((x0, y0, x1, y1), outline="red", width=3)
draw.text((x0 + 4, y0 + 4), detection.label, fill="red", font=font)
return canvas
annotated = draw_detections(image, detections)
annotated.save("paligemma2-detections.jpg")
Optional Gradio Demo
Once the core functions work, the UI is just a thin wrapper. Let the user pass a comma-separated list of candidate labels.
import gradio as gr
def gradio_detect(image: Image.Image, labels: str):
label_list = [label.strip() for label in labels.split(",") if label.strip()]
raw_output, detections = detect_objects(image, label_list)
annotated = draw_detections(image, detections)
rows = [
{"label": detection.label, "box_xyxy": detection.box_xyxy}
for detection in detections
]
return annotated, raw_output, rows
demo = gr.Interface(
fn=gradio_detect,
inputs=[
gr.Image(type="pil", label="Image"),
gr.Textbox(value="person, car, dog", label="Objects to detect"),
],
outputs=[
gr.Image(type="pil", label="Detections"),
gr.Textbox(label="Raw model output"),
gr.JSON(label="Parsed boxes"),
],
title="PaliGemma 2 Prompt-Driven Object Detection",
description="Enter object names, then PaliGemma 2 returns generated location tokens that are parsed into boxes.",
)
demo.launch()
Failure Modes & Caveats
- PaliGemma 2 detection is prompt-driven. If you do not name an object, the model is not guaranteed to return it.
- The output is generated text, so malformed or partial
<loc...>sequences can happen. Keep the raw output visible while debugging. - The Gemma/PaliGemma license applies. Accept the model terms before running the checkpoints.
- The
28bmodel is large. Use int4 loading, a large GPU, or swap to10b/3b. - Detection quality depends heavily on the label wording. Try singular nouns first, then add more specific phrases if needed.
Practical Guidance
- Start with
google/paligemma2-3b-mix-448while building your app, then move up to10bor28b. - Use short prompts such as
detect car ; person, not long natural-language questions. - Keep
do_sample=Falsefor repeatable detections. - Store both the raw generated text and parsed boxes during evaluation. Parser bugs are easier to spot when you can inspect the text.
- Treat this as open-vocabulary localization, not a drop-in replacement for a production detector with calibrated confidence scores.
Sanity Check
processor(text=..., images=..., return_tensors="pt")returnsinput_ids,attention_mask, andpixel_values.- The decoded model output contains
<loc0000>-style tokens. parse_detections(...)returns pixel-space(x0, y0, x1, y1)boxes.- The annotated image draws boxes in the right region of the source image.
Summary
PaliGemma 2 makes object detection feel closer to prompting than configuring a detector head. You name the objects, the model generates location-token text, and a small parser turns that text into boxes. Using the current Transformers API keeps the code compact: AutoProcessor prepares the image and prompt, PaliGemmaForConditionalGeneration generates the detection string, and plain Python handles the rest.
Next Steps
- Evaluate prompts across a small labeled image set and track parser failures separately from localization misses.
- Add support for
segment {object}prompts if masks matter more than boxes. - Fine-tune a
ptcheckpoint when you need a domain-specific detector instead of a general prompt-driven baseline.
Related reading:
- For zero-shot phrase grounding with Qwen3-VL, read Find What You Mean.
- For fine-tuning PaliGemma on a detection dataset, read Teaching PaliGemma 2 to Spot Construction-Site Hazards.
If you need detection that survives your own images and labels, talk to us. We can help choose the model path, build the eval set, and turn prompt-driven boxes into something your team can trust.