diff --git a/examples/index.html b/examples/index.html index 4b9835d..46832e4 100644 --- a/examples/index.html +++ b/examples/index.html @@ -5,6 +5,7 @@
diff --git a/examples/omezarr.html b/examples/omezarr.html new file mode 100644 index 0000000..4188365 --- /dev/null +++ b/examples/omezarr.html @@ -0,0 +1,17 @@ + + + + +
+ + + diff --git a/examples/package.json b/examples/package.json index 19aa863..b9ccfc3 100644 --- a/examples/package.json +++ b/examples/package.json @@ -46,6 +46,7 @@ "@alleninstitute/vis-geometry": "workspace:*", "@alleninstitute/vis-scatterbrain": "workspace:*", "@alleninstitute/vis-dzi": "workspace:*", + "@alleninstitute/vis-omezarr": "workspace:*", "@czi-sds/components": "^20.0.1", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", diff --git a/examples/src/common/loaders/ome-zarr/fetchSlice.worker.ts b/examples/src/common/loaders/ome-zarr/fetchSlice.worker.ts index f261238..23f5c15 100644 --- a/examples/src/common/loaders/ome-zarr/fetchSlice.worker.ts +++ b/examples/src/common/loaders/ome-zarr/fetchSlice.worker.ts @@ -1,6 +1,6 @@ // 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, getSlice } from '@alleninstitute/vis-omezarr'; const ctx = self; type ZarrSliceRequest = { diff --git a/examples/src/common/loaders/ome-zarr/sliceWorkerPool.ts b/examples/src/common/loaders/ome-zarr/sliceWorkerPool.ts index e2d4271..e0a32d5 100644 --- a/examples/src/common/loaders/ome-zarr/sliceWorkerPool.ts +++ b/examples/src/common/loaders/ome-zarr/sliceWorkerPool.ts @@ -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; diff --git a/examples/src/dzi/render-server-provider.tsx b/examples/src/common/react/render-server-provider.tsx similarity index 81% rename from examples/src/dzi/render-server-provider.tsx rename to examples/src/common/react/render-server-provider.tsx index 4a0ad6b..de5e1a7 100644 --- a/examples/src/dzi/render-server-provider.tsx +++ b/examples/src/common/react/render-server-provider.tsx @@ -7,7 +7,8 @@ export function RenderServerProvider(props: PropsWithChildren<{}>) { const server = useRef(); const { children } = props; useEffect(() => { - server.current = new RenderServer([2048, 2048], []); + server.current = new RenderServer([2048, 2048], ['oes_texture_float']); + console.log('server started...'); }, []); return {children}; } diff --git a/examples/src/data-renderers/versa-renderer.ts b/examples/src/data-renderers/versa-renderer.ts index cdd9fbf..5d510bb 100644 --- a/examples/src/data-renderers/versa-renderer.ts +++ b/examples/src/data-renderers/versa-renderer.ts @@ -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'; diff --git a/examples/src/data-renderers/volumeSliceRenderer.ts b/examples/src/data-renderers/volumeSliceRenderer.ts index 0caa6c8..1f90976 100644 --- a/examples/src/data-renderers/volumeSliceRenderer.ts +++ b/examples/src/data-renderers/volumeSliceRenderer.ts @@ -12,7 +12,7 @@ import { sizeInVoxels, sliceDimensionForPlane, uvForPlane, -} from '~/common/loaders/ome-zarr/zarr-data'; +} from '@alleninstitute/vis-omezarr'; import { cacheKeyFactory, getVisibleTiles, diff --git a/examples/src/data-sources/ome-zarr/planar-slice.ts b/examples/src/data-sources/ome-zarr/planar-slice.ts index 76327c7..310041a 100644 --- a/examples/src/data-sources/ome-zarr/planar-slice.ts +++ b/examples/src/data-sources/ome-zarr/planar-slice.ts @@ -1,4 +1,4 @@ -import { type ZarrDataset, load } from '~/common/loaders/ome-zarr/zarr-data'; +import { loadMetadata, 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'; @@ -32,7 +32,7 @@ function assembleZarrSlice(config: ZarrSliceConfig, dataset: ZarrDataset): AxisA } export function createZarrSlice(config: ZarrSliceConfig): Promise { const { url } = config; - return load(url).then((dataset) => { + return loadMetadata(url).then((dataset) => { return assembleZarrSlice(config, dataset); }); } diff --git a/examples/src/data-sources/ome-zarr/slice-grid.ts b/examples/src/data-sources/ome-zarr/slice-grid.ts index 1940b88..2baf963 100644 --- a/examples/src/data-sources/ome-zarr/slice-grid.ts +++ b/examples/src/data-sources/ome-zarr/slice-grid.ts @@ -1,4 +1,4 @@ -import { type ZarrDataset, load } from '~/common/loaders/ome-zarr/zarr-data'; +import { loadMetadata, 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'; @@ -33,7 +33,7 @@ function assembleZarrSliceGrid(config: ZarrSliceGridConfig, dataset: ZarrDataset } export function createZarrSliceGrid(config: ZarrSliceGridConfig): Promise { const { url } = config; - return load(url).then((dataset) => { + return loadMetadata(url).then((dataset) => { return assembleZarrSliceGrid(config, dataset); }); } diff --git a/examples/src/dzi/double.tsx b/examples/src/dzi/double.tsx index 1870904..19bb3eb 100644 --- a/examples/src/dzi/double.tsx +++ b/examples/src/dzi/double.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { RenderServerProvider } from './render-server-provider'; +import { RenderServerProvider } from '../common/react/render-server-provider'; import React from 'react'; import { DziView } from './dziView'; import type { DziImage, DziRenderSettings } from '@alleninstitute/vis-dzi'; diff --git a/examples/src/dzi/dziView.tsx b/examples/src/dzi/dziView.tsx index f50667c..79d0d4e 100644 --- a/examples/src/dzi/dziView.tsx +++ b/examples/src/dzi/dziView.tsx @@ -10,7 +10,7 @@ import { import React from 'react'; import { buildAsyncRenderer, type RenderFrameFn } from '@alleninstitute/vis-scatterbrain'; import { isEqual } from 'lodash'; -import { renderServerContext } from './render-server-provider'; +import { renderServerContext } from '../common/react/render-server-provider'; import { Vec2, type vec2 } from '@alleninstitute/vis-geometry'; type Props = { diff --git a/examples/src/layers.ts b/examples/src/layers.ts index bd403d6..cac8181 100644 --- a/examples/src/layers.ts +++ b/examples/src/layers.ts @@ -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'; diff --git a/examples/src/omezarr/app.tsx b/examples/src/omezarr/app.tsx new file mode 100644 index 0000000..c9a8c96 --- /dev/null +++ b/examples/src/omezarr/app.tsx @@ -0,0 +1,26 @@ +import React, { useEffect, useState } from 'react'; +import { RenderServerProvider } from '~/common/react/render-server-provider'; +import { SliceView } from './sliceview'; +import { type OmeZarrDataset, loadOmeZarr } from '@alleninstitute/vis-omezarr'; + +const demo_versa = 'https://neuroglancer-vis-prototype.s3.amazonaws.com/VERSA/scratch/0500408166/'; + +export function AppUi() { + return ; +} + +function DataPlease() { + // load our canned data for now: + const [omezarr, setfile] = useState(undefined); + useEffect(() => { + loadOmeZarr(demo_versa).then((dataset) => { + setfile(dataset); + console.log('loaded!'); + }); + }, []); + return ( + + + + ); +} diff --git a/examples/src/omezarr/omezarr.ts b/examples/src/omezarr/omezarr.ts new file mode 100644 index 0000000..90a8fc7 --- /dev/null +++ b/examples/src/omezarr/omezarr.ts @@ -0,0 +1,5 @@ +import { createRoot } from 'react-dom/client'; +import { AppUi } from './app'; + +const uiroot = createRoot(document.getElementById('main')!); +uiroot.render(AppUi()); diff --git a/examples/src/omezarr/sliceview.tsx b/examples/src/omezarr/sliceview.tsx new file mode 100644 index 0000000..d3132b0 --- /dev/null +++ b/examples/src/omezarr/sliceview.tsx @@ -0,0 +1,99 @@ +import { type box2D, Box2D, Vec2 } from '@alleninstitute/vis-geometry'; +import { + buildAsyncOmezarrRenderer, + defaultDecoder, + type VoxelTile, + type ZarrDataset, + type RenderSettings, +} from '@alleninstitute/vis-omezarr'; +import type { RenderFrameFn } from '@alleninstitute/vis-scatterbrain'; +import React, { useState } from 'react'; +import { useContext, useEffect, useRef } from 'react'; +import { renderServerContext } from '~/common/react/render-server-provider'; +type Props = { + omezarr: ZarrDataset | undefined; +}; +const settings: RenderSettings = { + tileSize: 256, + gamut: { + R: { gamut: { min: 0, max: 80 }, index: 0 }, + G: { gamut: { min: 0, max: 100 }, index: 1 }, + B: { gamut: { min: 0, max: 100 }, index: 2 }, + }, + plane: 'xy', + planeIndex: 0, + camera: { + view: Box2D.create([0, 0], [250, 120]), + screenSize: [500, 500], + }, +}; +function compose(ctx: CanvasRenderingContext2D, image: ImageData) { + ctx.putImageData(image, 0, 0); +} + +export function SliceView(props: Props) { + const { omezarr } = props; + const server = useContext(renderServerContext); + const cnvs = useRef(null); + const renderer = useRef>(); + const [view, setView] = useState(Box2D.create([0, 0], [250, 120])); + useEffect(() => { + if (server && server.regl) { + renderer.current = buildAsyncOmezarrRenderer(server.regl, defaultDecoder); + } + return () => { + if (cnvs.current) { + server?.destroyClient(cnvs.current); + } + }; + }, [server]); + + useEffect(() => { + if (server && renderer.current && cnvs.current && omezarr) { + const hey: RenderFrameFn = (target, cache, callback) => { + if (renderer.current) { + return renderer.current( + omezarr, + { ...settings, camera: { ...settings.camera, view } }, + callback, + target, + cache + ); + } + return null; + }; + server.beginRendering( + hey, + (e) => { + switch (e.status) { + case 'begin': + server.regl?.clear({ framebuffer: e.target, color: [0, 0, 0, 0], depth: 1 }); + break; + case 'progress': + // wanna see the tiles as they arrive? + e.server.copyToClient(compose); + break; + case 'finished': { + e.server.copyToClient(compose); + } + } + }, + cnvs.current + ); + } + }, [server, renderer.current, cnvs.current, omezarr, view]); + return ( + { + const scale = e.deltaY > 0 ? 1.1 : 0.9; + const m = Box2D.midpoint(view); + const v = Box2D.translate(Box2D.scale(Box2D.translate(view, Vec2.scale(m, -1)), [scale, scale]), m); + setView(v); + }} + width={settings.camera.screenSize[0]} + height={settings.camera.screenSize[1]} + > + ); +} diff --git a/packages/omezarr/package.json b/packages/omezarr/package.json new file mode 100644 index 0000000..9e0aa8e --- /dev/null +++ b/packages/omezarr/package.json @@ -0,0 +1,58 @@ +{ + "name": "@alleninstitute/vis-omezarr", + "version": "0.0.1", + "contributors": [ + { + "name": "Lane Sawyer", + "email": "lane.sawyer@alleninstitute.org" + }, + { + "name": "Noah Shepard", + "email": "noah.shepard@alleninstitute.org" + }, + { + "name": "Skyler Moosman", + "email": "skyler.moosman@alleninstitute.org" + }, + { + "name": "Su Li", + "email": "su.li@alleninstitute.org" + } + ], + "license": "BSD-3-Clause", + "source": "src/index.ts", + "main": "dist/main.js", + "module": "dist/module.js", + "types": "dist/types.d.ts", + "files": [ + "dist" + ], + "scripts": { + "preinstall": "npx only-allow pnpm", + "typecheck": "tsc --noEmit", + "build": "parcel build --no-cache", + "dev": "vite", + "test": "vitest --watch" + }, + "repository": { + "type": "git", + "url": "https://github.com/AllenInstitute/vis.git" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com/AllenInstitute" + }, + "devDependencies": { + "@types/lodash": "^4.14.202", + "typescript": "^5.3.3", + "parcel": "2.12.0", + "@parcel/packager-ts": "^2.12.0", + "@parcel/transformer-typescript-types": "^2.12.0" + }, + "dependencies": { + "@alleninstitute/vis-geometry": "workspace:*", + "@alleninstitute/vis-scatterbrain": "workspace:*", + "lodash": "^4.17.21", + "regl": "^2.1.0", + "zarrita": "0.4.0-next.14" + } +} diff --git a/packages/omezarr/src/index.ts b/packages/omezarr/src/index.ts new file mode 100644 index 0000000..1927d1e --- /dev/null +++ b/packages/omezarr/src/index.ts @@ -0,0 +1,21 @@ +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 { + loadMetadata, + pickBestScale, + getSlice, + sizeInUnits, + sizeInVoxels, + sliceDimensionForPlane, + uvForPlane, + planeSizeInVoxels, + type ZarrDataset, + type ZarrRequest, +} from './zarr-data'; diff --git a/packages/omezarr/src/sliceview/loader.ts b/packages/omezarr/src/sliceview/loader.ts new file mode 100644 index 0000000..fa86017 --- /dev/null +++ b/packages/omezarr/src/sliceview/loader.ts @@ -0,0 +1,107 @@ +import { Box2D, Vec2, type box2D, type vec2 } from '@alleninstitute/vis-geometry'; +import type { AxisAlignedPlane, ZarrDataset, ZarrRequest } from '../zarr-data'; +import { getSlice, pickBestScale, planeSizeInVoxels, sizeInUnits, uvForPlane } from '../zarr-data'; +import type { VoxelTileImage } from './slice-renderer'; +import type { Chunk } from 'zarrita'; + +export type VoxelTile = { + plane: AxisAlignedPlane; // the plane in which the tile sits + realBounds: box2D; // in the space given by the axis descriptions of the omezarr dataset + bounds: box2D; // in voxels, in the plane + planeIndex: number; // the index of this slice along the axis being sliced (orthoganal to plane) + layerIndex: number; // the index in the resolution pyramid of the omezarr dataset +}; + +/** + * given a image with @param size pixels, break it into tiles, each @param idealTilePx. + * for all such tiles which intersect the given bounds, call the visitor + * @param idealTilePx the size of a tile, in pixels + * @param size the size of the image at this level of detail + * @param bounds visit only the tiles that are within the given bounds (in pixels) + */ +function visitTilesWithin(idealTilePx: vec2, size: vec2, bounds: box2D, visit: (tile: box2D) => void) { + const withinBoth = Box2D.intersection(bounds, Box2D.create([0, 0], size)); + if (!withinBoth) { + return; + } + // convert the image into tile indexes: + const boundsInTiles = Box2D.map(withinBoth, (corner) => Vec2.div(corner, idealTilePx)); + for (let x = Math.floor(boundsInTiles.minCorner[0]); x < Math.ceil(boundsInTiles.maxCorner[0]); x += 1) { + for (let y = Math.floor(boundsInTiles.minCorner[1]); y < Math.ceil(boundsInTiles.maxCorner[1]); y += 1) { + // all tiles visited are always within both the bounds, and the image itself + const lo = Vec2.mul([x, y], idealTilePx); + const hi = Vec2.add(lo, idealTilePx); + visit(Box2D.create(lo, hi)); + } + } +} +function getVisibleTilesInLayer( + camera: { + view: box2D; + screenSize: vec2; + }, + plane: AxisAlignedPlane, + planeIndex: number, + dataset: ZarrDataset, + tileSize: number, + layerIndex: number +) { + const uv = uvForPlane(plane); + const layer = dataset.multiscales[0].datasets[layerIndex]; + if (!layer) return []; + const size = planeSizeInVoxels(uv, dataset.multiscales[0].axes, layer); + const realSize = sizeInUnits(uv, dataset.multiscales[0].axes, layer); + if (!size || !realSize) return []; + const scale = Vec2.div(realSize, size); + const vxlToReal = (vxl: box2D) => Box2D.scale(vxl, scale); + const realToVxl = (real: box2D) => Box2D.scale(real, Vec2.div([1, 1], scale)); + const visibleTiles: VoxelTile[] = []; + visitTilesWithin([tileSize, tileSize], size, realToVxl(camera.view), (uv) => { + visibleTiles.push({ + plane, + realBounds: vxlToReal(uv), + bounds: uv, + planeIndex, + layerIndex, + }); + }); + return visibleTiles; +} +export function getVisibleTiles( + camera: { + view: box2D; + screenSize: vec2; + }, + plane: AxisAlignedPlane, + planeIndex: number, + dataset: ZarrDataset, + tileSize: number +): VoxelTile[] { + const uv = uvForPlane(plane); + // TODO (someday) open the array, look at its chunks, use that size for the size of the tiles I request! + + const layer = pickBestScale(dataset, uv, camera.view, camera.screenSize); + // using [1,1] here is asking for the best LOD to fill a single pixel - aka + // the lowest LOD - this is safer than just assuming that layer will be + // the first or last in the list. + const baseLayer = pickBestScale(dataset, uv, camera.view, [1, 1]); + const layerIndex = dataset.multiscales[0].datasets.indexOf(layer); + const baselayerIndex = dataset.multiscales[0].datasets.indexOf(baseLayer); + if (layer.path !== baseLayer.path) { + // if the layer we want to draw is not the lowest-level of detail, + // then we inject the low-level of detail tiles into the returned result - the idea + // is that we draw the low LOD data underneath the desired LOD as a fallback to prevent flickering. + return [ + ...getVisibleTilesInLayer(camera, plane, planeIndex, dataset, tileSize, baselayerIndex), + ...getVisibleTilesInLayer(camera, plane, planeIndex, dataset, tileSize, layerIndex), + ]; + } + return getVisibleTilesInLayer(camera, plane, planeIndex, dataset, tileSize, layerIndex); +} + +export const defaultDecoder = (metadata: ZarrDataset, r: ZarrRequest, layerIndex: number): Promise => { + return getSlice(metadata, r, layerIndex).then((result: { shape: number[]; buffer: Chunk<'float32'> }) => { + const { shape, buffer } = result; + return { shape, data: new Float32Array(buffer.data) }; + }); +}; diff --git a/packages/omezarr/src/sliceview/slice-renderer.ts b/packages/omezarr/src/sliceview/slice-renderer.ts new file mode 100644 index 0000000..4202c02 --- /dev/null +++ b/packages/omezarr/src/sliceview/slice-renderer.ts @@ -0,0 +1,139 @@ +import REGL from 'regl'; +import { Box2D, type Interval, type box2D, type vec2 } from '@alleninstitute/vis-geometry'; +import { + type Renderer, + type ReglCacheEntry, + type CachedTexture, + buildAsyncRenderer, +} from '@alleninstitute/vis-scatterbrain'; +import type { AxisAlignedPlane, ZarrDataset, ZarrRequest } from '../zarr-data'; +import { buildTileRenderer } from './tile-renderer'; +import { type VoxelTile, getVisibleTiles } from './loader'; + +type RenderSettings = { + camera: { + view: box2D; + screenSize: vec2; + }; + planeIndex: number; + tileSize: number; + plane: AxisAlignedPlane; + gamut: Record<'R' | 'G' | 'B', { gamut: Interval; index: number }>; +}; +export type OmeZarrDataset = ZarrDataset; + +// represent a 2D slice of a volume + +// a slice of a volume (as voxels suitable for display) +export type VoxelTileImage = { + data: Float32Array; + shape: number[]; +}; +type ImageChannels = { + R: CachedTexture; + G: CachedTexture; + B: CachedTexture; +}; +function toZarrRequest(tile: VoxelTile, channel: number): ZarrRequest { + const { plane, planeIndex, bounds } = tile; + const { minCorner: min, maxCorner: max } = bounds; + const u = { min: min[0], max: max[0] }; + const v = { min: min[1], max: max[1] }; + switch (plane) { + case 'xy': + return { + x: u, + y: v, + t: 0, + c: channel, + z: planeIndex, + }; + case 'xz': + return { + x: u, + z: v, + t: 0, + c: channel, + y: planeIndex, + }; + case 'yz': + return { + y: u, + z: v, + t: 0, + c: channel, + x: planeIndex, + }; + } +} +function isPrepared(cacheData: Record): cacheData is ImageChannels { + return ( + 'R' in cacheData && + 'G' in cacheData && + 'B' in cacheData && + cacheData.R?.type === 'texture' && + cacheData.G?.type === 'texture' && + cacheData.B?.type === 'texture' + ); +} +const intervalToVec2 = (i: Interval): vec2 => [i.min, i.max]; + +type Decoder = (dataset: OmeZarrDataset, req: ZarrRequest, layerIndex: number) => Promise; +export function buildOmeZarrSliceRenderer( + regl: REGL.Regl, + decoder: Decoder +): Renderer { + function sliceAsTexture(slice: VoxelTileImage): CachedTexture { + const { data, shape } = slice; + return { + bytes: data.byteLength, + texture: regl.texture({ data: data, width: shape[1], height: shape[0], format: 'luminance' }), + type: 'texture', + }; + } + const cmd = buildTileRenderer(regl); + return { + cacheKey: (item, requestKey, dataset, settings) => { + const col = requestKey as keyof RenderSettings['gamut']; + const index = settings.gamut[col]?.index ?? 0; + return `${dataset.url}_${JSON.stringify(item)}_ch=${index.toFixed(0)}`; + }, + destroy: () => {}, + getVisibleItems: (dataset, settings) => { + const { camera, plane, planeIndex, tileSize } = settings; + return getVisibleTiles(camera, plane, planeIndex, dataset, tileSize); + }, + fetchItemContent: (item, dataset, settings, signal) => { + return { + R: () => + decoder(dataset, toZarrRequest(item, settings.gamut.R.index), item.layerIndex).then(sliceAsTexture), + G: () => + decoder(dataset, toZarrRequest(item, settings.gamut.G.index), item.layerIndex).then(sliceAsTexture), + B: () => + decoder(dataset, toZarrRequest(item, settings.gamut.B.index), item.layerIndex).then(sliceAsTexture), + }; + }, + isPrepared, + renderItem: (target, item, dataset, settings, gpuData) => { + const { R, G, B } = gpuData; + const { camera } = settings; + const Rgamut = intervalToVec2(settings.gamut.R.gamut); + const Ggamut = intervalToVec2(settings.gamut.G.gamut); + const Bgamut = intervalToVec2(settings.gamut.B.gamut); + cmd({ + R: R.texture, + G: G.texture, + B: B.texture, + Rgamut, + Ggamut, + Bgamut, + target, + tile: Box2D.toFlatArray(item.realBounds), + view: Box2D.toFlatArray(camera.view), + }); + }, + }; +} +export function buildAsyncOmezarrRenderer(regl: REGL.Regl, decoder: Decoder) { + return buildAsyncRenderer(buildOmeZarrSliceRenderer(regl, decoder)); +} diff --git a/packages/omezarr/src/sliceview/tile-renderer.ts b/packages/omezarr/src/sliceview/tile-renderer.ts new file mode 100644 index 0000000..e23b1a8 --- /dev/null +++ b/packages/omezarr/src/sliceview/tile-renderer.ts @@ -0,0 +1,99 @@ +// render a slice of an ome-zarr file as a 2D image +// note that the ome-zarr data must have exactly 3 channels +// the channels may be mapped to color-channels (RGB) with a basic 2-post gamut control + +import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; +import REGL, { type Framebuffer2D } from 'regl'; + +type Props = { + target: Framebuffer2D | null; + tile: vec4; + view: vec4; + Rgamut: vec2; + Ggamut: vec2; + Bgamut: vec2; + R: REGL.Texture2D; + G: REGL.Texture2D; + B: REGL.Texture2D; +}; + +export function buildTileRenderer(regl: REGL.Regl) { + const cmd = regl< + { + view: vec4; + tile: vec4; + R: REGL.Texture2D; + G: REGL.Texture2D; + B: REGL.Texture2D; + Rgamut: vec2; + Ggamut: vec2; + Bgamut: vec2; + }, + { pos: REGL.BufferData }, + Props + >({ + vert: ` precision highp float; + attribute vec2 pos; + + uniform vec4 view; + uniform vec4 tile; + varying vec2 texCoord; + uniform float rot; + + void main(){ + vec2 tileSize = tile.zw-tile.xy; + texCoord = pos; + vec2 obj = (pos.xy*tileSize+tile.xy); + + vec2 p = (obj-view.xy)/(view.zw-view.xy); + // now, to clip space + p = (p*2.0)-1.0; + gl_Position = vec4(p.x,p.y,0.0,1.0); + }`, + + frag: ` + precision highp float; + uniform sampler2D R; + uniform sampler2D G; + uniform sampler2D B; // for reasons which are pretty annoying + // its more direct to do 3 separate channels... + uniform vec2 Rgamut; + uniform vec2 Ggamut; + uniform vec2 Bgamut; + + varying vec2 texCoord; + void main(){ + vec3 mins = vec3(Rgamut.x,Ggamut.x,Bgamut.x); + vec3 maxs = vec3(Rgamut.y,Ggamut.y,Bgamut.y); + vec3 span = maxs-mins; + vec3 color = (vec3( + texture2D(R, texCoord).r, + texture2D(G, texCoord).r, + texture2D(B, texCoord).r + )-mins) /span; + + gl_FragColor = vec4(color, 1.0); + }`, + framebuffer: regl.prop('target'), + attributes: { + pos: [0, 0, 1, 0, 1, 1, 0, 1], + }, + uniforms: { + tile: regl.prop('tile'), + view: regl.prop('view'), + R: regl.prop('R'), + G: regl.prop('G'), + B: regl.prop('B'), + Rgamut: regl.prop('Rgamut'), + Ggamut: regl.prop('Ggamut'), + Bgamut: regl.prop('Bgamut'), + }, + depth: { + enable: false, + }, + count: 4, + primitive: 'triangle fan', + }); + + return (p: Props) => cmd(p); +} diff --git a/examples/src/common/loaders/ome-zarr/zarr-data.ts b/packages/omezarr/src/zarr-data.ts similarity index 93% rename from examples/src/common/loaders/ome-zarr/zarr-data.ts rename to packages/omezarr/src/zarr-data.ts index 1c14120..42fae66 100644 --- a/examples/src/common/loaders/ome-zarr/zarr-data.ts +++ b/packages/omezarr/src/zarr-data.ts @@ -1,9 +1,6 @@ -// 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: @@ -16,7 +13,7 @@ type AxisDesc = { unit: string; // see list of possible units: https://ngff.openmicroscopy.org/latest/#axes-md }; -// todo, there are other types of coordinate transforms... +// todo, there are other types of coordinate transforms, however we only support scale transforms for now type ScaleTransform = { type: 'scale'; scale: ReadonlyArray; @@ -55,7 +52,7 @@ async function mapAsync(arr: ReadonlyArray, fn: (t: T, index: number) = 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) { +export async function loadMetadata(url: string) { const store = new zarr.FetchStore(url); const root = zarr.root(store); const attrs: ZarrAttrs = await getRawInfo(store); @@ -71,19 +68,22 @@ async function loadMetadata(url: string) { })), }; } - -type OmeDimension = 'x' | 'y' | 'z' | 't' | 'c'; +export type AxisAlignedPlane = 'xy' | 'xz' | 'yz'; +export type OmeDimension = 'x' | 'y' | 'z' | 't' | 'c'; +export type PlaneMapping = { u: OmeDimension; v: OmeDimension }; +// we could be tricky and try to statically prevent a uv mapping like xx or xy, but theres no real value in it 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) { +export function uvForPlane(plane: T) { return uvTable[plane]; } export function sliceDimensionForPlane(plane: AxisAlignedPlane) { diff --git a/packages/omezarr/tsconfig.json b/packages/omezarr/tsconfig.json new file mode 100644 index 0000000..baaee9e --- /dev/null +++ b/packages/omezarr/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "paths": { + "~/*": ["./*"] + }, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["es2022", "DOM"] + }, + "include": ["./src/index.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8ffb39..0b2db88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@alleninstitute/vis-geometry': specifier: workspace:* version: link:../packages/geometry + '@alleninstitute/vis-omezarr': + specifier: workspace:* + version: link:../packages/omezarr '@alleninstitute/vis-scatterbrain': specifier: workspace:* version: link:../packages/scatterbrain @@ -166,6 +169,40 @@ importers: specifier: ^1.4.0 version: 1.4.0 + packages/omezarr: + dependencies: + '@alleninstitute/vis-geometry': + specifier: workspace:* + version: link:../geometry + '@alleninstitute/vis-scatterbrain': + specifier: workspace:* + version: link:../scatterbrain + lodash: + specifier: ^4.17.21 + version: 4.17.21 + regl: + specifier: ^2.1.0 + version: 2.1.0 + zarrita: + specifier: 0.4.0-next.14 + version: 0.4.0-next.14 + devDependencies: + '@parcel/packager-ts': + specifier: ^2.12.0 + version: 2.12.0(@parcel/core@2.12.0) + '@parcel/transformer-typescript-types': + specifier: ^2.12.0 + version: 2.12.0(@parcel/core@2.12.0)(typescript@5.3.3) + '@types/lodash': + specifier: ^4.14.202 + version: 4.14.202 + parcel: + specifier: 2.12.0 + version: 2.12.0(typescript@5.3.3) + typescript: + specifier: ^5.3.3 + version: 5.3.3 + packages/scatterbrain: dependencies: '@alleninstitute/vis-geometry': @@ -1551,7 +1588,7 @@ packages: '@parcel/plugin': 2.12.0(@parcel/core@2.12.0) '@parcel/source-map': 2.1.1 '@parcel/utils': 2.12.0 - '@swc/core': 1.7.6 + '@swc/core': 1.7.6(@swc/helpers@0.5.6) nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' @@ -1575,7 +1612,7 @@ packages: '@parcel/types': 2.12.0(@parcel/core@2.12.0) '@parcel/utils': 2.12.0 '@parcel/workers': 2.12.0(@parcel/core@2.12.0) - '@swc/core': 1.3.107(@swc/helpers@0.5.6) + '@swc/core': 1.7.6(@swc/helpers@0.5.6) semver: 7.5.4 transitivePeerDependencies: - '@swc/helpers' @@ -2525,18 +2562,6 @@ packages: } dev: true - /@swc/core-darwin-arm64@1.3.107: - resolution: - { - integrity: sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==, - } - engines: { node: '>=10' } - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@swc/core-darwin-arm64@1.7.6: resolution: { @@ -2549,18 +2574,6 @@ packages: dev: true optional: true - /@swc/core-darwin-x64@1.3.107: - resolution: - { - integrity: sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==, - } - engines: { node: '>=10' } - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@swc/core-darwin-x64@1.7.6: resolution: { @@ -2573,18 +2586,6 @@ packages: dev: true optional: true - /@swc/core-linux-arm-gnueabihf@1.3.107: - resolution: - { - integrity: sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==, - } - engines: { node: '>=10' } - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@swc/core-linux-arm-gnueabihf@1.7.6: resolution: { @@ -2597,18 +2598,6 @@ packages: dev: true optional: true - /@swc/core-linux-arm64-gnu@1.3.107: - resolution: - { - integrity: sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==, - } - engines: { node: '>=10' } - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@swc/core-linux-arm64-gnu@1.7.6: resolution: { @@ -2621,18 +2610,6 @@ packages: dev: true optional: true - /@swc/core-linux-arm64-musl@1.3.107: - resolution: - { - integrity: sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==, - } - engines: { node: '>=10' } - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@swc/core-linux-arm64-musl@1.7.6: resolution: { @@ -2645,18 +2622,6 @@ packages: dev: true optional: true - /@swc/core-linux-x64-gnu@1.3.107: - resolution: - { - integrity: sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==, - } - engines: { node: '>=10' } - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@swc/core-linux-x64-gnu@1.7.6: resolution: { @@ -2669,18 +2634,6 @@ packages: dev: true optional: true - /@swc/core-linux-x64-musl@1.3.107: - resolution: - { - integrity: sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==, - } - engines: { node: '>=10' } - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@swc/core-linux-x64-musl@1.7.6: resolution: { @@ -2693,18 +2646,6 @@ packages: dev: true optional: true - /@swc/core-win32-arm64-msvc@1.3.107: - resolution: - { - integrity: sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==, - } - engines: { node: '>=10' } - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@swc/core-win32-arm64-msvc@1.7.6: resolution: { @@ -2717,18 +2658,6 @@ packages: dev: true optional: true - /@swc/core-win32-ia32-msvc@1.3.107: - resolution: - { - integrity: sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==, - } - engines: { node: '>=10' } - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@swc/core-win32-ia32-msvc@1.7.6: resolution: { @@ -2741,18 +2670,6 @@ packages: dev: true optional: true - /@swc/core-win32-x64-msvc@1.3.107: - resolution: - { - integrity: sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==, - } - engines: { node: '>=10' } - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@swc/core-win32-x64-msvc@1.7.6: resolution: { @@ -2765,36 +2682,7 @@ packages: dev: true optional: true - /@swc/core@1.3.107(@swc/helpers@0.5.6): - resolution: - { - integrity: sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==, - } - engines: { node: '>=10' } - requiresBuild: true - peerDependencies: - '@swc/helpers': ^0.5.0 - peerDependenciesMeta: - '@swc/helpers': - optional: true - dependencies: - '@swc/counter': 0.1.2 - '@swc/helpers': 0.5.6 - '@swc/types': 0.1.5 - optionalDependencies: - '@swc/core-darwin-arm64': 1.3.107 - '@swc/core-darwin-x64': 1.3.107 - '@swc/core-linux-arm-gnueabihf': 1.3.107 - '@swc/core-linux-arm64-gnu': 1.3.107 - '@swc/core-linux-arm64-musl': 1.3.107 - '@swc/core-linux-x64-gnu': 1.3.107 - '@swc/core-linux-x64-musl': 1.3.107 - '@swc/core-win32-arm64-msvc': 1.3.107 - '@swc/core-win32-ia32-msvc': 1.3.107 - '@swc/core-win32-x64-msvc': 1.3.107 - dev: true - - /@swc/core@1.7.6: + /@swc/core@1.7.6(@swc/helpers@0.5.6): resolution: { integrity: sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==, @@ -2808,6 +2696,7 @@ packages: optional: true dependencies: '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.6 '@swc/types': 0.1.12 optionalDependencies: '@swc/core-darwin-arm64': 1.7.6 @@ -2822,13 +2711,6 @@ packages: '@swc/core-win32-x64-msvc': 1.7.6 dev: true - /@swc/counter@0.1.2: - resolution: - { - integrity: sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==, - } - dev: true - /@swc/counter@0.1.3: resolution: { @@ -2854,13 +2736,6 @@ packages: '@swc/counter': 0.1.3 dev: true - /@swc/types@0.1.5: - resolution: - { - integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==, - } - dev: true - /@trysound/sax@0.2.0: resolution: { @@ -2946,7 +2821,7 @@ packages: peerDependencies: vite: ^4 || ^5 dependencies: - '@swc/core': 1.7.6 + '@swc/core': 1.7.6(@swc/helpers@0.5.6) vite: 5.3.5(@types/node@22.1.0) transitivePeerDependencies: - '@swc/helpers' @@ -4368,9 +4243,6 @@ packages: } engines: { node: '>= 12.0.0' } hasBin: true - peerDependenciesMeta: - '@parcel/core': - optional: true dependencies: '@parcel/config-default': 2.12.0(@parcel/core@2.12.0)(typescript@5.3.3) '@parcel/core': 2.12.0