Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 18 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions extensions/cornerstone-dicom-seg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/adapters": "^1.68.1",
"@cornerstonejs/core": "^1.68.1",
"@kitware/vtk.js": "29.7.0",
"react-color": "^2.19.3"
}
Expand Down
6 changes: 3 additions & 3 deletions extensions/cornerstone-dicom-sr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/tools": "^1.67.0",
"@cornerstonejs/adapters": "^1.68.1",
"@cornerstonejs/core": "^1.68.1",
"@cornerstonejs/tools": "^1.68.1",
"classnames": "^2.3.2"
}
}
49 changes: 44 additions & 5 deletions extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion extensions/cornerstone-dicom-sr/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
14 changes: 8 additions & 6 deletions extensions/cornerstone-dicom-sr/src/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
addTool,
AngleTool,
annotation,
ArrowAnnotateTool,
Expand All @@ -9,6 +8,7 @@ import {
CircleROITool,
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
} from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import addToolInstance from './utils/addToolInstance';
Expand All @@ -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,
Expand All @@ -45,6 +46,7 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
SRCobbAngle: dashedLine,
SRAngle: dashedLine,
SRPlanarFreehandROI: dashedLine,
SRRectangleROI: dashedLine,
global: {},
});
}
14 changes: 11 additions & 3 deletions extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand All @@ -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 =
Expand All @@ -98,6 +103,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
color,
lineDash,
lineWidth,
...groupStyle,
};

Object.keys(renderableData).forEach(GraphicType => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -307,6 +314,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
{
color: options.color,
width: options.lineWidth,
lineDash: options.lineDash,
}
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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: {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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([
{
Expand Down
Loading
Loading