diff --git a/src/utilities/MeasurementBuilder.js b/src/utilities/MeasurementBuilder.js new file mode 100644 index 00000000..cbadebe8 --- /dev/null +++ b/src/utilities/MeasurementBuilder.js @@ -0,0 +1,181 @@ +import unit2CodingValue from "./TID300/unit2CodingValue"; + +/** + * Utility class for constructing DICOM SR Numeric (NUM) measurement items + * associated with a spatial coordinate (SCOORD) annotation. + * + * Each measurement produced by this builder includes: + * - A NUM content item describing the measurement value + * - A MeasuredValueSequence with its unit encoded in UCUM or another scheme + * - A ContentSequence containing an INFERRED FROM reference linking the + * measurement back to the SCOORD item using ReferencedContentItemIdentifier. + * + * This ensures that all derived measurements correctly reference the + * annotation from which they were computed. + */ +class MeasurementBuilder { + /** + * Creates a NUM (Numeric Measurement) content item for a DICOM SR. + * + * @param {string} codeValue - Code value representing the type of measurement. + * @param {string} codingScheme - Coding scheme designator (e.g., "SCT", "DCM"). + * @param {string} codeMeaning - Human-readable meaning of the measurement code. + * @param {number|string} value - The numeric measurement value. + * @param {Object} unit - Unit definition object (UCUM or other coding scheme). + * @param {number} annotationIndex - Index used to populate ReferencedContentItemIdentifier, + * ensuring the NUM item correctly references its SCOORD. + * + * @returns {Object} DICOM SR content item representing a numeric measurement. + */ + + static createNumericMeasurement( + codeValue, + codingScheme, + codeMeaning, + value, + unit, + annotationIndex, + { scoordContentItem = null } = {} + ) { + return { + RelationshipType: "CONTAINS", + ValueType: "NUM", + ConceptNameCodeSequence: { + CodeValue: codeValue, + CodingSchemeDesignator: codingScheme, + CodeMeaning: codeMeaning + }, + MeasuredValueSequence: { + MeasurementUnitsCodeSequence: unit2CodingValue(unit), + NumericValue: value + }, + ContentSequence: scoordContentItem + ? scoordContentItem // FIRST item -> SCOORD + : { + RelationshipType: "INFERRED FROM", // Remaining items -> reference + ReferencedContentItemIdentifier: [1, 1, annotationIndex] + } + }; + } + + static createAreaMeasurement( + area, + areaUnit, + annotationIndex, + { scoordContentItem } + ) { + return MeasurementBuilder.createNumericMeasurement( + "42798000", + "SCT", + "Area", + area, + areaUnit, + annotationIndex, + { scoordContentItem } + ); + } + + static createRadiusMeasurement( + radius, + radiusUnit, + annotationIndex, + { scoordContentItem } + ) { + return MeasurementBuilder.createNumericMeasurement( + "131190003", + "SCT", + "Radius", + radius, + radiusUnit, + annotationIndex, + { scoordContentItem } + ); + } + + static createMaxMeasurement( + max, + modalityUnit, + annotationIndex, + { scoordContentItem } + ) { + return MeasurementBuilder.createNumericMeasurement( + "56851009", + "SCT", + "Maximum", + max, + modalityUnit, + annotationIndex, + { scoordContentItem } + ); + } + + static createMinMeasurement( + min, + modalityUnit, + annotationIndex, + { scoordContentItem } + ) { + return MeasurementBuilder.createNumericMeasurement( + "255605001", + "SCT", + "Minimum", + min, + modalityUnit, + annotationIndex, + { scoordContentItem } + ); + } + + static createMeanMeasurement( + mean, + modalityUnit, + annotationIndex, + { scoordContentItem } + ) { + return MeasurementBuilder.createNumericMeasurement( + "373098007", + "SCT", + "Mean", + mean, + modalityUnit, + annotationIndex, + { scoordContentItem } + ); + } + + static createStdDevMeasurement( + stdDev, + modalityUnit, + annotationIndex, + { scoordContentItem } + ) { + return MeasurementBuilder.createNumericMeasurement( + "386136009", + "SCT", + "Standard Deviation", + stdDev, + modalityUnit, + annotationIndex, + { scoordContentItem } + ); + } + + static createPerimeterMeasurement( + perimeter, + unit, + annotationIndex, + { scoordContentItem } + ) { + return MeasurementBuilder.createNumericMeasurement( + "131191004", + "SCT", + "Perimeter", + perimeter, + unit, + annotationIndex, + { scoordContentItem } + ); + } +} + +export default MeasurementBuilder; diff --git a/src/utilities/TID300/Bidirectional.js b/src/utilities/TID300/Bidirectional.js index 76d23dd2..b74959cb 100644 --- a/src/utilities/TID300/Bidirectional.js +++ b/src/utilities/TID300/Bidirectional.js @@ -1,5 +1,6 @@ import TID300Measurement from "./TID300Measurement.js"; import unit2CodingValue from "./unit2CodingValue.js"; +import TID320ContentItem from "./TID320ContentItem.js"; export default class Bidirectional extends TID300Measurement { contentItem() { @@ -23,64 +24,50 @@ export default class Bidirectional extends TID300Measurement { use3DSpatialCoordinates }); + const longAxisContentSequence = new TID320ContentItem({ + graphicType: "POLYLINE", + graphicData: longAxisGraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + + const shortAxisContentSequence = new TID320ContentItem({ + graphicType: "POLYLINE", + graphicData: shortAxisGraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + return this.getMeasurement([ { RelationshipType: "CONTAINS", ValueType: "NUM", ConceptNameCodeSequence: { - CodeValue: "G-A185", - CodingSchemeDesignator: "SRT", + CodeValue: "103339001", + CodingSchemeDesignator: "SCT", CodeMeaning: "Long Axis" }, MeasuredValueSequence: { MeasurementUnitsCodeSequence: unit2CodingValue(unit), NumericValue: longAxisLength }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYLINE", - GraphicData: longAxisGraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + ContentSequence: longAxisContentSequence }, { RelationshipType: "CONTAINS", ValueType: "NUM", ConceptNameCodeSequence: { - CodeValue: "G-A186", - CodingSchemeDesignator: "SRT", + CodeValue: "103340004", + CodingSchemeDesignator: "SCT", CodeMeaning: "Short Axis" }, MeasuredValueSequence: { MeasurementUnitsCodeSequence: unit2CodingValue(unit), NumericValue: shortAxisLength }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYLINE", - GraphicData: shortAxisGraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + ContentSequence: shortAxisContentSequence } ]); } diff --git a/src/utilities/TID300/Calibration.js b/src/utilities/TID300/Calibration.js index db695065..8c6627a1 100644 --- a/src/utilities/TID300/Calibration.js +++ b/src/utilities/TID300/Calibration.js @@ -1,5 +1,6 @@ import TID300Measurement from "./TID300Measurement.js"; import unit2CodingValue from "./unit2CodingValue.js"; +import TID320ContentItem from "./TID320ContentItem.js"; export default class Calibration extends TID300Measurement { contentItem() { @@ -18,6 +19,14 @@ export default class Calibration extends TID300Measurement { use3DSpatialCoordinates }); + const graphicContentSequence = new TID320ContentItem({ + graphicType: "POLYLINE", + graphicData: GraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + return this.getMeasurement([ { RelationshipType: "CONTAINS", @@ -31,22 +40,7 @@ export default class Calibration extends TID300Measurement { MeasurementUnitsCodeSequence: unit2CodingValue(unit), NumericValue: distance }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYLINE", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + ContentSequence: graphicContentSequence } ]); } diff --git a/src/utilities/TID300/Circle.js b/src/utilities/TID300/Circle.js index e35c2250..13fe8edf 100644 --- a/src/utilities/TID300/Circle.js +++ b/src/utilities/TID300/Circle.js @@ -1,5 +1,7 @@ import TID300Measurement from "./TID300Measurement.js"; import unit2CodingValue from "./unit2CodingValue.js"; +import TID320ContentItem from "./TID320ContentItem.js"; +import MeasurementBuilder from "../MeasurementBuilder.js"; export default class Circle extends TID300Measurement { contentItem() { @@ -11,7 +13,15 @@ export default class Circle extends TID300Measurement { area, areaUnit = "mm2", unit = "mm", - ReferencedFrameOfReferenceUID + max, + min, + mean, + stdDev, + radiusUnit, + modalityUnit, + ReferencedFrameOfReferenceUID, + radius, + annotationIndex } = this.props; // Combine all lengths to save the perimeter @@ -23,65 +33,63 @@ export default class Circle extends TID300Measurement { use3DSpatialCoordinates }); - // TODO: Add Mean and STDev value of (modality?) pixels - - return this.getMeasurement([ + const measurementConfigs = [ + { + value: perimeter, + unit: unit, + builder: MeasurementBuilder.createPerimeterMeasurement + }, + { + value: area, + unit: areaUnit, + builder: MeasurementBuilder.createAreaMeasurement + }, + { + value: radius, + unit: radiusUnit, + builder: MeasurementBuilder.createRadiusMeasurement + }, { - RelationshipType: "CONTAINS", - ValueType: "NUM", - ConceptNameCodeSequence: { - CodeValue: "G-A197", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Perimeter" // TODO: Look this up from a Code Meaning dictionary - }, - MeasuredValueSequence: { - MeasurementUnitsCodeSequence: unit2CodingValue(unit), - NumericValue: perimeter - }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "CIRCLE", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + value: max, + unit: modalityUnit, + builder: MeasurementBuilder.createMaxMeasurement }, { - // 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: unit2CodingValue(areaUnit), - NumericValue: area - }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "CIRCLE", - GraphicData, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + value: min, + unit: modalityUnit, + builder: MeasurementBuilder.createMinMeasurement + }, + { + value: mean, + unit: modalityUnit, + builder: MeasurementBuilder.createMeanMeasurement + }, + { + value: stdDev, + unit: modalityUnit, + builder: MeasurementBuilder.createStdDevMeasurement } - ]); + ]; + + const scoordContentItem = new TID320ContentItem({ + graphicType: "CIRCLE", + graphicData: GraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + + const measurements = [ + ...measurementConfigs + .filter(config => config.value !== undefined) + .map((config, index) => + config.builder(config.value, config.unit, annotationIndex, { + scoordContentItem: + index === 0 ? scoordContentItem : null + }) + ) + ]; + + return this.getMeasurement(measurements); } } diff --git a/src/utilities/TID300/CobbAngle.js b/src/utilities/TID300/CobbAngle.js index afefd7ce..a074988d 100644 --- a/src/utilities/TID300/CobbAngle.js +++ b/src/utilities/TID300/CobbAngle.js @@ -1,4 +1,5 @@ import TID300Measurement from "./TID300Measurement.js"; +import TID320ContentItem from "./TID320ContentItem.js"; export default class CobbAngle extends TID300Measurement { contentItem() { @@ -18,6 +19,14 @@ export default class CobbAngle extends TID300Measurement { use3DSpatialCoordinates }); + const graphicContentSequence = new TID320ContentItem({ + graphicType: "POLYLINE", + graphicData: GraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + return this.getMeasurement([ { RelationshipType: "CONTAINS", @@ -36,22 +45,7 @@ export default class CobbAngle extends TID300Measurement { }, NumericValue: rAngle }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYLINE", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + ContentSequence: graphicContentSequence } ]); } diff --git a/src/utilities/TID300/Ellipse.js b/src/utilities/TID300/Ellipse.js index 0fd4696f..a6ac2455 100644 --- a/src/utilities/TID300/Ellipse.js +++ b/src/utilities/TID300/Ellipse.js @@ -1,5 +1,7 @@ import TID300Measurement from "./TID300Measurement.js"; import unit2CodingValue from "./unit2CodingValue.js"; +import TID320ContentItem from "./TID320ContentItem.js"; +import MeasurementBuilder from "../MeasurementBuilder.js"; export default class Ellipse extends TID300Measurement { contentItem() { @@ -9,7 +11,13 @@ export default class Ellipse extends TID300Measurement { ReferencedSOPSequence, area, areaUnit, - ReferencedFrameOfReferenceUID + max, + min, + mean, + stdDev, + modalityUnit, + ReferencedFrameOfReferenceUID, + annotationIndex } = this.props; const GraphicData = this.flattenPoints({ @@ -17,36 +25,53 @@ export default class Ellipse extends TID300Measurement { use3DSpatialCoordinates }); - return this.getMeasurement([ + const measurementConfigs = [ { - RelationshipType: "CONTAINS", - ValueType: "NUM", - ConceptNameCodeSequence: { - CodeValue: "G-D7FE", - CodingSchemeDesignator: "SRT", - CodeMeaning: "AREA" - }, - MeasuredValueSequence: { - MeasurementUnitsCodeSequence: unit2CodingValue(areaUnit), - NumericValue: area - }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "ELLIPSE", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + value: area, + unit: areaUnit, + builder: MeasurementBuilder.createAreaMeasurement + }, + { + value: max, + unit: modalityUnit, + builder: MeasurementBuilder.createMaxMeasurement + }, + { + value: min, + unit: modalityUnit, + builder: MeasurementBuilder.createMinMeasurement + }, + { + value: mean, + unit: modalityUnit, + builder: MeasurementBuilder.createMeanMeasurement + }, + { + value: stdDev, + unit: modalityUnit, + builder: MeasurementBuilder.createStdDevMeasurement } - ]); + ]; + + const scoordContentItem = new TID320ContentItem({ + graphicType: "ELLIPSE", + graphicData: GraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + + const measurements = [ + ...measurementConfigs + .filter(config => config.value !== undefined) + .map((config, index) => + config.builder(config.value, config.unit, annotationIndex, { + scoordContentItem: + index === 0 ? scoordContentItem : null + }) + ) + ]; + + return this.getMeasurement(measurements); } } diff --git a/src/utilities/TID300/Length.js b/src/utilities/TID300/Length.js index ddc1dcac..27ce6696 100644 --- a/src/utilities/TID300/Length.js +++ b/src/utilities/TID300/Length.js @@ -23,8 +23,8 @@ export default class Length extends TID300Measurement { RelationshipType: "CONTAINS", ValueType: "NUM", ConceptNameCodeSequence: { - CodeValue: "G-D7FE", - CodingSchemeDesignator: "SRT", + CodeValue: "410668003", + CodingSchemeDesignator: "SCT", CodeMeaning: "Length" }, MeasuredValueSequence: { diff --git a/src/utilities/TID300/Point.js b/src/utilities/TID300/Point.js index cf022904..01777584 100644 --- a/src/utilities/TID300/Point.js +++ b/src/utilities/TID300/Point.js @@ -1,4 +1,5 @@ import TID300Measurement from "./TID300Measurement.js"; +import TID320ContentItem from "./TID320ContentItem.js"; export default class Point extends TID300Measurement { contentItem() { @@ -15,6 +16,14 @@ export default class Point extends TID300Measurement { use3DSpatialCoordinates }); + const graphicContentSequence = new TID320ContentItem({ + graphicType: "POINT", + graphicData: GraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + return this.getMeasurement([ { RelationshipType: "CONTAINS", @@ -25,22 +34,7 @@ export default class Point extends TID300Measurement { CodeMeaning: "Center" }, //MeasuredValueSequence: , - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POINT", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + ContentSequence: graphicContentSequence } ]); } diff --git a/src/utilities/TID300/Polygon.js b/src/utilities/TID300/Polygon.js index 74da2a6c..b26cf4ae 100644 --- a/src/utilities/TID300/Polygon.js +++ b/src/utilities/TID300/Polygon.js @@ -1,5 +1,6 @@ import TID300Measurement from "./TID300Measurement.js"; import unit2CodingValue from "./unit2CodingValue.js"; +import TID320ContentItem from "./TID320ContentItem.js"; export default class Polygon extends TID300Measurement { contentItem() { @@ -19,6 +20,14 @@ export default class Polygon extends TID300Measurement { use3DSpatialCoordinates }); + const graphicContentSequence = new TID320ContentItem({ + graphicType: "POLYGON", + graphicData: GraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + return this.getMeasurement([ { RelationshipType: "CONTAINS", @@ -32,22 +41,7 @@ export default class Polygon extends TID300Measurement { MeasurementUnitsCodeSequence: unit2CodingValue(unit), NumericValue: perimeter }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYGON", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + ContentSequence: graphicContentSequence }, { RelationshipType: "CONTAINS", @@ -61,22 +55,7 @@ export default class Polygon extends TID300Measurement { MeasurementUnitsCodeSequence: unit2CodingValue(areaUnit), NumericValue: area }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYGON", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + ContentSequence: graphicContentSequence } ]); } diff --git a/src/utilities/TID300/Polyline.js b/src/utilities/TID300/Polyline.js index 285fe2b0..254c3c89 100644 --- a/src/utilities/TID300/Polyline.js +++ b/src/utilities/TID300/Polyline.js @@ -1,5 +1,7 @@ import TID300Measurement from "./TID300Measurement"; import unit2CodingValue from "./unit2CodingValue"; +import TID320ContentItem from "./TID320ContentItem.js"; +import MeasurementBuilder from "../MeasurementBuilder.js"; export default class Polyline extends TID300Measurement { contentItem() { @@ -11,7 +13,13 @@ export default class Polyline extends TID300Measurement { use3DSpatialCoordinates = false, perimeter, unit = "mm", - ReferencedFrameOfReferenceUID + modalityUnit, + min, + max, + mean, + stdDev, + ReferencedFrameOfReferenceUID, + annotationIndex } = this.props; const GraphicData = this.flattenPoints({ @@ -19,67 +27,57 @@ export default class Polyline extends TID300Measurement { use3DSpatialCoordinates }); - // TODO: Add Mean and STDev value of (modality?) pixels - return this.getMeasurement([ + const measurementConfigs = [ { - RelationshipType: "CONTAINS", - ValueType: "NUM", - ConceptNameCodeSequence: { - CodeValue: "131191004", - CodingSchemeDesignator: "SCT", - CodeMeaning: "Perimeter" - }, - MeasuredValueSequence: { - MeasurementUnitsCodeSequence: unit2CodingValue(unit), - NumericValue: perimeter - }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYLINE", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + value: perimeter, + unit: unit, + builder: MeasurementBuilder.createPerimeterMeasurement }, { - // 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: unit2CodingValue(areaUnit), - NumericValue: area - }, - ContentSequence: { - RelationshipType: "INFERRED FROM", - ValueType: use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", - GraphicType: "POLYLINE", - GraphicData, - ReferencedFrameOfReferenceUID: use3DSpatialCoordinates - ? ReferencedFrameOfReferenceUID - : undefined, - ContentSequence: use3DSpatialCoordinates - ? undefined - : { - RelationshipType: "SELECTED FROM", - ValueType: "IMAGE", - ReferencedSOPSequence - } - } + value: area, + unit: areaUnit, + builder: MeasurementBuilder.createAreaMeasurement + }, + { + value: max, + unit: modalityUnit, + builder: MeasurementBuilder.createMaxMeasurement + }, + { + value: min, + unit: modalityUnit, + builder: MeasurementBuilder.createMinMeasurement + }, + { + value: mean, + unit: modalityUnit, + builder: MeasurementBuilder.createMeanMeasurement + }, + { + value: stdDev, + unit: modalityUnit, + builder: MeasurementBuilder.createStdDevMeasurement } - ]); + ]; + const scoordContentItem = new TID320ContentItem({ + graphicType: "POLYLINE", + graphicData: GraphicData, + use3DSpatialCoordinates, + referencedSOPSequence: ReferencedSOPSequence, + referencedFrameOfReferenceUID: ReferencedFrameOfReferenceUID + }).contentItem(); + + const measurements = [ + ...measurementConfigs + .filter(config => config.value !== undefined) + .map((config, index) => + config.builder(config.value, config.unit, annotationIndex, { + scoordContentItem: + index === 0 ? scoordContentItem : null + }) + ) + ]; + + return this.getMeasurement(measurements); } } diff --git a/src/utilities/TID300/TID320ContentItem.js b/src/utilities/TID300/TID320ContentItem.js new file mode 100644 index 00000000..81b2c7d1 --- /dev/null +++ b/src/utilities/TID300/TID320ContentItem.js @@ -0,0 +1,41 @@ +/** + * Builds a DICOM SR ContentSequence block for geometric measurements + * that share the same structure across tools (Circle, Ellipse, Polyline, etc.) + */ +export default class TID320ContentItem { + constructor({ + graphicType, + graphicData, + use3DSpatialCoordinates = false, + referencedSOPSequence, + referencedFrameOfReferenceUID + }) { + this.graphicType = graphicType; + this.graphicData = graphicData; + this.use3DSpatialCoordinates = use3DSpatialCoordinates; + this.referencedSOPSequence = referencedSOPSequence; + this.referencedFrameOfReferenceUID = referencedFrameOfReferenceUID; + } + + contentItem() { + const content = { + RelationshipType: "INFERRED FROM", + ValueType: this.use3DSpatialCoordinates ? "SCOORD3D" : "SCOORD", + GraphicType: this.graphicType, + GraphicData: this.graphicData + }; + + if (this.use3DSpatialCoordinates) { + content.ReferencedFrameOfReferenceUID = + this.referencedFrameOfReferenceUID; + } else { + content.ContentSequence = { + RelationshipType: "SELECTED FROM", + ValueType: "IMAGE", + ReferencedSOPSequence: this.referencedSOPSequence + }; + } + + return content; + } +} diff --git a/src/utilities/TID300/unit2CodingValue.js b/src/utilities/TID300/unit2CodingValue.js index 54de41ce..baf0ae04 100644 --- a/src/utilities/TID300/unit2CodingValue.js +++ b/src/utilities/TID300/unit2CodingValue.js @@ -1,36 +1,181 @@ import log from "../../log.js"; -const MM_UNIT = { - CodeValue: "mm", - CodingSchemeDesignator: "UCUM", - CodingSchemeVersion: "1.4", - CodeMeaning: "millimeter" -}; +const knownUnits = [ + // Standard UCUM units. + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "mm", + CodeMeaning: "mm" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "mm2", + CodeMeaning: "mm2" + }, + // Units defined in https://dicom.nema.org/medical/dicom/current/output/chtml/part16/sect_CID_83.html + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "[hnsf'U]", + CodeMeaning: "Hounsfield unit" + }, + // Units defined in https://dicom.nema.org/medical/dicom/current/output/chtml/part16/sect_CID_84.html + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "{counts}", + CodeMeaning: "Counts" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "{counts}/s", + CodeMeaning: "Counts per second" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "{propcounts}", + CodeMeaning: "Proportional to counts" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "{propcounts}/s", + CodeMeaning: "Proportional to counts per second" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "cm2", + CodeMeaning: "Centimeter**2" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "cm2/ml", + CodeMeaning: "Centimeter**2/milliliter" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "%", + CodeMeaning: "Percent" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "Bq/ml", + CodeMeaning: "Becquerels/milliliter" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "mg/min/ml", + CodeMeaning: "Milligrams/minute/milliliter" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "umol/min/ml", + CodeMeaning: "Micromole/minute/milliliter" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "ml/min/g", + CodeMeaning: "Milliliter/minute/gram" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "ml/g", + CodeMeaning: "Milliliter/gram" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "/cm", + CodeMeaning: "/Centimeter" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "umol/ml", + CodeMeaning: "Micromole/milliliter" + }, + // Units defined in https://dicom.nema.org/medical/dicom/current/output/chtml/part16/sect_CID_85.html + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "g/ml{SUVbw}", + CodeMeaning: "Standardized Uptake Value body weight" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "g/ml{SUVlbm}", + CodeMeaning: "Standardized Uptake Value lean body mass (James)" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "g/ml{SUVlbm(James128)}", + CodeMeaning: + "Standardized Uptake Value lean body mass (James 128 multiplier)" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "g/ml{SUVlbm(Janma)}", + CodeMeaning: "Standardized Uptake Value lean body mass (Janma)" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "cm2/ml{SUVbsa}", + CodeMeaning: "Standardized Uptake Value body surface area" + }, + { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "g/ml{SUVibw}", + CodeMeaning: "Standardized Uptake Value ideal body weight" + } +]; -const MM2_UNIT = { - CodeValue: "mm2", - CodingSchemeDesignator: "UCUM", - CodingSchemeVersion: "1.4", - CodeMeaning: "SquareMilliMeter" -}; +// Create unitCodeMap from knownUnits for efficient lookup +const unitCodeMap = {}; +knownUnits.forEach(unit => { + unitCodeMap[unit.CodeValue] = unit; +}); +const noUnitCodeValues = ["px", "px\xB2"]; const NO_UNIT = { CodeValue: "1", CodingSchemeDesignator: "UCUM", CodingSchemeVersion: "1.4", CodeMeaning: "px" }; +noUnitCodeValues.forEach(codeValue => { + unitCodeMap[codeValue] = NO_UNIT; +}); -const NO2_UNIT = NO_UNIT; - -const measurementMap = { - px: NO_UNIT, - mm: MM_UNIT, - mm2: MM2_UNIT, - "mm\xB2": MM2_UNIT, - "px\xB2": NO2_UNIT +unitCodeMap["mm\xB2"] = { + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeValue: "mm2", + CodeMeaning: "mm2" }; +const MM_UNIT = { + CodeValue: "mm", + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeMeaning: "millimeter" +}; /** Converts the given unit into the * specified coding values. * Has .measurementMap on the function specifying global units for measurements. @@ -39,7 +184,7 @@ const unit2CodingValue = units => { if (!units) return NO_UNIT; const space = units.indexOf(" "); const baseUnit = space === -1 ? units : units.substring(0, space); - const codingUnit = measurementMap[units] || measurementMap[baseUnit]; + const codingUnit = unitCodeMap[units] || unitCodeMap[baseUnit]; if (!codingUnit) { log.error("Unspecified units", units); return MM_UNIT; @@ -47,6 +192,6 @@ const unit2CodingValue = units => { return codingUnit; }; -unit2CodingValue.measurementMap = measurementMap; +unit2CodingValue.measurementMap = unitCodeMap; export default unit2CodingValue;