spad_for_vision / templates /material_detection_head.html
0001AMA's picture
SPAD for Vision: app and docs
e5f2dfe
Raw
History Blame Contribute Delete
56.5 kB
{% extends "base.html" %}
{% block title %}Material Detection Head - 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;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
justify-content: center;
align-items: center;
}
.file-browser-modal.active {
display: flex;
}
.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);
}
.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-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-wrapper {
position: relative;
width: 100%;
height: 300px;
border: 2px dashed #B91D30;
border-radius: 15px;
overflow: hidden;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.image-preview-wrapper.has-image {
border-style: solid;
background: transparent;
}
.image-preview-wrapper img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 10px;
}
.image-preview-wrapper .placeholder {
color: #666;
text-align: center;
font-size: 1rem;
}
.image-preview-wrapper .placeholder i {
font-size: 3rem;
margin-bottom: 10px;
display: block;
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>
Material 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="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>
<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 Material
</button>
<div class="batch-navigation" id="batchNavigation" style="display: none;">
<button class="nav-btn" id="prevImageBtn" disabled>
Previous
</button>
<button class="nav-btn" id="nextImageBtn" disabled>
Next
</button>
</div>
</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 material(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 material(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 currentRotation = 0;
let currentFlip = { horizontal: false, vertical: false };
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() {
// 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 singleFileInput = document.getElementById('singleFileInput');
const batchFileInput = document.getElementById('batchFileInput');
let selectedFiles = [];
let isBatchMode = false;
// Upload options - open file browser modal when clicked
const uploadOptions = document.querySelector('.upload-options');
if (uploadOptions) {
uploadOptions.addEventListener('click', (e) => {
// Don't trigger if clicking directly on the buttons
if (e.target.closest('.upload-btn')) {
return;
}
// Default to single mode when clicking on upload-options
isBatchMode = false;
selectedFiles = [];
openFileBrowser();
});
}
// Upload buttons - open file browser modal
document.getElementById('singleUploadBtn').addEventListener('click', () => {
isBatchMode = false;
selectedFiles = [];
openFileBrowser();
});
document.getElementById('batchUploadBtn').addEventListener('click', () => {
isBatchMode = true;
selectedFiles = [];
openFileBrowser();
});
// Close modal handlers
fileBrowserClose.addEventListener('click', closeFileBrowser);
fileBrowserCancel.addEventListener('click', closeFileBrowser);
fileBrowserModal.addEventListener('click', (e) => {
if (e.target === fileBrowserModal) {
closeFileBrowser();
}
});
// Select button handler
fileBrowserSelect.addEventListener('click', () => {
if (selectedFiles.length === 0) return;
if (isBatchMode) {
loadFilesFromBrowser(selectedFiles);
} else {
loadFilesFromBrowser([selectedFiles[0]]);
}
closeFileBrowser();
});
// File browser functions
let currentPath = '';
async function openFileBrowser(subpath = '') {
fileBrowserModal.classList.add('active');
fileBrowserList.innerHTML = '<div class="file-browser-loading">Loading images...</div>';
fileBrowserSelect.disabled = true;
selectedFiles = [];
currentPath = subpath;
try {
let url = '/api/list_testimages/material_detection_head';
if (subpath) {
url += '/' + encodeURIComponent(subpath);
}
const response = await fetch(url);
const data = await response.json();
if (data.success && (data.directories && data.directories.length > 0 || data.files && data.files.length > 0)) {
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;
openFileBrowser(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 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');
const filePath = currentPath ? `${currentPath}/${file.name}` : file.name;
selectedFiles = selectedFiles.filter(f => f !== filePath);
} else {
fileItem.classList.add('selected');
const filePath = currentPath ? `${currentPath}/${file.name}` : file.name;
selectedFiles.push(filePath);
}
fileBrowserSelect.disabled = selectedFiles.length === 0;
} else {
document.querySelectorAll('.file-browser-item').forEach(item => {
item.classList.remove('selected');
});
fileItem.classList.add('selected');
const filePath = currentPath ? `${currentPath}/${file.name}` : file.name;
selectedFiles = [filePath];
fileBrowserSelect.disabled = false;
loadFilesFromBrowser([filePath]);
closeFileBrowser();
}
});
// Double click - always import directly
fileItem.addEventListener('dblclick', () => {
const filePath = currentPath ? `${currentPath}/${file.name}` : file.name;
loadFilesFromBrowser([filePath]);
closeFileBrowser();
});
fileBrowserList.appendChild(fileItem);
});
}
} else {
fileBrowserList.innerHTML = '<div class="file-browser-empty">No files or directories found</div>';
}
} catch (error) {
console.error('Error loading test images:', error);
fileBrowserList.innerHTML = '<div class="file-browser-empty">Error loading images. Please try uploading from your device.</div>';
}
}
function closeFileBrowser() {
fileBrowserModal.classList.remove('active');
selectedFiles = [];
currentPath = '';
fileBrowserSelect.disabled = true;
}
async function loadFilesFromBrowser(filenames) {
try {
if (isBatchMode) {
const files = [];
for (const filename of filenames) {
const response = await fetch(`/api/get_testimage/material_detection_head/${encodeURIComponent(filename)}`);
if (!response.ok) {
throw new Error(`Failed to load ${filename}: ${response.statusText}`);
}
const blob = await response.blob();
const ext = filename.split('.').pop();
const mimeTypes = {
'jpg': 'image/jpeg', 'JPG': 'image/jpeg',
'jpeg': 'image/jpeg', 'JPEG': 'image/jpeg',
'png': 'image/png', 'PNG': 'image/png',
'gif': 'image/gif', 'GIF': 'image/gif',
'webp': 'image/webp', 'WEBP': 'image/webp',
'bmp': 'image/bmp', 'BMP': 'image/bmp',
'tiff': 'image/tiff', 'TIFF': 'image/tiff',
'tif': 'image/tiff', 'TIF': 'image/tiff',
'heic': 'image/heic', 'HEIC': 'image/heic',
'sto': 'application/octet-stream', 'STO': 'application/octet-stream'
};
const mimeType = mimeTypes[ext] || mimeTypes[ext.toLowerCase()] || blob.type || 'image/jpeg';
const file = new File([blob], filename, { type: mimeType, lastModified: Date.now() });
files.push(file);
}
const dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
// Clear the input first, then set files
batchFileInput.value = '';
batchFileInput.files = dataTransfer.files;
// Trigger change event to process the files and display preview
const changeEvent = new Event('change', { bubbles: true });
batchFileInput.dispatchEvent(changeEvent);
} else {
const filename = filenames[0];
const response = await fetch(`/api/get_testimage/material_detection_head/${encodeURIComponent(filename)}`);
if (!response.ok) {
throw new Error(`Failed to load ${filename}: ${response.statusText}`);
}
const blob = await response.blob();
const ext = filename.split('.').pop().toLowerCase();
const mimeTypes = {
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
'gif': 'image/gif', 'webp': 'image/webp', 'heic': 'image/heic', 'HEIC': 'image/heic'
};
const mimeType = mimeTypes[ext] || blob.type || 'image/jpeg';
const file = new File([blob], filename, { type: mimeType, lastModified: Date.now() });
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
// Clear the input first, then set files
singleFileInput.value = '';
singleFileInput.files = dataTransfer.files;
// Trigger change event to process the file and display preview
const changeEvent = new Event('change', { bubbles: true });
singleFileInput.dispatchEvent(changeEvent);
}
} 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];
}
// File inputs
document.getElementById('singleFileInput').addEventListener('change', handleSingleFileSelect);
document.getElementById('batchFileInput').addEventListener('change', handleBatchFileSelect);
// Detect button
document.getElementById('detectBtn').addEventListener('click', detectObjects);
// Clear status 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';
// 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/material_detection_head_weights');
fetch('/api/material_detection_head_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 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(data => {
if (data.success && data.image) {
const img = new Image();
img.onload = function() {
selectedFile = file;
batchFiles = [file];
currentBatchIndex = 0;
currentImage = img;
resetImage();
// Call displayImage to handle filename update and prediction reset
displayImage(file);
updateImageMetrics(img);
const psEl = document.getElementById('processingStatusValue');
const hasWeight = !!document.getElementById('modelSelect').value;
if (psEl) psEl.textContent = hasWeight ? 'Ready' : 'Not ready';
};
img.src = data.image;
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 {
// Image controls are hidden via CSS
} 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(data => {
if (data.success && data.image) {
const img = new Image();
img.onload = function() {
selectedFile = first;
currentImage = img;
resetImage();
// Call displayImage to handle filename update and prediction reset
displayImage(first);
updateImageMetrics(img);
const psEl2 = document.getElementById('processingStatusValue');
const hasWeight2 = !!document.getElementById('modelSelect').value;
if (psEl2) psEl2.textContent = hasWeight2 ? 'Ready' : 'Not ready';
};
img.src = data.image;
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 material(s)';
} catch (e) { /* ignore */ }
// Update file metrics
updateFileMetrics(file);
const reader = new FileReader();
reader.onload = function(e) {
// Load the image for transformations
const img = new Image();
img.onload = function() {
currentImage = img;
resetImage();
// Update image-specific metrics
updateImageMetrics(img);
};
img.src = e.target.result;
};
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 rotateImage(degrees) {
if (!currentImage) return;
currentRotation = (currentRotation + degrees) % 360;
if (currentRotation < 0) currentRotation += 360;
}
function flipImage(direction) {
if (!currentImage) return;
if (direction === 'horizontal') {
currentFlip.horizontal = !currentFlip.horizontal;
} else if (direction === 'vertical') {
currentFlip.vertical = !currentFlip.vertical;
}
}
function resetImage() {
if (!currentImage) return;
currentRotation = 0;
currentFlip = { horizontal: false, vertical: false };
}
// Function to create a hash of current image state
function createImageHash() {
if (!currentImage || !selectedFile) return null;
// Create a simple hash based on filename, rotation, and flip state
const hashString = `${selectedFile.name}_${currentRotation}_${currentFlip.horizontal}_${currentFlip.vertical}`;
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);
}
if (currentImage) {
updateImageMetrics(currentImage);
}
} 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 - only count objects with >90% confidence
let count = 0;
if (data.top3_predictions && data.top3_predictions.length > 0) {
count = data.top3_predictions.filter(pred => pred.probability > 0.9).length;
}
document.getElementById('detectionCount').textContent = `Found ${count} object(s)`;
}
function showLoading() {
document.getElementById('detectBtn').disabled = true;
document.getElementById('detectBtn').innerHTML = 'Detecting...';
}
function hideLoading() {
document.getElementById('detectBtn').disabled = false;
document.getElementById('detectBtn').innerHTML = 'Detect Material';
}
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);
}
}
</script>
{% endblock %}