diff --git a/models/fmcib_radiomics/config/default.yml b/models/fmcib_radiomics/config/default.yml new file mode 100644 index 00000000..297a8a14 --- /dev/null +++ b/models/fmcib_radiomics/config/default.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 or slicer exports 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_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 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 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 new file mode 100644 index 00000000..54059428 --- /dev/null +++ b/models/fmcib_radiomics/dockerfiles/Dockerfile @@ -0,0 +1,21 @@ +FROM mhubai/base:latest + +LABEL authors="bspai@bwh.harvard.edu,lnuernberg@bwh.harvard.edu" + +# download model weights +RUN wget https://zenodo.org/records/10528450/files/model_weights.torch?download=1 -O /app/model_weights.torch + +# clone mhub implementation +ARG MHUB_MODELS_REPO +RUN buildutils/import_mhub_model.sh fmcib_radiomics ${MHUB_MODELS_REPO} + + +# 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 + +ENTRYPOINT ["mhub.run"] +CMD ["--workflow", "default"] diff --git a/models/fmcib_radiomics/meta.json b/models/fmcib_radiomics/meta.json new file mode 100644 index 00000000..ba3f4087 --- /dev/null +++ b/models/fmcib_radiomics/meta.json @@ -0,0 +1,137 @@ +{ + "id": "26e98e14-b605-4007-bd8b-79d517c935b5", + "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_radiomics/utils/CentroidExtractor.py b/models/fmcib_radiomics/utils/CentroidExtractor.py new file mode 100644 index 00000000..1e5154cb --- /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_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 new file mode 100644 index 00000000..0729413d --- /dev/null +++ b/models/fmcib_radiomics/utils/FMCIBRunner.py @@ -0,0 +1,114 @@ +""" +--------------------------------------------------------- +Author: Suraj Pai, Leonard Nürnberg +Email: bspai@bwh.harvard.edu, lnuernberg@bwh.harvard.edu +Date: 06.03.2024 +--------------------------------------------------------- +""" +import json, jsonschema, os +from mhubio.core import Instance, InstanceData, IO, Module + +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 len(markups) == 1, "Currently, only one point per file is supported." + markup = markups[0] + + assert markup["coordinateSystem"] == "LPS" + + controlPoints = markup["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. + + 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.models import fmcib_model + from fmcib.preprocessing import preprocess + + # initialize the ResNet50 model with pretrained weights + 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|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: + + # read centroids from json file + coordinates = get_coordinates(centroids_json.abspath) + + # define input dictionary + input_dict = { + "image_path": in_data.abspath, + **coordinates + } + + + # run model + fmcib(input_dict, feature_json.abspath) \ No newline at end of file diff --git a/models/fmcib_radiomics/utils/__init__.py b/models/fmcib_radiomics/utils/__init__.py new file mode 100644 index 00000000..6d0f2d8d --- /dev/null +++ b/models/fmcib_radiomics/utils/__init__.py @@ -0,0 +1 @@ +from .FMCIBRunner import FMCIBRunner \ 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 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" } + } + } + } + ] + } + } + } + } + ] + } + } + } +}