Skip to content

Commit

Permalink
non-destructively this time
Browse files Browse the repository at this point in the history
  • Loading branch information
TheMooseman committed Nov 14, 2024
1 parent 7dd3ecf commit 31491a2
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 8 deletions.
3 changes: 2 additions & 1 deletion examples/src/common/loaders/ome-zarr/fetchSlice.worker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// a web-worker which fetches slices of data, decodes them, and returns the result as a flat float32 array, using transferables
import type { Chunk } from 'zarrita';
import { getSlice, type ZarrDataset, type ZarrRequest } from './zarr-data';
import { type ZarrDataset, type ZarrRequest } from '@alleninstitute/vis-omezarr';
import { getSlice } from './zarr-data';

const ctx = self;
type ZarrSliceRequest = {
Expand Down
2 changes: 1 addition & 1 deletion examples/src/common/loaders/ome-zarr/sliceWorkerPool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { uniqueId } from 'lodash';
import type { ZarrDataset, ZarrRequest } from './zarr-data';
import type { ZarrDataset, ZarrRequest } from '@alleninstitute/vis-omezarr';

type PromisifiedMessage = {
requestCacheKey: string;
Expand Down
236 changes: 236 additions & 0 deletions examples/src/common/loaders/ome-zarr/zarr-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// lets make some easy to understand utils to access .zarr data stored in an s3 bucket somewhere
// import { HTTPStore, NestedArray, type TypedArray, openArray, openGroup, slice } from "zarr";
import * as zarr from 'zarrita';
import { some } from 'lodash';
import { Box2D, type Interval, Vec2, type box2D, limit, type vec2 } from '@alleninstitute/vis-geometry';
import type { AxisAlignedPlane } from '~/data-renderers/versa-renderer';

// documentation for ome-zarr datasets (from which these types are built)
// can be found here:
// https://ngff.openmicroscopy.org/latest/#multiscale-md
//
export type ZarrDataset = Awaited<ReturnType<typeof loadMetadata>>;
type AxisDesc = {
name: string; // x or y or z or time or ?
type: string; // space or time or ?
unit: string; // see list of possible units: https://ngff.openmicroscopy.org/latest/#axes-md
};

// todo, there are other types of coordinate transforms...
type ScaleTransform = {
type: 'scale';
scale: ReadonlyArray<number>;
};

function isScaleTransform(trn: unknown): trn is ScaleTransform {
if (typeof trn === 'object' && trn !== null) {
const scaleTransform = trn as ScaleTransform;
return scaleTransform.type === 'scale' && scaleTransform.scale !== undefined;
}
return false;
}
type DatasetDesc = {
path: string;
coordinateTransformations: ReadonlyArray<ScaleTransform | unknown>;
};
type DatasetWithShape = DatasetDesc & {
shape: number[];
};
type ZarrAttr = {
axes: ReadonlyArray<AxisDesc>;
datasets: ReadonlyArray<DatasetDesc>;
};
type ZarrAttrs = {
multiscales: ReadonlyArray<ZarrAttr>;
};

async function getRawInfo(store: zarr.FetchStore) {
const group = await zarr.open(store, { kind: 'group' });
return group.attrs as ZarrAttrs;
// TODO HACK ALERT: I am once again doing the thing that I hate, in which I promise to my friend Typescript that
// the junk I just pulled out of this internet file is exactly what I expect it to be: :fingers_crossed:
}

async function mapAsync<T, R>(arr: ReadonlyArray<T>, fn: (t: T, index: number) => Promise<R>) {
return Promise.all(arr.map((v, i) => fn(v, i)));
}
// return the mapping from path (aka resolution group???) to the dimensional shape of the data
async function loadMetadata(url: string) {
const store = new zarr.FetchStore(url);
const root = zarr.root(store);
const attrs: ZarrAttrs = await getRawInfo(store);
const addShapeToDesc = async (d: DatasetDesc) => ({
...d,
shape: (await zarr.open(root.resolve(d.path), { kind: 'array' })).shape,
});
return {
url,
multiscales: await mapAsync(attrs.multiscales, async (attr) => ({
...attr,
datasets: await mapAsync<DatasetDesc, DatasetWithShape>(attr.datasets, addShapeToDesc),
})),
};
}

type OmeDimension = 'x' | 'y' | 'z' | 't' | 'c';
const uvTable = {
xy: { u: 'x', v: 'y' },
xz: { u: 'x', v: 'z' },
yz: { u: 'y', v: 'z' },
} as const;
const sliceDimension = {
xy: 'z',
xz: 'y',
yz: 'x',
} as const;
export function uvForPlane(plane: AxisAlignedPlane) {
return uvTable[plane];
}
export function sliceDimensionForPlane(plane: AxisAlignedPlane) {
return sliceDimension[plane];
}
export type ZarrRequest = Record<OmeDimension, number | Interval | null>;
export function pickBestScale(
dataset: ZarrDataset,
plane: {
u: OmeDimension;
v: OmeDimension;
},
relativeView: box2D, // a box in data-unit-space
// in the plane given above
displayResolution: vec2
) {
const datasets = dataset.multiscales[0].datasets;
const axes = dataset.multiscales[0].axes;
const realSize = sizeInUnits(plane, axes, datasets[0])!;

const vxlPitch = (size: vec2) => Vec2.div(realSize, size);
// size, in dataspace, of a pixel 1/res
const pxPitch = Vec2.div(Box2D.size(relativeView), displayResolution);
const dstToDesired = (a: vec2, goal: vec2) => {
const diff = Vec2.sub(a, goal);
if (diff[0] * diff[1] > 0) {
// the res (a) is higher than our goal -
// weight this heavily to prefer smaller than the goal
return 1000 * Vec2.length(Vec2.sub(a, goal));
}
return Vec2.length(Vec2.sub(a, goal));
};
// we assume the datasets are ordered... hmmm TODO
const choice = datasets.reduce(
(bestSoFar, cur) =>
dstToDesired(vxlPitch(planeSizeInVoxels(plane, axes, bestSoFar)!), pxPitch) >
dstToDesired(vxlPitch(planeSizeInVoxels(plane, axes, cur)!), pxPitch)
? cur
: bestSoFar,
datasets[0]
);
return choice ?? datasets[datasets.length - 1];
}
function indexFor(dim: OmeDimension, axes: readonly AxisDesc[]) {
return axes.findIndex((axe) => axe.name === dim);
}

export function sizeInUnits(
plane:
| AxisAlignedPlane
| {
u: OmeDimension;
v: OmeDimension;
},
axes: readonly AxisDesc[],
dataset: DatasetWithShape
): vec2 | undefined {
const planeUV = typeof plane === 'string' ? uvForPlane(plane) : plane;
const vxls = planeSizeInVoxels(planeUV, axes, dataset);

if (vxls === undefined) return undefined;
let size: vec2 = vxls;
// now, just apply the correct transforms, if they exist...

dataset.coordinateTransformations.forEach((trn) => {
if (isScaleTransform(trn)) {
// try to apply it!
const uIndex = indexOfDimension(axes, planeUV.u);
const vIndex = indexOfDimension(axes, planeUV.v);
size = Vec2.mul(size, [trn.scale[uIndex], trn.scale[vIndex]]);
}
});
return size;
}
export function sizeInVoxels(dim: OmeDimension, axes: readonly AxisDesc[], dataset: DatasetWithShape) {
const uI = indexFor(dim, axes);
if (uI === -1) return undefined;

return dataset.shape[uI];
}
export function planeSizeInVoxels(
plane: {
u: OmeDimension;
v: OmeDimension;
},
axes: readonly AxisDesc[],
dataset: DatasetWithShape
): vec2 | undefined {
// first - u&v must not refer to the same dimension,
// and both should exist in the axes...
const { u, v } = plane;
if (u === v) return undefined;
const uI = indexFor(u, axes);
const vI = indexFor(v, axes);

if (uI === -1 || vI === -1) return undefined;

return [dataset.shape[uI], dataset.shape[vI]] as const;
}
// feel free to freak out if the request is over or under determined or whatever
function buildQuery(r: Readonly<ZarrRequest>, axes: readonly AxisDesc[], shape: number[]) {
const ordered = axes.map((a) => r[a.name as OmeDimension]);
// if any are undefined, throw up
if (some(ordered, (a) => a === undefined)) {
throw new Error('request does not match expected dimensions of ome-zarr dataset!');
}

return ordered.map((d, i) => {
const bounds = { min: 0, max: shape[i] };
if (d === null) {
return d;
} else if (typeof d === 'number') {
return limit(bounds, d);
}
return zarr.slice(limit(bounds, d.min), limit(bounds, d.max));
});
}

export async function explain(z: ZarrDataset) {
console.dir(z);
const root = zarr.root(new zarr.FetchStore(z.url));
for (const d of z.multiscales[0].datasets) {
zarr.open(root.resolve(d.path), { kind: 'array' }).then((arr) => {
console.dir(arr);
});
}
}

export function indexOfDimension(axes: readonly AxisDesc[], dim: OmeDimension) {
return axes.findIndex((ax) => ax.name === dim);
}
export async function getSlice(metadata: ZarrDataset, r: ZarrRequest, layerIndex: number) {
// put the request in native order
const root = zarr.root(new zarr.FetchStore(metadata.url));
const scene = metadata.multiscales[0];
const { axes } = scene;
const level = scene.datasets[layerIndex] ?? scene.datasets[scene.datasets.length - 1];
const arr = await zarr.open(root.resolve(level.path), { kind: 'array' });
const result = await zarr.get(arr, buildQuery(r, axes, level.shape));
if (typeof result == 'number') {
throw new Error('oh noes, slice came back all weird');
}
return {
shape: result.shape,
buffer: result,
};
}
export async function load(url: string) {
return loadMetadata(url);
}
2 changes: 1 addition & 1 deletion examples/src/data-renderers/versa-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
sizeInUnits,
type ZarrDataset,
type ZarrRequest,
} from '~/common/loaders/ome-zarr/zarr-data';
} from '@alleninstitute/vis-omezarr';
import { getSlicePool } from '~/common/loaders/ome-zarr/sliceWorkerPool';
import type { Camera } from '~/common/camera';

