@aics/vole-core 3.12.4
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/LICENSE.txt +26 -0
- package/README.md +119 -0
- package/es/Atlas2DSlice.js +224 -0
- package/es/Channel.js +264 -0
- package/es/FileSaver.js +31 -0
- package/es/FusedChannelData.js +192 -0
- package/es/Histogram.js +250 -0
- package/es/ImageInfo.js +127 -0
- package/es/Light.js +74 -0
- package/es/Lut.js +500 -0
- package/es/MarchingCubes.js +507 -0
- package/es/MeshVolume.js +334 -0
- package/es/NaiveSurfaceNets.js +251 -0
- package/es/PathTracedVolume.js +482 -0
- package/es/RayMarchedAtlasVolume.js +250 -0
- package/es/RenderToBuffer.js +31 -0
- package/es/ThreeJsPanel.js +633 -0
- package/es/Timing.js +28 -0
- package/es/TrackballControls.js +538 -0
- package/es/View3d.js +848 -0
- package/es/Volume.js +352 -0
- package/es/VolumeCache.js +161 -0
- package/es/VolumeDims.js +16 -0
- package/es/VolumeDrawable.js +702 -0
- package/es/VolumeMaker.js +101 -0
- package/es/VolumeRenderImpl.js +1 -0
- package/es/VolumeRenderSettings.js +203 -0
- package/es/constants/basicShaders.js +29 -0
- package/es/constants/colors.js +59 -0
- package/es/constants/denoiseShader.js +43 -0
- package/es/constants/lights.js +42 -0
- package/es/constants/materials.js +85 -0
- package/es/constants/pathtraceOutputShader.js +13 -0
- package/es/constants/scaleBarSVG.js +21 -0
- package/es/constants/time.js +34 -0
- package/es/constants/volumePTshader.js +153 -0
- package/es/constants/volumeRayMarchShader.js +123 -0
- package/es/constants/volumeSliceShader.js +115 -0
- package/es/index.js +21 -0
- package/es/loaders/IVolumeLoader.js +131 -0
- package/es/loaders/JsonImageInfoLoader.js +255 -0
- package/es/loaders/OmeZarrLoader.js +495 -0
- package/es/loaders/OpenCellLoader.js +65 -0
- package/es/loaders/RawArrayLoader.js +89 -0
- package/es/loaders/TiffLoader.js +219 -0
- package/es/loaders/VolumeLoadError.js +44 -0
- package/es/loaders/VolumeLoaderUtils.js +221 -0
- package/es/loaders/index.js +40 -0
- package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +143 -0
- package/es/loaders/zarr_utils/WrappedStore.js +51 -0
- package/es/loaders/zarr_utils/types.js +24 -0
- package/es/loaders/zarr_utils/utils.js +225 -0
- package/es/loaders/zarr_utils/validation.js +49 -0
- package/es/test/ChunkPrefetchIterator.test.js +208 -0
- package/es/test/RequestQueue.test.js +442 -0
- package/es/test/SubscribableRequestQueue.test.js +244 -0
- package/es/test/VolumeCache.test.js +118 -0
- package/es/test/VolumeRenderSettings.test.js +71 -0
- package/es/test/lut.test.js +671 -0
- package/es/test/num_utils.test.js +140 -0
- package/es/test/volume.test.js +98 -0
- package/es/test/zarr_utils.test.js +358 -0
- package/es/types/Atlas2DSlice.d.ts +41 -0
- package/es/types/Channel.d.ts +44 -0
- package/es/types/FileSaver.d.ts +6 -0
- package/es/types/FusedChannelData.d.ts +26 -0
- package/es/types/Histogram.d.ts +57 -0
- package/es/types/ImageInfo.d.ts +87 -0
- package/es/types/Light.d.ts +27 -0
- package/es/types/Lut.d.ts +67 -0
- package/es/types/MarchingCubes.d.ts +53 -0
- package/es/types/MeshVolume.d.ts +40 -0
- package/es/types/NaiveSurfaceNets.d.ts +11 -0
- package/es/types/PathTracedVolume.d.ts +65 -0
- package/es/types/RayMarchedAtlasVolume.d.ts +41 -0
- package/es/types/RenderToBuffer.d.ts +17 -0
- package/es/types/ThreeJsPanel.d.ts +107 -0
- package/es/types/Timing.d.ts +11 -0
- package/es/types/TrackballControls.d.ts +51 -0
- package/es/types/View3d.d.ts +357 -0
- package/es/types/Volume.d.ts +152 -0
- package/es/types/VolumeCache.d.ts +43 -0
- package/es/types/VolumeDims.d.ts +28 -0
- package/es/types/VolumeDrawable.d.ts +108 -0
- package/es/types/VolumeMaker.d.ts +49 -0
- package/es/types/VolumeRenderImpl.d.ts +22 -0
- package/es/types/VolumeRenderSettings.d.ts +98 -0
- package/es/types/constants/basicShaders.d.ts +4 -0
- package/es/types/constants/colors.d.ts +2 -0
- package/es/types/constants/denoiseShader.d.ts +40 -0
- package/es/types/constants/lights.d.ts +38 -0
- package/es/types/constants/materials.d.ts +20 -0
- package/es/types/constants/pathtraceOutputShader.d.ts +11 -0
- package/es/types/constants/scaleBarSVG.d.ts +2 -0
- package/es/types/constants/time.d.ts +19 -0
- package/es/types/constants/volumePTshader.d.ts +137 -0
- package/es/types/constants/volumeRayMarchShader.d.ts +117 -0
- package/es/types/constants/volumeSliceShader.d.ts +109 -0
- package/es/types/glsl.d.js +0 -0
- package/es/types/index.d.ts +28 -0
- package/es/types/loaders/IVolumeLoader.d.ts +113 -0
- package/es/types/loaders/JsonImageInfoLoader.d.ts +80 -0
- package/es/types/loaders/OmeZarrLoader.d.ts +87 -0
- package/es/types/loaders/OpenCellLoader.d.ts +9 -0
- package/es/types/loaders/RawArrayLoader.d.ts +33 -0
- package/es/types/loaders/TiffLoader.d.ts +45 -0
- package/es/types/loaders/VolumeLoadError.d.ts +18 -0
- package/es/types/loaders/VolumeLoaderUtils.d.ts +38 -0
- package/es/types/loaders/index.d.ts +22 -0
- package/es/types/loaders/zarr_utils/ChunkPrefetchIterator.d.ts +22 -0
- package/es/types/loaders/zarr_utils/WrappedStore.d.ts +24 -0
- package/es/types/loaders/zarr_utils/types.d.ts +94 -0
- package/es/types/loaders/zarr_utils/utils.d.ts +23 -0
- package/es/types/loaders/zarr_utils/validation.d.ts +7 -0
- package/es/types/test/ChunkPrefetchIterator.test.d.ts +1 -0
- package/es/types/test/RequestQueue.test.d.ts +1 -0
- package/es/types/test/SubscribableRequestQueue.test.d.ts +1 -0
- package/es/types/test/VolumeCache.test.d.ts +1 -0
- package/es/types/test/VolumeRenderSettings.test.d.ts +1 -0
- package/es/types/test/lut.test.d.ts +1 -0
- package/es/types/test/num_utils.test.d.ts +1 -0
- package/es/types/test/volume.test.d.ts +1 -0
- package/es/types/test/zarr_utils.test.d.ts +1 -0
- package/es/types/types.d.ts +115 -0
- package/es/types/utils/RequestQueue.d.ts +112 -0
- package/es/types/utils/SubscribableRequestQueue.d.ts +52 -0
- package/es/types/utils/num_utils.d.ts +43 -0
- package/es/types/workers/VolumeLoaderContext.d.ts +106 -0
- package/es/types/workers/types.d.ts +101 -0
- package/es/types/workers/util.d.ts +3 -0
- package/es/types.js +75 -0
- package/es/typings.d.js +0 -0
- package/es/utils/RequestQueue.js +267 -0
- package/es/utils/SubscribableRequestQueue.js +187 -0
- package/es/utils/num_utils.js +231 -0
- package/es/workers/FetchTiffWorker.js +153 -0
- package/es/workers/VolumeLoadWorker.js +129 -0
- package/es/workers/VolumeLoaderContext.js +271 -0
- package/es/workers/types.js +41 -0
- package/es/workers/util.js +8 -0
- package/package.json +83 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Color, DataTexture, RedFormat, UnsignedByteType, ClampToEdgeWrapping, Scene, OrthographicCamera, WebGLRenderTarget, RGBAFormat, ShaderMaterial, Mesh, PlaneGeometry, OneFactor, CustomBlending, MaxEquation, LinearFilter, Vector2 } from "three";
|
|
2
|
+
import { renderToBufferVertShader } from "./constants/basicShaders.js";
|
|
3
|
+
/* babel-plugin-inline-import './constants/shaders/fuseUI.frag' */
|
|
4
|
+
const fuseShaderSrcUI = "precision highp float;\nprecision highp int;\nprecision highp usampler2D;\nprecision highp sampler3D;\n\n// the lut texture is a 256x1 rgba texture for each channel\nuniform sampler2D lutSampler;\n\nuniform vec2 lutMinMax;\n\n// src texture is the raw volume intensity data\nuniform usampler2D srcTexture;\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n uint intensity = texelFetch(srcTexture, vUv, 0).r;\n float ilookup = float(float(intensity) - lutMinMax.x) / float(lutMinMax.y - lutMinMax.x);\n // apply lut to intensity:\n vec4 pix = texture(lutSampler, vec2(ilookup, 0.5));\n gl_FragColor = vec4(pix.xyz*pix.w, pix.w);\n}\n";
|
|
5
|
+
/* babel-plugin-inline-import './constants/shaders/fuseF.frag' */
|
|
6
|
+
const fuseShaderSrcF = "precision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\n\n// the lut texture is a 256x1 rgba texture for each channel\nuniform sampler2D lutSampler;\n\nuniform vec2 lutMinMax;\n\n// src texture is the raw volume intensity data\nuniform sampler2D srcTexture;\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n\n // load from channel\n float intensity = texelFetch(srcTexture, vUv, 0).r;\n\n float ilookup = float(float(intensity) - lutMinMax.x) / float(lutMinMax.y - lutMinMax.x);\n // apply lut to intensity:\n vec4 pix = texture(lutSampler, vec2(ilookup, 0.5));\n gl_FragColor = vec4(pix.xyz*pix.w, pix.w);\n}\n";
|
|
7
|
+
/* babel-plugin-inline-import './constants/shaders/fuseI.frag' */
|
|
8
|
+
const fuseShaderSrcI = "precision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\n\n// the lut texture is a 256x1 rgba texture for each channel\nuniform sampler2D lutSampler;\n\nuniform vec2 lutMinMax;\n\n// src texture is the raw volume intensity data\nuniform isampler2D srcTexture;\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n int intensity = texelFetch(srcTexture, vUv, 0).r;\n float ilookup = float(float(intensity) - lutMinMax.x) / float(lutMinMax.y - lutMinMax.x);\n // apply lut to intensity:\n vec4 pix = texture(lutSampler, vec2(ilookup, 0.5));\n gl_FragColor = vec4(pix.xyz*pix.w, pix.w);\n}\n";
|
|
9
|
+
// This is the owner of the fused RGBA volume texture atlas, and the mask texture atlas.
|
|
10
|
+
// This module is responsible for updating the fused texture, given the read-only volume channel data.
|
|
11
|
+
export default class FusedChannelData {
|
|
12
|
+
constructor(atlasX, atlasY) {
|
|
13
|
+
// allow for resizing
|
|
14
|
+
this.width = atlasX;
|
|
15
|
+
this.height = atlasY;
|
|
16
|
+
this.maskTexture = new DataTexture(new Uint8ClampedArray(this.width * this.height).fill(255), this.width, this.height, RedFormat, UnsignedByteType);
|
|
17
|
+
this.maskTexture.generateMipmaps = false;
|
|
18
|
+
this.maskTexture.magFilter = LinearFilter;
|
|
19
|
+
this.maskTexture.minFilter = LinearFilter;
|
|
20
|
+
this.maskTexture.wrapS = ClampToEdgeWrapping;
|
|
21
|
+
this.maskTexture.wrapT = ClampToEdgeWrapping;
|
|
22
|
+
// for single-channel tightly packed array data:
|
|
23
|
+
this.maskTexture.unpackAlignment = 1;
|
|
24
|
+
this.fuseRequested = null;
|
|
25
|
+
this.channelsDataToFuse = [];
|
|
26
|
+
this.fuseScene = new Scene();
|
|
27
|
+
this.quadCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
28
|
+
this.fuseRenderTarget = new WebGLRenderTarget(this.width, this.height, {
|
|
29
|
+
minFilter: LinearFilter,
|
|
30
|
+
magFilter: LinearFilter,
|
|
31
|
+
format: RGBAFormat,
|
|
32
|
+
type: UnsignedByteType,
|
|
33
|
+
// FloatType ?
|
|
34
|
+
depthBuffer: false,
|
|
35
|
+
stencilBuffer: false,
|
|
36
|
+
generateMipmaps: false,
|
|
37
|
+
wrapS: ClampToEdgeWrapping,
|
|
38
|
+
wrapT: ClampToEdgeWrapping
|
|
39
|
+
});
|
|
40
|
+
this.fuseMaterialProps = {
|
|
41
|
+
vertexShader: renderToBufferVertShader,
|
|
42
|
+
depthTest: false,
|
|
43
|
+
depthWrite: false,
|
|
44
|
+
blending: CustomBlending,
|
|
45
|
+
blendSrc: OneFactor,
|
|
46
|
+
blendDst: OneFactor,
|
|
47
|
+
blendEquation: MaxEquation
|
|
48
|
+
};
|
|
49
|
+
// this exists to keep one reference alive
|
|
50
|
+
// to make sure we do not fully delete and re-create
|
|
51
|
+
// a shader every time.
|
|
52
|
+
this.fuseMaterialF = this.setupFuseMaterial(fuseShaderSrcF);
|
|
53
|
+
this.fuseMaterialUI = this.setupFuseMaterial(fuseShaderSrcUI);
|
|
54
|
+
this.fuseMaterialI = this.setupFuseMaterial(fuseShaderSrcI);
|
|
55
|
+
this.fuseMaterialF.needsUpdate = true;
|
|
56
|
+
this.fuseMaterialUI.needsUpdate = true;
|
|
57
|
+
this.fuseMaterialI.needsUpdate = true;
|
|
58
|
+
this.fuseGeometry = new PlaneGeometry(2, 2);
|
|
59
|
+
}
|
|
60
|
+
setupFuseMaterial(fragShaderSrc) {
|
|
61
|
+
return new ShaderMaterial({
|
|
62
|
+
uniforms: {
|
|
63
|
+
lutSampler: {
|
|
64
|
+
value: null
|
|
65
|
+
},
|
|
66
|
+
lutMinMax: {
|
|
67
|
+
value: new Vector2(0, 255)
|
|
68
|
+
},
|
|
69
|
+
srcTexture: {
|
|
70
|
+
value: null
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
fragmentShader: fragShaderSrc,
|
|
74
|
+
...this.fuseMaterialProps
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
getFusedTexture() {
|
|
78
|
+
return this.fuseRenderTarget.texture;
|
|
79
|
+
}
|
|
80
|
+
cleanup() {
|
|
81
|
+
this.fuseScene.clear();
|
|
82
|
+
this.maskTexture.dispose();
|
|
83
|
+
}
|
|
84
|
+
getShader(dtype) {
|
|
85
|
+
switch (dtype) {
|
|
86
|
+
case "float32":
|
|
87
|
+
return this.fuseMaterialF;
|
|
88
|
+
case "uint8":
|
|
89
|
+
case "uint16":
|
|
90
|
+
case "uint32":
|
|
91
|
+
return this.fuseMaterialUI;
|
|
92
|
+
case "int8":
|
|
93
|
+
case "int16":
|
|
94
|
+
case "int32":
|
|
95
|
+
return this.fuseMaterialI;
|
|
96
|
+
default:
|
|
97
|
+
throw new Error("Unsupported data type for fuse shader");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
fuse(combination, channels) {
|
|
101
|
+
// we can fuse if we have any loaded channels that are showing.
|
|
102
|
+
// actually, we can fuse if no channels are showing (but they are loaded), too.
|
|
103
|
+
let canFuse = false;
|
|
104
|
+
for (let i = 0; i < combination.length; ++i) {
|
|
105
|
+
const c = combination[i];
|
|
106
|
+
const idx = c.chIndex;
|
|
107
|
+
if (channels[idx].loaded) {
|
|
108
|
+
// set the lut in this fuse combination.
|
|
109
|
+
// can optimize by calling combineLuts more lazily
|
|
110
|
+
c.lut = channels[idx].combineLuts(c.rgbColor, c.lut);
|
|
111
|
+
canFuse = true;
|
|
112
|
+
//break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!canFuse) {
|
|
116
|
+
this.channelsDataToFuse = [];
|
|
117
|
+
this.fuseRequested = [];
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.fuseRequested = combination;
|
|
121
|
+
this.channelsDataToFuse = channels;
|
|
122
|
+
}
|
|
123
|
+
gpuFuse(renderer) {
|
|
124
|
+
const combination = this.fuseRequested;
|
|
125
|
+
const channels = this.channelsDataToFuse;
|
|
126
|
+
if (!combination) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// webgl draw one mesh per channel to fuse. clear texture to 0,0,0,0
|
|
131
|
+
|
|
132
|
+
this.fuseScene.traverse(node => {
|
|
133
|
+
if (node instanceof Mesh) {
|
|
134
|
+
// materials were holding references to the channel data textures
|
|
135
|
+
// causing mem leak so we must dispose before clearing the scene
|
|
136
|
+
node.material.dispose();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
this.fuseScene.clear();
|
|
140
|
+
for (let i = 0; i < combination.length; ++i) {
|
|
141
|
+
if (combination[i].rgbColor) {
|
|
142
|
+
const chIndex = combination[i].chIndex;
|
|
143
|
+
if (!channels[chIndex].loaded) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// add a draw call per channel here.
|
|
147
|
+
// must clone the material to keep a unique set of uniforms
|
|
148
|
+
const mat = this.getShader(channels[chIndex].dtype).clone();
|
|
149
|
+
mat.uniforms.lutSampler.value = channels[chIndex].lutTexture;
|
|
150
|
+
// the lut texture is spanning only the data range of the channel, not the datatype range
|
|
151
|
+
mat.uniforms.lutMinMax.value = new Vector2(channels[chIndex].rawMin, channels[chIndex].rawMax);
|
|
152
|
+
mat.uniforms.srcTexture.value = channels[chIndex].dataTexture;
|
|
153
|
+
this.fuseScene.add(new Mesh(this.fuseGeometry, mat));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (this.fuseScene.children.length > 0) {
|
|
157
|
+
renderer.setRenderTarget(this.fuseRenderTarget);
|
|
158
|
+
renderer.autoClearColor = true;
|
|
159
|
+
const prevClearColor = new Color();
|
|
160
|
+
renderer.getClearColor(prevClearColor);
|
|
161
|
+
const prevClearAlpha = renderer.getClearAlpha();
|
|
162
|
+
renderer.setClearColor(0x000000, 0);
|
|
163
|
+
renderer.render(this.fuseScene, this.quadCamera);
|
|
164
|
+
renderer.setRenderTarget(null);
|
|
165
|
+
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
166
|
+
}
|
|
167
|
+
// "dirty flag"
|
|
168
|
+
this.fuseRequested = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// currently only one channel can be selected to participate as a mask
|
|
172
|
+
setChannelAsMask(idx, channel) {
|
|
173
|
+
if (!channel || !channel.loaded) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// binarize the data
|
|
177
|
+
// (TODO consider whether it should be binarized or not?)
|
|
178
|
+
const datacopy = new Uint8ClampedArray(channel.imgData.data.length);
|
|
179
|
+
for (let i = 0; i < channel.imgData.data.length; i++) {
|
|
180
|
+
datacopy[i] = channel.imgData.data[i] > 0 ? 255 : 0;
|
|
181
|
+
}
|
|
182
|
+
const maskData = {
|
|
183
|
+
data: datacopy,
|
|
184
|
+
width: this.width,
|
|
185
|
+
height: this.height,
|
|
186
|
+
colorSpace: "srgb"
|
|
187
|
+
};
|
|
188
|
+
this.maskTexture.image = maskData;
|
|
189
|
+
this.maskTexture.needsUpdate = true;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
package/es/Histogram.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const NBINS = 256;
|
|
2
|
+
/**
|
|
3
|
+
* Builds a histogram with 256 bins from a data array. Assume data is 8 bit single channel grayscale.
|
|
4
|
+
* @class
|
|
5
|
+
* @param {Array.<number>} data
|
|
6
|
+
*/
|
|
7
|
+
export default class Histogram {
|
|
8
|
+
// no more than 2^32 pixels of any one intensity in the data!?!?!
|
|
9
|
+
|
|
10
|
+
/** Min value in the original raw data. */
|
|
11
|
+
|
|
12
|
+
/** Max value in the original raw data. */
|
|
13
|
+
|
|
14
|
+
/** Size of each histogram bin in the scale of the original data. */
|
|
15
|
+
|
|
16
|
+
/** Index of the first bin (other than 0) with at least 1 value. */
|
|
17
|
+
|
|
18
|
+
/** Index of the last bin (other than 0) with at least 1 value. */
|
|
19
|
+
|
|
20
|
+
constructor(data) {
|
|
21
|
+
this.dataMinBin = 0;
|
|
22
|
+
this.dataMaxBin = 0;
|
|
23
|
+
this.maxBin = 0;
|
|
24
|
+
this.bins = new Uint32Array();
|
|
25
|
+
this.min = 0;
|
|
26
|
+
this.max = 0;
|
|
27
|
+
this.binSize = 0;
|
|
28
|
+
|
|
29
|
+
// build up the histogram
|
|
30
|
+
const hinfo = Histogram.calculateHistogram(data, NBINS);
|
|
31
|
+
this.bins = hinfo.bins;
|
|
32
|
+
this.min = hinfo.min;
|
|
33
|
+
this.max = hinfo.max;
|
|
34
|
+
this.binSize = hinfo.binSize;
|
|
35
|
+
|
|
36
|
+
// TODO: These should always return 0 and NBINS - 1, respectively. Test if these
|
|
37
|
+
// can be removed.
|
|
38
|
+
for (let i = 0; i < this.bins.length; i++) {
|
|
39
|
+
if (this.bins[i] > 0) {
|
|
40
|
+
this.dataMinBin = i;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (let i = this.bins.length - 1; i >= 0; i--) {
|
|
45
|
+
if (this.bins[i] > 0) {
|
|
46
|
+
this.dataMaxBin = i;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.pixelCount = data.length;
|
|
51
|
+
|
|
52
|
+
// get the bin with the most frequently occurring NONZERO value
|
|
53
|
+
this.maxBin = 1;
|
|
54
|
+
let max = this.bins[1];
|
|
55
|
+
for (let i = 1; i < this.bins.length; i++) {
|
|
56
|
+
if (this.bins[i] > max) {
|
|
57
|
+
this.maxBin = i;
|
|
58
|
+
max = this.bins[i];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// return the bin index of the given data value
|
|
64
|
+
static findBin(dataValue, dataMin, binSize, numBins) {
|
|
65
|
+
let binIndex = Math.floor((dataValue - dataMin) / binSize);
|
|
66
|
+
// for values that lie exactly on last bin we need to subtract one
|
|
67
|
+
if (binIndex === numBins) {
|
|
68
|
+
binIndex--;
|
|
69
|
+
}
|
|
70
|
+
return binIndex;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// return the bin index of the given data value
|
|
74
|
+
findBinOfValue(value) {
|
|
75
|
+
return Histogram.findBin(value, this.min, this.binSize, NBINS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return the min data value
|
|
80
|
+
* @return {number}
|
|
81
|
+
*/
|
|
82
|
+
getDataMin() {
|
|
83
|
+
return this.min;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Return the max data value
|
|
88
|
+
* @return {number}
|
|
89
|
+
*/
|
|
90
|
+
getDataMax() {
|
|
91
|
+
return this.max;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns the first bin index with at least 1 value, other than the 0th bin.
|
|
96
|
+
* @return {number}
|
|
97
|
+
*/
|
|
98
|
+
getMin() {
|
|
99
|
+
return this.dataMinBin;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns the last bin index with at least 1 value, other than the 0th bin.
|
|
104
|
+
* @return {number}
|
|
105
|
+
*/
|
|
106
|
+
getMax() {
|
|
107
|
+
// Note that this will always return `NBINS - 1`.
|
|
108
|
+
return this.dataMaxBin;
|
|
109
|
+
}
|
|
110
|
+
getNumBins() {
|
|
111
|
+
return this.bins.length;
|
|
112
|
+
}
|
|
113
|
+
getBin(i) {
|
|
114
|
+
return this.bins[i];
|
|
115
|
+
}
|
|
116
|
+
getBinRange(i) {
|
|
117
|
+
return [this.min + i * this.binSize, this.min + (i + 1) * this.binSize];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Find the bin that contains the percentage of pixels below it
|
|
122
|
+
* @return {number}
|
|
123
|
+
* @param {number} pct
|
|
124
|
+
*/
|
|
125
|
+
findBinOfPercentile(pct) {
|
|
126
|
+
const limit = this.pixelCount * pct;
|
|
127
|
+
let i = 0;
|
|
128
|
+
let count = 0;
|
|
129
|
+
for (i = 0; i < this.bins.length; ++i) {
|
|
130
|
+
count += this.bins[i];
|
|
131
|
+
if (count > limit) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return i;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Find bins at 10th / 90th percentile
|
|
139
|
+
findBestFitBins() {
|
|
140
|
+
const pixcount = this.pixelCount;
|
|
141
|
+
//const pixcount = this.imgData.data.length;
|
|
142
|
+
const limit = pixcount / 10;
|
|
143
|
+
let i = 0;
|
|
144
|
+
let count = 0;
|
|
145
|
+
for (i = 1; i < this.bins.length; ++i) {
|
|
146
|
+
count += this.bins[i];
|
|
147
|
+
if (count > limit) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const hmin = i;
|
|
152
|
+
count = 0;
|
|
153
|
+
for (i = this.bins.length - 1; i >= 1; --i) {
|
|
154
|
+
count += this.bins[i];
|
|
155
|
+
if (count > limit) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const hmax = i;
|
|
160
|
+
return [hmin, hmax];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Find min and max bins attempting to replicate ImageJ's "Auto" button
|
|
164
|
+
findAutoIJBins() {
|
|
165
|
+
// note that consecutive applications of this should modify the auto threshold. see:
|
|
166
|
+
// https://github.com/imagej/ImageJ/blob/7746fcb0f5744a7a7758244c5dcd2193459e6e0e/ij/plugin/frame/ContrastAdjuster.java#L816
|
|
167
|
+
const AUTO_THRESHOLD = 5000;
|
|
168
|
+
const pixcount = this.pixelCount;
|
|
169
|
+
// const pixcount = this.imgData.data.length;
|
|
170
|
+
const limit = pixcount / 10;
|
|
171
|
+
const threshold = pixcount / AUTO_THRESHOLD;
|
|
172
|
+
|
|
173
|
+
// this will skip the "zero" bin which contains pixels of zero intensity.
|
|
174
|
+
let hmin = this.bins.length - 1;
|
|
175
|
+
let hmax = 1;
|
|
176
|
+
for (let i = 1; i < this.bins.length; ++i) {
|
|
177
|
+
if (this.bins[i] > threshold && this.bins[i] <= limit) {
|
|
178
|
+
hmin = i;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (let i = this.bins.length - 1; i >= 1; --i) {
|
|
183
|
+
if (this.bins[i] > threshold && this.bins[i] <= limit) {
|
|
184
|
+
hmax = i;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (hmax < hmin) {
|
|
189
|
+
hmin = 0;
|
|
190
|
+
hmax = 255;
|
|
191
|
+
}
|
|
192
|
+
return [hmin, hmax];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Find min and max bins using a percentile of the most commonly occurring value
|
|
196
|
+
findAutoMinMax() {
|
|
197
|
+
// simple linear mapping cutting elements with small appearence
|
|
198
|
+
// get 10% threshold
|
|
199
|
+
const PERCENTAGE = 0.1;
|
|
200
|
+
const th = Math.floor(this.bins[this.maxBin] * PERCENTAGE);
|
|
201
|
+
let b = 0;
|
|
202
|
+
let e = this.bins.length - 1;
|
|
203
|
+
for (let x = 1; x < this.bins.length; ++x) {
|
|
204
|
+
if (this.bins[x] > th) {
|
|
205
|
+
b = x;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (let x = this.bins.length - 1; x >= 1; --x) {
|
|
210
|
+
if (this.bins[x] > th) {
|
|
211
|
+
e = x;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return [b, e];
|
|
216
|
+
}
|
|
217
|
+
static calculateHistogram(arr, numBins = 1) {
|
|
218
|
+
if (numBins < 1) {
|
|
219
|
+
numBins = 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// calculate min and max of arr
|
|
223
|
+
// TODO See convertChannel, which will also compute min and max!
|
|
224
|
+
// We could save a whole extra loop over the data, or have convertChannel compute the whole histogram.
|
|
225
|
+
// need to be careful about computing over chunks or whole ready-to-display volume
|
|
226
|
+
|
|
227
|
+
let min = arr[0];
|
|
228
|
+
let max = arr[0];
|
|
229
|
+
for (let i = 1; i < arr.length; i++) {
|
|
230
|
+
if (arr[i] < min) {
|
|
231
|
+
min = arr[i];
|
|
232
|
+
} else if (arr[i] > max) {
|
|
233
|
+
max = arr[i];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const bins = new Uint32Array(numBins).fill(0);
|
|
237
|
+
const binSize = (max - min) / numBins === 0 ? 1 : (max - min) / numBins;
|
|
238
|
+
for (let i = 0; i < arr.length; i++) {
|
|
239
|
+
const item = arr[i];
|
|
240
|
+
const binIndex = Histogram.findBin(item, min, binSize, numBins);
|
|
241
|
+
bins[binIndex]++;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
bins,
|
|
245
|
+
min,
|
|
246
|
+
max,
|
|
247
|
+
binSize
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
package/es/ImageInfo.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { volumeSize, physicalPixelSize } from "./VolumeDims.js";
|
|
2
|
+
import { Vector3, Vector2 } from "three";
|
|
3
|
+
export function defaultImageInfo() {
|
|
4
|
+
return {
|
|
5
|
+
name: "",
|
|
6
|
+
atlasTileDims: [1, 1],
|
|
7
|
+
subregionSize: [1, 1, 1],
|
|
8
|
+
subregionOffset: [0, 0, 0],
|
|
9
|
+
combinedNumChannels: 1,
|
|
10
|
+
channelNames: ["0"],
|
|
11
|
+
channelColors: [[255, 255, 255]],
|
|
12
|
+
multiscaleLevel: 0,
|
|
13
|
+
multiscaleLevelDims: [{
|
|
14
|
+
shape: [1, 1, 1, 1, 1],
|
|
15
|
+
spacing: [1, 1, 1, 1, 1],
|
|
16
|
+
spaceUnit: "",
|
|
17
|
+
timeUnit: "",
|
|
18
|
+
dataType: "uint8"
|
|
19
|
+
}],
|
|
20
|
+
transform: {
|
|
21
|
+
translation: [0, 0, 0],
|
|
22
|
+
rotation: [0, 0, 0],
|
|
23
|
+
scale: [1, 1, 1]
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export class CImageInfo {
|
|
28
|
+
constructor(imageInfo) {
|
|
29
|
+
this.imageInfo = imageInfo || defaultImageInfo();
|
|
30
|
+
}
|
|
31
|
+
get currentLevelDims() {
|
|
32
|
+
return this.imageInfo.multiscaleLevelDims[this.imageInfo.multiscaleLevel];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Number of channels in the image */
|
|
36
|
+
get numChannels() {
|
|
37
|
+
return this.imageInfo.combinedNumChannels;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** XYZ size of the *original* (not downsampled) volume, in pixels */
|
|
41
|
+
get originalSize() {
|
|
42
|
+
return volumeSize(this.imageInfo.multiscaleLevelDims[0]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Size of the volume, in pixels */
|
|
46
|
+
get volumeSize() {
|
|
47
|
+
return volumeSize(this.currentLevelDims);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Size of a single *original* (not downsampled) pixel, in spatial units */
|
|
51
|
+
get physicalPixelSize() {
|
|
52
|
+
return physicalPixelSize(this.imageInfo.multiscaleLevelDims[0]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Symbol of physical spatial unit used by `physicalPixelSize` */
|
|
56
|
+
get spatialUnit() {
|
|
57
|
+
return this.imageInfo.multiscaleLevelDims[0].spaceUnit;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Number of timesteps in the time series, or 1 if the image is not a time series */
|
|
61
|
+
get times() {
|
|
62
|
+
// 0 is T
|
|
63
|
+
return this.currentLevelDims.shape[0];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Size of each timestep in temporal units */
|
|
67
|
+
get timeScale() {
|
|
68
|
+
// 0 is T
|
|
69
|
+
return this.currentLevelDims.spacing[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Symbol of physical time unit used by `timeScale` */
|
|
73
|
+
get timeUnit() {
|
|
74
|
+
return this.currentLevelDims.timeUnit;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Number of scale levels available for this volume */
|
|
78
|
+
get numMultiscaleLevels() {
|
|
79
|
+
return this.imageInfo.multiscaleLevelDims.length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The names of each channel */
|
|
83
|
+
get channelNames() {
|
|
84
|
+
return this.imageInfo.channelNames;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Optional overrides to default channel colors, in 0-255 range */
|
|
88
|
+
get channelColors() {
|
|
89
|
+
return this.imageInfo.channelColors;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Size of the currently loaded subregion, in pixels */
|
|
93
|
+
get subregionSize() {
|
|
94
|
+
return new Vector3(...this.imageInfo.subregionSize);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Offset of the loaded subregion into the total volume, in pixels */
|
|
98
|
+
get subregionOffset() {
|
|
99
|
+
return new Vector3(...this.imageInfo.subregionOffset);
|
|
100
|
+
}
|
|
101
|
+
get multiscaleLevel() {
|
|
102
|
+
return this.imageInfo.multiscaleLevel;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* XY dimensions of the texture atlas used by `RayMarchedAtlasVolume` and `Atlas2DSlice`, in number of z-slice
|
|
107
|
+
* tiles (not pixels). Chosen by the loader to lay out the 3D volume in the squarest possible 2D texture atlas.
|
|
108
|
+
*/
|
|
109
|
+
get atlasTileDims() {
|
|
110
|
+
return new Vector2(...this.imageInfo.atlasTileDims);
|
|
111
|
+
}
|
|
112
|
+
get transform() {
|
|
113
|
+
return {
|
|
114
|
+
translation: new Vector3(...this.imageInfo.transform.translation),
|
|
115
|
+
rotation: new Vector3(...this.imageInfo.transform.rotation),
|
|
116
|
+
scale: new Vector3(...this.imageInfo.transform.scale)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function computeAtlasSize(imageInfo) {
|
|
121
|
+
const {
|
|
122
|
+
atlasTileDims
|
|
123
|
+
} = imageInfo;
|
|
124
|
+
const volDims = imageInfo.multiscaleLevelDims[imageInfo.multiscaleLevel];
|
|
125
|
+
// TCZYX: 4 = x, 3 = y
|
|
126
|
+
return [atlasTileDims[0] * volDims.shape[4], atlasTileDims[1] * volDims.shape[3]];
|
|
127
|
+
}
|
package/es/Light.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Vector3 } from "three";
|
|
2
|
+
export const AREA_LIGHT = 0;
|
|
3
|
+
export const SKY_LIGHT = 1;
|
|
4
|
+
export class Light {
|
|
5
|
+
// type = 1 for sky light, 0 for area light
|
|
6
|
+
constructor(type) {
|
|
7
|
+
this.mTheta = 14 * Math.PI / 180.0;
|
|
8
|
+
this.mPhi = 54 * Math.PI / 180.0;
|
|
9
|
+
this.mWidth = 1.0;
|
|
10
|
+
this.mHeight = 1.0;
|
|
11
|
+
this.mHalfWidth = 0.5 * this.mWidth;
|
|
12
|
+
this.mHalfHeight = 0.5 * this.mHeight;
|
|
13
|
+
this.mDistance = 4.0;
|
|
14
|
+
this.mSkyRadius = 1000.0;
|
|
15
|
+
this.mP = new Vector3();
|
|
16
|
+
this.mTarget = new Vector3();
|
|
17
|
+
this.mArea = 1.0;
|
|
18
|
+
this.mAreaPdf = 1.0 / this.mArea;
|
|
19
|
+
this.mColor = new Vector3(75, 75, 75);
|
|
20
|
+
this.mColorTop = new Vector3(0.3, 0.3, 0.3);
|
|
21
|
+
this.mColorMiddle = new Vector3(0.3, 0.3, 0.3);
|
|
22
|
+
this.mColorBottom = new Vector3(0.3, 0.3, 0.3);
|
|
23
|
+
|
|
24
|
+
// type = 1 for sky light, 0 for area light
|
|
25
|
+
this.mT = type;
|
|
26
|
+
|
|
27
|
+
// secondary properties:
|
|
28
|
+
this.mN = new Vector3(0, 0, 1);
|
|
29
|
+
this.mU = new Vector3(1, 0, 0);
|
|
30
|
+
this.mV = new Vector3(0, 1, 0);
|
|
31
|
+
this.update(new Vector3(0, 0, 0));
|
|
32
|
+
}
|
|
33
|
+
update(targetPoint, cameraMatrix) {
|
|
34
|
+
this.mHalfWidth = 0.5 * this.mWidth;
|
|
35
|
+
this.mHalfHeight = 0.5 * this.mHeight;
|
|
36
|
+
this.mTarget.copy(targetPoint);
|
|
37
|
+
|
|
38
|
+
// Determine light position
|
|
39
|
+
this.mP.x = this.mDistance * Math.sin(this.mPhi) * Math.sin(this.mTheta);
|
|
40
|
+
this.mP.z = this.mDistance * Math.sin(this.mPhi) * Math.cos(this.mTheta);
|
|
41
|
+
this.mP.y = this.mDistance * Math.cos(this.mPhi);
|
|
42
|
+
this.mP.add(this.mTarget);
|
|
43
|
+
if (cameraMatrix) {
|
|
44
|
+
// We want to treat the lights as positioned relative to the camera, so camera rotations should not move them.
|
|
45
|
+
// In other words, when we rotate the camera, it should seem like we are tumbling the volume under fixed lighting.
|
|
46
|
+
this.mP.applyMatrix4(cameraMatrix);
|
|
47
|
+
this.mTarget.applyMatrix4(cameraMatrix);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Determine area
|
|
51
|
+
if (this.mT === AREA_LIGHT) {
|
|
52
|
+
this.mArea = this.mWidth * this.mHeight;
|
|
53
|
+
this.mAreaPdf = 1.0 / this.mArea;
|
|
54
|
+
} else if (this.mT === SKY_LIGHT) {
|
|
55
|
+
this.mP.copy(targetPoint);
|
|
56
|
+
// shift by nonzero amount
|
|
57
|
+
this.mTarget.addVectors(this.mP, new Vector3(0.0, 0.0, 1.0));
|
|
58
|
+
this.mSkyRadius = 1000.0 * targetPoint.length() * 2.0;
|
|
59
|
+
this.mArea = 4.0 * Math.PI * Math.pow(this.mSkyRadius, 2.0);
|
|
60
|
+
this.mAreaPdf = 1.0 / this.mArea;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Compute orthogonal basis frame
|
|
64
|
+
this.mN.subVectors(this.mTarget, this.mP).normalize();
|
|
65
|
+
// if N and "up" are parallel, then just choose a different "up"
|
|
66
|
+
if (this.mN.y === 1.0 || this.mN.y === -1.0) {
|
|
67
|
+
this.mU.crossVectors(this.mN, new Vector3(1.0, 0.0, 0.0)).normalize();
|
|
68
|
+
} else {
|
|
69
|
+
// standard "up" vector
|
|
70
|
+
this.mU.crossVectors(this.mN, new Vector3(0.0, 1.0, 0.0)).normalize();
|
|
71
|
+
}
|
|
72
|
+
this.mV.crossVectors(this.mN, this.mU).normalize();
|
|
73
|
+
}
|
|
74
|
+
}
|