diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 20a810b59..8759b1114 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -36,7 +36,7 @@ "@ohif/extension-cornerstone": "^3.0.0", "@ohif/extension-measurement-tracking": "^3.0.0", "@ohif/ui": "^2.0.0", - "dcmjs": "0.29.4", + "dcmjs": "^0.29.4", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", @@ -45,7 +45,8 @@ "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", - "@cornerstonejs/core": "0.30.1", - "@cornerstonejs/tools": "0.39.0" + "@cornerstonejs/adapters": "^0.3.1", + "@cornerstonejs/core": "^0.30.1", + "@cornerstonejs/tools": "^0.44.0" } } diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.js b/extensions/cornerstone-dicom-sr/src/commandsModule.js index 00431a3c8..7b685ed44 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.js +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.js @@ -30,14 +30,12 @@ const _generateReport = ( const report = MeasurementReport.generateReport( filteredToolState, metaData, - utilities.worldToImageCoords + utilities.worldToImageCoords, + options ); const { dataset } = report; - // Add in top level series options - Object.assign(dataset, options); - // Set the default character set as UTF-8 // https://dicom.innolitics.com/ciods/nm-image/sop-common/00080005 if (typeof dataset.SpecificCharacterSet === 'undefined') { @@ -89,7 +87,7 @@ const commandsModule = ({}) => { additionalFindingTypes, options = {}, }) => { - // TODO -> Eventually use the measurements directly and not the dcmjs adapter, + // Use the @cornerstonejs adapter for converting to/from DICOM // But it is good enough for now whilst we only have cornerstone as a datasource. log.info('[DICOMSR] storeMeasurements'); diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts similarity index 93% rename from extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.js rename to extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index 7bc1e1fdf..54a810a8d 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -51,13 +51,17 @@ const RELATIONSHIP_TYPE = { const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; /** - * Basic SOPClassHandler: - * - For all Image types that are stackable, create - * a displaySet with a stack of images + * DICOM SR SOP Class Handler + * For all referenced images in the TID 1500/300 sections, add an image to the + * display (this is TODO - it is not the actual behaviour below unfortunately) * - * @param {Array} sopClassHandlerModules List of SOP Class Modules - * @param {SeriesMetadata} series The series metadata object from which the display sets will be created - * @returns {Array} The list of display sets created for the given series object + * This will only display and rehydrate the latest DICOM SR in the given series + * It would be possible to add the ability to view older series rehydrations + * in the future. + * + * @param instances is a set of instances all from the same series + * @param servicesManager is the services that can be used for creating + * @returns The list of display sets created for the given instances object */ function _getDisplaySetsFromSeries( instances, @@ -69,7 +73,8 @@ function _getDisplaySetsFromSeries( throw new Error('No instances were provided'); } - const instance = instances[0]; + utils.sortStudyInstances(instances); + const instance = instances[instances.length - 1]; const { StudyInstanceUID, @@ -85,10 +90,10 @@ function _getDisplaySetsFromSeries( if ( !ConceptNameCodeSequence || ConceptNameCodeSequence.CodeValue !== - CodeNameCodeSequenceValues.ImagingMeasurementReport + CodeNameCodeSequenceValues.ImagingMeasurementReport ) { - console.warn( - 'Only support Imaging Measurement Report SRs (TID1500) for now' + console.log( + 'Only support Imaging Measurement Report SRs (TID1500) for this renderer.' ); return []; } @@ -105,6 +110,9 @@ function _getDisplaySetsFromSeries( StudyInstanceUID, SOPClassHandlerId, SOPClassUID, + instances, + // Others is a historical value used for instances which is deprecated and will be removed + others: instances, referencedImages: null, measurements: null, isDerivedDisplaySet: true, @@ -313,13 +321,13 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) { MeasurementGroups ); - let measurements = []; + const measurements = []; Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach( trackingUniqueIdentifier => { const mergedContentSequence = mergedContentSequencesByTrackingUniqueIdentifiers[ - trackingUniqueIdentifier + trackingUniqueIdentifier ]; const measurement = _processMeasurement(mergedContentSequence); @@ -359,7 +367,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers( if ( mergedContentSequencesByTrackingUniqueIdentifiers[ - trackingUniqueIdentifier + trackingUniqueIdentifier ] === undefined ) { // Add the full ContentSequence @@ -474,9 +482,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { const FindingSites = mergedContentSequence.filter( item => item.ConceptNameCodeSequence.CodingSchemeDesignator === - CodingSchemeDesignators.SRT && + CodingSchemeDesignators.SRT && item.ConceptNameCodeSequence.CodeValue === - CodeNameCodeSequenceValues.FindingSite + CodeNameCodeSequenceValues.FindingSite ); const measurement = { @@ -493,7 +501,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { Finding.ConceptCodeSequence.CodingSchemeDesignator ) && Finding.ConceptCodeSequence.CodeValue === - CodeNameCodeSequenceValues.CornerstoneFreeText + CodeNameCodeSequenceValues.CornerstoneFreeText ) { measurement.labels.push({ label: CORNERSTONE_FREETEXT_CODE_VALUE, @@ -509,7 +517,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { FindingSite.ConceptCodeSequence.CodingSchemeDesignator ) && FindingSite.ConceptCodeSequence.CodeValue === - CodeNameCodeSequenceValues.CornerstoneFreeText + CodeNameCodeSequenceValues.CornerstoneFreeText ); if (cornerstoneFreeTextFindingSite) { diff --git a/extensions/cornerstone-dicom-sr/src/init.js b/extensions/cornerstone-dicom-sr/src/init.js deleted file mode 100644 index 92a907ff8..000000000 --- a/extensions/cornerstone-dicom-sr/src/init.js +++ /dev/null @@ -1,29 +0,0 @@ -import { addTool, annotation } from '@cornerstonejs/tools'; -import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; -import SRLengthTool from './tools/tools/SRLength'; -import SRBidirectionalTool from './tools/tools/SRBidirectional'; -import SREllipticalROITool from './tools/tools/SREllipticalROI'; -import SRArrowAnnotateTool from './tools/tools/SRArrowAnnotate'; - -/** - * @param {object} configuration - */ -export default function init({ configuration = {} }) { - addTool(DICOMSRDisplayTool); - addTool(SRLengthTool); - addTool(SRBidirectionalTool); - addTool(SREllipticalROITool); - addTool(SRArrowAnnotateTool); - - // Modify annotation tools to use dashed lines on SR - const dashedLine = { - lineDash: '4,4', - }; - annotation.config.style.setToolGroupToolStyles('SRToolGroup', { - [SRLengthTool.toolName]: dashedLine, - [SRBidirectionalTool.toolName]: dashedLine, - [SREllipticalROITool.toolName]: dashedLine, - [SRArrowAnnotateTool.toolName]: dashedLine, - global: {}, - }); -} diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts new file mode 100644 index 000000000..ec24b5c48 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -0,0 +1,49 @@ +import { + addTool, + AngleTool, + annotation, + ArrowAnnotateTool, + BidirectionalTool, + CobbAngleTool, + EllipticalROITool, + LengthTool, + PlanarFreehandROITool, +} from '@cornerstonejs/tools'; +import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; +import addToolInstance from './utils/addToolInstance'; +import { Types } from '@ohif/core'; +import toolNames from './tools/toolNames'; + +/** + * @param {object} configuration + */ +export default function init({ + configuration = {}, +}: Types.Extensions.ExtensionParams): void { + addTool(DICOMSRDisplayTool); + addToolInstance(toolNames.SRLength, LengthTool, {}); + addToolInstance(toolNames.SRBidirectional, BidirectionalTool); + addToolInstance(toolNames.SREllipticalROI, EllipticalROITool); + addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool); + addToolInstance(toolNames.SRAngle, AngleTool); + // TODO - fix the SR display of Cobb Angle, as it joins the two lines + addToolInstance(toolNames.SRCobbAngle, CobbAngleTool); + // TODO - fix the rehydration of Freehand, as it throws an exception + // on a missing polyline. The fix is probably in CS3D + addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool); + + // Modify annotation tools to use dashed lines on SR + const dashedLine = { + lineDash: '4,4', + }; + annotation.config.style.setToolGroupToolStyles('SRToolGroup', { + SRLength: dashedLine, + SRBidirectional: dashedLine, + SREllipticalROI: dashedLine, + SRArrowAnnotate: dashedLine, + SRCobbAngle: dashedLine, + SRAngle: dashedLine, + SRPlanarFreehandROI: dashedLine, + global: {}, + }); +} diff --git a/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts index 5e714f831..48375f4cb 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts @@ -1,15 +1,15 @@ import DICOMSRDisplayTool from './DICOMSRDisplayTool'; -import SRLengthTool from './tools/SRLength'; -import SRBidirectional from './tools/SRBidirectional'; -import SREllipticalROI from './tools/SREllipticalROI'; -import SRArrowAnnotate from './tools/SRArrowAnnotate'; const toolNames = { DICOMSRDisplay: DICOMSRDisplayTool.toolName, - SRLength: SRLengthTool.toolName, - SRBidirectional: SRBidirectional.toolName, - SREllipticalROI: SREllipticalROI.toolName, - SRArrowAnnotate: SRArrowAnnotate.toolName, + SRLength: 'SRLength', + SRBidirectional: 'SRBidirectional', + SREllipticalROI: 'SREllipticalROI', + SRArrowAnnotate: 'SRArrowAnnotate', + SRAngle: 'SRAngle', + SRCobbAngle: 'SRCobbAngle', + SRRectangleROI: 'SRRectangleROI', + SRPlanarFreehandROI: 'SRPlanarFreehandROI', }; export default toolNames; diff --git a/extensions/cornerstone-dicom-sr/src/tools/tools/SRArrowAnnotate.ts b/extensions/cornerstone-dicom-sr/src/tools/tools/SRArrowAnnotate.ts deleted file mode 100644 index 2500a2dc0..000000000 --- a/extensions/cornerstone-dicom-sr/src/tools/tools/SRArrowAnnotate.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ArrowAnnotateTool } from '@cornerstonejs/tools'; - -/** - * The reason we are extending ArrowAnnotateTool is to create a new tool for SR - * viewport which basically has a different name. This is done since Cornerstone - * has shifted from creating tool instances for each annotation, and we have ArrowAnnotate - * mappers at the measurementService, so if we didn't do this, we would be mapping - * the SR annotation to the measurementService (since there is a ArrowAnnotateTool mapper), - * but with extending and renaming it, there is not mapper for SRArrowAnnotateTool; hence - * no mapping; hence no new measurement, just temporary ones for the SR viewport. - */ -class SRArrowAnnotateTool extends ArrowAnnotateTool { - static toolName = 'SRArrowAnnotate'; -} - -export default SRArrowAnnotateTool; diff --git a/extensions/cornerstone-dicom-sr/src/tools/tools/SRBidirectional.ts b/extensions/cornerstone-dicom-sr/src/tools/tools/SRBidirectional.ts deleted file mode 100644 index 2b121d0d0..000000000 --- a/extensions/cornerstone-dicom-sr/src/tools/tools/SRBidirectional.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BidirectionalTool } from '@cornerstonejs/tools'; - -/** - * The reason we are extending BidirectionalTool is to create a new tool for SR - * viewport which basically has a different name. This is done since Cornerstone - * has shifted from creating tool instances for each annotation, and we have Bidirectional - * mappers at the measurementService, so if we didn't do this, we would be mapping - * the SR annotation to the measurementService (since there is a BidirectionalTool mapper), - * but with extending and renaming it, there is not mapper for SRBidirectionalTool; hence - * no mapping; hence no new measurement, just temporary ones for the SR viewport. - */ -class SRBidirectional extends BidirectionalTool { - static toolName = 'SRBidirectional'; -} - -export default SRBidirectional; diff --git a/extensions/cornerstone-dicom-sr/src/tools/tools/SREllipticalROI.ts b/extensions/cornerstone-dicom-sr/src/tools/tools/SREllipticalROI.ts deleted file mode 100644 index 66d97da94..000000000 --- a/extensions/cornerstone-dicom-sr/src/tools/tools/SREllipticalROI.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { EllipticalROITool } from '@cornerstonejs/tools'; - -/** - * The reason we are extending EllipticalROITool is to create a new tool for SR - * viewport which basically has a different name. This is done since Cornerstone - * has shifted from creating tool instances for each annotation, and we have EllipticalROI - * mappers at the measurementService, so if we didn't do this, we would be mapping - * the SR annotation to the measurementService (since there is a EllipticalROITool mapper), - * but with extending and renaming it, there is not mapper for SREllipticalROITool; hence - * no mapping; hence no new measurement, just temporary ones for the SR viewport. - */ -class SREllipticalROI extends EllipticalROITool { - static toolName = 'SREllipticalROI'; -} - -export default SREllipticalROI; diff --git a/extensions/cornerstone-dicom-sr/src/tools/tools/SRLength.ts b/extensions/cornerstone-dicom-sr/src/tools/tools/SRLength.ts deleted file mode 100644 index cc3c44de9..000000000 --- a/extensions/cornerstone-dicom-sr/src/tools/tools/SRLength.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { LengthTool } from '@cornerstonejs/tools'; - -/** - * The reason we are extending LengthTool is to create a new tool for SR - * viewport which basically has a different name. This is done since Cornerstone - * has shifted from creating tool instances for each annotation, and we have Length - * mappers at the measurementService, so if we didn't do this, we would be mapping - * the SR annotation to the measurementService (since there is a LengthTool mapper), - * but with extending and renaming it, there is not mapper for SRLengthTool; hence - * no mapping; hence no new measurement, just temporary ones for the SR viewport. - */ -class SRLengthTool extends LengthTool { - static toolName = 'SRLength'; -} - -export default SRLengthTool; diff --git a/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts b/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts new file mode 100644 index 000000000..889926d81 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts @@ -0,0 +1,12 @@ +import { addTool } from '@cornerstonejs/tools'; + +export default function addToolInstance( + name: string, + toolClass, + configuration? +): void { + class InstanceClass extends toolClass { + static toolName = name; + } + addTool(InstanceClass); +} diff --git a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js index 3474e3b11..41602dd25 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js +++ b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js @@ -1,6 +1,8 @@ import { adaptersSR } from '@cornerstonejs/adapters'; -const cornerstoneAdapters = adaptersSR.Cornerstone3D; +const cornerstoneAdapters = + adaptersSR.Cornerstone3D.MeasurementReport + .CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG; @@ -37,7 +39,7 @@ export default function isRehydratable(displaySet, mappings) { }); for (let i = 0; i < measurements.length; i++) { - const TrackingIdentifier = measurements[i].TrackingIdentifier; + const { TrackingIdentifier } = measurements[i] || {}; const hydratable = adapters.some(adapter => { let [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) { @@ -54,7 +56,13 @@ export default function isRehydratable(displaySet, mappings) { if (hydratable) { return true; } + console.log( + 'Measurement is not rehydratable', + TrackingIdentifier, + measurements[i] + ); } + console.log('No measurements found which were rehydratable'); return false; } diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index 8c788e5b9..cb13803d7 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -284,7 +284,7 @@ function OHIFCornerstoneSRViewport(props) { * isHydratable check, the outcome for the isHydrated state here is always FALSE * since we don't do the hydration here. Todo: can't we just set it as false? why * we are changing the state here? isHydrated is always false at this stage, and - * if it is hydrated we don't event use the SR viewport. + * if it is hydrated we don't even use the SR viewport. */ useEffect(() => { if (!srDisplaySet.isLoaded) { diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 0518ba474..7049b4cde 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -30,7 +30,7 @@ "@ohif/core": "^3.0.0", "@ohif/ui": "^2.0.0", "cornerstone-wado-image-loader": "^4.2.1", - "dcmjs": "0.29.4", + "dcmjs": "^0.29.4", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", @@ -43,10 +43,10 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "0.3.0", - "@cornerstonejs/core": "0.30.1", - "@cornerstonejs/streaming-image-volume-loader": "^0.11.2", - "@cornerstonejs/tools": "0.39.0", + "@cornerstonejs/adapters": "^0.3.1", + "@cornerstonejs/core": "^0.30.1", + "@cornerstonejs/streaming-image-volume-loader": "^0.8.2", + "@cornerstonejs/tools": "^0.44.0", "@kitware/vtk.js": "26.4.0", "html2canvas": "^1.4.1", "lodash.debounce": "4.0.8", diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index a3ebc57c2..5259b267c 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -25,6 +25,7 @@ import { registerColormap } from './utils/colormap/transferFunctionHelpers'; import { id } from './id'; import * as csWADOImageLoader from './initWADOImageLoader.js'; +import { measurementMappingUtils } from './utils/measurementServiceMappings'; const Component = React.lazy(() => { return import( @@ -62,10 +63,9 @@ const cornerstoneExtension: Types.Extensions.Extension = { }, /** + * Register the Cornerstone 3D services and set them up for use. * - * - * @param {object} [configuration={}] - * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` + * @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools` */ async preRegistration({ servicesManager, @@ -151,3 +151,4 @@ const cornerstoneExtension: Types.Extensions.Extension = { }; export default cornerstoneExtension; +export { measurementMappingUtils }; diff --git a/extensions/cornerstone/src/initCornerstoneTools.js b/extensions/cornerstone/src/initCornerstoneTools.js index 635b7b77e..80dc0678b 100644 --- a/extensions/cornerstone/src/initCornerstoneTools.js +++ b/extensions/cornerstone/src/initCornerstoneTools.js @@ -14,6 +14,8 @@ import { DragProbeTool, ProbeTool, AngleTool, + CobbAngleTool, + PlanarFreehandROITool, MagnifyTool, CrosshairsTool, SegmentationDisplayTool, @@ -40,6 +42,8 @@ export default function initCornerstoneTools(configuration = {}) { addTool(ArrowAnnotateTool); addTool(DragProbeTool); addTool(AngleTool); + addTool(CobbAngleTool); + addTool(PlanarFreehandROITool); addTool(MagnifyTool); addTool(CrosshairsTool); addTool(SegmentationDisplayTool); @@ -76,6 +80,8 @@ const toolNames = { EllipticalROI: EllipticalROITool.toolName, Bidirectional: BidirectionalTool.toolName, Angle: AngleTool.toolName, + CobbAngle: CobbAngleTool.toolName, + PlanarFreehandROI: PlanarFreehandROITool.toolName, Magnify: MagnifyTool.toolName, Crosshairs: CrosshairsTool.toolName, SegmentationDisplay: SegmentationDisplayTool.toolName, diff --git a/extensions/cornerstone/src/initMeasurementService.js b/extensions/cornerstone/src/initMeasurementService.js index 8a0d492bc..9ce6fc5c7 100644 --- a/extensions/cornerstone/src/initMeasurementService.js +++ b/extensions/cornerstone/src/initMeasurementService.js @@ -1,6 +1,6 @@ import { eventTarget } from '@cornerstonejs/core'; import { Enums, annotation } from '@cornerstonejs/tools'; -import { DicomMetadataStore } from '@ohif/core'; +import { DicomMetadataStore, MeasurementService } from '@ohif/core'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; @@ -23,6 +23,10 @@ const initMeasurementService = ( Bidirectional, EllipticalROI, ArrowAnnotate, + Angle, + CobbAngle, + RectangleROI, + PlanarFreehandROI, } = measurementServiceMappingsFactory( measurementService, displaySetService, @@ -66,6 +70,38 @@ const initMeasurementService = ( ArrowAnnotate.toMeasurement ); + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'CobbAngle', + CobbAngle.matchingCriteria, + CobbAngle.toAnnotation, + CobbAngle.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'Angle', + Angle.matchingCriteria, + Angle.toAnnotation, + Angle.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'RectangleROI', + RectangleROI.matchingCriteria, + RectangleROI.toAnnotation, + RectangleROI.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'PlanarFreehandROI', + PlanarFreehandROI.matchingCriteria, + PlanarFreehandROI.toAnnotation, + PlanarFreehandROI.toMeasurement + ); + return csTools3DVer1MeasurementSource; }; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts new file mode 100644 index 000000000..dfbe9aa62 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts @@ -0,0 +1,210 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; + +const Angle = { + toAnnotation: measurement => { }, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType + ) => { + const { annotation, viewportId } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes( + referencedImageId, + CornerstoneViewportService, + viewportId + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + const { points } = data.handles; + + const mappedAnnotations = getMappedAnnotations( + annotation, + displaySetService + ); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + frameNumber: mappedAnnotations?.[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, DisplaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + if (!referencedImageId) { + throw new Error( + 'Non-acquisition plane measurement mapping not supported' + ); + } + + const { + SOPInstanceUID, + SeriesInstanceUID, + frameNumber, + } = getSOPInstanceAttributes(referencedImageId); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID, + frameNumber + ); + + const { SeriesNumber } = displaySet; + const { angle } = targetStats; + const unit = '\u00B0'; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + angle, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:Angle'); + + mappedAnnotations.forEach(annotation => { + const { angle, unit } = annotation; + columns.push(`Angle (${unit})`); + values.push(angle); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + if (!mappedAnnotations || !mappedAnnotations.length) { + return ''; + } + + const displayText = []; + + // Area is the same for all series + const { + angle, + unit, + SeriesNumber, + SOPInstanceUID, + frameNumber, + } = mappedAnnotations[0]; + + const instance = displaySet.images.find( + image => image.SOPInstanceUID === SOPInstanceUID + ); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + if (angle === undefined) return displayText; + const roundedAngle = utils.roundNumber(angle, 2); + displayText.push( + `${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})` + ); + + return displayText; +} + +export default Angle; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts new file mode 100644 index 000000000..53dace2b4 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts @@ -0,0 +1,210 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; + +const CobbAngle = { + toAnnotation: measurement => { }, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType + ) => { + const { annotation, viewportId } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('Cobb Angle tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes( + referencedImageId, + CornerstoneViewportService, + viewportId + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + const { points } = data.handles; + + const mappedAnnotations = getMappedAnnotations( + annotation, + displaySetService + ); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + frameNumber: mappedAnnotations?.[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, DisplaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + if (!referencedImageId) { + throw new Error( + 'Non-acquisition plane measurement mapping not supported' + ); + } + + const { + SOPInstanceUID, + SeriesInstanceUID, + frameNumber, + } = getSOPInstanceAttributes(referencedImageId); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID, + frameNumber + ); + + const { SeriesNumber } = displaySet; + const { angle } = targetStats; + const unit = '\u00B0'; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + angle, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:CobbAngle'); + + mappedAnnotations.forEach(annotation => { + const { angle, unit } = annotation; + columns.push(`Angle (${unit})`); + values.push(angle); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + if (!mappedAnnotations || !mappedAnnotations.length) { + return ''; + } + + const displayText = []; + + // Area is the same for all series + const { + angle, + unit, + SeriesNumber, + SOPInstanceUID, + frameNumber, + } = mappedAnnotations[0]; + + const instance = displaySet.images.find( + image => image.SOPInstanceUID === SOPInstanceUID + ); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + if (angle === undefined) return displayText; + const roundedAngle = utils.roundNumber(angle, 2); + displayText.push( + `${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})` + ); + + return displayText; +} + +export default CobbAngle; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts new file mode 100644 index 000000000..71ded00c1 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts @@ -0,0 +1,148 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const PlanarFreehandROI = { + toAnnotation: measurement => { }, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + DisplaySetService, + CornerstoneViewportService, + getValueTypeFromToolType + ) => { + const { annotation, viewportId } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('PlanarFreehandROI tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes( + referencedImageId, + CornerstoneViewportService, + viewportId + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + const { points } = data.handles; + + const mappedAnnotations = getMappedAnnotations( + annotation, + DisplaySetService + ); + + const displayText = getDisplayText(mappedAnnotations); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: { ...data, ...data.cachedStats }, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +/** + * It maps an imaging library annotation to a list of simplified annotation properties. + * + * @param {Object} annotationData + * @param {Object} DisplaySetService + * @returns + */ +function getMappedAnnotations(annotationData, DisplaySetService) { + const { metadata, data } = annotationData; + const { label } = data; + const { referencedImageId } = metadata; + + const annotations = []; + + const { + SOPInstanceUID: _SOPInstanceUID, + SeriesInstanceUID: _SeriesInstanceUID, + } = getSOPInstanceAttributes(referencedImageId) || {}; + + if (!_SOPInstanceUID || !_SeriesInstanceUID) { + return annotations; + } + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + _SOPInstanceUID, + _SeriesInstanceUID + ); + + const { SeriesNumber, SeriesInstanceUID } = displaySet; + + annotations.push({ + SeriesInstanceUID, + SeriesNumber, + label, + data, + }); + + return annotations; +} + +/** + * TBD + * This function is used to convert the measurement data to a format that is suitable for the report generation (e.g. for the csv report). + * The report returns a list of columns and corresponding values. + * @param {*} mappedAnnotations + * @param {*} points + * @param {*} FrameOfReferenceUID + * @returns Object representing the report's content for this tool. + */ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { + const columns = []; + const values = []; + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations) { + return ''; +} + +export default PlanarFreehandROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts new file mode 100644 index 000000000..7560dc137 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts @@ -0,0 +1,228 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import getModalityUnit from './utils/getModalityUnit'; +import { utils } from '@ohif/core'; + +const RectangleROI = { + toAnnotation: measurement => { }, + toMeasurement: ( + csToolsEventDetail, + DisplaySetService, + CornerstoneViewportService, + getValueTypeFromToolType + ) => { + const { annotation, viewportId } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('Rectangle ROI tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes( + referencedImageId, + CornerstoneViewportService, + viewportId + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + const { points } = data.handles; + + const mappedAnnotations = getMappedAnnotations( + annotation, + DisplaySetService + ); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, DisplaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + if (!referencedImageId) { + // Todo: Non-acquisition plane measurement mapping not supported yet + throw new Error( + 'Non-acquisition plane measurement mapping not supported' + ); + } + + const { + SOPInstanceUID, + SeriesInstanceUID, + frameNumber, + } = getSOPInstanceAttributes(referencedImageId); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID, + frameNumber + ); + + const { SeriesNumber } = displaySet; + const { mean, stdDev, max, area, Modality } = targetStats; + const unit = getModalityUnit(Modality); + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + Modality, + unit, + mean, + stdDev, + max, + area, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:EllipticalROI'); + + mappedAnnotations.forEach(annotation => { + const { mean, stdDev, max, area, unit } = annotation; + + if (!mean || !unit || !max || !area) { + return; + } + + columns.push( + `max (${unit})`, + `mean (${unit})`, + `std (${unit})`, + `area (mm2)` + ); + values.push(max, mean, stdDev, area); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + if (!mappedAnnotations || !mappedAnnotations.length) { + return ''; + } + + const displayText = []; + + // Area is the same for all series + const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + + const instance = displaySet.images.find( + image => image.SOPInstanceUID === SOPInstanceUID + ); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + 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`); + + // Todo: we need a better UI for displaying all these information + mappedAnnotations.forEach(mappedAnnotation => { + const { unit, max, SeriesNumber } = mappedAnnotation; + + let maxStr = ''; + if (max) { + const roundedMax = utils.roundNumber(max, 2); + maxStr = `Max: ${roundedMax} ${unit} `; + } + + const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`; + if (!displayText.includes(str)) { + displayText.push(str); + } + }); + + return displayText; +} + +export default RectangleROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js index ed45a00c9..00be9ae14 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -1 +1,11 @@ -export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate']; +export default [ + 'Length', + 'EllipticalROI', + 'Bidirectional', + 'ArrowAnnotate', + 'Angle', + 'CobbAngle', + 'Probe', + 'RectangleROI', + 'PlanarFreehandROI', +]; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/index.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/index.ts new file mode 100644 index 000000000..795c744b8 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/index.ts @@ -0,0 +1,3 @@ +import * as measurementMappingUtils from './utils'; + +export { measurementMappingUtils }; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts similarity index 52% rename from extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js rename to extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts index 1e2f91daf..4d5c77f17 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts @@ -1,19 +1,24 @@ +import { MeasurementService } from '@ohif/core'; import Length from './Length'; import Bidirectional from './Bidirectional'; import EllipticalROI from './EllipticalROI'; import ArrowAnnotate from './ArrowAnnotate'; +import CobbAngle from './CobbAngle'; +import Angle from './Angle'; +import PlanarFreehandROI from './PlanarFreehandROI'; +import RectangleROI from './RectangleROI'; const measurementServiceMappingsFactory = ( - measurementService, + measurementService: MeasurementService, displaySetService, cornerstoneViewportService ) => { /** * Maps measurement service format object to cornerstone annotation object. * - * @param {Measurement} measurement The measurement instance - * @param {string} definition The source definition - * @return {Object} Cornerstone annotation data + * @param measurement The measurement instance + * @param definition The source definition + * @return Cornerstone annotation data */ const _getValueTypeFromToolType = toolType => { @@ -23,7 +28,8 @@ const measurementServiceMappingsFactory = ( RECTANGLE, BIDIRECTIONAL, POINT, - } = measurementService.VALUE_TYPES; + ANGLE, + } = MeasurementService.VALUE_TYPES; // TODO -> I get why this was attempted, but its not nearly flexible enough. // A single measurement may have an ellipse + a bidirectional measurement, for instances. @@ -32,14 +38,17 @@ const measurementServiceMappingsFactory = ( Length: POLYLINE, EllipticalROI: ELLIPSE, RectangleROI: RECTANGLE, + PlanarFreehandROI: POLYLINE, Bidirectional: BIDIRECTIONAL, ArrowAnnotate: POINT, + CobbAngle: ANGLE, + Angle: ANGLE, }; return TOOL_TYPE_TO_VALUE_TYPE[toolType]; }; - return { + const factories = { Length: { toAnnotation: Length.toAnnotation, toMeasurement: csToolsAnnotation => @@ -51,7 +60,7 @@ const measurementServiceMappingsFactory = ( ), matchingCriteria: [ { - valueType: measurementService.VALUE_TYPES.POLYLINE, + valueType: MeasurementService.VALUE_TYPES.POLYLINE, points: 2, }, ], @@ -69,15 +78,16 @@ const measurementServiceMappingsFactory = ( // TODO -> We should eventually do something like shortAxis + longAxis, // But its still a little unclear how these automatic interpretations will work. { - valueType: measurementService.VALUE_TYPES.POLYLINE, + valueType: MeasurementService.VALUE_TYPES.POLYLINE, points: 2, }, { - valueType: measurementService.VALUE_TYPES.POLYLINE, + valueType: MeasurementService.VALUE_TYPES.POLYLINE, points: 2, }, ], }, + EllipticalROI: { toAnnotation: EllipticalROI.toAnnotation, toMeasurement: csToolsAnnotation => @@ -89,10 +99,43 @@ const measurementServiceMappingsFactory = ( ), matchingCriteria: [ { - valueType: measurementService.VALUE_TYPES.ELLIPSE, + valueType: MeasurementService.VALUE_TYPES.ELLIPSE, }, ], }, + + RectangleROI: { + toAnnotation: RectangleROI.toAnnotation, + toMeasurement: csToolsAnnotation => + RectangleROI.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + }, + ], + }, + + PlanarFreehandROI: { + toAnnotation: PlanarFreehandROI.toAnnotation, + toMeasurement: csToolsAnnotation => + PlanarFreehandROI.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + }, + ], + }, + ArrowAnnotate: { toAnnotation: ArrowAnnotate.toAnnotation, toMeasurement: csToolsAnnotation => @@ -104,12 +147,46 @@ const measurementServiceMappingsFactory = ( ), matchingCriteria: [ { - valueType: measurementService.VALUE_TYPES.POINT, + valueType: MeasurementService.VALUE_TYPES.POINT, points: 1, }, ], }, + + CobbAngle: { + toAnnotation: CobbAngle.toAnnotation, + toMeasurement: csToolsAnnotation => + CobbAngle.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.ANGLE, + }, + ], + }, + + Angle: { + toAnnotation: Angle.toAnnotation, + toMeasurement: csToolsAnnotation => + Angle.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.ANGLE, + }, + ], + }, }; + + return factories; }; export default measurementServiceMappingsFactory; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/index.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/index.ts new file mode 100644 index 000000000..c31392196 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/index.ts @@ -0,0 +1,17 @@ +import getHandlesFromPoints from './getHandlesFromPoints'; +import { + isAnnotationSelected, + setAnnotationSelected, + getFirstAnnotationSelected, +} from './selection'; +import getModalityUnit from './getModalityUnit'; +import getSOPInstanceAttributes from './getSOPInstanceAttributes'; + +export { + getModalityUnit, + getHandlesFromPoints, + getSOPInstanceAttributes, + isAnnotationSelected, + setAnnotationSelected, + getFirstAnnotationSelected, +}; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/selection.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/selection.ts new file mode 100644 index 000000000..c70bf832e --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/selection.ts @@ -0,0 +1,44 @@ +import { annotation as cs3dToolAnnotationUtils } from '@cornerstonejs/tools'; + +/** + * Check whether an annotation from imaging library is selected or not. + * @param {string} annotationUID uid of imaging library annotation + * @returns boolean + */ +function isAnnotationSelected(annotationUID: string): boolean { + return cs3dToolAnnotationUtils.selection.isAnnotationSelected(annotationUID); +} + +/** + * Change an annotation from imaging library's selected property. + * @param annotationUID - uid of imaging library annotation + * @param selected - new value for selected + */ +function setAnnotationSelected(annotationUID: string, selected: boolean): void { + const isCurrentSelected = isAnnotationSelected(annotationUID); + // branch cut, avoid invoking imaging library unnecessarily. + if (isCurrentSelected !== selected) { + cs3dToolAnnotationUtils.selection.setAnnotationSelected( + annotationUID, + selected + ); + } +} + +function getFirstAnnotationSelected(element) { + const [selectedAnnotationUID] = + cs3dToolAnnotationUtils.selection.getAnnotationsSelected() || []; + + if (selectedAnnotationUID) { + return cs3dToolAnnotationUtils.state.getAnnotation( + selectedAnnotationUID, + element + ); + } +} + +export { + isAnnotationSelected, + setAnnotationSelected, + getFirstAnnotationSelected, +}; diff --git a/extensions/default/package.json b/extensions/default/package.json index 8c15e721a..80332ef34 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -32,8 +32,8 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "@ohif/i18n": "^1.0.0", - "dcmjs": "0.29.4", - "dicomweb-client": "^0.6.0", + "dcmjs": "^0.29.4", + "dicomweb-client": "^0.8.4", "prop-types": "^15.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx index be95aa0b2..c2314bf08 100644 --- a/extensions/default/src/Panels/PanelMeasurementTable.tsx +++ b/extensions/default/src/Panels/PanelMeasurementTable.tsx @@ -1,15 +1,15 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; +import { utils, ServicesManager } from '@ohif/core'; import { MeasurementTable, Dialog, Input, useViewportGrid } from '@ohif/ui'; import ActionButtons from './ActionButtons'; import debounce from 'lodash.debounce'; -import { utils } from '@ohif/core'; import createReportDialogPrompt, { CREATE_REPORT_DIALOG_RESPONSE, } from './createReportDialogPrompt'; import createReportAsync from '../Actions/createReportAsync'; -import getNextSRSeriesNumber from '../utils/getNextSRSeriesNumber'; +import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription'; const { downloadCSVReport } = utils; @@ -17,7 +17,7 @@ export default function PanelMeasurementTable({ servicesManager, commandsManager, extensionManager, -}) { +}): React.FunctionComponent { const [viewportGrid, viewportGridService] = useViewportGrid(); const { activeViewportIndex, viewports } = viewportGrid; const { @@ -25,7 +25,7 @@ export default function PanelMeasurementTable({ uiDialogService, uiNotificationService, displaySetService, - } = servicesManager.services; + } = (servicesManager as ServicesManager).services; const [displayMeasurements, setDisplayMeasurements] = useState([]); useEffect(() => { @@ -72,7 +72,7 @@ export default function PanelMeasurementTable({ measurementService.clearMeasurements(); } - async function createReport() { + async function createReport(): Promise { // filter measurements that are added to the active study const activeViewport = viewports[activeViewportIndex]; const measurements = measurementService.getMeasurements(); @@ -109,17 +109,19 @@ export default function PanelMeasurementTable({ ? 'Research Derived Series' // default : promptResult.value; // provided value - const SeriesNumber = getNextSRSeriesNumber(displaySetService); + // Re-use an existing series having the same series description to avoid + // creating too many series instances. + const options = findSRWithSameSeriesDescription( + SeriesDescription, + displaySetService + ); - const displaySetInstanceUIDs = await createReportAsync( + return createReportAsync( servicesManager, commandsManager, dataSource, trackedMeasurements, - { - SeriesDescription, - SeriesNumber, - } + options ); } } @@ -233,16 +235,7 @@ export default function PanelMeasurementTable({ } PanelMeasurementTable.propTypes = { - servicesManager: PropTypes.shape({ - services: PropTypes.shape({ - measurementService: PropTypes.shape({ - getMeasurements: PropTypes.func.isRequired, - subscribe: PropTypes.func.isRequired, - EVENTS: PropTypes.object.isRequired, - VALUE_TYPES: PropTypes.object.isRequired, - }).isRequired, - }).isRequired, - }).isRequired, + servicesManager: PropTypes.instanceOf(ServicesManager).isRequired, }; function _getMappedMeasurements(measurementService) { diff --git a/extensions/default/src/utils/findSRWithSameSeriesDescription.ts b/extensions/default/src/utils/findSRWithSameSeriesDescription.ts new file mode 100644 index 000000000..476343fb5 --- /dev/null +++ b/extensions/default/src/utils/findSRWithSameSeriesDescription.ts @@ -0,0 +1,49 @@ +import { DisplaySetService, Types } from '@ohif/core'; + +import getNextSRSeriesNumber from './getNextSRSeriesNumber'; + +/** + * Find an SR having the same series description. + * This is used by the store service in order to store DICOM SR's having the + * same Series Description into a single series under consecutive instance numbers + * That way, they are all organized as a set and could have tools to view + * "prior" SR instances. + * + * @param SeriesDescription - is the description to look for + * @param displaySetService - the display sets to search for DICOM SR in + * @returns SeriesMetadata from a DICOM SR having the same series description + */ +export default function findSRWithSameSeriesDescription( + SeriesDescription: string, + displaySetService: DisplaySetService +): Types.SeriesMetadata { + const activeDisplaySets = displaySetService.getActiveDisplaySets(); + const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR'); + const sameSeries = srDisplaySets.find( + ds => ds.SeriesDescription === SeriesDescription + ); + if (sameSeries) { + console.log('Storing to same series', sameSeries); + const { instance } = sameSeries; + const { + SeriesInstanceUID, + SeriesDescription, + SeriesDate, + SeriesTime, + SeriesNumber, + Modality, + } = instance; + return { + SeriesInstanceUID, + SeriesDescription, + SeriesDate, + SeriesTime, + SeriesNumber, + Modality, + InstanceNumber: sameSeries.others.length + 1, + }; + } + + const SeriesNumber = getNextSRSeriesNumber(displaySetService); + return { SeriesDescription, SeriesNumber }; +} diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json index 50aa40246..85684879c 100644 --- a/extensions/dicom-pdf/package.json +++ b/extensions/dicom-pdf/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "@ohif/ui": "^2.0.0", - "dcmjs": "0.29.4", + "dcmjs": "^0.29.4", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json index a03edeeeb..83f4e060b 100644 --- a/extensions/dicom-video/package.json +++ b/extensions/dicom-video/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "@ohif/ui": "^2.0.0", - "dcmjs": "0.29.4", + "dcmjs": "^0.29.4", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 89ef83c8a..86f91e615 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,10 +32,10 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "classnames": "^2.3.2", - "@cornerstonejs/core": "0.30.1", - "@cornerstonejs/tools": "0.39.0", + "@cornerstonejs/core": "^0.30.1", + "@cornerstonejs/tools": "^0.44.0", "@ohif/extension-cornerstone-dicom-sr": "^3.0.0", - "dcmjs": "0.29.4", + "dcmjs": "^0.29.4", "prop-types": "^15.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/extensions/tmtv/package.json b/extensions/tmtv/package.json index f5b5a0255..9210d7cfa 100644 --- a/extensions/tmtv/package.json +++ b/extensions/tmtv/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "@ohif/ui": "^2.0.0", - "dcmjs": "0.29.4", + "dcmjs": "^0.29.4", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js index 105b1a2f8..a5af64e77 100644 --- a/modes/longitudinal/src/initToolGroups.js +++ b/modes/longitudinal/src/initToolGroups.js @@ -35,6 +35,8 @@ function initDefaultToolGroup( { toolName: toolNames.RectangleROI }, { toolName: toolNames.StackScroll }, { toolName: toolNames.Angle }, + { toolName: toolNames.CobbAngle }, + { toolName: toolNames.PlanarFreehandROI }, { toolName: toolNames.Magnify }, { toolName: toolNames.SegmentationDisplay }, ], @@ -173,6 +175,8 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { { toolName: toolNames.RectangleROI }, { toolName: toolNames.StackScroll }, { toolName: toolNames.Angle }, + { toolName: toolNames.CobbAngle }, + { toolName: toolNames.PlanarFreehandROI }, { toolName: toolNames.SegmentationDisplay }, ], disabled: [ diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index a8c8a8535..e62fc2f2e 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -493,6 +493,38 @@ const toolbarButtons = [ ], 'Angle' ), + + // Next two tools can be added once icons are added + // _createToolButton( + // 'Cobb Angle', + // 'tool-cobb-angle', + // 'Cobb Angle', + // [ + // { + // commandName: 'setToolActive', + // commandOptions: { + // toolName: 'CobbAngle', + // }, + // context: 'CORNERSTONE', + // }, + // ], + // 'Cobb Angle' + // ), + // _createToolButton( + // 'Planar Freehand ROI', + // 'tool-freehand', + // 'PlanarFreehandROI', + // [ + // { + // commandName: 'setToolActive', + // commandOptions: { + // toolName: 'PlanarFreehandROI', + // }, + // context: 'CORNERSTONE', + // }, + // ], + // 'Planar Freehand ROI' + // ), _createToolButton( 'Magnify', 'tool-magnify', diff --git a/modes/tmtv/src/initToolGroups.js b/modes/tmtv/src/initToolGroups.js index 21e14e126..990b70058 100644 --- a/modes/tmtv/src/initToolGroups.js +++ b/modes/tmtv/src/initToolGroups.js @@ -34,6 +34,7 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager) { { toolName: toolNames.RectangleROI }, { toolName: toolNames.StackScroll }, { toolName: toolNames.Angle }, + { toolName: toolNames.CobbAngle }, { toolName: toolNames.Magnify }, ], enabled: [{ toolName: toolNames.SegmentationDisplay }], @@ -149,6 +150,7 @@ function initMPRToolGroup(toolNames, Enums, toolGroupService, commandsManager) { { toolName: toolNames.RectangleROI }, { toolName: toolNames.StackScroll }, { toolName: toolNames.Angle }, + { toolName: toolNames.CobbAngle }, { toolName: toolNames.SegmentationDisplay }, ], disabled: [{ toolName: toolNames.Crosshairs }], diff --git a/platform/core/package.json b/platform/core/package.json index 486fcc36d..f2b11412d 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -37,8 +37,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "dcmjs": "0.29.4", - "dicomweb-client": "^0.6.0", + "dcmjs": "^0.29.4", + "dicomweb-client": "^0.8.4", "isomorphic-base64": "^1.0.2", "lodash.merge": "^4.6.1", "lodash.clonedeep": "^4.5.0", diff --git a/platform/core/src/measurements/tools/polygonRoi.js b/platform/core/src/measurements/tools/polygonRoi.js index 4ebdc4d97..05876e581 100644 --- a/platform/core/src/measurements/tools/polygonRoi.js +++ b/platform/core/src/measurements/tools/polygonRoi.js @@ -11,7 +11,7 @@ export const polygonRoi = { id: 'PolygonRoi', name: 'Polygon', toolGroup: 'allTools', - cornerstoneToolType: 'FreehandRoiTool', + cornerstoneToolType: 'PlanarFreehandROITool', options: { measurementTable: { displayFunction, diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 64cb53561..1fb9550f2 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -71,6 +71,7 @@ const EVENTS = { }; const VALUE_TYPES = { + ANGLE: 'value_type::polyline', POLYLINE: 'value_type::polyline', POINT: 'value_type::point', BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here. @@ -103,18 +104,15 @@ class MeasurementService extends PubSubService { }, }; + public static VALUE_TYPES = VALUE_TYPES; + public readonly VALUE_TYPES = VALUE_TYPES; + constructor() { super(EVENTS); this.sources = {}; this.mappings = {}; this.measurements = {}; this._jumpToMeasurementCache = {}; - Object.defineProperty(this, 'VALUE_TYPES', { - value: VALUE_TYPES, - writable: false, - enumerable: true, - configurable: false, - }); } /** @@ -529,6 +527,7 @@ class MeasurementService extends PubSubService { measurement = toMeasurementSchema(sourceAnnotationDetail); measurement.source = source; } catch (error) { + console.log('Failed to map', error); throw new Error( `Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}` ); diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 235a838db..b7fe69b1e 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -66,8 +66,7 @@ "core-js": "^3.16.1", "cornerstone-math": "^0.1.9", "cornerstone-wado-image-loader": "^4.2.1", - "dayjs": "^1.11.6", - "dcmjs": "0.29.4", + "dcmjs": "^0.29.4", "detect-gpu": "^4.0.16", "dicom-parser": "^1.8.9", "dotenv-webpack": "^1.7.0", diff --git a/testdata b/testdata index 4d59660c2..1bca96aa2 160000 --- a/testdata +++ b/testdata @@ -1 +1 @@ -Subproject commit 4d59660c2883ed749a680e5fb6d4624ab54c9422 +Subproject commit 1bca96aa2aeae7d90b3c8db73845c85dd27b34d5 diff --git a/yarn.lock b/yarn.lock index 8f2187364..434106df8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1407,10 +1407,10 @@ resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cornerstonejs/adapters@0.3.0": - version "0.3.0" - resolved "https://registry.npmjs.org/@cornerstonejs/adapters/-/adapters-0.3.0.tgz#c0a2a904c92ebfae77520eba4fc64c07ebd06db4" - integrity sha512-o3GIrccNM0q/QF0//ueVIXeRltUmtTBkTR47qPgIY9gdi+3sYEFHEW6RTXe3OOtDDXOWgM88Jp3mWWUK4ZeXxg== +"@cornerstonejs/adapters@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-0.3.1.tgz#50cfaf3de3c5a55f22f7fae1a9f6584a1c343ffa" + integrity sha512-+tqMLVYeSZjzwtzbAPqRMq5iI47nrtXz4f3GQ1jKUSmZqQyvgWRzAizcfnl6OEIrgjeg/2MHdSGOEWu12H8M+w== dependencies: "@babel/runtime-corejs2" "^7.17.8" dcmjs "^0.29.4" @@ -1443,7 +1443,15 @@ resolved "https://registry.npmjs.org/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.2.tgz#e96721d56f6ec96f7f95c16321d88cc8467d8d81" integrity sha512-lgdvBvvNezleY+4pIe2ceUsJzlZe/0PipdeubQ3vZZOz3xxtHHMR1XFCl4fgd8gosR8COHuD7h6q+MwgrwBsng== -"@cornerstonejs/core@0.30.1", "@cornerstonejs/core@^0.30.1": +"@cornerstonejs/core@^0.25.1": + version "0.25.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.25.1.tgz#7e5c70858018d99417b61965ac606754542f77ad" + integrity sha512-/Ve9qwGyRRK1uUCrefh9cVG3OIaaS9LQoTUk9YMUbsoWxd6JitXh31XVHjlvNtBYGMRVdz+uGPS2zSxq1vi4aA== + dependencies: + detect-gpu "^4.0.45" + lodash.clonedeep "4.5.0" + +"@cornerstonejs/core@^0.30.1": version "0.30.1" resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.30.1.tgz#6cb7088260641e7e55de2edb11979b1a0c00c794" integrity sha512-A7t6WtPut17uLGpv3cUfk1gSzEvWiFcCg1K3H0WGySdZsf4tIinb5xA+9oD2eaBPbvTXmIef4HWFIAsN3kAEgg== @@ -1451,28 +1459,28 @@ detect-gpu "^4.0.45" lodash.clonedeep "4.5.0" -"@cornerstonejs/core@^0.28.0": - version "0.28.0" - resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.28.0.tgz#bc5114dfd332fcc5caf4ab049bcb08f6457e5f97" - integrity sha512-DSAdH2+8h/rRCTR6WbbGkY+vqnAVVZH47+GgGQQHscEwDD/K3ZO62JMl4LgcgkDzI0LIIZAk6CgAtok31yG/3Q== +"@cornerstonejs/core@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.31.0.tgz#087731cc9c1745f02e8936a55956c4ca5d52f3d9" + integrity sha512-qJmR2JpPPuSJsIDW7seH+bJrhE/e5QNwuk2T8EzlWtHR3QcB2lSq/o7cX6kGOJDwn4KrE1p/9QZAJ4GaFlBoyQ== dependencies: detect-gpu "^4.0.45" lodash.clonedeep "4.5.0" -"@cornerstonejs/streaming-image-volume-loader@^0.11.2": - version "0.11.8" - resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.11.8.tgz#79dc9bf7f00a3277d9880f919c324cdeda2c28d6" - integrity sha512-fp+mmCUMWQQFzx9AzzmOJO1ntBStHQmxK3Zbi/sMs9TYcue+Zw14oA+qcuJ/7UZayb+c99unfWjhafjBiHWNeg== +"@cornerstonejs/streaming-image-volume-loader@^0.8.2": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.8.3.tgz#9370f1a609c5d83b3be59af2a0f44c57f68efdb0" + integrity sha512-p4kQisqQM/hD5yr/TZKSlq2Ux6eOJteuJrJRNaub46bGklU5M2J0TPShEkwpMDPIJeir9hNeQyg7q8fy5IoH+A== dependencies: - "@cornerstonejs/core" "^0.30.1" - cornerstone-wado-image-loader "^4.8.0" + "@cornerstonejs/core" "^0.25.1" + cornerstone-wado-image-loader "^4.7.0" -"@cornerstonejs/tools@0.39.0": - version "0.39.0" - resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.39.0.tgz#7d9bac4c55bc19145c2051f7c7150b34570849e4" - integrity sha512-xDR51lOEuNIDaTJltZvxDK+BIdIbznXUsWt+OpYiiygCQYt1Lt2eHufO2wVFVPrMgvm0dryF+WQbRVwtWvx4yA== +"@cornerstonejs/tools@^0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.44.0.tgz#7bec3dbcd25fce648cab49021c67ca2e3a80b1f1" + integrity sha512-/LfxjXwR4o1qzsByk4pf3vgRK+4BJGkth1G+dqYLj06iUsDYX724kR6AodEFZniwXUiSCZO7JDYOyns6fk7+GQ== dependencies: - "@cornerstonejs/core" "^0.28.0" + "@cornerstonejs/core" "^0.31.0" lodash.clonedeep "4.5.0" lodash.get "^4.4.2" @@ -8544,7 +8552,7 @@ cornerstone-math@^0.1.9: resolved "https://registry.npmjs.org/cornerstone-math/-/cornerstone-math-0.1.10.tgz#a3f99db64d73c5adee61ae0d570128eca1682d07" integrity sha512-23XSAyP7t70ANvhFyqwvva+zFd1bQ2d5GL7tg9qKE932WmImjA2Y9tiy5n0iTtnf51W/78Png8Lia2o4dCdJaQ== -cornerstone-wado-image-loader@^4.2.1, cornerstone-wado-image-loader@^4.8.0: +cornerstone-wado-image-loader@^4.2.1, cornerstone-wado-image-loader@^4.7.0: version "4.9.1" resolved "https://registry.npmjs.org/cornerstone-wado-image-loader/-/cornerstone-wado-image-loader-4.9.1.tgz#442eeca78ff0bcf9f9cbde04e8c429f35b31c55a" integrity sha512-l0HRxGAupfufnHjT9uFpwAtafvuGMKKB9SCL8dHUwCK+7jwNeVigy8s6+Oki2nycaJUzy2TRQaauO78mIf3grg== @@ -9259,12 +9267,12 @@ dateformat@^3.0.0: resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dayjs@^1.10.4, dayjs@^1.11.6: +dayjs@^1.10.4: version "1.11.7" resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -dcmjs@0.29.4, dcmjs@^0.29.4: +dcmjs@^0.29.4: version "0.29.4" resolved "https://registry.npmjs.org/dcmjs/-/dcmjs-0.29.4.tgz#3fb13945611979f756bc91b3f199fd87b4eabdb4" integrity sha512-PzD6C4oB7v/AVTIPbkS6D6hjgVcSW7+T+DsxFpkD1xcEgU4qjGM16WGEHH3ZAPDBF0Xgm2d9FXUu/lXZIyfmow== @@ -9649,10 +9657,10 @@ dicom-parser@^1.8.9: resolved "https://registry.npmjs.org/dicom-parser/-/dicom-parser-1.8.20.tgz#e5ef817d80d2fbc093ffd8c2a73a1cb5b47f50bd" integrity sha512-R8NXEcaqXu7Qe5exY662aXiJCbiE6fEF8+QpSkVcXTTecFzfIaW8KJ2tSwYvt0UMHP0AUBW13VxQSXfKv/0FUA== -dicomweb-client@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/dicomweb-client/-/dicomweb-client-0.6.0.tgz#5e35ada52fe0155af1cc1f0e84f9c7f76477f92d" - integrity sha512-VAkBg4W6odIo2XsFxqjN/rptd7bQ8oHpRuKH5d46E9BUIPzRschazE8Dx1xg7/l3N3f1M70jB7yJ339arAXiDQ== +dicomweb-client@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.8.4.tgz#3da814cedb9415facb50bc5f43af8d961a991c74" + integrity sha512-/6oY3/Fg9JyAlbTWuJOYbVqici3+nlZt43+Z/Y47RNiqLc028JcxNlY28u4VQqksxfB59f1hhNbsqsHyDT4vhw== didyoumean@^1.2.2: version "1.2.2"