diff --git a/src/DicomMetaDictionary.js b/src/DicomMetaDictionary.js index 5d56b41e..6b0f7c0a 100644 --- a/src/DicomMetaDictionary.js +++ b/src/DicomMetaDictionary.js @@ -367,7 +367,8 @@ DicomMetaDictionary.sopClassNamesByUID = { "1.2.840.10008.5.1.4.1.1.88.33": "ComprehensiveSR", "1.2.840.10008.5.1.4.1.1.128": "PETImage", "1.2.840.10008.5.1.4.1.1.130": "EnhancedPETImage", - "1.2.840.10008.5.1.4.1.1.128.1": "LegacyConvertedEnhancedPETImage" + "1.2.840.10008.5.1.4.1.1.128.1": "LegacyConvertedEnhancedPETImage", + "1.2.840.10008.5.1.4.1.1.481.3": "RTStructureSetStorage" }; DicomMetaDictionary.dictionary = dictionary; diff --git a/src/adapters/Cornerstone/RTSS.js b/src/adapters/Cornerstone/RTSS.js new file mode 100644 index 00000000..1b4bf4e4 --- /dev/null +++ b/src/adapters/Cornerstone/RTSS.js @@ -0,0 +1,102 @@ +import log from "../../log.js"; +import { datasetToBlob } from "../../datasetToBlob.js"; +import { DicomMessage } from "../../DicomMessage.js"; +import { DicomMetaDictionary } from "../../DicomMetaDictionary.js"; +import { Normalizer } from "../../normalizers.js"; +import { RTSS as RTSSDerivation } from "../../derivations/index.js"; + +const RTSS = { + generateMockRTSS +}; + +export default RTSS; + +/** + * + * @typedef {Object} BrushData + * @property {Object} toolState - The cornerstoneTools global toolState. + * @property {Object[]} rtss - The cornerstoneTools segment metadata that corresponds to the + * seriesInstanceUid. + */ + +const generateRTSSDefaultOptions = { + includeSliceSpacing: true, + rleEncode: true +}; + +/** + * generateMockRTSS - Generates cornerstoneTools brush data, given a stack of + * imageIds, images and the cornerstoneTools brushData. + * + * @param {object[]} images An array of cornerstone images that contain the source + * data under `image.data.byteArray.buffer`. + * @param {Object|Object[]} inputLabelmaps3D The cornerstone `Labelmap3D` object, or an array of objects. + * @param {Object} userOptions Options to pass to the segmentation derivation and `fillSegmentation`. + * @returns {Blob} + */ +function generateMockRTSS(images, inputLabelmaps3D, userOptions = {}) { + const isMultiFrame = images[0].imageId.includes("?frame"); + const rtss = _createRTSSFromImages(images, isMultiFrame, userOptions); + + console.log(images); + + return fillRTSS(rtss, inputLabelmaps3D, userOptions); +} + +/** + * fillRTSS - Fills a derived rtss dataset with array of cornerstoneTools `ROIContour` data. + * + * @param {object[]} rtss An empty rtss derived dataset. + * @param {Object[]} inputROIContours The array of cornerstone `ROIContour` objects to add. + * @param {Object} userOptions Options object to override default options. + * @returns {Blob} description + */ +function fillRTSS(rtss, inputROIContours, userOptions = {}) { + const options = Object.assign({}, generateRTSSDefaultOptions, userOptions); + + inputROIContours.forEach(roiContour => { + rtss.addContour(roiContour); + }); + + const rtssBlob = datasetToBlob(rtss.dataset); + return rtssBlob; +} + +/** + * _createRTSSFromImages - description + * + * @param {Object[]} images An array of the cornerstone image objects. + * @param {Boolean} isMultiframe Whether the images are multiframe. + * @returns {Object} The RTSS derived dataSet. + */ +function _createRTSSFromImages(images, isMultiframe, options) { + const datasets = []; + + if (isMultiframe) { + const image = images[0]; + const arrayBuffer = image.data.byteArray.buffer; + + const dicomData = DicomMessage.readFile(arrayBuffer); + const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); + + dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); + + datasets.push(dataset); + } else { + for (let i = 0; i < images.length; i++) { + const image = images[i]; + const arrayBuffer = image.data.byteArray.buffer; + const dicomData = DicomMessage.readFile(arrayBuffer); + const dataset = DicomMetaDictionary.naturalizeDataset( + dicomData.dict + ); + + dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); + datasets.push(dataset); + } + } + + const multiframe = Normalizer.normalizeToDataset(datasets); + + return new RTSSDerivation([multiframe], options); +} diff --git a/src/adapters/Cornerstone/index.js b/src/adapters/Cornerstone/index.js index e8c4b6f6..935d7cb9 100644 --- a/src/adapters/Cornerstone/index.js +++ b/src/adapters/Cornerstone/index.js @@ -6,6 +6,7 @@ import EllipticalRoi from "./EllipticalRoi.js"; import CircleRoi from "./CircleRoi.js"; import ArrowAnnotate from "./ArrowAnnotate.js"; import Segmentation from "./Segmentation.js"; +import RTSS from "./RTSS.js"; import CobbAngle from "./CobbAngle"; import Angle from "./Angle"; import RectangleRoi from "./RectangleRoi"; @@ -19,6 +20,7 @@ const Cornerstone = { ArrowAnnotate, MeasurementReport, Segmentation, + RTSS, CobbAngle, Angle, RectangleRoi diff --git a/src/derivations/RTSS.js b/src/derivations/RTSS.js new file mode 100644 index 00000000..a981f39d --- /dev/null +++ b/src/derivations/RTSS.js @@ -0,0 +1,128 @@ +import DerivedDataset from "./DerivedDataset"; +import { DicomMetaDictionary } from "../DicomMetaDictionary"; + +export default class RTSS extends DerivedDataset { + constructor(datasets, options = {}) { + super(datasets, options); + } + + // this assumes a normalized multiframe input and will create + // a multiframe derived image + derive() { + super.derive(); + + this.assignToDataset({ + SOPClassUID: + DicomMetaDictionary.sopClassUIDsByName.RTStructureSetStorage, + Modality: "RTSTRUCT", + ValueType: "CONTAINER" + }); + + this.assignFromReference([]); + + this.dataset.ReferencedFrameOfReferenceSequence = []; + this.dataset.StructureSetROISequence = []; + this.dataset.ROIContourSequence = []; + this.dataset.RTROIObservationsSequence = []; + //console.log(this.referencedDataset); + + // NOTE: was previously under addContour + const dataset = this.dataset; + + // Populate ReferencedFrameOfReferenceSequence + // Referenced DICOM data + const ReferencedFrameOfReferenceSequence = + dataset.ReferencedFrameOfReferenceSequence; + + //const FrameOfReferenceUID = + const FrameOfReferenceUID = this.referencedDataset.FrameOfReferenceUID; + + // DICOM set that is referenced + const ContourImageSequence = []; + this.referencedDataset.ReferencedSeriesSequence.ReferencedInstanceSequence.forEach( + instance => { + ContourImageSequence.push(instance); + } + ); + + const RTReferencedSeriesSequence = []; + const RTReferencedSeries = { + SeriesInstaceUID: this.referencedDataset.ReferencedSeriesSequence + .SeriesInstanceUID, + ContourImageSequence + }; + RTReferencedSeriesSequence.push(RTReferencedSeries); + + const RTReferencedStudySequence = []; + + const RTReferencedStudy = { + ReferencedSOPClassUID: "1.2.840.10008.3.1.2.3.1", // Detached Study Management SOP Class + ReferencedSOPInstanceUID: this.referencedDataset.StudyInstanceUID, + RTReferencedSeriesSequence + }; + + RTReferencedStudySequence.push(RTReferencedStudy); + + const ReferencedFrameOfReference = { + FrameOfReferenceUID, + RTReferencedStudySequence + }; + ReferencedFrameOfReferenceSequence.push(ReferencedFrameOfReference); + } + + /** + * addContour - Adds a new ROI with related contours to ROIContourSequence + * + * @param {Object} newContour cornerstoneTools `ROIContour` object + * + * newContour = { + * name: string, + * description: string, + * contourSequence: array[contour] + * } + * + * contour = { + * ContourImageSequence: array[ + * { ReferencedSOPClassUID: string, ReferencedSOPInstanceUID: string} + * ] + * ContourGeometricType: string, + * NumberOfContourPoints: number, + * ContourData: array[number] + * } + */ + addContour(newContour) { + // Start + const dataset = this.dataset; + + // ROI set information + const ReferencedFrameOfReferenceUID = this.referencedDataset + .FrameOfReferenceUID; + const StructureSetROISequence = dataset.StructureSetROISequence; + + const ROINumber = StructureSetROISequence.length + 1; + + const StructureSetROI = { + ROINumber, + ReferencedFrameOfReferenceUID, + ROIName: newContour.name, + ROIDescription: newContour.description, + ROIGenerationAlgorithm: "MANUAL" + }; + + StructureSetROISequence.push(StructureSetROI); + + // Contour Data + const ROIContourSequence = dataset.ROIContourSequence; + + const roiContour = { + ROIDisplayColor: [255, 0, 0], // implement in tool? + ContourSequence: newContour.contourSequence, + ReferencedROINumber: ROINumber + }; + + ROIContourSequence.push(roiContour); + + // ROI Observation data + //const RTROIObservationsSequence = dataset.RTROIObservationsSequence; + } +} diff --git a/src/derivations/index.js b/src/derivations/index.js index 92545940..34554438 100644 --- a/src/derivations/index.js +++ b/src/derivations/index.js @@ -4,6 +4,7 @@ import DerivedImage from "./DerivedImage"; import Segmentation from "./Segmentation"; import ParametricMap from "./ParametricMap"; import StructuredReport from "./StructuredReport"; +import RTSS from "./RTSS"; export { DerivedDataset, @@ -11,5 +12,6 @@ export { DerivedImage, Segmentation, ParametricMap, - StructuredReport + StructuredReport, + RTSS }; diff --git a/src/rtss/coding.js b/src/rtss/coding.js new file mode 100644 index 00000000..3da10b03 --- /dev/null +++ b/src/rtss/coding.js @@ -0,0 +1,77 @@ +class Code { + constructor(options) { + this[_value] = options.value; + this[_meaning] = options.meaning; + this[_schemeDesignator] = options.schemeDesignator; + this[_schemeVersion] = options.schemeVersion || null; + } + + get value() { + return this[_value]; + } + + get meaning() { + return this[_meaning]; + } + + get schemeDesignator() { + return this[_schemeDesignator]; + } + + get schemeVersion() { + return this[_schemeVersion]; + } +} + +class CodedConcept { + constructor(options) { + if (options.value === undefined) { + throw new Error("Option 'value' is required for CodedConcept."); + } + if (options.meaning === undefined) { + throw new Error("Option 'meaning' is required for CodedConcept."); + } + if (options.schemeDesignator === undefined) { + throw new Error( + "Option 'schemeDesignator' is required for CodedConcept." + ); + } + this.CodeValue = options.value; + this.CodeMeaning = options.meaning; + this.CodingSchemeDesignator = options.schemeDesignator; + if ("schemeVersion" in options) { + this.CodingSchemeVersion = options.schemeVersion; + } + } + + equals(other) { + if ( + other.value === this.value && + other.schemeDesignator === this.schemeDesignator + ) { + if (other.schemeVersion && this.schemeVersion) { + return other.schemeVersion === this.schemeVersion; + } + return true; + } + return false; + } + + get value() { + return this.CodeValue; + } + + get meaning() { + return this.CodeMeaning; + } + + get schemeDesignator() { + return this.CodingSchemeDesignator; + } + + get schemeVersion() { + return this.CodingSchemeVersion; + } +} + +export { Code, CodedConcept }; diff --git a/src/rtss/documents.js b/src/rtss/documents.js new file mode 100644 index 00000000..2900e65a --- /dev/null +++ b/src/rtss/documents.js @@ -0,0 +1,470 @@ +import { DicomMetaDictionary } from "../DicomMetaDictionary.js"; + +const _attributesToInclude = [ + // Patient + "00080054", + "00080100", + "00080102", + "00080103", + "00080104", + "00080105", + "00080106", + "00080107", + "0008010B", + "0008010D", + "0008010F", + "00080117", + "00080118", + "00080119", + "00080120", + "00080121", + "00080122", + "00081120", + "00081150", + "00081155", + "00081160", + "00081190", + "00081199", + "00100010", + "00100020", + "00100021", + "00100022", + "00100024", + "00100026", + "00100027", + "00100028", + "00100030", + "00100032", + "00100033", + "00100034", + "00100035", + "00100040", + "00100200", + "00100212", + "00100213", + "00100214", + "00100215", + "00100216", + "00100217", + "00100218", + "00100219", + "00100221", + "00100222", + "00100223", + "00100229", + "00101001", + "00101002", + "00101100", + "00102160", + "00102201", + "00102202", + "00102292", + "00102293", + "00102294", + "00102295", + "00102296", + "00102297", + "00102298", + "00102299", + "00104000", + "00120062", + "00120063", + "00120064", + "0020000D", + "00400031", + "00400032", + "00400033", + "00400035", + "00400036", + "00400039", + "0040003A", + "0040E001", + "0040E010", + "0040E020", + "0040E021", + "0040E022", + "0040E023", + "0040E024", + "0040E025", + "0040E030", + "0040E031", + "0062000B", + "00880130", + "00880140", + // Patient Study + "00080100", + "00080102", + "00080103", + "00080104", + "00080105", + "00080106", + "00080107", + "0008010B", + "0008010D", + "0008010F", + "00080117", + "00080118", + "00080119", + "00080120", + "00080121", + "00080122", + "00081080", + "00081084", + "00101010", + "00101020", + "00101021", + "00101022", + "00101023", + "00101024", + "00101030", + "00102000", + "00102110", + "00102180", + "001021A0", + "001021B0", + "001021C0", + "001021D0", + "00102203", + "00380010", + "00380014", + "00380060", + "00380062", + "00380064", + "00380500", + "00400031", + "00400032", + "00400033", + // General Study + "00080020", + "00080030", + "00080050", + "00080051", + "00080080", + "00080081", + "00080082", + "00080090", + "00080096", + "0008009C", + "0008009D", + "00080100", + "00080102", + "00080103", + "00080104", + "00080105", + "00080106", + "00080107", + "0008010B", + "0008010D", + "0008010F", + "00080117", + "00080118", + "00080119", + "00080120", + "00080121", + "00080122", + "00081030", + "00081032", + "00081048", + "00081049", + "00081060", + "00081062", + "00081110", + "00081150", + "00081155", + "0020000D", + "00200010", + "00321034", + "00400031", + "00400032", + "00400033", + "00401012", + "00401101", + "00401102", + "00401103", + "00401104", + // Clinical Trial Subject + "00120010", + "00120020", + "00120021", + "00120030", + "00120031", + "00120040", + "00120042", + "00120081", + "00120082", + // Clinical Trial Study + "00120020", + "00120050", + "00120051", + "00120052", + "00120053", + "00120083", + "00120084", + "00120085" +]; + +class RTStructureSet { + constructor(options) { + //if (options.evidence === undefined) { + // throw new Error( + // "Option 'evidence' is required for Comprehensive3DSR." + // ); + //} + //if ( + // !( + // typeof options.evidence === "object" || + // options.evidence instanceof Array + // ) + //) { + // throw new Error("Option 'evidence' must have type Array."); + //} + //if (options.evidence.length === 0) { + // throw new Error("Option 'evidence' must have non-zero length."); + //} + //if (options.content === undefined) { + // throw new Error( + // "Option 'content' is required for Comprehensive3DSR." + // ); + //} + //if (options.seriesInstanceUID === undefined) { + // throw new Error( + // "Option 'seriesInstanceUID' is required for Comprehensive3DSR." + // ); + //} + //if (options.seriesNumber === undefined) { + // throw new Error( + // "Option 'seriesNumber' is required for Comprehensive3DSR." + // ); + //} + //if (options.seriesDescription === undefined) { + // throw new Error( + // "Option 'seriesDescription' is required for Comprehensive3DSR." + // ); + //} + //if (options.sopInstanceUID === undefined) { + // throw new Error( + // "Option 'sopInstanceUID' is required for Comprehensive3DSR." + // ); + //} + //if (options.instanceNumber === undefined) { + // throw new Error( + // "Option 'instanceNumber' is required for Comprehensive3DSR." + // ); + //} + //if (options.manufacturer === undefined) { + // throw new Error( + // "Option 'manufacturer' is required for Comprehensive3DSR." + // ); + //} + + // >>>>>>>>>>>>>RTSS + if (options.seriesInstanceUID === undefined) { + throw new Error( + "Option 'seriesInstanceUID' is required for RTStructureSet." + ); + } + if (options.seriesNumber === undefined) { + throw new Error( + "Option 'seriesNumber' is required for RTStructureSet." + ); + } + if (options.seriesDescription === undefined) { + throw new Error( + "Option 'seriesDescription' is required for RTStructureSet." + ); + } + if (options.sopInstanceUID === undefined) { + throw new Error( + "Option 'sopInstanceUID' is required for RTStructureSet." + ); + } + if (options.instanceNumber === undefined) { + throw new Error( + "Option 'instanceNumber' is required for RTStructureSet." + ); + } + if (options.manufacturer === undefined) { + throw new Error( + "Option 'manufacturer' is required for RTStructureSet." + ); + } + // <<<<<<<<<<<<< + + (this.SOPClassUID = + DicomMetaDictionary.sopClassUIDsByName.RTStructureSetStorage), + (this.SOPInstanceUID = options.sopInstanceUID); + this.Modality = "SR"; + this.SeriesDescription = options.seriesDescription; + this.SeriesInstanceUID = options.seriesInstanceUID; + this.SeriesNumber = options.seriesNumber; + this.InstanceNumber = options.instanceNumber; + + this.Manufacturer = options.manufacturer; + //if (options.institutionName !== undefined) { + // this.InstitutionName = options.institutionName; + // if (options.institutionalDepartmentName !== undefined) { + // this.InstitutionalDepartmentName = + // options.institutionDepartmentName; + // } + //} + + //if (options.isComplete) { + // this.CompletionFlag = "COMPLETE"; + //} else { + // this.CompletionFlag = "PARTIAL"; + //} + //if (options.isVerified) { + // if (options.verifyingObserverName === undefined) { + // throw new Error( + // "Verifying Observer Name must be specified if SR document " + + // "has been verified." + // ); + // } + // if (options.verifyingOrganization === undefined) { + // throw new Error( + // "Verifying Organization must be specified if SR document " + + // "has been verified." + // ); + // } + // this.VerificationFlag = "VERIFIED"; + // const ovserver_item = {}; + // ovserver_item.VerifyingObserverName = options.verifyingObserverName; + // ovserver_item.VerifyingOrganization = options.verifyingOrganization; + // ovserver_item.VerificationDateTime = DicomMetaDictionary.dateTime(); + // this.VerifyingObserverSequence = [observer_item]; + //} else { + // this.VerificationFlag = "UNVERIFIED"; + //} + //if (options.isFinal) { + // this.PreliminaryFlag = "FINAL"; + //} else { + // this.PreliminaryFlag = "PRELIMINARY"; + //} + + // NOTE: ContentDate/ContentTime refers to this dcmjs doc instance + this.ContentDate = DicomMetaDictionary.date(); + this.ContentTime = DicomMetaDictionary.time(); + + // Get rest of fields + Object.keys(options.content).forEach(keyword => { + this[keyword] = options.content[keyword]; + }); + + //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> options.evidence + //const evidenceCollection = {}; + //options.evidence.forEach(evidence => { + // if ( + // evidence.StudyInstanceUID !== + // options.evidence[0].StudyInstanceUID + // ) { + // throw new Error( + // "Referenced data sets must all belong to the same study." + // ); + // } + // if (!(evidence.SeriesInstanceUID in evidenceCollection)) { + // evidenceCollection[evidence.SeriesInstanceUID] = []; + // } + // const instanceItem = {}; + // instanceItem.ReferencedSOPClassUID = evidence.SOPClassUID; + // instanceItem.ReferencedSOPInstanceUID = evidence.SOPInstanceUID; + // evidenceCollection[evidence.SeriesInstanceUID].push(instanceItem); + //}); + //const evidenceStudyItem = {}; + //evidenceStudyItem.StudyInstanceUID = + // options.evidence[0].StudyInstanceUID; + //evidenceStudyItem.ReferencedSeriesSequence = []; + //Object.keys(evidenceCollection).forEach(seriesInstanceUID => { + // const seriesItem = {}; + // seriesItem.SeriesInstanceUID = seriesInstanceUID; + // seriesItem.ReferencedSOPSequence = + // evidenceCollection[seriesInstanceUID]; + // evidenceStudyItem.ReferencedSeriesSequence.push(seriesItem); + //}); + //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + //if (options.requestedProcedures !== undefined) { + // if ( + // !( + // typeof options.requestedProcedures === "object" || + // options.requestedProcedures instanceof Array + // ) + // ) { + // throw new Error( + // "Option 'requestedProcedures' must have type Array." + // ); + // } + // this.ReferencedRequestSequence = new ContentSequence( + // ...options.requestedProcedures + // ); + // this.CurrentRequestedProcedureEvidenceSequence = [ + // evidenceStudyItem + // ]; + //} else { + // this.PertinentOtherEvidenceSequence = [evidenceStudyItem]; + //} + + //if (options.previousVersions !== undefined) { + // const preCollection = {}; + // options.previousVersions.forEach(version => { + // if ( + // version.StudyInstanceUID != + // options.evidence[0].StudyInstanceUID + // ) { + // throw new Error( + // "Previous version data sets must belong to the same study." + // ); + // } + // const instanceItem = {}; + // instanceItem.ReferencedSOPClassUID = version.SOPClassUID; + // instanceItem.ReferencedSOPInstanceUID = version.SOPInstanceUID; + // preCollection[version.SeriesInstanceUID].push(instanceItem); + // }); + // const preStudyItem = {}; + // preStudyItem.StudyInstanceUID = + // options.previousVersions[0].StudyInstanceUID; + // preStudyItem.ReferencedSeriesSequence = []; + // Object.keys(preCollection).forEach(seriesInstanceUID => { + // const seriesItem = {}; + // seriesItem.SeriesInstanceUID = seriesInstanceUID; + // seriesItem.ReferencedSOPSequence = + // preCollection[seriesInstanceUID]; + // preStudyItem.ReferencedSeriesSequence.push(seriesItem); + // }); + // this.PredecessorDocumentsSequence = [preStudyItem]; + //} + + //if (options.performedProcedureCodes !== undefined) { + // if ( + // !( + // typeof options.performedProcedureCodes === "object" || + // options.performedProcedureCodes instanceof Array + // ) + // ) { + // throw new Error( + // "Option 'performedProcedureCodes' must have type Array." + // ); + // } + // this.PerformedProcedureCodeSequence = new ContentSequence( + // ...options.performedProcedureCodes + // ); + //} else { + // this.PerformedProcedureCodeSequence = []; + //} + + //this.ReferencedPerformedProcedureStepSequence = []; + + _attributesToInclude.forEach(tag => { + const key = DicomMetaDictionary.punctuateTag(tag); + const element = DicomMetaDictionary.dictionary[key]; + if (element !== undefined) { + const keyword = element.name; + const value = options.evidence[0][keyword]; + if (value !== undefined) { + this[keyword] = value; + } + } + }); + } +} + +export { RTStructureSet }; diff --git a/src/rtss/index.js b/src/rtss/index.js new file mode 100644 index 00000000..3f8e7977 --- /dev/null +++ b/src/rtss/index.js @@ -0,0 +1,15 @@ +import * as coding from "./coding.js"; +import * as contentItems from "./contentItems.js"; +import * as templates from "./templates.js"; +import * as valueTypes from "./valueTypes.js"; +import * as documents from "./documents.js"; + +const rtss = { + coding, + contentItems, + documents, + templates, + valueTypes +}; + +export default rtss;