Fix/sr hydration non tracking (#3080)

* fix:DICOM SR hydration - remove redundancies

feat:Allow save and restore on non-tracking view

* PR comments
This commit is contained in:
Bill Wallace authored and GitHub committed 2022-12-29 10:13:06 -05:00
1 parent ec7cdba901
commit 9a03bde0ae
21 files changed
+466 -73

No files matched your search

@@ -37,6 +37,12 @@ const _generateReport = (
// Add in top level series options
Object.assign(dataset, options);
// Set the default character set as UTF-8
// https://dicom.innolitics.com/ciods/nm-image/sop-common/00080005
if (typeof dataset.SpecificCharacterSet === 'undefined') {
dataset.SpecificCharacterSet = 'ISO_IR 192';
}
return dataset;
};
@@ -5,6 +5,7 @@ import commandsModule from './commandsModule';
import init from './init';
import { id } from './id.js';
import toolNames from './tools/toolNames';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
const Component = React.lazy(() => {
return import(
@@ -74,3 +75,4 @@ const dicomSRExtension = {
};
export default dicomSRExtension;
export { hydrateStructuredReport };
@@ -0,0 +1,23 @@
/**
* Extracts the label from the toolData imported from dcmjs. We need to do this
* as dcmjs does not depeend on OHIF/the MeasurementService, it just produces data for cornestoneTools.
* This optional data is available for the consumer to process if they wish to.
* @param {object} toolData The tooldata relating to the
*
* @returns {string} The extracted label.
*/
export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData;
let freeTextLabel = findingSites.find(
fs => fs.CodeValue === 'CORNERSTONEFREETEXT'
);
if (freeTextLabel) {
return freeTextLabel.CodeMeaning;
}
if (finding && finding.CodeValue === 'CORNERSTONEFREETEXT') {
return finding.CodeMeaning;
}
}
@@ -0,0 +1,262 @@
import { utilities, metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adapters } from 'dcmjs';
const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adapters.Cornerstone3D;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
/**
* Hydrates a structured report, for default viewports.
*
*/
export default function hydrateStructuredReport(
{ servicesManager, extensionManager },
displaySetInstanceUID
) {
const dataSource = extensionManager.getActiveDataSource()[0];
const { MeasurementService, DisplaySetService } = servicesManager.services;
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
// TODO -> We should define a strict versioning somewhere.
const mappings = MeasurementService.getSourceMappings(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
if (!mappings || !mappings.length) {
throw new Error(
`Attempting to hydrate measurements service when no mappings present. This shouldn't be reached.`
);
}
const instance = DicomMetadataStore.getInstance(
displaySet.StudyInstanceUID,
displaySet.SeriesInstanceUID,
displaySet.SOPInstanceUID
);
const sopInstanceUIDToImageId = {};
const imageIdsForToolState = {};
displaySet.measurements.forEach(measurement => {
const { ReferencedSOPInstanceUID, imageId, frameNumber } = measurement;
if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
imageIdsForToolState[ReferencedSOPInstanceUID] = [];
}
if (!imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber]) {
imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber] = imageId;
}
});
const datasetToUse = _mapLegacyDataSet(instance);
// Use dcmjs to generate toolState.
const storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse,
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
// for the imageToWorld transformation. The following assumes that the order
// that measurements were added to the display set are the same order as
// the measurementGroups in the instance.
sopInstanceUIDToImageId,
utilities.imageToWorldCoords,
metaData
);
// Filter what is found by DICOM SR to measurements we support.
const mappingDefinitions = mappings.map(m => m.annotationType);
const hydratableMeasurementsInSR = {};
Object.keys(storedMeasurementByAnnotationType).forEach(key => {
if (mappingDefinitions.includes(key)) {
hydratableMeasurementsInSR[key] = storedMeasurementByAnnotationType[key];
}
});
// Set the series touched as tracked.
const imageIds = [];
// TODO: notification if no hydratable?
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType =
hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// 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)) {
imageIds.push(imageId);
}
});
});
let targetStudyInstanceUID;
const SeriesInstanceUIDs = [];
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get(
'instance',
imageId
);
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
SeriesInstanceUIDs.push(SeriesInstanceUID);
}
if (!targetStudyInstanceUID) {
targetStudyInstanceUID = StudyInstanceUID;
} else if (targetStudyInstanceUID !== StudyInstanceUID) {
console.warn(
'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'
);
}
}
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType =
hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// 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();
const instance = metaData.get('instance', imageId);
const {
FrameOfReferenceUID,
// SOPInstanceUID,
// SeriesInstanceUID,
// StudyInstanceUID,
} = instance;
const annotation = {
annotationUID: toolData.annotation.annotationUID,
data: toolData.annotation.data,
metadata: {
toolName: annotationType,
referencedImageId: imageId,
FrameOfReferenceUID,
},
};
const source = MeasurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
const matchingMapping = mappings.find(
m => m.annotationType === annotationType
);
MeasurementService.addRawMeasurement(
source,
annotationType,
{ annotation },
matchingMapping.toMeasurementSchema,
dataSource
);
if (!imageIds.includes(imageId)) {
imageIds.push(imageId);
}
});
});
displaySet.isHydrated = true;
return {
StudyInstanceUID: targetStudyInstanceUID,
SeriesInstanceUIDs,
};
}
function _mapLegacyDataSet(dataset) {
const REPORT = 'Imaging Measurements';
const GROUP = 'Measurement Group';
const TRACKING_IDENTIFIER = 'Tracking Identifier';
// Identify the Imaging Measurements
const imagingMeasurementContent = toArray(dataset.ContentSequence).find(
codeMeaningEquals(REPORT)
);
// Retrieve the Measurements themselves
const measurementGroups = toArray(
imagingMeasurementContent.ContentSequence
).filter(codeMeaningEquals(GROUP));
// For each of the supported measurement types, compute the measurement data
const measurementData = {};
const cornerstoneToolClasses =
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const registeredToolClasses = [];
Object.keys(cornerstoneToolClasses).forEach(key => {
registeredToolClasses.push(cornerstoneToolClasses[key]);
measurementData[key] = [];
});
measurementGroups.forEach((measurementGroup, index) => {
const measurementGroupContentSequence = toArray(
measurementGroup.ContentSequence
);
const TrackingIdentifierGroup = measurementGroupContentSequence.find(
contentItem =>
contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
);
const TrackingIdentifier = TrackingIdentifierGroup.TextValue;
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
cornerstoneTag = CORNERSTONE_3D_TAG;
}
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
TrackingIdentifierGroup.TextValue = mappedTrackingIdentifier;
});
return dataset;
}
const toArray = function (x) {
return Array.isArray(x) ? x : [x];
};
const codeMeaningEquals = codeMeaningName => {
return contentItem => {
return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName;
};
};
@@ -13,6 +13,7 @@ import {
Icon,
} from '@ohif/ui';
import classNames from 'classnames';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
const { formatDate } = utils;
@@ -37,6 +38,7 @@ function OHIFCornerstoneSRViewport(props) {
const {
DisplaySetService,
CornerstoneViewportService,
MeasurementService,
} = servicesManager.services;
// SR viewport will always have a single display set
@@ -69,18 +71,25 @@ function OHIFCornerstoneSRViewport(props) {
MEASUREMENT_TRACKING_EXTENSION_ID
);
// TODO: this is a hook that fails if we register/de-register
if (hasMeasurementTrackingExtension) {
const contextModule = extensionManager.getModuleEntry(
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
);
const useTrackedMeasurements = () => useContext(contextModule.context);
[
trackedMeasurements,
sendTrackedMeasurementsEvent,
] = useTrackedMeasurements();
const tracked = useContext(contextModule.context);
trackedMeasurements = tracked?.[0];
sendTrackedMeasurementsEvent = tracked?.[1];
}
if (!sendTrackedMeasurementsEvent) {
// if no panels from measurement-tracking extension is used, this code will trun
trackedMeasurements = null;
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
MeasurementService.clearMeasurements();
hydrateStructuredReport(
{ servicesManager, extensionManager },
displaySetInstanceUID
);
};
}
/**
@@ -362,7 +371,7 @@ function OHIFCornerstoneSRViewport(props) {
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: SeriesDescription,
seriesDescription: SeriesDescription || '',
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)