diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index aed08389e..70e56cfa9 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -34,7 +34,7 @@ "@ohif/ui": "^0.50.0", "cornerstone-core": "^2.2.8", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.12.0", + "cornerstone-tools": "4.12.5", "cornerstone-wado-image-loader": "^3.0.0", "dcmjs": "^0.10.1", "dicom-parser": "^1.8.3", diff --git a/extensions/cornerstone/src/OHIFCornerstoneViewport.js b/extensions/cornerstone/src/OHIFCornerstoneViewport.js index 6a45f4d7f..fbed63b14 100644 --- a/extensions/cornerstone/src/OHIFCornerstoneViewport.js +++ b/extensions/cornerstone/src/OHIFCornerstoneViewport.js @@ -4,19 +4,9 @@ import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport'; import OHIF from '@ohif/core'; import PropTypes from 'prop-types'; import cornerstone from 'cornerstone-core'; -import handleSegmentationStorage from './handleSegmentationStorage.js'; const { StackManager } = OHIF.utils; -const SOP_CLASSES = { - SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4', -}; - -const specialCaseHandlers = {}; -specialCaseHandlers[ - SOP_CLASSES.SEGMENTATION_STORAGE -] = handleSegmentationStorage; - class OHIFCornerstoneViewport extends Component { state = { viewportData: null, @@ -124,42 +114,24 @@ class OHIFCornerstoneViewport extends Component { studies, StudyInstanceUID, displaySetInstanceUID, - SOPClassUID, SOPInstanceUID, frameIndex ) => { let viewportData; - switch (SOPClassUID) { - case SOP_CLASSES.SEGMENTATION_STORAGE: - const specialCaseHandler = - specialCaseHandlers[SOP_CLASSES.SEGMENTATION_STORAGE]; + const stack = OHIFCornerstoneViewport.getCornerstoneStack( + studies, + StudyInstanceUID, + displaySetInstanceUID, + SOPInstanceUID, + frameIndex + ); - viewportData = await specialCaseHandler( - studies, - StudyInstanceUID, - displaySetInstanceUID, - SOPInstanceUID, - frameIndex - ); - break; - default: - const stack = OHIFCornerstoneViewport.getCornerstoneStack( - studies, - StudyInstanceUID, - displaySetInstanceUID, - SOPInstanceUID, - frameIndex - ); - - viewportData = { - StudyInstanceUID, - displaySetInstanceUID, - stack, - }; - - break; - } + viewportData = { + StudyInstanceUID, + displaySetInstanceUID, + stack, + }; return viewportData; }; @@ -184,13 +156,10 @@ class OHIFCornerstoneViewport extends Component { ); } - const SOPClassUID = sopClassUIDs && sopClassUIDs[0]; - this.getViewportData( studies, StudyInstanceUID, displaySetInstanceUID, - SOPClassUID, SOPInstanceUID, frameIndex ).then(viewportData => { @@ -205,7 +174,7 @@ class OHIFCornerstoneViewport extends Component { } componentDidUpdate(prevProps) { - const { studies, displaySet } = this.props.viewportData; + const { displaySet } = this.props.viewportData; const prevDisplaySet = prevProps.viewportData.displaySet; if ( diff --git a/extensions/cornerstone/src/handleSegmentationStorage.js b/extensions/cornerstone/src/handleSegmentationStorage.js deleted file mode 100644 index 799204982..000000000 --- a/extensions/cornerstone/src/handleSegmentationStorage.js +++ /dev/null @@ -1,128 +0,0 @@ -import * as dcmjs from 'dcmjs'; - -import OHIF from '@ohif/core'; -import cornerstone from 'cornerstone-core'; -import cornerstoneTools from 'cornerstone-tools'; - -const { StackManager, DicomLoaderService } = OHIF.utils; - -function getDisplaySet(studies, StudyInstanceUID, displaySetInstanceUID) { - const study = studies.find( - study => study.StudyInstanceUID === StudyInstanceUID - ); - - const displaySet = study.displaySets.find(set => { - return set.displaySetInstanceUID === displaySetInstanceUID; - }); - - return displaySet; -} - -function getDisplaySetsBySeries(studies, StudyInstanceUID, SeriesInstanceUID) { - const study = studies.find( - study => study.StudyInstanceUID === StudyInstanceUID - ); - - return study.displaySets.filter(set => { - return set.SeriesInstanceUID === SeriesInstanceUID; - }); -} - -function parseSeg(arrayBuffer, imageIds) { - return dcmjs.adapters.Cornerstone.Segmentation.generateToolState( - imageIds, - arrayBuffer, - cornerstone.metaData - ); -} - -function addSegMetadataToCornerstoneToolState( - segMetadata, - toolState, - displaySetInstanceUID -) { - cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState( - toolState - ); - - const brushModule = cornerstoneTools.store.modules.brush; - - for (let i = 0; i < segMetadata.length; i++) { - brushModule.setters.metadata(displaySetInstanceUID, i, segMetadata[i]); - } -} - -async function handleSegmentationStorage( - studies, - StudyInstanceUID, - displaySetInstanceUID -) { - const study = studies.find( - study => study.StudyInstanceUID === StudyInstanceUID - ); - const displaySet = getDisplaySet( - studies, - StudyInstanceUID, - displaySetInstanceUID - ); - - const arrayBuffer = await DicomLoaderService.findDicomDataPromise( - displaySet, - studies - ); - const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer); - const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( - dicomData.dict - ); - - dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta); - - const SeriesInstanceUID = dataset.ReferencedSeriesSequence.SeriesInstanceUID; - const displaySets = getDisplaySetsBySeries( - studies, - StudyInstanceUID, - SeriesInstanceUID - ); - - if (displaySets.length > 1) { - console.warn( - 'More than one display set with the same SeriesInstanceUID. This is not supported yet...' - ); - } - - const referenceDisplaySet = displaySets[0]; - const imageIds = referenceDisplaySet.images.map(image => image.getImageId()); - const results = parseSeg(arrayBuffer, imageIds); - - if (!results) { - throw new Error('Fractional segmentations are not supported'); - } - - const { labelmapBuffer, segMetadata, segmentsOnFrame } = results; - const { setters } = cornerstoneTools.getModule('segmentation'); - - setters.labelmap3DByFirstImageId( - imageIds[0], - labelmapBuffer, - 0, // TODO -> Can define a color LUT based on colors in the SEG later. - segMetadata, - imageIds.length, - segmentsOnFrame - ); - - const cachedStack = StackManager.findOrCreateStack( - study, - referenceDisplaySet - ); - const stack = Object.assign({}, cachedStack); - - stack.currentImageIdIndex = 0; - - return { - StudyInstanceUID, - displaySetInstanceUID, - stack, - }; -} - -export default handleSegmentationStorage; diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index 3389840e9..27e962b80 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -66,7 +66,6 @@ export default function init({ servicesManager, configuration }) { csTools.DragProbeTool, csTools.RectangleRoiTool, ], - segmentation: [csTools.BrushTool], other: [ csTools.PanTool, csTools.ZoomTool, diff --git a/extensions/dicom-segmentation/.webpack/webpack.dev.js b/extensions/dicom-segmentation/.webpack/webpack.dev.js new file mode 100644 index 000000000..1ae308448 --- /dev/null +++ b/extensions/dicom-segmentation/.webpack/webpack.dev.js @@ -0,0 +1,8 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); +}; diff --git a/extensions/dicom-segmentation/.webpack/webpack.prod.js b/extensions/dicom-segmentation/.webpack/webpack.prod.js new file mode 100644 index 000000000..4fe9a46db --- /dev/null +++ b/extensions/dicom-segmentation/.webpack/webpack.prod.js @@ -0,0 +1,33 @@ +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); + + return merge(commonConfig, { + devtool: 'source-map', + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'OHIFExtDicomSeg', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], + }); +}; diff --git a/extensions/dicom-segmentation/LICENSE b/extensions/dicom-segmentation/LICENSE new file mode 100644 index 000000000..19e20dd35 --- /dev/null +++ b/extensions/dicom-segmentation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/dicom-segmentation/README.md b/extensions/dicom-segmentation/README.md new file mode 100644 index 000000000..2b2ac7cf5 --- /dev/null +++ b/extensions/dicom-segmentation/README.md @@ -0,0 +1,65 @@ +# @ohif/dicom-segmentation + + + +This extension adds support to view and navigate the segments available in the DICOM SEG, +allowing control over visibility of a given segment and providing metadata about the segment(s). + + + +#### Index + +Extension Id: `com.ohif.dicom-segmentation` + +## Tool Configuration + +Tools can be configured through extension configuration using the tools key: + +```js + ... + cornerstoneExtensionConfig: { + tools: { + ArrowAnnotate: { + configuration: { + getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')), + }, + }, + }, + }, + ... +``` + +## Annotate Tools Configuration + +*We currently support one property for annotation tools.* + +### Hide handles +This extension configuration allows you to toggle on/off handle rendering for all annotate tools: + +```js + ... + cornerstoneExtensionConfig: { + hideHandles: true, + }, + ... + +## Resources + +### Repositories + +- [cornerstonejs/react-cornerstone-viewport][react-viewport] +- [cornerstonejs/cornerstoneTools][cornerstone-tools] +- [cornerstonejs/cornerstone][cornerstone] + + + + +[docs-commands]: https://www.com +[docs-hotkeys]: https://www.com +[docs-userprefs]: htt +[react-viewport]: https://github.com/cornerstonejs/react-cornerstone-viewport +[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools +[cornerstone]: https://github.com/cornerstonejs/cornerstone + diff --git a/extensions/dicom-segmentation/package.json b/extensions/dicom-segmentation/package.json new file mode 100644 index 000000000..66b7238bd --- /dev/null +++ b/extensions/dicom-segmentation/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ohif/extension-dicom-segmentation", + "version": "0.0.1", + "description": "OHIF extension for viewing segmentations in the 2D MPR view", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/index.umd.js", + "module": "src/index.js", + "engines": { + "node": ">=10", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", + "dev:dicom-html": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "prepublishOnly": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "^0.50.0", + "cornerstone-core": "^2.2.8", + "cornerstone-tools": "4.12.5", + "dcmjs": "^0.6.1", + "prop-types": "^15.6.2", + "react": "^16.8.6", + "react-dom": "^16.8.6" + }, + "dependencies": { + "@babel/runtime": "^7.5.5", + "react-select": "^3.0.8" + } +} diff --git a/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js b/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js new file mode 100644 index 000000000..52b5bac08 --- /dev/null +++ b/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js @@ -0,0 +1,71 @@ +import { MODULE_TYPES, utils } from '@ohif/core'; +import loadSegmentation from './loadSegmentation'; + +// TODO: Should probably use dcmjs for this +const SOP_CLASS_UIDS = { + DICOM_SEG: '1.2.840.10008.5.1.4.1.1.66.4', +}; + +const sopClassUIDs = Object.values(SOP_CLASS_UIDS); + +// TODO: Handle the case where there is more than one SOP Class Handler for the +// same SOP Class. +const OHIFDicomSegSopClassHandler = { + id: 'OHIFDicomSegSopClassHandler', + type: MODULE_TYPES.SOP_CLASS_HANDLER, + sopClassUIDs, + getDisplaySetFromSeries: function( + series, + study, + dicomWebClient, + authorizationHeaders + ) { + const instance = series.getFirstInstance(); + const metadata = instance.getData().metadata; + + const { + SeriesDate, + SeriesTime, + SeriesDescription, + FrameOfReferenceUID, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = metadata; + + const segDisplaySet = { + Modality: 'SEG', + displaySetInstanceUID: utils.guid(), + wadoRoot: study.getData().wadoRoot, + wadoUri: instance.getData().wadouri, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + FrameOfReferenceUID, + authorizationHeaders, + metadata, + isDerived: true, + referencedDisplaySetUID: null, // Assigned when loaded. + labelmapIndex: null, // Assigned when loaded. + isLoaded: false, + SeriesDate, + SeriesTime, + SeriesDescription, + }; + + segDisplaySet.load = function(referencedDisplaySet, studies) { + return loadSegmentation( + segDisplaySet, + referencedDisplaySet, + studies + ).catch(error => { + segDisplaySet.isLoaded = false; + throw new Error(error); + }); + }; + + return segDisplaySet; + }, +}; + +export default OHIFDicomSegSopClassHandler; diff --git a/extensions/dicom-segmentation/src/components/BrushColorSelector/BrushColorSelector.css b/extensions/dicom-segmentation/src/components/BrushColorSelector/BrushColorSelector.css new file mode 100644 index 000000000..66c838648 --- /dev/null +++ b/extensions/dicom-segmentation/src/components/BrushColorSelector/BrushColorSelector.css @@ -0,0 +1,41 @@ +.dcmseg-brush-color-selector .selector-buttons button { + color: #333; + background-color: #fff; + padding: 4px 12px; + line-height: 1.4; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + user-select: none; + border: 1px solid #ccc; + border-radius: 4px; + transition: all 0.15s ease; +} + +.dcmseg-brush-color-selector .selector-buttons button:hover, +.dcmseg-brush-color-selector .selector-buttons button:active, +.dcmseg-brush-color-selector .selector-buttons button.active { + background-color: var(--active-color); + border-color: var(--ui-border-color-active); + color: #fff; +} + +.dcmseg-brush-color-selector .selector-buttons { + margin-left: 10px; + display: flex; + flex-direction: row; + align-items: center; +} + +.dcmseg-brush-color-selector .selector-buttons button:last-of-type { + margin-left: 5px; +} + +.dcmseg-brush-color-selector .selector-active-segment { + border-radius: 100%; + width: 32px; + height: 32px; + text-align: center; + line-height: 32px; +} diff --git a/extensions/dicom-segmentation/src/components/BrushColorSelector/BrushColorSelector.js b/extensions/dicom-segmentation/src/components/BrushColorSelector/BrushColorSelector.js new file mode 100644 index 000000000..37593dd51 --- /dev/null +++ b/extensions/dicom-segmentation/src/components/BrushColorSelector/BrushColorSelector.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './BrushColorSelector.css'; + +const BrushColorSelector = ({ defaultColor, index, onNext, onPrev }) => ( +