File size: 3,354 Bytes
42b53b4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | // SPDX-License-Identifier: Apache-2.0
// Where the CT volume comes from: the public OME-Zarr scans in the Vesuvius Challenge open data bucket,
// read straight from S3 over HTTPS (the bucket answers GET with Access-Control-Allow-Origin: *).
// The global endpoint is used: the us-east-1 regional host answers 403 for the same keys from here.
export const BUCKET = 'https://vesuvius-challenge-open-data.s3.amazonaws.com';
// scan volumes whose voxel grid the meshes of a scroll were traced on (tifxyz x, y, z = zarr x, y, z at level 0)
export const VOLUMES = {
PHerc0139: { zarr: `${BUCKET}/PHerc0139/volumes/20250728140407-9.362um-1.2m-113keV-masked.zarr`, voxelUM: 9.362 },
};
export function volumeFor(scroll, manifest) {
const q = new URLSearchParams(location.search).get('ctzarr');
if (q && q.startsWith(BUCKET + '/')) return { zarr: q.replace(/\/$/, ''), voxelUM: +(new URLSearchParams(location.search).get('ctum')) || 9.362 };
if (manifest?.ctZarr?.startsWith?.(BUCKET + '/')) return { zarr: manifest.ctZarr, voxelUM: manifest.ctVoxelUM || 9.362 };
return VOLUMES[scroll] || null;
}
// level metadata (.zarray, zarr v2)
const META = new Map();
export async function levelMeta(zarr, level) {
const k = `${zarr}/${level}`;
if (!META.has(k)) META.set(k, fetch(`${zarr}/${level}/.zarray`).then((r) => { if (!r.ok) throw new Error(`no level ${level} (${r.status})`); return r.json(); }));
return META.get(k);
}
// Read the box [start, start + size) (z, y, x) of one level into a Uint8Array (C order, x fastest).
// Simple path: uncompressed uint8 chunks fetched in parallel on the main thread; onChunk(box) reports each one.
export async function readRegion(zarr, level, start, size, onChunk, signal) {
const m = await levelMeta(zarr, level);
if (m.dtype !== '|u1' || m.compressor) throw new Error(`level ${level}: ${m.dtype} ${m.compressor?.id || 'raw'} needs the streaming reader`);
const [Z, Y, X] = size, out = new Uint8Array(Z * Y * X), C = m.chunks, sep = m.dimension_separator || '.';
const jobs = [];
for (let cz = Math.floor(start[0] / C[0]); cz * C[0] < start[0] + Z; cz++)
for (let cy = Math.floor(start[1] / C[1]); cy * C[1] < start[1] + Y; cy++)
for (let cx = Math.floor(start[2] / C[2]); cx * C[2] < start[2] + X; cx++) jobs.push([cz, cy, cx]);
let i = 0, n = 0;
const one = async ([cz, cy, cx]) => {
const r = await fetch(`${zarr}/${level}/${[cz, cy, cx].join(sep)}`, { signal });
const buf = r.ok ? new Uint8Array(await r.arrayBuffer()) : null; // a missing chunk is fill_value (0)
const z0 = Math.max(start[0], cz * C[0]), z1 = Math.min(start[0] + Z, (cz + 1) * C[0]);
const y0 = Math.max(start[1], cy * C[1]), y1 = Math.min(start[1] + Y, (cy + 1) * C[1]);
const x0 = Math.max(start[2], cx * C[2]), x1 = Math.min(start[2] + X, (cx + 1) * C[2]);
if (buf) for (let z = z0; z < z1; z++) for (let y = y0; y < y1; y++) {
const src = ((z - cz * C[0]) * C[1] + (y - cy * C[1])) * C[2] + (x0 - cx * C[2]);
out.set(buf.subarray(src, src + (x1 - x0)), ((z - start[0]) * Y + (y - start[1])) * X + (x0 - start[2]));
}
n++; onChunk?.({ done: n, total: jobs.length, bytes: buf ? buf.length : 0 });
};
await Promise.all(Array.from({ length: 6 }, async () => { while (i < jobs.length) await one(jobs[i++]); }));
return { data: out, chunks: jobs.length };
}
|