import { useRef, useEffect, memo } from 'react'; import type { OilDetectionResult } from '../utils/oilDetection'; import { OIL_CLASSES, OIL_CLASS_NAMES } from '../utils/oilDetection'; export interface OilDetectionOverlayProps { result: OilDetectionResult | null; isAnalyzing?: boolean; error?: string | null; } /** 클래스 ID → RGBA 색상 (오버레이용) */ const CLASS_COLORS: Record = { 1: [0, 0, 204, 90], // black oil → 파란색 2: [180, 180, 180, 90], // brown oil → 회색 3: [255, 255, 0, 90], // rainbow oil → 노란색 4: [178, 102, 255, 90], // silver oil → 보라색 }; const OilDetectionOverlay = memo(({ result, isAnalyzing = false, error = null }: OilDetectionOverlayProps) => { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const dpr = window.devicePixelRatio || 1; const displayW = canvas.clientWidth; const displayH = canvas.clientHeight; canvas.width = displayW * dpr; canvas.height = displayH * dpr; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, displayW, displayH); if (!result || result.regions.length === 0) return; const { mask, maskWidth, maskHeight } = result; // 클래스별 색상으로 마스크 렌더링 const offscreen = new OffscreenCanvas(maskWidth, maskHeight); const offCtx = offscreen.getContext('2d'); if (offCtx) { const imageData = new ImageData(maskWidth, maskHeight); for (let i = 0; i < mask.length; i++) { const classId = mask[i]; if (classId === 0) continue; // background skip const color = CLASS_COLORS[classId]; if (!color) continue; const pixelIdx = i * 4; imageData.data[pixelIdx] = color[0]; imageData.data[pixelIdx + 1] = color[1]; imageData.data[pixelIdx + 2] = color[2]; imageData.data[pixelIdx + 3] = color[3]; } offCtx.putImageData(imageData, 0, 0); ctx.drawImage(offscreen, 0, 0, displayW, displayH); } }, [result]); const formatArea = (m2: number): string => { if (m2 >= 1000) { return `${(m2 / 1_000_000).toFixed(1)} km²`; } return `~${Math.round(m2)} m²`; }; const hasRegions = result !== null && result.regions.length > 0; return ( <> {/* OSD — bottom-8로 좌표 OSD(bottom-2)와 겹침 방지 */}
{/* 에러 표시 */} {error && (
추론 서버 연결 불가
)} {/* 클래스별 감지 결과 */} {hasRegions && result !== null && ( <> {result.regions.map((region) => { const oilClass = OIL_CLASSES.find((c) => c.classId === region.classId); const color = oilClass ? `rgb(${oilClass.color.join(',')})` : '#f87171'; const label = OIL_CLASS_NAMES[region.classId] || region.className; return (
{label}: {formatArea(region.areaM2)} ({region.percentage.toFixed(1)}%)
); })} {/* 합계 */}
합계: {formatArea(result.totalAreaM2)} ({result.totalPercentage.toFixed(1)}%)
)} {/* 감지 없음 */} {!hasRegions && !isAnalyzing && !error && (
감지 없음
)} {/* 분석 중 */} {isAnalyzing && ( 분석중... )}
); }); OilDetectionOverlay.displayName = 'OilDetectionOverlay'; export default OilDetectionOverlay;