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
48 changes: 35 additions & 13 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
# Python
__pycache__/
*.pyc
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual Environment
.venv/
env/
venv/
.env
env/

# IDEs
.idea/
.vscode/
*.log
dist/
build/
*.egg-info/
.DS_Store
model_output/

# Project Specific
*.pth
*.pt
*.h5
*.ckpt
*.onnx
model_output/
.gemini/
.agent/
brain/
scratch/

# Logs
*.log
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# NeuroSeg-Pro: MRI Segmentation & Visualization Tool

NeuroSeg-Pro is a PyQt5-based application designed for visualizing and segmenting brain MRI scans using deep learning models. It provides a rich interface for comparing model outputs, ground truth, and difference maps.

## 🚀 Key Features

### Visualization
- **Multi-Planar Reconstruction (MPR):** View Axial, Sagittal, and Coronal planes simultaneously.
- **3D Viewport:** Interactive 3D visualization of segmentation masks (if enabled).
- **Advanced Tools:**
- **Grid Overlay:** Helper grid for alignment.
- **Crosshair:** Navigate specific coordinates across all views.
- **MRI Visibility Toggle:** Focus on the mask by hiding the background MRI.

### AI Analysis
- **Model Selection:** Choose between different pre-trained models (e.g., Teacher/Student variants).
- **Comparison Modes:**
- **Model A vs Model B:** Side-by-side comparison.
- **Model A vs Ground Truth:** Validate predictions against expert annotations.
- **Overlay vs Raw:** Compare active segmentation with the raw scan.
- **Metrics:** Real-time calculation of Dice Coefficient, Sensitivity, Specificity, and Hausdorff Distance.

### User Interface
- **Modern Dark Theme:** "Deep Space Medical" theme for comfortable viewing.
- **Dynamic Legend:** Legend updates automatically based on the classes present in the current view.
- **One-Click Export:** Save segmentation masks as `.nii.gz` files.

---

## 🛠️ Installation & Setup

