diff --git a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py new file mode 100644 index 00000000..05e717de --- /dev/null +++ b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py @@ -0,0 +1,111 @@ +from PySide2 import QtWidgets + +from ccpi.viewer.ui.SettingsDialog import SettingsDialog +from ccpi.viewer.ui.VolumeRenderSettingsDialog import VolumeRenderSettingsDialog + + +class QCILViewer3DToolBar(QtWidgets.QToolBar): + def __init__(self, parent=None, viewer=None, **kwargs): + """ + Parameters + ----------- + viewer: an instance of viewer2D or viewer3D + the viewer which the toolbar is for. The viewer instance + is passed to allow interactions to be controlled using the + toolbar. + + """ + self.parent = parent + self.viewer = viewer + super(QCILViewer3DToolBar, self).__init__(parent=parent, **kwargs) + self.dialog = {"settings": None, "volume_render_settings": None} + + # Settings button + settings_2d = QtWidgets.QToolButton() + settings_2d.setText("Settings ⚙️") + self.addWidget(settings_2d) + settings_2d.clicked.connect(lambda: self.open_dialog("settings")) + + # Volume render settings button + settings_3d = QtWidgets.QToolButton() + settings_3d.setText("Volume Render Settings ⚙️") + self.addWidget(settings_3d) + settings_3d.clicked.connect(lambda: self.open_dialog("volume_render_settings")) + + # Reset camera button + settings_reset = QtWidgets.QToolButton() + settings_reset.setText("Reset ⚙️") + self.addWidget(settings_reset) + + # Reset settings button + reset_camera_btn = QtWidgets.QToolButton() + reset_camera_btn.setText("Reset 📷") + self.addWidget(reset_camera_btn) + reset_camera_btn.clicked.connect(self.reset_camera) + + # Save image button + save_image = QtWidgets.QToolButton() + save_image.setText("Save 💾") + self.addWidget(save_image) + save_image.clicked.connect(self.save_render) + + def reset_camera(self): + """Reset camera to default position.""" + self.viewer.resetCameraToDefault() + self.viewer.updatePipeline() + + def change_orientation(self, dialog): + """Change orientation of the viewer.""" + orientation = 1 + self.viewer.style.SetSliceOrientation(orientation) + self.viewer.updatePipeline() + + def open_dialog(self, mode): + """Open a dialog box for the settings of the viewer.""" + # pylint(access-member-before-definition) + if mode == "settings": + if self.dialog["settings"] is None: + dialog = SettingsDialog(parent=self.parent, title="Settings") + dialog.Ok.clicked.connect(lambda: self.accepted(mode)) + dialog.Cancel.clicked.connect(lambda: self.rejected(mode)) + dialog.set_viewer(self.viewer) + self.dialog[mode] = dialog + # self.default_settings = self.dialog[mode].get_settings() + + self.settings = self.dialog[mode].get_settings() + self.dialog[mode].open() + return + + if mode == "volume_render_settings": + if self.dialog["volume_render_settings"] is None: + dialog = VolumeRenderSettingsDialog(parent=self.parent, title="Volume Render Settings") + dialog.Ok.clicked.connect(lambda: self.accepted(mode)) + dialog.Cancel.clicked.connect(lambda: self.rejected(mode)) + dialog.set_viewer(self.viewer) + self.dialog[mode] = dialog + + self.settings = self.dialog[mode].get_settings() + self.dialog[mode].open() + return + + def accepted(self, mode): + """Extract settings and apply them.""" + self.dialog[mode].close() + + def rejected(self, mode): + """Reapply previous settings.""" + self.dialog[mode].apply_settings(self.settings) + self.dialog[mode].close() + + def save_render(self): + """Save the render to a file.""" + if self.dialog.get("settings") is None: + self.viewer.saveRender("render") + else: + self.viewer.saveRender(self.dialog.get("settings").file_location) + + def save_dialog_settings(self): + pass + + def apply_dialog_settings(self, mode, settings): + pass diff --git a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py index 7b745c73..13ae6f8f 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py @@ -1,75 +1,88 @@ -from PySide2 import QtWidgets import vtk import sys import vtk from PySide2 import QtCore, QtWidgets from ccpi.viewer.QCILRenderWindowInteractor import QCILRenderWindowInteractor -from ccpi.viewer import viewer2D +from ccpi.viewer import viewer2D, viewer3D +from ccpi.viewer.QCILViewer3DToolBar import QCILViewer3DToolBar class QCILViewerWidget(QtWidgets.QFrame): - '''A QFrame to embed in Qt application containing a VTK Render Window - + """A QFrame to embed in Qt application containing a VTK Render Window + All the interaction is passed from Qt to VTK. :param viewer: The viewer you want to embed in Qt: CILViewer2D or CILViewer - :param interactorStyle: The interactor style for the Viewer. - ''' + :param interactorStyle: The interactor style for the Viewer. + """ def __init__(self, parent=None, **kwargs): - '''Creator. Creates an instance of a QFrame and of a CILViewer - - The viewer is placed in the QFrame inside a QVBoxLayout. + """Creator. Creates an instance of a QFrame and of a CILViewer + + The viewer is placed in the QFrame inside a QVBoxLayout. The viewer is accessible as member 'viewer' - ''' + """ super(QCILViewerWidget, self).__init__(parent=parent) # currently the size of the frame is set by stretching to the whole # area in the main window. A resize of the MainWindow triggers a resize of # the QFrame to occupy the whole area available. - dimx, dimy = kwargs.get('shape', (600, 600)) + dimx, dimy = kwargs.get("shape", (600, 600)) # self.resize(dimx, dimy) - self.vl = QtWidgets.QVBoxLayout() - #self.vtkWidget = QVTKRenderWindowInteractor(self) self.vtkWidget = QCILRenderWindowInteractor(self) - self.vl.addWidget(self.vtkWidget) - if 'renderer' in kwargs.keys(): - self.ren = kwargs['renderer'] + if "renderer" in kwargs.keys(): + self.ren = kwargs["renderer"] else: self.ren = vtk.vtkRenderer() self.vtkWidget.GetRenderWindow().AddRenderer(self.ren) # https://discourse.vtk.org/t/qvtkwidget-render-window-is-outside-main-qt-app-window/1539/8?u=edoardo_pasca self.iren = self.vtkWidget.GetRenderWindow().GetInteractor() + try: # print ("provided viewer class ", kwargs['viewer']) - self.viewer = kwargs['viewer'](renWin=self.vtkWidget.GetRenderWindow(), - iren=self.iren, - ren=self.ren, - dimx=dimx, - dimy=dimy, - debug=kwargs.get('debug', False)) + self.viewer = kwargs["viewer"]( + renWin=self.vtkWidget.GetRenderWindow(), + iren=self.iren, + ren=self.ren, + dimx=dimx, + dimy=dimy, + debug=kwargs.get("debug", False), + ) except KeyError: - raise KeyError("Viewer class not provided. Submit an uninstantiated viewer class object" - "using 'viewer' keyword") + raise KeyError( + "Viewer class not provided. Submit an uninstantiated viewer class object" "using 'viewer' keyword" + ) - if 'interactorStyle' in kwargs.keys(): - self.viewer.style = kwargs['interactorStyle'](self.viewer) + if "interactorStyle" in kwargs.keys(): + self.viewer.style = kwargs["interactorStyle"](self.viewer) self.viewer.iren.SetInteractorStyle(self.viewer.style) + self.vl = QtWidgets.QVBoxLayout() + + toolBar = self.getToolbar(parent) + if toolBar is not None: + self.vl.addWidget(toolBar) + self.vl.addWidget(self.vtkWidget) + self.setLayout(self.vl) self.adjustSize() + def getToolbar(self, parent=None): + # Adds a toolbar to the QFrame if we have a 3D viewer + if isinstance(self.viewer, viewer3D): + toolBar = QCILViewer3DToolBar(viewer=self.viewer, parent=parent) + return toolBar -class QCILDockableWidget(QtWidgets.QDockWidget): +class QCILDockableWidget(QtWidgets.QDockWidget): def __init__(self, parent=None, **kwargs): - viewer = kwargs.get('viewer', viewer2D) - shape = kwargs.get('shape', (600, 600)) - title = kwargs.get('title', "3D View") + viewer = kwargs.get("viewer", viewer2D) + shape = kwargs.get("shape", (600, 600)) + title = kwargs.get("title", "3D View") super(QCILDockableWidget, self).__init__(parent) diff --git a/Wrappers/Python/ccpi/viewer/iviewer.py b/Wrappers/Python/ccpi/viewer/iviewer.py index c0ed8b52..cabbd6c9 100644 --- a/Wrappers/Python/ccpi/viewer/iviewer.py +++ b/Wrappers/Python/ccpi/viewer/iviewer.py @@ -1,6 +1,6 @@ import sys import vtk -from PySide2 import QtCore, QtWidgets +from PySide2 import QtWidgets from ccpi.viewer import viewer2D, viewer3D from ccpi.viewer.QCILViewerWidget import QCILViewerWidget import ccpi.viewer.viewerLinker as vlink @@ -10,14 +10,13 @@ class SingleViewerCenterWidget(QtWidgets.QMainWindow): - def __init__(self, parent=None, viewer=viewer2D): QtWidgets.QMainWindow.__init__(self, parent) self.frame = QCILViewerWidget(viewer=viewer, shape=(600, 600)) if viewer == viewer3D: - self.frame.viewer.setVolumeRenderOpacityMethod('scalar') + self.frame.viewer.setVolumeRenderOpacityMethod("scalar") self.setCentralWidget(self.frame) @@ -28,19 +27,18 @@ def set_input(self, data): class TwoLinkedViewersCenterWidget(QtWidgets.QMainWindow): - - def __init__(self, parent=None, viewer1='2D', viewer2='2D'): + def __init__(self, parent=None, viewer1="2D", viewer2="2D"): QtWidgets.QMainWindow.__init__(self, parent) - #self.resize(800,600) + # self.resize(800,600) styles = [] viewers = [] for viewer in [viewer1, viewer2]: - if viewer == '2D': + if viewer == "2D": styles.append(vlink.Linked2DInteractorStyle) - elif viewer == '3D': + elif viewer == "3D": styles.append(vlink.Linked3DInteractorStyle) - viewers.append(eval('viewer' + viewer)) + viewers.append(eval("viewer" + viewer)) self.frame1 = QCILViewerWidget(viewer=viewers[0], shape=(600, 600), interactorStyle=styles[0]) self.frame2 = QCILViewerWidget(viewer=viewers[1], shape=(600, 600), interactorStyle=styles[1]) @@ -74,7 +72,7 @@ def set_input(self, data1, data2): class iviewer(object): - ''' + """ a Qt interactive viewer that can be used as plotter2D with one single dataset Parameters ---------- @@ -86,11 +84,11 @@ class iviewer(object): the type of viewer to display the first image on viewer2: string - '2D' or '3D', optional the type of viewer to display the second image on (if present) - - ''' + + """ def __init__(self, data, *moredata, **kwargs): - '''Creator''' + """Creator""" app = QtWidgets.QApplication(sys.argv) self.app = app @@ -101,16 +99,16 @@ def setUp(self, data, *moredata, **kwargs): if len(moredata) == 0: # can change the behaviour by setting which viewer you want # between viewer2D and viewer3D - viewer_type = kwargs.get('viewer1', '2D') - if viewer_type == '2D': + viewer_type = kwargs.get("viewer1", "2D") + if viewer_type == "2D": viewer = viewer2D - elif viewer_type == '3D': + elif viewer_type == "3D": viewer = viewer3D window = SingleViewerCenterWidget(viewer=viewer) window.set_input(self.convert_to_vtkImage(data)) else: - viewer1 = kwargs.get('viewer1', '2D') - viewer2 = kwargs.get('viewer2', '2D') + viewer1 = kwargs.get("viewer1", "2D") + viewer2 = kwargs.get("viewer2", "2D") window = TwoLinkedViewersCenterWidget(viewer1=viewer1, viewer2=viewer2) window.set_input(self.convert_to_vtkImage(data), self.convert_to_vtkImage(moredata[0])) viewer_type = None @@ -125,21 +123,21 @@ def show(self): if self.has_run is None: self.has_run = self.app.exec_() else: - print('No instance can be run interactively again. Delete and re-instantiate.') + print("No instance can be run interactively again. Delete and re-instantiate.") def __del__(self): - '''destructor''' + """destructor""" self.app.exit() def convert_to_vtkImage(self, data): - '''convert the data to vtkImageData for the viewer''' + """convert the data to vtkImageData for the viewer""" if isinstance(data, vtk.vtkImageData): vtkImage = data elif isinstance(data, np.ndarray): vtkImage = Converter.numpy2vtkImage(data) - elif hasattr(data, 'as_array'): + elif hasattr(data, "as_array"): # this makes it likely it is a CIL/SIRF DataContainer # currently this will only deal with the actual data # but it will parse the metadata in future @@ -155,10 +153,10 @@ def convert_to_vtkImage(self, data): vtk.vtkOutputWindow.SetInstance(err) data = example_data.HEAD.get() - iviewer(data, data, viewer1='2D', viewer2='3D') + iviewer(data, data, viewer1="2D", viewer2="3D") # To use your own metaimage file, uncomment: # reader = vtk.vtkMetaImageReader() # reader.SetFileName('head.mha') # reader.Update() - #iviewer(reader.GetOutput(), reader.GetOutput(), viewer1='2D', viewer2='3D') + # iviewer(reader.GetOutput(), reader.GetOutput(), viewer1='2D', viewer2='3D') diff --git a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py new file mode 100644 index 00000000..fe050c8a --- /dev/null +++ b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py @@ -0,0 +1,158 @@ +import os + +from eqt.ui import FormDialog, UISliderWidget +from PySide2 import QtCore, QtWidgets +from vtkmodules.util import colors + +from ccpi.viewer.ui.helpers import background_color_list + + +class SettingsDialog(FormDialog): + """Slice settings dialog.""" + + def __init__(self, parent=None, title=None): + FormDialog.__init__(self, parent, title=title) + self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) + self.file_location = "." + + # Background color + background_color = QtWidgets.QComboBox(self.groupBox) + for i in background_color_list(): + background_color.addItem(i["text"]) + self.addWidget(background_color, "Background color", "background_color") + + # Slice orientation + orientation = QtWidgets.QComboBox(self.groupBox) + orientation.addItems(["YZ", "XZ", "XY"]) + orientation.setCurrentIndex(2) + self.addWidget(orientation, "Orientation", "orientation") + + # Slice visibility + slice_visibility = QtWidgets.QCheckBox("Slice Visibility", self.groupBox) + self.addWidget(slice_visibility, "", "slice_visibility") + + # Auto window/level + auto_window_level = QtWidgets.QPushButton("Auto Window/Level") + self.addWidget(auto_window_level, "", "auto_window_level") + + # Slice window sliders + slice_window_label = QtWidgets.QLabel("Slice Window") + slice_window_slider = UISliderWidget.UISliderWidget(slice_window_label) + self.addWidget(slice_window_slider, "Slice Window", "slice_window_slider") + self.addWidget(slice_window_label, "", "slice_window_label") + + # Slice level sliders + slice_level_label = QtWidgets.QLabel("Slice Level") + slice_level_slider = UISliderWidget.UISliderWidget(slice_level_label) + self.addWidget(slice_level_slider, "Slice Level", "slice_level_slider") + self.addWidget(slice_level_label, "", "slice_level_label") + + # Render save location + render_save_location = QtWidgets.QLabel("'render'") + open_location_browser = QtWidgets.QPushButton("Open location browser") + self.addWidget(render_save_location, "Render save location", "render_save_location") + self.addWidget(open_location_browser, "", "open_location_browser") + + def get_settings(self): + """Return a dictionary of settings from the dialog.""" + settings = {} + for key, value in self.formWidget.widgets.items(): + if isinstance(value, QtWidgets.QLabel): + settings[key] = value.text() + elif isinstance(value, QtWidgets.QCheckBox): + settings[key] = value.isChecked() + elif isinstance(value, QtWidgets.QComboBox): + settings[key] = value.currentIndex() + elif isinstance(value, UISliderWidget.UISliderWidget): + settings[key] = value.value() + + return settings + + def apply_settings(self, settings): + """Apply the settings to the dialog.""" + for key, value in settings.items(): + widg = self.formWidget.widgets[key] + if isinstance(widg, QtWidgets.QLabel): + widg.setText(value) + elif isinstance(widg, QtWidgets.QCheckBox): + widg.setChecked(value) + elif isinstance(widg, QtWidgets.QComboBox): + widg.setCurrentIndex(value) + elif isinstance(widg, UISliderWidget.UISliderWidget): + widg.setValue(value) + + def auto_window_level(self): + """Set the window and level to the default values.""" + self.viewer.autoWindowLevelOnSliceRange() + + window_default = self.viewer.getSliceColorWindow() + self.getWidget("slice_window_slider").setValue(window_default) + + level_default = self.viewer.getSliceColorLevel() + self.getWidget("slice_level_slider").setValue(level_default) + + def set_viewer(self, viewer): + """Attach the events to the viewer.""" + self.viewer = viewer + + # Orientation + self.getWidget("orientation").currentIndexChanged.connect(self.change_viewer_orientation) + + # Slice visibility + self.getWidget("slice_visibility").setChecked(True) + self.getWidget("slice_visibility").stateChanged.connect(self.viewer.style.ToggleSliceVisibility) + + # Auto window/level + self.getWidget("auto_window_level").clicked.connect(self.auto_window_level) + + # Slice window sliders + window_min, window_max = self.viewer.getImageMapRange((0.0, 100.0), "scalar") + self.getWidget("slice_window_slider").setRange(window_min, window_max) + self.getWidget("slice_window_slider").setTickInterval((window_max - window_min) / 10) + window_default = self.viewer.getSliceColorWindow() + self.getWidget("slice_window_slider").setValue(window_default) + self.getWidget("slice_window_slider").valueChanged.connect( + lambda: self.viewer.setSliceColorWindow(self.getWidget("slice_window_slider").value()) + ) + + # Level window sliders + level_min, level_max = self.viewer.getImageMapRange((0.0, 100.0), "scalar") + self.getWidget("slice_level_slider").setRange(level_min, level_max) + self.getWidget("slice_level_slider").setTickInterval((level_max - level_min) / 10) + level_default = self.viewer.getSliceColorLevel() + self.getWidget("slice_level_slider").setValue(level_default) + self.getWidget("slice_level_slider").valueChanged.connect( + lambda: self.viewer.setSliceColorLevel(self.getWidget("slice_level_slider").value()) + ) + + # Background color + self.getWidget("background_color").currentIndexChanged.connect(self.change_background_color) + + # Render save location + self.getWidget("open_location_browser").clicked.connect(self.open_file_location_dialog) + + def open_file_location_dialog(self): + """Open file location dialog.""" + dialog = QtWidgets.QFileDialog() + self.file_location = dialog.getSaveFileName(self, "Select File")[0] + + self.getWidget("render_save_location").setText(f"'{os.path.relpath(self.file_location, os.getcwd())}'") + + def change_viewer_orientation(self): + """Change the viewer orientation.""" + index = self.getWidget("orientation").currentIndex() + self.viewer.style.SetSliceOrientation(index) + self.viewer.style.UpdatePipeline(resetcamera=True) + + def change_background_color(self): + """Change the background color.""" + color = self.getWidget("background_color").currentText().replace(" ", "_").lower() + if color == "miles_blue": + color_data = (0.1, 0.2, 0.4) + else: + color_data = getattr(colors, color.lower()) + self.viewer.ren.SetBackground(color_data) + self.viewer.updatePipeline() + + def adjust_slider(self, value): + pass diff --git a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py new file mode 100644 index 00000000..cba88df0 --- /dev/null +++ b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py @@ -0,0 +1,236 @@ +from eqt.ui import FormDialog, UISliderWidget +from PySide2 import QtCore, QtWidgets + +from ccpi.viewer.ui.helpers import color_scheme_list + + +class VolumeRenderSettingsDialog(FormDialog): + """Volume render settings dialogue.""" + + def __init__(self, parent=None, title=None): + FormDialog.__init__(self, parent, title=title) + self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) + + # 3D Volume visibility + volume_visibility = QtWidgets.QCheckBox("3D Volume Visibility", self.groupBox) + self.addWidget(volume_visibility, "", "volume_visibility") + + # Windowing min + windowing_label_min = QtWidgets.QLabel("Windowing min") + windowing_slider_min = UISliderWidget.UISliderWidget(windowing_label_min) + self.addWidget(windowing_slider_min, "Windowing min", "windowing_slider_min") + self.addWidget(windowing_label_min, "", "windowing_label") + + # Windowing max + windowing_label_max = QtWidgets.QLabel("Windowing max") + windowing_slider_max = UISliderWidget.UISliderWidget(windowing_label_max) + self.addWidget(windowing_slider_max, "Windowing max", "windowing_slider_max") + self.addWidget(windowing_label_max, "", "windowing_label_max") + + # Opacity mapping + opacity_mapping = QtWidgets.QComboBox(self.groupBox) + opacity_mapping.addItems(["Scalar", "Gradient"]) + self.addWidget(opacity_mapping, "Opacity mapping", "opacity_mapping") + + # Color scheme + color_scheme = QtWidgets.QComboBox(self.groupBox) + color_scheme.addItems(color_scheme_list()) + self.addWidget(color_scheme, "Color scheme", "color_scheme") + + # Volume clipping + volume_clipping = QtWidgets.QCheckBox("Volume clipping", self.groupBox) + self.addWidget(volume_clipping, "", "volume_clipping") + volume_clipping_reset = QtWidgets.QPushButton("Reset volume clipping", self.groupBox) + self.addWidget(volume_clipping_reset, "", "volume_clipping_reset") + + # Color range min + color_range_label_min = QtWidgets.QLabel("Color range min") + color_range_slider_min = UISliderWidget.UISliderWidget(color_range_label_min) + self.addWidget(color_range_slider_min, "Color range min", "color_range_slider_min") + self.addWidget(color_range_label_min, "", "color_range_label_min") + + # Color range max + color_range_label_max = QtWidgets.QLabel("Color range max") + color_range_slider_max = UISliderWidget.UISliderWidget(color_range_label_max) + self.addWidget(color_range_slider_max, "Color range max", "color_range_slider_max") + self.addWidget(color_range_label_max, "", "color_range_label_max") + + # Disable 3D related widgets if volume visibility is not checked + volume_visibility_checked = self.getWidget("volume_visibility").isChecked() + self.getWidget("opacity_mapping").setEnabled(volume_visibility_checked) + self.getWidget("color_scheme").setEnabled(volume_visibility_checked) + self.getWidget("volume_clipping").setEnabled(volume_visibility_checked) + self.getWidget("volume_clipping_reset").setEnabled(volume_visibility_checked) + self.getWidget("color_range_slider_min").setEnabled(volume_visibility_checked) + self.getWidget("color_range_slider_max").setEnabled(volume_visibility_checked) + self.getWidget("windowing_slider_min").setEnabled(volume_visibility_checked) + self.getWidget("windowing_slider_max").setEnabled(volume_visibility_checked) + + def set_viewer(self, viewer): + """Attach the events to the viewer.""" + self.viewer = viewer + + # Volume visibility + self.getWidget("volume_visibility").stateChanged.connect(self.toggle_volume_visibility) + + # Opacity mapping + self.getWidget("opacity_mapping").currentIndexChanged.connect(self.change_opacity_mapping) + + # Color scheme + self.getWidget("color_scheme").currentIndexChanged.connect(self.change_color_scheme) + + # Volume clipping + self.getWidget("volume_clipping").stateChanged.connect(self.viewer.style.ToggleVolumeClipping) + + # Reset volume clipping + self.getWidget("volume_clipping_reset").clicked.connect(self.reset_volume_clipping) + + # Color range slider min + self.getWidget("color_range_slider_min").setRange(0, 100) + self.getWidget("color_range_slider_min").setTickInterval(10) + self.getWidget("color_range_slider_min").setValue(85) + self.getWidget("color_range_slider_min").valueChanged.connect(self.change_color_range_min) + + # Color range slider max + self.getWidget("color_range_slider_max").setRange(0, 100) + self.getWidget("color_range_slider_max").setTickInterval(10) + self.getWidget("color_range_slider_max").setValue(95) + self.getWidget("color_range_slider_max").valueChanged.connect(self.change_color_range_max) + + # Windowing slider min + self.getWidget("windowing_slider_min").setRange(0, 100) + self.getWidget("windowing_slider_min").setTickInterval(10) + self.getWidget("windowing_slider_min").setValue(80) + self.getWidget("windowing_slider_min").valueChanged.connect(self.change_volume_opacity_min) + + # Windowing slider max + self.getWidget("windowing_slider_max").setRange(0, 100) + self.getWidget("windowing_slider_max").setTickInterval(10) + self.getWidget("windowing_slider_max").setValue(99) + self.getWidget("windowing_slider_max").valueChanged.connect(self.change_volume_opacity_max) + + def change_color_range_min(self): + """Change the volume color range min value.""" + if self.getWidget("color_range_slider_min").value() >= self.getWidget("color_range_slider_max").value(): + self.getWidget("color_range_slider_min").setValue(self.getWidget("color_range_slider_max").value() - 1) + + self.change_color_range() + + def change_color_range_max(self): + """Change the volume color range max value.""" + if self.getWidget("color_range_slider_max").value() <= self.getWidget("color_range_slider_min").value(): + self.getWidget("color_range_slider_max").setValue(self.getWidget("color_range_slider_min").value() + 1) + self.change_color_range() + + def change_color_range(self): + """Change the volume color range.""" + self.viewer.setVolumeColorPercentiles( + self.getWidget("color_range_slider_min").value(), self.getWidget("color_range_slider_max").value() + ) + + def change_volume_opacity_min(self): + """Change the volume opacity mapping min value.""" + if self.getWidget("windowing_slider_min").value() >= self.getWidget("windowing_slider_max").value(): + self.getWidget("windowing_slider_min").setValue(self.getWidget("windowing_slider_max").value() - 1) + + self.change_volume_opacity() + + def change_volume_opacity_max(self): + """Change the volume opacity mapping.""" + if self.getWidget("windowing_slider_max").value() <= self.getWidget("windowing_slider_min").value(): + self.getWidget("windowing_slider_max").setValue(self.getWidget("windowing_slider_min").value() + 1) + + self.change_volume_opacity() + + def change_volume_opacity(self): + """Change the volume opacity mapping""" + opacity = self.getWidget("opacity_mapping").currentText() + opacity_min, opacity_max = ( + self.getWidget("windowing_slider_min").value(), + self.getWidget("windowing_slider_max").value(), + ) + if opacity == "Gradient": + self.viewer.setGradientOpacityPercentiles(opacity_min, opacity_max) + elif opacity == "Scalar": + self.viewer.setScalarOpacityPercentiles(opacity_min, opacity_max) + + def reset_volume_clipping(self): + """Reset the volume clipping to the default state.""" + self.getWidget("volume_clipping").setChecked(False) + if self.viewer.volume_render_initialised: + if self.viewer.volume.GetMapper().GetClippingPlanes() is not None: + self.viewer.volume.GetMapper().RemoveAllClippingPlanes() + if self.viewer.clipping_plane_initialised: + self.viewer.style.SetVolumeClipping(False) + self.remove_clipping_plane() + + def remove_clipping_plane(self): + """Remove the clipping plane from the viewer.""" + if hasattr(self.viewer, "planew"): + self.viewer.remove_clipping_plane() + self.viewer.getRenderer().Render() + self.viewer.updatePipeline() + + def get_settings(self): + """Return a dictionary of settings from the dialog.""" + settings = {} + for key, value in self.formWidget.widgets.items(): + if isinstance(value, QtWidgets.QLabel): + settings[key] = value.text() + elif isinstance(value, QtWidgets.QCheckBox): + settings[key] = value.isChecked() + elif isinstance(value, QtWidgets.QComboBox): + settings[key] = value.currentIndex() + + return settings + + def apply_settings(self, settings): + """Apply the settings to the dialog.""" + for key, value in settings.items(): + widg = self.formWidget.widgets[key] + if isinstance(widg, QtWidgets.QLabel): + widg.setText(value) + elif isinstance(widg, QtWidgets.QCheckBox): + widg.setChecked(value) + elif isinstance(widg, QtWidgets.QComboBox): + widg.setCurrentIndex(value) + + def toggle_volume_visibility(self): + """Toggle volume visibility.""" + # Set 3D widgets enabled/disabled depending on volume visibility checkbox + volume_visibility_checked = self.getWidget("volume_visibility").isChecked() + self.getWidget("windowing_slider_min").setEnabled(volume_visibility_checked) + self.getWidget("windowing_slider_max").setEnabled(volume_visibility_checked) + self.getWidget("opacity_mapping").setEnabled(volume_visibility_checked) + self.getWidget("color_scheme").setEnabled(volume_visibility_checked) + self.getWidget("volume_clipping").setEnabled(volume_visibility_checked) + self.getWidget("volume_clipping_reset").setEnabled(volume_visibility_checked) + self.getWidget("color_range_slider_min").setEnabled(volume_visibility_checked) + self.getWidget("color_range_slider_max").setEnabled(volume_visibility_checked) + + self.viewer.style.ToggleVolumeVisibility() + + if volume_visibility_checked: + self.change_opacity_mapping() + if self.getWidget("volume_clipping").isChecked() and hasattr(self.viewer, "planew"): + print("Volume visibility on") + self.viewer.planew.On() + self.viewer.updatePipeline() + elif hasattr(self.viewer, "planew"): + self.viewer.planew.Off() + self.viewer.updatePipeline() + print("Volume visibility off") + + self.viewer.updateVolumePipeline() + + def change_opacity_mapping(self): + """Change opacity mapping method.""" + method = self.getWidget("opacity_mapping").currentText().lower() + self.viewer.setVolumeRenderOpacityMethod(method) + self.viewer.updateVolumePipeline() + + def change_color_scheme(self): + """Change color scheme.""" + color_scheme = self.getWidget("color_scheme").currentText() + self.viewer.setVolumeColorMapName(color_scheme) + self.viewer.updateVolumePipeline() diff --git a/Wrappers/Python/ccpi/viewer/ui/__init__.py b/Wrappers/Python/ccpi/viewer/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Wrappers/Python/ccpi/viewer/ui/helpers.py b/Wrappers/Python/ccpi/viewer/ui/helpers.py new file mode 100644 index 00000000..6a40630c --- /dev/null +++ b/Wrappers/Python/ccpi/viewer/ui/helpers.py @@ -0,0 +1,47 @@ +from vtkmodules.util import colors + +try: + import matplotlib.pyplot as plt +except ImportError: + # Add optional overload to allow plt.colormaps to be called without matplotlib + from ccpi.viewer.utils import CILColorMaps + + class BackupColorMaps: + @staticmethod + def colormaps(): + return ["viridis", "plasma", "inferno", "magma"] + + plt = BackupColorMaps() + + +def color_scheme_list(): + """Return a list of color schemes for the color scheme dropdown menu.""" + initial_list = plt.colormaps() + initial_list.insert(0, initial_list.pop(initial_list.index("viridis"))) + return initial_list + + +def background_color_list(): + """Return a list of background colors for the background color dropdown menu.""" + initial_list = dir(colors) + color_list = [ + { + "text": "Miles blue", + "value": "cil_viewer_blue", + } + ] + + initial_list.insert(0, initial_list.pop(initial_list.index("white"))) + initial_list.insert(1, initial_list.pop(initial_list.index("black"))) + + for color in initial_list: + if "__" in color: + continue + if "_" in color: + filtered_color = color.replace("_", " ") + else: + filtered_color = color + filtered_color = filtered_color.capitalize() + color_list.append({"text": filtered_color, "value": color}) + + return color_list diff --git a/Wrappers/Python/setup.py b/Wrappers/Python/setup.py index c96ecc8d..af54db15 100644 --- a/Wrappers/Python/setup.py +++ b/Wrappers/Python/setup.py @@ -19,15 +19,15 @@ def version2pep440(version): - '''normalises the version from git describe to pep440 - + """normalises the version from git describe to pep440 + https://www.python.org/dev/peps/pep-0440/#id29 - ''' - if version[0] == 'v': + """ + if version[0] == "v": version = version[1:] - if u'-' in version: - v = version.split('-') + if u"-" in version: + v = version.split("-") v_pep440 = "{}.dev{}".format(v[0], v[1]) else: v_pep440 = version @@ -35,46 +35,48 @@ def version2pep440(version): return v_pep440 -git_version_string = subprocess.check_output('git describe', shell=True).decode("utf-8").rstrip()[1:] +git_version_string = subprocess.check_output("git describe", shell=True).decode("utf-8").rstrip()[1:] -if os.environ.get('CONDA_BUILD', 0) == '1': - cwd = os.path.join(os.environ.get('RECIPE_DIR'), '..') +if os.environ.get("CONDA_BUILD", 0) == "1": + cwd = os.path.join(os.environ.get("RECIPE_DIR"), "..") # requirements are processed by conda requires = [] else: - requires = ['numpy', 'vtk'] + requires = ["numpy", "vtk"] cwd = os.getcwd() version = version2pep440(git_version_string) # update the version string -fname = os.path.join(cwd, 'ccpi', 'viewer', 'version.py') +fname = os.path.join(cwd, "ccpi", "viewer", "version.py") if os.path.exists(fname): os.remove(fname) -with open(fname, 'w') as f: - f.write('version = \'{}\''.format(version)) +with open(fname, "w") as f: + f.write("version = '{}'".format(version)) setup( name="ccpi-viewer", version=version, packages=[ - 'ccpi', - 'ccpi.viewer', - 'ccpi.viewer.utils', - 'ccpi.web_viewer', - 'ccpi.viewer.widgets', - 'ccpi.viewer.cli', + "ccpi", + "ccpi.viewer", + "ccpi.viewer.utils", + "ccpi.web_viewer", + "ccpi.viewer.widgets", + "ccpi.viewer.cli", + "ccpi.viewer.ui", ], install_requires=requires, zip_safe=False, # metadata for upload to PyPI author="Edoardo Pasca", author_email="edoardo.pasca@stfc.ac.uk", - description='CCPi CILViewer', + description="CCPi CILViewer", license="Apache v2.0", keywords="3D data viewer", url="http://www.ccpi.ac.uk", # project home page, if any entry_points={ - 'console_scripts': ['resample = ccpi.viewer.cli.resample:main', 'web_cilviewer = ccpi.web_viewer.web_app:main'] - }) + "console_scripts": ["resample = ccpi.viewer.cli.resample:main", "web_cilviewer = ccpi.web_viewer.web_app:main"] + }, +)