From c81011aea517fc007f559e2ffeb8a5190ebe083b Mon Sep 17 00:00:00 2001 From: Sachidanand Alle Date: Tue, 19 Nov 2024 11:37:57 +0000 Subject: [PATCH 01/11] First draft - support latest ohif 3.10.x Signed-off-by: Sachidanand Alle --- plugins/ohifv3/build.sh | 4 +- plugins/ohifv3/extensions.patch | 3 +- .../monai-label/.webpack/webpack.dev.js | 12 + .../extensions/monai-label/package.json | 44 +- .../src/components/ModelSelector.css | 11 +- .../src/components/ModelSelector.tsx | 6 +- .../src/components/MonaiLabelPanel.css | 20 +- .../src/components/MonaiLabelPanel.tsx | 568 +++++++++--------- .../src/components/MonaiSegmentation.tsx | 319 ---------- .../src/components/SegmentationToolbox.tsx | 376 ------------ .../src/components/SettingsTable.css | 1 + .../src/components/SettingsTable.tsx | 4 +- .../src/components/actions/ActiveLearning.css | 30 - .../src/components/actions/ActiveLearning.tsx | 329 ++++------ .../components/actions/AutoSegmentation.tsx | 123 ++-- .../src/components/actions/BaseTab.css | 44 ++ .../src/components/actions/BaseTab.tsx | 15 +- .../src/components/actions/ClassPrompts.tsx | 425 +++++++++++++ .../src/components/actions/NextSampleForm.css | 12 - .../src/components/actions/NextSampleForm.tsx | 14 - .../src/components/actions/OptionTable.css | 30 - .../src/components/actions/OptionTable.tsx | 222 ------- .../src/components/actions/PointPrompts.css | 22 + .../src/components/actions/PointPrompts.tsx | 401 +++++++++++++ .../src/components/actions/SmartEdit.tsx | 327 ---------- .../src/components/colorPickerDialog.css | 3 - .../src/components/colorPickerDialog.tsx | 58 -- .../extensions/monai-label/src/config.json | 6 + .../monai-label/src/getCommandsModule.ts | 25 +- .../extensions/monai-label/src/index.tsx | 45 -- .../src/services/MonaiLabelClient.js | 20 +- .../src/tools/ProbeMONAILabelTool.ts | 1 - .../src/utils/GenericAnatomyColors.js | 6 + .../monai-label/src/utils/GenericUtils.js | 36 +- .../monai-label/src/utils/SegUtils.js | 21 + .../src/utils/SegmentationReader.js | 71 --- .../src/utils/SegmentationUtils.tsx | 51 -- .../monai-label/src/utils/addToolInstance.ts | 1 + .../monai-label/.webpack/webpack.prod.js | 104 ++-- .../ohifv3/modes/monai-label/babel.config.js | 6 +- plugins/ohifv3/modes/monai-label/package.json | 30 +- .../ohifv3/modes/monai-label/src/index.tsx | 70 +-- .../modes/monai-label/src/initToolGroups.js | 8 +- .../modes/monai-label/src/toolbarButtons.js | 45 +- 44 files changed, 1651 insertions(+), 2318 deletions(-) create mode 100644 plugins/ohifv3/extensions/monai-label/.webpack/webpack.dev.js delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/MonaiSegmentation.tsx delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/SegmentationToolbox.tsx delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/ActiveLearning.css create mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.css create mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.css delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.tsx create mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.css create mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/SmartEdit.tsx delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/colorPickerDialog.css delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/colorPickerDialog.tsx create mode 100644 plugins/ohifv3/extensions/monai-label/src/config.json create mode 100644 plugins/ohifv3/extensions/monai-label/src/utils/SegUtils.js delete mode 100644 plugins/ohifv3/extensions/monai-label/src/utils/SegmentationUtils.tsx diff --git a/plugins/ohifv3/build.sh b/plugins/ohifv3/build.sh index 5c0e53aec..febe3ad31 100755 --- a/plugins/ohifv3/build.sh +++ b/plugins/ohifv3/build.sh @@ -27,7 +27,7 @@ cd ${my_dir} rm -rf Viewers git clone https://github.com/OHIF/Viewers.git cd Viewers -git checkout 33f125940863607f8dba82c71b27a43f35431dd5 +git checkout d8ef36ed24466988586e19b855d2bbb86f8c657a #cp -r ../extensions/monai-label extensions/ #cp -r ../modes/monai-label modes/monai-label @@ -45,6 +45,8 @@ cp ../config/monai_label.js platform/app/public/config/monai_label.js yarn config set workspaces-experimental true yarn install +yarn run cli list + APP_CONFIG=config/monai_label.js PUBLIC_URL=/ohif/ QUICK_BUILD=true yarn run build rm -rf ${install_dir} diff --git a/plugins/ohifv3/extensions.patch b/plugins/ohifv3/extensions.patch index 83d3be2bc..602ec5f75 100644 --- a/plugins/ohifv3/extensions.patch +++ b/plugins/ohifv3/extensions.patch @@ -9,7 +9,7 @@ index 08a42deb0..69e5aa005 100644 + { + "packageName": "@ohif/extension-monai-label", + "default": false, -+ "version": "0.0.1" ++ "version": "3.0.0" + }, { "packageName": "@ohif/extension-dicom-microscopy", @@ -20,6 +20,7 @@ index 08a42deb0..69e5aa005 100644 }, + { + "packageName": "@ohif/mode-monai-label" ++ "version": "3.0.0" + }, { "packageName": "@ohif/mode-tmtv" diff --git a/plugins/ohifv3/extensions/monai-label/.webpack/webpack.dev.js b/plugins/ohifv3/extensions/monai-label/.webpack/webpack.dev.js new file mode 100644 index 000000000..6aea859ca --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/plugins/ohifv3/extensions/monai-label/package.json b/plugins/ohifv3/extensions/monai-label/package.json index 753bfafff..bd4932fee 100644 --- a/plugins/ohifv3/extensions/monai-label/package.json +++ b/plugins/ohifv3/extensions/monai-label/package.json @@ -1,10 +1,11 @@ { "name": "@ohif/extension-monai-label", - "version": "0.0.1", + "version": "3.0.0", "description": "OHIFv3 extension for MONAI Label", "author": "OHIF,NVIDIA,KCL", "license": "MIT", - "main": "dist/umd/extension-monai-label/index.umd.js", + "main": "dist/ohif-extension-monai-label.umd.js", + "module": "src/index.tsx", "files": [ "dist/**", "public/**", @@ -14,7 +15,6 @@ "keywords": [ "ohif-extension" ], - "module": "src/index.tsx", "publishConfig": { "access": "public" }, @@ -24,41 +24,49 @@ "yarn": ">=1.18.0" }, "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", - "dev:my-extension": "yarn run dev", "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", - "build:package": "yarn run build", - "start": "yarn run dev" + "build:package-1": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "^3.7.0-beta.80", - "@ohif/extension-default": "^3.7.0-beta.80", - "@ohif/extension-cornerstone": "^3.7.0-beta.80", - "@ohif/i18n": "^3.7.0-beta.80", + "@ohif/core": "3.10.0-beta.5", + "@ohif/extension-cornerstone": "3.10.0-beta.5", + "@ohif/extension-default": "3.10.0-beta.5", + "@ohif/i18n": "3.10.0-beta.5", "prop-types": "^15.6.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-i18next": "^12.2.2", - "react-router": "^6.8.1", - "react-router-dom": "^6.8.1" + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1" }, "dependencies": { "@babel/runtime": "^7.20.13", + "@cornerstonejs/adapters": "^2.2.3", + "@cornerstonejs/core": "^2.2.3", + "@kitware/vtk.js": "32.1.0", + "react-color": "^2.19.3", + "md5.js": "^1.3.5", "axios": "^0.21.1", "arraybuffer-concat": "^0.0.1", "ndarray": "^1.0.19", "nrrd-js": "^0.2.1", "pako": "^2.0.3", - "react-color": "^2.19.3", "bootstrap": "^5.0.2", "react-select": "^4.3.1", - "chroma-js": "^2.1.2", - "itk": "^14.1.1" + "chroma-js": "^2.1.2" }, "devDependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/tools": "^1.16.4", + "@cornerstonejs/adapters": "^2.2.3", + "@cornerstonejs/core": "^2.2.3", + "@cornerstonejs/tools": "^2.2.3", "react-color": "^2.19.3" } } diff --git a/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.css b/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.css index 153d01263..78cd084b3 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.css +++ b/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.css @@ -8,23 +8,26 @@ } .modelSelector .selectBox { width: 100%; + color: #000; + font-size: smaller; + height: 18px; } .modelSelector .actionButton { border: 2px solid #000; border-radius: 15px; - background-color: #add8e6; + background-color: #00a4d9; color: var(--ui-gray-dark); - line-height: 25px; + line-height: 20px; padding: 0 15px; outline: none; cursor: pointer; } .modelSelector .actionButton:hover, .modelSelector .actionButton:active { - background-color: var(--ui-sky-blue); + background-color: #00a4d9; } .modelSelector .actionButton:disabled { - background-color: var(--ui-sky-blue); + background-color: #00a4d9; } .modelSelector .actionButton svg { margin-right: 4px; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.tsx b/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.tsx index b70475148..9b57544a5 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.tsx @@ -12,7 +12,6 @@ export default class ModelSelector extends Component { usage: PropTypes.any, onClick: PropTypes.func, onSelectModel: PropTypes.func, - scribblesSelector: PropTypes.any, }; constructor(props) { @@ -21,8 +20,8 @@ export default class ModelSelector extends Component { const currentModel = props.currentModel ? props.currentModel : props.models.length > 0 - ? props.models[0] - : ''; + ? props.models[0] + : ''; this.state = { models: props.models, currentModel: currentModel, @@ -107,7 +106,6 @@ export default class ModelSelector extends Component { - {this.props.scribblesSelector} {this.props.usage} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.css b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.css index 6c069c0ed..3ad7b2fa4 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.css +++ b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.css @@ -1,14 +1,12 @@ @import url("w3.css"); .monaiLabelPanel { - background-color: #789; + background-color: #000; height: 100%; width: 100%; - display: flex; flex-direction: column; color: var(--text-primary-color); padding: 2px; - overflow-y: scroll; /* Make the panel scrollable vertically */ /* Accordion styles */ } .monaiLabelPanel .subtitle { @@ -23,7 +21,7 @@ border-radius: 4px; overflow: auto; box-shadow: 0 4px 4px -2px rgba(0,0,0,0.5); - background: #000; + background: #91b9cd; margin: 1rem 0; } .monaiLabelPanel .tab { @@ -61,11 +59,11 @@ .monaiLabelPanel .tab-content { max-height: 0; padding: 0 1em; - background: #808080; + background: #000; transition: all 0.35s; width: 90%; font-size: small; - color: #000; + color: #fff; } .monaiLabelPanel .tab-close { display: flex; @@ -79,7 +77,7 @@ background: #1a252f; } .monaiLabelPanel input:checked + .tab-label { - background: #000; + background: #00a4d9; } .monaiLabelPanel input:checked + .tab-label::after { transform: rotate(90deg); @@ -98,22 +96,21 @@ width: 100%; padding: 1px; border: 1px solid #000; - border-radius: 5px; color: #000; } .monaiLabelPanel .actionButton { border: 1px solid #000; border-radius: 15px; - background-color: #add8e6; + background-color: #00a4d9; color: #000; line-height: 25px; - padding: 10px; + padding: 10px 20px; outline: none; cursor: pointer; } .monaiLabelPanel .actionButton:hover, .monaiLabelPanel .actionButton:active { - background-color: var(--ui-sky-blue); + background-color: #00a4d9; } .monaiLabelPanel .actionButton:disabled { background-color: var(--ui-sky-blue); @@ -122,6 +119,7 @@ margin-right: 4px; position: relative; top: 2px; + bottom: 2px; } .scrollbar { overflow-y: scroll; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx index 5a13d1f54..be6b75703 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx @@ -1,17 +1,18 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { cache, triggerEvent, eventTarget } from '@cornerstonejs/core'; -import { Enums } from '@cornerstonejs/tools'; import './MonaiLabelPanel.css'; -import SettingsTable from './SettingsTable'; -import AutoSegmentation from './actions/AutoSegmentation'; -import SmartEdit from './actions/SmartEdit'; -import OptionTable from './actions/OptionTable'; import ActiveLearning from './actions/ActiveLearning'; +import AutoSegmentation from './actions/AutoSegmentation'; +import PointPrompts from './actions/PointPrompts'; +import ClassPrompts from './actions/ClassPrompts'; import MonaiLabelClient from '../services/MonaiLabelClient'; +import { hideNotification, getLabelColor } from '../utils/GenericUtils'; +import { Enums } from '@cornerstonejs/tools'; +import { cache, triggerEvent, eventTarget } from '@cornerstonejs/core'; import SegmentationReader from '../utils/SegmentationReader'; -import MonaiSegmentation from './MonaiSegmentation'; -import SegmentationToolbox from './SegmentationToolbox'; +import { currentSegmentsInfo } from '../utils/SegUtils'; +import SettingsTable from './SettingsTable'; +import * as cornerstoneTools from '@cornerstonejs/tools'; export default class MonaiLabelPanel extends Component { static propTypes = { @@ -24,14 +25,16 @@ export default class MonaiLabelPanel extends Component { settings: any; state: { info: {}; action: {} }; actions: { - options: any; activelearning: any; segmentation: any; - smartedit: any; + pointprompts: any; + classprompts: any; }; props: any; SeriesInstanceUID: any; StudyInstanceUID: any; + FrameOfReferenceUID: any; + displaySetInstanceUID: any; constructor(props) { super(props); @@ -40,64 +43,38 @@ export default class MonaiLabelPanel extends Component { props.servicesManager.services; this.notification = uiNotificationService; - this.settings = React.createRef(); - this.actions = { - options: React.createRef(), - activeLearning: React.createRef(), + activelearning: React.createRef(), segmentation: React.createRef(), - smartedit: React.createRef(), + pointprompts: React.createRef(), + classprompts: React.createRef(), }; this.state = { - info: {}, + info: { models: [], datasets: [] }, action: {}, - segmentations: [], }; - // Todo: fix this hack - setTimeout(() => { - const { viewports, activeViewportId } = viewportGridService.getState(); - const viewport = viewports.get(activeViewportId); - const displaySet = displaySetService.getDisplaySetByUID( - viewport.displaySetInstanceUIDs[0] - ); - - this.SeriesInstanceUID = displaySet.SeriesInstanceUID; - this.StudyInstanceUID = displaySet.StudyInstanceUID; - this.FrameOfReferenceUID = displaySet.instances[0].FrameOfReferenceUID; - this.displaySetInstanceUID = displaySet.displaySetInstanceUID; - }, 1000); - } + viewportGridService.subscribe( + viewportGridService.EVENTS.GRID_SIZE_CHANGED, + () => { + const { viewports, activeViewportId } = viewportGridService.getState(); + const viewport = viewports.get(activeViewportId); - async componentDidMount() { - const { segmentationService } = this.props.servicesManager.services; - const added = segmentationService.EVENTS.SEGMENTATION_ADDED; - const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED; - const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED; - const subscriptions = []; - - [added, updated, removed].forEach((evt) => { - const { unsubscribe } = segmentationService.subscribe(evt, () => { - const segmentations = segmentationService.getSegmentations(); - - if (!segmentations?.length) { + if (!viewport) { return; } - this.setState({ segmentations }); - }); - subscriptions.push(unsubscribe); - }); - - this.unsubscribe = () => { - subscriptions.forEach((unsubscribe) => unsubscribe()); - }; - } + const displaySet = displaySetService.getDisplaySetByUID( + viewport.displaySetInstanceUIDs[0] + ); - // componentDidUnmount? Doesn't exist this method anymore in V3? - async componentWillUnmount() { - this.unsubscribe(); + this.SeriesInstanceUID = displaySet.SeriesInstanceUID; + this.StudyInstanceUID = displaySet.StudyInstanceUID; + this.FrameOfReferenceUID = displaySet.instances[0].FrameOfReferenceUID; + this.displaySetInstanceUID = displaySet.displaySetInstanceUID; + } + ); } client = () => { @@ -110,57 +87,164 @@ export default class MonaiLabelPanel extends Component { ); }; + segmentColor(label) { + const color = getLabelColor(label); + const rgbColor = []; + for (const key in color) { + rgbColor.push(color[key]); + } + rgbColor.push(255); + return rgbColor; + } + + getActiveViewportInfo = () => { + const { viewportGridService } = this.props.servicesManager.services; + const { viewports, activeViewportId } = viewportGridService.getState(); + const viewport = viewports.get(activeViewportId); + return viewport; + } + onInfo = async () => { - this.notification.show({ + const nid = this.notification.show({ title: 'MONAI Label', message: 'Connecting to MONAI Label', type: 'info', - duration: 3000, + duration: 2000, }); const response = await this.client().info(); + console.log(response.data); - // remove the background - const labels = response.data.labels.splice(1); - - const segmentations = [ - { - id: '1', - label: 'Segmentations', - segments: labels.map((label, index) => ({ - segmentIndex: index + 1, - label, - })), - isActive: true, - activeSegmentIndex: 1, - }, - ]; - - this.props.commandsManager.runCommand('loadSegmentationsForViewport', { - segmentations, - }); - + hideNotification(nid, this.notification); if (response.status !== 200) { this.notification.show({ title: 'MONAI Label', - message: 'Failed to Connect to MONAI Label Server', + message: 'Failed to Connect to MONAI Label', type: 'error', duration: 5000, }); - } else { - this.notification.show({ - title: 'MONAI Label', - message: 'Connected to MONAI Label Server - Successful', - type: 'success', - duration: 2000, + return; + } + + this.notification.show({ + title: 'MONAI Label', + message: 'Connected to MONAI Label - Successful', + type: 'success', + duration: 2000, + }); + + const all_models = response.data.models; + const all_model_names = Object.keys(all_models); + const deepgrow_models = all_model_names.filter( + (m) => all_models[m].type === 'deepgrow' + ); + const deepedit_models = all_model_names.filter( + (m) => all_models[m].type === 'deepedit' + ); + const vista3d_models = all_model_names.filter( + (m) => all_models[m].type === 'vista3d' + ); + const segmentation_models = all_model_names.filter( + (m) => all_models[m].type === 'segmentation' + ); + const models = deepgrow_models + .concat(deepedit_models) + .concat(vista3d_models) + .concat(segmentation_models); + const all_labels = response.data.labels; + + const modelLabelToIdxMap = {}; + const modelIdxToLabelMap = {}; + const modelLabelNames = {}; + const modelLabelIndices = {}; + for (const model of models) { + const labels = all_models[model]['labels']; + modelLabelToIdxMap[model] = {}; + modelIdxToLabelMap[model] = {}; + if (Array.isArray(labels)) { + for (let label_idx = 1; label_idx <= labels.length; label_idx++) { + const label = labels[label_idx-1]; + all_labels.push(label); + modelLabelToIdxMap[model][label] = label_idx; + modelIdxToLabelMap[model][label_idx] = label; + } + } else { + for (const label of Object.keys(labels)) { + const label_idx = labels[label]; + all_labels.push(label); + modelLabelToIdxMap[model][label] = label_idx; + modelIdxToLabelMap[model][label_idx] = label; + } + } + modelLabelNames[model] = [ + ...Object.keys(modelLabelToIdxMap[model]), + ].sort(); + modelLabelIndices[model] = [...Object.keys(modelIdxToLabelMap[model])] + .sort() + .map(Number); + } + + const labelsOrdered = [...new Set(all_labels)].sort(); + const segmentations = [ + { + segmentationId: '1', + representation: { + type: Enums.SegmentationRepresentations.Labelmap, + }, + config: { + label: 'Segmentations', + segments: labelsOrdered.reduce((acc, label, index) => { + acc[index + 1] = { + segmentIndex: index + 1, + label: label, + active: index === 0, // First segment is active + locked: false, + color: this.segmentColor(label), + }; + return acc; + }, {}), + }, + }, + ]; + + const initialSegs = segmentations[0].config.segments; + const volumeLoadObject = cache.getVolume('1'); + if (!volumeLoadObject) { + this.props.commandsManager.runCommand('loadSegmentationsForViewport', { + segmentations, }); - this.setState({ info: response.data }); + // Wait for Above Segmentations to be added/available + setTimeout(() => { + const { viewportId } = this.getActiveViewportInfo(); + for (const segmentIndex of Object.keys(initialSegs)) { + cornerstoneTools.segmentation.config.color.setSegmentIndexColor( + viewportId, + '1', + initialSegs[segmentIndex].segmentIndex, + initialSegs[segmentIndex].color, + ); + } + }, 1000); } + + const info = { + models: models, + labels: labelsOrdered, + data: response.data, + modelLabelToIdxMap: modelLabelToIdxMap, + modelIdxToLabelMap: modelIdxToLabelMap, + modelLabelNames: modelLabelNames, + modelLabelIndices: modelLabelIndices, + initialSegs: initialSegs, + }; + + console.log(info); + this.setState({ info: info }); + this.setState({ isDataReady: true }); // Mark as ready }; onSelectActionTab = (name) => { - // Leave Event for (const action of Object.keys(this.actions)) { if (this.state.action === action) { if (this.actions[action].current) { @@ -169,7 +253,6 @@ export default class MonaiLabelPanel extends Component { } } - // Enter Event for (const action of Object.keys(this.actions)) { if (name === action) { if (this.actions[action].current) { @@ -180,224 +263,161 @@ export default class MonaiLabelPanel extends Component { this.setState({ action: name }); }; - onOptionsConfig = () => { - return this.actions['options'].current && - this.actions['options'].current.state - ? this.actions['options'].current.state.config - : {}; - }; - - _update = async (response, labelNames) => { - // Process the obtained binary file from the MONAI Label server - /* const onInfoLabelNames = this.state.info.labels */ - const onInfoLabelNames = labelNames; - - console.info('These are the predicted labels'); - console.info(onInfoLabelNames); - - if (onInfoLabelNames.hasOwnProperty('background')) { - delete onInfoLabelNames.background; - } - + updateView = async (response, model_id, labels, override = false, point_prompts = false) => { + console.log('Update View: ', model_id, labels, override); const ret = SegmentationReader.parseNrrdData(response.data); - if (!ret) { throw new Error('Failed to parse NRRD data'); } - const { image: buffer, header } = ret; - const data = new Uint16Array(buffer); + const labelNames = {}; + const currentSegs = currentSegmentsInfo( + this.props.servicesManager.services.segmentationService + ); + const modelToSegMapping = {}; + modelToSegMapping[0] = 0; + + let tmp_model_seg_idx = 1; + for (const label of labels) { + const s = currentSegs.info[label]; + if (!s) { + for (let i = 1; i <= 255; i++) { + if (!currentSegs.indices.has(i)) { + labelNames[label] = i; + currentSegs.indices.add(i); + break; + } + } + } else { + labelNames[label] = s.segmentIndex; + } - // reformat centroids - const centroidsIJK = new Map(); - for (const [key, value] of Object.entries(response.centroids)) { - const segmentIndex = parseInt(value[0], 10); - const image = value.slice(1).map((v) => parseFloat(v)); - centroidsIJK.set(segmentIndex, { image: image, world: [] }); + const seg_idx = labelNames[label]; + let model_seg_idx = this.state.info.modelLabelToIdxMap[model_id][label]; + model_seg_idx = model_seg_idx ? model_seg_idx : tmp_model_seg_idx; + modelToSegMapping[model_seg_idx] = 0xff & seg_idx; + tmp_model_seg_idx++; } - const segmentations = [ - { - id: '1', - label: 'Segmentations', - segments: Object.keys(onInfoLabelNames).map((key) => ({ - segmentIndex: onInfoLabelNames[key], - label: key, - })), - isActive: true, - activeSegmentIndex: 1, - scalarData: data, - FrameOfReferenceUID: this.FrameOfReferenceUID, - centroidsIJK: centroidsIJK, - }, - ]; + console.log('Index Remap', labels, modelToSegMapping); + const data = new Uint8Array(ret.image); - // Todo: rename volumeId - const volumeLoadObject = cache.getVolume('1'); + const { segmentationService } = this.props.servicesManager.services; + const volumeLoadObject = segmentationService.getLabelmapVolume('1'); if (volumeLoadObject) { - const { scalarData } = volumeLoadObject; - scalarData.set(data); + console.log('Volume Object is In Cache....'); + let convertedData = data; + for (let i = 0; i < convertedData.length; i++) { + const midx = convertedData[i]; + const sidx = modelToSegMapping[midx]; + if (midx && sidx) { + convertedData[i] = sidx; + } else if (override && point_prompts && labels.length === 1) { + convertedData[i] = midx ? labelNames[labels[0]] : 0; + } else if (labels.length > 0) { + convertedData[i] = 0; + } + } + + if (override === true) { + const { segmentationService } = this.props.servicesManager.services; + const volumeLoadObject = segmentationService.getLabelmapVolume('1'); + const { voxelManager } = volumeLoadObject; + const scalarData = voxelManager?.getCompleteScalarDataArray() + + // console.log('Current ScalarData: ', scalarData); + const currentSegArray = new Uint8Array(scalarData.length); + currentSegArray.set(scalarData); + + // get unique values to determine which organs to update, keep rest + const updateTargets = new Set(convertedData); + for (let i = 0; i < convertedData.length; i++) { + if ( + convertedData[i] !== 255 && + updateTargets.has(currentSegArray[i]) + ) { + currentSegArray[i] = convertedData[i]; + } + } + convertedData = currentSegArray; + } + const { voxelManager } = volumeLoadObject; + voxelManager?.setCompleteScalarDataArray(convertedData); triggerEvent(eventTarget, Enums.Events.SEGMENTATION_DATA_MODIFIED, { segmentationId: '1', }); - console.debug("updated the segmentation's scalar data"); + console.log("updated the segmentation's scalar data"); } else { - this.props.commandsManager.runCommand('hydrateSegmentationsForViewport', { - segmentations, - }); + console.log('TODO:: Volume Object is NOT In Cache....'); } }; - _debug = async () => { - const nrrdFetch = await fetch('http://localhost:3000/pred2.nrrd'); - - const info = { - spleen: 1, - 'right kidney': 2, - 'left kidney': 3, - liver: 6, - stomach: 7, - aorta: 8, - 'inferior vena cava': 9, - }; - - const nrrd = await nrrdFetch.arrayBuffer(); - - this._update({ data: nrrd }, info); - }; - - parseResponse = (response) => { - const buffer = response.data; - const contentType = response.headers['content-type']; - const boundaryMatch = contentType.match(/boundary=([^;]+)/i); - const boundary = boundaryMatch ? boundaryMatch[1] : null; - - const text = new TextDecoder().decode(buffer); - const parts = text - .split(`--${boundary}`) - .filter((part) => part.trim() !== ''); - - // Find the JSON part and NRRD part - const jsonPart = parts.find((part) => - part.includes('Content-Type: application/json') - ); - const nrrdPart = parts.find((part) => - part.includes('Content-Type: application/octet-stream') - ); - - // Extract JSON data - const jsonStartIndex = jsonPart.indexOf('{'); - const jsonEndIndex = jsonPart.lastIndexOf('}'); - const jsonData = JSON.parse( - jsonPart.slice(jsonStartIndex, jsonEndIndex + 1) - ); - - // Extract NRRD data - const binaryData = nrrdPart.split('\r\n\r\n')[1]; - const binaryDataEnd = binaryData.lastIndexOf('\r\n'); - - const nrrdArrayBuffer = new Uint8Array( - binaryData - .slice(0, binaryDataEnd) - .split('') - .map((c) => c.charCodeAt(0)) - ).buffer; - - return { data: nrrdArrayBuffer, centroids: jsonData.centroids }; - }; + async componentDidMount() { + if (this.state.isDataReady) { + return; + } - updateView = async (response, labelNames) => { - const { data, centroids } = this.parseResponse(response); - this._update({ data, centroids }, labelNames); - }; + console.log('(Component Mounted) Connect to MONAI Server...'); + // await this.onInfo(); + } render() { + const { isDataReady, isInteractiveSeg } = this.state; return (

