|
|
<!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; |
|
|
background-color: #f4f4f4; |
|
|
color: #333; |
|
|
} |
|
|
.header { |
|
|
background-color: #ffffff; |
|
|
padding: 15px 0; |
|
|
display: flex; |
|
|
justify-content: space-around; |
|
|
align-items: center; |
|
|
margin-bottom: 0; |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
|
border-bottom: 2px solid #eeeeee; |
|
|
min-height: 100px; |
|
|
} |
|
|
.header img { |
|
|
|
|
|
max-height: 100%; |
|
|
max-width: 250px; |
|
|
object-fit: contain; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
#brainiac-logo { |
|
|
|
|
|
max-height: 100%; |
|
|
max-width: 420px; |
|
|
} |
|
|
.container { |
|
|
max-width: 700px; |
|
|
margin: 40px auto; |
|
|
background: #fff; |
|
|
padding: 30px; |
|
|
border-radius: 8px; |
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
h1 { |
|
|
text-align: center; |
|
|
color: #34495e; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
.upload-form { |
|
|
margin-top: 20px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
border-top: 1px solid #eee; |
|
|
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; |
|
|
width: 100%; |
|
|
} |
|
|
.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; |
|
|
border: 1px solid #ccc; |
|
|
padding: 10px; |
|
|
border-radius: 4px; |
|
|
width: 90%; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
.upload-form input[type="submit"] { |
|
|
background-color: #3498db; |
|
|
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; |
|
|
} |
|
|
.result { |
|
|
margin-top: 30px; |
|
|
padding: 20px; |
|
|
background-color: #eafaf1; |
|
|
border: 1px solid #c3e6cb; |
|
|
border-radius: 5px; |
|
|
text-align: center; |
|
|
} |
|
|
.result h2 { |
|
|
margin-top: 0; |
|
|
color: #155724; |
|
|
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; |
|
|
} |
|
|
.saliency-plot { |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.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%; |
|
|
} |
|
|
.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 { |
|
|
background-color: #d4edda; |
|
|
color: #155724; |
|
|
border: 1px solid #c3e6cb; |
|
|
} |
|
|
|
|
|
|
|
|
#loading-indicator { |
|
|
display: none; |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(255, 255, 255, 0.7); |
|
|
z-index: 1000; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
flex-direction: column; |
|
|
} |
|
|
.spinner { |
|
|
border: 5px solid #f3f3f3; |
|
|
border-top: 5px solid #3498db; |
|
|
border-radius: 50%; |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
animation: spin 1s linear infinite; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
@keyframes spin { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
#loading-indicator p { |
|
|
font-weight: bold; |
|
|
color: #333; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="header"> |
|
|
|
|
|
<img src="{{ url_for('static', filename='images/kannlab.png') }}" alt="Kann Lab Logo"> |
|
|
|
|
|
<img id="brainiac-logo" src="{{ url_for('static', filename='images/brainiac.jpeg') }}" alt="BrainIAC 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> |
|
|
|
|
|
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'); |
|
|
|
|
|
const uniqueId = slider.dataset.uniqueId; |
|
|
const tempPath = slider.dataset.tempPath; |
|
|
const totalSlices = parseInt(slider.max, 10) + 1; |
|
|
let isLoading = false; |
|
|
let debounceTimer; |
|
|
|
|
|
|
|
|
function updateSliceImages(sliceIndex) { |
|
|
if (isLoading) return; |
|
|
isLoading = true; |
|
|
loadingIndicator.style.display = 'inline'; |
|
|
|
|
|
|
|
|
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 => { |
|
|
|
|
|
console.log("Received data:", data); |
|
|
if (data.error) { |
|
|
console.error("Error fetching slice:", data.error); |
|
|
|
|
|
} else { |
|
|
|
|
|
if (data.input_slice) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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) { |
|
|
|
|
|
overlayImg.src = `data:image/png;base64,${data.overlay_slice}`; |
|
|
console.log("Updated overlayImg src"); |
|
|
} else { |
|
|
console.error("overlay_slice key missing in response data"); |
|
|
} |
|
|
|
|
|
sliceLabel.textContent = sliceIndex + 1; |
|
|
} |
|
|
}) |
|
|
.catch(error => { |
|
|
|
|
|
console.error('Error fetching or processing slice data:', error); |
|
|
|
|
|
}) |
|
|
.finally(() => { |
|
|
isLoading = false; |
|
|
loadingIndicator.style.display = 'none'; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
slider.addEventListener('input', function() { |
|
|
const sliceIndex = parseInt(this.value, 10); |
|
|
sliceLabel.textContent = sliceIndex + 1; |
|
|
|
|
|
|
|
|
clearTimeout(debounceTimer); |
|
|
debounceTimer = setTimeout(() => { |
|
|
if (sliceIndex >= 0 && sliceIndex < totalSlices) { |
|
|
updateSliceImages(sliceIndex); |
|
|
} |
|
|
}, 150); |
|
|
}); |
|
|
|
|
|
|
|
|
</script> |
|
|
|
|
|
{% endif %} |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="loading-indicator"> |
|
|
<div class="spinner"></div> |
|
|
<p>Processing... Please wait.</p> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
function showLoader() { |
|
|
|
|
|
document.getElementById('loading-indicator').style.display = 'flex'; |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |