Skip to content

Commit

Permalink
Add nii2mesh features
Browse files Browse the repository at this point in the history
  • Loading branch information
neurolabusc committed Jul 29, 2024
1 parent 9de1aa5 commit 00fd8b0
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 9 deletions.
56 changes: 54 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,67 @@
<label for="opacitySlider1">Overlay Opacity</label>
<input type="range" min="0" max="255" value="128" class="slider" id="opacitySlider1" />
&nbsp;
<button id="saveImgBtn">Save Overlay</button>
<label for="meshCheck">Show Mesh</label>
<input type="checkbox" id="meshCheck" checked />
<button id="saveImgBtn">Save Image</button>
<button id="createMeshBtn">Create Mesh</button>
<button id="saveMeshBtn">Save Mesh</button>
&nbsp;
<div id="loadingCircle" class="loading-circle hidden"></div>
</header>
<main id="canvas-container">
<canvas id="gl1"></canvas>
</main>
<footer id="intensity">&nbsp;</footer>
<dialog id="remeshDialog">
<form method="dialog">
<p>
<label id="isoLabel">Isosurface Threshold</label>
<input id="isoNumber" type="text" value="0.9">
<p>
<p>
<input type="checkbox" id="bubbleCheck" checked/>
<label>Fill bubbles</label>
</p>
<p>
<input type="checkbox" id="largestCheck" checked/>
<label>Largest cluster only</label>
</p>
<p>
<label for="smoothSlide">Smoothing</label>
<input
type="range"
min="0"
max="20"
value="5"
class="slider"
id="smoothSlide"
/>
</p>
<p>
<label>Simplify Percent (1..100)</label>
<input id="shrinkPct" type="number" min="1" value="30" max="100">
</p>
<button id="cancelBtn" formmethod="dialog">Cancel</button>
<button autofocus id="applyBtn" value="default">Apply</button>
</form>
</dialog>
<dialog id="saveDialog">
<form method="dialog">
<p>
<label>
Format:
<select id="formatSelect">
<option>MZ3 small and precise</option>
<option selected>OBJ widely supported</option>
<option>STL popular for printing</option>
</select>
</label>
</p>
<button id="cancelSaveBtn" formmethod="dialog">Cancel</button>
<button autofocus id="applySaveBtn" value="default">Save</button>
</form>
</dialog>
<script type="module" src="/main.js"></script>
</body>

</html>
101 changes: 100 additions & 1 deletion main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,104 @@
import { Niivue } from '@niivue/niivue'
import { Niivue, NVMeshUtilities } from '@niivue/niivue'
// IMPORTANT: we need to import this specific file.
import * as ort from "./node_modules/onnxruntime-web/dist/ort.all.mjs"

