Skip to content

Commit

Permalink
Multiple models, do not simplify by default
Browse files Browse the repository at this point in the history
  • Loading branch information
neurolabusc committed Jul 17, 2024
1 parent fd344c8 commit 7261977
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 417 deletions.
58 changes: 47 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,28 @@

<body>
<header>
<label for="volumeSelect">Volume</label>
<select id="volumeSelect">
<option selected>bet</option>
<option>Iguana</option>
<option>CT_Philips</option>
<option>mni152</option>
<option>wm.mgz</option>
<option>lh.pial</option>
<option>fs_LR.32k.L.inflated.surf.gii</option>
</select>
<label for="shaderSelect">Shader</label>
<select id="shaderSelect">
<option value="Flat">Flat</option>
<option value="Matcap" selected>Matcap</option>
<option value="Matte">Matte</option>
<option value="Phong">Phong</option>
</select>
<label for="visibleCheck">Mesh Visible</label>
<input type="checkbox" id="visibleCheck" checked />
<button id="remeshBtn">Create Mesh</button>
<button id="simplifyBtn">Simplify Mesh</button>
<button id="saveBtn">Save Mesh</button>
<button id="aboutBtn">About Mesh</button>
<div id="loadingCircle" class="loading-circle hidden"></div>
</header>
<main id="canvas-container">
Expand All @@ -24,27 +41,46 @@
<dialog id="remeshDialog">
<form method="dialog">
<p>
<label for="isoSlide">Isosurface boundary</label>
<input type="range" min="0" max="255" value="108" class="slider" id="isoSlide" />
<label id="isoLabel">Isosurface Threshold</label>
<input id="isoNumber" type="text" value="100">
<p>
<p>
<input type="checkbox" id="bubbleCheck" checked/>
<label>Fill bubbles</label>
</p>
<p>
<label>
<input type="checkbox" id="bubbleCheck" checked /> Fill bubbles
</label>
<input type="checkbox" id="largestCheck" unchecked/>
<label>Largest cluster only</label>
</p>
<p>
<label>
<input type="checkbox" id="largestCheck" /> Largest cluster only
</label>
<label for="smoothSlide">Smoothing</label>
<input
type="range"
min="0"
max="20"
value="5"
class="slider"
id="smoothSlide"
/>
</p>
<p>
<label for="shrinkSlide">Simplify mesh</label>
<input type="range" min="2" max="100" value="20" class="slider" id="shrinkSlide" />
<label>Simplify Percent (1..100)</label>
<input id="shrinkPct" type="number" min="1" value="100" max="100">
</p>
<button id="cancelBtn" formmethod="dialog">Cancel</button>
<button autofocus id="applyBtn" value="default">Apply</button>
</form>
</dialog>
<dialog id="simplifyDialog">
<form method="dialog">
<p>
<label>Simplify Percent (1..100)</label>
<input id="shrinkSimplePct" type="number" min="1" value="10" max="100">
</p>
<button id="cancelBtn" formmethod="dialog">Cancel</button>
<button autofocus id="applySimpleBtn" value="default">Apply</button>
</form>
</dialog>
<dialog id="saveDialog">
<form method="dialog">
<p>
Expand Down
277 changes: 186 additions & 91 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,119 +1,214 @@
import { Niivue } from '@niivue/niivue';
import { createMZ3, downloadMesh } from './marching-cubes.js';
import { Niivue, NVMeshUtilities } from '@niivue/niivue'
// Dynamically load the worker modules
async function createVoxelWorker() {
if (window.Worker) {
const { default: VoxelWorker } = await import('./voxelWorker?worker')
return new VoxelWorker()
}
throw new Error('Web Workers are not supported in this environment.')
}

