From abeacbd1e4d0556da76ec3a4da55f98da9c07be3 Mon Sep 17 00:00:00 2001 From: jithenece Date: Wed, 1 May 2024 03:44:38 +0700 Subject: [PATCH 01/15] define flow using mhub modules --- models/bamf_nnunet_ct_lung/config/default.yml | 46 ++++ .../dockerfiles/Dockerfile | 40 ++++ .../utils/CustomNNUnetRunner.py | 212 ++++++++++++++++++ 3 files changed, 298 insertions(+) create mode 100644 models/bamf_nnunet_ct_lung/config/default.yml create mode 100644 models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile create mode 100644 models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lung/config/default.yml new file mode 100644 index 00000000..2e34fc43 --- /dev/null +++ b/models/bamf_nnunet_ct_lung/config/default.yml @@ -0,0 +1,46 @@ +general: + data_base_dir: /app/data + version: 1.0 + description: default configuration for Bamf NNUnet Lung, Nodules and Tumor segmentation (dicom to dicom) + +execute: +- DicomImporter +- NiftiConverter +- NNUnetRunner +- CustomNNUnetRunner +- DsegConverter +- DataOrganizer + +modules: + DicomImporter: + source_dir: input_data + import_dir: sorted_data + sort_data: true + meta: + mod: '%Modality' + + CustomNNUnetRunner: + in_data: nifti:mod=ct + nnunet_task: Task777_CT_Nodules + nnunet_model: 3d_fullres + roi: LEFT_LUNG,LEFT_LUNG+RIGHT_LUNG + export_prob_maps: False + + NNUnetRunner: + in_data: nifti:mod=ct + nnunet_task: Task775_CT_NSCLC_RG + nnunet_model: 3d_fullres + roi: NEOPLASM_MALIGNANT_PRIMARY + export_prob_maps: False + + DsegConverter: + model_name: Bamf NNUnet CT Lung + target_dicom: dicom:mod=ct + source_segs: nifti:mod=seg:roi=* + skip_empty_slices: True + body_part_examined: LEFT_LUNG+RIGHT_LUNG + segment_id_meta_key: roi + + DataOrganizer: + targets: + - dicomseg-->[i:sid]/bamf_nnunet_ct_lung.seg.dcm \ No newline at end of file diff --git a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile b/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile new file mode 100644 index 00000000..caa43b4d --- /dev/null +++ b/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile @@ -0,0 +1,40 @@ +FROM mhubai/base:latest + +# FIXME: set this environment variable as a shortcut to avoid nnunet crashing the build +# by pulling sklearn instead of scikit-learn +# N.B. this is a known issue: +# https://github.com/MIC-DKFZ/nnUNet/issues/1281 +# https://github.com/MIC-DKFZ/nnUNet/pull/1209 +ENV SKLEARN_ALLOW_DEPRECATED_SKLEARN_PACKAGE_INSTALL=True + +# Install nnunet and platipy +RUN pip3 install --no-cache-dir \ + nnunet + +# Clone the main branch of MHubAI/models +ARG MHUB_MODELS_REPO +RUN buildutils/import_mhub_model.sh bamf_nnunet_ct_kidney ${MHUB_MODELS_REPO} + +# Pull nnUNet model weights into the container for Task777_CT_Nodules +ENV WEIGHTS_DIR=/root/.nnunet/nnUNet_models/nnUNet/ +RUN mkdir -p $WEIGHTS_DIR +ENV WEIGHTS_FN=Task777_CT_Nodules.zip +ENV WEIGHTS_URL=https://zenodo.org/record/8290147/files/$WEIGHTS_FN +RUN wget --directory-prefix ${WEIGHTS_DIR} ${WEIGHTS_URL} +RUN unzip ${WEIGHTS_DIR}${WEIGHTS_FN} -d ${WEIGHTS_DIR} +RUN rm ${WEIGHTS_DIR}${WEIGHTS_FN} + +# Pull nnUNet model weights into the container for Task775_CT_NSCLC_RG +ENV TASK_NAME_NSCLC_RG=Task775_CT_NSCLC_RG +ENV WEIGHTS_FN=$TASK_NAME_NSCLC_RG.zip +ENV WEIGHTS_URL=https://zenodo.org/record/8290169/files/$WEIGHTS_FN +RUN wget --directory-prefix ${WEIGHTS_DIR} ${WEIGHTS_URL} +RUN unzip ${WEIGHTS_DIR}${WEIGHTS_FN} -d ${WEIGHTS_DIR} +RUN rm ${WEIGHTS_DIR}${WEIGHTS_FN} + +# specify nnunet specific environment variables +ENV WEIGHTS_FOLDER=$WEIGHTS_DIR + +# Default run script +ENTRYPOINT ["mhub.run"] +CMD ["--config", "/app/models/bamf_nnunet_ct_lung/config/default.yml"] \ No newline at end of file diff --git a/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py b/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py new file mode 100644 index 00000000..892037aa --- /dev/null +++ b/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py @@ -0,0 +1,212 @@ +""" +------------------------------------------------- +MHub - NNU-Net Runner + This is a base runner for pre-trained + nnunet models. +------------------------------------------------- + +------------------------------------------------- +Author: Leonard Nürnberg +Email: leonard.nuernberg@maastrichtuniversity.nl +------------------------------------------------- +""" +# TODO: support multi-i/o and batch processing on multiple instances + +from typing import List, Optional +import os, subprocess, shutil +import SimpleITK as sitk, numpy as np +from mhubio.core import Module, Instance, InstanceData, DataType, FileType, IO + +# TODO: add an optional evaluation pattern (regex) to IO.Config +nnunet_task_name_regex = r"Task[0-9]{3}_[a-zA-Z0-9_]+" + +@IO.ConfigInput('in_data', 'nifti:mod=ct', the="input data to run nnunet on") +@IO.Config('nnunet_task', str, None, the='nnunet task name') +@IO.Config('nnunet_model', str, None, the='nnunet model name (2d, 3d_lowres, 3d_fullres, 3d_cascade_fullres)') +#@IO.Config('input_data_type', DataType, 'nifti:mod=ct', factory=DataType.fromString, the='input data type') +@IO.Config('folds', int, None, the='number of folds to run nnunet on') +@IO.Config('use_tta', bool, True, the='flag to enable test time augmentation') +@IO.Config('export_prob_maps', bool, False, the='flag to export probability maps') +@IO.Config('prob_map_segments', list, [], the='segment labels for probability maps') +@IO.Config('roi', str, None, the='roi or comma separated list of roi the nnunet segments') +class CustomNNUnetRunner(Module): + + nnunet_task: str + nnunet_model: str + input_data_type: DataType + folds: int # TODO: support optional config attributes + use_tta: bool + export_prob_maps: bool + prob_map_segments: list + roi: str + + def export_prob_mask(self, nnunet_out_dir: str, ref_file: InstanceData, output_dtype: str = 'float32', structure_list: Optional[List[str]] = None): + """ + Convert softmax probability maps to NRRD. For simplicity, the probability maps + are converted by default to UInt8 + Arguments: + model_output_folder : required - path to the folder where the inferred segmentation masks should be stored. + ref_file : required - InstanceData object of the generated segmentation mask used as reference file. + output_dtype : optional - output data type. Data type float16 is not supported by the NRRD standard, + so the choice should be between uint8, uint16 or float32. + structure_list : optional - list of the structures whose probability maps are stored in the + first channel of the `.npz` file (output from the nnU-Net pipeline + when `export_prob_maps` is set to True). + Outputs: + This function [...] + """ + + # initialize structure list + if structure_list is None: + if self.roi is not None: + structure_list = self.roi.split(',') + else: + structure_list = [] + + # sanity check user inputs + assert(output_dtype in ["uint8", "uint16", "float32"]) + + # input file containing the raw information + pred_softmax_fn = 'VOLUME_001.npz' + pred_softmax_path = os.path.join(nnunet_out_dir, pred_softmax_fn) + + # parse NRRD file - we will make use of if to populate the header of the + # NRRD mask we are going to get from the inferred segmentation mask + sitk_ct = sitk.ReadImage(ref_file.abspath) + + # generate bundle for prob masks + # TODO: we really have to create folders (or add this as an option that defaults to true) automatically + prob_masks_bundle = ref_file.getDataBundle('prob_masks') + if not os.path.isdir(prob_masks_bundle.abspath): + os.mkdir(prob_masks_bundle.abspath) + + # load softmax probability maps + pred_softmax_all = np.load(pred_softmax_path)["softmax"] + + # iterate all channels + for channel in range(0, len(pred_softmax_all)): + + structure = structure_list[channel] if channel < len(structure_list) else f"structure_{channel}" + pred_softmax_segmask = pred_softmax_all[channel].astype(dtype = np.float32) + + if output_dtype == "float32": + # no rescale needed - the values will be between 0 and 1 + # set SITK image dtype to Float32 + sitk_dtype = sitk.sitkFloat32 + + elif output_dtype == "uint8": + # rescale between 0 and 255, quantize + pred_softmax_segmask = (255*pred_softmax_segmask).astype(np.int32) + # set SITK image dtype to UInt8 + sitk_dtype = sitk.sitkUInt8 + + elif output_dtype == "uint16": + # rescale between 0 and 65536 + pred_softmax_segmask = (65536*pred_softmax_segmask).astype(np.int32) + # set SITK image dtype to UInt16 + sitk_dtype = sitk.sitkUInt16 + else: + raise ValueError("Invalid output data type. Please choose between uint8, uint16 or float32.") + + pred_softmax_segmask_sitk = sitk.GetImageFromArray(pred_softmax_segmask) + pred_softmax_segmask_sitk.CopyInformation(sitk_ct) + pred_softmax_segmask_sitk = sitk.Cast(pred_softmax_segmask_sitk, sitk_dtype) + + # generate data + prob_mask = InstanceData(f'{structure}.nrrd', DataType(FileType.NRRD, {'mod': 'prob_mask', 'structure': structure}), bundle=prob_masks_bundle) + + # export file + writer = sitk.ImageFileWriter() + writer.UseCompressionOn() + writer.SetFileName(prob_mask.abspath) + writer.Execute(pred_softmax_segmask_sitk) + + # check if the file was written + if os.path.isfile(prob_mask.abspath): + self.v(f" > prob mask for {structure} saved to {prob_mask.abspath}") + prob_mask.confirm() + + @IO.Instance() + @IO.Input("in_data", the="input data to run nnunet on") + @IO.Output("out_data", 'VOLUME_001.nii.gz', 'nifti:mod=seg:model=nnunet_nodule', data='in_data', the="output data from nnunet nodules") + def task(self, instance: Instance, in_data: InstanceData, out_data: InstanceData) -> None: + + # get the nnunet model to run + self.v("Running nnUNet_predict.") + self.v(f" > task: {self.nnunet_task}") + self.v(f" > model: {self.nnunet_model}") + self.v(f" > input data: {in_data.abspath}") + self.v(f" > output data: {out_data.abspath}") + + # download weights if not found + # NOTE: only for testing / debugging. For productiio always provide the weights in the Docker container. + if not os.path.isdir(os.path.join(os.environ["WEIGHTS_FOLDER"], '')): + print("Downloading nnUNet model weights...") + bash_command = ["nnUNet_download_pretrained_model", self.nnunet_task] + self.subprocess(bash_command, text=True) + + # bring input data in nnunet specific format + # NOTE: only for nifti data as we hardcode the nnunet-formatted-filename (and extension) for now. + assert in_data.type.ftype == FileType.NIFTI + assert in_data.abspath.endswith('.nii.gz') + inp_dir = self.config.data.requestTempDir(label="nnunet-model-inp") + inp_file = f'VOLUME_001_0000.nii.gz' + shutil.copyfile(in_data.abspath, os.path.join(inp_dir, inp_file)) + + # define output folder (temp dir) and also override environment variable for nnunet + out_dir = self.config.data.requestTempDir(label="nnunet-model-out") + os.environ['RESULTS_FOLDER'] = out_dir + + # symlink nnunet input folder to the input data with python + # create symlink in python + # NOTE: this is a workaround for the nnunet bash script that expects the input data to be in a specific folder + # structure. This is not the case for the mhub data structure. So we create a symlink to the input data + # in the nnunet input folder structure. + os.symlink(os.environ['WEIGHTS_FOLDER'], os.path.join(out_dir, 'nnUNet')) + + # NOTE: instead of running from commandline this could also be done in a pythonic way: + # `nnUNet/nnunet/inference/predict.py` - but it would require + # to set manually all the arguments that the user is not intended + # to fiddle with; so stick with the bash executable + + # construct nnunet inference command + bash_command = ["nnUNet_predict"] + bash_command += ["--input_folder", str(inp_dir)] + bash_command += ["--output_folder", str(out_dir)] + bash_command += ["--task_name", self.nnunet_task] + bash_command += ["--model", self.nnunet_model] + + # add optional arguments + if self.folds is not None: + bash_command += ["--folds", str(self.folds)] + + if not self.use_tta: + bash_command += ["--disable_tta"] + + if self.export_prob_maps: + bash_command += ["--save_npz"] + + # run command + self.subprocess(bash_command, text=True) + + # output meta + meta = { + "model": "nnunet", + "nnunet_task": self.nnunet_task, + "nnunet_model": self.nnunet_model, + "roi": self.roi + } + + # get output data + out_file = f'VOLUME_001.nii.gz' + out_path = os.path.join(out_dir, out_file) + + # copy output data to instance + shutil.copyfile(out_path, out_data.abspath) + + # export probabiliy maps if requested as dynamic data + if self.export_prob_maps: + self.export_prob_mask(str(out_dir), out_data, 'float32', self.prob_map_segments) + + # update meta dynamically + out_data.type.meta += meta From 02798b99e3aa8ea48be1acadcf7f6858685a7c1f Mon Sep 17 00:00:00 2001 From: jithenece Date: Thu, 2 May 2024 18:34:50 +0700 Subject: [PATCH 02/15] combine lung, nodules and tumor --- models/bamf_nnunet_ct_lung/config/default.yml | 14 +++-- models/bamf_nnunet_ct_lung/config/dseg.json | 63 +++++++++++++++++++ .../dockerfiles/Dockerfile | 2 +- .../utils/LungPostProcessor.py | 29 +++++++++ 4 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 models/bamf_nnunet_ct_lung/config/dseg.json create mode 100644 models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lung/config/default.yml index 2e34fc43..c035586a 100644 --- a/models/bamf_nnunet_ct_lung/config/default.yml +++ b/models/bamf_nnunet_ct_lung/config/default.yml @@ -8,6 +8,7 @@ execute: - NiftiConverter - NNUnetRunner - CustomNNUnetRunner +- LungPostProcessor - DsegConverter - DataOrganizer @@ -23,7 +24,7 @@ modules: in_data: nifti:mod=ct nnunet_task: Task777_CT_Nodules nnunet_model: 3d_fullres - roi: LEFT_LUNG,LEFT_LUNG+RIGHT_LUNG + roi: LEFT_LUNG+RIGHT_LUNG,LEFT_LUNG+RIGHT_LUNG+NODULES export_prob_maps: False NNUnetRunner: @@ -32,14 +33,17 @@ modules: nnunet_model: 3d_fullres roi: NEOPLASM_MALIGNANT_PRIMARY export_prob_maps: False - + + LungPostProcessor: + in_rg_data: nifti:mod=seg:nnunet_task=Task775_CT_NSCLC_RG + in_nodules_data: nifti:mod=seg:nnunet_task=Task777_CT_Nodules + DsegConverter: model_name: Bamf NNUnet CT Lung target_dicom: dicom:mod=ct - source_segs: nifti:mod=seg:roi=* + source_segs: nifti:mod=seg:processor=bamf skip_empty_slices: True - body_part_examined: LEFT_LUNG+RIGHT_LUNG - segment_id_meta_key: roi + json_config_path: models/bamf_nnunet_ct_lung/config/dseg.json DataOrganizer: targets: diff --git a/models/bamf_nnunet_ct_lung/config/dseg.json b/models/bamf_nnunet_ct_lung/config/dseg.json new file mode 100644 index 00000000..5d318d36 --- /dev/null +++ b/models/bamf_nnunet_ct_lung/config/dseg.json @@ -0,0 +1,63 @@ +{ + "ContentCreatorName": "BAMFHealth^AI", + "ClinicalTrialSeriesID": "Session1", + "ClinicalTrialTimePointID": "1", + "SeriesDescription": "AIMI Lung Nodule Segmentation", + "SeriesNumber": "300", + "InstanceNumber": "1", + "BodyPartExamined": "LUNG", + "segmentAttributes": [ + [ + { + "labelID": 1, + "SegmentDescription": "Lung", + "SegmentAlgorithmType": "AUTOMATIC", + "SegmentAlgorithmName": "BAMF-Lung-CT", + "SegmentedPropertyCategoryCodeSequence": { + "CodeValue": "123037004", + "CodingSchemeDesignator": "SCT", + "CodeMeaning": "Anatomical Structure" + }, + "SegmentedPropertyTypeCodeSequence": { + "CodeValue": "39607008", + "CodingSchemeDesignator": "SCT", + "CodeMeaning": "Lung" + }, + "SegmentedPropertyTypeModifierCodeSequence": { + "CodeValue": "51440002", + "CodingSchemeDesignator": "SCT", + "CodeMeaning": "Right and left" + } + }, + { + "labelID": 2, + "SegmentDescription": "Nodule", + "SegmentAlgorithmType": "AUTOMATIC", + "SegmentAlgorithmName": "BAMF-Lung-CT", + "AnatomicRegionSequence": { + "CodeValue": "39607008", + "CodingSchemeDesignator": "SCT", + "CodeMeaning": "Lung" + }, + "AnatomicRegionModifierSequence": { + "CodeValue": "51440002", + "CodingSchemeDesignator": "SCT", + "CodeMeaning": "Right and left" + }, + "SegmentedPropertyCategoryCodeSequence": { + "CodeValue": "49755003", + "CodingSchemeDesignator": "SCT", + "CodeMeaning": "Morphologically Altered Structure" + }, + "SegmentedPropertyTypeCodeSequence": { + "CodeValue": "27925004", + "CodingSchemeDesignator": "SCT", + "CodeMeaning": "Nodule" + } + } + ] + ], + "ContentLabel": "SEGMENTATION", + "ContentDescription": "Image segmentation", + "ClinicalTrialCoordinatingCenterName": "dcmqi" +} \ No newline at end of file diff --git a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile b/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile index caa43b4d..bd838944 100644 --- a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile +++ b/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile @@ -13,7 +13,7 @@ RUN pip3 install --no-cache-dir \ # Clone the main branch of MHubAI/models ARG MHUB_MODELS_REPO -RUN buildutils/import_mhub_model.sh bamf_nnunet_ct_kidney ${MHUB_MODELS_REPO} +RUN buildutils/import_mhub_model.sh bamf_nnunet_ct_lung ${MHUB_MODELS_REPO} # Pull nnUNet model weights into the container for Task777_CT_Nodules ENV WEIGHTS_DIR=/root/.nnunet/nnUNet_models/nnUNet/ diff --git a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py new file mode 100644 index 00000000..4684c275 --- /dev/null +++ b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py @@ -0,0 +1,29 @@ +from mhubio.core import IO +from mhubio.core import Module, Instance, InstanceData +import os, shutil +import SimpleITK as sitk +import numpy as np + +class LungPostProcessor(Module): + + @IO.Instance() + @IO.Input('in_rg_data', 'nifti:mod=seg:nnunet_task=Task775_CT_NSCLC_RG', the='input data from custom nnunet module') + @IO.Input('in_nodules_data', 'nifti:mod=seg:nnunet_task=Task777_CT_Nodules', the='input data nnunet nodule') + @IO.Output('out_data', 'bamf_processed.nii.gz', 'nifti:mod=seg:processor=bamf', data='in_rg_data', + the="keep the two largest connected components of the segmentation and remove all other ones") + def task(self, instance: Instance, in_rg_data: InstanceData, in_nodules_data: InstanceData, out_data: InstanceData): + print('input nodules data: ' + in_nodules_data.abspath) + print('store results in ' + out_data.abspath) + in_nodules_img = sitk.ReadImage(in_nodules_data.abspath) + in_nodules_d = sitk.GetArrayFromImage(sitk.ReadImage(in_nodules_data.abspath)) + in_rg_d = sitk.GetArrayFromImage(sitk.ReadImage(in_rg_data.abspath)) + seg_data = np.zeros(in_rg_d.shape) + seg_data[in_rg_d == 1] = 1 + seg_data[in_nodules_data == 1] = 2 + seg_data[in_rg_d == 2] = 3 + seg_img = sitk.GetImageFromArray(in_rg_d) + inp_dir = self.config.data.requestTempDir(label="lung-post-processor") + inp_file = os.path.join(inp_dir, f'final.nii.gz') + seg_img.CopyInformation(in_nodules_img) + sitk.WriteImage(seg_img, inp_file) + shutil.copyfile(inp_file, out_data.abspath) From 5e5c8e4e545df72235e0496852849c8172ac44ae Mon Sep 17 00:00:00 2001 From: jithenece Date: Thu, 2 May 2024 18:35:27 +0700 Subject: [PATCH 03/15] draft meta info --- models/bamf_nnunet_ct_lung/meta.json | 139 +++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 models/bamf_nnunet_ct_lung/meta.json diff --git a/models/bamf_nnunet_ct_lung/meta.json b/models/bamf_nnunet_ct_lung/meta.json new file mode 100644 index 00000000..e6440f48 --- /dev/null +++ b/models/bamf_nnunet_ct_lung/meta.json @@ -0,0 +1,139 @@ +{ + "id": "", + "name": "bamf_nnunet_ct_lung", + "title": "AIMI CT Lung and Nodules", + "summary": { + "description": "An nnU-Net based model to segment lung from CT scans", + "inputs": [ + { + "label": "Input Image", + "description": "The CT scan of a patient.", + "format": "DICOM", + "modality": "CT", + "bodypartexamined": "LUNG", + "slicethickness": "2.5mm", + "non-contrast": true, + "contrast": false + } + ], + "outputs": [ + { + "label": "Segmentation", + "type": "Segmentation", + "description": "Segmentation lung", + "classes": [ + "LUNG", + "LUNG+NODULES", + "LUNG+NEOPLASM_MALIGNANT_PRIMARY" + ] + } + ], + "model": { + "architecture": "U-net", + "training": "supervised", + "cmpapproach": "3D" + }, + "data": { + "training": { + "vol_samples": 833 + }, + "evaluation": { + "vol_samples": 47 + }, + "public": true, + "external": true + } + }, + "details": { + "name": "AIMI CT Lung", + "version": "1.0.0", + "devteam": "BAMF Health", + "authors": [ + "Soni, Rahul", + "McCrumb, Diana", + "Murugesan, Gowtham Krishnan", + "Van Oss, Jeff" + ], + "type": "nnU-Net (U-Net structure, optimized by data-driven heuristics)", + "date": { + "code": "17.10.2023", + "weights": "28.08.2023", + "pub": "23.10.2023" + }, + "cite": "Murugesan, Gowtham Krishnan, Diana McCrumb, Mariam Aboian, Tej Verma, Rahul Soni, Fatima Memon, and Jeff Van Oss. The AIMI Initiative: AI-Generated Annotations for Imaging Data Commons Collections. arXiv preprint arXiv:2310.14897 (2023).", + "license": { + "code": "MIT", + "weights": "CC BY-NC 4.0" + }, + "publications": [ + { + "title": "The AIMI Initiative: AI-Generated Annotations in IDC Collections", + "uri": "https://arxiv.org/abs/2310.14897" + } + ], + "github": "https://github.com/bamf-health/aimi-ct-lung" + }, + "info": { + "use": { + "title": "Intended Use", + "text": "This model is intended to perform lung, nodules and tumor segmentation in CT scans. The model has been trained and tested on scans aquired during clinical care of patients, so it might not be suited for a healthy population. The generalization capabilities of the model on a range of ages, genders, and ethnicities are unknown." + }, + "analyses": { + "title": "Quantitative Analyses", + "text": "The model's performance was assessed using the Dice Coefficient and Normalized Surface Distance (NSD) with tolerance 7mm, as specified in the CT Liver segmentation task in the Medical Segmentation Decathlon challenge. The model was used to segment cases from the IDC collection TCGA-LIHC [1]. Nine of those cases were reviewed and corrected by a board-certified radiologist and a non-expert. The analysis is published here [2]", + "tables": [ + { + "label": "Label-wise metrics (mean (standard deviation)) between AI derived and manually corrected CT lung annotations", + "entries": { + "Dice: Radiologist": "0.97 (0.07)", + "NSD: Radiologist": "0.96 (0.09)", + "Dice: Non-expert": "0.97 (0.08)", + "NSD: Non-expert": "0.95 (0.11)" + } + } + ], + "references": [ + { + "label": "DICOM-LIDC-IDRI-Nodules", + "uri": "https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=6885436" + }, + { + "label": "The AIMI Initiative: AI-Generated Annotations for Imaging Data Commons Collections", + "uri": "https://arxiv.org/abs/2310.14897" + } + ] + }, + "evaluation": { + "title": "Evaluation Data", + "text": "The model was used to segment cases 89 from the IDC [1] collection TCGA-LIHC [1]. Nine of those cases were randomly selected to be reviewed and corrected by a board-certified radiologist. The model predictions, and radiologist corrections are published on zenodo [3]", + "references": [ + { + "label": "Imaging Data Collections (IDC)", + "uri": "https://datacommons.cancer.gov/repository/imaging-data-commons" + }, + { + "label": "TCGA-LIHC", + "uri": "https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=6885436" + }, + { + "label": "Image segmentations produced by the AIMI Annotations initiative", + "uri": "https://zenodo.org/records/10009368" + } + ] + }, + "training": { + "title": "Training Data", + "text": "The training dataset consists of 1565 CT liver annotations taken from TotalSegmentator [1] (N=1204) and Flare21 Liver Dataset [2] (N=361).", + "references": [ + { + "label": "TotalSegmentator Dataset", + "uri": "https://zenodo.org/records/10047292" + }, + { + "label": "Flare21 Dataset", + "uri": "https://zenodo.org/records/5903672" + } + ] + } + } +} From 158ec130c7cd8a6ec790e4702a7bedda1f0a9ff5 Mon Sep 17 00:00:00 2001 From: jithenece Date: Mon, 6 May 2024 17:08:28 +0530 Subject: [PATCH 04/15] combine lung and nodule segmentation --- .../utils/LungPostProcessor.py | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py index 4684c275..4eeb0dfc 100644 --- a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py +++ b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py @@ -3,27 +3,70 @@ import os, shutil import SimpleITK as sitk import numpy as np +from skimage import measure, filters + class LungPostProcessor(Module): + def get_lung(self, ip_path): + # get lung segmentation + seg_data = sitk.GetArrayFromImage(sitk.ReadImage(ip_path)) + seg_data[seg_data > 0] = 1 + return seg_data + + def n_connected(self, img_data): + img_data_mask = np.zeros(img_data.shape) + img_data_mask[img_data >= 1] = 1 + img_filtered = np.zeros(img_data_mask.shape) + blobs_labels = measure.label(img_data_mask, background=0) + lbl, counts = np.unique(blobs_labels, return_counts=True) + lbl_dict = {} + for i, j in zip(lbl, counts): + lbl_dict[i] = j + sorted_dict = dict(sorted(lbl_dict.items(), key=lambda x: x[1], reverse=True)) + count = 0 + + for key, value in sorted_dict.items(): + if count >= 1 and count <= 2: + print(key, value) + img_filtered[blobs_labels == key] = 1 + count += 1 + + img_data[img_filtered != 1] = 0 + return img_data + + def get_seg_img(self, lungs, nodules, ct_path): + seg_data = np.zeros(lungs.shape) + seg_data[lungs == 1] = 1 + seg_data[nodules == 1] = 2 + ref = sitk.ReadImage(ct_path) + seg_img = sitk.GetImageFromArray(seg_data) + seg_img.CopyInformation(ref) + return seg_img + @IO.Instance() @IO.Input('in_rg_data', 'nifti:mod=seg:nnunet_task=Task775_CT_NSCLC_RG', the='input data from custom nnunet module') @IO.Input('in_nodules_data', 'nifti:mod=seg:nnunet_task=Task777_CT_Nodules', the='input data nnunet nodule') + @IO.Input('in_ct_data', 'nifti:mod=ct', the='input ct data') @IO.Output('out_data', 'bamf_processed.nii.gz', 'nifti:mod=seg:processor=bamf', data='in_rg_data', the="keep the two largest connected components of the segmentation and remove all other ones") - def task(self, instance: Instance, in_rg_data: InstanceData, in_nodules_data: InstanceData, out_data: InstanceData): + def task(self, instance: Instance, in_rg_data: InstanceData, in_nodules_data: InstanceData, + in_ct_data: InstanceData, out_data: InstanceData): + print('input nodules data: ' + in_nodules_data.abspath) print('store results in ' + out_data.abspath) - in_nodules_img = sitk.ReadImage(in_nodules_data.abspath) - in_nodules_d = sitk.GetArrayFromImage(sitk.ReadImage(in_nodules_data.abspath)) - in_rg_d = sitk.GetArrayFromImage(sitk.ReadImage(in_rg_data.abspath)) - seg_data = np.zeros(in_rg_d.shape) - seg_data[in_rg_d == 1] = 1 - seg_data[in_nodules_data == 1] = 2 - seg_data[in_rg_d == 2] = 3 - seg_img = sitk.GetImageFromArray(in_rg_d) - inp_dir = self.config.data.requestTempDir(label="lung-post-processor") - inp_file = os.path.join(inp_dir, f'final.nii.gz') - seg_img.CopyInformation(in_nodules_img) - sitk.WriteImage(seg_img, inp_file) - shutil.copyfile(inp_file, out_data.abspath) + + seg_data = self.get_lung(in_rg_data.abspath) + lungs = self.n_connected(seg_data) + + nodules = self.get_lung(in_nodules_data.abspath) + nodules[lungs == 0] = 0 + + seg_img = self.get_seg_img(np.copy(lungs), np.copy(nodules), in_ct_data.abspath) + process_dir = self.config.data.requestTempDir(label="nnunet-lung-processor") + process_file = os.path.join(process_dir, f'final.nii.gz') + sitk.WriteImage( + seg_img, + process_file, + ) + shutil.copyfile(process_file, out_data.abspath) From a08d448d59f614fd702fba83dfc1c0c99cfe6190 Mon Sep 17 00:00:00 2001 From: jithenece Date: Mon, 6 May 2024 17:09:34 +0530 Subject: [PATCH 05/15] avoid same file override with nnunet run --- models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py b/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py index 892037aa..79d81b51 100644 --- a/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py +++ b/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py @@ -128,7 +128,7 @@ def export_prob_mask(self, nnunet_out_dir: str, ref_file: InstanceData, output_d @IO.Instance() @IO.Input("in_data", the="input data to run nnunet on") - @IO.Output("out_data", 'VOLUME_001.nii.gz', 'nifti:mod=seg:model=nnunet_nodule', data='in_data', the="output data from nnunet nodules") + @IO.Output("out_data", 'VOLUME_002.nii.gz', 'nifti:mod=seg:model=nnunet_nodule', data='in_data', the="output data from nnunet nodules") def task(self, instance: Instance, in_data: InstanceData, out_data: InstanceData) -> None: # get the nnunet model to run From d32956b919dc4f2750fdc4d2ff7be55e3ae8b9fb Mon Sep 17 00:00:00 2001 From: jithenece Date: Wed, 8 May 2024 00:31:02 +0530 Subject: [PATCH 06/15] remove custom dseg meta --- models/bamf_nnunet_ct_lung/config/dseg.json | 63 --------------------- 1 file changed, 63 deletions(-) delete mode 100644 models/bamf_nnunet_ct_lung/config/dseg.json diff --git a/models/bamf_nnunet_ct_lung/config/dseg.json b/models/bamf_nnunet_ct_lung/config/dseg.json deleted file mode 100644 index 5d318d36..00000000 --- a/models/bamf_nnunet_ct_lung/config/dseg.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "ContentCreatorName": "BAMFHealth^AI", - "ClinicalTrialSeriesID": "Session1", - "ClinicalTrialTimePointID": "1", - "SeriesDescription": "AIMI Lung Nodule Segmentation", - "SeriesNumber": "300", - "InstanceNumber": "1", - "BodyPartExamined": "LUNG", - "segmentAttributes": [ - [ - { - "labelID": 1, - "SegmentDescription": "Lung", - "SegmentAlgorithmType": "AUTOMATIC", - "SegmentAlgorithmName": "BAMF-Lung-CT", - "SegmentedPropertyCategoryCodeSequence": { - "CodeValue": "123037004", - "CodingSchemeDesignator": "SCT", - "CodeMeaning": "Anatomical Structure" - }, - "SegmentedPropertyTypeCodeSequence": { - "CodeValue": "39607008", - "CodingSchemeDesignator": "SCT", - "CodeMeaning": "Lung" - }, - "SegmentedPropertyTypeModifierCodeSequence": { - "CodeValue": "51440002", - "CodingSchemeDesignator": "SCT", - "CodeMeaning": "Right and left" - } - }, - { - "labelID": 2, - "SegmentDescription": "Nodule", - "SegmentAlgorithmType": "AUTOMATIC", - "SegmentAlgorithmName": "BAMF-Lung-CT", - "AnatomicRegionSequence": { - "CodeValue": "39607008", - "CodingSchemeDesignator": "SCT", - "CodeMeaning": "Lung" - }, - "AnatomicRegionModifierSequence": { - "CodeValue": "51440002", - "CodingSchemeDesignator": "SCT", - "CodeMeaning": "Right and left" - }, - "SegmentedPropertyCategoryCodeSequence": { - "CodeValue": "49755003", - "CodingSchemeDesignator": "SCT", - "CodeMeaning": "Morphologically Altered Structure" - }, - "SegmentedPropertyTypeCodeSequence": { - "CodeValue": "27925004", - "CodingSchemeDesignator": "SCT", - "CodeMeaning": "Nodule" - } - } - ] - ], - "ContentLabel": "SEGMENTATION", - "ContentDescription": "Image segmentation", - "ClinicalTrialCoordinatingCenterName": "dcmqi" -} \ No newline at end of file From d1ea4066286cda1edd7eba6448b77afef73acff4 Mon Sep 17 00:00:00 2001 From: jithenece Date: Wed, 8 May 2024 00:31:30 +0530 Subject: [PATCH 07/15] use segdb code --- models/bamf_nnunet_ct_lung/config/default.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lung/config/default.yml index c035586a..93021ba5 100644 --- a/models/bamf_nnunet_ct_lung/config/default.yml +++ b/models/bamf_nnunet_ct_lung/config/default.yml @@ -24,14 +24,14 @@ modules: in_data: nifti:mod=ct nnunet_task: Task777_CT_Nodules nnunet_model: 3d_fullres - roi: LEFT_LUNG+RIGHT_LUNG,LEFT_LUNG+RIGHT_LUNG+NODULES + roi: LUNG+NODULE export_prob_maps: False NNUnetRunner: in_data: nifti:mod=ct nnunet_task: Task775_CT_NSCLC_RG nnunet_model: 3d_fullres - roi: NEOPLASM_MALIGNANT_PRIMARY + roi: LUNG,LUNG+NEOPLASM_MALIGNANT_PRIMARY export_prob_maps: False LungPostProcessor: @@ -43,7 +43,6 @@ modules: target_dicom: dicom:mod=ct source_segs: nifti:mod=seg:processor=bamf skip_empty_slices: True - json_config_path: models/bamf_nnunet_ct_lung/config/dseg.json DataOrganizer: targets: From 870905ce474f9609e9d58864255384c78196cdca Mon Sep 17 00:00:00 2001 From: jithenece Date: Wed, 8 May 2024 00:34:16 +0530 Subject: [PATCH 08/15] execute NNUnet using single module --- models/bamf_nnunet_ct_lung/config/default.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lung/config/default.yml index 93021ba5..38aef83c 100644 --- a/models/bamf_nnunet_ct_lung/config/default.yml +++ b/models/bamf_nnunet_ct_lung/config/default.yml @@ -6,7 +6,12 @@ general: execute: - DicomImporter - NiftiConverter -- NNUnetRunner +- module: NNUnetRunner + nnunet_task: Task777_CT_Nodules + roi: LUNG+NODULE +- module: NNUnetRunner + nnunet_task: Task775_CT_NSCLC_RG + roi: LUNG,LUNG+NEOPLASM_MALIGNANT_PRIMARY - CustomNNUnetRunner - LungPostProcessor - DsegConverter @@ -20,18 +25,9 @@ modules: meta: mod: '%Modality' - CustomNNUnetRunner: - in_data: nifti:mod=ct - nnunet_task: Task777_CT_Nodules - nnunet_model: 3d_fullres - roi: LUNG+NODULE - export_prob_maps: False - NNUnetRunner: in_data: nifti:mod=ct - nnunet_task: Task775_CT_NSCLC_RG - nnunet_model: 3d_fullres - roi: LUNG,LUNG+NEOPLASM_MALIGNANT_PRIMARY + nnunet_model: 3d_fullres export_prob_maps: False LungPostProcessor: From 644afacba9ce80d62fcc25c2f412cba48682cebb Mon Sep 17 00:00:00 2001 From: jithenece Date: Mon, 20 May 2024 14:58:15 +0530 Subject: [PATCH 09/15] remove CustomNNUnetRunner.py --- .../utils/CustomNNUnetRunner.py | 212 ------------------ .../utils/LungPostProcessor.py | 4 +- 2 files changed, 2 insertions(+), 214 deletions(-) delete mode 100644 models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py diff --git a/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py b/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py deleted file mode 100644 index 79d81b51..00000000 --- a/models/bamf_nnunet_ct_lung/utils/CustomNNUnetRunner.py +++ /dev/null @@ -1,212 +0,0 @@ -""" -------------------------------------------------- -MHub - NNU-Net Runner - This is a base runner for pre-trained - nnunet models. -------------------------------------------------- - -------------------------------------------------- -Author: Leonard Nürnberg -Email: leonard.nuernberg@maastrichtuniversity.nl -------------------------------------------------- -""" -# TODO: support multi-i/o and batch processing on multiple instances - -from typing import List, Optional -import os, subprocess, shutil -import SimpleITK as sitk, numpy as np -from mhubio.core import Module, Instance, InstanceData, DataType, FileType, IO - -# TODO: add an optional evaluation pattern (regex) to IO.Config -nnunet_task_name_regex = r"Task[0-9]{3}_[a-zA-Z0-9_]+" - -@IO.ConfigInput('in_data', 'nifti:mod=ct', the="input data to run nnunet on") -@IO.Config('nnunet_task', str, None, the='nnunet task name') -@IO.Config('nnunet_model', str, None, the='nnunet model name (2d, 3d_lowres, 3d_fullres, 3d_cascade_fullres)') -#@IO.Config('input_data_type', DataType, 'nifti:mod=ct', factory=DataType.fromString, the='input data type') -@IO.Config('folds', int, None, the='number of folds to run nnunet on') -@IO.Config('use_tta', bool, True, the='flag to enable test time augmentation') -@IO.Config('export_prob_maps', bool, False, the='flag to export probability maps') -@IO.Config('prob_map_segments', list, [], the='segment labels for probability maps') -@IO.Config('roi', str, None, the='roi or comma separated list of roi the nnunet segments') -class CustomNNUnetRunner(Module): - - nnunet_task: str - nnunet_model: str - input_data_type: DataType - folds: int # TODO: support optional config attributes - use_tta: bool - export_prob_maps: bool - prob_map_segments: list - roi: str - - def export_prob_mask(self, nnunet_out_dir: str, ref_file: InstanceData, output_dtype: str = 'float32', structure_list: Optional[List[str]] = None): - """ - Convert softmax probability maps to NRRD. For simplicity, the probability maps - are converted by default to UInt8 - Arguments: - model_output_folder : required - path to the folder where the inferred segmentation masks should be stored. - ref_file : required - InstanceData object of the generated segmentation mask used as reference file. - output_dtype : optional - output data type. Data type float16 is not supported by the NRRD standard, - so the choice should be between uint8, uint16 or float32. - structure_list : optional - list of the structures whose probability maps are stored in the - first channel of the `.npz` file (output from the nnU-Net pipeline - when `export_prob_maps` is set to True). - Outputs: - This function [...] - """ - - # initialize structure list - if structure_list is None: - if self.roi is not None: - structure_list = self.roi.split(',') - else: - structure_list = [] - - # sanity check user inputs - assert(output_dtype in ["uint8", "uint16", "float32"]) - - # input file containing the raw information - pred_softmax_fn = 'VOLUME_001.npz' - pred_softmax_path = os.path.join(nnunet_out_dir, pred_softmax_fn) - - # parse NRRD file - we will make use of if to populate the header of the - # NRRD mask we are going to get from the inferred segmentation mask - sitk_ct = sitk.ReadImage(ref_file.abspath) - - # generate bundle for prob masks - # TODO: we really have to create folders (or add this as an option that defaults to true) automatically - prob_masks_bundle = ref_file.getDataBundle('prob_masks') - if not os.path.isdir(prob_masks_bundle.abspath): - os.mkdir(prob_masks_bundle.abspath) - - # load softmax probability maps - pred_softmax_all = np.load(pred_softmax_path)["softmax"] - - # iterate all channels - for channel in range(0, len(pred_softmax_all)): - - structure = structure_list[channel] if channel < len(structure_list) else f"structure_{channel}" - pred_softmax_segmask = pred_softmax_all[channel].astype(dtype = np.float32) - - if output_dtype == "float32": - # no rescale needed - the values will be between 0 and 1 - # set SITK image dtype to Float32 - sitk_dtype = sitk.sitkFloat32 - - elif output_dtype == "uint8": - # rescale between 0 and 255, quantize - pred_softmax_segmask = (255*pred_softmax_segmask).astype(np.int32) - # set SITK image dtype to UInt8 - sitk_dtype = sitk.sitkUInt8 - - elif output_dtype == "uint16": - # rescale between 0 and 65536 - pred_softmax_segmask = (65536*pred_softmax_segmask).astype(np.int32) - # set SITK image dtype to UInt16 - sitk_dtype = sitk.sitkUInt16 - else: - raise ValueError("Invalid output data type. Please choose between uint8, uint16 or float32.") - - pred_softmax_segmask_sitk = sitk.GetImageFromArray(pred_softmax_segmask) - pred_softmax_segmask_sitk.CopyInformation(sitk_ct) - pred_softmax_segmask_sitk = sitk.Cast(pred_softmax_segmask_sitk, sitk_dtype) - - # generate data - prob_mask = InstanceData(f'{structure}.nrrd', DataType(FileType.NRRD, {'mod': 'prob_mask', 'structure': structure}), bundle=prob_masks_bundle) - - # export file - writer = sitk.ImageFileWriter() - writer.UseCompressionOn() - writer.SetFileName(prob_mask.abspath) - writer.Execute(pred_softmax_segmask_sitk) - - # check if the file was written - if os.path.isfile(prob_mask.abspath): - self.v(f" > prob mask for {structure} saved to {prob_mask.abspath}") - prob_mask.confirm() - - @IO.Instance() - @IO.Input("in_data", the="input data to run nnunet on") - @IO.Output("out_data", 'VOLUME_002.nii.gz', 'nifti:mod=seg:model=nnunet_nodule', data='in_data', the="output data from nnunet nodules") - def task(self, instance: Instance, in_data: InstanceData, out_data: InstanceData) -> None: - - # get the nnunet model to run - self.v("Running nnUNet_predict.") - self.v(f" > task: {self.nnunet_task}") - self.v(f" > model: {self.nnunet_model}") - self.v(f" > input data: {in_data.abspath}") - self.v(f" > output data: {out_data.abspath}") - - # download weights if not found - # NOTE: only for testing / debugging. For productiio always provide the weights in the Docker container. - if not os.path.isdir(os.path.join(os.environ["WEIGHTS_FOLDER"], '')): - print("Downloading nnUNet model weights...") - bash_command = ["nnUNet_download_pretrained_model", self.nnunet_task] - self.subprocess(bash_command, text=True) - - # bring input data in nnunet specific format - # NOTE: only for nifti data as we hardcode the nnunet-formatted-filename (and extension) for now. - assert in_data.type.ftype == FileType.NIFTI - assert in_data.abspath.endswith('.nii.gz') - inp_dir = self.config.data.requestTempDir(label="nnunet-model-inp") - inp_file = f'VOLUME_001_0000.nii.gz' - shutil.copyfile(in_data.abspath, os.path.join(inp_dir, inp_file)) - - # define output folder (temp dir) and also override environment variable for nnunet - out_dir = self.config.data.requestTempDir(label="nnunet-model-out") - os.environ['RESULTS_FOLDER'] = out_dir - - # symlink nnunet input folder to the input data with python - # create symlink in python - # NOTE: this is a workaround for the nnunet bash script that expects the input data to be in a specific folder - # structure. This is not the case for the mhub data structure. So we create a symlink to the input data - # in the nnunet input folder structure. - os.symlink(os.environ['WEIGHTS_FOLDER'], os.path.join(out_dir, 'nnUNet')) - - # NOTE: instead of running from commandline this could also be done in a pythonic way: - # `nnUNet/nnunet/inference/predict.py` - but it would require - # to set manually all the arguments that the user is not intended - # to fiddle with; so stick with the bash executable - - # construct nnunet inference command - bash_command = ["nnUNet_predict"] - bash_command += ["--input_folder", str(inp_dir)] - bash_command += ["--output_folder", str(out_dir)] - bash_command += ["--task_name", self.nnunet_task] - bash_command += ["--model", self.nnunet_model] - - # add optional arguments - if self.folds is not None: - bash_command += ["--folds", str(self.folds)] - - if not self.use_tta: - bash_command += ["--disable_tta"] - - if self.export_prob_maps: - bash_command += ["--save_npz"] - - # run command - self.subprocess(bash_command, text=True) - - # output meta - meta = { - "model": "nnunet", - "nnunet_task": self.nnunet_task, - "nnunet_model": self.nnunet_model, - "roi": self.roi - } - - # get output data - out_file = f'VOLUME_001.nii.gz' - out_path = os.path.join(out_dir, out_file) - - # copy output data to instance - shutil.copyfile(out_path, out_data.abspath) - - # export probabiliy maps if requested as dynamic data - if self.export_prob_maps: - self.export_prob_mask(str(out_dir), out_data, 'float32', self.prob_map_segments) - - # update meta dynamically - out_data.type.meta += meta diff --git a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py index 4eeb0dfc..755ceebc 100644 --- a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py +++ b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py @@ -48,8 +48,8 @@ def get_seg_img(self, lungs, nodules, ct_path): @IO.Input('in_rg_data', 'nifti:mod=seg:nnunet_task=Task775_CT_NSCLC_RG', the='input data from custom nnunet module') @IO.Input('in_nodules_data', 'nifti:mod=seg:nnunet_task=Task777_CT_Nodules', the='input data nnunet nodule') @IO.Input('in_ct_data', 'nifti:mod=ct', the='input ct data') - @IO.Output('out_data', 'bamf_processed.nii.gz', 'nifti:mod=seg:processor=bamf', data='in_rg_data', - the="keep the two largest connected components of the segmentation and remove all other ones") + @IO.Output('out_data', 'bamf_processed.nii.gz', 'nifti:mod=seg:processor=bamf:roi=LUNG,LUNG+NODULE', + data='in_rg_data', the="get lung and lung nodule segmentation file") def task(self, instance: Instance, in_rg_data: InstanceData, in_nodules_data: InstanceData, in_ct_data: InstanceData, out_data: InstanceData): From 92ced5b0d38a95e2850c74b55607a546a17f8849 Mon Sep 17 00:00:00 2001 From: jithenece Date: Thu, 23 May 2024 00:04:04 +0530 Subject: [PATCH 10/15] use FDG_AVID_TUMOR label --- models/bamf_nnunet_ct_lung/config/default.yml | 4 +- models/bamf_nnunet_ct_lung/meta.json | 66 ++++++++++++------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lung/config/default.yml index 38aef83c..0d5f44e0 100644 --- a/models/bamf_nnunet_ct_lung/config/default.yml +++ b/models/bamf_nnunet_ct_lung/config/default.yml @@ -1,7 +1,7 @@ general: data_base_dir: /app/data version: 1.0 - description: default configuration for Bamf NNUnet Lung, Nodules and Tumor segmentation (dicom to dicom) + description: default configuration for Bamf NNUnet Lung and Nodules segmentation (dicom to dicom) execute: - DicomImporter @@ -11,7 +11,7 @@ execute: roi: LUNG+NODULE - module: NNUnetRunner nnunet_task: Task775_CT_NSCLC_RG - roi: LUNG,LUNG+NEOPLASM_MALIGNANT_PRIMARY + roi: LUNG,LUNG+FDG_AVID_TUMOR - CustomNNUnetRunner - LungPostProcessor - DsegConverter diff --git a/models/bamf_nnunet_ct_lung/meta.json b/models/bamf_nnunet_ct_lung/meta.json index e6440f48..92c2bb52 100644 --- a/models/bamf_nnunet_ct_lung/meta.json +++ b/models/bamf_nnunet_ct_lung/meta.json @@ -20,11 +20,10 @@ { "label": "Segmentation", "type": "Segmentation", - "description": "Segmentation lung", + "description": "Lung and Nodules (3mm-30mm) from CT scans", "classes": [ "LUNG", - "LUNG+NODULES", - "LUNG+NEOPLASM_MALIGNANT_PRIMARY" + "LUNG+NODULE" ] } ], @@ -35,7 +34,7 @@ }, "data": { "training": { - "vol_samples": 833 + "vol_samples": 1405 }, "evaluation": { "vol_samples": 47 @@ -71,32 +70,47 @@ "uri": "https://arxiv.org/abs/2310.14897" } ], - "github": "https://github.com/bamf-health/aimi-ct-lung" + "github": "https://github.com/bamf-health/aimi-lung-ct" }, "info": { "use": { "title": "Intended Use", - "text": "This model is intended to perform lung, nodules and tumor segmentation in CT scans. The model has been trained and tested on scans aquired during clinical care of patients, so it might not be suited for a healthy population. The generalization capabilities of the model on a range of ages, genders, and ethnicities are unknown." + "text": "This model is intended to perform lung and nodules segmentation in CT scans. The model has been trained and tested on scans acquired during clinical care of patients, so it might not be suited for a healthy population. The generalization capabilities of the model on a range of ages, genders, and ethnicities are unknown." }, "analyses": { "title": "Quantitative Analyses", - "text": "The model's performance was assessed using the Dice Coefficient and Normalized Surface Distance (NSD) with tolerance 7mm, as specified in the CT Liver segmentation task in the Medical Segmentation Decathlon challenge. The model was used to segment cases from the IDC collection TCGA-LIHC [1]. Nine of those cases were reviewed and corrected by a board-certified radiologist and a non-expert. The analysis is published here [2]", + "text": "The model's performance was assessed using the Dice Coefficient and 95% Hausdorff (mm). ", "tables": [ { - "label": "Label-wise metrics (mean (standard deviation)) between AI derived and manually corrected CT lung annotations", + "label": "Segmentation Metric - Lung - Expert 1", "entries": { - "Dice: Radiologist": "0.97 (0.07)", - "NSD: Radiologist": "0.96 (0.09)", - "Dice: Non-expert": "0.97 (0.08)", - "NSD: Non-expert": "0.95 (0.11)" + "DSC": "0.99 (0.02)", + "95% Hausdorff (mm)": "2.34 (5.89)" + } + }, + { + "label": "Segmentation Metric - Nodule - Expert 1", + "entries": { + "DSC": "0.60 (0.42)", + "95% Hausdorff (mm)": "56.72 (64.36)" + } + }, + { + "label": "Segmentation Metric - Lung - Expert 2", + "entries": { + "DSC": "1.00 (0.00)", + "95% Hausdorff (mm)": "0.30 (1.70)" + } + }, + { + "label": "Segmentation Metric - Nodule - Expert 2", + "entries": { + "DSC": "0.78 (0.34)", + "95% Hausdorff (mm)": "26.06 (48.63)" } } ], "references": [ - { - "label": "DICOM-LIDC-IDRI-Nodules", - "uri": "https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=6885436" - }, { "label": "The AIMI Initiative: AI-Generated Annotations for Imaging Data Commons Collections", "uri": "https://arxiv.org/abs/2310.14897" @@ -105,15 +119,19 @@ }, "evaluation": { "title": "Evaluation Data", - "text": "The model was used to segment cases 89 from the IDC [1] collection TCGA-LIHC [1]. Nine of those cases were randomly selected to be reviewed and corrected by a board-certified radiologist. The model predictions, and radiologist corrections are published on zenodo [3]", + "text": "A validation set of 47 cases were reviewed by two radiologists", "references": [ { "label": "Imaging Data Collections (IDC)", "uri": "https://datacommons.cancer.gov/repository/imaging-data-commons" }, { - "label": "TCGA-LIHC", - "uri": "https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=6885436" + "label": "DICOM-LIDC-IDRI-Nodule", + "uri": "https://doi.org/10.7937/TCIA.2018.h7umfurq" + }, + { + "label": "NSCLC Radiomics", + "uri": "https://doi.org/10.7937/K9/TCIA.2017.7hs46erv" }, { "label": "Image segmentations produced by the AIMI Annotations initiative", @@ -123,15 +141,15 @@ }, "training": { "title": "Training Data", - "text": "The training dataset consists of 1565 CT liver annotations taken from TotalSegmentator [1] (N=1204) and Flare21 Liver Dataset [2] (N=361).", + "text": "The DICOM-LIDC-IDRI-Nodules dataset was used to train an AI model to annotate lung nodules. This dataset included 883 studies with annotated nodules from 875 patients. Within the dataset only nodules that were identified by all four of their radiologists (size condition: 3mm ≤ diameter ≤ 30mm), were considered for AI model training for this task. The lung annotations AI model was trained on 411 and 111 lung CT data from NSCLC Radiomics and NSCLC Radiogenomics respectively.", "references": [ { - "label": "TotalSegmentator Dataset", - "uri": "https://zenodo.org/records/10047292" + "label": "DICOM-LIDC-IDRI-Nodule", + "uri": "https://doi.org/10.7937/TCIA.2018.h7umfurq" }, { - "label": "Flare21 Dataset", - "uri": "https://zenodo.org/records/5903672" + "label": "NSCLC Radiomics", + "uri": "https://doi.org/10.7937/K9/TCIA.2017.7hs46erv" } ] } From 67307ce128ba20db3909d3a943183f2022210680 Mon Sep 17 00:00:00 2001 From: jithenece Date: Thu, 23 May 2024 01:06:42 +0530 Subject: [PATCH 11/15] remove customNNUnet Runner --- models/bamf_nnunet_ct_lung/config/default.yml | 3 +-- models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile | 3 +-- models/bamf_nnunet_ct_lung/meta.json | 6 +++--- models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lung/config/default.yml index 0d5f44e0..b7774c1b 100644 --- a/models/bamf_nnunet_ct_lung/config/default.yml +++ b/models/bamf_nnunet_ct_lung/config/default.yml @@ -12,7 +12,6 @@ execute: - module: NNUnetRunner nnunet_task: Task775_CT_NSCLC_RG roi: LUNG,LUNG+FDG_AVID_TUMOR -- CustomNNUnetRunner - LungPostProcessor - DsegConverter - DataOrganizer @@ -35,7 +34,7 @@ modules: in_nodules_data: nifti:mod=seg:nnunet_task=Task777_CT_Nodules DsegConverter: - model_name: Bamf NNUnet CT Lung + model_name: Bamf NNUnet CT Lung and Nodule target_dicom: dicom:mod=ct source_segs: nifti:mod=seg:processor=bamf skip_empty_slices: True diff --git a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile b/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile index bd838944..31d689c1 100644 --- a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile +++ b/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile @@ -8,8 +8,7 @@ FROM mhubai/base:latest ENV SKLEARN_ALLOW_DEPRECATED_SKLEARN_PACKAGE_INSTALL=True # Install nnunet and platipy -RUN pip3 install --no-cache-dir \ - nnunet +RUN pip3 install --no-cache-dir nnunet # Clone the main branch of MHubAI/models ARG MHUB_MODELS_REPO diff --git a/models/bamf_nnunet_ct_lung/meta.json b/models/bamf_nnunet_ct_lung/meta.json index 92c2bb52..dfedfbf8 100644 --- a/models/bamf_nnunet_ct_lung/meta.json +++ b/models/bamf_nnunet_ct_lung/meta.json @@ -3,7 +3,7 @@ "name": "bamf_nnunet_ct_lung", "title": "AIMI CT Lung and Nodules", "summary": { - "description": "An nnU-Net based model to segment lung from CT scans", + "description": "An nnU-Net based model to segment Lung and Nodules from CT scans", "inputs": [ { "label": "Input Image", @@ -37,14 +37,14 @@ "vol_samples": 1405 }, "evaluation": { - "vol_samples": 47 + "vol_samples": 77 }, "public": true, "external": true } }, "details": { - "name": "AIMI CT Lung", + "name": "AIMI CT Lung and Nodule", "version": "1.0.0", "devteam": "BAMF Health", "authors": [ diff --git a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py index 755ceebc..b37b1a33 100644 --- a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py +++ b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py @@ -45,8 +45,8 @@ def get_seg_img(self, lungs, nodules, ct_path): return seg_img @IO.Instance() - @IO.Input('in_rg_data', 'nifti:mod=seg:nnunet_task=Task775_CT_NSCLC_RG', the='input data from custom nnunet module') - @IO.Input('in_nodules_data', 'nifti:mod=seg:nnunet_task=Task777_CT_Nodules', the='input data nnunet nodule') + @IO.Input('in_rg_data', 'nifti:mod=seg:nnunet_task=Task775_CT_NSCLC_RG', the='input data from rg nnunet module') + @IO.Input('in_nodules_data', 'nifti:mod=seg:nnunet_task=Task777_CT_Nodules', the='input data from nodules nnunet nodule') @IO.Input('in_ct_data', 'nifti:mod=ct', the='input ct data') @IO.Output('out_data', 'bamf_processed.nii.gz', 'nifti:mod=seg:processor=bamf:roi=LUNG,LUNG+NODULE', data='in_rg_data', the="get lung and lung nodule segmentation file") From ee4a7d39c89b2f7e2d3e3f7a43b10c0b66a0a171 Mon Sep 17 00:00:00 2001 From: jithenece Date: Wed, 5 Jun 2024 01:48:05 +0530 Subject: [PATCH 12/15] set nnunet version --- models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile b/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile index 31d689c1..b8db2d51 100644 --- a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile +++ b/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile @@ -8,7 +8,7 @@ FROM mhubai/base:latest ENV SKLEARN_ALLOW_DEPRECATED_SKLEARN_PACKAGE_INSTALL=True # Install nnunet and platipy -RUN pip3 install --no-cache-dir nnunet +RUN pip3 install --no-cache-dir nnunet==1.7.1 # Clone the main branch of MHubAI/models ARG MHUB_MODELS_REPO From e266294d0821fcbacc92654ee2c3969e1bf4ebdc Mon Sep 17 00:00:00 2001 From: jithenece Date: Wed, 3 Jul 2024 01:37:01 +0530 Subject: [PATCH 13/15] update meta info --- models/bamf_nnunet_ct_lung/config/default.yml | 2 +- models/bamf_nnunet_ct_lung/meta.json | 38 ++++++++++++++----- .../utils/LungPostProcessor.py | 11 +++++- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lung/config/default.yml index b7774c1b..e6e97a0f 100644 --- a/models/bamf_nnunet_ct_lung/config/default.yml +++ b/models/bamf_nnunet_ct_lung/config/default.yml @@ -34,7 +34,7 @@ modules: in_nodules_data: nifti:mod=seg:nnunet_task=Task777_CT_Nodules DsegConverter: - model_name: Bamf NNUnet CT Lung and Nodule + model_name: Bamf CT Lung and Nodule Segmentation target_dicom: dicom:mod=ct source_segs: nifti:mod=seg:processor=bamf skip_empty_slices: True diff --git a/models/bamf_nnunet_ct_lung/meta.json b/models/bamf_nnunet_ct_lung/meta.json index dfedfbf8..585d383e 100644 --- a/models/bamf_nnunet_ct_lung/meta.json +++ b/models/bamf_nnunet_ct_lung/meta.json @@ -3,7 +3,7 @@ "name": "bamf_nnunet_ct_lung", "title": "AIMI CT Lung and Nodules", "summary": { - "description": "An nnU-Net based model to segment Lung and Nodules from CT scans", + "description": "An nnU-Net based model to segment Lung and Nodules (3mm-30mm) from CT scans", "inputs": [ { "label": "Input Image", @@ -11,7 +11,7 @@ "format": "DICOM", "modality": "CT", "bodypartexamined": "LUNG", - "slicethickness": "2.5mm", + "slicethickness": "10mm", "non-contrast": true, "contrast": false } @@ -37,7 +37,7 @@ "vol_samples": 1405 }, "evaluation": { - "vol_samples": 77 + "vol_samples": 47 }, "public": true, "external": true @@ -119,19 +119,39 @@ }, "evaluation": { "title": "Evaluation Data", - "text": "A validation set of 47 cases were reviewed by two radiologists", + "text": "The model was used to segment cases from the IDC [1] collection of ACRIN-NSCLC-FDG-PET [2], Anti-PD-1-Lung [3], LUNG-PET-CT-Dx[4], NSCLC Radiogenomics[5], RIDER Lung PET-CT[6], TCGA-LUAD[7], TCGA-LUSC[8] . Approximately 47 of those cases were randomly selected to be reviewed and corrected by a board-certified radiologist. The model predictions, and radiologist corrections are published on zenodo [9]", "references": [ { "label": "Imaging Data Collections (IDC)", "uri": "https://datacommons.cancer.gov/repository/imaging-data-commons" }, { - "label": "DICOM-LIDC-IDRI-Nodule", - "uri": "https://doi.org/10.7937/TCIA.2018.h7umfurq" + "label": "ACRIN-NSCLC-FDG-PET", + "uri": "https://www.cancerimagingarchive.net/collection/acrin-nsclc-fdg-pet/" }, { - "label": "NSCLC Radiomics", - "uri": "https://doi.org/10.7937/K9/TCIA.2017.7hs46erv" + "label": "Anti-PD-1-Lung", + "uri": "https://www.cancerimagingarchive.net/collection/anti-pd-1_lung/" + }, + { + "label": "LUNG-PET-CT-Dx", + "uri": "https://www.cancerimagingarchive.net/collection/lung-pet-ct-dx/" + }, + { + "label": "NSCLC Radiogenomics", + "uri": "https://www.cancerimagingarchive.net/collection/nsclc-radiogenomics/" + }, + { + "label": "RIDER Lung PET-CT", + "uri": "https://wiki.cancerimagingarchive.net/display/Public/RIDER+Collections" + }, + { + "label": "TCGA-LUAD", + "uri": "https://www.cancerimagingarchive.net/collection/tcga-luad/" + }, + { + "label": "TCGA-LUSC", + "uri": "https://www.cancerimagingarchive.net/collection/tcga-lusc/" }, { "label": "Image segmentations produced by the AIMI Annotations initiative", @@ -141,7 +161,7 @@ }, "training": { "title": "Training Data", - "text": "The DICOM-LIDC-IDRI-Nodules dataset was used to train an AI model to annotate lung nodules. This dataset included 883 studies with annotated nodules from 875 patients. Within the dataset only nodules that were identified by all four of their radiologists (size condition: 3mm ≤ diameter ≤ 30mm), were considered for AI model training for this task. The lung annotations AI model was trained on 411 and 111 lung CT data from NSCLC Radiomics and NSCLC Radiogenomics respectively.", + "text": "The DICOM-LIDC-IDRI-Nodules[1] dataset was used to train an AI model to annotate lung nodules. This dataset included 883 studies with annotated nodules from 875 patients. Within the dataset only nodules that were identified by all four of their radiologists (size condition: 3mm ≤ diameter ≤ 30mm), were considered for AI model training for this task. The lung annotations AI model was trained on 411 and 111 lung CT data from NSCLC Radiomics and NSCLC Radiogenomics respectively[2].", "references": [ { "label": "DICOM-LIDC-IDRI-Nodule", diff --git a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py index b37b1a33..d86e4a3b 100644 --- a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py +++ b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py @@ -1,3 +1,12 @@ +""" +------------------------------------------------- +MHub - Run Module for perform postprocessing logic on segmentations. +------------------------------------------------- +------------------------------------------------- +Author: Jithendra Kumar +Email: jithendra.kumar@bamfhealth.com +------------------------------------------------- +""" from mhubio.core import IO from mhubio.core import Module, Instance, InstanceData import os, shutil @@ -45,7 +54,7 @@ def get_seg_img(self, lungs, nodules, ct_path): return seg_img @IO.Instance() - @IO.Input('in_rg_data', 'nifti:mod=seg:nnunet_task=Task775_CT_NSCLC_RG', the='input data from rg nnunet module') + @IO.Input('in_rg_data', 'nifti:mod=seg:nnunet_task=Task775_CT_NSCLC_RG', the='input data from lung nnunet module') @IO.Input('in_nodules_data', 'nifti:mod=seg:nnunet_task=Task777_CT_Nodules', the='input data from nodules nnunet nodule') @IO.Input('in_ct_data', 'nifti:mod=ct', the='input ct data') @IO.Output('out_data', 'bamf_processed.nii.gz', 'nifti:mod=seg:processor=bamf:roi=LUNG,LUNG+NODULE', From b84f9c71ef655539a8debe3e348b9f9126c1bec2 Mon Sep 17 00:00:00 2001 From: jithenece Date: Thu, 25 Jul 2024 21:17:52 +0530 Subject: [PATCH 14/15] fix review comments --- models/bamf_nnunet_ct_lung/config/default.yml | 2 +- .../utils/LungPostProcessor.py | 59 ++++++++++++------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lung/config/default.yml index e6e97a0f..6d76791a 100644 --- a/models/bamf_nnunet_ct_lung/config/default.yml +++ b/models/bamf_nnunet_ct_lung/config/default.yml @@ -41,4 +41,4 @@ modules: DataOrganizer: targets: - - dicomseg-->[i:sid]/bamf_nnunet_ct_lung.seg.dcm \ No newline at end of file + - dicomseg-->[i:sid]/bamf_nnunet_ct_lungnodules.seg.dcm \ No newline at end of file diff --git a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py index d86e4a3b..164a2725 100644 --- a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py +++ b/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py @@ -9,21 +9,37 @@ """ from mhubio.core import IO from mhubio.core import Module, Instance, InstanceData -import os, shutil import SimpleITK as sitk import numpy as np -from skimage import measure, filters +from skimage import measure class LungPostProcessor(Module): - def get_lung(self, ip_path): - # get lung segmentation + def perform_binary_threshold_segmentation(self, ip_path): + """ + Perform binary threshold segmentation on the input image. + + Args: + - ip_path (str): Path to the input image file. + + Returns: + - numpy.ndarray: Segmented binary mask where non-zero values represent the segmented region. + """ seg_data = sitk.GetArrayFromImage(sitk.ReadImage(ip_path)) seg_data[seg_data > 0] = 1 return seg_data - def n_connected(self, img_data): + def extract_largest_connected_component(self, img_data): + """ + Extract the single largest connected component from the segmentation image data. + + Args: + - img_data (numpy.ndarray): Segmentation image data where connected components are to be identified. + + Returns: + - numpy.ndarray: Binary image data with only the largest connected component retained. + """ img_data_mask = np.zeros(img_data.shape) img_data_mask[img_data >= 1] = 1 img_filtered = np.zeros(img_data_mask.shape) @@ -37,14 +53,24 @@ def n_connected(self, img_data): for key, value in sorted_dict.items(): if count >= 1 and count <= 2: - print(key, value) img_filtered[blobs_labels == key] = 1 count += 1 img_data[img_filtered != 1] = 0 return img_data - def get_seg_img(self, lungs, nodules, ct_path): + def create_segmentation_image(self, lungs, nodules, ct_path): + """ + Create a segmentation image combining lung and nodule segmentations. + + Args: + - lungs (numpy.ndarray): Binary mask of lung segmentation. + - nodules (numpy.ndarray): Binary mask of nodule segmentation. + - ct_path (str): Path to the original CT image used as reference. + + Returns: + - SimpleITK.Image: Segmentation image where lung and nodule regions are labeled as 1 and 2, respectively. + """ seg_data = np.zeros(lungs.shape) seg_data[lungs == 1] = 1 seg_data[nodules == 1] = 2 @@ -62,20 +88,13 @@ def get_seg_img(self, lungs, nodules, ct_path): def task(self, instance: Instance, in_rg_data: InstanceData, in_nodules_data: InstanceData, in_ct_data: InstanceData, out_data: InstanceData): - print('input nodules data: ' + in_nodules_data.abspath) - print('store results in ' + out_data.abspath) + self.v('running LungPostProcessor') - seg_data = self.get_lung(in_rg_data.abspath) - lungs = self.n_connected(seg_data) + seg_data = self.perform_binary_threshold_segmentation(in_rg_data.abspath) + lungs = self.extract_largest_connected_component(seg_data) - nodules = self.get_lung(in_nodules_data.abspath) + nodules = self.perform_binary_threshold_segmentation(in_nodules_data.abspath) nodules[lungs == 0] = 0 - seg_img = self.get_seg_img(np.copy(lungs), np.copy(nodules), in_ct_data.abspath) - process_dir = self.config.data.requestTempDir(label="nnunet-lung-processor") - process_file = os.path.join(process_dir, f'final.nii.gz') - sitk.WriteImage( - seg_img, - process_file, - ) - shutil.copyfile(process_file, out_data.abspath) + final_seg_img = self.create_segmentation_image(np.copy(lungs), np.copy(nodules), in_ct_data.abspath) + sitk.WriteImage(final_seg_img,out_data.abspath) From 6836183a9f2896be45ea9f05c3585463fc863b8d Mon Sep 17 00:00:00 2001 From: jithenece Date: Mon, 29 Jul 2024 13:30:23 +0530 Subject: [PATCH 15/15] change directory name to match output file name --- .../config/default.yml | 0 .../dockerfiles/Dockerfile | 4 ++-- .../meta.json | 2 +- .../utils/LungPostProcessor.py | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename models/{bamf_nnunet_ct_lung => bamf_nnunet_ct_lungnodules}/config/default.yml (100%) rename models/{bamf_nnunet_ct_lung => bamf_nnunet_ct_lungnodules}/dockerfiles/Dockerfile (89%) rename models/{bamf_nnunet_ct_lung => bamf_nnunet_ct_lungnodules}/meta.json (99%) rename models/{bamf_nnunet_ct_lung => bamf_nnunet_ct_lungnodules}/utils/LungPostProcessor.py (100%) diff --git a/models/bamf_nnunet_ct_lung/config/default.yml b/models/bamf_nnunet_ct_lungnodules/config/default.yml similarity index 100% rename from models/bamf_nnunet_ct_lung/config/default.yml rename to models/bamf_nnunet_ct_lungnodules/config/default.yml diff --git a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile b/models/bamf_nnunet_ct_lungnodules/dockerfiles/Dockerfile similarity index 89% rename from models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile rename to models/bamf_nnunet_ct_lungnodules/dockerfiles/Dockerfile index b8db2d51..298ebaae 100644 --- a/models/bamf_nnunet_ct_lung/dockerfiles/Dockerfile +++ b/models/bamf_nnunet_ct_lungnodules/dockerfiles/Dockerfile @@ -12,7 +12,7 @@ RUN pip3 install --no-cache-dir nnunet==1.7.1 # Clone the main branch of MHubAI/models ARG MHUB_MODELS_REPO -RUN buildutils/import_mhub_model.sh bamf_nnunet_ct_lung ${MHUB_MODELS_REPO} +RUN buildutils/import_mhub_model.sh bamf_nnunet_ct_lungnodules ${MHUB_MODELS_REPO} # Pull nnUNet model weights into the container for Task777_CT_Nodules ENV WEIGHTS_DIR=/root/.nnunet/nnUNet_models/nnUNet/ @@ -36,4 +36,4 @@ ENV WEIGHTS_FOLDER=$WEIGHTS_DIR # Default run script ENTRYPOINT ["mhub.run"] -CMD ["--config", "/app/models/bamf_nnunet_ct_lung/config/default.yml"] \ No newline at end of file +CMD ["--config", "/app/models/bamf_nnunet_ct_lungnodules/config/default.yml"] \ No newline at end of file diff --git a/models/bamf_nnunet_ct_lung/meta.json b/models/bamf_nnunet_ct_lungnodules/meta.json similarity index 99% rename from models/bamf_nnunet_ct_lung/meta.json rename to models/bamf_nnunet_ct_lungnodules/meta.json index 585d383e..a516c082 100644 --- a/models/bamf_nnunet_ct_lung/meta.json +++ b/models/bamf_nnunet_ct_lungnodules/meta.json @@ -1,6 +1,6 @@ { "id": "", - "name": "bamf_nnunet_ct_lung", + "name": "bamf_nnunet_ct_lungnodules", "title": "AIMI CT Lung and Nodules", "summary": { "description": "An nnU-Net based model to segment Lung and Nodules (3mm-30mm) from CT scans", diff --git a/models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py b/models/bamf_nnunet_ct_lungnodules/utils/LungPostProcessor.py similarity index 100% rename from models/bamf_nnunet_ct_lung/utils/LungPostProcessor.py rename to models/bamf_nnunet_ct_lungnodules/utils/LungPostProcessor.py