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(null); const imgRef = useRef(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 (
Detection result
); }