@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/README.md +15 -17
- package/dist/niivue.es.js +2496 -540
- package/dist/niivue.umd.js +295 -125
- package/package.json +1 -1
- package/src/colortables.js +2 -2
- package/src/index.html +3 -2
- package/src/indexDraw.html +11 -4
- package/src/indexMM.html +229 -0
- package/src/indexModulate.html +223 -0
- package/src/niivue.js +2110 -616
- package/src/nvimage.js +132 -30
- package/src/nvmesh.js +33 -14
- package/src/shader-srcs.js +248 -67
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
|
-
|
|
901
|
-
|
|
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
|
-
|
|
2590
|
-
//
|
|
2591
|
-
let
|
|
2592
|
-
let
|
|
2593
|
-
let
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
let
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
);
|
|
2602
|
-
let
|
|
2603
|
-
|
|
2604
|
-
|
|
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);
|
|
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
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
1980
|
+
let t0 = parseInt(tn[0]) - 1; //first vertex
|
|
1968
1981
|
tn = items[2].split("/");
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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);
|