src/dicom/dicomSpatialCoordinate.js
import {Point2D} from '../math/point';
import {Line, areOrthogonal} from '../math/line';
import {Protractor} from '../math/protractor';
import {ROI} from '../math/roi';
import {Circle} from '../math/circle';
import {Ellipse} from '../math/ellipse';
import {Rectangle} from '../math/rectangle';
// doc imports
/* eslint-disable no-unused-vars */
import {DataElement} from './dataElement';
/* eslint-enable no-unused-vars */
/**
* Related DICOM tag keys.
*/
const TagKeys = {
PixelOriginInterpretation: '00480301',
GraphicData: '00700022',
GraphicType: '00700023',
FiducialUID: '0070031A'
};
/**
* DICOM graphic types.
*/
export const GraphicTypes = {
point: 'POINT',
multipoint: 'MULTIPOINT',
polyline: 'POLYLINE',
circle: 'CIRCLE',
ellipse: 'ELLIPSE'
};
/**
* DICOM spatial coordinate (SCOORD): item of a SR content sequence.
*
* Ref: {@link https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_C.18.6.html#table_C.18.6-1}.
*/
export class SpatialCoordinate {
/**
* @type {string[]}
*/
graphicData;
/**
* @type {string}
*/
graphicType;
/**
* @type {string}
*/
pixelOriginInterpretation;
/**
* @type {string}
*/
fiducialUID;
/**
* Get a string representation of this object.
*
* @returns {string} The object as string.
*/
toString() {
return this.graphicType +
' {' + this.graphicData + '}';
};
};
/**
* Get a scoord object from a dicom element.
*
* @param {Object<string, DataElement>} dataElements The dicom element.
* @returns {SpatialCoordinate} A scoord object.
*/
export function getSpatialCoordinate(dataElements) {
const scoord = new SpatialCoordinate();
if (typeof dataElements[TagKeys.GraphicData] !== 'undefined') {
scoord.graphicData = dataElements[TagKeys.GraphicData].value;
}
if (typeof dataElements[TagKeys.GraphicType] !== 'undefined') {
scoord.graphicType = dataElements[TagKeys.GraphicType].value[0];
}
if (typeof dataElements[TagKeys.PixelOriginInterpretation] !== 'undefined') {
scoord.pixelOriginInterpretation =
dataElements[TagKeys.PixelOriginInterpretation].value[0];
}
if (typeof dataElements[TagKeys.FiducialUID] !== 'undefined') {
scoord.fiducialUID = dataElements[TagKeys.FiducialUID].value[0];
}
return scoord;
};
/**
* Get a simple dicom element item from a scoord object.
*
* @param {SpatialCoordinate} scoord The scoord object.
* @returns {Object<string, any>} The item as a list of (key, value) pairs.
*/
export function getDicomSpatialCoordinateItem(scoord) {
// dicom item (tags are in group/element order)
const item = {};
if (typeof scoord.pixelOriginInterpretation !== 'undefined') {
item.PixelOriginInterpretation = scoord.pixelOriginInterpretation;
}
if (typeof scoord.graphicData !== 'undefined') {
item.GraphicData = scoord.graphicData;
}
if (typeof scoord.graphicType !== 'undefined') {
item.GraphicType = scoord.graphicType;
}
if (typeof scoord.fiducialUID !== 'undefined') {
item.FiducialUID = scoord.fiducialUID;
}
// return
return item;
}
/**
* Get a DICOM spatial coordinate (SCOORD) from a mathematical shape.
*
* @param {Point2D|Line|Protractor|ROI|Circle|Ellipse|Rectangle} shape
* The math shape.
* @returns {SpatialCoordinate} The DICOM scoord.
*/
export function getScoordFromShape(shape) {
const scoord = new SpatialCoordinate();
if (shape instanceof Point2D) {
scoord.graphicData = [
shape.getX().toString(),
shape.getY().toString(),
];
scoord.graphicType = GraphicTypes.point;
} else if (shape instanceof Line) {
scoord.graphicData = [
shape.getBegin().getX().toString(),
shape.getBegin().getY().toString(),
shape.getEnd().getX().toString(),
shape.getEnd().getY().toString(),
];
scoord.graphicType = GraphicTypes.polyline;
} else if (shape instanceof Protractor) {
scoord.graphicData = [];
for (let i = 0; i < 3; ++i) {
scoord.graphicData.push(shape.getPoint(i).getX().toString());
scoord.graphicData.push(shape.getPoint(i).getY().toString());
}
scoord.graphicType = GraphicTypes.polyline;
} else if (shape instanceof ROI) {
scoord.graphicData = [];
for (let i = 0; i < shape.getLength(); ++i) {
scoord.graphicData.push(shape.getPoint(i).getX().toString());
scoord.graphicData.push(shape.getPoint(i).getY().toString());
}
// repeat first point to close shape
const firstPoint = shape.getPoint(0);
scoord.graphicData.push(firstPoint.getX().toString());
scoord.graphicData.push(firstPoint.getY().toString());
scoord.graphicType = GraphicTypes.polyline;
} else if (shape instanceof Circle) {
const center = shape.getCenter();
const pointPerimeter = new Point2D(
center.getX() + shape.getRadius(), center.getY()
);
scoord.graphicData = [
center.getX().toString(),
center.getY().toString(),
pointPerimeter.getX().toString(),
pointPerimeter.getY().toString(),
];
scoord.graphicType = GraphicTypes.circle;
} else if (shape instanceof Ellipse) {
const center = shape.getCenter();
const radiusX = shape.getA();
const radiusY = shape.getB();
scoord.graphicData = [
(center.getX() - radiusX).toString(),
center.getY().toString(),
(center.getX() + radiusX).toString(),
center.getY().toString(),
center.getX().toString(),
(center.getY() - radiusY).toString(),
center.getX().toString(),
(center.getY() + radiusY).toString()
];
scoord.graphicType = GraphicTypes.ellipse;
} else if (shape instanceof Rectangle) {
const begin = shape.getBegin();
const end = shape.getEnd();
// begin as first and last point to close shape
scoord.graphicData = [
begin.getX().toString(),
begin.getY().toString(),
begin.getX().toString(),
end.getY().toString(),
end.getX().toString(),
end.getY().toString(),
end.getX().toString(),
begin.getY().toString(),
begin.getX().toString(),
begin.getY().toString()
];
scoord.graphicType = GraphicTypes.polyline;
}
return scoord;
};
/**
* Get a mathematical shape from a DICOM spatial coordinate (SCOORD).
*
* @param {SpatialCoordinate} scoord The DICOM scoord.
* @returns {Point2D|Line|Protractor|ROI|Circle|Ellipse|Rectangle}
* The math shape.
*/
export function getShapeFromScoord(scoord) {
// extract points
const dataLength = scoord.graphicData.length;
if (dataLength % 2 !== 0) {
throw new Error('Expecting even number of coordinates in scroord data');
}
const points = [];
for (let i = 0; i < dataLength; i += 2) {
points.push(new Point2D(
parseFloat(scoord.graphicData[i]),
parseFloat(scoord.graphicData[i + 1])
));
}
let isClosed = false;
const numberOfPoints = points.length;
if (numberOfPoints > 2) {
const firstPoint = points[0];
const lastPoint = points[numberOfPoints - 1];
isClosed = firstPoint.equals(lastPoint);
}
// create math shape
let shape;
if (scoord.graphicType === GraphicTypes.point) {
if (points.length !== 1) {
throw new Error('Expecting 1 point for point');
}
shape = points[0];
} else if (scoord.graphicType === GraphicTypes.circle) {
if (points.length !== 2) {
throw new Error('Expecting 2 points for circles');
}
const center = points[0];
const pointPerimeter = points[1];
const radius = pointPerimeter.getDistance(center);
shape = new Circle(center, radius);
} else if (scoord.graphicType === GraphicTypes.ellipse) {
if (points.length !== 4) {
throw new Error('Expecting 4 points for ellipses');
}
// TODO: make more generic
const radiusX = points[0].getDistance(points[1]) / 2;
const radiusY = points[2].getDistance(points[3]) / 2;
const center = new Point2D(
points[0].getX() + radiusX,
points[0].getY()
);
shape = new Ellipse(center, radiusX, radiusY);
} else if (scoord.graphicType === GraphicTypes.polyline) {
if (!isClosed) {
if (points.length === 2) {
shape = new Line(points[0], points[1]);
} else if (points.length === 3) {
shape = new Protractor([points[0], points[1], points[2]]);
}
} else {
if (points.length === 5) {
const line0 = new Line(points[0], points[1]);
const line1 = new Line(points[1], points[2]);
const line2 = new Line(points[2], points[3]);
const line3 = new Line(points[3], points[4]);
if (areOrthogonal(line0, line1) &&
areOrthogonal(line1, line2) &&
areOrthogonal(line2, line3)) {
shape = new Rectangle(points[0], points[2]);
} else {
// remove last=first point for closed shape
shape = new ROI(points.slice(0, -1));
}
} else {
// remove last=first point for closed shape
shape = new ROI(points.slice(0, -1));
}
}
}
return shape;
};