|
|
|
|
// app.js
|
|
|
|
|
|
|
|
|
|
// DOM(HTML)이 모두 로드되었을 때 실행
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
|
|
|
|
// 탭 버튼과 콘텐츠 요소를 가져옵니다.
|
|
|
|
|
const tabVideo = document.getElementById('tab-video');
|
|
|
|
|
const tabModels = document.getElementById('tab-models');
|
|
|
|
|
|
|
|
|
|
const contentVideo = document.getElementById('content-video');
|
|
|
|
|
const contentModels = document.getElementById('content-models');
|
|
|
|
|
|
|
|
|
|
// 비디오 탭 클릭 시
|
|
|
|
|
if (tabVideo && tabModels) { // 탭 요소가 있는지 확인
|
|
|
|
|
tabVideo.addEventListener('click', () => {
|
|
|
|
|
// 버튼 활성화
|
|
|
|
|
tabVideo.classList.add('active');
|
|
|
|
|
tabModels.classList.remove('active');
|
|
|
|
|
|
|
|
|
|
// 콘텐츠 표시
|
|
|
|
|
contentVideo.classList.add('active');
|
|
|
|
|
contentModels.classList.remove('active');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AI 모델 탭 클릭 시
|
|
|
|
|
if (tabVideo && tabModels) { // 탭 요소가 있는지 확인
|
|
|
|
|
tabModels.addEventListener('click', () => {
|
|
|
|
|
// 버튼 활성화
|
|
|
|
|
tabModels.classList.add('active');
|
|
|
|
|
tabVideo.classList.remove('active');
|
|
|
|
|
|
|
|
|
|
// 콘텐츠 표시
|
|
|
|
|
contentModels.classList.add('active');
|
|
|
|
|
contentVideo.classList.remove('active');
|
|
|
|
|
|
|
|
|
|
// [추가] AI 모델 탭을 클릭할 때 목록 새로고침
|
|
|
|
|
loadModelList();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== AI 모델 목록 로드 함수 ==========
|
|
|
|
|
function loadModelList() {
|
|
|
|
|
// 테이블 본문(tbody)을 선택
|
|
|
|
|
const tableBody = document.querySelector('.model-table tbody');
|
|
|
|
|
if (!tableBody) return; // 테이블이 없으면 중단
|
|
|
|
|
|
|
|
|
|
console.log('Loading model list...');
|
|
|
|
|
|
|
|
|
|
fetch('/list-models') // server.js에 추가한 엔드포인트
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(models => {
|
|
|
|
|
// models = { "OBJDET": { "file": "...", "version": "..." }, ... }
|
|
|
|
|
|
|
|
|
|
// 테이블의 모든 'data-role' 행을 순회
|
|
|
|
|
tableBody.querySelectorAll('tr[data-role]').forEach(row => {
|
|
|
|
|
const role = row.dataset.role; // e.g., "OBJDET"
|
|
|
|
|
const modelData = models[role]; // 해당 역할의 모델 데이터 (없으면 undefined)
|
|
|
|
|
|
|
|
|
|
const fileCell = row.querySelector('.model-filename');
|
|
|
|
|
const versionCell = row.querySelector('.model-version');
|
|
|
|
|
const deleteButton = row.querySelector('.btn-delete'); // [수정] 삭제 버튼 선택
|
|
|
|
|
|
|
|
|
|
if (modelData) {
|
|
|
|
|
// 일치하는 파일이 있으면, 파일명과 버전 업데이트
|
|
|
|
|
if (fileCell) fileCell.textContent = modelData.file;
|
|
|
|
|
if (versionCell) versionCell.textContent = modelData.version;
|
|
|
|
|
// [수정] 삭제 버튼 표시
|
|
|
|
|
if (deleteButton) deleteButton.classList.remove('hidden');
|
|
|
|
|
} else {
|
|
|
|
|
// 일치하는 파일이 없으면, 기본값 '-'으로 설정
|
|
|
|
|
if (fileCell) fileCell.textContent = '-';
|
|
|
|
|
if (versionCell) versionCell.textContent = '-';
|
|
|
|
|
// [수정] 삭제 버튼 숨김
|
|
|
|
|
if (deleteButton) deleteButton.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
console.error('모델 목록 로드 실패:', error);
|
|
|
|
|
alert('모델 목록을 불러오는 데 실패했습니다.');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== AI 모델 목록 로드 함수 끝 ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========== [추가됨] 모델 삭제 요청 함수 ==========
|
|
|
|
|
function deleteModelFile(filename) {
|
|
|
|
|
console.log(`Requesting deletion of: ${filename}`);
|
|
|
|
|
|
|
|
|
|
fetch('/delete-model', { // server.js에 추가할 엔드포인트
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({filename: filename}) // { "filename": "CUUVA_..." }
|
|
|
|
|
})
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
alert(`파일이 성공적으로 삭제되었습니다: ${filename}`);
|
|
|
|
|
loadModelList(); // 삭제 성공 시 목록 새로고침
|
|
|
|
|
} else {
|
|
|
|
|
alert(`삭제 실패: ${data.message}`);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
console.error('Delete error:', error);
|
|
|
|
|
alert('삭제 중 오류가 발생했습니다.');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 모델 삭제 요청 함수 끝 ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========== AI 모델 업로드/관리 로직 시작 ==========
|
|
|
|
|
if (contentModels) {
|
|
|
|
|
// 전역 업로드 관련 요소 가져오기
|
|
|
|
|
const globalFileInput = document.getElementById('global-file-input');
|
|
|
|
|
const fileNameDisplay = document.getElementById('file-name-display');
|
|
|
|
|
const globalUploadButton = document.getElementById('global-upload-button');
|
|
|
|
|
|
|
|
|
|
// 새로고침 버튼 요소
|
|
|
|
|
const refreshButton = contentModels.querySelector('.refresh-button');
|
|
|
|
|
|
|
|
|
|
// [추가] 모델 테이블 본문 (이벤트 위임용)
|
|
|
|
|
const tableBody = contentModels.querySelector('.model-table tbody');
|
|
|
|
|
|
|
|
|
|
// '찾아보기'로 파일 선택 시
|
|
|
|
|
if (globalFileInput && fileNameDisplay) {
|
|
|
|
|
globalFileInput.addEventListener('change', () => {
|
|
|
|
|
if (globalFileInput.files.length > 0) {
|
|
|
|
|
const file = globalFileInput.files[0];
|
|
|
|
|
|
|
|
|
|
// 파일 확장자 확인
|
|
|
|
|
if (!file.name.endsWith('.aiwbin')) {
|
|
|
|
|
alert('잘못된 파일 형식입니다. .aiwbin 파일만 선택할 수 있습니다.');
|
|
|
|
|
globalFileInput.value = ''; // 파일 선택 초기화
|
|
|
|
|
fileNameDisplay.textContent = '선택된 파일 없음';
|
|
|
|
|
} else {
|
|
|
|
|
// 파일명 표시
|
|
|
|
|
fileNameDisplay.textContent = file.name;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
fileNameDisplay.textContent = '선택된 파일 없음';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// '업로드' 버튼 클릭 시
|
|
|
|
|
if (globalUploadButton && globalFileInput) {
|
|
|
|
|
globalUploadButton.addEventListener('click', () => {
|
|
|
|
|
// 파일이 선택되었는지 확인
|
|
|
|
|
if (globalFileInput.files.length === 0) {
|
|
|
|
|
alert('먼저 .aiwbin 파일을 선택해주세요.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = globalFileInput.files[0];
|
|
|
|
|
|
|
|
|
|
// (이중 확인) 파일 확장자 확인
|
|
|
|
|
if (!file.name.endsWith('.aiwbin')) {
|
|
|
|
|
alert('잘못된 파일 형식입니다. .aiwbin 파일만 업로드할 수 있습니다.');
|
|
|
|
|
globalFileInput.value = '';
|
|
|
|
|
fileNameDisplay.textContent = '선택된 파일 없음';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FormData 객체 생성
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('modelFile', file); // 'modelFile'은 server.js와 일치해야 함
|
|
|
|
|
|
|
|
|
|
// 업로드 중 버튼 비활성화
|
|
|
|
|
globalUploadButton.disabled = true;
|
|
|
|
|
globalUploadButton.textContent = '업로드 중...';
|
|
|
|
|
|
|
|
|
|
// Fetch API를 사용하여 파일 업로드
|
|
|
|
|
fetch('/upload-model', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: formData
|
|
|
|
|
})
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
alert(`업로드 성공!\n파일: ${file.name}`);
|
|
|
|
|
globalFileInput.value = ''; // 파일 선택 초기화
|
|
|
|
|
fileNameDisplay.textContent = '선택된 파일 없음';
|
|
|
|
|
|
|
|
|
|
// 업로드 성공 시 모델 목록 새로고침
|
|
|
|
|
loadModelList();
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
alert(`업로드 실패: ${data.message}`);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
console.error('Upload error:', error);
|
|
|
|
|
alert('업로드 중 오류가 발생했습니다.');
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
// 버튼 다시 활성화
|
|
|
|
|
globalUploadButton.disabled = false;
|
|
|
|
|
globalUploadButton.textContent = '업로드';
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// '새로고침' 버튼 클릭 시
|
|
|
|
|
if (refreshButton) {
|
|
|
|
|
refreshButton.addEventListener('click', () => {
|
|
|
|
|
loadModelList(); // 목록 새로고침 함수 호출
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== [추가됨] 삭제 버튼 이벤트 리스너 (이벤트 위임) ==========
|
|
|
|
|
if (tableBody) {
|
|
|
|
|
tableBody.addEventListener('click', (event) => {
|
|
|
|
|
// 클릭된 요소가 'btn-delete' 클래스를 가지고 있는지 확인
|
|
|
|
|
if (event.target.classList.contains('btn-delete')) {
|
|
|
|
|
|
|
|
|
|
// 클릭된 버튼에서 가장 가까운 <tr>(행)을 찾습니다.
|
|
|
|
|
const row = event.target.closest('tr');
|
|
|
|
|
if (!row) return;
|
|
|
|
|
|
|
|
|
|
// 행에서 파일명 셀(.model-filename)을 찾아 파일명을 가져옵니다.
|
|
|
|
|
const fileCell = row.querySelector('.model-filename');
|
|
|
|
|
const filename = fileCell ? fileCell.textContent : null;
|
|
|
|
|
|
|
|
|
|
// 파일명이 없거나 '-' 이면 (파일이 할당되지 않음) 중단
|
|
|
|
|
if (!filename || filename === '-') {
|
|
|
|
|
alert('삭제할 파일이 없습니다.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 행의 두 번째 셀(<td>)에서 역할 텍스트를 가져옵니다.
|
|
|
|
|
const roleText = row.cells[1] ? row.cells[1].textContent : '알 수 없는 역할';
|
|
|
|
|
|
|
|
|
|
// 사용자에게 삭제 확인을 받습니다.
|
|
|
|
|
if (confirm(`정말로 이 모델 파일을 삭제하시겠습니까?\n\n역할: ${roleText}\n파일: ${filename}`)) {
|
|
|
|
|
// 확인 시, 삭제 함수 호출
|
|
|
|
|
deleteModelFile(filename);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// ========== 삭제 버튼 이벤트 리스너 끝 ==========
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
// ========== AI 모델 업로드/관리 로직 끝 ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 로그아웃 버튼 처리 ==========
|
|
|
|
|
const logoutButton = document.getElementById('logout-button');
|
|
|
|
|
|
|
|
|
|
if (logoutButton) { // 로그아웃 버튼이 있는지 확인
|
|
|
|
|
logoutButton.addEventListener('click', () => {
|
|
|
|
|
console.log('Logout clicked');
|
|
|
|
|
|
|
|
|
|
// [수정됨] 사용자에게 확인을 받습니다.
|
|
|
|
|
if (confirm('정말로 로그아웃하시겠습니까?')) {
|
|
|
|
|
// 확인을 누르면 로그인 페이지(루트 '/')로 이동
|
|
|
|
|
window.location.href = '/';
|
|
|
|
|
}
|
|
|
|
|
// 취소를 누르면 아무 일도 일어나지 않습니다.
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// ========== 로그아웃 버튼 처리 끝 ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========== dashboard.html 스크립트 추가 시작 ==========
|
|
|
|
|
const uri = "ws://10.10.11.246:8765"; // 필요하면 여기만 수정
|
|
|
|
|
const imgEl = document.getElementById("frame");
|
|
|
|
|
const statusEl = document.getElementById("status");
|
|
|
|
|
const frameInfoEl = document.getElementById("frame-info");
|
|
|
|
|
const detListEl = document.getElementById("det-list");
|
|
|
|
|
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
|
|
|
|
|
'#38BFFF', // 1: Blue
|
|
|
|
|
'#38FF4E', // 2: Green
|
|
|
|
|
'#FFF238', // 3: Yellow
|
|
|
|
|
'#FF9D38', // 4: Orange
|
|
|
|
|
'#C538FF', // 5: Purple
|
|
|
|
|
'#FF38C5', // 6: Pink
|
|
|
|
|
'#FFFFFF' // 7: White
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// [추가] FPS 계산용 변수
|
|
|
|
|
let frameCount = 0;
|
|
|
|
|
let lastFpsCheckTime = performance.now();
|
|
|
|
|
|
|
|
|
|
let lastFrameMeta = null;
|
|
|
|
|
|
|
|
|
|
if (imgEl && statusEl && frameInfoEl && detListEl && bboxContainerEl && modelContainerEl) {
|
|
|
|
|
|
|
|
|
|
// 모델 변경 이벤트 리스너
|
|
|
|
|
modelContainerEl.addEventListener('change', (event) => {
|
|
|
|
|
if (event.target && event.target.name === 'current-model') {
|
|
|
|
|
// [추가] 모델 이름 즉시 업데이트
|
|
|
|
|
updateModelDisplay();
|
|
|
|
|
|
|
|
|
|
const selectedModel = event.target.value;
|
|
|
|
|
console.log(`Model changed to: ${selectedModel}`);
|
|
|
|
|
|
|
|
|
|
fetch('/set-model', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({model: selectedModel}),
|
|
|
|
|
})
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
console.log('Server response:', data);
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
logStatus(`모델 변경 완료: ${selectedModel}`);
|
|
|
|
|
} else {
|
|
|
|
|
logStatus(`모델 변경 실패: ${data.message}`, true);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
console.error('Error setting model:', error);
|
|
|
|
|
logStatus('모델 변경 중 오류 발생', true);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 박스 컨테이너 위치/크기 조절 함수
|
|
|
|
|
function updateBboxContainerPosition() {
|
|
|
|
|
if (!imgEl || !bboxContainerEl) return;
|
|
|
|
|
const top = imgEl.offsetTop;
|
|
|
|
|
const left = imgEl.offsetLeft;
|
|
|
|
|
const width = imgEl.offsetWidth;
|
|
|
|
|
const height = imgEl.offsetHeight;
|
|
|
|
|
bboxContainerEl.style.top = `${top}px`;
|
|
|
|
|
bboxContainerEl.style.left = `${left}px`;
|
|
|
|
|
bboxContainerEl.style.width = `${width}px`;
|
|
|
|
|
bboxContainerEl.style.height = `${height}px`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function logStatus(msg, isError = false) {
|
|
|
|
|
statusEl.textContent = msg;
|
|
|
|
|
statusEl.className = "status" + (isError ? " err" : "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showFrameMeta(meta) {
|
|
|
|
|
frameInfoEl.textContent =
|
|
|
|
|
` | 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 || [];
|
|
|
|
|
let lines = [];
|
|
|
|
|
lines.push(`DET ch=${meta.ch} seq=${meta.seq} ts=${meta.ts_us} cnt=${items.length}`);
|
|
|
|
|
bboxContainerEl.innerHTML = "";
|
|
|
|
|
|
|
|
|
|
if (!imgEl || !lastFrameMeta) {
|
|
|
|
|
// [수정됨] x1,y1,x2,y2 형식에 맞게 텍스트 로그 수정
|
|
|
|
|
items.forEach((it, i) => {
|
|
|
|
|
// x1, y1, x2, y2 -> x, y, w, h
|
|
|
|
|
const x = it.x1;
|
|
|
|
|
const y = it.y1;
|
|
|
|
|
const w = it.x2 - it.x1;
|
|
|
|
|
const h = it.y2 - it.y1;
|
|
|
|
|
|
|
|
|
|
lines.push(
|
|
|
|
|
`#${i} cls=${it.cls} tid=${it.tid} tag=${it.tag}` // prob, resv 대신 tid, tag 사용
|
|
|
|
|
+ ` x=${x.toFixed(3)} y=${y.toFixed(3)}`
|
|
|
|
|
+ ` w=${w.toFixed(3)} h=${h.toFixed(3)}`
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
detListEl.textContent = lines.join("\n");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateBboxContainerPosition();
|
|
|
|
|
const imgWidth = imgEl.clientWidth;
|
|
|
|
|
const imgHeight = imgEl.clientHeight;
|
|
|
|
|
const frameWidth = lastFrameMeta.w;
|
|
|
|
|
const frameHeight = lastFrameMeta.h;
|
|
|
|
|
|
|
|
|
|
if (frameWidth === 0 || frameHeight === 0 || imgWidth === 0 || imgHeight === 0) {
|
|
|
|
|
// [수정됨] x1,y1,x2,y2 형식에 맞게 텍스트 로그 수정
|
|
|
|
|
items.forEach((it, i) => {
|
|
|
|
|
// x1, y1, x2, y2 -> x, y, w, h
|
|
|
|
|
const x = it.x1;
|
|
|
|
|
const y = it.y1;
|
|
|
|
|
const w = it.x2 - it.x1;
|
|
|
|
|
const h = it.y2 - it.y1;
|
|
|
|
|
|
|
|
|
|
lines.push(
|
|
|
|
|
`#${i} cls=${it.cls} tid=${it.tid} tag=${it.tag}` // prob, resv 대신 tid, tag 사용
|
|
|
|
|
+ ` x=${x.toFixed(3)} y=${y.toFixed(3)}`
|
|
|
|
|
+ ` w=${w.toFixed(3)} h=${h.toFixed(3)}`
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
detListEl.textContent = lines.join("\n");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const widthRatio = imgWidth / frameWidth;
|
|
|
|
|
const heightRatio = imgHeight / frameHeight;
|
|
|
|
|
const ratio = Math.min(widthRatio, heightRatio);
|
|
|
|
|
const videoDisplayWidth = frameWidth * ratio;
|
|
|
|
|
const videoDisplayHeight = frameHeight * ratio;
|
|
|
|
|
const offsetX = (imgWidth - videoDisplayWidth) / 2;
|
|
|
|
|
const offsetY = (imgHeight - videoDisplayHeight) / 2;
|
|
|
|
|
|
|
|
|
|
// [수정됨] x1,y1,x2,y2 형식에 맞게 텍스트 로그 및 박스 계산 수정
|
|
|
|
|
items.forEach((it, i) => {
|
|
|
|
|
// x1, y1, x2, y2 -> x, y, w, h
|
|
|
|
|
const x = it.x1;
|
|
|
|
|
const y = it.y1;
|
|
|
|
|
const w = it.x2 - it.x1;
|
|
|
|
|
const h = it.y2 - it.y1;
|
|
|
|
|
|
|
|
|
|
// [추가] 현재 항목의 클래스(cls) 가져오기
|
|
|
|
|
const cls = it.cls;
|
|
|
|
|
|
|
|
|
|
lines.push(
|
|
|
|
|
`#${i} cls=${cls} tid=${it.tid} tag=${it.tag}` // prob, resv 대신 tid, tag 사용
|
|
|
|
|
+ ` x=${x.toFixed(3)} y=${y.toFixed(3)}`
|
|
|
|
|
+ ` w=${w.toFixed(3)} h=${h.toFixed(3)}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 계산된 x, y, w, h를 사용하여 박스 위치 계산
|
|
|
|
|
const boxLeft = (x * ratio) + offsetX;
|
|
|
|
|
const boxTop = (y * ratio) + offsetY;
|
|
|
|
|
const boxWidth = w * ratio;
|
|
|
|
|
const boxHeight = h * ratio;
|
|
|
|
|
|
|
|
|
|
// [추가] 클래스(cls) 값에 따라 색상 결정
|
|
|
|
|
// cls를 정수로 변환 (혹시 문자열일 경우 대비)
|
|
|
|
|
const classIndex = parseInt(cls, 10) || 0;
|
|
|
|
|
// 모듈러(%) 연산을 사용해 색상 배열을 순환
|
|
|
|
|
const color = CLASS_COLORS[classIndex % CLASS_COLORS.length];
|
|
|
|
|
|
|
|
|
|
const bbox = document.createElement('div');
|
|
|
|
|
bbox.className = 'bbox';
|
|
|
|
|
bbox.style.left = `${boxLeft}px`;
|
|
|
|
|
bbox.style.top = `${boxTop}px`;
|
|
|
|
|
bbox.style.width = `${boxWidth}px`;
|
|
|
|
|
bbox.style.height = `${boxHeight}px`;
|
|
|
|
|
|
|
|
|
|
// [추가] CSS의 'border' 대신 'borderColor'를 동적으로 설정
|
|
|
|
|
// 이렇게 하면 style.css의 'border-width', 'border-style'은 유지됩니다.
|
|
|
|
|
bbox.style.borderColor = color;
|
|
|
|
|
|
|
|
|
|
bboxContainerEl.appendChild(bbox);
|
|
|
|
|
});
|
|
|
|
|
detListEl.textContent = lines.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WebSocket 연결 함수
|
|
|
|
|
function connect() {
|
|
|
|
|
const ws = new WebSocket(uri);
|
|
|
|
|
ws.binaryType = "arraybuffer";
|
|
|
|
|
ws.onopen = () => {
|
|
|
|
|
logStatus(`연결됨: ${uri}`);
|
|
|
|
|
};
|
|
|
|
|
ws.onclose = (ev) => {
|
|
|
|
|
logStatus(`연결 종료 (code=${ev.code}) 재접속 대기중...`, true);
|
|
|
|
|
setTimeout(connect, 2000);
|
|
|
|
|
};
|
|
|
|
|
ws.onerror = (err) => {
|
|
|
|
|
logStatus("WebSocket 오류 발생", true);
|
|
|
|
|
console.error(err);
|
|
|
|
|
};
|
|
|
|
|
ws.onmessage = (event) => {
|
|
|
|
|
const data = event.data;
|
|
|
|
|
if (typeof data === "string") {
|
|
|
|
|
try {
|
|
|
|
|
const meta = JSON.parse(data);
|
|
|
|
|
const t = meta.type;
|
|
|
|
|
if (t === "frame") {
|
|
|
|
|
lastFrameMeta = meta;
|
|
|
|
|
showFrameMeta(meta);
|
|
|
|
|
} else if (t === "det") {
|
|
|
|
|
showDetections(meta);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("JSON parse error:", e, data);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (data instanceof ArrayBuffer && lastFrameMeta) {
|
|
|
|
|
const blob = new Blob([data], {type: "image/jpeg"});
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
// 창 크기 변경 시 이벤트
|
|
|
|
|
window.addEventListener('resize', updateBboxContainerPosition);
|
|
|
|
|
|
|
|
|
|
} // if (요소 확인) 끝
|
|
|
|
|
// ========== video-test.html 스크립트 추가 끝 ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 페이지 로드 시 모델 목록 즉시 로드 ==========
|
|
|
|
|
loadModelList();
|
|
|
|
|
|
|
|
|
|
});
|