diff --git a/.github/workflows/build_macos.yml b/.github/workflows/build_macos.yml index 65703c7..e27a5e4 100644 --- a/.github/workflows/build_macos.yml +++ b/.github/workflows/build_macos.yml @@ -50,6 +50,13 @@ jobs: pip install matplotlib==3.3.4 pip install --force-reinstall --no-cache-dir pyside6 + - name: Integration tests + env: + DISPLAY: ':99.0' + run: | + pip install pytest-qt pytest-cov + pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests + - name: Build software run: | pip install pyinstaller==5.13.2 @@ -72,8 +79,8 @@ jobs: - name: Make installer run: | git clone https://github.com/dbouget/quickpkg.git - quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.2.4-macOS.pkg - cp -r Raidionics-1.2.4-macOS.pkg dist/Raidionics-1.2.4-macOS-x86_64.pkg + quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.0-macOS.pkg + cp -r Raidionics-1.3.0-macOS.pkg dist/Raidionics-1.3.0-macOS-x86_64.pkg - name: Upload package uses: actions/upload-artifact@v4 diff --git a/.github/workflows/build_macos_arm.yml b/.github/workflows/build_macos_arm.yml index c3ecef4..55ff4c4 100644 --- a/.github/workflows/build_macos_arm.yml +++ b/.github/workflows/build_macos_arm.yml @@ -49,6 +49,16 @@ jobs: mkdir ${{github.workspace}}/ANTs mv ${{github.workspace}}/downloads/install ${{github.workspace}}/ANTs/ + - name: Integration tests + env: + DISPLAY: ':99.0' + run: | + cd ${{github.workspace}} + source tmp/venv/bin/activate + pip3 install pytest-qt pytest-cov + pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests + deactivate + - name: Build software run: | cd ${{github.workspace}} @@ -67,8 +77,8 @@ jobs: - name: Make installer run: | git clone https://github.com/dbouget/quickpkg.git - quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.2.4-macOS.pkg - cp -r Raidionics-1.2.4-macOS.pkg dist/Raidionics-1.2.4-macOS-arm64.pkg + quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.0-macOS.pkg + cp -r Raidionics-1.3.0-macOS.pkg dist/Raidionics-1.3.0-macOS-arm64.pkg - name: Upload package uses: actions/upload-artifact@v4 diff --git a/.github/workflows/build_ubuntu.yml b/.github/workflows/build_ubuntu.yml index 55b1373..8f4d997 100644 --- a/.github/workflows/build_ubuntu.yml +++ b/.github/workflows/build_ubuntu.yml @@ -77,6 +77,14 @@ jobs: pip uninstall -y PySide6 PySide6-Addons PySide6-Essentials pip install --force-reinstall --no-cache-dir pyside6==6.2.4 + - name: Integration tests + env: + DISPLAY: ':99.0' + run: | + pip install pytest-qt pytest-cov + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX + pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests --cov-report=xml + - name: Build software run: | pip install pyinstaller==5.13.2 @@ -108,11 +116,17 @@ jobs: cp -r dist/Raidionics assets/Raidionics_ubuntu/usr/local/bin dpkg-deb --build --root-owner-group assets/Raidionics_ubuntu ls -la - cp -r assets/Raidionics_ubuntu.deb dist/Raidionics-1.2.4-ubuntu.deb + cp -r assets/Raidionics_ubuntu.deb dist/Raidionics-1.3.0-ubuntu.deb - name: Upload package uses: actions/upload-artifact@v4 with: name: Package path: ${{github.workspace}}/dist/Raidionics-* - if-no-files-found: error \ No newline at end of file + if-no-files-found: error + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index 6354433..ecd02c6 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -46,6 +46,13 @@ jobs: pip install matplotlib==3.3.4 pip install --force-reinstall --no-cache-dir pyside6 +# - name: Integration tests +# env: +# DISPLAY: ':99.0' +# run: | +# pip install pytest-qt pytest-cov +# pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests + - name: Build software run: | pip install pyinstaller==5.13.2 @@ -62,7 +69,7 @@ jobs: - name: Make installer run: | makensis.exe assets/Raidionics.nsi - cp -r assets/Raidionics-1.2.4-win.exe dist/Raidionics-1.2.4-win.exe + cp -r assets/Raidionics-1.3.0-win.exe dist/Raidionics-1.3.0-win.exe - name: Upload package uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 352500f..3027409 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ quickpkg/ *.zip *.tar.gz assets/Raidionics_ubuntu/usr/local/bin +.coverage diff --git a/assets/Raidionics.nsi b/assets/Raidionics.nsi index 8ce92c4..4604a20 100644 --- a/assets/Raidionics.nsi +++ b/assets/Raidionics.nsi @@ -1,8 +1,8 @@ !define APP_NAME "Raidionics" !define COMP_NAME "SINTEF" -!define VERSION "1.2.2" +!define VERSION "1.3.0" !define DESCRIPTION "Application" -!define INSTALLER_NAME "Raidionics-1.2.4-win.exe" +!define INSTALLER_NAME "Raidionics-1.3.0-win.exe" !define MAIN_APP_EXE "Raidionics.exe" !define INSTALL_TYPE "SetShellVarContext current" !define REG_ROOT "HKLM" diff --git a/assets/Raidionics_ubuntu/DEBIAN/control b/assets/Raidionics_ubuntu/DEBIAN/control index 9bffdec..c9f4de6 100644 --- a/assets/Raidionics_ubuntu/DEBIAN/control +++ b/assets/Raidionics_ubuntu/DEBIAN/control @@ -1,5 +1,5 @@ Package: Raidionics -Version: 1.2.2 +Version: 1.3.0 Architecture: i386 Maintainer: David Bouget Description: Raidionics—Reporting and Data System. diff --git a/assets/main.spec b/assets/main.spec index 11c2ed3..407f0db 100644 --- a/assets/main.spec +++ b/assets/main.spec @@ -85,7 +85,7 @@ if sys.platform == "darwin": 'CFBundleIdentifier': 'Raidionics', 'CFBundleInfoDictionaryVersion': '6.0', 'CFBundleName': 'Raidionics', - 'CFBundleVersion': '1.2.4', + 'CFBundleVersion': '1.3.0', 'CFBundlePackageType': 'APPL', 'LSBackgroundOnly': 'false', }, diff --git a/assets/main_arm.spec b/assets/main_arm.spec index 7e4b880..0827698 100644 --- a/assets/main_arm.spec +++ b/assets/main_arm.spec @@ -84,7 +84,7 @@ if sys.platform == "darwin": 'CFBundleIdentifier': 'Raidionics', 'CFBundleInfoDictionaryVersion': '6.0', 'CFBundleName': 'Raidionics', - 'CFBundleVersion': '1.2.4', + 'CFBundleVersion': '1.3.0', 'CFBundlePackageType': 'APPL', 'LSBackgroundOnly': 'false', }, diff --git a/gui/RaidionicsMainWindow.py b/gui/RaidionicsMainWindow.py index 181f256..981fddc 100644 --- a/gui/RaidionicsMainWindow.py +++ b/gui/RaidionicsMainWindow.py @@ -46,13 +46,15 @@ class RaidionicsMainWindow(QMainWindow): reload_interface = Signal() new_patient_clicked = Signal(str) # Internal unique_id of the clicked patient - def __init__(self, application, *args, **kwargs): + def __init__(self, application=None, *args, **kwargs): super(RaidionicsMainWindow, self).__init__(*args, **kwargs) - self.app = application - self.app.setWindowIcon(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'Images/raidionics-icon.png'))) - self.app.setStyle("Fusion") # @TODO: Should we remove Fusion style? Looks strange on macOS + self.app = None + if application is not None: + self.app = application + self.app.setWindowIcon(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'Images/raidionics-icon.png'))) + self.app.setStyle("Fusion") # @TODO: Should we remove Fusion style? Looks strange on macOS self.logs_thread = LogReaderThread() self.logs_thread.start() self.__set_interface() @@ -70,9 +72,11 @@ def closeEvent(self, event): and SoftwareConfigResources.getInstance().get_active_patient_uid() \ and SoftwareConfigResources.getInstance().get_active_patient().has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 0: # Operation cancelled event.ignore() + if self.logs_thread.isRunning(): + self.logs_thread.stop() logging.info("Graceful exit.") def resizeEvent(self, event): @@ -86,11 +90,14 @@ def __on_exit_software(self) -> None: """ Mirroring of the closeEvent, for when the user press the Quit action in the main menu. """ - self.logs_thread.stop() + if self.logs_thread.isRunning(): + self.logs_thread.stop() + if not SoftwareConfigResources.getInstance().is_patient_list_empty()\ + and SoftwareConfigResources.getInstance().get_active_patient() is not None\ and SoftwareConfigResources.getInstance().get_active_patient().has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 1: # Operation approved logging.info("Graceful exit.") sys.exit() @@ -123,6 +130,10 @@ def __set_mainmenu_interface(self): 'Images/download-tray-icon.png')), 'Download test data', self) self.file_menu.addAction(self.download_example_data_action) + self.clear_scene_action = QAction(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'Images/trash-bin_icon.png')), 'Clear', self) + self.file_menu.addAction(self.clear_scene_action) + self.quit_action = QAction(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Images/power-icon.png')), 'Quit', self) self.quit_action.setShortcut("Ctrl+Q") @@ -360,12 +371,16 @@ def __set_menubar_connections(self): self.batch_mode_action.triggered.connect(self.__on_study_batch_clicked) self.settings_preferences_action.triggered.connect(self.__on_settings_preferences_clicked) # self.quit_action.triggered.connect(sys.exit) + self.clear_scene_action.triggered.connect(self.on_clear_scene) self.quit_action.triggered.connect(self.__on_exit_software) self.download_example_data_action.triggered.connect(self.__on_download_example_data) def __get_screen_dimensions(self): - screen = self.app.primaryScreen() - self.primary_screen_dimensions = screen.size() + if self.app is None: + self.primary_screen_dimensions = QSize(1200, 700) + else: + screen = self.app.primaryScreen() + self.primary_screen_dimensions = screen.size() logging.debug("Detected primary screen size [w: {}, h: {}]".format(self.primary_screen_dimensions.width(), self.primary_screen_dimensions.height())) @@ -439,7 +454,7 @@ def __on_study_batch_clicked(self): def __on_settings_preferences_clicked(self): patient_space = UserPreferencesStructure.getInstance().display_space diag = SoftwareSettingsDialog(self) - diag.exec_() + diag.exec() # Reloading the interface is mainly meant to perform a visual refreshment based on the latest user display choices # For now: changing the display space for viewing a patient images. @@ -464,11 +479,11 @@ def __on_patient_selected(self, patient_uid: str) -> None: def __on_community_action_triggered(self): popup = ResearchCommunityDialog(self) - popup.exec_() + popup.exec() def __on_about_action_triggered(self): popup = AboutDialog() - popup.exec_() + popup.exec() def __on_help_action_triggered(self) -> None: """ @@ -484,11 +499,11 @@ def __on_view_logs_triggered(self) -> None: Opens up a pop-up dialog allowing to read through the log file. """ diag = LogsViewerDialog(self) - diag.exec_() + diag.exec() def __on_shortcuts_action_triggered(self): popup = KeyboardShortcutsDialog(self) - popup.exec_() + popup.exec() def __on_save_file_triggered(self): if SoftwareConfigResources.getInstance().get_active_patient_uid() \ @@ -499,7 +514,7 @@ def __on_save_file_triggered(self): SoftwareConfigResources.getInstance().get_active_study().save() def __on_download_example_data(self): - QDesktopServices.openUrl(QUrl("https://drive.google.com/file/d/1W3klW_F7Rfge9-utczz9qp7uWh-pVPS1/view?usp=sharing")) + QDesktopServices.openUrl(QUrl("https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.zip")) def on_process_log_message(self, log_msg: str) -> None: """ @@ -510,7 +525,7 @@ def on_process_log_message(self, log_msg: str) -> None: if True in [x in log_msg for x in cases]: diag = QErrorMessage(self) diag.setWindowTitle("Error or warning identified!") - diag.showMessage(log_msg + "\nPlease visit the log file (Settings > Logs)") + diag.showMessage(log_msg + "

