diff --git a/README.md b/README.md index aac0520..d57eb4a 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ segmentation and standardized reporting", which has been published in [Frontiers Or * git clone --single-branch --branch master https://github.com/dbouget/Raidionics-Slicer.git /path/to/folder/. -2.3 Download and install Docker (see Section 3). +2.3 Download and install Docker (see below). 2.3 Load the plugin into 3DSlicer: ∘ All Modules > Extension Wizard. ∘ Developer Tools > Extension Wizard. ∘ Select Extension > point to the folder (second Raidionics) and add it to the path (tick the small box at the bottom). -A restart of 3DSlicer is necessary after the initial launch with the plugin to have the proper Python environment. +:warning: A restart of 3DSlicer is necessary after the initial launch with the plugin to have the proper Python environment.
diff --git a/Raidionics/Raidionics/Resources/Icons/Raidionics-Slicer.gif b/Raidionics/Raidionics/Resources/Icons/Raidionics-Slicer.gif index 1b32de7..e94efa9 100644 Binary files a/Raidionics/Raidionics/Resources/Icons/Raidionics-Slicer.gif and b/Raidionics/Raidionics/Resources/Icons/Raidionics-Slicer.gif differ diff --git a/Raidionics/Raidionics/src/RaidionicsLogic.py b/Raidionics/Raidionics/src/RaidionicsLogic.py index bd3bee2..33d802e 100644 --- a/Raidionics/Raidionics/src/RaidionicsLogic.py +++ b/Raidionics/Raidionics/src/RaidionicsLogic.py @@ -152,11 +152,6 @@ def thread_doit(self, model_parameters): try: self.main_queue_start() self.logic_target_space = "neuro_diagnosis" if modelTarget == "Neuro" else "mediastinum_diagnosis" - # if model_parameters.json_dict['task'] == 'Diagnosis': - # if model_parameters.json_dict['organ'] == 'Brain': - # SharedResources.getInstance().user_diagnosis_configuration['Default']['task'] = 'neuro_diagnosis' - # elif model_parameters.json_dict['organ'] == 'Mediastinum': - # SharedResources.getInstance().user_diagnosis_configuration['Default']['task'] = 'mediastinum_diagnosis' self.executeDocker(dockerName, modelName, dataPath, iodict, inputs, outputs, params, widgets) if not self.abort: @@ -368,20 +363,21 @@ def executeDocker(self, dockerName, modelName, dataPath, iodict, inputs, outputs if iodict[item]["type"] == "volume": # print(inputs[item]) input_node_name = inputs[item].GetName() - #try: - img = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(input_node_name)) - input_sequence_type = iodict[item]["sequence_type"] - fileName = 'input_' + input_sequence_type + self.file_extension_docker - # @TODO. hard-coding to improve. - if input_sequence_type == "T1-CE": - fileName = 'input_t1gd' + self.file_extension_docker - inputDict[item] = fileName - input_timestamp_order = iodict[item]["timestamp_order"] - os.makedirs(str(os.path.join(SharedResources.getInstance().data_path, "T" + input_timestamp_order))) - sitk.WriteImage(img, str(os.path.join(SharedResources.getInstance().data_path, - "T" + input_timestamp_order, fileName))) - #except Exception as e: - # print(e.message) + try: + img = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(input_node_name)) + input_sequence_type = iodict[item]["sequence_type"] + fileName = 'input_' + input_sequence_type + self.file_extension_docker + # @TODO. hard-coding to improve. + if input_sequence_type == "T1-CE": + fileName = 'input_t1gd' + self.file_extension_docker + inputDict[item] = fileName + input_timestamp_order = iodict[item]["timestamp_order"] + os.makedirs(str(os.path.join(SharedResources.getInstance().data_path, "T" + input_timestamp_order))) + sitk.WriteImage(img, str(os.path.join(SharedResources.getInstance().data_path, + "T" + input_timestamp_order, fileName))) + except Exception as e: + print("Issue preparing input volume.") + print(traceback.format_exc()) elif iodict[item]["type"] == "configuration": generate_backend_config(SharedResources.getInstance().data_path, iodict, self.logic_target_space, modelName) diff --git a/Raidionics/Raidionics/src/gui/Diagnosis/DiagnosisInterfaceWidget.py b/Raidionics/Raidionics/src/gui/Diagnosis/DiagnosisInterfaceWidget.py index cea0313..f5ad7a3 100644 --- a/Raidionics/Raidionics/src/gui/Diagnosis/DiagnosisInterfaceWidget.py +++ b/Raidionics/Raidionics/src/gui/Diagnosis/DiagnosisInterfaceWidget.py @@ -154,10 +154,7 @@ def populate_local_diagnosis(self): self.local_diagnosis_selector_combobox.addItem(name, idx + 1) def on_diagnosis_selection(self, index): - if index < 1 or self.local_diagnosis_selector_combobox.count == 1: - return - - jsonIndex = self.local_diagnosis_selector_combobox.itemData(index) + selected_model = self.local_diagnosis_selector_combobox.currentText selected_diagnosis = self.local_diagnosis_selector_combobox.currentText if SharedResources.getInstance().global_active_model_update: dl_req = check_local_diagnosis_for_update(selected_diagnosis) @@ -167,11 +164,11 @@ def on_diagnosis_selection(self, index): diag.exec() self.diagnosis_model_parameters.destroy() - json_model = self.json_diagnoses[jsonIndex - 1] + json_model = self.find_json_model(selected_model_name=selected_model) self.diagnosis_model_parameters.create(json_model) - if "briefdescription" in self.json_diagnoses[jsonIndex - 1]: - tip = self.json_diagnoses[jsonIndex - 1]["briefdescription"] + if "briefdescription" in json_model: + tip = json_model["briefdescription"] tip = tip.rstrip() self.local_diagnosis_selector_combobox.setToolTip(tip) else: @@ -189,7 +186,7 @@ def on_diagnosis_selection(self, index): if new_docker_status: self.diagnosis_available_signal.emit(True) else: - tip = 'The required Docker image could not be downloaded, maybe because of read/write access rights or because the image is private:\n' + tip = 'The required Docker image could not be downloaded, because of inadequet access rights or missing Docker installation.\n' tip += ' * Open the command line editor (On Windows, type \'cmd\' in the search bar.)\n' tip += ' * Copy and execute: docker image pull {}\n'.format(self.model_parameters.dockerImageName) tip += ' * Wait for the download to be complete, then exit the popup.\n' @@ -239,3 +236,11 @@ def on_cloud_diagnosis_download_selected(self): # if True: #success: self.populate_local_diagnosis() self.populate_cloud_diagnosis() + + def find_json_model(self, selected_model_name): + json_model = None + for m in self.jsonModels: + if m['name'] == selected_model_name: + json_model = m + break + return json_model diff --git a/Raidionics/Raidionics/src/gui/RaidionicsWidget.py b/Raidionics/Raidionics/src/gui/RaidionicsWidget.py index db12820..c629e2a 100644 --- a/Raidionics/Raidionics/src/gui/RaidionicsWidget.py +++ b/Raidionics/Raidionics/src/gui/RaidionicsWidget.py @@ -129,6 +129,8 @@ def setup_user_interactions_widget(self): self.tasks_tabwidget.addTab(self.base_segmentation_widget, 'Segmentation') self.base_diagnosis_widget = BaseDiagnosisWidget(self.parent) self.tasks_tabwidget.addTab(self.base_diagnosis_widget, 'Reporting (RADS)') + self.base_diagnosis_widget.setEnabled(False) + self.base_diagnosis_widget.setToolTip("Currently disabled for maintenance, please use Raidionics in the meantime.") self.logging_textedit = qt.QTextEdit() #self.logging_textedit.setEnabled(False) self.logging_textedit.setReadOnly(True) diff --git a/Raidionics/Raidionics/src/gui/Segmentation/ModelsInterfaceWidget.py b/Raidionics/Raidionics/src/gui/Segmentation/ModelsInterfaceWidget.py index d95c852..0b61b48 100644 --- a/Raidionics/Raidionics/src/gui/Segmentation/ModelsInterfaceWidget.py +++ b/Raidionics/Raidionics/src/gui/Segmentation/ModelsInterfaceWidget.py @@ -143,10 +143,10 @@ def populate_cloud_models(self): self.cloud_model_download_pushbutton.setEnabled(False) def on_model_selection(self, index): - # @TODO. Maybe need a flush method to remove old models, in case the users don't know how to do it themselves? - if index < 1 or self.local_model_selector_combobox.count == 1: - return - + """ + Updates the model parameters GUI part based on the currently selected model. + The index parameter is currently not used, as the current combobox text is directly retrieved and used. + """ selected_model = self.local_model_selector_combobox.currentText # @TODO. Should also check if the files are still on disk, before sending the OK signal? # @TODO. Should also check for an update of the docker image by comparing sha numbers? @@ -157,12 +157,14 @@ def on_model_selection(self, index): diag.set_model_name(selected_model) diag.exec() self.model_parameters.destroy() - jsonIndex = self.local_model_selector_combobox.itemData(index) - json_model = self.jsonModels[jsonIndex - 1] + json_model = self.find_json_model(selected_model_name=selected_model) + if not json_model: + return + self.model_parameters.create(json_model) - if "briefdescription" in self.jsonModels[jsonIndex - 1]: - tip = self.jsonModels[jsonIndex - 1]["briefdescription"] + if "briefdescription" in json_model: + tip = json_model["briefdescription"] tip = tip.rstrip() self.local_model_selector_combobox.setToolTip(tip) else: @@ -180,7 +182,7 @@ def on_model_selection(self, index): if new_docker_status: self.segmentation_available_signal.emit(True) else: - tip = 'The required Docker image could not be downloaded, maybe because of read/write access rights or because the image is private:\n' + tip = 'The required Docker image could not be downloaded, because of inadequate access rights or missing Docker installation.\n' tip += ' * Open the command line editor (On Windows, type \'cmd\' in the search bar.)\n' tip += ' * Copy and execute: docker image pull {}\n'.format(self.model_parameters.dockerImageName) tip += ' * Wait for the download to be complete, then exit the popup.\n' @@ -276,3 +278,11 @@ def on_model_details_selected(self): popup.setWindowTitle('Exhaustive description for {}'.format(self.local_model_selector_combobox.currentText)) popup.setText(tip) x = popup.exec_() + + def find_json_model(self, selected_model_name): + json_model = None + for m in self.jsonModels: + if m['name'] == selected_model_name: + json_model = m + break + return json_model diff --git a/Raidionics/Raidionics/src/utils/backend_utilities.py b/Raidionics/Raidionics/src/utils/backend_utilities.py index b916ded..9b1ab0d 100644 --- a/Raidionics/Raidionics/src/utils/backend_utilities.py +++ b/Raidionics/Raidionics/src/utils/backend_utilities.py @@ -6,7 +6,21 @@ from src.utils.resources import SharedResources -def generate_backend_config(input_folder, parameters, logic_target_space, model_name): +def generate_backend_config(input_folder: str, parameters, logic_target_space: str, model_name: str) -> None: + """ + Preparing the configuration file to be used as input by raidionics_rads_lib (processing backend). + + Parameters + ---------- + input_folder: str + Folder to be used as input by the backend. + parameters: ? + Not used anymore + logic_target_space: str + Description of the targeted medical specialty, between neuro and mediastinum for now. + model_name: str + Name of the model to be executed in the backend. + """ try: rads_config = configparser.ConfigParser() rads_config.add_section('Default') @@ -19,6 +33,7 @@ def generate_backend_config(input_folder, parameters, logic_target_space, model_ rads_config.set('System', 'model_folder', '/home/ubuntu/resources/models') rads_config.set('System', 'pipeline_filename', '/home/ubuntu/resources/models/' + model_name + '/pipeline.json') rads_config.add_section('Runtime') + # @TODO. The backend disregards those parameters after the latest RADS lib update, has to be fixed rads_config.set('Runtime', 'reconstruction_method', SharedResources.getInstance().user_configuration['Predictions']['reconstruction_method']) rads_config.set('Runtime', 'reconstruction_order', diff --git a/Raidionics/Raidionics/src/utils/io_utilities.py b/Raidionics/Raidionics/src/utils/io_utilities.py index 7e9fa27..9d2670f 100644 --- a/Raidionics/Raidionics/src/utils/io_utilities.py +++ b/Raidionics/Raidionics/src/utils/io_utilities.py @@ -7,6 +7,7 @@ import csv import threading import hashlib +from typing import List import json import datetime import zipfile @@ -27,7 +28,15 @@ from src.utils.resources import SharedResources -def get_available_cloud_models_list(): +def get_available_cloud_models_list() -> List[List[str]]: + """ + Collects a csv file from Google Drive, summarizing all available models. + + Returns + ------ + List of all available models on the cloud, each expressed as a List[str]. + Each model list element corresponds to the following headers: Item,link,dependencies,sum. + """ cloud_models_list = [] # cloud_models_list_url = 'https://drive.google.com/uc?id=1wVjqpQ7S3xTcNJyV2Sp_hSyKglcxfQLe' cloud_models_list_url = 'https://drive.google.com/uc?id=1uibFBPBQywX7EGK5G_Oc6CXlDSiOePKF' @@ -56,6 +65,19 @@ def download_cloud_model_thread(selected_model): def download_cloud_model(selected_model): + """ + Legacy, but still needed, model download method (use recursively from within the DownloadWorker thread. + @TODO. Should be removed and the recursive download should just happen inside the worker. + + Parameters + ---------- + selected_model: str + Unique name identifier of the model to be downloaded. + + Returns + ------- + Boolean to indicate if the download operation succeeded or failed. + """ model_url = '' model_dependencies = [] model_checksum = None @@ -126,9 +148,10 @@ def download_cloud_model(selected_model): def check_local_model_for_update(selected_model): """ + Compares the existing local model with the remote ones, to identify if a new version is available for download, + by checking the checksums. + - :param selected_model: - :return: """ model_url = '' model_dependencies = [] @@ -263,7 +286,6 @@ class DownloadWorker(qt.QObject): #qt.QThread def __init__(self): super(qt.QObject, self).__init__() - # self.finished_signal = qt.Signal(bool) #WorkerFinishedSignal() #qt.Signal() #qt.Signal(name='workerFinished') def onWorkerStart(self, model=None, diagnosis=None, docker_image=None): try: diff --git a/Raidionics/Raidionics/src/utils/resources.py b/Raidionics/Raidionics/src/utils/resources.py index 673745e..5c3f02b 100644 --- a/Raidionics/Raidionics/src/utils/resources.py +++ b/Raidionics/Raidionics/src/utils/resources.py @@ -73,6 +73,7 @@ def set_environment(self): self.global_active_model_update = False def __set_runtime_parameters(self): + # Most likely deprecated, as we moved from the seg backend to the rads one! # Set of variables sent to the docker images as runtime config, manually chosen by the user. self.user_configuration = configparser.ConfigParser() self.user_configuration['Predictions'] = {} @@ -94,6 +95,3 @@ def __set_runtime_parameters(self): self.user_diagnosis_configuration['Neuro']['tumor_segmentation_filename'] = '' self.user_diagnosis_configuration['Neuro']['brain_segmentation_filename'] = '' self.user_diagnosis_configuration['Neuro']['tumor_type'] = '' - # @TODO. Give the option to the user to decide what to include in the standardized report generation - self.user_diagnosis_configuration['Neuro']['compute_cortical_structures'] = 'True' - self.user_diagnosis_configuration['Neuro']['compute_subcortical_structures'] = 'True'