jim800121chen 5c1c37d151 feat(frontend): classification 結果呈現與推論期控制項
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>
2026-07-22 19:27:28 +08:00

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"
/>
);
}