309 lines
9.2 KiB
JavaScript
309 lines
9.2 KiB
JavaScript
import OHIF from '@ohif/core';
|
|
import dcmjs from 'dcmjs';
|
|
import cornerstone from 'cornerstone-core';
|
|
import cornerstoneTools from 'cornerstone-tools';
|
|
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
|
import TOOL_NAMES from './utils/toolNames';
|
|
|
|
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
|
|
const globalImageIdSpecificToolStateManager =
|
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
|
const { DicomLoaderService } = OHIF.utils;
|
|
|
|
export default async function loadRTStruct(
|
|
rtStructDisplaySet,
|
|
referencedDisplaySet,
|
|
studies
|
|
) {
|
|
const rtStructModule = cornerstoneTools.getModule('rtstruct');
|
|
|
|
// Set here is loading is asynchronous.
|
|
// If this function throws its set back to false.
|
|
rtStructDisplaySet.isLoaded = true;
|
|
|
|
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
|
|
|
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
|
rtStructDisplaySet,
|
|
studies
|
|
);
|
|
|
|
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
|
|
const rtStructDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
|
dicomData.dict
|
|
);
|
|
|
|
rtStructDataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
|
|
dicomData.meta
|
|
);
|
|
|
|
// global cornerstone tools state to attach measurements to.
|
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
|
|
|
const {
|
|
StructureSetROISequence,
|
|
ROIContourSequence,
|
|
RTROIObservationsSequence,
|
|
StructureSetLabel,
|
|
} = rtStructDataset;
|
|
|
|
// Define our structure set entry and add it to the rtstruct module state.
|
|
const structureSet = {
|
|
StructureSetLabel,
|
|
SeriesInstanceUID: rtStructDataset.SeriesInstanceUID,
|
|
ROIContours: [],
|
|
referencedSeriesSequence:
|
|
rtStructDisplaySet.metadata.ReferencedSeriesSequence,
|
|
visible: true,
|
|
};
|
|
|
|
rtStructModule.setters.structureSet(structureSet);
|
|
|
|
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
|
|
studies,
|
|
StudyInstanceUID,
|
|
SeriesInstanceUID
|
|
);
|
|
|
|
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
|
|
|
for (let i = 0; i < ROIContourSequence.length; i++) {
|
|
const ROIContour = ROIContourSequence[i];
|
|
const { ReferencedROINumber, ContourSequence } = ROIContour;
|
|
|
|
if (!ContourSequence) {
|
|
continue;
|
|
}
|
|
|
|
_setROIContourMetadata(
|
|
structureSet,
|
|
StructureSetROISequence,
|
|
RTROIObservationsSequence,
|
|
ROIContour
|
|
);
|
|
|
|
for (let c = 0; c < ContourSequence.length; c++) {
|
|
const {
|
|
ContourImageSequence,
|
|
ContourData,
|
|
NumberOfContourPoints,
|
|
ContourGeometricType,
|
|
} = ContourSequence[c];
|
|
|
|
if (ContourGeometricType !== 'CLOSED_PLANAR') {
|
|
// TODO: Do we want to visualise types other than closed planar?
|
|
// We could easily do open planar.
|
|
continue;
|
|
}
|
|
|
|
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
|
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
|
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
|
|
toolState,
|
|
imageId,
|
|
rtStructDisplayToolName
|
|
);
|
|
|
|
const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
|
|
const points = [];
|
|
|
|
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
|
|
points.push({
|
|
x: ContourData[p],
|
|
y: ContourData[p + 1],
|
|
z: ContourData[p + 2],
|
|
});
|
|
}
|
|
|
|
transformPointsToImagePlane(points, imagePlane);
|
|
|
|
const measurementData = {
|
|
handles: {
|
|
points,
|
|
},
|
|
structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
|
|
ROINumber: ReferencedROINumber,
|
|
};
|
|
|
|
imageIdSpecificToolData.push(measurementData);
|
|
}
|
|
}
|
|
|
|
_setToolEnabledIfNotEnabled(rtStructDisplayToolName);
|
|
|
|
/*
|
|
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
|
*
|
|
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
|
* or other components that could rely on loaded rtstructs that
|
|
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
|
* before the structs are fully loaded, the panel can subscribe to this custom event
|
|
* and update itself with the new structs.
|
|
*
|
|
* This limitation is due to the fact that the rtmodule is an object (which will be
|
|
* updated after the structs are loaded) that React its not aware of its changes
|
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
|
*
|
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
|
* allows us to easily watch the module or the rtstruct loading process in any other component
|
|
* without subscribing to external events.
|
|
*/
|
|
const event = new CustomEvent('extensiondicomrtrtloaded');
|
|
document.dispatchEvent(event);
|
|
}
|
|
|
|
function _setROIContourMetadata(
|
|
structureSet,
|
|
StructureSetROISequence,
|
|
RTROIObservationsSequence,
|
|
ROIContour
|
|
) {
|
|
const StructureSetROI = StructureSetROISequence.find(
|
|
structureSetROI =>
|
|
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
|
);
|
|
|
|
const ROIContourData = {
|
|
ROINumber: StructureSetROI.ROINumber,
|
|
ROIName: StructureSetROI.ROIName,
|
|
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
|
|
ROIDescription: StructureSetROI.ROIDescription,
|
|
visible: true,
|
|
};
|
|
|
|
_setROIContourDataColor(ROIContour, ROIContourData);
|
|
|
|
if (RTROIObservationsSequence) {
|
|
// If present, add additional RTROIObservations metadata.
|
|
_setROIContourRTROIObservations(
|
|
ROIContourData,
|
|
RTROIObservationsSequence,
|
|
ROIContour.ReferencedROINumber
|
|
);
|
|
}
|
|
|
|
structureSet.ROIContours.push(ROIContourData);
|
|
}
|
|
|
|
function _setROIContourDataColor(ROIContour, ROIContourData) {
|
|
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
|
|
|
|
if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
|
|
// If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
|
|
ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
|
|
}
|
|
|
|
if (ROIDisplayColor) {
|
|
ROIContourData.colorArray = [...ROIDisplayColor];
|
|
} else {
|
|
//Choose a color from the cornerstoneTools colorLUT
|
|
// We sample from the default color LUT here (i.e. 0), as we have nothing else to go on.
|
|
const { getters } = cornerstoneTools.getModule('segmentation');
|
|
const color = getters.colorForSegmentIndexColorLUT(
|
|
0,
|
|
ROIContourData.ROINumber
|
|
);
|
|
|
|
ROIContourData.colorArray = [...color];
|
|
}
|
|
}
|
|
|
|
function _setROIContourRTROIObservations(
|
|
ROIContourData,
|
|
RTROIObservationsSequence,
|
|
ROINumber
|
|
) {
|
|
const RTROIObservations = RTROIObservationsSequence.find(
|
|
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
|
|
);
|
|
|
|
if (RTROIObservations) {
|
|
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
|
|
const {
|
|
ObservationNumber,
|
|
ROIObservationDescription,
|
|
RTROIInterpretedType,
|
|
ROIInterpreter,
|
|
} = RTROIObservations;
|
|
|
|
ROIContourData.RTROIObservations = {
|
|
ObservationNumber,
|
|
ROIObservationDescription,
|
|
RTROIInterpretedType,
|
|
ROIInterpreter,
|
|
};
|
|
}
|
|
}
|
|
|
|
function _setToolEnabledIfNotEnabled(toolName) {
|
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
|
const { element } = enabledElement;
|
|
const tool = cornerstoneTools.getToolForElement(element, toolName);
|
|
|
|
if (tool.mode !== 'enabled') {
|
|
// If not already active or passive, set passive so contours render.
|
|
cornerstoneTools.setToolEnabled(toolName);
|
|
}
|
|
|
|
cornerstone.updateImage(element);
|
|
});
|
|
}
|
|
|
|
function _getOrCreateImageIdSpecificToolData(toolState, imageId, toolName) {
|
|
if (toolState.hasOwnProperty(imageId) === false) {
|
|
toolState[imageId] = {};
|
|
}
|
|
|
|
const imageIdToolState = toolState[imageId];
|
|
|
|
// If we don't have tool state for this type of tool, add an empty object
|
|
if (imageIdToolState.hasOwnProperty(toolName) === false) {
|
|
imageIdToolState[toolName] = {
|
|
data: [],
|
|
};
|
|
}
|
|
|
|
return imageIdToolState[toolName].data;
|
|
}
|
|
|
|
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
|
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
|
imageIdSopInstanceUidPairsEntry =>
|
|
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
|
);
|
|
|
|
return imageIdSopInstanceUidPairsEntry.imageId;
|
|
};
|
|
|
|
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
|
studies,
|
|
StudyInstanceUID,
|
|
SeriesInstanceUID
|
|
) {
|
|
const study = studies.find(
|
|
study => study.StudyInstanceUID === StudyInstanceUID
|
|
);
|
|
|
|
const displaySets = study.displaySets.filter(set => {
|
|
return set.SeriesInstanceUID === SeriesInstanceUID;
|
|
});
|
|
|
|
if (displaySets.length > 1) {
|
|
console.warn(
|
|
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
|
);
|
|
// TODO -> We could make check the instance list and see if any match?
|
|
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
|
// ^ Will that even happen?
|
|
}
|
|
|
|
const referencedDisplaySet = displaySets[0];
|
|
|
|
return referencedDisplaySet.images.map(image => {
|
|
return {
|
|
imageId: image.getImageId(),
|
|
sopInstanceUID: image.getSOPInstanceUID(),
|
|
};
|
|
});
|
|
}
|