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