feat(custom displayOptions): add custom options to every level of hp (#3372)

* feat(custom displayOptions): add custom options to every level of hanging protocols

* move set preset down to cornerstone3d

* works

* fix tmtv based on new api

* fix unit test

* close

* review1

* upgrade packages

* update packages

* apply review comments

* recursive call

* less copy

* apply review comments

* fix e2e tests

* apply review comments

* revert

* apply review comments

* apply review comments

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
Alireza 2023-05-16 13:07:37 -04:00 committed by GitHub
parent 045e54aa02
commit 1881ce5cbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 240 additions and 851 deletions

View File

@ -39,8 +39,8 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"dcmjs": "^0.29.5",
"dicom-parser": "^1.8.9",
"dcmjs": "^0.29.6",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^17.0.2",

View File

@ -14,7 +14,6 @@ import { Types as OhifTypes } from '@ohif/core';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog';
import { setColormap } from './utils/colormap/transferFunctionHelpers';
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
@ -556,9 +555,9 @@ function commandsModule({
return actorEntry.uid.includes(displaySetInstanceUID);
});
const { actor: volumeActor } = actorEntry;
const { actor: volumeActor, uid: volumeId } = actorEntry;
setColormap(volumeActor, colormap);
viewport.setProperties({ colormap, volumeActor }, volumeId);
if (immediate) {
viewport.render();

View File

@ -315,7 +315,7 @@ function DicomUploadProgress({
className="ml-auto"
onClick={onComplete}
>
{'Open Viewer'}
{'Close'}
</Button>
</>
) : (

View File

@ -245,7 +245,7 @@ const mprAnd3DVolumeViewport = {
{
id: 'mprDisplaySet',
options: {
presetName: 'CT-Bone',
displayPreset: 'CT-Bone',
},
},
],

View File

@ -121,7 +121,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
},
getEnabledElement,
dicomLoaderService,
registerColormap,
},
},
{

View File

@ -30,10 +30,6 @@ import {
VolumeViewportData,
} from '../../types/CornerstoneCacheService';
import { Presentation, Presentations } from '../../types/Presentation';
import {
setColormap,
setLowerUpperColorTransferFunction,
} from '../../utils/colormap/transferFunctionHelpers';
import JumpPresets from '../../utils/JumpPresets';
@ -525,16 +521,9 @@ class CornerstoneViewportService extends PubSubService
const displaySetOptions = displaySetOptionsArray[index];
const { volumeId } = volume;
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),
});
@ -577,7 +566,44 @@ class CornerstoneViewportService extends PubSubService
toolGroupService,
} = this.servicesManager.services;
const viewportInfo = this.getViewportInfo(viewport.id);
const displaySetOptions = viewportInfo.getDisplaySetOptions();
// Todo: use presentations states
const volumesProperties = volumeInputArray.map((volumeInput, index) => {
const { volumeId } = volumeInput;
const displaySetOption = displaySetOptions[index];
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
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;
}
if (colormap !== undefined) {
properties.colormap = colormap;
}
if (displayPreset !== undefined) {
properties.preset = displayPreset;
}
return { properties, volumeId };
});
await viewport.setVolumes(volumeInputArray);
volumesProperties.forEach(({ properties, volumeId }) => {
viewport.setProperties(properties, volumeId);
});
this.setPresentations(viewport, presentations);
// load any secondary displaySets
@ -602,7 +628,6 @@ class CornerstoneViewportService extends PubSubService
);
}
const viewportInfo = this.getViewportInfo(viewport.id);
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
@ -737,50 +762,6 @@ class CornerstoneViewportService extends PubSubService
}
}
_getVOICallbacks(volumeId, displaySetOptions) {
const {
voi,
voiInverted: inverted,
colormap,
presetName,
} = 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,
})
);
}
if (presetName) {
voiCallbackArray.push(volumeActor => {
utilities.applyPreset(
volumeActor,
CONSTANTS.VIEWPORT_PRESETS.find(preset => {
return preset.name === presetName;
})
);
});
}
return voiCallbackArray;
}
_setDisplaySets(
viewport: StackViewport | VolumeViewport,
viewportData: StackViewportData | VolumeViewportData,

View File

@ -63,7 +63,7 @@ export type PublicDisplaySetOptions = {
blendMode?: string;
slabThickness?: number;
colormap?: string;
presetName?: string;
displayPreset?: string;
};
export type DisplaySetOptions = {
@ -73,7 +73,7 @@ export type DisplaySetOptions = {
blendMode?: Enums.BlendModes;
slabThickness?: number;
colormap?: string;
presetName?: string;
displayPreset?: string;
};
type VOI = {
@ -315,7 +315,7 @@ class ViewportInfo {
colormap: option.colormap,
slabThickness: option.slabThickness,
blendMode,
presetName: option.presetName,
displayPreset: option.displayPreset,
});
});

