@aics/vole-core 3.15.6 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/es/ContourPass.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Color, DataTexture, FloatType, RedIntegerFormat, RGBAFormat, Uniform, UnsignedIntType } from "three";
2
- import RenderToBuffer, { RenderPassType } from "./RenderToBuffer";
2
+ import { clamp } from "three/src/math/MathUtils.js";
3
+ import RenderToBuffer, { RenderPassType } from "./RenderToBuffer.js";
3
4
  /* babel-plugin-inline-import './constants/shaders/contour.frag' */
4
5
  const contourFragShader = "precision highp float;\nprecision highp int;\nprecision highp usampler2D;\nprecision highp sampler3D;\n\n/**\n * LUT mapping from the segmentation ID (raw pixel value) to the\n * global ID (index in data buffers like `featureData` and `outlierData`).\n * \n * For a given local pixel ID `localId`, the global ID is given by:\n * `localIdToGlobalId[localId - localIdOffset] - 1`.\n*/\nuniform usampler2D localIdToGlobalId;\nuniform uint localIdOffset;\nuniform bool useGlobalIdLookup;\n/* Pick buffer. Used to determine IDs. */\nuniform sampler2D pickBuffer;\n\nuniform int highlightedId;\nuniform int outlineThickness;\nuniform float outlineAlpha;\nuniform vec3 outlineColor;\nuniform float devicePixelRatio;\n\nconst uint BACKGROUND_ID = 0u;\nconst uint MISSING_DATA_ID = 0xFFFFFFFFu;\nconst int ID_OFFSET = 1;\n\nuvec4 getUintFromTex(usampler2D tex, int index) {\n int width = textureSize(tex, 0).x;\n ivec2 featurePos = ivec2(index % width, index / width);\n return uvec4(texelFetch(tex, featurePos, 0));\n}\n\nuint getId(ivec2 uv) {\n float rawId = texelFetch(pickBuffer, uv, 0).g;\n if (rawId == 0.0) {\n return BACKGROUND_ID;\n }\n int localId = int(rawId) - int(localIdOffset);\n if (!useGlobalIdLookup) {\n return uint(localId + ID_OFFSET);\n }\n uvec4 c = getUintFromTex(localIdToGlobalId, localId);\n // Note: IDs are offset by `ID_OFFSET` (`=1`) to reserve `0` for local IDs\n // that don't have associated data in the global lookup. `ID_OFFSET` MUST be\n // subtracted from the ID when accessing data buffers.\n uint globalId = c.r;\n if (globalId == 0u) {\n return MISSING_DATA_ID;\n }\n return globalId;\n}\n\nbool isEdge(ivec2 uv, int id, int thickness) {\n float wStep = 1.0;\n float hStep = 1.0;\n float thicknessFloat = float(thickness);\n // sample around the pixel to see if we are on an edge\n int R = int(getId(uv + ivec2(thicknessFloat * wStep, 0))) - ID_OFFSET;\n int L = int(getId(uv + ivec2(-thicknessFloat * wStep, 0))) - ID_OFFSET;\n int T = int(getId(uv + ivec2(0, thicknessFloat * hStep))) - ID_OFFSET;\n int B = int(getId(uv + ivec2(0, -thicknessFloat * hStep))) - ID_OFFSET;\n // if any neighbors are not id then this is an edge\n return id != -1 && (R != id || L != id || T != id || B != id);\n}\n\nvoid main(void) {\n ivec2 vUv = ivec2(int(gl_FragCoord.x / devicePixelRatio), int(gl_FragCoord.y / devicePixelRatio));\n\n uint rawId = getId(vUv);\n int id = int(rawId) - ID_OFFSET;\n\n if (id == highlightedId && isEdge(vUv, id, outlineThickness)) {\n gl_FragColor = vec4(outlineColor, outlineAlpha);\n } else {\n gl_FragColor = vec4(0, 0, 0, 0.0);\n }\n}";
5
- import { clamp } from "three/src/math/MathUtils";
6
6
  const makeDefaultUniforms = () => {
7
7
  const pickBufferTex = new DataTexture(new Float32Array([1, 0, 0, 0]), 1, 1, RGBAFormat, FloatType);
8
8
  const localIdToGlobalId = new DataTexture(new Uint32Array([0]), 1, 1, RedIntegerFormat, UnsignedIntType);
package/es/Histogram.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isFloatTypeArray } from "./types.js";
1
2
  const NBINS = 256;
