feat(pmap): added support for parametric map (#4284)
This commit is contained in:
parent
9f3fc382c3
commit
fc0064fd9d
12
extensions/cornerstone-dicom-pmap/.webpack/webpack.dev.js
Normal file
12
extensions/cornerstone-dicom-pmap/.webpack/webpack.dev.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
const ENTRY = {
|
||||||
|
app: `${SRC_DIR}/index.tsx`,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||||
|
};
|
||||||
54
extensions/cornerstone-dicom-pmap/.webpack/webpack.prod.js
Normal file
54
extensions/cornerstone-dicom-pmap/.webpack/webpack.prod.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
|
||||||
|
const pkg = require('./../package.json');
|
||||||
|
|
||||||
|
const ROOT_DIR = path.join(__dirname, '../');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
const ENTRY = {
|
||||||
|
app: `${SRC_DIR}/index.tsx`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const outputName = `ohif-${pkg.name.split('/').pop()}`;
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||||
|
|
||||||
|
return merge(commonConfig, {
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
hash: true,
|
||||||
|
timings: true,
|
||||||
|
assets: true,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
warnings: true,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: ROOT_DIR,
|
||||||
|
library: 'ohif-extension-cornerstone-dicom-pmap',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
filename: pkg.main,
|
||||||
|
},
|
||||||
|
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: `./dist/${outputName}.css`,
|
||||||
|
chunkFilename: `./dist/${outputName}.css`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
4
extensions/cornerstone-dicom-pmap/CHANGELOG.md
Normal file
4
extensions/cornerstone-dicom-pmap/CHANGELOG.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
20
extensions/cornerstone-dicom-pmap/LICENSE
Normal file
20
extensions/cornerstone-dicom-pmap/LICENSE
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Open Health Imaging Foundation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
12
extensions/cornerstone-dicom-pmap/README.md
Normal file
12
extensions/cornerstone-dicom-pmap/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# dicom-pmap
|
||||||
|
## Description
|
||||||
|
|
||||||
|
DICOM PMAP read workflow. This extension will allow you to load a DICOM Parametric
|
||||||
|
Map image and display it on OHIF.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
OHIF
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
||||||
44
extensions/cornerstone-dicom-pmap/babel.config.js
Normal file
44
extensions/cornerstone-dicom-pmap/babel.config.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ['@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__'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
54
extensions/cornerstone-dicom-pmap/package.json
Normal file
54
extensions/cornerstone-dicom-pmap/package.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
||||||
|
"version": "3.9.0-beta.64",
|
||||||
|
"description": "DICOM Parametric Map read workflow",
|
||||||
|
"author": "OHIF",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js",
|
||||||
|
"module": "src/index.tsx",
|
||||||
|
"files": [
|
||||||
|
"dist/**",
|
||||||
|
"public/**",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"repository": "OHIF/Viewers",
|
||||||
|
"keywords": [
|
||||||
|
"ohif-extension"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1.18.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "shx rm -rf dist",
|
||||||
|
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||||
|
"dev:dicom-pmap": "yarn run dev",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
|
"build:package-1": "yarn run build",
|
||||||
|
"start": "yarn run dev"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ohif/core": "3.9.0-beta.64",
|
||||||
|
"@ohif/extension-cornerstone": "3.9.0-beta.64",
|
||||||
|
"@ohif/extension-default": "3.9.0-beta.64",
|
||||||
|
"@ohif/i18n": "3.9.0-beta.64",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-i18next": "^12.2.2",
|
||||||
|
"react-router": "^6.8.1",
|
||||||
|
"react-router-dom": "^6.8.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@cornerstonejs/adapters": "^1.77.6",
|
||||||
|
"@cornerstonejs/core": "^1.77.6",
|
||||||
|
"@kitware/vtk.js": "30.4.1",
|
||||||
|
"react-color": "^2.19.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,217 @@
|
|||||||
|
import { utils } from '@ohif/core';
|
||||||
|
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
|
||||||
|
import { adaptersPMAP } from '@cornerstonejs/adapters';
|
||||||
|
import { SOPClassHandlerId } from './id';
|
||||||
|
import { dicomLoaderService } from '@ohif/extension-cornerstone';
|
||||||
|
|
||||||
|
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||||
|
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
|
||||||
|
|
||||||
|
function _getDisplaySetsFromSeries(
|
||||||
|
instances,
|
||||||
|
servicesManager: AppTypes.ServicesManager,
|
||||||
|
extensionManager
|
||||||
|
) {
|
||||||
|
const instance = instances[0];
|
||||||
|
|
||||||
|
const {
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesDescription,
|
||||||
|
SeriesNumber,
|
||||||
|
SeriesDate,
|
||||||
|
SOPClassUID,
|
||||||
|
wadoRoot,
|
||||||
|
wadoUri,
|
||||||
|
wadoUriRoot,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
const displaySet = {
|
||||||
|
// Parametric map use to have the same modality as its referenced volume but
|
||||||
|
// "PMAP" is used in the viewer even though this is not a valid DICOM modality
|
||||||
|
Modality: 'PMAP',
|
||||||
|
isReconstructable: true, // by default for now
|
||||||
|
displaySetInstanceUID: `pmap.${utils.guid()}`,
|
||||||
|
SeriesDescription,
|
||||||
|
SeriesNumber,
|
||||||
|
SeriesDate,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPClassHandlerId,
|
||||||
|
SOPClassUID,
|
||||||
|
referencedImages: null,
|
||||||
|
referencedSeriesInstanceUID: null,
|
||||||
|
referencedDisplaySetInstanceUID: null,
|
||||||
|
referencedVolumeURI: null,
|
||||||
|
referencedVolumeId: null,
|
||||||
|
isDerivedDisplaySet: true,
|
||||||
|
loadStatus: {
|
||||||
|
loading: false,
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
sopClassUids,
|
||||||
|
instance,
|
||||||
|
instances: [instance],
|
||||||
|
wadoRoot,
|
||||||
|
wadoUriRoot,
|
||||||
|
wadoUri,
|
||||||
|
isOverlayDisplaySet: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||||
|
|
||||||
|
if (!referencedSeriesSequence) {
|
||||||
|
console.error('ReferencedSeriesSequence is missing for the parametric map');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
|
||||||
|
|
||||||
|
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||||
|
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||||
|
|
||||||
|
// Does not get the referenced displaySet during parametric displaySet creation
|
||||||
|
// because it is still not available (getDisplaySetByUID returns `undefined`).
|
||||||
|
displaySet.getReferenceDisplaySet = () => {
|
||||||
|
const { displaySetService } = servicesManager.services;
|
||||||
|
|
||||||
|
if (displaySet.referencedDisplaySetInstanceUID) {
|
||||||
|
return displaySetService.getDisplaySetByUID(displaySet.referencedDisplaySetInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||||
|
displaySet.referencedSeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||||
|
throw new Error('Referenced displaySet is missing for the parametric map');
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = referencedDisplaySets[0];
|
||||||
|
|
||||||
|
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||||
|
|
||||||
|
return referencedDisplaySet;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Does not get the referenced volumeId during parametric displaySet creation because the
|
||||||
|
// referenced displaySet is still not avaialble (getDisplaySetByUID returns `undefined`).
|
||||||
|
displaySet.getReferencedVolumeId = () => {
|
||||||
|
if (displaySet.referencedVolumeId) {
|
||||||
|
return displaySet.referencedVolumeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = displaySet.getReferenceDisplaySet();
|
||||||
|
const referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
|
||||||
|
const referencedVolumeId = `${VOLUME_LOADER_SCHEME}:${referencedVolumeURI}`;
|
||||||
|
|
||||||
|
displaySet.referencedVolumeURI = referencedVolumeURI;
|
||||||
|
displaySet.referencedVolumeId = referencedVolumeId;
|
||||||
|
|
||||||
|
return referencedVolumeId;
|
||||||
|
};
|
||||||
|
|
||||||
|
displaySet.load = async ({ headers }) =>
|
||||||
|
await _load(displaySet, servicesManager, extensionManager, headers);
|
||||||
|
|
||||||
|
return [displaySet];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _load(
|
||||||
|
displaySet,
|
||||||
|
servicesManager: AppTypes.ServicesManager,
|
||||||
|
extensionManager,
|
||||||
|
headers
|
||||||
|
) {
|
||||||
|
const volumeId = `${VOLUME_LOADER_SCHEME}:${displaySet.displaySetInstanceUID}`;
|
||||||
|
const volumeLoadObject = cache.getVolumeLoadObject(volumeId);
|
||||||
|
|
||||||
|
if (volumeLoadObject) {
|
||||||
|
return volumeLoadObject.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySet.loadStatus.loading = true;
|
||||||
|
|
||||||
|
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
||||||
|
// and also return the same promise to any other callers.
|
||||||
|
// loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||||
|
const promise = _loadParametricMap({
|
||||||
|
extensionManager,
|
||||||
|
displaySet,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.putVolumeLoadObject(volumeId, { promise }).catch(err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then(() => {
|
||||||
|
displaySet.loadStatus.loading = false;
|
||||||
|
displaySet.loadStatus.loaded = true;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
displaySet.loadStatus.loading = false;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) {
|
||||||
|
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
|
||||||
|
const referencedVolumeId = displaySet.getReferencedVolumeId();
|
||||||
|
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
|
||||||
|
|
||||||
|
// Parametric map can be loaded only if its referenced volume exists otherwise it will fail
|
||||||
|
if (!cachedReferencedVolume) {
|
||||||
|
throw new Error(
|
||||||
|
'Referenced Volume is missing for the PMAP, and stack viewport PMAP is not supported yet'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { imageIds } = cachedReferencedVolume;
|
||||||
|
const results = await adaptersPMAP.Cornerstone3D.ParametricMap.generateToolState(
|
||||||
|
imageIds,
|
||||||
|
arrayBuffer,
|
||||||
|
metaData
|
||||||
|
);
|
||||||
|
const { pixelData } = results;
|
||||||
|
const TypedArrayConstructor = pixelData.constructor;
|
||||||
|
const paramMapId = displaySet.displaySetInstanceUID;
|
||||||
|
|
||||||
|
const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, {
|
||||||
|
volumeId: paramMapId,
|
||||||
|
targetBuffer: {
|
||||||
|
type: TypedArrayConstructor.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
derivedVolume.getScalarData().set(pixelData);
|
||||||
|
|
||||||
|
const range = derivedVolume.imageData.getPointData().getScalars().getRange();
|
||||||
|
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
|
||||||
|
|
||||||
|
derivedVolume.metadata.voiLut = [windowLevel];
|
||||||
|
derivedVolume.loadStatus = { loaded: true };
|
||||||
|
|
||||||
|
return derivedVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||||
|
const getDisplaySetsFromSeries = instances => {
|
||||||
|
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'dicom-pmap',
|
||||||
|
sopClassUids,
|
||||||
|
getDisplaySetsFromSeries,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getSopClassHandlerModule;
|
||||||
7
extensions/cornerstone-dicom-pmap/src/id.js
Normal file
7
extensions/cornerstone-dicom-pmap/src/id.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import packageJson from '../package.json';
|
||||||
|
|
||||||
|
const id = packageJson.name;
|
||||||
|
const SOPClassHandlerName = 'dicom-pmap';
|
||||||
|
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||||
|
|
||||||
|
export { id, SOPClassHandlerId, SOPClassHandlerName };
|
||||||
39
extensions/cornerstone-dicom-pmap/src/index.tsx
Normal file
39
extensions/cornerstone-dicom-pmap/src/index.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { id } from './id';
|
||||||
|
import React from 'react';
|
||||||
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
|
|
||||||
|
const Component = React.lazy(() => {
|
||||||
|
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstonePMAPViewport');
|
||||||
|
});
|
||||||
|
|
||||||
|
const OHIFCornerstonePMAPViewport = props => {
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<Component {...props} />
|
||||||
|
</React.Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can remove any of the following modules if you don't need them.
|
||||||
|
*/
|
||||||
|
const extension = {
|
||||||
|
id,
|
||||||
|
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||||
|
const ExtendedOHIFCornerstonePMAPViewport = props => {
|
||||||
|
return (
|
||||||
|
<OHIFCornerstonePMAPViewport
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [{ name: 'dicom-pmap', component: ExtendedOHIFCornerstonePMAPViewport }];
|
||||||
|
},
|
||||||
|
getSopClassHandlerModule,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default extension;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
function createPMAPToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
||||||
|
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||||
|
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createPMAPToolGroupAndAddTools;
|
||||||
@ -0,0 +1,163 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { useViewportGrid } from '@ohif/ui';
|
||||||
|
import createPMAPToolGroupAndAddTools from '../utils/initPMAPToolGroup';
|
||||||
|
|
||||||
|
const PMAP_TOOLGROUP_BASE_NAME = 'PMAPToolGroup';
|
||||||
|
|
||||||
|
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||||
|
const { displaySets, viewportOptions, displaySetOptions, servicesManager, extensionManager } =
|
||||||
|
props;
|
||||||
|
const viewportId = viewportOptions.viewportId;
|
||||||
|
const { displaySetService, toolGroupService, customizationService } = servicesManager.services;
|
||||||
|
const toolGroupId = `${PMAP_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||||
|
|
||||||
|
// PMAP viewport will always have a single display set
|
||||||
|
if (displaySets.length !== 1) {
|
||||||
|
throw new Error('PMAP viewport must have a single display set');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pmapDisplaySet = displaySets[0];
|
||||||
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
|
const referencedDisplaySetRef = useRef(null);
|
||||||
|
const { viewports, activeViewportId } = viewportGrid;
|
||||||
|
const referencedDisplaySet = pmapDisplaySet.getReferenceDisplaySet();
|
||||||
|
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||||
|
referencedDisplaySet,
|
||||||
|
pmapDisplaySet
|
||||||
|
);
|
||||||
|
|
||||||
|
referencedDisplaySetRef.current = {
|
||||||
|
displaySet: referencedDisplaySet,
|
||||||
|
metadata: referencedDisplaySetMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCornerstoneViewport = useCallback(() => {
|
||||||
|
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||||
|
const { component: Component } = extensionManager.getModuleEntry(
|
||||||
|
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||||
|
);
|
||||||
|
|
||||||
|
displaySetOptions.unshift({});
|
||||||
|
const [pmapDisplaySetOptions] = displaySetOptions;
|
||||||
|
|
||||||
|
// Make sure `options` exists
|
||||||
|
pmapDisplaySetOptions.options = pmapDisplaySetOptions.options ?? {};
|
||||||
|
|
||||||
|
Object.assign(pmapDisplaySetOptions.options, {
|
||||||
|
colormap: {
|
||||||
|
name: 'rainbow',
|
||||||
|
opacity: [
|
||||||
|
{ value: 0, opacity: 0.5 },
|
||||||
|
{ value: 1, opacity: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...props}
|
||||||
|
// Referenced + PMAP displaySets must be passed as parameter in this order
|
||||||
|
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
||||||
|
viewportOptions={{
|
||||||
|
viewportType: 'volume',
|
||||||
|
toolGroupId: toolGroupId,
|
||||||
|
orientation: viewportOptions.orientation,
|
||||||
|
viewportId: viewportOptions.viewportId,
|
||||||
|
}}
|
||||||
|
displaySetOptions={[{}, pmapDisplaySetOptions]}
|
||||||
|
></Component>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
extensionManager,
|
||||||
|
displaySetOptions,
|
||||||
|
props,
|
||||||
|
pmapDisplaySet,
|
||||||
|
toolGroupId,
|
||||||
|
viewportOptions.orientation,
|
||||||
|
viewportOptions.viewportId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cleanup the PMAP viewport when the viewport is destroyed
|
||||||
|
useEffect(() => {
|
||||||
|
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
||||||
|
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||||
|
({ displaySetInstanceUIDs }) => {
|
||||||
|
const activeViewport = viewports.get(activeViewportId);
|
||||||
|
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
|
||||||
|
viewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportId: activeViewportId,
|
||||||
|
displaySetInstanceUIDs: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
onDisplaySetsRemovedSubscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||||
|
|
||||||
|
if (toolGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This creates a custom tool group which has the lifetime of this view only
|
||||||
|
toolGroup = createPMAPToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||||
|
|
||||||
|
return () => toolGroupService.destroyToolGroup(toolGroupId);
|
||||||
|
}, [customizationService, toolGroupId, toolGroupService]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
|
{getCornerstoneViewport()}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OHIFCornerstonePMAPViewport.propTypes = {
|
||||||
|
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
viewportId: PropTypes.string.isRequired,
|
||||||
|
dataSource: PropTypes.object,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getReferencedDisplaySetMetadata(referencedDisplaySet, pmapDisplaySet) {
|
||||||
|
const { SharedFunctionalGroupsSequence } = pmapDisplaySet.instance;
|
||||||
|
|
||||||
|
const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
|
||||||
|
? SharedFunctionalGroupsSequence[0]
|
||||||
|
: SharedFunctionalGroupsSequence;
|
||||||
|
|
||||||
|
const { PixelMeasuresSequence } = SharedFunctionalGroup;
|
||||||
|
|
||||||
|
const PixelMeasures = Array.isArray(PixelMeasuresSequence)
|
||||||
|
? PixelMeasuresSequence[0]
|
||||||
|
: PixelMeasuresSequence;
|
||||||
|
|
||||||
|
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
|
||||||
|
|
||||||
|
const image0 = referencedDisplaySet.images[0];
|
||||||
|
const referencedDisplaySetMetadata = {
|
||||||
|
PatientID: image0.PatientID,
|
||||||
|
PatientName: image0.PatientName,
|
||||||
|
PatientSex: image0.PatientSex,
|
||||||
|
PatientAge: image0.PatientAge,
|
||||||
|
SliceThickness: image0.SliceThickness || SliceThickness,
|
||||||
|
StudyDate: image0.StudyDate,
|
||||||
|
SeriesDescription: image0.SeriesDescription,
|
||||||
|
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||||
|
SeriesNumber: image0.SeriesNumber,
|
||||||
|
ManufacturerModelName: image0.ManufacturerModelName,
|
||||||
|
SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
|
||||||
|
};
|
||||||
|
|
||||||
|
return referencedDisplaySetMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OHIFCornerstonePMAPViewport;
|
||||||
@ -131,6 +131,11 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
|||||||
throw new Error('Viewport ID is required');
|
throw new Error('Viewport ID is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure displaySetOptions has one object per displaySet
|
||||||
|
while (displaySetOptions.length < displaySets.length) {
|
||||||
|
displaySetOptions.push({});
|
||||||
|
}
|
||||||
|
|
||||||
// Since we only have support for dynamic data in volume viewports, we should
|
// Since we only have support for dynamic data in volume viewports, we should
|
||||||
// handle this case here and set the viewportType to volume if any of the
|
// handle this case here and set the viewportType to volume if any of the
|
||||||
// displaySets are dynamic volumes
|
// displaySets are dynamic volumes
|
||||||
|
|||||||
@ -231,5 +231,6 @@ export {
|
|||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
ImageOverlayViewerTool,
|
ImageOverlayViewerTool,
|
||||||
getSOPInstanceAttributes,
|
getSOPInstanceAttributes,
|
||||||
|
dicomLoaderService,
|
||||||
};
|
};
|
||||||
export default cornerstoneExtension;
|
export default cornerstoneExtension;
|
||||||
|
|||||||
@ -200,6 +200,9 @@ class CornerstoneCacheService {
|
|||||||
const volumeData = [];
|
const volumeData = [];
|
||||||
|
|
||||||
for (const displaySet of displaySets) {
|
for (const displaySet of displaySets) {
|
||||||
|
const { Modality } = displaySet;
|
||||||
|
const isParametricMap = Modality === 'PMAP';
|
||||||
|
|
||||||
// Don't create volumes for the displaySets that have custom load
|
// Don't create volumes for the displaySets that have custom load
|
||||||
// function (e.g., SEG, RT, since they rely on the reference volumes
|
// function (e.g., SEG, RT, since they rely on the reference volumes
|
||||||
// and they take care of their own loading after they are created in their
|
// and they take care of their own loading after they are created in their
|
||||||
@ -210,6 +213,9 @@ class CornerstoneCacheService {
|
|||||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||||
await displaySet.load({ headers });
|
await displaySet.load({ headers });
|
||||||
|
|
||||||
|
// Parametric maps have a `load` method but it should not be loaded in the
|
||||||
|
// same way as SEG and RTSTRUCT but like a normal volume
|
||||||
|
if (!isParametricMap) {
|
||||||
volumeData.push({
|
volumeData.push({
|
||||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
@ -218,16 +224,16 @@ class CornerstoneCacheService {
|
|||||||
// Todo: do some cache check and empty the cache if needed
|
// Todo: do some cache check and empty the cache if needed
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const volumeLoaderSchema = displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
|
const volumeLoaderSchema = displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
|
||||||
|
|
||||||
const volumeId = `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
|
const volumeId = `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
|
||||||
|
|
||||||
let volumeImageIds = this.volumeImageIds.get(displaySet.displaySetInstanceUID);
|
let volumeImageIds = this.volumeImageIds.get(displaySet.displaySetInstanceUID);
|
||||||
|
|
||||||
let volume = cs3DCache.getVolume(volumeId);
|
let volume = cs3DCache.getVolume(volumeId);
|
||||||
|
|
||||||
if (!volumeImageIds || !volume) {
|
// Parametric maps do not have image ids but they already have volume data
|
||||||
|
// therefore a new volume should not be created.
|
||||||
|
if (!isParametricMap && (!volumeImageIds || !volume)) {
|
||||||
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
|
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
|
||||||
|
|
||||||
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||||
|
|||||||
@ -814,12 +814,16 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
// load any secondary displaySets
|
// load any secondary displaySets
|
||||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||||
|
|
||||||
// can be SEG or RTSTRUCT for now
|
// Can be SEG or RTSTRUCT for now but not PMAP
|
||||||
const overlayDisplaySet = displaySetInstanceUIDs
|
const segOrRTSOverlayDisplaySet = displaySetInstanceUIDs
|
||||||
.map(displaySetService.getDisplaySetByUID)
|
.map(displaySetService.getDisplaySetByUID)
|
||||||
.find(displaySet => displaySet?.isOverlayDisplaySet);
|
.find(
|
||||||
if (overlayDisplaySet) {
|
displaySet =>
|
||||||
this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport);
|
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (segOrRTSOverlayDisplaySet) {
|
||||||
|
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
|
||||||
} else {
|
} else {
|
||||||
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
||||||
// and we can look into hydrated segmentations to check if any of them are
|
// and we can look into hydrated segmentations to check if any of them are
|
||||||
|
|||||||
@ -27,7 +27,7 @@ function ToolbarSplitButtonWithServices({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
[]
|
[groupId, onInteraction]
|
||||||
);
|
);
|
||||||
|
|
||||||
const PrimaryButtonComponent =
|
const PrimaryButtonComponent =
|
||||||
|
|||||||
@ -624,7 +624,17 @@ function _mapDisplaySets(
|
|||||||
return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
|
return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbnailNoImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE', 'DOC', 'OT'];
|
const thumbnailNoImageModalities = [
|
||||||
|
'SR',
|
||||||
|
'SEG',
|
||||||
|
'SM',
|
||||||
|
'RTSTRUCT',
|
||||||
|
'RTPLAN',
|
||||||
|
'RTDOSE',
|
||||||
|
'DOC',
|
||||||
|
'OT',
|
||||||
|
'PMAP',
|
||||||
|
];
|
||||||
|
|
||||||
function _getComponentType(ds) {
|
function _getComponentType(ds) {
|
||||||
if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) {
|
if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) {
|
||||||
|
|||||||
@ -43,6 +43,11 @@ const dicomSeg = {
|
|||||||
panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
|
panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dicomPmap = {
|
||||||
|
sopClassHandler: '@ohif/extension-cornerstone-dicom-pmap.sopClassHandlerModule.dicom-pmap',
|
||||||
|
viewport: '@ohif/extension-cornerstone-dicom-pmap.viewportModule.dicom-pmap',
|
||||||
|
};
|
||||||
|
|
||||||
const extensionDependencies = {
|
const extensionDependencies = {
|
||||||
// Can derive the versions at least process.env.from npm_package_version
|
// Can derive the versions at least process.env.from npm_package_version
|
||||||
'@ohif/extension-default': '^3.0.0',
|
'@ohif/extension-default': '^3.0.0',
|
||||||
@ -50,6 +55,7 @@ const extensionDependencies = {
|
|||||||
'@ohif/extension-measurement-tracking': '^3.0.0',
|
'@ohif/extension-measurement-tracking': '^3.0.0',
|
||||||
'@ohif/extension-cornerstone-dicom-sr': '^3.0.0',
|
'@ohif/extension-cornerstone-dicom-sr': '^3.0.0',
|
||||||
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
|
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
|
||||||
|
'@ohif/extension-cornerstone-dicom-pmap': '^3.0.0',
|
||||||
'@ohif/extension-dicom-pdf': '^3.0.1',
|
'@ohif/extension-dicom-pdf': '^3.0.1',
|
||||||
'@ohif/extension-dicom-video': '^3.0.1',
|
'@ohif/extension-dicom-video': '^3.0.1',
|
||||||
'@ohif/extension-test': '^0.0.1',
|
'@ohif/extension-test': '^0.0.1',
|
||||||
@ -162,6 +168,10 @@ function modeFactory() {
|
|||||||
namespace: dicomSeg.viewport,
|
namespace: dicomSeg.viewport,
|
||||||
displaySetsToDisplay: [dicomSeg.sopClassHandler],
|
displaySetsToDisplay: [dicomSeg.sopClassHandler],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
namespace: dicomPmap.viewport,
|
||||||
|
displaySetsToDisplay: [dicomPmap.sopClassHandler],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -42,6 +42,11 @@ const dicomSeg = {
|
|||||||
panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
|
panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dicomPmap = {
|
||||||
|
sopClassHandler: '@ohif/extension-cornerstone-dicom-pmap.sopClassHandlerModule.dicom-pmap',
|
||||||
|
viewport: '@ohif/extension-cornerstone-dicom-pmap.viewportModule.dicom-pmap',
|
||||||
|
};
|
||||||
|
|
||||||
const dicomRT = {
|
const dicomRT = {
|
||||||
viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt',
|
viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt',
|
||||||
sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt',
|
sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt',
|
||||||
@ -54,6 +59,7 @@ const extensionDependencies = {
|
|||||||
'@ohif/extension-measurement-tracking': '^3.0.0',
|
'@ohif/extension-measurement-tracking': '^3.0.0',
|
||||||
'@ohif/extension-cornerstone-dicom-sr': '^3.0.0',
|
'@ohif/extension-cornerstone-dicom-sr': '^3.0.0',
|
||||||
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
|
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
|
||||||
|
'@ohif/extension-cornerstone-dicom-pmap': '^3.0.0',
|
||||||
'@ohif/extension-cornerstone-dicom-rt': '^3.0.0',
|
'@ohif/extension-cornerstone-dicom-rt': '^3.0.0',
|
||||||
'@ohif/extension-dicom-pdf': '^3.0.1',
|
'@ohif/extension-dicom-pdf': '^3.0.1',
|
||||||
'@ohif/extension-dicom-video': '^3.0.1',
|
'@ohif/extension-dicom-video': '^3.0.1',
|
||||||
@ -206,6 +212,10 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
namespace: dicomSeg.viewport,
|
namespace: dicomSeg.viewport,
|
||||||
displaySetsToDisplay: [dicomSeg.sopClassHandler],
|
displaySetsToDisplay: [dicomSeg.sopClassHandler],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
namespace: dicomPmap.viewport,
|
||||||
|
displaySetsToDisplay: [dicomPmap.sopClassHandler],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
namespace: dicomRT.viewport,
|
namespace: dicomRT.viewport,
|
||||||
displaySetsToDisplay: [dicomRT.sopClassHandler],
|
displaySetsToDisplay: [dicomRT.sopClassHandler],
|
||||||
@ -226,6 +236,7 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
sopClassHandlers: [
|
sopClassHandlers: [
|
||||||
dicomvideo.sopClassHandler,
|
dicomvideo.sopClassHandler,
|
||||||
dicomSeg.sopClassHandler,
|
dicomSeg.sopClassHandler,
|
||||||
|
dicomPmap.sopClassHandler,
|
||||||
ohif.sopClassHandler,
|
ohif.sopClassHandler,
|
||||||
dicompdf.sopClassHandler,
|
dicompdf.sopClassHandler,
|
||||||
dicomsr.sopClassHandler,
|
dicomsr.sopClassHandler,
|
||||||
|
|||||||
@ -22,6 +22,11 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"version": "3.0.0"
|
"version": "3.0.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"packageName": "@ohif/extension-cornerstone-dicom-pmap",
|
||||||
|
"default": false,
|
||||||
|
"version": "3.0.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"packageName": "@ohif/extension-cornerstone-dynamic-volume",
|
"packageName": "@ohif/extension-cornerstone-dynamic-volume",
|
||||||
"default": false,
|
"default": false,
|
||||||
|
|||||||
@ -150,6 +150,7 @@ function _getModalityTooltip(modality) {
|
|||||||
const _modalityTooltips = {
|
const _modalityTooltips = {
|
||||||
SR: 'Structured Report',
|
SR: 'Structured Report',
|
||||||
SEG: 'Segmentation',
|
SEG: 'Segmentation',
|
||||||
|
OT: 'Other',
|
||||||
RTSTRUCT: 'RT Structure Set',
|
RTSTRUCT: 'RT Structure Set',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user