// Dynamically load the worker module
async function createWorker() {
async function createMeshWorker() {
if (window.Worker) {
const { default: MeshWorker } = await import('./meshWorker?worker');
return new MeshWorker();
const { default: MeshWorker } = await import('./meshWorker?worker')
return new MeshWorker()
}
throw new Error('Web Workers are not supported in this environment.');
throw new Error('Web Workers are not supported in this environment.')
}

function formatNumber(value) {
if (Math.abs(value) >= 1) {
// For numbers >= 1, use up to 1 decimal place
return value.toFixed(1)
} else {
// For numbers < 1, use up to 3 significant digits
return value.toPrecision(3)
}
}
async function main() {
const worker = await createWorker();
const loadingCircle = document.getElementById('loadingCircle');

worker.onmessage = async function (e) {
const { vertices, triangles } = e.data;

const verticesArray = new Float32Array(vertices);
const trianglesArray = new Uint32Array(triangles);

const meshBuffer = createMZ3(verticesArray, trianglesArray, false);
await nv1.loadFromArrayBuffer(meshBuffer, 'test.mz3');
loadingCircle.classList.add('hidden');
};

const MeshWorker = await createMeshWorker()
const VoxelWorker = await createVoxelWorker()
const loadingCircle = document.getElementById('loadingCircle')
function meshStatus() {
const str = `Mesh has ${nv1.meshes[0].pts.length / 3} vertices and ${nv1.meshes[0].tris.length / 3} triangles`
document.getElementById('location').innerHTML = str
shaderSelect.onchange()
nv1.setMeshProperty(nv1.meshes[0].id, 'visible', visibleCheck.checked)
}
async function loadMesh(vertices, triangles) {
if (nv1.meshes.length > 0) {
nv1.removeMesh(nv1.meshes[0])
}
const verticesArray = new Float32Array(vertices)
const trianglesArray = new Uint32Array(triangles)
const meshBuffer = NVMeshUtilities.createMZ3(verticesArray, trianglesArray, false)
await nv1.loadFromArrayBuffer(meshBuffer, 'test.mz3')
loadingCircle.classList.add('hidden')
meshStatus()
}
MeshWorker.onmessage = async function (e) {
const { vertices, triangles } = e.data
await loadMesh(vertices, triangles)
}
VoxelWorker.onmessage = async function (e) {
const { vertices, triangles } = e.data
await loadMesh(vertices, triangles)
}
saveBtn.onclick = function () {
if (nv1.meshes.length < 1) {
window.alert("No mesh open for saving. Use 'Create Mesh'.");
window.alert("No mesh open for saving. Use 'Create Mesh'.")
} else {
saveDialog.show();
saveDialog.show()
}
};

}
volumeSelect.onchange = function () {
const selectedOption = volumeSelect.options[volumeSelect.selectedIndex]
const txt = selectedOption.text
let fnm = './' + txt
if (volumeSelect.selectedIndex > 3) {
fnm = 'https://niivue.github.io/niivue/images/' + txt
} else if (volumeSelect.selectedIndex > 0) {
fnm = 'https://niivue.github.io/niivue-demo-images/' + txt
}
if (nv1.meshes.length > 0) {
nv1.removeMesh(nv1.meshes[0])
}
if (nv1.volumes.length > 0) {
nv1.removeVolumeByIndex(0)
}
if (volumeSelect.selectedIndex > 4) {
nv1.loadMeshes([{ url: fnm }])
} else {
if (!fnm.endsWith('.mgz')) {
fnm += '.nii.gz'
}
nv1.loadVolumes([{ url: fnm }])
}
}
applySaveBtn.onclick = function () {
if (nv1.meshes.length < 1) {
return;
}
if (formatSelect.selectedIndex == 0) {
downloadMesh(nv1.meshes[0].pts, nv1.meshes[0].tris, 'simplified_mesh.mz3', true);
return
}
if (formatSelect.selectedIndex == 1) {
downloadMesh(nv1.meshes[0].pts, nv1.meshes[0].tris, 'simplified_mesh.obj');
let format = 'obj'
if (formatSelect.selectedIndex === 0) {
format = 'mz3'
}
if (formatSelect.selectedIndex == 2) {
downloadMesh(nv1.meshes[0].pts, nv1.meshes[0].tris, 'simplified_mesh.stl');
if (formatSelect.selectedIndex === 2) {
format = 'stl'
}
};

NVMeshUtilities.saveMesh(nv1.meshes[0].pts, nv1.meshes[0].tris, `mesh.${format}`, true)
}
remeshBtn.onclick = function () {
remeshDialog.show();
};

applyBtn.onclick = async function () {
if (nv1.meshes.length > 0) {
nv1.removeMesh(nv1.meshes[0]);
if (nv1.volumes.length < 1) {
window.alert('No voxel-based image open for meshing. Drag and drop an image.')
} else {
remeshDialog.show()
}
const img = new Uint8ClampedArray(nv1.volumes[0].img);
const dims = [
nv1.volumes[0].hdr.dims[1],
nv1.volumes[0].hdr.dims[2],
nv1.volumes[0].hdr.dims[3]
];
const isoValue = isoSlide.value;
const largestCheckValue = largestCheck.checked;
const bubbleCheckValue = bubbleCheck.checked;
const affine = nv1.volumes[0].hdr.affine;
const shrinkValue = shrinkSlide.value / shrinkSlide.max;

loadingCircle.classList.remove('hidden');

worker.postMessage({
img: img.buffer,
dims,
isoValue,
largestCheck: largestCheckValue,
bubbleCheck: bubbleCheckValue,
affine,
shrinkValue
}, [img.buffer]);
};

aboutBtn.onclick = function () {
}
simplifyBtn.onclick = function () {
if (nv1.meshes.length < 1) {
window.alert("No mesh open for saving. Use 'Create Mesh'.");
window.alert('No mesh open to simplify. Drag and drop a mesh or create a mesh from a voxel based image.')
} else {
window.alert(
`Mesh has ${nv1.meshes[0].pts.length / 3} vertices and ${nv1.meshes[0].tris.length / 3} triangles`
);
simplifyDialog.show()
}
};

}
applySimpleBtn.onclick = async function () {
if (nv1.meshes.length < 1) {
console.log('No mesh open to simplify.')
return
}
const verts = nv1.meshes[0].pts.slice()
const tris = nv1.meshes[0].tris.slice()
const shrinkValue = Math.min(Math.max(Number(shrinkSimplePct.value) / 100, 0.01), 1)
loadingCircle.classList.remove('hidden')
MeshWorker.postMessage({
verts,
tris,
shrinkValue
})
}
applyBtn.onclick = async function () {
if (nv1.volumes.length < 1) {
console.log('No volume open to meshify.')
return
}
const isoValue = Number(isoNumber.value)
let img = new Float32Array(nv1.volumes[0].img)
const scl_slope = nv1.volumes[0].hdr.scl_slope
const scl_inter = nv1.volumes[0].hdr.scl_inter
if (scl_slope !== 1.0 || scl_inter !== 0) {
img = new Float32Array(img)
for (let i = 0; i < img.length; i++) {
img[i] = img[i] * scl_slope + scl_inter
}
}
const dims = [nv1.volumes[0].hdr.dims[1], nv1.volumes[0].hdr.dims[2], nv1.volumes[0].hdr.dims[3]]
const largestCheckValue = largestCheck.checked
const bubbleCheckValue = bubbleCheck.checked
const affine = nv1.volumes[0].hdr.affine
const shrinkValue = Math.min(Math.max(Number(shrinkPct.value) / 100, 0.01), 1)
const smoothValue = smoothSlide.value
loadingCircle.classList.remove('hidden')
VoxelWorker.postMessage(
{
img,
dims,
isoValue,
largestCheck: largestCheckValue,
bubbleCheck: bubbleCheckValue,
smoothValue,
affine,
shrinkValue
},
[img.buffer]
)
}
visibleCheck.onchange = function () {
nv1.setMeshProperty(nv1.meshes[0].id, 'visible', this.checked);
};

nv1.setMeshProperty(nv1.meshes[0].id, 'visible', this.checked)
}
function handleLocationChange(data) {
document.getElementById('location').innerHTML = '&nbsp;&nbsp;' + data.string;
document.getElementById('location').innerHTML = '&nbsp;&nbsp;' + data.string
}
shaderSelect.onchange = function () {
nv1.setMeshShader(nv1.meshes[0].id, this.value)
}
function handleMeshLoaded() {
meshStatus()
}

const defaults = {
onMeshLoaded: handleMeshLoaded,
onLocationChange: handleLocationChange,
backColor: [0.2, 0.2, 0.3, 1],
show3Dcrosshair: true
};
const nv1 = new Niivue(defaults);
nv1.attachToCanvas(gl1);
nv1.setClipPlane([0.2, 0, 120]);
nv1.opts.dragMode = nv1.dragModes.pan;
nv1.setRenderAzimuthElevation(245, 15);
nv1.opts.multiplanarForceRender = true;
nv1.opts.yoke3Dto2DZoom = true;
nv1.opts.crosshairGap = 11;
nv1.setInterpolation(true);
await nv1.loadVolumes([{ url: './bet.nii.gz' }]);
applyBtn.onclick();
}
const nv1 = new Niivue(defaults)
nv1.attachToCanvas(gl1)
nv1.isAlphaClipDark = true
function imageStatus() {
const otsu = nv1.findOtsu(3)
isoLabel.textContent =
'Isosurface Threshold (' +
formatNumber(nv1.volumes[0].cal_min) +
'...' +
formatNumber(nv1.volumes[0].cal_max) +
')'
isoNumber.value = formatNumber(otsu[1])
const str = `Image has ${nv1.volumes[0].dims[1]}×${nv1.volumes[0].dims[2]}×${nv1.volumes[0].dims[3]} voxels`
document.getElementById('location').innerHTML = str
nv1.setSliceType(nv1.sliceTypeMultiplanar)
}
nv1.onImageLoaded = () => {
imageStatus()
}
nv1.setClipPlane([0.2, 0, 120])
nv1.opts.dragMode = nv1.dragModes.pan
nv1.setRenderAzimuthElevation(245, 15)
nv1.opts.multiplanarForceRender = true
nv1.opts.yoke3Dto2DZoom = true
nv1.setInterpolation(true)
await nv1.loadVolumes([{ url: './bet.nii.gz' }])
imageStatus()
applyBtn.onclick()
}

main();
main()
Loading

0 comments on commit 7261977

Please sign in to comment.