2022-07-27 18:39:04 +02:00
|
|
|
import { utilities, metaData } from '@cornerstonejs/core';
|
2020-08-04 11:11:23 +02:00
|
|
|
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
2022-12-29 16:13:06 +01:00
|
|
|
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
|
2023-02-06 23:05:13 +01:00
|
|
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
2023-08-16 16:26:28 +02:00
|
|
|
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
|
|
|
|
|
const { locking } = CsAnnotation;
|
2020-08-04 11:11:23 +02:00
|
|
|
|
|
|
|
|
const { guid } = OHIF.utils;
|
2023-02-06 23:05:13 +01:00
|
|
|
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
|
2022-07-27 18:39:04 +02:00
|
|
|
|
|
|
|
|
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
|
|
|
|
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
|
|
|
|
|
|
|
|
|
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2023-03-24 21:45:21 +01:00
|
|
|
const convertCode = (codingValues, code) => {
|
2023-08-09 16:07:33 +02:00
|
|
|
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-03-24 21:45:21 +01:00
|
|
|
const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`;
|
|
|
|
|
const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning };
|
|
|
|
|
return ret;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const convertSites = (codingValues, sites) => {
|
2023-08-09 16:07:33 +02:00
|
|
|
if (!sites || !sites.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-03-24 21:45:21 +01:00
|
|
|
const ret = [];
|
|
|
|
|
// Do as a loop to convert away from Proxy instances
|
|
|
|
|
for (let i = 0; i < sites.length; i++) {
|
|
|
|
|
// Deal with irregular conversion from dcmjs
|
|
|
|
|
const site = convertCode(codingValues, sites[i][0] || sites[i]);
|
2023-08-09 16:07:33 +02:00
|
|
|
if (site) {
|
|
|
|
|
ret.push(site);
|
|
|
|
|
}
|
2023-03-24 21:45:21 +01:00
|
|
|
}
|
|
|
|
|
return (ret.length && ret) || undefined;
|
|
|
|
|
};
|
|
|
|
|
|
2020-08-04 11:11:23 +02:00
|
|
|
/**
|
2022-12-29 16:13:06 +01:00
|
|
|
* Hydrates a structured report, for default viewports.
|
2020-08-04 11:11:23 +02:00
|
|
|
*
|
|
|
|
|
*/
|
2022-12-29 16:13:06 +01:00
|
|
|
export default function hydrateStructuredReport(
|
2023-08-16 16:26:28 +02:00
|
|
|
{ servicesManager, extensionManager, appConfig },
|
2020-08-04 11:11:23 +02:00
|
|
|
displaySetInstanceUID
|
|
|
|
|
) {
|
2023-08-16 16:26:28 +02:00
|
|
|
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
|
|
|
|
const disableEditing = appConfig?.disableEditing;
|
2021-05-28 20:32:48 +02:00
|
|
|
const dataSource = extensionManager.getActiveDataSource()[0];
|
2023-09-01 22:19:39 +02:00
|
|
|
const { measurementService, displaySetService, customizationService } = servicesManager.services;
|
|
|
|
|
|
|
|
|
|
const codingValues = customizationService.getCustomization('codingValues', {});
|
|
|
|
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
2020-08-04 11:11:23 +02:00
|
|
|
|
|
|
|
|
// TODO -> We should define a strict versioning somewhere.
|
2023-02-10 22:46:09 +01:00
|
|
|
const mappings = measurementService.getSourceMappings(
|
2022-07-27 18:39:04 +02:00
|
|
|
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
|
|
|
|
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
2020-08-04 11:11:23 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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 = {};
|
2022-10-13 17:07:56 +02:00
|
|
|
const imageIdsForToolState = {};
|
2020-08-04 11:11:23 +02:00
|
|
|
|
|
|
|
|
displaySet.measurements.forEach(measurement => {
|
2022-10-13 17:07:56 +02:00
|
|
|
const { ReferencedSOPInstanceUID, imageId, frameNumber } = measurement;
|
|
|
|
|
|
2020-08-04 11:11:23 +02:00
|
|
|
if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
|
|
|
|
|
sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
|
2022-10-13 17:07:56 +02:00
|
|
|
imageIdsForToolState[ReferencedSOPInstanceUID] = [];
|
|
|
|
|
}
|
|
|
|
|
if (!imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber]) {
|
|
|
|
|
imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber] = imageId;
|
2020-08-04 11:11:23 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
const datasetToUse = _mapLegacyDataSet(instance);
|
|
|
|
|
|
2020-08-04 11:11:23 +02:00
|
|
|
// Use dcmjs to generate toolState.
|
2022-07-27 18:39:04 +02:00
|
|
|
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
|
2020-08-04 11:11:23 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Filter what is found by DICOM SR to measurements we support.
|
2022-07-27 18:39:04 +02:00
|
|
|
const mappingDefinitions = mappings.map(m => m.annotationType);
|
2020-08-04 11:11:23 +02:00
|
|
|
const hydratableMeasurementsInSR = {};
|
|
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
Object.keys(storedMeasurementByAnnotationType).forEach(key => {
|
2020-08-04 11:11:23 +02:00
|
|
|
if (mappingDefinitions.includes(key)) {
|
2022-07-27 18:39:04 +02:00
|
|
|
hydratableMeasurementsInSR[key] = storedMeasurementByAnnotationType[key];
|
2020-08-04 11:11:23 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Set the series touched as tracked.
|
|
|
|
|
const imageIds = [];
|
|
|
|
|
|
|
|
|
|
// TODO: notification if no hydratable?
|
2022-07-27 18:39:04 +02:00
|
|
|
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
|
2023-09-01 22:19:39 +02:00
|
|
|
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
toolDataForAnnotationType.forEach(toolData => {
|
2020-08-04 11:11:23 +02:00
|
|
|
// Add the measurement to toolState
|
2022-10-13 17:07:56 +02:00
|
|
|
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
|
|
|
|
|
// files, and looking up the imageId from sopInstanceUIDToImageId results
|
|
|
|
|
// in the wrong value.
|
2023-09-01 22:19:39 +02:00
|
|
|
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
|
2022-10-13 17:07:56 +02:00
|
|
|
const imageId =
|
|
|
|
|
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
|
|
|
|
|
sopInstanceUIDToImageId[toolData.sopInstanceUid];
|
2020-08-04 11:11:23 +02:00
|
|
|
|
|
|
|
|
if (!imageIds.includes(imageId)) {
|
|
|
|
|
imageIds.push(imageId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let targetStudyInstanceUID;
|
|
|
|
|
const SeriesInstanceUIDs = [];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < imageIds.length; i++) {
|
|
|
|
|
const imageId = imageIds[i];
|
2023-09-01 22:19:39 +02:00
|
|
|
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
|
2020-08-04 11:11:23 +02:00
|
|
|
|
|
|
|
|
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
|
|
|
|
|
SeriesInstanceUIDs.push(SeriesInstanceUID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!targetStudyInstanceUID) {
|
|
|
|
|
targetStudyInstanceUID = StudyInstanceUID;
|
|
|
|
|
} else if (targetStudyInstanceUID !== StudyInstanceUID) {
|
2023-09-01 22:19:39 +02:00
|
|
|
console.warn('NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.');
|
2020-08-04 11:11:23 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
|
2023-09-01 22:19:39 +02:00
|
|
|
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
toolDataForAnnotationType.forEach(toolData => {
|
2020-08-04 11:11:23 +02:00
|
|
|
// Add the measurement to toolState
|
2022-10-13 17:07:56 +02:00
|
|
|
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
|
|
|
|
|
// files, and looking up the imageId from sopInstanceUIDToImageId results
|
|
|
|
|
// in the wrong value.
|
2023-09-01 22:19:39 +02:00
|
|
|
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
|
2022-10-13 17:07:56 +02:00
|
|
|
const imageId =
|
|
|
|
|
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
|
|
|
|
|
sopInstanceUIDToImageId[toolData.sopInstanceUid];
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
toolData.uid = guid();
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
const instance = metaData.get('instance', imageId);
|
2021-05-28 20:32:48 +02:00
|
|
|
const {
|
|
|
|
|
FrameOfReferenceUID,
|
2022-07-27 18:39:04 +02:00
|
|
|
// SOPInstanceUID,
|
|
|
|
|
// SeriesInstanceUID,
|
|
|
|
|
// StudyInstanceUID,
|
2021-05-28 20:32:48 +02:00
|
|
|
} = instance;
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
const annotation = {
|
|
|
|
|
annotationUID: toolData.annotation.annotationUID,
|
|
|
|
|
data: toolData.annotation.data,
|
|
|
|
|
metadata: {
|
|
|
|
|
toolName: annotationType,
|
|
|
|
|
referencedImageId: imageId,
|
|
|
|
|
FrameOfReferenceUID,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2023-02-10 22:46:09 +01:00
|
|
|
const source = measurementService.getSource(
|
2022-07-27 18:39:04 +02:00
|
|
|
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
|
|
|
|
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
2020-08-04 11:11:23 +02:00
|
|
|
);
|
2022-07-27 18:39:04 +02:00
|
|
|
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
|
2023-09-01 22:19:39 +02:00
|
|
|
annotation.data.finding = convertCode(codingValues, toolData.finding?.[0]);
|
|
|
|
|
annotation.data.findingSites = convertSites(codingValues, toolData.findingSites);
|
2023-03-24 21:45:21 +01:00
|
|
|
annotation.data.site = annotation.data.findingSites?.[0];
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
const matchingMapping = mappings.find(m => m.annotationType === annotationType);
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2023-08-16 16:26:28 +02:00
|
|
|
const newAnnotationUID = measurementService.addRawMeasurement(
|
2020-08-04 11:11:23 +02:00
|
|
|
source,
|
2022-07-27 18:39:04 +02:00
|
|
|
annotationType,
|
|
|
|
|
{ annotation },
|
|
|
|
|
matchingMapping.toMeasurementSchema,
|
2021-05-28 20:32:48 +02:00
|
|
|
dataSource
|
2020-08-04 11:11:23 +02:00
|
|
|
);
|
|
|
|
|
|
2023-08-16 16:26:28 +02:00
|
|
|
if (disableEditing) {
|
2023-09-01 22:19:39 +02:00
|
|
|
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
|
2023-08-16 16:26:28 +02:00
|
|
|
locking.setAnnotationLocked(addedAnnotation, true);
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-04 11:11:23 +02:00
|
|
|
if (!imageIds.includes(imageId)) {
|
|
|
|
|
imageIds.push(imageId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
displaySet.isHydrated = true;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
StudyInstanceUID: targetStudyInstanceUID,
|
|
|
|
|
SeriesInstanceUIDs,
|
|
|
|
|
};
|
|
|
|
|
}
|
2022-07-27 18:39:04 +02:00
|
|
|
|
|
|
|
|
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
|
2023-09-01 22:19:39 +02:00
|
|
|
const measurementGroups = toArray(imagingMeasurementContent.ContentSequence).filter(
|
|
|
|
|
codeMeaningEquals(GROUP)
|
|
|
|
|
);
|
2022-07-27 18:39:04 +02:00
|
|
|
|
|
|
|
|
// For each of the supported measurement types, compute the measurement data
|
|
|
|
|
const measurementData = {};
|
|
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
const cornerstoneToolClasses = MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
2022-07-27 18:39:04 +02:00
|
|
|
|
|
|
|
|
const registeredToolClasses = [];
|
|
|
|
|
|
|
|
|
|
Object.keys(cornerstoneToolClasses).forEach(key => {
|
|
|
|
|
registeredToolClasses.push(cornerstoneToolClasses[key]);
|
|
|
|
|
measurementData[key] = [];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
measurementGroups.forEach((measurementGroup, index) => {
|
2023-09-01 22:19:39 +02:00
|
|
|
const measurementGroupContentSequence = toArray(measurementGroup.ContentSequence);
|
2022-07-27 18:39:04 +02:00
|
|
|
|
|
|
|
|
const TrackingIdentifierGroup = measurementGroupContentSequence.find(
|
2023-09-01 22:19:39 +02:00
|
|
|
contentItem => contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
|
2022-07-27 18:39:04 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
const toArray = function (x) {
|
2022-07-27 18:39:04 +02:00
|
|
|
return Array.isArray(x) ? x : [x];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const codeMeaningEquals = codeMeaningName => {
|
|
|
|
|
return contentItem => {
|
|
|
|
|
return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName;
|
|
|
|
|
};
|
|
|
|
|
};
|