@niivue/niivue 0.22.2 → 0.24.0

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