@niivue/niivue 0.22.2 → 0.24.0
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 +62 -23
- package/dist/niivue.es.js +108187 -10401
- package/dist/niivue.umd.js +506 -94
- package/package.json +11 -2
- package/src/cmaps/index.js +1 -0
- package/src/cmaps/turbo.json +9 -0
- package/src/colortables.js +79 -0
- package/src/index.html +45 -27
- package/src/indexDraw.html +191 -0
- package/src/logger.js +5 -0
- package/src/meshes.html +161 -0
- package/src/niivue-object3D.js +243 -29
- package/src/niivue-shader3D.js +6 -0
- package/src/niivue.js +2577 -648
- package/src/nvimage.js +1821 -153
- package/src/nvmesh.js +3351 -0
- package/src/shader-srcs.js +565 -11
- package/src/shader.js +11 -2
- package/src/AIL.nii.gz +0 -0
- package/src/hippo.nii.gz +0 -0
- package/src/mni152.nii.gz +0 -0
package/src/nvimage.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import nifti from "nifti-reader-js";
|
|
2
|
+
import daikon from "daikon";
|
|
2
3
|
import { v4 as uuidv4 } from "uuid";
|
|
3
|
-
import
|
|
4
|
+
import { mat3, mat4, vec3, vec4 } from "gl-matrix";
|
|
4
5
|
import * as cmaps from "./cmaps";
|
|
6
|
+
import * as fflate from "fflate";
|
|
5
7
|
import { NiivueObject3D } from "./niivue-object3D";
|
|
6
8
|
import { Log } from "./logger";
|
|
7
9
|
const log = new Log();
|
|
8
10
|
|
|
11
|
+
function isPlatformLittleEndian() {
|
|
12
|
+
//inspired by https://github.com/rii-mango/Papaya
|
|
13
|
+
var buffer = new ArrayBuffer(2);
|
|
14
|
+
new DataView(buffer).setInt16(0, 256, true);
|
|
15
|
+
return new Int16Array(buffer)[0] === 256;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/**
|
|
10
19
|
* query all available color maps that can be applied to volumes
|
|
11
20
|
* @param {boolean} [sort=true] whether or not to sort the returned array
|
|
@@ -17,6 +26,7 @@ const log = new Log();
|
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
28
|
* @class NVImage
|
|
29
|
+
* @type NVImage
|
|
20
30
|
* @description
|
|
21
31
|
* a NVImage encapsulates some images data and provides methods to query and operate on images
|
|
22
32
|
* @constructor
|
|
@@ -29,15 +39,19 @@ const log = new Log();
|
|
|
29
39
|
* @param {boolean} [ignoreZeroVoxels=false] whether or not to ignore zero voxels in setting the robust range of display values
|
|
30
40
|
* @param {boolean} [visible=true] whether or not this image is to be visible
|
|
31
41
|
*/
|
|
32
|
-
export
|
|
33
|
-
dataBuffer,
|
|
42
|
+
export function NVImage(
|
|
43
|
+
dataBuffer, // can be an array of Typed arrays or just a typed array. If an array of Typed arrays then it is assumed you are loading DICOM (perhaps the only real use case?)
|
|
34
44
|
name = "",
|
|
35
45
|
colorMap = "gray",
|
|
36
46
|
opacity = 1.0,
|
|
47
|
+
pairedImgData = null,
|
|
48
|
+
cal_min = NaN,
|
|
49
|
+
cal_max = NaN,
|
|
37
50
|
trustCalMinMax = true,
|
|
38
51
|
percentileFrac = 0.02,
|
|
39
52
|
ignoreZeroVoxels = false,
|
|
40
53
|
visible = true,
|
|
54
|
+
isDICOMDIR = false,
|
|
41
55
|
useQFormNotSForm = false
|
|
42
56
|
) {
|
|
43
57
|
// https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h
|
|
@@ -65,18 +79,103 @@ export var NVImage = function (
|
|
|
65
79
|
this.name = name;
|
|
66
80
|
this.id = uuidv4();
|
|
67
81
|
this.colorMap = colorMap;
|
|
82
|
+
this.frame4D = 0; //indexed from 0!
|
|
68
83
|
this.opacity = opacity > 1.0 ? 1.0 : opacity; //make sure opacity can't be initialized greater than 1 see: #107 and #117 on github
|
|
69
84
|
this.percentileFrac = percentileFrac;
|
|
70
85
|
this.ignoreZeroVoxels = ignoreZeroVoxels;
|
|
71
86
|
this.trustCalMinMax = trustCalMinMax;
|
|
72
87
|
this.visible = visible;
|
|
88
|
+
this.series = []; // for concatenating dicom images
|
|
73
89
|
|
|
74
90
|
// Added to support zerosLike
|
|
75
91
|
if (!dataBuffer) {
|
|
76
92
|
return;
|
|
77
93
|
}
|
|
94
|
+
var re = /(?:\.([^.]+))?$/;
|
|
95
|
+
let ext = re.exec(name)[1] || "";
|
|
96
|
+
ext = ext.toUpperCase();
|
|
97
|
+
if (ext === "GZ") {
|
|
98
|
+
ext = re.exec(name.slice(0, -3))[1]; //img.trk.gz -> img.trk
|
|
99
|
+
ext = ext.toUpperCase();
|
|
100
|
+
}
|
|
101
|
+
let imgRaw = null;
|
|
102
|
+
this.hdr = null;
|
|
103
|
+
if (ext === "" && isDICOMDIR && Array.isArray(dataBuffer)) {
|
|
104
|
+
imgRaw = this.readDICOM(dataBuffer);
|
|
105
|
+
} else if (ext === "MIH" || ext === "MIF") {
|
|
106
|
+
imgRaw = this.readMIF(dataBuffer, pairedImgData); //detached
|
|
107
|
+
} else if (ext === "NHDR" || ext === "NRRD") {
|
|
108
|
+
imgRaw = this.readNRRD(dataBuffer, pairedImgData); //detached
|
|
109
|
+
} else if (ext === "MHD" || ext === "MHA") {
|
|
110
|
+
imgRaw = this.readMHA(dataBuffer); //to do: pairedImgData
|
|
111
|
+
} else if (ext === "MGH" || ext === "MGZ") {
|
|
112
|
+
imgRaw = this.readMGH(dataBuffer);
|
|
113
|
+
} else if (ext === "V") {
|
|
114
|
+
imgRaw = this.readECAT(dataBuffer);
|
|
115
|
+
} else if (ext === "V16") {
|
|
116
|
+
imgRaw = this.readV16(dataBuffer);
|
|
117
|
+
} else if (ext === "VMR") {
|
|
118
|
+
imgRaw = this.readVMR(dataBuffer);
|
|
119
|
+
} else if (ext === "HEAD") {
|
|
120
|
+
imgRaw = this.readHEAD(dataBuffer, pairedImgData); //paired = .BRIK
|
|
121
|
+
} else if (ext === "NII") {
|
|
122
|
+
this.hdr = nifti.readHeader(dataBuffer);
|
|
123
|
+
if (this.hdr.cal_min === 0 && this.hdr.cal_max === 255)
|
|
124
|
+
this.hdr.cal_max = 0.0;
|
|
125
|
+
if (nifti.isCompressed(dataBuffer)) {
|
|
126
|
+
imgRaw = nifti.readImage(this.hdr, nifti.decompress(dataBuffer));
|
|
127
|
+
} else {
|
|
128
|
+
imgRaw = nifti.readImage(this.hdr, dataBuffer);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
//DICOMs do not always end .dcm, so DICOM is our format of last resort
|
|
132
|
+
imgRaw = this.readDICOM(dataBuffer);
|
|
133
|
+
// if loading a DICOM directory
|
|
134
|
+
}
|
|
135
|
+
this.nFrame4D = 1;
|
|
136
|
+
for (let i = 4; i < 7; i++)
|
|
137
|
+
if (this.hdr.dims[i] > 1) this.nFrame4D *= this.hdr.dims[i];
|
|
138
|
+
this.nVox3D = this.hdr.dims[1] * this.hdr.dims[2] * this.hdr.dims[3];
|
|
139
|
+
let nVol4D = imgRaw.byteLength / this.nVox3D / (this.hdr.numBitsPerVoxel / 8);
|
|
140
|
+
if (nVol4D !== this.nFrame4D)
|
|
141
|
+
console.log(
|
|
142
|
+
"This header does not match voxel data",
|
|
143
|
+
this.hdr,
|
|
144
|
+
imgRaw.byteLength
|
|
145
|
+
);
|
|
146
|
+
if (
|
|
147
|
+
this.hdr.intent_code === 1007 &&
|
|
148
|
+
this.nFrame4D === 3 &&
|
|
149
|
+
this.hdr.datatypeCode === this.DT_FLOAT
|
|
150
|
+
) {
|
|
151
|
+
let tmp = new Float32Array(imgRaw);
|
|
152
|
+
let f32 = tmp.slice();
|
|
153
|
+
this.hdr.datatypeCode = this.DT_RGB;
|
|
154
|
+
this.nFrame4D = 1;
|
|
155
|
+
for (let i = 4; i < 7; i++) this.hdr.dims[i] = 1;
|
|
156
|
+
this.hdr.dims[0] = 3; //3D
|
|
157
|
+
imgRaw = new Uint8Array(this.nVox3D * 3); //*3 for RGB
|
|
158
|
+
let mx = Math.abs(f32[0]);
|
|
159
|
+
for (let i = 0; i < this.nVox3D * 3; i++)
|
|
160
|
+
mx = Math.max(mx, Math.abs(f32[i]));
|
|
161
|
+
let slope = 1.0;
|
|
162
|
+
if (mx > 0) slope = 1.0 / mx;
|
|
78
163
|
|
|
79
|
-
|
|
164
|
+
let nVox3D2 = this.nVox3D * 2;
|
|
165
|
+
let j = 0;
|
|
166
|
+
for (let i = 0; i < this.nVox3D; i++) {
|
|
167
|
+
imgRaw[j] = 255.0 * Math.abs(f32[i] * slope);
|
|
168
|
+
imgRaw[j + 1] = 255.0 * Math.abs(f32[i + this.nVox3D] * slope);
|
|
169
|
+
imgRaw[j + 2] = 255.0 * Math.abs(f32[i + nVox3D2] * slope);
|
|
170
|
+
j += 3;
|
|
171
|
+
}
|
|
172
|
+
} //NIFTI_INTENT_VECTOR: this is a RGB tensor
|
|
173
|
+
if (
|
|
174
|
+
this.hdr.pixDims[1] === 0.0 ||
|
|
175
|
+
this.hdr.pixDims[2] === 0.0 ||
|
|
176
|
+
this.hdr.pixDims[3] === 0.0
|
|
177
|
+
)
|
|
178
|
+
console.log("pixDims not plausible", this.hdr);
|
|
80
179
|
function isAffineOK(mtx) {
|
|
81
180
|
//A good matrix should not have any components that are not a number
|
|
82
181
|
//A good spatial transformation matrix should not have a row or column that is all zeros
|
|
@@ -175,19 +274,53 @@ export var NVImage = function (
|
|
|
175
274
|
];
|
|
176
275
|
this.hdr.affine = affine;
|
|
177
276
|
} //defective affine
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
277
|
+
//swap data if foreign endian:
|
|
278
|
+
if (
|
|
279
|
+
this.hdr.datatypeCode !== this.DT_RGB &&
|
|
280
|
+
this.hdr.datatypeCode !== this.DT_RGBA32 &&
|
|
281
|
+
this.hdr.littleEndian !== isPlatformLittleEndian() &&
|
|
282
|
+
this.hdr.numBitsPerVoxel > 8
|
|
283
|
+
) {
|
|
284
|
+
if (this.hdr.numBitsPerVoxel === 16) {
|
|
285
|
+
//inspired by https://github.com/rii-mango/Papaya
|
|
286
|
+
var u16 = new Uint16Array(imgRaw);
|
|
287
|
+
for (let i = 0; i < u16.length; i++) {
|
|
288
|
+
let val = u16[i];
|
|
289
|
+
u16[i] = ((((val & 0xff) << 8) | ((val >> 8) & 0xff)) << 16) >> 16; // since JS uses 32-bit when bit shifting
|
|
290
|
+
}
|
|
291
|
+
} else if (this.hdr.numBitsPerVoxel === 32) {
|
|
292
|
+
//inspired by https://github.com/rii-mango/Papaya
|
|
293
|
+
var u32 = new Uint32Array(imgRaw);
|
|
294
|
+
for (let i = 0; i < u32.length; i++) {
|
|
295
|
+
let val = u32[i];
|
|
296
|
+
u32[i] =
|
|
297
|
+
((val & 0xff) << 24) |
|
|
298
|
+
((val & 0xff00) << 8) |
|
|
299
|
+
((val >> 8) & 0xff00) |
|
|
300
|
+
((val >> 24) & 0xff);
|
|
301
|
+
}
|
|
302
|
+
} else if (this.hdr.numBitsPerVoxel === 64) {
|
|
303
|
+
//inspired by MIT licensed code: https://github.com/rochars/endianness
|
|
304
|
+
let numBytesPerVoxel = this.hdr.numBitsPerVoxel / 8;
|
|
305
|
+
var u8 = new Uint8Array(imgRaw);
|
|
306
|
+
for (let index = 0; index < u8.length; index += numBytesPerVoxel) {
|
|
307
|
+
let offset = bytesPer - 1;
|
|
308
|
+
for (let x = 0; x < offset; x++) {
|
|
309
|
+
let theByte = u8[index + x];
|
|
310
|
+
u8[index + x] = u8[index + offset];
|
|
311
|
+
u8[index + offset] = theByte;
|
|
312
|
+
offset--;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} //if 64-bits
|
|
316
|
+
} //swap byte order
|
|
185
317
|
switch (this.hdr.datatypeCode) {
|
|
186
318
|
case this.DT_UNSIGNED_CHAR:
|
|
187
319
|
this.img = new Uint8Array(imgRaw);
|
|
188
320
|
break;
|
|
189
321
|
case this.DT_SIGNED_SHORT:
|
|
190
322
|
this.img = new Int16Array(imgRaw);
|
|
323
|
+
|
|
191
324
|
break;
|
|
192
325
|
case this.DT_FLOAT:
|
|
193
326
|
this.img = new Float32Array(imgRaw);
|
|
@@ -204,51 +337,57 @@ export var NVImage = function (
|
|
|
204
337
|
case this.DT_RGBA32:
|
|
205
338
|
this.img = new Uint8Array(imgRaw);
|
|
206
339
|
break;
|
|
207
|
-
case this.DT_INT8:
|
|
340
|
+
case this.DT_INT8: {
|
|
208
341
|
let i8 = new Int8Array(imgRaw);
|
|
209
342
|
var vx8 = i8.length;
|
|
210
343
|
this.img = new Int16Array(vx8);
|
|
211
|
-
for (
|
|
344
|
+
for (let i = 0; i < vx8 - 1; i++) this.img[i] = i8[i];
|
|
212
345
|
this.hdr.datatypeCode = this.DT_SIGNED_SHORT;
|
|
213
346
|
break;
|
|
214
|
-
|
|
347
|
+
}
|
|
348
|
+
case this.DT_UINT32: {
|
|
215
349
|
let u32 = new Uint32Array(imgRaw);
|
|
216
350
|
var vx32 = u32.length;
|
|
217
351
|
this.img = new Float64Array(vx32);
|
|
218
|
-
for (
|
|
352
|
+
for (let i = 0; i < vx32 - 1; i++) this.img[i] = u32[i];
|
|
219
353
|
this.hdr.datatypeCode = this.DT_DOUBLE;
|
|
220
354
|
break;
|
|
221
|
-
|
|
355
|
+
}
|
|
356
|
+
case this.DT_SIGNED_INT: {
|
|
222
357
|
let i32 = new Int32Array(imgRaw);
|
|
223
358
|
var vxi32 = i32.length;
|
|
224
359
|
this.img = new Float64Array(vxi32);
|
|
225
|
-
for (
|
|
360
|
+
for (let i = 0; i < vxi32 - 1; i++) this.img[i] = i32[i];
|
|
226
361
|
this.hdr.datatypeCode = this.DT_DOUBLE;
|
|
227
362
|
break;
|
|
228
|
-
|
|
363
|
+
}
|
|
364
|
+
case this.DT_INT64: {
|
|
365
|
+
// eslint-disable-next-line no-undef
|
|
229
366
|
let i64 = new BigInt64Array(imgRaw);
|
|
230
367
|
let vx = i64.length;
|
|
231
368
|
this.img = new Float64Array(vx);
|
|
232
|
-
for (
|
|
369
|
+
for (let i = 0; i < vx - 1; i++) this.img[i] = Number(i64[i]);
|
|
233
370
|
this.hdr.datatypeCode = this.DT_DOUBLE;
|
|
234
371
|
break;
|
|
372
|
+
}
|
|
235
373
|
default:
|
|
236
374
|
throw "datatype " + this.hdr.datatypeCode + " not supported";
|
|
237
375
|
}
|
|
238
|
-
|
|
239
376
|
this.calculateRAS();
|
|
377
|
+
if (!isNaN(cal_min)) this.hdr.cal_min = cal_min;
|
|
378
|
+
if (!isNaN(cal_max)) this.hdr.cal_max = cal_max;
|
|
240
379
|
this.calMinMax();
|
|
241
|
-
}
|
|
380
|
+
}
|
|
242
381
|
|
|
243
382
|
NVImage.prototype.calculateOblique = function () {
|
|
244
383
|
let LPI = this.vox2mm([0.0, 0.0, 0.0], this.matRAS);
|
|
245
384
|
let X1mm = this.vox2mm([1.0 / this.pixDimsRAS[1], 0.0, 0.0], this.matRAS);
|
|
246
385
|
let Y1mm = this.vox2mm([0.0, 1.0 / this.pixDimsRAS[2], 0.0], this.matRAS);
|
|
247
386
|
let Z1mm = this.vox2mm([0.0, 0.0, 1.0 / this.pixDimsRAS[3]], this.matRAS);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
let oblique =
|
|
387
|
+
vec3.subtract(X1mm, X1mm, LPI);
|
|
388
|
+
vec3.subtract(Y1mm, Y1mm, LPI);
|
|
389
|
+
vec3.subtract(Z1mm, Z1mm, LPI);
|
|
390
|
+
let oblique = mat4.fromValues(
|
|
252
391
|
X1mm[0],
|
|
253
392
|
X1mm[1],
|
|
254
393
|
X1mm[2],
|
|
@@ -266,15 +405,1291 @@ NVImage.prototype.calculateOblique = function () {
|
|
|
266
405
|
0,
|
|
267
406
|
1
|
|
268
407
|
);
|
|
269
|
-
this.obliqueRAS =
|
|
270
|
-
let XY = Math.abs(90 -
|
|
271
|
-
let XZ = Math.abs(90 -
|
|
272
|
-
let YZ = Math.abs(90 -
|
|
408
|
+
this.obliqueRAS = mat4.clone(oblique);
|
|
409
|
+
let XY = Math.abs(90 - vec3.angle(X1mm, Y1mm) * (180 / Math.PI));
|
|
410
|
+
let XZ = Math.abs(90 - vec3.angle(X1mm, Z1mm) * (180 / Math.PI));
|
|
411
|
+
let YZ = Math.abs(90 - vec3.angle(Y1mm, Z1mm) * (180 / Math.PI));
|
|
273
412
|
let maxShear = Math.max(Math.max(XY, XZ), YZ);
|
|
274
413
|
if (maxShear > 0.1)
|
|
275
414
|
log.debug("Warning: shear detected (gantry tilt) of %f degrees", maxShear);
|
|
276
415
|
};
|
|
277
416
|
|
|
417
|
+
NVImage.prototype.THD_daxes_to_NIFTI = function (
|
|
418
|
+
xyzDelta,
|
|
419
|
+
xyzOrigin,
|
|
420
|
+
orientSpecific
|
|
421
|
+
) {
|
|
422
|
+
//https://github.com/afni/afni/blob/d6997e71f2b625ac1199460576d48f3136dac62c/src/thd_niftiwrite.c#L315
|
|
423
|
+
let hdr = this.hdr;
|
|
424
|
+
hdr.sform_code = 2;
|
|
425
|
+
const ORIENT_xyz = "xxyyzzg"; //note strings indexed from 0!
|
|
426
|
+
let nif_x_axnum = -1;
|
|
427
|
+
let nif_y_axnum = -1;
|
|
428
|
+
let nif_z_axnum = -1;
|
|
429
|
+
let axcode = ["x", "y", "z"];
|
|
430
|
+
axcode[0] = ORIENT_xyz[orientSpecific[0]];
|
|
431
|
+
axcode[1] = ORIENT_xyz[orientSpecific[1]];
|
|
432
|
+
axcode[2] = ORIENT_xyz[orientSpecific[2]];
|
|
433
|
+
let axstep = xyzDelta.slice(0, 3);
|
|
434
|
+
let axstart = xyzOrigin.slice(0, 3);
|
|
435
|
+
for (var ii = 0; ii < 3; ii++) {
|
|
436
|
+
if (axcode[ii] === "x") nif_x_axnum = ii;
|
|
437
|
+
else if (axcode[ii] === "y") nif_y_axnum = ii;
|
|
438
|
+
else nif_z_axnum = ii;
|
|
439
|
+
}
|
|
440
|
+
if (nif_x_axnum < 0 || nif_y_axnum < 0 || nif_z_axnum < 0) return; //not assigned
|
|
441
|
+
if (
|
|
442
|
+
nif_x_axnum === nif_y_axnum ||
|
|
443
|
+
nif_x_axnum === nif_z_axnum ||
|
|
444
|
+
nif_y_axnum === nif_z_axnum
|
|
445
|
+
)
|
|
446
|
+
return; //not assigned
|
|
447
|
+
hdr.pixDims[1] = Math.abs(axstep[0]);
|
|
448
|
+
hdr.pixDims[2] = Math.abs(axstep[1]);
|
|
449
|
+
hdr.pixDims[3] = Math.abs(axstep[2]);
|
|
450
|
+
hdr.affine = [
|
|
451
|
+
[1, 0, 0, 0],
|
|
452
|
+
[0, 1, 0, 0],
|
|
453
|
+
[0, 0, 1, 0],
|
|
454
|
+
[0, 0, 0, 1],
|
|
455
|
+
];
|
|
456
|
+
hdr.affine[0][nif_x_axnum] = -axstep[nif_x_axnum];
|
|
457
|
+
hdr.affine[1][nif_y_axnum] = -axstep[nif_y_axnum];
|
|
458
|
+
hdr.affine[2][nif_z_axnum] = axstep[nif_z_axnum];
|
|
459
|
+
hdr.affine[0][3] = -axstart[nif_x_axnum];
|
|
460
|
+
hdr.affine[1][3] = -axstart[nif_y_axnum];
|
|
461
|
+
hdr.affine[2][3] = axstart[nif_z_axnum];
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
NVImage.prototype.SetPixDimFromSForm = function () {
|
|
465
|
+
let m = this.hdr.affine;
|
|
466
|
+
let mat = mat4.fromValues(
|
|
467
|
+
m[0][0],
|
|
468
|
+
m[0][1],
|
|
469
|
+
m[0][2],
|
|
470
|
+
m[0][3],
|
|
471
|
+
m[1][0],
|
|
472
|
+
m[1][1],
|
|
473
|
+
m[1][2],
|
|
474
|
+
m[1][3],
|
|
475
|
+
m[2][0],
|
|
476
|
+
m[2][1],
|
|
477
|
+
m[2][2],
|
|
478
|
+
m[2][3],
|
|
479
|
+
m[3][0],
|
|
480
|
+
m[3][1],
|
|
481
|
+
m[3][2],
|
|
482
|
+
m[3][3]
|
|
483
|
+
);
|
|
484
|
+
let mm000 = this.vox2mm([0, 0, 0], mat);
|
|
485
|
+
let mm100 = this.vox2mm([1, 0, 0], mat);
|
|
486
|
+
vec3.subtract(mm100, mm100, mm000);
|
|
487
|
+
let mm010 = this.vox2mm([0, 1, 0], mat);
|
|
488
|
+
vec3.subtract(mm010, mm010, mm000);
|
|
489
|
+
let mm001 = this.vox2mm([0, 0, 1], mat);
|
|
490
|
+
vec3.subtract(mm001, mm001, mm000);
|
|
491
|
+
this.hdr.pixDims[1] = vec3.length(mm100);
|
|
492
|
+
this.hdr.pixDims[2] = vec3.length(mm010);
|
|
493
|
+
this.hdr.pixDims[3] = vec3.length(mm001);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
function getBestTransform(imageDirections, voxelDimensions, imagePosition) {
|
|
497
|
+
//https://github.com/rii-mango/Papaya/blob/782a19341af77a510d674c777b6da46afb8c65f1/src/js/volume/dicom/header-dicom.js#L605
|
|
498
|
+
/*Copyright (c) 2012-2015, RII-UTHSCSA
|
|
499
|
+
All rights reserved.
|
|
500
|
+
|
|
501
|
+
THIS PRODUCT IS NOT FOR CLINICAL USE.
|
|
502
|
+
|
|
503
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
504
|
+
following conditions are met:
|
|
505
|
+
|
|
506
|
+
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
507
|
+
disclaimer.
|
|
508
|
+
|
|
509
|
+
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
|
510
|
+
disclaimer in the documentation and/or other materials provided with the distribution.
|
|
511
|
+
|
|
512
|
+
- Neither the name of the RII-UTHSCSA nor the names of its contributors may be used to endorse or promote products
|
|
513
|
+
derived from this software without specific prior written permission.
|
|
514
|
+
|
|
515
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
516
|
+
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
517
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
518
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
519
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
520
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
521
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
522
|
+
*/
|
|
523
|
+
var cosines = imageDirections,
|
|
524
|
+
m = null;
|
|
525
|
+
if (cosines) {
|
|
526
|
+
var vs = {
|
|
527
|
+
colSize: voxelDimensions[0],
|
|
528
|
+
rowSize: voxelDimensions[1],
|
|
529
|
+
sliceSize: voxelDimensions[2],
|
|
530
|
+
};
|
|
531
|
+
var coord = imagePosition;
|
|
532
|
+
var cosx = [cosines[0], cosines[1], cosines[2]];
|
|
533
|
+
var cosy = [cosines[3], cosines[4], cosines[5]];
|
|
534
|
+
var cosz = [
|
|
535
|
+
cosx[1] * cosy[2] - cosx[2] * cosy[1],
|
|
536
|
+
cosx[2] * cosy[0] - cosx[0] * cosy[2],
|
|
537
|
+
cosx[0] * cosy[1] - cosx[1] * cosy[0],
|
|
538
|
+
];
|
|
539
|
+
m = [
|
|
540
|
+
[
|
|
541
|
+
cosx[0] * vs.colSize * -1,
|
|
542
|
+
cosy[0] * vs.rowSize * -1,
|
|
543
|
+
cosz[0] * vs.sliceSize * -1,
|
|
544
|
+
-1 * coord[0],
|
|
545
|
+
],
|
|
546
|
+
[
|
|
547
|
+
cosx[1] * vs.colSize * -1,
|
|
548
|
+
cosy[1] * vs.rowSize * -1,
|
|
549
|
+
cosz[1] * vs.sliceSize * -1,
|
|
550
|
+
-1 * coord[1],
|
|
551
|
+
],
|
|
552
|
+
[
|
|
553
|
+
cosx[2] * vs.colSize,
|
|
554
|
+
cosy[2] * vs.rowSize,
|
|
555
|
+
cosz[2] * vs.sliceSize,
|
|
556
|
+
coord[2],
|
|
557
|
+
],
|
|
558
|
+
[0, 0, 0, 1],
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
return m;
|
|
562
|
+
} // getBestTransform()
|
|
563
|
+
|
|
564
|
+
NVImage.prototype.readDICOM = function (buf) {
|
|
565
|
+
this.series = new daikon.Series();
|
|
566
|
+
// parse DICOM file
|
|
567
|
+
if (Array.isArray(buf)) {
|
|
568
|
+
for (let i = 0; i < buf.length; i++) {
|
|
569
|
+
let image = daikon.Series.parseImage(new DataView(buf[i]));
|
|
570
|
+
if (image === null) {
|
|
571
|
+
console.error(daikon.Series.parserError);
|
|
572
|
+
} else if (image.hasPixelData()) {
|
|
573
|
+
// if it's part of the same series, add it
|
|
574
|
+
if (
|
|
575
|
+
this.series.images.length === 0 ||
|
|
576
|
+
image.getSeriesId() === this.series.images[0].getSeriesId()
|
|
577
|
+
) {
|
|
578
|
+
this.series.addImage(image);
|
|
579
|
+
}
|
|
580
|
+
} // if hasPixelData
|
|
581
|
+
} // for i
|
|
582
|
+
} else {
|
|
583
|
+
// Array.isArray
|
|
584
|
+
var image = daikon.Series.parseImage(new DataView(buf));
|
|
585
|
+
if (image === null) {
|
|
586
|
+
console.error(daikon.Series.parserError);
|
|
587
|
+
} else if (image.hasPixelData()) {
|
|
588
|
+
// if it's part of the same series, add it
|
|
589
|
+
if (
|
|
590
|
+
this.series.images.length === 0 ||
|
|
591
|
+
image.getSeriesId() === this.series.images[0].getSeriesId()
|
|
592
|
+
) {
|
|
593
|
+
this.series.addImage(image);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// order the image files, determines number of frames, etc.
|
|
598
|
+
this.series.buildSeries();
|
|
599
|
+
// output some header info
|
|
600
|
+
this.hdr = new nifti.NIFTI1();
|
|
601
|
+
let hdr = this.hdr;
|
|
602
|
+
hdr.scl_inter = 0;
|
|
603
|
+
hdr.scl_slope = 1;
|
|
604
|
+
if (this.series.images[0].getDataScaleIntercept())
|
|
605
|
+
hdr.scl_inter = this.series.images[0].getDataScaleIntercept();
|
|
606
|
+
if (this.series.images[0].getDataScaleSlope())
|
|
607
|
+
hdr.scl_slope = this.series.images[0].getDataScaleSlope();
|
|
608
|
+
if (hdr.scl_slope === 0) hdr.scl_slope;
|
|
609
|
+
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
610
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
611
|
+
hdr.dims[1] = this.series.images[0].getCols();
|
|
612
|
+
hdr.dims[2] = this.series.images[0].getRows();
|
|
613
|
+
hdr.dims[3] = this.series.images[0].getNumberOfFrames();
|
|
614
|
+
if (this.series.images.length > 1) {
|
|
615
|
+
if (hdr.dims[3] > 1)
|
|
616
|
+
console.log(
|
|
617
|
+
"To Do: multiple slices per file and multiple files (XA30 DWI)"
|
|
618
|
+
);
|
|
619
|
+
hdr.dims[3] = this.series.images.length;
|
|
620
|
+
}
|
|
621
|
+
let rc = this.series.images[0].getPixelSpacing(); //TODO: order?
|
|
622
|
+
hdr.pixDims[1] = rc[0];
|
|
623
|
+
hdr.pixDims[2] = rc[1];
|
|
624
|
+
hdr.pixDims[3] = Math.max(
|
|
625
|
+
this.series.images[0].getSliceGap(),
|
|
626
|
+
this.series.images[0].getSliceThickness()
|
|
627
|
+
);
|
|
628
|
+
hdr.pixDims[4] = this.series.images[0].getTR() / 1000.0; //msec -> sec
|
|
629
|
+
let dt = this.series.images[0].getDataType(); //2=int,3=uint,4=float,
|
|
630
|
+
let bpv = this.series.images[0].getBitsAllocated();
|
|
631
|
+
hdr.numBitsPerVoxel = bpv;
|
|
632
|
+
this.hdr.littleEndian = this.series.images[0].littleEndian;
|
|
633
|
+
if (bpv === 8 && dt === 2) hdr.datatypeCode = this.DT_INT8;
|
|
634
|
+
else if (bpv === 8 && dt === 3) hdr.datatypeCode = this.DT_UNSIGNED_CHAR;
|
|
635
|
+
else if (bpv === 16 && dt === 2) hdr.datatypeCode = this.DT_SIGNED_SHORT;
|
|
636
|
+
else if (bpv === 16 && dt === 3) hdr.datatypeCode = this.DT_UINT16;
|
|
637
|
+
else if (bpv === 32 && dt === 2) hdr.datatypeCode = this.DT_SIGNED_INT;
|
|
638
|
+
else if (bpv === 32 && dt === 3) hdr.datatypeCode = this.DT_UINT32;
|
|
639
|
+
else if (bpv === 32 && dt === 4) hdr.datatypeCode = this.DT_FLOAT;
|
|
640
|
+
else if (bpv === 64 && dt === 4) hdr.datatypeCode = this.DT_DOUBLE;
|
|
641
|
+
else console.log("Unsupported DICOM format: " + dt + " " + bpv);
|
|
642
|
+
let voxelDimensions = hdr.pixDims.slice(1, 4);
|
|
643
|
+
//console.log("dir", this.series.images[0].getImageDirections());
|
|
644
|
+
//console.log("pos", this.series.images[0].getImagePosition());
|
|
645
|
+
//console.log("dims", voxelDimensions);
|
|
646
|
+
let m = getBestTransform(
|
|
647
|
+
this.series.images[0].getImageDirections(),
|
|
648
|
+
voxelDimensions,
|
|
649
|
+
this.series.images[0].getImagePosition()
|
|
650
|
+
);
|
|
651
|
+
if (m) {
|
|
652
|
+
hdr.sform_code = 1;
|
|
653
|
+
hdr.affine = [
|
|
654
|
+
[m[0][0], m[0][1], m[0][2], m[0][3]],
|
|
655
|
+
[m[1][0], m[1][1], m[1][2], m[1][3]],
|
|
656
|
+
[m[2][0], m[2][1], m[2][2], m[2][3]],
|
|
657
|
+
[0, 0, 0, 1],
|
|
658
|
+
];
|
|
659
|
+
}
|
|
660
|
+
console.log("DICOM", this.series.images[0]);
|
|
661
|
+
console.log("NIfTI", hdr);
|
|
662
|
+
let imgRaw = [];
|
|
663
|
+
//let byteLength = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * (bpv / 8);
|
|
664
|
+
let data;
|
|
665
|
+
let length = this.series.validatePixelDataLength(this.series.images[0]);
|
|
666
|
+
let buffer = new Uint8Array(
|
|
667
|
+
new ArrayBuffer(length * this.series.images.length)
|
|
668
|
+
);
|
|
669
|
+
// implementation copied from:
|
|
670
|
+
// https://github.com/rii-mango/Daikon/blob/bbe08bad9758dfbdf31ca22fb79048c7bad85706/src/series.js#L496
|
|
671
|
+
for (let i = 0; i < this.series.images.length; i++) {
|
|
672
|
+
if (this.series.isMosaic) {
|
|
673
|
+
data = this.series.getMosaicData(
|
|
674
|
+
this.series.images[i],
|
|
675
|
+
this.series.images[i].getPixelDataBytes()
|
|
676
|
+
);
|
|
677
|
+
} else {
|
|
678
|
+
data = this.series.images[i].getPixelDataBytes();
|
|
679
|
+
}
|
|
680
|
+
length = this.series.validatePixelDataLength(this.series.images[i]);
|
|
681
|
+
this.series.images[i].clearPixelData();
|
|
682
|
+
buffer.set(new Uint8Array(data, 0, length), length * i);
|
|
683
|
+
} // for images.length
|
|
684
|
+
imgRaw = buffer.buffer;
|
|
685
|
+
return imgRaw;
|
|
686
|
+
}; // readDICOM()
|
|
687
|
+
|
|
688
|
+
NVImage.prototype.readECAT = function (buffer) {
|
|
689
|
+
//https://github.com/openneuropet/PET2BIDS/tree/28aae3fab22309047d36d867c624cd629c921ca6/ecat_validation/ecat_info
|
|
690
|
+
this.hdr = new nifti.NIFTI1();
|
|
691
|
+
let hdr = this.hdr;
|
|
692
|
+
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
693
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
694
|
+
var reader = new DataView(buffer);
|
|
695
|
+
var raw = buffer;
|
|
696
|
+
let signature = reader.getInt32(0, false); //"MATR"
|
|
697
|
+
let filetype = reader.getInt16(50, false);
|
|
698
|
+
if (signature !== 1296127058 || filetype < 1 || filetype > 14) {
|
|
699
|
+
console.log("Not a valid ECAT file");
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
//list header, starts at 512 bytes: int32_t hdr[4], r[31][4];
|
|
703
|
+
let pos = 512; //512=main header, 4*32-bit hdr
|
|
704
|
+
let vols = 0;
|
|
705
|
+
let frame_duration = [];
|
|
706
|
+
let rawImg = [];
|
|
707
|
+
while (true) {
|
|
708
|
+
//read 512 block lists
|
|
709
|
+
let hdr0 = reader.getInt32(pos, false);
|
|
710
|
+
let hdr3 = reader.getInt32(pos + 12, false);
|
|
711
|
+
if (hdr0 + hdr3 !== 31) break;
|
|
712
|
+
let lpos = pos + 20; //skip hdr and read slice offset (r[0][1])
|
|
713
|
+
let r = 0;
|
|
714
|
+
let voloffset = 0;
|
|
715
|
+
while (r < 31) {
|
|
716
|
+
//r[0][1]...r[30][1]
|
|
717
|
+
voloffset = reader.getInt32(lpos, false);
|
|
718
|
+
lpos += 16; //e.g. r[0][1] to r[1][1]
|
|
719
|
+
if (voloffset === 0) break;
|
|
720
|
+
r++;
|
|
721
|
+
let ipos = voloffset * 512; //image start position
|
|
722
|
+
let spos = ipos - 512; //subheader for matrix image, immediately before image
|
|
723
|
+
let data_type = reader.getUint16(spos, false);
|
|
724
|
+
hdr.dims[1] = reader.getUint16(spos + 4, false);
|
|
725
|
+
hdr.dims[2] = reader.getUint16(spos + 6, false);
|
|
726
|
+
hdr.dims[3] = reader.getUint16(spos + 8, false);
|
|
727
|
+
let scale_factor = reader.getFloat32(spos + 26, false);
|
|
728
|
+
hdr.pixDims[1] = reader.getFloat32(spos + 34, false) * 10.0; //cm -> mm
|
|
729
|
+
hdr.pixDims[2] = reader.getFloat32(spos + 38, false) * 10.0; //cm -> mm
|
|
730
|
+
hdr.pixDims[3] = reader.getFloat32(spos + 42, false) * 10.0; //cm -> mm
|
|
731
|
+
hdr.pixDims[4] = reader.getUint32(spos + 46, false) / 1000.0; //ms -> sec
|
|
732
|
+
frame_duration.push(hdr.pixDims[4]);
|
|
733
|
+
let nvox3D = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
734
|
+
var newImg = new Float32Array(nvox3D); //convert to float32 as scale varies
|
|
735
|
+
if (data_type == 1)
|
|
736
|
+
//uint8
|
|
737
|
+
for (var i = 0; i < nvox3D; i++) {
|
|
738
|
+
newImg[i] = reader.getUint8(ipos) * scale_factor;
|
|
739
|
+
ipos++;
|
|
740
|
+
}
|
|
741
|
+
else if (data_type == 6) {
|
|
742
|
+
//uint16
|
|
743
|
+
for (var i = 0; i < nvox3D; i++) {
|
|
744
|
+
newImg[i] = reader.getUint16(ipos, false) * scale_factor;
|
|
745
|
+
ipos += 2;
|
|
746
|
+
}
|
|
747
|
+
} else if (ihdr.data_type == 7) {
|
|
748
|
+
//uint32
|
|
749
|
+
for (var i = 0; i < nvox3D; i++) {
|
|
750
|
+
newImg[i] = reader.getUint32(ipos, false) * scale_factor;
|
|
751
|
+
ipos += 4;
|
|
752
|
+
}
|
|
753
|
+
} else console.log("Unknown ECAT data type " + data_type);
|
|
754
|
+
let prevImg = rawImg.slice();
|
|
755
|
+
rawImg = new Float32Array(prevImg.length + newImg.length);
|
|
756
|
+
rawImg.set(prevImg);
|
|
757
|
+
rawImg.set(newImg, prevImg.length);
|
|
758
|
+
vols++;
|
|
759
|
+
}
|
|
760
|
+
if (voloffset === 0) break;
|
|
761
|
+
pos += 512; //possible to have multiple 512-byte lists of images
|
|
762
|
+
}
|
|
763
|
+
hdr.dims[4] = vols;
|
|
764
|
+
hdr.pixDims[4] = frame_duration[0];
|
|
765
|
+
if (vols > 1) {
|
|
766
|
+
hdr.dims[0] = 4;
|
|
767
|
+
let isFDvaries = false;
|
|
768
|
+
for (var i = 0; i < vols; i++)
|
|
769
|
+
if (frame_duration[i] !== frame_duration[0]) isFDvaries = true;
|
|
770
|
+
if (isFDvaries) console.log("Frame durations vary");
|
|
771
|
+
}
|
|
772
|
+
hdr.sform_code = 1;
|
|
773
|
+
hdr.affine = [
|
|
774
|
+
[-hdr.pixDims[1], 0, 0, (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],
|
|
775
|
+
[0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],
|
|
776
|
+
[0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],
|
|
777
|
+
[0, 0, 0, 1],
|
|
778
|
+
];
|
|
779
|
+
hdr.numBitsPerVoxel = 32;
|
|
780
|
+
hdr.datatypeCode = this.DT_FLOAT;
|
|
781
|
+
return rawImg;
|
|
782
|
+
}; // readECAT()
|
|
783
|
+
|
|
784
|
+
NVImage.prototype.readV16 = function (buffer) {
|
|
785
|
+
this.hdr = new nifti.NIFTI1();
|
|
786
|
+
let hdr = this.hdr;
|
|
787
|
+
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
788
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
789
|
+
var reader = new DataView(buffer);
|
|
790
|
+
hdr.dims[1] = reader.getUint16(0, true);
|
|
791
|
+
hdr.dims[2] = reader.getUint16(2, true);
|
|
792
|
+
hdr.dims[3] = reader.getUint16(4, true);
|
|
793
|
+
let nBytes = 2 * hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
794
|
+
if (nBytes + 6 !== buffer.byteLength)
|
|
795
|
+
console.log("This does not look like a valid BrainVoyager V16 file");
|
|
796
|
+
hdr.numBitsPerVoxel = 16;
|
|
797
|
+
hdr.datatypeCode = this.DT_UINT16;
|
|
798
|
+
console.log("Warning: V16 files have no spatial transforms");
|
|
799
|
+
hdr.affine = [
|
|
800
|
+
[0, 0, -hdr.pixDims[1], (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],
|
|
801
|
+
[-hdr.pixDims[2], 0, 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],
|
|
802
|
+
[0, -hdr.pixDims[3], 0, (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],
|
|
803
|
+
[0, 0, 0, 1],
|
|
804
|
+
];
|
|
805
|
+
hdr.littleEndian = true;
|
|
806
|
+
return buffer.slice(6);
|
|
807
|
+
}; // readV16()
|
|
808
|
+
|
|
809
|
+
NVImage.prototype.readVMR = function (buffer) {
|
|
810
|
+
//https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/343-developer-guide-2-6-the-format-of-vmr-files
|
|
811
|
+
this.hdr = new nifti.NIFTI1();
|
|
812
|
+
let hdr = this.hdr;
|
|
813
|
+
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
814
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
815
|
+
var reader = new DataView(buffer);
|
|
816
|
+
let version = reader.getUint16(0, true);
|
|
817
|
+
if (version !== 4) console.log("Not a valid version 4 VMR image");
|
|
818
|
+
hdr.dims[1] = reader.getUint16(2, true);
|
|
819
|
+
hdr.dims[2] = reader.getUint16(4, true);
|
|
820
|
+
hdr.dims[3] = reader.getUint16(6, true);
|
|
821
|
+
let nBytes = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
822
|
+
if (version >= 4) {
|
|
823
|
+
let pos = 8 + nBytes; //offset to post header
|
|
824
|
+
let xoff = reader.getUint16(pos, true);
|
|
825
|
+
let yoff = reader.getUint16(pos + 2, true);
|
|
826
|
+
let zoff = reader.getUint16(pos + 4, true);
|
|
827
|
+
let framingCube = reader.getUint16(pos + 6, true);
|
|
828
|
+
let posInfo = reader.getUint32(pos + 8, true);
|
|
829
|
+
let coordSys = reader.getUint32(pos + 12, true);
|
|
830
|
+
let XmmStart = reader.getFloat32(pos + 16, true);
|
|
831
|
+
let YmmStart = reader.getFloat32(pos + 20, true);
|
|
832
|
+
let ZmmStart = reader.getFloat32(pos + 24, true);
|
|
833
|
+
let XmmEnd = reader.getFloat32(pos + 28, true);
|
|
834
|
+
let YmmEnd = reader.getFloat32(pos + 32, true);
|
|
835
|
+
let ZmmEnd = reader.getFloat32(pos + 36, true);
|
|
836
|
+
let Xsl = reader.getFloat32(pos + 40, true);
|
|
837
|
+
let Ysl = reader.getFloat32(pos + 44, true);
|
|
838
|
+
let Zsl = reader.getFloat32(pos + 48, true);
|
|
839
|
+
let colDirX = reader.getFloat32(pos + 52, true);
|
|
840
|
+
let colDirY = reader.getFloat32(pos + 56, true);
|
|
841
|
+
let colDirZ = reader.getFloat32(pos + 60, true);
|
|
842
|
+
let nRow = reader.getUint32(pos + 64, true);
|
|
843
|
+
let nCol = reader.getUint32(pos + 68, true);
|
|
844
|
+
let FOVrow = reader.getFloat32(pos + 72, true);
|
|
845
|
+
let FOVcol = reader.getFloat32(pos + 76, true);
|
|
846
|
+
let sliceThickness = reader.getFloat32(pos + 80, true);
|
|
847
|
+
let gapThickness = reader.getFloat32(pos + 84, true);
|
|
848
|
+
let nSpatialTransforms = reader.getUint32(pos + 88, true);
|
|
849
|
+
pos = pos + 92;
|
|
850
|
+
if (nSpatialTransforms > 0) {
|
|
851
|
+
let len = buffer.byteLength;
|
|
852
|
+
for (let i = 0; i < nSpatialTransforms; i++) {
|
|
853
|
+
//read variable length name name...
|
|
854
|
+
while (pos < len && reader.getUint8(pos) !== 0) pos++;
|
|
855
|
+
pos++;
|
|
856
|
+
let typ = reader.getUint32(pos, true);
|
|
857
|
+
pos += 4;
|
|
858
|
+
//read variable length name name...
|
|
859
|
+
while (pos < len && reader.getUint8(pos) !== 0) pos++;
|
|
860
|
+
pos++;
|
|
861
|
+
let nValues = reader.getUint32(pos, true);
|
|
862
|
+
pos += 4;
|
|
863
|
+
for (let j = 0; j < nValues; j++) pos += 4;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
let LRconv = reader.getUint8(pos);
|
|
867
|
+
let ref = reader.getUint8(pos + 1);
|
|
868
|
+
hdr.pixDims[1] = reader.getFloat32(pos + 2, true);
|
|
869
|
+
hdr.pixDims[2] = reader.getFloat32(pos + 6, true);
|
|
870
|
+
hdr.pixDims[3] = reader.getFloat32(pos + 10, true);
|
|
871
|
+
let isVer = reader.getUint8(pos + 14);
|
|
872
|
+
let isTal = reader.getUint8(pos + 15);
|
|
873
|
+
let minInten = reader.getInt32(pos + 16, true);
|
|
874
|
+
let meanInten = reader.getInt32(pos + 20, true);
|
|
875
|
+
let maxInten = reader.getInt32(pos + 24, true);
|
|
876
|
+
}
|
|
877
|
+
console.log("Warning: VMR spatial transform not implemented");
|
|
878
|
+
//if (XmmStart === XmmEnd) { // https://brainvoyager.com/bv/sampledata/index.html??
|
|
879
|
+
hdr.affine = [
|
|
880
|
+
[0, 0, -hdr.pixDims[1], (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],
|
|
881
|
+
[-hdr.pixDims[2], 0, 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],
|
|
882
|
+
[0, -hdr.pixDims[3], 0, (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],
|
|
883
|
+
[0, 0, 0, 1],
|
|
884
|
+
];
|
|
885
|
+
//}
|
|
886
|
+
console.log(hdr);
|
|
887
|
+
hdr.numBitsPerVoxel = 8;
|
|
888
|
+
hdr.datatypeCode = this.DT_UNSIGNED_CHAR;
|
|
889
|
+
return buffer.slice(8, 8 + nBytes);
|
|
890
|
+
}; // readVMR()
|
|
891
|
+
|
|
892
|
+
NVImage.prototype.readMGH = function (buffer) {
|
|
893
|
+
this.hdr = new nifti.NIFTI1();
|
|
894
|
+
let hdr = this.hdr;
|
|
895
|
+
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
896
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
897
|
+
var reader = new DataView(buffer);
|
|
898
|
+
var raw = buffer;
|
|
899
|
+
if (reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {
|
|
900
|
+
var raw = fflate.decompressSync(new Uint8Array(buffer));
|
|
901
|
+
reader = new DataView(raw.buffer);
|
|
902
|
+
}
|
|
903
|
+
let version = reader.getInt32(0, false);
|
|
904
|
+
let width = reader.getInt32(4, false);
|
|
905
|
+
let height = reader.getInt32(8, false);
|
|
906
|
+
let depth = reader.getInt32(12, false);
|
|
907
|
+
let nframes = reader.getInt32(16, false);
|
|
908
|
+
let mtype = reader.getInt32(20, false);
|
|
909
|
+
let dof = reader.getInt32(24, false);
|
|
910
|
+
let goodRASFlag = reader.getInt16(28, false);
|
|
911
|
+
let spacingX = reader.getFloat32(30, false);
|
|
912
|
+
let spacingY = reader.getFloat32(34, false);
|
|
913
|
+
let spacingZ = reader.getFloat32(38, false);
|
|
914
|
+
let xr = reader.getFloat32(42, false);
|
|
915
|
+
let xa = reader.getFloat32(46, false);
|
|
916
|
+
let xs = reader.getFloat32(50, false);
|
|
917
|
+
let yr = reader.getFloat32(54, false);
|
|
918
|
+
let ya = reader.getFloat32(58, false);
|
|
919
|
+
let ys = reader.getFloat32(62, false);
|
|
920
|
+
let zr = reader.getFloat32(66, false);
|
|
921
|
+
let za = reader.getFloat32(70, false);
|
|
922
|
+
let zs = reader.getFloat32(74, false);
|
|
923
|
+
let cr = reader.getFloat32(78, false);
|
|
924
|
+
let ca = reader.getFloat32(82, false);
|
|
925
|
+
let cs = reader.getFloat32(86, false);
|
|
926
|
+
if (version !== 1 || mtype < 0 || mtype > 4)
|
|
927
|
+
console.log("Not a valid MGH file");
|
|
928
|
+
if (mtype === 0) {
|
|
929
|
+
hdr.numBitsPerVoxel = 8;
|
|
930
|
+
hdr.datatypeCode = this.DT_UNSIGNED_CHAR;
|
|
931
|
+
} else if (mtype === 4) {
|
|
932
|
+
hdr.numBitsPerVoxel = 16;
|
|
933
|
+
hdr.datatypeCode = this.DT_SIGNED_SHORT;
|
|
934
|
+
} else if (mtype === 1) {
|
|
935
|
+
hdr.numBitsPerVoxel = 32;
|
|
936
|
+
hdr.datatypeCode = this.DT_SIGNED_INT;
|
|
937
|
+
} else if (mtype === 3) {
|
|
938
|
+
hdr.numBitsPerVoxel = 32;
|
|
939
|
+
hdr.datatypeCode = this.DT_FLOAT;
|
|
940
|
+
}
|
|
941
|
+
hdr.dims[1] = width;
|
|
942
|
+
hdr.dims[2] = height;
|
|
943
|
+
hdr.dims[3] = depth;
|
|
944
|
+
hdr.dims[4] = nframes;
|
|
945
|
+
if (nframes > 1) hdr.dims[0] = 4;
|
|
946
|
+
hdr.pixDims[1] = spacingX;
|
|
947
|
+
hdr.pixDims[2] = spacingY;
|
|
948
|
+
hdr.pixDims[3] = spacingZ;
|
|
949
|
+
hdr.vox_offset = 284;
|
|
950
|
+
hdr.sform_code = 1;
|
|
951
|
+
let rot44 = mat4.fromValues(
|
|
952
|
+
xr * hdr.pixDims[1],
|
|
953
|
+
yr * hdr.pixDims[2],
|
|
954
|
+
zr * hdr.pixDims[3],
|
|
955
|
+
0,
|
|
956
|
+
xa * hdr.pixDims[1],
|
|
957
|
+
ya * hdr.pixDims[2],
|
|
958
|
+
za * hdr.pixDims[3],
|
|
959
|
+
0,
|
|
960
|
+
xs * hdr.pixDims[1],
|
|
961
|
+
ys * hdr.pixDims[2],
|
|
962
|
+
zs * hdr.pixDims[3],
|
|
963
|
+
0,
|
|
964
|
+
0,
|
|
965
|
+
0,
|
|
966
|
+
0,
|
|
967
|
+
1
|
|
968
|
+
);
|
|
969
|
+
let base = 0.0; //0 or 1: are voxels indexed from 0 or 1?
|
|
970
|
+
let Pcrs = [
|
|
971
|
+
hdr.dims[1] / 2.0 + base,
|
|
972
|
+
hdr.dims[2] / 2.0 + base,
|
|
973
|
+
hdr.dims[3] / 2.0 + base,
|
|
974
|
+
1,
|
|
975
|
+
];
|
|
976
|
+
let PxyzOffset = [0, 0, 0, 0];
|
|
977
|
+
for (var i = 0; i < 3; i++) {
|
|
978
|
+
//multiply Pcrs * m
|
|
979
|
+
for (var j = 0; j < 3; j++) {
|
|
980
|
+
PxyzOffset[i] = PxyzOffset[i] + rot44[i + j * 4] * Pcrs[j];
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
hdr.affine = [
|
|
984
|
+
[rot44[0], rot44[1], rot44[2], PxyzOffset[0]],
|
|
985
|
+
[rot44[4], rot44[5], rot44[6], PxyzOffset[1]],
|
|
986
|
+
[rot44[8], rot44[9], rot44[10], PxyzOffset[2]],
|
|
987
|
+
[0, 0, 0, 1],
|
|
988
|
+
];
|
|
989
|
+
let nBytes =
|
|
990
|
+
hdr.dims[1] *
|
|
991
|
+
hdr.dims[2] *
|
|
992
|
+
hdr.dims[3] *
|
|
993
|
+
hdr.dims[4] *
|
|
994
|
+
(hdr.numBitsPerVoxel / 8);
|
|
995
|
+
return raw.slice(hdr.vox_offset, hdr.vox_offset + nBytes);
|
|
996
|
+
}; // readMGH()
|
|
997
|
+
|
|
998
|
+
NVImage.prototype.readHEAD = function (dataBuffer, pairedImgData) {
|
|
999
|
+
this.hdr = new nifti.NIFTI1();
|
|
1000
|
+
let hdr = this.hdr;
|
|
1001
|
+
hdr.dims[0] = 3;
|
|
1002
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
1003
|
+
let orientSpecific = [0, 0, 0];
|
|
1004
|
+
let xyzOrigin = [0, 0, 0];
|
|
1005
|
+
let xyzDelta = [1, 1, 1];
|
|
1006
|
+
let txt = new TextDecoder().decode(dataBuffer);
|
|
1007
|
+
var lines = txt.split("\n");
|
|
1008
|
+
let nlines = lines.length;
|
|
1009
|
+
let i = 0;
|
|
1010
|
+
let hasIJK_TO_DICOM_REAL = false;
|
|
1011
|
+
while (i < nlines) {
|
|
1012
|
+
let line = lines[i]; //e.g. 'type = string-attribute'
|
|
1013
|
+
i++;
|
|
1014
|
+
if (!line.startsWith("type")) continue; //n.b. white space varies, "type =" vs "type ="
|
|
1015
|
+
let isInt = line.includes("integer-attribute");
|
|
1016
|
+
let isFloat = line.includes("float-attribute");
|
|
1017
|
+
line = lines[i]; //e.g. 'name = IDCODE_DATE'
|
|
1018
|
+
i++;
|
|
1019
|
+
if (!line.startsWith("name")) continue;
|
|
1020
|
+
let items = line.split("= ");
|
|
1021
|
+
let key = items[1]; //e.g. 'IDCODE_DATE'
|
|
1022
|
+
line = lines[i]; //e.g. 'count = 5'
|
|
1023
|
+
i++;
|
|
1024
|
+
items = line.split("= ");
|
|
1025
|
+
let count = parseInt(items[1]); //e.g. '5'
|
|
1026
|
+
if (count < 1) continue;
|
|
1027
|
+
line = lines[i]; //e.g. ''LSB_FIRST~'
|
|
1028
|
+
i++;
|
|
1029
|
+
items = line.trim().split(/\s+/);
|
|
1030
|
+
if (isFloat || isInt) {
|
|
1031
|
+
//read arrays written on multiple lines
|
|
1032
|
+
while (items.length < count) {
|
|
1033
|
+
line = lines[i]; //e.g. ''LSB_FIRST~'
|
|
1034
|
+
i++;
|
|
1035
|
+
let items2 = line.trim().split(/\s+/);
|
|
1036
|
+
items.push(...items2);
|
|
1037
|
+
}
|
|
1038
|
+
for (var j = 0; j < count; j++) items[j] = parseFloat(items[j]);
|
|
1039
|
+
}
|
|
1040
|
+
switch (key) {
|
|
1041
|
+
case "BYTEORDER_STRING":
|
|
1042
|
+
if (items[0].includes("LSB_FIRST")) hdr.littleEndian = true;
|
|
1043
|
+
else if (items[0].includes("MSB_FIRST")) hdr.littleEndian = false;
|
|
1044
|
+
break;
|
|
1045
|
+
case "BRICK_TYPES":
|
|
1046
|
+
hdr.dims[4] = count;
|
|
1047
|
+
let datatype = parseInt(items[0]);
|
|
1048
|
+
if (datatype === 0) {
|
|
1049
|
+
hdr.numBitsPerVoxel = 8;
|
|
1050
|
+
hdr.datatypeCode = this.DT_UNSIGNED_CHAR;
|
|
1051
|
+
} else if (datatype === 1) {
|
|
1052
|
+
hdr.numBitsPerVoxel = 16;
|
|
1053
|
+
hdr.datatypeCode = this.DT_SIGNED_SHORT;
|
|
1054
|
+
} else if (datatype === 1) {
|
|
1055
|
+
hdr.numBitsPerVoxel = 32;
|
|
1056
|
+
hdr.datatypeCode = this.DT_FLOAT;
|
|
1057
|
+
} else console.log("Unknown BRICK_TYPES ", datatype);
|
|
1058
|
+
break;
|
|
1059
|
+
case "IJK_TO_DICOM_REAL":
|
|
1060
|
+
if (count < 12) break;
|
|
1061
|
+
hasIJK_TO_DICOM_REAL = true;
|
|
1062
|
+
hdr.sform_code = 2;
|
|
1063
|
+
//note DICOM space is LPS while NIfTI is RAS
|
|
1064
|
+
hdr.affine = [
|
|
1065
|
+
[-items[0], -items[1], -items[2], -items[3]],
|
|
1066
|
+
[-items[4], -items[5], -items[6], -items[7]],
|
|
1067
|
+
[items[8], items[9], items[10], items[11]],
|
|
1068
|
+
[0, 0, 0, 1],
|
|
1069
|
+
];
|
|
1070
|
+
break;
|
|
1071
|
+
case "DATASET_DIMENSIONS":
|
|
1072
|
+
count = Math.max(count, 3);
|
|
1073
|
+
for (var j = 0; j < count; j++) hdr.dims[j + 1] = items[j];
|
|
1074
|
+
break;
|
|
1075
|
+
case "ORIENT_SPECIFIC":
|
|
1076
|
+
orientSpecific = items;
|
|
1077
|
+
break;
|
|
1078
|
+
case "ORIGIN":
|
|
1079
|
+
xyzOrigin = items;
|
|
1080
|
+
break;
|
|
1081
|
+
case "DELTA":
|
|
1082
|
+
xyzDelta = items;
|
|
1083
|
+
break;
|
|
1084
|
+
case "TAXIS_FLOATS":
|
|
1085
|
+
hdr.pixDims[4] = items[0];
|
|
1086
|
+
break;
|
|
1087
|
+
default:
|
|
1088
|
+
//console.log('Unknown:',key);
|
|
1089
|
+
} //read item
|
|
1090
|
+
} //read all lines
|
|
1091
|
+
if (!hasIJK_TO_DICOM_REAL)
|
|
1092
|
+
this.THD_daxes_to_NIFTI(xyzDelta, xyzOrigin, orientSpecific);
|
|
1093
|
+
else this.SetPixDimFromSForm();
|
|
1094
|
+
let nBytes =
|
|
1095
|
+
(hdr.numBitsPerVoxel / 8) *
|
|
1096
|
+
hdr.dims[1] *
|
|
1097
|
+
hdr.dims[2] *
|
|
1098
|
+
hdr.dims[3] *
|
|
1099
|
+
hdr.dims[4];
|
|
1100
|
+
if (pairedImgData.byteLength < nBytes) {
|
|
1101
|
+
//n.b. npm run dev implicitly extracts gz, npm run demo does not!
|
|
1102
|
+
//assume gz compressed
|
|
1103
|
+
var raw = fflate.decompressSync(new Uint8Array(pairedImgData));
|
|
1104
|
+
return raw.buffer;
|
|
1105
|
+
}
|
|
1106
|
+
return pairedImgData.slice();
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
NVImage.prototype.readMHA = function (buffer, pairedImgData) {
|
|
1110
|
+
//https://itk.org/Wiki/ITK/MetaIO/Documentation#Reading_a_Brick-of-Bytes_.28an_N-Dimensional_volume_in_a_single_file.29
|
|
1111
|
+
let len = buffer.byteLength;
|
|
1112
|
+
if (len < 20)
|
|
1113
|
+
throw new Error("File too small to be VTK: bytes = " + buffer.byteLength);
|
|
1114
|
+
var bytes = new Uint8Array(buffer);
|
|
1115
|
+
let pos = 0;
|
|
1116
|
+
function readStr() {
|
|
1117
|
+
while (pos < len && bytes[pos] === 10) pos++; //skip blank lines
|
|
1118
|
+
let startPos = pos;
|
|
1119
|
+
while (pos < len && bytes[pos] !== 10) pos++;
|
|
1120
|
+
pos++; //skip EOLN
|
|
1121
|
+
if (pos - startPos < 1) return "";
|
|
1122
|
+
return new TextDecoder().decode(buffer.slice(startPos, pos - 1));
|
|
1123
|
+
}
|
|
1124
|
+
let line = readStr(); //1st line: signature
|
|
1125
|
+
this.hdr = new nifti.NIFTI1();
|
|
1126
|
+
let hdr = this.hdr;
|
|
1127
|
+
hdr.littleEndian = true;
|
|
1128
|
+
let isGz = false;
|
|
1129
|
+
let isDetached = false;
|
|
1130
|
+
let compressedDataSize = 0;
|
|
1131
|
+
let mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
|
|
1132
|
+
let offset = vec3.fromValues(0, 0, 0);
|
|
1133
|
+
while (line !== "") {
|
|
1134
|
+
let items = line.split(" ");
|
|
1135
|
+
if (items.length > 2);
|
|
1136
|
+
items = items.slice(2);
|
|
1137
|
+
if (line.startsWith("BinaryDataByteOrderMSB") && items[0].includes("False"))
|
|
1138
|
+
hdr.littleEndian = true;
|
|
1139
|
+
if (line.startsWith("BinaryDataByteOrderMSB") && items[0].includes("True"))
|
|
1140
|
+
hdr.littleEndian = false;
|
|
1141
|
+
if (line.startsWith("CompressedData") && items[0].includes("True"))
|
|
1142
|
+
isGz = true;
|
|
1143
|
+
if (line.startsWith("CompressedDataSize"))
|
|
1144
|
+
compressedDataSize = parseInt(items[0]);
|
|
1145
|
+
if (line.startsWith("TransformMatrix")) {
|
|
1146
|
+
for (var d = 0; d < 9; d++) mat33[d] = parseFloat(items[d]);
|
|
1147
|
+
}
|
|
1148
|
+
if (line.startsWith("Offset")) {
|
|
1149
|
+
offset[0] = parseFloat(items[0]);
|
|
1150
|
+
offset[1] = parseFloat(items[1]);
|
|
1151
|
+
offset[2] = parseFloat(items[2]);
|
|
1152
|
+
}
|
|
1153
|
+
//if (line.startsWith("AnatomicalOrientation")) //we can ignore, tested with Slicer3D converting NIfTIspace images
|
|
1154
|
+
if (line.startsWith("ElementSpacing")) {
|
|
1155
|
+
hdr.pixDims[1] = parseFloat(items[0]);
|
|
1156
|
+
hdr.pixDims[2] = parseFloat(items[1]);
|
|
1157
|
+
hdr.pixDims[3] = parseFloat(items[2]);
|
|
1158
|
+
if (items.length > 3) hdr.pixDims[4] = parseFloat(items[3]);
|
|
1159
|
+
}
|
|
1160
|
+
if (line.startsWith("DimSize")) {
|
|
1161
|
+
hdr.dims[0] = items.length;
|
|
1162
|
+
for (var d = 0; d < items.length; d++)
|
|
1163
|
+
hdr.dims[d + 1] = parseInt(items[d]);
|
|
1164
|
+
}
|
|
1165
|
+
if (line.startsWith("ElementType")) {
|
|
1166
|
+
switch (items[0]) {
|
|
1167
|
+
case "MET_UCHAR":
|
|
1168
|
+
hdr.numBitsPerVoxel = 8;
|
|
1169
|
+
hdr.datatypeCode = this.DT_UNSIGNED_CHAR;
|
|
1170
|
+
break;
|
|
1171
|
+
case "MET_CHAR":
|
|
1172
|
+
hdr.numBitsPerVoxel = 8;
|
|
1173
|
+
hdr.datatypeCode = this.DT_INT8;
|
|
1174
|
+
break;
|
|
1175
|
+
case "MET_SHORT":
|
|
1176
|
+
hdr.numBitsPerVoxel = 16;
|
|
1177
|
+
hdr.datatypeCode = this.DT_SIGNED_SHORT;
|
|
1178
|
+
break;
|
|
1179
|
+
case "MET_USHORT":
|
|
1180
|
+
hdr.numBitsPerVoxel = 16;
|
|
1181
|
+
hdr.datatypeCode = this.DT_UINT16;
|
|
1182
|
+
break;
|
|
1183
|
+
case "MET_INT":
|
|
1184
|
+
hdr.numBitsPerVoxel = 32;
|
|
1185
|
+
hdr.datatypeCode = this.DT_SIGNED_INT;
|
|
1186
|
+
break;
|
|
1187
|
+
case "MET_UINT":
|
|
1188
|
+
hdr.numBitsPerVoxel = 32;
|
|
1189
|
+
hdr.datatypeCode = this.DT_UINT32;
|
|
1190
|
+
break;
|
|
1191
|
+
case "MET_FLOAT":
|
|
1192
|
+
hdr.numBitsPerVoxel = 32;
|
|
1193
|
+
hdr.datatypeCode = this.DT_FLOAT;
|
|
1194
|
+
break;
|
|
1195
|
+
case "MET_DOUBLE":
|
|
1196
|
+
hdr.numBitsPerVoxel = 64;
|
|
1197
|
+
hdr.datatypeCode = this.DT_DOUBLE;
|
|
1198
|
+
break;
|
|
1199
|
+
default:
|
|
1200
|
+
throw new Error("Unsupported NRRD data type: " + value);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (line.startsWith("ObjectType") && !items[0].includes("Image"))
|
|
1204
|
+
console.log("Only able to read ObjectType = Image, not " + line);
|
|
1205
|
+
if (line.startsWith("ElementDataFile")) {
|
|
1206
|
+
if (items[0] !== "LOCAL") isDetached = true;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
line = readStr();
|
|
1210
|
+
}
|
|
1211
|
+
let mmMat = mat3.fromValues(
|
|
1212
|
+
hdr.pixDims[1],
|
|
1213
|
+
0,
|
|
1214
|
+
0,
|
|
1215
|
+
0,
|
|
1216
|
+
hdr.pixDims[2],
|
|
1217
|
+
0,
|
|
1218
|
+
0,
|
|
1219
|
+
0,
|
|
1220
|
+
hdr.pixDims[3]
|
|
1221
|
+
);
|
|
1222
|
+
mat3.multiply(mat33, mmMat, mat33);
|
|
1223
|
+
hdr.affine = [
|
|
1224
|
+
[-mat33[0], -mat33[3], -mat33[6], -offset[0]],
|
|
1225
|
+
[-mat33[1], -mat33[4], -mat33[7], -offset[1]],
|
|
1226
|
+
[mat33[2], mat33[5], mat33[8], offset[2]],
|
|
1227
|
+
[0, 0, 0, 1],
|
|
1228
|
+
];
|
|
1229
|
+
hdr.vox_offset = pos;
|
|
1230
|
+
if (isDetached && pairedImgData) {
|
|
1231
|
+
if (isGz)
|
|
1232
|
+
return fflate.decompressSync(new Uint8Array(buffer.slice(hdr.vox_offset)))
|
|
1233
|
+
.buffer;
|
|
1234
|
+
return pairedImgData.slice();
|
|
1235
|
+
}
|
|
1236
|
+
if (isGz)
|
|
1237
|
+
return fflate.decompressSync(new Uint8Array(buffer.slice(hdr.vox_offset)))
|
|
1238
|
+
.buffer;
|
|
1239
|
+
return buffer.slice(hdr.vox_offset);
|
|
1240
|
+
}; //readMHA()
|
|
1241
|
+
|
|
1242
|
+
NVImage.prototype.readMIF = function (buffer, pairedImgData) {
|
|
1243
|
+
//https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#mrtrix-image-formats
|
|
1244
|
+
//MIF files typically 3D (e.g. anatomical), 4D (fMRI, DWI). 5D rarely seen
|
|
1245
|
+
//This read currently supports up to 5D. To create test: "mrcat -axis 4 a4d.mif b4d.mif out5d.mif"
|
|
1246
|
+
this.hdr = new nifti.NIFTI1();
|
|
1247
|
+
let hdr = this.hdr;
|
|
1248
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
1249
|
+
hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1];
|
|
1250
|
+
let len = buffer.byteLength;
|
|
1251
|
+
if (len < 20) throw new Error("File too small to be MIF: bytes = " + len);
|
|
1252
|
+
var bytes = new Uint8Array(buffer);
|
|
1253
|
+
if (bytes[0] === 31 && bytes[1] === 139) {
|
|
1254
|
+
console.log("MIF with GZ decompression");
|
|
1255
|
+
var raw = fflate.decompressSync(new Uint8Array(buffer));
|
|
1256
|
+
buffer = raw.buffer;
|
|
1257
|
+
len = buffer.byteLength;
|
|
1258
|
+
}
|
|
1259
|
+
let pos = 0;
|
|
1260
|
+
function readStr() {
|
|
1261
|
+
while (pos < len && bytes[pos] === 10) pos++; //skip blank lines
|
|
1262
|
+
let startPos = pos;
|
|
1263
|
+
while (pos < len && bytes[pos] !== 10) pos++;
|
|
1264
|
+
pos++; //skip EOLN
|
|
1265
|
+
if (pos - startPos < 1) return "";
|
|
1266
|
+
return new TextDecoder().decode(buffer.slice(startPos, pos - 1));
|
|
1267
|
+
}
|
|
1268
|
+
let line = readStr(); //1st line: signature 'mrtrix tracks'
|
|
1269
|
+
if (!line.startsWith("mrtrix image")) {
|
|
1270
|
+
console.log("Not a valid MIF file");
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
let layout = [];
|
|
1274
|
+
let nTransform = 0;
|
|
1275
|
+
let TR = 0;
|
|
1276
|
+
let isDetached = false;
|
|
1277
|
+
line = readStr();
|
|
1278
|
+
while (pos < len && !line.startsWith("END")) {
|
|
1279
|
+
let items = line.split(":"); // "vox: 1,1,1" -> "vox", " 1,1,1"
|
|
1280
|
+
line = readStr();
|
|
1281
|
+
if (items.length < 2) break; //
|
|
1282
|
+
let tag = items[0]; // "datatype", "dim"
|
|
1283
|
+
items = items[1].split(","); // " 1,1,1" -> " 1", "1", "1"
|
|
1284
|
+
for (let i = 0; i < items.length; i++) items[i] = items[i].trim(); // " 1", "1", "1" -> "1", "1", "1"
|
|
1285
|
+
switch (tag) {
|
|
1286
|
+
case "dim":
|
|
1287
|
+
hdr.dims[0] = items.length;
|
|
1288
|
+
for (let i = 0; i < items.length; i++)
|
|
1289
|
+
hdr.dims[i + 1] = parseInt(items[i]);
|
|
1290
|
+
break;
|
|
1291
|
+
case "vox":
|
|
1292
|
+
for (let i = 0; i < items.length; i++) {
|
|
1293
|
+
hdr.pixDims[i + 1] = parseFloat(items[i]);
|
|
1294
|
+
if (isNaN(hdr.pixDims[i + 1])) hdr.pixDims[i + 1] = 0.0;
|
|
1295
|
+
}
|
|
1296
|
+
break;
|
|
1297
|
+
case "layout":
|
|
1298
|
+
for (let i = 0; i < items.length; i++) layout.push(parseInt(items[i])); //n.b. JavaScript preserves sign for -0
|
|
1299
|
+
break;
|
|
1300
|
+
case "datatype":
|
|
1301
|
+
let dt = items[0];
|
|
1302
|
+
if (dt.startsWith("Int8")) hdr.datatypeCode = this.DT_INT8;
|
|
1303
|
+
else if (dt.startsWith("UInt8"))
|
|
1304
|
+
hdr.datatypeCode = this.DT_UNSIGNED_CHAR;
|
|
1305
|
+
else if (dt.startsWith("Int16"))
|
|
1306
|
+
hdr.datatypeCode = this.DT_SIGNED_SHORT;
|
|
1307
|
+
else if (dt.startsWith("UInt16")) hdr.datatypeCode = this.DT_UINT16;
|
|
1308
|
+
else if (dt.startsWith("Int32")) hdr.datatypeCode = this.DT_SIGNED_INT;
|
|
1309
|
+
else if (dt.startsWith("UInt32")) hdr.datatypeCode = this.DT_UINT32;
|
|
1310
|
+
else if (dt.startsWith("Float32")) hdr.datatypeCode = this.DT_FLOAT;
|
|
1311
|
+
else if (dt.startsWith("Float64")) hdr.datatypeCode = this.DT_DOUBLE;
|
|
1312
|
+
else console.log("Unsupported datatype " + dt);
|
|
1313
|
+
if (dt.includes("8")) hdr.numBitsPerVoxel = 8;
|
|
1314
|
+
else if (dt.includes("16")) hdr.numBitsPerVoxel = 16;
|
|
1315
|
+
else if (dt.includes("32")) hdr.numBitsPerVoxel = 32;
|
|
1316
|
+
else if (dt.includes("64")) hdr.numBitsPerVoxel = 64;
|
|
1317
|
+
hdr.littleEndian = true; //native, to do support big endian readers
|
|
1318
|
+
if (dt.endsWith("LE")) hdr.littleEndian = true;
|
|
1319
|
+
if (dt.endsWith("BE")) hdr.littleEndian = false;
|
|
1320
|
+
break;
|
|
1321
|
+
case "transform":
|
|
1322
|
+
if (nTransform > 2 || items.length !== 4) break;
|
|
1323
|
+
hdr.affine[nTransform][0] = parseFloat(items[0]);
|
|
1324
|
+
hdr.affine[nTransform][1] = parseFloat(items[1]);
|
|
1325
|
+
hdr.affine[nTransform][2] = parseFloat(items[2]);
|
|
1326
|
+
hdr.affine[nTransform][3] = parseFloat(items[3]);
|
|
1327
|
+
nTransform++;
|
|
1328
|
+
break;
|
|
1329
|
+
case "RepetitionTime":
|
|
1330
|
+
TR = parseFloat(items[0]);
|
|
1331
|
+
break;
|
|
1332
|
+
case "file":
|
|
1333
|
+
isDetached = !items[0].startsWith(". ");
|
|
1334
|
+
if (!isDetached) {
|
|
1335
|
+
items = items[0].split(" "); //". 2336" -> ". ", "2336"
|
|
1336
|
+
hdr.vox_offset = parseInt(items[1]);
|
|
1337
|
+
}
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
let ndim = hdr.dims[0];
|
|
1342
|
+
if (ndim > 5)
|
|
1343
|
+
console.log("reader only designed for a maximum of 5 dimensions (XYZTD)");
|
|
1344
|
+
let nvox = 1;
|
|
1345
|
+
for (let i = 0; i < ndim; i++) nvox *= Math.max(hdr.dims[i + 1], 1);
|
|
1346
|
+
console.log(nvox);
|
|
1347
|
+
//let nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4];
|
|
1348
|
+
for (let i = 0; i < 3; i++) {
|
|
1349
|
+
for (let j = 0; j < 3; j++) {
|
|
1350
|
+
//hdr.affine[i][j] *= hdr.pixDims[i + 1];
|
|
1351
|
+
hdr.affine[i][j] *= hdr.pixDims[j + 1];
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
console.log("mif affine:" + hdr.affine[0]);
|
|
1355
|
+
if (TR > 0) hdr.pixDims[4] = TR;
|
|
1356
|
+
if (isDetached && !pairedImgData)
|
|
1357
|
+
console.log("MIH header provided without paired image data");
|
|
1358
|
+
let rawImg = [];
|
|
1359
|
+
if (isDetached) rawImg = pairedImgData.slice();
|
|
1360
|
+
//n.b. mrconvert can pad files? See dtitest_Siemens_SC 4_dti_nopf_x2_pitch
|
|
1361
|
+
else
|
|
1362
|
+
rawImg = buffer.slice(
|
|
1363
|
+
hdr.vox_offset,
|
|
1364
|
+
hdr.vox_offset + nvox * (hdr.numBitsPerVoxel / 8)
|
|
1365
|
+
);
|
|
1366
|
+
if (layout.length != hdr.dims[0]) console.log("dims does not match layout");
|
|
1367
|
+
//estimate strides:
|
|
1368
|
+
let stride = 1;
|
|
1369
|
+
let instride = [1, 1, 1, 1, 1];
|
|
1370
|
+
let inflip = [false, false, false, false, false];
|
|
1371
|
+
for (let i = 0; i < layout.length; i++) {
|
|
1372
|
+
for (let j = 0; j < layout.length; j++) {
|
|
1373
|
+
let a = Math.abs(layout[j]);
|
|
1374
|
+
if (a != i) continue;
|
|
1375
|
+
instride[j] = stride;
|
|
1376
|
+
//detect -0: https://medium.com/coding-at-dawn/is-negative-zero-0-a-number-in-javascript-c62739f80114
|
|
1377
|
+
if (layout[j] < 0 || Object.is(layout[j], -0)) inflip[j] = true;
|
|
1378
|
+
stride *= hdr.dims[j + 1];
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
//lookup table for flips and stride offsets:
|
|
1382
|
+
const range = (start, stop, step) =>
|
|
1383
|
+
Array.from(
|
|
1384
|
+
{ length: (stop - start) / step + 1 },
|
|
1385
|
+
(_, i) => start + i * step
|
|
1386
|
+
);
|
|
1387
|
+
let xlut = range(0, hdr.dims[1] - 1, 1);
|
|
1388
|
+
if (inflip[0]) xlut = range(hdr.dims[1] - 1, 0, -1);
|
|
1389
|
+
for (let i = 0; i < hdr.dims[1]; i++) xlut[i] *= instride[0];
|
|
1390
|
+
let ylut = range(0, hdr.dims[2] - 1, 1);
|
|
1391
|
+
if (inflip[1]) ylut = range(hdr.dims[2] - 1, 0, -1);
|
|
1392
|
+
for (let i = 0; i < hdr.dims[2]; i++) ylut[i] *= instride[1];
|
|
1393
|
+
let zlut = range(0, hdr.dims[3] - 1, 1);
|
|
1394
|
+
if (inflip[2]) zlut = range(hdr.dims[3] - 1, 0, -1);
|
|
1395
|
+
for (let i = 0; i < hdr.dims[3]; i++) zlut[i] *= instride[2];
|
|
1396
|
+
let tlut = range(0, hdr.dims[4] - 1, 1);
|
|
1397
|
+
if (inflip[3]) tlut = range(hdr.dims[4] - 1, 0, -1);
|
|
1398
|
+
for (let i = 0; i < hdr.dims[4]; i++) tlut[i] *= instride[3];
|
|
1399
|
+
let dlut = range(0, hdr.dims[5] - 1, 1);
|
|
1400
|
+
if (inflip[4]) dlut = range(hdr.dims[5] - 1, 0, -1);
|
|
1401
|
+
for (let i = 0; i < hdr.dims[5]; i++) dlut[i] *= instride[4];
|
|
1402
|
+
//input and output arrays
|
|
1403
|
+
let j = 0;
|
|
1404
|
+
let inVs = [];
|
|
1405
|
+
let outVs = [];
|
|
1406
|
+
if (hdr.numBitsPerVoxel === 8) {
|
|
1407
|
+
inVs = new Uint8Array(rawImg);
|
|
1408
|
+
outVs = new Uint8Array(nvox);
|
|
1409
|
+
} //8bit
|
|
1410
|
+
if (hdr.numBitsPerVoxel === 16) {
|
|
1411
|
+
inVs = new Uint16Array(rawImg);
|
|
1412
|
+
outVs = new Uint16Array(nvox);
|
|
1413
|
+
}
|
|
1414
|
+
if (hdr.numBitsPerVoxel === 32) {
|
|
1415
|
+
inVs = new Uint32Array(rawImg);
|
|
1416
|
+
outVs = new Uint32Array(nvox);
|
|
1417
|
+
} //32bit
|
|
1418
|
+
if (hdr.numBitsPerVoxel === 64) {
|
|
1419
|
+
inVs = new BigUint64Array(rawImg);
|
|
1420
|
+
outVs = new BigUint64Array(nvox);
|
|
1421
|
+
} //64bit
|
|
1422
|
+
for (let d = 0; d < hdr.dims[5]; d++) {
|
|
1423
|
+
for (let t = 0; t < hdr.dims[4]; t++) {
|
|
1424
|
+
for (let z = 0; z < hdr.dims[3]; z++) {
|
|
1425
|
+
for (let y = 0; y < hdr.dims[2]; y++) {
|
|
1426
|
+
for (let x = 0; x < hdr.dims[1]; x++) {
|
|
1427
|
+
outVs[j] = inVs[xlut[x] + ylut[y] + zlut[z] + tlut[t] + dlut[d]];
|
|
1428
|
+
j++;
|
|
1429
|
+
} //for x
|
|
1430
|
+
} //for y
|
|
1431
|
+
} //for z
|
|
1432
|
+
} //for t (time)
|
|
1433
|
+
} // for d (direction, phase/real, etc)
|
|
1434
|
+
return outVs;
|
|
1435
|
+
}; // readMIF()
|
|
1436
|
+
|
|
1437
|
+
NVImage.prototype.readNRRD = function (dataBuffer, pairedImgData) {
|
|
1438
|
+
//inspired by parserNRRD.js in https://github.com/xtk
|
|
1439
|
+
//Copyright (c) 2012 The X Toolkit Developers <dev@goXTK.com>
|
|
1440
|
+
// http://www.opensource.org/licenses/mit-license.php
|
|
1441
|
+
this.hdr = new nifti.NIFTI1();
|
|
1442
|
+
let hdr = this.hdr;
|
|
1443
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
1444
|
+
let len = dataBuffer.byteLength;
|
|
1445
|
+
//extract initial text header
|
|
1446
|
+
var txt = null;
|
|
1447
|
+
var bytes = new Uint8Array(dataBuffer);
|
|
1448
|
+
for (var i = 1; i < len; i++) {
|
|
1449
|
+
if (bytes[i - 1] == 10 && bytes[i] == 10) {
|
|
1450
|
+
let v = dataBuffer.slice(0, i - 1);
|
|
1451
|
+
txt = new TextDecoder().decode(v);
|
|
1452
|
+
hdr.vox_offset = i + 1;
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
var lines = txt.split("\n");
|
|
1457
|
+
if (!lines[0].startsWith("NRRD")) alert("Invalid NRRD image");
|
|
1458
|
+
var n = lines.length;
|
|
1459
|
+
let isGz = false;
|
|
1460
|
+
let isMicron = false;
|
|
1461
|
+
let isDetached = false;
|
|
1462
|
+
let mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
|
|
1463
|
+
let offset = vec3.fromValues(0, 0, 0);
|
|
1464
|
+
let rot33 = mat3.create();
|
|
1465
|
+
for (let i = 1; i < n; i++) {
|
|
1466
|
+
let str = lines[i];
|
|
1467
|
+
if (str[0] === "#") continue; //comment
|
|
1468
|
+
str = str.toLowerCase();
|
|
1469
|
+
let items = str.split(":");
|
|
1470
|
+
if (items.length < 2) continue;
|
|
1471
|
+
let key = items[0].trim();
|
|
1472
|
+
let value = items[1].trim();
|
|
1473
|
+
value = value.replaceAll(")", " ");
|
|
1474
|
+
value = value.replaceAll("(", " ");
|
|
1475
|
+
value = value.trim();
|
|
1476
|
+
switch (key) {
|
|
1477
|
+
case "data file":
|
|
1478
|
+
isDetached = true;
|
|
1479
|
+
break;
|
|
1480
|
+
case "encoding":
|
|
1481
|
+
if (value.includes("raw")) isGz = false;
|
|
1482
|
+
else if (value.includes("gz")) isGz = true;
|
|
1483
|
+
else alert("Unsupported NRRD encoding");
|
|
1484
|
+
break;
|
|
1485
|
+
case "type":
|
|
1486
|
+
switch (value) {
|
|
1487
|
+
case "uchar":
|
|
1488
|
+
case "unsigned char":
|
|
1489
|
+
case "uint8":
|
|
1490
|
+
case "uint8_t":
|
|
1491
|
+
hdr.numBitsPerVoxel = 8;
|
|
1492
|
+
hdr.datatypeCode = this.DT_UNSIGNED_CHAR;
|
|
1493
|
+
break;
|
|
1494
|
+
case "signed char":
|
|
1495
|
+
case "int8":
|
|
1496
|
+
case "int8_t":
|
|
1497
|
+
hdr.numBitsPerVoxel = 8;
|
|
1498
|
+
hdr.datatypeCode = this.DT_INT8;
|
|
1499
|
+
break;
|
|
1500
|
+
case "short":
|
|
1501
|
+
case "short int":
|
|
1502
|
+
case "signed short":
|
|
1503
|
+
case "signed short int":
|
|
1504
|
+
case "int16":
|
|
1505
|
+
case "int16_t":
|
|
1506
|
+
hdr.numBitsPerVoxel = 16;
|
|
1507
|
+
hdr.datatypeCode = this.DT_SIGNED_SHORT;
|
|
1508
|
+
break;
|
|
1509
|
+
case "ushort":
|
|
1510
|
+
case "unsigned short":
|
|
1511
|
+
case "unsigned short int":
|
|
1512
|
+
case "uint16":
|
|
1513
|
+
case "uint16_t":
|
|
1514
|
+
hdr.numBitsPerVoxel = 16;
|
|
1515
|
+
hdr.datatypeCode = this.DT_UINT16;
|
|
1516
|
+
break;
|
|
1517
|
+
case "int":
|
|
1518
|
+
case "signed int":
|
|
1519
|
+
case "int32":
|
|
1520
|
+
case "int32_t":
|
|
1521
|
+
hdr.numBitsPerVoxel = 32;
|
|
1522
|
+
hdr.datatypeCode = this.DT_SIGNED_INT;
|
|
1523
|
+
break;
|
|
1524
|
+
case "uint":
|
|
1525
|
+
case "unsigned int":
|
|
1526
|
+
case "uint32":
|
|
1527
|
+
case "uint32_t":
|
|
1528
|
+
hdr.numBitsPerVoxel = 32;
|
|
1529
|
+
hdr.datatypeCode = this.DT_UINT32;
|
|
1530
|
+
break;
|
|
1531
|
+
case "float":
|
|
1532
|
+
hdr.numBitsPerVoxel = 32;
|
|
1533
|
+
hdr.datatypeCode = this.DT_FLOAT;
|
|
1534
|
+
break;
|
|
1535
|
+
case "double":
|
|
1536
|
+
hdr.numBitsPerVoxel = 64;
|
|
1537
|
+
hdr.datatypeCode = this.DT_DOUBLE;
|
|
1538
|
+
break;
|
|
1539
|
+
default:
|
|
1540
|
+
throw new Error("Unsupported NRRD data type: " + value);
|
|
1541
|
+
}
|
|
1542
|
+
break;
|
|
1543
|
+
case "spacings":
|
|
1544
|
+
let pixdims = value.split(/[ ,]+/);
|
|
1545
|
+
for (var d = 0; d < pixdims.length; d++)
|
|
1546
|
+
hdr.pixDims[d + 1] = parseFloat(dims[d]);
|
|
1547
|
+
case "sizes":
|
|
1548
|
+
let dims = value.split(/[ ,]+/);
|
|
1549
|
+
hdr.dims[0] = dims.length;
|
|
1550
|
+
for (let d = 0; d < dims.length; d++)
|
|
1551
|
+
hdr.dims[d + 1] = parseInt(dims[d]);
|
|
1552
|
+
break;
|
|
1553
|
+
case "endian":
|
|
1554
|
+
if (value.includes("little")) hdr.littleEndian = true;
|
|
1555
|
+
else if (value.includes("big")) hdr.littleEndian = false;
|
|
1556
|
+
break;
|
|
1557
|
+
case "space directions":
|
|
1558
|
+
let vs = value.split(/[ ,]+/);
|
|
1559
|
+
if (vs.length !== 9) break;
|
|
1560
|
+
for (var d = 0; d < 9; d++) mat33[d] = parseFloat(vs[d]);
|
|
1561
|
+
break;
|
|
1562
|
+
case "space origin":
|
|
1563
|
+
let ts = value.split(/[ ,]+/);
|
|
1564
|
+
if (ts.length !== 3) break;
|
|
1565
|
+
offset[0] = parseFloat(ts[0]);
|
|
1566
|
+
offset[1] = parseFloat(ts[1]);
|
|
1567
|
+
offset[2] = parseFloat(ts[2]);
|
|
1568
|
+
break;
|
|
1569
|
+
case "space units":
|
|
1570
|
+
if (value.includes("microns")) isMicron = true;
|
|
1571
|
+
break;
|
|
1572
|
+
case "space":
|
|
1573
|
+
if (value.includes("right-anterior-superior") || value.includes("RAS"))
|
|
1574
|
+
rot33 = mat3.fromValues(
|
|
1575
|
+
1,
|
|
1576
|
+
0,
|
|
1577
|
+
0,
|
|
1578
|
+
|
|
1579
|
+
0,
|
|
1580
|
+
1,
|
|
1581
|
+
0,
|
|
1582
|
+
|
|
1583
|
+
0,
|
|
1584
|
+
0,
|
|
1585
|
+
1
|
|
1586
|
+
);
|
|
1587
|
+
else if (
|
|
1588
|
+
value.includes("left-anterior-superior") ||
|
|
1589
|
+
value.includes("LAS")
|
|
1590
|
+
)
|
|
1591
|
+
rot33 = mat3.fromValues(
|
|
1592
|
+
-1,
|
|
1593
|
+
0,
|
|
1594
|
+
0,
|
|
1595
|
+
|
|
1596
|
+
0,
|
|
1597
|
+
1,
|
|
1598
|
+
0,
|
|
1599
|
+
|
|
1600
|
+
0,
|
|
1601
|
+
0,
|
|
1602
|
+
1
|
|
1603
|
+
);
|
|
1604
|
+
else if (
|
|
1605
|
+
value.includes("left-posterior-superior") ||
|
|
1606
|
+
value.includes("LPS")
|
|
1607
|
+
)
|
|
1608
|
+
rot33 = mat3.fromValues(
|
|
1609
|
+
-1,
|
|
1610
|
+
0,
|
|
1611
|
+
0,
|
|
1612
|
+
|
|
1613
|
+
0,
|
|
1614
|
+
-1,
|
|
1615
|
+
0,
|
|
1616
|
+
|
|
1617
|
+
0,
|
|
1618
|
+
0,
|
|
1619
|
+
1
|
|
1620
|
+
);
|
|
1621
|
+
else console.log("Unsupported NRRD space value:", value);
|
|
1622
|
+
break;
|
|
1623
|
+
default:
|
|
1624
|
+
//console.log('Unknown:',key);
|
|
1625
|
+
} //read line
|
|
1626
|
+
} //read all lines
|
|
1627
|
+
if (!isNaN(mat33[0])) {
|
|
1628
|
+
//if spatial transform provided
|
|
1629
|
+
this.hdr.sform_code = 2;
|
|
1630
|
+
if (isMicron) {
|
|
1631
|
+
//convert micron to mm
|
|
1632
|
+
mat4.multiplyScalar(mat33, mat33, 0.001);
|
|
1633
|
+
offset[0] *= 0.001;
|
|
1634
|
+
offset[1] *= 0.001;
|
|
1635
|
+
offset[2] *= 0.001;
|
|
1636
|
+
}
|
|
1637
|
+
if (rot33[0] < 0) offset[0] = -offset[0]; //origin L<->R
|
|
1638
|
+
if (rot33[4] < 0) offset[1] = -offset[1]; //origin A<->P
|
|
1639
|
+
if (rot33[8] < 0) offset[2] = -offset[2]; //origin S<->I
|
|
1640
|
+
mat3.multiply(mat33, rot33, mat33);
|
|
1641
|
+
let mat = mat4.fromValues(
|
|
1642
|
+
mat33[0],
|
|
1643
|
+
mat33[3],
|
|
1644
|
+
mat33[6],
|
|
1645
|
+
offset[0],
|
|
1646
|
+
mat33[1],
|
|
1647
|
+
mat33[4],
|
|
1648
|
+
mat33[7],
|
|
1649
|
+
offset[1],
|
|
1650
|
+
mat33[2],
|
|
1651
|
+
mat33[5],
|
|
1652
|
+
mat33[8],
|
|
1653
|
+
offset[2],
|
|
1654
|
+
0,
|
|
1655
|
+
0,
|
|
1656
|
+
0,
|
|
1657
|
+
1
|
|
1658
|
+
);
|
|
1659
|
+
let mm000 = this.vox2mm([0, 0, 0], mat);
|
|
1660
|
+
let mm100 = this.vox2mm([1, 0, 0], mat);
|
|
1661
|
+
vec3.subtract(mm100, mm100, mm000);
|
|
1662
|
+
let mm010 = this.vox2mm([0, 1, 0], mat);
|
|
1663
|
+
vec3.subtract(mm010, mm010, mm000);
|
|
1664
|
+
let mm001 = this.vox2mm([0, 0, 1], mat);
|
|
1665
|
+
vec3.subtract(mm001, mm001, mm000);
|
|
1666
|
+
hdr.pixDims[1] = vec3.length(mm100);
|
|
1667
|
+
hdr.pixDims[2] = vec3.length(mm010);
|
|
1668
|
+
hdr.pixDims[3] = vec3.length(mm001);
|
|
1669
|
+
hdr.affine = [
|
|
1670
|
+
[mat[0], mat[1], mat[2], mat[3]],
|
|
1671
|
+
[mat[4], mat[5], mat[6], mat[7]],
|
|
1672
|
+
[mat[8], mat[9], mat[10], mat[11]],
|
|
1673
|
+
[0, 0, 0, 1],
|
|
1674
|
+
];
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
let nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
1678
|
+
if (isDetached && pairedImgData) {
|
|
1679
|
+
//??? .gz files automatically decompressed?
|
|
1680
|
+
return pairedImgData.slice();
|
|
1681
|
+
}
|
|
1682
|
+
if (isDetached)
|
|
1683
|
+
console.log(
|
|
1684
|
+
"Missing data: NRRD header describes detached data file but only one URL provided"
|
|
1685
|
+
);
|
|
1686
|
+
if (isGz)
|
|
1687
|
+
return fflate.decompressSync(
|
|
1688
|
+
new Uint8Array(dataBuffer.slice(hdr.vox_offset))
|
|
1689
|
+
).buffer;
|
|
1690
|
+
else return dataBuffer.slice(hdr.vox_offset);
|
|
1691
|
+
}; //readNRRD()
|
|
1692
|
+
|
|
278
1693
|
// not included in public docs
|
|
279
1694
|
NVImage.prototype.calculateRAS = function () {
|
|
280
1695
|
//Transform to orient NIfTI image to Left->Right,Posterior->Anterior,Inferior->Superior (48 possible permutations)
|
|
@@ -282,7 +1697,7 @@ NVImage.prototype.calculateRAS = function () {
|
|
|
282
1697
|
// not elegant, as JavaScript arrays are always 1D
|
|
283
1698
|
let a = this.hdr.affine;
|
|
284
1699
|
let header = this.hdr;
|
|
285
|
-
let absR =
|
|
1700
|
+
let absR = mat3.fromValues(
|
|
286
1701
|
Math.abs(a[0][0]),
|
|
287
1702
|
Math.abs(a[0][1]),
|
|
288
1703
|
Math.abs(a[0][2]),
|
|
@@ -330,7 +1745,7 @@ NVImage.prototype.calculateRAS = function () {
|
|
|
330
1745
|
perm[ixyz[0] - 1] = 1;
|
|
331
1746
|
perm[ixyz[1] - 1] = 2;
|
|
332
1747
|
perm[ixyz[2] - 1] = 3;
|
|
333
|
-
let rotM =
|
|
1748
|
+
let rotM = mat4.fromValues(
|
|
334
1749
|
a[0][0],
|
|
335
1750
|
a[0][1],
|
|
336
1751
|
a[0][2],
|
|
@@ -353,23 +1768,17 @@ NVImage.prototype.calculateRAS = function () {
|
|
|
353
1768
|
this.mm100 = this.vox2mm([header.dims[1] - 0.5, -0.5, -0.5], rotM);
|
|
354
1769
|
this.mm010 = this.vox2mm([-0.5, header.dims[2] - 0.5, -0.5], rotM);
|
|
355
1770
|
this.mm001 = this.vox2mm([-0.5, -0.5, header.dims[3] - 0.5], rotM);
|
|
356
|
-
let R =
|
|
357
|
-
|
|
1771
|
+
let R = mat4.create();
|
|
1772
|
+
mat4.copy(R, rotM);
|
|
358
1773
|
for (let i = 0; i < 3; i++) {
|
|
359
1774
|
for (let j = 0; j < 3; j++) {
|
|
360
1775
|
R[i * 4 + j] = rotM[i * 4 + perm[j] - 1]; //rotM[i+(4*(perm[j]-1))];//rotM[i],[perm[j]-1];
|
|
361
1776
|
}
|
|
362
1777
|
}
|
|
363
1778
|
let flip = [0, 0, 0];
|
|
364
|
-
if (R[0] < 0)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (R[5] < 0) {
|
|
368
|
-
flip[1] = 1; //R[1][1]
|
|
369
|
-
}
|
|
370
|
-
if (R[10] < 0) {
|
|
371
|
-
flip[2] = 1; //R[2][2]
|
|
372
|
-
}
|
|
1779
|
+
if (R[0] < 0) flip[0] = 1; //R[0][0]
|
|
1780
|
+
if (R[5] < 0) flip[1] = 1; //R[1][1]
|
|
1781
|
+
if (R[10] < 0) flip[2] = 1; //R[2][2]
|
|
373
1782
|
this.dimsRAS = [
|
|
374
1783
|
header.dims[0],
|
|
375
1784
|
header.dims[perm[0]],
|
|
@@ -382,31 +1791,34 @@ NVImage.prototype.calculateRAS = function () {
|
|
|
382
1791
|
header.pixDims[perm[1]],
|
|
383
1792
|
header.pixDims[perm[2]],
|
|
384
1793
|
];
|
|
1794
|
+
this.permRAS = perm.slice();
|
|
1795
|
+
for (let i = 0; i < 3; i++)
|
|
1796
|
+
if (flip[i] === 1) this.permRAS[i] = -this.permRAS[i];
|
|
385
1797
|
if (this.arrayEquals(perm, [1, 2, 3]) && this.arrayEquals(flip, [0, 0, 0])) {
|
|
386
|
-
this.toRAS =
|
|
387
|
-
this.matRAS =
|
|
1798
|
+
this.toRAS = mat4.create(); //aka fromValues(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);
|
|
1799
|
+
this.matRAS = mat4.clone(rotM);
|
|
388
1800
|
this.calculateOblique();
|
|
389
1801
|
return; //no rotation required!
|
|
390
1802
|
}
|
|
391
|
-
|
|
1803
|
+
mat4.identity(rotM);
|
|
392
1804
|
rotM[0 + 0 * 4] = 1 - flip[0] * 2;
|
|
393
1805
|
rotM[1 + 1 * 4] = 1 - flip[1] * 2;
|
|
394
1806
|
rotM[2 + 2 * 4] = 1 - flip[2] * 2;
|
|
395
1807
|
rotM[3 + 0 * 4] = (header.dims[perm[0]] - 1) * flip[0];
|
|
396
1808
|
rotM[3 + 1 * 4] = (header.dims[perm[1]] - 1) * flip[1];
|
|
397
1809
|
rotM[3 + 2 * 4] = (header.dims[perm[2]] - 1) * flip[2];
|
|
398
|
-
let residualR =
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
this.matRAS =
|
|
402
|
-
rotM =
|
|
1810
|
+
let residualR = mat4.create();
|
|
1811
|
+
mat4.invert(residualR, rotM);
|
|
1812
|
+
mat4.multiply(residualR, residualR, R);
|
|
1813
|
+
this.matRAS = mat4.clone(residualR);
|
|
1814
|
+
rotM = mat4.fromValues(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
|
|
403
1815
|
rotM[perm[0] - 1 + 0 * 4] = -flip[0] * 2 + 1;
|
|
404
1816
|
rotM[perm[1] - 1 + 1 * 4] = -flip[1] * 2 + 1;
|
|
405
1817
|
rotM[perm[2] - 1 + 2 * 4] = -flip[2] * 2 + 1;
|
|
406
1818
|
rotM[3 + 0 * 4] = flip[0];
|
|
407
1819
|
rotM[3 + 1 * 4] = flip[1];
|
|
408
1820
|
rotM[3 + 2 * 4] = flip[2];
|
|
409
|
-
this.toRAS =
|
|
1821
|
+
this.toRAS = mat4.clone(rotM);
|
|
410
1822
|
log.debug(this.hdr.dims);
|
|
411
1823
|
log.debug(this.dimsRAS);
|
|
412
1824
|
this.calculateOblique();
|
|
@@ -414,22 +1826,22 @@ NVImage.prototype.calculateRAS = function () {
|
|
|
414
1826
|
|
|
415
1827
|
// not included in public docs
|
|
416
1828
|
NVImage.prototype.vox2mm = function (XYZ, mtx) {
|
|
417
|
-
let sform =
|
|
418
|
-
|
|
419
|
-
let pos =
|
|
420
|
-
|
|
421
|
-
let pos3 =
|
|
1829
|
+
let sform = mat4.clone(mtx);
|
|
1830
|
+
mat4.transpose(sform, sform);
|
|
1831
|
+
let pos = vec4.fromValues(XYZ[0], XYZ[1], XYZ[2], 1);
|
|
1832
|
+
vec4.transformMat4(pos, pos, sform);
|
|
1833
|
+
let pos3 = vec3.fromValues(pos[0], pos[1], pos[2]);
|
|
422
1834
|
return pos3;
|
|
423
1835
|
}; // vox2mm()
|
|
424
1836
|
|
|
425
1837
|
NVImage.prototype.mm2vox = function (mm) {
|
|
426
|
-
let sform =
|
|
427
|
-
let out =
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
let pos =
|
|
431
|
-
|
|
432
|
-
let pos3 =
|
|
1838
|
+
let sform = mat4.fromValues(...this.hdr.affine.flat());
|
|
1839
|
+
let out = mat4.clone(sform);
|
|
1840
|
+
mat4.transpose(out, sform);
|
|
1841
|
+
mat4.invert(out, out);
|
|
1842
|
+
let pos = vec4.fromValues(mm[0], mm[1], mm[2], 1);
|
|
1843
|
+
vec4.transformMat4(pos, pos, out);
|
|
1844
|
+
let pos3 = vec3.fromValues(pos[0], pos[1], pos[2]);
|
|
433
1845
|
return [Math.round(pos3[0]), Math.round(pos3[1]), Math.round(pos3[2])];
|
|
434
1846
|
}; // vox2mm()
|
|
435
1847
|
|
|
@@ -459,12 +1871,32 @@ NVImage.prototype.colorMaps = function (sort = true) {
|
|
|
459
1871
|
return sort === true ? cm.sort() : cm;
|
|
460
1872
|
};
|
|
461
1873
|
|
|
1874
|
+
NVImage.prototype.setColorMap = function (cm) {
|
|
1875
|
+
let allColorMaps = this.colorMaps();
|
|
1876
|
+
if (allColorMaps.indexOf(cm.toLowerCase()) !== -1) {
|
|
1877
|
+
this.colorMap = cm.toLowerCase();
|
|
1878
|
+
this.calMinMax();
|
|
1879
|
+
} else {
|
|
1880
|
+
log.warn(`color map ${cm} is not a valid color map`);
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
|
|
462
1884
|
// not included in public docs
|
|
463
1885
|
// given an overlayItem and its img TypedArray, calculate 2% and 98% display range if needed
|
|
464
1886
|
//clone FSL robust_range estimates https://github.com/rordenlab/niimath/blob/331758459140db59290a794350d0ff3ad4c37b67/src/core32.c#L1215
|
|
465
1887
|
//ToDo: convert to web assembly, this is slow in JavaScript
|
|
466
1888
|
NVImage.prototype.calMinMax = function () {
|
|
1889
|
+
let cm = this.colorMap;
|
|
1890
|
+
let allColorMaps = this.colorMaps();
|
|
1891
|
+
let cmMin = 0;
|
|
1892
|
+
let cmMax = 0;
|
|
1893
|
+
if (allColorMaps.indexOf(cm.toLowerCase()) !== -1) {
|
|
1894
|
+
cmMin = cmaps[cm.toLowerCase()].min;
|
|
1895
|
+
cmMax = cmaps[cm.toLowerCase()].max;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
467
1898
|
if (
|
|
1899
|
+
cmMin === cmMax &&
|
|
468
1900
|
this.trustCalMinMax &&
|
|
469
1901
|
isFinite(this.hdr.cal_min) &&
|
|
470
1902
|
isFinite(this.hdr.cal_max) &&
|
|
@@ -483,16 +1915,6 @@ NVImage.prototype.calMinMax = function () {
|
|
|
483
1915
|
this.hdr.cal_max,
|
|
484
1916
|
];
|
|
485
1917
|
}
|
|
486
|
-
|
|
487
|
-
let cm = this.colorMap;
|
|
488
|
-
let allColorMaps = this.colorMaps();
|
|
489
|
-
let cmMin = 0;
|
|
490
|
-
let cmMax = 0;
|
|
491
|
-
if (allColorMaps.indexOf(cm.toLowerCase()) != -1) {
|
|
492
|
-
cmMin = cmaps[cm.toLowerCase()].min;
|
|
493
|
-
cmMax = cmaps[cm.toLowerCase()].max;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
1918
|
// if color map specifies non zero values for min and max then use them
|
|
497
1919
|
if (cmMin != cmMax) {
|
|
498
1920
|
this.cal_min = cmMin;
|
|
@@ -501,7 +1923,6 @@ NVImage.prototype.calMinMax = function () {
|
|
|
501
1923
|
this.robust_max = this.cal_max;
|
|
502
1924
|
return [cmMin, cmMax, cmMin, cmMax];
|
|
503
1925
|
}
|
|
504
|
-
|
|
505
1926
|
//determine full range: min..max
|
|
506
1927
|
let mn = this.img[0];
|
|
507
1928
|
let mx = this.img[0];
|
|
@@ -587,20 +2008,11 @@ NVImage.prototype.calMinMax = function () {
|
|
|
587
2008
|
} //if lo == hi
|
|
588
2009
|
var pct2 = this.intensityRaw2Scaled(this.hdr, lo / scl + mn);
|
|
589
2010
|
var pct98 = this.intensityRaw2Scaled(this.hdr, hi / scl + mn);
|
|
590
|
-
// console.log(
|
|
591
|
-
// "full range %f..%f (voxels 0 or NaN = %i) robust range %f..%f",
|
|
592
|
-
// mnScale,
|
|
593
|
-
// mxScale,
|
|
594
|
-
// nZero,
|
|
595
|
-
// pct2,
|
|
596
|
-
// pct98
|
|
597
|
-
// );
|
|
598
2011
|
if (
|
|
599
2012
|
this.hdr.cal_min < this.hdr.cal_max &&
|
|
600
2013
|
this.hdr.cal_min >= mnScale &&
|
|
601
2014
|
this.hdr.cal_max <= mxScale
|
|
602
2015
|
) {
|
|
603
|
-
// console.log("ignoring robust range: using header cal_min and cal_max");
|
|
604
2016
|
pct2 = this.hdr.cal_min;
|
|
605
2017
|
pct98 = this.hdr.cal_max;
|
|
606
2018
|
}
|
|
@@ -619,8 +2031,154 @@ NVImage.prototype.intensityRaw2Scaled = function (hdr, raw) {
|
|
|
619
2031
|
return raw * hdr.scl_slope + hdr.scl_inter;
|
|
620
2032
|
};
|
|
621
2033
|
|
|
2034
|
+
function str2Buffer(str) {
|
|
2035
|
+
//emulate node.js Buffer.from
|
|
2036
|
+
var bytes = [];
|
|
2037
|
+
for (var i = 0; i < str.length; i++) {
|
|
2038
|
+
var char = str.charCodeAt(i);
|
|
2039
|
+
bytes.push(char & 0xff);
|
|
2040
|
+
}
|
|
2041
|
+
return bytes;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function hdrToArrayBuffer(hdr, isDrawing8 = false) {
|
|
2045
|
+
const SHORT_SIZE = 2;
|
|
2046
|
+
const FLOAT32_SIZE = 4;
|
|
2047
|
+
|
|
2048
|
+
let byteArray = new Uint8Array(348);
|
|
2049
|
+
let view = new DataView(byteArray.buffer);
|
|
2050
|
+
// sizeof_hdr
|
|
2051
|
+
view.setInt32(0, 348, hdr.littleEndian);
|
|
2052
|
+
|
|
2053
|
+
// data_type, db_name, extents, session_error, regular are not used
|
|
2054
|
+
|
|
2055
|
+
// dim_info
|
|
2056
|
+
view.setUint8(39, hdr.dim_info);
|
|
2057
|
+
|
|
2058
|
+
// dims
|
|
2059
|
+
for (let i = 0; i < 8; i++) {
|
|
2060
|
+
view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], hdr.littleEndian);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// intent_p1, intent_p2, intent_p3
|
|
2064
|
+
view.setFloat32(56, hdr.intent_p1, hdr.littleEndian);
|
|
2065
|
+
view.setFloat32(60, hdr.intent_p2, hdr.littleEndian);
|
|
2066
|
+
view.setFloat32(64, hdr.intent_p3, hdr.littleEndian);
|
|
2067
|
+
|
|
2068
|
+
// intent_code, datatype, bitpix, slice_start
|
|
2069
|
+
view.setInt16(68, hdr.intent_code, hdr.littleEndian);
|
|
2070
|
+
if (isDrawing8) {
|
|
2071
|
+
view.setInt16(70, 2, hdr.littleEndian); //2 = DT_UNSIGNED_CHAR
|
|
2072
|
+
view.setInt16(72, 8, hdr.littleEndian);
|
|
2073
|
+
} else {
|
|
2074
|
+
view.setInt16(70, hdr.datatypeCode, hdr.littleEndian);
|
|
2075
|
+
view.setInt16(72, hdr.numBitsPerVoxel, hdr.littleEndian);
|
|
2076
|
+
}
|
|
2077
|
+
view.setInt16(74, hdr.slice_start, hdr.littleEndian);
|
|
2078
|
+
|
|
2079
|
+
// pixdim[8], vox_offset, scl_slope, scl_inter
|
|
2080
|
+
for (let i = 0; i < 8; i++) {
|
|
2081
|
+
view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], hdr.littleEndian);
|
|
2082
|
+
}
|
|
2083
|
+
if (isDrawing8) {
|
|
2084
|
+
view.setFloat32(108, 352, hdr.littleEndian);
|
|
2085
|
+
view.setFloat32(112, 1.0, hdr.littleEndian);
|
|
2086
|
+
view.setFloat32(116, 0.0, hdr.littleEndian);
|
|
2087
|
+
} else {
|
|
2088
|
+
view.setFloat32(108, hdr.vox_offset, hdr.littleEndian);
|
|
2089
|
+
view.setFloat32(112, hdr.scl_slope, hdr.littleEndian);
|
|
2090
|
+
view.setFloat32(116, hdr.scl_inter, hdr.littleEndian);
|
|
2091
|
+
}
|
|
2092
|
+
// slice_end
|
|
2093
|
+
view.setInt16(120, hdr.slice_end, hdr.littleEndian);
|
|
2094
|
+
|
|
2095
|
+
// slice_code, xyzt_units
|
|
2096
|
+
view.setUint8(122, hdr.slice_code);
|
|
2097
|
+
view.setUint8(123, hdr.xyzt_units);
|
|
2098
|
+
|
|
2099
|
+
// cal_max, cal_min, slice_duration, toffset
|
|
2100
|
+
if (isDrawing8) {
|
|
2101
|
+
view.setFloat32(124, 0, hdr.littleEndian);
|
|
2102
|
+
view.setFloat32(128, 0, hdr.littleEndian);
|
|
2103
|
+
} else {
|
|
2104
|
+
view.setFloat32(124, hdr.cal_max, hdr.littleEndian);
|
|
2105
|
+
view.setFloat32(128, hdr.cal_min, hdr.littleEndian);
|
|
2106
|
+
}
|
|
2107
|
+
view.setFloat32(132, hdr.slice_duration, hdr.littleEndian);
|
|
2108
|
+
view.setFloat32(136, hdr.toffset, hdr.littleEndian);
|
|
2109
|
+
|
|
2110
|
+
// glmax, glmin are unused
|
|
2111
|
+
|
|
2112
|
+
// descrip and aux_file
|
|
2113
|
+
//node.js byteArray.set(Buffer.from(hdr.description), 148);
|
|
2114
|
+
byteArray.set(str2Buffer(hdr.description), 148);
|
|
2115
|
+
//node.js: byteArray.set(Buffer.from(hdr.aux_file), 228);
|
|
2116
|
+
byteArray.set(str2Buffer(hdr.aux_file), 228);
|
|
2117
|
+
// qform_code, sform_code
|
|
2118
|
+
view.setInt16(252, hdr.qform_code, hdr.littleEndian);
|
|
2119
|
+
view.setInt16(254, hdr.sform_code, hdr.littleEndian);
|
|
2120
|
+
|
|
2121
|
+
// quatern_b, quatern_c, quatern_d, qoffset_x, qoffset_y, qoffset_z, srow_x[4], srow_y[4], and srow_z[4]
|
|
2122
|
+
view.setFloat32(256, hdr.quatern_b, hdr.littleEndian);
|
|
2123
|
+
view.setFloat32(260, hdr.quatern_c, hdr.littleEndian);
|
|
2124
|
+
view.setFloat32(264, hdr.quatern_d, hdr.littleEndian);
|
|
2125
|
+
view.setFloat32(268, hdr.qoffset_x, hdr.littleEndian);
|
|
2126
|
+
view.setFloat32(272, hdr.qoffset_y, hdr.littleEndian);
|
|
2127
|
+
view.setFloat32(276, hdr.qoffset_z, hdr.littleEndian);
|
|
2128
|
+
const flattened = hdr.affine.flat();
|
|
2129
|
+
// we only want the first three rows
|
|
2130
|
+
for (let i = 0; i < 12; i++) {
|
|
2131
|
+
view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], hdr.littleEndian);
|
|
2132
|
+
}
|
|
2133
|
+
//node.js https://www.w3schools.com/nodejs/met_buffer_from.asp
|
|
2134
|
+
// intent_name and magic
|
|
2135
|
+
//node.js byteArray.set(Buffer.from(hdr.intent_name), 328);
|
|
2136
|
+
byteArray.set(str2Buffer(hdr.intent_name), 328);
|
|
2137
|
+
//node.js byteArray.set(Buffer.from(hdr.magic), 344);
|
|
2138
|
+
byteArray.set(str2Buffer(hdr.magic), 344);
|
|
2139
|
+
return byteArray;
|
|
2140
|
+
//return byteArray.buffer;
|
|
2141
|
+
} // hdrToArrayBuffer()
|
|
2142
|
+
|
|
2143
|
+
NVImage.prototype.saveToDisk = async function (fnm, drawing8 = null) {
|
|
2144
|
+
let isDrawing8 = !(drawing8 == null);
|
|
2145
|
+
let hdrBytes = hdrToArrayBuffer(this.hdr, isDrawing8);
|
|
2146
|
+
let opad = new Uint8Array(4);
|
|
2147
|
+
let img8 = new Uint8Array(this.img.buffer);
|
|
2148
|
+
if (isDrawing8) img8 = new Uint8Array(drawing8.buffer);
|
|
2149
|
+
var odata = new Uint8Array(hdrBytes.length + opad.length + img8.length);
|
|
2150
|
+
odata.set(hdrBytes);
|
|
2151
|
+
odata.set(opad, hdrBytes.length);
|
|
2152
|
+
|
|
2153
|
+
odata.set(img8, hdrBytes.length + opad.length);
|
|
2154
|
+
let saveData = null;
|
|
2155
|
+
let compress = fnm.endsWith(".gz"); // true if name ends with .gz
|
|
2156
|
+
if (compress) {
|
|
2157
|
+
saveData = fflate.gzipSync(odata, {
|
|
2158
|
+
// GZIP-specific: the filename to use when decompressed
|
|
2159
|
+
filename: fnm,
|
|
2160
|
+
// GZIP-specific: the modification time. Can be a Date, date string,
|
|
2161
|
+
// or Unix timestamp
|
|
2162
|
+
mtime: Date.now(),
|
|
2163
|
+
level: 6, // the default
|
|
2164
|
+
});
|
|
2165
|
+
} else {
|
|
2166
|
+
saveData = odata;
|
|
2167
|
+
}
|
|
2168
|
+
let blob = new Blob([saveData.buffer], { type: "application/octet-stream" });
|
|
2169
|
+
let blobUrl = URL.createObjectURL(blob);
|
|
2170
|
+
const link = document.createElement("a");
|
|
2171
|
+
link.setAttribute("href", blobUrl);
|
|
2172
|
+
link.setAttribute("download", fnm);
|
|
2173
|
+
link.style.visibility = "hidden";
|
|
2174
|
+
document.body.appendChild(link);
|
|
2175
|
+
link.click();
|
|
2176
|
+
document.body.removeChild(link);
|
|
2177
|
+
}; // saveToDisk()
|
|
2178
|
+
|
|
622
2179
|
/**
|
|
623
2180
|
* factory function to load and return a new NVImage instance from a given URL
|
|
2181
|
+
* @constructs NVImage
|
|
624
2182
|
* @param {string} url the resolvable URL pointing to a nifti image to load
|
|
625
2183
|
* @param {string} [name=''] a name for this image. Default is an empty string
|
|
626
2184
|
* @param {string} [colorMap='gray'] a color map to use. default is gray
|
|
@@ -633,34 +2191,60 @@ NVImage.prototype.intensityRaw2Scaled = function (hdr, raw) {
|
|
|
633
2191
|
* @example
|
|
634
2192
|
* myImage = NVImage.loadFromUrl('./someURL/image.nii.gz') // must be served from a server (local or remote)
|
|
635
2193
|
*/
|
|
636
|
-
NVImage.loadFromUrl = async function (
|
|
637
|
-
url,
|
|
2194
|
+
NVImage.loadFromUrl = async function ({
|
|
2195
|
+
url = "",
|
|
2196
|
+
urlImgData = "",
|
|
638
2197
|
name = "",
|
|
639
2198
|
colorMap = "gray",
|
|
640
2199
|
opacity = 1.0,
|
|
2200
|
+
cal_min = NaN,
|
|
2201
|
+
cal_max = NaN,
|
|
641
2202
|
trustCalMinMax = true,
|
|
642
2203
|
percentileFrac = 0.02,
|
|
643
2204
|
ignoreZeroVoxels = false,
|
|
644
|
-
visible = true
|
|
645
|
-
) {
|
|
2205
|
+
visible = true,
|
|
2206
|
+
} = {}) {
|
|
2207
|
+
if (url === "") {
|
|
2208
|
+
throw Error("url must not be empty");
|
|
2209
|
+
}
|
|
646
2210
|
let response = await fetch(url);
|
|
647
|
-
|
|
648
2211
|
let nvimage = null;
|
|
649
|
-
|
|
650
2212
|
if (!response.ok) {
|
|
651
2213
|
throw Error(response.statusText);
|
|
652
2214
|
}
|
|
653
|
-
|
|
2215
|
+
var re = /(?:\.([^.]+))?$/;
|
|
2216
|
+
let ext = re.exec(url)[1];
|
|
2217
|
+
if (ext.toUpperCase() === "NHDR") {
|
|
2218
|
+
if (urlImgData === "") {
|
|
2219
|
+
}
|
|
2220
|
+
} else if (ext.toUpperCase() === "HEAD") {
|
|
2221
|
+
if (urlImgData === "") {
|
|
2222
|
+
urlImgData = url.substring(0, url.lastIndexOf("HEAD")) + "BRIK";
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
654
2225
|
let urlParts = url.split("/"); // split url parts at slash
|
|
655
2226
|
name = urlParts.slice(-1)[0]; // name will be last part of url (e.g. some/url/image.nii.gz --> image.nii.gz)
|
|
656
|
-
|
|
657
2227
|
let dataBuffer = await response.arrayBuffer();
|
|
2228
|
+
let pairedImgData = null;
|
|
2229
|
+
if (urlImgData.length > 0) {
|
|
2230
|
+
let resp = await fetch(urlImgData);
|
|
2231
|
+
if (resp.status === 404) {
|
|
2232
|
+
if (urlImgData.lastIndexOf("BRIK") !== -1) {
|
|
2233
|
+
resp = await fetch(urlImgData + ".gz");
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
pairedImgData = await resp.arrayBuffer();
|
|
2237
|
+
}
|
|
2238
|
+
|
|
658
2239
|
if (dataBuffer) {
|
|
659
2240
|
nvimage = new NVImage(
|
|
660
2241
|
dataBuffer,
|
|
661
2242
|
name,
|
|
662
2243
|
colorMap,
|
|
663
2244
|
opacity,
|
|
2245
|
+
pairedImgData,
|
|
2246
|
+
cal_min,
|
|
2247
|
+
cal_max,
|
|
664
2248
|
trustCalMinMax,
|
|
665
2249
|
percentileFrac,
|
|
666
2250
|
ignoreZeroVoxels,
|
|
@@ -680,7 +2264,11 @@ NVImage.readFileAsync = function (file) {
|
|
|
680
2264
|
let reader = new FileReader();
|
|
681
2265
|
|
|
682
2266
|
reader.onload = () => {
|
|
683
|
-
|
|
2267
|
+
if (file.name.lastIndexOf("gz") !== -1) {
|
|
2268
|
+
resolve(nifti.decompress(reader.result));
|
|
2269
|
+
} else {
|
|
2270
|
+
resolve(reader.result);
|
|
2271
|
+
}
|
|
684
2272
|
};
|
|
685
2273
|
|
|
686
2274
|
reader.onerror = reject;
|
|
@@ -691,6 +2279,7 @@ NVImage.readFileAsync = function (file) {
|
|
|
691
2279
|
|
|
692
2280
|
/**
|
|
693
2281
|
* factory function to load and return a new NVImage instance from a file in the browser
|
|
2282
|
+
* @constructs NVImage
|
|
694
2283
|
* @param {string} file the file object
|
|
695
2284
|
* @param {string} [name=''] a name for this image. Default is an empty string
|
|
696
2285
|
* @param {string} [colorMap='gray'] a color map to use. default is gray
|
|
@@ -703,24 +2292,90 @@ NVImage.readFileAsync = function (file) {
|
|
|
703
2292
|
* @example
|
|
704
2293
|
* myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop
|
|
705
2294
|
*/
|
|
706
|
-
NVImage.loadFromFile = async function (
|
|
707
|
-
file,
|
|
2295
|
+
NVImage.loadFromFile = async function ({
|
|
2296
|
+
file = null, // file can be an array of file objects or a single file object
|
|
708
2297
|
name = "",
|
|
709
2298
|
colorMap = "gray",
|
|
710
2299
|
opacity = 1.0,
|
|
2300
|
+
urlImgData = null,
|
|
2301
|
+
cal_min = NaN,
|
|
2302
|
+
cal_max = NaN,
|
|
711
2303
|
trustCalMinMax = true,
|
|
712
2304
|
percentileFrac = 0.02,
|
|
713
2305
|
ignoreZeroVoxels = false,
|
|
714
|
-
visible = true
|
|
715
|
-
|
|
2306
|
+
visible = true,
|
|
2307
|
+
isDICOMDIR = false,
|
|
2308
|
+
} = {}) {
|
|
2309
|
+
let nvimage = null;
|
|
2310
|
+
let dataBuffer = [];
|
|
2311
|
+
try {
|
|
2312
|
+
if (Array.isArray(file)) {
|
|
2313
|
+
for (let i = 0; i < file.length; i++) {
|
|
2314
|
+
dataBuffer.push(await this.readFileAsync(file[i]));
|
|
2315
|
+
}
|
|
2316
|
+
} else {
|
|
2317
|
+
dataBuffer = await this.readFileAsync(file);
|
|
2318
|
+
}
|
|
2319
|
+
let pairedImgData = null;
|
|
2320
|
+
if (urlImgData) {
|
|
2321
|
+
pairedImgData = await this.readFileAsync(urlImgData);
|
|
2322
|
+
}
|
|
2323
|
+
name = file.name;
|
|
2324
|
+
nvimage = new NVImage(
|
|
2325
|
+
dataBuffer,
|
|
2326
|
+
name,
|
|
2327
|
+
colorMap,
|
|
2328
|
+
opacity,
|
|
2329
|
+
pairedImgData,
|
|
2330
|
+
cal_min,
|
|
2331
|
+
cal_max,
|
|
2332
|
+
trustCalMinMax,
|
|
2333
|
+
percentileFrac,
|
|
2334
|
+
ignoreZeroVoxels,
|
|
2335
|
+
visible,
|
|
2336
|
+
isDICOMDIR
|
|
2337
|
+
);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
log.debug(err);
|
|
2340
|
+
}
|
|
2341
|
+
return nvimage;
|
|
2342
|
+
};
|
|
2343
|
+
|
|
2344
|
+
NVImage.loadFromBase64 = async function ({
|
|
2345
|
+
base64 = null,
|
|
2346
|
+
name = "",
|
|
2347
|
+
colorMap = "gray",
|
|
2348
|
+
opacity = 1.0,
|
|
2349
|
+
cal_min = NaN,
|
|
2350
|
+
cal_max = NaN,
|
|
2351
|
+
trustCalMinMax = true,
|
|
2352
|
+
percentileFrac = 0.02,
|
|
2353
|
+
ignoreZeroVoxels = false,
|
|
2354
|
+
visible = true,
|
|
2355
|
+
} = {}) {
|
|
2356
|
+
//https://stackoverflow.com/questions/21797299/convert-base64-string-to-arraybuffer
|
|
2357
|
+
function base64ToArrayBuffer(base64) {
|
|
2358
|
+
var binary_string = window.atob(base64);
|
|
2359
|
+
var len = binary_string.length;
|
|
2360
|
+
var bytes = new Uint8Array(len);
|
|
2361
|
+
for (var i = 0; i < len; i++) {
|
|
2362
|
+
bytes[i] = binary_string.charCodeAt(i);
|
|
2363
|
+
}
|
|
2364
|
+
return bytes.buffer;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
716
2367
|
let nvimage = null;
|
|
717
2368
|
try {
|
|
718
|
-
let dataBuffer =
|
|
2369
|
+
let dataBuffer = base64ToArrayBuffer(base64);
|
|
2370
|
+
let pairedImgData = null;
|
|
719
2371
|
nvimage = new NVImage(
|
|
720
2372
|
dataBuffer,
|
|
721
2373
|
name,
|
|
722
2374
|
colorMap,
|
|
723
2375
|
opacity,
|
|
2376
|
+
pairedImgData,
|
|
2377
|
+
cal_min,
|
|
2378
|
+
cal_max,
|
|
724
2379
|
trustCalMinMax,
|
|
725
2380
|
percentileFrac,
|
|
726
2381
|
ignoreZeroVoxels,
|
|
@@ -761,8 +2416,8 @@ NVImage.prototype.zeroImage = function () {
|
|
|
761
2416
|
|
|
762
2417
|
/**
|
|
763
2418
|
* Image M.
|
|
764
|
-
* @typedef {Object}
|
|
765
|
-
* @property {
|
|
2419
|
+
* @typedef {Object} NVImageMetadata
|
|
2420
|
+
* @property {string} id - unique if of image
|
|
766
2421
|
* @property {number} datatypeCode - data type
|
|
767
2422
|
* @property {number} nx - number of columns
|
|
768
2423
|
* @property {number} ny - number of rows
|
|
@@ -777,7 +2432,7 @@ NVImage.prototype.zeroImage = function () {
|
|
|
777
2432
|
|
|
778
2433
|
/**
|
|
779
2434
|
* get nifti specific metadata about the image
|
|
780
|
-
* @returns {
|
|
2435
|
+
* @returns {NVImageMetadata} - {@link NVImageMetadata}
|
|
781
2436
|
*/
|
|
782
2437
|
NVImage.prototype.getImageMetadata = function () {
|
|
783
2438
|
const id = this.id;
|
|
@@ -811,18 +2466,27 @@ NVImage.prototype.getImageMetadata = function () {
|
|
|
811
2466
|
/**
|
|
812
2467
|
* a factory function to make a zero filled image given a NVImage as a reference
|
|
813
2468
|
* @param {NVImage} nvImage an existing NVImage as a reference
|
|
2469
|
+
* @param {dataType} string the output data type. Options: 'same', 'uint8'
|
|
814
2470
|
* @returns {NVImage} returns a new NVImage filled with zeros for the image data
|
|
815
2471
|
* @example
|
|
816
2472
|
* myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop
|
|
817
2473
|
* newZeroImage = NVImage.zerosLike(myImage)
|
|
818
2474
|
*/
|
|
819
|
-
NVImage.zerosLike = function (nvImage) {
|
|
2475
|
+
NVImage.zerosLike = function (nvImage, dataType = "same") {
|
|
2476
|
+
// dataType can be: 'same', 'uint8'
|
|
2477
|
+
// 'same' means that the zeroed image data type is the same as the input image
|
|
820
2478
|
let zeroClone = nvImage.clone();
|
|
821
2479
|
zeroClone.zeroImage();
|
|
2480
|
+
if (dataType === "uint8") {
|
|
2481
|
+
zeroClone.img = Uint8Array.from(zeroClone.img);
|
|
2482
|
+
zeroClone.hdr.datatypeCode = zeroClone.DT_UNSIGNED_CHAR;
|
|
2483
|
+
zeroClone.hdr.numBitsPerVoxel = 8;
|
|
2484
|
+
}
|
|
822
2485
|
return zeroClone;
|
|
823
2486
|
};
|
|
824
2487
|
|
|
825
2488
|
String.prototype.getBytes = function () {
|
|
2489
|
+
//CR??? What does this do?
|
|
826
2490
|
let bytes = [];
|
|
827
2491
|
for (var i = 0; i < this.length; i++) {
|
|
828
2492
|
bytes.push(this.charCodeAt(i));
|
|
@@ -831,8 +2495,8 @@ String.prototype.getBytes = function () {
|
|
|
831
2495
|
return bytes;
|
|
832
2496
|
};
|
|
833
2497
|
|
|
834
|
-
NVImage.prototype.getValue = function (x, y, z) {
|
|
835
|
-
const { nx, ny } = this.getImageMetadata();
|
|
2498
|
+
NVImage.prototype.getValue = function (x, y, z, frame4D = 0) {
|
|
2499
|
+
const { nx, ny, nz } = this.getImageMetadata();
|
|
836
2500
|
if (this.hdr.datatypeCode === this.DT_RGBA32) {
|
|
837
2501
|
let vx = 4 * (x + y * nx + z * nx * ny);
|
|
838
2502
|
//convert rgb to luminance
|
|
@@ -847,7 +2511,8 @@ NVImage.prototype.getValue = function (x, y, z) {
|
|
|
847
2511
|
this.img[vx] * 0.21 + this.img[vx + 1] * 0.72 + this.img[vx + 2] * 0.07
|
|
848
2512
|
);
|
|
849
2513
|
}
|
|
850
|
-
let
|
|
2514
|
+
let vol = frame4D * nx * ny * nz;
|
|
2515
|
+
let i = this.img[x + y * nx + z * nx * ny + vol];
|
|
851
2516
|
return this.hdr.scl_slope * i + this.hdr.scl_inter;
|
|
852
2517
|
};
|
|
853
2518
|
|
|
@@ -865,28 +2530,28 @@ NVImage.prototype.getValue = function (x, y, z) {
|
|
|
865
2530
|
*/
|
|
866
2531
|
function getExtents(positions, forceOriginInVolume = true) {
|
|
867
2532
|
let nV = (positions.length / 3).toFixed(); //each vertex has 3 components: XYZ
|
|
868
|
-
let origin =
|
|
869
|
-
let mn =
|
|
870
|
-
let mx =
|
|
2533
|
+
let origin = vec3.fromValues(0, 0, 0); //default center of rotation
|
|
2534
|
+
let mn = vec3.create();
|
|
2535
|
+
let mx = vec3.create();
|
|
871
2536
|
let mxDx = 0.0;
|
|
872
2537
|
let nLoops = 1;
|
|
873
2538
|
if (forceOriginInVolume) nLoops = 2; //second pass to reposition origin
|
|
874
2539
|
for (let loop = 0; loop < nLoops; loop++) {
|
|
875
2540
|
mxDx = 0.0;
|
|
876
2541
|
for (let i = 0; i < nV; i++) {
|
|
877
|
-
let v =
|
|
2542
|
+
let v = vec3.fromValues(
|
|
878
2543
|
positions[i * 3],
|
|
879
2544
|
positions[i * 3 + 1],
|
|
880
2545
|
positions[i * 3 + 2]
|
|
881
2546
|
);
|
|
882
2547
|
if (i === 0) {
|
|
883
|
-
|
|
884
|
-
|
|
2548
|
+
vec3.copy(mn, v);
|
|
2549
|
+
vec3.copy(mx, v);
|
|
885
2550
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
let dx =
|
|
2551
|
+
vec3.min(mn, mn, v);
|
|
2552
|
+
vec3.max(mx, mx, v);
|
|
2553
|
+
vec3.subtract(v, v, origin);
|
|
2554
|
+
let dx = vec3.len(v);
|
|
890
2555
|
mxDx = Math.max(mxDx, dx);
|
|
891
2556
|
}
|
|
892
2557
|
if (loop + 1 >= nLoops) break;
|
|
@@ -896,7 +2561,7 @@ function getExtents(positions, forceOriginInVolume = true) {
|
|
|
896
2561
|
if (mx[j] < origin[j]) ok = false;
|
|
897
2562
|
}
|
|
898
2563
|
if (ok) break;
|
|
899
|
-
|
|
2564
|
+
vec3.lerp(origin, mn, mx, 0.5);
|
|
900
2565
|
log.debug("origin moved inside volume: ", origin);
|
|
901
2566
|
}
|
|
902
2567
|
let min = [mn[0], mn[1], mn[2]];
|
|
@@ -943,44 +2608,29 @@ NVImage.prototype.toNiivueObject3D = function (id, gl) {
|
|
|
943
2608
|
this.matRAS
|
|
944
2609
|
);
|
|
945
2610
|
|
|
946
|
-
|
|
2611
|
+
let posTex = [
|
|
2612
|
+
//spatial position (XYZ), texture coordinates UVW
|
|
947
2613
|
// Superior face
|
|
948
2614
|
...LPS,
|
|
2615
|
+
...[0.0, 0.0, 1.0],
|
|
949
2616
|
...RPS,
|
|
2617
|
+
...[1.0, 0.0, 1.0],
|
|
950
2618
|
...RAS,
|
|
2619
|
+
...[1.0, 1.0, 1.0],
|
|
951
2620
|
...LAS,
|
|
2621
|
+
...[0.0, 1.0, 1.0],
|
|
952
2622
|
|
|
953
2623
|
// Inferior face
|
|
954
2624
|
...LPI,
|
|
2625
|
+
...[0.0, 0.0, 0.0],
|
|
955
2626
|
...LAI,
|
|
2627
|
+
...[0.0, 1.0, 0.0],
|
|
956
2628
|
...RAI,
|
|
2629
|
+
...[1.0, 1.0, 0.0],
|
|
957
2630
|
...RPI,
|
|
2631
|
+
...[1.0, 0.0, 0.0],
|
|
958
2632
|
];
|
|
959
2633
|
|
|
960
|
-
const textureCoordinates = [
|
|
961
|
-
// Superior Z=1.0
|
|
962
|
-
0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
|
|
963
|
-
|
|
964
|
-
// Inferior Z=1.0
|
|
965
|
-
0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0,
|
|
966
|
-
|
|
967
|
-
// Anterior Y=1
|
|
968
|
-
0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0,
|
|
969
|
-
|
|
970
|
-
// Posterior Y=0
|
|
971
|
-
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0,
|
|
972
|
-
|
|
973
|
-
// Right X=1
|
|
974
|
-
1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0,
|
|
975
|
-
|
|
976
|
-
// Left X=0
|
|
977
|
-
0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0,
|
|
978
|
-
];
|
|
979
|
-
|
|
980
|
-
const vertexBuffer = gl.createBuffer();
|
|
981
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
982
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
|
983
|
-
|
|
984
2634
|
const indexBuffer = gl.createBuffer();
|
|
985
2635
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
986
2636
|
|
|
@@ -989,6 +2639,7 @@ NVImage.prototype.toNiivueObject3D = function (id, gl) {
|
|
|
989
2639
|
// position.
|
|
990
2640
|
|
|
991
2641
|
const indices = [
|
|
2642
|
+
//six faces of cube: each has 2 triangles (6 indices)
|
|
992
2643
|
0,
|
|
993
2644
|
3,
|
|
994
2645
|
2,
|
|
@@ -1034,28 +2685,45 @@ NVImage.prototype.toNiivueObject3D = function (id, gl) {
|
|
|
1034
2685
|
gl.STATIC_DRAW
|
|
1035
2686
|
);
|
|
1036
2687
|
|
|
1037
|
-
const
|
|
1038
|
-
gl.bindBuffer(gl.ARRAY_BUFFER,
|
|
1039
|
-
gl.bufferData(
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
);
|
|
2688
|
+
const posTexBuffer = gl.createBuffer();
|
|
2689
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
|
|
2690
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posTex), gl.STATIC_DRAW);
|
|
2691
|
+
|
|
2692
|
+
const vao = gl.createVertexArray();
|
|
2693
|
+
gl.bindVertexArray(vao);
|
|
2694
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
2695
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
|
|
2696
|
+
//vertex spatial position: 3 floats X,Y,Z
|
|
2697
|
+
gl.enableVertexAttribArray(0);
|
|
2698
|
+
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
|
|
2699
|
+
//UVW texCoord: (also three floats)
|
|
2700
|
+
gl.enableVertexAttribArray(1);
|
|
2701
|
+
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
|
|
2702
|
+
gl.bindVertexArray(null);
|
|
1044
2703
|
|
|
1045
2704
|
const obj3D = new NiivueObject3D(
|
|
1046
2705
|
id,
|
|
1047
|
-
|
|
2706
|
+
posTexBuffer,
|
|
1048
2707
|
gl.TRIANGLES,
|
|
1049
2708
|
indices.length,
|
|
1050
2709
|
indexBuffer,
|
|
1051
|
-
|
|
2710
|
+
vao
|
|
1052
2711
|
);
|
|
1053
2712
|
|
|
1054
|
-
const extents = getExtents(
|
|
2713
|
+
const extents = getExtents([
|
|
2714
|
+
...LPS,
|
|
2715
|
+
...RPS,
|
|
2716
|
+
...RAS,
|
|
2717
|
+
...LAS,
|
|
2718
|
+
...LPI,
|
|
2719
|
+
...LAI,
|
|
2720
|
+
...RAI,
|
|
2721
|
+
...RPI,
|
|
2722
|
+
]);
|
|
1055
2723
|
obj3D.extentsMin = extents.min;
|
|
1056
2724
|
obj3D.extentsMax = extents.max;
|
|
1057
2725
|
obj3D.furthestVertexFromOrigin = extents.furthestVertexFromOrigin;
|
|
1058
|
-
obj3D.originNegate =
|
|
1059
|
-
|
|
2726
|
+
obj3D.originNegate = vec3.clone(extents.origin);
|
|
2727
|
+
vec3.negate(obj3D.originNegate, obj3D.originNegate);
|
|
1060
2728
|
return obj3D;
|
|
1061
2729
|
};
|