@fideus-labs/fidnii 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +9 -0
- package/README.md +180 -0
- package/dist/BufferManager.d.ts +86 -0
- package/dist/BufferManager.d.ts.map +1 -0
- package/dist/BufferManager.js +146 -0
- package/dist/BufferManager.js.map +1 -0
- package/dist/ClipPlanes.d.ts +180 -0
- package/dist/ClipPlanes.d.ts.map +1 -0
- package/dist/ClipPlanes.js +513 -0
- package/dist/ClipPlanes.js.map +1 -0
- package/dist/OMEZarrNVImage.d.ts +545 -0
- package/dist/OMEZarrNVImage.d.ts.map +1 -0
- package/dist/OMEZarrNVImage.js +1799 -0
- package/dist/OMEZarrNVImage.js.map +1 -0
- package/dist/RegionCoalescer.d.ts +75 -0
- package/dist/RegionCoalescer.d.ts.map +1 -0
- package/dist/RegionCoalescer.js +151 -0
- package/dist/RegionCoalescer.js.map +1 -0
- package/dist/ResolutionSelector.d.ts +88 -0
- package/dist/ResolutionSelector.d.ts.map +1 -0
- package/dist/ResolutionSelector.js +224 -0
- package/dist/ResolutionSelector.js.map +1 -0
- package/dist/ViewportBounds.d.ts +50 -0
- package/dist/ViewportBounds.d.ts.map +1 -0
- package/dist/ViewportBounds.js +325 -0
- package/dist/ViewportBounds.js.map +1 -0
- package/dist/events.d.ts +122 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +273 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +126 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/affine.d.ts +72 -0
- package/dist/utils/affine.d.ts.map +1 -0
- package/dist/utils/affine.js +173 -0
- package/dist/utils/affine.js.map +1 -0
- package/dist/utils/coordinates.d.ts +80 -0
- package/dist/utils/coordinates.d.ts.map +1 -0
- package/dist/utils/coordinates.js +207 -0
- package/dist/utils/coordinates.js.map +1 -0
- package/package.json +61 -0
- package/src/BufferManager.ts +176 -0
- package/src/ClipPlanes.ts +640 -0
- package/src/OMEZarrNVImage.ts +2286 -0
- package/src/RegionCoalescer.ts +217 -0
- package/src/ResolutionSelector.ts +325 -0
- package/src/ViewportBounds.ts +369 -0
- package/src/events.ts +146 -0
- package/src/index.ts +153 -0
- package/src/types.ts +429 -0
- package/src/utils/affine.ts +218 -0
- package/src/utils/coordinates.ts +271 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import * as zarr from "zarrita";
|
|
5
|
+
import type { NgffImage } from "@fideus-labs/ngff-zarr";
|
|
6
|
+
import { zarrGet } from "@fideus-labs/ngff-zarr/browser";
|
|
7
|
+
import type {
|
|
8
|
+
ChunkCache,
|
|
9
|
+
PixelRegion,
|
|
10
|
+
RegionFetchResult,
|
|
11
|
+
TypedArray,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Represents a pending request that may have multiple consumers waiting for the result.
|
|
16
|
+
*/
|
|
17
|
+
interface PendingRequest {
|
|
18
|
+
/** The promise that resolves when the request completes */
|
|
19
|
+
promise: Promise<RegionFetchResult>;
|
|
20
|
+
/** Function to resolve the promise with the result */
|
|
21
|
+
resolve: (data: RegionFetchResult) => void;
|
|
22
|
+
/** Function to reject the promise with an error */
|
|
23
|
+
reject: (error: Error) => void;
|
|
24
|
+
/** Set of requester IDs waiting for this result */
|
|
25
|
+
requesters: Set<string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* RegionCoalescer handles fetching sub-regions from OME-Zarr images with:
|
|
30
|
+
*
|
|
31
|
+
* 1. Request deduplication - Multiple async triggers (zoom, crop changes, etc.)
|
|
32
|
+
* requesting the same region receive the same promise
|
|
33
|
+
* 2. Parallel fetching - Uses fizarrita's worker-pool-accelerated zarrGet
|
|
34
|
+
* for concurrent, worker-offloaded chunk fetches
|
|
35
|
+
* 3. Requester tracking - Tracks who is waiting for each request
|
|
36
|
+
* 4. Chunk caching - Optional decoded-chunk cache to avoid redundant
|
|
37
|
+
* decompression on repeated or overlapping reads
|
|
38
|
+
*
|
|
39
|
+
* This design supports future scenarios where multiple UI events may trigger
|
|
40
|
+
* overlapping region requests simultaneously.
|
|
41
|
+
*/
|
|
42
|
+
export class RegionCoalescer {
|
|
43
|
+
private readonly pending: Map<string, PendingRequest> = new Map();
|
|
44
|
+
|
|
45
|
+
/** Optional decoded-chunk cache forwarded to fizarrita's getWorker. */
|
|
46
|
+
private readonly _cache: ChunkCache | undefined;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param cache - Optional decoded-chunk cache. When provided, `zarrGet`
|
|
50
|
+
* caches decoded chunks to avoid redundant decompression on repeated
|
|
51
|
+
* or overlapping reads.
|
|
52
|
+
*/
|
|
53
|
+
constructor(cache?: ChunkCache) {
|
|
54
|
+
this._cache = cache;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate a unique key for a request based on image path, level index, and region.
|
|
59
|
+
*/
|
|
60
|
+
private makeKey(
|
|
61
|
+
imagePath: string,
|
|
62
|
+
levelIndex: number,
|
|
63
|
+
region: PixelRegion,
|
|
64
|
+
): string {
|
|
65
|
+
const start = region.start.join(",");
|
|
66
|
+
const end = region.end.join(",");
|
|
67
|
+
return `${imagePath}:${levelIndex}:${start}:${end}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Request a region, coalescing with any in-flight request for the same data.
|
|
72
|
+
*
|
|
73
|
+
* @param ngffImage - The NgffImage to fetch from
|
|
74
|
+
* @param levelIndex - The resolution level index
|
|
75
|
+
* @param region - The pixel region to fetch
|
|
76
|
+
* @param requesterId - ID of the requester (e.g., 'zoom', 'crop-change', 'progressive-load')
|
|
77
|
+
* @returns The fetched region data
|
|
78
|
+
*/
|
|
79
|
+
async fetchRegion(
|
|
80
|
+
ngffImage: NgffImage,
|
|
81
|
+
levelIndex: number,
|
|
82
|
+
region: PixelRegion,
|
|
83
|
+
requesterId: string = "default",
|
|
84
|
+
): Promise<RegionFetchResult> {
|
|
85
|
+
const key = this.makeKey(ngffImage.data.path, levelIndex, region);
|
|
86
|
+
|
|
87
|
+
// Check if there's already a pending request for this data
|
|
88
|
+
const existing = this.pending.get(key);
|
|
89
|
+
if (existing) {
|
|
90
|
+
// Add this requester to the waiters and return the existing promise
|
|
91
|
+
existing.requesters.add(requesterId);
|
|
92
|
+
return existing.promise;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create a new pending request
|
|
96
|
+
let resolvePromise!: (data: RegionFetchResult) => void;
|
|
97
|
+
let rejectPromise!: (error: Error) => void;
|
|
98
|
+
|
|
99
|
+
const promise = new Promise<RegionFetchResult>((resolve, reject) => {
|
|
100
|
+
resolvePromise = resolve;
|
|
101
|
+
rejectPromise = reject;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const pendingRequest: PendingRequest = {
|
|
105
|
+
promise,
|
|
106
|
+
resolve: resolvePromise,
|
|
107
|
+
reject: rejectPromise,
|
|
108
|
+
requesters: new Set([requesterId]),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
this.pending.set(key, pendingRequest);
|
|
112
|
+
|
|
113
|
+
// Fetch using fizarrita's worker-accelerated zarrGet
|
|
114
|
+
try {
|
|
115
|
+
const selection = [
|
|
116
|
+
zarr.slice(region.start[0], region.end[0]),
|
|
117
|
+
zarr.slice(region.start[1], region.end[1]),
|
|
118
|
+
zarr.slice(region.start[2], region.end[2]),
|
|
119
|
+
];
|
|
120
|
+
// Pass the chunk cache to fizarrita's getWorker via zarrGet.
|
|
121
|
+
// The `cache` option is available in @fideus-labs/fizarrita >=1.2.0.
|
|
122
|
+
const zarrOpts = this._cache
|
|
123
|
+
? { cache: this._cache } as Record<string, unknown>
|
|
124
|
+
: undefined;
|
|
125
|
+
const result = await zarrGet(ngffImage.data, selection, zarrOpts);
|
|
126
|
+
|
|
127
|
+
const fetchResult: RegionFetchResult = {
|
|
128
|
+
data: result.data as TypedArray,
|
|
129
|
+
shape: result.shape,
|
|
130
|
+
stride: result.stride,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
pendingRequest.resolve(fetchResult);
|
|
134
|
+
return fetchResult;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
137
|
+
pendingRequest.reject(err);
|
|
138
|
+
throw err;
|
|
139
|
+
} finally {
|
|
140
|
+
this.pending.delete(key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Fetch multiple regions in parallel, with deduplication.
|
|
146
|
+
* Useful for fetching multiple chunks for a single view update.
|
|
147
|
+
*
|
|
148
|
+
* @param ngffImage - The NgffImage to fetch from
|
|
149
|
+
* @param levelIndex - The resolution level index
|
|
150
|
+
* @param regions - Array of pixel regions to fetch
|
|
151
|
+
* @param requesterId - ID of the requester
|
|
152
|
+
* @returns Array of fetched region data
|
|
153
|
+
*/
|
|
154
|
+
async fetchRegions(
|
|
155
|
+
ngffImage: NgffImage,
|
|
156
|
+
levelIndex: number,
|
|
157
|
+
regions: PixelRegion[],
|
|
158
|
+
requesterId: string = "default",
|
|
159
|
+
): Promise<RegionFetchResult[]> {
|
|
160
|
+
return Promise.all(
|
|
161
|
+
regions.map((region) =>
|
|
162
|
+
this.fetchRegion(ngffImage, levelIndex, region, requesterId)
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if there's a pending request for the given parameters.
|
|
169
|
+
*/
|
|
170
|
+
hasPending(
|
|
171
|
+
ngffImage: NgffImage,
|
|
172
|
+
levelIndex: number,
|
|
173
|
+
region: PixelRegion,
|
|
174
|
+
): boolean {
|
|
175
|
+
const key = this.makeKey(ngffImage.data.path, levelIndex, region);
|
|
176
|
+
return this.pending.has(key);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the set of requesters waiting for a pending request.
|
|
181
|
+
* Returns undefined if no pending request exists.
|
|
182
|
+
*/
|
|
183
|
+
getPendingRequesters(
|
|
184
|
+
ngffImage: NgffImage,
|
|
185
|
+
levelIndex: number,
|
|
186
|
+
region: PixelRegion,
|
|
187
|
+
): Set<string> | undefined {
|
|
188
|
+
const key = this.makeKey(ngffImage.data.path, levelIndex, region);
|
|
189
|
+
return this.pending.get(key)?.requesters;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Wait for all pending fetches to complete.
|
|
194
|
+
*/
|
|
195
|
+
async onIdle(): Promise<void> {
|
|
196
|
+
// Wait for all in-flight requests to settle
|
|
197
|
+
const promises = Array.from(this.pending.values()).map((p) =>
|
|
198
|
+
p.promise.catch(() => {})
|
|
199
|
+
);
|
|
200
|
+
await Promise.all(promises);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the number of pending requests (unique region requests).
|
|
205
|
+
*/
|
|
206
|
+
get pendingCount(): number {
|
|
207
|
+
return this.pending.size;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Clear all pending requests.
|
|
212
|
+
* Note: Does not resolve or reject pending promises.
|
|
213
|
+
*/
|
|
214
|
+
clear(): void {
|
|
215
|
+
this.pending.clear();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { Multiscales, NgffImage } from "@fideus-labs/ngff-zarr";
|
|
5
|
+
import type {
|
|
6
|
+
ClipPlanes,
|
|
7
|
+
PixelRegion,
|
|
8
|
+
ResolutionSelection,
|
|
9
|
+
VolumeBounds,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
import { clipPlanesToPixelRegion } from "./ClipPlanes.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Orthogonal axis index in [z, y, x] order.
|
|
15
|
+
* 0 = Z (axial view), 1 = Y (coronal view), 2 = X (sagittal view)
|
|
16
|
+
*/
|
|
17
|
+
export type OrthogonalAxis = 0 | 1 | 2;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Select the appropriate resolution level based on pixel budget and clip planes.
|
|
21
|
+
*
|
|
22
|
+
* The selection process:
|
|
23
|
+
* 1. Starts from the highest resolution (level 0)
|
|
24
|
+
* 2. Finds the highest resolution that fits within maxPixels
|
|
25
|
+
* 3. Considers the clipped region size, not full volume
|
|
26
|
+
*
|
|
27
|
+
* @param multiscales - The OME-Zarr multiscales data
|
|
28
|
+
* @param maxPixels - Maximum number of pixels to use
|
|
29
|
+
* @param clipPlanes - Current clip planes in world space
|
|
30
|
+
* @param volumeBounds - Full volume bounds in world space
|
|
31
|
+
* @param viewportBounds - Optional viewport bounds (for viewport-aware mode)
|
|
32
|
+
* @returns The selected resolution level and buffer dimensions
|
|
33
|
+
*/
|
|
34
|
+
export function selectResolution(
|
|
35
|
+
multiscales: Multiscales,
|
|
36
|
+
maxPixels: number,
|
|
37
|
+
clipPlanes: ClipPlanes,
|
|
38
|
+
volumeBounds: VolumeBounds,
|
|
39
|
+
viewportBounds?: VolumeBounds,
|
|
40
|
+
): ResolutionSelection {
|
|
41
|
+
const images = multiscales.images;
|
|
42
|
+
|
|
43
|
+
// Try each resolution from highest to lowest
|
|
44
|
+
for (let i = 0; i < images.length; i++) {
|
|
45
|
+
const image = images[i];
|
|
46
|
+
const region = clipPlanesToPixelRegion(
|
|
47
|
+
clipPlanes,
|
|
48
|
+
volumeBounds,
|
|
49
|
+
image,
|
|
50
|
+
viewportBounds,
|
|
51
|
+
);
|
|
52
|
+
const alignedRegion = alignRegionToChunks(region, image);
|
|
53
|
+
|
|
54
|
+
const dimensions: [number, number, number] = [
|
|
55
|
+
alignedRegion.end[0] - alignedRegion.start[0],
|
|
56
|
+
alignedRegion.end[1] - alignedRegion.start[1],
|
|
57
|
+
alignedRegion.end[2] - alignedRegion.start[2],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const pixelCount = dimensions[0] * dimensions[1] * dimensions[2];
|
|
61
|
+
|
|
62
|
+
if (pixelCount <= maxPixels) {
|
|
63
|
+
return {
|
|
64
|
+
levelIndex: i,
|
|
65
|
+
dimensions,
|
|
66
|
+
pixelCount,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fall back to lowest resolution
|
|
72
|
+
const lowestImage = images[images.length - 1];
|
|
73
|
+
const region = clipPlanesToPixelRegion(
|
|
74
|
+
clipPlanes,
|
|
75
|
+
volumeBounds,
|
|
76
|
+
lowestImage,
|
|
77
|
+
viewportBounds,
|
|
78
|
+
);
|
|
79
|
+
const alignedRegion = alignRegionToChunks(region, lowestImage);
|
|
80
|
+
|
|
81
|
+
const dimensions: [number, number, number] = [
|
|
82
|
+
alignedRegion.end[0] - alignedRegion.start[0],
|
|
83
|
+
alignedRegion.end[1] - alignedRegion.start[1],
|
|
84
|
+
alignedRegion.end[2] - alignedRegion.start[2],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
levelIndex: images.length - 1,
|
|
89
|
+
dimensions,
|
|
90
|
+
pixelCount: dimensions[0] * dimensions[1] * dimensions[2],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the chunk shape for a volume.
|
|
96
|
+
*
|
|
97
|
+
* @param ngffImage - The NgffImage to get chunk shape from
|
|
98
|
+
* @returns Chunk shape as [z, y, x]
|
|
99
|
+
*/
|
|
100
|
+
export function getChunkShape(ngffImage: NgffImage): [number, number, number] {
|
|
101
|
+
const chunks = ngffImage.data.chunks;
|
|
102
|
+
const dims = ngffImage.dims;
|
|
103
|
+
|
|
104
|
+
// Find z, y, x indices in dims
|
|
105
|
+
const zIdx = dims.indexOf("z");
|
|
106
|
+
const yIdx = dims.indexOf("y");
|
|
107
|
+
const xIdx = dims.indexOf("x");
|
|
108
|
+
|
|
109
|
+
if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
|
|
110
|
+
// Fallback: assume last 3 dimensions are z, y, x
|
|
111
|
+
const n = chunks.length;
|
|
112
|
+
return [chunks[n - 3] || 1, chunks[n - 2] || 1, chunks[n - 1] || 1];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [chunks[zIdx], chunks[yIdx], chunks[xIdx]];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the shape of a volume as [z, y, x].
|
|
120
|
+
*
|
|
121
|
+
* @param ngffImage - The NgffImage
|
|
122
|
+
* @returns Shape as [z, y, x]
|
|
123
|
+
*/
|
|
124
|
+
export function getVolumeShape(ngffImage: NgffImage): [number, number, number] {
|
|
125
|
+
const shape = ngffImage.data.shape;
|
|
126
|
+
const dims = ngffImage.dims;
|
|
127
|
+
|
|
128
|
+
// Find z, y, x indices in dims
|
|
129
|
+
const zIdx = dims.indexOf("z");
|
|
130
|
+
const yIdx = dims.indexOf("y");
|
|
131
|
+
const xIdx = dims.indexOf("x");
|
|
132
|
+
|
|
133
|
+
if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
|
|
134
|
+
// Fallback: assume last 3 dimensions are z, y, x
|
|
135
|
+
const n = shape.length;
|
|
136
|
+
return [shape[n - 3] || 1, shape[n - 2] || 1, shape[n - 1] || 1];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [shape[zIdx], shape[yIdx], shape[xIdx]];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Align a pixel region to chunk boundaries.
|
|
144
|
+
* Expands the region to include complete chunks.
|
|
145
|
+
*
|
|
146
|
+
* @param region - The pixel region to align
|
|
147
|
+
* @param ngffImage - The NgffImage (for chunk shape)
|
|
148
|
+
* @returns Aligned region
|
|
149
|
+
*/
|
|
150
|
+
export function alignRegionToChunks(
|
|
151
|
+
region: PixelRegion,
|
|
152
|
+
ngffImage: NgffImage,
|
|
153
|
+
): PixelRegion {
|
|
154
|
+
const chunkShape = getChunkShape(ngffImage);
|
|
155
|
+
const volumeShape = getVolumeShape(ngffImage);
|
|
156
|
+
|
|
157
|
+
// Align start down to chunk boundary
|
|
158
|
+
const alignedStart: [number, number, number] = [
|
|
159
|
+
Math.floor(region.start[0] / chunkShape[0]) * chunkShape[0],
|
|
160
|
+
Math.floor(region.start[1] / chunkShape[1]) * chunkShape[1],
|
|
161
|
+
Math.floor(region.start[2] / chunkShape[2]) * chunkShape[2],
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// Align end up to chunk boundary (but don't exceed volume size)
|
|
165
|
+
const alignedEnd: [number, number, number] = [
|
|
166
|
+
Math.min(
|
|
167
|
+
Math.ceil(region.end[0] / chunkShape[0]) * chunkShape[0],
|
|
168
|
+
volumeShape[0],
|
|
169
|
+
),
|
|
170
|
+
Math.min(
|
|
171
|
+
Math.ceil(region.end[1] / chunkShape[1]) * chunkShape[1],
|
|
172
|
+
volumeShape[1],
|
|
173
|
+
),
|
|
174
|
+
Math.min(
|
|
175
|
+
Math.ceil(region.end[2] / chunkShape[2]) * chunkShape[2],
|
|
176
|
+
volumeShape[2],
|
|
177
|
+
),
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
start: alignedStart,
|
|
182
|
+
end: alignedEnd,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Calculate the middle resolution level index.
|
|
188
|
+
*
|
|
189
|
+
* @param multiscales - The OME-Zarr multiscales data
|
|
190
|
+
* @returns Middle resolution level index
|
|
191
|
+
*/
|
|
192
|
+
export function getMiddleResolutionIndex(multiscales: Multiscales): number {
|
|
193
|
+
return Math.floor(multiscales.images.length / 2);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Calculate upsample factor from one resolution level to another.
|
|
198
|
+
*
|
|
199
|
+
* @param multiscales - The OME-Zarr multiscales data
|
|
200
|
+
* @param fromLevel - Source resolution level
|
|
201
|
+
* @param toLevel - Target resolution level (should be higher resolution, i.e., lower index)
|
|
202
|
+
* @returns Upsample factor for each dimension [z, y, x]
|
|
203
|
+
*/
|
|
204
|
+
export function calculateUpsampleFactor(
|
|
205
|
+
multiscales: Multiscales,
|
|
206
|
+
fromLevel: number,
|
|
207
|
+
toLevel: number,
|
|
208
|
+
): [number, number, number] {
|
|
209
|
+
const fromImage = multiscales.images[fromLevel];
|
|
210
|
+
const toImage = multiscales.images[toLevel];
|
|
211
|
+
|
|
212
|
+
const fromShape = getVolumeShape(fromImage);
|
|
213
|
+
const toShape = getVolumeShape(toImage);
|
|
214
|
+
|
|
215
|
+
return [
|
|
216
|
+
toShape[0] / fromShape[0],
|
|
217
|
+
toShape[1] / fromShape[1],
|
|
218
|
+
toShape[2] / fromShape[2],
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get dimensions for the full volume at a given resolution level.
|
|
224
|
+
*/
|
|
225
|
+
export function getFullVolumeDimensions(
|
|
226
|
+
multiscales: Multiscales,
|
|
227
|
+
levelIndex: number,
|
|
228
|
+
): [number, number, number] {
|
|
229
|
+
return getVolumeShape(multiscales.images[levelIndex]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Select the appropriate resolution level for a 2D slice view.
|
|
234
|
+
*
|
|
235
|
+
* Unlike `selectResolution` which counts all 3D pixels (z*y*x), this function
|
|
236
|
+
* counts only the 2D in-plane pixels (e.g., x*y for axial), ignoring the
|
|
237
|
+
* orthogonal axis. This allows much higher resolution for 2D slice views.
|
|
238
|
+
*
|
|
239
|
+
* The slab dimensions returned include one chunk of thickness in the
|
|
240
|
+
* orthogonal direction (needed for zarr fetching efficiency). The pixel
|
|
241
|
+
* budget accounts for the full 3D slab volume (in-plane area multiplied
|
|
242
|
+
* by the chunk thickness along the orthogonal axis).
|
|
243
|
+
*
|
|
244
|
+
* @param multiscales - The OME-Zarr multiscales data
|
|
245
|
+
* @param maxPixels - Maximum number of voxels for the slab (in-plane area × chunk depth)
|
|
246
|
+
* @param clipPlanes - Current clip planes in world space
|
|
247
|
+
* @param volumeBounds - Full volume bounds in world space
|
|
248
|
+
* @param orthogonalAxis - The axis perpendicular to the slice plane (0=Z, 1=Y, 2=X)
|
|
249
|
+
* @param viewportBounds - Optional viewport bounds (for viewport-aware mode)
|
|
250
|
+
* @returns The selected resolution level and slab dimensions
|
|
251
|
+
*/
|
|
252
|
+
export function select2DResolution(
|
|
253
|
+
multiscales: Multiscales,
|
|
254
|
+
maxPixels: number,
|
|
255
|
+
clipPlanes: ClipPlanes,
|
|
256
|
+
volumeBounds: VolumeBounds,
|
|
257
|
+
orthogonalAxis: OrthogonalAxis,
|
|
258
|
+
viewportBounds?: VolumeBounds,
|
|
259
|
+
): ResolutionSelection {
|
|
260
|
+
const images = multiscales.images;
|
|
261
|
+
|
|
262
|
+
// Try each resolution from highest to lowest
|
|
263
|
+
for (let i = 0; i < images.length; i++) {
|
|
264
|
+
const image = images[i];
|
|
265
|
+
const region = clipPlanesToPixelRegion(
|
|
266
|
+
clipPlanes,
|
|
267
|
+
volumeBounds,
|
|
268
|
+
image,
|
|
269
|
+
viewportBounds,
|
|
270
|
+
);
|
|
271
|
+
const alignedRegion = alignRegionToChunks(region, image);
|
|
272
|
+
|
|
273
|
+
const dimensions: [number, number, number] = [
|
|
274
|
+
alignedRegion.end[0] - alignedRegion.start[0],
|
|
275
|
+
alignedRegion.end[1] - alignedRegion.start[1],
|
|
276
|
+
alignedRegion.end[2] - alignedRegion.start[2],
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// Count the full slab volume: in-plane area × chunk depth along the
|
|
280
|
+
// orthogonal axis. The slab is always one chunk thick, so we use the
|
|
281
|
+
// chunk shape rather than the full volume extent in that axis.
|
|
282
|
+
const chunkShape = getChunkShape(image);
|
|
283
|
+
const slabDepth = chunkShape[orthogonalAxis];
|
|
284
|
+
const inPlaneAxes = ([0, 1, 2] as const).filter((a) =>
|
|
285
|
+
a !== orthogonalAxis
|
|
286
|
+
);
|
|
287
|
+
const slabVoxelCount = dimensions[inPlaneAxes[0]] *
|
|
288
|
+
dimensions[inPlaneAxes[1]] * slabDepth;
|
|
289
|
+
|
|
290
|
+
if (slabVoxelCount <= maxPixels) {
|
|
291
|
+
return {
|
|
292
|
+
levelIndex: i,
|
|
293
|
+
dimensions,
|
|
294
|
+
pixelCount: slabVoxelCount,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Fall back to lowest resolution
|
|
300
|
+
const lowestImage = images[images.length - 1];
|
|
301
|
+
const region = clipPlanesToPixelRegion(
|
|
302
|
+
clipPlanes,
|
|
303
|
+
volumeBounds,
|
|
304
|
+
lowestImage,
|
|
305
|
+
viewportBounds,
|
|
306
|
+
);
|
|
307
|
+
const alignedRegion = alignRegionToChunks(region, lowestImage);
|
|
308
|
+
|
|
309
|
+
const dimensions: [number, number, number] = [
|
|
310
|
+
alignedRegion.end[0] - alignedRegion.start[0],
|
|
311
|
+
alignedRegion.end[1] - alignedRegion.start[1],
|
|
312
|
+
alignedRegion.end[2] - alignedRegion.start[2],
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
const chunkShape = getChunkShape(lowestImage);
|
|
316
|
+
const slabDepth = chunkShape[orthogonalAxis];
|
|
317
|
+
const inPlaneAxes = ([0, 1, 2] as const).filter((a) => a !== orthogonalAxis);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
levelIndex: images.length - 1,
|
|
321
|
+
dimensions,
|
|
322
|
+
pixelCount: dimensions[inPlaneAxes[0]] * dimensions[inPlaneAxes[1]] *
|
|
323
|
+
slabDepth,
|
|
324
|
+
};
|
|
325
|
+
}
|