@cornerstonejs/core
Version:
Cornerstone3D Core
484 lines (483 loc) • 18 kB
JavaScript
import { vec3, mat4 } from 'gl-matrix';
import { Events as EVENTS, MetadataModules } from '../enums/index.js';
import uuidv4 from '../utilities/uuidv4.js';
import * as metaData from '../metaData.js';
import { Transform } from './helpers/cpuFallback/rendering/transform.js';
import Viewport from './Viewport.js';
import { getOrCreateCanvas } from './helpers/index.js';
import { EPSILON } from '../constants/index.js';
import triggerEvent from '../utilities/triggerEvent.js';
import eventTarget from '../eventTarget.js';
import imageIdToURI from '../utilities/imageIdToURI.js';
import { getGenericViewportWSIDisplaySet } from './GenericViewport/genericViewportDisplaySetAccess.js';
import { addWSIMiniNavigationOverlayCss, getDicomMicroscopyViewer, loadWSIData, } from '../utilities/WSIUtilities.js';
let WSIUtilFunctions = null;
const EVENT_POSTRENDER = 'postrender';
const ANNOTATION_REMOVED = 'CORNERSTONE_TOOLS_ANNOTATION_REMOVED';
class WSIViewport extends Viewport {
constructor(props) {
super({
...props,
canvas: props.canvas || getOrCreateCanvas(props.element),
});
this.imageURISet = new Set();
this.internalCamera = {
rotation: 0,
centerIndex: [0, 0],
extent: [0, -2, 1, -1],
xSpacing: 1,
ySpacing: 1,
resolution: 1,
zoom: 1,
};
this.annotationRemovedListener = (evt) => {
const { detail } = evt;
const metadata = detail?.annotation?.metadata;
if (!metadata) {
this.postrender();
return;
}
const referencedImageURI = metadata.referencedImageURI ??
(metadata.referencedImageId
? imageIdToURI(metadata.referencedImageId)
: null);
if (referencedImageURI && !this.hasImageURI(referencedImageURI)) {
return;
}
const annotationFOR = metadata.FrameOfReferenceUID ?? null;
if (annotationFOR && annotationFOR !== this.frameOfReferenceUID) {
return;
}
this.postrender();
};
this.voiRange = {
lower: 0,
upper: 255,
};
this.getProperties = () => {
return {
voiRange: { ...this.voiRange },
};
};
this.resetCamera = () => {
return true;
};
this.getNumberOfSlices = () => {
return 1;
};
this.getFrameOfReferenceUID = () => {
return this.frameOfReferenceUID;
};
this.resize = () => {
const canvas = this.canvas;
const { clientWidth, clientHeight } = canvas;
if (canvas.width !== clientWidth || canvas.height !== clientHeight) {
canvas.width = clientWidth;
canvas.height = clientHeight;
}
this.refreshRenderValues();
};
this.canvasToWorld = (canvasPos) => {
if (!this.metadata) {
return;
}
const indexPoint = this.canvasToIndex(canvasPos);
indexPoint[1] = -indexPoint[1];
return this.indexToWorld(indexPoint);
};
this.worldToCanvas = (worldPos) => {
if (!this.metadata) {
return;
}
const indexPoint = this.worldToIndex(worldPos);
indexPoint[1] = -indexPoint[1];
const canvasPoint = this.indexToCanvas([indexPoint[0], indexPoint[1], 0]);
return canvasPoint;
};
this.postrender = () => {
this.refreshRenderValues();
triggerEvent(this.element, EVENTS.IMAGE_RENDERED, {
element: this.element,
viewportId: this.id,
viewport: this,
renderingEngineId: this.renderingEngineId,
});
};
this.getRotation = () => 0;
this.canvasToIndex = (canvasPos) => {
const transform = this.getTransform();
transform.invert();
const indexPoint = transform.transformPoint(canvasPos);
return [indexPoint[0], indexPoint[1], 0];
};
this.indexToCanvas = (indexPos) => {
const transform = this.getTransform();
return transform.transformPoint([indexPos[0], indexPos[1]]);
};
this.customRenderViewportToCanvas = () => {
};
this.getImageIds = () => {
return [this.imageIds[0]];
};
this.renderingEngineId = props.renderingEngineId;
this.element.setAttribute('data-viewport-uid', this.id);
this.element.setAttribute('data-rendering-engine-uid', this.renderingEngineId);
this.element.style.position = 'relative';
this.microscopyElement = document.createElement('div');
this.microscopyElement.setAttribute('class', 'DicomMicroscopyViewer');
this.microscopyElement.id = uuidv4();
this.microscopyElement.innerText = 'Initial';
this.microscopyElement.style.background = 'grey';
this.microscopyElement.style.width = '100%';
this.microscopyElement.style.height = '100%';
this.microscopyElement.style.position = 'absolute';
this.microscopyElement.style.left = '0';
this.microscopyElement.style.top = '0';
const cs3dElement = this.element.firstElementChild;
cs3dElement.insertBefore(this.microscopyElement, cs3dElement.childNodes[1]);
this.addEventListeners();
this.addWidget('DicomMicroscopyViewer', {
getEnabled: () => !!this.viewer,
setEnabled: () => {
this.elementDisabledHandler();
},
});
this.resize();
}
static get useCustomRenderingPipeline() {
return true;
}
addEventListeners() {
this.canvas.addEventListener(EVENTS.ELEMENT_DISABLED, this.elementDisabledHandler);
eventTarget.addEventListener(ANNOTATION_REMOVED, this.annotationRemovedListener);
}
removeEventListeners() {
this.canvas.removeEventListener(EVENTS.ELEMENT_DISABLED, this.elementDisabledHandler);
eventTarget.removeEventListener(ANNOTATION_REMOVED, this.annotationRemovedListener);
}
elementDisabledHandler() {
this.removeEventListeners();
this.viewer?.cleanup();
this.viewer = null;
const cs3dElement = this.element.firstElementChild;
cs3dElement.removeChild(this.microscopyElement);
this.microscopyElement = null;
this.imageURISet.clear();
}
async setFrameNumber(_frame) {
}
setProperties(props) {
if (props.voiRange) {
this.setVOI(props.voiRange);
}
}
resetProperties() {
this.setProperties({
voiRange: {
lower: 0,
upper: 255,
},
});
}
setVOI(voiRange) {
this.voiRange = voiRange;
const feFilter = this.setColorTransform(voiRange, this.averageWhite);
const olCanvases = this.map
.getViewport()
.querySelectorAll('.ol-layers canvas');
olCanvases.forEach((canvas) => {
canvas.style.filter = feFilter;
});
}
setAverageWhite(averageWhite) {
this.averageWhite = averageWhite;
this.setColorTransform(this.voiRange, averageWhite);
}
getScalarData() {
return null;
}
computeTransforms() {
const indexToWorld = mat4.create();
const worldToIndex = mat4.create();
mat4.fromTranslation(indexToWorld, this.metadata.origin);
indexToWorld[0] = this.metadata.direction[0];
indexToWorld[1] = this.metadata.direction[1];
indexToWorld[2] = this.metadata.direction[2];
indexToWorld[4] = this.metadata.direction[3];
indexToWorld[5] = this.metadata.direction[4];
indexToWorld[6] = this.metadata.direction[5];
indexToWorld[8] = this.metadata.direction[6];
indexToWorld[9] = this.metadata.direction[7];
indexToWorld[10] = this.metadata.direction[8];
mat4.scale(indexToWorld, indexToWorld, this.metadata.spacing);
mat4.invert(worldToIndex, indexToWorld);
return { indexToWorld, worldToIndex };
}
getImageData() {
const { metadata } = this;
if (!metadata) {
return null;
}
const { spacing } = metadata;
const imageData = {
getDirection: () => metadata.direction,
getDimensions: () => metadata.dimensions,
getRange: () => [0, 255],
getScalarData: () => this.getScalarData(),
getSpacing: () => metadata.spacing,
worldToIndex: (point) => {
return this.worldToIndex(point);
},
indexToWorld: (point) => {
return this.indexToWorld(point);
},
};
const imageDataReturn = {
dimensions: metadata.dimensions,
spacing,
numberOfComponents: 3,
origin: metadata.origin,
direction: metadata.direction,
metadata: {
Modality: this.modality,
FrameOfReferenceUID: this.frameOfReferenceUID,
},
hasPixelSpacing: this.hasPixelSpacing,
calibration: this.calibration,
preScale: {
scaled: false,
},
scalarData: this.getScalarData(),
imageData,
};
return imageDataReturn;
}
hasImageURI(imageURI) {
if (!imageURI) {
return false;
}
return this.imageURISet.has(imageURI);
}
setCamera(camera) {
const previousCamera = this.getCamera();
const { parallelScale, focalPoint } = camera;
const view = this.getView();
const { xSpacing } = this.internalCamera;
if (parallelScale) {
const worldToCanvasRatio = this.element.clientHeight / parallelScale;
const resolution = 1 / xSpacing / worldToCanvasRatio;
view.setResolution?.(resolution);
}
if (focalPoint) {
const newCanvas = this.worldToCanvas(focalPoint);
const newIndex = this.canvasToIndex(newCanvas);
view.setCenter([newIndex[0], newIndex[1]]);
}
const updatedCamera = this.getCamera();
this.triggerCameraModifiedEventIfNecessary(previousCamera, updatedCamera);
}
getCurrentImageId() {
return this.imageIds[0];
}
getFrameNumber() {
return 1;
}
getCamera() {
this.refreshRenderValues();
const { resolution, xSpacing, centerIndex } = this.internalCamera;
const canvasToWorldRatio = resolution * xSpacing;
const canvasCenter = this.indexToCanvas([
centerIndex[0],
centerIndex[1],
0,
]);
const focalPoint = this.canvasToWorld(canvasCenter);
return {
parallelProjection: true,
focalPoint,
position: focalPoint,
viewUp: [0, -1, 0],
parallelScale: this.element.clientHeight * canvasToWorldRatio,
viewPlaneNormal: [0, 0, 1],
};
}
worldToIndexWSI(point) {
if (!WSIUtilFunctions) {
return;
}
const affine = this.viewer.getAffine();
const pixelCoords = WSIUtilFunctions.applyInverseTransform({
coordinate: [point[0], point[1]],
affine,
});
return [pixelCoords[0], pixelCoords[1]];
}
indexToWorldWSI(point) {
if (!WSIUtilFunctions) {
return;
}
const sliceCoords = WSIUtilFunctions.applyTransform({
coordinate: [point[0], point[1]],
affine: this.viewer.getAffine(),
});
return [sliceCoords[0], sliceCoords[1], 0];
}
worldToIndex(point) {
const { worldToIndex: worldToIndexMatrix } = this.computeTransforms();
const imageCoord = vec3.create();
vec3.transformMat4(imageCoord, point, worldToIndexMatrix);
return imageCoord;
}
indexToWorld(point) {
const { indexToWorld: indexToWorldMatrix } = this.computeTransforms();
const worldPos = vec3.create();
const point3D = vec3.fromValues(...point);
vec3.transformMat4(worldPos, point3D, indexToWorldMatrix);
return [worldPos[0], worldPos[1], worldPos[2]];
}
setDataList(entries) {
if (!entries.length) {
return;
}
const { dataId } = entries[0];
const dataSet = metaData.get('genericViewportDisplaySet', dataId);
if (dataSet?.imageIds) {
return this.setDataIds(dataSet.imageIds, {
webClient: dataSet.options?.webClient,
});
}
return this.setDataIds([dataId]);
}
setDataIds(imageIds, options) {
if (options?.miniNavigationOverlay !== false) {
addWSIMiniNavigationOverlayCss();
}
const webClient = options?.webClient ||
metaData.get(MetadataModules.WADO_WEB_CLIENT, imageIds[0]);
if (!webClient) {
throw new Error(`To use setDataIds on WSI data, you must provide metaData.webClient for ${imageIds[0]}.`);
}
return this.setWSI(imageIds, webClient);
}
async setWSI(imageIds, client) {
this.clearDisplaySets();
this.microscopyElement.style.background = 'black';
this.microscopyElement.innerText = 'Loading';
this.imageIds = imageIds;
const DicomMicroscopyViewer = await getDicomMicroscopyViewer();
WSIUtilFunctions ||= DicomMicroscopyViewer.utils;
const loadedData = await loadWSIData({
imageIds: this.imageIds,
client,
dicomMicroscopyViewer: DicomMicroscopyViewer,
});
this.imageURISet = loadedData.imageURISet;
this.frameOfReferenceUID = loadedData.frameOfReferenceUID;
this.metadata = loadedData.metadata;
this.hasPixelSpacing = loadedData.metadata.hasPixelSpacing;
const viewer = new DicomMicroscopyViewer.viewer.VolumeImageViewer({
client,
metadata: loadedData.volumeImages,
controls: ['overview', 'position'],
retrieveRendered: false,
bindings: {},
});
viewer.render({ container: this.microscopyElement });
viewer.deactivateDragPanInteraction();
this.viewer = viewer;
this.map = viewer.getMap();
this.map.on(EVENT_POSTRENDER, this.postrender);
this.resize();
this.microscopyElement.innerText = '';
Object.assign(this.microscopyElement.style, {
'--ol-partial-background-color': 'rgba(127, 127, 127, 0.7)',
'--ol-foreground-color': '#000000',
'--ol-subtle-foreground-color': '#000',
'--ol-subtle-background-color': 'rgba(78, 78, 78, 0.5)',
background: 'none',
});
}
async setDisplaySets(...entries) {
await this.mountDisplaySets(entries, async (entry) => {
const dataSet = getGenericViewportWSIDisplaySet(entry.displaySetId);
if (!dataSet?.imageIds?.length || !dataSet.options?.webClient) {
throw new Error(`[WSIViewport] No registered WSI dataset (imageIds + webClient) for display set ${entry.displaySetId}`);
}
if (dataSet.options.miniNavigationOverlay !== false) {
addWSIMiniNavigationOverlayCss();
}
await WSIViewport.prototype.setWSI.call(this, dataSet.imageIds, dataSet.options.webClient);
});
}
scroll(delta) {
const camera = this.getCamera();
this.setCamera({
parallelScale: camera.parallelScale * (1 + 0.1 * delta),
});
}
getSliceIndex() {
return 0;
}
getView() {
if (!this.viewer) {
return;
}
const map = this.viewer.getMap();
const anyWindow = window;
anyWindow.map = map;
anyWindow.viewer = this.viewer;
anyWindow.view = map?.getView();
anyWindow.wsi = this;
return map?.getView();
}
refreshRenderValues() {
const view = this.getView();
if (!view) {
return;
}
const resolution = view.getResolution();
if (!resolution || resolution < EPSILON) {
return;
}
const centerIndex = view.getCenter();
const extent = view.getProjection().getExtent();
const rotation = view.getRotation();
const zoom = view.getZoom();
const { metadata: { spacing: [xSpacing, ySpacing], }, } = this;
const worldToCanvasRatio = 1 / resolution / xSpacing;
Object.assign(this.internalCamera, {
extent,
centerIndex,
worldToCanvasRatio,
xSpacing,
ySpacing,
resolution,
rotation,
zoom,
});
}
getZoom() {
return this.getView()?.getZoom();
}
setZoom(zoom) {
this.getView()?.setZoom(zoom);
}
getTransform() {
this.refreshRenderValues();
const { centerIndex: center, resolution, rotation } = this.internalCamera;
const halfCanvas = [this.canvas.width / 2, this.canvas.height / 2];
const transform = new Transform();
transform.translate(halfCanvas[0], halfCanvas[1]);
transform.rotate(rotation);
transform.scale(1 / resolution, -1 / resolution);
transform.translate(-center[0], -center[1]);
return transform;
}
getViewReferenceId() {
return `imageId:${this.getCurrentImageId()}`;
}
getCurrentImageIdIndex() {
return 0;
}
}
export default WSIViewport;