spikedoanz commited on
Commit
8bbab59
·
1 Parent(s): a8afc85

MindGrab-only WebGPU Space

Browse files
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.safetensors filter=lfs diff=lfs merge=lfs -text
2
+ *.nii.gz filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .DS_Store
2
+ dist
3
+ node_modules
LICENSE ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2025, Ahmed Harmouche
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.md CHANGED
@@ -1,11 +1,56 @@
1
  ---
2
- title: Mindgrab
3
- emoji: 🏢
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: static
 
 
 
7
  pinned: false
8
- license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: MindGrab WebGPU
3
+ emoji: 🧠
4
+ colorFrom: purple
5
+ colorTo: indigo
6
  sdk: static
7
+ app_file: dist/index.html
8
+ app_build_command: npm run build
9
+ license: apache-2.0
10
  pinned: false
 
11
  ---
12
 
13
+ ### Overview
14
+
15
+ This Space bundles the MindGrab skull-stripping model (and its three TTA variants) exported from [brainchop](https://github.com/neuroneural/brainchop) via tinygrad. Everything runs in the browser with WebGPU; MRI data never leaves the client. The UI embeds [Niivue](https://github.com/niivue/niivue) for visualization and uses the same conform/normalization steps as the CLI.
16
+
17
+ ### Local Development
18
+
19
+ ```bash
20
+ cd hf
21
+ npm install # once
22
+ npm run dev # hot reload, http://localhost:5173
23
+ npm run build # produces dist/
24
+ npx serve dist # static preview (or npm run preview)
25
+ ```
26
+
27
+ ### Deploying to Hugging Face Spaces
28
+
29
+ Follow the latest [Spaces overview docs](https://huggingface.co/docs/hub/spaces-overview) / [Static HTML guide](https://huggingface.co/docs/hub/spaces-sdks-static):
30
+
31
+ 1. **Create or open the Space** – visit https://huggingface.co/spaces/neuroneural/mindgrab, choose SDK `static`, and ensure the README metadata matches the block above (it declares `app_file` + `app_build_command` so Hugging Face runs the same `npm run build` step you run locally).
32
+ 2. **Authenticate once** – `pip install -U huggingface_hub` if needed, then run `huggingface-cli login` with a write-enabled token.
33
+ 3. **Clone the Space repo (instead of initializing a fresh Git repo)**:
34
+ ```bash
35
+ git clone https://huggingface.co/spaces/neuroneural/mindgrab mindgrab-space
36
+ cd mindgrab-space
37
+ git lfs install # .gitattributes already tracks .safetensors + .nii.gz
38
+ ```
39
+ 4. **Sync this project into the clone** (from the `brainchop-cli` root run something like):
40
+ ```bash
41
+ rsync -av --delete hf/ /path/to/mindgrab-space/
42
+ ```
43
+ or copy the files manually—the key is to keep `.gitattributes`, `public/`, and the built `dist/` folder together.
44
+ 5. **Build locally before pushing** (same command Hugging Face runs server-side):
45
+ ```bash
46
+ npm install
47
+ npm run build
48
+ ```
49
+ 6. **Commit + push** – every push to `main` triggers an automatic Space rebuild:
50
+ ```bash
51
+ git add -A
52
+ git commit -m "Update MindGrab Space assets"
53
+ git push origin main
54
+ ```
55
+
56
+ Because `.gitattributes` tracks `.safetensors` and `.nii.gz`, Git LFS handles the large weights/sample volumes automatically. Once pushed, the Space serves the built `dist/` bundle and fetches weights from `/public`, so inference continues entirely client-side over WebGPU.
index.html CHANGED
@@ -1,19 +1,42 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="stylesheet" href="./niivue.css" />
8
+ <title>MindGrab WebGPU</title>
9
+ </head>
10
+
11
+ <body>
12
+ <header>
13
+ <button id="segmentBtn">Segment</button>
14
+ <label for="clipCheck">Clip Plane</label>
15
+ <input type="checkbox" id="clipCheck" unchecked />
16
+ <label for="opacitySlider0">Background Opacity</label>
17
+ <input type="range" min="0" max="255" value="255" class="slider" id="opacitySlider0" />
18
+ &nbsp;
19
+ <label for="opacitySlider1">Overlay Opacity</label>
20
+ <input type="range" min="0" max="255" value="128" class="slider" id="opacitySlider1" />
21
+ &nbsp;
22
+ <button id="saveImgBtn">Save Overlay</button>
23
+ &nbsp;
24
+ <button id="aboutBtn">About</button>
25
+ &nbsp;
26
+ <div id="loadingCircle" class="loading-circle hidden"></div>
27
+ <label for="segmentationDropdown">MindGrab Variant:</label>
28
+ <select id="segmentationDropdown">
29
+ <option value="mindgrab">MindGrab (default)</option>
30
+ <option value="mindgrab_tta_sagittal">MindGrab (TTA Sagittal)</option>
31
+ <option value="mindgrab_tta_coronal">MindGrab (TTA Coronal)</option>
32
+ <option value="mindgrab_tta_axial">MindGrab (TTA Axial)</option>
33
+ </select>
34
+ </header>
35
+ <main id="canvas-container">
36
+ <canvas id="gl1"></canvas>
37
+ </main>
38
+ <footer id="intensity">&nbsp;</footer>
39
+ <script type="module" src="/main.js"></script>
40
+ </body>
41
+
42
  </html>
main.js ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Niivue } from '@niivue/niivue'
2
+ import mindgrab from "./net_mindgrab.js"
3
+ import mindgrab_tta_sagittal from "./net_mindgrab_tta_sagittal.js"
4
+ import mindgrab_tta_coronal from "./net_mindgrab_tta_coronal.js"
5
+ import mindgrab_tta_axial from "./net_mindgrab_tta_axial.js"
6
+
7
+ const models = {
8
+ "mindgrab": {
9
+ "net": mindgrab,
10
+ "weightPath": "./net_mindgrab.safetensors",
11
+ "colormap": "./colormap_mindgrab.json",
12
+ "volume": "./t1_crop.nii.gz",
13
+ "normalization": "qnormalize"
14
+ },
15
+ "mindgrab_tta_sagittal": {
16
+ "net": mindgrab_tta_sagittal,
17
+ "weightPath": "./net_mindgrab_tta_sagittal.safetensors",
18
+ "colormap": "./colormap_mindgrab.json",
19
+ "volume": "./t1_crop.nii.gz",
20
+ "normalization": "qnormalize"
21
+ },
22
+ "mindgrab_tta_coronal": {
23
+ "net": mindgrab_tta_coronal,
24
+ "weightPath": "./net_mindgrab_tta_coronal.safetensors",
25
+ "colormap": "./colormap_mindgrab.json",
26
+ "volume": "./t1_crop.nii.gz",
27
+ "normalization": "qnormalize"
28
+ },
29
+ "mindgrab_tta_axial": {
30
+ "net": mindgrab_tta_axial,
31
+ "weightPath": "./net_mindgrab_tta_axial.safetensors",
32
+ "colormap": "./colormap_mindgrab.json",
33
+ "volume": "./t1_crop.nii.gz",
34
+ "normalization": "qnormalize"
35
+ }
36
+ }
37
+
38
+ let selectedModel = models[document.getElementById("segmentationDropdown").value]
39
+
40
+ function qnormalize(img32, qmin = 0.02, qmax = 0.98, eps = 1e-3) {
41
+ // Create sorted copy to find quantiles
42
+ const sorted = [...img32].sort((a, b) => a - b);
43
+ // Calculate quantile indices
44
+ const n = sorted.length;
45
+ const qminIndex = Math.floor(qmin * (n - 1));
46
+ const qmaxIndex = Math.floor(qmax * (n - 1));
47
+ // Linear interpolation for accurate quantiles
48
+ const qminFrac = qmin * (n - 1) - qminIndex;
49
+ const qmaxFrac = qmax * (n - 1) - qmaxIndex;
50
+ let qlow = sorted[qminIndex];
51
+ if (qminIndex < n - 1) {
52
+ qlow += qminFrac * (sorted[qminIndex + 1] - sorted[qminIndex]);
53
+ }
54
+ let qhigh = sorted[qmaxIndex];
55
+ if (qmaxIndex < n - 1) {
56
+ qhigh += qmaxFrac * (sorted[qmaxIndex + 1] - sorted[qmaxIndex]);
57
+ }
58
+ // Normalize and clip in-place
59
+ const scale = 1 / (qhigh - qlow + eps);
60
+ for (let i = 0; i < img32.length; i++) {
61
+ img32[i] = Math.max(0, Math.min(1, (img32[i] - qlow) * scale));
62
+ }
63
+ }
64
+
65
+ async function main() {
66
+ clipCheck.onchange = function () {
67
+ if (clipCheck.checked) {
68
+ nv1.setClipPlane([0, 0, 90])
69
+ } else {
70
+ nv1.setClipPlane([2, 0, 90])
71
+ }
72
+ }
73
+ opacitySlider0.oninput = function () {
74
+ nv1.setOpacity(0, opacitySlider0.value / 255)
75
+ nv1.updateGLVolume()
76
+ }
77
+ opacitySlider1.oninput = function () {
78
+ nv1.setOpacity(1, opacitySlider1.value / 255)
79
+ }
80
+ function doLoadImage() {
81
+ opacitySlider0.oninput()
82
+ }
83
+ async function fetchJSON(fnm) {
84
+ const response = await fetch(fnm)
85
+ const js = await response.json()
86
+ return js
87
+ }
88
+ saveImgBtn.onclick = function () {
89
+ nv1.volumes[1].saveToDisk('Custom.nii')
90
+ }
91
+ aboutBtn.onclick = function () {
92
+ const url = "https://github.com/niivue/niivue-tinygrad"
93
+ window.open(url, "_blank")
94
+ }
95
+ async function ensureConformed() {
96
+ const nii = nv1.volumes[0]
97
+ let isConformed = nii.dims[1] === 256 && nii.dims[2] === 256 && nii.dims[3] === 256
98
+ if (nii.permRAS[0] !== -1 || nii.permRAS[1] !== 3 || nii.permRAS[2] !== -2) {
99
+ isConformed = false
100
+ }
101
+ if (isConformed) {
102
+ return
103
+ }
104
+ const nii2 = await nv1.conform(nii, false)
105
+ nv1.removeVolume(nv1.volumes[0])
106
+ nv1.addVolume(nii2)
107
+ }
108
+ async function closeAllOverlays() {
109
+ while (nv1.volumes.length > 1) {
110
+ nv1.removeVolume(nv1.volumes[1])
111
+ }
112
+ }
113
+ const getDevice = async () => {
114
+ if (!navigator.gpu) return false;
115
+ const adapter = await navigator.gpu.requestAdapter();
116
+
117
+ // Allow simulating lower buffer limits via URL param: ?maxBufferMB=128
118
+ const params = new URLSearchParams(window.location.search);
119
+ const simulatedMaxMB = params.get('maxBufferMB');
120
+ const simulatedMax = simulatedMaxMB ? parseInt(simulatedMaxMB) * 1024 * 1024 : Infinity;
121
+
122
+ const maxBufferSize = Math.min(simulatedMax, adapter.limits.maxBufferSize);
123
+ const maxStorageBufferBindingSize = Math.min(simulatedMax, adapter.limits.maxStorageBufferBindingSize);
124
+
125
+ console.log('Adapter limits:', adapter.limits);
126
+ console.log('Adapter max buffer size:', adapter.limits.maxBufferSize);
127
+ console.log('Requested max buffer size:', maxBufferSize, simulatedMaxMB ? `(simulated ${simulatedMaxMB}MB)` : '(using adapter max)');
128
+
129
+ return await adapter.requestDevice({
130
+ requiredLimits: {
131
+ maxBufferSize,
132
+ maxStorageBufferBindingSize,
133
+ },
134
+ requiredFeatures: ["shader-f16"]
135
+ });
136
+ };
137
+
138
+ const device = await getDevice();
139
+
140
+ // Buffer profiling monkeypatch
141
+ const bufferStats = { totalAllocated: 0, maxSingleBuffer: 0, count: 0 };
142
+ const _createBuffer = device.createBuffer.bind(device);
143
+ device.createBuffer = (descriptor) => {
144
+ bufferStats.totalAllocated += descriptor.size;
145
+ bufferStats.maxSingleBuffer = Math.max(bufferStats.maxSingleBuffer, descriptor.size);
146
+ bufferStats.count++;
147
+ return _createBuffer(descriptor);
148
+ };
149
+ window.bufferStats = bufferStats;
150
+ window.logBufferStats = () => console.log({
151
+ totalMB: (bufferStats.totalAllocated / 1024 / 1024).toFixed(2),
152
+ maxSingleMB: (bufferStats.maxSingleBuffer / 1024 / 1024).toFixed(2),
153
+ count: bufferStats.count
154
+ });
155
+
156
+ function convertInMemoryOrder(inverse, size, data) {
157
+ let output = new Float32Array(data.length)
158
+ let it = 0;
159
+
160
+ for (let x = 0; x < size; x++) {
161
+ for (let y = 0; y < size; y++) {
162
+ for (let z = 0; z < size; z++) {
163
+ let idx = x + y * size + z * size * size;
164
+ if (inverse) {
165
+ output[idx] = data[it++];
166
+ } else {
167
+ output[it++] = data[idx];
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ return output
174
+ }
175
+
176
+ segmentBtn.onclick = async function () {
177
+ if (nv1.volumes.length < 1) {
178
+ window.alert('Please open a voxel-based image')
179
+ return
180
+ }
181
+ const startTime = Date.now();
182
+ console.log('[Model] Starting segmentation...')
183
+ loadingCircle.classList.remove('hidden')
184
+
185
+ console.log('[Model] Phase 1: Preparing volume (closing overlays, conforming)...')
186
+ await closeAllOverlays()
187
+ await ensureConformed()
188
+ console.log('[Model] Phase 1: Volume preparation complete')
189
+
190
+ console.log('[Model] Phase 2: Normalizing input data...')
191
+ let img32 = convertInMemoryOrder(/*inverse*/ false, 256, new Float32Array(nv1.volumes[0].img))
192
+
193
+ console.log(selectedModel)
194
+ if (selectedModel['normalization'] === 'min-max') {
195
+ console.log('[Model] Phase 2: Using min-max normalization')
196
+ // normalize input data to range 0..1
197
+ let mx = img32[0]
198
+ let mn = mx
199
+ for (let i = 0; i < img32.length; i++) {
200
+ mx = Math.max(mx, img32[i])
201
+ mn = Math.min(mn, img32[i])
202
+ }
203
+ let scale32 = 1 / (mx - mn)
204
+ for (let i = 0; i < img32.length; i++) {
205
+ img32[i] = (img32[i] - mn) * scale32
206
+ }
207
+ } else {
208
+ console.log('[Model] Phase 2: Using quantile normalization')
209
+ qnormalize(img32);
210
+ }
211
+ console.log('[Model] Phase 2: Normalization complete')
212
+
213
+ console.log('[Model] Phase 3: Loading model weights...')
214
+ const session = await selectedModel["net"].load(device, selectedModel["weightPath"]);
215
+ console.log('[Model] Phase 3: Model weights loaded')
216
+
217
+ const shape = [1, 1, 256, 256, 256]
218
+ const nvox = shape.reduce((a, b) => a * b)
219
+ if (img32.length !== nvox) {
220
+ throw new Error(`img32 length (${img32.length}) does not match expected tensor length (${expectedLength})`)
221
+ }
222
+
223
+ console.log('[Model] Phase 4: Running inference...')
224
+ // Convert to Float16Array if model requires fp16 input
225
+ let inputData = img32;
226
+ if (selectedModel['fp16']) {
227
+ console.log('[Model] Phase 4: Converting to Float16Array for fp16 model')
228
+ inputData = new Float16Array(img32);
229
+ }
230
+ const results = await session(inputData);
231
+ console.log('[Model] Phase 4: Inference complete')
232
+
233
+ // Log label distribution in output cube
234
+ const outputData = results[0];
235
+ const totalVoxels = outputData.length;
236
+ const labelCounts = {};
237
+ for (let i = 0; i < totalVoxels; i++) {
238
+ const label = Math.round(outputData[i]);
239
+ labelCounts[label] = (labelCounts[label] || 0) + 1;
240
+ }
241
+ console.log('[Model] Label distribution in output cube:');
242
+ const sortedLabels = Object.keys(labelCounts).sort((a, b) => a - b);
243
+ for (const label of sortedLabels) {
244
+ const count = labelCounts[label];
245
+ const ratio = (count / totalVoxels * 100).toFixed(2);
246
+ console.log(` Label ${label}: ${count} voxels (${ratio}%)`);
247
+ }
248
+
249
+ console.log('[Model] Phase 5: Post-processing results...')
250
+ let segmentImg = nv1.cloneVolume(0)
251
+ segmentImg.img = convertInMemoryOrder(/*inverse*/ true, 256, results[0])
252
+ segmentImg.hdr.datatypeCode = 16
253
+ segmentImg.hdr.dims[4] = 1
254
+ segmentImg.trustCalMinMax = false
255
+
256
+ // Add the output to niivue
257
+ const cmap = await fetchJSON(selectedModel["colormap"])
258
+ segmentImg.setColormapLabel(cmap)
259
+ segmentImg.opacity = opacitySlider1.value / 255
260
+ nv1.addVolume(segmentImg)
261
+ console.log('[Model] Phase 5: Post-processing complete')
262
+
263
+ loadingCircle.classList.add('hidden')
264
+ const elapsedTime = Date.now() - startTime
265
+ console.log(`[Model] Segmentation complete in ${elapsedTime}ms`)
266
+ document.getElementById("intensity").innerHTML = `${elapsedTime}ms to segment`
267
+ }
268
+ function handleLocationChange(data) {
269
+ document.getElementById("intensity").innerHTML = data.string
270
+ }
271
+ const defaults = {
272
+ backColor: [0.4, 0.4, 0.4, 1],
273
+ onLocationChange: handleLocationChange,
274
+ }
275
+ const nv1 = new Niivue(defaults)
276
+ nv1.attachToCanvas(gl1)
277
+ nv1.opts.multiplanarForceRender = true
278
+ nv1.opts.yoke3Dto2DZoom = true
279
+ nv1.opts.crosshairGap = 11
280
+ nv1.setInterpolation(true)
281
+ nv1.onImageLoaded = doLoadImage
282
+ await nv1.loadVolumes([{ url: selectedModel["volume"] }])
283
+ segmentBtn.onclick()
284
+
285
+ document.getElementById("segmentationDropdown").addEventListener("change", async function () {
286
+ selectedModel = models[this.value]
287
+ if (nv1.volumes[0].url != selectedModel["volume"]) {
288
+ nv1.removeVolumeByIndex(0)
289
+ await nv1.loadVolumes([{ url: selectedModel["volume"] }])
290
+ }
291
+ segmentBtn.onclick()
292
+ });
293
+ }
294
+
295
+ main()
net_mindgrab.js ADDED
The diff for this file is too large to render. See raw diff
 
net_mindgrab_tta_axial.js ADDED
The diff for this file is too large to render. See raw diff
 
net_mindgrab_tta_coronal.js ADDED
The diff for this file is too large to render. See raw diff
 
net_mindgrab_tta_sagittal.js ADDED
The diff for this file is too large to render. See raw diff
 
package-lock.json ADDED
@@ -0,0 +1,953 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "niivue-tinygrad",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "niivue-tinygrad",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "@niivue/niivue": "^0.48.1"
12
+ },
13
+ "devDependencies": {
14
+ "vite": "^5.2.0"
15
+ }
16
+ },
17
+ "node_modules/@esbuild/aix-ppc64": {
18
+ "version": "0.20.2",
19
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
20
+ "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
21
+ "cpu": [
22
+ "ppc64"
23
+ ],
24
+ "dev": true,
25
+ "optional": true,
26
+ "os": [
27
+ "aix"
28
+ ],
29
+ "engines": {
30
+ "node": ">=12"
31
+ }
32
+ },
33
+ "node_modules/@esbuild/android-arm": {
34
+ "version": "0.20.2",
35
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
36
+ "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
37
+ "cpu": [
38
+ "arm"
39
+ ],
40
+ "dev": true,
41
+ "optional": true,
42
+ "os": [
43
+ "android"
44
+ ],
45
+ "engines": {
46
+ "node": ">=12"
47
+ }
48
+ },
49
+ "node_modules/@esbuild/android-arm64": {
50
+ "version": "0.20.2",
51
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
52
+ "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
53
+ "cpu": [
54
+ "arm64"
55
+ ],
56
+ "dev": true,
57
+ "optional": true,
58
+ "os": [
59
+ "android"
60
+ ],
61
+ "engines": {
62
+ "node": ">=12"
63
+ }
64
+ },
65
+ "node_modules/@esbuild/android-x64": {
66
+ "version": "0.20.2",
67
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
68
+ "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
69
+ "cpu": [
70
+ "x64"
71
+ ],
72
+ "dev": true,
73
+ "optional": true,
74
+ "os": [
75
+ "android"
76
+ ],
77
+ "engines": {
78
+ "node": ">=12"
79
+ }
80
+ },
81
+ "node_modules/@esbuild/darwin-arm64": {
82
+ "version": "0.20.2",
83
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
84
+ "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
85
+ "cpu": [
86
+ "arm64"
87
+ ],
88
+ "dev": true,
89
+ "optional": true,
90
+ "os": [
91
+ "darwin"
92
+ ],
93
+ "engines": {
94
+ "node": ">=12"
95
+ }
96
+ },
97
+ "node_modules/@esbuild/darwin-x64": {
98
+ "version": "0.20.2",
99
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
100
+ "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
101
+ "cpu": [
102
+ "x64"
103
+ ],
104
+ "dev": true,
105
+ "optional": true,
106
+ "os": [
107
+ "darwin"
108
+ ],
109
+ "engines": {
110
+ "node": ">=12"
111
+ }
112
+ },
113
+ "node_modules/@esbuild/freebsd-arm64": {
114
+ "version": "0.20.2",
115
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
116
+ "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
117
+ "cpu": [
118
+ "arm64"
119
+ ],
120
+ "dev": true,
121
+ "optional": true,
122
+ "os": [
123
+ "freebsd"
124
+ ],
125
+ "engines": {
126
+ "node": ">=12"
127
+ }
128
+ },
129
+ "node_modules/@esbuild/freebsd-x64": {
130
+ "version": "0.20.2",
131
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
132
+ "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
133
+ "cpu": [
134
+ "x64"
135
+ ],
136
+ "dev": true,
137
+ "optional": true,
138
+ "os": [
139
+ "freebsd"
140
+ ],
141
+ "engines": {
142
+ "node": ">=12"
143
+ }
144
+ },
145
+ "node_modules/@esbuild/linux-arm": {
146
+ "version": "0.20.2",
147
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
148
+ "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
149
+ "cpu": [
150
+ "arm"
151
+ ],
152
+ "dev": true,
153
+ "optional": true,
154
+ "os": [
155
+ "linux"
156
+ ],
157
+ "engines": {
158
+ "node": ">=12"
159
+ }
160
+ },
161
+ "node_modules/@esbuild/linux-arm64": {
162
+ "version": "0.20.2",
163
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
164
+ "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
165
+ "cpu": [
166
+ "arm64"
167
+ ],
168
+ "dev": true,
169
+ "optional": true,
170
+ "os": [
171
+ "linux"
172
+ ],
173
+ "engines": {
174
+ "node": ">=12"
175
+ }
176
+ },
177
+ "node_modules/@esbuild/linux-ia32": {
178
+ "version": "0.20.2",
179
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
180
+ "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
181
+ "cpu": [
182
+ "ia32"
183
+ ],
184
+ "dev": true,
185
+ "optional": true,
186
+ "os": [
187
+ "linux"
188
+ ],
189
+ "engines": {
190
+ "node": ">=12"
191
+ }
192
+ },
193
+ "node_modules/@esbuild/linux-loong64": {
194
+ "version": "0.20.2",
195
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
196
+ "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
197
+ "cpu": [
198
+ "loong64"
199
+ ],
200
+ "dev": true,
201
+ "optional": true,
202
+ "os": [
203
+ "linux"
204
+ ],
205
+ "engines": {
206
+ "node": ">=12"
207
+ }
208
+ },
209
+ "node_modules/@esbuild/linux-mips64el": {
210
+ "version": "0.20.2",
211
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
212
+ "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
213
+ "cpu": [
214
+ "mips64el"
215
+ ],
216
+ "dev": true,
217
+ "optional": true,
218
+ "os": [
219
+ "linux"
220
+ ],
221
+ "engines": {
222
+ "node": ">=12"
223
+ }
224
+ },
225
+ "node_modules/@esbuild/linux-ppc64": {
226
+ "version": "0.20.2",
227
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
228
+ "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
229
+ "cpu": [
230
+ "ppc64"
231
+ ],
232
+ "dev": true,
233
+ "optional": true,
234
+ "os": [
235
+ "linux"
236
+ ],
237
+ "engines": {
238
+ "node": ">=12"
239
+ }
240
+ },
241
+ "node_modules/@esbuild/linux-riscv64": {
242
+ "version": "0.20.2",
243
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
244
+ "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
245
+ "cpu": [
246
+ "riscv64"
247
+ ],
248
+ "dev": true,
249
+ "optional": true,
250
+ "os": [
251
+ "linux"
252
+ ],
253
+ "engines": {
254
+ "node": ">=12"
255
+ }
256
+ },
257
+ "node_modules/@esbuild/linux-s390x": {
258
+ "version": "0.20.2",
259
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
260
+ "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
261
+ "cpu": [
262
+ "s390x"
263
+ ],
264
+ "dev": true,
265
+ "optional": true,
266
+ "os": [
267
+ "linux"
268
+ ],
269
+ "engines": {
270
+ "node": ">=12"
271
+ }
272
+ },
273
+ "node_modules/@esbuild/linux-x64": {
274
+ "version": "0.20.2",
275
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
276
+ "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
277
+ "cpu": [
278
+ "x64"
279
+ ],
280
+ "dev": true,
281
+ "optional": true,
282
+ "os": [
283
+ "linux"
284
+ ],
285
+ "engines": {
286
+ "node": ">=12"
287
+ }
288
+ },
289
+ "node_modules/@esbuild/netbsd-x64": {
290
+ "version": "0.20.2",
291
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
292
+ "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
293
+ "cpu": [
294
+ "x64"
295
+ ],
296
+ "dev": true,
297
+ "optional": true,
298
+ "os": [
299
+ "netbsd"
300
+ ],
301
+ "engines": {
302
+ "node": ">=12"
303
+ }
304
+ },
305
+ "node_modules/@esbuild/openbsd-x64": {
306
+ "version": "0.20.2",
307
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
308
+ "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
309
+ "cpu": [
310
+ "x64"
311
+ ],
312
+ "dev": true,
313
+ "optional": true,
314
+ "os": [
315
+ "openbsd"
316
+ ],
317
+ "engines": {
318
+ "node": ">=12"
319
+ }
320
+ },
321
+ "node_modules/@esbuild/sunos-x64": {
322
+ "version": "0.20.2",
323
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
324
+ "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
325
+ "cpu": [
326
+ "x64"
327
+ ],
328
+ "dev": true,
329
+ "optional": true,
330
+ "os": [
331
+ "sunos"
332
+ ],
333
+ "engines": {
334
+ "node": ">=12"
335
+ }
336
+ },
337
+ "node_modules/@esbuild/win32-arm64": {
338
+ "version": "0.20.2",
339
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
340
+ "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
341
+ "cpu": [
342
+ "arm64"
343
+ ],
344
+ "dev": true,
345
+ "optional": true,
346
+ "os": [
347
+ "win32"
348
+ ],
349
+ "engines": {
350
+ "node": ">=12"
351
+ }
352
+ },
353
+ "node_modules/@esbuild/win32-ia32": {
354
+ "version": "0.20.2",
355
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
356
+ "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
357
+ "cpu": [
358
+ "ia32"
359
+ ],
360
+ "dev": true,
361
+ "optional": true,
362
+ "os": [
363
+ "win32"
364
+ ],
365
+ "engines": {
366
+ "node": ">=12"
367
+ }
368
+ },
369
+ "node_modules/@esbuild/win32-x64": {
370
+ "version": "0.20.2",
371
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
372
+ "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
373
+ "cpu": [
374
+ "x64"
375
+ ],
376
+ "dev": true,
377
+ "optional": true,
378
+ "os": [
379
+ "win32"
380
+ ],
381
+ "engines": {
382
+ "node": ">=12"
383
+ }
384
+ },
385
+ "node_modules/@lukeed/csprng": {
386
+ "version": "1.1.0",
387
+ "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
388
+ "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==",
389
+ "engines": {
390
+ "node": ">=8"
391
+ }
392
+ },
393
+ "node_modules/@lukeed/uuid": {
394
+ "version": "2.0.1",
395
+ "resolved": "https://registry.npmjs.org/@lukeed/uuid/-/uuid-2.0.1.tgz",
396
+ "integrity": "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==",
397
+ "dependencies": {
398
+ "@lukeed/csprng": "^1.1.0"
399
+ },
400
+ "engines": {
401
+ "node": ">=8"
402
+ }
403
+ },
404
+ "node_modules/@niivue/niivue": {
405
+ "version": "0.48.1",
406
+ "resolved": "https://registry.npmjs.org/@niivue/niivue/-/niivue-0.48.1.tgz",
407
+ "integrity": "sha512-xeSrJ/3BBWiQsI6tK8YL3DOGumnX4oMfMZPpUIji+2mIYlqlylLxBtzAKIBIttEmJcwx/PzYb4HeY6TQVUvMzw==",
408
+ "license": "BSD-2-Clause",
409
+ "dependencies": {
410
+ "@lukeed/uuid": "^2.0.1",
411
+ "@ungap/structured-clone": "^1.2.0",
412
+ "array-equal": "^1.0.2",
413
+ "daikon": "^1.2.46",
414
+ "fflate": "^0.8.2",
415
+ "gl-matrix": "^3.4.3",
416
+ "nifti-reader-js": "^0.6.8",
417
+ "rxjs": "^7.8.1"
418
+ },
419
+ "optionalDependencies": {
420
+ "@rollup/rollup-linux-x64-gnu": "^4.18.1"
421
+ }
422
+ },
423
+ "node_modules/@niivue/niivue/node_modules/@rollup/rollup-linux-x64-gnu": {
424
+ "version": "4.31.0",
425
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz",
426
+ "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==",
427
+ "cpu": [
428
+ "x64"
429
+ ],
430
+ "license": "MIT",
431
+ "optional": true,
432
+ "os": [
433
+ "linux"
434
+ ]
435
+ },
436
+ "node_modules/@rollup/rollup-android-arm-eabi": {
437
+ "version": "4.16.4",
438
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.4.tgz",
439
+ "integrity": "sha512-GkhjAaQ8oUTOKE4g4gsZ0u8K/IHU1+2WQSgS1TwTcYvL+sjbaQjNHFXbOJ6kgqGHIO1DfUhI/Sphi9GkRT9K+Q==",
440
+ "cpu": [
441
+ "arm"
442
+ ],
443
+ "dev": true,
444
+ "optional": true,
445
+ "os": [
446
+ "android"
447
+ ]
448
+ },
449
+ "node_modules/@rollup/rollup-android-arm64": {
450
+ "version": "4.16.4",
451
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.4.tgz",
452
+ "integrity": "sha512-Bvm6D+NPbGMQOcxvS1zUl8H7DWlywSXsphAeOnVeiZLQ+0J6Is8T7SrjGTH29KtYkiY9vld8ZnpV3G2EPbom+w==",
453
+ "cpu": [
454
+ "arm64"
455
+ ],
456
+ "dev": true,
457
+ "optional": true,
458
+ "os": [
459
+ "android"
460
+ ]
461
+ },
462
+ "node_modules/@rollup/rollup-darwin-arm64": {
463
+ "version": "4.16.4",
464
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.4.tgz",
465
+ "integrity": "sha512-i5d64MlnYBO9EkCOGe5vPR/EeDwjnKOGGdd7zKFhU5y8haKhQZTN2DgVtpODDMxUr4t2K90wTUJg7ilgND6bXw==",
466
+ "cpu": [
467
+ "arm64"
468
+ ],
469
+ "dev": true,
470
+ "optional": true,
471
+ "os": [
472
+ "darwin"
473
+ ]
474
+ },
475
+ "node_modules/@rollup/rollup-darwin-x64": {
476
+ "version": "4.16.4",
477
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.4.tgz",
478
+ "integrity": "sha512-WZupV1+CdUYehaZqjaFTClJI72fjJEgTXdf4NbW69I9XyvdmztUExBtcI2yIIU6hJtYvtwS6pkTkHJz+k08mAQ==",
479
+ "cpu": [
480
+ "x64"
481
+ ],
482
+ "dev": true,
483
+ "optional": true,
484
+ "os": [
485
+ "darwin"
486
+ ]
487
+ },
488
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
489
+ "version": "4.16.4",
490
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.4.tgz",
491
+ "integrity": "sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==",
492
+ "cpu": [
493
+ "arm"
494
+ ],
495
+ "dev": true,
496
+ "optional": true,
497
+ "os": [
498
+ "linux"
499
+ ]
500
+ },
501
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
502
+ "version": "4.16.4",
503
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.4.tgz",
504
+ "integrity": "sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==",
505
+ "cpu": [
506
+ "arm"
507
+ ],
508
+ "dev": true,
509
+ "optional": true,
510
+ "os": [
511
+ "linux"
512
+ ]
513
+ },
514
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
515
+ "version": "4.16.4",
516
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.4.tgz",
517
+ "integrity": "sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==",
518
+ "cpu": [
519
+ "arm64"
520
+ ],
521
+ "dev": true,
522
+ "optional": true,
523
+ "os": [
524
+ "linux"
525
+ ]
526
+ },
527
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
528
+ "version": "4.16.4",
529
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.4.tgz",
530
+ "integrity": "sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==",
531
+ "cpu": [
532
+ "arm64"
533
+ ],
534
+ "dev": true,
535
+ "optional": true,
536
+ "os": [
537
+ "linux"
538
+ ]
539
+ },
540
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
541
+ "version": "4.16.4",
542
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.4.tgz",
543
+ "integrity": "sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==",
544
+ "cpu": [
545
+ "ppc64"
546
+ ],
547
+ "dev": true,
548
+ "optional": true,
549
+ "os": [
550
+ "linux"
551
+ ]
552
+ },
553
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
554
+ "version": "4.16.4",
555
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.4.tgz",
556
+ "integrity": "sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==",
557
+ "cpu": [
558
+ "riscv64"
559
+ ],
560
+ "dev": true,
561
+ "optional": true,
562
+ "os": [
563
+ "linux"
564
+ ]
565
+ },
566
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
567
+ "version": "4.16.4",
568
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.4.tgz",
569
+ "integrity": "sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==",
570
+ "cpu": [
571
+ "s390x"
572
+ ],
573
+ "dev": true,
574
+ "optional": true,
575
+ "os": [
576
+ "linux"
577
+ ]
578
+ },
579
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
580
+ "version": "4.16.4",
581
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.4.tgz",
582
+ "integrity": "sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==",
583
+ "cpu": [
584
+ "x64"
585
+ ],
586
+ "dev": true,
587
+ "optional": true,
588
+ "os": [
589
+ "linux"
590
+ ]
591
+ },
592
+ "node_modules/@rollup/rollup-linux-x64-musl": {
593
+ "version": "4.16.4",
594
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.4.tgz",
595
+ "integrity": "sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==",
596
+ "cpu": [
597
+ "x64"
598
+ ],
599
+ "dev": true,
600
+ "optional": true,
601
+ "os": [
602
+ "linux"
603
+ ]
604
+ },
605
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
606
+ "version": "4.16.4",
607
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.4.tgz",
608
+ "integrity": "sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==",
609
+ "cpu": [
610
+ "arm64"
611
+ ],
612
+ "dev": true,
613
+ "optional": true,
614
+ "os": [
615
+ "win32"
616
+ ]
617
+ },
618
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
619
+ "version": "4.16.4",
620
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.4.tgz",
621
+ "integrity": "sha512-9m/ZDrQsdo/c06uOlP3W9G2ENRVzgzbSXmXHT4hwVaDQhYcRpi9bgBT0FTG9OhESxwK0WjQxYOSfv40cU+T69w==",
622
+ "cpu": [
623
+ "ia32"
624
+ ],
625
+ "dev": true,
626
+ "optional": true,
627
+ "os": [
628
+ "win32"
629
+ ]
630
+ },
631
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
632
+ "version": "4.16.4",
633
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.4.tgz",
634
+ "integrity": "sha512-YunpoOAyGLDseanENHmbFvQSfVL5BxW3k7hhy0eN4rb3gS/ct75dVD0EXOWIqFT/nE8XYW6LP6vz6ctKRi0k9A==",
635
+ "cpu": [
636
+ "x64"
637
+ ],
638
+ "dev": true,
639
+ "optional": true,
640
+ "os": [
641
+ "win32"
642
+ ]
643
+ },
644
+ "node_modules/@types/estree": {
645
+ "version": "1.0.5",
646
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
647
+ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
648
+ "dev": true
649
+ },
650
+ "node_modules/@ungap/structured-clone": {
651
+ "version": "1.2.0",
652
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
653
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
654
+ },
655
+ "node_modules/@wearemothership/dicom-character-set": {
656
+ "version": "1.0.4-opt.1",
657
+ "resolved": "https://registry.npmjs.org/@wearemothership/dicom-character-set/-/dicom-character-set-1.0.4-opt.1.tgz",
658
+ "integrity": "sha512-stqhnpawYHY2UZKj4RHTF71ab3q3z8S1SO9ToQKjsHQwowUdFVo6YFea93psFux3yqNbRlQjwoCdPjHcD0YQzw==",
659
+ "engines": {
660
+ "node": ">=10"
661
+ }
662
+ },
663
+ "node_modules/array-equal": {
664
+ "version": "1.0.2",
665
+ "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.2.tgz",
666
+ "integrity": "sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==",
667
+ "funding": {
668
+ "url": "https://github.com/sponsors/sindresorhus"
669
+ }
670
+ },
671
+ "node_modules/commander": {
672
+ "version": "2.20.3",
673
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
674
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
675
+ },
676
+ "node_modules/cssfilter": {
677
+ "version": "0.0.10",
678
+ "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
679
+ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
680
+ },
681
+ "node_modules/daikon": {
682
+ "version": "1.2.46",
683
+ "resolved": "https://registry.npmjs.org/daikon/-/daikon-1.2.46.tgz",
684
+ "integrity": "sha512-S8dTTlsWYTH3LQztjTW9KnNvxDeL2mr2cau0auLdYMJe4TrocYP1PmidHizO3rXUs+gXpBWI1PQ2qvB4b21QFw==",
685
+ "dependencies": {
686
+ "@wearemothership/dicom-character-set": "^1.0.4-opt.1",
687
+ "fflate": "*",
688
+ "jpeg-lossless-decoder-js": "2.0.7",
689
+ "pako": "^2.1",
690
+ "xss": "1.0.14"
691
+ }
692
+ },
693
+ "node_modules/esbuild": {
694
+ "version": "0.20.2",
695
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
696
+ "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
697
+ "dev": true,
698
+ "hasInstallScript": true,
699
+ "bin": {
700
+ "esbuild": "bin/esbuild"
701
+ },
702
+ "engines": {
703
+ "node": ">=12"
704
+ },
705
+ "optionalDependencies": {
706
+ "@esbuild/aix-ppc64": "0.20.2",
707
+ "@esbuild/android-arm": "0.20.2",
708
+ "@esbuild/android-arm64": "0.20.2",
709
+ "@esbuild/android-x64": "0.20.2",
710
+ "@esbuild/darwin-arm64": "0.20.2",
711
+ "@esbuild/darwin-x64": "0.20.2",
712
+ "@esbuild/freebsd-arm64": "0.20.2",
713
+ "@esbuild/freebsd-x64": "0.20.2",
714
+ "@esbuild/linux-arm": "0.20.2",
715
+ "@esbuild/linux-arm64": "0.20.2",
716
+ "@esbuild/linux-ia32": "0.20.2",
717
+ "@esbuild/linux-loong64": "0.20.2",
718
+ "@esbuild/linux-mips64el": "0.20.2",
719
+ "@esbuild/linux-ppc64": "0.20.2",
720
+ "@esbuild/linux-riscv64": "0.20.2",
721
+ "@esbuild/linux-s390x": "0.20.2",
722
+ "@esbuild/linux-x64": "0.20.2",
723
+ "@esbuild/netbsd-x64": "0.20.2",
724
+ "@esbuild/openbsd-x64": "0.20.2",
725
+ "@esbuild/sunos-x64": "0.20.2",
726
+ "@esbuild/win32-arm64": "0.20.2",
727
+ "@esbuild/win32-ia32": "0.20.2",
728
+ "@esbuild/win32-x64": "0.20.2"
729
+ }
730
+ },
731
+ "node_modules/fflate": {
732
+ "version": "0.8.2",
733
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
734
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
735
+ },
736
+ "node_modules/fsevents": {
737
+ "version": "2.3.3",
738
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
739
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
740
+ "dev": true,
741
+ "hasInstallScript": true,
742
+ "optional": true,
743
+ "os": [
744
+ "darwin"
745
+ ],
746
+ "engines": {
747
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
748
+ }
749
+ },
750
+ "node_modules/gl-matrix": {
751
+ "version": "3.4.3",
752
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
753
+ "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
754
+ },
755
+ "node_modules/jpeg-lossless-decoder-js": {
756
+ "version": "2.0.7",
757
+ "resolved": "https://registry.npmjs.org/jpeg-lossless-decoder-js/-/jpeg-lossless-decoder-js-2.0.7.tgz",
758
+ "integrity": "sha512-tbZlhFkKmx+JaqVMkq47SKWGuXLkIaV8fTbnhO39dYEnQrSShLGuLCGb0n6ntXjtmk6oAWGiIriWOLwj9od0yQ=="
759
+ },
760
+ "node_modules/nanoid": {
761
+ "version": "3.3.7",
762
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
763
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
764
+ "dev": true,
765
+ "funding": [
766
+ {
767
+ "type": "github",
768
+ "url": "https://github.com/sponsors/ai"
769
+ }
770
+ ],
771
+ "bin": {
772
+ "nanoid": "bin/nanoid.cjs"
773
+ },
774
+ "engines": {
775
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
776
+ }
777
+ },
778
+ "node_modules/nifti-reader-js": {
779
+ "version": "0.6.8",
780
+ "resolved": "https://registry.npmjs.org/nifti-reader-js/-/nifti-reader-js-0.6.8.tgz",
781
+ "integrity": "sha512-yIKNVzYFiUcSHazoR+sd6Ka7sUmZTabaVqJRFxbdlAKR1hnPBuNP71g3AyApo37nJ3k41c632QPij5q7gF1YPQ==",
782
+ "dependencies": {
783
+ "fflate": "*"
784
+ }
785
+ },
786
+ "node_modules/pako": {
787
+ "version": "2.1.0",
788
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
789
+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
790
+ },
791
+ "node_modules/picocolors": {
792
+ "version": "1.0.0",
793
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
794
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
795
+ "dev": true
796
+ },
797
+ "node_modules/postcss": {
798
+ "version": "8.4.38",
799
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
800
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
801
+ "dev": true,
802
+ "funding": [
803
+ {
804
+ "type": "opencollective",
805
+ "url": "https://opencollective.com/postcss/"
806
+ },
807
+ {
808
+ "type": "tidelift",
809
+ "url": "https://tidelift.com/funding/github/npm/postcss"
810
+ },
811
+ {
812
+ "type": "github",
813
+ "url": "https://github.com/sponsors/ai"
814
+ }
815
+ ],
816
+ "dependencies": {
817
+ "nanoid": "^3.3.7",
818
+ "picocolors": "^1.0.0",
819
+ "source-map-js": "^1.2.0"
820
+ },
821
+ "engines": {
822
+ "node": "^10 || ^12 || >=14"
823
+ }
824
+ },
825
+ "node_modules/rollup": {
826
+ "version": "4.16.4",
827
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.4.tgz",
828
+ "integrity": "sha512-kuaTJSUbz+Wsb2ATGvEknkI12XV40vIiHmLuFlejoo7HtDok/O5eDDD0UpCVY5bBX5U5RYo8wWP83H7ZsqVEnA==",
829
+ "dev": true,
830
+ "dependencies": {
831
+ "@types/estree": "1.0.5"
832
+ },
833
+ "bin": {
834
+ "rollup": "dist/bin/rollup"
835
+ },
836
+ "engines": {
837
+ "node": ">=18.0.0",
838
+ "npm": ">=8.0.0"
839
+ },
840
+ "optionalDependencies": {
841
+ "@rollup/rollup-android-arm-eabi": "4.16.4",
842
+ "@rollup/rollup-android-arm64": "4.16.4",
843
+ "@rollup/rollup-darwin-arm64": "4.16.4",
844
+ "@rollup/rollup-darwin-x64": "4.16.4",
845
+ "@rollup/rollup-linux-arm-gnueabihf": "4.16.4",
846
+ "@rollup/rollup-linux-arm-musleabihf": "4.16.4",
847
+ "@rollup/rollup-linux-arm64-gnu": "4.16.4",
848
+ "@rollup/rollup-linux-arm64-musl": "4.16.4",
849
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.16.4",
850
+ "@rollup/rollup-linux-riscv64-gnu": "4.16.4",
851
+ "@rollup/rollup-linux-s390x-gnu": "4.16.4",
852
+ "@rollup/rollup-linux-x64-gnu": "4.16.4",
853
+ "@rollup/rollup-linux-x64-musl": "4.16.4",
854
+ "@rollup/rollup-win32-arm64-msvc": "4.16.4",
855
+ "@rollup/rollup-win32-ia32-msvc": "4.16.4",
856
+ "@rollup/rollup-win32-x64-msvc": "4.16.4",
857
+ "fsevents": "~2.3.2"
858
+ }
859
+ },
860
+ "node_modules/rxjs": {
861
+ "version": "7.8.1",
862
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
863
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
864
+ "dependencies": {
865
+ "tslib": "^2.1.0"
866
+ }
867
+ },
868
+ "node_modules/source-map-js": {
869
+ "version": "1.2.0",
870
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
871
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
872
+ "dev": true,
873
+ "engines": {
874
+ "node": ">=0.10.0"
875
+ }
876
+ },
877
+ "node_modules/tslib": {
878
+ "version": "2.6.2",
879
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
880
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
881
+ },
882
+ "node_modules/vite": {
883
+ "version": "5.2.10",
884
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
885
+ "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
886
+ "dev": true,
887
+ "dependencies": {
888
+ "esbuild": "^0.20.1",
889
+ "postcss": "^8.4.38",
890
+ "rollup": "^4.13.0"
891
+ },
892
+ "bin": {
893
+ "vite": "bin/vite.js"
894
+ },
895
+ "engines": {
896
+ "node": "^18.0.0 || >=20.0.0"
897
+ },
898
+ "funding": {
899
+ "url": "https://github.com/vitejs/vite?sponsor=1"
900
+ },
901
+ "optionalDependencies": {
902
+ "fsevents": "~2.3.3"
903
+ },
904
+ "peerDependencies": {
905
+ "@types/node": "^18.0.0 || >=20.0.0",
906
+ "less": "*",
907
+ "lightningcss": "^1.21.0",
908
+ "sass": "*",
909
+ "stylus": "*",
910
+ "sugarss": "*",
911
+ "terser": "^5.4.0"
912
+ },
913
+ "peerDependenciesMeta": {
914
+ "@types/node": {
915
+ "optional": true
916
+ },
917
+ "less": {
918
+ "optional": true
919
+ },
920
+ "lightningcss": {
921
+ "optional": true
922
+ },
923
+ "sass": {
924
+ "optional": true
925
+ },
926
+ "stylus": {
927
+ "optional": true
928
+ },
929
+ "sugarss": {
930
+ "optional": true
931
+ },
932
+ "terser": {
933
+ "optional": true
934
+ }
935
+ }
936
+ },
937
+ "node_modules/xss": {
938
+ "version": "1.0.14",
939
+ "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz",
940
+ "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
941
+ "dependencies": {
942
+ "commander": "^2.20.3",
943
+ "cssfilter": "0.0.10"
944
+ },
945
+ "bin": {
946
+ "xss": "bin/xss"
947
+ },
948
+ "engines": {
949
+ "node": ">= 0.10.0"
950
+ }
951
+ }
952
+ }
953
+ }
package.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "niivue-tinygrad",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@niivue/niivue": "^0.48.1"
13
+ },
14
+ "devDependencies": {
15
+ "vite": "^5.2.0"
16
+ }
17
+ }
public/colormap_mindgrab.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "R": [0, 255],
3
+ "G": [0, 0],
4
+ "B": [0, 0],
5
+ "labels": ["background", "brain"]
6
+ }
public/favicon.ico ADDED
public/net_mindgrab.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ff5dc4075c10687a2023236248c1d3ba3d5adfa4a4a0b2150bcdd1e21b5d0554
3
+ size 587188
public/net_mindgrab_tta_axial.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36045f90556ed4969339654f7c6256c5af61ba8fa78e77dc6a67e064ca961f54
3
+ size 849596
public/net_mindgrab_tta_coronal.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36045f90556ed4969339654f7c6256c5af61ba8fa78e77dc6a67e064ca961f54
3
+ size 849596
public/net_mindgrab_tta_sagittal.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36045f90556ed4969339654f7c6256c5af61ba8fa78e77dc6a67e064ca961f54
3
+ size 849596
public/niivue.css ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html {
2
+ height: auto;
3
+ min-height: 100%;
4
+ margin: 0;
5
+ }
6
+ body {
7
+ display: flex;
8
+ flex-direction: column;
9
+ margin: 0;
10
+ min-height: 100%;
11
+ width: 100%;
12
+ position: absolute;
13
+ font-family: system-ui, Arial, Helvetica, sans-serif;
14
+ user-select: none; /* Standard syntax */
15
+ color: white;
16
+ background: #303030;
17
+ }
18
+ header {
19
+ margin: 10px;
20
+ }
21
+ main {
22
+ flex: 1;
23
+ background: #000000;
24
+ position: relative;
25
+ }
26
+ footer {
27
+ margin: 10px;
28
+ }
29
+ canvas {
30
+ position: absolute;
31
+ cursor: crosshair;
32
+ }
33
+ canvas:focus {
34
+ outline: 0px;
35
+ }
36
+ div {
37
+ display: table-row;
38
+ background-color: blue;
39
+ }
40
+ .dropdown {
41
+ float: left;
42
+ overflow: hidden;
43
+ }
44
+ .dropdown .dropbtn {
45
+ font-size: 16px;
46
+ border: none;
47
+ outline: none;
48
+ color: white;
49
+ padding: 12px 12px;
50
+ background-color: #303030;
51
+ font-family: inherit;
52
+ margin: 0;
53
+ }
54
+ .dropdown:hover .dropbtn {
55
+ background-color: #9a9;
56
+ }
57
+ .dropdown-content {
58
+ display: none;
59
+ position: absolute;
60
+ background-color: #303030;
61
+ min-width: 160px;
62
+ border-radius: 5px;
63
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
64
+ z-index: 1;
65
+ }
66
+ .dropdown-content a {
67
+ float: none;
68
+ color: white;
69
+ padding: 12px 16px;
70
+ text-decoration: none;
71
+ display: block;
72
+ text-align: left;
73
+ line-height: 6px;
74
+ }
75
+ .dropdown-content a:hover {
76
+ background-color: #aba;
77
+ }
78
+ .dropdown:hover .dropdown-content {
79
+ display: block;
80
+ }
81
+ .dropdown-item-checked::before {
82
+ position: absolute;
83
+ left: 0.2rem;
84
+ content: "\2022"; /* or '✓' */
85
+ font-weight: 600;
86
+ }
87
+ .divider {
88
+ border-top: 1px solid grey;
89
+ }
90
+ .vertical-divider {
91
+ border-left: 1px solid grey;
92
+ height: 40px;
93
+ }
94
+ .help-text {
95
+ margin: auto;
96
+ max-width: 150px;
97
+ padding: 0 10px;
98
+ }
99
+ .slidecontainer {
100
+ padding: 10px 10px;
101
+ white-space: normal;
102
+ word-break: break-word;
103
+ display: flex;
104
+ align-items: center;
105
+ flex: 0 0 auto;
106
+ }
107
+
108
+ div.footer { width: 100%; display: block; background: #303030;}
109
+ table.footer { width: 100%;height: 100%; table-layout: fixed;}
public/t1_crop.nii.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f5708dd1980fe0017ad70f63d895bae66bd9f5487eb1c38d129591116113613
3
+ size 3068922
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tensor-utils.js ADDED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as tf from '@tensorflow/tfjs'
2
+ import { BWLabeler } from './bwlabels.js'
3
+
4
+ export async function addZeroPaddingTo3dTensor(tensor3d, rowPadArr = [1, 1], colPadArr = [1, 1], depthPadArr = [1, 1]) {
5
+ if (tensor3d.rank !== 3) {
6
+ throw new Error('Tensor must be 3D')
7
+ }
8
+ return tensor3d.pad([rowPadArr, colPadArr, depthPadArr])
9
+ }
10
+
11
+ export async function applyMriThreshold(tensor, percentage) {
12
+ // Perform asynchronous operations outside of tf.tidy
13
+ const maxTensor = tensor.max()
14
+ const thresholdTensor = maxTensor.mul(percentage)
15
+ const threshold = await thresholdTensor.data() // Extracts the threshold value
16
+
17
+ // Dispose tensors not needed anymore
18
+ maxTensor.dispose()
19
+ thresholdTensor.dispose()
20
+
21
+ // Use tf.tidy for synchronous operations
22
+ return tf.tidy(() => {
23
+ const dataForProcessing = tensor.clone()
24
+
25
+ // Thresholding (assuming background has very low values compared to the head)
26
+ const mask = dataForProcessing.greater(threshold[0])
27
+ // -- const denoisedMriData = dataForProcessing.mul(mask)
28
+
29
+ // No need to manually dispose dataForProcessing and mask, as tf.tidy() will dispose them auto.
30
+ return mask
31
+ })
32
+
33
+ // -- return denoisedMriData
34
+ }
35
+
36
+ export async function binarizeVolumeDataTensor(volumeDataTensor) {
37
+ const alpha = 0
38
+ // element-wise: (x > 0 ? 1 : alpha * x ); e.g. Tenosr [0, 0.9, 0.8, -3] => Tensor [0, 1, 1, 0]
39
+ return volumeDataTensor.step(alpha)
40
+ }
41
+
42
+ async function calculateQuantiles(tensor, lowerQuantile = 0.01, upperQuantile = 0.99) {
43
+ // Flatten the tensor
44
+ const flatTensor = tensor.flatten()
45
+
46
+ // Convert the flattened tensor to an array to sort it
47
+ const flatArray = await flatTensor.array()
48
+ flatArray.sort((a, b) => a - b) // Sort the array in ascending order
49
+
50
+ // Convert the sorted array back to a tensor
51
+ const sortedTensor = tf.tensor1d(flatArray)
52
+
53
+ // Calculate the indices for the quantiles
54
+ const numElements = sortedTensor.shape[0]
55
+ const lowIndex = Math.floor(numElements * lowerQuantile)
56
+ const highIndex = Math.ceil(numElements * upperQuantile) - 1 // Subtract 1 because indices are 0-based
57
+
58
+ // Slice the sorted tensor to get qmin and qmax
59
+ const qmin = sortedTensor.slice(lowIndex, 1) // Get the value at the low index
60
+ const qmax = sortedTensor.slice(highIndex, 1) // Get the value at the high index
61
+
62
+ // Get the actual values from the tensors
63
+ const qminValue = (await qmin.array())[0]
64
+ const qmaxValue = (await qmax.array())[0]
65
+
66
+ // Clean up tensors to free memory
67
+ flatTensor.dispose()
68
+ sortedTensor.dispose()
69
+ qmin.dispose()
70
+ qmax.dispose()
71
+
72
+ return { qmin: qminValue, qmax: qmaxValue }
73
+ }
74
+
75
+ export async function convByOutputChannelAndInputSlicing(input, filter, biases, stride, pad, dilationRate, sliceSize) {
76
+ const inChannels = input.shape[4]
77
+ const outChannels = filter.shape[4]
78
+
79
+ // Create an empty array to hold the output channels
80
+ let outputChannels = null
81
+
82
+ // Slice the input tensor and process one output channel at a time
83
+ for (let channel = 0; channel < outChannels; channel++) {
84
+ const numSlices = Math.ceil(inChannels / sliceSize)
85
+ const biasesSlice = biases.slice([channel], [1])
86
+ let outputChannel = null
87
+
88
+ for (let i = 0; i < numSlices; i++) {
89
+ const startChannel = i * sliceSize
90
+ const endChannel = Math.min((i + 1) * sliceSize, inChannels)
91
+
92
+ // Only proceed if there are channels to process
93
+ if (startChannel < inChannels) {
94
+ const resultSlice = tf.tidy(() => {
95
+ const inputSlice = input.slice([0, 0, 0, 0, startChannel], [-1, -1, -1, -1, endChannel - startChannel])
96
+ const filterSlice = filter.slice([0, 0, 0, startChannel, channel], [-1, -1, -1, endChannel - startChannel, 1])
97
+ // Perform the convolution for the current slice and output channel
98
+ return tf.conv3d(inputSlice, filterSlice, stride, pad, 'NDHWC', dilationRate)
99
+ })
100
+
101
+ if (outputChannel === null) {
102
+ outputChannel = resultSlice
103
+ } else {
104
+ const updatedOutputChannel = outputChannel.add(resultSlice)
105
+ outputChannel.dispose()
106
+ resultSlice.dispose()
107
+ outputChannel = updatedOutputChannel
108
+ }
109
+ }
110
+ }
111
+
112
+ // Add the biases to the accumulated convolutions for this channel
113
+ const biasedOutputChannel = outputChannel.add(biasesSlice)
114
+ outputChannel.dispose()
115
+ biasesSlice.dispose()
116
+
117
+ // Accumulate the channel to the output array
118
+ if (outputChannels == null) {
119
+ outputChannels = biasedOutputChannel
120
+ } else {
121
+ const updatedOutputChannels = await tf.concat([outputChannels, biasedOutputChannel], 4)
122
+ biasedOutputChannel.dispose()
123
+ outputChannels.dispose()
124
+ outputChannels = updatedOutputChannels
125
+ }
126
+ }
127
+
128
+ return outputChannels
129
+ }
130
+
131
+ export async function draw3dObjBoundingVolume(unstackOutVolumeTensor, opts, modelEntry, callbackImg) {
132
+ const allOutputSlices3DCC = []
133
+
134
+ // dataSync() using to flatten array. Takes around 1.5 s
135
+ for (let sliceTensorIdx = 0; sliceTensorIdx < unstackOutVolumeTensor.length; sliceTensorIdx++) {
136
+ allOutputSlices3DCC[sliceTensorIdx] = Array.from(unstackOutVolumeTensor[sliceTensorIdx].dataSync())
137
+ }
138
+
139
+ // Use this conversion to download output slices as nii file. Takes around 30 ms
140
+ // does not use `push` to avoid stack overflows. In future: consider .set() with typed arrays
141
+ const allOutputSlices3DCC1DimArray = new Array(allOutputSlices3DCC[0].length * allOutputSlices3DCC.length)
142
+ let index = 0
143
+ for (let sliceIdx = 0; sliceIdx < allOutputSlices3DCC.length; sliceIdx++) {
144
+ for (let i = 0; i < allOutputSlices3DCC[sliceIdx].length; i++) {
145
+ allOutputSlices3DCC1DimArray[index++] = allOutputSlices3DCC[sliceIdx][i]
146
+ }
147
+ }
148
+ console.log('Done with allOutputSlices3DCC1DimArray ')
149
+ const brainMaskTensor1d = await binarizeVolumeDataTensor(tf.tensor1d(allOutputSlices3DCC1DimArray))
150
+ const brainOut = Array.from(brainMaskTensor1d.dataSync())
151
+ callbackImg(brainOut, opts, modelEntry)
152
+ }
153
+ // return first and last non-zero voxel in row (dim = 0), column (1) or slice (2) dimension
154
+ async function firstLastNonZero(tensor3D, dim = 0) {
155
+ let mxs = []
156
+ if (dim === 0) {
157
+ mxs = await tensor3D.max(2).max(1).arraySync()
158
+ } else if (dim === 1) {
159
+ mxs = await tensor3D.max(2).max(0).arraySync()
160
+ } else {
161
+ mxs = await tensor3D.max(1).max(0).arraySync()
162
+ }
163
+ let mn = mxs.length
164
+ let mx = 0
165
+ for (let i = 0; i < mxs.length; i++) {
166
+ if (mxs[i] > 0) {
167
+ mn = i
168
+ break
169
+ }
170
+ }
171
+ for (let i = mxs.length - 1; i >= 0; i--) {
172
+ if (mxs[i] > 0) {
173
+ mx = i
174
+ break
175
+ }
176
+ }
177
+ return [mn, mx]
178
+ }
179
+
180
+ export async function firstLastNonZero3D(tensor3D) {
181
+ const [row_min, row_max] = await firstLastNonZero(tensor3D, 0)
182
+ const [col_min, col_max] = await firstLastNonZero(tensor3D, 1)
183
+ const [depth_min, depth_max] = await firstLastNonZero(tensor3D, 2)
184
+ console.log('row min and max :', row_min, row_max)
185
+ console.log('col min and max :', col_min, col_max)
186
+ console.log('depth min and max :', depth_min, depth_max)
187
+ return [row_min, row_max, col_min, col_max, depth_min, depth_max]
188
+ }
189
+
190
+ /*
191
+ //simpler function, but x4 slower
192
+ export async function firstLastNonZero3D(tensor3D) {
193
+ const coords = await tf.whereAsync(tensor3D)
194
+ const row_min = coords.min(0).arraySync()[0]
195
+ const row_max = coords.max(0).arraySync()[0]
196
+ const col_min = coords.min(0).arraySync()[1]
197
+ const col_max = coords.max(0).arraySync()[1]
198
+ const depth_min = coords.min(0).arraySync()[2]
199
+ const depth_max = coords.max(0).arraySync()[2]
200
+ coords.dispose()
201
+ return [row_min, row_max, col_min, col_max, depth_min, depth_max]
202
+ }
203
+ */
204
+
205
+ export async function generateBrainMask(
206
+ unstackOutVolumeTensor,
207
+ num_of_slices,
208
+ slice_height,
209
+ slice_width,
210
+ modelEntry,
211
+ opts,
212
+ callbackUI,
213
+ callbackImg,
214
+ isFinalImage = true
215
+ ) {
216
+ if (unstackOutVolumeTensor[0].dtype !== 'int32') {
217
+ callbackUI('', -1, 'generateBrainMask assumes int32')
218
+ }
219
+ if (modelEntry.preModelPostProcess) {
220
+ callbackUI('', -1, 'generateBrainMask assumes BWLabeler instead of preModelPostProcess')
221
+ }
222
+ const numSlices = unstackOutVolumeTensor.length
223
+ const numPixels2D = unstackOutVolumeTensor[0].size
224
+ const numVox3D = numSlices * numPixels2D
225
+ // preallocate to reduce heap usage
226
+ const brainOut = new Int32Array(numVox3D)
227
+ let offset = 0
228
+ for (let i = 0; i < numSlices; i++) {
229
+ brainOut.set(unstackOutVolumeTensor[i].dataSync(), offset)
230
+ offset += numPixels2D
231
+ }
232
+ for (let i = 0; i < numVox3D; i++) {
233
+ brainOut[i] = brainOut[i] !== 0 ? 1 : 0
234
+ }
235
+ if (isFinalImage || opts.showPhase1Output) {
236
+ // all done
237
+ callbackImg(brainOut, opts, modelEntry)
238
+ callbackUI('Segmentation finished', 0)
239
+ }
240
+ return tf.tensor(brainOut, [num_of_slices, slice_height, slice_width])
241
+ }
242
+
243
+ export async function generateOutputSlicesV2(
244
+ img,
245
+ OutVolumeTensorShape,
246
+ OutVolumeTensorType,
247
+ num_of_slices,
248
+ numSegClasses,
249
+ slice_height,
250
+ slice_width,
251
+ modelEntry,
252
+ opts,
253
+ niftiImage
254
+ ) {
255
+ // Convert all slices into 1 Dim array
256
+ if (opts.isPostProcessEnable) {
257
+ const BWInstance = new BWLabeler()
258
+ const dim = new Uint32Array(OutVolumeTensorShape)
259
+ const conn = 26 // Example connectivity
260
+ const binarize = true
261
+ const onlyLargestClusterPerClass = true
262
+ const [_labelCount, labeledImage] = BWInstance.bwlabel(img, dim, conn, binarize, onlyLargestClusterPerClass)
263
+ for (let i = 0; i < img.length; i++) {
264
+ img[i] *= labeledImage[i]
265
+ }
266
+ } // if isPostProcessEnable
267
+ const typedArrayConstructor = {
268
+ float32: Float32Array,
269
+ int32: Int32Array
270
+ // Add other cases as needed for different dtypes
271
+ }[OutVolumeTensorType]
272
+ // Create a new TypedArray from img with the same type as outLabelVolume
273
+ const allOutputSlices3DCC1DimArray = new Uint8Array(img)
274
+ switch (modelEntry.type) {
275
+ case 'Brain_Masking': {
276
+ const brainMask = new Uint8Array(allOutputSlices3DCC1DimArray.length)
277
+ for (let i = 0; i < allOutputSlices3DCC1DimArray.length; i++) {
278
+ brainMask[i] = allOutputSlices3DCC1DimArray[i] !== 0 ? 1 : 0
279
+ }
280
+ return brainMask
281
+ }
282
+ case 'Brain_Extraction': {
283
+ const maskedData = new Uint8Array(allOutputSlices3DCC1DimArray.length)
284
+ for (let i = 0; i < allOutputSlices3DCC1DimArray.length; i++) {
285
+ // Create the mask - 1 where the value is non-zero, 0 where it is zero.
286
+ const maskValue = allOutputSlices3DCC1DimArray[i] !== 0 ? 1 : 0
287
+ // Apply the mask to the data - multiply by the mask value.
288
+ maskedData[i] = niftiImage[i] * maskValue
289
+ }
290
+ return maskedData
291
+ }
292
+ }
293
+ return img
294
+ }
295
+
296
+ export async function getAllSlicesDataAsTF3D(num_of_slices, niftiHeader, niftiImage) {
297
+ // Get nifti dimensions
298
+ const cols = niftiHeader.dims[1] // Slice width
299
+ const rows = niftiHeader.dims[2] // Slice height
300
+ let typedData
301
+ if (niftiHeader.datatypeCode === 2) {
302
+ // enum from nvimage/utils DT_UINT8 = 2
303
+ typedData = new Uint8Array(niftiImage)
304
+ } else if (niftiHeader.datatypeCode === 4) {
305
+ // DT_INT16 = 4
306
+ typedData = new Int16Array(niftiImage)
307
+ } else if (niftiHeader.datatypeCode === 8) {
308
+ // DT_INT32 = 8
309
+ typedData = new Int32Array(niftiImage)
310
+ } else if (niftiHeader.datatypeCode === 16) {
311
+ // DT_FLOAT32 = 16
312
+ typedData = new Float32Array(niftiImage)
313
+ } else if (niftiHeader.datatypeCode === 64) {
314
+ // DT_FLOAT64 = 64
315
+ typedData = new Float64Array(niftiImage)
316
+ } else if (niftiHeader.datatypeCode === 256) {
317
+ // DT_INT8 = 256
318
+ typedData = new Int8Array(niftiImage)
319
+ } else if (niftiHeader.datatypeCode === 512) {
320
+ // DT_UINT16 = 512
321
+ typedData = new Uint16Array(niftiImage)
322
+ } else if (niftiHeader.datatypeCode === 768) {
323
+ // DT_UINT32 = 768
324
+ typedData = new Uint32Array(niftiImage)
325
+ } else {
326
+ return
327
+ }
328
+ const allSlices_2D = []
329
+ let offset3D = 0
330
+ // Draw pixels
331
+ for (let slice = 0; slice < num_of_slices; slice++) {
332
+ const slice = new Array(rows * cols)
333
+ let offset2D = 0
334
+ for (let row = 0; row < rows; row++) {
335
+ for (let col = 0; col < cols; col++) {
336
+ const value = typedData[offset3D++]
337
+ // Create 1Dim Array of pixel value, this 1 dim represents one channel
338
+ slice[offset2D++] = value & 0xff
339
+ }
340
+ }
341
+ allSlices_2D.push(tf.tensor(slice, [rows, cols])) // slice_height, slice_width
342
+ }
343
+ const allSlices_3D = tf.stack(allSlices_2D)
344
+ tf.dispose(allSlices_2D)
345
+ return allSlices_3D
346
+ }
347
+
348
+ export async function getModelNumLayers(modelObj) {
349
+ return modelObj.layers.length
350
+ }
351
+
352
+ export async function getModelNumParameters(modelObj) {
353
+ let numParameters = 0
354
+ for (let layerIdx = 0; layerIdx < modelObj.layers.length; layerIdx++) {
355
+ numParameters += modelObj.layers[layerIdx].countParams()
356
+ }
357
+ return numParameters
358
+ }
359
+
360
+ export async function isModelChnlLast(modelObj) {
361
+ for (let layerIdx = 0; layerIdx < modelObj.layers.length; layerIdx++) {
362
+ if (modelObj.layersByDepth[layerIdx][0].dataFormat) {
363
+ return modelObj.layersByDepth[layerIdx][0].dataFormat === 'channelsLast'
364
+ }
365
+ }
366
+ }
367
+
368
+ export async function load_model(modelUrl) {
369
+ return await tf.loadLayersModel(modelUrl)
370
+ }
371
+
372
+ export async function minMaxNormalizeVolumeData(volumeData) {
373
+ // Normalize the data to the range 0 - 1 using min-max scaling
374
+ const volumeData_Max = volumeData.max()
375
+ const volumeData_Min = volumeData.min()
376
+ const normalizedSlices_3d = await volumeData.sub(volumeData_Min).div(volumeData_Max.sub(volumeData_Min))
377
+ return normalizedSlices_3d
378
+ }
379
+
380
+ function processTensorInChunks(inputTensor, filterWeights, chunkSize) {
381
+ // Assuming inputTensor's shape: [batch, depth, height, width, inChannels]
382
+ // and filterWeights's shape: [filterDepth, filterHeight, filterWidth, inChannels, outChannels]
383
+ const stride = 1
384
+ const pad = 0
385
+ const dilationRate = 1
386
+ const inChannels = inputTensor.shape[4]
387
+ const numSlices = Math.ceil(inChannels / chunkSize)
388
+
389
+ let accumulatedResult = null
390
+ for (let i = 0; i < numSlices; i++) {
391
+ const startChannel = i * chunkSize
392
+ const endChannel = Math.min((i + 1) * chunkSize, inChannels)
393
+ const channels = endChannel - startChannel
394
+
395
+ const inputSlice = tf.tidy(() => {
396
+ // Slice the input tensor to get the current chunk
397
+ return inputTensor.slice([0, 0, 0, 0, startChannel], [-1, -1, -1, -1, channels])
398
+ })
399
+ const filterSlice = tf.tidy(() => {
400
+ // Slice the filter weights to match the input tensor's current chunk
401
+ return filterWeights.slice([0, 0, 0, startChannel, 0], [-1, -1, -1, channels, -1])
402
+ })
403
+
404
+ const resultSlice = tf.conv3d(inputSlice, filterSlice, stride, pad, 'NDHWC', dilationRate)
405
+ // Clean up the slices to free memory
406
+ inputSlice.dispose()
407
+ filterSlice.dispose()
408
+
409
+ // Squeeze the result slice to remove dimensions of size 1
410
+ const squeezedResultSlice = tf.squeeze(resultSlice)
411
+ resultSlice.dispose() // Dispose of the original resultSlice after squeezing
412
+
413
+ if (accumulatedResult === null) {
414
+ accumulatedResult = squeezedResultSlice
415
+ } else {
416
+ // Accumulate the result by adding the new result slice to it
417
+ const newAccumulatedResult = accumulatedResult.add(squeezedResultSlice)
418
+
419
+ // Dispose of the previous accumulatedResult and squeezedResultSlice
420
+ accumulatedResult.dispose()
421
+ // Dispose of squeezedResultSlice only if it wasn't assigned to accumulatedResult
422
+ if (accumulatedResult !== squeezedResultSlice) {
423
+ squeezedResultSlice.dispose()
424
+ }
425
+ // Update accumulatedResult with the new result
426
+ accumulatedResult = newAccumulatedResult
427
+ }
428
+
429
+ tf.tidy(() => {
430
+ tf.matMul(tf.zeros([1, 1]), tf.zeros([1, 1]))
431
+ })
432
+ }
433
+
434
+ return accumulatedResult
435
+ }
436
+
437
+ export async function quantileNormalizeVolumeData(tensor, lowerQuantile = 0.05, upperQuantile = 0.95) {
438
+ // Call calculateQuantiles and wait for the result
439
+ const { qmin, qmax } = await calculateQuantiles(tensor, lowerQuantile, upperQuantile)
440
+
441
+ // Convert qmin and qmax back to scalars
442
+ const qminScalar = tf.scalar(qmin)
443
+ const qmaxScalar = tf.scalar(qmax)
444
+
445
+ // Perform the operation: (tensor - qmin) / (qmax - qmin)
446
+ const resultTensor = tensor.sub(qminScalar).div(qmaxScalar.sub(qminScalar))
447
+
448
+ // Dispose of the created scalars to free memory
449
+ qminScalar.dispose()
450
+ qmaxScalar.dispose()
451
+
452
+ // Return the resulting tensor
453
+ return resultTensor
454
+ }
455
+
456
+ export async function removeZeroPaddingFrom3dTensor(tensor3d, rowPad = 1, colPad = 1, depthPad = 1) {
457
+ if (tensor3d.rank !== 3) {
458
+ throw new Error('Tensor must be 3D')
459
+ }
460
+ const [h, w, d] = tensor3d.shape
461
+ return tensor3d.slice([rowPad, colPad, depthPad], [h - 2 * rowPad, w - 2 * colPad, d - 2 * depthPad])
462
+ }
463
+
464
+ export async function resizeWithZeroPadding(croppedTensor3d, newDepth, newHeight, newWidth, refVoxel, boundVolSizeArr) {
465
+ const row_pad_befor = refVoxel[0]
466
+ const col_pad_befor = refVoxel[1]
467
+ const depth_pad_befor = refVoxel[2]
468
+ // last and lower volume voxel
469
+ const row_max = row_pad_befor + boundVolSizeArr[0] - 1 // size [2, 2, 2] means 2 voxels total in each dim
470
+ const col_max = col_pad_befor + boundVolSizeArr[1] - 1
471
+ const depth_max = depth_pad_befor + boundVolSizeArr[2] - 1
472
+
473
+ const row_pad_after = newHeight - row_max - 1 > 0 ? newHeight - row_max - 1 : 0
474
+ const col_pad_after = newWidth - col_max - 1 > 0 ? newWidth - col_max - 1 : 0
475
+ const depth_pad_after = newDepth - depth_max - 1 > 0 ? newDepth - depth_max - 1 : 0
476
+
477
+ return croppedTensor3d.pad([
478
+ [row_pad_befor, row_pad_after],
479
+ [col_pad_befor, col_pad_after],
480
+ [depth_pad_befor, depth_pad_after]
481
+ ])
482
+ }
483
+
484
+ export class SequentialConvLayer {
485
+ constructor(model, chunkSize, isChannelLast, callbackUI, isWebWorker = true) {
486
+ this.model = model
487
+ this.outChannels = model.outputLayers[0].kernel.shape[4]
488
+ this.chunkSize = chunkSize
489
+ this.isChannelLast = isChannelLast
490
+ this.callbackUI = callbackUI
491
+ this.isWebWorker = isWebWorker
492
+ }
493
+
494
+ /**
495
+ * Apply sequential convolution layer
496
+ * @since 3.0.0
497
+ * @member SequentialConvLayer
498
+ * @param {tf.Tensor} inputTensor e.g. [ 1, 256, 256, 256, 5 ]
499
+ * @return {outC}
500
+ */
501
+
502
+ async apply(inputTensor) {
503
+ const oldDeleteTextureThreshold = tf.ENV.get('WEBGL_DELETE_TEXTURE_THRESHOLD')
504
+ tf.ENV.set('WEBGL_DELETE_TEXTURE_THRESHOLD', 0)
505
+
506
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
507
+ const self = this
508
+ // Important to avoid "undefined" class var members inside the timer.
509
+ // "this" has another meaning inside the timer.
510
+
511
+ // document.getElementById("progressBarChild").parentElement.style.visibility = "visible"
512
+ const startTime = performance.now()
513
+
514
+ const convLayer = self.model.layers[self.model.layers.length - 1]
515
+ const weights = convLayer.getWeights()[0] //
516
+ const biases = convLayer.getWeights()[1]
517
+ const outputShape = self.isChannelLast ? inputTensor.shape.slice(1, -1) : inputTensor.shape.slice(2)
518
+ // -- e.g. outputShape : [256,256,256] or cropped Dim
519
+ // -- if inputTensor [ 1, D, H, W, 50 ], channelLast true -> outputShape : outputShape [D, H, W]
520
+ // -- if inputTensor [ 1, 50, D, H, W ], channelLast false -> outputShape : outputShape [D, H, W]
521
+
522
+ let outB = tf.mul(tf.ones(outputShape), -10000)
523
+ // -- e.g. outB.shape [256,256,256]
524
+ let outC = tf.zeros(outputShape)
525
+ // -- e.g. outC.shape [256,256,256]
526
+ let chIdx = 0
527
+
528
+ // console.log("---------------------------------------------------------")
529
+ console.log(' channel loop')
530
+
531
+ while (true) {
532
+ tf.engine().startScope() // Start TensorFlow.js scope
533
+ /* console.log('=======================')
534
+ const memoryInfo0 = await tf.memory()
535
+ console.log(`| Number of Tensors: ${memoryInfo0.numTensors}`)
536
+ console.log(`| Number of Data Buffers: ${memoryInfo0.numDataBuffers}`) */
537
+
538
+ const result = await tf.tidy(() => {
539
+ const filterWeights = weights.slice([0, 0, 0, 0, chIdx], [-1, -1, -1, -1, 1])
540
+ // -- e.g. filterWeights.shape [ 1, 1, 1, 5, 1 ]
541
+ const filterBiases = biases.slice([chIdx], [1])
542
+ // -- e.g. filterBiases.shape [1] -> Tensor [-0.7850812]
543
+ const outA = processTensorInChunks(inputTensor, filterWeights, Math.min(self.chunkSize, self.outChannels)).add(
544
+ filterBiases
545
+ )
546
+ const greater = tf.greater(outA, outB)
547
+ const newoutB = tf.where(greater, outA, outB)
548
+ const newoutC = tf.where(greater, tf.fill(outC.shape, chIdx), outC)
549
+ // Dispose the old tensors before reassigning
550
+ tf.dispose([outB, outC, filterWeights, filterBiases, outA, greater])
551
+ // Dummy operation to trigger cleanup
552
+ tf.tidy(() => tf.matMul(tf.ones([1, 1]), tf.ones([1, 1])))
553
+ return [newoutC, newoutB]
554
+ })
555
+ console.log('=======================')
556
+ self.callbackUI(`Iteration ${chIdx}`, chIdx / self.outChannels)
557
+ if (!self.isWebWorker) {
558
+ // allow user interface to refresh
559
+ await new Promise((resolve) => setTimeout(resolve, 17))
560
+ }
561
+ const memoryInfo = await tf.memory()
562
+ console.log(`Number of Tensors: ${memoryInfo.numTensors}`)
563
+ console.log(`Number of Data Buffers: ${memoryInfo.numDataBuffers}`)
564
+ console.log(`Megabytes In Use: ${(memoryInfo.numBytes / 1048576).toFixed(3)} MB`)
565
+ if (memoryInfo.unreliable) {
566
+ console.log(`Unreliable: ${memoryInfo.unreliable}`)
567
+ }
568
+ // Dispose of previous values before assigning new tensors to outC and outB
569
+ if (typeof outC !== 'undefined') {
570
+ outC.dispose()
571
+ }
572
+ if (typeof outB !== 'undefined') {
573
+ outB.dispose()
574
+ }
575
+ // Assign the new values to outC and outB
576
+ outC = tf.keep(result[0])
577
+ outB = tf.keep(result[1])
578
+ // // Assign the new values to outC and outB
579
+ // outC = result[0]
580
+ // outB = result[1]
581
+ tf.engine().endScope()
582
+
583
+ if (chIdx === self.outChannels - 1) {
584
+ // document.getElementById("progressBarChild").style.width = 0 + "%"
585
+ tf.dispose(outB)
586
+ const endTime = performance.now()
587
+ const executionTime = endTime - startTime
588
+ console.log(`Execution time for output layer: ${executionTime} milliseconds`)
589
+ tf.ENV.set('WEBGL_DELETE_TEXTURE_THRESHOLD', oldDeleteTextureThreshold)
590
+ return outC
591
+ } else {
592
+ chIdx++
593
+
594
+ // the seemingly strange sequence of operations
595
+ // below prevents tfjs from uncontrolably
596
+ // grabbing buffers, even when all tensors have
597
+ // already been disposed
598
+
599
+ const outCShape = outC.shape
600
+ const outCdata = outC.dataSync()
601
+ const outBShape = outC.shape
602
+ const outBdata = outB.dataSync()
603
+ outC.dispose()
604
+ outB.dispose()
605
+ // tf.disposeVariables()
606
+ outC = tf.tensor(outCdata, outCShape)
607
+ outB = tf.tensor(outBdata, outBShape)
608
+
609
+ // document.getElementById("progressBarChild").style.width = (chIdx + 1) * 100 / self.outChannels + "%"
610
+ }
611
+ }
612
+ }
613
+ } // <<<< End of class
vite.config.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+
3
+ export default defineConfig({
4
+ root: '.',
5
+ base: '/niivue-tinygrad/',
6
+ server: {
7
+ open: 'index.html',
8
+ },
9
+ })