import {
dateToDateObj,
getDicomDate,
dateToTimeObj,
getDicomTime,
} from '../dicom/dicomDate.js';
import {safeGet} from '../dicom/dataElement.js';
import {
ValueTypes,
RelationshipTypes,
ContinuityOfContents,
getSRContent,
getDicomSRContentItem,
getContentTemplate,
DicomSRContent,
getSRContentFromValue
} from '../dicom/dicomSRContent.js';
import {
DcmCodes,
getDcmDicomCode,
getColourCode,
getQuantificationName,
getQuantificationUnit,
DicomCode,
getMeasurementUnitsCode,
isEqualCode
} from '../dicom/dicomCode.js';
import {
isVersionInBounds,
getDwvVersionFromImplementationClassUID
} from '../dicom/dicomParser.js';
import {MeasuredValue} from '../dicom/dicomMeasuredValue.js';
import {NumericMeasurement} from '../dicom/dicomNumericMeasurement.js';
import {getAsSimpleElements} from '../dicom/dicomTag.js';
import {getElementsFromJSONTags} from '../dicom/dicomWriter.js';
import {ImageReference} from '../dicom/dicomImageReference.js';
import {SopInstanceReference} from '../dicom/dicomSopInstanceReference.js';
import {
GraphicTypes,
getScoordFromShape,
getShapeFromScoord,
SpatialCoordinate
} from '../dicom/dicomSpatialCoordinate.js';
import {SpatialCoordinate3D} from '../dicom/dicomSpatialCoordinate3D.js';
import {BIG_EPSILON_EXPONENT} from '../math/matrix.js';
import {precisionRound} from '../utils/string.js';
import {logger} from '../utils/logger.js';
import {Annotation} from './annotation.js';
import {AnnotationGroup} from './annotationGroup.js';
import {Point2D, Point3D} from '../math/point.js';
// doc imports
/* eslint-disable no-unused-vars */
import {DataElement} from '../dicom/dataElement.js';
/* eslint-enable no-unused-vars */
/**
* Related DICOM tag keys.
*/
const TagKeys = {
ImplementationClassUID: '00020012',
StudyInstanceUID: '0020000D',
StudyID: '00200010',
SeriesInstanceUID: '0020000E',
SeriesNumber: '00200011',
Modality: '00080060',
PatientName: '00100010',
PatientID: '00100020',
PatientBirthDate: '00100030',
PatientSex: '00100040',
CurrentRequestedProcedureEvidenceSequence: '0040A375'
};
/**
* Merge two tag lists.
*
* @param {object} tags1 Base list, will be modified.
* @param {object} tags2 List to merge.
*/
function mergeTags(tags1, tags2) {
const keys2 = Object.keys(tags2);
for (const tagName2 of keys2) {
if (tags1[tagName2] !== undefined) {
logger.debug('Overwritting tag: ' + tagName2);
}
tags1[tagName2] = tags2[tagName2];
}
}
/**
* Response evaluation class.
*/
export class ResponseEvaluation {
/**
* Current response.
*
* @type {DicomCode}
*/
current;
/**
* Measurement of response (mm).
*
* @type {number}
*/
measure;
}
/**
* CAD report class.
*/
export class CADReport {
/**
* @type {AnnotationGroup[]}
*/
annotationGroups;
/**
* @type {ResponseEvaluation[]}
*/
responseEvaluations;
/**
* @type {string}
*/
comment;
}
/**
* {@link AnnotationGroup} factory.
*/
export class AnnotationGroupFactory {
/**
* Possible warning created by checkElements.
*
* @type {string|undefined}
*/
#warning;
/**
* Get a warning string if elements are not as expected.
* Created by checkElements.
*
* @returns {string|undefined} The warning.
*/
getWarning() {
return this.#warning;
}
/**
* Check if input elements contain a dwv 0.34 annotation.
*
* @param {Object<string, DataElement>} dataElements The DICOM data elements.
* @returns {boolean} True if the elements contain a dwv 0.34 annotation.
*/
#isDwv034AnnotationDicomSR(dataElements) {
// version
const classUID =
safeGet(dataElements, TagKeys.ImplementationClassUID);
const dwvVersion = getDwvVersionFromImplementationClassUID(classUID);
const isDwv034 = typeof dwvVersion !== 'undefined' &&
isVersionInBounds(dwvVersion, '0.34.0', '0.35.0-beta.21');
// content template
const contentTemplate = getContentTemplate(dataElements);
// root SR concept
let rootConcept;
const srContent = getSRContent(dataElements);
if (typeof srContent.conceptNameCode !== 'undefined') {
rootConcept = srContent.conceptNameCode.value;
}
// dwv 0.34 annotations do not have template
return isDwv034 &&
typeof contentTemplate === 'undefined' &&
rootConcept === DcmCodes.MeasurementGroup.value;
}
/**
* Check if input elements contain a TID 1500 annotation.
* Ref: {@link https://dicom.nema.org/medical/Dicom/2022a/output/chtml/part16/chapter_A.html#sect_TID_1500}.
*
* @param {Object<string, DataElement>} dataElements The DICOM data elements.
* @returns {boolean} True if the elements contain a TID 1500 annotation.
*/
#isTid1500AnnotationDicomSR(dataElements) {
// content template
const contentTemplate = getContentTemplate(dataElements);
const isTid1500Template = contentTemplate === 'DCMR-1500';
// root SR concept
let rootConcept;
const srContent = getSRContent(dataElements);
if (typeof srContent.conceptNameCode !== 'undefined') {
rootConcept = srContent.conceptNameCode.value;
}
const isImagingMeasurementReport =
rootConcept === DcmCodes.ImagingMeasurementReport.value;
let res = false;
if (isTid1500Template && isImagingMeasurementReport) {
// check for at least one:
// ImagingMeasurements
// - MeasurementGroup
// - ImageRegion (SCOORD) OR TID 300 Measure
const imagingMeas = srContent.contentSequence.find(
this.#isImagingMeasurementsItem
);
if (typeof imagingMeas !== 'undefined') {
const measGroup = imagingMeas.contentSequence.find(
this.#isMeasurementGroupItem
);
if (typeof measGroup !== 'undefined') {
const imageRegion = measGroup.contentSequence.find(
this.#isImageRegionItem
);
const measure = measGroup.contentSequence.find(
this.#isTid300Measurement
);
if (typeof imageRegion !== 'undefined' ||
typeof measure !== 'undefined'
) {
res = true;
}
}
}
}
return res;
}
/**
* Check if input elements contain a TID 4100 template.
* Ref: {@link https://dicom.nema.org/medical/Dicom/2022a/output/chtml/part16/chapter_A.html#sect_TID_4100}.
*
* @param {Object<string, DataElement>} dataElements The DICOM data elements.
* @returns {boolean} True if the elements contain a TID 4100 template.
*/
#isTid4100DicomSR(dataElements) {
// content template
const contentTemplate = getContentTemplate(dataElements);
return contentTemplate === 'DCMR-4100';
}
/**
* Check dicom elements.
*
* @param {Object<string, DataElement>} dataElements The DICOM data elements.
* @returns {string|undefined} A possible warning.
* @throws Error for missing or wrong data.
*/
checkElements(dataElements) {
// reset
this.#warning = undefined;
if (!this.#isDwv034AnnotationDicomSR(dataElements) &&
!this.#isTid1500AnnotationDicomSR(dataElements) &&
!this.#isTid4100DicomSR(dataElements)) {
this.#warning = 'Not a dwv supported annotation';
}
return this.#warning;
}
/**
* Add the source image to an annotation.
*
* @param {Annotation} annotation The annotation.
* @param {DicomSRContent} content The content to add.
*/
#addSourceImageToAnnotation(annotation, content) {
if (content.valueType === ValueTypes.image &&
content.relationshipType === RelationshipTypes.selectedFrom) {
annotation.referencedSopClassUID =
content.value.referencedSOPSequence.referencedSOPClassUID;
annotation.referencedSopInstanceUID =
content.value.referencedSOPSequence.referencedSOPInstanceUID;
if (typeof content.value.referencedFrameNumber !== 'undefined') {
annotation.referencedFrameNumber =
parseInt(content.value.referencedFrameNumber, 10);
}
}
}
/**
* Add ids to an annotation folowing the (wrong)
* dwv 0.34 format.
*
* @param {Annotation} annotation The annotation.
* @param {DicomSRContent} content The content to add.
*/
#addDwv034IdToAnnotation(annotation, content) {
// annotation id
if (content.hasHeader(
ValueTypes.uidref,
getDcmDicomCode(DcmCodes.TrackingIdentifier),
RelationshipTypes.hasProperties
)) {
annotation.trackingId = content.value;
// use it as uid
annotation.trackingUid = content.value;
}
}
/**
* Add ids to an annotation.
*
* @param {Annotation} annotation The annotation.
* @param {DicomSRContent} content The content to add.
*/
#addIdsToAnnotation(annotation, content) {
// annotation id
if (content.hasHeader(
ValueTypes.text,
getDcmDicomCode(DcmCodes.TrackingIdentifier),
RelationshipTypes.hasObsContext
)) {
annotation.trackingId = content.value;
}
// annotation uid
if (content.hasHeader(
ValueTypes.uidref,
getDcmDicomCode(DcmCodes.TrackingUniqueIdentifier),
RelationshipTypes.hasObsContext
)) {
annotation.trackingUid = content.value;
}
}
/**
* Add content to an annotation.
*
* @param {Annotation} annotation The annotation.
* @param {DicomSRContent} content The content to add.
* @param {boolean} [isDwv034] True if the content was written using dwv 0.34,
* defaults to false.
*/
#addContentToAnnotation(annotation, content, isDwv034) {
if (typeof isDwv034 === 'undefined') {
isDwv034 = false;
}
let relationshipType = RelationshipTypes.hasConceptMod;
if (isDwv034) {
relationshipType = RelationshipTypes.hasProperties;
}
// text expr
if (content.hasHeader(
ValueTypes.text,
getDcmDicomCode(DcmCodes.ShortLabel),
relationshipType
)) {
annotation.textExpr = content.value;
// optional label position
const scoord = content.contentSequence.find(function (item) {
return item.hasHeader(
ValueTypes.scoord,
getDcmDicomCode(DcmCodes.ReferencePoints),
RelationshipTypes.hasProperties
);
});
if (typeof scoord !== 'undefined') {
annotation.labelPosition = new Point2D(
scoord.value.graphicData[0],
scoord.value.graphicData[1]
);
}
}
// color
if (content.hasHeader(
ValueTypes.text,
getColourCode(),
relationshipType
)) {
annotation.colour = content.value;
}
// reference points
if (content.hasHeader(
ValueTypes.scoord,
getDcmDicomCode(DcmCodes.ReferencePoints),
relationshipType) &&
content.value.graphicType === GraphicTypes.multipoint
) {
const points = [];
for (let i = 0; i < content.value.graphicData.length; i += 2) {
points.push(new Point2D(
content.value.graphicData[i],
content.value.graphicData[i + 1]
));
}
annotation.referencePoints = points;
}
// plane points
if (content.hasHeader(
ValueTypes.scoord3d,
getDcmDicomCode(DcmCodes.ReferenceGeometry),
RelationshipTypes.contains) &&
content.value.graphicType === GraphicTypes.multipoint
) {
const data = content.value.graphicData;
const points = [];
const nPoints = Math.floor(data.length / 3);
for (let i = 0; i < nPoints; ++i) {
const j = i * 3;
points.push(new Point3D(data[j], data[j + 1], data[j + 2]));
}
annotation.planePoints = points;
}
// quantification
this.#addQuantificationToAnnotation(annotation, content);
// meta
this.#addMetaToAnnotation(annotation, content);
}
/**
* Add quantification to an annotation.
*
* @param {Annotation} annotation The annotation.
* @param {DicomSRContent} content The content to add.
* @param {string} [relationshipType] The content relationshipType, defaults
* to 'CONTAINS'.
*/
#addQuantificationToAnnotation(annotation, content, relationshipType) {
let relation;
if (typeof relationshipType === 'undefined') {
relation = RelationshipTypes.contains;
}
if (content.valueType === ValueTypes.num &&
content.relationshipType === relation) {
const quantifName =
getQuantificationName(content.conceptNameCode);
if (typeof quantifName !== 'undefined') {
const measuredValue = content.value.measuredValue;
const quantifUnit = getQuantificationUnit(
measuredValue.measurementUnitsCode);
if (typeof annotation.quantification === 'undefined') {
annotation.quantification = {};
}
annotation.quantification[quantifName] = {
value: measuredValue.numericValue,
unit: quantifUnit
};
}
}
}
/**
* Add meta to an annotation.
*
* @param {Annotation} annotation The annotation.
* @param {DicomSRContent} content The content to add.
*/
#addMetaToAnnotation(annotation, content) {
if ((content.valueType === ValueTypes.code ||
content.valueType === ValueTypes.text) &&
content.relationshipType === RelationshipTypes.contains) {
annotation.addMetaItem(
content.conceptNameCode,
content.value
);
}
}
/**
* Check if a DicomSRContent is an 'ImagingMeasurements'.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (CONTAINS) CONTAINER: (126010, DCM, 'Imaging Measurements').
*/
#isImagingMeasurementsItem(item) {
return item.hasHeader(
ValueTypes.container,
getDcmDicomCode(DcmCodes.ImagingMeasurements),
RelationshipTypes.contains
);
}
/**
* Check if a DicomSRContent is a 'MeasurementGroup'.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (CONTAINS) CONTAINER: (125007, DCM, 'Measurement Group').
*/
#isMeasurementGroupItem(item) {
return item.hasHeader(
ValueTypes.container,
getDcmDicomCode(DcmCodes.MeasurementGroup),
RelationshipTypes.contains
);
}
/**
* Check if a DicomSRContent is a 'SingleImageFinding' code.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (INFERRED FROM) CODE: (111059, DCM, 'Single Image Finding').
*/
#isSingleImageFindingItem(item) {
return item.hasHeader(
ValueTypes.code,
getDcmDicomCode(DcmCodes.SingleImageFinding),
RelationshipTypes.inferredFrom
);
}
/**
* Check if a DicomSRContent is a 'ResponseEvaluation' container.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (HAS PROPERTIES) CODE: (112020, DCM, 'Response Evaluation').
*/
#isResponseEvaluationItem(item) {
return item.hasHeader(
ValueTypes.container,
getDcmDicomCode(DcmCodes.ResponseEvaluation),
RelationshipTypes.hasProperties
);
}
/**
* Check if a DicomSRContent is a 'Comment' text.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (CONTAINS) TEXT: (121106, DCM, 'Comment').
*/
#isCommentItem(item) {
return item.hasHeader(
ValueTypes.text,
getDcmDicomCode(DcmCodes.Comment),
RelationshipTypes.contains
);
}
/**
* Check if a DicomSRContent is a 'CurrentResponse' code.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (CONTAINS) CODE: (112048, DCM, 'Current Response').
*/
#isCurrentResponseItem(item) {
return item.hasHeader(
ValueTypes.code,
getDcmDicomCode(DcmCodes.CurrentResponse),
RelationshipTypes.contains
);
}
/**
* Check if a DicomSRContent is a 'MeasurementOfResponse' number.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (CONTAINS) NUM: (112051, DCM, 'Measurement Of Response').
*/
#isMeasurementOfResponseItem(item) {
return item.hasHeader(
ValueTypes.num,
getDcmDicomCode(DcmCodes.MeasurementOfResponse),
RelationshipTypes.contains
);
}
/**
* Check if a DicomSRContent is a 'CADProcessingAndFindingsSummary' code.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (CONTAINS) CODE: (111017, DCM, 'CAD Processing and Findings Summary').
*/
#isCadProcessingSummaryItem(item) {
return item.hasHeader(
ValueTypes.code,
getDcmDicomCode(DcmCodes.CADProcessingAndFindingsSummary),
RelationshipTypes.contains
);
}
/**
* Check if a DicomSRContent is an 'ImageRegion'.
*
* @param {DicomSRContent} item The item to check.
* @returns {boolean} True if item has the properties:
* (CONTAINS) SCOORD: (111030, DCM, 'Image Region').
*/
#isImageRegionItem(item) {
return item.hasHeader(
ValueTypes.scoord,
getDcmDicomCode(DcmCodes.ImageRegion),
RelationshipTypes.contains
);
}
/**
* Check is a measurement group follows TID 1410:
* it must contain an image region.
* Ref: {@link https://dicom.nema.org/medical/Dicom/2022a/output/chtml/part16/chapter_A.html#sect_TID_1410}.
*
* @param {DicomSRContent} content The SR content.
* @returns {boolean} True if an image region was found.
*/
#isTid1410MeasGroup(content) {
const scoord = content.contentSequence.find(
this.#isImageRegionItem
);
return typeof scoord !== 'undefined';
}
/**
* Convert a TID 1410 measurement group into an annotation.
*
* @param {DicomSRContent} content The SR content.
* @returns {Annotation|undefined} The annotation.
*/
#tid1410MeasGroupToAnnotation(content) {
let annotation;
// get shape from scoord
const scoord = content.contentSequence.find(
this.#isImageRegionItem
);
if (typeof scoord !== 'undefined') {
annotation = new Annotation();
// shape
annotation.mathShape = getShapeFromScoord(scoord.value);
// shape source image
const fromImage = scoord.contentSequence.find(function (item) {
return item.valueType === ValueTypes.image &&
item.relationshipType === RelationshipTypes.selectedFrom;
});
if (typeof fromImage !== 'undefined') {
this.#addSourceImageToAnnotation(annotation, fromImage);
}
for (const item of content.contentSequence) {
// shape ids
this.#addIdsToAnnotation(annotation, item);
// shape extra
this.#addContentToAnnotation(annotation, item);
}
}
return annotation;
}
/**
* Check is an SR content follows TID 300: it must be
* a 'contains' numeric value with an 'inferred from' scoord.
* Ref: {@link https://dicom.nema.org/medical/Dicom/2022a/output/chtml/part16/chapter_A.html#sect_TID_300}.
*
* @param {DicomSRContent} item The SR content.
* @returns {boolean} True if TID 300 measure.
*/
#isTid300Measurement(item) {
// no specific concept
const isContainedNum =
item.valueType === ValueTypes.num &&
item.relationshipType === RelationshipTypes.contains;
let scoord;
if (isContainedNum) {
// no specific concept (?)
scoord = item.contentSequence.find(function (subItem) {
return subItem.valueType === ValueTypes.scoord &&
subItem.relationshipType === RelationshipTypes.inferredFrom;
});
}
return typeof scoord !== 'undefined';
}
/**
* Check is an SR content follows TID 1400: it must be
* a 'has properties' numeric value with an 'inferred from' scoord.
* Ref: {@link https://dicom.nema.org/medical/Dicom/current/output/chtml/part16/chapter_A.html#sect_TID_1400}.
*
* @param {DicomSRContent} item The SR content.
* @returns {boolean} True if TID 300 measure.
*/
#isTid1400LinearMeasurement(item) {
// no specific concept
const isHasPropNum =
item.valueType === ValueTypes.num &&
item.relationshipType === RelationshipTypes.hasProperties;
let scoord;
if (isHasPropNum) {
// no specific concept (?)
scoord = item.contentSequence.find(function (subItem) {
return subItem.valueType === ValueTypes.scoord &&
subItem.relationshipType === RelationshipTypes.inferredFrom;
});
}
return typeof scoord !== 'undefined';
}
/**
* Check is a measurement group follows TID 1501:
* it must contain a measure.
* Ref: {@link https://dicom.nema.org/medical/Dicom/2022a/output/chtml/part16/chapter_A.html#sect_TID_1501}.
*
* @param {DicomSRContent} content The SR content.
* @returns {boolean} True if a measure was found.
*/
#isTid1501MeasGroup(content) {
const measure = content.contentSequence.find(
this.#isTid300Measurement
);
return typeof measure !== 'undefined';
}
/**
* Convert a TID 1501 measurement group into an annotation.
*
* @param {DicomSRContent} content The SR content.
* @returns {Annotation|undefined} The annotation.
*/
#tid1501MeasGroupToAnnotation(content) {
let annotation;
// just use the first measure to get the scoord
// (expecting all measures to refer to the same scoord)
const measure = content.contentSequence.find(
this.#isTid300Measurement
);
if (typeof measure !== 'undefined') {
annotation = new Annotation();
// shape
// no specific concept (?)
const scoord = measure.contentSequence.find(function (subItem) {
return subItem.valueType === ValueTypes.scoord &&
subItem.relationshipType === RelationshipTypes.inferredFrom;
});
annotation.mathShape = getShapeFromScoord(scoord.value);
// special point/arrow case
// TODO: not very valid...
if (annotation.mathShape instanceof Point2D &&
scoord.value.graphicData.length >= 4
) {
annotation.referencePoints = [
new Point2D(scoord.value.graphicData[2], scoord.value.graphicData[3])
];
}
// shape source image
// no specific concept (?)
const fromImage = scoord.contentSequence.find(function (item) {
return item.valueType === ValueTypes.image &&
item.relationshipType === RelationshipTypes.selectedFrom;
});
if (typeof fromImage !== 'undefined') {
this.#addSourceImageToAnnotation(annotation, fromImage);
}
}
// shape extra
if (typeof annotation !== 'undefined') {
for (const item of content.contentSequence) {
// add ids
this.#addIdsToAnnotation(annotation, item);
// add quantification
this.#addQuantificationToAnnotation(annotation, measure);
// add meta
this.#addMetaToAnnotation(annotation, item);
}
}
return annotation;
}
/**
* Convert a TID 4104 image finding into an annotation.
* Similar to tid1501MeasGroupToAnnotation apart from measure
* code and quantification relationship.
*
* @param {DicomSRContent} content The SR content.
* @returns {Annotation|undefined} The annotation.
*/
#singleImageFindingToAnnotation(content) {
let annotation;
// just use the first measure to get the scoord
// (expecting all measures to refer to the same scoord)
const measure = content.contentSequence.find(
this.#isTid1400LinearMeasurement
);
if (typeof measure !== 'undefined') {
annotation = new Annotation();
// shape
// no specific concept (?)
const scoord = measure.contentSequence.find(function (subItem) {
return subItem.valueType === ValueTypes.scoord &&
subItem.relationshipType === RelationshipTypes.inferredFrom;
});
annotation.mathShape = getShapeFromScoord(scoord.value);
// special point/arrow case
// TODO: not very valid...
if (annotation.mathShape instanceof Point2D &&
scoord.value.graphicData.length >= 4
) {
annotation.referencePoints = [
new Point2D(scoord.value.graphicData[2], scoord.value.graphicData[3])
];
}
// shape source image
// no specific concept (?)
const fromImage = scoord.contentSequence.find(function (item) {
return item.valueType === ValueTypes.image &&
item.relationshipType === RelationshipTypes.selectedFrom;
});
if (typeof fromImage !== 'undefined') {
this.#addSourceImageToAnnotation(annotation, fromImage);
}
}
// shape extra
if (typeof annotation !== 'undefined') {
for (const item of content.contentSequence) {
// add ids
this.#addIdsToAnnotation(annotation, item);
// add quantification
this.#addQuantificationToAnnotation(
annotation, measure, RelationshipTypes.hasProperties);
// add meta
this.#addMetaToAnnotation(annotation, item);
}
}
return annotation;
}
/**
* Convert an imaging measurement into an annotation group.
* Supports TID1500 > TID1410 ("Planar ROI Measurements
* and Qualitative Evaluations”) or TID1501 (“Measurement and
* Qualitative Evaluation Group”).
*
* @param {DicomSRContent} content The SR content.
* @returns {AnnotationGroup|undefined} The annotation group.
*/
#imagingMeasToAnnotationGroup(content) {
if (content.contentSequence.length === 0) {
return undefined;
}
const item0 = content.contentSequence[0];
let isTid1410 = false;
let isTid1501 = false;
if (this.#isMeasurementGroupItem(item0)) {
isTid1410 = this.#isTid1410MeasGroup(item0);
isTid1501 = this.#isTid1501MeasGroup(item0);
}
const annotations = [];
let hasMeasGroup = false;
for (const item of content.contentSequence) {
// measurement group content
if (this.#isMeasurementGroupItem(item)) {
hasMeasGroup = true;
let annotation;
if (isTid1410) {
annotation = this.#tid1410MeasGroupToAnnotation(item);
} else if (isTid1501) {
annotation = this.#tid1501MeasGroupToAnnotation(item);
}
if (typeof annotation !== 'undefined') {
annotations.push(annotation);
}
}
}
let annotationGroup;
if (!hasMeasGroup) {
logger.warn('No measurement groups in TID 1500 SR');
} else {
if (annotations.length !== 0) {
annotationGroup = new AnnotationGroup(annotations);
} else {
logger.warn('No valid measurement groups in TID 1500 SR');
}
}
return annotationGroup;
}
/**
* Convert a CAD processing summary into an annotation group.
*
* @param {DicomSRContent} content The SR content.
* @returns {AnnotationGroup[]|undefined} The annotation group.
*/
#cadProcessingSummaryToAnnotationGroups(content) {
// get annotations
const annotations = [];
let hasMeasGroup = false;
for (const item of content.contentSequence) {
// measurement group content
if (this.#isSingleImageFindingItem(item)) {
hasMeasGroup = true;
const annotation = this.#singleImageFindingToAnnotation(item);
if (typeof annotation !== 'undefined') {
annotations.push(annotation);
}
}
}
// TODO: split annotations according to referenced image.
// create group
const annotationGroups = [];
if (!hasMeasGroup) {
logger.warn('No image findings in TID 4100 SR');
} else {
if (annotations.length !== 0) {
annotationGroups.push(new AnnotationGroup(annotations));
} else {
logger.warn('No valid measurement groups in TID 4100 SR');
}
}
return annotationGroups;
}
/**
* Convert a DICOM SR content of type SCOORD into an annotation.
*
* @param {DicomSRContent} content The input SCOORD.
* @returns {Annotation} The annotation.
*/
#dwv034ScoordToAnnotation(content) {
const annotation = new Annotation();
// shape
annotation.mathShape = getShapeFromScoord(content.value);
for (const item of content.contentSequence) {
// shape source image
this.#addSourceImageToAnnotation(annotation, item);
// shape id
this.#addDwv034IdToAnnotation(annotation, item);
// shape extra
this.#addContentToAnnotation(annotation, item, true);
}
return annotation;
}
/**
* Convert a DICOM SR content folowing TID 1500 into a list of annotations.
*
* Structure: 'Imaging Measurement Report' >
* 'Imaging Measurements' > 'Measurement Group'.
*
* Measurement Group can follow TID1410 "Planar ROI Measurements
* and Qualitative Evaluations” (scoord and measurements at
* same level) or TID1501 “Measurement and Qualitative Evaluation Group”
* (measurements with scoord child).
*
* @param {DicomSRContent} content The input SR content.
* @returns {AnnotationGroup|undefined} The annotation group.
*/
#tid1500ToAnnotationGroup(content) {
if (!(content.valueType === ValueTypes.container &&
isEqualCode(
content.conceptNameCode,
getDcmDicomCode(DcmCodes.ImagingMeasurementReport)
)
)) {
logger.warn('Not the expected TID 1500 SR content header');
}
let annotationGroup;
// imaging measurements content
const imagingMeas = content.contentSequence.find(
this.#isImagingMeasurementsItem
);
if (typeof imagingMeas !== 'undefined') {
annotationGroup = this.#imagingMeasToAnnotationGroup(imagingMeas);
} else {
logger.warn('No imaging measurements in TID 1500 SR');
}
return annotationGroup;
}
/**
* Get the CAD processing and findings summary from
* and input SR content.
*
* @param {DicomSRContent} content The input SR content.
* @returns {DicomSRContent|undefined} The summary.
*/
#getTid4100Summary(content) {
if (!(content.valueType === ValueTypes.container &&
isEqualCode(
content.conceptNameCode,
getDcmDicomCode(DcmCodes.ChestCADReport)
)
)) {
logger.warn('Not the expected TID 4100 SR content header');
}
// CAD processing summary content
return content.contentSequence.find(
this.#isCadProcessingSummaryItem
);
}
/**
* Convert a DICOM SR content 'Measurement group' into a list of annotations.
*
* Structure: (root) 'Measurement group'
* - scoord,
* - meta.
*
* @param {DicomSRContent} content The input.
* @returns {AnnotationGroup|undefined} The annotation.
*/
#dwv034MeasGroupToAnnotationGroup(content) {
if (!(content.valueType === ValueTypes.container &&
isEqualCode(
content.conceptNameCode,
getDcmDicomCode(DcmCodes.MeasurementGroup)
)
)) {
console.warn('Not the expected dwv034 content header');
}
const annotations = [];
for (const item of content.contentSequence) {
if (item.valueType === ValueTypes.scoord) {
annotations.push(this.#dwv034ScoordToAnnotation(item));
}
}
return new AnnotationGroup(annotations);
}
/**
* Add root meta data to an annotation group.
*
* @param {AnnotationGroup} annotationGroup The group to add meta to.
* @param {Object<string, DataElement>} dataElements The DICOM tags.
*/
#addMetaToAnnotationGroup(annotationGroup, dataElements) {
const safeGetLocal = function (key) {
return safeGet(dataElements, key);
};
// study
annotationGroup.setMetaValue('StudyInstanceUID',
safeGetLocal(TagKeys.StudyInstanceUID));
annotationGroup.setMetaValue('StudyID',
safeGetLocal(TagKeys.StudyID));
// series
annotationGroup.setMetaValue('SeriesInstanceUID',
safeGetLocal(TagKeys.SeriesInstanceUID));
annotationGroup.setMetaValue('SeriesNumber',
safeGetLocal(TagKeys.SeriesNumber));
// modality
annotationGroup.setMetaValue('Modality',
safeGetLocal(TagKeys.Modality));
// patient
annotationGroup.setMetaValue('PatientName',
safeGetLocal(TagKeys.PatientName));
annotationGroup.setMetaValue('PatientID',
safeGetLocal(TagKeys.PatientID));
annotationGroup.setMetaValue('PatientBirthDate',
safeGetLocal(TagKeys.PatientBirthDate));
annotationGroup.setMetaValue('PatientSex',
safeGetLocal(TagKeys.PatientSex));
// reference
const evidenceTagKey = TagKeys.CurrentRequestedProcedureEvidenceSequence;
const evidenceSq = dataElements[evidenceTagKey];
if (typeof evidenceSq !== 'undefined') {
const evidenceSqElement = {
[evidenceTagKey]: evidenceSq
};
const evidences = getAsSimpleElements(evidenceSqElement);
annotationGroup.setMetaValue(
'CurrentRequestedProcedureEvidenceSequence',
evidences.CurrentRequestedProcedureEvidenceSequence
);
}
}
/**
* Get an {@link AnnotationGroup} object from the read DICOM file.
*
* @param {Object<string, DataElement>} dataElements The DICOM tags.
* @returns {AnnotationGroup} A new annotation group.
* @throws Error for missing or wrong data.
*/
create(dataElements) {
const srContent = getSRContent(dataElements);
let annotationGroup;
let srType;
if (this.#isTid1500AnnotationDicomSR(dataElements)) {
srType = 'TID 1500 SR';
annotationGroup = this.#tid1500ToAnnotationGroup(srContent);
} else if (this.#isDwv034AnnotationDicomSR(dataElements)) {
logger.warn('DWV v0.34 annotation');
srType = 'DWV v0.34 SR';
annotationGroup = this.#dwv034MeasGroupToAnnotationGroup(srContent);
}
// CAD report
if (this.#isTid4100DicomSR(dataElements)) {
srType = 'TID 4100 SR';
const report = this.createCADReport(dataElements);
if (typeof report !== 'undefined') {
annotationGroup = report.annotationGroups[0];
// console.log('-- CAD report -- ');
// console.log('comment', report.comment);
// console.log('evaluations', report.responseEvaluations);
}
}
if (typeof annotationGroup === 'undefined') {
throw new Error('Cannot create annotation group from ' + srType);
}
// add dicom meta
this.#addMetaToAnnotationGroup(annotationGroup, dataElements);
return annotationGroup;
}
/**
* Get an {@link CADReport} object from the read DICOM file.
*
* @param {Object<string, DataElement>} dataElements The DICOM tags.
* @returns {CADReport|undefined} A new CAD report.
*/
createCADReport(dataElements) {
const srContent = getSRContent(dataElements);
// get the summary
const summary = this.#getTid4100Summary(srContent);
if (typeof summary === 'undefined') {
logger.warn('No CAD processing and Findings Summary in TID 4100 SR');
return;
}
let annotationGroups = [];
if (this.#isTid4100DicomSR(dataElements)) {
annotationGroups = this.#cadProcessingSummaryToAnnotationGroups(summary);
}
if (typeof annotationGroups === 'undefined') {
throw new Error('Cannot create annotation groups from TID 4100 SR');
}
// add dicom meta
for (const group of annotationGroups) {
this.#addMetaToAnnotationGroup(group, dataElements);
}
// evaluations
const evaluations = [];
let comment;
for (const item of summary.contentSequence) {
// measurement group content
if (this.#isResponseEvaluationItem(item)) {
const evaluation = this.#tid4106ResponseEvaluationToResponse(item);
if (typeof evaluation !== 'undefined') {
evaluations.push(evaluation);
}
} else if (this.#isCommentItem(item)) {
comment = item.value;
}
}
const report = new CADReport();
report.annotationGroups = annotationGroups;
report.responseEvaluations = evaluations;
report.comment = comment;
return report;
}
/**
* Get the annotation source image as SR content.
*
* @param {Annotation} annotation The input annotation.
* @returns {DicomSRContent} The SR content.
*/
#getAnnotationSourceImageContent(annotation) {
// reference image UID
const srImage = new DicomSRContent(ValueTypes.image);
srImage.relationshipType = RelationshipTypes.selectedFrom;
srImage.conceptNameCode = getDcmDicomCode(DcmCodes.SourceImage);
const sopRef = new SopInstanceReference();
sopRef.referencedSOPClassUID = annotation.referencedSopClassUID;
sopRef.referencedSOPInstanceUID = annotation.referencedSopInstanceUID;
const imageRef = new ImageReference();
imageRef.referencedSOPSequence = sopRef;
if (typeof annotation.referencedFrameNumber !== 'undefined') {
imageRef.referencedFrameNumber =
annotation.referencedFrameNumber.toString();
}
srImage.value = imageRef;
return srImage;
}
/**
* Get the annotation meta as SR content list.
*
* @param {Annotation} annotation The input annotation.
* @returns {DicomSRContent[]} The SR content list.
*/
#getAnnotationContentSequence(annotation) {
const contentSequence = [];
// annotation id
const srId = new DicomSRContent(ValueTypes.text);
srId.relationshipType = RelationshipTypes.hasObsContext;
srId.conceptNameCode = getDcmDicomCode(DcmCodes.TrackingIdentifier);
srId.value = annotation.trackingId;
contentSequence.push(srId);
// annotation uid
const srUid = new DicomSRContent(ValueTypes.uidref);
srUid.relationshipType = RelationshipTypes.hasObsContext;
srUid.conceptNameCode = getDcmDicomCode(DcmCodes.TrackingUniqueIdentifier);
srUid.value = annotation.trackingUid;
contentSequence.push(srUid);
// text expr
if (typeof annotation.textExpr !== 'undefined' &&
annotation.textExpr.length !== 0
) {
const shortLabel = new DicomSRContent(ValueTypes.text);
shortLabel.relationshipType = RelationshipTypes.hasConceptMod;
shortLabel.conceptNameCode = getDcmDicomCode(DcmCodes.ShortLabel);
shortLabel.value = annotation.textExpr;
// label position
if (typeof annotation.labelPosition !== 'undefined') {
const labelPosition = new DicomSRContent(ValueTypes.scoord);
labelPosition.relationshipType = RelationshipTypes.hasProperties;
labelPosition.conceptNameCode =
getDcmDicomCode(DcmCodes.ReferencePoints);
const labelPosScoord = new SpatialCoordinate();
labelPosScoord.graphicType = GraphicTypes.point;
const graphicData = [
annotation.labelPosition.getX().toString(),
annotation.labelPosition.getY().toString()
];
labelPosScoord.graphicData = graphicData;
labelPosition.value = labelPosScoord;
const srcImage = this.#getAnnotationSourceImageContent(annotation);
labelPosition.contentSequence = [srcImage];
// add position to label sequence
shortLabel.contentSequence = [labelPosition];
}
contentSequence.push(shortLabel);
}
// colour
const colour = new DicomSRContent(ValueTypes.text);
colour.relationshipType = RelationshipTypes.hasConceptMod;
colour.conceptNameCode = getColourCode();
colour.value = annotation.colour;
contentSequence.push(colour);
// reference points
if (typeof annotation.referencePoints !== 'undefined') {
const referencePoints = new DicomSRContent(ValueTypes.scoord);
referencePoints.relationshipType = RelationshipTypes.hasConceptMod;
referencePoints.conceptNameCode =
getDcmDicomCode(DcmCodes.ReferencePoints);
const refPointsScoord = new SpatialCoordinate();
refPointsScoord.graphicType = GraphicTypes.multipoint;
const graphicData = [];
for (const point of annotation.referencePoints) {
graphicData.push(point.getX().toString());
graphicData.push(point.getY().toString());
}
refPointsScoord.graphicData = graphicData;
referencePoints.value = refPointsScoord;
const srcImage = this.#getAnnotationSourceImageContent(annotation);
referencePoints.contentSequence = [srcImage];
contentSequence.push(referencePoints);
}
// plane points
if (typeof annotation.planePoints !== 'undefined') {
const planePoints = new DicomSRContent(ValueTypes.scoord3d);
planePoints.relationshipType = RelationshipTypes.contains;
planePoints.conceptNameCode = getDcmDicomCode(DcmCodes.ReferenceGeometry);
const pointsScoord = new SpatialCoordinate3D();
pointsScoord.graphicType = GraphicTypes.multipoint;
const graphicData = [];
for (const planePoint of annotation.planePoints) {
graphicData.push(planePoint.getX().toString());
graphicData.push(planePoint.getY().toString());
graphicData.push(planePoint.getZ().toString());
}
pointsScoord.graphicData = graphicData;
planePoints.value = pointsScoord;
const srcImage = this.#getAnnotationSourceImageContent(annotation);
planePoints.contentSequence = [srcImage];
contentSequence.push(planePoints);
}
// quantification
if (typeof annotation.quantification !== 'undefined') {
for (const key in annotation.quantification) {
const quantifContent = getSRContentFromValue(
key,
precisionRound(
annotation.quantification[key].value,
BIG_EPSILON_EXPONENT
),
annotation.quantification[key].unit
);
if (typeof quantifContent !== 'undefined') {
contentSequence.push(quantifContent);
}
}
}
// meta
const conceptIds = annotation.getMetaConceptIds();
for (const conceptId of conceptIds) {
const item = annotation.getMetaItem(conceptId);
let valueType = ValueTypes.text;
if (item.value instanceof DicomCode) {
valueType = ValueTypes.code;
}
const meta = new DicomSRContent(valueType);
meta.relationshipType = RelationshipTypes.contains;
meta.conceptNameCode = item.concept;
meta.value = item.value;
contentSequence.push(meta);
}
return contentSequence;
}
/**
* Convert an annotation into a 'Measurement Group' SR content.
*
* @param {Annotation} annotation The input annotation.
* @returns {DicomSRContent} The result SR content.
*/
#annotationToTid1410MeasGroup(annotation) {
// measurement group
const srContent = new DicomSRContent(ValueTypes.container);
srContent.relationshipType = RelationshipTypes.contains;
srContent.conceptNameCode = getDcmDicomCode(DcmCodes.MeasurementGroup);
srContent.value = ContinuityOfContents.separate;
// scoord
const srScoord = new DicomSRContent(ValueTypes.scoord);
srScoord.relationshipType = RelationshipTypes.contains;
srScoord.conceptNameCode = getDcmDicomCode(DcmCodes.ImageRegion);
srScoord.value = getScoordFromShape(annotation.mathShape);
const srcImage = this.#getAnnotationSourceImageContent(annotation);
srScoord.contentSequence = [srcImage];
// add extras
srContent.contentSequence = [srScoord].concat(
this.#getAnnotationContentSequence(annotation));
return srContent;
}
/**
* Convert an annotation group into a TID 1500 report SR content
* (internally using TID 1410).
*
* @param {AnnotationGroup} annotationGroup The input annotation group.
* @returns {DicomSRContent|undefined} The result SR content.
*/
#annotationGroupToTid1500(annotationGroup) {
let srContent;
if (annotationGroup.getList().length !== 0) {
// imaging measurements
const measContent = new DicomSRContent(ValueTypes.container);
measContent.conceptNameCode =
getDcmDicomCode(DcmCodes.ImagingMeasurements);
measContent.relationshipType = RelationshipTypes.contains;
measContent.value = ContinuityOfContents.separate;
const contentSequence = [];
for (const annotation of annotationGroup.getList()) {
contentSequence.push(
this.#annotationToTid1410MeasGroup(annotation)
);
}
measContent.contentSequence = contentSequence;
// imaging measurements report
srContent = new DicomSRContent(ValueTypes.container);
srContent.conceptNameCode =
getDcmDicomCode(DcmCodes.ImagingMeasurementReport);
srContent.value = ContinuityOfContents.separate;
srContent.contentSequence = [measContent];
}
return srContent;
}
/**
* Convert an annotation group into a DICOM SR object using the
* TID 1500 template.
*
* @param {AnnotationGroup} annotationGroup The annotation group.
* @param {Object<string, any>} [extraTags] Optional list of extra tags.
* @returns {Object<string, DataElement>} A list of dicom elements.
*/
toDicom(annotationGroup, extraTags) {
let tags = annotationGroup.getMeta();
// transfer syntax: ExplicitVRLittleEndian
tags.TransferSyntaxUID = '1.2.840.10008.1.2.1';
// class: Comprehensive 3D SR Storage
// https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_A.35.13.html
tags.SOPClassUID = '1.2.840.10008.5.1.4.1.1.88.34';
tags.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.88.34';
tags.CompletionFlag = 'PARTIAL';
tags.VerificationFlag = 'UNVERIFIED';
// date
const now = new Date();
tags.ContentDate = getDicomDate(dateToDateObj(now));
tags.ContentTime = getDicomTime(dateToTimeObj(now));
// reference
const evidenceSq = tags.CurrentRequestedProcedureEvidenceSequence;
// hoping for just one element...
const evidenceSq0 = evidenceSq.value[0];
const refSeriesSq = evidenceSq0.ReferencedSeriesSequence;
// hoping for just one element...
const refSeriesSq0 = refSeriesSq.value[0];
let refSopSq = refSeriesSq0.ReferencedSOPSequence;
if (typeof refSopSq === 'undefined') {
refSeriesSq0.ReferencedSOPSequence = {
value: []
};
refSopSq = refSeriesSq0.ReferencedSOPSequence;
}
const refs = refSopSq.value;
// add reference if not yet present
for (const annotation of annotationGroup.getList()) {
const ref = {
ReferencedSOPInstanceUID: annotation.referencedSopInstanceUID,
ReferencedSOPClassUID: annotation.referencedSopClassUID
};
const isSameRef = function (item) {
return item.ReferencedSOPInstanceUID === ref.ReferencedSOPInstanceUID &&
item.ReferencedSOPClassUID === ref.ReferencedSOPClassUID;
};
if (typeof refs.find(isSameRef) === 'undefined') {
refs.push(ref);
}
}
// TID 1500
tags.ContentTemplateSequence = {
value: [{
MappingResource: 'DCMR',
TemplateIdentifier: '1500'
}]
};
const srContent = this.#annotationGroupToTid1500(annotationGroup);
// main
if (typeof srContent !== 'undefined') {
tags = {
...tags,
...getDicomSRContentItem(srContent)
};
} else {
throw new Error('No annotation group SR content');
}
// merge extra tags if provided
if (typeof extraTags !== 'undefined') {
mergeTags(tags, extraTags);
}
return getElementsFromJSONTags(tags);
}
/**
* Convert an annotation into a 'Single Image Finding' SR content.
*
* @param {Annotation} annotation The input annotation.
* @returns {DicomSRContent} The result SR content.
*/
#annotationToTid4104SingleImageFinding(annotation) {
// image finding
const srContent = new DicomSRContent(ValueTypes.code);
srContent.relationshipType = RelationshipTypes.inferredFrom;
srContent.conceptNameCode = getDcmDicomCode(DcmCodes.SingleImageFinding);
// TODO: CID 6101
srContent.value = getDcmDicomCode(DcmCodes.SelectedRegion);
srContent.contentSequence = [];
// annotation id
const srId = new DicomSRContent(ValueTypes.text);
srId.relationshipType = RelationshipTypes.hasObsContext;
srId.conceptNameCode = getDcmDicomCode(DcmCodes.TrackingIdentifier);
srId.value = annotation.trackingId;
srContent.contentSequence.push(srId);
// annotation uid
const srUid = new DicomSRContent(ValueTypes.uidref);
srUid.relationshipType = RelationshipTypes.hasObsContext;
srUid.conceptNameCode = getDcmDicomCode(DcmCodes.TrackingUniqueIdentifier);
srUid.value = annotation.trackingUid;
srContent.contentSequence.push(srUid);
// quantification
if (typeof annotation.quantification !== 'undefined') {
for (const key in annotation.quantification) {
const quantifContent = getSRContentFromValue(
key,
precisionRound(
annotation.quantification[key].value,
BIG_EPSILON_EXPONENT
),
annotation.quantification[key].unit,
RelationshipTypes.hasProperties
);
if (typeof quantifContent !== 'undefined') {
// scoord as 'has properties'
const srScoord = new DicomSRContent(ValueTypes.scoord);
srScoord.relationshipType = RelationshipTypes.inferredFrom;
srScoord.conceptNameCode = getDcmDicomCode(DcmCodes.Path);
srScoord.value = getScoordFromShape(annotation.mathShape);
const srcImage = this.#getAnnotationSourceImageContent(annotation);
srScoord.contentSequence = [srcImage];
// add scoord to quantif
quantifContent.contentSequence = [srScoord];
// add quantif to root
srContent.contentSequence.push(quantifContent);
}
}
}
// meta
const conceptIds = annotation.getMetaConceptIds();
for (const conceptId of conceptIds) {
const item = annotation.getMetaItem(conceptId);
let valueType = ValueTypes.text;
if (item.value instanceof DicomCode) {
valueType = ValueTypes.code;
}
const meta = new DicomSRContent(valueType);
meta.relationshipType = RelationshipTypes.contains;
meta.conceptNameCode = item.concept;
meta.value = item.value;
srContent.contentSequence.push(meta);
}
return srContent;
}
/**
* Convert an annotation group into a TID 1500 report SR content
* (internally using TID 1410).
*
* @param {AnnotationGroup} annotationGroup The input annotation group.
* @param {DicomSRContent[]} contentSequence The content sequence to add to.
*/
#addAnnotationGroupToTid4101Sequence(annotationGroup, contentSequence) {
if (annotationGroup.getList().length !== 0) {
for (const annotation of annotationGroup.getList()) {
contentSequence.push(
this.#annotationToTid4104SingleImageFinding(annotation)
);
}
}
}
/**
* Get the SR content for a response evaluation.
*
* @param {ResponseEvaluation|undefined} response The response evaluation.
* @returns {DicomSRContent} The SR content.
*/
#responseToTid4106ResponseEvaluation(response) {
const srEvalutation = new DicomSRContent(ValueTypes.container);
srEvalutation.relationshipType = RelationshipTypes.hasProperties;
srEvalutation.conceptNameCode =
getDcmDicomCode(DcmCodes.ResponseEvaluation);
srEvalutation.value = ContinuityOfContents.separate;
srEvalutation.contentSequence = [];
// method: RECIST
const srMethod = new DicomSRContent(ValueTypes.code);
srMethod.relationshipType = RelationshipTypes.hasObsContext;
srMethod.conceptNameCode =
getDcmDicomCode(DcmCodes.ResponseEvaluationMethod);
srMethod.value = getDcmDicomCode(DcmCodes.RECIST);
srEvalutation.contentSequence.push(srMethod);
if (typeof response !== 'undefined') {
// current response
if (typeof response.current !== 'undefined') {
const srResponse = new DicomSRContent(ValueTypes.code);
srResponse.relationshipType = RelationshipTypes.contains;
srResponse.conceptNameCode = getDcmDicomCode(DcmCodes.CurrentResponse);
srResponse.value = response.current;
srEvalutation.contentSequence.push(srResponse);
}
// measurement of response
if (typeof response.measure !== 'undefined') {
const srMeas = new DicomSRContent(ValueTypes.num);
srMeas.relationshipType = RelationshipTypes.contains;
srMeas.conceptNameCode =
getDcmDicomCode(DcmCodes.MeasurementOfResponse);
const measure = new MeasuredValue();
measure.numericValue = response.measure;
measure.measurementUnitsCode = getMeasurementUnitsCode('unit.mm');
const numMeasure = new NumericMeasurement();
numMeasure.measuredValue = measure;
srMeas.value = numMeasure;
srEvalutation.contentSequence.push(srMeas);
}
}
return srEvalutation;
}
/**
* Convert a TID 4106 response evaluation into an response evaluation.
*
* @param {DicomSRContent} content The SR content.
* @returns {ResponseEvaluation|undefined} The response.
*/
#tid4106ResponseEvaluationToResponse(content) {
let currentResponse;
let measure;
for (const item of content.contentSequence) {
if (this.#isCurrentResponseItem(item)) {
currentResponse = item.value;
} else if (this.#isMeasurementOfResponseItem(item)) {
measure = item.value.measuredValue.numericValue;
}
}
let response;
if (typeof currentResponse !== 'undefined') {
response = new ResponseEvaluation();
response.current = currentResponse;
if (typeof measure !== 'undefined') {
response.measure = measure;
}
}
return response;
}
/**
* Convert a CAD report into a DICOM CAD report SR object using
* the TID 4100 template.
*
* @param {CADReport} report The CAD report.
* @param {Object<string, any>} [extraTags] Optional list of extra tags.
* @returns {Object<string, DataElement>} A list of dicom elements.
*/
toDicomCADReport(report, extraTags) {
// first group as tag base
let tags = report.annotationGroups[0].getMeta();
// transfer syntax: ExplicitVRLittleEndian
tags.TransferSyntaxUID = '1.2.840.10008.1.2.1';
// class: Comprehensive 3D SR Storage
// https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_A.35.13.html
tags.SOPClassUID = '1.2.840.10008.5.1.4.1.1.88.34';
tags.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.88.34';
tags.CompletionFlag = 'PARTIAL';
tags.VerificationFlag = 'UNVERIFIED';
// date
const now = new Date();
tags.ContentDate = getDicomDate(dateToDateObj(now));
tags.ContentTime = getDicomTime(dateToTimeObj(now));
// reference
const evidenceSq = tags.CurrentRequestedProcedureEvidenceSequence;
// hoping for just one element...
const evidenceSq0 = evidenceSq.value[0];
const refSeriesSq = evidenceSq0.ReferencedSeriesSequence;
// hoping for just one element...
const refSeriesSq0 = refSeriesSq.value[0];
let refSopSq = refSeriesSq0.ReferencedSOPSequence;
if (typeof refSopSq === 'undefined') {
refSeriesSq0.ReferencedSOPSequence = {
value: []
};
refSopSq = refSeriesSq0.ReferencedSOPSequence;
}
const refs = refSopSq.value;
// add reference if not yet present
for (const annotationGroup of report.annotationGroups) {
for (const annotation of annotationGroup.getList()) {
const ref = {
ReferencedSOPInstanceUID: annotation.referencedSopInstanceUID,
ReferencedSOPClassUID: annotation.referencedSopClassUID
};
const isSameRef = function (item) {
return item.ReferencedSOPInstanceUID ===
ref.ReferencedSOPInstanceUID &&
item.ReferencedSOPClassUID ===
ref.ReferencedSOPClassUID;
};
if (typeof refs.find(isSameRef) === 'undefined') {
refs.push(ref);
}
}
}
// TID 4100
tags.ContentTemplateSequence = {
value: [{
MappingResource: 'DCMR',
TemplateIdentifier: '4100'
}]
};
// findings summary
const srSummary = new DicomSRContent(ValueTypes.code);
srSummary.relationshipType = RelationshipTypes.contains;
srSummary.conceptNameCode =
getDcmDicomCode(DcmCodes.CADProcessingAndFindingsSummary);
// TODO: CID 6047 (All algorithms succeeded, ...)
srSummary.value =
getDcmDicomCode(DcmCodes.AllAlgorithmsSucceededWithFindings);
srSummary.contentSequence = [];
// response evaluation
for (const response of report.responseEvaluations) {
const srResponse =
this.#responseToTid4106ResponseEvaluation(response);
srSummary.contentSequence.push(srResponse);
}
// findings
for (const annotationGroup of report.annotationGroups) {
this.#addAnnotationGroupToTid4101Sequence(
annotationGroup, srSummary.contentSequence);
}
// comment (not part of TID 4100)
if (typeof report.comment !== 'undefined') {
const srComment = new DicomSRContent(ValueTypes.text);
srComment.relationshipType = RelationshipTypes.contains;
srComment.conceptNameCode = getDcmDicomCode(DcmCodes.Comment);
srComment.value = report.comment;
srSummary.contentSequence.push(srComment);
}
// main content
const srContent = new DicomSRContent(ValueTypes.container);
srContent.conceptNameCode = getDcmDicomCode(DcmCodes.ChestCADReport);
srContent.value = ContinuityOfContents.separate;
srContent.contentSequence.push(srSummary);
// main
if (typeof srContent !== 'undefined') {
tags = {
...tags,
...getDicomSRContentItem(srContent)
};
} else {
throw new Error('No annotation group SR content');
}
// merge extra tags if provided
if (typeof extraTags !== 'undefined') {
mergeTags(tags, extraTags);
}
return getElementsFromJSONTags(tags);
}
}