@niivue/niivue 0.44.2-dev.1 → 0.45.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 +15 -71
- package/build/index.js +197 -459
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/dist/index.d.ts +15 -71
- package/dist/index.js +198 -460
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/niivue.umd.js +27 -66
- package/package.json +1 -1
- package/src/cow.iwm.cbor +0 -0
- package/src/fslmean.iwi.cbor +0 -0
- package/src/index.html +1 -1
- package/src/niivue/index.ts +266 -532
- package/src/nvdocument.ts +5 -25
- package/src/shader-srcs.ts +0 -41
- package/src/types.ts +0 -13
- package/src/index_segment.html +0 -165
- package/src/index_sync.html +0 -289
package/src/niivue/index.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
vertLineShader,
|
|
13
13
|
vertLine3DShader,
|
|
14
14
|
fragRectShader,
|
|
15
|
-
fragRectOutlineShader,
|
|
16
15
|
vertRenderShader,
|
|
17
16
|
fragRenderShader,
|
|
18
17
|
fragRenderGradientShader,
|
|
@@ -98,6 +97,7 @@ import {
|
|
|
98
97
|
ImageFromUrlOptions
|
|
99
98
|
} from '../nvimage/index.js'
|
|
100
99
|
import { NVUtilities } from '../nvutilities.js'
|
|
100
|
+
import { NVMeshUtilities } from '../nvmesh-utilities.js'
|
|
101
101
|
import {
|
|
102
102
|
Connectome,
|
|
103
103
|
LegacyConnectome,
|
|
@@ -105,8 +105,7 @@ import {
|
|
|
105
105
|
NiftiHeader,
|
|
106
106
|
DragReleaseParams,
|
|
107
107
|
NiiVueLocation,
|
|
108
|
-
NiiVueLocationValue
|
|
109
|
-
SyncOpts
|
|
108
|
+
NiiVueLocationValue
|
|
110
109
|
} from '../types.js'
|
|
111
110
|
import {
|
|
112
111
|
clamp,
|
|
@@ -196,7 +195,6 @@ type Descriptive = {
|
|
|
196
195
|
cal_max: number
|
|
197
196
|
robust_min: number
|
|
198
197
|
robust_max: number
|
|
199
|
-
area: number | null
|
|
200
198
|
}
|
|
201
199
|
|
|
202
200
|
type SliceScale = {
|
|
@@ -332,6 +330,7 @@ const defaultSaveImageOptions: SaveImageOptions = {
|
|
|
332
330
|
* let niivue = new Niivue({crosshairColor: [0,1,0,0.5], textHeight: 0.5}) // a see-through green crosshair, and larger text labels
|
|
333
331
|
*/
|
|
334
332
|
export class Niivue {
|
|
333
|
+
loaders = {}
|
|
335
334
|
canvas: HTMLCanvasElement | null = null // the reference to the canvas element on the page
|
|
336
335
|
_gl: WebGL2RenderingContext | null = null // the gl context
|
|
337
336
|
isBusy = false // flag to indicate if the scene is busy drawing
|
|
@@ -358,7 +357,6 @@ export class Niivue {
|
|
|
358
357
|
orientCubeShader?: Shader
|
|
359
358
|
orientCubeShaderVAO: WebGLVertexArrayObject | null = null
|
|
360
359
|
rectShader?: Shader
|
|
361
|
-
rectOutlineShader?: Shader
|
|
362
360
|
renderShader?: Shader
|
|
363
361
|
lineShader?: Shader
|
|
364
362
|
line3DShader?: Shader
|
|
@@ -404,19 +402,7 @@ export class Niivue {
|
|
|
404
402
|
extentsMax?: vec3
|
|
405
403
|
// ResizeObserver
|
|
406
404
|
private resizeObserver: ResizeObserver | null = null
|
|
407
|
-
|
|
408
|
-
syncOpts: SyncOpts = {
|
|
409
|
-
'3d': false, // legacy option
|
|
410
|
-
'2d': false, // legacy option
|
|
411
|
-
zoomPan: false,
|
|
412
|
-
cal_min: false,
|
|
413
|
-
cal_max: false,
|
|
414
|
-
clipPlane: false,
|
|
415
|
-
gamma: false,
|
|
416
|
-
sliceType: false,
|
|
417
|
-
crosshair: false
|
|
418
|
-
}
|
|
419
|
-
|
|
405
|
+
syncOpts: Record<string, unknown> = {}
|
|
420
406
|
readyForSync = false
|
|
421
407
|
|
|
422
408
|
// UI Data
|
|
@@ -466,7 +452,7 @@ export class Niivue {
|
|
|
466
452
|
|
|
467
453
|
cuboidVertexBuffer?: WebGLBuffer
|
|
468
454
|
|
|
469
|
-
otherNV: Niivue[] | null = null // another niivue instance that we wish to sync position with
|
|
455
|
+
otherNV: Niivue | Niivue[] | null = null // another niivue instance that we wish to sync position with
|
|
470
456
|
volumeObject3D: NiivueObject3D | null = null
|
|
471
457
|
pivot3D = [0, 0, 0] // center for rendering rotation
|
|
472
458
|
furthestFromPivot = 10.0 // most distant point from pivot
|
|
@@ -935,11 +921,7 @@ export class Niivue {
|
|
|
935
921
|
* @deprecated use broadcastTo instead
|
|
936
922
|
* @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
|
|
937
923
|
*/
|
|
938
|
-
syncWith(otherNV: Niivue
|
|
939
|
-
// if otherNV is not an array, make it an array of one
|
|
940
|
-
if (!(otherNV instanceof Array)) {
|
|
941
|
-
otherNV = [otherNV]
|
|
942
|
-
}
|
|
924
|
+
syncWith(otherNV: Niivue, syncOpts = { '2d': true, '3d': true }): void {
|
|
943
925
|
this.otherNV = otherNV
|
|
944
926
|
this.syncOpts = { ...syncOpts }
|
|
945
927
|
}
|
|
@@ -956,71 +938,10 @@ export class Niivue {
|
|
|
956
938
|
* @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
|
|
957
939
|
*/
|
|
958
940
|
broadcastTo(otherNV: Niivue | Niivue[], syncOpts = { '2d': true, '3d': true }): void {
|
|
959
|
-
// if otherNV is a single instance then make it an array of one
|
|
960
|
-
if (!(otherNV instanceof Array)) {
|
|
961
|
-
otherNV = [otherNV]
|
|
962
|
-
}
|
|
963
941
|
this.otherNV = otherNV
|
|
964
942
|
this.syncOpts = syncOpts
|
|
965
943
|
}
|
|
966
944
|
|
|
967
|
-
doSync3d(otherNV: Niivue): void {
|
|
968
|
-
otherNV.scene.renderAzimuth = this.scene.renderAzimuth
|
|
969
|
-
otherNV.scene.renderElevation = this.scene.renderElevation
|
|
970
|
-
otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// both crosshair and zoomPan
|
|
974
|
-
doSync2d(otherNV: Niivue): void {
|
|
975
|
-
const thisMM = this.frac2mm(this.scene.crosshairPos)
|
|
976
|
-
otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM)
|
|
977
|
-
otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
doSyncGamma(otherNV: Niivue): void {
|
|
981
|
-
// gamma not dependent on 2d/3d
|
|
982
|
-
const thisGamma = this.scene.gamma
|
|
983
|
-
const otherGamma = otherNV.scene.gamma
|
|
984
|
-
if (thisGamma !== otherGamma) {
|
|
985
|
-
otherNV.setGamma(thisGamma)
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
doSyncZoomPan(otherNV: Niivue): void {
|
|
990
|
-
otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
doSyncCrosshair(otherNV: Niivue): void {
|
|
994
|
-
const thisMM = this.frac2mm(this.scene.crosshairPos)
|
|
995
|
-
otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM)
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
doSyncCalMin(otherNV: Niivue): void {
|
|
999
|
-
// only call updateGLVolume if the cal_min is different
|
|
1000
|
-
// because updateGLVolume is expensive, but required to update the volume
|
|
1001
|
-
if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
|
|
1002
|
-
otherNV.volumes[0].cal_min = this.volumes[0].cal_min
|
|
1003
|
-
otherNV.updateGLVolume()
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
doSyncCalMax(otherNV: Niivue): void {
|
|
1008
|
-
// only call updateGLVolume if the cal_max is different
|
|
1009
|
-
// because updateGLVolume is expensive, but required to update the volume
|
|
1010
|
-
if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
|
|
1011
|
-
otherNV.volumes[0].cal_max = this.volumes[0].cal_max
|
|
1012
|
-
otherNV.updateGLVolume()
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
doSyncSliceType(otherNV: Niivue): void {
|
|
1017
|
-
otherNV.setSliceType(this.opts.sliceType)
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
doSyncClipPlane(otherNV: Niivue): void {
|
|
1021
|
-
otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev)
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
945
|
/**
|
|
1025
946
|
* Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
|
|
1026
947
|
* @internal
|
|
@@ -1034,52 +955,58 @@ export class Niivue {
|
|
|
1034
955
|
if (!this.gl || !this.otherNV || typeof this.otherNV === 'undefined') {
|
|
1035
956
|
return
|
|
1036
957
|
}
|
|
958
|
+
// if (!this.otherNV.readyForSync || !this.readyForSync) {
|
|
959
|
+
// return;
|
|
960
|
+
// }
|
|
1037
961
|
// canvas must have focus to send messages issue706
|
|
1038
962
|
if (!(this.gl.canvas as HTMLCanvasElement).matches(':focus')) {
|
|
1039
963
|
return
|
|
1040
964
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
this.doSyncCrosshair(this.otherNV[i])
|
|
1052
|
-
}
|
|
1053
|
-
// zoomPan
|
|
1054
|
-
if (this.syncOpts.zoomPan) {
|
|
1055
|
-
this.doSyncZoomPan(this.otherNV[i])
|
|
1056
|
-
}
|
|
1057
|
-
// sliceType
|
|
1058
|
-
if (this.syncOpts.sliceType) {
|
|
1059
|
-
this.doSyncSliceType(this.otherNV[i])
|
|
1060
|
-
}
|
|
1061
|
-
// cal_min
|
|
1062
|
-
if (this.syncOpts.cal_min) {
|
|
1063
|
-
this.doSyncCalMin(this.otherNV[i])
|
|
1064
|
-
}
|
|
1065
|
-
// cal_max
|
|
1066
|
-
if (this.syncOpts.cal_max) {
|
|
1067
|
-
this.doSyncCalMax(this.otherNV[i])
|
|
1068
|
-
}
|
|
1069
|
-
// clipPlane
|
|
1070
|
-
if (this.syncOpts.clipPlane) {
|
|
1071
|
-
this.doSyncClipPlane(this.otherNV[i])
|
|
1072
|
-
}
|
|
1073
|
-
// legacy 2d option for multiple properties
|
|
965
|
+
const thisMM = this.frac2mm(this.scene.crosshairPos)
|
|
966
|
+
// if this.otherNV is an object, then it is a single Niivue instance
|
|
967
|
+
if (this.otherNV instanceof Niivue) {
|
|
968
|
+
// gamma not dependent on 2d/3d
|
|
969
|
+
const thisGamma = this.scene.gamma
|
|
970
|
+
const otherGamma = this.otherNV.scene.gamma
|
|
971
|
+
if (thisGamma !== otherGamma) {
|
|
972
|
+
this.otherNV.setGamma(thisGamma)
|
|
973
|
+
}
|
|
974
|
+
// options specific to 2d rendering
|
|
1074
975
|
if (this.syncOpts['2d']) {
|
|
1075
|
-
this.
|
|
976
|
+
this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM)
|
|
977
|
+
this.otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
1076
978
|
}
|
|
1077
|
-
//
|
|
979
|
+
// options specific to 3d rendering
|
|
1078
980
|
if (this.syncOpts['3d']) {
|
|
1079
|
-
this.
|
|
981
|
+
this.otherNV.scene.renderAzimuth = this.scene.renderAzimuth
|
|
982
|
+
this.otherNV.scene.renderElevation = this.scene.renderElevation
|
|
983
|
+
this.otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
984
|
+
}
|
|
985
|
+
this.otherNV.drawScene()
|
|
986
|
+
this.otherNV.createOnLocationChange()
|
|
987
|
+
} else if (Array.isArray(this.otherNV)) {
|
|
988
|
+
for (let i = 0; i < this.otherNV.length; i++) {
|
|
989
|
+
if (this.otherNV[i] === this) {
|
|
990
|
+
continue
|
|
991
|
+
}
|
|
992
|
+
// gamma not dependent on 2d/3d
|
|
993
|
+
const thisGamma = this.scene.gamma
|
|
994
|
+
const otherGamma = this.otherNV[i].scene.gamma
|
|
995
|
+
if (thisGamma !== otherGamma) {
|
|
996
|
+
this.otherNV[i].setGamma(thisGamma)
|
|
997
|
+
}
|
|
998
|
+
if (this.syncOpts['2d']) {
|
|
999
|
+
this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM)
|
|
1000
|
+
this.otherNV[i].scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
1001
|
+
}
|
|
1002
|
+
if (this.syncOpts['3d']) {
|
|
1003
|
+
this.otherNV[i].scene.renderAzimuth = this.scene.renderAzimuth
|
|
1004
|
+
this.otherNV[i].scene.renderElevation = this.scene.renderElevation
|
|
1005
|
+
this.otherNV[i].scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
1006
|
+
}
|
|
1007
|
+
this.otherNV[i].drawScene()
|
|
1008
|
+
this.otherNV[i].createOnLocationChange()
|
|
1080
1009
|
}
|
|
1081
|
-
this.otherNV[i].drawScene()
|
|
1082
|
-
this.otherNV[i].createOnLocationChange()
|
|
1083
1010
|
}
|
|
1084
1011
|
}
|
|
1085
1012
|
|
|
@@ -1381,6 +1308,7 @@ export class Niivue {
|
|
|
1381
1308
|
const mmLength = vec3.len(v)
|
|
1382
1309
|
const voxStart = this.frac2vox(fracStart)
|
|
1383
1310
|
const voxEnd = this.frac2vox(fracEnd)
|
|
1311
|
+
|
|
1384
1312
|
this.onDragRelease({
|
|
1385
1313
|
fracStart,
|
|
1386
1314
|
fracEnd,
|
|
@@ -1433,11 +1361,6 @@ export class Niivue {
|
|
|
1433
1361
|
const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]])
|
|
1434
1362
|
const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]])
|
|
1435
1363
|
this.generateMouseUpCallback(fracStart, fracEnd)
|
|
1436
|
-
// if roiSelection drag mode
|
|
1437
|
-
if (this.opts.dragMode === DRAG_MODE.roiSelection) {
|
|
1438
|
-
// do not call drawScene so that the selection box remains visible
|
|
1439
|
-
return
|
|
1440
|
-
}
|
|
1441
1364
|
if (this.opts.dragMode !== DRAG_MODE.contrast) {
|
|
1442
1365
|
return
|
|
1443
1366
|
}
|
|
@@ -1855,6 +1778,13 @@ export class Niivue {
|
|
|
1855
1778
|
if (ext === 'GZ') {
|
|
1856
1779
|
ext = re.exec(fullname.slice(0, -3))![1] // img.trk.gz -> img.trk
|
|
1857
1780
|
ext = ext.toUpperCase()
|
|
1781
|
+
} else if (ext === 'CBOR') {
|
|
1782
|
+
// we want to keep cbor WITH the extension before it.
|
|
1783
|
+
// e.g. if fullname is img.iwi.cbor, we want the ext to be iwi.cbor
|
|
1784
|
+
const endExt = ext
|
|
1785
|
+
ext = re.exec(fullname.slice(0, -5))![1] // img.iwi.cbor -> iwi.cbor
|
|
1786
|
+
ext = ext.toUpperCase()
|
|
1787
|
+
ext = `${ext}.${endExt}`
|
|
1858
1788
|
}
|
|
1859
1789
|
return upperCase ? ext : ext.toLowerCase() // developer can choose to have extensions as upper or lower
|
|
1860
1790
|
}
|
|
@@ -1877,6 +1807,33 @@ export class Niivue {
|
|
|
1877
1807
|
|
|
1878
1808
|
async addVolumesFromUrl(imageOptionsArray: ImageFromUrlOptions[]): Promise<NVImage[]> {
|
|
1879
1809
|
const promises = imageOptionsArray.map(async (imageItem) => {
|
|
1810
|
+
// first check this.loaders to see if the user has
|
|
1811
|
+
// registered a custom loader for this file type.
|
|
1812
|
+
// if so, use that loader to load the file.
|
|
1813
|
+
const ext = this.getFileExt(imageItem.url)
|
|
1814
|
+
if (this.loaders[ext]) {
|
|
1815
|
+
let itemToLoad: string | Uint8Array | ArrayBuffer = imageItem.url
|
|
1816
|
+
const toExt = this.loaders[ext].toExt
|
|
1817
|
+
let name = imageItem.name || imageItem.url
|
|
1818
|
+
// in case the name is a url, just get the basename without the slashes
|
|
1819
|
+
name = name.split('/').pop()
|
|
1820
|
+
// if url is a string fetch the file first
|
|
1821
|
+
if (typeof imageItem.url === 'string') {
|
|
1822
|
+
const url = imageItem.url
|
|
1823
|
+
try {
|
|
1824
|
+
const response = await fetch(url)
|
|
1825
|
+
if (!response.ok) {
|
|
1826
|
+
throw new Error(`Failed to load file: ${response.statusText}`)
|
|
1827
|
+
}
|
|
1828
|
+
itemToLoad = await response.arrayBuffer()
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
throw new Error(`Failed to load url ${url}: ${error}`)
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
const buffer = await this.loaders[ext].loader(itemToLoad)
|
|
1834
|
+
imageItem.url = buffer
|
|
1835
|
+
imageItem.name = `${name}.${toExt}`
|
|
1836
|
+
}
|
|
1880
1837
|
const imageOptions = {
|
|
1881
1838
|
url: imageItem.url!,
|
|
1882
1839
|
headers: imageItem.headers,
|
|
@@ -2032,6 +1989,41 @@ export class Niivue {
|
|
|
2032
1989
|
})
|
|
2033
1990
|
}
|
|
2034
1991
|
|
|
1992
|
+
/* useLoader
|
|
1993
|
+
|
|
1994
|
+
registers an external file loader for niivue to use when reading files.
|
|
1995
|
+
|
|
1996
|
+
the loader must return an array buffer of the file contents for niivue
|
|
1997
|
+
to parse and the extension of the file so niivue can infer the file type to load.
|
|
1998
|
+
|
|
1999
|
+
example:
|
|
2000
|
+
|
|
2001
|
+
const myCustomLoader = (File) => {
|
|
2002
|
+
// ... do parsing here ...
|
|
2003
|
+
return {
|
|
2004
|
+
arrayBuffer: ArrayBuffer,
|
|
2005
|
+
fileExt: 'iwi.cbor',
|
|
2006
|
+
positions: Float32Array | [], // allows for mesh loading to mz3
|
|
2007
|
+
indices: Uint32Array | [], // allows for mesh loading to mz3
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
nv.useLoader({
|
|
2011
|
+
loader: myCustomLoader,
|
|
2012
|
+
fileExt: 'iwi.cbor',
|
|
2013
|
+
toExt: 'nii'
|
|
2014
|
+
})
|
|
2015
|
+
|
|
2016
|
+
*/
|
|
2017
|
+
useLoader(loader: unknown, fileExt: string, toExt: string): void {
|
|
2018
|
+
this.loaders = {
|
|
2019
|
+
...this.loaders,
|
|
2020
|
+
[fileExt.toUpperCase()]: {
|
|
2021
|
+
loader,
|
|
2022
|
+
toExt
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2035
2027
|
// not included in public docs
|
|
2036
2028
|
dropListener(e: DragEvent): void {
|
|
2037
2029
|
e.stopPropagation()
|
|
@@ -3881,6 +3873,45 @@ export class Niivue {
|
|
|
3881
3873
|
return this.document.download(fileName, compress)
|
|
3882
3874
|
}
|
|
3883
3875
|
|
|
3876
|
+
// generic loadImages that wraps loadVolumes and loadMeshes
|
|
3877
|
+
async loadImages(images: Array<ImageFromUrlOptions | LoadFromUrlParams>): Promise<this> {
|
|
3878
|
+
const volumes = []
|
|
3879
|
+
const meshes = []
|
|
3880
|
+
for (const image of images) {
|
|
3881
|
+
if ('url' in image) {
|
|
3882
|
+
const ext = this.getFileExt(image.url)
|
|
3883
|
+
// check this.loaders to see if a user has register
|
|
3884
|
+
// a custom loader for this file extension
|
|
3885
|
+
if (this.loaders[ext]) {
|
|
3886
|
+
// check if the loader type property is a volume or mesh
|
|
3887
|
+
// by using the toExt property
|
|
3888
|
+
const toExt = this.loaders[ext].toExt.toUpperCase()
|
|
3889
|
+
if (MESH_EXTENSIONS.includes(toExt)) {
|
|
3890
|
+
meshes.push(image)
|
|
3891
|
+
} else {
|
|
3892
|
+
volumes.push(image)
|
|
3893
|
+
}
|
|
3894
|
+
// continue to the next image
|
|
3895
|
+
continue
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
if (MESH_EXTENSIONS.includes(ext.toUpperCase())) {
|
|
3899
|
+
meshes.push(image)
|
|
3900
|
+
} else {
|
|
3901
|
+
volumes.push(image)
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
if (volumes.length > 0) {
|
|
3906
|
+
await this.loadVolumes(volumes as ImageFromUrlOptions[])
|
|
3907
|
+
}
|
|
3908
|
+
if (meshes.length > 0) {
|
|
3909
|
+
await this.loadMeshes(meshes as LoadFromUrlParams[])
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
return this
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3884
3915
|
/**
|
|
3885
3916
|
* load an array of volume objects
|
|
3886
3917
|
* @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
|
|
@@ -3905,30 +3936,33 @@ export class Niivue {
|
|
|
3905
3936
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
|
|
3906
3937
|
// if more than one volume, then fetch them all simultaneously
|
|
3907
3938
|
// using addVolumesFromUrl (note the "s" in "Volumes")
|
|
3908
|
-
if (volumeList.length > 1) {
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
}
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
headers: volumeList[0].headers,
|
|
3915
|
-
name: volumeList[0].name,
|
|
3916
|
-
colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
|
|
3917
|
-
colormapNegative: volumeList[0].colormapNegative
|
|
3918
|
-
? volumeList[0].colormapNegative
|
|
3919
|
-
: volumeList[0].colorMapNegative,
|
|
3920
|
-
opacity: volumeList[0].opacity,
|
|
3921
|
-
urlImgData: volumeList[0].urlImgData,
|
|
3922
|
-
cal_min: volumeList[0].cal_min,
|
|
3923
|
-
cal_max: volumeList[0].cal_max,
|
|
3924
|
-
trustCalMinMax: this.opts.trustCalMinMax,
|
|
3925
|
-
isManifest: volumeList[0].isManifest,
|
|
3926
|
-
frame4D: volumeList[0].frame4D,
|
|
3927
|
-
limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
|
|
3928
|
-
colorbarVisible: volumeList[0].colorbarVisible
|
|
3929
|
-
}
|
|
3930
|
-
await this.addVolumeFromUrl(imageOptions)
|
|
3939
|
+
// if (volumeList.length > 1) {
|
|
3940
|
+
// await this.addVolumesFromUrl(volumeList)
|
|
3941
|
+
// return this
|
|
3942
|
+
// }
|
|
3943
|
+
|
|
3944
|
+
await this.addVolumesFromUrl(volumeList)
|
|
3931
3945
|
return this
|
|
3946
|
+
// const imageOptions = {
|
|
3947
|
+
// url: volumeList[0].url!,
|
|
3948
|
+
// headers: volumeList[0].headers,
|
|
3949
|
+
// name: volumeList[0].name,
|
|
3950
|
+
// colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
|
|
3951
|
+
// colormapNegative: volumeList[0].colormapNegative
|
|
3952
|
+
// ? volumeList[0].colormapNegative
|
|
3953
|
+
// : volumeList[0].colorMapNegative,
|
|
3954
|
+
// opacity: volumeList[0].opacity,
|
|
3955
|
+
// urlImgData: volumeList[0].urlImgData,
|
|
3956
|
+
// cal_min: volumeList[0].cal_min,
|
|
3957
|
+
// cal_max: volumeList[0].cal_max,
|
|
3958
|
+
// trustCalMinMax: this.opts.trustCalMinMax,
|
|
3959
|
+
// isManifest: volumeList[0].isManifest,
|
|
3960
|
+
// frame4D: volumeList[0].frame4D,
|
|
3961
|
+
// limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
|
|
3962
|
+
// colorbarVisible: volumeList[0].colorbarVisible
|
|
3963
|
+
// }
|
|
3964
|
+
// await this.addVolumeFromUrl(imageOptions)
|
|
3965
|
+
// return this
|
|
3932
3966
|
}
|
|
3933
3967
|
|
|
3934
3968
|
/**
|
|
@@ -3959,7 +3993,34 @@ export class Niivue {
|
|
|
3959
3993
|
*/
|
|
3960
3994
|
async addMeshesFromUrl(meshOptions: LoadFromUrlParams[]): Promise<NVMesh[]> {
|
|
3961
3995
|
const promises = meshOptions.map(async (meshItem) => {
|
|
3996
|
+
// first check this.loaders to see if the user has
|
|
3997
|
+
// registered a custom loader for this file type.
|
|
3998
|
+
// if so, use that loader to load the file.
|
|
3962
3999
|
const ext = this.getFileExt(meshItem.url)
|
|
4000
|
+
if (this.loaders[ext]) {
|
|
4001
|
+
let itemToLoad: string | Uint8Array | ArrayBuffer = meshItem.url
|
|
4002
|
+
const toExt = this.loaders[ext].toExt
|
|
4003
|
+
let name = meshItem.name || meshItem.url
|
|
4004
|
+
// in case the name is a url, just get the basename without the slashes
|
|
4005
|
+
name = name.split('/').pop()
|
|
4006
|
+
if (typeof meshItem.url === 'string') {
|
|
4007
|
+
const url = meshItem.url
|
|
4008
|
+
try {
|
|
4009
|
+
const response = await fetch(url)
|
|
4010
|
+
if (!response.ok) {
|
|
4011
|
+
throw new Error(`Failed to load file: ${response.statusText}`)
|
|
4012
|
+
}
|
|
4013
|
+
itemToLoad = await response.arrayBuffer()
|
|
4014
|
+
} catch (error) {
|
|
4015
|
+
throw new Error(`Failed to load url ${url}: ${error}`)
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
const { positions, indices } = await this.loaders[ext].loader(itemToLoad)
|
|
4019
|
+
meshItem.name = `${name}.${toExt}`
|
|
4020
|
+
const mz3 = NVMeshUtilities.createMZ3(positions, indices, false)
|
|
4021
|
+
meshItem.buffer = mz3
|
|
4022
|
+
// return await this.loadFromArrayBuffer(mz3, meshItem.name)
|
|
4023
|
+
}
|
|
3963
4024
|
if (ext === 'JCON' || ext === 'JSON') {
|
|
3964
4025
|
const response = await fetch(meshItem.url, {})
|
|
3965
4026
|
const json = await response.json()
|
|
@@ -4008,17 +4069,17 @@ export class Niivue {
|
|
|
4008
4069
|
|
|
4009
4070
|
// if more than one mesh, then fetch them all simultaneously
|
|
4010
4071
|
// using addMeshesFromUrl (note the "s" in "Meshes")
|
|
4011
|
-
if (meshList.length > 1) {
|
|
4012
|
-
|
|
4013
|
-
this.updateGLVolume()
|
|
4014
|
-
this.drawScene()
|
|
4015
|
-
return this
|
|
4016
|
-
}
|
|
4017
|
-
|
|
4018
|
-
await this.addMeshFromUrl(meshList[0])
|
|
4072
|
+
// if (meshList.length > 1) {
|
|
4073
|
+
await this.addMeshesFromUrl(meshList)
|
|
4019
4074
|
this.updateGLVolume()
|
|
4020
4075
|
this.drawScene()
|
|
4021
4076
|
return this
|
|
4077
|
+
// }
|
|
4078
|
+
|
|
4079
|
+
// await this.addMeshFromUrl(meshList[0])
|
|
4080
|
+
// this.updateGLVolume()
|
|
4081
|
+
// this.drawScene()
|
|
4082
|
+
// return this
|
|
4022
4083
|
}
|
|
4023
4084
|
|
|
4024
4085
|
/**
|
|
@@ -4313,29 +4374,6 @@ export class Niivue {
|
|
|
4313
4374
|
y = Math.min(Math.max(y, 0), dy - 1)
|
|
4314
4375
|
z = Math.min(Math.max(z, 0), dz - 1)
|
|
4315
4376
|
this.drawBitmap![x + y * dx + z * dx * dy] = penValue
|
|
4316
|
-
// get tile index for voxel
|
|
4317
|
-
const isAx = this.drawPenAxCorSag === 0
|
|
4318
|
-
const isCor = this.drawPenAxCorSag === 1
|
|
4319
|
-
const isSag = this.drawPenAxCorSag === 2
|
|
4320
|
-
// since the pen is only drawing in one 2D plane,
|
|
4321
|
-
// only draw the neighbors (based on penSize) in that plane.
|
|
4322
|
-
// if penSize is 1, only draw the voxel itself.
|
|
4323
|
-
// if penSize is even (2, 4, 6, etc.), then the extra voxel will be drawn in the positive direction.
|
|
4324
|
-
// if penSize is odd (3, 5, 7, etc.), then the the pen will be centered on the voxel.
|
|
4325
|
-
if (this.opts.penSize > 1) {
|
|
4326
|
-
const halfPenSize = Math.floor(this.opts.penSize / 2)
|
|
4327
|
-
for (let i = -halfPenSize; i <= halfPenSize; i++) {
|
|
4328
|
-
for (let j = -halfPenSize; j <= halfPenSize; j++) {
|
|
4329
|
-
if (isAx) {
|
|
4330
|
-
this.drawBitmap![x + i + (y + j) * dx + z * dx * dy] = penValue
|
|
4331
|
-
} else if (isCor) {
|
|
4332
|
-
this.drawBitmap![x + i + y * dx + (z + j) * dx * dy] = penValue
|
|
4333
|
-
} else if (isSag) {
|
|
4334
|
-
this.drawBitmap![x + (y + j) * dx + (z + i) * dx * dy] = penValue
|
|
4335
|
-
}
|
|
4336
|
-
}
|
|
4337
|
-
}
|
|
4338
|
-
}
|
|
4339
4377
|
}
|
|
4340
4378
|
|
|
4341
4379
|
// not included in public docs
|
|
@@ -4422,140 +4460,6 @@ export class Niivue {
|
|
|
4422
4460
|
}
|
|
4423
4461
|
}
|
|
4424
4462
|
|
|
4425
|
-
/**
|
|
4426
|
-
* Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
|
|
4427
|
-
*
|
|
4428
|
-
* @param seedXYZ - voxel index of the seed voxel in the mask array.
|
|
4429
|
-
* @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
|
|
4430
|
-
*/
|
|
4431
|
-
drawingBinaryDilationWithSeed(
|
|
4432
|
-
seedXYZ: number[], // seed voxel x,y,z
|
|
4433
|
-
neighbors: 6 | 18 | 26 = 6
|
|
4434
|
-
): void {
|
|
4435
|
-
try {
|
|
4436
|
-
const mask = this.drawBitmap
|
|
4437
|
-
const xDim = this.back.dims[1]
|
|
4438
|
-
const yDim = this.back.dims[2]
|
|
4439
|
-
const zDim = this.back.dims[3]
|
|
4440
|
-
const nx = xDim
|
|
4441
|
-
const nxy = xDim * yDim
|
|
4442
|
-
const totalVoxels = nxy * zDim
|
|
4443
|
-
function xyz2vx(pt: number[]): number {
|
|
4444
|
-
return pt[0] + pt[1] * nx + pt[2] * nxy
|
|
4445
|
-
}
|
|
4446
|
-
|
|
4447
|
-
const seedIndex = xyz2vx(seedXYZ)
|
|
4448
|
-
|
|
4449
|
-
// check that the seed index is within bounds
|
|
4450
|
-
if (seedIndex < 0 || seedIndex >= totalVoxels) {
|
|
4451
|
-
throw new Error('Seed index is out of bounds.')
|
|
4452
|
-
}
|
|
4453
|
-
|
|
4454
|
-
// get value of the seed voxel
|
|
4455
|
-
const seedValue = mask[seedIndex]
|
|
4456
|
-
|
|
4457
|
-
// check that the seed voxel is filled
|
|
4458
|
-
if (seedValue === 0) {
|
|
4459
|
-
throw new Error('Seed voxel is not part of a filled cluster.')
|
|
4460
|
-
}
|
|
4461
|
-
|
|
4462
|
-
// create a copy of the mask to work on
|
|
4463
|
-
const img = mask.slice()
|
|
4464
|
-
// binarise the img since there could be multiple colors in the mask
|
|
4465
|
-
for (let i = 0; i < totalVoxels; i++) {
|
|
4466
|
-
img[i] = img[i] === seedValue ? 1 : 0
|
|
4467
|
-
}
|
|
4468
|
-
|
|
4469
|
-
// use drawFloodFillCore to identify the connected cluster starting from seedIndex
|
|
4470
|
-
this.drawFloodFillCore(img, seedIndex, neighbors)
|
|
4471
|
-
|
|
4472
|
-
// now, img has the cluster marked with value 2
|
|
4473
|
-
// create an output mask for dilation
|
|
4474
|
-
const outputMask = mask.slice() // Clone the original mask
|
|
4475
|
-
|
|
4476
|
-
// precompute neighbor offsets based on connectivity
|
|
4477
|
-
const neighborOffsets: number[] = []
|
|
4478
|
-
|
|
4479
|
-
// offsets for 6-connectivity (face neighbors)
|
|
4480
|
-
const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1]
|
|
4481
|
-
|
|
4482
|
-
neighborOffsets.push(...offsets6)
|
|
4483
|
-
|
|
4484
|
-
if (neighbors > 6) {
|
|
4485
|
-
// offsets for 18-connectivity (edge neighbors)
|
|
4486
|
-
neighborOffsets.push(
|
|
4487
|
-
-xDim - 1,
|
|
4488
|
-
-xDim + 1,
|
|
4489
|
-
xDim - 1,
|
|
4490
|
-
xDim + 1,
|
|
4491
|
-
-nxy - xDim,
|
|
4492
|
-
-nxy + xDim,
|
|
4493
|
-
-nxy - 1,
|
|
4494
|
-
-nxy + 1,
|
|
4495
|
-
nxy - xDim,
|
|
4496
|
-
nxy + xDim,
|
|
4497
|
-
nxy - 1,
|
|
4498
|
-
nxy + 1
|
|
4499
|
-
)
|
|
4500
|
-
}
|
|
4501
|
-
|
|
4502
|
-
if (neighbors > 18) {
|
|
4503
|
-
// offsets for 26-connectivity (corner neighbors)
|
|
4504
|
-
neighborOffsets.push(
|
|
4505
|
-
-nxy - xDim - 1,
|
|
4506
|
-
-nxy - xDim + 1,
|
|
4507
|
-
-nxy + xDim - 1,
|
|
4508
|
-
-nxy + xDim + 1,
|
|
4509
|
-
nxy - xDim - 1,
|
|
4510
|
-
nxy - xDim + 1,
|
|
4511
|
-
nxy + xDim - 1,
|
|
4512
|
-
nxy + xDim + 1
|
|
4513
|
-
)
|
|
4514
|
-
}
|
|
4515
|
-
|
|
4516
|
-
// iterate over the cluster voxels (value 2 in img) to perform dilation
|
|
4517
|
-
for (let idx = 0; idx < totalVoxels; idx++) {
|
|
4518
|
-
if (img[idx] === 2) {
|
|
4519
|
-
const x = idx % xDim
|
|
4520
|
-
const y = Math.floor((idx % nxy) / xDim)
|
|
4521
|
-
const z = Math.floor(idx / nxy)
|
|
4522
|
-
|
|
4523
|
-
for (const offset of neighborOffsets) {
|
|
4524
|
-
const neighborIdx = idx + offset
|
|
4525
|
-
|
|
4526
|
-
// skip if neighbor index is out of bounds
|
|
4527
|
-
if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
|
|
4528
|
-
continue
|
|
4529
|
-
}
|
|
4530
|
-
|
|
4531
|
-
// calculate neighbor coordinates
|
|
4532
|
-
const nx = neighborIdx % xDim
|
|
4533
|
-
const ny = Math.floor((neighborIdx % nxy) / xDim)
|
|
4534
|
-
const nz = Math.floor(neighborIdx / nxy)
|
|
4535
|
-
|
|
4536
|
-
// ensure neighbor is adjacent (prevent wrapping around edges)
|
|
4537
|
-
if (Math.abs(nx - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
|
|
4538
|
-
continue
|
|
4539
|
-
}
|
|
4540
|
-
|
|
4541
|
-
// if the neighbor voxel is empty in the original mask, fill it in the output mask
|
|
4542
|
-
if (mask[neighborIdx] === 0) {
|
|
4543
|
-
outputMask[neighborIdx] = seedValue
|
|
4544
|
-
}
|
|
4545
|
-
}
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
// set the output as the new drawing bitmap
|
|
4549
|
-
this.drawBitmap = outputMask
|
|
4550
|
-
// update the undo stack
|
|
4551
|
-
this.drawAddUndoBitmap()
|
|
4552
|
-
// refresh the drawing (copy from cpu to gpu) and show immediately
|
|
4553
|
-
this.refreshDrawing(true)
|
|
4554
|
-
} catch (error) {
|
|
4555
|
-
log.error('Error in drawingBinaryDilationWithSeed:', error)
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
|
|
4559
4463
|
// a voxel can be defined as having 6, 18 or 26 neighbors:
|
|
4560
4464
|
// 6: neighbors share faces (distance=1)
|
|
4561
4465
|
// 18: neighbors share faces (distance=1) or edges (1.4)
|
|
@@ -4658,9 +4562,7 @@ export class Niivue {
|
|
|
4658
4562
|
growSelectedCluster = 0, // if non-zero, growth based on background intensity POSITIVE_INFINITY for selected or bright, NEGATIVE_INFINITY for selected or darker
|
|
4659
4563
|
forceMin = NaN,
|
|
4660
4564
|
forceMax = NaN,
|
|
4661
|
-
neighbors = 6
|
|
4662
|
-
// option for only flood filling within max distance from seed voxel
|
|
4663
|
-
maxDistanceMM = Number.POSITIVE_INFINITY
|
|
4565
|
+
neighbors = 6
|
|
4664
4566
|
): void {
|
|
4665
4567
|
if (!this.drawBitmap) {
|
|
4666
4568
|
throw new Error('drawBitmap undefined')
|
|
@@ -4686,29 +4588,10 @@ export class Niivue {
|
|
|
4686
4588
|
if (img.length !== nxy * dims[2]) {
|
|
4687
4589
|
return
|
|
4688
4590
|
}
|
|
4689
|
-
function vx2xyz(vx: number): number[] {
|
|
4690
|
-
// provided address in 1D array, return XYZ coordinate
|
|
4691
|
-
const Z = Math.floor(vx / nxy) // slice
|
|
4692
|
-
const Y = Math.floor((vx - Z * nxy) / nx) // column
|
|
4693
|
-
const X = Math.floor(vx % nx)
|
|
4694
|
-
return [X, Y, Z]
|
|
4695
|
-
}
|
|
4696
4591
|
function xyz2vx(pt: number[]): number {
|
|
4697
4592
|
// provided an XYZ 3D point, provide address in 1D array
|
|
4698
4593
|
return pt[0] + pt[1] * nx + pt[2] * nxy
|
|
4699
4594
|
}
|
|
4700
|
-
const vx2mm = (xyz: number[]): vec3 => {
|
|
4701
|
-
return this.vox2mm(xyz, this.back.matRAS)
|
|
4702
|
-
}
|
|
4703
|
-
// store seed vox as mm coordinates
|
|
4704
|
-
const seedMM = vx2mm(seedXYZ)
|
|
4705
|
-
const maxDistanceMM2 = maxDistanceMM ** 2
|
|
4706
|
-
// function to check if new point to be checked is less than maxDistanceMM
|
|
4707
|
-
function isWithinDistance(vx: number): boolean {
|
|
4708
|
-
const xyzMM = vx2mm(vx2xyz(vx))
|
|
4709
|
-
const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2
|
|
4710
|
-
return dist2 <= maxDistanceMM2
|
|
4711
|
-
}
|
|
4712
4595
|
const seedVx = xyz2vx(seedXYZ)
|
|
4713
4596
|
const seedColor = img[seedVx]
|
|
4714
4597
|
if (seedColor === newColor) {
|
|
@@ -4722,12 +4605,6 @@ export class Niivue {
|
|
|
4722
4605
|
for (let i = 1; i < nxyz; i++) {
|
|
4723
4606
|
img[i] = 0
|
|
4724
4607
|
if (this.drawBitmap[i] === seedColor) {
|
|
4725
|
-
// check if voxel index i is within maxDistanceMM from seed voxel
|
|
4726
|
-
if (!isWithinDistance(i)) {
|
|
4727
|
-
// move to next voxel if not within distance,
|
|
4728
|
-
// no need to check voxel intensity for cluster assignment
|
|
4729
|
-
continue
|
|
4730
|
-
}
|
|
4731
4608
|
img[i] = 1
|
|
4732
4609
|
}
|
|
4733
4610
|
}
|
|
@@ -4759,12 +4636,6 @@ export class Niivue {
|
|
|
4759
4636
|
for (let i = 1; i < nxyz; i++) {
|
|
4760
4637
|
img[i] = 0
|
|
4761
4638
|
if (backImg[i] >= mn && backImg[i] <= mx) {
|
|
4762
|
-
// check if voxel index i is within maxDistanceMM from seed voxel
|
|
4763
|
-
if (!isWithinDistance(i)) {
|
|
4764
|
-
// move to next voxel if not within distance,
|
|
4765
|
-
// no need to check voxel intensity for cluster assignment
|
|
4766
|
-
continue
|
|
4767
|
-
}
|
|
4768
4639
|
img[i] = 1
|
|
4769
4640
|
}
|
|
4770
4641
|
}
|
|
@@ -5473,8 +5344,6 @@ export class Niivue {
|
|
|
5473
5344
|
// rect shader (crosshair): horizontal and vertical lines only
|
|
5474
5345
|
this.rectShader = new Shader(gl, vertRectShader, fragRectShader)
|
|
5475
5346
|
this.rectShader.use(gl)
|
|
5476
|
-
this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader)
|
|
5477
|
-
this.rectOutlineShader.use(gl)
|
|
5478
5347
|
// line shader: diagonal lines
|
|
5479
5348
|
this.lineShader = new Shader(gl, vertLineShader, fragRectShader)
|
|
5480
5349
|
this.lineShader.use(gl)
|
|
@@ -5650,46 +5519,14 @@ export class Niivue {
|
|
|
5650
5519
|
|
|
5651
5520
|
/**
|
|
5652
5521
|
* basic statistics for selected voxel-based image
|
|
5653
|
-
* @param
|
|
5654
|
-
*
|
|
5655
|
-
*
|
|
5656
|
-
*
|
|
5657
|
-
* - roiIsMask: a boolean indicating if the ROI is used as a mask
|
|
5658
|
-
* - startVox: the starting voxel coordinates
|
|
5659
|
-
* - endVox: the ending voxel coordinates
|
|
5660
|
-
* @returns numeric values to describe image or regions of images
|
|
5661
|
-
* @example
|
|
5662
|
-
* niivue.getDescriptives({
|
|
5663
|
-
* layer: 0,
|
|
5664
|
-
* masks: [],
|
|
5665
|
-
* drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
|
|
5666
|
-
* roiIsMask: false,
|
|
5667
|
-
* startVox: [10, 20, 30], // ignored if roiIsMask is false
|
|
5668
|
-
* endVox: [40, 50, 60] // ignored if roiIsMask is false
|
|
5669
|
-
* });
|
|
5522
|
+
* @param layer - selects image to describe
|
|
5523
|
+
* @param masks - are optional binary images to filter voxles
|
|
5524
|
+
* @returns numeric values to describe image
|
|
5525
|
+
* @example niivue.getDescriptives(0);
|
|
5670
5526
|
* @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
|
|
5671
5527
|
*/
|
|
5672
|
-
getDescriptives(
|
|
5673
|
-
layer?: number
|
|
5674
|
-
masks?: number[]
|
|
5675
|
-
drawingIsMask?: boolean
|
|
5676
|
-
roiIsMask?: boolean
|
|
5677
|
-
startVox?: number[]
|
|
5678
|
-
endVox?: number[]
|
|
5679
|
-
}): Descriptive {
|
|
5680
|
-
const {
|
|
5681
|
-
layer = 0,
|
|
5682
|
-
masks = [],
|
|
5683
|
-
drawingIsMask = false,
|
|
5684
|
-
roiIsMask = false,
|
|
5685
|
-
startVox = [0, 0, 0],
|
|
5686
|
-
endVox = [0, 0, 0]
|
|
5687
|
-
} = options
|
|
5688
|
-
|
|
5689
|
-
// Rest of the code remains the same
|
|
5690
|
-
let area = null // used if roiIsMask since ROI is in 2D slice
|
|
5528
|
+
getDescriptives(layer = 0, masks = [], drawingIsMask = false): Descriptive {
|
|
5691
5529
|
const hdr = this.volumes[layer].hdr!
|
|
5692
|
-
const pixDimsRAS = this.volumes[layer].pixDimsRAS!
|
|
5693
5530
|
let slope = hdr.scl_slope
|
|
5694
5531
|
if (isNaN(slope)) {
|
|
5695
5532
|
slope = 1
|
|
@@ -5728,48 +5565,6 @@ export class Niivue {
|
|
|
5728
5565
|
mask[i] = 0
|
|
5729
5566
|
}
|
|
5730
5567
|
}
|
|
5731
|
-
} else if (masks.length < 1 && roiIsMask) {
|
|
5732
|
-
// fill mask with zeros
|
|
5733
|
-
mask.fill(0)
|
|
5734
|
-
console.log('startVox', startVox)
|
|
5735
|
-
console.log('endVox', endVox)
|
|
5736
|
-
// const startVox = roiInfo.startVox
|
|
5737
|
-
// const endVox = roiInfo.endVox
|
|
5738
|
-
const xrange = this.calculateMinMaxVoxIdx([startVox[0], endVox[0]])
|
|
5739
|
-
const yrange = this.calculateMinMaxVoxIdx([startVox[1], endVox[1]])
|
|
5740
|
-
const zrange = this.calculateMinMaxVoxIdx([startVox[2], endVox[2]])
|
|
5741
|
-
|
|
5742
|
-
// for our constant dimension we add one so that the for loop runs at least once
|
|
5743
|
-
if (startVox[0] - endVox[0] === 0) {
|
|
5744
|
-
xrange[1] = startVox[0] + 1
|
|
5745
|
-
} else if (startVox[1] - endVox[1] === 0) {
|
|
5746
|
-
yrange[1] = startVox[1] + 1
|
|
5747
|
-
} else if (startVox[2] - endVox[2] === 0) {
|
|
5748
|
-
zrange[1] = startVox[2] + 1
|
|
5749
|
-
}
|
|
5750
|
-
|
|
5751
|
-
const xdim = hdr.dims[1]
|
|
5752
|
-
const ydim = hdr.dims[2]
|
|
5753
|
-
for (let z = zrange[0]; z < zrange[1]; z++) {
|
|
5754
|
-
const zi = z * xdim * ydim
|
|
5755
|
-
for (let y = yrange[0]; y < yrange[1]; y++) {
|
|
5756
|
-
const yi = y * xdim
|
|
5757
|
-
for (let x = xrange[0]; x < xrange[1]; x++) {
|
|
5758
|
-
const index = zi + yi + x
|
|
5759
|
-
mask[index] = 1
|
|
5760
|
-
}
|
|
5761
|
-
}
|
|
5762
|
-
}
|
|
5763
|
-
// calculate area 2d area from startVox and endVox coordinates if roiIsMask is true (since mask is created from a 2d slice only)
|
|
5764
|
-
// area is in mm^2. The below calculations work regardless of the orientation of the image since
|
|
5765
|
-
// Niivue converts all images to RAS
|
|
5766
|
-
const xmm = Math.abs(pixDimsRAS[1] * (endVox[0] - startVox[0]))
|
|
5767
|
-
const ymm = Math.abs(pixDimsRAS[2] * (endVox[1] - startVox[1]))
|
|
5768
|
-
const zmm = Math.abs(pixDimsRAS[3] * (endVox[2] - startVox[2]))
|
|
5769
|
-
// one of the above will be zero since roiIsMask==true, so we filter out the non-zero value
|
|
5770
|
-
// and then multiply the other two to get the area
|
|
5771
|
-
const mms = [xmm, ymm, zmm]
|
|
5772
|
-
area = mms.filter((m) => m !== 0).reduce((a, b) => a * b, 1)
|
|
5773
5568
|
}
|
|
5774
5569
|
// Welfords method
|
|
5775
5570
|
// https://www.embeddedrelated.com/showarticle/785.php
|
|
@@ -5830,8 +5625,7 @@ export class Niivue {
|
|
|
5830
5625
|
cal_min: this.volumes[layer].cal_min!,
|
|
5831
5626
|
cal_max: this.volumes[layer].cal_max!,
|
|
5832
5627
|
robust_min: this.volumes[layer].robust_min!,
|
|
5833
|
-
robust_max: this.volumes[layer].robust_max
|
|
5834
|
-
area
|
|
5628
|
+
robust_max: this.volumes[layer].robust_max!
|
|
5835
5629
|
}
|
|
5836
5630
|
}
|
|
5837
5631
|
|
|
@@ -7597,70 +7391,39 @@ export class Niivue {
|
|
|
7597
7391
|
if (this.opts.drawingEnabled) {
|
|
7598
7392
|
// drawing done in voxels
|
|
7599
7393
|
const pt = this.frac2vox(this.scene.crosshairPos) as [number, number, number]
|
|
7394
|
+
// radius given in mm
|
|
7395
|
+
const ptMM = this.frac2mm(this.scene.crosshairPos)
|
|
7600
7396
|
// if click-to-segment enabled
|
|
7601
7397
|
if (this.opts.clickToSegment) {
|
|
7602
|
-
|
|
7603
|
-
const
|
|
7604
|
-
if (this.opts.clickToSegmentPercent > 0 && this.opts.clickToSegmentPercent <= 1) {
|
|
7605
|
-
this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + this.opts.clickToSegmentPercent)
|
|
7606
|
-
this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - this.opts.clickToSegmentPercent)
|
|
7607
|
-
}
|
|
7608
|
-
// if clickToSegmentAutoIntensity, then calculate if we need to flood fill
|
|
7609
|
-
// in a bright or dark region based on the intensity of the clicked voxel,
|
|
7610
|
-
// and where it falls in the range of cal_min and cal_max.
|
|
7611
|
-
// !important! If this option is true, then it will ignore the boolean value of
|
|
7612
|
-
// clickToSegmentBright supplied by the user
|
|
7613
|
-
if (this.opts.clickToSegmentAutoIntensity) {
|
|
7614
|
-
// if voxel intensity is greater than the midpoint of cal_min and cal_max,
|
|
7615
|
-
// then flood fill in a bright region
|
|
7616
|
-
if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
|
|
7617
|
-
this.opts.clickToSegmentBright = true
|
|
7618
|
-
} else {
|
|
7619
|
-
// else flood fill in a dark region
|
|
7620
|
-
this.opts.clickToSegmentBright = false
|
|
7621
|
-
}
|
|
7622
|
-
}
|
|
7623
|
-
// set brightOrDark now, if clickToSegmentAutoBrightOrDark is false,
|
|
7624
|
-
// then brightOrDark will be set to the value of clickToSegmentBright supplied by the user
|
|
7398
|
+
const radius = this.opts.clickToSegmentRadius
|
|
7399
|
+
const steps = this.opts.clickToSegmentSteps
|
|
7625
7400
|
const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY
|
|
7401
|
+
this.drawPenFillPts = []
|
|
7626
7402
|
this.drawPenAxCorSag = axCorSag
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
//
|
|
7630
|
-
const
|
|
7631
|
-
const
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
const
|
|
7635
|
-
const
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7403
|
+
for (let i = 1; i <= steps; i++) {
|
|
7404
|
+
const angle = (i / steps) * 2 * Math.PI
|
|
7405
|
+
// get the x,y,z in mm since radius given in mm
|
|
7406
|
+
const xMM = ptMM[0] + radius * Math.cos(angle)
|
|
7407
|
+
const yMM = ptMM[1] + radius * Math.sin(angle)
|
|
7408
|
+
const zMM = ptMM[2]
|
|
7409
|
+
// convert x,y,z in mm to voxels for drawing
|
|
7410
|
+
const xVox = this.back.mm2vox([xMM, yMM, zMM])[0]
|
|
7411
|
+
const yVox = this.back.mm2vox([xMM, yMM, zMM])[1]
|
|
7412
|
+
const zVox = this.back.mm2vox([xMM, yMM, zMM])[2]
|
|
7413
|
+
// draw the point
|
|
7414
|
+
this.drawPt(xVox, yVox, zVox, this.opts.penValue)
|
|
7415
|
+
this.drawPenFillPts.push([xVox, yVox, pt[2]])
|
|
7416
|
+
// fill in the circle if we are at the last step.
|
|
7417
|
+
// This also triggers the growth of the circle based on cluster intensity method of flood fill.
|
|
7418
|
+
// If the circle is drawn in a bright region, it will grow in the bright region using all connected bright voxels and vice versa.
|
|
7419
|
+
if (i === steps) {
|
|
7420
|
+
this.drawFloodFill([xVox, yVox, pt[2]], 0, brightOrDark, NaN, NaN, this.opts.floodFillNeighbors)
|
|
7421
|
+
}
|
|
7641
7422
|
}
|
|
7642
|
-
// draw the point at the cursor using the penSize to set the
|
|
7643
|
-
// seed for the flood fill
|
|
7644
|
-
// !important! clickToSegment performs two drawing operations,
|
|
7645
|
-
// therefore, if you want to undo the clickToSegment, you need to call undo twice
|
|
7646
|
-
this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue)
|
|
7647
|
-
this.drawFloodFill(
|
|
7648
|
-
[pt[0], pt[1], pt[2]],
|
|
7649
|
-
0,
|
|
7650
|
-
brightOrDark,
|
|
7651
|
-
this.opts.clickToSegmentIntensityMin,
|
|
7652
|
-
this.opts.clickToSegmentIntensityMax,
|
|
7653
|
-
this.opts.floodFillNeighbors,
|
|
7654
|
-
this.opts.clickToSegmentMaxDistanceMM
|
|
7655
|
-
)
|
|
7656
7423
|
this.drawScene()
|
|
7657
7424
|
this.createOnLocationChange(axCorSag)
|
|
7658
7425
|
// get the volume of the segmented region
|
|
7659
|
-
const info = this.getDescriptives(
|
|
7660
|
-
layer: 0,
|
|
7661
|
-
masks: [],
|
|
7662
|
-
drawingIsMask: true
|
|
7663
|
-
})
|
|
7426
|
+
const info = this.getDescriptives(0, [], true)
|
|
7664
7427
|
this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 })
|
|
7665
7428
|
return
|
|
7666
7429
|
}
|
|
@@ -7912,43 +7675,20 @@ export class Niivue {
|
|
|
7912
7675
|
if (!this.rectShader) {
|
|
7913
7676
|
throw new Error('rectShader undefined')
|
|
7914
7677
|
}
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
this.
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
|
|
7930
|
-
} else {
|
|
7931
|
-
// this.opts.selectionBoxIsOutline == true
|
|
7932
|
-
this.rectOutlineShader.use(this.gl)
|
|
7933
|
-
this.gl.enable(this.gl.BLEND)
|
|
7934
|
-
// set thickness of line
|
|
7935
|
-
this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness)
|
|
7936
|
-
this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor)
|
|
7937
|
-
this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
|
|
7938
|
-
this.gl.canvas.width,
|
|
7939
|
-
this.gl.canvas.height
|
|
7940
|
-
])
|
|
7941
|
-
this.gl.uniform4f(
|
|
7942
|
-
this.rectOutlineShader.uniforms.leftTopWidthHeight,
|
|
7943
|
-
leftTopWidthHeight[0],
|
|
7944
|
-
leftTopWidthHeight[1],
|
|
7945
|
-
leftTopWidthHeight[2],
|
|
7946
|
-
leftTopWidthHeight[3]
|
|
7947
|
-
)
|
|
7948
|
-
this.gl.bindVertexArray(this.genericVAO)
|
|
7949
|
-
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
7950
|
-
this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
|
|
7951
|
-
}
|
|
7678
|
+
this.rectShader.use(this.gl)
|
|
7679
|
+
this.gl.enable(this.gl.BLEND)
|
|
7680
|
+
this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor)
|
|
7681
|
+
this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height])
|
|
7682
|
+
this.gl.uniform4f(
|
|
7683
|
+
this.rectShader.uniforms.leftTopWidthHeight,
|
|
7684
|
+
leftTopWidthHeight[0],
|
|
7685
|
+
leftTopWidthHeight[1],
|
|
7686
|
+
leftTopWidthHeight[2],
|
|
7687
|
+
leftTopWidthHeight[3]
|
|
7688
|
+
)
|
|
7689
|
+
this.gl.bindVertexArray(this.genericVAO)
|
|
7690
|
+
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
7691
|
+
this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
|
|
7952
7692
|
}
|
|
7953
7693
|
|
|
7954
7694
|
drawCircle(leftTopWidthHeight: number[], circleColor = this.opts.fontColor, fillPercent = 1.0): void {
|
|
@@ -11120,18 +10860,12 @@ export class Niivue {
|
|
|
11120
10860
|
width,
|
|
11121
10861
|
height
|
|
11122
10862
|
])
|
|
11123
|
-
return
|
|
11124
10863
|
}
|
|
11125
10864
|
|
|
11126
10865
|
// draw circle at mouse position if clickToSegment is enabled
|
|
11127
10866
|
if (this.opts.clickToSegment) {
|
|
11128
10867
|
const x = this.mousePos[0]
|
|
11129
10868
|
const y = this.mousePos[1]
|
|
11130
|
-
// check if hovering over the 3D render tile
|
|
11131
|
-
if (this.inRenderTile(x, y) >= 0) {
|
|
11132
|
-
// exit early since we do not want to draw the cursor here!
|
|
11133
|
-
return
|
|
11134
|
-
}
|
|
11135
10869
|
// determine the tile the mouse is hovering in
|
|
11136
10870
|
const tileIdx = this.tileIndex(x, y)
|
|
11137
10871
|
// if a valid tile index, draw the circle
|