@niivue/niivue 0.52.0 → 0.53.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/build/index.d.ts +85 -29
- package/build/index.js +2087 -1389
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/build/metafile-esm.json +1 -1
- package/dist/index.d.ts +85 -29
- package/dist/index.js +1775 -1077
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/niivue.umd.js +75 -75
- package/package.json +1 -1
- package/src/index.html +1 -1
- package/src/index_magic.html +223 -0
- package/src/niivue/index.ts +718 -346
- package/src/nvdocument.ts +4 -2
- package/src/nvimage/ImageReaders/index.ts +13 -0
- package/src/nvimage/ImageReaders/mgh.ts +161 -0
- package/src/nvimage/ImageReaders/nii.ts +53 -0
- package/src/nvimage/ImageReaders/nrrd.ts +338 -0
- package/src/nvimage/ImageWriter.ts +298 -0
- package/src/nvimage/RenderingUtils.ts +158 -0
- package/src/nvimage/VolumeUtils.ts +348 -0
- package/src/nvimage/index.ts +278 -930
- package/src/nvimage/utils.ts +16 -1
- package/src/nvmesh.ts +8 -5
- package/src/nvutilities.ts +11 -3
- package/src/shader-srcs.ts +1 -1
- package/src/types.ts +1 -1
package/build/index.js
CHANGED
|
@@ -14,10 +14,10 @@ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot
|
|
|
14
14
|
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
15
15
|
|
|
16
16
|
// src/niivue/index.ts
|
|
17
|
-
import { mat4 as
|
|
17
|
+
import { mat4 as mat49, vec2, vec3 as vec314, vec4 as vec49 } from "gl-matrix";
|
|
18
18
|
|
|
19
19
|
// package.json
|
|
20
|
-
var version = "0.
|
|
20
|
+
var version = "0.53.0";
|
|
21
21
|
|
|
22
22
|
// src/logger.ts
|
|
23
23
|
var _Log = class _Log {
|
|
@@ -1161,7 +1161,7 @@ void main(void) {
|
|
|
1161
1161
|
//https://stackoverflow.com/questions/5879403/opengl-texture-coordinates-in-pixel-space
|
|
1162
1162
|
float nlayer = float(textureSize(colormap, 0).y);
|
|
1163
1163
|
//each volume has two color maps:
|
|
1164
|
-
// (layer*2) = negative and (layer * 2) + 1 =
|
|
1164
|
+
// (layer*2) = negative and (layer * 2) + 1 = positive
|
|
1165
1165
|
float y = ((2.0 * layer) + 1.5)/nlayer;
|
|
1166
1166
|
FragColor = texture(colormap, vec2(txl, y)).rgba;
|
|
1167
1167
|
//negative colors
|
|
@@ -3377,10 +3377,10 @@ __publicField(_NiivueObject3D, "makeCylinder", function(vertices, indices, start
|
|
|
3377
3377
|
const idx0 = Math.floor(vertices.length / 3);
|
|
3378
3378
|
const idx = new Uint32Array(num_f * 3);
|
|
3379
3379
|
const vtx = new Float32Array(num_v * 3);
|
|
3380
|
-
function setV(i,
|
|
3381
|
-
vtx[i * 3 + 0] =
|
|
3382
|
-
vtx[i * 3 + 1] =
|
|
3383
|
-
vtx[i * 3 + 2] =
|
|
3380
|
+
function setV(i, vec315) {
|
|
3381
|
+
vtx[i * 3 + 0] = vec315[0];
|
|
3382
|
+
vtx[i * 3 + 1] = vec315[1];
|
|
3383
|
+
vtx[i * 3 + 2] = vec315[2];
|
|
3384
3384
|
}
|
|
3385
3385
|
function setI(i, a, b, c) {
|
|
3386
3386
|
idx[i * 3 + 0] = a + idx0;
|
|
@@ -3448,7 +3448,7 @@ __publicField(_NiivueObject3D, "makeColoredSphere", function(vertices, indices,
|
|
|
3448
3448
|
var NiivueObject3D = _NiivueObject3D;
|
|
3449
3449
|
|
|
3450
3450
|
// src/nvmesh.ts
|
|
3451
|
-
import { vec3 as
|
|
3451
|
+
import { vec3 as vec310, vec4 as vec47 } from "gl-matrix";
|
|
3452
3452
|
import { v4 as uuidv42 } from "@lukeed/uuid";
|
|
3453
3453
|
|
|
3454
3454
|
// src/cmaps/index.js
|
|
@@ -11984,7 +11984,7 @@ var NVUtilities = class _NVUtilities {
|
|
|
11984
11984
|
const decompressed = await _NVUtilities.decompress(data);
|
|
11985
11985
|
return decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
|
|
11986
11986
|
}
|
|
11987
|
-
static async readMatV4(buffer) {
|
|
11987
|
+
static async readMatV4(buffer, isReplaceDots = false) {
|
|
11988
11988
|
let len = buffer.byteLength;
|
|
11989
11989
|
if (len < 40) {
|
|
11990
11990
|
throw new Error("File too small to be MAT v4: bytes = " + buffer.byteLength);
|
|
@@ -12040,7 +12040,10 @@ var NVUtilities = class _NVUtilities {
|
|
|
12040
12040
|
throw new Error("mrows * ncols must be greater than one");
|
|
12041
12041
|
}
|
|
12042
12042
|
const byteArray = new Uint8Array(bytes.subarray(pos, pos + namlen));
|
|
12043
|
-
|
|
12043
|
+
let tagName = textDecoder.decode(byteArray).trim().replaceAll("\0", "");
|
|
12044
|
+
if (isReplaceDots) {
|
|
12045
|
+
tagName = tagName.replaceAll(".", "_");
|
|
12046
|
+
}
|
|
12044
12047
|
const tagDataType = getTensDigit(mtype);
|
|
12045
12048
|
let tagBytesPerItem = 8;
|
|
12046
12049
|
if (tagDataType >= 1 && tagDataType <= 2) {
|
|
@@ -15726,18 +15729,11 @@ var NVMeshLoaders = class _NVMeshLoaders {
|
|
|
15726
15729
|
|
|
15727
15730
|
// src/nvdocument.ts
|
|
15728
15731
|
import { serialize, deserialize } from "@ungap/structured-clone";
|
|
15729
|
-
import { vec3 as
|
|
15732
|
+
import { vec3 as vec39, vec4 as vec46 } from "gl-matrix";
|
|
15730
15733
|
|
|
15731
15734
|
// src/nvimage/index.ts
|
|
15732
|
-
import {
|
|
15733
|
-
|
|
15734
|
-
NIFTIEXTENSION,
|
|
15735
|
-
isCompressed,
|
|
15736
|
-
decompressAsync,
|
|
15737
|
-
readImage,
|
|
15738
|
-
readHeaderAsync
|
|
15739
|
-
} from "nifti-reader-js";
|
|
15740
|
-
import { mat3, mat4 as mat44, vec3 as vec36, vec4 as vec44 } from "gl-matrix";
|
|
15735
|
+
import { NIFTI1 as NIFTI14, NIFTIEXTENSION, readHeaderAsync as readHeaderAsync2 } from "nifti-reader-js";
|
|
15736
|
+
import { mat3 as mat32, mat4 as mat46, vec3 as vec38, vec4 as vec45 } from "gl-matrix";
|
|
15741
15737
|
import { v4 as uuidv4 } from "@lukeed/uuid";
|
|
15742
15738
|
import { Gunzip } from "fflate";
|
|
15743
15739
|
|
|
@@ -15769,6 +15765,9 @@ var ImageType = /* @__PURE__ */ ((ImageType3) => {
|
|
|
15769
15765
|
ImageType3[ImageType3["SRC"] = 17] = "SRC";
|
|
15770
15766
|
ImageType3[ImageType3["FIB"] = 18] = "FIB";
|
|
15771
15767
|
ImageType3[ImageType3["BMP"] = 19] = "BMP";
|
|
15768
|
+
ImageType3[ImageType3["ZARR"] = 20] = "ZARR";
|
|
15769
|
+
ImageType3[ImageType3["NPY"] = 21] = "NPY";
|
|
15770
|
+
ImageType3[ImageType3["NPZ"] = 22] = "NPZ";
|
|
15772
15771
|
return ImageType3;
|
|
15773
15772
|
})(ImageType || {});
|
|
15774
15773
|
var NVIMAGE_TYPE = Object.freeze({
|
|
@@ -15783,6 +15782,9 @@ var NVIMAGE_TYPE = Object.freeze({
|
|
|
15783
15782
|
case "TXT":
|
|
15784
15783
|
imageType = 3 /* DCM_MANIFEST */;
|
|
15785
15784
|
break;
|
|
15785
|
+
case "FZ":
|
|
15786
|
+
case "GQI":
|
|
15787
|
+
case "QSDR":
|
|
15786
15788
|
case "FIB":
|
|
15787
15789
|
imageType = 18 /* FIB */;
|
|
15788
15790
|
break;
|
|
@@ -15813,6 +15815,12 @@ var NVIMAGE_TYPE = Object.freeze({
|
|
|
15813
15815
|
case "MGZ":
|
|
15814
15816
|
imageType = 11 /* MGZ */;
|
|
15815
15817
|
break;
|
|
15818
|
+
case "NPY":
|
|
15819
|
+
imageType = 21 /* NPY */;
|
|
15820
|
+
break;
|
|
15821
|
+
case "NPZ":
|
|
15822
|
+
imageType = 22 /* NPZ */;
|
|
15823
|
+
break;
|
|
15816
15824
|
case "SRC":
|
|
15817
15825
|
imageType = 17 /* SRC */;
|
|
15818
15826
|
break;
|
|
@@ -15835,6 +15843,9 @@ var NVIMAGE_TYPE = Object.freeze({
|
|
|
15835
15843
|
case "JPEG":
|
|
15836
15844
|
imageType = 19 /* BMP */;
|
|
15837
15845
|
break;
|
|
15846
|
+
case "ZARR":
|
|
15847
|
+
imageType = 20 /* ZARR */;
|
|
15848
|
+
break;
|
|
15838
15849
|
}
|
|
15839
15850
|
return imageType;
|
|
15840
15851
|
}
|
|
@@ -16073,6 +16084,882 @@ async function uncompressStream(stream) {
|
|
|
16073
16084
|
return uncompressedStream;
|
|
16074
16085
|
}
|
|
16075
16086
|
|
|
16087
|
+
// src/nvimage/ImageWriter.ts
|
|
16088
|
+
import { NIFTI1 } from "nifti-reader-js";
|
|
16089
|
+
function createNiftiHeader(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2 /* DT_UINT8 */) {
|
|
16090
|
+
const hdr = new NIFTI1();
|
|
16091
|
+
hdr.littleEndian = true;
|
|
16092
|
+
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
16093
|
+
hdr.dims[0] = Math.max(3, dims.length);
|
|
16094
|
+
for (let i = 0; i < dims.length; i++) {
|
|
16095
|
+
hdr.dims[i + 1] = dims[i];
|
|
16096
|
+
}
|
|
16097
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
16098
|
+
for (let i = 0; i < dims.length; i++) {
|
|
16099
|
+
hdr.pixDims[i + 1] = pixDims[i];
|
|
16100
|
+
}
|
|
16101
|
+
if (affine.length === 16) {
|
|
16102
|
+
let k = 0;
|
|
16103
|
+
for (let i = 0; i < 4; i++) {
|
|
16104
|
+
for (let j = 0; j < 4; j++) {
|
|
16105
|
+
hdr.affine[i][j] = affine[k];
|
|
16106
|
+
k++;
|
|
16107
|
+
}
|
|
16108
|
+
}
|
|
16109
|
+
}
|
|
16110
|
+
let bpv = 8;
|
|
16111
|
+
if (datatypeCode === 256 /* DT_INT8 */ || datatypeCode === 2 /* DT_UINT8 */) {
|
|
16112
|
+
bpv = 8;
|
|
16113
|
+
} else if (datatypeCode === 512 /* DT_UINT16 */ || datatypeCode === 4 /* DT_INT16 */) {
|
|
16114
|
+
bpv = 16;
|
|
16115
|
+
} else if (datatypeCode === 16 /* DT_FLOAT32 */ || datatypeCode === 768 /* DT_UINT32 */ || datatypeCode === 8 /* DT_INT32 */ || datatypeCode === 2304 /* DT_RGBA32 */) {
|
|
16116
|
+
bpv = 32;
|
|
16117
|
+
} else if (datatypeCode === 64 /* DT_FLOAT64 */) {
|
|
16118
|
+
bpv = 64;
|
|
16119
|
+
} else {
|
|
16120
|
+
log.warn("Unsupported NIfTI datatypeCode for header creation: " + datatypeCode);
|
|
16121
|
+
}
|
|
16122
|
+
hdr.datatypeCode = datatypeCode;
|
|
16123
|
+
hdr.numBitsPerVoxel = bpv;
|
|
16124
|
+
hdr.scl_inter = 0;
|
|
16125
|
+
hdr.scl_slope = 1;
|
|
16126
|
+
hdr.sform_code = 2;
|
|
16127
|
+
hdr.magic = "n+1";
|
|
16128
|
+
hdr.vox_offset = 352;
|
|
16129
|
+
return hdr;
|
|
16130
|
+
}
|
|
16131
|
+
function createNiftiArray(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2 /* DT_UINT8 */, img = new Uint8Array()) {
|
|
16132
|
+
const hdr = createNiftiHeader(dims, pixDims, affine, datatypeCode);
|
|
16133
|
+
const hdrBytes = hdrToArrayBuffer(hdr, false);
|
|
16134
|
+
hdr.vox_offset = Math.max(352, hdrBytes.length);
|
|
16135
|
+
const finalHdrBytes = hdrToArrayBuffer(hdr, false);
|
|
16136
|
+
if (img.length < 1) {
|
|
16137
|
+
return finalHdrBytes;
|
|
16138
|
+
}
|
|
16139
|
+
const paddingSize = Math.max(0, hdr.vox_offset - finalHdrBytes.length);
|
|
16140
|
+
const padding = new Uint8Array(paddingSize);
|
|
16141
|
+
const imgBytes = new Uint8Array(img.buffer, img.byteOffset, img.byteLength);
|
|
16142
|
+
const totalLength = hdr.vox_offset + imgBytes.length;
|
|
16143
|
+
const outputData = new Uint8Array(totalLength);
|
|
16144
|
+
outputData.set(finalHdrBytes, 0);
|
|
16145
|
+
outputData.set(padding, finalHdrBytes.length);
|
|
16146
|
+
outputData.set(imgBytes, hdr.vox_offset);
|
|
16147
|
+
return outputData;
|
|
16148
|
+
}
|
|
16149
|
+
function toUint8Array(nvImage, drawingBytes = null) {
|
|
16150
|
+
if (!nvImage.hdr) {
|
|
16151
|
+
throw new Error("NVImage header is not defined for toUint8Array");
|
|
16152
|
+
}
|
|
16153
|
+
if (!nvImage.img && drawingBytes === null) {
|
|
16154
|
+
throw new Error("NVImage image data is not defined for toUint8Array");
|
|
16155
|
+
}
|
|
16156
|
+
const isDrawing = drawingBytes !== null;
|
|
16157
|
+
const hdrCopy = JSON.parse(JSON.stringify(nvImage.hdr));
|
|
16158
|
+
hdrCopy.vox_offset = Math.max(352, hdrCopy.vox_offset);
|
|
16159
|
+
if (isDrawing) {
|
|
16160
|
+
hdrCopy.datatypeCode = 2 /* DT_UINT8 */;
|
|
16161
|
+
hdrCopy.numBitsPerVoxel = 8;
|
|
16162
|
+
hdrCopy.scl_slope = 1;
|
|
16163
|
+
hdrCopy.scl_inter = 0;
|
|
16164
|
+
}
|
|
16165
|
+
const hdrBytes = hdrToArrayBuffer(hdrCopy, isDrawing);
|
|
16166
|
+
let imageBytesToSave;
|
|
16167
|
+
if (isDrawing) {
|
|
16168
|
+
const drawingBytesCurrent = drawingBytes;
|
|
16169
|
+
const perm = nvImage.permRAS;
|
|
16170
|
+
if (perm && (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3)) {
|
|
16171
|
+
log.debug("Reorienting drawing bytes back to native space for saving...");
|
|
16172
|
+
const dims = nvImage.hdr.dims;
|
|
16173
|
+
const nVox = dims[1] * dims[2] * dims[3];
|
|
16174
|
+
const nVoxRAS = nvImage.dimsRAS ? nvImage.dimsRAS[1] * nvImage.dimsRAS[2] * nvImage.dimsRAS[3] : nVox;
|
|
16175
|
+
if (drawingBytesCurrent.length !== nVoxRAS) {
|
|
16176
|
+
console.warn(
|
|
16177
|
+
`Drawing length (${drawingBytesCurrent.length}) does not match expected RAS voxel count (${nVoxRAS}). Cannot reorient drawing reliably.`
|
|
16178
|
+
);
|
|
16179
|
+
imageBytesToSave = drawingBytesCurrent;
|
|
16180
|
+
} else if (!nvImage.img2RASstep || !nvImage.img2RASstart || !nvImage.dimsRAS) {
|
|
16181
|
+
console.warn(
|
|
16182
|
+
`Missing RAS transformation info (img2RASstep, img2RASstart, dimsRAS). Cannot reorient drawing reliably.`
|
|
16183
|
+
);
|
|
16184
|
+
imageBytesToSave = drawingBytesCurrent;
|
|
16185
|
+
} else {
|
|
16186
|
+
const step = nvImage.img2RASstep;
|
|
16187
|
+
const start = nvImage.img2RASstart;
|
|
16188
|
+
const dimsRAS = nvImage.dimsRAS;
|
|
16189
|
+
const nativeData = new Uint8Array(nVox);
|
|
16190
|
+
nativeData.fill(0);
|
|
16191
|
+
const inputDrawingRAS = drawingBytesCurrent;
|
|
16192
|
+
let rasIndex = 0;
|
|
16193
|
+
for (let rz = 0; rz < dimsRAS[3]; rz++) {
|
|
16194
|
+
const zi = start[2] + rz * step[2];
|
|
16195
|
+
for (let ry = 0; ry < dimsRAS[2]; ry++) {
|
|
16196
|
+
const yi = start[1] + ry * step[1];
|
|
16197
|
+
for (let rx = 0; rx < dimsRAS[1]; rx++) {
|
|
16198
|
+
const xi = start[0] + rx * step[0];
|
|
16199
|
+
const nativeIndex = xi + yi + zi;
|
|
16200
|
+
if (nativeIndex >= 0 && nativeIndex < nVox) {
|
|
16201
|
+
nativeData[nativeIndex] = inputDrawingRAS[rasIndex];
|
|
16202
|
+
} else if (rasIndex < inputDrawingRAS.length) {
|
|
16203
|
+
console.warn(
|
|
16204
|
+
`Calculated native index ${nativeIndex} is out of bounds [0..${nVox - 1}] during drawing reorientation.`
|
|
16205
|
+
);
|
|
16206
|
+
}
|
|
16207
|
+
rasIndex++;
|
|
16208
|
+
}
|
|
16209
|
+
}
|
|
16210
|
+
}
|
|
16211
|
+
imageBytesToSave = nativeData;
|
|
16212
|
+
}
|
|
16213
|
+
} else {
|
|
16214
|
+
imageBytesToSave = drawingBytesCurrent;
|
|
16215
|
+
}
|
|
16216
|
+
} else {
|
|
16217
|
+
if (!nvImage.img) {
|
|
16218
|
+
throw new Error("NVImage image data is null when trying to save non-drawing.");
|
|
16219
|
+
}
|
|
16220
|
+
imageBytesToSave = new Uint8Array(nvImage.img.buffer, nvImage.img.byteOffset, nvImage.img.byteLength);
|
|
16221
|
+
}
|
|
16222
|
+
const headerSize = hdrBytes.length;
|
|
16223
|
+
const paddingSize = Math.max(0, hdrCopy.vox_offset - headerSize);
|
|
16224
|
+
const padding = new Uint8Array(paddingSize);
|
|
16225
|
+
const totalLength = hdrCopy.vox_offset + imageBytesToSave.length;
|
|
16226
|
+
const outputData = new Uint8Array(totalLength);
|
|
16227
|
+
outputData.set(hdrBytes, 0);
|
|
16228
|
+
outputData.set(padding, headerSize);
|
|
16229
|
+
outputData.set(imageBytesToSave, hdrCopy.vox_offset);
|
|
16230
|
+
return outputData;
|
|
16231
|
+
}
|
|
16232
|
+
async function saveToUint8Array(nvImage, fnm, drawing8 = null) {
|
|
16233
|
+
const odata = toUint8Array(nvImage, drawing8);
|
|
16234
|
+
const compress = fnm.toLowerCase().endsWith(".gz");
|
|
16235
|
+
if (compress) {
|
|
16236
|
+
try {
|
|
16237
|
+
const compressedData = await NVUtilities.compress(odata, "gzip");
|
|
16238
|
+
return new Uint8Array(compressedData);
|
|
16239
|
+
} catch (error) {
|
|
16240
|
+
log.error("Compression failed:", error);
|
|
16241
|
+
log.warn("Returning uncompressed data due to compression error.");
|
|
16242
|
+
return odata;
|
|
16243
|
+
}
|
|
16244
|
+
} else {
|
|
16245
|
+
return odata;
|
|
16246
|
+
}
|
|
16247
|
+
}
|
|
16248
|
+
async function saveToDisk(nvImage, fnm = "", drawing8 = null) {
|
|
16249
|
+
const saveData = await saveToUint8Array(nvImage, fnm, drawing8);
|
|
16250
|
+
if (!fnm) {
|
|
16251
|
+
log.debug("saveToDisk: empty file name, returning data as Uint8Array rather than triggering download");
|
|
16252
|
+
return saveData;
|
|
16253
|
+
}
|
|
16254
|
+
try {
|
|
16255
|
+
const blob = new Blob([saveData.buffer], {
|
|
16256
|
+
type: "application/octet-stream"
|
|
16257
|
+
// Standard type for binary download
|
|
16258
|
+
});
|
|
16259
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
16260
|
+
const link = document.createElement("a");
|
|
16261
|
+
link.setAttribute("href", blobUrl);
|
|
16262
|
+
link.setAttribute("download", fnm);
|
|
16263
|
+
link.style.visibility = "hidden";
|
|
16264
|
+
document.body.appendChild(link);
|
|
16265
|
+
link.click();
|
|
16266
|
+
document.body.removeChild(link);
|
|
16267
|
+
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
|
|
16268
|
+
} catch (e) {
|
|
16269
|
+
log.error("Failed to trigger download:", e);
|
|
16270
|
+
}
|
|
16271
|
+
return saveData;
|
|
16272
|
+
}
|
|
16273
|
+
|
|
16274
|
+
// src/nvimage/VolumeUtils.ts
|
|
16275
|
+
function getValue(nvImage, x, y, z, frame4D = 0, isReadImaginary = false) {
|
|
16276
|
+
if (!nvImage.hdr) {
|
|
16277
|
+
throw new Error("getValue: NVImage header is not defined.");
|
|
16278
|
+
}
|
|
16279
|
+
if (!isReadImaginary && !nvImage.img) {
|
|
16280
|
+
throw new Error("getValue: NVImage image data is not defined.");
|
|
16281
|
+
}
|
|
16282
|
+
if (isReadImaginary && !nvImage.imaginary) {
|
|
16283
|
+
log.warn("getValue: Attempted to read imaginary data, but none exists.");
|
|
16284
|
+
return 0;
|
|
16285
|
+
}
|
|
16286
|
+
const nx = nvImage.hdr.dims[1];
|
|
16287
|
+
const ny = nvImage.hdr.dims[2];
|
|
16288
|
+
const nz = nvImage.hdr.dims[3];
|
|
16289
|
+
x = Math.max(0, Math.min(Math.round(x), nx - 1));
|
|
16290
|
+
y = Math.max(0, Math.min(Math.round(y), ny - 1));
|
|
16291
|
+
z = Math.max(0, Math.min(Math.round(z), nz - 1));
|
|
16292
|
+
frame4D = Math.max(0, frame4D);
|
|
16293
|
+
let vx = x + y * nx + z * nx * ny;
|
|
16294
|
+
if (nvImage.hdr.datatypeCode === 2304 /* DT_RGBA32 */) {
|
|
16295
|
+
if (!nvImage.img) {
|
|
16296
|
+
return 0;
|
|
16297
|
+
}
|
|
16298
|
+
vx *= 4;
|
|
16299
|
+
if (vx + 2 >= nvImage.img.length) {
|
|
16300
|
+
log.warn(`getValue: Calculated index ${vx} out of bounds for RGBA data.`);
|
|
16301
|
+
return 0;
|
|
16302
|
+
}
|
|
16303
|
+
const lum = nvImage.img[vx] * 0.2126 + nvImage.img[vx + 1] * 0.7152 + nvImage.img[vx + 2] * 0.0722;
|
|
16304
|
+
return Math.round(lum);
|
|
16305
|
+
}
|
|
16306
|
+
if (nvImage.hdr.datatypeCode === 128 /* DT_RGB24 */) {
|
|
16307
|
+
if (!nvImage.img) {
|
|
16308
|
+
return 0;
|
|
16309
|
+
}
|
|
16310
|
+
vx *= 3;
|
|
16311
|
+
if (vx + 2 >= nvImage.img.length) {
|
|
16312
|
+
log.warn(`getValue: Calculated index ${vx} out of bounds for RGB data.`);
|
|
16313
|
+
return 0;
|
|
16314
|
+
}
|
|
16315
|
+
const lum = nvImage.img[vx] * 0.2126 + nvImage.img[vx + 1] * 0.7152 + nvImage.img[vx + 2] * 0.0722;
|
|
16316
|
+
return Math.round(lum);
|
|
16317
|
+
}
|
|
16318
|
+
const nVox3D = nx * ny * nz;
|
|
16319
|
+
const volOffset = frame4D * nVox3D;
|
|
16320
|
+
const finalVxIndex = vx + volOffset;
|
|
16321
|
+
const dataArray = isReadImaginary ? nvImage.imaginary : nvImage.img;
|
|
16322
|
+
if (finalVxIndex < 0 || finalVxIndex >= dataArray.length) {
|
|
16323
|
+
return 0;
|
|
16324
|
+
}
|
|
16325
|
+
const rawValue = dataArray[finalVxIndex];
|
|
16326
|
+
const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1 : nvImage.hdr.scl_slope;
|
|
16327
|
+
const inter = isNaN(nvImage.hdr.scl_inter) ? 0 : nvImage.hdr.scl_inter;
|
|
16328
|
+
return slope * rawValue + inter;
|
|
16329
|
+
}
|
|
16330
|
+
function getVolumeData(nvImage, voxStartRAS = [-1, 0, 0], voxEndRAS = [0, 0, 0], dataType = "same") {
|
|
16331
|
+
const defaultResult = [new Uint8Array(), [0, 0, 0]];
|
|
16332
|
+
if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {
|
|
16333
|
+
log.error("getVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).");
|
|
16334
|
+
return defaultResult;
|
|
16335
|
+
}
|
|
16336
|
+
voxStartRAS = voxStartRAS.slice(0, 3);
|
|
16337
|
+
voxEndRAS = voxEndRAS.slice(0, 3);
|
|
16338
|
+
if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {
|
|
16339
|
+
log.warn("getVolumeData: Invalid start or end coordinates provided.");
|
|
16340
|
+
return defaultResult;
|
|
16341
|
+
}
|
|
16342
|
+
const dimsRAS = nvImage.dimsRAS.slice(1, 4);
|
|
16343
|
+
for (let i = 0; i < 3; i++) {
|
|
16344
|
+
voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1));
|
|
16345
|
+
voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1));
|
|
16346
|
+
if (voxEndRAS[i] < voxStartRAS[i]) {
|
|
16347
|
+
const tmp = voxEndRAS[i];
|
|
16348
|
+
voxEndRAS[i] = voxStartRAS[i];
|
|
16349
|
+
voxStartRAS[i] = tmp;
|
|
16350
|
+
}
|
|
16351
|
+
}
|
|
16352
|
+
const slabDims = [
|
|
16353
|
+
voxEndRAS[0] - voxStartRAS[0] + 1,
|
|
16354
|
+
voxEndRAS[1] - voxStartRAS[1] + 1,
|
|
16355
|
+
voxEndRAS[2] - voxStartRAS[2] + 1
|
|
16356
|
+
];
|
|
16357
|
+
const slabNVox = slabDims[0] * slabDims[1] * slabDims[2];
|
|
16358
|
+
if (slabNVox <= 0) {
|
|
16359
|
+
log.warn("getVolumeData: Calculated slab size is zero or negative.");
|
|
16360
|
+
return defaultResult;
|
|
16361
|
+
}
|
|
16362
|
+
let OutputArrayConstructor = nvImage.img.constructor;
|
|
16363
|
+
if (dataType === "uint8") {
|
|
16364
|
+
OutputArrayConstructor = Uint8Array;
|
|
16365
|
+
} else if (dataType === "int16") {
|
|
16366
|
+
OutputArrayConstructor = Int16Array;
|
|
16367
|
+
} else if (dataType === "uint16") {
|
|
16368
|
+
OutputArrayConstructor = Uint16Array;
|
|
16369
|
+
} else if (dataType === "float32" || dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
|
|
16370
|
+
OutputArrayConstructor = Float32Array;
|
|
16371
|
+
} else if (dataType === "float64") {
|
|
16372
|
+
OutputArrayConstructor = Float64Array;
|
|
16373
|
+
} else if (dataType !== "same") {
|
|
16374
|
+
log.warn(`getVolumeData: Unsupported dataType '${dataType}'. Using 'same'.`);
|
|
16375
|
+
}
|
|
16376
|
+
let outputImg;
|
|
16377
|
+
try {
|
|
16378
|
+
outputImg = new OutputArrayConstructor(slabNVox);
|
|
16379
|
+
} catch (e) {
|
|
16380
|
+
log.error(`getVolumeData: Failed to create output array for dataType '${dataType}'.`, e);
|
|
16381
|
+
return defaultResult;
|
|
16382
|
+
}
|
|
16383
|
+
const step = nvImage.img2RASstep;
|
|
16384
|
+
const start = nvImage.img2RASstart;
|
|
16385
|
+
const sourceImg = nvImage.img;
|
|
16386
|
+
let outputIndex = 0;
|
|
16387
|
+
for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {
|
|
16388
|
+
const zi = start[2] + rz * step[2];
|
|
16389
|
+
for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {
|
|
16390
|
+
const yi = start[1] + ry * step[1];
|
|
16391
|
+
for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {
|
|
16392
|
+
const xi = start[0] + rx * step[0];
|
|
16393
|
+
const nativeIndex = xi + yi + zi;
|
|
16394
|
+
let value = 0;
|
|
16395
|
+
if (nativeIndex >= 0 && nativeIndex < sourceImg.length) {
|
|
16396
|
+
value = sourceImg[nativeIndex];
|
|
16397
|
+
} else {
|
|
16398
|
+
}
|
|
16399
|
+
outputImg[outputIndex++] = value;
|
|
16400
|
+
}
|
|
16401
|
+
}
|
|
16402
|
+
}
|
|
16403
|
+
const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1 : nvImage.hdr.scl_slope;
|
|
16404
|
+
const inter = isNaN(nvImage.hdr.scl_inter) ? 0 : nvImage.hdr.scl_inter;
|
|
16405
|
+
if (dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
|
|
16406
|
+
if (!(outputImg instanceof Float32Array)) {
|
|
16407
|
+
log.warn(`getVolumeData: Converting output to Float32 for scaling type '${dataType}'.`);
|
|
16408
|
+
outputImg = Float32Array.from(outputImg);
|
|
16409
|
+
}
|
|
16410
|
+
for (let i = 0; i < outputImg.length; i++) {
|
|
16411
|
+
outputImg[i] = outputImg[i] * slope + inter;
|
|
16412
|
+
}
|
|
16413
|
+
}
|
|
16414
|
+
if (dataType === "normalized" || dataType === "windowed") {
|
|
16415
|
+
let minVal = nvImage.cal_min;
|
|
16416
|
+
let maxVal = nvImage.cal_max;
|
|
16417
|
+
if (dataType === "normalized") {
|
|
16418
|
+
minVal = nvImage.global_min;
|
|
16419
|
+
maxVal = nvImage.global_max;
|
|
16420
|
+
}
|
|
16421
|
+
const range = maxVal - minVal;
|
|
16422
|
+
const scale = range === 0 ? 0 : 1 / range;
|
|
16423
|
+
for (let i = 0; i < outputImg.length; i++) {
|
|
16424
|
+
outputImg[i] = (outputImg[i] - minVal) * scale;
|
|
16425
|
+
outputImg[i] = Math.max(0, Math.min(outputImg[i], 1));
|
|
16426
|
+
}
|
|
16427
|
+
}
|
|
16428
|
+
return [outputImg, slabDims];
|
|
16429
|
+
}
|
|
16430
|
+
function setVolumeData(nvImage, voxStartRAS = [-1, 0, 0], voxEndRAS = [0, 0, 0], slabData = new Uint8Array()) {
|
|
16431
|
+
if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {
|
|
16432
|
+
log.error("setVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).");
|
|
16433
|
+
return;
|
|
16434
|
+
}
|
|
16435
|
+
if (slabData.length < 1) {
|
|
16436
|
+
log.warn("setVolumeData: Input slabData is empty.");
|
|
16437
|
+
return;
|
|
16438
|
+
}
|
|
16439
|
+
voxStartRAS = voxStartRAS.slice(0, 3);
|
|
16440
|
+
voxEndRAS = voxEndRAS.slice(0, 3);
|
|
16441
|
+
if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {
|
|
16442
|
+
log.warn("setVolumeData: Invalid start or end coordinates provided.");
|
|
16443
|
+
return;
|
|
16444
|
+
}
|
|
16445
|
+
const dimsRAS = nvImage.dimsRAS.slice(1, 4);
|
|
16446
|
+
for (let i = 0; i < 3; i++) {
|
|
16447
|
+
voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1));
|
|
16448
|
+
voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1));
|
|
16449
|
+
if (voxEndRAS[i] < voxStartRAS[i]) {
|
|
16450
|
+
const tmp = voxEndRAS[i];
|
|
16451
|
+
voxEndRAS[i] = voxStartRAS[i];
|
|
16452
|
+
voxStartRAS[i] = tmp;
|
|
16453
|
+
}
|
|
16454
|
+
}
|
|
16455
|
+
const slabDims = [
|
|
16456
|
+
voxEndRAS[0] - voxStartRAS[0] + 1,
|
|
16457
|
+
voxEndRAS[1] - voxStartRAS[1] + 1,
|
|
16458
|
+
voxEndRAS[2] - voxStartRAS[2] + 1
|
|
16459
|
+
];
|
|
16460
|
+
const slabNVox = slabDims[0] * slabDims[1] * slabDims[2];
|
|
16461
|
+
if (slabNVox <= 0) {
|
|
16462
|
+
log.warn("setVolumeData: Calculated slab size is zero or negative.");
|
|
16463
|
+
return;
|
|
16464
|
+
}
|
|
16465
|
+
if (slabData.length < slabNVox) {
|
|
16466
|
+
log.error(
|
|
16467
|
+
`setVolumeData: Input slabData length (${slabData.length}) is less than the calculated slab size (${slabNVox}).`
|
|
16468
|
+
);
|
|
16469
|
+
return;
|
|
16470
|
+
}
|
|
16471
|
+
const step = nvImage.img2RASstep;
|
|
16472
|
+
const start = nvImage.img2RASstart;
|
|
16473
|
+
const targetImg = nvImage.img;
|
|
16474
|
+
let sourceIndex = 0;
|
|
16475
|
+
for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {
|
|
16476
|
+
const zi = start[2] + rz * step[2];
|
|
16477
|
+
for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {
|
|
16478
|
+
const yi = start[1] + ry * step[1];
|
|
16479
|
+
for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {
|
|
16480
|
+
const xi = start[0] + rx * step[0];
|
|
16481
|
+
const nativeIndex = xi + yi + zi;
|
|
16482
|
+
if (nativeIndex >= 0 && nativeIndex < targetImg.length) {
|
|
16483
|
+
targetImg[nativeIndex] = slabData[sourceIndex];
|
|
16484
|
+
} else {
|
|
16485
|
+
}
|
|
16486
|
+
sourceIndex++;
|
|
16487
|
+
}
|
|
16488
|
+
}
|
|
16489
|
+
}
|
|
16490
|
+
}
|
|
16491
|
+
|
|
16492
|
+
// src/nvimage/ImageReaders/mgh.ts
|
|
16493
|
+
var mgh_exports = {};
|
|
16494
|
+
__export(mgh_exports, {
|
|
16495
|
+
readMgh: () => readMgh
|
|
16496
|
+
});
|
|
16497
|
+
import { NIFTI1 as NIFTI12 } from "nifti-reader-js";
|
|
16498
|
+
import { mat4 as mat44, vec4 as vec44, vec3 as vec36 } from "gl-matrix";
|
|
16499
|
+
async function readMgh(nvImage, buffer) {
|
|
16500
|
+
if (!nvImage.hdr) {
|
|
16501
|
+
log.warn("readMgh called before nvImage.hdr was initialized. Creating default.");
|
|
16502
|
+
nvImage.hdr = new NIFTI12();
|
|
16503
|
+
}
|
|
16504
|
+
const hdr = nvImage.hdr;
|
|
16505
|
+
hdr.littleEndian = false;
|
|
16506
|
+
let raw = buffer;
|
|
16507
|
+
let reader = new DataView(raw);
|
|
16508
|
+
if (raw.byteLength >= 2 && reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {
|
|
16509
|
+
try {
|
|
16510
|
+
raw = await NVUtilities.decompressToBuffer(new Uint8Array(buffer));
|
|
16511
|
+
reader = new DataView(raw);
|
|
16512
|
+
} catch (err) {
|
|
16513
|
+
log.error("Failed to decompress MGZ file.", err);
|
|
16514
|
+
return null;
|
|
16515
|
+
}
|
|
16516
|
+
}
|
|
16517
|
+
if (raw.byteLength < 284) {
|
|
16518
|
+
log.error("File too small to be a valid MGH/MGZ header.");
|
|
16519
|
+
return null;
|
|
16520
|
+
}
|
|
16521
|
+
const version2 = reader.getInt32(0, false);
|
|
16522
|
+
const width = reader.getInt32(4, false);
|
|
16523
|
+
const height = reader.getInt32(8, false);
|
|
16524
|
+
const depth = reader.getInt32(12, false);
|
|
16525
|
+
const nframes = reader.getInt32(16, false);
|
|
16526
|
+
const mtype = reader.getInt32(20, false);
|
|
16527
|
+
const spacingX = reader.getFloat32(30, false);
|
|
16528
|
+
const spacingY = reader.getFloat32(34, false);
|
|
16529
|
+
const spacingZ = reader.getFloat32(38, false);
|
|
16530
|
+
const xr = reader.getFloat32(42, false);
|
|
16531
|
+
const xa = reader.getFloat32(46, false);
|
|
16532
|
+
const xs = reader.getFloat32(50, false);
|
|
16533
|
+
const yr = reader.getFloat32(54, false);
|
|
16534
|
+
const ya = reader.getFloat32(58, false);
|
|
16535
|
+
const ys = reader.getFloat32(62, false);
|
|
16536
|
+
const zr = reader.getFloat32(66, false);
|
|
16537
|
+
const za = reader.getFloat32(70, false);
|
|
16538
|
+
const zs = reader.getFloat32(74, false);
|
|
16539
|
+
const cr = reader.getFloat32(78, false);
|
|
16540
|
+
const ca = reader.getFloat32(82, false);
|
|
16541
|
+
const cs = reader.getFloat32(86, false);
|
|
16542
|
+
if (version2 !== 1) {
|
|
16543
|
+
log.warn(`Unexpected MGH version: ${version2}.`);
|
|
16544
|
+
}
|
|
16545
|
+
if (width <= 0 || height <= 0 || depth <= 0) {
|
|
16546
|
+
log.error(`Invalid MGH dimensions: ${width}x${height}x${depth}`);
|
|
16547
|
+
return null;
|
|
16548
|
+
}
|
|
16549
|
+
switch (mtype) {
|
|
16550
|
+
case 0:
|
|
16551
|
+
hdr.numBitsPerVoxel = 8;
|
|
16552
|
+
hdr.datatypeCode = 2 /* DT_UINT8 */;
|
|
16553
|
+
break;
|
|
16554
|
+
case 4:
|
|
16555
|
+
hdr.numBitsPerVoxel = 16;
|
|
16556
|
+
hdr.datatypeCode = 4 /* DT_INT16 */;
|
|
16557
|
+
break;
|
|
16558
|
+
case 1:
|
|
16559
|
+
hdr.numBitsPerVoxel = 32;
|
|
16560
|
+
hdr.datatypeCode = 8 /* DT_INT32 */;
|
|
16561
|
+
break;
|
|
16562
|
+
case 3:
|
|
16563
|
+
hdr.numBitsPerVoxel = 32;
|
|
16564
|
+
hdr.datatypeCode = 16 /* DT_FLOAT32 */;
|
|
16565
|
+
break;
|
|
16566
|
+
default:
|
|
16567
|
+
log.error(`Unsupported MGH data type: ${mtype}`);
|
|
16568
|
+
return null;
|
|
16569
|
+
}
|
|
16570
|
+
hdr.dims[1] = width;
|
|
16571
|
+
hdr.dims[2] = height;
|
|
16572
|
+
hdr.dims[3] = depth;
|
|
16573
|
+
hdr.dims[4] = Math.max(1, nframes);
|
|
16574
|
+
hdr.dims[0] = hdr.dims[4] > 1 ? 4 : 3;
|
|
16575
|
+
hdr.pixDims[1] = Math.abs(spacingX);
|
|
16576
|
+
hdr.pixDims[2] = Math.abs(spacingY);
|
|
16577
|
+
hdr.pixDims[3] = Math.abs(spacingZ);
|
|
16578
|
+
hdr.pixDims[4] = 0;
|
|
16579
|
+
hdr.sform_code = 1;
|
|
16580
|
+
hdr.qform_code = 0;
|
|
16581
|
+
const rot44 = mat44.fromValues(
|
|
16582
|
+
xr * hdr.pixDims[1],
|
|
16583
|
+
yr * hdr.pixDims[2],
|
|
16584
|
+
zr * hdr.pixDims[3],
|
|
16585
|
+
0,
|
|
16586
|
+
xa * hdr.pixDims[1],
|
|
16587
|
+
ya * hdr.pixDims[2],
|
|
16588
|
+
za * hdr.pixDims[3],
|
|
16589
|
+
0,
|
|
16590
|
+
xs * hdr.pixDims[1],
|
|
16591
|
+
ys * hdr.pixDims[2],
|
|
16592
|
+
zs * hdr.pixDims[3],
|
|
16593
|
+
0,
|
|
16594
|
+
0,
|
|
16595
|
+
0,
|
|
16596
|
+
0,
|
|
16597
|
+
1
|
|
16598
|
+
);
|
|
16599
|
+
const PcrsVec = vec44.fromValues(hdr.dims[1] / 2, hdr.dims[2] / 2, hdr.dims[3] / 2, 1);
|
|
16600
|
+
const PxyzOffsetVec = vec44.create();
|
|
16601
|
+
vec44.transformMat4(PxyzOffsetVec, PcrsVec, rot44);
|
|
16602
|
+
const translation = vec36.fromValues(cr - PxyzOffsetVec[0], ca - PxyzOffsetVec[1], cs - PxyzOffsetVec[2]);
|
|
16603
|
+
hdr.affine = [
|
|
16604
|
+
[rot44[0], rot44[1], rot44[2], translation[0]],
|
|
16605
|
+
[rot44[4], rot44[5], rot44[6], translation[1]],
|
|
16606
|
+
[rot44[8], rot44[9], rot44[10], translation[2]],
|
|
16607
|
+
[0, 0, 0, 1]
|
|
16608
|
+
];
|
|
16609
|
+
hdr.vox_offset = 284;
|
|
16610
|
+
hdr.magic = "n+1";
|
|
16611
|
+
const nBytesPerVoxel = hdr.numBitsPerVoxel / 8;
|
|
16612
|
+
const nVoxels = width * height * depth * hdr.dims[4];
|
|
16613
|
+
const expectedBytes = nVoxels * nBytesPerVoxel;
|
|
16614
|
+
const remainingBytes = raw.byteLength - hdr.vox_offset;
|
|
16615
|
+
if (remainingBytes < expectedBytes) {
|
|
16616
|
+
log.error(`MGH image data size mismatch: expected ${expectedBytes}, found ${remainingBytes}`);
|
|
16617
|
+
return null;
|
|
16618
|
+
} else if (remainingBytes > expectedBytes) {
|
|
16619
|
+
log.warn(`MGH file has extra ${remainingBytes - expectedBytes} bytes after image data. Truncating.`);
|
|
16620
|
+
}
|
|
16621
|
+
const imgRaw = raw.slice(hdr.vox_offset, hdr.vox_offset + expectedBytes);
|
|
16622
|
+
return imgRaw;
|
|
16623
|
+
}
|
|
16624
|
+
|
|
16625
|
+
// src/nvimage/ImageReaders/nii.ts
|
|
16626
|
+
var nii_exports = {};
|
|
16627
|
+
__export(nii_exports, {
|
|
16628
|
+
readNifti: () => readNifti
|
|
16629
|
+
});
|
|
16630
|
+
import { isCompressed, decompressAsync, readHeaderAsync, readImage } from "nifti-reader-js";
|
|
16631
|
+
async function readNifti(nvImage, buffer) {
|
|
16632
|
+
let dataBuffer = buffer;
|
|
16633
|
+
let imgRaw = null;
|
|
16634
|
+
try {
|
|
16635
|
+
if (isCompressed(dataBuffer)) {
|
|
16636
|
+
log.debug(`Decompressing NIfTI file: ${nvImage.name}`);
|
|
16637
|
+
dataBuffer = await decompressAsync(dataBuffer);
|
|
16638
|
+
log.debug(`Decompression complete for: ${nvImage.name}`);
|
|
16639
|
+
}
|
|
16640
|
+
if (!dataBuffer || dataBuffer.byteLength === 0) {
|
|
16641
|
+
throw new Error("Buffer became invalid after decompression attempt.");
|
|
16642
|
+
}
|
|
16643
|
+
nvImage.hdr = await readHeaderAsync(dataBuffer);
|
|
16644
|
+
if (nvImage.hdr === null) {
|
|
16645
|
+
throw new Error(`Failed to read NIfTI header: ${nvImage.name}`);
|
|
16646
|
+
}
|
|
16647
|
+
if (nvImage.hdr.cal_min === 0 && nvImage.hdr.cal_max === 255 && nvImage.hdr.datatypeCode !== 2 /* DT_UINT8 */) {
|
|
16648
|
+
log.debug(`Resetting suspicious cal_min/max (0/255) for non-uint8 NIfTI: ${nvImage.name}`);
|
|
16649
|
+
nvImage.hdr.cal_min = 0;
|
|
16650
|
+
nvImage.hdr.cal_max = 0;
|
|
16651
|
+
}
|
|
16652
|
+
imgRaw = readImage(nvImage.hdr, dataBuffer);
|
|
16653
|
+
if (imgRaw === null) {
|
|
16654
|
+
throw new Error(`nifti-reader-js readImage returned null for ${nvImage.name}`);
|
|
16655
|
+
}
|
|
16656
|
+
return imgRaw;
|
|
16657
|
+
} catch (err) {
|
|
16658
|
+
log.error(`Error processing NIfTI file ${nvImage.name}:`, err);
|
|
16659
|
+
nvImage.hdr = null;
|
|
16660
|
+
return null;
|
|
16661
|
+
}
|
|
16662
|
+
}
|
|
16663
|
+
|
|
16664
|
+
// src/nvimage/ImageReaders/nrrd.ts
|
|
16665
|
+
var nrrd_exports = {};
|
|
16666
|
+
__export(nrrd_exports, {
|
|
16667
|
+
readNrrd: () => readNrrd
|
|
16668
|
+
});
|
|
16669
|
+
import { NIFTI1 as NIFTI13 } from "nifti-reader-js";
|
|
16670
|
+
import { mat3, mat4 as mat45, vec3 as vec37 } from "gl-matrix";
|
|
16671
|
+
async function readNrrd(nvImage, dataBuffer, pairedImgData = null) {
|
|
16672
|
+
if (!nvImage.hdr) {
|
|
16673
|
+
log.warn("readNrrd called before nvImage.hdr was initialized. Creating default.");
|
|
16674
|
+
nvImage.hdr = new NIFTI13();
|
|
16675
|
+
}
|
|
16676
|
+
const hdr = nvImage.hdr;
|
|
16677
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
16678
|
+
const len = dataBuffer.byteLength;
|
|
16679
|
+
let txt = null;
|
|
16680
|
+
const bytes = new Uint8Array(dataBuffer);
|
|
16681
|
+
for (let i = 1; i < len; i++) {
|
|
16682
|
+
if (bytes[i - 1] === 10 && bytes[i] === 10) {
|
|
16683
|
+
const v = dataBuffer.slice(0, i - 1);
|
|
16684
|
+
txt = new TextDecoder().decode(v);
|
|
16685
|
+
hdr.vox_offset = i + 1;
|
|
16686
|
+
break;
|
|
16687
|
+
}
|
|
16688
|
+
}
|
|
16689
|
+
if (txt === null) {
|
|
16690
|
+
log.error("readNrrd: could not extract txt");
|
|
16691
|
+
return null;
|
|
16692
|
+
}
|
|
16693
|
+
const lines = txt.split("\n");
|
|
16694
|
+
if (!lines[0].startsWith("NRRD")) {
|
|
16695
|
+
log.error("Invalid NRRD image (magic signature missing)");
|
|
16696
|
+
return null;
|
|
16697
|
+
}
|
|
16698
|
+
const n = lines.length;
|
|
16699
|
+
let isGz = false;
|
|
16700
|
+
let isMicron = false;
|
|
16701
|
+
let isDetached = false;
|
|
16702
|
+
const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
|
|
16703
|
+
const offset = vec37.fromValues(0, 0, 0);
|
|
16704
|
+
let rot33 = mat3.create();
|
|
16705
|
+
for (let i = 1; i < n; i++) {
|
|
16706
|
+
let str = lines[i];
|
|
16707
|
+
if (str.length === 0 || str[0] === "#") {
|
|
16708
|
+
if (str.startsWith("#")) {
|
|
16709
|
+
continue;
|
|
16710
|
+
}
|
|
16711
|
+
if (str.trim().length === 0) {
|
|
16712
|
+
continue;
|
|
16713
|
+
}
|
|
16714
|
+
}
|
|
16715
|
+
str = str.toLowerCase();
|
|
16716
|
+
const items = str.split(":");
|
|
16717
|
+
if (items.length < 2) {
|
|
16718
|
+
continue;
|
|
16719
|
+
}
|
|
16720
|
+
const key = items[0].trim();
|
|
16721
|
+
let value = items[1].trim();
|
|
16722
|
+
value = value.replaceAll(")", " ");
|
|
16723
|
+
value = value.replaceAll("(", " ");
|
|
16724
|
+
value = value.trim();
|
|
16725
|
+
switch (key) {
|
|
16726
|
+
case "data file":
|
|
16727
|
+
isDetached = true;
|
|
16728
|
+
break;
|
|
16729
|
+
case "encoding":
|
|
16730
|
+
if (value.includes("raw")) {
|
|
16731
|
+
isGz = false;
|
|
16732
|
+
} else if (value.includes("gz")) {
|
|
16733
|
+
isGz = true;
|
|
16734
|
+
} else {
|
|
16735
|
+
log.error("Unsupported NRRD encoding");
|
|
16736
|
+
return null;
|
|
16737
|
+
}
|
|
16738
|
+
break;
|
|
16739
|
+
case "type":
|
|
16740
|
+
switch (value) {
|
|
16741
|
+
case "uchar":
|
|
16742
|
+
case "unsigned char":
|
|
16743
|
+
case "uint8":
|
|
16744
|
+
case "uint8_t":
|
|
16745
|
+
hdr.numBitsPerVoxel = 8;
|
|
16746
|
+
hdr.datatypeCode = 2 /* DT_UINT8 */;
|
|
16747
|
+
break;
|
|
16748
|
+
case "signed char":
|
|
16749
|
+
case "int8":
|
|
16750
|
+
case "int8_t":
|
|
16751
|
+
hdr.numBitsPerVoxel = 8;
|
|
16752
|
+
hdr.datatypeCode = 256 /* DT_INT8 */;
|
|
16753
|
+
break;
|
|
16754
|
+
case "short":
|
|
16755
|
+
case "short int":
|
|
16756
|
+
case "signed short":
|
|
16757
|
+
case "signed short int":
|
|
16758
|
+
case "int16":
|
|
16759
|
+
case "int16_t":
|
|
16760
|
+
hdr.numBitsPerVoxel = 16;
|
|
16761
|
+
hdr.datatypeCode = 4 /* DT_INT16 */;
|
|
16762
|
+
break;
|
|
16763
|
+
case "ushort":
|
|
16764
|
+
case "unsigned short":
|
|
16765
|
+
case "unsigned short int":
|
|
16766
|
+
case "uint16":
|
|
16767
|
+
case "uint16_t":
|
|
16768
|
+
hdr.numBitsPerVoxel = 16;
|
|
16769
|
+
hdr.datatypeCode = 512 /* DT_UINT16 */;
|
|
16770
|
+
break;
|
|
16771
|
+
case "int":
|
|
16772
|
+
case "signed int":
|
|
16773
|
+
case "int32":
|
|
16774
|
+
case "int32_t":
|
|
16775
|
+
hdr.numBitsPerVoxel = 32;
|
|
16776
|
+
hdr.datatypeCode = 8 /* DT_INT32 */;
|
|
16777
|
+
break;
|
|
16778
|
+
case "uint":
|
|
16779
|
+
case "unsigned int":
|
|
16780
|
+
case "uint32":
|
|
16781
|
+
case "uint32_t":
|
|
16782
|
+
hdr.numBitsPerVoxel = 32;
|
|
16783
|
+
hdr.datatypeCode = 768 /* DT_UINT32 */;
|
|
16784
|
+
break;
|
|
16785
|
+
case "float":
|
|
16786
|
+
hdr.numBitsPerVoxel = 32;
|
|
16787
|
+
hdr.datatypeCode = 16 /* DT_FLOAT32 */;
|
|
16788
|
+
break;
|
|
16789
|
+
case "double":
|
|
16790
|
+
hdr.numBitsPerVoxel = 64;
|
|
16791
|
+
hdr.datatypeCode = 64 /* DT_FLOAT64 */;
|
|
16792
|
+
break;
|
|
16793
|
+
default:
|
|
16794
|
+
log.error("Unsupported NRRD data type: " + value);
|
|
16795
|
+
return null;
|
|
16796
|
+
}
|
|
16797
|
+
break;
|
|
16798
|
+
case "spacings":
|
|
16799
|
+
{
|
|
16800
|
+
const values = value.split(/[ ,]+/);
|
|
16801
|
+
for (let d = 0; d < values.length; d++) {
|
|
16802
|
+
hdr.pixDims[d + 1] = parseFloat(values[d]);
|
|
16803
|
+
}
|
|
16804
|
+
}
|
|
16805
|
+
break;
|
|
16806
|
+
case "sizes":
|
|
16807
|
+
{
|
|
16808
|
+
const dims = value.split(/[ ,]+/);
|
|
16809
|
+
hdr.dims[0] = dims.length;
|
|
16810
|
+
for (let d = 0; d < dims.length; d++) {
|
|
16811
|
+
hdr.dims[d + 1] = parseInt(dims[d]);
|
|
16812
|
+
}
|
|
16813
|
+
}
|
|
16814
|
+
break;
|
|
16815
|
+
case "endian":
|
|
16816
|
+
if (value.includes("little")) {
|
|
16817
|
+
hdr.littleEndian = true;
|
|
16818
|
+
} else if (value.includes("big")) {
|
|
16819
|
+
hdr.littleEndian = false;
|
|
16820
|
+
}
|
|
16821
|
+
break;
|
|
16822
|
+
case "space directions":
|
|
16823
|
+
{
|
|
16824
|
+
const vs = value.split(/[ ,]+/);
|
|
16825
|
+
if (vs.length === 9) {
|
|
16826
|
+
for (let d = 0; d < 9; d++) {
|
|
16827
|
+
mat33[d] = parseFloat(vs[d]);
|
|
16828
|
+
}
|
|
16829
|
+
}
|
|
16830
|
+
}
|
|
16831
|
+
break;
|
|
16832
|
+
case "space origin":
|
|
16833
|
+
{
|
|
16834
|
+
const ts = value.split(/[ ,]+/);
|
|
16835
|
+
if (ts.length === 3) {
|
|
16836
|
+
offset[0] = parseFloat(ts[0]);
|
|
16837
|
+
offset[1] = parseFloat(ts[1]);
|
|
16838
|
+
offset[2] = parseFloat(ts[2]);
|
|
16839
|
+
}
|
|
16840
|
+
}
|
|
16841
|
+
break;
|
|
16842
|
+
case "space units":
|
|
16843
|
+
if (value.includes("microns")) {
|
|
16844
|
+
isMicron = true;
|
|
16845
|
+
}
|
|
16846
|
+
break;
|
|
16847
|
+
case "space":
|
|
16848
|
+
if (value.includes("right-anterior-superior") || value.includes("ras")) {
|
|
16849
|
+
rot33 = mat3.fromValues(1, 0, 0, 0, 1, 0, 0, 0, 1);
|
|
16850
|
+
} else if (value.includes("left-anterior-superior") || value.includes("las")) {
|
|
16851
|
+
rot33 = mat3.fromValues(-1, 0, 0, 0, 1, 0, 0, 0, 1);
|
|
16852
|
+
} else if (value.includes("left-posterior-superior") || value.includes("lps")) {
|
|
16853
|
+
rot33 = mat3.fromValues(-1, 0, 0, 0, -1, 0, 0, 0, 1);
|
|
16854
|
+
} else {
|
|
16855
|
+
log.warn("Unsupported NRRD space value:", value);
|
|
16856
|
+
}
|
|
16857
|
+
break;
|
|
16858
|
+
default:
|
|
16859
|
+
log.warn("Unknown:", key);
|
|
16860
|
+
break;
|
|
16861
|
+
}
|
|
16862
|
+
}
|
|
16863
|
+
if (!isNaN(mat33[0])) {
|
|
16864
|
+
hdr.sform_code = 2;
|
|
16865
|
+
if (isMicron) {
|
|
16866
|
+
mat45.multiplyScalar(mat33, mat33, 1e-3);
|
|
16867
|
+
offset[0] *= 1e-3;
|
|
16868
|
+
offset[1] *= 1e-3;
|
|
16869
|
+
offset[2] *= 1e-3;
|
|
16870
|
+
}
|
|
16871
|
+
if (rot33[0] < 0) {
|
|
16872
|
+
offset[0] = -offset[0];
|
|
16873
|
+
}
|
|
16874
|
+
if (rot33[4] < 0) {
|
|
16875
|
+
offset[1] = -offset[1];
|
|
16876
|
+
}
|
|
16877
|
+
if (rot33[8] < 0) {
|
|
16878
|
+
offset[2] = -offset[2];
|
|
16879
|
+
}
|
|
16880
|
+
mat3.multiply(mat33, rot33, mat33);
|
|
16881
|
+
const mat = mat45.fromValues(
|
|
16882
|
+
mat33[0],
|
|
16883
|
+
mat33[3],
|
|
16884
|
+
mat33[6],
|
|
16885
|
+
offset[0],
|
|
16886
|
+
mat33[1],
|
|
16887
|
+
mat33[4],
|
|
16888
|
+
mat33[7],
|
|
16889
|
+
offset[1],
|
|
16890
|
+
mat33[2],
|
|
16891
|
+
mat33[5],
|
|
16892
|
+
mat33[8],
|
|
16893
|
+
offset[2],
|
|
16894
|
+
0,
|
|
16895
|
+
0,
|
|
16896
|
+
0,
|
|
16897
|
+
1
|
|
16898
|
+
);
|
|
16899
|
+
if (!nvImage.vox2mm) {
|
|
16900
|
+
return null;
|
|
16901
|
+
}
|
|
16902
|
+
const mm000 = nvImage.vox2mm([0, 0, 0], mat);
|
|
16903
|
+
const mm100 = nvImage.vox2mm([1, 0, 0], mat);
|
|
16904
|
+
vec37.subtract(mm100, mm100, mm000);
|
|
16905
|
+
const mm010 = nvImage.vox2mm([0, 1, 0], mat);
|
|
16906
|
+
vec37.subtract(mm010, mm010, mm000);
|
|
16907
|
+
const mm001 = nvImage.vox2mm([0, 0, 1], mat);
|
|
16908
|
+
vec37.subtract(mm001, mm001, mm000);
|
|
16909
|
+
hdr.pixDims[1] = vec37.length(mm100);
|
|
16910
|
+
hdr.pixDims[2] = vec37.length(mm010);
|
|
16911
|
+
hdr.pixDims[3] = vec37.length(mm001);
|
|
16912
|
+
hdr.affine = [
|
|
16913
|
+
[mat[0], mat[1], mat[2], mat[3]],
|
|
16914
|
+
[mat[4], mat[5], mat[6], mat[7]],
|
|
16915
|
+
[mat[8], mat[9], mat[10], mat[11]],
|
|
16916
|
+
[0, 0, 0, 1]
|
|
16917
|
+
];
|
|
16918
|
+
}
|
|
16919
|
+
let imgRaw = null;
|
|
16920
|
+
const sourceBuffer = isDetached ? pairedImgData : dataBuffer;
|
|
16921
|
+
const sourceOffset = isDetached ? 0 : hdr.vox_offset;
|
|
16922
|
+
if (isDetached && !sourceBuffer) {
|
|
16923
|
+
log.warn("Missing data: NRRD header describes detached data file but only one URL provided");
|
|
16924
|
+
return null;
|
|
16925
|
+
}
|
|
16926
|
+
if (!sourceBuffer || sourceOffset >= sourceBuffer.byteLength) {
|
|
16927
|
+
log.error(`NRRD data offset (${sourceOffset}) invalid for buffer length (${sourceBuffer?.byteLength ?? 0})`);
|
|
16928
|
+
return null;
|
|
16929
|
+
}
|
|
16930
|
+
let dataSection = sourceBuffer.slice(sourceOffset);
|
|
16931
|
+
if (isGz) {
|
|
16932
|
+
try {
|
|
16933
|
+
log.debug("Decompressing NRRD data...");
|
|
16934
|
+
dataSection = await NVUtilities.decompressToBuffer(new Uint8Array(dataSection));
|
|
16935
|
+
log.debug("Decompression complete.");
|
|
16936
|
+
} catch (err) {
|
|
16937
|
+
log.error("Failed to decompress NRRD data.", err);
|
|
16938
|
+
return null;
|
|
16939
|
+
}
|
|
16940
|
+
}
|
|
16941
|
+
const nBytesPerVoxel = hdr.numBitsPerVoxel / 8;
|
|
16942
|
+
const nVoxels = hdr.dims.slice(1, hdr.dims[0] + 1).reduce((acc, dim) => acc * Math.max(1, dim), 1);
|
|
16943
|
+
const expectedBytes = nVoxels * nBytesPerVoxel;
|
|
16944
|
+
if (dataSection.byteLength < expectedBytes) {
|
|
16945
|
+
log.error(`NRRD image data size mismatch: expected ${expectedBytes}, found ${dataSection.byteLength}`);
|
|
16946
|
+
return null;
|
|
16947
|
+
} else if (dataSection.byteLength > expectedBytes) {
|
|
16948
|
+
log.warn(`NRRD has extra ${dataSection.byteLength - expectedBytes} bytes after expected image data. Truncating.`);
|
|
16949
|
+
dataSection = dataSection.slice(0, expectedBytes);
|
|
16950
|
+
}
|
|
16951
|
+
imgRaw = dataSection;
|
|
16952
|
+
if (!hdr.datatypeCode) {
|
|
16953
|
+
log.error("NRRD parsing failed to set datatypeCode.");
|
|
16954
|
+
return null;
|
|
16955
|
+
}
|
|
16956
|
+
if (!hdr.numBitsPerVoxel) {
|
|
16957
|
+
log.error("NRRD parsing failed to set numBitsPerVoxel.");
|
|
16958
|
+
return null;
|
|
16959
|
+
}
|
|
16960
|
+
return imgRaw;
|
|
16961
|
+
}
|
|
16962
|
+
|
|
16076
16963
|
// src/nvimage/index.ts
|
|
16077
16964
|
var NVImage = class _NVImage {
|
|
16078
16965
|
/**
|
|
@@ -16248,7 +17135,7 @@ var NVImage = class _NVImage {
|
|
|
16248
17135
|
this.nFrame4D = nVol4D;
|
|
16249
17136
|
}
|
|
16250
17137
|
if ((this.hdr.intent_code === 1007 /* NIFTI_INTENT_VECTOR */ || this.hdr.intent_code === 2003 /* NIFTI_INTENT_RGB_VECTOR */) && this.nFrame4D === 3 && this.hdr.datatypeCode === 16 /* DT_FLOAT32 */) {
|
|
16251
|
-
imgRaw = this.float32V1asRGBA(new Float32Array(imgRaw));
|
|
17138
|
+
imgRaw = this.float32V1asRGBA(new Float32Array(imgRaw)).buffer;
|
|
16252
17139
|
}
|
|
16253
17140
|
if (this.hdr.pixDims[1] === 0 || this.hdr.pixDims[2] === 0 || this.hdr.pixDims[3] === 0) {
|
|
16254
17141
|
log.error("pixDims not plausible", this.hdr);
|
|
@@ -16486,7 +17373,10 @@ var NVImage = class _NVImage {
|
|
|
16486
17373
|
break;
|
|
16487
17374
|
case NVIMAGE_TYPE.NHDR:
|
|
16488
17375
|
case NVIMAGE_TYPE.NRRD:
|
|
16489
|
-
imgRaw = await
|
|
17376
|
+
imgRaw = await nrrd_exports.readNrrd(newImg, dataBuffer);
|
|
17377
|
+
if (imgRaw === null) {
|
|
17378
|
+
throw new Error(`Failed to parse NHDR/NRRD file ${name}`);
|
|
17379
|
+
}
|
|
16490
17380
|
break;
|
|
16491
17381
|
case NVIMAGE_TYPE.MHD:
|
|
16492
17382
|
case NVIMAGE_TYPE.MHA:
|
|
@@ -16494,7 +17384,10 @@ var NVImage = class _NVImage {
|
|
|
16494
17384
|
break;
|
|
16495
17385
|
case NVIMAGE_TYPE.MGH:
|
|
16496
17386
|
case NVIMAGE_TYPE.MGZ:
|
|
16497
|
-
imgRaw = await
|
|
17387
|
+
imgRaw = await mgh_exports.readMgh(newImg, dataBuffer);
|
|
17388
|
+
if (imgRaw === null) {
|
|
17389
|
+
throw new Error(`Failed to parse MGH/MGZ file ${name}`);
|
|
17390
|
+
}
|
|
16498
17391
|
break;
|
|
16499
17392
|
case NVIMAGE_TYPE.SRC:
|
|
16500
17393
|
imgRaw = await newImg.readSRC(dataBuffer);
|
|
@@ -16514,20 +17407,18 @@ var NVImage = class _NVImage {
|
|
|
16514
17407
|
case NVIMAGE_TYPE.BMP:
|
|
16515
17408
|
imgRaw = await newImg.readBMP(dataBuffer);
|
|
16516
17409
|
break;
|
|
17410
|
+
case NVIMAGE_TYPE.NPY:
|
|
17411
|
+
imgRaw = await newImg.readNPY(dataBuffer);
|
|
17412
|
+
break;
|
|
17413
|
+
case NVIMAGE_TYPE.NPZ:
|
|
17414
|
+
imgRaw = await newImg.readNPZ(dataBuffer);
|
|
17415
|
+
break;
|
|
17416
|
+
case NVIMAGE_TYPE.ZARR:
|
|
17417
|
+
throw new Error("Image type ZARR not (yet) supported");
|
|
16517
17418
|
case NVIMAGE_TYPE.NII:
|
|
16518
|
-
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
-
newImg.hdr = await readHeaderAsync(dataBuffer);
|
|
16522
|
-
if (newImg.hdr !== null) {
|
|
16523
|
-
if (newImg.hdr.cal_min === 0 && newImg.hdr.cal_max === 255) {
|
|
16524
|
-
newImg.hdr.cal_max = 0;
|
|
16525
|
-
}
|
|
16526
|
-
if (isCompressed(dataBuffer)) {
|
|
16527
|
-
imgRaw = readImage(newImg.hdr, await decompressAsync(dataBuffer));
|
|
16528
|
-
} else {
|
|
16529
|
-
imgRaw = readImage(newImg.hdr, dataBuffer);
|
|
16530
|
-
}
|
|
17419
|
+
imgRaw = await nii_exports.readNifti(newImg, dataBuffer);
|
|
17420
|
+
if (imgRaw === null) {
|
|
17421
|
+
throw new Error(`Failed to parse NIfTI file ${name}.`);
|
|
16531
17422
|
}
|
|
16532
17423
|
break;
|
|
16533
17424
|
default:
|
|
@@ -16561,8 +17452,8 @@ var NVImage = class _NVImage {
|
|
|
16561
17452
|
// detect difference between voxel grid and world space
|
|
16562
17453
|
// https://github.com/afni/afni/blob/25e77d564f2c67ff480fa99a7b8e48ec2d9a89fc/src/thd_coords.c#L717
|
|
16563
17454
|
computeObliqueAngle(mtx44) {
|
|
16564
|
-
const mtx =
|
|
16565
|
-
|
|
17455
|
+
const mtx = mat46.clone(mtx44);
|
|
17456
|
+
mat46.transpose(mtx, mtx44);
|
|
16566
17457
|
const dxtmp = Math.sqrt(mtx[0] * mtx[0] + mtx[1] * mtx[1] + mtx[2] * mtx[2]);
|
|
16567
17458
|
const xmax = Math.max(Math.max(Math.abs(mtx[0]), Math.abs(mtx[1])), Math.abs(mtx[2])) / dxtmp;
|
|
16568
17459
|
const dytmp = Math.sqrt(mtx[4] * mtx[4] + mtx[5] * mtx[5] + mtx[6] * mtx[6]);
|
|
@@ -16664,10 +17555,10 @@ var NVImage = class _NVImage {
|
|
|
16664
17555
|
const X1mm = this.vox2mm([1 / this.pixDimsRAS[1], 0, 0], this.matRAS);
|
|
16665
17556
|
const Y1mm = this.vox2mm([0, 1 / this.pixDimsRAS[2], 0], this.matRAS);
|
|
16666
17557
|
const Z1mm = this.vox2mm([0, 0, 1 / this.pixDimsRAS[3]], this.matRAS);
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
const oblique =
|
|
17558
|
+
vec38.subtract(X1mm, X1mm, LPI);
|
|
17559
|
+
vec38.subtract(Y1mm, Y1mm, LPI);
|
|
17560
|
+
vec38.subtract(Z1mm, Z1mm, LPI);
|
|
17561
|
+
const oblique = mat46.fromValues(
|
|
16671
17562
|
X1mm[0],
|
|
16672
17563
|
X1mm[1],
|
|
16673
17564
|
X1mm[2],
|
|
@@ -16685,19 +17576,19 @@ var NVImage = class _NVImage {
|
|
|
16685
17576
|
0,
|
|
16686
17577
|
1
|
|
16687
17578
|
);
|
|
16688
|
-
this.obliqueRAS =
|
|
16689
|
-
const XY = Math.abs(90 -
|
|
16690
|
-
const XZ = Math.abs(90 -
|
|
16691
|
-
const YZ = Math.abs(90 -
|
|
17579
|
+
this.obliqueRAS = mat46.clone(oblique);
|
|
17580
|
+
const XY = Math.abs(90 - vec38.angle(X1mm, Y1mm) * (180 / Math.PI));
|
|
17581
|
+
const XZ = Math.abs(90 - vec38.angle(X1mm, Z1mm) * (180 / Math.PI));
|
|
17582
|
+
const YZ = Math.abs(90 - vec38.angle(Y1mm, Z1mm) * (180 / Math.PI));
|
|
16692
17583
|
this.maxShearDeg = Math.max(Math.max(XY, XZ), YZ);
|
|
16693
17584
|
if (this.maxShearDeg > 0.1) {
|
|
16694
17585
|
log.warn("Warning: voxels are rhomboidal, maximum shear is %f degrees.", this.maxShearDeg);
|
|
16695
17586
|
}
|
|
16696
|
-
const dim =
|
|
16697
|
-
const sform =
|
|
16698
|
-
|
|
16699
|
-
const shim =
|
|
16700
|
-
|
|
17587
|
+
const dim = vec45.fromValues(this.dimsRAS[1], this.dimsRAS[2], this.dimsRAS[3], 1);
|
|
17588
|
+
const sform = mat46.clone(this.matRAS);
|
|
17589
|
+
mat46.transpose(sform, sform);
|
|
17590
|
+
const shim = vec45.fromValues(-0.5, -0.5, -0.5, 0);
|
|
17591
|
+
mat46.translate(sform, sform, vec38.fromValues(shim[0], shim[1], shim[2]));
|
|
16701
17592
|
sform[0] *= dim[0];
|
|
16702
17593
|
sform[1] *= dim[0];
|
|
16703
17594
|
sform[2] *= dim[0];
|
|
@@ -16707,11 +17598,11 @@ var NVImage = class _NVImage {
|
|
|
16707
17598
|
sform[8] *= dim[2];
|
|
16708
17599
|
sform[9] *= dim[2];
|
|
16709
17600
|
sform[10] *= dim[2];
|
|
16710
|
-
this.frac2mm =
|
|
17601
|
+
this.frac2mm = mat46.clone(sform);
|
|
16711
17602
|
const pixdimX = this.pixDimsRAS[1];
|
|
16712
17603
|
const pixdimY = this.pixDimsRAS[2];
|
|
16713
17604
|
const pixdimZ = this.pixDimsRAS[3];
|
|
16714
|
-
const oform =
|
|
17605
|
+
const oform = mat46.clone(sform);
|
|
16715
17606
|
oform[0] = pixdimX * dim[0];
|
|
16716
17607
|
oform[1] = 0;
|
|
16717
17608
|
oform[2] = 0;
|
|
@@ -16725,11 +17616,11 @@ var NVImage = class _NVImage {
|
|
|
16725
17616
|
oform[12] = (-originVoxel[0] - 0.5) * pixdimX;
|
|
16726
17617
|
oform[13] = (-originVoxel[1] - 0.5) * pixdimY;
|
|
16727
17618
|
oform[14] = (-originVoxel[2] - 0.5) * pixdimZ;
|
|
16728
|
-
this.frac2mmOrtho =
|
|
17619
|
+
this.frac2mmOrtho = mat46.clone(oform);
|
|
16729
17620
|
this.extentsMinOrtho = [oform[12], oform[13], oform[14]];
|
|
16730
17621
|
this.extentsMaxOrtho = [oform[0] + oform[12], oform[5] + oform[13], oform[10] + oform[14]];
|
|
16731
|
-
this.mm2ortho =
|
|
16732
|
-
|
|
17622
|
+
this.mm2ortho = mat46.create();
|
|
17623
|
+
mat46.invert(this.mm2ortho, oblique);
|
|
16733
17624
|
}
|
|
16734
17625
|
// not included in public docs
|
|
16735
17626
|
// convert AFNI head/brik space to NIfTI format
|
|
@@ -16788,7 +17679,7 @@ var NVImage = class _NVImage {
|
|
|
16788
17679
|
throw new Error("hdr not defined");
|
|
16789
17680
|
}
|
|
16790
17681
|
const m = this.hdr.affine;
|
|
16791
|
-
const mat =
|
|
17682
|
+
const mat = mat46.fromValues(
|
|
16792
17683
|
m[0][0],
|
|
16793
17684
|
m[0][1],
|
|
16794
17685
|
m[0][2],
|
|
@@ -16808,14 +17699,14 @@ var NVImage = class _NVImage {
|
|
|
16808
17699
|
);
|
|
16809
17700
|
const mm000 = this.vox2mm([0, 0, 0], mat);
|
|
16810
17701
|
const mm100 = this.vox2mm([1, 0, 0], mat);
|
|
16811
|
-
|
|
17702
|
+
vec38.subtract(mm100, mm100, mm000);
|
|
16812
17703
|
const mm010 = this.vox2mm([0, 1, 0], mat);
|
|
16813
|
-
|
|
17704
|
+
vec38.subtract(mm010, mm010, mm000);
|
|
16814
17705
|
const mm001 = this.vox2mm([0, 0, 1], mat);
|
|
16815
|
-
|
|
16816
|
-
this.hdr.pixDims[1] =
|
|
16817
|
-
this.hdr.pixDims[2] =
|
|
16818
|
-
this.hdr.pixDims[3] =
|
|
17706
|
+
vec38.subtract(mm001, mm001, mm000);
|
|
17707
|
+
this.hdr.pixDims[1] = vec38.length(mm100);
|
|
17708
|
+
this.hdr.pixDims[2] = vec38.length(mm010);
|
|
17709
|
+
this.hdr.pixDims[3] = vec38.length(mm001);
|
|
16819
17710
|
}
|
|
16820
17711
|
// not included in public docs
|
|
16821
17712
|
// read DICOM format image and treat it like a NIfTI
|
|
@@ -16950,7 +17841,7 @@ var NVImage = class _NVImage {
|
|
|
16950
17841
|
// read ECAT7 format image
|
|
16951
17842
|
// https://github.com/openneuropet/PET2BIDS/tree/28aae3fab22309047d36d867c624cd629c921ca6/ecat_validation/ecat_info
|
|
16952
17843
|
readECAT(buffer) {
|
|
16953
|
-
this.hdr = new
|
|
17844
|
+
this.hdr = new NIFTI14();
|
|
16954
17845
|
const hdr = this.hdr;
|
|
16955
17846
|
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
16956
17847
|
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
@@ -17046,11 +17937,11 @@ var NVImage = class _NVImage {
|
|
|
17046
17937
|
];
|
|
17047
17938
|
hdr.numBitsPerVoxel = 32;
|
|
17048
17939
|
hdr.datatypeCode = 16 /* DT_FLOAT32 */;
|
|
17049
|
-
return rawImg;
|
|
17940
|
+
return rawImg.buffer;
|
|
17050
17941
|
}
|
|
17051
17942
|
// readECAT()
|
|
17052
17943
|
readV16(buffer) {
|
|
17053
|
-
this.hdr = new
|
|
17944
|
+
this.hdr = new NIFTI14();
|
|
17054
17945
|
const hdr = this.hdr;
|
|
17055
17946
|
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
17056
17947
|
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
@@ -17075,6 +17966,101 @@ var NVImage = class _NVImage {
|
|
|
17075
17966
|
return buffer.slice(6);
|
|
17076
17967
|
}
|
|
17077
17968
|
// readV16()
|
|
17969
|
+
async readNPY(buffer) {
|
|
17970
|
+
function getTypeSize(dtype2) {
|
|
17971
|
+
const typeMap = {
|
|
17972
|
+
"|b1": 1,
|
|
17973
|
+
// Boolean
|
|
17974
|
+
"<i1": 1,
|
|
17975
|
+
// Int8
|
|
17976
|
+
"<u1": 1,
|
|
17977
|
+
// UInt8
|
|
17978
|
+
"<i2": 2,
|
|
17979
|
+
// Int16
|
|
17980
|
+
"<u2": 2,
|
|
17981
|
+
// UInt16
|
|
17982
|
+
"<i4": 4,
|
|
17983
|
+
// Int32
|
|
17984
|
+
"<u4": 4,
|
|
17985
|
+
// UInt32
|
|
17986
|
+
"<f4": 4,
|
|
17987
|
+
// Float32
|
|
17988
|
+
"<f8": 8
|
|
17989
|
+
// Float64
|
|
17990
|
+
};
|
|
17991
|
+
return typeMap[dtype2] ?? 1;
|
|
17992
|
+
}
|
|
17993
|
+
function getDataTypeCode(dtype2) {
|
|
17994
|
+
const typeMap = {
|
|
17995
|
+
"|b1": 2,
|
|
17996
|
+
// DT_BINARY
|
|
17997
|
+
"<i1": 256,
|
|
17998
|
+
// DT_INT8
|
|
17999
|
+
"<u1": 2,
|
|
18000
|
+
// DT_UINT8
|
|
18001
|
+
"<i2": 4,
|
|
18002
|
+
// DT_INT16
|
|
18003
|
+
"<u2": 512,
|
|
18004
|
+
// DT_UINT16
|
|
18005
|
+
"<i4": 8,
|
|
18006
|
+
// DT_INT32
|
|
18007
|
+
"<u4": 768,
|
|
18008
|
+
// DT_UINT32
|
|
18009
|
+
"<f4": 16,
|
|
18010
|
+
// DT_FLOAT32
|
|
18011
|
+
"<f8": 64
|
|
18012
|
+
// DT_FLOAT64
|
|
18013
|
+
};
|
|
18014
|
+
return typeMap[dtype2] ?? 16;
|
|
18015
|
+
}
|
|
18016
|
+
const dv = new DataView(buffer);
|
|
18017
|
+
const magicBytes = [dv.getUint8(0), dv.getUint8(1), dv.getUint8(2), dv.getUint8(3), dv.getUint8(4), dv.getUint8(5)];
|
|
18018
|
+
const expectedMagic = [147, 78, 85, 77, 80, 89];
|
|
18019
|
+
if (!magicBytes.every((byte, i) => byte === expectedMagic[i])) {
|
|
18020
|
+
throw new Error("Not a valid NPY file: Magic number mismatch");
|
|
18021
|
+
}
|
|
18022
|
+
const headerLen = dv.getUint16(8, true);
|
|
18023
|
+
const headerText = new TextDecoder("utf-8").decode(buffer.slice(10, 10 + headerLen));
|
|
18024
|
+
const shapeMatch = headerText.match(/'shape': \((.*?)\)/);
|
|
18025
|
+
if (!shapeMatch) {
|
|
18026
|
+
throw new Error("Invalid NPY header: Shape not found");
|
|
18027
|
+
}
|
|
18028
|
+
const shape = shapeMatch[1].split(",").map((s) => s.trim()).filter((s) => s !== "").map(Number);
|
|
18029
|
+
const dtypeMatch = headerText.match(/'descr': '([^']+)'/);
|
|
18030
|
+
if (!dtypeMatch) {
|
|
18031
|
+
throw new Error("Invalid NPY header: Data type not found");
|
|
18032
|
+
}
|
|
18033
|
+
const dtype = dtypeMatch[1];
|
|
18034
|
+
const numElements = shape.reduce((a, b) => a * b, 1);
|
|
18035
|
+
const dataStart = 10 + headerLen;
|
|
18036
|
+
const dataBuffer = buffer.slice(dataStart, dataStart + numElements * getTypeSize(dtype));
|
|
18037
|
+
const width = shape.length > 0 ? shape[shape.length - 1] : 1;
|
|
18038
|
+
const height = shape.length > 1 ? shape[shape.length - 2] : 1;
|
|
18039
|
+
const slices = shape.length > 2 ? shape[shape.length - 3] : 1;
|
|
18040
|
+
this.hdr = new NIFTI14();
|
|
18041
|
+
const hdr = this.hdr;
|
|
18042
|
+
hdr.dims = [3, width, height, slices, 0, 0, 0, 0];
|
|
18043
|
+
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
18044
|
+
hdr.affine = [
|
|
18045
|
+
[hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],
|
|
18046
|
+
[0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],
|
|
18047
|
+
[0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],
|
|
18048
|
+
[0, 0, 0, 1]
|
|
18049
|
+
];
|
|
18050
|
+
hdr.numBitsPerVoxel = getTypeSize(dtype) * 8;
|
|
18051
|
+
hdr.datatypeCode = getDataTypeCode(dtype);
|
|
18052
|
+
return dataBuffer;
|
|
18053
|
+
}
|
|
18054
|
+
async readNPZ(buffer) {
|
|
18055
|
+
const zip = new Zip(buffer);
|
|
18056
|
+
for (let i = 0; i < zip.entries.length; i++) {
|
|
18057
|
+
const entry = zip.entries[i];
|
|
18058
|
+
if (entry.fileName.toLowerCase().endsWith(".npy")) {
|
|
18059
|
+
const data = await entry.extract();
|
|
18060
|
+
return await this.readNPY(data.buffer);
|
|
18061
|
+
}
|
|
18062
|
+
}
|
|
18063
|
+
}
|
|
17078
18064
|
async imageDataFromArrayBuffer(buffer) {
|
|
17079
18065
|
return new Promise((resolve, reject) => {
|
|
17080
18066
|
const blob = new Blob([buffer]);
|
|
@@ -17104,7 +18090,7 @@ var NVImage = class _NVImage {
|
|
|
17104
18090
|
async readBMP(buffer) {
|
|
17105
18091
|
const imageData = await this.imageDataFromArrayBuffer(buffer);
|
|
17106
18092
|
const { width, height, data } = imageData;
|
|
17107
|
-
this.hdr = new
|
|
18093
|
+
this.hdr = new NIFTI14();
|
|
17108
18094
|
const hdr = this.hdr;
|
|
17109
18095
|
hdr.dims = [3, width, height, 1, 0, 0, 0, 0];
|
|
17110
18096
|
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
@@ -17116,13 +18102,28 @@ var NVImage = class _NVImage {
|
|
|
17116
18102
|
];
|
|
17117
18103
|
hdr.numBitsPerVoxel = 8;
|
|
17118
18104
|
hdr.datatypeCode = 2304 /* DT_RGBA32 */;
|
|
18105
|
+
let isGrayscale = true;
|
|
18106
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
18107
|
+
if (data[i] !== data[i + 1] || data[i] !== data[i + 2]) {
|
|
18108
|
+
isGrayscale = false;
|
|
18109
|
+
break;
|
|
18110
|
+
}
|
|
18111
|
+
}
|
|
18112
|
+
if (isGrayscale) {
|
|
18113
|
+
hdr.datatypeCode = 2 /* DT_UINT8 */;
|
|
18114
|
+
const grayscaleData = new Uint8Array(width * height);
|
|
18115
|
+
for (let i = 0, j = 0; i < data.length; i += 4, j++) {
|
|
18116
|
+
grayscaleData[j] = data[i];
|
|
18117
|
+
}
|
|
18118
|
+
return grayscaleData.buffer;
|
|
18119
|
+
}
|
|
17119
18120
|
return data.buffer;
|
|
17120
18121
|
}
|
|
17121
18122
|
// not included in public docs
|
|
17122
18123
|
// read brainvoyager format VMR image
|
|
17123
18124
|
// https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/343-developer-guide-2-6-the-format-of-vmr-files
|
|
17124
18125
|
readVMR(buffer) {
|
|
17125
|
-
this.hdr = new
|
|
18126
|
+
this.hdr = new NIFTI14();
|
|
17126
18127
|
const hdr = this.hdr;
|
|
17127
18128
|
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
17128
18129
|
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
@@ -17176,119 +18177,20 @@ var NVImage = class _NVImage {
|
|
|
17176
18177
|
}
|
|
17177
18178
|
// readVMR()
|
|
17178
18179
|
// not included in public docs
|
|
17179
|
-
// read FreeSurfer MGH format image
|
|
17180
|
-
async readMGH(buffer) {
|
|
17181
|
-
this.hdr = new NIFTI1();
|
|
17182
|
-
const hdr = this.hdr;
|
|
17183
|
-
hdr.littleEndian = false;
|
|
17184
|
-
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
17185
|
-
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
17186
|
-
let raw = buffer;
|
|
17187
|
-
let reader = new DataView(raw);
|
|
17188
|
-
if (reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {
|
|
17189
|
-
raw = await NVUtilities.decompressToBuffer(new Uint8Array(buffer));
|
|
17190
|
-
reader = new DataView(raw);
|
|
17191
|
-
}
|
|
17192
|
-
const version2 = reader.getInt32(0, false);
|
|
17193
|
-
const width = reader.getInt32(4, false);
|
|
17194
|
-
const height = reader.getInt32(8, false);
|
|
17195
|
-
const depth = reader.getInt32(12, false);
|
|
17196
|
-
const nframes = reader.getInt32(16, false);
|
|
17197
|
-
const mtype = reader.getInt32(20, false);
|
|
17198
|
-
const spacingX = reader.getFloat32(30, false);
|
|
17199
|
-
const spacingY = reader.getFloat32(34, false);
|
|
17200
|
-
const spacingZ = reader.getFloat32(38, false);
|
|
17201
|
-
const xr = reader.getFloat32(42, false);
|
|
17202
|
-
const xa = reader.getFloat32(46, false);
|
|
17203
|
-
const xs = reader.getFloat32(50, false);
|
|
17204
|
-
const yr = reader.getFloat32(54, false);
|
|
17205
|
-
const ya = reader.getFloat32(58, false);
|
|
17206
|
-
const ys = reader.getFloat32(62, false);
|
|
17207
|
-
const zr = reader.getFloat32(66, false);
|
|
17208
|
-
const za = reader.getFloat32(70, false);
|
|
17209
|
-
const zs = reader.getFloat32(74, false);
|
|
17210
|
-
const cr = reader.getFloat32(78, false);
|
|
17211
|
-
const ca = reader.getFloat32(82, false);
|
|
17212
|
-
const cs = reader.getFloat32(86, false);
|
|
17213
|
-
if (version2 !== 1 || mtype < 0 || mtype > 4) {
|
|
17214
|
-
log.warn("Not a valid MGH file");
|
|
17215
|
-
}
|
|
17216
|
-
if (mtype === 0) {
|
|
17217
|
-
hdr.numBitsPerVoxel = 8;
|
|
17218
|
-
hdr.datatypeCode = 2 /* DT_UINT8 */;
|
|
17219
|
-
} else if (mtype === 4) {
|
|
17220
|
-
hdr.numBitsPerVoxel = 16;
|
|
17221
|
-
hdr.datatypeCode = 4 /* DT_INT16 */;
|
|
17222
|
-
} else if (mtype === 1) {
|
|
17223
|
-
hdr.numBitsPerVoxel = 32;
|
|
17224
|
-
hdr.datatypeCode = 8 /* DT_INT32 */;
|
|
17225
|
-
} else if (mtype === 3) {
|
|
17226
|
-
hdr.numBitsPerVoxel = 32;
|
|
17227
|
-
hdr.datatypeCode = 16 /* DT_FLOAT32 */;
|
|
17228
|
-
}
|
|
17229
|
-
hdr.dims[1] = width;
|
|
17230
|
-
hdr.dims[2] = height;
|
|
17231
|
-
hdr.dims[3] = depth;
|
|
17232
|
-
hdr.dims[4] = nframes;
|
|
17233
|
-
if (nframes > 1) {
|
|
17234
|
-
hdr.dims[0] = 4;
|
|
17235
|
-
}
|
|
17236
|
-
hdr.pixDims[1] = spacingX;
|
|
17237
|
-
hdr.pixDims[2] = spacingY;
|
|
17238
|
-
hdr.pixDims[3] = spacingZ;
|
|
17239
|
-
hdr.vox_offset = 284;
|
|
17240
|
-
hdr.sform_code = 1;
|
|
17241
|
-
const rot44 = mat44.fromValues(
|
|
17242
|
-
xr * hdr.pixDims[1],
|
|
17243
|
-
yr * hdr.pixDims[2],
|
|
17244
|
-
zr * hdr.pixDims[3],
|
|
17245
|
-
0,
|
|
17246
|
-
xa * hdr.pixDims[1],
|
|
17247
|
-
ya * hdr.pixDims[2],
|
|
17248
|
-
za * hdr.pixDims[3],
|
|
17249
|
-
0,
|
|
17250
|
-
xs * hdr.pixDims[1],
|
|
17251
|
-
ys * hdr.pixDims[2],
|
|
17252
|
-
zs * hdr.pixDims[3],
|
|
17253
|
-
0,
|
|
17254
|
-
0,
|
|
17255
|
-
0,
|
|
17256
|
-
0,
|
|
17257
|
-
1
|
|
17258
|
-
);
|
|
17259
|
-
const Pcrs = [hdr.dims[1] / 2, hdr.dims[2] / 2, hdr.dims[3] / 2, 1];
|
|
17260
|
-
const PxyzOffset = [0, 0, 0, 0];
|
|
17261
|
-
for (let i = 0; i < 3; i++) {
|
|
17262
|
-
PxyzOffset[i] = 0;
|
|
17263
|
-
for (let j = 0; j < 3; j++) {
|
|
17264
|
-
PxyzOffset[i] = PxyzOffset[i] + rot44[j + i * 4] * Pcrs[j];
|
|
17265
|
-
}
|
|
17266
|
-
}
|
|
17267
|
-
hdr.affine = [
|
|
17268
|
-
[rot44[0], rot44[1], rot44[2], cr - PxyzOffset[0]],
|
|
17269
|
-
[rot44[4], rot44[5], rot44[6], ca - PxyzOffset[1]],
|
|
17270
|
-
[rot44[8], rot44[9], rot44[10], cs - PxyzOffset[2]],
|
|
17271
|
-
[0, 0, 0, 1]
|
|
17272
|
-
];
|
|
17273
|
-
const nBytes = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4] * (hdr.numBitsPerVoxel / 8);
|
|
17274
|
-
return raw.slice(hdr.vox_offset, hdr.vox_offset + nBytes);
|
|
17275
|
-
}
|
|
17276
|
-
// readMGH()
|
|
17277
|
-
// not included in public docs
|
|
17278
18180
|
// read DSI-Studio FIB format image
|
|
17279
18181
|
// https://dsi-studio.labsolver.org/doc/cli_data.html
|
|
17280
18182
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17281
18183
|
async readFIB(buffer) {
|
|
17282
|
-
this.hdr = new
|
|
18184
|
+
this.hdr = new NIFTI14();
|
|
17283
18185
|
const hdr = this.hdr;
|
|
17284
18186
|
hdr.littleEndian = false;
|
|
17285
18187
|
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
17286
18188
|
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
17287
|
-
const mat = await NVUtilities.readMatV4(buffer);
|
|
18189
|
+
const mat = await NVUtilities.readMatV4(buffer, true);
|
|
17288
18190
|
if (!("dimension" in mat) || !("dti_fa" in mat)) {
|
|
17289
18191
|
throw new Error("Not a valid DSIstudio FIB file");
|
|
17290
18192
|
}
|
|
17291
|
-
const hasV1 = "index0" in mat && "index1" in mat && "index2" in mat;
|
|
18193
|
+
const hasV1 = "index0" in mat && "index1" in mat && "index2" in mat && "odf_vertices" in mat;
|
|
17292
18194
|
hdr.numBitsPerVoxel = 32;
|
|
17293
18195
|
hdr.datatypeCode = 16 /* DT_FLOAT32 */;
|
|
17294
18196
|
hdr.dims[1] = mat.dimension[0];
|
|
@@ -17330,13 +18232,35 @@ var NVImage = class _NVImage {
|
|
|
17330
18232
|
buff8v1.set(new Uint8Array(dir1.buffer, dir1.byteOffset, dir1.byteLength), 1 * nBytes3D);
|
|
17331
18233
|
buff8v1.set(new Uint8Array(dir2.buffer, dir2.byteOffset, dir2.byteLength), 2 * nBytes3D);
|
|
17332
18234
|
}
|
|
18235
|
+
if ("report" in mat) {
|
|
18236
|
+
hdr.description = new TextDecoder().decode(mat.report.subarray(0, Math.min(79, mat.report.byteLength)));
|
|
18237
|
+
}
|
|
17333
18238
|
const buff8 = new Uint8Array(new ArrayBuffer(nBytes));
|
|
17334
18239
|
const arrFA = Float32Array.from(mat.dti_fa);
|
|
18240
|
+
if ("mask" in mat) {
|
|
18241
|
+
console.log(mat);
|
|
18242
|
+
let slope = 1;
|
|
18243
|
+
if ("dti_fa_slope" in mat) {
|
|
18244
|
+
slope = mat.dti_fa_slope[0];
|
|
18245
|
+
}
|
|
18246
|
+
let inter = 1;
|
|
18247
|
+
if ("dti_fa_inter" in mat) {
|
|
18248
|
+
inter = mat.dti_fa_inter[0];
|
|
18249
|
+
}
|
|
18250
|
+
const nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
18251
|
+
const mask = mat.mask;
|
|
18252
|
+
const f32 = new Float32Array(nvox);
|
|
18253
|
+
let j = 0;
|
|
18254
|
+
for (let i = 0; i < nvox; i++) {
|
|
18255
|
+
if (mask[i] !== 0) {
|
|
18256
|
+
f32[i] = arrFA[j] * slope + inter;
|
|
18257
|
+
j++;
|
|
18258
|
+
}
|
|
18259
|
+
}
|
|
18260
|
+
return [f32.buffer, new Float32Array(buff8v1.buffer)];
|
|
18261
|
+
}
|
|
17335
18262
|
const imgFA = new Uint8Array(arrFA.buffer, arrFA.byteOffset, arrFA.byteLength);
|
|
17336
18263
|
buff8.set(imgFA, 0);
|
|
17337
|
-
if ("report" in mat) {
|
|
17338
|
-
hdr.description = new TextDecoder().decode(mat.report.subarray(0, Math.min(79, mat.report.byteLength)));
|
|
17339
|
-
}
|
|
17340
18264
|
return [buff8.buffer, new Float32Array(buff8v1.buffer)];
|
|
17341
18265
|
}
|
|
17342
18266
|
// readFIB()
|
|
@@ -17345,7 +18269,7 @@ var NVImage = class _NVImage {
|
|
|
17345
18269
|
// https://dsi-studio.labsolver.org/doc/cli_data.html
|
|
17346
18270
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17347
18271
|
async readSRC(buffer) {
|
|
17348
|
-
this.hdr = new
|
|
18272
|
+
this.hdr = new NIFTI14();
|
|
17349
18273
|
const hdr = this.hdr;
|
|
17350
18274
|
hdr.littleEndian = false;
|
|
17351
18275
|
hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
|
|
@@ -17415,7 +18339,7 @@ var NVImage = class _NVImage {
|
|
|
17415
18339
|
// not included in public docs
|
|
17416
18340
|
// read AFNI head/brik format image
|
|
17417
18341
|
async readHEAD(dataBuffer, pairedImgData) {
|
|
17418
|
-
this.hdr = new
|
|
18342
|
+
this.hdr = new NIFTI14();
|
|
17419
18343
|
const hdr = this.hdr;
|
|
17420
18344
|
hdr.dims[0] = 3;
|
|
17421
18345
|
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
@@ -17427,9 +18351,8 @@ var NVImage = class _NVImage {
|
|
|
17427
18351
|
const mod = (dataBuffer.byteLength + 8) % 16;
|
|
17428
18352
|
const len = dataBuffer.byteLength + (16 - mod);
|
|
17429
18353
|
log.debug(dataBuffer.byteLength, "len", len);
|
|
17430
|
-
const extBuffer = new
|
|
17431
|
-
extBuffer.
|
|
17432
|
-
extBuffer.set(new Uint8Array(dataBuffer));
|
|
18354
|
+
const extBuffer = new ArrayBuffer(len);
|
|
18355
|
+
new Uint8Array(extBuffer).set(new Uint8Array(dataBuffer));
|
|
17433
18356
|
const newExtension = new NIFTIEXTENSION(len + 8, 42, extBuffer, true);
|
|
17434
18357
|
hdr.addExtension(newExtension);
|
|
17435
18358
|
hdr.extensionCode = 42;
|
|
@@ -17509,7 +18432,7 @@ var NVImage = class _NVImage {
|
|
|
17509
18432
|
hdr.affine = [
|
|
17510
18433
|
[-items[0], -items[1], -items[2], -items[3]],
|
|
17511
18434
|
[-items[4], -items[5], -items[6], -items[7]],
|
|
17512
|
-
// TODO don't
|
|
18435
|
+
// TODO don't reuse items for numeric values
|
|
17513
18436
|
[items[8], items[9], items[10], items[11]],
|
|
17514
18437
|
[0, 0, 0, 1]
|
|
17515
18438
|
];
|
|
@@ -17577,15 +18500,15 @@ var NVImage = class _NVImage {
|
|
|
17577
18500
|
return new TextDecoder().decode(buffer.slice(startPos, pos));
|
|
17578
18501
|
}
|
|
17579
18502
|
let line = readStr();
|
|
17580
|
-
this.hdr = new
|
|
18503
|
+
this.hdr = new NIFTI14();
|
|
17581
18504
|
const hdr = this.hdr;
|
|
17582
18505
|
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
17583
18506
|
hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1];
|
|
17584
18507
|
hdr.littleEndian = true;
|
|
17585
18508
|
let isGz = false;
|
|
17586
18509
|
let isDetached = false;
|
|
17587
|
-
const mat33 =
|
|
17588
|
-
const offset =
|
|
18510
|
+
const mat33 = mat32.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
|
|
18511
|
+
const offset = vec38.fromValues(0, 0, 0);
|
|
17589
18512
|
while (line !== "") {
|
|
17590
18513
|
let items = line.split(" ");
|
|
17591
18514
|
if (items.length > 2) {
|
|
@@ -17670,8 +18593,8 @@ var NVImage = class _NVImage {
|
|
|
17670
18593
|
}
|
|
17671
18594
|
line = readStr();
|
|
17672
18595
|
}
|
|
17673
|
-
const mmMat =
|
|
17674
|
-
|
|
18596
|
+
const mmMat = mat32.fromValues(hdr.pixDims[1], 0, 0, 0, hdr.pixDims[2], 0, 0, 0, hdr.pixDims[3]);
|
|
18597
|
+
mat32.multiply(mat33, mat33, mmMat);
|
|
17675
18598
|
hdr.affine = [
|
|
17676
18599
|
[-mat33[0], -mat33[3], -mat33[6], -offset[0]],
|
|
17677
18600
|
[-mat33[1], -mat33[4], -mat33[7], -offset[1]],
|
|
@@ -17698,7 +18621,7 @@ var NVImage = class _NVImage {
|
|
|
17698
18621
|
// read mrtrix MIF format image
|
|
17699
18622
|
// https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#mrtrix-image-formats
|
|
17700
18623
|
async readMIF(buffer, pairedImgData) {
|
|
17701
|
-
this.hdr = new
|
|
18624
|
+
this.hdr = new NIFTI14();
|
|
17702
18625
|
const hdr = this.hdr;
|
|
17703
18626
|
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
17704
18627
|
hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1];
|
|
@@ -17986,290 +18909,10 @@ var NVImage = class _NVImage {
|
|
|
17986
18909
|
}
|
|
17987
18910
|
}
|
|
17988
18911
|
}
|
|
17989
|
-
return outVs;
|
|
18912
|
+
return outVs.buffer;
|
|
17990
18913
|
}
|
|
17991
18914
|
// readMIF()
|
|
17992
18915
|
// not included in public docs
|
|
17993
|
-
// read NRRD format image
|
|
17994
|
-
// http://teem.sourceforge.net/nrrd/format.html
|
|
17995
|
-
async readNRRD(dataBuffer, pairedImgData) {
|
|
17996
|
-
this.hdr = new NIFTI1();
|
|
17997
|
-
const hdr = this.hdr;
|
|
17998
|
-
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
17999
|
-
const len = dataBuffer.byteLength;
|
|
18000
|
-
let txt = null;
|
|
18001
|
-
const bytes = new Uint8Array(dataBuffer);
|
|
18002
|
-
for (let i = 1; i < len; i++) {
|
|
18003
|
-
if (bytes[i - 1] === 10 && bytes[i] === 10) {
|
|
18004
|
-
const v = dataBuffer.slice(0, i - 1);
|
|
18005
|
-
txt = new TextDecoder().decode(v);
|
|
18006
|
-
hdr.vox_offset = i + 1;
|
|
18007
|
-
break;
|
|
18008
|
-
}
|
|
18009
|
-
}
|
|
18010
|
-
if (txt === null) {
|
|
18011
|
-
throw new Error("could not extract txt");
|
|
18012
|
-
}
|
|
18013
|
-
const lines = txt.split("\n");
|
|
18014
|
-
if (!lines[0].startsWith("NRRD")) {
|
|
18015
|
-
alert("Invalid NRRD image");
|
|
18016
|
-
}
|
|
18017
|
-
const n = lines.length;
|
|
18018
|
-
let isGz = false;
|
|
18019
|
-
let isMicron = false;
|
|
18020
|
-
let isDetached = false;
|
|
18021
|
-
const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
|
|
18022
|
-
const offset = vec36.fromValues(0, 0, 0);
|
|
18023
|
-
let rot33 = mat3.create();
|
|
18024
|
-
for (let i = 1; i < n; i++) {
|
|
18025
|
-
let str = lines[i];
|
|
18026
|
-
if (str[0] === "#") {
|
|
18027
|
-
continue;
|
|
18028
|
-
}
|
|
18029
|
-
str = str.toLowerCase();
|
|
18030
|
-
const items = str.split(":");
|
|
18031
|
-
if (items.length < 2) {
|
|
18032
|
-
continue;
|
|
18033
|
-
}
|
|
18034
|
-
const key = items[0].trim();
|
|
18035
|
-
let value = items[1].trim();
|
|
18036
|
-
value = value.replaceAll(")", " ");
|
|
18037
|
-
value = value.replaceAll("(", " ");
|
|
18038
|
-
value = value.trim();
|
|
18039
|
-
switch (key) {
|
|
18040
|
-
case "data file":
|
|
18041
|
-
isDetached = true;
|
|
18042
|
-
break;
|
|
18043
|
-
case "encoding":
|
|
18044
|
-
if (value.includes("raw")) {
|
|
18045
|
-
isGz = false;
|
|
18046
|
-
} else if (value.includes("gz")) {
|
|
18047
|
-
isGz = true;
|
|
18048
|
-
} else {
|
|
18049
|
-
alert("Unsupported NRRD encoding");
|
|
18050
|
-
}
|
|
18051
|
-
break;
|
|
18052
|
-
case "type":
|
|
18053
|
-
switch (value) {
|
|
18054
|
-
case "uchar":
|
|
18055
|
-
case "unsigned char":
|
|
18056
|
-
case "uint8":
|
|
18057
|
-
case "uint8_t":
|
|
18058
|
-
hdr.numBitsPerVoxel = 8;
|
|
18059
|
-
hdr.datatypeCode = 2 /* DT_UINT8 */;
|
|
18060
|
-
break;
|
|
18061
|
-
case "signed char":
|
|
18062
|
-
case "int8":
|
|
18063
|
-
case "int8_t":
|
|
18064
|
-
hdr.numBitsPerVoxel = 8;
|
|
18065
|
-
hdr.datatypeCode = 256 /* DT_INT8 */;
|
|
18066
|
-
break;
|
|
18067
|
-
case "short":
|
|
18068
|
-
case "short int":
|
|
18069
|
-
case "signed short":
|
|
18070
|
-
case "signed short int":
|
|
18071
|
-
case "int16":
|
|
18072
|
-
case "int16_t":
|
|
18073
|
-
hdr.numBitsPerVoxel = 16;
|
|
18074
|
-
hdr.datatypeCode = 4 /* DT_INT16 */;
|
|
18075
|
-
break;
|
|
18076
|
-
case "ushort":
|
|
18077
|
-
case "unsigned short":
|
|
18078
|
-
case "unsigned short int":
|
|
18079
|
-
case "uint16":
|
|
18080
|
-
case "uint16_t":
|
|
18081
|
-
hdr.numBitsPerVoxel = 16;
|
|
18082
|
-
hdr.datatypeCode = 512 /* DT_UINT16 */;
|
|
18083
|
-
break;
|
|
18084
|
-
case "int":
|
|
18085
|
-
case "signed int":
|
|
18086
|
-
case "int32":
|
|
18087
|
-
case "int32_t":
|
|
18088
|
-
hdr.numBitsPerVoxel = 32;
|
|
18089
|
-
hdr.datatypeCode = 8 /* DT_INT32 */;
|
|
18090
|
-
break;
|
|
18091
|
-
case "uint":
|
|
18092
|
-
case "unsigned int":
|
|
18093
|
-
case "uint32":
|
|
18094
|
-
case "uint32_t":
|
|
18095
|
-
hdr.numBitsPerVoxel = 32;
|
|
18096
|
-
hdr.datatypeCode = 768 /* DT_UINT32 */;
|
|
18097
|
-
break;
|
|
18098
|
-
case "float":
|
|
18099
|
-
hdr.numBitsPerVoxel = 32;
|
|
18100
|
-
hdr.datatypeCode = 16 /* DT_FLOAT32 */;
|
|
18101
|
-
break;
|
|
18102
|
-
case "double":
|
|
18103
|
-
hdr.numBitsPerVoxel = 64;
|
|
18104
|
-
hdr.datatypeCode = 64 /* DT_FLOAT64 */;
|
|
18105
|
-
break;
|
|
18106
|
-
default:
|
|
18107
|
-
throw new Error("Unsupported NRRD data type: " + value);
|
|
18108
|
-
}
|
|
18109
|
-
break;
|
|
18110
|
-
case "spacings":
|
|
18111
|
-
{
|
|
18112
|
-
const values = value.split(/[ ,]+/);
|
|
18113
|
-
for (let d = 0; d < values.length; d++) {
|
|
18114
|
-
hdr.pixDims[d + 1] = parseFloat(values[d]);
|
|
18115
|
-
}
|
|
18116
|
-
}
|
|
18117
|
-
break;
|
|
18118
|
-
case "sizes":
|
|
18119
|
-
{
|
|
18120
|
-
const dims = value.split(/[ ,]+/);
|
|
18121
|
-
hdr.dims[0] = dims.length;
|
|
18122
|
-
for (let d = 0; d < dims.length; d++) {
|
|
18123
|
-
hdr.dims[d + 1] = parseInt(dims[d]);
|
|
18124
|
-
}
|
|
18125
|
-
}
|
|
18126
|
-
break;
|
|
18127
|
-
case "endian":
|
|
18128
|
-
if (value.includes("little")) {
|
|
18129
|
-
hdr.littleEndian = true;
|
|
18130
|
-
} else if (value.includes("big")) {
|
|
18131
|
-
hdr.littleEndian = false;
|
|
18132
|
-
}
|
|
18133
|
-
break;
|
|
18134
|
-
case "space directions":
|
|
18135
|
-
{
|
|
18136
|
-
const vs = value.split(/[ ,]+/);
|
|
18137
|
-
if (vs.length !== 9) {
|
|
18138
|
-
break;
|
|
18139
|
-
}
|
|
18140
|
-
for (let d = 0; d < 9; d++) {
|
|
18141
|
-
mat33[d] = parseFloat(vs[d]);
|
|
18142
|
-
}
|
|
18143
|
-
}
|
|
18144
|
-
break;
|
|
18145
|
-
case "space origin":
|
|
18146
|
-
{
|
|
18147
|
-
const ts = value.split(/[ ,]+/);
|
|
18148
|
-
if (ts.length !== 3) {
|
|
18149
|
-
break;
|
|
18150
|
-
}
|
|
18151
|
-
offset[0] = parseFloat(ts[0]);
|
|
18152
|
-
offset[1] = parseFloat(ts[1]);
|
|
18153
|
-
offset[2] = parseFloat(ts[2]);
|
|
18154
|
-
}
|
|
18155
|
-
break;
|
|
18156
|
-
case "space units":
|
|
18157
|
-
if (value.includes("microns")) {
|
|
18158
|
-
isMicron = true;
|
|
18159
|
-
}
|
|
18160
|
-
break;
|
|
18161
|
-
case "space":
|
|
18162
|
-
if (value.includes("right-anterior-superior") || value.includes("ras")) {
|
|
18163
|
-
rot33 = mat3.fromValues(
|
|
18164
|
-
1,
|
|
18165
|
-
0,
|
|
18166
|
-
0,
|
|
18167
|
-
0,
|
|
18168
|
-
1,
|
|
18169
|
-
0,
|
|
18170
|
-
0,
|
|
18171
|
-
0,
|
|
18172
|
-
1
|
|
18173
|
-
);
|
|
18174
|
-
} else if (value.includes("left-anterior-superior") || value.includes("las")) {
|
|
18175
|
-
rot33 = mat3.fromValues(
|
|
18176
|
-
-1,
|
|
18177
|
-
0,
|
|
18178
|
-
0,
|
|
18179
|
-
0,
|
|
18180
|
-
1,
|
|
18181
|
-
0,
|
|
18182
|
-
0,
|
|
18183
|
-
0,
|
|
18184
|
-
1
|
|
18185
|
-
);
|
|
18186
|
-
} else if (value.includes("left-posterior-superior") || value.includes("lps")) {
|
|
18187
|
-
rot33 = mat3.fromValues(
|
|
18188
|
-
-1,
|
|
18189
|
-
0,
|
|
18190
|
-
0,
|
|
18191
|
-
0,
|
|
18192
|
-
-1,
|
|
18193
|
-
0,
|
|
18194
|
-
0,
|
|
18195
|
-
0,
|
|
18196
|
-
1
|
|
18197
|
-
);
|
|
18198
|
-
} else {
|
|
18199
|
-
log.warn("Unsupported NRRD space value:", value);
|
|
18200
|
-
}
|
|
18201
|
-
break;
|
|
18202
|
-
default:
|
|
18203
|
-
log.warn("Unknown:", key);
|
|
18204
|
-
}
|
|
18205
|
-
}
|
|
18206
|
-
if (!isNaN(mat33[0])) {
|
|
18207
|
-
this.hdr.sform_code = 2;
|
|
18208
|
-
if (isMicron) {
|
|
18209
|
-
mat44.multiplyScalar(mat33, mat33, 1e-3);
|
|
18210
|
-
offset[0] *= 1e-3;
|
|
18211
|
-
offset[1] *= 1e-3;
|
|
18212
|
-
offset[2] *= 1e-3;
|
|
18213
|
-
}
|
|
18214
|
-
if (rot33[0] < 0) {
|
|
18215
|
-
offset[0] = -offset[0];
|
|
18216
|
-
}
|
|
18217
|
-
if (rot33[4] < 0) {
|
|
18218
|
-
offset[1] = -offset[1];
|
|
18219
|
-
}
|
|
18220
|
-
if (rot33[8] < 0) {
|
|
18221
|
-
offset[2] = -offset[2];
|
|
18222
|
-
}
|
|
18223
|
-
mat3.multiply(mat33, rot33, mat33);
|
|
18224
|
-
const mat = mat44.fromValues(
|
|
18225
|
-
mat33[0],
|
|
18226
|
-
mat33[3],
|
|
18227
|
-
mat33[6],
|
|
18228
|
-
offset[0],
|
|
18229
|
-
mat33[1],
|
|
18230
|
-
mat33[4],
|
|
18231
|
-
mat33[7],
|
|
18232
|
-
offset[1],
|
|
18233
|
-
mat33[2],
|
|
18234
|
-
mat33[5],
|
|
18235
|
-
mat33[8],
|
|
18236
|
-
offset[2],
|
|
18237
|
-
0,
|
|
18238
|
-
0,
|
|
18239
|
-
0,
|
|
18240
|
-
1
|
|
18241
|
-
);
|
|
18242
|
-
const mm000 = this.vox2mm([0, 0, 0], mat);
|
|
18243
|
-
const mm100 = this.vox2mm([1, 0, 0], mat);
|
|
18244
|
-
vec36.subtract(mm100, mm100, mm000);
|
|
18245
|
-
const mm010 = this.vox2mm([0, 1, 0], mat);
|
|
18246
|
-
vec36.subtract(mm010, mm010, mm000);
|
|
18247
|
-
const mm001 = this.vox2mm([0, 0, 1], mat);
|
|
18248
|
-
vec36.subtract(mm001, mm001, mm000);
|
|
18249
|
-
hdr.pixDims[1] = vec36.length(mm100);
|
|
18250
|
-
hdr.pixDims[2] = vec36.length(mm010);
|
|
18251
|
-
hdr.pixDims[3] = vec36.length(mm001);
|
|
18252
|
-
hdr.affine = [
|
|
18253
|
-
[mat[0], mat[1], mat[2], mat[3]],
|
|
18254
|
-
[mat[4], mat[5], mat[6], mat[7]],
|
|
18255
|
-
[mat[8], mat[9], mat[10], mat[11]],
|
|
18256
|
-
[0, 0, 0, 1]
|
|
18257
|
-
];
|
|
18258
|
-
}
|
|
18259
|
-
if (isDetached && pairedImgData) {
|
|
18260
|
-
return pairedImgData.slice(0);
|
|
18261
|
-
}
|
|
18262
|
-
if (isDetached) {
|
|
18263
|
-
log.warn("Missing data: NRRD header describes detached data file but only one URL provided");
|
|
18264
|
-
}
|
|
18265
|
-
if (isGz) {
|
|
18266
|
-
return await NVUtilities.decompressToBuffer(new Uint8Array(dataBuffer.slice(hdr.vox_offset)));
|
|
18267
|
-
} else {
|
|
18268
|
-
return dataBuffer.slice(hdr.vox_offset);
|
|
18269
|
-
}
|
|
18270
|
-
}
|
|
18271
|
-
// readNRRD()
|
|
18272
|
-
// not included in public docs
|
|
18273
18916
|
// Transform to orient NIfTI image to Left->Right,Posterior->Anterior,Inferior->Superior (48 possible permutations)
|
|
18274
18917
|
calculateRAS() {
|
|
18275
18918
|
if (!this.hdr) {
|
|
@@ -18277,7 +18920,7 @@ var NVImage = class _NVImage {
|
|
|
18277
18920
|
}
|
|
18278
18921
|
const a = this.hdr.affine;
|
|
18279
18922
|
const header = this.hdr;
|
|
18280
|
-
const absR =
|
|
18923
|
+
const absR = mat32.fromValues(
|
|
18281
18924
|
Math.abs(a[0][0]),
|
|
18282
18925
|
Math.abs(a[0][1]),
|
|
18283
18926
|
Math.abs(a[0][2]),
|
|
@@ -18320,7 +18963,7 @@ var NVImage = class _NVImage {
|
|
|
18320
18963
|
perm[ixyz[0] - 1] = 1;
|
|
18321
18964
|
perm[ixyz[1] - 1] = 2;
|
|
18322
18965
|
perm[ixyz[2] - 1] = 3;
|
|
18323
|
-
let rotM =
|
|
18966
|
+
let rotM = mat46.fromValues(
|
|
18324
18967
|
a[0][0],
|
|
18325
18968
|
a[0][1],
|
|
18326
18969
|
a[0][2],
|
|
@@ -18342,8 +18985,8 @@ var NVImage = class _NVImage {
|
|
|
18342
18985
|
this.mm100 = this.vox2mm([header.dims[1] - 0.5, -0.5, -0.5], rotM);
|
|
18343
18986
|
this.mm010 = this.vox2mm([-0.5, header.dims[2] - 0.5, -0.5], rotM);
|
|
18344
18987
|
this.mm001 = this.vox2mm([-0.5, -0.5, header.dims[3] - 0.5], rotM);
|
|
18345
|
-
const R =
|
|
18346
|
-
|
|
18988
|
+
const R = mat46.create();
|
|
18989
|
+
mat46.copy(R, rotM);
|
|
18347
18990
|
for (let i = 0; i < 3; i++) {
|
|
18348
18991
|
for (let j = 0; j < 3; j++) {
|
|
18349
18992
|
R[i * 4 + j] = rotM[i * 4 + perm[j] - 1];
|
|
@@ -18368,32 +19011,32 @@ var NVImage = class _NVImage {
|
|
|
18368
19011
|
}
|
|
18369
19012
|
}
|
|
18370
19013
|
if (this.arrayEquals(perm, [1, 2, 3]) && this.arrayEquals(flip, [0, 0, 0])) {
|
|
18371
|
-
this.toRAS =
|
|
18372
|
-
this.matRAS =
|
|
19014
|
+
this.toRAS = mat46.create();
|
|
19015
|
+
this.matRAS = mat46.clone(rotM);
|
|
18373
19016
|
this.calculateOblique();
|
|
18374
19017
|
this.img2RASstep = [1, this.dimsRAS[1], this.dimsRAS[1] * this.dimsRAS[2]];
|
|
18375
19018
|
this.img2RASstart = [0, 0, 0];
|
|
18376
19019
|
return;
|
|
18377
19020
|
}
|
|
18378
|
-
|
|
19021
|
+
mat46.identity(rotM);
|
|
18379
19022
|
rotM[0 + 0 * 4] = 1 - flip[0] * 2;
|
|
18380
19023
|
rotM[1 + 1 * 4] = 1 - flip[1] * 2;
|
|
18381
19024
|
rotM[2 + 2 * 4] = 1 - flip[2] * 2;
|
|
18382
19025
|
rotM[3 + 0 * 4] = (header.dims[perm[0]] - 1) * flip[0];
|
|
18383
19026
|
rotM[3 + 1 * 4] = (header.dims[perm[1]] - 1) * flip[1];
|
|
18384
19027
|
rotM[3 + 2 * 4] = (header.dims[perm[2]] - 1) * flip[2];
|
|
18385
|
-
const residualR =
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
this.matRAS =
|
|
18389
|
-
rotM =
|
|
19028
|
+
const residualR = mat46.create();
|
|
19029
|
+
mat46.invert(residualR, rotM);
|
|
19030
|
+
mat46.multiply(residualR, residualR, R);
|
|
19031
|
+
this.matRAS = mat46.clone(residualR);
|
|
19032
|
+
rotM = mat46.fromValues(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
|
|
18390
19033
|
rotM[perm[0] - 1 + 0 * 4] = -flip[0] * 2 + 1;
|
|
18391
19034
|
rotM[perm[1] - 1 + 1 * 4] = -flip[1] * 2 + 1;
|
|
18392
19035
|
rotM[perm[2] - 1 + 2 * 4] = -flip[2] * 2 + 1;
|
|
18393
19036
|
rotM[3 + 0 * 4] = flip[0];
|
|
18394
19037
|
rotM[3 + 1 * 4] = flip[1];
|
|
18395
19038
|
rotM[3 + 2 * 4] = flip[2];
|
|
18396
|
-
this.toRAS =
|
|
19039
|
+
this.toRAS = mat46.clone(rotM);
|
|
18397
19040
|
rotM[3] = 0;
|
|
18398
19041
|
rotM[7] = 0;
|
|
18399
19042
|
rotM[11] = 0;
|
|
@@ -18409,7 +19052,7 @@ var NVImage = class _NVImage {
|
|
|
18409
19052
|
if (this.permRAS[0] === -3 || this.permRAS[1] === -3 || this.permRAS[2] === -3) {
|
|
18410
19053
|
rotM[14] = header.dims[3] - 1;
|
|
18411
19054
|
}
|
|
18412
|
-
this.toRASvox =
|
|
19055
|
+
this.toRASvox = mat46.clone(rotM);
|
|
18413
19056
|
log.debug(this.hdr.dims);
|
|
18414
19057
|
log.debug(this.dimsRAS);
|
|
18415
19058
|
const hdr = this.hdr;
|
|
@@ -18439,7 +19082,7 @@ var NVImage = class _NVImage {
|
|
|
18439
19082
|
throw new Error("hdr undefined");
|
|
18440
19083
|
}
|
|
18441
19084
|
const hdrBytes = hdrToArrayBuffer({ ...this.hdr, vox_offset: 352 }, false);
|
|
18442
|
-
const hdr = await
|
|
19085
|
+
const hdr = await readHeaderAsync2(hdrBytes.buffer, true);
|
|
18443
19086
|
if (nVolumes === 1) {
|
|
18444
19087
|
hdr.dims[0] = 3;
|
|
18445
19088
|
hdr.dims[4] = 1;
|
|
@@ -18521,11 +19164,11 @@ var NVImage = class _NVImage {
|
|
|
18521
19164
|
// not included in public docs
|
|
18522
19165
|
// convert voxel location (row, column slice, indexed from 0) to world space
|
|
18523
19166
|
vox2mm(XYZ, mtx) {
|
|
18524
|
-
const sform =
|
|
18525
|
-
|
|
18526
|
-
const pos =
|
|
18527
|
-
|
|
18528
|
-
const pos3 =
|
|
19167
|
+
const sform = mat46.clone(mtx);
|
|
19168
|
+
mat46.transpose(sform, sform);
|
|
19169
|
+
const pos = vec45.fromValues(XYZ[0], XYZ[1], XYZ[2], 1);
|
|
19170
|
+
vec45.transformMat4(pos, pos, sform);
|
|
19171
|
+
const pos3 = vec38.fromValues(pos[0], pos[1], pos[2]);
|
|
18529
19172
|
return pos3;
|
|
18530
19173
|
}
|
|
18531
19174
|
// vox2mm()
|
|
@@ -18535,13 +19178,13 @@ var NVImage = class _NVImage {
|
|
|
18535
19178
|
if (!this.matRAS) {
|
|
18536
19179
|
throw new Error("matRAS undefined");
|
|
18537
19180
|
}
|
|
18538
|
-
const sform =
|
|
18539
|
-
const out =
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
const pos =
|
|
18543
|
-
|
|
18544
|
-
const pos3 =
|
|
19181
|
+
const sform = mat46.clone(this.matRAS);
|
|
19182
|
+
const out = mat46.clone(sform);
|
|
19183
|
+
mat46.transpose(out, sform);
|
|
19184
|
+
mat46.invert(out, out);
|
|
19185
|
+
const pos = vec45.fromValues(mm[0], mm[1], mm[2], 1);
|
|
19186
|
+
vec45.transformMat4(pos, pos, out);
|
|
19187
|
+
const pos3 = vec38.fromValues(pos[0], pos[1], pos[2]);
|
|
18545
19188
|
if (frac) {
|
|
18546
19189
|
return pos3;
|
|
18547
19190
|
}
|
|
@@ -18836,58 +19479,26 @@ var NVImage = class _NVImage {
|
|
|
18836
19479
|
}
|
|
18837
19480
|
return (scaled - this.hdr.scl_inter) / this.hdr.scl_slope;
|
|
18838
19481
|
}
|
|
18839
|
-
|
|
18840
|
-
|
|
19482
|
+
/**
|
|
19483
|
+
* Converts NVImage to NIfTI compliant byte array, potentially compressed.
|
|
19484
|
+
* Delegates to ImageWriter.saveToUint8Array.
|
|
19485
|
+
* @param fnm - Filename (determines if compression is needed via .gz suffix)
|
|
19486
|
+
* @param drawing8 - Optional Uint8Array drawing overlay
|
|
19487
|
+
* @returns Promise<Uint8Array>
|
|
19488
|
+
*/
|
|
18841
19489
|
async saveToUint8Array(fnm, drawing8 = null) {
|
|
18842
|
-
|
|
18843
|
-
throw new Error("hdr undefined");
|
|
18844
|
-
}
|
|
18845
|
-
if (!this.img) {
|
|
18846
|
-
throw new Error("img undefined");
|
|
18847
|
-
}
|
|
18848
|
-
const isDrawing8 = drawing8 !== null;
|
|
18849
|
-
const hdrBytes = hdrToArrayBuffer(this.hdr, isDrawing8);
|
|
18850
|
-
const opad = new Uint8Array(4);
|
|
18851
|
-
let img8 = new Uint8Array(this.img.buffer);
|
|
18852
|
-
if (isDrawing8) {
|
|
18853
|
-
img8 = new Uint8Array(drawing8.buffer);
|
|
18854
|
-
}
|
|
18855
|
-
const odata = new Uint8Array(hdrBytes.length + opad.length + img8.length);
|
|
18856
|
-
odata.set(hdrBytes);
|
|
18857
|
-
odata.set(opad, hdrBytes.length);
|
|
18858
|
-
odata.set(img8, hdrBytes.length + opad.length);
|
|
18859
|
-
let saveData = null;
|
|
18860
|
-
const compress = fnm.endsWith(".gz");
|
|
18861
|
-
if (compress) {
|
|
18862
|
-
saveData = new Uint8Array(await NVUtilities.compress(odata, "gzip"));
|
|
18863
|
-
} else {
|
|
18864
|
-
saveData = odata;
|
|
18865
|
-
}
|
|
18866
|
-
return saveData;
|
|
19490
|
+
return saveToUint8Array(this, fnm, drawing8);
|
|
18867
19491
|
}
|
|
18868
|
-
|
|
18869
|
-
|
|
18870
|
-
|
|
19492
|
+
/**
|
|
19493
|
+
* save image as NIfTI volume and trigger download.
|
|
19494
|
+
* Delegates to ImageWriter.saveToDisk.
|
|
19495
|
+
* @param fnm - Filename for download. If empty, returns data without download.
|
|
19496
|
+
* @param drawing8 - Optional Uint8Array drawing overlay
|
|
19497
|
+
* @returns Promise<Uint8Array>
|
|
19498
|
+
*/
|
|
18871
19499
|
async saveToDisk(fnm = "", drawing8 = null) {
|
|
18872
|
-
|
|
18873
|
-
if (fnm === "") {
|
|
18874
|
-
log.debug("saveToDisk: empty file name, returning data as Uint8Array rather than triggering download");
|
|
18875
|
-
return saveData;
|
|
18876
|
-
}
|
|
18877
|
-
const blob = new Blob([saveData.buffer], {
|
|
18878
|
-
type: "application/octet-stream"
|
|
18879
|
-
});
|
|
18880
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
18881
|
-
const link = document.createElement("a");
|
|
18882
|
-
link.setAttribute("href", blobUrl);
|
|
18883
|
-
link.setAttribute("download", fnm);
|
|
18884
|
-
link.style.visibility = "hidden";
|
|
18885
|
-
document.body.appendChild(link);
|
|
18886
|
-
link.click();
|
|
18887
|
-
document.body.removeChild(link);
|
|
18888
|
-
return saveData;
|
|
19500
|
+
return saveToDisk(this, fnm, drawing8);
|
|
18889
19501
|
}
|
|
18890
|
-
// saveToDisk()
|
|
18891
19502
|
static async fetchDicomData(url, headers = {}) {
|
|
18892
19503
|
if (url === "") {
|
|
18893
19504
|
throw Error("url must not be empty");
|
|
@@ -19007,7 +19618,7 @@ var NVImage = class _NVImage {
|
|
|
19007
19618
|
if (!isNifti1) {
|
|
19008
19619
|
return null;
|
|
19009
19620
|
}
|
|
19010
|
-
const hdr = await
|
|
19621
|
+
const hdr = await readHeaderAsync2(hdrU8s.buffer);
|
|
19011
19622
|
if (!hdr) {
|
|
19012
19623
|
throw new Error("Could not read NIfTI header");
|
|
19013
19624
|
}
|
|
@@ -19064,7 +19675,7 @@ var NVImage = class _NVImage {
|
|
|
19064
19675
|
}
|
|
19065
19676
|
hdrU8s = concatU8s(hdrU8s, value2);
|
|
19066
19677
|
}
|
|
19067
|
-
const hdr = await
|
|
19678
|
+
const hdr = await readHeaderAsync2(hdrU8s.buffer);
|
|
19068
19679
|
if (!hdr) {
|
|
19069
19680
|
throw new Error("Could not read NIfTI header");
|
|
19070
19681
|
}
|
|
@@ -19346,7 +19957,7 @@ var NVImage = class _NVImage {
|
|
|
19346
19957
|
if (!isNifti1) {
|
|
19347
19958
|
dataBuffer = await this.readFileAsync(file);
|
|
19348
19959
|
} else {
|
|
19349
|
-
const hdr = await
|
|
19960
|
+
const hdr = await readHeaderAsync2(headerBuffer);
|
|
19350
19961
|
if (!hdr) {
|
|
19351
19962
|
throw new Error("could not read nifti header");
|
|
19352
19963
|
}
|
|
@@ -19393,70 +20004,19 @@ var NVImage = class _NVImage {
|
|
|
19393
20004
|
return nvimage;
|
|
19394
20005
|
}
|
|
19395
20006
|
/**
|
|
19396
|
-
*
|
|
19397
|
-
*
|
|
19398
|
-
* @returns NVImage instance
|
|
19399
|
-
* @example
|
|
19400
|
-
* myImage = NVImage.loadFromBase64('SomeBase64String')
|
|
20007
|
+
* Creates a Uint8Array representing a NIFTI file (header + optional image data).
|
|
20008
|
+
* Delegates to ImageWriter.createNiftiArray.
|
|
19401
20009
|
*/
|
|
19402
|
-
static createNiftiArray(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2
|
|
19403
|
-
|
|
19404
|
-
const hdrBytes = hdrToArrayBuffer(hdr, false);
|
|
19405
|
-
if (img.length < 1) {
|
|
19406
|
-
return hdrBytes;
|
|
19407
|
-
}
|
|
19408
|
-
const opad = new Uint8Array(4);
|
|
19409
|
-
const img8 = new Uint8Array(img.buffer);
|
|
19410
|
-
const odata = new Uint8Array(hdrBytes.length + opad.length + img8.length);
|
|
19411
|
-
odata.set(hdrBytes);
|
|
19412
|
-
odata.set(opad, hdrBytes.length);
|
|
19413
|
-
odata.set(img8, hdrBytes.length + opad.length);
|
|
19414
|
-
return odata;
|
|
20010
|
+
static createNiftiArray(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2 /* DT_UINT8 */, img = new Uint8Array()) {
|
|
20011
|
+
return createNiftiArray(dims, pixDims, affine, datatypeCode, img);
|
|
19415
20012
|
}
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
for (let i = 0; i < dims.length; i++) {
|
|
19423
|
-
hdr.dims[i + 1] = dims[i];
|
|
19424
|
-
}
|
|
19425
|
-
hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
|
|
19426
|
-
for (let i = 0; i < dims.length; i++) {
|
|
19427
|
-
hdr.pixDims[i + 1] = pixDims[i];
|
|
19428
|
-
}
|
|
19429
|
-
if (affine.length === 16) {
|
|
19430
|
-
let k = 0;
|
|
19431
|
-
for (let i = 0; i < 4; i++) {
|
|
19432
|
-
for (let j = 0; j < 4; j++) {
|
|
19433
|
-
hdr.affine[i][j] = affine[k];
|
|
19434
|
-
k++;
|
|
19435
|
-
}
|
|
19436
|
-
}
|
|
19437
|
-
}
|
|
19438
|
-
let bpv = 8;
|
|
19439
|
-
if (datatypeCode === 256 || datatypeCode === 2) {
|
|
19440
|
-
bpv = 8;
|
|
19441
|
-
} else if (datatypeCode === 512 || datatypeCode === 4) {
|
|
19442
|
-
bpv = 16;
|
|
19443
|
-
} else if (datatypeCode === 16 || datatypeCode === 768 || datatypeCode === 8 || datatypeCode === 2304) {
|
|
19444
|
-
bpv = 32;
|
|
19445
|
-
} else if (datatypeCode === 64) {
|
|
19446
|
-
bpv = 64;
|
|
19447
|
-
} else {
|
|
19448
|
-
log.warn("Unsupported NIfTI datatypeCode: " + datatypeCode);
|
|
19449
|
-
}
|
|
19450
|
-
hdr.datatypeCode = datatypeCode;
|
|
19451
|
-
hdr.numBitsPerVoxel = bpv;
|
|
19452
|
-
hdr.scl_inter = 0;
|
|
19453
|
-
hdr.scl_slope = 0;
|
|
19454
|
-
hdr.sform_code = 2;
|
|
19455
|
-
hdr.magic = "n+1";
|
|
19456
|
-
hdr.vox_offset = 352;
|
|
19457
|
-
return hdr;
|
|
20013
|
+
/**
|
|
20014
|
+
* Creates a NIFTI1 header object with basic properties.
|
|
20015
|
+
* Delegates to ImageWriter.createNiftiHeader.
|
|
20016
|
+
*/
|
|
20017
|
+
static createNiftiHeader(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2 /* DT_UINT8 */) {
|
|
20018
|
+
return createNiftiHeader(dims, pixDims, affine, datatypeCode);
|
|
19458
20019
|
}
|
|
19459
|
-
// loadFromHeader
|
|
19460
20020
|
/**
|
|
19461
20021
|
* read a 3D slab of voxels from a volume
|
|
19462
20022
|
* @param voxStart - first row, column and slice (RAS order) for selection
|
|
@@ -19465,77 +20025,17 @@ var NVImage = class _NVImage {
|
|
|
19465
20025
|
* @returns the an array where ret[0] is the voxel values and ret[1] is dimension of selection
|
|
19466
20026
|
* @see {@link https://niivue.github.io/niivue/features/slab_selection.html | live demo usage}
|
|
19467
20027
|
*/
|
|
20028
|
+
/**
|
|
20029
|
+
* read a 3D slab of voxels from a volume, specified in RAS coordinates.
|
|
20030
|
+
* Delegates to VolumeUtils.getVolumeData.
|
|
20031
|
+
* @param voxStart - first row, column and slice (RAS order) for selection
|
|
20032
|
+
* @param voxEnd - final row, column and slice (RAS order) for selection
|
|
20033
|
+
* @param dataType - array data type. Options: 'same' (default), 'uint8', 'float32', 'scaled', 'normalized', 'windowed'
|
|
20034
|
+
* @returns the an array where ret[0] is the voxel values and ret[1] is dimension of selection
|
|
20035
|
+
*/
|
|
19468
20036
|
getVolumeData(voxStart = [-1, 0, 0], voxEnd = [0, 0, 0], dataType = "same") {
|
|
19469
|
-
|
|
19470
|
-
if (Math.min(...voxStart) < 0 || Math.min(...voxEnd) < 0) {
|
|
19471
|
-
return [img, [0, 0, 0]];
|
|
19472
|
-
}
|
|
19473
|
-
const dims = this.dimsRAS.slice(1, 4);
|
|
19474
|
-
for (let i2 = 0; i2 < 3; i2++) {
|
|
19475
|
-
voxStart[i2] = Math.min(voxStart[i2], dims[i2] - 1);
|
|
19476
|
-
voxEnd[i2] = Math.min(voxEnd[i2], dims[i2] - 1);
|
|
19477
|
-
if (voxEnd[i2] < voxStart[i2]) {
|
|
19478
|
-
const tmp = voxEnd[i2];
|
|
19479
|
-
voxEnd[i2] = voxStart[i2];
|
|
19480
|
-
voxStart[i2] = tmp;
|
|
19481
|
-
}
|
|
19482
|
-
}
|
|
19483
|
-
const slabDims = [voxEnd[0] - voxStart[0] + 1, voxEnd[1] - voxStart[1] + 1, voxEnd[2] - voxStart[2] + 1];
|
|
19484
|
-
const slabNVox = slabDims[0] * slabDims[1] * slabDims[2];
|
|
19485
|
-
let dt = this.hdr.datatypeCode;
|
|
19486
|
-
if (dataType === "uint8") {
|
|
19487
|
-
dt = 2 /* DT_UINT8 */;
|
|
19488
|
-
} else if (dataType === "float32" || dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
|
|
19489
|
-
dt = 16 /* DT_FLOAT32 */;
|
|
19490
|
-
}
|
|
19491
|
-
if (dt === 2 /* DT_UINT8 */) {
|
|
19492
|
-
img = new Uint8Array(slabNVox);
|
|
19493
|
-
} else if (dt === 4 /* DT_INT16 */) {
|
|
19494
|
-
img = new Int16Array(slabNVox);
|
|
19495
|
-
} else if (dt === 512 /* DT_UINT16 */) {
|
|
19496
|
-
img = new Uint16Array(slabNVox);
|
|
19497
|
-
} else if (dt === 16 /* DT_FLOAT32 */) {
|
|
19498
|
-
img = new Float32Array(slabNVox);
|
|
19499
|
-
} else if (dt === 64 /* DT_FLOAT64 */) {
|
|
19500
|
-
img = new Float64Array(slabNVox);
|
|
19501
|
-
} else {
|
|
19502
|
-
log.error("getVolumeData unsupported datatype");
|
|
19503
|
-
return [img, [0, 0, 0]];
|
|
19504
|
-
}
|
|
19505
|
-
const outStep = this.img2RASstep;
|
|
19506
|
-
const outStart = this.img2RASstart;
|
|
19507
|
-
let i = 0;
|
|
19508
|
-
for (let z = voxStart[2]; z <= voxEnd[2]; z++) {
|
|
19509
|
-
const zi = outStart[2] + z * outStep[2];
|
|
19510
|
-
for (let y = voxStart[1]; y <= voxEnd[1]; y++) {
|
|
19511
|
-
const yizi = zi + outStart[1] + y * outStep[1];
|
|
19512
|
-
for (let x = voxStart[0]; x <= voxEnd[0]; x++) {
|
|
19513
|
-
const xi = outStart[0] + x * outStep[0];
|
|
19514
|
-
img[i++] = this.img[xi + yizi];
|
|
19515
|
-
}
|
|
19516
|
-
}
|
|
19517
|
-
}
|
|
19518
|
-
if (dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
|
|
19519
|
-
for (let i2 = 0; i2 < img.length; i2++) {
|
|
19520
|
-
img[i2] = img[i2] * this.hdr.scl_slope + this.hdr.scl_inter;
|
|
19521
|
-
}
|
|
19522
|
-
}
|
|
19523
|
-
if (dataType === "normalized" || dataType === "windowed") {
|
|
19524
|
-
let mn = this.cal_min;
|
|
19525
|
-
let mx = this.cal_max;
|
|
19526
|
-
if (dataType === "normalized") {
|
|
19527
|
-
mn = this.global_min;
|
|
19528
|
-
mx = this.global_max;
|
|
19529
|
-
}
|
|
19530
|
-
const scale = 1 / (mx - mn);
|
|
19531
|
-
for (let i2 = 0; i2 < img.length; i2++) {
|
|
19532
|
-
img[i2] = (img[i2] - mn) * scale;
|
|
19533
|
-
img[i2] = Math.max(Math.min(img[i2], 1), 0);
|
|
19534
|
-
}
|
|
19535
|
-
}
|
|
19536
|
-
return [img, slabDims];
|
|
20037
|
+
return getVolumeData(this, voxStart, voxEnd, dataType);
|
|
19537
20038
|
}
|
|
19538
|
-
// getVolumeData()
|
|
19539
20039
|
/**
|
|
19540
20040
|
* write a 3D slab of voxels from a volume
|
|
19541
20041
|
* @param voxStart - first row, column and slice (RAS order) for selection
|
|
@@ -19543,41 +20043,17 @@ var NVImage = class _NVImage {
|
|
|
19543
20043
|
* @param img - array of voxel values to insert (RAS order)
|
|
19544
20044
|
* @see {@link https://niivue.github.io/niivue/features/slab_selection.html | live demo usage}
|
|
19545
20045
|
*/
|
|
20046
|
+
/**
|
|
20047
|
+
* write a 3D slab of voxels from a volume, specified in RAS coordinates.
|
|
20048
|
+
* Delegates to VolumeUtils.setVolumeData.
|
|
20049
|
+
* Input slabData is assumed to be in the correct raw data type for the target image.
|
|
20050
|
+
* @param voxStart - first row, column and slice (RAS order) for selection
|
|
20051
|
+
* @param voxEnd - final row, column and slice (RAS order) for selection
|
|
20052
|
+
* @param img - array of voxel values to insert (RAS order, raw data type)
|
|
20053
|
+
*/
|
|
19546
20054
|
setVolumeData(voxStart = [-1, 0, 0], voxEnd = [0, 0, 0], img = new Uint8Array()) {
|
|
19547
|
-
|
|
19548
|
-
return;
|
|
19549
|
-
}
|
|
19550
|
-
const dims = this.dimsRAS.slice(1, 4);
|
|
19551
|
-
for (let i2 = 0; i2 < 3; i2++) {
|
|
19552
|
-
voxStart[i2] = Math.min(voxStart[i2], dims[i2] - 1);
|
|
19553
|
-
voxEnd[i2] = Math.min(voxEnd[i2], dims[i2] - 1);
|
|
19554
|
-
if (voxEnd[i2] < voxStart[i2]) {
|
|
19555
|
-
const tmp = voxEnd[i2];
|
|
19556
|
-
voxEnd[i2] = voxStart[i2];
|
|
19557
|
-
voxStart[i2] = tmp;
|
|
19558
|
-
}
|
|
19559
|
-
}
|
|
19560
|
-
const slabDims = [voxEnd[0] - voxStart[0] + 1, voxEnd[1] - voxStart[1] + 1, voxEnd[2] - voxStart[2] + 1];
|
|
19561
|
-
const slabNVox = slabDims[0] * slabDims[1] * slabDims[2];
|
|
19562
|
-
if (img.length < slabNVox) {
|
|
19563
|
-
log.error("setVolumeData image does not have enough voxels");
|
|
19564
|
-
return;
|
|
19565
|
-
}
|
|
19566
|
-
const outStep = this.img2RASstep;
|
|
19567
|
-
const outStart = this.img2RASstart;
|
|
19568
|
-
let i = 0;
|
|
19569
|
-
for (let z = voxStart[2]; z <= voxEnd[2]; z++) {
|
|
19570
|
-
const zi = outStart[2] + z * outStep[2];
|
|
19571
|
-
for (let y = voxStart[1]; y <= voxEnd[1]; y++) {
|
|
19572
|
-
const yizi = zi + outStart[1] + y * outStep[1];
|
|
19573
|
-
for (let x = voxStart[0]; x <= voxEnd[0]; x++) {
|
|
19574
|
-
const xi = outStart[0] + x * outStep[0];
|
|
19575
|
-
this.img[xi + yizi] = img[i++];
|
|
19576
|
-
}
|
|
19577
|
-
}
|
|
19578
|
-
}
|
|
20055
|
+
setVolumeData(this, voxStart, voxEnd, img);
|
|
19579
20056
|
}
|
|
19580
|
-
// setVolumeData()
|
|
19581
20057
|
/**
|
|
19582
20058
|
* factory function to load and return a new NVImage instance from a base64 encoded string
|
|
19583
20059
|
*
|
|
@@ -19728,155 +20204,18 @@ var NVImage = class _NVImage {
|
|
|
19728
20204
|
}
|
|
19729
20205
|
return zeroClone;
|
|
19730
20206
|
}
|
|
19731
|
-
// not included in public docs
|
|
19732
|
-
// return voxel intensity at specific coordinates (xyz are zero indexed column row, slice)
|
|
19733
|
-
getValue(x, y, z, frame4D = 0, isReadImaginary = false) {
|
|
19734
|
-
if (!this.hdr) {
|
|
19735
|
-
throw new Error("hdr undefined");
|
|
19736
|
-
}
|
|
19737
|
-
if (!this.img) {
|
|
19738
|
-
throw new Error("img undefined");
|
|
19739
|
-
}
|
|
19740
|
-
const nx = this.hdr.dims[1];
|
|
19741
|
-
const ny = this.hdr.dims[2];
|
|
19742
|
-
const nz = this.hdr.dims[3];
|
|
19743
|
-
const perm = this.permRAS.slice();
|
|
19744
|
-
if (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3) {
|
|
19745
|
-
const pos = vec44.fromValues(x, y, z, 1);
|
|
19746
|
-
vec44.transformMat4(pos, pos, this.toRASvox);
|
|
19747
|
-
x = pos[0];
|
|
19748
|
-
y = pos[1];
|
|
19749
|
-
z = pos[2];
|
|
19750
|
-
}
|
|
19751
|
-
let vx = x + y * nx + z * nx * ny;
|
|
19752
|
-
if (this.hdr.datatypeCode === 2304 /* DT_RGBA32 */) {
|
|
19753
|
-
vx *= 4;
|
|
19754
|
-
return Math.round(this.img[vx] * 0.21 + this.img[vx + 1] * 0.72 + this.img[vx + 2] * 0.07);
|
|
19755
|
-
}
|
|
19756
|
-
if (this.hdr.datatypeCode === 128 /* DT_RGB24 */) {
|
|
19757
|
-
vx *= 3;
|
|
19758
|
-
return Math.round(this.img[vx] * 0.21 + this.img[vx + 1] * 0.72 + this.img[vx + 2] * 0.07);
|
|
19759
|
-
}
|
|
19760
|
-
const vol = frame4D * nx * ny * nz;
|
|
19761
|
-
let i = this.img[vx + vol];
|
|
19762
|
-
if (isReadImaginary) {
|
|
19763
|
-
i = this.imaginary[vx + vol];
|
|
19764
|
-
}
|
|
19765
|
-
return this.hdr.scl_slope * i + this.hdr.scl_inter;
|
|
19766
|
-
}
|
|
19767
20207
|
/**
|
|
19768
|
-
*
|
|
19769
|
-
*
|
|
19770
|
-
* @
|
|
20208
|
+
* Returns voxel intensity at specific native coordinates.
|
|
20209
|
+
* Delegates to VolumeUtils.getValue.
|
|
20210
|
+
* @param x - Native X coordinate (0-indexed)
|
|
20211
|
+
* @param y - Native Y coordinate (0-indexed)
|
|
20212
|
+
* @param z - Native Z coordinate (0-indexed)
|
|
20213
|
+
* @param frame4D - 4D frame index (0-indexed)
|
|
20214
|
+
* @param isReadImaginary - Flag to read from imaginary data array
|
|
20215
|
+
* @returns Scaled voxel intensity
|
|
19771
20216
|
*/
|
|
19772
|
-
|
|
19773
|
-
|
|
19774
|
-
const matRAS = this.matRAS;
|
|
19775
|
-
const pixDimsRAS = this.pixDimsRAS;
|
|
19776
|
-
const L = -0.5;
|
|
19777
|
-
const P = -0.5;
|
|
19778
|
-
const I = -0.5;
|
|
19779
|
-
const R = dimsRAS[1] - 1 + 0.5;
|
|
19780
|
-
const A = dimsRAS[2] - 1 + 0.5;
|
|
19781
|
-
const S = dimsRAS[3] - 1 + 0.5;
|
|
19782
|
-
const LPI = this.vox2mm([L, P, I], matRAS);
|
|
19783
|
-
const LAI = this.vox2mm([L, A, I], matRAS);
|
|
19784
|
-
const LPS = this.vox2mm([L, P, S], matRAS);
|
|
19785
|
-
const LAS = this.vox2mm([L, A, S], matRAS);
|
|
19786
|
-
const RPI = this.vox2mm([R, P, I], matRAS);
|
|
19787
|
-
const RAI = this.vox2mm([R, A, I], matRAS);
|
|
19788
|
-
const RPS = this.vox2mm([R, P, S], matRAS);
|
|
19789
|
-
const RAS = this.vox2mm([R, A, S], matRAS);
|
|
19790
|
-
const posTex = [
|
|
19791
|
-
// spatial position (XYZ), texture coordinates UVW
|
|
19792
|
-
// Superior face
|
|
19793
|
-
...LPS,
|
|
19794
|
-
...[0, 0, 1],
|
|
19795
|
-
...RPS,
|
|
19796
|
-
...[1, 0, 1],
|
|
19797
|
-
...RAS,
|
|
19798
|
-
...[1, 1, 1],
|
|
19799
|
-
...LAS,
|
|
19800
|
-
...[0, 1, 1],
|
|
19801
|
-
// Inferior face
|
|
19802
|
-
...LPI,
|
|
19803
|
-
...[0, 0, 0],
|
|
19804
|
-
...LAI,
|
|
19805
|
-
...[0, 1, 0],
|
|
19806
|
-
...RAI,
|
|
19807
|
-
...[1, 1, 0],
|
|
19808
|
-
...RPI,
|
|
19809
|
-
...[1, 0, 0]
|
|
19810
|
-
];
|
|
19811
|
-
const indexBuffer = gl.createBuffer();
|
|
19812
|
-
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
19813
|
-
const indices = [
|
|
19814
|
-
// six faces of cube: each has 2 triangles (6 indices)
|
|
19815
|
-
0,
|
|
19816
|
-
3,
|
|
19817
|
-
2,
|
|
19818
|
-
2,
|
|
19819
|
-
1,
|
|
19820
|
-
0,
|
|
19821
|
-
// Top
|
|
19822
|
-
4,
|
|
19823
|
-
7,
|
|
19824
|
-
6,
|
|
19825
|
-
6,
|
|
19826
|
-
5,
|
|
19827
|
-
4,
|
|
19828
|
-
// Bottom
|
|
19829
|
-
5,
|
|
19830
|
-
6,
|
|
19831
|
-
2,
|
|
19832
|
-
2,
|
|
19833
|
-
3,
|
|
19834
|
-
5,
|
|
19835
|
-
// Front
|
|
19836
|
-
4,
|
|
19837
|
-
0,
|
|
19838
|
-
1,
|
|
19839
|
-
1,
|
|
19840
|
-
7,
|
|
19841
|
-
4,
|
|
19842
|
-
// Back
|
|
19843
|
-
7,
|
|
19844
|
-
1,
|
|
19845
|
-
2,
|
|
19846
|
-
2,
|
|
19847
|
-
6,
|
|
19848
|
-
7,
|
|
19849
|
-
// Right
|
|
19850
|
-
4,
|
|
19851
|
-
5,
|
|
19852
|
-
3,
|
|
19853
|
-
3,
|
|
19854
|
-
0,
|
|
19855
|
-
4
|
|
19856
|
-
// Left
|
|
19857
|
-
];
|
|
19858
|
-
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
|
|
19859
|
-
const posTexBuffer = gl.createBuffer();
|
|
19860
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
|
|
19861
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posTex), gl.STATIC_DRAW);
|
|
19862
|
-
const vao = gl.createVertexArray();
|
|
19863
|
-
gl.bindVertexArray(vao);
|
|
19864
|
-
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
19865
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
|
|
19866
|
-
gl.enableVertexAttribArray(0);
|
|
19867
|
-
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
|
|
19868
|
-
gl.enableVertexAttribArray(1);
|
|
19869
|
-
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
|
|
19870
|
-
gl.bindVertexArray(null);
|
|
19871
|
-
const obj3D = new NiivueObject3D(id, posTexBuffer, gl.TRIANGLES, indices.length, indexBuffer, vao);
|
|
19872
|
-
const extents = getExtents([...LPS, ...RPS, ...RAS, ...LAS, ...LPI, ...LAI, ...RAI, ...RPI]);
|
|
19873
|
-
obj3D.extentsMin = extents.min.slice();
|
|
19874
|
-
obj3D.extentsMax = extents.max.slice();
|
|
19875
|
-
obj3D.furthestVertexFromOrigin = extents.furthestVertexFromOrigin;
|
|
19876
|
-
obj3D.originNegate = vec36.clone(extents.origin);
|
|
19877
|
-
vec36.negate(obj3D.originNegate, obj3D.originNegate);
|
|
19878
|
-
obj3D.fieldOfViewDeObliqueMM = [dimsRAS[1] * pixDimsRAS[1], dimsRAS[2] * pixDimsRAS[2], dimsRAS[3] * pixDimsRAS[3]];
|
|
19879
|
-
return obj3D;
|
|
20217
|
+
getValue(x, y, z, frame4D = 0, isReadImaginary = false) {
|
|
20218
|
+
return getValue(this, x, y, z, frame4D, isReadImaginary);
|
|
19880
20219
|
}
|
|
19881
20220
|
/**
|
|
19882
20221
|
* Update options for image
|
|
@@ -19920,89 +20259,18 @@ var NVImage = class _NVImage {
|
|
|
19920
20259
|
return options;
|
|
19921
20260
|
}
|
|
19922
20261
|
/**
|
|
19923
|
-
* Converts NVImage to NIfTI compliant byte array
|
|
20262
|
+
* Converts NVImage to NIfTI compliant byte array.
|
|
20263
|
+
* Handles potential re-orientation of drawing data.
|
|
20264
|
+
* Delegates to ImageWriter.toUint8Array.
|
|
20265
|
+
* @param drawingBytes - Optional Uint8Array drawing overlay
|
|
20266
|
+
* @returns Uint8Array
|
|
19924
20267
|
*/
|
|
19925
20268
|
toUint8Array(drawingBytes = null) {
|
|
19926
|
-
|
|
19927
|
-
const hdrBytes = hdrToArrayBuffer({ ...this.hdr, vox_offset: 352 }, isDrawing);
|
|
19928
|
-
let drawingBytesToBeConverted = drawingBytes;
|
|
19929
|
-
if (isDrawing) {
|
|
19930
|
-
const perm = this.permRAS;
|
|
19931
|
-
if (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3) {
|
|
19932
|
-
const dims = this.hdr.dims;
|
|
19933
|
-
const layout = [0, 0, 0];
|
|
19934
|
-
for (let i = 0; i < 3; i++) {
|
|
19935
|
-
for (let j2 = 0; j2 < 3; j2++) {
|
|
19936
|
-
if (Math.abs(perm[i]) - 1 !== j2) {
|
|
19937
|
-
continue;
|
|
19938
|
-
}
|
|
19939
|
-
layout[j2] = i * Math.sign(perm[i]);
|
|
19940
|
-
}
|
|
19941
|
-
}
|
|
19942
|
-
let stride = 1;
|
|
19943
|
-
const instride = [1, 1, 1];
|
|
19944
|
-
const inflip = [false, false, false];
|
|
19945
|
-
for (let i = 0; i < layout.length; i++) {
|
|
19946
|
-
for (let j2 = 0; j2 < layout.length; j2++) {
|
|
19947
|
-
const a = Math.abs(layout[j2]);
|
|
19948
|
-
if (a !== i) {
|
|
19949
|
-
continue;
|
|
19950
|
-
}
|
|
19951
|
-
instride[j2] = stride;
|
|
19952
|
-
if (layout[j2] < 0 || Object.is(layout[j2], -0)) {
|
|
19953
|
-
inflip[j2] = true;
|
|
19954
|
-
}
|
|
19955
|
-
stride *= dims[j2 + 1];
|
|
19956
|
-
}
|
|
19957
|
-
}
|
|
19958
|
-
let xlut = NVUtilities.range(0, dims[1] - 1, 1);
|
|
19959
|
-
if (inflip[0]) {
|
|
19960
|
-
xlut = NVUtilities.range(dims[1] - 1, 0, -1);
|
|
19961
|
-
}
|
|
19962
|
-
for (let i = 0; i < dims[1]; i++) {
|
|
19963
|
-
xlut[i] *= instride[0];
|
|
19964
|
-
}
|
|
19965
|
-
let ylut = NVUtilities.range(0, dims[2] - 1, 1);
|
|
19966
|
-
if (inflip[1]) {
|
|
19967
|
-
ylut = NVUtilities.range(dims[2] - 1, 0, -1);
|
|
19968
|
-
}
|
|
19969
|
-
for (let i = 0; i < dims[2]; i++) {
|
|
19970
|
-
ylut[i] *= instride[1];
|
|
19971
|
-
}
|
|
19972
|
-
let zlut = NVUtilities.range(0, dims[3] - 1, 1);
|
|
19973
|
-
if (inflip[2]) {
|
|
19974
|
-
zlut = NVUtilities.range(dims[3] - 1, 0, -1);
|
|
19975
|
-
}
|
|
19976
|
-
for (let i = 0; i < dims[3]; i++) {
|
|
19977
|
-
zlut[i] *= instride[2];
|
|
19978
|
-
}
|
|
19979
|
-
const inVs = new Uint8Array(drawingBytes);
|
|
19980
|
-
const outVs = new Uint8Array(dims[1] * dims[2] * dims[3]);
|
|
19981
|
-
let j = 0;
|
|
19982
|
-
for (let z = 0; z < dims[3]; z++) {
|
|
19983
|
-
for (let y = 0; y < dims[2]; y++) {
|
|
19984
|
-
for (let x = 0; x < dims[1]; x++) {
|
|
19985
|
-
outVs[j] = inVs[xlut[x] + ylut[y] + zlut[z]];
|
|
19986
|
-
j++;
|
|
19987
|
-
}
|
|
19988
|
-
}
|
|
19989
|
-
}
|
|
19990
|
-
drawingBytesToBeConverted = outVs;
|
|
19991
|
-
log.debug("drawing bytes");
|
|
19992
|
-
log.debug(drawingBytesToBeConverted);
|
|
19993
|
-
}
|
|
19994
|
-
}
|
|
19995
|
-
const img8 = isDrawing ? drawingBytesToBeConverted : new Uint8Array(this.img.buffer);
|
|
19996
|
-
const opad = new Uint8Array(4);
|
|
19997
|
-
const odata = new Uint8Array(hdrBytes.length + opad.length + img8.length);
|
|
19998
|
-
odata.set(hdrBytes);
|
|
19999
|
-
odata.set(opad, hdrBytes.length);
|
|
20000
|
-
odata.set(img8, hdrBytes.length + opad.length);
|
|
20001
|
-
return odata;
|
|
20269
|
+
return toUint8Array(this, drawingBytes);
|
|
20002
20270
|
}
|
|
20003
20271
|
// not included in public docs
|
|
20004
20272
|
convertVox2Frac(vox) {
|
|
20005
|
-
const frac =
|
|
20273
|
+
const frac = vec38.fromValues(
|
|
20006
20274
|
(vox[0] + 0.5) / this.dimsRAS[1],
|
|
20007
20275
|
(vox[1] + 0.5) / this.dimsRAS[2],
|
|
20008
20276
|
(vox[2] + 0.5) / this.dimsRAS[3]
|
|
@@ -20011,7 +20279,7 @@ var NVImage = class _NVImage {
|
|
|
20011
20279
|
}
|
|
20012
20280
|
// not included in public docs
|
|
20013
20281
|
convertFrac2Vox(frac) {
|
|
20014
|
-
const vox =
|
|
20282
|
+
const vox = vec38.fromValues(
|
|
20015
20283
|
Math.round(frac[0] * this.dims[1] - 0.5),
|
|
20016
20284
|
// dims === RAS
|
|
20017
20285
|
Math.round(frac[1] * this.dims[2] - 0.5),
|
|
@@ -20023,26 +20291,26 @@ var NVImage = class _NVImage {
|
|
|
20023
20291
|
}
|
|
20024
20292
|
// not included in public docs
|
|
20025
20293
|
convertFrac2MM(frac, isForceSliceMM = false) {
|
|
20026
|
-
const pos =
|
|
20294
|
+
const pos = vec45.fromValues(frac[0], frac[1], frac[2], 1);
|
|
20027
20295
|
if (isForceSliceMM) {
|
|
20028
|
-
|
|
20296
|
+
vec45.transformMat4(pos, pos, this.frac2mm);
|
|
20029
20297
|
} else {
|
|
20030
|
-
|
|
20298
|
+
vec45.transformMat4(pos, pos, this.frac2mmOrtho);
|
|
20031
20299
|
}
|
|
20032
20300
|
return pos;
|
|
20033
20301
|
}
|
|
20034
20302
|
// not included in public docs
|
|
20035
20303
|
convertMM2Frac(mm, isForceSliceMM = false) {
|
|
20036
|
-
const mm4 =
|
|
20304
|
+
const mm4 = vec45.fromValues(mm[0], mm[1], mm[2], 1);
|
|
20037
20305
|
const d = this.dimsRAS;
|
|
20038
|
-
const frac =
|
|
20306
|
+
const frac = vec38.fromValues(0, 0, 0);
|
|
20039
20307
|
if (typeof d === "undefined") {
|
|
20040
20308
|
return frac;
|
|
20041
20309
|
}
|
|
20042
20310
|
if (!isForceSliceMM) {
|
|
20043
|
-
const xform =
|
|
20044
|
-
|
|
20045
|
-
|
|
20311
|
+
const xform = mat46.clone(this.frac2mmOrtho);
|
|
20312
|
+
mat46.invert(xform, xform);
|
|
20313
|
+
vec45.transformMat4(mm4, mm4, xform);
|
|
20046
20314
|
frac[0] = mm4[0];
|
|
20047
20315
|
frac[1] = mm4[1];
|
|
20048
20316
|
frac[2] = mm4[2];
|
|
@@ -20051,10 +20319,10 @@ var NVImage = class _NVImage {
|
|
|
20051
20319
|
if (d[1] < 1 || d[2] < 1 || d[3] < 1) {
|
|
20052
20320
|
return frac;
|
|
20053
20321
|
}
|
|
20054
|
-
const sform =
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20322
|
+
const sform = mat46.clone(this.matRAS);
|
|
20323
|
+
mat46.invert(sform, sform);
|
|
20324
|
+
mat46.transpose(sform, sform);
|
|
20325
|
+
vec45.transformMat4(mm4, mm4, sform);
|
|
20058
20326
|
frac[0] = (mm4[0] + 0.5) / d[1];
|
|
20059
20327
|
frac[1] = (mm4[1] + 0.5) / d[2];
|
|
20060
20328
|
frac[2] = (mm4[2] + 0.5) / d[3];
|
|
@@ -20223,17 +20491,18 @@ var DEFAULT_OPTIONS = {
|
|
|
20223
20491
|
measureTextHeight: 0.03,
|
|
20224
20492
|
isAlphaClipDark: false,
|
|
20225
20493
|
gradientOrder: 1,
|
|
20226
|
-
gradientOpacity: 0
|
|
20494
|
+
gradientOpacity: 0,
|
|
20495
|
+
invertScrollDirection: false
|
|
20227
20496
|
};
|
|
20228
20497
|
var INITIAL_SCENE_DATA = {
|
|
20229
20498
|
gamma: 1,
|
|
20230
20499
|
azimuth: 110,
|
|
20231
20500
|
elevation: 10,
|
|
20232
|
-
crosshairPos:
|
|
20501
|
+
crosshairPos: vec39.fromValues(0.5, 0.5, 0.5),
|
|
20233
20502
|
clipPlane: [0, 0, 0, 0],
|
|
20234
20503
|
clipPlaneDepthAziElev: [2, 0, 0],
|
|
20235
20504
|
volScaleMultiplier: 1,
|
|
20236
|
-
pan2Dxyzmm:
|
|
20505
|
+
pan2Dxyzmm: vec46.fromValues(0, 0, 0, 1),
|
|
20237
20506
|
clipThick: 2,
|
|
20238
20507
|
clipVolumeLow: [0, 0, 0],
|
|
20239
20508
|
clipVolumeHigh: [1, 1, 1]
|
|
@@ -20264,8 +20533,8 @@ var NVDocument = class _NVDocument {
|
|
|
20264
20533
|
},
|
|
20265
20534
|
sceneData: {
|
|
20266
20535
|
...INITIAL_SCENE_DATA,
|
|
20267
|
-
pan2Dxyzmm:
|
|
20268
|
-
crosshairPos:
|
|
20536
|
+
pan2Dxyzmm: vec46.fromValues(0, 0, 0, 1),
|
|
20537
|
+
crosshairPos: vec39.fromValues(0.5, 0.5, 0.5)
|
|
20269
20538
|
},
|
|
20270
20539
|
get renderAzimuth() {
|
|
20271
20540
|
return this.sceneData.azimuth;
|
|
@@ -20891,7 +21160,7 @@ var NVMesh3 = class _NVMesh {
|
|
|
20891
21160
|
// given streamlines (which webGL renders as a single pixel), extrude to cylinders
|
|
20892
21161
|
linesToCylinders(gl, posClrF32, indices) {
|
|
20893
21162
|
function v4ToV3(v4) {
|
|
20894
|
-
return
|
|
21163
|
+
return vec310.fromValues(v4[0], v4[1], v4[2]);
|
|
20895
21164
|
}
|
|
20896
21165
|
const primitiveRestart = Math.pow(2, 32) - 1;
|
|
20897
21166
|
const n_count = indices.length;
|
|
@@ -20913,11 +21182,11 @@ var NVMesh3 = class _NVMesh {
|
|
|
20913
21182
|
const f32 = new Float32Array(npt * f32PerVertex);
|
|
20914
21183
|
const u8 = new Uint8Array(f32.buffer);
|
|
20915
21184
|
let vtx = 0;
|
|
20916
|
-
let prevV4 =
|
|
20917
|
-
let currV4 =
|
|
20918
|
-
let nextV4 =
|
|
20919
|
-
const v1 =
|
|
20920
|
-
let prevV2 =
|
|
21185
|
+
let prevV4 = vec47.create();
|
|
21186
|
+
let currV4 = vec47.create();
|
|
21187
|
+
let nextV4 = vec47.create();
|
|
21188
|
+
const v1 = vec310.create();
|
|
21189
|
+
let prevV2 = vec310.create();
|
|
20921
21190
|
let node = 0;
|
|
20922
21191
|
const radius = this.fiberRadius;
|
|
20923
21192
|
for (let i = 0; i < n_count; i++) {
|
|
@@ -20928,47 +21197,47 @@ var NVMesh3 = class _NVMesh {
|
|
|
20928
21197
|
let idx2 = indices[i] * 4;
|
|
20929
21198
|
node++;
|
|
20930
21199
|
if (node <= 1) {
|
|
20931
|
-
prevV4 =
|
|
20932
|
-
currV4 =
|
|
21200
|
+
prevV4 = vec47.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
|
|
21201
|
+
currV4 = vec47.clone(prevV4);
|
|
20933
21202
|
if (i + 1 < n_count && indices[i + 1] !== primitiveRestart) {
|
|
20934
21203
|
idx2 = indices[i + 1] * 4;
|
|
20935
|
-
nextV4 =
|
|
20936
|
-
|
|
20937
|
-
|
|
21204
|
+
nextV4 = vec47.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
|
|
21205
|
+
vec310.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4));
|
|
21206
|
+
vec310.normalize(v1, v1);
|
|
20938
21207
|
prevV2 = NiivueObject3D.getFirstPerpVector(v1);
|
|
20939
21208
|
}
|
|
20940
21209
|
continue;
|
|
20941
21210
|
}
|
|
20942
21211
|
if (isLineEnd) {
|
|
20943
|
-
nextV4 =
|
|
21212
|
+
nextV4 = vec47.clone(currV4);
|
|
20944
21213
|
} else {
|
|
20945
|
-
nextV4 =
|
|
20946
|
-
}
|
|
20947
|
-
|
|
20948
|
-
|
|
20949
|
-
const D =
|
|
20950
|
-
|
|
20951
|
-
const v2 =
|
|
20952
|
-
|
|
20953
|
-
prevV2 =
|
|
20954
|
-
const v3 =
|
|
20955
|
-
|
|
20956
|
-
|
|
20957
|
-
const vtxXYZ =
|
|
21214
|
+
nextV4 = vec47.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
|
|
21215
|
+
}
|
|
21216
|
+
vec310.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4));
|
|
21217
|
+
vec310.normalize(v1, v1);
|
|
21218
|
+
const D = vec310.create();
|
|
21219
|
+
vec310.cross(D, prevV2, v1);
|
|
21220
|
+
const v2 = vec310.create();
|
|
21221
|
+
vec310.cross(v2, v1, D);
|
|
21222
|
+
prevV2 = vec310.clone(prevV2);
|
|
21223
|
+
const v3 = vec310.create();
|
|
21224
|
+
vec310.cross(v3, v1, v2);
|
|
21225
|
+
vec310.normalize(v3, v3);
|
|
21226
|
+
const vtxXYZ = vec310.create();
|
|
20958
21227
|
for (let j = 0; j < cyl_sides; j++) {
|
|
20959
21228
|
const c = Math.cos(j / cyl_sides * 2 * Math.PI);
|
|
20960
21229
|
const s = Math.sin(j / cyl_sides * 2 * Math.PI);
|
|
20961
21230
|
vtxXYZ[0] = radius * (c * v2[0] + s * v3[0]);
|
|
20962
21231
|
vtxXYZ[1] = radius * (c * v2[1] + s * v3[1]);
|
|
20963
21232
|
vtxXYZ[2] = radius * (c * v2[2] + s * v3[2]);
|
|
20964
|
-
|
|
21233
|
+
vec310.add(vtxXYZ, v4ToV3(currV4), vtxXYZ);
|
|
20965
21234
|
const fidx = vtx * f32PerVertex;
|
|
20966
21235
|
f32[fidx + 0] = vtxXYZ[0];
|
|
20967
21236
|
f32[fidx + 1] = vtxXYZ[1];
|
|
20968
21237
|
f32[fidx + 2] = vtxXYZ[2];
|
|
20969
|
-
const n3 =
|
|
20970
|
-
|
|
20971
|
-
|
|
21238
|
+
const n3 = vec310.create();
|
|
21239
|
+
vec310.subtract(n3, vtxXYZ, v4ToV3(currV4));
|
|
21240
|
+
vec310.normalize(n3, n3);
|
|
20972
21241
|
const fidxU8 = (fidx + 3) * 4;
|
|
20973
21242
|
u8[fidxU8 + 0] = n3[0] * 127;
|
|
20974
21243
|
u8[fidxU8 + 1] = n3[1] * 127;
|
|
@@ -20976,8 +21245,8 @@ var NVMesh3 = class _NVMesh {
|
|
|
20976
21245
|
f32[fidx + 4] = currV4[3];
|
|
20977
21246
|
vtx++;
|
|
20978
21247
|
}
|
|
20979
|
-
prevV4 =
|
|
20980
|
-
currV4 =
|
|
21248
|
+
prevV4 = vec47.clone(currV4);
|
|
21249
|
+
currV4 = vec47.clone(nextV4);
|
|
20981
21250
|
if (isLineEnd) {
|
|
20982
21251
|
node = 0;
|
|
20983
21252
|
}
|
|
@@ -21145,8 +21414,8 @@ var NVMesh3 = class _NVMesh {
|
|
|
21145
21414
|
const vEnd3 = (offsetPt0[i + 1] - 1) * 3;
|
|
21146
21415
|
let len = 0;
|
|
21147
21416
|
for (let j = vStart3; j < vEnd3; j += 3) {
|
|
21148
|
-
const v =
|
|
21149
|
-
len +=
|
|
21417
|
+
const v = vec310.fromValues(pts[j + 0] - pts[j + 3], pts[j + 1] - pts[j + 4], pts[j + 2] - pts[j + 5]);
|
|
21418
|
+
len += vec310.len(v);
|
|
21150
21419
|
}
|
|
21151
21420
|
this.fiberLengths[i] = len;
|
|
21152
21421
|
}
|
|
@@ -21173,8 +21442,8 @@ var NVMesh3 = class _NVMesh {
|
|
|
21173
21442
|
return r + (g << 8) + (b << 16);
|
|
21174
21443
|
}
|
|
21175
21444
|
function direction2rgb(x1, y1, z1, x2, y2, z2, ditherFrac) {
|
|
21176
|
-
const v =
|
|
21177
|
-
|
|
21445
|
+
const v = vec310.fromValues(Math.abs(x1 - x2), Math.abs(y1 - y2), Math.abs(z1 - z2));
|
|
21446
|
+
vec310.normalize(v, v);
|
|
21178
21447
|
const r = ditherFrac - ditherHalf;
|
|
21179
21448
|
for (let j = 0; j < 3; j++) {
|
|
21180
21449
|
v[j] = 255 * Math.max(Math.min(Math.abs(v[j]) + r, 1), 0);
|
|
@@ -21820,9 +22089,14 @@ var NVMesh3 = class _NVMesh {
|
|
|
21820
22089
|
// TODO this method is too generic
|
|
21821
22090
|
setProperty(key, val, gl) {
|
|
21822
22091
|
if (!(key in this)) {
|
|
21823
|
-
|
|
22092
|
+
console.warn("Mesh does not have property:", key, this);
|
|
21824
22093
|
return;
|
|
21825
22094
|
}
|
|
22095
|
+
if (typeof val !== "number" && typeof val !== "string" && typeof val !== "boolean") {
|
|
22096
|
+
console.warn("Invalid value type. Expected number, string, or boolean but received:", typeof val);
|
|
22097
|
+
return;
|
|
22098
|
+
}
|
|
22099
|
+
;
|
|
21826
22100
|
this[key] = val;
|
|
21827
22101
|
this.updateMesh(gl);
|
|
21828
22102
|
}
|
|
@@ -23849,7 +24123,7 @@ var NVLabel3D = class {
|
|
|
23849
24123
|
};
|
|
23850
24124
|
|
|
23851
24125
|
// src/nvconnectome.ts
|
|
23852
|
-
import { vec3 as
|
|
24126
|
+
import { vec3 as vec311 } from "gl-matrix";
|
|
23853
24127
|
var defaultOptions = {
|
|
23854
24128
|
name: "untitled connectome",
|
|
23855
24129
|
nodeColormap: "warm",
|
|
@@ -24142,7 +24416,7 @@ var NVConnectome = class _NVConnectome extends NVMesh3 {
|
|
|
24142
24416
|
if (isNeg) {
|
|
24143
24417
|
rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
|
|
24144
24418
|
}
|
|
24145
|
-
const pt =
|
|
24419
|
+
const pt = vec311.fromValues(nodes[i].x, nodes[i].y, nodes[i].z);
|
|
24146
24420
|
NiivueObject3D.makeColoredSphere(pts, tris, rgba255, radius, pt, rgba);
|
|
24147
24421
|
}
|
|
24148
24422
|
lut = cmapper.colormap(this.edgeColormap, this.colormapInvert);
|
|
@@ -24183,8 +24457,8 @@ var NVConnectome = class _NVConnectome extends NVMesh3 {
|
|
|
24183
24457
|
if (isNeg) {
|
|
24184
24458
|
rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
|
|
24185
24459
|
}
|
|
24186
|
-
const pti =
|
|
24187
|
-
const ptj =
|
|
24460
|
+
const pti = vec311.fromValues(nodes[edge.first].x, nodes[edge.first].y, nodes[edge.first].z);
|
|
24461
|
+
const ptj = vec311.fromValues(nodes[edge.second].x, nodes[edge.second].y, nodes[edge.second].z);
|
|
24188
24462
|
NiivueObject3D.makeColoredCylinder(pts, tris, rgba255, pti, ptj, radius, rgba);
|
|
24189
24463
|
}
|
|
24190
24464
|
}
|
|
@@ -24224,8 +24498,141 @@ var NVConnectome = class _NVConnectome extends NVMesh3 {
|
|
|
24224
24498
|
}
|
|
24225
24499
|
};
|
|
24226
24500
|
|
|
24501
|
+
// src/nvimage/RenderingUtils.ts
|
|
24502
|
+
import { vec3 as vec312 } from "gl-matrix";
|
|
24503
|
+
function toNiivueObject3D(nvImage, id, gl) {
|
|
24504
|
+
if (!nvImage.dimsRAS || !nvImage.matRAS || !nvImage.pixDimsRAS || !nvImage.vox2mm) {
|
|
24505
|
+
throw new Error("Cannot create NiivueObject3D: Missing required RAS properties or vox2mm access on NVImage.");
|
|
24506
|
+
}
|
|
24507
|
+
const dimsRAS = nvImage.dimsRAS;
|
|
24508
|
+
const matRAS = nvImage.matRAS;
|
|
24509
|
+
const pixDimsRAS = nvImage.pixDimsRAS;
|
|
24510
|
+
const L = -0.5;
|
|
24511
|
+
const P = -0.5;
|
|
24512
|
+
const I = -0.5;
|
|
24513
|
+
const R = dimsRAS[1] - 1 + 0.5;
|
|
24514
|
+
const A = dimsRAS[2] - 1 + 0.5;
|
|
24515
|
+
const S = dimsRAS[3] - 1 + 0.5;
|
|
24516
|
+
const vox2mmFn = nvImage.vox2mm;
|
|
24517
|
+
const LPI = vox2mmFn.call(nvImage, [L, P, I], matRAS);
|
|
24518
|
+
const LAI = vox2mmFn.call(nvImage, [L, A, I], matRAS);
|
|
24519
|
+
const LPS = vox2mmFn.call(nvImage, [L, P, S], matRAS);
|
|
24520
|
+
const LAS = vox2mmFn.call(nvImage, [L, A, S], matRAS);
|
|
24521
|
+
const RPI = vox2mmFn.call(nvImage, [R, P, I], matRAS);
|
|
24522
|
+
const RAI = vox2mmFn.call(nvImage, [R, A, I], matRAS);
|
|
24523
|
+
const RPS = vox2mmFn.call(nvImage, [R, P, S], matRAS);
|
|
24524
|
+
const RAS = vox2mmFn.call(nvImage, [R, A, S], matRAS);
|
|
24525
|
+
const posTex = [
|
|
24526
|
+
// Superior face vertices (Indices 0-3)
|
|
24527
|
+
...LPS,
|
|
24528
|
+
...[0, 0, 1],
|
|
24529
|
+
// 0
|
|
24530
|
+
...RPS,
|
|
24531
|
+
...[1, 0, 1],
|
|
24532
|
+
// 1
|
|
24533
|
+
...RAS,
|
|
24534
|
+
...[1, 1, 1],
|
|
24535
|
+
// 2
|
|
24536
|
+
...LAS,
|
|
24537
|
+
...[0, 1, 1],
|
|
24538
|
+
// 3
|
|
24539
|
+
// Inferior face vertices (Indices 4-7)
|
|
24540
|
+
...LPI,
|
|
24541
|
+
...[0, 0, 0],
|
|
24542
|
+
// 4
|
|
24543
|
+
...LAI,
|
|
24544
|
+
...[0, 1, 0],
|
|
24545
|
+
// 5
|
|
24546
|
+
...RAI,
|
|
24547
|
+
...[1, 1, 0],
|
|
24548
|
+
// 6
|
|
24549
|
+
...RPI,
|
|
24550
|
+
...[1, 0, 0]
|
|
24551
|
+
// 7
|
|
24552
|
+
];
|
|
24553
|
+
const indexBuffer = gl.createBuffer();
|
|
24554
|
+
if (!indexBuffer) {
|
|
24555
|
+
throw new Error("Failed to create GL index buffer");
|
|
24556
|
+
}
|
|
24557
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
24558
|
+
const indices = [
|
|
24559
|
+
0,
|
|
24560
|
+
3,
|
|
24561
|
+
2,
|
|
24562
|
+
2,
|
|
24563
|
+
1,
|
|
24564
|
+
0,
|
|
24565
|
+
// Top
|
|
24566
|
+
4,
|
|
24567
|
+
7,
|
|
24568
|
+
6,
|
|
24569
|
+
6,
|
|
24570
|
+
5,
|
|
24571
|
+
4,
|
|
24572
|
+
// Bottom
|
|
24573
|
+
5,
|
|
24574
|
+
6,
|
|
24575
|
+
2,
|
|
24576
|
+
2,
|
|
24577
|
+
3,
|
|
24578
|
+
5,
|
|
24579
|
+
// Front -> Corresponds to LAI(5), RAI(6), RAS(2) / RAS(2), LAS(3), LAI(5)
|
|
24580
|
+
4,
|
|
24581
|
+
0,
|
|
24582
|
+
1,
|
|
24583
|
+
1,
|
|
24584
|
+
7,
|
|
24585
|
+
4,
|
|
24586
|
+
// Back -> Corresponds to LPI(4), LPS(0), RPS(1) / RPS(1), RPI(7), LPI(4)
|
|
24587
|
+
7,
|
|
24588
|
+
1,
|
|
24589
|
+
2,
|
|
24590
|
+
2,
|
|
24591
|
+
6,
|
|
24592
|
+
7,
|
|
24593
|
+
// Right -> Corresponds to RPI(7), RPS(1), RAS(2) / RAS(2), RAI(6), RPI(7)
|
|
24594
|
+
4,
|
|
24595
|
+
5,
|
|
24596
|
+
3,
|
|
24597
|
+
3,
|
|
24598
|
+
0,
|
|
24599
|
+
4
|
|
24600
|
+
// Left -> Corresponds to LPI(4), LAI(5), LAS(3) / LAS(3), LPS(0), LPI(4)
|
|
24601
|
+
];
|
|
24602
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
|
|
24603
|
+
const posTexBuffer = gl.createBuffer();
|
|
24604
|
+
if (!posTexBuffer) {
|
|
24605
|
+
throw new Error("Failed to create GL vertex buffer");
|
|
24606
|
+
}
|
|
24607
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
|
|
24608
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posTex), gl.STATIC_DRAW);
|
|
24609
|
+
const vao = gl.createVertexArray();
|
|
24610
|
+
if (!vao) {
|
|
24611
|
+
throw new Error("Failed to create GL VAO");
|
|
24612
|
+
}
|
|
24613
|
+
gl.bindVertexArray(vao);
|
|
24614
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
24615
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
|
|
24616
|
+
const stride = 24;
|
|
24617
|
+
gl.enableVertexAttribArray(0);
|
|
24618
|
+
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0);
|
|
24619
|
+
gl.enableVertexAttribArray(1);
|
|
24620
|
+
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, stride, 12);
|
|
24621
|
+
gl.bindVertexArray(null);
|
|
24622
|
+
const obj3D = new NiivueObject3D(id, posTexBuffer, gl.TRIANGLES, indices.length, indexBuffer, vao);
|
|
24623
|
+
const allCorners = [...LPS, ...RPS, ...RAS, ...LAS, ...LPI, ...LAI, ...RAI, ...RPI];
|
|
24624
|
+
const extents = getExtents(allCorners);
|
|
24625
|
+
obj3D.extentsMin = extents.min.slice();
|
|
24626
|
+
obj3D.extentsMax = extents.max.slice();
|
|
24627
|
+
obj3D.furthestVertexFromOrigin = extents.furthestVertexFromOrigin;
|
|
24628
|
+
obj3D.originNegate = vec312.clone(extents.origin);
|
|
24629
|
+
vec312.negate(obj3D.originNegate, obj3D.originNegate);
|
|
24630
|
+
obj3D.fieldOfViewDeObliqueMM = [dimsRAS[1] * pixDimsRAS[1], dimsRAS[2] * pixDimsRAS[2], dimsRAS[3] * pixDimsRAS[3]];
|
|
24631
|
+
return obj3D;
|
|
24632
|
+
}
|
|
24633
|
+
|
|
24227
24634
|
// src/niivue/utils.ts
|
|
24228
|
-
import { mat4 as
|
|
24635
|
+
import { mat4 as mat48, vec3 as vec313, vec4 as vec48 } from "gl-matrix";
|
|
24229
24636
|
function readFileAsDataURL(input) {
|
|
24230
24637
|
return new Promise((resolve, reject) => {
|
|
24231
24638
|
let filePromise;
|
|
@@ -24382,27 +24789,27 @@ function negMinMax(min, max, minNeg, maxNeg) {
|
|
|
24382
24789
|
return [mn, mx];
|
|
24383
24790
|
}
|
|
24384
24791
|
function swizzleVec3(vec, order = [0, 1, 2]) {
|
|
24385
|
-
const vout =
|
|
24792
|
+
const vout = vec313.create();
|
|
24386
24793
|
vout[0] = vec[order[0]];
|
|
24387
24794
|
vout[1] = vec[order[1]];
|
|
24388
24795
|
vout[2] = vec[order[2]];
|
|
24389
24796
|
return vout;
|
|
24390
24797
|
}
|
|
24391
24798
|
function isRadiological(mtx) {
|
|
24392
|
-
const vRight =
|
|
24393
|
-
const vRotated =
|
|
24394
|
-
|
|
24799
|
+
const vRight = vec48.fromValues(1, 0, 0, 0);
|
|
24800
|
+
const vRotated = vec48.create();
|
|
24801
|
+
vec48.transformMat4(vRotated, vRight, mtx);
|
|
24395
24802
|
return vRotated[0];
|
|
24396
24803
|
}
|
|
24397
24804
|
function unProject(winX, winY, winZ, mvpMatrix) {
|
|
24398
|
-
const inp =
|
|
24399
|
-
const finalMatrix =
|
|
24400
|
-
|
|
24805
|
+
const inp = vec48.fromValues(winX, winY, winZ, 1);
|
|
24806
|
+
const finalMatrix = mat48.clone(mvpMatrix);
|
|
24807
|
+
mat48.invert(finalMatrix, finalMatrix);
|
|
24401
24808
|
inp[0] = inp[0] * 2 - 1;
|
|
24402
24809
|
inp[1] = inp[1] * 2 - 1;
|
|
24403
24810
|
inp[2] = inp[2] * 2 - 1;
|
|
24404
|
-
const out =
|
|
24405
|
-
|
|
24811
|
+
const out = vec48.create();
|
|
24812
|
+
vec48.transformMat4(out, inp, finalMatrix);
|
|
24406
24813
|
if (out[3] === 0) {
|
|
24407
24814
|
return out;
|
|
24408
24815
|
}
|
|
@@ -24674,6 +25081,8 @@ var Niivue = class {
|
|
|
24674
25081
|
__publicField(this, "extentsMax");
|
|
24675
25082
|
// ResizeObserver
|
|
24676
25083
|
__publicField(this, "resizeObserver", null);
|
|
25084
|
+
__publicField(this, "resizeEventListener", null);
|
|
25085
|
+
__publicField(this, "canvasObserver", null);
|
|
24677
25086
|
// syncOpts: Record<string, unknown> = {}
|
|
24678
25087
|
__publicField(this, "syncOpts", {
|
|
24679
25088
|
"3d": false,
|
|
@@ -25062,6 +25471,42 @@ var Niivue = class {
|
|
|
25062
25471
|
set isAlphaClipDark(newVal) {
|
|
25063
25472
|
this.document.opts.isAlphaClipDark = newVal;
|
|
25064
25473
|
}
|
|
25474
|
+
/**
|
|
25475
|
+
* Clean up event listeners and observers
|
|
25476
|
+
* Call this when the Niivue instance is no longer needed.
|
|
25477
|
+
* This will be called when the canvas is detached from the DOM
|
|
25478
|
+
* @example niivue.cleanup();
|
|
25479
|
+
*/
|
|
25480
|
+
cleanup() {
|
|
25481
|
+
if (this.resizeEventListener) {
|
|
25482
|
+
window.removeEventListener("resize", this.resizeEventListener);
|
|
25483
|
+
this.resizeEventListener = null;
|
|
25484
|
+
}
|
|
25485
|
+
if (this.resizeObserver) {
|
|
25486
|
+
this.resizeObserver.disconnect();
|
|
25487
|
+
this.resizeObserver = null;
|
|
25488
|
+
}
|
|
25489
|
+
if (this.canvasObserver) {
|
|
25490
|
+
this.canvasObserver.disconnect();
|
|
25491
|
+
this.canvasObserver = null;
|
|
25492
|
+
}
|
|
25493
|
+
if (this.canvas && this.opts.interactive) {
|
|
25494
|
+
this.canvas.removeEventListener("mousedown", this.mouseDownListener.bind(this));
|
|
25495
|
+
this.canvas.removeEventListener("mouseup", this.mouseUpListener.bind(this));
|
|
25496
|
+
this.canvas.removeEventListener("mousemove", this.mouseMoveListener.bind(this));
|
|
25497
|
+
this.canvas.removeEventListener("touchstart", this.touchStartListener.bind(this));
|
|
25498
|
+
this.canvas.removeEventListener("touchend", this.touchEndListener.bind(this));
|
|
25499
|
+
this.canvas.removeEventListener("touchmove", this.touchMoveListener.bind(this));
|
|
25500
|
+
this.canvas.removeEventListener("wheel", this.wheelListener.bind(this));
|
|
25501
|
+
this.canvas.removeEventListener("contextmenu", this.mouseContextMenuListener.bind(this));
|
|
25502
|
+
this.canvas.removeEventListener("dblclick", this.resetBriCon.bind(this));
|
|
25503
|
+
this.canvas.removeEventListener("dragenter", this.dragEnterListener.bind(this));
|
|
25504
|
+
this.canvas.removeEventListener("dragover", this.dragOverListener.bind(this));
|
|
25505
|
+
this.canvas.removeEventListener("drop", this.dropListener.bind(this));
|
|
25506
|
+
this.canvas.removeEventListener("keyup", this.keyUpListener.bind(this));
|
|
25507
|
+
this.canvas.removeEventListener("keydown", this.keyDownListener.bind(this));
|
|
25508
|
+
}
|
|
25509
|
+
}
|
|
25065
25510
|
get volumes() {
|
|
25066
25511
|
return this.document.volumes;
|
|
25067
25512
|
}
|
|
@@ -25124,7 +25569,7 @@ var Niivue = class {
|
|
|
25124
25569
|
* @param id - the id of an html canvas element
|
|
25125
25570
|
* @param isAntiAlias - determines if anti-aliasing is requested (if not specified, AA usage depends on hardware)
|
|
25126
25571
|
* @example niivue = new Niivue().attachTo('gl')
|
|
25127
|
-
* @example niivue.attachTo('gl')
|
|
25572
|
+
* @example await niivue.attachTo('gl')
|
|
25128
25573
|
* @see {@link https://niivue.github.io/niivue/features/basic.multiplanar.html | live demo usage}
|
|
25129
25574
|
*/
|
|
25130
25575
|
async attachTo(id, isAntiAlias = null) {
|
|
@@ -25137,7 +25582,7 @@ var Niivue = class {
|
|
|
25137
25582
|
* @param canvas - the canvas element reference
|
|
25138
25583
|
* @example
|
|
25139
25584
|
* niivue = new Niivue()
|
|
25140
|
-
* niivue.attachToCanvas(document.getElementById(id))
|
|
25585
|
+
* await niivue.attachToCanvas(document.getElementById(id))
|
|
25141
25586
|
*/
|
|
25142
25587
|
async attachToCanvas(canvas, isAntiAlias = null) {
|
|
25143
25588
|
this.canvas = canvas;
|
|
@@ -25157,17 +25602,27 @@ var Niivue = class {
|
|
|
25157
25602
|
this.canvas.style.display = "block";
|
|
25158
25603
|
this.canvas.width = this.canvas.offsetWidth;
|
|
25159
25604
|
this.canvas.height = this.canvas.offsetHeight;
|
|
25160
|
-
|
|
25605
|
+
this.resizeEventListener = () => {
|
|
25161
25606
|
requestAnimationFrame(() => {
|
|
25162
25607
|
this.resizeListener();
|
|
25163
25608
|
});
|
|
25164
|
-
}
|
|
25609
|
+
};
|
|
25610
|
+
window.addEventListener("resize", this.resizeEventListener);
|
|
25165
25611
|
this.resizeObserver = new ResizeObserver(() => {
|
|
25166
25612
|
requestAnimationFrame(() => {
|
|
25167
25613
|
this.resizeListener();
|
|
25168
25614
|
});
|
|
25169
25615
|
});
|
|
25170
25616
|
this.resizeObserver.observe(this.canvas.parentElement);
|
|
25617
|
+
this.canvasObserver = new MutationObserver((mutations) => {
|
|
25618
|
+
for (const mutation of mutations) {
|
|
25619
|
+
if (mutation.type === "childList" && mutation.removedNodes.length > 0 && Array.from(mutation.removedNodes).includes(this.canvas)) {
|
|
25620
|
+
this.cleanup();
|
|
25621
|
+
break;
|
|
25622
|
+
}
|
|
25623
|
+
}
|
|
25624
|
+
});
|
|
25625
|
+
this.canvasObserver.observe(this.canvas.parentElement, { childList: true });
|
|
25171
25626
|
}
|
|
25172
25627
|
if (this.opts.interactive) {
|
|
25173
25628
|
this.registerInteractions();
|
|
@@ -25220,7 +25675,7 @@ var Niivue = class {
|
|
|
25220
25675
|
doSync2d(otherNV) {
|
|
25221
25676
|
const thisMM = this.frac2mm(this.scene.crosshairPos);
|
|
25222
25677
|
otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
|
|
25223
|
-
otherNV.scene.pan2Dxyzmm =
|
|
25678
|
+
otherNV.scene.pan2Dxyzmm = vec49.clone(this.scene.pan2Dxyzmm);
|
|
25224
25679
|
}
|
|
25225
25680
|
doSyncGamma(otherNV) {
|
|
25226
25681
|
const thisGamma = this.scene.gamma;
|
|
@@ -25230,7 +25685,7 @@ var Niivue = class {
|
|
|
25230
25685
|
}
|
|
25231
25686
|
}
|
|
25232
25687
|
doSyncZoomPan(otherNV) {
|
|
25233
|
-
otherNV.scene.pan2Dxyzmm =
|
|
25688
|
+
otherNV.scene.pan2Dxyzmm = vec49.clone(this.scene.pan2Dxyzmm);
|
|
25234
25689
|
}
|
|
25235
25690
|
doSyncCrosshair(otherNV) {
|
|
25236
25691
|
const thisMM = this.frac2mm(this.scene.crosshairPos);
|
|
@@ -25467,7 +25922,7 @@ var Niivue = class {
|
|
|
25467
25922
|
}
|
|
25468
25923
|
this.setDragStart(pos.x, pos.y);
|
|
25469
25924
|
if (!this.uiData.isDragging) {
|
|
25470
|
-
this.uiData.pan2DxyzmmAtMouseDown =
|
|
25925
|
+
this.uiData.pan2DxyzmmAtMouseDown = vec49.clone(this.scene.pan2Dxyzmm);
|
|
25471
25926
|
}
|
|
25472
25927
|
this.uiData.isDragging = true;
|
|
25473
25928
|
this.uiData.dragClipPlaneStartDepthAziElev = this.scene.clipPlaneDepthAziElev;
|
|
@@ -25483,7 +25938,7 @@ var Niivue = class {
|
|
|
25483
25938
|
}
|
|
25484
25939
|
this.setDragStart(pos.x, pos.y);
|
|
25485
25940
|
if (!this.uiData.isDragging) {
|
|
25486
|
-
this.uiData.pan2DxyzmmAtMouseDown =
|
|
25941
|
+
this.uiData.pan2DxyzmmAtMouseDown = vec49.clone(this.scene.pan2Dxyzmm);
|
|
25487
25942
|
}
|
|
25488
25943
|
this.uiData.isDragging = true;
|
|
25489
25944
|
this.uiData.dragClipPlaneStartDepthAziElev = this.scene.clipPlaneDepthAziElev;
|
|
@@ -25574,9 +26029,9 @@ var Niivue = class {
|
|
|
25574
26029
|
}
|
|
25575
26030
|
const mmStart = this.frac2mm(fracStart);
|
|
25576
26031
|
const mmEnd = this.frac2mm(fracEnd);
|
|
25577
|
-
const v =
|
|
25578
|
-
|
|
25579
|
-
const mmLength =
|
|
26032
|
+
const v = vec314.create();
|
|
26033
|
+
vec314.sub(v, vec314.fromValues(mmStart[0], mmStart[1], mmStart[2]), vec314.fromValues(mmEnd[0], mmEnd[1], mmEnd[2]));
|
|
26034
|
+
const mmLength = vec314.len(v);
|
|
25580
26035
|
const voxStart = this.frac2vox(fracStart);
|
|
25581
26036
|
const voxEnd = this.frac2vox(fracEnd);
|
|
25582
26037
|
this.onDragRelease({
|
|
@@ -25613,16 +26068,7 @@ var Niivue = class {
|
|
|
25613
26068
|
this.uiData.mouseButtonLeftDown = false;
|
|
25614
26069
|
if (this.drawPenFillPts.length > 0) {
|
|
25615
26070
|
this.drawPenFilled();
|
|
25616
|
-
} else if (this.drawPenAxCorSag >= 0) {
|
|
25617
|
-
if (this.opts.clickToSegment) {
|
|
25618
|
-
const sumBitmap = this.sumBitmap(this.drawBitmap);
|
|
25619
|
-
const sumSeg = this.sumBitmap(this.clickToSegmentGrowingBitmap);
|
|
25620
|
-
if (sumSeg > sumBitmap) {
|
|
25621
|
-
this.updateBitmapFromClickToSegment();
|
|
25622
|
-
}
|
|
25623
|
-
}
|
|
25624
26071
|
}
|
|
25625
|
-
this.drawAddUndoBitmap();
|
|
25626
26072
|
this.drawPenLocation = [NaN, NaN, NaN];
|
|
25627
26073
|
this.drawPenAxCorSag = -1;
|
|
25628
26074
|
if (isFunction(this.onMouseUp)) {
|
|
@@ -25760,6 +26206,30 @@ var Niivue = class {
|
|
|
25760
26206
|
this.uiData.windowY = y;
|
|
25761
26207
|
}
|
|
25762
26208
|
// not included in public docs
|
|
26209
|
+
// handler for mouse leaving the canvas
|
|
26210
|
+
mouseLeaveListener() {
|
|
26211
|
+
if (this.clickToSegmentIsGrowing) {
|
|
26212
|
+
log.debug("Mouse left canvas, stopping clickToSegment preview.");
|
|
26213
|
+
this.clickToSegmentIsGrowing = false;
|
|
26214
|
+
this.refreshDrawing(true, false);
|
|
26215
|
+
}
|
|
26216
|
+
if (this.opts.drawingEnabled && !isNaN(this.drawPenLocation[0])) {
|
|
26217
|
+
log.debug("Mouse left canvas during drawing, resetting pen state.");
|
|
26218
|
+
this.drawPenLocation = [NaN, NaN, NaN];
|
|
26219
|
+
this.drawPenAxCorSag = -1;
|
|
26220
|
+
this.drawPenFillPts = [];
|
|
26221
|
+
}
|
|
26222
|
+
if (this.uiData.isDragging) {
|
|
26223
|
+
log.debug("Mouse left canvas during drag, resetting drag state.");
|
|
26224
|
+
this.uiData.isDragging = false;
|
|
26225
|
+
this.uiData.mouseButtonLeftDown = false;
|
|
26226
|
+
this.uiData.mouseButtonCenterDown = false;
|
|
26227
|
+
this.uiData.mouseButtonRightDown = false;
|
|
26228
|
+
this.uiData.mousedown = false;
|
|
26229
|
+
this.drawScene();
|
|
26230
|
+
}
|
|
26231
|
+
}
|
|
26232
|
+
// not included in public docs
|
|
25763
26233
|
// handler for mouse move over canvas
|
|
25764
26234
|
// note: no test yet
|
|
25765
26235
|
mouseMoveListener(e) {
|
|
@@ -25798,7 +26268,21 @@ var Niivue = class {
|
|
|
25798
26268
|
const x = pos.x * this.uiData.dpr;
|
|
25799
26269
|
const y = pos.y * this.uiData.dpr;
|
|
25800
26270
|
this.mousePos = [x, y];
|
|
25801
|
-
this.
|
|
26271
|
+
const tileIdx = this.tileIndex(x, y);
|
|
26272
|
+
if (tileIdx >= 0 && this.opts.drawingEnabled) {
|
|
26273
|
+
const axCorSag = this.screenSlices[tileIdx].axCorSag;
|
|
26274
|
+
if (axCorSag <= 2 /* SAGITTAL */) {
|
|
26275
|
+
this.clickToSegmentXY = [x, y];
|
|
26276
|
+
this.clickToSegmentIsGrowing = true;
|
|
26277
|
+
this.doClickToSegment({
|
|
26278
|
+
x,
|
|
26279
|
+
// Screen X
|
|
26280
|
+
y,
|
|
26281
|
+
// Screen Y
|
|
26282
|
+
tileIndex: tileIdx
|
|
26283
|
+
});
|
|
26284
|
+
}
|
|
26285
|
+
}
|
|
25802
26286
|
}
|
|
25803
26287
|
}
|
|
25804
26288
|
// not included in public docs
|
|
@@ -25869,7 +26353,7 @@ var Niivue = class {
|
|
|
25869
26353
|
if (this.uiData.touchdown && e.touches.length < 2) {
|
|
25870
26354
|
const rect = this.canvas.getBoundingClientRect();
|
|
25871
26355
|
if (!this.uiData.isDragging) {
|
|
25872
|
-
this.uiData.pan2DxyzmmAtMouseDown =
|
|
26356
|
+
this.uiData.pan2DxyzmmAtMouseDown = vec49.clone(this.scene.pan2Dxyzmm);
|
|
25873
26357
|
}
|
|
25874
26358
|
this.uiData.isDragging = true;
|
|
25875
26359
|
if (this.uiData.doubleTouch && this.uiData.isDragging) {
|
|
@@ -26017,22 +26501,65 @@ var Niivue = class {
|
|
|
26017
26501
|
this.drawScene();
|
|
26018
26502
|
this.uiData.isDragging = false;
|
|
26019
26503
|
const tileIdx = this.tileIndex(this.uiData.dragStart[0], this.uiData.dragStart[1]);
|
|
26020
|
-
|
|
26021
|
-
this.
|
|
26022
|
-
|
|
26023
|
-
|
|
26504
|
+
if (tileIdx >= 0) {
|
|
26505
|
+
this.generateMouseUpCallback(
|
|
26506
|
+
this.screenXY2TextureFrac(this.uiData.dragStart[0], this.uiData.dragStart[1], tileIdx),
|
|
26507
|
+
this.screenXY2TextureFrac(this.uiData.dragEnd[0], this.uiData.dragEnd[1], tileIdx)
|
|
26508
|
+
);
|
|
26509
|
+
} else {
|
|
26510
|
+
log.warn("Could not generate drag release callback for ROI selection: Invalid tile index.");
|
|
26511
|
+
}
|
|
26512
|
+
return;
|
|
26513
|
+
}
|
|
26514
|
+
let scrollAmount = e.deltaY < 0 ? -0.01 : 0.01;
|
|
26515
|
+
if (this.opts.invertScrollDirection) {
|
|
26516
|
+
scrollAmount = -scrollAmount;
|
|
26517
|
+
}
|
|
26518
|
+
if (this.opts.clickToSegment) {
|
|
26519
|
+
if (scrollAmount < 0) {
|
|
26520
|
+
this.opts.clickToSegmentPercent -= 0.01;
|
|
26521
|
+
this.opts.clickToSegmentPercent = Math.max(this.opts.clickToSegmentPercent, 0);
|
|
26522
|
+
} else {
|
|
26523
|
+
this.opts.clickToSegmentPercent += 0.01;
|
|
26524
|
+
this.opts.clickToSegmentPercent = Math.min(this.opts.clickToSegmentPercent, 1);
|
|
26525
|
+
}
|
|
26526
|
+
const x2 = this.clickToSegmentXY[0];
|
|
26527
|
+
const y2 = this.clickToSegmentXY[1];
|
|
26528
|
+
const tileIdx = this.tileIndex(x2, y2);
|
|
26529
|
+
if (tileIdx >= 0 && this.screenSlices[tileIdx].axCorSag <= 2 /* SAGITTAL */) {
|
|
26530
|
+
log.debug(`Adjusting clickToSegment threshold: ${this.opts.clickToSegmentPercent.toFixed(3)}`);
|
|
26531
|
+
this.clickToSegmentIsGrowing = true;
|
|
26532
|
+
this.doClickToSegment({ x: x2, y: y2, tileIndex: tileIdx });
|
|
26533
|
+
}
|
|
26024
26534
|
return;
|
|
26025
26535
|
}
|
|
26026
26536
|
const rect = this.canvas.getBoundingClientRect();
|
|
26027
|
-
|
|
26028
|
-
|
|
26029
|
-
|
|
26030
|
-
|
|
26537
|
+
const x = e.clientX - rect.left;
|
|
26538
|
+
const y = e.clientY - rect.top;
|
|
26539
|
+
if (this.opts.dragMode === 3 /* pan */ && this.inRenderTile(this.uiData.dpr * x, this.uiData.dpr * y) === -1) {
|
|
26540
|
+
const zoomDirection = scrollAmount < 0 ? 1 : -1;
|
|
26541
|
+
let zoom = this.scene.pan2Dxyzmm[3] * (1 + 10 * (0.01 * zoomDirection));
|
|
26542
|
+
zoom = Math.round(zoom * 10) / 10;
|
|
26543
|
+
const zoomChange = this.scene.pan2Dxyzmm[3] - zoom;
|
|
26544
|
+
if (this.opts.yoke3Dto2DZoom) {
|
|
26545
|
+
this.scene.volScaleMultiplier = zoom;
|
|
26546
|
+
}
|
|
26547
|
+
this.scene.pan2Dxyzmm[3] = zoom;
|
|
26548
|
+
const mm = this.frac2mm(this.scene.crosshairPos);
|
|
26549
|
+
this.scene.pan2Dxyzmm[0] += zoomChange * mm[0];
|
|
26550
|
+
this.scene.pan2Dxyzmm[1] += zoomChange * mm[1];
|
|
26551
|
+
this.scene.pan2Dxyzmm[2] += zoomChange * mm[2];
|
|
26552
|
+
this.drawScene();
|
|
26553
|
+
this.canvas.focus();
|
|
26554
|
+
this.sync();
|
|
26555
|
+
return;
|
|
26031
26556
|
}
|
|
26557
|
+
this.sliceScroll2D(scrollAmount, x, y);
|
|
26032
26558
|
}
|
|
26033
26559
|
// not included in public docs
|
|
26034
26560
|
// setup interactions with the canvas. Mouse clicks and touches
|
|
26035
26561
|
// note: no test yet
|
|
26562
|
+
// note: any event listeners registered here should also be removed in `cleanup()`
|
|
26036
26563
|
registerInteractions() {
|
|
26037
26564
|
if (!this.canvas) {
|
|
26038
26565
|
throw new Error("canvas undefined");
|
|
@@ -26040,6 +26567,7 @@ var Niivue = class {
|
|
|
26040
26567
|
this.canvas.addEventListener("mousedown", this.mouseDownListener.bind(this));
|
|
26041
26568
|
this.canvas.addEventListener("mouseup", this.mouseUpListener.bind(this));
|
|
26042
26569
|
this.canvas.addEventListener("mousemove", this.mouseMoveListener.bind(this));
|
|
26570
|
+
this.canvas.addEventListener("mouseleave", this.mouseLeaveListener.bind(this));
|
|
26043
26571
|
this.canvas.addEventListener("touchstart", this.touchStartListener.bind(this));
|
|
26044
26572
|
this.canvas.addEventListener("touchend", this.touchEndListener.bind(this));
|
|
26045
26573
|
this.canvas.addEventListener("touchmove", this.touchMoveListener.bind(this));
|
|
@@ -26048,7 +26576,13 @@ var Niivue = class {
|
|
|
26048
26576
|
this.canvas.addEventListener("dblclick", this.resetBriCon.bind(this));
|
|
26049
26577
|
this.canvas.addEventListener("dragenter", this.dragEnterListener.bind(this), false);
|
|
26050
26578
|
this.canvas.addEventListener("dragover", this.dragOverListener.bind(this), false);
|
|
26051
|
-
this.canvas.addEventListener(
|
|
26579
|
+
this.canvas.addEventListener(
|
|
26580
|
+
"drop",
|
|
26581
|
+
(event) => {
|
|
26582
|
+
this.dropListener(event).catch(console.error);
|
|
26583
|
+
},
|
|
26584
|
+
false
|
|
26585
|
+
);
|
|
26052
26586
|
this.canvas.setAttribute("tabindex", "0");
|
|
26053
26587
|
this.canvas.addEventListener("keyup", this.keyUpListener.bind(this), false);
|
|
26054
26588
|
this.canvas.addEventListener("keydown", this.keyDownListener.bind(this), false);
|
|
@@ -26284,9 +26818,9 @@ var Niivue = class {
|
|
|
26284
26818
|
registers an external file loader for niivue to use when reading files.
|
|
26285
26819
|
|
|
26286
26820
|
the loader must return an array buffer of the file contents for niivue
|
|
26287
|
-
to parse and the extension of the file so niivue can infer the file type to load.
|
|
26821
|
+
to parse and the extension of the file so niivue can infer the file type to load.
|
|
26288
26822
|
|
|
26289
|
-
example:
|
|
26823
|
+
example:
|
|
26290
26824
|
|
|
26291
26825
|
const myCustomLoader = (File) => {
|
|
26292
26826
|
// ... do parsing here ...
|
|
@@ -27578,6 +28112,14 @@ var Niivue = class {
|
|
|
27578
28112
|
if (!this.drawBitmap) {
|
|
27579
28113
|
this.createEmptyDrawing();
|
|
27580
28114
|
}
|
|
28115
|
+
} else {
|
|
28116
|
+
if (this.clickToSegmentIsGrowing) {
|
|
28117
|
+
this.clickToSegmentIsGrowing = false;
|
|
28118
|
+
this.refreshDrawing(true, false);
|
|
28119
|
+
}
|
|
28120
|
+
this.drawPenLocation = [NaN, NaN, NaN];
|
|
28121
|
+
this.drawPenAxCorSag = -1;
|
|
28122
|
+
this.drawPenFillPts = [];
|
|
27581
28123
|
}
|
|
27582
28124
|
this.drawScene();
|
|
27583
28125
|
}
|
|
@@ -27969,7 +28511,7 @@ var Niivue = class {
|
|
|
27969
28511
|
}
|
|
27970
28512
|
|
|
27971
28513
|
var nv1 = new Niivue();
|
|
27972
|
-
nv1.attachTo("${canvasId}");
|
|
28514
|
+
await nv1.attachTo("${canvasId}");
|
|
27973
28515
|
var base64 = "${base64}";
|
|
27974
28516
|
NVUtilities.decompressBase64String(base64).then((jsonText) => {
|
|
27975
28517
|
var json = JSON.parse(jsonText); // string -> JSON
|
|
@@ -28544,7 +29086,10 @@ var Niivue = class {
|
|
|
28544
29086
|
}
|
|
28545
29087
|
// not included in public docs
|
|
28546
29088
|
// set color of single voxel in drawing
|
|
28547
|
-
drawPt(x, y, z, penValue) {
|
|
29089
|
+
drawPt(x, y, z, penValue, drawBitmap = null) {
|
|
29090
|
+
if (drawBitmap === null) {
|
|
29091
|
+
drawBitmap = this.drawBitmap;
|
|
29092
|
+
}
|
|
28548
29093
|
if (!this.back?.dims) {
|
|
28549
29094
|
throw new Error("back.dims not set");
|
|
28550
29095
|
}
|
|
@@ -28554,7 +29099,7 @@ var Niivue = class {
|
|
|
28554
29099
|
x = Math.min(Math.max(x, 0), dx - 1);
|
|
28555
29100
|
y = Math.min(Math.max(y, 0), dy - 1);
|
|
28556
29101
|
z = Math.min(Math.max(z, 0), dz - 1);
|
|
28557
|
-
|
|
29102
|
+
drawBitmap[x + y * dx + z * dx * dy] = penValue;
|
|
28558
29103
|
const isAx = this.drawPenAxCorSag === 0;
|
|
28559
29104
|
const isCor = this.drawPenAxCorSag === 1;
|
|
28560
29105
|
const isSag = this.drawPenAxCorSag === 2;
|
|
@@ -28563,11 +29108,11 @@ var Niivue = class {
|
|
|
28563
29108
|
for (let i = -halfPenSize; i <= halfPenSize; i++) {
|
|
28564
29109
|
for (let j = -halfPenSize; j <= halfPenSize; j++) {
|
|
28565
29110
|
if (isAx) {
|
|
28566
|
-
|
|
29111
|
+
drawBitmap[x + i + (y + j) * dx + z * dx * dy] = penValue;
|
|
28567
29112
|
} else if (isCor) {
|
|
28568
|
-
|
|
29113
|
+
drawBitmap[x + i + y * dx + (z + j) * dx * dy] = penValue;
|
|
28569
29114
|
} else if (isSag) {
|
|
28570
|
-
|
|
29115
|
+
drawBitmap[x + (y + j) * dx + (z + i) * dx * dy] = penValue;
|
|
28571
29116
|
}
|
|
28572
29117
|
}
|
|
28573
29118
|
}
|
|
@@ -28826,9 +29371,34 @@ var Niivue = class {
|
|
|
28826
29371
|
}
|
|
28827
29372
|
// not included in public docs
|
|
28828
29373
|
// set all connected voxels in drawing to new color
|
|
28829
|
-
drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY, is2D = false) {
|
|
29374
|
+
drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY, is2D = false, targetBitmap = null, isGrowClusterTool = false) {
|
|
28830
29375
|
if (!this.drawBitmap) {
|
|
28831
|
-
|
|
29376
|
+
log.warn("drawFloodFill called without an initialized drawBitmap.");
|
|
29377
|
+
this.createEmptyDrawing();
|
|
29378
|
+
if (!this.drawBitmap) {
|
|
29379
|
+
log.error("Failed to create drawing bitmap.");
|
|
29380
|
+
return;
|
|
29381
|
+
}
|
|
29382
|
+
}
|
|
29383
|
+
if (this.clickToSegmentIsGrowing && !this.clickToSegmentGrowingBitmap) {
|
|
29384
|
+
log.warn("drawFloodFill called in preview mode without initialized clickToSegmentGrowingBitmap.");
|
|
29385
|
+
if (this.drawBitmap) {
|
|
29386
|
+
this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
|
|
29387
|
+
} else {
|
|
29388
|
+
log.error("Cannot initialize growing bitmap as drawBitmap is null.");
|
|
29389
|
+
return;
|
|
29390
|
+
}
|
|
29391
|
+
if (!this.clickToSegmentGrowingBitmap) {
|
|
29392
|
+
log.error("Failed to create growing bitmap.");
|
|
29393
|
+
return;
|
|
29394
|
+
}
|
|
29395
|
+
}
|
|
29396
|
+
if (targetBitmap === null) {
|
|
29397
|
+
targetBitmap = this.drawBitmap;
|
|
29398
|
+
}
|
|
29399
|
+
if (!targetBitmap) {
|
|
29400
|
+
log.error("drawFloodFill targetBitmap is null.");
|
|
29401
|
+
return;
|
|
28832
29402
|
}
|
|
28833
29403
|
if (!this.back?.dims) {
|
|
28834
29404
|
throw new Error("back.dims undefined");
|
|
@@ -28844,15 +29414,12 @@ var Niivue = class {
|
|
|
28844
29414
|
const nx = dims[0];
|
|
28845
29415
|
const nxy = nx * dims[1];
|
|
28846
29416
|
const nxyz = nxy * dims[2];
|
|
28847
|
-
|
|
28848
|
-
|
|
28849
|
-
|
|
28850
|
-
img = this.clickToSegmentGrowingBitmap.slice();
|
|
28851
|
-
drawBitmap = this.clickToSegmentGrowingBitmap;
|
|
28852
|
-
}
|
|
28853
|
-
if (img.length !== nxy * dims[2]) {
|
|
29417
|
+
const originalBitmap = this.clickToSegmentIsGrowing ? this.drawBitmap : targetBitmap;
|
|
29418
|
+
if (!originalBitmap) {
|
|
29419
|
+
log.error("Could not determine original bitmap state.");
|
|
28854
29420
|
return;
|
|
28855
29421
|
}
|
|
29422
|
+
const img = new Uint8Array(nxyz).fill(0);
|
|
28856
29423
|
let constrainXYZ = -1;
|
|
28857
29424
|
if (is2D && this.drawPenAxCorSag === 0 /* AXIAL */) {
|
|
28858
29425
|
constrainXYZ = 2;
|
|
@@ -28885,73 +29452,141 @@ var Niivue = class {
|
|
|
28885
29452
|
return dist2 <= maxDistanceMM2;
|
|
28886
29453
|
}
|
|
28887
29454
|
const seedVx = xyz2vx(seedXYZ);
|
|
28888
|
-
const
|
|
28889
|
-
if (
|
|
28890
|
-
|
|
28891
|
-
|
|
28892
|
-
|
|
28893
|
-
|
|
29455
|
+
const originalSeedColor = originalBitmap[seedVx];
|
|
29456
|
+
if (isGrowClusterTool && originalSeedColor === 0) {
|
|
29457
|
+
log.debug("Grow/Erase Cluster tool requires starting on a masked voxel.");
|
|
29458
|
+
if (this.clickToSegmentIsGrowing && this.clickToSegmentGrowingBitmap && this.drawBitmap) {
|
|
29459
|
+
this.clickToSegmentGrowingBitmap.set(this.drawBitmap);
|
|
29460
|
+
this.refreshDrawing(true, true);
|
|
28894
29461
|
}
|
|
28895
29462
|
return;
|
|
28896
29463
|
}
|
|
28897
|
-
|
|
28898
|
-
|
|
28899
|
-
if (
|
|
28900
|
-
|
|
28901
|
-
continue;
|
|
28902
|
-
}
|
|
28903
|
-
img[i] = 1;
|
|
29464
|
+
if (growSelectedCluster === 0 && originalSeedColor === newColor && !isGrowClusterTool && newColor !== 0) {
|
|
29465
|
+
log.debug("drawFloodFill selected voxel is already desired color");
|
|
29466
|
+
if (!this.clickToSegmentIsGrowing) {
|
|
29467
|
+
return;
|
|
28904
29468
|
}
|
|
28905
29469
|
}
|
|
28906
|
-
|
|
28907
|
-
if (growSelectedCluster
|
|
29470
|
+
let baseIntensity = NaN;
|
|
29471
|
+
if (isGrowClusterTool && (growSelectedCluster === Number.POSITIVE_INFINITY || growSelectedCluster === Number.NEGATIVE_INFINITY)) {
|
|
29472
|
+
const tempImgForIdentification = originalBitmap.slice();
|
|
29473
|
+
for (let i = 0; i < nxyz; i++) {
|
|
29474
|
+
tempImgForIdentification[i] = tempImgForIdentification[i] === originalSeedColor && isWithinDistance(i) ? 1 : 0;
|
|
29475
|
+
}
|
|
29476
|
+
if (tempImgForIdentification[seedVx] !== 1) {
|
|
29477
|
+
log.error("Seed voxel could not be marked for cluster ID.");
|
|
29478
|
+
return;
|
|
29479
|
+
}
|
|
29480
|
+
this.drawFloodFillCore(tempImgForIdentification, seedVx, neighbors);
|
|
28908
29481
|
const backImg = this.volumes[0].img2RAS();
|
|
28909
|
-
let
|
|
28910
|
-
let
|
|
28911
|
-
|
|
28912
|
-
|
|
28913
|
-
|
|
29482
|
+
let clusterSum = 0;
|
|
29483
|
+
let clusterCount = 0;
|
|
29484
|
+
for (let i = 0; i < nxyz; i++) {
|
|
29485
|
+
if (tempImgForIdentification[i] === 2) {
|
|
29486
|
+
clusterSum += backImg[i];
|
|
29487
|
+
clusterCount++;
|
|
29488
|
+
}
|
|
29489
|
+
}
|
|
29490
|
+
baseIntensity = clusterCount > 0 ? clusterSum / clusterCount : backImg[seedVx];
|
|
29491
|
+
log.debug(`Grow Cluster using mean intensity: ${baseIntensity.toFixed(2)} from ${clusterCount} voxels.`);
|
|
29492
|
+
let fillMin = -Infinity;
|
|
29493
|
+
let fillMax = Infinity;
|
|
29494
|
+
if (growSelectedCluster === Number.POSITIVE_INFINITY) {
|
|
29495
|
+
fillMin = baseIntensity;
|
|
29496
|
+
}
|
|
29497
|
+
if (growSelectedCluster === Number.NEGATIVE_INFINITY) {
|
|
29498
|
+
fillMax = baseIntensity;
|
|
29499
|
+
}
|
|
29500
|
+
for (let i = 0; i < nxyz; i++) {
|
|
29501
|
+
if (tempImgForIdentification[i] === 2) {
|
|
29502
|
+
img[i] = 1;
|
|
29503
|
+
} else if (originalBitmap[i] === 0) {
|
|
29504
|
+
const intensity = backImg[i];
|
|
29505
|
+
if (intensity >= fillMin && intensity <= fillMax && isWithinDistance(i)) {
|
|
29506
|
+
img[i] = 1;
|
|
29507
|
+
}
|
|
29508
|
+
}
|
|
29509
|
+
}
|
|
29510
|
+
newColor = originalSeedColor;
|
|
29511
|
+
} else {
|
|
29512
|
+
if (growSelectedCluster === 0) {
|
|
29513
|
+
if (isGrowClusterTool && newColor === 0) {
|
|
29514
|
+
log.debug(`Erase Cluster: Identifying cluster with color ${originalSeedColor}`);
|
|
29515
|
+
for (let i = 0; i < nxyz; i++) {
|
|
29516
|
+
img[i] = originalBitmap[i] === originalSeedColor && isWithinDistance(i) ? 1 : 0;
|
|
29517
|
+
}
|
|
29518
|
+
} else {
|
|
29519
|
+
for (let i = 0; i < nxyz; i++) {
|
|
29520
|
+
if (originalBitmap[i] === originalSeedColor && isWithinDistance(i)) {
|
|
29521
|
+
if (originalSeedColor !== 0) {
|
|
29522
|
+
img[i] = 1;
|
|
29523
|
+
}
|
|
29524
|
+
}
|
|
29525
|
+
}
|
|
29526
|
+
}
|
|
28914
29527
|
} else {
|
|
28915
|
-
|
|
28916
|
-
|
|
28917
|
-
|
|
28918
|
-
|
|
29528
|
+
const backImg = this.volumes[0].img2RAS();
|
|
29529
|
+
baseIntensity = backImg[seedVx];
|
|
29530
|
+
let fillMin = -Infinity;
|
|
29531
|
+
let fillMax = Infinity;
|
|
29532
|
+
if (isFinite(forceMin) && isFinite(forceMax)) {
|
|
29533
|
+
fillMin = forceMin;
|
|
29534
|
+
fillMax = forceMax;
|
|
29535
|
+
} else if (growSelectedCluster === Number.POSITIVE_INFINITY) {
|
|
29536
|
+
fillMin = baseIntensity;
|
|
29537
|
+
} else if (growSelectedCluster === Number.NEGATIVE_INFINITY) {
|
|
29538
|
+
fillMax = baseIntensity;
|
|
29539
|
+
}
|
|
29540
|
+
for (let i = 0; i < nxyz; i++) {
|
|
29541
|
+
const intensity = backImg[i];
|
|
29542
|
+
if (intensity >= fillMin && intensity <= fillMax && isWithinDistance(i)) {
|
|
29543
|
+
img[i] = 1;
|
|
28919
29544
|
}
|
|
28920
29545
|
}
|
|
28921
|
-
|
|
28922
|
-
|
|
29546
|
+
newColor = originalBitmap[seedVx];
|
|
29547
|
+
if (newColor === 0) {
|
|
29548
|
+
newColor = this.opts.penValue;
|
|
29549
|
+
if (newColor < 1 || !isFinite(newColor)) {
|
|
29550
|
+
newColor = 1;
|
|
29551
|
+
}
|
|
29552
|
+
}
|
|
29553
|
+
}
|
|
29554
|
+
}
|
|
29555
|
+
if (img[seedVx] !== 1) {
|
|
29556
|
+
let isSeedValidOriginal = false;
|
|
29557
|
+
if (isGrowClusterTool && growSelectedCluster !== 0) {
|
|
29558
|
+
if (originalSeedColor !== 0) {
|
|
29559
|
+
isSeedValidOriginal = true;
|
|
28923
29560
|
}
|
|
28924
|
-
|
|
28925
|
-
|
|
29561
|
+
} else {
|
|
29562
|
+
if (originalSeedColor !== 0 || newColor === 0) {
|
|
29563
|
+
isSeedValidOriginal = true;
|
|
28926
29564
|
}
|
|
28927
29565
|
}
|
|
28928
|
-
|
|
28929
|
-
|
|
28930
|
-
|
|
28931
|
-
|
|
28932
|
-
|
|
28933
|
-
|
|
28934
|
-
|
|
28935
|
-
img[i] = 1;
|
|
29566
|
+
if (isSeedValidOriginal && isWithinDistance(seedVx)) {
|
|
29567
|
+
img[seedVx] = 1;
|
|
29568
|
+
log.debug("Forcing seed voxel to 1 in working buffer.");
|
|
29569
|
+
} else {
|
|
29570
|
+
log.debug("Seed voxel not marked as candidate '1' and not valid originally.");
|
|
29571
|
+
if (this.clickToSegmentIsGrowing && this.clickToSegmentGrowingBitmap && this.drawBitmap) {
|
|
29572
|
+
this.clickToSegmentGrowingBitmap.set(this.drawBitmap);
|
|
28936
29573
|
}
|
|
29574
|
+
return;
|
|
28937
29575
|
}
|
|
28938
|
-
this.drawFloodFillCore(img, seedVx, neighbors);
|
|
28939
|
-
newColor = seedColor;
|
|
28940
29576
|
}
|
|
28941
|
-
|
|
29577
|
+
this.drawFloodFillCore(img, seedVx, neighbors);
|
|
29578
|
+
for (let i = 0; i < nxyz; i++) {
|
|
28942
29579
|
if (img[i] === 2) {
|
|
28943
|
-
|
|
29580
|
+
targetBitmap[i] = newColor;
|
|
29581
|
+
} else if (this.clickToSegmentIsGrowing && targetBitmap === this.clickToSegmentGrowingBitmap) {
|
|
29582
|
+
targetBitmap[i] = originalBitmap[i];
|
|
28944
29583
|
}
|
|
28945
29584
|
}
|
|
28946
29585
|
if (!this.clickToSegmentIsGrowing) {
|
|
28947
|
-
this.
|
|
28948
|
-
|
|
28949
|
-
this.drawAddUndoBitmap();
|
|
28950
|
-
}
|
|
28951
|
-
this.refreshDrawing(true, this.clickToSegmentIsGrowing);
|
|
29586
|
+
this.drawAddUndoBitmap();
|
|
29587
|
+
this.refreshDrawing(true, false);
|
|
28952
29588
|
} else {
|
|
28953
|
-
this.
|
|
28954
|
-
this.refreshDrawing(true, this.clickToSegmentIsGrowing);
|
|
29589
|
+
this.refreshDrawing(true, true);
|
|
28955
29590
|
}
|
|
28956
29591
|
}
|
|
28957
29592
|
// not included in public docs
|
|
@@ -29122,36 +29757,67 @@ var Niivue = class {
|
|
|
29122
29757
|
* @see {@link https://niivue.github.io/niivue/features/cactus.html | live demo usage}
|
|
29123
29758
|
*/
|
|
29124
29759
|
refreshDrawing(isForceRedraw = true, useClickToSegmentBitmap = false) {
|
|
29760
|
+
if (useClickToSegmentBitmap && (!this.opts.drawingEnabled || !this.opts.clickToSegment)) {
|
|
29761
|
+
log.debug("refreshDrawing: Conditions not met for clickToSegment bitmap, using drawBitmap.");
|
|
29762
|
+
useClickToSegmentBitmap = false;
|
|
29763
|
+
}
|
|
29764
|
+
const selectedBitmap = useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap;
|
|
29765
|
+
if (!selectedBitmap && !useClickToSegmentBitmap && this.clickToSegmentGrowingBitmap) {
|
|
29766
|
+
log.warn("refreshDrawing: drawBitmap is null, but clickToSegmentGrowingBitmap exists. Check state.");
|
|
29767
|
+
} else if (!selectedBitmap && useClickToSegmentBitmap && this.drawBitmap) {
|
|
29768
|
+
log.warn("refreshDrawing: clickToSegmentGrowingBitmap is null, falling back to drawBitmap.");
|
|
29769
|
+
useClickToSegmentBitmap = false;
|
|
29770
|
+
} else if (!selectedBitmap) {
|
|
29771
|
+
log.warn("refreshDrawing: Both bitmaps are null. Uploading empty data.");
|
|
29772
|
+
}
|
|
29773
|
+
const bitmapDataSource = useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap;
|
|
29125
29774
|
if (!this.back?.dims) {
|
|
29126
|
-
|
|
29127
|
-
|
|
29128
|
-
if (!this.drawBitmap) {
|
|
29129
|
-
throw new Error("drawBitmap undefined");
|
|
29775
|
+
log.warn("refreshDrawing: back.dims undefined, cannot refresh drawing texture yet.");
|
|
29776
|
+
return;
|
|
29130
29777
|
}
|
|
29131
29778
|
const dims = this.back.dims.slice();
|
|
29132
29779
|
const vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3];
|
|
29133
|
-
if (
|
|
29780
|
+
if (!bitmapDataSource) {
|
|
29781
|
+
log.warn(
|
|
29782
|
+
`refreshDrawing: Bitmap data source (${useClickToSegmentBitmap ? "growing" : "main"}) is null. Cannot update texture.`
|
|
29783
|
+
);
|
|
29784
|
+
if (isForceRedraw) {
|
|
29785
|
+
this.drawScene();
|
|
29786
|
+
}
|
|
29787
|
+
return;
|
|
29788
|
+
}
|
|
29789
|
+
if (bitmapDataSource.length === 8) {
|
|
29134
29790
|
dims[1] = 2;
|
|
29135
29791
|
dims[2] = 2;
|
|
29136
29792
|
dims[3] = 2;
|
|
29137
|
-
} else if (vx !==
|
|
29138
|
-
log.warn(
|
|
29793
|
+
} else if (vx !== bitmapDataSource.length) {
|
|
29794
|
+
log.warn(`Drawing bitmap length (${bitmapDataSource.length}) must match the background image (${vx})`);
|
|
29139
29795
|
}
|
|
29140
29796
|
this.gl.activeTexture(TEXTURE7_DRAW);
|
|
29141
29797
|
this.gl.bindTexture(this.gl.TEXTURE_3D, this.drawTexture);
|
|
29142
|
-
this.
|
|
29143
|
-
|
|
29144
|
-
|
|
29145
|
-
|
|
29146
|
-
|
|
29147
|
-
|
|
29148
|
-
|
|
29149
|
-
|
|
29150
|
-
|
|
29151
|
-
|
|
29152
|
-
|
|
29153
|
-
|
|
29154
|
-
|
|
29798
|
+
if (!this.drawTexture) {
|
|
29799
|
+
log.error("refreshDrawing: drawTexture (GPU texture) is null.");
|
|
29800
|
+
return;
|
|
29801
|
+
}
|
|
29802
|
+
try {
|
|
29803
|
+
this.gl.texSubImage3D(
|
|
29804
|
+
this.gl.TEXTURE_3D,
|
|
29805
|
+
0,
|
|
29806
|
+
0,
|
|
29807
|
+
0,
|
|
29808
|
+
0,
|
|
29809
|
+
dims[1],
|
|
29810
|
+
dims[2],
|
|
29811
|
+
dims[3],
|
|
29812
|
+
this.gl.RED,
|
|
29813
|
+
this.gl.UNSIGNED_BYTE,
|
|
29814
|
+
bitmapDataSource
|
|
29815
|
+
// Use the determined source
|
|
29816
|
+
);
|
|
29817
|
+
} catch (error) {
|
|
29818
|
+
log.error("Error during texSubImage3D in refreshDrawing:", error);
|
|
29819
|
+
log.error("Dimensions:", dims, "Bitmap length:", bitmapDataSource?.length);
|
|
29820
|
+
}
|
|
29155
29821
|
if (isForceRedraw) {
|
|
29156
29822
|
this.drawScene();
|
|
29157
29823
|
}
|
|
@@ -29989,10 +30655,10 @@ var Niivue = class {
|
|
|
29989
30655
|
if (this.crosshairs3D) {
|
|
29990
30656
|
this.crosshairs3D.mm[0] = NaN;
|
|
29991
30657
|
}
|
|
29992
|
-
let mtx =
|
|
30658
|
+
let mtx = mat49.clone(overlayItem.toRAS);
|
|
29993
30659
|
if (layer === 0) {
|
|
29994
|
-
this.volumeObject3D =
|
|
29995
|
-
|
|
30660
|
+
this.volumeObject3D = toNiivueObject3D(overlayItem, this.VOLUME_ID, this.gl);
|
|
30661
|
+
mat49.invert(mtx, mtx);
|
|
29996
30662
|
this.back.matRAS = overlayItem.matRAS;
|
|
29997
30663
|
this.back.dims = overlayItem.dimsRAS;
|
|
29998
30664
|
this.back.pixDims = overlayItem.pixDimsRAS;
|
|
@@ -30022,10 +30688,10 @@ var Niivue = class {
|
|
|
30022
30688
|
let f100 = this.mm2frac(overlayItem.mm100, 0, true);
|
|
30023
30689
|
let f010 = this.mm2frac(overlayItem.mm010, 0, true);
|
|
30024
30690
|
let f001 = this.mm2frac(overlayItem.mm001, 0, true);
|
|
30025
|
-
f100 =
|
|
30026
|
-
f010 =
|
|
30027
|
-
f001 =
|
|
30028
|
-
mtx =
|
|
30691
|
+
f100 = vec314.subtract(f100, f100, f000);
|
|
30692
|
+
f010 = vec314.subtract(f010, f010, f000);
|
|
30693
|
+
f001 = vec314.subtract(f001, f001, f000);
|
|
30694
|
+
mtx = mat49.fromValues(
|
|
30029
30695
|
f100[0],
|
|
30030
30696
|
f010[0],
|
|
30031
30697
|
f001[0],
|
|
@@ -30043,7 +30709,7 @@ var Niivue = class {
|
|
|
30043
30709
|
0,
|
|
30044
30710
|
1
|
|
30045
30711
|
);
|
|
30046
|
-
|
|
30712
|
+
mat49.invert(mtx, mtx);
|
|
30047
30713
|
if (layer === 1) {
|
|
30048
30714
|
outTexture = this.rgbaTex(this.overlayTexture, TEXTURE2_OVERLAY_VOL, this.back.dims);
|
|
30049
30715
|
this.overlayTexture = outTexture;
|
|
@@ -30917,7 +31583,7 @@ var Niivue = class {
|
|
|
30917
31583
|
// https://github.com/nipy/nibabel/blob/a2e5dee05cf374c22670ff9fd0d385ce366eb495/nibabel/freesurfer/mghformat.py#L30
|
|
30918
31584
|
conformVox2Vox(inDims, inAffine, outDim = 256, outMM = 1, toRAS = false) {
|
|
30919
31585
|
const a = inAffine.flat();
|
|
30920
|
-
const affine =
|
|
31586
|
+
const affine = mat49.fromValues(
|
|
30921
31587
|
a[0],
|
|
30922
31588
|
a[1],
|
|
30923
31589
|
a[2],
|
|
@@ -30935,37 +31601,37 @@ var Niivue = class {
|
|
|
30935
31601
|
a[14],
|
|
30936
31602
|
a[15]
|
|
30937
31603
|
);
|
|
30938
|
-
const half =
|
|
30939
|
-
const Pxyz_c4 =
|
|
30940
|
-
const affineT =
|
|
30941
|
-
|
|
30942
|
-
|
|
30943
|
-
const Pxyz_c =
|
|
30944
|
-
const delta =
|
|
30945
|
-
let Mdc =
|
|
31604
|
+
const half = vec49.fromValues(inDims[1] / 2, inDims[2] / 2, inDims[3] / 2, 1);
|
|
31605
|
+
const Pxyz_c4 = vec49.create();
|
|
31606
|
+
const affineT = mat49.create();
|
|
31607
|
+
mat49.transpose(affineT, affine);
|
|
31608
|
+
vec49.transformMat4(Pxyz_c4, half, affineT);
|
|
31609
|
+
const Pxyz_c = vec314.fromValues(Pxyz_c4[0], Pxyz_c4[1], Pxyz_c4[2]);
|
|
31610
|
+
const delta = vec314.fromValues(outMM, outMM, outMM);
|
|
31611
|
+
let Mdc = mat49.fromValues(-1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
|
|
30946
31612
|
if (toRAS) {
|
|
30947
|
-
Mdc =
|
|
30948
|
-
}
|
|
30949
|
-
|
|
30950
|
-
const dims =
|
|
30951
|
-
const MdcD =
|
|
30952
|
-
|
|
30953
|
-
const vol_center =
|
|
30954
|
-
|
|
30955
|
-
|
|
30956
|
-
const translate =
|
|
30957
|
-
|
|
30958
|
-
const out_affine =
|
|
30959
|
-
|
|
31613
|
+
Mdc = mat49.fromValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
31614
|
+
}
|
|
31615
|
+
mat49.transpose(Mdc, Mdc);
|
|
31616
|
+
const dims = vec49.fromValues(outDim, outDim, outDim, 1);
|
|
31617
|
+
const MdcD = mat49.create();
|
|
31618
|
+
mat49.scale(MdcD, Mdc, delta);
|
|
31619
|
+
const vol_center = vec49.fromValues(dims[0], dims[1], dims[2], 1);
|
|
31620
|
+
vec49.transformMat4(vol_center, vol_center, MdcD);
|
|
31621
|
+
vec49.scale(vol_center, vol_center, 0.5);
|
|
31622
|
+
const translate = vec314.create();
|
|
31623
|
+
vec314.subtract(translate, Pxyz_c, vec314.fromValues(vol_center[0], vol_center[1], vol_center[2]));
|
|
31624
|
+
const out_affine = mat49.create();
|
|
31625
|
+
mat49.transpose(out_affine, MdcD);
|
|
30960
31626
|
out_affine[3] = translate[0];
|
|
30961
31627
|
out_affine[7] = translate[1];
|
|
30962
31628
|
out_affine[11] = translate[2];
|
|
30963
|
-
const inv_out_affine =
|
|
30964
|
-
|
|
30965
|
-
const vox2vox =
|
|
30966
|
-
|
|
30967
|
-
const inv_vox2vox =
|
|
30968
|
-
|
|
31629
|
+
const inv_out_affine = mat49.create();
|
|
31630
|
+
mat49.invert(inv_out_affine, out_affine);
|
|
31631
|
+
const vox2vox = mat49.create();
|
|
31632
|
+
mat49.mul(vox2vox, affine, inv_out_affine);
|
|
31633
|
+
const inv_vox2vox = mat49.create();
|
|
31634
|
+
mat49.invert(inv_vox2vox, vox2vox);
|
|
30969
31635
|
return [out_affine, vox2vox, inv_vox2vox];
|
|
30970
31636
|
}
|
|
30971
31637
|
// Create a binary byte array with a NIfTI format header as well as image data
|
|
@@ -31487,6 +32153,97 @@ var Niivue = class {
|
|
|
31487
32153
|
}
|
|
31488
32154
|
return sum;
|
|
31489
32155
|
}
|
|
32156
|
+
doClickToSegment(options) {
|
|
32157
|
+
const { tileIndex } = options;
|
|
32158
|
+
if (tileIndex < 0 || tileIndex >= this.screenSlices.length) {
|
|
32159
|
+
log.warn(`Invalid tileIndex ${tileIndex} received in doClickToSegment.`);
|
|
32160
|
+
return;
|
|
32161
|
+
}
|
|
32162
|
+
const axCorSag = this.screenSlices[tileIndex].axCorSag;
|
|
32163
|
+
if (axCorSag > 2 /* SAGITTAL */) {
|
|
32164
|
+
log.warn("ClickToSegment attempted on non-2D slice tile.");
|
|
32165
|
+
return;
|
|
32166
|
+
}
|
|
32167
|
+
const texFrac = this.screenXY2TextureFrac(
|
|
32168
|
+
this.clickToSegmentXY[0],
|
|
32169
|
+
// Use the stored click location
|
|
32170
|
+
this.clickToSegmentXY[1],
|
|
32171
|
+
tileIndex,
|
|
32172
|
+
false
|
|
32173
|
+
);
|
|
32174
|
+
if (texFrac[0] < 0) {
|
|
32175
|
+
log.debug("Click location outside valid texture fraction for the tile.");
|
|
32176
|
+
return;
|
|
32177
|
+
}
|
|
32178
|
+
const pt = this.frac2vox(texFrac);
|
|
32179
|
+
const threshold = this.opts.clickToSegmentPercent;
|
|
32180
|
+
let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
|
|
32181
|
+
if (this.opts.clickToSegmentAutoIntensity) {
|
|
32182
|
+
if (threshold !== 0) {
|
|
32183
|
+
if (voxelIntensity === 0) {
|
|
32184
|
+
voxelIntensity = 0.01;
|
|
32185
|
+
}
|
|
32186
|
+
this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold);
|
|
32187
|
+
this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold);
|
|
32188
|
+
}
|
|
32189
|
+
if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
|
|
32190
|
+
this.opts.clickToSegmentBright = true;
|
|
32191
|
+
} else {
|
|
32192
|
+
this.opts.clickToSegmentBright = false;
|
|
32193
|
+
}
|
|
32194
|
+
}
|
|
32195
|
+
const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
32196
|
+
this.drawPenAxCorSag = axCorSag;
|
|
32197
|
+
const targetBitmap = this.clickToSegmentIsGrowing ? this.clickToSegmentGrowingBitmap : this.drawBitmap;
|
|
32198
|
+
if (!targetBitmap) {
|
|
32199
|
+
log.error("Target bitmap for flood fill is null.");
|
|
32200
|
+
if (!this.clickToSegmentIsGrowing) {
|
|
32201
|
+
this.createEmptyDrawing();
|
|
32202
|
+
if (!this.drawBitmap) {
|
|
32203
|
+
return;
|
|
32204
|
+
}
|
|
32205
|
+
} else {
|
|
32206
|
+
if (!this.drawBitmap) {
|
|
32207
|
+
this.createEmptyDrawing();
|
|
32208
|
+
}
|
|
32209
|
+
if (!this.drawBitmap) {
|
|
32210
|
+
return;
|
|
32211
|
+
}
|
|
32212
|
+
this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
|
|
32213
|
+
}
|
|
32214
|
+
log.warn("Initialized missing bitmap in doClickToSegment.");
|
|
32215
|
+
}
|
|
32216
|
+
this.drawFloodFill(
|
|
32217
|
+
[pt[0], pt[1], pt[2]],
|
|
32218
|
+
this.opts.penValue,
|
|
32219
|
+
brightOrDark,
|
|
32220
|
+
this.opts.clickToSegmentIntensityMin,
|
|
32221
|
+
this.opts.clickToSegmentIntensityMax,
|
|
32222
|
+
this.opts.floodFillNeighbors,
|
|
32223
|
+
this.opts.clickToSegmentMaxDistanceMM,
|
|
32224
|
+
this.opts.clickToSegmentIs2D,
|
|
32225
|
+
targetBitmap
|
|
32226
|
+
);
|
|
32227
|
+
if (!this.clickToSegmentIsGrowing) {
|
|
32228
|
+
log.debug("Applying clickToSegment mask to drawBitmap.");
|
|
32229
|
+
if (this.drawBitmap) {
|
|
32230
|
+
this.refreshDrawing(false, false);
|
|
32231
|
+
this.drawScene();
|
|
32232
|
+
} else {
|
|
32233
|
+
log.error("Cannot refresh drawing after click-to-segment apply, drawBitmap is null.");
|
|
32234
|
+
}
|
|
32235
|
+
if (this.drawBitmap) {
|
|
32236
|
+
const info = this.getDescriptives({
|
|
32237
|
+
layer: 0,
|
|
32238
|
+
masks: [],
|
|
32239
|
+
drawingIsMask: true
|
|
32240
|
+
// Use the final this.drawBitmap
|
|
32241
|
+
});
|
|
32242
|
+
this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
|
|
32243
|
+
}
|
|
32244
|
+
}
|
|
32245
|
+
this.createOnLocationChange(axCorSag);
|
|
32246
|
+
}
|
|
31490
32247
|
// not included in public docs
|
|
31491
32248
|
// handle mouse click event on canvas
|
|
31492
32249
|
mouseClick(x, y, posChange = 0, isDelta = true) {
|
|
@@ -31533,138 +32290,94 @@ var Niivue = class {
|
|
|
31533
32290
|
if (this.drawPenAxCorSag >= 0 && this.drawPenAxCorSag !== axCorSag) {
|
|
31534
32291
|
continue;
|
|
31535
32292
|
}
|
|
31536
|
-
if (axCorSag > 2 /* SAGITTAL */) {
|
|
32293
|
+
if (axCorSag > 2 /* SAGITTAL */ && !this.opts.clickToSegment && posChange === 0) {
|
|
31537
32294
|
continue;
|
|
31538
32295
|
}
|
|
31539
|
-
|
|
32296
|
+
const texFrac = this.screenXY2TextureFrac(x, y, i, true);
|
|
31540
32297
|
if (texFrac[0] < 0) {
|
|
31541
32298
|
continue;
|
|
31542
32299
|
}
|
|
31543
|
-
if (!isDelta) {
|
|
31544
|
-
|
|
31545
|
-
|
|
31546
|
-
|
|
31547
|
-
|
|
31548
|
-
|
|
31549
|
-
|
|
31550
|
-
|
|
31551
|
-
posNeg = -1;
|
|
32300
|
+
if (posChange !== 0 || !isDelta) {
|
|
32301
|
+
if (!isDelta) {
|
|
32302
|
+
if (axCorSag <= 2 /* SAGITTAL */) {
|
|
32303
|
+
this.scene.crosshairPos[2 - axCorSag] = posChange;
|
|
32304
|
+
this.drawScene();
|
|
32305
|
+
this.createOnLocationChange(axCorSag);
|
|
32306
|
+
}
|
|
32307
|
+
return;
|
|
31552
32308
|
}
|
|
32309
|
+
const posNeg = posChange < 0 ? -1 : 1;
|
|
31553
32310
|
const xyz = [0, 0, 0];
|
|
31554
|
-
|
|
31555
|
-
|
|
31556
|
-
|
|
31557
|
-
|
|
32311
|
+
if (axCorSag <= 2 /* SAGITTAL */) {
|
|
32312
|
+
xyz[2 - axCorSag] = posNeg;
|
|
32313
|
+
this.moveCrosshairInVox(xyz[0], xyz[1], xyz[2]);
|
|
32314
|
+
}
|
|
31558
32315
|
return;
|
|
31559
32316
|
}
|
|
31560
32317
|
if (this.opts.isForceMouseClickToVoxelCenters) {
|
|
31561
|
-
this.scene.crosshairPos =
|
|
32318
|
+
this.scene.crosshairPos = vec314.clone(this.vox2frac(this.frac2vox(texFrac)));
|
|
31562
32319
|
} else {
|
|
31563
|
-
this.scene.crosshairPos =
|
|
32320
|
+
this.scene.crosshairPos = vec314.clone(texFrac);
|
|
31564
32321
|
}
|
|
31565
32322
|
if (this.opts.drawingEnabled) {
|
|
31566
|
-
|
|
31567
|
-
if (this.opts.
|
|
31568
|
-
|
|
31569
|
-
|
|
31570
|
-
|
|
31571
|
-
|
|
31572
|
-
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
diff = Math.max(Math.abs(xDiff), Math.abs(yDiff));
|
|
31576
|
-
threshold = this.opts.clickToSegmentPercent + diff;
|
|
31577
|
-
}
|
|
31578
|
-
let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
|
|
31579
|
-
if (this.opts.clickToSegmentAutoIntensity) {
|
|
31580
|
-
if (threshold !== 0) {
|
|
31581
|
-
if (voxelIntensity === 0) {
|
|
31582
|
-
voxelIntensity = 0.01;
|
|
31583
|
-
}
|
|
31584
|
-
this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold);
|
|
31585
|
-
this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold);
|
|
31586
|
-
}
|
|
31587
|
-
if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
|
|
31588
|
-
this.opts.clickToSegmentBright = true;
|
|
31589
|
-
} else {
|
|
31590
|
-
this.opts.clickToSegmentBright = false;
|
|
31591
|
-
}
|
|
31592
|
-
}
|
|
31593
|
-
const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
31594
|
-
this.drawPenAxCorSag = axCorSag;
|
|
31595
|
-
if (this.drawPenAxCorSag === 0 /* AXIAL */) {
|
|
31596
|
-
const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]];
|
|
31597
|
-
const minPixDim = Math.min(...pixDims);
|
|
31598
|
-
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
|
|
31599
|
-
} else if (this.drawPenAxCorSag === 1 /* CORONAL */) {
|
|
31600
|
-
const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]];
|
|
31601
|
-
const minPixDim = Math.min(...pixDims);
|
|
31602
|
-
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
|
|
31603
|
-
} else if (this.drawPenAxCorSag === 2 /* SAGITTAL */) {
|
|
31604
|
-
const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]];
|
|
31605
|
-
const minPixDim = Math.min(...pixDims);
|
|
31606
|
-
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
|
|
31607
|
-
}
|
|
31608
|
-
this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue);
|
|
31609
|
-
if (diff !== 0) {
|
|
31610
|
-
this.clickToSegmentIsGrowing = true;
|
|
31611
|
-
this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
|
|
32323
|
+
const pt = this.frac2vox(this.scene.crosshairPos);
|
|
32324
|
+
if (!isFinite(this.opts.penValue) || this.opts.penValue < 0 || Object.is(this.opts.penValue, -0)) {
|
|
32325
|
+
let growMode = 0;
|
|
32326
|
+
let floodFillNewColor = Math.abs(this.opts.penValue);
|
|
32327
|
+
const isGrowTool = true;
|
|
32328
|
+
if (Object.is(this.opts.penValue, -0)) {
|
|
32329
|
+
growMode = 0;
|
|
32330
|
+
floodFillNewColor = 0;
|
|
32331
|
+
log.debug("Erase Cluster selected");
|
|
31612
32332
|
} else {
|
|
31613
|
-
|
|
32333
|
+
growMode = this.opts.penValue;
|
|
32334
|
+
log.debug("Intensity Grow selected", growMode);
|
|
31614
32335
|
}
|
|
31615
32336
|
this.drawFloodFill(
|
|
31616
|
-
|
|
31617
|
-
|
|
31618
|
-
|
|
31619
|
-
|
|
31620
|
-
|
|
32337
|
+
pt,
|
|
32338
|
+
floodFillNewColor,
|
|
32339
|
+
growMode,
|
|
32340
|
+
NaN,
|
|
32341
|
+
NaN,
|
|
31621
32342
|
this.opts.floodFillNeighbors,
|
|
31622
|
-
|
|
31623
|
-
|
|
32343
|
+
Number.POSITIVE_INFINITY,
|
|
32344
|
+
false,
|
|
32345
|
+
this.drawBitmap,
|
|
32346
|
+
isGrowTool
|
|
31624
32347
|
);
|
|
31625
32348
|
this.drawScene();
|
|
31626
32349
|
this.createOnLocationChange(axCorSag);
|
|
31627
|
-
if (this.clickToSegmentIsGrowing) {
|
|
31628
|
-
return;
|
|
31629
|
-
}
|
|
31630
|
-
const info = this.getDescriptives({
|
|
31631
|
-
layer: 0,
|
|
31632
|
-
masks: [],
|
|
31633
|
-
drawingIsMask: true
|
|
31634
|
-
});
|
|
31635
|
-
this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
|
|
31636
32350
|
return;
|
|
31637
|
-
}
|
|
31638
|
-
|
|
31639
|
-
|
|
31640
|
-
this.
|
|
31641
|
-
|
|
31642
|
-
|
|
31643
|
-
|
|
31644
|
-
|
|
31645
|
-
this.opts.penValue,
|
|
31646
|
-
NaN,
|
|
31647
|
-
NaN,
|
|
31648
|
-
this.opts.floodFillNeighbors
|
|
31649
|
-
);
|
|
32351
|
+
} else if (this.opts.clickToSegment) {
|
|
32352
|
+
if (axCorSag <= 2 /* SAGITTAL */) {
|
|
32353
|
+
this.clickToSegmentIsGrowing = false;
|
|
32354
|
+
this.doClickToSegment({
|
|
32355
|
+
x: this.clickToSegmentXY[0],
|
|
32356
|
+
y: this.clickToSegmentXY[1],
|
|
32357
|
+
tileIndex: i
|
|
32358
|
+
});
|
|
31650
32359
|
}
|
|
32360
|
+
this.createOnLocationChange(axCorSag);
|
|
31651
32361
|
return;
|
|
31652
|
-
}
|
|
31653
|
-
if (isNaN(this.drawPenLocation[0])) {
|
|
31654
|
-
this.drawPenAxCorSag = axCorSag;
|
|
31655
|
-
this.drawPenFillPts = [];
|
|
31656
|
-
this.drawPt(...pt, this.opts.penValue);
|
|
31657
32362
|
} else {
|
|
31658
|
-
if (
|
|
31659
|
-
|
|
32363
|
+
if (isNaN(this.drawPenLocation[0])) {
|
|
32364
|
+
this.drawPenAxCorSag = axCorSag;
|
|
32365
|
+
this.drawPenFillPts = [];
|
|
32366
|
+
this.drawPt(...pt, this.opts.penValue);
|
|
32367
|
+
} else {
|
|
32368
|
+
if (pt[0] === this.drawPenLocation[0] && pt[1] === this.drawPenLocation[1] && pt[2] === this.drawPenLocation[2]) {
|
|
32369
|
+
this.drawScene();
|
|
32370
|
+
this.createOnLocationChange(axCorSag);
|
|
32371
|
+
return;
|
|
32372
|
+
}
|
|
32373
|
+
this.drawPenLine(pt, this.drawPenLocation, this.opts.penValue);
|
|
31660
32374
|
}
|
|
31661
|
-
this.
|
|
31662
|
-
|
|
31663
|
-
|
|
31664
|
-
|
|
31665
|
-
this.
|
|
32375
|
+
this.drawPenLocation = pt;
|
|
32376
|
+
if (this.opts.isFilledPen) {
|
|
32377
|
+
this.drawPenFillPts.push(pt);
|
|
32378
|
+
}
|
|
32379
|
+
this.refreshDrawing(false, false);
|
|
31666
32380
|
}
|
|
31667
|
-
this.refreshDrawing(false);
|
|
31668
32381
|
}
|
|
31669
32382
|
this.drawScene();
|
|
31670
32383
|
this.createOnLocationChange(axCorSag);
|
|
@@ -31755,9 +32468,9 @@ var Niivue = class {
|
|
|
31755
32468
|
continue;
|
|
31756
32469
|
}
|
|
31757
32470
|
const mm = this.frac2mm(texFrac);
|
|
31758
|
-
return
|
|
32471
|
+
return vec49.fromValues(mm[0], mm[1], mm[2], i);
|
|
31759
32472
|
}
|
|
31760
|
-
return
|
|
32473
|
+
return vec49.fromValues(NaN, NaN, NaN, NaN);
|
|
31761
32474
|
}
|
|
31762
32475
|
// not included in public docs
|
|
31763
32476
|
dragForPanZoom(startXYendXY) {
|
|
@@ -31769,9 +32482,9 @@ var Niivue = class {
|
|
|
31769
32482
|
if (isNaN(startMM[0]) || isNaN(endMM[0]) || isNaN(endMM[3])) {
|
|
31770
32483
|
return;
|
|
31771
32484
|
}
|
|
31772
|
-
const v =
|
|
32485
|
+
const v = vec49.create();
|
|
31773
32486
|
const zoom = this.uiData.pan2DxyzmmAtMouseDown[3];
|
|
31774
|
-
|
|
32487
|
+
vec49.sub(v, endMM, startMM);
|
|
31775
32488
|
this.scene.pan2Dxyzmm[0] = this.uiData.pan2DxyzmmAtMouseDown[0] + zoom * v[0];
|
|
31776
32489
|
this.scene.pan2Dxyzmm[1] = this.uiData.pan2DxyzmmAtMouseDown[1] + zoom * v[1];
|
|
31777
32490
|
this.scene.pan2Dxyzmm[2] = this.uiData.pan2DxyzmmAtMouseDown[2] + zoom * v[2];
|
|
@@ -31848,12 +32561,12 @@ var Niivue = class {
|
|
|
31848
32561
|
let endXY = this.canvasPos2frac([startXYendXY[2], startXYendXY[3]]);
|
|
31849
32562
|
if (startXY[0] >= 0 && endXY[0] >= 0) {
|
|
31850
32563
|
const startMm = this.frac2mm(startXY);
|
|
31851
|
-
startXY =
|
|
32564
|
+
startXY = vec314.fromValues(startMm[0], startMm[1], startMm[2]);
|
|
31852
32565
|
const endMm = this.frac2mm(endXY);
|
|
31853
|
-
endXY =
|
|
31854
|
-
const v =
|
|
31855
|
-
|
|
31856
|
-
const lenMM =
|
|
32566
|
+
endXY = vec314.fromValues(endMm[0], endMm[1], endMm[2]);
|
|
32567
|
+
const v = vec314.create();
|
|
32568
|
+
vec314.sub(v, startXY, endXY);
|
|
32569
|
+
const lenMM = vec314.len(v);
|
|
31857
32570
|
let decimals = 2;
|
|
31858
32571
|
if (lenMM > 9) {
|
|
31859
32572
|
decimals = 1;
|
|
@@ -32393,7 +33106,7 @@ var Niivue = class {
|
|
|
32393
33106
|
right = -mn[0];
|
|
32394
33107
|
}
|
|
32395
33108
|
const scale = 2 * Math.max(Math.abs(mn[2]), Math.abs(mx[2]));
|
|
32396
|
-
const projectionMatrix =
|
|
33109
|
+
const projectionMatrix = mat49.create();
|
|
32397
33110
|
let near = 0.01;
|
|
32398
33111
|
let far = scale * 8;
|
|
32399
33112
|
if (clipTolerance !== Infinity) {
|
|
@@ -32408,19 +33121,19 @@ var Niivue = class {
|
|
|
32408
33121
|
near = dx - clipTolerance;
|
|
32409
33122
|
far = dx + clipTolerance;
|
|
32410
33123
|
}
|
|
32411
|
-
|
|
32412
|
-
const modelMatrix =
|
|
33124
|
+
mat49.ortho(projectionMatrix, left, right, mn[1], mx[1], near, far);
|
|
33125
|
+
const modelMatrix = mat49.create();
|
|
32413
33126
|
modelMatrix[0] = -1;
|
|
32414
|
-
const translateVec3 =
|
|
32415
|
-
|
|
32416
|
-
|
|
32417
|
-
|
|
32418
|
-
const iModelMatrix =
|
|
32419
|
-
|
|
32420
|
-
const normalMatrix =
|
|
32421
|
-
|
|
32422
|
-
const modelViewProjectionMatrix =
|
|
32423
|
-
|
|
33127
|
+
const translateVec3 = vec314.fromValues(0, 0, -scale * 1.8);
|
|
33128
|
+
mat49.translate(modelMatrix, modelMatrix, translateVec3);
|
|
33129
|
+
mat49.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
|
|
33130
|
+
mat49.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
|
|
33131
|
+
const iModelMatrix = mat49.create();
|
|
33132
|
+
mat49.invert(iModelMatrix, modelMatrix);
|
|
33133
|
+
const normalMatrix = mat49.create();
|
|
33134
|
+
mat49.transpose(normalMatrix, iModelMatrix);
|
|
33135
|
+
const modelViewProjectionMatrix = mat49.create();
|
|
33136
|
+
mat49.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
|
|
32424
33137
|
return {
|
|
32425
33138
|
modelViewProjectionMatrix,
|
|
32426
33139
|
modelMatrix,
|
|
@@ -32440,7 +33153,7 @@ var Niivue = class {
|
|
|
32440
33153
|
}
|
|
32441
33154
|
// not included in public docs
|
|
32442
33155
|
screenFieldOfViewVox(axCorSag = 0) {
|
|
32443
|
-
const fov =
|
|
33156
|
+
const fov = vec314.clone(this.volumeObject3D.fieldOfViewDeObliqueMM);
|
|
32444
33157
|
return this.swizzleVec3MM(fov, axCorSag);
|
|
32445
33158
|
}
|
|
32446
33159
|
// not included in public docs
|
|
@@ -32451,25 +33164,25 @@ var Niivue = class {
|
|
|
32451
33164
|
}
|
|
32452
33165
|
const extentsMin = this.volumeObject3D.extentsMin;
|
|
32453
33166
|
const extentsMax = this.volumeObject3D.extentsMax;
|
|
32454
|
-
let mnMM =
|
|
32455
|
-
let mxMM =
|
|
33167
|
+
let mnMM = vec314.fromValues(extentsMin[0], extentsMin[1], extentsMin[2]);
|
|
33168
|
+
let mxMM = vec314.fromValues(extentsMax[0], extentsMax[1], extentsMax[2]);
|
|
32456
33169
|
mnMM = this.swizzleVec3MM(mnMM, axCorSag);
|
|
32457
33170
|
mxMM = this.swizzleVec3MM(mxMM, axCorSag);
|
|
32458
|
-
const fovMM =
|
|
32459
|
-
|
|
33171
|
+
const fovMM = vec314.create();
|
|
33172
|
+
vec314.subtract(fovMM, mxMM, mnMM);
|
|
32460
33173
|
return fovMM;
|
|
32461
33174
|
}
|
|
32462
33175
|
// not included in public docs
|
|
32463
33176
|
screenFieldOfViewExtendedVox(axCorSag = 0) {
|
|
32464
33177
|
const extentsMinOrtho = this.volumes[0].extentsMinOrtho;
|
|
32465
33178
|
const extentsMaxOrtho = this.volumes[0].extentsMaxOrtho;
|
|
32466
|
-
let mnMM =
|
|
32467
|
-
let mxMM =
|
|
32468
|
-
const rotation =
|
|
33179
|
+
let mnMM = vec314.fromValues(extentsMinOrtho[0], extentsMinOrtho[1], extentsMinOrtho[2]);
|
|
33180
|
+
let mxMM = vec314.fromValues(extentsMaxOrtho[0], extentsMaxOrtho[1], extentsMaxOrtho[2]);
|
|
33181
|
+
const rotation = mat49.create();
|
|
32469
33182
|
mnMM = this.swizzleVec3MM(mnMM, axCorSag);
|
|
32470
33183
|
mxMM = this.swizzleVec3MM(mxMM, axCorSag);
|
|
32471
|
-
const fovMM =
|
|
32472
|
-
|
|
33184
|
+
const fovMM = vec314.create();
|
|
33185
|
+
vec314.subtract(fovMM, mxMM, mnMM);
|
|
32473
33186
|
return { mnMM, mxMM, rotation, fovMM };
|
|
32474
33187
|
}
|
|
32475
33188
|
// not included in public docs
|
|
@@ -32479,13 +33192,13 @@ var Niivue = class {
|
|
|
32479
33192
|
}
|
|
32480
33193
|
const eMin = this.volumeObject3D.extentsMin;
|
|
32481
33194
|
const eMax = this.volumeObject3D.extentsMax;
|
|
32482
|
-
let mnMM =
|
|
32483
|
-
let mxMM =
|
|
32484
|
-
const rotation =
|
|
33195
|
+
let mnMM = vec314.fromValues(eMin[0], eMin[1], eMin[2]);
|
|
33196
|
+
let mxMM = vec314.fromValues(eMax[0], eMax[1], eMax[2]);
|
|
33197
|
+
const rotation = mat49.create();
|
|
32485
33198
|
mnMM = this.swizzleVec3MM(mnMM, axCorSag);
|
|
32486
33199
|
mxMM = this.swizzleVec3MM(mxMM, axCorSag);
|
|
32487
|
-
const fovMM =
|
|
32488
|
-
|
|
33200
|
+
const fovMM = vec314.create();
|
|
33201
|
+
vec314.subtract(fovMM, mxMM, mnMM);
|
|
32489
33202
|
return { mnMM, mxMM, rotation, fovMM };
|
|
32490
33203
|
}
|
|
32491
33204
|
// not included in public docs
|
|
@@ -32549,9 +33262,9 @@ var Niivue = class {
|
|
|
32549
33262
|
a = this.frac2mm(a);
|
|
32550
33263
|
b = this.frac2mm(b);
|
|
32551
33264
|
c = this.frac2mm(c);
|
|
32552
|
-
a = this.swizzleVec3MM(
|
|
32553
|
-
b = this.swizzleVec3MM(
|
|
32554
|
-
c = this.swizzleVec3MM(
|
|
33265
|
+
a = this.swizzleVec3MM(vec314.fromValues(a[0], a[1], a[2]), axCorSag);
|
|
33266
|
+
b = this.swizzleVec3MM(vec314.fromValues(b[0], b[1], b[2]), axCorSag);
|
|
33267
|
+
c = this.swizzleVec3MM(vec314.fromValues(c[0], c[1], c[2]), axCorSag);
|
|
32555
33268
|
const denom = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
|
32556
33269
|
let yMult = (b[0] - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (b[2] - a[2]);
|
|
32557
33270
|
yMult /= denom;
|
|
@@ -32570,10 +33283,10 @@ var Niivue = class {
|
|
|
32570
33283
|
draw2DMain(leftTopWidthHeight, axCorSag, customMM = NaN) {
|
|
32571
33284
|
let frac2mmTexture = this.volumes[0].frac2mm.slice();
|
|
32572
33285
|
let screen = this.screenFieldOfViewExtendedMM(axCorSag);
|
|
32573
|
-
let mesh2ortho =
|
|
33286
|
+
let mesh2ortho = mat49.create();
|
|
32574
33287
|
if (!this.opts.isSliceMM) {
|
|
32575
33288
|
frac2mmTexture = this.volumes[0].frac2mmOrtho.slice();
|
|
32576
|
-
mesh2ortho =
|
|
33289
|
+
mesh2ortho = mat49.clone(this.volumes[0].mm2ortho);
|
|
32577
33290
|
screen = this.screenFieldOfViewExtendedVox(axCorSag);
|
|
32578
33291
|
}
|
|
32579
33292
|
let isRadiolgical = this.opts.isRadiologicalConvention && axCorSag < 2 /* SAGITTAL */;
|
|
@@ -32616,7 +33329,7 @@ var Niivue = class {
|
|
|
32616
33329
|
}
|
|
32617
33330
|
if (isNaN(customMM)) {
|
|
32618
33331
|
const pan = this.scene.pan2Dxyzmm;
|
|
32619
|
-
const panXY = this.swizzleVec3MM(
|
|
33332
|
+
const panXY = this.swizzleVec3MM(vec314.fromValues(pan[0], pan[1], pan[2]), axCorSag);
|
|
32620
33333
|
const zoom = this.scene.pan2Dxyzmm[3];
|
|
32621
33334
|
screen.mnMM[0] -= panXY[0];
|
|
32622
33335
|
screen.mxMM[0] -= panXY[0];
|
|
@@ -32731,8 +33444,8 @@ var Niivue = class {
|
|
|
32731
33444
|
isRadiolgical
|
|
32732
33445
|
);
|
|
32733
33446
|
}
|
|
32734
|
-
const mx =
|
|
32735
|
-
|
|
33447
|
+
const mx = mat49.clone(obj.modelViewProjectionMatrix);
|
|
33448
|
+
mat49.multiply(mx, mx, mesh2ortho);
|
|
32736
33449
|
this.drawMesh3D(
|
|
32737
33450
|
true,
|
|
32738
33451
|
1,
|
|
@@ -32792,29 +33505,29 @@ var Niivue = class {
|
|
|
32792
33505
|
const whratio = leftTopWidthHeight[2] / leftTopWidthHeight[3];
|
|
32793
33506
|
let scale = this.furthestFromPivot;
|
|
32794
33507
|
const origin = this.pivot3D;
|
|
32795
|
-
const projectionMatrix =
|
|
33508
|
+
const projectionMatrix = mat49.create();
|
|
32796
33509
|
scale = 0.8 * scale / this.scene.volScaleMultiplier;
|
|
32797
33510
|
if (whratio < 1) {
|
|
32798
|
-
|
|
33511
|
+
mat49.ortho(projectionMatrix, -scale, scale, -scale / whratio, scale / whratio, scale * 0.01, scale * 8);
|
|
32799
33512
|
} else {
|
|
32800
|
-
|
|
33513
|
+
mat49.ortho(projectionMatrix, -scale * whratio, scale * whratio, -scale, scale, scale * 0.01, scale * 8);
|
|
32801
33514
|
}
|
|
32802
|
-
const modelMatrix =
|
|
33515
|
+
const modelMatrix = mat49.create();
|
|
32803
33516
|
modelMatrix[0] = -1;
|
|
32804
|
-
const translateVec3 =
|
|
32805
|
-
|
|
33517
|
+
const translateVec3 = vec314.fromValues(0, 0, -scale * 1.8);
|
|
33518
|
+
mat49.translate(modelMatrix, modelMatrix, translateVec3);
|
|
32806
33519
|
if (this.position) {
|
|
32807
|
-
|
|
32808
|
-
}
|
|
32809
|
-
|
|
32810
|
-
|
|
32811
|
-
|
|
32812
|
-
const iModelMatrix =
|
|
32813
|
-
|
|
32814
|
-
const normalMatrix =
|
|
32815
|
-
|
|
32816
|
-
const modelViewProjectionMatrix =
|
|
32817
|
-
|
|
33520
|
+
mat49.translate(modelMatrix, modelMatrix, this.position);
|
|
33521
|
+
}
|
|
33522
|
+
mat49.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
|
|
33523
|
+
mat49.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
|
|
33524
|
+
mat49.translate(modelMatrix, modelMatrix, [-origin[0], -origin[1], -origin[2]]);
|
|
33525
|
+
const iModelMatrix = mat49.create();
|
|
33526
|
+
mat49.invert(iModelMatrix, modelMatrix);
|
|
33527
|
+
const normalMatrix = mat49.create();
|
|
33528
|
+
mat49.transpose(normalMatrix, iModelMatrix);
|
|
33529
|
+
const modelViewProjectionMatrix = mat49.create();
|
|
33530
|
+
mat49.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
|
|
32818
33531
|
return [modelViewProjectionMatrix, modelMatrix, normalMatrix];
|
|
32819
33532
|
}
|
|
32820
33533
|
// not included in public docs
|
|
@@ -32822,13 +33535,13 @@ var Niivue = class {
|
|
|
32822
33535
|
if (!this.back) {
|
|
32823
33536
|
throw new Error("back undefined");
|
|
32824
33537
|
}
|
|
32825
|
-
const modelMatrix =
|
|
33538
|
+
const modelMatrix = mat49.create();
|
|
32826
33539
|
modelMatrix[0] = -1;
|
|
32827
|
-
|
|
32828
|
-
|
|
33540
|
+
mat49.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
|
|
33541
|
+
mat49.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
|
|
32829
33542
|
if (this.back.obliqueRAS) {
|
|
32830
|
-
const oblique =
|
|
32831
|
-
|
|
33543
|
+
const oblique = mat49.clone(this.back.obliqueRAS);
|
|
33544
|
+
mat49.multiply(modelMatrix, modelMatrix, oblique);
|
|
32832
33545
|
}
|
|
32833
33546
|
return modelMatrix;
|
|
32834
33547
|
}
|
|
@@ -32836,15 +33549,15 @@ var Niivue = class {
|
|
|
32836
33549
|
// calculate the near-far direction from the camera's perspective
|
|
32837
33550
|
calculateRayDirection(azimuth, elevation) {
|
|
32838
33551
|
const modelMatrix = this.calculateModelMatrix(azimuth, elevation);
|
|
32839
|
-
const projectionMatrix =
|
|
32840
|
-
const mvpMatrix =
|
|
32841
|
-
|
|
32842
|
-
const inv =
|
|
32843
|
-
|
|
32844
|
-
const rayDir4 =
|
|
32845
|
-
|
|
32846
|
-
const rayDir =
|
|
32847
|
-
|
|
33552
|
+
const projectionMatrix = mat49.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1);
|
|
33553
|
+
const mvpMatrix = mat49.create();
|
|
33554
|
+
mat49.multiply(mvpMatrix, projectionMatrix, modelMatrix);
|
|
33555
|
+
const inv = mat49.create();
|
|
33556
|
+
mat49.invert(inv, mvpMatrix);
|
|
33557
|
+
const rayDir4 = vec49.fromValues(0, 0, -1, 1);
|
|
33558
|
+
vec49.transformMat4(rayDir4, rayDir4, inv);
|
|
33559
|
+
const rayDir = vec314.fromValues(rayDir4[0], rayDir4[1], rayDir4[2]);
|
|
33560
|
+
vec314.normalize(rayDir, rayDir);
|
|
32848
33561
|
const tiny = 5e-5;
|
|
32849
33562
|
if (Math.abs(rayDir[0]) < tiny) {
|
|
32850
33563
|
rayDir[0] = tiny;
|
|
@@ -32859,29 +33572,29 @@ var Niivue = class {
|
|
|
32859
33572
|
}
|
|
32860
33573
|
// not included in public docs
|
|
32861
33574
|
sceneExtentsMinMax(isSliceMM = true) {
|
|
32862
|
-
let mn =
|
|
32863
|
-
let mx =
|
|
33575
|
+
let mn = vec314.fromValues(0, 0, 0);
|
|
33576
|
+
let mx = vec314.fromValues(0, 0, 0);
|
|
32864
33577
|
if (this.volumes.length > 0) {
|
|
32865
33578
|
if (!this.volumeObject3D) {
|
|
32866
33579
|
throw new Error("volumeObject3D undefined");
|
|
32867
33580
|
}
|
|
32868
|
-
mn =
|
|
33581
|
+
mn = vec314.fromValues(
|
|
32869
33582
|
this.volumeObject3D.extentsMin[0],
|
|
32870
33583
|
this.volumeObject3D.extentsMin[1],
|
|
32871
33584
|
this.volumeObject3D.extentsMin[2]
|
|
32872
33585
|
);
|
|
32873
|
-
mx =
|
|
33586
|
+
mx = vec314.fromValues(
|
|
32874
33587
|
this.volumeObject3D.extentsMax[0],
|
|
32875
33588
|
this.volumeObject3D.extentsMax[1],
|
|
32876
33589
|
this.volumeObject3D.extentsMax[2]
|
|
32877
33590
|
);
|
|
32878
33591
|
if (!isSliceMM) {
|
|
32879
|
-
mn =
|
|
33592
|
+
mn = vec314.fromValues(
|
|
32880
33593
|
this.volumes[0].extentsMinOrtho[0],
|
|
32881
33594
|
this.volumes[0].extentsMinOrtho[1],
|
|
32882
33595
|
this.volumes[0].extentsMinOrtho[2]
|
|
32883
33596
|
);
|
|
32884
|
-
mx =
|
|
33597
|
+
mx = vec314.fromValues(
|
|
32885
33598
|
this.volumes[0].extentsMaxOrtho[0],
|
|
32886
33599
|
this.volumes[0].extentsMaxOrtho[1],
|
|
32887
33600
|
this.volumes[0].extentsMaxOrtho[2]
|
|
@@ -32892,33 +33605,33 @@ var Niivue = class {
|
|
|
32892
33605
|
if (this.volumes.length < 1) {
|
|
32893
33606
|
const minExtents = this.meshes[0].extentsMin;
|
|
32894
33607
|
const maxExtents = this.meshes[0].extentsMax;
|
|
32895
|
-
mn =
|
|
32896
|
-
mx =
|
|
33608
|
+
mn = vec314.fromValues(minExtents[0], minExtents[1], minExtents[2]);
|
|
33609
|
+
mx = vec314.fromValues(maxExtents[0], maxExtents[1], maxExtents[2]);
|
|
32897
33610
|
}
|
|
32898
33611
|
for (let i = 0; i < this.meshes.length; i++) {
|
|
32899
33612
|
const minExtents = this.meshes[i].extentsMin;
|
|
32900
33613
|
const maxExtents = this.meshes[i].extentsMax;
|
|
32901
|
-
const vmn =
|
|
32902
|
-
|
|
32903
|
-
const vmx =
|
|
32904
|
-
|
|
33614
|
+
const vmn = vec314.fromValues(minExtents[0], minExtents[1], minExtents[2]);
|
|
33615
|
+
vec314.min(mn, mn, vmn);
|
|
33616
|
+
const vmx = vec314.fromValues(maxExtents[0], maxExtents[1], maxExtents[2]);
|
|
33617
|
+
vec314.max(mx, mx, vmx);
|
|
32905
33618
|
}
|
|
32906
33619
|
}
|
|
32907
|
-
const range =
|
|
32908
|
-
|
|
33620
|
+
const range = vec314.create();
|
|
33621
|
+
vec314.subtract(range, mx, mn);
|
|
32909
33622
|
return [mn, mx, range];
|
|
32910
33623
|
}
|
|
32911
33624
|
// not included in public docs
|
|
32912
33625
|
setPivot3D() {
|
|
32913
33626
|
const [mn, mx] = this.sceneExtentsMinMax();
|
|
32914
|
-
const pivot =
|
|
32915
|
-
|
|
32916
|
-
|
|
33627
|
+
const pivot = vec314.create();
|
|
33628
|
+
vec314.add(pivot, mn, mx);
|
|
33629
|
+
vec314.scale(pivot, pivot, 0.5);
|
|
32917
33630
|
this.pivot3D = [pivot[0], pivot[1], pivot[2]];
|
|
32918
|
-
|
|
33631
|
+
vec314.subtract(pivot, mx, mn);
|
|
32919
33632
|
this.extentsMin = mn;
|
|
32920
33633
|
this.extentsMax = mx;
|
|
32921
|
-
this.furthestFromPivot =
|
|
33634
|
+
this.furthestFromPivot = vec314.length(pivot) * 0.5;
|
|
32922
33635
|
}
|
|
32923
33636
|
// not included in public docs
|
|
32924
33637
|
getMaxVols() {
|
|
@@ -33253,10 +33966,10 @@ var Niivue = class {
|
|
|
33253
33966
|
gl.activeTexture(TEXTURE6_GRADIENT);
|
|
33254
33967
|
gl.bindTexture(gl.TEXTURE_3D, this.gradientTexture);
|
|
33255
33968
|
const modelMatrix = this.calculateModelMatrix(azimuth, elevation);
|
|
33256
|
-
const iModelMatrix =
|
|
33257
|
-
|
|
33258
|
-
const normalMatrix =
|
|
33259
|
-
|
|
33969
|
+
const iModelMatrix = mat49.create();
|
|
33970
|
+
mat49.invert(iModelMatrix, modelMatrix);
|
|
33971
|
+
const normalMatrix = mat49.create();
|
|
33972
|
+
mat49.transpose(normalMatrix, iModelMatrix);
|
|
33260
33973
|
gl.uniformMatrix4fv(shader.uniforms.normMtx, false, normalMatrix);
|
|
33261
33974
|
}
|
|
33262
33975
|
if (this.drawBitmap && this.drawBitmap.length > 8) {
|
|
@@ -33298,23 +34011,23 @@ var Niivue = class {
|
|
|
33298
34011
|
gl.cullFace(gl.BACK);
|
|
33299
34012
|
this.orientCubeShader.use(this.gl);
|
|
33300
34013
|
gl.bindVertexArray(this.orientCubeShaderVAO);
|
|
33301
|
-
const modelMatrix =
|
|
33302
|
-
const projectionMatrix =
|
|
33303
|
-
|
|
34014
|
+
const modelMatrix = mat49.create();
|
|
34015
|
+
const projectionMatrix = mat49.create();
|
|
34016
|
+
mat49.ortho(projectionMatrix, 0, gl.canvas.width, 0, gl.canvas.height, -10 * sz, 10 * sz);
|
|
33304
34017
|
let translateUpForColorbar = 0;
|
|
33305
34018
|
if (leftTopWidthHeight[1] === 0) {
|
|
33306
34019
|
translateUpForColorbar = gl.canvas.height - this.effectiveCanvasHeight();
|
|
33307
34020
|
}
|
|
33308
|
-
|
|
34021
|
+
mat49.translate(modelMatrix, modelMatrix, [
|
|
33309
34022
|
1.8 * sz + leftTopWidthHeight[0],
|
|
33310
34023
|
translateUpForColorbar + 1.8 * sz + leftTopWidthHeight[1],
|
|
33311
34024
|
0
|
|
33312
34025
|
]);
|
|
33313
|
-
|
|
33314
|
-
|
|
33315
|
-
|
|
33316
|
-
const modelViewProjectionMatrix =
|
|
33317
|
-
|
|
34026
|
+
mat49.scale(modelMatrix, modelMatrix, [sz, sz, sz]);
|
|
34027
|
+
mat49.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
|
|
34028
|
+
mat49.rotateZ(modelMatrix, modelMatrix, deg2rad(-azimuth));
|
|
34029
|
+
const modelViewProjectionMatrix = mat49.create();
|
|
34030
|
+
mat49.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
|
|
33318
34031
|
gl.uniformMatrix4fv(this.orientCubeShader.uniforms.u_matrix, false, modelViewProjectionMatrix);
|
|
33319
34032
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 168);
|
|
33320
34033
|
gl.bindVertexArray(this.unusedVAO);
|
|
@@ -33417,8 +34130,8 @@ var Niivue = class {
|
|
|
33417
34130
|
}
|
|
33418
34131
|
// not included in public docs
|
|
33419
34132
|
calculateScreenPoint(point, mvpMatrix, leftTopWidthHeight) {
|
|
33420
|
-
const screenPoint =
|
|
33421
|
-
|
|
34133
|
+
const screenPoint = vec49.create();
|
|
34134
|
+
vec49.transformMat4(screenPoint, [...point, 1], mvpMatrix);
|
|
33422
34135
|
if (screenPoint[3] !== 0) {
|
|
33423
34136
|
screenPoint[0] = (screenPoint[0] / screenPoint[3] + 1) * 0.5 * leftTopWidthHeight[2];
|
|
33424
34137
|
screenPoint[1] = (1 - screenPoint[1] / screenPoint[3]) * 0.5 * leftTopWidthHeight[3];
|
|
@@ -33876,7 +34589,7 @@ var Niivue = class {
|
|
|
33876
34589
|
// not included in public docs
|
|
33877
34590
|
mm2frac(mm, volIdx = 0, isForceSliceMM = false) {
|
|
33878
34591
|
if (this.volumes.length < 1) {
|
|
33879
|
-
const frac =
|
|
34592
|
+
const frac = vec314.fromValues(0.1, 0.5, 0.5);
|
|
33880
34593
|
const [mn, _mx, range] = this.sceneExtentsMinMax();
|
|
33881
34594
|
frac[0] = (mm[0] - mn[0]) / range[0];
|
|
33882
34595
|
frac[1] = (mm[1] - mn[1]) / range[1];
|
|
@@ -33932,7 +34645,7 @@ var Niivue = class {
|
|
|
33932
34645
|
}
|
|
33933
34646
|
// not included in public docs
|
|
33934
34647
|
frac2mm(frac, volIdx = 0, isForceSliceMM = false) {
|
|
33935
|
-
const pos =
|
|
34648
|
+
const pos = vec49.fromValues(frac[0], frac[1], frac[2], 1);
|
|
33936
34649
|
if (this.volumes.length > 0) {
|
|
33937
34650
|
return this.volumes[volIdx].convertFrac2MM(frac, isForceSliceMM || this.opts.isSliceMM);
|
|
33938
34651
|
} else {
|
|
@@ -33946,7 +34659,7 @@ var Niivue = class {
|
|
|
33946
34659
|
}
|
|
33947
34660
|
// not included in public docs
|
|
33948
34661
|
screenXY2TextureFrac(x, y, i, restrict0to1 = true) {
|
|
33949
|
-
const texFrac =
|
|
34662
|
+
const texFrac = vec314.fromValues(-1, -1, -1);
|
|
33950
34663
|
const axCorSag = this.screenSlices[i].axCorSag;
|
|
33951
34664
|
if (axCorSag > 2 /* SAGITTAL */) {
|
|
33952
34665
|
return texFrac;
|
|
@@ -33969,7 +34682,7 @@ var Niivue = class {
|
|
|
33969
34682
|
if (this.screenSlices[i].AxyzMxy.length < 4) {
|
|
33970
34683
|
return texFrac;
|
|
33971
34684
|
}
|
|
33972
|
-
let xyzMM =
|
|
34685
|
+
let xyzMM = vec314.fromValues(0, 0, 0);
|
|
33973
34686
|
xyzMM[0] = this.screenSlices[i].leftTopMM[0] + fracX * this.screenSlices[i].fovMM[0];
|
|
33974
34687
|
xyzMM[1] = this.screenSlices[i].leftTopMM[1] + fracY * this.screenSlices[i].fovMM[1];
|
|
33975
34688
|
const v = this.screenSlices[i].AxyzMxy;
|
|
@@ -34026,7 +34739,9 @@ var Niivue = class {
|
|
|
34026
34739
|
h = this.gl.canvas.width / this.bmpTextureWH;
|
|
34027
34740
|
w = this.gl.canvas.width;
|
|
34028
34741
|
}
|
|
34029
|
-
this.gl.
|
|
34742
|
+
const left = (this.gl.canvas.width - w) / 2;
|
|
34743
|
+
const top = (this.gl.canvas.height - h) / 2;
|
|
34744
|
+
this.gl.uniform4f(this.bmpShader.uniforms.leftTopWidthHeight, left, top, w, h);
|
|
34030
34745
|
this.gl.bindVertexArray(this.genericVAO);
|
|
34031
34746
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
34032
34747
|
this.gl.bindVertexArray(this.unusedVAO);
|
|
@@ -34722,23 +35437,6 @@ var Niivue = class {
|
|
|
34722
35437
|
]);
|
|
34723
35438
|
return;
|
|
34724
35439
|
}
|
|
34725
|
-
if (this.opts.clickToSegment) {
|
|
34726
|
-
const x = this.mousePos[0];
|
|
34727
|
-
const y = this.mousePos[1];
|
|
34728
|
-
if (this.inRenderTile(x, y) >= 0) {
|
|
34729
|
-
return;
|
|
34730
|
-
}
|
|
34731
|
-
const tileIdx = this.tileIndex(x, y);
|
|
34732
|
-
if (tileIdx > -1) {
|
|
34733
|
-
const fovMM = this.screenSlices[tileIdx].fovMM;
|
|
34734
|
-
const ltwh = this.screenSlices[tileIdx].leftTopWidthHeight;
|
|
34735
|
-
const pixPerMM = ltwh[2] / fovMM[0];
|
|
34736
|
-
const color = this.opts.crosshairColor;
|
|
34737
|
-
const segmentCursorColor = [color[0], color[1], color[2], 0.4];
|
|
34738
|
-
const radius = this.opts.clickToSegmentRadius * pixPerMM;
|
|
34739
|
-
this.drawCircle([x - radius, y - radius, radius * 2, radius * 2], segmentCursorColor, 1);
|
|
34740
|
-
}
|
|
34741
|
-
}
|
|
34742
35440
|
const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]]);
|
|
34743
35441
|
posString = pos[0].toFixed(2) + "\xD7" + pos[1].toFixed(2) + "\xD7" + pos[2].toFixed(2);
|
|
34744
35442
|
this.readyForSync = true;
|