From aa667fd65da3f3327ab0a0acd12d9de839ab9680 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Fri, 16 Jun 2023 00:41:48 +0100 Subject: [PATCH 01/35] update master with 3D toolbar --- .../Python/ccpi/viewer/QCILViewer3DToolBar.py | 111 ++++++++ .../Python/ccpi/viewer/QCILViewerWidget.py | 16 +- .../Python/ccpi/viewer/ui/SettingsDialog.py | 158 ++++++++++++ .../viewer/ui/VolumeRenderSettingsDialog.py | 236 ++++++++++++++++++ Wrappers/Python/ccpi/viewer/ui/__init__.py | 0 Wrappers/Python/ccpi/viewer/ui/helpers.py | 47 ++++ 6 files changed, 567 insertions(+), 1 deletion(-) create mode 100644 Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py create mode 100644 Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py create mode 100644 Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py create mode 100644 Wrappers/Python/ccpi/viewer/ui/__init__.py create mode 100644 Wrappers/Python/ccpi/viewer/ui/helpers.py 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 ab829012..30501354 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py @@ -3,7 +3,8 @@ 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): @@ -58,9 +59,22 @@ def __init__(self, parent=None, **kwargs): 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): 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 From 26517afd18a8107e8d3dbe630ee5480a66637c73 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Jun 2023 23:43:54 +0000 Subject: [PATCH 02/35] Automated autoyapf fixes --- Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py | 1 + Wrappers/Python/ccpi/viewer/QCILViewerWidget.py | 5 ++--- Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py | 6 ++---- .../ccpi/viewer/ui/VolumeRenderSettingsDialog.py | 4 ++-- Wrappers/Python/ccpi/viewer/ui/helpers.py | 11 +++++------ 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py index 05e717de..a4b9868c 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py @@ -5,6 +5,7 @@ class QCILViewer3DToolBar(QtWidgets.QToolBar): + def __init__(self, parent=None, viewer=None, **kwargs): """ Parameters diff --git a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py index 30501354..2810435e 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py @@ -63,13 +63,12 @@ def __init__(self, parent=None, **kwargs): 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): diff --git a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py index fe050c8a..63eefab6 100644 --- a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py @@ -112,8 +112,7 @@ def set_viewer(self, viewer): 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()) - ) + 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") @@ -122,8 +121,7 @@ def set_viewer(self, viewer): 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()) - ) + lambda: self.viewer.setSliceColorLevel(self.getWidget("slice_level_slider").value())) # Background color self.getWidget("background_color").currentIndexChanged.connect(self.change_background_color) diff --git a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py index cba88df0..505a1828 100644 --- a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py @@ -125,8 +125,8 @@ def change_color_range_max(self): 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() - ) + 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.""" diff --git a/Wrappers/Python/ccpi/viewer/ui/helpers.py b/Wrappers/Python/ccpi/viewer/ui/helpers.py index 6a40630c..7d6256ab 100644 --- a/Wrappers/Python/ccpi/viewer/ui/helpers.py +++ b/Wrappers/Python/ccpi/viewer/ui/helpers.py @@ -7,6 +7,7 @@ from ccpi.viewer.utils import CILColorMaps class BackupColorMaps: + @staticmethod def colormaps(): return ["viridis", "plasma", "inferno", "magma"] @@ -24,12 +25,10 @@ def color_scheme_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", - } - ] + 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"))) From 0c71af79f923ced5e96c0b8390fef903bedd425d Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Fri, 16 Jun 2023 00:45:21 +0100 Subject: [PATCH 03/35] update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50679ff4..8ecc659f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v23.x.x +- adds Qt GUI toolbar to control 3D viewer + ## v23.1.0 - Raise error if try to add multiple widgets with the same name to CILViewer or CILViewer2D. - Adds the following base classes to `ui.main_windows.py`: From 1fe64401f2e50ea44687b65391b3b58dbc745c8c Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Fri, 16 Jun 2023 21:59:37 +0100 Subject: [PATCH 04/35] works with vtk8 --- Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py | 8 +++++++- Wrappers/Python/ccpi/viewer/ui/helpers.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py index 63eefab6..8de1332c 100644 --- a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py @@ -2,7 +2,13 @@ from eqt.ui import FormDialog, UISliderWidget from PySide2 import QtCore, QtWidgets -from vtkmodules.util import colors + +try: + import vtkmodules.all as vtk + # from vtkmodules.util import colors +except ImportError: + import vtk +from vtk.util import colors from ccpi.viewer.ui.helpers import background_color_list diff --git a/Wrappers/Python/ccpi/viewer/ui/helpers.py b/Wrappers/Python/ccpi/viewer/ui/helpers.py index 7d6256ab..b59295f3 100644 --- a/Wrappers/Python/ccpi/viewer/ui/helpers.py +++ b/Wrappers/Python/ccpi/viewer/ui/helpers.py @@ -1,4 +1,10 @@ -from vtkmodules.util import colors +# from vtkmodules.util import colors +try: + import vtkmodules.all as vtk + # from vtkmodules.util import colors +except ImportError: + import vtk +from vtk.util import colors try: import matplotlib.pyplot as plt From 428f19c98ae2532b36cdd2c00f9b95e1bbd4a1cc Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 24 Jan 2024 00:06:15 +0000 Subject: [PATCH 05/35] fixed relu to go between 0 and 1 and then remain at 1 --- Wrappers/Python/ccpi/viewer/utils/colormaps.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/utils/colormaps.py b/Wrappers/Python/ccpi/viewer/utils/colormaps.py index c60bec1e..7b8c8944 100644 --- a/Wrappers/Python/ccpi/viewer/utils/colormaps.py +++ b/Wrappers/Python/ccpi/viewer/utils/colormaps.py @@ -411,7 +411,7 @@ def relu(x, xmin, xmax, scaling=1): returns values as 1. x< xmin : f(x) = 0 2. xmin <= x <= xmax : f(x) = (x - xmin) / (xmax - xmin) - 3. x > xmax: f(x) = 0 + 3. x > xmax: f(x) = 1 :param x: ndarray to evaluate the function at :param xmin: value at which the function start increasing @@ -422,8 +422,10 @@ def relu(x, xmin, xmax, scaling=1): out = [] dx = xmax - xmin for i, val in enumerate(x): - if val < xmin or val > xmax: + if val < xmin: out.append(0) + elif val > xmax: + out.append(scaling) else: out.append(scaling * ((val - xmin) / dx)) return numpy.asarray(out) From d18f089ba8b2d5d93077a0bc12a098985609878f Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 24 Jan 2024 00:06:59 +0000 Subject: [PATCH 06/35] added scale factor for more detailed slider --- .../viewer/ui/VolumeRenderSettingsDialog.py | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py index 505a1828..1537eedd 100644 --- a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py @@ -7,7 +7,7 @@ class VolumeRenderSettingsDialog(FormDialog): """Volume render settings dialogue.""" - def __init__(self, parent=None, title=None): + def __init__(self, parent=None, title=None, scale_factor=50): FormDialog.__init__(self, parent, title=title) self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) @@ -15,15 +15,16 @@ def __init__(self, parent=None, title=None): volume_visibility = QtWidgets.QCheckBox("3D Volume Visibility", self.groupBox) self.addWidget(volume_visibility, "", "volume_visibility") + self.scale_factor = scale_factor # Windowing min windowing_label_min = QtWidgets.QLabel("Windowing min") - windowing_slider_min = UISliderWidget.UISliderWidget(windowing_label_min) + windowing_slider_min = UISliderWidget.UISliderWidget(windowing_label_min, scale_factor=1/scale_factor) 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) + windowing_slider_max = UISliderWidget.UISliderWidget(windowing_label_max, scale_factor=1/scale_factor) self.addWidget(windowing_slider_max, "Windowing max", "windowing_slider_max") self.addWidget(windowing_label_max, "", "windowing_label_max") @@ -45,13 +46,13 @@ def __init__(self, parent=None, title=None): # Color range min color_range_label_min = QtWidgets.QLabel("Color range min") - color_range_slider_min = UISliderWidget.UISliderWidget(color_range_label_min) + color_range_slider_min = UISliderWidget.UISliderWidget(color_range_label_min, scale_factor=1/scale_factor) 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) + color_range_slider_max = UISliderWidget.UISliderWidget(color_range_label_max, scale_factor=1/scale_factor) self.addWidget(color_range_slider_max, "Color range max", "color_range_slider_max") self.addWidget(color_range_label_max, "", "color_range_label_max") @@ -86,27 +87,27 @@ def set_viewer(self, viewer): 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").setRange(0, 100 * self.scale_factor) + self.getWidget("color_range_slider_min").setTickInterval(10* self.scale_factor) + self.getWidget("color_range_slider_min").setValue(85 * self.scale_factor) 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").setRange(0, 100 * self.scale_factor) + self.getWidget("color_range_slider_max").setTickInterval(10 * self.scale_factor) + self.getWidget("color_range_slider_max").setValue(95 * self.scale_factor) 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").setRange(0, 100 * self.scale_factor) + self.getWidget("windowing_slider_min").setTickInterval(10 * self.scale_factor) + self.getWidget("windowing_slider_min").setValue(80 * self.scale_factor) 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").setRange(0, 100 * self.scale_factor) + self.getWidget("windowing_slider_max").setTickInterval(10 * self.scale_factor) + self.getWidget("windowing_slider_max").setValue(99 * self.scale_factor) self.getWidget("windowing_slider_max").valueChanged.connect(self.change_volume_opacity_max) def change_color_range_min(self): @@ -125,8 +126,8 @@ def change_color_range_max(self): 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()) + self.getWidget("color_range_slider_min").value() / self.scale_factor, + self.getWidget("color_range_slider_max").value() / self.scale_factor) def change_volume_opacity_min(self): """Change the volume opacity mapping min value.""" @@ -146,8 +147,8 @@ 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(), + self.getWidget("windowing_slider_min").value() / self.scale_factor, + self.getWidget("windowing_slider_max").value() / self.scale_factor, ) if opacity == "Gradient": self.viewer.setGradientOpacityPercentiles(opacity_min, opacity_max) From da6d6f1f2e5d5a8cf6b195f8d5d5b45b5e7ddeb3 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 24 Jan 2024 00:07:15 +0000 Subject: [PATCH 07/35] added logging --- Wrappers/Python/ccpi/viewer/CILViewerBase.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Wrappers/Python/ccpi/viewer/CILViewerBase.py b/Wrappers/Python/ccpi/viewer/CILViewerBase.py index bcd52bb5..fa2be712 100644 --- a/Wrappers/Python/ccpi/viewer/CILViewerBase.py +++ b/Wrappers/Python/ccpi/viewer/CILViewerBase.py @@ -3,6 +3,7 @@ LINEPLOT_ACTOR, OVERLAY_ACTOR, SHIFT_KEY, SLICE_ACTOR, SLICE_ORIENTATION_XY, SLICE_ORIENTATION_XZ, SLICE_ORIENTATION_YZ) from ccpi.viewer.utils.io import SaveRenderToPNG +import logging class ViewerEventManager(object): @@ -214,6 +215,10 @@ def getImageMapRange(self, percentiles, method): ia.SetAutoRangePercentiles(*percentiles) ia.Update() min, max = ia.GetAutoRange() + logging.debug(f"getImageMapRange: method{method}") + logging.debug(f"getImageMapRange: percentiles {percentiles}") + logging.debug(f"getImageMapRange: whole range ({ia.GetMinimum()}, {ia.GetMaximum()})") + logging.debug(f"getImageMapRange: percentile range ({min}, {max})") return min, max def getImageMapWholeRange(self, method): From d0f70b942716a052c90da3fcf1b18d4aec4c1717 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 24 Jan 2024 00:07:52 +0000 Subject: [PATCH 08/35] added logging and args parse in cilviewer exe --- Wrappers/Python/ccpi/viewer/standalone_viewer.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/standalone_viewer.py b/Wrappers/Python/ccpi/viewer/standalone_viewer.py index 302495d2..269bcb63 100644 --- a/Wrappers/Python/ccpi/viewer/standalone_viewer.py +++ b/Wrappers/Python/ccpi/viewer/standalone_viewer.py @@ -6,7 +6,8 @@ from ccpi.viewer.ui.main_windows import TwoViewersMainWindow from PySide2 import QtWidgets from PySide2.QtWidgets import QAction, QCheckBox - +import logging +import argparse class StandaloneViewerMainWindow(TwoViewersMainWindow): ''' @@ -148,8 +149,18 @@ def __del__(self): '''destructor''' self.app.exit() - def main(): + + parser = argparse.ArgumentParser(description='Standalone CIL Viewer') + + parser.add_argument('--debug', type=str) + args = parser.parse_args() + + if args.debug in ['debug', 'info', 'warning', 'error', 'critical']: + level = eval(f'logging.{args.debug.upper()}') + logging.basicConfig(level=level) + logging.info(f"iDVC: Setting debugging level to {args.debug.upper()}") + # Run a standalone viewer with a 2D and a 3D viewer: err = vtk.vtkFileOutputWindow() err.SetFileName("viewer.log") From c969bb77c01e16d2338ff03143957ae763f79f72 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 24 Jan 2024 00:10:29 +0000 Subject: [PATCH 09/35] Automated autoyapf fixes --- Wrappers/Python/ccpi/viewer/standalone_viewer.py | 4 +++- .../ccpi/viewer/ui/VolumeRenderSettingsDialog.py | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/standalone_viewer.py b/Wrappers/Python/ccpi/viewer/standalone_viewer.py index 269bcb63..d2146946 100644 --- a/Wrappers/Python/ccpi/viewer/standalone_viewer.py +++ b/Wrappers/Python/ccpi/viewer/standalone_viewer.py @@ -7,7 +7,8 @@ from PySide2 import QtWidgets from PySide2.QtWidgets import QAction, QCheckBox import logging -import argparse +import argparse + class StandaloneViewerMainWindow(TwoViewersMainWindow): ''' @@ -149,6 +150,7 @@ def __del__(self): '''destructor''' self.app.exit() + def main(): parser = argparse.ArgumentParser(description='Standalone CIL Viewer') diff --git a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py index 1537eedd..08743e76 100644 --- a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py @@ -18,13 +18,13 @@ def __init__(self, parent=None, title=None, scale_factor=50): self.scale_factor = scale_factor # Windowing min windowing_label_min = QtWidgets.QLabel("Windowing min") - windowing_slider_min = UISliderWidget.UISliderWidget(windowing_label_min, scale_factor=1/scale_factor) + windowing_slider_min = UISliderWidget.UISliderWidget(windowing_label_min, scale_factor=1 / scale_factor) 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, scale_factor=1/scale_factor) + windowing_slider_max = UISliderWidget.UISliderWidget(windowing_label_max, scale_factor=1 / scale_factor) self.addWidget(windowing_slider_max, "Windowing max", "windowing_slider_max") self.addWidget(windowing_label_max, "", "windowing_label_max") @@ -46,13 +46,13 @@ def __init__(self, parent=None, title=None, scale_factor=50): # Color range min color_range_label_min = QtWidgets.QLabel("Color range min") - color_range_slider_min = UISliderWidget.UISliderWidget(color_range_label_min, scale_factor=1/scale_factor) + color_range_slider_min = UISliderWidget.UISliderWidget(color_range_label_min, scale_factor=1 / scale_factor) 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, scale_factor=1/scale_factor) + color_range_slider_max = UISliderWidget.UISliderWidget(color_range_label_max, scale_factor=1 / scale_factor) self.addWidget(color_range_slider_max, "Color range max", "color_range_slider_max") self.addWidget(color_range_label_max, "", "color_range_label_max") @@ -88,7 +88,7 @@ def set_viewer(self, viewer): # Color range slider min self.getWidget("color_range_slider_min").setRange(0, 100 * self.scale_factor) - self.getWidget("color_range_slider_min").setTickInterval(10* self.scale_factor) + self.getWidget("color_range_slider_min").setTickInterval(10 * self.scale_factor) self.getWidget("color_range_slider_min").setValue(85 * self.scale_factor) self.getWidget("color_range_slider_min").valueChanged.connect(self.change_color_range_min) From 787bc99129ec61b35d2310f3161c447594718735 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:56:53 +0000 Subject: [PATCH 10/35] adds scale_factor and max_opacity to parameter at start of cilviewer These are workarounds to be able to configure the volume render --- .../Python/ccpi/viewer/QCILViewer3DToolBar.py | 6 +++-- .../Python/ccpi/viewer/QCILViewerWidget.py | 5 ++++ .../Python/ccpi/viewer/standalone_viewer.py | 24 +++++++++++++++---- .../Python/ccpi/viewer/ui/SettingsDialog.py | 7 +++--- .../viewer/ui/VolumeRenderSettingsDialog.py | 2 +- 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py index a4b9868c..e2e6f3d2 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py @@ -18,6 +18,8 @@ def __init__(self, parent=None, viewer=None, **kwargs): """ self.parent = parent self.viewer = viewer + self.scale_factor = kwargs.get('scale_factor', 1.0) + super(QCILViewer3DToolBar, self).__init__(parent=parent, **kwargs) self.dialog = {"settings": None, "volume_render_settings": None} @@ -66,7 +68,7 @@ def open_dialog(self, mode): # pylint(access-member-before-definition) if mode == "settings": if self.dialog["settings"] is None: - dialog = SettingsDialog(parent=self.parent, title="Settings") + dialog = SettingsDialog(parent=self.parent, title="Settings", scale_factor=self.scale_factor) dialog.Ok.clicked.connect(lambda: self.accepted(mode)) dialog.Cancel.clicked.connect(lambda: self.rejected(mode)) dialog.set_viewer(self.viewer) @@ -79,7 +81,7 @@ def open_dialog(self, mode): if mode == "volume_render_settings": if self.dialog["volume_render_settings"] is None: - dialog = VolumeRenderSettingsDialog(parent=self.parent, title="Volume Render Settings") + dialog = VolumeRenderSettingsDialog(parent=self.parent, title="Volume Render Settings", scale_factor=self.scale_factor) dialog.Ok.clicked.connect(lambda: self.accepted(mode)) dialog.Cancel.clicked.connect(lambda: self.rejected(mode)) dialog.set_viewer(self.viewer) diff --git a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py index 2810435e..c87fd8b7 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py @@ -31,6 +31,7 @@ def __init__(self, parent=None, **kwargs): dimx, dimy = kwargs.get('shape', (600, 600)) # self.resize(dimx, dimy) + self.vtkWidget = QCILRenderWindowInteractor(self) if 'renderer' in kwargs.keys(): @@ -60,6 +61,7 @@ def __init__(self, parent=None, **kwargs): self.vl = QtWidgets.QVBoxLayout() + self._toolBar = None toolBar = self.getToolbar(parent) if toolBar is not None: self.vl.addWidget(toolBar) @@ -70,9 +72,12 @@ def __init__(self, parent=None, **kwargs): self.adjustSize() def getToolbar(self, parent=None): + if self._toolBar is not None: + return self._toolBar # Adds a toolbar to the QFrame if we have a 3D viewer if isinstance(self.viewer, viewer3D): toolBar = QCILViewer3DToolBar(viewer=self.viewer, parent=parent) + self._toolBar = toolBar return toolBar diff --git a/Wrappers/Python/ccpi/viewer/standalone_viewer.py b/Wrappers/Python/ccpi/viewer/standalone_viewer.py index d2146946..692f7553 100644 --- a/Wrappers/Python/ccpi/viewer/standalone_viewer.py +++ b/Wrappers/Python/ccpi/viewer/standalone_viewer.py @@ -29,7 +29,7 @@ def __init__(self, settings_name=None, organisation_name=None, viewer1_type='2D', - viewer2_type='3D'): + viewer2_type='3D', scale_factor=1, max_opacity=0.1): super(StandaloneViewerMainWindow, self).__init__(title, app_name, settings_name, organisation_name, viewer1_type, viewer2_type) @@ -38,6 +38,15 @@ def __init__(self, self.image_overlay = vtk.vtkImageData() + # apply scale factor to sliders in the viewer3D dock widget: + logging.debug("scale_factor = {}".format(scale_factor)) + self.scale_factor = scale_factor + self.frames[1].getToolbar().scale_factor = scale_factor + logging.debug(f"Setting maximum opacity for volume render to: {max_opacity}") + self.frames[1].viewer.setMaximumOpacity(max_opacity) + + + def addToMenu(self): ''' Adds actions to the menu bar for selecting the images to be displayed @@ -130,7 +139,7 @@ def set_up(self, title, viewer1_type, viewer2_type=None, *args, **kwargs): if None, only one viewer is displayed ''' - window = StandaloneViewerMainWindow(title, viewer1_type, viewer2_type) + window = StandaloneViewerMainWindow(title, viewer1_type, viewer2_type, *args, **kwargs) self.window = window self.has_run = None @@ -156,18 +165,25 @@ def main(): parser = argparse.ArgumentParser(description='Standalone CIL Viewer') parser.add_argument('--debug', type=str) + parser.add_argument('--scale_factor', type=float, default=1) + parser.add_argument('--max_opacity', type=float, default=0.1) args = parser.parse_args() if args.debug in ['debug', 'info', 'warning', 'error', 'critical']: level = eval(f'logging.{args.debug.upper()}') logging.basicConfig(level=level) - logging.info(f"iDVC: Setting debugging level to {args.debug.upper()}") + logging.info(f"cilviewer: Setting debugging level to {args.debug.upper()}") + + logging.debug(f"scale_factor: {args.scale_factor}") + logging.debug(f"max_opacity: {args.max_opacity}") # Run a standalone viewer with a 2D and a 3D viewer: err = vtk.vtkFileOutputWindow() err.SetFileName("viewer.log") vtk.vtkOutputWindow.SetInstance(err) - standalone_viewer("Standalone Viewer", viewer1_type='2D', viewer2_type='3D') + standalone_viewer("Standalone Viewer", viewer1_type='2D', viewer2_type='3D', \ + scale_factor=args.scale_factor, max_opacity=args.max_opacity + ) return 0 diff --git a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py index 8de1332c..b5d6e680 100644 --- a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py @@ -16,7 +16,7 @@ class SettingsDialog(FormDialog): """Slice settings dialog.""" - def __init__(self, parent=None, title=None): + def __init__(self, parent=None, title=None, scale_factor=1): FormDialog.__init__(self, parent, title=title) self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) self.file_location = "." @@ -42,14 +42,15 @@ def __init__(self, parent=None, title=None): self.addWidget(auto_window_level, "", "auto_window_level") # Slice window sliders + self.scale_factor = scale_factor slice_window_label = QtWidgets.QLabel("Slice Window") - slice_window_slider = UISliderWidget.UISliderWidget(slice_window_label) + slice_window_slider = UISliderWidget.UISliderWidget(slice_window_label, scale_factor=1/scale_factor) 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) + slice_level_slider = UISliderWidget.UISliderWidget(slice_level_label, scale_factor=1/scale_factor) self.addWidget(slice_level_slider, "Slice Level", "slice_level_slider") self.addWidget(slice_level_label, "", "slice_level_label") diff --git a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py index 08743e76..4ecdaa48 100644 --- a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py @@ -7,7 +7,7 @@ class VolumeRenderSettingsDialog(FormDialog): """Volume render settings dialogue.""" - def __init__(self, parent=None, title=None, scale_factor=50): + def __init__(self, parent=None, title=None, scale_factor=1): FormDialog.__init__(self, parent, title=title) self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) From 9287248ccf12f2a8e1e549d9637770aca01ce2e0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 24 Jan 2024 14:58:00 +0000 Subject: [PATCH 11/35] Automated autoyapf fixes --- Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py | 4 +++- Wrappers/Python/ccpi/viewer/QCILViewerWidget.py | 1 - Wrappers/Python/ccpi/viewer/standalone_viewer.py | 8 ++++---- Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py index e2e6f3d2..5d0a4e0a 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py @@ -81,7 +81,9 @@ def open_dialog(self, mode): if mode == "volume_render_settings": if self.dialog["volume_render_settings"] is None: - dialog = VolumeRenderSettingsDialog(parent=self.parent, title="Volume Render Settings", scale_factor=self.scale_factor) + dialog = VolumeRenderSettingsDialog(parent=self.parent, + title="Volume Render Settings", + scale_factor=self.scale_factor) dialog.Ok.clicked.connect(lambda: self.accepted(mode)) dialog.Cancel.clicked.connect(lambda: self.rejected(mode)) dialog.set_viewer(self.viewer) diff --git a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py index c87fd8b7..adc3056b 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py @@ -31,7 +31,6 @@ def __init__(self, parent=None, **kwargs): dimx, dimy = kwargs.get('shape', (600, 600)) # self.resize(dimx, dimy) - self.vtkWidget = QCILRenderWindowInteractor(self) if 'renderer' in kwargs.keys(): diff --git a/Wrappers/Python/ccpi/viewer/standalone_viewer.py b/Wrappers/Python/ccpi/viewer/standalone_viewer.py index 692f7553..9c10d65f 100644 --- a/Wrappers/Python/ccpi/viewer/standalone_viewer.py +++ b/Wrappers/Python/ccpi/viewer/standalone_viewer.py @@ -29,7 +29,9 @@ def __init__(self, settings_name=None, organisation_name=None, viewer1_type='2D', - viewer2_type='3D', scale_factor=1, max_opacity=0.1): + viewer2_type='3D', + scale_factor=1, + max_opacity=0.1): super(StandaloneViewerMainWindow, self).__init__(title, app_name, settings_name, organisation_name, viewer1_type, viewer2_type) @@ -45,8 +47,6 @@ def __init__(self, logging.debug(f"Setting maximum opacity for volume render to: {max_opacity}") self.frames[1].viewer.setMaximumOpacity(max_opacity) - - def addToMenu(self): ''' Adds actions to the menu bar for selecting the images to be displayed @@ -173,7 +173,7 @@ def main(): level = eval(f'logging.{args.debug.upper()}') logging.basicConfig(level=level) logging.info(f"cilviewer: Setting debugging level to {args.debug.upper()}") - + logging.debug(f"scale_factor: {args.scale_factor}") logging.debug(f"max_opacity: {args.max_opacity}") diff --git a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py index b5d6e680..d037a710 100644 --- a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py @@ -44,13 +44,13 @@ def __init__(self, parent=None, title=None, scale_factor=1): # Slice window sliders self.scale_factor = scale_factor slice_window_label = QtWidgets.QLabel("Slice Window") - slice_window_slider = UISliderWidget.UISliderWidget(slice_window_label, scale_factor=1/scale_factor) + slice_window_slider = UISliderWidget.UISliderWidget(slice_window_label, scale_factor=1 / scale_factor) 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, scale_factor=1/scale_factor) + slice_level_slider = UISliderWidget.UISliderWidget(slice_level_label, scale_factor=1 / scale_factor) self.addWidget(slice_level_slider, "Slice Level", "slice_level_slider") self.addWidget(slice_level_label, "", "slice_level_label") From 928c41155f4ea3109dd11d9fd9d1464e0d1fd799 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:38:27 +0000 Subject: [PATCH 12/35] make the slider work for the slice --- .../Python/ccpi/viewer/ui/SettingsDialog.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py index d037a710..d0faf8a9 100644 --- a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py @@ -114,21 +114,25 @@ def set_viewer(self, viewer): # 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) + self.getWidget("slice_window_slider").setRange(0, 100) + self.getWidget("slice_window_slider").setTickInterval(100 / 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())) + self.getWidget("slice_window_slider").setValue((window_default - window_min)/(window_max - window_min) * 100) + self.getWidget("slice_window_slider").sliderReleased.connect( + lambda: self.viewer.setSliceColorWindow(window_min + self.getWidget("slice_window_slider").value() / 100 * (window_max - window_min)) + ) # 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) + self.getWidget("slice_level_slider").setRange(0, 100) + self.getWidget("slice_level_slider").setTickInterval(100 / 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())) + + self.getWidget("slice_level_slider").setValue((level_default - level_min)/(level_max - level_min) * 100) + self.getWidget("slice_level_slider").sliderReleased.connect( + lambda: self.viewer.setSliceColorLevel( + level_min + self.getWidget("slice_level_slider").value() / 100 * (level_max - level_min)) + ) # Background color self.getWidget("background_color").currentIndexChanged.connect(self.change_background_color) From 03ea2a05c7b27821c80a6bdad4059b44f333b002 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:38:43 +0000 Subject: [PATCH 13/35] use sliderChanged --- .../Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py index 4ecdaa48..7e38c8cd 100644 --- a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py @@ -90,25 +90,25 @@ def set_viewer(self, viewer): self.getWidget("color_range_slider_min").setRange(0, 100 * self.scale_factor) self.getWidget("color_range_slider_min").setTickInterval(10 * self.scale_factor) self.getWidget("color_range_slider_min").setValue(85 * self.scale_factor) - self.getWidget("color_range_slider_min").valueChanged.connect(self.change_color_range_min) + self.getWidget("color_range_slider_min").sliderReleased.connect(self.change_color_range_min) # Color range slider max self.getWidget("color_range_slider_max").setRange(0, 100 * self.scale_factor) self.getWidget("color_range_slider_max").setTickInterval(10 * self.scale_factor) self.getWidget("color_range_slider_max").setValue(95 * self.scale_factor) - self.getWidget("color_range_slider_max").valueChanged.connect(self.change_color_range_max) + self.getWidget("color_range_slider_max").sliderReleased.connect(self.change_color_range_max) # Windowing slider min self.getWidget("windowing_slider_min").setRange(0, 100 * self.scale_factor) self.getWidget("windowing_slider_min").setTickInterval(10 * self.scale_factor) self.getWidget("windowing_slider_min").setValue(80 * self.scale_factor) - self.getWidget("windowing_slider_min").valueChanged.connect(self.change_volume_opacity_min) + self.getWidget("windowing_slider_min").sliderReleased.connect(self.change_volume_opacity_min) # Windowing slider max self.getWidget("windowing_slider_max").setRange(0, 100 * self.scale_factor) self.getWidget("windowing_slider_max").setTickInterval(10 * self.scale_factor) self.getWidget("windowing_slider_max").setValue(99 * self.scale_factor) - self.getWidget("windowing_slider_max").valueChanged.connect(self.change_volume_opacity_max) + self.getWidget("windowing_slider_max").sliderReleased.connect(self.change_volume_opacity_max) def change_color_range_min(self): """Change the volume color range min value.""" From 97ce493e034775302d3fed5c6d8d6482fc3c4fe2 Mon Sep 17 00:00:00 2001 From: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> Date: Thu, 23 May 2024 11:16:30 +0100 Subject: [PATCH 14/35] merge with master --- Wrappers/Python/ccpi/viewer/standalone_viewer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/standalone_viewer.py b/Wrappers/Python/ccpi/viewer/standalone_viewer.py index 33dbf761..ac6dd655 100644 --- a/Wrappers/Python/ccpi/viewer/standalone_viewer.py +++ b/Wrappers/Python/ccpi/viewer/standalone_viewer.py @@ -180,9 +180,8 @@ def main(): err = vtk.vtkFileOutputWindow() err.SetFileName("viewer.log") vtk.vtkOutputWindow.SetInstance(err) - standalone_viewer("Standalone Viewer", viewer1_type='2D', viewer2_type='3D', \ - scale_factor=args.scale_factor, max_opacity=args.max_opacity - ) + standalone_viewer_instance = standalone_viewer("Standalone Viewer", viewer1_type='2D', viewer2_type='3D', scale_factor=args.scale_factor, max_opacity=args.max_opacity) + standalone_viewer_instance.show() return 0 From 8d6ec94adfcc68bed494570a823630977a5aed0c Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 23 May 2024 10:16:56 +0000 Subject: [PATCH 15/35] Automated autoyapf fixes --- Wrappers/Python/ccpi/viewer/standalone_viewer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Wrappers/Python/ccpi/viewer/standalone_viewer.py b/Wrappers/Python/ccpi/viewer/standalone_viewer.py index ac6dd655..49e8e89e 100644 --- a/Wrappers/Python/ccpi/viewer/standalone_viewer.py +++ b/Wrappers/Python/ccpi/viewer/standalone_viewer.py @@ -180,7 +180,11 @@ def main(): err = vtk.vtkFileOutputWindow() err.SetFileName("viewer.log") vtk.vtkOutputWindow.SetInstance(err) - standalone_viewer_instance = standalone_viewer("Standalone Viewer", viewer1_type='2D', viewer2_type='3D', scale_factor=args.scale_factor, max_opacity=args.max_opacity) + standalone_viewer_instance = standalone_viewer("Standalone Viewer", + viewer1_type='2D', + viewer2_type='3D', + scale_factor=args.scale_factor, + max_opacity=args.max_opacity) standalone_viewer_instance.show() return 0 From c47477b7c78b6a6c688837ac054a851ce21377db Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Tue, 11 Jun 2024 18:28:36 +0100 Subject: [PATCH 16/35] add createAnimation --- Wrappers/Python/ccpi/viewer/CILViewer.py | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/Wrappers/Python/ccpi/viewer/CILViewer.py b/Wrappers/Python/ccpi/viewer/CILViewer.py index aad89a13..e2188395 100644 --- a/Wrappers/Python/ccpi/viewer/CILViewer.py +++ b/Wrappers/Python/ccpi/viewer/CILViewer.py @@ -258,6 +258,8 @@ def OnKeyPress(self, interactor, _): self.ToggleSliceVisibility() elif interactor.GetKeyCode() == "i": self.ToggleSliceInterpolation() + elif interactor.GetKeyCode() == "o": + self._viewer.createAnimation() elif interactor.GetKeyCode() == "c" and self._viewer.volume_render_initialised: self.ToggleVolumeClipping() else: @@ -1073,3 +1075,68 @@ def remove_clipping_plane(self): self.getRenderer().Render() self.updatePipeline() + + def createAnimation(self, FrameCount=20, + InitialCameraPosition=None, FocalPoint=None, + ClippingRange=None, AngleRange = 360, ViewUp = None): + + viewer = self + + if InitialCameraPosition is None: + InitialCameraPosition = viewer.getCamera().GetPosition() + if FocalPoint is None: + FocalPoint = viewer.getCamera().GetFocalPoint() + if ClippingRange is None: + ClippingRange = (0,2000) + if ViewUp is None: + ViewUp = (0,0,1) + if FrameCount is None: + FrameCount = 100 + #Setting locked values for camera position + locX = InitialCameraPosition[0] + locY = InitialCameraPosition[1] + locZ = InitialCameraPosition[2] + + print('Initial Camera Position: {}'.format(InitialCameraPosition)) + #Setting camera position + viewer.getCamera().SetPosition(InitialCameraPosition) + viewer.getCamera().SetFocalPoint(FocalPoint) + + #Setting camera viewup + viewer.getCamera().SetViewUp(ViewUp) + + #Set camera clipping range + viewer.getCamera().SetClippingRange(ClippingRange) + + #Defining distance from camera to focal point + r = numpy.sqrt(((InitialCameraPosition[0]-FocalPoint[0])**2) + +(InitialCameraPosition[1]-FocalPoint[1])**2) + print('Radius (distance from camera to focal point): {}'.format(r)) + + + + #Animating the camera + for x in range(FrameCount): + angle = (2 * numpy.pi ) * (x/FrameCount) + NewLocationX = r * numpy.sin(angle) + FocalPoint[0] + NewLocationY = r * numpy.cos(angle) + FocalPoint[1] + NewLocation = (NewLocationX, NewLocationY, locZ) + camera = vtk.vtkCamera() + camera.SetFocalPoint(FocalPoint) + camera.SetViewUp(ViewUp) + camera.SetPosition(*NewLocation) + viewer.ren.SetActiveCamera(camera) + viewer.adjustCamera() + + import time + time.sleep(0.05) + print("render frame {} angle {}".format(x, angle)) + print('Camera Position: {}'.format(NewLocation)) + rp = numpy.sqrt(((NewLocation[0]-FocalPoint[0])**2) + +(NewLocation[1]-FocalPoint[1])**2) + print ('Camera trajectory radius {}'.format(rp)) + viewer.saveRender('test_{}'.format(x)) + #Rendering and saving the render + viewer.getRenderer().Render() + viewer.renWin.Render() + \ No newline at end of file From 6df3f46fe05a40b63d73616d6af1f5fdf77ff40c Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:27:12 +0100 Subject: [PATCH 17/35] create an example to make volume renders and orbits programmatically --- Wrappers/Python/ccpi/viewer/CILViewer.py | 25 +++++++-- .../examples/programmatic_volume_render.py | 55 +++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 Wrappers/Python/examples/programmatic_volume_render.py diff --git a/Wrappers/Python/ccpi/viewer/CILViewer.py b/Wrappers/Python/ccpi/viewer/CILViewer.py index eb805d26..3c2fcbb0 100644 --- a/Wrappers/Python/ccpi/viewer/CILViewer.py +++ b/Wrappers/Python/ccpi/viewer/CILViewer.py @@ -37,6 +37,14 @@ def __init__(self, callback): #self.AddObserver('RightButtonPressEvent', self.OnRightMousePress, -0.5) #self.AddObserver('RightButtonReleaseEvent', self.OnRightMouseRelease, -0.5) + self._volume_render_pars = { + 'color_percentiles' : (5., 95.), + 'scalar_opacity_percentiles' : (80., 99.), + 'gradient_opacity_percentiles' : (80., 99.), + 'max_opacity' : 0.1 + } + + def GetSliceOrientation(self): return self._viewer.sliceOrientation @@ -423,6 +431,11 @@ def GetImageWorldExtent(self): def GetInputData(self): return self._viewer.img3D + + def GetVolumeRenderParameters(self): + # set defaults for opacity and colour mapping: + return self._volume_render_pars + class CILViewer(CILViewerBase): @@ -456,7 +469,8 @@ def __init__(self, dimx=600, dimy=600, renWin=None, iren=None, ren=None, debug=F self.volume_render_initialised = False self.clipping_plane_initialised = False - + + def createPolyDataActor(self, polydata): '''returns an actor for a given polydata''' @@ -657,10 +671,11 @@ def installVolumeRenderActorPipeline(self): self.volume = volume # set defaults for opacity and colour mapping: - color_percentiles = (5., 95.) - scalar_opacity_percentiles = (80., 99.) - gradient_opacity_percentiles = (80., 99.) - max_opacity = 0.1 + color_percentiles = self.style.GetVolumeRenderParameters()['color_percentiles'] + scalar_opacity_percentiles = self.style.GetVolumeRenderParameters()['scalar_opacity_percentiles'] + gradient_opacity_percentiles = self.style.GetVolumeRenderParameters()['gradient_opacity_percentiles'] + max_opacity = self.style.GetVolumeRenderParameters()['max_opacity'] + self.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) self.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) diff --git a/Wrappers/Python/examples/programmatic_volume_render.py b/Wrappers/Python/examples/programmatic_volume_render.py new file mode 100644 index 00000000..72012ca1 --- /dev/null +++ b/Wrappers/Python/examples/programmatic_volume_render.py @@ -0,0 +1,55 @@ +from ccpi.viewer.CILViewer2D import CILViewer2D as viewer2D +from ccpi.viewer.CILViewer import CILViewer as viewer3D +import glob +import vtk + +if __name__ == '__main__': + v = viewer3D(debug=False) + from ccpi.viewer.utils.io import ImageReader, cilHDF5ResampleReader, cilTIFFResampleReader + from ccpi.viewer.utils.conversion import cilTIFFImageReaderInterface + import os + # reader = ImageReader(r"C:\Users\ofn77899\Data\dvc/frame_000_f.npy", resample=False) + # reader = ImageReader(r"{}/head_uncompressed.mha".format(os.path.dirname(__file__)), resample=False) + # data = reader.Read() + + dirname = r"C:\Users\ofn77899\Data\HOW\Not_Angled" + fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] + + reader = cilTIFFResampleReader() + reader.SetFileName(fnames) + reader.SetTargetSize(1024*1024*1024) + reader.Update() + data = reader.GetOutput() + + dims = data.GetDimensions() + spac = list(data.GetSpacing()) + spac[2] = dims[0]/dims[2] + print (data.GetSpacing()) + data.SetSpacing(*spac) + print (data.GetSpacing()) + + v.setInputData(reader.GetOutput()) + + v.style._volume_render_pars = { + 'color_percentiles' : (75., 99.), + 'scalar_opacity_percentiles' : (80., 99.), + 'gradient_opacity_percentiles' : (95., 99.9), + 'max_opacity' : 0.05 + } + + + v.setVolumeColorMapName('bone') + v.setVolumeRenderOpacityMethod('gradient') + + v.style.ToggleVolumeVisibility() + v.style.ToggleSliceVisibility() + + # default background + # self.ren.SetBackground(.1, .2, .4) + v.ren.SetBackground(0, 0, 0) + + v.startRenderLoop() + + v.createAnimation(FrameCount=10, InitialCameraPosition=((483.8653687626969, -2173.282759469902, 1052.4208133258792)), AngleRange=360) + + \ No newline at end of file From 88abdc274acbbcbeffa0bf67d4a7190b28d1d043 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:08:23 +0100 Subject: [PATCH 18/35] fixed save and naming --- .../examples/programmatic_volume_render.py | 128 ++++++++++++++++-- 1 file changed, 117 insertions(+), 11 deletions(-) diff --git a/Wrappers/Python/examples/programmatic_volume_render.py b/Wrappers/Python/examples/programmatic_volume_render.py index 72012ca1..0f59055d 100644 --- a/Wrappers/Python/examples/programmatic_volume_render.py +++ b/Wrappers/Python/examples/programmatic_volume_render.py @@ -2,6 +2,91 @@ from ccpi.viewer.CILViewer import CILViewer as viewer3D import glob import vtk +import numpy as np + +def createAnimation(viewer, FrameCount=20, + InitialCameraPosition=None, FocalPoint=None, + ClippingRange=None, AngleRange = 360, ViewUp = None): + + viewer + + if InitialCameraPosition is None: + InitialCameraPosition = viewer.getCamera().GetPosition() + if FocalPoint is None: + FocalPoint = viewer.getCamera().GetFocalPoint() + if ClippingRange is None: + ClippingRange = (0,2000) + if ViewUp is None: + ViewUp = (0,0,1) + if FrameCount is None: + FrameCount = 100 + #Setting locked values for camera position + locX = InitialCameraPosition[0] + locY = InitialCameraPosition[1] + locZ = InitialCameraPosition[2] + + print('Initial Camera Position: {}'.format(InitialCameraPosition)) + #Setting camera position + viewer.getCamera().SetPosition(InitialCameraPosition) + viewer.getCamera().SetFocalPoint(FocalPoint) + + #Setting camera viewup + viewer.getCamera().SetViewUp(ViewUp) + + #Set camera clipping range + viewer.getCamera().SetClippingRange(ClippingRange) + + #Defining distance from camera to focal point + r = np.sqrt(((InitialCameraPosition[0]-FocalPoint[0])**2) + +(InitialCameraPosition[1]-FocalPoint[1])**2) + print('Radius (distance from camera to focal point): {}'.format(r)) + + + + #Animating the camera + for x in range(FrameCount): + # move the slice during rotation + new_slice = round(x/FrameCount * viewer.img3D.GetDimensions()[2]) + print('displaying slice {}'.format(new_slice)) + viewer.style.SetActiveSlice(new_slice) + viewer.updatePipeline(False) + + + angle = (2 * np.pi ) * (x/FrameCount) + NewLocationX = r * np.sin(angle) + FocalPoint[0] + NewLocationY = r * np.cos(angle) + FocalPoint[1] + NewLocation = (NewLocationX, NewLocationY, locZ) + camera = vtk.vtkCamera() + camera.SetFocalPoint(FocalPoint) + camera.SetViewUp(ViewUp) + camera.SetPosition(*NewLocation) + viewer.ren.SetActiveCamera(camera) + viewer.adjustCamera() + + import time + time.sleep(0.1) + print("render frame {} angle {}".format(x, angle)) + print('Camera Position: {}'.format(NewLocation)) + rp = np.sqrt(((NewLocation[0]-FocalPoint[0])**2) + +(NewLocation[1]-FocalPoint[1])**2) + print ('Camera trajectory radius {}'.format(rp)) + #Rendering and saving the render + viewer.getRenderer().Render() + viewer.renWin.Render() + saveRender(viewer, x, 'test') + +def saveRender(viewer, number, file_prefix, directory='.'): + w2if = vtk.vtkWindowToImageFilter() + w2if.SetInput(viewer.renWin) + w2if.Update() + + + saveFilename = '{}_{:04d}.png'.format(os.path.join(directory, file_prefix), number) + + writer = vtk.vtkPNGWriter() + writer.SetFileName(saveFilename) + writer.SetInputConnection(w2if.GetOutputPort()) + writer.Write() if __name__ == '__main__': v = viewer3D(debug=False) @@ -30,26 +115,47 @@ v.setInputData(reader.GetOutput()) - v.style._volume_render_pars = { - 'color_percentiles' : (75., 99.), - 'scalar_opacity_percentiles' : (80., 99.), - 'gradient_opacity_percentiles' : (95., 99.9), - 'max_opacity' : 0.05 - } + v.style.ToggleVolumeVisibility() + + color_percentiles = (75., 99.) + scalar_opacity_percentiles = (80., 99.) + gradient_opacity_percentiles = (95., 99.9) + max_opacity = 0.05 + + # self.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) + # self.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) + # self.setGradientOpacityPercentiles(*gradient_opacity_percentiles, update_pipeline=False) + # self.setMaximumOpacity(max_opacity) v.setVolumeColorMapName('bone') - v.setVolumeRenderOpacityMethod('gradient') - v.style.ToggleVolumeVisibility() - v.style.ToggleSliceVisibility() + # define colors and opacity with default values + colors, opacity = v.getColorOpacityForVolumeRender() + + v.volume_property.SetColor(colors) + + method = 'gradient' + if method == 'gradient': + v.setVolumeRenderOpacityMethod('gradient') + v.setGradientOpacityPercentiles(*gradient_opacity_percentiles, update_pipeline=False) + v.volume_property.SetGradientOpacity(opacity) + else: + v.setVolumeRenderOpacityMethod('scalar') + v.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) + v.volume_property.SetScalarOpacity(opacity) + + v.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) + v.setMaximumOpacity(max_opacity) + + # v.style.ToggleSliceVisibility() # default background # self.ren.SetBackground(.1, .2, .4) v.ren.SetBackground(0, 0, 0) - v.startRenderLoop() + # v.startRenderLoop() - v.createAnimation(FrameCount=10, InitialCameraPosition=((483.8653687626969, -2173.282759469902, 1052.4208133258792)), AngleRange=360) + createAnimation(v, FrameCount=10, InitialCameraPosition=((483.8653687626969, -2173.282759469902, 1052.4208133258792)), AngleRange=360) \ No newline at end of file From 32d08e890236bb88dcb5916949d003ade24af760 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:24:24 +0100 Subject: [PATCH 19/35] updates to script --- .../examples/programmatic_volume_render.py | 94 +++++++++++++------ 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/Wrappers/Python/examples/programmatic_volume_render.py b/Wrappers/Python/examples/programmatic_volume_render.py index 0f59055d..60a2894b 100644 --- a/Wrappers/Python/examples/programmatic_volume_render.py +++ b/Wrappers/Python/examples/programmatic_volume_render.py @@ -6,7 +6,8 @@ def createAnimation(viewer, FrameCount=20, InitialCameraPosition=None, FocalPoint=None, - ClippingRange=None, AngleRange = 360, ViewUp = None): + ClippingRange=None, AngleRange = 360, ViewUp = None, clip_plane=False, + fname_offset=0, fname_prefix='test', output_dir='.'): viewer @@ -40,16 +41,28 @@ def createAnimation(viewer, FrameCount=20, r = np.sqrt(((InitialCameraPosition[0]-FocalPoint[0])**2) +(InitialCameraPosition[1]-FocalPoint[1])**2) print('Radius (distance from camera to focal point): {}'.format(r)) - - + if not clip_plane: + viewer.style.ToggleSliceVisibility() + #Animating the camera for x in range(FrameCount): - # move the slice during rotation - new_slice = round(x/FrameCount * viewer.img3D.GetDimensions()[2]) - print('displaying slice {}'.format(new_slice)) - viewer.style.SetActiveSlice(new_slice) - viewer.updatePipeline(False) + if clip_plane: + # move the slice during rotation + new_slice = round(x/FrameCount * viewer.img3D.GetDimensions()[2]) + print('displaying slice {}'.format(new_slice)) + viewer.style.SetActiveSlice(new_slice) + viewer.updatePipeline(False) + # plane on the slice plane + plane = vtk.vtkPlane() + plane.SetOrigin(0,0,new_slice * viewer.img3D.GetSpacing()[2]) + plane.SetNormal(0,0,-1) + + viewer.volume.GetMapper().RemoveAllClippingPlanes() + viewer.volume.GetMapper().AddClippingPlane(plane) + viewer.volume.Modified() + viewer.getRenderer().Render() + angle = (2 * np.pi ) * (x/FrameCount) @@ -73,7 +86,7 @@ def createAnimation(viewer, FrameCount=20, #Rendering and saving the render viewer.getRenderer().Render() viewer.renWin.Render() - saveRender(viewer, x, 'test') + saveRender(viewer, x + fname_offset, fname_prefix, directory=output_dir) def saveRender(viewer, number, file_prefix, directory='.'): w2if = vtk.vtkWindowToImageFilter() @@ -88,6 +101,7 @@ def saveRender(viewer, number, file_prefix, directory='.'): writer.SetInputConnection(w2if.GetOutputPort()) writer.Write() + if __name__ == '__main__': v = viewer3D(debug=False) from ccpi.viewer.utils.io import ImageReader, cilHDF5ResampleReader, cilTIFFResampleReader @@ -96,10 +110,22 @@ def saveRender(viewer, number, file_prefix, directory='.'): # reader = ImageReader(r"C:\Users\ofn77899\Data\dvc/frame_000_f.npy", resample=False) # reader = ImageReader(r"{}/head_uncompressed.mha".format(os.path.dirname(__file__)), resample=False) # data = reader.Read() + + pokemon = 'Picachu' + subdir = 'PicachuFull' + # subdir = 'Not_Angled' + + + dirname = os.path.abspath("C:/Users/ofn77899/Data\HOW/{}/{}/".format(pokemon, subdir)) - dirname = r"C:\Users\ofn77899\Data\HOW\Not_Angled" fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] + print ("#########################") + print (dirname, fnames) + + if len(fnames) == 0: + exit(1) + reader = cilTIFFResampleReader() reader.SetFileName(fnames) reader.SetTargetSize(1024*1024*1024) @@ -109,6 +135,8 @@ def saveRender(viewer, number, file_prefix, directory='.'): dims = data.GetDimensions() spac = list(data.GetSpacing()) spac[2] = dims[0]/dims[2] + spac[2] *= 0.75 + print (data.GetSpacing()) data.SetSpacing(*spac) print (data.GetSpacing()) @@ -117,30 +145,26 @@ def saveRender(viewer, number, file_prefix, directory='.'): v.style.ToggleVolumeVisibility() - color_percentiles = (75., 99.) - scalar_opacity_percentiles = (80., 99.) - gradient_opacity_percentiles = (95., 99.9) - max_opacity = 0.05 - - - # self.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) - # self.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) - # self.setGradientOpacityPercentiles(*gradient_opacity_percentiles, update_pipeline=False) - # self.setMaximumOpacity(max_opacity) - v.setVolumeColorMapName('bone') + v.setVolumeColorMapName('viridis') # define colors and opacity with default values colors, opacity = v.getColorOpacityForVolumeRender() v.volume_property.SetColor(colors) - method = 'gradient' + method = 'scalar' if method == 'gradient': + color_percentiles = (65., 98.) + gradient_opacity_percentiles = (75., 99.9) + max_opacity = 0.1 v.setVolumeRenderOpacityMethod('gradient') v.setGradientOpacityPercentiles(*gradient_opacity_percentiles, update_pipeline=False) v.volume_property.SetGradientOpacity(opacity) else: + color_percentiles = (75., 99.) + scalar_opacity_percentiles = (94., 99.5) + max_opacity = 0.1 v.setVolumeRenderOpacityMethod('scalar') v.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) v.volume_property.SetScalarOpacity(opacity) @@ -148,14 +172,30 @@ def saveRender(viewer, number, file_prefix, directory='.'): v.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) v.setMaximumOpacity(max_opacity) - # v.style.ToggleSliceVisibility() - # default background # self.ren.SetBackground(.1, .2, .4) v.ren.SetBackground(0, 0, 0) + # v.ren.SetBackground(1,1,1) - # v.startRenderLoop() - - createAnimation(v, FrameCount=10, InitialCameraPosition=((483.8653687626969, -2173.282759469902, 1052.4208133258792)), AngleRange=360) + + createAnimation(v, FrameCount=100, + InitialCameraPosition=( + (483.8653687626969, -2173.282759469902, 1052.4208133258792) + ), + AngleRange=360, + clip_plane=True, + fname_offset=0, + fname_prefix=pokemon, + output_dir="C:/Users/ofn77899/Data/HOW/Picachu/renders") + + createAnimation(v, FrameCount=100, + InitialCameraPosition=( + (483.8653687626969, -2173.282759469902, 1052.4208133258792) + ), + AngleRange=360, + clip_plane=False, + fname_offset=100, + fname_prefix=pokemon, + output_dir=r'C:\Users\ofn77899\Data\HOW\Picachu\renders') \ No newline at end of file From 2c217670c0debd88401d8dc4275fae8446ba96cc Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:46:08 +0100 Subject: [PATCH 20/35] updates --- Wrappers/Python/ccpi/viewer/CILViewer.py | 13 ++++++++++--- .../Python/examples/programmatic_volume_render.py | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/CILViewer.py b/Wrappers/Python/ccpi/viewer/CILViewer.py index 3c2fcbb0..2bbd0c47 100644 --- a/Wrappers/Python/ccpi/viewer/CILViewer.py +++ b/Wrappers/Python/ccpi/viewer/CILViewer.py @@ -273,7 +273,13 @@ def OnKeyPress(self, interactor, _): else: print("Unhandled event %s" % interactor.GetKeyCode()) - def CreateClippingPlane(self): + def CreateClippingPlane(self, proj=None, foc=None): + '''Create a clipping plane for the volume render + + foc: Focal Point. If None this is the active camera focal point + proj: Normal to the clipping plane. If None this is calculated from the active camera direction of projection + + ''' viewer = self._viewer planew = vtk.vtkImplicitPlaneWidget2() @@ -288,9 +294,10 @@ def CreateClippingPlane(self): rep.SetOutlineTranslation(False) # this means user can't move bounding box plane = vtk.vtkPlane() + if foc is None: # should be in the focal point - cam = self.GetActiveCamera() - foc = cam.GetFocalPoint() + cam = self.GetActiveCamera() + foc = cam.GetFocalPoint() plane.SetOrigin(*foc) proj = cam.GetDirectionOfProjection() diff --git a/Wrappers/Python/examples/programmatic_volume_render.py b/Wrappers/Python/examples/programmatic_volume_render.py index 60a2894b..ebcc92c0 100644 --- a/Wrappers/Python/examples/programmatic_volume_render.py +++ b/Wrappers/Python/examples/programmatic_volume_render.py @@ -112,8 +112,8 @@ def saveRender(viewer, number, file_prefix, directory='.'): # data = reader.Read() pokemon = 'Picachu' - subdir = 'PicachuFull' - # subdir = 'Not_Angled' + # subdir = 'PicachuFull' + subdir = 'Not_Angled' dirname = os.path.abspath("C:/Users/ofn77899/Data\HOW/{}/{}/".format(pokemon, subdir)) @@ -174,8 +174,8 @@ def saveRender(viewer, number, file_prefix, directory='.'): # default background # self.ren.SetBackground(.1, .2, .4) - v.ren.SetBackground(0, 0, 0) - # v.ren.SetBackground(1,1,1) + # v.ren.SetBackground(0, 0, 0) + v.ren.SetBackground(1,1,1) createAnimation(v, FrameCount=100, From 10e0dcbb3fdcb23b2acd0d165c89c9377fb9413c Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:42:18 +0100 Subject: [PATCH 21/35] added argparser --- .../examples/programmatic_volume_render.py | 162 +++++++++++++++--- 1 file changed, 138 insertions(+), 24 deletions(-) diff --git a/Wrappers/Python/examples/programmatic_volume_render.py b/Wrappers/Python/examples/programmatic_volume_render.py index ebcc92c0..e0f1f22d 100644 --- a/Wrappers/Python/examples/programmatic_volume_render.py +++ b/Wrappers/Python/examples/programmatic_volume_render.py @@ -1,8 +1,16 @@ -from ccpi.viewer.CILViewer2D import CILViewer2D as viewer2D from ccpi.viewer.CILViewer import CILViewer as viewer3D import glob import vtk import numpy as np +from ccpi.viewer.utils.io import ImageReader, \ + cilTIFFResampleReader +from ccpi.viewer.utils.conversion import cilNumpyResampleReader, \ + cilHDF5ResampleReader,\ + cilRawResampleReader + +import os, sys +import argparse + def createAnimation(viewer, FrameCount=20, InitialCameraPosition=None, FocalPoint=None, @@ -101,44 +109,150 @@ def saveRender(viewer, number, file_prefix, directory='.'): writer.SetInputConnection(w2if.GetOutputPort()) writer.Write() +class Readers(): + @staticmethod + def get_reader(filename, dimensions=None, data_path=None): + if os.path.isdir(filename): + # should return and TIFF reader + return Readers._return_tiff_reader(filename) + else: + if data_path is not None: + + # HDF5 file + import h5py + with h5py.File(filename, 'r') as f: + f.items() + reader = cilHDF5ResampleReader() + reader.SetFileName(filename) + reader.SetDatasetName(data_path) + return reader + + elif dimensions is not None: + reader = cilRawResampleReader() + reader.SetFileName(filename) + reader.SetStoredArrayShape(dimensions) + return reader + else: + # Figure out what file we have + # 2) npy + from ccpi.viewer.utils.conversion import parseNpyHeader + try: + header = parseNpyHeader(filename) + if header['type'] == 'NUMPY': + ret = cilNumpyResampleReader() + ret.SetFileName(filename) + return ret + except Exception as err: + pass + + # 3) mha/mhd + try: + from ccpi.viewer.utils.conversion import cilMetaImageResampleReader + reader = cilMetaImageResampleReader() + reader.SetFileName(filename) + + reader.ReadMetaImageHeader() + return reader + except Exception as err: + pass + + + # 1) raw file + + @staticmethod + def _return_tiff_reader(dirname): + fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] + + print ("#########################") + print (dirname, fnames) + + if len(fnames) == 0: + # try again with .tif extension + fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] + raise ValueError(f"Directory {dirname} does not contain tiff files with extension tiff or tif") + reader = cilTIFFResampleReader() + reader.SetFileName(fnames) + return reader + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Create awesome volume render') + + parser.add_argument('--verbose', type=int, default=0) + parser.add_argument('--volume', type=str, default='scalar') + parser.add_argument('--max_opacity', type=float, default=0.1) + parser.add_argument('--spacing', type=float, nargs=3, default=None) + parser.add_argument('--spacing_multiplier', type=float, nargs=3, default=None) + parser.add_argument('--max_size', type=int, nargs=3, default=[1024,1024,1024]) + parser.add_argument('--resample_z', type=bool, default=True) + parser.add_argument('--colormap', type=str, default='viridis') + parser.add_argument('--num_frames', type=int, default=10) + parser.add_argument('--output_dir', type=str, default='.') + parser.add_argument('--output_prefix', type=str, default='awesome') + parser.add_argument('--dimensions', type=int, nargs=3, default=None) + parser.add_argument('--data_path', type=str, default=None) + parser.add_argument('--camera_position', type=float) + + parser.add_argument('input', help='File or directory') + args = parser.parse_args() + + print (args) -if __name__ == '__main__': - v = viewer3D(debug=False) + # print ("################### ", len(sys.argv)) + # exit(0) + + from ccpi.viewer.CILViewer import CILViewer as viewer3D from ccpi.viewer.utils.io import ImageReader, cilHDF5ResampleReader, cilTIFFResampleReader - from ccpi.viewer.utils.conversion import cilTIFFImageReaderInterface - import os + import vtk + + v = viewer3D(debug=True if args.verbose >0 else False) + + + + # detect the file format and return the appropriate reader + + # reader = ImageReader(r"C:\Users\ofn77899\Data\dvc/frame_000_f.npy", resample=False) # reader = ImageReader(r"{}/head_uncompressed.mha".format(os.path.dirname(__file__)), resample=False) # data = reader.Read() - pokemon = 'Picachu' - # subdir = 'PicachuFull' - subdir = 'Not_Angled' + + pokemon = args.output_prefix + + # subdir = 'Not_Angled' + + reader = Readers.get_reader(args.input, dimensions=args.dimensions, + data_path=args.data_path) - dirname = os.path.abspath("C:/Users/ofn77899/Data\HOW/{}/{}/".format(pokemon, subdir)) + # dirname = os.path.abspath("C:/Users/ofn77899/Data\HOW/{}/{}/".format(pokemon, subdir)) - fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] + # fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] - print ("#########################") - print (dirname, fnames) + # print ("#########################") + # print (dirname, fnames) - if len(fnames) == 0: - exit(1) + # if len(fnames) == 0: + # exit(1) - reader = cilTIFFResampleReader() - reader.SetFileName(fnames) + # reader = cilTIFFResampleReader() + # reader.SetFileName(fnames) reader.SetTargetSize(1024*1024*1024) reader.Update() data = reader.GetOutput() - dims = data.GetDimensions() - spac = list(data.GetSpacing()) - spac[2] = dims[0]/dims[2] - spac[2] *= 0.75 - print (data.GetSpacing()) - data.SetSpacing(*spac) + + if args.spacing is not None: + data.SetSpacing(*args.spacing) + elif args.spacing_multiplier is not None: + dims = data.GetDimensions() + spac = list(data.GetSpacing()) + # spac[2] = dims[0]/dims[2] + # spac[2] *= 0.75 + for i in range(3): + spac[i] *= args.spacing_multiplier[i] + data.SetSpacing(*spac) print (data.GetSpacing()) v.setInputData(reader.GetOutput()) @@ -178,7 +292,7 @@ def saveRender(viewer, number, file_prefix, directory='.'): v.ren.SetBackground(1,1,1) - createAnimation(v, FrameCount=100, + createAnimation(v, FrameCount=args.num_frames, InitialCameraPosition=( (483.8653687626969, -2173.282759469902, 1052.4208133258792) ), @@ -188,7 +302,7 @@ def saveRender(viewer, number, file_prefix, directory='.'): fname_prefix=pokemon, output_dir="C:/Users/ofn77899/Data/HOW/Picachu/renders") - createAnimation(v, FrameCount=100, + createAnimation(v, FrameCount=args.num_frames, InitialCameraPosition=( (483.8653687626969, -2173.282759469902, 1052.4208133258792) ), From 7cfe7bb3a1b06054e286de8b9414720f90e59b29 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:41:07 +0100 Subject: [PATCH 22/35] add max opacity control as spinbox --- .../viewer/ui/VolumeRenderSettingsDialog.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py index 7e38c8cd..99b79e0e 100644 --- a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py @@ -1,5 +1,5 @@ from eqt.ui import FormDialog, UISliderWidget -from PySide2 import QtCore, QtWidgets +from PySide2 import QtCore, QtWidgets, QtGui from ccpi.viewer.ui.helpers import color_scheme_list @@ -56,6 +56,11 @@ def __init__(self, parent=None, title=None, scale_factor=1): self.addWidget(color_range_slider_max, "Color range max", "color_range_slider_max") self.addWidget(color_range_label_max, "", "color_range_label_max") + # Max opacity + max_opacity_label = QtWidgets.QLabel("Max opacity") + max_opacity_input = QtWidgets.QDoubleSpinBox(self.groupBox) + self.addWidget(max_opacity_input, max_opacity_label, "max_opacity_input") + # 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) @@ -110,6 +115,12 @@ def set_viewer(self, viewer): self.getWidget("windowing_slider_max").setValue(99 * self.scale_factor) self.getWidget("windowing_slider_max").sliderReleased.connect(self.change_volume_opacity_max) + # MaxOpacity slider max + self.getWidget("max_opacity_input").setRange(0,1) + self.getWidget("max_opacity_input").setDecimals(3) + self.getWidget("max_opacity_input").setValue(viewer.style.GetVolumeRenderParameters()['max_opacity']) + self.getWidget("max_opacity_input").valueChanged.connect(self.change_volume_max_opacity) + 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(): @@ -155,6 +166,11 @@ def change_volume_opacity(self): elif opacity == "Scalar": self.viewer.setScalarOpacityPercentiles(opacity_min, opacity_max) + def change_volume_max_opacity(self): + """Change the volume opacity mapping max value.""" + mo = self.getWidget("max_opacity_input").value() + self.viewer.setMaximumOpacity(mo) + def reset_volume_clipping(self): """Reset the volume clipping to the default state.""" self.getWidget("volume_clipping").setChecked(False) From 7247ca8859db83f90b54ee3d990b4f068619ae1f Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 25 Sep 2024 22:41:49 +0000 Subject: [PATCH 23/35] Automated autoyapf fixes --- Wrappers/Python/ccpi/viewer/CILViewer.py | 53 +++---- .../Python/ccpi/viewer/ui/SettingsDialog.py | 15 +- .../viewer/ui/VolumeRenderSettingsDialog.py | 4 +- .../examples/programmatic_volume_render.py | 146 +++++++++--------- 4 files changed, 101 insertions(+), 117 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/CILViewer.py b/Wrappers/Python/ccpi/viewer/CILViewer.py index 46837a9d..1f3db2bb 100644 --- a/Wrappers/Python/ccpi/viewer/CILViewer.py +++ b/Wrappers/Python/ccpi/viewer/CILViewer.py @@ -38,13 +38,12 @@ def __init__(self, callback): #self.AddObserver('RightButtonReleaseEvent', self.OnRightMouseRelease, -0.5) self._volume_render_pars = { - 'color_percentiles' : (5., 95.), - 'scalar_opacity_percentiles' : (80., 99.), - 'gradient_opacity_percentiles' : (80., 99.), - 'max_opacity' : 0.1 + 'color_percentiles': (5., 95.), + 'scalar_opacity_percentiles': (80., 99.), + 'gradient_opacity_percentiles': (80., 99.), + 'max_opacity': 0.1 } - def GetSliceOrientation(self): return self._viewer.sliceOrientation @@ -294,7 +293,7 @@ def CreateClippingPlane(self, proj=None, foc=None): plane = vtk.vtkPlane() if foc is None: - # should be in the focal point + # should be in the focal point cam = self.GetActiveCamera() foc = cam.GetFocalPoint() plane.SetOrigin(*foc) @@ -466,11 +465,10 @@ def GetExtentFromVoxels(self, voxel_min, voxel_max): def GetInputData(self): return self._viewer.img3D - + def GetVolumeRenderParameters(self): # set defaults for opacity and colour mapping: - return self._volume_render_pars - + return self._volume_render_pars class CILViewer(CILViewerBase): @@ -504,8 +502,7 @@ def __init__(self, dimx=600, dimy=600, renWin=None, iren=None, ren=None, debug=F self.volume_render_initialised = False self.clipping_plane_initialised = False - - + def createPolyDataActor(self, polydata): '''returns an actor for a given polydata''' @@ -711,7 +708,6 @@ def installVolumeRenderActorPipeline(self): gradient_opacity_percentiles = self.style.GetVolumeRenderParameters()['gradient_opacity_percentiles'] max_opacity = self.style.GetVolumeRenderParameters()['max_opacity'] - self.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) self.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) self.setGradientOpacityPercentiles(*gradient_opacity_percentiles, update_pipeline=False) @@ -1126,10 +1122,14 @@ def remove_clipping_plane(self): self.getRenderer().Render() self.updatePipeline() - def createAnimation(self, FrameCount=20, - InitialCameraPosition=None, FocalPoint=None, - ClippingRange=None, AngleRange = 360, ViewUp = None): - + def createAnimation(self, + FrameCount=20, + InitialCameraPosition=None, + FocalPoint=None, + ClippingRange=None, + AngleRange=360, + ViewUp=None): + viewer = self if InitialCameraPosition is None: @@ -1137,9 +1137,9 @@ def createAnimation(self, FrameCount=20, if FocalPoint is None: FocalPoint = viewer.getCamera().GetFocalPoint() if ClippingRange is None: - ClippingRange = (0,2000) + ClippingRange = (0, 2000) if ViewUp is None: - ViewUp = (0,0,1) + ViewUp = (0, 0, 1) if FrameCount is None: FrameCount = 100 #Setting locked values for camera position @@ -1152,22 +1152,19 @@ def createAnimation(self, FrameCount=20, viewer.getCamera().SetPosition(InitialCameraPosition) viewer.getCamera().SetFocalPoint(FocalPoint) - #Setting camera viewup + #Setting camera viewup viewer.getCamera().SetViewUp(ViewUp) #Set camera clipping range viewer.getCamera().SetClippingRange(ClippingRange) #Defining distance from camera to focal point - r = numpy.sqrt(((InitialCameraPosition[0]-FocalPoint[0])**2) - +(InitialCameraPosition[1]-FocalPoint[1])**2) + r = numpy.sqrt(((InitialCameraPosition[0] - FocalPoint[0])**2) + (InitialCameraPosition[1] - FocalPoint[1])**2) print('Radius (distance from camera to focal point): {}'.format(r)) - - #Animating the camera for x in range(FrameCount): - angle = (2 * numpy.pi ) * (x/FrameCount) + angle = (2 * numpy.pi) * (x / FrameCount) NewLocationX = r * numpy.sin(angle) + FocalPoint[0] NewLocationY = r * numpy.cos(angle) + FocalPoint[1] NewLocation = (NewLocationX, NewLocationY, locZ) @@ -1177,16 +1174,14 @@ def createAnimation(self, FrameCount=20, camera.SetPosition(*NewLocation) viewer.ren.SetActiveCamera(camera) viewer.adjustCamera() - + import time time.sleep(0.05) print("render frame {} angle {}".format(x, angle)) print('Camera Position: {}'.format(NewLocation)) - rp = numpy.sqrt(((NewLocation[0]-FocalPoint[0])**2) - +(NewLocation[1]-FocalPoint[1])**2) - print ('Camera trajectory radius {}'.format(rp)) + rp = numpy.sqrt(((NewLocation[0] - FocalPoint[0])**2) + (NewLocation[1] - FocalPoint[1])**2) + print('Camera trajectory radius {}'.format(rp)) viewer.saveRender('test_{}'.format(x)) #Rendering and saving the render viewer.getRenderer().Render() viewer.renWin.Render() - diff --git a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py index d0faf8a9..d2466738 100644 --- a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py @@ -117,10 +117,9 @@ def set_viewer(self, viewer): self.getWidget("slice_window_slider").setRange(0, 100) self.getWidget("slice_window_slider").setTickInterval(100 / 10) window_default = self.viewer.getSliceColorWindow() - self.getWidget("slice_window_slider").setValue((window_default - window_min)/(window_max - window_min) * 100) - self.getWidget("slice_window_slider").sliderReleased.connect( - lambda: self.viewer.setSliceColorWindow(window_min + self.getWidget("slice_window_slider").value() / 100 * (window_max - window_min)) - ) + self.getWidget("slice_window_slider").setValue((window_default - window_min) / (window_max - window_min) * 100) + self.getWidget("slice_window_slider").sliderReleased.connect(lambda: self.viewer.setSliceColorWindow( + window_min + self.getWidget("slice_window_slider").value() / 100 * (window_max - window_min))) # Level window sliders level_min, level_max = self.viewer.getImageMapRange((0.0, 100.0), "scalar") @@ -128,11 +127,9 @@ def set_viewer(self, viewer): self.getWidget("slice_level_slider").setTickInterval(100 / 10) level_default = self.viewer.getSliceColorLevel() - self.getWidget("slice_level_slider").setValue((level_default - level_min)/(level_max - level_min) * 100) - self.getWidget("slice_level_slider").sliderReleased.connect( - lambda: self.viewer.setSliceColorLevel( - level_min + self.getWidget("slice_level_slider").value() / 100 * (level_max - level_min)) - ) + self.getWidget("slice_level_slider").setValue((level_default - level_min) / (level_max - level_min) * 100) + self.getWidget("slice_level_slider").sliderReleased.connect(lambda: self.viewer.setSliceColorLevel( + level_min + self.getWidget("slice_level_slider").value() / 100 * (level_max - level_min))) # Background color self.getWidget("background_color").currentIndexChanged.connect(self.change_background_color) diff --git a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py index 99b79e0e..25a1df7d 100644 --- a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py +++ b/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py @@ -116,11 +116,11 @@ def set_viewer(self, viewer): self.getWidget("windowing_slider_max").sliderReleased.connect(self.change_volume_opacity_max) # MaxOpacity slider max - self.getWidget("max_opacity_input").setRange(0,1) + self.getWidget("max_opacity_input").setRange(0, 1) self.getWidget("max_opacity_input").setDecimals(3) self.getWidget("max_opacity_input").setValue(viewer.style.GetVolumeRenderParameters()['max_opacity']) self.getWidget("max_opacity_input").valueChanged.connect(self.change_volume_max_opacity) - + 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(): diff --git a/Wrappers/Python/examples/programmatic_volume_render.py b/Wrappers/Python/examples/programmatic_volume_render.py index e0f1f22d..cea5b7be 100644 --- a/Wrappers/Python/examples/programmatic_volume_render.py +++ b/Wrappers/Python/examples/programmatic_volume_render.py @@ -12,11 +12,18 @@ import argparse -def createAnimation(viewer, FrameCount=20, - InitialCameraPosition=None, FocalPoint=None, - ClippingRange=None, AngleRange = 360, ViewUp = None, clip_plane=False, - fname_offset=0, fname_prefix='test', output_dir='.'): - +def createAnimation(viewer, + FrameCount=20, + InitialCameraPosition=None, + FocalPoint=None, + ClippingRange=None, + AngleRange=360, + ViewUp=None, + clip_plane=False, + fname_offset=0, + fname_prefix='test', + output_dir='.'): + viewer if InitialCameraPosition is None: @@ -24,9 +31,9 @@ def createAnimation(viewer, FrameCount=20, if FocalPoint is None: FocalPoint = viewer.getCamera().GetFocalPoint() if ClippingRange is None: - ClippingRange = (0,2000) + ClippingRange = (0, 2000) if ViewUp is None: - ViewUp = (0,0,1) + ViewUp = (0, 0, 1) if FrameCount is None: FrameCount = 100 #Setting locked values for camera position @@ -39,41 +46,38 @@ def createAnimation(viewer, FrameCount=20, viewer.getCamera().SetPosition(InitialCameraPosition) viewer.getCamera().SetFocalPoint(FocalPoint) - #Setting camera viewup + #Setting camera viewup viewer.getCamera().SetViewUp(ViewUp) #Set camera clipping range viewer.getCamera().SetClippingRange(ClippingRange) #Defining distance from camera to focal point - r = np.sqrt(((InitialCameraPosition[0]-FocalPoint[0])**2) - +(InitialCameraPosition[1]-FocalPoint[1])**2) + r = np.sqrt(((InitialCameraPosition[0] - FocalPoint[0])**2) + (InitialCameraPosition[1] - FocalPoint[1])**2) print('Radius (distance from camera to focal point): {}'.format(r)) if not clip_plane: viewer.style.ToggleSliceVisibility() - + #Animating the camera for x in range(FrameCount): if clip_plane: # move the slice during rotation - new_slice = round(x/FrameCount * viewer.img3D.GetDimensions()[2]) + new_slice = round(x / FrameCount * viewer.img3D.GetDimensions()[2]) print('displaying slice {}'.format(new_slice)) viewer.style.SetActiveSlice(new_slice) viewer.updatePipeline(False) # plane on the slice plane plane = vtk.vtkPlane() - plane.SetOrigin(0,0,new_slice * viewer.img3D.GetSpacing()[2]) - plane.SetNormal(0,0,-1) + plane.SetOrigin(0, 0, new_slice * viewer.img3D.GetSpacing()[2]) + plane.SetNormal(0, 0, -1) viewer.volume.GetMapper().RemoveAllClippingPlanes() viewer.volume.GetMapper().AddClippingPlane(plane) viewer.volume.Modified() viewer.getRenderer().Render() - - - angle = (2 * np.pi ) * (x/FrameCount) + angle = (2 * np.pi) * (x / FrameCount) NewLocationX = r * np.sin(angle) + FocalPoint[0] NewLocationY = r * np.cos(angle) + FocalPoint[1] NewLocation = (NewLocationX, NewLocationY, locZ) @@ -83,25 +87,24 @@ def createAnimation(viewer, FrameCount=20, camera.SetPosition(*NewLocation) viewer.ren.SetActiveCamera(camera) viewer.adjustCamera() - + import time time.sleep(0.1) print("render frame {} angle {}".format(x, angle)) print('Camera Position: {}'.format(NewLocation)) - rp = np.sqrt(((NewLocation[0]-FocalPoint[0])**2) - +(NewLocation[1]-FocalPoint[1])**2) - print ('Camera trajectory radius {}'.format(rp)) + rp = np.sqrt(((NewLocation[0] - FocalPoint[0])**2) + (NewLocation[1] - FocalPoint[1])**2) + print('Camera trajectory radius {}'.format(rp)) #Rendering and saving the render viewer.getRenderer().Render() viewer.renWin.Render() saveRender(viewer, x + fname_offset, fname_prefix, directory=output_dir) + def saveRender(viewer, number, file_prefix, directory='.'): w2if = vtk.vtkWindowToImageFilter() w2if.SetInput(viewer.renWin) w2if.Update() - saveFilename = '{}_{:04d}.png'.format(os.path.join(directory, file_prefix), number) writer = vtk.vtkPNGWriter() @@ -109,7 +112,9 @@ def saveRender(viewer, number, file_prefix, directory='.'): writer.SetInputConnection(w2if.GetOutputPort()) writer.Write() + class Readers(): + @staticmethod def get_reader(filename, dimensions=None, data_path=None): if os.path.isdir(filename): @@ -117,7 +122,7 @@ def get_reader(filename, dimensions=None, data_path=None): return Readers._return_tiff_reader(filename) else: if data_path is not None: - + # HDF5 file import h5py with h5py.File(filename, 'r') as f: @@ -126,7 +131,7 @@ def get_reader(filename, dimensions=None, data_path=None): reader.SetFileName(filename) reader.SetDatasetName(data_path) return reader - + elif dimensions is not None: reader = cilRawResampleReader() reader.SetFileName(filename) @@ -144,27 +149,26 @@ def get_reader(filename, dimensions=None, data_path=None): return ret except Exception as err: pass - + # 3) mha/mhd try: from ccpi.viewer.utils.conversion import cilMetaImageResampleReader reader = cilMetaImageResampleReader() reader.SetFileName(filename) - + reader.ReadMetaImageHeader() return reader except Exception as err: - pass - - + pass + # 1) raw file - + @staticmethod - def _return_tiff_reader(dirname): + def _return_tiff_reader(dirname): fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] - - print ("#########################") - print (dirname, fnames) + + print("#########################") + print(dirname, fnames) if len(fnames) == 0: # try again with .tif extension @@ -173,9 +177,9 @@ def _return_tiff_reader(dirname): reader = cilTIFFResampleReader() reader.SetFileName(fnames) return reader - -if __name__ == '__main__': + +if __name__ == '__main__': parser = argparse.ArgumentParser(description='Create awesome volume render') parser.add_argument('--verbose', type=int, default=0) @@ -183,7 +187,7 @@ def _return_tiff_reader(dirname): parser.add_argument('--max_opacity', type=float, default=0.1) parser.add_argument('--spacing', type=float, nargs=3, default=None) parser.add_argument('--spacing_multiplier', type=float, nargs=3, default=None) - parser.add_argument('--max_size', type=int, nargs=3, default=[1024,1024,1024]) + parser.add_argument('--max_size', type=int, nargs=3, default=[1024, 1024, 1024]) parser.add_argument('--resample_z', type=bool, default=True) parser.add_argument('--colormap', type=str, default='viridis') parser.add_argument('--num_frames', type=int, default=10) @@ -192,11 +196,11 @@ def _return_tiff_reader(dirname): parser.add_argument('--dimensions', type=int, nargs=3, default=None) parser.add_argument('--data_path', type=str, default=None) parser.add_argument('--camera_position', type=float) - + parser.add_argument('input', help='File or directory') args = parser.parse_args() - print (args) + print(args) # print ("################### ", len(sys.argv)) # exit(0) @@ -205,31 +209,25 @@ def _return_tiff_reader(dirname): from ccpi.viewer.utils.io import ImageReader, cilHDF5ResampleReader, cilTIFFResampleReader import vtk - v = viewer3D(debug=True if args.verbose >0 else False) - - + v = viewer3D(debug=True if args.verbose > 0 else False) # detect the file format and return the appropriate reader - # reader = ImageReader(r"C:\Users\ofn77899\Data\dvc/frame_000_f.npy", resample=False) # reader = ImageReader(r"{}/head_uncompressed.mha".format(os.path.dirname(__file__)), resample=False) # data = reader.Read() - - pokemon = args.output_prefix - + # subdir = 'Not_Angled' - reader = Readers.get_reader(args.input, dimensions=args.dimensions, - data_path=args.data_path) - + reader = Readers.get_reader(args.input, dimensions=args.dimensions, data_path=args.data_path) + # dirname = os.path.abspath("C:/Users/ofn77899/Data\HOW/{}/{}/".format(pokemon, subdir)) - + # fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] - - # print ("#########################") + + # print ("#########################") # print (dirname, fnames) # if len(fnames) == 0: @@ -237,12 +235,12 @@ def _return_tiff_reader(dirname): # reader = cilTIFFResampleReader() # reader.SetFileName(fnames) - reader.SetTargetSize(1024*1024*1024) + reader.SetTargetSize(1024 * 1024 * 1024) reader.Update() data = reader.GetOutput() - print (data.GetSpacing()) - + print(data.GetSpacing()) + if args.spacing is not None: data.SetSpacing(*args.spacing) elif args.spacing_multiplier is not None: @@ -251,14 +249,13 @@ def _return_tiff_reader(dirname): # spac[2] = dims[0]/dims[2] # spac[2] *= 0.75 for i in range(3): - spac[i] *= args.spacing_multiplier[i] + spac[i] *= args.spacing_multiplier[i] data.SetSpacing(*spac) - print (data.GetSpacing()) + print(data.GetSpacing()) v.setInputData(reader.GetOutput()) v.style.ToggleVolumeVisibility() - v.setVolumeColorMapName('viridis') @@ -282,34 +279,29 @@ def _return_tiff_reader(dirname): v.setVolumeRenderOpacityMethod('scalar') v.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) v.volume_property.SetScalarOpacity(opacity) - + v.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) v.setMaximumOpacity(max_opacity) # default background # self.ren.SetBackground(.1, .2, .4) # v.ren.SetBackground(0, 0, 0) - v.ren.SetBackground(1,1,1) - - - createAnimation(v, FrameCount=args.num_frames, - InitialCameraPosition=( - (483.8653687626969, -2173.282759469902, 1052.4208133258792) - ), - AngleRange=360, - clip_plane=True, + v.ren.SetBackground(1, 1, 1) + + createAnimation(v, + FrameCount=args.num_frames, + InitialCameraPosition=((483.8653687626969, -2173.282759469902, 1052.4208133258792)), + AngleRange=360, + clip_plane=True, fname_offset=0, fname_prefix=pokemon, output_dir="C:/Users/ofn77899/Data/HOW/Picachu/renders") - - createAnimation(v, FrameCount=args.num_frames, - InitialCameraPosition=( - (483.8653687626969, -2173.282759469902, 1052.4208133258792) - ), - AngleRange=360, - clip_plane=False, + + createAnimation(v, + FrameCount=args.num_frames, + InitialCameraPosition=((483.8653687626969, -2173.282759469902, 1052.4208133258792)), + AngleRange=360, + clip_plane=False, fname_offset=100, fname_prefix=pokemon, output_dir=r'C:\Users\ofn77899\Data\HOW\Picachu\renders') - - \ No newline at end of file From 9bb3fa95b42cb91f6427df27c85680bd8f10b161 Mon Sep 17 00:00:00 2001 From: Murgatroyd Date: Tue, 10 Dec 2024 16:27:02 +0000 Subject: [PATCH 24/35] Make raw dialog settings saveable --- Wrappers/Python/ccpi/viewer/ui/dialogs.py | 137 ++++++++++++------ .../Python/ccpi/viewer/ui/main_windows.py | 6 +- Wrappers/Python/test/test_ui_dialogs.py | 60 +++++++- 3 files changed, 156 insertions(+), 47 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/dialogs.py b/Wrappers/Python/ccpi/viewer/ui/dialogs.py index 83217960..9323e350 100644 --- a/Wrappers/Python/ccpi/viewer/ui/dialogs.py +++ b/Wrappers/Python/ccpi/viewer/ui/dialogs.py @@ -3,7 +3,7 @@ from eqt.ui import FormDialog from eqt.ui.SessionDialogs import AppSettingsDialog, ErrorDialog from PySide2 import QtCore, QtGui, QtWidgets -from PySide2.QtWidgets import QCheckBox, QDoubleSpinBox, QLabel, QLineEdit, QComboBox +from PySide2.QtWidgets import QCheckBox, QDoubleSpinBox, QLabel, QLineEdit, QComboBox, QPushButton import numpy as np from ccpi.viewer.utils import Converter from ccpi.viewer.utils.conversion import cilRawCroppedReader @@ -260,48 +260,6 @@ def preview(self): reader2.SetTypeCodeName(dt.name) reader2.SetStoredArrayShape(shape) reader2.Update() - # image = reader2.GetOutput() - - # rawfname = os.path.join(tempfile.gettempdir(),"test.raw") - - # offset = offset * bytes_per_element - # slices_to_read = 1 - # if shape[2] > 1: - # slices_to_read = 2 - # with open(self.fname, 'br') as f: - # f.seek(offset) - # raw_data = f.read(slice_size*bytes_per_element* slices_to_read) - # with open(rawfname, 'wb') as f2: - # f2.write(raw_data) - - # reader2 = vtk.vtkImageReader2() - # reader2.SetFileName(rawfname) - - # vtktype = Converter.dtype_name_to_vtkType[dt.name] - # reader2.SetDataScalarType(vtktype) - - # if isBigEndian: - # reader2.SetDataByteOrderToBigEndian() - # else: - # reader2.SetDataByteOrderToLittleEndian() - - # reader2.SetFileDimensionality(len(shape)) - # vtkshape = shape[:] - # if not isFortran: - # # need to reverse the shape (again) - # vtkshape = shape[::-1] - # # vtkshape = shape[:] - # slice_idx = 0 - # if dimensionality == 3: - # slice_idx = vtkshape[2]//2 - # reader2.SetDataExtent(0, vtkshape[0]-1, 0, vtkshape[1]-1, slice_idx, slice_idx+slices_to_read-1) - # # DataSpacing and DataOrigin should be added to the interface - # reader2.SetDataSpacing(1, 1, 1) - # reader2.SetDataOrigin(0, 0, 0) - - # print("reading") - # reader2.Update() - # read one slice in the middle and display it in a viewer in a modal dialog diag = QtWidgets.QDialog(parent=self) diag.setModal(True) @@ -337,6 +295,99 @@ def preview(self): # finally open the dialog diag.open() +class SaveableRawInputDialog(RawInputDialog): + def __init__(self, parent, fname, qsettings): + super(SaveableRawInputDialog, self).__init__(parent, fname) + + + self.settings = qsettings + + self.formWidget.addTitle(QLabel('Load Settings'), 'load_settings_title') + load_label = QLabel('Settings Name: ') + load_drop_down = QComboBox() + load_drop_down.addItems(self._get_settings_names_for_dialog()) + self.formWidget.addWidget(load_drop_down, load_label, 'load_name') + + load_button = QPushButton('Load Settings') + load_button.clicked.connect(self._load_settings) + self.formWidget.addSpanningWidget(load_button, 'load') + + self.formWidget.addTitle(QLabel('Save Settings'), 'save_settings_title') + save_label = QLabel('Settings Name: ') + save_box = QLineEdit() + self.formWidget.addWidget(save_box, save_label, 'save_name') + + save_button = QPushButton('Save Settings') + save_button.clicked.connect(self._save_settings) + self.formWidget.addSpanningWidget(save_button,'save') + + + + def _save_settings(self): + ''' + Adds a dictionary to the qsettings 'raw_dialog' + dictionary : + key: name entred by the user + value: the status of all widgets on the form + ''' + settings_dict = self.settings.value('raw_dialog', {}) + + settings_name = self.formWidget.getWidget('save_name').text() + + self.saveAllWidgetStates() + current_widget_status = self.getSavedWidgetStates() + + settings_dict[settings_name] = current_widget_status + + self.settings.setValue('raw_dialog', settings_dict) + + print("Settings successfully saved") + # todo make dialog + + def _get_settings_names_for_dialog(self): + ''' + Retrive from self.settings the names of all settings previously saved in the + 'raw_dialog' entry + ''' + settings_dict = self.settings.value('raw_dialog', {}) + + print(type(self.settings.value('raw_dialog'))) + print(self.settings.value('raw_dialog')) + print("The settings dict: ", settings_dict) + return settings_dict.keys() + + def _load_settings(self): + ''' + Load all of the widget states saved in the 'raw_dialog' entry of + self.settings under the name selected by the user from the load_name comobox + ''' + settings_found = False + if self.settings.value('raw_dialog'): + settings_dict = self.settings.value('raw_dialog', {}) + + name_of_state = self.formWidget.getWidget('load_name').currentText() + state = settings_dict.get(name_of_state) + + if state is not None: + + # save current stae + + self.applyWidgetStates(state) + + # load current state of dropdown + settings_found = True + + + if not settings_found: + # create error dialog: + print("Settings not found") + + # todo make dialog + print("Settings loaded") + + + + class HDF5InputDialog(FormDialog): ''' diff --git a/Wrappers/Python/ccpi/viewer/ui/main_windows.py b/Wrappers/Python/ccpi/viewer/ui/main_windows.py index 90834957..5ced79b3 100644 --- a/Wrappers/Python/ccpi/viewer/ui/main_windows.py +++ b/Wrappers/Python/ccpi/viewer/ui/main_windows.py @@ -9,7 +9,7 @@ from ccpi.viewer.CILViewer2D import CILViewer2D from ccpi.viewer.CILViewer import CILViewer from ccpi.viewer.QCILViewerWidget import QCILDockableWidget -from ccpi.viewer.ui.dialogs import HDF5InputDialog, RawInputDialog, ViewerSettingsDialog +from ccpi.viewer.ui.dialogs import HDF5InputDialog, RawInputDialog, ViewerSettingsDialog, SaveableRawInputDialog from ccpi.viewer.ui.qt_widgets import ViewerCoordsDockWidget from ccpi.viewer.utils import cilPlaneClipper from ccpi.viewer.utils.io import ImageReader @@ -179,7 +179,7 @@ def selectImage(self, label=None): if hasattr(self, 'raw_dialog'): self.raw_dialog.restoreAllSavedWidgetStates() else: - raw_dialog = RawInputDialog(self, file) + raw_dialog = SaveableRawInputDialog(self, file, self.settings) raw_dialog.Ok.clicked.connect(lambda: self.getRawAttrsFromDialog(raw_dialog)) self.raw_dialog = raw_dialog # See https://doc.qt.io/qt-6/qdialog.html#exec @@ -211,7 +211,7 @@ def getRawAttrsFromDialog(self, dialog): Parameters ---------- - dialog : RawInputDialog + dialog : SaveableRawInputDialog The dialog to get the attributes from. ''' dialog.saveAllWidgetStates() diff --git a/Wrappers/Python/test/test_ui_dialogs.py b/Wrappers/Python/test/test_ui_dialogs.py index 26097a94..552c3112 100644 --- a/Wrappers/Python/test/test_ui_dialogs.py +++ b/Wrappers/Python/test/test_ui_dialogs.py @@ -1,6 +1,6 @@ import unittest from unittest import mock -from ccpi.viewer.ui.dialogs import ViewerSettingsDialog, HDF5InputDialog, RawInputDialog +from ccpi.viewer.ui.dialogs import ViewerSettingsDialog, HDF5InputDialog, RawInputDialog, SaveableRawInputDialog from eqt.ui.SessionDialogs import AppSettingsDialog from PySide2.QtWidgets import QMainWindow @@ -203,6 +203,64 @@ def test_getRawAttrs(self): 'preview_slice': 0 } +@unittest.skipIf(skip_as_conda_build, "On conda builds do not do any test with interfaces") +class TestSaveableRawInputDialog(unittest.TestCase): + + def setUp(self): + global _instance + if _instance is None: + _instance = QApplication(sys.argv) + self.parent = QMainWindow() + self.parent.settings = QSettings() + self.fname = "test.raw" + + def test_init(self): + rdi = SaveableRawInputDialog(self.parent, self.fname, self.parent.settings) + assert rdi is not None + + def test_init_creates_widgets(self): + rdi = SaveableRawInputDialog(self.parent, self.fname, self.parent.settings) + #Check we have the widgets that we expect: + assert isinstance(rdi.getWidget('dimensionality', 'label'), QLabel) + assert isinstance(rdi.getWidget('dimensionality', 'field'), QComboBox) + assert isinstance(rdi.getWidget('dim_Width', 'label'), QLabel) + assert isinstance(rdi.getWidget('dim_Width', 'field'), QLineEdit) + assert isinstance(rdi.getWidget('dim_Height', 'label'), QLabel) + assert isinstance(rdi.getWidget('dim_Height', 'field'), QLineEdit) + assert isinstance(rdi.getWidget('dim_Images', 'label'), QLabel) + assert isinstance(rdi.getWidget('dim_Images', 'field'), QLineEdit) + assert isinstance(rdi.getWidget('dtype', 'label'), QLabel) + assert isinstance(rdi.getWidget('dtype', 'field'), QComboBox) + assert isinstance(rdi.getWidget('endianness', 'label'), QLabel) + assert isinstance(rdi.getWidget('endianness', 'field'), QComboBox) + assert isinstance(rdi.getWidget('preview_slice', 'label'), QLabel) + assert isinstance(rdi.getWidget('preview_slice', 'field'), QLineEdit) + + def test_getRawAttrs(self): + rdi = SaveableRawInputDialog(self.parent, self.fname, self.parent.settings) + got_raw_attrs = rdi.getRawAttrs() + expected_raw_attrs = { + 'shape': [0, 0, 0], + 'typecode': 'uint8', + 'is_big_endian': True, + 'is_fortran': True, + 'preview_slice': 0 + } + assert got_raw_attrs == expected_raw_attrs + rdi.getWidget('dim_Width', 'field').setText('1') + rdi.getWidget('dim_Height', 'field').setText('2') + rdi.getWidget('dim_Images', 'field').setText('3') + rdi.getWidget('dtype', 'field').setCurrentText('uint16') + rdi.getWidget('endianness', 'field').setCurrentText('Big Endian') + rdi.getWidget('preview_slice', 'field').setText("0") + rdi.getWidget('is_fortran', 'field').setCurrentText("Images-Height-Width") + assert rdi.getRawAttrs() == { + 'shape': [1, 2, 3], + 'typecode': 'uint16', + 'is_big_endian': True, + 'is_fortran': False, + 'preview_slice': 0 + } if __name__ == '__main__': unittest.main() From beaa24a7e78cf412f8a702e13136127fa6bf3d0a Mon Sep 17 00:00:00 2001 From: Murgatroyd Date: Wed, 18 Dec 2024 14:29:37 +0000 Subject: [PATCH 25/35] Move save name into dialog and add edit checkbox --- Wrappers/Python/ccpi/viewer/ui/dialogs.py | 47 +++++++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/dialogs.py b/Wrappers/Python/ccpi/viewer/ui/dialogs.py index 9323e350..452a34a1 100644 --- a/Wrappers/Python/ccpi/viewer/ui/dialogs.py +++ b/Wrappers/Python/ccpi/viewer/ui/dialogs.py @@ -302,6 +302,10 @@ def __init__(self, parent, fname, qsettings): self.settings = qsettings + self.formWidget.addSpanningWidget(QCheckBox("Enable editing"), 'enable_edit') + self.getWidget('enable_edit').setChecked(True) + self.getWidget('enable_edit').stateChanged.connect(self._change_edit_state) + self.formWidget.addTitle(QLabel('Load Settings'), 'load_settings_title') load_label = QLabel('Settings Name: ') load_drop_down = QComboBox() @@ -312,16 +316,42 @@ def __init__(self, parent, fname, qsettings): load_button.clicked.connect(self._load_settings) self.formWidget.addSpanningWidget(load_button, 'load') - self.formWidget.addTitle(QLabel('Save Settings'), 'save_settings_title') - save_label = QLabel('Settings Name: ') - save_box = QLineEdit() - self.formWidget.addWidget(save_box, save_label, 'save_name') - save_button = QPushButton('Save Settings') - save_button.clicked.connect(self._save_settings) - self.formWidget.addSpanningWidget(save_button,'save') + self.buttonBox.addButton(QtWidgets.QDialogButtonBox.Save) + self.buttonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._open_save_dialog) + + def _change_edit_state(self, editable=True): + '''Changes the edit state of the form''' + + widgets = self.getWidgets() + for widget in widgets.values(): + widget.setEnabled(editable) + + if not editable: + widgets_to_preserve = ['load_name', 'load', 'enable_edit', 'load_name', 'load_settings_title', 'preview_slice', 'preview_button'] + for widget in widgets_to_preserve: + print("Enabling widgets") + self.getWidget(widget, 'field').setEnabled(True) + try: + self.getWidget(widget, 'label').setEnabled(True) + except: + pass + + + def _open_save_dialog(self): + '''Opens dialog for specifiying name to save settings under''' + dialog = FormDialog(self) + dialog.formWidget.addTitle(QLabel('Save Settings'), 'save_settings_title') + save_label = QLabel('Settings Name: ') + save_box = QLineEdit() + dialog.formWidget.addWidget(save_box, save_label, 'save_name') + dialog.Cancel.clicked.connect(dialog.close) + dialog.Ok.clicked.connect(self._save_settings) + dialog.Ok.clicked.connect(dialog.close) + dialog.open() + self.save_dialog=dialog def _save_settings(self): ''' @@ -332,7 +362,7 @@ def _save_settings(self): ''' settings_dict = self.settings.value('raw_dialog', {}) - settings_name = self.formWidget.getWidget('save_name').text() + settings_name = self.save_dialog.getWidget('save_name').text() self.saveAllWidgetStates() current_widget_status = self.getSavedWidgetStates() @@ -373,6 +403,7 @@ def _load_settings(self): # save current stae self.applyWidgetStates(state) + self.getWidget('enable_edit').setChecked(False) # load current state of dropdown settings_found = True From 28c168d6365b983cffdd0c5031a21ecb1abb2bf4 Mon Sep 17 00:00:00 2001 From: Murgatroyd Date: Wed, 18 Dec 2024 15:48:21 +0000 Subject: [PATCH 26/35] Add unit tests --- Wrappers/Python/ccpi/viewer/ui/dialogs.py | 29 +++---- Wrappers/Python/test/test_ui_dialogs.py | 97 ++++++++++++----------- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/dialogs.py b/Wrappers/Python/ccpi/viewer/ui/dialogs.py index 452a34a1..f8b8115a 100644 --- a/Wrappers/Python/ccpi/viewer/ui/dialogs.py +++ b/Wrappers/Python/ccpi/viewer/ui/dialogs.py @@ -330,7 +330,6 @@ def _change_edit_state(self, editable=True): if not editable: widgets_to_preserve = ['load_name', 'load', 'enable_edit', 'load_name', 'load_settings_title', 'preview_slice', 'preview_button'] for widget in widgets_to_preserve: - print("Enabling widgets") self.getWidget(widget, 'field').setEnabled(True) try: self.getWidget(widget, 'label').setEnabled(True) @@ -338,8 +337,6 @@ def _change_edit_state(self, editable=True): pass - - def _open_save_dialog(self): '''Opens dialog for specifiying name to save settings under''' dialog = FormDialog(self) @@ -353,16 +350,20 @@ def _open_save_dialog(self): dialog.open() self.save_dialog=dialog + def _get_settings_save_name(self): + return self.save_dialog.getWidget('save_name').text() + + def _save_settings(self): ''' Adds a dictionary to the qsettings 'raw_dialog' dictionary : - key: name entred by the user + key: name entered by the user value: the status of all widgets on the form ''' settings_dict = self.settings.value('raw_dialog', {}) - settings_name = self.save_dialog.getWidget('save_name').text() + settings_name = self._get_settings_save_name() self.saveAllWidgetStates() current_widget_status = self.getSavedWidgetStates() @@ -371,21 +372,17 @@ def _save_settings(self): self.settings.setValue('raw_dialog', settings_dict) - print("Settings successfully saved") - # todo make dialog - def _get_settings_names_for_dialog(self): ''' Retrive from self.settings the names of all settings previously saved in the 'raw_dialog' entry ''' settings_dict = self.settings.value('raw_dialog', {}) - - print(type(self.settings.value('raw_dialog'))) - print(self.settings.value('raw_dialog')) - print("The settings dict: ", settings_dict) return settings_dict.keys() + def _get_name_of_state_to_load(self): + return self.formWidget.getWidget('load_name').currentText() + def _load_settings(self): ''' Load all of the widget states saved in the 'raw_dialog' entry of @@ -395,7 +392,7 @@ def _load_settings(self): if self.settings.value('raw_dialog'): settings_dict = self.settings.value('raw_dialog', {}) - name_of_state = self.formWidget.getWidget('load_name').currentText() + name_of_state = self._get_name_of_state_to_load() state = settings_dict.get(name_of_state) if state is not None: @@ -408,16 +405,10 @@ def _load_settings(self): # load current state of dropdown settings_found = True - if not settings_found: # create error dialog: print("Settings not found") - # todo make dialog - print("Settings loaded") - - - class HDF5InputDialog(FormDialog): diff --git a/Wrappers/Python/test/test_ui_dialogs.py b/Wrappers/Python/test/test_ui_dialogs.py index 552c3112..e97917ed 100644 --- a/Wrappers/Python/test/test_ui_dialogs.py +++ b/Wrappers/Python/test/test_ui_dialogs.py @@ -7,7 +7,11 @@ import os from unittest import mock +from unittest.mock import patch from PySide2.QtWidgets import QApplication, QLabel, QFrame, QDoubleSpinBox, QCheckBox, QPushButton, QLineEdit, QComboBox, QWidget +from PySide2.QtCore import QSettings +from eqt.ui import FormDialog +from functools import partial import sys @@ -211,56 +215,59 @@ def setUp(self): if _instance is None: _instance = QApplication(sys.argv) self.parent = QMainWindow() - self.parent.settings = QSettings() + self.settings = QSettings() self.fname = "test.raw" + + @patch("ccpi.viewer.ui.dialogs.RawInputDialog.__init__") + def test_init_calls_raw_input_dialog_init(self, mock_init_call): + mock_init_call.return_value = partial(FormDialog.__init__) + # expect attribute error after init call: + with self.assertRaises(AttributeError): + rdi = SaveableRawInputDialog(self.parent, self.fname, self.settings) + mock_init_call.assert_called_once() + def test_init(self): - rdi = SaveableRawInputDialog(self.parent, self.fname, self.parent.settings) + rdi = SaveableRawInputDialog(self.parent, self.fname, self.settings) assert rdi is not None - def test_init_creates_widgets(self): - rdi = SaveableRawInputDialog(self.parent, self.fname, self.parent.settings) - #Check we have the widgets that we expect: - assert isinstance(rdi.getWidget('dimensionality', 'label'), QLabel) - assert isinstance(rdi.getWidget('dimensionality', 'field'), QComboBox) - assert isinstance(rdi.getWidget('dim_Width', 'label'), QLabel) - assert isinstance(rdi.getWidget('dim_Width', 'field'), QLineEdit) - assert isinstance(rdi.getWidget('dim_Height', 'label'), QLabel) - assert isinstance(rdi.getWidget('dim_Height', 'field'), QLineEdit) - assert isinstance(rdi.getWidget('dim_Images', 'label'), QLabel) - assert isinstance(rdi.getWidget('dim_Images', 'field'), QLineEdit) - assert isinstance(rdi.getWidget('dtype', 'label'), QLabel) - assert isinstance(rdi.getWidget('dtype', 'field'), QComboBox) - assert isinstance(rdi.getWidget('endianness', 'label'), QLabel) - assert isinstance(rdi.getWidget('endianness', 'field'), QComboBox) - assert isinstance(rdi.getWidget('preview_slice', 'label'), QLabel) - assert isinstance(rdi.getWidget('preview_slice', 'field'), QLineEdit) - - def test_getRawAttrs(self): - rdi = SaveableRawInputDialog(self.parent, self.fname, self.parent.settings) - got_raw_attrs = rdi.getRawAttrs() - expected_raw_attrs = { - 'shape': [0, 0, 0], - 'typecode': 'uint8', - 'is_big_endian': True, - 'is_fortran': True, - 'preview_slice': 0 - } - assert got_raw_attrs == expected_raw_attrs - rdi.getWidget('dim_Width', 'field').setText('1') - rdi.getWidget('dim_Height', 'field').setText('2') - rdi.getWidget('dim_Images', 'field').setText('3') - rdi.getWidget('dtype', 'field').setCurrentText('uint16') - rdi.getWidget('endianness', 'field').setCurrentText('Big Endian') - rdi.getWidget('preview_slice', 'field').setText("0") - rdi.getWidget('is_fortran', 'field').setCurrentText("Images-Height-Width") - assert rdi.getRawAttrs() == { - 'shape': [1, 2, 3], - 'typecode': 'uint16', - 'is_big_endian': True, - 'is_fortran': False, - 'preview_slice': 0 - } + @patch("ccpi.viewer.ui.dialogs.SaveableRawInputDialog._get_settings_save_name") + def test_save_settings_when_nothing_in_qsettings(self, mock_get_name): + mock_get_name.return_value = "my_name" + empty_settings = QSettings('A', 'A') + rdi = SaveableRawInputDialog(self.parent, self.fname, empty_settings) + rdi._save_settings() + the_dict = empty_settings.value('raw_dialog') + self.assertEqual(empty_settings.allKeys(), ['raw_dialog']) + self.assertEqual( the_dict, {'my_name': rdi.getAllWidgetStates()}) + + + @patch("ccpi.viewer.ui.dialogs.SaveableRawInputDialog._get_settings_save_name") + def test_save_settings_when_soemthing_in_qsettings(self, mock_get_name): + mock_get_name.return_value = "my_name_2" + pop_settings = QSettings('C', 'D') + pop_settings.setValue('raw_dialog', {'hi': "I'm not empty"}) + rdi = SaveableRawInputDialog(self.parent, self.fname, pop_settings) + rdi._save_settings() + the_dict = pop_settings.value('raw_dialog') + self.assertEqual( the_dict, {'hi': "I'm not empty", 'my_name_2': rdi.getAllWidgetStates()}) + + @patch("ccpi.viewer.ui.dialogs.SaveableRawInputDialog._get_name_of_state_to_load") + def test_load_settings(self, mock_get_name): + mock_get_name.return_value = "state" + example_qsettings = QSettings('B', 'B') + rdi = SaveableRawInputDialog(self.parent, self.fname, example_qsettings) + rdi.getWidget('dim_Images').setText('10') + example_settings = rdi.getAllWidgetStates() + + example_qsettings = QSettings('B', 'B') + + example_qsettings.setValue('raw_dialog', {'state': example_settings}) + + rdi2 = SaveableRawInputDialog(self.parent, self.fname, example_qsettings) + rdi2._load_settings() + self.assertEqual(rdi2.getWidget('dim_Images').text(), "10") + if __name__ == '__main__': unittest.main() From 6c503b9d6fd6234ff329fe06ff5b2e2f6bf512d5 Mon Sep 17 00:00:00 2001 From: Murgatroyd Date: Wed, 18 Dec 2024 15:56:10 +0000 Subject: [PATCH 27/35] reverting other files to master --- Wrappers/Python/ccpi/viewer/CILViewer.py | 143 ++++-------------- Wrappers/Python/ccpi/viewer/CILViewerBase.py | 5 - .../Python/ccpi/viewer/QCILViewerWidget.py | 17 --- .../Python/ccpi/viewer/standalone_viewer.py | 39 +---- .../Python/ccpi/viewer/utils/colormaps.py | 6 +- 5 files changed, 37 insertions(+), 173 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/CILViewer.py b/Wrappers/Python/ccpi/viewer/CILViewer.py index 1f3db2bb..5ac166fb 100644 --- a/Wrappers/Python/ccpi/viewer/CILViewer.py +++ b/Wrappers/Python/ccpi/viewer/CILViewer.py @@ -36,13 +36,7 @@ def __init__(self, callback): self.AddObserver('LeftButtonReleaseEvent', self.OnLeftMouseRelease) #self.AddObserver('RightButtonPressEvent', self.OnRightMousePress, -0.5) #self.AddObserver('RightButtonReleaseEvent', self.OnRightMouseRelease, -0.5) - - self._volume_render_pars = { - 'color_percentiles': (5., 95.), - 'scalar_opacity_percentiles': (80., 99.), - 'gradient_opacity_percentiles': (80., 99.), - 'max_opacity': 0.1 - } + self.htext = None def GetSliceOrientation(self): return self._viewer.sliceOrientation @@ -265,20 +259,12 @@ def OnKeyPress(self, interactor, _): self.ToggleSliceVisibility() elif interactor.GetKeyCode() == "i": self.ToggleSliceInterpolation() - elif interactor.GetKeyCode() == "o": - self._viewer.createAnimation() elif interactor.GetKeyCode() == "c" and self._viewer.volume_render_initialised: self.ToggleVolumeClipping() else: print("Unhandled event %s" % interactor.GetKeyCode()) - def CreateClippingPlane(self, proj=None, foc=None): - '''Create a clipping plane for the volume render - - foc: Focal Point. If None this is the active camera focal point - proj: Normal to the clipping plane. If None this is calculated from the active camera direction of projection - - ''' + def CreateClippingPlane(self): viewer = self._viewer planew = vtk.vtkImplicitPlaneWidget2() @@ -292,10 +278,9 @@ def CreateClippingPlane(self, proj=None, foc=None): rep.SetOutlineTranslation(False) # this means user can't move bounding box plane = vtk.vtkPlane() - if foc is None: - # should be in the focal point - cam = self.GetActiveCamera() - foc = cam.GetFocalPoint() + # should be in the focal point + cam = self.GetActiveCamera() + foc = cam.GetFocalPoint() plane.SetOrigin(*foc) proj = cam.GetDirectionOfProjection() @@ -339,7 +324,7 @@ def DisplayHelp(self): self.Render() return - font_size = 24 + font_size = 16 # Create the text mappers and the associated Actor2Ds. @@ -356,25 +341,27 @@ def DisplayHelp(self): # The text is on multiple lines and center-justified (both horizontal and # vertical). textMapperC = vtk.vtkTextMapper() - textMapperC.SetInput("Mouse Interactions:\n" - "\n" - " - Slice: Mouse Scroll\n" - " - Zoom: Right Mouse + Move Up/Down\n" - " - Pan: Middle Mouse Button + Move or Shift + Left Mouse + Move\n" - " - Adjust Camera: Left Mouse + Move\n" - " - Rotate: Ctrl + Left Mouse + Move\n" - "\n" - "Keyboard Interactions:\n" - "\n" - "h: Display this help\n" - "x: YZ Plane\n" - "y: XZ Plane\n" - "z: XY Plane\n" - "r: Save render to current_render.png\n" - "s: Toggle visibility of slice\n" - "v: Toggle visibility of volume render\n" - "c: Activates volume render clipping plane widget\n" - "a: Whole image Auto Window/Level\n") + if self.htext == None: + self.htext = """ +Mouse Interactions: + - Slice: Mouse Scroll + - Zoom: Right Mouse + Move Up/Down + - Pan: Middle Mouse Button + Move or Shift + Left Mouse + Move + - Adjust Camera: Left Mouse + Move + - Rotate: Ctrl + Left Mouse + Move + +Keyboard Interactions: + h: Display this help + x: YZ Plane + y: XZ Plane + z: XY Plane + r: Save render to current_render.png + s: Toggle visibility of slice + v: Toggle visibility of volume render + c: Activates volume render clipping plane widget + a: Whole image Auto Window/Level + """ + textMapperC.SetInput(self.htext) tprop = textMapperC.GetTextProperty() tprop.ShallowCopy(multiLineTextProp) tprop.SetJustificationToLeft() @@ -466,10 +453,6 @@ def GetExtentFromVoxels(self, voxel_min, voxel_max): def GetInputData(self): return self._viewer.img3D - def GetVolumeRenderParameters(self): - # set defaults for opacity and colour mapping: - return self._volume_render_pars - class CILViewer(CILViewerBase): '''Simple 3D Viewer based on VTK classes''' @@ -703,10 +686,10 @@ def installVolumeRenderActorPipeline(self): self.volume = volume # set defaults for opacity and colour mapping: - color_percentiles = self.style.GetVolumeRenderParameters()['color_percentiles'] - scalar_opacity_percentiles = self.style.GetVolumeRenderParameters()['scalar_opacity_percentiles'] - gradient_opacity_percentiles = self.style.GetVolumeRenderParameters()['gradient_opacity_percentiles'] - max_opacity = self.style.GetVolumeRenderParameters()['max_opacity'] + color_percentiles = (5., 95.) + scalar_opacity_percentiles = (80., 99.) + gradient_opacity_percentiles = (80., 99.) + max_opacity = 0.1 self.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) self.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) @@ -1121,67 +1104,3 @@ def remove_clipping_plane(self): self.getRenderer().Render() self.updatePipeline() - - def createAnimation(self, - FrameCount=20, - InitialCameraPosition=None, - FocalPoint=None, - ClippingRange=None, - AngleRange=360, - ViewUp=None): - - viewer = self - - if InitialCameraPosition is None: - InitialCameraPosition = viewer.getCamera().GetPosition() - if FocalPoint is None: - FocalPoint = viewer.getCamera().GetFocalPoint() - if ClippingRange is None: - ClippingRange = (0, 2000) - if ViewUp is None: - ViewUp = (0, 0, 1) - if FrameCount is None: - FrameCount = 100 - #Setting locked values for camera position - locX = InitialCameraPosition[0] - locY = InitialCameraPosition[1] - locZ = InitialCameraPosition[2] - - print('Initial Camera Position: {}'.format(InitialCameraPosition)) - #Setting camera position - viewer.getCamera().SetPosition(InitialCameraPosition) - viewer.getCamera().SetFocalPoint(FocalPoint) - - #Setting camera viewup - viewer.getCamera().SetViewUp(ViewUp) - - #Set camera clipping range - viewer.getCamera().SetClippingRange(ClippingRange) - - #Defining distance from camera to focal point - r = numpy.sqrt(((InitialCameraPosition[0] - FocalPoint[0])**2) + (InitialCameraPosition[1] - FocalPoint[1])**2) - print('Radius (distance from camera to focal point): {}'.format(r)) - - #Animating the camera - for x in range(FrameCount): - angle = (2 * numpy.pi) * (x / FrameCount) - NewLocationX = r * numpy.sin(angle) + FocalPoint[0] - NewLocationY = r * numpy.cos(angle) + FocalPoint[1] - NewLocation = (NewLocationX, NewLocationY, locZ) - camera = vtk.vtkCamera() - camera.SetFocalPoint(FocalPoint) - camera.SetViewUp(ViewUp) - camera.SetPosition(*NewLocation) - viewer.ren.SetActiveCamera(camera) - viewer.adjustCamera() - - import time - time.sleep(0.05) - print("render frame {} angle {}".format(x, angle)) - print('Camera Position: {}'.format(NewLocation)) - rp = numpy.sqrt(((NewLocation[0] - FocalPoint[0])**2) + (NewLocation[1] - FocalPoint[1])**2) - print('Camera trajectory radius {}'.format(rp)) - viewer.saveRender('test_{}'.format(x)) - #Rendering and saving the render - viewer.getRenderer().Render() - viewer.renWin.Render() diff --git a/Wrappers/Python/ccpi/viewer/CILViewerBase.py b/Wrappers/Python/ccpi/viewer/CILViewerBase.py index 263893be..2f16553c 100644 --- a/Wrappers/Python/ccpi/viewer/CILViewerBase.py +++ b/Wrappers/Python/ccpi/viewer/CILViewerBase.py @@ -3,7 +3,6 @@ LINEPLOT_ACTOR, OVERLAY_ACTOR, SHIFT_KEY, SLICE_ACTOR, SLICE_ORIENTATION_XY, SLICE_ORIENTATION_XZ, SLICE_ORIENTATION_YZ) from ccpi.viewer.utils.io import SaveRenderToPNG -import logging class ViewerEventManager(object): @@ -217,10 +216,6 @@ def getImageMapRange(self, percentiles, method): ia.SetAutoRangePercentiles(*percentiles) ia.Update() min, max = ia.GetAutoRange() - logging.debug(f"getImageMapRange: method{method}") - logging.debug(f"getImageMapRange: percentiles {percentiles}") - logging.debug(f"getImageMapRange: whole range ({ia.GetMinimum()}, {ia.GetMaximum()})") - logging.debug(f"getImageMapRange: percentile range ({min}, {max})") return min, max def getImageMapWholeRange(self, method): diff --git a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py index e0f08320..9271e7bb 100644 --- a/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py +++ b/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py @@ -4,7 +4,6 @@ from PySide2 import QtCore, QtWidgets from ccpi.viewer.QCILRenderWindowInteractor import QCILRenderWindowInteractor from ccpi.viewer import viewer2D, viewer3D -from ccpi.viewer.QCILViewer3DToolBar import QCILViewer3DToolBar class QCILViewerWidget(QtWidgets.QFrame): @@ -75,26 +74,10 @@ def __init__(self, self.viewer.iren.SetInteractorStyle(self.viewer.style) self.vl = QtWidgets.QVBoxLayout() - - self._toolBar = None - 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): - if self._toolBar is not None: - return self._toolBar - # Adds a toolbar to the QFrame if we have a 3D viewer - if isinstance(self.viewer, viewer3D): - toolBar = QCILViewer3DToolBar(viewer=self.viewer, parent=parent) - self._toolBar = toolBar - return toolBar - class QCILDockableWidget(QtWidgets.QDockWidget): '''Inserts a vtk viewer in a dock widget.''' diff --git a/Wrappers/Python/ccpi/viewer/standalone_viewer.py b/Wrappers/Python/ccpi/viewer/standalone_viewer.py index 49e8e89e..5dea00d3 100644 --- a/Wrappers/Python/ccpi/viewer/standalone_viewer.py +++ b/Wrappers/Python/ccpi/viewer/standalone_viewer.py @@ -6,8 +6,6 @@ from ccpi.viewer.ui.main_windows import TwoViewersMainWindow from PySide2 import QtWidgets from PySide2.QtWidgets import QAction, QCheckBox -import logging -import argparse class StandaloneViewerMainWindow(TwoViewersMainWindow): @@ -29,9 +27,7 @@ def __init__(self, settings_name=None, organisation_name=None, viewer1_type='2D', - viewer2_type='3D', - scale_factor=1, - max_opacity=0.1): + viewer2_type='3D'): super(StandaloneViewerMainWindow, self).__init__(title, app_name, settings_name, organisation_name, viewer1_type, viewer2_type) @@ -40,13 +36,6 @@ def __init__(self, self.image_overlay = vtk.vtkImageData() - # apply scale factor to sliders in the viewer3D dock widget: - logging.debug("scale_factor = {}".format(scale_factor)) - self.scale_factor = scale_factor - self.frames[1].getToolbar().scale_factor = scale_factor - logging.debug(f"Setting maximum opacity for volume render to: {max_opacity}") - self.frames[1].viewer.setMaximumOpacity(max_opacity) - def addToMenu(self): ''' Adds actions to the menu bar for selecting the images to be displayed @@ -138,7 +127,7 @@ def set_up(self, title, viewer1_type, viewer2_type=None, *args, **kwargs): if None, only one viewer is displayed ''' - window = StandaloneViewerMainWindow(title, viewer1_type, viewer2_type, *args, **kwargs) + window = StandaloneViewerMainWindow(title, viewer1_type, viewer2_type) self.window = window self.has_run = None @@ -160,34 +149,14 @@ def __del__(self): def main(): - - parser = argparse.ArgumentParser(description='Standalone CIL Viewer') - - parser.add_argument('--debug', type=str) - parser.add_argument('--scale_factor', type=float, default=1) - parser.add_argument('--max_opacity', type=float, default=0.1) - args = parser.parse_args() - - if args.debug in ['debug', 'info', 'warning', 'error', 'critical']: - level = eval(f'logging.{args.debug.upper()}') - logging.basicConfig(level=level) - logging.info(f"cilviewer: Setting debugging level to {args.debug.upper()}") - - logging.debug(f"scale_factor: {args.scale_factor}") - logging.debug(f"max_opacity: {args.max_opacity}") - # Run a standalone viewer with a 2D and a 3D viewer: err = vtk.vtkFileOutputWindow() err.SetFileName("viewer.log") vtk.vtkOutputWindow.SetInstance(err) - standalone_viewer_instance = standalone_viewer("Standalone Viewer", - viewer1_type='2D', - viewer2_type='3D', - scale_factor=args.scale_factor, - max_opacity=args.max_opacity) + standalone_viewer_instance = standalone_viewer("Standalone Viewer", viewer1_type='2D', viewer2_type='3D') standalone_viewer_instance.show() return 0 if __name__ == '__main__': - main() + main() \ No newline at end of file diff --git a/Wrappers/Python/ccpi/viewer/utils/colormaps.py b/Wrappers/Python/ccpi/viewer/utils/colormaps.py index 7b8c8944..c60bec1e 100644 --- a/Wrappers/Python/ccpi/viewer/utils/colormaps.py +++ b/Wrappers/Python/ccpi/viewer/utils/colormaps.py @@ -411,7 +411,7 @@ def relu(x, xmin, xmax, scaling=1): returns values as 1. x< xmin : f(x) = 0 2. xmin <= x <= xmax : f(x) = (x - xmin) / (xmax - xmin) - 3. x > xmax: f(x) = 1 + 3. x > xmax: f(x) = 0 :param x: ndarray to evaluate the function at :param xmin: value at which the function start increasing @@ -422,10 +422,8 @@ def relu(x, xmin, xmax, scaling=1): out = [] dx = xmax - xmin for i, val in enumerate(x): - if val < xmin: + if val < xmin or val > xmax: out.append(0) - elif val > xmax: - out.append(scaling) else: out.append(scaling * ((val - xmin) / dx)) return numpy.asarray(out) From b5096142bfaec39894a2448980b93033d1bfefe9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 18 Dec 2024 15:56:40 +0000 Subject: [PATCH 28/35] Automated autoyapf fixes --- Wrappers/Python/ccpi/viewer/ui/dialogs.py | 22 +++++++++++----------- Wrappers/Python/test/test_ui_dialogs.py | 11 +++++------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/dialogs.py b/Wrappers/Python/ccpi/viewer/ui/dialogs.py index f8b8115a..cba79ea1 100644 --- a/Wrappers/Python/ccpi/viewer/ui/dialogs.py +++ b/Wrappers/Python/ccpi/viewer/ui/dialogs.py @@ -295,11 +295,12 @@ def preview(self): # finally open the dialog diag.open() + class SaveableRawInputDialog(RawInputDialog): + def __init__(self, parent, fname, qsettings): super(SaveableRawInputDialog, self).__init__(parent, fname) - self.settings = qsettings self.formWidget.addSpanningWidget(QCheckBox("Enable editing"), 'enable_edit') @@ -316,7 +317,6 @@ def __init__(self, parent, fname, qsettings): load_button.clicked.connect(self._load_settings) self.formWidget.addSpanningWidget(load_button, 'load') - self.buttonBox.addButton(QtWidgets.QDialogButtonBox.Save) self.buttonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._open_save_dialog) @@ -326,9 +326,12 @@ def _change_edit_state(self, editable=True): widgets = self.getWidgets() for widget in widgets.values(): widget.setEnabled(editable) - + if not editable: - widgets_to_preserve = ['load_name', 'load', 'enable_edit', 'load_name', 'load_settings_title', 'preview_slice', 'preview_button'] + widgets_to_preserve = [ + 'load_name', 'load', 'enable_edit', 'load_name', 'load_settings_title', 'preview_slice', + 'preview_button' + ] for widget in widgets_to_preserve: self.getWidget(widget, 'field').setEnabled(True) try: @@ -336,7 +339,6 @@ def _change_edit_state(self, editable=True): except: pass - def _open_save_dialog(self): '''Opens dialog for specifiying name to save settings under''' dialog = FormDialog(self) @@ -348,26 +350,25 @@ def _open_save_dialog(self): dialog.Ok.clicked.connect(self._save_settings) dialog.Ok.clicked.connect(dialog.close) dialog.open() - self.save_dialog=dialog + self.save_dialog = dialog def _get_settings_save_name(self): return self.save_dialog.getWidget('save_name').text() - def _save_settings(self): ''' Adds a dictionary to the qsettings 'raw_dialog' dictionary : key: name entered by the user value: the status of all widgets on the form - ''' + ''' settings_dict = self.settings.value('raw_dialog', {}) settings_name = self._get_settings_save_name() self.saveAllWidgetStates() current_widget_status = self.getSavedWidgetStates() - + settings_dict[settings_name] = current_widget_status self.settings.setValue('raw_dialog', settings_dict) @@ -398,7 +399,7 @@ def _load_settings(self): if state is not None: # save current stae - + self.applyWidgetStates(state) self.getWidget('enable_edit').setChecked(False) @@ -410,7 +411,6 @@ def _load_settings(self): print("Settings not found") - class HDF5InputDialog(FormDialog): ''' This is a dialog window which allows the user to set: diff --git a/Wrappers/Python/test/test_ui_dialogs.py b/Wrappers/Python/test/test_ui_dialogs.py index e97917ed..55bbc928 100644 --- a/Wrappers/Python/test/test_ui_dialogs.py +++ b/Wrappers/Python/test/test_ui_dialogs.py @@ -207,6 +207,7 @@ def test_getRawAttrs(self): 'preview_slice': 0 } + @unittest.skipIf(skip_as_conda_build, "On conda builds do not do any test with interfaces") class TestSaveableRawInputDialog(unittest.TestCase): @@ -218,7 +219,6 @@ def setUp(self): self.settings = QSettings() self.fname = "test.raw" - @patch("ccpi.viewer.ui.dialogs.RawInputDialog.__init__") def test_init_calls_raw_input_dialog_init(self, mock_init_call): mock_init_call.return_value = partial(FormDialog.__init__) @@ -239,8 +239,7 @@ def test_save_settings_when_nothing_in_qsettings(self, mock_get_name): rdi._save_settings() the_dict = empty_settings.value('raw_dialog') self.assertEqual(empty_settings.allKeys(), ['raw_dialog']) - self.assertEqual( the_dict, {'my_name': rdi.getAllWidgetStates()}) - + self.assertEqual(the_dict, {'my_name': rdi.getAllWidgetStates()}) @patch("ccpi.viewer.ui.dialogs.SaveableRawInputDialog._get_settings_save_name") def test_save_settings_when_soemthing_in_qsettings(self, mock_get_name): @@ -250,7 +249,7 @@ def test_save_settings_when_soemthing_in_qsettings(self, mock_get_name): rdi = SaveableRawInputDialog(self.parent, self.fname, pop_settings) rdi._save_settings() the_dict = pop_settings.value('raw_dialog') - self.assertEqual( the_dict, {'hi': "I'm not empty", 'my_name_2': rdi.getAllWidgetStates()}) + self.assertEqual(the_dict, {'hi': "I'm not empty", 'my_name_2': rdi.getAllWidgetStates()}) @patch("ccpi.viewer.ui.dialogs.SaveableRawInputDialog._get_name_of_state_to_load") def test_load_settings(self, mock_get_name): @@ -261,13 +260,13 @@ def test_load_settings(self, mock_get_name): example_settings = rdi.getAllWidgetStates() example_qsettings = QSettings('B', 'B') - + example_qsettings.setValue('raw_dialog', {'state': example_settings}) rdi2 = SaveableRawInputDialog(self.parent, self.fname, example_qsettings) rdi2._load_settings() self.assertEqual(rdi2.getWidget('dim_Images').text(), "10") - + if __name__ == '__main__': unittest.main() From 33219adb908b65ecbc938c1ef3b17c5b467e0d01 Mon Sep 17 00:00:00 2001 From: Murgatroyd Date: Wed, 18 Dec 2024 15:59:45 +0000 Subject: [PATCH 29/35] reverting other files to master --- .../Python/ccpi/viewer/QCILViewer3DToolBar.py | 116 ------- Wrappers/Python/ccpi/viewer/ui/helpers.py | 52 --- .../examples/programmatic_volume_render.py | 307 ------------------ 3 files changed, 475 deletions(-) delete mode 100644 Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py delete mode 100644 Wrappers/Python/ccpi/viewer/ui/helpers.py delete mode 100644 Wrappers/Python/examples/programmatic_volume_render.py diff --git a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py b/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py deleted file mode 100644 index 5d0a4e0a..00000000 --- a/Wrappers/Python/ccpi/viewer/QCILViewer3DToolBar.py +++ /dev/null @@ -1,116 +0,0 @@ -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 - self.scale_factor = kwargs.get('scale_factor', 1.0) - - 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", scale_factor=self.scale_factor) - 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", - scale_factor=self.scale_factor) - 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/ui/helpers.py b/Wrappers/Python/ccpi/viewer/ui/helpers.py deleted file mode 100644 index b59295f3..00000000 --- a/Wrappers/Python/ccpi/viewer/ui/helpers.py +++ /dev/null @@ -1,52 +0,0 @@ -# from vtkmodules.util import colors -try: - import vtkmodules.all as vtk - # from vtkmodules.util import colors -except ImportError: - import vtk -from vtk.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/examples/programmatic_volume_render.py b/Wrappers/Python/examples/programmatic_volume_render.py deleted file mode 100644 index cea5b7be..00000000 --- a/Wrappers/Python/examples/programmatic_volume_render.py +++ /dev/null @@ -1,307 +0,0 @@ -from ccpi.viewer.CILViewer import CILViewer as viewer3D -import glob -import vtk -import numpy as np -from ccpi.viewer.utils.io import ImageReader, \ - cilTIFFResampleReader -from ccpi.viewer.utils.conversion import cilNumpyResampleReader, \ - cilHDF5ResampleReader,\ - cilRawResampleReader - -import os, sys -import argparse - - -def createAnimation(viewer, - FrameCount=20, - InitialCameraPosition=None, - FocalPoint=None, - ClippingRange=None, - AngleRange=360, - ViewUp=None, - clip_plane=False, - fname_offset=0, - fname_prefix='test', - output_dir='.'): - - viewer - - if InitialCameraPosition is None: - InitialCameraPosition = viewer.getCamera().GetPosition() - if FocalPoint is None: - FocalPoint = viewer.getCamera().GetFocalPoint() - if ClippingRange is None: - ClippingRange = (0, 2000) - if ViewUp is None: - ViewUp = (0, 0, 1) - if FrameCount is None: - FrameCount = 100 - #Setting locked values for camera position - locX = InitialCameraPosition[0] - locY = InitialCameraPosition[1] - locZ = InitialCameraPosition[2] - - print('Initial Camera Position: {}'.format(InitialCameraPosition)) - #Setting camera position - viewer.getCamera().SetPosition(InitialCameraPosition) - viewer.getCamera().SetFocalPoint(FocalPoint) - - #Setting camera viewup - viewer.getCamera().SetViewUp(ViewUp) - - #Set camera clipping range - viewer.getCamera().SetClippingRange(ClippingRange) - - #Defining distance from camera to focal point - r = np.sqrt(((InitialCameraPosition[0] - FocalPoint[0])**2) + (InitialCameraPosition[1] - FocalPoint[1])**2) - print('Radius (distance from camera to focal point): {}'.format(r)) - - if not clip_plane: - viewer.style.ToggleSliceVisibility() - - #Animating the camera - for x in range(FrameCount): - if clip_plane: - # move the slice during rotation - new_slice = round(x / FrameCount * viewer.img3D.GetDimensions()[2]) - print('displaying slice {}'.format(new_slice)) - viewer.style.SetActiveSlice(new_slice) - viewer.updatePipeline(False) - # plane on the slice plane - plane = vtk.vtkPlane() - plane.SetOrigin(0, 0, new_slice * viewer.img3D.GetSpacing()[2]) - plane.SetNormal(0, 0, -1) - - viewer.volume.GetMapper().RemoveAllClippingPlanes() - viewer.volume.GetMapper().AddClippingPlane(plane) - viewer.volume.Modified() - viewer.getRenderer().Render() - - angle = (2 * np.pi) * (x / FrameCount) - NewLocationX = r * np.sin(angle) + FocalPoint[0] - NewLocationY = r * np.cos(angle) + FocalPoint[1] - NewLocation = (NewLocationX, NewLocationY, locZ) - camera = vtk.vtkCamera() - camera.SetFocalPoint(FocalPoint) - camera.SetViewUp(ViewUp) - camera.SetPosition(*NewLocation) - viewer.ren.SetActiveCamera(camera) - viewer.adjustCamera() - - import time - time.sleep(0.1) - print("render frame {} angle {}".format(x, angle)) - print('Camera Position: {}'.format(NewLocation)) - rp = np.sqrt(((NewLocation[0] - FocalPoint[0])**2) + (NewLocation[1] - FocalPoint[1])**2) - print('Camera trajectory radius {}'.format(rp)) - #Rendering and saving the render - viewer.getRenderer().Render() - viewer.renWin.Render() - saveRender(viewer, x + fname_offset, fname_prefix, directory=output_dir) - - -def saveRender(viewer, number, file_prefix, directory='.'): - w2if = vtk.vtkWindowToImageFilter() - w2if.SetInput(viewer.renWin) - w2if.Update() - - saveFilename = '{}_{:04d}.png'.format(os.path.join(directory, file_prefix), number) - - writer = vtk.vtkPNGWriter() - writer.SetFileName(saveFilename) - writer.SetInputConnection(w2if.GetOutputPort()) - writer.Write() - - -class Readers(): - - @staticmethod - def get_reader(filename, dimensions=None, data_path=None): - if os.path.isdir(filename): - # should return and TIFF reader - return Readers._return_tiff_reader(filename) - else: - if data_path is not None: - - # HDF5 file - import h5py - with h5py.File(filename, 'r') as f: - f.items() - reader = cilHDF5ResampleReader() - reader.SetFileName(filename) - reader.SetDatasetName(data_path) - return reader - - elif dimensions is not None: - reader = cilRawResampleReader() - reader.SetFileName(filename) - reader.SetStoredArrayShape(dimensions) - return reader - else: - # Figure out what file we have - # 2) npy - from ccpi.viewer.utils.conversion import parseNpyHeader - try: - header = parseNpyHeader(filename) - if header['type'] == 'NUMPY': - ret = cilNumpyResampleReader() - ret.SetFileName(filename) - return ret - except Exception as err: - pass - - # 3) mha/mhd - try: - from ccpi.viewer.utils.conversion import cilMetaImageResampleReader - reader = cilMetaImageResampleReader() - reader.SetFileName(filename) - - reader.ReadMetaImageHeader() - return reader - except Exception as err: - pass - - # 1) raw file - - @staticmethod - def _return_tiff_reader(dirname): - fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] - - print("#########################") - print(dirname, fnames) - - if len(fnames) == 0: - # try again with .tif extension - fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] - raise ValueError(f"Directory {dirname} does not contain tiff files with extension tiff or tif") - reader = cilTIFFResampleReader() - reader.SetFileName(fnames) - return reader - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Create awesome volume render') - - parser.add_argument('--verbose', type=int, default=0) - parser.add_argument('--volume', type=str, default='scalar') - parser.add_argument('--max_opacity', type=float, default=0.1) - parser.add_argument('--spacing', type=float, nargs=3, default=None) - parser.add_argument('--spacing_multiplier', type=float, nargs=3, default=None) - parser.add_argument('--max_size', type=int, nargs=3, default=[1024, 1024, 1024]) - parser.add_argument('--resample_z', type=bool, default=True) - parser.add_argument('--colormap', type=str, default='viridis') - parser.add_argument('--num_frames', type=int, default=10) - parser.add_argument('--output_dir', type=str, default='.') - parser.add_argument('--output_prefix', type=str, default='awesome') - parser.add_argument('--dimensions', type=int, nargs=3, default=None) - parser.add_argument('--data_path', type=str, default=None) - parser.add_argument('--camera_position', type=float) - - parser.add_argument('input', help='File or directory') - args = parser.parse_args() - - print(args) - - # print ("################### ", len(sys.argv)) - # exit(0) - - from ccpi.viewer.CILViewer import CILViewer as viewer3D - from ccpi.viewer.utils.io import ImageReader, cilHDF5ResampleReader, cilTIFFResampleReader - import vtk - - v = viewer3D(debug=True if args.verbose > 0 else False) - - # detect the file format and return the appropriate reader - - # reader = ImageReader(r"C:\Users\ofn77899\Data\dvc/frame_000_f.npy", resample=False) - # reader = ImageReader(r"{}/head_uncompressed.mha".format(os.path.dirname(__file__)), resample=False) - # data = reader.Read() - - pokemon = args.output_prefix - - # subdir = 'Not_Angled' - - reader = Readers.get_reader(args.input, dimensions=args.dimensions, data_path=args.data_path) - - # dirname = os.path.abspath("C:/Users/ofn77899/Data\HOW/{}/{}/".format(pokemon, subdir)) - - # fnames = [el for el in glob.glob(os.path.join(dirname, "*.tiff"))] - - # print ("#########################") - # print (dirname, fnames) - - # if len(fnames) == 0: - # exit(1) - - # reader = cilTIFFResampleReader() - # reader.SetFileName(fnames) - reader.SetTargetSize(1024 * 1024 * 1024) - reader.Update() - data = reader.GetOutput() - - print(data.GetSpacing()) - - if args.spacing is not None: - data.SetSpacing(*args.spacing) - elif args.spacing_multiplier is not None: - dims = data.GetDimensions() - spac = list(data.GetSpacing()) - # spac[2] = dims[0]/dims[2] - # spac[2] *= 0.75 - for i in range(3): - spac[i] *= args.spacing_multiplier[i] - data.SetSpacing(*spac) - print(data.GetSpacing()) - - v.setInputData(reader.GetOutput()) - - v.style.ToggleVolumeVisibility() - - v.setVolumeColorMapName('viridis') - - # define colors and opacity with default values - colors, opacity = v.getColorOpacityForVolumeRender() - - v.volume_property.SetColor(colors) - - method = 'scalar' - if method == 'gradient': - color_percentiles = (65., 98.) - gradient_opacity_percentiles = (75., 99.9) - max_opacity = 0.1 - v.setVolumeRenderOpacityMethod('gradient') - v.setGradientOpacityPercentiles(*gradient_opacity_percentiles, update_pipeline=False) - v.volume_property.SetGradientOpacity(opacity) - else: - color_percentiles = (75., 99.) - scalar_opacity_percentiles = (94., 99.5) - max_opacity = 0.1 - v.setVolumeRenderOpacityMethod('scalar') - v.setScalarOpacityPercentiles(*scalar_opacity_percentiles, update_pipeline=False) - v.volume_property.SetScalarOpacity(opacity) - - v.setVolumeColorPercentiles(*color_percentiles, update_pipeline=False) - v.setMaximumOpacity(max_opacity) - - # default background - # self.ren.SetBackground(.1, .2, .4) - # v.ren.SetBackground(0, 0, 0) - v.ren.SetBackground(1, 1, 1) - - createAnimation(v, - FrameCount=args.num_frames, - InitialCameraPosition=((483.8653687626969, -2173.282759469902, 1052.4208133258792)), - AngleRange=360, - clip_plane=True, - fname_offset=0, - fname_prefix=pokemon, - output_dir="C:/Users/ofn77899/Data/HOW/Picachu/renders") - - createAnimation(v, - FrameCount=args.num_frames, - InitialCameraPosition=((483.8653687626969, -2173.282759469902, 1052.4208133258792)), - AngleRange=360, - clip_plane=False, - fname_offset=100, - fname_prefix=pokemon, - output_dir=r'C:\Users\ofn77899\Data\HOW\Picachu\renders') From 58d3ae7f12706d0b10b2fe9e88b473dad7f1c028 Mon Sep 17 00:00:00 2001 From: Murgatroyd Date: Wed, 18 Dec 2024 16:03:37 +0000 Subject: [PATCH 30/35] reverting other files to master --- CHANGELOG.md | 7 +- Wrappers/Python/ccpi/viewer/CILViewer.py | 43 ++- .../Python/ccpi/viewer/ui/SettingsDialog.py | 164 ------------ .../viewer/ui/VolumeRenderSettingsDialog.py | 253 ------------------ 4 files changed, 25 insertions(+), 442 deletions(-) delete mode 100644 Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py delete mode 100644 Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0957578e..8e9a867c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,16 @@ # Changelog -## v24.0.2 +## v24.1.0 Enhancements: + - Add option to edit help text #441 - Update corner annotation with axis label #433 - Add title to ViewerCoordsDockWidget #422 - Adds methods to CILviewer and CILviewer2D #425 - - adds Qt GUI toolbar to control 3D viewer Bugfix: + - Fix error when visualising 2D images #439 + - Hides the slider when one image dimension is 1 #432 - Differentiate 3D and 2D images in the converter `numpy2vtkImage` #437 - Edit slider min value #420 - Fix extent error when user clicks in the viewer to create a box by clicking outside of the image #425 @@ -50,7 +52,6 @@ Documentation - Add PR template #244 #373 - Edit README.md #344 - ## v23.1.0 - Raise error if try to add multiple widgets with the same name to CILViewer or CILViewer2D. - Adds the following base classes to `ui.main_windows.py`: diff --git a/Wrappers/Python/ccpi/viewer/CILViewer.py b/Wrappers/Python/ccpi/viewer/CILViewer.py index 5ac166fb..e61c7478 100644 --- a/Wrappers/Python/ccpi/viewer/CILViewer.py +++ b/Wrappers/Python/ccpi/viewer/CILViewer.py @@ -37,6 +37,7 @@ def __init__(self, callback): #self.AddObserver('RightButtonPressEvent', self.OnRightMousePress, -0.5) #self.AddObserver('RightButtonReleaseEvent', self.OnRightMouseRelease, -0.5) self.htext = None + self.htext = None def GetSliceOrientation(self): return self._viewer.sliceOrientation @@ -324,7 +325,7 @@ def DisplayHelp(self): self.Render() return - font_size = 16 + font_size = 24 # Create the text mappers and the associated Actor2Ds. @@ -341,27 +342,25 @@ def DisplayHelp(self): # The text is on multiple lines and center-justified (both horizontal and # vertical). textMapperC = vtk.vtkTextMapper() - if self.htext == None: - self.htext = """ -Mouse Interactions: - - Slice: Mouse Scroll - - Zoom: Right Mouse + Move Up/Down - - Pan: Middle Mouse Button + Move or Shift + Left Mouse + Move - - Adjust Camera: Left Mouse + Move - - Rotate: Ctrl + Left Mouse + Move - -Keyboard Interactions: - h: Display this help - x: YZ Plane - y: XZ Plane - z: XY Plane - r: Save render to current_render.png - s: Toggle visibility of slice - v: Toggle visibility of volume render - c: Activates volume render clipping plane widget - a: Whole image Auto Window/Level - """ - textMapperC.SetInput(self.htext) + textMapperC.SetInput("Mouse Interactions:\n" + "\n" + " - Slice: Mouse Scroll\n" + " - Zoom: Right Mouse + Move Up/Down\n" + " - Pan: Middle Mouse Button + Move or Shift + Left Mouse + Move\n" + " - Adjust Camera: Left Mouse + Move\n" + " - Rotate: Ctrl + Left Mouse + Move\n" + "\n" + "Keyboard Interactions:\n" + "\n" + "h: Display this help\n" + "x: YZ Plane\n" + "y: XZ Plane\n" + "z: XY Plane\n" + "r: Save render to current_render.png\n" + "s: Toggle visibility of slice\n" + "v: Toggle visibility of volume render\n" + "c: Activates volume render clipping plane widget\n" + "a: Whole image Auto Window/Level\n") tprop = textMapperC.GetTextProperty() tprop.ShallowCopy(multiLineTextProp) tprop.SetJustificationToLeft() diff --git a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py b/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py deleted file mode 100644 index d2466738..00000000 --- a/Wrappers/Python/ccpi/viewer/ui/SettingsDialog.py +++ /dev/null @@ -1,164 +0,0 @@ -import os - -from eqt.ui import FormDialog, UISliderWidget -from PySide2 import QtCore, QtWidgets - -try: - import vtkmodules.all as vtk - # from vtkmodules.util import colors -except ImportError: - import vtk -from vtk.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, scale_factor=1): - 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 - self.scale_factor = scale_factor - slice_window_label = QtWidgets.QLabel("Slice Window") - slice_window_slider = UISliderWidget.UISliderWidget(slice_window_label, scale_factor=1 / scale_factor) - 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, scale_factor=1 / scale_factor) - 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(0, 100) - self.getWidget("slice_window_slider").setTickInterval(100 / 10) - window_default = self.viewer.getSliceColorWindow() - self.getWidget("slice_window_slider").setValue((window_default - window_min) / (window_max - window_min) * 100) - self.getWidget("slice_window_slider").sliderReleased.connect(lambda: self.viewer.setSliceColorWindow( - window_min + self.getWidget("slice_window_slider").value() / 100 * (window_max - window_min))) - - # Level window sliders - level_min, level_max = self.viewer.getImageMapRange((0.0, 100.0), "scalar") - self.getWidget("slice_level_slider").setRange(0, 100) - self.getWidget("slice_level_slider").setTickInterval(100 / 10) - level_default = self.viewer.getSliceColorLevel() - - self.getWidget("slice_level_slider").setValue((level_default - level_min) / (level_max - level_min) * 100) - self.getWidget("slice_level_slider").sliderReleased.connect(lambda: self.viewer.setSliceColorLevel( - level_min + self.getWidget("slice_level_slider").value() / 100 * (level_max - level_min))) - - # 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 deleted file mode 100644 index 25a1df7d..00000000 --- a/Wrappers/Python/ccpi/viewer/ui/VolumeRenderSettingsDialog.py +++ /dev/null @@ -1,253 +0,0 @@ -from eqt.ui import FormDialog, UISliderWidget -from PySide2 import QtCore, QtWidgets, QtGui - -from ccpi.viewer.ui.helpers import color_scheme_list - - -class VolumeRenderSettingsDialog(FormDialog): - """Volume render settings dialogue.""" - - def __init__(self, parent=None, title=None, scale_factor=1): - 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") - - self.scale_factor = scale_factor - # Windowing min - windowing_label_min = QtWidgets.QLabel("Windowing min") - windowing_slider_min = UISliderWidget.UISliderWidget(windowing_label_min, scale_factor=1 / scale_factor) - 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, scale_factor=1 / scale_factor) - 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, scale_factor=1 / scale_factor) - 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, scale_factor=1 / scale_factor) - self.addWidget(color_range_slider_max, "Color range max", "color_range_slider_max") - self.addWidget(color_range_label_max, "", "color_range_label_max") - - # Max opacity - max_opacity_label = QtWidgets.QLabel("Max opacity") - max_opacity_input = QtWidgets.QDoubleSpinBox(self.groupBox) - self.addWidget(max_opacity_input, max_opacity_label, "max_opacity_input") - - # 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.scale_factor) - self.getWidget("color_range_slider_min").setTickInterval(10 * self.scale_factor) - self.getWidget("color_range_slider_min").setValue(85 * self.scale_factor) - self.getWidget("color_range_slider_min").sliderReleased.connect(self.change_color_range_min) - - # Color range slider max - self.getWidget("color_range_slider_max").setRange(0, 100 * self.scale_factor) - self.getWidget("color_range_slider_max").setTickInterval(10 * self.scale_factor) - self.getWidget("color_range_slider_max").setValue(95 * self.scale_factor) - self.getWidget("color_range_slider_max").sliderReleased.connect(self.change_color_range_max) - - # Windowing slider min - self.getWidget("windowing_slider_min").setRange(0, 100 * self.scale_factor) - self.getWidget("windowing_slider_min").setTickInterval(10 * self.scale_factor) - self.getWidget("windowing_slider_min").setValue(80 * self.scale_factor) - self.getWidget("windowing_slider_min").sliderReleased.connect(self.change_volume_opacity_min) - - # Windowing slider max - self.getWidget("windowing_slider_max").setRange(0, 100 * self.scale_factor) - self.getWidget("windowing_slider_max").setTickInterval(10 * self.scale_factor) - self.getWidget("windowing_slider_max").setValue(99 * self.scale_factor) - self.getWidget("windowing_slider_max").sliderReleased.connect(self.change_volume_opacity_max) - - # MaxOpacity slider max - self.getWidget("max_opacity_input").setRange(0, 1) - self.getWidget("max_opacity_input").setDecimals(3) - self.getWidget("max_opacity_input").setValue(viewer.style.GetVolumeRenderParameters()['max_opacity']) - self.getWidget("max_opacity_input").valueChanged.connect(self.change_volume_max_opacity) - - 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.scale_factor, - self.getWidget("color_range_slider_max").value() / self.scale_factor) - - 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.scale_factor, - self.getWidget("windowing_slider_max").value() / self.scale_factor, - ) - if opacity == "Gradient": - self.viewer.setGradientOpacityPercentiles(opacity_min, opacity_max) - elif opacity == "Scalar": - self.viewer.setScalarOpacityPercentiles(opacity_min, opacity_max) - - def change_volume_max_opacity(self): - """Change the volume opacity mapping max value.""" - mo = self.getWidget("max_opacity_input").value() - self.viewer.setMaximumOpacity(mo) - - 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() From c2487e9966d43a1cefd63c3cdf64fac2a3d61413 Mon Sep 17 00:00:00 2001 From: Murgatroyd Date: Wed, 18 Dec 2024 16:10:24 +0000 Subject: [PATCH 31/35] Update change log --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9a867c..9ce8408f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## vx.x.x +Enhancements: + - Add `SaveableRawInputDialog` #444 + - Standalone viewer uses `SaveableRawInputDialog` - this allows user to save and reload settings for loading raw data + ## v24.1.0 Enhancements: From e348aa246f5b56d48e2e13f5c1ad2e2e0baba88a Mon Sep 17 00:00:00 2001 From: Murgatroyd Date: Wed, 18 Dec 2024 16:13:58 +0000 Subject: [PATCH 32/35] Rename checkbox --- Wrappers/Python/ccpi/viewer/ui/dialogs.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/dialogs.py b/Wrappers/Python/ccpi/viewer/ui/dialogs.py index cba79ea1..8e42c2ee 100644 --- a/Wrappers/Python/ccpi/viewer/ui/dialogs.py +++ b/Wrappers/Python/ccpi/viewer/ui/dialogs.py @@ -297,13 +297,28 @@ def preview(self): class SaveableRawInputDialog(RawInputDialog): + ''' + This is a dialog window which allows the user to set information + for a raw file, including: + - dimensionality + - size of dimensions + - data type + - endianness + - fortran ordering + + The dialog can let the user preview the data and verify that it is correct. + + The dialog allows you to save load settings under a memorable name. + You can reload settings you have saved previously, by selecting their associated name from a dropdown. + ''' + def __init__(self, parent, fname, qsettings): super(SaveableRawInputDialog, self).__init__(parent, fname) self.settings = qsettings - self.formWidget.addSpanningWidget(QCheckBox("Enable editing"), 'enable_edit') + self.formWidget.addSpanningWidget(QCheckBox("Edit Parameters"), 'enable_edit') self.getWidget('enable_edit').setChecked(True) self.getWidget('enable_edit').stateChanged.connect(self._change_edit_state) @@ -388,6 +403,8 @@ def _load_settings(self): ''' Load all of the widget states saved in the 'raw_dialog' entry of self.settings under the name selected by the user from the load_name comobox + + Disable editing of parameters. ''' settings_found = False if self.settings.value('raw_dialog'): From 0828306a457a063090d44cc203ff624aca476f40 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 18 Dec 2024 16:14:30 +0000 Subject: [PATCH 33/35] Automated autoyapf fixes --- Wrappers/Python/ccpi/viewer/ui/dialogs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Wrappers/Python/ccpi/viewer/ui/dialogs.py b/Wrappers/Python/ccpi/viewer/ui/dialogs.py index 8e42c2ee..be2f968c 100644 --- a/Wrappers/Python/ccpi/viewer/ui/dialogs.py +++ b/Wrappers/Python/ccpi/viewer/ui/dialogs.py @@ -312,7 +312,6 @@ class SaveableRawInputDialog(RawInputDialog): You can reload settings you have saved previously, by selecting their associated name from a dropdown. ''' - def __init__(self, parent, fname, qsettings): super(SaveableRawInputDialog, self).__init__(parent, fname) From 91cfc4c89addad3ad95b1a59ed6dab29465dec8a Mon Sep 17 00:00:00 2001 From: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:49:08 +0000 Subject: [PATCH 34/35] Update conda_build_and_publish.yml --- .github/workflows/conda_build_and_publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/conda_build_and_publish.yml b/.github/workflows/conda_build_and_publish.yml index 06f0dbbe..23439559 100644 --- a/.github/workflows/conda_build_and_publish.yml +++ b/.github/workflows/conda_build_and_publish.yml @@ -12,7 +12,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: From 1f27e257aca96e4c71186ecf70808658e3f113ce Mon Sep 17 00:00:00 2001 From: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:49:29 +0000 Subject: [PATCH 35/35] Update docker_build_test_publish.yml --- .github/workflows/docker_build_test_publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_build_test_publish.yml b/.github/workflows/docker_build_test_publish.yml index b268e0d9..b7b4c886 100644 --- a/.github/workflows/docker_build_test_publish.yml +++ b/.github/workflows/docker_build_test_publish.yml @@ -6,7 +6,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3