View File

@ -1,139 +0,0 @@
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

@ -1,29 +0,0 @@
// 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

@ -1,435 +0,0 @@
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

@ -1,93 +0,0 @@
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

@ -2,10 +2,13 @@ import {
addTool,
RectangleROIStartEndThresholdTool,
} from '@cornerstonejs/tools';
import { utilities } from '@cornerstonejs/core';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import colormaps from './utils/colormaps';
const { registerColormap } = utilities.colormap;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
/**
@ -42,15 +45,5 @@ export default function init({ servicesManager, extensionManager }) {
RectangleROIStartEndThreshold.toMeasurement
);
initColormaps(extensionManager);
}
function initColormaps(extensionManager) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
const { registerColormap } = utilityModule.exports;
colormaps.forEach(registerColormap);
}

View File

@ -121,8 +121,7 @@ const ptAXIAL = {
{
options: {
voi: {
windowWidth: 5,
windowCenter: 2.5,
custom: 'getPTVOIRange',
},
voiInverted: true,
},
@ -163,8 +162,7 @@ const ptSAGITTAL = {
{
options: {
voi: {
windowWidth: 5,
windowCenter: 2.5,
custom: 'getPTVOIRange',
},
voiInverted: true,
},
@ -205,8 +203,7 @@ const ptCORONAL = {
{
options: {
voi: {
windowWidth: 5,
windowCenter: 2.5,
custom: 'getPTVOIRange',
},
voiInverted: true,
},
@ -258,10 +255,12 @@ const fusionAXIAL = {
},
{
options: {
colormap: 'hsv',
colormap: {
name: 'hsv',
opacityMapping: [{ value: 0.1, opacity: 0.9 }],
},
voi: {
windowWidth: 5,
windowCenter: 2.5,
custom: 'getPTVOIRange',
},
},
id: 'ptDisplaySet',
@ -312,10 +311,12 @@ const fusionSAGITTAL = {
},
{
options: {
colormap: 'hsv',
colormap: {
name: 'hsv',
opacityMapping: [{ value: 0.1, opacity: 0.9 }],
},
voi: {
windowWidth: 5,
windowCenter: 2.5,
custom: 'getPTVOIRange',
},
},
id: 'ptDisplaySet',
@ -366,10 +367,12 @@ const fusionCORONAL = {
},
{
options: {
colormap: 'hsv',
colormap: {
name: 'hsv',
opacityMapping: [{ value: 0.1, opacity: 0.9 }],
},
voi: {
windowWidth: 5,
windowCenter: 2.5,
custom: 'getPTVOIRange',
},
},
id: 'ptDisplaySet',
@ -414,8 +417,7 @@ const mipSAGITTAL = {
blendMode: 'MIP',
slabThickness: 'fullVolume',
voi: {
windowWidth: 5,
windowCenter: 2.5,
custom: 'getPTVOIRange',
},
voiInverted: true,
},

View File

@ -1,10 +1,12 @@
import { hotkeys } from '@ohif/core';
import { hotkeys, classes } from '@ohif/core';
import toolbarButtons from './toolbarButtons.js';
import { id } from './id.js';
import initToolGroups, { toolGroupIds } from './initToolGroups.js';
import setCrosshairsConfiguration from './utils/setCrosshairsConfiguration.js';
import setFusionActiveVolume from './utils/setFusionActiveVolume.js';
const { MetadataProvider } = classes;
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
@ -131,6 +133,43 @@ function modeFactory({ modeConfiguration }) {
'RectangleROIStartEndThreshold',
'fusionPTColormap',
]);
// For the hanging protocol we need to decide on the window level
// based on whether the SUV is corrected or not, hence we can't hard
// code the window level in the hanging protocol but we add a custom
// attribute to the hanging protocol that will be used to get the
// window level based on the metadata
hangingProtocolService.addCustomAttribute(
'getPTVOIRange',
'get PT VOI based on corrected or not',
props => {
const ptDisplaySet = props.find(
imageSet => imageSet.Modality === 'PT'
);
if (!ptDisplaySet) {
return;
}
const { imageId } = ptDisplaySet.images[0];
const imageIdScalingFactor = MetadataProvider.get(
'scalingModule',
imageId
);
const isSUVAvailable =
imageIdScalingFactor && imageIdScalingFactor.suvbw;
if (isSUVAvailable) {
return {
windowWidth: 5,
windowCenter: 2.5,
};
}
return;
}
);
},
onModeExit: ({ servicesManager }) => {
const {

View File

@ -37,7 +37,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"dicom-parser": "^1.8.9",
"dicom-parser": "^1.8.21",
"@ohif/ui": "^2.0.0"
},
"dependencies": {

View File

@ -1,3 +1,5 @@
import cloneDeep from 'lodash.clonedeep';
import { PubSubService } from '../_shared/pubSubServiceInterface';
import sortBy from '../../utils/sortBy';
import ProtocolEngine from './ProtocolEngine';
@ -53,6 +55,12 @@ export default class HangingProtocolService extends PubSubService {
activeProtocolIds: string[];
// the current protocol that is being applied to the viewports in object format
protocol: HangingProtocol.Protocol;
// The version of the protocol that must not be modified with customizations
// if it was defined in the protocol definition. This is a copy of the protocol
// that is used to recompute the computedOptions when necessary as we override
// the computedOptions in the protocol object itself.
_originalProtocol: HangingProtocol.Protocol;
stageIndex = 0;
_commandsManager: CommandsManager;
_servicesManager: ServicesManager;
@ -611,6 +619,12 @@ export default class HangingProtocolService extends PubSubService {
return defaultReturn;
}
const originalProtocol = this._originalProtocol;
let originalProtocolStage;
if (!(originalProtocol instanceof Function)) {
originalProtocolStage = originalProtocol.stages[this.stageIndex];
}
// if we reach here, it means that the displaySetInstanceUIDs to be dropped
// in the viewportIndex are valid, and we can proceed with the update. However
// we need to check if the displaySets that the viewport were showing
@ -619,45 +633,44 @@ export default class HangingProtocolService extends PubSubService {
// check if displaySetSelectors are used by other viewports, and
// store the viewportIndex and displaySetInstanceUIDs that need to be updated
const viewportsToUpdate = [];
protocolViewports.forEach((viewport, index) => {
let viewportNeedsUpdate;
for (const displaySet of viewport.displaySets) {
if (
const viewportNeedsUpdate = viewport.displaySets.some(
displaySet =>
displaySet.id === displaySetSelectorId &&
(displaySet.matchedDisplaySetsIndex || 0) === matchedDisplaySetsIndex
) {
viewportNeedsUpdate = true;
break;
}
}
);
if (viewportNeedsUpdate) {
// we can then loop over the displaySets and choose all of them,
// but for the one that matches the oldDisplaySetInstanceUID we need to
// replace it with the newDisplaySetInstanceUID
const {
// Try to recompute the viewport options based on the current
// viewportIndex that needs update but from its old/original un-computed
// viewport & displaySet options
if (originalProtocolStage) {
const originalViewport = originalProtocolStage.viewports[index];
const originalViewportOptions = originalViewport.viewportOptions;
const originalDisplaySetOptions = originalViewport.displaySets;
viewport.viewportOptions = this.getComputedOptions(
originalViewportOptions,
[newDisplaySetInstanceUID]
);
viewport.displaySets = this.getComputedOptions(
originalDisplaySetOptions,
[newDisplaySetInstanceUID]
);
}
const displaySetInstanceUIDs = [];
const displaySetOptions = [];
this._updateDisplaySetInstanceUIDs(
viewport,
displaySetSelectorId,
newDisplaySetInstanceUID,
this.displaySetMatchDetails,
displaySetInstanceUIDs,
displaySetOptions,
} = viewport.displaySets.reduce(
(acc, displaySet) => {
const { id } = displaySet;
let {
displaySetInstanceUID: displaySetInstanceUIDToUse,
} = this.displaySetMatchDetails.get(id);
if (displaySet.id === displaySetSelectorId) {
displaySetInstanceUIDToUse = newDisplaySetInstanceUID;
}
acc.displaySetInstanceUIDs.push(displaySetInstanceUIDToUse);
acc.displaySetOptions.push(displaySet);
return acc;
},
{ displaySetInstanceUIDs: [], displaySetOptions: [] }
displaySetOptions
);
viewportsToUpdate.push({
@ -672,6 +685,29 @@ export default class HangingProtocolService extends PubSubService {
return viewportsToUpdate;
}
private _updateDisplaySetInstanceUIDs(
viewport: HangingProtocol.Viewport,
displaySetSelectorId: string,
newDisplaySetInstanceUID: string,
displaySetMatchDetails: Map<string, HangingProtocol.DisplaySetMatchDetails>,
displaySetInstanceUIDs: string[],
displaySetOptions: HangingProtocol.DisplaySetOptions[]
) {
viewport.displaySets.forEach(displaySet => {
const { id } = displaySet;
const {
displaySetInstanceUID: oldDisplaySetInstanceUID,
} = displaySetMatchDetails.get(id);
displaySetInstanceUIDs.push(
displaySet.id === displaySetSelectorId
? newDisplaySetInstanceUID
: oldDisplaySetInstanceUID
);
displaySetOptions.push(displaySet);
});
}
/**
* Gets a computed options value, or a copy of the options
* This allows computing values such as the initial image index to use
@ -681,28 +717,53 @@ export default class HangingProtocolService extends PubSubService {
* simpler than recomputing the entire protocol.
*/
public getComputedOptions(
options: Record<string, unknown>,
options: Record<string, unknown> | Array<Record<string, unknown>>,
displaySetUIDs: string[]
) {
const computed = { ...options };
let displaySets;
for (const key in computed) {
const value = computed[key];
if (!value) continue;
if (value.custom) {
if (!displaySets) {
displaySets = this.displaySets.filter(
displaySet =>
displaySetUIDs.indexOf(displaySet.displaySetInstanceUID) !== -1
);
}
computed[key] = this.customAttributeRetrievalCallbacks[
value.custom
].callback.call(computed, displaySets);
if (computed[key] === undefined) computed[key] = computed.defaultValue;
): any {
// Base case: if options is an array, map over the array and recursively call getComputedOptions
if (Array.isArray(options)) {
return options.map(option =>
this.getComputedOptions(option, displaySetUIDs)
);
}
if (options === null) return options;
if (typeof options !== 'object') return options;
// If options is an object with a custom attribute, compute a new options object
if (options.custom) {
const displaySets = this.displaySets.filter(displaySet =>
displaySetUIDs.includes(displaySet.displaySetInstanceUID)
);
const customKey = options.custom as string;
if (!(customKey in this.customAttributeRetrievalCallbacks)) {
throw new Error(
`Custom key "${customKey}" not found in customAttributeRetrievalCallbacks.`
);
}
const callback = this.customAttributeRetrievalCallbacks[customKey]
.callback;
let newOptions = callback.call(options, displaySets);
if (newOptions === undefined) {
newOptions = options.defaultValue;
}
return this.getComputedOptions(newOptions, displaySetUIDs);
}
// If options is an object without a custom attribute, recursively call getComputedOptions on its properties
const newOptions = {} as Record<string, unknown>;
for (const key in options) {
// if not undefined
if (options[key] !== undefined) {
newOptions[key] = this.getComputedOptions(options[key], displaySetUIDs);
}
}
return computed;
return newOptions;
}
/**
@ -859,7 +920,8 @@ export default class HangingProtocolService extends PubSubService {
try {
if (!this.protocol || this.protocol.id !== protocol.id) {
this.stageIndex = options?.stageIndex || 0;
this.protocol = this._copyProtocol(protocol);
this._originalProtocol = this._copyProtocol(protocol);
this.protocol = protocol;
const { imageLoadStrategy } = protocol;
if (imageLoadStrategy) {
@ -1522,6 +1584,6 @@ export default class HangingProtocolService extends PubSubService {
}
_copyProtocol(protocol: Protocol) {
return JSON.parse(JSON.stringify(protocol));
return cloneDeep(protocol);
}
}

View File

@ -423,7 +423,8 @@ As you can see in the hanging protocol we defined three viewports (but only show
- `options` (optional): options for the display set
- voi: windowing options for the display set (optional: windowWidth, windowCenter)
- voiInverted: whether the VOI is inverted or not (optional)
- colormap: colormap for the display set (optional: 'hsv' etc.)
- colormap: colormap for the display set (optional, it is an object with `{ name }` and optional extra `opacityMapping` property)
- displayPreset: display preset for the display set (optional, used for 3D volume rendering. e.g., 'CT-Bone')
### Custom attribute

View File

@ -309,6 +309,8 @@ viewportOptions: {
custom: 'sopInstanceLocation',
// This is the value returned if the above doesn't return anything
defaultValue: { index: 5 },
}
}
```
### Included Custom Attributes

View File

@ -72,13 +72,20 @@ function ViewerViewportGrid(props) {
}
);
const computedViewportOptions = hangingProtocolService.getComputedOptions(
viewportOptions,
displaySetUIDsToHang
);
const computedDisplaySetOptions = hangingProtocolService.getComputedOptions(
displaySetUIDsToHangOptions,
displaySetUIDsToHang
);
return {
displaySetInstanceUIDs: displaySetUIDsToHang,
displaySetOptions: displaySetUIDsToHangOptions,
viewportOptions: hangingProtocolService.getComputedOptions(
viewportOptions,
displaySetUIDsToHang
),
displaySetOptions: computedDisplaySetOptions,
viewportOptions: computedViewportOptions,
};
};