src/engine/imgproc/Gauss.js (202 lines of code) (raw):
/*
* Copyright 2021 EPAM Systems, Inc. (https://www.epam.com/)
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Gauss filtering
* @module demo/engine/imgproc/Gauss
*/
import * as THREE from 'three';
import Volume from '../Volume';
import BilateralHW from './BilateralHW';
//
// Gauss edge detectio
//
class GaussSmoother {
constructor(needHw = false) {
this.m_needHw = needHw;
this.m_vol = null;
this.m_z = -1;
this.m_iter = -1;
this.m_pixelsDst = null;
this.m_kernel = null;
this.m_bilateralHw = null;
}
getPixelsDst() {
if (this.m_needHw) {
const arr = this.m_bilateralHw.getImageDst();
return arr;
}
return this.m_pixelsDst;
}
createKernel(kernelSize, sigma) {
const side = kernelSize * 2 + 1;
const xyzKernel = side * side * side;
const arr = new Float32Array(xyzKernel);
const mult = 1.0 / (2.0 * sigma * sigma);
let off = 0;
let sum = 0.0;
for (let dz = -kernelSize; dz <= +kernelSize; dz++) {
const tz = dz / kernelSize;
for (let dy = -kernelSize; dy <= +kernelSize; dy++) {
const ty = dy / kernelSize;
for (let dx = -kernelSize; dx <= +kernelSize; dx++) {
const tx = dx / kernelSize;
const w = Math.exp(-(tx * tx + ty * ty + tz * tz) * mult);
arr[off++] = w;
sum += w;
} // for dx
} // for dy
} // for dz
// normalize arr
const scl = 1.0 / sum;
for (let i = 0; i < xyzKernel; i++) arr[i] *= scl;
this.m_kernel = arr;
}
start(vol, kernelSize, koefDist, koefVal = 0.1) {
const sigmaDist = (1.0 / kernelSize) * koefDist;
this.createKernel(kernelSize, sigmaDist);
this.m_kernelSize = kernelSize;
console.assert(vol != null);
console.assert(vol.m_dataArray !== null);
this.m_vol = vol;
this.m_z = 0;
this.m_iter = 0;
const xDim = vol.m_xDim;
const yDim = vol.m_yDim;
const zDim = vol.m_zDim;
this.m_pixelsDst = new Float32Array(xDim * yDim * zDim);
// start perform hardware gauss filtering
if (this.m_needHw) {
this.m_bilateralHw = new BilateralHW();
const vTexelSize = new THREE.Vector3(1.0 / xDim, 1.0 / yDim, 1.0 / zDim);
this.m_bilateralHw.create(vol, vTexelSize, kernelSize, koefDist, koefVal);
} // if HW gauss
}
normalizeDstImage() {
if (this.m_needHw) {
return;
}
let valMax = 0.0;
const xyzDim = this.m_vol.m_xDim * this.m_vol.m_yDim * this.m_vol.m_zDim;
for (let i = 0; i < xyzDim; i++) {
const val = this.m_pixelsDst[i];
valMax = val > valMax ? val : valMax;
} // for i
valMax += 0.9;
const scl = 255.0 / valMax;
for (let i = 0; i < xyzDim; i++) {
this.m_pixelsDst[i] = Math.floor(this.m_pixelsDst[i] * scl);
} // for i
}
stop() {
this.m_pixelsDst = null;
this.m_kernel = null;
}
// return ratio in [0..1]
getRatio() {
const zDim = this.m_vol.m_zDim;
let ratio01 = 0.0;
if (this.m_needHw) {
ratio01 = this.m_bilateralHw.m_z / zDim;
} else {
ratio01 = this.m_z / zDim;
}
return ratio01;
}
isFinished() {
console.assert(this.m_z >= 0);
const zDim = this.m_vol.m_zDim;
if (this.m_needHw) {
if (this.m_bilateralHw.m_z >= zDim) {
return true;
}
} else {
if (this.m_z >= zDim) {
return true;
}
}
return false;
}
// invoked several times externally, until entire image processed
update() {
if (this.m_needHw) {
this.m_bilateralHw.update();
return;
}
// perform slow software gauss update with portion of slices
console.assert(this.m_z >= 0);
console.assert(this.m_pixelsDst !== null);
const pixelsSrc = this.m_vol.m_dataArray;
const xDim = this.m_vol.m_xDim;
const yDim = this.m_vol.m_yDim;
const zDim = this.m_vol.m_zDim;
const xyDim = xDim * yDim;
const STEP = zDim > 16 ? 24 : 2;
const zNext = Math.floor(((this.m_iter + 1) * zDim) / STEP);
// console.log('Gauss update z from ' + this.m_z.toString() + ' until ' + zNext.toString());
const arrKernel = this.m_kernel;
const kernelSize = this.m_kernelSize;
// update all z slices from this.m_z to zNext
let off = this.m_z * xyDim;
for (let z = this.m_z; z < zNext; z++) {
for (let y = 0; y < yDim; y++) {
for (let x = 0; x < xDim; x++) {
// process pixel (x,y,z)
let sum = 0.0;
let offKer = 0;
for (let dz = -kernelSize; dz <= +kernelSize; dz++) {
let zz = z + dz;
zz = zz >= 0 ? zz : 0;
zz = zz < zDim ? zz : zDim - 1;
const zzOff = zz * xyDim;
for (let dy = -kernelSize; dy <= +kernelSize; dy++) {
let yy = y + dy;
yy = yy >= 0 ? yy : 0;
yy = yy < yDim ? yy : yDim - 1;
const yyOff = yy * xDim;
for (let dx = -kernelSize; dx <= +kernelSize; dx++) {
let xx = x + dx;
xx = xx >= 0 ? xx : 0;
xx = xx < xDim ? xx : xDim - 1;
sum += arrKernel[offKer] * pixelsSrc[xx + yyOff + zzOff];
offKer++;
} // for dx
} // for dy
} // for dz
this.m_pixelsDst[off] = sum;
off++;
} // for x
} // for y
} // for z all slices
// console.log('max sqrt = ' + maxSqrt.toString());
// update iteration parameters
this.m_iter += 1;
this.m_z = zNext;
}
testSimple() {
// create simple small volume
const SZ = 16;
const HALF_SZ = SZ / 2;
const volume = new Volume();
volume.createEmptyBytesVolume(SZ, SZ, SZ);
let offDst = 0;
const pixelsSrc = volume.m_dataArray;
for (let z = 0; z < SZ; z++) {
for (let y = 0; y < SZ; y++) {
for (let x = 0; x < SZ; x++) {
pixelsSrc[offDst++] = z < HALF_SZ ? 0 : 255;
}
}
}
// apply gauss
const kernelSize = 2;
const sigma = kernelSize / 6.0;
this.start(volume, kernelSize, sigma);
while (!this.isFinished()) {
this.update();
}
// gauss.normalizeDstImage();
const pixelsDst = this.getPixelsDst();
// check some pixels
const xOff = HALF_SZ;
const yOff = HALF_SZ * SZ;
const xyDim = SZ * SZ;
let valPrev = -1;
for (let z = 0; z < SZ; z++) {
const srcVal = pixelsSrc[xOff + yOff + z * xyDim];
const val = pixelsDst[xOff + yOff + z * xyDim];
console.log('src val / gauss val = ' + srcVal.toString() + ' / ' + val.toString());
const isGood = val >= valPrev ? true : false;
if (!isGood) {
console.log('gauss test failed');
}
valPrev = val;
} // for z
this.stop();
console.log('gauss test completed on a small volume');
} // end test simple
} // end class
export default GaussSmoother;