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 @@
[](https://doi.org/10.1038/s41598-023-42048-7)
[](https://codecov.io/gh/raidionics/Raidionics)
[](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',