From 5cb6e63b880d5598f9043f50c10e2c8f28733c34 Mon Sep 17 00:00:00 2001 From: James Petts Date: Thu, 10 Sep 2020 10:11:59 +0100 Subject: [PATCH] [IDC-1672] Highlight SEG segment/ RT structure when click to jump. (#2034) * Highlight for RTSTRUCT. * SEG temp crosshairs. * Disable RT highlighting for now. * Remove TODO --- extensions/debugging/src/DebugReportModal.js | 2 - .../src/components/RTPanel/RTPanel.js | 24 +++- .../dicom-rt/src/tools/RTStructDisplayTool.js | 58 ++++++-- .../src/tools/modules/rtStructModule.js | 3 +- .../src/utils/drawCanvasCrosshairs.js | 56 ++++++++ .../SegmentationPanel/SegmentationPanel.js | 7 + extensions/dicom-segmentation/src/init.js | 3 + .../src/tools/DICOMSegTempCrosshairsTool.js | 127 ++++++++++++++++++ .../src/tools/TOOL_NAMES.js | 5 + .../src/utils/drawCanvasCrosshairs.js | 56 ++++++++ 10 files changed, 321 insertions(+), 20 deletions(-) create mode 100644 extensions/dicom-rt/src/utils/drawCanvasCrosshairs.js create mode 100644 extensions/dicom-segmentation/src/tools/DICOMSegTempCrosshairsTool.js create mode 100644 extensions/dicom-segmentation/src/tools/TOOL_NAMES.js create mode 100644 extensions/dicom-segmentation/src/utils/drawCanvasCrosshairs.js diff --git a/extensions/debugging/src/DebugReportModal.js b/extensions/debugging/src/DebugReportModal.js index bc0572127..b95503670 100644 --- a/extensions/debugging/src/DebugReportModal.js +++ b/extensions/debugging/src/DebugReportModal.js @@ -131,8 +131,6 @@ const getBrowserInfo = () => { const getSeriesInstanceUIDsPerRow = viewports => { const { viewportSpecificData, numColumns } = viewports; - debugger; - // NOTE viewportSpecificData is actually an object with numerical keys. return Object.keys(viewportSpecificData).map(viewportIndex => { const vsd = viewportSpecificData[viewportIndex]; diff --git a/extensions/dicom-rt/src/components/RTPanel/RTPanel.js b/extensions/dicom-rt/src/components/RTPanel/RTPanel.js index 4e1e46c71..856c60d56 100644 --- a/extensions/dicom-rt/src/components/RTPanel/RTPanel.js +++ b/extensions/dicom-rt/src/components/RTPanel/RTPanel.js @@ -12,6 +12,9 @@ import StructureSetItem from '../StructureSetItem/StructureSetItem'; import RTPanelSettings from '../RTSettings/RTSettings'; import PanelSection from '../PanelSection/PanelSection'; import LoadingIndicator from '../LoadingIndicator/LoadingIndicator'; +import TOOL_NAMES from '../../utils/toolNames'; + +const { RTSTRUCT_DISPLAY_TOOL } = TOOL_NAMES; const { studyMetadataManager } = utils; @@ -144,13 +147,16 @@ const RTPanel = ({ if (isCornerstone()) { const enabledElements = cornerstone.getEnabledElements(); const element = enabledElements[activeIndex].element; - const toolState = cornerstoneTools.getToolState(element, 'stack'); + const stackToolState = cornerstoneTools.getToolState( + element, + 'stack' + ); - if (!toolState) { + if (!stackToolState) { return; } - const imageIds = toolState.data[0].imageIds; + const imageIds = stackToolState.data[0].imageIds; const module = cornerstoneTools.getModule('rtstruct'); const imageId = module.getters.imageIdOfCenterFrameOfROIContour( @@ -159,6 +165,18 @@ const RTPanel = ({ imageIds ); + const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState(); + const imageIdSpecificToolState = toolState[imageId]; + + const rtstructData = + imageIdSpecificToolState[RTSTRUCT_DISPLAY_TOOL]; + + const specificData = rtstructData.data.find( + rtData => rtData.ROINumber === ROINumber + ); + + specificData.highlight = true; + const frameIndex = imageIds.indexOf(imageId); const SOPInstanceUID = cornerstone.metaData.get( 'SOPInstanceUID', diff --git a/extensions/dicom-rt/src/tools/RTStructDisplayTool.js b/extensions/dicom-rt/src/tools/RTStructDisplayTool.js index e60b8ce91..2f65507d6 100644 --- a/extensions/dicom-rt/src/tools/RTStructDisplayTool.js +++ b/extensions/dicom-rt/src/tools/RTStructDisplayTool.js @@ -1,6 +1,6 @@ import { importInternal, getToolState, toolColors } from 'cornerstone-tools'; - import TOOL_NAMES from '../utils/toolNames'; +import drawCanvasCrosshairs from '../utils/drawCanvasCrosshairs'; // Cornerstone 3rd party dev kit imports const draw = importInternal('drawing/draw'); @@ -37,11 +37,17 @@ export default class RTStructDisplayTool extends BaseTool { return; } - const { lineWidth, opacity } = rtstructModule.configuration; + const { + lineWidth, + opacity, + highlightOpacity, + } = rtstructModule.configuration; // We have tool data for this element - iterate over each one and draw it const context = getNewContext(eventData.canvasContext.canvas); + let crossHairCenter; + for (let i = 0; i < toolState.data.length; i++) { const data = toolState.data[i]; @@ -75,27 +81,51 @@ export default class RTStructDisplayTool extends BaseTool { colorArray[2] },${opacity})`; + let highlight = data.highlight; + const options = { color, lineWidth }; + + if (highlight) { + crossHairCenter = { x: 0, y: 0 }; + + points.forEach(point => { + crossHairCenter.x += point.x; + crossHairCenter.y += point.y; + }); + + crossHairCenter.x /= points.length; + crossHairCenter.y /= points.length; + + // TODO: Disabling hightlight for now, it'd be good to bring it back + // when we have a good way of doing this for SEG. + + // options.fillStyle = color = `rgba(${colorArray[0]},${colorArray[1]},${ + // colorArray[2] + // },${highlightOpacity})`; + + // Draw highlight lines. + + delete data.highlight; // Don't highlight on next render. + } + switch (data.type) { case 'CLOSED_PLANAR': - this._renderClosedPlanar(context, eventData.element, points, { - color, - lineWidth, - }); + this._renderClosedPlanar(context, eventData.element, points, options); break; case 'POINT': - this._renderPoint(context, eventData.element, points, { - color, - lineWidth, - }); + this._renderPoint(context, eventData.element, points, options); break; case 'OPEN_PLANAR': - this._renderOpenPlanar(context, eventData.element, points, { - color, - lineWidth, - }); + this._renderOpenPlanar(context, eventData.element, points, options); break; } } + + if (crossHairCenter) { + drawCanvasCrosshairs(eventData, crossHairCenter, { + color: toolColors.getActiveColor(), + lineWidth: 1, + }); + } } _renderClosedPlanar(context, element, points, options) { diff --git a/extensions/dicom-rt/src/tools/modules/rtStructModule.js b/extensions/dicom-rt/src/tools/modules/rtStructModule.js index 0cca6e662..7d0a4ddf7 100644 --- a/extensions/dicom-rt/src/tools/modules/rtStructModule.js +++ b/extensions/dicom-rt/src/tools/modules/rtStructModule.js @@ -13,7 +13,8 @@ function refreshViewport() { const configuration = { lineWidth: 3, - opacity: 1.0, + opacity: 0.75, + highlightOpacity: 0.5, }; const state = { diff --git a/extensions/dicom-rt/src/utils/drawCanvasCrosshairs.js b/extensions/dicom-rt/src/utils/drawCanvasCrosshairs.js new file mode 100644 index 000000000..cb4645e24 --- /dev/null +++ b/extensions/dicom-rt/src/utils/drawCanvasCrosshairs.js @@ -0,0 +1,56 @@ +import cornerstone from 'cornerstone-core'; +import cornerstoneTools from 'cornerstone-tools'; + +const { importInternal } = cornerstoneTools; +const draw = importInternal('drawing/draw'); +const drawLine = importInternal('drawing/drawLine'); +const getNewContext = importInternal('drawing/getNewContext'); + +export default function _drawCanvasCrosshairs(eventData, center, options) { + const context = getNewContext(eventData.canvasContext.canvas); + const { element } = eventData; + + const centerCanvas = cornerstone.pixelToCanvas(element, center); + + const { clientWidth: width, clientHeight: height } = element; + + const offset = 10; + + draw(context, context => { + drawLine( + context, + element, + { x: centerCanvas.x + offset, y: centerCanvas.y }, + { x: width, y: centerCanvas.y }, + options, + 'canvas' + ); + + drawLine( + context, + element, + { x: centerCanvas.x - offset, y: centerCanvas.y }, + { x: 0, y: centerCanvas.y }, + options, + 'canvas' + ); + + drawLine( + context, + element, + { x: centerCanvas.x, y: centerCanvas.y + offset }, + { x: centerCanvas.x, y: height }, + options, + 'canvas' + ); + + drawLine( + context, + element, + { x: centerCanvas.x, y: centerCanvas.y - offset }, + { x: centerCanvas.x, y: 0 }, + options, + 'canvas' + ); + }); +} diff --git a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js index 79936efee..7fb9d6bc9 100644 --- a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js +++ b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js @@ -5,6 +5,7 @@ import cornerstone from 'cornerstone-core'; import moment from 'moment'; import { utils, log } from '@ohif/core'; import { ScrollableArea, TableList, Icon } from '@ohif/ui'; +import DICOMSegTempCrosshairsTool from '../../tools/DICOMSegTempCrosshairsTool'; import setActiveLabelmap from '../../utils/setActiveLabelMap'; import refreshViewports from '../../utils/refreshViewports'; @@ -334,6 +335,12 @@ const SegmentationPanel = ({ imageId ); + DICOMSegTempCrosshairsTool.addCrosshair( + element, + imageId, + segmentNumber + ); + onSegmentItemClick({ StudyInstanceUID, SOPInstanceUID, diff --git a/extensions/dicom-segmentation/src/init.js b/extensions/dicom-segmentation/src/init.js index 504b1e92d..e114dcd12 100644 --- a/extensions/dicom-segmentation/src/init.js +++ b/extensions/dicom-segmentation/src/init.js @@ -1,4 +1,5 @@ import csTools from 'cornerstone-tools'; +import DICOMSegTempCrosshairsTool from './tools/DICOMSegTempCrosshairsTool'; /** * @@ -17,4 +18,6 @@ export default function init({ servicesManager, configuration = {} }) { alwaysEraseOnClick: true, }, }); + + csTools.addTool(DICOMSegTempCrosshairsTool); } diff --git a/extensions/dicom-segmentation/src/tools/DICOMSegTempCrosshairsTool.js b/extensions/dicom-segmentation/src/tools/DICOMSegTempCrosshairsTool.js new file mode 100644 index 000000000..8bafbbc7b --- /dev/null +++ b/extensions/dicom-segmentation/src/tools/DICOMSegTempCrosshairsTool.js @@ -0,0 +1,127 @@ +import cornerstoneTools, { + importInternal, + getToolState, + toolColors, + getModule, + globalImageIdSpecificToolStateManager, +} from 'cornerstone-tools'; +import cornerstone from 'cornerstone-core'; +import drawCanvasCrosshairs from '../utils/drawCanvasCrosshairs'; +import TOOL_NAMES from './TOOL_NAMES'; + +const { DICOM_SEG_TEMP_CROSSHAIRS_TOOL } = TOOL_NAMES; +const { getters } = getModule('segmentation'); + +// Cornerstone 3rd party dev kit imports +const BaseTool = importInternal('base/BaseTool'); + +/** + * @class RTStructDisplayTool - Renders RTSTRUCT data in a read only manner (i.e. as an overlay). + * @extends cornerstoneTools.BaseTool + */ +export default class DICOMSegTempCrosshairsTool extends BaseTool { + constructor(props = {}) { + const defaultProps = { + mixins: ['enabledOrDisabledBinaryTool'], + name: DICOM_SEG_TEMP_CROSSHAIRS_TOOL, + }; + + const initialProps = Object.assign(defaultProps, props); + + super(initialProps); + + this._rtStructModule = cornerstoneTools.getModule('rtstruct'); + } + + renderToolData(evt) { + const eventData = evt.detail; + const { element } = eventData; + const toolState = getToolState(evt.currentTarget, this.name); + + if (!toolState) { + return; + } + + // We have tool data for this element - iterate over each one and draw it + + for (let i = 0; i < toolState.data.length; i++) { + const data = toolState.data[i]; + const crossHairCenter = data.center; + + drawCanvasCrosshairs(eventData, crossHairCenter, { + color: toolColors.getActiveColor(), + lineWidth: 1, + }); + + // Remove the crosshairs, we only render them for one redraw. + toolState.data.pop(); + } + } +} + +DICOMSegTempCrosshairsTool.addCrosshair = (element, imageId, segmentNumber) => { + const labelmap3D = getters.labelmap3D(element); + const stackToolState = cornerstoneTools.getToolState(element, 'stack'); + const enabledElement = cornerstone.getEnabledElement(element); + + const { rows, columns } = enabledElement.image; + + if (!stackToolState) { + return; + } + + const imageIds = stackToolState.data[0].imageIds; + const imageIdIndex = imageIds.findIndex(imgId => imgId === imageId); + + const labelmap2D = labelmap3D.labelmaps2D[imageIdIndex]; + const { pixelData } = labelmap2D; + + let xCenter = 0; + let yCenter = 0; + + let count = 0; + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < columns; x++) { + if (pixelData[y * columns + x] === segmentNumber) { + count++; + xCenter += x + 0.5; + yCenter += y + 0.5; + } + } + } + + xCenter /= count; + yCenter /= count; + + const globalToolState = globalImageIdSpecificToolStateManager.saveToolState(); + + if (!globalToolState[imageId]) { + globalToolState[imageId] = {}; + } + + const imageIdSpecificToolState = globalToolState[imageId]; + + if (!imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL]) { + imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL] = { data: [] }; + } else if (!imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL].data) { + imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL].data = []; + } + + const toolSpecificData = + imageIdSpecificToolState[DICOM_SEG_TEMP_CROSSHAIRS_TOOL].data; + + toolSpecificData.push({ center: { x: xCenter, y: yCenter }, segmentNumber }); + + // Enable the tool if not enabled for the element. + + const tool = cornerstoneTools.getToolForElement( + element, + DICOM_SEG_TEMP_CROSSHAIRS_TOOL + ); + + if (tool.mode !== 'enabled') { + // If not already active or passive, set passive so contours render. + cornerstoneTools.setToolEnabled(DICOM_SEG_TEMP_CROSSHAIRS_TOOL); + } +}; diff --git a/extensions/dicom-segmentation/src/tools/TOOL_NAMES.js b/extensions/dicom-segmentation/src/tools/TOOL_NAMES.js new file mode 100644 index 000000000..57fc2dff5 --- /dev/null +++ b/extensions/dicom-segmentation/src/tools/TOOL_NAMES.js @@ -0,0 +1,5 @@ +const TOOL_NAMES = { + DICOM_SEG_TEMP_CROSSHAIRS_TOOL: 'DICOMSegTempCrosshairsTool', +}; + +export default TOOL_NAMES; diff --git a/extensions/dicom-segmentation/src/utils/drawCanvasCrosshairs.js b/extensions/dicom-segmentation/src/utils/drawCanvasCrosshairs.js new file mode 100644 index 000000000..cb4645e24 --- /dev/null +++ b/extensions/dicom-segmentation/src/utils/drawCanvasCrosshairs.js @@ -0,0 +1,56 @@ +import cornerstone from 'cornerstone-core'; +import cornerstoneTools from 'cornerstone-tools'; + +const { importInternal } = cornerstoneTools; +const draw = importInternal('drawing/draw'); +const drawLine = importInternal('drawing/drawLine'); +const getNewContext = importInternal('drawing/getNewContext'); + +export default function _drawCanvasCrosshairs(eventData, center, options) { + const context = getNewContext(eventData.canvasContext.canvas); + const { element } = eventData; + + const centerCanvas = cornerstone.pixelToCanvas(element, center); + + const { clientWidth: width, clientHeight: height } = element; + + const offset = 10; + + draw(context, context => { + drawLine( + context, + element, + { x: centerCanvas.x + offset, y: centerCanvas.y }, + { x: width, y: centerCanvas.y }, + options, + 'canvas' + ); + + drawLine( + context, + element, + { x: centerCanvas.x - offset, y: centerCanvas.y }, + { x: 0, y: centerCanvas.y }, + options, + 'canvas' + ); + + drawLine( + context, + element, + { x: centerCanvas.x, y: centerCanvas.y + offset }, + { x: centerCanvas.x, y: height }, + options, + 'canvas' + ); + + drawLine( + context, + element, + { x: centerCanvas.x, y: centerCanvas.y - offset }, + { x: centerCanvas.x, y: 0 }, + options, + 'canvas' + ); + }); +}