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

feat: Annotation and Measurements support on multi-frame DICOM #2973

Merged
merged 23 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
961bec5
added utilities to get frameNumber from imageId and add frameNumber t…
md-prog Oct 3, 2022
35418bd
Fix measurements' display texts on the Measurements Panel to have the…
md-prog Oct 3, 2022
03cce52
fixed minor React bugs (errors on console)
md-prog Oct 3, 2022
b87522d
fixed React's console bugs that are logged for some series that doesn…
md-prog Oct 4, 2022
ca7c7af
bug fix - multi-frame files was not loading frames correctly. It was …
md-prog Oct 4, 2022
6849109
metadata parser fix - providing default values of imagePlaneModule
md-prog Oct 4, 2022
4c2de3b
measurement SR support on multi-frame DICOM
md-prog Oct 4, 2022
0f5697a
bug fix - jumping to the selected measurment on multi-frame DICOM
md-prog Oct 4, 2022
c7b8bdd
upgrade dcmjs dependency to 2.8.1
md-prog Oct 4, 2022
7059d29
StudySummary component - allow "description" to be null
md-prog Oct 5, 2022
3ee3807
make getUIDsFromImageID() method public from MetadataProvider
md-prog Oct 5, 2022
75b4cfc
imageId usage fixes to be more stable
md-prog Oct 5, 2022
ea05ac7
change the variable name to be more meaningful
md-prog Oct 6, 2022
1469ba7
fix metaProvider importing
md-prog Oct 6, 2022
9d380ba
for(...of) instead of for(i=0;i<length;..)
md-prog Oct 6, 2022
52b4f8b
use Array.findIndex instead of plain for loop
md-prog Oct 6, 2022
c48cbff
use ReferencedSOPSequence[0] - because the ReferencedSOPSequence is …
md-prog Oct 6, 2022
a979c66
DisplaySetService.getDisplaySetForSOPInstanceUID() - added optional f…
md-prog Oct 6, 2022
471e9c6
simple code refactoring
md-prog Oct 7, 2022
fed74aa
refactoring for checking undefined values - mappedAnnotations
md-prog Oct 7, 2022
9348a17
remove unreachable code
md-prog Oct 7, 2022
a9d22ef
code refactoring - prefer conditional chaining
md-prog Oct 7, 2022
566ba8c
fix how we access imageIds of viewport (StackViewport)
md-prog Oct 7, 2022
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
2 changes: 1 addition & 1 deletion extensions/cornerstone-dicom-sr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"dcmjs": "^0.24.5",
"dcmjs": "^0.28.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
Expand Down
38 changes: 30 additions & 8 deletions extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes } from '@ohif/core';
import metadataProvider from '@ohif/core/src/classes/MetadataProvider';
md-prog marked this conversation as resolved.
Show resolved Hide resolved
import addMeasurement from './utils/addMeasurement';
import isRehydratable from './utils/isRehydratable';
import { adapters } from 'dcmjs';
Expand Down Expand Up @@ -212,20 +213,27 @@ function _checkIfCanAddMeasurementsToDisplaySet(
newDisplaySet
);

for (let i = 0; i < images.length; i++) {
for (let i = 0; i < imageIdsForDisplaySet.length; i++) {
md-prog marked this conversation as resolved.
Show resolved Hide resolved
if (!unloadedMeasurements.length) {
// All measurements loaded.
break;
return;
md-prog marked this conversation as resolved.
Show resolved Hide resolved
}

const image = images[i];
const { SOPInstanceUID } = image;
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
const imageId = imageIdsForDisplaySet[i];
md-prog marked this conversation as resolved.
Show resolved Hide resolved
const imageId = imageIdsForDisplaySet[i];
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(
imageId
);

if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j];
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
if (
_measurementReferencesSOPInstanceUID(
measurement,
SOPInstanceUID,
frameNumber
)
) {
addMeasurement(
measurement,
imageId,
Expand All @@ -239,9 +247,23 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
}

function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
function _measurementReferencesSOPInstanceUID(
measurement,
SOPInstanceUID,
frameNumber
) {
const { coords } = measurement;

// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
// Standard. But for now, we will support only one ReferenceFrameNumber.
const ReferencedFrameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence.ReferencedFrameNumber) ||
md-prog marked this conversation as resolved.
Show resolved Hide resolved
1;

if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber))
return false;

