Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e61c736
first pass at implementing a decoder for ng-precomputed annotation files
froyo-np May 30, 2025
d17a40f
a little fiddling to promote type safety when extracting these annota…
froyo-np Jun 2, 2025
3c26561
fill out a renderer system for this data type (points only for now)
froyo-np Jun 2, 2025
7c39e47
first draft of one way of doing this
froyo-np Jun 3, 2025
7389252
Merge branch 'main' into noah/ng-annotations
froyo-np Jun 3, 2025
3f0a46d
less copy pasta
froyo-np Jun 3, 2025
cd1a2f0
Merge branch 'noah/ng-annotations' of https://github.com/AllenInstitu…
froyo-np Jun 3, 2025
26414bc
update omezarr render settings to be easier to use for "real" volumet…
froyo-np Jun 4, 2025
22b75f1
fix up handling of volumetric slicing. correct disparity between exam…
froyo-np Jun 4, 2025
ecf6256
fmt
froyo-np Jun 4, 2025
7a8cfdb
put the logger back
froyo-np Jun 4, 2025
98535c1
Merge branch 'main' into noah/ng-annotations
froyo-np Jun 4, 2025
89c233f
lint
froyo-np Jun 6, 2025
4a6a8e0
Merge branch 'noah/ng-annotations' of https://github.com/AllenInstitu…
froyo-np Jun 6, 2025
9b74b14
lockfile
froyo-np Jun 6, 2025
a830850
fmt again
froyo-np Jun 6, 2025
4de0fd7
Merge branch 'main' into noah/ng-annotations
froyo-np Jun 26, 2025
ac3d62e
missed a spot in the refactor
froyo-np Jun 26, 2025
65baf43
fmt
froyo-np Jun 26, 2025
8ef0e44
cleanup, fix the schema to match real data, rather than the documenta…
froyo-np Jun 26, 2025
b101c8a
fmt
froyo-np Jun 26, 2025
1d96b5e
timewasters
froyo-np Jun 26, 2025
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
15 changes: 15 additions & 0 deletions packages/core/src/abstract/render-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ export class RenderServer {
cache: AsyncDataCache<string, string, ReglCacheEntry>;
private clients: Map<Client, ClientEntry>;
private maxSize: vec2;
private cancelled: boolean;
constructor(maxSize: vec2, extensions: string[], cacheByteLimit: number = 2000 * oneMB) {
this.canvas = new OffscreenCanvas(10, 10); // we always render to private buffers, so we dont need a real resolution here...
this.clients = new Map();
this.maxSize = maxSize;
this.refreshRequested = false;
this.cancelled = false;
const gl = this.canvas.getContext('webgl', {
alpha: true,
preserveDrawingBuffer: false,
Expand Down Expand Up @@ -119,6 +121,9 @@ export class RenderServer {
}
}
private requestComposition(client: Client, composite: Compositor) {
if (this.cancelled) {
return;
}
const c = this.clients.get(client);
if (c) {
if (!c.updateRequested) {
Expand All @@ -145,6 +150,16 @@ export class RenderServer {
}
this.clients.delete(client);
}
destroyServer() {
this.cancelled = true; // we need this flag,
// because when we inform clients that they are cancelled,
// they could respond by requesting a new frame!
for (const c of this.clients.values()) {
c.frame?.cancelFrame();
}
this.clients.clear();
this.regl.destroy();
}
private prepareToRenderToClient(client: Client) {
const previousEntry = this.clients.get(client);
if (previousEntry) {
Expand Down
23 changes: 18 additions & 5 deletions packages/omezarr/src/sliceview/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@alleninstitute/vis-geometry';
import type { Chunk } from 'zarrita';
import type { ZarrRequest } from '../zarr/loading';
import { loadSlice, pickBestScale, planeSizeInVoxels, sizeInUnits } from '../zarr/loading';
import { indexOfRelativeSlice, loadSlice, pickBestScale, planeSizeInVoxels, sizeInUnits } from '../zarr/loading';
import type { VoxelTileImage } from './slice-renderer';
import type { OmeZarrMetadata, OmeZarrShapedDataset } from '../zarr/types';

Expand Down Expand Up @@ -93,16 +93,29 @@ export function getVisibleTiles(
view: box2D;
screenSize: vec2;
},
plane: CartesianPlane,
orthoVal: number,
plane: CartesianPlane, // the plane along which we extract a slice
planeLocation: // where that slice sits in the volume along the axis that is orthagonal to the plane of the slice - eg. Z for XY slices
| {
// EITHER
index: number; // the specific index (caution - not all volumes have the same number of slices at each level of detail)
parameter?: never;
} // OR
| {
parameter: number; // a parameter [0:1] along the axis, 0 would be the first slice, 1 would be the last
index?: never;
},
metadata: OmeZarrMetadata,
tileSize: number,
): VoxelTile[] {
// TODO (someday) open the array, look at its chunks, use that size for the size of the tiles I request!

const layer = pickBestScale(metadata, plane, camera.view, camera.screenSize);
return getVisibleTilesInLayer(camera, plane, orthoVal, metadata, tileSize, layer);
const sliceIndex =
planeLocation.index ??
indexOfRelativeSlice(layer, metadata.attrs.multiscales[0].axes, planeLocation.parameter, plane.ortho);

return getVisibleTilesInLayer(camera, plane, sliceIndex, metadata, tileSize, layer);
}

/**
* 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)
Expand Down
15 changes: 11 additions & 4 deletions packages/omezarr/src/sliceview/slice-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@ export type RenderSettingsChannel = {
export type RenderSettingsChannels = {
[key: string]: RenderSettingsChannel;
};

export type RenderSettings = {
camera: {
view: box2D;
screenSize: vec2;
};
orthoVal: number; // the value of the orthogonal axis, e.g. Z value relative to an XY plane
planeLocation:
| {
parameter?: never;
index: number;
}
| {
index?: never;
parameter: number;
};
tileSize: number;
plane: CartesianPlane;
channels: RenderSettingsChannels;
Expand Down Expand Up @@ -144,8 +151,8 @@ export function buildOmeZarrSliceRenderer(
},
destroy: () => {},
getVisibleItems: (dataset, settings) => {
const { camera, plane, orthoVal, tileSize } = settings;
return getVisibleTiles(camera, plane, orthoVal, dataset, tileSize);
const { camera, plane, planeLocation, tileSize } = settings;
return getVisibleTiles(camera, plane, planeLocation, dataset, tileSize);
},
fetchItemContent: (item, dataset, settings, signal) => {
const contents: Record<string, () => Promise<ReglCacheEntry>> = {};
Expand Down
19 changes: 18 additions & 1 deletion packages/omezarr/src/zarr/loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,27 @@ export function pickBestScale(
}, datasets[0]);
return choice ?? datasets[datasets.length - 1];
}

// TODO this is a duplicate of indexOfDimension... delete one of them!
function indexFor(dim: ZarrDimension, axes: readonly OmeZarrAxis[]) {
return axes.findIndex((axis) => axis.name === dim);
}
/**
*
* @param layer a shaped layer from within the omezarr dataset
* @param axes the axes describing this omezarr dataset
* @param parameter a value from [0:1] indicating a parameter of the volume, along the given dimension @param dim,
* @param dim the dimension (axis) along which @param parameter refers
* @returns a valid index (between [0,layer.shape[axis] ]) from the volume, suitable for
*/
export function indexOfRelativeSlice(
layer: OmeZarrShapedDataset,
axes: readonly OmeZarrAxis[],
parameter: number,
dim: ZarrDimension,
): number {
const dimIndex = indexFor(dim, axes);
return Math.floor(layer.shape[dimIndex] * Math.max(0, Math.min(1, parameter)));
}

/**
* determine the size of a slice of the volume, in the units specified by the axes metadata
Expand Down
55 changes: 55 additions & 0 deletions packages/precomputed/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@alleninstitute/vis-precomputed",
"version": "0.0.1",
"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]"
},
{
"name": "Joel Arbuckle",
"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": {
"typecheck": "tsc --noEmit",
"build": "parcel build --no-cache",
"watch": "parcel watch",
"test": "vitest --watch",
"test:ci": "vitest run",
"coverage": "vitest run --coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/AllenInstitute/vis.git"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com/AllenInstitute"
},
"packageManager": "[email protected]",
"dependencies": {
"@alleninstitute/vis-geometry": "workspace:*",
"@alleninstitute/vis-core": "workspace:*",
"regl": "2.1.0",
"ts-pattern": "5.7.1",
"zod": "3.24.3"
}
}
14 changes: 14 additions & 0 deletions packages/precomputed/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export {
type AnnotationInfo,
isPointAnnotation,
isBoxAnnotation,
isEllipsoidAnnotation,
isLineAnnotation,
parseInfoFromJson as ParseNGPrecomputedInfo,
getAnnotations,
} from './loader/annotations';
export {
buildNGPointAnnotationRenderer,
buildAsyncNGPointRenderer,
type AnnotationChunk,
} from './render/annotationRenderer';
70 changes: 70 additions & 0 deletions packages/precomputed/src/loader/annotations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest';
import {
AnnoStream,
type AnnotationInfo,
computeStride,
extractPoint,
getAnnotationBuffer,
isPointAnnotation,
parseInfoFromJson,
} from './annotations';

describe('quick check', () => {
it('can parse a real (although simple) file, at least a little bit', async () => {
const base =
'https://aind-open-data.s3.amazonaws.com/SmartSPIM_787715_2025-04-08_18-33-36_stitched_2025-04-09_22-42-59/image_cell_segmentation/Ex_445_Em_469/visualization/detected_precomputed/';
const expectedMetadata: AnnotationInfo<'point'> = {
annotation_type: 'point', // TODO: the real json files here use lowercase, wtf
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

remove todo

type: 'neuroglancer_annotations_v1',
dimensions: [
{ name: 'z', scale: 2e-6, unit: 'm' },
{ name: 'y', scale: 1.8e-6, unit: 'm' },
{ name: 'x', scale: 1.8e-6, unit: 'm' },
],
lower_bound: [4.0, 94.0, 558.0],
upper_bound: [3542.0, 8784.0, 7166.0],
properties: [],
relationships: [],
by_id: { key: 'by_id' }, //what?
spatial: [
{
key: 'spatial0',
grid_shape: [1, 1, 1],
chunk_size: [3538.0, 8690.0, 6608.0],
limit: 150378,
},
],
};
const infoFileJSON = await (await fetch(`${base}info`)).json();
// biome-ignore lint/style/noNonNullAssertion: this is a test
const sanitized = parseInfoFromJson(infoFileJSON)!;
const stride = computeStride(expectedMetadata);
expect(stride).toBe(12);
expect(sanitized).toEqual(expectedMetadata);
const raw = await getAnnotationBuffer(base, expectedMetadata, { level: 0, cell: [0, 0, 0] });
expect(raw.numAnnotations).toBe(150378n);
// each annotation (its shape and its properties) are written sequentially in the buffer,followed by all the ids for the annotations, like this:
// [{num_annotations:uint64},{annotation_0_and_properties_and_optional_padding},...,{annotation_n_and_properties_and_optional_padding},{id_of_anno_0:uint64},...,{id_of_anno_n:uint64}}]
// thus: 8 + (length*stride) + (size_in_bytes(uint64)*length)
expect(raw.view.buffer.byteLength).toBe(150378 * stride + 8 + 150378 * 8);
if (isPointAnnotation(sanitized)) {
const annoStream = await AnnoStream(sanitized, extractPoint, raw.view, raw.numAnnotations);
let count = 0;
// the ids in here just count up... I think the spec says they should be added at random when doing spatial indexing, so this is sus....
let lastId: bigint | undefined;
for (const point of annoStream) {
count += 1;
if (lastId === undefined) {
lastId = point.id;
} else {
expect(point.id).toBe(lastId + 1n);
lastId = point.id;
}
expect(point.properties).toEqual({});
}
expect(count).toBe(150378);
} else {
expect(sanitized?.annotation_type).toBe('point');
}
});
});
Loading