Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ome-zarr slice-view renderer package #34

Merged
merged 22 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b425288
first pass at moving a pile of various demos out into a more organize…
froyo-np Oct 24, 2024
5e0aa78
super basic poc-style demo for the new package - a non-gross demo wou…
froyo-np Oct 28, 2024
4daf9ef
oops actually export helpful things from the library
froyo-np Oct 28, 2024
bcd7013
Update packages/omezarr/src/sliceview/slice-renderer.ts
froyo-np Nov 1, 2024
e56c1b4
rename
froyo-np Nov 4, 2024
85930c2
turns out parcel is the way to go for packages still - whatever vite …
froyo-np Nov 5, 2024
a092b28
Merge branch 'noah/ome-zarr-slice-viewer' of https://github.com/Allen…
froyo-np Nov 5, 2024
030c68a
Merge branch 'main' into noah/ome-zarr-slice-viewer
froyo-np Nov 12, 2024
3836b0f
Merge branch 'main' into noah/ome-zarr-slice-viewer
TheMooseman Nov 14, 2024
3c6af3f
formatting
TheMooseman Nov 14, 2024
7dd3ecf
remove duplicated zarr data file
TheMooseman Nov 14, 2024
31491a2
non-destructively this time
TheMooseman Nov 14, 2024
dfabeed
remove duplicate file from common/loaders
froyo-np Nov 19, 2024
5fc4ca6
remove in-package demo, which was not helpful
froyo-np Nov 19, 2024
2b97a05
update lockfile
froyo-np Nov 19, 2024
69446e6
format the lockfile...
froyo-np Nov 19, 2024
97320ff
export more omezarr data stuff, fix one last reference
froyo-np Nov 19, 2024
c889c93
add very basic zooming to the demo
froyo-np Nov 19, 2024
4ad7255
fmt
froyo-np Nov 19, 2024
da6e832
do the TODO to prevent some potential innefficiency when gathing in-v…
froyo-np Nov 19, 2024
b398f87
fmt
froyo-np Nov 19, 2024
357dccc
explain suspicious numbers
froyo-np Nov 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/omezarr/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html>
<body>
<canvas
id="main"
style="top: 0; left: 0; width: 100%; height: 100%; position: absolute"
/>
</body>
</html>
<script
type="module"
src="./src/demo.ts"
></script>
57 changes: 57 additions & 0 deletions packages/omezarr/package.json
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't matching the other packages. Should be parcel stuff instead in this one!

Copy link
Collaborator Author

@froyo-np froyo-np Nov 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parcel seems incapable of building when importing zarrita - I tried to figure it out, but I lost that fight, which is why the examples package also does not use parcel. I figured since we're feeling good about vite elsewhere, and use it in the examples, I'd just roll with it. I seems to support building libraries/packages just fine

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turn out I was completely wrong about this - parcel is back in

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@alleninstitute/vis-omezarr",
"version": "0.0.3",
"contributors": [
{
"name": "Lane Sawyer",
"email": "[email protected]"
},
{
"name": "Noah Shepard",
"email": "[email protected]"
},
{
"name": "Skyler Moosman",
"email": "[email protected]"
},
{
"name": "Su Li",
"email": "[email protected]"
}
],
"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": "vite build",
"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",
"vitest": "^1.4.0",
"vite": "^5.3.5"
},
"dependencies": {
"@alleninstitute/vis-geometry": "workspace:*",
"@alleninstitute/vis-scatterbrain": "workspace:*",
"lodash": "^4.17.21",
"regl": "^2.1.0",
"zarrita": "0.4.0-next.14"
}
}
59 changes: 59 additions & 0 deletions packages/omezarr/src/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import REGL from 'regl'
import { buildOmeZarrSliceRenderer } from './sliceview/slice-renderer';
import { defaultDecoder } from './sliceview/loader';
import { AsyncDataCache, type ReglCacheEntry, buildAsyncRenderer } from '@alleninstitute/vis-scatterbrain';
import { load } from './zarr-data';
import { Box2D, type vec2 } from '@alleninstitute/vis-geometry';

// this is the most barebones "does it work" demo I can think of.
// do not follow any patterns in this demo - I took as many shortcuts as possible to be able to just call
// the async omezarr renderer once with some static data. see the other renderers (with real cameras, handlers, caches, etc)
// for examples of how to maybe integrate this into a real app - this is just proof of life.

