@aics/vole-core 4.4.0 → 4.5.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/Channel.js CHANGED
@@ -157,7 +157,7 @@ export default class Channel {
157
157
  };
158
158
  this.rebuildDataTexture(this.imgData.data, w, h);
159
159
  this.loaded = true;
160
- this.histogram = new Histogram(bitsArray);
160
+ this.histogram = new Histogram(bitsArray, rawMin, rawMax);
161
161
  this.frame = frame;
162
162
 
163
163
  // reuse old lut but auto-remap it to new data range
@@ -205,7 +205,7 @@ export default class Channel {
205
205
  this.loaded = true;
206
206
  // update from current histogram?
207
207
  this.setRawDataRange(rawMin, rawMax);
208
- this.histogram = new Histogram(this.volumeData);
208
+ this.histogram = new Histogram(this.volumeData, rawMin, rawMax);
209
209
  }
210
210
 
211
211
  // given this.volumeData, let's unpack it into a flat textureatlas and fill up this.imgData.
package/es/Histogram.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isFloatTypeArray } from "./types.js";
2
+ import { getDataRange } from "./utils/num_utils.js";
2
3
  const NBINS = 256;
3
4
  /**
4
5
  * Builds a histogram with 256 bins from a data array. Assume data is 8 bit single channel grayscale.
@@ -18,7 +19,7 @@ export default class Histogram {
18
19
 
19
20
  /** Index of the last bin (other than 0) with at least 1 value. */
20
21
 
