@niivue/niivue 0.12.2 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niivue/niivue",
3
- "version": "0.12.2",
3
+ "version": "0.13.2",
4
4
  "description": "minimal webgl2 nifti image viewer",
5
5
  "main": "./src/niivue.js",
6
6
  "unpkg": "./dist/niivue.umd.js",
@@ -12,7 +12,7 @@
12
12
  "demo-win": "npm run build-win && npx http-server demos/",
13
13
  "copy-test": "cp ./dist/niivue.umd.js ./tests/niivue.umd.js",
14
14
  "copy-test-win": "copy .\\dist\\niivue.umd.js .\\tests\\niivue.umd.js",
15
- "copy-demo": "cp ./dist/niivue.umd.js ./demos/niivue.umd.js",
15
+ "copy-demo": "cp -r ./dist/* ./demos/",
16
16
  "copy-demo-win": "copy .\\dist\\niivue.umd.js .\\demos\\niivue.umd.js",
17
17
  "test": "npm run build && jest --maxWorkers=1",
18
18
  "test-win": "npm run build-win && jest --maxWorkers=1",
@@ -42,22 +42,22 @@
42
42
  },
43
43
  "homepage": "https://github.com/niivue/niivue#readme",
44
44
  "dependencies": {
45
- "gl-matrix": "^3.3.0",
45
+ "gl-matrix": "^3.4.3",
46
46
  "nifti-reader-js": "^0.5.4",
47
- "rxjs": "^7.1.0",
47
+ "rxjs": "^7.4.0",
48
48
  "uuid": "^8.3.2"
49
49
  },
50
50
  "devDependencies": {
51
- "clean-jsdoc-theme": "^3.2.10",
52
- "eslint": "^8.2.0",
51
+ "clean-jsdoc-theme": "^3.3.1",
52
+ "eslint": "^8.4.1",
53
53
  "express": "^4.17.1",
54
- "http-server": "^0.12.3",
55
- "jest": "^27.0.6",
54
+ "http-server": "^14.0.0",
55
+ "jest": "^27.4.4",
56
56
  "jest-image-snapshot": "^4.5.1",
57
- "jest-puppeteer": "^5.0.4",
57
+ "jest-puppeteer": "^6.0.2",
58
58
  "jsdoc": "^3.6.7",
59
- "prettier": "^2.3.2",
59
+ "prettier": "^2.5.1",
60
60
  "puppeteer": "^9.1.1",
61
- "vite": "^2.6.14"
61
+ "vite": "^2.7.1"
62
62
  }
63
63
  }
@@ -0,0 +1,51 @@
1
+ let granule_size = 8;
2
+ let bits_per_byte = 8;
3
+ let bits_per_byte_log2 = 3;
4
+
5
+ function assert(c, msg) {
6
+ if (!c) throw new Error(msg);
7
+ }
8
+ function power_of_two(x) {
9
+ return x && (x & (x - 1)) == 0;
10
+ }
11
+ function assert_power_of_two(x) {
12
+ assert(power_of_two(x), `not power of two: ${x}`);
13
+ }
14
+ function aligned(x, y) {
15
+ assert_power_of_two(y);
16
+ return (x & (y - 1)) == 0;
17
+ }
18
+
19
+ function assert_aligned(x, y) {
20
+ assert(aligned(x, y), `bad alignment: ${x} % ${y}`);
21
+ }
22
+
23
+ export class HeapVerifier {
24
+ constructor(maxbytes) {
25
+ this.maxwords = maxbytes / granule_size;
26
+ this.state = new Uint8Array(this.maxwords / bits_per_byte);
27
+ this.allocations = new Map();
28
+ }
29
+ acquire(offset, len) {
30
+ assert_aligned(offset, granule_size);
31
+ for (let i = 0; i < len; i += granule_size) {
32
+ let bit = (offset + i) / granule_size;
33
+ let byte = bit >> bits_per_byte_log2;
34
+ let mask = 1 << (bit & (bits_per_byte - 1));
35
+ assert((this.state[byte] & mask) == 0, "word in use");
36
+ this.state[byte] |= mask;
37
+ }
38
+ this.allocations.set(offset, len);
39
+ }
40
+ release(offset) {
41
+ assert(this.allocations.has(offset));
42
+ let len = this.allocations.get(offset);
43
+ this.allocations.delete(offset);
44
+ for (let i = 0; i < len; i += granule_size) {
45
+ let bit = (offset + i) / granule_size;
46
+ let byte = bit >> bits_per_byte_log2;
47
+ let mask = 1 << (bit & (bits_per_byte - 1));
48
+ this.state[byte] &= ~mask;
49
+ }
50
+ }
51
+ }
package/src/index.html CHANGED
@@ -51,19 +51,19 @@
51
51
  <!-- <script type="module" src="./index.js">
52
52
  </script> -->
53
53
  <script type="module">
54
- import {Niivue} from './niivue.js'
54
+ import { Niivue } from './niivue.js'
55
55
  // console.log(Niivue)
56
56
  var volumeList1 = [
57
57
  // first item is brackground image
58
58
  {
59
- // url: "./mni152.nii.gz",//"./images/RAS.nii.gz", "./images/spm152.nii.gz",
60
- url: "https://openneuro.org/crn/datasets/ds002328/snapshots/1.0.0/files/sub-01:anat:sub-01_T1w.nii.gz",
59
+ url: "./mni152.nii.gz",//"./images/RAS.nii.gz", "./images/spm152.nii.gz",
61
60
  colorMap: "gray",
62
61
  opacity: 1,
63
62
  visible: true,
64
63
  }
65
64
  ]
66
65
  var nv1 = new Niivue()
66
+ nv1.setRadiologicalConvention(false)
67
67
  nv1.attachTo('gl1')
68
68
  nv1.loadVolumes(volumeList1)
69
69
  nv1.setSliceType(nv1.sliceTypeMultiplanar)
@@ -0,0 +1,33 @@
1
+ import { HeapVerifier } from "./heap-verifier";
2
+
3
+ export class LinearMemory {
4
+ constructor({ initial = 2048, maximum = 2048 }) {
5
+ this.memory = new WebAssembly.Memory({ initial, maximum });
6
+ this.verifier = new HeapVerifier(maximum * 65536);
7
+ }
8
+ record_malloc(ptr, len) {
9
+ this.verifier.acquire(ptr, len);
10
+ }
11
+ record_free(ptr) {
12
+ this.verifier.release(ptr);
13
+ }
14
+ read_string(offset) {
15
+ let view = new Uint8Array(this.memory.buffer);
16
+ let bytes = [];
17
+ for (let byte = view[offset]; byte; byte = view[++offset]) bytes.push(byte);
18
+ return String.fromCharCode(...bytes);
19
+ }
20
+ log(str) {
21
+ console.log(`wasm log: ${str}`);
22
+ }
23
+ log_i(str, i) {
24
+ console.log(`wasm log: ${str}: ${i}`);
25
+ }
26
+ env() {
27
+ return {
28
+ memory: this.memory,
29
+ wasm_log: (off) => this.log(this.read_string(off)),
30
+ wasm_log_i: (off, i) => this.log_i(this.read_string(off), i),
31
+ };
32
+ }
33
+ }
package/src/niivue.js CHANGED
@@ -29,7 +29,17 @@ import { NVImage } from "./nvimage.js";
29
29
  export { NVImage } from "./nvimage";
30
30
  import defaultFontPNG from "./fonts/Roboto-Regular.png";
31
31
  import defaultFontMetrics from "./fonts/Roboto-Regular.json";
32
-
32
+ // import createModule from "./process-image";
33
+ // import init from "./process-image.wasm";
34
+ // import { LinearMemory } from "./linear-memory.js";
35
+ // import workerString from "./worker.js?raw";
36
+ // const workerBlob = new Blob([workerString], { type: "text/javascript" });
37
+ // const workerURL = URL.createObjectURL(workerBlob);
38
+ // const worker = new Worker(workerURL, { type: "classic" });
39
+ import NiiMathWorker from "./worker?worker";
40
+ import { v4 as uuidv4 } from "uuid";
41
+
42
+ var Module;
33
43
  /**
34
44
  * @class Niivue
35
45
  * @description
@@ -49,6 +59,7 @@ import defaultFontMetrics from "./fonts/Roboto-Regular.json";
49
59
  * @param {string} [options.clipPlaneHotKey="KeyC"] the keyboard key used to cycle through clip plane orientations. The default is "c"
50
60
  * @param {string} [options.viewModeHotKey="KeyV"] the keyboard key used to cycle through view modes. The default is "v"
51
61
  * @param {number} [options.keyDebounceTime=50] the keyUp debounce time in milliseconds. The default is 50 ms. You must wait this long before a new hot-key keystroke will be registered by the event listener
62
+ * @param {boolean} [options.isRadiologicalConvention=false] whether or not to use radiological convention in the display
52
63
  * @example
53
64
  * let niivue = new Niivue({crosshairColor: [0,1,0,0.5], textHeight: 0.5}) // a see-through green crosshair, and larger text labels
54
65
  */
@@ -67,6 +78,7 @@ export const Niivue = function (options = {}) {
67
78
  clipPlaneHotKey: "KeyC", // keyboard short cut to activate the clip plane
68
79
  viewModeHotKey: "KeyV", // keyboard shortcut to switch view modes
69
80
  keyDebounceTime: 50, // default debounce time used in keyup listeners
81
+ isRadiologicalConvention: false,
70
82
  };
71
83
 
72
84
  this.canvas = null; // the canvas element on the page
@@ -117,7 +129,6 @@ export const Niivue = function (options = {}) {
117
129
  this.volumes = []; // all loaded images. Can add in the ability to push or slice as needed
118
130
  this.backTexture = [];
119
131
  this.objectsToRender3D = [];
120
- this.isRadiologicalConvention = false;
121
132
  this.volScaleMultiplier = 1;
122
133
  this.volScale = [];
123
134
  this.vox = [];
@@ -205,7 +216,6 @@ Niivue.prototype.attachTo = async function (id) {
205
216
  Niivue.prototype.on = function (event, callback) {
206
217
  let knownEvents = Object.keys(this.eventsToSubjects);
207
218
  if (knownEvents.indexOf(event) == -1) {
208
- console.log(`there is no known event ${event}`);
209
219
  return;
210
220
  }
211
221
  let subject = this.eventsToSubjects[event];
@@ -225,7 +235,6 @@ Niivue.prototype.on = function (event, callback) {
225
235
  Niivue.prototype.off = function (event) {
226
236
  let knownEvents = Object.keys(this.eventsToSubjects);
227
237
  if (knownEvents.indexOf(event) == -1) {
228
- console.log(`there is no known event ${event}`);
229
238
  return;
230
239
  }
231
240
  let nsubs = this.subscriptions.length;
@@ -779,6 +788,16 @@ Niivue.prototype.dropListener = async function (e) {
779
788
  }
780
789
  };
781
790
 
791
+ Niivue.prototype.setRadiologicalConvention = function (
792
+ isRadiologicalConvention
793
+ ) {
794
+ this.opts.isRadiologicalConvention = isRadiologicalConvention;
795
+ };
796
+
797
+ Niivue.prototype.getRadiologicalConvention = function () {
798
+ return this.opts.isRadiologicalConvention;
799
+ };
800
+
782
801
  /**
783
802
  * add a new volume to the canvas
784
803
  * @param {NVImage} volume the new volume to add to the canvas
@@ -1343,6 +1362,73 @@ Niivue.prototype.initText = async function () {
1343
1362
  this.drawLoadingText(this.loadingText);
1344
1363
  }; // initText()
1345
1364
 
1365
+ Niivue.prototype.processImage = function (imageIndex, cmd, isNewLayer = true) {
1366
+ let image = this.volumes[imageIndex].clone();
1367
+ let metadata = image.getImageMetadata();
1368
+ this.worker.postMessage([metadata, image.img.buffer, cmd, isNewLayer]);
1369
+ };
1370
+
1371
+ Niivue.prototype.initWasm = async function () {
1372
+ this.worker = new NiiMathWorker();
1373
+ this.worker.onmessage = (e) => {
1374
+ // find our processed image
1375
+ const id = e.data.id;
1376
+ let processedImage = this.volumes.find((image) => image.id == id);
1377
+ if (!processedImage) {
1378
+ console.log("image not found");
1379
+ return;
1380
+ }
1381
+
1382
+ const isNewLayer = e.data.isNewLayer;
1383
+ if (isNewLayer) {
1384
+ processedImage = processedImage.clone();
1385
+ processedImage.id = uuidv4();
1386
+ }
1387
+
1388
+ let imageBytes = e.data.imageBytes;
1389
+
1390
+ switch (processedImage.hdr.datatypeCode) {
1391
+ case processedImage.DT_UNSIGNED_CHAR:
1392
+ processedImage.img = new Uint8Array(imageBytes);
1393
+ break;
1394
+ case processedImage.DT_SIGNED_SHORT:
1395
+ processedImage.img = new Int16Array(imageBytes);
1396
+ break;
1397
+ case processedImage.DT_FLOAT:
1398
+ processedImage.img = new Float32Array(imageBytes);
1399
+ break;
1400
+ case processedImage.DT_DOUBLE:
1401
+ throw "datatype " + processedImage.hdr.datatypeCode + " not supported";
1402
+ case processedImage.DT_RGB:
1403
+ processedImage.img = new Uint8Array(imageBytes);
1404
+ break;
1405
+ case processedImage.DT_UINT16:
1406
+ processedImage.img = new Uint16Array(imageBytes);
1407
+ break;
1408
+ case processedImage.DT_RGBA32:
1409
+ processedImage.img = new Uint8Array(imageBytes);
1410
+ break;
1411
+ default:
1412
+ throw "datatype " + processedImage.hdr.datatypeCode + " not supported";
1413
+ }
1414
+
1415
+ // recalculate
1416
+ processedImage.trustCalMinMax = false;
1417
+ processedImage.calMinMax();
1418
+
1419
+ let imageIndex = this.volumes.length;
1420
+ if (isNewLayer) {
1421
+ this.setVolume(processedImage, this.volumes.length);
1422
+ } else {
1423
+ imageIndex = this.volumes.indexOf(processedImage);
1424
+ }
1425
+ this.refreshLayers(processedImage, imageIndex, this.volumes.length);
1426
+ this.drawScene();
1427
+
1428
+ this.updateGLVolume();
1429
+ };
1430
+ };
1431
+
1346
1432
  // not included in public docs
1347
1433
  Niivue.prototype.init = async function () {
1348
1434
  //initial setup: only at the startup of the component
@@ -1353,6 +1439,8 @@ Niivue.prototype.init = async function () {
1353
1439
  // console.log("gpu vendor: ", vendor);
1354
1440
  // console.log("gpu renderer: ", renderer);
1355
1441
  // await this.loadFont()
1442
+ await this.initWasm();
1443
+
1356
1444
  this.gl.enable(this.gl.CULL_FACE);
1357
1445
  this.gl.cullFace(this.gl.FRONT);
1358
1446
  this.gl.enable(this.gl.BLEND);
@@ -1941,6 +2029,20 @@ Niivue.prototype.colorMaps = function (sort = true) {
1941
2029
  return sort === true ? cm.sort() : cm;
1942
2030
  };
1943
2031
 
2032
+ /**
2033
+ * update the colormap of an image given its ID
2034
+ * @param {string} id the ID of the NVImage
2035
+ * @param {string} colorMap the name of the colorMap to use
2036
+ * @example
2037
+ * niivue = new Niivue()
2038
+ * niivue.setColorMap(someImage.id, 'red')
2039
+ */
2040
+ Niivue.prototype.setColorMap = function (id, colorMap) {
2041
+ let idx = this.getVolumeIndexByID(id);
2042
+ this.volumes[idx].colorMap = colorMap;
2043
+ this.updateGLVolume();
2044
+ };
2045
+
1944
2046
  // not included in public docs
1945
2047
  Niivue.prototype.colormapFromKey = function (name) {
1946
2048
  let availMaps = this.colorMaps();
@@ -2342,7 +2444,7 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
2342
2444
  this.scene.crosshairPos[0],
2343
2445
  ]; //sagittal: width=j, height=k, slice=i
2344
2446
  let isMirrorLR =
2345
- this.isRadiologicalConvention && axCorSag < this.sliceTypeSagittal;
2447
+ this.opts.isRadiologicalConvention && axCorSag < this.sliceTypeSagittal;
2346
2448
  this.sliceShader.use(this.gl);
2347
2449
  this.gl.uniform1f(this.sliceShader.uniforms["opacity"], this.backOpacity);
2348
2450
  this.gl.uniform1i(this.sliceShader.uniforms["axCorSag"], axCorSag);
package/src/nvimage.js CHANGED
@@ -535,6 +535,7 @@ NVImage.loadFromFile = async function (
535
535
  */
536
536
  NVImage.prototype.clone = function () {
537
537
  let clonedImage = new NVImage();
538
+ clonedImage.id = this.id;
538
539
  clonedImage.hdr = Object.assign({}, this.hdr);
539
540
  clonedImage.img = this.img.slice();
540
541
  clonedImage.calculateRAS();
@@ -552,6 +553,55 @@ NVImage.prototype.zeroImage = function () {
552
553
  this.img.fill(0);
553
554
  };
554
555
 
556
+ /**
557
+ * Image M.
558
+ * @typedef {Object} NVImage~MetaData
559
+ * @property {uuidv4} id - unique if of image
560
+ * @property {number} datatypeCode - data type
561
+ * @property {number} nx - number of columns
562
+ * @property {number} ny - number of rows
563
+ * @property {number} nz - number of slices
564
+ * @property {number} nt - number of volumes
565
+ * @property {number} dx - space between columns
566
+ * @property {number} dy - space between rows
567
+ * @property {number} dz - space between slices
568
+ * @property {number} dt - time between volumes
569
+ * @property {number} bpx - bits per voxel
570
+ */
571
+
572
+ /**
573
+ * get nifti specific metadata about the image
574
+ * @returns {NVImage~Metadata} - {@link NVImage~Metadata}
575
+ */
576
+ NVImage.prototype.getImageMetadata = function () {
577
+ const id = this.id;
578
+ const datatypeCode = this.hdr.datatypeCode;
579
+ const dims = this.hdr.dims;
580
+ const nx = dims[1];
581
+ const ny = dims[2];
582
+ const nz = dims[3];
583
+ const nt = Math.max(1, dims[4]);
584
+ const pixDims = this.hdr.pixDims;
585
+ const dx = pixDims[1];
586
+ const dy = pixDims[2];
587
+ const dz = pixDims[3];
588
+ const dt = pixDims[4];
589
+ const bpv = Math.floor(this.hdr.numBitsPerVoxel / 8);
590
+
591
+ return {
592
+ id,
593
+ datatypeCode,
594
+ nx,
595
+ ny,
596
+ nz,
597
+ nt,
598
+ dx,
599
+ dy,
600
+ dz,
601
+ dt,
602
+ bpv,
603
+ };
604
+ };
555
605
  /**
556
606
  * a factory function to make a zero filled image given a NVImage as a reference
557
607
  * @param {NVImage} nvImage an existing NVImage as a reference
@@ -565,3 +615,12 @@ NVImage.zerosLike = function (nvImage) {
565
615
  zeroClone.zeroImage();
566
616
  return zeroClone;
567
617
  };
618
+
619
+ String.prototype.getBytes = function () {
620
+ let bytes = [];
621
+ for (var i = 0; i < this.length; i++) {
622
+ bytes.push(this.charCodeAt(i));
623
+ }
624
+
625
+ return bytes;
626
+ };
Binary file