spad_for_vision / templates /flat_surface_detection.html
0001AMA's picture
SPAD for Vision: app and docs
e5f2dfe
{% extends "base.html" %}
{% block title %}Flat Surface Detection - MV+{% endblock %}
{% block content %}
<style>
html, body {
height: 100%;
overflow: auto;
margin: 0;
padding: 0;
}
.spatiotemporal-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
box-sizing: border-box;
}
.spatiotemporal-container .main-content {
display: grid !important;
grid-template-columns: 1fr 1fr 1fr !important;
gap: 20px !important;
margin-bottom: 30px !important;
margin-top: 0 !important;
min-height: auto !important;
padding: 0 !important;
overflow-y: visible !important;
flex-direction: row !important;
}
.card {
background: rgba(0, 0, 0, 0.8);
border-radius: 20px;
padding: 20px;
box-shadow: 0 10px 20px rgba(0,0,0,0.3);
border: 1px solid rgba(185, 29, 48, 0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
min-height: 700px;
max-height: calc(100vh - 40px);
overflow-y: auto;
box-sizing: border-box;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 15px 30px rgba(0,0,0,0.4);
border-color: #B91D30;
}
.card h2 {
color: #ffffff;
margin-bottom: 15px;
font-size: 1.5rem;
font-weight: normal;
display: flex;
align-items: center;
gap: 10px;
}
.upload-section {
margin-bottom: 20px;
}
.upload-options {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.upload-btn {
flex: 1;
padding: 15px;
border: 2px dashed #555555;
border-radius: 15px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: rgba(0, 0, 0, 0.3);
color: #ffffff;
font-size: 1rem;
}
.upload-btn:hover {
background: rgba(185, 29, 48, 0.1);
color: white;
border-color: #ffffff;
}
.upload-btn.active {
background: rgba(185, 29, 48, 0.2);
border-color: #ffffff;
}
.upload-btn i {
margin-right: 8px;
font-size: 1rem;
}
.file-input {
display: none;
}
/* File Browser Modal */
.file-browser-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
justify-content: center;
align-items: center;
overflow: hidden;
transform: none !important;
will-change: auto;
}
.file-browser-modal.active {
display: flex;
}
body.modal-open {
overflow: hidden;
}
.file-browser-content {
background: rgba(0, 0, 0, 0.95);
border: 2px solid #B91D30;
border-radius: 20px;
padding: 30px;
max-width: 800px;
max-height: 80vh;
width: 90%;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
flex-shrink: 0;
}
.file-browser-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #333;
}
.file-browser-header h3 {
color: #ffffff;
margin: 0;
font-size: 1.5rem;
}
.file-browser-close {
background: none;
border: none;
color: #ffffff;
font-size: 1.5rem;
cursor: pointer;
padding: 5px 10px;
border-radius: 5px;
transition: background 0.3s ease;
}
.file-browser-close:hover {
background: rgba(185, 29, 48, 0.3);
}
.file-browser-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
max-height: 400px;
overflow-y: auto;
}
.file-browser-item {
background: rgba(0, 0, 0, 0.6);
border: 2px solid #333;
border-radius: 8px;
padding: 12px 15px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
text-align: left;
}
.file-browser-item:hover {
border-color: #B91D30;
background: rgba(185, 29, 48, 0.1);
transform: translateX(5px);
}
.file-browser-item.selected {
border-color: #B91D30;
background: rgba(185, 29, 48, 0.2);
}
.file-browser-item img {
display: none;
}
.file-browser-item .file-name {
color: #ffffff;
font-size: 0.9rem;
word-break: break-word;
flex: 1;
margin-right: 15px;
}
.file-browser-item .file-size {
color: #999;
font-size: 0.8rem;
white-space: nowrap;
}
.file-browser-item.directory {
border-color: #555;
}
.file-browser-item.directory:hover {
border-color: #B91D30;
}
.file-browser-item.directory .file-name {
color: #B91D30;
font-weight: bold;
}
.file-browser-item.directory .directory-icon {
font-size: 2rem;
color: #B91D30;
margin-bottom: 10px;
}
.file-browser-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
padding: 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
flex-wrap: wrap;
}
.file-browser-breadcrumb-item {
color: #ffffff;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.3s ease;
}
.file-browser-breadcrumb-item:hover {
background: rgba(185, 29, 48, 0.3);
}
.file-browser-breadcrumb-item.active {
color: #B91D30;
cursor: default;
}
.file-browser-breadcrumb-separator {
color: #666;
}
.file-browser-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #333;
}
.file-browser-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
}
.file-browser-btn-primary {
background: #B91D30;
color: white;
}
.file-browser-btn-primary:hover {
background: #8a1523;
}
.file-browser-btn-secondary {
background: #333;
color: white;
}
.file-browser-btn-secondary:hover {
background: #555;
}
.file-browser-loading {
text-align: center;
color: #ffffff;
padding: 40px;
}
.file-browser-empty {
text-align: center;
color: #999;
padding: 40px;
}
.image-controls {
margin-bottom: 25px;
display: none !important;
}
.control-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.control-group.full-width {
grid-template-columns: 1fr;
}
.control-label {
color: #ffffff;
font-size: 0.9rem;
margin-bottom: 5px;
font-weight: 500;
}
.model-select {
width: 100%;
padding: 12px;
border: 1px solid #3C3435;
border-radius: 8px;
background: rgba(0, 0, 0, 0.5);
color: #ffffff;
font-size: 0.9rem;
}
.model-select:focus {
outline: none;
border-color: #ffffff;
box-shadow: 0 0 0 2px rgba(185, 29, 48, 0.3);
}
.model-select option {
background: #000000;
color: #ffffff;
}
.image-preview-container {
margin-bottom: 25px;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.preview-header h4 {
color: #ffffff;
margin: 0;
font-size: 1rem;
font-weight: normal;
}
.image-preview-wrapper {
position: relative;
display: inline-block;
border: none;
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
max-width: 100%;
max-height: 300px;
}
.image-preview-canvas {
max-width: 100%;
max-height: 300px;
cursor: crosshair;
display: block;
image-rendering: auto;
image-rendering: smooth;
}
.crop-overlay {
position: absolute;
border: 2px dashed #B91D30;
background: rgba(185, 29, 48, 0.1);
pointer-events: none;
display: none;
}
.crop-instructions {
margin-top: 10px;
padding: 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 6px;
font-size: 0.8rem;
color: #cccccc;
text-align: center;
}
.crop-instructions i {
margin-right: 5px;
color: #B91D30;
}
.transform-buttons-compact {
display: flex;
gap: 8px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.transform-btn {
padding: 8px 12px;
border: 1px solid #B91D30;
border-radius: 6px;
background: rgba(0, 0, 0, 0.3);
color: #ffffff;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 4px;
}
.transform-btn:hover {
background: rgba(185, 29, 48, 0.2);
border-color: #ffffff;
}
.transform-btn.active {
background: rgba(185, 29, 48, 0.3);
border-color: #ffffff;
}
.detect-btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #2F1113, #2f0e11);
border: none;
border-radius: 10px;
color: white;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 20px;
}
.detect-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #2f0e11, #2F1113);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(47, 17, 19, 0.4);
}
.detect-btn:disabled {
background: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.performance-section {
background: rgba(0, 0, 0, 0.6);
border-radius: 15px;
padding: 15px;
border: 1px solid rgba(185, 29, 48, 0.2);
}
.performance-title {
color: #ffffff;
font-size: 1.1rem;
margin-bottom: 15px;
font-weight: 600;
}
.performance-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.performance-item {
background: rgba(0, 0, 0, 0.3);
padding: 12px;
border-radius: 8px;
border: 1px solid rgba(185, 29, 48, 0.1);
}
.performance-label {
color: #ccc;
font-size: 0.8rem;
margin-bottom: 5px;
}
.performance-value {
color: #ffffff;
font-size: 1rem;
font-weight: 600;
}
.metrics-section {
background: rgba(0, 0, 0, 0.6);
border-radius: 15px;
padding: 15px;
margin-top: 15px;
border: 1px solid rgba(185, 29, 48, 0.2);
}
.metrics-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.metric-item {
background: rgba(0, 0, 0, 0.3);
padding: 10px;
border-radius: 8px;
border: 1px solid rgba(185, 29, 48, 0.1);
}
.metric-label {
color: #ccc;
font-size: 0.8rem;
margin-bottom: 5px;
}
.metric-value {
color: #ffffff;
font-size: 1rem;
font-weight: 600;
}
.detection-results {
background: rgba(0, 0, 0, 0.6);
border-radius: 15px;
padding: 20px;
margin-top: 20px;
border: 1px solid rgba(185, 29, 48, 0.2);
}
.detection-title {
color: #ffffff;
font-size: 1.1rem;
margin-bottom: 15px;
font-weight: 600;
}
.prediction-item {
background: rgba(0, 0, 0, 0.3);
padding: 12px;
border-radius: 8px;
margin-bottom: 10px;
border: 1px solid rgba(185, 29, 48, 0.1);
color: #ffffff;
font-size: 0.9rem;
}
.detection-results-dual {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 10px;
}
.detection-results-column {
display: flex;
flex-direction: column;
}
.detection-subtitle {
font-size: 1rem;
font-weight: 600;
color: #cccccc;
margin-bottom: 10px;
text-align: center;
}
.detection-results-column .prediction-item {
margin-bottom: 5px;
flex: 1;
}
.prediction-item:last-child {
margin-bottom: 0;
}
.result-image {
width: 100%;
max-height: 200px;
object-fit: contain;
border-radius: 8px;
margin-top: 15px;
border: 1px solid rgba(185, 29, 48, 0.2);
}
.batch-navigation {
display: flex;
gap: 10px;
margin-top: 15px;
}
.nav-btn {
flex: 1;
padding: 10px;
border: 1px solid #B91D30;
border-radius: 6px;
background: rgba(0, 0, 0, 0.3);
color: #ffffff;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
}
.nav-btn:hover:not(:disabled) {
background: rgba(185, 29, 48, 0.2);
border-color: #ffffff;
}
.nav-btn:disabled {
background: #333;
cursor: not-allowed;
border-color: #666;
}
.top-sections {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
}
.filename-display {
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.filename-label {
font-size: 0.8rem;
color: #888;
margin-bottom: 5px;
}
.filename-value {
font-size: 0.9rem;
color: #fff;
font-weight: 500;
word-break: break-all;
}
@media (max-width: 768px) {
.spatiotemporal-container .main-content {
grid-template-columns: 1fr !important;
}
.control-group {
grid-template-columns: 1fr !important;
}
.performance-grid {
grid-template-columns: 1fr !important;
}
}
</style>
<div class="spatiotemporal-container">
<!-- Simple Status Message -->
<div id="statusMessage" style="color: #ccc; font-size: 0.9rem; margin-bottom: 15px; text-align: center;">Please select image(s)</div>
<div class="main-content">
<div class="card">
<h2>
Flat Surface Detection
</h2>
<div class="upload-section">
<div class="upload-options">
<div class="upload-btn" id="singleUploadBtn">
Select Image
</div>
<div class="upload-btn" id="batchUploadBtn">
Batch Upload
</div>
</div>
<input type="file" id="singleFileInput" class="file-input" accept="image/png,.sto">
<input type="file" id="batchFileInput" class="file-input" accept="image/png,.sto" multiple>
</div>
<!-- File Browser Modal -->
<div class="file-browser-modal" id="fileBrowserModal">
<div class="file-browser-content">
<div class="file-browser-header">
<h3>Select Test Image</h3>
<button class="file-browser-close" id="fileBrowserClose">&times;</button>
</div>
<div id="fileBrowserBreadcrumb" class="file-browser-breadcrumb"></div>
<div id="fileBrowserList" class="file-browser-list">
<div class="file-browser-loading">Loading images...</div>
</div>
<div class="file-browser-actions">
<button class="file-browser-btn file-browser-btn-secondary" id="fileBrowserCancel">Cancel</button>
<button class="file-browser-btn file-browser-btn-primary" id="fileBrowserSelect" disabled>Select</button>
</div>
</div>
</div>
<!-- Batch Navigation Controls (hidden until batch upload) -->
<div class="batch-navigation" id="batchNavigation" style="display: none; margin-bottom: 20px;">
<button class="nav-btn" id="prevImageBtn" disabled>
Previous
</button>
<button class="nav-btn" id="nextImageBtn" disabled>
Next
</button>
</div>
<div class="image-controls" id="imageControls" style="display: none;">
<div class="control-group">
<div>
<div class="control-label">Select Model Weight:</div>
<select id="modelSelect" class="model-select">
<option value="">Loading weights...</option>
</select>
</div>
</div>
<button class="detect-btn" id="detectBtn" disabled>
Detect Flat Surface
</button>
</div>
</div>
<!-- Right Card: Results -->
<div class="card">
<h2>
Detection Results
</h2>
<div class="top-sections">
<div class="filename-display">
<div class="filename-label">Selected Image:</div>
<div class="filename-value" id="filenameValue">No image selected</div>
</div>
</div>
<div class="detection-results">
<div class="detection-subtitle">Detected flat surface(s)</div>
<div id="prediction1" class="prediction-item">1. -</div>
<div id="prediction2" class="prediction-item">2. -</div>
<div id="prediction3" class="prediction-item">3. -</div>
<div id="detectionCount" class="prediction-item">Found no flat surface(s)</div>
</div>
</div>
<!-- Performance and Architecture Card -->
<div class="card">
<h2>
Performance and Architecture
</h2>
<div class="metrics-section">
<div class="metrics-grid">
<div class="metric-item">
<div class="metric-label">Model Architecture</div>
<div class="metric-value" id="architectureValue">-</div>
</div>
<div class="metric-item">
<div class="metric-label">Inference Time</div>
<div class="metric-value" id="inferenceTimeValue">-</div>
</div>
<div class="metric-item">
<div class="metric-label">Classes</div>
<div class="metric-value" id="classesValue" style="font-size: 0.85rem; word-break: break-word;">-</div>
</div>
<div class="metric-item">
<div class="metric-label">File Size</div>
<div class="metric-value" id="fileSizeValue">-</div>
</div>
<div class="metric-item">
<div class="metric-label">transient image Dimensions</div>
<div class="metric-value" id="transientImageDimensions">-</div>
</div>
<div class="metric-item">
<div class="metric-label">Upload Time</div>
<div class="metric-value" id="uploadTimeValue">-</div>
</div>
<div class="metric-item">
<div class="metric-label">Processing Status</div>
<div class="metric-value" id="processingStatusValue"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let currentImage = null;
let batchFiles = [];
let currentBatchIndex = 0;
let selectedFile = null;
// Cache for detection results
let detectionCache = {};
let lastImageHash = null;
let lastWeightPath = null;
// Initialize the page
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
loadModelWeights();
});
function setupEventListeners() {
// Upload buttons now use file browser modal (see file browser code below)
// Old file picker code removed - file browser modal handles file selection
/*
document.getElementById('singleUploadBtn').addEventListener('click', async () => {
try {
if ('showOpenFilePicker' in window) {
// Use File System Access API (Chrome/Edge) - user can navigate to testimages
const handle = await window.showOpenFilePicker({
types: [{
description: 'Images',
accept: {
'image/png': ['.png'],
'image/*': ['.sto']
}
}],
multiple: false
});
const file = await handle[0].getFile();
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
document.getElementById('singleFileInput').files = dataTransfer.files;
document.getElementById('singleFileInput').dispatchEvent(new Event('change', { bubbles: true }));
} else {
// Fallback: regular file input
document.getElementById('singleFileInput').click();
}
} catch (error) {
// User cancelled or API not supported - fallback to regular file input
document.getElementById('singleFileInput').click();
}
});
document.getElementById('batchUploadBtn').addEventListener('click', async () => {
try {
if ('showOpenFilePicker' in window) {
// Use File System Access API (Chrome/Edge) - user can navigate to testimages
const handles = await window.showOpenFilePicker({
types: [{
description: 'Images',
accept: {
'image/png': ['.png'],
'image/*': ['.sto']
}
}],
multiple: true
});
const files = await Promise.all(handles.map(h => h.getFile()));
const dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
document.getElementById('batchFileInput').files = dataTransfer.files;
document.getElementById('batchFileInput').dispatchEvent(new Event('change', { bubbles: true }));
} else {
// Fallback: regular file input
document.getElementById('batchFileInput').click();
}
} catch (error) {
// User cancelled or API not supported - fallback to regular file input
document.getElementById('batchFileInput').click();
}
});
*/
// File inputs
document.getElementById('singleFileInput').addEventListener('change', handleSingleFileSelect);
document.getElementById('batchFileInput').addEventListener('change', handleBatchFileSelect);
// Detect button
document.getElementById('detectBtn').addEventListener('click', detectObjects);
// Clear status and update model info when a model weight is selected
const modelSelect = document.getElementById('modelSelect');
modelSelect.addEventListener('change', () => {
const statusDiv = document.getElementById('statusMessage');
if (statusDiv) statusDiv.textContent = '';
const ps = document.getElementById('processingStatusValue');
if (ps) ps.textContent = 'Ready';
// Fetch and display model info when weight is selected
const selectedWeightPath = modelSelect.value;
if (selectedWeightPath) {
updateModelInfo(selectedWeightPath);
} else {
// Reset to defaults when no weight is selected
document.getElementById('architectureValue').textContent = '-';
document.getElementById('classesValue').textContent = '-';
}
// Update detect button state based on both image and weight selection
updateDetectButtonState();
});
// Batch navigation
document.getElementById('prevImageBtn').addEventListener('click', () => navigateBatch(-1));
document.getElementById('nextImageBtn').addEventListener('click', () => navigateBatch(1));
}
function loadModelWeights() {
console.log('Loading model weights from /api/flat_surface_detection_weights');
fetch('/api/flat_surface_detection_weights')
.then(response => {
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Weights data received:', data);
const select = document.getElementById('modelSelect');
if (!select) {
console.error('modelSelect element not found');
return;
}
select.innerHTML = '<option value="">Select a model weight...</option>';
// Display repo link if available
if (data.repo_url) {
const repoLink = document.createElement('div');
repoLink.className = 'repo-link';
repoLink.style.cssText = 'margin-top: 5px; font-size: 0.85em; color: #00CED1;';
repoLink.innerHTML = `<a href="${data.repo_url}" target="_blank" style="color: #00CED1; text-decoration: none;">📦 View on Hugging Face: ${data.repo_id || 'Model Repository'}</a>`;
select.parentElement.appendChild(repoLink);
}
if (data.success && data.weights && data.weights.length > 0) {
console.log(`Found ${data.weights.length} weights`);
data.weights.forEach(weight => {
const option = document.createElement('option');
option.value = weight.path;
option.textContent = weight.display_name || weight.filename;
select.appendChild(option);
});
} else {
console.warn('No weights found or empty response:', data);
select.innerHTML = '<option value="">No weights available</option>';
}
})
.catch(error => {
console.error('Error loading weights:', error);
const select = document.getElementById('modelSelect');
if (select) {
select.innerHTML = '<option value="">Error loading weights</option>';
}
});
}
function updateModelInfo(weightPath) {
const formData = new FormData();
formData.append('weight_path', weightPath);
fetch('/api/get_model_info', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update architecture
const architectureElement = document.getElementById('architectureValue');
if (architectureElement) {
architectureElement.textContent = data.architecture || '-';
}
// Update classes
const classesElement = document.getElementById('classesValue');
if (classesElement) {
if (data.classes_display) {
classesElement.textContent = data.classes_display;
} else if (data.classes && data.classes.length > 0) {
const classesStr = Array.isArray(data.classes)
? data.classes.join(', ')
: (typeof data.classes === 'string' ? data.classes : '-');
classesElement.textContent = classesStr;
} else {
classesElement.textContent = '-';
}
}
console.log('Model info updated:', {
architecture: data.architecture,
classes: data.classes,
classes_display: data.classes_display
});
}
})
.catch(error => {
console.error('Error loading model info:', error);
});
}
function handleSingleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
// Immediately show controls
try {
const ic0 = document.getElementById('imageControls');
if (ic0) ic0.style.display = 'block';
} catch (e) {}
const lower = file.name.toLowerCase();
if (lower.endsWith('.png')) {
selectedFile = file;
batchFiles = [file];
currentBatchIndex = 0;
displayImage(file);
showImageControls();
// Image controls are hidden via CSS
// Update detect button state (will be enabled only if weight is also selected)
updateDetectButtonState();
hideBatchNavigation();
} else if (lower.endsWith('.sto')) {
const formData = new FormData();
formData.append('file', file);
fetch('/api/extract_sto_index0', { method: 'POST', body: formData })
.then(async r => {
const raw = await r.text();
try { return JSON.parse(raw); } catch(e) { console.error('Non-JSON response:', raw); throw e; }
})
.then(async data => {
if (data.success && data.image) {
// Convert base64 image to File object for consistency
const base64Response = await fetch(data.image);
const blob = await base64Response.blob();
const stoFile = new File([blob], file.name.replace('.sto', '.png'), { type: 'image/png', lastModified: Date.now() });
selectedFile = stoFile;
batchFiles = [stoFile];
currentBatchIndex = 0;
// Use displayImage to update filename and enable controls
displayImage(stoFile);
showImageControls();
// Image controls are hidden via CSS
// Update detect button state (will be enabled only if weight is also selected)
updateDetectButtonState();
hideBatchNavigation();
} else {
updateStatus(data.error || 'Invalid STO file: missing PNG at index 0', true);
}
})
.catch(err => {
console.error('STO extract failed', err);
updateStatus('Invalid STO file: missing PNG at index 0', true);
});
} else {
updateStatus('Unsupported file type. Please select PNG or STO.', true);
}
}
function handleBatchFileSelect(event) {
const files = Array.from(event.target.files);
if (!files.length) return;
// Immediately show controls
try {
const icb0 = document.getElementById('imageControls');
if (icb0) icb0.style.display = 'block';
} catch (e) {}
batchFiles = files;
currentBatchIndex = 0;
const first = files[0];
const lower = first.name.toLowerCase();
if (lower.endsWith('.png')) {
selectedFile = first;
displayImage(first);
showImageControls();
// Image controls are hidden via CSS
// Update detect button state (will be enabled only if weight is also selected)
updateDetectButtonState();
showBatchNavigation();
} else if (lower.endsWith('.sto')) {
const formData = new FormData();
formData.append('file', first);
fetch('/api/extract_sto_index0', { method: 'POST', body: formData })
.then(async r => {
const raw = await r.text();
try { return JSON.parse(raw); } catch(e) { console.error('Non-JSON response:', raw); throw e; }
})
.then(async data => {
if (data.success && data.image) {
// Convert base64 image to File object for consistency
const base64Response = await fetch(data.image);
const blob = await base64Response.blob();
const stoFile = new File([blob], first.name.replace('.sto', '.png'), { type: 'image/png', lastModified: Date.now() });
selectedFile = stoFile;
// Use displayImage to update filename and enable controls
displayImage(stoFile);
showImageControls();
// Image controls are hidden via CSS
// Update detect button state (will be enabled only if weight is also selected)
updateDetectButtonState();
showBatchNavigation();
} else {
updateStatus(data.error || 'Invalid STO file: missing PNG at index 0', true);
}
})
.catch(err => {
console.error('STO extract failed', err);
updateStatus('Invalid STO file: missing PNG at index 0', true);
});
} else {
updateStatus('Unsupported file type. Please select PNG or STO.', true);
}
}
function displayImage(file) {
// Update filename display
const filenameElement = document.getElementById('filenameValue');
if (filenameElement) {
filenameElement.textContent = file.name;
console.log('Updated filename to:', file.name);
} else {
console.error('filenameValue element not found');
}
// Clear status message after image selection
const statusDiv = document.getElementById('statusMessage');
if (statusDiv) statusDiv.textContent = '';
// Reset detection results until next detect
try {
document.getElementById('prediction1').textContent = '1. -';
document.getElementById('prediction2').textContent = '2. -';
document.getElementById('prediction3').textContent = '3. -';
document.getElementById('detectionCount').textContent = 'Found no flat surface(s)';
} catch (e) { /* ignore */ }
// Update file metrics
updateFileMetrics(file);
// Load image for metrics only (no canvas display needed)
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
currentImage = img;
// Update image-specific metrics
updateImageMetrics(img);
// Show image controls and enable detect button
showImageControls();
};
img.onerror = function() {
console.error('Failed to load image:', file.name);
alert('Failed to load image. Please try again.');
};
img.src = e.target.result;
};
reader.onerror = function() {
console.error('Failed to read file:', file.name);
alert('Failed to read file. Please try again.');
};
reader.readAsDataURL(file);
}
function updateFileMetrics(file) {
// File size
const fileSizeKB = (file.size / 1024).toFixed(2);
document.getElementById('fileSizeValue').textContent = `${fileSizeKB} KB`;
// File format removed
// Upload time
const uploadTime = new Date().toLocaleTimeString();
document.getElementById('uploadTimeValue').textContent = uploadTime;
// Processing status: show when image selected
const psElUp = document.getElementById('processingStatusValue');
const hasWeightUp = !!document.getElementById('modelSelect').value;
if (psElUp) psElUp.textContent = hasWeightUp ? 'Ready' : 'Not ready';
}
function updateImageMetrics(img) {
// Image dimensions → write to transientImageDimensions (we removed imageDimensionsValue)
const transientEl = document.getElementById('transientImageDimensions');
if (transientEl) {
transientEl.textContent = `${img.width} × ${img.height}`;
}
// Processing status: clear after image metrics updated
const ps = document.getElementById('processingStatusValue');
if (ps) ps.textContent = '';
}
function showImageControls() {
// Image controls are hidden via CSS (display: none !important)
// No need to show them, but still ensure weights are loaded
const select = document.getElementById('modelSelect');
if (select && select.options.length <= 1) {
// Only reload if dropdown is empty or only has placeholder
loadModelWeights();
}
// Update detect button state based on both image and weight selection
updateDetectButtonState();
}
// Helper function to update detect button state based on image and weight selection
function updateDetectButtonState() {
const detectBtn = document.getElementById('detectBtn');
if (!detectBtn) return;
const hasImage = selectedFile !== null && selectedFile !== undefined;
const hasWeight = document.getElementById('modelSelect').value !== '';
// Enable detect button only when both image is uploaded and weight is selected
detectBtn.disabled = !(hasImage && hasWeight);
}
function hideImageControls() {
document.getElementById('imageControls').style.display = 'none';
document.getElementById('detectBtn').disabled = true;
}
function showBatchNavigation() {
document.getElementById('batchNavigation').style.display = 'flex';
updateBatchNavigation();
}
function hideBatchNavigation() {
document.getElementById('batchNavigation').style.display = 'none';
}
function updateBatchNavigation() {
document.getElementById('prevImageBtn').disabled = currentBatchIndex === 0;
document.getElementById('nextImageBtn').disabled = currentBatchIndex === batchFiles.length - 1;
}
function navigateBatch(direction) {
const newIndex = currentBatchIndex + direction;
if (newIndex >= 0 && newIndex < batchFiles.length) {
currentBatchIndex = newIndex;
selectedFile = batchFiles[currentBatchIndex];
displayImage(selectedFile);
updateBatchNavigation();
// Update detect button state after navigating to new image
updateDetectButtonState();
}
}
// Function to create a hash of current image state
function createImageHash() {
if (!currentImage || !selectedFile) return null;
// Create a simple hash based on filename
const hashString = `${selectedFile.name}`;
return btoa(hashString).substring(0, 16); // Simple hash
}
async function detectObjects() {
// Validate that image is uploaded
if (!selectedFile) {
updateStatus('Please upload an image first', true);
return;
}
// Validate that model weight is selected
const weightPath = document.getElementById('modelSelect').value;
if (!weightPath) {
updateStatus('Please select a model weight first', true);
return;
}
// Get filename from the displayed value
const filenameElement = document.getElementById('filenameValue');
const displayedFilename = filenameElement ? filenameElement.textContent : selectedFile.name;
// Clear any prior status message before starting detection
const statusDivDetect = document.getElementById('statusMessage');
if (statusDivDetect) statusDivDetect.textContent = '';
// Repopulate Input File Metrics at detection time
try {
if (selectedFile) {
updateFileMetrics(selectedFile);
}
// Always update transient image dimensions when detect button is clicked
if (currentImage) {
updateImageMetrics(currentImage);
} else if (selectedFile) {
// If currentImage is not available, load it to get dimensions
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
updateImageMetrics(img);
};
img.src = e.target.result;
};
reader.readAsDataURL(selectedFile);
}
} catch (e) {
// Safe guard - ignore metric update errors
console.warn('Metrics update failed:', e);
}
// Always perform fresh inference on each Detect click (no frontend caching)
try {
showLoading();
const psStart = document.getElementById('processingStatusValue');
if (psStart) psStart.textContent = 'Processing...';
// Prepare form data
const formData = new FormData();
// Use the uploaded image file (selectedFile) for inference
console.log('Detecting with uploaded image:', displayedFilename);
console.log('File object:', selectedFile.name);
formData.append('file', selectedFile);
formData.append('weight_path', weightPath);
// Send to API
const response = await fetch('/api/detect_material_head', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
const psDone = document.getElementById('processingStatusValue');
if (psDone) psDone.textContent = 'Inference Complete';
// Ensure status is cleared on success
const statusDivSuccess = document.getElementById('statusMessage');
if (statusDivSuccess) statusDivSuccess.textContent = '';
displayResults(data);
} else {
updateStatus(data.error || 'Detection failed', true);
}
} catch (error) {
console.error('Detection error:', error);
updateStatus('Detection failed: ' + error.message, true);
} finally {
hideLoading();
}
}
function displayResults(data) {
// Ensure Performance & Architecture section is visible
const perfArchSection = document.querySelector('.performance-stats, .architecture-details, .performance-metrics');
if (perfArchSection) {
perfArchSection.style.display = 'block';
perfArchSection.style.visibility = 'visible';
}
// Update performance metrics - ensure all fields are updated when detect is clicked
const architectureEl = document.getElementById('architectureValue');
const inferenceTimeEl = document.getElementById('inferenceTimeValue');
if (architectureEl) {
architectureEl.textContent = data.architecture || '-';
}
if (inferenceTimeEl && data.inference_time !== undefined) {
inferenceTimeEl.textContent = `${data.inference_time.toFixed(1)}ms`;
} else if (inferenceTimeEl) {
inferenceTimeEl.textContent = '-';
}
// Update classes display
console.log('DEBUG: Classes data from API:', data.classes);
console.log('DEBUG: Classes display from API:', data.classes_display);
const classesElement = document.getElementById('classesValue');
if (!classesElement) {
console.error('DEBUG: classesValue element not found!');
return;
}
if (data.classes_display) {
// Use the formatted display string if available
console.log('DEBUG: Using classes_display:', data.classes_display);
classesElement.textContent = data.classes_display;
} else if (data.classes && data.classes.length > 0) {
// Otherwise format from array
const classesStr = Array.isArray(data.classes)
? data.classes.join(', ')
: (typeof data.classes === 'string' ? data.classes : '-');
console.log('DEBUG: Using formatted classes array:', classesStr);
classesElement.textContent = classesStr;
} else {
console.log('DEBUG: No classes data found, setting to -');
classesElement.textContent = '-';
}
console.log('DEBUG: Final classesValue textContent:', classesElement.textContent);
// Update predictions - always show 3 with percentages (pad with 0%)
const preds = (data.top3_predictions || []).slice(0, 3);
while (preds.length < 3) {
preds.push({ class: '-', probability: 0 });
}
for (let i = 0; i < 3; i++) {
const element = document.getElementById(`prediction${i + 1}`);
const pred = preds[i];
element.textContent = `${i + 1}. ${pred.class} - ${(pred.probability * 100).toFixed(2)}%`;
}
// Update detection count - for classification models, show the predicted class
if (data.predicted_class && data.confidence) {
const confidencePercent = (data.confidence * 100).toFixed(2);
document.getElementById('detectionCount').textContent = `Predicted: ${data.predicted_class} (${confidencePercent}% confidence)`;
} else if (data.top3_predictions && data.top3_predictions.length > 0) {
// Fallback: show top prediction
const topPred = data.top3_predictions[0];
const confidencePercent = (topPred.probability * 100).toFixed(2);
document.getElementById('detectionCount').textContent = `Predicted: ${topPred.class} (${confidencePercent}% confidence)`;
} else {
document.getElementById('detectionCount').textContent = `No prediction available`;
}
}
function showLoading() {
document.getElementById('detectBtn').disabled = true;
document.getElementById('detectBtn').innerHTML = 'Detecting...';
}
function hideLoading() {
document.getElementById('detectBtn').disabled = false;
document.getElementById('detectBtn').innerHTML = 'Detect Flat Surface';
}
function updateStatus(message, isError = false) {
const statusDiv = document.getElementById('statusMessage');
statusDiv.textContent = message;
statusDiv.style.color = isError ? '#ff6666' : '#ccc';
if (isError) {
setTimeout(() => {
statusDiv.textContent = '';
statusDiv.style.color = '#ccc';
}, 3000);
}
}
// File Browser Modal
const fileBrowserModal = document.getElementById('fileBrowserModal');
const fileBrowserClose = document.getElementById('fileBrowserClose');
const fileBrowserCancel = document.getElementById('fileBrowserCancel');
const fileBrowserSelect = document.getElementById('fileBrowserSelect');
const fileBrowserList = document.getElementById('fileBrowserList');
const fileBrowserBreadcrumb = document.getElementById('fileBrowserBreadcrumb');
let selectedFiles = [];
let isBatchMode = false;
let currentPath = ''; // Track current directory path
// Upload buttons - open file browser modal
const singleUploadBtn = document.getElementById('singleUploadBtn');
const batchUploadBtn = document.getElementById('batchUploadBtn');
if (singleUploadBtn) {
singleUploadBtn.addEventListener('click', () => {
isBatchMode = false;
selectedFiles = [];
openFileBrowser();
});
}
if (batchUploadBtn) {
batchUploadBtn.addEventListener('click', () => {
isBatchMode = true;
selectedFiles = [];
openFileBrowser();
});
}
// Close modal handlers
if (fileBrowserClose) {
fileBrowserClose.addEventListener('click', closeFileBrowser);
}
if (fileBrowserCancel) {
fileBrowserCancel.addEventListener('click', closeFileBrowser);
}
if (fileBrowserModal) {
fileBrowserModal.addEventListener('click', (e) => {
if (e.target === fileBrowserModal) {
closeFileBrowser();
}
});
}
// Select button handler
if (fileBrowserSelect) {
fileBrowserSelect.addEventListener('click', () => {
if (selectedFiles.length === 0) return;
if (isBatchMode) {
loadFilesFromBrowser(selectedFiles);
} else {
loadFilesFromBrowser([selectedFiles[0]]);
}
closeFileBrowser();
});
}
// File browser functions
async function openFileBrowser() {
if (!fileBrowserModal || !fileBrowserList) return;
fileBrowserModal.classList.add('active');
document.body.classList.add('modal-open'); // Prevent body scrolling
fileBrowserList.innerHTML = '<div class="file-browser-loading">Loading images...</div>';
if (fileBrowserSelect) fileBrowserSelect.disabled = true;
selectedFiles = [];
currentPath = ''; // Reset to root directory
await loadDirectory('');
}
async function loadDirectory(subpath) {
if (!fileBrowserList) return;
fileBrowserList.innerHTML = '<div class="file-browser-loading">Loading...</div>';
if (fileBrowserSelect) fileBrowserSelect.disabled = true;
selectedFiles = [];
currentPath = subpath;
try {
let url = '/api/list_testimages/flat_surface_detection';
if (subpath) {
url += '/' + encodeURIComponent(subpath);
}
const response = await fetch(url);
const data = await response.json();
// Display dataset repo link if available (only at root level)
if (data.repo_url && !subpath) {
const repoLinkDiv = document.createElement('div');
repoLinkDiv.className = 'dataset-repo-link';
repoLinkDiv.style.cssText = 'padding: 10px; margin-bottom: 10px; background: rgba(0, 206, 209, 0.1); border-radius: 5px; border: 1px solid rgba(0, 206, 209, 0.3);';
repoLinkDiv.innerHTML = `<a href="${data.repo_url}" target="_blank" style="color: #00CED1; text-decoration: none; font-size: 0.9em;"><i class="fas fa-external-link-alt"></i> View Dataset on Hugging Face: ${data.repo_id || 'Dataset Repository'}</a>`;
fileBrowserList.appendChild(repoLinkDiv);
}
if (data.success && (data.directories && data.directories.length > 0 || data.files && data.files.length > 0)) {
// Update breadcrumb
updateBreadcrumb(data.relative_path || '');
if (!subpath || !data.repo_url) {
// Only clear if we haven't already added repo link
fileBrowserList.innerHTML = '';
}
// Display directories first
if (data.directories && data.directories.length > 0) {
data.directories.forEach(dir => {
const dirItem = document.createElement('div');
dirItem.className = 'file-browser-item directory';
dirItem.dataset.filename = dir.name;
dirItem.dataset.type = 'directory';
dirItem.innerHTML = `
<div class="file-name">📁 ${dir.name}</div>
<div class="file-size">Directory</div>
`;
// Click to navigate into directory
dirItem.addEventListener('click', () => {
const newPath = currentPath ? `${currentPath}/${dir.name}` : dir.name;
loadDirectory(newPath);
});
fileBrowserList.appendChild(dirItem);
});
}
// Display files
if (data.files && data.files.length > 0) {
data.files.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-browser-item';
fileItem.dataset.filename = file.name;
fileItem.dataset.type = 'file';
const filePath = currentPath ? `${currentPath}/${file.name}` : file.name;
const fileSize = formatFileSize(file.size);
fileItem.innerHTML = `
<div class="file-name">${file.name}</div>
<div class="file-size">${fileSize}</div>
`;
// Single click - select in batch mode, import directly in single mode
fileItem.addEventListener('click', () => {
if (isBatchMode) {
if (fileItem.classList.contains('selected')) {
fileItem.classList.remove('selected');
selectedFiles = selectedFiles.filter(f => f.path !== filePath);
} else {
fileItem.classList.add('selected');
selectedFiles.push({ name: file.name, path: filePath });
}
if (fileBrowserSelect) fileBrowserSelect.disabled = selectedFiles.length === 0;
} else {
document.querySelectorAll('.file-browser-item').forEach(item => {
item.classList.remove('selected');
});
fileItem.classList.add('selected');
selectedFiles = [{ name: file.name, path: filePath }];
if (fileBrowserSelect) fileBrowserSelect.disabled = false;
loadFilesFromBrowser([{ name: file.name, path: filePath }]);
closeFileBrowser();
}
});
// Double click - always import directly
fileItem.addEventListener('dblclick', () => {
loadFilesFromBrowser([{ name: file.name, path: filePath }]);
closeFileBrowser();
});
fileBrowserList.appendChild(fileItem);
});
}
} else {
updateBreadcrumb(data.relative_path || '');
fileBrowserList.innerHTML = '<div class="file-browser-empty">No files or directories found</div>';
}
} catch (error) {
console.error('Error loading directory:', error);
fileBrowserList.innerHTML = '<div class="file-browser-empty">Error loading directory. Please try again.</div>';
}
}
function updateBreadcrumb(relativePath) {
if (!fileBrowserBreadcrumb) return;
fileBrowserBreadcrumb.innerHTML = '';
// Add root
const rootItem = document.createElement('span');
rootItem.className = 'file-browser-breadcrumb-item' + (relativePath === '' ? ' active' : '');
rootItem.textContent = 'Root';
rootItem.addEventListener('click', () => {
if (relativePath !== '') {
loadDirectory('');
}
});
fileBrowserBreadcrumb.appendChild(rootItem);
// Add path segments
if (relativePath) {
const segments = relativePath.split('/');
let currentPath = '';
segments.forEach((segment, index) => {
// Separator
const separator = document.createElement('span');
separator.className = 'file-browser-breadcrumb-separator';
separator.textContent = ' / ';
fileBrowserBreadcrumb.appendChild(separator);
// Path segment
currentPath = currentPath ? `${currentPath}/${segment}` : segment;
const isLast = index === segments.length - 1;
const pathItem = document.createElement('span');
pathItem.className = 'file-browser-breadcrumb-item' + (isLast ? ' active' : '');
pathItem.textContent = segment;
if (!isLast) {
pathItem.addEventListener('click', () => {
loadDirectory(currentPath);
});
}
fileBrowserBreadcrumb.appendChild(pathItem);
});
}
}
function closeFileBrowser() {
if (fileBrowserModal) fileBrowserModal.classList.remove('active');
document.body.classList.remove('modal-open'); // Re-enable body scrolling
selectedFiles = [];
currentPath = '';
if (fileBrowserSelect) fileBrowserSelect.disabled = true;
}
async function loadFilesFromBrowser(fileItems) {
try {
// fileItems can be either array of strings (old format) or array of objects with {name, path}
const normalizedItems = fileItems.map(item => {
if (typeof item === 'string') {
return { name: item, path: item };
}
return item;
});
if (isBatchMode) {
const files = [];
for (const fileItem of normalizedItems) {
const response = await fetch(`/api/get_testimage/flat_surface_detection/${encodeURIComponent(fileItem.path)}`);
if (!response.ok) {
throw new Error(`Failed to load ${fileItem.name}: ${response.statusText}`);
}
const blob = await response.blob();
const ext = fileItem.name.split('.').pop().toLowerCase();
const mimeTypes = {
'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
'sto': 'application/octet-stream'
};
const mimeType = mimeTypes[ext] || blob.type || 'image/png';
const file = new File([blob], fileItem.name, { type: mimeType, lastModified: Date.now() });
files.push(file);
}
// Set selectedFile to first file
if (files.length > 0) {
selectedFile = files[0];
batchFiles = files;
currentBatchIndex = 0;
}
const dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
const batchFileInput = document.getElementById('batchFileInput');
if (batchFileInput) {
batchFileInput.files = dataTransfer.files;
batchFileInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// Ensure controls are enabled after file is loaded
// Use setTimeout to ensure change event handlers have run
setTimeout(() => {
enableControlsAfterUpload();
}, 100);
} else {
const fileItem = normalizedItems[0];
const response = await fetch(`/api/get_testimage/flat_surface_detection/${encodeURIComponent(fileItem.path)}`);
if (!response.ok) {
throw new Error(`Failed to load ${fileItem.name}: ${response.statusText}`);
}
const blob = await response.blob();
const ext = fileItem.name.split('.').pop().toLowerCase();
const mimeTypes = {
'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
'sto': 'application/octet-stream'
};
const mimeType = mimeTypes[ext] || blob.type || 'image/png';
const file = new File([blob], fileItem.name, { type: mimeType, lastModified: Date.now() });
// Set selectedFile before triggering change event
selectedFile = file;
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
const singleFileInput = document.getElementById('singleFileInput');
if (singleFileInput) {
singleFileInput.files = dataTransfer.files;
singleFileInput.value = '';
singleFileInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// Ensure controls are enabled after file is loaded
// Use setTimeout to ensure change event handlers have run
setTimeout(() => {
enableControlsAfterUpload();
}, 100);
}
} catch (error) {
console.error('Error loading file from browser:', error);
alert(`Error loading file: ${error.message}. Please try again.`);
}
}
function enableControlsAfterUpload() {
// Update filename display
const filenameElement = document.getElementById('filenameValue');
if (selectedFile && filenameElement) {
filenameElement.textContent = selectedFile.name;
}
// Show image controls
const imageControls = document.getElementById('imageControls');
if (imageControls) {
// Image controls are hidden via CSS
}
// Enable weight selection dropdown
const modelSelect = document.getElementById('modelSelect');
if (modelSelect) {
modelSelect.disabled = false;
// Ensure weights are loaded if not already
if (modelSelect.options.length <= 1) {
loadModelWeights();
}
}
// Update detect button state based on both image and weight selection
updateDetectButtonState();
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
</script>
{% endblock %}