diff --git a/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js b/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js index 8b22794646..84ed903d53 100644 --- a/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js +++ b/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js @@ -238,6 +238,7 @@ export default class MeasurementReport { derivationSourceDataset._vrMap = _vrMap; const report = new StructuredReport([derivationSourceDataset]); + const contentItem = MeasurementReport.contentItem( derivationSourceDataset ); diff --git a/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js b/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js index bbd49b729f..7d9cb7b058 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js +++ b/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js @@ -4,44 +4,54 @@ import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import CodingScheme from "./CodingScheme"; const ARROW_ANNOTATE = "ArrowAnnotate"; -const trackingIdentifierTextValue = "Cornerstone3DTools@^0.1.0:ArrowAnnotate"; +const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${ARROW_ANNOTATE}`; const { codeValues, CodingSchemeDesignator } = CodingScheme; class ArrowAnnotate { constructor() {} - static getMeasurementData(MeasurementGroup, imageId, imageToWorldCoords) { + static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata + ) { const { defaultState, - SCOORDGroup, - findingGroup - } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + ArrowAnnotate.toolType + ); - const text = findingGroup.ConceptCodeSequence.CodeMeaning; + const referencedImageId = + defaultState.annotation.metadata.referencedImageId; + + const text = defaultState.metadata.label; const { GraphicData } = SCOORDGroup; const worldCoords = []; for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(imageId, [ + const point = imageToWorldCoords(referencedImageId, [ GraphicData[i], GraphicData[i + 1] ]); worldCoords.push(point); } - const state = { - ...defaultState, - toolType: ArrowAnnotate.toolType, - data: { - text, - handles: { - points: [worldCoords[0], worldCoords[1]], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } + const state = defaultState; + + state.annotation.data = { + text, + handles: { + points: [worldCoords[0], worldCoords[1]], + activeHandleIndex: 0, + textBox: { + hasMoved: false } } }; @@ -99,9 +109,9 @@ ArrowAnnotate.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { return false; } - const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - if (cornerstone4Tag !== CORNERSTONE_3D_TAG) { + if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { return false; } diff --git a/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.js b/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.js index e20fddeba7..83d9464500 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.js +++ b/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.js @@ -9,24 +9,27 @@ const LONG_AXIS = "Long Axis"; const SHORT_AXIS = "Short Axis"; const FINDING = "121071"; const FINDING_SITE = "G-C0E3"; -const trackingIdentifierTextValue = "Cornerstone3DTools@^0.1.0:Bidirectional"; +const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${BIDIRECTIONAL}`; class Bidirectional { constructor() {} - static getMeasurementData(MeasurementGroup, imageId, imageToWorldCoords) { + static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata + ) { const { defaultState } = MeasurementReport.getSetupMeasurementData( - MeasurementGroup - ); - const { ContentSequence } = MeasurementGroup; - - const findingGroup = toArray(ContentSequence).find( - group => group.ConceptNameCodeSequence.CodeValue === FINDING + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + Bidirectional.toolType ); - const findingSiteGroups = toArray(ContentSequence).filter( - group => group.ConceptNameCodeSequence.CodeValue === FINDING_SITE - ); + const referencedImageId = + defaultState.annotation.metadata.referencedImageId; + const { ContentSequence } = MeasurementGroup; const longAxisNUMGroup = toArray(ContentSequence).find( group => group.ConceptNameCodeSequence.CodeMeaning === LONG_AXIS @@ -49,7 +52,7 @@ class Bidirectional { [longAxisSCOORDGroup, shortAxisSCOORDGroup].forEach(group => { const { GraphicData } = group; for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(imageId, [ + const point = imageToWorldCoords(referencedImageId, [ GraphicData[i], GraphicData[i + 1] ]); @@ -57,35 +60,25 @@ class Bidirectional { } }); - const state = { - ...defaultState, - finding: findingGroup - ? findingGroup.ConceptCodeSequence - : undefined, - findingSites: findingSiteGroups.map(fsg => { - return { ...fsg.ConceptCodeSequence }; - }), - toolType: Bidirectional.toolType, - data: { - handles: { - points: [ - worldCoords[0], - worldCoords[1], - worldCoords[2], - worldCoords[3] - ], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: { - [`imageId:${imageId}`]: { - length: - longAxisNUMGroup.MeasuredValueSequence.NumericValue, - width: - shortAxisNUMGroup.MeasuredValueSequence.NumericValue - } + const state = defaultState; + + state.annotation.data = { + handles: { + points: [ + worldCoords[0], + worldCoords[1], + worldCoords[2], + worldCoords[3] + ], + activeHandleIndex: 0, + textBox: { + hasMoved: false + } + }, + cachedStats: { + [`imageId:${referencedImageId}`]: { + length: longAxisNUMGroup.MeasuredValueSequence.NumericValue, + width: shortAxisNUMGroup.MeasuredValueSequence.NumericValue } } }; @@ -189,9 +182,9 @@ Bidirectional.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { return false; } - const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - if (cornerstone4Tag !== CORNERSTONE_3D_TAG) { + if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { return false; } diff --git a/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.js b/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.js index dc354668ce..af33926c48 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.js +++ b/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.js @@ -8,14 +8,14 @@ const FINDING = "121071"; const FINDING_SITE = "G-C0E3"; const EPSILON = 1e-4; -const trackingIdentifierTextValue = "Cornerstone3DTools@^0.1.0:EllipticalROI"; +const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${ELLIPTICALROI}`; class EllipticalROI { constructor() {} static getMeasurementData( MeasurementGroup, - imageId, + sopInstanceUIDToImageIdMap, imageToWorldCoords, metadata ) { @@ -23,7 +23,15 @@ class EllipticalROI { defaultState, NUMGroup, SCOORDGroup - } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); + } = MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + EllipticalROI.toolType + ); + + const referencedImageId = + defaultState.annotation.metadata.referencedImageId; const { GraphicData } = SCOORDGroup; @@ -33,7 +41,7 @@ class EllipticalROI { // in the image plane and then choose the correct points to use for the ellipse. const pointsWorld = []; for (let i = 0; i < GraphicData.length; i += 2) { - const worldPos = imageToWorldCoords(imageId, [ + const worldPos = imageToWorldCoords(referencedImageId, [ GraphicData[i], GraphicData[i + 1] ]); @@ -56,7 +64,10 @@ class EllipticalROI { vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart); vec3.normalize(minorAxisVec, minorAxisVec); - const imagePlaneModule = metadata.get("imagePlaneModule", imageId); + const imagePlaneModule = metadata.get( + "imagePlaneModule", + referencedImageId + ); if (!imagePlaneModule) { throw new Error("imageId does not have imagePlaneModule metadata"); @@ -99,24 +110,23 @@ class EllipticalROI { console.warn("OBLIQUE ELLIPSE NOT YET SUPPORTED"); } - const state = { - ...defaultState, - toolType: EllipticalROI.toolType, - data: { - handles: { - points: [...ellipsePoints], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: { - [`imageId:${imageId}`]: { - area: NUMGroup.MeasuredValueSequence.NumericValue - } + const state = defaultState; + + state.annotation.data = { + handles: { + points: [...ellipsePoints], + activeHandleIndex: 0, + textBox: { + hasMoved: false + } + }, + cachedStats: { + [`imageId:${referencedImageId}`]: { + area: NUMGroup.MeasuredValueSequence.NumericValue } } }; + return state; } @@ -180,9 +190,9 @@ EllipticalROI.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { return false; } - const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - if (cornerstone4Tag !== CORNERSTONE_3D_TAG) { + if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { return false; } diff --git a/packages/adapters/src/adapters/Cornerstone3D/Length.js b/packages/adapters/src/adapters/Cornerstone3D/Length.js index 9f55402cb5..23483475e7 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Length.js +++ b/packages/adapters/src/adapters/Cornerstone3D/Length.js @@ -5,45 +5,55 @@ import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; const LENGTH = "Length"; const FINDING = "121071"; const FINDING_SITE = "G-C0E3"; -const trackingIdentifierTextValue = "Cornerstone3DTools@^0.1.0:Length"; +const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${LENGTH}`; class Length { constructor() {} // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. - static getMeasurementData(MeasurementGroup, imageId, imageToWorldCoords) { + static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata + ) { const { defaultState, NUMGroup, SCOORDGroup - } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); + } = MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + Length.toolType + ); + + const referencedImageId = + defaultState.annotation.metadata.referencedImageId; const { GraphicData } = SCOORDGroup; const worldCoords = []; for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(imageId, [ + const point = imageToWorldCoords(referencedImageId, [ GraphicData[i], GraphicData[i + 1] ]); worldCoords.push(point); } - const state = { - ...defaultState, - length: NUMGroup.MeasuredValueSequence.NumericValue, - toolType: Length.toolType, - data: { - handles: { - points: [worldCoords[0], worldCoords[1]], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: { - [`imageId:${imageId}`]: { - length: NUMGroup.MeasuredValueSequence.NumericValue - } + const state = defaultState; + + state.annotation.data = { + handles: { + points: [worldCoords[0], worldCoords[1]], + activeHandleIndex: 0, + textBox: { + hasMoved: false + } + }, + cachedStats: { + [`imageId:${referencedImageId}`]: { + length: NUMGroup.MeasuredValueSequence.NumericValue } } }; @@ -90,9 +100,9 @@ Length.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { return false; } - const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - if (cornerstone4Tag !== CORNERSTONE_3D_TAG) { + if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { return false; } diff --git a/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.js b/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.js index 711ae22a1b..526457bf02 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.js +++ b/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.js @@ -3,6 +3,7 @@ import { DicomMetaDictionary } from "../../DicomMetaDictionary.js"; import { StructuredReport } from "../../derivations/index.js"; import TID1500MeasurementReport from "../../utilities/TID1500/TID1500MeasurementReport.js"; import TID1501MeasurementGroup from "../../utilities/TID1500/TID1501MeasurementGroup.js"; +import Cornerstone3DCodingScheme from "./CodingScheme"; import { toArray, codeMeaningEquals } from "../helpers.js"; @@ -77,7 +78,82 @@ function getMeasurementGroup( export default class MeasurementReport { constructor() {} - static getSetupMeasurementData(MeasurementGroup) { + static getCornerstoneLabelFromDefaultState(defaultState) { + const { findingSites = [], finding } = defaultState; + + const cornersoneFreeTextCodingValue = + Cornerstone3DCodingScheme.codeValues.CORNERSTONEFREETEXT; + + let freeTextLabel = findingSites.find( + fs => fs.CodeValue === cornersoneFreeTextCodingValue + ); + + if (freeTextLabel) { + return freeTextLabel.CodeMeaning; + } + + if (finding && finding.CodeValue === cornersoneFreeTextCodingValue) { + return finding.CodeMeaning; + } + } + + static generateDatasetMeta() { + // TODO: what is the correct metaheader + // http://dicom.nema.org/medical/Dicom/current/output/chtml/part10/chapter_7.html + // TODO: move meta creation to happen in derivations.js + const fileMetaInformationVersionArray = new Uint8Array(2); + fileMetaInformationVersionArray[1] = 1; + + const _meta = { + FileMetaInformationVersion: { + Value: [fileMetaInformationVersionArray.buffer], + vr: "OB" + }, + //MediaStorageSOPClassUID + //MediaStorageSOPInstanceUID: sopCommonModule.sopInstanceUID, + TransferSyntaxUID: { + Value: ["1.2.840.10008.1.2.1"], + vr: "UI" + }, + ImplementationClassUID: { + Value: [DicomMetaDictionary.uid()], // TODO: could be git hash or other valid id + vr: "UI" + }, + ImplementationVersionName: { + Value: ["dcmjs"], + vr: "SH" + } + }; + + return _meta; + } + + static generateDerivationSourceDataset( + StudyInstanceUID, + SeriesInstanceUID + ) { + const _vrMap = { + PixelData: "OW" + }; + + const _meta = MeasurementReport.generateDatasetMeta(); + + const derivationSourceDataset = { + StudyInstanceUID, + SeriesInstanceUID, + _meta: _meta, + _vrMap: _vrMap + }; + + return derivationSourceDataset; + } + + static getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + toolType + ) { const { ContentSequence } = MeasurementGroup; const contentSequenceArr = toArray(ContentSequence); @@ -100,31 +176,43 @@ export default class MeasurementReport { ReferencedFrameNumber } = ReferencedSOPSequence; + const referencedImageId = + sopInstanceUIDToImageIdMap[ReferencedSOPInstanceUID]; + const imagePlaneModule = metadata.get( + "imagePlaneModule", + referencedImageId + ); + + const finding = findingGroup + ? findingGroup.ConceptCodeSequence[0] + : undefined; + const findingSites = findingSiteGroups.map(fsg => { + return { ...fsg.ConceptCodeSequence[0] }; + }); + const defaultState = { - sopInstanceUid: ReferencedSOPInstanceUID, - frameIndex: ReferencedFrameNumber || 1, - complete: true, - finding: findingGroup - ? findingGroup.ConceptCodeSequence - : undefined, - findingSites: findingSiteGroups.map(fsg => { - return { ...fsg.ConceptCodeSequence }; - }) + annotation: { + annotationUID: DicomMetaDictionary.uid(), + metadata: { + toolName: toolType, + referencedImageId, + FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID, + label: "" + } + }, + finding, + findingSites }; if (defaultState.finding) { defaultState.description = defaultState.finding.CodeMeaning; } - const findingSite = - defaultState.findingSites && defaultState.findingSites[0]; - if (findingSite) { - defaultState.location = - (findingSite[0] && findingSite[0].CodeMeaning) || - findingSite.CodeMeaning; - } + + defaultState.annotation.metadata.label = MeasurementReport.getCornerstoneLabelFromDefaultState( + defaultState + ); + return { defaultState, - findingGroup, - findingSiteGroups, NUMGroup, SCOORDGroup, ReferencedSOPSequence, @@ -142,10 +230,6 @@ export default class MeasurementReport { // ToolState for array of imageIDs to a Report // Assume Cornerstone metadata provider has access to Study / Series / Sop Instance UID let allMeasurementGroups = []; - const firstImageId = Object.keys(toolState)[0]; - if (!firstImageId) { - throw new Error("No measurements provided."); - } /* Patient ID Warning - Missing attribute or value that would be needed to build DICOMDIR - Patient ID @@ -153,12 +237,11 @@ export default class MeasurementReport { Warning - Missing attribute or value that would be needed to build DICOMDIR - Study Time Warning - Missing attribute or value that would be needed to build DICOMDIR - Study ID */ - const generalSeriesModule = metadataProvider.get( - "generalSeriesModule", - firstImageId - ); - //const sopCommonModule = metadataProvider.get('sopCommonModule', firstImageId); - const { studyInstanceUID, seriesInstanceUID } = generalSeriesModule; + + const sopInstanceUIDsToSeriesInstanceUIDMap = {}; + const derivationSourceDatasets = []; + + const _meta = MeasurementReport.generateDatasetMeta(); // Loop through each image in the toolData Object.keys(toolState).forEach(imageId => { @@ -166,18 +249,42 @@ export default class MeasurementReport { "sopCommonModule", imageId ); + const generalSeriesModule = metadataProvider.get( + "generalSeriesModule", + imageId + ); + + const { sopInstanceUID, sopClassUID } = sopCommonModule; + const { studyInstanceUID, seriesInstanceUID } = generalSeriesModule; + + sopInstanceUIDsToSeriesInstanceUIDMap[ + sopInstanceUID + ] = seriesInstanceUID; + + if ( + !derivationSourceDatasets.find( + dsd => dsd.SeriesInstanceUID === seriesInstanceUID + ) + ) { + // Entry not present for series, create one. + const derivationSourceDataset = MeasurementReport.generateDerivationSourceDataset( + studyInstanceUID, + seriesInstanceUID + ); + + derivationSourceDatasets.push(derivationSourceDataset); + } + const frameNumber = metadataProvider.get("frameNumber", imageId); const toolData = toolState[imageId]; const toolTypes = Object.keys(toolData); const ReferencedSOPSequence = { - ReferencedSOPClassUID: sopCommonModule.sopClassUID, - ReferencedSOPInstanceUID: sopCommonModule.sopInstanceUID + ReferencedSOPClassUID: sopClassUID, + ReferencedSOPInstanceUID: sopInstanceUID }; - if ( - Normalizer.isMultiframeSOPClassUID(sopCommonModule.sopClassUID) - ) { + if (Normalizer.isMultiframeSOPClassUID(sopClassUID)) { ReferencedSOPSequence.ReferencedFrameNumber = frameNumber; } @@ -201,55 +308,16 @@ export default class MeasurementReport { ); }); - const MeasurementReport = new TID1500MeasurementReport( + const tid1500MeasurementReport = new TID1500MeasurementReport( { TID1501MeasurementGroups: allMeasurementGroups }, options ); - // TODO: what is the correct metaheader - // http://dicom.nema.org/medical/Dicom/current/output/chtml/part10/chapter_7.html - // TODO: move meta creation to happen in derivations.js - const fileMetaInformationVersionArray = new Uint8Array(2); - fileMetaInformationVersionArray[1] = 1; - - const derivationSourceDataset = { - StudyInstanceUID: studyInstanceUID, - SeriesInstanceUID: seriesInstanceUID - //SOPInstanceUID: sopInstanceUID, // TODO: Necessary? - //SOPClassUID: sopClassUID, - }; - - const _meta = { - FileMetaInformationVersion: { - Value: [fileMetaInformationVersionArray.buffer], - vr: "OB" - }, - //MediaStorageSOPClassUID - //MediaStorageSOPInstanceUID: sopCommonModule.sopInstanceUID, - TransferSyntaxUID: { - Value: ["1.2.840.10008.1.2.1"], - vr: "UI" - }, - ImplementationClassUID: { - Value: [DicomMetaDictionary.uid()], // TODO: could be git hash or other valid id - vr: "UI" - }, - ImplementationVersionName: { - Value: ["dcmjs"], - vr: "SH" - } - }; - - const _vrMap = { - PixelData: "OW" - }; - - derivationSourceDataset._meta = _meta; - derivationSourceDataset._vrMap = _vrMap; + const report = new StructuredReport(derivationSourceDatasets); - const report = new StructuredReport([derivationSourceDataset]); - const contentItem = MeasurementReport.contentItem( - derivationSourceDataset + const contentItem = tid1500MeasurementReport.contentItem( + derivationSourceDatasets, + { sopInstanceUIDsToSeriesInstanceUIDMap } ); // Merge the derived dataset with the content from the Measurement Report @@ -268,7 +336,7 @@ export default class MeasurementReport { */ static generateToolState( dataset, - imageIds, + sopInstanceUIDToImageIdMap, imageToWorldCoords, metadata, hooks = {} @@ -308,8 +376,6 @@ export default class MeasurementReport { }); measurementGroups.forEach((measurementGroup, index) => { - const imageId = imageIds[index]; - const measurementGroupContentSequence = toArray( measurementGroup.ContentSequence ); @@ -337,7 +403,7 @@ export default class MeasurementReport { if (toolClass) { const measurement = toolClass.getMeasurementData( measurementGroup, - imageId, + sopInstanceUIDToImageIdMap, imageToWorldCoords, metadata ); diff --git a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.js b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.js new file mode 100644 index 0000000000..33b4a47eaf --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.js @@ -0,0 +1,141 @@ +import MeasurementReport from "./MeasurementReport"; +import TID300Polyline from "../../utilities/TID300/Polyline"; +import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; +import { vec3 } from "gl-matrix"; + +const PLANARFREEHANDROI = "PlanarFreehandROI"; +const perimeterCodeValue = "131191004"; +const sctCodingSchemeDesignator = "SCT"; +const polylineGraphicType = "POLYLINE"; +const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${PLANARFREEHANDROI}`; +const closedContourThreshold = 1e-5; + +class PlanarFreehandROI { + constructor() {} + + static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata + ) { + const { + defaultState, + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + PlanarFreehandROI.toolType + ); + + const referencedImageId = + defaultState.annotation.metadata.referencedImageId; + const { GraphicData } = SCOORDGroup; + + const worldCoords = []; + + for (let i = 0; i < GraphicData.length; i += 2) { + const point = imageToWorldCoords(referencedImageId, [ + GraphicData[i], + GraphicData[i + 1] + ]); + + worldCoords.push(point); + } + + const distanceBetweenFirstAndLastPoint = vec3.distance( + worldCoords[worldCoords.length - 1], + worldCoords[0] + ); + + let isOpenContour = true; + + // If the contour is closed, this should have been encoded as exactly the same point, so check for a very small difference. + if (distanceBetweenFirstAndLastPoint < closedContourThreshold) { + worldCoords.pop(); // Remove the last element which is duplicated. + + isOpenContour = false; + } + + let points = []; + + if (isOpenContour) { + points.push(worldCoords[0], worldCoords[worldCoords.length - 1]); + } + + const state = defaultState; + + state.annotation.data = { + polyline: worldCoords, + isOpenContour, + handles: { + points, + activeHandleIndex: null, + textBox: { + hasMoved: false + } + } + }; + + return state; + } + + static getTID300RepresentationArguments(tool, worldToImageCoords) { + const { data, finding, findingSites, metadata } = tool; + const { isOpenContour, polyline } = data; + + const { referencedImageId } = metadata; + + if (!referencedImageId) { + throw new Error( + "PlanarFreehandROI.getTID300RepresentationArguments: referencedImageId is not defined" + ); + } + + const points = polyline.map(worldPos => + worldToImageCoords(referencedImageId, worldPos) + ); + + if (!isOpenContour) { + // Need to repeat the first point at the end of to have an explicitly closed contour. + const lastPoint = points[points.length - 1]; + + // Explicitly expand to avoid ciruclar references. + points.push([lastPoint[0], lastPoint[1]]); + } + + const area = 0; // TODO -> The tool doesn't have these stats yet. + const perimeter = 0; + + return { + points, + area, + perimeter, + trackingIdentifierTextValue, + finding, + findingSites: findingSites || [] + }; + } +} + +PlanarFreehandROI.toolType = PLANARFREEHANDROI; +PlanarFreehandROI.utilityToolType = PLANARFREEHANDROI; +PlanarFreehandROI.TID300Representation = TID300Polyline; +PlanarFreehandROI.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { + if (!TrackingIdentifier.includes(":")) { + return false; + } + + const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); + + if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { + return false; + } + + return toolType === PLANARFREEHANDROI; +}; + +MeasurementReport.registerTool(PlanarFreehandROI); + +export default PlanarFreehandROI; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Probe.js b/packages/adapters/src/adapters/Cornerstone3D/Probe.js new file mode 100644 index 0000000000..7cd1f012fc --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/Probe.js @@ -0,0 +1,107 @@ +import MeasurementReport from "./MeasurementReport.js"; +import TID300Point from "../../utilities/TID300/Point.js"; +import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; + +const PROBE = "Probe"; +const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${PROBE}`; + +class Probe { + constructor() {} + + static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata + ) { + const { + defaultState, + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + Probe.toolType + ); + + const referencedImageId = + defaultState.annotation.metadata.referencedImageId; + + const { GraphicData } = SCOORDGroup; + + const worldCoords = []; + for (let i = 0; i < GraphicData.length; i += 2) { + const point = imageToWorldCoords(referencedImageId, [ + GraphicData[i], + GraphicData[i + 1] + ]); + worldCoords.push(point); + } + + const state = defaultState; + + state.annotation.data = { + handles: { + points: worldCoords, + activeHandleIndex: null, + textBox: { + hasMoved: false + } + } + }; + + return state; + } + + static getTID300RepresentationArguments(tool, worldToImageCoords) { + const { data, metadata } = tool; + let { finding, findingSites } = tool; + const { referencedImageId } = metadata; + + if (!referencedImageId) { + throw new Error( + "Probe.getTID300RepresentationArguments: referencedImageId is not defined" + ); + } + + const { points } = data.handles; + + const pointsImage = points.map(point => { + const pointImage = worldToImageCoords(referencedImageId, point); + return { + x: pointImage[0], + y: pointImage[1] + }; + }); + + const TID300RepresentationArguments = { + points: pointsImage, + trackingIdentifierTextValue, + findingSites: findingSites || [], + finding + }; + + return TID300RepresentationArguments; + } +} + +Probe.toolType = PROBE; +Probe.utilityToolType = PROBE; +Probe.TID300Representation = TID300Point; +Probe.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { + if (!TrackingIdentifier.includes(":")) { + return false; + } + + const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); + + if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { + return false; + } + + return toolType === PROBE; +}; + +MeasurementReport.registerTool(Probe); + +export default Probe; diff --git a/packages/adapters/src/adapters/Cornerstone3D/index.js b/packages/adapters/src/adapters/Cornerstone3D/index.js index 2030209753..d93a2f2f5f 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/index.js +++ b/packages/adapters/src/adapters/Cornerstone3D/index.js @@ -3,6 +3,8 @@ import Length from "./Length.js"; import Bidirectional from "./Bidirectional.js"; import EllipticalROI from "./EllipticalROI.js"; import ArrowAnnotate from "./ArrowAnnotate.js"; +import Probe from "./Probe.js"; +import PlanarFreehandROI from "./PlanarFreehandROI.js"; import CodeScheme from "./CodingScheme"; const Cornerstone3D = { @@ -10,6 +12,8 @@ const Cornerstone3D = { Bidirectional, EllipticalROI, ArrowAnnotate, + Probe, + PlanarFreehandROI, MeasurementReport, CodeScheme }; diff --git a/packages/adapters/src/utilities/TID1500/TID1500MeasurementReport.js b/packages/adapters/src/utilities/TID1500/TID1500MeasurementReport.js index aa7823492b..69bb357798 100644 --- a/packages/adapters/src/utilities/TID1500/TID1500MeasurementReport.js +++ b/packages/adapters/src/utilities/TID1500/TID1500MeasurementReport.js @@ -114,23 +114,41 @@ export default class TID1500MeasurementReport { validate() {} - contentItem(derivationSourceDataset, options = {}) { + contentItem(derivationSourceDatasetOrDatasets, options = {}) { if (options.PersonName) { this.PersonObserverName.PersonName = options.PersonName; } + // Note this is left in for compatibility with the Cornerstone Legacy adapter which only supports one series for now. + const derivationSourceDatasets = Array.isArray( + derivationSourceDatasetOrDatasets + ) + ? derivationSourceDatasetOrDatasets + : [derivationSourceDatasetOrDatasets]; + // Add the Measurement Groups to the Measurement Report - this.addTID1501MeasurementGroups(derivationSourceDataset, options); + this.addTID1501MeasurementGroups(derivationSourceDatasets, options); return this.tid1500; } - addTID1501MeasurementGroups(derivationSourceDataset, options) { + addTID1501MeasurementGroups(derivationSourceDatasets, options = {}) { const { CurrentRequestedProcedureEvidenceSequence, ImageLibraryContentSequence } = this; + const { sopInstanceUIDsToSeriesInstanceUIDMap } = options; + + if ( + derivationSourceDatasets.length > 1 && + sopInstanceUIDsToSeriesInstanceUIDMap === undefined + ) { + throw new Error( + `addTID1501MeasurementGroups provided with ${derivationSourceDatasets.length} derivationSourceDatasets, with no sopInstanceUIDsToSeriesInstanceUIDMap in options.` + ); + } + const { TID1501MeasurementGroups } = this.TIDIncludeGroups; if (!TID1501MeasurementGroups) { @@ -161,6 +179,27 @@ export default class TID1500MeasurementReport { ReferencedSOPSequence: measurement.ReferencedSOPSequence }); + let derivationSourceDataset; + + if (derivationSourceDatasets.length === 1) { + // If there is only one derivationSourceDataset, use it. + derivationSourceDataset[0]; + } else { + const SeriesInstanceUID = + sopInstanceUIDsToSeriesInstanceUIDMap[ + ReferencedSOPInstanceUID + ]; + + derivationSourceDataset = derivationSourceDatasets.find( + dsd => dsd.SeriesInstanceUID === SeriesInstanceUID + ); + } + + /** + * Note: the VM of the ReferencedSeriesSequence and ReferencedSOPSequence are 1, so + * it is correct that we have a full `CurrentRequestedProcedureEvidenceSequence` + * item per `SOPInstanceUID`. + */ CurrentRequestedProcedureEvidenceSequence.push({ StudyInstanceUID: derivationSourceDataset.StudyInstanceUID,