-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 3 commits
b425288
5e0aa78
4daf9ef
bcd7013
e56c1b4
85930c2
a092b28
030c68a
3836b0f
3c6af3f
7dd3ecf
31491a2
dfabeed
5fc4ca6
2b97a05
69446e6
97320ff
c889c93
4ad7255
da6e832
b398f87
357dccc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> |
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" | ||
} | ||
} |
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(); |
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' |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO now or later? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) } | ||
}) | ||
|
||
} |
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)) | ||
} |
There was a problem hiding this comment.
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 beparcel
stuff instead in this one!There was a problem hiding this comment.
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
There was a problem hiding this comment.
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