UNPKG

@cornerstonejs/core

Version:
470 lines (469 loc) • 19.7 kB
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(); }