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:
md-prog 2022-10-13 11:07:56 -04:00 committed by GitHub
parent af09578544
commit b93067b6a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 276 additions and 132 deletions

View File

@ -34,7 +34,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^3.0.0", "@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"dcmjs": "^0.24.5", "dcmjs": "^0.28.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -6,7 +6,7 @@ import { adapters } from 'dcmjs';
const { CodeScheme: Cornerstone3DCodeScheme } = adapters.Cornerstone3D; const { CodeScheme: Cornerstone3DCodeScheme } = adapters.Cornerstone3D;
const { ImageSet } = classes; const { ImageSet, MetadataProvider: metadataProvider } = classes;
// TODO -> // TODO ->
// Add SR thumbnail // Add SR thumbnail
// Make viewport // Make viewport
@ -214,20 +214,26 @@ function _checkIfCanAddMeasurementsToDisplaySet(
newDisplaySet newDisplaySet
); );
for (let i = 0; i < images.length; i++) { for (const imageId of imageIdsForDisplaySet) {
if (!unloadedMeasurements.length) { if (!unloadedMeasurements.length) {
// All measurements loaded. // All measurements loaded.
break; return;
} }
const image = images[i]; const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(
const { SOPInstanceUID } = image; imageId
if (SOPInstanceUIDs.includes(SOPInstanceUID)) { );
const imageId = imageIdsForDisplaySet[i];
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j]; const measurement = unloadedMeasurements[j];
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID)) { if (
_measurementReferencesSOPInstanceUID(
measurement,
SOPInstanceUID,
frameNumber
)
) {
addMeasurement( addMeasurement(
measurement, measurement,
imageId, imageId,
@ -241,9 +247,23 @@ function _checkIfCanAddMeasurementsToDisplaySet(
} }
} }
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID) { function _measurementReferencesSOPInstanceUID(
measurement,
SOPInstanceUID,
frameNumber
) {
const { coords } = measurement; 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[0]?.ReferencedFrameNumber) ||
1;
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber))
return false;
for (let j = 0; j < coords.length; j++) { for (let j = 0; j < coords.length; j++) {
const coord = coords[j]; const coord = coords[j];
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence; const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;

View File

@ -45,6 +45,12 @@ export default function addMeasurement(
const annotationManager = annotation.state.getDefaultAnnotationManager(); const annotationManager = annotation.state.getDefaultAnnotationManager();
// Create Cornerstone3D Annotation from measurement
const frameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
1;
const SRAnnotation: Types.Annotation = { const SRAnnotation: Types.Annotation = {
annotationUID: measurement.TrackingUniqueIdentifier, annotationUID: measurement.TrackingUniqueIdentifier,
metadata: { metadata: {
@ -61,6 +67,7 @@ export default function addMeasurement(
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier, TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData, renderableData: measurementData.renderableData,
}, },
frameNumber: frameNumber,
}, },
}; };
@ -75,6 +82,7 @@ export default function addMeasurement(
// It'd be super weird if it didn't anyway as a SCOORD. // It'd be super weird if it didn't anyway as a SCOORD.
measurement.ReferencedSOPInstanceUID = measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID; measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
measurement.frameNumber = frameNumber;
delete measurement.coords; delete measurement.coords;
} }

View File

@ -30,7 +30,7 @@
"@ohif/core": "^3.0.0", "@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"cornerstone-wado-image-loader": "^4.2.1", "cornerstone-wado-image-loader": "^4.2.1",
"dcmjs": "^0.24.5", "dcmjs": "^0.28.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -3,13 +3,23 @@ import ReactResizeDetector from 'react-resize-detector';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useViewportGrid } from '@ohif/ui'; import { useViewportGrid } from '@ohif/ui';
import * as cs3DTools from '@cornerstonejs/tools'; 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 { setEnabledElement } from '../state';
import CornerstoneCacheService from '../services/ViewportService/CornerstoneCacheService'; import CornerstoneCacheService from '../services/ViewportService/CornerstoneCacheService';
import './OHIFCornerstoneViewport.css'; import './OHIFCornerstoneViewport.css';
import CornerstoneOverlays from './Overlays/CornerstoneOverlays'; import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
import {
IStackViewport,
IVolumeViewport,
} from '@cornerstonejs/core/dist/esm/types';
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
const STACK = 'stack'; const STACK = 'stack';
@ -237,7 +247,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
displaySets, displaySets,
viewportOptions.viewportType, viewportOptions.viewportType,
dataSource, dataSource,
(viewportDataLoaded) => { viewportDataLoaded => {
CornerstoneViewportService.setViewportDisplaySets( CornerstoneViewportService.setViewportDisplaySets(
viewportIndex, viewportIndex,
viewportDataLoaded, viewportDataLoaded,
@ -394,7 +404,7 @@ function _jumpToMeasurement(
viewportGridService viewportGridService
) { ) {
const targetElement = targetElementRef.current; const targetElement = targetElementRef.current;
const { displaySetInstanceUID, SOPInstanceUID } = measurement; const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement;
if (!SOPInstanceUID) { if (!SOPInstanceUID) {
console.warn('cannot jump in a non-acquisition plane measurements yet'); console.warn('cannot jump in a non-acquisition plane measurements yet');
@ -404,17 +414,37 @@ function _jumpToMeasurement(
displaySetInstanceUID displaySetInstanceUID
); );
const imageIdIndex = referencedDisplaySet.images.findIndex(
i => i.SOPInstanceUID === SOPInstanceUID
);
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager // Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
// to set it properly // to set it properly
// setCornerstoneMeasurementActive(measurement); // setCornerstoneMeasurementActive(measurement);
viewportGridService.setActiveViewportIndex(viewportIndex); 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, { cs3DTools.utilities.jumpToSlice(targetElement, {
imageIndex: imageIdIndex, imageIndex: imageIdIndex,
}); });

View File

@ -3,6 +3,7 @@ import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core'; import { DicomMetadataStore } from '@ohif/core';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
const { removeAnnotation } = annotation.state; const { removeAnnotation } = annotation.state;
@ -252,7 +253,18 @@ const connectMeasurementServiceToTools = (
SOPInstanceUID 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(); const annotationManager = annotation.state.getDefaultAnnotationManager();
annotationManager.addAnnotation({ annotationManager.addAnnotation({
annotationUID: measurement.uid, annotationUID: measurement.uid,
@ -269,6 +281,7 @@ const connectMeasurementServiceToTools = (
handles: { ...data.annotation.data.handles }, handles: { ...data.annotation.data.handles },
cachedStats: { ...data.annotation.data.cachedStats }, cachedStats: { ...data.annotation.data.cachedStats },
label: data.annotation.data.label, label: data.annotation.data.label,
frameNumber: frameNumber,
}, },
}); });
} }

View File

@ -69,6 +69,7 @@ const Length = {
metadata, metadata,
referenceSeriesUID: SeriesInstanceUID, referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID, referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName, toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID, displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.text, label: data.text,
@ -90,13 +91,16 @@ function getMappedAnnotations(annotation, DisplaySetService) {
const annotations = []; const annotations = [];
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( const {
referencedImageId SOPInstanceUID,
); SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID SeriesInstanceUID,
frameNumber
); );
const { SeriesNumber } = displaySet; const { SeriesNumber } = displaySet;
@ -105,6 +109,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
SeriesInstanceUID, SeriesInstanceUID,
SOPInstanceUID, SOPInstanceUID,
SeriesNumber, SeriesNumber,
frameNumber,
text, text,
}); });
@ -119,7 +124,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
const displayText = []; const displayText = [];
// Area is the same for all series // Area is the same for all series
const { SeriesNumber, SOPInstanceUID } = mappedAnnotations[0]; const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const instance = displaySet.images.find( const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID image => image.SOPInstanceUID === SOPInstanceUID
@ -130,11 +135,10 @@ function getDisplayText(mappedAnnotations, displaySet) {
InstanceNumber = instance.InstanceNumber; InstanceNumber = instance.InstanceNumber;
} }
displayText.push( const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
InstanceNumber const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
? `(S: ${SeriesNumber} I: ${InstanceNumber})`
: `(S: ${SeriesNumber})` displayText.push(`(S: ${SeriesNumber}${instanceText}${frameText})`);
);
return displayText; return displayText;
} }

