diff --git a/public/app.js b/public/app.js index 8140253..c902466 100644 --- a/public/app.js +++ b/public/app.js @@ -277,6 +277,10 @@ document.addEventListener('DOMContentLoaded', () => { const bboxContainerEl = document.getElementById("bbox-container"); const modelContainerEl = document.getElementById("current-model-container"); + // [추가] 오버레이 표시용 요소 + const modelDisplayEl = document.getElementById("current-model-display"); + const fpsDisplayEl = document.getElementById("fps-display"); + // [추가] 클래스별 색상 팔레트 (원하는 색상으로 수정 가능) const CLASS_COLORS = [ '#FF3838', // 0: Red @@ -289,6 +293,10 @@ document.addEventListener('DOMContentLoaded', () => { '#FFFFFF' // 7: White ]; + // [추가] FPS 계산용 변수 + let frameCount = 0; + let lastFpsCheckTime = performance.now(); + let lastFrameMeta = null; if (imgEl && statusEl && frameInfoEl && detListEl && bboxContainerEl && modelContainerEl) { @@ -296,6 +304,9 @@ document.addEventListener('DOMContentLoaded', () => { // 모델 변경 이벤트 리스너 modelContainerEl.addEventListener('change', (event) => { if (event.target && event.target.name === 'current-model') { + // [추가] 모델 이름 즉시 업데이트 + updateModelDisplay(); + const selectedModel = event.target.value; console.log(`Model changed to: ${selectedModel}`); @@ -345,6 +356,25 @@ document.addEventListener('DOMContentLoaded', () => { ` | FRAME ch=${meta.ch} ts=${meta.ts_us} w=${meta.w} h=${meta.h}`; } + // [추가] 선택된 AI 모델 이름을 상단 우측에 표시하는 함수 + function updateModelDisplay() { + if (!modelContainerEl || !modelDisplayEl) return; + + // 현재 선택된 라디오 버튼을 찾습니다. + const checkedRadio = modelContainerEl.querySelector('input[name="current-model"]:checked'); + if (checkedRadio) { + // 해당 라디오 버튼의 ID로 label을 찾습니다. + const label = modelContainerEl.querySelector(`label[for="${checkedRadio.id}"]`); + if (label) { + modelDisplayEl.textContent = label.textContent; // "군중 위험 인식" + } else { + modelDisplayEl.textContent = checkedRadio.value; // "CROWD" (대체) + } + } else { + modelDisplayEl.textContent = "모델 선택 안됨"; + } + } + // Detections 표시 함수 (수정됨) function showDetections(meta) { const items = meta.items || []; @@ -487,12 +517,27 @@ document.addEventListener('DOMContentLoaded', () => { imgEl.onload = () => { URL.revokeObjectURL(url); updateBboxContainerPosition(); + + // [추가] FPS 계산 + frameCount++; + const now = performance.now(); + const delta = now - lastFpsCheckTime; + + if (delta >= 1000 && fpsDisplayEl) { // 1초마다 업데이트 + const fps = (frameCount * 1000) / delta; + fpsDisplayEl.textContent = `${Math.round(fps)} FPS`; + frameCount = 0; + lastFpsCheckTime = now; + } }; imgEl.src = url; } }; } + // [추가] 초기 모델 이름 설정 + updateModelDisplay(); + // WebSocket 연결 시작 connect(); diff --git a/public/dashboard.html b/public/dashboard.html index e9c0778..bd139f4 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -30,6 +30,10 @@ 연결 시도 중... + + + + video frame
@@ -54,13 +58,13 @@ - - + + - + @@ -79,7 +83,8 @@ 번호 Al Model 역할 - 파일명 현재 버전 + 파일명 + 현재 버전 삭제 @@ -101,7 +106,8 @@ - 3 + + 3 이상행동(쓰러짐, 폭행) 감지 - v1.0 diff --git a/public/style.css b/public/style.css index 0523a1b..10df12d 100644 --- a/public/style.css +++ b/public/style.css @@ -553,4 +553,42 @@ main { justify-content: center; /* 수평 중앙 정렬 */ align-items: center; /* 수직 중앙 정렬 */ gap: 20px; /* 자식 요소(이미지) 사이의 간격 */ +} + +/* ====================================================== */ +/* [수정] 비디오 오버레이 (FPS, 모델명) 스타일 */ +/* ====================================================== */ +.video-overlay { + position: absolute; + z-index: 10; /* #info와 동일한 레벨 */ + background-color: rgba(0, 0, 0, 0.5); /* 반투명 검은 배경 */ + color: #ffffff; /* 흰색 텍스트 */ + padding: 4px 8px; + border-radius: 4px; + font-size: 14px; /* 기본 폰트 크기 */ + font-weight: bold; + pointer-events: none; /* 클릭 이벤트 방지 */ + white-space: nowrap; /* 줄바꿈 방지 */ + + /* [추가] 요청사항: 가독성을 위한 그림자 */ + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8); +} + +.video-overlay.top-right { + top: 100px; + right: 10px; + + /* [추가] 요청사항: AI 모델명 3배 크게 (14px * 3) */ + font-size: 60px; +} + +.video-overlay.bottom-right { + /* [수정] 요청사항: 10px -> 30px (조금 더 위로) */ + bottom: 70px; + right: 10px; + + /* [추가] 요청사항: FPS 표시 2배 크게 (14px * 2) */ + font-size: 28px; + /* [추가] 요청사항: 폰트 색상 노란색 */ + color: #FFEE00; } \ No newline at end of file diff --git a/server.js b/server.js index 55d9f1b..f5c9d5a 100644 --- a/server.js +++ b/server.js @@ -58,9 +58,9 @@ app.get('/dashboard', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); }); -// 6. 모델 변경 API 엔드포인트 +// 6. 모델 변경 API 엔드포인트 (*** 수정된 부분 ***) app.post('/set-model', (req, res) => { - const { model } = req.body; // app.js에서 보낸 { model: "..." } + const { model } = req.body; // app.js에서 보낸 { model: "OBJDET" } 등 if (!model) { return res.status(400).json({ status: 'error', message: '모델 값이 없습니다.' }); @@ -68,40 +68,42 @@ app.post('/set-model', (req, res) => { console.log(`[서버] 모델 변경 명령 수신: ${model}`); - // --- 요청하신 파이썬 스크립트 실행 --- - const pythonCommand = 'python3'; - const scriptPath = '/mnt/user_data/ctrl_cli.py'; - const combinedArg = `"ON ${model}"`; - const args = [scriptPath, combinedArg]; + // --- 요청하신 쉘 스크립트 실행으로 변경 --- + const scriptCommand = '/mnt/user_data/feat_control/feat_on.sh'; + const args = [model]; // OBJDET, ABNORM, CROWD 등 - console.log(`[서버] 실행: ${pythonCommand} ${args.join(' ')}`); + console.log(`[서버] 실행: ${scriptCommand} ${args.join(' ')}`); - const py = spawn(pythonCommand, args); + // 스크립트 파일에 실행 권한(chmod +x)이 있다고 가정합니다. + const scriptProcess = spawn(scriptCommand, args); let stdoutData = ''; let stderrData = ''; - py.stdout.on('data', (data) => { - console.log(`Python stdout: ${data}`); + scriptProcess.stdout.on('data', (data) => { + console.log(`Script stdout: ${data}`); stdoutData += data.toString(); }); - py.stderr.on('data', (data) => { - console.error(`Python stderr: ${data}`); + scriptProcess.stderr.on('data', (data) => { + console.error(`Script stderr: ${data}`); stderrData += data.toString(); }); - py.on('close', (code) => { - console.log(`Python process exited with code ${code}`); + scriptProcess.on('close', (code) => { + console.log(`Script process exited with code ${code}`); if (code === 0) { res.json({ status: 'success', message: `모델이 ${model}(으)로 변경됨`, output: stdoutData }); } else { - res.status(500).json({ status: 'error', message: '파이썬 스크립트 실행 실패', error: stderrData }); + res.status(500).json({ status: 'error', message: '쉘 스크립트 실행 실패', error: stderrData }); } }); - py.on('error', (err) => { - console.error('[서버] 파이썬 프로세스 시작 실패:', err); + scriptProcess.on('error', (err) => { + console.error('[서버] 쉘 프로세스 시작 실패:', err); + // "ENOENT" 오류는 + // 1) 스크립트 경로가 잘못되었거나, + // 2) 스크립트 파일에 실행 권한이 없을 때 자주 발생합니다. res.status(500).json({ status: 'error', message: '프로세스 시작 실패', error: err.message }); }); // --- 스크립트 실행 끝 --- @@ -244,4 +246,4 @@ app.post('/delete-model', (req, res) => { app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); -//``` \ No newline at end of file +// \ No newline at end of file