feat: Annotation and Measurements support on multi-frame DICOM (#2973)
* added utilities to get frameNumber from imageId and add frameNumber to per-frame instance copy * Fix measurements' display texts on the Measurements Panel to have the correct instance number and frame number * fixed minor React bugs (errors on console) * fixed React's console bugs that are logged for some series that doesn't have "description" * bug fix - multi-frame files was not loading frames correctly. It was loading Frame 1 twice, and not loading the last frame. Due to wrong frame number indexing (frame number begins with 1, not 0) * metadata parser fix - providing default values of imagePlaneModule * measurement SR support on multi-frame DICOM * bug fix - jumping to the selected measurment on multi-frame DICOM * upgrade dcmjs dependency to 2.8.1 * StudySummary component - allow "description" to be null * make getUIDsFromImageID() method public from MetadataProvider * imageId usage fixes to be more stable * change the variable name to be more meaningful * fix metaProvider importing * for(...of) instead of for(i=0;i<length;..) as that doesn't assume anything about the layout/design and just gets next until done. * use Array.findIndex instead of plain for loop * use ReferencedSOPSequence[0] - because the ReferencedSOPSequence is an array that happens to have attributes of child zero when of length 1, but you shouldn't count on that. * DisplaySetService.getDisplaySetForSOPInstanceUID() - added optional frameNumber parameter for future usage : now they are just ignored as we are not supporting multiframe splits * simple code refactoring * refactoring for checking undefined values - mappedAnnotations * remove unreachable code * code refactoring - prefer conditional chaining * fix how we access imageIds of viewport (StackViewport)
This commit is contained in:
1 parent
af09578544
commit
b93067b6a2
30 files changed
+276
-132
No files matched your search
@@ -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",
|
||||
|
||||
@@ -3,13 +3,23 @@ import ReactResizeDetector from 'react-resize-detector';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import * as cs3DTools from '@cornerstonejs/tools';
|
||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
||||
import {
|
||||
Enums,
|
||||
eventTarget,
|
||||
getEnabledElement,
|
||||
StackViewport,
|
||||
} from '@cornerstonejs/core';
|
||||
|
||||
import { setEnabledElement } from '../state';
|
||||
import CornerstoneCacheService from '../services/ViewportService/CornerstoneCacheService';
|
||||
|
||||
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';
|
||||
|
||||
@@ -237,7 +247,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
displaySets,
|
||||
viewportOptions.viewportType,
|
||||
dataSource,
|
||||
(viewportDataLoaded) => {
|
||||
viewportDataLoaded => {
|
||||
CornerstoneViewportService.setViewportDisplaySets(
|
||||
viewportIndex,
|
||||
viewportDataLoaded,
|
||||
@@ -394,7 +404,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');
|
||||
@@ -404,17 +414,37 @@ function _jumpToMeasurement(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
const imageIdIndex = referencedDisplaySet.images.findIndex(
|
||||
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;
|
||||
|
||||
let imageIdIndex = 0;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const imageIds = viewport.getImageIds();
|
||||
imageIdIndex = imageIds.findIndex(imageId => {
|
||||
const {
|
||||
SOPInstanceUID: aSOPInstanceUID,
|
||||
frameNumber: aFrameNumber,
|
||||
} = getSOPInstanceAttributes(imageId);
|
||||
return (
|
||||
aSOPInstanceUID === SOPInstanceUID &&
|
||||
(!frameNumber || frameNumber === aFrameNumber)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
imageIdIndex = referencedDisplaySet.images.findIndex(
|
||||
i => i.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
cs3DTools.utilities.jumpToSlice(targetElement, {
|
||||
imageIndex: imageIdIndex,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -252,7 +253,18 @@ const connectMeasurementServiceToTools = (
|
||||
SOPInstanceUID
|
||||
);
|
||||
|
||||
const imageId = dataSource.getImageIdsForInstance({ instance });
|
||||
let imageId;
|
||||
let frameNumber = 1;
|
||||
|
||||
if (measurement?.metadata?.referencedImageId) {
|
||||
imageId = measurement.metadata.referencedImageId;
|
||||
frameNumber = getSOPInstanceAttributes(
|
||||
measurement.metadata.referencedImageId
|
||||
).frameNumber;
|
||||
} else {
|
||||
imageId = dataSource.getImageIdsForInstance({ instance });
|
||||
}
|
||||
|
||||
const annotationManager = annotation.state.getDefaultAnnotationManager();
|
||||
annotationManager.addAnnotation({
|
||||
annotationUID: measurement.uid,
|
||||
@@ -269,6 +281,7 @@ const connectMeasurementServiceToTools = (
|
||||
handles: { ...data.annotation.data.handles },
|
||||
cachedStats: { ...data.annotation.data.cachedStats },
|
||||
label: data.annotation.data.label,
|
||||
frameNumber: frameNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ const Length = {
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.text,
|
||||
@@ -90,13 +91,16 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
||||
|
||||
const annotations = [];
|
||||
|
||||
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
||||
referencedImageId
|
||||
);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber,
|
||||
} = getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
SeriesInstanceUID,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
@@ -105,6 +109,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
text,
|
||||
});
|
||||
|
||||
@@ -119,7 +124,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
|
||||
@@ -130,11 +135,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})`);
|
||||
|
||||
return displayText;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ const Bidirectional = {
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
@@ -92,30 +93,33 @@ 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,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
const { length, width } = targetStats;
|
||||
const unit = 'mm';
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
unit,
|
||||
length,
|
||||
width,
|
||||
@@ -171,7 +175,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);
|
||||
|
||||
@@ -184,10 +194,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`);
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ const EllipticalROI = {
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
@@ -91,31 +92,34 @@ 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) {
|
||||
// Todo: Non-acquisition plane measurement mapping not supported yet
|
||||
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,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
const { mean, stdDev, max, area, Modality } = targetStats;
|
||||
const unit = getModalityUnit(Modality);
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
Modality,
|
||||
unit,
|
||||
mean,
|
||||
@@ -184,7 +188,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
||||
const displayText = [];
|
||||
|
||||
// Area is the same for all series
|
||||
const { area, SOPInstanceUID } = mappedAnnotations[0];
|
||||
const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(
|
||||
image => image.SOPInstanceUID === SOPInstanceUID
|
||||
@@ -195,6 +199,9 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
}
|
||||
|
||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||
|
||||
const roundedArea = utils.roundNumber(area, 2);
|
||||
displayText.push(`${roundedArea} mm<sup>2</sup>`);
|
||||
|
||||
@@ -202,14 +209,15 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
||||
mappedAnnotations.forEach(mappedAnnotation => {
|
||||
const { unit, max, SeriesNumber } = mappedAnnotation;
|
||||
|
||||
let maxStr = '';
|
||||
if (max) {
|
||||
const roundedMax = utils.roundNumber(max, 2);
|
||||
maxStr = `Max: ${roundedMax} <small>${unit}</small> `;
|
||||
}
|
||||
|
||||
displayText.push(
|
||||
InstanceNumber
|
||||
? `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber} I:${InstanceNumber})`
|
||||
: `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber})`
|
||||
);
|
||||
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
|
||||
if (!displayText.includes(str)) {
|
||||
displayText.push(str);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ const Length = {
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
@@ -97,30 +98,33 @@ 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,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
const { length } = targetStats;
|
||||
const unit = 'mm';
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
unit,
|
||||
length,
|
||||
});
|
||||
@@ -175,7 +179,12 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
||||
const displayText = [];
|
||||
|
||||
// Area is the same for all series
|
||||
const { length, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
|
||||
const {
|
||||
length,
|
||||
SeriesNumber,
|
||||
SOPInstanceUID,
|
||||
frameNumber,
|
||||
} = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(
|
||||
image => image.SOPInstanceUID === SOPInstanceUID
|
||||
@@ -186,11 +195,12 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
}
|
||||
|
||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||
|
||||
const roundedLength = utils.roundNumber(length, 2);
|
||||
displayText.push(
|
||||
InstanceNumber
|
||||
? `${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
|
||||
: `${roundedLength} mm (S: ${SeriesNumber})`
|
||||
`${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
|
||||
);
|
||||
|
||||
return displayText;
|
||||
|
||||
+1
@@ -45,6 +45,7 @@ function _getUIDFromImageID(imageId) {
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
frameNumber: instance.frameNumber || 1,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user