diff --git a/extensions/cornerstone/src/tools/CalibrationLineTool.ts b/extensions/cornerstone/src/tools/CalibrationLineTool.ts index ae914019bae..3b4a9bede0b 100644 --- a/extensions/cornerstone/src/tools/CalibrationLineTool.ts +++ b/extensions/cornerstone/src/tools/CalibrationLineTool.ts @@ -76,11 +76,12 @@ export function onCompletedCalibrationLine(servicesManager, csToolsEvent) { const adjustCalibration = newLength => { const spacingScale = newLength / length; - const rowSpacing = spacingScale * currentRowPixelSpacing; - const colSpacing = spacingScale * currentColumnPixelSpacing; // trigger resize of the viewport to adjust the world/pixel mapping - calibrateImageSpacing(imageId, viewport.getRenderingEngine(), rowSpacing, colSpacing); + calibrateImageSpacing(imageId, viewport.getRenderingEngine(), { + type: 'User', + scale: 1 / spacingScale, + }); }; return new Promise((resolve, reject) => { diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts index a1c214e1c76..8652c7c8a2e 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts @@ -99,8 +99,7 @@ function getMappedAnnotations(annotation, displaySetService) { ); const { SeriesNumber } = displaySet; - const { length, width } = targetStats; - const unit = 'mm'; + const { length, width, unit } = targetStats; annotations.push({ SeriesInstanceUID, @@ -130,9 +129,9 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { values.push('Cornerstone:Bidirectional'); mappedAnnotations.forEach(annotation => { - const { length, width } = annotation; - columns.push(`Length (mm)`, `Width (mm)`); - values.push(length, width); + const { length, width, unit } = annotation; + columns.push(`Length`, `Width`, 'Unit'); + values.push(length, width, unit); }); if (FrameOfReferenceUID) { @@ -162,7 +161,14 @@ function getDisplayText(mappedAnnotations, displaySet) { const displayText = []; // Area is the same for all series - const { length, width, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + const { + length, + width, + unit, + SeriesNumber, + SOPInstanceUID, + frameNumber, + } = mappedAnnotations[0]; const roundedLength = utils.roundNumber(length, 2); const roundedWidth = utils.roundNumber(width, 2); @@ -176,8 +182,10 @@ function getDisplayText(mappedAnnotations, displaySet) { const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - displayText.push(`L: ${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`); - displayText.push(`W: ${roundedWidth} mm`); + displayText.push( + `L: ${roundedLength} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})` + ); + displayText.push(`W: ${roundedWidth} ${unit}`); return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts index ac784878b86..7c8f8f7cb0b 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts @@ -98,7 +98,7 @@ function getMappedAnnotations(annotation, DisplaySetService) { ); const { SeriesNumber } = displaySet; - const { mean, stdDev, max, area, Modality, modalityUnit } = targetStats; + const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; annotations.push({ SeriesInstanceUID, @@ -111,6 +111,7 @@ function getMappedAnnotations(annotation, DisplaySetService) { stdDev, max, area, + areaUnit, }); }); @@ -131,14 +132,20 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { values.push('Cornerstone:CircleROI'); mappedAnnotations.forEach(annotation => { - const { mean, stdDev, max, area, unit } = annotation; + const { mean, stdDev, max, area, unit, areaUnit } = annotation; if (!mean || !unit || !max || !area) { return; } - columns.push(`max (${unit})`, `mean (${unit})`, `std (${unit})`, `area (mm2)`); - values.push(max, mean, stdDev, area); + columns.push( + `max (${unit})`, + `mean (${unit})`, + `std (${unit})`, + 'Area', + 'Unit' + ); + values.push(max, mean, stdDev, area, areaUnit); }); if (FrameOfReferenceUID) { @@ -168,7 +175,7 @@ function getDisplayText(mappedAnnotations, displaySet) { const displayText = []; // Area is the same for all series - const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); @@ -182,7 +189,7 @@ function getDisplayText(mappedAnnotations, displaySet) { // Area sometimes becomes undefined if `preventHandleOutsideImage` is off. const roundedArea = utils.roundNumber(area || 0, 2); - displayText.push(`${roundedArea} mm2`); + displayText.push(`${roundedArea} ${areaUnit}`); // Todo: we need a better UI for displaying all these information mappedAnnotations.forEach(mappedAnnotation => { diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts index aa7e5f12977..9bcf67d5dd4 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts @@ -98,7 +98,7 @@ function getMappedAnnotations(annotation, displaySetService) { ); const { SeriesNumber } = displaySet; - const { mean, stdDev, max, area, Modality, modalityUnit } = targetStats; + const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; annotations.push({ SeriesInstanceUID, @@ -107,6 +107,7 @@ function getMappedAnnotations(annotation, displaySetService) { frameNumber, Modality, unit: modalityUnit, + areaUnit, mean, stdDev, max, @@ -131,14 +132,20 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { values.push('Cornerstone:EllipticalROI'); mappedAnnotations.forEach(annotation => { - const { mean, stdDev, max, area, unit } = annotation; + const { mean, stdDev, max, area, unit, areaUnit } = annotation; if (!mean || !unit || !max || !area) { return; } - columns.push(`max (${unit})`, `mean (${unit})`, `std (${unit})`, `area (mm2)`); - values.push(max, mean, stdDev, area); + columns.push( + `max (${unit})`, + `mean (${unit})`, + `std (${unit})`, + 'Area', + 'Unit' + ); + values.push(max, mean, stdDev, area, areaUnit); }); if (FrameOfReferenceUID) { @@ -168,7 +175,7 @@ function getDisplayText(mappedAnnotations, displaySet) { const displayText = []; // Area is the same for all series - const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); @@ -180,9 +187,8 @@ function getDisplayText(mappedAnnotations, displaySet) { const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - // Area sometimes becomes undefined if `preventHandleOutsideImage` is off. - const roundedArea = utils.roundNumber(area || 0, 2); - displayText.push(`${roundedArea} mm2`); + const roundedArea = utils.roundNumber(area, 2); + displayText.push(`${roundedArea} ${areaUnit}`); // Todo: we need a better UI for displaying all these information mappedAnnotations.forEach(mappedAnnotation => { diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts index 249522e847a..5c5072d2c19 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts @@ -32,7 +32,11 @@ const Length = { throw new Error('Tool not supported'); } - const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + const { + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes( referencedImageId, cornerstoneViewportService, viewportId @@ -94,8 +98,11 @@ function getMappedAnnotations(annotation, displaySetService) { throw new Error('Non-acquisition plane measurement mapping not supported'); } - const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = - getSOPInstanceAttributes(referencedImageId); + const { + SOPInstanceUID, + SeriesInstanceUID, + frameNumber, + } = getSOPInstanceAttributes(referencedImageId); const displaySet = displaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, @@ -104,8 +111,7 @@ function getMappedAnnotations(annotation, displaySetService) { ); const { SeriesNumber } = displaySet; - const { length } = targetStats; - const unit = 'mm'; + const { length, unit = 'mm' } = targetStats; annotations.push({ SeriesInstanceUID, @@ -134,9 +140,11 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { values.push('Cornerstone:Length'); mappedAnnotations.forEach(annotation => { - const { length } = annotation; - columns.push(`Length (mm)`); + const { length, unit } = annotation; + columns.push(`Length`); values.push(length); + columns.push('Unit'); + values.push(unit); }); if (FrameOfReferenceUID) { @@ -166,7 +174,13 @@ function getDisplayText(mappedAnnotations, displaySet) { const displayText = []; // Area is the same for all series - const { length, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + const { + length, + SeriesNumber, + SOPInstanceUID, + frameNumber, + unit, + } = mappedAnnotations[0]; const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); @@ -182,7 +196,9 @@ function getDisplayText(mappedAnnotations, displaySet) { return displayText; } const roundedLength = utils.roundNumber(length, 2); - displayText.push(`${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`); + displayText.push( + `${roundedLength} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})` + ); return displayText; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts index 035b87457c6..567badd1d2d 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts @@ -25,7 +25,11 @@ const RectangleROI = { throw new Error('Tool not supported'); } - const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + const { + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes( referencedImageId, CornerstoneViewportService, viewportId @@ -88,8 +92,11 @@ function getMappedAnnotations(annotation, DisplaySetService) { throw new Error('Non-acquisition plane measurement mapping not supported'); } - const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = - getSOPInstanceAttributes(referencedImageId); + const { + SOPInstanceUID, + SeriesInstanceUID, + frameNumber, + } = getSOPInstanceAttributes(referencedImageId); const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, @@ -98,7 +105,7 @@ function getMappedAnnotations(annotation, DisplaySetService) { ); const { SeriesNumber } = displaySet; - const { mean, stdDev, max, area, Modality, modalityUnit } = targetStats; + const { mean, stdDev, max, area, Modality, modalityUnit, areaUnit } = targetStats; annotations.push({ SeriesInstanceUID, @@ -111,6 +118,7 @@ function getMappedAnnotations(annotation, DisplaySetService) { stdDev, max, area, + areaUnit, }); }); @@ -131,14 +139,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { values.push('Cornerstone:RectangleROI'); mappedAnnotations.forEach(annotation => { - const { mean, stdDev, max, area, unit } = annotation; + const { mean, stdDev, max, area, unit, areaUnit } = annotation; if (!mean || !unit || !max || !area) { return; } - columns.push(`max (${unit})`, `mean (${unit})`, `std (${unit})`, `area (mm2)`); - values.push(max, mean, stdDev, area); + columns.push(`Maximum`, `Mean`, `Std Dev`, 'Pixel Unit', `Area`, 'Unit'); + values.push(max, mean, stdDev, unit, area, areaUnit); }); if (FrameOfReferenceUID) { @@ -168,7 +176,7 @@ function getDisplayText(mappedAnnotations, displaySet) { const displayText = []; // Area is the same for all series - const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); @@ -182,7 +190,7 @@ function getDisplayText(mappedAnnotations, displaySet) { // Area sometimes becomes undefined if `preventHandleOutsideImage` is off. const roundedArea = utils.roundNumber(area || 0, 2); - displayText.push(`${roundedArea} mm2`); + displayText.push(`${roundedArea} ${areaUnit}`); // Todo: we need a better UI for displaying all these information mappedAnnotations.forEach(mappedAnnotation => { diff --git a/platform/core/src/classes/MetadataProvider.js b/platform/core/src/classes/MetadataProvider.ts similarity index 89% rename from platform/core/src/classes/MetadataProvider.js rename to platform/core/src/classes/MetadataProvider.ts index 41e6cdbc6db..7b59c3ebd68 100644 --- a/platform/core/src/classes/MetadataProvider.js +++ b/platform/core/src/classes/MetadataProvider.ts @@ -68,7 +68,7 @@ class MetadataProvider { if (!instance) { return; - } + } return (frameNumber && combineFrameInstance(frameNumber, instance)) || instance; } @@ -114,8 +114,20 @@ class MetadataProvider { return this._getCornerstoneDICOMImageLoaderTag(naturalizedTagOrWADOImageLoaderTag, instance); } + /** + * Adds a new handler for the given tag. The handler will be provided an + * instance object that it can read values from. + */ + public addHandler( + wadoImageLoaderTag: string, + handler + ) { + WADO_IMAGE_LOADER[wadoImageLoaderTag] = handler; + } + _getCornerstoneDICOMImageLoaderTag(wadoImageLoaderTag, instance) { - let metadata; + let metadata = WADO_IMAGE_LOADER[wadoImageLoaderTag]?.(instance); + if (metadata) return metadata; switch (wadoImageLoaderTag) { case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE: @@ -153,45 +165,6 @@ class MetadataProvider { patientSex: instance.PatientSex, }; break; - case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE: - const { ImageOrientationPatient } = instance; - - // Fallback for DX images. - // TODO: We should use the rest of the results of this function - // to update the UI somehow - const { PixelSpacing } = getPixelSpacingInformation(instance); - - let rowPixelSpacing; - let columnPixelSpacing; - - let rowCosines; - let columnCosines; - - if (PixelSpacing) { - rowPixelSpacing = PixelSpacing[0]; - columnPixelSpacing = PixelSpacing[1]; - } - - if (ImageOrientationPatient) { - rowCosines = ImageOrientationPatient.slice(0, 3); - columnCosines = ImageOrientationPatient.slice(3, 6); - } - - metadata = { - frameOfReferenceUID: instance.FrameOfReferenceUID, - rows: toNumber(instance.Rows), - columns: toNumber(instance.Columns), - imageOrientationPatient: toNumber(ImageOrientationPatient), - rowCosines: toNumber(rowCosines || [0, 1, 0]), - columnCosines: toNumber(columnCosines || [0, 0, -1]), - imagePositionPatient: toNumber(instance.ImagePositionPatient || [0, 0, 0]), - sliceThickness: toNumber(instance.SliceThickness), - sliceLocation: toNumber(instance.SliceLocation), - pixelSpacing: toNumber(PixelSpacing || 1), - rowPixelSpacing: toNumber(rowPixelSpacing || 1), - columnPixelSpacing: toNumber(columnPixelSpacing || 1), - }; - break; case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE: metadata = { samplesPerPixel: toNumber(instance.SamplesPerPixel), @@ -484,11 +457,54 @@ const metadataProvider = new MetadataProvider(); export default metadataProvider; +const WADO_IMAGE_LOADER = { + imagePlaneModule: instance => { + const { ImageOrientationPatient } = instance; + + // Fallback for DX images. + // TODO: We should use the rest of the results of this function + // to update the UI somehow + const { PixelSpacing } = getPixelSpacingInformation(instance); + + let rowPixelSpacing; + let columnPixelSpacing; + + let rowCosines; + let columnCosines; + + if (PixelSpacing) { + rowPixelSpacing = PixelSpacing[0]; + columnPixelSpacing = PixelSpacing[1]; + } + + if (ImageOrientationPatient) { + rowCosines = ImageOrientationPatient.slice(0, 3); + columnCosines = ImageOrientationPatient.slice(3, 6); + } + + return { + frameOfReferenceUID: instance.FrameOfReferenceUID, + rows: toNumber(instance.Rows), + columns: toNumber(instance.Columns), + imageOrientationPatient: toNumber(ImageOrientationPatient), + rowCosines: toNumber(rowCosines || [0, 1, 0]), + columnCosines: toNumber(columnCosines || [0, 0, -1]), + imagePositionPatient: toNumber( + instance.ImagePositionPatient || [0, 0, 0] + ), + sliceThickness: toNumber(instance.SliceThickness), + sliceLocation: toNumber(instance.SliceLocation), + pixelSpacing: toNumber(PixelSpacing || 1), + rowPixelSpacing: rowPixelSpacing ? toNumber(rowPixelSpacing) : null, + columnPixelSpacing: columnPixelSpacing ? toNumber(columnPixelSpacing) : null, + }; + }, +}; + const WADO_IMAGE_LOADER_TAGS = { // dicomImageLoader specific GENERAL_SERIES_MODULE: 'generalSeriesModule', PATIENT_STUDY_MODULE: 'patientStudyModule', - IMAGE_PLANE_MODULE: 'imagePlaneModule', IMAGE_PIXEL_MODULE: 'imagePixelModule', VOI_LUT_MODULE: 'voiLutModule', MODALITY_LUT_MODULE: 'modalityLutModule', diff --git a/platform/core/src/utils/roundNumber.js b/platform/core/src/utils/roundNumber.js index a441225da50..be76c7edea3 100644 --- a/platform/core/src/utils/roundNumber.js +++ b/platform/core/src/utils/roundNumber.js @@ -1,9 +1,34 @@ /** - * @param {string | number} value - * @param {number} decimals + * Truncates decimal points to that there is at least 1+precision significant + * digits. + * + * For example, with the default precision 2 (3 significant digits) + * * Values larger than 100 show no information after the decimal point + * * Values between 10 and 99 show 1 decimal point + * * Values between 1 and 9 show 2 decimal points + * + * @param value - to return a fixed measurement value from + * @param precision - defining how many digits after 1..9 are desired */ -function _round(value, decimals) { - return Number(value).toFixed(decimals); +function roundNumber(value: string | number, precision = 2): string { + if (value === undefined || value === null || value === '') return 'NaN'; + value = Number(value); + if (value < 0.0001) return `${value}`; + const fixedPrecision = + value >= 100 + ? precision - 2 + : value >= 10 + ? precision - 1 + : value >= 1 + ? precision + : value >= 0.1 + ? precision + 1 + : value >= 0.01 + ? precision + 2 + : value >= 0.001 + ? precision + 3 + : precision + 4; + return value.toFixed(fixedPrecision); } -export default _round; +export default roundNumber;