@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/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,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 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,
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
- this.hdr = nifti.readHeader(dataBuffer);
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
- 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
-
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 (var i = 0; i < vx8 - 1; i++) this.img[i] = i8[i];
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
- case this.DT_UINT32:
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 (var i = 0; i < vx32 - 1; i++) this.img[i] = u32[i];
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
- case this.DT_SIGNED_INT:
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 (var i = 0; i < vxi32 - 1; i++) this.img[i] = i32[i];
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
- case this.DT_INT64:
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 (var i = 0; i < vx - 1; i++) this.img[i] = Number(i64[i]);
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
- 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(
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 = 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));
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 = mat.mat3.fromValues(
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 = mat.mat4.fromValues(
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 = mat.mat4.create();
357
- mat.mat4.copy(R, rotM);
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
- 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
- }
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 = 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);
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
- mat.mat4.identity(rotM);
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 = 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);
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 = mat.mat4.clone(rotM);
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 = 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]);
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 = 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]);
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
- resolve(reader.result);
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
- let dataBuffer = await this.readFileAsync(file);
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} NVImage~MetaData
765
- * @property {uuidv4} id - unique if of image
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 {NVImage~Metadata} - {@link NVImage~Metadata}
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 = mat.vec3.fromValues(0, 0, 0); //default center of rotation
869
- let mn = mat.vec3.create();
870
- let mx = mat.vec3.create();
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 = mat.vec3.fromValues(
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
- mat.vec3.copy(mn, v);
884
- mat.vec3.copy(mx, v);
2526
+ vec3.copy(mn, v);
2527
+ vec3.copy(mx, v);
885
2528
  }
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);
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
- mat.vec3.lerp(origin, mn, mx, 0.5);
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
- const positions = [
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 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
- );
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
- vertexBuffer,
2684
+ posTexBuffer,
1048
2685
  gl.TRIANGLES,
1049
2686
  indices.length,
1050
2687
  indexBuffer,
1051
- textureCoordBuffer
2688
+ vao
1052
2689
  );
1053
2690
 
1054
- const extents = getExtents(positions);
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 = mat.vec3.clone(extents.origin);
1059
- mat.vec3.negate(obj3D.originNegate, obj3D.originNegate);
2704
+ obj3D.originNegate = vec3.clone(extents.origin);
2705
+ vec3.negate(obj3D.originNegate, obj3D.originNegate);
1060
2706
  return obj3D;
1061
2707
  };