Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

MA-2 Upgrade to PySide 6 #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,4 @@ ENV/
data/
log.txt
.idea/
dist
Binary file removed dist/Theia
Binary file not shown.
25 changes: 0 additions & 25 deletions dist/Theia.app/Contents/Info.plist

This file was deleted.

Binary file removed dist/Theia.app/Contents/MacOS/Theia
Binary file not shown.
Binary file removed dist/Theia.exe
Binary file not shown.
28 changes: 17 additions & 11 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@

<img src="https://github.com/adamkwolf/3d-nii-visualizer/blob/master/images/visualization.png" style="width: 100px;"/>

### Run with Python
## Install & Run with Python

1. Create a virtual environment. Mac can use virtualenv or conda. Windows must use conda.
2. Install the dependencies (PyQt5, vtk, and sip) `pip install PyQt5 vtk`
3. Start the program `python ./visualizer/brain_tumor_3d.py -i "./sample_data/10labels_example/T1CE.nii.gz" -m "./sample_data/10labels_example/mask.nii.gz"`
1. Install the dependencies (PySide6, VTK, and sip) `pip install -r requirements.txt`
2. Start the program `python ./visualizer/brain_tumor_3d.py -i "data.nii.gz" -m "data_mask.nii.gz"`

## Generate PyInstaller Binaries

> **Note**: Must modify the paths in .spec file to match your project directory

### Generate PyInstaller Binaries
**Note**: Must modify the paths in .spec file to match your project directory
* Mac: `pyinstaller Theia_Mac.spec`
* Windows: `pyinstaller Theia_Windows.spec`

### Test
* `python -m pytest`
## Test

```shell
python -m pytest
```

### Acknowledgements
## Acknowledgements

[1] S.Bakas et al, "Advancing The Cancer Genome Atlas glioma MRI collections with expert segmentation labels and radiomic features", Nature Scientific Data, 4:170117 (2017) DOI: 10.1038/sdata.2017.117
[1] S.Bakas et al, "Advancing The Cancer Genome Atlas glioma MRI collections with expert segmentation labels and radiomic features", Nature Scientific Data, 4:
170117 (2017) DOI: 10.1038/sdata.2017.117

[2] B.Menze et al, "The Multimodal Brain Tumor Image Segmentation Benchmark (BRATS)", IEEE Transactions on Medical Imaging 34(10), 1993-2024 (2015) DOI: 10.1109/TMI.2014.2377694
[2] B.Menze et al, "The Multimodal Brain Tumor Image Segmentation Benchmark (BRATS)", IEEE Transactions on Medical Imaging 34(10), 1993-2024 (2015) DOI:
10.1109/TMI.2014.2377694
7 changes: 4 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PyInstaller==3.3.1
PyQt5==5.10.1
vtk==8.1.0
PyInstaller>=6.9.0
pyside6>=6.7.2
vtk>=9.3.1
qt-material
2 changes: 1 addition & 1 deletion visualizer/ErrorObserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ def ErrorOccurred(self):
return occ

def ErrorMessage(self):
return self.__ErrorMessage
return self.__ErrorMessage
84 changes: 54 additions & 30 deletions visualizer/MainWindow.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import math
import time
import os
import time

import PySide6.QtCore as Qt
import PySide6.QtWidgets as QtWidgets
import vtk
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor

import PyQt5.QtWidgets as QtWidgets
import PyQt5.QtCore as Qt
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from vtkUtils import *
from config import *
from visualizer.config import BRAIN_OPACITY, BRAIN_SMOOTHNESS, MASK_OPACITY, MASK_SMOOTHNESS, APPLICATION_TITLE, MASK_COLORS
from visualizer.vtkUtils import setup_brain, setup_mask, setup_projection, setup_slicer


class MainWindow(QtWidgets.QMainWindow, QtWidgets.QApplication):
Expand All @@ -21,21 +23,27 @@ def __init__(self, app):

# setup brain projection and slicer
self.brain_image_prop = setup_projection(self.brain, self.renderer)
self.brain_slicer_props = setup_slicer(self.renderer, self.brain) # causing issues with rotation
self.brain_slicer_props = setup_slicer(
self.renderer, self.brain) # causing issues with rotation
self.slicer_widgets = []