2
3
  /**
3
4
  * Builds a histogram with 256 bins from a data array. Assume data is 8 bit single channel grayscale.
@@ -59,20 +60,38 @@ export default class Histogram {
59
60
  }
60
61
  }
61
62
  }
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--;
63
+ static findBin(dataValue, dataMin, binSize, castToInt) {
64
+ const binIndex = (dataValue - dataMin) / binSize;
65
+ if (!castToInt) {
66
+ return binIndex;
69
67
  }
70
- return binIndex;
68
+ return Math.max(Math.min(Math.floor(binIndex), NBINS - 1), 0);
71
69
  }
72
70
 
73
- // return the bin index of the given data value
71
+ /**
72
+ * Returns the integer bin index for the given value. If a value is outside
73
+ * the histogram range, it will be clamped to the nearest bin.
74
+ */
74
75
  findBinOfValue(value) {
75
- return Histogram.findBin(value, this.min, this.binSize, NBINS);
76
+ return Histogram.findBin(value, this.min, this.binSize, true);
77
+ }
78
+
79
+ /**
80
+ * Returns a fractional bin index for the given value. If a value is not a bin
81
+ * boundary, returns an interpolated index. Note that this can return a value
82
+ * outside the range of valid bins.
83
+ */
84
+ findFractionalBinOfValue(value) {
85
+ return Histogram.findBin(value, this.min, this.binSize, false);
86
+ }
87
+
88
+ /**
89
+ * Returns an absolute data value from a given (integer or fractional) bin index.
90
+ * Note that, if the bin index is outside of the bin range, the returned value
91
+ * will also be outside the value range.
92
+ */
93
+ getValueFromBinIndex(binIndex) {
94
+ return this.min + binIndex * this.binSize;
76
95
  }
77
96
 
