@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
package/es/Volume.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { Vector3 } from "three";
|
|
2
|
+
import Channel from "./Channel.js";
|
|
3
|
+
import { getColorByChannelIndex } from "./constants/colors.js";
|
|
4
|
+
import { LoadSpec } from "./loaders/IVolumeLoader.js";
|
|
5
|
+
import { MAX_ATLAS_EDGE, pickLevelToLoadUnscaled } from "./loaders/VolumeLoaderUtils.js";
|
|
6
|
+
import { CImageInfo, defaultImageInfo } from "./ImageInfo.js";
|
|
7
|
+
/**
|
|
8
|
+
* A renderable multichannel volume image with 8-bits per channel intensity values.
|
|
9
|
+
* @class
|
|
10
|
+
* @param {ImageInfo} imageInfo
|
|
11
|
+
*/
|
|
12
|
+
export default class Volume {
|
|
13
|
+
/** `LoadSpec` representing the minimum data required to display what's in the viewer (subregion, channels, etc.).
|
|
14
|
+
* Used to intelligently issue load requests whenever required by a state change. Modify with `updateRequiredData`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The maximum of the measurements of 3 axes in physical units (pixels*physicalSize) */
|
|
18
|
+
|
|
19
|
+
/** The physical size of a voxel in the original level 0 volume */
|
|
20
|
+
|
|
21
|
+
/** The physical dims of the whole volume (not accounting for subregion) */
|
|
22
|
+
|
|
23
|
+
/** Normalized physical size of the whole volume (not accounting for subregion) */
|
|
24
|
+
|
|
25
|
+
constructor(imageInfo = defaultImageInfo(), loadSpec = new LoadSpec(), loader) {
|
|
26
|
+
this.loaded = false;
|
|
27
|
+
this.imageInfo = new CImageInfo(imageInfo);
|
|
28
|
+
// TODO: use getter?
|
|
29
|
+
this.name = imageInfo.name;
|
|
30
|
+
this.loadSpec = {
|
|
31
|
+
// Fill in defaults for optional properties
|
|
32
|
+
multiscaleLevel: 0,
|
|
33
|
+
scaleLevelBias: 0,
|
|
34
|
+
maxAtlasEdge: MAX_ATLAS_EDGE,
|
|
35
|
+
channels: Array.from({
|
|
36
|
+
length: this.imageInfo.numChannels
|
|
37
|
+
}, (_val, idx) => idx),
|
|
38
|
+
...loadSpec
|
|
39
|
+
};
|
|
40
|
+
this.loadSpecRequired = {
|
|
41
|
+
...this.loadSpec,
|
|
42
|
+
channels: this.loadSpec.channels.slice(),
|
|
43
|
+
subregion: this.loadSpec.subregion.clone()
|
|
44
|
+
};
|
|
45
|
+
this.loader = loader;
|
|
46
|
+
// imageMetadata to be filled in by Volume Loaders
|
|
47
|
+
this.imageMetadata = {};
|
|
48
|
+
this.normRegionSize = new Vector3(1, 1, 1);
|
|
49
|
+
this.normRegionOffset = new Vector3(0, 0, 0);
|
|
50
|
+
this.physicalSize = new Vector3(1, 1, 1);
|
|
51
|
+
this.physicalScale = 1;
|
|
52
|
+
this.normPhysicalSize = new Vector3(1, 1, 1);
|
|
53
|
+
this.physicalPixelSize = this.imageInfo.physicalPixelSize;
|
|
54
|
+
this.tickMarkPhysicalLength = 1;
|
|
55
|
+
this.setVoxelSize(this.physicalPixelSize);
|
|
56
|
+
this.numChannels = this.imageInfo.numChannels;
|
|
57
|
+
this.channelNames = this.imageInfo.channelNames.slice();
|
|
58
|
+
this.channelColorsDefault = this.imageInfo.channelColors ? this.imageInfo.channelColors.slice() : this.channelNames.map((name, index) => getColorByChannelIndex(index));
|
|
59
|
+
// fill in gaps
|
|
60
|
+
if (this.channelColorsDefault.length < this.imageInfo.numChannels) {
|
|
61
|
+
for (let i = this.channelColorsDefault.length - 1; i < this.imageInfo.numChannels; ++i) {
|
|
62
|
+
this.channelColorsDefault[i] = getColorByChannelIndex(i);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.channels = [];
|
|
66
|
+
for (let i = 0; i < this.imageInfo.numChannels; ++i) {
|
|
67
|
+
const channel = new Channel(this.channelNames[i]);
|
|
68
|
+
this.channels.push(channel);
|
|
69
|
+
// TODO pass in channel constructor...
|
|
70
|
+
channel.dims = this.imageInfo.subregionSize.toArray();
|
|
71
|
+
}
|
|
72
|
+
this.physicalUnitSymbol = this.imageInfo.spatialUnit;
|
|
73
|
+
this.volumeDataObservers = [];
|
|
74
|
+
}
|
|
75
|
+
setUnloaded() {
|
|
76
|
+
this.loaded = false;
|
|
77
|
+
this.channels.forEach(channel => {
|
|
78
|
+
channel.loaded = false;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
isLoaded() {
|
|
82
|
+
return this.loaded;
|
|
83
|
+
}
|
|
84
|
+
updateDimensions() {
|
|
85
|
+
const {
|
|
86
|
+
volumeSize,
|
|
87
|
+
subregionSize,
|
|
88
|
+
subregionOffset
|
|
89
|
+
} = this.imageInfo;
|
|
90
|
+
this.setVoxelSize(this.physicalPixelSize);
|
|
91
|
+
this.normRegionSize = subregionSize.clone().divide(volumeSize);
|
|
92
|
+
this.normRegionOffset = subregionOffset.clone().divide(volumeSize);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Returns `true` iff differences between `loadSpec` and `loadSpecRequired` indicate new data *must* be loaded. */
|
|
96
|
+
mustLoadNewData() {
|
|
97
|
+
return this.loadSpec.useExplicitLevel !== this.loadSpecRequired.useExplicitLevel ||
|
|
98
|
+
// explicit vs automatic level changed
|
|
99
|
+
this.loadSpec.time !== this.loadSpecRequired.time ||
|
|
100
|
+
// time point changed
|
|
101
|
+
!this.loadSpec.subregion.containsBox(this.loadSpecRequired.subregion) ||
|
|
102
|
+
// new subregion not contained in old
|
|
103
|
+
this.loadSpecRequired.channels.some(channel => !this.loadSpec.channels.includes(channel)) // new channel(s)
|
|
104
|
+
;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns `true` iff differences between `loadSpec` and `loadSpecRequired` indicate a new load *may* get a
|
|
109
|
+
* different scale level than is currently loaded.
|
|
110
|
+
*
|
|
111
|
+
* This checks for changes in properties that *can*, but do not *always*, change the scale level the loader picks.
|
|
112
|
+
* For example, a smaller `subregion` *may* mean a higher scale level will fit within memory constraints, or it may
|
|
113
|
+
* not. A higher `scaleLevelBias` *may* nudge the volume into a higher scale level, or we may already be at the max
|
|
114
|
+
* imposed by `multiscaleLevel`.
|
|
115
|
+
*/
|
|
116
|
+
mayLoadNewScaleLevel() {
|
|
117
|
+
return !this.loadSpec.subregion.equals(this.loadSpecRequired.subregion) || this.loadSpecRequired.maxAtlasEdge !== this.loadSpec.maxAtlasEdge || this.loadSpecRequired.multiscaleLevel !== this.loadSpec.multiscaleLevel || this.loadSpecRequired.scaleLevelBias !== this.loadSpec.scaleLevelBias;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Call on any state update that may require new data to be loaded (subregion, enabled channels, time, etc.) */
|
|
121
|
+
async updateRequiredData(required, onChannelLoaded) {
|
|
122
|
+
this.loadSpecRequired = {
|
|
123
|
+
...this.loadSpecRequired,
|
|
124
|
+
...required
|
|
125
|
+
};
|
|
126
|
+
let shouldReload = this.mustLoadNewData();
|
|
127
|
+
|
|
128
|
+
// If we're not reloading due to required data changes, check if we should load a new scale level
|
|
129
|
+
if (!shouldReload && this.mayLoadNewScaleLevel()) {
|
|
130
|
+
// Loaders should cache loaded dimensions so that this call blocks no more than once per valid `LoadSpec`.
|
|
131
|
+
const dims = await this.loadScaleLevelDims();
|
|
132
|
+
if (dims) {
|
|
133
|
+
const dimsZYX = dims.map(({
|
|
134
|
+
shape
|
|
135
|
+
}) => [shape[2], shape[3], shape[4]]);
|
|
136
|
+
// Determine which scale level *would* be loaded, and see if it's different than what we have
|
|
137
|
+
const levelToLoad = pickLevelToLoadUnscaled(this.loadSpecRequired, dimsZYX);
|
|
138
|
+
shouldReload = this.imageInfo.multiscaleLevel !== levelToLoad;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (shouldReload) {
|
|
142
|
+
this.loadNewData(onChannelLoaded);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async loadScaleLevelDims() {
|
|
146
|
+
try {
|
|
147
|
+
return await this.loader?.loadDims(this.loadSpecRequired);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
this.volumeDataObservers.forEach(observer => observer.onVolumeLoadError(this, e));
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Loads new data as specified in `this.loadSpecRequired`. Clones `loadSpecRequired` into `loadSpec` to indicate
|
|
156
|
+
* that the data that *must* be loaded is now the data that *has* been loaded.
|
|
157
|
+
*/
|
|
158
|
+
async loadNewData(onChannelLoaded) {
|
|
159
|
+
this.setUnloaded();
|
|
160
|
+
this.loadSpec = {
|
|
161
|
+
...this.loadSpecRequired,
|
|
162
|
+
subregion: this.loadSpecRequired.subregion.clone()
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
await this.loader?.loadVolumeData(this, undefined, onChannelLoaded);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
this.volumeDataObservers.forEach(observer => observer.onVolumeLoadError(this, e));
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// we calculate the physical size of the volume (voxels*pixel_size)
|
|
173
|
+
// and then normalize to the max physical dimension
|
|
174
|
+
// NOTE: This function MUST be called to set up some important dimensional info
|
|
175
|
+
setVoxelSize(size) {
|
|
176
|
+
// only set the data if it is > 0. zero is not an allowed value.
|
|
177
|
+
size.x = size.x > 0 ? size.x : 1.0;
|
|
178
|
+
size.y = size.y > 0 ? size.y : 1.0;
|
|
179
|
+
size.z = size.z > 0 ? size.z : 1.0;
|
|
180
|
+
this.physicalPixelSize = size;
|
|
181
|
+
this.physicalSize = this.imageInfo.originalSize.clone().multiply(this.physicalPixelSize);
|
|
182
|
+
// Volume is scaled such that its largest physical dimension is 1 world unit - save that dimension for conversions
|
|
183
|
+
this.physicalScale = Math.max(this.physicalSize.x, this.physicalSize.y, this.physicalSize.z);
|
|
184
|
+
// Compute the volume's max extent - scaled to max dimension.
|
|
185
|
+
this.normPhysicalSize = this.physicalSize.clone().divideScalar(this.physicalScale);
|
|
186
|
+
// While we're here, pick a power of 10 that divides into our max dimension a reasonable number of times
|
|
187
|
+
// and save it to be the length of tick marks in 3d.
|
|
188
|
+
this.tickMarkPhysicalLength = 10 ** Math.floor(Math.log10(this.physicalScale / 2));
|
|
189
|
+
}
|
|
190
|
+
setUnitSymbol(symbol) {
|
|
191
|
+
this.physicalUnitSymbol = symbol;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Computes the center of the volume subset */
|
|
195
|
+
getContentCenter() {
|
|
196
|
+
// center point: (normRegionSize / 2 + normRegionOffset - 0.5) * normPhysicalSize;
|
|
197
|
+
return this.normRegionSize.clone().divideScalar(2).add(this.normRegionOffset).subScalar(0.5).multiply(this.normPhysicalSize);
|
|
198
|
+
}
|
|
199
|
+
cleanup() {
|
|
200
|
+
// no op
|
|
201
|
+
}
|
|
202
|
+
getChannel(channelIndex) {
|
|
203
|
+
return this.channels[channelIndex];
|
|
204
|
+
}
|
|
205
|
+
onChannelLoaded(batch) {
|
|
206
|
+
// check to see if all channels are now loaded, and fire an event(?)
|
|
207
|
+
if (this.loadSpec.channels.every(channelIndex => this.channels[channelIndex].loaded)) {
|
|
208
|
+
this.loaded = true;
|
|
209
|
+
}
|
|
210
|
+
batch.forEach(channelIndex => this.channelLoadCallback?.(this, channelIndex));
|
|
211
|
+
this.volumeDataObservers.forEach(observer => observer.onVolumeData(this, batch));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Assign volume data via a 2d array containing the z slices as tiles across it. Assumes that the incoming data is consistent with the image's pre-existing imageInfo tile metadata.
|
|
216
|
+
* @param {number} channelIndex
|
|
217
|
+
* @param {Uint8Array} atlasdata
|
|
218
|
+
* @param {number} atlaswidth
|
|
219
|
+
* @param {number} atlasheight
|
|
220
|
+
*/
|
|
221
|
+
setChannelDataFromAtlas(channelIndex, atlasdata, atlaswidth, atlasheight, range, dtype = "uint8") {
|
|
222
|
+
this.channels[channelIndex].setFromAtlas(atlasdata, atlaswidth, atlasheight, dtype, range[0], range[1], this.imageInfo.subregionSize);
|
|
223
|
+
this.onChannelLoaded([channelIndex]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ASSUMES that this.channelData.options is already set and incoming data is consistent with it
|
|
227
|
+
/**
|
|
228
|
+
* Assign volume data as a 3d array ordered x,y,z. The xy size must be equal to tilewidth*tileheight from the imageInfo used to construct this Volume. Assumes that the incoming data is consistent with the image's pre-existing imageInfo tile metadata.
|
|
229
|
+
* @param {number} channelIndex
|
|
230
|
+
* @param {Uint8Array} volumeData
|
|
231
|
+
*/
|
|
232
|
+
setChannelDataFromVolume(channelIndex, volumeData, range, dtype = "uint8") {
|
|
233
|
+
const {
|
|
234
|
+
subregionSize,
|
|
235
|
+
atlasTileDims
|
|
236
|
+
} = this.imageInfo;
|
|
237
|
+
this.channels[channelIndex].setFromVolumeData(volumeData, subregionSize.x, subregionSize.y, subregionSize.z, atlasTileDims.x * subregionSize.x, atlasTileDims.y * subregionSize.y, range[0], range[1], dtype);
|
|
238
|
+
this.onChannelLoaded([channelIndex]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// TODO: decide if this should update imageInfo or not. For now, leave imageInfo alone as the "original" data
|
|
242
|
+
/**
|
|
243
|
+
* Add a new channel ready to receive data from one of the setChannelDataFrom* calls.
|
|
244
|
+
* Name and color will be defaulted if not provided. For now, leave imageInfo alone as the "original" data
|
|
245
|
+
* @param {string} name
|
|
246
|
+
* @param {Array.<number>} color [r,g,b]
|
|
247
|
+
*/
|
|
248
|
+
appendEmptyChannel(name, color) {
|
|
249
|
+
const idx = this.imageInfo.numChannels;
|
|
250
|
+
const chname = name || "channel_" + idx;
|
|
251
|
+
const chcolor = color || getColorByChannelIndex(idx);
|
|
252
|
+
this.numChannels += 1;
|
|
253
|
+
this.channelNames.push(chname);
|
|
254
|
+
this.channelColorsDefault.push(chcolor);
|
|
255
|
+
this.channels.push(new Channel(chname));
|
|
256
|
+
for (let i = 0; i < this.volumeDataObservers.length; ++i) {
|
|
257
|
+
this.volumeDataObservers[i].onVolumeChannelAdded(this, idx);
|
|
258
|
+
}
|
|
259
|
+
return idx;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get a value from the volume data
|
|
264
|
+
* @return {number} the intensity value from the given channel at the given xyz location
|
|
265
|
+
* @param {number} c The channel index
|
|
266
|
+
* @param {number} x
|
|
267
|
+
* @param {number} y
|
|
268
|
+
* @param {number} z
|
|
269
|
+
*/
|
|
270
|
+
getIntensity(c, x, y, z) {
|
|
271
|
+
return this.channels[c].getIntensity(x, y, z);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get the 256-bin histogram for the given channel
|
|
276
|
+
* @return {Histogram} the histogram
|
|
277
|
+
* @param {number} c The channel index
|
|
278
|
+
*/
|
|
279
|
+
getHistogram(c) {
|
|
280
|
+
return this.channels[c].getHistogram();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Set the lut for the given channel
|
|
285
|
+
* @param {number} c The channel index
|
|
286
|
+
* @param {Array.<number>} lut The lut as a 256 element array
|
|
287
|
+
*/
|
|
288
|
+
setLut(c, lut) {
|
|
289
|
+
this.channels[c].setLut(lut);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Set the color palette for the given channel
|
|
294
|
+
* @param {number} c The channel index
|
|
295
|
+
* @param {Array.<number>} palette The colors as a 256 element array * RGBA
|
|
296
|
+
*/
|
|
297
|
+
setColorPalette(c, palette) {
|
|
298
|
+
this.channels[c].setColorPalette(palette);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Set the color palette alpha multiplier for the given channel.
|
|
303
|
+
* This will blend between the ordinary color lut and this colorPalette lut.
|
|
304
|
+
* @param {number} c The channel index
|
|
305
|
+
* @param {number} alpha The alpha value as a number from 0 to 1
|
|
306
|
+
*/
|
|
307
|
+
setColorPaletteAlpha(c, alpha) {
|
|
308
|
+
this.channels[c].setColorPaletteAlpha(alpha);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Return the intrinsic rotation associated with this volume (radians)
|
|
313
|
+
* @return {Array.<number>} the xyz Euler angles (radians)
|
|
314
|
+
*/
|
|
315
|
+
getRotation() {
|
|
316
|
+
// default axis order is XYZ
|
|
317
|
+
return this.imageInfo.transform.rotation.toArray();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Return the intrinsic translation (pivot center delta) associated with this volume, in normalized volume units
|
|
322
|
+
* @return {Array.<number>} the xyz translation in normalized volume units
|
|
323
|
+
*/
|
|
324
|
+
getTranslation() {
|
|
325
|
+
return this.voxelsToWorldSpace(this.imageInfo.transform.translation.toArray());
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Return a translation in normalized volume units, given a translation in image voxels
|
|
330
|
+
* @return {Array.<number>} the xyz translation in normalized volume units
|
|
331
|
+
*/
|
|
332
|
+
voxelsToWorldSpace(xyz) {
|
|
333
|
+
// ASSUME: xyz is in original (level 0) image voxels, compatible with physicalPixelSize.
|
|
334
|
+
// account for pixel_size and normalized scaling in the threejs volume representation we're using
|
|
335
|
+
const m = 1.0 / Math.max(this.physicalSize.x, Math.max(this.physicalSize.y, this.physicalSize.z));
|
|
336
|
+
return new Vector3().fromArray(xyz).multiply(this.physicalPixelSize).multiplyScalar(m).toArray();
|
|
337
|
+
}
|
|
338
|
+
addVolumeDataObserver(o) {
|
|
339
|
+
this.volumeDataObservers.push(o);
|
|
340
|
+
}
|
|
341
|
+
removeVolumeDataObserver(o) {
|
|
342
|
+
if (o) {
|
|
343
|
+
const i = this.volumeDataObservers.indexOf(o);
|
|
344
|
+
if (i !== -1) {
|
|
345
|
+
this.volumeDataObservers.splice(i, 1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
removeAllVolumeDataObservers() {
|
|
350
|
+
this.volumeDataObservers = [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** Default: 250MB. Should be large enough to be useful but safe for most any computer that can run the app */
|
|
2
|
+
const CACHE_MAX_SIZE_DEFAULT = 250_000_000;
|
|
3
|
+
export default class VolumeCache {
|
|
4
|
+
// Ends of a linked list of entries, to track LRU and evict efficiently
|
|
5
|
+
|
|
6
|
+
// TODO implement some way to manage used vs unused (prefetched) entries so
|
|
7
|
+
// that prefetched entries which are never used don't get highest priority!
|
|
8
|
+
|
|
9
|
+
constructor(maxSize = CACHE_MAX_SIZE_DEFAULT) {
|
|
10
|
+
this.entries = new Map();
|
|
11
|
+
this.maxSize = maxSize;
|
|
12
|
+
this.currentSize = 0;
|
|
13
|
+
this.first = null;
|
|
14
|
+
this.last = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Hide these behind getters so they're definitely never set from the outside
|
|
18
|
+
/** The size of all data arrays currently stored in this cache, in bytes. */
|
|
19
|
+
get size() {
|
|
20
|
+
return this.currentSize;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The number of entries currently stored in this cache. */
|
|
24
|
+
get numberOfEntries() {
|
|
25
|
+
return this.entries.size;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Removes an entry from a store but NOT the LRU list.
|
|
30
|
+
* Only call from a method with the word "evict" in it!
|
|
31
|
+
*/
|
|
32
|
+
removeEntryFromStore(entry) {
|
|
33
|
+
this.entries.delete(entry.key);
|
|
34
|
+
this.currentSize -= entry.data.byteLength;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Removes an entry from the LRU list but NOT its store.
|
|
39
|
+
* Entry must be replaced in list or removed from store, or it will never be evicted!
|
|
40
|
+
*/
|
|
41
|
+
removeEntryFromList(entry) {
|
|
42
|
+
const {
|
|
43
|
+
prev,
|
|
44
|
+
next
|
|
45
|
+
} = entry;
|
|
46
|
+
if (prev) {
|
|
47
|
+
prev.next = next;
|
|
48
|
+
} else {
|
|
49
|
+
this.first = next;
|
|
50
|
+
}
|
|
51
|
+
if (next) {
|
|
52
|
+
next.prev = prev;
|
|
53
|
+
} else {
|
|
54
|
+
this.last = prev;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Adds an entry which is *not currently in the list* to the front of the list. */
|
|
59
|
+
addEntryAsFirst(entry) {
|
|
60
|
+
if (this.first) {
|
|
61
|
+
this.first.prev = entry;
|
|
62
|
+
} else {
|
|
63
|
+
this.last = entry;
|
|
64
|
+
}
|
|
65
|
+
entry.next = this.first;
|
|
66
|
+
entry.prev = null;
|
|
67
|
+
this.first = entry;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Moves an entry which is *currently in the list* to the front of the list. */
|
|
71
|
+
moveEntryToFirst(entry) {
|
|
72
|
+
if (entry === this.first) return;
|
|
73
|
+
this.removeEntryFromList(entry);
|
|
74
|
+
this.addEntryAsFirst(entry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Evicts the least recently used entry from the cache. */
|
|
78
|
+
evictLast() {
|
|
79
|
+
if (!this.last) {
|
|
80
|
+
console.error("VolumeCache: attempt to evict last entry from cache when no last entry is set");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.removeEntryFromStore(this.last);
|
|
84
|
+
if (this.last.prev) {
|
|
85
|
+
this.last.prev.next = null;
|
|
86
|
+
}
|
|
87
|
+
this.last = this.last.prev;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Evicts a specific entry from the cache. */
|
|
91
|
+
evict(entry) {
|
|
92
|
+
this.removeEntryFromStore(entry);
|
|
93
|
+
this.removeEntryFromList(entry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Adds a new entry to the cache.
|
|
98
|
+
* @returns {boolean} a boolean indicating whether the insertion succeeded.
|
|
99
|
+
*/
|
|
100
|
+
insert(key, data) {
|
|
101
|
+
if (data.byteLength > this.maxSize) {
|
|
102
|
+
console.error("VolumeCache: attempt to insert a single entry larger than the cache");
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if entry is already in cache
|
|
107
|
+
// This will move the entry to the front of the LRU list, if present
|
|
108
|
+
const getResult = this.getEntry(key);
|
|
109
|
+
if (getResult !== undefined) {
|
|
110
|
+
getResult.data = data;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add new entry to cache
|
|
115
|
+
const newEntry = {
|
|
116
|
+
data,
|
|
117
|
+
prev: null,
|
|
118
|
+
next: null,
|
|
119
|
+
key
|
|
120
|
+
};
|
|
121
|
+
this.addEntryAsFirst(newEntry);
|
|
122
|
+
this.entries.set(key, newEntry);
|
|
123
|
+
this.currentSize += data.byteLength;
|
|
124
|
+
|
|
125
|
+
// Evict until size is within limit
|
|
126
|
+
while (this.currentSize > this.maxSize) {
|
|
127
|
+
this.evictLast();
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Internal implementation of `get`. Returns all entry metadata, not just the raw data. */
|
|
133
|
+
getEntry(key) {
|
|
134
|
+
const result = this.entries.get(key);
|
|
135
|
+
if (result) {
|
|
136
|
+
this.moveEntryToFirst(result);
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Attempts to get a single entry from the cache. */
|
|
142
|
+
get(key) {
|
|
143
|
+
return this.getEntry(key)?.data;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Clears all cache entries whose keys begin with the specified prefix. */
|
|
147
|
+
clearWithPrefix(prefix) {
|
|
148
|
+
for (const [key, entry] of this.entries.entries()) {
|
|
149
|
+
if (key.startsWith(prefix)) {
|
|
150
|
+
this.evict(entry);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Clears all data from the cache. */
|
|
156
|
+
clear() {
|
|
157
|
+
while (this.last) {
|
|
158
|
+
this.evictLast();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
package/es/VolumeDims.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Vector3 } from "three";
|
|
2
|
+
export function defaultVolumeDims() {
|
|
3
|
+
return {
|
|
4
|
+
shape: [0, 0, 0, 0, 0],
|
|
5
|
+
spacing: [1, 1, 1, 1, 1],
|
|
6
|
+
spaceUnit: "μm",
|
|
7
|
+
timeUnit: "s",
|
|
8
|
+
dataType: "uint8"
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function volumeSize(volumeDims) {
|
|
12
|
+
return new Vector3(volumeDims.shape[4], volumeDims.shape[3], volumeDims.shape[2]);
|
|
13
|
+
}
|
|
14
|
+
export function physicalPixelSize(volumeDims) {
|
|
15
|
+
return new Vector3(volumeDims.spacing[4], volumeDims.spacing[3], volumeDims.spacing[2]);
|
|
16
|
+
}
|