@aics/vole-core 4.2.0 → 4.3.1
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/es/MeshVolume.js +4 -0
- package/es/View3d.js +3 -3
- package/es/Volume.js +1 -1
- package/es/VolumeDrawable.js +2 -2
- package/es/constants/lights.js +4 -4
- package/es/loaders/OmeZarrLoader.js +13 -5
- package/es/loaders/TiffLoader.js +30 -11
- package/es/loaders/zarr_utils/utils.js +40 -5
- package/es/types/ImageInfo.d.ts +2 -2
- package/es/types/constants/lights.d.ts +4 -4
- package/es/types/loaders/zarr_utils/types.d.ts +1 -1
- package/es/types/loaders/zarr_utils/utils.d.ts +10 -1
- package/package.json +1 -1
package/es/MeshVolume.js
CHANGED
|
@@ -19,6 +19,10 @@ export default class MeshVolume {
|
|
|
19
19
|
this.meshRoot = new Object3D(); //create an empty container
|
|
20
20
|
this.meshRoot.name = "Mesh Surface Group";
|
|
21
21
|
|
|
22
|
+
// compensating for generated isosurface vertex coordinates
|
|
23
|
+
// arguably this could be set on meshPivot
|
|
24
|
+
this.meshRoot.scale.setScalar(0.5);
|
|
25
|
+
|
|
22
26
|
// handle transform ordering for giving the meshroot a rotation about a pivot point
|
|
23
27
|
this.meshPivot = new Group();
|
|
24
28
|
this.meshPivot.name = "MeshContainerNode";
|
package/es/View3d.js
CHANGED
|
@@ -928,12 +928,12 @@ export class View3d {
|
|
|
928
928
|
color: {
|
|
929
929
|
type: "float"
|
|
930
930
|
}
|
|
931
|
-
});
|
|
931
|
+
}).on("change", _event => this.redraw());
|
|
932
932
|
folder.addInput(light, "intensity", {
|
|
933
933
|
min: 0
|
|
934
|
-
});
|
|
934
|
+
}).on("change", _event => this.redraw());
|
|
935
935
|
if (!light.isAmbientLight) {
|
|
936
|
-
folder.addInput(light, "position");
|
|
936
|
+
folder.addInput(light, "position").on("change", _event => this.redraw());
|
|
937
937
|
}
|
|
938
938
|
};
|
|
939
939
|
addFolderForLight(this.spotLight, "spot light");
|
package/es/Volume.js
CHANGED
|
@@ -72,7 +72,7 @@ export default class Volume {
|
|
|
72
72
|
this.setVoxelSize(this.physicalPixelSize);
|
|
73
73
|
this.numChannels = this.imageInfo.numChannels;
|
|
74
74
|
this.channelNames = this.imageInfo.channelNames.slice();
|
|
75
|
-
this.channelColorsDefault = this.imageInfo.channelColors ? this.imageInfo.channelColors.
|
|
75
|
+
this.channelColorsDefault = this.imageInfo.channelColors ? this.imageInfo.channelColors.map((color, index) => color ?? getColorByChannelIndex(index)) : this.channelNames.map((name, index) => getColorByChannelIndex(index));
|
|
76
76
|
// fill in gaps
|
|
77
77
|
if (this.channelColorsDefault.length < this.imageInfo.numChannels) {
|
|
78
78
|
for (let i = this.channelColorsDefault.length - 1; i < this.imageInfo.numChannels; ++i) {
|
package/es/VolumeDrawable.js
CHANGED
|
@@ -711,7 +711,7 @@ export default class VolumeDrawable {
|
|
|
711
711
|
|
|
712
712
|
// remove old 3d object from scene
|
|
713
713
|
if (this.renderMode === RenderMode.SLICE || this.renderMode === RenderMode.RAYMARCH) {
|
|
714
|
-
this.
|
|
714
|
+
this.childObjectsGroup.remove(this.meshVolume.get3dObject());
|
|
715
715
|
}
|
|
716
716
|
this.sceneRoot.remove(this.volumeRendering.get3dObject());
|
|
717
717
|
|
|
@@ -747,7 +747,7 @@ export default class VolumeDrawable {
|
|
|
747
747
|
if (this.renderUpdateListener) {
|
|
748
748
|
this.renderUpdateListener(0);
|
|
749
749
|
}
|
|
750
|
-
this.
|
|
750
|
+
this.childObjectsGroup.add(this.meshVolume.get3dObject());
|
|
751
751
|
}
|
|
752
752
|
|
|
753
753
|
// add new 3d object to scene
|
package/es/constants/lights.js
CHANGED
|
@@ -3,7 +3,7 @@ const spotlightSettings = Object.freeze({
|
|
|
3
3
|
angle: 36 * MathUtils.DEG2RAD,
|
|
4
4
|
castShadow: false,
|
|
5
5
|
color: 0xffffff,
|
|
6
|
-
intensity: 0
|
|
6
|
+
intensity: 15.0,
|
|
7
7
|
position: {
|
|
8
8
|
x: -4,
|
|
9
9
|
y: 3.5,
|
|
@@ -12,12 +12,12 @@ const spotlightSettings = Object.freeze({
|
|
|
12
12
|
});
|
|
13
13
|
const ambientLightSettings = Object.freeze({
|
|
14
14
|
color: 0xffffff,
|
|
15
|
-
intensity:
|
|
15
|
+
intensity: 1.75
|
|
16
16
|
});
|
|
17
17
|
const reflectedLightSettings = Object.freeze({
|
|
18
18
|
castShadow: false,
|
|
19
19
|
color: 0xff88aa,
|
|
20
|
-
intensity: 0
|
|
20
|
+
intensity: 2.0,
|
|
21
21
|
position: {
|
|
22
22
|
x: 1,
|
|
23
23
|
y: -5,
|
|
@@ -27,7 +27,7 @@ const reflectedLightSettings = Object.freeze({
|
|
|
27
27
|
const fillLightSettings = Object.freeze({
|
|
28
28
|
castShadow: false,
|
|
29
29
|
color: 0xe8d1a9,
|
|
30
|
-
intensity:
|
|
30
|
+
intensity: 1.5,
|
|
31
31
|
position: {
|
|
32
32
|
x: 2.5,
|
|
33
33
|
y: 0.2,
|
|
@@ -7,7 +7,7 @@ import SubscribableRequestQueue from "../utils/SubscribableRequestQueue.js";
|
|
|
7
7
|
import { ThreadableVolumeLoader } from "./IVolumeLoader.js";
|
|
8
8
|
import { composeSubregion, computePackedAtlasDims, convertSubregionToPixels, pickLevelToLoad, unitNameToSymbol } from "./VolumeLoaderUtils.js";
|
|
9
9
|
import ChunkPrefetchIterator from "./zarr_utils/ChunkPrefetchIterator.js";
|
|
10
|
-
import { getScale,
|
|
10
|
+
import { getScale, getSourceChannelMeta, matchSourceScaleLevels, orderByDimension, orderByTCZYX, remapAxesToTCZYX } from "./zarr_utils/utils.js";
|
|
11
11
|
import { VolumeLoadError, VolumeLoadErrorType, wrapVolumeLoadError } from "./VolumeLoadError.js";
|
|
12
12
|
import wrapArray, { RelaxedFetchStore } from "./zarr_utils/wrappers.js";
|
|
13
13
|
import { assertMetadataHasMultiscales, toOMEZarrMetaV4, validateOMEZarrMetadata } from "./zarr_utils/validation.js";
|
|
@@ -281,11 +281,16 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
281
281
|
// Channel names is the other place where we have to check every source
|
|
282
282
|
// Track which channel names we've seen so far, so that we can rename them to avoid name collisions
|
|
283
283
|
const channelNamesMap = new Map();
|
|
284
|
-
const channelNames =
|
|
285
|
-
|
|
284
|
+
const channelNames = [];
|
|
285
|
+
const channelColors = [];
|
|
286
|
+
for (const src of this.sources) {
|
|
287
|
+
const {
|
|
288
|
+
names,
|
|
289
|
+
colors
|
|
290
|
+
} = getSourceChannelMeta(src);
|
|
286
291
|
|
|
287
292
|
// Resolve name collisions
|
|
288
|
-
|
|
293
|
+
const resolvedNames = names.map(channelName => {
|
|
289
294
|
const numMatchingChannels = channelNamesMap.get(channelName);
|
|
290
295
|
if (numMatchingChannels !== undefined) {
|
|
291
296
|
// If e.g. we've seen channel "Membrane" once before, rename this one to "Membrane (1)"
|
|
@@ -296,7 +301,9 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
296
301
|
return channelName;
|
|
297
302
|
}
|
|
298
303
|
});
|
|
299
|
-
|
|
304
|
+
channelNames.push(...resolvedNames);
|
|
305
|
+
channelColors.push(...colors);
|
|
306
|
+
}
|
|
300
307
|
const alldims = source0.scaleLevels.map((level, i) => {
|
|
301
308
|
const dims = {
|
|
302
309
|
spaceUnit: spatialUnit,
|
|
@@ -314,6 +321,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
314
321
|
subregionOffset: [0, 0, 0],
|
|
315
322
|
numChannelsPerSource,
|
|
316
323
|
channelNames,
|
|
324
|
+
channelColors,
|
|
317
325
|
multiscaleLevel: levelToLoad,
|
|
318
326
|
multiscaleLevelDims: alldims,
|
|
319
327
|
transform: {
|
package/es/loaders/TiffLoader.js
CHANGED
|
@@ -4,20 +4,17 @@ import { ThreadableVolumeLoader, LoadSpec } from "./IVolumeLoader.js";
|
|
|
4
4
|
import { computePackedAtlasDims, MAX_ATLAS_EDGE } from "./VolumeLoaderUtils.js";
|
|
5
5
|
import { VolumeLoadError, VolumeLoadErrorType, wrapVolumeLoadError } from "./VolumeLoadError.js";
|
|
6
6
|
import { CImageInfo } from "../ImageInfo.js";
|
|
7
|
-
function
|
|
7
|
+
function trimNull(xml) {
|
|
8
8
|
// trim trailing unicode zeros?
|
|
9
|
-
|
|
10
|
-
const expr = /[\u0000]$/g;
|
|
11
|
-
return xml.trim().replace(expr, "").trim();
|
|
9
|
+
return xml && xml.trim().replace(/\0/g, "").trim();
|
|
12
10
|
}
|
|
13
11
|
function getOME(xml) {
|
|
14
|
-
if (xml
|
|
12
|
+
if (typeof xml !== "string") {
|
|
15
13
|
return undefined;
|
|
16
14
|
}
|
|
17
|
-
const prepared = prepareXML(xml);
|
|
18
15
|
const parser = new DOMParser();
|
|
19
16
|
try {
|
|
20
|
-
const xmlDoc = parser.parseFromString(
|
|
17
|
+
const xmlDoc = parser.parseFromString(xml, "text/xml");
|
|
21
18
|
return xmlDoc.getElementsByTagName("OME")[0];
|
|
22
19
|
} catch (e) {
|
|
23
20
|
return undefined;
|
|
@@ -104,20 +101,42 @@ class TiffLoader extends ThreadableVolumeLoader {
|
|
|
104
101
|
// const imagecount = await tiff.getImageCount();
|
|
105
102
|
// read the FIRST image
|
|
106
103
|
const image = await tiff.getImage().catch(wrapVolumeLoadError("Failed to open TIFF image", VolumeLoadErrorType.NOT_FOUND));
|
|
107
|
-
const
|
|
104
|
+
const image0DescriptionRaw = image.getFileDirectory().ImageDescription;
|
|
105
|
+
// Get rid of null terminator, if it's there (`JSON.parse` doesn't know what to do with it)
|
|
106
|
+
const image0Description = trimNull(image0DescriptionRaw);
|
|
107
|
+
const omeEl = getOME(image0Description);
|
|
108
108
|
if (omeEl !== undefined) {
|
|
109
109
|
const image0El = omeEl.getElementsByTagName("Image")[0];
|
|
110
110
|
this.dims = getOMEDims(image0El);
|
|
111
111
|
} else {
|
|
112
112
|
console.warn("Could not read OME-TIFF metadata from file. Doing our best with base TIFF metadata.");
|
|
113
113
|
this.dims = new OMEDims();
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
let shape = [];
|
|
115
|
+
if (typeof image0Description === "string") {
|
|
116
|
+
try {
|
|
117
|
+
const description = JSON.parse(image0Description);
|
|
118
|
+
if (Array.isArray(description.shape)) {
|
|
119
|
+
shape = description.shape;
|
|
120
|
+
}
|
|
121
|
+
// eslint-disable-next-line no-empty
|
|
122
|
+
} catch (_e) {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// if `ImageDescription` is valid JSON with a `shape` field, we expect it to be an array of [t?, c?, z?, y, x].
|
|
126
|
+
this.dims.sizex = shape[shape.length - 1] ?? image.getWidth();
|
|
127
|
+
this.dims.sizey = shape[shape.length - 2] ?? image.getHeight();
|
|
128
|
+
this.dims.sizez = shape[shape.length - 3] ?? (await tiff.getImageCount());
|
|
129
|
+
|
|
116
130
|
// TODO this is a big hack/assumption about only loading multi-source tiffs that are not OMETIFF.
|
|
117
131
|
// We really have to check each url in the array for sizec to get the total number of channels
|
|
118
132
|
// See combinedNumChannels in ImageInfo below.
|
|
119
133
|
// Also compare with how OMEZarrLoader does this.
|
|
120
|
-
|
|
134
|
+
if (this.url.length > 1) {
|
|
135
|
+
// if multiple urls, assume one channel per url
|
|
136
|
+
this.dims.sizec = this.url.length;
|
|
137
|
+
} else {
|
|
138
|
+
this.dims.sizec = shape[shape.length - 4] ?? 1;
|
|
139
|
+
}
|
|
121
140
|
this.dims.pixeltype = getPixelType(image.getBytesPerPixel());
|
|
122
141
|
this.dims.channelnames = Array.from({
|
|
123
142
|
length: this.dims.sizec
|
|
@@ -1,16 +1,51 @@
|
|
|
1
1
|
import { VolumeLoadErrorType, VolumeLoadError } from "../VolumeLoadError.js";
|
|
2
|
+
/**
|
|
3
|
+
* Attempts to parse `color` as a 24-bit (6-digit) hexadecimal color with a possible leading `#`.
|
|
4
|
+
*
|
|
5
|
+
* Six-digit hex is the only allowable color representation in the OMERO metadata spec.
|
|
6
|
+
*/
|
|
7
|
+
export function parseHexColor(color) {
|
|
8
|
+
if (color === undefined) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
const result = /^#?([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2})$/i.exec(color);
|
|
12
|
+
if (result) {
|
|
13
|
+
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
|
|
14
|
+
} else {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
2
19
|
/** Extracts channel names from a `ZarrSource`. Handles missing `omeroMetadata`. Does *not* resolve name collisions. */
|
|
3
|
-
export function
|
|
20
|
+
export function getSourceChannelMeta(src) {
|
|
4
21
|
if (src.omeroMetadata?.channels) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
22
|
+
const {
|
|
23
|
+
channels
|
|
24
|
+
} = src.omeroMetadata;
|
|
25
|
+
const names = [];
|
|
26
|
+
const colors = [];
|
|
27
|
+
for (let i = 0; i < channels.length; i++) {
|
|
28
|
+
const channel = channels[i];
|
|
29
|
+
names.push(channel.label ?? `Channel ${i + src.channelOffset}`);
|
|
30
|
+
colors.push(parseHexColor(channel.color));
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
names,
|
|
34
|
+
colors
|
|
35
|
+
};
|
|
8
36
|
}
|
|
9
37
|
const cIdx = src.axesTCZYX[1];
|
|
10
38
|
const length = cIdx < 0 ? 1 : src.scaleLevels[0].shape[cIdx];
|
|
11
|
-
|
|
39
|
+
const names = Array.from({
|
|
12
40
|
length
|
|
13
41
|
}, (_, idx) => `Channel ${idx + src.channelOffset}`);
|
|
42
|
+
const colors = Array.from({
|
|
43
|
+
length
|
|
44
|
+
}, () => undefined);
|
|
45
|
+
return {
|
|
46
|
+
names,
|
|
47
|
+
colors
|
|
48
|
+
};
|
|
14
49
|
}
|
|
15
50
|
|
|
16
51
|
/** Turns `axesTCZYX` into the number of dimensions in the array */
|
package/es/types/ImageInfo.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type ImageInfo = Readonly<{
|
|
|
17
17
|
/** The names of each channel */
|
|
18
18
|
channelNames: string[];
|
|
19
19
|
/** Optional overrides to default channel colors, in 0-255 range, RGB order */
|
|
20
|
-
channelColors?: [number, number, number][];
|
|
20
|
+
channelColors?: ([number, number, number] | undefined)[];
|
|
21
21
|
/** Dimensions of each scale level, at original size, from the first data source */
|
|
22
22
|
multiscaleLevelDims: VolumeDims[];
|
|
23
23
|
/** The scale level from which this image was loaded, between `0` and `numMultiscaleLevels-1` */
|
|
@@ -66,7 +66,7 @@ export declare class CImageInfo {
|
|
|
66
66
|
/** The names of each channel */
|
|
67
67
|
get channelNames(): string[];
|
|
68
68
|
/** Optional overrides to default channel colors, in 0-255 range */
|
|
69
|
-
get channelColors(): [number, number, number][] | undefined;
|
|
69
|
+
get channelColors(): ([number, number, number] | undefined)[] | undefined;
|
|
70
70
|
/** Size of the currently loaded subregion, in pixels */
|
|
71
71
|
get subregionSize(): Vector3;
|
|
72
72
|
/** Offset of the loaded subregion into the total volume, in pixels */
|
|
@@ -3,7 +3,7 @@ declare const _default: {
|
|
|
3
3
|
angle: number;
|
|
4
4
|
castShadow: false;
|
|
5
5
|
color: 16777215;
|
|
6
|
-
intensity:
|
|
6
|
+
intensity: 15;
|
|
7
7
|
position: {
|
|
8
8
|
x: number;
|
|
9
9
|
y: number;
|
|
@@ -12,12 +12,12 @@ declare const _default: {
|
|
|
12
12
|
}>;
|
|
13
13
|
ambientLightSettings: Readonly<{
|
|
14
14
|
color: 16777215;
|
|
15
|
-
intensity:
|
|
15
|
+
intensity: 1.75;
|
|
16
16
|
}>;
|
|
17
17
|
reflectedLightSettings: Readonly<{
|
|
18
18
|
castShadow: false;
|
|
19
19
|
color: 16746666;
|
|
20
|
-
intensity:
|
|
20
|
+
intensity: 2;
|
|
21
21
|
position: {
|
|
22
22
|
x: number;
|
|
23
23
|
y: number;
|
|
@@ -27,7 +27,7 @@ declare const _default: {
|
|
|
27
27
|
fillLightSettings: Readonly<{
|
|
28
28
|
castShadow: false;
|
|
29
29
|
color: 15258025;
|
|
30
|
-
intensity:
|
|
30
|
+
intensity: 1.5;
|
|
31
31
|
position: {
|
|
32
32
|
x: number;
|
|
33
33
|
y: number;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { OMEAxis, OMEDataset, OMEMultiscale, TCZYX, ZarrSource } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Attempts to parse `color` as a 24-bit (6-digit) hexadecimal color with a possible leading `#`.
|
|
4
|
+
*
|
|
5
|
+
* Six-digit hex is the only allowable color representation in the OMERO metadata spec.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseHexColor(color: string | undefined): [number, number, number] | undefined;
|
|
2
8
|
/** Extracts channel names from a `ZarrSource`. Handles missing `omeroMetadata`. Does *not* resolve name collisions. */
|
|
3
|
-
export declare function
|
|
9
|
+
export declare function getSourceChannelMeta(src: ZarrSource): {
|
|
10
|
+
names: string[];
|
|
11
|
+
colors: ([number, number, number] | undefined)[];
|
|
12
|
+
};
|
|
4
13
|
/** Turns `axesTCZYX` into the number of dimensions in the array */
|
|
5
14
|
export declare const getDimensionCount: ([t, c, z]: TCZYX<number>) => number;
|
|
6
15
|
export declare function remapAxesToTCZYX(axes: OMEAxis[]): TCZYX<number>;
|