Please visit the log file (Settings > Logs)") diag.setMinimumSize(QSize(400, 150)) diag.exec() @@ -523,3 +538,11 @@ def standardOutputWritten(self, text): # self.singleuse_mode_widget.standardOutputWritten(text) elif self.central_stackedwidget.currentIndex() == 2: self.batch_mode_widget.standardOutputWritten(text) + + def on_clear_scene(self): + logging.info("[RaidionicsMainWindow] Interface clean-up. Removing all loaded patients and studies.") + self.batch_study_widget.on_clear_scene() + self.single_patient_widget.on_clear_scene() + SoftwareConfigResources.getInstance().reset() # <= Necessary for the integration tests not to crash... + if len(list(SoftwareConfigResources.getInstance().patients_parameters.keys())) > 0: + raise ValueError("[Software error] Existing patient IDs after clearing the scene!") \ No newline at end of file diff --git a/gui/SinglePatientComponent/CentralAreaExecutionWidget.py b/gui/SinglePatientComponent/CentralAreaExecutionWidget.py index 425dea9..5154067 100644 --- a/gui/SinglePatientComponent/CentralAreaExecutionWidget.py +++ b/gui/SinglePatientComponent/CentralAreaExecutionWidget.py @@ -34,6 +34,7 @@ def __init__(self, parent=None): super(CentralAreaExecutionWidget, self).__init__() self.parent = parent self.widget_name = "central_area_execution_widget" + self._tumor_type_diag = TumorTypeSelectionQDialog(self) self.__set_interface() self.__set_layout_dimensions() self.__set_stylesheets() @@ -147,33 +148,31 @@ def on_pipeline_execution(self, pipeline_code: str) -> None: """ self.model_name = "" if ("Classification" not in pipeline_code) and ("Brain" not in pipeline_code) and ("postop" not in pipeline_code) and ("Edema" not in pipeline_code) and ("Cavity" not in pipeline_code): - diag = TumorTypeSelectionQDialog(self) - code = diag.exec_() + code = self._tumor_type_diag.exec() if code == 0: # Operation cancelled return - if diag.tumor_type == 'Glioblastoma': + if self._tumor_type_diag.tumor_type == 'Glioblastoma': self.model_name = "MRI_GBM" - elif diag.tumor_type == 'Low-Grade Glioma': + elif self._tumor_type_diag.tumor_type == 'Low-Grade Glioma': self.model_name = "MRI_LGGlioma" - elif diag.tumor_type == 'Metastasis': + elif self._tumor_type_diag.tumor_type == 'Metastasis': self.model_name = "MRI_Metastasis" - elif diag.tumor_type == 'Meningioma': + elif self._tumor_type_diag.tumor_type == 'Meningioma': self.model_name = "MRI_Meningioma" if UserPreferencesStructure.getInstance().segmentation_tumor_model_type != "Tumor": self.model_name = self.model_name + '_multiclass' - if diag.tumor_type == 'Low-Grade Glioma': + if self._tumor_type_diag.tumor_type == 'Low-Grade Glioma': self.model_name = "MRI_GBM_multiclass" elif "postop" in pipeline_code: - diag = TumorTypeSelectionQDialog(self) - code = diag.exec_() + code = self._tumor_type_diag.exec() if code == 0: # Operation cancelled return - if diag.tumor_type == 'Glioblastoma': + if self._tumor_type_diag.tumor_type == 'Glioblastoma': self.model_name = "MRI_GBM_Postop_FV_4p" pipeline_code = pipeline_code + '_GBM' - elif diag.tumor_type == 'Low-Grade Glioma': + elif self._tumor_type_diag.tumor_type == 'Low-Grade Glioma': self.model_name = "MRI_LGGlioma_Postop" pipeline_code = pipeline_code + '_LGGlioma' elif "Brain" in pipeline_code: @@ -258,7 +257,7 @@ def assertion_input_compatible(self, tumor_type: str) -> bool: color: rgba(0, 0, 0, 1); background-color: rgba(255, 255, 255, 1); }""") - box.exec_() + box.exec() return False else: valid_ids = SoftwareConfigResources.getInstance().get_active_patient().get_all_mri_volumes_for_sequence_type(MRISequenceType.FLAIR) @@ -273,7 +272,7 @@ def assertion_input_compatible(self, tumor_type: str) -> bool: color: rgba(0, 0, 0, 1); background-color: rgba(255, 255, 255, 1); }""") - box.exec_() + box.exec() return False return True diff --git a/gui/SinglePatientComponent/CentralDisplayArea/CustomQGraphicsView.py b/gui/SinglePatientComponent/CentralDisplayArea/CustomQGraphicsView.py index c98cbac..eea61a9 100644 --- a/gui/SinglePatientComponent/CentralDisplayArea/CustomQGraphicsView.py +++ b/gui/SinglePatientComponent/CentralDisplayArea/CustomQGraphicsView.py @@ -178,7 +178,7 @@ def dropEvent(self, event): dialog.patient_imported.connect(self.patient_imported) dialog.setup_interface_from_files(entered_eligible_files) - code = dialog.exec_() + code = dialog.exec() # if code == QDialog.Accepted: diff --git a/gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/AnnotationSingleLayerWidget.py b/gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/AnnotationSingleLayerWidget.py index 30fa8ad..863cb2f 100644 --- a/gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/AnnotationSingleLayerWidget.py +++ b/gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/AnnotationSingleLayerWidget.py @@ -488,7 +488,7 @@ def adjustSize(self): self.resizeRequested.emit() def on_options_clicked(self, point): - self.options_menu.exec_(self.options_pushbutton.mapToGlobal(QPoint(0, 0))) + self.options_menu.exec(self.options_pushbutton.mapToGlobal(QPoint(0, 0))) def on_advanced_options_clicked(self): self.adjustSize() @@ -521,7 +521,7 @@ def __on_opacity_changed(self, value): self.opacity_value_changed.emit(self.uid, value) def __on_color_selector_clicked(self): - code = self.color_dialog.exec_() + code = self.color_dialog.exec() if code == QColorDialog.Accepted: color = self.color_dialog.currentColor() self.color_value_changed.emit(self.uid, color) diff --git a/gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/AnnotationsLayersInteractor.py b/gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/AnnotationsLayersInteractor.py index 4e20c53..735b65e 100644 --- a/gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/AnnotationsLayersInteractor.py +++ b/gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/AnnotationsLayersInteractor.py @@ -87,6 +87,17 @@ def reset(self): self.volumes_widget.pop(w) self.header.collapse() + def get_layer_widget_by_index(self, index: int) -> AnnotationSingleLayerWidget: + if index >= len(self.volumes_widget): + raise ValueError("[AnnotationsLayerInteractor] Trying to retrieve an Annotation layer widget with an out-of-bound index value.") + return self.volumes_widget[list(self.volumes_widget.keys())[index]] + + def get_layer_widget_by_visible_name(self, name: str) -> AnnotationSingleLayerWidget: + for w in list(self.volumes_widget.keys()): + if self.volumes_widget[w].visible_name == name: + return self.volumes_widget[w] + raise ValueError("[AnnotationsLayerInteractor] Trying to retrieve a non-existing Annotation layer widget by visible name with: {}.".format(name)) + def on_volume_view_toggled(self, volume_uid, state): """ @TODO. Might not be necessary, don't care about uid and state, just that the current annotations must be removed diff --git a/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasSingleLayerCollapsibleGroupBox.py b/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasSingleLayerCollapsibleGroupBox.py index 5788918..9bcf485 100644 --- a/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasSingleLayerCollapsibleGroupBox.py +++ b/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasSingleLayerCollapsibleGroupBox.py @@ -247,7 +247,7 @@ def on_visibility_toggled(self, state): self.visibility_toggled.emit(self.atlas_id, struct_ind, state) def __on_color_selector_clicked(self): - code = self.color_dialog.exec_() + code = self.color_dialog.exec() if code == QColorDialog.Accepted: color = self.color_dialog.currentColor() custom_color_str = "background-color:rgb({}, {}, {})".format(color.red(), color.green(), color.blue()) diff --git a/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasSingleLayerWidget.py b/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasSingleLayerWidget.py index bc6d54d..721686a 100644 --- a/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasSingleLayerWidget.py +++ b/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasSingleLayerWidget.py @@ -191,7 +191,7 @@ def adjustSize(self): self.resizeRequested.emit() def on_options_clicked(self, point): - self.options_menu.exec_(self.options_pushbutton.mapToGlobal(QPoint(0, 0))) + self.options_menu.exec(self.options_pushbutton.mapToGlobal(QPoint(0, 0))) def on_advanced_options_clicked(self): self.adjustSize() diff --git a/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasesLayersInteractor.py b/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasesLayersInteractor.py index 019cd34..63521b1 100644 --- a/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasesLayersInteractor.py +++ b/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasesLayersInteractor.py @@ -91,6 +91,17 @@ def reset(self): self.volumes_widget.pop(w) self.header.collapse() + def get_layer_widget_by_index(self, index: int) -> AtlasSingleLayerWidget: + if index >= len(self.volumes_widget): + raise ValueError("[AtlasesLayerInteractor] Trying to retrieve an Atlas layer widget with an out-of-bound index value.") + return self.volumes_widget[list(self.volumes_widget.keys())[index]] + + def get_layer_widget_by_visible_name(self, name: str) -> AtlasSingleLayerWidget: + for w in list(self.volumes_widget.keys()): + if self.volumes_widget[w].visible_name == name: + return self.volumes_widget[w] + raise ValueError("[AtlasesLayerInteractor] Trying to retrieve a non-existing Atlas layer widget by visible name with: {}.".format(name)) + def on_volume_view_toggled(self, volume_uid: str, state: bool) -> None: """ A change of the displayed MRI volume has been requested by the user, which should lead to an update of all diff --git a/gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/MRISeriesLayerWidget.py b/gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/MRISeriesLayerWidget.py index e8446f8..c092361 100644 --- a/gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/MRISeriesLayerWidget.py +++ b/gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/MRISeriesLayerWidget.py @@ -107,7 +107,7 @@ def __set_layout_dimensions(self): # logging.debug("MRISeriesLayerWidget size set to {}.\n".format(self.size())) def __set_connections(self): - self.display_name_lineedit.textEdited.connect(self.on_name_change) + self.display_name_lineedit.returnPressed.connect(self.on_name_change) self.display_toggle_radiobutton.toggled.connect(self.on_visibility_toggled) self.options_pushbutton.clicked.connect(self.on_options_clicked) self.sequence_type_combobox.currentTextChanged.connect(self.on_sequence_type_changed) @@ -311,7 +311,7 @@ def __on_display_dicom_metadata(self): QMessageBox.Ok) else: diag = DisplayMetadataDICOMDialog(dicom_tags) - diag.exec_() + diag.exec() def update_interface_from_external_toggle(self, state): """ @@ -343,19 +343,20 @@ def on_visibility_toggled(self, state): self.contrast_adjuster_pushbutton.setEnabled(self.display_toggle_radiobutton.isChecked()) logging.info("[MRISeriesLayerWidget] Visibility toggled to {} for {}".format(state, self.uid)) - def on_name_change(self, text): + def on_name_change(self): # @TODO. Should there be a check that the name is available? - SoftwareConfigResources.getInstance().get_active_patient().get_mri_by_uid(self.uid).display_name = text - self.display_name_changed.emit(self.uid, text) + new_name = self.display_name_lineedit.text() + SoftwareConfigResources.getInstance().get_active_patient().get_mri_by_uid(self.uid).display_name = new_name + self.display_name_changed.emit(self.uid, new_name) def on_sequence_type_changed(self, text) -> None: SoftwareConfigResources.getInstance().get_active_patient().get_mri_by_uid(self.uid).set_sequence_type(text) def on_contrast_adjustment_clicked(self): - self.contrast_adjuster.exec_() + self.contrast_adjuster.exec() def on_options_clicked(self, point): - self.options_menu.exec_(self.options_pushbutton.mapToGlobal(QPoint(0, 0))) + self.options_menu.exec(self.options_pushbutton.mapToGlobal(QPoint(0, 0))) def on_contrast_changed(self): self.contrast_changed.emit(self.uid) diff --git a/gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/MRIVolumesLayerInteractor.py b/gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/MRIVolumesLayerInteractor.py index af2a5b6..8998336 100644 --- a/gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/MRIVolumesLayerInteractor.py +++ b/gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/MRIVolumesLayerInteractor.py @@ -90,6 +90,17 @@ def reset(self) -> None: self.volumes_widget.pop(w) self.header.collapse() + def get_layer_widget_by_index(self, index: int) -> MRISeriesLayerWidget: + if index >= len(self.volumes_widget): + raise ValueError("[MRIVolumeLayerInteractor] Trying to retrieve an MRI Layer widget with an out-of-bound index value.") + return self.volumes_widget[list(self.volumes_widget.keys())[index]] + + def get_layer_widget_by_visible_name(self, name: str) -> MRISeriesLayerWidget: + for w in list(self.volumes_widget.keys()): + if self.volumes_widget[w].visible_name == name: + return self.volumes_widget[w] + raise ValueError("[TimestampsLayerInteractor] Trying to retrieve a non-existing MRI layer widget by visible name with: {}.".format(name)) + def on_mri_volume_import(self, uid: str) -> None: """ Default slot anytime a new MRI volume is added to the scene (i.e., on the current active patient) diff --git a/gui/SinglePatientComponent/LayersInteractorSidePanel/TimestampsInteractor/TimestampsLayerInteractor.py b/gui/SinglePatientComponent/LayersInteractorSidePanel/TimestampsInteractor/TimestampsLayerInteractor.py index 50b8c88..fecc7ed 100644 --- a/gui/SinglePatientComponent/LayersInteractorSidePanel/TimestampsInteractor/TimestampsLayerInteractor.py +++ b/gui/SinglePatientComponent/LayersInteractorSidePanel/TimestampsInteractor/TimestampsLayerInteractor.py @@ -312,6 +312,17 @@ def reset(self): # self.timestamp_add_pushbutton.setEnabled(False) # Unsure if it should be disabled or not. self.timestamp_remove_pushbutton.setEnabled(False) + def get_timestamp_widget_by_index(self, index: int) -> TimestampLayerWidget: + if index >= len(self.timestamps_widget): + raise ValueError("[TimestampsLayerInteractor] Trying to retrieve a timestamp widget with an out-of-bound index value.") + return self.timestamps_widget[list(self.timestamps_widget.keys())[index]] + + def get_timestamp_widget_by_visible_name(self, name: str) -> TimestampLayerWidget: + for w in list(self.timestamps_widget.keys()): + if self.timestamps_widget[w].visible_name == name: + return self.timestamps_widget[w] + raise ValueError("[TimestampsLayerInteractor] Trying to retrieve a non-existing timestamp widget by visible name with: {}.".format(name)) + def on_patient_view_toggled(self, patient_uid: str) -> None: """ The active patient has been changed by the user. All displayed info in the widget are obsolete and should diff --git a/gui/SinglePatientComponent/PatientResultsSidePanel/PatientResultsSinglePatientSidePanelWidget.py b/gui/SinglePatientComponent/PatientResultsSidePanel/PatientResultsSinglePatientSidePanelWidget.py index 46f96d1..ca5930c 100644 --- a/gui/SinglePatientComponent/PatientResultsSidePanel/PatientResultsSinglePatientSidePanelWidget.py +++ b/gui/SinglePatientComponent/PatientResultsSidePanel/PatientResultsSinglePatientSidePanelWidget.py @@ -164,6 +164,20 @@ def adjustSize(self) -> None: self.patient_list_scrollarea_dummy_widget.setFixedSize(QSize(self.size().width(), actual_height)) self.repaint() + def get_patient_results_widget_by_index(self, index: int) -> SinglePatientResultsWidget: + if index >= len(self.patient_results_widgets): + raise ValueError( + "[PatientResultsSinglePatientSidePanelWidget] Trying to retrieve a patient result widget with an out-of-bound index value.") + return self.patient_results_widgets[list(self.patient_results_widgets.keys())[index]] + + def get_patient_results_widget_by_display_name(self, name: str) -> SinglePatientResultsWidget: + for w in list(self.patient_results_widgets.keys()): + if self.patient_results_widgets[w].header.title_label == name: + return self.patient_results_widgets[w] + raise ValueError( + "[PatientResultsSinglePatientSidePanelWidget] Trying to retrieve a non-existing patient result widget by visible name with: {}.".format( + name)) + def on_import_data(self): """ In case some patients where imported at the same time as some image for the current patient? @@ -218,8 +232,11 @@ def on_external_patient_selection(self, patient_id): When the patient selection has been requested from a module (e.g. study) outside the single-use mode. """ self.__on_patient_selection(True, patient_id) - self.patient_results_widgets[patient_id].manual_header_pushbutton_clicked(True) - self.adjustSize() # To trigger a proper redrawing after the previous call + # Next two lines are not the cleanest, but it seems to work. @TODO. Have to improve this part. + self.patient_results_widgets[patient_id].header.expand() + self.patient_results_widgets[patient_id].set_stylesheets(True) + # To trigger a proper redrawing after the previous call + self.adjustSize() def on_process_started(self): self.bottom_add_patient_pushbutton.setEnabled(False) @@ -262,7 +279,7 @@ def __on_patient_closed(self, widget_id: str) -> None: if SoftwareConfigResources.getInstance().get_patient(widget_id).has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 0: # Operation cancelled return @@ -305,7 +322,7 @@ def __on_patient_selection(self, state: bool, widget_id: str) -> None: if SoftwareConfigResources.getInstance().get_active_patient_uid() != None \ and SoftwareConfigResources.getInstance().get_active_patient().has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 0: # Operation cancelled # The widget for the clicked patient must be collapsed back down, since the change has not # been confirmed by the user in the end. @@ -329,15 +346,15 @@ def on_add_new_empty_patient(self): if SoftwareConfigResources.getInstance().active_patient_name and\ SoftwareConfigResources.getInstance().get_active_patient().has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 1: # Changes have been either saved or discarded - uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) + uid = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) self.add_new_patient(uid) # Both lines are needed to uncollapse the widget for the new patient and collapse the previous self.patient_results_widgets[uid].manual_header_pushbutton_clicked(True) self.__on_patient_selection(True, uid) else: - uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) + uid = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) self.add_new_patient(uid) self.patient_results_widgets[uid].manual_header_pushbutton_clicked(True) self.__on_patient_selection(True, uid) @@ -351,12 +368,12 @@ def on_patient_report_imported(self, patient_uid: str, report_uid: str) -> None: def on_import_options_clicked(self, point): ## Bottom position # if os.name == 'nt': - # self.options_menu.exec_(self.bottom_add_patient_pushbutton.mapToGlobal(QPoint(0, -106))) + # self.options_menu.exec(self.bottom_add_patient_pushbutton.mapToGlobal(QPoint(0, -106))) # else: - # self.options_menu.exec_(self.bottom_add_patient_pushbutton.mapToGlobal(QPoint(0, -95))) + # self.options_menu.exec(self.bottom_add_patient_pushbutton.mapToGlobal(QPoint(0, -95))) # Top position - self.options_menu.exec_(self.bottom_add_patient_pushbutton.mapToGlobal(QPoint(0, 0))) + self.options_menu.exec(self.bottom_add_patient_pushbutton.mapToGlobal(QPoint(0, 0))) def on_import_patient_from_data_requested(self): self.on_add_new_empty_patient() diff --git a/gui/SinglePatientComponent/PatientResultsSidePanel/SinglePatientResultsWidget.py b/gui/SinglePatientComponent/PatientResultsSidePanel/SinglePatientResultsWidget.py index 7cec9f5..42f058a 100644 --- a/gui/SinglePatientComponent/PatientResultsSidePanel/SinglePatientResultsWidget.py +++ b/gui/SinglePatientComponent/PatientResultsSidePanel/SinglePatientResultsWidget.py @@ -306,7 +306,7 @@ def __on_patient_closed(self) -> None: """ # if SoftwareConfigResources.getInstance().get_patient(self.uid).has_unsaved_changes(): # dialog = SavePatientChangesDialog() - # code = dialog.exec_() + # code = dialog.exec() # if code == 0: # Operation cancelled # # The widget for the clicked patient must be collapsed back down, since the change has not # # been confirmed by the user in the end. @@ -318,21 +318,21 @@ def __on_patient_toggled(self, state): def __on_patient_name_modified(self): new_name = self.patient_name_lineedit.text() - code, msg = SoftwareConfigResources.getInstance().get_active_patient().set_display_name(new_name) - if code == 0: # Name edition was successful - self.header.title_label.setText(new_name) - self.header.title = new_name - self.patient_name_edited.emit(self.uid, new_name) - self.output_dir_lineedit.setText(SoftwareConfigResources.getInstance().get_active_patient().output_folder) - - # If some ongoing studies are opened, the associated folder must also be changed there - if not SoftwareConfigResources.getInstance().is_study_list_empty(): - SoftwareConfigResources.getInstance().propagate_patient_name_change(SoftwareConfigResources.getInstance().get_active_patient_uid()) - else: # Requested name already exists, operation cancelled and user warned. + try: + SoftwareConfigResources.getInstance().get_active_patient().set_display_name(new_name) + except Exception as e: self.patient_name_lineedit.setText(SoftwareConfigResources.getInstance().get_active_patient().display_name) - diag = QErrorMessage(self) - diag.setWindowTitle("Operation not permitted") - diag.showMessage(msg) + logging.error("[Software error] Editing the patient name failed with: {}.".format(e)) + return + self.header.title_label.setText(new_name) + self.header.title = new_name + self.patient_name_edited.emit(self.uid, new_name) + self.output_dir_lineedit.setText(SoftwareConfigResources.getInstance().get_active_patient().output_folder) + + # If some ongoing studies are opened, the associated folder must also be changed there + if not SoftwareConfigResources.getInstance().is_study_list_empty(): + SoftwareConfigResources.getInstance().propagate_patient_name_change(SoftwareConfigResources.getInstance().get_active_patient_uid()) + def __on_results_selector_index_changed(self, index): self.results_display_stackedwidget.setCurrentIndex(index) diff --git a/gui/SinglePatientComponent/PatientResultsSidePanel/TumorCharacteristicsWidget.py b/gui/SinglePatientComponent/PatientResultsSidePanel/TumorCharacteristicsWidget.py index c94a8ed..b1909f6 100644 --- a/gui/SinglePatientComponent/PatientResultsSidePanel/TumorCharacteristicsWidget.py +++ b/gui/SinglePatientComponent/PatientResultsSidePanel/TumorCharacteristicsWidget.py @@ -793,7 +793,7 @@ def populate_from_report(self) -> None: # BrainGrid structures self.braingridstructures_collapsiblegroupbox.clear_content_layout() - if UserPreferencesStructure.getInstance().compute_braingrid_structures: + if 'BrainGrid' in list(report_json['Main']['Total'].keys()): #UserPreferencesStructure.getInstance().compute_braingrid_structures: lay = QHBoxLayout() label_header = QLabel("Infiltration count:") label_header.setStyleSheet(""" diff --git a/gui/SinglePatientComponent/SinglePatientWidget.py b/gui/SinglePatientComponent/SinglePatientWidget.py index 4387a0c..bc0c805 100644 --- a/gui/SinglePatientComponent/SinglePatientWidget.py +++ b/gui/SinglePatientComponent/SinglePatientWidget.py @@ -224,14 +224,14 @@ def __on_import_file_clicked(self) -> None: """ self.import_data_dialog.reset() self.import_data_dialog.set_parsing_filter("data") - code = self.import_data_dialog.exec_() + code = self.import_data_dialog.exec() # if code == QDialog.Accepted: # self.import_data_triggered.emit() def __on_import_custom_clicked(self) -> None: self.import_data_dialog.reset() self.import_data_dialog.set_parsing_filter("patient") - code = self.import_data_dialog.exec_() + code = self.import_data_dialog.exec() if code == QDialog.Accepted: self.top_logo_panel_label_import_dicom_pushbutton.setEnabled(True) self.top_logo_panel_statistics_pushbutton.setEnabled(True) @@ -243,7 +243,7 @@ def __on_import_dicom_clicked(self) -> None: patient_dicom_id = SoftwareConfigResources.getInstance().get_active_patient().get_dicom_id() if patient_dicom_id: self.import_dicom_dialog.set_fixed_patient(patient_dicom_id) - code = self.import_dicom_dialog.exec_() + code = self.import_dicom_dialog.exec() if code == QDialog.Accepted: self.top_logo_panel_label_import_dicom_pushbutton.setEnabled(True) self.top_logo_panel_statistics_pushbutton.setEnabled(True) @@ -252,7 +252,7 @@ def __on_import_patient_dicom_clicked(self) -> None: """ """ - code = self.import_dicom_dialog.exec_() + code = self.import_dicom_dialog.exec() # if code == QDialog.Accepted: # self.import_data_triggered.emit() @@ -260,14 +260,14 @@ def __on_import_folder_clicked(self) -> None: self.import_folder_dialog.reset() self.import_folder_dialog.set_parsing_mode("single") self.import_folder_dialog.set_target_type("regular") - code = self.import_folder_dialog.exec_() + code = self.import_folder_dialog.exec() if code == QDialog.Accepted: self.top_logo_panel_label_import_dicom_pushbutton.setEnabled(True) self.top_logo_panel_statistics_pushbutton.setEnabled(True) def __on_show_statistics_clicked(self): diag = VolumeStatisticsDialog(self) - diag.exec_() + diag.exec() def __on_patient_selected(self, patient_uid): # @TODO. Quick dirty hack, should not have to set the flag everytime a patient is selected, but only once. @@ -278,6 +278,7 @@ def __on_patient_selected(self, patient_uid): def on_reload_interface(self) -> None: """ In order to generate a new central panel, for example because the display space has changed. + @TODO. Should also reload the interface for the right-hand panel and set to default """ if not SoftwareConfigResources.getInstance().is_patient_list_empty(): SoftwareConfigResources.getInstance().get_active_patient().load_in_memory() @@ -285,6 +286,8 @@ def on_reload_interface(self) -> None: def on_patient_selected(self, patient_name): self.results_panel.on_external_patient_selection(patient_name) + self.center_panel.on_patient_selected(patient_name) + self.layers_panel.on_patient_selected(patient_name) def on_single_patient_clicked(self, patient_name): # @TODO. Renaming to do, confusing name since it adds a new patient... @@ -332,3 +335,6 @@ def on_annotation_volume_imported(self, uid: str) -> None: def on_atlas_volume_imported(self, uid: str) -> None: self.layers_panel.on_atlas_volume_import(uid) + def on_clear_scene(self): + for w in list(self.results_panel.patient_results_widgets.keys()): + self.results_panel.patient_results_widgets[w].patient_closed.emit(w) \ No newline at end of file diff --git a/gui/StudyBatchComponent/PatientsSummaryPanel/StudyPatientsSegmentationSummaryWidget.py b/gui/StudyBatchComponent/PatientsSummaryPanel/StudyPatientsSegmentationSummaryWidget.py index e1705d6..b923545 100644 --- a/gui/StudyBatchComponent/PatientsSummaryPanel/StudyPatientsSegmentationSummaryWidget.py +++ b/gui/StudyBatchComponent/PatientsSummaryPanel/StudyPatientsSegmentationSummaryWidget.py @@ -90,7 +90,7 @@ def adjustSize(self) -> None: def __on_header_section_clicked(self, pos): # @TODO. Have to make a custom QTableWidget and make the whole header section fully custom - self.sorting_options_menu.exec_(self.mapToGlobal(pos)) + self.sorting_options_menu.exec(self.mapToGlobal(pos)) def on_patients_import(self) -> None: """ diff --git a/gui/StudyBatchComponent/StudiesSidePanel/SingleStudyWidget.py b/gui/StudyBatchComponent/StudiesSidePanel/SingleStudyWidget.py index b8bfc11..d9cdea1 100644 --- a/gui/StudyBatchComponent/StudiesSidePanel/SingleStudyWidget.py +++ b/gui/StudyBatchComponent/StudiesSidePanel/SingleStudyWidget.py @@ -493,7 +493,7 @@ def __on_study_closed(self) -> None: """ if SoftwareConfigResources.getInstance().get_study(self.uid).has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 0: # Operation cancelled # The widget for the clicked patient must be collapsed back down, since the change has not # been confirmed by the user in the end. @@ -505,17 +505,19 @@ def __on_study_name_modified(self) -> None: Internal update of the visible study name after user manual editing, the folder name on disk for the study is also updated with the new requested name (if available). """ - code, err_msg = SoftwareConfigResources.getInstance().get_active_study().set_display_name(self.study_name_lineedit.text()) - if code == 1: # Operation failed + try: + SoftwareConfigResources.getInstance().get_active_study().set_display_name(self.study_name_lineedit.text()) + except Exception as e: self.study_name_lineedit.blockSignals(True) self.study_name_lineedit.setText(SoftwareConfigResources.getInstance().get_active_study().display_name) self.study_name_lineedit.blockSignals(False) - else: - self.header.title_label.setText(self.study_name_lineedit.text()) - self.header.title = self.study_name_lineedit.text() - self.output_dir_lineedit.setText(SoftwareConfigResources.getInstance().get_active_study().output_study_folder) - self.output_dir_lineedit.setCursorPosition(0) - self.output_dir_lineedit.home(True) + logging.error("[Software error] Editing the patient name failed with: {}.".format(e)) + return + self.header.title_label.setText(self.study_name_lineedit.text()) + self.header.title = self.study_name_lineedit.text() + self.output_dir_lineedit.setText(SoftwareConfigResources.getInstance().get_active_study().output_study_folder) + self.output_dir_lineedit.setCursorPosition(0) + self.output_dir_lineedit.home(True) def manual_header_pushbutton_clicked(self, state): if state: @@ -541,7 +543,7 @@ def __on_include_single_patient_folder_clicked(self) -> None: self.import_data_dialog.reset() self.import_data_dialog.set_parsing_mode('single') self.import_data_dialog.set_target_type('regular') - code = self.import_data_dialog.exec_() + code = self.import_data_dialog.exec() if code == QDialog.Accepted: self.patients_import_finished.emit() @@ -549,7 +551,7 @@ def __on_include_multiple_patients_folder_clicked(self): self.import_data_dialog.reset() self.import_data_dialog.set_parsing_mode('multiple') self.import_data_dialog.set_target_type('regular') - code = self.import_data_dialog.exec_() + code = self.import_data_dialog.exec() if code == QDialog.Accepted: self.patients_import_finished.emit() @@ -562,7 +564,7 @@ def __on_include_single_dicom_patient_folder_clicked(self): self.import_data_dialog.reset() self.import_data_dialog.set_parsing_mode('single') self.import_data_dialog.set_target_type('dicom') - code = self.import_data_dialog.exec_() + code = self.import_data_dialog.exec() if code == QDialog.Accepted: self.patients_import_finished.emit() @@ -570,7 +572,7 @@ def __on_include_multiple_dicom_patients_folder_clicked(self): self.import_data_dialog.reset() self.import_data_dialog.set_parsing_mode('multiple') self.import_data_dialog.set_target_type('dicom') - code = self.import_data_dialog.exec_() + code = self.import_data_dialog.exec() if code == QDialog.Accepted: self.patients_import_finished.emit() @@ -587,7 +589,7 @@ def __on_run_pipeline(self) -> None: self.model_name = "" if pipeline_task != "folders_classification": diag = TumorTypeSelectionQDialog(self) - code = diag.exec_() + code = diag.exec() if code == 0: # Operation was cancelled by the user return diff --git a/gui/StudyBatchComponent/StudiesSidePanel/StudiesSidePanelWidget.py b/gui/StudyBatchComponent/StudiesSidePanel/StudiesSidePanelWidget.py index c4158b2..41d25c3 100644 --- a/gui/StudyBatchComponent/StudiesSidePanel/StudiesSidePanelWidget.py +++ b/gui/StudyBatchComponent/StudiesSidePanel/StudiesSidePanelWidget.py @@ -193,7 +193,7 @@ def __on_study_selection(self, state, widget_id): if SoftwareConfigResources.getInstance().get_active_study_uid() and SoftwareConfigResources.getInstance().get_active_study().has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 0: # Operation cancelled return @@ -212,7 +212,7 @@ def on_add_new_empty_study(self): if SoftwareConfigResources.getInstance().active_study_name and\ SoftwareConfigResources.getInstance().get_active_study().has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 1: # Changes have been either saved or discarded uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_study(active=False) self.add_new_study(uid) @@ -250,18 +250,18 @@ def on_import_options_clicked(self, point): """ ## Bottom position # if os.name == 'nt': - # self.options_menu.exec_(self.bottom_add_study_pushbutton.mapToGlobal(QPoint(0, -18))) + # self.options_menu.exec(self.bottom_add_study_pushbutton.mapToGlobal(QPoint(0, -18))) # else: - # self.options_menu.exec_(self.bottom_add_study_pushbutton.mapToGlobal(QPoint(0, -15))) + # self.options_menu.exec(self.bottom_add_study_pushbutton.mapToGlobal(QPoint(0, -15))) # Top position - self.options_menu.exec_(self.bottom_add_study_pushbutton.mapToGlobal(QPoint(0, 0))) + self.options_menu.exec(self.bottom_add_study_pushbutton.mapToGlobal(QPoint(0, 0))) def on_add_existing_study_requested(self): if SoftwareConfigResources.getInstance().active_study_name and\ SoftwareConfigResources.getInstance().get_active_study().has_unsaved_changes(): dialog = SavePatientChangesDialog() - code = dialog.exec_() + code = dialog.exec() if code == 1: # Changes have been either saved or discarded self.import_study_from_file_requested.emit() else: diff --git a/gui/StudyBatchComponent/StudyBatchWidget.py b/gui/StudyBatchComponent/StudyBatchWidget.py index ca14327..f8753b6 100644 --- a/gui/StudyBatchComponent/StudyBatchWidget.py +++ b/gui/StudyBatchComponent/StudyBatchWidget.py @@ -145,7 +145,7 @@ def on_process_finished(self): def __on_import_custom_clicked(self) -> None: self.import_data_dialog.reset() self.import_data_dialog.set_parsing_filter("study") - code = self.import_data_dialog.exec_() + code = self.import_data_dialog.exec() # if code == QDialog.Accepted: # self.study_imported.emit() @@ -192,3 +192,8 @@ def on_patient_refresh_triggered(self, patient_uid: str) -> None: SoftwareConfigResources.getInstance().get_active_study().refresh_patient_statistics(patient_uid, SoftwareConfigResources.getInstance().get_patient(patient_uid)) self.patient_refreshed.emit(patient_uid) + + def on_clear_scene(self): + # @TODO. Not enough, the graphical display for the center and right panels should also be cleared and updated. + for w in list(self.studies_panel.single_study_widgets.keys()): + self.studies_panel.single_study_widgets[w].study_closed.emit(w) \ No newline at end of file diff --git a/gui/UtilsWidgets/CustomQDialog/AboutDialog.py b/gui/UtilsWidgets/CustomQDialog/AboutDialog.py index 13fde61..d24e6e5 100644 --- a/gui/UtilsWidgets/CustomQDialog/AboutDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/AboutDialog.py @@ -18,8 +18,8 @@ def __init__(self, parent=None): self.__set_stylesheets() self.__textfill() - def exec_(self) -> int: - return super().exec_() + def exec(self) -> int: + return super().exec() def __set_interface(self): self.layout = QVBoxLayout(self) diff --git a/gui/UtilsWidgets/CustomQDialog/ContrastAdjustmentDialog.py b/gui/UtilsWidgets/CustomQDialog/ContrastAdjustmentDialog.py index 2eb1fd8..74eb986 100644 --- a/gui/UtilsWidgets/CustomQDialog/ContrastAdjustmentDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/ContrastAdjustmentDialog.py @@ -25,7 +25,7 @@ def __init__(self, volume_uid, parent=None): self.volume_uid = volume_uid self.starting_contrast = None - def exec_(self) -> int: + def exec(self) -> int: curr_img = SoftwareConfigResources.getInstance().get_active_patient().get_mri_by_uid(self.volume_uid) self.intensity_window_min_spinbox.blockSignals(True) self.intensity_window_min_spinbox.setMinimum(curr_img.get_resampled_minimum_intensity()) @@ -39,7 +39,7 @@ def exec_(self) -> int: self.intensity_window_max_spinbox.blockSignals(False) self.__set_hist() self.starting_contrast = [curr_img.get_contrast_window_minimum(), curr_img.get_contrast_window_maximum()] - return super().exec_() + return super().exec() def __set_interface(self): self.base_layout = QVBoxLayout(self) diff --git a/gui/UtilsWidgets/CustomQDialog/ImportDICOMDataQDialog.py b/gui/UtilsWidgets/CustomQDialog/ImportDICOMDataQDialog.py index 9e0d1e1..294ea56 100644 --- a/gui/UtilsWidgets/CustomQDialog/ImportDICOMDataQDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/ImportDICOMDataQDialog.py @@ -246,52 +246,45 @@ def __on_import_directory_clicked(self): return self.current_folder = os.path.dirname(input_directory) - dicom_holder = PatientDICOM(input_directory) - error_msg = dicom_holder.parse_dicom_folder() - if error_msg is not None: - diag = QMessageBox.warning(self, "DICOM parsing warnings", error_msg) - if dicom_holder.patient_id not in list(self.dicom_holders.keys()): - self.dicom_holders[dicom_holder.patient_id] = dicom_holder + try: + dicom_holder = PatientDICOM(input_directory) + dicom_holder.parse_dicom_folder() + if dicom_holder.patient_id not in list(self.dicom_holders.keys()): + self.dicom_holders[dicom_holder.patient_id] = dicom_holder - self.dicom_holder = dicom_holder - self.__populate_dicom_browser() + self.dicom_holder = dicom_holder + self.__populate_dicom_browser() + except Exception as e: + logging.error("[Software error] Importing DICOM folder failed.

