src/image/annotationGroupFactory.js
import {
dateToDateObj,
getDicomDate,
dateToTimeObj,
getDicomTime,
} from '../dicom/dicomDate';
import {
ValueTypes,
RelationshipTypes,
getSRContent,
getDicomSRContentItem,
DicomSRContent,
getSRContentFromValue
} from '../dicom/dicomSRContent';
import {
isEqualCode,
getPathCode,
getMeasurementGroupCode,
getImageRegionCode,
getReferenceGeometryCode,
getSourceImageCode,
getTrackingIdentifierCode,
getShortLabelCode,
getReferencePointsCode,
getColourCode,
getQuantificationName,
getQuantificationUnit
} from '../dicom/dicomCode';
import {getElementsFromJSONTags} from '../dicom/dicomWriter';
import {ImageReference} from '../dicom/dicomImageReference';
import {SopInstanceReference} from '../dicom/dicomSopInstanceReference';
import {
GraphicTypes,
getScoordFromShape,
getShapeFromScoord,
SpatialCoordinate
} from '../dicom/dicomSpatialCoordinate';
import {SpatialCoordinate3D} from '../dicom/dicomSpatialCoordinate3D';
import {guid} from '../math/stats';
import {logger} from '../utils/logger';
import {Annotation} from './annotation';
import {AnnotationGroup} from './annotationGroup';
import {Line} from '../math/line';
import {Point2D, Point3D} from '../math/point';
// doc imports
/* eslint-disable no-unused-vars */
import {DataElement} from '../dicom/dataElement';
/* eslint-enable no-unused-vars */
/**
* 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.trace('Overwritting tag: ' + tagName2);
}
tags1[tagName2] = tags2[tagName2];
}
}
/**
* {@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 dicom elements. Throws an error if not suitable.
*
* @param {Object<string, DataElement>} dataElements The DICOM data elements.
* @returns {string|undefined} A possible warning.
*/
checkElements(dataElements) {
// reset
this.#warning = undefined;
const srContent = getSRContent(dataElements);
if (typeof srContent.conceptNameCode !== 'undefined') {
if (srContent.conceptNameCode.value !== getMeasurementGroupCode().value) {
this.#warning = 'Not a measurement group';
}
} else {
this.#warning = 'No root concept name code';
}
return this.#warning;
}
/**
* Convert a DICOM SR content of type SCOORD into an annotation.
*
* @param {DicomSRContent} item The input SCOORD.
* @returns {Annotation} The annotation.
*/
#scoordToAnnotation(item) {
const annotation = new Annotation();
annotation.mathShape = getShapeFromScoord(item.value);
// default
annotation.id = guid();
annotation.textExpr = '';
for (const subItem of item.contentSequence) {
// reference image UID
if (subItem.valueType === ValueTypes.image &&
subItem.relationshipType === RelationshipTypes.selectedFrom &&
isEqualCode(subItem.conceptNameCode, getSourceImageCode())) {
annotation.referenceSopUID =
subItem.value.referencedSOPSequence.referencedSOPInstanceUID;
}
// annotation id
if (subItem.valueType === ValueTypes.uidref &&
subItem.relationshipType === RelationshipTypes.hasProperties &&
isEqualCode(subItem.conceptNameCode, getTrackingIdentifierCode())) {
annotation.id = subItem.value;
}
// text expr
if (subItem.valueType === ValueTypes.text &&
subItem.relationshipType === RelationshipTypes.hasProperties &&
isEqualCode(subItem.conceptNameCode, getShortLabelCode())) {
annotation.textExpr = subItem.value;
if (typeof subItem.contentSequence !== 'undefined') {
for (const subsubItem of subItem.contentSequence) {
if (subsubItem.valueType === ValueTypes.scoord &&
subsubItem.relationshipType === RelationshipTypes.hasProperties &&
isEqualCode(
subsubItem.conceptNameCode, getReferencePointsCode())) {
annotation.labelPosition = new Point2D(
subsubItem.value.graphicData[0],
subsubItem.value.graphicData[1]
);
}
}
}
}
// color
if (subItem.valueType === ValueTypes.text &&
subItem.relationshipType === RelationshipTypes.hasProperties &&
isEqualCode(subItem.conceptNameCode, getColourCode())) {
annotation.colour = subItem.value;
}
// reference points
if (subItem.valueType === ValueTypes.scoord &&
subItem.relationshipType === RelationshipTypes.hasProperties &&
isEqualCode(subItem.conceptNameCode, getReferencePointsCode()) &&
subItem.value.graphicType === GraphicTypes.multipoint) {
const points = [];
for (let i = 0; i < subItem.value.graphicData.length; i += 2) {
points.push(new Point2D(
subItem.value.graphicData[i],
subItem.value.graphicData[i + 1]
));
}
annotation.referencePoints = points;
}
// plane points
if (subItem.valueType === ValueTypes.scoord3d &&
subItem.relationshipType === RelationshipTypes.hasProperties &&
isEqualCode(
subItem.conceptNameCode, getReferenceGeometryCode()) &&
subItem.value.graphicType === GraphicTypes.multipoint) {
const data = subItem.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
if (subItem.valueType === ValueTypes.num &&
subItem.relationshipType === RelationshipTypes.contains) {
const quantifName =
getQuantificationName(subItem.conceptNameCode);
if (typeof quantifName === 'undefined') {
continue;
}
const measuredValue = subItem.value.measuredValue;
const quantifUnit = getQuantificationUnit(
measuredValue.measurementUnitsCode);
if (typeof annotation.quantification === 'undefined') {
annotation.quantification = {};
}
annotation.quantification[quantifName] = {
value: measuredValue.numericValue,
unit: quantifUnit
};
}
}
return annotation;
}
/**
* Get an {@link Annotation} object from the read DICOM file.
*
* @param {Object<string, DataElement>} dataElements The DICOM tags.
* @returns {AnnotationGroup} A new annotation group.
*/
create(dataElements) {
const annotations = [];
const srContent = getSRContent(dataElements);
for (const item of srContent.contentSequence) {
if (item.valueType === ValueTypes.scoord) {
annotations.push(this.#scoordToAnnotation(item));
}
}
const annotationGroup = new AnnotationGroup(annotations);
const safeGet = function (key) {
let res;
const element = dataElements[key];
if (typeof element !== 'undefined') {
res = element.value[0];
}
return res;
};
// StudyInstanceUID
annotationGroup.setMetaValue('StudyInstanceUID', safeGet('0020000D'));
// Modality
annotationGroup.setMetaValue('Modality', safeGet('00080060'));
// patient info
annotationGroup.setMetaValue('PatientName', safeGet('00100010'));
annotationGroup.setMetaValue('PatientID', safeGet('00100020'));
annotationGroup.setMetaValue('PatientBirthDate', safeGet('00100030'));
annotationGroup.setMetaValue('PatientSex', safeGet('00100040'));
// ReferencedSeriesSequence
const element = dataElements['00081115'];
if (typeof element !== 'undefined') {
const seriesElement = element.value[0]['0020000E'];
if (typeof seriesElement !== 'undefined') {
annotationGroup.setMetaValue(
'ReferencedSeriesSequence', {
value: [{
SeriesInstanceUID: seriesElement.value[0]
}]
}
);
}
}
return annotationGroup;
}
/**
* Convert an annotation into a DICOM SCOORD.
*
* @param {Annotation} annotation The input annotation.
* @returns {DicomSRContent} An SR content of type SCOORD.
*/
#annotationToScoord(annotation) {
const srScoord = new DicomSRContent(ValueTypes.scoord);
srScoord.relationshipType = RelationshipTypes.contains;
if (annotation.mathShape instanceof Line) {
srScoord.conceptNameCode = getPathCode();
} else {
srScoord.conceptNameCode = getImageRegionCode();
}
srScoord.value = getScoordFromShape(annotation.mathShape);
const itemContentSequence = [];
// reference image UID
const srImage = new DicomSRContent(ValueTypes.image);
srImage.relationshipType = RelationshipTypes.selectedFrom;
srImage.conceptNameCode = getSourceImageCode();
const sopRef = new SopInstanceReference();
sopRef.referencedSOPClassUID = '';
sopRef.referencedSOPInstanceUID = annotation.referenceSopUID;
const imageRef = new ImageReference();
imageRef.referencedSOPSequence = sopRef;
srImage.value = imageRef;
itemContentSequence.push(srImage);
// annotation id
const srUid = new DicomSRContent(ValueTypes.uidref);
srUid.relationshipType = RelationshipTypes.hasProperties;
srUid.conceptNameCode = getTrackingIdentifierCode();
srUid.value = annotation.id;
itemContentSequence.push(srUid);
// text expr
const shortLabel = new DicomSRContent(ValueTypes.text);
shortLabel.relationshipType = RelationshipTypes.hasProperties;
shortLabel.conceptNameCode = getShortLabelCode();
shortLabel.value = annotation.textExpr;
// label position
if (typeof annotation.labelPosition !== 'undefined') {
const labelPosition = new DicomSRContent(ValueTypes.scoord);
labelPosition.relationshipType = RelationshipTypes.hasProperties;
labelPosition.conceptNameCode = getReferencePointsCode();
const labelPosScoord = new SpatialCoordinate();
labelPosScoord.graphicType = GraphicTypes.point;
const graphicData = [
annotation.labelPosition.getX().toString(),
annotation.labelPosition.getY().toString()
];
labelPosScoord.graphicData = graphicData;
labelPosition.value = labelPosScoord;
// add position to label sequence
shortLabel.contentSequence = [labelPosition];
}
itemContentSequence.push(shortLabel);
// colour
const colour = new DicomSRContent(ValueTypes.text);
colour.relationshipType = RelationshipTypes.hasProperties;
colour.conceptNameCode = getColourCode();
colour.value = annotation.colour;
itemContentSequence.push(colour);
// reference points
if (typeof annotation.referencePoints !== 'undefined') {
const referencePoints = new DicomSRContent(ValueTypes.scoord);
referencePoints.relationshipType = RelationshipTypes.hasProperties;
referencePoints.conceptNameCode = getReferencePointsCode();
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;
itemContentSequence.push(referencePoints);
}
// plane points
if (typeof annotation.planePoints !== 'undefined') {
const planePoints = new DicomSRContent(ValueTypes.scoord3d);
planePoints.relationshipType = RelationshipTypes.hasProperties;
planePoints.conceptNameCode = getReferenceGeometryCode();
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;
itemContentSequence.push(planePoints);
}
// quantification
if (typeof annotation.quantification !== 'undefined') {
for (const key in annotation.quantification) {
const quatifContent = getSRContentFromValue(
key,
annotation.quantification[key].value,
annotation.quantification[key].unit
);
if (typeof quatifContent !== 'undefined') {
itemContentSequence.push(quatifContent);
}
}
}
srScoord.contentSequence = itemContentSequence;
return srScoord;
}
/**
* Convert an annotation group into a DICOM SR object.
*
* @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: Basic Text SR Storage
tags.SOPClassUID = '1.2.840.10008.5.1.4.1.1.88.11';
tags.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.88.11';
tags.CompletionFlag = 'PARTIAL';
tags.VerificationFlag = 'UNVERIFIED';
const now = new Date();
tags.ContentDate = getDicomDate(dateToDateObj(now));
tags.ContentTime = getDicomTime(dateToTimeObj(now));
const contentSequence = [];
for (const annotation of annotationGroup.getList()) {
contentSequence.push(this.#annotationToScoord(annotation));
}
// main
if (contentSequence.length !== 0) {
const srContent = new DicomSRContent(ValueTypes.container);
srContent.conceptNameCode = getMeasurementGroupCode();
srContent.contentSequence = contentSequence;
tags = {
...tags,
...getDicomSRContentItem(srContent)
};
}
// merge extra tags if provided
if (typeof extraTags !== 'undefined') {
mergeTags(tags, extraTags);
}
return getElementsFromJSONTags(tags);
}
}