@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,231 @@
|
|
|
1
|
+
import { parseTimeUnit, TimeUnit } from "../constants/time.js";
|
|
2
|
+
import { Axis } from "../VolumeRenderSettings.js";
|
|
3
|
+
export const DEFAULT_SIG_FIGS = 5;
|
|
4
|
+
const SECONDS_IN_MS = 1000;
|
|
5
|
+
const MINUTES_IN_MS = SECONDS_IN_MS * 60;
|
|
6
|
+
const HOURS_IN_MS = MINUTES_IN_MS * 60;
|
|
7
|
+
const DAYS_IN_MS = HOURS_IN_MS * 24;
|
|
8
|
+
|
|
9
|
+
// Adapted from https://gist.github.com/ArneS/2ecfbe4a9d7072ac56c0.
|
|
10
|
+
function digitToUnicodeSupercript(n) {
|
|
11
|
+
const subst = [0x2070, 185, 178, 179, 0x2074, 0x2075, 0x2076, 0x2077, 0x2078, 0x2079];
|
|
12
|
+
return String.fromCharCode(subst[n]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a number to scientific notation with the specified number of significant
|
|
17
|
+
* figures, handling negative numbers and rounding.
|
|
18
|
+
* @param input The number to convert.
|
|
19
|
+
* @param significantFigures the number of signficant figures/digits. Must be >= 1.
|
|
20
|
+
* @returns a string, formatted as a number in scientific notation.
|
|
21
|
+
* @example
|
|
22
|
+
* ```
|
|
23
|
+
* numberToSciNotation(1, 3) // "1.00×10⁰"
|
|
24
|
+
* numberToSciNotation(0.99, 2) // "9.9×10⁻¹"
|
|
25
|
+
* numberToSciNotation(0.999, 2) // "1.0×10⁰"
|
|
26
|
+
* numberToSciNotation(-0.05, 1) // "-5×10⁻²"
|
|
27
|
+
* numberToSciNotation(1400, 3) // "1.40×10³"
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function numberToSciNotation(input, sigFigs = DEFAULT_SIG_FIGS) {
|
|
31
|
+
const nativeExpForm = input.toExponential(sigFigs - 1);
|
|
32
|
+
const [significand, exponent] = nativeExpForm.split("e");
|
|
33
|
+
const expSign = exponent[0] === "-" ? "⁻" : "";
|
|
34
|
+
const expDigits = exponent.slice(1).split("");
|
|
35
|
+
const expSuperscript = expDigits.map(digit => digitToUnicodeSupercript(Number(digit))).join("");
|
|
36
|
+
return `${significand}×10${expSign}${expSuperscript}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a string-encoded number rounded to a specified decimal precision, without ever formatting in scientific
|
|
41
|
+
* notation like `Number.toPrecision` might do.
|
|
42
|
+
*/
|
|
43
|
+
function toSigFigs(value, sigFigs) {
|
|
44
|
+
const exponent = Math.floor(Math.log10(Math.abs(value)));
|
|
45
|
+
return value.toFixed(Math.max(sigFigs - exponent - 1, 0));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Trims trailing instances of `char` off the end of `str`. */
|
|
49
|
+
// This is not technically a number utility, but it's useful to `formatNumber` below.
|
|
50
|
+
function trimTrailing(str, char) {
|
|
51
|
+
let i = str.length - 1;
|
|
52
|
+
while (str[i] === char) {
|
|
53
|
+
i--;
|
|
54
|
+
}
|
|
55
|
+
return str.slice(0, i + 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Formats numbers for display as a string with a (hopefully) limited length.
|
|
60
|
+
*
|
|
61
|
+
* - If the number is an integer with 4 or fewer digits, it is returned as a string.
|
|
62
|
+
* - If the number is a decimal, it is rounded to `sigFigs` significant figures. (default 5)
|
|
63
|
+
* - If the number's absolute value is over 10,000 or less than 0.01, it is formatted in scientific notation to
|
|
64
|
+
* `sciSigFigs` significant figures. (Default `sigFigs - 2`, so 3 if neither are specified. The `- 2` leaves space
|
|
65
|
+
* for the exponential part. Remember: the purpose of this function is keeping number strings *consistently* short!)
|
|
66
|
+
*/
|
|
67
|
+
export function formatNumber(value, sigFigs = DEFAULT_SIG_FIGS, sciSigFigs = sigFigs - 2) {
|
|
68
|
+
const valueAbs = Math.abs(value);
|
|
69
|
+
if (Number.isInteger(value)) {
|
|
70
|
+
// Format integers with 5+ digits in scientific notation
|
|
71
|
+
if (valueAbs >= 10_000) {
|
|
72
|
+
return numberToSciNotation(value, sciSigFigs);
|
|
73
|
+
}
|
|
74
|
+
// Just stringify other integers
|
|
75
|
+
return value.toString();
|
|
76
|
+
} else {
|
|
77
|
+
const numStr = toSigFigs(value, sigFigs);
|
|
78
|
+
const numRounded = Math.abs(Number(numStr));
|
|
79
|
+
if (numRounded >= 10_000 || numRounded < 0.01) {
|
|
80
|
+
return numberToSciNotation(value, sciSigFigs);
|
|
81
|
+
}
|
|
82
|
+
const trimmed = trimTrailing(numStr, "0");
|
|
83
|
+
return trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const timeUnitEnumToMilliseconds = {
|
|
87
|
+
[TimeUnit.MILLISECOND]: 1,
|
|
88
|
+
[TimeUnit.SECOND]: SECONDS_IN_MS,
|
|
89
|
+
[TimeUnit.MINUTE]: MINUTES_IN_MS,
|
|
90
|
+
[TimeUnit.HOUR]: HOURS_IN_MS,
|
|
91
|
+
[TimeUnit.DAY]: DAYS_IN_MS
|
|
92
|
+
};
|
|
93
|
+
export function timeToMilliseconds(time, unit) {
|
|
94
|
+
const timeUnitMultiplier = timeUnitEnumToMilliseconds[unit];
|
|
95
|
+
if (timeUnitMultiplier === undefined) {
|
|
96
|
+
throw new Error("Unrecognized time unit");
|
|
97
|
+
}
|
|
98
|
+
return time * timeUnitMultiplier;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Pads the `value` with zeroes to the specified `length` if `shouldPad` is true
|
|
103
|
+
* and returns the resulting string. Otherwise, returns the string representation of `value`.
|
|
104
|
+
*/
|
|
105
|
+
function padConditionally(value, length, shouldPad) {
|
|
106
|
+
return shouldPad ? value.toString().padStart(length, "0") : value.toString();
|
|
107
|
+
}
|
|
108
|
+
function formatTimestamp(timeMs, options) {
|
|
109
|
+
const {
|
|
110
|
+
useMs,
|
|
111
|
+
useSec,
|
|
112
|
+
useMin,
|
|
113
|
+
useHours,
|
|
114
|
+
useDays
|
|
115
|
+
} = options;
|
|
116
|
+
const digits = [];
|
|
117
|
+
const units = [];
|
|
118
|
+
if (useDays) {
|
|
119
|
+
const days = Math.floor(timeMs / DAYS_IN_MS);
|
|
120
|
+
digits.push(days.toString());
|
|
121
|
+
units.push("d");
|
|
122
|
+
}
|
|
123
|
+
if (useHours) {
|
|
124
|
+
const hours = Math.floor(timeMs % DAYS_IN_MS / HOURS_IN_MS);
|
|
125
|
+
// If the previous unit is included, pad the hours to 2 digits so the
|
|
126
|
+
// timestamp is consistent.
|
|
127
|
+
digits.push(padConditionally(hours, 2, useDays));
|
|
128
|
+
units.push("h");
|
|
129
|
+
}
|
|
130
|
+
if (useMin) {
|
|
131
|
+
const minutes = Math.floor(timeMs % HOURS_IN_MS / MINUTES_IN_MS);
|
|
132
|
+
digits.push(padConditionally(minutes, 2, useHours));
|
|
133
|
+
units.push("m");
|
|
134
|
+
}
|
|
135
|
+
if (useSec) {
|
|
136
|
+
const seconds = Math.floor(timeMs % MINUTES_IN_MS / SECONDS_IN_MS);
|
|
137
|
+
let secondString = padConditionally(seconds, 2, useMin);
|
|
138
|
+
units.push("s");
|
|
139
|
+
// If using milliseconds, add as a decimal to the seconds string.
|
|
140
|
+
if (useMs) {
|
|
141
|
+
const milliseconds = Math.floor(timeMs % SECONDS_IN_MS);
|
|
142
|
+
secondString += "." + milliseconds.toString().padStart(3, "0");
|
|
143
|
+
// Do not add milliseconds to unit label, since they'll be shown as
|
|
144
|
+
// part of the seconds string.
|
|
145
|
+
}
|
|
146
|
+
digits.push(secondString);
|
|
147
|
+
} else if (useMs) {
|
|
148
|
+
const milliseconds = Math.floor(timeMs % SECONDS_IN_MS);
|
|
149
|
+
digits.push(milliseconds.toString());
|
|
150
|
+
units.push("ms");
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
timestamp: digits.join(":"),
|
|
154
|
+
units: units.join(":")
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets a timestamp formatted as `{time} / {total} {unit}`. If `unit` is a recognized
|
|
160
|
+
* time unit, the timestamp will be formatted as a `d:hh:mm:ss.ms` string.
|
|
161
|
+
*
|
|
162
|
+
* @param time Current time, in specified units.
|
|
163
|
+
* @param total Total time, in specified units.
|
|
164
|
+
* @param unit The unit of time.
|
|
165
|
+
* @returns A formatted timestamp string.
|
|
166
|
+
* - If `unit` is not recognized, the timestamp will be formatted as `{time} / {total} {unit}`,
|
|
167
|
+
* where `time` and `total` are formatted with significant digits as needed.
|
|
168
|
+
* - If `unit` is recognized, the timestamp will be formatted as `d:hh:mm:ss.ms`, specifying
|
|
169
|
+
* the most significant unit based on the total time, and the least significant unit with
|
|
170
|
+
* `unit`. See `parseTimeUnit()` for recognized time units.
|
|
171
|
+
*/
|
|
172
|
+
export function getTimestamp(time, total, unit) {
|
|
173
|
+
const timeUnit = parseTimeUnit(unit);
|
|
174
|
+
if (timeUnit === undefined) {
|
|
175
|
+
return `${formatNumber(time)} / ${formatNumber(total)} ${unit}`;
|
|
176
|
+
}
|
|
177
|
+
const timeMs = timeToMilliseconds(time, timeUnit);
|
|
178
|
+
const totalMs = timeToMilliseconds(total, timeUnit);
|
|
179
|
+
|
|
180
|
+
// Toggle each unit based on the total time and the provided timeUnit.
|
|
181
|
+
// Exploit an enum property where TimeUnit.Milliseconds < TimeUnit.Second < TimeUnit.Minute ... etc.
|
|
182
|
+
const options = {
|
|
183
|
+
useMs: timeUnit == TimeUnit.MILLISECOND,
|
|
184
|
+
useSec: timeUnit == TimeUnit.SECOND || timeUnit <= TimeUnit.SECOND && totalMs >= SECONDS_IN_MS,
|
|
185
|
+
useMin: timeUnit == TimeUnit.MINUTE || timeUnit <= TimeUnit.MINUTE && totalMs >= MINUTES_IN_MS,
|
|
186
|
+
useHours: timeUnit == TimeUnit.HOUR || timeUnit <= TimeUnit.HOUR && totalMs >= HOURS_IN_MS,
|
|
187
|
+
useDays: timeUnit == TimeUnit.DAY || timeUnit <= TimeUnit.DAY && totalMs >= DAYS_IN_MS
|
|
188
|
+
};
|
|
189
|
+
const {
|
|
190
|
+
timestamp,
|
|
191
|
+
units
|
|
192
|
+
} = formatTimestamp(timeMs, options);
|
|
193
|
+
const {
|
|
194
|
+
timestamp: totalTimestamp
|
|
195
|
+
} = formatTimestamp(totalMs, options);
|
|
196
|
+
return `${timestamp} / ${totalTimestamp} ${units}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Constrains the `src` vector relative to the `target` so it only has freedom along the
|
|
201
|
+
* specified `axis`. Does nothing if `axis = Axis.NONE`.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```
|
|
205
|
+
* const src = [1, 2, 3];
|
|
206
|
+
* const target = [4, 5, 6];
|
|
207
|
+
* const constrained = constrainToAxis(src, target, Axis.X);
|
|
208
|
+
* console.log(constrained); // [1, 5, 6]
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export function constrainToAxis(src, target, axis) {
|
|
212
|
+
switch (axis) {
|
|
213
|
+
case Axis.X:
|
|
214
|
+
return [src[0], target[1], target[2]];
|
|
215
|
+
case Axis.Y:
|
|
216
|
+
return [target[0], src[1], target[2]];
|
|
217
|
+
case Axis.Z:
|
|
218
|
+
return [target[0], target[1], src[2]];
|
|
219
|
+
default:
|
|
220
|
+
return [...src];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export function getDataRange(data) {
|
|
224
|
+
let min = data[0];
|
|
225
|
+
let max = data[0];
|
|
226
|
+
for (let i = 1; i < data.length; i++) {
|
|
227
|
+
min = Math.min(min, data[i]);
|
|
228
|
+
max = Math.max(max, data[i]);
|
|
229
|
+
}
|
|
230
|
+
return [min, max];
|
|
231
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { fromUrl } from "geotiff";
|
|
2
|
+
import { serializeError } from "serialize-error";
|
|
3
|
+
import { VolumeLoadError, VolumeLoadErrorType } from "../loaders/VolumeLoadError.js";
|
|
4
|
+
// from TIFF
|
|
5
|
+
const SAMPLEFORMAT_UINT = 1;
|
|
6
|
+
const SAMPLEFORMAT_INT = 2;
|
|
7
|
+
const SAMPLEFORMAT_IEEEFP = 3;
|
|
8
|
+
function getDtype(sampleFormat, bytesPerPixel) {
|
|
9
|
+
if (sampleFormat === SAMPLEFORMAT_IEEEFP) {
|
|
10
|
+
if (bytesPerPixel === 4) {
|
|
11
|
+
return "float32";
|
|
12
|
+
}
|
|
13
|
+
} else if (sampleFormat === SAMPLEFORMAT_INT) {
|
|
14
|
+
if (bytesPerPixel === 1) {
|
|
15
|
+
return "int8";
|
|
16
|
+
} else if (bytesPerPixel === 2) {
|
|
17
|
+
return "int16";
|
|
18
|
+
} else if (bytesPerPixel === 4) {
|
|
19
|
+
return "int32";
|
|
20
|
+
}
|
|
21
|
+
} else if (sampleFormat === SAMPLEFORMAT_UINT) {
|
|
22
|
+
if (bytesPerPixel === 1) {
|
|
23
|
+
return "uint8";
|
|
24
|
+
} else if (bytesPerPixel === 2) {
|
|
25
|
+
return "uint16";
|
|
26
|
+
} else if (bytesPerPixel === 4) {
|
|
27
|
+
return "uint32";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
console.error(`TIFF Worker: unsupported sample format ${sampleFormat} and bytes per pixel ${bytesPerPixel}`);
|
|
31
|
+
return "uint8";
|
|
32
|
+
}
|
|
33
|
+
function castToArray(buf, bytesPerPixel, sampleFormat) {
|
|
34
|
+
if (sampleFormat === SAMPLEFORMAT_IEEEFP) {
|
|
35
|
+
if (bytesPerPixel === 4) {
|
|
36
|
+
return new Float32Array(buf);
|
|
37
|
+
}
|
|
38
|
+
} else if (sampleFormat === SAMPLEFORMAT_INT) {
|
|
39
|
+
if (bytesPerPixel === 1) {
|
|
40
|
+
return new Int8Array(buf);
|
|
41
|
+
} else if (bytesPerPixel === 2) {
|
|
42
|
+
return new Int16Array(buf);
|
|
43
|
+
} else if (bytesPerPixel === 4) {
|
|
44
|
+
return new Int32Array(buf);
|
|
45
|
+
}
|
|
46
|
+
} else if (sampleFormat === SAMPLEFORMAT_UINT) {
|
|
47
|
+
if (bytesPerPixel === 1) {
|
|
48
|
+
return new Uint8Array(buf);
|
|
49
|
+
} else if (bytesPerPixel === 2) {
|
|
50
|
+
return new Uint16Array(buf);
|
|
51
|
+
} else if (bytesPerPixel === 4) {
|
|
52
|
+
return new Uint32Array(buf);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
console.error(`TIFF Worker: unsupported sample format ${sampleFormat} and bytes per pixel ${bytesPerPixel}`);
|
|
56
|
+
return new Uint8Array(buf);
|
|
57
|
+
}
|
|
58
|
+
async function loadTiffChannel(e) {
|
|
59
|
+
// TODO index images by time
|
|
60
|
+
// const time = e.data.time;
|
|
61
|
+
|
|
62
|
+
const channelIndex = e.data.channel;
|
|
63
|
+
const tilesizex = e.data.tilesizex;
|
|
64
|
+
const tilesizey = e.data.tilesizey;
|
|
65
|
+
const sizez = e.data.sizez;
|
|
66
|
+
const sizec = e.data.sizec;
|
|
67
|
+
const dimensionOrder = e.data.dimensionOrder;
|
|
68
|
+
const bytesPerSample = e.data.bytesPerSample;
|
|
69
|
+
const tiff = await fromUrl(e.data.url, {
|
|
70
|
+
allowFullFile: true
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// load the images of this channel from the tiff
|
|
74
|
+
// today assume TCZYX so the slices are already in order.
|
|
75
|
+
let startindex = 0;
|
|
76
|
+
let incrementz = 1;
|
|
77
|
+
if (dimensionOrder === "XYZCT") {
|
|
78
|
+
// we have XYZCT which is the "good" case
|
|
79
|
+
// TCZYX
|
|
80
|
+
startindex = sizez * channelIndex;
|
|
81
|
+
incrementz = 1;
|
|
82
|
+
} else if (dimensionOrder === "XYCZT") {
|
|
83
|
+
// we have to loop differently to increment channels
|
|
84
|
+
// TZCYX
|
|
85
|
+
startindex = channelIndex;
|
|
86
|
+
incrementz = sizec;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// huge assumption: planes are in a particular order relative to z and c
|
|
90
|
+
|
|
91
|
+
// get first plane, to get some details about the image
|
|
92
|
+
const image = await tiff.getImage(startindex);
|
|
93
|
+
// on first image, set up some stuff:
|
|
94
|
+
const sampleFormat = image.getSampleFormat();
|
|
95
|
+
const bytesPerPixel = image.getBytesPerPixel();
|
|
96
|
+
// allocate a buffer for one channel
|
|
97
|
+
const buffer = new ArrayBuffer(tilesizex * tilesizey * sizez * bytesPerPixel);
|
|
98
|
+
const u8 = new Uint8Array(buffer);
|
|
99
|
+
for (let imageIndex = startindex, zslice = 0; zslice < sizez; imageIndex += incrementz, ++zslice) {
|
|
100
|
+
const image = await tiff.getImage(imageIndex);
|
|
101
|
+
// download and downsample on client
|
|
102
|
+
const result = await image.readRasters({
|
|
103
|
+
width: tilesizex,
|
|
104
|
+
height: tilesizey
|
|
105
|
+
});
|
|
106
|
+
const arrayresult = Array.isArray(result) ? result[0] : result;
|
|
107
|
+
// deposit in full channel array in the right place
|
|
108
|
+
const offset = zslice * tilesizex * tilesizey;
|
|
109
|
+
if (arrayresult.BYTES_PER_ELEMENT > 4) {
|
|
110
|
+
throw new VolumeLoadError("byte size not supported yet: " + arrayresult.BYTES_PER_ELEMENT, {
|
|
111
|
+
type: VolumeLoadErrorType.INVALID_METADATA
|
|
112
|
+
});
|
|
113
|
+
} else if (arrayresult.BYTES_PER_ELEMENT !== bytesPerSample) {
|
|
114
|
+
throw new VolumeLoadError("tiff bytes per element mismatch with OME metadata", {
|
|
115
|
+
type: VolumeLoadErrorType.INVALID_METADATA
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
u8.set(new Uint8Array(arrayresult.buffer), offset * arrayresult.BYTES_PER_ELEMENT);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// all slices collected, now resample to 8 bits full data range
|
|
122
|
+
const src = castToArray(buffer, bytesPerPixel, sampleFormat);
|
|
123
|
+
const dtype = getDtype(sampleFormat, bytesPerPixel);
|
|
124
|
+
let chmin = src[0];
|
|
125
|
+
let chmax = src[0];
|
|
126
|
+
for (let j = 0; j < src.length; ++j) {
|
|
127
|
+
const val = src[j];
|
|
128
|
+
if (val < chmin) {
|
|
129
|
+
chmin = val;
|
|
130
|
+
}
|
|
131
|
+
if (val > chmax) {
|
|
132
|
+
chmax = val;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
data: src,
|
|
137
|
+
channel: channelIndex,
|
|
138
|
+
range: [chmin, chmax],
|
|
139
|
+
dtype: dtype,
|
|
140
|
+
isError: false
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
self.onmessage = async e => {
|
|
144
|
+
try {
|
|
145
|
+
const result = await loadTiffChannel(e);
|
|
146
|
+
self.postMessage(result, [result.data.buffer]);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
self.postMessage({
|
|
149
|
+
isError: true,
|
|
150
|
+
error: serializeError(err)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { serializeError } from "serialize-error";
|
|
2
|
+
import VolumeCache from "../VolumeCache.js";
|
|
3
|
+
import { VolumeFileFormat, createVolumeLoader, pathToFileType } from "../loaders/index.js";
|
|
4
|
+
import { VolumeLoadError } from "../loaders/VolumeLoadError.js";
|
|
5
|
+
import RequestQueue from "../utils/RequestQueue.js";
|
|
6
|
+
import SubscribableRequestQueue from "../utils/SubscribableRequestQueue.js";
|
|
7
|
+
import { WorkerEventType, WorkerMsgType, WorkerResponseResult } from "./types.js";
|
|
8
|
+
import { rebuildLoadSpec } from "./util.js";
|
|
9
|
+
let cache = undefined;
|
|
10
|
+
let queue = undefined;
|
|
11
|
+
let subscribableQueue = undefined;
|
|
12
|
+
let loader = undefined;
|
|
13
|
+
let initialized = false;
|
|
14
|
+
let copyOnLoad = false;
|
|
15
|
+
const messageHandlers = {
|
|
16
|
+
[WorkerMsgType.INIT]: ({
|
|
17
|
+
maxCacheSize,
|
|
18
|
+
maxActiveRequests,
|
|
19
|
+
maxLowPriorityRequests
|
|
20
|
+
}) => {
|
|
21
|
+
if (!initialized) {
|
|
22
|
+
cache = new VolumeCache(maxCacheSize);
|
|
23
|
+
queue = new RequestQueue(maxActiveRequests, maxLowPriorityRequests);
|
|
24
|
+
subscribableQueue = new SubscribableRequestQueue(queue);
|
|
25
|
+
initialized = true;
|
|
26
|
+
}
|
|
27
|
+
return Promise.resolve();
|
|
28
|
+
},
|
|
29
|
+
[WorkerMsgType.CREATE_LOADER]: async ({
|
|
30
|
+
path,
|
|
31
|
+
options
|
|
32
|
+
}) => {
|
|
33
|
+
const pathString = Array.isArray(path) ? path[0] : path;
|
|
34
|
+
const fileType = options?.fileType || pathToFileType(pathString);
|
|
35
|
+
copyOnLoad = fileType === VolumeFileFormat.JSON;
|
|
36
|
+
loader = await createVolumeLoader(path, {
|
|
37
|
+
...options,
|
|
38
|
+
cache,
|
|
39
|
+
queue: subscribableQueue
|
|
40
|
+
});
|
|
41
|
+
return loader !== undefined;
|
|
42
|
+
},
|
|
43
|
+
[WorkerMsgType.CREATE_VOLUME]: async loadSpec => {
|
|
44
|
+
if (loader === undefined) {
|
|
45
|
+
throw new VolumeLoadError("No loader created");
|
|
46
|
+
}
|
|
47
|
+
return await loader.createImageInfo(rebuildLoadSpec(loadSpec));
|
|
48
|
+
},
|
|
49
|
+
[WorkerMsgType.LOAD_DIMS]: async loadSpec => {
|
|
50
|
+
if (loader === undefined) {
|
|
51
|
+
throw new VolumeLoadError("No loader created");
|
|
52
|
+
}
|
|
53
|
+
return await loader.loadDims(rebuildLoadSpec(loadSpec));
|
|
54
|
+
},
|
|
55
|
+
[WorkerMsgType.LOAD_VOLUME_DATA]: ({
|
|
56
|
+
imageInfo,
|
|
57
|
+
loadSpec,
|
|
58
|
+
loaderId,
|
|
59
|
+
loadId
|
|
60
|
+
}) => {
|
|
61
|
+
if (loader === undefined) {
|
|
62
|
+
throw new VolumeLoadError("No loader created");
|
|
63
|
+
}
|
|
64
|
+
return loader.loadRawChannelData(imageInfo, rebuildLoadSpec(loadSpec), (imageInfo, loadSpec) => {
|
|
65
|
+
const message = {
|
|
66
|
+
responseResult: WorkerResponseResult.EVENT,
|
|
67
|
+
eventType: WorkerEventType.METADATA_UPDATE,
|
|
68
|
+
loaderId,
|
|
69
|
+
loadId,
|
|
70
|
+
imageInfo,
|
|
71
|
+
loadSpec
|
|
72
|
+
};
|
|
73
|
+
self.postMessage(message);
|
|
74
|
+
}, (channelIndex, dtype, data, ranges, atlasDims) => {
|
|
75
|
+
const message = {
|
|
76
|
+
responseResult: WorkerResponseResult.EVENT,
|
|
77
|
+
eventType: WorkerEventType.CHANNEL_LOAD,
|
|
78
|
+
loaderId,
|
|
79
|
+
loadId,
|
|
80
|
+
channelIndex,
|
|
81
|
+
dtype,
|
|
82
|
+
data,
|
|
83
|
+
ranges,
|
|
84
|
+
atlasDims
|
|
85
|
+
};
|
|
86
|
+
self.postMessage(message, copyOnLoad ? [] : data.map(d => d.buffer));
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
[WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS]: directions => {
|
|
90
|
+
// Silently does nothing if the loader isn't an `OMEZarrLoader`
|
|
91
|
+
loader?.setPrefetchPriority(directions);
|
|
92
|
+
return Promise.resolve();
|
|
93
|
+
},
|
|
94
|
+
[WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING]: syncChannels => {
|
|
95
|
+
loader?.syncMultichannelLoading(syncChannels);
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
},
|
|
98
|
+
[WorkerMsgType.UPDATE_FETCH_OPTIONS]: fetchOptions => {
|
|
99
|
+
loader?.updateFetchOptions(fetchOptions);
|
|
100
|
+
return Promise.resolve();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
self.onmessage = async ({
|
|
104
|
+
data
|
|
105
|
+
}) => {
|
|
106
|
+
const {
|
|
107
|
+
msgId,
|
|
108
|
+
type,
|
|
109
|
+
payload
|
|
110
|
+
} = data;
|
|
111
|
+
let message;
|
|
112
|
+
try {
|
|
113
|
+
const response = await messageHandlers[type](payload);
|
|
114
|
+
message = {
|
|
115
|
+
responseResult: WorkerResponseResult.SUCCESS,
|
|
116
|
+
msgId,
|
|
117
|
+
type,
|
|
118
|
+
payload: response
|
|
119
|
+
};
|
|
120
|
+
} catch (e) {
|
|
121
|
+
message = {
|
|
122
|
+
responseResult: WorkerResponseResult.ERROR,
|
|
123
|
+
msgId,
|
|
124
|
+
type,
|
|
125
|
+
payload: serializeError(e)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
self.postMessage(message);
|
|
129
|
+
};
|