diff --git a/.github/workflows/ghpages.yml b/.github/workflows/ghpages.yml new file mode 100644 index 0000000..3780672 --- /dev/null +++ b/.github/workflows/ghpages.yml @@ -0,0 +1,21 @@ +name: Build and Deploy +on: + push: + branches: + - main +jobs: + build-and-publish-live-demo: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install and Build + run: | + npm install + npm run build + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: demo # The branch the action should deploy to. + folder: dist # The folder the action should deploy. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0738cb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +dist \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb6ffd0 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# NiiVue + ScribblePromt + +TBD \ No newline at end of file diff --git a/docs/export.py b/docs/export.py new file mode 100644 index 0000000..7ea2422 --- /dev/null +++ b/docs/export.py @@ -0,0 +1,69 @@ +import torch +import torch.nn.functional as F +from scribbleprompt.unet import UNet + +class Predictor: + """ + wrapper for ScribblePrompt-UNet model with ONNX export functionality. + """ + def __init__(self, path: str, verbose: bool = True): + self.path = path + self.verbose = verbose + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + self.build_model() + self.load() + self.model.eval() + self.to_device() + + def build_model(self): + """ + build the ScribblePrompt-UNet model. + """ + self.model = UNet( + in_channels=5, + out_channels=1, + features=[192, 192, 192, 192], + ) + + def load(self): + """ + load the state of the model from a checkpoint file. + """ + with open(self.path, "rb") as f: + state = torch.load(f, map_location=self.device) + self.model.load_state_dict(state, strict=True) + if self.verbose: + print(f"loaded checkpoint from {self.path} to {self.device}") + + def to_device(self): + """ + move the model to the appropriate device. + """ + self.model = self.model.to(self.device) + + def export_to_onnx(self, onnx_path="model.onnx"): + """ + export the model to ONNX format with dynamic H and W (height and width). + """ + # prepare a dummy input with arbitrary H and W, as ONNX export requires a concrete input shape + dummy_input = torch.randn(1, 5, 256, 256).to(self.device) + torch.onnx.export( + self.model, + dummy_input, + onnx_path, + export_params=True, + opset_version=20, + do_constant_folding=True, + input_names=['input'], + output_names=['output'], + # make H and W dynamic, along with the batch size + dynamic_axes={'input': {0: 'batch_size', 2: 'height', 3: 'width'}, + 'output': {0: 'batch_size', 2: 'height', 3: 'width'}} + ) + print(f"model exported to {onnx_path}") + +# usage from CLI +if __name__ == "__main__": + checkpoint_path = "../checkpoints/ScribblePrompt_unet_v1_nf192_res128.pt" + predictor = Predictor(checkpoint_path) + predictor.export_to_onnx("scribbleprompt_unet.onnx") \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3dff415 --- /dev/null +++ b/index.html @@ -0,0 +1,39 @@ + + + + + + + + Niivue ScribblePromt + + + +
+ + + + + +   + + +   + + + +   + + + + +   + +
+
+ +
+ + + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..5fea992 --- /dev/null +++ b/main.js @@ -0,0 +1,384 @@ +import { Niivue, DRAG_MODE, SLICE_TYPE, MULTIPLANAR_TYPE, SHOW_RENDER } from '@niivue/niivue' +import * as ort from "./node_modules/onnxruntime-web/dist/ort.all.mjs" + +const nv = new Niivue() +const isiPad = navigator.userAgent.match(/iPad/i) != null + +const onnxOptions = { + executionProviders: [ + { + name: 'webgpu', + }, + { + name: 'wasm', + } + ], + graphOptimizationLevel: 'disabled' +} // n.b. in future graphOptimizationLevel extended + +const onClipChange = (e) => { + if (e.target.checked) { + nv.setClipPlane([0, 0, 90]) + } else { + nv.setClipPlane([2, 0, 90]) + } +} + +const onBackOpacityChange = (e) => { + nv.setOpacity(0, e.target.value / 255) + nv.updateGLVolume() +} + +const onOverlayOpacityChange = (e) => { + nv.setOpacity(1, e.target.value / 255) + nv.updateGLVolume() +} + +const onDrawingOpacityChange = (e) => { + nv.drawOpacity = e.target.value / 255 + nv.drawScene() +} + +const onImageLoaded = async () => { + // if more than one image aleady, then just return. We only run + // this if the user drags and drops a new image. + if (nv.volumes.length > 1) { + return + } + // check input image dimensions. If they are not equal then + // give the user a diaglog to conform the image. + const inDims = nv.volumes[0].dims + const isEqualDims = inDims[1] === inDims[2] && inDims[2] === inDims[3] + // if not equal then give the user a dialog to conform the image + if (!isEqualDims) { + const userConfirmed = window.confirm('The input image dimensions are not equal. Would you like to conform the image?') + if (userConfirmed) { + // fire off the conform check event + conform.checked = true + await onConformChange({ target: { checked: true } }) + } else { + window.alert('Please conform the image before proceeding. The segmentation will not work correctly.') + } + } else { + // clone the loaded image to create an overlay that stores the segmentation + const overlay = nv.cloneVolume(0) + overlay.img.fill(0) // fill with zeros since it will hold binary segmentation later + overlay.opacity = 0.8 + // add the overlay to niivue + nv.addVolume(overlay) + } + // set drawing enabled to true + // if ipad, set to false by default + // if (isiPad) { + // nv.setDrawingEnabled(false) + // } else { + // nv.setDrawingEnabled(true) + // } + nv.setDrawingEnabled(true) +} + +const onConformChange = async (e) => { + if (nv.volumes.length < 1) { + return + } + if (e.target.checked) { + closeAllOverlays() + await ensureConformed() + nv.closeDrawing() + nv.createEmptyDrawing() + // clone the loaded image to create an overlay that stores the segmentation + const overlay = nv.cloneVolume(0) + overlay.img.fill(0) // fill with zeros since it will hold binary segmentation later + overlay.opacity = 0.5 + // add the overlay to niivue + nv.addVolume(overlay) + } +} + +const onSaveImageClick = () => { + nv.volumes[1].saveToDisk('segmentation.nii') +} + +const ensureConformed = async () => { + const nii = nv.volumes[0] + let isConformed = nii.dims[1] === 256 && nii.dims[2] === 256 && nii.dims[3] === 256 + if (nii.permRAS[0] !== -1 || nii.permRAS[1] !== 3 || nii.permRAS[2] !== -2) { + isConformed = false + } + if (isConformed) { + return + } + const nii2 = await nv.conform(nii, true, true, false) + nv.removeVolume(nv.volumes[0]) + nv.addVolume(nii2) +} + +const closeAllOverlays = () => { + while (nv.volumes.length > 1) { + nv.removeVolume(nv.volumes[1]) + } +} + +const showLoadingCircle = () => { + loadingCircle.classList.remove('hidden') +} + +const hideLoadingCircle = () => { + loadingCircle.classList.add('hidden') +} + +const getSlice = (volIdx, start, end) => { + const slice = nv.volumes[volIdx].getVolumeData(start, end) + const data = slice[0] + const dims = slice[1] + const axcorsag = nv.tileIndex(...nv.mousePos) + const ax = 0 + const cor = 1 + const sag = 2 + // dims will hold H and W, but we need to determine which is which based + // on the plane the user is drawing on. + // if the user is drawing on the axial plane, then the first dimension is W + // and the second dimension is H. + // if the user is drawing on the coronal plane, then the first dimension is W + // and the third dimension is H + // if the user is drawing on the sagittal plane, then the second dimension is W + // and the third dimension is H. + // we need to determine which plane the user is drawing on, and then set the + // dimensions accordingly. + if (axcorsag === ax) { + return { data, dims: [dims[0], dims[1]] } + } else if (axcorsag === cor) { + return { data, dims: [dims[0], dims[2]] } + } else if (axcorsag === sag) { + return { data, dims: [dims[2], dims[1]] } + } +} + +const copyDrawingToOverlay = (drawing, overlay) => { + for (let i = 0; i < drawing.length; i++) { + if (drawing[i] > 0) { + overlay.img[i] = 1 + } else { + overlay.img[i] = overlay.img[i] === 1 ? 1 : 0 + } + } +} + +const normalize = (img) => { + // TODO: ONNX not JavaScript https://onnx.ai/onnx/operators/onnx_aionnxml_Normalizer.html + let mx = img[0] + let mn = mx + for (let i = 0; i < img.length; i++) { + mx = Math.max(mx, img[i]) + mn = Math.min(mn, img[i]) + } + let scale = 1 / (mx - mn) + for (let i = 0; i < img.length; i++) { + img[i] = (img[i] - mn) * scale + } +} + +// drawing pen can be 1, 2, 3, (e.g. red, green, blue) +const binarizeDrawing = (drawing) => { + for (let i = 0; i < drawing.length; i++) { + drawing[i] = drawing[i] > 0 ? 1 : 0 + } +} + +const sigmoid = (x) => { + return 1 / (1 + Math.exp(-x)) +} + +// when user is done making scribbles, run the segmentation +const doSegment = async (start, end) => { + // show loading circle + showLoadingCircle() + // background image index + const backIdx = 0 + // get the slice data from the active slice the user was drawing on + const inputSlice = getSlice(backIdx, start, end) + // get W, H, D of the slice + const [W, H] = inputSlice.dims + // get reference to the overlay volume for later + const overlay = nv.volumes[1] + // make sure slice data is a Float32Array + const inputSlice32 = new Float32Array(inputSlice.data) + // get reference to drawing from user clicks + const drawing = nv.drawBitmap + // is there a drawing? detect if any elements are greater than 0 + const hasDrawing = drawing.some(v => v > 0) + // if no drawing, then return + if (!hasDrawing) { + hideLoadingCircle() + return + } + copyDrawingToOverlay(drawing, overlay) + // get the the slice data from the drawing overlay that + // has now been populated from the drawBitmap (which came from user clicks) + const drawingSlice = getSlice(1, start, end) + const drawingSlice32 = new Float32Array(drawingSlice.data) + // binarize the drawing + binarizeDrawing(drawingSlice32) + // normalize input data to range 0..1 + normalize(inputSlice32) // no return value since it does this in place + + // create onnx runtime session for running inference + const session = await ort.InferenceSession.create( + './scribbleprompt_unet.onnx', + onnxOptions + ) + // expected scribble shape = [B, 5, H, W] + const HW = H * W + const shape = [1, 5, H, W] + const componentShape = [1, 1, H, W] + // create tensor with correct shape filled with zeros + const nvox = shape.reduce((a, b) => a * b) + const inputTensor = new ort.Tensor('float32', new Float32Array(nvox), shape) + // make 5 tensors: img, box, click, scribble, mask + // img is the input slice data from the background image + const imgTensor = new ort.Tensor('float32', inputSlice32, componentShape) + // box not used + const boxTensor = new ort.Tensor('float32', new Float32Array(HW).fill(0), componentShape) + // clicks are the user clicks (the drawing from niivue) + const clickTensor = new ort.Tensor('float32', drawingSlice32, componentShape) + // scribble is not used. Not sure what the difference between clicks and scribble is, + // but using drawingSlice32 for scribbles does not work as expected. + const scribbleTensor = new ort.Tensor('float32', new Float32Array(HW).fill(0), componentShape) + // mask is the previous mask from scribble inference. Not used here, but perhaps could be + const maskTensor = new ort.Tensor('float32', new Float32Array(HW).fill(0), componentShape) + + // now concatenate the tensors for input to the model + inputTensor.data.set(imgTensor.data, 0) + inputTensor.data.set(boxTensor.data, HW) + inputTensor.data.set(clickTensor.data, 2 * HW) + inputTensor.data.set(scribbleTensor.data, 3 * HW) + inputTensor.data.set(maskTensor.data, 4 * HW) + const modelInputs = { "input": inputTensor } + + // run onnx inference + // get time stamp for timing + const t0 = performance.now() + const results = await session.run(modelInputs) + const t1 = performance.now() + const inferenceTime = t1 - t0 + console.log(`inference time: ${inferenceTime} ms`) + + // output has shape [1, 1, H, W] + const outSliceData = results.output.cpuData + const threshold = 0.5 + // model returns logits, so we need to apply sigmoid. + for (let i = 0; i < outSliceData.length; i++) { + outSliceData[i] = sigmoid(outSliceData[i]) + outSliceData[i] = outSliceData[i] < threshold ? 0 : 1 + // combine the segmentation with the the original drawing + if (drawingSlice32[i] === 1) { + outSliceData[i] = 1 + } + } + + // update the overlay slice with the segmentation + overlay.setVolumeData(start, end, outSliceData) + // make sure the overlay colormap is red + overlay.setColormap('red') + nv.updateGLVolume() + // reset the drawing for the next call + nv.closeDrawing() + nv.createEmptyDrawing() + // hide the loading circle + hideLoadingCircle() + +} + +const onSegmentClick = async () => { + if (nv.volumes.length < 1) { + window.alert('Please open a voxel-based image') + return + } + const vox = nv.frac2vox(nv.scene.crosshairPos, 0) + const dims = nv.volumes[0].dims + // const start = [0, 0, vox[2]] + // const end = [dims[1] - 1, dims[2] - 1, vox[2]] + const axcorsag = nv.tileIndex(...nv.mousePos) + const ax = 0 + const cor = 1 + const sag = 2 + // determine which plane the user is drawing on, and set the start and end + // to the full slice of that plane + let start, end = [0, 0, 0] + if (axcorsag === ax) { + start = [0, 0, vox[2]] + end = [dims[1] - 1, dims[2] - 1, vox[2]] + } else if (axcorsag === cor) { + start = [0, vox[1], 0] + end = [dims[1] - 1, vox[1], dims[3] - 1] + } else if (axcorsag === sag) { + start = [vox[0], 0, 0] + end = [vox[0], dims[2] - 1, dims[3] - 1] + } + // run the segmentation and send the slice info + await doSegment(start, end) +} + +const handleLocationChange = (data) => { + document.getElementById("intensity").innerHTML = data.string +} + +async function main() { + // set callback for clip plane checkbox + clipCheck.onchange = onClipChange + // set callback for back opacity slider + opacitySlider0.onchange = onBackOpacityChange + // set callback for overlay opacity slider + opacitySlider1.onchange = onOverlayOpacityChange + // set callback for drawing opacity slider + opacitySlider2.onchange = onDrawingOpacityChange + // set callback for save image button (saves the segmentation) + saveImgBtn.onclick = onSaveImageClick + // set callback for segment button + segmentBtn.onclick = onSegmentClick + // set callback for conform checkbox + conform.onchange = onConformChange + // bind "s" key up to onSegmentClick also + document.addEventListener('keyup', (e) => { + if (e.key === 's') { + onSegmentClick() + } + }) + // bind shift + z to undo + document.addEventListener('keyup', (e) => { + if (e.key === 'z') { + nv.drawUndo() + } + }) + // set callback for when the crosshair moves + // to report the intensity of the voxel under the crosshair + // and the location. + nv.onLocationChange = handleLocationChange + // get canvas element + const gl1 = document.getElementById('gl1') + nv.attachToCanvas(gl1) + nv.setDrawingEnabled(false) + nv.opts.maxDrawUndoBitmaps = 30 + nv.opts.multiplanarForceRender = true + nv.opts.yoke3Dto2DZoom = true + nv.opts.crosshairGap = 5 + nv.setInterpolation(false) // use nearest neighbor + nv.onImageLoaded = onImageLoaded + await nv.loadVolumes([{ url: './stroke.nii.gz' }]) + nv.setPenValue(2, false) + nv.drawOpacity = 1 + nv.setMultiplanarLayout(MULTIPLANAR_TYPE.GRID) + nv.setSliceType(SLICE_TYPE.MULTIPLANAR) + nv.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS + nv.opts.dragMode = DRAG_MODE.slicer3D + + // on mouse up dosegment + window.addEventListener('pointerup', async function(event) { + if (event.pointerType === 'pen') { + await onSegmentClick() + } + }) + +} + +main() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b94ce46 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1155 @@ +{ + "name": "niivue-scribble-prompt", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "niivue-scribble-prompt", + "version": "1.0.0", + "dependencies": { + "@niivue/niivue": "^0.45.1", + "onnxruntime-web": "^1.19.2" + }, + "devDependencies": { + "vite": "^5.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@lukeed/uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lukeed/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@niivue/niivue": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@niivue/niivue/-/niivue-0.45.1.tgz", + "integrity": "sha512-1KfuiIbdJr0fi9Odv/Wjrej580B295q+fE+PAIW9L5ZrJhCmsMzYgxYecJGcj67tu8mcwQkKjbLaO6VxdVS3+g==", + "license": "BSD-2-Clause", + "dependencies": { + "@lukeed/uuid": "^2.0.1", + "@ungap/structured-clone": "^1.2.0", + "array-equal": "^1.0.2", + "daikon": "^1.2.46", + "fflate": "^0.8.2", + "gl-matrix": "^3.4.3", + "nifti-reader-js": "^0.6.8", + "rxjs": "^7.8.1" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.18.1" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.0.tgz", + "integrity": "sha512-MOdOibwBs6KW1vfqz2uKMlxq5xAfAZ98SZjO8e3XnAbFnTJtAspqhWk7hrdSAs9/Y14ZWMiy7/MxMUzAOadYEw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "license": "ISC" + }, + "node_modules/@wearemothership/dicom-character-set": { + "version": "1.0.4-opt.1", + "resolved": "https://registry.npmjs.org/@wearemothership/dicom-character-set/-/dicom-character-set-1.0.4-opt.1.tgz", + "integrity": "sha512-stqhnpawYHY2UZKj4RHTF71ab3q3z8S1SO9ToQKjsHQwowUdFVo6YFea93psFux3yqNbRlQjwoCdPjHcD0YQzw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/array-equal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.2.tgz", + "integrity": "sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, + "node_modules/daikon": { + "version": "1.2.46", + "resolved": "https://registry.npmjs.org/daikon/-/daikon-1.2.46.tgz", + "integrity": "sha512-S8dTTlsWYTH3LQztjTW9KnNvxDeL2mr2cau0auLdYMJe4TrocYP1PmidHizO3rXUs+gXpBWI1PQ2qvB4b21QFw==", + "license": "MIT", + "dependencies": { + "@wearemothership/dicom-character-set": "^1.0.4-opt.1", + "fflate": "*", + "jpeg-lossless-decoder-js": "2.0.7", + "pako": "^2.1", + "xss": "1.0.14" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/flatbuffers": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", + "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==", + "license": "SEE LICENSE IN LICENSE.txt" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", + "license": "MIT" + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/jpeg-lossless-decoder-js": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/jpeg-lossless-decoder-js/-/jpeg-lossless-decoder-js-2.0.7.tgz", + "integrity": "sha512-tbZlhFkKmx+JaqVMkq47SKWGuXLkIaV8fTbnhO39dYEnQrSShLGuLCGb0n6ntXjtmk6oAWGiIriWOLwj9od0yQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nifti-reader-js": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/nifti-reader-js/-/nifti-reader-js-0.6.8.tgz", + "integrity": "sha512-yIKNVzYFiUcSHazoR+sd6Ka7sUmZTabaVqJRFxbdlAKR1hnPBuNP71g3AyApo37nJ3k41c632QPij5q7gF1YPQ==", + "license": "MIT", + "dependencies": { + "fflate": "*" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.19.2.tgz", + "integrity": "sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q==", + "license": "MIT" + }, + "node_modules/onnxruntime-web": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.19.2.tgz", + "integrity": "sha512-r0ok6KpTUXR4WA+rHvUiZn7JoH02e8iS7XE1p5bXk7q3E0UaRFfYvpMNUHqEPiTBMuIssfBxDCQjUihV8dDFPg==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^1.12.0", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.19.2", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/xss": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz", + "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3d1091c --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "niivue-scribble-prompt", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build && npm run copyMJS && npm run copyWASM", + "copyMJS": "cp ./node_modules/onnxruntime-web/dist/*.mjs ./dist/assets/", + "copyWASM": "cp ./node_modules/onnxruntime-web/dist/*.wasm ./dist/assets/", + "preview": "vite preview" + }, + "dependencies": { + "@niivue/niivue": "^0.45.1", + "onnxruntime-web": "^1.19.2" + }, + "devDependencies": { + "vite": "^5.2.0" + } + } + \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..67916f4 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/niivue.css b/public/niivue.css new file mode 100644 index 0000000..4c13be6 --- /dev/null +++ b/public/niivue.css @@ -0,0 +1,130 @@ +html { + height: auto; + min-height: 100%; + margin: 0; +} +body { + display: flex; + flex-direction: column; + margin: 0; + min-height: 100%; + width: 100%; + position: absolute; + font-family: system-ui, Arial, Helvetica, sans-serif; + user-select: none; /* Standard syntax */ + color: white; + background: #303030; +} +header { + margin: 10px; +} +main { + flex: 1; + background: #000000; + position: relative; +} +footer { + margin: 10px; +} +canvas { + position: absolute; + cursor: crosshair; +} +canvas:focus { + outline: 0px; +} +div { + display: table-row; +} +.dropdown { + float: left; + overflow: hidden; +} +.dropdown .dropbtn { + font-size: 16px; + border: none; + outline: none; + color: white; + padding: 12px 12px; + background-color: #303030; + font-family: inherit; + margin: 0; +} +.dropdown:hover .dropbtn { + background-color: #9a9; +} +.dropdown-content { + display: none; + position: absolute; + background-color: #303030; + min-width: 160px; + border-radius: 5px; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; +} +.dropdown-content a { + float: none; + color: white; + padding: 12px 16px; + text-decoration: none; + display: block; + text-align: left; + line-height: 6px; +} +.dropdown-content a:hover { + background-color: #aba; +} +.dropdown:hover .dropdown-content { + display: block; +} +.dropdown-item-checked::before { + position: absolute; + left: 0.2rem; + content: "\2022"; /* or '✓' */ + font-weight: 600; +} +.divider { + border-top: 1px solid grey; +} +.vertical-divider { + border-left: 1px solid grey; + height: 40px; +} +.help-text { + margin: auto; + max-width: 150px; + padding: 0 10px; +} +.slidecontainer { + padding: 10px 10px; + white-space: normal; + word-break: break-word; + display: flex; + align-items: center; + flex: 0 0 auto; +} + +div.footer { width: 100%; display: block; background: #303030;} +table.footer { width: 100%;height: 100%; table-layout: fixed;} + +.loading-circle { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; + margin-left: 10px; + vertical-align: middle; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/public/scribbleprompt_unet.onnx b/public/scribbleprompt_unet.onnx new file mode 100644 index 0000000..95f22a8 Binary files /dev/null and b/public/scribbleprompt_unet.onnx differ diff --git a/public/stroke.nii.gz b/public/stroke.nii.gz new file mode 100644 index 0000000..72b9ec1 Binary files /dev/null and b/public/stroke.nii.gz differ diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..2be600b --- /dev/null +++ b/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + root: '.', + base: './', + server: { + open: 'index.html', + }, + optimizeDeps: { + exclude: ['onnxruntime-web'] + }, + worker: { + format: 'es' + } +}) \ No newline at end of file