detection 畫框、classification 改在影像右上角疊標籤(不畫框), 並在推論面板提供即時切換解析方式與上傳 label 檔。 - 新增 InferenceOverlay 依 taskType 分派;ClassificationOverlay 用 DOM 而非 canvas,讓 CJK 排版與 aria-live 交給瀏覽器處理 - 標籤防閃爍:挑戰者需連續 3 幀居冠才切換,或信心度領先 15% 直接切; 低於門檻立即清空(顯示過期標籤比空白更糟) - 身分比對優先用 classIndex,避免換 label 檔時被誤判為換類別 - 推論設定卡片:即時切 detection/classification、上傳 .txt label、 清除 label,皆不需重燒 - classification-result 原本無條件渲染,導致 detection 模式下側欄 永遠顯示「分類結果」與空圖表,改為依 taskType 切換 - 清掉 camera-overlay 每幀執行的 debug console.log - 補 ResizeObserver stub,原本任何渲染 InferencePanel 的測試都會 在 jsdom 直接拋錯 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
1.8 KiB
TypeScript
65 lines
1.8 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
import type { DetectionResult } from '@/types/inference';
|
|
|
|
interface CameraOverlayProps {
|
|
detections: DetectionResult[];
|
|
width: number;
|
|
height: number;
|
|
confidenceThreshold: number;
|
|
}
|
|
|
|
const COLORS = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'];
|
|
|
|
export function CameraOverlay({ detections, width, height, confidenceThreshold }: CameraOverlayProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
const filtered = detections.filter((d) => d.confidence >= confidenceThreshold);
|
|
|
|
filtered.forEach((det, i) => {
|
|
const color = COLORS[i % COLORS.length];
|
|
// Convert normalized coordinates (0-1) to pixel values
|
|
const px = det.bbox.x * width;
|
|
const py = det.bbox.y * height;
|
|
const pw = det.bbox.width * width;
|
|
const ph = det.bbox.height * height;
|
|
|
|
// Draw bounding box
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(px, py, pw, ph);
|
|
|
|
// Draw label background
|
|
const label = `${det.label} ${(det.confidence * 100).toFixed(0)}%`;
|
|
ctx.font = '14px sans-serif';
|
|
const textWidth = ctx.measureText(label).width;
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(px, py - 20, textWidth + 8, 20);
|
|
|
|
// Draw label text
|
|
ctx.fillStyle = '#fff';
|
|
ctx.fillText(label, px + 4, py - 5);
|
|
});
|
|
}, [detections, width, height, confidenceThreshold]);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
width={width}
|
|
height={height}
|
|
style={{ width, height }}
|
|
className="absolute left-0 top-0 pointer-events-none"
|
|
data-testid="camera-overlay"
|
|
/>
|
|
);
|
|
}
|