From e3f2e26e3bff79857a29b54ee8ec3bc81150c34e Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Wed, 1 Jul 2020 11:59:29 +0100 Subject: [PATCH] Render text properly, render arrow + freetext. Need to move textboxes appropriately. --- .../dicom-sr/src/getSopClassHandlerModule.js | 73 +++----- .../dicom-sr/src/tools/DICOMSRDisplayTool.js | 168 ++++++++++++++++-- .../dicom-sr/src/utils/addMeasurement.js | 5 - yarn.lock | 21 +++ 4 files changed, 200 insertions(+), 67 deletions(-) diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index ec91a1bc5..51ebd5963 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -26,12 +26,16 @@ const CodeNameCodeSequenceValues = { ImageLibraryGroup: '126200', TrackingUniqueIdentifier: '112040', TrackingIdentifier: '112039', + Finding: '121071', }; const RELATIONSHIP_TYPE = { INFERRED_FROM: 'INFERRED FROM', }; +const CORNERSTONE_CODING_SCHEME_DESIGNATOR = 'CST4'; +const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; + /** * Basic SOPClassHandler: * - For all Image types that are stackable, create @@ -68,8 +72,9 @@ function _getDisplaySetsFromSeries( const { ConceptNameCodeSequence, ContentSequence } = instance; if ( + !ConceptNameCodeSequence || ConceptNameCodeSequence.CodeValue !== - CodeNameCodeSequenceValues.ImagingMeasurementReport + CodeNameCodeSequenceValues.ImagingMeasurementReport ) { console.warn( 'Only support Imaging Measurement Report SRs (TID1500) for now' @@ -134,49 +139,6 @@ function _getDisplaySetsFromSeries( return [displaySet]; } -function _isRehydratable(displaySet, MeasurementService) { - const mappings = MeasurementService.getSourceMappings( - 'CornerstoneTools', - '4' - ); - - if (!mappings || !mappings.length) { - return false; - } - - const mappingDefinitions = mappings.map(m => m.definition); - const { measurements } = displaySet; - - const adapterKeys = Object.keys(cornerstoneAdapters).filter( - adapterKey => - typeof cornerstoneAdapters[adapterKey] - .isValidCornerstoneTrackingIdentifier === 'function' - ); - - const adapters = []; - - adapterKeys.forEach(key => { - if (mappingDefinitions.includes(key)) { - // Must have both a dcmjs adapter and a MeasurementService - // Definition in order to be a candidate for import. - adapters.push(cornerstoneAdapters[key]); - } - }); - - for (let i = 0; i < measurements.length; i++) { - const TrackingIdentifier = measurements[i].TrackingIdentifier; - const hydratable = adapters.some(adapter => - adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier) - ); - - if (hydratable) { - return true; - } - } - - return false; -} - function _checkIfCanAddMeasurementsToDisplaySet( srDisplaySet, newDisplaySet, @@ -459,6 +421,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { CodeNameCodeSequenceValues.TrackingIdentifier ); + const Findings = mergedContentSequence.filter( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.Finding + ); + const measurement = { loaded: false, labels: [], @@ -467,6 +435,23 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { TrackingIdentifier: TrackingIdentifierContentItem.TextValue, }; + if (Findings.length) { + // TODO -> Pull in labels when we have them, just free text for now. + const cornerstoneFreeTextFinding = Findings.find( + Finding => + Finding.ConceptCodeSequence.CodingSchemeDesignator === + CORNERSTONE_CODING_SCHEME_DESIGNATOR && + Finding.ConceptCodeSequence.CodeValue === + CORNERSTONE_FREETEXT_CODE_VALUE + ); + if (cornerstoneFreeTextFinding) { + measurement.labels.push({ + label: CORNERSTONE_FREETEXT_CODE_VALUE, + value: cornerstoneFreeTextFinding.ConceptCodeSequence.CodeMeaning, + }); + } + } + NUMContentItems.forEach(item => { const { ConceptNameCodeSequence, diff --git a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js index f744c5c91..f78c053dc 100644 --- a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js +++ b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js @@ -1,4 +1,5 @@ import { importInternal, getToolState, toolColors } from 'cornerstone-tools'; +import { pixelToCanvas } from 'cornerstone-core'; import TOOL_NAMES from '../constants/toolNames'; import SCOORD_TYPES from '../constants/scoordTypes'; @@ -10,6 +11,7 @@ const drawJoinedLines = importInternal('drawing/drawJoinedLines'); const drawCircle = importInternal('drawing/drawCircle'); const drawEllipse = importInternal('drawing/drawEllipse'); const drawHandles = importInternal('drawing/drawHandles'); +const drawArrow = importInternal('drawing/drawArrow'); const getNewContext = importInternal('drawing/getNewContext'); const BaseTool = importInternal('base/BaseTool'); const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox'); @@ -60,6 +62,12 @@ export default class DICOMSRDisplayTool extends BaseTool { trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier) ); + // First: Render annotations with textboxes moved to closest side + // TODO: Render all annotations and get their locations. + // TODO: Find suitable places for textboxes that aren't covered. + + let shouldRepositionTextBoxes = false; + for (let i = 0; i < filteredToolData.length; i++) { const data = filteredToolData[i]; const { renderableData, labels } = data; @@ -80,8 +88,10 @@ export default class DICOMSRDisplayTool extends BaseTool { switch (GraphicType) { case SCOORD_TYPES.POINT: + this.renderPoint(renderableDataForGraphicType, eventData, options); + break; case SCOORD_TYPES.MULTIPOINT: - this.renderPointOrMultipoint( + this.renderMultipoint( renderableDataForGraphicType, eventData, options @@ -110,36 +120,53 @@ export default class DICOMSRDisplayTool extends BaseTool { const { element } = eventData; const context = getNewContext(eventData.canvasContext.canvas); - debugger; + if (!data.handles || !data.handles.textBox) { + const textBox = { + active: false, + hasMoved: true, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true, + }; + + const anchorPoints = _getTextBoxAnchorPointsForRenderableData( + renderableData, + eventData + ); + textBox.anchorPoints = anchorPoints; + + const bottomRight = { + x: Math.max(...anchorPoints.map(point => point.x)), + y: Math.max(...anchorPoints.map(point => point.y)), + }; + + textBox.x = bottomRight.x; + textBox.y = bottomRight.y; + + data.handles = {}; + data.handles.textBox = textBox; + + shouldRepositionTextBoxes = true; + } const text = _getTextBoxLinesFromLabels(labels); - debugger; - function textBoxAnchorPoints() { - return [{ x: 1000, y: 1000 }]; + return data.handles.textBox.anchorPoints; } draw(context, context => { drawLinkedTextBox( context, element, - { - active: false, - hasMoved: false, - movesIndependently: false, - drawnIndependently: true, - allowedOutsideImage: true, - hasBoundingBox: true, - x: 256, - y: 256, - }, + data.handles.textBox, text, - null, + data.handles, textBoxAnchorPoints, color, lineWidth, - 10, + 0, true ); }); @@ -157,7 +184,7 @@ export default class DICOMSRDisplayTool extends BaseTool { }); } - renderPointOrMultipoint(renderableData, eventData, options) { + renderMultipoint(renderableData, eventData, options) { const context = getNewContext(eventData.canvasContext.canvas); renderableData.forEach(points => { @@ -167,6 +194,46 @@ export default class DICOMSRDisplayTool extends BaseTool { }); } + renderPoint(renderableData, eventData, options) { + // Render single point as an arrow. + debugger; + const { element, image } = eventData; + const { rows, columns } = image; + const context = getNewContext(eventData.canvasContext.canvas); + + const { color, lineWidth } = options; + + // Find a suitable length for the image size. + + const xOffset = columns / 10; + const yOffset = rows / 10; + + debugger; + + renderableData.forEach(points => { + const point = points[0]; // The SCOORD type is POINT so the array length is 1. + draw(context, context => { + // Draw the arrow + const handleStartCanvas = pixelToCanvas(element, point); + const handleEndCanvas = pixelToCanvas(element, { + x: point.x + xOffset, + y: point.y + yOffset, + }); + + debugger; + + drawArrow( + context, + handleEndCanvas, + handleStartCanvas, + color, + lineWidth, + false + ); + }); + }); + } + renderCircle(renderableData, eventData, options) { const { element } = eventData; @@ -220,6 +287,7 @@ const SHORT_HAND_MAP = { 'Long Axis': 'L ', AREA: 'Area ', Length: '', + CORNERSTONEFREETEXT: '', }; function _labelToShorthand(label) { @@ -231,3 +299,67 @@ function _labelToShorthand(label) { return label; } + +function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) { + let anchorPoints = []; + + Object.keys(renderableData).forEach(GraphicType => { + const renderableDataForGraphicType = renderableData[GraphicType]; + + switch (GraphicType) { + case SCOORD_TYPES.POINT: + renderableDataForGraphicType.forEach(points => { + anchorPoints = [...anchorPoints, ...points]; + + // Add other arrow point based on image size. + const { image } = eventData; + const { rows, columns } = image; + + const xOffset = columns / 10; + const yOffset = rows / 10; + const point = points[0]; + + anchorPoints.push({ x: point.x + xOffset, y: point.y + yOffset }); + }); + + break; + case SCOORD_TYPES.MULTIPOINT: + case SCOORD_TYPES.POLYLINE: + renderableDataForGraphicType.forEach(points => { + anchorPoints = [...anchorPoints, ...points]; + }); + break; + case SCOORD_TYPES.CIRCLE: + renderableDataForGraphicType.forEach(circle => { + const { center, radius } = circle; + + anchorPoints.push({ x: center.x + radius, y: center.y }); + anchorPoints.push({ x: center.x - radius, y: center.y }); + anchorPoints.push({ x: center.x, y: center.y + radius }); + anchorPoints.push({ x: center.x, y: center.y - radius }); + }); + + break; + case SCOORD_TYPES.ELLIPSE: + renderableDataForGraphicType.forEach(ellipse => { + const { corner1, corner2 } = ellipse; + + const halfWidth = Math.abs(corner1.x - corner2.x) / 2; + const halfHeight = Math.abs(corner1.y - corner2.y) / 2; + + const center = { + x: (corner1.x + corner2.x) / 2, + y: (corner1.y + corner2.y) / 2, + }; + + anchorPoints.push({ x: center.x + halfWidth, y: center.y }); + anchorPoints.push({ x: center.x - halfWidth, y: center.y }); + anchorPoints.push({ x: center.x, y: center.y + halfHeight }); + anchorPoints.push({ x: center.x, y: center.y - halfHeight }); + }); + break; + } + }); + + return anchorPoints; +} diff --git a/extensions/dicom-sr/src/utils/addMeasurement.js b/extensions/dicom-sr/src/utils/addMeasurement.js index dc2211244..b7ea21440 100644 --- a/extensions/dicom-sr/src/utils/addMeasurement.js +++ b/extensions/dicom-sr/src/utils/addMeasurement.js @@ -14,11 +14,6 @@ export default function addMeasurement( imageId, displaySetInstanceUID ) { - console.log('== ADD MEASUREMENT TO CST =='); - console.log(measurement); - console.log(imageId); - console.log('============================'); - // TODO -> Render rotated ellipse . const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; diff --git a/yarn.lock b/yarn.lock index ee59610f2..46de652dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2679,6 +2679,17 @@ dependencies: "@types/node" ">= 8" +"@ohif/extension-cornerstone@^2.4.0": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-2.8.2.tgz#b8d95610b91eb43c05f84adcdd699183918a57ff" + integrity sha512-fIkPPQdDCkoVHCYTUFB4UhmdL9VJRyYawa6FxXiKXjj9OcUsrkXj+BORYgzcEkLWTLmrB+66qtIqpQ7FyXlzJQ== + dependencies: + "@babel/runtime" "^7.5.5" + classnames "^2.2.6" + lodash.merge "^4.6.2" + lodash.throttle "^4.1.1" + react-cornerstone-viewport "2.3.9" + "@ohif/extension-lesion-tracker@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@ohif/extension-lesion-tracker/-/extension-lesion-tracker-0.2.0.tgz#37fda345204041539051750fad17551846fba40f" @@ -17889,6 +17900,16 @@ react-cornerstone-viewport@2.3.8: prop-types "^15.7.2" react-resize-detector "^4.2.1" +react-cornerstone-viewport@2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.3.9.tgz#f9761da8e536f0a217137c6ca1a983f5882249f9" + integrity sha512-qrhq8CbX/jq6b93cQjV2qC/mhHOvFBpxzxcHlvHzEQZt/rmRMcaYxCqjpNaNbUmmBu61wNkxesUVsggkPTTcqg== + dependencies: + classnames "^2.2.6" + date-fns "^2.2.1" + prop-types "^15.7.2" + react-resize-detector "^4.2.1" + react-dates@21.2.1: version "21.2.1" resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.2.1.tgz#a979ed6876326ccfbf754a019bc95458cc061ad8"