Reason: {}".format(e)) - def __on_exit_accept_clicked(self): + def __on_exit_accept_clicked(self) -> None: """ @TODO. Rename the patient with the content of the metadata tag. """ - if len(SoftwareConfigResources.getInstance().patients_parameters) == 0: - uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_patient() - if error_msg: - diag = QMessageBox() - diag.setText("Unable to create empty patient.\nError message: {}.\n".format(error_msg)) - diag.exec_() - - if (error_msg and 'Import patient failed' not in error_msg) or not error_msg: + try: + if len(SoftwareConfigResources.getInstance().patients_parameters) == 0: + uid = SoftwareConfigResources.getInstance().add_new_empty_patient() self.patient_imported.emit(uid) - self.load_progressbar.reset() - self.load_progressbar.setMinimum(0) - self.load_progressbar.setMaximum(self.selected_series_tablewidget.rowCount()) - self.load_progressbar.setVisible(True) - self.load_progressbar.setValue(0) - - # Ordering selecting series by date for timestamp-ordered import. - ordered_series_ids = self.__sort_selected_series_by_date(study_uids=[self.selected_series_tablewidget.item(elem, 0).text() for - elem in range(self.selected_series_tablewidget.rowCount())], - series_uids=[self.selected_series_tablewidget.item(elem, 2).text() for - elem in range(self.selected_series_tablewidget.rowCount())]) - - for i, elem in enumerate(ordered_series_ids.keys()): - series_reader = self.dicom_holder.studies[ordered_series_ids[elem]].dicom_series[elem] - uid, error_msg = SoftwareConfigResources.getInstance().get_active_patient().import_dicom_data(series_reader) - if error_msg: - diag = QMessageBox() - diag.setText("Unable to load series: {}.\nError message: {}.\n".format(elem, error_msg)) - diag.exec_() - else: + self.load_progressbar.reset() + self.load_progressbar.setMinimum(0) + self.load_progressbar.setMaximum(self.selected_series_tablewidget.rowCount()) + self.load_progressbar.setVisible(True) + self.load_progressbar.setValue(0) + + # Ordering selecting series by date for timestamp-ordered import. + ordered_series_ids = self.__sort_selected_series_by_date(study_uids=[self.selected_series_tablewidget.item(elem, 0).text() for + elem in range(self.selected_series_tablewidget.rowCount())], + series_uids=[self.selected_series_tablewidget.item(elem, 2).text() for + elem in range(self.selected_series_tablewidget.rowCount())]) + + for i, elem in enumerate(ordered_series_ids.keys()): + series_reader = self.dicom_holder.studies[ordered_series_ids[elem]].dicom_series[elem] + uid = SoftwareConfigResources.getInstance().get_active_patient().import_dicom_data(series_reader) self.mri_volume_imported.emit(uid) - self.load_progressbar.setValue(i + 1) + self.load_progressbar.setValue(i + 1) + except Exception as e: + logging.error("[Software error] Importing new DICOM data failed.

Reason: {}".format(e)) # @TODO. The following is not enough, it updates internally and on disk the patient name, but the GUI part # is not updated. Have to emit a specific signal with the new display name here. @@ -447,7 +440,7 @@ def __on_display_metadata_triggered(self, series_index: int) -> None: # print("Metadata for row {}".format(series_index)) study_id_item = self.content_study_tablewidget.item(self.content_study_tablewidget.currentRow(), 1) diag = DisplayMetadataDICOMDialog(self.dicom_holder.studies[study_id_item.text()].dicom_series[self.content_series_tablewidget.item(series_index, 0).text()].dicom_tags) - diag.exec_() + diag.exec() def __on_remove_selected_series_triggered(self, series_index: int) -> None: self.selected_series_tablewidget.removeRow(series_index) diff --git a/gui/UtilsWidgets/CustomQDialog/ImportDataQDialog.py b/gui/UtilsWidgets/CustomQDialog/ImportDataQDialog.py index 3447b08..a7f4f04 100644 --- a/gui/UtilsWidgets/CustomQDialog/ImportDataQDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/ImportDataQDialog.py @@ -188,63 +188,44 @@ def __on_exit_accept_clicked(self): else: raidionics_selected.append(f) except Exception: - diag = QMessageBox() - error_msg = 'Unable to determine volume category for: {}.\n'.format(f) + \ - 'Try converting the file to nifti before attempting loading it again.\n\n' + \ - 'Error message: {}\n'.format(traceback.format_exc()) + error_msg = '[Software error] Unable to determine volume category for: {}

'.format(f) + \ + 'Try converting the file to nifti before attempting loading it again.' logging.error(error_msg) - diag.setText(error_msg) - diag.exec_() for i, pf in enumerate(raidionics_selected): - ext = pf.split('.')[-1] - if ext == SoftwareConfigResources.getInstance().accepted_scene_file_format[0]: - uid, error_msg = SoftwareConfigResources.getInstance().load_patient(pf, active=False) - if error_msg: - diag = QMessageBox() - diag.setText("Unable to load: {}.\nError message: {}.\n".format(os.path.basename(pf), - error_msg)) - diag.exec_() - - if (error_msg and 'Import patient failed' not in error_msg) or not error_msg: + try: + ext = pf.split('.')[-1] + if ext == SoftwareConfigResources.getInstance().accepted_scene_file_format[0]: + uid = SoftwareConfigResources.getInstance().load_patient(pf, active=False) self.patient_imported.emit(uid) - else: - uid, error_msg = SoftwareConfigResources.getInstance().load_study(pf) - if error_msg: - diag = QMessageBox() - diag.setText("Unable to load: {}.\nError message: {}.\n".format(os.path.basename(pf), - error_msg)) - diag.exec_() - - if (error_msg and 'Import study failed' not in error_msg) or not error_msg: + else: + uid = SoftwareConfigResources.getInstance().load_study(pf) self.study_imported.emit(uid) - + except Exception as e: + error_msg = '[Software error] Failed to load the following Raidionics file: {}'.format(pf) + \ + '

Reason: {}.'.format(e) + logging.error(error_msg) self.load_progressbar.setValue(i + 1) # @TODO. Might try something more advanced for pairing annotations with MRIs for i, pf in enumerate(mris_selected): - uid, error_msg = SoftwareConfigResources.getInstance().get_active_patient().import_data(pf, type="MRI") - if error_msg: - if "[Doppelganger]" not in error_msg: - diag = QMessageBox() - diag.setText("Unable to load: {}.\nError message: {}.\n".format(os.path.basename(pf), - error_msg)) - diag.exec_() - else: + try: + uid = SoftwareConfigResources.getInstance().get_active_patient().import_data(pf, type="MRI") self.mri_volume_imported.emit(uid) + except Exception as e: + error_msg = '[Software error] Failed to load the following file: {}'.format(pf) + \ + '

Reason: {}.'.format(e) + logging.error(error_msg) self.load_progressbar.setValue(i + 1) for i, pf in enumerate(annotations_selected): - uid, error_msg = SoftwareConfigResources.getInstance().get_active_patient().import_data(pf, - type="Annotation") - if error_msg: - if "[Doppelganger]" not in error_msg: - diag = QMessageBox() - diag.setText("Unable to load: {}.\nError message: {}.\n".format(os.path.basename(pf), - error_msg)) - diag.exec_() - else: + try: + uid = SoftwareConfigResources.getInstance().get_active_patient().import_data(pf, type="Annotation") self.annotation_volume_imported.emit(uid) + except Exception as e: + error_msg = '[Software error] Failed to load the following file: {}'.format(pf) + \ + '

Reason: {}.'.format(e) + logging.error(error_msg) self.load_progressbar.setValue(len(mris_selected) + i + 1) self.load_progressbar.setVisible(False) diff --git a/gui/UtilsWidgets/CustomQDialog/ImportFoldersQDialog.py b/gui/UtilsWidgets/CustomQDialog/ImportFoldersQDialog.py index de9001e..457625a 100644 --- a/gui/UtilsWidgets/CustomQDialog/ImportFoldersQDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/ImportFoldersQDialog.py @@ -192,6 +192,7 @@ def __on_exit_accept_clicked(self) -> None: self.load_progressbar.setValue(0) for input_folderpath in input_folderpaths: + logging.debug("[ImportFoldersQDialog] Parsing the current folder input with value: {}".format(input_folderpath)) try: folders_in_path = [] @@ -202,36 +203,25 @@ def __on_exit_accept_clicked(self) -> None: # Checking for the proper use-case based on the type of folder architecture if len(folders_in_path) == 0 and self.parsing_mode == 'single' and self.target_type == 'regular': # Case (i) - imports, error_msg = import_patient_from_folder(folder_path=input_folderpath) + imports = import_patient_from_folder(folder_path=input_folderpath) pat_uid = imports['Patient'][0] self.patient_imported.emit(pat_uid) SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() if self.operation_mode == 'study': - msg = SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, - folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, - patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) + SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, + folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, + patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) self.load_progressbar.setValue(self.load_progressbar.value() + 1) - if error_msg: - diag = QMessageBox() - diag.setText("Unable to load patient.\nError message: {}.\n".format(error_msg)) - diag.exec_() elif len(folders_in_path) != 0 and self.parsing_mode == 'single' and self.target_type == 'regular': # Case (vi) - collective_errors = "" - imports, error_msg = import_patient_from_timestamped_folder(folder_path=input_folderpath) + imports = import_patient_from_timestamped_folder(folder_path=input_folderpath) pat_uid = imports['Patient'][0] self.patient_imported.emit(pat_uid) SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() if self.operation_mode == 'study': - msg = SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, - folder_name=SoftwareConfigResources.getInstance().get_patient( - pat_uid).output_folder, - patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) - collective_errors = collective_errors + error_msg + SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, + folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, + patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) self.load_progressbar.setValue(self.load_progressbar.value() + 1) - if collective_errors != "": - diag = QMessageBox() - diag.setText("Unable to load patients.\nError message: {}.\n".format(collective_errors)) - diag.exec_() elif len(folders_in_path) == 0 and self.target_type == 'regular': # Case (iii) single_files_in_path = [] for _, _, files in os.walk(input_folderpath): @@ -242,26 +232,23 @@ def __on_exit_accept_clicked(self) -> None: self.load_progressbar.setMaximum(len(single_files_in_path)) for p in single_files_in_path: - pat_uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) - if error_msg: - patient_include_error_msg = "Unable to create empty patient.\nError message: {}.\n".format( - error_msg) - - SoftwareConfigResources.getInstance().get_patient(pat_uid).display_name = p.split('.')[0] - uid, error_msg = SoftwareConfigResources.getInstance().get_patient(pat_uid).import_data( - os.path.join(input_folderpath, p)) - if error_msg: - patient_include_error_msg = "Unable to load {}.\nError message: {}.\n".format( - os.path.join(input_folderpath, p), error_msg) - self.patient_imported.emit(pat_uid) - SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() - if self.operation_mode == 'study': - msg = SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, - folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, - patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) + try: + pat_uid = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) + + SoftwareConfigResources.getInstance().get_patient(pat_uid).display_name = p.split('.')[0] + uid = SoftwareConfigResources.getInstance().get_patient(pat_uid).import_data(os.path.join(input_folderpath, p)) + self.patient_imported.emit(pat_uid) + SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() + if self.operation_mode == 'study': + SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, + folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, + patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) + except Exception as e: + error_msg = '[Software error] Failed to load the following file: {}'.format(p) + \ + '

Reason: {}.'.format(e) + logging.error(error_msg) self.load_progressbar.setValue(self.load_progressbar.value() + 1) elif self.target_type == 'regular': # Case (ii) and (vii) - collective_errors = "" self.load_progressbar.setMaximum(len(folders_in_path)) for patient in folders_in_path: tmp_dirs = [] @@ -270,74 +257,50 @@ def __on_exit_accept_clicked(self) -> None: tmp_dirs.append(d) break if len(tmp_dirs) == 0: - imports, error_msg = import_patient_from_folder(folder_path=os.path.join(input_folderpath, patient)) + imports = import_patient_from_folder(folder_path=os.path.join(input_folderpath, patient)) else: - imports, error_msg = import_patient_from_timestamped_folder(folder_path=os.path.join(input_folderpath, patient)) + imports = import_patient_from_timestamped_folder(folder_path=os.path.join(input_folderpath, patient)) pat_uid = imports['Patient'][0] self.patient_imported.emit(pat_uid) SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() if self.operation_mode == 'study': - msg = SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, - folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, - patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) - collective_errors = collective_errors + error_msg + SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, + folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, + patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) self.load_progressbar.setValue(self.load_progressbar.value() + 1) - if collective_errors != "": - diag = QMessageBox() - diag.setText("Unable to load patients.\nError message: {}.\n".format(collective_errors)) - diag.exec_() elif self.target_type == 'dicom' and self.parsing_mode == 'single': # Case (iv) dicom_holder = PatientDICOM(input_folderpath) - error_msg = dicom_holder.parse_dicom_folder() - # if error_msg is not None: - # diag = QMessageBox.warning(self, "DICOM parsing warnings", error_msg) - pat_uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) - if error_msg: - patient_include_error_msg = "Unable to create empty patient.\nError message: {}.\n".format( - error_msg) + dicom_holder.parse_dicom_folder() + pat_uid = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) SoftwareConfigResources.getInstance().get_patient(uid=pat_uid).set_display_name(dicom_holder.patient_id) for study_id in dicom_holder.studies.keys(): for series_id in dicom_holder.studies[study_id].dicom_series.keys(): - volume_uid, err_msg = SoftwareConfigResources.getInstance().get_patient(uid=pat_uid).import_dicom_data(dicom_holder.studies[study_id].dicom_series[series_id]) + volume_uid = SoftwareConfigResources.getInstance().get_patient(uid=pat_uid).import_dicom_data(dicom_holder.studies[study_id].dicom_series[series_id]) self.patient_imported.emit(pat_uid) SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() if self.operation_mode == 'study': - msg = SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, - folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, - patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) + SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, + folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, + patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) self.load_progressbar.setValue(self.load_progressbar.value() + 1) - if error_msg: - diag = QMessageBox() - diag.setText("Unable to load patient.\nError message: {}.\n".format(error_msg)) - diag.exec_() elif self.target_type == 'dicom' and self.parsing_mode == 'multiple': # Case (v) for patient in folders_in_path: dicom_holder = PatientDICOM(os.path.join(input_folderpath, patient)) - error_msg = dicom_holder.parse_dicom_folder() - # if error_msg is not None: - # diag = QMessageBox.warning(self, "DICOM parsing warnings", error_msg) - pat_uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) - if error_msg: - patient_include_error_msg = "Unable to create empty patient.\nError message: {}.\n".format( - error_msg) + dicom_holder.parse_dicom_folder() + pat_uid = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) SoftwareConfigResources.getInstance().get_patient(uid=pat_uid).set_display_name(dicom_holder.patient_id) for study_id in dicom_holder.studies.keys(): for series_id in dicom_holder.studies[study_id].dicom_series.keys(): - volume_uid, err_msg = SoftwareConfigResources.getInstance().get_patient(uid=pat_uid).import_dicom_data(dicom_holder.studies[study_id].dicom_series[series_id]) + volume_uid = SoftwareConfigResources.getInstance().get_patient(uid=pat_uid).import_dicom_data(dicom_holder.studies[study_id].dicom_series[series_id]) self.patient_imported.emit(pat_uid) SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() if self.operation_mode == 'study': - msg = SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, - folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, - patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) + SoftwareConfigResources.getInstance().get_active_study().include_study_patient(uid=pat_uid, + folder_name=SoftwareConfigResources.getInstance().get_patient(pat_uid).output_folder, + patient_parameters=SoftwareConfigResources.getInstance().get_patient(pat_uid)) self.load_progressbar.setValue(self.load_progressbar.value() + 1) - if error_msg: - diag = QMessageBox() - diag.setText("Unable to load patient.\nError message: {}.\n".format(error_msg)) - diag.exec_() except Exception as e: - logging.error("Folder import failed for {} with: \n {}".format(input_folderpath, - traceback.format_exc())) + logging.error("[Software error] Folder import failed for {} with: {}".format(input_folderpath, e)) self.load_progressbar.setVisible(False) self.accept() @@ -459,7 +422,7 @@ def __int__(self): # if f_tree_view: # f_tree_view.setSelectionMode(QAbstractItemView.MultiSelection) - # def exec_(self) -> int: + # def exec(self) -> int: # # self.setFileMode(QFileDialog.DirectoryOnly) # # self.setOption(QFileDialog.DontUseNativeDialog, True) # file_view = self.findChild(QListView, 'listView') @@ -471,63 +434,55 @@ def __int__(self): # if f_tree_view: # f_tree_view.setSelectionMode(QAbstractItemView.MultiSelection) # - # self.exec_() + # self.exec() -def import_patient_from_folder(folder_path: str) -> Tuple[dict, str]: +def import_patient_from_folder(folder_path: str) -> dict: """ @TODO. To finish, by filling the imports dict, to know which signals to emit afterwards, import patient/MRI/annotation/etc... @TODO2. The signal for each patient should be emitted here to visually update the list as it goes. @TODO3. Should not open message box in here if the parsing failed, should bounce it above and recap all errors after. """ - patient_include_error_msg = "" - files_in_path = [] - raidionics_scene_file = None - for _, _, files in os.walk(folder_path): - for f in files: - files_in_path.append(f) - if f.split('.')[-1] == SoftwareConfigResources.getInstance().accepted_scene_file_format[0]: - raidionics_scene_file = f - break - - imports = {} # Holder for all image/segmentation files loaded from the patient folder - if raidionics_scene_file: - pat_uid, error_msg = SoftwareConfigResources.getInstance().load_patient(os.path.join(folder_path, raidionics_scene_file)) - imports['Patient'] = [pat_uid] - if error_msg: - patient_include_error_msg = "Unable to open patient.\nError message: {}.\n".format(error_msg) - return imports, patient_include_error_msg - else: - pat_uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) - imports['Patient'] = [pat_uid] - if error_msg: - patient_include_error_msg = "Unable to create empty patient.\nError message: {}.\n".format(error_msg) - return imports, patient_include_error_msg - - SoftwareConfigResources.getInstance().get_patient(pat_uid).set_display_name(os.path.basename(folder_path)) - mris_in_path = [] - annotations_in_path = [] - for f in files_in_path: - ft = input_file_category_disambiguation(input_filename=os.path.join(folder_path, f)) - if ft == "MRI": - mris_in_path.append(f) - else: - annotations_in_path.append(f) - - # @TODO. Might try to infer from the filenames if some annotations are belonging to some MRIs. - # for now just processing MRIs first and annotations after. - files_list = mris_in_path + annotations_in_path - for f in files_list: - uid, error_msg = SoftwareConfigResources.getInstance().get_patient(pat_uid).import_data( - os.path.join(folder_path, f)) - if error_msg: - patient_include_error_msg = "Unable to load: {}.\nError message: {}.\n".format( - os.path.basename(folder_path), error_msg) - SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() - return imports, patient_include_error_msg - - -def import_patient_from_timestamped_folder(folder_path: str) -> Tuple[dict, str]: + try: + files_in_path = [] + raidionics_scene_file = None + for _, _, files in os.walk(folder_path): + for f in files: + files_in_path.append(f) + if f.split('.')[-1] == SoftwareConfigResources.getInstance().accepted_scene_file_format[0]: + raidionics_scene_file = f + break + + imports = {} # Holder for all image/segmentation files loaded from the patient folder + if raidionics_scene_file: + pat_uid = SoftwareConfigResources.getInstance().load_patient(os.path.join(folder_path, raidionics_scene_file)) + imports['Patient'] = [pat_uid] + else: + pat_uid = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) + imports['Patient'] = [pat_uid] + + SoftwareConfigResources.getInstance().get_patient(pat_uid).set_display_name(os.path.basename(folder_path)) + mris_in_path = [] + annotations_in_path = [] + for f in files_in_path: + ft = input_file_category_disambiguation(input_filename=os.path.join(folder_path, f)) + if ft == "MRI": + mris_in_path.append(f) + else: + annotations_in_path.append(f) + + # @TODO. Might try to infer from the filenames if some annotations are belonging to some MRIs. + # for now just processing MRIs first and annotations after. + files_list = mris_in_path + annotations_in_path + for f in files_list: + uid = SoftwareConfigResources.getInstance().get_patient(pat_uid).import_data(os.path.join(folder_path, f)) + SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() + return imports + except Exception as e: + raise RuntimeError("Importing a patient from a folder failed with: {}".format(e)) + + +def import_patient_from_timestamped_folder(folder_path: str) -> dict: """ The provided folder for the patient to import contains multiple sub-folders, indicating the existence of data across multiple timestamps. @@ -541,82 +496,73 @@ def import_patient_from_timestamped_folder(folder_path: str) -> Tuple[dict, str] Returns ------- - res: Tuple[dict, str] - A Tuple[dict, str] whereby the first element indicates which elements have been imported and the second - component stores error messages which might have arisen during the import process. + res: dict + A dict to indicate which elements have been imported. """ - patient_include_error_msg = "" - - files_in_path = [] - raidionics_scene_file = None - for _, _, files in os.walk(folder_path): - for f in files: - files_in_path.append(f) - if f.split('.')[-1] == SoftwareConfigResources.getInstance().accepted_scene_file_format[0]: - raidionics_scene_file = f - break - - imports = {} # Holder for all image/segmentation files loaded from the patient folder - if raidionics_scene_file: - pat_uid, error_msg = SoftwareConfigResources.getInstance().load_patient(os.path.join(folder_path, - raidionics_scene_file)) - imports['Patient'] = [pat_uid] - if error_msg: - patient_include_error_msg = "Unable to open patient.\nError message: {}.\n".format(error_msg) - return imports, patient_include_error_msg - else: - pat_uid, error_msg = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) - imports['Patient'] = [pat_uid] - if error_msg: - patient_include_error_msg = "Unable to create empty patient.\nError message: {}.\n".format(error_msg) - return imports, patient_include_error_msg - - code, err_msg = SoftwareConfigResources.getInstance().get_patient(pat_uid).set_display_name(os.path.basename(folder_path)) - - timestamp_folders = [] - for _, dirs, _ in os.walk(folder_path): - for d in dirs: - timestamp_folders.append(d) + try: + files_in_path = [] + raidionics_scene_file = None + for _, _, files in os.walk(folder_path): + for f in files: + files_in_path.append(f) + if f.split('.')[-1] == SoftwareConfigResources.getInstance().accepted_scene_file_format[0]: + raidionics_scene_file = f break - # What if the folders are named PreOp and PostOp? - ts_folders_dict = {} - for i in timestamp_folders: - if re.search(r'\d+', i): # Skipping folders without an integer inside, otherwise assuming timestamps from 0 onwards - ts_folders_dict[int(re.search(r'\d+', i).group())] = i - - ordered_ts_folders = dict(sorted(ts_folders_dict.items(), key=lambda item: item[0], reverse=False)) - - for i, ts in enumerate(list(ordered_ts_folders.keys())): - ts_folder = os.path.join(folder_path, ordered_ts_folders[ts]) - ts_uid, ts_error_msg = SoftwareConfigResources.getInstance().get_patient(pat_uid).insert_investigation_timestamp(order=i) - files_in_path = [] - for _, _, files in os.walk(ts_folder): - for f in files: - if '.'.join(f.split('.')[1:]) in SoftwareConfigResources.getInstance().get_accepted_image_formats(): - files_in_path.append(f) + imports = {} # Holder for all image/segmentation files loaded from the patient folder + if raidionics_scene_file: + pat_uid = SoftwareConfigResources.getInstance().load_patient(os.path.join(folder_path, + raidionics_scene_file)) + imports['Patient'] = [pat_uid] + else: + pat_uid = SoftwareConfigResources.getInstance().add_new_empty_patient(active=False) + imports['Patient'] = [pat_uid] + + SoftwareConfigResources.getInstance().get_patient(pat_uid).set_display_name(os.path.basename(folder_path)) + + timestamp_folders = [] + for _, dirs, _ in os.walk(folder_path): + for d in dirs: + timestamp_folders.append(d) break - mris_in_path = [] - annotations_in_path = [] - for f in files_in_path: - ft = input_file_category_disambiguation(input_filename=os.path.join(ts_folder, f)) - if ft == "MRI": - mris_in_path.append(f) - else: - annotations_in_path.append(f) + # What if the folders are named PreOp and PostOp? + ts_folders_dict = {} + for i in timestamp_folders: + if re.search(r'\d+', i): # Skipping folders without an integer inside, otherwise assuming timestamps from 0 onwards + ts_folders_dict[int(re.search(r'\d+', i).group())] = i - # @TODO. Might try to infer from the filenames if some annotations are belonging to some MRIs. - # for now just processing MRIs first and annotations after. - files_list = mris_in_path + annotations_in_path - for f in files_list: - uid, error_msg = SoftwareConfigResources.getInstance().get_patient(pat_uid).import_data(filename=os.path.join(ts_folder, f), - investigation_ts=ts_uid) - if error_msg: - patient_include_error_msg = "Unable to load: {}.\nError message: {}.\n".format( - os.path.join(ts_folder, f), error_msg) - SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() - return imports, patient_include_error_msg + ordered_ts_folders = dict(sorted(ts_folders_dict.items(), key=lambda item: item[0], reverse=False)) + + for i, ts in enumerate(list(ordered_ts_folders.keys())): + ts_folder = os.path.join(folder_path, ordered_ts_folders[ts]) + ts_uid, ts_error_msg = SoftwareConfigResources.getInstance().get_patient(pat_uid).insert_investigation_timestamp(order=i) + files_in_path = [] + for _, _, files in os.walk(ts_folder): + for f in files: + if '.'.join(f.split('.')[1:]) in SoftwareConfigResources.getInstance().get_accepted_image_formats(): + files_in_path.append(f) + break + + mris_in_path = [] + annotations_in_path = [] + for f in files_in_path: + ft = input_file_category_disambiguation(input_filename=os.path.join(ts_folder, f)) + if ft == "MRI": + mris_in_path.append(f) + else: + annotations_in_path.append(f) + + # @TODO. Might try to infer from the filenames if some annotations are belonging to some MRIs. + # for now just processing MRIs first and annotations after. + files_list = mris_in_path + annotations_in_path + for f in files_list: + uid = SoftwareConfigResources.getInstance().get_patient(pat_uid).import_data(filename=os.path.join(ts_folder, f), + investigation_ts=ts_uid) + SoftwareConfigResources.getInstance().get_patient(pat_uid).save_patient() + return imports + except Exception as e: + raise RuntimeError("Importing a patient from a timestamped folder failed with: {}".format(e)) def precompute_total_elements_to_add(selected_folderpaths: List[str], parsing_mode: str, target_type: str) -> int: diff --git a/gui/UtilsWidgets/CustomQDialog/KeyboardShortcutsDialog.py b/gui/UtilsWidgets/CustomQDialog/KeyboardShortcutsDialog.py index fd1d237..465f9be 100644 --- a/gui/UtilsWidgets/CustomQDialog/KeyboardShortcutsDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/KeyboardShortcutsDialog.py @@ -17,8 +17,8 @@ def __init__(self, parent=None): self.__set_stylesheets() self.__fill_table() - def exec_(self) -> int: - return super().exec_() + def exec(self) -> int: + return super().exec() def __set_interface(self): self.layout = QVBoxLayout(self) diff --git a/gui/UtilsWidgets/CustomQDialog/LogsViewerDialog.py b/gui/UtilsWidgets/CustomQDialog/LogsViewerDialog.py index 2f02318..32d001c 100644 --- a/gui/UtilsWidgets/CustomQDialog/LogsViewerDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/LogsViewerDialog.py @@ -19,8 +19,8 @@ def __init__(self, parent=None): self.__set_stylesheets() self.__refresh_logs() - def exec_(self) -> int: - return super().exec_() + def exec(self) -> int: + return super().exec() def __set_interface(self): self.layout = QVBoxLayout(self) diff --git a/gui/UtilsWidgets/CustomQDialog/ResearchCommunityDialog.py b/gui/UtilsWidgets/CustomQDialog/ResearchCommunityDialog.py index 77bcc0e..b037c74 100644 --- a/gui/UtilsWidgets/CustomQDialog/ResearchCommunityDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/ResearchCommunityDialog.py @@ -16,8 +16,8 @@ def __init__(self, parent=None): self.__set_connections() self.__set_stylesheets() - def exec_(self) -> int: - return super().exec_() + def exec(self) -> int: + return super().exec() def __set_interface(self): self.layout = QVBoxLayout(self) diff --git a/gui/UtilsWidgets/CustomQDialog/SavePatientChangesDialog.py b/gui/UtilsWidgets/CustomQDialog/SavePatientChangesDialog.py index f47c2f3..67d5647 100644 --- a/gui/UtilsWidgets/CustomQDialog/SavePatientChangesDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/SavePatientChangesDialog.py @@ -50,12 +50,12 @@ def __set_stylesheets(self): font-size: 12px; }""") - def exec_(self) -> int: + def exec(self) -> int: curr_patient = SoftwareConfigResources.getInstance().get_active_patient() self.destination_folder_lineedit.blockSignals(True) self.destination_folder_lineedit.setText(curr_patient.output_folder) self.destination_folder_lineedit.blockSignals(False) - return super().exec_() + return super().exec() def save_changes(self): SoftwareConfigResources.getInstance().get_active_patient().save_patient() diff --git a/gui/UtilsWidgets/CustomQDialog/SoftwareSettingsDialog.py b/gui/UtilsWidgets/CustomQDialog/SoftwareSettingsDialog.py index 54d93d8..f34bdd2 100644 --- a/gui/UtilsWidgets/CustomQDialog/SoftwareSettingsDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/SoftwareSettingsDialog.py @@ -21,8 +21,8 @@ def __init__(self, parent=None): self.__set_connections() self.__set_stylesheets() - def exec_(self) -> int: - return super().exec_() + def exec(self) -> int: + return super().exec() def __set_interface(self): self.layout = QVBoxLayout(self) diff --git a/gui/UtilsWidgets/CustomQDialog/VolumeStatisticsDialog.py b/gui/UtilsWidgets/CustomQDialog/VolumeStatisticsDialog.py index 698361a..341ccfc 100644 --- a/gui/UtilsWidgets/CustomQDialog/VolumeStatisticsDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/VolumeStatisticsDialog.py @@ -24,8 +24,8 @@ def __init__(self, parent=None): self.__set_connections() self.__default_setup() - def exec_(self) -> int: - return super().exec_() + def exec(self) -> int: + return super().exec() def __set_interface(self): self.layout = QVBoxLayout(self) diff --git a/gui/UtilsWidgets/CustomQTableWidget/ContextMenuQTableWidget.py b/gui/UtilsWidgets/CustomQTableWidget/ContextMenuQTableWidget.py index 62b540a..b48492b 100644 --- a/gui/UtilsWidgets/CustomQTableWidget/ContextMenuQTableWidget.py +++ b/gui/UtilsWidgets/CustomQTableWidget/ContextMenuQTableWidget.py @@ -35,7 +35,7 @@ def mousePressEvent(self, event): if item is not None: # print('Table Item:', item.row(), item.column()) self.current_item = item - self.context_menu.exec_(event.globalPos()) + self.context_menu.exec(event.globalPos()) super(ContextMenuQTableWidget, self).mousePressEvent(event) def get_column_values(self, column_index): diff --git a/integration_tests/__init__.py b/integration_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integration_tests/batch_study_empty_study_test.py b/integration_tests/batch_study_empty_study_test.py new file mode 100644 index 0000000..842b2f2 --- /dev/null +++ b/integration_tests/batch_study_empty_study_test.py @@ -0,0 +1,79 @@ +import os +import shutil +from time import sleep + +import requests +import zipfile + +import pytest +from PySide6.QtCore import Qt + +from gui.RaidionicsMainWindow import RaidionicsMainWindow +from gui.UtilsWidgets.CustomQDialog.ImportDataQDialog import ImportDataQDialog +from utils.software_config import SoftwareConfigResources +from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure + +def_loc = UserPreferencesStructure.getInstance().user_home_location + +@pytest.fixture +def test_location(): + test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + UserPreferencesStructure.getInstance().user_home_location = test_loc + if os.path.exists(test_loc): + shutil.rmtree(test_loc) + os.makedirs(test_loc) + return test_loc + +@pytest.fixture +def test_data_folder(): + test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.zip' + test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + test_data_dir = os.path.join(test_dir, 'ApprovedExample') + if os.path.exists(test_data_dir) and len(os.listdir(test_data_dir)) > 0: + return test_data_dir + + archive_dl_dest = os.path.join(test_dir, 'raidionics_resources.zip') + headers = {} + response = requests.get(test_data_url, headers=headers, stream=True) + response.raise_for_status() + if response.status_code == requests.codes.ok: + with open(archive_dl_dest, "wb") as f: + for chunk in response.iter_content(chunk_size=1048576): + f.write(chunk) + with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: + zip_ref.extractall(test_dir) + return test_data_dir + + +@pytest.fixture +def window(): + """ + + """ + window = RaidionicsMainWindow() + window.on_clear_scene() + return window + +""" +Remaining tests to add: +* Import patient and jump to patient view and assert that the MRIs are correctly displayed (not working now) +""" + + +def test_empty_study_creation(qtbot, test_location, window): + """ + Creation of a new empty patient. + """ + qtbot.addWidget(window) + qtbot.mouseClick(window.welcome_widget.left_panel_multiple_patients_pushbutton, Qt.MouseButton.LeftButton) + window.batch_study_widget.studies_panel.add_empty_study_action.trigger() + assert len(SoftwareConfigResources.getInstance().study_parameters) == 1 + +def test_cleanup(window): + if window.logs_thread.isRunning(): + window.logs_thread.stop() + sleep(2) + UserPreferencesStructure.getInstance().user_home_location = def_loc + test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + if os.path.exists(test_loc): + shutil.rmtree(test_loc) diff --git a/integration_tests/batch_study_loading_study_test.py b/integration_tests/batch_study_loading_study_test.py new file mode 100644 index 0000000..2cceab5 --- /dev/null +++ b/integration_tests/batch_study_loading_study_test.py @@ -0,0 +1,90 @@ +import os +import shutil +from time import sleep + +import requests +import zipfile + +import pytest +from PySide6.QtCore import Qt + +from gui.RaidionicsMainWindow import RaidionicsMainWindow +from gui.UtilsWidgets.CustomQDialog.ImportDataQDialog import ImportDataQDialog +from utils.software_config import SoftwareConfigResources +from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure + +def_loc = UserPreferencesStructure.getInstance().user_home_location + +@pytest.fixture +def test_location(): + test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + UserPreferencesStructure.getInstance().user_home_location = test_loc + if os.path.exists(test_loc): + shutil.rmtree(test_loc) + os.makedirs(test_loc) + return test_loc + +@pytest.fixture +def test_data_folder(): + test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.zip' + test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + test_data_dir = os.path.join(test_dir, 'ApprovedExample') + if os.path.exists(test_data_dir) and len(os.listdir(test_data_dir)) > 0: + return test_data_dir + + archive_dl_dest = os.path.join(test_dir, 'raidionics_resources.zip') + headers = {} + response = requests.get(test_data_url, headers=headers, stream=True) + response.raise_for_status() + if response.status_code == requests.codes.ok: + with open(archive_dl_dest, "wb") as f: + for chunk in response.iter_content(chunk_size=1048576): + f.write(chunk) + with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: + zip_ref.extractall(test_dir) + return test_data_dir + + +@pytest.fixture +def window(): + """ + + """ + window = RaidionicsMainWindow() + window.on_clear_scene() + return window + +""" +Remaining tests to add: +* Import patient and jump to patient view and assert that the MRIs are correctly displayed (not working now) +""" + + +def test_study_reloading(qtbot, test_location, test_data_folder, window): + """ + Creation of a new empty patient. + """ + qtbot.addWidget(window) + + # Entering the batch study widget view + qtbot.mouseClick(window.welcome_widget.left_panel_multiple_patients_pushbutton, Qt.MouseButton.LeftButton) + + # Importing existing study from Add study > Existing study (*.sraidionics) + # window.batch_study_widget.results_panel.add_existing_study_actionadd_raidionics_patient_action.trigger() <= Cannot use the actual pushbutton action as it would open the QDialog... + raidionics_filename = os.path.join(test_data_folder, 'Raidionics', "studies", "study1", "study1_study.sraidionics") + window.batch_study_widget.import_data_dialog.reset() + window.batch_study_widget.import_data_dialog.set_parsing_filter("study") + window.batch_study_widget.import_data_dialog.setup_interface_from_files([raidionics_filename]) + window.batch_study_widget.import_data_dialog.__on_exit_accept_clicked() + sleep(10) + assert len(list(SoftwareConfigResources.getInstance().get_active_study().included_patients_uids.keys())) == 2 + + +def test_cleanup(window): + if window.logs_thread.isRunning(): + window.logs_thread.stop() + sleep(2) + UserPreferencesStructure.getInstance().user_home_location = def_loc + test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + if os.path.exists(test_loc): + shutil.rmtree(test_loc) diff --git a/integration_tests/single_patient_empty_patient_test.py b/integration_tests/single_patient_empty_patient_test.py new file mode 100644 index 0000000..c8e9b84 --- /dev/null +++ b/integration_tests/single_patient_empty_patient_test.py @@ -0,0 +1,209 @@ +import logging +import os +import shutil +from time import sleep + +import requests +import zipfile + +import pytest +from PySide6.QtCore import Qt + +from gui.RaidionicsMainWindow import RaidionicsMainWindow +from gui.UtilsWidgets.CustomQDialog.ImportDataQDialog import ImportDataQDialog +from utils.software_config import SoftwareConfigResources +from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure + +def_loc = UserPreferencesStructure.getInstance().user_home_location + +@pytest.fixture +def test_location(): + test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + UserPreferencesStructure.getInstance().user_home_location = test_loc + os.makedirs(test_loc, exist_ok=True) + if os.path.exists(os.path.join(test_loc, "patients")): + shutil.rmtree(os.path.join(test_loc, "patients")) + if os.path.exists(os.path.join(test_loc, "studies")): + shutil.rmtree(os.path.join(test_loc, "studies")) + return test_loc + +@pytest.fixture +def test_data_folder(): + test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.zip' + test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + test_data_dir = os.path.join(test_dir, 'ApprovedExample') + if os.path.exists(test_data_dir) and len(os.listdir(test_data_dir)) > 0: + return test_data_dir + + archive_dl_dest = os.path.join(test_dir, 'raidionics_resources.zip') + headers = {} + response = requests.get(test_data_url, headers=headers, stream=True) + response.raise_for_status() + if response.status_code == requests.codes.ok: + with open(archive_dl_dest, "wb") as f: + for chunk in response.iter_content(chunk_size=1048576): + f.write(chunk) + with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: + zip_ref.extractall(test_dir) + return test_data_dir + + +@pytest.fixture +def window(): + """ + + """ + window = RaidionicsMainWindow() + window.on_clear_scene() + return window + + +""" Remaining tests to add +# * Loading data from DICOM folder (must add a DICOM folder in the test data package +# * Adding extra MRI volume from the same DICOM folder afterwards +# * Open a new DICOM folder +# * Delete an image +""" + +def test_empty_patient_creation(qtbot, test_location, window): + """ + Creation of a new empty patient. + """ + qtbot.addWidget(window) + qtbot.mouseClick(window.welcome_widget.left_panel_single_patient_pushbutton, Qt.MouseButton.LeftButton) + window.single_patient_widget.results_panel.add_empty_patient_action.trigger() + assert len(SoftwareConfigResources.getInstance().patients_parameters) == 1 + +def test_empty_patient_renaming(qtbot, test_location, window): + """ + Creation of a new empty patient followed by renaming. + """ + qtbot.addWidget(window) + qtbot.mouseClick(window.welcome_widget.left_panel_single_patient_pushbutton, Qt.MouseButton.LeftButton) + window.single_patient_widget.results_panel.add_empty_patient_action.trigger() + window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).patient_name_lineedit.setText("Patient1") + qtbot.keyClick(window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).patient_name_lineedit, Qt.Key_Enter) + assert SoftwareConfigResources.getInstance().get_active_patient().display_name == "Patient1" + + qtbot.mouseClick(window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).save_patient_pushbutton, Qt.MouseButton.LeftButton) + +def test_empty_patient_timestamp_creation_and_renaming(qtbot, test_location, window): + """ + Creation of a new timestamp for an empty patient and renaming. + """ + qtbot.addWidget(window) + + # Entering the single patient widget view + qtbot.mouseClick(window.welcome_widget.left_panel_single_patient_pushbutton, Qt.MouseButton.LeftButton) + + # Clicking on the Import patient > Empty patient button + window.single_patient_widget.results_panel.add_empty_patient_action.trigger() + qtbot.mouseClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamp_add_pushbutton, Qt.MouseButton.LeftButton) + window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).timestamp_name_lineedit.setText("PreOp") + qtbot.keyClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).timestamp_name_lineedit, Qt.Key_Enter) + assert SoftwareConfigResources.getInstance().get_active_patient().get_active_investigation_timestamp().display_name == "PreOp" + + qtbot.mouseClick(window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).save_patient_pushbutton, Qt.MouseButton.LeftButton) + +def test_empty_patient_timestamp_selection_and_removal(qtbot, test_location, window): + """ + Creation of multiple timestamps for an empty patient, renaming, swapping between them, and removing them. + """ + qtbot.addWidget(window) + + # Entering the single patient widget view + qtbot.mouseClick(window.welcome_widget.left_panel_single_patient_pushbutton, Qt.MouseButton.LeftButton) + + # Clicking on the Import patient > Empty patient button + window.single_patient_widget.results_panel.add_empty_patient_action.trigger() + + # Adding a new timestamp and renaming to PreOp + qtbot.mouseClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamp_add_pushbutton, Qt.MouseButton.LeftButton) + window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).timestamp_name_lineedit.setText("PreOp") + qtbot.keyClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).timestamp_name_lineedit, Qt.Key_Enter) + + # Adding a new timestamp and renaming to PostOp (will automatically display the new timestamp) + qtbot.mouseClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamp_add_pushbutton, Qt.MouseButton.LeftButton) + window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(1).timestamp_name_lineedit.setText("PostOp") + qtbot.keyClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(1).timestamp_name_lineedit, Qt.Key_Enter) + + # Setting the first timestamp in view (i.e., PreOp) + window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamp_selector_combobox.setCurrentIndex(0) + assert SoftwareConfigResources.getInstance().get_active_patient().get_active_investigation_timestamp().display_name == "PreOp" + + # Deleting the current timestamp (i.e., PreOp) + qtbot.mouseClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamp_remove_pushbutton, Qt.MouseButton.LeftButton) + assert SoftwareConfigResources.getInstance().get_active_patient().get_active_investigation_timestamp().display_name == "PostOp" + + # Deleting the current timestamp (i.e., PostOp) + qtbot.mouseClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamp_remove_pushbutton, Qt.MouseButton.LeftButton) + assert len(list(SoftwareConfigResources.getInstance().get_active_patient()._investigation_timestamps.keys())) == 0 + + # Saving the latest modifications to the patient on disk by pressing the disk icon + qtbot.mouseClick(window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).save_patient_pushbutton, Qt.MouseButton.LeftButton) + +def test_empty_patient_timestamp_data_inclusion(qtbot, test_location, test_data_folder, window): + """ + Creation of a new timestamp for an empty patient and importing two radiological volumes. + """ + qtbot.addWidget(window) + + # Entering the single patient widget view + qtbot.mouseClick(window.welcome_widget.left_panel_single_patient_pushbutton, Qt.MouseButton.LeftButton) + + # Clicking on the Import patient > Empty patient button + window.single_patient_widget.results_panel.add_empty_patient_action.trigger() + + # Adding a new timestamp and renaming to PreOp + qtbot.mouseClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamp_add_pushbutton, Qt.MouseButton.LeftButton) + window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).timestamp_name_lineedit.setText("PreOp") + qtbot.keyClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).timestamp_name_lineedit, Qt.Key_Enter) + + # Importing two radiological volumes to the current timestamp (e.g., PreOp) + t1_sample_mri_filename = os.path.join(test_data_folder, 'Raw', 'Case27-T1.nii.gz') + flair_sample_mri_filename = os.path.join(test_data_folder, 'Raw', 'Case27-FLAIR.nii.gz') + window.single_patient_widget.import_data_dialog.setup_interface_from_files([t1_sample_mri_filename, flair_sample_mri_filename]) + window.single_patient_widget.import_data_dialog.__on_exit_accept_clicked() + assert len(list(SoftwareConfigResources.getInstance().get_active_patient().mri_volumes.keys())) == 2 + + # Using the ComboBox to change the radiological volume sequence from T1-w to T1-CE + window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).volumes_collapsiblegroupbox.volumes_widget[list(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamps_widget[ + list(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamps_widget.keys())[0]].volumes_collapsiblegroupbox.volumes_widget.keys())[0]].sequence_type_combobox.setCurrentIndex(1) + + # Changing the display name for the T1-CE MRI volume to case27-t1c + window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).volumes_collapsiblegroupbox.get_layer_widget_by_index(0).display_name_lineedit.setText("case27-t1c") + qtbot.keyClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).volumes_collapsiblegroupbox.get_layer_widget_by_index(0).display_name_lineedit, Qt.Key_Enter) + + # Changing the display name for the FLAIR MRI volume to case27-flair + window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).volumes_collapsiblegroupbox.get_layer_widget_by_index(1).display_name_lineedit.setText("case27-flair") + qtbot.keyClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).volumes_collapsiblegroupbox.get_layer_widget_by_index(1).display_name_lineedit, Qt.Key_Enter) + + t1c_display_name = window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).volumes_collapsiblegroupbox.volumes_widget[list(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamps_widget[ + list(window.single_patient_widget.layers_panel.timestamp_layer_widget.timestamps_widget.keys())[0]].volumes_collapsiblegroupbox.volumes_widget.keys())[0]].display_name_lineedit.text() + assert t1c_display_name == "case27-t1c" + assert SoftwareConfigResources.getInstance().get_active_patient().mri_volumes[SoftwareConfigResources.getInstance().get_active_patient().get_mri_by_display_name(t1c_display_name)].get_sequence_type_str() == "T1-CE" + + # Setting the FLAIR input volume visible + qtbot.mouseClick(window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).volumes_collapsiblegroupbox.get_layer_widget_by_index(1).display_toggle_radiobutton, Qt.MouseButton.LeftButton) + assert window.single_patient_widget.center_panel.display_area_widget.displayed_image_uid == SoftwareConfigResources.getInstance().get_active_patient().get_mri_volume_by_display_name("case27-flair").unique_id + + # Removing the FLAIR radiological input from the timestamp + window.single_patient_widget.layers_panel.timestamp_layer_widget.get_timestamp_widget_by_index(0).volumes_collapsiblegroupbox.get_layer_widget_by_index(1).delete_layer_action.trigger() + assert window.single_patient_widget.center_panel.display_area_widget.displayed_image_uid == SoftwareConfigResources.getInstance().get_active_patient().get_mri_volume_by_display_name("case27-t1c").unique_id + + # Saving the latest modifications to the patient on disk by pressing the disk icon + qtbot.mouseClick(window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).save_patient_pushbutton, Qt.MouseButton.LeftButton) + + +def test_cleanup(window): + """ + To delete the temporary resources needed for running the different tests and to prevent Core dumped error when + the window object will be destroyed (manually stopping running threads) + """ + if window.logs_thread.isRunning(): + window.logs_thread.stop() + sleep(2) + UserPreferencesStructure.getInstance().user_home_location = def_loc + test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + if os.path.exists(test_loc): + shutil.rmtree(test_loc) diff --git a/integration_tests/single_patient_patient_loading_test.py b/integration_tests/single_patient_patient_loading_test.py new file mode 100644 index 0000000..505aa07 --- /dev/null +++ b/integration_tests/single_patient_patient_loading_test.py @@ -0,0 +1,142 @@ +import os +import shutil +from time import sleep + +import requests +import zipfile + +import pytest +from PySide6.QtCore import Qt + +from gui.RaidionicsMainWindow import RaidionicsMainWindow +from gui.UtilsWidgets.CustomQDialog.ImportDataQDialog import ImportDataQDialog +from gui.UtilsWidgets.CustomQDialog.ImportFoldersQDialog import ImportFolderLineWidget +from utils.data_structures.AnnotationStructure import AnnotationClassType +from utils.software_config import SoftwareConfigResources +from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure + + +def_loc = UserPreferencesStructure.getInstance().user_home_location + +@pytest.fixture +def test_location(): + test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + UserPreferencesStructure.getInstance().user_home_location = test_loc + os.makedirs(test_loc, exist_ok=True) + if os.path.exists(os.path.join(test_loc, "patients")): + shutil.rmtree(os.path.join(test_loc, "patients")) + if os.path.exists(os.path.join(test_loc, "studies")): + shutil.rmtree(os.path.join(test_loc, "studies")) + return test_loc + +@pytest.fixture +def test_data_folder(): + test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.zip' + test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + test_data_dir = os.path.join(test_dir, 'ApprovedExample') + if os.path.exists(test_data_dir) and len(os.listdir(test_data_dir)) > 0: + return test_data_dir + + archive_dl_dest = os.path.join(test_dir, 'raidionics_resources.zip') + headers = {} + response = requests.get(test_data_url, headers=headers, stream=True) + response.raise_for_status() + if response.status_code == requests.codes.ok: + with open(archive_dl_dest, "wb") as f: + for chunk in response.iter_content(chunk_size=1048576): + f.write(chunk) + with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: + zip_ref.extractall(test_dir) + return test_data_dir + + +@pytest.fixture +def window(): + """ + + """ + window = RaidionicsMainWindow() + window.on_clear_scene() + return window + +""" Remaining tests to add: +# * Changing display space from Patient to MNI and back +# * Changing the parent MRI for annotation files +# * Deleting an annotation +""" + +def test_patient_loading_from_files(qtbot, test_location, test_data_folder, window): + """ + + """ + qtbot.addWidget(window) + # Entering the single patient widget view + qtbot.mouseClick(window.welcome_widget.left_panel_single_patient_pushbutton, Qt.MouseButton.LeftButton) + + # Importing MRI files from Import patient > Other data type (*.nii) + # window.single_patient_widget.results_panel.add_other_data_action.trigger() <= Cannot use the actual pushbutton action as it would open the QDialog... + window.single_patient_widget.results_panel.on_add_new_empty_patient() + t1_sample_mri_filename = os.path.join(test_data_folder, 'Raw', 'Case27-T1.nii.gz') + flair_sample_mri_filename = os.path.join(test_data_folder, 'Raw', 'Case27-FLAIR.nii.gz') + window.single_patient_widget.import_data_dialog.setup_interface_from_files([t1_sample_mri_filename, flair_sample_mri_filename]) + window.single_patient_widget.import_data_dialog.__on_exit_accept_clicked() + assert len(list(SoftwareConfigResources.getInstance().get_active_patient().mri_volumes.keys())) == 2 + + # Saving the latest modifications to the patient on disk by pressing the disk icon + qtbot.mouseClick(window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).save_patient_pushbutton, Qt.MouseButton.LeftButton) + +def test_patient_loading_from_folder(qtbot, test_location, test_data_folder, window): + """ + + """ + qtbot.addWidget(window) + # Entering the single patient widget view + qtbot.mouseClick(window.welcome_widget.left_panel_single_patient_pushbutton, Qt.MouseButton.LeftButton) + + # Importing MRI files from Import patient > Other data type (*.nii) + # window.single_patient_widget.results_panel.add_folder_data_action.trigger() <= Cannot use the actual pushbutton action as it would open the QDialog... + window.single_patient_widget.results_panel.on_add_new_empty_patient() + sample_folder = os.path.join(test_data_folder, 'Raw') + + window.single_patient_widget.import_folder_dialog.reset() + window.single_patient_widget.import_folder_dialog.set_parsing_mode("single") + window.single_patient_widget.import_folder_dialog.set_target_type("regular") + wid = ImportFolderLineWidget() + wid.filepath_lineedit.setText(sample_folder) + window.single_patient_widget.import_folder_dialog.import_scrollarea_layout.insertWidget(window.single_patient_widget.import_folder_dialog.import_scrollarea_layout.count() - 1, wid) + window.single_patient_widget.import_folder_dialog.__on_exit_accept_clicked() + assert len(list(SoftwareConfigResources.getInstance().get_patient_by_display_name("Raw").mri_volumes.keys())) == 2 + + # Saving the latest modifications to the patient on disk by pressing the disk icon + qtbot.mouseClick(window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).save_patient_pushbutton, Qt.MouseButton.LeftButton) + + +def test_patient_loading_from_raidionics(qtbot, test_location, test_data_folder, window): + """ + + """ + qtbot.addWidget(window) + # Entering the single patient widget view + qtbot.mouseClick(window.welcome_widget.left_panel_single_patient_pushbutton, Qt.MouseButton.LeftButton) + + # Importing MRI files from Import patient > Other data type (*.nii) + # window.single_patient_widget.results_panel.add_raidionics_patient_action.trigger() <= Cannot use the actual pushbutton action as it would open the QDialog... + raidionics_filename = os.path.join(test_data_folder, 'Raidionics', "patients", "patient1", "patient1_scene.raidionics") + window.single_patient_widget.import_data_dialog.reset() + window.single_patient_widget.import_data_dialog.set_parsing_filter("patient") + window.single_patient_widget.import_data_dialog.setup_interface_from_files([raidionics_filename]) + window.single_patient_widget.import_data_dialog.__on_exit_accept_clicked() + sleep(5) + assert len(list(SoftwareConfigResources.getInstance().get_active_patient().mri_volumes.keys())) == 2 + + # Saving the latest modifications to the patient on disk by pressing the disk icon + qtbot.mouseClick(window.single_patient_widget.results_panel.get_patient_results_widget_by_index(0).save_patient_pushbutton, Qt.MouseButton.LeftButton) + +def test_cleanup(window): + if window.logs_thread.isRunning(): + window.logs_thread.stop() + sleep(2) + UserPreferencesStructure.getInstance().user_home_location = def_loc + test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') + if os.path.exists(test_loc): + shutil.rmtree(test_loc) diff --git a/utils/backend_logic.py b/utils/backend_logic.py index d508649..c77a7cc 100644 --- a/utils/backend_logic.py +++ b/utils/backend_logic.py @@ -145,9 +145,9 @@ def run_pipeline(task: str, model_name: str, patient_parameters: PatientParamete # ret = result.get()[0] results = collect_results(patient_parameters, pipeline) - except Exception: - logging.error('Pipeline process for patient {}, for task {} failed with: \n{}'.format(patient_parameters.unique_id, - task, traceback.format_exc())) + except Exception as e: + logging.error('[Software error] Pipeline process for patient {}, for task {} failed.

