@cornerstonejs/core
Version:
Cornerstone3D Core
687 lines (686 loc) • 32.4 kB
JavaScript
import { vec3 } from 'gl-matrix';
import { EPSILON } from '../../../constants/index.js';
import { InterpolationType, VOILUTFunctionType } from '../../../enums/index.js';
import { resolveCPUFallbackColormap } from '../../helpers/cpuFallback/colors/index.js';
import VoxelManager from '../../../utilities/VoxelManager.js';
import getDefaultViewport from '../../helpers/cpuFallback/rendering/getDefaultViewport.js';
import getSpacingInNormalDirection from '../../../utilities/getSpacingInNormalDirection.js';
import { getPlanarScaleRatio } from './planarCameraScale.js';
import PlanarCPUScalarViewportSampler from './PlanarCPUScalarViewportSampler.js';
import { getIndexMajorAxis, getNearestVoxelIndex, getSpatiallyClampedContinuousIndex, SOURCE_SLICE_INDEX_TOLERANCE, } from './planarCPUVolumeSamplingUtils.js';
const MAX_POOLED_SLICE_ARRAYS_PER_SHAPE = 1;
function dot(a, b) {
return vec3.dot(a, b);
}
function clampFiniteSample(value, fallbackMin, fallbackMax, round) {
if (!Number.isFinite(value)) {
return fallbackMin;
}
let clampedValue = value;
if (clampedValue < fallbackMin) {
clampedValue = fallbackMin;
}
else if (clampedValue > fallbackMax) {
clampedValue = fallbackMax;
}
return round ? Math.round(clampedValue) : clampedValue;
}
function expandSampleValueRange(sampleRange, value) {
if (value < sampleRange.min) {
sampleRange.min = value;
}
if (value > sampleRange.max) {
sampleRange.max = value;
}
}
function subtractPoints(a, b) {
return vec3.subtract([0, 0, 0], a, b);
}
function arePointsClose(a, b, tolerance = 1e-4) {
return vec3.distance(a, b) <= tolerance;
}
function indexToWorld(volume, ijk) {
const [i, j, k] = ijk;
const [sx, sy, sz] = volume.spacing;
const row = volume.direction.slice(0, 3);
const col = volume.direction.slice(3, 6);
const scan = volume.direction.slice(6, 9);
const world = vec3.copy([0, 0, 0], volume.origin);
vec3.scaleAndAdd(world, world, row, sx * i);
vec3.scaleAndAdd(world, world, col, sy * j);
vec3.scaleAndAdd(world, world, scan, sz * k);
return world;
}
function worldVectorToContinuousIndexDelta(volume, worldVector) {
const row = volume.direction.slice(0, 3);
const col = volume.direction.slice(3, 6);
const scan = volume.direction.slice(6, 9);
return [
dot(worldVector, row) / volume.spacing[0],
dot(worldVector, col) / volume.spacing[1],
dot(worldVector, scan) / volume.spacing[2],
];
}
export default class PlanarCPUVolumeSampler {
constructor() {
this.sampleSequence = 0;
this.scalarViewportSampler = new PlanarCPUScalarViewportSampler();
this.scalarRangeCache = new WeakMap();
this.sliceArrayPool = new Map();
}
clearCachedScalarRange(voxelManager) {
this.scalarRangeCache.delete(voxelManager);
this.scalarViewportSampler.clearCachedVoxelManager(voxelManager);
}
releaseSampledSliceState(sampledSliceState) {
const scalarData = sampledSliceState?.image.getPixelData?.();
if (!scalarData ||
!ArrayBuffer.isView(scalarData) ||
scalarData instanceof DataView) {
return;
}
this.releaseSliceArray(scalarData);
}
clearSliceArrayPool() {
this.sliceArrayPool.clear();
}
getScalarDataRange(voxelManager) {
let scalarData;
scalarData =
this.scalarViewportSampler.getCompleteScalarDataArray(voxelManager) ??
voxelManager.getScalarData();
let min = Infinity;
let max = -Infinity;
for (let index = 0; index < scalarData.length; index++) {
const value = Number(scalarData[index]);
if (!Number.isFinite(value)) {
continue;
}
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
}
return { min, max };
}
getCameraBasis(camera) {
const normal = vec3.normalize(vec3.create(), camera.viewPlaneNormal);
const rawUp = vec3.normalize(vec3.create(), camera.viewUp);
let right = vec3.cross(vec3.create(), rawUp, normal);
if (vec3.length(right) < EPSILON) {
right = vec3.cross(vec3.create(), [0, 1, 0], normal);
}
right = vec3.normalize(vec3.create(), right);
const up = vec3.normalize(vec3.create(), vec3.cross(vec3.create(), normal, right));
return { right, up, normal };
}
getResolvedVOIRange(voiRange, fallbackLower, fallbackUpper) {
if (voiRange &&
Number.isFinite(voiRange.lower) &&
Number.isFinite(voiRange.upper) &&
voiRange.upper > voiRange.lower) {
return voiRange;
}
if (fallbackUpper > fallbackLower) {
return { lower: fallbackLower, upper: fallbackUpper };
}
return { lower: fallbackLower, upper: fallbackLower + 1 };
}
getFallbackStoredRange(volume) {
const voxelManager = volume.voxelManager;
const [volumeMin, volumeMax] = voxelManager.getRange();
let min = Number.isFinite(volumeMin) ? Math.floor(volumeMin) : 0;
let max = Number.isFinite(volumeMax) ? Math.ceil(volumeMax) : min + 1;
if (max <= min) {
const cachedRange = this.scalarRangeCache.get(voxelManager);
if (cachedRange) {
return cachedRange;
}
const resolvedRange = this.getScalarDataRange(voxelManager);
if (Number.isFinite(resolvedRange.min)) {
min = Math.floor(resolvedRange.min);
}
if (Number.isFinite(resolvedRange.max)) {
max = Math.ceil(resolvedRange.max);
}
}
if (max <= min) {
max = min + 1;
}
const resolvedRange = { min, max };
this.scalarRangeCache.set(voxelManager, resolvedRange);
return resolvedRange;
}
createOrUpdateEnabledElement(args) {
const { enabledElement, canvas, image, modality } = args;
if (enabledElement) {
enabledElement.canvas = canvas;
enabledElement.image = image;
enabledElement.options ||= {};
enabledElement.options.transparentBackground = true;
enabledElement.viewport = getDefaultViewport(canvas, image, modality);
return enabledElement;
}
return {
canvas,
image,
options: {
transparentBackground: true,
},
renderingTools: {},
viewport: getDefaultViewport(canvas, image, modality),
};
}
updateCPUFallbackViewport(args) {
const { enabledElement, sampledSliceState, camera, dataPresentation, defaultVOIRange, } = args;
const rowPixelSpacing = sampledSliceState.image.rowPixelSpacing || 1;
const columnPixelSpacing = sampledSliceState.image.columnPixelSpacing || 1;
const focalDelta = subtractPoints(camera.focalPoint, sampledSliceState.translationReferenceFocalPoint);
const viewport = enabledElement.viewport;
const resolvedVOI = this.getResolvedVOIRange(dataPresentation?.voiRange ?? defaultVOIRange, sampledSliceState.image.minPixelValue ?? 0, sampledSliceState.image.maxPixelValue ?? 1);
viewport.translation = {
x: -dot(focalDelta, sampledSliceState.right) / columnPixelSpacing,
y: dot(focalDelta, sampledSliceState.up) / rowPixelSpacing,
};
viewport.scale = resolveViewportScale({
canvas: enabledElement.canvas,
camera,
columnPixelSpacing,
rowPixelSpacing,
});
viewport.parallelScale = camera.parallelScale;
viewport.colormap = resolveCPUFallbackColormap(dataPresentation?.colormap, sampledSliceState.image.colormap, {
voiRange: resolvedVOI,
});
viewport.invert = dataPresentation?.invert ?? false;
viewport.pixelReplication =
dataPresentation?.interpolationType === InterpolationType.NEAREST;
viewport.voi = {
windowCenter: (resolvedVOI.lower + resolvedVOI.upper) / 2,
windowWidth: Math.max(resolvedVOI.upper - resolvedVOI.lower, 1),
voiLUTFunction: VOILUTFunctionType.LINEAR,
};
}
needsResample(args) {
return this.getResampleDecision(args) !== 'reuse';
}
getResampleDecision(args) {
const { sampledSliceState, width, height, camera, dataPresentation, deferViewportResample = false, } = args;
if (!sampledSliceState) {
return 'resample';
}
const { right, up, normal } = this.getCameraBasis(camera);
const focalPoint = camera.focalPoint;
const interpolationType = dataPresentation?.interpolationType ?? InterpolationType.LINEAR;
const focalDelta = subtractPoints(focalPoint, sampledSliceState.focalPoint);
const deltaInNormal = Math.abs(dot(focalDelta, sampledSliceState.normal));
const columnPixelSpacing = sampledSliceState.image.columnPixelSpacing || 1;
const rowPixelSpacing = sampledSliceState.image.rowPixelSpacing || 1;
const shiftXPixels = Math.abs(dot(focalDelta, sampledSliceState.right)) / columnPixelSpacing;
const shiftYPixels = Math.abs(dot(focalDelta, sampledSliceState.up)) / rowPixelSpacing;
const samplingMode = sampledSliceState.samplingMode ?? 'viewport';
const parallelScale = Math.max(camera.parallelScale ?? 1, EPSILON);
const scaleRatio = getPlanarScaleRatio(camera.presentationScale);
const orientationChanged = !arePointsClose(sampledSliceState.right, right) ||
!arePointsClose(sampledSliceState.up, up) ||
!arePointsClose(sampledSliceState.normal, normal);
const sliceChanged = deltaInNormal > sampledSliceState.spacingInNormalDirection * 0.5;
const requiresImmediateResample = sampledSliceState.canvasWidth !== width ||
sampledSliceState.canvasHeight !== height ||
sampledSliceState.interpolationType !== interpolationType ||
orientationChanged ||
sliceChanged;
if (requiresImmediateResample) {
return 'resample';
}
if (samplingMode === 'source-slice') {
return 'reuse';
}
const viewportSampleNeedsRefresh = Math.abs(sampledSliceState.parallelScale - parallelScale) >
parallelScale * 1e-4 ||
Math.abs(sampledSliceState.scaleRatio - scaleRatio) > 1e-4 ||
shiftXPixels > 1e-3 ||
shiftYPixels > 1e-3 ||
shiftXPixels > sampledSliceState.image.width * 0.35 ||
shiftYPixels > sampledSliceState.image.height * 0.35;
if (!viewportSampleNeedsRefresh) {
return 'reuse';
}
return deferViewportResample ? 'defer' : 'resample';
}
sampleSliceImage(args) {
const { volume, width, height, camera, dataPresentation, useViewportSamplingForLinear = true, } = args;
const { right, up, normal } = this.getCameraBasis(camera);
const numberOfComponents = this.getVolumeNumberOfComponents(volume);
const preserveFloatScalarSamples = numberOfComponents === 1 && this.shouldPreserveFloatScalarSamples(volume);
const interpolationType = dataPresentation?.interpolationType ?? InterpolationType.LINEAR;
const canUseOrthogonalSourceSlice = interpolationType === InterpolationType.NEAREST ||
!useViewportSamplingForLinear;
const orthogonalSlice = canUseOrthogonalSourceSlice
? this.trySampleOrthogonalSliceFromVoxelManager(volume, camera, right, up, normal, interpolationType)
: undefined;
const fallbackRange = this.getFallbackStoredRange(volume);
const voiRange = this.getResolvedVOIRange(dataPresentation?.voiRange, fallbackRange.min, fallbackRange.max);
if (orthogonalSlice) {
return {
image: this.createSliceImage(volume, orthogonalSlice.scalarData, orthogonalSlice.width, orthogonalSlice.height, orthogonalSlice.columnPixelSpacing, orthogonalSlice.rowPixelSpacing, orthogonalSlice.minPixelValue, orthogonalSlice.maxPixelValue, orthogonalSlice.numberOfComponents, voiRange),
samplingMode: 'source-slice',
focalPoint: vec3.clone(camera.focalPoint),
translationReferenceFocalPoint: vec3.clone(orthogonalSlice.translationReferenceFocalPoint),
right: vec3.clone(right),
up: vec3.clone(up),
normal: vec3.clone(normal),
spacingInNormalDirection: Math.max(getSpacingInNormalDirection(volume, normal), EPSILON),
canvasWidth: width,
canvasHeight: height,
parallelScale: Math.max(camera.parallelScale ?? 1, EPSILON),
scaleRatio: getPlanarScaleRatio(camera.presentationScale),
interpolationType,
};
}
const voxelManager = volume.voxelManager;
if (!voxelManager) {
throw new Error('[PlanarViewport] CPU volume rendering requires voxels');
}
const parallelScale = Math.max(camera.parallelScale ?? 1, EPSILON);
const worldHeight = parallelScale * 2;
const worldWidth = worldHeight *
(width / Math.max(height, 1)) *
(1 / getPlanarScaleRatio(camera.presentationScale));
const xStep = worldWidth / Math.max(width, 1);
const yStep = worldHeight / Math.max(height, 1);
const xStart = -worldWidth / 2 + xStep / 2;
const yStart = worldHeight / 2 - yStep / 2;
const centerIndex = VoxelManager.worldToIndexContinuous(volume, camera.focalPoint);
const startIndexDelta = worldVectorToContinuousIndexDelta(volume, [
right[0] * xStart + up[0] * yStart,
right[1] * xStart + up[1] * yStart,
right[2] * xStart + up[2] * yStart,
]);
const xStepIndexDelta = worldVectorToContinuousIndexDelta(volume, [
right[0] * xStep,
right[1] * xStep,
right[2] * xStep,
]);
const yStepIndexDelta = worldVectorToContinuousIndexDelta(volume, [
-up[0] * yStep,
-up[1] * yStep,
-up[2] * yStep,
]);
const SliceArrayConstructor = this.getSliceArrayConstructor(volume, fallbackRange.min, fallbackRange.max, numberOfComponents, preserveFloatScalarSamples);
const sliceScalarData = this.acquireSliceArray(SliceArrayConstructor, width * height * numberOfComponents);
const rowStartIndex = [
centerIndex[0] + startIndexDelta[0],
centerIndex[1] + startIndexDelta[1],
centerIndex[2] + startIndexDelta[2],
];
const sampledRange = {
min: Infinity,
max: -Infinity,
};
const scalarViewportSample = this.scalarViewportSampler.sampleAxisAligned({
volume,
voxelManager,
pixelData: sliceScalarData,
width,
height,
rowStartIndex,
xStepIndexDelta,
yStepIndexDelta,
right,
up,
normal,
interpolationType,
numberOfComponents,
fallbackMin: fallbackRange.min,
fallbackMax: fallbackRange.max,
});
if (scalarViewportSample) {
sampledRange.min = scalarViewportSample.min;
sampledRange.max = scalarViewportSample.max;
}
else {
const sampleIndex = [...rowStartIndex];
for (let y = 0; y < height; y++) {
sampleIndex[0] = rowStartIndex[0];
sampleIndex[1] = rowStartIndex[1];
sampleIndex[2] = rowStartIndex[2];
for (let x = 0; x < width; x++) {
const sampledValue = this.sampleVoxelAtContinuousIndex(voxelManager, volume.dimensions, sampleIndex, numberOfComponents, interpolationType);
this.writeVoxelValue(sliceScalarData, y * width + x, sampledValue, numberOfComponents, fallbackRange.min, fallbackRange.max, sampledRange);
sampleIndex[0] += xStepIndexDelta[0];
sampleIndex[1] += xStepIndexDelta[1];
sampleIndex[2] += xStepIndexDelta[2];
}
rowStartIndex[0] += yStepIndexDelta[0];
rowStartIndex[1] += yStepIndexDelta[1];
rowStartIndex[2] += yStepIndexDelta[2];
}
}
const minPixelValue = Number.isFinite(sampledRange.min)
? preserveFloatScalarSamples
? sampledRange.min
: Math.floor(sampledRange.min)
: fallbackRange.min;
const maxPixelValue = Number.isFinite(sampledRange.max) && sampledRange.max > sampledRange.min
? preserveFloatScalarSamples
? sampledRange.max
: Math.ceil(sampledRange.max)
: Math.max(minPixelValue + 1, fallbackRange.max);
return {
image: this.createSliceImage(volume, sliceScalarData, width, height, worldWidth / Math.max(width, 1), worldHeight / Math.max(height, 1), minPixelValue, maxPixelValue, numberOfComponents, voiRange),
samplingMode: 'viewport',
focalPoint: vec3.clone(camera.focalPoint),
translationReferenceFocalPoint: vec3.clone(camera.focalPoint),
right: vec3.clone(right),
up: vec3.clone(up),
normal: vec3.clone(normal),
spacingInNormalDirection: Math.max(getSpacingInNormalDirection(volume, normal), EPSILON),
canvasWidth: width,
canvasHeight: height,
parallelScale,
scaleRatio: getPlanarScaleRatio(camera.presentationScale),
interpolationType,
};
}
trySampleOrthogonalSliceFromVoxelManager(volume, camera, right, up, normal, interpolationType) {
const voxelManager = volume.voxelManager;
if (!voxelManager) {
return;
}
const normalAxis = getIndexMajorAxis(volume, normal);
const rightAxis = getIndexMajorAxis(volume, right);
const upAxis = getIndexMajorAxis(volume, up);
if (!normalAxis || !rightAxis || !upAxis) {
return;
}
if (normalAxis.axis === rightAxis.axis ||
normalAxis.axis === upAxis.axis ||
rightAxis.axis === upAxis.axis) {
return;
}
const downAxis = upAxis.axis;
const downSign = -upAxis.sign;
const rightSign = rightAxis.sign;
const numberOfComponents = this.getVolumeNumberOfComponents(volume);
const continuousIndex = VoxelManager.worldToIndexContinuous(volume, camera.focalPoint);
const continuousNormalIndex = continuousIndex[normalAxis.axis];
const roundedNormalIndex = Math.round(continuousNormalIndex);
const normalUpperBound = volume.dimensions[normalAxis.axis] - 0.5;
if (continuousNormalIndex < -0.5 - SOURCE_SLICE_INDEX_TOLERANCE ||
continuousNormalIndex > normalUpperBound + SOURCE_SLICE_INDEX_TOLERANCE) {
return;
}
if (interpolationType !== InterpolationType.NEAREST &&
Math.abs(continuousNormalIndex - roundedNormalIndex) >
SOURCE_SLICE_INDEX_TOLERANCE) {
return;
}
const normalIndex = interpolationType === InterpolationType.NEAREST
? Math.min(volume.dimensions[normalAxis.axis] - 1, Math.max(0, getNearestVoxelIndex(continuousNormalIndex)))
: roundedNormalIndex;
if (normalIndex < 0 || normalIndex >= volume.dimensions[normalAxis.axis]) {
return;
}
const referenceIndex = [
(volume.dimensions[0] - 1) / 2,
(volume.dimensions[1] - 1) / 2,
(volume.dimensions[2] - 1) / 2,
];
const preserveFloatScalarSamples = numberOfComponents === 1 && this.shouldPreserveFloatScalarSamples(volume);
referenceIndex[normalAxis.axis] = normalIndex;
const translationReferenceFocalPoint = indexToWorld(volume, referenceIndex);
const outputWidth = volume.dimensions[rightAxis.axis];
const outputHeight = volume.dimensions[downAxis];
const fallbackRange = this.getFallbackStoredRange(volume);
const SliceArrayConstructor = this.getSliceArrayConstructor(volume, fallbackRange.min, fallbackRange.max, numberOfComponents, preserveFloatScalarSamples);
const scalarData = this.acquireSliceArray(SliceArrayConstructor, outputWidth * outputHeight * numberOfComponents);
const sampleRange = {
min: Infinity,
max: -Infinity,
};
const ijk = [0, 0, 0];
ijk[normalAxis.axis] = normalIndex;
for (let y = 0; y < outputHeight; y++) {
ijk[downAxis] = downSign > 0 ? y : outputHeight - 1 - y;
for (let x = 0; x < outputWidth; x++) {
ijk[rightAxis.axis] = rightSign > 0 ? x : outputWidth - 1 - x;
this.writeVoxelValue(scalarData, y * outputWidth + x, voxelManager.getAtIJK(ijk[0], ijk[1], ijk[2]), numberOfComponents, fallbackRange.min, fallbackRange.max, sampleRange);
}
}
if (!Number.isFinite(sampleRange.min)) {
sampleRange.min = fallbackRange.min;
}
if (!Number.isFinite(sampleRange.max) ||
sampleRange.max <= sampleRange.min) {
sampleRange.max = Math.max(sampleRange.min + 1, fallbackRange.max);
}
return {
scalarData,
width: outputWidth,
height: outputHeight,
columnPixelSpacing: volume.spacing[rightAxis.axis],
rowPixelSpacing: volume.spacing[downAxis],
minPixelValue: preserveFloatScalarSamples
? sampleRange.min
: Math.floor(sampleRange.min),
maxPixelValue: preserveFloatScalarSamples
? sampleRange.max
: Math.ceil(sampleRange.max),
numberOfComponents,
translationReferenceFocalPoint,
};
}
sampleVoxelAtContinuousIndex(voxelManager, dimensions, continuousIndex, numberOfComponents, interpolationType) {
const clampedIndex = getSpatiallyClampedContinuousIndex(dimensions, continuousIndex);
if (!clampedIndex) {
return numberOfComponents < 2
? NaN
: this.createDefaultColorSample(numberOfComponents);
}
if (numberOfComponents < 2) {
return VoxelManager.sampleAtContinuousIndex(voxelManager, dimensions, clampedIndex, interpolationType);
}
return interpolationType === InterpolationType.NEAREST
? this.sampleNearestColorAtContinuousIndex(voxelManager, dimensions, clampedIndex, numberOfComponents)
: this.sampleLinearColorAtContinuousIndex(voxelManager, dimensions, clampedIndex, numberOfComponents);
}
sampleNearestColorAtContinuousIndex(voxelManager, dimensions, continuousIndex, numberOfComponents) {
const i = Math.floor(continuousIndex[0] + 0.5 - 1e-6);
const j = Math.floor(continuousIndex[1] + 0.5 - 1e-6);
const k = Math.floor(continuousIndex[2] + 0.5 - 1e-6);
if (i < 0 ||
i >= dimensions[0] ||
j < 0 ||
j >= dimensions[1] ||
k < 0 ||
k >= dimensions[2]) {
return this.createDefaultColorSample(numberOfComponents);
}
return this.toColorSample(voxelManager.getAtIJK(i, j, k), numberOfComponents);
}
sampleLinearColorAtContinuousIndex(voxelManager, dimensions, continuousIndex, numberOfComponents) {
const [i, j, k] = continuousIndex;
if (i < 0 ||
i > dimensions[0] - 1 ||
j < 0 ||
j > dimensions[1] - 1 ||
k < 0 ||
k > dimensions[2] - 1) {
return this.createDefaultColorSample(numberOfComponents);
}
const i0 = Math.floor(i);
const j0 = Math.floor(j);
const k0 = Math.floor(k);
const i1 = Math.min(i0 + 1, dimensions[0] - 1);
const j1 = Math.min(j0 + 1, dimensions[1] - 1);
const k1 = Math.min(k0 + 1, dimensions[2] - 1);
const di = i - i0;
const dj = j - j0;
const dk = k - k0;
const oneMinusDi = 1 - di;
const oneMinusDj = 1 - dj;
const oneMinusDk = 1 - dk;
const c000 = this.toColorSample(voxelManager.getAtIJK(i0, j0, k0), numberOfComponents);
const c100 = this.toColorSample(voxelManager.getAtIJK(i1, j0, k0), numberOfComponents);
const c010 = this.toColorSample(voxelManager.getAtIJK(i0, j1, k0), numberOfComponents);
const c110 = this.toColorSample(voxelManager.getAtIJK(i1, j1, k0), numberOfComponents);
const c001 = this.toColorSample(voxelManager.getAtIJK(i0, j0, k1), numberOfComponents);
const c101 = this.toColorSample(voxelManager.getAtIJK(i1, j0, k1), numberOfComponents);
const c011 = this.toColorSample(voxelManager.getAtIJK(i0, j1, k1), numberOfComponents);
const c111 = this.toColorSample(voxelManager.getAtIJK(i1, j1, k1), numberOfComponents);
const sample = this.createDefaultColorSample(numberOfComponents);
for (let component = 0; component < numberOfComponents; component++) {
const c00 = c000[component] * oneMinusDi + c100[component] * di;
const c10 = c010[component] * oneMinusDi + c110[component] * di;
const c01 = c001[component] * oneMinusDi + c101[component] * di;
const c11 = c011[component] * oneMinusDi + c111[component] * di;
const c0 = c00 * oneMinusDj + c10 * dj;
const c1 = c01 * oneMinusDj + c11 * dj;
sample[component] = c0 * oneMinusDk + c1 * dk;
}
return sample;
}
toColorSample(voxelValue, numberOfComponents) {
if (Array.isArray(voxelValue)) {
return Array.from({ length: numberOfComponents }, (_unused, index) => Number(voxelValue[index] ?? 0));
}
const scalar = Number(voxelValue) || 0;
return Array.from({ length: numberOfComponents }, () => scalar);
}
createDefaultColorSample(numberOfComponents) {
return Array.from({ length: numberOfComponents }, () => 0);
}
writeVoxelValue(pixelData, pixelIndex, voxelValue, numberOfComponents, fallbackMin, fallbackMax, sampleRange) {
if (numberOfComponents < 2) {
const scalar = Number(voxelValue);
const preserveFloatScalarSamples = pixelData instanceof Float32Array || pixelData instanceof Float64Array;
const clampedValue = clampFiniteSample(scalar, fallbackMin, fallbackMax, !preserveFloatScalarSamples);
pixelData[pixelIndex] = clampedValue;
expandSampleValueRange(sampleRange, clampedValue);
return;
}
const color = this.toColorSample(voxelValue, numberOfComponents);
const baseIndex = pixelIndex * numberOfComponents;
for (let component = 0; component < numberOfComponents; component++) {
const value = Number(color[component]);
const clampedValue = clampFiniteSample(value, fallbackMin, fallbackMax, true);
pixelData[baseIndex + component] = clampedValue;
expandSampleValueRange(sampleRange, clampedValue);
}
}
getVolumeNumberOfComponents(volume) {
const imageDataNumberOfComponents = volume.imageData?.get('numberOfComponents');
return Math.max(1, imageDataNumberOfComponents?.numberOfComponents ??
volume.voxelManager?.numberOfComponents ??
1);
}
getSliceArrayConstructor(volume, minPixelValue, maxPixelValue, numberOfComponents, preserveFloatScalarSamples = false) {
if (numberOfComponents > 1) {
return volume.voxelManager?.getConstructor() || Uint8Array;
}
if (preserveFloatScalarSamples) {
return Float32Array;
}
if (minPixelValue >= 0 && maxPixelValue <= 65535) {
return Uint16Array;
}
if (minPixelValue >= -32768 && maxPixelValue <= 32767) {
return Int16Array;
}
return Int32Array;
}
acquireSliceArray(SliceArrayConstructor, length) {
const key = this.getSliceArrayPoolKey(SliceArrayConstructor, length);
const bucket = this.sliceArrayPool.get(key);
const reusable = bucket?.pop();
if (reusable) {
return reusable;
}
return new SliceArrayConstructor(length);
}
releaseSliceArray(scalarData) {
const SliceArrayConstructor = scalarData.constructor;
const key = this.getSliceArrayPoolKey(SliceArrayConstructor, scalarData.length);
let bucket = this.sliceArrayPool.get(key);
if (!bucket) {
bucket = [];
this.sliceArrayPool.set(key, bucket);
}
if (bucket.length < MAX_POOLED_SLICE_ARRAYS_PER_SHAPE) {
bucket.push(scalarData);
}
}
getSliceArrayPoolKey(SliceArrayConstructor, length) {
const constructorName = SliceArrayConstructor.name ||
'SliceArray';
return `${constructorName}:${length}`;
}
createSliceImage(volume, scalarData, width, height, columnPixelSpacing, rowPixelSpacing, minPixelValue, maxPixelValue, numberOfComponents, voiRange) {
const resolvedVOI = voiRange && voiRange.upper > voiRange.lower
? voiRange
: { lower: minPixelValue, upper: maxPixelValue };
const windowWidth = Math.max(1, resolvedVOI.upper - resolvedVOI.lower);
const windowCenter = (resolvedVOI.lower + resolvedVOI.upper) / 2;
const imageId = `cpuVolumeSlice:${volume.volumeId}:${++this.sampleSequence}`;
const voxelManager = VoxelManager.createImageVoxelManager({
width,
height,
scalarData,
numberOfComponents,
id: imageId,
});
return {
imageId,
intercept: 0,
windowCenter,
windowWidth,
voiLUTFunction: VOILUTFunctionType.LINEAR,
isPreScaled: volume.isPreScaled,
scaling: volume.scaling,
color: numberOfComponents > 1,
numberOfComponents,
dataType: scalarData.constructor.name,
slope: 1,
minPixelValue,
maxPixelValue,
rows: height,
columns: width,
getCanvas: undefined,
height,
width,
rgba: numberOfComponents === 4,
columnPixelSpacing,
rowPixelSpacing,
FrameOfReferenceUID: volume.metadata?.FrameOfReferenceUID,
invert: false,
photometricInterpretation: numberOfComponents > 1 ? 'RGB' : undefined,
getPixelData: () => scalarData,
voxelManager,
sizeInBytes: scalarData.byteLength,
};
}
shouldPreserveFloatScalarSamples(volume) {
return volume.metadata?.Modality === 'PT';
}
}
function resolveViewportScale(args) {
const { camera, canvas, columnPixelSpacing, rowPixelSpacing } = args;
const worldHeight = Math.max((camera.parallelScale ?? 1) * 2, EPSILON);
const worldToCanvasScale = canvas.height / worldHeight;
const scaleRatio = getPlanarScaleRatio(camera.presentationScale);
if (Math.abs(scaleRatio - 1) > EPSILON) {
const safeCanvasHeight = Math.max(canvas.height, 1);
const safeCanvasWidth = Math.max(canvas.width, 1);
const worldWidth = worldHeight * (safeCanvasWidth / safeCanvasHeight) * (1 / scaleRatio);
return [
Math.max((safeCanvasWidth * (columnPixelSpacing || 1)) /
Math.max(worldWidth, EPSILON), EPSILON),
Math.max((safeCanvasHeight * (rowPixelSpacing || 1)) / worldHeight, EPSILON),
];
}
return Math.max(Math.min(rowPixelSpacing || 1, columnPixelSpacing || 1) *
worldToCanvasScale, EPSILON);
}