init
This commit is contained in:
12
modes/tmtv/.webpack/webpack.dev.js
Normal file
12
modes/tmtv/.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.ts`,
|
||||
};
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||
};
|
||||
54
modes/tmtv/.webpack/webpack.prod.js
Normal file
54
modes/tmtv/.webpack/webpack.prod.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const webpack = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const pkg = require('./../package.json');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './../');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
const ENTRY = {
|
||||
app: `${SRC_DIR}/index.ts`,
|
||||
};
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||
|
||||
return merge(commonConfig, {
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: false,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'ohif-mode-tmtv',
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
filename: pkg.main,
|
||||
},
|
||||
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
// new MiniCssExtractPlugin({
|
||||
// filename: './dist/[name].css',
|
||||
// chunkFilename: './dist/[id].css',
|
||||
// }),
|
||||
],
|
||||
});
|
||||
};
|
||||
2177
modes/tmtv/CHANGELOG.md
Normal file
2177
modes/tmtv/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
21
modes/tmtv/LICENSE
Normal file
21
modes/tmtv/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
78
modes/tmtv/README.md
Normal file
78
modes/tmtv/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Total Metabolic Tumor Volume
|
||||
|
||||
## Introduction
|
||||
|
||||
Total Metabolic Tumor Volume (TMTV) workflow mode enables quantitatively measurement of a tumor volume in a patient's body.
|
||||
This mode is accessible in any study that has a PT and CT image series as you can see below
|
||||
|
||||
|
||||

|
||||
|
||||
Note: If the study does not have a PT and CT image series, the TMTV workflow mode will not be available
|
||||
and will become grayed out.
|
||||
|
||||
## Layout
|
||||
The designed layout for the viewports follows a predefined hanging protocol which will place
|
||||
10 viewports containing CT, PT, Fusion and Maximum Intensity Projection (MIP) PT scenes.
|
||||
|
||||
The hanging protocol will match the CT and PT displaySets based on series description. In terms
|
||||
of PT displaySets, the hanging protocol will match the PT displaySet that has attenuated
|
||||
corrected PET image data.
|
||||
|
||||
As seen in the image below, the first row contains CT volume in 3 different views of Axial,
|
||||
Sagittal and Coronal. The second row contains PT volume in the same views as the first row.
|
||||
The last row contains the fusion volume and the viewport to the right is a MIP of the PT
|
||||
Volume in the Sagittal view.
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
## Synchronization
|
||||
|
||||
The viewports in the 3 rows are synchronized both for the Camera and WindowLevel.
|
||||
It means that when you interact with the CT viewport (pan, zoom, scroll),
|
||||
the PT and Fusion viewports will be synchronized to the same view. In addition
|
||||
to camera synchronization, the window level of the CT viewport will be synchronized
|
||||
with the fusion viewport.
|
||||
|
||||
|
||||
### MIP
|
||||
The tools that are activated on each viewport is unique to its data. For instance,
|
||||
the mouse scroll tool for PT, CT and Fusion viewports are scrolling through the image data
|
||||
(in different directions); however, the mouse scroll tool for the MIP viewport will
|
||||
rotate the camera to match the usecase for the MIP.
|
||||
|
||||
|
||||
## Panels
|
||||
There are two panels that are available in the TMTV workflow mode and we will
|
||||
discuss them in detail below.
|
||||
|
||||
### SUV Panel
|
||||
This panel shows the PT metadata derived from the matched PT displaySet. The user
|
||||
can edit/change the metadata if needed, and by reloading the data the new
|
||||
metadata will be applied to the PT volume.
|
||||
|
||||
|
||||
## ROI Threshold Panel
|
||||
The ROI Threshold panel is a panel that allows the user to use the `RectangleROIStartEnd`
|
||||
tool from Cornerstone to define and edit a region of interest. Then, the user can
|
||||
apply a threshold to the pixels in the ROI and save the result as a segmentation volume.
|
||||
|
||||
By applying each threshold to the ROI, the Total Metabolic Tumor Volume (TMTV), and
|
||||
the SUV Peak values will get calculated for the labelmap segments and shown in the
|
||||
panel.
|
||||
|
||||
|
||||
## Export Report
|
||||
|
||||
Finally, the results can be saved in the CSV format. The RectangleROI annotations
|
||||
can also be extracted as a dicom RT Structure Set and saved as a DICOM file.
|
||||
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
Below you can see a video tutorial on how to use the TMTV workflow mode.
|
||||
|
||||
|
||||
https://user-images.githubusercontent.com/7490180/171065443-35369fba-e955-48ac-94da-d262e0fccb6b.mp4
|
||||
BIN
modes/tmtv/assets/modeLayout.png
Normal file
BIN
modes/tmtv/assets/modeLayout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 698 KiB |
BIN
modes/tmtv/assets/modeValid.png
Normal file
BIN
modes/tmtv/assets/modeValid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
1
modes/tmtv/babel.config.js
Normal file
1
modes/tmtv/babel.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../babel.config.js');
|
||||
53
modes/tmtv/package.json
Normal file
53
modes/tmtv/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@ohif/mode-tmtv",
|
||||
"version": "3.9.1",
|
||||
"description": "Total Metabolic Tumor Volume Workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/ohif-mode-tmtv.umd.js",
|
||||
"module": "src/index.ts",
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"ohif-mode"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:cornerstone": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"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.9.1",
|
||||
"@ohif/extension-cornerstone": "3.9.1",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.9.1",
|
||||
"@ohif/extension-default": "3.9.1",
|
||||
"@ohif/extension-dicom-pdf": "3.9.1",
|
||||
"@ohif/extension-dicom-video": "3.9.1",
|
||||
"@ohif/extension-measurement-tracking": "3.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"i18next": "^17.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "5.94.0",
|
||||
"webpack-merge": "^5.7.3"
|
||||
}
|
||||
}
|
||||
5
modes/tmtv/src/id.js
Normal file
5
modes/tmtv/src/id.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
|
||||
export { id };
|
||||
245
modes/tmtv/src/index.ts
Normal file
245
modes/tmtv/src/index.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { hotkeys, classes } from '@ohif/core';
|
||||
import toolbarButtons from './toolbarButtons.js';
|
||||
import { id } from './id.js';
|
||||
import initToolGroups from './initToolGroups.js';
|
||||
import setCrosshairsConfiguration from './utils/setCrosshairsConfiguration.js';
|
||||
import setFusionActiveVolume from './utils/setFusionActiveVolume.js';
|
||||
import i18n from 'i18next';
|
||||
|
||||
const { MetadataProvider } = classes;
|
||||
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
||||
};
|
||||
|
||||
const cs3d = {
|
||||
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
|
||||
segPanel: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader',
|
||||
measurements: '@ohif/extension-cornerstone.panelModule.measurements',
|
||||
};
|
||||
|
||||
const tmtv = {
|
||||
hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
|
||||
petSUV: '@ohif/extension-tmtv.panelModule.petSUV',
|
||||
tmtv: '@ohif/extension-tmtv.panelModule.tmtv',
|
||||
};
|
||||
|
||||
const extensionDependencies = {
|
||||
// Can derive the versions at least process.env.from npm_package_version
|
||||
'@ohif/extension-default': '^3.0.0',
|
||||
'@ohif/extension-cornerstone': '^3.0.0',
|
||||
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
|
||||
'@ohif/extension-tmtv': '^3.0.0',
|
||||
};
|
||||
|
||||
const unsubscriptions = [];
|
||||
function modeFactory({ modeConfiguration }) {
|
||||
return {
|
||||
// TODO: We're using this as a route segment
|
||||
// We should not be.
|
||||
id,
|
||||
routeName: 'tmtv',
|
||||
displayName: i18n.t('Modes:Total Metabolic Tumor Volume'),
|
||||
/**
|
||||
* Lifecycle hooks
|
||||
*/
|
||||
onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => {
|
||||
const {
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
hangingProtocolService,
|
||||
displaySetService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||
);
|
||||
|
||||
const { toolNames, Enums } = utilityModule.exports;
|
||||
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(toolNames, Enums, toolGroupService, commandsManager, null, servicesManager);
|
||||
|
||||
const { unsubscribe } = toolGroupService.subscribe(
|
||||
toolGroupService.EVENTS.VIEWPORT_ADDED,
|
||||
() => {
|
||||
// For fusion toolGroup we need to add the volumeIds for the crosshairs
|
||||
// since in the fusion viewport we don't want both PT and CT to render MIP
|
||||
// when slabThickness is modified
|
||||
const { displaySetMatchDetails } = hangingProtocolService.getMatchDetails();
|
||||
|
||||
setCrosshairsConfiguration(
|
||||
displaySetMatchDetails,
|
||||
toolNames,
|
||||
toolGroupService,
|
||||
displaySetService
|
||||
);
|
||||
|
||||
setFusionActiveVolume(
|
||||
displaySetMatchDetails,
|
||||
toolNames,
|
||||
toolGroupService,
|
||||
displaySetService
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
unsubscriptions.push(unsubscribe);
|
||||
toolbarService.addButtons(toolbarButtons);
|
||||
toolbarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
'Crosshairs',
|
||||
'Pan',
|
||||
]);
|
||||
toolbarService.createButtonSection('ROIThresholdToolbox', [
|
||||
'RectangleROIStartEndThreshold',
|
||||
'BrushTools',
|
||||
]);
|
||||
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'PanelSegmentation.tableMode',
|
||||
mode: 'expanded',
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: () => {
|
||||
commandsManager.run('createNewLabelmapFromPT');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.readableText',
|
||||
// remove following if you are not interested in that stats
|
||||
readableText: {
|
||||
lesionStats: 'Lesion Statistics',
|
||||
minValue: 'Minimum Value',
|
||||
maxValue: 'Maximum Value',
|
||||
meanValue: 'Mean Value',
|
||||
volume: 'Volume',
|
||||
suvPeak: 'SUV Peak',
|
||||
suvMax: 'Maximum SUV',
|
||||
suvMaxIJK: 'SUV Max IJK',
|
||||
lesionGlyoclysisStats: 'Lesion Glycolysis',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// For the hanging protocol we need to decide on the window level
|
||||
// based on whether the SUV is corrected or not, hence we can't hard
|
||||
// code the window level in the hanging protocol but we add a custom
|
||||
// attribute to the hanging protocol that will be used to get the
|
||||
// window level based on the metadata
|
||||
hangingProtocolService.addCustomAttribute(
|
||||
'getPTVOIRange',
|
||||
'get PT VOI based on corrected or not',
|
||||
props => {
|
||||
const ptDisplaySet = props.find(imageSet => imageSet.Modality === 'PT');
|
||||
|
||||
if (!ptDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageId } = ptDisplaySet.images[0];
|
||||
const imageIdScalingFactor = MetadataProvider.get('scalingModule', imageId);
|
||||
|
||||
const isSUVAvailable = imageIdScalingFactor && imageIdScalingFactor.suvbw;
|
||||
|
||||
if (isSUVAvailable) {
|
||||
return {
|
||||
windowWidth: 5,
|
||||
windowCenter: 2.5,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
);
|
||||
},
|
||||
onModeExit: ({ servicesManager }: withAppTypes) => {
|
||||
const {
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
segmentationService,
|
||||
cornerstoneViewportService,
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
} = servicesManager.services;
|
||||
|
||||
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
uiDialogService.dismissAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
segmentationService.destroy();
|
||||
cornerstoneViewportService.destroy();
|
||||
},
|
||||
validationTags: {
|
||||
study: [],
|
||||
series: [],
|
||||
},
|
||||
isValidMode: ({ modalities, study }) => {
|
||||
const modalities_list = modalities.split('\\');
|
||||
const invalidModalities = ['SM'];
|
||||
|
||||
const isValid =
|
||||
modalities_list.includes('CT') &&
|
||||
study.mrn !== 'M1' &&
|
||||
modalities_list.includes('PT') &&
|
||||
!invalidModalities.some(modality => modalities_list.includes(modality)) &&
|
||||
// This is study is a 4D study with PT and CT and not a 3D study for the tmtv
|
||||
// mode, until we have a better way to identify 4D studies we will use the
|
||||
// StudyInstanceUID to identify the study
|
||||
// Todo: when we add the 4D mode which comes with a mechanism to identify
|
||||
// 4D studies we can use that
|
||||
study.studyInstanceUid !== '1.3.6.1.4.1.12842.1.1.14.3.20220915.105557.468.2963630849';
|
||||
|
||||
// there should be both CT and PT modalities and the modality should not be SM
|
||||
return {
|
||||
valid: isValid,
|
||||
description: 'The mode requires both PT and CT series in the study',
|
||||
};
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
path: 'tmtv',
|
||||
/*init: ({ servicesManager, extensionManager }) => {
|
||||
//defaultViewerRouteInit
|
||||
},*/
|
||||
layoutTemplate: () => {
|
||||
return {
|
||||
id: ohif.layout,
|
||||
props: {
|
||||
leftPanels: [ohif.thumbnailList],
|
||||
leftPanelClosed: true,
|
||||
rightPanels: [tmtv.tmtv, tmtv.petSUV],
|
||||
viewports: [
|
||||
{
|
||||
namespace: cs3d.viewport,
|
||||
displaySetsToDisplay: [ohif.sopClassHandler],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
extensions: extensionDependencies,
|
||||
hangingProtocol: tmtv.hangingProtocol,
|
||||
sopClassHandlers: [ohif.sopClassHandler],
|
||||
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
||||
...modeConfiguration,
|
||||
};
|
||||
}
|
||||
|
||||
const mode = {
|
||||
id,
|
||||
modeFactory,
|
||||
extensionDependencies,
|
||||
};
|
||||
|
||||
export default mode;
|
||||
187
modes/tmtv/src/initToolGroups.js
Normal file
187
modes/tmtv/src/initToolGroups.js
Normal file
@@ -0,0 +1,187 @@
|
||||
export const toolGroupIds = {
|
||||
CT: 'ctToolGroup',
|
||||
PT: 'ptToolGroup',
|
||||
Fusion: 'fusionToolGroup',
|
||||
MIP: 'mipToolGroup',
|
||||
default: 'default',
|
||||
};
|
||||
|
||||
function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig) {
|
||||
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.StackScroll,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
|
||||
},
|
||||
],
|
||||
passive: [
|
||||
{ toolName: toolNames.Length },
|
||||
{
|
||||
toolName: toolNames.ArrowAnnotate,
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) => {
|
||||
if (modeLabelConfig) {
|
||||
callback(' ');
|
||||
} else {
|
||||
commandsManager.runCommand('arrowTextCallback', {
|
||||
callback,
|
||||
eventDetails,
|
||||
});
|
||||
}
|
||||
},
|
||||
changeTextCallback: (data, eventDetails, callback) => {
|
||||
if (modeLabelConfig === undefined) {
|
||||
commandsManager.runCommand('arrowTextCallback', {
|
||||
callback,
|
||||
data,
|
||||
eventDetails,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{ toolName: toolNames.Bidirectional },
|
||||
{ toolName: toolNames.DragProbe },
|
||||
{ toolName: toolNames.Probe },
|
||||
{ toolName: toolNames.EllipticalROI },
|
||||
{ toolName: toolNames.RectangleROI },
|
||||
{ toolName: toolNames.StackScroll },
|
||||
{ toolName: toolNames.Angle },
|
||||
{ toolName: toolNames.CobbAngle },
|
||||
{ toolName: toolNames.Magnify },
|
||||
{
|
||||
toolName: 'CircularBrush',
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'FILL_INSIDE_CIRCLE',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'CircularEraser',
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'ERASE_INSIDE_CIRCLE',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'SphereBrush',
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'FILL_INSIDE_SPHERE',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'SphereEraser',
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'ERASE_INSIDE_SPHERE',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'ThresholdCircularBrush',
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'ThresholdSphereBrush',
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'THRESHOLD_INSIDE_SPHERE',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'ThresholdCircularBrushDynamic',
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
|
||||
// preview: {
|
||||
// enabled: true,
|
||||
// },
|
||||
strategySpecificConfiguration: {
|
||||
// to use the use the center segment index to determine
|
||||
// if inside -> same segment, if outside -> eraser
|
||||
// useCenterSegmentIndex: true,
|
||||
THRESHOLD: {
|
||||
isDynamic: true,
|
||||
dynamicRadius: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
enabled: [],
|
||||
disabled: [
|
||||
{
|
||||
toolName: toolNames.Crosshairs,
|
||||
configuration: {
|
||||
disableOnPassive: true,
|
||||
autoPan: {
|
||||
enabled: false,
|
||||
panSize: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupIds.CT, tools);
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupIds.PT, {
|
||||
active: tools.active,
|
||||
passive: [...tools.passive, { toolName: 'RectangleROIStartEndThreshold' }],
|
||||
enabled: tools.enabled,
|
||||
disabled: tools.disabled,
|
||||
});
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupIds.Fusion, tools);
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupIds.default, tools);
|
||||
|
||||
const mipTools = {
|
||||
active: [
|
||||
{
|
||||
toolName: toolNames.VolumeRotate,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
|
||||
configuration: {
|
||||
rotateIncrementDegrees: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: toolNames.MipJumpToClick,
|
||||
configuration: {
|
||||
toolGroupId: toolGroupIds.PT,
|
||||
},
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||
},
|
||||
],
|
||||
enabled: [
|
||||
{
|
||||
toolName: toolNames.OrientationMarker,
|
||||
configuration: {
|
||||
orientationWidget: {
|
||||
viewportCorner: 'BOTTOM_LEFT',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupIds.MIP, mipTools);
|
||||
}
|
||||
|
||||
function initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig) {
|
||||
_initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig);
|
||||
}
|
||||
|
||||
export default initToolGroups;
|
||||
284
modes/tmtv/src/toolbarButtons.js
Normal file
284
modes/tmtv/src/toolbarButtons.js
Normal file
@@ -0,0 +1,284 @@
|
||||
import { ToolbarService } from '@ohif/core';
|
||||
import { toolGroupIds } from './initToolGroups';
|
||||
|
||||
const setToolActiveToolbar = {
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: [toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion],
|
||||
},
|
||||
};
|
||||
|
||||
const toolbarButtons = [
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'Length',
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
tooltip: 'Length Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
tooltip: 'More Measure Tools',
|
||||
},
|
||||
items: [
|
||||
ToolbarService.createButton({
|
||||
id: 'Bidirectional',
|
||||
icon: 'tool-bidirectional',
|
||||
label: 'Bidirectional',
|
||||
tooltip: 'Bidirectional Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'ArrowAnnotate',
|
||||
icon: 'tool-annotate',
|
||||
label: 'Arrow Annotate',
|
||||
tooltip: 'Arrow Annotate Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'EllipticalROI',
|
||||
icon: 'tool-ellipse',
|
||||
label: 'Ellipse',
|
||||
tooltip: 'Ellipse Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Zoom',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Window Level + Presets
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Crosshairs Button
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Pan Button
|
||||
{
|
||||
id: 'Pan',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Rectangle ROI Start End Threshold Button
|
||||
{
|
||||
id: 'RectangleROIStartEndThreshold',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-create-threshold',
|
||||
label: 'Rectangle ROI Threshold',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: [
|
||||
'evaluate.cornerstone.segmentation',
|
||||
// need to put the disabled text last, since each evaluator will
|
||||
// merge the result text into the final result
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool',
|
||||
disabledText: 'Select the PT Axial to enable this tool',
|
||||
},
|
||||
],
|
||||
options: 'tmtv.RectangleROIThresholdOptions',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.buttonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
items: [
|
||||
{
|
||||
id: 'Brush',
|
||||
icon: 'icon-tool-brush',
|
||||
label: 'Brush',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircularBrush', 'SphereBrush'],
|
||||
disabledText: 'Create new segmentation to enable this tool.',
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'brush-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
step: 0.5,
|
||||
value: 25,
|
||||
commands: {
|
||||
commandName: 'setBrushSize',
|
||||
commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shape',
|
||||
type: 'radio',
|
||||
id: 'brush-mode',
|
||||
value: 'CircularBrush',
|
||||
values: [
|
||||
{ value: 'CircularBrush', label: 'Circle' },
|
||||
{ value: 'SphereBrush', label: 'Sphere' },
|
||||
],
|
||||
commands: 'setToolActiveToolbar',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Eraser',
|
||||
icon: 'icon-tool-eraser',
|
||||
label: 'Eraser',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircularEraser', 'SphereEraser'],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'eraser-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
step: 0.5,
|
||||
value: 25,
|
||||
commands: {
|
||||
commandName: 'setBrushSize',
|
||||
commandOptions: { toolNames: ['CircularEraser', 'SphereEraser'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shape',
|
||||
type: 'radio',
|
||||
id: 'eraser-mode',
|
||||
value: 'CircularEraser',
|
||||
values: [
|
||||
{ value: 'CircularEraser', label: 'Circle' },
|
||||
{ value: 'SphereEraser', label: 'Sphere' },
|
||||
],
|
||||
commands: 'setToolActiveToolbar',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Threshold',
|
||||
icon: 'icon-tool-threshold',
|
||||
label: 'Threshold Tool',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'threshold-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
step: 0.5,
|
||||
value: 25,
|
||||
commands: {
|
||||
commandName: 'setBrushSize',
|
||||
commandOptions: {
|
||||
toolNames: [
|
||||
'ThresholdCircularBrush',
|
||||
'ThresholdSphereBrush',
|
||||
'ThresholdCircularBrushDynamic',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Threshold',
|
||||
type: 'radio',
|
||||
id: 'dynamic-mode',
|
||||
value: 'ThresholdRange',
|
||||
values: [
|
||||
{ value: 'ThresholdDynamic', label: 'Dynamic' },
|
||||
{ value: 'ThresholdRange', label: 'Range' },
|
||||
],
|
||||
commands: ({ value, commandsManager }) => {
|
||||
if (value === 'ThresholdDynamic') {
|
||||
commandsManager.run('setToolActive', {
|
||||
toolName: 'ThresholdCircularBrushDynamic',
|
||||
});
|
||||
} else {
|
||||
commandsManager.run('setToolActive', {
|
||||
toolName: 'ThresholdCircularBrush',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shape',
|
||||
type: 'radio',
|
||||
id: 'eraser-mode',
|
||||
value: 'ThresholdCircularBrush',
|
||||
values: [
|
||||
{ value: 'ThresholdCircularBrush', label: 'Circle' },
|
||||
{ value: 'ThresholdSphereBrush', label: 'Sphere' },
|
||||
],
|
||||
condition: ({ options }) =>
|
||||
options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange',
|
||||
commands: 'setToolActiveToolbar',
|
||||
},
|
||||
{
|
||||
name: 'ThresholdRange',
|
||||
type: 'double-range',
|
||||
id: 'threshold-range',
|
||||
min: 0,
|
||||
max: 50,
|
||||
step: 0.5,
|
||||
value: [2.5, 50],
|
||||
condition: ({ options }) =>
|
||||
options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange',
|
||||
commands: {
|
||||
commandName: 'setThresholdRange',
|
||||
commandOptions: {
|
||||
toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default toolbarButtons;
|
||||
33
modes/tmtv/src/utils/setCrosshairsConfiguration.js
Normal file
33
modes/tmtv/src/utils/setCrosshairsConfiguration.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { toolGroupIds } from '../initToolGroups';
|
||||
|
||||
export default function setCrosshairsConfiguration(
|
||||
matches,
|
||||
toolNames,
|
||||
toolGroupService,
|
||||
displaySetService
|
||||
) {
|
||||
const matchDetails = matches.get('ctDisplaySet');
|
||||
|
||||
if (!matchDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { SeriesInstanceUID } = matchDetails;
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
|
||||
const toolConfig = toolGroupService.getToolConfiguration(
|
||||
toolGroupIds.Fusion,
|
||||
toolNames.Crosshairs
|
||||
);
|
||||
|
||||
const crosshairsConfig = {
|
||||
...toolConfig,
|
||||
filterActorUIDsToSetSlabThickness: [displaySets[0].displaySetInstanceUID],
|
||||
};
|
||||
|
||||
toolGroupService.setToolConfiguration(
|
||||
toolGroupIds.Fusion,
|
||||
toolNames.Crosshairs,
|
||||
crosshairsConfig
|
||||
);
|
||||
}
|
||||
61
modes/tmtv/src/utils/setFusionActiveVolume.js
Normal file
61
modes/tmtv/src/utils/setFusionActiveVolume.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { toolGroupIds } from '../initToolGroups';
|
||||
|
||||
export default function setFusionActiveVolume(
|
||||
matches,
|
||||
toolNames,
|
||||
toolGroupService,
|
||||
displaySetService
|
||||
) {
|
||||
const matchDetails = matches.get('ptDisplaySet');
|
||||
const matchDetails2 = matches.get('ctDisplaySet');
|
||||
|
||||
if (!matchDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { SeriesInstanceUID } = matchDetails;
|
||||
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
|
||||
if (!displaySets || displaySets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wlToolConfig = toolGroupService.getToolConfiguration(
|
||||
toolGroupIds.Fusion,
|
||||
toolNames.WindowLevel
|
||||
);
|
||||
|
||||
const ellipticalToolConfig = toolGroupService.getToolConfiguration(
|
||||
toolGroupIds.Fusion,
|
||||
toolNames.EllipticalROI
|
||||
);
|
||||
|
||||
// Todo: this should not take into account the loader id
|
||||
const volumeId = `cornerstoneStreamingImageVolume:${displaySets[0].displaySetInstanceUID}`;
|
||||
const { SeriesInstanceUID: SeriesInstanceUID2 } = matchDetails2;
|
||||
const ctDisplaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID2);
|
||||
const ctVolumeId = `cornerstoneStreamingImageVolume:${ctDisplaySets[0].displaySetInstanceUID}`;
|
||||
|
||||
const windowLevelConfig = {
|
||||
...wlToolConfig,
|
||||
volumeId: ctVolumeId,
|
||||
};
|
||||
|
||||
const ellipticalROIConfig = {
|
||||
...ellipticalToolConfig,
|
||||
volumeId,
|
||||
};
|
||||
|
||||
toolGroupService.setToolConfiguration(
|
||||
toolGroupIds.Fusion,
|
||||
toolNames.WindowLevel,
|
||||
windowLevelConfig
|
||||
);
|
||||
|
||||
toolGroupService.setToolConfiguration(
|
||||
toolGroupIds.Fusion,
|
||||
toolNames.EllipticalROI,
|
||||
ellipticalROIConfig
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user