import { utils } from '@ohif/core'; import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core'; import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools'; import { adaptersSEG, Enums } from '@cornerstonejs/adapters'; import { SOPClassHandlerId } from './id'; import { dicomlabToRGB } from './utils/dicomlabToRGB'; const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4']; const loadPromises = {}; function _getDisplaySetsFromSeries( instances, servicesManager: AppTypes.ServicesManager, extensionManager ) { const instance = instances[0]; const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, SeriesDescription, SeriesNumber, SeriesDate, SOPClassUID, wadoRoot, wadoUri, wadoUriRoot, } = instance; const displaySet = { Modality: 'SEG', loading: false, isReconstructable: true, // by default for now since it is a volumetric SEG currently displaySetInstanceUID: utils.guid(), SeriesDescription, SeriesNumber, SeriesDate, SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID, SOPClassHandlerId, SOPClassUID, referencedImages: null, referencedSeriesInstanceUID: null, referencedDisplaySetInstanceUID: null, isDerivedDisplaySet: true, isLoaded: false, isHydrated: false, segments: {}, sopClassUids, instance, instances: [instance], wadoRoot, wadoUriRoot, wadoUri, isOverlayDisplaySet: true, }; const referencedSeriesSequence = instance.ReferencedSeriesSequence; if (!referencedSeriesSequence) { console.error('ReferencedSeriesSequence is missing for the SEG'); return; } const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence; displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; const { displaySetService } = servicesManager.services; const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( displaySet.referencedSeriesInstanceUID ); const referencedDisplaySet = referencedDisplaySets[0]; if (!referencedDisplaySet) { // subscribe to display sets added which means at some point it will be available const { unsubscribe } = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, ({ displaySetsAdded }) => { // here we can also do a little bit of search, since sometimes DICOM SEG // does not contain the referenced display set uid , and we can just // see which of the display sets added is more similar and assign it // to the referencedDisplaySet const addedDisplaySet = displaySetsAdded[0]; if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) { displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID; unsubscribe(); } } ); } else { displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; } displaySet.load = async ({ headers }) => await _load(displaySet, servicesManager, extensionManager, headers); return [displaySet]; } function _load( segDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers ) { const { SOPInstanceUID } = segDisplaySet; const { segmentationService } = servicesManager.services; if ( (segDisplaySet.loading || segDisplaySet.isLoaded) && loadPromises[SOPInstanceUID] && _segmentationExists(segDisplaySet) ) { return loadPromises[SOPInstanceUID]; } segDisplaySet.loading = true; // We don't want to fire multiple loads, so we'll wait for the first to finish // and also return the same promise to any other callers. loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => { if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) { try { await _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers, }); } catch (e) { segDisplaySet.loading = false; return reject(e); } } segmentationService .createSegmentationForSEGDisplaySet(segDisplaySet) .then(() => { segDisplaySet.loading = false; resolve(); }) .catch(error => { segDisplaySet.loading = false; reject(error); }); }); return loadPromises[SOPInstanceUID]; } async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers, }: withAppTypes) { const utilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone.utilityModule.common' ); const { segmentationService, uiNotificationService } = servicesManager.services; const { dicomLoaderService } = utilityModule.exports; const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers); const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID( segDisplaySet.referencedDisplaySetInstanceUID ); if (!referencedDisplaySet) { throw new Error('referencedDisplaySet is missing for SEG'); } const { instances: images } = referencedDisplaySet; const imageIds = images.map(({ imageId }) => imageId); // Todo: what should be defaults here const tolerance = 0.001; const skipOverlapping = true; eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => { const { percentComplete } = evt.detail; segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, { percentComplete, }); }); const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState( imageIds, arrayBuffer, metaData, { skipOverlapping, tolerance, eventTarget, triggerEvent } ); let usedRecommendedDisplayCIELabValue = true; results.segMetadata.data.forEach((data, i) => { if (i > 0) { data.rgba = data.RecommendedDisplayCIELabValue; if (data.rgba) { data.rgba = dicomlabToRGB(data.rgba); } else { usedRecommendedDisplayCIELabValue = false; data.rgba = CONSTANTS.COLOR_LUT[i % CONSTANTS.COLOR_LUT.length]; } } }); if (results.overlappingSegments) { uiNotificationService.show({ title: 'Overlapping Segments', message: 'Unsupported overlapping segments detected, segmentation rendering results may be incorrect.', type: 'warning', }); } if (!usedRecommendedDisplayCIELabValue) { // Display a notification about the non-utilization of RecommendedDisplayCIELabValue uiNotificationService.show({ title: 'DICOM SEG import', message: 'RecommendedDisplayCIELabValue not found for one or more segments. The default color was used instead.', type: 'warning', duration: 5000, }); } Object.assign(segDisplaySet, results); } function _segmentationExists(segDisplaySet) { return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID); } function getSopClassHandlerModule({ servicesManager, extensionManager }) { const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; return [ { name: 'dicom-seg', sopClassUids, getDisplaySetsFromSeries, }, ]; } export default getSopClassHandlerModule;