UNPKG

@cornerstonejs/core

Version:
687 lines (686 loc) • 32.4 kB
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); }