src/engine/volumeFilter3d.js (306 lines of code) (raw):
/*
* Copyright 2021 EPAM Systems, Inc. (https://www.epam.com/)
* SPDX-License-Identifier: Apache-2.0
*/
/**
* 3D volume processing engine: blur, contrast filter
* @module lib/scripts/graphics3d/volumeFilter3d
*/
import * as THREE from 'three';
import MaterialBlur from './gfx/matblur';
import GlSelector from './GlSelector';
import AmbientTexture from './ambientTexture';
import TransferTexture from './transferTexture';
import Eraser from './Eraser';
/** Class VolumeFilter3d is used for 3d render */
export default class VolumeFilter3d {
constructor() {
this.transferFunc = null;
this.xDim = 0;
this.yDim = 0;
this.zDim = 0;
this.bufferTextureCPU = null;
this.eraser = null;
}
/**
* Filtering the source data and building the normals on the GPU
* @param isRoiVolume
* @param roiColors Array of roi colors in RGBA format
*/
initRenderer(isRoiVolume, roiColors) {
this.sceneBlur = new THREE.Scene();
const blurSigma = 0.8;
this.numRois = 256;
// eslint-disable-next-line
this.cameraOrtho = new THREE.OrthographicCamera(this.xDim / -2, this.xDim / 2, this.yDim / 2, this.yDim / -2, 0.1, 100);
const glSelector = new GlSelector();
this.context = glSelector.createWebGLContext();
this.canvas3d = glSelector.getCanvas();
this.rendererBlur = new THREE.WebGLRenderer({
canvas: this.canvas3d,
context: this.context,
});
this.ambientTexture = new AmbientTexture({
xDim: this.xDim,
yDim: this.yDim,
zDim: this.zDim,
renderer: this.rendererBlur,
scene: this.sceneBlur,
camera: this.cameraOrtho,
});
console.log('rendererBlur done');
const geometryBlur = new THREE.PlaneGeometry(1.0, 1.0);
this.rendererBlur.setSize(this.xDim, this.yDim);
//
this.transferFunc = new TransferTexture();
this.transferFunc.init(isRoiVolume, roiColors);
const texelSize = new THREE.Vector3(1.0 / this.xDim, 1.0 / this.yDim, 1.0 / this.zDim);
const matBlur = new MaterialBlur();
matBlur.create(this.origVolumeTex, this.RoiVolumeTex, texelSize, this.transferFunc.texRoiColor, this.transferFunc.texRoiId, (mat) => {
const mesh = new THREE.Mesh(geometryBlur, mat);
mat.uniforms.volumeSizeZ.value = this.zDim;
mat.uniforms.xDim.value = this.xDim;
mat.uniforms.yDim.value = this.yDim;
mat.defines.useWebGL2 = this.isWebGL2;
this.material = mat;
this.sceneBlur.add(mesh);
if (isRoiVolume === false) {
this.switchToBlurRender();
} else {
this.switchToRoiMapRender();
}
// render with blur and copy pixels back to this.bufferRgba
console.log(`isRoiVolume: ${isRoiVolume}`);
this.setVolumeTexture(blurSigma);
});
this.vectorsTex = null;
}
/**
* Create 2D texture containing transfer func colors
*/
createTransferFuncTexture() {
if (this.transferFunc !== null) {
return this.transferFunc.createTransferFuncTexture();
}
return null;
}
/**
* Creates transfer function color map
* @param ctrlPts Array of control points of type HEX = color value
*/
setTransferFuncColors(ctrlPtsColorsHex) {
this.transferFunc.setTransferFuncColors(ctrlPtsColorsHex);
}
/**
* Creates transfer function color map
* @param ctrlPts Array of Vector2 where (x,y) = x coordinate in [0, 1], alpha value in [0, 1]
* //intensity [0,255] opacity [0,1]
*/
updateTransferFuncTexture(intensities, opacities) {
return this.transferFunc.updateTransferFuncTexture(intensities, opacities);
}
/**
* Setting a variable for conditional compilation (Roi Render)
*/
switchToRoiMapRender() {
this.material.defines.renderRoiMap = 1;
this.material.needsUpdate = true;
}
/**
* Setting a variable for conditional compilation (Blur)
*/
switchToBlurRender() {
this.material.defines.renderRoiMap = 0;
this.material.needsUpdate = true;
}
/**
* Filtering the source data and building the normals on the GPU
* @param blurSigma Gauss sigma parameter
*/
setVolumeTexture(blurSigma) {
if (!this.material || typeof this.material === 'undefined') {
console.log('blur material null');
return;
}
console.log('blur material NOT null');
this.setVolumeTextureWebGL2(blurSigma);
this.updatableTexture.needsUpdate = true;
}
updateVolumeTextureWithMask() {
if (this.eraser.bufferMask === null) {
console.log('volTextureMask null');
}
for (let z = 0; z < this.zDim; z++) {
const zVolOff = z * this.xDim * this.yDim;
for (let y = 0; y < this.yDim; y++) {
const yVol = y;
const yVolOff = yVol * this.xDim;
for (let x = 0; x < this.xDim; x++) {
const xVol = x;
const offSrc = xVol + yVolOff + zVolOff;
let valInt = this.arrPixels[offSrc];
const offDst = offSrc;
if (this.zDim > 5 && (z === 0 || z === this.zDim - 1 || this.eraser.bufferMask[offSrc] === 0)) {
valInt = 0;
}
this.bufferR[offDst] = valInt;
}
}
}
this.origVolumeTex.needsUpdate = true;
this.setVolumeTexture(1.0);
}
setVolumeTextureWebGL2(blurSigma) {
this.material.uniforms.blurSigma.value = blurSigma;
this.material.uniforms.blurSigma.needsUpdate = true;
const VAL_1 = 1;
const VAL_2 = 2;
const VAL_3 = 3;
const VAL_4 = 4;
const frameBuf = new Uint8Array(VAL_4 * this.xDim * this.yDim);
const gl = this.rendererBlur.getContext();
for (let z = 1; z < this.zDim - 1; ++z) {
this.material.uniforms.curZ.value = z / this.zDim;
this.material.uniforms.curZ.needsUpdate = true;
this.rendererBlur.render(this.sceneBlur, this.cameraOrtho, this.bufferTexture);
gl.readPixels(0, 0, this.xDim, this.yDim, gl.RGBA, gl.UNSIGNED_BYTE, frameBuf);
const zOffs = z * this.xDim * this.yDim;
for (let y = 0; y < this.yDim; y++) {
for (let x = 0; x < this.xDim; x++) {
if (this.isRoiVolume) {
const indxR = VAL_4 * (x + y * this.xDim);
const indxL = indxR + zOffs * VAL_4;
this.bufferTextureCPU[indxL] = frameBuf[indxR];
this.bufferTextureCPU[indxL + VAL_1] = frameBuf[indxR + VAL_1];
this.bufferTextureCPU[indxL + VAL_2] = frameBuf[indxR + VAL_2];
this.bufferTextureCPU[indxL + VAL_3] = frameBuf[indxR + VAL_3];
} else {
this.bufferTextureCPU[x + y * this.xDim + zOffs] = frameBuf[VAL_4 * (x + y * this.xDim)]; //256.0 * k / this.zDim;
}
}
}
}
}
/**
* Copies the source data into the buffer (bufferRgba) from which the 3� texture is created
*/
set3DTextureFrom1Byte() {
const OFF0 = 0;
this.bufferTextureCPU = new Uint8Array(this.xDim * this.yDim * this.zDim);
this.bufferR = new Uint8Array(this.xDim * this.yDim * this.zDim);
// Fill initial rgba array
for (let z = 0; z < this.zDim; z++) {
const zVolOff = z * this.xDim * this.yDim;
for (let y = 0; y < this.yDim; y++) {
const yVol = y;
const yVolOff = yVol * this.xDim;
for (let x = 0; x < this.xDim; x++) {
const xVol = x;
const offSrc = xVol + yVolOff + zVolOff;
let valInt = this.arrPixels[offSrc + 0];
const offDst = offSrc;
if (this.zDim > 5 && (z === 0 || z === this.zDim - 1)) {
valInt = 0;
}
this.bufferR[offDst + OFF0] = valInt;
this.bufferTextureCPU[offDst + OFF0] = valInt;
}
}
}
console.log('setBufferRgbaFrom1Bytes for 3d texture');
}
/**
* Copies the source data into the buffer (bufferRgba) from which the 3� texture is created
*/
set3DTextureFrom4Bytes() {
const OFF0 = 0;
const OFF1 = 1;
const OFF2 = 2;
const OFF3 = 3;
const BID = 4;
if (this.isRoiVolume) {
this.bufferRoi = new Uint8Array(this.xDim * this.yDim * this.zDim);
this.bufferTextureCPU = new Uint8Array(BID * this.xDim * this.yDim * this.zDim);
console.log('ROI');
}
this.bufferR = new Uint8Array(this.xDim * this.yDim * this.zDim);
// Fill initial rgba array
for (let z = 0; z < this.zDim; z++) {
const zVolOff = z * this.xDim * this.yDim;
for (let y = 0; y < this.yDim; y++) {
const yVol = y;
const yVolOff = yVol * this.xDim;
for (let x = 0; x < this.xDim; x++) {
const xVol = x;
const offSrc = (xVol + yVolOff + zVolOff) * BID;
const valInt = this.arrPixels[offSrc + 0];
const valRoi = this.arrPixels[offSrc + OFF3];
//const valRoi = this.arrPixels[offSrc + 0];
//const valInt = this.arrPixels[offSrc + OFF3];
const offDst = xVol + yVolOff + zVolOff;
//if (x === 0 || x === this.xDim - 1 || y === 0 || y === this.yDim - 1 || z === 0 || z === this.zDim - 1)
if (x < 2 || x > this.xDim - 4 || y < 2 || y > this.yDim - 4 || z < 2 || z > this.zDim - 4) this.bufferR[offDst + OFF0] = 0;
else this.bufferR[offDst + OFF0] = valInt;
this.bufferTextureCPU[BID * offDst + OFF0] = valInt;
this.bufferTextureCPU[BID * offDst + OFF1] = valInt;
this.bufferTextureCPU[BID * offDst + OFF2] = valInt;
this.bufferTextureCPU[BID * offDst + OFF3] = valInt;
this.bufferRoi[offDst + OFF0] = valRoi;
}
}
}
console.log('setBufferRgbaFrom4Bytes for 3D texture');
}
/**
* Create 2D texture containing roi color map
* @param colorArray 256 RGBA roi colors
*/
createRoiColorMap(colorArray) {
return this.transferFunc.createRoiColorMap(colorArray);
}
/**
* Create 2D texture containing selected ROIs
* @param colorArray 256 RGBA roi colors
*/
createSelectedRoiMap() {
return this.createSelectedRoiMap();
}
/**
* Create 2D texture containing selected ROIs
* @param selectedROIs 256 byte roi values
*/
updateSelectedRoiMap(selectedROIs) {
this.transferFunc.updateSelectedRoiMap(selectedROIs);
this.setVolumeTexture(1.0);
}
/**
* Update roi selection map
* @param roiId ROI id from 0..255
* @param selectedState True if roi must be visible
*/
// eslint-disable-next-line
updateSelectedRoi(roiId, selectedState) {
this.transferFunc.updateSelectedRoi(roiId, selectedState);
this.setVolumeTexture(1.0);
}
/**
* Create 3D texture containing filtered source data and calculated normal values
* @param props An object that contains all volume-related info
* @param
* @param roiColors Array of roi colors in RGBA format
* @return (object) Created texture
*/
createUpdatableVolumeTex(volume, isRoiVolume, roiColors) {
this.isWebGL2 = 1;
this.arrPixels = volume.m_dataArray;
const xDim = volume.m_xDim;
const yDim = volume.m_yDim;
const zDim = volume.m_zDim;
this.xDim = xDim;
this.yDim = yDim;
this.zDim = zDim;
this.isRoiVolume = isRoiVolume;
this.RoiVolumeTex = null;
if (!isRoiVolume) {
this.set3DTextureFrom1Byte();
} else {
this.set3DTextureFrom4Bytes();
this.RoiVolumeTex = new THREE.DataTexture3D(this.bufferRoi, this.xDim, this.yDim, this.zDim);
this.RoiVolumeTex.format = THREE.RedFormat; //RedFormat?
this.RoiVolumeTex.type = THREE.UnsignedByteType;
this.RoiVolumeTex.wrapS = THREE.ClampToEdgeWrapping;
this.RoiVolumeTex.wrapT = THREE.ClampToEdgeWrapping;
this.RoiVolumeTex.magFilter = THREE.NearestFilter;
this.RoiVolumeTex.minFilter = THREE.NearestFilter;
this.RoiVolumeTex.needsUpdate = true;
}
this.bufferTexture = new THREE.WebGLRenderTarget(this.xDim, this.yDim, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
depthBuffer: false,
});
if (this.origVolumeTex) {
this.origVolumeTex.dispose();
}
this.origVolumeTex = new THREE.DataTexture3D(this.bufferR, this.xDim, this.yDim, this.zDim);
this.origVolumeTex.format = THREE.RedFormat;
this.origVolumeTex.type = THREE.UnsignedByteType;
this.origVolumeTex.wrapR = THREE.ClampToEdgeWrapping;
this.origVolumeTex.wrapS = THREE.ClampToEdgeWrapping;
this.origVolumeTex.wrapT = THREE.ClampToEdgeWrapping;
this.origVolumeTex.magFilter = THREE.NearestFilter; //THREE.LinearFilter;
this.origVolumeTex.minFilter = THREE.NearestFilter; //THREE.LinearFilter;
this.origVolumeTex.needsUpdate = true;
let volTexFormat = THREE.RedFormat;
if (isRoiVolume) {
volTexFormat = THREE.RGBAFormat;
}
this.updatableTexture = new THREE.DataTexture3D(this.bufferTextureCPU, this.xDim, this.yDim, this.zDim);
this.updatableTexture.format = volTexFormat;
this.updatableTexture.type = THREE.UnsignedByteType;
this.updatableTexture.wrapR = THREE.ClampToEdgeWrapping;
this.updatableTexture.wrapS = THREE.ClampToEdgeWrapping;
this.updatableTexture.wrapT = THREE.ClampToEdgeWrapping;
this.updatableTexture.magFilter = THREE.LinearFilter;
this.updatableTexture.minFilter = THREE.LinearFilter;
if (this.zDim > 1) {
this.initRenderer(isRoiVolume, roiColors);
}
this.updatableTexture.needsUpdate = true;
this.eraser = new Eraser();
return this.updatableTexture;
}
/**
* Create 3D texture containing mask of data which were erase
* @param volume An object that contains all volume-related info
* @return (object) Created texture
*/
createUpdatableVolumeMask(params) {
return this.eraser.createUpdatableVolumeMask(params, this.bufferTextureCPU);
}
} // class VolumeFilter3d