# brain pickers
self.brain_threshold_sp = self.create_new_picker(self.brain.scalar_range[1], self.brain.scalar_range[0], 5.0,
sum(self.brain.scalar_range) / 2, self.brain_threshold_vc)
self.brain_opacity_sp = self.create_new_picker(1.0, 0.0, 0.1, BRAIN_OPACITY, self.brain_opacity_vc)
self.brain_smoothness_sp = self.create_new_picker(1000, 100, 100, BRAIN_SMOOTHNESS, self.brain_smoothness_vc)
self.brain_lut_sp = self.create_new_picker(3.0, 0.0, 0.1, 2.0, self.lut_value_changed)
self.brain_opacity_sp = self.create_new_picker(
1.0, 0.0, 0.1, BRAIN_OPACITY, self.brain_opacity_vc)
self.brain_smoothness_sp = self.create_new_picker(
1000, 100, 100, BRAIN_SMOOTHNESS, self.brain_smoothness_vc)
self.brain_lut_sp = self.create_new_picker(
3.0, 0.0, 0.1, 2.0, self.lut_value_changed)
self.brain_projection_cb = self.add_brain_projection()
self.brain_slicer_cb = self.add_brain_slicer()

# mask pickers
self.mask_opacity_sp = self.create_new_picker(1.0, 0.0, 0.1, MASK_OPACITY, self.mask_opacity_vc)
self.mask_smoothness_sp = self.create_new_picker(1000, 100, 100, MASK_SMOOTHNESS, self.mask_smoothness_vc)
self.mask_opacity_sp = self.create_new_picker(
1.0, 0.0, 0.1, MASK_OPACITY, self.mask_opacity_vc)
self.mask_smoothness_sp = self.create_new_picker(
1000, 100, 100, MASK_SMOOTHNESS, self.mask_smoothness_vc)
self.mask_label_cbs = []

# create grid for all widgets
Expand All @@ -59,7 +67,7 @@ def __init__(self, app):
@staticmethod
def setup():
"""
Create and setup the base vtk and Qt objects for the application
Create and set up the base vtk and Qt objects for the application
"""
renderer = vtk.vtkRenderer()
frame = QtWidgets.QFrame()
Expand Down Expand Up @@ -116,7 +124,8 @@ def add_brain_settings_widget(self):
brain_group_layout = QtWidgets.QGridLayout()
brain_group_layout.addWidget(QtWidgets.QLabel("Brain Threshold"), 0, 0)
brain_group_layout.addWidget(QtWidgets.QLabel("Brain Opacity"), 1, 0)
brain_group_layout.addWidget(QtWidgets.QLabel("Brain Smoothness"), 2, 0)
brain_group_layout.addWidget(
QtWidgets.QLabel("Brain Smoothness"), 2, 0)
brain_group_layout.addWidget(QtWidgets.QLabel("Image Intensity"), 3, 0)
brain_group_layout.addWidget(self.brain_threshold_sp, 0, 1, 1, 2)
brain_group_layout.addWidget(self.brain_opacity_sp, 1, 1, 1, 2)
Expand All @@ -130,7 +139,8 @@ def add_brain_settings_widget(self):
brain_group_layout.addWidget(QtWidgets.QLabel("Sagittal Slice"), 8, 0)

# order is important
slicer_funcs = [self.axial_slice_changed, self.coronal_slice_changed, self.sagittal_slice_changed]
slicer_funcs = [self.axial_slice_changed,
self.coronal_slice_changed, self.sagittal_slice_changed]
current_label_row = 6
# data extent is array [xmin, xmax, ymin, ymax, zmin, zmax)
# we want all the max values for the range
Expand All @@ -139,10 +149,12 @@ def add_brain_settings_widget(self):
slice_widget = QtWidgets.QSlider(Qt.Qt.Horizontal)
slice_widget.setDisabled(True)
self.slicer_widgets.append(slice_widget)
brain_group_layout.addWidget(slice_widget, current_label_row, 1, 1, 2)
brain_group_layout.addWidget(
slice_widget, current_label_row, 1, 1, 2)
slice_widget.valueChanged.connect(func)
slice_widget.setRange(self.brain.extent[extent_index - 1], self.brain.extent[extent_index])
slice_widget.setValue(self.brain.extent[extent_index] / 2)
slice_widget.setRange(
self.brain.extent[extent_index - 1], self.brain.extent[extent_index])
slice_widget.setValue(int(self.brain.extent[extent_index] / 2))
current_label_row += 1
extent_index -= 2