78
97
  /**
@@ -234,10 +253,34 @@ export default class Histogram {
234
253
  }
235
254
  }
236
255
  const bins = new Uint32Array(numBins).fill(0);
237
- const binSize = (max - min) / numBins === 0 ? 1 : (max - min) / numBins;
256
+
257
+ // Bins should have equal widths and span the data min to the data max. The
258
+ // handling of the max value is slightly different between integers and
259
+ // floats; in the float case, the last bin should have `max` as its
260
+ // inclusive upper bound, while in the integer case, the last bin should
261
+ // have an exclusive upper bound of `max + 1`.
262
+ //
263
+ // For example, let's say we have a data range of `[min=0, max=3]` and 4
264
+ // bins.
265
+ //
266
+ // If this is integer data, we want each bin to have ranges [0, 1), [1, 2),
267
+ // [2, 3), [3, 4), and a bin size of 1.
268
+ //
269
+ // |----|----|----|----|
270
+ // 0 1 2 3 4 <- exclusive
271
+ //
272
+ // For continuous (float) data, our bins should have ranges [0, 0.75),
273
+ // [0.75, 1.5), [1.5, 2.25), [2.25, 3] and a bin size of 0.75.
274
+ //
275
+ // |----|----|----|----|
276
+ // 0 0.75 1.5 2.25 3 <- inclusive
277
+ //
278
+
279
+ const binMax = isFloatTypeArray(arr) ? max : max + 1;
280
+ const binSize = binMax <= min ? 1 : (binMax - min) / numBins;
238
281
  for (let i = 0; i < arr.length; i++) {
239
282
  const item = arr[i];
240
- const binIndex = Histogram.findBin(item, min, binSize, numBins);
283
+ const binIndex = Histogram.findBin(item, min, binSize, true);
241
284
  bins[binIndex]++;
242
285
  }
243
286
  return {
package/es/Line3d.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Group, Vector3 } from "three";
2
- import { LineMaterial } from "three/addons/lines/LineMaterial";
3
- import { MESH_NO_PICK_OCCLUSION_LAYER, OVERLAY_LAYER } from "./ThreeJsPanel";
4
- import { LineSegments2 } from "three/addons/lines/LineSegments2";
5
- import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry";
2
+ import { LineMaterial } from "three/addons/lines/LineMaterial.js";
3
+ import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
4
+ import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
5
+ import { MESH_NO_PICK_OCCLUSION_LAYER, OVERLAY_LAYER } from "./ThreeJsPanel.js";
6
6
  const DEFAULT_VERTEX_BUFFER_SIZE = 1020;
7
7
 
8
8
  /**
@@ -11,15 +11,16 @@ function prepareXML(xml) {
11
11
  return xml.trim().replace(expr, "").trim();
12
12
  }
13
13
  function getOME(xml) {
14
+ if (xml === undefined) {
15
+ return undefined;
16
+ }
17
+ const prepared = prepareXML(xml);
14
18
  const parser = new DOMParser();
15
19
  try {
16
- const xmlDoc = parser.parseFromString(xml, "text/xml");
20
+ const xmlDoc = parser.parseFromString(prepared, "text/xml");
17
21
  return xmlDoc.getElementsByTagName("OME")[0];
18
22
  } catch (e) {
19
- throw new VolumeLoadError("Could not find OME metadata in TIFF file", {
20
- type: VolumeLoadErrorType.INVALID_METADATA,
21
- cause: e
22
- });
23
+ return undefined;
23
24
  }
24
25
  }
25
26
  class OMEDims {
@@ -85,6 +86,7 @@ function getOMEDims(imageEl) {
85
86
  return dims;
86
87
  }
87
88
  const getBytesPerSample = type => type === "uint8" ? 1 : type === "uint16" ? 2 : 4;
89
+ const getPixelType = pxSize => pxSize === 1 ? "uint8" : pxSize === 2 ? "uint16" : "uint32";
88
90
 
89
91
  // Despite the class `TiffLoader` extends, this loader is not threadable, since geotiff internally uses features that
90
92
  // aren't available on workers. It uses its own specialized workers anyways.
@@ -95,17 +97,32 @@ class TiffLoader extends ThreadableVolumeLoader {
95
97
  }
96
98
  async loadOmeDims() {
97
99
  if (!this.dims) {
98
- const tiff = await fromUrl(this.url, {
100
+ const tiff = await fromUrl(this.url[0], {
99
101
  allowFullFile: true
100
- }).catch(wrapVolumeLoadError(`Could not open TIFF file at ${this.url}`, VolumeLoadErrorType.NOT_FOUND));
102
+ }).catch(wrapVolumeLoadError(`Could not open TIFF file at ${this.url[0]}`, VolumeLoadErrorType.NOT_FOUND));
101
103
  // DO NOT DO THIS, ITS SLOW
102
104
  // const imagecount = await tiff.getImageCount();
103
105
  // read the FIRST image
104
106
  const image = await tiff.getImage().catch(wrapVolumeLoadError("Failed to open TIFF image", VolumeLoadErrorType.NOT_FOUND));
105
- const tiffimgdesc = prepareXML(image.getFileDirectory().ImageDescription);
106
- const omeEl = getOME(tiffimgdesc);
107
- const image0El = omeEl.getElementsByTagName("Image")[0];
108
- this.dims = getOMEDims(image0El);
107
+ const omeEl = getOME(image.getFileDirectory().ImageDescription);
108
+ if (omeEl !== undefined) {
109
+ const image0El = omeEl.getElementsByTagName("Image")[0];
110
+ this.dims = getOMEDims(image0El);
111
+ } else {
112
+ console.warn("Could not read OME-TIFF metadata from file. Doing our best with base TIFF metadata.");
113
+ this.dims = new OMEDims();
114
+ this.dims.sizex = image.getWidth();
115
+ this.dims.sizey = image.getHeight();
116
+ // TODO this is a big hack/assumption about only loading multi-source tiffs that are not OMETIFF.
117
+ // We really have to check each url in the array for sizec to get the total number of channels
118
+ // See combinedNumChannels in ImageInfo below.
119
+ // Also compare with how OMEZarrLoader does this.
120
+ this.dims.sizec = this.url.length > 1 ? this.url.length : 1; // if multiple urls, assume one channel per url
121
+ this.dims.pixeltype = getPixelType(image.getBytesPerPixel());
122
+ this.dims.channelnames = Array.from({
123
+ length: this.dims.sizec
124
+ }, (_, i) => "Channel" + i);
125
+ }
109
126
  }
110
127
  return this.dims;
111
128
  }
@@ -189,7 +206,7 @@ class TiffLoader extends ThreadableVolumeLoader {
189
206
  sizez: volumeSize.z,
190
207
  dimensionOrder: dims.dimensionorder,
191
208
  bytesPerSample: getBytesPerSample(dims.pixeltype),
192
- url: this.url
209
+ url: this.url.length > 1 ? this.url[channel] : this.url[0] // if multiple urls, use the channel index to select the right one
193
210
  };
194
211
  const worker = new Worker(new URL("../workers/FetchTiffWorker", import.meta.url), {
195
212
  type: "module"
@@ -24,13 +24,14 @@ export function pathToFileType(path) {
24
24
  export async function createVolumeLoader(path, options) {
25
25
  const pathString = Array.isArray(path) ? path[0] : path;
26
26
  const fileType = options?.fileType || pathToFileType(pathString);
27
+ const pathArrayForTiffLoader = Array.isArray(path) ? path : [path];
27
28
  switch (fileType) {
28
29
  case VolumeFileFormat.ZARR:
29
30
  return await OMEZarrLoader.createLoader(path, options?.scene, options?.cache, options?.queue, options?.fetchOptions);
30
31
  case VolumeFileFormat.JSON:
31
32
  return new JsonImageInfoLoader(path, options?.cache);
32
33
  case VolumeFileFormat.TIFF:
33
- return new TiffLoader(pathString);
34
+ return new TiffLoader(pathArrayForTiffLoader);
34
35
  case VolumeFileFormat.DATA:
35
36
  if (!options?.rawArrayOptions) {
36
37
  throw new Error("Must provide RawArrayOptions for RawArrayLoader");
@@ -1,5 +1,5 @@
1
1
  import { Color, WebGLRenderer, WebGLRenderTarget } from "three";
2
- import { ColorizeFeature } from "./types";
2
+ import { ColorizeFeature } from "./types.js";
3
3
  export default class ContourPass {
4
4
  private pass;
5
5
  private frameToGlobalIdLookup;
@@ -1,4 +1,4 @@
1
- import type { TypedArray, NumberType } from "./types.js";
1
+ import { type TypedArray, type NumberType } from "./types.js";
2
2
  /**
3
3
  * Builds a histogram with 256 bins from a data array. Assume data is 8 bit single channel grayscale.
4
4
  * @class
@@ -19,8 +19,24 @@ export default class Histogram {
19
19
  private pixelCount;
20
20
  maxBin: number;
21
21
  constructor(data: TypedArray<NumberType>);
22
- static findBin(dataValue: number, dataMin: number, binSize: number, numBins: number): number;
22
+ private static findBin;
23
+ /**
24
+ * Returns the integer bin index for the given value. If a value is outside
25
+ * the histogram range, it will be clamped to the nearest bin.
26
+ */
23
27
  findBinOfValue(value: number): number;