-
-

{this.state.info.name}

- {/* */} -
- - - - - - -
- - {this.state.segmentations?.map((segmentation) => ( - <> - - + + + - - ))} +
+ )} ); } diff --git a/plugins/ohifv3/extensions/monai-label/src/components/MonaiSegmentation.tsx b/plugins/ohifv3/extensions/monai-label/src/components/MonaiSegmentation.tsx deleted file mode 100644 index f112a84c8..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/MonaiSegmentation.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { createReportAsync } from '@ohif/extension-default'; -import React, { useEffect, useState, useCallback } from 'react'; -import PropTypes from 'prop-types'; -import { SegmentationGroupTable } from '@ohif/ui'; - -import callInputDialog from './callInputDialog'; -import callColorPickerDialog from './colorPickerDialog'; -import { useTranslation } from 'react-i18next'; - -export default function MonaiSegmentation({ - servicesManager, - commandsManager, - extensionManager, - configuration, -}) { - const { segmentationService, uiDialogService } = servicesManager.services; - - const { t } = useTranslation('PanelSegmentation'); - - const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); - const [segmentationConfiguration, setSegmentationConfiguration] = useState( - segmentationService.getConfiguration() - ); - - const [segmentations, setSegmentations] = useState(() => - segmentationService.getSegmentations() - ); - - useEffect(() => { - // ~~ Subscription - const added = segmentationService.EVENTS.SEGMENTATION_ADDED; - const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED; - const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED; - const subscriptions = []; - - [added, updated, removed].forEach((evt) => { - const { unsubscribe } = segmentationService.subscribe(evt, () => { - const segmentations = segmentationService.getSegmentations(); - setSegmentations(segmentations); - setSegmentationConfiguration(segmentationService.getConfiguration()); - }); - subscriptions.push(unsubscribe); - }); - - return () => { - subscriptions.forEach((unsub) => { - unsub(); - }); - }; - }, []); - - const getToolGroupIds = (segmentationId) => { - const toolGroupIds = - segmentationService.getToolGroupIdsWithSegmentation(segmentationId); - - return toolGroupIds; - }; - - const onSegmentationAdd = async () => { - commandsManager.runCommand('addSegmentationForActiveViewport'); - }; - - const onSegmentationClick = (segmentationId: string) => { - segmentationService.setActiveSegmentationForToolGroup(segmentationId); - }; - - const onSegmentationDelete = (segmentationId: string) => { - segmentationService.remove(segmentationId); - }; - - const onSegmentAdd = (segmentationId) => { - segmentationService.addSegment(segmentationId); - }; - - const onSegmentClick = (segmentationId, segmentIndex) => { - segmentationService.setActiveSegment(segmentationId, segmentIndex); - - const toolGroupIds = getToolGroupIds(segmentationId); - - toolGroupIds.forEach((toolGroupId) => { - // const toolGroupId = - segmentationService.setActiveSegmentationForToolGroup( - segmentationId, - toolGroupId - ); - segmentationService.jumpToSegmentCenter( - segmentationId, - segmentIndex, - toolGroupId - ); - }); - }; - - const onSegmentEdit = (segmentationId, segmentIndex) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - - const segment = segmentation.segments[segmentIndex]; - const { label } = segment; - - callInputDialog(uiDialogService, label, (label, actionId) => { - if (label === '') { - return; - } - - segmentationService.setSegmentLabel(segmentationId, segmentIndex, label); - }); - }; - - const onSegmentationEdit = (segmentationId) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - const { label } = segmentation; - - callInputDialog(uiDialogService, label, (label, actionId) => { - if (label === '') { - return; - } - - segmentationService.addOrUpdateSegmentation( - { - id: segmentationId, - label, - }, - false, // suppress event - true // notYetUpdatedAtSource - ); - }); - }; - - const onSegmentColorClick = (segmentationId, segmentIndex) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - - const segment = segmentation.segments[segmentIndex]; - const { color, opacity } = segment; - - const rgbaColor = { - r: color[0], - g: color[1], - b: color[2], - a: opacity / 255.0, - }; - - callColorPickerDialog( - uiDialogService, - rgbaColor, - (newRgbaColor, actionId) => { - if (actionId === 'cancel') { - return; - } - - segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [ - newRgbaColor.r, - newRgbaColor.g, - newRgbaColor.b, - newRgbaColor.a * 255.0, - ]); - } - ); - }; - - const onSegmentDelete = (segmentationId, segmentIndex) => { - segmentationService.removeSegment(segmentationId, segmentIndex); - }; - - const onToggleSegmentVisibility = (segmentationId, segmentIndex) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - const segmentInfo = segmentation.segments[segmentIndex]; - const isVisible = !segmentInfo.isVisible; - const toolGroupIds = getToolGroupIds(segmentationId); - - // Todo: right now we apply the visibility to all tool groups - toolGroupIds.forEach((toolGroupId) => { - segmentationService.setSegmentVisibility( - segmentationId, - segmentIndex, - isVisible, - toolGroupId - ); - }); - }; - - const onToggleSegmentLock = (segmentationId, segmentIndex) => { - segmentationService.toggleSegmentLocked(segmentationId, segmentIndex); - }; - - const onToggleSegmentationVisibility = (segmentationId) => { - segmentationService.toggleSegmentationVisibility(segmentationId); - }; - - const _setSegmentationConfiguration = useCallback( - (segmentationId, key, value) => { - segmentationService.setConfiguration({ - segmentationId, - [key]: value, - }); - }, - [segmentationService] - ); - - const onSegmentationDownload = (segmentationId) => { - commandsManager.runCommand('downloadSegmentation', { - segmentationId, - }); - }; - - const onSegmentationDownloadRTSS = (segmentationId) => {}; - - const storeSegmentation = (segmentationId) => { - const datasources = extensionManager.getActiveDataSource(); - - const getReport = async () => { - return await commandsManager.runCommand('storeSegmentation', { - segmentationId, - dataSource: datasources[0], - }); - }; - - createReportAsync({ - servicesManager, - getReport, - reportType: 'Segmentation', - }); - }; - - return ( - <> -
- - _setSegmentationConfiguration( - selectedSegmentationId, - 'renderOutline', - value - ) - } - setOutlineOpacityActive={(value) => - _setSegmentationConfiguration( - selectedSegmentationId, - 'outlineOpacity', - value - ) - } - setRenderFill={(value) => - _setSegmentationConfiguration( - selectedSegmentationId, - 'renderFill', - value - ) - } - setRenderInactiveSegmentations={(value) => - _setSegmentationConfiguration( - selectedSegmentationId, - 'renderInactiveSegmentations', - value - ) - } - setOutlineWidthActive={(value) => - _setSegmentationConfiguration( - selectedSegmentationId, - 'outlineWidthActive', - value - ) - } - setFillAlpha={(value) => - _setSegmentationConfiguration( - selectedSegmentationId, - 'fillAlpha', - value - ) - } - setFillAlphaInactive={(value) => - _setSegmentationConfiguration( - selectedSegmentationId, - 'fillAlphaInactive', - value - ) - } - /> -
- - ); -} - -MonaiSegmentation.propTypes = { - commandsManager: PropTypes.shape({ - runCommand: PropTypes.func.isRequired, - }), - servicesManager: PropTypes.shape({ - services: PropTypes.shape({ - segmentationService: PropTypes.shape({ - getSegmentation: PropTypes.func.isRequired, - getSegmentations: PropTypes.func.isRequired, - toggleSegmentationVisibility: PropTypes.func.isRequired, - subscribe: PropTypes.func.isRequired, - EVENTS: PropTypes.object.isRequired, - }).isRequired, - }).isRequired, - }).isRequired, -}; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/SegmentationToolbox.tsx b/plugins/ohifv3/extensions/monai-label/src/components/SegmentationToolbox.tsx deleted file mode 100644 index 338b389ab..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/SegmentationToolbox.tsx +++ /dev/null @@ -1,376 +0,0 @@ -import React, { useCallback, useEffect, useState, useReducer } from 'react'; -import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui'; -import { Types } from '@ohif/extension-cornerstone'; -import { utilities } from '@cornerstonejs/tools'; - -const { segmentation: segmentationUtils } = utilities; - -const ACTIONS = { - SET_BRUSH_SIZE: 'SET_BRUSH_SIZE', - SET_TOOL_CONFIG: 'SET_TOOL_CONFIG', - SET_ACTIVE_TOOL: 'SET_ACTIVE_TOOL', -}; - -const initialState = { - Brush: { - brushSize: 15, - mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush' - }, - Eraser: { - brushSize: 15, - mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser' - }, - Scissors: { - brushSize: 15, - mode: 'CircleScissor', // E.g., 'CircleScissor', 'RectangleScissor', or 'SphereScissor' - }, - ThresholdBrush: { - brushSize: 15, - thresholdRange: [-500, 500], - }, - activeTool: null, -}; - -function toolboxReducer(state, action) { - switch (action.type) { - case ACTIONS.SET_TOOL_CONFIG: - const { tool, config } = action.payload; - return { - ...state, - [tool]: { - ...state[tool], - ...config, - }, - }; - case ACTIONS.SET_ACTIVE_TOOL: - return { ...state, activeTool: action.payload }; - default: - return state; - } -} - -function SegmentationToolbox({ servicesManager, extensionManager }) { - const { toolbarService, segmentationService, toolGroupService } = - servicesManager.services as Types.CornerstoneServices; - - const [viewportGrid] = useViewportGrid(); - const { viewports, activeViewportId } = viewportGrid; - - const [toolsEnabled, setToolsEnabled] = useState(false); - const [state, dispatch] = useReducer(toolboxReducer, initialState); - - const updateActiveTool = useCallback(() => { - if (!viewports?.size || activeViewportId === undefined) { - return; - } - - const viewport = viewports.get(activeViewportId); - - dispatch({ - type: ACTIONS.SET_ACTIVE_TOOL, - payload: toolGroupService.getActiveToolForViewport(viewport.viewportId), - }); - }, [activeViewportId, viewports, toolGroupService, dispatch]); - - /** - * sets the tools enabled IF there are segmentations - */ - useEffect(() => { - const events = [ - segmentationService.EVENTS.SEGMENTATION_ADDED, - segmentationService.EVENTS.SEGMENTATION_UPDATED, - ]; - - const unsubscriptions = []; - - events.forEach((event) => { - const { unsubscribe } = segmentationService.subscribe(event, () => { - const segmentations = segmentationService.getSegmentations(); - - const activeSegmentation = segmentations?.find((seg) => seg.isActive); - - setToolsEnabled(activeSegmentation?.segmentCount > 0); - }); - - unsubscriptions.push(unsubscribe); - }); - - return () => { - unsubscriptions.forEach((unsubscribe) => unsubscribe()); - }; - }, [activeViewportId, viewports, segmentationService]); - - /** - * Update the active tool when the toolbar state changes - */ - useEffect(() => { - const { unsubscribe } = toolbarService.subscribe( - toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED, - () => { - updateActiveTool(); - } - ); - - return () => { - unsubscribe(); - }; - }, [toolbarService, updateActiveTool]); - - const setToolActive = useCallback( - (toolName) => { - toolbarService.recordInteraction({ - groupId: 'SegmentationTools', - itemId: 'Brush', - interactionType: 'tool', - commands: [ - { - commandName: 'setToolActive', - commandOptions: { - toolName, - }, - }, - ], - }); - - dispatch({ type: ACTIONS.SET_ACTIVE_TOOL, payload: toolName }); - }, - [toolbarService, dispatch] - ); - - const updateBrushSize = useCallback( - (toolName, brushSize) => { - toolGroupService.getToolGroupIds()?.forEach((toolGroupId) => { - segmentationUtils.setBrushSizeForToolGroup( - toolGroupId, - brushSize, - toolName - ); - }); - }, - [toolGroupService] - ); - - const onBrushSizeChange = useCallback( - (valueAsStringOrNumber, toolCategory) => { - const value = Number(valueAsStringOrNumber); - - _getToolNamesFromCategory(toolCategory).forEach((toolName) => { - updateBrushSize(toolName, value); - }); - - dispatch({ - type: ACTIONS.SET_TOOL_CONFIG, - payload: { - tool: toolCategory, - config: { brushSize: value }, - }, - }); - }, - [toolGroupService, dispatch] - ); - - const handleRangeChange = useCallback( - (newRange) => { - if ( - newRange[0] === state.ThresholdBrush.thresholdRange[0] && - newRange[1] === state.ThresholdBrush.thresholdRange[1] - ) { - return; - } - - const toolNames = _getToolNamesFromCategory('ThresholdBrush'); - - toolNames.forEach((toolName) => { - toolGroupService.getToolGroupIds()?.forEach((toolGroupId) => { - const toolGroup = toolGroupService.getToolGroup(toolGroupId); - toolGroup.setToolConfiguration(toolName, { - strategySpecificConfiguration: { - THRESHOLD_INSIDE_CIRCLE: { - threshold: newRange, - }, - }, - }); - }); - }); - - dispatch({ - type: ACTIONS.SET_TOOL_CONFIG, - payload: { - tool: 'ThresholdBrush', - config: { thresholdRange: newRange }, - }, - }); - }, - [toolGroupService, dispatch, state.ThresholdBrush.thresholdRange] - ); - - return ( - setToolActive('CircularBrush'), - options: [ - { - name: 'Radius (mm)', - id: 'brush-radius', - type: 'range', - min: 0.01, - max: 100, - value: state.Brush.brushSize, - step: 0.5, - onChange: (value) => onBrushSizeChange(value, 'Brush'), - }, - { - name: 'Mode', - type: 'radio', - id: 'brush-mode', - value: state.Brush.mode, - values: [ - { value: 'CircularBrush', label: 'Circle' }, - { value: 'SphereBrush', label: 'Sphere' }, - ], - onChange: (value) => setToolActive(value), - }, - ], - }, - { - name: 'Eraser', - icon: 'icon-tool-eraser', - disabled: !toolsEnabled, - active: - state.activeTool === 'CircularEraser' || - state.activeTool === 'SphereEraser', - onClick: () => setToolActive('CircularEraser'), - options: [ - { - name: 'Radius (mm)', - type: 'range', - id: 'eraser-radius', - min: 0.01, - max: 100, - value: state.Eraser.brushSize, - step: 0.5, - onChange: (value) => onBrushSizeChange(value, 'Eraser'), - }, - { - name: 'Mode', - type: 'radio', - id: 'eraser-mode', - value: state.Eraser.mode, - values: [ - { value: 'CircularEraser', label: 'Circle' }, - { value: 'SphereEraser', label: 'Sphere' }, - ], - onChange: (value) => setToolActive(value), - }, - ], - }, - { - name: 'Scissor', - icon: 'icon-tool-scissor', - disabled: !toolsEnabled, - active: - state.activeTool === 'CircleScissor' || - state.activeTool === 'RectangleScissor' || - state.activeTool === 'SphereScissor', - onClick: () => setToolActive('CircleScissor'), - options: [ - { - name: 'Mode', - type: 'radio', - value: state.Scissors.mode, - id: 'scissor-mode', - values: [ - { value: 'CircleScissor', label: 'Circle' }, - { value: 'RectangleScissor', label: 'Rectangle' }, - { value: 'SphereScissor', label: 'Sphere' }, - ], - onChange: (value) => setToolActive(value), - }, - ], - }, - { - name: 'Threshold Tool', - icon: 'icon-tool-threshold', - disabled: !toolsEnabled, - active: - state.activeTool === 'ThresholdCircularBrush' || - state.activeTool === 'ThresholdSphereBrush', - onClick: () => setToolActive('ThresholdCircularBrush'), - options: [ - { - name: 'Radius (mm)', - id: 'threshold-radius', - type: 'range', - min: 0.01, - max: 100, - value: state.ThresholdBrush.brushSize, - step: 0.5, - onChange: (value) => onBrushSizeChange(value, 'ThresholdBrush'), - }, - { - name: 'Mode', - type: 'radio', - id: 'threshold-mode', - value: state.activeTool, - values: [ - { value: 'ThresholdCircularBrush', label: 'Circle' }, - { value: 'ThresholdSphereBrush', label: 'Sphere' }, - ], - onChange: (value) => setToolActive(value), - }, - { - type: 'custom', - children: () => { - return ( -
-
-
Threshold
- -
- ); - }, - }, - ], - }, - ]} - /> - ); -} - -function _getToolNamesFromCategory(category) { - let toolNames = []; - switch (category) { - case 'Brush': - toolNames = ['CircularBrush', 'SphereBrush']; - break; - case 'Eraser': - toolNames = ['CircularEraser', 'SphereEraser']; - break; - case 'ThresholdBrush': - toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush']; - break; - default: - break; - } - - return toolNames; -} - -export default SegmentationToolbox; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.css b/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.css index 4cd128a74..abbca0c73 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.css +++ b/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.css @@ -3,6 +3,7 @@ border: 0 solid; width: 100%; font-size: small; + color: #fff; } .settingsTable tr td { border: 0 solid; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx b/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx index 12fe1a284..5da2ea563 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx @@ -8,9 +8,7 @@ export default class SettingsTable extends Component { constructor(props) { super(props); - const onInfo = props.onInfo; - this.onInfo = onInfo; - + this.onInfo = props.onInfo; this.state = this.getSettings(); } diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/ActiveLearning.css b/plugins/ohifv3/extensions/monai-label/src/components/actions/ActiveLearning.css deleted file mode 100644 index c69d56a48..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/ActiveLearning.css +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright (c) MONAI Consortium -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -.optionsTable { - font-family: arial, sans-serif; - border-collapse: collapse; - width: 100%; - font-size: smaller; -} -.optionsTable th { - border: 0px solid #070303; - text-align: left; - background-color: #789; -} -.optionsTable td { - border: 0px solid #070202; - text-align: left; -} -.optionsInput { - width: 100%; -} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/ActiveLearning.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/ActiveLearning.tsx index 9925c1146..af79d4bdf 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/ActiveLearning.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/ActiveLearning.tsx @@ -1,78 +1,43 @@ -/* -Copyright (c) MONAI Consortium -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - import React from 'react'; - -import './ActiveLearning.css'; import BaseTab from './BaseTab'; +import { cache, triggerEvent, eventTarget } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/tools'; import NextSampleForm from './NextSampleForm'; -import { createSegmentMetadata } from '../../utils/SegmentationUtils'; -export default class OptionTable extends BaseTab { +export default class ActiveLearning extends BaseTab { constructor(props) { super(props); - this.state = { - strategy: 'random', - training: false, - segmentId: 'liver', - }; } - onChangeStrategy = (evt) => { - this.setState({ strategy: evt.target.value }); - }; - - onSegmentSelected = (id) => { - this.setState({ segmentId: id }); - }; - - onSegmentDeleted = (id) => { - this.setState({ segmentId: null }); - }; - onClickNextSample = async () => { const nid = this.notification.show({ title: 'MONAI Label', - message: 'Running Active Learning strategy - ' + this.state.strategy, + message: 'Fetching Next Recommended Image for Annotation', type: 'info', - duration: 60000, + duration: 4000, }); - const strategy = this.state.strategy; - const config = this.props.onOptionsConfig(); - const params = - config && config.activelearning && config.activelearning[strategy] - ? config.activelearning[strategy] - : {}; - const response = await this.props.client().next_sample(strategy, params); + const response = await this.props.client().next_image(); + console.log(response.data); + if (!nid) { window.snackbar.hideAll(); } else { this.notification.hide(nid); } - if (response.status !== 200) { + if (response.status !== 201) { this.notification.show({ - title: 'MONAI Label', + title: 'Active Learning', message: 'Failed to Fetch Next Sample', type: 'error', - duration: 5000, + duration: 6000, }); } else { this.uiModelService.show({ content: NextSampleForm, contentProps: { - info: response.data, + info: response.data.meta, }, shouldCloseOnEsc: true, title: 'Active Learning - Next Sample', @@ -81,127 +46,103 @@ export default class OptionTable extends BaseTab { } }; - onClickUpdateModel = async () => { - const training = this.state.training; - console.debug('Current training status: ' + training); - const config = this.props.onOptionsConfig(); - const params = config && config.train ? config.train : {}; + onClickRecoverSeg = async () => { + const nid = this.notification.show({ + title: 'MONAI Label', + message: 'Recover Annotation from Most Recent Inference', + type: 'info', + duration: 2000, + }); + + const scalarDataRecover = window.ScalarDataBuffer; + const volumeLoadObject = cache.getVolume('1'); - const response = training - ? await this.props.client().stop_train() - : await this.props.client().run_train(params); + console.log(volumeLoadObject); - if (response.status !== 200) { - this.notification.show({ - title: 'MONAI Label', - message: 'Failed to ' + (training ? 'STOP' : 'RUN') + ' training', - type: 'error', - duration: 5000, + if (volumeLoadObject) { + const { scalarData } = volumeLoadObject; + scalarData.set(scalarDataRecover); + triggerEvent(eventTarget, Enums.Events.SEGMENTATION_DATA_MODIFIED, { + segmentationId: '1', }); + console.debug("updated the segmentation's scalar data"); } else { this.notification.show({ title: 'MONAI Label', - message: 'Model update task ' + (training ? 'STOPPED' : 'STARTED'), - type: 'success', - duration: 2000, + message: 'No latest inference to recover', + type: 'error', + duration: 6000, }); - this.setState({ training: !training }); - } - }; - - onClickSubmitLabel = async () => { - const labelmaps3D = cornerstone.cache.getVolume('1'); - - if (!labelmaps3D) { - console.info('LabelMap3D is empty.. so zero segments'); return; } + }; - this.notification.show({ - title: 'MONAI Label', - message: 'Preparing the labelmap to submit', - type: 'info', - duration: 5000, - }); - - const labelNames = this.props.info.labels; - const segments = []; - for (let i = 0; i < labelNames.length; i++) { - if (labelNames[i] === 'background') { - console.debug('Ignore Background...'); - continue; + onClickNotifyServer = async () => { + const { displaySetService } = this.props.servicesManager.services; + const activeDisplaySets = displaySetService.activeDisplaySets; + + // iterate all display sets and check if SEG exists, get series IDs + let segCount = 0; + let latestLabelSeriesTimestamp = 0; + let latestLabelSeriesInstanceUID = ''; + let latestImageSeriesInstanceUID = ''; + + for (const item of activeDisplaySets) { + if (item.Modality === 'SEG') { + segCount++; + const curLabelTimestamp = `${item.SeriesDate}${item.instance.SeriesTime}`; + if (curLabelTimestamp > latestLabelSeriesTimestamp) { + latestLabelSeriesTimestamp = curLabelTimestamp; + latestLabelSeriesInstanceUID = item.SeriesInstanceUID; + } + } else if (item.Modality === 'CT') { + //TODO: if image modality is other than CT, add others + latestImageSeriesInstanceUID = item.SeriesInstanceUID; } - const segment = createSegmentMetadata(labelNames[i], i, ''); - segments.push(segment); } - const params = { label_info: segments }; - - const image = this.props.viewConstants.SeriesInstanceUID; + if (segCount === 0) { + this.notification.show({ + title: 'MONAI Label', + message: 'No segmentation added.', + type: 'error', + duration: 5000, + }); + return; + } + const data = { + added: [], + updated: [], + removed: [], + }; - const label = new Blob([labelmaps3D.scalarData], { - type: 'application/octet-stream', + data[segCount > 1 ? 'updated' : 'added'].push({ + image: latestImageSeriesInstanceUID, + label: latestLabelSeriesInstanceUID, }); - const response = await this.props.client().save_label(image, label, params); + const response = await this.props.client().notify(data); - if (response.status !== 200) { + console.log(response.data); + if (response.status !== 201) { this.notification.show({ title: 'MONAI Label', - message: 'Failed to save label', + message: 'Failed. Response Error', type: 'error', duration: 5000, }); - } else { - this.notification.show({ - title: 'MONAI Label', - message: 'Label submitted to server', - type: 'success', - duration: 2000, - }); + return; } - }; - async componentDidMount() { - const training = await this.props.client().is_train_running(); - this.setState({ training: training }); - } + this.notification.show({ + title: 'MONAI Label', + message: 'Notified Server - Successful', + type: 'success', + duration: 2000, + }); + }; render() { - /* const segmentId = this.state.segmentId - ? this.state.segmentId - : getFirstSegmentId(this.props.viewConstants.element); */ - - const segmentId = this.state.segmentId; - - const ds = this.props.info.datastore; - const completed = ds && ds.completed ? ds.completed : 0; - const total = ds && ds.total ? ds.total : 1; - const activelearning = Math.round(100 * (completed / total)) + '%'; - const activelearningTip = completed + '/' + total + ' samples annotated'; - - const ts = this.props.info.train_stats - ? Object.values(this.props.info.train_stats)[0] - : null; - - const epochs = ts ? (ts.total_time ? 0 : ts.epoch ? ts.epoch : 1) : 0; - const total_epochs = ts && ts.total_epochs ? ts.total_epochs : 1; - const training = Math.round(100 * (epochs / total_epochs)) + '%'; - const trainingTip = epochs - ? epochs + '/' + total_epochs + ' epochs completed' - : 'Not Running'; - - const accuracy = - ts && ts.best_metric ? Math.round(100 * ts.best_metric) + '%' : '0%'; - const accuracyTip = - ts && ts.best_metric - ? accuracy + ' is current best metric' - : 'not determined'; - - const strategies = this.props.info.strategies - ? this.props.info.strategies - : {}; - return (
-   + + - {/* */} - {/**/} - {/* */} - {/* Submit Label*/} - {/* */} - {/**/}
- - - - - - - - - - - - - - - - - - - - - - - -
Strategy: - -
 
Annotated: -
-
- {activelearning} -
-
-
Training: -
-
- {training} -
-
-
Train Acc: -
-
- {accuracy} -
-
-
+ + this.onClickRecoverSeg()} + > + Recover Seg + +
); diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx index 7430ea967..20953d9f7 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx @@ -1,8 +1,11 @@ import React from 'react'; import ModelSelector from '../ModelSelector'; import BaseTab from './BaseTab'; +import { hideNotification } from '../../utils/GenericUtils'; export default class AutoSegmentation extends BaseTab { + modelSelector: any; + constructor(props) { super(props); @@ -13,66 +16,101 @@ export default class AutoSegmentation extends BaseTab { } onSelectModel = (model) => { + console.log('Selecting Auto Segmentation Model...'); + console.log(model); this.setState({ currentModel: model }); }; + getModels() { + const { info } = this.props; + const models = Object.keys(info.data.models).filter( + (m) => + info.data.models[m].type === 'segmentation' || + info.data.models[m].type === 'vista3d' + ); + return models; + } + onSegmentation = async () => { + const { currentModel, currentLabel, clickPoints } = this.state; + const { info, viewConstants } = this.props; + + const models = this.getModels(); + let selectedModel = 0; + for (const model of models) { + if (!currentModel || model === currentModel) { + break; + } + selectedModel++; + } + + const model = models.length > 0 ? models[selectedModel] : null; + if (!model) { + this.notification.show({ + title: 'MONAI Label', + message: 'Something went wrong: Model is not selected', + type: 'error', + duration: 10000, + }); + return; + } + const nid = this.notification.show({ - title: 'MONAI Label', + title: 'MONAI Label - ' + model, message: 'Running Auto-Segmentation...', type: 'info', - duration: 60000, + duration: 7000, }); - // TODO:: Fix Image ID... - const { info, viewConstants } = this.props; - const image = viewConstants.SeriesInstanceUID; - const model = this.modelSelector.current.currentModel(); - const config = this.props.onOptionsConfig(); - const params = - config && config.infer && config.infer[model] ? config.infer[model] : {}; + const params = {}; + const label_names = info.modelLabelNames[model]; + const label_classes = info.modelLabelIndices[model]; + if (info.data.models[model].type === 'vista3d') { + const bodyComponents = [ + 'kidney', + 'lung', + 'bone', + 'lung tumor', + 'uterus', + 'postcava', + ]; + const exclusionValues = bodyComponents.map( + (cls_name) => info.modelLabelToIdxMap[model][cls_name] + ); + const filteredLabelClasses = label_classes.filter( + (value) => !exclusionValues.includes(value) + ); + params['label_prompt'] = filteredLabelClasses; + } - const labels = info.models[model].labels; const response = await this.props .client() - .segmentation(model, image, params); - - // Bug:: Notification Service on show doesn't return id - if (!nid) { - window.snackbar.hideAll(); - } else { - this.notification.hide(nid); - } + .infer(model, viewConstants.SeriesInstanceUID, params); + console.log(response); + hideNotification(nid, this.notification); if (response.status !== 200) { this.notification.show({ - title: 'MONAI Label', + title: 'MONAI Label - ' + model, message: 'Failed to Run Segmentation', type: 'error', - duration: 5000, - }); - } else { - this.notification.show({ - title: 'MONAI Label', - message: 'Run Segmentation - Successful', - type: 'success', - duration: 2000, + duration: 6000, }); - - await this.props.updateView(response, labels); + return; } + + this.notification.show({ + title: 'MONAI Label - ' + model, + message: 'Running Segmentation - Successful', + type: 'success', + duration: 4000, + }); + + this.props.updateView(response, model, label_names); }; render() { - const models = []; - if (this.props.info && this.props.info.models) { - for (const [name, model] of Object.entries(this.props.info.models)) { - if (model.type === 'segmentation') { - models.push(name); - } - } - } - + const models = this.getModels(); return (
- Fully automated segmentation without any user prompt. - Just select a model and click to run -

+
+
+

Experience fully automated segmentation for everything{' '} + from the pre-trained model.

+
} />
diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.css b/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.css new file mode 100644 index 000000000..7c7664179 --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.css @@ -0,0 +1,44 @@ +.optionsTable { + font-family: arial, sans-serif; + border-collapse: collapse; + width: 99%; + font-size: smaller; +} +.optionsTable th { + border: 1px solid #070303; + text-align: left; + background-color: #789; +} +.optionsTable td { + border: 1px solid #070202; + text-align: left; +} +.optionsInput { + width: 100%; +} +.bodyTableContainer { + max-height: 300px; + overflow-y: auto; +} +.segColor { + height: 15px; + width: 15px; + background-color: #bbb; + border-radius: 50%; + display: inline-block; +} +.clickable-row { + cursor: pointer; +} +.clickable-row:hover { + background: #add8e6 !important; +} +.actionInput { + padding: '8px 16px'; + borderRadius: '4px'; + border: '2px'; + cursor: 'pointer'; +} +hr { + margin-top: 6px; +} \ No newline at end of file diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx index 4bc6267ea..8669849fb 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx @@ -1,7 +1,9 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import './BaseTab.css'; import { UIModalService, UINotificationService } from '@ohif/core'; +import { currentSegmentsInfo } from '../../utils/SegUtils'; export default class BaseTab extends Component { static propTypes = { @@ -12,9 +14,12 @@ export default class BaseTab extends Component { client: PropTypes.func, updateView: PropTypes.func, onSelectActionTab: PropTypes.func, - onOptionsConfig: PropTypes.func, }; + notification: any; + uiModelService: any; + tabId: string; + constructor(props) { super(props); this.notification = new UINotificationService(); @@ -25,13 +30,17 @@ export default class BaseTab extends Component { onSelectActionTab = (evt) => { this.props.onSelectActionTab(evt.currentTarget.value); }; - onEnterActionTab = () => {}; onLeaveActionTab = () => {}; - onSegmentCreated = (id) => {}; onSegmentUpdated = (id) => {}; onSegmentDeleted = (id) => {}; onSegmentSelected = (id) => {}; onSelectModel = (model) => {}; + + segmentInfo = () => { + return currentSegmentsInfo( + this.props.servicesManager.services.segmentationService + ).info; + }; } diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx new file mode 100644 index 000000000..fb49429ee --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx @@ -0,0 +1,425 @@ +import React from 'react'; +import ModelSelector from '../ModelSelector'; +import BaseTab from './BaseTab'; +import { hideNotification } from '../../utils/GenericUtils'; + +export default class ClassPrompts extends BaseTab { + modelSelector: any; + + constructor(props) { + super(props); + + this.modelSelector = React.createRef(); + this.state = { + currentModel: null, + selectedOrgans: {}, + }; + } + + onSelectModel = (model) => { + console.log('Selecting (Class/Vista) Interaction Model...'); + console.log(model); + + this.setState({ + currentModel: model, + selectedOrgans: this.getModelOrgans(model), + }); + }; + + getModels() { + const { info } = this.props; + const models = Object.keys(info.data.models).filter( + (m) => + info.data.models[m].type === 'segmentation' || + info.data.models[m].type === 'vista3d' + ); + return models; + } + + getModelOrgans(model) { + const selectedOrgans = {}; + const { info } = this.props; + if (model && info.modelLabelNames[model].length) { + for (const label of info.modelLabelNames[model]) { + if (label !== 'background') { + selectedOrgans[label] = false; + } + } + } + console.log(selectedOrgans); + return selectedOrgans; + } + + onChangeOrgans = (k, evt) => { + const selectedOrgans = this.state.selectedOrgans; + selectedOrgans[k] = !!evt.target.checked; + this.setState({ selectedOrgans: selectedOrgans }); + }; + + onRunInference = async () => { + const { currentModel, currentLabel, clickPoints } = this.state; + const { info, viewConstants } = this.props; + + const models = this.getModels(); + let selectedModel = 0; + for (const model of models) { + if (!currentModel || model === currentModel) { + break; + } + selectedModel++; + } + + const model = models.length > 0 ? models[selectedModel] : null; + if (!model) { + this.notification.show({ + title: 'MONAI Label', + message: 'Something went wrong: Model is not selected', + type: 'error', + duration: 10000, + }); + return; + } + + const nid = this.notification.show({ + title: 'MONAI Label - ' + model, + message: 'Running Class Based Inference...', + type: 'info', + duration: 4000, + }); + + const label_names = []; + const label_classes = []; + for (const label in this.state.selectedOrgans) { + if (!this.state.selectedOrgans[label]) { + continue; + } + const idx = info.modelLabelToIdxMap[model][label]; + if (idx) { + label_names.push(label); + label_classes.push(idx); + } else { + console.log( + "Ignoring this class as it's not defined part of the model", + label + ); + } + } + + const params = { + label_prompt: label_classes, + }; + + const response = await this.props + .client() + .infer(model, viewConstants.SeriesInstanceUID, params); + console.log(response.data); + + hideNotification(nid, this.notification); + if (response.status !== 200) { + this.notification.show({ + title: 'MONAI Label', + message: 'Failed to Run Class Based Inference', + type: 'error', + duration: 6000, + }); + console.log(response.data); + return; + } + + this.notification.show({ + title: 'MONAI Label', + message: 'Run Class Based Inference - Successful', + type: 'success', + duration: 4000, + }); + + this.props.updateView(response, model, label_names, true); + }; + + segColorToRgb(s) { + const c = s ? s.color : [0, 0, 0]; + return `rgb(${c[0]}, ${c[1]}, ${c[2]})`; + } + + updateOrganSelection = (label_classes) => { + const { selectedOrgans } = this.state; + + const models = this.getModels(); + const model_id = this.state.currentModel + ? this.state.currentModel + : models[0]; + + // Reset Previous selection + for (const name in selectedOrgans) { + selectedOrgans[name] = false; + } + + for (const cls_name of label_classes) { + const idx = this.props.info.modelLabelToIdxMap[model_id][cls_name]; + if (idx) { + selectedOrgans[cls_name] = true; + } + } + + this.setState({ selectedOrgans: selectedOrgans }); + }; + + // TODO:: Select By label name instead of hard-coded indices + onOrgansClickBtn = async () => { + const selected_organs_indx = [ + 'liver', + 'bladder', + 'colon', + 'dudenum', + 'esphagus', + 'gallbladder', + 'spleen', + 'pancreas', + 'right kidney', + 'right adrenal gland', + 'left adrenal gland', + 'stomach', + 'left kidney', + 'bladder', + 'prostate or uterus', + 'rectum', + 'small bowel', + ]; + this.updateOrganSelection(selected_organs_indx); + }; + onVascularClickBtn = async () => { + const selected_organs_indx = [ + 'aorta', + 'inferior vena cava', + 'portal vein and splenic vein', + 'hepatic vessel', + 'pulmonary artery', + 'left iliac artery', + 'right iliac artery', + 'left iliac vena', + 'right iliac vena', + ]; + this.updateOrganSelection(selected_organs_indx); + }; + onBonesClickBtn = async () => { + const selected_organs_indx = [ + 'vertebrae l5', + 'vertebrae l4', + 'vertebrae l3', + 'vertebrae l2', + 'vertebrae l1', + 'vertebrae t12', + 'vertebrae t11', + 'vertebrae t10', + 'vertebrae t9', + 'vertebrae t8', + 'vertebrae t7', + 'vertebrae t6', + 'vertebrae t5', + 'vertebrae t4', + 'vertebrae t3', + 'vertebrae t2', + 'vertebrae t1', + 'vertebrae c7', + 'vertebrae c6', + 'vertebrae c5', + 'vertebrae c4', + 'vertebrae c3', + 'vertebrae c2', + 'vertebrae c1', + 'left rib 1', + 'left rib 2', + 'left rib 3', + 'left rib 4', + 'left rib 5', + 'left rib 6', + 'left rib 7', + 'left rib 8', + 'left rib 9', + 'left rib 10', + 'left rib 11', + 'left rib 12', + 'right rib 1', + 'right rib 2', + 'right rib 3', + 'right rib 4', + 'right rib 5', + 'right rib 6', + 'right rib 7', + 'right rib 8', + 'right rib 9', + 'right rib 10', + 'right rib 11', + 'right rib 12', + 'left humerus', + 'right humerus', + 'left scapula', + 'right scapula', + 'left clavicula', + 'right clavicula', + 'left femur', + 'right femur', + 'left hip', + 'right hip', + 'sacrum', + ]; + this.updateOrganSelection(selected_organs_indx); + }; + + onLungsClickBtn = async () => { + const selected_organs_indx = [ + 'left lung upper lobe', + 'left lung lower lobe', + 'right lung upper lobe', + 'right lung middle lobe', + 'right lung lower lobe', + 'trachea', + 'heart', + 'heart myocardium', + 'left heart atrium', + 'left heart ventricle', + 'right heart atrium', + 'right heart ventricle', + ]; + this.updateOrganSelection(selected_organs_indx); + }; + onMusclesClickBtn = async () => { + const selected_organs_indx = [ + 'left gluteus maximus', + 'right gluteus maximus', + 'left gluteus medius', + 'right gluteus medius', + 'left gluteus minimus', + 'right gluteus minimus', + 'left autochthon', + 'right autochthon', + 'left iliopsoas', + 'left iliopsoas', + ]; + this.updateOrganSelection(selected_organs_indx); + }; + + render() { + const models = this.getModels(); + const display = models.length > 0 ? 'block' : 'none'; + const segInfo = this.segmentInfo(); + if ( + Object.keys(this.state.selectedOrgans).length === 0 && + models.length > 0 + ) { + this.state.selectedOrgans = this.getModelOrgans(models[0]); + } + + return ( +
+ + +
+ +
+

Choose following structures or individual classes

+
+ } + /> + + + + + + + + +
+
+
+

Selected Organ(s):

+
+
+ + + {Object.entries(this.state.selectedOrgans).map(([k, v]) => ( + + + + + + ))} + +
+ this.onChangeOrgans(k, e)} + /> + + + {k}
+
+
+
+ + ); + } +} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/NextSampleForm.css b/plugins/ohifv3/extensions/monai-label/src/components/actions/NextSampleForm.css index b381c3607..7fe193316 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/NextSampleForm.css +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/NextSampleForm.css @@ -1,15 +1,3 @@ -/* -Copyright (c) MONAI Consortium -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ .nextSampleForm { width: 750px; } diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/NextSampleForm.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/NextSampleForm.tsx index 80ceaffd8..c355acaf2 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/NextSampleForm.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/NextSampleForm.tsx @@ -1,16 +1,3 @@ -/* -Copyright (c) MONAI Consortium -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - import React, { Component } from 'react'; import './NextSampleForm.css'; @@ -43,7 +30,6 @@ export default class NextSampleForm extends Component { render() { const fields = { - id: 'Image ID (MONAILabel)', Modality: 'Modality', StudyDate: 'Study Date', StudyTime: 'Study Time', diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.css b/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.css deleted file mode 100644 index c02744028..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.css +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright (c) MONAI Consortium -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -.optionsTable { - font-family: arial, sans-serif; - border-collapse: collapse; - width: 100%; - font-size: smaller; -} -.optionsTable th { - border: 1px solid #070303; - text-align: left; - background-color: #789; -} -.optionsTable td { - border: 1px solid #070202; - text-align: left; -} -.optionsInput { - width: 100%; -} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.tsx deleted file mode 100644 index af1f45543..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.tsx +++ /dev/null @@ -1,222 +0,0 @@ -/* -Copyright (c) MONAI Consortium -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; - -import './OptionTable.css'; -import BaseTab from './BaseTab'; - -export default class OptionTable extends BaseTab { - constructor(props) { - super(props); - - this.state = { - section: '', - name: '', - config: null, - }; - } - - onChangeSection = (evt) => { - this.state.section = evt.target.value; - this.setState({ section: evt.target.value }); - }; - - onChangeName = (evt) => { - this.state.name = evt.target.value; - this.setState({ name: evt.target.value }); - }; - - onChangeConfig = (s, n, k, evt) => { - console.debug(s + ' => ' + n + ' => ' + k); - - const c = this.state.config; - if (typeof c[s][n][k] === 'boolean') { - c[s][n][k] = !!evt.target.checked; - } else { - if (typeof c[s][n][k] === 'number') { - c[s][n][k] = Number.isInteger(c[s][n][k]) - ? parseInt(evt.target.value) - : parseFloat(evt.target.value); - } else { - c[s][n][k] = evt.target.value; - } - } - this.setState({ config: c }); - }; - - render() { - const config = this.state.config ? this.state.config : {}; - if (!Object.keys(config).length) { - const info = this.props.info; - const mapping = { - infer: 'models', - train: 'trainers', - activelearning: 'strategies', - scoring: 'scoring', - }; - for (const [m, n] of Object.entries(mapping)) { - for (const [k, v] of Object.entries(info && info[n] ? info[n] : {})) { - if (v && v.config && Object.keys(v.config).length) { - if (!config[m]) { - config[m] = {}; - } - config[m][k] = v.config; - } - } - } - - this.state.config = config; - } - - const section = - this.state.section.length && config[this.state.section] - ? this.state.section - : Object.keys(config).length - ? Object.keys(config)[0] - : ''; - this.state.section = section; - const section_map = config[section] ? config[section] : {}; - - const name = - this.state.name.length && section_map[this.state.name] - ? this.state.name - : Object.keys(section_map).length - ? Object.keys(section_map)[0] - : ''; - this.state.name = name; - const name_map = section_map[name] ? section_map[name] : {}; - - //console.log('Section: ' + section + '; Name: ' + name); - //console.log(name_map); - - return ( -
- - -
- - - - - - - - - - - -
Section: - -
Name: - -
-
- - - - - - - - - {Object.entries(name_map).map(([k, v]) => ( - - - - - ))} - -
KeyValue
{k} - {v !== null && typeof v === 'boolean' ? ( - - this.onChangeConfig( - this.state.section, - this.state.name, - k, - e - ) - } - /> - ) : v !== null && typeof v === 'object' ? ( - - ) : ( - - this.onChangeConfig( - this.state.section, - this.state.name, - k, - e - ) - } - /> - )} -
-
-
- ); - } -} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.css b/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.css new file mode 100644 index 000000000..7ef8790c2 --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.css @@ -0,0 +1,22 @@ +.tmpActionButton { + border: 2px solid #000; + border-radius: 15px; + background-color: #00a4d9; + color: var(--ui-gray-dark); + line-height: 25px; + padding: 0 15px; + outline: none; + cursor: pointer; +} +.tmpActionButton:hover, +.tmpActionButton:active { + background-color: #00a4d9; +} +.tmpActionButton:disabled { + background-color: #00a4d9; +} +.tmpActionButton svg { + margin-right: 4px; + position: relative; + top: 2px; +} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx new file mode 100644 index 000000000..a02a5bfcb --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx @@ -0,0 +1,401 @@ +import React from 'react'; +import './PointPrompts.css'; +import ModelSelector from '../ModelSelector'; +import BaseTab from './BaseTab'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import { hideNotification } from '../../utils/GenericUtils'; +import { cache } from '@cornerstonejs/core'; + +export default class PointPrompts extends BaseTab { + modelSelector: any; + + constructor(props) { + super(props); + + this.modelSelector = React.createRef(); + this.state = { + currentModel: null, + currentLabel: null, + clickPoints: new Map(), + availableOrgans: {}, + }; + } + + onSelectModel = (model) => { + // console.log('Selecting (Point) Interaction Model...'); + const currentLabel = null; + const clickPoints = new Map(); + this.setState({ + currentModel: model, + currentLabel: currentLabel, + clickPoints: clickPoints, + availableOrgans: this.getModelLabels(model), + }); + + this.clearAllPoints(); + }; + + onEnterActionTab = () => { + this.props.commandsManager.runCommand('setToolActive', { + toolName: 'ProbeMONAILabel', + }); + // console.info('Here we activate the probe'); + }; + + onLeaveActionTab = () => { + this.onChangeLabel(null); + this.props.commandsManager.runCommand('setToolDisable', { + toolName: 'ProbeMONAILabel', + }); + // console.info('Here we deactivate the probe'); + }; + + onRunInference = async () => { + const { currentModel, currentLabel, clickPoints } = this.state; + const { info, viewConstants } = this.props; + + const models = this.getModels(); + let selectedModel = 0; + for (const model of models) { + if (!currentModel || model === currentModel) { + break; + } + selectedModel++; + } + + const model = models.length > 0 ? models[selectedModel] : null; + if (!model) { + this.notification.show({ + title: 'MONAI Label', + message: 'Something went wrong: Model is not selected', + type: 'error', + duration: 10000, + }); + return; + } + + const nid = this.notification.show({ + title: 'MONAI Label - ' + model, + message: 'Running Point Based Inference...', + type: 'info', + duration: 4000, + }); + + const { cornerstoneViewportService } = this.props.servicesManager.services; + const viewPort = cornerstoneViewportService.viewportsById.get('mpr-axial'); + const { worldToIndex } = viewPort.viewportData.data[0].volume.imageData; + + // console.log(seriesInstanceUID); + // console.log(viewPort); + const manager = cornerstoneTools.annotation.state.getAnnotationManager(); + clickPoints[currentLabel] = manager.saveAnnotations( + null, + 'ProbeMONAILabel' + ); + + const points = {}; + let label_names = []; + for (const label in clickPoints) { + // console.log(clickPoints[label]); + for (const uid in clickPoints[label]) { + const annotations = clickPoints[label][uid]['ProbeMONAILabel']; + // console.log(annotations); + points[label] = []; + for (const annotation of annotations) { + const pt = annotation.data.handles.points[0]; + points[label].push(worldToIndex(pt).map(Math.round)); + } + } + label_names.push(label); + } + + const params = {}; + if (info.data.models[model].type === 'vista3d') { + params['points'] = points[currentLabel]; + params['point_labels'] = new Array(params['points'].length).fill(1); + if (points['background'] && points['background'].length > 0) { + for (let i = 0; i < points['background'].length; i++) { + params['point_labels'].push(0); + } + params['points'] = params['points'].concat(points['background']); + } + + params['label_prompt'] = [info.modelLabelToIdxMap[model][currentLabel]]; + label_names = [currentLabel]; + } else if (info.data.models[model].type === 'deepedit') { + params['background'] = []; + for (const label in points) { + params[label] = points[label]; + } + } else { + let bg = points['background'] && points['background'].length ? points['background'] : []; + let fg = points[currentLabel]; + if (info.data.models[model].dimension === 2) { + const sidx = fg.length ? fg[fg.length - 1][2] : bg.length ? bg[bg.length - 1][2] : -1; + fg = fg.filter(p => p[2] === sidx); + bg = bg.filter(p => p[2] === sidx); + } + params['foreground'] = fg; + params['background'] = bg; + label_names = [currentLabel]; + } + + const response = await this.props + .client() + .infer(model, viewConstants.SeriesInstanceUID, params); + + hideNotification(nid, this.notification); + if (response.status !== 200) { + this.notification.show({ + title: 'MONAI Label - ' + model, + message: 'Failed to Run Inference for Point Prompts', + type: 'error', + duration: 6000, + }); + return; + } + + this.notification.show({ + title: 'MONAI Label - ' + model, + message: 'Running Inference for Point Prompts - Successful', + type: 'success', + duration: 4000, + }); + + console.log("Target Labels to update: ", label_names) + this.props.updateView(response, model, label_names, true, true); + }; + + initPoints = () => { + const label = this.state.currentLabel; + if (!label) { + console.log('Current Label is Null (No need to init)'); + return; + } + + const { toolGroupService, viewportGridService } = + this.props.servicesManager.services; + const { viewports, activeViewportId } = viewportGridService.getState(); + const viewport = viewports.get(activeViewportId); + const { viewportOptions } = viewport; + const toolGroupId = viewportOptions.toolGroupId; + + const colorMap = this.segmentInfo(); + const customColor = this.segColorToRgb(colorMap[label]); + toolGroupService.setToolConfiguration(toolGroupId, 'ProbeMONAILabel', { + customColor: customColor, + }); + + const annotations = this.state.clickPoints[label]; + if (annotations) { + const manager = cornerstoneTools.annotation.state.getAnnotationManager(); + manager.restoreAnnotations(annotations, null, 'ProbeMONAILabel'); + } + }; + + clearPoints = () => { + cornerstoneTools.annotation.state + .getAnnotationManager() + .removeAllAnnotations(); + this.props.servicesManager.services.cornerstoneViewportService + .getRenderingEngine() + .render(); + }; + + clearAllPoints = () => { + const clickPoints = new Map(); + this.setState({ clickPoints: clickPoints }); + this.clearPoints(); + }; + + segColorToRgb(s) { + const c = s ? s.color : [0, 0, 0]; + return `rgb(${c[0]}, ${c[1]}, ${c[2]})`; + } + + onChangeLabel = (name) => { + console.log(name, this.state.currentLabel); + if (name === this.state.currentLabel) { + console.log('Both new and prev are same'); + return; + } + + const prev = this.state.currentLabel; + const clickPoints = this.state.clickPoints; + if (prev) { + const manager = cornerstoneTools.annotation.state.getAnnotationManager(); + const annotations = manager.saveAnnotations(null, 'ProbeMONAILabel'); + console.log('Saving Prev annotations...', annotations); + + this.state.clickPoints[prev] = annotations; + this.clearPoints(); + } + + this.state.currentLabel = name; + this.setState({ currentLabel: name, clickPoints: clickPoints }); + this.initPoints(); + }; + + getModels() { + const { info } = this.props; + const models = Object.keys(info.data.models).filter( + (m) => + info.data.models[m].type === 'deepgrow' || + info.data.models[m].type === 'deepedit' || + info.data.models[m].type === 'vista3d' + ); + return models; + } + + getModelLabels(model) { + const { info } = this.props; + if (model && info.modelLabelNames[model].length) { + return info.modelLabelNames[model]; + } + return info.labels; + } + + getSelectedModel() { + let selectedModel = 0; + const models = this.getModels(); + for (const model of models) { + if (!this.state.currentModel || model === this.state.currentModel) { + break; + } + selectedModel++; + } + const model = models.length > 0 ? models[selectedModel] : null; + // console.log('Selected Model: ', model); + if (!model) { + console.log('Something went error..'); + return null; + } + return model; + } + + render() { + const models = this.getModels(); + const display = models.length > 0 ? 'block' : 'none'; + const segInfo = this.segmentInfo(); + const labels = this.getModelLabels(this.getSelectedModel()); + + return ( +
+ + +
+ + {/*

*/} + {/*  */} + {/* Auto Run on every*/} + {/* click*/} + {/*

*/} +
+

Select an anatomy from the segments menu below.

+

To guide the inference, add foreground clicks:

+ + this.clearPoints()} + > + Clear Points + + {' '} + |{' '} + + this.clearAllPoints()} + > + Clear All Points + + +
+ } + /> +
+
+

Available Organ(s):

+
+
+ + + this.onChangeLabel('background')} + > + + + + {labels + .filter((l) => l !== 'background') + .map((label) => ( + this.onChangeLabel(label)} + > + + + + ))} + +
+ {/* Content for the "background" entry */} + + background
+ + {label}
+
+
+
+ + ); + } +} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/SmartEdit.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/SmartEdit.tsx deleted file mode 100644 index a2774c48a..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/SmartEdit.tsx +++ /dev/null @@ -1,327 +0,0 @@ -/* -Copyright (c) MONAI Consortium -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import ModelSelector from '../ModelSelector'; -import BaseTab from './BaseTab'; -import * as cornerstoneTools from '@cornerstonejs/tools'; -import { vec3 } from 'gl-matrix'; -/* import { getFirstSegmentId } from '../../utils/SegmentationUtils'; */ - -export default class SmartEdit extends BaseTab { - constructor(props) { - super(props); - - this.modelSelector = React.createRef(); - - this.state = { - segmentId: null, - currentPoint: null, - deepgrowPoints: new Map(), - currentEvent: null, - currentModel: null, - }; - } - - componentDidMount() { - const { segmentationService, toolGroupService, viewportGridService } = - this.props.servicesManager.services; - - const added = segmentationService.EVENTS.SEGMENTATION_ADDED; - const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED; - const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED; - const subscriptions = []; - - [added, updated, removed].forEach((evt) => { - const { unsubscribe } = segmentationService.subscribe(evt, () => { - const segmentations = segmentationService.getSegmentations(); - - if (!segmentations?.length) { - return; - } - - // get the first segmentation Todo: fix this to be active - const segmentation = segmentations[0]; - const { segments, activeSegmentIndex } = segmentation; - - const selectedSegment = segments[activeSegmentIndex]; - - const color = selectedSegment.color; - - // get the active viewport toolGroup - const { viewports, activeViewportId } = viewportGridService.getState(); - const viewport = viewports.get(activeViewportId); - const { viewportOptions } = viewport; - const toolGroupId = viewportOptions.toolGroupId; - - toolGroupService.setToolConfiguration(toolGroupId, 'ProbeMONAILabel', { - customColor: `rgb(${color[0]}, ${color[1]}, ${color[2]})`, - }); - }); - subscriptions.push(unsubscribe); - }); - - this.unsubscribe = () => { - subscriptions.forEach((unsubscribe) => unsubscribe()); - }; - } - - componentWillUnmount() { - this.unsubscribe(); - } - - onSelectModel = (model) => { - this.setState({ currentModel: model }); - }; - - onDeepgrow = async () => { - const { - segmentationService, - cornerstoneViewportService, - viewportGridService, - } = this.props.servicesManager.services; - const { info, viewConstants } = this.props; - const image = viewConstants.SeriesInstanceUID; - const model = this.modelSelector.current.currentModel(); - - const activeSegment = segmentationService.getActiveSegment(); - const segmentId = activeSegment.label; - - if (segmentId && !this.state.segmentId) { - this.onSegmentSelected(segmentId); - } - - const is3D = info.models[model].dimension === 3; - if (!segmentId) { - this.notification.show({ - title: 'MONAI Label', - message: 'Please create/select a label first', - type: 'warning', - }); - return; - } - - /* const points = this.state.deepgrowPoints.get(segmentId); */ - - // Getting the clicks in IJK format - - const { activeViewportId } = viewportGridService.getState(); - const viewPort = - cornerstoneViewportService.getCornerstoneViewport(activeViewportId); - - const pts = cornerstoneTools.annotation.state.getAnnotations( - 'ProbeMONAILabel', - viewPort.element - ); - - const pointsWorld = pts.map((pt) => pt.data.handles.points[0]); - const { imageData } = viewPort.getImageData(); - const ijk = vec3.fromValues(0, 0, 0); - - // Rounding is not working - /* const pointsIJK = pointsWorld.map((world) => - Math.round(imageData.worldToIndex(world, ijk)) - ); */ - - const pointsIJK = pointsWorld.map((world) => - imageData.worldToIndex(world, ijk) - ); - - /* const roundPointsIJK = pointsIJK.map(ind => Math.round(ind)) */ - - this.state.deepgrowPoints.set(segmentId, pointsIJK); - - // when changing label, delete previous? or just keep track of all provided clicks per labels - const points = this.state.deepgrowPoints.get(segmentId); - - // Error as ctrlKey is part of the points? - - /* if (!points.length) { - return; - } - - const currentPoint = points[points.length - 1]; */ - - const config = this.props.onOptionsConfig(); - - const labels = info.models[model].labels; - - const params = - config && config.infer && config.infer[model] ? config.infer[model] : {}; - - // block the cursor while waiting for MONAI Label response? - - for (const l in labels) { - if (l === segmentId) { - console.log('This is the segmentId'); - const p = []; - for (let i = 0; i < pointsIJK.length; i++) { - p.push(Array.from(pointsIJK[i])); - console.log(p[i]); - } - params[l] = p; - continue; - } - console.log(l); - params[l] = []; - } - - const response = await this.props.client().infer(model, image, params); - - if (response.status !== 200) { - this.notification.show({ - title: 'MONAI Label', - message: 'Failed to Run Deepgrow', - type: 'error', - duration: 3000, - }); - } else { - await this.props.updateView( - response, - labels, - 'override', - is3D ? -1 : currentPoint.z - ); - } - - // Remove the segmentation and create a new one with a differen index - /* debugger; - this.props.servicesManager.services.segmentationService.remove('1') */ - }; - - getPointData = (evt) => { - const { x, y, imageId } = evt.detail; - const z = this.props.viewConstants.imageIdsToIndex.get(imageId); - - console.debug('X: ' + x + '; Y: ' + y + '; Z: ' + z); - return { x, y, z, data: evt.detail, imageId }; - }; - - onSegmentDeleted = (id) => { - this.clearPoints(id); - this.setState({ segmentId: null }); - }; - onSegmentSelected = (id) => { - this.initPoints(id); - this.setState({ segmentId: id }); - }; - - initPoints = (id) => { - console.log('Initializing points'); - }; - - clearPoints = (id) => { - cornerstoneTools.annotation.state - .getAnnotationManager() - .removeAllAnnotations(); - this.props.servicesManager.services.cornerstoneViewportService - .getRenderingEngine() - .render(); - console.log('Clearing all points'); - }; - - onSelectActionTab = (evt) => { - this.props.onSelectActionTab(evt.currentTarget.value); - }; - - onEnterActionTab = () => { - this.props.commandsManager.runCommand('setToolActive', { - toolName: 'ProbeMONAILabel', - }); - console.info('Here we activate the probe'); - }; - - onLeaveActionTab = () => { - this.props.commandsManager.runCommand('setToolDisable', { - toolName: 'ProbeMONAILabel', - }); - console.info('Here we deactivate the probe'); - /* cornerstoneTools.setToolDisabled('DeepgrowProbe', {}); - this.removeEventListeners(); */ - }; - - addEventListeners = (eventName, handler) => { - this.removeEventListeners(); - - const { element } = this.props.viewConstants; - element.addEventListener(eventName, handler); - this.setState({ currentEvent: { name: eventName, handler: handler } }); - }; - - removeEventListeners = () => { - if (!this.state.currentEvent) { - return; - } - - const { element } = this.props.viewConstants; - const { currentEvent } = this.state; - - element.removeEventListener(currentEvent.name, currentEvent.handler); - this.setState({ currentEvent: null }); - }; - - render() { - const models = []; - if (this.props.info && this.props.info.models) { - for (const [name, model] of Object.entries(this.props.info.models)) { - if ( - model.type === 'deepgrow' || - model.type === 'deepedit' || - model.type === 'vista' - ) { - models.push(name); - } - } - } - - return ( -
- - -
- -

- Create a label and annotate any organ. -

- this.clearPoints()} - > - Clear Points - -
- } - /> -
- - ); - } -} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/colorPickerDialog.css b/plugins/ohifv3/extensions/monai-label/src/components/colorPickerDialog.css deleted file mode 100644 index 1c6bb2067..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/colorPickerDialog.css +++ /dev/null @@ -1,3 +0,0 @@ -.chrome-picker { - background: #090c29 !important; -} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/colorPickerDialog.tsx b/plugins/ohifv3/extensions/monai-label/src/components/colorPickerDialog.tsx deleted file mode 100644 index 1337ee103..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/colorPickerDialog.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { Dialog } from '@ohif/ui'; -import { ChromePicker } from 'react-color'; - -import './colorPickerDialog.css'; - -function callColorPickerDialog(uiDialogService, rgbaColor, callback) { - const dialogId = 'pick-color'; - - const onSubmitHandler = ({ action, value }) => { - switch (action.id) { - case 'save': - callback(value.rgbaColor, action.id); - break; - case 'cancel': - callback('', action.id); - break; - } - uiDialogService.dismiss({ id: dialogId }); - }; - - if (uiDialogService) { - uiDialogService.create({ - id: dialogId, - centralize: true, - isDraggable: false, - showOverlay: true, - content: Dialog, - contentProps: { - title: 'Segment Color', - value: { rgbaColor }, - noCloseButton: true, - onClose: () => uiDialogService.dismiss({ id: dialogId }), - actions: [ - { id: 'cancel', text: 'Cancel', type: 'primary' }, - { id: 'save', text: 'Save', type: 'secondary' }, - ], - onSubmit: onSubmitHandler, - body: ({ value, setValue }) => { - const handleChange = (color) => { - setValue({ rgbaColor: color.rgb }); - }; - - return ( - - ); - }, - }, - }); - } -} - -export default callColorPickerDialog; diff --git a/plugins/ohifv3/extensions/monai-label/src/config.json b/plugins/ohifv3/extensions/monai-label/src/config.json new file mode 100644 index 000000000..f225827f7 --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/config.json @@ -0,0 +1,6 @@ +{ + "MONAI_LABEL_API_ENDPOINT": "http://localhost:8000", + "MONAI_LABEL_USER_ID": "9967f4fc-5a63-11ee-8c99-0242ac120002", + "MONAI_LABEL_ACCESS_TOKEN": "" +} + diff --git a/plugins/ohifv3/extensions/monai-label/src/getCommandsModule.ts b/plugins/ohifv3/extensions/monai-label/src/getCommandsModule.ts index f61ec9bc0..3b96dd695 100644 --- a/plugins/ohifv3/extensions/monai-label/src/getCommandsModule.ts +++ b/plugins/ohifv3/extensions/monai-label/src/getCommandsModule.ts @@ -1,22 +1,5 @@ -import { ServicesManager, CommandsManager, ExtensionManager } from '@ohif/core'; -import { Enums } from '@cornerstonejs/tools'; - -export default function getCommandsModule({ - servicesManager, - commandsManager, - extensionManager, -}: { - servicesManager: ServicesManager; - commandsManager: CommandsManager; - extensionManager: ExtensionManager; -}) { - const { - viewportGridService, - toolGroupService, - cineService, - toolbarService, - uiNotificationService, - } = servicesManager.services; +export default function getCommandsModule({ servicesManager }) { + const { uiNotificationService } = servicesManager.services; const actions = { setToolActive: ({ toolName }) => { @@ -30,9 +13,9 @@ export default function getCommandsModule({ }; const definitions = { - /* setToolActive: { + setToolActive: { commandFn: actions.setToolActive, - }, */ + }, }; return { diff --git a/plugins/ohifv3/extensions/monai-label/src/index.tsx b/plugins/ohifv3/extensions/monai-label/src/index.tsx index 6ff0b7294..37578c659 100644 --- a/plugins/ohifv3/extensions/monai-label/src/index.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/index.tsx @@ -5,52 +5,7 @@ import preRegistration from './init'; export default { id, - preRegistration, - getPanelModule, - - getViewportModule: ({ - servicesManager, - commandsManager, - extensionManager, - }) => {}, - - getToolbarModule: ({ - servicesManager, - commandsManager, - extensionManager, - }) => {}, - - getLayoutTemplateModule: ({ - servicesManager, - commandsManager, - extensionManager, - }) => {}, - - getSopClassHandlerModule: ({ - servicesManager, - commandsManager, - extensionManager, - }) => {}, - - getHangingProtocolModule: ({ - servicesManager, - commandsManager, - extensionManager, - }) => {}, - getCommandsModule, - - getContextModule: ({ - servicesManager, - commandsManager, - extensionManager, - }) => {}, - - getDataSourcesModule: ({ - servicesManager, - commandsManager, - extensionManager, - }) => {}, }; diff --git a/plugins/ohifv3/extensions/monai-label/src/services/MonaiLabelClient.js b/plugins/ohifv3/extensions/monai-label/src/services/MonaiLabelClient.js index 45ebe6cfa..502b9d09a 100644 --- a/plugins/ohifv3/extensions/monai-label/src/services/MonaiLabelClient.js +++ b/plugins/ohifv3/extensions/monai-label/src/services/MonaiLabelClient.js @@ -10,28 +10,18 @@ export default class MonaiLabelClient { return await MonaiLabelClient.api_get(url.toString()); } - async segmentation(model, image, params = {}, label = null) { - // label is used to send label volumes, e.g. scribbles, - // that are to be used during segmentation - return this.infer(model, image, params, label); - } - - async deepgrow(model, image, foreground, background, params = {}) { - params['foreground'] = foreground; - params['background'] = background; - return this.infer(model, image, params); - } - async infer(model, image, params, label = null, result_extension = '.nrrd') { + console.log('Running Infer for: ', model, image, params); + let url = new URL('infer/' + encodeURIComponent(model), this.server_url); url.searchParams.append('image', image); - url.searchParams.append('output', 'all'); - // url.searchParams.append('output', 'image'); + // url.searchParams.append('output', 'all'); + url.searchParams.append('output', 'image'); url = url.toString(); if (result_extension) { params.result_extension = result_extension; - params.result_dtype = 'uint16'; + params.result_dtype = 'uint8'; params.result_compress = false; } diff --git a/plugins/ohifv3/extensions/monai-label/src/tools/ProbeMONAILabelTool.ts b/plugins/ohifv3/extensions/monai-label/src/tools/ProbeMONAILabelTool.ts index 8997271da..6ad8a7ee9 100644 --- a/plugins/ohifv3/extensions/monai-label/src/tools/ProbeMONAILabelTool.ts +++ b/plugins/ohifv3/extensions/monai-label/src/tools/ProbeMONAILabelTool.ts @@ -1,4 +1,3 @@ -import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core'; import { ProbeTool, annotation, drawing } from '@cornerstonejs/tools'; const { getAnnotations } = annotation.state; diff --git a/plugins/ohifv3/extensions/monai-label/src/utils/GenericAnatomyColors.js b/plugins/ohifv3/extensions/monai-label/src/utils/GenericAnatomyColors.js index 666b3e552..dcb70ce3e 100644 --- a/plugins/ohifv3/extensions/monai-label/src/utils/GenericAnatomyColors.js +++ b/plugins/ohifv3/extensions/monai-label/src/utils/GenericAnatomyColors.js @@ -407,6 +407,8 @@ export const GenericAnatomyColors = [ { label: 'thorax', value: rgbToHex(177, 122, 101) }, { label: 'trachea', value: rgbToHex(182, 228, 255) }, { label: 'bronchi', value: rgbToHex(175, 216, 244) }, + { label: 'lung', value: rgbToHex(197, 165, 145) }, + { label: 'lung tumor', value: rgbToHex(144, 238, 144) }, { label: 'right lung', value: rgbToHex(197, 165, 145) }, { label: 'left lung', value: rgbToHex(197, 165, 145) }, { label: 'superior lobe of right lung', value: rgbToHex(172, 138, 115) }, @@ -452,11 +454,15 @@ export const GenericAnatomyColors = [ { label: 'colon', value: rgbToHex(204, 168, 143) }, { label: 'anus', value: rgbToHex(255, 224, 199) }, { label: 'liver', value: rgbToHex(221, 130, 101) }, + { label: 'liver tumor', value: rgbToHex(144, 238, 144) }, { label: 'biliary tree', value: rgbToHex(0, 145, 30) }, { label: 'gallbladder', value: rgbToHex(139, 150, 98) }, { label: 'pancreas', value: rgbToHex(249, 180, 111) }, + { label: 'pancreatic tumor', value: rgbToHex(144, 238, 144) }, { label: 'spleen', value: rgbToHex(157, 108, 162) }, { label: 'urinary system', value: rgbToHex(203, 136, 116) }, + { label: 'kidney', value: rgbToHex(185, 102, 83) }, + { label: 'kidney tumor', value: rgbToHex(144, 238, 144) }, { label: 'right kidney', value: rgbToHex(185, 102, 83) }, { label: 'left kidney', value: rgbToHex(185, 102, 83) }, { label: 'right ureter', value: rgbToHex(247, 182, 164) }, diff --git a/plugins/ohifv3/extensions/monai-label/src/utils/GenericUtils.js b/plugins/ohifv3/extensions/monai-label/src/utils/GenericUtils.js index b28881df7..a74e38604 100644 --- a/plugins/ohifv3/extensions/monai-label/src/utils/GenericUtils.js +++ b/plugins/ohifv3/extensions/monai-label/src/utils/GenericUtils.js @@ -6,11 +6,14 @@ function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive } -function randomRGB() { +function randomRGB(toHex = false) { const o = Math.round, r = Math.random, s = 255; - return rgbToHex(o(r() * s), o(r() * s), o(r() * s)); + const x = o(r() * s); + const y = o(r() * s); + const z = o(r() * s); + return toHex ? rgbToHex(x, y, z) : { r: x, g: y, b: z }; } function randomName() { @@ -37,16 +40,42 @@ function hexToRgb(hex) { : null; } -function getLabelColor(label, rgb = true) { +function fixedRGBForLabel(str, toHex = false) { + const r = generateIntForString(str); + const x = 2*r % 256; + const y = 3*r % 256; + const z = 5*r % 256; + return toHex ? rgbToHex(x, y, z) : { r: x, g: y, b: z }; +} + +function generateIntForString(str) { + let hash = str.length * 4; + for (let i = 0; i < str.length; ++i) + hash += str.charCodeAt(i); + return hash; +} + +function getLabelColor(label, rgb = true, random = true) { const name = label.toLowerCase(); for (const i of GenericAnatomyColors) { if (i.label === name) { return rgb ? hexToRgb(i.value) : i.value; } } + if (random) { + return fixedRGBForLabel(label, !rgb); + } return null; } +function hideNotification(nid, notification) { + if (!nid) { + window.snackbar.hideAll(); + } else { + notification.hide(nid); + } +} + export class CookieUtils { static setCookie(name, value, exp_y, exp_m, exp_d, path, domain, secure) { let cookie_string = name + '=' + escape(value); @@ -105,4 +134,5 @@ export { rgbToHex, hexToRgb, getLabelColor, + hideNotification, }; diff --git a/plugins/ohifv3/extensions/monai-label/src/utils/SegUtils.js b/plugins/ohifv3/extensions/monai-label/src/utils/SegUtils.js new file mode 100644 index 000000000..f3147ee39 --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/utils/SegUtils.js @@ -0,0 +1,21 @@ +function currentSegmentsInfo(segmentationService) { + const info = {}; + const indices = new Set(); + + const segmentations = segmentationService.getSegmentations(); + if (segmentations && Object.keys(segmentations).length > 0) { + const segmentation = segmentations['0']; + const { segments } = segmentation.config; + for (const segmentIndex of Object.keys(segments)) { + const segment = segments[segmentIndex]; + info[segment.label] = { + segmentIndex: segment.segmentIndex, + color: segment.color, + }; + indices.add(segment.segmentIndex); + } + } + return { info, indices }; +} + +export { currentSegmentsInfo }; diff --git a/plugins/ohifv3/extensions/monai-label/src/utils/SegmentationReader.js b/plugins/ohifv3/extensions/monai-label/src/utils/SegmentationReader.js index ca117754a..2ea06f9cf 100644 --- a/plugins/ohifv3/extensions/monai-label/src/utils/SegmentationReader.js +++ b/plugins/ohifv3/extensions/monai-label/src/utils/SegmentationReader.js @@ -1,25 +1,5 @@ -/* -Copyright (c) MONAI Consortium -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - import nrrd from 'nrrd-js'; import pako from 'pako'; -import readImageArrayBuffer from 'itk/readImageArrayBuffer'; -import writeArrayBuffer from 'itk/writeArrayBuffer'; -import config from 'itk/itkConfig'; - -const pkgJSON = require('../../package.json'); -const itkVersion = pkgJSON.dependencies.itk.substring(1); -config.itkModulesPath = 'https://unpkg.com/itk@' + itkVersion; // HACK to use ITK from CDN export default class SegmentationReader { static parseNrrdData(data) { @@ -44,55 +24,4 @@ export default class SegmentationReader { image, }; } - - static saveFile(blob, filename) { - if (window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveOrOpenBlob(blob, filename); - } else { - const a = document.createElement('a'); - document.body.appendChild(a); - - const url = window.URL.createObjectURL(blob); - a.href = url; - a.download = filename; - a.click(); - - setTimeout(() => { - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - }, 0); - } - } - - // GZIP write not supported by nrrd-js (so use ITK save with compressed = true) - static serializeNrrdCompressed(header, image, filename) { - const nrrdBuffer = SegmentationReader.serializeNrrd(header, image); - - const reader = readImageArrayBuffer(null, nrrdBuffer, 'temp.nrrd'); - reader.then(function (response) { - const writer = writeArrayBuffer( - response.webWorker, - true, - response.image, - filename - ); - writer.then(function (response) { - SegmentationReader.saveFile(new Blob([response.arrayBuffer]), filename); - console.debug('File downloaded: ' + filename); - }); - }); - } - - static serializeNrrd(header, image, filename) { - let nrrdOrg = Object.assign({}, header); - nrrdOrg.buffer = image; - nrrdOrg.data = new Uint16Array(image); - - const nrrdBuffer = nrrd.serialize(nrrdOrg); - if (filename) { - SegmentationReader.saveFile(new Blob([nrrdBuffer]), filename); - console.debug('File downloaded: ' + filename); - } - return nrrdBuffer; - } } diff --git a/plugins/ohifv3/extensions/monai-label/src/utils/SegmentationUtils.tsx b/plugins/ohifv3/extensions/monai-label/src/utils/SegmentationUtils.tsx deleted file mode 100644 index 5a762d957..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/utils/SegmentationUtils.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { getLabelColor } from './GenericUtils'; - -function createSegmentMetadata( - label, - segmentId, - description = '', - newLabelMap = false -) { - const labelMeta = { - SegmentedPropertyCategoryCodeSequence: { - CodeValue: 'T-D0050', - CodingSchemeDesignator: 'SRT', - CodeMeaning: 'Tissue', - }, - SegmentNumber: 1, - SegmentLabel: label ? label : 'label-0-1', - SegmentDescription: description, - SegmentAlgorithmType: 'SEMIAUTOMATIC', - SegmentAlgorithmName: 'MONAI', - SegmentedPropertyTypeCodeSequence: { - CodeValue: 'T-D0050', - CodingSchemeDesignator: 'SRT', - CodeMeaning: 'Tissue', - }, - }; - - if (newLabelMap) { - console.debug('Logic to create a new segment'); - } - - const color = getLabelColor(label); - - const rgbColor = []; - for (const key in color) { - rgbColor.push(color[key]); - } - - rgbColor.push(255); - - return { - id: '0+' + segmentId, - color: rgbColor, - labelmapIndex: 0, - name: label, - segmentIndex: segmentId, - description: description, - meta: labelMeta, - }; -} - -export { createSegmentMetadata }; diff --git a/plugins/ohifv3/extensions/monai-label/src/utils/addToolInstance.ts b/plugins/ohifv3/extensions/monai-label/src/utils/addToolInstance.ts index 889926d81..2181abbef 100644 --- a/plugins/ohifv3/extensions/monai-label/src/utils/addToolInstance.ts +++ b/plugins/ohifv3/extensions/monai-label/src/utils/addToolInstance.ts @@ -8,5 +8,6 @@ export default function addToolInstance( class InstanceClass extends toolClass { static toolName = name; } + addTool(InstanceClass); } diff --git a/plugins/ohifv3/modes/monai-label/.webpack/webpack.prod.js b/plugins/ohifv3/modes/monai-label/.webpack/webpack.prod.js index 163392a69..4403d12a1 100644 --- a/plugins/ohifv3/modes/monai-label/.webpack/webpack.prod.js +++ b/plugins/ohifv3/modes/monai-label/.webpack/webpack.prod.js @@ -1,62 +1,54 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); const path = require('path'); -const pkg = require('../package.json'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const outputFile = 'index.umd.js'; -const rootDir = path.resolve(__dirname, '../'); -const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); +const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); -// Todo: add ESM build for the mode in addition to umd build -const config = { - mode: 'production', - entry: rootDir + '/' + pkg.module, - devtool: 'source-map', - output: { - path: outputFolder, - filename: outputFile, - library: pkg.name, - libraryTarget: 'umd', - chunkFilename: '[name].chunk.js', - umdNamedDefine: true, - globalObject: "typeof self !== 'undefined' ? self : this", - }, - externals: [ - { - react: { - root: 'React', - commonjs2: 'react', - commonjs: 'react', - amd: 'react', - }, - '@ohif/core': { - commonjs2: '@ohif/core', - commonjs: '@ohif/core', - amd: '@ohif/core', - root: '@ohif/core', - }, - '@ohif/ui': { - commonjs2: '@ohif/ui', - commonjs: '@ohif/ui', - amd: '@ohif/ui', - root: '@ohif/ui', - }, +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, }, - ], - module: { - rules: [ - { - test: /(\.jsx|\.js|\.tsx|\.ts)$/, - loader: 'babel-loader', - exclude: /(node_modules|bower_components)/, - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, - }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-mode-monai-label', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new MiniCssExtractPlugin({ + // filename: './dist/[name].css', + // chunkFilename: './dist/[id].css', + // }), ], - }, - resolve: { - modules: [path.resolve('./node_modules'), path.resolve('./src')], - extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], - }, + }); }; - -module.exports = config; diff --git a/plugins/ohifv3/modes/monai-label/babel.config.js b/plugins/ohifv3/modes/monai-label/babel.config.js index a38ddda21..92fbbdeaf 100644 --- a/plugins/ohifv3/modes/monai-label/babel.config.js +++ b/plugins/ohifv3/modes/monai-label/babel.config.js @@ -10,7 +10,7 @@ module.exports = { modules: 'commonjs', debug: false, }, - '@babel/preset-typescript', + "@babel/preset-typescript", ], '@babel/preset-react', ], @@ -26,7 +26,7 @@ module.exports = { // WebPack handles ES6 --> Target Syntax ['@babel/preset-env', { modules: false }], '@babel/preset-react', - '@babel/preset-typescript', + "@babel/preset-typescript", ], ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], }, @@ -35,7 +35,7 @@ module.exports = { // WebPack handles ES6 --> Target Syntax ['@babel/preset-env', { modules: false }], '@babel/preset-react', - '@babel/preset-typescript', + "@babel/preset-typescript", ], plugins: ['react-hot-loader/babel'], ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], diff --git a/plugins/ohifv3/modes/monai-label/package.json b/plugins/ohifv3/modes/monai-label/package.json index ecbae9558..b9d8028d7 100644 --- a/plugins/ohifv3/modes/monai-label/package.json +++ b/plugins/ohifv3/modes/monai-label/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/mode-monai-label", - "version": "0.0.1", + "version": "3.0.0", "description": "OHIFv3 mode for MONAI Label", "author": "OHIF,NVIDIA,KCL", "license": "MIT", @@ -14,6 +14,9 @@ "keywords": [ "ohif-mode" ], + "publishConfig": { + "access": "public" + }, "module": "src/index.tsx", "engines": { "node": ">=14", @@ -21,6 +24,8 @@ "yarn": ">=1.16.0" }, "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", "dev:cornerstone": "yarn run dev", "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", @@ -30,35 +35,36 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "^3.0.0" + "@ohif/core": "3.10.0-beta.5" }, "dependencies": { - "@babel/runtime": "^7.20.13" + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3" }, "devDependencies": { - "@babel/core": "^7.21.4", + "@babel/core": "7.24.7", "@babel/plugin-proposal-class-properties": "^7.16.7", "@babel/plugin-proposal-object-rest-spread": "^7.17.3", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.16.7", "@babel/plugin-transform-regenerator": "^7.16.7", - "@babel/plugin-transform-runtime": "^7.17.0", + "@babel/plugin-transform-runtime": "7.24.7", "@babel/plugin-transform-typescript": "^7.13.0", - "@babel/preset-env": "^7.16.11", + "@babel/preset-env": "7.23.2", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.13.0", - "babel-eslint": "^8.0.3", + "@svgr/webpack": "^8.1.0", + "babel-eslint": "^10.1.0", "babel-loader": "^8.0.0-beta.4", - "babel-plugin-inline-react-svg": "^2.0.1", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^10.2.0", "cross-env": "^7.0.3", "dotenv": "^14.1.0", - "eslint": "^5.0.1", + "eslint": "^8.39.0", "eslint-loader": "^2.0.0", - "webpack": "^5.50.0", - "webpack-merge": "^5.7.3", - "webpack-cli": "^4.7.2" + "webpack": "5.94.0", + "webpack-cli": "^4.7.2", + "webpack-merge": "^5.7.3" } } diff --git a/plugins/ohifv3/modes/monai-label/src/index.tsx b/plugins/ohifv3/modes/monai-label/src/index.tsx index 00e4be7da..ba87904a9 100644 --- a/plugins/ohifv3/modes/monai-label/src/index.tsx +++ b/plugins/ohifv3/modes/monai-label/src/index.tsx @@ -3,28 +3,34 @@ import toolbarButtons from './toolbarButtons.js'; import { id } from './id.js'; import initToolGroups from './initToolGroups.js'; + +const monailabel = { + monaiLabel: '@ohif/extension-monai-label.panelModule.monailabel', +} + + const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default', leftPanel: '@ohif/extension-default.panelModule.seriesList', -}; - -const monailabel = { - monaiLabel: '@ohif/extension-monai-label.panelModule.monailabel', + rightPanel: '@ohif/extension-default.panelModule.measure', }; const cornerstone = { viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', + panelTool: + '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools', }; -const dicomSeg = { +const segmentation = { sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', - panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', }; + + /** * Just two dependencies to be able to render a viewport with panels in order * to make sure that the mode is working. @@ -34,7 +40,7 @@ const extensionDependencies = { '@ohif/extension-cornerstone': '^3.0.0', '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', '@ohif/extension-test': '^0.0.1', - '@ohif/extension-monai-label': '^0.0.1', + '@ohif/extension-monai-label': '^3.0.0', }; function modeFactory({ modeConfiguration }) { @@ -67,41 +73,12 @@ function modeFactory({ modeConfiguration }) { // Init Default and SR ToolGroups initToolGroups(extensionManager, toolGroupService, commandsManager); + toolbarService.addButtons(toolbarButtons); // init customizations customizationService.addModeCustomizations([ '@ohif/extension-test.customizationModule.custom-context-menu', ]); - let unsubscribe; - - const activateTool = () => { - toolbarService.recordInteraction({ - groupId: 'WindowLevel', - itemId: 'WindowLevel', - interactionType: 'tool', - commands: [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'WindowLevel', - }, - context: 'CORNERSTONE', - }, - ], - }); - - // We don't need to reset the active tool whenever a viewport is getting - // added to the toolGroup. - unsubscribe(); - }; - - // Since we only have one viewport for the basic cs3d mode and it has - // only one hanging protocol, we can just use the first viewport - ({ unsubscribe } = toolGroupService.subscribe( - toolGroupService.EVENTS.VIEWPORT_ADDED, - activateTool - )); - toolbarService.addButtons(toolbarButtons); toolbarService.createButtonSection('primary', [ 'MeasurementTools', @@ -114,6 +91,13 @@ function modeFactory({ modeConfiguration }) { 'Crosshairs', 'MoreTools', ]); + + toolbarService.createButtonSection('segmentationToolbox', [ + 'BrushTools', + 'Shapes', + ]); + + }, onModeExit: ({ servicesManager }) => { const { @@ -121,8 +105,12 @@ function modeFactory({ modeConfiguration }) { syncGroupService, segmentationService, cornerstoneViewportService, + uiDialogService, + uiModalService, } = servicesManager.services; + uiDialogService.dismissAll(); + uiModalService.hide(); toolGroupService.destroy(); syncGroupService.destroy(); segmentationService.destroy(); @@ -173,8 +161,8 @@ function modeFactory({ modeConfiguration }) { displaySetsToDisplay: [ohif.sopClassHandler], }, { - namespace: dicomSeg.viewport, - displaySetsToDisplay: [dicomSeg.sopClassHandler], + namespace: segmentation.viewport, + displaySetsToDisplay: [segmentation.sopClassHandler], }, ], }, @@ -189,7 +177,7 @@ function modeFactory({ modeConfiguration }) { // hangingProtocol: [''], /** SopClassHandlers used by the mode */ sopClassHandlers: [ - dicomSeg.sopClassHandler, + segmentation.sopClassHandler, ohif.sopClassHandler, ] /** hotkeys for mode */, hotkeys: [...hotkeys.defaults.hotkeyBindings], @@ -202,4 +190,4 @@ const mode = { extensionDependencies, }; -export default mode; +export default mode; \ No newline at end of file diff --git a/plugins/ohifv3/modes/monai-label/src/initToolGroups.js b/plugins/ohifv3/modes/monai-label/src/initToolGroups.js index 81bc09eb6..88715c8e8 100644 --- a/plugins/ohifv3/modes/monai-label/src/initToolGroups.js +++ b/plugins/ohifv3/modes/monai-label/src/initToolGroups.js @@ -20,10 +20,10 @@ function initDefaultToolGroup( extensionManager, toolGroupService, commandsManager, - toolGroupId + toolGroupId, ) { const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.tools' + '@ohif/extension-cornerstone.utilityModule.tools', ); const { toolNames, Enums } = utilityModule.exports; @@ -105,7 +105,7 @@ function initDefaultToolGroup( function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.tools' + '@ohif/extension-cornerstone.utilityModule.tools', ); const { toolNames, Enums } = utilityModule.exports; @@ -201,7 +201,7 @@ function initToolGroups(extensionManager, toolGroupService, commandsManager) { extensionManager, toolGroupService, commandsManager, - 'default' + 'default', ); initMPRToolGroup(extensionManager, toolGroupService, commandsManager); } diff --git a/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js b/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js index 5b71b36c2..4cd9e9ff0 100644 --- a/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js +++ b/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js @@ -8,6 +8,7 @@ import { import { defaults } from '@ohif/core'; const { windowLevelPresets } = defaults; + /** * * @param {*} type - 'tool' | 'action' | 'toggle' @@ -64,7 +65,7 @@ const toolGroupIds = ['default', 'mpr', 'SRToolGroup']; * @returns {Array} an array of 'setToolActive' commands */ function _createSetToolActiveCommands(toolName) { - const temp = toolGroupIds.map((toolGroupId) => ({ + const temp = toolGroupIds.map(toolGroupId => ({ commandName: 'setToolActive', commandOptions: { toolGroupId, @@ -106,7 +107,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Length' + 'Length', ), secondary: { icon: 'chevron-down', @@ -137,7 +138,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Length Tool' + 'Length Tool', ), _createToolButton( 'Bidirectional', @@ -160,7 +161,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Bidirectional Tool' + 'Bidirectional Tool', ), _createToolButton( 'ArrowAnnotate', @@ -183,7 +184,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Arrow Annotate' + 'Arrow Annotate', ), _createToolButton( 'EllipticalROI', @@ -206,7 +207,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Ellipse Tool' + 'Ellipse Tool', ), _createToolButton( 'CircleROI', @@ -229,7 +230,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Circle Tool' + 'Circle Tool', ), ], }, @@ -264,7 +265,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Window Level' + 'Window Level', ), secondary: { icon: 'chevron-down', @@ -373,7 +374,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Reset' + 'Reset', ), secondary: { icon: 'chevron-down', @@ -393,7 +394,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Reset' + 'Reset', ), _createActionButton( 'rotate-right', @@ -406,7 +407,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Rotate +90' + 'Rotate +90', ), _createActionButton( 'flip-horizontal', @@ -419,7 +420,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Flip Horizontal' + 'Flip Horizontal', ), _createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [ { @@ -438,7 +439,7 @@ const toolbarButtons = [ commandOptions: {}, context: 'CORNERSTONE', }, - ] + ], ), _createToolButton( 'StackScroll', @@ -453,7 +454,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Stack Scroll' + 'Stack Scroll', ), _createActionButton( 'invert', @@ -466,7 +467,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Invert Colors' + 'Invert Colors', ), _createToolButton( 'Probe', @@ -481,7 +482,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Probe' + 'Probe', ), _createToggleButton( 'cine', @@ -493,7 +494,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Cine' + 'Cine', ), _createToolButton( 'Angle', @@ -508,7 +509,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Angle' + 'Angle', ), // Next two tools can be added once icons are added @@ -555,7 +556,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Magnify' + 'Magnify', ), _createToolButton( 'Rectangle', @@ -570,7 +571,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Rectangle' + 'Rectangle', ), _createToolButton( 'CalibrationLine', @@ -585,7 +586,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Calibration Line' + 'Calibration Line', ), _createActionButton( 'TagBrowser', @@ -598,7 +599,7 @@ const toolbarButtons = [ context: 'DEFAULT', }, ], - 'Dicom Tag Browser' + 'Dicom Tag Browser', ), ], }, From 77119032849c8656d39e473453861f20050403aa Mon Sep 17 00:00:00 2001 From: Sachidanand Alle Date: Tue, 19 Nov 2024 11:49:44 +0000 Subject: [PATCH 02/11] Fix probe tool Signed-off-by: Sachidanand Alle --- .../ohifv3/extensions/monai-label/src/getCommandsModule.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/ohifv3/extensions/monai-label/src/getCommandsModule.ts b/plugins/ohifv3/extensions/monai-label/src/getCommandsModule.ts index 3b96dd695..266a61374 100644 --- a/plugins/ohifv3/extensions/monai-label/src/getCommandsModule.ts +++ b/plugins/ohifv3/extensions/monai-label/src/getCommandsModule.ts @@ -13,9 +13,9 @@ export default function getCommandsModule({ servicesManager }) { }; const definitions = { - setToolActive: { - commandFn: actions.setToolActive, - }, + // setToolActive: { + // commandFn: actions.setToolActive, + // }, }; return { From c1c7a077c23ac219602f5677ca2419e22805d181 Mon Sep 17 00:00:00 2001 From: Sachidanand Alle Date: Tue, 19 Nov 2024 12:15:37 +0000 Subject: [PATCH 03/11] Fix remove server uri Signed-off-by: Sachidanand Alle --- .../ohif/monai-label/src/components/SettingsTable.js | 2 +- .../monai-label/src/components/MonaiLabelPanel.tsx | 8 +++++--- .../monai-label/src/components/SettingsTable.tsx | 12 +++++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/plugins/ohif/monai-label/src/components/SettingsTable.js b/plugins/ohif/monai-label/src/components/SettingsTable.js index 12683b5ec..dfa2deb43 100644 --- a/plugins/ohif/monai-label/src/components/SettingsTable.js +++ b/plugins/ohif/monai-label/src/components/SettingsTable.js @@ -63,7 +63,7 @@ export default class SettingsTable extends Component { name="aiaaServerURL" type="text" defaultValue={this.state.url} - onBlur={this.onBlurSeverURL} + onChange={this.onBlurSeverURL} />   diff --git a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx index be6b75703..82b516f91 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx @@ -35,6 +35,7 @@ export default class MonaiLabelPanel extends Component { StudyInstanceUID: any; FrameOfReferenceUID: any; displaySetInstanceUID: any; + serverURI = 'http://127.0.0.1:8000' constructor(props) { super(props); @@ -83,7 +84,7 @@ export default class MonaiLabelPanel extends Component { ? this.settings.current.state : null; return new MonaiLabelClient( - settings ? settings.url : 'http://127.0.0.1:8000' + settings ? settings.url : this.serverURI ); }; @@ -104,7 +105,7 @@ export default class MonaiLabelPanel extends Component { return viewport; } - onInfo = async () => { + onInfo = async (serverURI) => { const nid = this.notification.show({ title: 'MONAI Label', message: 'Connecting to MONAI Label', @@ -112,6 +113,7 @@ export default class MonaiLabelPanel extends Component { duration: 2000, }); + this.serverURI = serverURI; const response = await this.client().info(); console.log(response.data); @@ -362,7 +364,7 @@ export default class MonaiLabelPanel extends Component { } render() { - const { isDataReady, isInteractiveSeg } = this.state; + const { isDataReady } = this.state; return (

diff --git a/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx b/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx index 5da2ea563..8172e1dd2 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx @@ -37,8 +37,17 @@ export default class SettingsTable extends Component { const url = evt.target.value; this.setState({ url: url }); CookieUtils.setCookie('MONAILABEL_SERVER_URL', url); + console.log("Settings onBlurSeverURL", url); }; + onConnect = ()=> { + const url = document.getElementById("monailabelServerURL").value; + this.setState({ url: url }); + CookieUtils.setCookie('MONAILABEL_SERVER_URL', url); + console.log("Connecting Server", url); + this.onInfo(url); + } + render() { return ( @@ -47,6 +56,7 @@ export default class SettingsTable extends Component { From 8499ceab3986b72a418fa597eddd50be04318b50 Mon Sep 17 00:00:00 2001 From: Sachidanand Alle Date: Thu, 21 Nov 2024 07:41:18 +0000 Subject: [PATCH 04/11] Add options/config dialog + fix 2D segment Signed-off-by: Sachidanand Alle --- .../src/components/MonaiLabelPanel.css | 9 +- .../src/components/MonaiLabelPanel.tsx | 92 +++++-- .../src/components/OptionsInputDialog.tsx | 48 ++++ .../src/components/SettingsTable.tsx | 17 +- .../components/actions/AutoSegmentation.tsx | 12 +- .../src/components/actions/BaseTab.tsx | 1 + .../src/components/actions/ClassPrompts.tsx | 9 +- .../src/components/actions/OptionTable.tsx | 113 +++++++++ .../src/components/actions/OptionsForm.css | 57 +++++ .../src/components/actions/OptionsForm.tsx | 211 ++++++++++++++++ .../src/components/actions/PointPrompts.tsx | 34 ++- .../src/components/callInputDialog.tsx | 62 ----- .../monai-label/src/components/w3.css | 235 ------------------ .../monai-label/src/utils/GenericUtils.js | 9 +- .../ohifv3/modes/monai-label/src/index.tsx | 10 +- .../modes/monai-label/src/initToolGroups.js | 8 +- .../modes/monai-label/src/toolbarButtons.js | 44 ++-- 17 files changed, 589 insertions(+), 382 deletions(-) create mode 100644 plugins/ohifv3/extensions/monai-label/src/components/OptionsInputDialog.tsx create mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.tsx create mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/OptionsForm.css create mode 100644 plugins/ohifv3/extensions/monai-label/src/components/actions/OptionsForm.tsx delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/callInputDialog.tsx delete mode 100644 plugins/ohifv3/extensions/monai-label/src/components/w3.css diff --git a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.css b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.css index 3ad7b2fa4..aa3d60e72 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.css +++ b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.css @@ -1,4 +1,4 @@ -@import url("w3.css"); +/*@import url("w3.css");*/ .monaiLabelPanel { background-color: #000; @@ -10,10 +10,10 @@ /* Accordion styles */ } .monaiLabelPanel .subtitle { - font-size: 14px; + font-size: 9px; text-decoration: underline; font-weight: 500; - color: #000; + color: aqua; margin: 1px; text-align: center; } @@ -21,12 +21,11 @@ border-radius: 4px; overflow: auto; box-shadow: 0 4px 4px -2px rgba(0,0,0,0.5); - background: #91b9cd; margin: 1rem 0; } .monaiLabelPanel .tab { width: 100%; - color: #fff; + /*color: #fff;*/ overflow: hidden; } .monaiLabelPanel .tab-switch { diff --git a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx index 82b516f91..57e4ac26d 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import './MonaiLabelPanel.css'; -import ActiveLearning from './actions/ActiveLearning'; import AutoSegmentation from './actions/AutoSegmentation'; import PointPrompts from './actions/PointPrompts'; import ClassPrompts from './actions/ClassPrompts'; @@ -13,6 +12,7 @@ import SegmentationReader from '../utils/SegmentationReader'; import { currentSegmentsInfo } from '../utils/SegUtils'; import SettingsTable from './SettingsTable'; import * as cornerstoneTools from '@cornerstonejs/tools'; +import optionsInputDialog from './OptionsInputDialog'; export default class MonaiLabelPanel extends Component { static propTypes = { @@ -23,8 +23,9 @@ export default class MonaiLabelPanel extends Component { notification: any; settings: any; - state: { info: {}; action: {} }; + state: { info: {}; action: {}; options: {} }; actions: { + options: any; activelearning: any; segmentation: any; pointprompts: any; @@ -35,7 +36,8 @@ export default class MonaiLabelPanel extends Component { StudyInstanceUID: any; FrameOfReferenceUID: any; displaySetInstanceUID: any; - serverURI = 'http://127.0.0.1:8000' + numberOfFrames: any; + serverURI = 'http://127.0.0.1:8000'; constructor(props) { super(props); @@ -44,7 +46,9 @@ export default class MonaiLabelPanel extends Component { props.servicesManager.services; this.notification = uiNotificationService; + this.settings = React.createRef(); this.actions = { + options: React.createRef(), activelearning: React.createRef(), segmentation: React.createRef(), pointprompts: React.createRef(), @@ -74,6 +78,7 @@ export default class MonaiLabelPanel extends Component { this.StudyInstanceUID = displaySet.StudyInstanceUID; this.FrameOfReferenceUID = displaySet.instances[0].FrameOfReferenceUID; this.displaySetInstanceUID = displaySet.displaySetInstanceUID; + this.numberOfFrames = displaySet.instances.length; } ); } @@ -83,9 +88,7 @@ export default class MonaiLabelPanel extends Component { this.settings && this.settings.current && this.settings.current.state ? this.settings.current.state : null; - return new MonaiLabelClient( - settings ? settings.url : this.serverURI - ); + return new MonaiLabelClient(settings ? settings.url : this.serverURI); }; segmentColor(label) { @@ -103,7 +106,7 @@ export default class MonaiLabelPanel extends Component { const { viewports, activeViewportId } = viewportGridService.getState(); const viewport = viewports.get(activeViewportId); return viewport; - } + }; onInfo = async (serverURI) => { const nid = this.notification.show({ @@ -165,7 +168,7 @@ export default class MonaiLabelPanel extends Component { modelIdxToLabelMap[model] = {}; if (Array.isArray(labels)) { for (let label_idx = 1; label_idx <= labels.length; label_idx++) { - const label = labels[label_idx-1]; + const label = labels[label_idx - 1]; all_labels.push(label); modelLabelToIdxMap[model][label] = label_idx; modelIdxToLabelMap[model][label_idx] = label; @@ -224,7 +227,7 @@ export default class MonaiLabelPanel extends Component { viewportId, '1', initialSegs[segmentIndex].segmentIndex, - initialSegs[segmentIndex].color, + initialSegs[segmentIndex].color ); } }, 1000); @@ -244,6 +247,7 @@ export default class MonaiLabelPanel extends Component { console.log(info); this.setState({ info: info }); this.setState({ isDataReady: true }); // Mark as ready + this.setState({ options: {} }); }; onSelectActionTab = (name) => { @@ -265,8 +269,21 @@ export default class MonaiLabelPanel extends Component { this.setState({ action: name }); }; - updateView = async (response, model_id, labels, override = false, point_prompts = false) => { - console.log('Update View: ', model_id, labels, override); + updateView = async ( + response, + model_id, + labels, + override = false, + label_class_unknown = false, + sidx = -1 + ) => { + console.log('UpdateView: ', { + model_id, + labels, + override, + label_class_unknown, + sidx, + }); const ret = SegmentationReader.parseNrrdData(response.data); if (!ret) { throw new Error('Failed to parse NRRD data'); @@ -307,14 +324,14 @@ export default class MonaiLabelPanel extends Component { const { segmentationService } = this.props.servicesManager.services; const volumeLoadObject = segmentationService.getLabelmapVolume('1'); if (volumeLoadObject) { - console.log('Volume Object is In Cache....'); + // console.log('Volume Object is In Cache....'); let convertedData = data; for (let i = 0; i < convertedData.length; i++) { const midx = convertedData[i]; const sidx = modelToSegMapping[midx]; if (midx && sidx) { convertedData[i] = sidx; - } else if (override && point_prompts && labels.length === 1) { + } else if (override && label_class_unknown && labels.length === 1) { convertedData[i] = midx ? labelNames[labels[0]] : 0; } else if (labels.length > 0) { convertedData[i] = 0; @@ -325,7 +342,7 @@ export default class MonaiLabelPanel extends Component { const { segmentationService } = this.props.servicesManager.services; const volumeLoadObject = segmentationService.getLabelmapVolume('1'); const { voxelManager } = volumeLoadObject; - const scalarData = voxelManager?.getCompleteScalarDataArray() + const scalarData = voxelManager?.getCompleteScalarDataArray(); // console.log('Current ScalarData: ', scalarData); const currentSegArray = new Uint8Array(scalarData.length); @@ -333,7 +350,15 @@ export default class MonaiLabelPanel extends Component { // get unique values to determine which organs to update, keep rest const updateTargets = new Set(convertedData); + const sliceLength = scalarData.length / this.numberOfFrames; + const sliceBegin = sliceLength * sidx; + const sliceEnd = sliceBegin + sliceLength; + for (let i = 0; i < convertedData.length; i++) { + if (sidx >= 0 && (i < sliceBegin || i >= sliceEnd)) { + continue; + } + if ( convertedData[i] !== 255 && updateTargets.has(currentSegArray[i]) @@ -354,15 +379,35 @@ export default class MonaiLabelPanel extends Component { } }; + openConfigurations = (e) => { + e.preventDefault(); + + const { uiDialogService } = this.props.servicesManager.services; + optionsInputDialog( + uiDialogService, + this.state.options, + this.state.info, + (options, actionId) => { + if (actionId === 'save' || actionId == 'reset') { + this.setState({ options: options }); + } + } + ); + }; + async componentDidMount() { if (this.state.isDataReady) { return; } - console.log('(Component Mounted) Connect to MONAI Server...'); + console.log('(Component Mounted) Ready to Connect to MONAI Server...'); // await this.onInfo(); } + onOptionsConfig = () => { + return this.state.options; + }; + render() { const { isDataReady } = this.state; return ( @@ -370,9 +415,17 @@ export default class MonaiLabelPanel extends Component {
-
-

{this.state.info.name}

- + {isDataReady && ( +
+

{this.state.info.data.name}

+
+
+ + Options / Configurations + +
+
+ )} {isDataReady && (
)} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/OptionsInputDialog.tsx b/plugins/ohifv3/extensions/monai-label/src/components/OptionsInputDialog.tsx new file mode 100644 index 000000000..91ca70e1a --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/components/OptionsInputDialog.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Dialog, ButtonEnums } from '@ohif/ui'; +import OptionsForm from './actions/OptionsForm'; + +function optionsInputDialog(uiDialogService, config, info, callback) { + const dialogId = 'monai-label-options'; + const optionsRef = React.createRef(); + + const onSubmitHandler = ({ action }) => { + switch (action.id) { + case 'save': + callback(optionsRef.current.state.config, action.id); + uiDialogService.dismiss({ id: dialogId }); + break; + case 'cancel': + callback({}, action.id); + uiDialogService.dismiss({ id: dialogId }); + break; + case 'reset': + optionsRef.current.onReset(); + break; + } + }; + + uiDialogService.create({ + id: dialogId, + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Options / Configurations', + noCloseButton: true, + onClose: () => uiDialogService.dismiss({ id: dialogId }), + actions: [ + { id: 'reset', text: 'Reset', type: ButtonEnums.type.secondary }, + { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary }, + { id: 'save', text: 'Confirm', type: ButtonEnums.type.primary }, + ], + onSubmit: onSubmitHandler, + body: () => { + return ; + }, + }, + }); +} + +export default optionsInputDialog; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx b/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx index 8172e1dd2..be23b334f 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/SettingsTable.tsx @@ -37,23 +37,25 @@ export default class SettingsTable extends Component { const url = evt.target.value; this.setState({ url: url }); CookieUtils.setCookie('MONAILABEL_SERVER_URL', url); - console.log("Settings onBlurSeverURL", url); + console.log('Settings onBlurSeverURL', url); }; - onConnect = ()=> { - const url = document.getElementById("monailabelServerURL").value; + onConnect = () => { + const url = document.getElementById('monailabelServerURL').value; this.setState({ url: url }); CookieUtils.setCookie('MONAILABEL_SERVER_URL', url); - console.log("Connecting Server", url); + console.log('Connecting Server', url); this.onInfo(url); - } + }; render() { return (
Server:   -
- + + + - - - - +
Server:Server:
  + -
-

Experience fully automated segmentation for everything{' '} - from the pre-trained model.

+
+

+ Experience fully automated segmentation for everything{' '} + from the pre-trained model. +

} /> diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx index 8669849fb..54ee18b31 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx @@ -14,6 +14,7 @@ export default class BaseTab extends Component { client: PropTypes.func, updateView: PropTypes.func, onSelectActionTab: PropTypes.func, + onOptionsConfig: PropTypes.func, }; notification: any; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx index fb49429ee..0e88992ec 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx @@ -105,9 +105,10 @@ export default class ClassPrompts extends BaseTab { } } - const params = { - label_prompt: label_classes, - }; + const config = this.props.onOptionsConfig(); + const params = + config && config.infer && config.infer[model] ? config.infer[model] : {}; + params['label_prompt'] = label_classes; const response = await this.props .client() @@ -334,7 +335,7 @@ export default class ClassPrompts extends BaseTab { onSelectModel={this.onSelectModel} usage={
-
+

Choose following structures or individual classes

} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.tsx new file mode 100644 index 000000000..0dc688f96 --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionTable.tsx @@ -0,0 +1,113 @@ +/* +Copyright (c) MONAI Consortium +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { Component } from 'react'; + +import PropTypes from 'prop-types'; + +export default class OptionTable extends Component { + static propTypes = { + section: PropTypes.string, + name: PropTypes.string, + name_map: PropTypes.any, + update_map: PropTypes.any, + onChangeConfig: PropTypes.func, + }; + + state = { + seed: 0, + }; + + componentDidUpdate(prevProps, prevState) { + if (prevProps.update_map !== this.props.update_map) { + // console.log('update_map - Prop changed!'); + if (!Object.keys(this.props.update_map).length) { + // console.log('Forcing the update.'); + this.setState({ seed: Math.random() }); + return; + } + + Object.entries(this.props.update_map).map(([k, v]) => { + const e = document.getElementById( + this.props.section + this.props.name + k + ); + if (e.type === 'checkbox') { + e.checked = v; + } else { + e.value = v; + } + }); + } + } + + render() { + const { section, name, name_map } = this.props; + const { seed } = this.state; + // console.log('Render Table Object: ' + seed); + return ( +
+ + + + + + + + + {Object.entries(name_map).map(([k, v]) => ( + + + + + ))} + +
KeyValue
{k} + {v !== null && typeof v === 'boolean' ? ( + + this.props.onChangeConfig(section, name, k, e) + } + /> + ) : v !== null && typeof v === 'object' ? ( + + ) : ( + + this.props.onChangeConfig(section, name, k, e) + } + /> + )} +
+
+ ); + } +} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionsForm.css b/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionsForm.css new file mode 100644 index 000000000..00b1381d4 --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionsForm.css @@ -0,0 +1,57 @@ +/* +Copyright (c) MONAI Consortium +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +.optionsForm { + background-color: white; +} +.optionsSection { + background-color: lightyellow; + width: 100%; + border-collapse: collapse; +} +.optionsSection .selectBox { + background-color: white; + border: 1px solid grey; + font-size: medium; + height: fit-content; +} +.optionsConfig { + width: 800px; + height: 300px; + overflow: auto; +} +.optionsConfigTable { + background-color: azure; + font-family: arial, sans-serif; + border-collapse: collapse; + width: 100%; + font-size: smaller; +} +.optionsConfigTable th { + border: 1px solid #070303; + text-align: left; + background-color: #789; +} +.optionsConfigTable td { + border: 1px solid #070202; + text-align: left; +} +.optionsInput { + width: 100%; + color: #000; +} +.selectBox { + width: 100%; + color: #000; + font-size: smaller; + height: 18px; +} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionsForm.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionsForm.tsx new file mode 100644 index 000000000..f436e3aa7 --- /dev/null +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/OptionsForm.tsx @@ -0,0 +1,211 @@ +/* +Copyright (c) MONAI Consortium +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { Component } from 'react'; +import './OptionsForm.css'; + +import OptionTable from './OptionTable'; +import PropTypes from 'prop-types'; + +export default class OptionsForm extends Component { + static propTypes = { + info: PropTypes.any, + config: PropTypes.any, + }; + private configs = {}; + + constructor(props) { + super(props); + + this.state = { + config: { ...props.config }, + section: '', + name: '', + }; + } + + getConfigs() { + const { info } = this.props; + const mapping = { + infer: 'models', + train: 'trainers', + activelearning: 'strategies', + scoring: 'scoring', + }; + + if (!Object.keys(this.configs).length) { + Object.entries(mapping).forEach(([m, n]) => { + const obj = info && info.data && info.data[n] ? info.data[n] : {}; + Object.entries(obj).forEach(([k, v]) => { + if (v && v.config && Object.keys(v.config).length) { + if (!this.configs[m]) { + this.configs[m] = {}; + } + this.configs[m][k] = v.config; + } + }); + }); + } + return this.configs; + } + + getSection() { + return this.state.section.length && this.configs[this.state.section] + ? this.state.section + : Object.keys(this.configs).length + ? Object.keys(this.configs)[0] + : ''; + } + + getSectionMap(section) { + return section && this.configs[section] + ? this.configs[section] + : Object.keys(this.configs).length + ? this.configs[Object.keys(this.configs)[0]] + : {}; + } + + getName(section_map) { + return this.state.name.length && section_map[this.state.name] + ? this.state.name + : Object.keys(section_map).length + ? Object.keys(section_map)[0] + : ''; + } + + getNameMap(name, section_map) { + return name && section_map[name] + ? section_map[name] + : Object.keys(section_map).length + ? section_map[Object.keys(section_map)[0]] + : {}; + } + + onChangeSection = (evt) => { + this.setState({ section: evt.target.value }); + }; + + onChangeName = (evt) => { + this.setState({ name: evt.target.value }); + }; + + onReset = () => { + console.log('Reset the config map'); + this.configs = {}; + this.setState({ + config: {}, + section: '', + name: '', + }); + }; + + onChangeConfig = (s, n, k, evt) => { + // console.log(s + ' => ' + n + ' => ' + k, evt); + const c = { ...this.state.config }; + if (!c[s]) { + c[s] = {}; + } + if (!c[s][n]) { + c[s][n] = {}; + } + + if (typeof this.configs[s][n][k] === 'boolean') { + c[s][n][k] = !!evt.target.checked; + } else { + if (typeof this.configs[s][n][k] === 'number') { + c[s][n][k] = Number.isInteger(this.configs[s][n][k]) + ? parseInt(evt.target.value) + : parseFloat(evt.target.value); + } else { + c[s][n][k] = evt.target.value; + } + } + this.setState({ config: c }); + }; + + render() { + // console.log('Render Options Table..'); + // console.log('State Config: ', this.state.config); + + const config = this.getConfigs(); + const section = this.getSection(); + const section_map = this.getSectionMap(section); + const name = this.getName(section_map); + const name_map = this.getNameMap(name, section_map); + const update_map = {}; + + // console.log('Config State: ', this.state.config); + Object.keys(name_map).forEach((k) => { + if (this.state.config[section] && this.state.config[section][name]) { + const x = this.state.config[section][name][k]; + if (x !== null && x !== undefined) { + update_map[k] = x; + } + } + }); + // console.log('Update Map: ', update_map); + + return ( +
+ + + + + + + + + + + + + + +
Section: + +
Name: + +
 
+ +
+ ); + } +} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx index a02a5bfcb..1b8f73c75 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx @@ -109,7 +109,10 @@ export default class PointPrompts extends BaseTab { label_names.push(label); } - const params = {}; + const config = this.props.onOptionsConfig(); + const params = + config && config.infer && config.infer[model] ? config.infer[model] : {}; + let sidx = -1; if (info.data.models[model].type === 'vista3d') { params['points'] = points[currentLabel]; params['point_labels'] = new Array(params['points'].length).fill(1); @@ -128,12 +131,19 @@ export default class PointPrompts extends BaseTab { params[label] = points[label]; } } else { - let bg = points['background'] && points['background'].length ? points['background'] : []; + let bg = + points['background'] && points['background'].length + ? points['background'] + : []; let fg = points[currentLabel]; if (info.data.models[model].dimension === 2) { - const sidx = fg.length ? fg[fg.length - 1][2] : bg.length ? bg[bg.length - 1][2] : -1; - fg = fg.filter(p => p[2] === sidx); - bg = bg.filter(p => p[2] === sidx); + sidx = fg.length + ? fg[fg.length - 1][2] + : bg.length + ? bg[bg.length - 1][2] + : -1; + fg = fg.filter((p) => p[2] === sidx); + bg = bg.filter((p) => p[2] === sidx); } params['foreground'] = fg; params['background'] = bg; @@ -162,8 +172,16 @@ export default class PointPrompts extends BaseTab { duration: 4000, }); - console.log("Target Labels to update: ", label_names) - this.props.updateView(response, model, label_names, true, true); + const label_class_unknown = info.data.models[model].type === 'deepgrow'; + console.log('Target Labels to update: ', label_names, label_class_unknown); + this.props.updateView( + response, + model, + label_names, + true, + label_class_unknown, + sidx + ); }; initPoints = () => { @@ -308,7 +326,7 @@ export default class PointPrompts extends BaseTab { {/* Auto Run on every*/} {/* click*/} {/*

*/} -
+

Select an anatomy from the segments menu below.

To guide the inference, add foreground clicks:

diff --git a/plugins/ohifv3/extensions/monai-label/src/components/callInputDialog.tsx b/plugins/ohifv3/extensions/monai-label/src/components/callInputDialog.tsx deleted file mode 100644 index c7db40fdb..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/callInputDialog.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { Input, Dialog, ButtonEnums } from '@ohif/ui'; - -function callInputDialog(uiDialogService, label, callback) { - const dialogId = 'enter-segment-label'; - - const onSubmitHandler = ({ action, value }) => { - switch (action.id) { - case 'save': - callback(value.label, action.id); - break; - case 'cancel': - callback('', action.id); - break; - } - uiDialogService.dismiss({ id: dialogId }); - }; - - if (uiDialogService) { - uiDialogService.create({ - id: dialogId, - centralize: true, - isDraggable: false, - showOverlay: true, - content: Dialog, - contentProps: { - title: 'Segment', - value: { label }, - noCloseButton: true, - onClose: () => uiDialogService.dismiss({ id: dialogId }), - actions: [ - { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary }, - { id: 'save', text: 'Confirm', type: ButtonEnums.type.primary }, - ], - onSubmit: onSubmitHandler, - body: ({ value, setValue }) => { - return ( - { - event.persist(); - setValue((value) => ({ ...value, label: event.target.value })); - }} - onKeyPress={(event) => { - if (event.key === 'Enter') { - onSubmitHandler({ value, action: { id: 'save' } }); - } - }} - /> - ); - }, - }, - }); - } -} - -export default callInputDialog; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/w3.css b/plugins/ohifv3/extensions/monai-label/src/components/w3.css deleted file mode 100644 index 3e78e3b2c..000000000 --- a/plugins/ohifv3/extensions/monai-label/src/components/w3.css +++ /dev/null @@ -1,235 +0,0 @@ -/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */ -html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} -/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ -html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} -article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item} -audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} -audio:not([controls]){display:none;height:0}[hidden],template{display:none} -a{background-color:transparent}a:active,a:hover{outline-width:0} -abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} -b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000} -small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} -sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none} -code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} -button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold} -button,input{overflow:visible}button,select{text-transform:none} -button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button} -button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0} -button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText} -fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} -legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} -[type=checkbox],[type=radio]{padding:0} -[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} -[type=search]{-webkit-appearance:textfield;outline-offset:-2px} -[type=search]::-webkit-search-decoration{-webkit-appearance:none} -::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} -/* End extract */ -html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} -h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px} -.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace} -h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} -hr{border:0;border-top:1px solid #eee;margin:20px 0} -.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} -.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} -.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} -.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} -.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} -.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} -.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} -.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} -.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} -.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} -.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} -.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} -.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} -.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} -.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} -.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} -.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} -.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} -.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} -.w3-dropdown-hover:hover .w3-dropdown-content{display:block} -.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} -.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} -.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} -.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} -.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} -.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} -.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} -.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} -.w3-main,#main{transition:margin-left .4s} -.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} -.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} -.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} -.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} -.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} -.w3-bar .w3-button{white-space:normal} -.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} -.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} -.w3-responsive{display:block;overflow-x:auto} -.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, -.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} -.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} -.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} -.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} -.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} -@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} -.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} -.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} -@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} -.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} -.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} -.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px} -.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px} -.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} -.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} -.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} -@media (max-width:1205px){.w3-auto{max-width:95%}} -@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} -.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative} -.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} -.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} -@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} -@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} -@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} -@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}} -.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} -.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} -.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} -.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} -.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} -.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} -.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} -.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} -.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} -.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} -.w3-display-position{position:absolute} -.w3-circle{border-radius:50%} -.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} -.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} -.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} -.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} -.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} -.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} -.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} -.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} -.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} -.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} -.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} -.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} -.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} -.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} -.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} -.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} -.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} -.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} -.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} -.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} -.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} -.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} -.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} -.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} -.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} -.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} -.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} -.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} -.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} -.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} -.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} -.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} -.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} -.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} -.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} -.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} -.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} -.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important} -.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important} -.w3-left{float:left!important}.w3-right{float:right!important} -.w3-button:hover{color:#000!important;background-color:#ccc!important} -.w3-transparent,.w3-hover-none:hover{background-color:transparent!important} -.w3-hover-none:hover{box-shadow:none!important} -/* Colors */ -.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} -.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} -.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} -.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} -.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} -.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} -.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} -.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} -.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} -.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} -.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} -.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} -.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} -.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} -.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} -.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} -.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} -.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} -.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} -.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} -.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} -.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} -.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} -.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} -.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} -.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} -.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} -.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} -.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} -.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} -.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} -.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} -.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} -.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} -.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} -.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} -.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} -.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} -.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} -.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} -.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} -.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} -.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} -.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} -.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} -.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} -.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} -.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} -.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} -.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} -.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} -.w3-text-white,.w3-hover-text-white:hover{color:#fff!important} -.w3-text-black,.w3-hover-text-black:hover{color:#000!important} -.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} -.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} -.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} -.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} -.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} -.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} -.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} -.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} -.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} -.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} -.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} -.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} -.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} -.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} -.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} -.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} -.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} -.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} -.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} -.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} -.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} -.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} -.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} -.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} -.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} -.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} -.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} -.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} -.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} -.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} -.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} diff --git a/plugins/ohifv3/extensions/monai-label/src/utils/GenericUtils.js b/plugins/ohifv3/extensions/monai-label/src/utils/GenericUtils.js index a74e38604..9484d16a6 100644 --- a/plugins/ohifv3/extensions/monai-label/src/utils/GenericUtils.js +++ b/plugins/ohifv3/extensions/monai-label/src/utils/GenericUtils.js @@ -42,16 +42,17 @@ function hexToRgb(hex) { function fixedRGBForLabel(str, toHex = false) { const r = generateIntForString(str); - const x = 2*r % 256; - const y = 3*r % 256; - const z = 5*r % 256; + const x = (2 * r) % 256; + const y = (3 * r) % 256; + const z = (5 * r) % 256; return toHex ? rgbToHex(x, y, z) : { r: x, g: y, b: z }; } function generateIntForString(str) { let hash = str.length * 4; - for (let i = 0; i < str.length; ++i) + for (let i = 0; i < str.length; ++i) { hash += str.charCodeAt(i); + } return hash; } diff --git a/plugins/ohifv3/modes/monai-label/src/index.tsx b/plugins/ohifv3/modes/monai-label/src/index.tsx index ba87904a9..5937f387d 100644 --- a/plugins/ohifv3/modes/monai-label/src/index.tsx +++ b/plugins/ohifv3/modes/monai-label/src/index.tsx @@ -3,11 +3,9 @@ import toolbarButtons from './toolbarButtons.js'; import { id } from './id.js'; import initToolGroups from './initToolGroups.js'; - const monailabel = { monaiLabel: '@ohif/extension-monai-label.panelModule.monailabel', -} - +}; const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', @@ -29,8 +27,6 @@ const segmentation = { viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', }; - - /** * Just two dependencies to be able to render a viewport with panels in order * to make sure that the mode is working. @@ -96,8 +92,6 @@ function modeFactory({ modeConfiguration }) { 'BrushTools', 'Shapes', ]); - - }, onModeExit: ({ servicesManager }) => { const { @@ -190,4 +184,4 @@ const mode = { extensionDependencies, }; -export default mode; \ No newline at end of file +export default mode; diff --git a/plugins/ohifv3/modes/monai-label/src/initToolGroups.js b/plugins/ohifv3/modes/monai-label/src/initToolGroups.js index 88715c8e8..81bc09eb6 100644 --- a/plugins/ohifv3/modes/monai-label/src/initToolGroups.js +++ b/plugins/ohifv3/modes/monai-label/src/initToolGroups.js @@ -20,10 +20,10 @@ function initDefaultToolGroup( extensionManager, toolGroupService, commandsManager, - toolGroupId, + toolGroupId ) { const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.tools', + '@ohif/extension-cornerstone.utilityModule.tools' ); const { toolNames, Enums } = utilityModule.exports; @@ -105,7 +105,7 @@ function initDefaultToolGroup( function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.tools', + '@ohif/extension-cornerstone.utilityModule.tools' ); const { toolNames, Enums } = utilityModule.exports; @@ -201,7 +201,7 @@ function initToolGroups(extensionManager, toolGroupService, commandsManager) { extensionManager, toolGroupService, commandsManager, - 'default', + 'default' ); initMPRToolGroup(extensionManager, toolGroupService, commandsManager); } diff --git a/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js b/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js index 4cd9e9ff0..4836fed27 100644 --- a/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js +++ b/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js @@ -65,7 +65,7 @@ const toolGroupIds = ['default', 'mpr', 'SRToolGroup']; * @returns {Array} an array of 'setToolActive' commands */ function _createSetToolActiveCommands(toolName) { - const temp = toolGroupIds.map(toolGroupId => ({ + const temp = toolGroupIds.map((toolGroupId) => ({ commandName: 'setToolActive', commandOptions: { toolGroupId, @@ -107,7 +107,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Length', + 'Length' ), secondary: { icon: 'chevron-down', @@ -138,7 +138,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Length Tool', + 'Length Tool' ), _createToolButton( 'Bidirectional', @@ -161,7 +161,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Bidirectional Tool', + 'Bidirectional Tool' ), _createToolButton( 'ArrowAnnotate', @@ -184,7 +184,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Arrow Annotate', + 'Arrow Annotate' ), _createToolButton( 'EllipticalROI', @@ -207,7 +207,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Ellipse Tool', + 'Ellipse Tool' ), _createToolButton( 'CircleROI', @@ -230,7 +230,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Circle Tool', + 'Circle Tool' ), ], }, @@ -265,7 +265,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Window Level', + 'Window Level' ), secondary: { icon: 'chevron-down', @@ -374,7 +374,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Reset', + 'Reset' ), secondary: { icon: 'chevron-down', @@ -394,7 +394,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Reset', + 'Reset' ), _createActionButton( 'rotate-right', @@ -407,7 +407,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Rotate +90', + 'Rotate +90' ), _createActionButton( 'flip-horizontal', @@ -420,7 +420,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Flip Horizontal', + 'Flip Horizontal' ), _createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [ { @@ -439,7 +439,7 @@ const toolbarButtons = [ commandOptions: {}, context: 'CORNERSTONE', }, - ], + ] ), _createToolButton( 'StackScroll', @@ -454,7 +454,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Stack Scroll', + 'Stack Scroll' ), _createActionButton( 'invert', @@ -467,7 +467,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Invert Colors', + 'Invert Colors' ), _createToolButton( 'Probe', @@ -482,7 +482,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Probe', + 'Probe' ), _createToggleButton( 'cine', @@ -494,7 +494,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Cine', + 'Cine' ), _createToolButton( 'Angle', @@ -509,7 +509,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Angle', + 'Angle' ), // Next two tools can be added once icons are added @@ -556,7 +556,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Magnify', + 'Magnify' ), _createToolButton( 'Rectangle', @@ -571,7 +571,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Rectangle', + 'Rectangle' ), _createToolButton( 'CalibrationLine', @@ -586,7 +586,7 @@ const toolbarButtons = [ context: 'CORNERSTONE', }, ], - 'Calibration Line', + 'Calibration Line' ), _createActionButton( 'TagBrowser', @@ -599,7 +599,7 @@ const toolbarButtons = [ context: 'DEFAULT', }, ], - 'Dicom Tag Browser', + 'Dicom Tag Browser' ), ], }, From 6123369857ecdafa6c6aacf40c289f35c918f9b1 Mon Sep 17 00:00:00 2001 From: Sachidanand Alle Date: Fri, 22 Nov 2024 00:31:14 +0000 Subject: [PATCH 05/11] Fix monailable mode visibility Signed-off-by: Sachidanand Alle --- .../ohifv3/modes/monai-label/src/index.tsx | 64 ++++++++----------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/plugins/ohifv3/modes/monai-label/src/index.tsx b/plugins/ohifv3/modes/monai-label/src/index.tsx index 5937f387d..301219d03 100644 --- a/plugins/ohifv3/modes/monai-label/src/index.tsx +++ b/plugins/ohifv3/modes/monai-label/src/index.tsx @@ -1,7 +1,7 @@ import { hotkeys } from '@ohif/core'; -import toolbarButtons from './toolbarButtons.js'; -import { id } from './id.js'; -import initToolGroups from './initToolGroups.js'; +import { id } from './id'; +import toolbarButtons from './toolbarButtons'; +import initToolGroups from './initToolGroups'; const monailabel = { monaiLabel: '@ohif/extension-monai-label.panelModule.monailabel', @@ -17,13 +17,11 @@ const ohif = { const cornerstone = { viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', - panelTool: - '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools', + panelTool: '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools', }; const segmentation = { - sopClassHandler: - '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', }; @@ -35,7 +33,6 @@ const extensionDependencies = { '@ohif/extension-default': '^3.0.0', '@ohif/extension-cornerstone': '^3.0.0', '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', - '@ohif/extension-test': '^0.0.1', '@ohif/extension-monai-label': '^3.0.0', }; @@ -56,13 +53,8 @@ function modeFactory({ modeConfiguration }) { * Runs when the Mode Route is mounted to the DOM. Usually used to initialize * Services and other resources. */ - onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { - const { - measurementService, - toolbarService, - toolGroupService, - customizationService, - } = servicesManager.services; + onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => { + const { measurementService, toolbarService, toolGroupService } = servicesManager.services; measurementService.clearMeasurements(); @@ -70,30 +62,22 @@ function modeFactory({ modeConfiguration }) { initToolGroups(extensionManager, toolGroupService, commandsManager); toolbarService.addButtons(toolbarButtons); - // init customizations - customizationService.addModeCustomizations([ - '@ohif/extension-test.customizationModule.custom-context-menu', - ]); + // toolbarService.addButtons(segmentationButtons); - toolbarService.addButtons(toolbarButtons); toolbarService.createButtonSection('primary', [ - 'MeasurementTools', - 'Zoom', 'WindowLevel', 'Pan', + 'Zoom', + 'TrackballRotate', 'Capture', 'Layout', 'MPR', 'Crosshairs', 'MoreTools', ]); - - toolbarService.createButtonSection('segmentationToolbox', [ - 'BrushTools', - 'Shapes', - ]); + toolbarService.createButtonSection('segmentationToolbox', ['BrushTools', 'Shapes']); }, - onModeExit: ({ servicesManager }) => { + onModeExit: ({ servicesManager }: withAppTypes) => { const { toolGroupService, syncGroupService, @@ -117,14 +101,18 @@ function modeFactory({ modeConfiguration }) { }, /** * A boolean return value that indicates whether the mode is valid for the - * modalities of the selected studies. For instance a PET/CT mode should be + * modalities of the selected studies. Currently we don't have stack viewport + * segmentations and we should exclude them */ - isValidMode: function ({ modalities }) { - const modalities_list = modalities.split('\\'); - const isValid = - modalities_list.includes('CT') || modalities_list.includes('MR'); - // Only CT or MR modalities - return isValid; + isValidMode: ({ modalities }) => { + // Don't show the mode if the selected studies have only one modality + // that is not supported by the mode + const modalitiesArray = modalities.split('\\'); + return { + valid: modalitiesArray.includes('CT') || modalitiesArray.includes('MR'), + description: + 'The mode does not support studies that ONLY include the following modalities: SM, OT, DOC', + }; }, /** * Mode Routes are used to define the mode's behavior. A list of Mode Route @@ -170,10 +158,8 @@ function modeFactory({ modeConfiguration }) { hangingProtocol: 'mpr', // hangingProtocol: [''], /** SopClassHandlers used by the mode */ - sopClassHandlers: [ - segmentation.sopClassHandler, - ohif.sopClassHandler, - ] /** hotkeys for mode */, + sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler], + /** hotkeys for mode */ hotkeys: [...hotkeys.defaults.hotkeyBindings], }; } From c43cc25e6d9a6e251560a3aa4f6e1f02e5718301 Mon Sep 17 00:00:00 2001 From: Sachidanand Alle Date: Fri, 22 Nov 2024 10:05:14 +0000 Subject: [PATCH 06/11] Fix toolbar + viewinfo constants Signed-off-by: Sachidanand Alle --- .../src/components/MonaiLabelPanel.tsx | 89 +-- .../components/actions/AutoSegmentation.tsx | 7 +- .../src/components/actions/BaseTab.tsx | 5 +- .../src/components/actions/ClassPrompts.tsx | 7 +- .../src/components/actions/PointPrompts.tsx | 14 +- .../modes/monai-label/src/initToolGroups.js | 243 +++--- .../modes/monai-label/src/toolbarButtons.js | 752 +++++------------- 7 files changed, 372 insertions(+), 745 deletions(-) diff --git a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx index 57e4ac26d..c8cee03d4 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx @@ -22,34 +22,21 @@ export default class MonaiLabelPanel extends Component { }; notification: any; - settings: any; - state: { info: {}; action: {}; options: {} }; + settings; actions: { - options: any; - activelearning: any; segmentation: any; pointprompts: any; classprompts: any; }; - props: any; - SeriesInstanceUID: any; - StudyInstanceUID: any; - FrameOfReferenceUID: any; - displaySetInstanceUID: any; - numberOfFrames: any; serverURI = 'http://127.0.0.1:8000'; constructor(props) { super(props); - const { uiNotificationService, viewportGridService, displaySetService } = - props.servicesManager.services; - + const { uiNotificationService } = props.servicesManager.services; this.notification = uiNotificationService; this.settings = React.createRef(); this.actions = { - options: React.createRef(), - activelearning: React.createRef(), segmentation: React.createRef(), pointprompts: React.createRef(), classprompts: React.createRef(), @@ -58,29 +45,8 @@ export default class MonaiLabelPanel extends Component { this.state = { info: { models: [], datasets: [] }, action: {}, + options: {}, }; - - viewportGridService.subscribe( - viewportGridService.EVENTS.GRID_SIZE_CHANGED, - () => { - const { viewports, activeViewportId } = viewportGridService.getState(); - const viewport = viewports.get(activeViewportId); - - if (!viewport) { - return; - } - - const displaySet = displaySetService.getDisplaySetByUID( - viewport.displaySetInstanceUIDs[0] - ); - - this.SeriesInstanceUID = displaySet.SeriesInstanceUID; - this.StudyInstanceUID = displaySet.StudyInstanceUID; - this.FrameOfReferenceUID = displaySet.instances[0].FrameOfReferenceUID; - this.displaySetInstanceUID = displaySet.displaySetInstanceUID; - this.numberOfFrames = displaySet.instances.length; - } - ); } client = () => { @@ -102,10 +68,21 @@ export default class MonaiLabelPanel extends Component { } getActiveViewportInfo = () => { - const { viewportGridService } = this.props.servicesManager.services; + const { viewportGridService, displaySetService } = + this.props.servicesManager.services; const { viewports, activeViewportId } = viewportGridService.getState(); const viewport = viewports.get(activeViewportId); - return viewport; + const displaySet = displaySetService.getDisplaySetByUID( + viewport.displaySetInstanceUIDs[0] + ); + + // viewportId = viewport.viewportId + // SeriesInstanceUID = displaySet.SeriesInstanceUID; + // StudyInstanceUID = displaySet.StudyInstanceUID; + // FrameOfReferenceUID = displaySet.instances[0].FrameOfReferenceUID; + // displaySetInstanceUID = displaySet.displaySetInstanceUID; + // numImageFrames = displaySet.numImageFrames; + return { viewport, displaySet }; }; onInfo = async (serverURI) => { @@ -221,10 +198,10 @@ export default class MonaiLabelPanel extends Component { // Wait for Above Segmentations to be added/available setTimeout(() => { - const { viewportId } = this.getActiveViewportInfo(); + const { viewport } = this.getActiveViewportInfo(); for (const segmentIndex of Object.keys(initialSegs)) { cornerstoneTools.segmentation.config.color.setSegmentIndexColor( - viewportId, + viewport.viewportId, '1', initialSegs[segmentIndex].segmentIndex, initialSegs[segmentIndex].color @@ -350,7 +327,9 @@ export default class MonaiLabelPanel extends Component { // get unique values to determine which organs to update, keep rest const updateTargets = new Set(convertedData); - const sliceLength = scalarData.length / this.numberOfFrames; + const numImageFrames = + this.getActiveViewportInfo().displaySet.numImageFrames; + const sliceLength = scalarData.length / numImageFrames; const sliceBegin = sliceLength * sidx; const sliceEnd = sliceBegin + sliceLength; @@ -432,47 +411,35 @@ export default class MonaiLabelPanel extends Component { ref={this.actions['segmentation']} tabIndex={2} info={this.state.info} - viewConstants={{ - SeriesInstanceUID: this.SeriesInstanceUID, - StudyInstanceUID: this.StudyInstanceUID, - }} client={this.client} - notification={this.notification} updateView={this.updateView} onSelectActionTab={this.onSelectActionTab} onOptionsConfig={this.onOptionsConfig} + getActiveViewportInfo={this.getActiveViewportInfo} /> )} diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx index 2af85c1c5..0af6e57a5 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx @@ -33,7 +33,8 @@ export default class AutoSegmentation extends BaseTab { onSegmentation = async () => { const { currentModel, currentLabel, clickPoints } = this.state; - const { info, viewConstants } = this.props; + const { info } = this.props; + const { displaySet } = this.props.getActiveViewportInfo(); const models = this.getModels(); let selectedModel = 0; @@ -87,8 +88,8 @@ export default class AutoSegmentation extends BaseTab { const response = await this.props .client() - .infer(model, viewConstants.SeriesInstanceUID, params); - console.log(response); + .infer(model, displaySet.SeriesInstanceUID, params); + // console.log(response); hideNotification(nid, this.notification); if (response.status !== 200) { diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx index 54ee18b31..cb64b60d5 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/BaseTab.tsx @@ -9,12 +9,13 @@ export default class BaseTab extends Component { static propTypes = { tabIndex: PropTypes.number, info: PropTypes.any, - segmentId: PropTypes.string, - viewConstants: PropTypes.any, client: PropTypes.func, updateView: PropTypes.func, onSelectActionTab: PropTypes.func, onOptionsConfig: PropTypes.func, + getActiveViewportInfo: PropTypes.func, + servicesManager: PropTypes.any, + commandsManager: PropTypes.any, }; notification: any; diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx index 0e88992ec..086d2f8f7 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx @@ -58,7 +58,8 @@ export default class ClassPrompts extends BaseTab { onRunInference = async () => { const { currentModel, currentLabel, clickPoints } = this.state; - const { info, viewConstants } = this.props; + const { info } = this.props; + const { displaySet } = this.props.getActiveViewportInfo(); const models = this.getModels(); let selectedModel = 0; @@ -112,8 +113,8 @@ export default class ClassPrompts extends BaseTab { const response = await this.props .client() - .infer(model, viewConstants.SeriesInstanceUID, params); - console.log(response.data); + .infer(model, displaySet.SeriesInstanceUID, params); + // console.log(response.data); hideNotification(nid, this.notification); if (response.status !== 200) { diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx index 1b8f73c75..2dcebd936 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx @@ -52,7 +52,8 @@ export default class PointPrompts extends BaseTab { onRunInference = async () => { const { currentModel, currentLabel, clickPoints } = this.state; - const { info, viewConstants } = this.props; + const { info } = this.props; + const { viewport, displaySet } = this.props.getActiveViewportInfo(); const models = this.getModels(); let selectedModel = 0; @@ -82,11 +83,11 @@ export default class PointPrompts extends BaseTab { }); const { cornerstoneViewportService } = this.props.servicesManager.services; - const viewPort = cornerstoneViewportService.viewportsById.get('mpr-axial'); - const { worldToIndex } = viewPort.viewportData.data[0].volume.imageData; + const viewportInfo = cornerstoneViewportService.getViewportInfo( + viewport.viewportId + ); + const { worldToIndex } = viewportInfo.viewportData.data[0].volume.imageData; - // console.log(seriesInstanceUID); - // console.log(viewPort); const manager = cornerstoneTools.annotation.state.getAnnotationManager(); clickPoints[currentLabel] = manager.saveAnnotations( null, @@ -152,7 +153,8 @@ export default class PointPrompts extends BaseTab { const response = await this.props .client() - .infer(model, viewConstants.SeriesInstanceUID, params); + .infer(model, displaySet.SeriesInstanceUID, params); + // console.log(response); hideNotification(nid, this.notification); if (response.status !== 200) { diff --git a/plugins/ohifv3/modes/monai-label/src/initToolGroups.js b/plugins/ohifv3/modes/monai-label/src/initToolGroups.js index 81bc09eb6..2f540d32a 100644 --- a/plugins/ohifv3/modes/monai-label/src/initToolGroups.js +++ b/plugins/ohifv3/modes/monai-label/src/initToolGroups.js @@ -1,105 +1,105 @@ -const brushInstanceNames = { - CircularBrush: 'CircularBrush', - CircularEraser: 'CircularEraser', - SphereBrush: 'SphereBrush', - SphereEraser: 'SphereEraser', - ThresholdCircularBrush: 'ThresholdCircularBrush', - ThresholdSphereBrush: 'ThresholdSphereBrush', +const colours = { + 'viewport-0': 'rgb(200, 0, 0)', + 'viewport-1': 'rgb(200, 200, 0)', + 'viewport-2': 'rgb(0, 200, 0)', }; -const brushStrategies = { - [brushInstanceNames.CircularBrush]: 'FILL_INSIDE_CIRCLE', - [brushInstanceNames.CircularEraser]: 'ERASE_INSIDE_CIRCLE', - [brushInstanceNames.SphereBrush]: 'FILL_INSIDE_SPHERE', - [brushInstanceNames.SphereEraser]: 'ERASE_INSIDE_SPHERE', - [brushInstanceNames.ThresholdCircularBrush]: 'THRESHOLD_INSIDE_CIRCLE', - [brushInstanceNames.ThresholdSphereBrush]: 'THRESHOLD_INSIDE_SPHERE', +const colorsByOrientation = { + axial: 'rgb(200, 0, 0)', + sagittal: 'rgb(200, 200, 0)', + coronal: 'rgb(0, 200, 0)', }; -function initDefaultToolGroup( - extensionManager, - toolGroupService, - commandsManager, - toolGroupId -) { - const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.tools' - ); - +function createTools(utilityModule) { const { toolNames, Enums } = utilityModule.exports; - - const tools = { + return { active: [ - { - toolName: toolNames.WindowLevel, - bindings: [{ mouseButton: Enums.MouseBindings.Primary }], - }, - { - toolName: toolNames.Pan, - bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], - }, - { - toolName: toolNames.Zoom, - bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], - }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + { toolName: toolNames.WindowLevel, bindings: [{ mouseButton: Enums.MouseBindings.Primary }] }, + { toolName: toolNames.Pan, bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }] }, + { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }] }, + { toolName: toolNames.StackScroll, bindings: [{ mouseButton: Enums.MouseBindings.Wheel }] }, ], passive: [ - { toolName: toolNames.CircleScissors }, - { toolName: toolNames.RectangleScissors }, - { toolName: toolNames.SphereScissors }, { - toolName: brushInstanceNames.CircularBrush, + toolName: 'CircularBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'FILL_INSIDE_CIRCLE', + }, + }, + { + toolName: 'CircularEraser', parentTool: 'Brush', configuration: { - activeStrategy: brushStrategies.CircularBrush, + activeStrategy: 'ERASE_INSIDE_CIRCLE', }, }, { - toolName: brushInstanceNames.CircularEraser, + toolName: 'SphereBrush', parentTool: 'Brush', configuration: { - activeStrategy: brushStrategies.CircularEraser, + activeStrategy: 'FILL_INSIDE_SPHERE', }, }, { - toolName: brushInstanceNames.SphereEraser, + toolName: 'SphereEraser', parentTool: 'Brush', configuration: { - activeStrategy: brushStrategies.SphereEraser, + activeStrategy: 'ERASE_INSIDE_SPHERE', }, }, { - toolName: brushInstanceNames.SphereBrush, + toolName: 'ThresholdCircularBrush', parentTool: 'Brush', configuration: { - activeStrategy: brushStrategies.SphereBrush, + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', }, }, { - toolName: brushInstanceNames.ThresholdCircularBrush, + toolName: 'ThresholdSphereBrush', parentTool: 'Brush', configuration: { - activeStrategy: brushStrategies.ThresholdCircularBrush, + activeStrategy: 'THRESHOLD_INSIDE_SPHERE', }, }, { - toolName: brushInstanceNames.ThresholdSphereBrush, + toolName: 'ThresholdCircularBrushDynamic', parentTool: 'Brush', configuration: { - activeStrategy: brushStrategies.ThresholdSphereBrush, + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + // preview: { + // enabled: true, + // }, + strategySpecificConfiguration: { + // to use the use the center segment index to determine + // if inside -> same segment, if outside -> eraser + // useCenterSegmentIndex: true, + THRESHOLD: { + isDynamic: true, + dynamicRadius: 3, + }, + }, }, }, + { toolName: toolNames.CircleScissors }, + { toolName: toolNames.RectangleScissors }, + { toolName: toolNames.SphereScissors }, { toolName: toolNames.StackScroll }, { toolName: toolNames.Magnify }, - { toolName: toolNames.SegmentationDisplay }, - { toolName: 'ProbeMONAILabel' }, + { toolName: toolNames.WindowLevelRegion }, + + { toolName: toolNames.UltrasoundDirectional }, + { toolName: 'ProbeMONAILabel' } ], - // enabled - // disabled - disabled: [{ toolName: toolNames.ReferenceLines }], + disabled: [{ toolName: toolNames.ReferenceLines }, { toolName: toolNames.AdvancedMagnify }], }; +} +function initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, toolGroupId) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + const tools = createTools(utilityModule); toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); } @@ -107,103 +107,76 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { const utilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone.utilityModule.tools' ); + const servicesManager = extensionManager._servicesManager; + const { cornerstoneViewportService } = servicesManager.services; + const tools = createTools(utilityModule); + tools.disabled.push( + { + toolName: utilityModule.exports.toolNames.Crosshairs, + configuration: { + viewportIndicators: true, + viewportIndicatorsConfig: { + circleRadius: 5, + xOffset: 0.95, + yOffset: 0.05, + }, + disableOnPassive: true, + autoPan: { + enabled: false, + panSize: 10, + }, + getReferenceLineColor: viewportId => { + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + const viewportOptions = viewportInfo?.viewportOptions; + if (viewportOptions) { + return ( + colours[viewportOptions.id] || + colorsByOrientation[viewportOptions.orientation] || + '#0c0' + ); + } else { + console.warn('missing viewport?', viewportId); + return '#0c0'; + } + }, + }, + }, + { toolName: utilityModule.exports.toolNames.ReferenceLines } + ); + toolGroupService.createToolGroupAndAddTools('mpr', tools); +} + +function initVolume3DToolGroup(extensionManager, toolGroupService) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); const { toolNames, Enums } = utilityModule.exports; const tools = { active: [ { - toolName: toolNames.WindowLevel, + toolName: toolNames.TrackballRotateTool, bindings: [{ mouseButton: Enums.MouseBindings.Primary }], }, - { - toolName: toolNames.Pan, - bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], - }, { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, - ], - passive: [ - { toolName: toolNames.CircleScissors }, - { toolName: toolNames.RectangleScissors }, - { toolName: toolNames.SphereScissors }, - { - toolName: brushInstanceNames.CircularBrush, - parentTool: 'Brush', - configuration: { - activeStrategy: brushStrategies.CircularBrush, - }, - }, - { - toolName: brushInstanceNames.CircularEraser, - parentTool: 'Brush', - configuration: { - activeStrategy: brushStrategies.CircularEraser, - }, - }, - { - toolName: brushInstanceNames.SphereEraser, - parentTool: 'Brush', - configuration: { - activeStrategy: brushStrategies.SphereEraser, - }, - }, { - toolName: brushInstanceNames.SphereBrush, - parentTool: 'Brush', - configuration: { - activeStrategy: brushStrategies.SphereBrush, - }, - }, - { - toolName: brushInstanceNames.ThresholdCircularBrush, - parentTool: 'Brush', - configuration: { - activeStrategy: brushStrategies.ThresholdCircularBrush, - }, - }, - { - toolName: brushInstanceNames.ThresholdSphereBrush, - parentTool: 'Brush', - configuration: { - activeStrategy: brushStrategies.ThresholdSphereBrush, - }, - }, - { toolName: toolNames.SegmentationDisplay }, - { toolName: 'ProbeMONAILabel' }, - { toolName: 'ProbeMONAILabel' }, - ], - disabled: [ - { - toolName: toolNames.Crosshairs, - configuration: { - viewportIndicators: false, - autoPan: { - enabled: false, - panSize: 10, - }, - }, + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], }, - { toolName: toolNames.ReferenceLines }, ], - // enabled - // disabled }; - toolGroupService.createToolGroupAndAddTools('mpr', tools); + toolGroupService.createToolGroupAndAddTools('volume3d', tools); } function initToolGroups(extensionManager, toolGroupService, commandsManager) { - initDefaultToolGroup( - extensionManager, - toolGroupService, - commandsManager, - 'default' - ); + initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default'); initMPRToolGroup(extensionManager, toolGroupService, commandsManager); + initVolume3DToolGroup(extensionManager, toolGroupService); } export default initToolGroups; diff --git a/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js b/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js index 4836fed27..a59c994eb 100644 --- a/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js +++ b/plugins/ohifv3/modes/monai-label/src/toolbarButtons.js @@ -1,606 +1,288 @@ -// TODO: torn, can either bake this here; or have to create a whole new button type -// Only ways that you can pass in a custom React component for render :l -import { - // ExpandableToolbarButton, - // ListMenu, - WindowLevelMenuItem, -} from '@ohif/ui'; -import { defaults } from '@ohif/core'; +import type { Button } from '@ohif/core/types'; +import { ToolbarService, ViewportGridService } from '@ohif/core'; -const { windowLevelPresets } = defaults; +const { createButton } = ToolbarService; -/** - * - * @param {*} type - 'tool' | 'action' | 'toggle' - * @param {*} id - * @param {*} icon - * @param {*} label - */ -function _createButton(type, id, icon, label, commands, tooltip, uiType) { - return { - id, - icon, - label, - type, - commands, - tooltip, - uiType, - }; -} - -const _createActionButton = _createButton.bind(null, 'action'); -const _createToggleButton = _createButton.bind(null, 'toggle'); -const _createToolButton = _createButton.bind(null, 'tool'); - -/** - * - * @param {*} preset - preset number (from above import) - * @param {*} title - * @param {*} subtitle - */ -function _createWwwcPreset(preset, title, subtitle) { - return { - id: preset.toString(), - title, - subtitle, - type: 'action', - commands: [ - { - commandName: 'setWindowLevel', - commandOptions: { - ...windowLevelPresets[preset], - }, - context: 'CORNERSTONE', - }, - ], - }; -} - -const toolGroupIds = ['default', 'mpr', 'SRToolGroup']; - -/** - * Creates an array of 'setToolActive' commands for the given toolName - one for - * each toolGroupId specified in toolGroupIds. - * @param {string} toolName - * @returns {Array} an array of 'setToolActive' commands - */ -function _createSetToolActiveCommands(toolName) { - const temp = toolGroupIds.map((toolGroupId) => ({ - commandName: 'setToolActive', - commandOptions: { - toolGroupId, - toolName, - }, +const ReferenceLinesListeners: RunCommand = [ + { + commandName: 'setSourceViewportForReferenceLinesTool', context: 'CORNERSTONE', - })); - return temp; -} + }, +]; -const toolbarButtons = [ - // Measurement - { - id: 'MeasurementTools', - type: 'ohif.splitButton', - props: { - groupId: 'MeasurementTools', - isRadio: true, // ? - // Switch? - primary: _createToolButton( - 'Length', - 'tool-length', - 'Length', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'Length', - }, - context: 'CORNERSTONE', - }, - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'SRLength', - toolGroupId: 'SRToolGroup', - }, - // we can use the setToolActive command for this from Cornerstone commandsModule - context: 'CORNERSTONE', - }, - ], - 'Length' - ), - secondary: { - icon: 'chevron-down', - label: '', - isActive: true, - tooltip: 'More Measure Tools', - }, - items: [ - _createToolButton( - 'Length', - 'tool-length', - 'Length', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'Length', - }, - context: 'CORNERSTONE', - }, - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'SRLength', - toolGroupId: 'SRToolGroup', - }, - // we can use the setToolActive command for this from Cornerstone commandsModule - context: 'CORNERSTONE', - }, - ], - 'Length Tool' - ), - _createToolButton( - 'Bidirectional', - 'tool-bidirectional', - 'Bidirectional', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'Bidirectional', - }, - context: 'CORNERSTONE', - }, - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'SRBidirectional', - toolGroupId: 'SRToolGroup', - }, - context: 'CORNERSTONE', - }, - ], - 'Bidirectional Tool' - ), - _createToolButton( - 'ArrowAnnotate', - 'tool-annotate', - 'Annotation', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'ArrowAnnotate', - }, - context: 'CORNERSTONE', - }, - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'SRArrowAnnotate', - toolGroupId: 'SRToolGroup', - }, - context: 'CORNERSTONE', - }, - ], - 'Arrow Annotate' - ), - _createToolButton( - 'EllipticalROI', - 'tool-elipse', - 'Ellipse', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'EllipticalROI', - }, - context: 'CORNERSTONE', - }, - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'SREllipticalROI', - toolGroupId: 'SRToolGroup', - }, - context: 'CORNERSTONE', - }, - ], - 'Ellipse Tool' - ), - _createToolButton( - 'CircleROI', - 'tool-circle', - 'Circle', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'CircleROI', - }, - context: 'CORNERSTONE', - }, - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'SRCircleROI', - toolGroupId: 'SRToolGroup', - }, - context: 'CORNERSTONE', - }, - ], - 'Circle Tool' - ), - ], - }, +export const setToolActiveToolbar = { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'], }, - // Zoom.. +}; + +const toolbarButtons: Button[] = [ { id: 'Zoom', - type: 'ohif.radioGroup', + uiType: 'ohif.radioGroup', props: { - type: 'tool', icon: 'tool-zoom', label: 'Zoom', - commands: _createSetToolActiveCommands('Zoom'), + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', }, }, - // Window Level + Presets... { id: 'WindowLevel', - type: 'ohif.splitButton', + uiType: 'ohif.radioGroup', props: { - groupId: 'WindowLevel', - primary: _createToolButton( - 'WindowLevel', - 'tool-window-level', - 'Window Level', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'WindowLevel', - }, - context: 'CORNERSTONE', - }, - ], - 'Window Level' - ), - secondary: { - icon: 'chevron-down', - label: 'W/L Manual', - isActive: true, - tooltip: 'W/L Presets', - }, - isAction: true, // ? - renderer: WindowLevelMenuItem, - items: [ - _createWwwcPreset(1, 'Soft tissue', '400 / 40'), - _createWwwcPreset(2, 'Lung', '1500 / -600'), - _createWwwcPreset(3, 'Liver', '150 / 90'), - _createWwwcPreset(4, 'Bone', '2500 / 480'), - _createWwwcPreset(5, 'Brain', '80 / 40'), - ], + icon: 'tool-window-level', + label: 'Window Level', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', }, }, - // Pan... { id: 'Pan', - type: 'ohif.radioGroup', + uiType: 'ohif.radioGroup', props: { - type: 'tool', icon: 'tool-move', label: 'Pan', - commands: _createSetToolActiveCommands('Pan'), + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'TrackballRotate', + uiType: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-3d-rotate', + label: '3D Rotate', + commands: setToolActiveToolbar, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select a 3D viewport to enable this tool', + }, }, }, { id: 'Capture', - type: 'ohif.action', + uiType: 'ohif.radioGroup', props: { icon: 'tool-capture', label: 'Capture', - type: 'action', - commands: [ + commands: 'showDownloadViewportModal', + evaluate: [ + 'evaluate.action', { - commandName: 'showDownloadViewportModal', - commandOptions: {}, - context: 'CORNERSTONE', + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'wholeSlide'], }, ], }, }, { id: 'Layout', - type: 'ohif.layoutSelector', + uiType: 'ohif.layoutSelector', props: { rows: 3, - columns: 3, - }, - }, - { - id: 'MPR', - type: 'ohif.action', - props: { - type: 'toggle', - icon: 'icon-mpr', - label: 'MPR', - commands: [ - { - commandName: 'toggleHangingProtocol', - commandOptions: { - protocolId: 'mpr', - }, - context: 'DEFAULT', - }, - ], + columns: 4, + evaluate: 'evaluate.action', + commands: 'setViewportGridLayout', }, }, { id: 'Crosshairs', - type: 'ohif.radioGroup', + uiType: 'ohif.radioGroup', props: { - type: 'tool', icon: 'tool-crosshair', label: 'Crosshairs', - commands: [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'Crosshairs', - toolGroupId: 'mpr', - }, - context: 'CORNERSTONE', + commands: { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['mpr'], }, - ], + }, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select an MPR viewport to enable this tool', + }, }, }, - // More... { id: 'MoreTools', - type: 'ohif.splitButton', + uiType: 'ohif.splitButton', props: { - isRadio: true, // ? groupId: 'MoreTools', - primary: _createActionButton( - 'Reset', - 'tool-reset', - 'Reset View', - [ - { - commandName: 'resetViewport', - commandOptions: {}, - context: 'CORNERSTONE', - }, - ], - 'Reset' - ), + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: createButton({ + id: 'Reset', + icon: 'tool-reset', + tooltip: 'Reset View', + label: 'Reset', + commands: 'resetViewport', + evaluate: 'evaluate.action', + }), secondary: { icon: 'chevron-down', label: '', - isActive: true, tooltip: 'More Tools', }, items: [ - _createActionButton( - 'Reset', - 'tool-reset', - 'Reset View', - [ - { - commandName: 'resetViewport', - commandOptions: {}, - context: 'CORNERSTONE', - }, - ], - 'Reset' - ), - _createActionButton( - 'rotate-right', - 'tool-rotate-right', - 'Rotate Right', - [ + createButton({ + id: 'Reset', + icon: 'tool-reset', + label: 'Reset View', + tooltip: 'Reset View', + commands: 'resetViewport', + evaluate: 'evaluate.action', + }), + createButton({ + id: 'rotate-right', + icon: 'tool-rotate-right', + label: 'Rotate Right', + tooltip: 'Rotate +90', + commands: 'rotateViewportCW', + evaluate: 'evaluate.action', + }), + createButton({ + id: 'flipHorizontal', + icon: 'tool-flip-horizontal', + label: 'Flip Horizontal', + tooltip: 'Flip Horizontally', + commands: 'flipViewportHorizontal', + evaluate: [ + 'evaluate.viewportProperties.toggle', { - commandName: 'rotateViewportCW', - commandOptions: {}, - context: 'CORNERSTONE', + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], }, ], - 'Rotate +90' - ), - _createActionButton( - 'flip-horizontal', - 'tool-flip-horizontal', - 'Flip Horizontally', - [ - { - commandName: 'flipViewportHorizontal', - commandOptions: {}, - context: 'CORNERSTONE', - }, - ], - 'Flip Horizontal' - ), - _createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [ - { - commandName: 'toggleStackImageSync', - commandOptions: {}, - context: 'CORNERSTONE', + }), + createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: 'toggleEnabledDisabledToolbar', + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, }, - ]), - _createToggleButton( - 'ReferenceLines', - 'tool-referenceLines', // change this with the new icon - 'Reference Lines', - [ - { - commandName: 'toggleReferenceLines', - commandOptions: {}, - context: 'CORNERSTONE', - }, - ] - ), - _createToolButton( - 'StackScroll', - 'tool-stack-scroll', - 'Stack Scroll', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'StackScroll', - }, - context: 'CORNERSTONE', - }, - ], - 'Stack Scroll' - ), - _createActionButton( - 'invert', - 'tool-invert', - 'Invert', - [ - { - commandName: 'invertViewport', - commandOptions: {}, - context: 'CORNERSTONE', - }, - ], - 'Invert Colors' - ), - _createToolButton( - 'Probe', - 'tool-probe', - 'Probe', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'DragProbe', - }, - context: 'CORNERSTONE', - }, - ], - 'Probe' - ), - _createToggleButton( - 'cine', - 'tool-cine', - 'Cine', - [ - { - commandName: 'toggleCine', - context: 'CORNERSTONE', - }, - ], - 'Cine' - ), - _createToolButton( - 'Angle', - 'tool-angle', - 'Angle', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'Angle', - }, - context: 'CORNERSTONE', - }, - ], - 'Angle' - ), - - // Next two tools can be added once icons are added - // _createToolButton( - // 'Cobb Angle', - // 'tool-cobb-angle', - // 'Cobb Angle', - // [ - // { - // commandName: 'setToolActive', - // commandOptions: { - // toolName: 'CobbAngle', - // }, - // context: 'CORNERSTONE', - // }, - // ], - // 'Cobb Angle' - // ), - // _createToolButton( - // 'Planar Freehand ROI', - // 'tool-freehand', - // 'PlanarFreehandROI', - // [ - // { - // commandName: 'setToolActive', - // commandOptions: { - // toolName: 'PlanarFreehandROI', - // }, - // context: 'CORNERSTONE', - // }, - // ], - // 'Planar Freehand ROI' - // ), - _createToolButton( - 'Magnify', - 'tool-magnify', - 'Magnify', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'Magnify', - }, - context: 'CORNERSTONE', - }, - ], - 'Magnify' - ), - _createToolButton( - 'Rectangle', - 'tool-rectangle', - 'Rectangle', - [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'RectangleROI', - }, - context: 'CORNERSTONE', - }, - ], - 'Rectangle' - ), - _createToolButton( - 'CalibrationLine', - 'tool-calibration', - 'Calibration', - [ + evaluate: 'evaluate.cornerstoneTool.toggle', + }), + createButton({ + id: 'ImageOverlayViewer', + icon: 'toggle-dicom-overlay', + label: 'Image Overlay', + tooltip: 'Toggle Image Overlay', + commands: 'toggleEnabledDisabledToolbar', + evaluate: 'evaluate.cornerstoneTool.toggle', + }), + createButton({ + id: 'StackScroll', + icon: 'tool-stack-scroll', + label: 'Stack Scroll', + tooltip: 'Stack Scroll', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'invert', + icon: 'tool-invert', + label: 'Invert', + tooltip: 'Invert Colors', + commands: 'invertViewport', + evaluate: 'evaluate.viewportProperties.toggle', + }), + createButton({ + id: 'Probe', + icon: 'tool-probe', + label: 'Probe', + tooltip: 'Probe', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'Cine', + icon: 'tool-cine', + label: 'Cine', + tooltip: 'Cine', + commands: 'toggleCine', + evaluate: [ + 'evaluate.cine', { - commandName: 'setToolActive', - commandOptions: { - toolName: 'CalibrationLine', - }, - context: 'CORNERSTONE', + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], }, ], - 'Calibration Line' - ), - _createActionButton( - 'TagBrowser', - 'list-bullets', - 'Dicom Tag Browser', - [ + }), + createButton({ + id: 'Angle', + icon: 'tool-angle', + label: 'Angle', + tooltip: 'Angle', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'Magnify', + icon: 'tool-magnify', + label: 'Zoom-in', + tooltip: 'Zoom-in', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'RectangleROI', + icon: 'tool-rectangle', + label: 'Rectangle', + tooltip: 'Rectangle', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'CalibrationLine', + icon: 'tool-calibration', + label: 'Calibration', + tooltip: 'Calibration Line', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'TagBrowser', + icon: 'dicom-tag-browser', + label: 'Dicom Tag Browser', + tooltip: 'Dicom Tag Browser', + commands: 'openDICOMTagViewer', + }), + createButton({ + id: 'AdvancedMagnify', + icon: 'icon-tool-loupe', + label: 'Magnify Probe', + tooltip: 'Magnify Probe', + commands: 'toggleActiveDisabledToolbar', + evaluate: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled', + }), + createButton({ + id: 'UltrasoundDirectionalTool', + icon: 'icon-tool-ultrasound-bidirectional', + label: 'Ultrasound Directional', + tooltip: 'Ultrasound Directional', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstoneTool', { - commandName: 'openDICOMTagViewer', - commandOptions: {}, - context: 'DEFAULT', + name: 'evaluate.modality.supported', + supportedModalities: ['US'], }, ], - 'Dicom Tag Browser' - ), + }), + createButton({ + id: 'WindowLevelRegion', + icon: 'icon-tool-window-region', + label: 'Window Level Region', + tooltip: 'Window Level Region', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), ], }, }, From 6fdd2edea7b016983589711f65d488aa26a48677 Mon Sep 17 00:00:00 2001 From: Sachidanand Alle Date: Fri, 22 Nov 2024 10:46:41 +0000 Subject: [PATCH 07/11] Add Active Learning Section Signed-off-by: Sachidanand Alle --- .../src/components/ModelSelector.tsx | 2 +- .../src/components/MonaiLabelPanel.tsx | 13 + .../src/components/actions/ActiveLearning.tsx | 272 ++++++++++-------- .../components/actions/AutoSegmentation.tsx | 2 +- .../src/components/actions/ClassPrompts.tsx | 2 +- .../src/components/actions/NextSampleForm.css | 3 +- .../src/components/actions/NextSampleForm.tsx | 2 +- .../src/components/actions/OptionTable.tsx | 2 +- .../src/components/actions/OptionsForm.tsx | 4 +- .../src/components/actions/PointPrompts.tsx | 2 +- 10 files changed, 173 insertions(+), 131 deletions(-) diff --git a/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.tsx b/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.tsx index 9b57544a5..49ed04ccd 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/ModelSelector.tsx @@ -82,7 +82,7 @@ export default class ModelSelector extends Component {
 

- - this.onClickRecoverSeg()} - > - Recover Seg - - + + + + + + + + + + + + + + + + + + + + + + + +
Strategy: + +
 
Annotated: +
+
+ {activelearning} +
+
+
Training: +
+
+ {training} +
+
+
Train Acc: +
+
+ {accuracy} +
+
+
); diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx index 0af6e57a5..d249a8d5f 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx @@ -121,7 +121,7 @@ export default class AutoSegmentation extends BaseTab { name="rd" id={this.tabId} className="tab-switch" - value="segmentation" + defaultValue="segmentation" onClick={this.onSelectActionTab} defaultChecked /> diff --git a/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx b/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx index 086d2f8f7..0601ea76f 100644 --- a/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx +++ b/plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx @@ -319,7 +319,7 @@ export default class ClassPrompts extends BaseTab { name="rd" id={this.tabId} className="tab-switch" - value="segmentation" + defaultValue="segmentation" onClick={this.onSelectActionTab} />