feat: cornerstone3D stack and volume viewports (#2787)

* feat: cs3d working stack viewport and tools (#19)

* Squashed everything

* fix weird react issue

* fix eslint / prettier stuff

* thumbnails work now with cpu rendering

* feat: use new loadImageToCanvas in cs3d

* make jump to measurement work

* fix active thumbnail

* fix measurement delete

* fix window level presets

* remove segmentation and sync groups for now

* fix the dicom pdf and dicom video

* fix cornerstone window assignment for cypress

* apply review comments

Co-authored-by: Erik <erik.sweed@gmail.com>

* feat: cs3d tools and toolGroups (#20)

* add more tools to work with cs3d mode

* fix hotkeys

* add stack manager usage for stach viewports

* add image scrollbar

* wip viewport overlay

* fix toAnnotation schema for tools

* fix the unnecessary size change that triggered resize

* hanging protocol improvement to allow unmatched errors

* study description matching for hanging protocol

* fix the displaysetOptions to work

* fix handle the active tool when a new viewport is added

* fix separate toolGroups for mode

* apply review comments

* apply review comments

* yarn lock

* feat: overlay component (#21)

* fix default displayset options

* add viewport overlay

* apply review comments

* feat: loading and orientation indicators (#22)

* add loading indicator

* add orientation marker initial work

* apply review comments

* fix: orientation markers (#23)

* finished the orientation markers

* fix various broken cypress tests

* apply review comments

* update yarn lock

* feat: re-working measurement tracking mode with cornerstone3d (#2805)

* feat: cs3d working stack viewport and tools (#19)

* Squashed everything

* fix weird react issue

* fix eslint / prettier stuff

* thumbnails work now with cpu rendering

* feat: use new loadImageToCanvas in cs3d

* make jump to measurement work

* fix active thumbnail

* fix measurement delete

* fix window level presets

* remove segmentation and sync groups for now

* fix the dicom pdf and dicom video

* fix cornerstone window assignment for cypress

* apply review comments

Co-authored-by: Erik <erik.sweed@gmail.com>

* feat: Add Measurement tracking mode with cs3D (#2789)

* feat: first render for cornerstone3d tracked viewport

* make tool active work

* wip for SR extension

* renamed dicom sr to cornerstone dicom sr

* remove cornerstone from dicom pdf and video

* move dicom sr logic to sr extension

* feat: Add hydration for the length tool

* fix SR display tool for length using cs3d

* fix default config

* fix: various bugs with sr viewport and tracking

* fix promptying to continue tracking for when SR is created

* feat: add keep trackign of unique identifiers

* fix hydration for same imageIds

* feat: Add SR toolGroup creation on modeEnter

* feat: remove the need for separate mapper for SR hydration

* add SR display for ellipse

* handle hydration of elliptical ROI tool

* remove cornerstone extension

* add arrow mapping

* feat: Add ArrowAnnotate SR display and hydration

* apply review comments

* apply review comments

* move viewport labels to the viewportData

* apply review comments

* fix: integration cypress tests with cornerstone3D and add CINE tool (#2795)

* fix: integration cypress tests with cornerstone3D

* revert to addOrUpdate as it makes more sense

* fix local drag and drop

* fix tests

* move dicomLoaderService to cornerstone extension

* fix various import bugs

* fix bug for local PT series

* fix various unit tests

* bump cs3d versions

* add angle and magnify tool

* bump dependencies to avoid broken peerDeps

* feat: add initial work for capture using cs3d

* feat: show annotations on the image capture

* feat: add svg layer export

* feat: Add CINE Tool

* feat: remove unnecessary viewport rendering for cine state changes

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>

* docs: modify and improve documentation (#2800)

* cleanup docs versionings

* feat: Add all version explanations

* version docs for 3.0

* wip for changing docs

* wip for updated docs

* add utility module documentation

* fix demo with nohoisting of history

* add slides and video to resources

* apply review comments

* fix: drag and drop SR into SR viewport (#2803)

* update yarn lock

Co-authored-by: Erik <erik.sweed@gmail.com>

* update pathnames to match v3-stable

* update the e2e pathname

* fix: various bugs with regard to tracking workflow (#2811)

* fix: various issues with measurement panel

* fix: update default tool style for annotations

* fix: annotatoin label getting removed

* feat: Add backward compatibility for SR hydration with legacy cornerstone

* fix: cursors and ellipse ROI max style

* fix: ArrowAnnotate SRDisplay

* apply review comments

* bump package versions

* fix: bugin rehydration of SR

* fix: e2e tests

* fix active viewport thickness and arrowTool ui

* add readme for measurement tracking

* use uploaded image for readme

* add back images

* try to fix e2e test

* fix: window level presets hotkeys

* Update README.md

* update yarn lock

* feat: volume api and TMTV mode (#2817)

* feat: volumeAPI and TMTV mode

* feat: use cs3d cache service to obtain viewportData

* wip for volume api

* wip: fusion viewport

* feat: add blend mode option

* wip for image scrollbar

* fix drag and drop thumbnail

* wip for image scrollbar for voluems

* fix: element mismatch bug for scroll

* feat: Add image scrollbar to volumes

* feat: add syncGroups to volume api

* feat: add tmtv mode initial setup

* feat: add initial image options for the stack viewports

* feat: Add custom load strategy for volume viewports via HP

* apply review comments

fix: Jump presets cs3d (#2812)

* feat: Add JumpPreset to OHIF for Cornerstone3D

* fix: accessing viewport service from servicesManager

feat: volume API and TMTV mode (#2814)

* fix: do not display overlays on mip viewports

* feat: add optional disableCommands for toggle buttons

* fix: toggleCrossharis

* feat: config the crosshairs

* feat: add PetSUV Panel for changing metadata

* feat: initial work for the rectangleROIThreshold panel

* wip: measurement service

* roi threshold working

* feat: Add displayText to segmentations

* feat: add remove segmentation

* feat: add csv export

* feat: add RT export for annotations in tmtv mode

* fix: fusion to use pt in tmtv mode and measuremet mappings

* fix: various bugs

* apply review comments

* apply review comments

* feat: add fusion color maps

* add readme to tmtv mode

* Update README.md

* fix: try to fix build

* fix unit tests

* Update README.md

* feat: add about to the panel

* fix: changing strategy in roi panel

* apply review comments

* update package versions

* wip for stackPrefetch

* fix: cornerstone3d hydration and renaming (#2818)

* update readme

* renamed cornerstone extension

* wip for renaming cornerstone3D variables

* wip for renaming cornerstone3D variables

* wip for fixing bugs for SR viewport

* fix: jumpToMeasurement and initial label after hydration

* fix: fileName capitalization

* feat: use the new prefetch stack in the cs3d (#2820)

* feat: use the new prefetch stack in the cs3d

* use viewport scroll api for stack viewport

* fix cine stop when scrollbar changes

* feat: use new prefetch events

* fix loading state to not show repeatedly

* fix: various bugs for tmtv mode thresholding and new icons (#2823)

* feat: make tmtv mode available in worklist

* feat: add new icons for tmtv mode

* feat: add fusion color icon

* fix: parallel scale calculation

* fix: bump Cornerstone to get large image support working

* fix: Fix issues with CPU viewport flipping, including config files

* fix: bump cornerstone to fix magnify tool

* fix: Bump Cornerstone version to fix resetCamera issue in StackViewport

* ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys

* fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors

* feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838)

Also works with the compile time add that the existing cli uses, so that both build types work.
Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes.

* bump: dependency versions to fix hydration bugs (#2848)

* bump: dcmjs version to fix hydration bugs

* try to fix tests

* bump dependency versions

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
Erik Ziegler 2022-07-27 16:39:04 +00:00 committed by GitHub
parent 86dcaf6293
commit bd1c38c00c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
668 changed files with 41669 additions and 13410 deletions

View File

@ -1,11 +1,18 @@
{
"plugins": [
"@typescript-eslint",
"import",
"eslint-plugin-tsdoc",
"prettier"
],
"extends": [
"react-app",
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parser": "babel-eslint",
"parser": "@typescript-eslint/parser",
"env": {
"jest": true
},

13
.vscode/launch.json vendored
View File

@ -11,5 +11,18 @@
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
// {
// "name": "Debug Jest Tests",
// "type": "node",
// "request": "launch",
// "runtimeArgs": [
// "--inspect-brk",
// "${workspaceRoot}/node_modules/.bin/jest",
// "--runInBand"
// ],
// "console": "integratedTerminal",
// "internalConsoleOptions": "neverOpen",
// "port": 9229
// }
]
}

View File

@ -4,7 +4,6 @@ function transpileJavaScript(mode) {
const exclude =
mode === 'production'
? excludeNodeModulesExcept([
'vtk.js',
// 'dicomweb-client',
// https://github.com/react-dnd/react-dnd/blob/master/babel.config.js
'react-dnd',
@ -21,7 +20,8 @@ function transpileJavaScript(mode) {
: excludeNodeModulesExcept([]);
return {
test: /\.(mjs|js|jsx)?$/,
// Include mjs, ts, tsx, js, and jsx files.
test: /\.(mjs|ts|js)x?$/,
// These are packages that are not transpiled to our lowest supported
// JS version (currently ES5). Most of these leverage ES6+ features,
// that we need to transpile to a different syntax.

View File

@ -12,6 +12,8 @@ const CopyPlugin = require('copy-webpack-plugin');
// ~~ PackageJSON
const PACKAGE = require('../platform/viewer/package.json');
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
// .rules;
// ~~ RULES
const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
@ -66,11 +68,20 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
children: false,
warnings: true,
},
devServer: {
open: true,
port: 3000,
historyApiFallback: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
module: {
rules: [
transpileJavaScriptRule(mode),
loadWebWorkersRule,
loadShadersRule,
// loadShadersRule,
{
test: /\.m?js/,
resolve: {
@ -78,7 +89,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
},
},
cssToJavaScript,
],
], //.concat(vtkRules),
},
resolve: {
mainFields: ['module', 'browser', 'main'],
@ -106,7 +117,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
SRC_DIR,
],
// Attempt to resolve these extensions in order.
extensions: ['.js', '.jsx', '.json', '*'],
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
// symlinked resources are resolved to their real path, not their symlinked location
symlinks: true,
fallback: { fs: false, path: false, zlib: false },

View File

@ -190,13 +190,14 @@ you'll see the following:
├── extensions #
│ ├── _example # Skeleton of example extension
│ ├── default #
│ ├── cornerstone # 2D images w/ Cornerstone.js
│ ├── dicom-sr #
│ ├── cornerstone # image rendering and tools w/ Cornerstone
│ ├── cornerstone- dicom-sr #
│ └── measurement-tracking #
├── modes #
│ ├── _example # Skeleton of example mode
│ └── longitudinal #
│ ├── basic-dev-mode # Basic development mode
│ └── longitudinal # Longitudinal mode (measurement tracking)
├── platform #
│ ├── core # Business Logic

View File

@ -1,10 +1,16 @@
const aliases = require('./aliases.config');
const path = require('path');
// https://babeljs.io/docs/en/options#babelrcroots
module.exports = {
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'inline-react-svg',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-typescript',
],
env: {
test: {
presets: [
@ -17,12 +23,14 @@ module.exports = {
},
],
'@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: {
@ -30,6 +38,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
@ -38,6 +47,7 @@ module.exports = {
// 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__'],

View File

@ -14,7 +14,6 @@ module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,

View File

@ -1,12 +1,12 @@
{
"name": "@ohif/extension-dicom-sr",
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.0.0",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
@ -34,21 +34,18 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"cornerstone-core": "^2.6.0",
"cornerstone-math": "^0.1.9",
"cornerstone-tools": "6.0.2",
"cornerstone-wado-image-loader": "4.0.4",
"dcmjs": "0.16.1",
"dcmjs": "^0.24.5",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-cornerstone-viewport": "4.1.2",
"@ohif/extension-cornerstone": "^3.0.0",
"@ohif/extension-measurement-tracking": "^3.0.0"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"classnames": "^2.2.6"
"classnames": "^2.2.6",
"@cornerstonejs/core": "^0.13.11",
"@cornerstonejs/tools": "^0.20.15"
}
}

View File

@ -0,0 +1,143 @@
import { metaData, utilities } from '@cornerstonejs/core';
import OHIF from '@ohif/core';
import dcmjs from 'dcmjs';
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
const { MeasurementReport } = dcmjs.adapters.Cornerstone3D;
const { log } = OHIF;
/**
*
* @param measurementData An array of measurements from the measurements service
* that you wish to serialize.
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
*
*/
const _generateReport = (
measurementData,
additionalFindingTypes,
options = {}
) => {
const filteredToolState = getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
);
const report = MeasurementReport.generateReport(
filteredToolState,
metaData,
utilities.worldToImageCoords
);
const { dataset } = report;
// Add in top level series options
Object.assign(dataset, options);
return dataset;
};
const commandsModule = ({}) => {
const actions = {
/**
*
* @param measurementData An array of measurements from the measurements service
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
* as opposed to Finding Sites.
* that you wish to serialize.
*/
downloadReport: ({
measurementData,
additionalFindingTypes,
options = {},
}) => {
const srDataset = actions.generateReport(
measurementData,
additionalFindingTypes,
options
);
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
var objectUrl = URL.createObjectURL(reportBlob);
window.location.assign(objectUrl);
},
/**
*
* @param measurementData An array of measurements from the measurements service
* that you wish to serialize.
* @param dataSource The dataSource that you wish to use to persist the data.
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
* @return The naturalized report
*/
storeMeasurements: async ({
measurementData,
dataSource,
additionalFindingTypes,
options = {},
}) => {
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
// But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements');
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
log.error(
'[DICOMSR] datasource has no dataSource.store.dicom endpoint!'
);
return Promise.reject({});
}
try {
const naturalizedReport = _generateReport(
measurementData,
additionalFindingTypes,
options
);
const { StudyInstanceUID } = naturalizedReport;
await dataSource.store.dicom(naturalizedReport);
if (StudyInstanceUID) {
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
}
return naturalizedReport;
} catch (error) {
console.warn(error);
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
throw new Error(
error.message || 'Error while saving the measurements.'
);
}
},
};
const definitions = {
downloadReport: {
commandFn: actions.downloadReport,
storeContexts: [],
options: {},
},
storeMeasurements: {
commandFn: actions.storeMeasurements,
storeContexts: [],
options: {},
},
};
return {
actions,
definitions,
defaultContext: 'CORNERSTONE_STRUCTURED_REPORT',
};
};
export default commandsModule;

View File

@ -2,9 +2,11 @@ import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes } from '@ohif/core';
import addMeasurement from './utils/addMeasurement';
import isRehydratable from './utils/isRehydratable';
import { adapters } from 'dcmjs';
const { CodeScheme: Cornerstone3DCodeScheme } = adapters.Cornerstone3D;
const { ImageSet } = classes;
// TODO ->
// Add SR thumbnail
// Make viewport
@ -16,6 +18,9 @@ const sopClassUids = [
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
];
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
@ -26,12 +31,15 @@ const CodeNameCodeSequenceValues = {
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
};
const CodingSchemeDesignators = {
SRT: 'SRT',
cornerstoneTools4: 'CST4',
CornerstoneCodeSchemes: [
Cornerstone3DCodeScheme.CodingSchemeDesignator,
'CST4',
],
};
const RELATIONSHIP_TYPE = {
@ -117,8 +125,8 @@ function _load(displaySet, servicesManager, extensionManager) {
displaySet.measurements = _getMeasurements(ContentSequence);
const mappings = MeasurementService.getSourceMappings(
'CornerstoneTools',
'4'
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
displaySet.isHydrated = false;
@ -217,7 +225,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j];
if (_measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
addMeasurement(
measurement,
imageId,
@ -231,7 +239,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
}
function _measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
const { coords } = measurement;
for (let j = 0; j < coords.length; j++) {
@ -458,8 +466,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
if (
Finding &&
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.cornerstoneTools4 &&
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
Finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
Finding.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
) {
@ -473,8 +482,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
if (FindingSites.length) {
const cornerstoneFreeTextFindingSite = FindingSites.find(
FindingSite =>
FindingSite.ConceptCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.cornerstoneTools4 &&
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
);

View File

@ -1,8 +1,10 @@
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import onModeEnter from './onModeEnter';
import commandsModule from './commandsModule';
import init from './init';
import { id } from './id.js';
import toolNames from './tools/toolNames';
const Component = React.lazy(() => {
return import(
@ -21,11 +23,12 @@ const OHIFCornerstoneSRViewport = props => {
/**
*
*/
export default {
const dicomSRExtension = {
/**
* Only required property. Should be a unique value across all extensions.
*/
id,
onModeEnter,
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
@ -50,50 +53,24 @@ export default {
return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }];
},
getCommandsModule({ servicesManager }) {
return {
definitions: {
setToolActive: {
commandFn: ({ toolName, element }) => {
if (!toolName) {
console.warn('No toolname provided to setToolActive command');
}
// Set same tool or alt tool
const toolAlias = _getToolAlias(toolName);
cornerstoneTools.setToolActiveForElement(element, toolAlias, {
mouseButtonMask: 1,
});
},
storeContexts: [],
options: {},
},
},
defaultContext: 'ACTIVE_VIEWPORT::STRUCTURED_REPORT',
};
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
return commandsModule({
servicesManager,
commandsManager,
extensionManager,
});
},
getSopClassHandlerModule,
onModeEnter,
getUtilityModule({ servicesManager }) {
return [
{
name: 'tools',
exports: {
toolNames,
},
},
];
},
};
function _getToolAlias(toolName) {
let toolAlias = toolName;
switch (toolName) {
case 'Length':
toolAlias = 'SRLength';
break;
case 'Bidirectional':
toolAlias = 'SRBidirectional';
break;
case 'ArrowAnnotate':
toolAlias = 'SRArrowAnnotate';
break;
case 'EllipticalRoi':
toolAlias = 'SREllipticalRoi';
break;
}
return toolAlias;
}
export default dicomSRExtension;

View File

@ -0,0 +1,29 @@
import { addTool, annotation } from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SRLengthTool from './tools/tools/SRLength';
import SRBidirectionalTool from './tools/tools/SRBidirectional';
import SREllipticalROITool from './tools/tools/SREllipticalROI';
import SRArrowAnnotateTool from './tools/tools/SRArrowAnnotate';
/**
* @param {object} configuration
*/
export default function init({ configuration = {} }) {
addTool(DICOMSRDisplayTool);
addTool(SRLengthTool);
addTool(SRBidirectionalTool);
addTool(SREllipticalROITool);
addTool(SRArrowAnnotateTool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
[SRLengthTool.toolName]: dashedLine,
[SRBidirectionalTool.toolName]: dashedLine,
[SREllipticalROITool.toolName]: dashedLine,
[SRArrowAnnotateTool.toolName]: dashedLine,
global: {},
});
}

View File

@ -0,0 +1,388 @@
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
import {
AnnotationTool,
annotation,
drawing,
utilities,
Types as cs3DToolsTypes,
} from '@cornerstonejs/tools';
import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule';
import SCOORD_TYPES from '../constants/scoordTypes';
export default class DICOMSRDisplayTool extends AnnotationTool {
static toolName = 'DICOMSRDisplay';
constructor(
toolProps = {},
defaultToolProps = {
configuration: {},
}
) {
super(toolProps, defaultToolProps);
}
_getTextBoxLinesFromLabels(labels) {
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 3);
const lines = [];
for (let i = 0; i < labelLength; i++) {
const labelEntry = labels[i];
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
}
return lines;
}
// This tool should not inherit from AnnotationTool and we should not need
// to add the following lines.
isPointNearTool = () => null;
getHandleNearImagePoint = () => null;
renderAnnotation = (
enabledElement: Types.IEnabledElement,
svgDrawingHelper: any
): void => {
const { viewport } = enabledElement;
const { element } = viewport;
let annotations = annotation.state.getAnnotations(
element,
this.getToolName()
);
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
if (!annotations?.length) {
return;
}
annotations = this.filterInteractableAnnotationsForElement(
element,
annotations
);
if (!annotations?.length) {
return;
}
const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(
element
);
const {
activeIndex,
trackingUniqueIdentifiers,
} = trackingUniqueIdentifiersForElement;
const activeTrackingUniqueIdentifier =
trackingUniqueIdentifiers[activeIndex];
// Filter toolData to only render the data for the active SR.
const filteredAnnotations = annotations.filter(annotation =>
trackingUniqueIdentifiers.includes(
annotation.data?.cachedStats?.TrackingUniqueIdentifier
)
);
if (!viewport._actors?.size) {
return;
}
const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = {
toolGroupId: this.toolGroupId,
toolName: this.getToolName(),
viewportId: enabledElement.viewport.id,
};
for (let i = 0; i < filteredAnnotations.length; i++) {
const annotation = filteredAnnotations[i];
const annotationUID = annotation.annotationUID;
const { renderableData } = annotation.data.cachedStats;
const { label, cachedStats } = annotation.data;
const { referencedImageId } = annotation.metadata;
styleSpecifier.annotationUID = annotationUID;
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
const color =
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
? 'rgb(0, 255, 0)'
: this.getStyle('color', styleSpecifier, annotation);
const options = {
color,
lineDash,
lineWidth,
};
Object.keys(renderableData).forEach(GraphicType => {
const renderableDataForGraphicType = renderableData[GraphicType];
let renderMethod;
switch (GraphicType) {
case SCOORD_TYPES.POINT:
renderMethod = this.renderPoint;
break;
case SCOORD_TYPES.MULTIPOINT:
renderMethod = this.renderMultipoint;
break;
case SCOORD_TYPES.POLYLINE:
renderMethod = this.renderPolyLine;
break;
case SCOORD_TYPES.CIRCLE:
renderMethod = this.renderEllipse;
break;
case SCOORD_TYPES.ELLIPSE:
renderMethod = this.renderEllipse;
break;
default:
throw new Error(`Unsupported GraphicType: ${GraphicType}`);
}
const canvasCoordinates = renderMethod(
svgDrawingHelper,
viewport,
renderableDataForGraphicType,
annotationUID,
referencedImageId,
options
);
if (!canvasCoordinates) {
return;
}
const textLines = this._getTextBoxLinesFromLabels(label);
let canvasCornersToUseForTextBox = canvasCoordinates;
if (GraphicType === SCOORD_TYPES.ELLIPSE) {
canvasCornersToUseForTextBox = utilities.math.ellipse.getCanvasEllipseCorners(
canvasCoordinates
);
}
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(
canvasCornersToUseForTextBox
);
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(
canvasTextBoxCoords
);
const textBoxPosition = viewport.worldToCanvas(
annotation.data.handles.textBox.worldPosition
);
const textBoxUID = '1';
const textBoxOptions = this.getLinkedTextBoxStyle(
styleSpecifier,
annotation
);
const boundingBox = drawing.drawLinkedTextBox(
svgDrawingHelper,
annotationUID,
textBoxUID,
textLines,
textBoxPosition,
canvasCoordinates,
{},
{
...textBoxOptions,
color,
}
);
const { x: left, y: top, width, height } = boundingBox;
annotation.data.handles.textBox.worldBoundingBox = {
topLeft: viewport.canvasToWorld([left, top]),
topRight: viewport.canvasToWorld([left + width, top]),
bottomLeft: viewport.canvasToWorld([left, top + height]),
bottomRight: viewport.canvasToWorld([left + width, top + height]),
};
});
}
};
renderPolyLine(
svgDrawingHelper,
viewport,
renderableData,
annotationUID,
referencedImageId,
options
) {
// Todo: this needs to use the drawPolyLine from cs3D since it is implemented
// now, before it was implemented with a loop over drawLine which is hacky
let canvasCoordinates;
renderableData.map((data, index) => {
canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
if (canvasCoordinates.length === 2) {
const lineUID = `${index}`;
drawing.drawLine(
svgDrawingHelper,
annotationUID,
lineUID,
canvasCoordinates[0],
canvasCoordinates[1],
{
color: options.color,
width: options.lineWidth,
}
);
} else {
throw new Error('Drawing polyline for SR not yet implemented');
}
});
return canvasCoordinates; // used for drawing textBox
}
renderMultipoint(
svgDrawingHelper,
viewport,
renderableData,
annotationUID,
referencedImageId,
options
) {
let canvasCoordinates;
renderableData.map((data, index) => {
canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
const handleGroupUID = '0';
drawing.drawHandles(
svgDrawingHelper,
annotationUID,
handleGroupUID,
canvasCoordinates,
{
color: options.color,
}
);
});
}
renderPoint(
svgDrawingHelper,
viewport,
renderableData,
annotationUID,
referencedImageId,
options
) {
const canvasCoordinates = [];
renderableData.map((data, index) => {
const point = data[0];
// This gives us one point for arrow
canvasCoordinates.push(viewport.worldToCanvas(point));
// We get the other point for the arrow by using the image size
const imagePixelModule = metaData.get(
'imagePixelModule',
referencedImageId
);
let xOffset = 10;
let yOffset = 10;
if (imagePixelModule) {
const { columns, rows } = imagePixelModule;
xOffset = columns / 10;
yOffset = rows / 10;
}
const imagePoint = csUtils.worldToImageCoords(referencedImageId, point);
const arrowEnd = csUtils.imageToWorldCoords(referencedImageId, [
imagePoint[0] + xOffset,
imagePoint[1] + yOffset,
]);
canvasCoordinates.push(viewport.worldToCanvas(arrowEnd));
const arrowUID = `${index}`;
// Todo: handle drawing probe as probe, currently we are drawing it as an arrow
drawing.drawArrow(
svgDrawingHelper,
annotationUID,
arrowUID,
canvasCoordinates[1],
canvasCoordinates[0],
{
color: options.color,
width: options.lineWidth,
}
);
});
return canvasCoordinates; // used for drawing textBox
}
renderEllipse(
svgDrawingHelper,
viewport,
renderableData,
annotationUID,
referencedImageId,
options
) {
let canvasCoordinates;
renderableData.map((data, index) => {
if (data.length === 0) {
// since oblique ellipse is not supported for hydration right now
// we just return
return;
}
const ellipsePointsWorld = data;
canvasCoordinates = ellipsePointsWorld.map(p =>
viewport.worldToCanvas(p)
);
const canvasCorners = <Array<Types.Point2>>(
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates)
);
const lineUID = `${index}`;
drawing.drawEllipse(
svgDrawingHelper,
annotationUID,
lineUID,
canvasCorners[0],
canvasCorners[1],
{
color: options.color,
width: options.lineWidth,
}
);
});
return canvasCoordinates;
}
}
const SHORT_HAND_MAP = {
'Short Axis': 'W ',
'Long Axis': 'L ',
AREA: 'Area ',
Length: '',
CORNERSTONEFREETEXT: '',
};
function _labelToShorthand(label) {
const shortHand = SHORT_HAND_MAP[label];
if (shortHand !== undefined) {
return shortHand;
}
return label;
}

View File

@ -0,0 +1,64 @@
import { getEnabledElement } from '@cornerstonejs/core';
const state = {
TrackingUniqueIdentifier: null,
trackingIdentifiersByViewportId: {},
};
/**
* This file is being used to store the per-viewport state of the SR tools,
* Since, all the toolStates are added to the cornerstoneTools, when displaying the SRTools,
* if there are two viewports rendering the same imageId, we don't want to show
* the same SR annotation twice on irrelevant viewport, hence, we are storing the state
* of the SR tools in state here, so that we can filter them later.
*/
function setTrackingUniqueIdentifiersForElement(
element,
trackingUniqueIdentifiers,
activeIndex = 0
) {
const enabledElement = getEnabledElement(element);
const { viewport } = enabledElement;
state.trackingIdentifiersByViewportId[viewport.id] = {
trackingUniqueIdentifiers,
activeIndex,
};
}
function setActiveTrackingUniqueIdentifierForElement(
element,
TrackingUniqueIdentifier
) {
const enabledElement = getEnabledElement(element);
const { viewport } = enabledElement;
const trackingIdentifiersForElement =
state.trackingIdentifiersByViewportId[viewport.id];
if (trackingIdentifiersForElement) {
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(
tuid => tuid === TrackingUniqueIdentifier
);
trackingIdentifiersForElement.activeIndex = activeIndex;
}
}
function getTrackingUniqueIdentifiersForElement(element) {
const enabledElement = getEnabledElement(element);
const { viewport } = enabledElement;
if (state.trackingIdentifiersByViewportId[viewport.id]) {
return state.trackingIdentifiersByViewportId[viewport.id];
}
return { trackingUniqueIdentifiers: [] };
}
export {
setTrackingUniqueIdentifiersForElement,
setActiveTrackingUniqueIdentifierForElement,
getTrackingUniqueIdentifiersForElement,
};

View File

@ -0,0 +1,15 @@
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
import SRLengthTool from './tools/SRLength';
import SRBidirectional from './tools/SRBidirectional';
import SREllipticalROI from './tools/SREllipticalROI';
import SRArrowAnnotate from './tools/SRArrowAnnotate';
const toolNames = {
DICOMSRDisplay: DICOMSRDisplayTool.toolName,
SRLength: SRLengthTool.toolName,
SRBidirectional: SRBidirectional.toolName,
SREllipticalROI: SREllipticalROI.toolName,
SRArrowAnnotate: SRArrowAnnotate.toolName,
};
export default toolNames;

View File

@ -0,0 +1,16 @@
import { ArrowAnnotateTool } from '@cornerstonejs/tools';
/**
* The reason we are extending ArrowAnnotateTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have ArrowAnnotate
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
* the SR annotation to the MeasurementService (since there is a ArrowAnnotateTool mapper),
* but with extending and renaming it, there is not mapper for SRArrowAnnotateTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRArrowAnnotateTool extends ArrowAnnotateTool {
static toolName = 'SRArrowAnnotate';
}
export default SRArrowAnnotateTool;

View File

@ -0,0 +1,16 @@
import { BidirectionalTool } from '@cornerstonejs/tools';
/**
* The reason we are extending BidirectionalTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have Bidirectional
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
* the SR annotation to the MeasurementService (since there is a BidirectionalTool mapper),
* but with extending and renaming it, there is not mapper for SRBidirectionalTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRBidirectional extends BidirectionalTool {
static toolName = 'SRBidirectional';
}
export default SRBidirectional;

View File

@ -0,0 +1,16 @@
import { EllipticalROITool } from '@cornerstonejs/tools';
/**
* The reason we are extending EllipticalROITool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have EllipticalROI
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
* the SR annotation to the MeasurementService (since there is a EllipticalROITool mapper),
* but with extending and renaming it, there is not mapper for SREllipticalROITool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SREllipticalROI extends EllipticalROITool {
static toolName = 'SREllipticalROI';
}
export default SREllipticalROI;

View File

@ -0,0 +1,16 @@
import { LengthTool } from '@cornerstonejs/tools';
/**
* The reason we are extending LengthTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have Length
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
* the SR annotation to the MeasurementService (since there is a LengthTool mapper),
* but with extending and renaming it, there is not mapper for SRLengthTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRLengthTool extends LengthTool {
static toolName = 'SRLength';
}
export default SRLengthTool;

View File

@ -0,0 +1,244 @@
import { vec3 } from 'gl-matrix';
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
import toolNames from '../tools/toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
const EPSILON = 1e-4;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
export default function addMeasurement(
measurement,
imageId,
displaySetInstanceUID
) {
// TODO -> Render rotated ellipse .
const toolName = toolNames.DICOMSRDisplay;
const measurementData = {
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
renderableData: {},
labels: measurement.labels,
imageId,
};
measurement.coords.forEach(coord => {
const { GraphicType, GraphicData } = coord;
if (measurementData.renderableData[GraphicType] === undefined) {
measurementData.renderableData[GraphicType] = [];
}
measurementData.renderableData[GraphicType].push(
_getRenderableData(
GraphicType,
GraphicData,
imageId,
measurement.TrackingIdentifier
)
);
});
// Use the metadata provider to grab its imagePlaneModule metadata
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
const annotationManager = annotation.state.getDefaultAnnotationManager();
const SRAnnotation: Types.Annotation = {
annotationUID: measurement.TrackingUniqueIdentifier,
metadata: {
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
toolName: toolName,
referencedImageId: imageId,
},
data: {
label: measurement.labels,
handles: {
textBox: {},
},
cachedStats: {
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
},
},
};
annotationManager.addAnnotation(SRAnnotation);
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = displaySetInstanceUID;
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
// It'd be super weird if it didn't anyway as a SCOORD.
measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
delete measurement.coords;
}
function _getRenderableData(
GraphicType,
GraphicData,
imageId,
TrackingIdentifier
) {
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
let renderableData: csTypes.Point3[];
switch (GraphicType) {
case SCOORD_TYPES.POINT:
case SCOORD_TYPES.MULTIPOINT:
case SCOORD_TYPES.POLYLINE:
renderableData = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
renderableData.push(worldPos);
}
break;
case SCOORD_TYPES.CIRCLE: {
const pointsWorld = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
pointsWorld.push(worldPos);
}
// We do not have an explicit draw circle svg helper in Cornerstone3D at
// this time, but we can use the ellipse svg helper to draw a circle, so
// here we reshape the data for that purpose.
const center = pointsWorld[0];
const onPerimeter = pointsWorld[1];
const radius = vec3.distance(center, onPerimeter);
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
if (!imagePlaneModule) {
throw new Error('No imagePlaneModule found');
}
const {
columnCosines,
rowCosines,
}: {
columnCosines: csTypes.Point3;
rowCosines: csTypes.Point3;
} = imagePlaneModule;
// we need to get major/minor axis (which are both the same size major = minor)
// first axisStart
const firstAxisStart = vec3.create();
vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius);
const firstAxisEnd = vec3.create();
vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius);
// second axisStart
const secondAxisStart = vec3.create();
vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius);
const secondAxisEnd = vec3.create();
vec3.scaleAndAdd(secondAxisEnd, center, rowCosines, -radius);
renderableData = [
firstAxisStart as csTypes.Point3,
firstAxisEnd as csTypes.Point3,
secondAxisStart as csTypes.Point3,
secondAxisEnd as csTypes.Point3,
];
break;
}
case SCOORD_TYPES.ELLIPSE: {
// GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY]
// But Cornerstone3D points are ordered as top, bottom, left, right for the
// ellipse so we need to identify if the majorAxis is horizontal or vertical
// and then choose the correct points to use for the ellipse.
const pointsWorld: csTypes.Point3[] = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
pointsWorld.push(worldPos);
}
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
const minorAxisStart = vec3.fromValues(...pointsWorld[2]);
const minorAxisEnd = vec3.fromValues(...pointsWorld[3]);
const majorAxisVec = vec3.create();
vec3.sub(majorAxisVec, majorAxisEnd, majorAxisStart);
// normalize majorAxisVec to avoid scaling issues
vec3.normalize(majorAxisVec, majorAxisVec);
const minorAxisVec = vec3.create();
vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart);
vec3.normalize(minorAxisVec, minorAxisVec);
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
if (!imagePlaneModule) {
throw new Error('imageId does not have imagePlaneModule metadata');
}
const {
columnCosines,
}: { columnCosines: csTypes.Point3 } = imagePlaneModule;
// find which axis is parallel to the columnCosines
const columnCosinesVec = vec3.fromValues(...columnCosines);
const projectedMajorAxisOnColVec = Math.abs(
vec3.dot(columnCosinesVec, majorAxisVec)
);
const projectedMinorAxisOnColVec = Math.abs(
vec3.dot(columnCosinesVec, minorAxisVec)
);
const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec);
const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec);
renderableData = [];
if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) {
renderableData = [
pointsWorld[0],
pointsWorld[1],
pointsWorld[2],
pointsWorld[3],
];
} else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) {
renderableData = [
pointsWorld[2],
pointsWorld[3],
pointsWorld[0],
pointsWorld[1],
];
} else {
console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED');
}
break;
}
default:
console.warn('Unsupported GraphicType:', GraphicType);
}
return renderableData;
}

View File

@ -0,0 +1,110 @@
import OHIF from '@ohif/core';
import { annotation } from '@cornerstonejs/tools';
const { log } = OHIF;
function getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
) {
const filteredToolState = {};
function addToFilteredToolState(annotation, toolType) {
if (!annotation.metadata?.referencedImageId) {
log.warn(
`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`
);
return;
}
const imageId = annotation.metadata.referencedImageId;
if (!filteredToolState[imageId]) {
filteredToolState[imageId] = {};
}
const imageIdSpecificToolState = filteredToolState[imageId];
if (!imageIdSpecificToolState[toolType]) {
imageIdSpecificToolState[toolType] = {
data: [],
};
}
const measurementDataI = measurementData.find(
md => md.uid === annotation.annotationUID
);
const toolData = imageIdSpecificToolState[toolType].data;
let finding;
const findingSites = [];
// NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have
// defined in the dcmjs adapters
if (measurementDataI.label) {
if (additionalFindingTypes.includes(toolType)) {
finding = {
CodeValue: 'CORNERSTONEFREETEXT',
CodingSchemeDesignator: 'CORNERSTONEJS',
CodeMeaning: measurementDataI.label,
};
} else {
findingSites.push({
CodeValue: 'CORNERSTONEFREETEXT',
CodingSchemeDesignator: 'CORNERSTONEJS',
CodeMeaning: measurementDataI.label,
});
}
}
const measurement = Object.assign({}, annotation, {
finding,
findingSites,
});
toolData.push(measurement);
}
const uidFilter = measurementData.map(md => md.uid);
const uids = uidFilter.slice();
const annotationManager = annotation.state.getDefaultAnnotationManager();
const framesOfReference = annotationManager.getFramesOfReference();
for (let i = 0; i < framesOfReference.length; i++) {
const frameOfReference = framesOfReference[i];
const frameOfReferenceAnnotations = annotationManager.getFrameOfReferenceAnnotations(
frameOfReference
);
const toolTypes = Object.keys(frameOfReferenceAnnotations);
for (let j = 0; j < toolTypes.length; j++) {
const toolType = toolTypes[j];
const annotations = frameOfReferenceAnnotations[toolType];
if (annotations) {
for (let k = 0; k < annotations.length; k++) {
const annotation = annotations[k];
const uidIndex = uids.findIndex(
uid => uid === annotation.annotationUID
);
if (uidIndex !== -1) {
addToFilteredToolState(annotation, toolType);
uids.splice(uidIndex, 1);
if (!uids.length) {
return filteredToolState;
}
}
}
}
}
}
return filteredToolState;
}
export default getFilteredCornerstoneToolState;

View File

@ -1,6 +1,9 @@
import { adapters } from 'dcmjs';
const cornerstoneAdapters = adapters.Cornerstone;
const cornerstoneAdapters = adapters.Cornerstone3D;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
/**
* Checks if the given `displaySet`can be rehydrated into the `MeasurementService`.
@ -14,7 +17,7 @@ export default function isRehydratable(displaySet, mappings) {
return false;
}
const mappingDefinitions = mappings.map(m => m.definition);
const mappingDefinitions = mappings.map(m => m.annotationType);
const { measurements } = displaySet;
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
@ -35,9 +38,18 @@ export default function isRehydratable(displaySet, mappings) {
for (let i = 0; i < measurements.length; i++) {
const TrackingIdentifier = measurements[i].TrackingIdentifier;
const hydratable = adapters.some(adapter =>
adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier)
);
const hydratable = adapters.some(adapter => {
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
cornerstoneTag = CORNERSTONE_3D_TAG;
}
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
return adapter.isValidCornerstoneTrackingIdentifier(
mappedTrackingIdentifier
);
});
if (hydratable) {
return true;

View File

@ -7,7 +7,7 @@ import dcmjs from 'dcmjs';
* @returns {boolean}
*/
const isToolSupported = toolName => {
const adapter = dcmjs.adapters.Cornerstone;
const adapter = dcmjs.adapters.Cornerstone3D;
return !!adapter[toolName];
};

View File

@ -0,0 +1,452 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import {
Notification,
ViewportActionBar,
useViewportGrid,
useViewportDialog,
} from '@ohif/ui';
const { formatDate } = utils;
const MEASUREMENT_TRACKING_EXTENSION_ID =
'@ohif/extension-measurement-tracking';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRViewport(props) {
const {
children,
dataSource,
displaySets,
viewportIndex,
viewportLabel,
servicesManager,
extensionManager,
} = props;
const {
DisplaySetService,
CornerstoneViewportService,
} = servicesManager.services;
// SR viewport will always have a single display set
if (displaySets.length > 1) {
throw new Error('SR viewport should only have a single display set');
}
const srDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
const [measurementSelected, setMeasurementSelected] = useState(0);
const [measurementCount, setMeasurementCount] = useState(1);
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(
null
);
const [
referencedDisplaySetMetadata,
setReferencedDisplaySetMetadata,
] = useState(null);
const [isHydrated, setIsHydrated] = useState(srDisplaySet.isHydrated);
const [element, setElement] = useState(null);
const { viewports, activeViewportIndex } = viewportGrid;
// Optional hook into tracking extension, if present.
let trackedMeasurements;
let sendTrackedMeasurementsEvent;
const hasMeasurementTrackingExtension = extensionManager.registeredExtensionIds.includes(
MEASUREMENT_TRACKING_EXTENSION_ID
);
// TODO: this is a hook that fails if we register/de-register
if (hasMeasurementTrackingExtension) {
const contextModule = extensionManager.getModuleEntry(
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
);
const useTrackedMeasurements = () => useContext(contextModule.context);
[
trackedMeasurements,
sendTrackedMeasurementsEvent,
] = useTrackedMeasurements();
}
/**
* Store the tracking identifiers per viewport in order to be able to
* show the SR measurements on the referenced image on the correct viewport,
* when multiple viewports are used.
*/
const setTrackingIdentifiers = useCallback(
measurementSelected => {
const { measurements } = srDisplaySet;
setTrackingUniqueIdentifiersForElement(
element,
measurements.map(measurement => measurement.TrackingUniqueIdentifier),
measurementSelected
);
},
[element, measurementSelected, srDisplaySet]
);
/**
* OnElementEnabled callback which is called after the cornerstoneExtension
* has enabled the element. Note: we delegate all the image rendering to
* cornerstoneExtension, so we don't need to do anything here regarding
* the image rendering, element enabling etc.
*/
const onElementEnabled = evt => {
setElement(evt.detail.element);
};
const updateViewport = useCallback(
newMeasurementSelected => {
const {
StudyInstanceUID,
displaySetInstanceUID,
sopClassUids,
} = srDisplaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) {
return;
}
if (sopClassUids && sopClassUids.length > 1) {
// Todo: what happens if there are multiple SOP Classes? Why we are
// not throwing an error?
console.warn(
'More than one SOPClassUID in the same series is not yet supported.'
);
}
_getViewportReferencedDisplaySetData(
srDisplaySet,
newMeasurementSelected,
DisplaySetService
).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => {
setMeasurementSelected(newMeasurementSelected);
setActiveImageDisplaySetData(referencedDisplaySet);
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
if (
referencedDisplaySet.displaySetInstanceUID ===
activeImageDisplaySetData?.displaySetInstanceUID
) {
const { measurements } = srDisplaySet;
// it means that we have a new referenced display set, and the
// imageIdIndex will handle it by updating the viewport, but if they
// are the same we just need to use MeasurementService to jump to the
// new measurement
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const csViewport = CornerstoneViewportService.getCornerstoneViewport(
viewportInfo.getViewportId()
);
const imageIds = csViewport.getImageIds();
const imageIdIndex = imageIds.indexOf(
measurements[newMeasurementSelected].imageId
);
if (imageIdIndex !== -1) {
csViewport.setImageIdIndex(imageIdIndex);
}
}
});
},
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportIndex]
);
const getCornerstoneViewport = useCallback(() => {
if (!activeImageDisplaySetData) {
return null;
}
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { measurements } = srDisplaySet;
const measurement = measurements[measurementSelected];
if (!measurement) {
return null;
}
const initialImageIndex = activeImageDisplaySetData.images.findIndex(
image => image.imageId === measurement.imageId
);
return (
<Component
{...props}
// should be passed second since we don't want SR displaySet to
// override the activeImageDisplaySetData
displaySets={[activeImageDisplaySetData]}
viewportOptions={{
toolGroupId: `${SR_TOOLGROUP_BASE_NAME}`,
}}
onElementEnabled={onElementEnabled}
initialImageIndex={initialImageIndex}
></Component>
);
}, [activeImageDisplaySetData, viewportIndex, measurementSelected]);
const onMeasurementChange = useCallback(
direction => {
let newMeasurementSelected = measurementSelected;
if (direction === 'right') {
newMeasurementSelected++;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
}
} else {
newMeasurementSelected--;
if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
}
setTrackingIdentifiers(newMeasurementSelected);
updateViewport(newMeasurementSelected);
},
[
measurementSelected,
measurementCount,
updateViewport,
setTrackingIdentifiers,
]
);
/**
Cleanup the SR viewport when the viewport is destroyed
*/
useEffect(() => {
const onDisplaySetsRemovedSubscription = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex];
if (
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
) {
viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex,
displaySetInstanceUIDs: [],
});
}
}
);
return () => {
onDisplaySetsRemovedSubscription.unsubscribe();
};
}, []);
/**
* Loading the measurements from the SR viewport, which goes through the
* isHydratable check, the outcome for the isHydrated state here is always FALSE
* since we don't do the hydration here. Todo: can't we just set it as false? why
* we are changing the state here? isHydrated is always false at this stage, and
* if it is hydrated we don't event use the SR viewport.
*/
useEffect(() => {
if (!srDisplaySet.isLoaded) {
srDisplaySet.load();
}
setIsHydrated(srDisplaySet.isHydrated);
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
}, [srDisplaySet]);
/**
* Hook to update the tracking identifiers when the selected measurement changes or
* the element changes
*/
useEffect(() => {
if (!element || !srDisplaySet.isLoaded) {
return;
}
setTrackingIdentifiers(measurementSelected);
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
useEffect(() => {
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
}, [trackedMeasurements]);
/**
* Data fetching for the SR displaySet, which updates the measurements and
* also gets the referenced image displaySet that SR is based on.
*/
useEffect(() => {
updateViewport(measurementSelected);
}, [dataSource, srDisplaySet]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
if (!activeImageDisplaySetData || !referencedDisplaySetMetadata) {
return null;
}
if (children && children.length) {
childrenWithProps = children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportIndex,
key: index,
})
);
});
}
const { Modality } = srDisplaySet;
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetMetadata;
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onPillClick={() => {
sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportIndex,
});
}}
onSeriesChange={onMeasurementChange}
studyData={{
label: viewportLabel,
useAltStyling: true,
isTracked: false,
isLocked,
isRehydratable: srDisplaySet.isRehydratable,
isHydrated,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: SeriesDescription,
modality: Modality,
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined
? `${SpacingBetweenSlices.toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex flex-row w-full h-full overflow-hidden">
{getCornerstoneViewport()}
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
<Notification
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
/>
)}
</div>
{childrenWithProps}
</div>
</>
);
}
OHIFCornerstoneSRViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportIndex: PropTypes.number.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
OHIFCornerstoneSRViewport.defaultProps = {
customProps: {},
};
async function _getViewportReferencedDisplaySetData(
displaySet,
measurementSelected,
DisplaySetService
) {
const { measurements } = displaySet;
const measurement = measurements[measurementSelected];
const { displaySetInstanceUID } = measurement;
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
PatientID: image0.PatientID,
PatientName: image0.PatientName,
PatientSex: image0.PatientSex,
PatientAge: image0.PatientAge,
SliceThickness: image0.SliceThickness,
StudyDate: image0.StudyDate,
SeriesDescription: image0.SeriesDescription,
SeriesInstanceUID: image0.SeriesInstanceUID,
SeriesNumber: image0.SeriesNumber,
ManufacturerModelName: image0.ManufacturerModelName,
SpacingBetweenSlices: image0.SpacingBetweenSlices,
};
return { referencedDisplaySetMetadata, referencedDisplaySet };
}
// function _onDoubleClick() {
// const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
// 'manipulatorState'
// ).setters.cancelActiveManipulatorsForElement;
// const enabledElements = cornerstoneTools.store.state.enabledElements;
// enabledElements.forEach(element => {
// cancelActiveManipulatorsForElement(element);
// });
// }
export default OHIFCornerstoneSRViewport;

View File

@ -1,5 +1,5 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');

View File

@ -1,9 +1,8 @@
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const merge = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const pkg = require('./../package.json');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
@ -13,7 +12,6 @@ module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,
@ -40,10 +38,6 @@ module.exports = (env, argv) => {
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
new MiniCssExtractPlugin({
filename: './dist/[name].css',
chunkFilename: './dist/[id].css',
}),
],
});
};

View File

@ -1,593 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.2...@ohif/extension-cornerstone@2.7.3) (2020-05-12)
### Bug Fixes
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
## [2.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.1...@ohif/extension-cornerstone@2.7.2) (2020-05-04)
### Bug Fixes
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.0...@ohif/extension-cornerstone@2.7.1) (2020-05-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.1...@ohif/extension-cornerstone@2.7.0) (2020-04-24)
### Features
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.0...@ohif/extension-cornerstone@2.6.1) (2020-04-23)
### Bug Fixes
* 🐛 Multiframe fix ([#1661](https://github.com/OHIF/Viewers/issues/1661)) ([7120561](https://github.com/OHIF/Viewers/commit/71205618ecb8b592247c5acb32284bfe7e18fce5))
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.2...@ohif/extension-cornerstone@2.6.0) (2020-04-23)
### Features
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.1...@ohif/extension-cornerstone@2.5.2) (2020-04-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.0...@ohif/extension-cornerstone@2.5.1) (2020-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.3...@ohif/extension-cornerstone@2.5.0) (2020-03-13)
### Features
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
## [2.4.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.2...@ohif/extension-cornerstone@2.4.3) (2020-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.4.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.1...@ohif/extension-cornerstone@2.4.2) (2020-03-09)
### Bug Fixes
* Remove Eraser and ROI Window ([6c950a9](https://github.com/OHIF/Viewers/commit/6c950a9669f7fbf3c46e48679fa26ee514824156))
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.0...@ohif/extension-cornerstone@2.4.1) (2020-03-03)
### Bug Fixes
* bump react-cornerstone-viewport version to address critical issue ([#1473](https://github.com/OHIF/Viewers/issues/1473)) ([ee80e02](https://github.com/OHIF/Viewers/commit/ee80e026610442e94caf5e4e3e4d193220cd0ece))
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.1...@ohif/extension-cornerstone@2.4.0) (2020-02-20)
### Features
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.0...@ohif/extension-cornerstone@2.3.1) (2020-02-14)
### Bug Fixes
* Creating 2 commands to activate zoom tool and also to move between displaySets ([#1446](https://github.com/OHIF/Viewers/issues/1446)) ([06a4af0](https://github.com/OHIF/Viewers/commit/06a4af06faaecf6fa06ccd90cdfa879ee8d53053))
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.2...@ohif/extension-cornerstone@2.3.0) (2020-02-10)
### Features
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.1...@ohif/extension-cornerstone@2.2.2) (2020-01-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.0...@ohif/extension-cornerstone@2.2.1) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.1...@ohif/extension-cornerstone@2.2.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.0...@ohif/extension-cornerstone@2.1.1) (2019-12-16)
### Bug Fixes
* 🐛 add WwwcRegionTool to cornerstone tools initialization ([#1302](https://github.com/OHIF/Viewers/issues/1302)) ([d5bf728](https://github.com/OHIF/Viewers/commit/d5bf72851a32dff9fd3fc09332ea5250bc7e6114))
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.0...@ohif/extension-cornerstone@1.7.1) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.6.0...@ohif/extension-cornerstone@1.7.0) (2019-11-25)
### Features
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.1...@ohif/extension-cornerstone@1.6.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.0...@ohif/extension-cornerstone@1.5.1) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.1...@ohif/extension-cornerstone@1.5.0) (2019-11-13)
### Features
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.0...@ohif/extension-cornerstone@1.4.1) (2019-11-08)
### Bug Fixes
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.1...@ohif/extension-cornerstone@1.4.0) (2019-10-26)
### Features
* Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.0...@ohif/extension-cornerstone@1.3.1) (2019-10-09)
### Bug Fixes
* 🐛 set tools bidirectional, eraser and annotate command ([#1020](https://github.com/OHIF/Viewers/issues/1020)) ([a28984e](https://github.com/OHIF/Viewers/commit/a28984e))
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.5...@ohif/extension-cornerstone@1.3.0) (2019-10-09)
### Features
* Multiple fixes and implementation changes to react-cornerstone-viewport ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3))
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.4...@ohif/extension-cornerstone@1.2.5) (2019-09-27)
### Bug Fixes
* version bump issue ([#963](https://github.com/OHIF/Viewers/issues/963)) ([e607ed2](https://github.com/OHIF/Viewers/commit/e607ed2)), closes [#962](https://github.com/OHIF/Viewers/issues/962)
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
### Bug Fixes
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
* version bump issue ([#962](https://github.com/OHIF/Viewers/issues/962)) ([c80ea17](https://github.com/OHIF/Viewers/commit/c80ea17))
### BREAKING CHANGES
* DICOM Seg
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
### Bug Fixes
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
### BREAKING CHANGES
* DICOM Seg
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.1...@ohif/extension-cornerstone@1.2.2) (2019-09-26)
### Bug Fixes
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.0...@ohif/extension-cornerstone@1.2.1) (2019-09-17)
### Bug Fixes
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.1...@ohif/extension-cornerstone@1.2.0) (2019-09-12)
### Features
* **Annotate:** Add annotate tool back to toolbar ([26be967](https://github.com/OHIF/Viewers/commit/26be967))
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.0...@ohif/extension-cornerstone@1.1.1) (2019-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.0.1...@ohif/extension-cornerstone@1.1.0) (2019-09-12)
### Features
* **BidirectionalTool:** Add BidrectionalTool to "more" menu ([#911](https://github.com/OHIF/Viewers/issues/911)) ([e40cbae](https://github.com/OHIF/Viewers/commit/e40cbae))
* **EraserTool:** add eraserTool to @ohif/extension-cornerstone ([#912](https://github.com/OHIF/Viewers/issues/912)) ([698d274](https://github.com/OHIF/Viewers/commit/698d274))
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.0.0...@ohif/extension-cornerstone@1.0.1) (2019-09-10)
### Bug Fixes
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.5...@ohif/extension-cornerstone@1.0.0) (2019-09-09)
### Features
* 🎸 Upgraded to cornerstoneTools 4.0 ([86adb51](https://github.com/OHIF/Viewers/commit/86adb51))
### BREAKING CHANGES
* n
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.4...@ohif/extension-cornerstone@0.50.5) (2019-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.3...@ohif/extension-cornerstone@0.50.4) (2019-09-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.2...@ohif/extension-cornerstone@0.50.3) (2019-09-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.1...@ohif/extension-cornerstone@0.50.2) (2019-09-04)
### Bug Fixes
* measurementsAPI issue caused by production build ([#842](https://github.com/OHIF/Viewers/issues/842)) ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.0-alpha.10...@ohif/extension-cornerstone@0.50.1) (2019-08-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.9...@ohif/extension-cornerstone@0.50.0-alpha.10) (2019-08-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [0.0.39-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.8...@ohif/extension-cornerstone@0.0.39-alpha.9) (2019-08-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
## 0.0.39-alpha.8 (2019-08-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.39-alpha.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.6...@ohif/extension-cornerstone@0.0.39-alpha.7) (2019-08-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.39-alpha.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.5...@ohif/extension-cornerstone@0.0.39-alpha.6) (2019-08-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.39-alpha.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.4...@ohif/extension-cornerstone@0.0.39-alpha.5) (2019-08-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.39-alpha.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.3...@ohif/extension-cornerstone@0.0.39-alpha.4) (2019-08-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.39-alpha.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.2...@ohif/extension-cornerstone@0.0.39-alpha.3) (2019-08-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.39-alpha.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.1...@ohif/extension-cornerstone@0.0.39-alpha.2) (2019-08-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [0.0.39-alpha.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.0...@ohif/extension-cornerstone@0.0.39-alpha.1) (2019-08-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
## 0.0.39-alpha.0 (2019-08-05)
**Note:** Version bump only for package @ohif/extension-cornerstone

View File

@ -1,123 +1 @@
# @ohif/extension-cornerstone
![npm (scoped)](https://img.shields.io/npm/v/@ohif/extension-cornerstone.svg?style=flat-square)
This extension adds support for viewing and manipulating 2D medical images via a
viewport. The underlying implementation wraps the
`cornerstonejs/react-cornerstone-viewport`, and provides basic commands and
toolbar buttons for common actions.
<!-- TODO: Simple image or GIF? -->
#### Index
Extension Id: `cornerstone`
- [Commands Module](#commands-module)
- [Toolbar Module](#toolbar-module)
- [Viewport Module](#viewport-module)
## Commands Module
This extensions includes the following `Commands` and `Command Definitions`.
These can be registered with `@ohif/core`'s `CommandManager`. After registering
the commands, they can be bound to `hotkeys` using the `HotkeysManager` and
listed in the `UserPreferences` modal.
You can read more about [`Commands`][docs-commands], [`Hotkeys`][docs-hotkeys],
and the [`UserPreferences` Modal][docs-userprefs] in their respective locations
in the OHIF Viewer's documentation.
| Command Name | Description | Store Contexts |
| ---------------------------- | --------------------------------------- | -------------- |
| `rotateViewportCW` | | viewports |
| `rotateViewportCCW` | | viewports |
| `invertViewport` | | viewports |
| `flipViewportVertical` | | viewports |
| `flipViewportHorizontal` | | viewports |
| `scaleUpViewport` | | viewports |
| `scaleDownViewport` | | viewports |
| `fitViewportToWindow` | | viewports |
| `resetViewport` | | viewports |
| clearAnnotations | TODO | |
| next/previous Image | TODO | |
| first/last Image | TODO | |
| `nextViewportDisplaySet` | | |
| `previousViewportDisplaySet` | | |
| `setToolActive` | Activates tool for primary button/touch | |
## Toolbar Module
Our toolbar module contains definitions for:
- `StackScroll`
- `Zoom`
- `Wwwc`
- `Pan`
- `Length`
- `Angle`
- `Reset`
- `Cine`
All use the `ACTIVE_VIEWPORT::CORNERSTONE` context.
## Viewport Module
Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport].
This module is the most prone to change as we hammer out our Viewport interface.
## Tool Configuration
Tools can be configured through extension configuration using the tools key:
```js
...
cornerstoneExtensionConfig: {
tools: {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
},
},
},
},
...
```
## Annotate Tools Configuration
_We currently support one property for annotation tools._
### Hide handles
This extension configuration allows you to toggle on/off handle rendering for
all annotate tools:
```js
...
cornerstoneExtensionConfig: {
hideHandles: true,
},
...
## Resources
### Repositories
- [cornerstonejs/react-cornerstone-viewport][react-viewport]
- [cornerstonejs/cornerstoneTools][cornerstone-tools]
- [cornerstonejs/cornerstone][cornerstone]
<!--
Links
-->
<!-- prettier-ignore-start -->
[docs-commands]: https://www.com
[docs-hotkeys]: https://www.com
[docs-userprefs]: htt
[react-viewport]: https://github.com/cornerstonejs/react-cornerstone-viewport
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
[cornerstone]: https://github.com/cornerstonejs/cornerstone
<!-- prettier-ignore-end -->
```
# Cornerstone

View File

@ -6,9 +6,9 @@
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"node": ">=10",
"npm": ">=6",
"yarn": ">=1.16.0"
},
@ -16,9 +16,6 @@
"dist",
"README.md"
],
"keywords": [
"ohif-extension"
],
"publishConfig": {
"access": "public"
},
@ -27,29 +24,33 @@
"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"
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"cornerstone-core": "2.6.0",
"cornerstone-math": "0.1.9",
"cornerstone-tools": "6.0.2",
"cornerstone-wado-image-loader": "4.0.4",
"dcmjs": "0.16.1",
"cornerstone-wado-image-loader": "^4.2.0",
"dcmjs": "^0.24.5",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-resize-detector": "^3.4.0"
"react-resize-detector": "^6.7.6"
},
"devDependencies": {
"lodash": "4.17.21"
},
"dependencies": {
"@babel/runtime": "7.16.3",
"lodash.merge": "^4.6.2",
"@babel/runtime": "7.17.9",
"@cornerstonejs/core": "^0.13.11",
"@cornerstonejs/streaming-image-volume-loader": "^0.4.10",
"@cornerstonejs/tools": "^0.20.15",
"@kitware/vtk.js": "^24.18.7",
"dom-to-image": "^2.6.0",
"lodash.debounce": "4.0.8",
"react-cornerstone-viewport": "4.1.2"
"lodash.merge": "^4.6.2",
"shader-loader": "^1.3.1",
"worker-loader": "^3.0.8"
}
}

View File

@ -1,152 +0,0 @@
import React from 'react';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import PropTypes from 'prop-types';
import { ViewportDownloadForm } from '@ohif/ui';
import { utils } from '@ohif/core';
import { getEnabledElement } from './state';
const MINIMUM_SIZE = 100;
const DEFAULT_SIZE = 512;
const MAX_TEXTURE_SIZE = 10000;
const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => {
const enabledElement = getEnabledElement(
activeViewportIndex
);
const activeEnabledElement = enabledElement?.element
const enableViewport = viewportElement => {
if (viewportElement) {
cornerstone.enable(viewportElement);
}
};
const disableViewport = viewportElement => {
if (viewportElement) {
cornerstone.disable(viewportElement);
}
};
const updateViewportPreview = (viewportElement, downloadCanvas, fileType) =>
new Promise(resolve => {
cornerstone.fitToWindow(viewportElement);
viewportElement.addEventListener(
'cornerstoneimagerendered',
function updateViewport(event) {
const enabledElement = cornerstone.getEnabledElement(event.target)
.element;
const type = 'image/' + fileType;
const dataUrl = downloadCanvas.toDataURL(type, 1);
let newWidth = enabledElement.offsetHeight;
let newHeight = enabledElement.offsetWidth;
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
newHeight *= multiplier;
newWidth *= multiplier;
}
resolve({ dataUrl, width: newWidth, height: newHeight });
viewportElement.removeEventListener(
'cornerstoneimagerendered',
updateViewport
);
}
);
});
const loadImage = (activeViewport, viewportElement, width, height) =>
new Promise(resolve => {
if (activeViewport && viewportElement) {
const enabledElement = cornerstone.getEnabledElement(activeViewport);
const viewport = Object.assign({}, enabledElement.viewport);
delete viewport.scale;
viewport.translation = {
x: 0,
y: 0,
};
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
cornerstone.displayImage(viewportElement, image);
cornerstone.setViewport(viewportElement, viewport);
cornerstone.resize(viewportElement, true);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
resolve({ image, width: newWidth, height: newHeight });
});
}
});
const toggleAnnotations = (toggle, viewportElement) => {
cornerstoneTools.store.state.tools.forEach(({ name }) => {
if (toggle) {
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
} else {
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
}
});
};
const downloadBlob = (
filename,
fileType,
viewportElement,
downloadCanvas
) => {
const file = `${filename}.${fileType}`;
const mimetype = `image/${fileType}`;
/* Handles JPEG images for IE11 */
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
const image = downloadCanvas.toDataURL(mimetype, 1);
const blob = utils.b64toBlob(
image.replace('data:image/jpeg;base64,', ''),
mimetype
);
return window.navigator.msSaveBlob(blob, file);
}
viewportElement.querySelector('canvas').toBlob(blob => {
const URLObj = window.URL || window.webkitURL;
const a = document.createElement('a');
a.href = URLObj.createObjectURL(blob);
a.download = file;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
};
return (
<ViewportDownloadForm
onClose={onClose}
minimumSize={MINIMUM_SIZE}
maximumSize={MAX_TEXTURE_SIZE}
defaultSize={DEFAULT_SIZE}
canvasClass={'cornerstone-canvas'}
activeViewport={activeEnabledElement}
enableViewport={enableViewport}
disableViewport={disableViewport}
updateViewportPreview={updateViewportPreview}
loadImage={loadImage}
toggleAnnotations={toggleAnnotations}
downloadBlob={downloadBlob}
/>
);
};
CornerstoneViewportDownloadForm.propTypes = {
onClose: PropTypes.func,
activeViewportIndex: PropTypes.number.isRequired,
};
export default CornerstoneViewportDownloadForm;

View File

@ -1,398 +0,0 @@
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF from '@ohif/core';
import { useCine, useViewportGrid } from '@ohif/ui';
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
import setCornerstoneMeasurementActive from './_shared/setCornerstoneMeasurementActive';
import setActiveAndPassiveToolsForElement from './_shared/setActiveAndPassiveToolsForElement';
import getTools from './_shared/getTools';
import ViewportOverlay from './ViewportOverlay';
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
const BaseAnnotationTool = cornerstoneTools.importInternal(
'base/BaseAnnotationTool'
);
const { StackManager } = OHIF.utils;
function OHIFCornerstoneViewport({
children,
dataSource,
displaySet,
onElementEnabled,
element,
viewportIndex,
servicesManager,
commandsManager,
}) {
const {
ToolBarService,
DisplaySetService,
MeasurementService,
HangingProtocolService,
} = servicesManager.services;
const [viewportData, setViewportData] = useState(null);
const [{ cines }, cineService] = useCine();
const [{ viewports }, viewportGridService] = useViewportGrid();
const isMounted = useRef(false);
const stageChangedRef = useRef(false);
const onNewImage = (element, callback) => {
const handler = () => {
element.removeEventListener(cornerstone.EVENTS.IMAGE_RENDERED, handler);
callback(element, ToolBarService);
};
element.addEventListener(cornerstone.EVENTS.IMAGE_RENDERED, handler);
};
const defaultOnElementEnabled = evt => {
const eventData = evt.detail;
const targetElement = eventData.element;
const tools = getTools();
const toolAlias = ToolBarService.state.primaryToolId;
// Activate appropriate tool bindings for element
setActiveAndPassiveToolsForElement(targetElement, tools);
cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, {
mouseButtonMask: 1,
});
// Set dashed, based on tracking, for this viewport
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(
tool => tool.element === targetElement
);
toolsForElement.forEach(tool => {
if (tool instanceof BaseAnnotationTool) {
const configuration = tool.configuration;
configuration.renderDashed = true;
tool.configuration = configuration;
}
});
// Update image after setting tool config
const enabledElement = cornerstone.getEnabledElement(targetElement);
if (enabledElement.image) {
cornerstone.updateImage(targetElement);
}
};
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
StackManager.clearStacks();
};
}, []);
useEffect(() => {
const { unsubscribe } = HangingProtocolService.subscribe(
HangingProtocolService.EVENTS.STAGE_CHANGE,
() => {
stageChangedRef.current = true;
}
);
return () => {
unsubscribe();
};
}, []);
useEffect(() => {
cineService.setCine({ id: viewportIndex });
}, [viewportIndex]);
useEffect(() => {
const {
StudyInstanceUID,
displaySetInstanceUID,
sopClassUids,
} = displaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) {
return;
}
if (sopClassUids && sopClassUids.length > 1) {
console.warn(
'More than one SOPClassUID in the same series is not yet supported.'
);
}
_getViewportData(dataSource, displaySet).then(data => {
if (isMounted.current) setViewportData(data);
});
}, [dataSource, displaySet, viewportIndex]);
useEffect(() => {
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
element,
viewportIndex,
displaySet.displaySetInstanceUID,
viewportGridService
);
_checkForCachedJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
element,
viewportIndex,
displaySet.displaySetInstanceUID,
viewportGridService
);
// reseting the HP stage changed flag
if (element) {
onNewImage(element, () => {
stageChangedRef.current = false;
});
}
// running HP-defined callbacks: invert, window level ...
if (element && displaySet.renderedCallback) {
onNewImage(element, displaySet.renderedCallback);
}
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [element, displaySet]);
let childrenWithProps = null;
if (!viewportData) {
return null;
}
const {
imageIds,
initialImageIdIndex,
// If this comes from the instance, would be a better default
// `FrameTime` in the instance
// frameRate = 0,
} = viewportData.stack;
// TODO: Does it make more sense to use Context?
if (children && children.length) {
childrenWithProps = children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportIndex,
key: index,
})
);
});
}
// We have...
// StudyInstanceUid, DisplaySetInstanceUid
// Use displaySetInstanceUid --> SeriesInstanceUid
// Get meta for series, map to actionBar
// const displaySet = DisplaySetService.getDisplaySetByUID(
// dSet.displaySetInstanceUID
// );
// TODO: This display contains the meta for all instances.
// That can't be right...
// console.log('DISPLAYSET', displaySet);
// const seriesMeta = DicomMetadataStore.getSeries(this.props.displaySet.StudyInstanceUID, '');
// console.log(seriesMeta);
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const frameRate = (cine && cine.frameRate) || 24;
return (
<div className="relative flex flex-row w-full h-full overflow-hidden">
<CornerstoneViewport
onElementEnabled={
onElementEnabled ? onElementEnabled : defaultOnElementEnabled
}
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={initialImageIdIndex}
initialViewport={displaySet.initialViewport} // from hanging protocol
stageChanged={stageChangedRef.current}
// Sync resize throttle w/ sidepanel animation duration to prevent
// seizure inducing strobe blinking effect
resizeRefreshRateMs={150}
// TODO: ViewportGrid Context?
isActive={true} // todo
isStackPrefetchEnabled={true} // todo
isPlaying={isPlaying}
frameRate={frameRate}
isOverlayVisible={true}
loadingIndicatorComponent={ViewportLoadingIndicator}
viewportOverlayComponent={props => {
return (
<ViewportOverlay
{...props}
activeTools={ToolBarService.getActiveTools()}
/>
);
}}
/>
{childrenWithProps}
</div>
);
}
OHIFCornerstoneViewport.propTypes = {
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
ToolBarService: PropTypes.object,
};
OHIFCornerstoneViewport.defaultProps = {
customProps: {},
};
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
function _getCornerstoneStack(displaySet, dataSource) {
// Get stack from Stack Manager
const storedStack = StackManager.findOrCreateStack(displaySet, dataSource);
// Clone the stack here so we don't mutate it
const stack = Object.assign({}, storedStack);
return stack;
}
async function _getViewportData(dataSource, displaySet) {
const stack = _getCornerstoneStack(displaySet, dataSource);
const viewportData = {
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
stack,
};
return viewportData;
}
function _subscribeToJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
element,
viewportIndex,
displaySetInstanceUID,
viewportGridService
) {
const { unsubscribe } = MeasurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT,
({ measurement }) => {
if (!measurement) return;
// check if the correct viewport index.
// if (viewportIndex !== jumpToMeasurementViewportIndex) {
// // Event for a different viewport.
// return;
// }
// Jump the the measurement if the displaySetInstanceUID matches
if (measurement.displaySetInstanceUID === displaySetInstanceUID) {
_jumpToMeasurement(
measurement,
element,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
);
}
}
);
return unsubscribe;
}
function _checkForCachedJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
element,
viewportIndex,
displaySetInstanceUID,
viewportGridService
) {
// Check if there is a queued jumpToMeasurement event
const measurementIdToJumpTo = MeasurementService.getJumpToMeasurement(
viewportIndex
);
if (measurementIdToJumpTo && element) {
// Jump to measurement if the measurement exists
const measurement = MeasurementService.getMeasurement(
measurementIdToJumpTo
);
if (measurement.displaySetInstanceUID === displaySetInstanceUID) {
_jumpToMeasurement(
measurement,
element,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
);
}
}
}
function _jumpToMeasurement(
measurement,
targetElement,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
) {
const { displaySetInstanceUID, SOPInstanceUID } = measurement;
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const imageIndex = referencedDisplaySet.images.findIndex(
i => i.SOPInstanceUID === SOPInstanceUID
);
setCornerstoneMeasurementActive(measurement);
viewportGridService.setActiveViewportIndex(viewportIndex);
if (targetElement !== null) {
const enabledElement = cornerstone.getEnabledElement(targetElement);
// Wait for the image to update or we get a race condition when the element has only just been enabled.
const scrollToHandler = evt => {
scrollToIndex(targetElement, imageIndex);
targetElement.removeEventListener(
'cornerstoneimagerendered',
scrollToHandler
);
};
targetElement.addEventListener('cornerstoneimagerendered', scrollToHandler);
if (enabledElement.image) {
cornerstone.updateImage(targetElement);
}
// Jump to measurement consumed, remove.
MeasurementService.removeJumpToMeasurement(viewportIndex);
}
}
export default OHIFCornerstoneViewport;

View File

@ -0,0 +1,19 @@
.viewport-wrapper {
width: 100%;
height: 100%; /* MUST have `height` to prevent resize infinite loop */
position: relative;
}
.cornerstone-viewport-element {
width: 100%;
height: 100%;
position: relative;
background-color: black;
/* Prevent the blue outline in Chrome when a viewport is selected */
outline: 0 !important;
/* Prevents the entire page from getting larger
when the magnify tool is near the sides/corners of the page */
overflow: hidden;
}

View File

@ -0,0 +1,432 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import PropTypes from 'prop-types';
import { useViewportGrid } from '@ohif/ui';
import * as cs3DTools from '@cornerstonejs/tools';
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
import { setEnabledElement } from '../state';
import CornerstoneCacheService from '../services/ViewportService/CornerstoneCacheService';
import './OHIFCornerstoneViewport.css';
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
const STACK = 'stack';
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
return false;
}
const prevDisplaySets = prevProps.displaySets[0];
const nextDisplaySets = nextProps.displaySets[0];
if (prevDisplaySets && nextDisplaySets) {
const areSameDisplaySetInstanceUIDs =
prevDisplaySets.displaySetInstanceUID ===
nextDisplaySets.displaySetInstanceUID;
const areSameImageLength =
prevDisplaySets.images.length === nextDisplaySets.images.length;
const areSameImageIds = prevDisplaySets.images.every(
(prevImage, index) =>
prevImage.imageId === nextDisplaySets.images[index].imageId
);
return (
areSameDisplaySetInstanceUIDs && areSameImageLength && areSameImageIds
);
}
return false;
}
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
// Then we don't need to worry about the re-renders if the props change.
const OHIFCornerstoneViewport = React.memo(props => {
const {
viewportIndex,
displaySets,
dataSource,
viewportOptions,
displaySetOptions,
servicesManager,
onElementEnabled,
onElementDisabled,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIndex,
} = props;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [viewportData, setViewportData] = useState(null);
const [_, viewportGridService] = useViewportGrid();
const elementRef = useRef();
const {
MeasurementService,
DisplaySetService,
ToolBarService,
ToolGroupService,
SyncGroupService,
CornerstoneViewportService,
} = servicesManager.services;
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
CornerstoneViewportService.resize();
setImageScrollBarHeight();
}
}, [elementRef]);
const elementEnabledHandler = useCallback(
evt => {
// check this is this element reference and return early if doesn't match
if (evt.detail.element !== elementRef.current) {
return;
}
const { viewportId, element } = evt.detail;
const viewportInfo = CornerstoneViewportService.getViewportInfo(
viewportId
);
const viewportIndex = viewportInfo.getViewportIndex();
setEnabledElement(viewportIndex, element);
const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups();
ToolGroupService.addViewportToToolGroup(
viewportId,
renderingEngineId,
toolGroupId
);
SyncGroupService.addViewportToSyncGroup(
viewportId,
renderingEngineId,
syncGroups
);
if (onElementEnabled) {
onElementEnabled(evt);
}
},
[viewportIndex, onElementEnabled, ToolGroupService]
);
// disable the element upon unmounting
useEffect(() => {
CornerstoneViewportService.enableElement(
viewportIndex,
viewportOptions,
elementRef.current
);
eventTarget.addEventListener(
Enums.Events.ELEMENT_ENABLED,
elementEnabledHandler
);
setImageScrollBarHeight();
return () => {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const viewportId = viewportInfo.getViewportId();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
ToolGroupService.disable(viewportId, renderingEngineId);
SyncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
syncGroups
);
CornerstoneViewportService.disableElement(viewportIndex);
eventTarget.removeEventListener(
Enums.Events.ELEMENT_ENABLED,
elementEnabledHandler
);
if (onElementDisabled) {
onElementDisabled();
}
};
}, []);
// subscribe to displaySet metadata invalidation (updates)
// Currently, if the metadata changes we need to re-render the display set
// for it to take effect in the viewport. As we deal with scaling in the loading,
// we need to remove the old volume from the cache, and let the
// viewport to re-add it which will use the new metadata. Otherwise, the
// viewport will use the cached volume and the new metadata will not be used.
// Note: this approach does not actually end of sending network requests
// and it uses the network cache
useEffect(() => {
const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async invalidatedDisplaySetInstanceUID => {
if (
viewportData.displaySetInstanceUIDs.includes(
invalidatedDisplaySetInstanceUID
)
) {
const newViewportData = await CornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
DisplaySetService
);
CornerstoneViewportService.updateViewport(
viewportIndex,
newViewportData
);
setViewportData(newViewportData);
}
}
);
return () => {
unsubscribe();
};
}, [viewportData, viewportIndex]);
useEffect(() => {
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
}
const loadViewportData = async () => {
const viewportData = await CornerstoneCacheService.getViewportData(
viewportIndex,
displaySets,
viewportOptions.viewportType,
dataSource,
initialImageIndex
);
CornerstoneViewportService.setViewportDisplaySets(
viewportIndex,
viewportData,
viewportOptions,
displaySetOptions
);
setViewportData(viewportData);
};
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
elementRef,
viewportIndex,
displaySets,
viewportGridService
);
_checkForCachedJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
elementRef,
viewportIndex,
displaySets,
viewportGridService
);
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportIndex, viewportData]);
return (
<div className="viewport-wrapper">
<ReactResizeDetector
handleWidth
handleHeight
skipOnMount={true} // Todo: make these configurable
refreshMode={'debounce'}
refreshRate={100}
onResize={onResize}
targetRef={elementRef.current}
/>
<div
className="cornerstone-viewport-element"
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={elementRef}
></div>
<CornerstoneOverlays
viewportIndex={viewportIndex}
ToolBarService={ToolBarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
</div>
);
}, areEqual);
function _subscribeToJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
elementRef,
viewportIndex,
displaySets,
viewportGridService
) {
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
);
const { unsubscribe } = MeasurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT,
({ measurement }) => {
if (!measurement) return;
// Jump the the measurement if the viewport contains the displaySetUID (fusion)
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
_jumpToMeasurement(
measurement,
elementRef,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
);
}
}
);
return unsubscribe;
}
// Check if there is a queued jumpToMeasurement event
function _checkForCachedJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
elementRef,
viewportIndex,
displaySets,
viewportGridService
) {
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
);
const measurementIdToJumpTo = MeasurementService.getJumpToMeasurement(
viewportIndex
);
if (measurementIdToJumpTo && elementRef) {
// Jump to measurement if the measurement exists
const measurement = MeasurementService.getMeasurement(
measurementIdToJumpTo
);
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
_jumpToMeasurement(
measurement,
elementRef,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
);
}
}
}
function _jumpToMeasurement(
measurement,
targetElementRef,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
) {
const targetElement = targetElementRef.current;
const { displaySetInstanceUID, SOPInstanceUID } = measurement;
if (!SOPInstanceUID) {
console.warn('cannot jump in a non-acquisition plane measurements yet');
}
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const imageIdIndex = referencedDisplaySet.images.findIndex(
i => i.SOPInstanceUID === SOPInstanceUID
);
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
// to set it properly
// setCornerstoneMeasurementActive(measurement);
viewportGridService.setActiveViewportIndex(viewportIndex);
if (getEnabledElement(targetElement)) {
cs3DTools.utilities.jumpToSlice(targetElement, {
imageIndex: imageIdIndex,
});
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
// Jump to measurement consumed, remove.
MeasurementService.removeJumpToMeasurement(viewportIndex);
}
}
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
OHIFCornerstoneViewport.propTypes = {
viewportIndex: PropTypes.number.isRequired,
displaySets: PropTypes.array.isRequired,
dataSource: PropTypes.object.isRequired,
viewportOptions: PropTypes.object,
displaySetOptions: PropTypes.arrayOf(PropTypes.object),
servicesManager: PropTypes.object.isRequired,
onElementEnabled: PropTypes.func,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIdOrIndex: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
export default OHIFCornerstoneViewport;

View File

@ -0,0 +1,78 @@
import React, { useEffect, useState } from 'react';
import ViewportImageScrollbar from './ViewportImageScrollbar';
import ViewportOverlay from './ViewportOverlay';
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
import CornerstoneCacheService from '../../services/ViewportService/CornerstoneCacheService';
function CornerstoneOverlays(props) {
const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
const { CornerstoneViewportService } = servicesManager.services;
const [imageSliceData, setImageSliceData] = useState({
imageIndex: 0,
numberOfSlices: 0,
});
const [viewportData, setViewportData] = useState(null);
useEffect(() => {
const { unsubscribe } = CornerstoneCacheService.subscribe(
CornerstoneCacheService.EVENTS.VIEWPORT_DATA_CHANGED,
props => {
if (props.viewportIndex !== viewportIndex) {
return;
}
setViewportData(props.viewportData);
}
);
return () => {
unsubscribe();
};
}, [viewportIndex]);
if (!element) {
return null;
}
if (viewportData) {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (viewportInfo?.viewportOptions?.customViewportOptions?.hideOverlays) {
return null;
}
}
return (
<div>
<ViewportImageScrollbar
viewportIndex={viewportIndex}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
setImageSliceData={setImageSliceData}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
<ViewportOverlay
imageSliceData={imageSliceData}
viewportData={viewportData}
viewportIndex={viewportIndex}
servicesManager={servicesManager}
element={element}
/>
<ViewportLoadingIndicator viewportData={viewportData} element={element} />
<ViewportOrientationMarkers
imageSliceData={imageSliceData}
element={element}
viewportData={viewportData}
viewportIndex={viewportIndex}
/>
</div>
);
}
export default CornerstoneOverlays;

View File

@ -0,0 +1,145 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Enums, Types, utilities } from '@cornerstonejs/core';
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
import { ImageScrollbar } from '@ohif/ui';
function CornerstoneImageScrollbar({
viewportData,
viewportIndex,
element,
imageSliceData,
setImageSliceData,
scrollbarHeight,
servicesManager,
}) {
const { CineService, CornerstoneViewportService } = servicesManager.services;
const onImageScrollbarChange = (imageIndex, viewportIndex) => {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const viewportId = viewportInfo.getViewportId();
const viewport = CornerstoneViewportService.getCornerstoneViewport(
viewportId
);
// on image scrollbar change, stop the CINE if it is playing
CineService.stopClip(element);
CineService.setCine({ id: viewportIndex, isPlaying: false });
csToolsUtils.jumpToSlice(viewport.element, {
imageIndex,
debounceLoading: true,
});
};
useEffect(() => {
if (!viewportData) {
return;
}
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
if (!viewport) {
return;
}
if (viewportData.viewportType === Enums.ViewportType.STACK) {
const imageIndex = viewport.getCurrentImageIdIndex();
setImageSliceData({
imageIndex: imageIndex,
numberOfSlices: viewportData.imageIds.length,
});
return;
}
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const sliceData = utilities.getImageSliceDataForVolumeViewport(
viewport as Types.IVolumeViewport
);
if (!sliceData) {
return;
}
const { imageIndex, numberOfSlices } = sliceData;
setImageSliceData({ imageIndex, numberOfSlices });
}
}, [viewportIndex, viewportData]);
useEffect(() => {
if (viewportData?.viewportType !== Enums.ViewportType.STACK) {
return;
}
const updateStackIndex = event => {
const { newImageIdIndex } = event.detail;
// find the index of imageId in the imageIds
setImageSliceData({
imageIndex: newImageIdIndex,
numberOfSlices: viewportData.imageIds.length,
});
};
element.addEventListener(
Enums.Events.STACK_VIEWPORT_SCROLL,
updateStackIndex
);
return () => {
element.removeEventListener(
Enums.Events.STACK_VIEWPORT_SCROLL,
updateStackIndex
);
};
}, [viewportData, element]);
useEffect(() => {
if (viewportData?.viewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
return;
}
const updateVolumeIndex = event => {
const { imageIndex, numberOfSlices } = event.detail;
// find the index of imageId in the imageIds
setImageSliceData({ imageIndex, numberOfSlices });
};
element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
return () => {
element.removeEventListener(
Enums.Events.VOLUME_NEW_IMAGE,
updateVolumeIndex
);
};
}, [viewportData, element]);
return (
<ImageScrollbar
onChange={evt => onImageScrollbarChange(evt, viewportIndex)}
max={
imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0
}
height={scrollbarHeight}
value={imageSliceData.imageIndex}
/>
);
}
CornerstoneImageScrollbar.propTypes = {
viewportData: PropTypes.object,
viewportIndex: PropTypes.number.isRequired,
element: PropTypes.instanceOf(Element),
scrollbarHeight: PropTypes.string,
imageSliceData: PropTypes.object.isRequired,
setImageSliceData: PropTypes.func.isRequired,
};
export default CornerstoneImageScrollbar;

View File

@ -0,0 +1,103 @@
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { Enums } from '@cornerstonejs/core';
function ViewportLoadingIndicator({ viewportData, element }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const loadIndicatorRef = useRef(null);
const imageIdToBeLoaded = useRef(null);
const setLoadingState = evt => {
clearTimeout(loadIndicatorRef.current);
loadIndicatorRef.current = setTimeout(() => {
setLoading(true);
}, 50);
};
const setFinishLoadingState = evt => {
clearTimeout(loadIndicatorRef.current);
setLoading(false);
};
const setErrorState = evt => {
clearTimeout(loadIndicatorRef.current);
if (imageIdToBeLoaded.current === evt.detail.imageId) {
setError(evt.detail.error);
imageIdToBeLoaded.current = null;
}
};
useEffect(() => {
element.addEventListener(
Enums.Events.STACK_VIEWPORT_SCROLL,
setLoadingState
);
element.addEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
element.addEventListener(
Enums.Events.STACK_NEW_IMAGE,
setFinishLoadingState
);
return () => {
element.removeEventListener(
Enums.Events.STACK_VIEWPORT_SCROLL,
setLoadingState
);
element.removeEventListener(
Enums.Events.STACK_NEW_IMAGE,
setFinishLoadingState
);
element.removeEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
};
}, [element, viewportData]);
if (error) {
return (
<>
<div className="bg-black opacity-50 absolute h-full w-full top-0 left-0">
<div className="flex transparent items-center justify-center w-full h-full">
<p className="text-primary-light text-xl font-light">
<h4>Error Loading Image</h4>
<p>An error has occurred.</p>
<p>{error}</p>
</p>
</div>
</div>
</>
);
}
if (loading) {
return (
// IMPORTANT: we need to use the pointer-events-none class to prevent the loading indicator from
// interacting with the mouse, since scrolling should propagate to the viewport underneath
<div className="pointer-events-none bg-black opacity-50 absolute h-full w-full top-0 left-0">
<div className="flex transparent items-center justify-center w-full h-full">
<p className="text-primary-light text-xl font-light">Loading...</p>
</div>
</div>
);
}
return null;
}
ViewportLoadingIndicator.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
element: PropTypes.object,
};
ViewportLoadingIndicator.defaultProps = {
percentComplete: 0,
error: null,
};
export default ViewportLoadingIndicator;

View File

@ -0,0 +1,39 @@
.ViewportOrientationMarkers {
--marker-width: 100px;
--marker-height: 100px;
--scrollbar-width: 20px;
pointer-events: none;
font-size: 15px;
color: #ccc;
line-height: 18px;
}
.ViewportOrientationMarkers .orientation-marker {
position: absolute;
}
.ViewportOrientationMarkers .top-mid {
top: 5px;
left: 50%;
}
.ViewportOrientationMarkers .left-mid {
top: 47%;
left: 5px;
}
.ViewportOrientationMarkers .right-mid {
top: 47%;
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
}
.ViewportOrientationMarkers .bottom-mid {
top: calc(100% - var(--marker-height) - 5px);
left: 47%;
}
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
display: flex;
justify-content: flex-end;
min-width: var(--marker-width);
}
.ViewportOrientationMarkers .bottom-mid .orientation-marker-value {
display: flex;
justify-content: flex-start;
min-height: var(--marker-height);
flex-direction: column-reverse;
}

View File

@ -0,0 +1,195 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { metaData, Enums, Types } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
import './ViewportOrientationMarkers.css';
import { getEnabledElement } from '../../state';
/**
*
* Computes the orientation labels on a Cornerstone-enabled Viewport element
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
*
* @param {*} rowCosines
* @param {*} columnCosines
* @param {*} rotation in degrees
* @returns
*/
function getOrientationMarkers(
rowCosines,
columnCosines,
rotation,
flipVertical,
flipHorizontal
) {
const {
getOrientationStringLPS,
invertOrientationStringLPS,
} = utilities.orientation;
const rowString = getOrientationStringLPS(rowCosines);
const columnString = getOrientationStringLPS(columnCosines);
const oppositeRowString = invertOrientationStringLPS(rowString);
const oppositeColumnString = invertOrientationStringLPS(columnString);
const markers = {
top: oppositeColumnString,
left: oppositeRowString,
right: rowString,
bottom: columnString,
};
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
// the rotation applications
if (flipVertical) {
markers.top = invertOrientationStringLPS(markers.top);
markers.bottom = invertOrientationStringLPS(markers.bottom);
}
if (flipHorizontal) {
markers.left = invertOrientationStringLPS(markers.left);
markers.right = invertOrientationStringLPS(markers.right);
}
// Swap the labels accordingly if the viewport has been rotated
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
if (rotation === 90 || rotation === -270) {
return {
top: markers.left,
left: invertOrientationStringLPS(markers.top),
right: invertOrientationStringLPS(markers.bottom),
bottom: markers.right, // left
};
} else if (rotation === -90 || rotation === 270) {
return {
top: invertOrientationStringLPS(markers.left),
left: markers.top,
bottom: markers.left,
right: markers.bottom,
};
} else if (rotation === 180 || rotation === -180) {
return {
top: invertOrientationStringLPS(markers.top),
left: invertOrientationStringLPS(markers.left),
bottom: invertOrientationStringLPS(markers.bottom),
right: invertOrientationStringLPS(markers.right),
};
}
return markers;
}
function ViewportOrientationMarkers({
element,
viewportData,
imageSliceData,
viewportIndex,
orientationMarkers = ['top', 'left'],
}) {
// Rotation is in degrees
const [rotation, setRotation] = useState(0);
const [flipHorizontal, setFlipHorizontal] = useState(false);
const [flipVertical, setFlipVertical] = useState(false);
useEffect(() => {
const cameraModifiedListener = (
evt: Types.EventTypes.CameraModifiedEvent
) => {
const { rotation, previousCamera, camera } = evt.detail;
if (rotation !== undefined) {
setRotation(rotation);
}
if (camera.flipHorizontal !== undefined &&
previousCamera.flipHorizontal !== camera.flipHorizontal) {
setFlipHorizontal(camera.flipHorizontal);
}
if (camera.flipVertical !== undefined &&
previousCamera.flipVertical !== camera.flipVertical) {
setFlipVertical(camera.flipVertical);
}
};
element.addEventListener(
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
return () => {
element.removeEventListener(
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
};
}, []);
const getMarkers = useCallback(
orientationMarkers => {
// Todo: support orientation markers for the volume viewports
if (
!viewportData ||
viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC
) {
return '';
}
const imageIndex = imageSliceData.imageIndex;
const imageId = viewportData?.imageIds[imageIndex];
// Workaround for below TODO stub
if (!imageId) {
return false;
}
const { rowCosines, columnCosines } =
metaData.get('imagePlaneModule', imageId) || {};
if (!rowCosines || !columnCosines || rotation === undefined) {
return false;
}
if (!rowCosines || !columnCosines) {
return '';
}
const markers = getOrientationMarkers(
rowCosines,
columnCosines,
rotation,
flipVertical,
flipHorizontal
);
return orientationMarkers.map((m, index) => (
<div
className={`${m}-mid orientation-marker`}
key={`${m}-mid orientation-marker`}
>
<div className="orientation-marker-value">{markers[m]}</div>
</div>
));
},
[flipHorizontal, flipVertical, rotation, viewportData, imageSliceData]
);
return (
<div className="ViewportOrientationMarkers noselect">
{getMarkers(orientationMarkers)}
</div>
);
}
ViewportOrientationMarkers.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
};
ViewportOrientationMarkers.defaultProps = {
percentComplete: 0,
error: null,
};
export default ViewportOrientationMarkers;

View File

@ -0,0 +1,273 @@
import React, { useCallback, useEffect, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';
const EPSILON = 1e-4;
function CornerstoneViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
servicesManager,
}) {
const {
CornerstoneViewportService,
ToolBarService,
} = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(ToolBarService.getActiveTools());
}, []);
/**
* Updating the VOI when the viewport changes its voi
*/
useEffect(() => {
const updateVOI = eventDetail => {
const { range } = eventDetail.detail;
if (!range) {
return;
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
lower,
upper
);
setVOI({ windowCenter, windowWidth });
};
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportIndex, viewportData, voi, element]);
/**
* Updating the scale when the viewport changes its zoom
*/
useEffect(() => {
const updateScale = eventDetail => {
const { previousCamera, camera } = eventDetail.detail;
if (
previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale
) {
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
if (!viewport) {
return;
}
const imageData = viewport.getImageData();
if (!imageData) {
return;
}
if (camera.scale) {
setScale(camera.scale);
return;
}
const { spacing } = imageData;
// convert parallel scale to scale
const scale =
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale);
}
};
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportIndex, viewportData]);
/**
* Updating the active tools when the toolbar changes
*/
// Todo: this should act on the toolGroups instead of the toolbar state
useEffect(() => {
const { unsubscribe } = ToolBarService.subscribe(
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
setActiveTools(ToolBarService.getActiveTools());
}
);
return () => {
unsubscribe();
};
}, [ToolBarService]);
const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi;
if (activeTools.includes('WindowLevel')) {
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div className="flex flex-row">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
if (activeTools.includes('Zoom')) {
return (
<div className="flex flex-row">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
return null;
}, [voi, scale, activeTools]);
const getTopRightContent = useCallback(() => {
const { imageIndex, numberOfSlices } = imageSliceData;
if (!viewportData) {
return;
}
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
CornerstoneViewportService
);
}
return (
<div className="flex flex-row">
<span className="mr-1">I:</span>
<span className="font-light">
{instanceNumber !== undefined
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}, [imageSliceData, viewportData, viewportIndex]);
if (!viewportData) {
return null;
}
if (viewportData.imageIds.length === 0) {
throw new Error(
'ViewportOverlay: only viewports with imageIds is supported at this time'
);
}
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
/>
);
}
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.imageIds;
const imageId = imageIds[imageIndex];
if (!imageId) {
return;
}
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
const stackSize = imageIds.length;
if (stackSize <= 1) {
return;
}
return parseInt(instanceNumber);
}
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
function _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
CornerstoneViewportService
) {
const volumes = viewportData.volumes;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
return;
}
const volume = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = CornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
if (!cornerstoneViewport) {
return;
}
const camera = cornerstoneViewport.getCamera();
const { viewPlaneNormal } = camera;
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
const scanAxisNormal = direction.slice(6, 9);
// check if viewPlaneNormal is parallel to scanAxisNormal
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
if (isAcquisitionPlane) {
const { instanceNumber } =
metaData.get('generalImageModule', imageIds[imageIndex]) || {};
return parseInt(instanceNumber);
}
}
CornerstoneViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportIndex: PropTypes.number,
};
export default CornerstoneViewportOverlay;

View File

@ -1,39 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const ViewportLoadingIndicator = ({ error }) => {
if (error) {
return (
<>
<div className="bg-black h-full w-full absolute opacity-50"></div>
<div className="text-primary-light text-xl font-light">
<h4>Error Loading Image</h4>
<p>An error has occurred.</p>
<p>{error.message}</p>
</div>
</>
);
}
return (
<>
<div className="bg-black h-full w-full absolute opacity-50"></div>
<div className="absolute transparent w-full h-full flex items-center justify-center">
<p className="text-primary-light text-xl font-light">Loading...</p>
</div>
</>
);
};
ViewportLoadingIndicator.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
};
ViewportLoadingIndicator.defaultProps = {
percentComplete: 0,
error: null,
};
export default ViewportLoadingIndicator;

View File

@ -1,95 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import classnames from 'classnames';
const ViewportOverlay = ({
imageId,
scale,
windowWidth,
windowCenter,
imageIndex,
stackSize,
activeTools,
}) => {
const topLeft = 'top-viewport left-viewport';
const topRight = 'top-viewport right-viewport-scrollbar';
const bottomRight = 'bottom-viewport right-viewport-scrollbar';
const bottomLeft = 'bottom-viewport left-viewport';
const overlay = 'absolute pointer-events-none';
const isZoomActive = activeTools.includes('Zoom');
const isWwwcActive = activeTools.includes('Wwwc');
if (!imageId) {
return null;
}
// TODO: this component should be presentational only. Right now it has a weird dependency on Cornerstone
const generalImageModule =
cornerstone.metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
return (
<div className="text-primary-light">
<div
data-cy={'viewport-overlay-top-left'}
className={classnames(overlay, topLeft)}
>
{isZoomActive && (
<div className="flex flex-row">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
)}
{isWwwcActive && (
<div className="flex flex-row">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">
{windowWidth.toFixed(0)}
</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
)}
</div>
<div
data-cy={'viewport-overlay-top-right'}
className={classnames(overlay, topRight)}
>
{stackSize > 1 && (
<div className="flex flex-row">
<span className="mr-1">I:</span>
<span className="font-light">
{`${instanceNumber} (${imageIndex}/${stackSize})`}
</span>
</div>
)}
</div>
<div
data-cy={'viewport-overlay-bottom-right'}
className={classnames(overlay, bottomRight)}
></div>
<div
data-cy={'viewport-overlay-bottom-left'}
className={classnames(overlay, bottomLeft)}
></div>
</div>
);
};
ViewportOverlay.propTypes = {
scale: PropTypes.number.isRequired,
windowWidth: PropTypes.number.isRequired,
windowCenter: PropTypes.number.isRequired,
imageId: PropTypes.string.isRequired,
imageIndex: PropTypes.number.isRequired,
stackSize: PropTypes.number.isRequired,
activeTools: PropTypes.arrayOf(PropTypes.string),
};
ViewportOverlay.defaultProps = {
activeTools: [],
};
export default ViewportOverlay;

View File

@ -1,36 +0,0 @@
import csTools from 'cornerstone-tools';
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
other: [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
csTools.OverlayTool,
],
};
export default function getTools() {
const tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
return tools;
}
export { toolsGroupedByType };

View File

@ -1,21 +0,0 @@
import csTools from 'cornerstone-tools';
export default function _setActiveAndPassiveToolsForElement(element, tools) {
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
tools.forEach(tool => {
if (tool.prototype instanceof BaseAnnotationTool) {
// BaseAnnotationTool would likely come from csTools lib exports
const toolName = new tool().name;
csTools.setToolPassiveForElement(element, toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
csTools.setToolActiveForElement(element, 'Pan', { mouseButtonMask: 4 });
csTools.setToolActiveForElement(element, 'Zoom', { mouseButtonMask: 2 });
csTools.setToolActiveForElement(element, 'Wwwc', { mouseButtonMask: 1 });
csTools.setToolActiveForElement(element, 'StackScrollMouseWheel', {}); // TODO: Empty options should not be required
csTools.setToolActiveForElement(element, 'PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
csTools.setToolActiveForElement(element, 'ZoomTouchPinch', {});
csTools.setToolEnabledForElement(element, 'Overlay', {});
}

View File

@ -1,39 +0,0 @@
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
const { globalImageIdSpecificToolStateManager } = cornerstoneTools;
export default function setCornerstoneMeasurementActive(measurement) {
const { id } = measurement;
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
Object.keys(toolState).forEach(imageId => {
const imageIdSpecificToolState = toolState[imageId];
Object.keys(imageIdSpecificToolState).forEach(toolType => {
const toolSpecificToolState = imageIdSpecificToolState[toolType];
const toolSpecificToolData = toolSpecificToolState.data;
if (toolSpecificToolData && toolSpecificToolData.length) {
toolSpecificToolData.forEach(data => {
data.active = data.id === id ? true : false;
});
}
});
});
const enabledElements = cornerstoneTools.store.state.enabledElements;
enabledElements.forEach(element => {
try {
cornerstone.updateImage(element);
} catch (ex) {
// https://github.com/cornerstonejs/cornerstone/blob/master/src/updateImage.js#L16
// This fails if enabledElement.image is undefined and we have no layers
// Instead of throwing, it should _probably_ do nothing.
// We'll just swallow the exception
}
});
}

View File

@ -1,281 +1,204 @@
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import {
getEnabledElement,
StackViewport,
volumeLoader,
cache,
utilities as csUtils,
} from '@cornerstonejs/core';
import {
ToolGroupManager,
Enums,
segmentation,
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
//import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll');
import { getEnabledElement as OHIFgetEnabledElement } from './state';
import callInputDialog from './utils/callInputDialog';
import { setColormap } from './utils/colormap/transferFunctionHelpers';
const { studyMetadataManager } = OHIF.utils;
const commandsModule = ({ servicesManager }) => {
const {
ViewportGridService,
ToolGroupService,
CineService,
ToolBarService,
UIDialogService,
CornerstoneViewportService,
SegmentationService,
} = servicesManager.services;
const imagePositionSynchronizer = new cornerstoneTools.Synchronizer(
cornerstone.EVENTS.NEW_IMAGE,
cornerstoneTools.stackImagePositionSynchronizer
);
const panZoomSynchronizer = new cornerstoneTools.Synchronizer(
cornerstone.EVENTS.IMAGE_RENDERED,
cornerstoneTools.panZoomSynchronizer
);
function onElementEnabledAddToSync(event) {
const { element } = event.detail;
imagePositionSynchronizer.add(element);
// panZoomSynchronizer.add(element);
}
function onElementDisabledRemoveFromSync(event) {
const { element } = event.detail;
imagePositionSynchronizer.remove(element);
panZoomSynchronizer.remove(element);
}
const commandsModule = ({ servicesManager, commandsManager }) => {
const { ViewportGridService } = servicesManager.services;
function _getActiveViewportsEnabledElement() {
function _getActiveViewportEnabledElement() {
const { activeViewportIndex } = ViewportGridService.getState();
const { element } = getEnabledElement(activeViewportIndex) || {};
return element;
const { element } = OHIFgetEnabledElement(activeViewportIndex) || {};
const enabledElement = getEnabledElement(element);
return enabledElement;
}
function _getToolGroup(toolGroupId) {
let toolGroupIdToUse = toolGroupId;
if (!toolGroupIdToUse) {
// Use the active viewport's tool group if no tool group id is provided
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
const { renderingEngineId, viewportId } = enabledElement;
const toolGroup = ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);
if (!toolGroup) {
console.warn(
'No tool group found for viewportId:',
viewportId,
'and renderingEngineId:',
renderingEngineId
);
return;
}
toolGroupIdToUse = toolGroup.id;
}
const toolGroup = ToolGroupService.getToolGroup(toolGroupIdToUse);
return toolGroup;
}
const actions = {
getCornerstoneLibraries: () => {
return { cornerstone, cornerstoneTools };
getActiveViewportEnabledElement: () => {
return _getActiveViewportEnabledElement();
},
rotateViewport: ({ rotation }) => {
const enabledElement = _getActiveViewportsEnabledElement();
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.rotation += rotation;
cornerstone.setViewport(enabledElement, viewport);
}
},
flipViewportHorizontal: () => {
const enabledElement = _getActiveViewportsEnabledElement();
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.hflip = !viewport.hflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
flipViewportVertical: () => {
const enabledElement = _getActiveViewportsEnabledElement();
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.vflip = !viewport.vflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
scaleViewport: ({ direction }) => {
const enabledElement = _getActiveViewportsEnabledElement();
const step = direction * 0.15;
if (enabledElement) {
if (step) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.scale += step;
cornerstone.setViewport(enabledElement, viewport);
} else {
cornerstone.fitToWindow(enabledElement);
}
}
},
resetViewport: () => {
const enabledElement = _getActiveViewportsEnabledElement();
if (enabledElement) {
cornerstone.reset(enabledElement);
}
},
toggleSynchronizer: ({ toggledState }) => {
const synchronizers = [imagePositionSynchronizer];
// Set synchronizer state when the command is run.
synchronizers.forEach(s => {
s.enabled = toggledState;
});
const unsubscribe = () => {
cornerstone.events.removeEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
onElementEnabledAddToSync
);
cornerstone.events.removeEventListener(
cornerstone.EVENTS.ELEMENT_DISABLED,
onElementDisabledRemoveFromSync
);
};
const subscribe = () => {
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
onElementEnabledAddToSync
);
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_DISABLED,
onElementDisabledRemoveFromSync
);
};
// Add event handlers so that if the layout is changed, new elements
// are automatically added to the synchronizer while it is enabled.
if (toggledState === true) {
subscribe();
} else {
// If the synchronizer is disabled, remove the event handlers
unsubscribe();
setViewportActive: ({ viewportId }) => {
const viewportInfo = CornerstoneViewportService.getViewportInfo(
viewportId
);
if (!viewportInfo) {
console.warn('No viewport found for viewportId:', viewportId);
return;
}
// Erase existing state and then set up all currently existing elements
cornerstone.getEnabledElements().map(e => {
synchronizers.forEach(s => {
s.remove(e.element);
s.add(e.element);
const viewportIndex = viewportInfo.getViewportIndex();
ViewportGridService.setActiveViewportIndex(viewportIndex);
},
arrowTextCallback: ({ callback, data }) => {
callInputDialog(UIDialogService, data, callback);
},
toggleCine: () => {
const { viewports } = ViewportGridService.getState();
const { isCineEnabled } = CineService.getState();
CineService.setIsCineEnabled(!isCineEnabled);
ToolBarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
viewports.forEach((_, index) =>
CineService.setCine({ id: index, isPlaying: false })
);
},
setWindowLevel({ window, level, toolGroupId }) {
// convert to numbers
const windowWidthNum = Number(window);
const windowCenterNum = Number(level);
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = ToolGroupService.getToolGroupForViewport(
viewportId
);
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
// get actor from the viewport
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
const viewport = renderingEngine.getViewport(viewportId);
const lower = windowCenterNum - windowWidthNum / 2.0;
const upper = windowCenterNum + windowWidthNum / 2.0;
if (viewport instanceof StackViewport) {
viewport.setProperties({
voiRange: {
upper,
lower,
},
});
});
return unsubscribe;
},
invertViewport: ({ element }) => {
let enabledElement;
if (element === undefined) {
enabledElement = _getActiveViewportsEnabledElement();
} else {
enabledElement = element;
}
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.invert = !viewport.invert;
cornerstone.setViewport(enabledElement, viewport);
viewport.render();
}
},
cancelMeasurement: () => {
const enabledElement = _getActiveViewportsEnabledElement();
toggleCrosshairs({ toolGroupId, toggledState }) {
const toolName = 'Crosshairs';
// If it is Enabled
if (toggledState) {
actions.setToolActive({ toolName, toolGroupId });
return;
}
const toolGroup = _getToolGroup(toolGroupId);
if (enabledElement) {
const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
'manipulatorState'
).setters.cancelActiveManipulatorsForElement;
if (!toolGroup) {
return;
}
cancelActiveManipulatorsForElement(enabledElement);
toolGroup.setToolDisabled(toolName);
cornerstone.updateImage(enabledElement);
// Get the primary toolId from the ToolBarService and set it to active
// Since it was set to passive if not already active
const primaryActiveTool = ToolBarService.state.primaryToolId;
if (
toolGroup?.toolOptions[primaryActiveTool]?.mode ===
Enums.ToolModes.Passive
) {
toolGroup.setToolActive(primaryActiveTool, {
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
});
}
},
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
// better mouseButtonMask sets.
setToolActive: ({ toolName }) => {
if (!toolName) {
console.warn('No toolname provided to setToolActive command');
setToolActive: ({ toolName, toolGroupId = null }) => {
const toolGroup = _getToolGroup(toolGroupId);
if (!toolGroup) {
console.warn('No tool group found for toolGroupId:', toolGroupId);
return;
}
// Todo: we need to check if the viewports of the toolGroup is actually
// parts of the ViewportGrid's viewports, if not we return
// Find total number of tool indexes
const { viewports } = ViewportGridService.getState() || { viewports: [] };
for (let i = 0; i < viewports.length; i++) {
const viewport = viewports[i];
const hasDisplaySet = viewport.displaySetInstanceUID !== undefined;
const { viewports } = ViewportGridService.getState() || {
viewports: [],
};
if (!hasDisplaySet) {
// iterate over all viewports and set the tool active for the
// viewports that belong to the toolGroup
for (let index = 0; index < viewports.length; index++) {
const ohifEnabledElement = OHIFgetEnabledElement(index);
if (!ohifEnabledElement) {
continue;
}
const viewportInfo = getEnabledElement(i);
if (!viewportInfo) continue;
const hasCornerstoneContext =
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
const viewport = getEnabledElement(ohifEnabledElement.element);
if (hasCornerstoneContext) {
cornerstoneTools.setToolActiveForElement(
viewportInfo.element,
toolName,
{ mouseButtonMask: 1 }
);
} else {
commandsManager.runCommand(
'setToolActive',
{
element: viewportInfo.element,
toolName,
},
viewportInfo.context
);
if (!viewport) {
continue;
}
}
},
clearAnnotations: () => {
const element = _getActiveViewportsEnabledElement();
if (!element) {
return;
}
const { enabledElement } = cornerstone.getEnabledElement(element) || {};
if (!enabledElement || !enabledElement.image) {
return;
}
// Find the current active tool and set it to be passive
const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
const {
toolState,
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
if (
!toolState ||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
) {
return;
}
if (activeTool) {
toolGroup.setToolPassive(activeTool);
}
const imageIdToolState = toolState[enabledElement.image.imageId];
const measurementsToRemove = [];
Object.keys(imageIdToolState).forEach(toolType => {
const { data } = imageIdToolState[toolType];
data.forEach(measurementData => {
const {
_id,
lesionNamingNumber,
measurementNumber,
} = measurementData;
if (!_id) {
return;
}
measurementsToRemove.push({
toolType,
_id,
lesionNamingNumber,
measurementNumber,
});
// Set the new toolName to be active
toolGroup.setToolActive(toolName, {
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
});
});
measurementsToRemove.forEach(measurementData => {
OHIF.measurements.MeasurementHandlers.onRemoved({
detail: {
toolType: measurementData.toolType,
measurementData,
},
});
});
},
nextImage: () => {
const enabledElement = _getActiveViewportsEnabledElement();
scroll(enabledElement, 1);
},
previousImage: () => {
const enabledElement = _getActiveViewportsEnabledElement();
scroll(enabledElement, -1);
},
getActiveViewportEnabledElement: () => {
const enabledElement = _getActiveViewportsEnabledElement();
return enabledElement;
return;
}
},
showDownloadViewportModal: () => {
const { activeViewportIndex } = ViewportGridService.getState();
@ -288,119 +211,215 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
CornerstoneViewportService,
},
});
}
},
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
const nearbyTool = {};
let pointNearTool = false;
rotateViewport: ({ rotation }) => {
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
availableToolTypes.forEach(toolType => {
const elementToolData = cornerstoneTools.getToolState(
element,
toolType
);
const { viewport } = enabledElement;
if (!elementToolData) {
return;
if (viewport instanceof StackViewport) {
const { rotation: currentRotation } = viewport.getProperties();
const newRotation = (currentRotation + rotation) % 360;
viewport.setProperties({ rotation: newRotation });
viewport.render();
}
},
flipViewportHorizontal: () => {
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { flipHorizontal } = viewport.getCamera();
viewport.setCamera({ flipHorizontal: !flipHorizontal });
viewport.render();
}
},
flipViewportVertical: () => {
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { flipVertical } = viewport.getCamera();
viewport.setCamera({ flipVertical: !flipVertical });
viewport.render();
}
},
invertViewport: ({ element }) => {
let enabledElement;
if (element === undefined) {
enabledElement = _getActiveViewportEnabledElement();
} else {
enabledElement = element;
}
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { invert } = viewport.getProperties();
viewport.setProperties({ invert: !invert });
viewport.render();
}
},
resetViewport: () => {
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
viewport.resetProperties();
viewport.resetCamera();
viewport.render();
}
},
scaleViewport: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement();
const scaleFactor = direction > 0 ? 0.9 : 1.1;
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
if (direction) {
const { parallelScale } = viewport.getCamera();
viewport.setCamera({ parallelScale: parallelScale * scaleFactor });
viewport.render();
} else {
viewport.resetCamera();
viewport.render();
}
}
},
scroll: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement();
elementToolData.data.forEach((toolData, index) => {
let elementToolInstance = cornerstoneTools.getToolForElement(
element,
toolType
);
if (!enabledElement) {
return;
}
if (!elementToolInstance) {
elementToolInstance = cornerstoneTools.getToolForElement(
element,
`${toolType}Tool`
);
}
const { viewport } = enabledElement;
const options = { delta: direction };
if (!elementToolInstance) {
console.warn('Tool not found.');
return undefined;
}
csToolsUtils.scroll(viewport, options);
},
async createSegmentationForDisplaySet({ displaySetInstanceUID }) {
const volumeId = displaySetInstanceUID;
if (
elementToolInstance.pointNearTool(
element,
toolData,
canvasCoordinates
)
) {
pointNearTool = true;
nearbyTool.tool = toolData;
nearbyTool.index = index;
nearbyTool.toolType = toolType;
}
});
const segmentationUID = csUtils.uuidv4();
const segmentationId = `${volumeId}::${segmentationUID}`;
if (pointNearTool) {
return false;
}
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
volumeId: segmentationId,
});
return pointNearTool ? nearbyTool : undefined;
},
removeToolState: ({ element, toolType, tool }) => {
cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element);
},
// setCornerstoneLayout: () => {
// setCornerstoneLayout();
// },
setWindowLevel: ({ window, level }) => {
const enabledElement = _getActiveViewportsEnabledElement();
// Add the segmentations to state
segmentation.addSegmentations([
{
segmentationId,
representation: {
// The type of segmentation
type: Enums.SegmentationRepresentations.Labelmap,
// The actual segmentation data, in the case of labelmap this is a
// reference to the source volume of the segmentation.
data: {
volumeId: segmentationId,
},
},
},
]);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
return segmentationId;
},
async addSegmentationRepresentationToToolGroup({
segmentationId,
toolGroupId,
representationType,
}) {
// // Add the segmentation representation to the toolgroup
await segmentation.addSegmentationRepresentations(toolGroupId, [
{
segmentationId,
type: representationType,
},
]);
},
getLabelmapVolumes: ({ segmentations }) => {
if (!segmentations || !segmentations.length) {
segmentations = SegmentationService.getSegmentations();
}
viewport.voi = {
windowWidth: Number(window),
windowCenter: Number(level),
};
cornerstone.setViewport(enabledElement, viewport);
const labelmapVolumes = segmentations.map(segmentation => {
return cache.getVolume(segmentation.id);
});
return labelmapVolumes;
},
setViewportColormap: ({
viewportIndex,
displaySetInstanceUID,
colormap,
immediate = false,
}) => {
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const actorEntries = viewport.getActors();
const actorEntry = actorEntries.find(actorEntry => {
return actorEntry.uid === displaySetInstanceUID;
});
const { actor: volumeActor } = actorEntry;
setColormap(volumeActor, colormap);
if (immediate) {
viewport.render();
}
},
};
const definitions = {
jumpToImage: {
commandFn: actions.jumpToImage,
setWindowLevel: {
commandFn: actions.setWindowLevel,
storeContexts: [],
options: {},
},
getCornerstoneLibraries: {
commandFn: actions.getCornerstoneLibraries,
storeContexts: [],
options: {},
context: 'VIEWER',
},
getNearbyToolData: {
commandFn: actions.getNearbyToolData,
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
options: {},
},
toggleSynchronizer: {
commandFn: actions.toggleSynchronizer,
storeContexts: [],
options: {},
},
removeToolState: {
commandFn: actions.removeToolState,
storeContexts: [],
options: {},
},
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
storeContexts: [],
options: {},
},
getActiveViewportEnabledElement: {
commandFn: actions.getActiveViewportEnabledElement,
toggleCrosshairs: {
commandFn: actions.toggleCrosshairs,
storeContexts: [],
options: {},
},
@ -414,13 +433,8 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
storeContexts: [],
options: { rotation: -90 },
},
invertViewport: {
commandFn: actions.invertViewport,
storeContexts: [],
options: {},
},
cancelMeasurement: {
commandFn: actions.cancelMeasurement,
flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal,
storeContexts: [],
options: {},
},
@ -429,8 +443,13 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
storeContexts: [],
options: {},
},
flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal,
invertViewport: {
commandFn: actions.invertViewport,
storeContexts: [],
options: {},
},
resetViewport: {
commandFn: actions.resetViewport,
storeContexts: [],
options: {},
},
@ -449,45 +468,54 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
storeContexts: [],
options: { direction: 0 },
},
resetViewport: {
commandFn: actions.resetViewport,
storeContexts: [],
options: {},
},
clearAnnotations: {
commandFn: actions.clearAnnotations,
storeContexts: [],
options: {},
},
nextImage: {
commandFn: actions.nextImage,
commandFn: actions.scroll,
storeContexts: [],
options: {},
options: { direction: 1 },
},
previousImage: {
commandFn: actions.previousImage,
commandFn: actions.scroll,
storeContexts: [],
options: { direction: -1 },
},
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
storeContexts: [],
options: {},
},
// TOOLS
setToolActive: {
commandFn: actions.setToolActive,
toggleCine: {
commandFn: actions.toggleCine,
storeContexts: [],
options: {},
},
setZoomTool: {
commandFn: actions.setToolActive,
arrowTextCallback: {
commandFn: actions.arrowTextCallback,
storeContexts: [],
options: { toolName: 'Zoom' },
options: {},
},
// setCornerstoneLayout: {
// commandFn: actions.setCornerstoneLayout,
// storeContexts: [],
// options: {},
// context: 'VIEWER',
// },
setWindowLevel: {
commandFn: actions.setWindowLevel,
setViewportActive: {
commandFn: actions.setViewportActive,
storeContexts: [],
options: {},
},
createSegmentationForDisplaySet: {
commandFn: actions.createSegmentationForDisplaySet,
storeContexts: [],
options: {},
},
addSegmentationRepresentationToToolGroup: {
commandFn: actions.addSegmentationRepresentationToToolGroup,
storeContexts: [],
options: {},
},
getLabelmapVolumes: {
commandFn: actions.getLabelmapVolumes,
storeContexts: [],
options: {},
},
setViewportColormap: {
commandFn: actions.setViewportColormap,
storeContexts: [],
options: {},
},
@ -496,7 +524,7 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
return {
actions,
definitions,
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
defaultContext: 'CORNERSTONE',
};
};

View File

@ -1,61 +0,0 @@
import React from 'react';
import init from './init.js';
import commandsModule from './commandsModule.js';
import { id } from './id.js';
// import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './OHIFCornerstoneViewport');
});
const OHIFCornerstoneViewport = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
/**
*
*/
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id,
/**
*
*
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration });
},
getViewportModule({ servicesManager, commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => {
const onNewImageHandler = jumpData => {
commandsManager.runCommand('jumpToImage', jumpData);
};
const { ToolBarService } = servicesManager.services;
return (
<OHIFCornerstoneViewport
{...props}
ToolBarService={ToolBarService}
servicesManager={servicesManager}
commandsManager={commandsManager}
onNewImage={onNewImageHandler}
/>
);
};
return [
{ name: 'cornerstone', component: ExtendedOHIFCornerstoneViewport },
];
},
getCommandsModule({ servicesManager, commandsManager }) {
return commandsModule({ servicesManager, commandsManager });
},
};

View File

@ -0,0 +1,136 @@
import React from 'react';
import * as cornerstone from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
import {
Enums as cs3DEnums,
CONSTANTS,
imageLoadPoolManager,
imageRetrievalPoolManager,
} from '@cornerstonejs/core';
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
import init from './init.js';
import commandsModule from './commandsModule';
import ToolGroupService from './services/ToolGroupService';
import SyncGroupService from './services/SyncGroupService';
import { toolNames } from './initCornerstoneTools';
import { getEnabledElement } from './state';
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
import dicomLoaderService from './utils/dicomLoaderService';
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
import { id } from './id';
const Component = React.lazy(() => {
return import(
/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport'
);
});
const OHIFCornerstoneViewport = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
/**
*
*/
const cornerstoneExtension = {
/**
* Only required property. Should be a unique value across all extensions.
*/
id,
onModeExit: () => {
// Empty out the image load and retrieval pools to prevent memory leaks
// on the mode exits
Object.values(cs3DEnums.RequestType).forEach(type => {
imageLoadPoolManager.clearRequestStack(type);
imageRetrievalPoolManager.clearRequestStack(type);
});
},
/**
*
*
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
async preRegistration({
servicesManager,
commandsManager,
configuration = {},
appConfig,
}) {
servicesManager.registerService(
CornerstoneViewportService(servicesManager)
);
servicesManager.registerService(ToolGroupService(servicesManager));
servicesManager.registerService(SyncGroupService(servicesManager));
await init({ servicesManager, commandsManager, configuration, appConfig });
},
getViewportModule({ servicesManager, commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => {
// const onNewImageHandler = jumpData => {
// commandsManager.runCommand('jumpToImage', jumpData);
// };
const { ToolBarService } = servicesManager.services;
return (
<OHIFCornerstoneViewport
{...props}
ToolBarService={ToolBarService}
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
);
};
return [
{
name: 'cornerstone',
component: ExtendedOHIFCornerstoneViewport,
},
];
},
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
return commandsModule({
servicesManager,
commandsManager,
extensionManager,
});
},
getUtilityModule({ servicesManager }) {
return [
{
name: 'common',
exports: {
getCornerstoneLibraries: () => {
return { cornerstone, cornerstoneTools };
},
getEnabledElement,
dicomLoaderService,
registerColormap,
},
},
{
name: 'core',
exports: {
Enums: cs3DEnums,
CONSTANTS,
},
},
{
name: 'tools',
exports: {
toolNames,
Enums: cs3DToolsEnums,
},
},
];
},
};
export default cornerstoneExtension;

View File

@ -1,86 +1,126 @@
import OHIF from '@ohif/core';
import { ContextMenuMeasurements } from '@ohif/ui';
import cs from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import merge from 'lodash.merge';
import getTools, { toolsGroupedByType } from './utils/getTools.js';
import initCornerstoneTools from './initCornerstoneTools.js';
import initWADOImageLoader from './initWADOImageLoader.js';
import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import { setEnabledElement } from './state';
import callInputDialog from './callInputDialog.js';
// TODO -> Global "context menu open state", or lots of expensive searches on drag?
import * as cornerstone from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
import {
init as cs3DInit,
eventTarget,
EVENTS,
metaData,
volumeLoader,
imageLoader,
imageLoadPoolManager,
Settings,
} from '@cornerstonejs/core';
import { Enums, utilities } from '@cornerstonejs/tools';
import {
cornerstoneStreamingImageVolumeLoader,
sharedArrayBufferImageLoader,
} from '@cornerstonejs/streaming-image-volume-loader';
import initWADOImageLoader from './initWADOImageLoader';
import initCornerstoneTools from './initCornerstoneTools';
import { connectToolsToMeasurementService } from './initMeasurementService';
import callInputDialog from './utils/callInputDialog';
import initCineService from './initCineService';
import interleaveCenterLoader from './utils/interleaveCenterLoader';
import interleaveTopToBottom from './utils/interleaveTopToBottom';
import initSegmentationService from './initSegmentationService';
const cs3DToolsEvents = Enums.Events;
let CONTEXT_MENU_OPEN = false;
const { globalImageIdSpecificToolStateManager } = csTools;
const TOOL_TYPES_WITH_CONTEXT_MENU = [
'Angle',
'ArrowAnnotate',
'Bidirectional',
'Length',
'FreehandMouse',
'EllipticalRoi',
'CircleRoi',
'RectangleRoi',
// SR Viewport...
'SRLength',
'SRBidirectional',
'SRArrowAnnotate',
'SREllipticalRoi',
];
const _refreshViewports = () =>
cs.getEnabledElements().forEach(({ element }) => cs.updateImage(element));
/* Add extension tools configuration here. */
const _createInternalToolsConfig = UIDialogService => {
return {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(UIDialogService, null, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(UIDialogService, data, callback),
allowEmptyLabel: true,
},
},
DragProbe: {
defaultStrategy: 'minimal',
},
};
};
// TODO: Cypress tests are currently grabbing this from the window?
window.cornerstone = cornerstone;
window.cornerstoneTools = cornerstoneTools;
/**
*
* @param {Object} servicesManager
* @param {Object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({
export default async function init({
servicesManager,
commandsManager,
configuration,
appConfig,
}) {
await cs3DInit();
// For debugging e2e tests that are failing on CI
//cornerstone.setUseCPURendering(true);
// For debugging large datasets
//cornerstone.cache.setMaxCacheSize(3000000000);
initCornerstoneTools();
// Don't use cursors in viewports
// Todo: this should come from extension/app configuration
Settings.getRuntimeSettings().set('useCursors', false);
const {
UIDialogService,
UserAuthenticationService,
ToolGroupService,
MeasurementService,
DisplaySetService,
ToolBarService,
UserAuthenticationService,
UIDialogService,
CineService,
CornerstoneViewportService,
HangingProtocolService,
SegmentationService,
} = servicesManager.services;
const tools = getTools();
console.log(servicesManager.services);
const metadataProvider = OHIF.classes.MetadataProvider;
volumeLoader.registerUnknownVolumeLoader(
cornerstoneStreamingImageVolumeLoader
);
volumeLoader.registerVolumeLoader(
'cornerstoneStreamingImageVolume',
cornerstoneStreamingImageVolumeLoader
);
HangingProtocolService.registerImageLoadStrategy(
'interleaveCenter',
interleaveCenterLoader
);
HangingProtocolService.registerImageLoadStrategy(
'interleaveTopToBottom',
interleaveTopToBottom
);
imageLoader.registerImageLoader(
'streaming-wadors',
sharedArrayBufferImageLoader
);
metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
imageLoadPoolManager.maxNumRequests = {
interaction: appConfig?.maxNumRequests?.interaction || 100,
thumbnail: appConfig?.maxNumRequests?.thumbnail || 75,
prefetch: appConfig?.maxNumRequests?.prefetch || 10,
};
initWADOImageLoader(UserAuthenticationService, appConfig);
/* Measurement Service */
const measurementServiceSource = _connectToolsToMeasurementService(
const measurementServiceSource = connectToolsToMeasurementService(
MeasurementService,
DisplaySetService
DisplaySetService,
CornerstoneViewportService
);
initSegmentationService(SegmentationService, CornerstoneViewportService);
initCineService(CineService);
const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client[0]) || 0,
y: (event && event.currentPoints.client[1]) || 0,
});
const onRightClick = event => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
@ -89,16 +129,17 @@ export default function init({
const onGetMenuItems = defaultMenuItems => {
const { element, currentPoints } = event.detail;
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
const nearbyToolData = utilities.getAnnotationNearPoint(
element,
canvasCoordinates: currentPoints.canvas,
availableToolTypes: TOOL_TYPES_WITH_CONTEXT_MENU,
});
currentPoints.canvas
);
let menuItems = [];
if (nearbyToolData) {
defaultMenuItems.forEach(item => {
item.value = nearbyToolData;
item.element = element;
menuItems.push(item);
});
}
@ -123,41 +164,15 @@ export default function init({
onGetMenuItems,
eventData: event.detail,
onDelete: item => {
const { tool: measurementData, toolType } = item.value;
const { annotationUID } = item.value;
const uid = annotationUID;
// Sync'd w/ Measurement Service
if (measurementData.id) {
measurementServiceSource.remove(measurementData.id);
}
// Only in cstools
else {
const toolState =
csTools.globalImageIdSpecificToolStateManager.toolState;
Object.keys(toolState).forEach(imageId => {
Object.keys(toolState[imageId]).forEach(imageToolType => {
const numMeasurements =
toolState[imageId][imageToolType].data.length;
for (let i = 0; i < numMeasurements; i++) {
const toolData = toolState[imageId][imageToolType].data[i];
if (measurementData.uuid === toolData.uuid) {
// Clear and nuke matching measurement
toolState[imageId][imageToolType].data[i] = null;
delete toolState[imageId][imageToolType].data[i];
toolState[imageId][imageToolType].data.splice(i, 1);
// Delete "data" if we deleted last measurement in array
// if (toolState[imageId][imageToolType].data.length === 0) {
// delete toolState[imageId][imageToolType].data;
// }
}
}
});
if (uid) {
measurementServiceSource.remove(uid, {
element: item.element,
});
console.log(csTools.store.state);
}
_refreshViewports();
CONTEXT_MENU_OPEN = false;
},
onClose: () => {
@ -165,11 +180,9 @@ export default function init({
UIDialogService.dismiss({ id: 'context-menu' });
},
onSetLabel: item => {
const { tool: measurementData } = item.value;
const { annotationUID } = item.value;
const measurement = MeasurementService.getMeasurement(
measurementData.id
);
const measurement = MeasurementService.getMeasurement(annotationUID);
callInputDialog(
UIDialogService,
@ -184,7 +197,7 @@ export default function init({
});
MeasurementService.update(
updatedMeasurement.id,
updatedMeasurement.uid,
updatedMeasurement,
true
);
@ -198,19 +211,6 @@ export default function init({
});
};
const onTouchPress = event => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.create({
eventData: event.detail,
content: ContextMenuMeasurements,
contentProps: { isTouchEvent: true },
});
};
const resetContextMenu = () => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
@ -222,6 +222,24 @@ export default function init({
UIDialogService.dismiss({ id: 'context-menu' });
};
// When a custom image load is performed, update the relevant viewports
HangingProtocolService.subscribe(
HangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
volumeInputArrayMap => {
for (const entry of volumeInputArrayMap.entries()) {
const [viewportId, volumeInputArray] = entry;
const viewport = CornerstoneViewportService.getCornerstoneViewport(
viewportId
);
CornerstoneViewportService.setVolumesForViewport(
viewport,
volumeInputArray
);
}
}
);
/*
* Because click gives us the native "mouse up", buttons will always be `0`
* Need to fallback to event.which;
@ -235,445 +253,50 @@ export default function init({
clickMethodHandler(evt);
};
const cancelContextMenuIfOpen = evt => {
if (CONTEXT_MENU_OPEN) {
resetContextMenu();
}
};
// const cancelContextMenuIfOpen = evt => {
// if (CONTEXT_MENU_OPEN) {
// resetContextMenu();
// }
// };
// TODO: This is the handler for ALL ENABLED ELEMENT EVENTS
// ... Activation logic should take place per element, not for all (diff behavior per ext)
function elementEnabledHandler(tools, evt) {
const element = evt.detail.element;
function elementEnabledHandler(evt) {
const { element } = evt.detail;
_addConfiguredToolsForElement(
UIDialogService,
element,
tools,
configuration
);
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.addEventListener(
csTools.EVENTS.MOUSE_CLICK,
cs3DToolsEvents.MOUSE_CLICK,
contextMenuHandleClick
);
element.addEventListener(cs.EVENTS.NEW_IMAGE, cancelContextMenuIfOpen);
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
const { element } = evt.detail;
utilities.stackPrefetch.enable(element);
});
}
function elementDisabledHandler(evt) {
const element = evt.detail.element;
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
const { viewportId, element } = evt.detail;
const viewportInfo = CornerstoneViewportService.getViewportInfo(viewportId);
ToolGroupService.disable(viewportInfo);
element.removeEventListener(
csTools.EVENTS.MOUSE_CLICK,
cs3DToolsEvents.MOUSE_CLICK,
contextMenuHandleClick
);
element.removeEventListener(cs.EVENTS.NEW_IMAGE, cancelContextMenuIfOpen);
}
const { csToolsConfig } = configuration;
const metadataProvider = OHIF.classes.MetadataProvider;
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
initWADOImageLoader(UserAuthenticationService);
// ~~
const defaultCsToolsConfig = csToolsConfig || {
globalToolSyncEnabled: false, // hold on to your pants!
showSVGCursors: false,
autoResizeViewports: false,
};
initCornerstoneTools(defaultCsToolsConfig);
// TODO: Extensions are still registered at time of registration globally
// These should be registered as a part of mode route spin up,
// and they need to self-clean on mode route destroy
// THIS
// is a way for extensions that "depend" on this extension to notify it of
// new cornerstone enabled elements so it's commands continue to work.
const handleOhifCornerstoneEnabledElementEvent = function(evt) {
const { context, viewportIndex, enabledElement } = evt.detail;
setEnabledElement(viewportIndex, enabledElement, context);
};
document.addEventListener(
'ohif-cornerstone-enabled-element-event',
handleOhifCornerstoneEnabledElementEvent
);
cs.events.addEventListener(
cs.EVENTS.ELEMENT_ENABLED,
elementEnabledHandler.bind(null, tools)
);
cs.events.addEventListener(
cs.EVENTS.ELEMENT_DISABLED,
elementDisabledHandler
);
}
const _initMeasurementService = (MeasurementService, DisplaySetService) => {
/* Initialization */
const {
Length,
Bidirectional,
EllipticalRoi,
ArrowAnnotate,
} = measurementServiceMappingsFactory(MeasurementService, DisplaySetService);
const csToolsVer4MeasurementSource = MeasurementService.createSource(
'CornerstoneTools',
'4'
);
/* Mappings */
MeasurementService.addMapping(
csToolsVer4MeasurementSource,
'Length',
Length.matchingCriteria,
Length.toAnnotation,
Length.toMeasurement
);
MeasurementService.addMapping(
csToolsVer4MeasurementSource,
'Bidirectional',
Bidirectional.matchingCriteria,
Bidirectional.toAnnotation,
Bidirectional.toMeasurement
);
MeasurementService.addMapping(
csToolsVer4MeasurementSource,
'EllipticalRoi',
EllipticalRoi.matchingCriteria,
EllipticalRoi.toAnnotation,
EllipticalRoi.toMeasurement
);
MeasurementService.addMapping(
csToolsVer4MeasurementSource,
'ArrowAnnotate',
ArrowAnnotate.matchingCriteria,
ArrowAnnotate.toAnnotation,
ArrowAnnotate.toMeasurement
);
return csToolsVer4MeasurementSource;
};
const _connectToolsToMeasurementService = (
MeasurementService,
DisplaySetService
) => {
const csToolsVer4MeasurementSource = _initMeasurementService(
MeasurementService,
DisplaySetService
);
_connectMeasurementServiceToTools(
MeasurementService,
csToolsVer4MeasurementSource
);
const { addOrUpdate, remove } = csToolsVer4MeasurementSource;
const elementEnabledEvt = cs.EVENTS.ELEMENT_ENABLED;
/* Measurement Service Events */
cs.events.addEventListener(elementEnabledEvt, evt => {
// TODO: Debounced update of measurements that are modified
function addMeasurement(csToolsEvent) {
console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail);
try {
const evtDetail = csToolsEvent.detail;
const { toolName, toolType, measurementData } = evtDetail;
const csToolName = toolName || measurementData.toolType || toolType;
const measurementId = addOrUpdate(csToolName, evtDetail);
if (measurementId) {
measurementData.id = measurementId;
}
} catch (error) {
console.warn('Failed to add measurement:', error);
}
}
function updateMeasurement(csToolsEvent) {
try {
if (!csToolsEvent.detail.measurementData.id) {
return;
}
const evtDetail = csToolsEvent.detail;
const { toolName, toolType, measurementData } = evtDetail;
const csToolName = toolName || measurementData.toolType || toolType;
evtDetail.id = csToolsEvent.detail.measurementData.id;
addOrUpdate(csToolName, evtDetail);
} catch (error) {
console.warn('Failed to update measurement:', error);
}
}
/**
* When csTools fires a removed event, remove the same measurement
* from the measurement service
*
* @param {*} csToolsEvent
*/
function removeMeasurement(csToolsEvent) {
console.log('~~ removeEvt', csToolsEvent);
try {
if (csToolsEvent.detail.measurementData.id) {
remove(csToolsEvent.detail.measurementData.id);
}
} catch (error) {
console.warn('Failed to remove measurement:', error);
}
}
const {
MEASUREMENTS_CLEARED,
MEASUREMENT_UPDATED,
} = MeasurementService.EVENTS;
MeasurementService.subscribe(MEASUREMENTS_CLEARED, () => {
globalImageIdSpecificToolStateManager.restoreToolState({});
_refreshViewports();
eventTarget.removeEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
const { element } = evt.detail;
utilities.stackPrefetch.disable(element);
});
MeasurementService.subscribe(
MEASUREMENT_UPDATED,
({ source, measurement, notYetUpdatedAtSource }) => {
const { id, label } = measurement;
if (
source.name == 'CornerstoneTools' &&
notYetUpdatedAtSource === false
) {
// This event was fired by cornerstone telling the measurement service to sync. Already in sync.
return;
}
const cornerstoneMeasurement = getCornerstoneMeasurementById(id);
if (cornerstoneMeasurement) {
cornerstoneMeasurement.label = label;
if (cornerstoneMeasurement.hasOwnProperty('text')) {
// Deal with the weird case of ArrowAnnotate.
cornerstoneMeasurement.text = label;
}
_refreshViewports();
}
}
);
// on display sets added, check if there are any measurements in measurement service that need to be
// put into cornerstone tools tooldata
const enabledElement = evt.detail.element;
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
const removedEvt = csTools.EVENTS.MEASUREMENT_REMOVED;
enabledElement.addEventListener(completedEvt, addMeasurement);
enabledElement.addEventListener(updatedEvt, updateMeasurement);
enabledElement.addEventListener(removedEvt, removeMeasurement);
});
return csToolsVer4MeasurementSource;
};
const _connectMeasurementServiceToTools = (
MeasurementService,
measurementSource,
dataSource
) => {
const {
MEASUREMENT_REMOVED,
RAW_MEASUREMENT_ADDED,
} = MeasurementService.EVENTS;
const sourceId = measurementSource.id;
// TODO: This is an unsafe delete
// Cornerstone-tools should probably expose a more generic "delete by id"
// And have toolState managers expose a method to find any of their toolState by ID
// --> csTools.deleteById --> internally checks all registered modules/managers?
//
// This implementation assumes a single globalImageIdSpecificToolStateManager
// It iterates all toolState for all toolTypes, and deletes any with a matching id
//
// Could potentially use "source" from event to determine tool type and skip some
// iterations?
const {
POLYLINE,
ELLIPSE,
POINT,
BIDIRECTIONAL,
} = MeasurementService.VALUE_TYPES;
// TODO -> I get why this was attemped, but its not nearly flexible enough.
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
// You can't define a bidirectional tool as a single type..
// OHIF-230
const TOOL_TYPE_TO_VALUE_TYPE = {
Length: POLYLINE,
EllipticalRoi: ELLIPSE,
Bidirectional: BIDIRECTIONAL,
ArrowAnnotate: POINT,
};
const VALUE_TYPE_TO_TOOL_TYPE = {
[POLYLINE]: 'Length',
[ELLIPSE]: 'EllipticalRoi',
[BIDIRECTIONAL]: 'Bidirectional',
[POINT]: 'ArrowAnnotate',
};
MeasurementService.subscribe(
RAW_MEASUREMENT_ADDED,
({ source, measurement, data, dataSource }) => {
const {
referenceStudyUID: StudyInstanceUID,
referenceSeriesUID: SeriesInstanceUID,
SOPInstanceUID,
} = measurement;
let toolType;
try {
toolType = VALUE_TYPE_TO_TOOL_TYPE[measurement.type];
} catch {
throw Error('Cannot add tool to cornerstone tools');
}
let imageId;
if (data.imageId) {
// handle dicom json launch, since we cannot create image id from
// instance UIDs, each tool should embed the instance metadata
// TODO: handle multi instance case
imageId = data.imageId;
} else {
// handle general case of dicom web
const instance = {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
};
// TODO: handle multi frame
imageId = dataSource.getImageIdsForInstance({ instance });
}
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
if (toolState[imageId] === undefined) {
toolState[imageId] = {};
}
const imageIdToolState = toolState[imageId];
// If we don't have tool state for this type of tool, add an empty object
if (imageIdToolState[toolType] === undefined) {
imageIdToolState[toolType] = {
data: [],
};
}
const toolData = imageIdToolState[toolType];
toolData.data.push(data);
}
);
MeasurementService.subscribe(
MEASUREMENT_REMOVED,
({ source, measurement: removedMeasurementId }) => {
// THIS POINTS TO ORIGINAL; Not a copy
const imageIdSpecificToolState = globalImageIdSpecificToolStateManager.saveToolState();
// ImageId -->
Object.keys(imageIdSpecificToolState).forEach(imageId => {
// ImageId --> Tool -->
Object.keys(imageIdSpecificToolState[imageId]).forEach(toolName => {
const toolState = imageIdSpecificToolState[imageId][toolName];
let annotationIndex = toolState.data.length - 1;
while (annotationIndex >= 0) {
const annotation = toolState.data[annotationIndex];
if (annotation.id === removedMeasurementId) {
toolState.data.splice(annotationIndex, 1);
}
annotationIndex--;
}
});
});
}
);
};
const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client.x) || 0,
y: (event && event.currentPoints.client.y) || 0,
});
/**
* @private
*/
function _addConfiguredToolsForElement(
UIDialogService,
element,
tools,
configuration
) {
const internalToolsConfig = _createInternalToolsConfig(UIDialogService);
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = new tool().name;
const externalToolsConfig = configuration.tools || {};
const externalToolProps = externalToolsConfig[toolName] || {};
const internalToolProps = internalToolsConfig[toolName] || {};
const props = merge(
internalToolProps,
_parseToolProps(configuration, externalToolProps, tool)
);
csTools.addToolForElement(element, tool, props);
});
}
/* Abstract tools configuration using extension configuration. */
function _parseToolProps(configuration, props, tool) {
const { annotations } = toolsGroupedByType;
// An alternative approach would be to remove the `drawHandlesOnHover` config
// from the supported configuration properties in `cornerstone-tools`
const toolsWithHideableHandles = annotations.filter(
tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name)
);
let parsedProps = { ...props };
/**
* drawHandles - Never/Always show handles
* drawHandlesOnHover - Only show handles on handle hover (pointNearHandle)
* hideHandlesIfMoving - Hides the handles whilst you are moving them, for better visibility.
*
* Does not apply to tools where handles aren't placed in predictable
* locations.
*/
if (
configuration.hideHandles !== false &&
toolsWithHideableHandles.includes(tool)
) {
if (props.configuration) {
parsedProps.configuration.drawHandlesOnHover = true;
parsedProps.configuration.hideHandlesIfMoving = true;
} else {
parsedProps.configuration = {
drawHandlesOnHover: true,
hideHandlesIfMoving: true,
};
}
}
return parsedProps;
eventTarget.addEventListener(
EVENTS.ELEMENT_ENABLED,
elementEnabledHandler.bind(null)
);
eventTarget.addEventListener(
EVENTS.ELEMENT_DISABLED,
elementDisabledHandler.bind(null)
);
}

View File

@ -0,0 +1,15 @@
import { utilities } from '@cornerstonejs/tools';
function initCineService(CineService) {
const playClip = (element, playClipOptions) => {
return utilities.cine.playClip(element, playClipOptions);
};
const stopClip = element => {
return utilities.cine.stopClip(element);
};
CineService.setServiceImplementation({ playClip, stopClip });
}
export default initCineService;

View File

@ -1,47 +1,79 @@
import Hammer from 'hammerjs';
import cornerstone from 'cornerstone-core';
import cornerstoneMath from 'cornerstone-math';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import {
PanTool,
WindowLevelTool,
StackScrollTool,
StackScrollMouseWheelTool,
ZoomTool,
VolumeRotateMouseWheelTool,
MIPJumpToClickTool,
LengthTool,
RectangleROITool,
EllipticalROITool,
BidirectionalTool,
ArrowAnnotateTool,
DragProbeTool,
AngleTool,
MagnifyTool,
CrosshairsTool,
SegmentationDisplayTool,
init,
addTool,
annotation,
} from '@cornerstonejs/tools';
const { log } = OHIF;
export default function initCornerstoneTools(configuration = {}) {
init(configuration);
addTool(PanTool);
addTool(WindowLevelTool);
addTool(StackScrollMouseWheelTool);
addTool(StackScrollTool);
addTool(ZoomTool);
addTool(VolumeRotateMouseWheelTool);
addTool(MIPJumpToClickTool);
addTool(LengthTool);
addTool(RectangleROITool);
addTool(EllipticalROITool);
addTool(BidirectionalTool);
addTool(ArrowAnnotateTool);
addTool(DragProbeTool);
addTool(AngleTool);
addTool(MagnifyTool);
addTool(CrosshairsTool);
addTool(SegmentationDisplayTool);
export default function(configuration = {}) {
// TODO: Cypress tests are currently grabbing this from the window?
window.cornerstone = cornerstone;
// Modify annotation tools to use dashed lines on SR
const annotationStyle = {
textBoxFontSize: '15px',
lineWidth: '1.5',
};
// For debugging
window.cornerstoneTools = cornerstoneTools;
cornerstoneTools.external.cornerstone = cornerstone;
cornerstoneTools.external.Hammer = Hammer;
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
cornerstoneTools.init(configuration);
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(
(element, imageId, error) => {
log.error(imageId);
throw error;
}
);
// Set the tool font and font size
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
const fontFamily =
'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`);
// Tool styles/colors
cornerstoneTools.toolStyle.setToolWidth(2);
cornerstoneTools.toolColors.setToolColor('rgb(255, 255, 0)');
cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)');
cornerstoneTools.store.state.touchProximity = 40;
// Configure stack prefetch
cornerstoneTools.stackPrefetch.setConfiguration({
maxImagesToPrefetch: Infinity,
preserveExistingPool: false,
maxSimultaneousRequests: 20,
const defaultStyles = annotation.config.style.getDefaultToolStyles();
annotation.config.style.setDefaultToolStyles({
global: {
...defaultStyles.global,
...annotationStyle,
},
});
}
const toolNames = {
Pan: PanTool.toolName,
ArrowAnnotate: ArrowAnnotateTool.toolName,
WindowLevel: WindowLevelTool.toolName,
StackScroll: StackScrollTool.toolName,
StackScrollMouseWheel: StackScrollMouseWheelTool.toolName,
Zoom: ZoomTool.toolName,
VolumeRotateMouseWheel: VolumeRotateMouseWheelTool.toolName,
MipJumpToClick: MIPJumpToClickTool.toolName,
Length: LengthTool.toolName,
DragProbe: DragProbeTool.toolName,
RectangleROI: RectangleROITool.toolName,
EllipticalROI: EllipticalROITool.toolName,
Bidirectional: BidirectionalTool.toolName,
Angle: AngleTool.toolName,
Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName,
SegmentationDisplay: SegmentationDisplayTool.toolName,
};
export { toolNames };

View File

@ -0,0 +1,296 @@
import { eventTarget } from '@cornerstonejs/core';
import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
const { removeAnnotation } = annotation.state;
const csToolsEvents = Enums.Events;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const initMeasurementService = (
MeasurementService,
DisplaySetService,
CornerstoneViewportService
) => {
/* Initialization */
const {
Length,
Bidirectional,
EllipticalROI,
ArrowAnnotate,
} = measurementServiceMappingsFactory(
MeasurementService,
DisplaySetService,
CornerstoneViewportService
);
const csTools3DVer1MeasurementSource = MeasurementService.createSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
/* Mappings */
MeasurementService.addMapping(
csTools3DVer1MeasurementSource,
'Length',
Length.matchingCriteria,
Length.toAnnotation,
Length.toMeasurement
);
MeasurementService.addMapping(
csTools3DVer1MeasurementSource,
'Bidirectional',
Bidirectional.matchingCriteria,
Bidirectional.toAnnotation,
Bidirectional.toMeasurement
);
MeasurementService.addMapping(
csTools3DVer1MeasurementSource,
'EllipticalROI',
EllipticalROI.matchingCriteria,
EllipticalROI.toAnnotation,
EllipticalROI.toMeasurement
);
MeasurementService.addMapping(
csTools3DVer1MeasurementSource,
'ArrowAnnotate',
ArrowAnnotate.matchingCriteria,
ArrowAnnotate.toAnnotation,
ArrowAnnotate.toMeasurement
);
return csTools3DVer1MeasurementSource;
};
const connectToolsToMeasurementService = (
MeasurementService,
DisplaySetService,
CornerstoneViewportService
) => {
const csTools3DVer1MeasurementSource = initMeasurementService(
MeasurementService,
DisplaySetService,
CornerstoneViewportService
);
connectMeasurementServiceToTools(
MeasurementService,
CornerstoneViewportService,
csTools3DVer1MeasurementSource
);
const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource;
//
function addMeasurement(csToolsEvent) {
try {
const annotationAddedEventDetail = csToolsEvent.detail;
const {
annotation: { metadata, annotationUID },
} = annotationAddedEventDetail;
const { toolName } = metadata;
// To force the measurementUID be the same as the annotationUID
// Todo: this should be changed when a measurement can include multiple annotations
// in the future
annotationAddedEventDetail.uid = annotationUID;
annotationToMeasurement(toolName, annotationAddedEventDetail);
} catch (error) {
console.warn('Failed to update measurement:', error);
}
}
function updateMeasurement(csToolsEvent) {
try {
const annotationModifiedEventDetail = csToolsEvent.detail;
const {
annotation: { metadata, annotationUID },
} = annotationModifiedEventDetail;
// If the measurement hasn't been added, don't modify it
const measurement = MeasurementService.getMeasurement(annotationUID);
if (!measurement) {
return;
}
const { toolName } = metadata;
annotationModifiedEventDetail.uid = annotationUID;
annotationToMeasurement(toolName, annotationModifiedEventDetail);
} catch (error) {
console.warn('Failed to update measurement:', error);
}
}
/**
* When csTools fires a removed event, remove the same measurement
* from the measurement service
*
* @param {*} csToolsEvent
*/
function removeMeasurement(csToolsEvent) {
try {
try {
const annotationRemovedEventDetail = csToolsEvent.detail;
const {
annotation: { annotationUID },
} = annotationRemovedEventDetail;
const measurement = MeasurementService.getMeasurement(annotationUID);
if (measurement) {
console.log('~~ removeEvt', csToolsEvent);
remove(annotationUID, annotationRemovedEventDetail);
}
} catch (error) {
console.warn('Failed to update measurement:', error);
}
} catch (error) {
console.warn('Failed to remove measurement:', error);
}
}
// on display sets added, check if there are any measurements in measurement service that need to be
// put into cornerstone tools
const completedEvt = csToolsEvents.ANNOTATION_COMPLETED;
const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED;
const removedEvt = csToolsEvents.ANNOTATION_REMOVED;
eventTarget.addEventListener(completedEvt, addMeasurement);
eventTarget.addEventListener(updatedEvt, updateMeasurement);
eventTarget.addEventListener(removedEvt, removeMeasurement);
return csTools3DVer1MeasurementSource;
};
const connectMeasurementServiceToTools = (
MeasurementService,
CornerstoneViewportService,
measurementSource
) => {
const {
MEASUREMENT_REMOVED,
MEASUREMENTS_CLEARED,
MEASUREMENT_UPDATED,
RAW_MEASUREMENT_ADDED,
} = MeasurementService.EVENTS;
const csTools3DVer1MeasurementSource = MeasurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
MeasurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => {
if (!Object.keys(measurements).length) {
return;
}
for (const measurement of Object.values(measurements)) {
const { uid, source } = measurement;
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
continue;
}
removeAnnotation(uid);
}
});
MeasurementService.subscribe(
MEASUREMENT_UPDATED,
({ source, measurement, notYetUpdatedAtSource }) => {
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
return;
}
if (notYetUpdatedAtSource === false) {
// This event was fired by cornerstone telling the measurement service to sync.
// Already in sync.
return;
}
const { uid, label } = measurement;
const sourceAnnotation = annotation.state.getAnnotation(uid);
const { data, metadata } = sourceAnnotation;
if (!data) {
return;
}
if (data.label !== label) {
data.label = label;
}
if (metadata.toolName === 'ArrowAnnotate') {
data.text = label;
}
// Todo: trigger render for annotation
}
);
MeasurementService.subscribe(
RAW_MEASUREMENT_ADDED,
({ source, measurement, data, dataSource }) => {
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
return;
}
const {
referenceSeriesUID,
referenceStudyUID,
SOPInstanceUID,
} = measurement;
const instance = DicomMetadataStore.getInstance(
referenceStudyUID,
referenceSeriesUID,
SOPInstanceUID
);
const imageId = dataSource.getImageIdsForInstance({ instance });
const annotationManager = annotation.state.getDefaultAnnotationManager();
annotationManager.addAnnotation({
annotationUID: measurement.uid,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
toolName: measurement.toolName,
FrameOfReferenceUID: measurement.FrameOfReferenceUID,
referencedImageId: imageId,
},
data: {
text: data.annotation.data.text,
handles: { ...data.annotation.data.handles },
cachedStats: { ...data.annotation.data.cachedStats },
label: data.annotation.data.label,
},
});
}
);
MeasurementService.subscribe(
MEASUREMENT_REMOVED,
({ source, measurement: removedMeasurementId }) => {
if (source?.name && source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
return;
}
removeAnnotation(removedMeasurementId);
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
// Note: We could do a better job by triggering the render on the
// viewport itself, but the removeAnnotation does not include that info...
renderingEngine.render();
}
);
};
export {
initMeasurementService,
connectToolsToMeasurementService,
connectMeasurementServiceToTools,
};

View File

@ -0,0 +1,141 @@
import { eventTarget, cache, triggerEvent } from '@cornerstonejs/core';
import * as csTools from '@cornerstonejs/tools';
import { Enums as csToolsEnums } from '@cornerstonejs/tools';
import Labelmap from './utils/segmentationServiceMappings/Labelmap';
function initSegmentationService(
SegmentationService,
CornerstoneViewportService
) {
connectToolsToSegmentationService(
SegmentationService,
CornerstoneViewportService
);
connectSegmentationServiceToTools(
SegmentationService,
CornerstoneViewportService
);
}
function connectToolsToSegmentationService(
SegmentationService,
CornerstoneViewportService
) {
connectSegmentationServiceToTools(
SegmentationService,
CornerstoneViewportService
);
const segmentationUpdated = csToolsEnums.Events.SEGMENTATION_MODIFIED;
eventTarget.addEventListener(segmentationUpdated, evt => {
const { segmentationId } = evt.detail;
const segmentationState = csTools.segmentation.state.getSegmentation(
segmentationId
);
if (!segmentationState) {
return;
}
if (
!Object.keys(segmentationState.representationData).includes(
csToolsEnums.SegmentationRepresentations.Labelmap
)
) {
throw new Error('Non-labelmap representations are not supported yet');
}
// Todo: handle other representations when available in cornerstone3D
const segmentationSchema = Labelmap.toSegmentation(segmentationState);
try {
SegmentationService.addOrUpdateSegmentation(
segmentationId,
segmentationSchema
);
} catch (error) {
console.warn(
`Failed to add/update segmentation ${segmentationId}`,
error
);
}
});
}
function connectSegmentationServiceToTools(
SegmentationService,
CornerstoneViewportService
) {
const {
SEGMENTATION_UPDATED,
SEGMENTATION_REMOVED,
} = SegmentationService.EVENTS;
SegmentationService.subscribe(SEGMENTATION_REMOVED, ({ id }) => {
// Todo: This should be from the configuration
const removeFromCache = true;
const sourceSegState = csTools.segmentation.state.getSegmentation(id);
if (!sourceSegState) {
return;
}
const toolGroupIds = csTools.segmentation.state.getToolGroupsWithSegmentation(
id
);
toolGroupIds.forEach(toolGroupId => {
const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations(
toolGroupId
);
const UIDsToRemove = [];
segmentationRepresentations.forEach(representation => {
if (representation.segmentationId === id) {
UIDsToRemove.push(representation.segmentationRepresentationUID);
}
});
csTools.segmentation.removeSegmentationsFromToolGroup(
toolGroupId,
UIDsToRemove
);
});
// cleanup the segmentation state too
csTools.segmentation.state.removeSegmentation(id);
if (removeFromCache) {
cache.removeVolumeLoadObject(id);
}
});
SegmentationService.subscribe(
SEGMENTATION_UPDATED,
({ id, segmentation, notYetUpdatedAtSource }) => {
if (notYetUpdatedAtSource === false) {
return;
}
const { label, text } = segmentation;
const sourceSegmentation = csTools.segmentation.state.getSegmentation(id);
// Update the label in the source if necessary
if (sourceSegmentation.label !== label) {
sourceSegmentation.label = label;
}
if (sourceSegmentation.text !== text) {
sourceSegmentation.text = text;
}
triggerEvent(eventTarget, csTools.Enums.Events.SEGMENTATION_MODIFIED, {
segmentationId: id,
});
}
);
}
export default initSegmentationService;

View File

@ -1,13 +1,20 @@
import cornerstone from 'cornerstone-core';
import * as cornerstone from '@cornerstonejs/core';
import { volumeLoader } from '@cornerstonejs/core';
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import dicomParser from 'dicom-parser';
import { errorHandler } from '@ohif/core';
const { registerVolumeLoader } = volumeLoader;
let initialized = false;
function initWebWorkers() {
function initWebWorkers(appConfig) {
const config = {
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
maxWebWorkers: Math.min(
Math.max(navigator.hardwareConcurrency - 1, 1),
appConfig.maxNumberOfWebWorkers
),
startWebWorkersOnDemand: true,
taskConfiguration: {
decodeTask: {
@ -24,11 +31,27 @@ function initWebWorkers() {
}
}
export default function initWADOImageLoader(UserAuthenticationService) {
export default function initWADOImageLoader(
UserAuthenticationService,
appConfig
) {
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
registerVolumeLoader(
'cornerstoneStreamingImageVolume',
cornerstoneStreamingImageVolumeLoader
);
cornerstoneWADOImageLoader.configure({
decodeConfig: {
// !! IMPORTANT !!
// We should set this flag to false, since, by default cornerstone-wado-image-loader
// will convert everything to integers (to be able to work with cornerstone-2d).
// Until the default is set to true (which is the case for cornerstone3D),
// we should set this flag to false.
convertFloatPixelDataToInt: false,
},
beforeSend: function(xhr) {
const headers = UserAuthenticationService.getAuthorizationHeader();
@ -38,10 +61,7 @@ export default function initWADOImageLoader(UserAuthenticationService) {
// For now we use image/jls and image/x-jls because some servers still use the old type
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
const xhrRequestHeaders = {
// To prevent Preflight requests:
accept: 'multipart/related; type=application/octet-stream',
//
//accept: 'multipart/related; type="image/x-jls"',
// 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*',
};
@ -56,5 +76,5 @@ export default function initWADOImageLoader(UserAuthenticationService) {
},
});
initWebWorkers();
initWebWorkers(appConfig);
}

View File

@ -0,0 +1,108 @@
import { synchronizers, SynchronizerManager } from '@cornerstonejs/tools';
import { pubSubServiceInterface } from '@ohif/core';
const EVENTS = {
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
};
export type SyncGroup = {
type: string;
id: string;
source: boolean;
target: boolean;
};
const POSITION = 'cameraposition';
const VOI = 'voi';
export default class SyncGroupService {
serviceManager: any;
listeners: { [key: string]: (...args: any[]) => void } = {};
EVENTS: { [key: string]: string };
constructor(serviceManager) {
this.serviceManager = serviceManager;
this.listeners = {};
this.EVENTS = EVENTS;
//
Object.assign(this, pubSubServiceInterface);
}
private _createSynchronizer(type: string, id: string) {
type = type.toLowerCase();
if (type === POSITION) {
return synchronizers.createCameraPositionSynchronizer(id);
} else if (type === VOI) {
return synchronizers.createVOISynchronizer(id);
}
}
public addViewportToSyncGroup(
viewportId: string,
renderingEngineId: string,
syncGroups?: SyncGroup[]
): void {
if (!syncGroups || !syncGroups.length) {
return;
}
syncGroups.forEach(syncGroup => {
const { type, id, target, source } = syncGroup;
let synchronizer = SynchronizerManager.getSynchronizer(id);
if (!synchronizer) {
synchronizer = this._createSynchronizer(type, id);
}
if (target && source) {
synchronizer.add({
viewportId,
renderingEngineId,
});
return;
} else if (source) {
synchronizer.addSource({
viewportId,
renderingEngineId,
});
} else if (target) {
synchronizer.addTarget({
viewportId,
renderingEngineId,
});
}
});
}
public destroy() {
SynchronizerManager.destroy();
}
public removeViewportFromSyncGroup(
viewportId: string,
renderingEngineId: string
): void {
const synchronizers = SynchronizerManager.getAllSynchronizers();
synchronizers.forEach(synchronizer => {
if (!synchronizer) {
return;
}
synchronizer.remove({
viewportId,
renderingEngineId,
});
// check if any viewport is left in any of the sync groups, if not, delete that sync group
const sourceViewports = synchronizer.getSourceViewports();
const targetViewports = synchronizer.getTargetViewports();
if (!sourceViewports.length && !targetViewports.length) {
SynchronizerManager.destroySynchronizer(synchronizer.id);
}
});
}
}

View File

@ -0,0 +1,10 @@
import SyncGroupService from './SyncGroupService';
export default function ExtendedSyncGroupService(serviceManager) {
return {
name: 'SyncGroupService',
create: ({ configuration = {} }) => {
return new SyncGroupService(serviceManager);
},
};
}

View File

@ -0,0 +1,260 @@
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
import { pubSubServiceInterface } from '@ohif/core';
const EVENTS = {
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
TOOLGROUP_CREATED: 'event::cornerstone::toolgroupservice:toolgroupcreated',
};
type Tool = {
toolName: string;
bindings?: typeof Enums.MouseBindings | Enums.KeyboardBindings;
};
type Tools = {
active: Tool[];
passive?: Tool[];
enabled?: Tool[];
disabled?: Tool[];
};
export default class ToolGroupService {
serviceManager: any;
private toolGroupIds: Set<string> = new Set();
/**
* Service-specific
*/
listeners: { [key: string]: Function[] };
EVENTS: { [key: string]: string };
constructor(serviceManager) {
const { CornerstoneViewportService } = serviceManager.services;
this.CornerstoneViewportService = CornerstoneViewportService;
this.listeners = {};
this.EVENTS = EVENTS;
Object.assign(this, pubSubServiceInterface);
}
/**
* Returns the cornerstone ToolGroup for a given toolGroup UID
* @param {string} toolGroupId - The toolGroup uid
* @returns {IToolGroup} - The toolGroup
*/
public getToolGroup(toolGroupId: string): Types.IToolGroup | void {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
return toolGroup;
}
public getToolGroupIds(): string[] {
return Array.from(this.toolGroupIds);
}
public getToolGroupForViewport(viewportId: string): Types.IToolGroup | void {
const renderingEngine = this.CornerstoneViewportService.getRenderingEngine();
return ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngine.id
);
}
public getActiveToolForViewport(viewportId: string): string {
const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return null;
}
return toolGroup.getActivePrimaryMouseButtonTool();
}
public destroy() {
ToolGroupManager.destroy();
this.toolGroupIds = new Set();
}
public disable(viewportId: string, renderingEngineId: string): void {
const toolGroup = ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);
if (!toolGroup) {
return;
}
toolGroup.removeViewports(renderingEngineId, viewportId);
const viewportIds = toolGroup.getViewportIds();
// if (viewportIds.length === 0) {
// ToolGroupManager.destroyToolGroup(toolGroup.id);
// }
}
public addViewportToToolGroup(
viewportId: string,
renderingEngineId: string,
toolGroupId?: string
): void {
if (!toolGroupId) {
// If toolGroupId is not provided, add the viewport to all toolGroups
const toolGroups = ToolGroupManager.getAllToolGroups();
toolGroups.forEach(toolGroup => {
toolGroup.addViewport(viewportId, renderingEngineId);
});
} else {
let toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
if (!toolGroup) {
toolGroup = this.createToolGroup(toolGroupId);
}
toolGroup.addViewport(viewportId, renderingEngineId);
}
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId, toolGroupId });
}
public createToolGroup(toolGroupId: string): Types.IToolGroup {
if (this.getToolGroup(toolGroupId)) {
throw new Error(`ToolGroup ${toolGroupId} already exists`);
}
// if the toolGroup doesn't exist, create it
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
this.toolGroupIds.add(toolGroupId);
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { toolGroupId });
return toolGroup;
}
public addToolsToToolGroup(
toolGroupId: string,
tools: Array<Tool>,
configs: any = {}
): void {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
// this.changeConfigurationIfNecessary(toolGroup, volumeId);
this._addTools(toolGroup, tools, configs);
this._setToolsMode(toolGroup, tools);
}
public createToolGroupAndAddTools(
toolGroupId: string,
tools: Array<Tool>,
configs: any = {}
): Types.IToolGroup {
const toolGroup = this.createToolGroup(toolGroupId);
this.addToolsToToolGroup(toolGroupId, tools, configs);
return toolGroup;
}
/**
private changeConfigurationIfNecessary(toolGroup, volumeUID) {
// handle specific assignment for volumeUID (e.g., fusion)
const toolInstances = toolGroup._toolInstances;
// Object.values(toolInstances).forEach(toolInstance => {
// if (toolInstance.configuration) {
// toolInstance.configuration.volumeUID = volumeUID;
// }
// });
}
*/
/**
* Get the tool's configuration based on the tool name and tool group id
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
* @param toolName - The name of the tool
* @returns The configuration of the tool.
*/
public getToolConfiguration(toolGroupId: string, toolName: string) {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
if (!toolGroup) {
return null;
}
const tool = toolGroup.getToolInstance(toolName);
if (!tool) {
return null;
}
return tool.configuration;
}
/**
* Set the tool instance configuration. This will update the tool instance configuration
* on the toolGroup
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
* @param toolName - The name of the tool
* @param config - The configuration object that you want to set.
*/
public setToolConfiguration(toolGroupId, toolName, config) {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
const toolInstance = toolGroup.getToolInstance(toolName);
toolInstance.configuration = config;
}
private _getToolNames(toolGroupTools: Tools): string[] {
const toolNames = [];
toolGroupTools.active.forEach(tool => {
toolNames.push(tool.toolName);
});
if (toolGroupTools.passive) {
toolGroupTools.passive.forEach(tool => {
toolNames.push(tool.toolName);
});
}
if (toolGroupTools.enabled) {
toolGroupTools.enabled.forEach(tool => {
toolNames.push(tool.toolName);
});
}
if (toolGroupTools.disabled) {
toolGroupTools.disabled.forEach(tool => {
toolNames.push(tool.toolName);
});
}
return toolNames;
}
private _setToolsMode(toolGroup, tools) {
const { active, passive, enabled, disabled } = tools;
active.forEach(({ toolName, bindings }) => {
toolGroup.setToolActive(toolName, { bindings });
});
if (passive) {
passive.forEach(({ toolName }) => {
toolGroup.setToolPassive(toolName);
});
}
if (enabled) {
enabled.forEach(({ toolName }) => {
toolGroup.setToolEnabled(toolName);
});
}
if (disabled) {
disabled.forEach(({ toolName }) => {
toolGroup.setToolDisabled(toolName);
});
}
}
private _addTools(toolGroup, tools, configs) {
const toolNames = this._getToolNames(tools);
toolNames.forEach(toolName => {
// Initialize the toolConfig if no configuration is provided
const toolConfig = configs[toolName] ?? {};
// if (volumeUID) {
// toolConfig.volumeUID = volumeUID;
// }
toolGroup.addTool(toolName, { ...toolConfig });
});
}
}

View File

@ -0,0 +1,10 @@
import ToolGroupService from './ToolGroupService';
export default function ExtendedToolGroupService(serviceManager) {
return {
name: 'ToolGroupService',
create: ({ configuration = {} }) => {
return new ToolGroupService(serviceManager);
},
};
}

View File

@ -0,0 +1,227 @@
import {
cache as cs3DCache,
Enums,
Types,
volumeLoader,
} from '@cornerstonejs/core';
import { utils, pubSubServiceInterface } from '@ohif/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
export type StackData = {
StudyInstanceUID: string;
displaySetInstanceUID: string;
imageIds: string[];
frameRate?: number;
isClip?: boolean;
initialImageIndex?: number | string | null;
viewportType: Enums.ViewportType;
};
export type VolumeData = {
StudyInstanceUID: string;
displaySetInstanceUIDs: string[]; // can have more than one displaySet (fusion)
imageIds: string[][]; // can have more than one imageId list (fusion)
volumes: Types.IVolume[];
viewportType: Enums.ViewportType;
};
const VOLUME_LOADER_SCHEME = 'streaming-wadors';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstone::viewportdatachanged',
};
class CornerstoneCacheService {
stackImageIds: Map<string, string[]> = new Map();
volumeImageIds: Map<string, string[]> = new Map();
listeners: { [key: string]: (...args: any[]) => void } = {};
EVENTS: { [key: string]: string };
constructor() {
this.listeners = {};
this.EVENTS = EVENTS;
Object.assign(this, pubSubServiceInterface);
}
public getCacheSize() {
return cs3DCache.getCacheSize();
}
public getCacheFreeSpace() {
return cs3DCache.getBytesAvailable();
}
public async getViewportData(
viewportIndex: number,
displaySets: unknown[],
viewportType: string,
dataSource: unknown,
initialImageIndex?: number
): Promise<StackData | VolumeData> {
const cs3DViewportType = getCornerstoneViewportType(viewportType);
let viewportData: StackData | VolumeData;
if (cs3DViewportType === Enums.ViewportType.STACK) {
viewportData = await this._getStackViewportData(
dataSource,
displaySets,
initialImageIndex
);
}
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
}
viewportData.viewportType = cs3DViewportType;
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
viewportData,
viewportIndex,
});
return viewportData;
}
public async invalidateViewportData(
viewportData: VolumeData,
invalidatedDisplaySetInstanceUID: string,
dataSource,
DisplaySetService
) {
if (viewportData.viewportType === Enums.ViewportType.STACK) {
throw new Error('Invalidation of StackViewport is not supported yet');
}
const volumeId = invalidatedDisplaySetInstanceUID;
const volume = cs3DCache.getVolume(volumeId);
if (volume) {
cs3DCache.removeVolumeLoadObject(volumeId);
}
const displaySets = viewportData.displaySetInstanceUIDs.map(
DisplaySetService.getDisplaySetByUID
);
const newViewportData = await this._getVolumeViewportData(
dataSource,
displaySets
);
return newViewportData;
}
private _getStackViewportData(
dataSource,
displaySets,
initialImageIndex
): StackData {
// For Stack Viewport we don't have fusion currently
const displaySet = displaySets[0];
let stackImageIds = this.stackImageIds.get(
displaySet.displaySetInstanceUID
);
if (!stackImageIds) {
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
const stackData: StackData = {
StudyInstanceUID,
displaySetInstanceUID,
viewportType: Enums.ViewportType.STACK,
imageIds: stackImageIds,
};
if (typeof initialImageIndex === 'number') {
stackData.initialImageIndex = initialImageIndex;
}
return stackData;
}
private async _getVolumeViewportData(
dataSource,
displaySets
): Promise<VolumeData> {
// Check the cache for multiple scenarios to see if we need to
// decache the volume data from other viewports or not
const volumeImageIdsArray = [];
const volumes = [];
for (const displaySet of displaySets) {
const volumeId = displaySet.displaySetInstanceUID;
let volumeImageIds = this.volumeImageIds.get(
displaySet.displaySetInstanceUID
);
let volume = cs3DCache.getVolume(volumeId);
if (!volumeImageIds || !volume) {
volumeImageIds = this._getCornerstoneVolumeImageIds(
displaySet,
dataSource
);
volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds: volumeImageIds,
});
this.volumeImageIds.set(
displaySet.displaySetInstanceUID,
volumeImageIds
);
}
volumeImageIdsArray.push(volumeImageIds);
volumes.push(volume);
}
// assert displaySets are from the same study
const { StudyInstanceUID } = displaySets[0];
const displaySetInstanceUIDs = [];
displaySets.forEach(displaySet => {
if (displaySet.StudyInstanceUID !== StudyInstanceUID) {
throw new Error('Display sets are not from the same study');
}
displaySetInstanceUIDs.push(displaySet.displaySetInstanceUID);
});
return {
StudyInstanceUID,
displaySetInstanceUIDs,
imageIds: volumeImageIdsArray,
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
volumes,
};
}
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
return dataSource.getImageIdsForDisplaySet(displaySet);
}
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
const stackImageIds = this._getCornerstoneStackImageIds(
displaySet,
dataSource
);
return stackImageIds.map(imageId => {
const imageURI = utils.imageIdToURI(imageId);
return `${VOLUME_LOADER_SCHEME}:${imageURI}`;
});
}
}
const CacheService = new CornerstoneCacheService();
export default CacheService;

View File

@ -0,0 +1,579 @@
import { pubSubServiceInterface } from '@ohif/core';
import {
RenderingEngine,
StackViewport,
Enums,
Types,
getRenderingEngine,
utilities as csUtils,
VolumeViewport,
cache,
} from '@cornerstonejs/core';
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants';
import ViewportInfo, {
ViewportOptions,
DisplaySetOptions,
PublicViewportOptions,
} from './Viewport';
import { StackData, VolumeData } from './CornerstoneCacheService';
import {
setColormap,
setLowerUpperColorTransferFunction,
} from '../../utils/colormap/transferFunctionHelpers';
import JumpPresets from '../../utils/JumpPresets';
const EVENTS = {
VIEWPORT_INFO_CREATED:
'event::cornerstone::viewportservice:viewportinfocreated',
};
/**
* Handles cornerstone viewport logic including enabling, disabling, and
* updating the viewport.
*/
class CornerstoneViewportService implements IViewportService {
renderingEngine: Types.IRenderingEngine | null;
viewportsInfo: Map<number, ViewportInfo>;
viewportGridResizeObserver: ResizeObserver | null;
/**
* Service-specific
*/
EVENTS: { [key: string]: string };
listeners: { [key: string]: Array<(...args: any[]) => void> };
_broadcastEvent: unknown; // we should be able to extend the PubSub class to get this
// Some configs
enableResizeDetector: true;
resizeRefreshRateMs: 200;
resizeRefreshMode: 'debounce';
constructor(servicesManager) {
this.renderingEngine = null;
this.viewportGridResizeObserver = null;
this.viewportsInfo = new Map();
//
this.listeners = {};
this.EVENTS = EVENTS;
const { HangingProtocolService } = servicesManager.services;
this.HangingProtocolService = HangingProtocolService;
Object.assign(this, pubSubServiceInterface);
//
}
/**
* Adds the HTML element to the viewportService
* @param {*} viewportIndex
* @param {*} elementRef
*/
public enableElement(
viewportIndex: number,
viewportOptions: PublicViewportOptions,
elementRef: HTMLDivElement
) {
const viewportId =
viewportOptions.viewportId || this.getViewportId(viewportIndex);
const viewportInfo = new ViewportInfo(viewportIndex, viewportId);
viewportInfo.setElement(elementRef);
this.viewportsInfo.set(viewportIndex, viewportInfo);
}
public getViewportId(viewportIndex: number): string {
return `viewport-${viewportIndex}`;
}
/**
* It retrieves the renderingEngine if it does exist, or creates one otherwise
* @returns {RenderingEngine} rendering engine
*/
public getRenderingEngine() {
// get renderingEngine from cache if it exists
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
if (renderingEngine) {
this.renderingEngine = renderingEngine;
return this.renderingEngine;
}
if (!renderingEngine || renderingEngine.hasBeenDestroyed) {
this.renderingEngine = new RenderingEngine(RENDERING_ENGINE_ID);
}
return this.renderingEngine;
}
/**
* It triggers the resize on the rendering engine.
*/
public resize() {
const immediate = true;
const resetPan = false;
const resetZoom = false;
this.renderingEngine.resize(immediate, resetPan, resetZoom);
this.renderingEngine.render();
}
/**
* Removes the viewport from cornerstone, and destroys the rendering engine
*/
public destroy() {
this._removeResizeObserver();
this.viewportGridResizeObserver = null;
this.renderingEngine.destroy();
this.renderingEngine = null;
cache.purgeVolumeCache();
}
/**
* Disables the viewport inside the renderingEngine, if no viewport is left
* it destroys the renderingEngine.
* @param viewportIndex
*/
public disableElement(viewportIndex: number) {
const viewportInfo = this.viewportsInfo.get(viewportIndex);
if (!viewportInfo) {
return;
}
const viewportId = viewportInfo.getViewportId();
this.renderingEngine.disableElement(viewportId);
this.viewportsInfo.delete(viewportIndex);
if (this.viewportsInfo.size === 0) {
this.destroy();
}
}
/**
* Uses the renderingEngine to enable the element for the given viewport index
* and sets the displaySet data to the viewport
* @param {*} viewportIndex
* @param {*} displaySet
* @param {*} dataSource
* @returns
*/
public setViewportDisplaySets(
viewportIndex: number,
viewportData: StackData | VolumeData,
publicViewportOptions: PublicViewportOptions,
publicDisplaySetOptions: DisplaySetOptions[]
): void {
const renderingEngine = this.getRenderingEngine();
const viewportInfo = this.viewportsInfo.get(viewportIndex);
viewportInfo.setRenderingEngineId(renderingEngine.id);
const {
viewportOptions,
displaySetOptions,
} = this._getViewportAndDisplaySetOptions(
publicViewportOptions,
publicDisplaySetOptions,
viewportInfo
);
viewportInfo.setViewportOptions(viewportOptions);
viewportInfo.setDisplaySetOptions(displaySetOptions);
this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo);
const viewportId = viewportInfo.getViewportId();
const element = viewportInfo.getElement();
const type = viewportInfo.getViewportType();
const background = viewportInfo.getBackground();
const orientation = viewportInfo.getOrientation();
const viewportInput: Types.PublicViewportInput = {
viewportId,
element,
type,
defaultOptions: {
background,
orientation,
},
};
// Todo: this is not optimal at all, we are re-enabling the already enabled
// element which is not what we want. But enabledElement as part of the
// renderingEngine is designed to be used like this. This will trigger
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
renderingEngine.enableElement(viewportInput);
this._setDisplaySets(viewportId, viewportData, viewportInfo);
}
public getCornerstoneViewport(
viewportId: string
): Types.IStackViewport | Types.IVolumeViewport | null {
const viewportInfo = this.getViewportInfo(viewportId);
if (
!viewportInfo ||
!this.renderingEngine ||
this.renderingEngine.hasBeenDestroyed
) {
return null;
}
const viewport = this.renderingEngine.getViewport(viewportId);
return viewport;
}
public getCornerstoneViewportByIndex(
viewportIndex: number
): Types.IStackViewport | Types.IVolumeViewport | null {
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
if (
!viewportInfo ||
!this.renderingEngine ||
this.renderingEngine.hasBeenDestroyed
) {
return null;
}
const viewport = this.renderingEngine.getViewport(
viewportInfo.getViewportId()
);
return viewport;
}
/**
* Returns the viewportIndex for the provided viewportId
* @param {string} viewportId - the viewportId
* @returns {number} - the viewportIndex
*/
public getViewportInfoByIndex(viewportIndex: number): ViewportInfo {
return this.viewportsInfo.get(viewportIndex);
}
public getViewportInfo(viewportId: string): ViewportInfo {
// @ts-ignore
for (const [index, viewport] of this.viewportsInfo.entries()) {
if (viewport.getViewportId() === viewportId) {
return viewport;
}
}
return null;
}
_setStackViewport(
viewport: Types.IStackViewport,
viewportData: StackData,
viewportInfo: ViewportInfo
) {
const displaySetOptions = viewportInfo.getDisplaySetOptions();
const { imageIds, initialImageIndex } = viewportData;
let initialImageIndexToUse = initialImageIndex;
if (!initialImageIndexToUse) {
initialImageIndexToUse =
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
}
const { voi, voiInverted } = displaySetOptions[0];
const properties = {};
if (voi && (voi.windowWidth || voi.windowCenter)) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
voi.windowWidth,
voi.windowCenter
);
properties.voiRange = { lower, upper };
}
if (voiInverted !== undefined) {
properties.invert = voiInverted;
}
viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties(properties);
});
}
private _getInitialImageIndexForStackViewport(
viewportInfo: ViewportInfo,
imageIds?: string[]
): number {
const initialImageOptions = viewportInfo.getInitialImageOptions();
if (!initialImageOptions) {
return;
}
const { index, preset } = initialImageOptions;
return this._getInitialImageIndex(imageIds.length, index, preset);
}
_getInitialImageIndex(
numberOfSlices: number,
imageIndex?: number,
preset?: JumpPresets
): number {
const lastSliceIndex = numberOfSlices - 1;
if (imageIndex !== undefined) {
return csToolsUtils.clip(imageIndex, 0, lastSliceIndex);
}
if (preset === JumpPresets.First) {
return 0;
}
if (preset === JumpPresets.Last) {
return lastSliceIndex;
}
if (preset === JumpPresets.Middle) {
return Math.floor(lastSliceIndex / 2);
}
return 0;
}
async _setVolumeViewport(
viewport: Types.IVolumeViewport,
viewportData: VolumeData,
viewportInfo: ViewportInfo
): Promise<void> {
// TODO: We need to overhaul the way data sources work so requests can be made
// async. I think we should follow the image loader pattern which is async and
// has a cache behind it.
// The problem is that to set this volume, we need the metadata, but the request is
// already in-flight, and the promise is not cached, so we have no way to wait for
// it and know when it has fully arrived.
// loadStudyMetadata(StudyInstanceUID) => Promise([instances for study])
// loadSeriesMetadata(StudyInstanceUID, SeriesInstanceUID) => Promise([instances for series])
// If you call loadStudyMetadata and it's not in the DicomMetadataStore cache, it should fire
// a request through the data source?
// (This call may or may not create sub-requests for series metadata)
const volumeInputArray = [];
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
const { HangingProtocolService } = this;
for (let i = 0; i < viewportData.imageIds.length; i++) {
const imageIds = viewportData.imageIds[i];
const displaySetInstanceUID = viewportData.displaySetInstanceUIDs[i];
const displaySetOptions = displaySetOptionsArray[i];
const volumeId = displaySetInstanceUID;
// if (displaySet.needsRerendering) {
// console.warn('Removing volume from cache', volumeId);
// cache.removeVolumeLoadObject(volumeId);
// displaySet.needsRerendering = false;
// this.displaySetsNeedRerendering.add(displaySet.displaySetInstanceUID);
// }
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
const callback = ({ volumeActor }) => {
voiCallbacks.forEach(callback => callback(volumeActor));
};
volumeInputArray.push({
imageIds,
volumeId,
callback,
blendMode: displaySetOptions.blendMode,
slabThickness: this._getSlabThickness(displaySetOptions, volumeId),
});
}
if (
HangingProtocolService.hasCustomImageLoadStrategy() &&
!HangingProtocolService.customImageLoadPerformed
) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
return HangingProtocolService.runImageLoadStrategy({
viewportId: viewport.id,
volumeInputArray,
});
}
viewportData.volumes.forEach(volume => {
volume.load();
});
this.setVolumesForViewport(viewport, volumeInputArray);
}
public setVolumesForViewport(viewport, volumeInputArray) {
viewport.setVolumes(volumeInputArray).then(() => {
const viewportInfo = this.getViewportInfo(viewport.id);
const initialImageOptions = viewportInfo.getInitialImageOptions();
if (
initialImageOptions &&
(initialImageOptions.preset !== undefined ||
initialImageOptions.index !== undefined)
) {
const { index, preset } = initialImageOptions;
const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport(
viewport
);
const imageIndex = this._getInitialImageIndex(
numberOfSlices,
index,
preset
);
csToolsUtils.jumpToSlice(viewport.element, {
imageIndex,
});
}
viewport.render();
});
}
public updateViewport(viewportIndex, viewportData) {
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
const viewportId = viewportInfo.getViewportId();
const viewport = this.getCornerstoneViewport(viewportId);
if (viewport instanceof VolumeViewport) {
this._setVolumeViewport(viewport, viewportData, viewportInfo);
return;
}
if (viewport instanceof StackViewport) {
this._setStackViewport(viewport, viewportData, viewportInfo);
return;
}
}
_getVOICallbacks(volumeId, displaySetOptions) {
const { voi, voiInverted: inverted, colormap } = displaySetOptions;
const voiCallbackArray = [];
// If colormap is set, use it to set the color transfer function
if (colormap) {
voiCallbackArray.push(volumeActor => setColormap(volumeActor, colormap));
}
if (voi instanceof Object && voi.windowWidth && voi.windowCenter) {
const { windowWidth, windowCenter } = voi;
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
windowWidth,
windowCenter
);
voiCallbackArray.push(volumeActor =>
setLowerUpperColorTransferFunction({
volumeActor,
lower,
upper,
inverted,
})
);
}
return voiCallbackArray;
}
_setDisplaySets(
viewportId: string,
viewportData: StackData | VolumeData,
viewportInfo: ViewportInfo
): void {
const viewport = this.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
this._setStackViewport(viewport, viewportData as StackData, viewportInfo);
} else if (viewport instanceof VolumeViewport) {
this._setVolumeViewport(
viewport,
viewportData as VolumeData,
viewportInfo
);
} else {
throw new Error('Unknown viewport type');
}
}
/**
* Removes the resize observer from the viewport element
*/
_removeResizeObserver() {
if (this.viewportGridResizeObserver) {
this.viewportGridResizeObserver.disconnect();
}
}
_getSlabThickness(displaySetOptions, volumeId) {
const { blendMode } = displaySetOptions;
if (
blendMode === undefined ||
displaySetOptions.slabThickness === undefined
) {
return;
}
// if there is a slabThickness set as a number then use it
if (typeof displaySetOptions.slabThickness === 'number') {
return displaySetOptions.slabThickness;
}
if (displaySetOptions.slabThickness.toLowerCase() === 'fullvolume') {
// calculate the slab thickness based on the volume dimensions
const imageVolume = cache.getVolume(volumeId);
const { dimensions } = imageVolume;
const slabThickness = Math.sqrt(
dimensions[0] * dimensions[0] +
dimensions[1] * dimensions[1] +
dimensions[2] * dimensions[2]
);
return slabThickness;
}
}
_getViewportAndDisplaySetOptions(
publicViewportOptions: PublicViewportOptions,
publicDisplaySetOptions: DisplaySetOptions[],
viewportInfo: ViewportInfo
): {
viewportOptions: ViewportOptions;
displaySetOptions: DisplaySetOptions[];
} {
const viewportIndex = viewportInfo.getViewportIndex();
// Creating a temporary viewportInfo to handle defaults
const newViewportInfo = new ViewportInfo(
viewportIndex,
viewportInfo.getViewportId()
);
// To handle setting the default values if missing for the viewportOptions and
// displaySetOptions
newViewportInfo.setPublicViewportOptions(publicViewportOptions);
newViewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
const newViewportOptions = newViewportInfo.getViewportOptions();
const newDisplaySetOptions = newViewportInfo.getDisplaySetOptions();
return {
viewportOptions: newViewportOptions,
displaySetOptions: newDisplaySetOptions,
};
}
}
export default function ExtendedCornerstoneViewportService(serviceManager) {
return {
name: 'CornerstoneViewportService',
create: ({ configuration = {} }) => {
return new CornerstoneViewportService(serviceManager);
},
};
}

