From 566b4cac89021dedd4b8c65d9b0e2de6000451b1 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Fri, 2 Apr 2021 00:59:15 -0700 Subject: [PATCH 01/19] get trackingUniqueIdentifier from props to transfer tracking unique identifier from AIM --- src/utilities/TID300/TID300Measurement.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utilities/TID300/TID300Measurement.js b/src/utilities/TID300/TID300Measurement.js index ae4ef1ff..4aecf0f2 100644 --- a/src/utilities/TID300/TID300Measurement.js +++ b/src/utilities/TID300/TID300Measurement.js @@ -16,8 +16,10 @@ export default class TID300Measurement { } getTrackingGroups() { - let { trackingIdentifierTextValue } = this.props; - + let { + trackingIdentifierTextValue, + trackingUniqueIdentifier + } = this.props; return [ { RelationshipType: "HAS OBS CONTEXT", @@ -37,7 +39,7 @@ export default class TID300Measurement { CodingSchemeDesignator: "DCM", CodeMeaning: "Tracking Unique Identifier" }, - UID: DicomMetaDictionary.uid() + UID: trackingUniqueIdentifier || DicomMetaDictionary.uid() } ]; } From b94f2dbdf53247a13814b7d39cd5eefe1842eab1 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Fri, 2 Apr 2021 01:04:11 -0700 Subject: [PATCH 02/19] add quantitative evaluations from options --- src/utilities/TID1500/TID1500MeasurementReport.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/utilities/TID1500/TID1500MeasurementReport.js b/src/utilities/TID1500/TID1500MeasurementReport.js index aa782349..5fdbb65a 100644 --- a/src/utilities/TID1500/TID1500MeasurementReport.js +++ b/src/utilities/TID1500/TID1500MeasurementReport.js @@ -190,5 +190,20 @@ export default class TID1500MeasurementReport { }; this.tid1500.ContentSequence.push(ImagingMeasurments); + + if (options.quantitativeEvaluations) { + const quantitativeEvaluations = { + RelationshipType: "CONTAINS", + ValueType: "CONTAINER", + ConceptNameCodeSequence: { + CodeValue: "C0034375", + CodingSchemeDesignator: "UMLS", + CodeMeaning: "Qualitative Evaluations" + }, + ContinuityOfContent: "SEPARATE", + ContentSequence: options.quantitativeEvaluations + }; + this.tid1500.ContentSequence.push(quantitativeEvaluations); + } } } From faba7fb9305bf8de953da55b14a9f0a1645e3739 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Mon, 5 Apr 2021 17:22:36 -0700 Subject: [PATCH 03/19] pass options to content item --- src/adapters/Cornerstone/MeasurementReport.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/adapters/Cornerstone/MeasurementReport.js b/src/adapters/Cornerstone/MeasurementReport.js index d20afc60..6b90b246 100644 --- a/src/adapters/Cornerstone/MeasurementReport.js +++ b/src/adapters/Cornerstone/MeasurementReport.js @@ -160,7 +160,8 @@ export default class MeasurementReport { const report = new StructuredReport([derivationSourceDataset]); const contentItem = MeasurementReport.contentItem( - derivationSourceDataset + derivationSourceDataset, + options ); // Merge the derived dataset with the content from the Measurement Report From bcda3533c73e9bc15b1f31278c44f85592aa7905 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Mon, 5 Apr 2021 18:47:48 -0700 Subject: [PATCH 04/19] add support for getting procedure reported from options and correct the default value according to DICOM standard --- .../TID1500/TID1500MeasurementReport.js | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/utilities/TID1500/TID1500MeasurementReport.js b/src/utilities/TID1500/TID1500MeasurementReport.js index 5fdbb65a..9ff1f48e 100644 --- a/src/utilities/TID1500/TID1500MeasurementReport.js +++ b/src/utilities/TID1500/TID1500MeasurementReport.js @@ -19,6 +19,22 @@ export default class TID1500MeasurementReport { PersonName: "unknown^unknown" }; + this.ProcedureReported = { + RelationshipType: "HAS CONCEPT MOD", + ValueType: "CODE", + ConceptNameCodeSequence: { + CodeValue: "121058", + CodingSchemeDesignator: "DCM", + CodeMeaning: "Procedure reported" + }, + // The default per PS3.21 + ConceptCodeSequence: { + CodeValue: "P0-0099A", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Imaging procedure" + } + }; + this.tid1500 = { ConceptNameCodeSequence: { CodeValue: "126000", @@ -73,20 +89,7 @@ export default class TID1500MeasurementReport { } }, this.PersonObserverName, - { - RelationshipType: "HAS CONCEPT MOD", - ValueType: "CODE", - ConceptNameCodeSequence: { - CodeValue: "121058", - CodingSchemeDesignator: "DCM", - CodeMeaning: "Procedure reported" - }, - ConceptCodeSequence: { - CodeValue: "1", - CodingSchemeDesignator: "99dcmjs", - CodeMeaning: "Unknown procedure" - } - }, + this.ProcedureReported, { RelationshipType: "CONTAINS", ValueType: "CONTAINER", @@ -118,7 +121,10 @@ export default class TID1500MeasurementReport { if (options.PersonName) { this.PersonObserverName.PersonName = options.PersonName; } - + if (options.ProcedureReported) { + this.ProcedureReported.ConceptCodeSequence = + options.ProcedureReported; + } // Add the Measurement Groups to the Measurement Report this.addTID1501MeasurementGroups(derivationSourceDataset, options); From 8d47565e96823bcd0f02096d6c33028a58a73f68 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Mon, 5 Apr 2021 20:28:42 -0700 Subject: [PATCH 05/19] remove extra tooltype --- src/adapters/Cornerstone/Bidirectional.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/adapters/Cornerstone/Bidirectional.js b/src/adapters/Cornerstone/Bidirectional.js index 7650287f..d8e827fd 100644 --- a/src/adapters/Cornerstone/Bidirectional.js +++ b/src/adapters/Cornerstone/Bidirectional.js @@ -129,7 +129,6 @@ class Bidirectional { isCreating: false, longestDiameter, shortestDiameter, - toolType: "Bidirectional", toolName: "Bidirectional", visible: true, finding: findingGroup From 2d57421b2c70f4696937ce44bf9b53e21cb9444f Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Mon, 5 Apr 2021 21:49:05 -0700 Subject: [PATCH 06/19] add comment --- src/utilities/TID300/TID300Measurement.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/utilities/TID300/TID300Measurement.js b/src/utilities/TID300/TID300Measurement.js index 4aecf0f2..6473f6f7 100644 --- a/src/utilities/TID300/TID300Measurement.js +++ b/src/utilities/TID300/TID300Measurement.js @@ -11,10 +11,29 @@ export default class TID300Measurement { ...this.getTrackingGroups(), ...this.getFindingGroup(), ...this.getFindingSiteGroups(), + ...this.getComment(), ...contentSequenceEntries ]; } + getComment() { + let { comment } = this.props; + return comment + ? [ + { + RelationshipType: "CONTAINS", + ValueType: "TEXT", + ConceptNameCodeSequence: { + CodeValue: "121106", + CodingSchemeDesignator: "DCM", + CodeMeaning: "Comment" + }, + TextValue: comment + } + ] + : []; + } + getTrackingGroups() { let { trackingIdentifierTextValue, From 69b77fc743ff9506169125dcc20e53519470be10 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Mon, 5 Apr 2021 22:13:36 -0700 Subject: [PATCH 07/19] add comment to Bidirectional --- src/adapters/Cornerstone/Bidirectional.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/adapters/Cornerstone/Bidirectional.js b/src/adapters/Cornerstone/Bidirectional.js index d8e827fd..bf951740 100644 --- a/src/adapters/Cornerstone/Bidirectional.js +++ b/src/adapters/Cornerstone/Bidirectional.js @@ -8,6 +8,7 @@ const LONG_AXIS = "Long Axis"; const SHORT_AXIS = "Short Axis"; const FINDING = "121071"; const FINDING_SITE = "G-C0E3"; +const COMMENT = "121106"; class Bidirectional { constructor() {} @@ -24,6 +25,10 @@ class Bidirectional { group => group.ConceptNameCodeSequence.CodeValue === FINDING_SITE ); + const comment = toArray(ContentSequence).find( + group => group.ConceptNameCodeSequence.CodeValue === COMMENT + ); + const longAxisNUMGroup = toArray(ContentSequence).find( group => group.ConceptNameCodeSequence.CodeMeaning === LONG_AXIS ); @@ -136,7 +141,8 @@ class Bidirectional { : undefined, findingSites: findingSiteGroups.map(fsg => { return { ...fsg.ConceptCodeSequence }; - }) + }), + comment: comment ? comment.TextValue : undefined }; return state; @@ -153,7 +159,8 @@ class Bidirectional { shortestDiameter, longestDiameter, finding, - findingSites + findingSites, + comment } = tool; const trackingIdentifierTextValue = @@ -172,7 +179,8 @@ class Bidirectional { shortAxisLength: shortestDiameter, trackingIdentifierTextValue, finding: finding, - findingSites: findingSites || [] + findingSites: findingSites || [], + comment: comment }; } } From f9d483bc2841552638bf97a6a6636f7d09b4cc63 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Mon, 5 Apr 2021 23:55:19 -0700 Subject: [PATCH 08/19] add patient info --- src/adapters/Cornerstone/MeasurementReport.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/adapters/Cornerstone/MeasurementReport.js b/src/adapters/Cornerstone/MeasurementReport.js index 6b90b246..08a0000e 100644 --- a/src/adapters/Cornerstone/MeasurementReport.js +++ b/src/adapters/Cornerstone/MeasurementReport.js @@ -69,8 +69,18 @@ export default class MeasurementReport { "generalSeriesModule", firstImageId ); + const patientModule = metadataProvider.get( + "patientModule", + firstImageId + ); //const sopCommonModule = metadataProvider.get('sopCommonModule', firstImageId); const { studyInstanceUID, seriesInstanceUID } = generalSeriesModule; + const { + patientID, + patientName, + patientBirthDate, + patientSex + } = patientModule; // Loop through each image in the toolData Object.keys(toolState).forEach(imageId => { @@ -125,9 +135,13 @@ export default class MeasurementReport { const derivationSourceDataset = { StudyInstanceUID: studyInstanceUID, - SeriesInstanceUID: seriesInstanceUID + SeriesInstanceUID: seriesInstanceUID, //SOPInstanceUID: sopInstanceUID, // TODO: Necessary? //SOPClassUID: sopClassUID, + PatientID: patientID, + PatientName: patientName, + PatientBirthDate: patientBirthDate, + PatientSex: patientSex }; const _meta = { From 1118002e544edeb899f2cd90f72b9772294e91d8 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Tue, 6 Apr 2021 12:31:22 -0700 Subject: [PATCH 09/19] fix qualitative typo --- src/utilities/TID1500/TID1500MeasurementReport.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utilities/TID1500/TID1500MeasurementReport.js b/src/utilities/TID1500/TID1500MeasurementReport.js index 9ff1f48e..7c84397b 100644 --- a/src/utilities/TID1500/TID1500MeasurementReport.js +++ b/src/utilities/TID1500/TID1500MeasurementReport.js @@ -197,8 +197,8 @@ export default class TID1500MeasurementReport { this.tid1500.ContentSequence.push(ImagingMeasurments); - if (options.quantitativeEvaluations) { - const quantitativeEvaluations = { + if (options.qualitativeEvaluations) { + const qualitativeEvaluations = { RelationshipType: "CONTAINS", ValueType: "CONTAINER", ConceptNameCodeSequence: { @@ -207,9 +207,9 @@ export default class TID1500MeasurementReport { CodeMeaning: "Qualitative Evaluations" }, ContinuityOfContent: "SEPARATE", - ContentSequence: options.quantitativeEvaluations + ContentSequence: options.qualitativeEvaluations }; - this.tid1500.ContentSequence.push(quantitativeEvaluations); + this.tid1500.ContentSequence.push(qualitativeEvaluations); } } } From 2f3180f6968e3dd0f9743a0164adee542d513362 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Thu, 6 May 2021 19:22:55 -0700 Subject: [PATCH 10/19] fix finding site relationship type --- src/utilities/TID300/TID300Measurement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/TID300/TID300Measurement.js b/src/utilities/TID300/TID300Measurement.js index 6473f6f7..6f573de1 100644 --- a/src/utilities/TID300/TID300Measurement.js +++ b/src/utilities/TID300/TID300Measurement.js @@ -100,7 +100,7 @@ export default class TID300Measurement { CodeMeaning } = findingSite; return { - RelationshipType: "CONTAINS", + RelationshipType: "HAS CONCEPT MOD", ValueType: "CODE", ConceptNameCodeSequence: { CodeValue: "G-C0E3", From f35eb474d0a72141b5b35704f80522f2b8beb6b3 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Tue, 11 May 2021 12:24:00 -0700 Subject: [PATCH 11/19] get identifier text value from tool if exists --- src/adapters/Cornerstone/ArrowAnnotate.js | 5 +++-- src/adapters/Cornerstone/Bidirectional.js | 5 +++-- src/adapters/Cornerstone/EllipticalRoi.js | 10 ++++++++-- src/adapters/Cornerstone/Length.js | 5 +++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/adapters/Cornerstone/ArrowAnnotate.js b/src/adapters/Cornerstone/ArrowAnnotate.js index bccb7084..42381845 100644 --- a/src/adapters/Cornerstone/ArrowAnnotate.js +++ b/src/adapters/Cornerstone/ArrowAnnotate.js @@ -85,11 +85,12 @@ class ArrowAnnotate { static getTID300RepresentationArguments(tool) { const points = [tool.handles.start]; - let { finding, findingSites } = tool; + let { finding, findingSites, identifier } = tool; const TID300RepresentationArguments = { points, - trackingIdentifierTextValue: `cornerstoneTools@^4.0.0:ArrowAnnotate`, + trackingIdentifierTextValue: + identifier || `cornerstoneTools@^4.0.0:ArrowAnnotate`, findingSites: findingSites || [] }; diff --git a/src/adapters/Cornerstone/Bidirectional.js b/src/adapters/Cornerstone/Bidirectional.js index bf951740..8e20d5b3 100644 --- a/src/adapters/Cornerstone/Bidirectional.js +++ b/src/adapters/Cornerstone/Bidirectional.js @@ -160,11 +160,12 @@ class Bidirectional { longestDiameter, finding, findingSites, - comment + comment, + identifier } = tool; const trackingIdentifierTextValue = - "cornerstoneTools@^4.0.0:Bidirectional"; + identifier || "cornerstoneTools@^4.0.0:Bidirectional"; return { longAxis: { diff --git a/src/adapters/Cornerstone/EllipticalRoi.js b/src/adapters/Cornerstone/EllipticalRoi.js index c0e3c6b9..ae4273a4 100644 --- a/src/adapters/Cornerstone/EllipticalRoi.js +++ b/src/adapters/Cornerstone/EllipticalRoi.js @@ -115,7 +115,13 @@ class EllipticalRoi { } static getTID300RepresentationArguments(tool) { - const { cachedStats, handles, finding, findingSites } = tool; + const { + cachedStats, + handles, + finding, + findingSites, + identifier + } = tool; const { start, end } = handles; const { area } = cachedStats; @@ -145,7 +151,7 @@ class EllipticalRoi { } const trackingIdentifierTextValue = - "cornerstoneTools@^4.0.0:EllipticalRoi"; + identifier || "cornerstoneTools@^4.0.0:EllipticalRoi"; return { area, diff --git a/src/adapters/Cornerstone/Length.js b/src/adapters/Cornerstone/Length.js index 517e5602..9b0f579e 100644 --- a/src/adapters/Cornerstone/Length.js +++ b/src/adapters/Cornerstone/Length.js @@ -70,12 +70,13 @@ class Length { } static getTID300RepresentationArguments(tool) { - const { handles, finding, findingSites } = tool; + const { handles, finding, findingSites, identifier } = tool; const point1 = handles.start; const point2 = handles.end; const distance = tool.length; - const trackingIdentifierTextValue = "cornerstoneTools@^4.0.0:Length"; + const trackingIdentifierTextValue = + identifier || "cornerstoneTools@^4.0.0:Length"; return { point1, From dec70716e129ea2593602eee52564166ec218ded Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Fri, 21 May 2021 20:53:10 -0700 Subject: [PATCH 12/19] modify Long Axis, Short Axis and Length to use SCT codes instead of SRT (David Clunie's recommendation) --- src/utilities/TID300/Bidirectional.js | 8 ++++---- src/utilities/TID300/Length.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utilities/TID300/Bidirectional.js b/src/utilities/TID300/Bidirectional.js index bd7dd476..6f5b3ecb 100644 --- a/src/utilities/TID300/Bidirectional.js +++ b/src/utilities/TID300/Bidirectional.js @@ -16,8 +16,8 @@ export default class Bidirectional extends TID300Measurement { RelationshipType: "CONTAINS", ValueType: "NUM", ConceptNameCodeSequence: { - CodeValue: "G-A185", - CodingSchemeDesignator: "SRT", + CodeValue: "103339001", // David Clunie's recommendation + CodingSchemeDesignator: "SCT", CodeMeaning: "Long Axis" }, MeasuredValueSequence: { @@ -50,8 +50,8 @@ export default class Bidirectional extends TID300Measurement { RelationshipType: "CONTAINS", ValueType: "NUM", ConceptNameCodeSequence: { - CodeValue: "G-A186", - CodingSchemeDesignator: "SRT", + CodeValue: "103340004", // David Clunie's recommendation + CodingSchemeDesignator: "SCT", CodeMeaning: "Short Axis" }, MeasuredValueSequence: { diff --git a/src/utilities/TID300/Length.js b/src/utilities/TID300/Length.js index bc418f4d..a5d1d7fa 100644 --- a/src/utilities/TID300/Length.js +++ b/src/utilities/TID300/Length.js @@ -10,8 +10,8 @@ export default class Length extends TID300Measurement { RelationshipType: "CONTAINS", ValueType: "NUM", ConceptNameCodeSequence: { - CodeValue: "G-D7FE", - CodingSchemeDesignator: "SRT", + CodeValue: "410668003", // David Clunie's recommendation + CodingSchemeDesignator: "SCT", CodeMeaning: "Length" }, MeasuredValueSequence: { From ebafd0533b08c3beae09eb0a8ec0849abd8090ee Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Wed, 26 May 2021 18:01:26 -0700 Subject: [PATCH 13/19] change the name of the identifier to trackingIdentifier --- src/adapters/Cornerstone/ArrowAnnotate.js | 4 ++-- src/adapters/Cornerstone/EllipticalRoi.js | 4 ++-- src/adapters/Cornerstone/Length.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/adapters/Cornerstone/ArrowAnnotate.js b/src/adapters/Cornerstone/ArrowAnnotate.js index 42381845..8fd05a47 100644 --- a/src/adapters/Cornerstone/ArrowAnnotate.js +++ b/src/adapters/Cornerstone/ArrowAnnotate.js @@ -85,12 +85,12 @@ class ArrowAnnotate { static getTID300RepresentationArguments(tool) { const points = [tool.handles.start]; - let { finding, findingSites, identifier } = tool; + let { finding, findingSites, trackingIdentifier } = tool; const TID300RepresentationArguments = { points, trackingIdentifierTextValue: - identifier || `cornerstoneTools@^4.0.0:ArrowAnnotate`, + trackingIdentifier || `cornerstoneTools@^4.0.0:ArrowAnnotate`, findingSites: findingSites || [] }; diff --git a/src/adapters/Cornerstone/EllipticalRoi.js b/src/adapters/Cornerstone/EllipticalRoi.js index ae4273a4..7076848d 100644 --- a/src/adapters/Cornerstone/EllipticalRoi.js +++ b/src/adapters/Cornerstone/EllipticalRoi.js @@ -120,7 +120,7 @@ class EllipticalRoi { handles, finding, findingSites, - identifier + trackingIdentifier } = tool; const { start, end } = handles; const { area } = cachedStats; @@ -151,7 +151,7 @@ class EllipticalRoi { } const trackingIdentifierTextValue = - identifier || "cornerstoneTools@^4.0.0:EllipticalRoi"; + trackingIdentifier || "cornerstoneTools@^4.0.0:EllipticalRoi"; return { area, diff --git a/src/adapters/Cornerstone/Length.js b/src/adapters/Cornerstone/Length.js index 9b0f579e..968961fb 100644 --- a/src/adapters/Cornerstone/Length.js +++ b/src/adapters/Cornerstone/Length.js @@ -70,13 +70,13 @@ class Length { } static getTID300RepresentationArguments(tool) { - const { handles, finding, findingSites, identifier } = tool; + const { handles, finding, findingSites, trackingIdentifier } = tool; const point1 = handles.start; const point2 = handles.end; const distance = tool.length; const trackingIdentifierTextValue = - identifier || "cornerstoneTools@^4.0.0:Length"; + trackingIdentifier || "cornerstoneTools@^4.0.0:Length"; return { point1, From 77c67883e3b8b08270ce78ee163df8a92165f1e8 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Wed, 26 May 2021 18:02:48 -0700 Subject: [PATCH 14/19] change the name of the identifier to trackingIdentifier, handle reading the tracking identifier and tracking unique identifier from the report and adding it to the tool --- src/adapters/Cornerstone/Bidirectional.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/adapters/Cornerstone/Bidirectional.js b/src/adapters/Cornerstone/Bidirectional.js index 8e20d5b3..dd77c3e0 100644 --- a/src/adapters/Cornerstone/Bidirectional.js +++ b/src/adapters/Cornerstone/Bidirectional.js @@ -9,6 +9,8 @@ const SHORT_AXIS = "Short Axis"; const FINDING = "121071"; const FINDING_SITE = "G-C0E3"; const COMMENT = "121106"; +const TRACKING_IDENTIFIER = "112039"; +const TRACKING_UNIQUE_IDENTIFIER = "112040"; class Bidirectional { constructor() {} @@ -29,6 +31,17 @@ class Bidirectional { group => group.ConceptNameCodeSequence.CodeValue === COMMENT ); + const trackingIdentifier = toArray(ContentSequence).find( + group => + group.ConceptNameCodeSequence.CodeValue === TRACKING_IDENTIFIER + ); + + const trackingUniqueIdentifier = toArray(ContentSequence).find( + group => + group.ConceptNameCodeSequence.CodeValue === + TRACKING_UNIQUE_IDENTIFIER + ); + const longAxisNUMGroup = toArray(ContentSequence).find( group => group.ConceptNameCodeSequence.CodeMeaning === LONG_AXIS ); @@ -142,7 +155,9 @@ class Bidirectional { findingSites: findingSiteGroups.map(fsg => { return { ...fsg.ConceptCodeSequence }; }), - comment: comment ? comment.TextValue : undefined + comment: comment ? comment.TextValue : undefined, + trackingIdentifier: trackingIdentifier.TextValue, + trackingUniqueIdentifier: trackingUniqueIdentifier.UID }; return state; @@ -161,11 +176,11 @@ class Bidirectional { finding, findingSites, comment, - identifier + trackingIdentifier } = tool; const trackingIdentifierTextValue = - identifier || "cornerstoneTools@^4.0.0:Bidirectional"; + trackingIdentifier || "cornerstoneTools@^4.0.0:Bidirectional"; return { longAxis: { From fc5c6fb17bc6d3fca846f087a953c7f4290d7897 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Wed, 26 May 2021 18:21:27 -0700 Subject: [PATCH 15/19] pass options to StructuredReport --- src/adapters/Cornerstone/MeasurementReport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/Cornerstone/MeasurementReport.js b/src/adapters/Cornerstone/MeasurementReport.js index 08a0000e..a180e87f 100644 --- a/src/adapters/Cornerstone/MeasurementReport.js +++ b/src/adapters/Cornerstone/MeasurementReport.js @@ -172,7 +172,7 @@ export default class MeasurementReport { derivationSourceDataset._meta = _meta; derivationSourceDataset._vrMap = _vrMap; - const report = new StructuredReport([derivationSourceDataset]); + const report = new StructuredReport([derivationSourceDataset], options); const contentItem = MeasurementReport.contentItem( derivationSourceDataset, options From e037f7e17e78fb6679f4f0fa9e6a29156972a458 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Fri, 28 May 2021 22:59:02 -0700 Subject: [PATCH 16/19] add the tracking unique identifier to the TID300 representation --- src/adapters/Cornerstone/Bidirectional.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/adapters/Cornerstone/Bidirectional.js b/src/adapters/Cornerstone/Bidirectional.js index dd77c3e0..abd2ea53 100644 --- a/src/adapters/Cornerstone/Bidirectional.js +++ b/src/adapters/Cornerstone/Bidirectional.js @@ -176,7 +176,8 @@ class Bidirectional { finding, findingSites, comment, - trackingIdentifier + trackingIdentifier, + trackingUniqueIdentifier } = tool; const trackingIdentifierTextValue = @@ -194,6 +195,7 @@ class Bidirectional { longAxisLength: longestDiameter, shortAxisLength: shortestDiameter, trackingIdentifierTextValue, + trackingUniqueIdentifier, finding: finding, findingSites: findingSites || [], comment: comment From 651b6bb2506b074b2bc9b9942ddd3e4c15cdd6af Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Thu, 1 Jul 2021 02:07:07 -0700 Subject: [PATCH 17/19] add statistics and points support to Freehand --- src/adapters/Cornerstone/Freehand.js | 166 +++++++++++++++++++--- src/utilities/TID300/Polyline.js | 199 +++++++++++++++++---------- src/utilities/index.js | 75 +++++++++- 3 files changed, 347 insertions(+), 93 deletions(-) diff --git a/src/adapters/Cornerstone/Freehand.js b/src/adapters/Cornerstone/Freehand.js index 4c695662..04c278a7 100644 --- a/src/adapters/Cornerstone/Freehand.js +++ b/src/adapters/Cornerstone/Freehand.js @@ -1,38 +1,158 @@ import MeasurementReport from "./MeasurementReport"; import TID300Polyline from "../../utilities/TID300/Polyline"; import CORNERSTONE_4_TAG from "./cornerstone4Tag"; +import { toArray } from "../helpers.js"; + +const FREEHAND = "Freehand"; +const FINDING = "121071"; +const FINDING_SITE = "G-C0E3"; +const COMMENT = "121106"; +const TRACKING_IDENTIFIER = "112039"; +const TRACKING_UNIQUE_IDENTIFIER = "112040"; + +const statNameMap = { + Minimum: "min", + Maximum: "max", + Mean: "mean", + "Standard Deviation": "stdDev" +}; class Freehand { constructor() {} - static measurementContentToLengthState(groupItemContent) { - const content = groupItemContent.ContentSequence; - const { ReferencedSOPSequence } = content.ContentSequence; - const { - ReferencedSOPInstanceUID, - ReferencedFrameNumber - } = ReferencedSOPSequence; - const state = { - sopInstanceUid: ReferencedSOPInstanceUID, - frameIndex: ReferencedFrameNumber || 0, - toolType: Freehand.toolType + static parseNumComponent(num) { + const SCOORDGroup = toArray(num.ContentSequence).find( + group => group.ValueType === "SCOORD" + ); + const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence; + return { + type: num.ConceptNameCodeSequence.CodeMeaning, + value: num.MeasuredValueSequence.NumericValue, + points: Freehand.extractPoints(SCOORDGroup.GraphicData), + ReferencedSOPSequence: ReferencedSOPSequence }; + } + + static parseNumGroup(numGroup) { + const stats = {}; + const scoords = []; + const refs = []; + + numGroup.forEach(num => { + const { + type, + value, + points, + ReferencedSOPSequence + } = Freehand.parseNumComponent(num); + scoords.push(points); + refs.push(ReferencedSOPSequence); + stats[statNameMap[type]] = value; + }); + return { scoords, refs, stats }; + } + + static extractPoints(points) { + const allPoints = []; + + for (let i = 0; i < points.length; i += 2) { + // TODO z + allPoints.push({ x: points[i], y: points[i + 1] }); + } + + return allPoints; + } - // TODO: To be implemented! - // Needs to add points, lengths + static measurementContentToLengthState(MeasurementGroup) { + const { ContentSequence } = MeasurementGroup; + + const findingGroup = toArray(ContentSequence).find( + group => group.ConceptNameCodeSequence.CodeValue === FINDING + ); + + const findingSiteGroups = toArray(ContentSequence).filter( + group => group.ConceptNameCodeSequence.CodeValue === FINDING_SITE + ); + + const comment = toArray(ContentSequence).find( + group => group.ConceptNameCodeSequence.CodeValue === COMMENT + ); + + const trackingIdentifier = toArray(ContentSequence).find( + group => + group.ConceptNameCodeSequence.CodeValue === TRACKING_IDENTIFIER + ); + + const trackingUniqueIdentifier = toArray(ContentSequence).find( + group => + group.ConceptNameCodeSequence.CodeValue === + TRACKING_UNIQUE_IDENTIFIER + ); + const NUMGroup = toArray(ContentSequence).filter( + group => group.ValueType === "NUM" + ); + const { scoords, refs, stats } = Freehand.parseNumGroup(NUMGroup); + // TODO get/handle distinct shapes. using the first shape for now + const state = { + sopInstanceUid: refs[0].ReferencedSOPInstanceUID, + frameIndex: refs[0].ReferencedFrameNumber || 0, + toolType: Freehand.toolType, + handles: { + points: scoords[0], + textBox: {}, + invalidHandlePlacement: false + }, + active: false, + visible: true, + toolName: "Freehand", + invalidated: false, + finding: findingGroup + ? findingGroup.ConceptCodeSequence + : undefined, + findingSites: findingSiteGroups.map(fsg => { + return { ...fsg.ConceptCodeSequence }; + }), + comment: comment ? comment.TextValue : undefined, + trackingIdentifier: trackingIdentifier.TextValue, + trackingUniqueIdentifier: trackingUniqueIdentifier.UID, + meanStdDev: stats // TODO check if SUV/Pet + }; return state; } // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData(measurementContent) { - return measurementContent.map(Freehand.measurementContentToLengthState); + return Freehand.measurementContentToLengthState(measurementContent); } - static getTID300RepresentationArguments(/*tool*/) { - // TO BE IMPLEMENTED + static getTID300RepresentationArguments(tool) { + const { + handles, + finding, + findingSites, + trackingIdentifier, + perimeter, + area, + meanStdDev, + meanStdDevSUV, + pixelUnit + } = tool; + const points = handles.points; + // console.error('tool', handles); + const trackingIdentifierTextValue = + trackingIdentifier || "cornerstoneTools@^4.0.0:Freehand"; + return { - /*points, lengths*/ + points, + perimeter, + area, + meanStdDev, + meanStdDevSUV, + pixelUnit, + trackingIdentifierTextValue, + finding, + findingSites: findingSites || [] }; } } @@ -41,7 +161,17 @@ Freehand.toolType = "Freehand"; Freehand.utilityToolType = "Freehand"; Freehand.TID300Representation = TID300Polyline; Freehand.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - return false; // TODO + if (!TrackingIdentifier.includes(":")) { + return false; + } + + const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + + if (cornerstone4Tag !== CORNERSTONE_4_TAG) { + return false; + } + + return toolType === FREEHAND; }; MeasurementReport.registerTool(Freehand); diff --git a/src/utilities/TID300/Polyline.js b/src/utilities/TID300/Polyline.js index b91ac2a0..1f964eb6 100644 --- a/src/utilities/TID300/Polyline.js +++ b/src/utilities/TID300/Polyline.js @@ -1,5 +1,6 @@ import { DicomMetaDictionary } from "../../DicomMetaDictionary.js"; import TID300Measurement from "./TID300Measurement.js"; +import utilities from "../index.js"; /** * Expand an array of points stored as objects into @@ -12,97 +13,147 @@ function expandPoints(points) { const allPoints = []; points.forEach(point => { - allPoints.push(point[0]); - allPoints.push(point[1]); - if (point[2] !== undefined) { - allPoints.push(point[2]); + allPoints.push(point.x); + allPoints.push(point.y); + if (point.z !== undefined) { + allPoints.push(point.z); } }); return allPoints; } +function getMeasurementComponent( + measurementType, + value, + unit, + GraphicData, + use3DSpatialCoordinates, + ReferencedSOPSequence +) { + return { + // TODO: This feels weird to repeat the GraphicData + RelationshipType: "CONTAINS", + ValueType: "NUM", + ConceptNameCodeSequence: + utilities.quantificationCodedTermMap[measurementType], + MeasuredValueSequence: { + MeasurementUnitsCodeSequence: + utilities.unitsCodedTermMap[unit] || + utilities.unitsCodedTermMap["1"], + NumericValue: value + }, + ContentSequence: { + RelationshipType: "INFERRED FROM", + ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", + GraphicType: "POLYLINE", + GraphicData, + ContentSequence: use3DSpatialCoordinates + ? undefined + : { + RelationshipType: "SELECTED FROM", + ValueType: "IMAGE", + ReferencedSOPSequence + } + } + }; +} + export default class Polyline extends TID300Measurement { contentItem() { const { points, ReferencedSOPSequence, - use3DSpatialCoordinates = false + use3DSpatialCoordinates = false, + perimeter, + area, + meanStdDev, + meanStdDevSUV, + pixelUnit } = this.props; - // Combine all lengths to save the perimeter - // @ToDO The permiter has to be implemented - // const reducer = (accumulator, currentValue) => accumulator + currentValue; - // const perimeter = lengths.reduce(reducer); - const perimeter = {}; const GraphicData = expandPoints(points); - // TODO: Add Mean and STDev value of (modality?) pixels - - return this.getMeasurement([ - { - RelationshipType: "CONTAINS", - ValueType: "NUM", - ConceptNameCodeSequence: { - CodeValue: "G-A197", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Perimeter" // TODO: Look this up from a Code Meaning dictionary - }, - MeasuredValueSequence: { - MeasurementUnitsCodeSequence: { - CodeValue: "mm", - CodingSchemeDesignator: "UCUM", - CodingSchemeVersion: "1.4", - CodeMeaning: "millimeter" - }, - NumericValue: perimeter - }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYLINE", + const measurements = []; + // Talking with Steve, we decided it should not be calculated here. + // it should either come from cornerstone tools or should not be saved + if (perimeter) { + measurements.push( + getMeasurementComponent( + "Perimeter", + perimeter, + "mm", GraphicData, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } - }, - { - // TODO: This feels weird to repeat the GraphicData - RelationshipType: "CONTAINS", - ValueType: "NUM", - ConceptNameCodeSequence: { - CodeValue: "G-A166", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Area" // TODO: Look this up from a Code Meaning dictionary - }, - MeasuredValueSequence: { - MeasurementUnitsCodeSequence: { - CodeValue: "mm2", - CodingSchemeDesignator: "UCUM", - CodingSchemeVersion: "1.4", - CodeMeaning: "SquareMilliMeter" - }, - NumericValue: perimeter - }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYLINE", + use3DSpatialCoordinates, + ReferencedSOPSequence + ) + ); + } + if (area) { + measurements.push( + getMeasurementComponent( + "Area", + area, + "mm2", GraphicData, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + use3DSpatialCoordinates, + ReferencedSOPSequence + ) + ); + } + // if the pixelUnit is not sent, check if tool calculated SUV, otherwise put HU. we cannot check modality here + const unit = pixelUnit || (meanStdDevSUV ? "suv" : "hu"); + const stats = meanStdDevSUV || meanStdDev; + if (stats) { + if (stats.min) { + measurements.push( + getMeasurementComponent( + "Min", + stats.min, + unit, + GraphicData, + use3DSpatialCoordinates, + ReferencedSOPSequence + ) + ); + } + if (stats.max) { + measurements.push( + getMeasurementComponent( + "Max", + stats.max, + unit, + GraphicData, + use3DSpatialCoordinates, + ReferencedSOPSequence + ) + ); + } + if (stats.stdDev) { + measurements.push( + getMeasurementComponent( + "StdDev", + stats.stdDev, + unit, + GraphicData, + use3DSpatialCoordinates, + ReferencedSOPSequence + ) + ); } - ]); + if (stats.mean) { + measurements.push( + getMeasurementComponent( + "Mean", + stats.mean, + unit, + GraphicData, + use3DSpatialCoordinates, + ReferencedSOPSequence + ) + ); + } + } + return this.getMeasurement(measurements); } } diff --git a/src/utilities/index.js b/src/utilities/index.js index 75e29a64..aa35f320 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -2,10 +2,83 @@ import TID1500 from "./TID1500/index.js"; import TID300 from "./TID300/index.js"; import message from "./Message.js"; +const unitsCodedTermMap = { + hu: { + CodeValue: "hnsf'U", + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeMeaning: "Hounsfield Unit" + }, + suv: { + CodeValue: "{SUVbw}g/ml", + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeMeaning: "Standardized Uptake Value body weight" + }, + mm: { + CodeValue: "mm", + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeMeaning: "MilliMeter" + }, + mm2: { + CodeValue: "mm2", + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeMeaning: "SquareMilliMeter" + }, + "1": { + CodeValue: "1", + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeMeaning: "no units" + } +}; + +const quantificationCodedTermMap = { + Perimeter: { + CodeValue: "131191004", + CodingSchemeDesignator: "SCT", + CodeMeaning: "Perimeter" + }, + Area: { + CodeValue: "2798000", + CodingSchemeDesignator: "SCT", + CodeMeaning: "Area" + }, + Volume: { + CodeValue: "1185650", + CodingSchemeDesignator: "SCT", + CodeMeaning: "Volume" + }, + Min: { + CodeValue: "R-404FB", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Min" + }, + Max: { + CodeValue: "G-A437", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Max" + }, + StdDev: { + CodeValue: "R-10047", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Standard Deviation" + }, + Mean: { + CodeValue: "R-00317", + CodingSchemeDesignator: "SRT", + CodeMeaning: "Mean" + } +}; + const utilities = { TID1500, TID300, - message + message, + unitsCodedTermMap, + quantificationCodedTermMap }; export default utilities; From 64c7aac70f6d885da523af97449b2b4f43bf32f0 Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Fri, 2 Jul 2021 01:24:41 -0700 Subject: [PATCH 18/19] add read tests for length, bidirectional, point and bounding box --- test/test_sr.js | 251 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 249 insertions(+), 2 deletions(-) diff --git a/test/test_sr.js b/test/test_sr.js index 3c715208..bf564459 100644 --- a/test/test_sr.js +++ b/test/test_sr.js @@ -1,5 +1,252 @@ +const expect = require("chai").expect; +const dcmjs = require("../build/dcmjs"); -exports.test = () => { - console.log('no tests yet'); +const fs = require("fs"); +const { http, https } = require("follow-redirects"); +const os = require("os"); +const path = require("path"); + +const { DicomMetaDictionary, DicomDict, DicomMessage } = dcmjs.data; + +const IMAGING_MEASUREMENT = "126010"; +const FINDING = "121071"; +const FINDING_SITE_SCT = "363698007"; +const FINDING_SITE_SRT = "G-C0E3"; +const LONG_AXIS = "G-A185"; +const SHORT_AXIS = "G-A186"; +const LENGTH = "G-D7FE"; + +function toArray(x) { + return Array.isArray(x) ? x : [x]; +} + +function downloadToFile(url, filePath) { + return new Promise((resolve, reject) => { + const fileStream = fs.createWriteStream(filePath); + const request = https + .get(url, response => { + response.pipe(fileStream); + fileStream.on("finish", () => { + resolve(filePath); + }); + }) + .on("error", reject); + }); +} + + +const tests = { + test_point: () => { + const srURL = + "https://github.com/dcmjs-org/data/releases/download/DICOMSR_Prostate_X/ProstateX-sr.dcm"; + const srFilePath = path.join(os.tmpdir(), "ProstateX-sr.dcm"); + + downloadToFile(srURL, srFilePath).then(() => { + const arrayBuffer = fs.readFileSync(srFilePath) + .buffer; + const dicomDict = DicomMessage.readFile( + arrayBuffer + ); + const dataset = DicomMetaDictionary.naturalizeDataset( + dicomDict.dict + ); + expect(dataset.Modality).to.equal('SR'); + expect(dataset.ContentSequence).to.not.equal(undefined); + + const measurement = toArray(dataset.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === IMAGING_MEASUREMENT + ); + const finding = toArray(measurement.ContentSequence[0].ContentSequence).find( + (group) => group.ConceptNameCodeSequence.CodeValue === FINDING + ); + expect(finding.ConceptCodeSequence.CodeValue).to.equal("52988006"); + expect(finding.ConceptCodeSequence.CodingSchemeDesignator).to.equal("SCT"); + expect(finding.ConceptCodeSequence.CodeMeaning).to.equal("Lesion"); + + const findingSite = toArray(measurement.ContentSequence[0].ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === FINDING_SITE_SCT + ); + expect(findingSite.ConceptCodeSequence.CodeValue).to.equal("279706003"); + expect(findingSite.ConceptCodeSequence.CodingSchemeDesignator).to.equal("SCT"); + expect(findingSite.ConceptCodeSequence.CodeMeaning).to.equal("Peripheral zone of the prostate"); + + const roi = toArray( + measurement.ContentSequence[0].ContentSequence + ).find((group) => group.ValueType === "SCOORD3D"); + + expect(roi.GraphicType).to.equal("POINT"); + expect(roi.GraphicData).to.deep.equal([ + -0.7838299870491028, + 30.95800018310547, + -29.05820083618164, + ]); + + console.log("Finished test_point"); + }); + }, + test_bounding_box: () => { + const srURL = + "https://github.com/dcmjs-org/data/releases/download/DICOMSR_PetCtLung_BB/Lung_Dx-SR.dcm"; + const srFilePath = path.join(os.tmpdir(), "Lung_Dx-SR.dcm"); + + downloadToFile(srURL, srFilePath).then(() => { + const arrayBuffer = fs.readFileSync(srFilePath) + .buffer; + const dicomDict = DicomMessage.readFile( + arrayBuffer + ); + const dataset = DicomMetaDictionary.naturalizeDataset( + dicomDict.dict + ); + expect(dataset.Modality).to.equal('SR'); + expect(dataset.ContentSequence).to.not.equal(undefined); + const measurement = toArray(dataset.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === IMAGING_MEASUREMENT + ); + const finding = toArray(measurement.ContentSequence[0].ContentSequence).find( + (group) => group.ConceptNameCodeSequence.CodeValue === FINDING + ); + expect(finding.ConceptCodeSequence.CodeValue).to.equal("108369006"); + expect(finding.ConceptCodeSequence.CodingSchemeDesignator).to.equal("SCT"); + expect(finding.ConceptCodeSequence.CodeMeaning).to.equal("Neoplasm"); + + const findingSite = toArray(measurement.ContentSequence[0].ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === FINDING_SITE_SCT + ); + expect(findingSite.ConceptCodeSequence.CodeValue).to.equal("39607008"); + expect(findingSite.ConceptCodeSequence.CodingSchemeDesignator).to.equal("SCT"); + expect(findingSite.ConceptCodeSequence.CodeMeaning).to.equal("Lung"); + + const roi = toArray( + measurement.ContentSequence[0].ContentSequence + ).find((group) => group.ValueType === "SCOORD"); + + expect(roi.GraphicType).to.equal("POLYLINE"); + expect(roi.GraphicData).to.deep.equal([ + 134, + 198, + 165, + 198, + 165, + 233, + 134, + 233, + 134, + 198 + ]); + console.log("Finished test_bounding_box"); + }); + }, + test_bidirectional: () => { + const srURL = + "https://github.com/dcmjs-org/data/releases/download/DICOMSR_CCC2018_Bidirectional/ccc2018_bidirectional_sr.dcm"; + const srFilePath = path.join(os.tmpdir(), "ccc2018_bidirectional_sr.dcm"); + + downloadToFile(srURL, srFilePath).then(() => { + const arrayBuffer = fs.readFileSync(srFilePath) + .buffer; + const dicomDict = DicomMessage.readFile( + arrayBuffer + ); + const dataset = DicomMetaDictionary.naturalizeDataset( + dicomDict.dict + ); + expect(dataset.Modality).to.equal('SR'); + expect(dataset.ContentSequence).to.not.equal(undefined); + const measurement = toArray(dataset.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === IMAGING_MEASUREMENT + ); + const finding = toArray(measurement.ContentSequence.ContentSequence).find( + (group) => group.ConceptNameCodeSequence.CodeValue === FINDING + ); + expect(finding.ConceptCodeSequence.CodeValue).to.equal("RID5741"); + expect(finding.ConceptCodeSequence.CodingSchemeDesignator).to.equal("RADLEX"); + expect(finding.ConceptCodeSequence.CodeMeaning).to.equal("solid"); + + const findingSite = toArray(measurement.ContentSequence.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === FINDING_SITE_SRT + ); + expect(findingSite.ConceptCodeSequence.CodeValue).to.equal("39607008"); + expect(findingSite.ConceptCodeSequence.CodingSchemeDesignator).to.equal("SRT"); + expect(findingSite.ConceptCodeSequence.CodeMeaning).to.equal("Lung structure"); + + const longaxis = toArray(measurement.ContentSequence.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === LONG_AXIS + ); + + expect(longaxis.MeasuredValueSequence.NumericValue).to.equal(16.1); + + const shortaxis = toArray(measurement.ContentSequence.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === SHORT_AXIS + ); + + expect(shortaxis.MeasuredValueSequence.NumericValue).to.equal(8.1); + + console.log("Finished test_bidirectional"); + }); + }, + test_length: () => { + const srURL = + "https://github.com/dcmjs-org/data/releases/download/DICOMSR_CCC2017_Length/ccc2017_length_sr.dcm"; + const srFilePath = path.join(os.tmpdir(), "ccc2017_length_sr.dcm"); + + downloadToFile(srURL, srFilePath).then(() => { + const arrayBuffer = fs.readFileSync(srFilePath) + .buffer; + const dicomDict = DicomMessage.readFile( + arrayBuffer + ); + const dataset = DicomMetaDictionary.naturalizeDataset( + dicomDict.dict + ); + expect(dataset.Modality).to.equal('SR'); + expect(dataset.ContentSequence).to.not.equal(undefined); + const measurement = toArray(dataset.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === IMAGING_MEASUREMENT + ); + + const findingSite = toArray(measurement.ContentSequence.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === FINDING_SITE_SRT + ); + expect(findingSite.ConceptCodeSequence.CodeValue).to.equal("T-87000"); + expect(findingSite.ConceptCodeSequence.CodingSchemeDesignator).to.equal("SRT"); + expect(findingSite.ConceptCodeSequence.CodeMeaning).to.equal("Ovary"); + + const length = toArray(measurement.ContentSequence.ContentSequence).find( + (group) => + group.ConceptNameCodeSequence.CodeValue === LENGTH + ); + + expect(length.MeasuredValueSequence.NumericValue).to.equal(51.86852852); + + console.log("Finished test_bidirectional"); + }); + }, } +exports.test = async testToRun => { + Object.keys(tests).forEach(testName => { + if ( + testToRun && + !testName.toLowerCase().includes(testToRun.toLowerCase()) + ) { + console.log("-- Skipping " + testName); + return false; + } + console.log("-- Starting " + testName); + tests[testName](); + }); +}; + +exports.tests = tests; + From 9847e911df90371e6bce244df3414ce7bd56312c Mon Sep 17 00:00:00 2001 From: Emel Alkim Date: Fri, 2 Jul 2021 01:25:05 -0700 Subject: [PATCH 19/19] add bidirectional adapter test --- test/test_adapters.js | 176 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 2 deletions(-) diff --git a/test/test_adapters.js b/test/test_adapters.js index 3c715208..e8fa774e 100644 --- a/test/test_adapters.js +++ b/test/test_adapters.js @@ -1,5 +1,177 @@ +const expect = require("chai").expect; +const dcmjs = require("../build/dcmjs"); +const { MeasurementReport } = dcmjs.adapters.Cornerstone; -exports.test = () => { - console.log('no tests yet'); +const bidirectionalToolstate = { + "1.3.6.1.4.1.14519.5.2.1.4334.1501.204038471157187984095376885814&frame=1": { + Bidirectional: { + data: [ + { + toolType: "Bidirectional", + isCreating: false, + visible: true, + active: false, + invalidated: true, + handles: { + start: { + x: 358.3673400878906, + y: 204.6700439453125, + index: 0, + drawnIndependently: false, + allowedOutsideImage: false, + highlight: true, + active: false, + }, + end: { + x: 358.6566162109375, + y: 182.97471618652344, + index: 1, + drawnIndependently: false, + allowedOutsideImage: false, + highlight: true, + active: false, + }, + perpendicularStart: { + x: 363.9358215332031, + y: 193.8946990966797, + index: 2, + drawnIndependently: false, + allowedOutsideImage: false, + highlight: true, + active: false, + locked: false, + }, + perpendicularEnd: { + x: 353.088134765625, + y: 193.75006103515625, + index: 3, + drawnIndependently: false, + allowedOutsideImage: false, + highlight: true, + active: false, + }, + textBox: { + x: 358.3673400878906, + y: 204.6700439453125, + index: null, + drawnIndependently: true, + allowedOutsideImage: true, + highlight: false, + active: false, + hasMoved: false, + movesIndependently: false, + hasBoundingBox: true, + }, + }, + aimId: "2.25.746948110250556618597051441934648086314", + longestDiameter: 16.1, + unit: "mm", + shortestDiameter: 8.1, + trackingIdentifier: "web annotation", + trackingUniqueIdentifier: + "2.25.797388882284565263543728324912886045063", + finding: { + CodeValue: "ROI", + CodingSchemeDesignator: "99EPAD", + CodeMeaning: "ROI Only", + }, + findingSites: [], + comment: "undefined / undefined / undefined / undefined", + }, + ], + }, + }, +}; +const imageIds =[ + '1.3.6.1.4.1.14519.5.2.1.4334.1501.204038471157187984095376885814&frame=1' +]; + +const bidirectionalMetaDataProvider = { + get(type, imageId) { + if (type === "generalSeriesModule") { + if (imageIds.includes(imageId)) { + return { + studyInstanceUID: '1.3.6.1.4.1.14519.5.2.1.4334.1501.772823147212833057678103865443', + seriesInstanceUID: '1.3.6.1.4.1.14519.5.2.1.4334.1501.128241934543986080196677451907', + }; + } + } + if (type === "sopCommonModule") { + if (imageIds.includes(imageId)) { + return { + sopInstanceUID: imageId.split("&frame=")[0], + sopClassUID: '1.2.840.10008.5.1.4.1.1.2' + }; + } + } + if (type === "frameNumber") { + if (imageIds.includes(imageId)) { + return imageId.split("&frame=")[1] + ? imageId.split("&frame=")[1] + : 1; + } + } + if (type === "patientModule") { + if (imageIds.includes(imageId)) { + return { + patientID: 'AMC-001', + patientName : '', + patientBirthDate: '', + patientSex: '', + }; + } + } + return null; + }, +}; + +const IMAGING_MEASUREMENT = "126010"; +const LONG_AXIS = "103339001"; +const SHORT_AXIS = "103340004"; + +function toArray(x) { + return Array.isArray(x) ? x : [x]; } +const tests = { + test_bidirectional_adapter: () => { + const report = MeasurementReport.generateReport( + bidirectionalToolstate, + bidirectionalMetaDataProvider, + {} + ); + expect(report.dataset.Modality).to.equal("SR"); + expect(report.dataset.ContentSequence).to.not.equal(undefined); + + const measurement = toArray(report.dataset.ContentSequence).find( + (group) => group.ConceptNameCodeSequence.CodeValue === IMAGING_MEASUREMENT + ); + const longaxis = toArray(measurement.ContentSequence[0].ContentSequence).find( + (group) => group.ConceptNameCodeSequence.CodeValue === LONG_AXIS + ); + expect(longaxis.MeasuredValueSequence.NumericValue).to.equal(16.1); + + const shortaxis = toArray(measurement.ContentSequence[0].ContentSequence).find( + (group) => group.ConceptNameCodeSequence.CodeValue === SHORT_AXIS + ); + expect(shortaxis.MeasuredValueSequence.NumericValue).to.equal(8.1); + + console.log("Finished test_bidirectional_adapter"); + }, +}; + +exports.test = async (testToRun) => { + Object.keys(tests).forEach((testName) => { + if ( + testToRun && + !testName.toLowerCase().includes(testToRun.toLowerCase()) + ) { + console.log("-- Skipping " + testName); + return false; + } + console.log("-- Starting " + testName); + tests[testName](); + }); +}; + +exports.tests = tests;