Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions src/datasource/precomputed/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ export interface MultiscaleVolumeInfo {
modelSpace: CoordinateSpace;
}

// Volume of a single voxel, used to order scales from finest to coarsest. Only
// the spatial dimensions participate; the channel resolution is always 1.
function voxelVolume(resolution: Float64Array) {
return resolution[0] * resolution[1] * resolution[2];
}

export function parseMultiscaleVolumeInfo(obj: unknown): MultiscaleVolumeInfo {
verifyObject(obj);
const dataType = verifyObjectProperty(obj, "data_type", (x) =>
Expand Down Expand Up @@ -257,6 +263,16 @@ export function parseMultiscaleVolumeInfo(obj: unknown): MultiscaleVolumeInfo {
parseArray(x, (y) => new ScaleInfo(y, numChannels)),
);
if (scaleInfos.length === 0) throw new Error("Expected at least one scale");
// The format requires the resolution not to decrease as the index into
// `"scales"` increases, but files that list the scales in another order do
// occur in practice. Sort them rather than trusting the order: the first
// scale defines the model coordinate space, and `getSources` must return the
// scales ordered from finest to coarsest. Sorting is a no-op for conforming
// files, since `Array.prototype.sort` is stable and the voxel volume cannot
// decrease while no individual resolution does.
scaleInfos.sort(
(a, b) => voxelVolume(a.resolution) - voxelVolume(b.resolution),
);
const baseScale = scaleInfos[0];
const rank = numChannels === 1 ? 3 : 4;
const scales = new Float64Array(rank);
Expand Down
29 changes: 27 additions & 2 deletions testdata/datasource/precomputed/generate_precomputed.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# "numpy",
# ]
# ///
import json
import os
import shutil

Expand Down Expand Up @@ -33,14 +34,38 @@ def write_multiscale(path: str, num_channels: int, num_scales: int):
).result()


def write_reversed_scales(source_path: str, path: str):
"""Writes a copy of `source_path` with the scales listed coarsest first.

The format requires the resolution not to decrease as the index into
`"scales"` increases, but files violating that do occur in practice, so
Neuroglancer sorts the scales rather than trusting the order. tensorstore
always writes them in order, hence this derived copy.
"""
shutil.rmtree(path, ignore_errors=True)
os.makedirs(path)
with open(os.path.join(source_path, "info")) as f:
info = json.load(f)
info["scales"] = info["scales"][::-1]
with open(os.path.join(path, "info"), "w") as f:
json.dump(info, f)


base = os.path.abspath(os.path.dirname(__file__))

write_multiscale(
os.path.abspath(os.path.join(os.path.dirname(__file__), "one_channel")),
os.path.join(base, "one_channel"),
num_channels=1,
num_scales=3,
)

write_multiscale(
os.path.abspath(os.path.join(os.path.dirname(__file__), "two_channels")),
os.path.join(base, "two_channels"),
num_channels=2,
num_scales=3,
)

write_reversed_scales(
os.path.join(base, "one_channel"),
os.path.join(base, "reversed_scales"),
)
1 change: 1 addition & 0 deletions testdata/datasource/precomputed/reversed_scales/info
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"@type":"neuroglancer_multiscale_volume","data_type":"uint16","num_channels":1,"scales":[{"chunk_sizes":[[3,5,8]],"encoding":"raw","key":"12_16_20","resolution":[12,16,20],"size":[3,5,8],"voxel_offset":[0,0,0]},{"chunk_sizes":[[5,10,15]],"encoding":"raw","key":"6_8_10","resolution":[6,8,10],"size":[5,10,15],"voxel_offset":[0,0,0]},{"chunk_sizes":[[10,20,30]],"encoding":"raw","key":"3_4_5","resolution":[3,4,5],"size":[10,20,30],"voxel_offset":[0,0,0]}],"type":"image"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
{
"canonicalUrl": "http://localhost:*/datasource/precomputed/reversed_scales/|neuroglancer-precomputed:",
"modelTransform": {
"inputSpace": {
"bounds": {
"lowerBounds": Float64Array [
0,
0,
0,
],
"upperBounds": Float64Array [
10,
20,
30,
],
"voxelCenterAtIntegerCoordinates": [
false,
false,
false,
],
},
"coordinateArrays": [
,
,
,
],
"names": [
"x",
"y",
"z",
],
"scales": Float64Array [
3e-9,
4e-9,
5e-9,
],
"units": [
"m",
"m",
"m",
],
"valid": true,
},
},
"subsources": [
{
"default": true,
"id": "default",
"subsource": {
"volume": {
"dataType": "UINT16",
"rank": 3,
"sources": [
[
{
"chunkSource": {
"parameters": {
"encoding": 0,
"sharding": undefined,
"url": "http://localhost:*/datasource/precomputed/reversed_scales/3_4_5/",
},
"spec": {
"baseVoxelOffset": [
0,
0,
0,
],
"chunkDataSize": [
10,
20,
30,
],
"compressedSegmentationBlockSize": undefined,
"dataType": "UINT16",
"lowerVoxelBound": [
0,
0,
0,
],
"upperVoxelBound": [
10,
20,
30,
],
},
},
"lowerClipBound": Float32Array [
0,
0,
0,
],
"upperClipBound": Float32Array [
10,
20,
30,
],
},
{
"chunkSource": {
"parameters": {
"encoding": 0,
"sharding": undefined,
"url": "http://localhost:*/datasource/precomputed/reversed_scales/6_8_10/",
},
"spec": {
"baseVoxelOffset": [
0,
0,
0,
],
"chunkDataSize": [
5,
10,
15,
],
"compressedSegmentationBlockSize": undefined,
"dataType": "UINT16",
"lowerVoxelBound": [
0,
0,
0,
],
"upperVoxelBound": [
5,
10,
15,
],
},
},
"chunkToMultiscaleTransform": [
[
2,
0,
0,
0,
],
[
0,
2,
0,
0,
],
[
0,
0,
2,
0,
],
[
0,
0,
0,
1,
],
],
"lowerClipBound": Float32Array [
0,
0,
0,
],
"upperClipBound": Float32Array [
5,
10,
15,
],
},
{
"chunkSource": {
"parameters": {
"encoding": 0,
"sharding": undefined,
"url": "http://localhost:*/datasource/precomputed/reversed_scales/12_16_20/",
},
"spec": {
"baseVoxelOffset": [
0,
0,
0,
],
"chunkDataSize": [
3,
5,
8,
],
"compressedSegmentationBlockSize": undefined,
"dataType": "UINT16",
"lowerVoxelBound": [
0,
0,
0,
],
"upperVoxelBound": [
3,
5,
8,
],
},
},
"chunkToMultiscaleTransform": [
[
4,
0,
0,
0,
],
[
0,
4,
0,
0,
],
[
0,
0,
4,
0,
],
[
0,
0,
0,
1,
],
],
"lowerClipBound": Float32Array [
0,
0,
0,
],
"upperClipBound": Float32Array [
2.5,
5,
7.5,
],
},
],
],
"volumeType": "IMAGE",
},
},
},
{
"default": true,
"id": "bounds",
"subsource": {
"staticAnnotations": [
{
"description": "Data Bounds",
"id": "data-bounds",
"pointA": [
0,
0,
0,
],
"pointB": [
10,
20,
30,
],
"type": "axis_aligned_bounding_box",
},
],
},
},
],
}
9 changes: 8 additions & 1 deletion tests/datasource/precomputed.browser_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ import "#src/datasource/precomputed/register_default";
import "#src/sliceview/uncompressed_chunk_format.js";
import { datasourceMetadataSnapshotTests } from "#tests/datasource/metadata_snapshot_test_util.js";

datasourceMetadataSnapshotTests("precomputed", ["one_channel", "two_channels"]);
datasourceMetadataSnapshotTests("precomputed", [
"one_channel",
"two_channels",
// Same volume as `one_channel`, but with the scales listed coarsest first.
// The metadata must be identical apart from the URL, since the scales are
// sorted rather than trusted to be in order.
"reversed_scales",
]);