Expand Down Expand Up @@ -171,23 +183,28 @@ def add_mask_settings_widget(self):
mask_settings_group_box = QtWidgets.QGroupBox("Mask Settings")
mask_settings_layout = QtWidgets.QGridLayout()
mask_settings_layout.addWidget(QtWidgets.QLabel("Mask Opacity"), 0, 0)
mask_settings_layout.addWidget(QtWidgets.QLabel("Mask Smoothness"), 1, 0)
mask_settings_layout.addWidget(
QtWidgets.QLabel("Mask Smoothness"), 1, 0)
mask_settings_layout.addWidget(self.mask_opacity_sp, 0, 1)
mask_settings_layout.addWidget(self.mask_smoothness_sp, 1, 1)
mask_multi_color_radio = QtWidgets.QRadioButton("Multi Color")
mask_multi_color_radio.setChecked(True)
mask_multi_color_radio.clicked.connect(self.mask_multi_color_radio_checked)
mask_multi_color_radio.clicked.connect(
self.mask_multi_color_radio_checked)
mask_single_color_radio = QtWidgets.QRadioButton("Single Color")
mask_single_color_radio.clicked.connect(self.mask_single_color_radio_checked)
mask_single_color_radio.clicked.connect(
self.mask_single_color_radio_checked)
mask_settings_layout.addWidget(mask_multi_color_radio, 2, 0)
mask_settings_layout.addWidget(mask_single_color_radio, 2, 1)
mask_settings_layout.addWidget(self.create_new_separator(), 3, 0, 1, 2)

self.mask_label_cbs = []
c_col, c_row = 0, 4 # c_row must always be (+1) of last row
for i in range(1, 11):
self.mask_label_cbs.append(QtWidgets.QCheckBox("Label {}".format(i)))
mask_settings_layout.addWidget(self.mask_label_cbs[i - 1], c_row, c_col)
self.mask_label_cbs.append(
QtWidgets.QCheckBox("Label {}".format(i)))
mask_settings_layout.addWidget(
self.mask_label_cbs[i - 1], c_row, c_col)
c_row = c_row + 1 if c_col == 1 else c_row
c_col = 0 if c_col == 1 else 1

Expand Down Expand Up @@ -238,7 +255,8 @@ def add_brain_projection(self):
def mask_label_checked(self):
for i, cb in enumerate(self.mask_label_cbs):
if cb.isChecked():
self.mask.labels[i].property.SetOpacity(self.mask_opacity_sp.value())
self.mask.labels[i].property.SetOpacity(
self.mask_opacity_sp.value())
elif cb.isEnabled(): # labels without data are disabled
self.mask.labels[i].property.SetOpacity(0)
self.render_window.Render()
Expand All @@ -257,7 +275,8 @@ def mask_multi_color_radio_checked(self):

def brain_projection_vc(self):
projection_checked = self.brain_projection_cb.isChecked()
self.brain_slicer_cb.setDisabled(projection_checked) # disable slicer checkbox, cant use both at same time
# disable slicer checkbox, cant use both at same time
self.brain_slicer_cb.setDisabled(projection_checked)
self.brain_image_prop.SetOpacity(projection_checked)
self.render_window.Render()

Expand All @@ -267,7 +286,8 @@ def brain_slicer_vc(self):
for widget in self.slicer_widgets:
widget.setEnabled(slicer_checked)

