UNPKG

@cornerstonejs/core

Version:
348 lines (347 loc) • 13.4 kB
import { vec3 } from 'gl-matrix'; import { InterpolationType, OrientationAxis } from '../../../enums/index.js'; import { getImageDataMetadata } from '../../../utilities/getImageDataMetadata.js'; import { getCubeSizeInView } from '../../../utilities/getPlaneCubeIntersectionDimensions.js'; import getSpacingInNormalDirection from '../../../utilities/getSpacingInNormalDirection.js'; import { getVolumeCenterIJK } from '../../Viewport.js'; import { getCpuEquivalentParallelScale, getOrthogonalVolumeSliceGeometry, } from './planarAdapterCoordinateTransforms.js'; import { getPlanarViewStateVectors } from './planarOrientationVectors.js'; import { getSafeCanvasDimension, normalizePoint3 } from './planarMath.js'; const MIN_CAMERA_DISTANCE = 1; const MIN_SLICE_SPACING = 1e-6; const ORTHONORMAL_EPSILON = 1e-6; function getFitParallelScaleFromWorldSize(args) { const { canvasHeight, canvasWidth, heightWorld, widthWorld } = args; const safeCanvasWidth = getSafeCanvasDimension(canvasWidth); const safeCanvasHeight = getSafeCanvasDimension(canvasHeight); const safeWidthWorld = Math.max(widthWorld, MIN_SLICE_SPACING); const safeHeightWorld = Math.max(heightWorld, MIN_SLICE_SPACING); const sliceAspectRatio = safeWidthWorld / safeHeightWorld; const canvasAspectRatio = safeCanvasWidth / safeCanvasHeight; const scaleFactor = sliceAspectRatio / canvasAspectRatio; return scaleFactor < 1 ? safeHeightWorld / 2 : (safeHeightWorld * scaleFactor) / 2; } function createFallbackVolumeSliceFitMetrics() { return { fitParallelScale: MIN_CAMERA_DISTANCE, sliceWidthWorld: MIN_CAMERA_DISTANCE * 2, sliceHeightWorld: MIN_CAMERA_DISTANCE * 2, }; } function isOne(value) { return Math.abs(Math.abs(value) - 1) < ORTHONORMAL_EPSILON; } function isUnitAxis(direction, offset) { return (isOne(direction[offset]) || isOne(direction[offset + 1]) || isOne(direction[offset + 2])); } function isOrthonormalDirection(direction) { return (isUnitAxis(direction, 0) && isUnitAxis(direction, 3) && isUnitAxis(direction, 6)); } function buildPlanarImageSliceBasis(args) { const { canvasHeight, canvasWidth, columnOffset, columnsForFit, image, rowOffset, rowsForFit, } = args; const { direction, origin } = getImageDataMetadata(image); const rowVector = direction.slice(0, 3); const columnVector = direction.slice(3, 6); const viewPlaneNormal = direction .slice(6, 9) .map((value) => -value); const viewUp = columnVector.map((value) => -value); const sliceCenterWorld = [...origin]; const sliceWidthWorld = Math.max(columnsForFit, 1) * (image.columnPixelSpacing || 1); const sliceHeightWorld = Math.max(rowsForFit, 1) * (image.rowPixelSpacing || 1); vec3.scaleAndAdd(sliceCenterWorld, sliceCenterWorld, rowVector, rowOffset); vec3.scaleAndAdd(sliceCenterWorld, sliceCenterWorld, columnVector, columnOffset); return { sliceCenterWorld, viewPlaneNormal: vec3.clone(viewPlaneNormal), viewUp, fitParallelScale: getCpuEquivalentParallelScale({ canvasHeight, canvasWidth, columnPixelSpacing: image.columnPixelSpacing || 1, columns: columnsForFit, rowPixelSpacing: image.rowPixelSpacing || 1, rows: rowsForFit, }), sliceWidthWorld, sliceHeightWorld, cameraDistance: 1, }; } export function createPlanarImageSliceBasis(args) { const { image } = args; const { dimensions, spacing } = getImageDataMetadata(image); return buildPlanarImageSliceBasis({ ...args, rowOffset: ((dimensions[0] - 1) / 2) * spacing[0], columnOffset: ((dimensions[1] - 1) / 2) * spacing[1], rowsForFit: Math.max(image.rows, 1), columnsForFit: Math.max(image.columns, 1), }); } export function createPlanarCpuImageSliceBasis(args) { return createPlanarImageSliceBasis(args); } function buildImageVolumeCorners(imageVolume) { const imageData = imageVolume.imageData; if (!imageData) { return []; } const direction = imageData.getDirection(); if (isOrthonormalDirection(direction)) { const bounds = imageData.extentToBounds(imageData.getExtent()); return [ [bounds[0], bounds[2], bounds[4]], [bounds[0], bounds[2], bounds[5]], [bounds[0], bounds[3], bounds[4]], [bounds[0], bounds[3], bounds[5]], [bounds[1], bounds[2], bounds[4]], [bounds[1], bounds[2], bounds[5]], [bounds[1], bounds[3], bounds[4]], [bounds[1], bounds[3], bounds[5]], ]; } const [dx, dy, dz] = imageData.getDimensions(); const cornersIdx = [ [0, 0, 0], [dx - 1, 0, 0], [0, dy - 1, 0], [dx - 1, dy - 1, 0], [0, 0, dz - 1], [dx - 1, 0, dz - 1], [0, dy - 1, dz - 1], [dx - 1, dy - 1, dz - 1], ]; return cornersIdx.map((it) => imageData.indexToWorld(it)); } function getGeometricImageVolumeCenter(imageVolume, viewPlaneNormal) { const imageData = imageVolume.imageData; if (imageData) { const dimensions = imageData.getDimensions(); const ijk = viewPlaneNormal ? getVolumeCenterIJK(dimensions, imageData.getDirection(), viewPlaneNormal) : dimensions.map((d) => (d - 1) / 2); return imageData.indexToWorld(ijk); } const corners = buildImageVolumeCorners(imageVolume); if (!corners.length) { return [0, 0, 0]; } const center = vec3.create(); for (const corner of corners) { vec3.add(center, center, corner); } return vec3.scale(center, center, 1 / corners.length); } export function getVolumeImageIdIndexWorldPoint(imageVolume, imageIdIndex) { const imageData = imageVolume?.imageData; if (!imageData) { return; } const dimensions = imageData.getDimensions(); const flatToGroupLocal = imageVolume.flatImageIdIndexToImageIdIndex; const groupLocalIndex = typeof flatToGroupLocal === 'function' ? flatToGroupLocal.call(imageVolume, Math.max(0, imageIdIndex)) : imageIdIndex; const k = Math.min(Math.max(0, groupLocalIndex), dimensions[2] - 1); return imageData.indexToWorld([ (dimensions[0] - 1) / 2, (dimensions[1] - 1) / 2, k, ]); } function getSliceMetrics(args) { const { imageVolume, viewPlaneNormal } = args; const corners = buildImageVolumeCorners(imageVolume); const spacingInNormalDirection = Math.max(getSpacingInNormalDirection(imageVolume, viewPlaneNormal), MIN_SLICE_SPACING); if (!corners.length) { return { min: 0, max: 0, spacingInNormalDirection, maxImageIdIndex: 0, }; } const projectedValues = corners.map((corner) => vec3.dot(corner, viewPlaneNormal)); const min = Math.min(...projectedValues); const max = Math.max(...projectedValues); const maxImageIdIndex = Math.max(0, Math.round((max - min) / spacingInNormalDirection)); return { min, max, spacingInNormalDirection, maxImageIdIndex, }; } function resolveCenteredImageIdIndex(args) { const { centerProjection, min, max, maxImageIdIndex } = args; const range = max - min; if (!(range > 0) || maxImageIdIndex === 0) { return 0; } const fraction = (centerProjection - min) / range; const floatingImageIdIndex = fraction * maxImageIdIndex; return Math.round(floatingImageIdIndex); } function clampImageIdIndex(args) { const { centerProjection, imageIdIndex, max, maxImageIdIndex, min } = args; if (typeof imageIdIndex !== 'number') { return resolveCenteredImageIdIndex({ centerProjection, min, max, maxImageIdIndex, }); } return Math.min(Math.max(0, imageIdIndex), maxImageIdIndex); } export function resolvePlanarVolumeImageIdIndex(args) { const { viewState, fallbackImageIdIndex } = args; const slice = viewState?.slice; if (slice?.kind === 'volumePoint') { return; } if (slice?.kind === 'stackIndex') { return slice.imageIdIndex; } if (viewState?.orientation === OrientationAxis.ACQUISITION) { return fallbackImageIdIndex; } } function getCpuVolumeSliceFitMetrics(args) { const { imageVolume, viewPlaneNormal, viewUp, canvasWidth, canvasHeight } = args; const geometry = getOrthogonalVolumeSliceGeometry({ dimensions: imageVolume.dimensions, direction: imageVolume.direction, spacing: imageVolume.spacing, viewPlaneNormal, viewUp, }); if (geometry) { const columns = Math.max(geometry.columns, 1); const rows = Math.max(geometry.rows, 1); const sliceWidthWorld = columns * geometry.columnPixelSpacing; const sliceHeightWorld = rows * geometry.rowPixelSpacing; return { fitParallelScale: getCpuEquivalentParallelScale({ canvasHeight: getSafeCanvasDimension(canvasHeight), canvasWidth: getSafeCanvasDimension(canvasWidth), columnPixelSpacing: geometry.columnPixelSpacing, columns, rowPixelSpacing: geometry.rowPixelSpacing, rows, }), sliceWidthWorld, sliceHeightWorld, }; } const imageData = imageVolume.imageData; if (imageData) { const { widthWorld, heightWorld } = getCubeSizeInView(imageData, viewPlaneNormal, viewUp); if (widthWorld > 0 && heightWorld > 0) { return { fitParallelScale: getFitParallelScaleFromWorldSize({ canvasHeight, canvasWidth, widthWorld, heightWorld, }), sliceWidthWorld: widthWorld, sliceHeightWorld: heightWorld, }; } } return createFallbackVolumeSliceFitMetrics(); } function buildPlanarVolumeSliceBasis(args) { const { center, fitParallelScale, imageIdIndex, imageVolume, orientation, sliceHeightWorld, sliceWidthWorld, viewState, } = args; const cameraValues = getPlanarViewStateVectors({ imageVolume, orientation, }); if (!cameraValues) { return { sliceBasis: { sliceCenterWorld: [0, 0, 0], viewPlaneNormal: [0, 0, 1], viewUp: [0, -1, 0], fitParallelScale: MIN_CAMERA_DISTANCE, sliceWidthWorld: MIN_CAMERA_DISTANCE * 2, sliceHeightWorld: MIN_CAMERA_DISTANCE * 2, cameraDistance: MIN_CAMERA_DISTANCE, }, currentImageIdIndex: 0, maxImageIdIndex: 0, }; } const viewPlaneNormal = normalizePoint3(cameraValues.viewPlaneNormal); const viewUp = normalizePoint3(cameraValues.viewUp); const { max, maxImageIdIndex, min, spacingInNormalDirection } = getSliceMetrics({ imageVolume, viewPlaneNormal, }); const centerProjection = vec3.dot(center, viewPlaneNormal); const volumePoint = viewState?.slice?.kind === 'volumePoint' ? viewState.slice.sliceWorldPoint : undefined; const requestedSliceProjection = Math.min(max, Math.max(min, volumePoint ? vec3.dot(volumePoint, viewPlaneNormal) : centerProjection)); const currentImageIdIndex = clampImageIdIndex({ imageIdIndex, centerProjection: requestedSliceProjection, min, max, maxImageIdIndex, }); const targetProjection = typeof imageIdIndex === 'number' ? Math.min(max, min + currentImageIdIndex * spacingInNormalDirection) : requestedSliceProjection; const scalarOffset = targetProjection - centerProjection; const sliceCenterWorld = vec3.scaleAndAdd(vec3.create(), center, viewPlaneNormal, scalarOffset); const cameraDistance = Math.max(max - min, spacingInNormalDirection, 1); return { sliceBasis: { sliceCenterWorld, viewPlaneNormal, viewUp, fitParallelScale, sliceWidthWorld, sliceHeightWorld, cameraDistance, }, currentImageIdIndex, maxImageIdIndex, }; } export function createPlanarVolumeSliceBasis(args) { const { imageVolume, orientation } = args; const cameraValues = getPlanarViewStateVectors({ imageVolume, orientation, }); const sliceFitMetrics = cameraValues ? getCpuVolumeSliceFitMetrics({ ...args, viewPlaneNormal: cameraValues.viewPlaneNormal, viewUp: cameraValues.viewUp, }) : createFallbackVolumeSliceFitMetrics(); return buildPlanarVolumeSliceBasis({ ...args, center: getGeometricImageVolumeCenter(imageVolume, cameraValues?.viewPlaneNormal), ...sliceFitMetrics, }); } export function createPlanarCpuVolumeSliceBasis(args) { return createPlanarVolumeSliceBasis(args); } export function shouldUsePlanarCpuVolumeSliceBasis(interpolationType = InterpolationType.LINEAR) { return interpolationType === InterpolationType.NEAREST; }