Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 238 additions & 53 deletions app.py

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions requirements-web.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Web Application Dependencies
Flask==3.1.3
flask-cors==6.0.0
Flask-Caching>=2.0.0
Flask-Limiter>=3.0.0
flask-talisman>=1.1.0
gunicorn>=21.2.0

python-dotenv==1.2.2
pymongo>=4.6.0
Expand Down Expand Up @@ -32,3 +36,7 @@ scipy>=1.10.0

# Visualization
plotly>=5.13.0

# Medical Imaging
pydicom>=2.4.0

105 changes: 105 additions & 0 deletions static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -15785,4 +15785,109 @@ section {
.seg-transform-v3 .transform-arrow i {
transform: rotate(90deg);
}
}

/* Dummy Weights Banner styling */
.dummy-weights-banner {
background-color: #fef3c7;
border-bottom: 1px solid #f59e0b;
color: #92400e;
padding: 0.75rem 1rem;
font-size: 0.9rem;
font-family: var(--font-primary);
position: relative;
z-index: 1000;
}

.dark-theme .dummy-weights-banner {
background-color: #78350f;
border-bottom: 1px solid #f59e0b;
color: #fef3c7;
}

.dummy-weights-banner .banner-content {
display: flex;
align-items: center;
gap: 0.75rem;
}

.dummy-weights-banner .banner-icon {
color: #d97706;
font-size: 1.2rem;
flex-shrink: 0;
}

.dummy-weights-banner .banner-text a {
color: #3b82f6;
text-decoration: underline;
font-weight: 600;
}

.dark-theme .dummy-weights-banner .banner-text a {
color: #60a5fa;
}

/* Patient & Scan Info Section (DICOM) */
.patient-info-section {
margin-top: 1.5rem;
padding: 1.25rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--gray-200);
border-radius: 12px;
}

.dark-theme .patient-info-section {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--gray-700);
}

.patient-info-section .patient-info-title {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.1rem;
color: var(--planetary);
display: flex;
align-items: center;
gap: 0.5rem;
}

.dark-theme .patient-info-section .patient-info-title {
color: var(--universe);
}

.patient-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
}

.patient-info-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.05);
}

.dark-theme .patient-info-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255,255,255,0.05);
}

.patient-info-item .info-label {
font-size: 0.75rem;
color: var(--gray-500);
font-weight: 500;
}

.patient-info-item .info-value {
font-size: 0.95rem;
font-weight: 600;
color: var(--gray-800);
}

.dark-theme .patient-info-item .info-value {
color: var(--gray-200);
}
53 changes: 48 additions & 5 deletions static/js/script.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// NeuroScan AI - Interactive JavaScript

// ==================== Configuration ====================
const API_BASE_URL = 'http://localhost:5000';
const API_BASE_URL = window.location.origin;

// ==================== Theme ====================
const savedTheme = localStorage.getItem('theme');
Expand Down Expand Up @@ -234,15 +234,15 @@ function validateAndPreviewFile(file) {
}

// Validate file type
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/tiff'];
const validExtensions = ['.jpg', '.jpeg', '.png', '.tif', '.tiff'];
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/tiff', 'application/dicom', 'application/octet-stream'];
const validExtensions = ['.jpg', '.jpeg', '.png', '.tif', '.tiff', '.dcm'];

const fileName = file.name.toLowerCase();
const isValidType = validTypes.includes(file.type) ||
validExtensions.some(ext => fileName.endsWith(ext));

if (!isValidType) {
showNotification('Please upload a valid image file (JPG, PNG, TIF, TIFF)', 'error');
showNotification('Please upload a valid image file (JPG, PNG, TIF, TIFF, DCM)', 'error');
return;
}

