diff --git a/public/app.js b/public/app.js index 76f90cc..1d7dfc4 100644 --- a/public/app.js +++ b/public/app.js @@ -169,7 +169,7 @@ document.addEventListener('DOMContentLoaded', () => { classes: ["plate", "person", "car", "motor", "bus", "truck"], defaults: ["plate", "car", "motor", "bus", "truck"] }, - 'VIPTRACK': {name: "관심 인물", classes: ["person"], defaults: ["person"]} + 'VIPTRACK': {name: "관심 인물", classes: ["person", "face"], defaults: ["person", "face"]} }; let currentModelCode = 'OBJDET'; @@ -177,6 +177,8 @@ document.addEventListener('DOMContentLoaded', () => { const zoomInContainer = document.getElementById('zoom-in-container'); const zoomToggle = document.getElementById('zoom-toggle'); const showLabelToggle = document.getElementById('show-label-toggle'); + const showTidToggle = document.getElementById('show-tid-toggle'); + const riskSummaryContainer = document.getElementById('risk-summary-container'); if (zoomToggle) { zoomToggle.addEventListener('change', (e) => { @@ -200,6 +202,14 @@ document.addEventListener('DOMContentLoaded', () => { }); } + if (showTidToggle) { + showTidToggle.addEventListener('change', () => { + if (lastFrameMeta) { + showDetections(lastFrameMeta); + } + }); + } + const navItems = document.querySelectorAll('.nav-item'); navItems.forEach(item => { @@ -212,13 +222,20 @@ document.addEventListener('DOMContentLoaded', () => { currentModelCode = modelCode; changeModel(modelCode); updateFilterBar(modelCode); - // 모델 변경 시 Summary 패널 초기화 + + if (modelCode === 'CROWD') { + riskSummaryContainer.classList.remove('hidden'); + } else { + riskSummaryContainer.classList.add('hidden'); + } + if (modelCode === 'FACEATTR' || modelCode === 'ABNORM' || modelCode === 'LPR') { summaryListEl.className = 'summary-content card-list'; summaryListEl.innerHTML = '
Waiting for card data...
'; } else { summaryListEl.className = 'summary-content'; - summaryListEl.innerHTML = '
No detection
'; + summaryListEl.innerHTML = ''; + detectedClasses.clear(); } } }); @@ -254,7 +271,6 @@ document.addEventListener('DOMContentLoaded', () => { const defaults = def.defaults || def.classes; - // classes와 defaults가 같은지 확인 (순서 무관하게 내용 비교) const isAllDefault = (def.classes.length === defaults.length) && def.classes.every(c => defaults.includes(c)); @@ -346,6 +362,34 @@ document.addEventListener('DOMContentLoaded', () => { let lastFpsCheckTime = performance.now(); let lastFrameMeta = null; let viewConfig = {r: 1, dx: 0, dy: 0}; + let detectedClasses = new Set(); // 한 번이라도 탐지된 클래스 목록 + let mosaicState = {}; // TID별 모자이크 상태 저장 (true: 모자이크, false: 해제) + + // 모자이크용 오프스크린 캔버스 + const mosaicCanvas = document.createElement('canvas'); + const mosaicCtx = mosaicCanvas.getContext('2d'); + + function applyMosaic(x, y, width, height) { + if (!ctx || width <= 0 || height <= 0) return; + + const blockSize = 20; // 모자이크 블록 크기 + const smallW = Math.floor(width / blockSize); + const smallH = Math.floor(height / blockSize); + + if (smallW <= 0 || smallH <= 0) return; + + mosaicCanvas.width = smallW; + mosaicCanvas.height = smallH; + + // 1. 원본 캔버스의 영역을 축소해서 임시 캔버스에 그림 + mosaicCtx.drawImage(canvasEl, x, y, width, height, 0, 0, smallW, smallH); + + // 2. 임시 캔버스의 내용을 다시 원본 캔버스에 확대해서 그림 (픽셀화) + ctx.save(); + ctx.imageSmoothingEnabled = false; // 픽셀화 효과를 위해 스무딩 끄기 + ctx.drawImage(mosaicCanvas, 0, 0, smallW, smallH, x, y, width, height); + ctx.restore(); + } function connect() { const ws = new WebSocket(uri); @@ -362,6 +406,7 @@ document.addEventListener('DOMContentLoaded', () => { ws.onmessage = (event) => { const data = event.data; if (typeof data === "string") { + console.log("-----" + data); updateLogPanel(data); try { const meta = JSON.parse(data); @@ -369,13 +414,9 @@ document.addEventListener('DOMContentLoaded', () => { lastFrameMeta = meta; showDetections(meta); } else if (meta.type === "card" && (currentModelCode === 'FACEATTR' || currentModelCode === 'ABNORM' || currentModelCode === 'LPR')) { - lastFrameMeta = meta; // 카드 데이터도 BBox를 포함할 수 있으므로 저장 - showDetections(meta); // BBox 그리기 - updateCardPanel(meta.items); // 카드 패널 업데이트 - - if (meta.type === "card" && currentModelCode === 'LPR') { - console.log("-----" + JSON.stringify(meta)); - } + lastFrameMeta = meta; + showDetections(meta); + updateCardPanel(meta.items); } } catch (e) { console.error(e); @@ -404,6 +445,32 @@ document.addEventListener('DOMContentLoaded', () => { viewConfig = {r, dx, dy}; ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); ctx.drawImage(bmp, dx, dy, dw, dh); + + if (currentModelCode === 'VIPTRACK' && lastFrameMeta && lastFrameMeta.items) { + lastFrameMeta.items.forEach(item => { + const tagId = item.tag !== undefined ? item.tag : -1; + const clsId = item.cls !== undefined ? item.cls : -1; + let displayClassName = ''; + if (LABEL_MAP[tagId] && LABEL_MAP[tagId].classes[clsId]) { + displayClassName = LABEL_MAP[tagId].classes[clsId]; + } + + if (displayClassName === 'face') { + const { r, dx, dy } = viewConfig; + const x1 = item.x1 || 0, y1 = item.y1 || 0, x2 = item.x2 || 0, y2 = item.y2 || 0; + const screenX = dx + (x1 * r), screenY = dy + (y1 * r); + const screenW = (x2 - x1) * r, screenH = (y2 - y1) * r; + + const tid = item.tid; + const isMosaicOn = (tid !== undefined && mosaicState[tid] !== undefined) ? mosaicState[tid] : true; + + if (isMosaicOn && screenW > 0 && screenH > 0) { + applyMosaic(screenX, screenY, screenW, screenH); + } + } + }); + } + bmp.close(); updateBboxContainerPosition(); }); @@ -411,11 +478,118 @@ document.addEventListener('DOMContentLoaded', () => { }; } + if (canvasEl) { + canvasEl.addEventListener('click', (e) => { + if (currentModelCode !== 'VIPTRACK' || !lastFrameMeta || !lastFrameMeta.items) return; + + const rect = canvasEl.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + + const { r, dx, dy } = viewConfig; + + lastFrameMeta.items.forEach(item => { + const tagId = item.tag !== undefined ? item.tag : -1; + const clsId = item.cls !== undefined ? item.cls : -1; + let displayClassName = ''; + if (LABEL_MAP[tagId] && LABEL_MAP[tagId].classes[clsId]) { + displayClassName = LABEL_MAP[tagId].classes[clsId]; + } + + if (displayClassName === 'face') { + const x1 = item.x1 || 0; + const y1 = item.y1 || 0; + const x2 = item.x2 || 0; + const y2 = item.y2 || 0; + + const screenX = dx + (x1 * r); + const screenY = dy + (y1 * r); + const screenW = (x2 - x1) * r; + const screenH = (y2 - y1) * r; + + if (clickX >= screenX && clickX <= screenX + screenW && + clickY >= screenY && clickY <= screenY + screenH) { + + const tid = item.tid; + if (tid !== undefined && tid !== 65535) { + if (mosaicState[tid] === undefined) { + mosaicState[tid] = false; + } else { + mosaicState[tid] = !mosaicState[tid]; + } + } + } + } + }); + }); + + canvasEl.addEventListener('mousemove', (e) => { + if (currentModelCode !== 'VIPTRACK' || !lastFrameMeta || !lastFrameMeta.items) { + canvasEl.style.cursor = 'default'; + return; + } + + const rect = canvasEl.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + const { r, dx, dy } = viewConfig; + let onFace = false; + + for (const item of lastFrameMeta.items) { + const tagId = item.tag !== undefined ? item.tag : -1; + const clsId = item.cls !== undefined ? item.cls : -1; + let displayClassName = ''; + if (LABEL_MAP[tagId] && LABEL_MAP[tagId].classes[clsId]) { + displayClassName = LABEL_MAP[tagId].classes[clsId]; + } + + if (displayClassName === 'face' && item.tid !== 65535) { + const x1 = item.x1 || 0, y1 = item.y1 || 0, x2 = item.x2 || 0, y2 = item.y2 || 0; + const screenX = dx + (x1 * r), screenY = dy + (y1 * r); + const screenW = (x2 - x1) * r, screenH = (y2 - y1) * r; + + if (mouseX >= screenX && mouseX <= screenX + screenW && + mouseY >= screenY && mouseY <= screenY + screenH) { + onFace = true; + break; + } + } + } + canvasEl.style.cursor = onFace ? 'pointer' : 'default'; + }); + } + function showDetections(meta) { bboxContainerEl.innerHTML = ""; + + if (currentModelCode === 'CROWD' && meta.trajectory) { + const personColor = getBoxColor('person'); + ctx.save(); + ctx.lineWidth = 2; + ctx.strokeStyle = personColor; + + Object.values(meta.trajectory).forEach(points => { + if (points.length < 2) return; + + ctx.beginPath(); + const startX = viewConfig.dx + (points[0][0] * viewConfig.r); + const startY = viewConfig.dy + (points[0][1] * viewConfig.r); + ctx.moveTo(startX, startY); + + for (let i = 1; i < points.length; i++) { + const px = viewConfig.dx + (points[i][0] * viewConfig.r); + const py = viewConfig.dy + (points[i][1] * viewConfig.r); + ctx.lineTo(px, py); + } + ctx.stroke(); + }); + ctx.restore(); + } + const items = meta.items || []; const currentCounts = {}; const shouldShowLabel = showLabelToggle ? showLabelToggle.checked : false; + const shouldShowTid = showTidToggle ? showTidToggle.checked : false; items.forEach((it) => { const tagId = it.tag !== undefined ? it.tag : -1; @@ -457,7 +631,15 @@ document.addEventListener('DOMContentLoaded', () => { boxDiv.style.height = `${screenH}px`; boxDiv.style.borderColor = boxColor; + let labelText = ''; if (shouldShowLabel) { + labelText += displayClassName; + } + if (shouldShowTid && it.tid !== undefined) { + labelText += ` ${it.tid}`; + } + + if (labelText) { const label = document.createElement('div'); label.style.position = 'absolute'; label.style.top = '-20px'; @@ -467,7 +649,7 @@ document.addEventListener('DOMContentLoaded', () => { label.style.fontSize = '11px'; label.style.fontWeight = 'bold'; label.style.padding = '1px 4px'; - label.textContent = displayClassName; + label.textContent = labelText.trim(); boxDiv.appendChild(label); } @@ -475,31 +657,51 @@ document.addEventListener('DOMContentLoaded', () => { }); if (currentModelCode !== 'FACEATTR' && currentModelCode !== 'ABNORM' && currentModelCode !== 'LPR') { - updateSummaryPanel(currentCounts); + updateSummaryPanel(currentCounts, meta.risk); } } - function updateSummaryPanel(counts) { + function updateSummaryPanel(counts, riskData) { if (!summaryListEl) return; - summaryListEl.innerHTML = ''; - const keys = Object.keys(counts); - if (keys.length === 0) { - summaryListEl.innerHTML = '
No detection
'; - return; - } - keys.forEach(key => { + + Object.keys(counts).forEach(key => detectedClasses.add(key)); + + summaryListEl.innerHTML = ''; + detectedClasses.forEach(key => { + const count = counts[key] || 0; const row = document.createElement('div'); row.className = 'summary-row'; const color = getBoxColor(key); const colorBox = ``; - row.innerHTML = `${colorBox}${key}${counts[key]}`; + row.innerHTML = `${colorBox}${key}${count}`; summaryListEl.appendChild(row); }); + + if (currentModelCode === 'CROWD' && riskData) { + const distRisk = (riskData.dist_risk !== undefined) ? (riskData.dist_risk * 100).toFixed(2) : '-'; + const motionRisk = (riskData.motion_risk !== undefined) ? (riskData.motion_risk * 100).toFixed(2) : '-'; + const totalRisk = (riskData.risk_total !== undefined) ? (riskData.risk_total * 100).toFixed(2) : '-'; + + riskSummaryContainer.innerHTML = ` +
+ 거리 위험도(%) + ${distRisk} +
+
+ 이동 위험도(%) + ${motionRisk} +
+
+ 종합 위험도(%) + ${totalRisk} +
+ `; + } } function updateCardPanel(items) { if (!summaryListEl) return; - summaryListEl.innerHTML = ''; // 기존 내용 초기화 + summaryListEl.innerHTML = ''; if (!items || items.length === 0) { summaryListEl.innerHTML = '
No card data
'; @@ -528,7 +730,7 @@ document.addEventListener('DOMContentLoaded', () => { card.innerHTML = `
-
쓰러짐 발생
+
Event ID: ${item.tid || '-'}
`; } else if (currentModelCode === 'LPR') { diff --git a/public/dashboard.html b/public/dashboard.html index 75ce9f3..6519b97 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -15,6 +15,10 @@
+
+ + +
@@ -146,6 +150,9 @@ -
+