### Prerequisites
- Python 3.8+
- [Git](https://git-scm.com/)

### 1. Clone the Repository
```bash
git clone https://github.com/yourusername/NeuroSeg-Pro.git
cd NeuroSeg-Pro
```

### 2. Create a Virtual Environment (Recommended)
```bash
# Windows
python -m venv .venv
.venv\Scripts\activate

# Linux/Mac
python3 -m venv .venv
source .venv/bin/activate
```

### 3. Install Dependencies
```bash
pip install -r requirements.txt
```

### 4. Setup Models
> **Note:** Due to file size limits, pre-trained model weights (`.pth` files) are NOT included in this repository.
> Please place your model files in the `models/` directory.

Example structure:
```
NeuroSeg-Pro/
├── app/
├── models/
│ ├── Teacher_model_50.pth
│ └── ...
├── assets/
└── ...
```

---

## ▶️ Running the Application

To launch the main application interface:

```bash
python -m app.main
```

---

## 📂 Project Structure

- `app/` - Source code.
- `main.py` - Entry point.
- `ui/` - Interface widgets and themes.
- `core/` - Logic for inference and image processing.
- `assets/` - Icons and resources.
- `models/` - Directory for model weights (ignored by git).
- `model_output/` - Saved segmentation masks.

---

## 🤝 Contributing
Contributions are welcome! Please open an issue or submit a pull request for any improvements.

## 📄 License
MIT License
11 changes: 11 additions & 0 deletions app/core/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class InferenceEngine:
def __init__(self, model_path: str = None, device: str = None):
self.device = device if device else ("cuda" if torch.cuda.is_available() else "cpu")
self.model = None
self.current_model_path = None
if model_path:
self.load_model(model_path)

Expand Down Expand Up @@ -62,6 +63,7 @@ def load_model(self, model_path: str):
self.model.load_state_dict(new_state_dict, strict=False)

self.model.eval()
self.current_model_path = model_path
print(f"Model loaded from {model_path}")
except Exception as e:
print(f"Error loading model: {e}")
Expand Down Expand Up @@ -104,3 +106,12 @@ def predict(self, input_data: np.ndarray):
outputs = torch.nn.functional.interpolate(outputs, size=original_shape, mode='trilinear', align_corners=False)

return torch.argmax(outputs, dim=1).detach().cpu().numpy()[0] # Remove batch dim

def run_inference(self, volume: np.ndarray, model_path: str):
"""
Loads the model if necessary and runs inference.
"""
if self.current_model_path != model_path:
self.load_model(model_path)

return self.predict(volume)
33 changes: 28 additions & 5 deletions app/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.settings = Settings()
self.sidebar_expanded_width = 260
self.sidebar_collapsed_width = 76
self.sidebar_collapsed = bool(self.settings.get("sidebar_collapsed") or False)
self.setWindowTitle("Brain Tumor Segmentation Pro")
self.resize(1280, 850) # Slightly larger default

Expand Down Expand Up @@ -96,13 +99,30 @@ def setup_header(self):
self.right_layout.addWidget(self.header_frame)

def toggle_sidebar(self):
if self.sidebar.isVisible():
self.sidebar.hide()
self.sidebar_collapsed = not self.sidebar_collapsed
self.settings.set("sidebar_collapsed", self.sidebar_collapsed)
self.apply_sidebar_state()

def apply_sidebar_state(self):
if self.sidebar_collapsed:
self.sidebar.setFixedWidth(self.sidebar_collapsed_width)
self.title_label.setVisible(False)
self.btn_toggle_sidebar.setText("☷")
for btn in [self.btn_dashboard, self.btn_viewer, self.btn_help, self.btn_settings]:
btn.setText("")
btn.setToolTip(btn.property("full_text"))
else:
self.sidebar.show()
self.sidebar.setFixedWidth(self.sidebar_expanded_width)
self.title_label.setVisible(True)
self.btn_toggle_sidebar.setText("☰")
for btn in [self.btn_dashboard, self.btn_viewer, self.btn_help, self.btn_settings]:
btn.setText(btn.property("full_text"))
btn.setToolTip("")

def on_recent_file_clicked(self, file_path):
self.on_file_loaded(file_path, 'mri') # Default to MRI
import os
file_type = 'folder' if os.path.isdir(file_path) else 'mri'
self.on_file_loaded(file_path, file_type)

def on_file_loaded(self, file_path, file_type):
print(f"Loading {file_type}: {file_path}")
Expand Down Expand Up @@ -152,7 +172,7 @@ def refresh_theme(self):
def setup_sidebar(self):
self.sidebar = QFrame()
self.sidebar.setObjectName("Sidebar")
self.sidebar.setFixedWidth(260)
self.sidebar.setFixedWidth(self.sidebar_expanded_width)

self.sidebar_layout = QVBoxLayout(self.sidebar)
self.sidebar_layout.setContentsMargins(20, 40, 20, 20)
Expand Down Expand Up @@ -190,13 +210,16 @@ def setup_sidebar(self):
self.btn_settings.clicked.connect(lambda: self.switch_page(2))
self.btn_help.clicked.connect(self.show_tutorial)

self.apply_sidebar_state()

def show_tutorial(self):
from app.ui.tutorial_dialog import TutorialDialog
dlg = TutorialDialog(self)
dlg.exec_()

def create_nav_button(self, text, icon_name=None):
btn = QPushButton(text)
btn.setProperty("full_text", text)
btn.setCheckable(True)
btn.setFixedHeight(50)
btn.setCursor(Qt.PointingHandCursor)
Expand Down
40 changes: 32 additions & 8 deletions app/ui/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,28 @@ def apply_theme(app: QApplication):
border-radius: 8px;
}}

/* GroupBox - Modern & Minimal */
/* GroupBox - Modern & Block-like */
/* GroupBox - Modern & Block-like */
QGroupBox {{
background-color: {c["SURFACE"]};
border: 1px solid {c["BORDER"]};
border-radius: 12px;
margin-top: 24px;
padding-top: 10px;
border-radius: 10px;
margin-top: 20px; /* Leave space for title */
padding-top: 15px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 5px;
left: 10px;
bottom: 0px; /* Overlap border */
color: {c["ACCENT"]};
font-weight: bold;
font-size: {base_font_size + 1}px;
font-size: {base_font_size + 2}px;
background-color: {c["BACKGROUND"]}; /* Mask border behind title */
}}

/* Buttons */
Expand Down Expand Up @@ -327,8 +333,26 @@ def apply_theme(app: QApplication):
border: 1px solid {c["PRIMARY"]};
image: url(assets/icons/check.svg);
}}
/* Fallback check */
QCheckBox::indicator:checked {{
background-color: {c["PRIMARY"]};
/* Table Widget */
QTableWidget {{
background-color: {c["SURFACE"]};
border: 1px solid {c["BORDER"]};
border-radius: 8px;
gridline-color: {c["BORDER"]};
}}
QHeaderView::section {{
background-color: {c["SURFACE_LIGHT"]};
padding: 4px;
border: none;
border-bottom: 2px solid {c["BORDER"]};
color: {c["TEXT_SECONDARY"]};
font-weight: bold;
}}
QTableWidget::item {{
padding: 5px;
}}
QTableWidget::item:selected {{
background-color: {c["PRIMARY"]}30;
color: {c["TEXT_PRIMARY"]};
}}
""")
Loading