223 lines
7.5 KiB
TypeScript
223 lines
7.5 KiB
TypeScript
import { vec3 } from 'gl-matrix';
|
|
import { Types, annotation } from '@cornerstonejs/tools';
|
|
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
|
|
import toolNames from '../tools/toolNames';
|
|
import SCOORD_TYPES from '../constants/scoordTypes';
|
|
|
|
const EPSILON = 1e-4;
|
|
|
|
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
|
|
|
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
|
|
// TODO -> Render rotated ellipse .
|
|
const toolName = toolNames.DICOMSRDisplay;
|
|
|
|
const measurementData = {
|
|
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
|
|
renderableData: {},
|
|
labels: measurement.labels,
|
|
imageId,
|
|
};
|
|
|
|
measurement.coords.forEach(coord => {
|
|
const { GraphicType, GraphicData } = coord;
|
|
|
|
if (measurementData.renderableData[GraphicType] === undefined) {
|
|
measurementData.renderableData[GraphicType] = [];
|
|
}
|
|
|
|
measurementData.renderableData[GraphicType].push(
|
|
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
|
|
);
|
|
});
|
|
|
|
// Use the metadata provider to grab its imagePlaneModule metadata
|
|
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
|
|
|
const annotationManager = annotation.state.getAnnotationManager();
|
|
|
|
// Create Cornerstone3D Annotation from measurement
|
|
const frameNumber =
|
|
(measurement.coords[0].ReferencedSOPSequence &&
|
|
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
|
1;
|
|
|
|
const SRAnnotation: Types.Annotation = {
|
|
annotationUID: measurement.TrackingUniqueIdentifier,
|
|
metadata: {
|
|
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
|
toolName: toolName,
|
|
referencedImageId: imageId,
|
|
},
|
|
data: {
|
|
label: measurement.labels,
|
|
handles: {
|
|
textBox: {},
|
|
},
|
|
cachedStats: {
|
|
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
|
renderableData: measurementData.renderableData,
|
|
},
|
|
frameNumber: frameNumber,
|
|
},
|
|
};
|
|
|
|
annotationManager.addAnnotation(SRAnnotation);
|
|
|
|
measurement.loaded = true;
|
|
measurement.imageId = imageId;
|
|
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
|
|
|
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
|
|
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
|
|
// It'd be super weird if it didn't anyway as a SCOORD.
|
|
measurement.ReferencedSOPInstanceUID =
|
|
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
|
measurement.frameNumber = frameNumber;
|
|
delete measurement.coords;
|
|
}
|
|
|
|
function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) {
|
|
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
|
|
|
let renderableData: csTypes.Point3[];
|
|
|
|
switch (GraphicType) {
|
|
case SCOORD_TYPES.POINT:
|
|
case SCOORD_TYPES.MULTIPOINT:
|
|
case SCOORD_TYPES.POLYLINE:
|
|
renderableData = [];
|
|
|
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
|
const worldPos = utilities.imageToWorldCoords(imageId, [
|
|
GraphicData[i],
|
|
GraphicData[i + 1],
|
|
]);
|
|
|
|
renderableData.push(worldPos);
|
|
}
|
|
|
|
break;
|
|
case SCOORD_TYPES.CIRCLE: {
|
|
const pointsWorld = [];
|
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
|
const worldPos = utilities.imageToWorldCoords(imageId, [
|
|
GraphicData[i],
|
|
GraphicData[i + 1],
|
|
]);
|
|
|
|
pointsWorld.push(worldPos);
|
|
}
|
|
|
|
// We do not have an explicit draw circle svg helper in Cornerstone3D at
|
|
// this time, but we can use the ellipse svg helper to draw a circle, so
|
|
// here we reshape the data for that purpose.
|
|
const center = pointsWorld[0];
|
|
const onPerimeter = pointsWorld[1];
|
|
|
|
const radius = vec3.distance(center, onPerimeter);
|
|
|
|
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
|
|
|
if (!imagePlaneModule) {
|
|
throw new Error('No imagePlaneModule found');
|
|
}
|
|
|
|
const {
|
|
columnCosines,
|
|
rowCosines,
|
|
}: {
|
|
columnCosines: csTypes.Point3;
|
|
rowCosines: csTypes.Point3;
|
|
} = imagePlaneModule;
|
|
|
|
// we need to get major/minor axis (which are both the same size major = minor)
|
|
|
|
// first axisStart
|
|
const firstAxisStart = vec3.create();
|
|
vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius);
|
|
|
|
const firstAxisEnd = vec3.create();
|
|
vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius);
|
|
|
|
// second axisStart
|
|
const secondAxisStart = vec3.create();
|
|
vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius);
|
|
|
|
const secondAxisEnd = vec3.create();
|
|
vec3.scaleAndAdd(secondAxisEnd, center, rowCosines, -radius);
|
|
|
|
renderableData = [
|
|
firstAxisStart as csTypes.Point3,
|
|
firstAxisEnd as csTypes.Point3,
|
|
secondAxisStart as csTypes.Point3,
|
|
secondAxisEnd as csTypes.Point3,
|
|
];
|
|
|
|
break;
|
|
}
|
|
case SCOORD_TYPES.ELLIPSE: {
|
|
// GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY]
|
|
// But Cornerstone3D points are ordered as top, bottom, left, right for the
|
|
// ellipse so we need to identify if the majorAxis is horizontal or vertical
|
|
// and then choose the correct points to use for the ellipse.
|
|
|
|
const pointsWorld: csTypes.Point3[] = [];
|
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
|
const worldPos = utilities.imageToWorldCoords(imageId, [
|
|
GraphicData[i],
|
|
GraphicData[i + 1],
|
|
]);
|
|
|
|
pointsWorld.push(worldPos);
|
|
}
|
|
|
|
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
|
|
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
|
|
const minorAxisStart = vec3.fromValues(...pointsWorld[2]);
|
|
const minorAxisEnd = vec3.fromValues(...pointsWorld[3]);
|
|
|
|
const majorAxisVec = vec3.create();
|
|
vec3.sub(majorAxisVec, majorAxisEnd, majorAxisStart);
|
|
|
|
// normalize majorAxisVec to avoid scaling issues
|
|
vec3.normalize(majorAxisVec, majorAxisVec);
|
|
|
|
const minorAxisVec = vec3.create();
|
|
vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart);
|
|
vec3.normalize(minorAxisVec, minorAxisVec);
|
|
|
|
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
|
|
|
if (!imagePlaneModule) {
|
|
throw new Error('imageId does not have imagePlaneModule metadata');
|
|
}
|
|
|
|
const { columnCosines }: { columnCosines: csTypes.Point3 } = imagePlaneModule;
|
|
|
|
// find which axis is parallel to the columnCosines
|
|
const columnCosinesVec = vec3.fromValues(...columnCosines);
|
|
|
|
const projectedMajorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, majorAxisVec));
|
|
const projectedMinorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, minorAxisVec));
|
|
|
|
const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec);
|
|
const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec);
|
|
|
|
renderableData = [];
|
|
if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) {
|
|
renderableData = [pointsWorld[0], pointsWorld[1], pointsWorld[2], pointsWorld[3]];
|
|
} else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) {
|
|
renderableData = [pointsWorld[2], pointsWorld[3], pointsWorld[0], pointsWorld[1]];
|
|
} else {
|
|
console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED');
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
console.warn('Unsupported GraphicType:', GraphicType);
|
|
}
|
|
|
|
return renderableData;
|
|
}
|