From e7522e7bf64fd10a4203c5da41d9a14fb45b4b0a Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Tue, 12 Dec 2023 08:40:54 -0300 Subject: [PATCH 01/21] Add docs --- .../src/getSopClassHandlerModule.ts | 124 +++++++++++++++--- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index fbeaa531643..d91c81d0345 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -10,16 +10,18 @@ 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.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 */, ]; const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; @@ -44,7 +46,7 @@ const CodeNameCodeSequenceValues = { TrackingIdentifier: '112039', Finding: '121071', FindingSite: 'G-C0E3', // SRT - CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, // + CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, }; const CodingSchemeDesignators = { @@ -116,7 +118,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) servicesManager.services.uiNotificationService.show({ title: 'DICOM SR', message: - 'OHIF only supports TID1500 Imaging Measurement Report Structured Reports. The SR you’re trying to view is not supported.', + 'OHIF only supports TID1500 Imaging Measurement Report Structured Reports. The SR you are trying to view is not supported.', type: 'warning', duration: 6000, }); @@ -150,6 +152,12 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) return [displaySet]; } +/** + * Loads the display set with the given services and extension manager. + * @param {Object} displaySet - The display set to load. + * @param {Object} servicesManager - The services manager containing displaySetService and measurementService. + * @param {Object} extensionManager - The extension manager containing data sources. + */ function _load(displaySet, servicesManager, extensionManager) { const { displaySetService, measurementService } = servicesManager.services; const dataSources = extensionManager.getDataSources(); @@ -195,13 +203,17 @@ function _load(displaySet, servicesManager, extensionManager) { }); } -function _checkIfCanAddMeasurementsToDisplaySet( - srDisplaySet, - newDisplaySet, - dataSource, - servicesManager -) { +/** + * 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, dataSource, servicesManager) { const { customizationService } = servicesManager.services; + let unloadedMeasurements = srDisplaySet.measurements.filter( measurement => measurement.loaded === false ); @@ -284,6 +296,13 @@ function _checkIfCanAddMeasurementsToDisplaySet( } } +/** + * 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; @@ -308,6 +327,14 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame } } +/** + * 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); @@ -322,6 +349,12 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) { ]; } +/** + * 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 => @@ -353,6 +386,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,6 +432,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( @@ -405,6 +453,14 @@ 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 @@ -447,6 +503,12 @@ function _processTID1410Measurement(mergedContentSequence) { 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'); @@ -532,6 +594,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { return measurement; } +/** + * Retrieves coordinates from an item of type SCOORD or SCOORD3D. + * + * @param item - The item containing the coordinates. + * @returns The coordinates extracted from the item. + */ function _getCoordsFromSCOORDOrSCOORD3D(item) { const { ValueType, RelationshipType, GraphicType, GraphicData } = item; @@ -564,6 +632,15 @@ function _getCoordsFromSCOORDOrSCOORD3D(item) { 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; @@ -577,6 +654,12 @@ function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredVal }; // 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 @@ -608,6 +691,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 []; From f9603b86995c3cbd8f28d256551aac2f90013f55 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Tue, 12 Dec 2023 14:00:50 -0300 Subject: [PATCH 02/21] Make panel work --- .../src/DICOMSRDisplayMapping.js | 122 ++++++++++++++++ .../src/getSopClassHandlerModule.ts | 132 +++++++++++++----- extensions/cornerstone-dicom-sr/src/index.tsx | 2 +- extensions/cornerstone-dicom-sr/src/init.ts | 60 +++++++- .../src/utils/addMeasurement.ts | 9 +- .../getClosestInstanceInfoRelativeToPoint.js | 87 ++++++++++++ .../src/utils/getSCOORD3DReferencedImages.js | 54 +++++++ .../src/utils/getSOPInstanceAttributes.js | 18 +++ .../src/utils/isRehydratable.js | 3 +- .../cornerstone/src/initMeasurementService.js | 11 +- .../src/getPanelModule.tsx | 1 - .../PanelMeasurementTableTracking/index.tsx | 94 +++++++------ .../MeasurementService/MeasurementService.ts | 2 + 13 files changed, 503 insertions(+), 92 deletions(-) create mode 100644 extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js create mode 100644 extensions/cornerstone-dicom-sr/src/utils/getClosestInstanceInfoRelativeToPoint.js create mode 100644 extensions/cornerstone-dicom-sr/src/utils/getSCOORD3DReferencedImages.js create mode 100644 extensions/cornerstone-dicom-sr/src/utils/getSOPInstanceAttributes.js diff --git a/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js b/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js new file mode 100644 index 00000000000..bf8c04f4225 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js @@ -0,0 +1,122 @@ +import { MeasurementService } from '@ohif/core'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const DICOMSRDisplayMapping = { + toAnnotation: () => {}, + toMeasurement: ( + csToolsEventDetail, + displaySetService, + cornerstoneViewportService, + getValueTypeFromToolType + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('DICOM SR Diaply tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = + getSOPInstanceAttributes(referencedImageId); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + const { points } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + const displayText = getDisplayText(mappedAnnotations, displaySet); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.text, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport: () => { + throw new Error('Not implemented'); + }, + }; + }, + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POINT, + points: 1, + }, + ], +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { text } = data; + const { referencedImageId } = metadata; + + const annotations = []; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = + getSOPInstanceAttributes(referencedImageId); + + const displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID, + frameNumber + ); + + const { SeriesNumber } = displaySet; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + text, + }); + + return annotations; +} + +function getDisplayText(mappedAnnotations, displaySet) { + if (!mappedAnnotations) { + return ''; + } + + const displayText = []; + + // Area is the same for all series + const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + + const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + displayText.push(`(S: ${SeriesNumber}${instanceText}${frameText})`); + + return displayText; +} + +export default DICOMSRDisplayMapping; diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index d91c81d0345..7f7e0c17546 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -3,6 +3,7 @@ import { utils, classes, DisplaySetService, Types } from '@ohif/core'; import addMeasurement from './utils/addMeasurement'; import isRehydratable from './utils/isRehydratable'; import { adaptersSR } from '@cornerstonejs/adapters'; +import getSCOORD3DReferencedImages from './utils/getSCOORD3DReferencedImages'; type InstanceMetadata = Types.InstanceMetadata; @@ -165,6 +166,9 @@ function _load(displaySet, servicesManager, extensionManager) { const { ContentSequence } = displaySet.instance; + const referencedImages1 = getSCOORD3DReferencedImages(ContentSequence, displaySet); + const referencedImages2 = _getReferencedImagesList(ContentSequence); + displaySet.referencedImages = _getReferencedImagesList(ContentSequence); displaySet.measurements = _getMeasurements(ContentSequence); @@ -223,7 +227,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat return; } - if (!newDisplaySet instanceof ImageSet) { + if (!(newDisplaySet instanceof ImageSet)) { // This also filters out _this_ displaySet, as it is not an ImageSet. return; } @@ -232,13 +236,50 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat return; } - const { sopClassUids } = newDisplaySet; + const { sopClassUids, images } = newDisplaySet; // Check if any have the newDisplaySet is the correct SOPClass. unloadedMeasurements = unloadedMeasurements.filter(measurement => - measurement.coords.some(coord => - sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID) - ) + measurement.coords.some(coord => { + /** Get image by approximation */ + if (coord.ReferencedSOPSequence === undefined) { + for (let i = 0; i < images.length; ++i) { + const imageMetadata = images[i]; + if (imageMetadata.FrameOfReferenceUID !== coord.ReferencedFrameOfReferenceSequence) { + continue; + } + + const sliceNormal = [0, 0, 0]; + const orientation = imageMetadata.ImageOrientationPatient; + sliceNormal[0] = orientation[1] * orientation[5] - orientation[2] * orientation[4]; + sliceNormal[1] = orientation[2] * orientation[3] - orientation[0] * orientation[5]; + sliceNormal[2] = orientation[0] * orientation[4] - orientation[1] * orientation[3]; + + let distanceAlongNormal = 0; + for (let j = 0; j < 3; ++j) { + distanceAlongNormal += sliceNormal[j] * imageMetadata.ImagePositionPatient[j]; + } + + // assuming 1 mm tolerance + if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) { + continue; + } + + coord.ReferencedSOPSequence = { + ReferencedSOPClassUID: imageMetadata.SOPClassUID, + ReferencedSOPInstanceUID: imageMetadata.SOPInstanceUID, + }; + + break; + } + + if (coord.ReferencedSOPSequence === undefined) { + return false; + } + } + + return sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID); + }) ); if (unloadedMeasurements.length === 0) { @@ -465,7 +506,9 @@ 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'); @@ -595,42 +638,60 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { } /** - * Retrieves coordinates from an item of type SCOORD or SCOORD3D. - * - * @param item - The item containing the coordinates. - * @returns The coordinates extracted from the item. - */ -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; - } - +// * Retrieves coordinates from an item of type SCOORD or SCOORD3D. +// * +// * @param item - The item containing the coordinates. +// * @returns The coordinates extracted from the item. +// */ +// 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; +// } + +// 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; +// } + +// return coords; +// } +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; - + const { ReferencedSOPSequence } = graphicItem.ContentSequence; coords.ReferencedSOPSequence = ReferencedSOPSequence; } else if (ValueType === 'SCOORD3D') { - const { ReferencedFrameOfReferenceSequence } = item.ContentSequence; - - coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence; + if (graphicItem.ReferencedFrameOfReferenceUID) { + coords.ReferencedFrameOfReferenceSequence = graphicItem.ReferencedFrameOfReferenceUID; + } else if (graphicItem.ContentSequence) { + const { ReferencedFrameOfReferenceSequence } = graphicItem.ContentSequence; + coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence; + } } return coords; -} +}; /** * Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence. @@ -668,6 +729,9 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup ); + if (!ImageLibraryGroup) { + return []; + } const referencedImages = []; diff --git a/extensions/cornerstone-dicom-sr/src/index.tsx b/extensions/cornerstone-dicom-sr/src/index.tsx index 9b0700481ed..b0b5e7abb1e 100644 --- a/extensions/cornerstone-dicom-sr/src/index.tsx +++ b/extensions/cornerstone-dicom-sr/src/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; -import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule'; +import { srProtocol } from './getHangingProtocolModule'; import onModeEnter from './onModeEnter'; import getCommandsModule from './commandsModule'; import preRegistration from './init'; diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index 1ab3300d778..39b45730355 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -12,14 +12,44 @@ import { } from '@cornerstonejs/tools'; import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import addToolInstance from './utils/addToolInstance'; -import { Types } from '@ohif/core'; +import { MeasurementService, Types } from '@ohif/core'; import toolNames from './tools/toolNames'; +import DICOMSRDisplayMapping from './DICOMSRDisplayMapping'; + +const _getValueTypeFromToolType = toolType => { + const { POLYLINE, ELLIPSE, CIRCLE, RECTANGLE, BIDIRECTIONAL, POINT, ANGLE } = + MeasurementService.VALUE_TYPES; + + // TODO -> I get why this was attempted, but its not nearly flexible enough. + // A single measurement may have an ellipse + a bidirectional measurement, for instances. + // You can't define a bidirectional tool as a single type.. + const TOOL_TYPE_TO_VALUE_TYPE = { + Length: POLYLINE, + EllipticalROI: ELLIPSE, + CircleROI: CIRCLE, + RectangleROI: RECTANGLE, + PlanarFreehandROI: POLYLINE, + Bidirectional: BIDIRECTIONAL, + ArrowAnnotate: POINT, + CobbAngle: ANGLE, + Angle: ANGLE, + }; + + return TOOL_TYPE_TO_VALUE_TYPE[toolType]; +}; /** * @param {object} configuration */ -export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void { +export default function init({ + servicesManager, + configuration = {}, +}: Types.Extensions.ExtensionParams): void { + const { measurementService, displaySetService, cornerstoneViewportService } = + servicesManager.services; + addTool(DICOMSRDisplayTool); + console.debug('Adding SR tool...'); addToolInstance(toolNames.SRLength, LengthTool, {}); addToolInstance(toolNames.SRBidirectional, BidirectionalTool); addToolInstance(toolNames.SREllipticalROI, EllipticalROITool); @@ -32,6 +62,32 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP // on a missing polyline. The fix is probably in CS3D addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool); + /** TODO: Get name/version from cs extension */ + const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; + const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; + const source = measurementService.getSource( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + measurementService.addMapping( + source, + 'DICOMSRDisplay', + [ + { + valueType: MeasurementService.VALUE_TYPES.POINT, + points: 1, + }, + ], + DICOMSRDisplayMapping.toAnnotation, + csToolsAnnotation => + DICOMSRDisplayMapping.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType + ) + ); + // Modify annotation tools to use dashed lines on SR const dashedLine = { lineDash: '4,4', diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index 348a7a79d85..4cb5d7f315c 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -6,8 +6,6 @@ import SCOORD_TYPES from '../constants/scoordTypes'; const EPSILON = 1e-4; -const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; - export default function addMeasurement(measurement, imageId, displaySetInstanceUID) { // TODO -> Render rotated ellipse . const toolName = toolNames.DICOMSRDisplay; @@ -27,7 +25,7 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU } measurementData.renderableData[GraphicType].push( - _getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier) + _getRenderableData(GraphicType, GraphicData, imageId) ); }); @@ -63,6 +61,7 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU }; annotationManager.addAnnotation(SRAnnotation); + console.debug('Adding annotation:', SRAnnotation); measurement.loaded = true; measurement.imageId = imageId; @@ -77,9 +76,7 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU delete measurement.coords; } -function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) { - const [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); - +function _getRenderableData(GraphicType, GraphicData, imageId) { let renderableData: csTypes.Point3[]; switch (GraphicType) { diff --git a/extensions/cornerstone-dicom-sr/src/utils/getClosestInstanceInfoRelativeToPoint.js b/extensions/cornerstone-dicom-sr/src/utils/getClosestInstanceInfoRelativeToPoint.js new file mode 100644 index 00000000000..164248ac7fd --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getClosestInstanceInfoRelativeToPoint.js @@ -0,0 +1,87 @@ +import { vec3 } from 'gl-matrix'; + +/** + * Calculates the plane normal given the image orientation vector + * @param imageOrientation + * @returns + */ +function calculatePlaneNormal(imageOrientation) { + const rowCosineVec = vec3.fromValues( + imageOrientation[0], + imageOrientation[1], + imageOrientation[2] + ); + const colCosineVec = vec3.fromValues( + imageOrientation[3], + imageOrientation[4], + imageOrientation[5] + ); + return vec3.cross(vec3.create(), rowCosineVec, colCosineVec); +} + +/** + * Calculates the minimum distance between a world point and an image plane + * @param point + * @param instance + * @returns + */ +function planeDistance(point, instance) { + const imageOrientation = instance.ImageOrientationPatient; + const imagePositionPatient = instance.ImagePositionPatient; + const scanAxisNormal = calculatePlaneNormal(imageOrientation); + const [A, B, C] = scanAxisNormal; + + const D = + -A * imagePositionPatient[0] - B * imagePositionPatient[1] - C * imagePositionPatient[2]; + + return Math.abs(A * point[0] + B * point[1] + C * point[2] + D); // Denominator is sqrt(A**2 + B**2 + C**2) which is 1 as its a normal vector +} + +/** + * Gets the closest instance of a displaySet related to a given world point + * @param targetPoint target world point + * @param displaySet displaySet to check + * @param closestInstanceInfo last closest instance + * @returns + */ +function getClosestInstanceRelativeToPoint(targetPoint, displaySet, closestInstanceInfos) { + // todo: this does not assume orientation yet, but that can be added later + const displaySetInstanceUID = displaySet.displaySetInstanceUID; + return displaySet.instances.reduce((closestInstanceInfos, instance) => { + const distance = planeDistance(targetPoint, instance); + + // the threshold is half of the slicethickness or 5 mm + const threshold = 0.1; //(instance?.SliceThickness || 5) / 2; + + if (distance < threshold) { + const closestInstanceInfo = { + distance, + instance, + displaySetInstanceUID, + }; + closestInstanceInfos.push(closestInstanceInfo); + } + return closestInstanceInfos; + }, closestInstanceInfos); +} + +/** + * Return the information of the closest instance respective to a target world point + * of all displaySets that shares a given FrameOfReferenceUID + * @param targetPoint + * @param displaySets + * @returns + */ +export default function getClosestInstanceInfoRelativeToPoint( + targetPoint, + frameOfReferenceUID, + displaySets +) { + return displaySets.reduce((closestInstanceInfos, displaySet) => { + if (displaySet.instance.FrameOfReferenceUID === frameOfReferenceUID) { + return getClosestInstanceRelativeToPoint(targetPoint, displaySet, closestInstanceInfos); + } else { + return closestInstanceInfos; + } + }, []); +} diff --git a/extensions/cornerstone-dicom-sr/src/utils/getSCOORD3DReferencedImages.js b/extensions/cornerstone-dicom-sr/src/utils/getSCOORD3DReferencedImages.js new file mode 100644 index 00000000000..75305997797 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getSCOORD3DReferencedImages.js @@ -0,0 +1,54 @@ +import getClosestInstanceInfoRelativeToPoint from './getClosestInstanceInfoRelativeToPoint'; + +/** + * Converts a value to an array. + * + * @param {*} x - The value to convert. + * @returns {Array} - The converted array. + */ +function toArray(x) { + return Array.isArray(x) ? x : [x]; +} + +/** + * Retrieves the referenced images from a measurement group. + * + * @param {Object} ContentSequence - The measurement ContentSequence. + * @param {Array} displaySets - The array of display sets. + * @returns {Array} - The array of referenced images. + */ +export function getSCOORD3DReferencedImages(ContentSequence, displaySets) { + const referencedImages = []; + const measurementGroupContentSequence = toArray(ContentSequence); + const SCOORD3DContentItems = measurementGroupContentSequence.filter( + group => group.ValueType === 'SCOORD3D' + ); + const NUMContentItems = measurementGroupContentSequence.filter( + group => group.ValueType === 'NUM' + ); + + if (!NUMContentItems.length) { + if (SCOORD3DContentItems.length) { + const frameOfReference = SCOORD3DContentItems[0].ReferencedFrameOfReferenceUID; + const closestInstanceInfos = getClosestInstanceInfoRelativeToPoint( + SCOORD3DContentItems[0].GraphicData, + frameOfReference, + displaySets + ); + + for (let i = 0; i < closestInstanceInfos.length; i++) { + const closestInstanceInfo = closestInstanceInfos[i]; + const SOPClassUID = closestInstanceInfo.instance.SOPClassUID; + const SOPInstanceUID = closestInstanceInfo.instance.SOPInstanceUID; + referencedImages.push({ + ReferencedSOPClassUID: SOPClassUID, + ReferencedSOPInstanceUID: SOPInstanceUID, + }); + } + } + } + + return referencedImages; +} + +export default getSCOORD3DReferencedImages; diff --git a/extensions/cornerstone-dicom-sr/src/utils/getSOPInstanceAttributes.js b/extensions/cornerstone-dicom-sr/src/utils/getSOPInstanceAttributes.js new file mode 100644 index 00000000000..ccd594e7242 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getSOPInstanceAttributes.js @@ -0,0 +1,18 @@ +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/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js index d6f6a748413..0d03d6349fa 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js +++ b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js @@ -1,7 +1,6 @@ import { adaptersSR } from '@cornerstonejs/adapters'; -const cornerstoneAdapters = - adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE; +const cornerstoneAdapters = adaptersSR.Cornerstone3D; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG; diff --git a/extensions/cornerstone/src/initMeasurementService.js b/extensions/cornerstone/src/initMeasurementService.js index 2ab5cce55be..16d1e9d1fd9 100644 --- a/extensions/cornerstone/src/initMeasurementService.js +++ b/extensions/cornerstone/src/initMeasurementService.js @@ -173,9 +173,10 @@ const connectToolsToMeasurementService = servicesManager => { // in the future annotationAddedEventDetail.uid = annotationUID; annotationToMeasurement(toolName, annotationAddedEventDetail); + console.debug('Adding annotation to measurement service', annotationAddedEventDetail); } } catch (error) { - console.warn('Failed to update measurement:', error); + console.error('Failed to update measurement:', error); } } @@ -199,7 +200,7 @@ const connectToolsToMeasurementService = servicesManager => { // Passing true to indicate this is an update and NOT a annotation (start) completion. annotationToMeasurement(toolName, annotationModifiedEventDetail, true); } catch (error) { - console.warn('Failed to update measurement:', error); + console.error('Failed to update measurement:', error); } } function selectMeasurement(csToolsEvent) { @@ -221,7 +222,7 @@ const connectToolsToMeasurementService = servicesManager => { ); } } catch (error) { - console.warn('Failed to select and unselect measurements:', error); + console.error('Failed to select and unselect measurements:', error); } } @@ -246,10 +247,10 @@ const connectToolsToMeasurementService = servicesManager => { remove(annotationUID, annotationRemovedEventDetail); } } catch (error) { - console.warn('Failed to update measurement:', error); + console.error('Failed to update measurement:', error); } } catch (error) { - console.warn('Failed to remove measurement:', error); + console.error('Failed to remove measurement:', error); } } diff --git a/extensions/measurement-tracking/src/getPanelModule.tsx b/extensions/measurement-tracking/src/getPanelModule.tsx index a49a44a5bf6..d78c477de23 100644 --- a/extensions/measurement-tracking/src/getPanelModule.tsx +++ b/extensions/measurement-tracking/src/getPanelModule.tsx @@ -19,7 +19,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }): servicesManager, }), }, - { name: 'trackedMeasurements', iconName: 'tab-linear', diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index 7b0ca797719..3266c4bf779 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -24,7 +24,7 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { description: '', // 'CHEST/ABD/PELVIS W CONTRAST', }; -function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { +function PanelMeasurementTableTracking({ servicesManager }) { const [viewportGrid] = useViewportGrid(); const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString()); const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200); @@ -39,49 +39,49 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { useEffect(() => { const measurements = measurementService.getMeasurements(); - const filteredMeasurements = measurements.filter( - m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) - ); - - const mappedMeasurements = filteredMeasurements.map(m => + const mappedMeasurements = measurements.map(m => _mapMeasurementToDisplay(m, measurementService.VALUE_TYPES, 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('/'); + }, [ + measurementService, + trackedStudy, + trackedSeries, + displaySetService, + debouncedMeasurementChangeTimestamp, + ]); - if (displayStudySummary.key !== StudyInstanceUID) { - setDisplayStudySummary({ - key: StudyInstanceUID, - date: StudyDate, // TODO: Format: '07-Sep-2010' - modality, - description: StudyDescription, + 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); } - } else if (trackedStudy === '' || trackedStudy === undefined) { - setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); - } - }; + }; - // ~~ DisplayStudySummary - useEffect(() => { updateDisplayStudySummary(); - }, [displayStudySummary.key, trackedMeasurements, trackedStudy, updateDisplayStudySummary]); + }, [displayStudySummary.key, trackedMeasurements, trackedSeries, trackedStudy]); // TODO: Better way to consolidated, debounce, check on change? // Are we exposing the right API for measurementService? @@ -121,13 +121,11 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { 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 }); }; @@ -199,7 +197,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { 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); @@ -207,7 +204,13 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { }; const displayMeasurementsWithoutFindings = displayMeasurements.filter( - dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT + dm => + trackedStudy === dm.referenceStudyUID && + trackedSeries.includes(dm.referenceSeriesUID) && + dm.measurementType !== measurementService.VALUE_TYPES.POINT + ); + const untrackedMeasurements = displayMeasurements.filter( + dm => trackedStudy !== dm.referenceStudyUID || !trackedSeries.includes(dm.referenceSeriesUID) ); const additionalFindings = displayMeasurements.filter( dm => dm.measurementType === measurementService.VALUE_TYPES.POINT @@ -234,6 +237,14 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { onClick={jumpToImage} onEdit={onMeasurementItemEditHandler} /> + {untrackedMeasurements.length !== 0 && ( + + )} {additionalFindings.length !== 0 && ( Date: Wed, 13 Dec 2023 15:00:08 -0300 Subject: [PATCH 03/21] Address jump to measurement by for --- extensions/cornerstone-dicom-sr/src/enums.js | 37 ++++++++ .../src/getSopClassHandlerModule.ts | 90 ++++++------------- extensions/cornerstone-dicom-sr/src/init.ts | 2 +- .../src/utils/addMeasurement.ts | 2 + .../viewports/OHIFCornerstoneSRViewport.tsx | 2 +- .../src/Viewport/OHIFCornerstoneViewport.tsx | 64 +++++++++++-- .../cornerstone/src/initMeasurementService.js | 1 - .../DisplaySetService/DisplaySetService.ts | 5 ++ 8 files changed, 129 insertions(+), 74 deletions(-) create mode 100644 extensions/cornerstone-dicom-sr/src/enums.js diff --git a/extensions/cornerstone-dicom-sr/src/enums.js b/extensions/cornerstone-dicom-sr/src/enums.js new file mode 100644 index 00000000000..77a515f5422 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/enums.js @@ -0,0 +1,37 @@ +import { adaptersSR } from '@cornerstonejs/adapters'; + +const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; + +export 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 const CodingSchemeDesignators = { + SRT: 'SRT', + CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'], +}; + +export const RELATIONSHIP_TYPE = { + INFERRED_FROM: 'INFERRED FROM', + CONTAINS: 'CONTAINS', +}; + +export const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; + +const enums = { + CodeNameCodeSequenceValues, + CodingSchemeDesignators, + RELATIONSHIP_TYPE, + CORNERSTONE_FREETEXT_CODE_VALUE, +}; + +export default enums; diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index 7f7e0c17546..83d6612e646 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -1,14 +1,19 @@ -import { SOPClassHandlerName, SOPClassHandlerId } from './id'; import { utils, classes, DisplaySetService, Types } from '@ohif/core'; +import cloneDeep from 'lodash.clonedeep'; + import addMeasurement from './utils/addMeasurement'; import isRehydratable from './utils/isRehydratable'; -import { adaptersSR } from '@cornerstonejs/adapters'; -import getSCOORD3DReferencedImages from './utils/getSCOORD3DReferencedImages'; +import { SOPClassHandlerName, SOPClassHandlerId } from './id'; +import { + CodeNameCodeSequenceValues, + CodingSchemeDesignators, + RELATIONSHIP_TYPE, + CORNERSTONE_FREETEXT_CODE_VALUE, +} from './enums'; +import getClosestInstanceInfoRelativeToPoint from './utils/getClosestInstanceInfoRelativeToPoint'; type InstanceMetadata = Types.InstanceMetadata; -const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; - const { ImageSet, MetadataProvider: metadataProvider } = classes; /** @@ -37,31 +42,6 @@ const validateSameStudyUID = (uid: string, instances): void => { }); }; -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, -}; - -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 @@ -161,14 +141,9 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) */ function _load(displaySet, servicesManager, extensionManager) { const { displaySetService, measurementService } = servicesManager.services; - const dataSources = extensionManager.getDataSources(); - const dataSource = dataSources[0]; + const dataSource = extensionManager.getActiveDataSource()[0]; const { ContentSequence } = displaySet.instance; - - const referencedImages1 = getSCOORD3DReferencedImages(ContentSequence, displaySet); - const referencedImages2 = _getReferencedImagesList(ContentSequence); - displaySet.referencedImages = _getReferencedImagesList(ContentSequence); displaySet.measurements = _getMeasurements(ContentSequence); @@ -218,17 +193,17 @@ function _load(displaySet, servicesManager, extensionManager) { function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource, servicesManager) { const { customizationService } = servicesManager.services; - let unloadedMeasurements = srDisplaySet.measurements.filter( + /** TODO: Investigate why without deepClone the measurements are not rendering */ + let unloadedMeasurements = cloneDeep(srDisplaySet.measurements).filter( measurement => measurement.loaded === false ); + /** All measurements were loaded */ if (unloadedMeasurements.length === 0) { - // All already loaded! return; } if (!(newDisplaySet instanceof ImageSet)) { - // This also filters out _this_ displaySet, as it is not an ImageSet. return; } @@ -241,7 +216,6 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat // Check if any have the newDisplaySet is the correct SOPClass. unloadedMeasurements = unloadedMeasurements.filter(measurement => measurement.coords.some(coord => { - /** Get image by approximation */ if (coord.ReferencedSOPSequence === undefined) { for (let i = 0; i < images.length; ++i) { const imageMetadata = images[i]; @@ -261,7 +235,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat } // assuming 1 mm tolerance - if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) { + if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 20) { continue; } @@ -288,29 +262,18 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat } const SOPInstanceUIDs = []; - unloadedMeasurements.forEach(measurement => { - const { coords } = measurement; - - coords.forEach(coord => { - const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID; - + measurement.coords.forEach(({ ReferencedSOPSequence }) => { + const SOPInstanceUID = 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; - } - + const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet); + for (const imageId of imageIds) { const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId); - if (SOPInstanceUIDs.includes(SOPInstanceUID)) { for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { let measurement = unloadedMeasurements[j]; @@ -329,7 +292,6 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) { addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID); - unloadedMeasurements.splice(j, 1); } } @@ -347,11 +309,14 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat 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 firstCoord = measurement.coords[0]; const ReferencedFrameNumber = - (measurement.coords[0].ReferencedSOPSequence && - measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) || + (firstCoord.ReferencedSOPSequence && + firstCoord.ReferencedSOPSequence[0]?.ReferencedFrameNumber) || 1; if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) { @@ -361,11 +326,12 @@ 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; } /** diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index 39b45730355..2306c1220e4 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -43,13 +43,13 @@ const _getValueTypeFromToolType = toolType => { */ export default function init({ servicesManager, + extensionManager, configuration = {}, }: Types.Extensions.ExtensionParams): void { const { measurementService, displaySetService, cornerstoneViewportService } = servicesManager.services; addTool(DICOMSRDisplayTool); - console.debug('Adding SR tool...'); addToolInstance(toolNames.SRLength, LengthTool, {}); addToolInstance(toolNames.SRBidirectional, BidirectionalTool); addToolInstance(toolNames.SREllipticalROI, EllipticalROITool); diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index 4cb5d7f315c..490e2985550 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -46,6 +46,8 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID, toolName: toolName, referencedImageId: imageId, + /** Use to properly jump to different viewports based on frame of reference */ + coords: measurement.coords, }, data: { label: measurement.labels, diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index 0937ca1ff10..afacd10af5d 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -101,7 +101,7 @@ function OHIFCornerstoneSRViewport(props) { measurementSelected ); }, - [element, measurementSelected, srDisplaySet] + [element, srDisplaySet] ); /** diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index 3de29dfd611..bd1f763593d 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import * as cs3DTools from '@cornerstonejs/tools'; import { Enums, + metaData, eventTarget, getEnabledElement, StackViewport, @@ -415,9 +416,9 @@ function _subscribeToJumpToMeasurementEvents( { referencedImageId: measurement.referencedImageId } ); } - if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) { - return; - } + // if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) { + // return; + // } _jumpToMeasurement( measurement, elementRef, @@ -482,7 +483,7 @@ function _jumpToMeasurement( cornerstoneViewportService ) { const targetElement = targetElementRef.current; - const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement; + const { displaySetInstanceUID, SOPInstanceUID, frameNumber, metadata } = measurement; if (!SOPInstanceUID) { console.warn('cannot jump in a non-acquisition plane measurements yet'); @@ -507,12 +508,57 @@ function _jumpToMeasurement( let viewportCameraDirectionMatch = true; if (viewport instanceof StackViewport) { + const findCOORDByFOR = (imageIds, measurement) => { + return measurement.metadata.coords.find(coord => { + for (let i = 0; i < imageIds.length; ++i) { + const imageMetadata = metaData.get('instance', imageIds[i]); + if (imageMetadata.FrameOfReferenceUID !== coord.ReferencedFrameOfReferenceSequence) { + continue; + } + + const sliceNormal = [0, 0, 0]; + const orientation = imageMetadata.ImageOrientationPatient; + sliceNormal[0] = orientation[1] * orientation[5] - orientation[2] * orientation[4]; + sliceNormal[1] = orientation[2] * orientation[3] - orientation[0] * orientation[5]; + sliceNormal[2] = orientation[0] * orientation[4] - orientation[1] * orientation[3]; + + let distanceAlongNormal = 0; + for (let j = 0; j < 3; ++j) { + distanceAlongNormal += sliceNormal[j] * imageMetadata.ImagePositionPatient[j]; + } + + // assuming 1 mm tolerance + if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 20) { + continue; + } else { + coord.ReferencedSOPSequence = { + imageIdIndex: i, + ReferencedSOPClassUID: imageMetadata.SOPClassUID, + ReferencedSOPInstanceUID: imageMetadata.SOPInstanceUID, + }; + return true; + } + } + }); + }; + const imageIds = viewport.getImageIds(); - imageIdIndex = imageIds.findIndex(imageId => { - const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } = - getSOPInstanceAttributes(imageId); - return aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber); - }); + const coord = findCOORDByFOR(imageIds, measurement); + if (coord) { + imageIdIndex = coord.ReferencedSOPSequence.imageIdIndex; + console.debug('Trying..', imageIdIndex, coord); + } + + if (imageIdIndex < 0) { + const imageIds = viewport.getImageIds(); + imageIdIndex = imageIds.findIndex(imageId => { + const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } = + getSOPInstanceAttributes(imageId); + return ( + aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber) + ); + }); + } } else { // for volume viewport we can't rely on the imageIdIndex since it can be // a reconstructed view that doesn't match the original slice numbers etc. diff --git a/extensions/cornerstone/src/initMeasurementService.js b/extensions/cornerstone/src/initMeasurementService.js index 16d1e9d1fd9..b9ed8a28796 100644 --- a/extensions/cornerstone/src/initMeasurementService.js +++ b/extensions/cornerstone/src/initMeasurementService.js @@ -173,7 +173,6 @@ const connectToolsToMeasurementService = servicesManager => { // in the future annotationAddedEventDetail.uid = annotationUID; annotationToMeasurement(toolName, annotationAddedEventDetail); - console.debug('Adding annotation to measurement service', annotationAddedEventDetail); } } catch (error) { console.error('Failed to update measurement:', error); diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.ts b/platform/core/src/services/DisplaySetService/DisplaySetService.ts index 3db737c0cbe..895c6db0486 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.ts +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.ts @@ -100,6 +100,7 @@ export default class DisplaySetService extends PubSubService { displaySetsAdded: displaySets, options: { madeInClient: displaySets[0].madeInClient }, }); + return displaySets; } @@ -115,6 +116,10 @@ export default class DisplaySetService extends PubSubService { return this.activeDisplaySets; } + public getDisplaySets() { + return [...displaySetCache.values()]; + } + public getDisplaySetsForSeries = (seriesInstanceUID: string): DisplaySet[] => { return [...displaySetCache.values()].filter( displaySet => displaySet.SeriesInstanceUID === seriesInstanceUID From 8b5b0a91d940e316323b95b2902b70bc5bdac7cb Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Wed, 13 Dec 2023 19:22:22 -0300 Subject: [PATCH 04/21] Fix labels --- .../src/DICOMSRDisplayMapping.js | 2 +- extensions/cornerstone-dicom-sr/src/enums.js | 2 + .../src/getSopClassHandlerModule.ts | 39 ++-- .../src/tools/DICOMSRDisplayTool.ts | 30 +-- .../src/utils/addMeasurement.ts | 152 +------------- .../src/utils/getRenderableData.ts | 195 ++++++++++++++++++ .../src/Viewport/OHIFCornerstoneViewport.tsx | 6 +- 7 files changed, 244 insertions(+), 182 deletions(-) create mode 100644 extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts diff --git a/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js b/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js index bf8c04f4225..16b61ecd46f 100644 --- a/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js +++ b/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js @@ -43,13 +43,13 @@ const DICOMSRDisplayMapping = { SOPInstanceUID, FrameOfReferenceUID, points, + label: data.label[0].value, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, frameNumber: mappedAnnotations[0]?.frameNumber || 1, toolName: metadata.toolName, displaySetInstanceUID: displaySet.displaySetInstanceUID, - label: data.text, displayText: displayText, data: data.cachedStats, type: getValueTypeFromToolType(toolName), diff --git a/extensions/cornerstone-dicom-sr/src/enums.js b/extensions/cornerstone-dicom-sr/src/enums.js index 77a515f5422..1d35e59afb5 100644 --- a/extensions/cornerstone-dicom-sr/src/enums.js +++ b/extensions/cornerstone-dicom-sr/src/enums.js @@ -12,11 +12,13 @@ export const CodeNameCodeSequenceValues = { TrackingIdentifier: '112039', Finding: '121071', FindingSite: 'G-C0E3', // SRT + FindingSiteSCT: '363698007', // SCT CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, }; export const CodingSchemeDesignators = { SRT: 'SRT', + SCT: 'SCT', CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'], }; diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index 83d6612e646..f30a66c8677 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -10,7 +10,6 @@ import { RELATIONSHIP_TYPE, CORNERSTONE_FREETEXT_CODE_VALUE, } from './enums'; -import getClosestInstanceInfoRelativeToPoint from './utils/getClosestInstanceInfoRelativeToPoint'; type InstanceMetadata = Types.InstanceMetadata; @@ -156,7 +155,7 @@ function _load(displaySet, servicesManager, extensionManager) { displaySet.isRehydratable = isRehydratable(displaySet, mappings); displaySet.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, @@ -166,11 +165,13 @@ function _load(displaySet, servicesManager, extensionManager) { ); }); - // 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, @@ -213,7 +214,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat const { sopClassUids, images } = newDisplaySet; - // Check if any have the newDisplaySet is the correct SOPClass. + /** Check if any have the newDisplaySet is the correct SOPClass */ unloadedMeasurements = unloadedMeasurements.filter(measurement => measurement.coords.some(coord => { if (coord.ReferencedSOPSequence === undefined) { @@ -235,7 +236,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat } // assuming 1 mm tolerance - if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 20) { + if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) { continue; } @@ -257,7 +258,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat ); if (unloadedMeasurements.length === 0) { - // New displaySet isn't the correct SOPClass, so can't contain the referenced images. + /** New displaySet isn't the correct SOPClassso can't contain the referenced images */ return; } @@ -346,7 +347,6 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) { const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; - return [ { name: SOPClassHandlerName, @@ -383,7 +383,6 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) { mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier]; const measurement = _processMeasurement(mergedContentSequence); - if (measurement) { measurements.push(measurement); } @@ -410,7 +409,6 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingUniqueIdentifier ); - if (!TrackingUniqueIdentifierItem) { console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.'); } @@ -501,7 +499,6 @@ function _processTID1410Measurement(mergedContentSequence) { NUMContentItems.forEach(item => { const { ConceptNameCodeSequence, MeasuredValueSequence } = item; - if (MeasuredValueSequence) { measurement.labels.push( _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) @@ -509,6 +506,18 @@ 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; } @@ -520,7 +529,6 @@ function _processTID1410Measurement(mergedContentSequence) { */ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); - const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF'); const TrackingIdentifierContentItem = mergedContentSequence.find( @@ -580,15 +588,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); } @@ -672,9 +677,7 @@ function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredVal const { CodeMeaning } = ConceptNameCodeSequence; const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence; const { CodeValue } = MeasurementUnitsCodeSequence; - const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : ''; - return { label: CodeMeaning, value: `${formatedNumericValue} ${CodeValue}`, diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index 7b14b107f59..8219ff0c210 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -6,8 +6,10 @@ import { utilities, Types as cs3DToolsTypes, } from '@cornerstonejs/tools'; + import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule'; import SCOORD_TYPES from '../constants/scoordTypes'; +import { CodeNameCodeSequenceValues } from '../enums'; export default class DICOMSRDisplayTool extends AnnotationTool { static toolName = 'DICOMSRDisplay'; @@ -44,29 +46,31 @@ export default class DICOMSRDisplayTool extends AnnotationTool { const { viewport } = enabledElement; const { element } = viewport; - let annotations = annotation.state.getAnnotations(this.getToolName(), element); - // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender + let annotations = annotation.state.getAnnotations(this.getToolName(), element); if (!annotations?.length) { return; } annotations = this.filterInteractableAnnotationsForElement(element, annotations); - if (!annotations?.length) { return; } + let filteredAnnotations = annotations; + let activeTrackingUniqueIdentifier; const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(element); - - const { activeIndex, trackingUniqueIdentifiers } = trackingUniqueIdentifiersForElement; - - const activeTrackingUniqueIdentifier = trackingUniqueIdentifiers[activeIndex]; - - // Filter toolData to only render the data for the active SR. - const filteredAnnotations = annotations.filter(annotation => - trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier) - ); + if ( + trackingUniqueIdentifiersForElement && + trackingUniqueIdentifiersForElement.trackingUniqueIdentifiers.length + ) { + const { activeIndex, trackingUniqueIdentifiers } = trackingUniqueIdentifiersForElement; + activeTrackingUniqueIdentifier = trackingUniqueIdentifiers[activeIndex]; + // Filter toolData to only render the data for the active SR. + filteredAnnotations = annotations.filter(annotation => + trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier) + ); + } if (!viewport._actors?.size) { return; @@ -90,6 +94,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation); const lineDash = this.getStyle('lineDash', styleSpecifier, annotation); const color = + activeTrackingUniqueIdentifier && cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier ? 'rgb(0, 255, 0)' : this.getStyle('color', styleSpecifier, annotation); @@ -379,6 +384,7 @@ const SHORT_HAND_MAP = { AREA: 'Area: ', Length: '', CORNERSTONEFREETEXT: '', + [CodeNameCodeSequenceValues.FindingSiteSCT]: '', }; function _labelToShorthand(label) { diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index 490e2985550..55a57fa2e48 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -1,10 +1,8 @@ -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 { metaData } from '@cornerstonejs/core'; -const EPSILON = 1e-4; +import getRenderableData from './getRenderableData'; +import toolNames from '../tools/toolNames'; export default function addMeasurement(measurement, imageId, displaySetInstanceUID) { // TODO -> Render rotated ellipse . @@ -25,7 +23,7 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU } measurementData.renderableData[GraphicType].push( - _getRenderableData(GraphicType, GraphicData, imageId) + getRenderableData(GraphicType, GraphicData, imageId) ); }); @@ -77,145 +75,3 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU measurement.frameNumber = frameNumber; delete measurement.coords; } - -function _getRenderableData(GraphicType, GraphicData, imageId) { - let renderableData: csTypes.Point3[]; - - 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); - } - - 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); - } - - // 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'); - } - - const { - columnCosines, - rowCosines, - }: { - columnCosines: csTypes.Point3; - rowCosines: csTypes.Point3; - } = imagePlaneModule; - - // 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); - - const secondAxisEnd = vec3.create(); - vec3.scaleAndAdd(secondAxisEnd, center, rowCosines, -radius); - - renderableData = [ - firstAxisStart as csTypes.Point3, - firstAxisEnd as csTypes.Point3, - secondAxisStart as csTypes.Point3, - secondAxisEnd as csTypes.Point3, - ]; - - break; - } - case SCOORD_TYPES.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 majorAxisStart = vec3.fromValues(...pointsWorld[0]); - const majorAxisEnd = vec3.fromValues(...pointsWorld[1]); - const minorAxisStart = vec3.fromValues(...pointsWorld[2]); - const minorAxisEnd = vec3.fromValues(...pointsWorld[3]); - - const majorAxisVec = vec3.create(); - vec3.sub(majorAxisVec, majorAxisEnd, majorAxisStart); - - // normalize majorAxisVec to avoid scaling issues - vec3.normalize(majorAxisVec, majorAxisVec); - - const minorAxisVec = vec3.create(); - vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart); - vec3.normalize(minorAxisVec, minorAxisVec); - - const imagePlaneModule = metaData.get('imagePlaneModule', imageId); - - if (!imagePlaneModule) { - throw new Error('imageId does not have imagePlaneModule metadata'); - } - - const { columnCosines }: { columnCosines: csTypes.Point3 } = imagePlaneModule; - - // find which axis is parallel to the columnCosines - const columnCosinesVec = vec3.fromValues(...columnCosines); - - const projectedMajorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, majorAxisVec)); - const projectedMinorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, minorAxisVec)); - - const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec); - const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec); - - renderableData = []; - if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) { - renderableData = [pointsWorld[0], pointsWorld[1], pointsWorld[2], pointsWorld[3]]; - } else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) { - renderableData = [pointsWorld[2], pointsWorld[3], pointsWorld[0], pointsWorld[1]]; - } else { - console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED'); - } - break; - } - default: - console.warn('Unsupported GraphicType:', GraphicType); - } - - return renderableData; -} diff --git a/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts new file mode 100644 index 00000000000..74ceae903f8 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts @@ -0,0 +1,195 @@ +import { vec3 } from 'gl-matrix'; +import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core'; +import SCOORD_TYPES from '../constants/scoordTypes'; + +const EPSILON = 1e-4; + +function imageToWorldCoords(imageId, imageCoords) { + const imagePlaneModule = metaData.get('imagePlaneModule', imageId); + + if (!imagePlaneModule) { + throw new Error(`No imagePlaneModule found for imageId: ${imageId}`); + } + + const { columnCosines, rowCosines, imagePositionPatient: origin } = imagePlaneModule; + + let { columnPixelSpacing, rowPixelSpacing } = imagePlaneModule; + // Use ||= to convert null and 0 as well as undefined to 1 + columnPixelSpacing ||= 1; + rowPixelSpacing ||= 1; + + // calculate the image coordinates in the world space + const imageCoordsInWorld = vec3.create(); + + // move from origin in the direction of the row cosines with the amount of + // row pixel spacing times the first element of the image coordinates vector + vec3.scaleAndAdd( + imageCoordsInWorld, + origin, + rowCosines, + // to accommodate the [0,0] being on the top left corner of the top left pixel + // but the origin is at the center of the top left pixel + rowPixelSpacing * (imageCoords[0] - 0.5) + ); + + vec3.scaleAndAdd( + imageCoordsInWorld, + imageCoordsInWorld, + columnCosines, + columnPixelSpacing * (imageCoords[1] - 0.5) + ); + + vec3.scaleAndAdd( + imageCoordsInWorld, + imageCoordsInWorld, + columnCosines, + columnPixelSpacing * (imageCoords[2] - 0.5) + ); + + return Array.from(imageCoordsInWorld); +} + +function getRenderableData(GraphicType, GraphicData, 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 += 3) { + const worldPos = imageToWorldCoords(imageId, [ + GraphicData[i], + GraphicData[i + 1], + GraphicData[i + 2], + ]); + + renderableData.push(worldPos); + } + + 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); + } + + // 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'); + } + + const { + columnCosines, + rowCosines, + }: { + columnCosines: csTypes.Point3; + rowCosines: csTypes.Point3; + } = imagePlaneModule; + + // 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); + + const secondAxisEnd = vec3.create(); + vec3.scaleAndAdd(secondAxisEnd, center, rowCosines, -radius); + + renderableData = [ + firstAxisStart as csTypes.Point3, + firstAxisEnd as csTypes.Point3, + secondAxisStart as csTypes.Point3, + secondAxisEnd as csTypes.Point3, + ]; + + break; + } + case SCOORD_TYPES.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 majorAxisStart = vec3.fromValues(...pointsWorld[0]); + const majorAxisEnd = vec3.fromValues(...pointsWorld[1]); + const minorAxisStart = vec3.fromValues(...pointsWorld[2]); + const minorAxisEnd = vec3.fromValues(...pointsWorld[3]); + + const majorAxisVec = vec3.create(); + vec3.sub(majorAxisVec, majorAxisEnd, majorAxisStart); + + // normalize majorAxisVec to avoid scaling issues + vec3.normalize(majorAxisVec, majorAxisVec); + + const minorAxisVec = vec3.create(); + vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart); + vec3.normalize(minorAxisVec, minorAxisVec); + + const imagePlaneModule = metaData.get('imagePlaneModule', imageId); + + if (!imagePlaneModule) { + throw new Error('imageId does not have imagePlaneModule metadata'); + } + + const { columnCosines }: { columnCosines: csTypes.Point3 } = imagePlaneModule; + + // find which axis is parallel to the columnCosines + const columnCosinesVec = vec3.fromValues(...columnCosines); + + const projectedMajorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, majorAxisVec)); + const projectedMinorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, minorAxisVec)); + + const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec); + const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec); + + renderableData = []; + if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) { + renderableData = [pointsWorld[0], pointsWorld[1], pointsWorld[2], pointsWorld[3]]; + } else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) { + renderableData = [pointsWorld[2], pointsWorld[3], pointsWorld[0], pointsWorld[1]]; + } else { + console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED'); + } + break; + } + default: + console.warn('Unsupported GraphicType:', GraphicType); + } + + return renderableData; +} + +export default getRenderableData; diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index bd1f763593d..e61d3f6af11 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -483,7 +483,7 @@ function _jumpToMeasurement( cornerstoneViewportService ) { const targetElement = targetElementRef.current; - const { displaySetInstanceUID, SOPInstanceUID, frameNumber, metadata } = measurement; + const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement; if (!SOPInstanceUID) { console.warn('cannot jump in a non-acquisition plane measurements yet'); @@ -528,7 +528,7 @@ function _jumpToMeasurement( } // assuming 1 mm tolerance - if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 20) { + if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) { continue; } else { coord.ReferencedSOPSequence = { @@ -546,7 +546,7 @@ function _jumpToMeasurement( const coord = findCOORDByFOR(imageIds, measurement); if (coord) { imageIdIndex = coord.ReferencedSOPSequence.imageIdIndex; - console.debug('Trying..', imageIdIndex, coord); + console.debug('Jumping to...', imageIdIndex, coord); } if (imageIdIndex < 0) { From a8bf58b1a7f48387f2f498c788733885a480a1fe Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Wed, 13 Dec 2023 19:27:01 -0300 Subject: [PATCH 05/21] Remove unused code --- .../src/getSopClassHandlerModule.ts | 40 +-------- .../getClosestInstanceInfoRelativeToPoint.js | 87 ------------------- .../src/utils/getSCOORD3DReferencedImages.js | 54 ------------ 3 files changed, 4 insertions(+), 177 deletions(-) delete mode 100644 extensions/cornerstone-dicom-sr/src/utils/getClosestInstanceInfoRelativeToPoint.js delete mode 100644 extensions/cornerstone-dicom-sr/src/utils/getSCOORD3DReferencedImages.js diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index f30a66c8677..dfc3748f740 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -609,42 +609,10 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { } /** -// * Retrieves coordinates from an item of type SCOORD or SCOORD3D. -// * -// * @param item - The item containing the coordinates. -// * @returns The coordinates extracted from the item. -// */ -// 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; -// } - -// 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; -// } - -// return coords; -// } + * 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 }; diff --git a/extensions/cornerstone-dicom-sr/src/utils/getClosestInstanceInfoRelativeToPoint.js b/extensions/cornerstone-dicom-sr/src/utils/getClosestInstanceInfoRelativeToPoint.js deleted file mode 100644 index 164248ac7fd..00000000000 --- a/extensions/cornerstone-dicom-sr/src/utils/getClosestInstanceInfoRelativeToPoint.js +++ /dev/null @@ -1,87 +0,0 @@ -import { vec3 } from 'gl-matrix'; - -/** - * Calculates the plane normal given the image orientation vector - * @param imageOrientation - * @returns - */ -function calculatePlaneNormal(imageOrientation) { - const rowCosineVec = vec3.fromValues( - imageOrientation[0], - imageOrientation[1], - imageOrientation[2] - ); - const colCosineVec = vec3.fromValues( - imageOrientation[3], - imageOrientation[4], - imageOrientation[5] - ); - return vec3.cross(vec3.create(), rowCosineVec, colCosineVec); -} - -/** - * Calculates the minimum distance between a world point and an image plane - * @param point - * @param instance - * @returns - */ -function planeDistance(point, instance) { - const imageOrientation = instance.ImageOrientationPatient; - const imagePositionPatient = instance.ImagePositionPatient; - const scanAxisNormal = calculatePlaneNormal(imageOrientation); - const [A, B, C] = scanAxisNormal; - - const D = - -A * imagePositionPatient[0] - B * imagePositionPatient[1] - C * imagePositionPatient[2]; - - return Math.abs(A * point[0] + B * point[1] + C * point[2] + D); // Denominator is sqrt(A**2 + B**2 + C**2) which is 1 as its a normal vector -} - -/** - * Gets the closest instance of a displaySet related to a given world point - * @param targetPoint target world point - * @param displaySet displaySet to check - * @param closestInstanceInfo last closest instance - * @returns - */ -function getClosestInstanceRelativeToPoint(targetPoint, displaySet, closestInstanceInfos) { - // todo: this does not assume orientation yet, but that can be added later - const displaySetInstanceUID = displaySet.displaySetInstanceUID; - return displaySet.instances.reduce((closestInstanceInfos, instance) => { - const distance = planeDistance(targetPoint, instance); - - // the threshold is half of the slicethickness or 5 mm - const threshold = 0.1; //(instance?.SliceThickness || 5) / 2; - - if (distance < threshold) { - const closestInstanceInfo = { - distance, - instance, - displaySetInstanceUID, - }; - closestInstanceInfos.push(closestInstanceInfo); - } - return closestInstanceInfos; - }, closestInstanceInfos); -} - -/** - * Return the information of the closest instance respective to a target world point - * of all displaySets that shares a given FrameOfReferenceUID - * @param targetPoint - * @param displaySets - * @returns - */ -export default function getClosestInstanceInfoRelativeToPoint( - targetPoint, - frameOfReferenceUID, - displaySets -) { - return displaySets.reduce((closestInstanceInfos, displaySet) => { - if (displaySet.instance.FrameOfReferenceUID === frameOfReferenceUID) { - return getClosestInstanceRelativeToPoint(targetPoint, displaySet, closestInstanceInfos); - } else { - return closestInstanceInfos; - } - }, []); -} diff --git a/extensions/cornerstone-dicom-sr/src/utils/getSCOORD3DReferencedImages.js b/extensions/cornerstone-dicom-sr/src/utils/getSCOORD3DReferencedImages.js deleted file mode 100644 index 75305997797..00000000000 --- a/extensions/cornerstone-dicom-sr/src/utils/getSCOORD3DReferencedImages.js +++ /dev/null @@ -1,54 +0,0 @@ -import getClosestInstanceInfoRelativeToPoint from './getClosestInstanceInfoRelativeToPoint'; - -/** - * Converts a value to an array. - * - * @param {*} x - The value to convert. - * @returns {Array} - The converted array. - */ -function toArray(x) { - return Array.isArray(x) ? x : [x]; -} - -/** - * Retrieves the referenced images from a measurement group. - * - * @param {Object} ContentSequence - The measurement ContentSequence. - * @param {Array} displaySets - The array of display sets. - * @returns {Array} - The array of referenced images. - */ -export function getSCOORD3DReferencedImages(ContentSequence, displaySets) { - const referencedImages = []; - const measurementGroupContentSequence = toArray(ContentSequence); - const SCOORD3DContentItems = measurementGroupContentSequence.filter( - group => group.ValueType === 'SCOORD3D' - ); - const NUMContentItems = measurementGroupContentSequence.filter( - group => group.ValueType === 'NUM' - ); - - if (!NUMContentItems.length) { - if (SCOORD3DContentItems.length) { - const frameOfReference = SCOORD3DContentItems[0].ReferencedFrameOfReferenceUID; - const closestInstanceInfos = getClosestInstanceInfoRelativeToPoint( - SCOORD3DContentItems[0].GraphicData, - frameOfReference, - displaySets - ); - - for (let i = 0; i < closestInstanceInfos.length; i++) { - const closestInstanceInfo = closestInstanceInfos[i]; - const SOPClassUID = closestInstanceInfo.instance.SOPClassUID; - const SOPInstanceUID = closestInstanceInfo.instance.SOPInstanceUID; - referencedImages.push({ - ReferencedSOPClassUID: SOPClassUID, - ReferencedSOPInstanceUID: SOPInstanceUID, - }); - } - } - } - - return referencedImages; -} - -export default getSCOORD3DReferencedImages; From 2410996dd83dd296f6647973f3af64225f4e94c8 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Wed, 13 Dec 2023 19:45:29 -0300 Subject: [PATCH 06/21] Address coordinates --- .../src/getSopClassHandlerModule.ts | 2 +- .../src/utils/addMeasurement.ts | 4 +- .../src/utils/getRenderableData.ts | 69 ++++--------------- .../src/Viewport/OHIFCornerstoneViewport.tsx | 2 +- 4 files changed, 18 insertions(+), 59 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index dfc3748f740..99de8a088ce 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -236,7 +236,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat } // assuming 1 mm tolerance - if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) { + if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 5) { continue; } diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index 55a57fa2e48..6c0b590eac2 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -16,14 +16,14 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU }; measurement.coords.forEach(coord => { - const { GraphicType, GraphicData } = coord; + const { GraphicType, GraphicData, ValueType } = coord; if (measurementData.renderableData[GraphicType] === undefined) { measurementData.renderableData[GraphicType] = []; } measurementData.renderableData[GraphicType].push( - getRenderableData(GraphicType, GraphicData, imageId) + getRenderableData(GraphicType, GraphicData, ValueType, imageId) ); }); diff --git a/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts index 74ceae903f8..57eae1b97b4 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts @@ -4,53 +4,8 @@ import SCOORD_TYPES from '../constants/scoordTypes'; const EPSILON = 1e-4; -function imageToWorldCoords(imageId, imageCoords) { - const imagePlaneModule = metaData.get('imagePlaneModule', imageId); - - if (!imagePlaneModule) { - throw new Error(`No imagePlaneModule found for imageId: ${imageId}`); - } - - const { columnCosines, rowCosines, imagePositionPatient: origin } = imagePlaneModule; - - let { columnPixelSpacing, rowPixelSpacing } = imagePlaneModule; - // Use ||= to convert null and 0 as well as undefined to 1 - columnPixelSpacing ||= 1; - rowPixelSpacing ||= 1; - - // calculate the image coordinates in the world space - const imageCoordsInWorld = vec3.create(); - - // move from origin in the direction of the row cosines with the amount of - // row pixel spacing times the first element of the image coordinates vector - vec3.scaleAndAdd( - imageCoordsInWorld, - origin, - rowCosines, - // to accommodate the [0,0] being on the top left corner of the top left pixel - // but the origin is at the center of the top left pixel - rowPixelSpacing * (imageCoords[0] - 0.5) - ); - - vec3.scaleAndAdd( - imageCoordsInWorld, - imageCoordsInWorld, - columnCosines, - columnPixelSpacing * (imageCoords[1] - 0.5) - ); - - vec3.scaleAndAdd( - imageCoordsInWorld, - imageCoordsInWorld, - columnCosines, - columnPixelSpacing * (imageCoords[2] - 0.5) - ); - - return Array.from(imageCoordsInWorld); -} - -function getRenderableData(GraphicType, GraphicData, imageId) { - let renderableData; +function getRenderableData(GraphicType, GraphicData, ValueType, imageId) { + let renderableData = []; switch (GraphicType) { case SCOORD_TYPES.POINT: @@ -58,14 +13,18 @@ function getRenderableData(GraphicType, GraphicData, imageId) { case SCOORD_TYPES.POLYLINE: renderableData = []; - for (let i = 0; i < GraphicData.length; i += 3) { - const worldPos = imageToWorldCoords(imageId, [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2], - ]); - - renderableData.push(worldPos); + 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); + } } break; diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index e61d3f6af11..f16f6a085dc 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -528,7 +528,7 @@ function _jumpToMeasurement( } // assuming 1 mm tolerance - if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) { + if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 5) { continue; } else { coord.ReferencedSOPSequence = { From b3b7411d5ff95039e3e494b9db13863230fa4863 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Thu, 14 Dec 2023 16:01:08 -0300 Subject: [PATCH 07/21] Update get rendrable data --- .../src/utils/getRenderableData.ts | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts index 57eae1b97b4..01f65209320 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts @@ -10,7 +10,7 @@ function getRenderableData(GraphicType, GraphicData, ValueType, imageId) { switch (GraphicType) { case SCOORD_TYPES.POINT: case SCOORD_TYPES.MULTIPOINT: - case SCOORD_TYPES.POLYLINE: + case SCOORD_TYPES.POLYLINE: { renderableData = []; if (ValueType === 'SCOORD3D') { @@ -28,15 +28,22 @@ function getRenderableData(GraphicType, 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); + if (ValueType === 'SCOORD3D') { + for (let i = 0; i < GraphicData.length; i += 3) { + pointsWorld.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], + ]); + pointsWorld.push(worldPos); + } } // We do not have an explicit draw circle svg helper in Cornerstone3D at @@ -93,13 +100,19 @@ function getRenderableData(GraphicType, GraphicData, ValueType, imageId) { // 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); + if (ValueType === 'SCOORD3D') { + for (let i = 0; i < GraphicData.length; i += 3) { + pointsWorld.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], + ]); + pointsWorld.push(worldPos); + } } const majorAxisStart = vec3.fromValues(...pointsWorld[0]); From 2fb8540c5ec1e5502ddc57b6168253ff71216126 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Thu, 14 Dec 2023 16:49:01 -0300 Subject: [PATCH 08/21] Update jump to measurement logic --- .../src/getSopClassHandlerModule.ts | 3 +- .../src/utils/addMeasurement.ts | 8 +- .../src/Viewport/OHIFCornerstoneViewport.tsx | 90 ++++++++----------- .../findImageIdIndexFromMeasurementByFOR.js | 45 ++++++++++ 4 files changed, 91 insertions(+), 55 deletions(-) create mode 100644 extensions/cornerstone/src/utils/findImageIdIndexFromMeasurementByFOR.js diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index 99de8a088ce..07af340f2b6 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -217,6 +217,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat /** Check if any have the newDisplaySet is the correct SOPClass */ unloadedMeasurements = unloadedMeasurements.filter(measurement => measurement.coords.some(coord => { + /** Find reference sop instance uid by frame of reference if not present */ if (coord.ReferencedSOPSequence === undefined) { for (let i = 0; i < images.length; ++i) { const imageMetadata = images[i]; @@ -235,7 +236,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat distanceAlongNormal += sliceNormal[j] * imageMetadata.ImagePositionPatient[j]; } - // assuming 1 mm tolerance + // assuming 5 mm tolerance if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 5) { continue; } diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index 6c0b590eac2..49cec607b84 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -30,8 +30,6 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU // Use the metadata provider to grab its imagePlaneModule metadata const imagePlaneModule = metaData.get('imagePlaneModule', imageId); - const annotationManager = annotation.state.getAnnotationManager(); - // Create Cornerstone3D Annotation from measurement const frameNumber = (measurement.coords[0].ReferencedSOPSequence && @@ -44,7 +42,10 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID, toolName: toolName, referencedImageId: imageId, - /** Use to properly jump to different viewports based on frame of reference */ + /** + * Used to jump to measurement using currently + * selected viewport if it shares frame of reference + */ coords: measurement.coords, }, data: { @@ -60,6 +61,7 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU }, }; + const annotationManager = annotation.state.getAnnotationManager(); annotationManager.addAnnotation(SRAnnotation); console.debug('Adding annotation:', SRAnnotation); diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index f16f6a085dc..e6e60fbe4cb 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import * as cs3DTools from '@cornerstonejs/tools'; import { Enums, - metaData, eventTarget, getEnabledElement, StackViewport, @@ -22,6 +21,7 @@ import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/ import CornerstoneServices from '../types/CornerstoneServices'; import CinePlayer from '../components/CinePlayer'; import { Types } from '@ohif/core'; +import findImageIdIndexFromMeasurementByFOR from '../utils/findImageIdIndexFromMeasurementByFOR'; const STACK = 'stack'; @@ -416,9 +416,22 @@ function _subscribeToJumpToMeasurementEvents( { referencedImageId: measurement.referencedImageId } ); } - // if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) { - // return; - // } + + const targetElement = elementRef.current; + const enabledElement = getEnabledElement(targetElement); + const viewport = enabledElement.viewport; + const { SOPInstanceUID, frameNumber } = measurement; + const imageIdIndex = findImageIdIndex(viewport, SOPInstanceUID, frameNumber); + + /** + * Only check for cached viewport if image id exists in that viewport. + * Otherwise the jump to measurement will try to load a different viewport + * by frame of reference. + */ + if (imageIdIndex >= 0 && cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) { + return; + } + _jumpToMeasurement( measurement, elementRef, @@ -473,6 +486,22 @@ function _checkForCachedJumpToMeasurementEvents( } } +/** + * Finds the index of the image ID in the viewport that matches the given SOPInstanceUID and frameNumber. + * @param viewport - The viewport object. + * @param SOPInstanceUID - The SOPInstanceUID to match. + * @param frameNumber - The frame number to match (optional). + * @returns The index of the matching image ID, or -1 if not found. + */ +function findImageIdIndex(viewport, SOPInstanceUID, frameNumber) { + const imageIds = viewport.getImageIds(); + return imageIds.findIndex(imageId => { + const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } = + getSOPInstanceAttributes(imageId); + return aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber); + }); +} + function _jumpToMeasurement( measurement, targetElementRef, @@ -508,56 +537,15 @@ function _jumpToMeasurement( let viewportCameraDirectionMatch = true; if (viewport instanceof StackViewport) { - const findCOORDByFOR = (imageIds, measurement) => { - return measurement.metadata.coords.find(coord => { - for (let i = 0; i < imageIds.length; ++i) { - const imageMetadata = metaData.get('instance', imageIds[i]); - if (imageMetadata.FrameOfReferenceUID !== coord.ReferencedFrameOfReferenceSequence) { - continue; - } - - const sliceNormal = [0, 0, 0]; - const orientation = imageMetadata.ImageOrientationPatient; - sliceNormal[0] = orientation[1] * orientation[5] - orientation[2] * orientation[4]; - sliceNormal[1] = orientation[2] * orientation[3] - orientation[0] * orientation[5]; - sliceNormal[2] = orientation[0] * orientation[4] - orientation[1] * orientation[3]; - - let distanceAlongNormal = 0; - for (let j = 0; j < 3; ++j) { - distanceAlongNormal += sliceNormal[j] * imageMetadata.ImagePositionPatient[j]; - } - - // assuming 1 mm tolerance - if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 5) { - continue; - } else { - coord.ReferencedSOPSequence = { - imageIdIndex: i, - ReferencedSOPClassUID: imageMetadata.SOPClassUID, - ReferencedSOPInstanceUID: imageMetadata.SOPInstanceUID, - }; - return true; - } - } - }); - }; - - const imageIds = viewport.getImageIds(); - const coord = findCOORDByFOR(imageIds, measurement); - if (coord) { - imageIdIndex = coord.ReferencedSOPSequence.imageIdIndex; - console.debug('Jumping to...', imageIdIndex, coord); - } + imageIdIndex = findImageIdIndex(viewport, SOPInstanceUID, frameNumber); + /** If could not find image id index using sop instance try frame of reference */ if (imageIdIndex < 0) { const imageIds = viewport.getImageIds(); - imageIdIndex = imageIds.findIndex(imageId => { - const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } = - getSOPInstanceAttributes(imageId); - return ( - aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber) - ); - }); + const imageIdIndexFound = findImageIdIndexFromMeasurementByFOR(imageIds, measurement); + if (imageIdIndexFound) { + imageIdIndex = imageIdIndexFound; + } } } else { // for volume viewport we can't rely on the imageIdIndex since it can be diff --git a/extensions/cornerstone/src/utils/findImageIdIndexFromMeasurementByFOR.js b/extensions/cornerstone/src/utils/findImageIdIndexFromMeasurementByFOR.js new file mode 100644 index 00000000000..49a17487981 --- /dev/null +++ b/extensions/cornerstone/src/utils/findImageIdIndexFromMeasurementByFOR.js @@ -0,0 +1,45 @@ +import { metaData } from '@cornerstonejs/core'; + +/** + * Finds the index of the image ID from the given array of image IDs based on the measurement data. + * @param {string[]} imageIds - The array of image IDs. + * @param {object} measurement - The measurement object. + * @returns {number} - The index of the image ID, or -1 if not found. + */ +const findImageIdIndexFromMeasurementByFOR = (imageIds, measurement) => { + let imageIdIndex = -1; + measurement.metadata.coords.forEach(coord => { + for (let i = 0; i < imageIds.length; ++i) { + const imageMetadata = metaData.get('instance', imageIds[i]); + if (imageMetadata.FrameOfReferenceUID !== coord.ReferencedFrameOfReferenceSequence) { + continue; + } + + const sliceNormal = [0, 0, 0]; + const orientation = imageMetadata.ImageOrientationPatient; + sliceNormal[0] = orientation[1] * orientation[5] - orientation[2] * orientation[4]; + sliceNormal[1] = orientation[2] * orientation[3] - orientation[0] * orientation[5]; + sliceNormal[2] = orientation[0] * orientation[4] - orientation[1] * orientation[3]; + + let distanceAlongNormal = 0; + for (let j = 0; j < 3; ++j) { + distanceAlongNormal += sliceNormal[j] * imageMetadata.ImagePositionPatient[j]; + } + + // assuming 5 mm tolerance + if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 5) { + continue; + } else { + coord.ReferencedSOPSequence = { + ReferencedSOPClassUID: imageMetadata.SOPClassUID, + ReferencedSOPInstanceUID: imageMetadata.SOPInstanceUID, + }; + imageIdIndex = i; + i = imageIds.length; + } + } + }); + return imageIdIndex; +}; + +export default findImageIdIndexFromMeasurementByFOR; From 42514701208eeb5fe76dc7fbca57664bcefc02f6 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Thu, 14 Dec 2023 17:08:11 -0300 Subject: [PATCH 09/21] Pass appConfig to onModeEnter --- platform/app/src/routes/Mode/Mode.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/app/src/routes/Mode/Mode.tsx b/platform/app/src/routes/Mode/Mode.tsx index 000a28fed53..c6f93389cfd 100644 --- a/platform/app/src/routes/Mode/Mode.tsx +++ b/platform/app/src/routes/Mode/Mode.tsx @@ -341,6 +341,7 @@ export default function ModeRoute({ servicesManager, extensionManager, commandsManager, + appConfig, }); /** From c56dc6d0fb0e5832182a13f4e4d6493c3dee9236 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 15 Dec 2023 16:32:17 -0300 Subject: [PATCH 10/21] Address SR loading for OHIF master --- .../src/DICOMSRDisplayMapping.js | 17 ++++++------ .../src/getSopClassHandlerModule.ts | 11 +++++--- extensions/cornerstone-dicom-sr/src/init.ts | 27 ++----------------- .../src/tools/DICOMSRDisplayTool.ts | 3 ++- .../src/tools/toolNames.ts | 4 +-- .../src/utils/addMeasurement.ts | 13 ++++++--- .../src/Viewport/OHIFCornerstoneViewport.tsx | 2 +- .../PanelMeasurementTableTracking/index.tsx | 11 -------- 8 files changed, 33 insertions(+), 55 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js b/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js index 16b61ecd46f..de99ccfa466 100644 --- a/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js +++ b/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayMapping.js @@ -1,14 +1,15 @@ import { MeasurementService } from '@ohif/core'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +const getValueTypeByGraphicType = graphicType => { + const { POINT } = MeasurementService.VALUE_TYPES; + const GRAPHIC_TYPE_TO_VALUE_TYPE = { POINT: POINT }; + return GRAPHIC_TYPE_TO_VALUE_TYPE[graphicType]; +}; + const DICOMSRDisplayMapping = { toAnnotation: () => {}, - toMeasurement: ( - csToolsEventDetail, - displaySetService, - cornerstoneViewportService, - getValueTypeFromToolType - ) => { + toMeasurement: (csToolsEventDetail, displaySetService) => { const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; @@ -17,7 +18,7 @@ const DICOMSRDisplayMapping = { return null; } - const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const { graphicType, referencedImageId, FrameOfReferenceUID } = metadata; const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes(referencedImageId); @@ -52,7 +53,7 @@ const DICOMSRDisplayMapping = { displaySetInstanceUID: displaySet.displaySetInstanceUID, displayText: displayText, data: data.cachedStats, - type: getValueTypeFromToolType(toolName), + type: getValueTypeByGraphicType(graphicType), getReport: () => { throw new Error('Not implemented'); }, diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index 07af340f2b6..4fff656c1d1 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -1,5 +1,4 @@ import { utils, classes, DisplaySetService, Types } from '@ohif/core'; -import cloneDeep from 'lodash.clonedeep'; import addMeasurement from './utils/addMeasurement'; import isRehydratable from './utils/isRehydratable'; @@ -191,11 +190,16 @@ function _load(displaySet, servicesManager, extensionManager) { * @param dataSource - The data source used to retrieve image IDs. * @param servicesManager - The services manager. */ -function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource, servicesManager) { +function _checkIfCanAddMeasurementsToDisplaySet( + srDisplaySet, + newDisplaySet, + dataSource, + servicesManager +) { const { customizationService } = servicesManager.services; /** TODO: Investigate why without deepClone the measurements are not rendering */ - let unloadedMeasurements = cloneDeep(srDisplaySet.measurements).filter( + let unloadedMeasurements = srDisplaySet.measurements.filter( measurement => measurement.loaded === false ); @@ -241,6 +245,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat continue; } + measurement.loadedByFOR = true; coord.ReferencedSOPSequence = { ReferencedSOPClassUID: imageMetadata.SOPClassUID, ReferencedSOPInstanceUID: imageMetadata.SOPInstanceUID, diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index 2306c1220e4..d9ab58a9fc9 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -16,28 +16,6 @@ import { MeasurementService, Types } from '@ohif/core'; import toolNames from './tools/toolNames'; import DICOMSRDisplayMapping from './DICOMSRDisplayMapping'; -const _getValueTypeFromToolType = toolType => { - const { POLYLINE, ELLIPSE, CIRCLE, RECTANGLE, BIDIRECTIONAL, POINT, ANGLE } = - MeasurementService.VALUE_TYPES; - - // TODO -> I get why this was attempted, but its not nearly flexible enough. - // A single measurement may have an ellipse + a bidirectional measurement, for instances. - // You can't define a bidirectional tool as a single type.. - const TOOL_TYPE_TO_VALUE_TYPE = { - Length: POLYLINE, - EllipticalROI: ELLIPSE, - CircleROI: CIRCLE, - RectangleROI: RECTANGLE, - PlanarFreehandROI: POLYLINE, - Bidirectional: BIDIRECTIONAL, - ArrowAnnotate: POINT, - CobbAngle: ANGLE, - Angle: ANGLE, - }; - - return TOOL_TYPE_TO_VALUE_TYPE[toolType]; -}; - /** * @param {object} configuration */ @@ -71,7 +49,7 @@ export default function init({ ); measurementService.addMapping( source, - 'DICOMSRDisplay', + toolNames.DICOMSRDisplay, [ { valueType: MeasurementService.VALUE_TYPES.POINT, @@ -83,8 +61,7 @@ export default function init({ DICOMSRDisplayMapping.toMeasurement( csToolsAnnotation, displaySetService, - cornerstoneViewportService, - _getValueTypeFromToolType + cornerstoneViewportService ) ); diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index 8219ff0c210..5a0e656b714 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -10,9 +10,10 @@ import { import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule'; import SCOORD_TYPES from '../constants/scoordTypes'; import { CodeNameCodeSequenceValues } from '../enums'; +import toolNames from './toolNames'; export default class DICOMSRDisplayTool extends AnnotationTool { - static toolName = 'DICOMSRDisplay'; + static toolName = toolNames.DICOMSRDisplay; constructor( toolProps = {}, diff --git a/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts index 2e52ec111cf..fcf629c8c09 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', diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index 49cec607b84..c89a06da773 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -41,6 +41,8 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU metadata: { FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID, toolName: toolName, + valueType: measurement.coords[0].ValueType, + graphicType: measurement.coords[0].GraphicType, referencedImageId: imageId, /** * Used to jump to measurement using currently @@ -65,15 +67,20 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU annotationManager.addAnnotation(SRAnnotation); console.debug('Adding annotation:', SRAnnotation); - measurement.loaded = true; measurement.imageId = imageId; measurement.displaySetInstanceUID = displaySetInstanceUID; - // Remove the unneeded coord now its processed, but keep the SOPInstanceUID. // NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame, // It'd be super weird if it didn't anyway as a SCOORD. measurement.ReferencedSOPInstanceUID = measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID; measurement.frameNumber = frameNumber; - delete measurement.coords; + + /** This way we create measurements per display set (by FOR) and not only for the one that has referenced image */ + measurement.loaded = false; + if (measurement.loadedByFOR === true) { + measurement.coords.forEach(coord => { + delete coord.ReferencedSOPSequence; + }); + } } diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index e6e60fbe4cb..c881fc97667 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -543,7 +543,7 @@ function _jumpToMeasurement( if (imageIdIndex < 0) { const imageIds = viewport.getImageIds(); const imageIdIndexFound = findImageIdIndexFromMeasurementByFOR(imageIds, measurement); - if (imageIdIndexFound) { + if (imageIdIndexFound > -1) { imageIdIndex = imageIdIndexFound; } } diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index 3266c4bf779..2328ef239ca 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -209,9 +209,6 @@ function PanelMeasurementTableTracking({ servicesManager }) { trackedSeries.includes(dm.referenceSeriesUID) && dm.measurementType !== measurementService.VALUE_TYPES.POINT ); - const untrackedMeasurements = displayMeasurements.filter( - dm => trackedStudy !== dm.referenceStudyUID || !trackedSeries.includes(dm.referenceSeriesUID) - ); const additionalFindings = displayMeasurements.filter( dm => dm.measurementType === measurementService.VALUE_TYPES.POINT ); @@ -237,14 +234,6 @@ function PanelMeasurementTableTracking({ servicesManager }) { onClick={jumpToImage} onEdit={onMeasurementItemEditHandler} /> - {untrackedMeasurements.length !== 0 && ( - - )} {additionalFindings.length !== 0 && ( Date: Fri, 15 Dec 2023 16:47:46 -0300 Subject: [PATCH 11/21] Clean up --- .../PanelMeasurementTableTracking/index.tsx | 94 +++++++++++-------- .../DisplaySetService/DisplaySetService.ts | 5 - .../MeasurementService/MeasurementService.ts | 2 - 3 files changed, 55 insertions(+), 46 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index 2328ef239ca..bfefcb99e3b 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -24,7 +24,7 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { description: '', // 'CHEST/ABD/PELVIS W CONTRAST', }; -function PanelMeasurementTableTracking({ servicesManager }) { +function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { const [viewportGrid] = useViewportGrid(); const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString()); const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200); @@ -39,49 +39,46 @@ function PanelMeasurementTableTracking({ servicesManager }) { useEffect(() => { const measurements = measurementService.getMeasurements(); + const mappedMeasurements = measurements.map(m => _mapMeasurementToDisplay(m, measurementService.VALUE_TYPES, displaySetService) ); setDisplayMeasurements(mappedMeasurements); - }, [ - measurementService, - trackedStudy, - trackedSeries, - displaySetService, - debouncedMeasurementChangeTimestamp, - ]); + // eslint-ignore-next-line + }, [measurementService, trackedStudy, trackedSeries, debouncedMeasurementChangeTimestamp]); - 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, - }); + 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); } - } else if (trackedStudy === '' || trackedStudy === undefined) { - setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); + }); + 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); + } + }; + // ~~ DisplayStudySummary + useEffect(() => { updateDisplayStudySummary(); - }, [displayStudySummary.key, trackedMeasurements, trackedSeries, trackedStudy]); + }, [displayStudySummary.key, trackedMeasurements, trackedStudy, updateDisplayStudySummary]); // TODO: Better way to consolidated, debounce, check on change? // Are we exposing the right API for measurementService? @@ -121,11 +118,13 @@ function PanelMeasurementTableTracking({ servicesManager }) { const trackedMeasurements = measurements.filter( m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) ); - downloadCSVReport(trackedMeasurements); + + downloadCSVReport(trackedMeasurements, measurementService); } const jumpToImage = ({ uid, isActive }) => { measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid); + onMeasurementItemClickHandler({ uid, isActive }); }; @@ -197,6 +196,7 @@ function PanelMeasurementTableTracking({ servicesManager }) { 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); @@ -210,7 +210,13 @@ function PanelMeasurementTableTracking({ servicesManager }) { dm.measurementType !== measurementService.VALUE_TYPES.POINT ); const additionalFindings = displayMeasurements.filter( - dm => dm.measurementType === measurementService.VALUE_TYPES.POINT + dm => + trackedStudy === dm.referenceStudyUID && + trackedSeries.includes(dm.referenceSeriesUID) && + dm.measurementType === measurementService.VALUE_TYPES.POINT + ); + const untracked = displayMeasurements.filter( + dm => trackedStudy !== dm.referenceStudyUID && !trackedSeries.includes(dm.referenceSeriesUID) ); return ( @@ -243,6 +249,15 @@ function PanelMeasurementTableTracking({ servicesManager }) { onEdit={onMeasurementItemEditHandler} /> )} + {untracked.length !== 0 && ( + + )}
{ return [...displaySetCache.values()].filter( displaySet => displaySet.SeriesInstanceUID === seriesInstanceUID diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 03bd7644e7a..7d253ecffb3 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)) { - this.unmappedMeasurements.add(sourceAnnotationDetail.uid); throw new Error(`No measurement mappings found for '${sourceInfo}' source. Exiting early.`); } @@ -478,7 +477,6 @@ class MeasurementService extends PubSubService { ); if (!sourceMapping) { console.log('No source mapping', source); - this.unmappedMeasurements.add(sourceAnnotationDetail.uid); return; } const { toMeasurementSchema } = sourceMapping; From 6f74fc1b24746c2ebc799848662ee42012570cb2 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 8 Jan 2024 16:21:54 -0300 Subject: [PATCH 12/21] Update manager to add annotation --- extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index c89a06da773..576221e29fb 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -63,8 +63,7 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU }, }; - const annotationManager = annotation.state.getAnnotationManager(); - annotationManager.addAnnotation(SRAnnotation); + annotation.state.addAnnotation(SRAnnotation); console.debug('Adding annotation:', SRAnnotation); measurement.imageId = imageId; From 2d920a96256f01cb4b02c5c7c087fa16c376d8e9 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Wed, 15 May 2024 19:03:17 -0300 Subject: [PATCH 13/21] Revert config changes --- platform/app/public/config/default.js | 173 +++++++++----------------- 1 file changed, 60 insertions(+), 113 deletions(-) diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js index e0badb44c35..e242501ece4 100644 --- a/platform/app/public/config/default.js +++ b/platform/app/public/config/default.js @@ -1,22 +1,6 @@ window.config = { routerBasename: '/', - whiteLabeling: { - createLogoComponentFn: function (React) { - return React.createElement( - 'a', - { - target: '_self', - rel: 'noopener noreferrer', - className: 'text-purple-600 line-through', - href: '/', - }, - React.createElement('img', { - src: '/assets/idc.svg', - className: 'w-14 h-14', - }) - ); - }, - }, + // whiteLabeling: {}, extensions: [], modes: [], customizationService: {}, @@ -37,8 +21,7 @@ window.config = { prefetch: 25, }, // filterQueryParam: false, - defaultDataSourceName: 'idc-dicomweb', - defaultGCPDataSourceName: 'idc-gcp-dicomweb', + defaultDataSourceName: 'dicomweb', /* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */ // dangerouslyUseDynamicConfig: { // enabled: true, @@ -49,120 +32,50 @@ window.config = { // // regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/ // regex: /.*/, // }, - oidc: [ - { - authority: 'https://accounts.google.com', - client_id: '370953977065-o32uf5cn5f4bovtogdu862mlnhbcv9hk.apps.googleusercontent.com', - redirect_uri: '/callback', - response_type: 'id_token token', - scope: - 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', - post_logout_redirect_uri: '/logout-redirect.html', - revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', - automaticSilentRenew: true, - revokeAccessTokenOnSignout: true, - }, - ], dataSources: [ { - friendlyName: 'Primary GCP DICOMWeb Server', - namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', - sourceName: 'idc-gcp-dicomweb', - configuration: { - name: 'idc-gcp-dicomweb', - qidoSupportsIncludeField: false, - imageRendering: 'wadors', - thumbnailRendering: 'wadors', - enableStudyLazyLoad: true, - supportsFuzzyMatching: false, - supportsWildcard: false, - singlepart: 'bulkdata,video,pdf', - useBulkDataURI: false, - onConfiguration: (dicomWebConfig, options) => { - const { params } = options; - const { project, location, dataset, dicomStore } = params; - const pathUrl = `https://healthcare.googleapis.com/v1/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}/dicomWeb`; - return { - ...dicomWebConfig, - wadoRoot: pathUrl, - qidoRoot: pathUrl, - wadoUri: pathUrl, - wadoUriRoot: pathUrl, - }; - }, - }, - }, - { - friendlyName: 'Secondary GCP DICOMWeb Server', namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', - sourceName: 'gcp', + sourceName: 'dicomweb', configuration: { - name: 'gcp', + friendlyName: 'AWS S3 Static wado server', + name: 'aws', + wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', + qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', + wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', qidoSupportsIncludeField: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, supportsFuzzyMatching: false, - supportsWildcard: false, - singlepart: 'bulkdata,video,pdf', - useBulkDataURI: false, - onConfiguration: (dicomWebConfig, options) => { - const extractParams = url => ({ - project: url.split('projects/')[1].split('/')[0], - location: url.split('locations/')[1].split('/')[0], - dataset: url.split('datasets/')[1].split('/')[0], - dicomStore: url.split('dicomStores/')[1].split('/')[0], - }); - const { query } = options; - const gcp = query.get('gcp'); - if (gcp) { - const { project, location, dataset, dicomStore } = extractParams(gcp); - const pathUrl = `https://healthcare.googleapis.com/v1/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}/dicomWeb`; - return { - ...dicomWebConfig, - wadoRoot: pathUrl, - qidoRoot: pathUrl, - wadoUri: pathUrl, - wadoUriRoot: pathUrl, - qidoSupportsIncludeField: false, - imageRendering: 'wadors', - thumbnailRendering: 'wadors', - enableStudyLazyLoad: true, - supportsFuzzyMatching: false, - supportsWildcard: false, - singlepart: 'bulkdata,video,pdf', - useBulkDataURI: false, - bulkDataURI: undefined, - }; - } + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', }, + omitQuotationForMultipartRequest: true, }, }, { - friendlyName: 'dcmjs DICOMWeb Server', namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', - sourceName: 'idc-dicomweb', + sourceName: 'dicomweb2', configuration: { - name: 'idc-dicomweb', - wadoUriRoot: - 'https://proxy.imaging.datacommons.cancer.gov/current/viewer-only-no-downloads-see-tinyurl-dot-com-slash-3j3d9jyp/dicomWeb', - qidoRoot: - 'https://proxy.imaging.datacommons.cancer.gov/current/viewer-only-no-downloads-see-tinyurl-dot-com-slash-3j3d9jyp/dicomWeb', - wadoRoot: - 'https://proxy.imaging.datacommons.cancer.gov/current/viewer-only-no-downloads-see-tinyurl-dot-com-slash-3j3d9jyp/dicomWeb', - wadoUriRoot: - 'https://testing-proxy.canceridc.dev/current/viewer-only-no-downloads-see-tinyurl-dot-com-slash-3j3d9jyp/dicomWeb', - qidoRoot: - 'https://testing-proxy.canceridc.dev/current/viewer-only-no-downloads-see-tinyurl-dot-com-slash-3j3d9jyp/dicomWeb', - wadoRoot: - 'https://testing-proxy.canceridc.dev/current/viewer-only-no-downloads-see-tinyurl-dot-com-slash-3j3d9jyp/dicomWeb', + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb', + qidoRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb', + wadoRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb', qidoSupportsIncludeField: false, supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, supportsFuzzyMatching: false, - supportsWildcard: false, + supportsWildcard: true, staticWado: true, singlepart: 'bulkdata,video', // whether the data source should use retrieveBulkData to grab metadata, @@ -183,6 +96,21 @@ window.config = { name: 'dicomwebproxy', }, }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, ], httpErrorHandler: error => { // This is 429 when rejected from the public idc sandbox too often. @@ -191,6 +119,25 @@ window.config = { // Could use services manager here to bring up a dialog/modal if needed. console.warn('test, navigate to https://ohif.org/'); }, + // whiteLabeling: { + // /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ + // createLogoComponentFn: function (React) { + // return React.createElement( + // 'a', + // { + // target: '_self', + // rel: 'noopener noreferrer', + // className: 'text-purple-600 line-through', + // href: '/', + // }, + // React.createElement('img', + // { + // src: './assets/customLogo.svg', + // className: 'w-8 h-8', + // } + // )) + // }, + // }, hotkeys: [ { commandName: 'incrementActiveViewport', @@ -284,4 +231,4 @@ window.config = { keys: ['9'], }, ], -}; +}; \ No newline at end of file From d1c0e22f0354a0cd7b7b5154459033c169cd6545 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Wed, 15 May 2024 19:05:26 -0300 Subject: [PATCH 14/21] Revert extension config --- platform/app/.webpack/webpack.pwa.js | 2 -- platform/app/pluginConfig.json | 8 -------- 2 files changed, 10 deletions(-) diff --git a/platform/app/.webpack/webpack.pwa.js b/platform/app/.webpack/webpack.pwa.js index 13af88b093b..c07fdfb366c 100644 --- a/platform/app/.webpack/webpack.pwa.js +++ b/platform/app/.webpack/webpack.pwa.js @@ -69,8 +69,6 @@ module.exports = (env, argv) => { // Hoisted Yarn Workspace Modules path.resolve(__dirname, '../../../node_modules'), SRC_DIR, - path.resolve(__dirname, '../../idc/ohif-gcp-mode/node_modules'), - path.resolve(__dirname, '../../idc/ohif-gcp-extension/node_modules'), ], }, plugins: [ diff --git a/platform/app/pluginConfig.json b/platform/app/pluginConfig.json index f6686ba01e1..68a835853d9 100644 --- a/platform/app/pluginConfig.json +++ b/platform/app/pluginConfig.json @@ -57,10 +57,6 @@ "default": false, "version": "3.0.0" }, - { - "packageName": "ohif-gcp-extension", - "version": "0.0.1" - } ], "modes": [ { @@ -88,10 +84,6 @@ "default": false, "version": "3.0.0" }, - { - "packageName": "ohif-gcp-mode", - "version": "0.0.2" - } ], "public": [ { From 5833e3821ee20b88de6d351c21247abc02dfd16a Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 27 May 2024 16:39:13 -0300 Subject: [PATCH 15/21] Revert plugin json config --- platform/app/pluginConfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/app/pluginConfig.json b/platform/app/pluginConfig.json index 68a835853d9..a910bee9fbf 100644 --- a/platform/app/pluginConfig.json +++ b/platform/app/pluginConfig.json @@ -56,7 +56,7 @@ "packageName": "@ohif/extension-cornerstone-dicom-rt", "default": false, "version": "3.0.0" - }, + } ], "modes": [ { @@ -83,7 +83,7 @@ "packageName": "@ohif/mode-basic-dev-mode", "default": false, "version": "3.0.0" - }, + } ], "public": [ { From 941d828760942d94184a17d416efaf43b80ed73b Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 27 May 2024 17:12:59 -0300 Subject: [PATCH 16/21] Revert some measurement tracking table updates --- .../PanelMeasurementTableTracking/index.tsx | 50 +++++++++---------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index e95c815bf39..eed4f8fba0e 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -25,7 +25,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200); const { measurementService, - viewportGridService, uiDialogService, displaySetService, customizationService, @@ -55,34 +54,31 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi // ~~ 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; + 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, - }); + const modalities = new Set(); + studyMeta.series.forEach(series => { + if (trackedSeries.includes(series.SeriesInstanceUID)) { + modalities.add(series.instances[0].Modality); } - } else if (trackedStudy === '' || trackedStudy === undefined) { - setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); + }); + const modality = Array.from(modalities).join('/'); + + if (displayStudySummary.key !== StudyInstanceUID) { + setDisplayStudySummary({ + key: StudyInstanceUID, + date: StudyDate, // TODO: Format: '07-Sep-2010' + modality, + description: StudyDescription, + }); } - }; - updateDisplayStudySummary(); + } else if (trackedStudy === '' || trackedStudy === undefined) { + setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); + } }, [displayStudySummary.key, trackedMeasurements, trackedStudy, trackedSeries]); // TODO: Better way to consolidated, debounce, check on change? @@ -124,7 +120,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) ); - downloadCSVReport(trackedMeasurements, measurementService); + downloadCSVReport(trackedMeasurements); } const jumpToImage = ({ uid, isActive }) => { From c1d82571daf4e5e71cd61bb7b347fe99683afe89 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 27 May 2024 17:16:30 -0300 Subject: [PATCH 17/21] Revert viewport grid service updates --- .../src/services/ViewportGridService/ViewportGridService.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts index 966704bfd3f..60309eb0972 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.ts +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts @@ -124,12 +124,6 @@ class ViewportGridService extends PubSubService { }); } - public getViewport(viewportId) { - const state = this.getState(); - const viewport = state.viewports.get(viewportId); - return viewport; - } - /** * Retrieves the display set instance UIDs for a given viewport. * @param viewportId The ID of the viewport. From 4528712aabe92a76a58105bd34f9d58255ab7665 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 27 May 2024 17:38:51 -0300 Subject: [PATCH 18/21] Revert isRehydratable changes --- extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js index 0d03d6349fa..31705c3a216 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js +++ b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js @@ -1,9 +1,10 @@ import { adaptersSR } from '@cornerstonejs/adapters'; -const cornerstoneAdapters = adaptersSR.Cornerstone3D; +const cornerstoneAdapters = + adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; -const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG; +const CORNERSTONE_3D_TAG = adaptersSR.Cornerstone3D.CORNERSTONE_3D_TAG; /** * Checks if the given `displaySet`can be rehydrated into the `measurementService`. From c5c7f7ce963c9477878f2e97efa7817a1ba4f705 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Tue, 28 May 2024 08:42:13 -0300 Subject: [PATCH 19/21] Fix SR style and rollback tracking panel changes --- extensions/cornerstone-dicom-sr/src/init.ts | 2 +- extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts | 2 ++ .../src/panels/PanelMeasurementTableTracking/index.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index 4758764e76e..486102647d0 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -62,7 +62,7 @@ export default function init({ lineDash: '4,4', }; annotation.config.style.setToolGroupToolStyles('default', { - [toolNames.DICOMSRDisplay]: dashedLine, + [toolNames.DICOMSRDisplay]: {}, }); annotation.config.style.setToolGroupToolStyles('SRToolGroup', { [toolNames.DICOMSRDisplay]: dashedLine, diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index fafdeaafb47..5b24cd3c0be 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -218,6 +218,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { const handleGroupUID = '0'; drawing.drawHandles(svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, { color: options.color, + lineDash: options.lineDash, }); }); } @@ -268,6 +269,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { { color: options.color, width: options.lineWidth, + lineDash: options.lineDash, } ); }); diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index eed4f8fba0e..a7aa7c16acc 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -316,4 +316,4 @@ function _mapMeasurementToDisplay(measurement, types, displaySetService) { }; } -export default PanelMeasurementTableTracking; +export default PanelMeasurementTableTracking; \ No newline at end of file From b6cafc7db9c6b0089a01d4200a66f86ef7472a6b Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Tue, 28 May 2024 09:21:26 -0300 Subject: [PATCH 20/21] Rollback tracking panel changes --- .../PanelMeasurementTableTracking/index.tsx | 61 +++++++------------ 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index a7aa7c16acc..7d734c917e7 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -23,12 +23,8 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi const { t } = useTranslation('MeasurementTable'); const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString()); const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200); - const { - measurementService, - uiDialogService, - displaySetService, - customizationService, - } = servicesManager.services; + const { measurementService, uiDialogService, displaySetService, customizationService } = + servicesManager.services; const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); const { trackedStudy, trackedSeries } = trackedMeasurements.context; const [displayStudySummary, setDisplayStudySummary] = useState( @@ -40,20 +36,18 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi useEffect(() => { const measurements = measurementService.getMeasurements(); - const mappedMeasurements = measurements.map(m => + const filteredMeasurements = measurements.filter( + m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) + ); + + const mappedMeasurements = filteredMeasurements.map(m => _mapMeasurementToDisplay(m, measurementService.VALUE_TYPES, displaySetService) ); setDisplayMeasurements(mappedMeasurements); - }, [ - measurementService, - displaySetService, - trackedStudy, - trackedSeries, - debouncedMeasurementChangeTimestamp, - ]); + // eslint-ignore-next-line + }, [measurementService, trackedStudy, trackedSeries, debouncedMeasurementChangeTimestamp]); - // ~~ DisplayStudySummary - useEffect(() => { + const updateDisplayStudySummary = async () => { if (trackedMeasurements.matches('tracking')) { const StudyInstanceUID = trackedStudy; const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID); @@ -79,7 +73,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi } else if (trackedStudy === '' || trackedStudy === undefined) { setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); } - }, [displayStudySummary.key, trackedMeasurements, trackedStudy, trackedSeries]); + }; + + // ~~ DisplayStudySummary + useEffect(() => { + updateDisplayStudySummary(); + }, [displayStudySummary.key, trackedMeasurements, trackedStudy, updateDisplayStudySummary]); // TODO: Better way to consolidated, debounce, check on change? // Are we exposing the right API for measurementService? @@ -120,11 +119,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) ); - downloadCSVReport(trackedMeasurements); + downloadCSVReport(trackedMeasurements, measurementService); } const jumpToImage = ({ uid, isActive }) => { measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid); + onMeasurementItemClickHandler({ uid, isActive }); }; @@ -153,6 +153,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi 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); @@ -160,19 +161,10 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi }; const displayMeasurementsWithoutFindings = displayMeasurements.filter( - dm => - trackedStudy === dm.referenceStudyUID && - trackedSeries.includes(dm.referenceSeriesUID) && - dm.measurementType !== measurementService.VALUE_TYPES.POINT + dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT ); const additionalFindings = displayMeasurements.filter( - dm => - trackedStudy === dm.referenceStudyUID && - trackedSeries.includes(dm.referenceSeriesUID) && - dm.measurementType === measurementService.VALUE_TYPES.POINT - ); - const untrackableFindings = displayMeasurements.filter( - dm => trackedStudy !== dm.referenceStudyUID && !trackedSeries.includes(dm.referenceSeriesUID) + dm => dm.measurementType === measurementService.VALUE_TYPES.POINT ); const disabled = @@ -208,15 +200,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi onEdit={onMeasurementItemEditHandler} /> )} - {untrackableFindings.length !== 0 && ( - - )}
{!appConfig?.disableEditing && (
@@ -311,8 +294,6 @@ function _mapMeasurementToDisplay(measurement, types, displaySetService) { isActive: selected, finding, findingSites, - referenceStudyUID, - referenceSeriesUID, }; } From 6033d3eda8363c6a26e56ea1002ed420e9cea5a7 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Tue, 28 May 2024 13:35:00 -0300 Subject: [PATCH 21/21] CR Update --- .../src/DICOMSRDisplayPoint.js | 2 +- .../src/utils/getSOPInstanceAttributes.js | 18 ------------------ extensions/cornerstone/src/index.tsx | 3 +++ .../RectangleROIStartEndThreshold.js | 2 +- .../utils/getSOPInstanceAttributes.js | 18 ------------------ 5 files changed, 5 insertions(+), 38 deletions(-) delete mode 100644 extensions/cornerstone-dicom-sr/src/utils/getSOPInstanceAttributes.js delete mode 100644 extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js diff --git a/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayPoint.js b/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayPoint.js index 8f4b333e4fc..676534fbc47 100644 --- a/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayPoint.js +++ b/extensions/cornerstone-dicom-sr/src/DICOMSRDisplayPoint.js @@ -1,5 +1,5 @@ import { MeasurementService } from '@ohif/core'; -import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { getSOPInstanceAttributes } from '@ohif/extension-cornerstone'; const getValueTypeByGraphicType = graphicType => { const { POINT } = MeasurementService.VALUE_TYPES; diff --git a/extensions/cornerstone-dicom-sr/src/utils/getSOPInstanceAttributes.js b/extensions/cornerstone-dicom-sr/src/utils/getSOPInstanceAttributes.js deleted file mode 100644 index ccd594e7242..00000000000 --- a/extensions/cornerstone-dicom-sr/src/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/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 748d579426e..126de33cfa8 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -37,6 +37,9 @@ import ViewportActionCornersService from './services/ViewportActionCornersServic import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider'; import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel'; +import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; +export { getSOPInstanceAttributes }; + const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader; const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {}; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js index 539f45f853b..57ecd7bc016 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/utils/getSOPInstanceAttributes.js b/extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js deleted file mode 100644 index ccd594e7242..00000000000 --- 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, - }; -}