From 42b02a62d3475e47f9e14880e606fdc9685c35f9 Mon Sep 17 00:00:00 2001 From: James Petts Date: Wed, 19 Aug 2020 20:31:25 +0100 Subject: [PATCH] OHIF-326, export as CSV. (#1976) --- extensions/cornerstone/package.json | 2 +- .../EllipticalRoi.js | 27 ++- extensions/dicom-rt/package.json | 2 +- extensions/dicom-segmentation/package.json | 2 +- extensions/dicom-sr/package.json | 2 +- extensions/measurement-tracking/package.json | 2 +- .../src/_shared/downloadCSVReport.js | 164 ++++++++++++++++++ .../ActionButtons.jsx | 12 +- .../PanelMeasurementTableTracking/index.js | 24 +-- extensions/vtk/package.json | 2 +- platform/core/package.json | 2 +- .../MeasurementService/MeasurementService.js | 2 + platform/viewer/package.json | 2 +- yarn.lock | 16 +- 14 files changed, 211 insertions(+), 50 deletions(-) create mode 100644 extensions/measurement-tracking/src/_shared/downloadCSVReport.js diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 7f6017d82..fdbefb78e 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -33,7 +33,7 @@ "@ohif/ui": "^0.50.0", "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.19.1", + "cornerstone-tools": "4.20.1", "cornerstone-wado-image-loader": "^3.1.2", "dcmjs": "0.16.1", "dicom-parser": "^1.8.3", diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js index 5ecf0b19a..b951da0dc 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js @@ -33,7 +33,9 @@ const EllipticalRoi = { SeriesInstanceUID ); - const { start, end } = measurementData.handles; + const { cachedStats, handles } = measurementData; + + const { start, end } = handles; const halfXLength = Math.abs(start.x - end.x) / 2; const halfYLength = Math.abs(start.y - end.y) / 2; @@ -60,6 +62,20 @@ const EllipticalRoi = { points.push({ x: center.x + halfXLength, y: center.y }); } + let meanSUV; + let stdDevSUV; + + if ( + cachedStats && + cachedStats.meanStdDevSUV && + cachedStats.meanStdDevSUV.mean !== 0 + ) { + const { meanStdDevSUV } = cachedStats; + + meanSUV = meanStdDevSUV.mean; + stdDevSUV = meanStdDevSUV.stdDev; + } + return { id: measurementData.id, SOPInstanceUID, @@ -70,10 +86,11 @@ const EllipticalRoi = { label: measurementData.label, description: measurementData.description, unit: measurementData.unit, - area: - measurementData.cachedStats && - measurementData.cachedStats - .area /* TODO: Add concept names instead (descriptor) */, + area: cachedStats && cachedStats.area, + mean: cachedStats && cachedStats.mean, + stdDev: cachedStats && cachedStats.stdDev, + meanSUV, + stdDevSUV, type: getValueTypeFromToolType(tool), points, }; diff --git a/extensions/dicom-rt/package.json b/extensions/dicom-rt/package.json index eb4140ba2..335d90e13 100644 --- a/extensions/dicom-rt/package.json +++ b/extensions/dicom-rt/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", - "cornerstone-tools": "4.19.1", + "cornerstone-tools": "4.20.1", "dcmjs": "0.16.1", "prop-types": "^15.6.2", "react": "^16.8.6", diff --git a/extensions/dicom-segmentation/package.json b/extensions/dicom-segmentation/package.json index f10491825..d9a3a0455 100644 --- a/extensions/dicom-segmentation/package.json +++ b/extensions/dicom-segmentation/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", - "cornerstone-tools": "4.19.0", + "cornerstone-tools": "4.20.1", "dcmjs": "0.16.1", "prop-types": "^15.6.2", "react": "^16.8.6", diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json index d886b8ae8..7a6309c59 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -33,7 +33,7 @@ "@ohif/ui": "^0.50.0", "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.19.1", + "cornerstone-tools": "4.20.1", "cornerstone-wado-image-loader": "^3.1.2", "dcmjs": "0.16.1", "dicom-parser": "^1.8.3", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 327c4f0a4..ab8222bbd 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -28,7 +28,7 @@ }, "peerDependencies": { "@ohif/core": "^0.50.0", - "cornerstone-tools": "4.19.0", + "cornerstone-tools": "4.20.1", "dcmjs": "0.16.1", "prop-types": "^15.6.2", "react": "^16.13.1", diff --git a/extensions/measurement-tracking/src/_shared/downloadCSVReport.js b/extensions/measurement-tracking/src/_shared/downloadCSVReport.js new file mode 100644 index 000000000..458513fcb --- /dev/null +++ b/extensions/measurement-tracking/src/_shared/downloadCSVReport.js @@ -0,0 +1,164 @@ +import { DicomMetadataStore } from '@ohif/core'; + +export default function downloadCSVReport( + measurementData, + StudyInstanceUID, + referencedSeriesInstanceUIDs, + MeasurementService +) { + const { VALUE_TYPES } = MeasurementService; + + if (measurementData.length === 0) { + // Prevent download of report with no measurements. + return; + } + + const seriesMetadataMap = {}; + + referencedSeriesInstanceUIDs.forEach(SeriesInstanceUID => { + seriesMetadataMap[SeriesInstanceUID] = DicomMetadataStore.getSeries( + StudyInstanceUID, + SeriesInstanceUID + ); + }); + + const rows = [ + [ + 'Patient ID', + 'Patient Name', + 'Study UID', + 'Series UID', + 'SOP Instance UID', + 'Label', + 'Long Axis (mm)', + 'Short Axis (mm)', + 'Length (mm)', + 'Area (mmˆ2)', + 'Mean', + 'StDev', + 'Units', + ], + ]; + + measurementData.forEach(measurement => { + switch (measurement.type) { + case VALUE_TYPES.BIDIRECTIONAL: + rows.push(_getBidirectionalRow(measurement, seriesMetadataMap)); + break; + case VALUE_TYPES.ELLIPSE: + rows.push(_getEllipseRow(measurement, seriesMetadataMap)); + break; + case VALUE_TYPES.POINT: + rows.push(_getAdditionalFindingRow(measurement, seriesMetadataMap)); + break; + case VALUE_TYPES.POLYLINE: + rows.push(_getLengthRow(measurement, seriesMetadataMap)); + } + }); + + let csvContent = + 'data:text/csv;charset=utf-8,' + rows.map(row => row.join(',')).join('\n'); + + _createAndDownloadFile(csvContent); +} + +function _getBidirectionalRow(measurement, seriesMetadataMap) { + const commonRowItems = _getCommonRowItems(measurement, seriesMetadataMap); + + return [ + ...commonRowItems, + measurement.longestDiameter, // Long Axis (mm) + measurement.shortestDiameter, // Short Axis (mm) + ]; +} + +function _getCommonRowItems(measurement, seriesMetadataMap) { + const seriesMetadata = seriesMetadataMap[measurement.referenceSeriesUID]; + const firstInstance = seriesMetadata.instances[0]; + + const row = [ + firstInstance.PatientID, // Patient ID + firstInstance.PatientName.Alphabetic, // PatientName + measurement.referenceStudyUID, // StudyInstanceUID + measurement.referenceSeriesUID, // SeriesInstanceUID + measurement.SOPInstanceUID, // SOPInstanceUID + measurement.label || '', // Label + ]; + + return row; +} + +function _getAdditionalFindingRow(measurement, seriesMetadataMap) { + const commonRowItems = _getCommonRowItems(measurement, seriesMetadataMap); + + return [...commonRowItems]; +} + +function _getEllipseRow(measurement, seriesMetadataMap) { + const commonRowItems = _getCommonRowItems(measurement, seriesMetadataMap); + const blankColumns = ['', '', '']; + + const { area, mean, stdDev, unit } = _getEllipseStatsWithUnits( + measurement, + seriesMetadataMap + ); + + return [ + ...commonRowItems, + ...blankColumns, + area, // Area (mmˆ2) + mean, // Mean + stdDev, // StDev + unit, + ]; +} + +function _getEllipseStatsWithUnits(measurement, seriesMetadataMap) { + const seriesMetadata = seriesMetadataMap[measurement.referenceSeriesUID]; + const firstInstance = seriesMetadata.instances[0]; + const modality = firstInstance.Modality; + + switch (modality) { + case 'CT': + return { + mean: `${measurement.mean.toFixed(1)}`, + stdDev: `${measurement.stdDev.toFixed(1)}`, + area: measurement.area.toFixed(1), + unit: 'HU', + }; + case 'PT': + if (measurement.meanSUV) { + return { + mean: `${measurement.meanSUV.toFixed(1)}`, + stdDev: `${measurement.stdDevSUV.toFixed(1)}`, + area: measurement.area.toFixed(1), + unit: 'SUV', + }; + } + // If no SUV calculation then fallback to default. + default: + return { + mean: `${measurement.mean.toFixed(1)}`, + stdDev: `${measurement.stdDev.toFixed(1)}`, + area: measurement.area.toFixed(1), + unit: 'Pixel Values', + }; + } +} + +function _getLengthRow(measurement, seriesMetadataMap) { + const commonRowItems = _getCommonRowItems(measurement, seriesMetadataMap); + const blankColumns = ['', '']; + + return [...commonRowItems, ...blankColumns, measurement.length.toFixed(1)]; +} + +function _createAndDownloadFile(csvContent) { + const encodedUri = encodeURI(csvContent); + + const link = document.createElement('a'); + link.setAttribute('href', encodedUri); + link.setAttribute('download', 'MeasurementReport.csv'); + document.body.appendChild(link); + link.click(); +} diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx index 93118b6a2..5842fe3c3 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx @@ -6,15 +6,9 @@ function ActionButtons({ onExportClick, onCreateReportClick }) { return ( - - - - +