diff --git a/.gitignore b/.gitignore index 59af1f056..69f5d3f66 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,7 @@ junit test-output.xml studies tests/data +yarn.lock +# Packages +node_modules +yarn.lock diff --git a/monailabel/config.py b/monailabel/config.py index 39c5db3a4..e041cacce 100644 --- a/monailabel/config.py +++ b/monailabel/config.py @@ -62,7 +62,7 @@ class Settings(BaseSettings): MONAI_LABEL_DICOMWEB_FETCH_BY_FRAME: bool = False MONAI_LABEL_DICOMWEB_CONVERT_TO_NIFTI: bool = True MONAI_LABEL_DICOMWEB_SEARCH_FILTER: Dict[str, Any] = {"Modality": "CT"} - MONAI_LABEL_DICOMWEB_CACHE_EXPIRY: int = 180 + MONAI_LABEL_DICOMWEB_CACHE_EXPIRY: int = 7200 MONAI_LABEL_DICOMWEB_PROXY_TIMEOUT: float = 30.0 MONAI_LABEL_DICOMWEB_READ_TIMEOUT: float = 5.0 diff --git a/monailabel/datastore/utils/convert.py b/monailabel/datastore/utils/convert.py index 2d6b0ea94..a0403e1d7 100644 --- a/monailabel/datastore/utils/convert.py +++ b/monailabel/datastore/utils/convert.py @@ -60,7 +60,7 @@ def dicom_to_nifti(series_dir, is_seg=False): return output_file -def binary_to_image(reference_image, label, dtype=np.uint16, file_ext=".nii.gz"): +def binary_to_image(reference_image, label, dtype=np.uint8, file_ext=".nii.gz"): start = time.time() image_np, meta_dict = LoadImage(image_only=False)(reference_image) diff --git a/monailabel/endpoints/ohif.py b/monailabel/endpoints/ohif.py index b4d9ad68f..55a4ea597 100644 --- a/monailabel/endpoints/ohif.py +++ b/monailabel/endpoints/ohif.py @@ -38,8 +38,17 @@ def get_ohif(path: str): if not os.path.exists(file): logger.info(file) raise HTTPException(status_code=404, detail="Resource NOT Found") + return FileResponse(file, media_type=get_mime_type(file)) + # headers = { + # "Cross-Origin-Opener-Policy": "same-origin", + # "Cross-Origin-Embedder-Policy": "require-corp", + # "Cross-Origin-Resource-Policy": "same-site", + # } + + # return FileResponse(file, media_type=get_mime_type(file), headers=headers) + @router.get("/{path:path}", include_in_schema=False) async def api_get_ohif(path: str, user: User = Depends(RBAC(settings.MONAI_LABEL_AUTH_ROLE_USER))): diff --git a/monailabel/endpoints/proxy.py b/monailabel/endpoints/proxy.py index 0dc4709ed..8b2529832 100644 --- a/monailabel/endpoints/proxy.py +++ b/monailabel/endpoints/proxy.py @@ -65,6 +65,9 @@ async def proxy_dicom(op: str, path: str, response: Response): ) # some version of ohif requests metadata using qido so change it to wado + print( + f"This is the server {server} - This is the op {op} - This is the prefix {prefix} - This is the path {path}" + ) if path.endswith("metadata") and op == "qido": prefix = settings.MONAI_LABEL_WADO_PREFIX @@ -78,10 +81,16 @@ async def proxy_dicom(op: str, path: str, response: Response): settings.MONAI_LABEL_DICOMWEB_PROXY_TIMEOUT, read=settings.MONAI_LABEL_DICOMWEB_READ_TIMEOUT, ) + + print(f"This is the proxy path: {proxy_path}") proxy = await client.get(proxy_path, timeout=timeout) response.body = proxy.content response.status_code = proxy.status_code + # response.headers["Cross-Origin-Opener-Policy"] = "same-origin" + # response.headers["Cross-Origin-Embedder-Policy"] = "require-corp" + # response.headers["Cross-Origin-Resource-Policy"] = "same-site" + # response.headers["Cross-Origin-Resource-Policy"] = "cross-origin" return response diff --git a/monailabel/tasks/infer/basic_infer.py b/monailabel/tasks/infer/basic_infer.py index 2c335d353..e0b12eddc 100644 --- a/monailabel/tasks/infer/basic_infer.py +++ b/monailabel/tasks/infer/basic_infer.py @@ -355,6 +355,17 @@ def __call__( "transform": data.get("latencies"), } + # Add Centroids to the result json to consume in OHIF v3 + centroids = data.get("centroids", None) + if centroids is not None: + centroids_dict = dict() + for c in centroids: + all_items = list(c.items()) + centroids_dict[all_items[0][0]] = [str(i) for i in all_items[0][1]] # making it json compatible + result_json["centroids"] = centroids_dict + else: + result_json["centroids"] = dict() + if result_file_name is not None and isinstance(result_file_name, str): logger.info(f"Result File: {result_file_name}") logger.info(f"Result Json Keys: {list(result_json.keys())}") diff --git a/plugins/ohif/monai-label/src/components/MonaiLabelPanel.js b/plugins/ohif/monai-label/src/components/MonaiLabelPanel.js index 6ed5a442e..c17675044 100644 --- a/plugins/ohif/monai-label/src/components/MonaiLabelPanel.js +++ b/plugins/ohif/monai-label/src/components/MonaiLabelPanel.js @@ -39,6 +39,10 @@ export default class MonaiLabelPanel extends Component { super(props); const { viewports, studies, activeIndex } = props; + /* setTimeout(() => { + this.viewConstants = this.getViewConstants(viewports, studies, activeIndex); + }, 2000) */ + this.viewConstants = this.getViewConstants(viewports, studies, activeIndex); console.debug(this.viewConstants); diff --git a/plugins/ohifv3/README.md b/plugins/ohifv3/README.md new file mode 100644 index 000000000..f7b1920d7 --- /dev/null +++ b/plugins/ohifv3/README.md @@ -0,0 +1,149 @@ + + +## MONAI Label Plugin for OHIF Viewer +The Open Health Imaging Foundation (OHIF) Viewer is an open-source, web-based platform for medical imaging. OHIF Viewer provides a framework for building complex imaging applications with user-friendly interfaces. MONAI Label supports the web-based OHIF viewer with connectivity to a remote DICOM server via DICOMweb. + + + +### Table of Contents +- [Supported Applications](#supported-applications) +- [Installing OHIF](#installing-ohif) +- [Installing Orthanc](#installing-orthanc-dicomweb) +- [Converting NIFTI Images to DICOM](#converting-nifti-images-to-dicom) +- [Converting NIFTI Annotations to DICOM-SEG](#converting-nifti-annotations-to-dicom-seg) +- [Uploading DICOM to Orthanc](#uploading-dicom-to-orthanc) + +### Supported Applications +Supported applications can be found in the [sample-apps](../../sample-apps/radiology/) folder under the radiology section. These applications include models like DeepEdit, DeepGrow, Segmentation, and more, which can be used to create and refine labels for various medical imaging tasks. + +### Installing OHIF +When installing MONAI Label with `pip install monailabel`, a version of OHIF that is pre-built with the MONAI Label library is automatically installed. OHIF will be accessible at http://127.0.0.1:8000/ohif/ when you start the MONAI Label server and connect to local/remote DICOM-web storage. + +#### Development setup + +To build the OHIF plugin for development, follow these steps: + ```bash + sudo sh requirements.sh # installs yarn + sh build.sh + ``` + +To run Orthanc from the submodule and avoid building the OHIF package for every code change, use the following commands: +```bash +cd plugins/ohif/Viewers + +yarn run dev:orthanc + +# OHIF will run at http://127.0.0.1:3000/ +``` +You can then visit http://127.0.0.1:3000/ on your browser to see the running OHIF. + +### Installing Orthanc (DICOMWeb) + +#### Ubuntu 20.x + +```bash +# Install Orthanc and DICOMweb plugin +sudo apt-get install orthanc orthanc-dicomweb -y + +# Install Plastimatch +sudo apt-get install plastimatch -y +``` + +However, you must upgrade to the latest version by following the steps mentioned on the [Orthanc Installation Guide](https://book.orthanc-server.com/users/debian-packages.html#replacing-the-package-from-the-service-by-the-lsb-binaries) + +```bash +sudo service orthanc stop +sudo wget https://lsb.orthanc-server.com/orthanc/1.9.7/Orthanc --output-document /usr/sbin/Orthanc +sudo rm -f /usr/share/orthanc/plugins/*.so + +sudo wget https://lsb.orthanc-server.com/orthanc/1.9.7/libServeFolders.so --output-document /usr/share/orthanc/plugins/libServeFolders.so +sudo wget https://lsb.orthanc-server.com/orthanc/1.9.7/libModalityWorklists.so --output-document /usr/share/orthanc/plugins/libModalityWorklists.so +sudo wget https://lsb.orthanc-server.com/plugin-dicom-web/1.6/libOrthancDicomWeb.so --output-document /usr/share/orthanc/plugins/libOrthancDicomWeb.so + +sudo service orthanc restart +``` + +#### Windows/Others _(latest version)_ + +- Download and Install Orthanc from https://www.orthanc-server.com/download.php + +### Converting NIFTI images to DICOM +To use Orthanc, your files need to be in DICOM format. You can convert any Nifti files to DICOM using the following command: +```bash +plastimatch convert --patient-id patient1 --input image.nii.gz --output-dicom test +``` +- `plastimatch convert`: This is the command to run the conversion tool. +- `--patient-id patient1`: This option specifies the ID of the patient to be associated with the DICOM files. In this example, it is set to `patient1`. +- `--input image.nii.gz`: This option specifies the path to the input Nifti file that you want to convert to DICOM. In this example, the input file is `image.nii.gz`. +- `--output-dicom test`: This option specifies the path to the output folder where the DICOM files will be saved. In this example, the output folder is named `test`. + +When you run this command, plastimatch will read the input Nifti file and convert it to a series of DICOM files with the specified patient ID. These files will be saved in the test folder in DICOM format, which can be uploaded to an Orthanc server. + +### Converting NIFTI Annotations to DICOM-SEG + +If you want to upload image annotations to the DICOMWeb server (such as Orthanc), you should use DICOM-SEG format. DICOM-SEG is a DICOM standard format for image segmentation. To convert NIFTI annotations to DICOM-SEG, you can use the itkimage2segimage tool from ITK (Insight Toolkit). + +Here's how to use itkimage2segimage to convert NIFTI annotations to DICOM-SEG: + +1. Install ITK: You can download and install ITK from their website, or use a package manager such as pip or conda to install it. +2. Convert NIFTI annotations to NIFTI-Segmentations: Use the `plastimatch convert` command to convert the NIFTI annotations to NIFTI-Segmentations format. This format is required as an input to `itkimage2segimage`. The command is similar to the one used to convert NIFTI images to DICOM, but with an additional option: + +```bash +plastimatch convert --input annotations.nii.gz --output-nifti-seg annotations-seg.nii.gz +``` +This command will convert the NIFTI annotations file named `annotations.nii.gz` to NIFTI-Segmentations format and save the result in a file named `annotations-seg.nii.gz`. + +3. Convert NIFTI-Segmentations to DICOM-SEG: Use the `itkimage2segimage` command to convert the NIFTI-Segmentations file to DICOM-SEG format. Here's an example command: + +```bash +itkimage2segimage --inputImage annotations-seg.nii.gz --outputDICOM annotations-seg.dcm --patientID patient1 +``` +This command will convert the `annotations-seg.nii.gz` file to DICOM-SEG format and save the result in a file named `annotations-seg.dcm`. The `--patientID` option specifies the ID of the patient to be associated with the DICOM-SEG file. + +After conversion, you can upload the DICOM-SEG file to the DICOMWeb server such as Orthanc. + + +### Uploading DICOM to Orthanc + +#### Use Orthanc Browser +You can use the Orthanc browser located at http://127.0.0.1:8042/app/explorer.html#upload to upload files. + +#### Using STORE SCP/SCU +To upload files using STORE SCP/SCU, you need to enable AET by editing the `orthanc.json` file: +`sudo vim /etc/orthanc/orthanc.json` + +```json5 +// The list of the known DICOM modalities +"DicomModalities" : { +/** + * Uncommenting the following line would enable Orthanc to + * connect to an instance of the "storescp" open-source DICOM + * store (shipped in the DCMTK distribution) started by the + * command line "storescp 2000". + **/ +"sample": ["MONAILABEL", "127.0.0.1", 104] +``` +Then, restart Orthanc: + +``` +sudo service orthanc restart +``` + +#### Upload Files +To upload files, use the following command: + +```bash +python -m pynetdicom storescu 127.0.0.1 4242 test -aet MONAILABEL -r +``` +That's it! With these steps, you should have successfully installed MONAI Label with the OHIF viewer plugin and connected it to a remote DICOM server via DICOMweb. diff --git a/plugins/ohifv3/build.sh b/plugins/ohifv3/build.sh new file mode 100755 index 000000000..a47134569 --- /dev/null +++ b/plugins/ohifv3/build.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# 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. + +curr_dir="$(pwd)" +my_dir="$(dirname "$(readlink -f "$0")")" + +echo "Installing requirements..." +sh $my_dir/requirements.sh + +install_dir=${1:-$my_dir/../../monailabel/endpoints/static/ohif} + +echo "Current Dir: ${curr_dir}" +echo "My Dir: ${my_dir}" +echo "Installing OHIF at: ${install_dir}" + +cd ${my_dir} +rm -rf Viewers +git clone https://github.com/OHIF/Viewers.git +cd Viewers +git checkout feat/monai-label + + +sed -i "s|routerBasename: '/'|routerBasename: '/ohif/'|g" ./platform/app/public/config/default.js +sed -i "s|name: 'aws'|name: 'Orthanc'|g" ./platform/app/public/config/default.js +sed -i "s|wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb'|wadoUriRoot: 'http://localhost/dicom-web'|g" ./platform/app/public/config/default.js +sed -i "s|wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb'|wadoRoot: 'http://localhost/dicom-web'|g" ./platform/app/public/config/default.js +sed -i "s|qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb'|qidoRoot: 'http://localhost/dicom-web'|g" ./platform/app/public/config/default.js + +sed -i "s|PUBLIC_URL=/|PUBLIC_URL=/ohif/|g" ./platform/app/.env + + +yarn install + +# Link the mode and extension HERE +echo "Linking extension and mode at: $(pwd)" +yarn run cli link-extension ../extension-monai-label +yarn run cli link-mode ../mode-monai-label + +cd ../extension-monai-label + +echo "Running install again at: $(pwd)" + +yarn install + +echo "Moving nrrd-js and itk node modules to Viewersnode_modules/" + +cp -r ./node_modules/nrrd-js ../Viewers/node_modules/ + +cp -r ./node_modules/itk ../Viewers/node_modules/ + +echo "Moving to Viewers folder to build OHIF" + +cd ../Viewers + +echo "Viewers folder before building OHIF $(pwd)" + +QUICK_BUILD=true yarn run build + +rm -rf ${install_dir} +mv ./platform/app/dist/ ${install_dir} +echo "Copied OHIF to ${install_dir}" + +rm -rf ../Viewers + +cd ${curr_dir} diff --git a/plugins/ohifv3/extension-monai-label/.gitignore b/plugins/ohifv3/extension-monai-label/.gitignore new file mode 100644 index 000000000..67045665d --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/plugins/ohifv3/extension-monai-label/.prettierrc b/plugins/ohifv3/extension-monai-label/.prettierrc new file mode 100644 index 000000000..b80ec6b34 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "es5", + "printWidth": 80, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/plugins/ohifv3/extension-monai-label/.webpack/webpack.prod.js b/plugins/ohifv3/extension-monai-label/.webpack/webpack.prod.js new file mode 100644 index 000000000..070a723e3 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/.webpack/webpack.prod.js @@ -0,0 +1,63 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); + +// Todo: add ESM build for the extension 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', + }, + }, + ], + module: { + rules: [ + { + test: /(\.jsx|\.js|\.tsx|\.ts)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], + }, +}; + +module.exports = config; diff --git a/plugins/ohifv3/extension-monai-label/LICENSE b/plugins/ohifv3/extension-monai-label/LICENSE new file mode 100644 index 000000000..f0f1d2279 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 extension-monai-label (adiazpinto@nvidia.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/ohifv3/extension-monai-label/README.md b/plugins/ohifv3/extension-monai-label/README.md new file mode 100644 index 000000000..a8b628c56 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/README.md @@ -0,0 +1,7 @@ +# extension-monai-label +## Description +OHIFv3 extension for MONAI Label +## Author +OHIF,NVIDIA,KCL +## License +MIT diff --git a/plugins/ohifv3/extension-monai-label/babel.config.js b/plugins/ohifv3/extension-monai-label/babel.config.js new file mode 100644 index 000000000..371f77fcf --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/babel.config.js @@ -0,0 +1,66 @@ +// https://babeljs.io/docs/en/options#babelrcroots +const { extendDefaultPlugins } = require('svgo'); + +module.exports = { + plugins: [ + [ + 'inline-react-svg', + { + svgo: { + plugins: extendDefaultPlugins([ + { + name: 'removeViewBox', + active: false, + }, + ]), + }, + }, + ], + ['@babel/plugin-proposal-class-properties', { loose: true }], + '@babel/plugin-transform-typescript', + ['@babel/plugin-proposal-private-property-in-object', { loose: true }], + ['@babel/plugin-proposal-private-methods', { loose: true }], + ], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + ], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + '@babel/plugin-transform-typescript', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: ['react-hot-loader/babel'], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/plugins/ohifv3/extension-monai-label/package.json b/plugins/ohifv3/extension-monai-label/package.json new file mode 100644 index 000000000..6b82ff88c --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/package.json @@ -0,0 +1,88 @@ +{ + "name": "@ohif/extension-monai-label", + "version": "0.0.1", + "description": "OHIFv3 extension for MONAI Label", + "author": "OHIF,NVIDIA,KCL", + "license": "MIT", + "main": "dist/umd/extension-monai-label/index.umd.js", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-extension" + ], + "module": "src/index.tsx", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "scripts": { + "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" + }, + "peerDependencies": { + "@ohif/core": "^3.0.0", + "@ohif/extension-default": "^3.0.0", + "@ohif/extension-cornerstone": "^3.0.0", + "@ohif/i18n": "^1.0.0", + "prop-types": "^15.6.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-i18next": "^12.2.2", + "react-router": "^6.8.1", + "react-router-dom": "^6.8.1", + "webpack": "^5.50.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "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", + "itk": "^14.1.1", + "react-color": "^2.19.3", + "bootstrap": "^5.0.2", + "react-select": "^4.3.1", + "chroma-js": "^2.1.2" + }, + "devDependencies": { + "@babel/core": "^7.21.4", + "@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-typescript": "^7.13.0", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "babel-eslint": "9.x", + "babel-loader": "^8.2.4", + "babel-plugin-inline-react-svg": "^2.0.2", + "babel-plugin-module-resolver": "^5.0.0", + "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-loader": "^2.0.0", + "webpack": "^5.50.0", + "webpack-merge": "^5.7.3", + "webpack-cli": "^4.7.2" + } +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/ModelSelector.styl b/plugins/ohifv3/extension-monai-label/src/components/ModelSelector.styl new file mode 100644 index 000000000..ec1dbeccc --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/ModelSelector.styl @@ -0,0 +1,33 @@ + +.modelSelector + .table + border-collapse: collapse; + border: 0 solid; + width: 100%; + + .table tr td + border: 0 solid; + + .selectBox + width: 100% + + .actionButton + border: 2px solid black + border-radius: 15px + background-color: lightblue + color: var(--ui-gray-dark) + line-height: 25px + padding: 0 15px + outline: none + cursor: pointer + + &:hover, &:active + background-color: var(--ui-sky-blue) + + &:disabled + background-color: var(--ui-sky-blue) + + svg + margin-right: 4px + position: relative + top: 2px diff --git a/plugins/ohifv3/extension-monai-label/src/components/ModelSelector.tsx b/plugins/ohifv3/extension-monai-label/src/components/ModelSelector.tsx new file mode 100644 index 000000000..e9c582a6a --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/ModelSelector.tsx @@ -0,0 +1,116 @@ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './ModelSelector.styl'; + +export default class ModelSelector extends Component { + static propTypes = { + name: PropTypes.string, + title: PropTypes.string, + models: PropTypes.array, + currentModel: PropTypes.string, + usage: PropTypes.any, + onClick: PropTypes.func, + onSelectModel: PropTypes.func, + scribblesSelector: PropTypes.any, + }; + + constructor(props) { + super(props); + + const currentModel = props.currentModel + ? props.currentModel + : props.models.length > 0 + ? props.models[0] + : ''; + this.state = { + models: props.models, + currentModel: currentModel, + buttonDisabled: false, + }; + } + + static getDerivedStateFromProps(props, current_state) { + if (current_state.models !== props.models) { + return { + models: props.models, + currentModel: props.models.length > 0 ? props.models[0] : '', + }; + } + return null; + } + + onChangeModel = evt => { + this.setState({ currentModel: evt.target.value }); + if (this.props.onSelectModel) this.props.onSelectModel(evt.target.value); + }; + + currentModel = () => { + return this.props.currentModel + ? this.props.currentModel + : this.state.currentModel; + }; + + onClickBtn = async () => { + if (this.state.buttonDisabled) { + return; + } + + let model = this.state.currentModel; + if (!model) { + console.error('This should never happen!'); + model = this.props.models.length > 0 ? this.props.models[0] : ''; + } + + this.setState({ buttonDisabled: true }); + await this.props.onClick(model); + this.setState({ buttonDisabled: false }); + }; + + render() { + const currentModel = this.currentModel(); + return ( +
+ + + + + + + + + + + {this.props.scribblesSelector} + +
Models:
+ +   + +
+ {this.props.usage} +
+ ); + } +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/MonaiLabelPanel.styl b/plugins/ohifv3/extension-monai-label/src/components/MonaiLabelPanel.styl new file mode 100644 index 000000000..5069ab8dc --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/MonaiLabelPanel.styl @@ -0,0 +1,146 @@ + +@import url("https://www.w3schools.com/w3css/4/w3.css") + +.monaiLabelPanel + background-color: lightslategray + height: 100% + width: 100% + display: flex + flex-direction: column + color: var(--text-primary-color) + padding: 2px + overflow-y: scroll; /* Make the panel scrollable vertically */ + + .subtitle + font-size: 14px + text-decoration: underline + font-weight 500 + color black + margin 1px + text-align center + + /* Accordion styles */ + .tabs { + border-radius: 4px; + overflow: auto; + box-shadow: 0 4px 4px -2px rgba(0, 0, 0, 0.5); + background: black + margin: 1rem 0; + } + + .tab { + width: 100%; + color: white; + overflow: hidden; + } + .tab-switch { + position: absolute; + opacity: 0; + z-index: -1; + } + .tab-label { + display: flex; + justify-content: space-between; + padding: 0.4em; + background: #16202b; + border-right: 1px dotted #3c5d80; + color: #fff; + font-size: 12px; + font-weight: normal; + cursor: pointer; + /* Icon */ + } + .tab-label:hover { + background: #3e5975; + } + .tab-label::after { + content: "❯"; + width: 1em; + height: 1em; + text-align: center; + transition: all 0.35s; + } + .tab-content { + max-height: 0; + padding: 0 1em; + background: gray; + transition: all 0.35s; + width: 100%; + font-size: small + color: black + } + .tab-close { + display: flex; + justify-content: flex-end; + padding: 1em; + font-size: 0.75em; + background: #2c3e50; + cursor: pointer; + } + .tab-close:hover { + background: #1a252f; + } + + input:checked + .tab-label { + background: black; + } + input:checked + .tab-label::after { + transform: rotate(90deg); + } + input:checked ~ .tab-content { + max-height: 100vh; + padding: 1em; + } + + .separator + border: 0.01em solid #44626f + width: 100% + margin-top: 3px + margin-bottom: 3px + + .actionInput + width: 100% + padding: 1px + border: 1px solid black + color: black + + // Theming has changed: https://docs.ohif.org/platform/themeing/ + .actionButton + // https://github.com/OHIF/Viewers/blob/58d38495f097afc6333937b6fbaf60ae473957c0/platform/docs/versioned_docs/version-2.0-deprecated/viewer/themeing.md?plain=1#L5 + border: 1px solid black + border-radius: 15px + background-color: lightblue + color: black + line-height: 25px + padding: 10px + outline: none + cursor: pointer + + &:hover, &:active + background-color: var(--ui-sky-blue) + + &:disabled + background-color: var(--ui-sky-blue) + + svg + margin-right: 4px + position: relative + top: 2px + +.scrollbar { + overflow-y: scroll; +} + +#style-3::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + background-color: #000000; +} + +#style-3::-webkit-scrollbar { + width: 6px; + background-color: #000000; +} + +#style-3::-webkit-scrollbar-thumb { + background-color: #f5f5f5; +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/MonaiLabelPanel.tsx b/plugins/ohifv3/extension-monai-label/src/components/MonaiLabelPanel.tsx new file mode 100644 index 000000000..19a407f13 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/MonaiLabelPanel.tsx @@ -0,0 +1,403 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { cache, triggerEvent, eventTarget } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/tools'; +import './MonaiLabelPanel.styl'; +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 MonaiLabelClient from '../services/MonaiLabelClient'; +import SegmentationReader from '../utils/SegmentationReader'; +import MonaiSegmentation from './MonaiSegmentation'; +import SegmentationToolbox from './SegmentationToolbox'; + +export default class MonaiLabelPanel extends Component { + static propTypes = { + commandsManager: PropTypes.any, + servicesManager: PropTypes.any, + extensionManager: PropTypes.any, + }; + + notification: any; + settings: any; + state: { info: {}; action: {} }; + actions: { + options: any; + activelearning: any; + segmentation: any; + smartedit: any; + }; + props: any; + SeriesInstanceUID: any; + StudyInstanceUID: any; + + constructor(props) { + super(props); + + const { uiNotificationService, viewportGridService, displaySetService } = + props.servicesManager.services; + + this.notification = uiNotificationService; + this.settings = React.createRef(); + + this.actions = { + options: React.createRef(), + activeLearning: React.createRef(), + segmentation: React.createRef(), + smartedit: React.createRef(), + }; + + this.state = { + info: {}, + 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); + } + + 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) { + return; + } + + this.setState({ segmentations }); + }); + subscriptions.push(unsubscribe); + }); + + this.unsubscribe = () => { + subscriptions.forEach((unsubscribe) => unsubscribe()); + }; + } + + // componentDidUnmount? Doesn't exist this method anymore in V3? + async componentWillUnmount() { + this.unsubscribe(); + } + + client = () => { + const settings = + this.settings && this.settings.current && this.settings.current.state + ? this.settings.current.state + : null; + return new MonaiLabelClient( + settings ? settings.url : 'http://127.0.0.1:8000' + ); + }; + + onInfo = async () => { + this.notification.show({ + title: 'MONAI Label', + message: 'Connecting to MONAI Label', + type: 'info', + duration: 3000, + }); + + const response = await this.client().info(); + + // 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 + }); + + if (response.status !== 200) { + this.notification.show({ + title: 'MONAI Label', + message: 'Failed to Connect to MONAI Label Server', + type: 'error', + duration: 5000, + }); + } else { + this.notification.show({ + title: 'MONAI Label', + message: 'Connected to MONAI Label Server - Successful', + type: 'success', + duration: 2000, + }); + + this.setState({ info: response.data }); + } + }; + + onSelectActionTab = (name) => { + // Leave Event + for (const action of Object.keys(this.actions)) { + if (this.state.action === action) { + if (this.actions[action].current) + this.actions[action].current.onLeaveActionTab(); + } + } + + // Enter Event + for (const action of Object.keys(this.actions)) { + if (name === action) { + if (this.actions[action].current) + this.actions[action].current.onEnterActionTab(); + } + } + 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; + } + + + 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); + + // 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 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, + }, + ]; + + // Todo: rename volumeId + const volumeLoadObject = cache.getVolume('1'); + if (volumeLoadObject) { + const { scalarData } = volumeLoadObject; + scalarData.set(data); + triggerEvent(eventTarget, Enums.Events.SEGMENTATION_DATA_MODIFIED, { + segmentationId: '1', + }); + console.debug("updated the segmentation's scalar data"); + } else { + this.props.commandsManager.runCommand('hydrateSegmentationsForViewport', { + segmentations, + }); + } + }; + + _debug = async () => { + let 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 }; + }; + + updateView = async (response, labelNames) => { + const { data, centroids } = this.parseResponse(response); + this._update({ data, centroids }, labelNames); + }; + + render() { + return ( +
+
+ + + +
+ +

{this.state.info.name}

+ + {/* */} +
+ + + + + + +
+ + {this.state.segmentations?.map((segmentation) => ( + <> + + + + ))} +
+ ); + } +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/MonaiSegmentation.tsx b/plugins/ohifv3/extension-monai-label/src/components/MonaiSegmentation.tsx new file mode 100644 index 000000000..d24d198d6 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/MonaiSegmentation.tsx @@ -0,0 +1,316 @@ +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 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/extension-monai-label/src/components/SegmentationToolbox.tsx b/plugins/ohifv3/extension-monai-label/src/components/SegmentationToolbox.tsx new file mode 100644 index 000000000..4577a56b7 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/SegmentationToolbox.tsx @@ -0,0 +1,368 @@ +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/extension-monai-label/src/components/SettingsTable.styl b/plugins/ohifv3/extension-monai-label/src/components/SettingsTable.styl new file mode 100644 index 000000000..9416781ea --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/SettingsTable.styl @@ -0,0 +1,53 @@ + +.settingsTable + border-collapse: collapse; + border: 0 solid; + width: 100%; + font-size: small + +.settingsTable tr td + border: 0 solid; + +.settings_active, +.settings_header { + background-color: black; + color: white; + cursor: pointer; + padding: 5px; + width: 100%; + border: none; + text-align: left; + outline: none; +} + +.settings_active { + background-color: #403f3d; +} + +.settings_header:hover { + background-color: #2b2a28; +} + +.settings_header:after { + content: '\002B'; + color: white; + font-weight: bold; + float: right; + margin-left: 5px; +} + +.settings_active:after { + content: '\2212'; + color: white; + font-weight: bold; + float: right; + margin-left: 5px; +} + +.settings_content { + cursor: pointer; + color: #fff; + background-color: #2b2a28; + padding: 10px + font-size: smaller +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/SettingsTable.tsx b/plugins/ohifv3/extension-monai-label/src/components/SettingsTable.tsx new file mode 100644 index 000000000..967c0baf9 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/SettingsTable.tsx @@ -0,0 +1,90 @@ +import React, { Component } from 'react'; + +import './MonaiLabelPanel.styl'; +import { Icon } from '@ohif/ui'; +import { CookieUtils } from '../utils/GenericUtils'; + +export default class SettingsTable extends Component { + constructor(props) { + super(props); + + const onInfo = props.onInfo + this.onInfo = onInfo + + this.state = this.getSettings(); + } + + getSettings = () => { + const url = CookieUtils.getCookieString( + 'MONAILABEL_SERVER_URL', + 'http://' + window.location.host.split(':')[0] + ':8000/' + ); + const overlap_segments = CookieUtils.getCookieBool( + 'MONAILABEL_OVERLAP_SEGMENTS', + true + ); + const export_format = CookieUtils.getCookieString( + 'MONAILABEL_EXPORT_FORMAT', + 'NRRD' + ); + + return { + url: url, + overlap_segments: overlap_segments, + export_format: export_format, + }; + }; + + onBlurSeverURL = evt => { + let url = evt.target.value; + this.setState({ url: url }); + CookieUtils.setCookie('MONAILABEL_SERVER_URL', url); + }; + + render() { + return ( + + + + + + + + + + + + + +
Server: + +   + +
  + + Info + +   |   + + Logs + +
+ ); + } +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/Toolbox/ThresholdSettingsPreset.tsx b/plugins/ohifv3/extension-monai-label/src/components/Toolbox/ThresholdSettingsPreset.tsx new file mode 100644 index 000000000..f708233c3 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/Toolbox/ThresholdSettingsPreset.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { InputDoubleRange, Select } from '@ohif/ui'; + +const defaultOptions = [ + { + value: 'Soft tissue', + label: 'Soft tissue', + range: [-160, 240] as [number, number], + }, + { + value: 'Lung', + label: 'Lung', + range: [-1350, 150] as [number, number], + }, + { + value: 'Liver', + label: 'Liver', + range: [15, 165] as [number, number], + }, + { + value: 'Bone', + label: 'Bone', + range: [-770, 1730] as [number, number], + }, + { + value: 'Brain', + label: 'Brain', + range: [0, 80] as [number, number], + }, +]; + +function ThresholdSettings({ onRangeChange }) { + const [options, setOptions] = useState(defaultOptions); + const [selectedPreset, setSelectedPreset] = useState(defaultOptions[0].value); + + const handleRangeChange = newRange => { + const selectedOption = options.find(o => o.value === selectedPreset); + + if (newRange[0] === selectedOption.range[0] && newRange[1] === selectedOption.range[1]) { + return; + } + + onRangeChange(newRange); + + const updatedOptions = options.map(o => { + if (o.value === selectedPreset) { + return { + ...o, + range: newRange, + }; + } + return o; + }); + + setOptions(updatedOptions); + }; + + const selectedPresetRange = options.find(ds => ds.value === selectedPreset).range; + + return ( +
+
+
Threshold
+
+ + +
+ + + + + + + + + +
+ + + +   + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Strategy: + +
 
Annotated: +
+
+ {activelearning} +
+
+
Training: +
+
+ {training} +
+
+
Train Acc: +
+
+ {accuracy} +
+
+
+
+
+ ); + } +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/AutoSegmentation.styl b/plugins/ohifv3/extension-monai-label/src/components/actions/AutoSegmentation.styl new file mode 100644 index 000000000..b0bf58765 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/AutoSegmentation.styl @@ -0,0 +1,14 @@ +/* +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 "BaseTab.styl" diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/AutoSegmentation.tsx b/plugins/ohifv3/extension-monai-label/src/components/actions/AutoSegmentation.tsx new file mode 100644 index 000000000..100777113 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/AutoSegmentation.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import './AutoSegmentation.styl'; +import ModelSelector from '../ModelSelector'; +import BaseTab from './BaseTab'; + +export default class AutoSegmentation extends BaseTab { + constructor(props) { + super(props); + + this.modelSelector = React.createRef(); + this.state = { + currentModel: null, + }; + } + + onSelectModel = model => { + this.setState({ currentModel: model }); + }; + + onSegmentation = async () => { + const nid = this.notification.show({ + title: 'MONAI Label', + message: 'Running Auto-Segmentation...', + type: 'info', + duration: 60000, + }); + + // 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 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); + } + + if (response.status !== 200) { + this.notification.show({ + title: 'MONAI Label', + message: 'Failed to Run Segmentation', + type: 'error', + duration: 5000, + }); + } else { + this.notification.show({ + title: 'MONAI Label', + message: 'Run Segmentation - Successful', + type: 'success', + duration: 2000, + }); + + await this.props.updateView(response, labels); + } + }; + + render() { + let models = []; + if (this.props.info && this.props.info.models) { + for (let [name, model] of Object.entries(this.props.info.models)) { + if (model.type === 'segmentation') { + models.push(name); + } + } + } + + return ( +
+ + +
+ + Fully automated segmentation without any user prompt. Just + select a model and click to run +

+ } + /> +
+
+ ); + } +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/BaseTab.styl b/plugins/ohifv3/extension-monai-label/src/components/actions/BaseTab.styl new file mode 100644 index 000000000..bbbd02694 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/BaseTab.styl @@ -0,0 +1,14 @@ +/* +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 "../ModelSelector.styl" diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/BaseTab.tsx b/plugins/ohifv3/extension-monai-label/src/components/actions/BaseTab.tsx new file mode 100644 index 000000000..f40f1276e --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/BaseTab.tsx @@ -0,0 +1,39 @@ + +import { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './BaseTab.styl'; +import { UIModalService, UINotificationService } from '@ohif/core'; + +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, + }; + + constructor(props) { + super(props); + this.notification = new UINotificationService(); + this.uiModelService = new UIModalService(); + this.tabId = 'tab-' + this.props.tabIndex; + } + + onSelectActionTab = evt => { + this.props.onSelectActionTab(evt.currentTarget.value); + }; + + onEnterActionTab = () => {}; + onLeaveActionTab = () => {}; + + onSegmentCreated = id => {}; + onSegmentUpdated = id => {}; + onSegmentDeleted = id => {}; + onSegmentSelected = id => {}; + onSelectModel = model => {}; +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/NextSampleForm.styl b/plugins/ohifv3/extension-monai-label/src/components/actions/NextSampleForm.styl new file mode 100644 index 000000000..0dce82fbc --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/NextSampleForm.styl @@ -0,0 +1,54 @@ +/* +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 + + .actionButton + border: 1px solid var(--ui-border-color-active) + border-radius: 15px + background-color: var(--active-color) + color: var(--ui-gray-dark) + line-height: 25px + padding: 0 15px + outline: none + cursor: pointer + + &:hover, &:active + background-color: var(--ui-sky-blue) + + &:disabled + background-color: var(--ui-sky-blue) + + svg + margin-right: 4px + position: relative + top: 2px + + .optionsTable { + font-family: arial, sans-serif; + border-collapse: collapse; + width: 100%; + font-size: smaller; + } + + .optionsTable th { + border: 1px solid #dddddd; + text-align: left; + background-color: lightslategray; + } + + .optionsTable td { + border: 1px solid #dddddd; + text-align: left; + } diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/NextSampleForm.tsx b/plugins/ohifv3/extension-monai-label/src/components/actions/NextSampleForm.tsx new file mode 100644 index 000000000..3112fec5f --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/NextSampleForm.tsx @@ -0,0 +1,95 @@ +/* +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.styl'; +import PropTypes from 'prop-types'; + +export default class NextSampleForm extends Component { + static propTypes = { + info: PropTypes.any, + }; + + constructor(props) { + super(props); + } + + onSubmit = () => { + // TODO:: OHIF Doesn't support loading exact series in URI + let path = window.location.href.split('='); + path[path.length - 1] = this.props.info.StudyInstanceUID; + + const pathname = path.join('='); + console.info(pathname); + + let msg = + 'This action will reload current page. Are you sure to continue?'; + if (!window.confirm(msg)) return; + window.location.href = pathname; + }; + + render() { + const fields = { + id: 'Image ID (MONAILabel)', + Modality: 'Modality', + StudyDate: 'Study Date', + StudyTime: 'Study Time', + PatientID: 'Patient ID', + StudyInstanceUID: 'Study Instance UID', + SeriesInstanceUID: 'Series Instance UID', + }; + return ( +
+ + + + + + + + + {Object.keys(fields).map(field => ( + + + {field === 'SeriesInstanceUID' ? ( + + ) : ( + + )} + + ))} + +
FieldValue
{fields[field]} + + {this.props.info[field]} + + {this.props.info[field]}
+
+
+ +
+
+ ); + } +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/OptionTable.styl b/plugins/ohifv3/extension-monai-label/src/components/actions/OptionTable.styl new file mode 100644 index 000000000..ed32a4c5a --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/OptionTable.styl @@ -0,0 +1,35 @@ +/* +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 "BaseTab.styl" + +.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: lightslategray; +} + +.optionsTable td { + border: 1px solid #070202; + text-align: left; +} + +.optionsInput + width: 100% diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/OptionTable.tsx b/plugins/ohifv3/extension-monai-label/src/components/actions/OptionTable.tsx new file mode 100644 index 000000000..8e41765d7 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/OptionTable.tsx @@ -0,0 +1,218 @@ +/* +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.styl'; +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() { + let 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/extension-monai-label/src/components/actions/SmartEdit.styl b/plugins/ohifv3/extension-monai-label/src/components/actions/SmartEdit.styl new file mode 100644 index 000000000..b0bf58765 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/SmartEdit.styl @@ -0,0 +1,14 @@ +/* +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 "BaseTab.styl" diff --git a/plugins/ohifv3/extension-monai-label/src/components/actions/SmartEdit.tsx b/plugins/ohifv3/extension-monai-label/src/components/actions/SmartEdit.tsx new file mode 100644 index 000000000..d71ae4156 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/actions/SmartEdit.tsx @@ -0,0 +1,326 @@ +/* +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 './SmartEdit.styl'; +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 (let l in labels){ + if (l === segmentId) { + console.log('This is the segmentId') + let p = [] + for (var 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() { + let models = []; + if (this.props.info && this.props.info.models) { + for (let [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/extension-monai-label/src/components/callInputDialog.tsx b/plugins/ohifv3/extension-monai-label/src/components/callInputDialog.tsx new file mode 100644 index 000000000..6de2470d8 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/callInputDialog.tsx @@ -0,0 +1,62 @@ +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/extension-monai-label/src/components/colorPickerDialog.css b/plugins/ohifv3/extension-monai-label/src/components/colorPickerDialog.css new file mode 100644 index 000000000..1c6bb2067 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/colorPickerDialog.css @@ -0,0 +1,3 @@ +.chrome-picker { + background: #090c29 !important; +} diff --git a/plugins/ohifv3/extension-monai-label/src/components/colorPickerDialog.tsx b/plugins/ohifv3/extension-monai-label/src/components/colorPickerDialog.tsx new file mode 100644 index 000000000..38e85efb2 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/components/colorPickerDialog.tsx @@ -0,0 +1,58 @@ +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/extension-monai-label/src/getCommandsModule.ts b/plugins/ohifv3/extension-monai-label/src/getCommandsModule.ts new file mode 100644 index 000000000..a1f4c751a --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/getCommandsModule.ts @@ -0,0 +1,47 @@ +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; + + const actions = { + setToolActive: ({ toolName }) => { + + uiNotificationService.show({ + title: 'MONAI Label probe', + message: + 'MONAI Label Probe Activated.', + type: 'info', + duration: 3000, + }); + }, + }; + + const definitions = { + /* setToolActive: { + commandFn: actions.setToolActive, + }, */ + }; + + return { + actions, + definitions, + defaultContext: 'MONAILabel', + }; +} diff --git a/plugins/ohifv3/extension-monai-label/src/getPanelModule.tsx b/plugins/ohifv3/extension-monai-label/src/getPanelModule.tsx new file mode 100644 index 000000000..bb43be2d8 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/getPanelModule.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import MonaiLabelPanel from './components/MonaiLabelPanel'; + + +function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}) { + + const WrappedMonaiLabelPanel = () => { + return ( + + ); + }; + + return [ + { + name: 'monailabel', + iconName: 'tab-patient-info', + iconLabel: 'MONAI', + label: 'MONAI Label', + secondaryLabel: 'MONAI Label', + component: WrappedMonaiLabelPanel, + }, + ]; +} + +export default getPanelModule; diff --git a/plugins/ohifv3/extension-monai-label/src/id.js b/plugins/ohifv3/extension-monai-label/src/id.js new file mode 100644 index 000000000..ebe5acd98 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/plugins/ohifv3/extension-monai-label/src/index.tsx b/plugins/ohifv3/extension-monai-label/src/index.tsx new file mode 100644 index 000000000..5b3e76f6b --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/index.tsx @@ -0,0 +1,57 @@ +import { id } from './id'; +import getPanelModule from './getPanelModule'; +import getCommandsModule from './getCommandsModule'; +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/extension-monai-label/src/init.ts b/plugins/ohifv3/extension-monai-label/src/init.ts new file mode 100644 index 000000000..58c3afdef --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/init.ts @@ -0,0 +1,15 @@ +import { + addTool, +} from '@cornerstonejs/tools'; +import { Types } from '@ohif/core'; +import ProbeMONAILabelTool from './tools/ProbeMONAILabelTool'; + +/** + * @param {object} configuration + */ +export default function init({ + servicesManager, + configuration = {}, +}: Types.Extensions.ExtensionParams): void { + addTool(ProbeMONAILabelTool); +} diff --git a/plugins/ohifv3/extension-monai-label/src/services/MonaiLabelClient.js b/plugins/ohifv3/extension-monai-label/src/services/MonaiLabelClient.js new file mode 100644 index 000000000..1b2081fcc --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/services/MonaiLabelClient.js @@ -0,0 +1,208 @@ +import axios from 'axios'; + +export default class MonaiLabelClient { + constructor(server_url) { + this.server_url = new URL(server_url); + } + + async info() { + let url = new URL('info/', this.server_url); + 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') { + 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 = url.toString(); + + + if (result_extension) { + params.result_extension = result_extension; + params.result_dtype = 'uint16'; + params.result_compress = false; + } + + // return the indexes as defined in the config file + params.restore_label_idx = false + + return await MonaiLabelClient.api_post( + url, + params, + label, + true, + 'arraybuffer' + ); + } + + async next_sample(stategy = 'random', params = {}) { + const url = new URL( + 'activelearning/' + encodeURIComponent(stategy), + this.server_url + ).toString(); + + return await MonaiLabelClient.api_post(url, params, null, false, 'json'); + } + + async save_label(image, label, params) { + let url = new URL('datastore/label', this.server_url); + url.searchParams.append('image', image); + url = url.toString(); + + /* debugger; */ + + const data = MonaiLabelClient.constructFormDataFromArray( + params, + label, + 'label', + 'label.bin' + ); + + return await MonaiLabelClient.api_put_data(url, data, 'json'); + } + + async is_train_running() { + let url = new URL('train/', this.server_url); + url.searchParams.append('check_if_running', 'true'); + url = url.toString(); + + const response = await MonaiLabelClient.api_get(url); + return ( + response && response.status === 200 && response.data.status === 'RUNNING' + ); + } + + async run_train(params) { + const url = new URL('train/', this.server_url).toString(); + return await MonaiLabelClient.api_post(url, params, null, false, 'json'); + } + + async stop_train() { + const url = new URL('train/', this.server_url).toString(); + return await MonaiLabelClient.api_delete(url); + } + + static constructFormDataFromArray(params, data, name, fileName) { + let formData = new FormData(); + formData.append('params', JSON.stringify(params)); + formData.append(name, data, fileName); + return formData; + } + + static constructFormData(params, files) { + let formData = new FormData(); + formData.append('params', JSON.stringify(params)); + + if (files) { + if (!Array.isArray(files)) { + files = [files]; + } + for (let i = 0; i < files.length; i++) { + formData.append(files[i].name, files[i].data, files[i].fileName); + } + } + return formData; + } + + static constructFormOrJsonData(params, files) { + return files ? MonaiLabelClient.constructFormData(params, files) : params; + } + + static api_get(url) { + console.debug('GET:: ' + url); + return axios + .get(url) + .then(function (response) { + console.debug(response); + return response; + }) + .catch(function (error) { + return error; + }) + .finally(function () {}); + } + + static api_delete(url) { + console.debug('DELETE:: ' + url); + return axios + .delete(url) + .then(function (response) { + console.debug(response); + return response; + }) + .catch(function (error) { + return error; + }) + .finally(function () {}); + } + + static api_post( + url, + params, + files, + form = true, + responseType = 'arraybuffer' + ) { + const data = form + ? MonaiLabelClient.constructFormData(params, files) + : MonaiLabelClient.constructFormOrJsonData(params, files); + return MonaiLabelClient.api_post_data(url, data, responseType); + } + + static api_post_data(url, data, responseType) { + console.debug('POST:: ' + url); + return axios + .post(url, data, { + responseType: responseType, + headers: { + accept: ['application/json', 'multipart/form-data'], + }, + }) + .then(function (response) { + console.debug(response); + return response; + }) + .catch(function (error) { + return error; + }) + .finally(function () {}); + } + + static api_put(url, params, files, form = false, responseType = 'json') { + const data = form + ? MonaiLabelClient.constructFormData(params, files) + : MonaiLabelClient.constructFormOrJsonData(params, files); + return MonaiLabelClient.api_put_data(url, data, responseType); + } + + static api_put_data(url, data, responseType = 'json') { + console.debug('PUT:: ' + url); + return axios + .put(url, data, { + responseType: responseType, + headers: { + accept: ['application/json', 'multipart/form-data'], + }, + }) + .then(function (response) { + console.debug(response); + return response; + }) + .catch(function (error) { + return error; + }); + } +} diff --git a/plugins/ohifv3/extension-monai-label/src/tools/ProbeMONAILabelTool.ts b/plugins/ohifv3/extension-monai-label/src/tools/ProbeMONAILabelTool.ts new file mode 100644 index 000000000..8997271da --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/tools/ProbeMONAILabelTool.ts @@ -0,0 +1,83 @@ +import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core'; +import { ProbeTool, annotation, drawing } from '@cornerstonejs/tools'; + +const { getAnnotations } = annotation.state; + +export default class ProbeMONAILabelTool extends ProbeTool { + static toolName = 'ProbeMONAILabel'; + + constructor( + toolProps = {}, + defaultToolProps = { + configuration: { + customColor: undefined, + }, + } + ) { + super(toolProps, defaultToolProps); + } + + renderAnnotation = (enabledElement, svgDrawingHelper): boolean => { + let renderStatus = false; + const { viewport } = enabledElement; + const { element } = viewport; + + let annotations = getAnnotations(this.getToolName(), element); + + if (!annotations?.length) { + return renderStatus; + } + + annotations = this.filterInteractableAnnotationsForElement( + element, + annotations + ); + + if (!annotations?.length) { + return renderStatus; + } + + const targetId = this.getTargetId(viewport); + const renderingEngine = viewport.getRenderingEngine(); + + const styleSpecifier: StyleSpecifier = { + toolGroupId: this.toolGroupId, + toolName: this.getToolName(), + viewportId: enabledElement.viewport.id, + }; + + for (let i = 0; i < annotations.length; i++) { + const annotation = annotations[i] as ProbeAnnotation; + const annotationUID = annotation.annotationUID; + const data = annotation.data; + const point = data.handles.points[0]; + const canvasCoordinates = viewport.worldToCanvas(point); + + styleSpecifier.annotationUID = annotationUID; + + const color = + this.configuration?.customColor ?? + this.getStyle('color', styleSpecifier, annotation); + + // If rendering engine has been destroyed while rendering + if (!viewport.getRenderingEngine()) { + console.warn('Rendering Engine has been destroyed'); + return renderStatus; + } + + const handleGroupUID = '0'; + + drawing.drawHandles( + svgDrawingHelper, + annotationUID, + handleGroupUID, + [canvasCoordinates], + { color } + ); + + renderStatus = true; + } + + return renderStatus; + }; +} diff --git a/plugins/ohifv3/extension-monai-label/src/utils/GenericAnatomyColors.js b/plugins/ohifv3/extension-monai-label/src/utils/GenericAnatomyColors.js new file mode 100644 index 000000000..6a35cf9d7 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/utils/GenericAnatomyColors.js @@ -0,0 +1,549 @@ + +function componentToHex(c) { + const hex = c.toString(16); + return hex.length === 1 ? '0' + hex : hex; +} + +function rgbToHex(r, g, b) { + return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b); +} + +export const GenericNames = [ + 'james', + 'robert', + 'john', + 'michael', + 'william', + 'david', + 'richard', + 'joseph', + 'thomas', + 'charles', + 'christopher', + 'daniel', + 'matthew', + 'anthony', + 'mark', + 'donald', + 'steven', + 'paul', + 'andrew', + 'joshua', + 'kenneth', + 'kevin', + 'brian', + 'george', + 'edward', + 'ronald', + 'timothy', + 'jason', + 'jeffrey', + 'ryan', + 'jacob', + 'gary', + 'nicholas', + 'eric', + 'jonathan', + 'stephen', + 'larry', + 'justin', + 'scott', + 'brandon', + 'benjamin', + 'samuel', + 'gregory', + 'frank', + 'alexander', + 'raymond', + 'patrick', + 'jack', + 'dennis', + 'jerry', + 'tyler', + 'aaron', + 'jose', + 'adam', + 'henry', + 'nathan', + 'douglas', + 'zachary', + 'peter', + 'kyle', + 'walter', + 'ethan', + 'jeremy', + 'harold', + 'keith', + 'christian', + 'roger', + 'noah', + 'gerald', + 'carl', + 'terry', + 'sean', + 'austin', + 'arthur', + 'lawrence', + 'jesse', + 'dylan', + 'bryan', + 'joe', + 'jordan', + 'billy', + 'bruce', + 'albert', + 'willie', + 'gabriel', + 'logan', + 'alan', + 'juan', + 'wayne', + 'roy', + 'ralph', + 'randy', + 'eugene', + 'vincent', + 'russell', + 'elijah', + 'louis', + 'bobby', + 'philip', + 'johnny', + 'mary', + 'patricia', + 'jennifer', + 'linda', + 'elizabeth', + 'barbara', + 'susan', + 'jessica', + 'sarah', + 'karen', + 'nancy', + 'lisa', + 'betty', + 'margaret', + 'sandra', + 'ashley', + 'kimberly', + 'emily', + 'donna', + 'michelle', + 'dorothy', + 'carol', + 'amanda', + 'melissa', + 'deborah', + 'stephanie', + 'rebecca', + 'sharon', + 'laura', + 'cynthia', + 'kathleen', + 'amy', + 'shirley', + 'angela', + 'helen', + 'anna', + 'brenda', + 'pamela', + 'nicole', + 'emma', + 'samantha', + 'katherine', + 'christine', + 'debra', + 'rachel', + 'catherine', + 'carolyn', + 'janet', + 'ruth', + 'maria', + 'heather', + 'diane', + 'virginia', + 'julie', + 'joyce', + 'victoria', + 'olivia', + 'kelly', + 'christina', + 'lauren', + 'joan', + 'evelyn', + 'judith', + 'megan', + 'cheryl', + 'andrea', + 'hannah', + 'martha', + 'jacqueline', + 'frances', + 'gloria', + 'ann', + 'teresa', + 'kathryn', + 'sara', + 'janice', + 'jean', + 'alice', + 'madison', + 'doris', + 'abigail', + 'julia', + 'judy', + 'grace', + 'denise', + 'amber', + 'marilyn', + 'beverly', + 'danielle', + 'theresa', + 'sophia', + 'marie', + 'diana', + 'brittany', + 'natalie', + 'isabella', + 'charlotte', + 'rose', + 'alexis', + 'kayla', +]; + +export const GenericAnatomyColors = [ + { label: 'background', value: rgbToHex(0, 0, 0) }, + { label: 'tissue', value: rgbToHex(128, 174, 128) }, + { label: 'bone', value: rgbToHex(241, 214, 145) }, + { label: 'skin', value: rgbToHex(177, 122, 101) }, + { label: 'connective tissue', value: rgbToHex(111, 184, 210) }, + { label: 'blood', value: rgbToHex(216, 101, 79) }, + { label: 'organ', value: rgbToHex(221, 130, 101) }, + { label: 'mass', value: rgbToHex(144, 238, 144) }, + { label: 'muscle', value: rgbToHex(192, 104, 88) }, + { label: 'foreign object', value: rgbToHex(220, 245, 20) }, + { label: 'waste', value: rgbToHex(78, 63, 0) }, + { label: 'teeth', value: rgbToHex(255, 250, 220) }, + { label: 'fat', value: rgbToHex(230, 220, 70) }, + { label: 'gray matter', value: rgbToHex(200, 200, 235) }, + { label: 'white matter', value: rgbToHex(250, 250, 210) }, + { label: 'nerve', value: rgbToHex(244, 214, 49) }, + { label: 'vein', value: rgbToHex(0, 151, 206) }, + { label: 'artery', value: rgbToHex(216, 101, 79) }, + { label: 'capillary', value: rgbToHex(183, 156, 220) }, + { label: 'ligament', value: rgbToHex(183, 214, 211) }, + { label: 'tendon', value: rgbToHex(152, 189, 207) }, + { label: 'cartilage', value: rgbToHex(111, 184, 210) }, + { label: 'meniscus', value: rgbToHex(178, 212, 242) }, + { label: 'lymph node', value: rgbToHex(68, 172, 100) }, + { label: 'lymphatic vessel', value: rgbToHex(111, 197, 131) }, + { label: 'cerebro-spinal fluid', value: rgbToHex(85, 188, 255) }, + { label: 'bile', value: rgbToHex(0, 145, 30) }, + { label: 'urine', value: rgbToHex(214, 230, 130) }, + { label: 'feces', value: rgbToHex(78, 63, 0) }, + { label: 'gas', value: rgbToHex(218, 255, 255) }, + { label: 'fluid', value: rgbToHex(170, 250, 250) }, + { label: 'edema', value: rgbToHex(140, 224, 228) }, + { label: 'bleeding', value: rgbToHex(188, 65, 28) }, + { label: 'necrosis', value: rgbToHex(216, 191, 216) }, + { label: 'clot', value: rgbToHex(145, 60, 66) }, + { label: 'embolism', value: rgbToHex(150, 98, 83) }, + { label: 'head', value: rgbToHex(177, 122, 101) }, + { label: 'central nervous system', value: rgbToHex(244, 214, 49) }, + { label: 'brain', value: rgbToHex(250, 250, 225) }, + { label: 'gray matter of brain', value: rgbToHex(200, 200, 215) }, + { label: 'telencephalon', value: rgbToHex(68, 131, 98) }, + { label: 'cerebral cortex', value: rgbToHex(128, 174, 128) }, + { label: 'right frontal lobe', value: rgbToHex(83, 146, 164) }, + { label: 'left frontal lobe', value: rgbToHex(83, 146, 164) }, + { label: 'right temporal lobe', value: rgbToHex(162, 115, 105) }, + { label: 'left temporal lobe', value: rgbToHex(162, 115, 105) }, + { label: 'right parietal lobe', value: rgbToHex(141, 93, 137) }, + { label: 'left parietal lobe', value: rgbToHex(141, 93, 137) }, + { label: 'right occipital lobe', value: rgbToHex(182, 166, 110) }, + { label: 'left occipital lobe', value: rgbToHex(182, 166, 110) }, + { label: 'right insular lobe', value: rgbToHex(188, 135, 166) }, + { label: 'left insular lobe', value: rgbToHex(188, 135, 166) }, + { label: 'right limbic lobe', value: rgbToHex(154, 150, 201) }, + { label: 'left limbic lobe', value: rgbToHex(154, 150, 201) }, + { label: 'right striatum', value: rgbToHex(177, 140, 190) }, + { label: 'left striatum', value: rgbToHex(177, 140, 190) }, + { label: 'right caudate nucleus', value: rgbToHex(30, 111, 85) }, + { label: 'left caudate nucleus', value: rgbToHex(30, 111, 85) }, + { label: 'right putamen', value: rgbToHex(210, 157, 166) }, + { label: 'left putamen', value: rgbToHex(210, 157, 166) }, + { label: 'right pallidum', value: rgbToHex(48, 129, 126) }, + { label: 'left pallidum', value: rgbToHex(48, 129, 126) }, + { label: 'right amygdaloid complex', value: rgbToHex(98, 153, 112) }, + { label: 'left amygdaloid complex', value: rgbToHex(98, 153, 112) }, + { label: 'diencephalon', value: rgbToHex(69, 110, 53) }, + { label: 'thalamus', value: rgbToHex(166, 113, 137) }, + { label: 'right thalamus', value: rgbToHex(122, 101, 38) }, + { label: 'left thalamus', value: rgbToHex(122, 101, 38) }, + { label: 'pineal gland', value: rgbToHex(253, 135, 192) }, + { label: 'midbrain', value: rgbToHex(145, 92, 109) }, + { label: 'substantia nigra', value: rgbToHex(46, 101, 131) }, + { label: 'right substantia nigra', value: rgbToHex(0, 108, 112) }, + { label: 'left substantia nigra', value: rgbToHex(0, 108, 112) }, + { label: 'cerebral white matter', value: rgbToHex(250, 250, 225) }, + { + label: 'right superior longitudinal fasciculus', + value: rgbToHex(127, 150, 88), + }, + { + label: 'left superior longitudinal fasciculus', + value: rgbToHex(127, 150, 88), + }, + { + label: 'right inferior longitudinal fasciculus', + value: rgbToHex(159, 116, 163), + }, + { + label: 'left inferior longitudinal fasciculus', + value: rgbToHex(159, 116, 163), + }, + { label: 'right arcuate fasciculus', value: rgbToHex(125, 102, 154) }, + { label: 'left arcuate fasciculus', value: rgbToHex(125, 102, 154) }, + { label: 'right uncinate fasciculus', value: rgbToHex(106, 174, 155) }, + { label: 'left uncinate fasciculus', value: rgbToHex(106, 174, 155) }, + { label: 'right cingulum bundle', value: rgbToHex(154, 146, 83) }, + { label: 'left cingulum bundle', value: rgbToHex(154, 146, 83) }, + { label: 'projection fibers', value: rgbToHex(126, 126, 55) }, + { label: 'right corticospinal tract', value: rgbToHex(201, 160, 133) }, + { label: 'left corticospinal tract', value: rgbToHex(201, 160, 133) }, + { label: 'right optic radiation', value: rgbToHex(78, 152, 141) }, + { label: 'left optic radiation', value: rgbToHex(78, 152, 141) }, + { label: 'right medial lemniscus', value: rgbToHex(174, 140, 103) }, + { label: 'left medial lemniscus', value: rgbToHex(174, 140, 103) }, + { + label: 'right superior cerebellar peduncle', + value: rgbToHex(139, 126, 177), + }, + { + label: 'left superior cerebellar peduncle', + value: rgbToHex(139, 126, 177), + }, + { label: 'right middle cerebellar peduncle', value: rgbToHex(148, 120, 72) }, + { label: 'left middle cerebellar peduncle', value: rgbToHex(148, 120, 72) }, + { + label: 'right inferior cerebellar peduncle', + value: rgbToHex(186, 135, 135), + }, + { + label: 'left inferior cerebellar peduncle', + value: rgbToHex(186, 135, 135), + }, + { label: 'optic chiasm', value: rgbToHex(99, 106, 24) }, + { label: 'right optic tract', value: rgbToHex(156, 171, 108) }, + { label: 'left optic tract', value: rgbToHex(156, 171, 108) }, + { label: 'right fornix', value: rgbToHex(64, 123, 147) }, + { label: 'left fornix', value: rgbToHex(64, 123, 147) }, + { label: 'commissural fibers', value: rgbToHex(138, 95, 74) }, + { label: 'corpus callosum', value: rgbToHex(97, 113, 158) }, + { label: 'posterior commissure', value: rgbToHex(126, 161, 197) }, + { label: 'cerebellar white matter', value: rgbToHex(194, 195, 164) }, + { label: 'CSF space', value: rgbToHex(85, 188, 255) }, + { label: 'ventricles of brain', value: rgbToHex(88, 106, 215) }, + { label: 'right lateral ventricle', value: rgbToHex(88, 106, 215) }, + { label: 'left lateral ventricle', value: rgbToHex(88, 106, 215) }, + { label: 'right third ventricle', value: rgbToHex(88, 106, 215) }, + { label: 'left third ventricle', value: rgbToHex(88, 106, 215) }, + { label: 'cerebral aqueduct', value: rgbToHex(88, 106, 215) }, + { label: 'fourth ventricle', value: rgbToHex(88, 106, 215) }, + { label: 'subarachnoid space', value: rgbToHex(88, 106, 215) }, + { label: 'spinal cord', value: rgbToHex(244, 214, 49) }, + { label: 'gray matter of spinal cord', value: rgbToHex(200, 200, 215) }, + { label: 'white matter of spinal cord', value: rgbToHex(250, 250, 225) }, + { label: 'endocrine system of brain', value: rgbToHex(82, 174, 128) }, + { label: 'pituitary gland', value: rgbToHex(57, 157, 110) }, + { label: 'adenohypophysis', value: rgbToHex(60, 143, 83) }, + { label: 'neurohypophysis', value: rgbToHex(92, 162, 109) }, + { label: 'meninges', value: rgbToHex(255, 244, 209) }, + { label: 'dura mater', value: rgbToHex(255, 244, 209) }, + { label: 'arachnoid', value: rgbToHex(255, 244, 209) }, + { label: 'pia mater', value: rgbToHex(255, 244, 209) }, + { label: 'muscles of head', value: rgbToHex(201, 121, 77) }, + { label: 'salivary glands', value: rgbToHex(70, 163, 117) }, + { label: 'lips', value: rgbToHex(188, 91, 95) }, + { label: 'nose', value: rgbToHex(177, 122, 101) }, + { label: 'tongue', value: rgbToHex(166, 84, 94) }, + { label: 'soft palate', value: rgbToHex(182, 105, 107) }, + { label: 'right inner ear', value: rgbToHex(229, 147, 118) }, + { label: 'left inner ear', value: rgbToHex(229, 147, 118) }, + { label: 'right external ear', value: rgbToHex(174, 122, 90) }, + { label: 'left external ear', value: rgbToHex(174, 122, 90) }, + { label: 'right middle ear', value: rgbToHex(201, 112, 73) }, + { label: 'left middle ear', value: rgbToHex(201, 112, 73) }, + { label: 'right eyeball', value: rgbToHex(194, 142, 0) }, + { label: 'left eyeball', value: rgbToHex(194, 142, 0) }, + { label: 'skull', value: rgbToHex(241, 213, 144) }, + { label: 'right frontal bone', value: rgbToHex(203, 179, 77) }, + { label: 'left frontal bone', value: rgbToHex(203, 179, 77) }, + { label: 'right parietal bone', value: rgbToHex(229, 204, 109) }, + { label: 'left parietal bone', value: rgbToHex(229, 204, 109) }, + { label: 'right temporal bone', value: rgbToHex(255, 243, 152) }, + { label: 'left temporal bone', value: rgbToHex(255, 243, 152) }, + { label: 'right sphenoid bone', value: rgbToHex(209, 185, 85) }, + { label: 'left sphenoid bone', value: rgbToHex(209, 185, 85) }, + { label: 'right ethmoid bone', value: rgbToHex(248, 223, 131) }, + { label: 'left ethmoid bone', value: rgbToHex(248, 223, 131) }, + { label: 'occipital bone', value: rgbToHex(255, 230, 138) }, + { label: 'maxilla', value: rgbToHex(196, 172, 68) }, + { label: 'right zygomatic bone', value: rgbToHex(255, 255, 167) }, + { label: 'right lacrimal bone', value: rgbToHex(255, 250, 160) }, + { label: 'vomer bone', value: rgbToHex(255, 237, 145) }, + { label: 'right palatine bone', value: rgbToHex(242, 217, 123) }, + { label: 'left palatine bone', value: rgbToHex(242, 217, 123) }, + { label: 'mandible', value: rgbToHex(222, 198, 101) }, + { label: 'neck', value: rgbToHex(177, 122, 101) }, + { label: 'muscles of neck', value: rgbToHex(213, 124, 109) }, + { label: 'pharynx', value: rgbToHex(184, 105, 108) }, + { label: 'larynx', value: rgbToHex(150, 208, 243) }, + { label: 'thyroid gland', value: rgbToHex(62, 162, 114) }, + { label: 'right parathyroid glands', value: rgbToHex(62, 162, 114) }, + { label: 'left parathyroid glands', value: rgbToHex(62, 162, 114) }, + { label: 'skeleton of neck', value: rgbToHex(242, 206, 142) }, + { label: 'hyoid bone', value: rgbToHex(250, 210, 139) }, + { label: 'cervical vertebral column', value: rgbToHex(255, 255, 207) }, + { label: 'thorax', value: rgbToHex(177, 122, 101) }, + { label: 'trachea', value: rgbToHex(182, 228, 255) }, + { label: 'bronchi', value: rgbToHex(175, 216, 244) }, + { 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) }, + { label: 'superior lobe of left lung', value: rgbToHex(172, 138, 115) }, + { label: 'middle lobe of right lung', value: rgbToHex(202, 164, 140) }, + { label: 'inferior lobe of right lung', value: rgbToHex(224, 186, 162) }, + { label: 'inferior lobe of left lung', value: rgbToHex(224, 186, 162) }, + { label: 'pleura', value: rgbToHex(255, 245, 217) }, + { label: 'heart', value: rgbToHex(206, 110, 84) }, + { label: 'right atrium', value: rgbToHex(210, 115, 89) }, + { label: 'left atrium', value: rgbToHex(203, 108, 81) }, + { label: 'atrial septum', value: rgbToHex(233, 138, 112) }, + { label: 'ventricular septum', value: rgbToHex(195, 100, 73) }, + { label: 'right ventricle of heart', value: rgbToHex(181, 85, 57) }, + { label: 'left ventricle of heart', value: rgbToHex(152, 55, 13) }, + { label: 'mitral valve', value: rgbToHex(159, 63, 27) }, + { label: 'tricuspid valve', value: rgbToHex(166, 70, 38) }, + { label: 'aortic valve', value: rgbToHex(218, 123, 97) }, + { label: 'pulmonary valve', value: rgbToHex(225, 130, 104) }, + { label: 'aorta', value: rgbToHex(224, 97, 76) }, + { label: 'pericardium', value: rgbToHex(255, 244, 209) }, + { label: 'pericardial cavity', value: rgbToHex(184, 122, 154) }, + { label: 'esophagus', value: rgbToHex(211, 171, 143) }, + { label: 'thymus', value: rgbToHex(47, 150, 103) }, + { label: 'mediastinum', value: rgbToHex(255, 244, 209) }, + { label: 'skin of thoracic wall', value: rgbToHex(173, 121, 88) }, + { label: 'muscles of thoracic wall', value: rgbToHex(188, 95, 76) }, + { label: 'skeleton of thorax', value: rgbToHex(255, 239, 172) }, + { label: 'thoracic vertebral column', value: rgbToHex(226, 202, 134) }, + { label: 'ribs', value: rgbToHex(253, 232, 158) }, + { label: 'sternum', value: rgbToHex(244, 217, 154) }, + { label: 'right clavicle', value: rgbToHex(205, 179, 108) }, + { label: 'left clavicle', value: rgbToHex(205, 179, 108) }, + { label: 'abdominal cavity', value: rgbToHex(186, 124, 161) }, + { label: 'abdomen', value: rgbToHex(177, 122, 101) }, + { label: 'peritoneum', value: rgbToHex(255, 255, 220) }, + { label: 'omentum', value: rgbToHex(234, 234, 194) }, + { label: 'peritoneal cavity', value: rgbToHex(204, 142, 178) }, + { label: 'retroperitoneal space', value: rgbToHex(180, 119, 153) }, + { label: 'stomach', value: rgbToHex(216, 132, 105) }, + { label: 'duodenum', value: rgbToHex(255, 253, 229) }, + { label: 'small bowel', value: rgbToHex(205, 167, 142) }, + { label: 'colon', value: rgbToHex(204, 168, 143) }, + { label: 'anus', value: rgbToHex(255, 224, 199) }, + { label: 'liver', value: rgbToHex(221, 130, 101) }, + { label: 'biliary tree', value: rgbToHex(0, 145, 30) }, + { label: 'gallbladder', value: rgbToHex(139, 150, 98) }, + { label: 'pancreas', value: rgbToHex(249, 180, 111) }, + { label: 'spleen', value: rgbToHex(157, 108, 162) }, + { label: 'urinary system', value: rgbToHex(203, 136, 116) }, + { label: 'right kidney', value: rgbToHex(185, 102, 83) }, + { label: 'left kidney', value: rgbToHex(185, 102, 83) }, + { label: 'right ureter', value: rgbToHex(247, 182, 164) }, + { label: 'left ureter', value: rgbToHex(247, 182, 164) }, + { label: 'urinary bladder', value: rgbToHex(222, 154, 132) }, + { label: 'urethra', value: rgbToHex(124, 186, 223) }, + { label: 'right adrenal gland', value: rgbToHex(249, 186, 150) }, + { label: 'left adrenal gland', value: rgbToHex(249, 186, 150) }, + { label: 'female internal genitalia', value: rgbToHex(244, 170, 147) }, + { label: 'uterus', value: rgbToHex(255, 181, 158) }, + { label: 'right fallopian tube', value: rgbToHex(255, 190, 165) }, + { label: 'left fallopian tube', value: rgbToHex(227, 153, 130) }, + { label: 'right ovary', value: rgbToHex(213, 141, 113) }, + { label: 'left ovary', value: rgbToHex(213, 141, 113) }, + { label: 'vagina', value: rgbToHex(193, 123, 103) }, + { label: 'male internal genitalia', value: rgbToHex(216, 146, 127) }, + { label: 'prostate', value: rgbToHex(230, 158, 140) }, + { label: 'right seminal vesicle', value: rgbToHex(245, 172, 147) }, + { label: 'left seminal vesicle', value: rgbToHex(245, 172, 147) }, + { label: 'right deferent duct', value: rgbToHex(241, 172, 151) }, + { label: 'left deferent duct', value: rgbToHex(241, 172, 151) }, + { label: 'skin of abdominal wall', value: rgbToHex(177, 124, 92) }, + { label: 'muscles of abdominal wall', value: rgbToHex(171, 85, 68) }, + { label: 'skeleton of abdomen', value: rgbToHex(217, 198, 131) }, + { label: 'lumbar vertebral column', value: rgbToHex(212, 188, 102) }, + { label: 'female external genitalia', value: rgbToHex(185, 135, 134) }, + { label: 'male external genitalia', value: rgbToHex(185, 135, 134) }, + { label: 'skeleton of upper limb', value: rgbToHex(198, 175, 125) }, + { label: 'muscles of upper limb', value: rgbToHex(194, 98, 79) }, + { label: 'right upper limb', value: rgbToHex(177, 122, 101) }, + { label: 'left upper limb', value: rgbToHex(177, 122, 101) }, + { label: 'right shoulder', value: rgbToHex(177, 122, 101) }, + { label: 'left shoulder', value: rgbToHex(177, 122, 101) }, + { label: 'right arm', value: rgbToHex(177, 122, 101) }, + { label: 'left arm', value: rgbToHex(177, 122, 101) }, + { label: 'right elbow', value: rgbToHex(177, 122, 101) }, + { label: 'left elbow', value: rgbToHex(177, 122, 101) }, + { label: 'right forearm', value: rgbToHex(177, 122, 101) }, + { label: 'left forearm', value: rgbToHex(177, 122, 101) }, + { label: 'right wrist', value: rgbToHex(177, 122, 101) }, + { label: 'left wrist', value: rgbToHex(177, 122, 101) }, + { label: 'right hand', value: rgbToHex(177, 122, 101) }, + { label: 'left hand', value: rgbToHex(177, 122, 101) }, + { label: 'skeleton of lower limb', value: rgbToHex(255, 238, 170) }, + { label: 'muscles of lower limb', value: rgbToHex(206, 111, 93) }, + { label: 'right lower limb', value: rgbToHex(177, 122, 101) }, + { label: 'left lower limb', value: rgbToHex(177, 122, 101) }, + { label: 'right hip', value: rgbToHex(177, 122, 101) }, + { label: 'left hip', value: rgbToHex(177, 122, 101) }, + { label: 'right thigh', value: rgbToHex(177, 122, 101) }, + { label: 'left thigh', value: rgbToHex(177, 122, 101) }, + { label: 'right knee', value: rgbToHex(177, 122, 101) }, + { label: 'left knee', value: rgbToHex(177, 122, 101) }, + { label: 'right leg', value: rgbToHex(177, 122, 101) }, + { label: 'left leg', value: rgbToHex(177, 122, 101) }, + { label: 'right foot', value: rgbToHex(177, 122, 101) }, + { label: 'left foot', value: rgbToHex(177, 122, 101) }, + { label: 'peripheral nervous system', value: rgbToHex(216, 186, 0) }, + { label: 'autonomic nerve', value: rgbToHex(255, 226, 77) }, + { label: 'sympathetic trunk', value: rgbToHex(255, 243, 106) }, + { label: 'cranial nerves', value: rgbToHex(255, 234, 92) }, + { label: 'vagus nerve', value: rgbToHex(240, 210, 35) }, + { label: 'peripheral nerve', value: rgbToHex(224, 194, 0) }, + { label: 'circulatory system', value: rgbToHex(213, 99, 79) }, + { label: 'systemic arterial system', value: rgbToHex(217, 102, 81) }, + { label: 'systemic venous system', value: rgbToHex(0, 147, 202) }, + { label: 'pulmonary arterial system', value: rgbToHex(0, 122, 171) }, + { label: 'pulmonary venous system', value: rgbToHex(186, 77, 64) }, + { label: 'lymphatic system', value: rgbToHex(111, 197, 131) }, + { label: 'needle', value: rgbToHex(240, 255, 30) }, + { label: 'region 0', value: rgbToHex(185, 232, 61) }, + { label: 'region 1', value: rgbToHex(0, 226, 255) }, + { label: 'region 2', value: rgbToHex(251, 159, 255) }, + { label: 'region 3', value: rgbToHex(230, 169, 29) }, + { label: 'region 4', value: rgbToHex(0, 194, 113) }, + { label: 'region 5', value: rgbToHex(104, 160, 249) }, + { label: 'region 6', value: rgbToHex(221, 108, 158) }, + { label: 'region 7', value: rgbToHex(137, 142, 0) }, + { label: 'region 8', value: rgbToHex(230, 70, 0) }, + { label: 'region 9', value: rgbToHex(0, 147, 0) }, + { label: 'region 10', value: rgbToHex(0, 147, 248) }, + { label: 'region 11', value: rgbToHex(231, 0, 206) }, + { label: 'region 12', value: rgbToHex(129, 78, 0) }, + { label: 'region 13', value: rgbToHex(0, 116, 0) }, + { label: 'region 14', value: rgbToHex(0, 0, 255) }, + { label: 'region 15', value: rgbToHex(157, 0, 0) }, + { label: 'unknown', value: rgbToHex(100, 100, 130) }, + { label: 'cyst', value: rgbToHex(205, 205, 100) }, +]; diff --git a/plugins/ohifv3/extension-monai-label/src/utils/GenericUtils.js b/plugins/ohifv3/extension-monai-label/src/utils/GenericUtils.js new file mode 100644 index 000000000..e13c10259 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/utils/GenericUtils.js @@ -0,0 +1,100 @@ + +import { GenericAnatomyColors, GenericNames } from './GenericAnatomyColors'; + +function getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive +} + +function randomRGB() { + const o = Math.round, + r = Math.random, + s = 255; + return rgbToHex(o(r() * s), o(r() * s), o(r() * s)); +} + +function randomName() { + return GenericNames[getRandomInt(0, GenericNames.length)]; +} + +function componentToHex(c) { + const hex = c.toString(16); + return hex.length === 1 ? '0' + hex : hex; +} + +function rgbToHex(r, g, b) { + return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b); +} + +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null; +} + +function getLabelColor(label, rgb = true) { + const name = label.toLowerCase(); + for (const i of GenericAnatomyColors) { + if (i.label === name) { + return rgb ? hexToRgb(i.value) : i.value; + } + } + return null; +} + +export class CookieUtils { + static setCookie(name, value, exp_y, exp_m, exp_d, path, domain, secure) { + let cookie_string = name + '=' + escape(value); + if (exp_y) { + let expires = new Date(exp_y, exp_m, exp_d); + cookie_string += '; expires=' + expires.toGMTString(); + } + if (path) cookie_string += '; path=' + escape(path); + if (domain) cookie_string += '; domain=' + escape(domain); + if (secure) cookie_string += '; secure'; + document.cookie = cookie_string; + } + + static getCookie(cookie_name) { + let results = document.cookie.match( + '(^|;) ?' + cookie_name + '=([^;]*)(;|$)' + ); + if (results) return unescape(results[2]); + else return null; + } + + static getCookieString(name, defaultVal = '') { + const val = CookieUtils.getCookie(name); + console.debug(name + ' = ' + val + ' (default: ' + defaultVal + ' )'); + if (!val) { + CookieUtils.setCookie(name, defaultVal); + return defaultVal; + } + return val; + } + + static getCookieBool(name, defaultVal = false) { + const val = CookieUtils.getCookie(name, defaultVal); + return !!JSON.parse(String(val).toLowerCase()); + } + + static getCookieNumber(name, defaultVal = 0) { + const val = CookieUtils.getCookie(name, defaultVal); + return Number(val); + } +} + +export { + getRandomInt, + randomRGB, + randomName, + rgbToHex, + hexToRgb, + getLabelColor, +}; diff --git a/plugins/ohifv3/extension-monai-label/src/utils/SegmentationReader.js b/plugins/ohifv3/extension-monai-label/src/utils/SegmentationReader.js new file mode 100644 index 000000000..47c0dc4d0 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/utils/SegmentationReader.js @@ -0,0 +1,98 @@ +/* +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) { + let nrrdfile = nrrd.parse(data); + + // Currently gzip is not supported in nrrd.js + if (nrrdfile.encoding === 'gzip') { + const buffer = pako.inflate(nrrdfile.buffer).buffer; + + nrrdfile.encoding = 'raw'; + nrrdfile.data = new Uint16Array(buffer); + nrrdfile.buffer = buffer; + } + + const image = nrrdfile.buffer; + const header = nrrdfile; + delete header.data; + delete header.buffer; + + return { + header, + 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/extension-monai-label/src/utils/SegmentationUtils.tsx b/plugins/ohifv3/extension-monai-label/src/utils/SegmentationUtils.tsx new file mode 100644 index 000000000..6be5582e9 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/utils/SegmentationUtils.tsx @@ -0,0 +1,57 @@ +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 (let 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/extension-monai-label/src/utils/addToolInstance.ts b/plugins/ohifv3/extension-monai-label/src/utils/addToolInstance.ts new file mode 100644 index 000000000..889926d81 --- /dev/null +++ b/plugins/ohifv3/extension-monai-label/src/utils/addToolInstance.ts @@ -0,0 +1,12 @@ +import { addTool } from '@cornerstonejs/tools'; + +export default function addToolInstance( + name: string, + toolClass, + configuration? +): void { + class InstanceClass extends toolClass { + static toolName = name; + } + addTool(InstanceClass); +} diff --git a/plugins/ohifv3/images/ohifv3.png b/plugins/ohifv3/images/ohifv3.png new file mode 100644 index 000000000..0679559e5 Binary files /dev/null and b/plugins/ohifv3/images/ohifv3.png differ diff --git a/plugins/ohifv3/mode-monai-label/.gitignore b/plugins/ohifv3/mode-monai-label/.gitignore new file mode 100644 index 000000000..67045665d --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/plugins/ohifv3/mode-monai-label/.prettierrc b/plugins/ohifv3/mode-monai-label/.prettierrc new file mode 100644 index 000000000..b80ec6b34 --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "es5", + "printWidth": 80, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/plugins/ohifv3/mode-monai-label/.webpack/webpack.prod.js b/plugins/ohifv3/mode-monai-label/.webpack/webpack.prod.js new file mode 100644 index 000000000..163392a69 --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/.webpack/webpack.prod.js @@ -0,0 +1,62 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); + +// 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', + }, + }, + ], + module: { + rules: [ + { + test: /(\.jsx|\.js|\.tsx|\.ts)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], + }, +}; + +module.exports = config; diff --git a/plugins/ohifv3/mode-monai-label/LICENSE b/plugins/ohifv3/mode-monai-label/LICENSE new file mode 100644 index 000000000..68d51b46c --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 mode-monai-label (adiazpinto@nvidia.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/ohifv3/mode-monai-label/README.md b/plugins/ohifv3/mode-monai-label/README.md new file mode 100644 index 000000000..e0ce2d4de --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/README.md @@ -0,0 +1,7 @@ +# mode-monai-label +## Description +OHIFv3 mode for MONAI Label +## Author +OHIF,NVIDIA,KCL +## License +MIT diff --git a/plugins/ohifv3/mode-monai-label/babel.config.js b/plugins/ohifv3/mode-monai-label/babel.config.js new file mode 100644 index 000000000..92fbbdeaf --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/babel.config.js @@ -0,0 +1,44 @@ +module.exports = { + plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + "@babel/preset-typescript", + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + "@babel/preset-typescript", + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + "@babel/preset-typescript", + ], + plugins: ['react-hot-loader/babel'], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/plugins/ohifv3/mode-monai-label/package.json b/plugins/ohifv3/mode-monai-label/package.json new file mode 100644 index 000000000..ecbae9558 --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/package.json @@ -0,0 +1,64 @@ +{ + "name": "@ohif/mode-monai-label", + "version": "0.0.1", + "description": "OHIFv3 mode for MONAI Label", + "author": "OHIF,NVIDIA,KCL", + "license": "MIT", + "main": "dist/umd/mode-monai-label/index.umd.js", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-mode" + ], + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "scripts": { + "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", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "^3.0.0" + }, + "dependencies": { + "@babel/runtime": "^7.20.13" + }, + "devDependencies": { + "@babel/core": "^7.21.4", + "@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-typescript": "^7.13.0", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "babel-eslint": "^8.0.3", + "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-loader": "^2.0.0", + "webpack": "^5.50.0", + "webpack-merge": "^5.7.3", + "webpack-cli": "^4.7.2" + } +} diff --git a/plugins/ohifv3/mode-monai-label/src/id.js b/plugins/ohifv3/mode-monai-label/src/id.js new file mode 100644 index 000000000..ebe5acd98 --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/plugins/ohifv3/mode-monai-label/src/index.tsx b/plugins/ohifv3/mode-monai-label/src/index.tsx new file mode 100644 index 000000000..f832dd869 --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/src/index.tsx @@ -0,0 +1,206 @@ +import { hotkeys } from '@ohif/core'; +import toolbarButtons from './toolbarButtons.js'; +import { id } from './id.js'; +import initToolGroups from './initToolGroups.js'; + +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', +} + +const cornerstone = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +const dicomSeg = { + 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. + */ +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': '^0.0.1', +}; + +function modeFactory({ modeConfiguration }) { + return { + /** + * Mode ID, which should be unique among modes used by the viewer. This ID + * is used to identify the mode in the viewer's state. + */ + id, + routeName: 'monai-label', + /** + * Mode name, which is displayed in the viewer's UI in the workList, for the + * user to select the mode. + */ + displayName: 'MONAI Label', + /** + * 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; + + measurementService.clearMeasurements(); + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, toolGroupService, commandsManager); + + // 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.init(extensionManager); + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'MPR', + 'Crosshairs', + 'MoreTools', + ]); + }, + onModeExit: ({ servicesManager }) => { + const { + toolGroupService, + syncGroupService, + segmentationService, + cornerstoneViewportService, + } = servicesManager.services; + + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + /** */ + validationTags: { + study: [], + series: [], + }, + /** + * 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 + */ + 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; + }, + /** + * Mode Routes are used to define the mode's behavior. A list of Mode Route + * that includes the mode's path and the layout to be used. The layout will + * include the components that are used in the layout. For instance, if the + * default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout') + * it will include the leftPanels, rightPanels, and viewports. However, if + * you define another layoutTemplate that includes a Footer for instance, + * you should provide the Footer component here too. Note: We use Strings + * to reference the component's ID as they are registered in the internal + * ExtensionManager. The template for the string is: + * `${extensionId}.{moduleType}.${componentId}`. + */ + routes: [ + { + path: 'monai-label', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + rightPanelDefaultClosed: false, + /* leftPanelDefaultClosed: true, */ + leftPanels: [ohif.leftPanel], + rightPanels: [monailabel.monaiLabel], + viewports: [ + { + namespace: cornerstone.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + { + namespace: dicomSeg.viewport, + displaySetsToDisplay: [dicomSeg.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + /** List of extensions that are used by the mode */ + extensions: extensionDependencies, + /** HangingProtocol used by the mode */ + hangingProtocol: 'mpr', + // hangingProtocol: [''], + /** SopClassHandlers used by the mode */ + sopClassHandlers: [ + dicomSeg.sopClassHandler, + ohif.sopClassHandler, + ] /** hotkeys for mode */, + hotkeys: [...hotkeys.defaults.hotkeyBindings], + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/plugins/ohifv3/mode-monai-label/src/initToolGroups.js b/plugins/ohifv3/mode-monai-label/src/initToolGroups.js new file mode 100644 index 000000000..81bc09eb6 --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/src/initToolGroups.js @@ -0,0 +1,209 @@ +const brushInstanceNames = { + CircularBrush: 'CircularBrush', + CircularEraser: 'CircularEraser', + SphereBrush: 'SphereBrush', + SphereEraser: 'SphereEraser', + ThresholdCircularBrush: 'ThresholdCircularBrush', + ThresholdSphereBrush: 'ThresholdSphereBrush', +}; + +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', +}; + +function initDefaultToolGroup( + extensionManager, + toolGroupService, + commandsManager, + toolGroupId +) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + 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: [] }, + ], + 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.StackScroll }, + { toolName: toolNames.Magnify }, + { toolName: toolNames.SegmentationDisplay }, + { toolName: 'ProbeMONAILabel' }, + ], + // enabled + // disabled + disabled: [{ toolName: toolNames.ReferenceLines }], + }; + + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + 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: [] }, + ], + 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.ReferenceLines }, + ], + // enabled + // disabled + }; + + toolGroupService.createToolGroupAndAddTools('mpr', tools); +} + +function initToolGroups(extensionManager, toolGroupService, commandsManager) { + initDefaultToolGroup( + extensionManager, + toolGroupService, + commandsManager, + 'default' + ); + initMPRToolGroup(extensionManager, toolGroupService, commandsManager); +} + +export default initToolGroups; diff --git a/plugins/ohifv3/mode-monai-label/src/toolbarButtons.js b/plugins/ohifv3/mode-monai-label/src/toolbarButtons.js new file mode 100644 index 000000000..92de37e89 --- /dev/null +++ b/plugins/ohifv3/mode-monai-label/src/toolbarButtons.js @@ -0,0 +1,608 @@ +// 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'; + +const { windowLevelPresets } = defaults; +/** + * + * @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, + }, + 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' + ), + ], + }, + }, + // Zoom.. + { + id: 'Zoom', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-zoom', + label: 'Zoom', + commands: _createSetToolActiveCommands('Zoom'), + }, + }, + // Window Level + Presets... + { + id: 'WindowLevel', + type: 'ohif.splitButton', + 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'), + ], + }, + }, + // Pan... + { + id: 'Pan', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-move', + label: 'Pan', + commands: _createSetToolActiveCommands('Pan'), + }, + }, + { + id: 'Capture', + type: 'ohif.action', + props: { + icon: 'tool-capture', + label: 'Capture', + type: 'action', + commands: [ + { + commandName: 'showDownloadViewportModal', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + }, + }, + { + id: 'Layout', + type: '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', + }, + ], + }, + }, + { + id: 'Crosshairs', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-crosshair', + label: 'Crosshairs', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Crosshairs', + toolGroupId: 'mpr', + }, + context: 'CORNERSTONE', + }, + ], + }, + }, + // More... + { + id: 'MoreTools', + type: 'ohif.splitButton', + props: { + isRadio: true, // ? + groupId: 'MoreTools', + primary: _createActionButton( + 'Reset', + 'tool-reset', + 'Reset View', + [ + { + commandName: 'resetViewport', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + 'Reset' + ), + 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', + [ + { + commandName: 'rotateViewportCW', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + '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', + }, + ]), + _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', + [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'CalibrationLine', + }, + context: 'CORNERSTONE', + }, + ], + 'Calibration Line' + ), + _createActionButton( + 'TagBrowser', + 'list-bullets', + 'Dicom Tag Browser', + [ + { + commandName: 'openDICOMTagViewer', + commandOptions: {}, + context: 'DEFAULT', + }, + ], + 'Dicom Tag Browser' + ), + ], + }, + }, +]; + +export default toolbarButtons; diff --git a/plugins/ohifv3/requirements.sh b/plugins/ohifv3/requirements.sh new file mode 100755 index 000000000..064952153 --- /dev/null +++ b/plugins/ohifv3/requirements.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# 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. + +if which yarn >/dev/null; then + echo "node/yarn is already installed" +else + echo "installing yarn..." + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list + apt update + apt-get install yarn -y +fi diff --git a/sample-apps/radiology/lib/infers/deepedit.py b/sample-apps/radiology/lib/infers/deepedit.py index 6a182ed4d..afc755c98 100644 --- a/sample-apps/radiology/lib/infers/deepedit.py +++ b/sample-apps/radiology/lib/infers/deepedit.py @@ -8,9 +8,10 @@ # 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 logging from typing import Callable, Sequence, Union +from lib.transforms.transforms import GetCentroidsd from monai.apps.deepedit.transforms import ( AddGuidanceFromPointsDeepEditd, AddGuidanceSignalDeepEditd, @@ -35,6 +36,8 @@ from monailabel.tasks.infer.basic_infer import BasicInferTask from monailabel.transform.post import Restored +logger = logging.getLogger(__name__) + class DeepEdit(BasicInferTask): """ @@ -124,4 +127,5 @@ def post_transforms(self, data=None) -> Sequence[Callable]: ref_image="image", config_labels=self.labels if data.get("restore_label_idx", False) else None, ), + GetCentroidsd(keys="pred", centroids_key="centroids"), ] diff --git a/sample-apps/radiology/lib/infers/segmentation.py b/sample-apps/radiology/lib/infers/segmentation.py index 1eadab540..b10c9f499 100644 --- a/sample-apps/radiology/lib/infers/segmentation.py +++ b/sample-apps/radiology/lib/infers/segmentation.py @@ -11,6 +11,7 @@ from typing import Callable, Sequence +from lib.transforms.transforms import GetCentroidsd from monai.inferers import Inferer, SlidingWindowInferer from monai.transforms import ( Activationsd, @@ -93,11 +94,14 @@ def post_transforms(self, data=None) -> Sequence[Callable]: if data and data.get("largest_cc", False): t.append(KeepLargestConnectedComponentd(keys="pred")) - t.append( - Restored( - keys="pred", - ref_image="image", - config_labels=self.labels if data.get("restore_label_idx", False) else None, - ) + t.extend( + [ + Restored( + keys="pred", + ref_image="image", + config_labels=self.labels if data.get("restore_label_idx", False) else None, + ), + GetCentroidsd(keys="pred", centroids_key="centroids"), + ] ) return t diff --git a/sample-apps/radiology/lib/transforms/transforms.py b/sample-apps/radiology/lib/transforms/transforms.py index bc0e6719a..9dcb44b3f 100644 --- a/sample-apps/radiology/lib/transforms/transforms.py +++ b/sample-apps/radiology/lib/transforms/transforms.py @@ -132,7 +132,6 @@ def _get_centroids(self, label): centre.append(avg_indices) c[f"label_{int(seg_class)}"] = [int(seg_class), centre[-3], centre[-2], centre[-1]] centroids.append(c) - return centroids def __call__(self, data): diff --git a/tests/unit/datastore/test_convert.py b/tests/unit/datastore/test_convert.py index 68cfb4d5a..9c190f162 100644 --- a/tests/unit/datastore/test_convert.py +++ b/tests/unit/datastore/test_convert.py @@ -35,7 +35,7 @@ def test_dicom_to_nifti(self): def test_binary_to_image(self): reference_image = os.path.join(self.local_dataset, "labels", "final", "spleen_3.nii.gz") label = LoadImage(image_only=True)(reference_image) - label = label.astype(np.uint16) + label = label.astype(np.uint8) label = label.flatten(order="F") label_bin = tempfile.NamedTemporaryFile(suffix=".bin").name