Divytak's picture
Update src/BrainIAC/templates/index.html
0027b61 verified
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Brain Age Prediction</title>
<style>
body {
font-family: sans-serif;
margin: 0; /* Remove default margin */
background-color: #f4f4f4;
color: #333;
}
.header {
background-color: #ffffff; /* White background */
padding: 15px 0; /* Reduced padding slightly */
display: flex;
justify-content: space-around;
align-items: center; /* Align items vertically centered */
margin-bottom: 0;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
border-bottom: 2px solid #eeeeee;
min-height: 100px; /* Set a minimum height for the header */
}
.header img {
/* height: 100px; */ /* REMOVED fixed height */
max-height: 100%; /* Allow scaling up to container height */
max-width: 250px; /* Keep max width constraint for default */
object-fit: contain;
vertical-align: middle;
}
/* Style for the larger middle logo */
#brainiac-logo {
/* height: 180px; */ /* REMOVED fixed height */
max-height: 100%; /* Allow scaling up to container height */
max-width: 420px; /* Keep max width constraint */
}
.container {
max-width: 700px;
margin: 40px auto; /* Add margin back to container top/bottom */
background: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #34495e; /* Darker heading color */
margin-bottom: 30px;
}
.upload-form {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
border-top: 1px solid #eee; /* Separator line */
padding-top: 30px;
}
.upload-form label {
margin-bottom: 10px;
font-weight: bold;
color: #555;
}
.upload-form .file-type-selector, .upload-form .preprocess-selector, .upload-form .saliency-selector {
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
gap: 15px; /* Space between items */
width: 100%; /* Make selectors take full width */
}
.upload-form .file-type-selector span, .upload-form .preprocess-selector, .upload-form .saliency-selector {
display: flex;
align-items: center;
}
.upload-form .file-type-selector label, .upload-form .preprocess-selector label, .upload-form .saliency-selector label {
font-weight: normal;
margin-bottom: 0;
margin-left: 5px;
color: #333;
}
.upload-form input[type="file"] {
margin-bottom: 25px; /* More space before button */
border: 1px solid #ccc;
padding: 10px;
border-radius: 4px;
width: 90%;
box-sizing: border-box; /* Include padding in width */
}
.upload-form input[type="submit"] {
background-color: #3498db; /* Brighter blue */
color: white;
padding: 12px 25px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
.upload-form input[type="submit"]:hover {
background-color: #2980b9; /* Darker blue on hover */
}
.result {
margin-top: 30px;
padding: 20px;
background-color: #eafaf1; /* Light green background */
border: 1px solid #c3e6cb;
border-radius: 5px;
text-align: center;
}
.result h2 {
margin-top: 0;
color: #155724; /* Dark green text */
margin-bottom: 10px;
}
.result p {
font-size: 1.1em;
color: #333;
}
.saliency-results {
margin-top: 40px;
padding-top: 30px;
border-top: 1px solid #eee;
text-align: center;
}
.saliency-results h2 {
color: #34495e;
margin-bottom: 20px;
}
.saliency-plots {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 15px; /* Gap between plots */
}
.saliency-plot {
margin-bottom: 20px; /* Space below each plot container */
}
.saliency-plot h4 {
margin-bottom: 8px;
color: #555;
font-size: 0.9em;
text-transform: uppercase;
}
.saliency-plot img {
border: 1px solid #ddd;
padding: 3px;
background-color: #fff;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
max-width: 100%; /* Ensure images are responsive */
}
.flash-messages {
list-style: none;
padding: 0;
margin-bottom: 20px;
}
.flash-messages li {
padding: 10px 15px;
margin-bottom: 10px;
border-radius: 4px;
}
.flash-messages .error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.flash-messages .success { /* Optional: for success messages */
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
/* Loading Indicator Styles */
#loading-indicator {
display: none; /* Hidden by default */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7); /* Semi-transparent white background */
z-index: 1000; /* Ensure it's on top */
justify-content: center;
align-items: center;
flex-direction: column; /* Stack spinner and text vertically */
}
.spinner {
border: 5px solid #f3f3f3; /* Light grey */
border-top: 5px solid #3498db; /* Blue */
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 15px; /* Space between spinner and text */
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#loading-indicator p {
font-weight: bold;
color: #333;
}
</style>
</head>
<body>
<div class="header">
<!-- Left Logo -->
<img src="{{ url_for('static', filename='images/kannlab.png') }}" alt="Kann Lab Logo">
<!-- Middle Logo (Larger) -->
<img id="brainiac-logo" src="{{ url_for('static', filename='images/brainiac.jpeg') }}" alt="BrainIAC Logo">
<!-- Right Logo -->
<img src="{{ url_for('static', filename='images/brainage.jpeg') }}" alt="Brain Age Logo">
</div>
<div class="container">
<h1>Brain Age Prediction</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class=flash-messages>
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<form method="post" action="{{ url_for('predict') }}" enctype="multipart/form-data" class="upload-form" onsubmit="showLoader()">
<div class="file-type-selector">
<label>Select file type:</label>
<span>
<input type="radio" id="nifti_type" name="file_type" value="nifti" checked>
<label for="nifti_type">NIfTI (.nii.gz)</label>
</span>
<span>
<input type="radio" id="dicom_type" name="file_type" value="dicom">
<label for="dicom_type">DICOM (.zip)</label>
</span>
</div>
<div class="preprocess-selector">
<input type="checkbox" id="preprocess_check" name="preprocess" value="yes">
<label for="preprocess_check">Run Preprocessing (Registration, Normalization, Skull Stripping)?</label>
</div>
<div class="saliency-selector">
<input type="checkbox" id="saliency_check" name="generate_saliency" value="yes">
<label for="saliency_check">Generate Saliency Map Visualizations?</label>
</div>
<label for="scan_file" style="margin-top: 20px;">Upload Scan File:</label>
<input type="file" id="scan_file" name="scan_file" accept="application/gzip,application/x-gzip,.nii.gz,.zip" required>
<input type="submit" value="Predict Brain Age">
</form>
{% if prediction %}
<div class="result">
<h2>Prediction Result</h2>
<p>Predicted Brain Age: <strong>{{ prediction }}</strong></p>
</div>
{% endif %}
{# Saliency Map Display Section - Modified for Dynamic Loading #}
{% if saliency_info %}
<div class="saliency-results">
<h2>Saliency Map Visualizations (Axial View)</h2>
{# Slice Slider and Label #}
<div class="slice-slider-container" style="margin-bottom: 20px; text-align: center;">
<label for="slice-slider">Slice: <span id="slice-number-label">{{ saliency_info.center_slice_index + 1 }}</span> / {{ saliency_info.num_slices }}</label><br>
{# Store unique_id AND temp_dir_path as data attributes #}
<input type="range" id="slice-slider"
min="0" max="{{ saliency_info.num_slices - 1 }}"
value="{{ saliency_info.center_slice_index }}"
style="width: 80%; margin-top: 5px;"
data-unique-id="{{ saliency_info.unique_id }}"
data-temp-path="{{ saliency_info.temp_dir_path }}">
<span id="slice-loading-indicator" style="display: none; margin-left: 10px;">Loading...</span> {# Small loading text #}
</div>
<div class="saliency-plots">
<div class="saliency-plot">
<h4>Input Slice</h4>
{# Use initial center slice data #}
<img id="input-slice-img" src="data:image/png;base64,{{ saliency_info.center_slice_plots.input_slice }}" alt="Input MRI Slice" width="200">
</div>
<div class="saliency-plot">
<h4>Saliency Heatmap</h4>
{# Use initial center slice data #}
<img id="heatmap-slice-img" src="data:image/png;base64,{{ saliency_info.center_slice_plots.heatmap_slice }}" alt="Saliency Heatmap" width="200">
</div>
<div class="saliency-plot">
<h4>Overlay</h4>
{# Use initial center slice data #}
<img id="overlay-slice-img" src="data:image/png;base64,{{ saliency_info.center_slice_plots.overlay_slice }}" alt="Saliency Overlay" width="200">
</div>
</div>
</div>
{# JavaScript for Slider Interaction with Fetch #}
<script>
// Get DOM elements
const slider = document.getElementById('slice-slider');
const sliceLabel = document.getElementById('slice-number-label');
const inputImg = document.getElementById('input-slice-img');
const heatmapImg = document.getElementById('heatmap-slice-img');
const overlayImg = document.getElementById('overlay-slice-img');
const loadingIndicator = document.getElementById('slice-loading-indicator');
// Get unique ID and TEMP PATH from data attributes
const uniqueId = slider.dataset.uniqueId;
const tempPath = slider.dataset.tempPath;
const totalSlices = parseInt(slider.max, 10) + 1;
let isLoading = false;
let debounceTimer;
// Function to fetch and update slice images
function updateSliceImages(sliceIndex) {
if (isLoading) return;
isLoading = true;
loadingIndicator.style.display = 'inline';
// Construct URL with path as a query parameter (URL-encoded)
const encodedPath = encodeURIComponent(tempPath);
const fetchUrl = `/get_slice/${uniqueId}/${sliceIndex}?path=${encodedPath}`;
console.log("Fetching:", fetchUrl);
fetch(fetchUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
// Log the received data for debugging
console.log("Received data:", data);
if (data.error) {
console.error("Error fetching slice:", data.error);
// Optionally display an error to the user
} else {
// Check if keys exist before assigning & log
if (data.input_slice) {
// PREPEND the Data URI prefix
inputImg.src = `data:image/png;base64,${data.input_slice}`;
console.log("Updated inputImg src");
} else {
console.error("input_slice key missing in response data");
}
if (data.heatmap_slice) {
// PREPEND the Data URI prefix
heatmapImg.src = `data:image/png;base64,${data.heatmap_slice}`;
console.log("Updated heatmapImg src");
} else {
console.error("heatmap_slice key missing in response data");
}
if (data.overlay_slice) {
// PREPEND the Data URI prefix
overlayImg.src = `data:image/png;base64,${data.overlay_slice}`;
console.log("Updated overlayImg src");
} else {
console.error("overlay_slice key missing in response data");
}
// Update label (display 1-based index)
sliceLabel.textContent = sliceIndex + 1;
}
})
.catch(error => {
// Catch network errors or errors thrown from .then(response => ...)
console.error('Error fetching or processing slice data:', error);
// Optionally display an error to the user
})
.finally(() => {
isLoading = false;
loadingIndicator.style.display = 'none'; // Hide loading indicator
});
}
// Add event listener to the slider with debouncing
slider.addEventListener('input', function() {
const sliceIndex = parseInt(this.value, 10);
sliceLabel.textContent = sliceIndex + 1; // Update label immediately
// Debounce the fetch call
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (sliceIndex >= 0 && sliceIndex < totalSlices) {
updateSliceImages(sliceIndex);
}
}, 150); // Adjust debounce time (ms) as needed
});
// Initial state is set by Jinja server-side
</script>
{% endif %}
</div>
<!-- Loading Indicator HTML -->
<div id="loading-indicator">
<div class="spinner"></div>
<p>Processing...</p>
</div>
<script>
function showLoader() {
// Display the loading indicator using flex for centering
document.getElementById('loading-indicator').style.display = 'flex';
}
</script>
</body>
</html>