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:
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)
|
||||
|
||||
Reference in new issue
Block a user