vision-web-app / frontend /build /similar-images.html
David Ko
์œ ์‚ฌ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€: CLIP ๋ชจ๋ธ๊ณผ ChromaDB ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™œ์šฉ
9d90c9e
raw
history blame
11.7 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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<style>
.image-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 20px;
}
.image-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
width: 220px;
}
.image-preview {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 10px;
}
.spinner-border {
display: none;
}
.result-container {
margin-top: 30px;
}
.similar-image {
width: 150px;
height: 150px;
object-fit: cover;
border-radius: 4px;
}
.similar-item {
margin-bottom: 15px;
}
</style>
</head>
<body>
<div class="container mt-5">
<h1 class="mb-4">์œ ์‚ฌ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰</h1>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</h5>
</div>
<div class="card-body">
<form id="uploadForm">
<div class="mb-3">
<label for="imageInput" class="form-label">์ด๋ฏธ์ง€ ์„ ํƒ</label>
<input type="file" class="form-control" id="imageInput" accept="image/*">
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="addToCollection">
<label class="form-check-label" for="addToCollection">
์ปฌ๋ ‰์…˜์— ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">
<span class="spinner-border spinner-border-sm" id="searchSpinner" role="status" aria-hidden="true"></span>
์œ ์‚ฌ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰
</button>
</form>
<div class="mt-3">
<div id="previewContainer" style="display: none;">
<h6>์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€:</h6>
<img id="imagePreview" class="image-preview" src="" alt="Preview">
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5>์ƒ˜ํ”Œ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€</h5>
</div>
<div class="card-body">
<p>๋ฒกํ„ฐ DB์— ์ƒ˜ํ”Œ ์ด๋ฏธ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.</p>
<button id="addSamplesBtn" class="btn btn-secondary">
<span class="spinner-border spinner-border-sm" id="sampleSpinner" role="status" aria-hidden="true"></span>
์ƒ˜ํ”Œ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ</h5>
</div>
<div class="card-body">
<div id="resultsContainer">
<p id="noResults">๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</p>
<div id="similarImagesContainer" class="row"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
document.getElementById('imageInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
document.getElementById('imagePreview').src = event.target.result;
document.getElementById('previewContainer').style.display = 'block';
};
reader.readAsDataURL(file);
}
});
// ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ
document.getElementById('uploadForm').addEventListener('submit', async function(e) {
e.preventDefault();
const fileInput = document.getElementById('imageInput');
const addToCollection = document.getElementById('addToCollection').checked;
if (!fileInput.files[0]) {
alert('์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.');
return;
}
// ๋กœ๋”ฉ ํ‘œ์‹œ
document.getElementById('searchSpinner').style.display = 'inline-block';
const formData = new FormData();
formData.append('image', fileInput.files[0]);
try {
// ์ปฌ๋ ‰์…˜์— ์ถ”๊ฐ€ ์˜ต์…˜์ด ์„ ํƒ๋œ ๊ฒฝ์šฐ
if (addToCollection) {
const addResponse = await fetch('/api/add-to-collection', {
method: 'POST',
body: formData
});
const addResult = await addResponse.json();
console.log('Add to collection result:', addResult);
}
// ์œ ์‚ฌ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰
const searchResponse = await fetch('/api/similar-images', {
method: 'POST',
body: formData
});
const searchResult = await searchResponse.json();
console.log('Search result:', searchResult);
// ๊ฒฐ๊ณผ ํ‘œ์‹œ
displayResults(searchResult);
} catch (error) {
console.error('Error:', error);
alert('์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + error.message);
} finally {
// ๋กœ๋”ฉ ํ‘œ์‹œ ์ œ๊ฑฐ
document.getElementById('searchSpinner').style.display = 'none';
}
});
// ๊ฒฐ๊ณผ ํ‘œ์‹œ ํ•จ์ˆ˜
function displayResults(results) {
const container = document.getElementById('similarImagesContainer');
const noResults = document.getElementById('noResults');
container.innerHTML = '';
if (results.error) {
noResults.textContent = '์˜ค๋ฅ˜: ' + results.error;
noResults.style.display = 'block';
return;
}
if (!results.similar_images || results.similar_images.length === 0) {
noResults.textContent = '์œ ์‚ฌํ•œ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ์ด๋ฏธ์ง€๋ฅผ ์ปฌ๋ ‰์…˜์— ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”.';
noResults.style.display = 'block';
return;
}
noResults.style.display = 'none';
results.similar_images.forEach((item, index) => {
const col = document.createElement('div');
col.className = 'col-6 similar-item';
const card = document.createElement('div');
card.className = 'card h-100';
// ์ด๋ฏธ์ง€ URL์ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์žˆ๋Š” ๊ฒฝ์šฐ
let imageUrl = '';
if (item.metadata && item.metadata.url) {
imageUrl = item.metadata.url;
} else {
// ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์ด๋ฏธ์ง€ ID๋กœ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ
imageUrl = 'https://via.placeholder.com/150?text=Image+' + (index + 1);
}
const distance = item.distance ? item.distance.toFixed(4) : 'N/A';
card.innerHTML = `
<img src="${imageUrl}" class="similar-image card-img-top" alt="Similar Image ${index + 1}">
<div class="card-body">
<h6 class="card-title">์œ ์‚ฌ๋„: ${distance}</h6>
<p class="card-text">ID: ${item.id.substring(0, 8)}...</p>
</div>
`;
col.appendChild(card);
container.appendChild(col);
});
}
// ์ƒ˜ํ”Œ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
document.getElementById('addSamplesBtn').addEventListener('click', async function() {
const spinner = document.getElementById('sampleSpinner');
spinner.style.display = 'inline-block';
try {
// ์ƒ˜ํ”Œ ์ด๋ฏธ์ง€ URL ๋ฐฐ์—ด (์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์ ์ ˆํ•œ ์ด๋ฏธ์ง€๋กœ ๋ณ€๊ฒฝ)
const sampleImages = [
{ url: 'https://source.unsplash.com/random/300x300?cat', label: 'cat' },
{ url: 'https://source.unsplash.com/random/300x300?dog', label: 'dog' },
{ url: 'https://source.unsplash.com/random/300x300?bird', label: 'bird' },
{ url: 'https://source.unsplash.com/random/300x300?flower', label: 'flower' },
{ url: 'https://source.unsplash.com/random/300x300?car', label: 'car' }
];
for (const sample of sampleImages) {
// ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ
const response = await fetch(sample.url);
const blob = await response.blob();
// FormData ์ƒ์„ฑ
const formData = new FormData();
formData.append('image', blob, 'sample.jpg');
formData.append('metadata', JSON.stringify({
label: sample.label,
url: sample.url
}));
// API ํ˜ธ์ถœ
const addResponse = await fetch('/api/add-to-collection', {
method: 'POST',
body: formData
});
const result = await addResponse.json();
console.log(`Added sample ${sample.label}:`, result);
}
alert('5๊ฐœ์˜ ์ƒ˜ํ”Œ ์ด๋ฏธ์ง€๊ฐ€ ์ปฌ๋ ‰์…˜์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
} catch (error) {
console.error('Error adding samples:', error);
alert('์ƒ˜ํ”Œ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + error.message);
} finally {
spinner.style.display = 'none';
}
});
</script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>