@cornerstonejs/core
Version:
Cornerstone3D Core
470 lines (469 loc) • 19.7 kB
JavaScript
import { vec3 } from 'gl-matrix';
import { buildPlanarActorEntry } from './buildPlanarActorEntry.js';
import CanvasActor from '../../CanvasActor/index.js';
import calculateTransform from '../../helpers/cpuFallback/rendering/calculateTransform.js';
import canvasToPixel from '../../helpers/cpuFallback/rendering/canvasToPixel.js';
import correctShift from '../../helpers/cpuFallback/rendering/correctShift.js';
import getDefaultViewport from '../../helpers/cpuFallback/rendering/getDefaultViewport.js';
import { getDefaultImageVOIRange } from '../../helpers/planarImageRendering.js';
import resizeEnabledElement from '../../helpers/cpuFallback/rendering/resize.js';
import drawImageSync from '../../helpers/cpuFallback/drawImageSync.js';
import { resolveCPUFallbackColormap } from '../../helpers/cpuFallback/colors/index.js';
import { InterpolationType, MetadataModules, ViewportType, Events, ViewportStatus, } from '../../../enums/index.js';
import { loadAndCacheImage } from '../../../loaders/imageLoader.js';
import * as metaData from '../../../metaData.js';
import { ActorRenderMode } from '../../../types/index.js';
import { getImageDataMetadata } from '../../../utilities/getImageDataMetadata.js';
import triggerEvent from '../../../utilities/triggerEvent.js';
import uuidv4 from '../../../utilities/uuidv4.js';
import { toWindowLevel } from '../../../utilities/windowLevel.js';
import { resolvePlanarCpuImageDisplayedArea, resolvePlanarCpuViewportScale, } from './planarCpuViewportMath.js';
import { triggerPlanarNewImage } from './planarImageEvents.js';
import { canvasToWorldPlanarRenderPathProjection, resolvePlanarRenderPathCurrentImageIdIndex, resolvePlanarRenderPathProjection, worldToCanvasPlanarRenderPathProjection, } from './planarRenderPathProjection.js';
export class CpuImageSliceRenderPath {
async addData(ctx, data, options) {
const payload = data;
if (!payload.image) {
throw new Error('[PlanarViewport] CPU rendering requires an image');
}
ctx.display.activateRenderMode(ActorRenderMode.CPU_IMAGE);
let rendering;
markCpuImagePreScaled(payload.image);
const compatibilityActor = new CanvasActor({
getImageData: () => this.getImageData(rendering),
}, payload.image);
compatibilityActor.setVisibility(true);
const defaultViewport = getDefaultViewport(ctx.cpu.canvas, payload.image, getCPUFallbackViewportModality(payload.image));
defaultViewport.displayedArea = resolvePlanarCpuImageDisplayedArea(payload.image);
const enabledElement = {
canvas: ctx.cpu.canvas,
image: payload.image,
renderingTools: {},
viewport: defaultViewport,
};
resizeEnabledElement(enabledElement, true);
enabledElement.transform = calculateTransform(enabledElement);
rendering = {
renderMode: ActorRenderMode.CPU_IMAGE,
actorEntryUID: uuidv4(),
enabledElement,
compatibilityActor,
currentImageIdIndex: payload.initialImageIdIndex ?? 0,
defaultVOIRange: getDefaultImageVOIRange(payload.image),
dataPresentation: undefined,
fitScale: getCPUFallbackScalarScale(enabledElement.viewport.scale),
loadRequestId: 0,
renderingInvalidated: true,
};
triggerPlanarNewImage(ctx, {
image: payload.image,
imageIdIndex: payload.initialImageIdIndex ?? 0,
});
return {
rendering,
updateDataPresentation: (props) => {
this.updateDataPresentation(rendering, props);
},
applyViewState: (camera) => {
this.applyViewState(ctx, rendering, data.id, camera, payload.imageIds);
},
getFrameOfReferenceUID: () => {
return this.getFrameOfReferenceUID(rendering);
},
getActorEntry: (data) => {
return buildPlanarActorEntry(data, {
actor: rendering.compatibilityActor,
mapper: rendering.compatibilityActor.getMapper(),
renderMode: ActorRenderMode.CPU_IMAGE,
uid: rendering.actorEntryUID,
referencedIdFallback: rendering.enabledElement.image?.imageId,
});
},
getImageData: () => {
return this.getImageData(rendering);
},
render: () => {
this.render(ctx, rendering, data.id);
},
resize: () => {
this.resize(ctx, rendering, data.id);
},
removeData: () => {
this.removeData(ctx, rendering);
},
};
}
updateDataPresentation(rendering, props) {
rendering.dataPresentation = props;
applyDataPresentation(rendering, rendering.dataPresentation);
}
applyViewState(ctx, rendering, dataId, camera, imageIds) {
const planarCamera = camera;
const image = rendering.enabledElement.image;
ctx.display.activateRenderMode(ActorRenderMode.CPU_IMAGE);
const projection = image
? resolvePlanarRenderPathProjection({
ctx,
dataId,
imageIds,
rendering,
viewState: planarCamera,
})
: undefined;
const nextImageIdIndex = resolvePlanarRenderPathCurrentImageIdIndex({
projection,
rendering,
viewState: planarCamera,
});
if (projection) {
applyPresentationState(rendering, projection.presentation, projection.resolvedICamera);
}
const dedupTarget = rendering.lastRequestedImageIdIndex ?? rendering.currentImageIdIndex;
if (nextImageIdIndex === dedupTarget) {
return;
}
if (nextImageIdIndex < 0 || nextImageIdIndex >= imageIds.length) {
return;
}
rendering.lastRequestedImageIdIndex = nextImageIdIndex;
const requestId = ++rendering.loadRequestId;
void loadAndCacheImage(imageIds[nextImageIdIndex]).then((image) => {
if (requestId !== rendering.loadRequestId) {
return;
}
void updateRenderedImage({
ctx,
dataId,
image,
imageIdIndex: nextImageIdIndex,
props: rendering.dataPresentation,
rendering,
});
});
}
canvasToWorld(ctx, rendering, canvasPos) {
return canvasToWorldPlanarRenderPathProjection({
canvas: ctx.cpu.canvas,
canvasPos,
ctx,
});
}
worldToCanvas(ctx, rendering, worldPos) {
return worldToCanvasPlanarRenderPathProjection({
canvas: ctx.cpu.canvas,
ctx,
worldPos,
});
}
getFrameOfReferenceUID(rendering) {
const imageId = rendering.enabledElement.image?.imageId;
const imagePlaneModule = imageId
? metaData.get(MetadataModules.IMAGE_PLANE, imageId)
: undefined;
return imagePlaneModule?.frameOfReferenceUID;
}
getImageData(rendering) {
const image = rendering.enabledElement.image;
if (!image) {
return;
}
return buildPlanarImageData(image, this.getFrameOfReferenceUID(rendering));
}
render(ctx, rendering, dataId) {
if (!ctx.viewport.isCurrentDataId(dataId)) {
return;
}
renderCPUImage(rendering);
renderCompatibilityOverlayActors(ctx);
ctx.display.markRendered();
triggerEvent(ctx.viewport.element, Events.IMAGE_RENDERED, {
element: ctx.viewport.element,
viewportId: ctx.viewportId,
renderingEngineId: ctx.renderingEngineId,
viewportStatus: ViewportStatus.RENDERED,
});
}
resize(ctx, rendering, dataId) {
resizeEnabledElement(rendering.enabledElement, true);
const image = rendering.enabledElement.image;
const camera = ctx.viewport.getViewState();
if (!image) {
return;
}
const projection = resolvePlanarRenderPathProjection({
ctx,
dataId,
rendering,
viewState: camera,
});
rendering.fitScale = getCPUFallbackScalarScale(getDefaultViewport(rendering.enabledElement.canvas, image, getCPUFallbackViewportModality(image)).scale);
rendering.renderingInvalidated = true;
if (projection) {
applyPresentationState(rendering, projection.presentation, projection.resolvedICamera);
}
}
removeData(ctx, rendering) {
const { enabledElement } = rendering;
ctx.cpu.context.setTransform(1, 0, 0, 1, 0, 0);
ctx.cpu.context.clearRect(0, 0, ctx.cpu.canvas.width, ctx.cpu.canvas.height);
enabledElement.image = undefined;
}
}
export class CpuImageSlicePath {
constructor() {
this.id = 'planar:cpu-image-slice';
this.type = ViewportType.PLANAR_NEXT;
}
matches(data, options) {
return (data.type === 'image' && options.renderMode === ActorRenderMode.CPU_IMAGE);
}
createRenderPath() {
return new CpuImageSliceRenderPath();
}
selectContext(rootContext) {
return {
viewportId: rootContext.viewportId,
renderingEngineId: rootContext.renderingEngineId,
type: rootContext.type,
viewport: rootContext.viewport,
renderPath: rootContext.renderPath,
view: rootContext.view,
display: rootContext.display,
cpu: rootContext.cpu,
};
}
}
function applyDataPresentation(rendering, props) {
const { enabledElement, defaultVOIRange } = rendering;
const { viewport } = enabledElement;
const canvas = enabledElement.canvas;
const voiRange = props?.voiRange ?? defaultVOIRange;
canvas.style.display = props?.visible === false ? 'none' : '';
canvas.style.opacity = String(props?.opacity ?? 1);
viewport.colormap = resolveCPUFallbackColormap(props?.colormap, enabledElement.image?.colormap);
viewport.invert = props?.invert ?? false;
if (voiRange) {
const { windowCenter, windowWidth } = toWindowLevel(voiRange.lower, voiRange.upper);
viewport.voi = {
windowCenter,
windowWidth,
voiLUTFunction: enabledElement.image?.voiLUTFunction,
};
}
rendering.renderingInvalidated = true;
if (props?.interpolationType !== undefined) {
viewport.pixelReplication =
props.interpolationType !== InterpolationType.LINEAR;
}
}
function getCPUFallbackScalarScale(scale) {
return Array.isArray(scale) ? scale[1] : (scale ?? 1);
}
function applyPresentationState(rendering, presentation, activeSourceICamera) {
const { enabledElement, fitScale } = rendering;
const viewport = enabledElement.viewport;
const desiredPan = presentation?.pan ?? [0, 0];
const zoom = Math.max(presentation?.zoom ?? 1, 0.001);
viewport.hflip = presentation?.flipHorizontal ?? false;
viewport.vflip = presentation?.flipVertical ?? false;
viewport.rotation = presentation?.rotation ?? 0;
viewport.scale =
typeof activeSourceICamera?.parallelScale === 'number'
? resolvePlanarCpuViewportScale({
canvas: enabledElement.canvas,
parallelScale: activeSourceICamera.parallelScale,
presentationScale: activeSourceICamera.presentationScale,
columnPixelSpacing: enabledElement.image?.columnPixelSpacing || 1,
rowPixelSpacing: enabledElement.image?.rowPixelSpacing || 1,
})
: [fitScale * (presentation?.scale?.[0] ?? zoom), fitScale * zoom];
viewport.parallelScale = activeSourceICamera?.parallelScale;
viewport.translation = activeSourceICamera?.focalPoint
? resolveCPUImageViewportTranslationFromFocalPoint(enabledElement, activeSourceICamera.focalPoint)
: resolveCPUImageViewportTranslation(enabledElement, desiredPan);
enabledElement.transform = calculateTransform(enabledElement);
}
function resolveCPUImageViewportTranslationFromFocalPoint(enabledElement, focalPoint) {
const { image, viewport } = enabledElement;
const originalTranslation = viewport.translation || { x: 0, y: 0 };
if (!image) {
return originalTranslation;
}
viewport.translation = { x: 0, y: 0 };
enabledElement.transform = calculateTransform(enabledElement);
const referencePoint = canvasToPixel(enabledElement, [
enabledElement.canvas.width / 2,
enabledElement.canvas.height / 2,
]);
const targetPoint = worldToCPUImageDrawPoint(enabledElement, focalPoint);
const shift = correctShift({
x: targetPoint[0] - referencePoint[0],
y: targetPoint[1] - referencePoint[1],
}, viewport);
viewport.translation = originalTranslation;
return {
x: -shift.x,
y: -shift.y,
};
}
function resolveCPUImageViewportTranslation(enabledElement, desiredPan) {
const { viewport } = enabledElement;
const originalTranslation = viewport.translation || { x: 0, y: 0 };
viewport.translation = { x: 0, y: 0 };
const baseOrigin = calculateTransform(enabledElement).transformPoint([0, 0]);
viewport.translation = { x: 1, y: 0 };
const translatedXOrigin = calculateTransform(enabledElement).transformPoint([
0, 0,
]);
viewport.translation = { x: 0, y: 1 };
const translatedYOrigin = calculateTransform(enabledElement).transformPoint([
0, 0,
]);
viewport.translation = originalTranslation;
const deltaX = [
translatedXOrigin[0] - baseOrigin[0],
translatedXOrigin[1] - baseOrigin[1],
];
const deltaY = [
translatedYOrigin[0] - baseOrigin[0],
translatedYOrigin[1] - baseOrigin[1],
];
const determinant = deltaX[0] * deltaY[1] - deltaX[1] * deltaY[0];
if (Math.abs(determinant) < 1e-6) {
return { x: 0, y: 0 };
}
return {
x: (desiredPan[0] * deltaY[1] - desiredPan[1] * deltaY[0]) / determinant,
y: (deltaX[0] * desiredPan[1] - deltaX[1] * desiredPan[0]) / determinant,
};
}
function worldToCPUImageDrawPoint(enabledElement, worldPos) {
const { image, viewport } = enabledElement;
if (!image) {
return [0, 0];
}
const { spacing, direction, origin } = getImageDataMetadata(image);
const rowVector = direction.slice(0, 3);
const columnVector = direction.slice(3, 6);
const displayedArea = viewport.displayedArea;
const sourceX = (displayedArea?.tlhc.x ?? 1) - 1;
const sourceY = (displayedArea?.tlhc.y ?? 1) - 1;
const diff = vec3.subtract(vec3.create(), worldPos, origin);
return [
vec3.dot(diff, rowVector) / spacing[0] + 0.5 - sourceX,
vec3.dot(diff, columnVector) / spacing[1] +
0.5 -
sourceY,
];
}
function markCpuImagePreScaled(image) {
image.isPreScaled = image.preScale?.scaled;
}
function getCPUFallbackViewportModality(image) {
return (image.preScale?.scalingParameters?.modality ||
metaData.get(MetadataModules.GENERAL_SERIES, image.imageId)?.modality);
}
export function buildPlanarImageData(image, frameOfReferenceUID) {
const metadata = getImageDataMetadata(image);
const { dimensions, direction, modality, origin, spacing } = metadata;
const userCalibration = metaData.get('calibratedPixelSpacing', image.imageId);
const calibration = userCalibration
? { ...metadata.calibration, ...userCalibration }
: metadata.calibration;
const rowVector = direction.slice(0, 3);
const columnVector = direction.slice(3, 6);
const scalarData = image.voxelManager?.getScalarData() || image.getPixelData?.();
return {
dimensions,
spacing,
origin,
direction,
metadata: {
Modality: modality,
FrameOfReferenceUID: frameOfReferenceUID,
},
imageData: {
getDirection: () => direction,
getDimensions: () => dimensions,
getScalarData: () => scalarData,
getSpacing: () => spacing,
worldToIndex: (point) => {
const diff = vec3.subtract(vec3.create(), point, origin);
return [
vec3.dot(diff, rowVector) / spacing[0],
vec3.dot(diff, columnVector) / spacing[1],
0,
];
},
indexToWorld: (point) => {
const worldPoint = [...origin];
vec3.scaleAndAdd(worldPoint, worldPoint, rowVector, point[0] * spacing[0]);
vec3.scaleAndAdd(worldPoint, worldPoint, columnVector, point[1] * spacing[1]);
return worldPoint;
},
},
scalarData,
scaling: image.scaling,
hasPixelSpacing: Boolean(image.rowPixelSpacing || image.columnPixelSpacing),
calibration,
preScale: image.preScale,
voxelManager: image.voxelManager,
};
}
function renderCPUImage(rendering) {
const { enabledElement, renderingInvalidated } = rendering;
if (!enabledElement.image) {
return;
}
drawImageSync(enabledElement, renderingInvalidated);
rendering.renderingInvalidated = false;
}
function renderCompatibilityOverlayActors(ctx) {
const overlayActors = ctx.viewport.getOverlayActors();
for (const actorEntry of overlayActors) {
if (actorEntry.actorMapper?.renderMode !== ActorRenderMode.CPU_IMAGE) {
continue;
}
actorEntry.actor.render(undefined, ctx.cpu.context);
}
}
async function updateRenderedImage(args) {
const { ctx, dataId, image, imageIdIndex, props, rendering } = args;
const enabledElement = rendering.enabledElement;
markCpuImagePreScaled(image);
const camera = ctx.viewport.getViewState();
const defaultViewport = getDefaultViewport(ctx.cpu.canvas, image, getCPUFallbackViewportModality(image));
const previousViewport = enabledElement.viewport;
defaultViewport.displayedArea = resolvePlanarCpuImageDisplayedArea(image);
enabledElement.image = image;
enabledElement.viewport = {
...defaultViewport,
hflip: previousViewport?.hflip ?? defaultViewport.hflip,
invert: defaultViewport.invert,
pixelReplication: previousViewport?.pixelReplication ?? defaultViewport.pixelReplication,
rotation: previousViewport?.rotation ?? defaultViewport.rotation,
translation: previousViewport?.translation ?? defaultViewport.translation,
vflip: previousViewport?.vflip ?? defaultViewport.vflip,
};
rendering.currentImageIdIndex = imageIdIndex;
rendering.defaultVOIRange = getDefaultImageVOIRange(image);
rendering.fitScale = getCPUFallbackScalarScale(defaultViewport.scale);
rendering.renderingInvalidated = true;
ctx.viewport.invalidateResolvedView();
rendering.compatibilityActor
.getMapper()
.getInputData()
.setDerivedImage(image);
applyDataPresentation(rendering, props);
const projection = resolvePlanarRenderPathProjection({
ctx,
dataId,
rendering,
viewState: camera,
});
if (projection) {
applyPresentationState(rendering, projection.presentation, projection.resolvedICamera);
}
triggerPlanarNewImage(ctx, { image, imageIdIndex });
ctx.display.renderNow();
}