From a52b616c215bbad879f7d2c3d1b31ff8c23e21a8 Mon Sep 17 00:00:00 2001 From: Suraj Pai Date: Fri, 16 Feb 2024 11:09:53 -0500 Subject: [PATCH 01/13] Added first working FMCIB model container --- models/fmcib/config/default.yml | 20 +++++++++++ models/fmcib/dockerfiles/Dockerfile | 16 +++++++++ models/fmcib/meta.json | 35 +++++++++++++++++++ models/fmcib/utils/FMCIBRunner.py | 53 +++++++++++++++++++++++++++++ models/fmcib/utils/__init__.py | 1 + 5 files changed, 125 insertions(+) create mode 100644 models/fmcib/config/default.yml create mode 100644 models/fmcib/dockerfiles/Dockerfile create mode 100644 models/fmcib/meta.json create mode 100644 models/fmcib/utils/FMCIBRunner.py create mode 100644 models/fmcib/utils/__init__.py diff --git a/models/fmcib/config/default.yml b/models/fmcib/config/default.yml new file mode 100644 index 00000000..4f91e608 --- /dev/null +++ b/models/fmcib/config/default.yml @@ -0,0 +1,20 @@ +general: + data_base_dir: /app/data + version: 1.0 + description: "FMCIB pipeline" + +execute: +- FileStructureImporter +- FMCIBRunner +- DataOrganizer + +modules: + FileStructureImporter: + structures: + - $patientID/CT.nrrd@instance@nrrd:mod=ct + - $patientID/masks/GTV-1.nrrd@nrrd + import_id: patientID + + DataOrganizer: + targets: + - json-->[i:patientID]/features.json \ No newline at end of file diff --git a/models/fmcib/dockerfiles/Dockerfile b/models/fmcib/dockerfiles/Dockerfile new file mode 100644 index 00000000..9f3d9603 --- /dev/null +++ b/models/fmcib/dockerfiles/Dockerfile @@ -0,0 +1,16 @@ +FROM mhubai/base:latest + +LABEL authors="bspai@bwh.harvard.edu" + + + +RUN wget https://zenodo.org/records/10528450/files/model_weights.torch?download=1 -O /app/model_weights.torch + +RUN mkdir models +RUN mkdir models/fmcib + +# Install FMCIB package, should install everything else ... +RUN pip install foundation-cancer-image-biomarker --pre + +ENTRYPOINT ["python3", "-m", "mhubio.run"] +CMD ["--config", "/app/models/fmcib/config/default.yml", "--print"] diff --git a/models/fmcib/meta.json b/models/fmcib/meta.json new file mode 100644 index 00000000..9256e570 --- /dev/null +++ b/models/fmcib/meta.json @@ -0,0 +1,35 @@ +{ + "id": "", + "name": "fmcib", + "title": "Foundation Model for Cancer Imaging Biomarkers", + "summary": { + "description": "This algorithm extracts a 4096 dimensonal feature set for a volume centered on the tumor location", + "inputs": [ + { + + }, + { + + } + ], + "outputs": [ + { + + } + ], + "model": { + "architecture": "Resnet50 (2x wide)", + "training": "weakly-supervised contrastive learning", + "cmpapproach": "3D" + }, + "data": { + "training": { + "vol_samples": 11467 + }, + "evaluation": { + }, + "public": true, + "external": false + } + } +} \ No newline at end of file diff --git a/models/fmcib/utils/FMCIBRunner.py b/models/fmcib/utils/FMCIBRunner.py new file mode 100644 index 00000000..4e783867 --- /dev/null +++ b/models/fmcib/utils/FMCIBRunner.py @@ -0,0 +1,53 @@ +""" +--------------------------------------------------------- +Author: Suraj Pia +Email: bspai@bwh.harvard.edu +--------------------------------------------------------- +""" + +import json +import torch +from fmcib.models import fmcib_model +import SimpleITK as sitk +from mhubio.core import Instance, InstanceData, IO, Module +from fmcib.preprocessing import preprocess + + +class FMCIBRunner(Module): + @IO.Instance() + @IO.Input('in_data', 'nrrd:mod=ct', the='Input NRRD file') + @IO.Input('in_mask', 'nrrd|json', the='Tumor mask for the input NRRD file') + @IO.Output('feature_json', 'features.json', "json", bundle='model', the='output JSON file') + def task(self, instance: Instance, in_data: InstanceData, in_mask: InstanceData, feature_json: InstanceData) -> None: + mask_path = in_mask.abspath + mask = sitk.ReadImage(mask_path) + + # Get the CoM of the mask + label_shape_filter = sitk.LabelShapeStatisticsImageFilter() + label_shape_filter.Execute(mask) + try: + centroid = label_shape_filter.GetCentroid(255) + except: + centroid = label_shape_filter.GetCentroid(1) + + x, y, z = centroid + + input_dict = { + "image_path": in_data.abspath, + "coordX": x, + "coordY": y, + "coordZ": z, + } + + image = preprocess(input_dict) + image = image.unsqueeze(0) + model = fmcib_model() + + model.eval() + with torch.no_grad(): + features = model(image) + + feature_dict = {f"feature_{idx}": feature for idx, feature in enumerate(features.flatten().tolist())} + + with open(feature_json.abspath, "w") as f: + json.dump(feature_dict, f) diff --git a/models/fmcib/utils/__init__.py b/models/fmcib/utils/__init__.py new file mode 100644 index 00000000..6d0f2d8d --- /dev/null +++ b/models/fmcib/utils/__init__.py @@ -0,0 +1 @@ +from .FMCIBRunner import FMCIBRunner \ No newline at end of file From c864936958be00745ef42e4c9411cec38363255f Mon Sep 17 00:00:00 2001 From: Suraj Pai Date: Tue, 5 Mar 2024 20:09:19 -0500 Subject: [PATCH 02/13] Add meta --- models/fmcib/meta.json | 35 ----- .../config/default.yml | 0 .../dockerfiles/Dockerfile | 10 +- models/fmcib_radiomics/meta.json | 138 ++++++++++++++++++ .../utils/FMCIBRunner.py | 0 .../utils/__init__.py | 0 6 files changed, 145 insertions(+), 38 deletions(-) delete mode 100644 models/fmcib/meta.json rename models/{fmcib => fmcib_radiomics}/config/default.yml (100%) rename models/{fmcib => fmcib_radiomics}/dockerfiles/Dockerfile (67%) create mode 100644 models/fmcib_radiomics/meta.json rename models/{fmcib => fmcib_radiomics}/utils/FMCIBRunner.py (100%) rename models/{fmcib => fmcib_radiomics}/utils/__init__.py (100%) diff --git a/models/fmcib/meta.json b/models/fmcib/meta.json deleted file mode 100644 index 9256e570..00000000 --- a/models/fmcib/meta.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "id": "", - "name": "fmcib", - "title": "Foundation Model for Cancer Imaging Biomarkers", - "summary": { - "description": "This algorithm extracts a 4096 dimensonal feature set for a volume centered on the tumor location", - "inputs": [ - { - - }, - { - - } - ], - "outputs": [ - { - - } - ], - "model": { - "architecture": "Resnet50 (2x wide)", - "training": "weakly-supervised contrastive learning", - "cmpapproach": "3D" - }, - "data": { - "training": { - "vol_samples": 11467 - }, - "evaluation": { - }, - "public": true, - "external": false - } - } -} \ No newline at end of file diff --git a/models/fmcib/config/default.yml b/models/fmcib_radiomics/config/default.yml similarity index 100% rename from models/fmcib/config/default.yml rename to models/fmcib_radiomics/config/default.yml diff --git a/models/fmcib/dockerfiles/Dockerfile b/models/fmcib_radiomics/dockerfiles/Dockerfile similarity index 67% rename from models/fmcib/dockerfiles/Dockerfile rename to models/fmcib_radiomics/dockerfiles/Dockerfile index 9f3d9603..2ef43e85 100644 --- a/models/fmcib/dockerfiles/Dockerfile +++ b/models/fmcib_radiomics/dockerfiles/Dockerfile @@ -2,15 +2,19 @@ FROM mhubai/base:latest LABEL authors="bspai@bwh.harvard.edu" - - +ARG MHUB_MODELS_REPO +# Add pull models repo command here after local testingRUN +RUN buildutils/import_mhub_model.sh fmcib_radiomics ${MHUB_MODELS_REPO} RUN wget https://zenodo.org/records/10528450/files/model_weights.torch?download=1 -O /app/model_weights.torch + RUN mkdir models RUN mkdir models/fmcib # Install FMCIB package, should install everything else ... RUN pip install foundation-cancer-image-biomarker --pre + + ENTRYPOINT ["python3", "-m", "mhubio.run"] -CMD ["--config", "/app/models/fmcib/config/default.yml", "--print"] +CMD ["--workflow", "default"] diff --git a/models/fmcib_radiomics/meta.json b/models/fmcib_radiomics/meta.json new file mode 100644 index 00000000..66f00159 --- /dev/null +++ b/models/fmcib_radiomics/meta.json @@ -0,0 +1,138 @@ +{ + "id": "...", + "name": "fmcib_radiomics", + "title": "Foundation Model for Cancer Imaging Biomarkers", + "summary": { + "description": "A foundation model for cancer imaging biomarker discovery trained through self-supervised learning using a dataset of 11,467 radiographic lesions. The model features can be used as a data-driven substitute for classical radiomic features", + "inputs": [ + { + "label": "Input CT Image", + "description": "CT imaging data containing lesions of interest, such as nodules or tumors", + "format": "DICOM", + "modality": "CT", + "slicethickness": "5mm", + "bodypartexamined": "Whole", + "non-contrast": true, + "contrast": true + }, + { + "label": "Center of mass", + "description": "Center of mass of the lesion in the CT image", + "format": "JSON", + "modality": "JSON", + "slicethickness": "5mm", + "bodypartexamined": "Whole", + "non-contrast": true, + "contrast": true + } + ], + "outputs": [ + { + "type": "Prediction", + "valueType": "Feature vector", + "description": "A set of features extracted from the input CT image", + "label": "Features" + + } + ], + "model": { + "architecture": "3D ResNet50", + "training": "other", + "cmpapproach": "3D" + }, + "data": { + "training": { + "vol_samples": 11467 + }, + "evaluation": { + "vol_samples": 1944 + }, + "public": true, + "external": true + } + }, + "details": { + "name": "Foundation Model for Cancer Imaging Biomarkers", + "version": "0.0.1", + "type": "Feature extractor", + "devteam": "Researchers from the Artificial Intelligence in Medicine (AIM) Program, Mass General Brigham, Harvard Medical School and other institutions", + "date": { + "pub": "2023 (preprint)", + "code": "n/a", + "weights": "18.01.2024" + }, + "cite": "Pai, S., Bontempi, D., Hadzic, I., Prudente, V., et al. Foundation Model for Cancer Imaging Biomarkers. 2023.", + "license": { + "code": "MIT", + "weights": "CC BY-NC 4.0" + }, + "publications": [ + { + "title": "Foundation Model for Cancer Imaging Biomarkers", + "uri": "https://www.medrxiv.org/content/10.1101/2023.09.04.23294952v1" + } + ], + "github": "https://github.com/AIM-Harvard/foundation-cancer-image-biomarker", + "zenodo": "https://zenodo.org/records/10528450", + "colab": "https://colab.research.google.com/drive/1JMtj_4W0uNPzrVnM9EpN1_xpaB-5KC1H?usp=sharing", + "slicer": false + }, + "info": { + "use": { + "title": "Intended Use", + "text": "The foundation model is intended to extract features from several different types of lesions (lung, liver, kidney, mediastinal, abdominal, pelvic, bone and soft tissue). These features can be used for a variety of predictive and clustering tasks as a data-driven substitute for classical radiomic features." + }, + "analyses": { + "title": "Quantitative Analyses", + "text": "The model's performance was assessed using three different downstream tasks, including malignancy prediction and lung cancer risk prediction. Refer to the publication for more details [1].", + "references": [ + { + "label": "Foundation model for cancer image biomarkers", + "uri": "https://www.medrxiv.org/content/10.1101/2023.09.04.23294952v1" + } + ] + }, + "evaluation": { + "title": "Evaluation Data", + "text": "The evaluation dataset consists of 1,944 lesions, including 1,221 lesions for anatomical site classification, 170 nodules for malignancy prediction, and 553 tumors (420 LUNG1 + 133 RADIO) for prognostication. The dataset was held out from the training data and gathered from several different sources [1, 2, 3, 4].", + "tables": [ + { + "label": "Evaluation Tasks & Datasets", + "entries": { + "Lesion Anatomical Site Prediction": "DeepLesion (n=1221)", + "Nodule Malignancy Prediction": "LUNA16 (n=170)", + "Tumor Prognostication": "NSCLC-Radiomics (n=420) + NSCLC-Radiogenomics (n=133)" + } + } + ], + "references": [ + { + "label": "DeepLesion: automated mining of large-scale lesion annotations and universal lesion detection with deep learning.", + "uri": "https://pubmed.ncbi.nlm.nih.gov/30035154/" + }, + { + "label": "LUNA16", + "uri": "https://www.cancerimagingarchive.net/collection/lidc-idri/" + }, + { + "label": "NSCLC-Radiomics", + "uri": "https://www.cancerimagingarchive.net/collection/nsclc-radiomics/" + }, + { + "label": "NSCLC-Radiogenomics", + "uri": "https://www.cancerimagingarchive.net/analysis-result/nsclc-radiogenomics-stanford/" + } + ] + }, + "training": { + "title": "Training Data", + "text": "The training dataset consists of 11467 lesions sourced from 5,513 unique CT scans across 2,312 different patients. This was curated from the DeepLesion dataset [1] following two steps - 1) Lesions that did not contain anatomical labels were selected, 2) Scans with spacing 5mm or more were removed.", + "references": [ + { + "label": "DeepLesion: automated mining of large-scale lesion annotations and universal lesion detection with deep learning.", + "uri": "https://pubmed.ncbi.nlm.nih.gov/30035154/" + } + ] + } + } +} diff --git a/models/fmcib/utils/FMCIBRunner.py b/models/fmcib_radiomics/utils/FMCIBRunner.py similarity index 100% rename from models/fmcib/utils/FMCIBRunner.py rename to models/fmcib_radiomics/utils/FMCIBRunner.py diff --git a/models/fmcib/utils/__init__.py b/models/fmcib_radiomics/utils/__init__.py similarity index 100% rename from models/fmcib/utils/__init__.py rename to models/fmcib_radiomics/utils/__init__.py From 351eff88f36dfb3fdb6c27f7089991eb1368d137 Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Wed, 6 Mar 2024 10:36:56 +0100 Subject: [PATCH 03/13] adding uuid to meta json --- models/fmcib_radiomics/meta.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models/fmcib_radiomics/meta.json b/models/fmcib_radiomics/meta.json index 66f00159..ba3f4087 100644 --- a/models/fmcib_radiomics/meta.json +++ b/models/fmcib_radiomics/meta.json @@ -1,5 +1,5 @@ { - "id": "...", + "id": "26e98e14-b605-4007-bd8b-79d517c935b5", "name": "fmcib_radiomics", "title": "Foundation Model for Cancer Imaging Biomarkers", "summary": { @@ -32,7 +32,6 @@ "valueType": "Feature vector", "description": "A set of features extracted from the input CT image", "label": "Features" - } ], "model": { From 4cf674dcace8c568d95ff5d0cc8f80403baff322 Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Wed, 6 Mar 2024 11:12:27 +0100 Subject: [PATCH 04/13] update Dockerfile, outsource centroid extraction, add coords schema --- models/fmcib_radiomics/dockerfiles/Dockerfile | 21 ++--- .../utils/CentroidExtractor.py | 43 ++++++++++ models/fmcib_radiomics/utils/FMCIBRunner.py | 81 +++++++++++-------- .../fmcib_radiomics/utils/coords.schema.json | 20 +++++ 4 files changed, 123 insertions(+), 42 deletions(-) create mode 100644 models/fmcib_radiomics/utils/CentroidExtractor.py create mode 100644 models/fmcib_radiomics/utils/coords.schema.json diff --git a/models/fmcib_radiomics/dockerfiles/Dockerfile b/models/fmcib_radiomics/dockerfiles/Dockerfile index 2ef43e85..ec4851ab 100644 --- a/models/fmcib_radiomics/dockerfiles/Dockerfile +++ b/models/fmcib_radiomics/dockerfiles/Dockerfile @@ -1,20 +1,23 @@ FROM mhubai/base:latest -LABEL authors="bspai@bwh.harvard.edu" +LABEL authors="bspai@bwh.harvard.edu,lnuernberg@bwh.harvard.edu" -ARG MHUB_MODELS_REPO -# Add pull models repo command here after local testingRUN -RUN buildutils/import_mhub_model.sh fmcib_radiomics ${MHUB_MODELS_REPO} -RUN wget https://zenodo.org/records/10528450/files/model_weights.torch?download=1 -O /app/model_weights.torch +# create +RUN mkdir -p models/fmcib +# download model weights +RUN wget https://zenodo.org/records/10528450/files/model_weights.torch?download=1 -O /app/model_weights.torch -RUN mkdir models -RUN mkdir models/fmcib +# clone mhub implementation +ARG MHUB_MODELS_REPO +RUN buildutils/import_mhub_model.sh fmcib_radiomics ${MHUB_MODELS_REPO} # Install FMCIB package, should install everything else ... RUN pip install foundation-cancer-image-biomarker --pre +# Install additional pip packages +RUN pip3 install --upgrade pip && pip3 install --no-cache-dir \ + jsonschema==4.21.1 - -ENTRYPOINT ["python3", "-m", "mhubio.run"] +ENTRYPOINT ["mhub.run"] CMD ["--workflow", "default"] diff --git a/models/fmcib_radiomics/utils/CentroidExtractor.py b/models/fmcib_radiomics/utils/CentroidExtractor.py new file mode 100644 index 00000000..89d5bf65 --- /dev/null +++ b/models/fmcib_radiomics/utils/CentroidExtractor.py @@ -0,0 +1,43 @@ +""" +--------------------------------------------------------- +Author: Leonard Nürnberg +Email: lnuernberg@bwh.harvard.edu +Date: 06.03.2024 +--------------------------------------------------------- +""" + +import json, jsonschema +from mhubio.core import Instance, InstanceData, IO, Module +import SimpleITK as sitk + +class CentroidExtractor(Module): + + @IO.Instance() + @IO.Input('in_mask', 'nrrd:mod=seg', the='Tumor segmentation mask for the input NRRD file.') + @IO.Output('centroids_json', 'centroids.json', "json:type=fmcibcoordinates", the='JSON file containing 3D coordinates of the centroid of the input mask.') + def task(self, instance: Instance, in_data: InstanceData, in_mask: InstanceData, centroids_json: InstanceData) -> None: + + # read the input mask + mask = sitk.ReadImage(in_mask.abspath) + + # get the center of massk from the mask via ITK + label_shape_filter = sitk.LabelShapeStatisticsImageFilter() + label_shape_filter.Execute(mask) + try: + centroid = label_shape_filter.GetCentroid(255) + except: + centroid = label_shape_filter.GetCentroid(1) + + # extract x, y, and z coordinates from the centroid + x, y, z = centroid + + # set up the coordinate dictionary + coordinate_dict = { + "coordX": x, + "coordY": y, + "coordZ": z, + } + + # write the coordinate dictionary to a json file + with open(centroids_json.abspath, "w") as f: + json.dump(coordinate_dict, f) diff --git a/models/fmcib_radiomics/utils/FMCIBRunner.py b/models/fmcib_radiomics/utils/FMCIBRunner.py index 4e783867..8595e795 100644 --- a/models/fmcib_radiomics/utils/FMCIBRunner.py +++ b/models/fmcib_radiomics/utils/FMCIBRunner.py @@ -5,49 +5,64 @@ --------------------------------------------------------- """ -import json -import torch +import json, jsonschema, os from fmcib.models import fmcib_model import SimpleITK as sitk from mhubio.core import Instance, InstanceData, IO, Module -from fmcib.preprocessing import preprocess +COORDS_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "coords.schema.json") + +def fmcib(input_dict: dict, json_output_file_path: str): + """Run the FCMIB pipeline. + + Args: + input_dict (dict): The input dictionary containing the image path and the seed point coordinates. + json_output_file_path (str): The path were the features are exported to as a json file. + """ + # model dependency imports + import torch + from fmcib.preprocessing import preprocess + + # initialize model + model = fmcib_model() + + # run model preroecessing + image = preprocess(input_dict) + image = image.unsqueeze(0) + + # run model inference + model.eval() + with torch.no_grad(): + features = model(image) + + # generate fearure dictionary + feature_dict = {f"feature_{idx}": feature for idx, feature in enumerate(features.flatten().tolist())} + + # write feature dictionary to json file + with open(json_output_file_path, "w") as f: + json.dump(feature_dict, f) class FMCIBRunner(Module): + @IO.Instance() @IO.Input('in_data', 'nrrd:mod=ct', the='Input NRRD file') - @IO.Input('in_mask', 'nrrd|json', the='Tumor mask for the input NRRD file') - @IO.Output('feature_json', 'features.json', "json", bundle='model', the='output JSON file') - def task(self, instance: Instance, in_data: InstanceData, in_mask: InstanceData, feature_json: InstanceData) -> None: - mask_path = in_mask.abspath - mask = sitk.ReadImage(mask_path) - - # Get the CoM of the mask - label_shape_filter = sitk.LabelShapeStatisticsImageFilter() - label_shape_filter.Execute(mask) - try: - centroid = label_shape_filter.GetCentroid(255) - except: - centroid = label_shape_filter.GetCentroid(1) - - x, y, z = centroid + @IO.Input('centroids_json', 'json:type=fmcibcoordinates', the='The centroids in the input image coordinate space') + @IO.Output('feature_json', 'features.json', "json:type=fmcibfeatures", bundle='model', the='Features extracted from the input image at the specified seed point.') + def task(self, instance: Instance, in_data: InstanceData, centroids_json: InstanceData, feature_json: InstanceData) -> None: + + # read centroids from json file + centroids = json.load(centroids_json.abspath) + # verify input data schema + with open("models/fmcib_radiomics/utils/input_schema.json") as f: + schema = json.load(f) + jsonschema.validate(centroids, schema) + + # define input dictionary input_dict = { "image_path": in_data.abspath, - "coordX": x, - "coordY": y, - "coordZ": z, + **centroids } - image = preprocess(input_dict) - image = image.unsqueeze(0) - model = fmcib_model() - - model.eval() - with torch.no_grad(): - features = model(image) - - feature_dict = {f"feature_{idx}": feature for idx, feature in enumerate(features.flatten().tolist())} - - with open(feature_json.abspath, "w") as f: - json.dump(feature_dict, f) + # run model + fmcib(input_dict, feature_json.abspath) \ No newline at end of file diff --git a/models/fmcib_radiomics/utils/coords.schema.json b/models/fmcib_radiomics/utils/coords.schema.json new file mode 100644 index 00000000..1ee86a00 --- /dev/null +++ b/models/fmcib_radiomics/utils/coords.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "coordX": { + "type": "number" + }, + "coordY": { + "type": "number" + }, + "coordZ": { + "type": "number" + } + }, + "required": [ + "coordX", + "coordY", + "coordZ" + ] +} \ No newline at end of file From fcb7f637fe0e617e2bd8c6aa0771840aa139cc2e Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Wed, 6 Mar 2024 11:12:45 +0100 Subject: [PATCH 05/13] update default workflow and propose alternative workflow --- models/fmcib_radiomics/config/default.yml | 5 +++-- .../fmcib_radiomics/config/from_centroids.yml | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 models/fmcib_radiomics/config/from_centroids.yml diff --git a/models/fmcib_radiomics/config/default.yml b/models/fmcib_radiomics/config/default.yml index 4f91e608..cc4b1559 100644 --- a/models/fmcib_radiomics/config/default.yml +++ b/models/fmcib_radiomics/config/default.yml @@ -5,6 +5,7 @@ general: execute: - FileStructureImporter +- CentroidExtractor - FMCIBRunner - DataOrganizer @@ -12,9 +13,9 @@ modules: FileStructureImporter: structures: - $patientID/CT.nrrd@instance@nrrd:mod=ct - - $patientID/masks/GTV-1.nrrd@nrrd + - $patientID/masks/GTV-1.nrrd@nrrd:mod=seg import_id: patientID DataOrganizer: targets: - - json-->[i:patientID]/features.json \ No newline at end of file + - json:type=fmcibfeatures-->[i:patientID]/features.json \ No newline at end of file diff --git a/models/fmcib_radiomics/config/from_centroids.yml b/models/fmcib_radiomics/config/from_centroids.yml new file mode 100644 index 00000000..462fc8b4 --- /dev/null +++ b/models/fmcib_radiomics/config/from_centroids.yml @@ -0,0 +1,20 @@ +general: + data_base_dir: /app/data + version: 1.0 + description: "FMCIB pipeline starting from a coordinate json file" + +execute: +- FileStructureImporter +- FMCIBRunner +- DataOrganizer + +modules: + FileStructureImporter: + structures: + - $patientID/CT.nrrd@instance@nrrd:mod=ct + - $patientID/centroids.json@json:type=fmcibcoordinates + import_id: patientID + + DataOrganizer: + targets: + - json:type=fmcibfeatures-->[i:patientID]/features.json \ No newline at end of file From 0a255eda4c980d9c333af18f5b84d7014dac1a28 Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Wed, 6 Mar 2024 18:32:39 +0100 Subject: [PATCH 06/13] add automatic support for various json schemas --- models/fmcib_radiomics/utils/FMCIBRunner.py | 63 +- .../utils/slicermarkup.schema.json | 699 ++++++++++++++++++ 2 files changed, 753 insertions(+), 9 deletions(-) create mode 100644 models/fmcib_radiomics/utils/slicermarkup.schema.json diff --git a/models/fmcib_radiomics/utils/FMCIBRunner.py b/models/fmcib_radiomics/utils/FMCIBRunner.py index 8595e795..0c28386f 100644 --- a/models/fmcib_radiomics/utils/FMCIBRunner.py +++ b/models/fmcib_radiomics/utils/FMCIBRunner.py @@ -9,8 +9,58 @@ from fmcib.models import fmcib_model import SimpleITK as sitk from mhubio.core import Instance, InstanceData, IO, Module +from enum import Enum +from typing import Optional COORDS_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "coords.schema.json") +SLICERMARKUP_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "slicermarkup.schema.json") + +def is_valid(json_data: dict, schema_file_path: str) -> bool: + """Check if a json file is valid according to a given schema. + + Args: + json_data (dict): The json data to be validated. + schema_file_path (str): The path to the schema file. + + Returns: + bool: True if the json file is valid according to the schema, False otherwise. + """ + with open(schema_file_path) as f: + schema = json.load(f) + + try: + jsonschema.validate(json_data, schema) + return True + except: + return False + +def get_coordinates(json_file_path: str) -> dict: + + # read json file + with open(json_file_path) as f: + json_data = json.load(f) + + # check which schema the json file adheres to + if is_valid(json_data, COORDS_SCHEMA_PATH): + return json_data + + if is_valid(json_data, SLICERMARKUP_SCHEMA_PATH): + markups = json_data["markups"] + assert markups["coordinateSystem"] == "LPS" + + controlPoints = markups["controlPoints"] + assert len(controlPoints) == 1 + + position = controlPoints[0]["position"] + return { + "coordX": position[0], + "coordY": position[1], + "coordZ": position[2] + } + + # + raise ValueError("The input json file does not adhere to the expected schema.") + def fmcib(input_dict: dict, json_output_file_path: str): """Run the FCMIB pipeline. @@ -46,22 +96,17 @@ class FMCIBRunner(Module): @IO.Instance() @IO.Input('in_data', 'nrrd:mod=ct', the='Input NRRD file') - @IO.Input('centroids_json', 'json:type=fmcibcoordinates', the='The centroids in the input image coordinate space') + @IO.Input('coordinates_json', 'json:type=fmcibcoordinates', the='The coordinates of the 3D seed point in the input image') @IO.Output('feature_json', 'features.json', "json:type=fmcibfeatures", bundle='model', the='Features extracted from the input image at the specified seed point.') - def task(self, instance: Instance, in_data: InstanceData, centroids_json: InstanceData, feature_json: InstanceData) -> None: + def task(self, instance: Instance, in_data: InstanceData, coordinates_json: InstanceData, feature_json: InstanceData) -> None: # read centroids from json file - centroids = json.load(centroids_json.abspath) - - # verify input data schema - with open("models/fmcib_radiomics/utils/input_schema.json") as f: - schema = json.load(f) - jsonschema.validate(centroids, schema) + coordinates = get_coordinates(coordinates_json.abspath) # define input dictionary input_dict = { "image_path": in_data.abspath, - **centroids + **coordinates } # run model diff --git a/models/fmcib_radiomics/utils/slicermarkup.schema.json b/models/fmcib_radiomics/utils/slicermarkup.schema.json new file mode 100644 index 00000000..3ca04d45 --- /dev/null +++ b/models/fmcib_radiomics/utils/slicermarkup.schema.json @@ -0,0 +1,699 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://raw.githubusercontent.com/Slicer/Slicer/main/Modules/Loadable/Markups/Resources/Schema/markups-v1.0.3-schema.json#", + "type": "object", + "title": "Schema for storing one or more markups", + "description": "Stores points, lines, curves, etc.", + "required": ["@schema", "markups"], + "additionalProperties": true, + "properties": { + "@schema": { + "$id": "#schema", + "type": "string", + "title": "Schema", + "description": "URL of versioned schema." + }, + "markups": { + "$id": "#markups", + "type": "array", + "title": "Markups", + "description": "Stores position and display properties of one or more markups.", + "additionalItems": true, + "items": { + "$id": "#markupItems", + "anyOf": [ + { + "$id": "#markup", + "type": "object", + "title": "Markup", + "description": "Stores a single markup.", + "default": {}, + "required": ["type"], + "additionalProperties": true, + "properties": { + "type": { + "$id": "#markup/type", + "type": "string", + "title": "Basic type", + "enum": ["Fiducial", "Line", "Angle", "Curve", "ClosedCurve", "Plane", "ROI"] + }, + "name": { + "$id": "#markup/name", + "type": "string", + "title": "Name", + "description": "Displayed name of the markup.", + "default": "" + }, + "coordinateSystem": { + "$id": "#markup/coordinateSystem", + "type": "string", + "title": "Control point positions coordinate system name", + "description": "Coordinate system name. Medical images most commonly use LPS patient coordinate system.", + "default": "LPS", + "enum": ["LPS", "RAS"] + }, + "coordinateUnits": { + "$id": "#markup/coordinateUnits", + "anyOf": [ + { + "type": "string", + "title": "Units of control point coordinates", + "description": "Control point coordinate values are specified in this length unit. Specified in UCUM.", + "default": "mm", + "enum": ["mm", "um"] + }, + { + "type": "array", + "title": "Coordinates units code", + "description": "Standard DICOM-compliant terminology item containing code, coding scheme designator, code meaning.", + "examples": [["mm", "UCUM", "millimeter"]], + "additionalItems": false, + "items": { "type": "string" }, + "minItems": 3, + "maxItems": 3 + } + ] + }, + "locked": { + "$id": "#markup/locked", + "type": "boolean", + "title": "Locked", + "description": "Markup can be interacted with on the user interface.", + "default": true + }, + "fixedNumberOfControlPoints": { + "$id": "#markup/fixedNumberOfControlPoints", + "type": "boolean", + "title": "Fixed number of control points", + "description": "Number of control points is fixed at the current value. Control points may not be added or removed (point positions can be unset instead of deleting).", + "default": false + }, + "labelFormat": { + "$id": "#markup/labelFormat", + "type": "string", + "title": "Label format", + "description": "Format of generation new labels. %N refers to node name, %d refers to point index.", + "default": "%N-%d" + }, + "lastUsedControlPointNumber": { + "$id": "#markup/lastUsedControlPointNumber", + "type": "integer", + "title": "Last used control point number", + "description": "This value is used for generating number in the control point's name when a new point is added.", + "default": 0 + }, + "roiType": { + "$id": "#markup/roiType", + "type": "string", + "title": "ROI type", + "description": "Method used to determine ROI bounds from control points. Ex. 'Box', 'BoundingBox'.", + "default": "Box" + }, + "insideOut": { + "$id": "#markup/insideOut", + "type": "boolean", + "title": "Inside out", + "description": "ROI is inside out. Objects that would normally be inside are considered outside and vice versa.", + "default": false + }, + "planeType": { + "$id": "#markup/planeType", + "type": "string", + "title": "Plane type", + "description": "Method used to determine dimensions from control points. Ex. 'PointNormal', '3Points'.", + "default": "PointNormal" + }, + "sizeMode": { + "$id": "#markup/sizeMode", + "type": "string", + "title": "Plane size mode", + "description": "Mode used to calculate the size of the plane representation. (Ex. Static absolute or automatically calculated plane size based on control points).", + "default": "auto" + }, + "autoScalingSizeFactor": { + "$id": "#markup/autoScalingSizeFactor", + "type": "number", + "title": "Plane auto scaling size factor", + "description": "When the plane size mode is 'auto', the size of the plane is scaled by the auto size scaling factor.", + "default": "1.0" + }, + "center": { + "$id": "#markup/center", + "type": "array", + "title": "Center", + "description": "The center of the markups representation. Ex. center of ROI or plane markups.", + "examples": [[0.0, 0.0, 0.0]], + "additionalItems": false, + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + }, + "normal": { + "$id": "#markup/normal", + "type": "array", + "title": "Normal", + "description": "The normal direction of plane markups.", + "examples": [[0.0, 0.0, 1.0]], + "additionalItems": false, + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + }, + "size": { + "$id": "#markup/size", + "type": "array", + "title": "Size", + "description": "The size of the markups representation. For example, axis-aligned edge lengths of the ROI or plane markups.", + "examples": [[5.0, 5.0, 4.0], [5.0, 5.0, 0.0]], + "additionalItems": false, + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + }, + "planeBounds": { + "$id": "#markup/planeBounds", + "type": "array", + "title": "Plane bounds", + "description": "The bounds of the plane representation.", + "examples": [[-50, 50, -50, 50]], + "additionalItems": false, + "items": { "type": "number" }, + "minItems": 4, + "maxItems": 4 + }, + "objectToBase": { + "$id": "#markup/objectToBase", + "type": "array", + "title": "Object to Base matrix", + "description": "4x4 transform matrix from the object representation to the coordinate system defined by the control points.", + "examples": [[-0.9744254538021788, -0.15660098593235834, -0.16115572030626558, 26.459385388492746, + -0.08525118065879463, -0.4059244688892957, 0.9099217338613386, -48.04154530201596, + -0.20791169081775938, 0.9003896138683279, 0.3821927158637956, -53.35829266424462, + 0.0, 0.0, 0.0, 1.0]], + "additionalItems": false, + "items": { "type": "number" }, + "minItems": 16, + "maxItems": 16 + }, + "baseToNode": { + "$id": "#markup/baseToNode", + "type": "array", + "title": "Base to Node matrix", + "description": "4x4 transform matrix from the base representation to the node coordinate system.", + "examples": [[-0.9744254538021788, -0.15660098593235834, -0.16115572030626558, 26.459385388492746, + -0.08525118065879463, -0.4059244688892957, 0.9099217338613386, -48.04154530201596, + -0.20791169081775938, 0.9003896138683279, 0.3821927158637956, -53.35829266424462, + 0.0, 0.0, 0.0, 1.0]], + "additionalItems": false, + "items": { "type": "number" }, + "minItems": 16, + "maxItems": 16 + }, + "orientation": { + "$id": "#markup/orientation", + "type": "array", + "title": "Markups orientation", + "description": "3x3 orientation matrix of the markups representation. Ex. [orientation[0], orientation[3], orientation[6]] is the x vector of the object coordinate system in the node coordinate system.", + "examples": [[-0.6157905804369491, -0.3641498920623639, 0.6987108251316091, + -0.7414677108739087, -0.03213048377225371, -0.6702188193000602, + 0.2665100275346712, -0.9307859518297049, -0.2502197376306259]], + "additionalItems": false, + "items": { "type": "number" }, + "minItems": 9, + "maxItems": 9 + }, + "controlPoints": { + "$id": "#markup/controlPoints", + "type": "array", + "title": "Control points", + "description": "Stores all control points of this markup.", + "default": [], + "additionalItems": true, + "items": { + "$id": "#markup/controlPointItems", + "anyOf": [ + { + "$id": "#markup/controlPoint", + "type": "object", + "title": "The first anyOf schema", + "description": "Object containing the properties of a single control point.", + "default": {}, + "required": [], + "additionalProperties": true, + "properties": { + "id": { + "$id": "#markup/controlPoint/id", + "type": "string", + "title": "Control point ID", + "description": "Identifier of the control point within this markup", + "default": "", + "examples": ["2", "5"] + }, + "label": { + "$id": "#markup/controlPoint/label", + "type": "string", + "title": "Control point label", + "description": "Label displayed next to the control point.", + "default": "", + "examples": ["F_1"] + }, + "description": { + "$id": "#markup/controlPoint/description", + "type": "string", + "title": "Control point description", + "description": "Details about the control point.", + "default": "" + }, + "associatedNodeID": { + "$id": "#markup/controlPoint/associatedNodeID", + "type": "string", + "title": "Associated node ID", + "description": "ID of the node where this markups is defined on.", + "default": "", + "examples": ["vtkMRMLModelNode1"] + }, + "position": { + "$id": "#markup/controlPoint/position", + "type": "array", + "title": "Control point position", + "description": "Tuple of 3 defined in the specified coordinate system.", + "examples": [[-9.9, 1.1, 12.3]], + "additionalItems": false, + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + }, + "orientation": { + "$id": "#markup/controlPoint/orientation", + "type": "array", + "title": "Control point orientation", + "description": "3x3 orientation matrix", + "examples": [[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ]], + "additionalItems": false, + "items": {"type": "number"}, + "minItems": 9, + "maxItems": 9 + }, + "selected": { + "$id": "#markup/controlPoint/selected", + "type": "boolean", + "title": "Control point is selected", + "description": "Specifies if the control point is selected or unselected.", + "default": true + }, + "locked": { + "$id": "#markup/controlPoint/locked", + "type": "boolean", + "title": "Control point locked", + "description": "Control point cannot be moved on the user interface.", + "default": false + }, + "visibility": { + "$id": "#markup/controlPoint/visibility", + "type": "boolean", + "title": "The visibility schema", + "description": "Visibility of the control point.", + "default": true + }, + "positionStatus": { + "$id": "#markup/controlPoint/positionStatus", + "type": "string", + "title": "The positionStatus schema", + "description": "Status of the control point position.", + "enum": ["undefined", "preview", "defined"], + "default": "defined" + } + } + } + ] + } + }, + "display": { + "$id": "#display", + "type": "object", + "title": "The display schema", + "description": "Object holding markups display properties.", + "default": {}, + "required": [], + "additionalProperties": true, + "properties": { + "visibility": { + "$id": "#display/visibility", + "type": "boolean", + "title": "Markup visibility", + "description": "Visibility of the entire markup.", + "default": true + }, + "opacity": { + "$id": "#display/opacity", + "type": "number", + "title": "Markup opacity", + "description": "Overall opacity of the markup.", + "minimum": 0.0, + "maximum": 1.0, + "default": 1.0 + }, + "color": { + "$id": "#display/color", + "type": "array", + "title": "Markup color", + "description": "Overall RGB color of the markup.", + "default": [0.4, 1.0, 1.0], + "additionalItems": false, + "items": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "minItems": 3, + "maxItems": 3 + }, + "selectedColor": { + "$id": "#display/selectedColor", + "title": "Markup selected color", + "description": "Overall RGB color of selected points in the markup.", + "default": [1.0, 0.5, 0.5], + "additionalItems": false, + "items": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "minItems": 3, + "maxItems": 3 + }, + "activeColor": { + "$id": "#display/activeColor", + "title": "Markup active color", + "description": "Overall RGB color of active points in the markup.", + "default": [0.4, 1.0, 0.0], + "additionalItems": false, + "items": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "minItems": 3, + "maxItems": 3 + }, + "propertiesLabelVisibility": { + "$id": "#display/propertiesLabelVisibility", + "type": "boolean", + "title": "Properties label visibility", + "description": "Visibility of the label that shows basic properties.", + "default": false + }, + "pointLabelsVisibility": { + "$id": "#display/pointLabelsVisibility", + "type": "boolean", + "title": "Point labels visibility", + "description": "Visibility of control point labels.", + "default": false + }, + "textScale": { + "$id": "#display/textScale", + "type": "number", + "title": "Markup overall text scale", + "description": "Size of displayed text as percentage of window size.", + "default": 3.0, + "minimum": 0.0 + }, + "glyphType": { + "$id": "#display/glyphType", + "type": "string", + "title": "The glyphType schema", + "description": "Enum representing the displayed glyph type.", + "default": "Sphere3D", + "enum": ["Vertex2D", "Dash2D", "Cross2D", "ThickCross2D", "Triangle2D", "Square2D", + "Circle2D", "Diamond2D", "Arrow2D", "ThickArrow2D", "HookedArrow2D", "StarBurst2D", + "Sphere3D", "Diamond3D"] + }, + "glyphScale": { + "$id": "#display/glyphScale", + "type": "number", + "title": "Point glyph scale", + "description": "Glyph size as percentage of window size.", + "default": 1.0, + "minimum": 0.0 + }, + "glyphSize": { + "$id": "#display/glyphSize", + "type": "number", + "title": "Point glyph size", + "description": "Absolute glyph size.", + "default": 5.0, + "minimum": 0.0 + }, + "useGlyphScale": { + "$id": "#display/useGlyphScale", + "type": "boolean", + "title": "Use glyph scale", + "description": "Use relative glyph scale.", + "default": true + }, + "sliceProjection": { + "$id": "#display/sliceProjection", + "type": "boolean", + "title": "Slice projection", + "description": "Enable project markups to slice views.", + "default": false + }, + "sliceProjectionUseFiducialColor": { + "$id": "#display/sliceProjectionUseFiducialColor", + "type": "boolean", + "title": "Use fiducial color for slice projection", + "description": "Choose between projection color or fiducial color for projections.", + "default": true + }, + "sliceProjectionOutlinedBehindSlicePlane": { + "$id": "#display/sliceProjectionOutlinedBehindSlicePlane", + "type": "boolean", + "title": "Display slice projection as outline", + "description": "Display slice projection as outline if behind slice plane.", + "default": false + }, + "sliceProjectionColor": { + "$id": "#display/sliceProjectionColor", + "type": "array", + "title": "Slice projection color", + "description": "Overall RGB color for displaying projection.", + "default": [1.0, 1.0, 1.0], + "additionalItems": false, + "items": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "minItems": 3, + "maxItems": 3 + }, + "sliceProjectionOpacity": { + "$id": "#display/sliceProjectionOpacity", + "type": "number", + "title": "Slice projection opacity", + "description": "Overall opacity of markup slice projection.", + "minimum": 0.0, + "maximum": 1.0, + "default": 0.6 + }, + "lineThickness": { + "$id": "#display/lineThickness", + "type": "number", + "title": "Line thickness", + "description": "Line thickness relative to markup size.", + "default": 0.2, + "minimum": 0.0 + }, + "lineColorFadingStart": { + "$id": "#display/lineColorFadingStart", + "type": "number", + "title": "Line color fading start", + "description": "Distance where line starts to fade out.", + "default": 1.0, + "minimum": 0.0 + }, + "lineColorFadingEnd": { + "$id": "#display/lineColorFadingEnd", + "type": "number", + "title": "Line color fading end", + "description": "Distance where line fades out completely.", + "default": 10.0, + "minimum": 0.0 + }, + "lineColorFadingSaturation": { + "$id": "#display/lineColorFadingSaturation", + "type": "number", + "title": "Color fading saturation", + "description": "Amount of color saturation change as the line fades out.", + "default": 1.0 + }, + "lineColorFadingHueOffset": { + "$id": "#display/lineColorFadingHueOffset", + "type": "number", + "title": "Color fadue hue offset", + "description": "Change in color hue as the line fades out.", + "default": 0.0 + }, + "handlesInteractive": { + "$id": "#display/handlesInteractive", + "type": "boolean", + "title": "Handles interactive", + "description": "Show interactive handles to transform this markup.", + "default": false + }, + "translationHandleVisibility": { + "$id": "#display/translationHandleVisibility", + "type": "boolean", + "title": "Translation handle visibility", + "description": "Visibility of the translation interaction handles", + "default": false + }, + "rotationHandleVisibility": { + "$id": "#display/rotationHandleVisibility", + "type": "boolean", + "title": "Rotation handle visibility", + "description": "Visibility of the rotation interaction handles", + "default": false + }, + "scaleHandleVisibility": { + "$id": "#display/scaleHandleVisibility", + "type": "boolean", + "title": "Scale handle visibility", + "description": "Visibility of the scale interaction handles", + "default": false + }, + "interactionHandleScale": { + "$id": "#display/interactionHandleScale", + "type": "number", + "title": "Interaction handle glyph scale", + "description": "Interaction handle size as percentage of window size.", + "default": 3.0 + }, + "snapMode": { + "$id": "#display/snapMode", + "type": "string", + "title": "Snap mode", + "description": "How control points can be defined and moved.", + "default": "toVisibleSurface", + "enum": ["unconstrained", "toVisibleSurface"] + } + } + }, + "measurements": { + "$id": "#markup/measurements", + "type": "array", + "title": "Measurements", + "description": "Stores all measurements for this markup.", + "default": [], + "additionalItems": true, + "items": { + "$id": "#markup/measurementItems", + "anyOf": [ + { + "$id": "#markup/measurement", + "type": "object", + "title": "Measurement", + "description": "Store a single measurement.", + "default": {}, + "required": [], + "additionalProperties": true, + "properties": { + "name": { + "$id": "#markup/measurement/name", + "type": "string", + "title": "Measurement name", + "description": "Printable name of the measurement", + "default": "", + "examples": ["length", "area"] + }, + "enabled": { + "$id": "#markup/measurement/enabled", + "type": "boolean", + "title": "Computation of the measurement is enabled", + "description": "This can be used to define measurements but prevent automatic updates.", + "default": true + }, + "value": { + "$id": "#display/measurement/value", + "type": "number", + "title": "Measurement value", + "description": "Numeric value of the measurement." + }, + "units": { + "$id": "#markup/measurement/units", + "anyOf": [ + { + "type": "string", + "title": "Measurement unit", + "description": "Printable measurement unit. Use of UCUM is preferred.", + "default": "", + "examples": ["mm", "mm2"] + }, + { + "type": "array", + "title": "Measurement units code", + "description": "Standard DICOM-compliant terminology item containing code, coding scheme designator, code meaning.", + "examples": [["cm3", "UCUM", "cubic centimeter"]], + "additionalItems": false, + "items": { "type": "string" }, + "minItems": 3, + "maxItems": 3 + } + ] + }, + "description": { + "$id": "#markup/measurement/description", + "type": "string", + "title": "Measurement description", + "description": "Explanation of the measurement.", + "default": "" + }, + "printFormat": { + "$id": "#markup/measurement/printFormat", + "type": "string", + "title": "Print format", + "description": "Format string (printf-style) to create user-displayable string from value and units.", + "default": "", + "examples": ["%5.3f %s"] + }, + "quantityCode": { + "$id": "#markup/measurement/quantityCode", + "type": "array", + "title": "Measurement quantity code", + "description": "Standard DICOM-compliant terminology item containing code, coding scheme designator, code meaning.", + "default": [], + "examples": [["118565006", "SCT", "Volume"]], + "additionalItems": false, + "items": { "type": "string" }, + "minItems": 3, + "maxItems": 3 + }, + "derivationCode": { + "$id": "#markup/measurement/derivationCode", + "type": "array", + "title": "Measurement derivation code", + "description": "Standard DICOM-compliant terminology item containing code, coding scheme designator, code meaning.", + "default": [], + "examples": [["255605001", "SCT", "Minimum"]], + "additionalItems": false, + "items": { "type": "string" }, + "minItems": 3, + "maxItems": 3 + }, + "methodCode": { + "$id": "#markup/measurement/methodCode", + "type": "array", + "title": "Measurement method code", + "description": "Standard DICOM-compliant terminology item containing code, coding scheme designator, code meaning.", + "default": [], + "examples": [["126030", "DCM", "Sum of segmented voxel volumes"]], + "additionalItems": false, + "items": { "type": "string" }, + "minItems": 3, + "maxItems": 3 + }, + "controlPointValues": { + "$id": "#markup/controlPoint/controlPointValues", + "type": "array", + "title": "Measurement values for each control point.", + "description": "This stores measurement result if it has value for each control point.", + "examples": [[-9.9, 1.1, 12.3, 4.3, 4.8]], + "additionalItems": false, + "items": { "type": "number" } + } + } + } + ] + } + } + } + } + ] + } + } + } +} From df1d8ba69badc7b38e793e76ff3ade19fd9b4002 Mon Sep 17 00:00:00 2001 From: Suraj Pai Date: Thu, 7 Mar 2024 01:40:31 -0500 Subject: [PATCH 07/13] Tested default workflow --- models/fmcib_radiomics/dockerfiles/Dockerfile | 13 +++++++------ .../fmcib_radiomics/utils/CentroidExtractor.py | 2 +- models/fmcib_radiomics/utils/FMCIBRunner.py | 17 +++++++++-------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/models/fmcib_radiomics/dockerfiles/Dockerfile b/models/fmcib_radiomics/dockerfiles/Dockerfile index ec4851ab..1c9716ed 100644 --- a/models/fmcib_radiomics/dockerfiles/Dockerfile +++ b/models/fmcib_radiomics/dockerfiles/Dockerfile @@ -2,9 +2,6 @@ FROM mhubai/base:latest LABEL authors="bspai@bwh.harvard.edu,lnuernberg@bwh.harvard.edu" -# create -RUN mkdir -p models/fmcib - # download model weights RUN wget https://zenodo.org/records/10528450/files/model_weights.torch?download=1 -O /app/model_weights.torch @@ -12,12 +9,16 @@ RUN wget https://zenodo.org/records/10528450/files/model_weights.torch?download= ARG MHUB_MODELS_REPO RUN buildutils/import_mhub_model.sh fmcib_radiomics ${MHUB_MODELS_REPO} -# Install FMCIB package, should install everything else ... -RUN pip install foundation-cancer-image-biomarker --pre # Install additional pip packages RUN pip3 install --upgrade pip && pip3 install --no-cache-dir \ jsonschema==4.21.1 +# Install FMCIB package, should install everything else ... +RUN pip3 install foundation-cancer-image-biomarker --pre + +# Fix for mpmath torch bug: https://github.com/underworldcode/underworld3/issues/167 +RUN pip3 install mpmath==1.3.0 + ENTRYPOINT ["mhub.run"] -CMD ["--workflow", "default"] +CMD ["--workflow", "default", "--print"] diff --git a/models/fmcib_radiomics/utils/CentroidExtractor.py b/models/fmcib_radiomics/utils/CentroidExtractor.py index 89d5bf65..1e5154cb 100644 --- a/models/fmcib_radiomics/utils/CentroidExtractor.py +++ b/models/fmcib_radiomics/utils/CentroidExtractor.py @@ -15,7 +15,7 @@ class CentroidExtractor(Module): @IO.Instance() @IO.Input('in_mask', 'nrrd:mod=seg', the='Tumor segmentation mask for the input NRRD file.') @IO.Output('centroids_json', 'centroids.json', "json:type=fmcibcoordinates", the='JSON file containing 3D coordinates of the centroid of the input mask.') - def task(self, instance: Instance, in_data: InstanceData, in_mask: InstanceData, centroids_json: InstanceData) -> None: + def task(self, instance: Instance, in_mask: InstanceData, centroids_json: InstanceData) -> None: # read the input mask mask = sitk.ReadImage(in_mask.abspath) diff --git a/models/fmcib_radiomics/utils/FMCIBRunner.py b/models/fmcib_radiomics/utils/FMCIBRunner.py index 0c28386f..f3273156 100644 --- a/models/fmcib_radiomics/utils/FMCIBRunner.py +++ b/models/fmcib_radiomics/utils/FMCIBRunner.py @@ -1,12 +1,11 @@ """ --------------------------------------------------------- -Author: Suraj Pia -Email: bspai@bwh.harvard.edu +Author: Suraj Pai, Leonard Nürnberg +Email: bspai@bwh.harvard.edu, lnuernberg@bwh.harvard.edu +Date: 06.03.2024 --------------------------------------------------------- """ - import json, jsonschema, os -from fmcib.models import fmcib_model import SimpleITK as sitk from mhubio.core import Instance, InstanceData, IO, Module from enum import Enum @@ -71,9 +70,10 @@ def fmcib(input_dict: dict, json_output_file_path: str): """ # model dependency imports import torch + from fmcib.models import fmcib_model from fmcib.preprocessing import preprocess - # initialize model + # initialize the ResNet50 model with pretrained weights model = fmcib_model() # run model preroecessing @@ -96,12 +96,12 @@ class FMCIBRunner(Module): @IO.Instance() @IO.Input('in_data', 'nrrd:mod=ct', the='Input NRRD file') - @IO.Input('coordinates_json', 'json:type=fmcibcoordinates', the='The coordinates of the 3D seed point in the input image') + @IO.Input('centroids_json', "json:type=fmcibcoordinates", the='JSON file containing 3D coordinates of the centroid of the input mask.') @IO.Output('feature_json', 'features.json', "json:type=fmcibfeatures", bundle='model', the='Features extracted from the input image at the specified seed point.') - def task(self, instance: Instance, in_data: InstanceData, coordinates_json: InstanceData, feature_json: InstanceData) -> None: + def task(self, instance: Instance, in_data: InstanceData, centroids_json: InstanceData, feature_json: InstanceData) -> None: # read centroids from json file - coordinates = get_coordinates(coordinates_json.abspath) + coordinates = get_coordinates(centroids_json.abspath) # define input dictionary input_dict = { @@ -109,5 +109,6 @@ def task(self, instance: Instance, in_data: InstanceData, coordinates_json: Inst **coordinates } + # run model fmcib(input_dict, feature_json.abspath) \ No newline at end of file From a50b34521400cfd03c76aa802a7876a288a65dd6 Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Thu, 7 Mar 2024 08:26:05 +0100 Subject: [PATCH 08/13] remove outdated imports --- models/fmcib_radiomics/utils/FMCIBRunner.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/models/fmcib_radiomics/utils/FMCIBRunner.py b/models/fmcib_radiomics/utils/FMCIBRunner.py index 0c28386f..c02ca429 100644 --- a/models/fmcib_radiomics/utils/FMCIBRunner.py +++ b/models/fmcib_radiomics/utils/FMCIBRunner.py @@ -7,10 +7,7 @@ import json, jsonschema, os from fmcib.models import fmcib_model -import SimpleITK as sitk from mhubio.core import Instance, InstanceData, IO, Module -from enum import Enum -from typing import Optional COORDS_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "coords.schema.json") SLICERMARKUP_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "slicermarkup.schema.json") From 21206b4540845b4abce62a2c1f269296322397df Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Thu, 7 Mar 2024 08:30:12 +0100 Subject: [PATCH 09/13] remove outdated fileglobal imports --- models/fmcib_radiomics/utils/FMCIBRunner.py | 1 - 1 file changed, 1 deletion(-) diff --git a/models/fmcib_radiomics/utils/FMCIBRunner.py b/models/fmcib_radiomics/utils/FMCIBRunner.py index 64409369..a930c04c 100644 --- a/models/fmcib_radiomics/utils/FMCIBRunner.py +++ b/models/fmcib_radiomics/utils/FMCIBRunner.py @@ -6,7 +6,6 @@ --------------------------------------------------------- """ import json, jsonschema, os -from fmcib.models import fmcib_model from mhubio.core import Instance, InstanceData, IO, Module COORDS_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "coords.schema.json") From 4e1525cf0d6abafd35985e15b843736be70487e7 Mon Sep 17 00:00:00 2001 From: Suraj Pai Date: Thu, 7 Mar 2024 02:43:47 -0500 Subject: [PATCH 10/13] Test slicer config --- models/fmcib_radiomics/config/from_slicer.yml | 20 +++++++++++++++++++ models/fmcib_radiomics/dockerfiles/Dockerfile | 5 +---- models/fmcib_radiomics/utils/FMCIBRunner.py | 8 ++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 models/fmcib_radiomics/config/from_slicer.yml diff --git a/models/fmcib_radiomics/config/from_slicer.yml b/models/fmcib_radiomics/config/from_slicer.yml new file mode 100644 index 00000000..1c5682a9 --- /dev/null +++ b/models/fmcib_radiomics/config/from_slicer.yml @@ -0,0 +1,20 @@ +general: + data_base_dir: /app/data + version: 1.0 + description: "FMCIB pipeline" + +execute: +- FileStructureImporter +- FMCIBRunner +- DataOrganizer + +modules: + FileStructureImporter: + structures: + - $patientID@instance/re:^.*\.nrrd$::@nrrd:mod=ct + - $patientID/re:^.*\.json$::@json:type=fmcibcoordinates + import_id: patientID + + DataOrganizer: + targets: + - json:type=fmcibfeatures-->[i:patientID]/features.json \ No newline at end of file diff --git a/models/fmcib_radiomics/dockerfiles/Dockerfile b/models/fmcib_radiomics/dockerfiles/Dockerfile index 1c9716ed..54059428 100644 --- a/models/fmcib_radiomics/dockerfiles/Dockerfile +++ b/models/fmcib_radiomics/dockerfiles/Dockerfile @@ -17,8 +17,5 @@ RUN pip3 install --upgrade pip && pip3 install --no-cache-dir \ # Install FMCIB package, should install everything else ... RUN pip3 install foundation-cancer-image-biomarker --pre -# Fix for mpmath torch bug: https://github.com/underworldcode/underworld3/issues/167 -RUN pip3 install mpmath==1.3.0 - ENTRYPOINT ["mhub.run"] -CMD ["--workflow", "default", "--print"] +CMD ["--workflow", "default"] diff --git a/models/fmcib_radiomics/utils/FMCIBRunner.py b/models/fmcib_radiomics/utils/FMCIBRunner.py index f3273156..70310e81 100644 --- a/models/fmcib_radiomics/utils/FMCIBRunner.py +++ b/models/fmcib_radiomics/utils/FMCIBRunner.py @@ -45,9 +45,13 @@ def get_coordinates(json_file_path: str) -> dict: if is_valid(json_data, SLICERMARKUP_SCHEMA_PATH): markups = json_data["markups"] - assert markups["coordinateSystem"] == "LPS" + + assert len(markups) == 1, "Currently, only one point per file is supported." + markup = markups[0] + + assert markup["coordinateSystem"] == "LPS" - controlPoints = markups["controlPoints"] + controlPoints = markup["controlPoints"] assert len(controlPoints) == 1 position = controlPoints[0]["position"] From 6243b98582ad55ce511f622b04a4e4e9b105b677 Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Wed, 13 Mar 2024 18:38:10 +0100 Subject: [PATCH 11/13] minor updates on runner module --- models/fmcib_radiomics/utils/FMCIBRunner.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models/fmcib_radiomics/utils/FMCIBRunner.py b/models/fmcib_radiomics/utils/FMCIBRunner.py index faf2e23f..0729413d 100644 --- a/models/fmcib_radiomics/utils/FMCIBRunner.py +++ b/models/fmcib_radiomics/utils/FMCIBRunner.py @@ -61,7 +61,6 @@ def get_coordinates(json_file_path: str) -> dict: # raise ValueError("The input json file does not adhere to the expected schema.") - def fmcib(input_dict: dict, json_output_file_path: str): """Run the FCMIB pipeline. @@ -96,7 +95,7 @@ def fmcib(input_dict: dict, json_output_file_path: str): class FMCIBRunner(Module): @IO.Instance() - @IO.Input('in_data', 'nrrd:mod=ct', the='Input NRRD file') + @IO.Input('in_data', 'nrrd|nifti:mod=ct', the='Input nrrd or nifti ct image file') @IO.Input('centroids_json', "json:type=fmcibcoordinates", the='JSON file containing 3D coordinates of the centroid of the input mask.') @IO.Output('feature_json', 'features.json', "json:type=fmcibfeatures", bundle='model', the='Features extracted from the input image at the specified seed point.') def task(self, instance: Instance, in_data: InstanceData, centroids_json: InstanceData, feature_json: InstanceData) -> None: From a184cfe98b676e95261c68dd1c23c011b1ad0c0f Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Wed, 13 Mar 2024 18:38:21 +0100 Subject: [PATCH 12/13] add dicom workflow --- models/fmcib_radiomics/config/dicom.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 models/fmcib_radiomics/config/dicom.yml diff --git a/models/fmcib_radiomics/config/dicom.yml b/models/fmcib_radiomics/config/dicom.yml new file mode 100644 index 00000000..da7dc513 --- /dev/null +++ b/models/fmcib_radiomics/config/dicom.yml @@ -0,0 +1,22 @@ +general: + data_base_dir: /app/data + version: 1.0 + description: FMCIB pipeline starting from DICOM files and centroids in json files named by their SeriesInstanceUID + +execute: +- DicomImporter +- FileImporter +- NiftiConverter +- FMCIBRunner +- DataOrganizer + +modules: + + FileImporter: + instance_id: sid + meta: type=fmcibcoordinates + type: json + + DataOrganizer: + targets: + - json:type=fmcibfeatures-->[i:sid]/features.json \ No newline at end of file From 94d46a8eff8045acf8bffb10fc6785d80c3f7864 Mon Sep 17 00:00:00 2001 From: LennyN95 Date: Thu, 14 Mar 2024 10:40:09 +0100 Subject: [PATCH 13/13] reorganize configs --- models/fmcib_radiomics/config/default.yml | 19 ++++++++-------- models/fmcib_radiomics/config/dicom.yml | 22 ------------------- .../fmcib_radiomics/config/from_nrrd_mask.yml | 21 ++++++++++++++++++ 3 files changed, 31 insertions(+), 31 deletions(-) delete mode 100644 models/fmcib_radiomics/config/dicom.yml create mode 100644 models/fmcib_radiomics/config/from_nrrd_mask.yml diff --git a/models/fmcib_radiomics/config/default.yml b/models/fmcib_radiomics/config/default.yml index cc4b1559..297a8a14 100644 --- a/models/fmcib_radiomics/config/default.yml +++ b/models/fmcib_radiomics/config/default.yml @@ -1,21 +1,22 @@ general: data_base_dir: /app/data version: 1.0 - description: "FMCIB pipeline" + description: FMCIB pipeline starting from DICOM files and centroids in json files or slicer exports named by their SeriesInstanceUID execute: -- FileStructureImporter -- CentroidExtractor +- DicomImporter +- FileImporter +- NiftiConverter - FMCIBRunner - DataOrganizer modules: - FileStructureImporter: - structures: - - $patientID/CT.nrrd@instance@nrrd:mod=ct - - $patientID/masks/GTV-1.nrrd@nrrd:mod=seg - import_id: patientID + + FileImporter: + instance_id: sid + meta: type=fmcibcoordinates + type: json DataOrganizer: targets: - - json:type=fmcibfeatures-->[i:patientID]/features.json \ No newline at end of file + - json:type=fmcibfeatures-->[i:sid]/features.json \ No newline at end of file diff --git a/models/fmcib_radiomics/config/dicom.yml b/models/fmcib_radiomics/config/dicom.yml deleted file mode 100644 index da7dc513..00000000 --- a/models/fmcib_radiomics/config/dicom.yml +++ /dev/null @@ -1,22 +0,0 @@ -general: - data_base_dir: /app/data - version: 1.0 - description: FMCIB pipeline starting from DICOM files and centroids in json files named by their SeriesInstanceUID - -execute: -- DicomImporter -- FileImporter -- NiftiConverter -- FMCIBRunner -- DataOrganizer - -modules: - - FileImporter: - instance_id: sid - meta: type=fmcibcoordinates - type: json - - DataOrganizer: - targets: - - json:type=fmcibfeatures-->[i:sid]/features.json \ No newline at end of file diff --git a/models/fmcib_radiomics/config/from_nrrd_mask.yml b/models/fmcib_radiomics/config/from_nrrd_mask.yml new file mode 100644 index 00000000..22644ffc --- /dev/null +++ b/models/fmcib_radiomics/config/from_nrrd_mask.yml @@ -0,0 +1,21 @@ +general: + data_base_dir: /app/data + version: 1.0 + description: "FMCIB pipeline starting from a nrrd file image and a nnrd binary mask of the GTV." + +execute: +- FileStructureImporter +- CentroidExtractor +- FMCIBRunner +- DataOrganizer + +modules: + FileStructureImporter: + structures: + - $patientID/CT.nrrd@instance@nrrd:mod=ct + - $patientID/masks/GTV-1.nrrd@nrrd:mod=seg + import_id: patientID + + DataOrganizer: + targets: + - json:type=fmcibfeatures-->[i:patientID]/features.json \ No newline at end of file