UNPKG

@cornerstonejs/core

Version:
517 lines (516 loc) • 22 kB
import { buildPlanarActorEntry } from './buildPlanarActorEntry.js'; import CanvasActor from '../../CanvasActor/index.js'; import { Events, ViewportStatus, ViewportType } from '../../../enums/index.js'; import eventTarget from '../../../eventTarget.js'; import { ActorRenderMode } from '../../../types/index.js'; import triggerEvent from '../../../utilities/triggerEvent.js'; import { createCanvas } from '../../helpers/getOrCreateCanvas.js'; import drawImageSync from '../../helpers/cpuFallback/drawImageSync.js'; import setToPixelCoordinateSystem from '../../helpers/cpuFallback/rendering/setToPixelCoordinateSystem.js'; import { getDefaultVolumeVOIRange } from '../../helpers/setDefaultVolumeVOI.js'; import { getConfiguration } from '../../../init.js'; import uuidv4 from '../../../utilities/uuidv4.js'; import PlanarCPUVolumeSampler from './PlanarCPUVolumeSampler.js'; import { triggerPlanarVolumeNewImage } from './planarImageEvents.js'; import { canvasToWorldPlanarRenderPathProjection, getPlanarRenderPathActiveSourceICamera, resolvePlanarRenderPathProjection, worldToCanvasPlanarRenderPathProjection, } from './planarRenderPathProjection.js'; const DEFERRED_VIEWPORT_RESAMPLE_DELAY_MS = 80; export class CpuVolumeSliceRenderPath { constructor() { this.sampler = new PlanarCPUVolumeSampler(); } async addData(ctx, data, _options) { const payload = data; if (!payload.imageVolume) { throw new Error('[PlanarViewport] CPU volume rendering requires a prepared image volume'); } const defaultVOIRange = await getDefaultVolumeVOIRange(payload.imageVolume); let rendering; const compatibilityActor = new CanvasActor({ getImageData: () => this.getImageData(rendering), }, payload.imageVolume.imageIds?.length ? { imageId: payload.imageVolume.imageIds[0] } : {}); compatibilityActor.setVisibility(true); ctx.display.activateRenderMode(ActorRenderMode.CPU_VOLUME); rendering = { renderMode: ActorRenderMode.CPU_VOLUME, actorEntryUID: uuidv4(), compatibilityActor, imageVolume: payload.imageVolume, imageIds: payload.imageIds, acquisitionOrientation: payload.acquisitionOrientation, layerCanvas: createCanvas(null, ctx.cpu.canvas.width, ctx.cpu.canvas.height), currentImageIdIndex: payload.initialImageIdIndex ?? 0, maxImageIdIndex: payload.imageIds.length - 1, defaultVOIRange, renderingInvalidated: true, compositeActor: payload.reference?.kind === 'segmentation', removeStreamingSubscriptions: (() => { let isActive = true; let pendingAnimationFrameId; let pendingVolumeModifiedTimeoutId; let lastVolumeModifiedRenderTime = -Infinity; const volumeModifiedThrottleMs = getCpuVolumeModifiedThrottleMs(); const renderVolumeModified = () => { if (!isActive) { return; } lastVolumeModifiedRenderTime = performance.now(); ctx.display.renderNow(); }; const scheduleVolumeModifiedRender = () => { if (volumeModifiedThrottleMs <= 0) { renderVolumeModified(); return; } if (pendingVolumeModifiedTimeoutId !== undefined) { return; } const remainingDelay = Math.max(0, volumeModifiedThrottleMs - (performance.now() - lastVolumeModifiedRenderTime)); if (remainingDelay === 0) { renderVolumeModified(); return; } pendingVolumeModifiedTimeoutId = window.setTimeout(() => { pendingVolumeModifiedTimeoutId = undefined; renderVolumeModified(); }, remainingDelay); }; const unsubscribe = subscribeToVolumeEvents(payload.volumeId, (eventType) => { if (!isActive) { return; } const shouldResampleOnDeferredPass = eventType === Events.IMAGE_VOLUME_LOADING_COMPLETED; const voxelManager = payload.imageVolume.voxelManager; voxelManager?.invalidateCache?.(); if (voxelManager) { this.sampler.clearCachedScalarRange(voxelManager); } cancelDeferredViewportResample(rendering); rendering.pendingVolumeLoadCallback = false; this.releaseSampledSliceState(rendering); rendering.renderingInvalidated = true; if (eventType === Events.IMAGE_VOLUME_MODIFIED) { scheduleVolumeModifiedRender(); } else { if (pendingVolumeModifiedTimeoutId !== undefined) { window.clearTimeout(pendingVolumeModifiedTimeoutId); pendingVolumeModifiedTimeoutId = undefined; } ctx.display.renderNow(); } if (!isActive) { return; } if (!shouldResampleOnDeferredPass) { return; } ctx.display.setNeedsRender(); pendingAnimationFrameId = window.requestAnimationFrame(() => { pendingAnimationFrameId = undefined; if (!isActive) { return; } if (shouldResampleOnDeferredPass) { voxelManager?.invalidateCache?.(); if (voxelManager) { this.sampler.clearCachedScalarRange(voxelManager); } this.releaseSampledSliceState(rendering); rendering.renderingInvalidated = true; } ctx.display.renderNow(); }); }); return () => { isActive = false; rendering.pendingVolumeLoadCallback = false; this.releaseSampledSliceState(rendering); if (pendingAnimationFrameId !== undefined) { window.cancelAnimationFrame(pendingAnimationFrameId); pendingAnimationFrameId = undefined; } if (pendingVolumeModifiedTimeoutId !== undefined) { window.clearTimeout(pendingVolumeModifiedTimeoutId); pendingVolumeModifiedTimeoutId = undefined; } unsubscribe(); }; })(), }; triggerPlanarVolumeNewImage(ctx, { camera: ctx.viewport.getViewState(), acquisitionOrientation: rendering.acquisitionOrientation, imageIds: rendering.imageIds, imageIdIndex: rendering.currentImageIdIndex, maxImageIdIndex: rendering.maxImageIdIndex, }); return { rendering, updateDataPresentation: (props) => { this.updateDataPresentation(ctx, rendering, data.id, props); }, applyViewState: (camera) => { this.applyViewState(ctx, rendering, data.id, camera); }, getFrameOfReferenceUID: () => { return this.getFrameOfReferenceUID(rendering); }, getActorEntry: (data) => { const planarData = data; return buildPlanarActorEntry(planarData, { actor: rendering.compatibilityActor, renderMode: ActorRenderMode.CPU_VOLUME, uid: rendering.actorEntryUID, referencedIdFallback: planarData.volumeId, }); }, getImageData: () => { return this.getImageData(rendering); }, render: () => { this.render(ctx, rendering); }, resize: () => { this.resize(ctx, rendering, data.id); }, removeData: () => { this.removeData(ctx, rendering); }, }; } updateDataPresentation(ctx, rendering, dataId, props) { const previousInterpolationType = rendering.dataPresentation?.interpolationType; const { slabThickness: _slabThickness, ...dataPresentation } = props || {}; rendering.dataPresentation = Object.keys(dataPresentation).length ? dataPresentation : undefined; if (previousInterpolationType !== rendering.dataPresentation?.interpolationType) { this.syncRenderCamera(ctx, rendering, dataId, ctx.viewport.getViewState()); return; } rendering.renderingInvalidated = true; } applyViewState(ctx, rendering, dataId, cameraInput) { ctx.display.activateRenderMode(ActorRenderMode.CPU_VOLUME); this.syncRenderCamera(ctx, rendering, dataId, cameraInput); } 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) { return rendering.imageVolume.metadata?.FrameOfReferenceUID; } getImageData(rendering) { return buildPlanarVolumeImageData(rendering.imageVolume); } render(ctx, rendering) { const runtime = rendering; ctx.display.activateRenderMode(ActorRenderMode.CPU_VOLUME); ctx.cpu.canvas.style.display = ''; ctx.cpu.canvas.style.opacity = '1'; if (runtime.dataPresentation?.visible === false) { beginCompositePass(ctx); return; } const loadStatus = runtime.imageVolume.loadStatus; const hasStreamedFrameData = hasStreamedVolumeData(runtime.imageVolume); const isVolumeReady = loadStatus === undefined || loadStatus.loaded === true || hasStreamedFrameData; if (!isVolumeReady) { if (!runtime.pendingVolumeLoadCallback) { runtime.pendingVolumeLoadCallback = true; runtime.imageVolume.load(); } ctx.display.setNeedsRender(); return; } runtime.pendingVolumeLoadCallback = false; if (!ctx.cpu.canvas.width || !ctx.cpu.canvas.height) { return; } const activeSourceICamera = getPlanarRenderPathActiveSourceICamera(ctx); if (!activeSourceICamera) { return; } beginCompositePass(ctx); const layerCanvasWasResized = syncLayerCanvasSize(runtime.layerCanvas, ctx.cpu.canvas); if (layerCanvasWasResized) { runtime.renderingInvalidated = true; } const useViewportSamplingForLinear = getUseViewportSamplingForLinearCPUVolume(); const resampleDecision = runtime.forceHighQualityResample ? 'resample' : this.sampler.getResampleDecision({ sampledSliceState: runtime.sampledSliceState, width: ctx.cpu.canvas.width, height: ctx.cpu.canvas.height, camera: activeSourceICamera, dataPresentation: runtime.dataPresentation, deferViewportResample: useViewportSamplingForLinear, }); const shouldResample = resampleDecision === 'resample'; if (resampleDecision === 'defer') { scheduleDeferredViewportResample(ctx, runtime); } else { cancelDeferredViewportResample(runtime); } let sampledSliceState = runtime.sampledSliceState; if (shouldResample) { this.releaseSampledSliceState(runtime); runtime.forceHighQualityResample = false; sampledSliceState = runtime.sampledSliceState = this.sampler.sampleSliceImage({ volume: runtime.imageVolume, width: ctx.cpu.canvas.width, height: ctx.cpu.canvas.height, camera: activeSourceICamera, dataPresentation: runtime.dataPresentation, useViewportSamplingForLinear, }); } if (!sampledSliceState) { return; } if (shouldResample) { runtime.renderingInvalidated = true; } runtime.enabledElement = this.sampler.createOrUpdateEnabledElement({ enabledElement: runtime.enabledElement, canvas: runtime.layerCanvas, image: sampledSliceState.image, modality: runtime.imageVolume.metadata?.Modality, }); this.sampler.updateCPUFallbackViewport({ enabledElement: runtime.enabledElement, sampledSliceState, camera: activeSourceICamera, dataPresentation: runtime.dataPresentation, defaultVOIRange: runtime.defaultVOIRange, }); runtime.defaultVOIRange ||= this.sampler.getResolvedVOIRange(runtime.dataPresentation?.voiRange, sampledSliceState.image.minPixelValue ?? 0, sampledSliceState.image.maxPixelValue ?? 1); if (shouldResample) { runtime.compatibilityActor .getMapper() .getInputData() .setDerivedImage(sampledSliceState.image); } if (runtime.compositeActor) { renderCanvasActorLayer(runtime); } else { drawImageSync(runtime.enabledElement, runtime.renderingInvalidated); } runtime.renderingInvalidated = false; compositeLayerCanvas(ctx, runtime); 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) { this.syncRenderCamera(ctx, rendering, dataId, ctx.viewport.getViewState()); } removeData(ctx, rendering) { const { removeStreamingSubscriptions } = rendering; cancelDeferredViewportResample(rendering); removeStreamingSubscriptions?.(); this.releaseSampledSliceState(rendering); this.sampler.clearSliceArrayPool?.(); } syncRenderCamera(ctx, rendering, dataId, camera) { const projection = resolvePlanarRenderPathProjection({ ctx, dataId, rendering, viewState: camera, }); if (!projection) { return; } const { currentImageIdIndex, maxImageIdIndex } = projection; const imageIdIndexChanged = currentImageIdIndex !== rendering.currentImageIdIndex; const maxImageIdIndexChanged = maxImageIdIndex !== rendering.maxImageIdIndex; rendering.currentImageIdIndex = currentImageIdIndex; rendering.maxImageIdIndex = maxImageIdIndex; rendering.renderingInvalidated = true; if (imageIdIndexChanged || maxImageIdIndexChanged) { ctx.viewport.invalidateResolvedView(); } if (imageIdIndexChanged) { triggerPlanarVolumeNewImage(ctx, { camera, acquisitionOrientation: rendering.acquisitionOrientation, imageIds: rendering.imageIds, imageIdIndex: rendering.currentImageIdIndex, maxImageIdIndex: rendering.maxImageIdIndex, }); } } releaseSampledSliceState(rendering) { this.sampler.releaseSampledSliceState?.(rendering.sampledSliceState); rendering.sampledSliceState = undefined; } } function subscribeToVolumeEvents(volumeId, onUpdate) { const handleUpdate = (evt) => { const detail = evt.detail; if (detail?.volumeId !== volumeId) { return; } onUpdate(evt.type); }; eventTarget.addEventListener(Events.IMAGE_VOLUME_MODIFIED, handleUpdate); eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, handleUpdate); return () => { eventTarget.removeEventListener(Events.IMAGE_VOLUME_MODIFIED, handleUpdate); eventTarget.removeEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, handleUpdate); }; } function hasStreamedVolumeData(imageVolume) { const streamingState = imageVolume; return Boolean((streamingState.framesLoaded ?? 0) > 0 || (streamingState.framesProcessed ?? 0) > 0 || (streamingState.framesUpdated ?? 0) > 0); } function getCpuVolumeModifiedThrottleMs() { const throttleMs = getConfiguration().rendering?.planar?.cpuVolume?.volumeModifiedThrottleMs; if (!Number.isFinite(throttleMs)) { return 0; } return Math.max(0, Math.trunc(throttleMs)); } function getUseViewportSamplingForLinearCPUVolume() { return (getConfiguration().rendering?.planar?.cpuVolume ?.useViewportSamplingForLinear === true); } function scheduleDeferredViewportResample(ctx, rendering) { cancelDeferredViewportResample(rendering); ctx.display.setNeedsRender(); rendering.deferredResampleTimeoutId = window.setTimeout(() => { rendering.deferredResampleTimeoutId = undefined; rendering.forceHighQualityResample = true; rendering.renderingInvalidated = true; ctx.display.renderNow(); }, DEFERRED_VIEWPORT_RESAMPLE_DELAY_MS); } function cancelDeferredViewportResample(rendering) { if (rendering.deferredResampleTimeoutId === undefined) { return; } window.clearTimeout(rendering.deferredResampleTimeoutId); rendering.deferredResampleTimeoutId = undefined; } export class CpuVolumeSlicePath { constructor() { this.id = 'planar:cpu-volume-slice'; this.type = ViewportType.PLANAR_NEXT; } matches(data, options) { return (data.type === 'image' && options.renderMode === ActorRenderMode.CPU_VOLUME); } createRenderPath() { return new CpuVolumeSliceRenderPath(); } 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 renderCanvasActorLayer(rendering) { const enabledElement = rendering.enabledElement; if (!enabledElement) { return; } const context = rendering.layerCanvas.getContext('2d'); if (!context) { return; } context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, rendering.layerCanvas.width, rendering.layerCanvas.height); context.imageSmoothingEnabled = !enabledElement.viewport.pixelReplication; setToPixelCoordinateSystem(enabledElement, context); rendering.compatibilityActor.render(undefined, context); } function clearToBackground(ctx) { ctx.cpu.context.setTransform(1, 0, 0, 1, 0, 0); ctx.cpu.context.clearRect(0, 0, ctx.cpu.canvas.width, ctx.cpu.canvas.height); ctx.cpu.context.fillStyle = '#000'; ctx.cpu.context.fillRect(0, 0, ctx.cpu.canvas.width, ctx.cpu.canvas.height); } function beginCompositePass(ctx) { if (ctx.cpu.composition.clearedRenderPassId === ctx.cpu.composition.renderPassId) { return; } clearToBackground(ctx); ctx.cpu.composition.clearedRenderPassId = ctx.cpu.composition.renderPassId; } function syncLayerCanvasSize(layerCanvas, sharedCanvas) { if (layerCanvas.width === sharedCanvas.width && layerCanvas.height === sharedCanvas.height) { return false; } layerCanvas.width = sharedCanvas.width; layerCanvas.height = sharedCanvas.height; return true; } function compositeLayerCanvas(ctx, rendering) { const compositeOpacity = rendering.dataPresentation?.opacity ?? 1; if (compositeOpacity <= 0) { return; } ctx.cpu.context.save(); ctx.cpu.context.globalAlpha = compositeOpacity; ctx.cpu.context.drawImage(rendering.layerCanvas, 0, 0); ctx.cpu.context.restore(); } function buildPlanarVolumeImageData(imageVolume) { const vtkImageData = imageVolume.imageData; if (!vtkImageData) { return; } return { dimensions: vtkImageData.getDimensions(), spacing: vtkImageData.getSpacing(), origin: vtkImageData.getOrigin(), direction: vtkImageData.getDirection(), imageData: vtkImageData, metadata: { Modality: imageVolume.metadata?.Modality, FrameOfReferenceUID: imageVolume.metadata?.FrameOfReferenceUID, }, get scalarData() { return imageVolume.voxelManager?.getScalarData(); }, scaling: imageVolume.scaling, hasPixelSpacing: imageVolume.hasPixelSpacing, voxelManager: imageVolume.voxelManager, }; }