@cornerstonejs/core
Version:
Cornerstone3D Core
348 lines (347 loc) • 13.4 kB
JavaScript
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;
}