Expand All @@ -259,9 +259,15 @@ function validateAndPreviewFile(file) {

function previewImage(file) {
const reader = new FileReader();
const isDicom = file.name.toLowerCase().endsWith('.dcm');

reader.onload = (e) => {
elements.previewImage.src = e.target.result;
if (isDicom) {
// Render a target scanner SVG placeholder for DICOM files since raw binary is not directly viewable in browser
elements.previewImage.src = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBmaWxsPSIjMzM0RUFDIj48cGF0aCBkPSJNMjU2IDBDMTE0LjYgMCAwIDExNC42IDAgMjU2czExNC42IDI1NiAyNTYgMjU2IDI1Ni0xMTQuNiAyNTYtMjU2UzM5Ny40IDAgMjU2IDB6bTAgNDY0Yy0xMTQuNyAwLTIwOC05My4zLTIwOC0yMDhTMTQxLjMgNDggMjU2IDQ4czIwOCA5My4zIDIwOCAyMDgtOTMuMyAyMDgtMjA4IDIwOHoiLz48cGF0aCBkPSJNMjU2IDkwYy05MS42IDAtMTY2IDc0LjQtMTY2IDE2NnM3NC40IDE2NiAxNjYgMTY2IDE2Ni03NC40IDE2Ni0xNjZTMzQ3LjYgOTAgMjU2IDkwem0xMTEuMyAxOTUuNWwtODAuNCA0MC4xIDQwLjEgODAuNGMtMTYuNiAxMC4xLTM1LjYgMTUuOS01NSAxNS45LTQ4LjYgMC04OC4xLTM5LjUtODguMS04OC4xczM5LjUtODguMSA4OC4xLTg4LjFjMTkuNCAwIDM4LjQgNS44IDU1IDE1LjlsLTQwLjEgODAuNCA4MC40LTQwLjFjNi43IDExLjggMTAuMiAyNSAxMC4yIDM4LjcgMCAxMy43LTMuNSAyNi45LTEwLjIgMzguN3oiLz48L3N2Zz4=`;
} else {
elements.previewImage.src = e.target.result;
}
elements.uploadArea.style.display = 'none';
elements.imagePreview.style.display = 'block';
setAnalyzeControlsLoading(false);
Expand Down Expand Up @@ -771,6 +777,43 @@ function generateResultsHTML(results) {
</div>
`;

// Render Patient & Scan info if DICOM metadata exists
if (report.patient_info) {
const pInfo = report.patient_info;
html += `
<!-- Patient & Scan Metadata (DICOM) -->
<div class="patient-info-section">
<h4 class="patient-info-title"><i class="fas fa-id-card"></i> Patient & Scan Metadata (DICOM)</h4>
<div class="patient-info-grid">
<div class="patient-info-item">
<span class="info-label">Patient Age</span>
<span class="info-value">${pInfo.patient_age || 'N/A'}</span>
</div>
<div class="patient-info-item">
<span class="info-label">Patient Gender</span>
<span class="info-value">${pInfo.patient_sex || 'N/A'}</span>
</div>
<div class="patient-info-item">
<span class="info-label">Imaging Modality</span>
<span class="info-value">${pInfo.modality || 'N/A'}</span>
</div>
<div class="patient-info-item">
<span class="info-label">Field Strength</span>
<span class="info-value">${pInfo.magnetic_field_strength || 'N/A'}</span>
</div>
<div class="patient-info-item">
<span class="info-label">Scanning Sequence</span>
<span class="info-value">${pInfo.scanning_sequence || 'N/A'}</span>
</div>
<div class="patient-info-item">
<span class="info-label">Manufacturer</span>
<span class="info-value">${pInfo.manufacturer || 'N/A'}</span>
</div>
</div>
</div>
`;
}

if (hasTumor && chars.detected) {
const size = chars.estimated_size || {};

Expand Down
24 changes: 20 additions & 4 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"></noscript>

<!-- Custom CSS (blocking — intentional: sets colours before paint) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}?v=1.0.1">
<!-- Performance optimisation layer — must load after style.css -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/performance.css') }}">
</head>
Expand Down Expand Up @@ -91,8 +91,23 @@
</div>
</nav>
<main id="main-content">

<!-- Dummy Weights Warning Banner -->
{% if dummy_weights %}
<div class="dummy-weights-banner">
<div class="container">
<div class="banner-content">
<i class="fas fa-exclamation-triangle banner-icon"></i>
<div class="banner-text">
<strong>Demo Mode (Untrained Models):</strong> The application is currently using randomly-initialized dummy weights. Predictions will be random (around 50% confidence). To enable real diagnostics, please download the trained weights from the <a href="https://drive.google.com/file/d/1FsuQvdxakt4AYcjA_D06la4fJ2kKxA9E/view?usp=sharing" target="_blank">Google Drive ZIP</a> or <a href="https://drive.google.com/drive/folders/13_EYULCrG8GIAvfSmaWXzFhIvfn_udYh" target="_blank">Google Drive Folder</a> and replace the weights files in the project root.
</div>
</div>
</div>
</div>
{% endif %}

<!-- Hero Section -->

<section id="home" class="hero-section">
<div class="hero-background">
<div class="blob blob-1"></div>
Expand Down Expand Up @@ -967,7 +982,7 @@ <h4>Multi-Format Support</h4>
<span class="chip active">PNG</span>
<span class="chip active">TIFF</span>
<span class="chip active">TIF</span>
<span class="chip coming">DICOM</span>
<span class="chip active">DICOM</span>
</div>
</div>

Expand Down Expand Up @@ -1237,7 +1252,7 @@ <h3>Upload Your MRI Scan</h3>
</div>

<div class="upload-zone-v2" id="uploadArea">
<input type="file" id="fileInput" accept="image/*,.tif,.tiff" hidden>
<input type="file" id="fileInput" accept="image/*,.tif,.tiff,.dcm" hidden>

<div class="upload-visual">
<div class="upload-animation">
Expand Down Expand Up @@ -1268,6 +1283,7 @@ <h4>Drop MRI scan here</h4>
<span class="format-badge"><i class="fas fa-check"></i> PNG</span>
<span class="format-badge"><i class="fas fa-check"></i> TIF</span>
<span class="format-badge"><i class="fas fa-check"></i> TIFF</span>
<span class="format-badge"><i class="fas fa-check"></i> DCM</span>
</div>
</div>

Expand Down Expand Up @@ -3888,7 +3904,7 @@ <h2 class="section-title">Proven Results</h2>
});
</script>
<!-- Custom JavaScript with cache busting -->
<script src="{{ url_for('static', filename='js/script.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/script.js') }}?v=1.0.1" defer></script>

</body>
</html>
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
for key in REQUIRED_ENV_VARS:
os.environ.setdefault(key, "test")

os.environ.setdefault("TESTING", "true")


def _install_tensorflow_stub():
if importlib.util.find_spec("tensorflow") is not None:
Expand Down Expand Up @@ -163,6 +165,7 @@ def app_state(app_module, monkeypatch, tmp_path):
app_module.app.config["SCAN_HISTORY_FOLDER"] = str(history_dir)

app_module.models_loaded = True
app_module.models_load_attempted = True
app_module.classification_models = []
app_module.segmentation_models = []
app_module.memory_storage = {"users": {}, "scan_history": {}}
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/test_dicom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import numpy as np
import pytest
from unittest.mock import MagicMock, patch
import app

def test_process_dicom_file():
# Set up mock dataset
mock_ds = MagicMock()
mock_ds.PatientAge = "045Y"
mock_ds.PatientSex = "M"
mock_ds.Modality = "MR"
mock_ds.MagneticFieldStrength = 3.0
mock_ds.ScanningSequence = "SE"
mock_ds.Manufacturer = "SIEMENS"
mock_ds.StudyDate = "20260712"

# 2D grayscale array of size 64x64
mock_ds.pixel_array = np.full((64, 64), 128, dtype=np.uint16)

with patch("pydicom.dcmread", return_value=mock_ds):
img_bgr, metadata = app.process_dicom_file("mock_file.dcm")

# Verify BGR conversion
assert img_bgr.shape == (64, 64, 3)
assert img_bgr.dtype == np.uint8

# Verify metadata extraction & normalization
assert metadata["patient_age"] == "45"
assert metadata["patient_sex"] == "M"
assert metadata["modality"] == "MR"
assert metadata["magnetic_field_strength"] == "3.0T"
assert metadata["scanning_sequence"] == "SE"
assert metadata["manufacturer"] == "SIEMENS"
assert metadata["study_date"] == "20260712"

def test_process_dicom_file_missing_fields():
# Set up mock dataset with no metadata attributes
# Delete optional properties to test missing field fallbacks
mock_ds = MagicMock()
del mock_ds.PatientAge
del mock_ds.PatientSex
del mock_ds.Modality
del mock_ds.MagneticFieldStrength
del mock_ds.ScanningSequence
del mock_ds.Manufacturer
del mock_ds.StudyDate

mock_ds.pixel_array = np.full((64, 64), 100, dtype=np.uint16)

with patch("pydicom.dcmread", return_value=mock_ds):
img_bgr, metadata = app.process_dicom_file("mock_file.dcm")

# Verify it falls back to 'N/A' safely without throwing AttributeErrors
assert metadata["patient_age"] == "N/A"
assert metadata["patient_sex"] == "N/A"
assert metadata["modality"] == "N/A"
assert metadata["magnetic_field_strength"] == "N/A"
assert metadata["scanning_sequence"] == "N/A"
assert metadata["manufacturer"] == "N/A"
assert metadata["study_date"] == "N/A"