function startDemo() {
console.warn('start demo!')
const cnvs = document.getElementById('main') as HTMLCanvasElement | null
if (cnvs) {
const gl = cnvs.getContext('webgl', {
alpha: true,
preserveDrawingBuffer: true,
antialias: true,
premultipliedAlpha: true,
});
if (!gl) {
throw new Error('WebGL not supported!');
}
const regl = REGL({
gl,
extensions: ['ANGLE_instanced_arrays', 'OES_texture_float', 'WEBGL_color_buffer_float'],
});
loadAndRenderOnce(regl, [cnvs.clientWidth, cnvs.clientHeight]);

}
}
const demo_versa = 'https://neuroglancer-vis-prototype.s3.amazonaws.com/VERSA/scratch/0500408166/'
async function loadAndRenderOnce(regl: REGL.Regl, screenSize: vec2) {
// this is a demo, so I didnt bother to prevent memory leaks or anything good at all!
const cache = new AsyncDataCache<string, string, ReglCacheEntry>(() => { }, () => 1, 3000)
const metadata = await load(demo_versa)
const renderer = buildAsyncRenderer(await buildOmeZarrSliceRenderer(regl, defaultDecoder))
// just draw a static dataset with the renderer
renderer(metadata, {
camera: {
screenSize,
view: Box2D.create([0, 0], [250, 120]),
},
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,
tileSize: 256,
},
() => { }, null, cache
)
}

startDemo();
2 changes: 2 additions & 0 deletions packages/omezarr/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { type OmeZarrDataset, buildOmeZarrSliceRenderer, buildAsyncOmezarrRenderer } from '~/src/sliceview/slice-renderer'
export { type VoxelTile, defaultDecoder, getVisibleTiles } from '~/src/sliceview/loader'
68 changes: 68 additions & 0 deletions packages/omezarr/src/sliceview/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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
}

function getAllTiles(idealTilePx: vec2, layerSize: vec2) {
// return the set of all our "tiles" of this layer, given the tilePx size
const tiles: box2D[] = [];
for (let x = 0; x < layerSize[0]; x += idealTilePx[0]) {
for (let y = 0; y < layerSize[1]; y += idealTilePx[1]) {
const xy: vec2 = [x, y];
tiles.push(Box2D.create(xy, Vec2.min(Vec2.add(xy, idealTilePx), layerSize)));
}
}
return tiles;
}
export function getVisibleTiles(
camera: {
view: box2D,
screenSize: vec2,
},
plane: AxisAlignedPlane,
planeIndex: number,
dataset: ZarrDataset,
tileSize: number
): VoxelTile[] {
const uv = uvForPlane(plane);
const layer = pickBestScale(dataset, uv, camera.view, camera.screenSize);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx for these comments!

// TODO: open the array, look at its chunks, use that size for the size of the tiles I request!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO now or later?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

later - updated comment!

const layerIndex = dataset.multiscales[0].datasets.indexOf(layer);

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);
// to go from a voxel-box to a real-box:
const vxlToReal = (vxl: box2D) => Box2D.translate(Box2D.scale(vxl, scale), [0, 0]);

// find the tiles, in voxels, to request...
const allTiles = getAllTiles([tileSize, tileSize], size);
// TODO: this is a pretty slow, and also somewhat flickery way to do this
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO now or later? Is this going to cause similar issues we saw with the dzi viewer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

const inView = allTiles.filter((tile) => !!Box2D.intersection(camera.view, vxlToReal(tile)));

return inView.map((uv) => ({
plane,
realBounds: vxlToReal(uv),
bounds: uv,
planeIndex,
layerIndex,
}))
}

export const defaultDecoder = (metadata: ZarrDataset, r: ZarrRequest, layerIndex: number): Promise<VoxelTileImage> => {
return getSlice(metadata, r, layerIndex).then((result: { shape: number[]; buffer: Chunk<'float32'> }) => {
const { shape, buffer } = result;
return { shape, data: new Float32Array(buffer.data) }
})

}
129 changes: 129 additions & 0 deletions packages/omezarr/src/sliceview/slice-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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 GpuData = {
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(stuff: Record<string, ReglCacheEntry | undefined>): stuff is GpuData {
froyo-np marked this conversation as resolved.
Show resolved Hide resolved
return 'R' in stuff && 'G' in stuff && 'B' in stuff &&
stuff.R?.type === 'texture' && stuff.G?.type === 'texture' && stuff.B?.type === 'texture'
}
const intervalToVec2 = (i: Interval): vec2 => [i.min, i.max]

type Decoder = (dataset: OmeZarrDataset, req: ZarrRequest, layerIndex: number) => Promise<VoxelTileImage>
export function buildOmeZarrSliceRenderer(regl: REGL.Regl, decoder: Decoder): Renderer<OmeZarrDataset, VoxelTile, RenderSettings, GpuData> {

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,
rotation: 0,
target,
tile: Box2D.toFlatArray(item.realBounds),
view: Box2D.toFlatArray(camera.view),
})
},
}
}
export function buildAsyncOmezarrRenderer(regl: REGL.Regl, decoder: Decoder) {
return buildAsyncRenderer(buildOmeZarrSliceRenderer(regl, decoder))
}
Loading