import json import re import subprocess import sys from pathlib import Path import gradio as gr import spaces import torch from PIL import Image, ImageDraw, ImageFont from transformers import AutoModelForCausalLM, AutoProcessor MODEL_ID = "google/gemma-4-26B-A4B-it" processor = AutoProcessor.from_pretrained(MODEL_ID) model = AutoModelForCausalLM.from_pretrained(MODEL_ID, device_map="cuda:0", dtype=torch.bfloat16) # Gemma 4 emits bounding boxes in a normalized 1000x1000 coordinate space. COORD_SPACE = 1000 EXAMPLES_DIR = Path(__file__).parent / "examples" def extract_json(text: str): """Pull the first JSON object/array out of a model response.""" text = text.strip() text = re.sub(r"^```(?:json)?\s*", "", text) text = re.sub(r"\s*```$", "", text) try: return json.loads(text) except json.JSONDecodeError: pass match = re.search(r"(\{.*\}|\[.*\])", text, re.DOTALL) if match: return json.loads(match.group(1)) raise ValueError("No valid JSON found in model output") def draw_box(image: Image.Image, box, label: str) -> Image.Image: """Draw a Pascal-VOC style bounding box on a copy of the image. Gemma returns ``[ymin, xmin, ymax, xmax]`` in a 1000x1000 normalized space; we rescale to the image's pixel dimensions. """ out = image.convert("RGB").copy() width, height = out.size ymin, xmin, ymax, xmax = box xmin = xmin / COORD_SPACE * width xmax = xmax / COORD_SPACE * width ymin = ymin / COORD_SPACE * height ymax = ymax / COORD_SPACE * height draw = ImageDraw.Draw(out) line_width = max(3, min(width, height) // 200) draw.rectangle([(xmin, ymin), (xmax, ymax)], outline="red", width=line_width) if label: font_size = max(14, min(width, height) // 40) try: font = ImageFont.truetype("DejaVuSans-Bold.ttf", font_size) except OSError: font = ImageFont.load_default() text_bbox = draw.textbbox((xmin, ymin), label, font=font) text_h = text_bbox[3] - text_bbox[1] text_w = text_bbox[2] - text_bbox[0] pad = 4 text_y = max(0, ymin - text_h - 2 * pad) draw.rectangle( [(xmin, text_y), (xmin + text_w + 2 * pad, text_y + text_h + 2 * pad)], fill="yellow", ) draw.text((xmin + pad, text_y + pad), label, fill="black", font=font) return out @spaces.GPU(duration=60) @torch.inference_mode() def _detect_on_gpu(image: Image.Image, what_object: str) -> str: messages = [ { "role": "user", "content": [ {"type": "image", "image": image}, {"type": "text", "text": f"What's the bounding box for the {what_object} in the image, in JSON format?"}, ], } ] inputs = processor.apply_chat_template( messages, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt", enable_thinking=False, ).to(device=model.device) input_len = inputs["input_ids"].shape[-1] generated = model.generate(**inputs, max_new_tokens=60, do_sample=False) return processor.decode(generated[0, input_len:], skip_special_tokens=True) def detect(image: Image.Image, what_object: str): if image is None: raise gr.Error("Please upload an image.") what_object = (what_object or "").strip() if not what_object: raise gr.Error("Please enter what to detect.") raw = _detect_on_gpu(image, what_object) if re.search(r"did not find", raw, re.IGNORECASE): gr.Info(f"No detections: the model could not find '{what_object}' in this image.") return image, raw try: parsed = extract_json(raw) except (ValueError, json.JSONDecodeError): gr.Warning(f"Could not parse model output as JSON. Raw response shown on the right.") return image, raw # Model usually returns a dict, but Claude insisted on testing for list detection = parsed[0] if isinstance(parsed, list) else parsed if "box_2d" not in detection: gr.Warning("Model output is missing 'box_2d'. Raw response shown on the right.") return image, json.dumps(detection, indent=2) box = detection["box_2d"] label = detection.get("label", what_object) annotated = draw_box(image, box, label) return annotated, json.dumps(detection, indent=2) examples = [ [str(EXAMPLES_DIR / "bike-48x48.jpg"), "bike"], [str(EXAMPLES_DIR / "boat-48x48.jpg"), "hat"], [str(EXAMPLES_DIR / "forbidden-48x48.jpg"), "person"], [str(EXAMPLES_DIR / "wheel-48x48.jpg"), "turquoise capsule"], [str(EXAMPLES_DIR / "recipe.png"), "view recipe button"], ] with gr.Blocks(title="Gemma 4 Object Detection") as demo: gr.Markdown( """ # Gemma 4 Object Detection This demo showcases the extraordinary out-of-the-box geometry awareness of Gemma 4. Just upload an image and ask the model ([Gemma 4 26B-A4B-it](https://huggingface.co/google/gemma-4-26B-A4B-it)) what to look for. It returns a bounding box for the requested object in JSON format, which we then draw on top of the image. To know more about Gemma 4, please visit [our blog post](https://hf.co/blog/gemma4). """ ) with gr.Row(): with gr.Column(): input_image = gr.Image(label="Input image", type="pil", sources=["upload", "clipboard"]) object_text = gr.Textbox( label="What to detect", placeholder="e.g. 'bike', 'person', 'turquoise capsule'", ) run_btn = gr.Button("Detect", variant="primary") with gr.Column(): output_image = gr.Image(label="Detection", type="pil") raw_json = gr.Code(label="Model output", language="json") gr.Examples( examples=examples, inputs=[input_image, object_text], outputs=[output_image, raw_json], fn=detect, cache_examples=False, ) run_btn.click(fn=detect, inputs=[input_image, object_text], outputs=[output_image, raw_json]) object_text.submit(fn=detect, inputs=[input_image, object_text], outputs=[output_image, raw_json]) if __name__ == "__main__": demo.launch()