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

Noah/documentation #46

Merged
merged 6 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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: 12 additions & 1 deletion examples/src/dzi/double.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,19 @@ const exampleSettings: DziRenderSettings = {
view: Box2D.create([0, 0], [1, 1]),
},
};

/**
* HEY!!!
* this is an example React Component for rendering two DZI images which share a camera.
* Additionally, both images have an SVG overlay.
* This example is as bare-bones as possible! It is NOT the recommended way to do anything, its just trying to show
* one way of:
* 1. using our rendering utilities for DZI data, specifically in a react component. Your needs for state-management,
* SVG overlays, etc may all be different!
*
*/
export function TwoClientsPOC() {
// the DZI renderer expects a "relative" camera - that means a box, from 0 to 1. 0 is the bottom or left of the image,
// and 1 is the top or right of the image, regardless of the aspect ratio of that image.
const [view, setView] = useState<box2D>(Box2D.create([0, 0], [1, 1]));
const zoom = (e: React.WheelEvent<HTMLCanvasElement>) => {
const scale = e.deltaY > 0 ? 1.1 : 0.9;
Expand Down
10 changes: 9 additions & 1 deletion examples/src/omezarr/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ const demo_versa = 'https://neuroglancer-vis-prototype.s3.amazonaws.com/VERSA/sc
export function AppUi() {
return <DataPlease />;
}

/**
* HEY!!!
* this is an example React Component for rendering A single slice of an OMEZARR image in a react component
* This example is as bare-bones as possible! It is NOT the recommended way to do anything, its just trying to show
* one way of:
* 1. using our rendering utilities for OmeZarr data, specifically in a react component. Your needs for state-management,
* slicing logic, etc might all be different!
*
*/
function DataPlease() {
// load our canned data for now:
const [omezarr, setfile] = useState<OmeZarrDataset | undefined>(undefined);
Expand Down
16 changes: 13 additions & 3 deletions examples/src/omezarr/sliceview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type Props = {
};
const settings: RenderSettings = {
tileSize: 256,
// in a "real" app, you'd most likely expose sliders to control how the data in the file
// gets mapped to pixel/color intensity on the screen. for now, we just use hardcoded data
gamut: {
R: { gamut: { min: 0, max: 80 }, index: 0 },
G: { gamut: { min: 0, max: 100 }, index: 1 },
Expand All @@ -23,10 +25,17 @@ const settings: RenderSettings = {
plane: 'xy',
planeIndex: 3,
camera: {
// the omezarr renderer expects a box in whatever space is given by the omezarr file itself in its
// axes metadata = for example, millimeters. if you load a volume that says its 30mm X 30mm X 10mm,
// and you want to view XY slices and have them fit perfectly on your screen, then a box
// like [0,0],[30,30] would be appropriate!
view: Box2D.create([0, 0], [250, 120]),
screenSize: [500, 500],
},
};
// this example uses the RenderServer utility - this lets you render to canvas elements without having to
// initialize WebGL on that canvas itself, at a small cost to performance. the compose function is the configurable
// step used to get the pixels from WebGL to the target canvas.
function compose(ctx: CanvasRenderingContext2D, image: ImageData) {
ctx.putImageData(image, 0, 0);
}
Expand All @@ -50,7 +59,7 @@ export function SliceView(props: Props) {

useEffect(() => {
if (server && renderer.current && cnvs.current && omezarr) {
const hey: RenderFrameFn<ZarrDataset, VoxelTile> = (target, cache, callback) => {
const renderFn: RenderFrameFn<ZarrDataset, VoxelTile> = (target, cache, callback) => {
if (renderer.current) {
return renderer.current(
omezarr,
Expand All @@ -63,17 +72,18 @@ export function SliceView(props: Props) {
return null;
};
server.beginRendering(
hey,
renderFn,
// here's where we handle lifecycle events in that rendering function (its async and slow because it may have to fetch data from far away)
(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': {
// the bare minimum event handling would be this: copy webGL's work to the target canvas using the compose function
e.server.copyToClient(compose);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/dzi/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@alleninstitute/vis-dzi",
"version": "0.0.5",
"version": "0.0.6",
"contributors": [
{
"name": "Lane Sawyer",
Expand Down Expand Up @@ -55,4 +55,4 @@
"lodash": "^4.17.21",
"regl": "^2.1.0"
}
}
}
2 changes: 1 addition & 1 deletion packages/dzi/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type DziTile = {
function tileUrl(dzi: DziImage, level: number, tile: TileIndex): string {
return `${dzi.imagesUrl}${level.toFixed(0)}/${tile.col.toFixed(0)}_${tile.row.toFixed(0)}.${dzi.format}`;
}
// some quick notes on this deepzoom image format:
// some quick notes on this deep zoom image format:
// 1. image / tile names are given by {column}_{row}.{format}
// 2. a layer (which may contain multiple tiles) is a folder
// 2.1 that folder contains all the tiles for that layer.
Expand Down
22 changes: 20 additions & 2 deletions packages/dzi/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,26 @@ import { buildTileRenderer } from './tile-renderer';

export type RenderSettings = {
camera: {
/**
* a region of a dzi image, expressed as a relative parameter (eg. [0,0],[1,1] means the whole image)
*/
view: box2D;
/**
* the resolution of the output screen on which to project the region of source pixels given by view
*/
screenSize: vec2;
};
};

type GpuProps = {
pixels: CachedTexture;
};
/**
*
* @param regl a valid REGL context (https://github.com/regl-project/regl)
* @returns an object which can fetch tiles from a DeepZoomImage, determine the visibility of those tiles given a simple camera, and render said tiles
* using regl (which uses webGL)
*/
export function buildDziRenderer(regl: REGL.Regl): Renderer<DziImage, DziTile, RenderSettings, GpuProps> {
const renderCmd = buildTileRenderer(regl, { enable: false });
const fetchDziTile = (
Expand All @@ -45,7 +57,7 @@ export function buildDziRenderer(regl: REGL.Regl): Renderer<DziImage, DziTile, R
};
};
return {
destroy: () => {}, // no private resources to destroy
destroy: () => { }, // no private resources to destroy
cacheKey: (item, _requestKey, _data, _settings) => `${item.url}`,
fetchItemContent: fetchDziTile,
getVisibleItems: (dzi, settings) => {
Expand All @@ -68,7 +80,13 @@ export function buildDziRenderer(regl: REGL.Regl): Renderer<DziImage, DziTile, R
},
};
}

/**
*
* @param regl a valid REGL context (https://github.com/regl-project/regl)
* @returns a function which creates a "Frame" of actions. each action represents loading
* and subsequently rendering a tile of the image as requested via its configuration -
* @see RenderSettings
*/
export function buildAsyncDziRenderer(regl: REGL.Regl) {
return buildAsyncRenderer(buildDziRenderer(regl));
}
4 changes: 2 additions & 2 deletions packages/omezarr/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@alleninstitute/vis-omezarr",
"version": "0.0.2",
"version": "0.0.3",
"contributors": [
{
"name": "Lane Sawyer",
Expand Down Expand Up @@ -55,4 +55,4 @@
"regl": "^2.1.0",
"zarrita": "0.4.0-next.14"
}
}
}
22 changes: 21 additions & 1 deletion packages/omezarr/src/sliceview/loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ZarrDataset } from '../zarr-data';
import { sizeInUnits, type ZarrDataset } from '../zarr-data';
import { describe, expect, it } from 'vitest';
import { Box2D, box2D } from '@alleninstitute/vis-geometry';
import { getVisibleTiles } from './loader';
Expand Down Expand Up @@ -192,4 +192,24 @@ describe('omezarr basic tiled loading', () => {
expect(visible[0].bounds).toEqual(Box2D.create([0, 0], [x, y]));
});
});
describe('sizeInUnits', () => {
it('respects scale transformations', () => {
const pyramid = exampleOmeZarr.multiscales[0]
const { axes, datasets } = pyramid

const layer9xy = sizeInUnits('xy', axes, datasets[9])
const layer0xy = sizeInUnits('xy', axes, datasets[0])

const layer9yz = sizeInUnits('yz', axes, datasets[9])
const layer0yz = sizeInUnits('yz', axes, datasets[0])
// we're looking at the highest resolution and lowest resolution layers.
// I think in an ideal world, we'd expect each layer to end up having an exactly equal size,
// however I think that isnt happening here for floating-point reasons - so the small differences are acceptable.
expect(layer9xy).toEqual([13.9776, 10.3936])
expect(layer0xy).toEqual([13.9993, 10.4993])
// note the Y coordinate (last above, first below) is as expected:
expect(layer9yz).toEqual([10.3936, 14.200000000000001])
expect(layer0yz).toEqual([10.4993, 14.200000000000001])
})
})
});
25 changes: 23 additions & 2 deletions packages/omezarr/src/sliceview/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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)
planeIndex: number; // the index of this slice along the axis being sliced (orthogonal to plane)
layerIndex: number; // the index in the resolution pyramid of the omezarr dataset
};

Expand Down Expand Up @@ -67,6 +67,19 @@ function getVisibleTilesInLayer(
});
return visibleTiles;
}
/**
* get tiles of the omezarr image which are visible (intersect with @param camera.view).
* @param camera an object describing the current view: the region of the omezarr, and the resolution at which it
* will be displayed.
* @param plane the plane (eg. 'xy') from which to draw tiles
* @param planeIndex the index of the plane along the orthogonal axis (if plane is xy, then the planes are slices along the Z axis)
* note that not all ome-zarr LOD layers can be expected to have the same number of slices! an index which exists at a high LOD may not
* exist at a low LOD.
* @param dataset the omezarr image to pull tiles from
* @param tileSize the size of the tiles, in pixels. it is recommended to use a size that agrees with the chunking used in the dataset, however,
* other utilities in this library will stitch together chunks to satisfy the requested tile size.
* @returns an array of objects representing tiles (bounding information, etc) which are visible from the given dataset.
*/
export function getVisibleTiles(
camera: {
view: box2D;
Expand Down Expand Up @@ -98,7 +111,15 @@ export function getVisibleTiles(
}
return getVisibleTilesInLayer(camera, plane, planeIndex, dataset, tileSize, layerIndex);
}

/**
* a function which returns a promise of float32 data from the requested region of an omezarr dataset.
* Note that omezarr decoding can be slow - consider wrapping this function in a web-worker (or a pool of them)
* to improve performance (note also that the webworker message passing will need to itself be wrapped in promises)
* @param metadata an omezarr object
* @param r a slice request @see getSlice
* @param layerIndex an index into the LOD pyramid of the given ZarrDataset.
* @returns the requested voxel information from the given layer of the given dataset.
*/
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;
Expand Down
18 changes: 12 additions & 6 deletions packages/omezarr/src/sliceview/tile-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ import REGL, { type Framebuffer2D } from 'regl';

type Props = {
target: Framebuffer2D | null;
tile: vec4;
view: vec4;
Rgamut: vec2;
Ggamut: vec2;
Bgamut: vec2;
tile: vec4; // [minx,miny,maxx,maxy] representing the bounding box of the tile we're rendering
view: vec4; // [minx,miny,maxx,maxy] representing the camera in the same space as the tile's bounding box
Rgamut: vec2; // [min,max] RedOut = RedChannelValue-Rgamut.min/(Rgamut.max-Rgamut.min)
Ggamut: vec2; // [min,max] GreenOut = GreenChannelValue-Ggamut.min/(Ggamut.max-Ggamut.min)
Bgamut: vec2; // [min,max] BlueOut = BlueChannelValue-Bgamut.min/(Bgamut.max-Bgamut.min)
R: REGL.Texture2D;
G: REGL.Texture2D;
B: REGL.Texture2D;
};

/**
*
* @param regl an active REGL context
* @returns a function (regl command) which renders 3 individual channels as the RGB
* components of an image. Each channel is mapped to the output RGB space via the given Gamut.
* the rendering is done in the given target buffer (or null for the screen).
*/
export function buildTileRenderer(regl: REGL.Regl) {
const cmd = regl<
{
Expand Down
Loading
Loading