self.brain_projection_cb.setDisabled(slicer_checked) # disable projection checkbox, cant use both at same time
# disable projection checkbox, cant use both at same time
self.brain_projection_cb.setDisabled(slicer_checked)
for prop in self.brain_slicer_props:
prop.GetProperty().SetOpacity(slicer_checked)
self.render_window.Render()
Expand Down Expand Up @@ -308,7 +328,8 @@ def set_axial_view(self):
self.renderer.ResetCamera()
fp = self.renderer.GetActiveCamera().GetFocalPoint()
p = self.renderer.GetActiveCamera().GetPosition()
dist = math.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1]) ** 2 + (p[2] - fp[2]) ** 2)
dist = math.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1])
** 2 + (p[2] - fp[2]) ** 2)
self.renderer.GetActiveCamera().SetPosition(fp[0], fp[1], fp[2] + dist)
self.renderer.GetActiveCamera().SetViewUp(0.0, 1.0, 0.0)
self.renderer.GetActiveCamera().Zoom(1.8)
Expand All @@ -318,7 +339,8 @@ def set_coronal_view(self):
self.renderer.ResetCamera()
fp = self.renderer.GetActiveCamera().GetFocalPoint()
p = self.renderer.GetActiveCamera().GetPosition()
dist = math.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1]) ** 2 + (p[2] - fp[2]) ** 2)
dist = math.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1])
** 2 + (p[2] - fp[2]) ** 2)
self.renderer.GetActiveCamera().SetPosition(fp[0], fp[2] - dist, fp[1])
self.renderer.GetActiveCamera().SetViewUp(0.0, 0.5, 0.5)
self.renderer.GetActiveCamera().Zoom(1.8)
Expand All @@ -328,7 +350,8 @@ def set_sagittal_view(self):
self.renderer.ResetCamera()
fp = self.renderer.GetActiveCamera().GetFocalPoint()
p = self.renderer.GetActiveCamera().GetPosition()
dist = math.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1]) ** 2 + (p[2] - fp[2]) ** 2)
dist = math.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1])
** 2 + (p[2] - fp[2]) ** 2)
self.renderer.GetActiveCamera().SetPosition(fp[2] + dist, fp[0], fp[1])
self.renderer.GetActiveCamera().SetViewUp(0.0, 0.0, 1.0)
self.renderer.GetActiveCamera().Zoom(1.6)
Expand All @@ -338,7 +361,8 @@ def set_sagittal_view(self):
def create_new_separator():
horizontal_line = QtWidgets.QWidget()
horizontal_line.setFixedHeight(1)
horizontal_line.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
horizontal_line.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
horizontal_line.setStyleSheet("background-color: #c8c8c8;")
return horizontal_line

Expand Down
2 changes: 1 addition & 1 deletion visualizer/NiiLabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ def __init__(self, color, opacity, smoothness):
self.smoother = None
self.color = color
self.opacity = opacity
self.smoothness = smoothness
self.smoothness = smoothness
24 changes: 17 additions & 7 deletions visualizer/brain_tumor_3d.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import argparse
import sys
import os
import sys

from MainWindow import *
import vtk
from PySide6 import QtWidgets
from qt_material import apply_stylesheet

from visualizer.MainWindow import MainWindow


def redirect_vtk_messages():
Expand All @@ -19,14 +23,18 @@ def redirect_vtk_messages():
def verify_type(file):
ext = os.path.basename(file).split(os.extsep, 1)
if ext[1] != 'nii.gz':
parser.error("File doesn't end with 'nii.gz'. Found: {}".format(ext[1]))
parser.error(
"File doesn't end with 'nii.gz'. Found: {}".format(ext[1]))
return file


if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Reads Nii.gz Files and renders them in 3D.')
parser.add_argument('-i', type=lambda fn: verify_type(fn), help='an mri scan (nii.gz)')
parser.add_argument('-m', type=lambda fn: verify_type(fn), help='the segmentation mask (nii.gz)')
parser = argparse.ArgumentParser(
description='Reads Nii.gz Files and renders them in 3D.')
parser.add_argument('-i', type=lambda fn: verify_type(fn),
help='an mri scan (nii.gz)')
parser.add_argument('-m', type=lambda fn: verify_type(fn),
help='the segmentation mask (nii.gz)')
args = parser.parse_args()

redirect_vtk_messages()
Expand All @@ -36,7 +44,9 @@ def verify_type(file):
# read_css = css.read()
# app.setStyleSheet(read_css)

apply_stylesheet(app, theme='light_cyan_500.xml')

app.BRAIN_FILE = args.i
app.MASK_FILE = args.m
window = MainWindow(app)
sys.exit(app.exec_())
sys.exit(app.exec())
16 changes: 8 additions & 8 deletions visualizer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
# default mask settings
MASK_SMOOTHNESS = 500
MASK_COLORS = [(1, 0, 0),
(0, 1, 0),
(1, 1, 0),
(0, 0, 1),
(1, 0, 1),
(0, 1, 1),
(1, 0.5, 0.5),
(0.5, 1, 0.5),
(0.5, 0.5, 1)] # RGB percentages
(0, 1, 0),
(1, 1, 0),
(0, 0, 1),
(1, 0, 1),
(0, 1, 1),
(1, 0.5, 0.5),
(0.5, 1, 0.5),
(0.5, 0.5, 1)] # RGB percentages
MASK_OPACITY = 1.0
Loading