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:
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';
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
+15
-32
@@ -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([
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user