Reason: {}. \n{}'.format(patient_parameters.unique_id, + task, e, traceback.format_exc())) if os.path.exists(rads_config_filename): os.remove(rads_config_filename) if os.path.exists(pipeline_filename): diff --git a/utils/data_structures/AnnotationStructure.py b/utils/data_structures/AnnotationStructure.py index e78f05a..5e253a2 100644 --- a/utils/data_structures/AnnotationStructure.py +++ b/utils/data_structures/AnnotationStructure.py @@ -82,22 +82,25 @@ class AnnotationVolume: def __init__(self, uid: str, input_filename: str, output_patient_folder: str, inv_ts_uid: str, parent_mri_uid: str, inv_ts_folder_name: str = None, reload_params: {} = None) -> None: - self.__reset() - self._unique_id = uid - self._raw_input_filepath = input_filename - self._output_patient_folder = output_patient_folder - self._timestamp_uid = inv_ts_uid - if inv_ts_folder_name: - self._timestamp_folder_name = inv_ts_folder_name - else: - self._timestamp_folder_name = self._timestamp_uid - self._display_name = uid - self._parent_mri_uid = parent_mri_uid - - if reload_params: - self.__reload_from_disk(reload_params) - else: - self.__init_from_scratch() + try: + self.__reset() + self._unique_id = uid + self._raw_input_filepath = input_filename + self._output_patient_folder = output_patient_folder + self._timestamp_uid = inv_ts_uid + if inv_ts_folder_name: + self._timestamp_folder_name = inv_ts_folder_name + else: + self._timestamp_folder_name = self._timestamp_uid + self._display_name = uid + self._parent_mri_uid = parent_mri_uid + + if reload_params: + self.__reload_from_disk(reload_params) + else: + self.__init_from_scratch() + except Exception as e: + raise RuntimeError(e) def __reset(self): """ @@ -130,11 +133,10 @@ def unique_id(self) -> str: def load_in_memory(self) -> None: try: - if UserPreferencesStructure.getInstance().display_space != 'Patient': - if self._resampled_input_volume_filepath and os.path.exists(self._resampled_input_volume_filepath): - self._resampled_input_volume = nib.load(self._resampled_input_volume_filepath).get_fdata()[:] - else: - self.__generate_standardized_input_volume() + if self._resampled_input_volume_filepath and os.path.exists(self._resampled_input_volume_filepath): + self._resampled_input_volume = nib.load(self._resampled_input_volume_filepath).get_fdata()[:] + else: + self.__generate_standardized_input_volume() self.__generate_display_volume() except Exception as e: raise ValueError("[AnnotationStructure] Loading in memory failed with: {}".format(e)) @@ -162,8 +164,7 @@ def delete(self) -> None: for k in list(self.registered_volume_filepaths.keys()): os.remove(self.registered_volume_filepaths[k]) except Exception as e: - logging.error("[Software error] Annotation structure deletion failed with: {}.\n {}".format( - e, traceback.format_exc())) + raise RuntimeError("Annotation structure deletion failed with: {}".format(e)) def set_unsaved_changes_state(self, state: bool) -> None: self._unsaved_changes = state @@ -235,16 +236,22 @@ def display_name(self, name: str) -> None: logging.debug("Unsaved changes - Annotation volume display name changed to {}.".format(name)) def set_output_patient_folder(self, output_folder: str) -> None: - if self._raw_input_filepath and self._output_patient_folder in self._raw_input_filepath: - self._raw_input_filepath = self._raw_input_filepath.replace(self._output_patient_folder, output_folder) - if self._usable_input_filepath and self._output_patient_folder in self._usable_input_filepath: - self._usable_input_filepath = self._usable_input_filepath.replace(self._output_patient_folder, - output_folder) - if self._resampled_input_volume_filepath: - self._resampled_input_volume_filepath = self._resampled_input_volume_filepath.replace( - self._output_patient_folder, output_folder) - - self._output_patient_folder = output_folder + try: + if self._raw_input_filepath and self._output_patient_folder in self._raw_input_filepath: + self._raw_input_filepath = self._raw_input_filepath.replace(self._output_patient_folder, output_folder) + if self._usable_input_filepath and self._output_patient_folder in self._usable_input_filepath: + self._usable_input_filepath = self._usable_input_filepath.replace(self._output_patient_folder, + output_folder) + if self._resampled_input_volume_filepath: + self._resampled_input_volume_filepath = self._resampled_input_volume_filepath.replace( + self._output_patient_folder, output_folder) + + for i, fn in enumerate(self.registered_volume_filepaths.keys()): + self.registered_volume_filepaths[fn] = self.registered_volume_filepaths[fn].replace(self._output_patient_folder, output_folder) + + self._output_patient_folder = output_folder + except Exception as e: + raise ValueError("Changing the output patient folder name for the AnnotationStructure failed with: {}".format(e)) @property def output_patient_folder(self) -> str: @@ -260,53 +267,58 @@ def timestamp_folder_name(self) -> str: @timestamp_folder_name.setter def timestamp_folder_name(self, folder_name: str) -> None: - self._timestamp_folder_name = folder_name - if self._output_patient_folder in self._raw_input_filepath: - if os.name == 'nt': - path_parts = list(PurePath(os.path.relpath(self._raw_input_filepath, - self._output_patient_folder)).parts[1:]) - rel_path = PurePath() - rel_path = rel_path.joinpath(self._output_patient_folder) - rel_path = rel_path.joinpath(self._timestamp_folder_name) - for x in path_parts: - rel_path = rel_path.joinpath(x) - self._raw_input_filepath = os.fspath(rel_path) - else: - rel_path = '/'.join(os.path.relpath(self._raw_input_filepath, - self._output_patient_folder).split('/')[1:]) - self._raw_input_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, - rel_path) - if self._output_patient_folder in self._usable_input_filepath: - if os.name == 'nt': - path_parts = list(PurePath(os.path.relpath(self._usable_input_filepath, - self._output_patient_folder)).parts[1:]) - rel_path = PurePath() - rel_path = rel_path.joinpath(self._output_patient_folder) - rel_path = rel_path.joinpath(self._timestamp_folder_name) - for x in path_parts: - rel_path = rel_path.joinpath(x) - self._usable_input_filepath = os.fspath(rel_path) - else: - rel_path = '/'.join(os.path.relpath(self._usable_input_filepath, - self._output_patient_folder).split('/')[1:]) - self._usable_input_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, - rel_path) - if self._resampled_input_volume_filepath and \ - self._output_patient_folder in self._resampled_input_volume_filepath: - if os.name == 'nt': - path_parts = list(PurePath(os.path.relpath(self._resampled_input_volume_filepath, - self._output_patient_folder)).parts[1:]) - rel_path = PurePath() - rel_path = rel_path.joinpath(self._output_patient_folder) - rel_path = rel_path.joinpath(self._timestamp_folder_name) - for x in path_parts: - rel_path = rel_path.joinpath(x) - self._resampled_input_volume_filepath = os.fspath(rel_path) - else: - rel_path = '/'.join(os.path.relpath(self._resampled_input_volume_filepath, - self._output_patient_folder).split('/')[1:]) - self._resampled_input_volume_filepath = os.path.join(self._output_patient_folder, - self._timestamp_folder_name, rel_path) + try: + for i, fn in enumerate(self.registered_volume_filepaths.keys()): + self.registered_volume_filepaths[fn] = self.registered_volume_filepaths[fn].replace(self._timestamp_folder_name, folder_name) + self._timestamp_folder_name = folder_name + if self._output_patient_folder in self._raw_input_filepath: + if os.name == 'nt': + path_parts = list(PurePath(os.path.relpath(self._raw_input_filepath, + self._output_patient_folder)).parts[1:]) + rel_path = PurePath() + rel_path = rel_path.joinpath(self._output_patient_folder) + rel_path = rel_path.joinpath(self._timestamp_folder_name) + for x in path_parts: + rel_path = rel_path.joinpath(x) + self._raw_input_filepath = os.fspath(rel_path) + else: + rel_path = '/'.join(os.path.relpath(self._raw_input_filepath, + self._output_patient_folder).split('/')[1:]) + self._raw_input_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, + rel_path) + if self._output_patient_folder in self._usable_input_filepath: + if os.name == 'nt': + path_parts = list(PurePath(os.path.relpath(self._usable_input_filepath, + self._output_patient_folder)).parts[1:]) + rel_path = PurePath() + rel_path = rel_path.joinpath(self._output_patient_folder) + rel_path = rel_path.joinpath(self._timestamp_folder_name) + for x in path_parts: + rel_path = rel_path.joinpath(x) + self._usable_input_filepath = os.fspath(rel_path) + else: + rel_path = '/'.join(os.path.relpath(self._usable_input_filepath, + self._output_patient_folder).split('/')[1:]) + self._usable_input_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, + rel_path) + if self._resampled_input_volume_filepath and \ + self._output_patient_folder in self._resampled_input_volume_filepath: + if os.name == 'nt': + path_parts = list(PurePath(os.path.relpath(self._resampled_input_volume_filepath, + self._output_patient_folder)).parts[1:]) + rel_path = PurePath() + rel_path = rel_path.joinpath(self._output_patient_folder) + rel_path = rel_path.joinpath(self._timestamp_folder_name) + for x in path_parts: + rel_path = rel_path.joinpath(x) + self._resampled_input_volume_filepath = os.fspath(rel_path) + else: + rel_path = '/'.join(os.path.relpath(self._resampled_input_volume_filepath, + self._output_patient_folder).split('/')[1:]) + self._resampled_input_volume_filepath = os.path.join(self._output_patient_folder, + self._timestamp_folder_name, rel_path) + except Exception as e: + raise ValueError("Changing the timestamp folder name for the AnnotationStructure failed with: {}".format(e)) def get_display_opacity(self) -> int: return self._display_opacity @@ -413,8 +425,7 @@ def save(self) -> dict: self._unsaved_changes = False return volume_params except Exception as e: - logging.error("[Software error] AnnotationStructure saving failed with: {}.\n {}".format( - e, traceback.format_exc())) + raise RuntimeError("AnnotationStructure saving failed with: {}".format(e)) def import_registered_volume(self, filepath: str, registration_space: str) -> None: """ @@ -431,8 +442,8 @@ def import_registered_volume(self, filepath: str, registration_space: str) -> No logging.debug("""Unsaved changes - Registered annotation volume to space {} added in {}.""".format( registration_space, dest_path)) self._unsaved_changes = True - except Exception: - logging.error("[Software error] Error while importing a registered annotation volume.\n {}".format(traceback.format_exc())) + except Exception as e: + raise RuntimeError("Error while importing a registered annotation volume with: {}".format(e)) def __init_from_scratch(self) -> None: try: @@ -449,8 +460,8 @@ def __init_from_scratch(self) -> None: if self._output_patient_folder not in self.raw_input_filepath: self.__generate_display_volume() except Exception as e: - logging.error("""[Software error] Initializing annotation structure from scratch failed - for: {} with: {}.\n {}""".format(self._raw_input_filepath, e, traceback.format_exc())) + raise RuntimeError("""Initializing annotation structure from scratch failed for: {} + with: {}.""".format(self._raw_input_filepath, e)) def __reload_from_disk(self, parameters: dict) -> None: """ @@ -502,8 +513,8 @@ def __reload_from_disk(self, parameters: dict) -> None: self._display_color = parameters['display_color'] self._display_opacity = parameters['display_opacity'] except Exception as e: - logging.error(""" [Software error] Reloading annotation structure from disk failed - for: {} with: {}.\n {}""".format(self.display_name, e, traceback.format_exc())) + raise RuntimeError("""Reloading annotation structure from disk failed for: {} + with: {}.""".format(self.display_name, e)) def __generate_standardized_input_volume(self) -> None: """ diff --git a/utils/data_structures/AtlasStructure.py b/utils/data_structures/AtlasStructure.py index 9e3211a..10f1da5 100644 --- a/utils/data_structures/AtlasStructure.py +++ b/utils/data_structures/AtlasStructure.py @@ -42,23 +42,26 @@ class AtlasVolume: def __init__(self, uid: str, input_filename: str, output_patient_folder: str, inv_ts_uid: str, parent_mri_uid: str, description_filename: str, inv_ts_folder_name: str = None, reload_params: dict = None) -> None: - self.__reset() - self._unique_id = uid - self._raw_input_filepath = input_filename - self._output_patient_folder = output_patient_folder - self._timestamp_uid = inv_ts_uid - if inv_ts_folder_name: - self._timestamp_folder_name = inv_ts_folder_name - else: - self._timestamp_folder_name = self._timestamp_uid - - self._class_description_filename = description_filename - self._parent_mri_uid = parent_mri_uid - - if reload_params: - self.__reload_from_disk(reload_params) - else: - self.__init_from_scratch() + try: + self.__reset() + self._unique_id = uid + self._raw_input_filepath = input_filename + self._output_patient_folder = output_patient_folder + self._timestamp_uid = inv_ts_uid + if inv_ts_folder_name: + self._timestamp_folder_name = inv_ts_folder_name + else: + self._timestamp_folder_name = self._timestamp_uid + + self._class_description_filename = description_filename + self._parent_mri_uid = parent_mri_uid + + if reload_params: + self.__reload_from_disk(reload_params) + else: + self.__init_from_scratch() + except Exception as e: + raise RuntimeError(e) def __reset(self): """ @@ -117,8 +120,8 @@ def __init_from_scratch(self) -> None: elif "MNI" in self._unique_id: self._display_name = "MNI group" except Exception as e: - logging.error("""[Software error] Initializing atlas structure from scratch failed - for: {} with: {}.\n {}""".format(self._raw_input_filepath, e, traceback.format_exc())) + raise RuntimeError("""Initializing atlas structure from scratch failed for: {} + with: {}.""".format(self._raw_input_filepath, e)) def __reload_from_disk(self, parameters: dict) -> None: """ @@ -156,8 +159,8 @@ def __reload_from_disk(self, parameters: dict) -> None: if 'display_opacities' in parameters.keys(): self._class_display_opacity = {int(k): v for k, v in parameters['display_opacities'].items()} except Exception as e: - logging.error(""" [Software error] Reloading atlas structure from disk failed - for: {} with: {}.\n {}""".format(self.display_name, e, traceback.format_exc())) + raise RuntimeError("""Reloading atlas structure from disk failed for: {} + with: {}.""".format(self.display_name, e)) def load_in_memory(self) -> None: try: @@ -258,15 +261,20 @@ def set_class_opacity_by_index(self, index: int, opacity: int) -> None: self._unsaved_changes = True def set_output_patient_folder(self, output_folder: str) -> None: - if self._raw_input_filepath and self._output_patient_folder in self._raw_input_filepath: - self._raw_input_filepath = self._raw_input_filepath.replace(self._output_patient_folder, output_folder) - if self._class_description_filename and self._output_patient_folder in self._class_description_filename: - self._class_description_filename = self._class_description_filename.replace(self._output_patient_folder, - output_folder) - if self._resampled_input_volume_filepath: - self._resampled_input_volume_filepath = self._resampled_input_volume_filepath.replace(self._output_patient_folder, - output_folder) - self._output_patient_folder = output_folder + try: + if self._raw_input_filepath and self._output_patient_folder in self._raw_input_filepath: + self._raw_input_filepath = self._raw_input_filepath.replace(self._output_patient_folder, output_folder) + if self._class_description_filename and self._output_patient_folder in self._class_description_filename: + self._class_description_filename = self._class_description_filename.replace(self._output_patient_folder, + output_folder) + if self._resampled_input_volume_filepath: + self._resampled_input_volume_filepath = self._resampled_input_volume_filepath.replace(self._output_patient_folder, + output_folder) + for i, fn in enumerate(self.atlas_space_filepaths.keys()): + self.atlas_space_filepaths[fn] = self.atlas_space_filepaths[fn].replace(self._output_patient_folder, output_folder) + self._output_patient_folder = output_folder + except Exception as e: + raise ValueError("Changing the output patient folder name for the AtlasStructure failed with: {}".format(e)) @property def timestamp_uid(self) -> str: @@ -282,38 +290,44 @@ def timestamp_folder_name(self) -> str: @timestamp_folder_name.setter def timestamp_folder_name(self, folder_name: str) -> None: - self._timestamp_folder_name = folder_name - if self._output_patient_folder in self._raw_input_filepath: - if os.name == 'nt': - path_parts = list(PurePath(os.path.relpath(self._raw_input_filepath, - self._output_patient_folder)).parts[1:]) - rel_path = PurePath() - rel_path = rel_path.joinpath(self._output_patient_folder) - rel_path = rel_path.joinpath(self._timestamp_folder_name) - for x in path_parts: - rel_path = rel_path.joinpath(x) - self._raw_input_filepath = os.fspath(rel_path) - else: - rel_path = '/'.join(os.path.relpath(self._raw_input_filepath, - self._output_patient_folder).split('/')[1:]) - self._raw_input_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, - rel_path) - if self._resampled_input_volume_filepath: - if os.name == 'nt': - path_parts = list(PurePath(os.path.relpath(self._resampled_input_volume_filepath, - self._output_patient_folder)).parts[1:]) - rel_path = PurePath() - rel_path = rel_path.joinpath(self._output_patient_folder) - rel_path = rel_path.joinpath(self._timestamp_folder_name) - for x in path_parts: - rel_path = rel_path.joinpath(x) - self._resampled_input_volume_filepath = os.fspath(rel_path) - else: - rel_path = '/'.join(os.path.relpath(self._resampled_input_volume_filepath, - self._output_patient_folder).split('/')[1:]) - self._resampled_input_volume_filepath = os.path.join(self._output_patient_folder, - self._timestamp_folder_name, - rel_path) + try: + for i, fn in enumerate(self.atlas_space_filepaths.keys()): + self.atlas_space_filepaths[fn] = self.atlas_space_filepaths[fn].replace(self._timestamp_folder_name, folder_name) + + self._timestamp_folder_name = folder_name + if self._output_patient_folder in self._raw_input_filepath: + if os.name == 'nt': + path_parts = list(PurePath(os.path.relpath(self._raw_input_filepath, + self._output_patient_folder)).parts[1:]) + rel_path = PurePath() + rel_path = rel_path.joinpath(self._output_patient_folder) + rel_path = rel_path.joinpath(self._timestamp_folder_name) + for x in path_parts: + rel_path = rel_path.joinpath(x) + self._raw_input_filepath = os.fspath(rel_path) + else: + rel_path = '/'.join(os.path.relpath(self._raw_input_filepath, + self._output_patient_folder).split('/')[1:]) + self._raw_input_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, + rel_path) + if self._resampled_input_volume_filepath: + if os.name == 'nt': + path_parts = list(PurePath(os.path.relpath(self._resampled_input_volume_filepath, + self._output_patient_folder)).parts[1:]) + rel_path = PurePath() + rel_path = rel_path.joinpath(self._output_patient_folder) + rel_path = rel_path.joinpath(self._timestamp_folder_name) + for x in path_parts: + rel_path = rel_path.joinpath(x) + self._resampled_input_volume_filepath = os.fspath(rel_path) + else: + rel_path = '/'.join(os.path.relpath(self._resampled_input_volume_filepath, + self._output_patient_folder).split('/')[1:]) + self._resampled_input_volume_filepath = os.path.join(self._output_patient_folder, + self._timestamp_folder_name, + rel_path) + except Exception as e: + raise ValueError("Changing the timestamp folder name for the AtlasStructure failed with: {}".format(e)) def get_class_description(self) -> Union[pd.DataFrame, dict]: return self._class_description @@ -344,8 +358,8 @@ def import_atlas_in_registration_space(self, filepath: str, registration_space: logging.debug("""Unsaved changes - Structures atlas in space {} added in {}.""".format( registration_space, filepath)) self._unsaved_changes = True - except Exception: - logging.error(" [Software error] Error while importing a registered radiological volume.\n {}".format(traceback.format_exc())) + except Exception as e: + raise ValueError("Error while importing a registered radiological volume with: {}".format(e)) def save(self) -> dict: """ @@ -382,8 +396,8 @@ def save(self) -> dict: volume_params['display_opacities'] = self._class_display_opacity self._unsaved_changes = False return volume_params - except Exception: - logging.error(" [Software error] AtlasStructure saving failed with:\n {}".format(traceback.format_exc())) + except Exception as e: + raise RuntimeError("AtlasStructure saving failed with: {}".format(e)) def __generate_standardized_input_volume(self) -> None: """ diff --git a/utils/data_structures/InvestigationTimestampStructure.py b/utils/data_structures/InvestigationTimestampStructure.py index ad745eb..4df41e6 100644 --- a/utils/data_structures/InvestigationTimestampStructure.py +++ b/utils/data_structures/InvestigationTimestampStructure.py @@ -41,20 +41,23 @@ class InvestigationTimestamp: def __init__(self, uid: str, order: int, output_patient_folder: str, dicom_study_id: str = None, inv_time: str = None, reload_params: dict = None) -> None: - self.__reset() - self._unique_id = uid - if dicom_study_id: - self._dicom_study_id = dicom_study_id - self._order = order - self._output_patient_folder = output_patient_folder - if inv_time: - self._datetime = datetime.datetime.strptime(inv_time, "%Y%m%d").date() - self._display_name = uid - - if reload_params: - self.__reload_from_disk(reload_params) - else: - self.__init_from_scratch() + try: + self.__reset() + self._unique_id = uid + if dicom_study_id: + self._dicom_study_id = dicom_study_id + self._order = order + self._output_patient_folder = output_patient_folder + if inv_time: + self._datetime = datetime.datetime.strptime(inv_time, "%Y%m%d").date() + self._display_name = uid + + if reload_params: + self.__reload_from_disk(reload_params) + else: + self.__init_from_scratch() + except Exception as e: + raise RuntimeError(e) def __reset(self): self._unique_id = None @@ -98,19 +101,24 @@ def display_name(self, text: str) -> None: logging.debug( "Unsaved changes - Investigation timestamp display name changed from {} to {}".format(self._display_name, text)) - self._display_name = text - new_folder_name = self._display_name.strip().replace(" ", "") - if os.path.exists(os.path.join(self._output_patient_folder, new_folder_name)): - # @TODO. Should return an error message, but then should be made into a set_display_name method.... - return - if os.path.exists(os.path.join(self._output_patient_folder, self._folder_name)): - shutil.move(src=os.path.join(self._output_patient_folder, self._folder_name), - dst=os.path.join(self._output_patient_folder, new_folder_name)) - logging.debug( - "Unsaved changes - Investigation timestamp folder name changed from {} to {}".format(self._folder_name, - new_folder_name)) - self._folder_name = new_folder_name - self._unsaved_changes = True + try: + self._display_name = text + new_folder_name = self._display_name.strip().replace(" ", "") + if os.path.exists(os.path.join(self._output_patient_folder, new_folder_name)): + msg = 'A timestamp with requested name already exists in the destination folder.
' + \ + 'Requested name: {}.
'.format(new_folder_name) + \ + 'Destination folder: {}.'.format(os.path.dirname(self._output_patient_folder)) + raise ValueError(msg) + if os.path.exists(os.path.join(self._output_patient_folder, self._folder_name)): + shutil.move(src=os.path.join(self._output_patient_folder, self._folder_name), + dst=os.path.join(self._output_patient_folder, new_folder_name)) + logging.debug( + "Unsaved changes - Investigation timestamp folder name changed from {} to {}".format(self._folder_name, + new_folder_name)) + self._folder_name = new_folder_name + self._unsaved_changes = True + except Exception as e: + raise RuntimeError("Changing Timestamp display name from {} to {} failed with: {}".format(self._folder_name, text, e)) def set_datetime(self, inv_time: str) -> None: self._datetime = datetime.datetime.strptime(inv_time, "%d/%m/%Y, %H:%M:%S") @@ -146,15 +154,18 @@ def save(self) -> dict: timestamp_params['datetime'] = self._datetime.strftime("%d/%m/%Y, %H:%M:%S") if self._datetime else None self._unsaved_changes = False return timestamp_params - except Exception: - logging.error("[Software error] InvestigationTimestampStructure saving failed with:\n {}".format(traceback.format_exc())) + except Exception as e: + raise RuntimeError("InvestigationTimestampStructure saving failed with: {}".format(e)) def delete(self) -> None: if os.path.exists(os.path.join(self._output_patient_folder, self._folder_name)): shutil.rmtree(os.path.join(self._output_patient_folder, self._folder_name)) def __init_from_scratch(self) -> None: - self._folder_name = self._display_name.strip().replace(" ", "") + try: + self._folder_name = self._display_name.strip().replace(" ", "") + except Exception as e: + raise RuntimeError("InvestigationTimestampStructure init from scratch failed with: {}".format(e)) def __reload_from_disk(self, parameters: dict) -> None: try: @@ -167,5 +178,5 @@ def __reload_from_disk(self, parameters: dict) -> None: self._folder_name = self._display_name.strip().replace(" ", "") if 'datetime' in list(parameters.keys()) and parameters['datetime']: self._datetime = datetime.datetime.strptime(parameters['datetime'], "%d/%m/%Y, %H:%M:%S") - except Exception: - logging.error("[Software error] InvestigationTimestampStructure reloading from disk failed with:\n {}".format(traceback.format_exc())) + except Exception as e: + raise RuntimeError("InvestigationTimestampStructure reloading from disk failed with: {}".format(e)) diff --git a/utils/data_structures/MRIVolumeStructure.py b/utils/data_structures/MRIVolumeStructure.py index a422934..1797afa 100644 --- a/utils/data_structures/MRIVolumeStructure.py +++ b/utils/data_structures/MRIVolumeStructure.py @@ -61,18 +61,20 @@ class MRIVolume: def __init__(self, uid: str, inv_ts_uid: str, input_filename: str, output_patient_folder: str, reload_params: dict = None) -> None: - # @TODO. Should also add the registered versions in here. - self.__reset() - self._unique_id = uid - self._timestamp_uid = inv_ts_uid - self._raw_input_filepath = input_filename - self._output_patient_folder = output_patient_folder - self._display_name = uid - - if reload_params: - self.__reload_from_disk(reload_params) - else: - self.__init_from_scratch() + try: + self.__reset() + self._unique_id = uid + self._timestamp_uid = inv_ts_uid + self._raw_input_filepath = input_filename + self._output_patient_folder = output_patient_folder + self._display_name = uid + + if reload_params: + self.__reload_from_disk(reload_params) + else: + self.__init_from_scratch() + except Exception as e: + raise RuntimeError(e) def __reset(self): """ @@ -107,11 +109,10 @@ def load_in_memory(self) -> None: loading of the patient in memory! """ try: - if UserPreferencesStructure.getInstance().display_space != 'Patient': - if self._resampled_input_volume_filepath and os.path.exists(self._resampled_input_volume_filepath): - self._resampled_input_volume = nib.load(self._resampled_input_volume_filepath).get_fdata()[:] - else: - self.__generate_standardized_input_volume() + if self._resampled_input_volume_filepath and os.path.exists(self._resampled_input_volume_filepath): + self._resampled_input_volume = nib.load(self._resampled_input_volume_filepath).get_fdata()[:] + else: + self.__generate_standardized_input_volume() self.__generate_display_volume() except Exception as e: raise ValueError("[MRIVolumeStructure] Loading in memory failed with: {}".format(e)) @@ -136,55 +137,77 @@ def timestamp_folder_name(self) -> str: @timestamp_folder_name.setter def timestamp_folder_name(self, folder_name: str) -> None: """ - @Behaviour. Should we also adjust the raw_input_filepath, in case it is inside the patient folder - (i.e., DICOM import)? + When the name given to a timestamp is changed to be more comprehensive (e.g. from T0 to PreOp), all relative + paths on disk must be adjusted since the folder name for the given timestamp will be changed. + + Parameters + ---------- + folder_name : str + New name for the timestamp folder on disk. """ - self._timestamp_folder_name = folder_name - if self._output_patient_folder in self._usable_input_filepath: - if os.name == 'nt': - path_parts = list( - PurePath(os.path.relpath(self._usable_input_filepath, self._output_patient_folder)).parts[1:]) - rel_path = PurePath() - rel_path = rel_path.joinpath(self._output_patient_folder) - rel_path = rel_path.joinpath(self._timestamp_folder_name) - for x in path_parts: - rel_path = rel_path.joinpath(x) - self._usable_input_filepath = os.fspath(rel_path) - else: - rel_path = '/'.join(os.path.relpath(self._usable_input_filepath, - self._output_patient_folder).split('/')[1:]) - self._usable_input_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, - rel_path) - if self._dicom_metadata_filepath: - if os.name == 'nt': - path_parts = list(PurePath(os.path.relpath(self._dicom_metadata_filepath, - self._output_patient_folder)).parts[1:]) - rel_path = PurePath() - rel_path = rel_path.joinpath(self._output_patient_folder) - rel_path = rel_path.joinpath(self._timestamp_folder_name) - for x in path_parts: - rel_path = rel_path.joinpath(x) - self._dicom_metadata_filepath = os.fspath(rel_path) - else: - rel_path = '/'.join(os.path.relpath(self._dicom_metadata_filepath, - self._output_patient_folder).split('/')[1:]) - self._dicom_metadata_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, - rel_path) - if self._resampled_input_volume_filepath: - if os.name == 'nt': - path_parts = list(PurePath(os.path.relpath(self._resampled_input_volume_filepath, - self._output_patient_folder)).parts[1:]) - rel_path = PurePath() - rel_path = rel_path.joinpath(self._output_patient_folder) - rel_path = rel_path.joinpath(self._timestamp_folder_name) - for x in path_parts: - rel_path = rel_path.joinpath(x) - self._resampled_input_volume_filepath = os.fspath(rel_path) - else: - rel_path = '/'.join(os.path.relpath(self._resampled_input_volume_filepath, - self._output_patient_folder).split('/')[1:]) - self._resampled_input_volume_filepath = os.path.join(self._output_patient_folder, - self._timestamp_folder_name, rel_path) + try: + for i, fn in enumerate(self.registered_volume_filepaths.keys()): + self.registered_volume_filepaths[fn] = self.registered_volume_filepaths[fn].replace(self._timestamp_folder_name, folder_name) + + if self._resampled_input_volume_filepath: + self._resampled_input_volume_filepath = self._resampled_input_volume_filepath.replace( + self._timestamp_folder_name, folder_name) + if self._usable_input_filepath: + self._usable_input_filepath = self._usable_input_filepath.replace(self._timestamp_folder_name, + folder_name) + if self._dicom_metadata_filepath: + self._dicom_metadata_filepath = self._dicom_metadata_filepath.replace(self._timestamp_folder_name, + folder_name) + self._timestamp_folder_name = folder_name + except Exception as e: + raise ValueError("Changing the timestamp folder name for the MRIVolumeStructure failed with: {}".format(e)) + + # if self._output_patient_folder in self._usable_input_filepath: + # if os.name == 'nt': + # path_parts = list( + # PurePath(os.path.relpath(self._usable_input_filepath, self._output_patient_folder)).parts[1:]) + # rel_path = PurePath() + # rel_path = rel_path.joinpath(self._output_patient_folder) + # rel_path = rel_path.joinpath(self._timestamp_folder_name) + # for x in path_parts: + # rel_path = rel_path.joinpath(x) + # self._usable_input_filepath = os.fspath(rel_path) + # else: + # rel_path = '/'.join(os.path.relpath(self._usable_input_filepath, + # self._output_patient_folder).split('/')[1:]) + # self._usable_input_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, + # rel_path) + # if self._dicom_metadata_filepath: + # if os.name == 'nt': + # path_parts = list(PurePath(os.path.relpath(self._dicom_metadata_filepath, + # self._output_patient_folder)).parts[1:]) + # rel_path = PurePath() + # rel_path = rel_path.joinpath(self._output_patient_folder) + # rel_path = rel_path.joinpath(self._timestamp_folder_name) + # for x in path_parts: + # rel_path = rel_path.joinpath(x) + # self._dicom_metadata_filepath = os.fspath(rel_path) + # else: + # rel_path = '/'.join(os.path.relpath(self._dicom_metadata_filepath, + # self._output_patient_folder).split('/')[1:]) + # self._dicom_metadata_filepath = os.path.join(self._output_patient_folder, self._timestamp_folder_name, + # rel_path) + # if self._resampled_input_volume_filepath: + # if os.name == 'nt': + # path_parts = list(PurePath(os.path.relpath(self._resampled_input_volume_filepath, + # self._output_patient_folder)).parts[1:]) + # rel_path = PurePath() + # rel_path = rel_path.joinpath(self._output_patient_folder) + # rel_path = rel_path.joinpath(self._timestamp_folder_name) + # for x in path_parts: + # rel_path = rel_path.joinpath(x) + # self._resampled_input_volume_filepath = os.fspath(rel_path) + # else: + # rel_path = '/'.join(os.path.relpath(self._resampled_input_volume_filepath, + # self._output_patient_folder).split('/')[1:]) + # self._resampled_input_volume_filepath = os.path.join(self._output_patient_folder, + # self._timestamp_folder_name, rel_path) + def set_unsaved_changes_state(self, state: bool) -> None: self._unsaved_changes = state @@ -223,16 +246,22 @@ def set_output_patient_folder(self, output_folder: str) -> None: output_folder: str New folder name where the patient data will be saved on disk. """ - if self._resampled_input_volume_filepath: - self._resampled_input_volume_filepath = self._resampled_input_volume_filepath.replace( - self._output_patient_folder, output_folder) - if self._usable_input_filepath: - self._usable_input_filepath = self._usable_input_filepath.replace(self._output_patient_folder, - output_folder) - if self._dicom_metadata_filepath: - self._dicom_metadata_filepath = self._dicom_metadata_filepath.replace(self._output_patient_folder, + try: + if self._resampled_input_volume_filepath: + self._resampled_input_volume_filepath = self._resampled_input_volume_filepath.replace( + self._output_patient_folder, output_folder) + if self._usable_input_filepath: + self._usable_input_filepath = self._usable_input_filepath.replace(self._output_patient_folder, output_folder) - self._output_patient_folder = output_folder + if self._dicom_metadata_filepath: + self._dicom_metadata_filepath = self._dicom_metadata_filepath.replace(self._output_patient_folder, + output_folder) + for i, fn in enumerate(self.registered_volume_filepaths.keys()): + self.registered_volume_filepaths[fn] = self.registered_volume_filepaths[fn].replace(self._output_patient_folder, output_folder) + + self._output_patient_folder = output_folder + except Exception as e: + raise ValueError("Changing the output patient folder name for the MRIVolumeStructure failed with: {}".format(e)) @property def output_patient_folder(self) -> str: @@ -350,8 +379,8 @@ def delete(self): if self.registered_volume_filepaths and len(self.registered_volume_filepaths.keys()) > 0: for k in list(self.registered_volume_filepaths.keys()): os.remove(self.registered_volume_filepaths[k]) - except Exception: - logging.error(" [Software error] Error while deleting a radiological volume from disk.\n {}".format(traceback.format_exc())) + except Exception as e: + raise RuntimeError("Error while deleting a radiological volume from disk with: {}".format(e)) def save(self) -> dict: """ @@ -403,8 +432,7 @@ def save(self) -> dict: self._contrast_changed = False return volume_params except Exception as e: - logging.error("[Software error] MRIVolumeStructure saving failed with: {}.\n {}".format( - e, traceback.format_exc())) + raise RuntimeError("MRIVolumeStructure saving failed with: {}".format(e)) def import_registered_volume(self, filepath: str, registration_space: str) -> None: """ @@ -423,7 +451,7 @@ def import_registered_volume(self, filepath: str, registration_space: str) -> No registration_space, dest_path)) self._unsaved_changes = True except Exception: - logging.error("[Software error] Error while importing a registered radiological volume.\n {}".format(traceback.format_exc())) + raise RuntimeError("Error while importing a registered radiological volume.\n {}".format(traceback.format_exc())) def __init_from_scratch(self) -> None: try: @@ -440,8 +468,7 @@ def __init_from_scratch(self) -> None: self.__parse_sequence_type() self.__generate_display_volume() except Exception as e: - logging.error("""[Software error] Initializing radiological structure from scratch failed - for: {} with: {}.\n {}""".format(self._raw_input_filepath, e, traceback.format_exc())) + raise RuntimeError("""Initializing radiological structure from scratch failed for: {} with: {}.""".format(self._raw_input_filepath, e)) def __reload_from_disk(self, parameters: dict) -> None: """ @@ -482,8 +509,7 @@ def __reload_from_disk(self, parameters: dict) -> None: parameters['registered_volume_filepaths'][k]) self.registered_volumes[k] = nib.load(self.registered_volume_filepaths[k]).get_fdata()[:] except Exception as e: - logging.error("""[Software error] Reloading radiological structure from disk failed - for: {} with {}.\n {}""".format(self.display_name, e, traceback.format_exc())) + raise RuntimeError("""Reloading radiological structure from disk failed for {} with {}.""".format(self.display_name, e)) def __parse_sequence_type(self): base_name = self._unique_id.lower() diff --git a/utils/data_structures/PatientParametersStructure.py b/utils/data_structures/PatientParametersStructure.py index 4b62694..898dfdc 100644 --- a/utils/data_structures/PatientParametersStructure.py +++ b/utils/data_structures/PatientParametersStructure.py @@ -134,8 +134,8 @@ def set_output_directory(self, directory: str) -> None: shutil.move(src=self._output_folder, dst=new_output_folder, copy_function=shutil.copytree) self._output_directory = directory self._output_folder = new_output_folder - for im in self._mri_volumes: - self._mri_volumes[im].set_output_patient_folder(self._output_folder) + for im in self.mri_volumes: + self.mri_volumes[im].set_output_patient_folder(self._output_folder) for an in self._annotation_volumes: self._annotation_volumes[an].set_output_patient_folder(self._output_folder) for at in self._atlas_volumes: @@ -150,6 +150,10 @@ def set_active_investigation_timestamp(self, timestamp_uid: str) -> None: def get_active_investigation_timestamp_uid(self) -> str: return self._active_investigation_timestamp_uid + def get_active_investigation_timestamp(self) -> InvestigationTimestamp: + assert self.get_active_investigation_timestamp_uid() in list(self._investigation_timestamps.keys()) + return self._investigation_timestamps[self.get_active_investigation_timestamp_uid()] + def release_from_memory(self) -> None: """ Releasing all data objects from memory when not viewing the results for the current patient. @@ -157,8 +161,8 @@ def release_from_memory(self) -> None: """ logging.debug("Unloading patient {} from memory.".format(self._unique_id)) try: - for im in self._mri_volumes: - self._mri_volumes[im].release_from_memory() + for im in self.mri_volumes: + self.mri_volumes[im].release_from_memory() for an in self._annotation_volumes: self._annotation_volumes[an].release_from_memory() for at in self._atlas_volumes: @@ -177,8 +181,8 @@ def load_in_memory(self) -> None: """ logging.debug("Loading patient {} from memory.".format(self._unique_id)) try: - for im in self._mri_volumes: - self._mri_volumes[im].load_in_memory() + for im in self.mri_volumes: + self.mri_volumes[im].load_in_memory() for an in self._annotation_volumes: self._annotation_volumes[an].load_in_memory() for at in self._atlas_volumes: @@ -195,8 +199,8 @@ def set_unsaved_changes_state(self, state: bool) -> None: self._unsaved_changes = state for ts in self._investigation_timestamps: self._investigation_timestamps[ts].set_unsaved_changes_state(state) - for im in self._mri_volumes: - self._mri_volumes[im].set_unsaved_changes_state(state) + for im in self.mri_volumes: + self.mri_volumes[im].set_unsaved_changes_state(state) for an in self._annotation_volumes: self._annotation_volumes[an].set_unsaved_changes_state(state) for at in self._atlas_volumes: @@ -206,8 +210,8 @@ def has_unsaved_changes(self) -> bool: status = self._unsaved_changes for ts in self._investigation_timestamps: status = status | self._investigation_timestamps[ts].has_unsaved_changes() - for im in self._mri_volumes: - status = status | self._mri_volumes[im].has_unsaved_changes() + for im in self.mri_volumes: + status = status | self.mri_volumes[im].has_unsaved_changes() for an in self._annotation_volumes: status = status | self._annotation_volumes[an].has_unsaved_changes() for at in self._atlas_volumes: @@ -219,7 +223,7 @@ def has_unsaved_changes(self) -> bool: def display_name(self) -> str: return self._display_name - def set_display_name(self, new_name: str, manual_change: bool = True) -> Tuple[int, str]: + def set_display_name(self, new_name: str, manual_change: bool = True) -> None: """ Edit to the display name for the current patient, which does not alter its unique_uid. The use of an additional boolean parameter is needed to prevent updating the unsaved_changes state when @@ -232,58 +236,54 @@ def set_display_name(self, new_name: str, manual_change: bool = True) -> Tuple[i Name to be given to the current patient. manual_change : bool Indication whether the modification has been triggered by the user (True) or the system (False) - - Returns - ------- - Tuple[int, str] - The first element is the code indicating success (0) or failure (1) of the operation. The second element - is a human-readable string describing the problem encountered, if any, otherwise is empty. """ # If a patient folder has been manually copied somewhere else, outside a proper raidionics home directory # environment, which should include patients and studies sub-folders. if not os.path.exists(os.path.join(self._output_directory, "patients")) or not os.path.join(self._output_directory, "patients") in self._output_folder: - msg = """The patient folder is used outside of a proper Raidionics home directory.\n - A proper home directory consists of a 'patients' and a 'studies' sub-folder.""" - return 1, msg + msg = 'The patient folder is used outside of a proper Raidionics home directory.
' + \ + 'A proper home directory consists of a patients and a studies sub-folder.' + raise ValueError(msg) # Removing spaces to prevent potential issues in folder name/access when performing disk IO operations new_output_folder = os.path.join(self._output_directory, "patients", new_name.strip().lower().replace(" ", '_')) if os.path.exists(new_output_folder): - msg = """A patient with requested name already exists in the destination folder.\n - Requested name: [{}].\n - Destination folder: [{}].""".format(new_name, os.path.dirname(self._output_folder)) - return 1, msg + msg = 'A patient with requested name already exists in the destination folder.
' + \ + 'Requested name: {}.
'.format(new_name) + \ + 'Destination folder: {}.'.format(os.path.dirname(self._output_folder)) + raise ValueError(msg) else: - self._display_name = new_name.strip() - new_patient_parameters_dict_filename = os.path.join(self._output_folder, - self._display_name.strip().lower().replace(" ", "_") - + '_scene.raidionics') - if os.path.exists(self._patient_parameters_dict_filename): - os.rename(src=self._patient_parameters_dict_filename, dst=new_patient_parameters_dict_filename) - self._patient_parameters_dict_filename = new_patient_parameters_dict_filename - - for i, disp in enumerate(list(self._investigation_timestamps.keys())): - self._investigation_timestamps[disp].output_patient_folder = new_output_folder - - for i, disp in enumerate(list(self._mri_volumes.keys())): - self._mri_volumes[disp].set_output_patient_folder(new_output_folder) - - for i, disp in enumerate(list(self._annotation_volumes.keys())): - self._annotation_volumes[disp].set_output_patient_folder(new_output_folder) - - for i, disp in enumerate(list(self._atlas_volumes.keys())): - self._atlas_volumes[disp].set_output_patient_folder(new_output_folder) - - for i, disp in enumerate(list(self._reportings.keys())): - self._reportings[disp].output_patient_folder = new_output_folder - - shutil.move(src=self._output_folder, dst=new_output_folder, copy_function=shutil.copytree) - self._output_folder = new_output_folder - logging.info("Renamed current output folder to: {}".format(self._output_folder)) - if manual_change: - self._unsaved_changes = True - logging.debug("Unsaved changes - Patient object display name edited to {}.".format(new_name)) - return 0, "" + try: + self._display_name = new_name.strip() + new_patient_parameters_dict_filename = os.path.join(self._output_folder, + self._display_name.strip().lower().replace(" ", "_") + + '_scene.raidionics') + if os.path.exists(self._patient_parameters_dict_filename): + os.rename(src=self._patient_parameters_dict_filename, dst=new_patient_parameters_dict_filename) + self._patient_parameters_dict_filename = new_patient_parameters_dict_filename + + for i, disp in enumerate(list(self._investigation_timestamps.keys())): + self._investigation_timestamps[disp].output_patient_folder = new_output_folder + + for i, disp in enumerate(list(self.mri_volumes.keys())): + self.mri_volumes[disp].set_output_patient_folder(new_output_folder) + + for i, disp in enumerate(list(self._annotation_volumes.keys())): + self._annotation_volumes[disp].set_output_patient_folder(new_output_folder) + + for i, disp in enumerate(list(self._atlas_volumes.keys())): + self._atlas_volumes[disp].set_output_patient_folder(new_output_folder) + + for i, disp in enumerate(list(self._reportings.keys())): + self._reportings[disp].output_patient_folder = new_output_folder + + shutil.move(src=self._output_folder, dst=new_output_folder, copy_function=shutil.copytree) + self._output_folder = new_output_folder + logging.info("Renamed current output folder to: {}".format(self._output_folder)) + if manual_change: + self._unsaved_changes = True + logging.debug("Unsaved changes - Patient object display name edited to {}.".format(new_name)) + except Exception as e: + raise RuntimeError("Attempting to change the patient display name failed with: {}".format(e)) def import_report(self, filename: str, inv_ts_uid: str) -> Tuple[str, Union[None, str]]: """ @@ -328,94 +328,53 @@ def import_patient(self, filename: str) -> Any: self._last_editing_timestamp = datetime.datetime.strptime(self._patient_parameters_dict["Parameters"]["Default"]['last_editing_timestamp'], "%d/%m/%Y, %H:%M:%S") for ts_id in list(self._patient_parameters_dict['Timestamps'].keys()): - try: - timestamp = InvestigationTimestamp(uid=ts_id, - order=self._patient_parameters_dict['Timestamps'][ts_id]['order'], - output_patient_folder=self._output_folder, - inv_time=self._patient_parameters_dict['Timestamps'][ts_id]['datetime'], - reload_params=self._patient_parameters_dict['Timestamps'][ts_id]) - self._investigation_timestamps[ts_id] = timestamp - except Exception: - logging.error(str(traceback.format_exc())) - if error_message: - error_message = error_message + "\nImport timestamp failed, for volume {}.\n".format(ts_id)\ - + str(traceback.format_exc()) - else: - error_message = "Import timestamp failed, for volume {}.\n".format(ts_id)\ - + str(traceback.format_exc()) + timestamp = InvestigationTimestamp(uid=ts_id, + order=self._patient_parameters_dict['Timestamps'][ts_id]['order'], + output_patient_folder=self._output_folder, + inv_time=self._patient_parameters_dict['Timestamps'][ts_id]['datetime'], + reload_params=self._patient_parameters_dict['Timestamps'][ts_id]) + self._investigation_timestamps[ts_id] = timestamp for volume_id in list(self._patient_parameters_dict['Volumes'].keys()): - try: - mri_volume = MRIVolume(uid=volume_id, - inv_ts_uid=self._patient_parameters_dict['Volumes'][volume_id]['investigation_timestamp_uid'], - input_filename=self._patient_parameters_dict['Volumes'][volume_id]['raw_input_filepath'], - output_patient_folder=self._output_folder, - reload_params=self._patient_parameters_dict['Volumes'][volume_id]) - self._mri_volumes[volume_id] = mri_volume - except Exception: - logging.error(str(traceback.format_exc())) - if error_message: - error_message = error_message + "\nImport MRI failed, for volume {}.\n".format(volume_id) + str(traceback.format_exc()) - else: - error_message = "Import MRI failed, for volume {}.\n".format(volume_id) + str(traceback.format_exc()) + mri_volume = MRIVolume(uid=volume_id, + inv_ts_uid=self._patient_parameters_dict['Volumes'][volume_id]['investigation_timestamp_uid'], + input_filename=self._patient_parameters_dict['Volumes'][volume_id]['raw_input_filepath'], + output_patient_folder=self._output_folder, + reload_params=self._patient_parameters_dict['Volumes'][volume_id]) + self.mri_volumes[volume_id] = mri_volume for volume_id in list(self._patient_parameters_dict['Annotations'].keys()): - try: - annotation_volume = AnnotationVolume(uid=volume_id, - input_filename=self._patient_parameters_dict['Annotations'][volume_id]['raw_input_filepath'], - output_patient_folder=self._output_folder, - parent_mri_uid=self._patient_parameters_dict['Annotations'][volume_id]['parent_mri_uid'], - inv_ts_uid=self._patient_parameters_dict['Annotations'][volume_id]['investigation_timestamp_uid'], - reload_params=self._patient_parameters_dict['Annotations'][volume_id]) - self._annotation_volumes[volume_id] = annotation_volume - except Exception: - logging.error(str(traceback.format_exc())) - if error_message: - error_message = error_message + "\nImport annotation failed, for volume {}.\n".format(volume_id) + str(traceback.format_exc()) - else: - error_message = "Import annotation failed, for volume {}.\n".format(volume_id) + str(traceback.format_exc()) + annotation_volume = AnnotationVolume(uid=volume_id, + input_filename=self._patient_parameters_dict['Annotations'][volume_id]['raw_input_filepath'], + output_patient_folder=self._output_folder, + parent_mri_uid=self._patient_parameters_dict['Annotations'][volume_id]['parent_mri_uid'], + inv_ts_uid=self._patient_parameters_dict['Annotations'][volume_id]['investigation_timestamp_uid'], + reload_params=self._patient_parameters_dict['Annotations'][volume_id]) + self._annotation_volumes[volume_id] = annotation_volume for volume_id in list(self._patient_parameters_dict['Atlases'].keys()): - try: - atlas_volume = AtlasVolume(uid=volume_id, - input_filename=self._patient_parameters_dict['Atlases'][volume_id]['raw_input_filepath'], - output_patient_folder=self._output_folder, - inv_ts_uid=self._patient_parameters_dict['Atlases'][volume_id]['investigation_timestamp_uid'], - parent_mri_uid=self._patient_parameters_dict['Atlases'][volume_id]['parent_mri_uid'], - description_filename=os.path.join(self._output_folder, self._patient_parameters_dict['Atlases'][volume_id]['description_filepath']), - reload_params=self._patient_parameters_dict['Atlases'][volume_id]) - self._atlas_volumes[volume_id] = atlas_volume - except Exception: - logging.error(str(traceback.format_exc())) - if error_message: - error_message = error_message + "\nImport atlas failed, for volume {}.\n".format(volume_id) + str(traceback.format_exc()) - else: - error_message = "Import atlas failed, for volume {}.\n".format(volume_id) + str(traceback.format_exc()) + atlas_volume = AtlasVolume(uid=volume_id, + input_filename=self._patient_parameters_dict['Atlases'][volume_id]['raw_input_filepath'], + output_patient_folder=self._output_folder, + inv_ts_uid=self._patient_parameters_dict['Atlases'][volume_id]['investigation_timestamp_uid'], + parent_mri_uid=self._patient_parameters_dict['Atlases'][volume_id]['parent_mri_uid'], + description_filename=os.path.join(self._output_folder, self._patient_parameters_dict['Atlases'][volume_id]['description_filepath']), + reload_params=self._patient_parameters_dict['Atlases'][volume_id]) + self._atlas_volumes[volume_id] = atlas_volume for report_id in list(self._patient_parameters_dict['Reports'].keys()): - try: - report = ReportingStructure(uid=report_id, - report_filename=os.path.join(self._output_folder, self._patient_parameters_dict['Reports'][report_id]['report_filename']), - output_patient_folder=self._output_folder, - inv_ts_uid=self._patient_parameters_dict['Reports'][report_id]['investigation_timestamp_uid'], - reload_params=self._patient_parameters_dict['Reports'][report_id]) - self._reportings[report_id] = report - except Exception: - logging.error(str(traceback.format_exc())) - if error_message: - error_message = error_message + "\nImport atlas failed, for volume {}.\n".format( - volume_id) + str(traceback.format_exc()) - else: - error_message = "Import atlas failed, for volume {}.\n".format(volume_id) + str( - traceback.format_exc()) - - except Exception: - error_message = "[Software error] Import patient failed, from {}.\n".format(os.path.basename(filename)) + str(traceback.format_exc()) - logging.error(error_message) + report = ReportingStructure(uid=report_id, + report_filename=os.path.join(self._output_folder, self._patient_parameters_dict['Reports'][report_id]['report_filename']), + output_patient_folder=self._output_folder, + inv_ts_uid=self._patient_parameters_dict['Reports'][report_id]['investigation_timestamp_uid'], + reload_params=self._patient_parameters_dict['Reports'][report_id]) + self._reportings[report_id] = report + except Exception as e: + raise RuntimeError("Import patient failed for {} with: {}.".format(os.path.basename(filename), e)) return error_message def import_data(self, filename: str, investigation_ts: str = None, investigation_ts_folder_name: str = None, - type: str = None) -> Tuple[str, Any]: + type: str = None) -> str: """ Defining how stand-alone MRI volumes or annotation volumes are loaded into the system for the current patient. @@ -439,12 +398,10 @@ def import_data(self, filename: str, investigation_ts: str = None, investigation Returns ------- - data_uid, error_message: [str, str] - A tuple [str, str] containing first the new internal unique identifier for the loaded volume, and second - a potential error message. + data_uid: str + A string containing the new internal unique identifier for the loaded volume. """ data_uid = None - error_message = None try: if not type: @@ -452,11 +409,9 @@ def import_data(self, filename: str, investigation_ts: str = None, investigation # @TODO. Maybe not the best solution to fix the QDialog push button multiple clicks issue. if type == "MRI" and self.is_mri_raw_filepath_already_loaded(filename): - error_message = "[Doppelganger] An MRI with the provided filename has already been loaded for the patient" - return data_uid, error_message + raise ValueError("[Doppelganger] An MRI with the provided filename ({}) has already been loaded for the patient".format(filename)) if type == "Annotation" and self.is_annotation_raw_filepath_already_loaded(filename): - error_message = "[Doppelganger] An annotation with the provided filename has already been loaded for the patient" - return data_uid, error_message + raise ValueError("[Doppelganger] An annotation with the provided filename ({}) has already been loaded for the patient".format(filename)) # When including data for a patient, creating a Timestamp if none exists, otherwise assign to the first one if not investigation_ts: @@ -476,17 +431,17 @@ def import_data(self, filename: str, investigation_ts: str = None, investigation non_available_uid = True while non_available_uid: data_uid = str(np.random.randint(0, 10000)) + '_' + base_data_uid - if data_uid not in list(self._mri_volumes.keys()): + if data_uid not in list(self.mri_volumes.keys()): non_available_uid = False - self._mri_volumes[data_uid] = MRIVolume(uid=data_uid, inv_ts_uid=investigation_ts, + self.mri_volumes[data_uid] = MRIVolume(uid=data_uid, inv_ts_uid=investigation_ts, input_filename=filename, output_patient_folder=self._output_folder) else: - if len(self._mri_volumes) != 0: + if len(self.mri_volumes) != 0: # @TODO. Not optimal to set a default parent MRI, forces a manual update after, must be improved. # Should at least take the first MRI series for the correct timestamp. - default_parent_mri_uid = self.get_all_mri_volumes_for_timestamp(investigation_ts)[0] # list(self._mri_volumes.keys())[0] + default_parent_mri_uid = self.get_all_mri_volumes_for_timestamp(investigation_ts)[0] # list(self.mri_volumes.keys())[0] # Generating a unique id for the annotation volume base_data_uid = os.path.basename(filename).strip().split('.')[0] non_available_uid = True @@ -501,18 +456,16 @@ def import_data(self, filename: str, investigation_ts: str = None, investigation inv_ts_uid=investigation_ts, inv_ts_folder_name=investigation_ts_folder_name) else: - error_message = "[Software error] No MRI volume has been imported yet. Mandatory for importing an annotation." - logging.error(error_message) + raise ValueError("Annotation import failed, no MRI volume has been imported yet (mandatory for importing an annotation).") except Exception as e: - error_message = traceback.format_exc() - logging.error(str(traceback.format_exc())) + raise RuntimeError("Importing data (i.e., radiological volume or annotation) failed with: {}".format(e)) logging.info("New data file imported: {}".format(data_uid)) self._unsaved_changes = True logging.debug("Unsaved changes - Patient object expanded with new volumes.") - return data_uid, error_message + return data_uid - def import_dicom_data(self, dicom_series: DICOMSeries, inv_ts: str = None) -> Union[str, Any]: + def import_dicom_data(self, dicom_series: DICOMSeries, inv_ts: str = None) -> str: """ Half the content should be deported within the MRI structure, so that the DICOM metadata can be properly saved. @@ -526,11 +479,10 @@ def import_dicom_data(self, dicom_series: DICOMSeries, inv_ts: str = None) -> Un Returns ------- - data_uid, error_message: Union[str, Any] - data_uid, error_message: The internal unique id of the newly created object, and the potential error message + data_uid: str + The internal unique id of the newly created object """ uid = None - error_msg = None ori_filename = None try: ori_filename = os.path.join(self._output_folder, dicom_series.get_unique_readable_name() + '.nii.gz') @@ -556,25 +508,22 @@ def import_dicom_data(self, dicom_series: DICOMSeries, inv_ts: str = None) -> Un else: inv_ts_uid = inv_ts_object.unique_id uid, error_msg = self.import_data(ori_filename, investigation_ts=inv_ts_uid, type="MRI") - self._mri_volumes[uid].set_dicom_metadata(dicom_series.dicom_tags) + self.mri_volumes[uid].set_dicom_metadata(dicom_series.dicom_tags) # Removing the temporary MRI Series placeholder. - self._mri_volumes[uid].set_usable_filepath_as_raw() + self.mri_volumes[uid].set_usable_filepath_as_raw() if ori_filename and os.path.exists(ori_filename): os.remove(ori_filename) self._unsaved_changes = True - except Exception: + return uid + except Exception as e: if ori_filename and os.path.exists(ori_filename): os.remove(ori_filename) - logging.error("[Software error] Import DICOM data failed with\n {}".format(traceback.format_exc())) - error_msg = error_msg + traceback.format_exc() if error_msg else traceback.format_exc() - return uid, error_msg + raise RuntimeError("DICOM data import failed with: {}".format(e)) def import_atlas_structures(self, filename: str, parent_mri_uid: str, investigation_ts_folder_name: str = None, - description: str = None, reference: str = 'Patient') -> Union[str, Any]: + description: str = None, reference: str = 'Patient') -> str: data_uid = None - error_message = None - try: if reference == 'Patient': # Generating a unique id for the atlas volume @@ -587,18 +536,18 @@ def import_atlas_structures(self, filename: str, parent_mri_uid: str, investigat self._atlas_volumes[data_uid] = AtlasVolume(uid=data_uid, input_filename=filename, output_patient_folder=self._output_folder, - inv_ts_uid=self._mri_volumes[parent_mri_uid].timestamp_uid, + inv_ts_uid=self.mri_volumes[parent_mri_uid].timestamp_uid, parent_mri_uid=parent_mri_uid, inv_ts_folder_name=investigation_ts_folder_name, description_filename=description) else: # Reference is MNI space then - pass + raise NotImplementedError("Importing atlas structure not inside the patient space failed. NIY...") except Exception as e: - error_message = e # traceback.format_exc() + raise RuntimeError("Importing atlas structure failed with: {}".format(e)) logging.info("New atlas file imported: {}".format(data_uid)) self._unsaved_changes = True - return data_uid, error_message + return data_uid def save_patient(self) -> None: """ @@ -626,8 +575,8 @@ def save_patient(self) -> None: for i, disp in enumerate(list(self._investigation_timestamps.keys())): self._patient_parameters_dict['Timestamps'][disp] = self._investigation_timestamps[disp].save() - for i, disp in enumerate(list(self._mri_volumes.keys())): - self._patient_parameters_dict['Volumes'][disp] = self._mri_volumes[disp].save() + for i, disp in enumerate(list(self.mri_volumes.keys())): + self._patient_parameters_dict['Volumes'][disp] = self.mri_volumes[disp].save() for i, disp in enumerate(list(self._annotation_volumes.keys())): self._patient_parameters_dict['Annotations'][disp] = self._annotation_volumes[disp].save() @@ -692,10 +641,11 @@ def set_new_timestamp_display_name(self, ts_uid: str, display_name: str) -> None display_name: str New display name to use to represent the investigation timestamp. """ + #@TODO. Should hold the previous name in case something goes wrong in order to "revert" the naming? try: self._investigation_timestamps[ts_uid].display_name = display_name for im in list(self.get_all_mri_volumes_for_timestamp(timestamp_uid=ts_uid)): - self._mri_volumes[im].timestamp_folder_name = self._investigation_timestamps[ts_uid].folder_name + self.mri_volumes[im].timestamp_folder_name = self._investigation_timestamps[ts_uid].folder_name for im in list(self.get_all_annotation_uids_for_timestamp(timestamp_uid=ts_uid)): self._annotation_volumes[im].timestamp_folder_name = self._investigation_timestamps[ts_uid].folder_name for im in list(self.get_all_atlas_uids_for_timestamp(timestamp_uid=ts_uid)): @@ -715,46 +665,65 @@ def get_dicom_id(self) -> Union[None, str]: When loading MRI series from DICOM, the patient DICOM ID can be retrieved. """ res = None - for im in list(self._mri_volumes.keys()): - if self._mri_volumes[im].get_dicom_metadata() and '0010|0020' in self._mri_volumes[im].get_dicom_metadata().keys(): - res = self._mri_volumes[im].get_dicom_metadata()['0010|0020'].strip() + for im in list(self.mri_volumes.keys()): + if self.mri_volumes[im].get_dicom_metadata() and '0010|0020' in self.mri_volumes[im].get_dicom_metadata().keys(): + res = self.mri_volumes[im].get_dicom_metadata()['0010|0020'].strip() return res return res def is_mri_raw_filepath_already_loaded(self, volume_filepath: str) -> bool: state = False - for im in self._mri_volumes: - if self._mri_volumes[im].raw_input_filepath == volume_filepath: + for im in self.mri_volumes: + if self.mri_volumes[im].raw_input_filepath == volume_filepath: return True return state def get_all_mri_volumes_uids(self) -> List[str]: - return list(self._mri_volumes.keys()) + return list(self.mri_volumes.keys()) def get_patient_mri_volumes_number(self) -> int: - return len(self._mri_volumes) + return len(self.mri_volumes) def get_all_mri_volumes_display_names(self) -> List[str]: res = [] - for im in self._mri_volumes: - res.append(self._mri_volumes[im].display_name) + for im in self.mri_volumes: + res.append(self.mri_volumes[im].display_name) return res def get_mri_by_uid(self, mri_uid: str) -> MRIVolume: - return self._mri_volumes[mri_uid] + assert mri_uid in list(self.mri_volumes.keys()) + return self.mri_volumes[mri_uid] def get_mri_by_display_name(self, display_name: str) -> str: res = "-1" - for im in self._mri_volumes: - if self._mri_volumes[im].display_name == display_name: + for im in self.mri_volumes: + if self.mri_volumes[im].display_name == display_name: return im return res + def get_mri_volume_by_display_name(self, display_name: str) -> MRIVolume: + """ + Return the radiological volume for the current patient based on the requested display name + + Parameters + ---------- + display_name: str + Display name of the radiological volume to retrieve + + Raises + ----- + ValueError if no radiological with the given display name can be found for the current patient. + """ + for im in self.mri_volumes: + if self.mri_volumes[im].display_name == display_name: + return self.mri_volumes[im] + raise ValueError("[PatientParametersStructure] No MRI volume exist with the following display name: {}".format(display_name)) + def get_mri_volume_by_base_filename(self, base_fn: str) -> Union[None, MRIVolume]: result = None - for im in self._mri_volumes: - if os.path.basename(self._mri_volumes[im].get_usable_input_filepath()) == base_fn: - return self._mri_volumes[im] + for im in self.mri_volumes: + if os.path.basename(self.mri_volumes[im].get_usable_input_filepath()) == base_fn: + return self.mri_volumes[im] return result def get_all_mri_volumes_for_sequence_type(self, sequence_type: MRISequenceType) -> List[str]: @@ -772,8 +741,8 @@ def get_all_mri_volumes_for_sequence_type(self, sequence_type: MRISequenceType) A list of unique identifiers for each MRI volume object associated with the given sequence type. """ res = [] - for im in self._mri_volumes: - if self._mri_volumes[im].get_sequence_type_enum() == sequence_type: + for im in self.mri_volumes: + if self.mri_volumes[im].get_sequence_type_enum() == sequence_type: res.append(im) return res @@ -792,8 +761,8 @@ def get_all_mri_volumes_for_timestamp(self, timestamp_uid: str) -> List[str]: """ res = [] - for im in self._mri_volumes: - if self._mri_volumes[im].timestamp_uid == timestamp_uid: + for im in self.mri_volumes: + if self.mri_volumes[im].timestamp_uid == timestamp_uid: res.append(im) return res @@ -822,9 +791,9 @@ def get_all_mri_volumes_for_sequence_type_and_timestamp(self, sequence_type: MRI if not inv_ts_uid: return res - for im in self._mri_volumes: - if self._mri_volumes[im].get_sequence_type_enum() == sequence_type \ - and self._mri_volumes[im].timestamp_uid == inv_ts_uid: + for im in self.mri_volumes: + if self.mri_volumes[im].get_sequence_type_enum() == sequence_type \ + and self.mri_volumes[im].timestamp_uid == inv_ts_uid: res.append(im) return res @@ -1054,8 +1023,8 @@ def remove_mri_volume(self, volume_uid: str) -> Tuple[dict, Union[None, str]]: if len(linked_atlases) != 0: results['Atlases'] = linked_atlases - self._mri_volumes[volume_uid].delete() - del self._mri_volumes[volume_uid] + self.mri_volumes[volume_uid].delete() + del self.mri_volumes[volume_uid] logging.info("Removed MRI volume {} for patient {}".format(volume_uid, self._unique_id)) self.save_patient() diff --git a/utils/data_structures/ReportingStructure.py b/utils/data_structures/ReportingStructure.py index 469bf2f..480444e 100644 --- a/utils/data_structures/ReportingStructure.py +++ b/utils/data_structures/ReportingStructure.py @@ -45,21 +45,24 @@ class ReportingStructure: def __init__(self, uid: str, report_filename: str, output_patient_folder: str, inv_ts_uid: str, inv_ts_folder_name: str = None, reload_params: dict = None) -> None: - self.__reset() - self._unique_id = uid - self._report_filename = report_filename - self._output_patient_folder = output_patient_folder - self._timestamp_uid = inv_ts_uid - if inv_ts_folder_name: - self._timestamp_folder_name = inv_ts_folder_name - - with open(self._report_filename, 'r') as infile: - self._report_content = json.load(infile) - - if reload_params: - self.__reload_from_disk(reload_params) - else: - self.__init_from_scratch() + try: + self.__reset() + self._unique_id = uid + self._report_filename = report_filename + self._output_patient_folder = output_patient_folder + self._timestamp_uid = inv_ts_uid + if inv_ts_folder_name: + self._timestamp_folder_name = inv_ts_folder_name + + with open(self._report_filename, 'r') as infile: + self._report_content = json.load(infile) + + if reload_params: + self.__reload_from_disk(reload_params) + else: + self.__init_from_scratch() + except Exception as e: + raise RuntimeError(e) def __reset(self): """ @@ -140,9 +143,14 @@ def output_patient_folder(self) -> str: @output_patient_folder.setter def output_patient_folder(self, output_folder: str) -> None: - if self._report_filename: - self._report_filename = self._report_filename.replace(self._output_patient_folder, output_folder) - self._output_patient_folder = output_folder + try: + if self._report_filename is not None and os.path.exists(self._report_filename): + self._report_filename = self._report_filename.replace(self._output_patient_folder, output_folder) + if self._report_filename_csv is not None and os.path.exists(self._report_filename_csv): + self._report_filename_csv = self._report_filename_csv.replace(self._output_patient_folder, output_folder) + self._output_patient_folder = output_folder + except Exception as e: + raise ValueError("Changing the patient folder for the reporting structure failed with: {}".format(e)) @property def parent_mri_uid(self) -> str: @@ -184,20 +192,23 @@ def __reload_from_disk(self, params: dict): """ """ - self._timestamp_uid = params['investigation_timestamp_uid'] - if self._timestamp_uid: - self._timestamp_folder_name = params['report_filename'].split('/')[0] - if os.name == 'nt': - self._timestamp_folder_name = list(PurePath(params['report_filename']).parts)[0] + try: + self._timestamp_uid = params['investigation_timestamp_uid'] + if self._timestamp_uid: + self._timestamp_folder_name = params['report_filename'].split('/')[0] + if os.name == 'nt': + self._timestamp_folder_name = list(PurePath(params['report_filename']).parts)[0] - if 'parent_mri_uid' in list(params.keys()): - self._parent_mri_uid = params['parent_mri_uid'] + if 'parent_mri_uid' in list(params.keys()): + self._parent_mri_uid = params['parent_mri_uid'] - if 'task' in list(params.keys()): - self.set_reporting_type(params['task']) + if 'task' in list(params.keys()): + self.set_reporting_type(params['task']) - if 'report_filename_csv' in list(params.keys()): - self._report_filename_csv = os.path.join(self._output_patient_folder, params["report_filename_csv"]) + if 'report_filename_csv' in list(params.keys()): + self._report_filename_csv = os.path.join(self._output_patient_folder, params["report_filename_csv"]) + except Exception as e: + raise RuntimeError("""Reloading the reporting structure from disk failed for {} with {}.""".format(self.report_filename, e)) def save(self) -> dict: """ @@ -222,5 +233,5 @@ def save(self) -> dict: report_params['report_filename_csv'] = os.path.relpath(self._report_filename_csv, base_patient_folder) self._unsaved_changes = False return report_params - except Exception: - logging.error("[Software error] ReportingStructure saving failed with:\n {}".format(traceback.format_exc())) + except Exception as e: + logging.error("ReportingStructure saving failed with: {}".format(e)) diff --git a/utils/data_structures/StudyParametersStructure.py b/utils/data_structures/StudyParametersStructure.py index 7f92348..3c6798b 100644 --- a/utils/data_structures/StudyParametersStructure.py +++ b/utils/data_structures/StudyParametersStructure.py @@ -38,17 +38,20 @@ def __init__(self, uid: str = "-1", dest_location: str = None, study_filename: s """ """ - self.__reset() - self._unique_id = uid.replace(" ", '_').strip() - - if study_filename: - # Empty init, self.import_study() must be called after the instance creation call. - pass - else: - if not dest_location: - logging.warning("Home folder location for new study creation is None.") - dest_location = os.path.join(os.path.expanduser('~'), '.raidionics') - self.__init_from_scratch(dest_location) + try: + self.__reset() + self._unique_id = uid.replace(" ", '_').strip() + + if study_filename: + # Empty init, self.import_study() must be called after the instance creation call. + pass + else: + if not dest_location: + logging.warning("Home folder location for new study creation is None.") + dest_location = os.path.join(os.path.expanduser('~'), '.raidionics') + self.__init_from_scratch(dest_location) + except Exception as e: + raise RuntimeError(e) def __reset(self): """ @@ -102,11 +105,15 @@ def has_unsaved_changes(self) -> bool: def set_unsaved_changes_state(self, state: bool) -> None: self._unsaved_changes = state + @property + def output_study_directory(self) -> str: + return self._output_study_directory + @property def display_name(self) -> str: return self._display_name - def set_display_name(self, new_name: str, manual_change: bool = True) -> Tuple[int, str]: + def set_display_name(self, new_name: str, manual_change: bool = True) -> None: """ Edit to the display name for the current study, which does not alter its unique_uid. The use of an additional boolean parameter is needed to prevent updating the unsaved_changes state when @@ -119,37 +126,34 @@ def set_display_name(self, new_name: str, manual_change: bool = True) -> Tuple[i Name to be given to the current study. manual_change : bool Indication whether the modification has been triggered by the user (True) or the system (False) - - Returns - ------- - Tuple[int, str] - The first element is the code indicating success (0) or failure (1) of the operation. The second element - is a human-readable string describing the problem encountered, if any, otherwise is empty. """ - # Removing spaces to prevent potential issues in folder name/access when performing disk IO operations - new_output_folder = os.path.join(self._output_study_directory, "studies", new_name.strip().lower().replace(" ", '_')) - if os.path.exists(new_output_folder): - msg = """A study with requested name already exists in the destination folder.\n - Requested name: [{}].\n - Destination folder: [{}].""".format(new_name, os.path.dirname(self._output_study_directory)) - return 1, msg - else: - self._display_name = new_name.strip() - new_study_parameters_filename = os.path.join(self._output_study_folder, - self._display_name.strip().lower().replace(" ", "_") - + '_study.sraidionics') - if os.path.exists(self._study_parameters_filename): - os.rename(src=self._study_parameters_filename, dst=new_study_parameters_filename) - self._study_parameters_filename = new_study_parameters_filename - - if os.path.exists(self._output_study_folder): - shutil.move(src=self._output_study_folder, dst=new_output_folder, copy_function=shutil.copytree) - self._output_study_folder = new_output_folder - - logging.info("Renamed current study destination folder to: {}".format(self._output_study_folder)) - if manual_change: - self._unsaved_changes = True - return 0, "" + try: + # Removing spaces to prevent potential issues in folder name/access when performing disk IO operations + new_output_folder = os.path.join(self.output_study_directory, "studies", + new_name.strip().lower().replace(" ", '_')) + if os.path.exists(new_output_folder): + msg = 'A study with requested name already exists in the destination folder.
' + \ + 'Requested name: {}.
'.format(new_name) + \ + 'Destination folder: {}.'.format(os.path.dirname(self.output_study_directory)) + raise ValueError(msg) + else: + self._display_name = new_name.strip() + new_study_parameters_filename = os.path.join(self._output_study_folder, + self._display_name.strip().lower().replace(" ", "_") + + '_study.sraidionics') + if os.path.exists(self._study_parameters_filename): + os.rename(src=self._study_parameters_filename, dst=new_study_parameters_filename) + self._study_parameters_filename = new_study_parameters_filename + + if os.path.exists(self._output_study_folder): + shutil.move(src=self._output_study_folder, dst=new_output_folder, copy_function=shutil.copytree) + self._output_study_folder = new_output_folder + + logging.info("Renamed current study destination folder to: {}".format(self._output_study_folder)) + if manual_change: + self._unsaved_changes = True + except Exception as e: + raise RuntimeError("Attempting to change the study display name failed with: {}".format(e)) def set_output_study_folder(self, output_folder: str) -> None: """ @@ -180,7 +184,7 @@ def segmentation_statistics_df(self) -> pd.DataFrame: def reporting_statistics_df(self) -> pd.DataFrame: return self._reporting_statistics_df - def include_study_patient(self, uid: str, folder_name: str, patient_parameters) -> int: + def include_study_patient(self, uid: str, folder_name: str, patient_parameters) -> None: """ When a patient is included in the study, the statistics components must be updated with whatever is accessible from this patient folder. @@ -194,15 +198,15 @@ def include_study_patient(self, uid: str, folder_name: str, patient_parameters) patient_parameters: Internal patient instance containing all elements loaded for the current patient. """ - if uid not in self._included_patients_uids.keys(): - self._included_patients_uids[uid] = os.path.basename(folder_name) - self.include_segmentation_statistics(patient_uid=uid, annotation_uids=[], - patient_parameters=patient_parameters) - self.include_reporting_statistics(patient_uid=uid, reporting_uids=[], patient_parameters=patient_parameters) - self._unsaved_changes = True - return 0 - else: - return 1 + try: + if uid not in self._included_patients_uids.keys(): + self._included_patients_uids[uid] = os.path.basename(folder_name) + self.include_segmentation_statistics(patient_uid=uid, annotation_uids=[], + patient_parameters=patient_parameters) + self.include_reporting_statistics(patient_uid=uid, reporting_uids=[], patient_parameters=patient_parameters) + self._unsaved_changes = True + except Exception as e: + raise RuntimeError("Including study patient failed with: {}".format(e)) def remove_study_patient(self, uid: str) -> int: if uid not in self._included_patients_uids.keys(): @@ -335,7 +339,8 @@ def include_segmentation_statistics(self, patient_uid: str, annotation_uids: Lis row_df = pd.DataFrame(data=np.array(row_values).reshape(1, len(self._seg_stats_cnames)), columns=self._seg_stats_cnames) # @TODO. Check that a similar row does not already exist? - self._segmentation_statistics_df = self._segmentation_statistics_df.append(row_df) + self._segmentation_statistics_df = pd.concat([self._segmentation_statistics_df, pd.DataFrame(row_df)], + ignore_index=True) def include_reporting_statistics(self, patient_uid: str, reporting_uids: List[str], patient_parameters) -> None: """ @@ -358,7 +363,8 @@ def include_reporting_statistics(self, patient_uid: str, reporting_uids: List[st row_df = pd.DataFrame(data=np.array(row_values).reshape(1, len(self._reporting_stats_cnames)), columns=self._reporting_stats_cnames) # @TODO. Check that a similar row does not already exist? - self._reporting_statistics_df = self._reporting_statistics_df.append(row_df) + self._reporting_statistics_df = pd.concat([self._reporting_statistics_df , pd.DataFrame(row_df)], + ignore_index=True) def refresh_patient_statistics(self, patient_uid: str, patient_parameters): """ diff --git a/utils/data_structures/UserPreferencesStructure.py b/utils/data_structures/UserPreferencesStructure.py index 6651ebb..d64cd52 100644 --- a/utils/data_structures/UserPreferencesStructure.py +++ b/utils/data_structures/UserPreferencesStructure.py @@ -56,10 +56,11 @@ def __setup(self) -> None: """ self._preferences_filename = os.path.join(expanduser('~'), '.raidionics', 'raidionics_preferences.json') + os.makedirs(os.path.dirname(self._preferences_filename), exist_ok=True) if os.path.exists(self._preferences_filename): self.__parse_preferences() else: - self._user_home_location = os.path.join(expanduser('~'), '.raidionics') + self.user_home_location = os.path.join(expanduser('~'), '.raidionics') self.save_preferences() @property diff --git a/utils/logic/PipelineCreationHandler.py b/utils/logic/PipelineCreationHandler.py index 2649cc2..76d9e0b 100644 --- a/utils/logic/PipelineCreationHandler.py +++ b/utils/logic/PipelineCreationHandler.py @@ -49,7 +49,7 @@ def create_pipeline(model_name: str, patient_parameters, task: str) -> dict: How to allow for all possible combinations of what to use/what to run/on which timestamps? @TODO. Still heavily hard-coded atm, will need to rely only on the pipeline json files - @TODO. Postop segmentation not adapting to the number of inputs, defaulting to the 4 inputs. + @TODO. Will have to clean up all this for dealing with the new use-cases and let the backend handle most of it Returns ------- dict @@ -66,9 +66,13 @@ def create_pipeline(model_name: str, patient_parameters, task: str) -> dict: if task == 'folders_classification': return __create_folders_classification_pipeline() elif task == 'preop_segmentation': + if UserPreferencesStructure.getInstance().segmentation_tumor_model_type != "Tumor" and "GBM" not in model_name: + logging.warning( + """[Software error] Using a multiple output class model can only be performed for the Glioblastoma type + (cf.Settings > Preferences > Processing - Segmentation > Segmentation models).""") + return {} return __create_segmentation_pipeline(model_name, patient_parameters) elif 'postop_segmentation' in task: - # @TODO. Will have to clean up all this for dealing with the new use-cases... if "GBM" in task: model_name = select_appropriate_postop_model(patient_parameters) download_model(model_name=model_name) @@ -76,6 +80,11 @@ def create_pipeline(model_name: str, patient_parameters, task: str) -> dict: elif task == 'other_segmentation': return __create_other_segmentation_pipeline(model_name, patient_parameters) elif task == 'preop_reporting': + if UserPreferencesStructure.getInstance().segmentation_tumor_model_type != "Tumor": + logging.warning( + """[Software error] The clinical reporting can only be performed when using a singular output class + model (cf.Settings > Preferences > Processing - Segmentation > Segmentation models).""") + return {} return __create_preop_reporting_pipeline(model_name, patient_parameters) elif task == 'postop_reporting': model_name = select_appropriate_postop_model(patient_parameters) diff --git a/utils/logic/PipelineResultsCollector.py b/utils/logic/PipelineResultsCollector.py index f7a1805..84657b9 100644 --- a/utils/logic/PipelineResultsCollector.py +++ b/utils/logic/PipelineResultsCollector.py @@ -58,10 +58,9 @@ def collect_results(patient_parameters, pipeline): dest_file = os.path.join(patient_parameters.output_folder, dest_ts.folder_name, 'raw', os.path.basename(seg_file)) shutil.move(seg_file, dest_file) - data_uid, error_msg = patient_parameters.import_data(dest_file, - investigation_ts=dest_ts.unique_id, - investigation_ts_folder_name=dest_ts.folder_name, - type='Annotation') + data_uid = patient_parameters.import_data(dest_file, investigation_ts=dest_ts.unique_id, + investigation_ts_folder_name=dest_ts.folder_name, + type='Annotation') patient_parameters.get_annotation_by_uid(data_uid).set_annotation_class_type(anno_str) patient_parameters.get_annotation_by_uid(data_uid).set_generation_type("Automatic") patient_parameters.get_annotation_by_uid(data_uid).set_parent_mri_uid(parent_mri_uid) @@ -148,11 +147,11 @@ def collect_results(patient_parameters, pipeline): os.makedirs(os.path.dirname(dest_desc_filename), exist_ok=True) if not os.path.exists(dest_desc_filename): shutil.move(description_filename, dest_desc_filename) - data_uid, error_msg = patient_parameters.import_atlas_structures(dest_atlas_filename, - parent_mri_uid=parent_mri_uid, - investigation_ts_folder_name=dest_ts_object.folder_name, - description=dest_desc_filename, - reference='Patient') + data_uid = patient_parameters.import_atlas_structures(dest_atlas_filename, + parent_mri_uid=parent_mri_uid, + investigation_ts_folder_name=dest_ts_object.folder_name, + description=dest_desc_filename, + reference='Patient') results['Atlas'].append(data_uid) # @TODO. Hard-coded MNI space for now as it is the only atlas space in use @@ -192,11 +191,11 @@ def collect_results(patient_parameters, pipeline): os.makedirs(os.path.dirname(dest_desc_filename), exist_ok=True) if not os.path.exists(dest_desc_filename): shutil.move(description_filename, dest_desc_filename) - data_uid, error_msg = patient_parameters.import_atlas_structures(dest_atlas_filename, - parent_mri_uid=parent_mri_uid, - investigation_ts_folder_name=dest_ts_object.folder_name, - description=dest_desc_filename, - reference='Patient') + data_uid = patient_parameters.import_atlas_structures(dest_atlas_filename, + parent_mri_uid=parent_mri_uid, + investigation_ts_folder_name=dest_ts_object.folder_name, + description=dest_desc_filename, + reference='Patient') results['Atlas'].append(data_uid) # @TODO. Hard-coded MNI space for now as it is the only atlas space in use @@ -232,11 +231,11 @@ def collect_results(patient_parameters, pipeline): os.makedirs(os.path.dirname(dest_desc_filename), exist_ok=True) if not os.path.exists(dest_desc_filename): shutil.move(description_filename, dest_desc_filename) - data_uid, error_msg = patient_parameters.import_atlas_structures(dest_atlas_filename, - parent_mri_uid=parent_mri_uid, - investigation_ts_folder_name=dest_ts_object.folder_name, - description=dest_desc_filename, - reference='Patient') + data_uid = patient_parameters.import_atlas_structures(dest_atlas_filename, + parent_mri_uid=parent_mri_uid, + investigation_ts_folder_name=dest_ts_object.folder_name, + description=dest_desc_filename, + reference='Patient') results['Atlas'].append(data_uid) # @TODO. Hard-coded MNI space for now as it is the only atlas space in use diff --git a/utils/patient_dicom.py b/utils/patient_dicom.py index 809c39e..ba12683 100644 --- a/utils/patient_dicom.py +++ b/utils/patient_dicom.py @@ -14,14 +14,13 @@ def __init__(self, dicom_folder): self.birth_date = "" self.studies = {} - def parse_dicom_folder(self): + def parse_dicom_folder(self) -> None: """ Initial parsing of a DICOM folder to retrieve the metadata and readers, to let the user choose which to import. """ - error_msg = None patient_base_dicom = os.path.join(self.dicom_folder, 'DICOM') - if not os.path.exists(patient_base_dicom): + if not os.path.exists(patient_base_dicom) or not os.path.isdir(patient_base_dicom): logging.warning('No existing DICOM folder in {}.\n Treating folder as a single volume.'.format(self.dicom_folder)) try: reader = sitk.ImageSeriesReader() @@ -43,13 +42,10 @@ def parse_dicom_folder(self): self.patient_id = dicom_series.get_patient_id() if self.gender == "" and dicom_series.get_patient_gender() is not None: self.gender = dicom_series.get_patient_gender() - except Exception as e: - error_msg = """[Software error] Provided folder does not contain any DICOM folder tree, nor can it be parsed as a - single MRI volume.\n Loading aborted with error:\n {}.""".format(traceback.format_exc()) - logging.error(error_msg) - return error_msg - return error_msg + error_msg = 'Provided folder does not contain any DICOM folder tree, nor can it be parsed as a' + \ + 'single MRI volume.
Loading aborted with: {}.'.format(e) + raise RuntimeError(error_msg) main_dicom_dir = [] for _, dirs, _ in os.walk(patient_base_dicom): @@ -58,10 +54,7 @@ def parse_dicom_folder(self): break if len(main_dicom_dir) == 0: - error_msg = """ [Software error] Provided folder does not contain any proper DICOM folder hierarchy.\n - Loading aborted.""" - logging.error(error_msg) - return error_msg + raise RuntimeError('Provided folder does not contain any proper DICOM folder hierarchy. Aborting loading.') try: main_dicom_investigations = [] @@ -141,12 +134,7 @@ def parse_dicom_folder(self): logging.warning("DICOM Series reading issue with:\n {}".format(traceback.format_exc())) continue except Exception as e: - error_msg = """[Software error] Provided DICOM could not be processed.\n - Encountered issue: {}.""".format(traceback.format_exc()) - logging.error(error_msg) - return error_msg - - return error_msg + raise RuntimeError("Provided DICOM could not be processed, with: {}".format(e)) class DICOMStudy(): diff --git a/utils/software_config.py b/utils/software_config.py index 2c1e0b2..e04ddcc 100644 --- a/utils/software_config.py +++ b/utils/software_config.py @@ -2,6 +2,7 @@ import configparser import platform import traceback +from multiprocessing.managers import Value from os.path import expanduser import numpy as np from typing import Union, Any, List, Optional @@ -25,7 +26,7 @@ class SoftwareConfigResources: _software_home_location = None # Main dump location for the software elements (e.g., models, runtime log) _user_preferences_filename = None # json file containing the user preferences (for when reopening the software). _session_log_filename = None # log filename containing the runtime logging for each software execution and backend. - _software_version = "1.2.4" # Current software version (minor) for selecting which models to use in the backend. + _software_version = "1.3.0" # Current software version (minor) for selecting which models to use in the backend. _software_medical_specialty = "neurology" # Overall medical target [neurology, thoracic] @staticmethod @@ -116,14 +117,13 @@ def set_dark_mode_state(self, state: bool) -> None: UserPreferencesStructure.getInstance().use_dark_mode = state self.__set_default_stylesheet_components() - def add_new_empty_patient(self, active: bool = True) -> Union[str, Any]: + def add_new_empty_patient(self, active: bool = True) -> str: """ At startup a new empty patient is created by default. Otherwise, a new empty patient is created everytime the user presses the corresponding button in the left-hand side panel. """ non_available_uid = True patient_uid = None - error_message = None logging.debug("New patient creation requested.") try: while non_available_uid: @@ -134,16 +134,14 @@ def add_new_empty_patient(self, active: bool = True) -> Union[str, Any]: self.patients_parameters[patient_uid] = PatientParameters(id=patient_uid, dest_location=UserPreferencesStructure.getInstance().user_home_location) random_name = names.get_full_name() - code, error_msg = self.patients_parameters[patient_uid].set_display_name(random_name, manual_change=False) + self.patients_parameters[patient_uid].set_display_name(random_name, manual_change=False) if active: self.set_active_patient(patient_uid) - except Exception: - error_message = "[Software error] Error while trying to create a new empty patient: \n" - error_message = error_message + traceback.format_exc() - logging.error(error_message) - return patient_uid, error_message + return patient_uid + except Exception as e: + raise RuntimeError("Error while trying to create a new empty patient with: {}".format(e)) - def load_patient(self, filename: str, active: bool = True) -> Union[str, Any]: + def load_patient(self, filename: str, active: bool = True) -> str: """ Loads all patient-related files from parsing the scene file (*.raidionics). The current active patient is filled with the information, as an empty patient was created when the call for importing was made. @@ -158,31 +156,27 @@ def load_patient(self, filename: str, active: bool = True) -> Union[str, Any]: ---------- patient_id str Unique id of the newly loaded parameter. - error_message Any (str or None) - None if no error was collected, otherwise a string with a human-readable description of the error. """ patient_id = None - error_message = None logging.debug("Patient loading requested from {}.".format(filename)) try: patient_instance = PatientParameters(dest_location=UserPreferencesStructure.getInstance().user_home_location, patient_filename=filename) - error_message = patient_instance.import_patient(filename) + _ = patient_instance.import_patient(filename) # To prevent the save changes dialog to pop-up straight up after loading a patient scene file. patient_instance.set_unsaved_changes_state(False) patient_id = patient_instance.unique_id if patient_id in self.patients_parameters.keys(): # @TODO. The random unique key number is encountered twice, have to randomize it again. - error_message = error_message + '\nImport patient failed, unique id already exists.\n' + raise ValueError('Import patient failed, unique id already exists.') self.patients_parameters[patient_id] = patient_instance if active: # Doing the following rather than set_active_patient(), to avoid the overhead of doing memory release/load. self.active_patient_name = patient_id except Exception as e: - error_message = "[Software error] Error while trying to load a patient with: {}. \n".format(e) - error_message = error_message + traceback.format_exc() - logging.error(error_message) - return patient_id, error_message + e_msg = str(e).replace("[Software error]", "") + raise RuntimeError("Error while trying to load a patient with: {}.".format(e_msg)) + return patient_id def update_active_patient_name(self, new_name: str) -> None: self.patients_parameters[self.active_patient_name].update_visible_name(new_name) @@ -248,11 +242,11 @@ def get_patient(self, uid: str) -> PatientParameters: logging.error("[Software error] Assertion error trying to query a missing patient with UID {}.\n {}".format( uid, traceback.format_exc())) - def get_patient_by_display_name(self, display_name: str) -> Union[PatientParameters, None]: + def get_patient_by_display_name(self, display_name: str) -> PatientParameters: for uid in list(self.patients_parameters.keys()): if self.patients_parameters[uid].display_name == display_name: return self.patients_parameters[uid] - return None + raise ValueError("[SoftwareConfig] No patient exists with the given display name: {}.".format(display_name)) def remove_patient(self, uid: str) -> None: """ @@ -311,7 +305,7 @@ def add_new_empty_study(self, active: bool = True) -> Union[str, Any]: logging.error(error_message) return study_uid, error_message - def load_study(self, filename: str, active: bool = True) -> Union[str, Any]: + def load_study(self, filename: str, active: bool = True) -> str: """ Loads all study-related and patient-related files from parsing the study file (*.sraidionics). The active patient is not changed at this point. @@ -322,17 +316,15 @@ def load_study(self, filename: str, active: bool = True) -> Union[str, Any]: The full filepath to the study file, of type .sraidionics active: bool Boolean to specify if the loaded study should be set as the active study or not. + Returns ---------- study_id str Unique id of the newly loaded study. - error_message Any (str or None) - None if no error was collected, otherwise a string with a human-readable description of the error. """ logging.info("Study loading requested from {}.".format(filename)) - study_id = None - error_message = None try: + study_id = None study_instance = StudyParameters(study_filename=filename) error_message = study_instance.import_study(filename) # To prevent the save changes dialog to pop-up straight up after loading a patient scene file. @@ -354,15 +346,11 @@ def load_study(self, filename: str, active: bool = True) -> Union[str, Any]: assumed_patient_filename = os.path.join(study_instance.output_study_directory, 'patients', included_pat_uids[p], included_pat_uids[p] + '_scene.raidionics') - pat_id, pat_err_mnsg = self.load_patient(filename=assumed_patient_filename, active=False) - if pat_err_mnsg: - error_message = error_message + "\n" + pat_err_mnsg - except Exception: - error_message = "[Software error] Error while trying to load a study: \n" - error_message = error_message + traceback.format_exc() - logging.error(error_message) + pat_id = self.load_patient(filename=assumed_patient_filename, active=False) + except Exception as e: + raise RuntimeError("Loading the study failed with: {}".format(e)) - return study_id, error_message + return study_id def save_study(self, study_id): self.study_parameters[study_id].save() @@ -468,3 +456,6 @@ def get_annotation_generation_types(self) -> List[str]: for anno in AnnotationGenerationType: results.append(anno.name) return results + + def reset(self): + self.__set_default_values() \ No newline at end of file