diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json index c1f9ec223..151426b5d 100644 --- a/extensions/cornerstone-dicom-pmap/package.json +++ b/extensions/cornerstone-dicom-pmap/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.77.6", - "@cornerstonejs/core": "^1.77.6", + "@cornerstonejs/adapters": "^1.86.0", + "@cornerstonejs/core": "^1.86.0", "@kitware/vtk.js": "30.4.1", "react-color": "^2.19.3" } diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index 65ac02e4e..3f011ff9f 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.81.6", - "@cornerstonejs/core": "^1.81.6", + "@cornerstonejs/adapters": "^1.86.0", + "@cornerstonejs/core": "^1.86.0", "@kitware/vtk.js": "30.4.1", "react-color": "^2.19.3" } diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 14723ae70..47fe761b8 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.82.4", - "@cornerstonejs/core": "^1.82.4", - "@cornerstonejs/tools": "^1.82.4", + "@cornerstonejs/adapters": "^1.86.0", + "@cornerstonejs/core": "^1.86.0", + "@cornerstonejs/tools": "^1.86.0", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx index b12aafb7b..bca590188 100644 --- a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import CodeNameCodeSequenceValues from '../constants/CodeNameCodeSequenceValues'; +import { CodeNameCodeSequenceValues } from '../enums'; import formatContentItemValue from '../utils/formatContentItem'; const EMPTY_TAG_VALUE = '[empty]'; diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx index e6f50fe22..32e28d022 100644 --- a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx @@ -135,13 +135,17 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { newMeasurementSelected, displaySetService ).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => { + if (!referencedDisplaySet || !referencedDisplaySetMetadata) { + return; + } + setMeasurementSelected(newMeasurementSelected); setActiveImageDisplaySetData(referencedDisplaySet); setReferencedDisplaySetMetadata(referencedDisplaySetMetadata); if ( referencedDisplaySet.displaySetInstanceUID === - activeImageDisplaySetData?.displaySetInstanceUID + activeImageDisplaySetData.displaySetInstanceUID ) { const { measurements } = srDisplaySet; @@ -151,6 +155,10 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { // new measurement const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (!csViewport) { + return; + } + const imageIds = csViewport.getImageIds(); const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId); @@ -395,6 +403,10 @@ async function _getViewportReferencedDisplaySetData( displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet); } + if (!displaySetInstanceUID) { + return { referencedDisplaySetMetadata: null, referencedDisplaySet: null }; + } + const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); const image0 = referencedDisplaySet.images[0]; diff --git a/extensions/cornerstone-dicom-sr/src/constants/CodeNameCodeSequenceValues.ts b/extensions/cornerstone-dicom-sr/src/constants/CodeNameCodeSequenceValues.ts deleted file mode 100644 index cba848cbe..000000000 --- a/extensions/cornerstone-dicom-sr/src/constants/CodeNameCodeSequenceValues.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { adaptersSR } from '@cornerstonejs/adapters'; - -const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; - -const CodeNameCodeSequenceValues = { - ImagingMeasurementReport: '126000', - ImageLibrary: '111028', - ImagingMeasurements: '126010', - MeasurementGroup: '125007', - ImageLibraryGroup: '126200', - TrackingUniqueIdentifier: '112040', - TrackingIdentifier: '112039', - Finding: '121071', - FindingSite: 'G-C0E3', // SRT - CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, -}; - -export { CodeNameCodeSequenceValues as default }; diff --git a/extensions/cornerstone-dicom-sr/src/constants/scoordTypes.js b/extensions/cornerstone-dicom-sr/src/constants/scoordTypes.js deleted file mode 100644 index 9120d71d3..000000000 --- a/extensions/cornerstone-dicom-sr/src/constants/scoordTypes.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - POINT: 'POINT', - MULTIPOINT: 'MULTIPOINT', - POLYLINE: 'POLYLINE', - CIRCLE: 'CIRCLE', - ELLIPSE: 'ELLIPSE', -}; diff --git a/extensions/cornerstone-dicom-sr/src/enums.ts b/extensions/cornerstone-dicom-sr/src/enums.ts new file mode 100644 index 000000000..e53688c8a --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/enums.ts @@ -0,0 +1,44 @@ +import { adaptersSR } from '@cornerstonejs/adapters'; + +const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; + +export const SCOORDTypes = { + POINT: 'POINT', + MULTIPOINT: 'MULTIPOINT', + POLYLINE: 'POLYLINE', + CIRCLE: 'CIRCLE', + ELLIPSE: 'ELLIPSE', +}; + +export const CodeNameCodeSequenceValues = { + ImagingMeasurementReport: '126000', + ImageLibrary: '111028', + ImagingMeasurements: '126010', + MeasurementGroup: '125007', + ImageLibraryGroup: '126200', + TrackingUniqueIdentifier: '112040', + TrackingIdentifier: '112039', + Finding: '121071', + FindingSite: 'G-C0E3', // SRT + FindingSiteSCT: '363698007', // SCT +}; + +export const CodingSchemeDesignators = { + SRT: 'SRT', + SCT: 'SCT', + CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'], +}; + +export const RelationshipType = { + INFERRED_FROM: 'INFERRED FROM', + CONTAINS: 'CONTAINS', +}; + +const enums = { + CodeNameCodeSequenceValues, + CodingSchemeDesignators, + RelationshipType, + SCOORDTypes, +}; + +export default enums; diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index 9c035faa2..7b867cae2 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -1,32 +1,37 @@ -import { SOPClassHandlerName, SOPClassHandlerId } from './id'; import { utils, classes, DisplaySetService, Types } from '@ohif/core'; -import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation'; -import isRehydratable from './utils/isRehydratable'; +import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; import { adaptersSR } from '@cornerstonejs/adapters'; -import CodeNameCodeSequenceValues from './constants/CodeNameCodeSequenceValues'; + +import addSRAnnotation from './utils/addSRAnnotation'; +import isRehydratable from './utils/isRehydratable'; +import { + SOPClassHandlerName, + SOPClassHandlerId, + SOPClassHandlerId3D, + SOPClassHandlerName3D, +} from './id'; +import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums'; + +const { sopClassDictionary } = utils; +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; +const { ImageSet, MetadataProvider: metadataProvider } = classes; +const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; type InstanceMetadata = Types.InstanceMetadata; -const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; - -const { ImageSet, MetadataProvider: metadataProvider } = classes; - -// TODO -> -// Add SR thumbnail -// Make viewport -// Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport. +/** + * TODO + * - [ ] Add SR thumbnail + * - [ ] Make viewport + * - [ ] Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport + */ const sopClassUids = [ - '1.2.840.10008.5.1.4.1.1.88.11', // BASIC TEXT SR - '1.2.840.10008.5.1.4.1.1.88.22', // ENHANCED SR - '1.2.840.10008.5.1.4.1.1.88.33', // COMPREHENSIVE SR - '1.2.840.10008.5.1.4.1.1.88.34', // Comprehensive 3D SR - // '1.2.840.10008.5.1.4.1.1.88.50', // Mammography CAD SR + sopClassDictionary.BasicTextSR, + sopClassDictionary.EnhancedSR, + sopClassDictionary.ComprehensiveSR, ]; -const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; -const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; - const validateSameStudyUID = (uid: string, instances): void => { instances.forEach(it => { if (it.StudyInstanceUID !== uid) { @@ -36,18 +41,6 @@ const validateSameStudyUID = (uid: string, instances): void => { }); }; -const CodingSchemeDesignators = { - SRT: 'SRT', - CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'], -}; - -const RELATIONSHIP_TYPE = { - INFERRED_FROM: 'INFERRED FROM', - CONTAINS: 'CONTAINS', -}; - -const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; - /** * Adds instances to the DICOM SR series, rather than creating a new * series, so that as SR's are saved, they append to the series, and the @@ -102,6 +95,8 @@ function _getDisplaySetsFromSeries( } = instance; validateSameStudyUID(instance.StudyInstanceUID, instances); + const is3DSR = SOPClassUID === sopClassDictionary.Comprehensive3DSR; + const isImagingMeasurementReport = ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport; @@ -114,7 +109,7 @@ function _getDisplaySetsFromSeries( SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID, - SOPClassHandlerId, + SOPClassHandlerId: is3DSR ? SOPClassHandlerId3D : SOPClassHandlerId, SOPClassUID, instances, referencedImages: null, @@ -132,11 +127,21 @@ function _getDisplaySetsFromSeries( return [displaySet]; } -async function _load(displaySet, servicesManager: AppTypes.ServicesManager, extensionManager) { +/** + * Loads the display set with the given services and extension manager. + * @param srDisplaySet - The display set to load. + * @param servicesManager - The services manager containing displaySetService and measurementService. + * @param extensionManager - The extension manager containing data sources. + */ +async function _load( + srDisplaySet: Types.DisplaySet, + servicesManager: AppTypes.ServicesManager, + extensionManager: AppTypes.ExtensionManager +) { const { displaySetService, measurementService } = servicesManager.services; const dataSources = extensionManager.getDataSources(); const dataSource = dataSources[0]; - const { ContentSequence } = displaySet.instance; + const { ContentSequence } = srDisplaySet.instance; async function retrieveBulkData(obj, parentObj = null, key = null) { for (const prop in obj) { @@ -147,9 +152,9 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte } else if (prop === 'BulkDataURI') { const value = await dataSource.retrieve.bulkDataURI({ BulkDataURI: obj[prop], - StudyInstanceUID: displaySet.instance.StudyInstanceUID, - SeriesInstanceUID: displaySet.instance.SeriesInstanceUID, - SOPInstanceUID: displaySet.instance.SOPInstanceUID, + StudyInstanceUID: srDisplaySet.instance.StudyInstanceUID, + SeriesInstanceUID: srDisplaySet.instance.SeriesInstanceUID, + SOPInstanceUID: srDisplaySet.instance.SOPInstanceUID, }); if (parentObj && key) { parentObj[key] = new Float32Array(value); @@ -158,16 +163,16 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte } } - if (displaySet.isLoaded !== true) { + if (srDisplaySet.isLoaded !== true) { await retrieveBulkData(ContentSequence); } - if (displaySet.isImagingMeasurementReport) { - displaySet.referencedImages = _getReferencedImagesList(ContentSequence); - displaySet.measurements = _getMeasurements(ContentSequence); + if (srDisplaySet.isImagingMeasurementReport) { + srDisplaySet.referencedImages = _getReferencedImagesList(ContentSequence); + srDisplaySet.measurements = _getMeasurements(ContentSequence); } else { - displaySet.referencedImages = []; - displaySet.measurements = []; + srDisplaySet.referencedImages = []; + srDisplaySet.measurements = []; } const mappings = measurementService.getSourceMappings( @@ -175,28 +180,30 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte CORNERSTONE_3D_TOOLS_SOURCE_VERSION ); - displaySet.isHydrated = false; - displaySet.isRehydratable = isRehydratable(displaySet, mappings); - displaySet.isLoaded = true; + srDisplaySet.isHydrated = false; + srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings); + srDisplaySet.isLoaded = true; - // Check currently added displaySets and add measurements if the sources exist. + /** Check currently added displaySets and add measurements if the sources exist */ displaySetService.activeDisplaySets.forEach(activeDisplaySet => { _checkIfCanAddMeasurementsToDisplaySet( - displaySet, + srDisplaySet, activeDisplaySet, dataSource, servicesManager ); }); - // Subscribe to new displaySets as the source may come in after. + /** Subscribe to new displaySets as the source may come in after */ displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { const { displaySetsAdded } = data; - // If there are still some measurements that have not yet been loaded into cornerstone, - // See if we can load them onto any of the new displaySets. + /** + * If there are still some measurements that have not yet been loaded into cornerstone, + * See if we can load them onto any of the new displaySets. + */ displaySetsAdded.forEach(newDisplaySet => { _checkIfCanAddMeasurementsToDisplaySet( - displaySet, + srDisplaySet, newDisplaySet, dataSource, servicesManager @@ -205,6 +212,14 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte }); } +/** + * Checks if measurements can be added to a display set. + * + * @param srDisplaySet - The source display set containing measurements. + * @param newDisplaySet - The new display set to check if measurements can be added. + * @param dataSource - The data source used to retrieve image IDs. + * @param servicesManager - The services manager. + */ function _checkIfCanAddMeasurementsToDisplaySet( srDisplaySet, newDisplaySet, @@ -212,108 +227,103 @@ function _checkIfCanAddMeasurementsToDisplaySet( servicesManager: AppTypes.ServicesManager ) { const { customizationService } = servicesManager.services; - let unloadedMeasurements = srDisplaySet.measurements.filter( + + const unloadedMeasurements = srDisplaySet.measurements.filter( measurement => measurement.loaded === false ); - if (unloadedMeasurements.length === 0) { - // All already loaded! + if ( + unloadedMeasurements.length === 0 || + !(newDisplaySet instanceof ImageSet) || + newDisplaySet.unsupported + ) { return; } - if ((!newDisplaySet) instanceof ImageSet) { - // This also filters out _this_ displaySet, as it is not an ImageSet. + // const { sopClassUids } = newDisplaySet; + // Create a Set for faster lookups + // const sopClassUidSet = new Set(sopClassUids); + + // Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number + const imageIdMap = new Map(); + const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet); + + for (const imageId of imageIds) { + const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId); + const key = `${SOPInstanceUID}:${frameNumber || 1}`; + imageIdMap.set(key, imageId); + } + + if (!unloadedMeasurements?.length) { return; } - if (newDisplaySet.unsupported) { - return; - } + const is3DSR = srDisplaySet.SOPClassUID === sopClassDictionary.Comprehensive3DSR; - const { sopClassUids } = newDisplaySet; + for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { + let measurement = unloadedMeasurements[j]; - // Check if any have the newDisplaySet is the correct SOPClass. - unloadedMeasurements = unloadedMeasurements.filter(measurement => - measurement.coords.some(coord => - sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID) - ) - ); + const onBeforeSRAddMeasurement = customizationService.getModeCustomization( + 'onBeforeSRAddMeasurement' + )?.value; - if (unloadedMeasurements.length === 0) { - // New displaySet isn't the correct SOPClass, so can't contain the referenced images. - return; - } - - const SOPInstanceUIDs = []; - - unloadedMeasurements.forEach(measurement => { - const { coords } = measurement; - - coords.forEach(coord => { - const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID; - - if (!SOPInstanceUIDs.includes(SOPInstanceUID)) { - SOPInstanceUIDs.push(SOPInstanceUID); - } - }); - }); - - const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet); - - for (const imageId of imageIdsForDisplaySet) { - if (!unloadedMeasurements.length) { - // All measurements loaded. - return; + if (typeof onBeforeSRAddMeasurement === 'function') { + measurement = onBeforeSRAddMeasurement({ + measurement, + StudyInstanceUID: srDisplaySet.StudyInstanceUID, + SeriesInstanceUID: srDisplaySet.SeriesInstanceUID, + }); } - const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId); + // if it is 3d SR we can just add the SR annotation + if (is3DSR) { + addSRAnnotation(measurement, null, null); + measurement.loaded = true; + continue; + } - if (SOPInstanceUIDs.includes(SOPInstanceUID)) { - for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { - let measurement = unloadedMeasurements[j]; + const referencedSOPSequence = measurement.coords[0].ReferencedSOPSequence; + if (!referencedSOPSequence) { + continue; + } - const onBeforeSRAddMeasurement = customizationService.getModeCustomization( - 'onBeforeSRAddMeasurement' - )?.value; + const { ReferencedSOPInstanceUID } = referencedSOPSequence; + const frame = referencedSOPSequence.ReferencedFrameNumber || 1; + const key = `${ReferencedSOPInstanceUID}:${frame}`; + const imageId = imageIdMap.get(key); - if (typeof onBeforeSRAddMeasurement === 'function') { - measurement = onBeforeSRAddMeasurement({ - measurement, - StudyInstanceUID: srDisplaySet.StudyInstanceUID, - SeriesInstanceUID: srDisplaySet.SeriesInstanceUID, - }); - } + if ( + imageId && + _measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame) + ) { + addSRAnnotation(measurement, imageId, frame); - if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) { - const frame = - (measurement.coords[0].ReferencedSOPSequence && - measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) || - 1; + // Update measurement properties + measurement.loaded = true; + measurement.imageId = imageId; + measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID; + measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID; + measurement.frameNumber = frame; - /** Add DICOMSRDisplay annotation for the SR viewport (only) */ - addDICOMSRDisplayAnnotation(measurement, imageId, frame); - - /** Update measurement properties */ - measurement.loaded = true; - measurement.imageId = imageId; - measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID; - measurement.ReferencedSOPInstanceUID = - measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID; - measurement.frameNumber = frame; - delete measurement.coords; - - unloadedMeasurements.splice(j, 1); - } - } + unloadedMeasurements.splice(j, 1); } } } +/** + * Checks if a measurement references a specific SOP Instance UID. + * @param measurement - The measurement object. + * @param SOPInstanceUID - The SOP Instance UID to check against. + * @param frameNumber - The frame number to check against (optional). + * @returns True if the measurement references the specified SOP Instance UID, false otherwise. + */ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) { const { coords } = measurement; - // NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM - // Standard. But for now, we will support only one ReferenceFrameNumber. + /** + * NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM + * Standard. But for now, we will support only one ReferenceFrameNumber. + */ const ReferencedFrameNumber = (measurement.coords[0].ReferencedSOPSequence && measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) || @@ -326,27 +336,46 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame for (let j = 0; j < coords.length; j++) { const coord = coords[j]; const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence; - if (ReferencedSOPInstanceUID === SOPInstanceUID) { return true; } } + + return false; } +/** + * Retrieves the SOP class handler module. + * + * @param {Object} options - The options for retrieving the SOP class handler module. + * @param {Object} options.servicesManager - The services manager. + * @param {Object} options.extensionManager - The extension manager. + * @returns {Array} An array containing the SOP class handler module. + */ function getSopClassHandlerModule({ servicesManager, extensionManager }) { const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; - return [ { name: SOPClassHandlerName, sopClassUids, getDisplaySetsFromSeries, }, + { + name: SOPClassHandlerName3D, + sopClassUids: [sopClassDictionary.Comprehensive3DSR], + getDisplaySetsFromSeries, + }, ]; } +/** + * Retrieves the measurements from the ImagingMeasurementReportContentSequence. + * + * @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array. + * @returns {Array} - The array of measurements. + */ function _getMeasurements(ImagingMeasurementReportContentSequence) { const ImagingMeasurements = ImagingMeasurementReportContentSequence.find( item => @@ -363,7 +392,6 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) { const mergedContentSequencesByTrackingUniqueIdentifiers = _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups); - const measurements = []; Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach( @@ -372,7 +400,6 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) { mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier]; const measurement = _processMeasurement(mergedContentSequence); - if (measurement) { measurements.push(measurement); } @@ -382,6 +409,12 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) { return measurements; } +/** + * Retrieves merged content sequences by tracking unique identifiers. + * + * @param {Array} MeasurementGroups - The measurement groups. + * @returns {Object} - The merged content sequences by tracking unique identifiers. + */ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) { const mergedContentSequencesByTrackingUniqueIdentifiers = {}; @@ -393,7 +426,6 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingUniqueIdentifier ); - if (!TrackingUniqueIdentifierItem) { console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.'); } @@ -422,6 +454,15 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups return mergedContentSequencesByTrackingUniqueIdentifiers; } +/** + * Processes the measurement based on the merged content sequence. + * If the merged content sequence contains SCOORD or SCOORD3D value types, + * it calls the _processTID1410Measurement function. + * Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function. + * + * @param {Array} mergedContentSequence - The merged content sequence to process. + * @returns {any} - The processed measurement result. + */ function _processMeasurement(mergedContentSequence) { if ( mergedContentSequence.some( @@ -434,11 +475,21 @@ function _processMeasurement(mergedContentSequence) { return _processNonGeometricallyDefinedMeasurement(mergedContentSequence); } +/** + * Processes TID 1410 style measurements from the mergedContentSequence. + * TID 1410 style measurements have a SCOORD or SCOORD3D at the top level, + * and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D. + * + * @param mergedContentSequence - The merged content sequence containing the measurements. + * @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier. + */ function _processTID1410Measurement(mergedContentSequence) { // Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level, // And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D - const graphicItem = mergedContentSequence.find(group => group.ValueType === 'SCOORD'); + const graphicItem = mergedContentSequence.find( + group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D' + ); const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF'); @@ -465,7 +516,6 @@ function _processTID1410Measurement(mergedContentSequence) { NUMContentItems.forEach(item => { const { ConceptNameCodeSequence, MeasuredValueSequence } = item; - if (MeasuredValueSequence) { measurement.labels.push( _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) @@ -473,12 +523,29 @@ function _processTID1410Measurement(mergedContentSequence) { } }); + const findingSites = mergedContentSequence.filter( + item => + item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SCT && + item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT + ); + if (findingSites.length) { + measurement.labels.push({ + label: CodeNameCodeSequenceValues.FindingSiteSCT, + value: findingSites[0].ConceptCodeSequence.CodeMeaning, + }); + } + return measurement; } +/** + * Processes the non-geometrically defined measurement from the merged content sequence. + * + * @param mergedContentSequence The merged content sequence containing the measurement data. + * @returns The processed measurement object. + */ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); - const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF'); const TrackingIdentifierContentItem = mergedContentSequence.find( @@ -508,10 +575,10 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { CodingSchemeDesignators.CornerstoneCodeSchemes.includes( finding.ConceptCodeSequence.CodingSchemeDesignator ) && - finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText + finding.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT ) { measurement.labels.push({ - label: CORNERSTONE_FREETEXT_CODE_VALUE, + label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, value: finding.ConceptCodeSequence.CodeMeaning, }); } @@ -523,12 +590,13 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { CodingSchemeDesignators.CornerstoneCodeSchemes.includes( FindingSite.ConceptCodeSequence.CodingSchemeDesignator ) && - FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText + FindingSite.ConceptCodeSequence.CodeValue === + Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT ); if (cornerstoneFreeTextFindingSite) { measurement.labels.push({ - label: CORNERSTONE_FREETEXT_CODE_VALUE, + label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning, }); } @@ -538,15 +606,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item; const { ValueType } = ContentSequence; - if (!ValueType === 'SCOORD') { console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`); - return; } const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence); - if (coords) { measurement.coords.push(coords); } @@ -561,51 +626,47 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { return measurement; } -function _getCoordsFromSCOORDOrSCOORD3D(item) { - const { ValueType, RelationshipType, GraphicType, GraphicData } = item; - - if ( - !( - RelationshipType == RELATIONSHIP_TYPE.INFERRED_FROM || - RelationshipType == RELATIONSHIP_TYPE.CONTAINS - ) - ) { - console.warn( - `Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM" or "CONTAINS"` - ); - - return; - } - +/** + * Extracts coordinates from a graphic item of type SCOORD or SCOORD3D. + * @param {object} graphicItem - The graphic item containing the coordinates. + * @returns {object} - The extracted coordinates. + */ +const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => { + const { ValueType, GraphicType, GraphicData } = graphicItem; const coords = { ValueType, GraphicType, GraphicData }; - - // ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM' - if (ValueType === 'SCOORD') { - const { ReferencedSOPSequence } = item.ContentSequence; - - coords.ReferencedSOPSequence = ReferencedSOPSequence; - } else if (ValueType === 'SCOORD3D') { - const { ReferencedFrameOfReferenceSequence } = item.ContentSequence; - - coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence; - } - + coords.ReferencedSOPSequence = graphicItem.ContentSequence?.ReferencedSOPSequence; + coords.ReferencedFrameOfReferenceSequence = + graphicItem.ReferencedFrameOfReferenceUID || + graphicItem.ContentSequence?.ReferencedFrameOfReferenceSequence; return coords; -} +}; +/** + * Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence. + * @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object. + * @param {Object} MeasuredValueSequence - The MeasuredValueSequence object. + * @returns {Object} - An object containing the label and value. + * The label represents the CodeMeaning from the ConceptNameCodeSequence. + * The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence. + * Example: { label: 'Long Axis', value: '31.00 mm' } + */ function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) { const { CodeMeaning } = ConceptNameCodeSequence; const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence; const { CodeValue } = MeasurementUnitsCodeSequence; - const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : ''; - return { label: CodeMeaning, value: `${formatedNumericValue} ${CodeValue}`, }; // E.g. Long Axis: 31.0 mm } +/** + * Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence. + * + * @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence. + * @returns {Array} - The list of referenced images. + */ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { const ImageLibrary = ImagingMeasurementReportContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary @@ -618,6 +679,9 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup ); + if (!ImageLibraryGroup) { + return []; + } const referencedImages = []; @@ -641,6 +705,15 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { return referencedImages; } +/** + * Converts a DICOM sequence to an array. + * If the sequence is null or undefined, an empty array is returned. + * If the sequence is already an array, it is returned as is. + * Otherwise, the sequence is wrapped in an array and returned. + * + * @param {any} sequence - The DICOM sequence to convert. + * @returns {any[]} - The converted array. + */ function _getSequenceAsArray(sequence) { if (!sequence) { return []; diff --git a/extensions/cornerstone-dicom-sr/src/id.js b/extensions/cornerstone-dicom-sr/src/id.js index 66191e8cd..f6706978f 100644 --- a/extensions/cornerstone-dicom-sr/src/id.js +++ b/extensions/cornerstone-dicom-sr/src/id.js @@ -5,4 +5,7 @@ const id = packageJson.name; const SOPClassHandlerName = 'dicom-sr'; const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`; -export { SOPClassHandlerName, SOPClassHandlerId, id }; +const SOPClassHandlerName3D = 'dicom-sr-3d'; +const SOPClassHandlerId3D = `${id}.sopClassHandlerModule.${SOPClassHandlerName3D}`; + +export { SOPClassHandlerName, SOPClassHandlerId, SOPClassHandlerName3D, SOPClassHandlerId3D, id }; diff --git a/extensions/cornerstone-dicom-sr/src/index.tsx b/extensions/cornerstone-dicom-sr/src/index.tsx index e479307d2..02380b115 100644 --- a/extensions/cornerstone-dicom-sr/src/index.tsx +++ b/extensions/cornerstone-dicom-sr/src/index.tsx @@ -8,6 +8,7 @@ import { id } from './id.js'; import toolNames from './tools/toolNames'; import hydrateStructuredReport from './utils/hydrateStructuredReport'; import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet'; +import Enums from './enums'; const Component = React.lazy(() => { return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport'); @@ -70,4 +71,4 @@ const dicomSRExtension = { export default dicomSRExtension; // Put static exports here so they can be type checked -export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol }; +export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol, Enums, toolNames }; diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index 949b4de2e..2e74751dc 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -9,16 +9,28 @@ import { LengthTool, PlanarFreehandROITool, RectangleROITool, + utilities as csToolsUtils, } from '@cornerstonejs/tools'; +import { Types, MeasurementService } from '@ohif/core'; +import { StackViewport } from '@cornerstonejs/core'; +import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; +import SCOORD3DPointTool from './tools/SCOORD3DPointTool'; +import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper'; import addToolInstance from './utils/addToolInstance'; -import { Types } from '@ohif/core'; import toolNames from './tools/toolNames'; +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; + /** * @param {object} configuration */ -export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void { +export default function init({ + configuration = {}, + servicesManager, +}: Types.Extensions.ExtensionParams): void { + const { measurementService, cornerstoneViewportService } = servicesManager.services; + addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool); addToolInstance(toolNames.SRLength, LengthTool); addToolInstance(toolNames.SRBidirectional, BidirectionalTool); @@ -28,10 +40,26 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP addToolInstance(toolNames.SRAngle, AngleTool); addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool); addToolInstance(toolNames.SRRectangleROI, RectangleROITool); + addToolInstance(toolNames.SRSCOORD3DPoint, SCOORD3DPointTool); // TODO - fix the SR display of Cobb Angle, as it joins the two lines addToolInstance(toolNames.SRCobbAngle, CobbAngleTool); + const csTools3DVer1MeasurementSource = measurementService.getSource( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + + const { POINT } = measurementService.VALUE_TYPES; + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'SRSCOORD3DPoint', + POINT, + SRSCOOR3DProbeMapper.toAnnotation, + SRSCOOR3DProbeMapper.toMeasurement + ); + // Modify annotation tools to use dashed lines on SR const dashedLine = { lineDash: '4,4', @@ -49,4 +77,28 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP SRRectangleROI: dashedLine, global: {}, }); + + measurementService.subscribe( + MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, + ({ viewportId, measurement, isConsumed }) => { + if (isConsumed) { + return; + } + try { + const currentViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const { viewPlaneNormal } = currentViewport.getCamera(); + const referencedImageId = csToolsUtils.getClosestImageIdForStackViewport( + currentViewport as StackViewport, + measurement.points[0], + viewPlaneNormal + ); + const imageIndex = (currentViewport as StackViewport) + .getImageIds() + .indexOf(referencedImageId); + csToolsUtils.jumpToSlice(currentViewport.element, { imageIndex }); + } catch (error) { + console.warn('Unable to jump to image based on measurement coordinate', error); + } + } + ); } diff --git a/extensions/cornerstone-dicom-sr/src/onModeEnter.js b/extensions/cornerstone-dicom-sr/src/onModeEnter.js index fc4b1efce..a23877204 100644 --- a/extensions/cornerstone-dicom-sr/src/onModeEnter.js +++ b/extensions/cornerstone-dicom-sr/src/onModeEnter.js @@ -1,11 +1,11 @@ -import { SOPClassHandlerId } from './id'; +import { SOPClassHandlerId, SOPClassHandlerId3D } from './id'; export default function onModeEnter({ servicesManager }) { const { displaySetService } = servicesManager.services; const displaySetCache = displaySetService.getDisplaySetCache(); const srDisplaySets = [...displaySetCache.values()].filter( - ds => ds.SOPClassHandlerId === SOPClassHandlerId + ds => ds.SOPClassHandlerId === SOPClassHandlerId || ds.SOPClassHandlerId === SOPClassHandlerId3D ); srDisplaySets.forEach(ds => { diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index eaf3bf6ea..99679d5e8 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -7,10 +7,11 @@ import { Types as cs3DToolsTypes, } from '@cornerstonejs/tools'; import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule'; -import SCOORD_TYPES from '../constants/scoordTypes'; +import { SCOORDTypes } from '../enums'; +import toolNames from './toolNames'; export default class DICOMSRDisplayTool extends AnnotationTool { - static toolName = 'DICOMSRDisplay'; + static toolName = toolNames.DICOMSRDisplay; constructor( toolProps = {}, @@ -112,19 +113,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool { let canvasCoordinatesAdapter; switch (GraphicType) { - case SCOORD_TYPES.POINT: + case SCOORDTypes.POINT: renderMethod = this.renderPoint; break; - case SCOORD_TYPES.MULTIPOINT: + case SCOORDTypes.MULTIPOINT: renderMethod = this.renderMultipoint; break; - case SCOORD_TYPES.POLYLINE: + case SCOORDTypes.POLYLINE: renderMethod = this.renderPolyLine; break; - case SCOORD_TYPES.CIRCLE: + case SCOORDTypes.CIRCLE: renderMethod = this.renderEllipse; break; - case SCOORD_TYPES.ELLIPSE: + case SCOORDTypes.ELLIPSE: renderMethod = this.renderEllipse; canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners; break; @@ -335,7 +336,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { } const { annotationUID, data = {} } = annotation; - const { label } = data; + const { labels } = data; const { color } = options; let adaptedCanvasCoordinates = canvasCoordinates; @@ -343,7 +344,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { if (typeof canvasCoordinatesAdapter === 'function') { adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates); } - const textLines = this._getTextBoxLinesFromLabels(label); + const textLines = this._getTextBoxLinesFromLabels(labels); const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates); if (!annotation.data?.handles?.textBox?.worldPosition) { diff --git a/extensions/cornerstone-dicom-sr/src/tools/SCOORD3DPointTool.ts b/extensions/cornerstone-dicom-sr/src/tools/SCOORD3DPointTool.ts new file mode 100644 index 000000000..e41260965 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/tools/SCOORD3DPointTool.ts @@ -0,0 +1,203 @@ +import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core'; +import { + annotation, + drawing, + utilities, + Types as cs3DToolsTypes, + AnnotationDisplayTool, +} from '@cornerstonejs/tools'; +import toolNames from './toolNames'; +import { Annotation } from '@cornerstonejs/tools/dist/types/types'; + +export default class SCOORD3DPointTool extends AnnotationDisplayTool { + static toolName = toolNames.SRSCOORD3DPoint; + + constructor( + toolProps = {}, + defaultToolProps = { + configuration: {}, + } + ) { + super(toolProps, defaultToolProps); + } + + _getTextBoxLinesFromLabels(labels) { + // TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this! + + const labelLength = Math.min(labels.length, 5); + const lines = []; + + return lines; + } + + // This tool should not inherit from AnnotationTool and we should not need + // to add the following lines. + isPointNearTool = () => null; + getHandleNearImagePoint = () => null; + + renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => { + const { viewport } = enabledElement; + const { element } = viewport; + + const annotations = annotation.state.getAnnotations(this.getToolName(), element); + + // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender + if (!annotations?.length) { + return; + } + + // Filter toolData to only render the data for the active SR. + const filteredAnnotations = annotations; + if (!viewport._actors?.size) { + return; + } + + const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = { + toolGroupId: this.toolGroupId, + toolName: this.getToolName(), + viewportId: enabledElement.viewport.id, + }; + + for (let i = 0; i < filteredAnnotations.length; i++) { + const annotation = filteredAnnotations[i]; + + const annotationUID = annotation.annotationUID; + const { renderableData } = annotation.data; + const { POINT: points } = renderableData; + + styleSpecifier.annotationUID = annotationUID; + + const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation); + const lineDash = this.getStyle('lineDash', styleSpecifier, annotation); + const color = this.getStyle('color', styleSpecifier, annotation); + + const options = { + color, + lineDash, + lineWidth, + }; + + const point = points[0][0]; + + // check if viewport can render it + const viewable = viewport.isReferenceViewable( + { FrameOfReferenceUID: annotation.metadata.FrameOfReferenceUID, cameraFocalPoint: point }, + { asNearbyProjection: true } + ); + + if (!viewable) { + continue; + } + + // render the point + const arrowPointCanvas = viewport.worldToCanvas(point); + // Todo: configure this + const arrowEndCanvas = [arrowPointCanvas[0] + 20, arrowPointCanvas[1] + 20]; + const canvasCoordinates = [arrowPointCanvas, arrowEndCanvas]; + + drawing.drawArrow( + svgDrawingHelper, + annotationUID, + '1', + canvasCoordinates[1], + canvasCoordinates[0], + { + color: options.color, + width: options.lineWidth, + } + ); + + this.renderTextBox( + svgDrawingHelper, + viewport, + canvasCoordinates, + annotation, + styleSpecifier, + options + ); + } + }; + + renderTextBox( + svgDrawingHelper, + viewport, + canvasCoordinates, + annotation, + styleSpecifier, + options = {} + ) { + if (!canvasCoordinates || !annotation) { + return; + } + + const { annotationUID, data = {} } = annotation; + const { labels } = data; + + const textLines = []; + + for (const label of labels) { + // make this generic + // fix this + if (label.label === '363698007') { + textLines.push(`Finding Site: ${label.value}`); + } + } + + const { color } = options; + + const adaptedCanvasCoordinates = canvasCoordinates; + // adapt coordinates if there is an adapter + const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates); + + if (!annotation.data?.handles?.textBox?.worldPosition) { + annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords); + } + + const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition); + + const textBoxUID = '1'; + const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation); + + const boundingBox = drawing.drawLinkedTextBox( + svgDrawingHelper, + annotationUID, + textBoxUID, + textLines, + textBoxPosition, + canvasCoordinates, + {}, + { + ...textBoxOptions, + color, + } + ); + + const { x: left, y: top, width, height } = boundingBox; + + annotation.data.handles.textBox.worldBoundingBox = { + topLeft: viewport.canvasToWorld([left, top]), + topRight: viewport.canvasToWorld([left + width, top]), + bottomLeft: viewport.canvasToWorld([left, top + height]), + bottomRight: viewport.canvasToWorld([left + width, top + height]), + }; + } + + public getLinkedTextBoxStyle( + specifications: cs3DToolsTypes.AnnotationStyle.StyleSpecifier, + annotation?: Annotation + ): Record { + // Todo: this function can be used to set different styles for different toolMode + // for the textBox. + + return { + visibility: this.getStyle('textBoxVisibility', specifications, annotation), + fontFamily: this.getStyle('textBoxFontFamily', specifications, annotation), + fontSize: this.getStyle('textBoxFontSize', specifications, annotation), + color: this.getStyle('textBoxColor', specifications, annotation), + shadow: this.getStyle('textBoxShadow', specifications, annotation), + background: this.getStyle('textBoxBackground', specifications, annotation), + lineWidth: this.getStyle('textBoxLinkLineWidth', specifications, annotation), + lineDash: this.getStyle('textBoxLinkLineDash', specifications, annotation), + }; + } +} diff --git a/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts index 2e52ec111..535c11f28 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts @@ -1,7 +1,5 @@ -import DICOMSRDisplayTool from './DICOMSRDisplayTool'; - const toolNames = { - DICOMSRDisplay: DICOMSRDisplayTool.toolName, + DICOMSRDisplay: 'DICOMSRDisplay', SRLength: 'SRLength', SRBidirectional: 'SRBidirectional', SREllipticalROI: 'SREllipticalROI', @@ -11,6 +9,7 @@ const toolNames = { SRCobbAngle: 'SRCobbAngle', SRRectangleROI: 'SRRectangleROI', SRPlanarFreehandROI: 'SRPlanarFreehandROI', + SRSCOORD3DPoint: 'SRSCOORD3DPoint', }; export default toolNames; diff --git a/extensions/cornerstone-dicom-sr/src/utils/SRSCOOR3DProbeMapper.ts b/extensions/cornerstone-dicom-sr/src/utils/SRSCOOR3DProbeMapper.ts new file mode 100644 index 000000000..b67c88772 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/SRSCOOR3DProbeMapper.ts @@ -0,0 +1,62 @@ +const SRSCOOR3DProbe = { + 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, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('Probe tool: Missing metadata or data'); + return null; + } + + const { toolName } = metadata; + const { points } = data.handles; + + const displayText = getDisplayText(annotation); + return { + uid: annotationUID, + points, + metadata, + toolName: metadata.toolName, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType?.(toolName) ?? null, + }; + }, +}; + +function getDisplayText(annotation) { + const { data } = annotation; + + if (!data) { + return ['']; + } + const { labels } = data; + + const displayText = []; + + for (const label of labels) { + // make this generic + if (label.label === '33636980076') { + displayText.push(`Finding Site: ${label.value}`); + } + } + + return displayText; +} + +export default SRSCOOR3DProbe; diff --git a/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts b/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts new file mode 100644 index 000000000..f50a0ccb3 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts @@ -0,0 +1,67 @@ +import { Types, annotation } from '@cornerstonejs/tools'; +import { metaData } from '@cornerstonejs/core'; + +import getRenderableData from './getRenderableData'; +import toolNames from '../tools/toolNames'; + +export default function addSRAnnotation(measurement, imageId, frameNumber) { + let toolName = toolNames.DICOMSRDisplay; + const renderableData = measurement.coords.reduce((acc, coordProps) => { + acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || []; + acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId })); + return acc; + }, {}); + + const { TrackingUniqueIdentifier } = measurement; + const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0]; + const graphicTypePoints = renderableData[graphicType]; + + /** TODO: Read the tool name from the DICOM SR identification type in the future. */ + let frameOfReferenceUID = null; + + if (imageId) { + const imagePlaneModule = metaData.get('imagePlaneModule', imageId); + frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID; + } + + if (valueType === 'SCOORD3D') { + toolName = toolNames.SRSCOORD3DPoint; + + // get the ReferencedFrameOfReferenceUID from the measurement + frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence; + } + + const SRAnnotation: Types.Annotation = { + annotationUID: TrackingUniqueIdentifier, + highlighted: false, + isLocked: false, + invalidated: false, + metadata: { + toolName, + valueType, + graphicType, + FrameOfReferenceUID: frameOfReferenceUID, + referencedImageId: imageId, + }, + data: { + label: measurement.labels?.[0]?.value || undefined, + displayText: measurement.displayText || undefined, + handles: { + textBox: measurement.textBox ?? {}, + points: graphicTypePoints[0], + }, + cachedStats: {}, + frameNumber, + renderableData, + TrackingUniqueIdentifier, + labels: measurement.labels, + }, + }; + + /** + * const annotationManager = annotation.annotationState.getAnnotationManager(); + * was not triggering annotation_added events. + */ + annotation.state.addAnnotation(SRAnnotation); + console.debug('Adding SR annotation:', SRAnnotation); +} diff --git a/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts b/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts index bd4cf2d1e..717b9dc31 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts @@ -1,8 +1,14 @@ import { addTool } from '@cornerstonejs/tools'; -export default function addToolInstance(name: string, toolClass, configuration?): void { +export default function addToolInstance(name: string, toolClass, configuration = {}): void { class InstanceClass extends toolClass { static toolName = name; + constructor(toolProps, defaultToolProps) { + toolProps.configuration = toolProps.configuration + ? { ...toolProps.configuration, ...configuration } + : configuration; + super(toolProps, defaultToolProps); + } } addTool(InstanceClass); } diff --git a/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts b/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts index 724164165..5e615f04e 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts @@ -61,6 +61,11 @@ const createReferencedImageDisplaySet = (displaySetService, displaySet) => { const imageSet = new ImageSet(instances); const instance = instances[0]; + + if (!instance) { + return; + } + imageSet.setAttributes({ displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID SeriesDate: instance.SeriesDate, diff --git a/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js b/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js index 1fa18ee9c..b814b42d3 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js +++ b/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js @@ -1,3 +1,7 @@ +import { adaptersSR } from '@cornerstonejs/adapters'; + +const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; + /** * Extracts the label from the toolData imported from dcmjs. We need to do this * as dcmjs does not depeend on OHIF/the measurementService, it just produces data for cornestoneTools. @@ -9,13 +13,15 @@ export default function getLabelFromDCMJSImportedToolData(toolData) { const { findingSites = [], finding } = toolData; - let freeTextLabel = findingSites.find(fs => fs.CodeValue === 'CORNERSTONEFREETEXT'); + let freeTextLabel = findingSites.find( + fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT + ); if (freeTextLabel) { return freeTextLabel.CodeMeaning; } - if (finding && finding.CodeValue === 'CORNERSTONEFREETEXT') { + if (finding && finding.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT) { return finding.CodeMeaning; } } diff --git a/extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts similarity index 56% rename from extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts rename to extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts index 585a9183a..a8546a28f 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts @@ -1,106 +1,49 @@ import { vec3 } from 'gl-matrix'; -import { Types, annotation } from '@cornerstonejs/tools'; import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core'; -import toolNames from '../tools/toolNames'; -import SCOORD_TYPES from '../constants/scoordTypes'; +import { SCOORDTypes } from '../enums'; const EPSILON = 1e-4; -export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) { - const toolName = toolNames.DICOMSRDisplay; - - const measurementData = { - TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier, - renderableData: {}, - labels: measurement.labels, - imageId, - }; - - measurement.coords.forEach(coord => { - const { GraphicType, GraphicData } = coord; - - if (measurementData.renderableData[GraphicType] === undefined) { - measurementData.renderableData[GraphicType] = []; +const getRenderableCoords = ({ GraphicData, ValueType, imageId }) => { + const renderableData = []; + if (ValueType === 'SCOORD3D') { + for (let i = 0; i < GraphicData.length; i += 3) { + renderableData.push([GraphicData[i], GraphicData[i + 1], GraphicData[i + 2]]); } + } else { + for (let i = 0; i < GraphicData.length; i += 2) { + const worldPos = utilities.imageToWorldCoords(imageId, [GraphicData[i], GraphicData[i + 1]]); + renderableData.push(worldPos); + } + } + return renderableData; +}; - measurementData.renderableData[GraphicType].push( - _getRenderableData(GraphicType, GraphicData, imageId) - ); - }); - - const imagePlaneModule = metaData.get('imagePlaneModule', imageId); - - /** - * This annotation (DICOMSRDisplay) is only used by the SR viewport. - * This is used before the annotation is hydrated. If hydrated the measurement will be added - * to the measurement service and will be available for the other viewports. - */ - const SRAnnotation: Types.Annotation = { - annotationUID: measurement.TrackingUniqueIdentifier, - highlighted: false, - isLocked: false, - invalidated: false, - metadata: { - toolName: toolName, - FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID, - referencedImageId: imageId, - }, - data: { - label: measurement.labels, - handles: { - textBox: measurement.textBox ?? {}, - }, - cachedStats: {}, - TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier, - renderableData: measurementData.renderableData, - frameNumber, - }, - }; - const annotationManager = annotation.state.getAnnotationManager(); - annotationManager.addAnnotation(SRAnnotation); -} - -function _getRenderableData(GraphicType, GraphicData, imageId) { - let renderableData: csTypes.Point3[]; +function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) { + let renderableData = []; switch (GraphicType) { - case SCOORD_TYPES.POINT: - case SCOORD_TYPES.MULTIPOINT: - case SCOORD_TYPES.POLYLINE: - renderableData = []; - - for (let i = 0; i < GraphicData.length; i += 2) { - const worldPos = utilities.imageToWorldCoords(imageId, [ - GraphicData[i], - GraphicData[i + 1], - ]); - - renderableData.push(worldPos); - } - + case SCOORDTypes.POINT: + case SCOORDTypes.MULTIPOINT: + case SCOORDTypes.POLYLINE: { + renderableData = getRenderableCoords({ GraphicData, ValueType, imageId }); break; - case SCOORD_TYPES.CIRCLE: { - const pointsWorld = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const worldPos = utilities.imageToWorldCoords(imageId, [ - GraphicData[i], - GraphicData[i + 1], - ]); - - pointsWorld.push(worldPos); - } - + } + case SCOORDTypes.CIRCLE: { + const pointsWorld: csTypes.Point3[] = getRenderableCoords({ + GraphicData, + ValueType, + imageId, + }); // We do not have an explicit draw circle svg helper in Cornerstone3D at // this time, but we can use the ellipse svg helper to draw a circle, so // here we reshape the data for that purpose. const center = pointsWorld[0]; const onPerimeter = pointsWorld[1]; - const radius = vec3.distance(center, onPerimeter); const imagePlaneModule = metaData.get('imagePlaneModule', imageId); - if (!imagePlaneModule) { throw new Error('No imagePlaneModule found'); } @@ -115,14 +58,12 @@ function _getRenderableData(GraphicType, GraphicData, imageId) { // we need to get major/minor axis (which are both the same size major = minor) - // first axisStart const firstAxisStart = vec3.create(); vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius); const firstAxisEnd = vec3.create(); vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius); - // second axisStart const secondAxisStart = vec3.create(); vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius); @@ -138,21 +79,16 @@ function _getRenderableData(GraphicType, GraphicData, imageId) { break; } - case SCOORD_TYPES.ELLIPSE: { + case SCOORDTypes.ELLIPSE: { // GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY] // But Cornerstone3D points are ordered as top, bottom, left, right for the // ellipse so we need to identify if the majorAxis is horizontal or vertical // and then choose the correct points to use for the ellipse. - - const pointsWorld: csTypes.Point3[] = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const worldPos = utilities.imageToWorldCoords(imageId, [ - GraphicData[i], - GraphicData[i + 1], - ]); - - pointsWorld.push(worldPos); - } + const pointsWorld: csTypes.Point3[] = getRenderableCoords({ + GraphicData, + ValueType, + imageId, + }); const majorAxisStart = vec3.fromValues(...pointsWorld[0]); const majorAxisEnd = vec3.fromValues(...pointsWorld[1]); @@ -202,3 +138,5 @@ function _getRenderableData(GraphicType, GraphicData, imageId) { return renderableData; } + +export default getRenderableData; diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts index c254d1758..a8660d316 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts @@ -3,14 +3,12 @@ import OHIF, { DicomMetadataStore } from '@ohif/core'; import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData'; import { adaptersSR } from '@cornerstonejs/adapters'; import { annotation as CsAnnotation } from '@cornerstonejs/tools'; -const { locking } = CsAnnotation; +import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; +const { locking } = CsAnnotation; const { guid } = OHIF.utils; const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D; - -const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; -const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; - +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const convertCode = (codingValues, code) => { diff --git a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js index d6f6a7484..31705c3a2 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js +++ b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js @@ -4,7 +4,7 @@ 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; +const CORNERSTONE_3D_TAG = adaptersSR.Cornerstone3D.CORNERSTONE_3D_TAG; /** * Checks if the given `displaySet`can be rehydrated into the `measurementService`. diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index 00fc46f92..d93aee9bb 100644 --- a/extensions/cornerstone-dynamic-volume/package.json +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -42,9 +42,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/core": "^1.82.4", + "@cornerstonejs/core": "^1.86.0", "@cornerstonejs/streaming-image-volume-loader": "^1.82.4", - "@cornerstonejs/tools": "^1.82.4", + "@cornerstonejs/tools": "^1.86.0", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 9b416e313..6d0f5524d 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -36,9 +36,9 @@ "peerDependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", - "@cornerstonejs/codec-openjpeg": "^1.2.2", - "@cornerstonejs/codec-openjph": "^2.4.2", - "@cornerstonejs/dicom-image-loader": "^1.82.4", + "@cornerstonejs/codec-openjpeg": "^1.2.4", + "@cornerstonejs/codec-openjph": "^2.4.5", + "@cornerstonejs/dicom-image-loader": "^1.86.0", "@icr/polyseg-wasm": "^0.4.0", "@ohif/core": "3.9.0-beta.98", "@ohif/ui": "3.9.0-beta.98", @@ -55,10 +55,10 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.82.4", - "@cornerstonejs/core": "^1.82.4", + "@cornerstonejs/adapters": "^1.86.0", + "@cornerstonejs/core": "^1.86.0", "@cornerstonejs/streaming-image-volume-loader": "^1.82.4", - "@cornerstonejs/tools": "^1.82.4", + "@cornerstonejs/tools": "^1.86.0", "@icr/polyseg-wasm": "^0.4.0", "@kitware/vtk.js": "30.4.1", "html2canvas": "^1.4.1", diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index ebcd9bfc8..3bee60ae6 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -610,5 +610,4 @@ OHIFCornerstoneViewport.propTypes = { // to set the initial state of the viewport's first image to render initialImageIdOrIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; - export default OHIFCornerstoneViewport; diff --git a/extensions/cornerstone/src/enums.ts b/extensions/cornerstone/src/enums.ts new file mode 100644 index 000000000..1c2285177 --- /dev/null +++ b/extensions/cornerstone/src/enums.ts @@ -0,0 +1,9 @@ +export const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; +export const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; + +const Enums = { + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION, +}; + +export default Enums; diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index e74a0de9f..bb6dc8bed 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -9,6 +9,7 @@ import { import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader'; import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; import { Types } from '@ohif/core'; +import Enums from './enums'; import init from './init'; import getCustomizationModule from './getCustomizationModule'; @@ -234,5 +235,6 @@ export { ImageOverlayViewerTool, getSOPInstanceAttributes, dicomLoaderService, + Enums, }; export default cornerstoneExtension; diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index b042d904f..19ccac95d 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -1,19 +1,17 @@ import { eventTarget } from '@cornerstonejs/core'; import { Enums, annotation } from '@cornerstonejs/tools'; import { DicomMetadataStore } from '@ohif/core'; + +import * as CSExtensionEnums from './enums'; import { toolNames } from './initCornerstoneTools'; import { onCompletedCalibrationLine } from './tools/CalibrationLineTool'; - import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; const { removeAnnotation } = annotation.state; - const csToolsEvents = Enums.Events; -const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; -const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; - const initMeasurementService = ( measurementService, displaySetService, @@ -189,11 +187,7 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana cornerstoneViewportService, customizationService ); - connectMeasurementServiceToTools( - measurementService, - cornerstoneViewportService, - csTools3DVer1MeasurementSource - ); + connectMeasurementServiceToTools(measurementService, cornerstoneViewportService); const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource; // @@ -210,7 +204,7 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana onCompletedCalibrationLine(servicesManager, csToolsEvent) .then( () => { - console.log('calibration applied'); + console.log('Calibration applied.'); }, () => true ) @@ -230,7 +224,7 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana annotationToMeasurement(toolName, annotationAddedEventDetail); } } catch (error) { - console.warn('Failed to update measurement:', error); + console.warn('Failed to add measurement:', error); } } @@ -257,6 +251,7 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana console.warn('Failed to update measurement:', error); } } + function selectMeasurement(csToolsEvent) { try { const annotationSelectionEventDetail = csToolsEvent.detail; @@ -276,7 +271,7 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana ); } } catch (error) { - console.warn('Failed to select and unselect measurements:', error); + console.warn('Failed to select/unselect measurements:', error); } } @@ -288,20 +283,13 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana */ function removeMeasurement(csToolsEvent) { try { - try { - const annotationRemovedEventDetail = csToolsEvent.detail; - const { - annotation: { annotationUID }, - } = annotationRemovedEventDetail; - - const measurement = measurementService.getMeasurement(annotationUID); - - if (measurement) { - console.log('~~ removeEvt', csToolsEvent); - remove(annotationUID, annotationRemovedEventDetail); - } - } catch (error) { - console.warn('Failed to update measurement:', error); + const annotationRemovedEventDetail = csToolsEvent.detail; + const { + annotation: { annotationUID }, + } = annotationRemovedEventDetail; + const measurement = measurementService.getMeasurement(annotationUID); + if (measurement) { + remove(annotationUID, annotationRemovedEventDetail); } } catch (error) { console.warn('Failed to remove measurement:', error); @@ -325,19 +313,10 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana return csTools3DVer1MeasurementSource; }; -const connectMeasurementServiceToTools = ( - measurementService, - cornerstoneViewportService, - measurementSource -) => { +const connectMeasurementServiceToTools = (measurementService, cornerstoneViewportService) => { const { MEASUREMENT_REMOVED, MEASUREMENTS_CLEARED, MEASUREMENT_UPDATED, RAW_MEASUREMENT_ADDED } = measurementService.EVENTS; - const csTools3DVer1MeasurementSource = measurementService.getSource( - CORNERSTONE_3D_TOOLS_SOURCE_NAME, - CORNERSTONE_3D_TOOLS_SOURCE_VERSION - ); - measurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => { if (!Object.keys(measurements).length) { return; @@ -348,7 +327,6 @@ const connectMeasurementServiceToTools = ( if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { continue; } - removeAnnotation(uid); } }); diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts index c428e8043..422c884a9 100644 --- a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts @@ -188,6 +188,12 @@ class CornerstoneCacheService { const displaySet = nonOverlayDisplaySets[0]; + if (displaySet.load && displaySet.load instanceof Function) { + const { userAuthenticationService } = this.servicesManager.services; + const headers = userAuthenticationService.getAuthorizationHeader(); + await displaySet.load({ headers }); + } + let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID); if (!stackImageIds) { diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 7fdaf65db..646c0fee4 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -404,6 +404,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi ): void { const renderingEngine = this.getRenderingEngine(); + // if not valid viewportData then return early + if (viewportData.viewportType === csEnums.ViewportType.STACK) { + // check if imageIds is valid + if (!viewportData.data[0].imageIds?.length) { + return; + } + } + // This is the old viewportInfo, which may have old options but we might be // using its viewport (same viewportId as the new viewportInfo) const viewportInfo = this.viewportsById.get(viewportId); @@ -641,6 +649,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi this._handleOverlays(viewport); + if (!imageIds?.length) { + return; + } + return viewport.setStack(imageIds, initialImageIndexToUse).then(() => { viewport.setProperties({ ...properties }); this.setPresentations(viewport.id, presentations, viewportInfo); diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts index 9ecb426f2..e6b401aff 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts @@ -19,11 +19,11 @@ const Probe = { getValueTypeFromToolType, customizationService ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; if (!metadata || !data) { - console.warn('Length tool: Missing metadata or data'); + console.warn('Probe tool: Missing metadata or data'); return null; } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js index 9f7bbb04a..a3f9c832c 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -1,4 +1,4 @@ -export default [ +const supportedTools = [ 'Length', 'EllipticalROI', 'CircleROI', @@ -11,6 +11,8 @@ export default [ 'PlanarFreehandROI', 'SplineROI', 'LivewireContour', - 'Probe', 'UltrasoundDirectionalTool', + 'SCOORD3DPoint', ]; + +export default supportedTools; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts index 0cc991fe7..041c767b6 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts @@ -94,7 +94,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - EllipticalROI: { toAnnotation: EllipticalROI.toAnnotation, toMeasurement: csToolsAnnotation => @@ -111,7 +110,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - CircleROI: { toAnnotation: CircleROI.toAnnotation, toMeasurement: csToolsAnnotation => @@ -128,7 +126,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - RectangleROI: { toAnnotation: RectangleROI.toAnnotation, toMeasurement: csToolsAnnotation => @@ -145,7 +142,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - PlanarFreehandROI: { toAnnotation: PlanarFreehandROI.toAnnotation, toMeasurement: csToolsAnnotation => @@ -162,7 +158,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - SplineROI: { toAnnotation: SplineROI.toAnnotation, toMeasurement: csToolsAnnotation => @@ -179,7 +174,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - LivewireContour: { toAnnotation: LivewireContour.toAnnotation, toMeasurement: csToolsAnnotation => @@ -196,7 +190,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - ArrowAnnotate: { toAnnotation: ArrowAnnotate.toAnnotation, toMeasurement: csToolsAnnotation => @@ -214,7 +207,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - Probe: { toAnnotation: Probe.toAnnotation, toMeasurement: csToolsAnnotation => @@ -232,7 +224,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - CobbAngle: { toAnnotation: CobbAngle.toAnnotation, toMeasurement: csToolsAnnotation => @@ -249,7 +240,6 @@ const measurementServiceMappingsFactory = ( }, ], }, - Angle: { toAnnotation: Angle.toAnnotation, toMeasurement: csToolsAnnotation => diff --git a/extensions/dicom-microscopy/package.json b/extensions/dicom-microscopy/package.json index 617af872e..604aa7ef0 100644 --- a/extensions/dicom-microscopy/package.json +++ b/extensions/dicom-microscopy/package.json @@ -45,7 +45,7 @@ "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", - "@cornerstonejs/codec-openjpeg": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", "colormap": "^2.3", "mathjs": "^12.4.2" } diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 91a778a6b..64eaa006c 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,8 +32,8 @@ "start": "yarn run dev" }, "peerDependencies": { - "@cornerstonejs/core": "^1.82.0", - "@cornerstonejs/tools": "^1.82.0", + "@cornerstonejs/core": "^1.86.0", + "@cornerstonejs/tools": "^1.86.0", "@ohif/core": "3.9.0-beta.98", "@ohif/extension-cornerstone-dicom-sr": "3.9.0-beta.98", "@ohif/ui": "3.9.0-beta.98", diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index 5fc3cd65c..09e6bb1fc 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -4,11 +4,12 @@ import { StudySummary, MeasurementTable, useViewportGrid, ActionButtons } from ' import { DicomMetadataStore, utils } from '@ohif/core'; import { useDebounce } from '@hooks'; import { useAppConfig } from '@state'; -import { useTrackedMeasurements } from '../../getContextModule'; import debounce from 'lodash.debounce'; import { useTranslation } from 'react-i18next'; import { Separator } from '@ohif/ui-next'; +import { useTrackedMeasurements } from '../../getContextModule'; + const { downloadCSVReport } = utils; const { formatDate } = utils; @@ -43,49 +44,50 @@ function PanelMeasurementTableTracking({ useEffect(() => { const measurements = measurementService.getMeasurements(); - const filteredMeasurements = measurements.filter( - m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) - ); - - const mappedMeasurements = filteredMeasurements.map(m => - _mapMeasurementToDisplay(m, measurementService.VALUE_TYPES, displaySetService) + const mappedMeasurements = measurements.map(m => + _mapMeasurementToDisplay(m, displaySetService) ); setDisplayMeasurements(mappedMeasurements); // eslint-ignore-next-line - }, [measurementService, trackedStudy, trackedSeries, debouncedMeasurementChangeTimestamp]); - - const updateDisplayStudySummary = async () => { - if (trackedMeasurements.matches('tracking')) { - const StudyInstanceUID = trackedStudy; - const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID); - const instanceMeta = studyMeta.series[0].instances[0]; - const { StudyDate, StudyDescription } = instanceMeta; - - const modalities = new Set(); - studyMeta.series.forEach(series => { - if (trackedSeries.includes(series.SeriesInstanceUID)) { - modalities.add(series.instances[0].Modality); - } - }); - const modality = Array.from(modalities).join('/'); - - if (displayStudySummary.key !== StudyInstanceUID) { - setDisplayStudySummary({ - key: StudyInstanceUID, - date: StudyDate, // TODO: Format: '07-Sep-2010' - modality, - description: StudyDescription, - }); - } - } else if (trackedStudy === '' || trackedStudy === undefined) { - setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); - } - }; + }, [ + measurementService, + displaySetService, + trackedStudy, + trackedSeries, + debouncedMeasurementChangeTimestamp, + ]); // ~~ DisplayStudySummary useEffect(() => { + const updateDisplayStudySummary = async () => { + if (trackedMeasurements.matches('tracking')) { + const StudyInstanceUID = trackedStudy; + const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID); + const instanceMeta = studyMeta.series[0].instances[0]; + const { StudyDate, StudyDescription } = instanceMeta; + + const modalities = new Set(); + studyMeta.series.forEach(series => { + if (trackedSeries.includes(series.SeriesInstanceUID)) { + modalities.add(series.instances[0].Modality); + } + }); + const modality = Array.from(modalities).join('/'); + + if (displayStudySummary.key !== StudyInstanceUID) { + setDisplayStudySummary({ + key: StudyInstanceUID, + date: StudyDate, // TODO: Format: '07-Sep-2010' + modality, + description: StudyDescription, + }); + } + } else if (trackedStudy === '' || trackedStudy === undefined) { + setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); + } + }; updateDisplayStudySummary(); - }, [displayStudySummary.key, trackedMeasurements, trackedStudy, updateDisplayStudySummary]); + }, [displayStudySummary.key, trackedSeries, trackedMeasurements, trackedStudy]); // TODO: Better way to consolidated, debounce, check on change? // Are we exposing the right API for measurementService? @@ -125,13 +127,11 @@ function PanelMeasurementTableTracking({ const trackedMeasurements = measurements.filter( m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) ); - - downloadCSVReport(trackedMeasurements, measurementService); + downloadCSVReport(trackedMeasurements); } const jumpToImage = ({ uid, isActive }) => { measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid); - onMeasurementItemClickHandler({ uid, isActive }); }; @@ -160,7 +160,6 @@ function PanelMeasurementTableTracking({ if (!isActive) { const measurements = [...displayMeasurements]; const measurement = measurements.find(m => m.uid === uid); - measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true)); measurement.isActive = true; setDisplayMeasurements(measurements); @@ -168,14 +167,25 @@ function PanelMeasurementTableTracking({ }; const displayMeasurementsWithoutFindings = displayMeasurements.filter( - dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT && dm.referencedImageId + dm => + dm.measurementType !== measurementService.VALUE_TYPES.POINT && + dm.referencedImageId && + trackedStudy === dm.referenceStudyUID && + trackedSeries.includes(dm.referenceSeriesUID) ); const additionalFindings = displayMeasurements.filter( - dm => dm.measurementType === measurementService.VALUE_TYPES.POINT && dm.referencedImageId + dm => + dm.measurementType === measurementService.VALUE_TYPES.POINT && + dm.referencedImageId && + trackedStudy === dm.referenceStudyUID && + trackedSeries.includes(dm.referenceSeriesUID) + ); + const nonAcquisitionMeasurements = displayMeasurements.filter( + dm => + dm.referencedImageId == null || + trackedStudy !== dm.referenceStudyUID || + trackedSeries.includes(dm.referenceSeriesUID) ); - - const nonAcquisitionMeasurements = displayMeasurements.filter(dm => dm.referencedImageId == null); - const disabled = additionalFindings.length === 0 && displayMeasurementsWithoutFindings.length === 0 && @@ -278,23 +288,11 @@ PanelMeasurementTableTracking.propTypes = { }; // TODO: This could be a measurementService mapper -function _mapMeasurementToDisplay(measurement, types, displaySetService) { - const { referenceStudyUID, referenceSeriesUID, SOPInstanceUID } = measurement; +function _mapMeasurementToDisplay(measurement, displaySetService) { + const { referenceSeriesUID } = measurement; // TODO: We don't deal with multiframe well yet, would need to update - // This in OHIF-312 when we add FrameIndex to measurements. - - const instance = DicomMetadataStore.getInstance( - referenceStudyUID, - referenceSeriesUID, - SOPInstanceUID - ); - - const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID); - - if (!displaySets[0]?.instances) { - throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.'); - } + // This in OHIF-312 when we add FrameIndex to measurements const { displayText: baseDisplayText, diff --git a/extensions/tmtv/src/init.js b/extensions/tmtv/src/init.js index 57ca26cd5..898d3411c 100644 --- a/extensions/tmtv/src/init.js +++ b/extensions/tmtv/src/init.js @@ -1,9 +1,14 @@ -import { addTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool } from '@cornerstonejs/tools'; +import { + addTool, + RectangleROIStartEndThresholdTool, + CircleROIStartEndThresholdTool, +} from '@cornerstonejs/tools'; +import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; -const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; -const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; + /** * * @param {Object} servicesManager @@ -17,11 +22,12 @@ export default function init({ servicesManager }) { addTool(RectangleROIStartEndThresholdTool); addTool(CircleROIStartEndThresholdTool); - const { RectangleROIStartEndThreshold, CircleROIStartEndThreshold } = measurementServiceMappingsFactory( - measurementService, - displaySetService, - cornerstoneViewportService - ); + const { RectangleROIStartEndThreshold, CircleROIStartEndThreshold } = + measurementServiceMappingsFactory( + measurementService, + displaySetService, + cornerstoneViewportService + ); const csTools3DVer1MeasurementSource = measurementService.getSource( CORNERSTONE_3D_TOOLS_SOURCE_NAME, diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js b/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js index bcd948589..d8442569d 100644 --- a/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js +++ b/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js @@ -1,8 +1,8 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; -import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { getSOPInstanceAttributes } from '@ohif/extension-cornerstone'; const CircleROIStartEndThreshold = { - toAnnotation: (measurement, definition) => { }, + toAnnotation: (measurement, definition) => {}, /** * Maps cornerstone annotation event data to measurement service format. diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js index 539f45f85..57ecd7bc0 100644 --- a/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js +++ b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js @@ -1,5 +1,5 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; -import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { getSOPInstanceAttributes } from '@ohif/extension-cornerstone'; const RectangleROIStartEndThreshold = { toAnnotation: (measurement, definition) => {}, diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js index af19d43d9..753729707 100644 --- a/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js +++ b/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -1,5 +1,5 @@ import RectangleROIStartEndThreshold from './RectangleROIStartEndThreshold'; -import CircleROIStartEndThreshold from './CircleROIStartEndThreshold' +import CircleROIStartEndThreshold from './CircleROIStartEndThreshold'; const measurementServiceMappingsFactory = ( measurementService, diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js b/extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js deleted file mode 100644 index ccd594e72..000000000 --- a/extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js +++ /dev/null @@ -1,18 +0,0 @@ -import { metaData } from '@cornerstonejs/core'; - -export default function getSOPInstanceAttributes(imageId) { - if (imageId) { - return _getUIDFromImageID(imageId); - } -} - -function _getUIDFromImageID(imageId) { - const instance = metaData.get('instance', imageId); - - return { - SOPInstanceUID: instance.SOPInstanceUID, - SeriesInstanceUID: instance.SeriesInstanceUID, - StudyInstanceUID: instance.StudyInstanceUID, - frameNumber: instance.frameNumber || 1, - }; -} diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts index 6f7622297..350a51b1f 100644 --- a/modes/basic-test-mode/src/index.ts +++ b/modes/basic-test-mode/src/index.ts @@ -162,10 +162,10 @@ function modeFactory() { namespace: dicomsr.viewport, displaySetsToDisplay: [dicomsr.sopClassHandler], }, - // { - // namespace: dicomvideo.viewport, - // displaySetsToDisplay: [dicomvideo.sopClassHandler], - // }, + { + namespace: dicomvideo.viewport, + displaySetsToDisplay: [dicomvideo.sopClassHandler], + }, { namespace: dicompdf.viewport, displaySetsToDisplay: [dicompdf.sopClassHandler], diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts index 00fda7976..fa4b90e9f 100644 --- a/modes/longitudinal/src/index.ts +++ b/modes/longitudinal/src/index.ts @@ -26,6 +26,7 @@ const tracked = { const dicomsr = { sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + sopClassHandler3D: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr-3d', viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', }; @@ -199,6 +200,7 @@ function modeFactory({ modeConfiguration }) { displaySetsToDisplay: [ ohif.sopClassHandler, dicomvideo.sopClassHandler, + dicomsr.sopClassHandler3D, ohif.wsiSopClassHandler, ], }, @@ -206,10 +208,6 @@ function modeFactory({ modeConfiguration }) { namespace: dicomsr.viewport, displaySetsToDisplay: [dicomsr.sopClassHandler], }, - // { - // namespace: dicomvideo.viewport, - // displaySetsToDisplay: [dicomvideo.sopClassHandler], - // }, { namespace: dicompdf.viewport, displaySetsToDisplay: [dicompdf.sopClassHandler], @@ -246,6 +244,7 @@ function modeFactory({ modeConfiguration }) { ohif.sopClassHandler, ohif.wsiSopClassHandler, dicompdf.sopClassHandler, + dicomsr.sopClassHandler3D, dicomsr.sopClassHandler, dicomRT.sopClassHandler, ], diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js index d50bf4fe9..20d1cd622 100644 --- a/modes/longitudinal/src/initToolGroups.js +++ b/modes/longitudinal/src/initToolGroups.js @@ -1,3 +1,5 @@ +import { toolNames as SRToolNames } from '@ohif/extension-cornerstone-dicom-sr'; + const colours = { 'viewport-0': 'rgb(200, 0, 0)', 'viewport-1': 'rgb(200, 200, 0)', @@ -89,7 +91,13 @@ function initDefaultToolGroup( { toolName: toolNames.LivewireContour }, { toolName: toolNames.WindowLevelRegion }, ], - enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }], + enabled: [ + { toolName: toolNames.ImageOverlayViewer }, + { toolName: toolNames.ReferenceLines }, + { + toolName: SRToolNames.SRSCOORD3DPoint, + }, + ], disabled: [ { toolName: toolNames.AdvancedMagnify, @@ -165,7 +173,6 @@ function initSRToolGroup(extensionManager, toolGroupService) { enabled: [ { toolName: SRToolNames.DICOMSRDisplay, - bindings: [], }, ], // disabled diff --git a/platform/app/package.json b/platform/app/package.json index 1740a2651..ae422d5e6 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -51,9 +51,9 @@ "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", - "@cornerstonejs/codec-openjpeg": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^1.77.6", + "@cornerstonejs/dicom-image-loader": "^1.86.0", "@emotion/serialize": "^1.1.3", "@ohif/core": "3.9.0-beta.98", "@ohif/extension-cornerstone": "3.9.0-beta.98", diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx index 56a150611..f520e6cd7 100644 --- a/platform/app/src/components/ViewportGrid.tsx +++ b/platform/app/src/components/ViewportGrid.tsx @@ -1,6 +1,5 @@ -import React, { useEffect, useCallback, useRef, useState } from 'react'; +import React, { useEffect, useCallback, useRef } from 'react'; import { useResizeDetector } from 'react-resize-detector'; -import PropTypes from 'prop-types'; import { Types, MeasurementService } from '@ohif/core'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import EmptyViewport from './EmptyViewport'; @@ -24,8 +23,13 @@ function ViewerViewportGrid(props: withAppTypes) { }); const layoutHash = useRef(null); - const { displaySetService, measurementService, hangingProtocolService, uiNotificationService } = - servicesManager.services; + const { + displaySetService, + measurementService, + hangingProtocolService, + cornerstoneViewportService, + uiNotificationService, + } = servicesManager.services; const generateLayoutHash = () => `${numCols}-${numRows}`; @@ -108,6 +112,10 @@ function ViewerViewportGrid(props: withAppTypes) { const _getUpdatedViewports = useCallback( (viewportId, displaySetInstanceUID) => { + if (!displaySetInstanceUID) { + return []; + } + let updatedViewports = []; try { updatedViewports = hangingProtocolService.getViewportsRequireUpdate( @@ -175,6 +183,7 @@ function ViewerViewportGrid(props: withAppTypes) { ); return; } + // Arbitrarily assign the viewport to element 0 // TODO - this should perform a search to find the most suitable viewport. updatedViewports[0] = { ...updatedViewports[0] }; diff --git a/platform/core/package.json b/platform/core/package.json index b425d1c29..c3954ba55 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -35,9 +35,9 @@ "peerDependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", - "@cornerstonejs/codec-openjpeg": "^1.2.3", + "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^1.82.0", + "@cornerstonejs/dicom-image-loader": "^1.86.0", "@ohif/ui": "3.9.0-beta.98", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21" diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 35383f89a..4ebce5d12 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -466,7 +466,6 @@ class MeasurementService extends PubSubService { } const sourceInfo = this._getSourceToString(source); - if (!this._sourceHasMappings(source)) { throw new Error(`No measurement mappings found for '${sourceInfo}' source. Exiting early.`); } diff --git a/yarn.lock b/yarn.lock index d09190de7..cfc47ed51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2428,39 +2428,13 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cornerstonejs/adapters@^1.77.6": - version "1.83.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.83.0.tgz#934c462d98b7857ef9670702547505d112fa13f8" - integrity sha512-PKNdzwI/i79Gu9ZxM4SemttCKJpSnMUSQSBu27mXzFzm5ENyOGnus3sLV5ciqZBRaHiuyteaHosJhxOavxaX1g== +"@cornerstonejs/adapters@^1.86.0": + version "1.86.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.86.0.tgz#389938f25b4b3f784c13dda46c3d2523a1db38cf" + integrity sha512-Ln8hC2W6d30q97Mhjboq57Qt5WhSl/2IG5Qf5VFRKha/k6Jgd4eP/gdhpUMxTlRZM3ibuQWwshsRZWCJl+VSgQ== dependencies: "@babel/runtime-corejs2" "^7.17.8" - "@cornerstonejs/tools" "^1.83.0" - buffer "^6.0.3" - dcmjs "^0.29.8" - gl-matrix "^3.4.3" - lodash.clonedeep "^4.5.0" - ndarray "^1.0.19" - -"@cornerstonejs/adapters@^1.81.6": - version "1.82.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.82.0.tgz#4c7ae6341ad7bcf5cf0213e5252fbac009da23b6" - integrity sha512-hcLmba3b7jC1wEorCzaS4OSFBuHz5V5I3IJW1PHbwIAWNHiuKMcC4j6ndMeQhVXHtmy56HUQP+yKAPez59k8Yw== - dependencies: - "@babel/runtime-corejs2" "^7.17.8" - "@cornerstonejs/tools" "^1.82.0" - buffer "^6.0.3" - dcmjs "^0.29.8" - gl-matrix "^3.4.3" - lodash.clonedeep "^4.5.0" - ndarray "^1.0.19" - -"@cornerstonejs/adapters@^1.82.4": - version "1.83.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.83.1.tgz#69c6bd33dd2ef3f392d4adafac89c0f50776ebf9" - integrity sha512-AQlgSMpxNfWEKHthZp050aEI1BEhgzSbc2MmX2NAvHdQglg7YYDusFxZMKXUBB3jr1X9eTP+4VcYwzp4hPyM0Q== - dependencies: - "@babel/runtime-corejs2" "^7.17.8" - "@cornerstonejs/tools" "^1.83.1" + "@cornerstonejs/tools" "^1.86.0" buffer "^6.0.3" dcmjs "^0.29.8" gl-matrix "^3.4.3" @@ -2482,7 +2456,7 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-1.2.2.tgz#ae384b149d6655e3dd6e18b9891fab479ab5e144" integrity sha512-aAUMK2958YNpOb/7G6e2/aG7hExTiFTASlMt/v90XA0pRHdWiNg5ny4S5SAju0FbIw4zcMnR0qfY+yW3VG2ivg== -"@cornerstonejs/codec-openjpeg@^1.2.2": +"@cornerstonejs/codec-openjpeg@^1.2.2", "@cornerstonejs/codec-openjpeg@^1.2.4": version "1.2.4" resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-1.2.4.tgz#c7cf67a34091eb74a6676abec80a5251c412b551" integrity sha512-UT2su6xZZnCPSuWf2ldzKa/2+guQ7BGgfBSKqxanggwJHh48gZqIAzekmsLyJHMMK5YDK+ti+fzvVJhBS3Xi/g== @@ -2492,40 +2466,7 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0" integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg== -"@cornerstonejs/core@^1.77.6", "@cornerstonejs/core@^1.83.0": - version "1.83.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.83.0.tgz#d8781fe95a404f609ebd25a3f3fd93a3a4f58c70" - integrity sha512-EEOHw+G6pjSmuPvTsANPKbY/sAuskIkt24qpuE6+1tpKMt7/1pTrJY8mVHIO4/g6cfyWJl9RO8w5nLyR77SomA== - dependencies: - "@kitware/vtk.js" "30.4.1" - comlink "^4.4.1" - detect-gpu "^5.0.22" - gl-matrix "^3.4.3" - lodash.clonedeep "4.5.0" - -"@cornerstonejs/core@^1.81.3": - version "1.81.3" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.81.3.tgz#76deaee4a2cce24db5695cb87857accea5645263" - integrity sha512-Uievs/wBpw20Xj4B+8UEjb/qe+cmSfz2oWfQzBANcWoqI5pXD77evXE0s6hORCMr4yFUmTIMbGnWwXT2fF2bYw== - dependencies: - "@kitware/vtk.js" "30.4.1" - comlink "^4.4.1" - detect-gpu "^5.0.22" - gl-matrix "^3.4.3" - lodash.clonedeep "4.5.0" - -"@cornerstonejs/core@^1.81.6", "@cornerstonejs/core@^1.82.0": - version "1.82.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.82.0.tgz#ad9169e912370a2f0c984923edd9782d9d9402ec" - integrity sha512-Pnqug+ZMPADd59CAISK7L+I2RiXlJFvdodV5CCgLkwLfqPXZv2nVyr9/BphE7gFAPhwBwKQwIr568Tuxlwim4Q== - dependencies: - "@kitware/vtk.js" "30.4.1" - comlink "^4.4.1" - detect-gpu "^5.0.22" - gl-matrix "^3.4.3" - lodash.clonedeep "4.5.0" - -"@cornerstonejs/core@^1.82.4", "@cornerstonejs/core@^1.83.1": +"@cornerstonejs/core@^1.83.1": version "1.83.1" resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.83.1.tgz#626df83e896a5e8328012c681343f1158840dfc1" integrity sha512-VuLjcBZ+qPeUh+8RDrma7SvDGMEA/kXb1nsf+1yQTJQsqJmNiSyV+Pb7q/bn4X270KtGrEiyCJSNkb7b2zvfhQ== @@ -2536,16 +2477,27 @@ gl-matrix "^3.4.3" lodash.clonedeep "4.5.0" -"@cornerstonejs/dicom-image-loader@^1.77.6": - version "1.81.3" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.81.3.tgz#8d9165b351954999bbc566d811a9eedd2363d55c" - integrity sha512-QFqr0Jq9DWvoBB1rTwI6gL4fc3pawpnG6ebDSZpz+uPmG/QcGYaeGNT+Szrcu2V/q1ByOU/Jxn8ZG5Nuo+kD6A== +"@cornerstonejs/core@^1.86.0": + version "1.86.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.86.0.tgz#19f94b8a4d37ffbd775b82f3963af5fbe38709de" + integrity sha512-yY3TC5abtU4hgcyBONQo+S0MXYgRyWzV/8bWujXilugAj5moDakex/zMc+YGIKlp4PvQBBRFjbu0H8YW3pseSw== + dependencies: + "@kitware/vtk.js" "30.4.1" + comlink "^4.4.1" + detect-gpu "^5.0.22" + gl-matrix "^3.4.3" + lodash.clonedeep "4.5.0" + +"@cornerstonejs/dicom-image-loader@^1.86.0": + version "1.86.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.86.0.tgz#19a529bf1e5d27a0a6a9d0994068b12dab5e88dc" + integrity sha512-OPZCGdtdomj3TGktATQcLVIpTVv7ZqF8EyPgkEX+eIRXILzpu5Nm6gf3Lwofa5HVFwvmoJmVnU8jToKcoKPkqQ== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" "@cornerstonejs/codec-openjpeg" "^1.2.2" "@cornerstonejs/codec-openjph" "^2.4.5" - "@cornerstonejs/core" "^1.81.3" + "@cornerstonejs/core" "^1.86.0" dicom-parser "^1.8.9" pako "^2.0.4" uuid "^9.0.0" @@ -2558,36 +2510,12 @@ "@cornerstonejs/core" "^1.83.1" comlink "^4.4.1" -"@cornerstonejs/tools@^1.82.0": - version "1.82.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.82.0.tgz#c7c9a24cc80755121c2209489e348b8c9d252d0d" - integrity sha512-1nnlloa6g5zfirAbVnZ+lfuM6DdtcN8kHkZ29xkedzpP24Le4fRHNeuNf6jTSGcgDXPhoZ+WA+q8NnH6wj1h1Q== +"@cornerstonejs/tools@^1.86.0": + version "1.86.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.86.0.tgz#ab7ca218faea4c6354866e02bcdf8005282d161c" + integrity sha512-yyy7r9Vo0NrTYYeKQtVqpbLRrVaequd9Q6aU2ifiNusdi6w+8Xslh1U/AHfw2Y9fueK8JBkLnKL+o/nLfdaOuQ== dependencies: - "@cornerstonejs/core" "^1.82.0" - "@icr/polyseg-wasm" "0.4.0" - "@types/offscreencanvas" "2019.7.3" - comlink "^4.4.1" - lodash.clonedeep "4.5.0" - lodash.get "^4.4.2" - -"@cornerstonejs/tools@^1.82.4", "@cornerstonejs/tools@^1.83.1": - version "1.83.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.83.1.tgz#56761fe339bd575b6636a4b4e12ac702ceb2a246" - integrity sha512-KbMIFgXhuMp2zl5ORLkbJY6SwioyGlDw91cU3Hu02WR2KNTbm2b8oIzTY09F4/UzI0n19064mmP5TsPMRPsjMw== - dependencies: - "@cornerstonejs/core" "^1.83.1" - "@icr/polyseg-wasm" "0.4.0" - "@types/offscreencanvas" "2019.7.3" - comlink "^4.4.1" - lodash.clonedeep "4.5.0" - lodash.get "^4.4.2" - -"@cornerstonejs/tools@^1.83.0": - version "1.83.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.83.0.tgz#6148e72972d826abfa640924f5c34f0d4acd75a3" - integrity sha512-P7b0iPGidBJ7rlfnpiIZV2jgivdVZKQ7emWd59CGN4HgEStPQOMrb6lnrDvZs1BDK2d2TjZB6vhwH0G/B/quUw== - dependencies: - "@cornerstonejs/core" "^1.83.0" + "@cornerstonejs/core" "^1.86.0" "@icr/polyseg-wasm" "0.4.0" "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1"