diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index dcec2f4b121..aafa33ed418 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -511,6 +511,10 @@ function _processTID1410Measurement(mergedContentSequence) { const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); + const finding = mergedContentSequence.find( + item => item.ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.Finding + ); + const { ConceptNameCodeSequence: conceptNameItem } = graphicItem; const { CodeValue: graphicValue, CodingSchemeDesignator: graphicDesignator } = conceptNameItem; const graphicCode = `${graphicDesignator}:${graphicValue}`; @@ -547,12 +551,18 @@ function _processTID1410Measurement(mergedContentSequence) { item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT ); if (findingSites.length) { + const siteItem = findingSites[0]; + const conceptName = siteItem.ConceptNameCodeSequence; measurement.labels.push({ - label: CodeNameCodeSequenceValues.FindingSiteSCT, - value: findingSites[0].ConceptCodeSequence.CodeMeaning, + label: conceptName?.CodeMeaning || 'Finding Site', + value: siteItem.ConceptCodeSequence.CodeMeaning, }); } + const measurementWithFinding = measurement as Record; + measurementWithFinding.srFinding = finding?.ConceptCodeSequence; + measurementWithFinding.srFindingSites = findingSites.map(fs => fs.ConceptCodeSequence).filter(Boolean); + return measurement; } @@ -660,6 +670,10 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { } }); + const measurementWithFinding = measurement as Record; + measurementWithFinding.srFinding = finding?.ConceptCodeSequence; + measurementWithFinding.srFindingSites = findingSites.map(fs => fs.ConceptCodeSequence).filter(Boolean); + return measurement; } diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index cf0ec0911b1..fa751a1a066 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -1,4 +1,5 @@ import { + addTool, AngleTool, annotation, ArrowAnnotateTool, @@ -15,6 +16,8 @@ import { Types } from '@ohif/core'; import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import addToolInstance from './utils/addToolInstance'; import toolNames from './tools/toolNames'; +import SRPointTool from './tools/SRPointTool'; +import { getSRRectangleROITextLines } from './utils/srToolGetTextLines'; /** * @param {object} configuration @@ -31,17 +34,22 @@ export default function init({ addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool); addToolInstance(toolNames.SRAngle, AngleTool); addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool); - addToolInstance(toolNames.SRRectangleROI, RectangleROITool); - // TODO - fix the SR display of Cobb Angle, as it joins the two lines + /** SR subtypes: show label (e.g. Lesion) instead of intensity/stats */ + addTool(SRPointTool); + addToolInstance(toolNames.SRRectangleROI, RectangleROITool, { + getTextLines: getSRRectangleROITextLines, + }); + + /** TODO - fix the SR display of Cobb Angle, as it joins the two lines */ addToolInstance(toolNames.SRCobbAngle, CobbAngleTool); - // Modify annotation tools to use dashed lines on SR const dashedLine = { lineDash: '4,4', }; annotation.config.style.setToolGroupToolStyles('SRToolGroup', { [toolNames.DICOMSRDisplay]: dashedLine, + [toolNames.SRPoint]: dashedLine, SRLength: dashedLine, SRBidirectional: dashedLine, SREllipticalROI: dashedLine, diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index 60d1c471418..a45a55425d7 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -25,12 +25,29 @@ export default class DICOMSRDisplayTool extends AnnotationTool { _getTextBoxLinesFromLabels(labels) { // TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this! + if (!labels?.length) { + return []; + } + const labelLength = Math.min(labels.length, 5); const lines = []; for (let i = 0; i < labelLength; i++) { const labelEntry = labels[i]; - lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`); + const shorthand = _labelToShorthand(labelEntry.label); + const value = labelEntry.value ?? ''; + + /** + * Empty shorthand (CORNERSTONEFREETEXT, Length, etc.): show value only — avoids ": text" or + * "363698007: site" when label was a raw code (fixed at source for finding site). + */ + if (shorthand === '' && value !== '') { + lines.push(String(value)); + } else if (shorthand === '') { + continue; + } else { + lines.push(`${shorthand}: ${value}`); + } } return lines; @@ -225,55 +242,41 @@ export default class DICOMSRDisplayTool extends AnnotationTool { options ) { const canvasCoordinates = []; + const crossSize = 6; + renderableData.map((data, index) => { const point = data[0]; - // This gives us one point for arrow + const [canvasX, canvasY] = viewport.worldToCanvas(point); canvasCoordinates.push(viewport.worldToCanvas(point)); if (data[1] !== undefined) { canvasCoordinates.push(viewport.worldToCanvas(data[1])); + drawing.drawArrow( + svgDrawingHelper, + annotationUID, + `arrow-${index}`, + canvasCoordinates[canvasCoordinates.length - 1], + canvasCoordinates[canvasCoordinates.length - 2], + { color: options.color, width: options.lineWidth } + ); + } else { + const cx = Number(canvasX); + const cy = Number(canvasY); + const crossPaths = [ + [[cx, cy - crossSize], [cx, cy + crossSize]], + [[cx - crossSize, cy], [cx + crossSize, cy]], + ]; + drawing.drawPath( + svgDrawingHelper, + annotationUID, + `cross-${index}`, + crossPaths, + { color: options.color, lineWidth: options.lineWidth || 2 } + ); } - else{ - // We get the other point for the arrow by using the image size - const imagePixelModule = metaData.get('imagePixelModule', referencedImageId); - - let xOffset = 10; - let yOffset = 10; - - if (imagePixelModule) { - const { columns, rows } = imagePixelModule; - xOffset = columns / 10; - yOffset = rows / 10; - } - - const imagePoint = csUtils.worldToImageCoords(referencedImageId, point); - const arrowEnd = csUtils.imageToWorldCoords(referencedImageId, [ - imagePoint[0] + xOffset, - imagePoint[1] + yOffset, - ]); - - canvasCoordinates.push(viewport.worldToCanvas(arrowEnd)); - - } - - - const arrowUID = `${index}`; - - // Todo: handle drawing probe as probe, currently we are drawing it as an arrow - drawing.drawArrow( - svgDrawingHelper, - annotationUID, - arrowUID, - canvasCoordinates[1], - canvasCoordinates[0], - { - color: options.color, - width: options.lineWidth, - } - ); }); - return canvasCoordinates; // used for drawing textBox + return canvasCoordinates; } renderEllipse( @@ -343,15 +346,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool { } const { annotationUID, data = {} } = annotation; - const { labels } = data; + const { labels, label } = data; const { color } = options; let adaptedCanvasCoordinates = canvasCoordinates; - // adapt coordinates if there is an adapter + if (typeof canvasCoordinatesAdapter === 'function') { adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates); } - const textLines = this._getTextBoxLinesFromLabels(labels); + + const textLines = + typeof label === 'string' && label.length > 0 + ? [label] + : this._getTextBoxLinesFromLabels(labels); const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates); if (!annotation.data?.handles?.textBox?.worldPosition) { @@ -374,6 +381,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { { ...textBoxOptions, color, + padding: textBoxOptions.padding ?? 6, } ); diff --git a/extensions/cornerstone-dicom-sr/src/tools/SRPointTool.ts b/extensions/cornerstone-dicom-sr/src/tools/SRPointTool.ts new file mode 100644 index 00000000000..bff21e89c6c --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/tools/SRPointTool.ts @@ -0,0 +1,118 @@ +import type { Types } from '@cornerstonejs/core'; +import { ProbeTool, drawing, annotation } from '@cornerstonejs/tools'; + +import type { SVGDrawingHelper } from '@cornerstonejs/tools'; +import type { StyleSpecifier } from '@cornerstonejs/tools'; +import type { ProbeAnnotation } from '@cornerstonejs/tools'; + +import { getSRPointTextLines } from '../utils/srToolGetTextLines'; + +const { drawPath, drawTextBox } = drawing; +const { getAnnotations } = annotation.state; +const { isAnnotationVisible } = annotation.visibility; + +const CROSS_SIZE = 6; + +/** + * SRPoint: sub-type of Probe for hydrated DICOM SR SCOORD/SCOORD3D points. + * Renders a cross (plus) marker per medical imaging convention; + * shows label only, no intensity/coordinates. + */ +class SRPointTool extends ProbeTool { + static toolName = 'SRPoint'; + + constructor(toolProps, defaultToolProps) { + super(toolProps, { + ...defaultToolProps, + configuration: { + ...defaultToolProps?.configuration, + getTextLines: getSRPointTextLines, + handleRadius: 6, + textCanvasOffset: { x: 4, y: 4 }, + }, + }); + } + + renderAnnotation = ( + enabledElement: Types.IEnabledElement, + svgDrawingHelper: SVGDrawingHelper + ): boolean => { + let renderStatus = false; + const { viewport } = enabledElement; + const { element } = viewport; + + const annotations = getAnnotations(this.getToolName(), element) as ProbeAnnotation[]; + if (!annotations?.length) { + return renderStatus; + } + + const filteredAnnotations = this.filterInteractableAnnotationsForElement( + element, + annotations + ); + if (!filteredAnnotations?.length) { + return renderStatus; + } + + const styleSpecifier: StyleSpecifier = { + toolGroupId: this.toolGroupId, + toolName: this.getToolName(), + viewportId: enabledElement.viewport.id, + }; + + for (const annotation of filteredAnnotations) { + const annotationUID = annotation.annotationUID; + const data = annotation.data; + const point = data.handles.points[0]; + const [canvasX, canvasY] = viewport.worldToCanvas(point); + + if (!isAnnotationVisible(annotationUID)) { + continue; + } + + styleSpecifier.annotationUID = annotationUID; + const { color, lineWidth } = this.getAnnotationStyle({ + annotation, + styleSpecifier, + }); + + const size = Number(this.configuration?.handleRadius) || CROSS_SIZE; + const cx = Number(canvasX); + const cy = Number(canvasY); + const crossPaths: [number, number][][] = [ + [[cx, cy - size], [cx, cy + size]], + [[cx - size, cy], [cx + size, cy]], + ]; + drawPath(svgDrawingHelper, annotationUID, 'cross', crossPaths, { + color, + lineWidth: lineWidth || 2, + }); + + renderStatus = true; + + const options = this.getLinkedTextBoxStyle(styleSpecifier, annotation); + if (options?.visibility !== false) { + const targetId = this.getTargetId(viewport, data); + const textLines = getSRPointTextLines(data, targetId); + if (textLines?.length) { + const textCanvasCoordinates = [ + cx + (this.configuration?.textCanvasOffset?.x ?? 4), + cy + (this.configuration?.textCanvasOffset?.y ?? 4), + ]; + drawTextBox( + svgDrawingHelper, + annotationUID, + '0', + textLines, + [textCanvasCoordinates[0], textCanvasCoordinates[1]], + { ...options, padding: options.padding ?? 4 } + ); + } + } + } + + return renderStatus; + }; +} + +export default SRPointTool; diff --git a/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts index fcf629c8c09..f10a938d7a7 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts @@ -9,6 +9,7 @@ const toolNames = { SRCobbAngle: 'SRCobbAngle', SRRectangleROI: 'SRRectangleROI', SRPlanarFreehandROI: 'SRPlanarFreehandROI', + SRPoint: 'SRPoint', }; export default toolNames; diff --git a/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts b/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts index c24105192d8..51423cc47f4 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts @@ -1,59 +1,20 @@ import { Types, annotation } from '@cornerstonejs/tools'; + +const { state: annotationState, locking } = annotation; import { metaData } from '@cornerstonejs/core'; import { adaptersSR } from '@cornerstonejs/adapters'; import getRenderableData from './getRenderableData'; +import getLabelForSRMeasurement from './getLabelForSRMeasurement'; import toolNames from '../tools/toolNames'; const { MeasurementReport } = adaptersSR.Cornerstone3D; /** - * Adds a DICOM SR (Structured Report) annotation to the annotation manager. - * This function processes measurement data from DICOM SR and converts it into - * a format suitable for display in the Cornerstone3D viewer. - * - * @param {Object} params - The parameters object - * @param {Object} params.measurement - The DICOM SR measurement data containing coordinates, labels, and metadata - * @param {Array} params.measurement.coords - Array of coordinate objects with GraphicType, ValueType, and other properties - * @param {string} params.measurement.TrackingUniqueIdentifier - Unique identifier for the measurement - * @param {string} params.measurement.TrackingIdentifier - Tracking identifier for adapter lookup - * @param {Array} [params.measurement.labels] - Optional array of label objects - * @param {string} [params.measurement.displayText] - Optional display text for the annotation - * @param {Object} [params.measurement.textBox] - Optional text box configuration - * @param {string|null} [params.imageId] - Optional image ID for the referenced image (defaults to null) - * @param {number|null} [params.frameNumber] - Optional frame number for multi-frame images (defaults to null) - * @param {Object} params.displaySet - The display set containing the image - * @param {string} params.displaySet.displaySetInstanceUID - Unique identifier for the display set - * @returns {void} - * - * @example - * ```typescript - * addSRAnnotation({ - * measurement: { - * TrackingUniqueIdentifier: '1.2.3.4.5', - * TrackingIdentifier: 'POINT', - * coords: [{ - * GraphicType: 'POINT', - * ValueType: 'SCOORD', - * // ... other coordinate properties - * }], - * labels: [{ value: 'Measurement Point' }], - * displayText: 'Point measurement' - * }, - * imageId: 'wadouri:file://path/to/image.dcm', // Optional - * frameNumber: 0, // Optional - * displaySet: { displaySetInstanceUID: '1.2.3.4' } - * }); - * ``` + * Adds a DICOM SR annotation to the annotation manager for preview before hydration. */ export default function addSRAnnotation({ measurement, imageId = null, frameNumber = null, displaySet }) { - /** @type {string} The tool name to use for the annotation, defaults to DICOMSRDisplay */ - let toolName = toolNames.DICOMSRDisplay; - - /** - * @type {Object} Renderable data organized by graphic type - * Groups coordinate data by GraphicType for efficient rendering - */ + const toolName = toolNames.DICOMSRDisplay; const renderableData = measurement.coords.reduce((acc, coordProps) => { acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || []; acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId })); @@ -64,23 +25,14 @@ export default function addSRAnnotation({ measurement, imageId = null, frameNumb const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0]; const graphicTypePoints = renderableData[graphicType]; - /** - * TODO: Read the tool name from the DICOM SR identification type in the future. - */ let frameOfReferenceUID = null; let planeRestriction = null; - /** - * Store the view reference for use in initial navigation - */ if (imageId) { const imagePlaneModule = metaData.get('imagePlaneModule', imageId); frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID; } - /** - * Store the view reference for use in initial navigation - */ if (valueType === 'SCOORD3D') { frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence; planeRestriction = { @@ -89,23 +41,16 @@ export default function addSRAnnotation({ measurement, imageId = null, frameNumb }; } - /** - * Store the view reference for use in initial navigation - */ measurement.viewReference = { planeRestriction, FrameOfReferenceUID: frameOfReferenceUID, referencedImageId: imageId, }; - /** - * @type {Types.Annotation} The annotation object to be added to the annotation manager - * Contains all necessary metadata and data for rendering the DICOM SR measurement - */ const SRAnnotation: Types.Annotation = { annotationUID: TrackingUniqueIdentifier, highlighted: false, - isLocked: false, + isLocked: true, isPreview: toolName === toolNames.DICOMSRDisplay, invalidated: false, metadata: { @@ -118,7 +63,7 @@ export default function addSRAnnotation({ measurement, imageId = null, frameNumb displaySetInstanceUID: displaySet.displaySetInstanceUID, }, data: { - label: measurement.labels?.[0]?.value || undefined, + label: getLabelForSRMeasurement(measurement) ?? measurement.labels?.[0]?.value, displayText: measurement.displayText || undefined, handles: { textBox: measurement.textBox ?? {}, @@ -132,12 +77,6 @@ export default function addSRAnnotation({ measurement, imageId = null, frameNumb }, }; - /** - * Add the annotation to the annotation state manager. - * Note: Using annotation.state.addAnnotation() instead of annotationManager.addAnnotation() - * because the latter was not triggering annotation_added events properly. - * - * @param {Types.Annotation} SRAnnotation - The annotation to add - */ - annotation.state.addAnnotation(SRAnnotation); + annotationState.addAnnotation(SRAnnotation); + locking.setAnnotationLocked(TrackingUniqueIdentifier, true); } diff --git a/extensions/cornerstone-dicom-sr/src/utils/getLabelForSRMeasurement.js b/extensions/cornerstone-dicom-sr/src/utils/getLabelForSRMeasurement.js new file mode 100644 index 00000000000..a3f5c7cf139 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getLabelForSRMeasurement.js @@ -0,0 +1,30 @@ +import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData'; + +/** + * Single display label for SR preview annotations, matching hydrate + measurement panel: + * free-text finding site before free-text finding before coded finding (see getLabelFromDCMJSImportedToolData). + */ +export default function getLabelForSRMeasurement(measurement) { + if (!measurement) { + return undefined; + } + const { srFinding, srFindingSites, labels } = measurement; + if (srFinding || (srFindingSites && srFindingSites.length)) { + const fromConcepts = getLabelFromDCMJSImportedToolData({ + annotation: { data: {} }, + finding: srFinding, + findingSites: srFindingSites || [], + }); + if (fromConcepts) { + return fromConcepts; + } + } + return _labelFromFindingSiteLine(labels); +} + +function _labelFromFindingSiteLine(labels) { + const siteLine = labels?.find( + e => typeof e?.label === 'string' && /finding site/i.test(e.label) && e.value + ); + return siteLine ? String(siteLine.value) : undefined; +} diff --git a/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js b/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js index d44f498f264..6e8593bad35 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js +++ b/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js @@ -3,29 +3,30 @@ import { adaptersSR } from '@cornerstonejs/adapters'; const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; /** - * Extracts the label from the toolData imported from dcmjs. We need to do this - * as dcmjs does not depeend on OHIF/the measurementService, it just produces data for cornestoneTools. - * This optional data is available for the consumer to process if they wish to. - * @param {object} toolData The tooldata relating to the - * - * @returns {string} The extracted label. + * Display label for SR annotations. Per TID 1500 and issue #107, show Finding + * CodeMeaning (e.g. "Lesion"). Priority: annotation label, free-text finding + * site, free-text finding, then Finding concept CodeMeaning. */ export default function getLabelFromDCMJSImportedToolData(toolData) { const { findingSites = [], finding, annotation } = toolData; - if (annotation.data.label) { + if (annotation?.data?.label) { return annotation.data.label; } - let freeTextLabel = findingSites.find( + const freeTextLabel = findingSites.find( fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT ); - if (freeTextLabel) { + if (freeTextLabel?.CodeMeaning) { return freeTextLabel.CodeMeaning; } - if (finding && finding.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT) { + if (finding?.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT && finding?.CodeMeaning) { + return finding.CodeMeaning; + } + + if (finding?.CodeMeaning) { return finding.CodeMeaning; } } diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts index c6c67477745..3de163848c2 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts @@ -40,8 +40,7 @@ const convertSites = (codingValues, sites) => { /** * Hydrates a structured report * Handles 2d and 3d hydration from SCOORD and SCOORD3D points - * For 3D hydration, chooses a volume display set to display with - * FOr 2D hydration, chooses the (first) display set containing the referenced image. + * For 3D: chooses a volume display set. For 2D: chooses the first display set containing the referenced image. */ export default function hydrateStructuredReport( { servicesManager, extensionManager, commandsManager }: withAppTypes, @@ -51,7 +50,6 @@ export default function hydrateStructuredReport( const { measurementService, displaySetService, customizationService } = servicesManager.services; const codingValues = customizationService.getCustomization('codingValues'); - const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing'); const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); const { @@ -85,16 +83,10 @@ export default function hydrateStructuredReport( } }); - // Mapping of legacy datasets is now directly handled by adapters module const datasetToUse = instance; - - // Use CS3D adapters to generate toolState. let storedMeasurementByAnnotationType = MeasurementReport.generateToolState( datasetToUse, - // NOTE: we need to pass in the imageIds to dcmjs since the we use them - // for the imageToWorld transformation. The following assumes that the order - // that measurements were added to the display set are the same order as - // the measurementGroups in the instance. + /** dcmjs needs imageIds for imageToWorld; assumes displaySet.measurements order matches instance measurementGroups */ sopInstanceUIDToImageId, metaData ); @@ -108,7 +100,6 @@ export default function hydrateStructuredReport( }); } - // Filter what is found by DICOM SR to measurements we support. const mappingDefinitions = mappings.map(m => m.annotationType); const hydratableMeasurementsInSR = {}; @@ -118,7 +109,6 @@ export default function hydrateStructuredReport( } }); - // Set the series touched as tracked. const imageIds = []; // TODO: notification if no hydratable? @@ -126,10 +116,6 @@ export default function hydrateStructuredReport( const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType]; toolDataForAnnotationType.forEach(toolData => { - // Add the measurement to toolState - // dcmjs and Cornerstone3D has structural defect in supporting multi-frame - // files, and looking up the imageId from sopInstanceUIDToImageId results - // in the wrong value. const frameNumber = toolData.annotation.data?.frameNumber || 1; const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`]; @@ -160,15 +146,7 @@ export default function hydrateStructuredReport( } } - /** - * Gets reference data for what frame of reference and the referenced - * image id, or for 3d measurements, the volumeId to apply this annotation to. - */ function getReferenceData(toolData): ToolTypes.AnnotationMetadata { - // Add the measurement to toolState - // dcmjs and Cornerstone3D has structural defect in supporting multi-frame - // files, and looking up the imageId from sopInstanceUIDToImageId results - // in the wrong value. const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1; const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`]; @@ -198,13 +176,29 @@ export default function hydrateStructuredReport( const referenceData = getReferenceData(toolData); const { imageId } = referenceData; + /** Use SR subtypes for Probe and RectangleROI - they show label (e.g. Lesion) instead of intensity/stats */ + const toolNameForRendering = + annotationType === 'Probe' + ? 'SRPoint' + : annotationType === 'RectangleROI' + ? 'SRRectangleROI' + : annotationType; + + /** Use SR subtypes for Probe and RectangleROI - they show label (e.g. Lesion) instead of intensity/stats */ + const srAnnotationType = + annotationType === 'Probe' + ? 'SRPoint' + : annotationType === 'RectangleROI' + ? 'SRRectangleROI' + : annotationType; + const annotation = { annotationUID: toolData.annotation.annotationUID, data: toolData.annotation.data, predecessorImageId: toolData.predecessorImageId, metadata: { ...referenceData, - toolName: annotationType, + toolName: srAnnotationType, }, }; utilities.updatePlaneRestriction(annotation.data.handles.points, annotation.metadata); @@ -222,11 +216,11 @@ export default function hydrateStructuredReport( } }); - const matchingMapping = mappings.find(m => m.annotationType === annotationType); + const matchingMapping = mappings.find(m => m.annotationType === srAnnotationType); const newAnnotationUID = measurementService.addRawMeasurement( source, - annotationType, + srAnnotationType, { annotation }, matchingMapping.toMeasurementSchema, dataSource @@ -237,9 +231,7 @@ export default function hydrateStructuredReport( code: annotation.data.finding, }); - if (disableEditing) { - locking.setAnnotationLocked(newAnnotationUID, true); - } + locking.setAnnotationLocked(newAnnotationUID, true); if (imageId && !imageIds.includes(imageId)) { imageIds.push(imageId); @@ -255,12 +247,6 @@ export default function hydrateStructuredReport( }; } -/** - * For 3d annotations, there are often several display sets which could - * be used to display the annotation. Choose the first annotation with the - * same frame of reference that is reconstructable, or the first display set - * otherwise. - */ function chooseDisplaySet(displaySets, annotation) { if (!displaySets?.length) { console.warn('No display set found for', annotation); @@ -276,10 +262,6 @@ function chooseDisplaySet(displaySets, annotation) { return displaySets[0]; } -/** - * Gets the additional reference data appropriate for a 3d reference. - * This will choose a volume id, frame of reference and a plane restriction. - */ function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) { const { FrameOfReferenceUID } = toolData.annotation.metadata; const { points } = toolData.annotation.data.handles; @@ -304,10 +286,6 @@ function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) { return viewReference; } -/** - * Chooses a possible camera view - right now this is fairly basic, - * just setting the unknowns to null. - */ function chooseCameraView(_ds, points) { const selectedPoints = choosePoints(points); const cameraFocalPoint = centerOf(selectedPoints); diff --git a/extensions/cornerstone-dicom-sr/src/utils/srToolGetTextLines.ts b/extensions/cornerstone-dicom-sr/src/utils/srToolGetTextLines.ts new file mode 100644 index 00000000000..5ca67f1ff74 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/srToolGetTextLines.ts @@ -0,0 +1,24 @@ +/** + * getTextLines for SR subtypes (SRPoint, SRRectangleROI). Shows semantic label from Finding concept + * (e.g. "Lesion") instead of intensity/coordinates or area/stats. Sub-types per maintainer so + * base Probe/RectangleROI remain unchanged. + */ +export function getSRPointTextLines( + data: { label?: string; cachedStats?: Record }, + _targetId: string +): string[] | undefined { + if (data.label && typeof data.label === 'string') { + return [data.label]; + } + return undefined; +} + +export function getSRRectangleROITextLines( + data: { label?: string; cachedStats?: Record }, + _targetId: string +): string[] | undefined { + if (data.label && typeof data.label === 'string') { + return [data.label]; + } + return undefined; +} diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index 9decf4cd433..6b429c2cc77 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -170,6 +170,23 @@ const initMeasurementService = ( Probe.toMeasurement ); + /** SR subtypes use same schema as base tools (Probe/RectangleROI) */ + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'SRPoint', + Probe.matchingCriteria, + Probe.toAnnotation, + Probe.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'SRRectangleROI', + RectangleROI.matchingCriteria, + RectangleROI.toAnnotation, + RectangleROI.toMeasurement + ); + measurementService.addMapping( csTools3DVer1MeasurementSource, 'UltrasoundDirectionalTool', diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js index 99713c3a466..34191672dd5 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -7,7 +7,9 @@ const supportedTools = [ 'Angle', 'CobbAngle', 'Probe', + 'SRPoint', 'RectangleROI', + 'SRRectangleROI', 'PlanarFreehandROI', 'SplineROI', 'LivewireContour', diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts index eab15b4fef2..0befb4dc57c 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts @@ -49,6 +49,8 @@ const measurementServiceMappingsFactory = ( SplineROI: POLYLINE, LivewireContour: POLYLINE, Probe: POINT, + SRPoint: POINT, + SRRectangleROI: RECTANGLE, UltrasoundDirectional: POLYLINE, SegmentBidirectional: BIDIRECTIONAL, }; diff --git a/modes/basic-test-mode/src/initToolGroups.ts b/modes/basic-test-mode/src/initToolGroups.ts index ba5ec9d5a1c..b392ddbd86a 100644 --- a/modes/basic-test-mode/src/initToolGroups.ts +++ b/modes/basic-test-mode/src/initToolGroups.ts @@ -17,6 +17,11 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage const { toolNames, Enums } = utilityModule.exports; + const SRUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' + ); + const SRToolNames = SRUtilityModule?.exports?.toolNames; + const tools = { active: [ { @@ -70,6 +75,12 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage { toolName: toolNames.PlanarFreehandROI }, { toolName: toolNames.SplineROI }, { toolName: toolNames.LivewireContour }, + ...(SRToolNames + ? [ + { toolName: SRToolNames.SRPoint }, + { toolName: SRToolNames.SRRectangleROI }, + ] + : []), ], // enabled enabled: [{ toolName: toolNames.ImageOverlayViewer }], @@ -129,6 +140,9 @@ function initSRToolGroup(extensionManager, toolGroupService, commandsManager) { { toolName: SRToolNames.SRBidirectional }, { toolName: SRToolNames.SREllipticalROI }, { toolName: SRToolNames.SRCircleROI }, + { toolName: SRToolNames.SRPlanarFreehandROI }, + { toolName: SRToolNames.SRRectangleROI }, + { toolName: SRToolNames.SRPoint }, { toolName: toolNames.WindowLevelRegion }, ], enabled: [ @@ -154,6 +168,11 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { const { toolNames, Enums } = utilityModule.exports; + const SRUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' + ); + const SRToolNames = SRUtilityModule?.exports?.toolNames; + const tools = { active: [ { @@ -204,6 +223,12 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { { toolName: toolNames.PlanarFreehandROI }, { toolName: toolNames.SplineROI }, { toolName: toolNames.LivewireContour }, + ...(SRToolNames + ? [ + { toolName: SRToolNames.SRPoint }, + { toolName: SRToolNames.SRRectangleROI }, + ] + : []), ], disabled: [ { diff --git a/modes/basic/src/initToolGroups.ts b/modes/basic/src/initToolGroups.ts index 6278f62619c..b8aa49034c5 100644 --- a/modes/basic/src/initToolGroups.ts +++ b/modes/basic/src/initToolGroups.ts @@ -17,6 +17,11 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage const { toolNames, Enums } = utilityModule.exports; + const SRUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' + ); + const SRToolNames = SRUtilityModule?.exports?.toolNames; + const tools = { active: [ { @@ -81,6 +86,13 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage { toolName: toolNames.SplineROI }, { toolName: toolNames.LivewireContour }, { toolName: toolNames.WindowLevelRegion }, + /** SR subtypes for hydrated DICOM SR annotations (render-only, show label instead of intensity/stats) */ + ...(SRToolNames + ? [ + { toolName: SRToolNames.SRPoint }, + { toolName: SRToolNames.SRRectangleROI }, + ] + : []), ], enabled: [ { toolName: toolNames.ImageOverlayViewer }, @@ -153,6 +165,7 @@ function initSRToolGroup(extensionManager, toolGroupService) { { toolName: SRToolNames.SRCircleROI }, { toolName: SRToolNames.SRPlanarFreehandROI }, { toolName: SRToolNames.SRRectangleROI }, + { toolName: SRToolNames.SRPoint }, { toolName: toolNames.WindowLevelRegion }, ], enabled: [ @@ -177,6 +190,11 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { const { toolNames, Enums } = utilityModule.exports; + const SRUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' + ); + const SRToolNames = SRUtilityModule?.exports?.toolNames; + const tools = { active: [ { @@ -235,6 +253,13 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { displayOnePointAsCrosshairs: true, }, }, + /** SR subtypes for hydrated DICOM SR annotations (render-only) */ + ...(SRToolNames + ? [ + { toolName: SRToolNames.SRPoint }, + { toolName: SRToolNames.SRRectangleROI }, + ] + : []), ], disabled: [ { diff --git a/modes/usAnnotation/src/initToolGroups.js b/modes/usAnnotation/src/initToolGroups.js index aebe679618f..30c8524acaa 100644 --- a/modes/usAnnotation/src/initToolGroups.js +++ b/modes/usAnnotation/src/initToolGroups.js @@ -1,5 +1,3 @@ -import { toolNames as SRToolNames } from '@ohif/extension-cornerstone-dicom-sr'; - const colours = { 'viewport-0': 'rgb(200, 0, 0)', 'viewport-1': 'rgb(200, 200, 0)', @@ -17,6 +15,11 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage '@ohif/extension-cornerstone.utilityModule.tools' ); + const SRUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' + ); + const SRToolNames = SRUtilityModule?.exports?.toolNames; + const { toolNames, Enums } = utilityModule.exports; const tools = { @@ -84,6 +87,13 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage { toolName: toolNames.SplineROI }, { toolName: toolNames.LivewireContour }, { toolName: toolNames.WindowLevelRegion }, + /** SR subtypes for hydrated DICOM SR annotations (render-only, show label instead of intensity/stats) */ + ...(SRToolNames + ? [ + { toolName: SRToolNames.SRPoint }, + { toolName: SRToolNames.SRRectangleROI }, + ] + : []), ], enabled: [ { toolName: toolNames.ImageOverlayViewer }, @@ -153,6 +163,7 @@ function initSRToolGroup(extensionManager, toolGroupService) { { toolName: SRToolNames.SRCircleROI }, { toolName: SRToolNames.SRPlanarFreehandROI }, { toolName: SRToolNames.SRRectangleROI }, + { toolName: SRToolNames.SRPoint }, { toolName: toolNames.WindowLevelRegion }, ], enabled: [ @@ -172,6 +183,11 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { '@ohif/extension-cornerstone.utilityModule.tools' ); + const SRUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' + ); + const SRToolNames = SRUtilityModule?.exports?.toolNames; + const serviceManager = extensionManager._servicesManager; const { cornerstoneViewportService } = serviceManager.services; @@ -235,6 +251,13 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { displayOnePointAsCrosshairs: true, }, }, + /** SR subtypes for hydrated DICOM SR annotations (render-only) */ + ...(SRToolNames + ? [ + { toolName: SRToolNames.SRPoint }, + { toolName: SRToolNames.SRRectangleROI }, + ] + : []), ], disabled: [ {