@cornerstonejs/core
Version:
Cornerstone3D Core
517 lines (516 loc) • 22 kB
JavaScript
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,
};
}