ami.js
Version:
<p align="center"> <img src="https://cloud.githubusercontent.com/assets/214063/23213764/78ade038-f90c-11e6-8208-4fcade5f3832.png" width="60%"> </p>
829 lines (723 loc) • 25.2 kB
JavaScript
/* globals Stats, dat*/
import CamerasOrthographic from 'base/cameras/cameras.orthographic';
import ControlsOrthographic from 'base/controls/controls.trackballortho';
import ControlsTrackball from 'base/controls/controls.trackball';
import CoreUtils from 'base/core/core.utils';
import HelpersBoundingBox from 'base/helpers/helpers.boundingbox';
import HelpersContour from 'base/helpers/helpers.contour';
import HelpersLocalizer from 'base/helpers/helpers.localizer';
import HelpersStack from 'base/helpers/helpers.stack';
import LoadersVolume from 'base/loaders/loaders.volume';
// standard global variables
let stats;
let ready = false;
let redContourHelper = null;
let redTextureTarget = null;
let redContourScene = null;
// 3d renderer
const r0 = {
domId: 'r0',
domElement: null,
renderer: null,
color: 0x212121,
targetID: 0,
camera: null,
controls: null,
scene: null,
light: null,
};
// 2d axial renderer
const r1 = {
domId: 'r1',
domElement: null,
renderer: null,
color: 0x121212,
sliceOrientation: 'axial',
sliceColor: 0xFF1744,
targetID: 1,
camera: null,
controls: null,
scene: null,
light: null,
stackHelper: null,
localizerHelper: null,
localizerScene: null,
};
// 2d sagittal renderer
const r2 = {
domId: 'r2',
domElement: null,
renderer: null,
color: 0x121212,
sliceOrientation: 'sagittal',
sliceColor: 0xFFEA00,
targetID: 2,
camera: null,
controls: null,
scene: null,
light: null,
stackHelper: null,
localizerHelper: null,
localizerScene: null,
};
// 2d coronal renderer
const r3 = {
domId: 'r3',
domElement: null,
renderer: null,
color: 0x121212,
sliceOrientation: 'coronal',
sliceColor: 0x76FF03,
targetID: 3,
camera: null,
controls: null,
scene: null,
light: null,
stackHelper: null,
localizerHelper: null,
localizerScene: null,
};
// data to be loaded
let dataInfo = [
['adi1', {
location:
'https://cdn.rawgit.com/FNNDSC/data/master/dicom/adi_brain/mesh.stl',
label: 'Left',
loaded: false,
material: null,
materialFront: null,
materialBack: null,
mesh: null,
meshFront: null,
meshBack: null,
color: 0xe91e63,
opacity: 0.7,
}],
['adi2', {
location:
'https://cdn.rawgit.com/FNNDSC/data/master/dicom/adi_brain/mesh2.stl',
label: 'Right',
loaded: false,
material: null,
materialFront: null,
materialBack: null,
mesh: null,
meshFront: null,
meshBack: null,
color: 0x03a9f4,
opacity: 1,
}],
];
let data = new Map(dataInfo);
// extra variables to show mesh plane intersections in 2D renderers
let sceneClip = new THREE.Scene();
let clipPlane1 = new THREE.Plane(new THREE.Vector3(0, 0, 0), 0);
let clipPlane2 = new THREE.Plane(new THREE.Vector3(0, 0, 0), 0);
let clipPlane3 = new THREE.Plane(new THREE.Vector3(0, 0, 0), 0);
function initRenderer3D(renderObj) {
// renderer
renderObj.domElement = document.getElementById(renderObj.domId);
renderObj.renderer = new THREE.WebGLRenderer({
antialias: true,
});
renderObj.renderer.setSize(
renderObj.domElement.clientWidth, renderObj.domElement.clientHeight);
renderObj.renderer.setClearColor(renderObj.color, 1);
renderObj.renderer.domElement.id = renderObj.targetID;
renderObj.domElement.appendChild(renderObj.renderer.domElement);
// camera
renderObj.camera = new THREE.PerspectiveCamera(
45, renderObj.domElement.clientWidth / renderObj.domElement.clientHeight,
0.1, 100000);
renderObj.camera.position.x = 250;
renderObj.camera.position.y = 250;
renderObj.camera.position.z = 250;
// controls
renderObj.controls = new ControlsTrackball(
renderObj.camera, renderObj.domElement);
renderObj.controls.rotateSpeed = 5.5;
renderObj.controls.zoomSpeed = 1.2;
renderObj.controls.panSpeed = 0.8;
renderObj.controls.staticMoving = true;
renderObj.controls.dynamicDampingFactor = 0.3;
// scene
renderObj.scene = new THREE.Scene();
// light
renderObj.light = new THREE.DirectionalLight(0xffffff, 1);
renderObj.light.position.copy(renderObj.camera.position);
renderObj.scene.add(renderObj.light);
// stats
stats = new Stats();
renderObj.domElement.appendChild(stats.domElement);
}
function initRenderer2D(rendererObj) {
// renderer
rendererObj.domElement = document.getElementById(rendererObj.domId);
rendererObj.renderer = new THREE.WebGLRenderer({
antialias: true,
});
rendererObj.renderer.autoClear = false;
rendererObj.renderer.localClippingEnabled = true;
rendererObj.renderer.setSize(
rendererObj.domElement.clientWidth, rendererObj.domElement.clientHeight);
rendererObj.renderer.setClearColor(0x121212, 1);
rendererObj.renderer.domElement.id = rendererObj.targetID;
rendererObj.domElement.appendChild(rendererObj.renderer.domElement);
// camera
rendererObj.camera = new CamerasOrthographic(
rendererObj.domElement.clientWidth / -2,
rendererObj.domElement.clientWidth / 2,
rendererObj.domElement.clientHeight / 2,
rendererObj.domElement.clientHeight / -2,
1, 1000);
// controls
rendererObj.controls = new ControlsOrthographic(
rendererObj.camera, rendererObj.domElement);
rendererObj.controls.staticMoving = true;
rendererObj.controls.noRotate = true;
rendererObj.camera.controls = rendererObj.controls;
// scene
rendererObj.scene = new THREE.Scene();
}
function initHelpersStack(rendererObj, stack) {
rendererObj.stackHelper = new HelpersStack(stack);
rendererObj.stackHelper.bbox.visible = false;
rendererObj.stackHelper.borderColor = rendererObj.sliceColor;
rendererObj.stackHelper.slice.canvasWidth =
rendererObj.domElement.clientWidth;
rendererObj.stackHelper.slice.canvasHeight =
rendererObj.domElement.clientHeight;
// set camera
let worldbb = stack.worldBoundingBox();
let lpsDims = new THREE.Vector3(
(worldbb[1] - worldbb[0])/2,
(worldbb[3] - worldbb[2])/2,
(worldbb[5] - worldbb[4])/2
);
// box: {halfDimensions, center}
let box = {
center: stack.worldCenter().clone(),
halfDimensions:
new THREE.Vector3(lpsDims.x + 10, lpsDims.y + 10, lpsDims.z + 10),
};
// init and zoom
let canvas = {
width: rendererObj.domElement.clientWidth,
height: rendererObj.domElement.clientHeight,
};
rendererObj.camera.directions =
[stack.xCosine, stack.yCosine, stack.zCosine];
rendererObj.camera.box = box;
rendererObj.camera.canvas = canvas;
rendererObj.camera.orientation = rendererObj.sliceOrientation;
rendererObj.camera.update();
rendererObj.camera.fitBox(2, 1);
rendererObj.stackHelper.orientation = rendererObj.camera.stackOrientation;
rendererObj.stackHelper.index =
Math.floor(rendererObj.stackHelper.orientationMaxIndex/2);
rendererObj.scene.add(rendererObj.stackHelper);
}
function initHelpersLocalizer(rendererObj, stack, referencePlane, localizers) {
rendererObj.localizerHelper = new HelpersLocalizer(
stack, rendererObj.stackHelper.slice.geometry, referencePlane);
for (let i = 0; i < localizers.length; i++) {
rendererObj.localizerHelper['plane' + (i + 1)] = localizers[i].plane;
rendererObj.localizerHelper['color' + (i + 1)] = localizers[i].color;
}
rendererObj.localizerHelper.canvasWidth =
rendererObj.domElement.clientWidth;
rendererObj.localizerHelper.canvasHeight =
rendererObj.domElement.clientHeight;
rendererObj.localizerScene = new THREE.Scene();
rendererObj.localizerScene.add(rendererObj.localizerHelper);
}
/**
* Init the quadview
*/
function init() {
/**
* Called on each animation frame
*/
function animate() {
// we are ready when both meshes have been loaded
if (ready) {
// render
r0.controls.update();
r1.controls.update();
r2.controls.update();
r3.controls.update();
r0.light.position.copy(r0.camera.position);
r0.renderer.render(r0.scene, r0.camera);
// r1
r1.renderer.clear();
r1.renderer.render(r1.scene, r1.camera);
// mesh
r1.renderer.clearDepth();
data.forEach(function(object, key) {
object.materialFront.clippingPlanes = [clipPlane1];
object.materialBack.clippingPlanes = [clipPlane1];
r1.renderer.render(object.scene, r1.camera, redTextureTarget, true);
r1.renderer.clearDepth();
redContourHelper.contourWidth = object.selected ? 2 : 1;
r1.renderer.render(redContourScene, r1.camera);
r1.renderer.clearDepth();
});
// localizer
r1.renderer.clearDepth();
r1.renderer.render(r1.localizerScene, r1.camera);
// r2
r2.renderer.clear();
r2.renderer.render(r2.scene, r2.camera);
// mesh
r2.renderer.clearDepth();
data.forEach(function(object, key) {
object.materialFront.clippingPlanes = [clipPlane2];
object.materialBack.clippingPlanes = [clipPlane2];
});
r2.renderer.render(sceneClip, r2.camera);
// localizer
r2.renderer.clearDepth();
r2.renderer.render(r2.localizerScene, r2.camera);
// r3
r3.renderer.clear();
r3.renderer.render(r3.scene, r3.camera);
// mesh
r3.renderer.clearDepth();
data.forEach(function(object, key) {
object.materialFront.clippingPlanes = [clipPlane3];
object.materialBack.clippingPlanes = [clipPlane3];
});
r3.renderer.render(sceneClip, r3.camera);
// localizer
r3.renderer.clearDepth();
r3.renderer.render(r3.localizerScene, r3.camera);
}
stats.update();
// request new frame
requestAnimationFrame(function() {
animate();
});
}
// renderers
initRenderer3D(r0);
initRenderer2D(r1);
initRenderer2D(r2);
initRenderer2D(r3);
// start rendering loop
animate();
}
window.onload = function() {
// init threeJS
init();
let t2 = [
'36444280', '36444294', '36444308', '36444322', '36444336',
'36444350', '36444364', '36444378', '36444392', '36444406',
'36444490', '36444504', '36444518', '36444532', '36746856',
'36746870', '36746884', '36746898', '36746912', '36746926',
'36746940', '36746954', '36746968', '36746982', '36746996',
'36747010', '36747024', '36748200', '36748214', '36748228',
'36748270', '36748284', '36748298', '36748312', '36748326',
'36748340', '36748354', '36748368', '36748382', '36748396',
'36748410', '36748424', '36748438', '36748452', '36748466',
'36748480', '36748494', '36748508', '36748522', '36748242',
'36748256', '36444434', '36444448', '36444462', '36444476',
];
let files = t2.map(function(v) {
return 'https://cdn.rawgit.com/FNNDSC/data/master/dicom/adi_brain/' + v;
});
// load sequence for each file
// instantiate the loader
// it loads and parses the dicom image
let loader = new LoadersVolume();
loader.load(files)
.then(function() {
let series = loader.data[0].mergeSeries(loader.data)[0];
loader.free();
loader = null;
// get first stack from series
let stack = series.stack[0];
stack.prepare();
// center 3d camera/control on the stack
let centerLPS = stack.worldCenter();
r0.camera.lookAt(centerLPS.x, centerLPS.y, centerLPS.z);
r0.camera.updateProjectionMatrix();
r0.controls.target.set(centerLPS.x, centerLPS.y, centerLPS.z);
// bouding box
let boxHelper = new HelpersBoundingBox(stack);
r0.scene.add(boxHelper);
// red slice
initHelpersStack(r1, stack);
r0.scene.add(r1.scene);
redTextureTarget = new THREE.WebGLRenderTarget(
r1.domElement.clientWidth,
r1.domElement.clientHeight,
{
minFilter: THREE.LinearFilter,
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat,
}
);
redContourHelper = new HelpersContour(stack, r1.stackHelper.slice.geometry);
redContourHelper.canvasWidth = redTextureTarget.width;
redContourHelper.canvasHeight = redTextureTarget.height;
redContourHelper.textureToFilter = redTextureTarget.texture;
redContourScene = new THREE.Scene();
redContourScene.add(redContourHelper);
// yellow slice
initHelpersStack(r2, stack);
r0.scene.add(r2.scene);
// green slice
initHelpersStack(r3, stack);
r0.scene.add(r3.scene);
// create new mesh with Localizer shaders
let plane1 = r1.stackHelper.slice.cartesianEquation();
let plane2 = r2.stackHelper.slice.cartesianEquation();
let plane3 = r3.stackHelper.slice.cartesianEquation();
// localizer red slice
initHelpersLocalizer(r1, stack, plane1, [
{plane: plane2,
color: new THREE.Color(r2.stackHelper.borderColor),
},
{plane: plane3,
color: new THREE.Color(r3.stackHelper.borderColor),
},
]);
// localizer yellow slice
initHelpersLocalizer(r2, stack, plane2, [
{plane: plane1,
color: new THREE.Color(r1.stackHelper.borderColor),
},
{plane: plane3,
color: new THREE.Color(r3.stackHelper.borderColor),
},
]);
// localizer green slice
initHelpersLocalizer(r3, stack, plane3, [
{plane: plane1,
color: new THREE.Color(r1.stackHelper.borderColor),
},
{plane: plane2,
color: new THREE.Color(r2.stackHelper.borderColor),
},
]);
let gui = new dat.GUI({
autoPlace: false,
});
let customContainer = document.getElementById('my-gui-container');
customContainer.appendChild(gui.domElement);
// Red
let stackFolder1 = gui.addFolder('Axial (Red)');
let redChanged = stackFolder1.add(
r1.stackHelper,
'index', 0, r1.stackHelper.orientationMaxIndex).step(1).listen();
stackFolder1.add(
r1.stackHelper.slice, 'interpolation', 0, 1).step(1).listen();
// Yellow
let stackFolder2 = gui.addFolder('Sagittal (yellow)');
let yellowChanged = stackFolder2.add(
r2.stackHelper,
'index', 0, r2.stackHelper.orientationMaxIndex).step(1).listen();
stackFolder2.add(
r2.stackHelper.slice, 'interpolation', 0, 1).step(1).listen();
// Green
let stackFolder3 = gui.addFolder('Coronal (green)');
let greenChanged = stackFolder3.add(
r3.stackHelper,
'index', 0, r3.stackHelper.orientationMaxIndex).step(1).listen();
stackFolder3.add(
r3.stackHelper.slice, 'interpolation', 0, 1).step(1).listen();
/**
* Update Layer Mix
*/
function updateLocalizer(refObj, targetLocalizersHelpers) {
let refHelper = refObj.stackHelper;
let localizerHelper = refObj.localizerHelper;
let plane = refHelper.slice.cartesianEquation();
localizerHelper.referencePlane = plane;
// bit of a hack... works fine for this application
for (let i = 0; i < targetLocalizersHelpers.length; i++) {
for (let j = 0; j < 3; j++) {
let targetPlane = targetLocalizersHelpers[i]['plane' + (j + 1)];
if (targetPlane &&
plane.x.toFixed(6) === targetPlane.x.toFixed(6) &&
plane.y.toFixed(6) === targetPlane.y.toFixed(6) &&
plane.z.toFixed(6) === targetPlane.z.toFixed(6)) {
targetLocalizersHelpers[i]['plane' + (j + 1)] = plane;
}
}
}
// update the geometry will create a new mesh
localizerHelper.geometry = refHelper.slice.geometry;
}
function updateClipPlane(refObj, clipPlane) {
const stackHelper = refObj.stackHelper;
const camera = refObj.camera;
let vertices = stackHelper.slice.geometry.vertices;
let p1 = new THREE.Vector3(vertices[0].x, vertices[0].y, vertices[0].z)
.applyMatrix4(stackHelper._stack.ijk2LPS);
let p2 = new THREE.Vector3(vertices[1].x, vertices[1].y, vertices[1].z)
.applyMatrix4(stackHelper._stack.ijk2LPS);
let p3 = new THREE.Vector3(vertices[2].x, vertices[2].y, vertices[2].z)
.applyMatrix4(stackHelper._stack.ijk2LPS);
clipPlane.setFromCoplanarPoints(p1, p2, p3);
let cameraDirection = new THREE.Vector3(1, 1, 1);
cameraDirection.applyQuaternion(camera.quaternion);
if (cameraDirection.dot(clipPlane.normal) > 0) {
clipPlane.negate();
}
}
function onYellowChanged() {
updateLocalizer(r2, [r1.localizerHelper, r3.localizerHelper]);
updateClipPlane(r2, clipPlane2);
}
yellowChanged.onChange(onYellowChanged);
function onRedChanged() {
updateLocalizer(r1, [r2.localizerHelper, r3.localizerHelper]);
updateClipPlane(r1, clipPlane1);
if (redContourHelper) {
redContourHelper.geometry = r1.stackHelper.slice.geometry;
}
}
redChanged.onChange(onRedChanged);
function onGreenChanged() {
updateLocalizer(r3, [r1.localizerHelper, r2.localizerHelper]);
updateClipPlane(r3, clipPlane3);
}
greenChanged.onChange(onGreenChanged);
function onDoubleClick(event) {
const canvas = event.target.parentElement;
const id = event.target.id;
const mouse = {
x: ((event.clientX - canvas.offsetLeft) / canvas.clientWidth) * 2 - 1,
y: - ((event.clientY - canvas.offsetTop) / canvas.clientHeight) * 2 + 1,
};
//
let camera = null;
let stackHelper = null;
let scene = null;
switch (id) {
case '0':
camera = r0.camera;
stackHelper = r1.stackHelper;
scene = r0.scene;
break;
case '1':
camera = r1.camera;
stackHelper = r1.stackHelper;
scene = r1.scene;
break;
case '2':
camera = r2.camera;
stackHelper = r2.stackHelper;
scene = r2.scene;
break;
case '3':
camera = r3.camera;
stackHelper = r3.stackHelper;
scene = r3.scene;
break;
}
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
let ijk =
CoreUtils.worldToData(stackHelper.stack.lps2IJK, intersects[0].point);
r1.stackHelper.index =
ijk.getComponent((r1.stackHelper.orientation + 2) % 3);
r2.stackHelper.index =
ijk.getComponent((r2.stackHelper.orientation + 2) % 3);
r3.stackHelper.index =
ijk.getComponent((r3.stackHelper.orientation + 2) % 3);
onGreenChanged();
onRedChanged();
onYellowChanged();
}
}
// event listeners
r0.domElement.addEventListener('dblclick', onDoubleClick);
r1.domElement.addEventListener('dblclick', onDoubleClick);
r2.domElement.addEventListener('dblclick', onDoubleClick);
r3.domElement.addEventListener('dblclick', onDoubleClick);
function onClick(event) {
const canvas = event.target.parentElement;
const id = event.target.id;
const mouse = {
x: ((event.clientX - canvas.offsetLeft) / canvas.clientWidth) * 2 - 1,
y: - ((event.clientY - canvas.offsetTop) / canvas.clientHeight) * 2 + 1,
};
//
let camera = null;
let stackHelper = null;
let scene = null;
switch (id) {
case '0':
camera = r0.camera;
stackHelper = r1.stackHelper;
scene = r0.scene;
break;
case '1':
camera = r1.camera;
stackHelper = r1.stackHelper;
scene = r1.scene;
break;
case '2':
camera = r2.camera;
stackHelper = r2.stackHelper;
scene = r2.scene;
break;
case '3':
camera = r3.camera;
stackHelper = r3.stackHelper;
scene = r3.scene;
break;
}
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
if (intersects[0].object && intersects[0].object.objRef) {
const refObject = intersects[0].object.objRef;
refObject.selected = !refObject.selected;
let color = refObject.color;
if (refObject.selected) {
color = 0xCCFF00;
}
// update materials colors
refObject.material.color.setHex(color);
refObject.materialFront.color.setHex(color);
refObject.materialBack.color.setHex(color);
}
}
}
r0.domElement.addEventListener('click', onClick);
function onScroll(event) {
const id = event.target.domElement.id;
let stackHelper = null;
switch (id) {
case 'r1':
stackHelper = r1.stackHelper;
break;
case 'r2':
stackHelper = r2.stackHelper;
break;
case 'r3':
stackHelper = r3.stackHelper;
break;
}
if (event.delta > 0) {
if (stackHelper.index >= stackHelper.orientationMaxIndex - 1) {
return false;
}
stackHelper.index += 1;
} else {
if (stackHelper.index <= 0) {
return false;
}
stackHelper.index -= 1;
}
onGreenChanged();
onRedChanged();
onYellowChanged();
}
// event listeners
r1.controls.addEventListener('OnScroll', onScroll);
r2.controls.addEventListener('OnScroll', onScroll);
r3.controls.addEventListener('OnScroll', onScroll);
function windowResize2D(rendererObj) {
rendererObj.camera.canvas = {
width: rendererObj.domElement.clientWidth,
height: rendererObj.domElement.clientHeight,
};
rendererObj.camera.fitBox(2, 1);
rendererObj.renderer.setSize(
rendererObj.domElement.clientWidth,
rendererObj.domElement.clientHeight);
// update info to draw borders properly
rendererObj.stackHelper.slice.canvasWidth =
rendererObj.domElement.clientWidth;
rendererObj.stackHelper.slice.canvasHeight =
rendererObj.domElement.clientHeight;
rendererObj.localizerHelper.canvasWidth =
rendererObj.domElement.clientWidth;
rendererObj.localizerHelper.canvasHeight =
rendererObj.domElement.clientHeight;
}
function onWindowResize() {
// update 3D
r0.camera.aspect = r0.domElement.clientWidth / r0.domElement.clientHeight;
r0.camera.updateProjectionMatrix();
r0.renderer.setSize(
r0.domElement.clientWidth, r0.domElement.clientHeight);
// update 2d
windowResize2D(r1);
windowResize2D(r2);
windowResize2D(r3);
}
window.addEventListener('resize', onWindowResize, false);
// load meshes on the stack is all set
let meshesLoaded = 0;
function loadSTLObject(object) {
const stlLoader = new THREE.STLLoader();
stlLoader.load(object.location, function(geometry) {
// 3D mesh
object.material = new THREE.MeshLambertMaterial({
opacity: object.opacity,
color: object.color,
clippingPlanes: [],
transparent: true,
});
object.mesh = new THREE.Mesh(geometry, object.material);
object.mesh.objRef = object;
const RASToLPS = new THREE.Matrix4();
RASToLPS.set(-1, 0, 0, 0,
0, -1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
// object.mesh.applyMatrix(RASToLPS);
r0.scene.add(object.mesh);
object.scene = new THREE.Scene();
// front
object.materialFront = new THREE.MeshBasicMaterial({
color: object.color,
side: THREE.FrontSide,
depthWrite: true,
opacity: 0,
transparent: true,
clippingPlanes: [],
});
object.meshFront = new THREE.Mesh(geometry, object.materialFront);
// object.meshFront.applyMatrix(RASToLPS);
object.scene.add(object.meshFront);
// back
object.materialBack = new THREE.MeshBasicMaterial({
color: object.color,
side: THREE.BackSide,
depthWrite: true,
opacity: object.opacity,
transparent: true,
clippingPlanes: [],
});
object.meshBack = new THREE.Mesh(geometry, object.materialBack);
// object.meshBack.applyMatrix(RASToLPS);
object.scene.add(object.meshBack);
sceneClip.add(object.scene);
meshesLoaded++;
onGreenChanged();
onRedChanged();
onYellowChanged();
// good to go
if (meshesLoaded === data.size) {
ready = true;
}
});
}
data.forEach(function(object, key) {
loadSTLObject(object);
});
})
.catch(function(error) {
window.console.log('oops... something went wrong...');
window.console.log(error);
});
};