async function main() {
function removeExtension(filename) {
if (filename.endsWith('.gz')) {
filename = filename.slice(0, -3)
}
let lastDotIndex = filename.lastIndexOf('.')
if (lastDotIndex !== -1) {
filename = filename.slice(0, lastDotIndex)
}
return filename
}
const Nii2meshWorker = await new Worker('./nii2meshWorker.js')
let startTime = Date.now()
function meshStatus(isTimed = true) {
let str = `Mesh has ${nv1.meshes[0].pts.length / 3} vertices and ${nv1.meshes[0].tris.length / 3} triangles`
if (isTimed)
str += ` ${Date.now() - startTime}ms`
document.getElementById('intensity').innerHTML = str
}
async function loadMz3(meshBuffer) {
if (nv1.meshes.length > 0) {
nv1.removeMesh(nv1.meshes[0])
}
await nv1.loadFromArrayBuffer(meshBuffer, 'test.mz3')
// TODO: we should not have to reverse faces
// Check determinant for conformed image
nv1.reverseFaces(0)
loadingCircle.classList.add('hidden')
meshStatus(true)
}
Nii2meshWorker.onmessage = async function (e) {
if (e.data.blob instanceof Blob) {
var reader = new FileReader()
reader.onload = () => {
loadMz3(reader.result)
}
reader.readAsArrayBuffer(e.data.blob)
}
}
applyBtn.onclick = async function () {
if (nv1.volumes.length < 2) {
return
}
startTime = Date.now()
loadingCircle.classList.remove('hidden')
const niiBuffer = await nv1.saveImage({volumeByIndex: 1}).buffer
let nii = await new Blob([niiBuffer], {
type: 'application/octet-stream'
})
let inName = removeExtension(nv1.volumes[0].name) + '.nii'
let fileNii = await new File([nii], inName)
let outName = removeExtension(nv1.volumes[0].name) + '.mz3'
const isoValue = Number(isoNumber.value)
const largestCheckValue = largestCheck.checked
const bubbleCheckValue = bubbleCheck.checked
const shrinkValue = Math.min(Math.max(Number(shrinkPct.value) / 100, 0.01), 1)
const smoothValue = smoothSlide.value
Nii2meshWorker.postMessage({
blob: fileNii,
percentage: shrinkValue,
simplify_name: outName,
isoValue: isoValue,
onlyLargest: largestCheckValue,
fillBubbles: bubbleCheckValue,
postSmooth: smoothValue
})
}
createMeshBtn.onclick = function () {
if (nv1.volumes.length < 2) {
window.alert("Segmented image not loaded. Press the 'Segment' button.")
} else {
remeshDialog.show()
}
}
meshCheck.onchange = function () {
nv1.setMeshProperty(nv1.meshes[0].id, 'visible', this.checked)
}
saveMeshBtn.onclick = function () {
if (nv1.meshes.length < 1) {
window.alert("No mesh open for saving. Use 'Create Mesh'.")
} else {
saveDialog.show()
}
}
applySaveBtn.onclick = function () {
if (nv1.meshes.length < 1) {
return
}
let format = 'obj'
if (formatSelect.selectedIndex === 0) {
format = 'mz3'
}
if (formatSelect.selectedIndex === 2) {
format = 'stl'
}
NVMeshUtilities.saveMesh(nv1.meshes[0].pts, nv1.meshes[0].tris, `mesh.${format}`, true)
}
clipCheck.onchange = function () {
if (clipCheck.checked) {
nv1.setClipPlane([0, 0, 90])
Expand Down Expand Up @@ -50,6 +147,7 @@ async function main() {
window.alert('Please open a voxel-based image')
return
}
startTime = Date.now()
loadingCircle.classList.remove('hidden')
await closeAllOverlays()
await ensureConformed()
Expand Down Expand Up @@ -118,6 +216,7 @@ async function main() {
segmentImg.opacity = opacitySlider1.value / 255
await nv1.addVolume(segmentImg)
loadingCircle.classList.add('hidden')
document.getElementById('intensity').innerHTML = ` ${Date.now() - startTime}ms`
}
function handleLocationChange(data) {
document.getElementById("intensity").innerHTML = data.string
Expand Down
25 changes: 20 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@niivue/niivue": "^0.43.3",
"@niivue/niivue": "^0.44.2",
"onnxruntime-web": "^1.19.0-dev.20240713-281ed8c12d"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions public/nii2mesh.js

Large diffs are not rendered by default.

Binary file added public/nii2mesh.wasm
Binary file not shown.
55 changes: 55 additions & 0 deletions public/nii2meshWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
self.addEventListener('message', function(e) {
const file = e.data.blob
const percentage = e.data.percentage || 0.5
const simplify_name = e.data.simplify_name
const isoValue = e.data.isoValue || NaN
onlyLargest = e.data.onlyLargest || false
fillBubbles = e.data.fillBubbles || false
postSmooth = e.data.postSmooth || 0
verbose = e.data.berbose || true
prepare_and_simplify(file, percentage, simplify_name, isoValue, onlyLargest, fillBubbles, postSmooth, verbose)
}, false)

var Module = {
'print': function(text) {
console.log(text)
self.postMessage({"log":text})
}
}

self.importScripts("nii2mesh.js?rnd="+Math.random())

let last_file_name = undefined

function prepare_and_simplify(file, percentage, simplify_name, isoValue = 1, onlyLargest = false, fillBubbles = false , postSmooth = 0, verbose = true) {
var filename = file.name
// if simplify on the same file, don't even read the file
if (filename === last_file_name) {
console.log("skipping load and create data file")
simplify(filename, percentage, simplify_name, isoValue, onlyLargest, fillBubbles, postSmooth, verbose)
return
} else { // remove last file in memory
if (last_file_name !== undefined)
Module.FS_unlink(last_file_name)
}
last_file_name = filename
var fr = new FileReader()
fr.readAsArrayBuffer(file)
fr. onloadend = function (e) {
var data = new Uint8Array(fr.result)
Module.FS_createDataFile(".", filename, data, true, true)
simplify(filename, percentage, simplify_name, isoValue, onlyLargest, fillBubbles, postSmooth, verbose)
}
}

function simplify(filename, percentage, simplify_name, isoValue = 1, onlyLargest = false, fillBubbles = false , postSmooth = 0, verbose = true) {
Module.ccall("simplify", // c function name
undefined, // return
["string", "number", "string", "number","boolean","boolean","number","boolean"], // param
[filename, percentage, simplify_name, isoValue, onlyLargest, fillBubbles,postSmooth, verbose]
)
let out_bin = Module.FS_readFile(simplify_name)
// sla should work for binary mz3
let file = new Blob([out_bin], {type: 'application/sla'})
self.postMessage({"blob":file})
}

0 comments on commit 00fd8b0

Please sign in to comment.