View File

@ -67,6 +67,7 @@ const Bidirectional = {
metadata, metadata,
referenceSeriesUID: SeriesInstanceUID, referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID, referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName, toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID, displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label, label: data.label,
@ -92,30 +93,33 @@ function getMappedAnnotations(annotation, DisplaySetService) {
Object.keys(cachedStats).forEach(targetId => { Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId]; const targetStats = cachedStats[targetId];
let displaySet; if (!referencedImageId) {
if (referencedImageId) {
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
throw new Error( throw new Error(
'Non-acquisition plane measurement mapping not supported' '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 { length, width } = targetStats;
const unit = 'mm'; const unit = 'mm';
annotations.push({ annotations.push({
SeriesInstanceUID, SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber, SeriesNumber,
frameNumber,
unit, unit,
length, length,
width, width,
@ -171,7 +175,13 @@ function getDisplayText(mappedAnnotations, displaySet) {
const displayText = []; const displayText = [];
// Area is the same for all series // 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 roundedLength = utils.roundNumber(length, 2);
const roundedWidth = utils.roundNumber(width, 2); const roundedWidth = utils.roundNumber(width, 2);
@ -184,10 +194,11 @@ function getDisplayText(mappedAnnotations, displaySet) {
InstanceNumber = instance.InstanceNumber; InstanceNumber = instance.InstanceNumber;
} }
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
displayText.push( displayText.push(
InstanceNumber `L: ${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
? `L: ${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
: `L: ${roundedLength} mm (S: ${SeriesNumber})`
); );
displayText.push(`W: ${roundedWidth} mm`); displayText.push(`W: ${roundedWidth} mm`);

View File

@ -66,6 +66,7 @@ const EllipticalROI = {
metadata, metadata,
referenceSeriesUID: SeriesInstanceUID, referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID, referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName, toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID, displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label, label: data.label,
@ -91,31 +92,34 @@ function getMappedAnnotations(annotation, DisplaySetService) {
Object.keys(cachedStats).forEach(targetId => { Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId]; const targetStats = cachedStats[targetId];
let displaySet; if (!referencedImageId) {
if (referencedImageId) {
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
// Todo: Non-acquisition plane measurement mapping not supported yet // Todo: Non-acquisition plane measurement mapping not supported yet
throw new Error( throw new Error(
'Non-acquisition plane measurement mapping not supported' '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 { mean, stdDev, max, area, Modality } = targetStats;
const unit = getModalityUnit(Modality); const unit = getModalityUnit(Modality);
annotations.push({ annotations.push({
SeriesInstanceUID, SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber, SeriesNumber,
frameNumber,
Modality, Modality,
unit, unit,
mean, mean,
@ -184,7 +188,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
const displayText = []; const displayText = [];
// Area is the same for all series // Area is the same for all series
const { area, SOPInstanceUID } = mappedAnnotations[0]; const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const instance = displaySet.images.find( const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID image => image.SOPInstanceUID === SOPInstanceUID
@ -195,6 +199,9 @@ function getDisplayText(mappedAnnotations, displaySet) {
InstanceNumber = instance.InstanceNumber; InstanceNumber = instance.InstanceNumber;
} }
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
const roundedArea = utils.roundNumber(area, 2); const roundedArea = utils.roundNumber(area, 2);
displayText.push(`${roundedArea} mm<sup>2</sup>`); displayText.push(`${roundedArea} mm<sup>2</sup>`);
@ -202,14 +209,15 @@ function getDisplayText(mappedAnnotations, displaySet) {
mappedAnnotations.forEach(mappedAnnotation => { mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation; const { unit, max, SeriesNumber } = mappedAnnotation;
let maxStr = '';
if (max) { if (max) {
const roundedMax = utils.roundNumber(max, 2); const roundedMax = utils.roundNumber(max, 2);
maxStr = `Max: ${roundedMax} <small>${unit}</small> `;
}
displayText.push( const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
InstanceNumber if (!displayText.includes(str)) {
? `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber} I:${InstanceNumber})` displayText.push(str);
: `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber})`
);
} }
}); });

View File

@ -72,6 +72,7 @@ const Length = {
metadata, metadata,
referenceSeriesUID: SeriesInstanceUID, referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID, referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName, toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID, displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label, label: data.label,
@ -97,30 +98,33 @@ function getMappedAnnotations(annotation, DisplaySetService) {
Object.keys(cachedStats).forEach(targetId => { Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId]; const targetStats = cachedStats[targetId];
let displaySet; if (!referencedImageId) {
if (referencedImageId) {
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
throw new Error( throw new Error(
'Non-acquisition plane measurement mapping not supported' '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 { length } = targetStats;
const unit = 'mm'; const unit = 'mm';
annotations.push({ annotations.push({
SeriesInstanceUID, SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber, SeriesNumber,
frameNumber,
unit, unit,
length, length,
}); });
@ -175,7 +179,12 @@ function getDisplayText(mappedAnnotations, displaySet) {
const displayText = []; const displayText = [];
// Area is the same for all series // 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( const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID image => image.SOPInstanceUID === SOPInstanceUID
@ -186,11 +195,12 @@ function getDisplayText(mappedAnnotations, displaySet) {
InstanceNumber = instance.InstanceNumber; InstanceNumber = instance.InstanceNumber;
} }
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
const roundedLength = utils.roundNumber(length, 2); const roundedLength = utils.roundNumber(length, 2);
displayText.push( displayText.push(
InstanceNumber `${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
? `${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
: `${roundedLength} mm (S: ${SeriesNumber})`
); );
return displayText; return displayText;

View File

@ -45,6 +45,7 @@ function _getUIDFromImageID(imageId) {
SOPInstanceUID: instance.SOPInstanceUID, SOPInstanceUID: instance.SOPInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID, SeriesInstanceUID: instance.SeriesInstanceUID,
StudyInstanceUID: instance.StudyInstanceUID, StudyInstanceUID: instance.StudyInstanceUID,
frameNumber: instance.frameNumber || 1,
}; };
} }

View File

@ -32,7 +32,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^3.0.0", "@ohif/core": "^3.0.0",
"@ohif/i18n": "^1.0.0", "@ohif/i18n": "^1.0.0",
"dcmjs": "^0.24.5", "dcmjs": "^0.28.1",
"dicomweb-client": "^0.6.0", "dicomweb-client": "^0.6.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^17.0.2", "react": "^17.0.2",

View File

@ -494,10 +494,10 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
const NumberOfFrames = instance.NumberOfFrames; const NumberOfFrames = instance.NumberOfFrames;
if (NumberOfFrames > 1) { if (NumberOfFrames > 1) {
for (let i = 0; i < NumberOfFrames; i++) { for (let frame = 1; frame <= NumberOfFrames; frame++) {
const imageId = this.getImageIdsForInstance({ const imageId = this.getImageIdsForInstance({
instance, instance,
frame: i, frame,
}); });
imageIds.push(imageId); imageIds.push(imageId);
} }

View File

@ -30,7 +30,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^3.0.0", "@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"dcmjs": "^0.24.5", "dcmjs": "^0.28.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -30,7 +30,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^3.0.0", "@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"dcmjs": "^0.24.5", "dcmjs": "^0.28.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -35,7 +35,7 @@
"@cornerstonejs/core": "^0.16.1", "@cornerstonejs/core": "^0.16.1",
"@cornerstonejs/tools": "^0.24.1", "@cornerstonejs/tools": "^0.24.1",
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0", "@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"dcmjs": "^0.24.5", "dcmjs": "^0.28.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",

View File

@ -44,13 +44,17 @@ export default function _hydrateStructuredReport(
); );
const sopInstanceUIDToImageId = {}; const sopInstanceUIDToImageId = {};
let imageIdsForToolState = []; const imageIdsForToolState = {};
displaySet.measurements.forEach(measurement => { displaySet.measurements.forEach(measurement => {
const { ReferencedSOPInstanceUID, imageId } = measurement; const { ReferencedSOPInstanceUID, imageId, frameNumber } = measurement;
imageIdsForToolState.push(imageId);
if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) { if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId; sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
imageIdsForToolState[ReferencedSOPInstanceUID] = [];
}
if (!imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber]) {
imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber] = imageId;
} }
}); });
@ -88,7 +92,14 @@ export default function _hydrateStructuredReport(
toolDataForAnnotationType.forEach(toolData => { toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState // Add the measurement to toolState
const imageId = sopInstanceUIDToImageId[toolData.sopInstanceUid]; // dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber =
(toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
if (!imageIds.includes(imageId)) { if (!imageIds.includes(imageId)) {
imageIds.push(imageId); imageIds.push(imageId);
@ -125,7 +136,14 @@ export default function _hydrateStructuredReport(
toolDataForAnnotationType.forEach(toolData => { toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState // Add the measurement to toolState
const imageId = sopInstanceUIDToImageId[toolData.sopInstanceUid]; // dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber =
(toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
toolData.uid = guid(); toolData.uid = guid();

View File

@ -17,9 +17,9 @@ const { formatDate } = utils;
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
key: undefined, // key: undefined, //
date: undefined, // '07-Sep-2010', date: '', // '07-Sep-2010',
modality: undefined, // 'CT', modality: '', // 'CT',
description: undefined, // 'CHEST/ABD/PELVIS W CONTRAST', description: '', // 'CHEST/ABD/PELVIS W CONTRAST',
}; };
function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {

View File

@ -30,7 +30,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^3.0.0", "@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"dcmjs": "0.22.0", "dcmjs": "0.28.0",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -13,5 +13,6 @@ function _getUIDFromImageID(imageId) {
SOPInstanceUID: instance.SOPInstanceUID, SOPInstanceUID: instance.SOPInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID, SeriesInstanceUID: instance.SeriesInstanceUID,
StudyInstanceUID: instance.StudyInstanceUID, StudyInstanceUID: instance.StudyInstanceUID,
frameNumber: instance.frameNumber || 1,
}; };
} }

View File

@ -37,7 +37,7 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.16.3", "@babel/runtime": "7.16.3",
"dcmjs": "^0.24.5", "dcmjs": "^0.28.1",
"dicomweb-client": "^0.6.0", "dicomweb-client": "^0.6.0",
"isomorphic-base64": "^1.0.2", "isomorphic-base64": "^1.0.2",
"lodash.merge": "^4.6.1", "lodash.merge": "^4.6.1",

View File

@ -52,7 +52,7 @@ class MetadataProvider {
} }
_getInstance(imageId) { _getInstance(imageId) {
const uids = this._getUIDsFromImageID(imageId); const uids = this.getUIDsFromImageID(imageId);
if (!uids) { if (!uids) {
return; return;
@ -191,14 +191,16 @@ class MetadataProvider {
rows: toNumber(instance.Rows), rows: toNumber(instance.Rows),
columns: toNumber(instance.Columns), columns: toNumber(instance.Columns),
imageOrientationPatient: toNumber(ImageOrientationPatient), imageOrientationPatient: toNumber(ImageOrientationPatient),
rowCosines: toNumber(rowCosines), rowCosines: toNumber(rowCosines || [0, 1, 0]),
columnCosines: toNumber(columnCosines), columnCosines: toNumber(columnCosines || [0, 0, -1]),
imagePositionPatient: toNumber(instance.ImagePositionPatient), imagePositionPatient: toNumber(
instance.ImagePositionPatient || [0, 0, 0]
),
sliceThickness: toNumber(instance.SliceThickness), sliceThickness: toNumber(instance.SliceThickness),
sliceLocation: toNumber(instance.SliceLocation), sliceLocation: toNumber(instance.SliceLocation),
pixelSpacing: toNumber(PixelSpacing), pixelSpacing: toNumber(PixelSpacing || 1),
rowPixelSpacing: toNumber(rowPixelSpacing), rowPixelSpacing: toNumber(rowPixelSpacing || 1),
columnPixelSpacing: toNumber(columnPixelSpacing), columnPixelSpacing: toNumber(columnPixelSpacing || 1),
}; };
break; break;
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE: case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
@ -409,7 +411,7 @@ class MetadataProvider {
return metadata; return metadata;
} }
_getUIDsFromImageID(imageId) { getUIDsFromImageID(imageId) {
// TODO: adding csiv here is not really correct. Probably need to use // TODO: adding csiv here is not really correct. Probably need to use
// metadataProvider.addImageIdToUIDs(imageId, { // metadataProvider.addImageIdToUIDs(imageId, {
// StudyInstanceUID, // StudyInstanceUID,

View File

@ -75,7 +75,11 @@ export default class DisplaySetService {
); );
}; };
getDisplaySetForSOPInstanceUID(SOPInstanceUID, SeriesInstanceUID) { getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID,
frameNumber
) {
const displaySets = SeriesInstanceUID const displaySets = SeriesInstanceUID
? this.getDisplaySetsForSeries(SeriesInstanceUID) ? this.getDisplaySetsForSeries(SeriesInstanceUID)
: this.getDisplaySetCache(); : this.getDisplaySetCache();

View File

@ -38,6 +38,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
'FrameOfReferenceUID', 'FrameOfReferenceUID',
'referenceStudyUID', 'referenceStudyUID',
'referenceSeriesUID', 'referenceSeriesUID',
'frameNumber',
'displaySetInstanceUID', 'displaySetInstanceUID',
'label', 'label',
'description', 'description',

View File

@ -12,22 +12,33 @@ const combineFrameInstance = (frame, instance) => {
const { const {
PerFrameFunctionalGroupsSequence, PerFrameFunctionalGroupsSequence,
SharedFunctionalGroupsSequence, SharedFunctionalGroupsSequence,
NumberOfFrames,
} = instance; } = instance;
if (!PerFrameFunctionalGroupsSequence) return instance;
const shared = Object.values(SharedFunctionalGroupsSequence[0]) if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) {
.map(it => it[0]) const frameNumber = Number.parseInt(frame || 1);
.filter(it => it !== undefined && typeof it === 'object'); const shared = (SharedFunctionalGroupsSequence
const perFrame = Object.values( ? Object.values(SharedFunctionalGroupsSequence[0])
PerFrameFunctionalGroupsSequence[(frame || 1) - 1] : []
) )
.map(it => it[0]) .map(it => it[0])
.filter(it => it !== undefined && typeof it === 'object'); .filter(it => it !== undefined && typeof it === 'object');
const perFrame = (PerFrameFunctionalGroupsSequence
? Object.values(PerFrameFunctionalGroupsSequence[frameNumber - 1])
: []
)
.map(it => it[0])
.filter(it => it !== undefined && typeof it === 'object');
return Object.assign( return Object.assign(
{}, { frameNumber: frameNumber },
instance, instance,
...Object.values(shared), ...Object.values(shared),
...Object.values(perFrame) ...Object.values(perFrame)
); );
} else {
return instance;
}
}; };
export default combineFrameInstance; export default combineFrameInstance;

View File

@ -235,6 +235,7 @@ Button.propTypes = {
]), ]),
border: PropTypes.oneOf([ border: PropTypes.oneOf([
'none', 'none',
'light',
'default', 'default',
'primary', 'primary',
'secondary', 'secondary',

View File

@ -52,9 +52,9 @@ const MeasurementItem = ({
</div> </div>
<div className="relative flex flex-col flex-1 px-2 py-1"> <div className="relative flex flex-col flex-1 px-2 py-1">
<span className="mb-1 text-base text-primary-light">{label}</span> <span className="mb-1 text-base text-primary-light">{label}</span>
{displayText.map(line => ( {displayText.map((line, i) => (
<span <span
key={line} key={i}
className="pl-2 text-base text-white border-l border-primary-light" className="pl-2 text-base text-white border-l border-primary-light"
dangerouslySetInnerHTML={{ __html: line }} dangerouslySetInnerHTML={{ __html: line }}
></span> ></span>

View File

@ -11,7 +11,7 @@ const StudySummary = ({ date, modality, description }) => {
</span> </span>
</div> </div>
<div className="pt-2 text-base leading-none truncate text-primary-light ellipse"> <div className="pt-2 text-base leading-none truncate text-primary-light ellipse">
{description} {description || ''}
</div> </div>
</div> </div>
); );
@ -20,7 +20,7 @@ const StudySummary = ({ date, modality, description }) => {
StudySummary.propTypes = { StudySummary.propTypes = {
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
modality: PropTypes.string.isRequired, modality: PropTypes.string.isRequired,
description: PropTypes.string.isRequired, description: PropTypes.string,
}; };
export default StudySummary; export default StudySummary;

View File

@ -63,7 +63,7 @@
"core-js": "^3.16.1", "core-js": "^3.16.1",
"cornerstone-math": "^0.1.9", "cornerstone-math": "^0.1.9",
"cornerstone-wado-image-loader": "^4.2.1", "cornerstone-wado-image-loader": "^4.2.1",
"dcmjs": "^0.24.5", "dcmjs": "^0.28.1",
"detect-gpu": "^4.0.16", "detect-gpu": "^4.0.16",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"dotenv-webpack": "^1.7.0", "dotenv-webpack": "^1.7.0",

View File

@ -10033,16 +10033,17 @@ dayjs@^1.10.4:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5"
integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw== integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==
dcmjs@^0.24.5: dcmjs@^0.28.1:
version "0.24.6" version "0.28.1"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.24.6.tgz#436d00361fb8d4286e68e2b0cab15939fa3acafb" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.28.1.tgz#e9b20dddef41fbdbf87c96d0e31531dd6ac93087"
integrity sha512-ts/DigszrYXMOmYLRVlik4Z6Oq0fb4ykrveFkcIPclTk/uoND0uwbhO4IPMNnuhGGwaVFF6uXQISY+kgbpjn2g== integrity sha512-PzlxfdZazOkv9OCFYtp9jWzMt2GnfSvBOQ2SCqnFlDbzZWAiT6kvrfVHx5ux65Lct0P0IGLGoqRdQChtsjzL5A==
dependencies: dependencies:
"@babel/runtime-corejs2" "^7.17.8" "@babel/runtime-corejs2" "^7.17.8"
gl-matrix "^3.1.0" gl-matrix "^3.1.0"
lodash.clonedeep "^4.5.0" lodash.clonedeep "^4.5.0"
loglevelnext "^3.0.1" loglevelnext "^3.0.1"
ndarray "^1.0.19" ndarray "^1.0.19"
pako "^2.0.4"
debug-log@^1.0.0: debug-log@^1.0.0:
version "1.0.1" version "1.0.1"