diff --git a/.github/workflows/build_macos.yml b/.github/workflows/build_macos.yml index ccaf09e..efb80c5 100644 --- a/.github/workflows/build_macos.yml +++ b/.github/workflows/build_macos.yml @@ -80,8 +80,8 @@ jobs: - name: Make installer run: | git clone https://github.com/dbouget/quickpkg.git - quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.1-macOS.pkg - cp -r Raidionics-1.3.1-macOS.pkg dist/Raidionics-1.3.1-macOS-x86_64.pkg + quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.2-macOS.pkg + cp -r Raidionics-1.3.2-macOS.pkg dist/Raidionics-1.3.2-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 d70caa3..f311148 100644 --- a/.github/workflows/build_macos_arm.yml +++ b/.github/workflows/build_macos_arm.yml @@ -91,8 +91,8 @@ jobs: - name: Make installer run: | git clone https://github.com/dbouget/quickpkg.git - quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.1-macOS.pkg - cp -r Raidionics-1.3.1-macOS.pkg dist/Raidionics-1.3.1-macOS-arm64.pkg + quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.2-macOS.pkg + cp -r Raidionics-1.3.2-macOS.pkg dist/Raidionics-1.3.2-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 f73f0c9..268c86d 100644 --- a/.github/workflows/build_ubuntu.yml +++ b/.github/workflows/build_ubuntu.yml @@ -115,7 +115,7 @@ 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.3.1-ubuntu.deb + cp -r assets/Raidionics_ubuntu.deb dist/Raidionics-1.3.2-ubuntu.deb - name: Upload package uses: actions/upload-artifact@v4 diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index 5b943e7..9df97c9 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -69,7 +69,7 @@ jobs: - name: Make installer run: | makensis.exe assets/Raidionics.nsi - cp -r assets/Raidionics-1.3.1-win.exe dist/Raidionics-1.3.1-win.exe + cp -r assets/Raidionics-1.3.2-win.exe dist/Raidionics-1.3.2-win.exe - name: Upload package uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index ee9828f..097511d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![Paper](https://zenodo.org/badge/DOI/10.1038/s41598-023-42048-7.svg)](https://doi.org/10.1038/s41598-023-42048-7) [![codecov](https://codecov.io/gh/raidionics/Raidionics/branch/master/graph/badge.svg?token=ZSPQVR7RKX)](https://codecov.io/gh/raidionics/Raidionics) [![GitHub release](https://img.shields.io/github/v/release/raidionics/raidionics?sort=semver)](https://github.com/raidionics/raidionics/releases) + **Raidionics** was developed by SINTEF Medical Image Analysis. A paper presenting the software and some benchmarks has been published in [Scientific Reports](https://doi.org/10.1038/s41598-023-42048-7). diff --git a/assets/Raidionics.nsi b/assets/Raidionics.nsi index 28cb650..2184b72 100644 --- a/assets/Raidionics.nsi +++ b/assets/Raidionics.nsi @@ -1,8 +1,8 @@ !define APP_NAME "Raidionics" !define COMP_NAME "SINTEF" -!define VERSION "1.3.1" +!define VERSION "1.3.2" !define DESCRIPTION "Application" -!define INSTALLER_NAME "Raidionics-1.3.1-win.exe" +!define INSTALLER_NAME "Raidionics-1.3.2-win.exe" !define MAIN_APP_EXE "Raidionics.exe" !define INSTALL_TYPE "SetShellVarContext current" !define REG_ROOT "HKLM" diff --git a/assets/main.spec b/assets/main.spec index bb8696f..741b570 100644 --- a/assets/main.spec +++ b/assets/main.spec @@ -84,7 +84,7 @@ if sys.platform == "darwin": 'CFBundleIdentifier': 'Raidionics', 'CFBundleInfoDictionaryVersion': '6.0', 'CFBundleName': 'Raidionics', - 'CFBundleVersion': '1.3.1', + 'CFBundleVersion': '1.3.2', 'CFBundlePackageType': 'APPL', 'LSBackgroundOnly': 'false', }, diff --git a/assets/main_arm.spec b/assets/main_arm.spec index fa826a0..8d15686 100644 --- a/assets/main_arm.spec +++ b/assets/main_arm.spec @@ -89,7 +89,7 @@ if sys.platform == "darwin": 'CFBundleIdentifier': 'Raidionics', 'CFBundleInfoDictionaryVersion': '6.0', 'CFBundleName': 'Raidionics', - 'CFBundleVersion': '1.3.1', + 'CFBundleVersion': '1.3.2', 'CFBundlePackageType': 'APPL', 'LSBackgroundOnly': 'false', }, diff --git a/gui/SinglePatientComponent/PatientResultsSidePanel/SinglePatientResultsWidget.py b/gui/SinglePatientComponent/PatientResultsSidePanel/SinglePatientResultsWidget.py index 6001711..b4a0efc 100644 --- a/gui/SinglePatientComponent/PatientResultsSidePanel/SinglePatientResultsWidget.py +++ b/gui/SinglePatientComponent/PatientResultsSidePanel/SinglePatientResultsWidget.py @@ -414,13 +414,23 @@ def on_standardized_report_imported(self, report_uid: str) -> None: report = None report = TumorCharacteristicsWidget(patient_uid=self.uid, report_uid=report_uid, structure_name=c) report_visible_name = f"Features: {c} - {report_structure.timestamp_folder_name}" + ritems = [self.results_selector_combobox.itemText(i) for i in range(self.results_selector_combobox.count())] - if report: - self.report_widgets[report_uid] = report - self.results_display_stackedwidget.addWidget(report) - self.results_selector_combobox.addItem(report_visible_name) - report.resizeRequested.connect(self.resizeRequested) - self.resizeRequested.emit() + if not report: + return + + if report_visible_name in ritems: + rind = self.results_selector_combobox.findText(report_visible_name) + dkey = list(self.report_widgets.keys())[rind] + self.results_display_stackedwidget.removeWidget(self.report_widgets[dkey]) + self.report_widgets[dkey].deleteLater() + self.report_widgets.pop(dkey) + + self.report_widgets[report_uid] = report + self.results_display_stackedwidget.addWidget(report) + self.results_selector_combobox.addItem(report_visible_name) + report.resizeRequested.connect(self.resizeRequested) + self.resizeRequested.emit() def on_size_request(self): self.resizeRequested.emit() diff --git a/gui/UtilsWidgets/CustomQDialog/ResearchCommunityDialog.py b/gui/UtilsWidgets/CustomQDialog/ResearchCommunityDialog.py index 57bba21..e32dc6b 100644 --- a/gui/UtilsWidgets/CustomQDialog/ResearchCommunityDialog.py +++ b/gui/UtilsWidgets/CustomQDialog/ResearchCommunityDialog.py @@ -159,7 +159,7 @@ def __set_interface(self): self.brats_widget = HospitalContributorWidget(self) self.brats_widget.set_hospital_name("The BraTS challenge 2023/2024") - self.brats_widget.set_hospital_participants("""Official website""") + self.brats_widget.set_hospital_participants("""Official website""") self.brats_widget.set_logo_icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../Images/brats-challenge-logo.png')) self.main_scrollarea_layout.addWidget(self.brats_widget, 5, 0, 1, 1) @@ -237,7 +237,8 @@ def __set_interface(self): self.hospital_name_label = QLabel() self.hospital_name_label.setTextInteractionFlags(Qt.TextSelectableByMouse) self.hospital_participants_label = QLabel() - self.hospital_participants_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.hospital_participants_label.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextBrowserInteraction) + self.hospital_participants_label.setOpenExternalLinks(True) self.hospital_location_layout = QVBoxLayout() self.hospital_location_layout.setSpacing(0) diff --git a/utils/backend_logic.py b/utils/backend_logic.py index 4fc396c..646a14a 100644 --- a/utils/backend_logic.py +++ b/utils/backend_logic.py @@ -257,7 +257,7 @@ def generate_surrogate_folder(patient_parameters: PatientParameters, output_fold for anno in manual_annos: shutil.copyfile(src=patient_parameters.get_annotation_by_uid(anno).usable_input_filepath, dst=os.path.join(surrogate_folder, "T" + str(ts_object.order), os.path.basename( - patient_parameters.get_mri_by_uid(im).usable_input_filepath[:-7] + '-label_' + str(c) + '.nii.gz'))) + patient_parameters.get_mri_by_uid(im).usable_input_filepath[:-7] + '-label_' + c.name + '.nii.gz'))) else: annos = patient_parameters.get_specific_annotations_for_mri(mri_volume_uid=im, generation_type=AnnotationGenerationType.Automatic, @@ -265,7 +265,7 @@ def generate_surrogate_folder(patient_parameters: PatientParameters, output_fold for anno in annos: shutil.copyfile(src=patient_parameters.get_annotation_by_uid(anno).usable_input_filepath, dst=os.path.join(surrogate_folder, "T" + str(ts_object.order), os.path.basename( - patient_parameters.get_mri_by_uid(im).usable_input_filepath[:-7] + '-label_' + str(c) + '.nii.gz'))) + patient_parameters.get_mri_by_uid(im).usable_input_filepath[:-7] + '-label_' + c.name + '.nii.gz'))) except Exception: logging.error('Pipeline surrogate folder creation failed with: \n{}'.format(traceback.format_exc())) diff --git a/utils/data_structures/AnnotationStructure.py b/utils/data_structures/AnnotationStructure.py index fcafa9a..7529c4f 100644 --- a/utils/data_structures/AnnotationStructure.py +++ b/utils/data_structures/AnnotationStructure.py @@ -29,6 +29,7 @@ class AnnotationClassType(Enum): Cavity = 4, 'Cavity' TumorCE = 5, 'Contrast-Enhancing Tumor' WT = 6, 'Whole Tumor' # Corresponds to the sum of the tumor-CE, necrosis, and edema + Edema = 7, 'Surrounding non-enhancing FLAIR changes' # @TODO. Is FLAIRChanges the whole tumor and should we support an edema category in addition? Lungs = 100, 'Lungs' diff --git a/utils/data_structures/PatientParametersStructure.py b/utils/data_structures/PatientParametersStructure.py index e2ad04c..dc9cfd3 100644 --- a/utils/data_structures/PatientParametersStructure.py +++ b/utils/data_structures/PatientParametersStructure.py @@ -949,6 +949,14 @@ def get_all_annotation_uids_for_radiological_volume(self, radiological_uid: str) res.append(im) return res + def get_all_reports_for_mri_and_type(self, mri_volume_uid: str, report_type: str) -> List[ReportingStructure]: + res = [] + for r in list(self._reportings.keys()): + if (self._reportings[r].parent_mri_uid == mri_volume_uid and + self._reportings[r].get_report_task_str() == report_type): + res.append(self._reportings[r]) + return res + def get_all_atlases_for_mri(self, mri_volume_uid: str) -> List[str]: """ Convenience method for collecting all atlas objects linked to a specific MRI volume. diff --git a/utils/logic/PipelineCreationHandler.py b/utils/logic/PipelineCreationHandler.py index ac63d1b..38d0803 100644 --- a/utils/logic/PipelineCreationHandler.py +++ b/utils/logic/PipelineCreationHandler.py @@ -135,6 +135,17 @@ def __create_preop_segmentation_pipeline(tumor_type: str) -> dict: pip[pip_num]["description"] = "Identifying the best necrosis segmentation model for existing inputs" download_model(model_name='MRI_Necrosis') + pip_num_int = pip_num_int + 1 + pip_num = str(pip_num_int) + pip[pip_num] = {} + pip[pip_num]["task"] = 'Model selection' + pip[pip_num]["model"] = 'MRI_SNFH' + pip[pip_num]["timestamp"] = 0 + pip[pip_num]["format"] = "thresholding" + pip[pip_num]["description"] = ("Identifying the best surrounding non-enhancing FLAIR hyperintensity (SNFH) " + "segmentation model for existing inputs") + download_model(model_name='MRI_SNFH') + pip_num_int = pip_num_int + 1 pip_num = str(pip_num_int) pip[pip_num] = {} @@ -238,6 +249,17 @@ def __create_postop_segmentation_pipeline(tumor_type: str) -> dict: pip[pip_num]["description"] = "Identifying the best rest enhancing tumor segmentation model for existing inputs" download_model(model_name='MRI_TumorCE_Postop') + pip_num_int = pip_num_int + 1 + pip_num = str(pip_num_int) + pip[pip_num] = {} + pip[pip_num]["task"] = 'Model selection' + pip[pip_num]["model"] = 'MRI_SNFH' + pip[pip_num]["timestamp"] = postop_ts + pip[pip_num]["format"] = "thresholding" + pip[pip_num]["description"] = ("Identifying the best surrounding non-enhancing FLAIR hyperintensity (SNFH) " + "segmentation model for existing inputs") + download_model(model_name='MRI_SNFH') + pip_num_int = pip_num_int + 1 pip_num = str(pip_num_int) pip[pip_num] = {} @@ -312,6 +334,17 @@ def __create_preop_reporting_pipeline(tumor_type: str) -> dict: pip[pip_num]["description"] = "Identifying the best necrosis segmentation model for existing inputs" download_model(model_name='MRI_Necrosis') + pip_num_int = pip_num_int + 1 + pip_num = str(pip_num_int) + pip[pip_num] = {} + pip[pip_num]["task"] = 'Model selection' + pip[pip_num]["model"] = 'MRI_SNFH' + pip[pip_num]["timestamp"] = 0 + pip[pip_num]["format"] = "thresholding" + pip[pip_num]["description"] = ("Identifying the best surrounding non-enhancing FLAIR hyperintensity (SNFH) " + "segmentation model for existing inputs") + download_model(model_name='MRI_SNFH') + pip_num_int = pip_num_int + 1 pip_num = str(pip_num_int) pip[pip_num] = {} @@ -362,6 +395,17 @@ def __create_postop_reporting_pipeline(tumor_type: str) -> dict: pip[pip_num]["description"] = "Identifying the best tumor core segmentation model for existing inputs" download_model(model_name='MRI_TumorCE_Postop') + pip_num_int = pip_num_int + 1 + pip_num = str(pip_num_int) + pip[pip_num] = {} + pip[pip_num]["task"] = 'Model selection' + pip[pip_num]["model"] = 'MRI_SNFH' + pip[pip_num]["timestamp"] = postop_ts + pip[pip_num]["format"] = "thresholding" + pip[pip_num]["description"] = ("Identifying the best surrounding non-enhancing FLAIR hyperintensity (SNFH) " + "segmentation model for existing inputs") + download_model(model_name='MRI_SNFH') + pip_num_int = pip_num_int + 1 pip_num = str(pip_num_int) pip[pip_num] = {} diff --git a/utils/logic/PipelineResultsCollector.py b/utils/logic/PipelineResultsCollector.py index df4ea40..7bbb92e 100644 --- a/utils/logic/PipelineResultsCollector.py +++ b/utils/logic/PipelineResultsCollector.py @@ -7,6 +7,7 @@ import pandas as pd import glob +from tmp_dependencies.utils.data_structures.ReportingStructure import ReportingType from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure from utils.data_structures.MRIVolumeStructure import MRISequenceType from utils.data_structures.AnnotationStructure import AnnotationClassType, AnnotationGenerationType @@ -414,11 +415,19 @@ def collect_results(patient_parameters, pipeline): report_filename = os.path.join(patient_parameters.output_folder, 'reporting', 'reporting', "T" + str(timestamp), 'neuro_clinical_report.json') - # @TODO. Hard-coded for contrast-enhanced, will have to make it adjustable (should the base image be - # returned from the backend? - # parent_mri_uid = patient_parameters.get_all_mri_volumes_for_timestamp(timestamp_uid=patient_parameters.get_timestamp_by_order(order=timestamp).unique_id) - parent_mri_uid = patient_parameters.get_all_mri_volumes_for_sequence_type_and_timestamp(sequence_type=MRISequenceType.T1c, - timestamp_order=timestamp) + parent_mri_uid = [] + if pip_step["tumor_type"] == "contrast-enhancing": + parent_mri_uid = patient_parameters.get_all_mri_volumes_for_sequence_type_and_timestamp( + sequence_type=MRISequenceType.T1c, + timestamp_order=timestamp) + elif pip_step["tumor_type"] == "non contrast-enhancing": + # @TODO. In the future, it might be T2 is the base image, should be adjustable? + parent_mri_uid = patient_parameters.get_all_mri_volumes_for_sequence_type_and_timestamp( + sequence_type=MRISequenceType.FLAIR, + timestamp_order=timestamp) + else: + logging.warning(f"[PipelineResultsCollector] Use-case not handled for updating a timestamp report" + f" for the following tumor type:{pip_step['tumor_type']}.") if len(parent_mri_uid) == 0: continue parent_mri_uid = parent_mri_uid[0] @@ -442,10 +451,17 @@ def collect_results(patient_parameters, pipeline): shutil.move(report_filename_txt, dest_file_txt) if os.path.exists(dest_file): # Should always exist - report_uid, error_msg = patient_parameters.import_report(dest_file, dest_ts_object.unique_id) - #@TODO. Maybe the reporting type could be named differently? - patient_parameters.reportings[report_uid].set_reporting_type("Tumor characteristics") - patient_parameters.reportings[report_uid].parent_mri_uid = parent_mri_uid + # If a report was previously computed, it should simply be updated + existing_reports = patient_parameters.get_all_reports_for_mri_and_type(mri_volume_uid=parent_mri_uid, + report_type=str(ReportingType.Features)) + if len(existing_reports) == 0: + report_uid, error_msg = patient_parameters.import_report(dest_file, dest_ts_object.unique_id) + patient_parameters.reportings[report_uid].set_reporting_type("Tumor characteristics") + patient_parameters.reportings[report_uid].parent_mri_uid = parent_mri_uid + else: + # @TODO. It shouldn't be allowed with more than 1, have to improve! + existing_report = existing_reports[0] + report_uid = existing_report.unique_id results['Report'].append(report_uid) elif pip_step["task"] == "Surgical reporting": report_filename = os.path.join(patient_parameters.output_folder, 'reporting', 'reporting',