From ce3142a98bcc21ae9877f9fe29b50711e31be9e9 Mon Sep 17 00:00:00 2001 From: dongjin kim Date: Fri, 21 Nov 2025 16:39:17 +0900 Subject: [PATCH] Fix draw detection box. --- public/app.js | 114 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 87 insertions(+), 27 deletions(-) diff --git a/public/app.js b/public/app.js index 4bef9c4..ac54112 100644 --- a/public/app.js +++ b/public/app.js @@ -234,14 +234,13 @@ document.addEventListener('DOMContentLoaded', () => { }); }; - // [수정됨] 다중 파일 업로드 처리 + // 다중 파일 업로드 처리 window.uploadPoiImage = function(name, input) { if (!input.files || input.files.length === 0) return; const formData = new FormData(); formData.append('poiName', name); - // 선택된 모든 파일을 formData에 추가 for (let i = 0; i < input.files.length; i++) { const file = input.files[i]; const ext = file.name.split('.').pop().toLowerCase(); @@ -251,7 +250,6 @@ document.addEventListener('DOMContentLoaded', () => { input.value = ''; return; } - formData.append('poiFile', file); } @@ -312,7 +310,7 @@ document.addEventListener('DOMContentLoaded', () => { } // ================================================= - // 4. 비디오 & 웹소켓 (기존 기능) + // 4. 비디오 & 웹소켓 (좌표 그리기 로직 추가됨) // ================================================= const uri = "ws://10.10.11.246:8765"; const canvasEl = document.getElementById("frame"); @@ -329,24 +327,42 @@ document.addEventListener('DOMContentLoaded', () => { let lastFpsCheckTime = performance.now(); let lastFrameMeta = null; + // [신규] 캔버스 영상의 배율(scale)과 여백(offset)을 저장하여 좌표 변환에 사용 + let viewConfig = { r: 1, dx: 0, dy: 0 }; + function connect() { const ws = new WebSocket(uri); ws.binaryType = "arraybuffer"; - ws.onopen = () => { statusEl.textContent = "연결됨"; statusEl.className = "status"; }; - ws.onclose = () => { statusEl.textContent = "연결 종료. 재접속..."; statusEl.className = "status err"; setTimeout(connect, 2000); }; + + ws.onopen = () => { + statusEl.textContent = "연결됨"; + statusEl.className = "status"; + }; + + ws.onclose = () => { + statusEl.textContent = "연결 종료. 재접속..."; + statusEl.className = "status err"; + setTimeout(connect, 2000); + }; + ws.onmessage = (event) => { const data = event.data; + + // 1. 텍스트 데이터(JSON) 처리 if (typeof data === "string") { + // console.log("-----" + data); try { - console.log("-----" + data); const meta = JSON.parse(data); if (meta.type === "frame") { lastFrameMeta = meta; document.getElementById("frame-info").textContent = ` | FRAME ${meta.w}x${meta.h}`; } else if (meta.type === "det") { + // 탐지 정보가 오면 박스를 그림 showDetections(meta); } - } catch(e){} + } catch(e){ console.error(e); } + + // 2. 바이너리 데이터(영상 프레임) 처리 } else if (data instanceof ArrayBuffer && lastFrameMeta) { frameCount++; const now = performance.now(); @@ -355,18 +371,28 @@ document.addEventListener('DOMContentLoaded', () => { frameCount = 0; lastFpsCheckTime = now; } + const blob = new Blob([data], {type: "image/jpeg"}); createImageBitmap(blob).then(bmp => { + // 캔버스 크기에 맞춰 영상 비율(레터박스) 계산 canvasEl.width = canvasEl.clientWidth; canvasEl.height = canvasEl.clientHeight; + + // r: 축소/확대 비율, dx/dy: 중앙 정렬을 위한 여백 const r = Math.min(canvasEl.width / bmp.width, canvasEl.height / bmp.height); const dw = bmp.width * r; const dh = bmp.height * r; const dx = (canvasEl.width - dw) / 2; const dy = (canvasEl.height - dh) / 2; - ctx.clearRect(0,0,canvasEl.width, canvasEl.height); + + // 좌표 변환을 위해 현재 뷰 설정 전역 변수에 저장 + viewConfig = { r, dx, dy }; + + ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); ctx.drawImage(bmp, dx, dy, dw, dh); bmp.close(); + + // 창 크기 변화 시 박스 컨테이너 위치도 동기화 updateBboxContainerPosition(); }); } @@ -374,50 +400,83 @@ document.addEventListener('DOMContentLoaded', () => { } /** - * [수정됨] 상세 탐지 정보 출력 함수 - * 입력 예: {"type": "det", "ch": 0, "seq": 16939, "ts_us": 833298660, "items": [{"x1": ..., "tag": 1, ...}]} + * [수정됨] 탐지 정보를 받아 화면에 Bounding Box를 그리는 함수 */ function showDetections(meta) { - bboxContainerEl.innerHTML = ""; // 기존 bbox 초기화 (필요시 여기에 박스 그리기 로직 추가 가능) + // 기존 박스들 초기화 + bboxContainerEl.innerHTML = ""; - // 1. 메타 정보 라인 구성 (ch, seq, ts_us) + // 메타 정보 표시 준비 const ch = meta.ch !== undefined ? meta.ch : '-'; const seq = meta.seq !== undefined ? meta.seq : '-'; const ts = meta.ts_us !== undefined ? meta.ts_us : '-'; + let outputLines = [`[META] CH:${ch} | SEQ:${seq} | TS:${ts}`]; - let outputLines = []; - outputLines.push(`[META] CH:${ch} | SEQ:${seq} | TS:${ts}`); - - // 2. 탐지 객체 리스트 구성 const items = meta.items || []; + if (items.length === 0) { outputLines.push("No detections"); } else { + // 각 탐지 객체에 대해 처리 items.forEach((it, i) => { - // 좌표값 소수점 2자리로 반올림 - const x1 = it.x1 ? it.x1.toFixed(2) : 0; - const y1 = it.y1 ? it.y1.toFixed(2) : 0; - const x2 = it.x2 ? it.x2.toFixed(2) : 0; - const y2 = it.y2 ? it.y2.toFixed(2) : 0; - const tag = it.tag !== undefined ? it.tag : '-'; - const cls = it.cls !== undefined ? it.cls : '-'; - - outputLines.push(`#${i} | Class:${cls} | Tag:${tag} | Box:[${x1}, ${y1}, ${x2}, ${y2}]`); + // 원본 영상 기준 좌표 + const x1 = it.x1 || 0; + const y1 = it.y1 || 0; + const x2 = it.x2 || 0; + const y2 = it.y2 || 0; + const cls = it.cls !== undefined ? it.cls : '?'; + const tag = it.tag !== undefined ? it.tag : ''; + + // 우측 정보창에 텍스트 로그 추가 + outputLines.push(`#${i} | Class:${cls} | Tag:${tag} | Box:[${x1.toFixed(1)}, ${y1.toFixed(1)}, ${x2.toFixed(1)}, ${y2.toFixed(1)}]`); + + // 좌표 변환: 원본 좌표 -> 캔버스 화면 좌표 + // 공식: 화면좌표 = 여백 + (원본좌표 * 배율) + const { r, dx, dy } = viewConfig; + const screenX = dx + (x1 * r); + const screenY = dy + (y1 * r); + const screenW = (x2 - x1) * r; + const screenH = (y2 - y1) * r; + + // 박스 DOM 요소 생성 + const boxDiv = document.createElement('div'); + boxDiv.className = 'bbox'; // style.css에 정의된 붉은 박스 스타일 사용 + boxDiv.style.left = `${screenX}px`; + boxDiv.style.top = `${screenY}px`; + boxDiv.style.width = `${screenW}px`; + boxDiv.style.height = `${screenH}px`; + + // 박스 위 라벨 (선택 사항) + const label = document.createElement('div'); + label.style.position = 'absolute'; + label.style.top = '-16px'; + label.style.left = '0'; + label.style.backgroundColor = 'red'; + label.style.color = 'white'; + label.style.fontSize = '10px'; + label.style.padding = '1px 3px'; + label.style.whiteSpace = 'nowrap'; + label.textContent = `C:${cls} T:${tag}`; + + boxDiv.appendChild(label); + bboxContainerEl.appendChild(boxDiv); }); } - // 3. 화면 출력 + // 우측 정보창 업데이트 detListEl.textContent = outputLines.join("\n"); } function updateBboxContainerPosition() { if (!canvasEl || !bboxContainerEl) return; + // 캔버스의 실제 위치와 크기에 박스 컨테이너를 일치시킴 bboxContainerEl.style.top = canvasEl.offsetTop + 'px'; bboxContainerEl.style.left = canvasEl.offsetLeft + 'px'; bboxContainerEl.style.width = canvasEl.offsetWidth + 'px'; bboxContainerEl.style.height = canvasEl.offsetHeight + 'px'; } + // 모델 변경 이벤트 if (modelContainerEl) { modelContainerEl.addEventListener('change', (e) => { if(e.target.name === 'current-model') { @@ -440,6 +499,7 @@ document.addEventListener('DOMContentLoaded', () => { } } + // 초기 실행 updateModelDisplay(); if (canvasEl) connect(); window.addEventListener('resize', updateBboxContainerPosition);