21
- constructor(data) {
22
+ constructor(data, dataMin = undefined, dataMax = undefined) {
22
23
  this.dataMinBin = 0;
23
24
  this.dataMaxBin = 0;
24
25
  this.maxBin = 0;
@@ -28,7 +29,7 @@ export default class Histogram {
28
29
  this.binSize = 0;
29
30
 
30
31
  // build up the histogram
31
- const hinfo = Histogram.calculateHistogram(data, NBINS);
32
+ const hinfo = Histogram.calculateHistogram(data, NBINS, dataMin, dataMax);
32
33
  this.bins = hinfo.bins;
33
34
  this.min = hinfo.min;
34
35
  this.max = hinfo.max;
@@ -233,24 +234,19 @@ export default class Histogram {
233
234
  }
234
235
  return [b, e];
235
236
  }
236
- static calculateHistogram(arr, numBins = 1) {
237
+ static calculateHistogram(arr, numBins = 1, dataMin = undefined, dataMax = undefined) {
237
238
  if (numBins < 1) {
238
239
  numBins = 1;
239
240
  }
240
241
 
241
- // calculate min and max of arr
242
- // TODO See convertChannel, which will also compute min and max!
243
- // We could save a whole extra loop over the data, or have convertChannel compute the whole histogram.
244
- // need to be careful about computing over chunks or whole ready-to-display volume
242
+ // ASSUMPTION: we will trust the min and max if provided.
243
+ let min = dataMin !== undefined ? dataMin : arr[0];
244
+ let max = dataMax !== undefined ? dataMax : arr[0];
245
245
 
246
- let min = arr[0];
247
- let max = arr[0];
248
- for (let i = 1; i < arr.length; i++) {
249
- if (arr[i] < min) {
250
- min = arr[i];
251
- } else if (arr[i] > max) {
252
- max = arr[i];
253
- }
246
+ // Find min and max in the array if the user did not provide them.
247
+ // Note that this is a completely separate walk through the data array which could be expensive.
248
+ if (dataMin === undefined || dataMax === undefined) {
249
+ [min, max] = getDataRange(arr);
254
250
  }
255
251
  const bins = new Uint32Array(numBins).fill(0);
256
252
 
package/es/MeshVolume.js CHANGED
@@ -250,11 +250,12 @@ export default class MeshVolume {
250
250
  this.meshrep[channel] = null;
251
251
  }
252
252
  }
253
- saveChannelIsosurface(channelIndex, type, namePrefix) {
253
+ saveChannelIsosurface(channelIndex, type, name) {
254
254
  const meshrep = this.meshrep[channelIndex];
255
255
  if (!meshrep) {
256
256
  return;
257
257
  }
258
+ const namePrefix = name !== undefined ? `${name}_` : "";
258
259
  if (type === "STL") {
259
260
  this.exportSTL(meshrep, namePrefix + "_" + this.volume.channelNames[channelIndex]);
260
261
  } else if (type === "GLTF") {
@@ -1,7 +1,6 @@
1
+ import BaseDrawableMeshObject from "./BaseDrawableMeshObject.js";
2
+ import { MESH_NO_PICK_OCCLUSION_LAYER } from "./ThreeJsPanel.js";
1
3
  import { InstancedMesh, CylinderGeometry, ConeGeometry, Object3D, Vector3, MeshBasicMaterial, Color, DynamicDrawUsage, Matrix4 } from "three";
2
- import BaseDrawableMeshObject from "./BaseDrawableMeshObject";
3
- import { MESH_NO_PICK_OCCLUSION_LAYER } from "./ThreeJsPanel";
4
-
5
4
  // Unscaled arrowhead dimensions.
6
5
  const SHAFT_BASE_RADIUS = 0.5;
7
6
  const HEAD_BASE_RADIUS = 1.5;
@@ -3,6 +3,7 @@ import { ThreadableVolumeLoader } from "./IVolumeLoader.js";
3
3
  import { computeAtlasSize } from "../ImageInfo.js";
4
4
  import { isChunk } from "../VolumeCache.js";
5
5
  import { getDataRange } from "../utils/num_utils.js";
6
+ import { remapUri } from "../utils/url_utils.js";
6
7
 
7
8
  /* eslint-disable @typescript-eslint/naming-convention */
8
9
 
@@ -70,7 +71,7 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader {
70
71
  if (cachedInfo) {
71
72
  return cachedInfo;
72
73
  }
73
- const response = await fetch(this.urls[time]);
74
+ const response = await fetch(remapUri(this.urls[time]));
74
75
  const imageInfo = await response.json();
75
76
  imageInfo.pixel_size_unit = imageInfo.pixel_size_unit || "μm";
76
77
  imageInfo.times = imageInfo.times || this.urls.length;
@@ -194,7 +195,7 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader {
194
195
  if (cacheHit) {
195
196
  return;
196
197
  }
197
- const response = await fetch(image.name, {
198
+ const response = await fetch(remapUri(image.name), {
198
199
  mode: "cors"
199
200
  });
200
201
  const blob = await response.blob();
@@ -3,6 +3,7 @@ import * as zarr from "zarrita";
3
3
  const {
4
4
  slice
5
5
  } = zarr;
6
+ import { getDataRange } from "../utils/num_utils.js";
6
7
  import SubscribableRequestQueue from "../utils/SubscribableRequestQueue.js";
7
8
  import { ThreadableVolumeLoader } from "./IVolumeLoader.js";
8
9
  import { composeSubregion, computePackedAtlasDims, convertSubregionToPixels, pickLevelToLoad, unitNameToSymbol } from "./VolumeLoaderUtils.js";
@@ -11,23 +12,13 @@ import { getScale, getSourceChannelMeta, matchSourceScaleLevels, orderByDimensio
11
12
  import { VolumeLoadError, VolumeLoadErrorType, wrapVolumeLoadError } from "./VolumeLoadError.js";
12
13
  import wrapArray, { RelaxedFetchStore } from "./zarr_utils/wrappers.js";
13
14
  import { assertMetadataHasMultiscales, toOMEZarrMetaV4, validateOMEZarrMetadata } from "./zarr_utils/validation.js";
15
+ import { remapUri } from "../utils/url_utils.js";
14
16
  const CHUNK_REQUEST_CANCEL_REASON = "chunk request cancelled";
15
17
 
16
18
  // returns the converted data and the original min and max values
17
19
  function convertChannel(channelData, dtype) {
18
20
  // get min and max
19
- // TODO FIXME Histogram will also compute min and max!
20
- let min = channelData[0];
21
- let max = channelData[0];
22
- for (let i = 0; i < channelData.length; i++) {
23
- const val = channelData[i];
24
- if (val < min) {
25
- min = val;
26
- }
27
- if (val > max) {
28
- max = val;
29
- }
30
- }
21
+ const [min, max] = getDataRange(channelData);
31
22
  if (dtype === "float64") {
32
23
  // convert to float32
33
24
  const f32 = new Float32Array(channelData.length);
@@ -91,7 +82,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
91
82
  if (!queue) {
92
83
  queue = new SubscribableRequestQueue(fetchOptions?.concurrencyLimit, fetchOptions?.prefetchConcurrencyLimit);
93
84
  }
94
- const urlsArr = Array.isArray(urls) ? urls : [urls];
85
+ const urlsArr = (Array.isArray(urls) ? urls : [urls]).map(remapUri);
95
86
  const scenesArr = Array.isArray(scenes) ? scenes : [scenes];
96
87
 
97
88
  // Create one `ZarrSource` per URL
@@ -315,7 +306,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
315
306
  return dims;
316
307
  });
317
308
  const imgdata = {
318
- name: source0.omeroMetadata?.name || "Volume",
309
+ name: source0.omeroMetadata?.name,
319
310
  atlasTileDims: [atlasTileDims.x, atlasTileDims.y],
320
311
  subregionSize: [pxSizeLv.x, pxSizeLv.y, pxSizeLv.z],
321
312
  subregionOffset: [0, 0, 0],
@@ -468,7 +459,6 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
468
459
  const level = this.sources[sourceIdx].scaleLevels[multiscaleLevel];
469
460
  const sliceSpec = this.orderByDimension(unorderedSpec, sourceIdx);
470
461
  const reportChunk = (coords, sub) => reportChunkBase(sourceIdx, coords, sub);
471
- console.log(level);
472
462
  const result = await zarr.get(level, sliceSpec, {
473
463
  opts: {
474
464
  subscriber,
@@ -4,6 +4,7 @@ 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
+ import { remapUri } from "../utils/url_utils.js";
7
8
  function trimNull(xml) {
8
9
  // trim trailing unicode zeros?
9
10
  return xml && xml.trim().replace(/\0/g, "").trim();
@@ -21,6 +22,7 @@ function getOME(xml) {
21
22
  }
22
23
  }
23
24
  class OMEDims {
25
+ name = undefined;
24
26
  sizex = 0;
25
27
  sizey = 0;
26
28
  sizez = 1;
@@ -63,14 +65,15 @@ function getAttributeOrError(el, attr) {
63
65
  function getOMEDims(imageEl) {
64
66
  const dims = new OMEDims();
65
67
  const pixelsEl = imageEl.getElementsByTagName("Pixels")[0];
68
+ dims.name = imageEl.getAttribute("Name") ?? "";
66
69
  dims.sizex = Number(getAttributeOrError(pixelsEl, "SizeX"));
67
70
  dims.sizey = Number(getAttributeOrError(pixelsEl, "SizeY"));
68
71
  dims.sizez = Number(pixelsEl.getAttribute("SizeZ"));
69
72
  dims.sizec = Number(pixelsEl.getAttribute("SizeC"));
70
73
  dims.sizet = Number(pixelsEl.getAttribute("SizeT"));
71
- dims.unit = pixelsEl.getAttribute("PhysicalSizeXUnit") || "";
72
- dims.pixeltype = pixelsEl.getAttribute("Type") || "";
73
- dims.dimensionorder = pixelsEl.getAttribute("DimensionOrder") || "XYZCT";
74
+ dims.unit = pixelsEl.getAttribute("PhysicalSizeXUnit") ?? "";
75
+ dims.pixeltype = pixelsEl.getAttribute("Type") ?? "";
76
+ dims.dimensionorder = pixelsEl.getAttribute("DimensionOrder") ?? "XYZCT";
74
77
  dims.pixelsizex = Number(pixelsEl.getAttribute("PhysicalSizeX"));
75
78
  dims.pixelsizey = Number(pixelsEl.getAttribute("PhysicalSizeY"));
76
79
  dims.pixelsizez = Number(pixelsEl.getAttribute("PhysicalSizeZ"));
@@ -90,7 +93,7 @@ const getPixelType = pxSize => pxSize === 1 ? "uint8" : pxSize === 2 ? "uint16"
90
93
  class TiffLoader extends ThreadableVolumeLoader {
91
94
  constructor(url) {
92
95
  super();
93
- this.url = url;
96
+ this.url = url.map(remapUri);
94
97
  }
95
98
  async loadOmeDims() {
96
99
  if (!this.dims) {
@@ -178,7 +181,7 @@ class TiffLoader extends ThreadableVolumeLoader {
178
181
  // load tiff and check metadata
179
182
  const numChannelsPerSource = this.url.length > 1 ? Array(this.url.length).fill(1) : [dims.sizec];
180
183
  const imgdata = {
181
- name: "TEST",
184
+ name: dims.name,
182
185
  atlasTileDims: [atlasDims.x, atlasDims.y],
183
186
  subregionSize: [tilesizex, tilesizey, dims.sizez],
184
187
  subregionOffset: [0, 0, 0],
@@ -188,7 +191,7 @@ class TiffLoader extends ThreadableVolumeLoader {
188
191
  multiscaleLevelDims: [{
189
192
  shape: [dims.sizet, dims.sizec, dims.sizez, tilesizey, tilesizex],
190
193
  spacing: [1, 1, dims.pixelsizez, dims.pixelsizey * dims.sizey / tilesizey, dims.pixelsizex * dims.sizex / tilesizex],
191
- spaceUnit: dims.unit || "",
194
+ spaceUnit: dims.unit ?? "",
192
195
  timeUnit: "",
193
196
  dataType: getDtype(dims.pixeltype)
194
197
  }],
@@ -1,5 +1,5 @@
1
1
  import { Euler, Group, Mesh, Vector3 } from "three";
2
- import { IDrawableObject } from "./types";
2
+ import { IDrawableObject } from "./types.js";
3
3
  /**
4
4
  * Abstract base class for drawable 3D mesh objects.
5
5
  *
@@ -18,7 +18,7 @@ export default class Histogram {
18
18
  private dataMaxBin;
19
19
  private pixelCount;
20
20
  maxBin: number;
21
- constructor(data: TypedArray<NumberType>);
21
+ constructor(data: TypedArray<NumberType>, dataMin?: number | undefined, dataMax?: number | undefined);
22
22
  private static findBin;
23
23
  /**
24
24
  * Returns the integer bin index for the given value. If a value is outside
@@ -1,7 +1,7 @@
1
1
  import { type VolumeDims } from "./VolumeDims.js";
2
2
  import { Vector3, Vector2 } from "three";
3
3
  export type ImageInfo = Readonly<{
4
- name: string;
4
+ name: string | undefined;
5
5
  /**
6
6
  * XY dimensions of the texture atlas used by `RayMarchedAtlasVolume` and
7
7
  * `Atlas2DSlice`, in number of z-slice tiles (not pixels). Chosen by the
@@ -34,7 +34,7 @@ export default class MeshVolume implements IDrawableObject {
34
34
  hasIsosurface(channel: number): boolean;
35
35
  createIsosurface(channel: number, color: [number, number, number], value?: number, alpha?: number, transp?: boolean): void;
36
36
  destroyIsosurface(channel: number): void;
37
- saveChannelIsosurface(channelIndex: number, type: string, namePrefix: string): void;
37
+ saveChannelIsosurface(channelIndex: number, type: string, name?: string): void;
38
38
  exportSTL(input: Object3D, fname: string): void;
39
39
  exportGLTF(input: Object3D, fname: string): void;
40
40
  generateIsosurfaceGeometry(channelIndex: number, isovalue: number): BufferGeometry[];
@@ -1,6 +1,6 @@
1
+ import BaseDrawableMeshObject from "./BaseDrawableMeshObject.js";
1
2
  import { Vector3, Color } from "three";
2
- import { IDrawableObject } from "./types";
3
- import BaseDrawableMeshObject from "./BaseDrawableMeshObject";
3
+ import { IDrawableObject } from "./types.js";
4
4
  /**
5
5
  * A drawable vector arrow field, which uses instanced meshes for performance.
6
6
  */
@@ -25,7 +25,7 @@ export default class Volume {
25
25
  loadSpecRequired: Required<LoadSpec>;
26
26
  channelLoadCallback?: PerChannelCallback;
27
27
  imageMetadata: Record<string, unknown>;
28
- name: string;
28
+ name: string | undefined;
29
29
  channels: Channel[];
30
30
  numChannels: number;
31
31
  channelNames: string[];
@@ -3,6 +3,7 @@ import { type ImageInfo } from "../ImageInfo.js";
3
3
  import type { VolumeDims } from "../VolumeDims.js";
4
4
  import { TypedArray, NumberType } from "../types.js";
5
5
  declare class OMEDims {
6
+ name: string | undefined;
6
7
  sizex: number;
7
8
  sizey: number;
8
9
  sizez: number;
@@ -34,7 +35,7 @@ export type TiffLoadResult = {
34
35
  range: [number, number];
35
36
  };
36
37
  declare class TiffLoader extends ThreadableVolumeLoader {
37
- url: string[];
38
+ private url;
38
39
  dims?: OMEDims;
39
40
  constructor(url: string[]);
40
41
  private loadOmeDims;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Remaps non-standard URIs (e.g. S3 (`s3://`), Google Cloud Storage (`gs://`), or
3
+ * VAST files (`/allen/aics/`)) to a standard HTTPS URL.
4
+ */
5
+ export declare function remapUri(url: string): string;
@@ -224,8 +224,12 @@ export function getDataRange(data) {
224
224
  let min = data[0];
225
225
  let max = data[0];
226
226
  for (let i = 1; i < data.length; i++) {
227
- min = Math.min(min, data[i]);
228
- max = Math.max(max, data[i]);
227
+ const value = data[i];
228
+ if (value < min) {
229
+ min = value;
230
+ } else if (value > max) {
231
+ max = value;
232
+ }
229
233
  }
230
234
  return [min, max];
231
235
  }
@@ -0,0 +1,25 @@
1
+ const S3_URL_PREFIX = "s3://";
2
+ const GCS_URL_PREFIX = "gs://";
3
+ const VAST_FILES_PREFIX = "/allen/aics/";
4
+ const VAST_FILES_URL = "https://vast-files.int.allencell.org/";
5
+
6
+ /**
7
+ * Remaps non-standard URIs (e.g. S3 (`s3://`), Google Cloud Storage (`gs://`), or
8
+ * VAST files (`/allen/aics/`)) to a standard HTTPS URL.
9
+ */
10
+ export function remapUri(url) {
11
+ let newUrl = url.trim();
12
+ if (newUrl.startsWith(S3_URL_PREFIX)) {
13
+ // remap s3://bucket/key to https://bucket.s3.amazonaws.com/key
14
+ const s3Path = newUrl.slice(S3_URL_PREFIX.length);
15
+ const pathSegments = s3Path.split("/");
16
+ newUrl = `https://${pathSegments[0]}.s3.amazonaws.com/${pathSegments.slice(1).join("/")}`;
17
+ } else if (newUrl.startsWith(GCS_URL_PREFIX)) {
18
+ // remap gs://bucket/key to https://storage.googleapis.com/bucket/key
19
+ newUrl = newUrl.replace(GCS_URL_PREFIX, "https://storage.googleapis.com/");
20
+ } else if (newUrl.startsWith(VAST_FILES_PREFIX)) {
21
+ // remap /allen/aics/... to https://vast-files.int.allencell.org/...
22
+ newUrl = newUrl.replace(VAST_FILES_PREFIX, VAST_FILES_URL);
23
+ }
24
+ return newUrl;
25
+ }
@@ -1,6 +1,7 @@
1
1
  import { fromUrl } from "geotiff";
2
2
  import { serializeError } from "serialize-error";
3
3
  import { VolumeLoadError, VolumeLoadErrorType } from "../loaders/VolumeLoadError.js";
4
+ import { getDataRange } from "../utils/num_utils.js";
4
5
  // from TIFF
5
6
  const SAMPLEFORMAT_UINT = 1;
6
7
  const SAMPLEFORMAT_INT = 2;
@@ -121,17 +122,7 @@ async function loadTiffChannel(e) {
121
122
  // all slices collected, now resample to 8 bits full data range
122
123
  const src = castToArray(buffer, bytesPerPixel, sampleFormat);
123
124
  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
- }
125
+ const [chmin, chmax] = getDataRange(src);
135
126
  return {
136
127
  data: src,
137
128
  channel: channelIndex,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aics/vole-core",
3
- "version": "4.4.0",
3
+ "version": "4.5.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",