View File

@ -0,0 +1,74 @@
import { Types } from '@cornerstonejs/core';
import { StackData, VolumeData } from './CornerstoneCacheService';
import {
DisplaySetOptions,
PublicViewportOptions,
ViewportOptions,
} from './Viewport';
/**
* Handles cornerstone viewport logic including enabling, disabling, and
* updating the viewport.
*/
export interface IViewportService {
servicesManager: unknown;
HangingProtocolService: unknown;
renderingEngine: unknown;
viewportGridResizeObserver: unknown;
viewportsInfo: unknown;
sceneVolumeInputs: unknown;
viewportIndexUIDMap: unknown;
viewportDivElements: unknown;
ViewportPropertiesMap: unknown;
volumeUIDs: unknown;
listeners: { [key: string]: {} };
displaySetsNeedRerendering: unknown;
viewportDisplaySets: unknown;
EVENTS: { [key: string]: string };
_broadcastEvent: unknown;
/**
* Adds the HTML element to the viewportService
* @param {*} viewportIndex
* @param {*} elementRef
*/
enableElement(
viewportIndex: number,
viewportOptions: ViewportOptions,
elementRef: HTMLDivElement
): void;
/**
* It retrieves the renderingEngine if it does exist, or creates one otherwise
* @returns {RenderingEngine} rendering engine
*/
getRenderingEngine(): Types.IRenderingEngine;
/**
* It creates a resize observer for the viewport element, and observes
* the element for resizing events
* @param {*} elementRef
*/
resize(element: HTMLDivElement): void;
/**
* Removes the viewport from cornerstone, and destroys the rendering engine
*/
destroy(): void;
/**
* Disables the viewport inside the renderingEngine, if no viewport is left
* it destroys the renderingEngine.
* @param viewportIndex
*/
disableElement(viewportIndex: number): void;
/**
* Uses the renderingEngine to enable the element for the given viewport index
* and sets the displaySet data to the viewport
* @param {*} viewportIndex
* @param {*} displaySet
* @param {*} dataSource
* @returns
*/
setViewportDisplaySets(
viewportIndex: number,
viewportData: StackData | VolumeData,
publicViewportOptions: PublicViewportOptions,
publicDisplaySetOptions: DisplaySetOptions[]
): void;
}

