Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1.0"> | |
| <title>Image SVD Processor</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| padding: 15px; | |
| background-color: #f5f5f5; | |
| } | |
| .container { | |
| background-color: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| h1 { | |
| text-align: center; | |
| background-color: #3498db; | |
| color: #fff; | |
| padding: 20px; | |
| margin: 0 0 20px 0; | |
| border-radius: 8px; | |
| } | |
| .controls { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin: 10px 0; | |
| padding: 10px; | |
| background-color: #f8f9fa; | |
| border-radius: 8px; | |
| gap: 10px; | |
| } | |
| .image-selection { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| } | |
| .predefined-images, .upload-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .slider-container { | |
| flex: 0 0 40%; | |
| margin-left: auto; | |
| text-align: right; | |
| } | |
| #r-value { | |
| width: 80%; | |
| } | |
| .image-container { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 15px; | |
| flex-wrap: wrap; | |
| gap: 15px; | |
| } | |
| .image-box { | |
| flex: 1; | |
| min-width: 250px; | |
| text-align: center; | |
| margin-bottom: 15px; | |
| padding: 10px; | |
| background: #fff; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| position: relative; | |
| } | |
| .image-box img { | |
| max-width: 100%; | |
| max-height: 300px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| padding: 8px; | |
| background: #fff; | |
| } | |
| .loading-overlay { | |
| display: none; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| background: rgba(255,255,255,0.8); | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .spinner { | |
| width: 50px; height: 50px; | |
| border: 5px solid #f3f3f3; | |
| border-top: 5px solid #3498db; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% {transform: rotate(0deg);} | |
| 100% {transform: rotate(360deg);} | |
| } | |
| .info { | |
| margin-top: 10px; | |
| color: #666; | |
| padding: 5px; | |
| background: #f8f9fa; | |
| border-radius: 4px; | |
| } | |
| input[type="file"] { | |
| padding: 5px; | |
| border: none; | |
| background: transparent; | |
| } | |
| input[type="file"]::-webkit-file-upload-button { | |
| background: #3498db; | |
| color: white; | |
| padding: 8px 16px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| input[type="range"] { | |
| height: 8px; | |
| -webkit-appearance: none; | |
| margin: 10px 0; | |
| background: #ddd; | |
| border-radius: 4px; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| background: #3498db; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| .storage-info { | |
| font-size: 0.9em; | |
| color: #666; | |
| margin-top: 5px; | |
| text-align: left; | |
| padding: 8px; | |
| background: #f8f9fa; | |
| border-radius: 4px; | |
| } | |
| .storage-details { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .storage-row { | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .highlight { | |
| color: #3498db; | |
| font-weight: bold; | |
| } | |
| .or-divider { | |
| display: flex; | |
| align-items: center; | |
| color: #666; | |
| font-size: 0.9em; | |
| padding: 5px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Image SVD Processor</h1> | |
| <div class="controls"> | |
| <div class="image-selection"> | |
| <div class="predefined-images"> | |
| <label for="predefinedImage" style="margin-top: -5px;padding-bottom: 5px;">Select a predefined image:</label> | |
| <select id="predefinedImage" style="height: 30px;"> | |
| <option value="">-- Select an image --</option> | |
| {% for image in predefined_images %} | |
| <option value="{{ image.path }}">{{ image.name }}</option> | |
| {% endfor %} | |
| </select> | |
| </div> | |
| <div class="or-divider"> | |
| <span>OR</span> | |
| </div> | |
| <div class="upload-section"> | |
| <label for="imageInput">Upload your own image:</label> | |
| <input type="file" id="imageInput" accept="image/*"> | |
| </div> | |
| </div> | |
| <div class="slider-container" style="margin-top: 10px;"> | |
| <label for="r-value">R Value: <span id="r-value-display">5</span></label> | |
| <input type="range" id="r-value" min="1" max="100" value="5"> | |
| </div> | |
| </div> | |
| <div class="image-container"> | |
| <div class="image-box"> | |
| <h3>Original Image</h3> | |
| <img id="originalImage" src="" alt="Original image will appear here"> | |
| <div class="info" id="originalInfo"></div> | |
| <div class="storage-info"> | |
| <div class="storage-details" id="originalStorage"> | |
| <div class="storage-row"> | |
| <span>Storage (bytes):</span> | |
| <span class="highlight">-</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>Dimensions:</span> | |
| <span>height × width × channels</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="image-box"> | |
| <h3>Processed Image</h3> | |
| <img id="processedImage" src="" alt="Processed image will appear here"> | |
| <div class="info" id="processedInfo"></div> | |
| <div class="storage-info"> | |
| <div class="storage-details" id="processedStorage"> | |
| <div class="storage-row"> | |
| <span>Storage (bytes):</span> | |
| <span class="highlight">-</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>SVD Components:</span> | |
| <span>U×r + r + V×r</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>Compression Ratio:</span> | |
| <span class="highlight">-</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="loading-overlay" id="loading"> | |
| <div class="spinner"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let originalImage = document.getElementById('originalImage'); | |
| let processedImage = document.getElementById('processedImage'); | |
| let rValue = document.getElementById('r-value'); | |
| let rValueDisplay = document.getElementById('r-value-display'); | |
| let loading = document.getElementById('loading'); | |
| let imageInput = document.getElementById('imageInput'); | |
| let predefinedSelect = document.getElementById('predefinedImage'); | |
| let maxR = 100; | |
| let currentImage = null; | |
| // Function to load first predefined image | |
| function loadFirstPredefinedImage() { | |
| if (predefinedSelect.options.length > 1) { // if we have any predefined images | |
| predefinedSelect.selectedIndex = 1; // select first image (index 0 is the placeholder) | |
| const selectedImageUrl = predefinedSelect.value; | |
| if (selectedImageUrl) { | |
| originalImage.src = selectedImageUrl; | |
| processImageFromUrl(selectedImageUrl); | |
| } | |
| } | |
| } | |
| // Load first image when page loads | |
| window.addEventListener('load', loadFirstPredefinedImage); | |
| document.getElementById('predefinedImage').addEventListener('change', function(e) { | |
| if (e.target.value) { | |
| imageInput.value = ''; | |
| originalImage.src = e.target.value; | |
| processImageFromUrl(e.target.value); | |
| } | |
| }); | |
| imageInput.addEventListener('change', function(e) { | |
| if (e.target.files && e.target.files[0]) { | |
| document.getElementById('predefinedImage').value = ''; | |
| let reader = new FileReader(); | |
| reader.onload = function(e) { | |
| originalImage.src = e.target.result; | |
| currentImage = imageInput.files[0]; | |
| processImage(); | |
| } | |
| reader.readAsDataURL(e.target.files[0]); | |
| } | |
| }); | |
| async function processImageFromUrl(imageUrl) { | |
| loading.style.display = 'flex'; | |
| const formData = new FormData(); | |
| formData.append('image_url', imageUrl); | |
| formData.append('r_value', rValue.value); | |
| try { | |
| const response = await fetch('/process', {method: 'POST', body: formData}); | |
| // Check if response is ok (status 200-299) | |
| if (!response.ok) { | |
| const text = await response.text(); | |
| console.error('Server response error:', response.status, text); | |
| throw new Error(`Server error: ${response.status}. Check console for details.`); | |
| } | |
| // Try parsing the response as JSON | |
| let data; | |
| try { | |
| data = await response.json(); | |
| } catch (parseError) { | |
| console.error('JSON parse error:', parseError); | |
| const text = await response.text(); | |
| console.error('Raw response:', text); | |
| throw new Error('Failed to parse server response as JSON'); | |
| } | |
| // Check for error in JSON response | |
| if (data.error) { | |
| console.error('API error:', data.error); | |
| if (data.traceback) console.error('Traceback:', data.traceback); | |
| throw new Error(data.error); | |
| } | |
| // Process successful response | |
| processedImage.src = 'data:image/png;base64,' + data.processed_image; | |
| maxR = data.max_r; | |
| rValue.max = maxR; | |
| // Update storage information | |
| document.getElementById('originalStorage').innerHTML = ` | |
| <div class="storage-row"> | |
| <span>Storage (bytes):</span> | |
| <span class="highlight">${data.storage.original.toLocaleString()}</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>Dimensions:</span> | |
| <span>${data.dimensions.join(' × ')}</span> | |
| </div>`; | |
| document.getElementById('processedStorage').innerHTML = ` | |
| <div class="storage-row"> | |
| <span>Storage (bytes):</span> | |
| <span class="highlight">${data.storage.compressed.toLocaleString()}</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>SVD Components:</span> | |
| <span>${data.dimensions[0]}×${rValue.value} + ${rValue.value} + ${data.dimensions[1]}×${rValue.value}</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>Compression Ratio:</span> | |
| <span class="highlight">${data.storage.compression_ratio.toFixed(2)}:1</span> | |
| </div>`; | |
| document.getElementById('originalInfo').textContent = | |
| `Dimensions: ${data.dimensions.join(' × ')}`; | |
| document.getElementById('processedInfo').textContent = | |
| `Using ${rValue.value} of ${maxR} singular values`; | |
| } catch (error) { | |
| console.error('Error details:', error); | |
| alert('Error processing image: ' + error.message); | |
| } finally { | |
| loading.style.display = 'none'; | |
| } | |
| } | |
| rValue.addEventListener('input', function() { | |
| rValueDisplay.textContent = this.value; | |
| const predefinedImage = document.getElementById('predefinedImage'); | |
| if (predefinedImage.value) { | |
| processImageFromUrl(predefinedImage.value); | |
| } else if (currentImage) { | |
| processImage(); | |
| } | |
| }); | |
| async function processImage() { | |
| if (!currentImage) {return;} | |
| loading.style.display = 'flex'; | |
| const formData = new FormData(); | |
| formData.append('image', currentImage); | |
| formData.append('r_value', rValue.value); | |
| try { | |
| const response = await fetch('/process', {method: 'POST', body: formData}); | |
| const data = await response.json(); | |
| if (data.error) { | |
| alert('Error: ' + data.error); | |
| return; | |
| } | |
| processedImage.src = 'data:image/png;base64,' + data.processed_image; | |
| maxR = data.max_r; | |
| rValue.max = maxR; | |
| document.getElementById('originalStorage').innerHTML = ` | |
| <div class="storage-row"> | |
| <span>Storage (bytes):</span> | |
| <span class="highlight">${data.storage.original.toLocaleString()}</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>Dimensions:</span> | |
| <span>${data.dimensions.join(' × ')}</span> | |
| </div>`; | |
| document.getElementById('processedStorage').innerHTML = ` | |
| <div class="storage-row"> | |
| <span>Storage (bytes):</span> | |
| <span class="highlight">${data.storage.compressed.toLocaleString()}</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>SVD Components:</span> | |
| <span>${data.dimensions[0]}×${rValue.value} + ${rValue.value} + ${data.dimensions[1]}×${rValue.value}</span> | |
| </div> | |
| <div class="storage-row"> | |
| <span>Compression Ratio:</span> | |
| <span class="highlight">${data.storage.compression_ratio.toFixed(2)}:1</span> | |
| </div>`; | |
| document.getElementById('originalInfo').textContent = | |
| `Dimensions: ${data.dimensions.join(' × ')}`; | |
| document.getElementById('processedInfo').textContent = | |
| `Using ${rValue.value} of ${maxR} singular values`; | |
| } catch (error) { | |
| alert('Error processing image: ' + error.message); | |
| } finally { | |
| loading.style.display = 'none'; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |