@niivue/niivue 0.24.2 → 0.26.3

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/src/nvimage.js CHANGED
@@ -52,7 +52,8 @@ export function NVImage(
52
52
  ignoreZeroVoxels = false,
53
53
  visible = true,
54
54
  isDICOMDIR = false,
55
- useQFormNotSForm = false
55
+ useQFormNotSForm = false,
56
+ colorMapNegative = ""
56
57
  ) {
57
58
  // https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h
58
59
  this.DT_NONE = 0;
@@ -84,7 +85,9 @@ export function NVImage(
84
85
  this.percentileFrac = percentileFrac;
85
86
  this.ignoreZeroVoxels = ignoreZeroVoxels;
86
87
  this.trustCalMinMax = trustCalMinMax;
88
+ this.colorMapNegative = colorMapNegative;
87
89
  this.visible = visible;
90
+ this.modulationImage = null;
88
91
  this.series = []; // for concatenating dicom images
89
92
 
90
93
  // Added to support zerosLike
@@ -143,8 +146,10 @@ export function NVImage(
143
146
  this.hdr,
144
147
  imgRaw.byteLength
145
148
  );
149
+ //1007 = NIFTI_INTENT_VECTOR; 2003 = NIFTI_INTENT_RGB_VECTOR
150
+ // n.b. NIfTI standard says "NIFTI_INTENT_RGB_VECTOR" should be RGBA, but FSL only stores RGB
146
151
  if (
147
- this.hdr.intent_code === 1007 &&
152
+ (this.hdr.intent_code === 1007 || this.hdr.intent_code === 2003) &&
148
153
  this.nFrame4D === 3 &&
149
154
  this.hdr.datatypeCode === this.DT_FLOAT
150
155
  ) {
@@ -379,7 +384,36 @@ export function NVImage(
379
384
  this.calMinMax();
380
385
  }
381
386
 
387
+ NVImage.prototype.computeObliqueAngle = function (mtx44) {
388
+ //https://github.com/afni/afni/blob/25e77d564f2c67ff480fa99a7b8e48ec2d9a89fc/src/thd_coords.c#L717
389
+ let mtx = mat4.clone(mtx44);
390
+ mat4.transpose(mtx, mtx44);
391
+ let dxtmp = Math.sqrt(mtx[0] * mtx[0] + mtx[1] * mtx[1] + mtx[2] * mtx[2]);
392
+ let xmax =
393
+ Math.max(Math.max(Math.abs(mtx[0]), Math.abs(mtx[1])), Math.abs(mtx[2])) /
394
+ dxtmp;
395
+ let dytmp = Math.sqrt(mtx[4] * mtx[4] + mtx[5] * mtx[5] + mtx[6] * mtx[6]);
396
+ let ymax =
397
+ Math.max(Math.max(Math.abs(mtx[4]), Math.abs(mtx[5])), Math.abs(mtx[6])) /
398
+ dytmp;
399
+ let dztmp = Math.sqrt(mtx[8] * mtx[8] + mtx[9] * mtx[9] + mtx[10] * mtx[10]);
400
+ let zmax =
401
+ Math.max(Math.max(Math.abs(mtx[8]), Math.abs(mtx[9])), Math.abs(mtx[10])) /
402
+ dztmp;
403
+ let fig_merit = Math.min(Math.min(xmax, ymax), zmax);
404
+ let oblique_angle = Math.abs((Math.acos(fig_merit) * 180.0) / 3.141592653);
405
+ if (oblique_angle > 0.01)
406
+ console.log(
407
+ "Warning voxels not aligned with world space: " +
408
+ oblique_angle +
409
+ " degrees from plumb.\n"
410
+ );
411
+ else oblique_angle = 0.0;
412
+ return oblique_angle;
413
+ };
414
+
382
415
  NVImage.prototype.calculateOblique = function () {
416
+ this.oblique_angle = this.computeObliqueAngle(this.matRAS);
383
417
  let LPI = this.vox2mm([0.0, 0.0, 0.0], this.matRAS);
384
418
  let X1mm = this.vox2mm([1.0 / this.pixDimsRAS[1], 0.0, 0.0], this.matRAS);
385
419
  let Y1mm = this.vox2mm([0.0, 1.0 / this.pixDimsRAS[2], 0.0], this.matRAS);
@@ -412,6 +446,28 @@ NVImage.prototype.calculateOblique = function () {
412
446
  let maxShear = Math.max(Math.max(XY, XZ), YZ);
413
447
  if (maxShear > 0.1)
414
448
  log.debug("Warning: shear detected (gantry tilt) of %f degrees", maxShear);
449
+ //compute a matrix to transform vectors from factional space to mm:
450
+ let dim = vec4.fromValues(
451
+ this.dimsRAS[1],
452
+ this.dimsRAS[2],
453
+ this.dimsRAS[3],
454
+ 1
455
+ );
456
+ let sform = mat4.clone(this.matRAS);
457
+ mat4.transpose(sform, sform);
458
+ let shim = vec4.fromValues(-0.5, -0.5, -0.5, 0); //bitmap with 5 voxels scaled 0..1, voxel centers are 0.1,0.3,0.5,0.7,0.9
459
+ mat4.translate(sform, sform, shim);
460
+ //mat.mat4.scale(sform, sform, dim);
461
+ sform[0] *= dim[0];
462
+ sform[1] *= dim[0];
463
+ sform[2] *= dim[0];
464
+ sform[4] *= dim[1];
465
+ sform[5] *= dim[1];
466
+ sform[6] *= dim[1];
467
+ sform[8] *= dim[2];
468
+ sform[9] *= dim[2];
469
+ sform[10] *= dim[2];
470
+ this.frac2mm = mat4.clone(sform);
415
471
  };
416
472
 
417
473
  NVImage.prototype.THD_daxes_to_NIFTI = function (
@@ -894,11 +950,12 @@ NVImage.prototype.readMGH = function (buffer) {
894
950
  let hdr = this.hdr;
895
951
  hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
896
952
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
897
- var reader = new DataView(buffer);
898
953
  var raw = buffer;
954
+ var reader = new DataView(raw);
899
955
  if (reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {
900
- var raw = fflate.decompressSync(new Uint8Array(buffer));
901
- reader = new DataView(raw.buffer);
956
+ let raw8 = fflate.decompressSync(new Uint8Array(buffer));
957
+ raw = raw8.buffer;
958
+ reader = new DataView(raw);
902
959
  }
903
960
  let version = reader.getInt32(0, false);
904
961
  let width = reader.getInt32(4, false);
@@ -2203,6 +2260,7 @@ NVImage.loadFromUrl = async function ({
2203
2260
  percentileFrac = 0.02,
2204
2261
  ignoreZeroVoxels = false,
2205
2262
  visible = true,
2263
+ colorMapNegative = "",
2206
2264
  } = {}) {
2207
2265
  if (url === "") {
2208
2266
  throw Error("url must not be empty");
@@ -2248,7 +2306,10 @@ NVImage.loadFromUrl = async function ({
2248
2306
  trustCalMinMax,
2249
2307
  percentileFrac,
2250
2308
  ignoreZeroVoxels,
2251
- visible
2309
+ visible,
2310
+ false,
2311
+ false,
2312
+ colorMapNegative
2252
2313
  );
2253
2314
  } else {
2254
2315
  alert("Unable to load buffer properly from volume");
@@ -2586,27 +2647,23 @@ function getExtents(positions, forceOriginInVolume = true) {
2586
2647
  */
2587
2648
  NVImage.prototype.toNiivueObject3D = function (id, gl) {
2588
2649
  //cube has 8 vertices: left/right, posterior/anterior, inferior/superior
2589
- let LPI = this.vox2mm([0.0, 0.0, 0.0], this.matRAS);
2590
- //TODO: ray direction needs to be corrected for oblique rotations
2591
- let LAI = this.vox2mm([0.0, this.dimsRAS[2] - 1, 0.0], this.matRAS);
2592
- let LPS = this.vox2mm([0.0, 0.0, this.dimsRAS[3] - 1], this.matRAS);
2593
- let LAS = this.vox2mm(
2594
- [0.0, this.dimsRAS[2] - 1, this.dimsRAS[3] - 1],
2595
- this.matRAS
2596
- );
2597
- let RPI = this.vox2mm([this.dimsRAS[1] - 1, 0.0, 0.0], this.matRAS);
2598
- let RAI = this.vox2mm(
2599
- [this.dimsRAS[1] - 1, this.dimsRAS[2] - 1, 0.0],
2600
- this.matRAS
2601
- );
2602
- let RPS = this.vox2mm(
2603
- [this.dimsRAS[1] - 1, 0.0, this.dimsRAS[3] - 1],
2604
- this.matRAS
2605
- );
2606
- let RAS = this.vox2mm(
2607
- [this.dimsRAS[1] - 1, this.dimsRAS[2] - 1, this.dimsRAS[3] - 1],
2608
- this.matRAS
2609
- );
2650
+ //n.b. voxel coordinates are from VOXEL centers
2651
+ // add/subtract 0.5 to get full image field of view
2652
+ let L = -0.5;
2653
+ let P = -0.5;
2654
+ let I = -0.5;
2655
+ let R = this.dimsRAS[1] - 1 + 0.5;
2656
+ let A = this.dimsRAS[2] - 1 + 0.5;
2657
+ let S = this.dimsRAS[3] - 1 + 0.5;
2658
+
2659
+ let LPI = this.vox2mm([L, P, I], this.matRAS);
2660
+ let LAI = this.vox2mm([L, A, I], this.matRAS);
2661
+ let LPS = this.vox2mm([L, P, S], this.matRAS);
2662
+ let LAS = this.vox2mm([L, A, S], this.matRAS);
2663
+ let RPI = this.vox2mm([R, P, I], this.matRAS);
2664
+ let RAI = this.vox2mm([R, A, I], this.matRAS);
2665
+ let RPS = this.vox2mm([R, P, S], this.matRAS);
2666
+ let RAS = this.vox2mm([R, A, S], this.matRAS);
2610
2667
 
2611
2668
  let posTex = [
2612
2669
  //spatial position (XYZ), texture coordinates UVW
@@ -2709,7 +2766,6 @@ NVImage.prototype.toNiivueObject3D = function (id, gl) {
2709
2766
  indexBuffer,
2710
2767
  vao
2711
2768
  );
2712
-
2713
2769
  const extents = getExtents([
2714
2770
  ...LPS,
2715
2771
  ...RPS,
@@ -2720,10 +2776,56 @@ NVImage.prototype.toNiivueObject3D = function (id, gl) {
2720
2776
  ...RAI,
2721
2777
  ...RPI,
2722
2778
  ]);
2723
- obj3D.extentsMin = extents.min;
2724
- obj3D.extentsMax = extents.max;
2779
+ obj3D.extentsMin = extents.min.slice();
2780
+ obj3D.extentsMax = extents.max.slice();
2781
+
2725
2782
  obj3D.furthestVertexFromOrigin = extents.furthestVertexFromOrigin;
2726
2783
  obj3D.originNegate = vec3.clone(extents.origin);
2727
2784
  vec3.negate(obj3D.originNegate, obj3D.originNegate);
2785
+ /*
2786
+ let matDeOblique = mat4.fromValues(
2787
+ this.pixDimsRAS[1],
2788
+ 0,
2789
+ 0,
2790
+ this.matRAS[3],
2791
+ 0,
2792
+ this.pixDimsRAS[2],
2793
+ 0,
2794
+ this.matRAS[7],
2795
+ 0,
2796
+ 0,
2797
+ this.pixDimsRAS[3],
2798
+ this.matRAS[11],
2799
+ 0,
2800
+ 0,
2801
+ 0,
2802
+ 1
2803
+ );
2804
+ LPI = this.vox2mm([L,P,I], matDeOblique);
2805
+ LAI = this.vox2mm([L,A,I], matDeOblique);
2806
+ LPS = this.vox2mm([L,P,S], matDeOblique);
2807
+ LAS = this.vox2mm([L,A,S], matDeOblique);
2808
+ RPI = this.vox2mm([R,P,I], matDeOblique);
2809
+ RAI = this.vox2mm([R,A,I], matDeOblique);
2810
+ RPS = this.vox2mm([R,P,S], matDeOblique);
2811
+ RAS = this.vox2mm([R,A,S], matDeOblique);
2812
+ const extentsDeOblique = getExtents([
2813
+ ...LPS,
2814
+ ...RPS,
2815
+ ...RAS,
2816
+ ...LAS,
2817
+ ...LPI,
2818
+ ...LAI,
2819
+ ...RAI,
2820
+ ...RPI,
2821
+ ]);
2822
+ obj3D.extentsDeObliqueMin = extentsDeOblique.min.slice();
2823
+ obj3D.extentsDeObliqueMax = extentsDeOblique.max.slice();
2824
+ */
2825
+ obj3D.fieldOfViewDeObliqueMM = [
2826
+ this.dimsRAS[1] * this.pixDimsRAS[1],
2827
+ this.dimsRAS[2] * this.pixDimsRAS[2],
2828
+ this.dimsRAS[3] * this.pixDimsRAS[3],
2829
+ ];
2728
2830
  return obj3D;
2729
2831
  };
package/src/nvmesh.js CHANGED
@@ -1394,7 +1394,8 @@ NVMesh.readVTK = function (buffer) {
1394
1394
  if (!line.startsWith("# vtk DataFile")) alert("Invalid VTK mesh");
1395
1395
  line = readStr(); //2nd line comment
1396
1396
  line = readStr(); //3rd line ASCII/BINARY
1397
- if (line.startsWith("ASCII")) return readTxtVTK(buffer); //from NiiVue
1397
+ if (line.startsWith("ASCII")) return readTxtVTK(buffer);
1398
+ //from NiiVue
1398
1399
  else if (!line.startsWith("BINARY"))
1399
1400
  alert("Invalid VTK image, expected ASCII or BINARY", line);
1400
1401
  line = readStr(); //5th line "DATASET POLYDATA"
@@ -1754,18 +1755,28 @@ NVMesh.readPLY = function (buffer) {
1754
1755
  }
1755
1756
  let indices = new Int32Array(nface * 3);
1756
1757
  let f = 0;
1757
- let isTriangular = true;
1758
1758
  for (var i = 0; i < nface; i++) {
1759
1759
  line = readStr();
1760
1760
  let items = line.split(/\s/);
1761
- if (parseInt(items[0]) > 3) isTriangular = false;
1762
- indices[f] = parseInt(items[1]);
1763
- indices[f + 1] = parseInt(items[2]);
1764
- indices[f + 2] = parseInt(items[3]);
1765
- f += 3;
1766
- }
1767
- if (!isTriangular)
1768
- console.log("Only able to read PLY meshes limited to triangles.");
1761
+ let nTri = parseInt(items[0]) - 2;
1762
+ if (nTri < 1) break; //error
1763
+ if (f + nTri * 3 > indices.length) {
1764
+ var c = new Int32Array(indices.length + indices.length);
1765
+ c.set(indices);
1766
+ indices = c.slice();
1767
+ }
1768
+ let idx0 = parseInt(items[1]);
1769
+ let idx1 = parseInt(items[2]);
1770
+ for (let j = 0; j < nTri; j++) {
1771
+ let idx2 = parseInt(items[3 + j]);
1772
+ indices[f + 0] = idx0;
1773
+ indices[f + 1] = idx1;
1774
+ indices[f + 2] = idx2;
1775
+ idx1 = idx2;
1776
+ f += 3;
1777
+ }
1778
+ }
1779
+ if (indices.length !== f) indices = indices.slice(0, f);
1769
1780
  return {
1770
1781
  positions,
1771
1782
  indices,
@@ -1963,12 +1974,20 @@ NVMesh.readOBJ = function (buffer) {
1963
1974
  }
1964
1975
  if (str[0] === "f") {
1965
1976
  let items = str.split(" ");
1977
+ let new_t = items.length - 3; //number of new triangles created
1978
+ if (new_t < 1) break; //error
1966
1979
  let tn = items[1].split("/");
1967
- t.push(parseInt(tn - 1));
1980
+ let t0 = parseInt(tn[0]) - 1; //first vertex
1968
1981
  tn = items[2].split("/");
1969
- t.push(parseInt(tn - 1));
1970
- tn = items[3].split("/");
1971
- t.push(parseInt(tn - 1));
1982
+ let tprev = parseInt(tn[0]) - 1; //previous vertex
1983
+ for (let j = 0; j < new_t; j++) {
1984
+ tn = items[3 + j].split("/");
1985
+ let tcurr = parseInt(tn[0]) - 1; //current vertex
1986
+ t.push(t0);
1987
+ t.push(tprev);
1988
+ t.push(tcurr);
1989
+ tprev = tcurr;
1990
+ }
1972
1991
  }
1973
1992
  } //for all lines
1974
1993
  var positions = new Float32Array(pts);