View File

@ -0,0 +1,221 @@
import { Types, Enums, CONSTANTS } from '@cornerstonejs/core';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import JumpPresets from '../../utils/JumpPresets';
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
export type InitialImageOptions = {
index?: number;
preset?: JumpPresets;
};
export type ViewportOptions = {
viewportType: Enums.ViewportType;
toolGroupId: string;
viewportId: string;
orientation?: Types.Orientation;
background?: Types.Point3;
initialView?: string;
syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions;
customViewportOptions?: Record<string, unknown>;
};
export type PublicViewportOptions = {
viewportType?: string;
toolGroupId?: string;
viewportId?: string;
orientation?: string;
background?: Types.Point3;
initialView?: string;
syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions;
customViewportOptions?: Record<string, unknown>;
};
export type PublicDisplaySetOptions = {
voi?: VOI;
voiInverted?: boolean;
blendMode?: string;
slabThickness?: number;
colormap?: string;
};
export type DisplaySetOptions = {
voi?: VOI;
voiInverted: boolean;
blendMode?: Enums.BlendModes;
slabThickness?: number;
colormap?: string;
};
type VOI = {
windowWidth: number;
windowCenter: number;
};
export type DisplaySet = {
displaySetInstanceUID: string;
};
const STACK = 'stack';
const VOLUME = 'volume';
const DEFAULT_TOOLGROUP_ID = 'default';
class ViewportInfo {
private viewportId = '';
private viewportIndex: number;
private element: HTMLDivElement;
private viewportOptions: ViewportOptions;
private displaySetOptions: Array<DisplaySetOptions>;
private renderingEngineId: string;
constructor(viewportIndex: number, viewportId: string) {
this.viewportIndex = viewportIndex;
this.viewportId = viewportId;
this.setPublicViewportOptions({});
this.setPublicDisplaySetOptions([{}]);
}
public setRenderingEngineId(renderingEngineId: string): void {
this.renderingEngineId = renderingEngineId;
}
public getRenderingEngineId(): string {
return this.renderingEngineId;
}
public setViewportId(viewportId: string): void {
this.viewportId = viewportId;
}
public setViewportIndex(viewportIndex: number): void {
this.viewportIndex = viewportIndex;
}
public setElement(element: HTMLDivElement): void {
this.element = element;
}
public getViewportIndex(): number {
return this.viewportIndex;
}
public getElement(): HTMLDivElement {
return this.element;
}
public getViewportId(): string {
return this.viewportId;
}
public setPublicDisplaySetOptions(
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
): void {
// map the displaySetOptions and check if they are undefined then set them to default values
const displaySetOptions = this.mapDisplaySetOptions(
publicDisplaySetOptions
);
this.setDisplaySetOptions(displaySetOptions);
}
public setPublicViewportOptions(
viewportOptionsEntry: PublicViewportOptions
): void {
let viewportType = viewportOptionsEntry.viewportType;
let toolGroupId = viewportOptionsEntry.toolGroupId;
let orientation;
if (!viewportType) {
viewportType = getCornerstoneViewportType(STACK);
} else {
viewportType = getCornerstoneViewportType(
viewportOptionsEntry.viewportType
);
}
// map SAGITTAL, AXIAL, CORONAL orientation to be used by cornerstone
if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) {
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
} else {
orientation = CONSTANTS.ORIENTATION.AXIAL;
}
if (!toolGroupId) {
toolGroupId = DEFAULT_TOOLGROUP_ID;
}
this.setViewportOptions({
...viewportOptionsEntry,
viewportId: this.viewportId,
viewportType: viewportType as Enums.ViewportType,
orientation,
toolGroupId,
});
}
public setViewportOptions(viewportOptions: ViewportOptions): void {
this.viewportOptions = viewportOptions;
}
public getViewportOptions(): ViewportOptions {
return this.viewportOptions;
}
public setDisplaySetOptions(
displaySetOptions: Array<DisplaySetOptions>
): void {
this.displaySetOptions = displaySetOptions;
}
public getSyncGroups(): SyncGroup[] {
return this.viewportOptions.syncGroups || [];
}
public getDisplaySetOptions(): Array<DisplaySetOptions> {
return this.displaySetOptions;
}
public getViewportType(): Enums.ViewportType {
return this.viewportOptions.viewportType || Enums.ViewportType.STACK;
}
public getToolGroupId(): string {
return this.viewportOptions.toolGroupId;
}
public getBackground(): Types.Point3 {
return this.viewportOptions.background || [0, 0, 0];
}
public getOrientation(): Types.Orientation {
return this.viewportOptions.orientation;
}
public getInitialImageOptions(): InitialImageOptions {
return this.viewportOptions.initialImageOptions;
}
private mapDisplaySetOptions(
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
): Array<DisplaySetOptions> {
const displaySetOptions: Array<DisplaySetOptions> = [];
publicDisplaySetOptions.forEach(option => {
const blendMode = getCornerstoneBlendMode(option.blendMode);
displaySetOptions.push({
voi: option.voi || ({} as VOI),
voiInverted: option.voiInverted || false,
colormap: option.colormap || undefined,
slabThickness: option.slabThickness,
blendMode,
});
});
return displaySetOptions;
}
}
export default ViewportInfo;

