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:
parent
045e54aa02
commit
1881ce5cbc
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -315,7 +315,7 @@ function DicomUploadProgress({
|
||||
className="ml-auto"
|
||||
onClick={onComplete}
|
||||
>
|
||||
{'Open Viewer'}
|
||||
{'Close'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@ -245,7 +245,7 @@ const mprAnd3DVolumeViewport = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
presetName: 'CT-Bone',
|
||||
displayPreset: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -121,7 +121,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
getEnabledElement,
|
||||
dicomLoaderService,
|
||||
registerColormap,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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,
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user