Expand Down
3 changes: 2 additions & 1 deletion examples/src/data-sources/ome-zarr/planar-slice.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type ZarrDataset, load } from '~/common/loaders/ome-zarr/zarr-data';
import { type ZarrDataset } from '@alleninstitute/vis-omezarr';
import type { AxisAlignedPlane } from '~/data-renderers/versa-renderer';
import type { ColorMapping } from '../../data-renderers/types';
import type { OptionalTransform, Simple2DTransform } from '../types';
import { load } from '~/common/loaders/ome-zarr/zarr-data';
export type ZarrSliceConfig = {
type: 'zarrSliceConfig';
url: string;
Expand Down
3 changes: 2 additions & 1 deletion examples/src/data-sources/ome-zarr/slice-grid.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type ZarrDataset, load } from '~/common/loaders/ome-zarr/zarr-data';
import { type ZarrDataset } from '@alleninstitute/vis-omezarr';
import type { AxisAlignedPlane } from '~/data-renderers/versa-renderer';
import type { ColorMapping } from '../../data-renderers/types';
import type { OptionalTransform, Simple2DTransform } from '../types';
import { load } from '~/common/loaders/ome-zarr/zarr-data';

export type ZarrSliceGridConfig = {
type: 'ZarrSliceGridConfig';
Expand Down
2 changes: 1 addition & 1 deletion examples/src/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { buildLoopRenderer, buildMeshRenderer } from './data-renderers/mesh-rend
import { saveAs } from 'file-saver';
import type { AnnotationGrid, AnnotationGridConfig } from './data-sources/annotation/annotation-grid';
import { buildRenderer } from './data-renderers/scatterplot';
import { sizeInUnits } from './common/loaders/ome-zarr/zarr-data';
import { sizeInUnits } from '@alleninstitute/vis-omezarr';
import type { ColumnRequest } from './common/loaders/scatterplot/scatterbrain-loader';
import { buildVersaRenderer, type AxisAlignedPlane } from './data-renderers/versa-renderer';
import { buildImageRenderer } from './common/image-renderer';
Expand Down
19 changes: 17 additions & 2 deletions packages/omezarr/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
export { type OmeZarrDataset, buildOmeZarrSliceRenderer, buildAsyncOmezarrRenderer } from './sliceview/slice-renderer';
export {
type OmeZarrDataset,
buildOmeZarrSliceRenderer,
buildAsyncOmezarrRenderer,
type VoxelTileImage,
} from './sliceview/slice-renderer';
export { type VoxelTile, defaultDecoder, getVisibleTiles } from './sliceview/loader';
export { buildTileRenderer } from './sliceview/tile-renderer';
export { load as loadOmeZarr } from './zarr-data';
export { pickBestScale, sizeInUnits, sizeInVoxels, sliceDimensionForPlane, uvForPlane } from './zarr-data';
export {
pickBestScale,
sizeInUnits,
sizeInVoxels,
sliceDimensionForPlane,
uvForPlane,
planeSizeInVoxels,
type ZarrDataset,
type ZarrRequest,
} from './zarr-data';

0 comments on commit 31491a2

Please sign in to comment.