vision-web-app / frontend /build /object-detection-search.html
David Ko
Add object detection vector search feature with UI and API endpoints
aa7ad0c
raw
history blame
24.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #f8f9fa;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.header {
margin-bottom: 30px;
text-align: center;
}
.upload-section, .detection-section, .search-section, .results-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #dee2e6;
border-radius: 5px;
}
.canvas-container {
position: relative;
margin: 20px 0;
}
#imageCanvas {
border: 1px solid #ddd;
max-width: 100%;
}
.object-item {
margin-bottom: 10px;
padding: 10px;
border: 1px solid #e9ecef;
border-radius: 5px;
background-color: #f8f9fa;
}
.result-item {
display: flex;
margin-bottom: 20px;
padding: 15px;
border: 1px solid #e9ecef;
border-radius: 5px;
background-color: #f8f9fa;
}
.result-image {
width: 150px;
height: 150px;
object-fit: cover;
margin-right: 15px;
border: 1px solid #ddd;
}
.result-details {
flex-grow: 1;
}
.badge {
margin-right: 5px;
}
.nav-tabs {
margin-bottom: 20px;
}
.tab-content {
padding: 20px;
border: 1px solid #dee2e6;
border-top: none;
border-radius: 0 0 5px 5px;
}
.bbox-overlay {
position: absolute;
border: 2px solid;
background-color: rgba(255, 255, 255, 0.2);
pointer-events: none;
}
.bbox-label {
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
pointer-events: none;
}
.spinner-border {
width: 1rem;
height: 1rem;
margin-right: 0.5rem;
}
.loading {
display: none;
align-items: center;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰</h1>
<p class="lead">์ด๋ฏธ์ง€์—์„œ ์ธ์‹๋œ ๊ฐ์ฒด๋ฅผ ๋ฒกํ„ฐ DB์— ์ €์žฅํ•˜๊ณ  ์œ ์‚ฌํ•œ ๊ฐ์ฒด๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.</p>
</div>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="detect-tab" data-bs-toggle="tab" data-bs-target="#detect" type="button" role="tab" aria-controls="detect" aria-selected="true">๊ฐ์ฒด ์ธ์‹ ๋ฐ ์ €์žฅ</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="search-tab" data-bs-toggle="tab" data-bs-target="#search" type="button" role="tab" aria-controls="search" aria-selected="false">๊ฐ์ฒด ๊ฒ€์ƒ‰</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<!-- ๊ฐ์ฒด ์ธ์‹ ๋ฐ ์ €์žฅ ํƒญ -->
<div class="tab-pane fade show active" id="detect" role="tabpanel" aria-labelledby="detect-tab">
<div class="upload-section">
<h3>์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</h3>
<div class="mb-3">
<input class="form-control" type="file" id="imageUpload" accept="image/*">
</div>
<button id="detectObjectsBtn" class="btn btn-primary" disabled>๊ฐ์ฒด ์ธ์‹ํ•˜๊ธฐ</button>
<div class="loading" id="detectLoading">
<div class="spinner-border text-primary" role="status"></div>
<span>๊ฐ์ฒด ์ธ์‹ ์ค‘...</span>
</div>
</div>
<div class="detection-section">
<h3>์ธ์‹ ๊ฒฐ๊ณผ</h3>
<div class="canvas-container">
<canvas id="imageCanvas"></canvas>
<!-- ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค์™€ ๋ผ๋ฒจ์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
</div>
<div id="detectedObjects" class="mt-3">
<p>์ธ์‹๋œ ๊ฐ์ฒด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</p>
</div>
<button id="saveToVectorDBBtn" class="btn btn-success mt-3" disabled>๋ฒกํ„ฐ DB์— ์ €์žฅํ•˜๊ธฐ</button>
<div class="loading" id="saveLoading">
<div class="spinner-border text-success" role="status"></div>
<span>๋ฒกํ„ฐ DB์— ์ €์žฅ ์ค‘...</span>
</div>
</div>
</div>
<!-- ๊ฐ์ฒด ๊ฒ€์ƒ‰ ํƒญ -->
<div class="tab-pane fade" id="search" role="tabpanel" aria-labelledby="search-tab">
<div class="search-section">
<h3>๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ•</h3>
<div class="mb-3">
<select class="form-select" id="searchType">
<option value="image">์ด๋ฏธ์ง€๋กœ ๊ฒ€์ƒ‰</option>
<option value="class">ํด๋ž˜์Šค๋กœ ๊ฒ€์ƒ‰</option>
</select>
</div>
<div id="imageSearchSection">
<div class="mb-3">
<label for="searchImageUpload" class="form-label">๊ฒ€์ƒ‰ํ•  ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</label>
<input class="form-control" type="file" id="searchImageUpload" accept="image/*">
</div>
</div>
<div id="classSearchSection" style="display: none;">
<div class="mb-3">
<label for="classNameInput" class="form-label">ํด๋ž˜์Šค ์ด๋ฆ„</label>
<input type="text" class="form-control" id="classNameInput" placeholder="์˜ˆ: person, car, dog ๋“ฑ">
</div>
</div>
<div class="mb-3">
<label for="resultCount" class="form-label">๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜</label>
<input type="number" class="form-control" id="resultCount" min="1" max="20" value="5">
</div>
<button id="searchBtn" class="btn btn-primary">๊ฒ€์ƒ‰ํ•˜๊ธฐ</button>
<div class="loading" id="searchLoading">
<div class="spinner-border text-primary" role="status"></div>
<span>๊ฒ€์ƒ‰ ์ค‘...</span>
</div>
</div>
<div class="results-section">
<h3>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ</h3>
<div id="searchResults">
<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</p>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
// ์ „์—ญ ๋ณ€์ˆ˜
let currentImage = null;
let detectedObjects = [];
let imageWidth = 0;
let imageHeight = 0;
const colors = ['#FF5733', '#33FF57', '#3357FF', '#F033FF', '#FF3333', '#33FFFF', '#FFFF33'];
// DOM ์š”์†Œ
const imageUpload = document.getElementById('imageUpload');
const detectObjectsBtn = document.getElementById('detectObjectsBtn');
const imageCanvas = document.getElementById('imageCanvas');
const ctx = imageCanvas.getContext('2d');
const detectedObjectsDiv = document.getElementById('detectedObjects');
const saveToVectorDBBtn = document.getElementById('saveToVectorDBBtn');
const searchType = document.getElementById('searchType');
const imageSearchSection = document.getElementById('imageSearchSection');
const classSearchSection = document.getElementById('classSearchSection');
const searchImageUpload = document.getElementById('searchImageUpload');
const classNameInput = document.getElementById('classNameInput');
const resultCount = document.getElementById('resultCount');
const searchBtn = document.getElementById('searchBtn');
const searchResults = document.getElementById('searchResults');
// ๋กœ๋”ฉ ํ‘œ์‹œ
const detectLoading = document.getElementById('detectLoading');
const saveLoading = document.getElementById('saveLoading');
const searchLoading = document.getElementById('searchLoading');
// ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
imageUpload.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
// ์บ”๋ฒ„์Šค ํฌ๊ธฐ ์„ค์ •
imageWidth = img.width;
imageHeight = img.height;
// ์บ”๋ฒ„์Šค ํฌ๊ธฐ๋ฅผ ์ด๋ฏธ์ง€์— ๋งž๊ฒŒ ์กฐ์ •ํ•˜๋˜, ์ตœ๋Œ€ ๋„ˆ๋น„ ์ œํ•œ
const maxWidth = 800;
let displayWidth = img.width;
let displayHeight = img.height;
if (displayWidth > maxWidth) {
const ratio = maxWidth / displayWidth;
displayWidth = maxWidth;
displayHeight = displayHeight * ratio;
}
imageCanvas.width = displayWidth;
imageCanvas.height = displayHeight;
// ์ด๋ฏธ์ง€ ๊ทธ๋ฆฌ๊ธฐ
ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
// ํ˜„์žฌ ์ด๋ฏธ์ง€ ์ €์žฅ
currentImage = event.target.result;
// ๊ฐ์ฒด ์ธ์‹ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
detectObjectsBtn.disabled = false;
// ์ด์ „ ๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ์ดˆ๊ธฐํ™”
detectedObjects = [];
detectedObjectsDiv.innerHTML = '<p>์ธ์‹๋œ ๊ฐ์ฒด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</p>';
saveToVectorDBBtn.disabled = true;
// ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ œ๊ฑฐ
clearBoundingBoxes();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
// ๊ฐ์ฒด ์ธ์‹ ์ฒ˜๋ฆฌ
detectObjectsBtn.addEventListener('click', function() {
if (!currentImage) return;
// ๋กœ๋”ฉ ํ‘œ์‹œ
detectLoading.style.display = 'flex';
detectObjectsBtn.disabled = true;
// ๊ฐ์ฒด ์ธ์‹ API ํ˜ธ์ถœ
fetch('/api/detect', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
image: currentImage,
model: 'yolo' // ๊ธฐ๋ณธ ๋ชจ๋ธ๋กœ YOLO ์‚ฌ์šฉ
})
})
.then(response => response.json())
.then(data => {
// ๋กœ๋”ฉ ์ˆจ๊ธฐ๊ธฐ
detectLoading.style.display = 'none';
detectObjectsBtn.disabled = false;
if (data.error) {
alert('๊ฐ์ฒด ์ธ์‹ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + data.error);
return;
}
// ์ธ์‹๋œ ๊ฐ์ฒด ์ €์žฅ
detectedObjects = data.objects || [];
// ๊ฒฐ๊ณผ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
if (detectedObjects.length === 0) {
detectedObjectsDiv.innerHTML = '<p>์ธ์‹๋œ ๊ฐ์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>';
saveToVectorDBBtn.disabled = true;
return;
}
// ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ทธ๋ฆฌ๊ธฐ
drawBoundingBoxes();
// ์ธ์‹๋œ ๊ฐ์ฒด ๋ชฉ๋ก ํ‘œ์‹œ
displayDetectedObjects();
// ๋ฒกํ„ฐ DB ์ €์žฅ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
saveToVectorDBBtn.disabled = false;
})
.catch(error => {
detectLoading.style.display = 'none';
detectObjectsBtn.disabled = false;
console.error('Error:', error);
alert('๊ฐ์ฒด ์ธ์‹ ์š”์ฒญ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
});
});
// ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ทธ๋ฆฌ๊ธฐ ํ•จ์ˆ˜
function drawBoundingBoxes() {
// ์บ”๋ฒ„์Šค ํฌ๊ธฐ ๊ฐ€์ ธ์˜ค๊ธฐ
const canvasWidth = imageCanvas.width;
const canvasHeight = imageCanvas.height;
// ๊ธฐ์กด ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ œ๊ฑฐ
clearBoundingBoxes();
// ์บ”๋ฒ„์Šค ์ปจํ…Œ์ด๋„ˆ ๊ฐ€์ ธ์˜ค๊ธฐ
const canvasContainer = document.querySelector('.canvas-container');
// ๊ฐ ๊ฐ์ฒด์— ๋Œ€ํ•ด ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ทธ๋ฆฌ๊ธฐ
detectedObjects.forEach((obj, index) => {
const bbox = obj.bbox;
const colorIndex = index % colors.length;
// ์ƒ๋Œ€ ์ขŒํ‘œ๋ฅผ ์บ”๋ฒ„์Šค ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜
const x = bbox.x * canvasWidth;
const y = bbox.y * canvasHeight;
const width = bbox.width * canvasWidth;
const height = bbox.height * canvasHeight;
// ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์š”์†Œ ์ƒ์„ฑ
const boxElement = document.createElement('div');
boxElement.className = 'bbox-overlay';
boxElement.style.left = `${x}px`;
boxElement.style.top = `${y}px`;
boxElement.style.width = `${width}px`;
boxElement.style.height = `${height}px`;
boxElement.style.borderColor = colors[colorIndex];
// ๋ผ๋ฒจ ์š”์†Œ ์ƒ์„ฑ
const labelElement = document.createElement('div');
labelElement.className = 'bbox-label';
labelElement.style.left = `${x}px`;
labelElement.style.top = `${y - 20}px`;
labelElement.style.backgroundColor = colors[colorIndex];
labelElement.textContent = `${obj.class} ${Math.round(obj.confidence * 100)}%`;
// ์š”์†Œ๋ฅผ ์บ”๋ฒ„์Šค ์ปจํ…Œ์ด๋„ˆ์— ์ถ”๊ฐ€
canvasContainer.appendChild(boxElement);
canvasContainer.appendChild(labelElement);
});
}
// ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ œ๊ฑฐ ํ•จ์ˆ˜
function clearBoundingBoxes() {
const overlays = document.querySelectorAll('.bbox-overlay, .bbox-label');
overlays.forEach(overlay => overlay.remove());
}
// ์ธ์‹๋œ ๊ฐ์ฒด ๋ชฉ๋ก ํ‘œ์‹œ ํ•จ์ˆ˜
function displayDetectedObjects() {
let html = '<h4>์ธ์‹๋œ ๊ฐ์ฒด ๋ชฉ๋ก:</h4><ul class="list-group">';
detectedObjects.forEach((obj, index) => {
const colorIndex = index % colors.length;
html += `
<li class="list-group-item object-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-primary">${index + 1}</span>
<span class="badge" style="background-color: ${colors[colorIndex]}">${obj.class}</span>
<span>์‹ ๋ขฐ๋„: ${Math.round(obj.confidence * 100)}%</span>
</div>
</div>
</li>
`;
});
html += '</ul>';
detectedObjectsDiv.innerHTML = html;
}
// ๋ฒกํ„ฐ DB์— ์ €์žฅ ์ฒ˜๋ฆฌ
saveToVectorDBBtn.addEventListener('click', function() {
if (!currentImage || detectedObjects.length === 0) return;
// ๋กœ๋”ฉ ํ‘œ์‹œ
saveLoading.style.display = 'flex';
saveToVectorDBBtn.disabled = true;
// ๋ฒกํ„ฐ DB ์ €์žฅ API ํ˜ธ์ถœ
fetch('/api/add-detected-objects', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
image: currentImage,
objects: detectedObjects
})
})
.then(response => response.json())
.then(data => {
// ๋กœ๋”ฉ ์ˆจ๊ธฐ๊ธฐ
saveLoading.style.display = 'none';
saveToVectorDBBtn.disabled = false;
if (data.error) {
alert('๋ฒกํ„ฐ DB ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + data.error);
return;
}
alert(`์„ฑ๊ณต์ ์œผ๋กœ ${data.object_count}๊ฐœ์˜ ๊ฐ์ฒด๋ฅผ ๋ฒกํ„ฐ DB์— ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.`);
})
.catch(error => {
saveLoading.style.display = 'none';
saveToVectorDBBtn.disabled = false;
console.error('Error:', error);
alert('๋ฒกํ„ฐ DB ์ €์žฅ ์š”์ฒญ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
});
});
// ๊ฒ€์ƒ‰ ์œ ํ˜• ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ
searchType.addEventListener('change', function() {
if (this.value === 'image') {
imageSearchSection.style.display = 'block';
classSearchSection.style.display = 'none';
} else if (this.value === 'class') {
imageSearchSection.style.display = 'none';
classSearchSection.style.display = 'block';
}
});
// ๊ฒ€์ƒ‰ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
searchImageUpload.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
// ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ €์žฅ
searchImageUpload.dataset.image = event.target.result;
};
reader.readAsDataURL(file);
});
// ๊ฒ€์ƒ‰ ์ฒ˜๋ฆฌ
searchBtn.addEventListener('click', function() {
// ๋กœ๋”ฉ ํ‘œ์‹œ
searchLoading.style.display = 'flex';
searchBtn.disabled = true;
// ๊ฒ€์ƒ‰ ์œ ํ˜•์— ๋”ฐ๋ผ ์š”์ฒญ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
const searchTypeValue = searchType.value;
const nResults = parseInt(resultCount.value) || 5;
let requestData = {
searchType: searchTypeValue,
nResults: nResults
};
if (searchTypeValue === 'image') {
const imageData = searchImageUpload.dataset.image;
if (!imageData) {
alert('๊ฒ€์ƒ‰ํ•  ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.');
searchLoading.style.display = 'none';
searchBtn.disabled = false;
return;
}
requestData.image = imageData;
} else if (searchTypeValue === 'class') {
const className = classNameInput.value.trim();
if (!className) {
alert('๊ฒ€์ƒ‰ํ•  ํด๋ž˜์Šค ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
searchLoading.style.display = 'none';
searchBtn.disabled = false;
return;
}
requestData.className = className;
}
// ๊ฒ€์ƒ‰ API ํ˜ธ์ถœ
fetch('/api/search-similar-objects', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
// ๋กœ๋”ฉ ์ˆจ๊ธฐ๊ธฐ
searchLoading.style.display = 'none';
searchBtn.disabled = false;
if (data.error) {
alert('๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + data.error);
return;
}
// ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ
displaySearchResults(data.results);
})
.catch(error => {
searchLoading.style.display = 'none';
searchBtn.disabled = false;
console.error('Error:', error);
alert('๊ฒ€์ƒ‰ ์š”์ฒญ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
});
});
// ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ ํ•จ์ˆ˜
function displaySearchResults(results) {
if (!results || results.length === 0) {
searchResults.innerHTML = '<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>';
return;
}
let html = '<div class="row">';
results.forEach((result, index) => {
const metadata = result.metadata || {};
const distance = result.distance ? (1 - result.distance).toFixed(2) * 100 : 0;
const imageId = metadata.image_id || '';
const className = metadata.class || '';
const confidence = metadata.confidence ? Math.round(metadata.confidence * 100) : 0;
const bbox = metadata.bbox || {};
html += `
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">๊ฒฐ๊ณผ #${index + 1}</h5>
<div class="d-flex justify-content-between">
<span class="badge bg-primary">${className}</span>
<span class="badge bg-info">์œ ์‚ฌ๋„: ${distance}%</span>
</div>
<p class="card-text mt-2">
<small>๊ฐ์ฒด ID: ${result.id}</small><br>
<small>์ด๋ฏธ์ง€ ID: ${imageId}</small><br>
<small>์‹ ๋ขฐ๋„: ${confidence}%</small><br>
<small>์œ„์น˜: X=${(bbox.x * 100).toFixed(1)}%, Y=${(bbox.y * 100).toFixed(1)}%, W=${(bbox.width * 100).toFixed(1)}%, H=${(bbox.height * 100).toFixed(1)}%</small>
</p>
</div>
</div>
</div>
`;
});
html += '</div>';
searchResults.innerHTML = html;
}
</script>
</body>
</html>