@cornerstonejs/core
Version:
Cornerstone3D Core
251 lines (250 loc) • 8.89 kB
JavaScript
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction.js';
import { createPlanarRGBTransferFunction } from '../../helpers/planarImageRendering.js';
import { mapBlendModeToSlabType, resolveSlabThickness, } from './planarVolumeSliceBlendMode.js';
export function applyPlanarVolumePresentation(args) {
const { actor, defaultVOIRange, mapper, props } = args;
const property = actor.getProperty();
const voiRange = props?.voiRange ?? defaultVOIRange;
actor.setVisibility(props?.visible === false ? false : true);
if (props?.interpolationType !== undefined) {
property.setInterpolationType(props.interpolationType);
}
if (props?.blendMode !== undefined) {
const slabType = mapBlendModeToSlabType(props.blendMode);
if (slabType !== undefined) {
mapper.setSlabType(slabType);
}
}
mapper.setSlabThickness(resolveSlabThickness(props?.slabThickness));
if (props?.opacity !== undefined) {
applyVolumeOpacity({
actor,
opacity: props.opacity,
voiRange,
});
}
if (!voiRange) {
return;
}
const transferFunction = createPlanarRGBTransferFunction({
colormap: props?.colormap,
invert: props?.invert,
voiRange,
});
property.setUseLookupTableScalarRange(true);
property.setRGBTransferFunction(0, transferFunction);
applyColormapOpacity({
actor,
colormap: props?.colormap,
voiRange,
});
}
function applyVolumeOpacity(args) {
const { actor, opacity, voiRange } = args;
if (getVolumeNumberOfComponents(actor) < 2) {
actor.getProperty().setOpacity(opacity);
return;
}
const opacityFunction = vtkPiecewiseFunction.newInstance();
const range = resolveVolumeOpacityRange(actor, voiRange);
opacityFunction.addPoint(range.lower, opacity);
if (range.upper > range.lower) {
opacityFunction.addPoint(range.upper, opacity);
}
actor.getProperty().setScalarOpacity(0, opacityFunction);
}
function applyColormapOpacity(args) {
const { actor, colormap, voiRange } = args;
if (!colormap) {
return;
}
if (colormap.opacity === undefined && colormap.threshold === undefined) {
return;
}
const range = colormap.threshold !== undefined
? (getActorScalarRange(actor) ??
resolveVolumeOpacityRange(actor, voiRange))
: resolveVolumeOpacityRange(actor, voiRange);
const opacityFunction = vtkPiecewiseFunction.newInstance();
const points = buildColormapOpacityPoints({
colormap,
range,
fallbackOpacity: getCurrentMaxOpacity(actor),
});
points.forEach(([value, opacity]) => {
opacityFunction.addPoint(value, opacity);
});
actor.getProperty().setScalarOpacity(0, opacityFunction);
}
function buildColormapOpacityPoints(args) {
const { colormap, range, fallbackOpacity } = args;
const span = range.upper - range.lower;
const delta = Math.max(Math.abs(span) * 0.001, 1e-3);
const toScalar = (normalized) => range.lower + normalized * span;
const threshold = colormap.threshold !== undefined
? Math.max(range.lower, Math.min(range.upper, colormap.threshold))
: undefined;
if (Array.isArray(colormap.opacity) && colormap.opacity.length) {
const sortedPoints = colormap.opacity
.map(({ opacity, value }) => ({
opacity: clampToUnit(opacity),
value: toScalar(value),
}))
.sort((a, b) => a.value - b.value);
if (threshold === undefined) {
return dedupeOpacityPoints(sortedPoints.map(({ value, opacity }) => [value, opacity]));
}
const thresholdOpacity = resolvePointOpacityAtValue(threshold, sortedPoints, fallbackOpacity);
const points = [
[range.lower, 0],
[Math.max(range.lower, threshold - delta), 0],
[threshold, thresholdOpacity],
];
sortedPoints.forEach(({ value, opacity }) => {
if (value > threshold) {
points.push([value, opacity]);
}
});
if (threshold < range.upper) {
const lastOpacity = points[points.length - 1][1];
if (points[points.length - 1][0] < range.upper) {
points.push([range.upper, lastOpacity]);
}
}
return dedupeOpacityPoints(points);
}
const opacity = colormap.opacity !== undefined
? clampToUnit(colormap.opacity)
: clampToUnit(fallbackOpacity);
if (threshold === undefined) {
return [
[range.lower, opacity],
[range.upper, opacity],
];
}
if (threshold >= range.upper) {
return [
[range.lower, 0],
[range.upper, 0],
];
}
return [
[range.lower, 0],
[Math.max(range.lower, threshold - delta), 0],
[threshold, opacity],
[range.upper, opacity],
];
}
function resolvePointOpacityAtValue(scalarValue, opacityPoints, fallbackOpacity) {
if (!opacityPoints.length) {
return clampToUnit(fallbackOpacity);
}
if (scalarValue <= opacityPoints[0].value) {
return opacityPoints[0].opacity;
}
for (let i = 1; i < opacityPoints.length; i++) {
const previous = opacityPoints[i - 1];
const current = opacityPoints[i];
if (scalarValue <= current.value) {
const pointRange = current.value - previous.value;
const t = pointRange > 0 ? (scalarValue - previous.value) / pointRange : 0;
return clampToUnit(previous.opacity + (current.opacity - previous.opacity) * t);
}
}
return opacityPoints[opacityPoints.length - 1].opacity;
}
function dedupeOpacityPoints(points) {
const deduped = [];
points.forEach(([value, opacity]) => {
const lastPoint = deduped[deduped.length - 1];
if (lastPoint && lastPoint[0] === value) {
lastPoint[1] = opacity;
return;
}
deduped.push([value, opacity]);
});
return deduped;
}
function getCurrentMaxOpacity(actor) {
const opacityFunction = actor.getProperty().getScalarOpacity(0);
const opacityValues = opacityFunction?.getDataPointer?.();
if (!opacityValues?.length) {
return actor.getProperty().getOpacity?.() ?? 1;
}
let maxOpacity = 0;
for (let i = 1; i < opacityValues.length; i += 2) {
if (opacityValues[i] > maxOpacity) {
maxOpacity = opacityValues[i];
}
}
return clampToUnit(maxOpacity || 1);
}
function clampToUnit(value) {
return Math.max(0, Math.min(1, value));
}
function getVolumeNumberOfComponents(actor) {
const mapperInputData = actor.getMapper()?.getInputData?.();
const scalars = mapperInputData?.getPointData?.()?.getScalars?.();
const imageDataMetadata = mapperInputData?.get?.('numberOfComponents');
return (scalars?.getNumberOfComponents?.() ??
imageDataMetadata?.numberOfComponents ??
1);
}
function getActorScalarRange(actor) {
const mapperInputData = actor.getMapper()?.getInputData?.();
const voxelManager = mapperInputData?.get?.('voxelManager')?.voxelManager;
const voxelRange = voxelManager?.getRange?.();
if (voxelRange &&
Number.isFinite(voxelRange[0]) &&
Number.isFinite(voxelRange[1]) &&
voxelRange[1] > voxelRange[0]) {
return { lower: voxelRange[0], upper: voxelRange[1] };
}
const scalarRange = mapperInputData
?.getPointData?.()
?.getScalars?.()
?.getRange?.();
if (scalarRange &&
Number.isFinite(scalarRange[0]) &&
Number.isFinite(scalarRange[1]) &&
scalarRange[1] > scalarRange[0]) {
return { lower: scalarRange[0], upper: scalarRange[1] };
}
return undefined;
}
function resolveVolumeOpacityRange(actor, voiRange) {
if (voiRange &&
Number.isFinite(voiRange.lower) &&
Number.isFinite(voiRange.upper)) {
return voiRange;
}
const mapperInputData = actor.getMapper()?.getInputData?.();
const scalarRange = mapperInputData
?.getPointData?.()
?.getScalars?.()
?.getRange?.();
if (scalarRange &&
Number.isFinite(scalarRange[0]) &&
Number.isFinite(scalarRange[1])) {
return {
lower: scalarRange[0],
upper: scalarRange[1],
};
}
const transferRange = actor
.getProperty()
.getRGBTransferFunction(0)
?.getRange?.();
if (transferRange &&
Number.isFinite(transferRange[0]) &&
Number.isFinite(transferRange[1])) {
return {
lower: transferRange[0],
upper: transferRange[1],
};
}
return {
lower: 0,
upper: 1,
};
}