Gemma4-WebGPU / src /components /DetectionOverlay.tsx
BryanBradfo's picture
Gemma 4 multimodal WebGPU detection Space
576d07a
Raw
History Blame Contribute Delete
3.75 kB
import { useEffect, useRef } from "react";
import type { Detection } from "../hooks/LLMContext";
import { toPixelCoords, DETECTION_COLORS } from "../utils/detection-parser";
interface DetectionOverlayProps {
imageUrl: string;
detections: Detection[];
}
export function DetectionOverlay({ imageUrl, detections }: DetectionOverlayProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const img = imgRef.current;
if (!canvas || !img || !img.complete) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Match canvas size to displayed image size
const rect = img.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Build unique label map for consistent colors
const labelSet = [...new Set(detections.map((d) => d.label))];
for (const det of detections) {
const [x1, y1, x2, y2] = toPixelCoords(
det.box_2d,
canvas.width,
canvas.height,
);
const colorIdx = labelSet.indexOf(det.label);
const color = DETECTION_COLORS[colorIdx % DETECTION_COLORS.length];
// Draw box
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
// Draw label background
ctx.font = "bold 13px 'Google Sans', sans-serif";
const textWidth = ctx.measureText(det.label).width;
const labelHeight = 20;
const labelY = Math.max(0, y1 - labelHeight);
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(x1, labelY, textWidth + 10, labelHeight, 4);
ctx.fill();
// Draw label text
ctx.fillStyle = "#ffffff";
ctx.fillText(det.label, x1 + 5, labelY + 14);
}
}, [detections]);
const handleImageLoad = () => {
// Re-trigger detection rendering once image loads
if (detections.length > 0) {
const canvas = canvasRef.current;
const img = imgRef.current;
if (!canvas || !img) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const rect = img.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const labelSet = [...new Set(detections.map((d) => d.label))];
for (const det of detections) {
const [x1, y1, x2, y2] = toPixelCoords(
det.box_2d,
canvas.width,
canvas.height,
);
const colorIdx = labelSet.indexOf(det.label);
const color = DETECTION_COLORS[colorIdx % DETECTION_COLORS.length];
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
ctx.font = "bold 13px 'Google Sans', sans-serif";
const textWidth = ctx.measureText(det.label).width;
const labelHeight = 20;
const labelY = Math.max(0, y1 - labelHeight);
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(x1, labelY, textWidth + 10, labelHeight, 4);
ctx.fill();
ctx.fillStyle = "#ffffff";
ctx.fillText(det.label, x1 + 5, labelY + 14);
}
}
};
return (
<div className="relative inline-block w-full">
<img
ref={imgRef}
src={imageUrl}
alt="Detection result"
className="w-full max-h-[500px] object-contain rounded-xl"
onLoad={handleImageLoad}
/>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 w-full h-full pointer-events-none"
style={{ objectFit: "contain" }}
/>
</div>
);
}