import { DicomMetadataStore, utils } from '@ohif/core'; import * as cs from '@cornerstonejs/core'; import * as csTools from '@cornerstonejs/tools'; const CHART_MODALITY = 'CHT'; const SEG_CHART_INSTANCE_UID = utils.guid(); // Private SOPClassUid for chart data const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1'; const { utilities: csToolsUtils } = csTools; function _getDateTimeStr() { const now = new Date(); const date = now.getFullYear() + ('0' + now.getUTCMonth()).slice(-2) + ('0' + now.getUTCDate()).slice(-2); const time = ('0' + now.getUTCHours()).slice(-2) + ('0' + now.getUTCMinutes()).slice(-2) + ('0' + now.getUTCSeconds()).slice(-2); return { date, time }; } function _getTimePointsDataByTagName(volume, timePointsTag) { const uniqueTimePoints = volume.imageIds.reduce((timePoints, imageId) => { const instance = DicomMetadataStore.getInstanceByImageId(imageId); const timePointValue = instance[timePointsTag]; if (timePointValue !== undefined) { timePoints.add(timePointValue); } return timePoints; }, new Set()); return Array.from(uniqueTimePoints).sort((a: number, b: number) => a - b); } function _convertTimePointsUnit(timePoints, timePointsUnit) { const validUnits = ['ms', 's', 'm', 'h']; const divisors = [1000, 60, 60]; const currentUnitIndex = validUnits.indexOf(timePointsUnit); let divisor = 1; if (currentUnitIndex !== -1) { for (let i = currentUnitIndex; i < validUnits.length - 1; i++) { const newDivisor = divisor * divisors[i]; const greaterThanDivisorCount = timePoints.filter(timePoint => timePoint > newDivisor).length; // Change the scale only if more than 50% of the time points are // greater than the new divisor. if (greaterThanDivisorCount <= timePoints.length / 2) { break; } divisor = newDivisor; timePointsUnit = validUnits[i + 1]; } if (divisor > 1) { timePoints = timePoints.map(timePoint => timePoint / divisor); } } return { timePoints, timePointsUnit }; } // It currently supports only one tag but a few other will be added soon // Supported 4D Tags // (0018,1060) Trigger Time [NOK] // (0018,0081) Echo Time [NOK] // (0018,0086) Echo Number [NOK] // (0020,0100) Temporal Position Identifier [NOK] // (0054,1300) FrameReferenceTime [OK] function _getTimePointsData(volume) { const timePointsTags = { FrameReferenceTime: { unit: 'ms', }, }; const timePointsTagNames = Object.keys(timePointsTags); let timePoints; let timePointsUnit; for (let i = 0; i < timePointsTagNames.length; i++) { const tagName = timePointsTagNames[i]; const curTimePoints = _getTimePointsDataByTagName(volume, tagName); if (curTimePoints.length) { timePoints = curTimePoints; timePointsUnit = timePointsTags[tagName].unit; break; } } if (!timePoints.length) { const concatTagNames = timePointsTagNames.join(', '); throw new Error(`Could not extract time points data for the following tags: ${concatTagNames}`); } const convertedTimePoints = _convertTimePointsUnit(timePoints, timePointsUnit); timePoints = convertedTimePoints.timePoints; timePointsUnit = convertedTimePoints.timePointsUnit; return { timePoints, timePointsUnit }; } function _getSegmentationData( segmentation, volumesTimePointsCache, { servicesManager }: { servicesManager: AppTypes.ServicesManager } ) { const { displaySetService, segmentationService, viewportGridService } = servicesManager.services; const displaySets = displaySetService.getActiveDisplaySets(); const dynamic4DDisplaySet = displaySets.find(displaySet => { const anInstance = displaySet.instances?.[0]; if (anInstance) { return ( anInstance.FrameReferenceTime !== undefined || anInstance.NumberOfTimeSlices !== undefined ); } return false; }); // const referencedDynamicVolume = cs.cache.getVolume(dynamic4DDisplaySet.displaySetInstanceUID); let volumeCacheKey: string | undefined; const volumeId = dynamic4DDisplaySet.displaySetInstanceUID; for (const [key] of cs.cache._volumeCache) { if (key.includes(volumeId)) { volumeCacheKey = key; break; } } let referencedDynamicVolume; if (volumeCacheKey) { referencedDynamicVolume = cs.cache.getVolume(volumeCacheKey); } const { StudyInstanceUID, StudyDescription } = DicomMetadataStore.getInstanceByImageId( referencedDynamicVolume.imageIds[0] ); const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId); const maskVolumeId = segmentationVolume?.volumeId; const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, { maskVolumeId, }) as number[][]; const pixelCount = timeData.length; if (pixelCount === 0) { return []; } // Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex // const color = csTools.segmentation.config.color.getSegmentIndexColor( // segmentationRepresentationUID, // 1 // segmentIndex // ); const viewportId = viewportGridService.getActiveViewportId(); const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1); const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]); let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume); if (!timePointsData) { timePointsData = _getTimePointsData(referencedDynamicVolume); volumesTimePointsCache.set(referencedDynamicVolume, timePointsData); } const { timePoints, timePointsUnit } = timePointsData; if (timePoints.length !== timeData[0].length) { throw new Error('Invalid number of time points returned'); } const timepointsCount = timePoints.length; const chartSeriesData = new Array(timepointsCount); for (let i = 0; i < timepointsCount; i++) { const average = timeData.reduce((acc, cur) => acc + cur[i] / pixelCount, 0); chartSeriesData[i] = [timePoints[i], average]; } return { StudyInstanceUID, StudyDescription, chartData: { series: { label: segmentation.label, points: chartSeriesData, color: hexColor, }, axis: { x: { label: `Time (${timePointsUnit})`, }, y: { label: `Vl (Bq/ml)`, }, }, }, }; } function _getInstanceFromSegmentations(segmentations, { servicesManager }) { if (!segmentations.length) { return; } const volumesTimePointsCache = new WeakMap(); const segmentationsData = segmentations.map(segmentation => _getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager }) ); const { date: seriesDate, time: seriesTime } = _getDateTimeStr(); const series = segmentationsData.reduce((allSeries, curSegData) => { return [...allSeries, curSegData.chartData.series]; }, []); const instance = { SOPClassUID: ChartDataSOPClassUid, Modality: CHART_MODALITY, SOPInstanceUID: utils.guid(), SeriesDate: seriesDate, SeriesTime: seriesTime, SeriesInstanceUID: SEG_CHART_INSTANCE_UID, StudyInstanceUID: segmentationsData[0].StudyInstanceUID, StudyDescription: segmentationsData[0].StudyDescription, SeriesNumber: 100, SeriesDescription: 'Segmentation chart series data', chartData: { series, axis: { ...segmentationsData[0].chartData.axis }, }, }; const seriesMetadata = { StudyInstanceUID: instance.StudyInstanceUID, StudyDescription: instance.StudyDescription, SeriesInstanceUID: instance.SeriesInstanceUID, SeriesDescription: instance.SeriesDescription, SeriesNumber: instance.SeriesNumber, SeriesTime: instance.SeriesTime, SOPClassUID: instance.SOPClassUID, Modality: instance.Modality, }; return { seriesMetadata, instance }; } function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void { debugger; const { segmentationService } = servicesManager.services; const segmentations = segmentationService.getSegmentations(); const { seriesMetadata, instance } = _getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {}; if (seriesMetadata && instance) { // An event is triggered after adding the instance and the displaySet is created DicomMetadataStore.addSeriesMetadata([seriesMetadata], true); DicomMetadataStore.addInstances([instance], true); } } export { updateSegmentationsChartDisplaySet as default };