for (let j = 0; j < coords.length; j++) {
const coord = coords[j];
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
Expand Down
8 changes: 8 additions & 0 deletions extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export default function addMeasurement(

const annotationManager = annotation.state.getDefaultAnnotationManager();

// Create Cornerstone3D Annotation from measurement
const frameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence.ReferencedFrameNumber) ||
1;

const SRAnnotation: Types.Annotation = {
annotationUID: measurement.TrackingUniqueIdentifier,
metadata: {
Expand All @@ -61,6 +67,7 @@ export default function addMeasurement(
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
},
frameNumber: frameNumber,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not stick the frameNumber here, it is not a data per se. can't we just infer it from the referencedImageId? seeing if it has frames/... at the end?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to conform with the changes we've made to dcmjs - cornerstone3D adapter: dcmjs-org/dcmjs#317

},
};

Expand All @@ -75,6 +82,7 @@ export default function addMeasurement(
// 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;
}

Expand Down
2 changes: 1 addition & 1 deletion extensions/cornerstone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"cornerstone-wado-image-loader": "^4.2.1",
"dcmjs": "^0.24.5",
"dcmjs": "^0.28.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
Expand Down
34 changes: 27 additions & 7 deletions extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import CornerstoneCacheService from '../services/ViewportService/CornerstoneCach

import './OHIFCornerstoneViewport.css';
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
import {
IStackViewport,
IVolumeViewport,
} from '@cornerstonejs/core/dist/esm/types';
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';

const STACK = 'stack';

Expand Down Expand Up @@ -237,7 +242,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
displaySets,
viewportOptions.viewportType,
dataSource,
(viewportDataLoaded) => {
viewportDataLoaded => {
CornerstoneViewportService.setViewportDisplaySets(
viewportIndex,
viewportDataLoaded,
Expand Down Expand Up @@ -394,7 +399,7 @@ function _jumpToMeasurement(
viewportGridService
) {
const targetElement = targetElementRef.current;
const { displaySetInstanceUID, SOPInstanceUID } = measurement;
const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement;

if (!SOPInstanceUID) {
console.warn('cannot jump in a non-acquisition plane measurements yet');
Expand All @@ -404,17 +409,32 @@ function _jumpToMeasurement(
displaySetInstanceUID
);

const imageIdIndex = referencedDisplaySet.images.findIndex(
md-prog marked this conversation as resolved.
Show resolved Hide resolved
i => i.SOPInstanceUID === SOPInstanceUID
);

// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
// to set it properly
// setCornerstoneMeasurementActive(measurement);

viewportGridService.setActiveViewportIndex(viewportIndex);

if (getEnabledElement(targetElement)) {
const enableElement = getEnabledElement(targetElement);
if (enableElement) {
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
const viewport = enableElement.viewport as IStackViewport | IVolumeViewport;
const { imageIds } = viewport;
md-prog marked this conversation as resolved.
Show resolved Hide resolved
let imageIdIndex;

for (let i = 0; i < imageIds.length; i++) {
md-prog marked this conversation as resolved.
Show resolved Hide resolved
const {
SOPInstanceUID: SOPInstanceUID1,
frameNumber: frameNumber1,
} = getSOPInstanceAttributes(imageIds[i]);
if (
SOPInstanceUID1 == SOPInstanceUID &&
(!frameNumber || frameNumber == frameNumber1)
) {
imageIdIndex = i;
}
}

cs3DTools.utilities.jumpToSlice(targetElement, {
imageIndex: imageIdIndex,
});
Expand Down
21 changes: 20 additions & 1 deletion extensions/cornerstone/src/initMeasurementService.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core';

import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';

const { removeAnnotation } = annotation.state;

Expand Down Expand Up @@ -252,7 +253,24 @@ const connectMeasurementServiceToTools = (
SOPInstanceUID
);

const imageId = dataSource.getImageIdsForInstance({ instance });
md-prog marked this conversation as resolved.
Show resolved Hide resolved
let imageId;
let frameNumber = 1;

if (measurement.metadata && measurement.metadata.referencedImageId) {
md-prog marked this conversation as resolved.
Show resolved Hide resolved
imageId = measurement.metadata.referencedImageId;
frameNumber = getSOPInstanceAttributes(
measurement.metadata.referencedImageId
).frameNumber;
} else if (measurement.data && measurement.data.frameNumber) {
md-prog marked this conversation as resolved.
Show resolved Hide resolved
frameNumber = measurement.data.frameNumber;
imageId = dataSource.getImageIdsForInstance({
instance,
frame: frameNumber,
});
} else {
imageId = dataSource.getImageIdsForInstance({ instance });
}

const annotationManager = annotation.state.getDefaultAnnotationManager();
annotationManager.addAnnotation({
annotationUID: measurement.uid,
Expand All @@ -269,6 +287,7 @@ const connectMeasurementServiceToTools = (
handles: { ...data.annotation.data.handles },
cachedStats: { ...data.annotation.data.cachedStats },
label: data.annotation.data.label,
frameNumber: frameNumber,
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const Length = {
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations[0].frameNumber || 1,
md-prog marked this conversation as resolved.
Show resolved Hide resolved
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.text,
Expand All @@ -90,9 +91,11 @@ function getMappedAnnotations(annotation, DisplaySetService) {

const annotations = [];

const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);

const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
Expand All @@ -105,6 +108,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
text,
});

Expand All @@ -119,7 +123,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
const displayText = [];

// Area is the same for all series
const { SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];

const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
Expand All @@ -130,11 +134,10 @@ function getDisplayText(mappedAnnotations, displaySet) {
InstanceNumber = instance.InstanceNumber;
}

displayText.push(
InstanceNumber
? `(S: ${SeriesNumber} I: ${InstanceNumber})`
: `(S: ${SeriesNumber})`
);
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';

displayText.push(`(S: ${SeriesNumber}${instanceText}${frameText})`);
md-prog marked this conversation as resolved.
Show resolved Hide resolved

return displayText;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const Bidirectional = {
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations[0].frameNumber || 1,
md-prog marked this conversation as resolved.
Show resolved Hide resolved
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
Expand All @@ -92,30 +93,32 @@ function getMappedAnnotations(annotation, DisplaySetService) {
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];

let displaySet;

if (referencedImageId) {
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);

displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
if (!referencedImageId) {
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}

const { SeriesNumber, SeriesInstanceUID } = displaySet;
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);

const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);

const { SeriesNumber } = displaySet;
const { length, width } = targetStats;
const unit = 'mm';

annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
unit,
length,
width,
Expand Down Expand Up @@ -171,7 +174,13 @@ function getDisplayText(mappedAnnotations, displaySet) {
const displayText = [];

// Area is the same for all series
const { length, width, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
const {
length,
width,
SeriesNumber,
SOPInstanceUID,
frameNumber,
} = mappedAnnotations[0];
const roundedLength = utils.roundNumber(length, 2);
const roundedWidth = utils.roundNumber(width, 2);

Expand All @@ -184,10 +193,11 @@ function getDisplayText(mappedAnnotations, displaySet) {
InstanceNumber = instance.InstanceNumber;
}

const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';

displayText.push(
InstanceNumber
? `L: ${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
: `L: ${roundedLength} mm (S: ${SeriesNumber})`
`L: ${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
);
displayText.push(`W: ${roundedWidth} mm`);

Expand Down
Loading