showeed's picture
Pythonのflaskを使って、画像を入力すると類似する画像を抽出するアプリを作りたいです。
115d306 verified
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ImageMatch | Visual Search</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
500: '#3b82f6',
},
secondary: {
500: '#6366f1',
}
}
}
}
}
</script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
.dropzone {
transition: all 0.3s ease;
}
.dropzone-active {
border-color: #3b82f6;
background-color: rgba(59, 130, 246, 0.05);
}
.result-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
@media (max-width: 640px) {
.result-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
.image-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body class="bg-gray-900 text-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-12 text-center">
<h1 class="text-4xl font-bold mb-2 bg-gradient-to-r from-primary-500 to-secondary-500 bg-clip-text text-transparent">ImageMatch</h1>
<p class="text-gray-400 max-w-2xl mx-auto">Upload an image to find visually similar matches from our collection</p>
</header>
<main class="max-w-4xl mx-auto">
<div class="mb-10">
<div
id="dropzone"
class="dropzone border-2 border-dashed border-gray-700 rounded-xl p-8 text-center cursor-pointer hover:border-gray-600 transition-colors"
>
<div class="flex flex-col items-center justify-center">
<i data-feather="upload" class="w-12 h-12 text-primary-500 mb-4"></i>
<h2 class="text-xl font-semibold mb-2">Drag & drop your image here</h2>
<p class="text-gray-400 mb-4">or click to browse files</p>
<input type="file" id="fileInput" class="hidden" accept="image/*">
<button id="browseBtn" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 rounded-md transition-colors">
Select Image
</button>
</div>
</div>
<div id="previewContainer" class="mt-4 flex justify-center hidden">
<div class="relative inline-block">
<img id="previewImage" src="" alt="Preview" class="max-h-48 rounded-md shadow-lg">
<button id="removeImageBtn" class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors">
<i data-feather="x" class="w-4 h-4"></i>
</button>
</div>
</div>
</div>
<div class="bg-gray-800 rounded-xl p-6 mb-10">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<h3 class="text-xl font-semibold">Search Settings</h3>
<div class="flex items-center space-x-4">
<div>
<label for="resultCount" class="block text-sm font-medium text-gray-300 mb-1">Number of Results</label>
<div class="flex items-center space-x-2">
<input
type="range"
id="resultCount"
min="3"
max="10"
value="5"
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
>
<span id="countValue" class="w-8 text-center">5</span>
</div>
</div>
<button
id="searchBtn"
class="px-4 py-2 bg-secondary-500 hover:bg-secondary-600 rounded-md transition-colors flex items-center space-x-2"
disabled
>
<i data-feather="search" class="w-4 h-4"></i>
<span>Find Matches</span>
</button>
</div>
</div>
<div class="flex items-center space-x-2 mb-4">
<i data-feather="info" class="w-4 h-4 text-blue-400"></i>
<p class="text-sm text-gray-400">For best results, use clear, high-quality images without excessive text.</p>
</div>
</div>
<div id="resultsSection" class="hidden">
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-semibold">Similar Images</h3>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-400">Sort by:</span>
<select class="bg-gray-800 border border-gray-700 text-gray-300 rounded-md px-3 py-1 text-sm">
<option>Most Similar</option>
<option>Color</option>
<option>Size</option>
</select>
</div>
</div>
<div class="bg-gray-800 rounded-xl p-6">
<div id="loadingIndicator" class="hidden flex justify-center items-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-500"></div>
</div>
<div id="resultsContainer" class="result-grid">
<!-- Results will be inserted here -->
</div>
<div id="noResults" class="hidden text-center py-12">
<i data-feather="frown" class="w-12 h-12 mx-auto text-gray-500 mb-4"></i>
<h4 class="text-xl font-medium text-gray-400 mb-2">No matches found</h4>
<p class="text-gray-500">Try uploading a different image or adjusting your search parameters.</p>
</div>
</div>
</div>
</main>
<footer class="mt-20 text-center text-gray-500 text-sm">
<p>© 2023 ImageMatch. All rights reserved.</p>
</footer>
</div>
<script>
feather.replace();
// DOM Elements
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const previewContainer = document.getElementById('previewContainer');
const previewImage = document.getElementById('previewImage');
const removeImageBtn = document.getElementById('removeImageBtn');
const searchBtn = document.getElementById('searchBtn');
const resultCount = document.getElementById('resultCount');
const countValue = document.getElementById('countValue');
const resultsSection = document.getElementById('resultsSection');
const loadingIndicator = document.getElementById('loadingIndicator');
const resultsContainer = document.getElementById('resultsContainer');
const noResults = document.getElementById('noResults');
// Update result count display
resultCount.addEventListener('input', () => {
countValue.textContent = resultCount.value;
});
// Browse button click handler
browseBtn.addEventListener('click', () => {
fileInput.click();
});
// File input change handler
fileInput.addEventListener('change', (e) => {
handleFileSelection(e.target.files[0]);
});
// Drag and drop handlers
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropzone.classList.add('dropzone-active');
}
function unhighlight() {
dropzone.classList.remove('dropzone-active');
}
dropzone.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const file = dt.files[0];
handleFileSelection(file);
});
// Handle selected file
function handleFileSelection(file) {
if (!file.type.match('image.*')) {
alert('Please select an image file');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
previewImage.src = e.target.result;
previewContainer.classList.remove('hidden');
searchBtn.disabled = false;
};
reader.readAsDataURL(file);
}
// Remove image handler
removeImageBtn.addEventListener('click', (e) => {
e.stopPropagation();
previewImage.src = '';
previewContainer.classList.add('hidden');
fileInput.value = '';
searchBtn.disabled = true;
resultsSection.classList.add('hidden');
});
// Search button handler (mock functionality)
searchBtn.addEventListener('click', () => {
// Show loading state
resultsSection.classList.remove('hidden');
loadingIndicator.classList.remove('hidden');
resultsContainer.innerHTML = '';
noResults.classList.add('hidden');
// Mock API call delay
setTimeout(() => {
loadingIndicator.classList.add('hidden');
// Mock results - in real app these would come from your Flask backend
const mockResults = Array(parseInt(resultCount.value)).fill().map((_, i) => ({
id: i,
similarity: (90 - (i * 5)) + '%',
url: `http://static.photos/technology/320x240/${100 + i}`
}));
if (mockResults.length > 0) {
resultsContainer.innerHTML = mockResults.map(result => `
<div class="image-card bg-gray-700 rounded-lg overflow-hidden group">
<div class="relative pt-[100%]">
<img src="${result.url}" alt="Result ${result.id}" class="absolute top-0 left-0 w-full h-full object-cover">
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 opacity-0 group-hover:opacity-100 transition-opacity">
<div class="flex justify-between items-end">
<span class="text-sm font-medium">Similarity: ${result.similarity}</span>
<button class="text-white hover:text-primary-500 transition-colors">
<i data-feather="download" class="w-4 h-4"></i>
</button>
</div>
</div>
</div>
</div>
`).join('');
feather.replace();
} else {
noResults.classList.remove('hidden');
}
}, 1500);
});
</script>
</body>
</html>