View File

@ -0,0 +1,3 @@
const RENDERING_ENGINE_ID = 'OHIFCornerstoneRenderingEngine';
export { RENDERING_ENGINE_ID };

View File

@ -1,6 +1,6 @@
const state = {
// The `defaultContext` of an extension's commandsModule
DEFAULT_CONTEXT: 'ACTIVE_VIEWPORT::CORNERSTONE',
DEFAULT_CONTEXT: 'CORNERSTONE',
enabledElements: {},
};
@ -9,7 +9,11 @@ const state = {
* @param {HTMLElement} dom Active viewport element.
* @return void
*/
const setEnabledElement = (viewportIndex, element, context) => {
const setEnabledElement = (
viewportIndex: number,
element: HTMLElement,
context?: string
): void => {
const targetContext = context || state.DEFAULT_CONTEXT;
state.enabledElements[viewportIndex] = {
@ -19,7 +23,7 @@ const setEnabledElement = (viewportIndex, element, context) => {
};
/**
* Grabs the enabled element `dom` reference of an adective viewport.
* Grabs the enabled element `dom` reference of an ative viewport.
*
* @return {HTMLElement} Active viewport element.
*/

View File

@ -1,123 +0,0 @@
// Visible?
// Disabled?
// Based on contexts or misc. criteria?
// -- ACTIVE_ROUTE::VIEWER
// -- ACTIVE_VIEWPORT::CORNERSTONE
// setToolActive commands should receive the button event that triggered
// so we can do the "bind to this button" magic
// const definitions = [
// // OLD
// {
// id: 'StackScroll',
// label: 'Stack Scroll',
// icon: 'bars',
// //
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
// commandName: 'setToolActive',
// commandOptions: { toolName: 'StackScroll' },
// },
// {
// id: 'Reset',
// label: 'Reset',
// icon: 'reset',
// //
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
// commandName: 'resetViewport',
// },
// {
// id: 'Cine',
// label: 'CINE',
// icon: 'youtube',
// //
// type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
// options: {
// behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
// },
// },
// {
// id: 'More',
// label: 'More',
// icon: 'ellipse-circle',
// buttons: [
// {
// id: 'Magnify',
// label: 'Magnify',
// icon: 'circle',
// //
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
// commandName: 'setToolActive',
// commandOptions: { toolName: 'Magnify' },
// },
// {
// id: 'WwwcRegion',
// label: 'ROI Window',
// icon: 'stop',
// //
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
// commandName: 'setToolActive',
// commandOptions: { toolName: 'WwwcRegion' },
// },
// {
// id: 'Invert',
// label: 'Invert',
// icon: 'adjust',
// //
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
// commandName: 'invertViewport',
// },
// {
// id: 'RotateRight',
// label: 'Rotate Right',
// icon: 'rotate-right',
// //
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
// commandName: 'rotateViewportCW',
// },
// {
// id: 'FlipH',
// label: 'Flip H',
// icon: 'ellipse-h',
// //
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
// commandName: 'flipViewportHorizontal',
// },
// {
// id: 'FlipV',
// label: 'Flip V',
// icon: 'ellipse-v',
// //
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
// commandName: 'flipViewportVertical',
// },
// {
// id: 'Clear',
// label: 'Clear',
// icon: 'trash',
// //
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
// commandName: 'clearAnnotations',
// },
// {
// id: 'Eraser',
// label: 'Eraser',
// icon: 'eraser',
// //
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
// commandName: 'setToolActive',
// commandOptions: { toolName: 'Eraser' },
// },
// ],
// },
// {
// id: 'Exit2DMPR',
// label: 'Exit 2D MPR',
// icon: 'times',
// //
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
// commandName: 'setCornerstoneLayout',
// context: 'ACTIVE_VIEWPORT::VTK',
// },
// ];
export default [];

View File

@ -0,0 +1,233 @@
import React from 'react';
import domtoimage from 'dom-to-image';
import {
Enums,
getEnabledElement,
getOrCreateCanvas,
StackViewport,
} from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
import { ViewportDownloadForm } from '@ohif/ui';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
const MINIMUM_SIZE = 100;
const DEFAULT_SIZE = 512;
const MAX_TEXTURE_SIZE = 10000;
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
const TOOLGROUP_ID = 'cornerstone-viewport-download-form-toolgroup';
const CornerstoneViewportDownloadForm = ({
onClose,
activeViewportIndex,
CornerstoneViewportService,
}) => {
const enabledElement = OHIFgetEnabledElement(activeViewportIndex);
const activeViewportElement = enabledElement?.element;
const enableViewport = viewportElement => {
if (viewportElement) {
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
const viewportInput = {
viewportId: VIEWPORT_ID,
element: viewportElement,
type: Enums.ViewportType.STACK,
};
renderingEngine.enableElement(viewportInput);
}
};
const disableViewport = viewportElement => {
if (viewportElement) {
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
return new Promise(resolve => {
renderingEngine.disableElement(VIEWPORT_ID);
ToolGroupManager.destroyToolGroup(TOOLGROUP_ID);
});
}
};
const updateViewportPreview = (
downloadViewportElement,
internalCanvas,
fileType
) =>
new Promise(resolve => {
const enabledElement = getEnabledElement(downloadViewportElement);
const { viewport: downloadViewport, renderingEngine } = enabledElement;
// Note: Since any trigger of dimensions will update the viewport,
// we need to resize the offScreenCanvas to accommodate for the new
// dimensions, this is due to the reason that we are using the GPU offScreenCanvas
// to render the viewport for the downloadViewport.
renderingEngine.resize();
// Trigger the render on the viewport to update the on screen
downloadViewport.render();
downloadViewportElement.addEventListener(
Enums.Events.IMAGE_RENDERED,
function updateViewport(event) {
const enabledElement = getEnabledElement(event.target);
const { viewport } = enabledElement;
const { element } = viewport;
const downloadCanvas = getOrCreateCanvas(element);
const type = 'image/' + fileType;
const dataUrl = downloadCanvas.toDataURL(type, 1);
let newWidth = element.offsetHeight;
let newHeight = element.offsetWidth;
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
newHeight *= multiplier;
newWidth *= multiplier;
}
resolve({ dataUrl, width: newWidth, height: newHeight });
downloadViewportElement.removeEventListener(
Enums.Events.IMAGE_RENDERED,
updateViewport
);
}
);
});
const loadImage = (activeViewportElement, viewportElement, width, height) =>
new Promise(resolve => {
if (activeViewportElement && viewportElement) {
const activeViewportEnabledElement = getEnabledElement(
activeViewportElement
);
if (!activeViewportEnabledElement) {
return;
}
const { viewport } = activeViewportEnabledElement;
if (!(viewport instanceof StackViewport)) {
throw new Error('Viewport is not a StackViewport');
}
const imageId = viewport.getCurrentImageId();
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
const downloadViewport = renderingEngine.getViewport(
VIEWPORT_ID
) as StackViewport;
downloadViewport.setStack([imageId]).then(() => {
const properties = viewport.getProperties();
downloadViewport.setProperties(properties);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
resolve({ width: newWidth, height: newHeight });
});
}
});
const toggleAnnotations = (
toggle,
viewportElement,
activeViewportElement
) => {
const activeViewportEnabledElement = getEnabledElement(
activeViewportElement
);
const downloadViewportElement = getEnabledElement(viewportElement);
const {
viewportId: activeViewportId,
renderingEngineId,
} = activeViewportEnabledElement;
const { viewportId: downloadViewportId } = downloadViewportElement;
if (!activeViewportEnabledElement || !downloadViewportElement) {
return;
}
const toolGroup = ToolGroupManager.getToolGroupForViewport(
activeViewportId,
renderingEngineId
);
let downloadToolGroup = ToolGroupManager.getToolGroupForViewport(
downloadViewportId,
renderingEngineId
);
if (downloadToolGroup === undefined) {
downloadToolGroup = ToolGroupManager.createToolGroup(TOOLGROUP_ID);
// what tools were in the active viewport?
// make them all enabled instances so that they can not be interacted
// with in the download viewport
Object.values(toolGroup._toolInstances).forEach(tool => {
downloadToolGroup.addTool(tool.getToolName());
});
// add the viewport to the toolGroup
downloadToolGroup.addViewport(downloadViewportId);
}
Object.values(downloadToolGroup._toolInstances).forEach(tool => {
const toolName = tool.getToolName();
if (toggle) {
downloadToolGroup.setToolEnabled(toolName);
} else {
downloadToolGroup.setToolDisabled(toolName);
}
});
};
const downloadBlob = (filename, fileType) => {
const file = `${filename}.${fileType}`;
const divForDownloadViewport = document.querySelector(
`div[data-viewport-uid="${VIEWPORT_ID}"]`
);
domtoimage.toPng(divForDownloadViewport).then(dataUrl => {
const link = document.createElement('a');
link.download = file;
link.href = dataUrl;
link.click();
});
};
return (
<ViewportDownloadForm
onClose={onClose}
minimumSize={MINIMUM_SIZE}
maximumSize={MAX_TEXTURE_SIZE}
defaultSize={DEFAULT_SIZE}
canvasClass={'cornerstone-canvas'}
activeViewportElement={activeViewportElement}
enableViewport={enableViewport}
disableViewport={disableViewport}
updateViewportPreview={updateViewportPreview}
loadImage={loadImage}
toggleAnnotations={toggleAnnotations}
downloadBlob={downloadBlob}
/>
);
};
CornerstoneViewportDownloadForm.propTypes = {
onClose: PropTypes.func,
activeViewportIndex: PropTypes.number.isRequired,
};
export default CornerstoneViewportDownloadForm;

View File

@ -0,0 +1,14 @@
/**
* Jump Presets - This enum defines the 3 jump states which are available
* to be used with the jumpToSlice utility function.
*/
enum JumpPresets {
/** Jumps to first slice */
First = 'first',
/** Jumps to last slice */
Last = 'last',
/** Jumps to the middle slice */
Middle = 'middle',
}
export default JumpPresets;

View File

@ -0,0 +1,139 @@
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
import presets from './presets';
// get preset from preset name
function getPreset(name) {
return presets.find(preset => preset.name === name);
}
// Todo: we should be able to register a new preset via the utilityModule
// of the cornerstone extension
export default function applyPreset(actor, presetName) {
const preset = getPreset(presetName);
// Create color transfer function
const colorTransferArray = preset.colorTransfer
.split(' ')
.splice(1)
.map(parseFloat);
const { shiftRange } = getShiftRange(colorTransferArray);
let min = shiftRange[0];
const width = shiftRange[1] - shiftRange[0];
const cfun = vtkColorTransferFunction.newInstance();
const normColorTransferValuePoints = [];
for (let i = 0; i < colorTransferArray.length; i += 4) {
let value = colorTransferArray[i];
const r = colorTransferArray[i + 1];
const g = colorTransferArray[i + 2];
const b = colorTransferArray[i + 3];
value = (value - min) / width;
normColorTransferValuePoints.push([value, r, g, b]);
}
applyPointsToRGBFunction(normColorTransferValuePoints, shiftRange, cfun);
actor.getProperty().setRGBTransferFunction(0, cfun);
// Create scalar opacity function
const scalarOpacityArray = preset.scalarOpacity
.split(' ')
.splice(1)
.map(parseFloat);
const ofun = vtkPiecewiseFunction.newInstance();
const normPoints = [];
for (let i = 0; i < scalarOpacityArray.length; i += 2) {
let value = scalarOpacityArray[i];
const opacity = scalarOpacityArray[i + 1];
value = (value - min) / width;
normPoints.push([value, opacity]);
}
applyPointsToPiecewiseFunction(normPoints, shiftRange, ofun);
actor.getProperty().setScalarOpacity(0, ofun);
const [
gradientMinValue,
gradientMinOpacity,
gradientMaxValue,
gradientMaxOpacity,
] = preset.gradientOpacity
.split(' ')
.splice(1)
.map(parseFloat);
actor.getProperty().setUseGradientOpacity(0, true);
actor.getProperty().setGradientOpacityMinimumValue(0, gradientMinValue);
actor.getProperty().setGradientOpacityMinimumOpacity(0, gradientMinOpacity);
actor.getProperty().setGradientOpacityMaximumValue(0, gradientMaxValue);
actor.getProperty().setGradientOpacityMaximumOpacity(0, gradientMaxOpacity);
if (preset.interpolation === '1') {
actor.getProperty().setInterpolationTypeToFastLinear();
//actor.getProperty().setInterpolationTypeToLinear()
}
const ambient = parseFloat(preset.ambient);
//const shade = preset.shade === '1'
const diffuse = parseFloat(preset.diffuse);
const specular = parseFloat(preset.specular);
const specularPower = parseFloat(preset.specularPower);
//actor.getProperty().setShade(shade)
actor.getProperty().setAmbient(ambient);
actor.getProperty().setDiffuse(diffuse);
actor.getProperty().setSpecular(specular);
actor.getProperty().setSpecularPower(specularPower);
}
function getShiftRange(colorTransferArray) {
// Credit to paraview-glance
// https://github.com/Kitware/paraview-glance/blob/3fec8eeff31e9c19ad5b6bff8e7159bd745e2ba9/src/components/controls/ColorBy/script.js#L133
// shift range is original rgb/opacity range centered around 0
let min = Infinity;
let max = -Infinity;
for (let i = 0; i < colorTransferArray.length; i += 4) {
min = Math.min(min, colorTransferArray[i]);
max = Math.max(max, colorTransferArray[i]);
}
const center = (max - min) / 2;
return {
shiftRange: [-center, center],
min,
max,
};
}
function applyPointsToRGBFunction(points, range, cfun) {
const width = range[1] - range[0];
const rescaled = points.map(([x, r, g, b]) => [
x * width + range[0],
r,
g,
b,
]);
cfun.removeAllPoints();
rescaled.forEach(([x, r, g, b]) => cfun.addRGBPoint(x, r, g, b));
return rescaled;
}
function applyPointsToPiecewiseFunction(points, range, pwf) {
const width = range[1] - range[0];
const rescaled = points.map(([x, y]) => [x * width + range[0], y]);
pwf.removeAllPoints();
rescaled.forEach(([x, y]) => pwf.addPoint(x, y));
return rescaled;
}

View File

@ -0,0 +1,29 @@
// https://www.slicer.org/w/index.php/Slicer3:2010_GenericAnatomyColors#Lookup_table
const colors = [
{
integerLabel: 0,
textLabel: 'background',
color: [0, 0, 0, 0],
},
{
integerLabel: 1,
textLabel: 'tissue',
// color: [255, 174, 128, 255],
color: [255, 0, 0, 255],
},
{
integerLabel: 2,
textLabel: 'bone',
// color: [241, 214, 145, 255],
color: [0, 255, 0, 255],
},
{
integerLabel: 3,
textLabel: 'skin',
// color: [177, 122, 101, 255],
color: [0, 0, 255, 255],
},
// ....
];
export default colors;

View File

@ -0,0 +1,435 @@
const presets = [
{
name: 'CT-AAA',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity:
'12 -3024 0 143.556 0 166.222 0.686275 214.389 0.696078 419.736 0.833333 3071 0.803922',
id: 'vtkMRMLVolumePropertyNode1',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'24 -3024 0 0 0 143.556 0.615686 0.356863 0.184314 166.222 0.882353 0.603922 0.290196 214.389 1 1 1 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '143.556 419.736',
},
{
name: 'CT-AAA2',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity:
'16 -3024 0 129.542 0 145.244 0.166667 157.02 0.5 169.918 0.627451 395.575 0.8125 1578.73 0.8125 3071 0.8125',
id: 'vtkMRMLVolumePropertyNode2',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'32 -3024 0 0 0 129.542 0.54902 0.25098 0.14902 145.244 0.6 0.627451 0.843137 157.02 0.890196 0.47451 0.6 169.918 0.992157 0.870588 0.392157 395.575 1 0.886275 0.658824 1578.73 1 0.829256 0.957922 3071 0.827451 0.658824 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '0 1600',
},
{
name: 'CT-Bone',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity: '8 -3024 0 -16.4458 0 641.385 0.715686 3071 0.705882',
id: 'vtkMRMLVolumePropertyNode3',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'16 -3024 0 0 0 -16.4458 0.729412 0.254902 0.301961 641.385 0.905882 0.815686 0.552941 3071 1 1 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '-16.4458 641.385',
},
{
name: 'CT-Bones',
gradientOpacity: '4 0 1 985.12 1',
specularPower: '1',
scalarOpacity: '8 -1000 0 152.19 0 278.93 0.190476 952 0.2',
id: 'vtkMRMLVolumePropertyNode4',
specular: '0',
shade: '1',
ambient: '0.2',
colorTransfer:
'20 -1000 0.3 0.3 1 -488 0.3 1 0.3 463.28 1 0 0 659.15 1 0.912535 0.0374849 953 1 0.3 0.3',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '152.19 952',
},
{
name: 'CT-Cardiac',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity:
'12 -3024 0 -77.6875 0 94.9518 0.285714 179.052 0.553571 260.439 0.848214 3071 0.875',
id: 'vtkMRMLVolumePropertyNode5',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'24 -3024 0 0 0 -77.6875 0.54902 0.25098 0.14902 94.9518 0.882353 0.603922 0.290196 179.052 1 0.937033 0.954531 260.439 0.615686 0 0 3071 0.827451 0.658824 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '-77.6875 260.439',
},
{
name: 'CT-Cardiac2',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity:
'12 -3024 0 42.8964 0 163.488 0.428571 277.642 0.776786 1587 0.754902 3071 0.754902',
id: 'vtkMRMLVolumePropertyNode6',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'24 -3024 0 0 0 42.8964 0.54902 0.25098 0.14902 163.488 0.917647 0.639216 0.0588235 277.642 1 0.878431 0.623529 1587 1 1 1 3071 0.827451 0.658824 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '42.8964 1587',
},
{
name: 'CT-Cardiac3',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity:
'14 -3024 0 -86.9767 0 45.3791 0.169643 139.919 0.589286 347.907 0.607143 1224.16 0.607143 3071 0.616071',
id: 'vtkMRMLVolumePropertyNode7',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'28 -3024 0 0 0 -86.9767 0 0.25098 1 45.3791 1 0 0 139.919 1 0.894893 0.894893 347.907 1 1 0.25098 1224.16 1 1 1 3071 0.827451 0.658824 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '-86.9767 1224.16',
},
{
name: 'CT-Chest-Contrast-Enhanced',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity:
'10 -3024 0 67.0106 0 251.105 0.446429 439.291 0.625 3071 0.616071',
id: 'vtkMRMLVolumePropertyNode8',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'20 -3024 0 0 0 67.0106 0.54902 0.25098 0.14902 251.105 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '67.0106 439.291',
},
{
name: 'CT-Chest-Vessels',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity:
'10 -3024 0 -1278.35 0 22.8277 0.428571 439.291 0.625 3071 0.616071',
id: 'vtkMRMLVolumePropertyNode9',
specular: '0',
shade: '1',
ambient: '0.2',
colorTransfer:
'20 -3024 0 0 0 -1278.35 0.54902 0.25098 0.14902 22.8277 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '-1278.35 439.291',
},
{
name: 'CT-Coronary-Arteries',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity:
'12 -2048 0 136.47 0 159.215 0.258929 318.43 0.571429 478.693 0.776786 3661 1',
id: 'vtkMRMLVolumePropertyNode10',
specular: '0',
shade: '0',
ambient: '0.2',
colorTransfer:
'24 -2048 0 0 0 136.47 0 0 0 159.215 0.159804 0.159804 0.159804 318.43 0.764706 0.764706 0.764706 478.693 1 1 1 3661 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '136.47 478.693',
},
{
name: 'CT-Coronary-Arteries-2',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity:
'14 -2048 0 142.677 0 145.016 0.116071 192.174 0.5625 217.24 0.776786 384.347 0.830357 3661 0.830357',
id: 'vtkMRMLVolumePropertyNode11',
specular: '0',
shade: '1',
ambient: '0.2',
colorTransfer:
'28 -2048 0 0 0 142.677 0 0 0 145.016 0.615686 0 0.0156863 192.174 0.909804 0.454902 0 217.24 0.972549 0.807843 0.611765 384.347 0.909804 0.909804 1 3661 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '142.677 384.347',
},
{
name: 'CT-Coronary-Arteries-3',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity:
'14 -2048 0 128.643 0 129.982 0.0982143 173.636 0.669643 255.884 0.857143 584.878 0.866071 3661 1',
id: 'vtkMRMLVolumePropertyNode12',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'28 -2048 0 0 0 128.643 0 0 0 129.982 0.615686 0 0.0156863 173.636 0.909804 0.454902 0 255.884 0.886275 0.886275 0.886275 584.878 0.968627 0.968627 0.968627 3661 1 1 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '128.643 584.878',
},
{
name: 'CT-Cropped-Volume-Bone',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity: '10 -2048 0 -451 0 -450 1 1050 1 3661 1',
id: 'vtkMRMLVolumePropertyNode13',
specular: '0',
shade: '0',
ambient: '0.2',
colorTransfer:
'20 -2048 0 0 0 -451 0 0 0 -450 0.0556356 0.0556356 0.0556356 1050 1 1 1 3661 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '-451 1050',
},
{
name: 'CT-Fat',
gradientOpacity: '6 0 1 985.12 1 988 1',
specularPower: '1',
scalarOpacity: '14 -1000 0 -100 0 -99 0.15 -60 0.15 -59 0 101.2 0 952 0',
id: 'vtkMRMLVolumePropertyNode14',
specular: '0',
references: '0',
shade: '0',
ambient: '0.2',
colorTransfer:
'36 -1000 0.3 0.3 1 -497.5 0.3 1 0.3 -99 0 0 1 -76.946 0 1 0 -65.481 0.835431 0.888889 0.0165387 83.89 1 0 0 463.28 1 0 0 659.15 1 0.912535 0.0374849 2952 1 0.300267 0.299886',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '-100 101.2',
},
{
name: 'CT-Liver-Vasculature',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity:
'14 -2048 0 149.113 0 157.884 0.482143 339.96 0.660714 388.526 0.830357 1197.95 0.839286 3661 0.848214',
id: 'vtkMRMLVolumePropertyNode15',
specular: '0',
shade: '0',
ambient: '0.2',
colorTransfer:
'28 -2048 0 0 0 149.113 0 0 0 157.884 0.501961 0.25098 0 339.96 0.695386 0.59603 0.36886 388.526 0.854902 0.85098 0.827451 1197.95 1 1 1 3661 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '149.113 1197.95',
},
{
name: 'CT-Lung',
gradientOpacity: '6 0 1 985.12 1 988 1',
specularPower: '1',
scalarOpacity: '12 -1000 0 -600 0 -599 0.15 -400 0.15 -399 0 2952 0',
id: 'vtkMRMLVolumePropertyNode16',
specular: '0',
references: '0',
shade: '1',
ambient: '0.2',
colorTransfer:
'24 -1000 0.3 0.3 1 -600 0 0 1 -530 0.134704 0.781726 0.0724558 -460 0.929244 1 0.109473 -400 0.888889 0.254949 0.0240258 2952 1 0.3 0.3',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '-600 -399',
},
{
name: 'CT-MIP',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity: '8 -3024 0 -637.62 0 700 1 3071 1',
id: 'vtkMRMLVolumePropertyNode17',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer: '16 -3024 0 0 0 -637.62 1 1 1 700 1 1 1 3071 1 1 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '-637.62 700',
},
{
name: 'CT-Muscle',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity:
'10 -3024 0 -155.407 0 217.641 0.676471 419.736 0.833333 3071 0.803922',
id: 'vtkMRMLVolumePropertyNode18',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'20 -3024 0 0 0 -155.407 0.54902 0.25098 0.14902 217.641 0.882353 0.603922 0.290196 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '-155.407 419.736',
},
{
name: 'CT-Pulmonary-Arteries',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity:
'14 -2048 0 -568.625 0 -364.081 0.0714286 -244.813 0.401786 18.2775 0.607143 447.798 0.830357 3592.73 0.839286',
id: 'vtkMRMLVolumePropertyNode19',
specular: '0',
shade: '1',
ambient: '0.2',
colorTransfer:
'28 -2048 0 0 0 -568.625 0 0 0 -364.081 0.396078 0.301961 0.180392 -244.813 0.611765 0.352941 0.0705882 18.2775 0.843137 0.0156863 0.156863 447.798 0.752941 0.752941 0.752941 3592.73 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '-568.625 447.798',
},
{
name: 'CT-Soft-Tissue',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity: '10 -2048 0 -167.01 0 -160 1 240 1 3661 1',
id: 'vtkMRMLVolumePropertyNode20',
specular: '0',
shade: '0',
ambient: '0.2',
colorTransfer:
'20 -2048 0 0 0 -167.01 0 0 0 -160 0.0556356 0.0556356 0.0556356 240 1 1 1 3661 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '-167.01 240',
},
{
name: 'CT-Air',
gradientOpacity: '4 0 1 255 1',
specularPower: '10',
scalarOpacity: '8 -3024 0.705882 -900.0 0.715686 -500.0 0 3071 0',
id: 'vtkMRMLVolumePropertyNode21',
specular: '0.2',
shade: '1',
ambient: '0.1',
colorTransfer:
'16 -3024 1 1 1 -900.0 0.2 1.0 1.0 -500.0 0.3 0.3 1.0 3071 0 0 0 ',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '-1200.0 -200.0',
},
{
name: 'MR-Angio',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity:
'12 -2048 0 151.354 0 158.279 0.4375 190.112 0.580357 200.873 0.732143 3661 0.741071',
id: 'vtkMRMLVolumePropertyNode22',
specular: '0',
shade: '1',
ambient: '0.2',
colorTransfer:
'24 -2048 0 0 0 151.354 0 0 0 158.279 0.74902 0.376471 0 190.112 1 0.866667 0.733333 200.873 0.937255 0.937255 0.937255 3661 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '151.354 200.873',
},
{
name: 'MR-Default',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity: '12 0 0 20 0 40 0.15 120 0.3 220 0.375 1024 0.5',
id: 'vtkMRMLVolumePropertyNode23',
specular: '0',
shade: '1',
ambient: '0.2',
colorTransfer:
'24 0 0 0 0 20 0.168627 0 0 40 0.403922 0.145098 0.0784314 120 0.780392 0.607843 0.380392 220 0.847059 0.835294 0.788235 1024 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '0 220',
},
{
name: 'MR-MIP',
gradientOpacity: '4 0 1 255 1',
specularPower: '1',
scalarOpacity: '8 0 0 98.3725 0 416.637 1 2800 1',
id: 'vtkMRMLVolumePropertyNode24',
specular: '0',
shade: '0',
ambient: '0.2',
colorTransfer: '16 0 1 1 1 98.3725 1 1 1 416.637 1 1 1 2800 1 1 1',
selectable: 'true',
diffuse: '1',
interpolation: '1',
effectiveRange: '0 416.637',
},
{
name: 'MR-T2-Brain',
gradientOpacity: '4 0 1 160.25 1',
specularPower: '40',
scalarOpacity: '10 0 0 36.05 0 218.302 0.171429 412.406 1 641 1',
id: 'vtkMRMLVolumePropertyNode25',
specular: '0.5',
shade: '1',
ambient: '0.3',
colorTransfer:
'16 0 0 0 0 98.7223 0.956863 0.839216 0.192157 412.406 0 0.592157 0.807843 641 1 1 1',
selectable: 'true',
diffuse: '0.6',
interpolation: '1',
effectiveRange: '0 412.406',
},
{
name: 'DTI-FA-Brain',
gradientOpacity: '4 0 1 0.9950 1',
specularPower: '40',
scalarOpacity:
'16 0 0 0 0 0.3501 0.0158 0.49379 0.7619 0.6419 1 0.9920 1 0.9950 0 0.9950 0',
id: 'vtkMRMLVolumePropertyNode26',
specular: '0.5',
shade: '1',
ambient: '0.3',
colorTransfer:
'28 0 1 0 0 0 1 0 0 0.24974 0.4941 1 0 0.49949 0 0.9882 1 0.7492 0.51764 0 1 0.9950 1 0 0 0.9950 1 0 0',
selectable: 'true',
diffuse: '0.9',
interpolation: '1',
effectiveRange: '0 1',
},
];
export default presets;

View File

@ -0,0 +1,93 @@
import { cache, utilities } from '@cornerstonejs/core';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
const colormaps = {};
export function registerColormap(colormap) {
colormaps[colormap.Name] = colormap;
}
function setColorTransferFunctionFromVolumeMetadata({
volumeActor,
volumeId,
inverted,
}) {
let lower, upper, windowWidth, windowCenter;
if (volumeId) {
const volume = cache.getVolume(volumeId);
const voiLutModule = volume.metadata.voiLut[0];
if (voiLutModule) {
windowWidth = voiLutModule.windowWidth;
windowCenter = voiLutModule.windowCenter;
}
} else {
windowWidth = 400;
windowCenter = 40;
}
if (windowWidth == undefined || windowCenter === undefined) {
// Set to something so we can window level it manually.
lower = 200;
upper = 400;
} else {
lower = windowCenter - windowWidth / 2.0;
upper = windowCenter + windowWidth / 2.0;
}
setLowerUpperColorTransferFunction({ volumeActor, lower, upper, inverted });
}
function setLowerUpperColorTransferFunction({
volumeActor,
lower,
upper,
inverted,
}) {
volumeActor
.getProperty()
.getRGBTransferFunction(0)
.setMappingRange(lower, upper);
if (inverted) {
utilities.invertRgbTransferFunction(
volumeActor.getProperty().getRGBTransferFunction(0)
);
}
}
function setColormap(volumeActor, colormap) {
const mapper = volumeActor.getMapper();
mapper.setSampleDistance(1.0);
const cfun = vtkColorTransferFunction.newInstance();
// if we have a custom colormap, use it
let preset;
if (colormaps[colormap]) {
preset = colormaps[colormap];
} else {
preset = vtkColorMaps.getPresetByName(colormap);
}
cfun.applyColorMap(preset);
cfun.setMappingRange(0, 5);
volumeActor.getProperty().setRGBTransferFunction(0, cfun);
// Create scalar opacity function
const ofun = vtkPiecewiseFunction.newInstance();
ofun.addPoint(0, 0.0);
ofun.addPoint(0.1, 0.9);
ofun.addPoint(5, 1.0);
volumeActor.getProperty().setScalarOpacity(0, ofun);
}
export {
setColormap,
setColorTransferFunctionFromVolumeMetadata,
setLowerUpperColorTransferFunction,
};

View File

@ -1,9 +1,7 @@
import cornerstone from 'cornerstone-core';
import { imageLoader } from '@cornerstonejs/core';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import { api } from 'dicomweb-client';
import DICOMWeb from '../DICOMWeb';
import errorHandler from '../errorHandler';
import { DICOMWeb, errorHandler } from '@ohif/core';
const getImageId = imageObj => {
if (!imageObj) {
@ -49,7 +47,7 @@ const fetchIt = (url, headers = DICOMWeb.getAuthorizationHeader()) => {
};
const cornerstoneRetriever = imageId => {
return cornerstone.loadAndCacheImage(imageId).then(image => {
return imageLoader.loadAndCacheImage(imageId).then(image => {
return image && image.data && image.data.byteArray.buffer;
});
};
@ -151,6 +149,10 @@ class DicomLoaderService {
}
getDicomDataMethod = fetchIt.bind(this, imageId);
break;
default:
throw new Error(
`Unsupported image type: ${loaderType} for imageId: ${imageId}`
);
}
return getDicomDataMethod();

View File

@ -0,0 +1,17 @@
import { Enums } from '@cornerstonejs/core';
const MIP = 'mip';
export default function getCornerstoneBlendMode(
blendMode: string
): Enums.BlendModes {
if (!blendMode) {
return Enums.BlendModes.COMPOSITE;
}
if (blendMode.toLowerCase() === MIP) {
return Enums.BlendModes.MAXIMUM_INTENSITY_BLEND;
}
throw new Error();
}

View File

@ -1,31 +0,0 @@
import cornerstoneTools from 'cornerstone-tools';
const { globalImageIdSpecificToolStateManager } = cornerstoneTools;
export default function getCornerstoneMeasurementById(id) {
const globalToolState = globalImageIdSpecificToolStateManager.saveToolState();
const imageIds = Object.keys(globalToolState);
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const imageIdSpecificToolState = globalToolState[imageId];
const toolTypes = Object.keys(imageIdSpecificToolState);
for (let j = 0; j < toolTypes.length; j++) {
const toolType = toolTypes[j];
const toolData = imageIdSpecificToolState[toolType].data;
if (toolData) {
for (let k = 0; k < toolData.length; k++) {
const toolDataK = toolData[k];
if (toolDataK.id === id) {
return toolDataK;
}
}
}
}
}
}

View File

@ -0,0 +1,22 @@
import { CONSTANTS } from '@cornerstonejs/core';
import { log } from '@ohif/core';
const AXIAL = 'axial';
const SAGITTAL = 'sagittal';
const CORONAL = 'coronal';
export default function getCornerstoneOrientation(
orientation: string
): CONSTANTS.ORIENTATION {
switch (orientation.toLowerCase()) {
case AXIAL:
return CONSTANTS.ORIENTATION.AXIAL;
case SAGITTAL:
return CONSTANTS.ORIENTATION.SAGITTAL;
case CORONAL:
return CONSTANTS.ORIENTATION.CORONAL;
default:
log.wanr('Choosing default orientation: axial');
return CONSTANTS.ORIENTATION.AXIAL;
}
}

View File

@ -0,0 +1,20 @@
import { Enums } from '@cornerstonejs/core';
const STACK = 'stack';
const VOLUME = 'volume';
export default function getCornerstoneViewportType(
viewportType: string
): Enums.ViewportType {
if (viewportType.toLowerCase() === STACK) {
return Enums.ViewportType.STACK;
}
if (viewportType.toLowerCase() === VOLUME) {
return Enums.ViewportType.ORTHOGRAPHIC;
}
throw new Error(
`Invalid viewport type: ${viewportType}. Valid types are: stack, volume`
);
}

View File

@ -0,0 +1,60 @@
export default function getInterleavedFrames(imageIds) {
const minImageIdIndex = 0;
const maxImageIdIndex = imageIds.length - 1;
const middleImageIdIndex = Math.floor(imageIds.length / 2);
let lowerImageIdIndex = middleImageIdIndex;
let upperImageIdIndex = middleImageIdIndex;
// Build up an array of images to prefetch, starting with the current image.
const imageIdsToPrefetch = [
{ imageId: imageIds[middleImageIdIndex], imageIdIndex: middleImageIdIndex },
];
const prefetchQueuedFilled = {
currentPositionDownToMinimum: false,
currentPositionUpToMaximum: false,
};
// Check if on edges and some criteria is already fulfilled
if (middleImageIdIndex === minImageIdIndex) {
prefetchQueuedFilled.currentPositionDownToMinimum = true;
} else if (middleImageIdIndex === maxImageIdIndex) {
prefetchQueuedFilled.currentPositionUpToMaximum = true;
}
while (
!prefetchQueuedFilled.currentPositionDownToMinimum ||
!prefetchQueuedFilled.currentPositionUpToMaximum
) {
if (!prefetchQueuedFilled.currentPositionDownToMinimum) {
// Add imageId bellow
lowerImageIdIndex--;
imageIdsToPrefetch.push({
imageId: imageIds[lowerImageIdIndex],
imageIdIndex: lowerImageIdIndex,
});
if (lowerImageIdIndex === minImageIdIndex) {
prefetchQueuedFilled.currentPositionDownToMinimum = true;
}
}
if (!prefetchQueuedFilled.currentPositionUpToMaximum) {
// Add imageId above
upperImageIdIndex++;
imageIdsToPrefetch.push({
imageId: imageIds[upperImageIdIndex],
imageIdIndex: upperImageIdIndex,
});
if (upperImageIdIndex === maxImageIdIndex) {
prefetchQueuedFilled.currentPositionUpToMaximum = true;
}
}
}
return imageIdsToPrefetch;
}

View File

@ -1,36 +0,0 @@
import csTools from 'cornerstone-tools';
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
other: [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
csTools.OverlayTool,
],
};
export default function getTools() {
const tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
return tools;
}
export { toolsGroupedByType };

View File

@ -0,0 +1,150 @@
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
import getInterleavedFrames from './getInterleavedFrames';
import { compact, flatten, zip } from 'lodash';
// Map of volumeId and SeriesInstanceId
const volumeIdMapsToLoad = new Map<string, string>();
const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
/**
* This function caches the volumeUIDs until all the volumes inside the
* hangging protocol are initialized. Then it goes through the imageIds
* of the volumes, and interleav them, in order for the volumes to be loaded
* together from middle to the start and the end.
* @param {Object} props image loading properties from Cornerstone ViewportService
* @returns
*/
export default function interleaveCenterLoader({
data: { viewportId, volumeInputArray },
displaySetsMatchDetails,
matchDetails,
}) {
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
// Based on the volumeInputs store the volumeIds and SeriesInstanceIds
// to keep track of the volumes being loaded
for (const volumeInput of volumeInputArray) {
const { volumeId } = volumeInput;
const volume = cache.getVolume(volumeId);
if (!volume) {
return;
}
// if the volumeUID is not in the volumeUIDs array, add it
if (!volumeIdMapsToLoad.has(volumeId)) {
const { metadata } = volume;
volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID);
}
}
/**
* The following is checking if all the viewports that were matched in the HP has been
* successfully created their cornerstone viewport or not. Todo: This can be
* improved by not checking it, and as soon as the matched DisplaySets have their
* volume loaded, we start the loading, but that comes at the cost of viewports
* not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one
* pt viewport are created we have a guarantee that the volumes are created in the cache
* but the rest of the viewports (fusion, mip etc.) are not created yet. So
* we can't initiate setting the volumes for those viewports. One solution can be
* to add an event when a viewport is created (not enabled element event) and then
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
return;
}
// Check if all the matched volumes are loaded
for (const [_, details] of displaySetsMatchDetails.entries()) {
const { SeriesInstanceUID } = details;
// HangingProtocol has matched, but don't have all the volumes created yet, so return
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
return;
}
}
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
// get volumes from cache
const volumes = volumeIds.map(volumeId => {
return cache.getVolume(volumeId);
});
// iterate over all volumes, and get their imageIds, and interleave
// the imageIds and save them in AllRequests for later use
const AllRequests = [];
volumes.forEach(volume => {
const requests = volume.getImageLoadRequests();
if (!requests.length || !requests[0] || !requests[0].imageId) {
return;
}
const requestImageIds = requests.map(request => {
return request.imageId;
});
const imageIds = getInterleavedFrames(requestImageIds);
const reOrderedRequests = imageIds.map(({ imageId }) => {
const request = requests.find(req => req.imageId === imageId);
return request;
});
AllRequests.push(reOrderedRequests);
});
// flatten the AllRequests array, which will result in a list of all the
// imageIds for all the volumes but interleaved
const interleavedRequests = compact(flatten(zip(...AllRequests)));
// set the finalRequests to the imageLoadPoolManager
const finalRequests = [];
interleavedRequests.forEach(request => {
const { imageId } = request;
AllRequests.forEach(volumeRequests => {
const volumeImageIdRequest = volumeRequests.find(
req => req.imageId === imageId
);
if (volumeImageIdRequest) {
finalRequests.push(volumeImageIdRequest);
}
});
});
const requestType = Enums.RequestType.Prefetch;
const priority = 0;
finalRequests.forEach(
({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => {
const callLoadImageBound = callLoadImage.bind(
null,
imageId,
imageIdIndex,
options
);
imageLoadPoolManager.addRequest(
callLoadImageBound,
requestType,
additionalDetails,
priority
);
}
);
// clear the volumeIdMapsToLoad
volumeIdMapsToLoad.clear();
// copy the viewportIdVolumeInputArrayMap
const viewportIdVolumeInputArrayMapCopy = new Map(
viewportIdVolumeInputArrayMap
);
// reset the viewportIdVolumeInputArrayMap
viewportIdVolumeInputArrayMap.clear();
return viewportIdVolumeInputArrayMapCopy;
}

View File

@ -0,0 +1,139 @@
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
import { compact, flatten, zip } from 'lodash';
// Map of volumeId and SeriesInstanceId
const volumeIdMapsToLoad = new Map<string, string>();
const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
/**
* This function caches the volumeIds until all the volumes inside the
* hanging protocol are initialized. Then it goes through the imageIds
* of the volumes, and interleave them, in order for the volumes to be loaded
* together from middle to the start and the end.
* @param {Object} {viewportData, displaySetMatchDetails}
* @returns
*/
export default function interleaveTopToBottom({
data: { viewportId, volumeInputArray },
displaySetsMatchDetails,
matchDetails,
}) {
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
// Based on the volumeInputs store the volumeIds and SeriesInstanceIds
// to keep track of the volumes being loaded
for (const volumeInput of volumeInputArray) {
const { volumeId } = volumeInput;
const volume = cache.getVolume(volumeId);
if (!volume) {
return;
}
// if the volumeUID is not in the volumeUIDs array, add it
if (!volumeIdMapsToLoad.has(volumeId)) {
const { metadata } = volume;
volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID);
}
}
/**
* The following is checking if all the viewports that were matched in the HP has been
* successfully created their cornerstone viewport or not. Todo: This can be
* improved by not checking it, and as soon as the matched DisplaySets have their
* volume loaded, we start the loading, but that comes at the cost of viewports
* not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one
* pt viewport are created we have a guarantee that the volumes are created in the cache
* but the rest of the viewports (fusion, mip etc.) are not created yet. So
* we can't initiate setting the volumes for those viewports. One solution can be
* to add an event when a viewport is created (not enabled element event) and then
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
return;
}
// Check if all the matched volumes are loaded
for (const [_, details] of displaySetsMatchDetails.entries()) {
const { SeriesInstanceUID } = details;
// HangingProtocol has matched, but don't have all the volumes created yet, so return
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
return;
}
}
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
// get volumes from cache
const volumes = volumeIds.map(volumeId => {
return cache.getVolume(volumeId);
});
// iterate over all volumes, and get their imageIds, and interleave
// the imageIds and save them in AllRequests for later use
const AllRequests = [];
volumes.forEach(volume => {
const requests = volume.getImageLoadRequests();
if (!requests.length || !requests[0] || !requests[0].imageId) {
return;
}
// reverse the requests
AllRequests.push(requests.reverse());
});
// flatten the AllRequests array, which will result in a list of all the
// imageIds for all the volumes but interleaved
const interleavedRequests = compact(flatten(zip(...AllRequests)));
// set the finalRequests to the imageLoadPoolManager
const finalRequests = [];
interleavedRequests.forEach(request => {
const { imageId } = request;
AllRequests.forEach(volumeRequests => {
const volumeImageIdRequest = volumeRequests.find(
req => req.imageId === imageId
);
if (volumeImageIdRequest) {
finalRequests.push(volumeImageIdRequest);
}
});
});
const requestType = Enums.RequestType.Prefetch;
const priority = 0;
finalRequests.forEach(
({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => {
const callLoadImageBound = callLoadImage.bind(
null,
imageId,
imageIdIndex,
options
);
imageLoadPoolManager.addRequest(
callLoadImageBound,
requestType,
additionalDetails,
priority
);
}
);
// clear the volumeIdMapsToLoad
volumeIdMapsToLoad.clear();
// copy the viewportIdVolumeInputArrayMap
const viewportIdVolumeInputArrayMapCopy = new Map(
viewportIdVolumeInputArrayMap
);
// reset the viewportIdVolumeInputArrayMap
viewportIdVolumeInputArrayMap.clear();
return viewportIdVolumeInputArrayMapCopy;
}

View File

@ -1,57 +1,142 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getPointsFromHandles from './utils/getPointsFromHandles';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const ArrowAnnotate = {
toAnnotation: (measurement, definition) => {},
const Length = {
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsAnnotation,
csToolsEventDetail,
DisplaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
}
if (!validToolType(tool)) {
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
const points = [];
points.push(measurementData.handles);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
DisplaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
return {
id: measurementData.id,
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: measurementData.text,
description: measurementData.description,
unit: measurementData.unit,
text: measurementData.text,
type: getValueTypeFromToolType(tool),
points: getPointsFromHandles(measurementData.handles),
label: data.text,
text: data.text,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport: () => {
throw new Error('Not implemented');
},
};
},
};
export default ArrowAnnotate;
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { text } = data;
const { referencedImageId } = metadata;
const annotations = [];
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
const { SeriesNumber } = displaySet;
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
text,
});
return annotations;
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations) {
return '';
}
const displayText = [];
// Area is the same for all series
const { SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
displayText.push(
InstanceNumber
? `(S: ${SeriesNumber} I: ${InstanceNumber})`
: `(S: ${SeriesNumber})`
);
return displayText;
}
export default Length;

View File

@ -1,59 +1,197 @@
import { annotation } from '@cornerstonejs/tools';
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
import { utils } from '@ohif/core';
const Bidirectional = {
toAnnotation: (measurement, definition) => {},
toAnnotation: measurement => {},
toMeasurement: (
csToolsAnnotation,
csToolsEventDetail,
DisplaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
}
if (!validToolType(tool)) {
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
const { handles } = measurementData;
let displaySet;
const longAxis = [handles.start, handles.end];
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
DisplaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
id: measurementData.id,
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: measurementData.label,
description: measurementData.description,
unit: measurementData.unit,
shortestDiameter: measurementData.shortestDiameter,
longestDiameter: measurementData.longestDiameter,
type: getValueTypeFromToolType(tool),
points: { longAxis, shortAxis },
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId, referencedSeriesInstanceUID } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return;
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
let displaySet;
if (referencedImageId) {
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const { SeriesNumber, SeriesInstanceUID } = displaySet;
const { length, width } = targetStats;
const unit = 'mm';
annotations.push({
SeriesInstanceUID,
SeriesNumber,
unit,
length,
width,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:Bidirectional');
mappedAnnotations.forEach(annotation => {
const { length, width } = annotation;
columns.push(`Length (mm)`, `Width (mm)`);
values.push(length, width);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const { length, width, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
const roundedLength = utils.roundNumber(length, 1);
const roundedWidth = utils.roundNumber(width, 1);
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
displayText.push(
InstanceNumber
? `L: ${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
: `L: ${roundedLength} mm (S: ${SeriesNumber})`
);
displayText.push(`W: ${roundedWidth} mm`);
return displayText;
}
export default Bidirectional;

View File

@ -0,0 +1,219 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getModalityUnit from './utils/getModalityUnit';
import { utils } from '@ohif/core';
const EllipticalROI = {
toAnnotation: measurement => {},
toMeasurement: (
csToolsEventDetail,
DisplaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
DisplaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return;
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
let displaySet;
if (referencedImageId) {
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
// Todo: Non-acquisition plane measurement mapping not supported yet
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const { SeriesNumber, SeriesInstanceUID } = displaySet;
const { mean, stdDev, max, area, Modality } = targetStats;
const unit = getModalityUnit(Modality);
annotations.push({
SeriesInstanceUID,
SeriesNumber,
Modality,
unit,
mean,
stdDev,
max,
area,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:EllipticalROI');
mappedAnnotations.forEach(annotation => {
const { mean, stdDev, max, area, unit } = annotation;
if (!mean || !unit || !max || !area) {
return;
}
columns.push(
`max (${unit})`,
`mean (${unit})`,
`std (${unit})`,
`area (mm2)`
);
values.push(max, mean, stdDev, area);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const { area, SOPInstanceUID } = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const roundedArea = utils.roundNumber(area, 2);
displayText.push(`${roundedArea} mm<sup>2</sup>`);
// Todo: we need a better UI for displaying all these information
mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation;
if (max) {
const roundedMax = utils.roundNumber(max, 2);
displayText.push(
InstanceNumber
? `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber} I:${InstanceNumber})`
: `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber})`
);
}
});
return displayText;
}
export default EllipticalROI;

View File

@ -1,100 +0,0 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
const EllipticalRoi = {
toAnnotation: (measurement, definition) => {},
toMeasurement: (
csToolsAnnotation,
DisplaySetService,
getValueTypeFromToolType
) => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!validToolType(tool)) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
const { cachedStats, handles } = measurementData;
const { start, end } = handles;
const halfXLength = Math.abs(start.x - end.x) / 2;
const halfYLength = Math.abs(start.y - end.y) / 2;
const points = [];
const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
// To store similar to SR.
if (halfXLength > halfYLength) {
// X-axis major
// Major axis
points.push({ x: center.x - halfXLength, y: center.y });
points.push({ x: center.x + halfXLength, y: center.y });
// Minor axis
points.push({ x: center.x, y: center.y - halfYLength });
points.push({ x: center.x, y: center.y + halfYLength });
} else {
// Y-axis major
// Major axis
points.push({ x: center.x, y: center.y - halfYLength });
points.push({ x: center.x, y: center.y + halfYLength });
// Minor axis
points.push({ x: center.x - halfXLength, y: center.y });
points.push({ x: center.x + halfXLength, y: center.y });
}
let meanSUV;
let stdDevSUV;
if (
cachedStats &&
cachedStats.meanStdDevSUV &&
cachedStats.meanStdDevSUV.mean !== 0
) {
const { meanStdDevSUV } = cachedStats;
meanSUV = meanStdDevSUV.mean;
stdDevSUV = meanStdDevSUV.stdDev;
}
return {
id: measurementData.id,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: measurementData.label,
description: measurementData.description,
unit: measurementData.unit,
area: cachedStats && cachedStats.area,
mean: cachedStats && cachedStats.mean,
stdDev: cachedStats && cachedStats.stdDev,
meanSUV,
stdDevSUV,
type: getValueTypeFromToolType(tool),
points,
};
},
};
export default EllipticalRoi;

View File

@ -1,10 +1,9 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
import getPointsFromHandles from './utils/getPointsFromHandles';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { DisplaySetService, utils } from '@ohif/core';
const Length = {
toAnnotation: (measurement, definition) => {},
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
@ -13,49 +12,188 @@ const Length = {
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsAnnotation,
csToolsEventDetail,
DisplaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
}
if (!validToolType(tool)) {
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
DisplaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
id: measurementData.id,
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: measurementData.label,
description: measurementData.description,
unit: measurementData.unit,
length: measurementData.length,
type: getValueTypeFromToolType(tool),
points: getPointsFromHandles(measurementData.handles),
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return;
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
let displaySet;
if (referencedImageId) {
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
referencedImageId
);
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const { SeriesNumber, SeriesInstanceUID } = displaySet;
const { length } = targetStats;
const unit = 'mm';
annotations.push({
SeriesInstanceUID,
SeriesNumber,
unit,
length,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:Length');
mappedAnnotations.forEach(annotation => {
const { length } = annotation;
columns.push(`Length (mm)`);
values.push(length);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const { length, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const roundedLength = utils.roundNumber(length, 2);
displayText.push(
InstanceNumber
? `${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
: `${roundedLength} mm (S: ${SeriesNumber})`
);
return displayText;
}
export default Length;

View File

@ -1 +1 @@
export default ['Length', 'EllipticalRoi', 'Bidirectional', 'ArrowAnnotate'];
export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate'];

View File

@ -1,11 +1,12 @@
import Length from './Length';
import Bidirectional from './Bidirectional';
import EllipticalROI from './EllipticalROI';
import ArrowAnnotate from './ArrowAnnotate';
import EllipticalRoi from './EllipticalRoi';
const measurementServiceMappingsFactory = (
MeasurementService,
DisplaySetService
DisplaySetService,
CornerstoneViewportService
) => {
/**
* Maps measurement service format object to cornerstone annotation object.
@ -19,16 +20,18 @@ const measurementServiceMappingsFactory = (
const {
POLYLINE,
ELLIPSE,
POINT,
RECTANGLE,
BIDIRECTIONAL,
POINT,
} = MeasurementService.VALUE_TYPES;
// TODO -> I get why this was attemped, but its not nearly flexible enough.
// TODO -> I get why this was attempted, but its not nearly flexible enough.
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
// You can't define a bidirectional tool as a single type..
const TOOL_TYPE_TO_VALUE_TYPE = {
Length: POLYLINE,
EllipticalRoi: ELLIPSE,
EllipticalROI: ELLIPSE,
RectangleROI: RECTANGLE,
Bidirectional: BIDIRECTIONAL,
ArrowAnnotate: POINT,
};
@ -43,6 +46,7 @@ const measurementServiceMappingsFactory = (
Length.toMeasurement(
csToolsAnnotation,
DisplaySetService,
CornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
@ -58,6 +62,7 @@ const measurementServiceMappingsFactory = (
Bidirectional.toMeasurement(
csToolsAnnotation,
DisplaySetService,
CornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
@ -73,12 +78,28 @@ const measurementServiceMappingsFactory = (
},
],
},
EllipticalROI: {
toAnnotation: EllipticalROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
EllipticalROI.toMeasurement(
csToolsAnnotation,
DisplaySetService,
CornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.ELLIPSE,
},
],
},
ArrowAnnotate: {
toAnnotation: ArrowAnnotate.toAnnotation,
toMeasurement: csToolsAnnotation =>
ArrowAnnotate.toMeasurement(
csToolsAnnotation,
DisplaySetService,
CornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
@ -88,20 +109,6 @@ const measurementServiceMappingsFactory = (
},
],
},
EllipticalRoi: {
toAnnotation: EllipticalRoi.toAnnotation,
toMeasurement: csToolsAnnotation =>
EllipticalRoi.toMeasurement(
csToolsAnnotation,
DisplaySetService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.ELLIPSE,
},
],
},
};
};

View File

@ -1,102 +0,0 @@
import measurementServiceMappingsFactory from './measurementServiceMappingsFactory';
jest.mock('cornerstone-core', () => ({
...jest.requireActual('cornerstone-core'),
getEnabledElement: () => ({
image: { imageId: 123 },
}),
metaData: {
...jest.requireActual('cornerstone-core').metaData,
get: () => ({
SOPInstanceUID: '123',
FrameOfReferenceUID: '123',
SeriesInstanceUID: '123',
StudyInstanceUID: '1234',
}),
},
}));
describe('measurementServiceMappings.js', () => {
let mappings;
let handles;
let points;
let csToolsAnnotation;
let measurement;
let measurementServiceMock;
let displaySetServiceMock;
let definition = 'Length';
beforeEach(() => {
measurementServiceMock = {
VALUE_TYPES: {
POLYLINE: 'value_type::polyline',
POINT: 'value_type::point',
ELLIPSE: 'value_type::ellipse',
MULTIPOINT: 'value_type::multipoint',
CIRCLE: 'value_type::circle',
},
};
displaySetServiceMock = {
getDisplaySetForSOPInstanceUID: (SOPInstanceUID, SeriesInstanceUID) => {
console.warn('SOPInstanceUID');
return {
displaySetInstanceUID: '1.2.3.4'
}
}
};
mappings = measurementServiceMappingsFactory(measurementServiceMock, displaySetServiceMock);
handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } };
points = [
{ x: 1, y: 2 },
{ x: 1, y: 2 },
];
csToolsAnnotation = {
toolName: definition,
measurementData: {
id: 1,
label: 'Test',
SOPInstanceUID: '123',
FrameOfReferenceUID: '123',
SeriesInstanceUID: '123',
handles,
text: 'Test',
description: 'Test',
unit: 'mm',
length: undefined
},
};
measurement = {
id: 1,
SOPInstanceUID: '123',
FrameOfReferenceUID: '123',
referenceSeriesUID: '123',
referenceStudyUID: '1234',
displaySetInstanceUID: '1.2.3.4',
label: 'Test',
description: 'Test',
unit: 'mm',
type: measurementServiceMock.VALUE_TYPES.POLYLINE,
points,
};
jest.clearAllMocks();
});
/*describe('toAnnotation()', () => {
it('map measurement service format to annotation', async () => {
const mappedMeasurement = await mappings[csToolsAnnotation.toolName].toAnnotation(
measurement,
definition
);
expect(mappedMeasurement).toEqual(csToolsAnnotation);
});
});*/
describe('toMeasurement()', () => {
it('map annotation to measurement service format', async () => {
const getValueTypeFromToolType = (toolType) => 'valueType';
const mappedAnnotation = await mappings[csToolsAnnotation.toolName].toMeasurement(csToolsAnnotation, displaySetServiceMock, getValueTypeFromToolType);
expect(mappedAnnotation).toEqual(measurement);
});
});
});

View File

@ -0,0 +1,11 @@
function getModalityUnit(modality) {
if (modality === 'CT') {
return 'HU';
} else if (modality === 'PT') {
return 'SUV';
} else {
return '';
}
}
export default getModalityUnit;

View File

@ -1,12 +0,0 @@
export default function getPointsFromHandles(handles) {
let points = [];
Object.keys(handles).map(handle => {
if (['start', 'end'].includes(handle)) {
let point = {};
if (handles[handle].x) point.x = handles[handle].x;
if (handles[handle].y) point.y = handles[handle].y;
points.push(point);
}
});
return points;
}

Some files were not shown because too many files have changed in this diff Show More