28
+ /**
29
+ * Returns a fractional bin index for the given value. If a value is not a bin
30
+ * boundary, returns an interpolated index. Note that this can return a value
31
+ * outside the range of valid bins.
32
+ */
33
+ findFractionalBinOfValue(value: number): number;
34
+ /**
35
+ * Returns an absolute data value from a given (integer or fractional) bin index.
36
+ * Note that, if the bin index is outside of the bin range, the returned value
37
+ * will also be outside the value range.
38
+ */
39
+ getValueFromBinIndex(binIndex: number): number;
24
40
  /**
25
41
  * Return the min data value
26
42
  * @return {number}
@@ -1,5 +1,5 @@
1
1
  import { Color, Euler, Group, Vector3 } from "three";
2
- import { IDrawableObject } from "./types";
2
+ import { IDrawableObject } from "./types.js";
3
3
  /**
4
4
  * Simple wrapper for a 3D line segments object, with controls for vertex data,
5
5
  * color, width, and segments visible.
@@ -34,9 +34,9 @@ export type TiffLoadResult = {
34
34
  range: [number, number];
35
35
  };
36
36
  declare class TiffLoader extends ThreadableVolumeLoader {
37
- url: string;
37
+ url: string[];
38
38
  dims?: OMEDims;
39
- constructor(url: string);
39
+ constructor(url: string[]);
40
40
  private loadOmeDims;
41
41
  loadDims(_loadSpec: LoadSpec): Promise<VolumeDims[]>;
42
42
  createImageInfo(_loadSpec: LoadSpec): Promise<LoadedVolumeInfo>;
@@ -27,6 +27,7 @@ export declare const ARRAY_CONSTRUCTORS: {
27
27
  float32: Float32ArrayConstructor;
28
28
  float64: Float64ArrayConstructor;
29
29
  };
30
+ export declare function isFloatTypeArray(array: TypedArray<NumberType>): array is Float32Array | Float64Array;
30
31
  export interface ColorizeFeature {
31
32
  idsToFeatureValue: DataTexture;
32
33
  featureValueToColor: DataTexture;
package/es/types.js CHANGED
@@ -13,6 +13,9 @@ export const ARRAY_CONSTRUCTORS = {
13
13
  float32: Float32Array,
14
14
  float64: Float64Array
15
15
  };
16
+ export function isFloatTypeArray(array) {
17
+ return array instanceof Float32Array || array instanceof Float64Array;
18
+ }
16
19
  /** If `FuseChannel.rgbColor` is this value, it is disabled from fusion. */
17
20
  export const FUSE_DISABLED_RGB_COLOR = 0;
18
21
 
@@ -164,7 +164,9 @@ class VolumeLoaderContext {
164
164
  const pathString = Array.isArray(path) ? path[0] : path;
165
165
  const fileType = options?.fileType || pathToFileType(pathString);
166
166
  if (fileType === VolumeFileFormat.TIFF) {
167
- return new TiffLoader(pathString);
167
+ // tiff loader accepts array of paths for separate channel sources
168
+ const pathArray = Array.isArray(path) ? path : [path];
169
+ return new TiffLoader(pathArray);
168
170
  } else if (fileType === VolumeFileFormat.DATA) {
169
171
  if (!options?.rawArrayOptions) {
170
172
  throw new Error("Failed to create loader: Must provide RawArrayOptions for RawArrayLoader");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aics/vole-core",
3
- "version": "3.15.6",
3
+ "version": "4.0.0",
4
4
  "description": "volume renderer for 3d, 4d, or 5d imaging data with OME-Zarr support",
5
5
  "main": "es/index.js",
6
6
  "type": "module",