fix(cornerstone-dicom-sr): Freehand SR hydration support (#3996)

Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
Co-authored-by: IbrahimCSAE <ibrahim.mdev@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-04-05 05:42:43 -04:00
1 parent 6e5892d202
commit 5645ac1b27
21 files changed
+366 -237

No files matched your search

@@ -1,6 +1,6 @@
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import addMeasurement from './utils/addMeasurement';
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
import isRehydratable from './utils/isRehydratable';
import { adaptersSR } from '@cornerstonejs/adapters';
@@ -150,13 +150,37 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
return [displaySet];
}
function _load(displaySet, servicesManager, extensionManager) {
async function _load(displaySet, servicesManager, extensionManager) {
const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const { ContentSequence } = displaySet.instance;
async function retrieveBulkData(obj, parentObj = null, key = null) {
for (const prop in obj) {
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
await retrieveBulkData(obj[prop], obj, prop);
} else if (Array.isArray(obj[prop])) {
await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop)));
} else if (prop === 'BulkDataURI') {
const value = await dataSource.retrieve.bulkDataURI({
BulkDataURI: obj[prop],
StudyInstanceUID: displaySet.instance.StudyInstanceUID,
SeriesInstanceUID: displaySet.instance.SeriesInstanceUID,
SOPInstanceUID: displaySet.instance.SOPInstanceUID,
});
if (parentObj && key) {
parentObj[key] = new Float32Array(value);
}
}
}
}
if (displaySet.isLoaded !== true) {
await retrieveBulkData(ContentSequence);
}
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
displaySet.measurements = _getMeasurements(ContentSequence);
@@ -211,7 +235,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
return;
}
if (!newDisplaySet instanceof ImageSet) {
if ((!newDisplaySet) instanceof ImageSet) {
// This also filters out _this_ displaySet, as it is not an ImageSet.
return;
}
@@ -275,7 +299,22 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
const frame =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
/** Add DICOMSRDisplay annotation for the SR viewport (only) */
addDICOMSRDisplayAnnotation(measurement, imageId, frame);
/** Update measurement properties */
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
measurement.frameNumber = frame;
delete measurement.coords;
unloadedMeasurements.splice(j, 1);
}
@@ -291,7 +330,7 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
// Standard. But for now, we will support only one ReferenceFrameNumber.
const ReferencedFrameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
@@ -1,6 +1,6 @@
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule';
import { srProtocol } from './getHangingProtocolModule';
import onModeEnter from './onModeEnter';
import getCommandsModule from './commandsModule';
import preRegistration from './init';
+8 -6
View File
@@ -1,5 +1,4 @@
import {
addTool,
AngleTool,
annotation,
ArrowAnnotateTool,
@@ -9,6 +8,7 @@ import {
CircleROITool,
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
} from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import addToolInstance from './utils/addToolInstance';
@@ -19,24 +19,25 @@ import toolNames from './tools/toolNames';
* @param {object} configuration
*/
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
addTool(DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool, {});
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool);
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
addToolInstance(toolNames.SRCircleROI, CircleROITool);
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
addToolInstance(toolNames.SRAngle, AngleTool);
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
// TODO - fix the rehydration of Freehand, as it throws an exception
// on a missing polyline. The fix is probably in CS3D
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
[toolNames.DICOMSRDisplay]: dashedLine,
SRLength: dashedLine,
SRBidirectional: dashedLine,
SREllipticalROI: dashedLine,
@@ -45,6 +46,7 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
SRCobbAngle: dashedLine,
SRAngle: dashedLine,
SRPlanarFreehandROI: dashedLine,
SRRectangleROI: dashedLine,
global: {},
});
}
@@ -22,14 +22,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
}
_getTextBoxLinesFromLabels(labels) {
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 3);
const labelLength = Math.min(labels.length, 5);
const lines = [];
for (let i = 0; i < labelLength; i++) {
const labelEntry = labels[i];
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`);
}
return lines;
@@ -77,6 +77,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
toolName: this.getToolName(),
viewportId: enabledElement.viewport.id,
};
const { style: annotationStyle } = annotation.config;
for (let i = 0; i < filteredAnnotations.length; i++) {
const annotation = filteredAnnotations[i];
@@ -87,6 +88,10 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
styleSpecifier.annotationUID = annotationUID;
const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[
this.getToolName()
];
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
const color =
@@ -98,6 +103,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
color,
lineDash,
lineWidth,
...groupStyle,
};
Object.keys(renderableData).forEach(GraphicType => {
@@ -160,6 +166,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const drawingOptions = {
color: options.color,
width: options.lineWidth,
lineDash: options.lineDash,
};
let allCanvasCoordinates = [];
renderableData.map((data, index) => {
@@ -307,6 +314,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
{
color: options.color,
width: options.lineWidth,
lineDash: options.lineDash,
}
);
});
@@ -1,15 +1,13 @@
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 .
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
const toolName = toolNames.DICOMSRDisplay;
const measurementData = {
@@ -27,26 +25,25 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
}
measurementData.renderableData[GraphicType].push(
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
_getRenderableData(GraphicType, GraphicData, imageId)
);
});
// 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;
/**
* This annotation (DICOMSRDisplay) is only used by the SR viewport.
* This is used before the annotation is hydrated. If hydrated the measurement will be added
* to the measurement service and will be available for the other viewports.
*/
const SRAnnotation: Types.Annotation = {
annotationUID: measurement.TrackingUniqueIdentifier,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
toolName: toolName,
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
@@ -58,28 +55,14 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
},
frameNumber: frameNumber,
frameNumber,
},
};
const annotationManager = annotation.state.getAnnotationManager();
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(':');
function _getRenderableData(GraphicType, GraphicData, imageId) {
let renderableData: csTypes.Point3[];
switch (GraphicType) {
@@ -125,6 +125,10 @@ function OHIFCornerstoneSRViewport(props) {
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
// return;
// }
_getViewportReferencedDisplaySetData(
srDisplaySet,
newMeasurementSelected,
@@ -253,12 +257,16 @@ function OHIFCornerstoneSRViewport(props) {
* if it is hydrated we don't even use the SR viewport.
*/
useEffect(() => {
if (!srDisplaySet.isLoaded) {
srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
}, [srDisplaySet]);
const loadSR = async () => {
if (!srDisplaySet.isLoaded) {
await srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
updateViewport(measurementSelected);
};
loadSR();
}, [dataSource, srDisplaySet]);
/**
* Hook to update the tracking identifiers when the selected measurement changes or
@@ -275,19 +283,11 @@ function OHIFCornerstoneSRViewport(props) {
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
useEffect(() => {
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
/**
* Data fetching for the SR displaySet, which updates the measurements and
* also gets the referenced image displaySet that SR is based on.
*/
useEffect(() => {
updateViewport(measurementSelected);
}, [dataSource, srDisplaySet]);
useEffect(() => {
viewportActionCornersService.setComponents([
{