Chase Hendrick
Claude Opus 5.5 (1M context)
Unroll 1.7.0: Live sheet, stream any public segment and volume and see the volume on the sheet
7391956 unverified Download lib/livesheet.js from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/livesheet.js
- Command line
-
hf download hf://spaces/SharpMeow/unroll/lib/livesheet.js
-
curl -L -o livesheet.js https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/livesheet.js
16.7 kB
| // SPDX-License-Identifier: Apache-2.0 | |
| // Live sheet: stream a tifxyz segment and a zarr volume from the open data bucket and sample the volume onto the | |
| // sheet in the browser, the same rule as Scrolls' `kit sheetview` (a volume value per grid node: the max or mean | |
| // along the node's normal over plus or minus `depth` level-0 voxels, nearest voxel of one pyramid level). | |
| // Pure JavaScript, no DOM. Only the rows of the tifxyz TIFFs inside the crop are fetched (HTTP Range), and only the | |
| // zarr chunks the samples touch. Raw chunks need nothing else; blosc chunks load a decoder from jsDelivr once. | |
| const BLOSC_URL = 'https://cdn.jsdelivr.net/npm/numcodecs@0.3.2/+esm'; | |
| // ------------------------------------------------------------------ fetching | |
| async function getRange(url, start, end, signal) { // bytes [start, end) | |
| const r = await fetch(url, { headers: { Range: `bytes=${start}-${end - 1}` }, signal }); | |
| if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`); | |
| const buf = new Uint8Array(await r.arrayBuffer()); | |
| if (r.status === 200) return buf.subarray(start, end); // a server that ignores Range sends the whole file | |
| return buf; | |
| } | |
| async function getJSON(url, signal) { | |
| const r = await fetch(url, { signal }); | |
| if (!r.ok) return null; | |
| return r.json(); | |
| } | |
| // ------------------------------------------------------------------ TIFF (uncompressed float32, strips) | |
| const TYPE_SIZE = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8, 16: 8 }; | |
| // The layout of a one-page float32 TIFF: width, height, and the byte offset of every row. | |
| export async function tiffLayout(url, signal) { | |
| let head = await getRange(url, 0, 65536, signal); | |
| const dv = () => new DataView(head.buffer, head.byteOffset, head.byteLength); | |
| const le = head[0] === 0x49; | |
| let d = dv(); | |
| const magic = d.getUint16(2, le), big = magic === 43; | |
| if (magic !== 42 && !big) throw new Error(`${url}: not a TIFF`); | |
| const u64 = (o) => Number(d.getBigUint64(o, le)); | |
| let ifd = big ? u64(8) : d.getUint32(4, le); | |
| if (ifd + 4096 > head.length) { head = await getRange(url, ifd, ifd + 65536, signal); d = dv(); ifd = 0; } | |
| const n = big ? u64(ifd) : d.getUint16(ifd, le); | |
| const tags = {}; | |
| for (let i = 0; i < n; i++) { | |
| const e = ifd + (big ? 8 + 20 * i : 2 + 12 * i); | |
| const tag = d.getUint16(e, le), type = d.getUint16(e + 2, le); | |
| const count = big ? u64(e + 4) : d.getUint32(e + 4, le); | |
| const size = (TYPE_SIZE[type] || 1) * count, inline = size <= (big ? 8 : 4); | |
| const voff = e + (big ? 12 : 8); | |
| tags[tag] = { type, count, inline, at: inline ? voff : (big ? u64(voff) : d.getUint32(voff, le)) }; | |
| } | |
| const read = async (t) => { // the tag's values as numbers | |
| const size = TYPE_SIZE[t.type] || 1; | |
| let view, base; | |
| if (t.inline) { view = d; base = t.at; } else { | |
| const b = await getRange(url, t.at, t.at + size * t.count, signal); | |
| view = new DataView(b.buffer, b.byteOffset, b.byteLength); base = 0; | |
| } | |
| const out = new Array(t.count); | |
| for (let i = 0; i < t.count; i++) { | |
| const o = base + i * size; | |
| out[i] = size === 2 ? view.getUint16(o, le) : size === 4 ? view.getUint32(o, le) : Number(view.getBigUint64(o, le)); | |
| } | |
| return out; | |
| }; | |
| const one = async (tag, dflt) => (tags[tag] ? (await read(tags[tag]))[0] : dflt); | |
| const width = await one(256), height = await one(257); | |
| if ((await one(259, 1)) !== 1) throw new Error(`${url}: compressed TIFF; only uncompressed tifxyz is supported`); | |
| if ((await one(258, 32)) !== 32 || (await one(339, 3)) !== 3) throw new Error(`${url}: not float32`); | |
| if (!tags[273]) throw new Error(`${url}: tiled TIFF; only strips are supported`); | |
| const offsets = await read(tags[273]); | |
| const rps = await one(278, height); | |
| const rowOffset = (r) => offsets[Math.floor(r / rps)] + (r % rps) * width * 4; | |
| return { width, height, le, rowOffset, rps }; | |
| } | |
| // Rows r0:r1 and columns c0:c1 of a float32 TIFF, as a Float32Array (row-major, (r1-r0) x (c1-c0)). | |
| export async function tiffRows(url, layout, r0, r1, c0, c1, signal) { | |
| const { width, le, rowOffset, rps } = layout, W = c1 - c0, out = new Float32Array((r1 - r0) * W); | |
| for (let s = r0; s < r1;) { // one request per strip span | |
| const e = Math.min(r1, (Math.floor(s / rps) + 1) * rps); | |
| const a = rowOffset(s), buf = await getRange(url, a, a + (e - s) * width * 4, signal); | |
| const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); | |
| for (let r = s; r < e; r++) for (let c = c0; c < c1; c++) | |
| out[(r - r0) * W + (c - c0)] = dv.getFloat32(((r - s) * width + c) * 4, le); | |
| s = e; | |
| } | |
| return out; | |
| } | |
| // ------------------------------------------------------------------ segment | |
| // A tifxyz crop: { xyz (Float64Array h*w*3), valid (Uint8Array h*w), h, w, meta, voxelUM }. | |
| export async function readSegment(base, { crop = null, stride = 1, signal } = {}) { | |
| base = base.replace(/\/$/, ''); | |
| const meta = (await getJSON(`${base}/meta.json`, signal)) || {}; | |
| const lay = await tiffLayout(`${base}/x.tif`, signal); | |
| const [r0, r1, c0, c1] = crop || [0, lay.height, 0, lay.width]; | |
| if (!(0 <= r0 && r0 < r1 && r1 <= lay.height && 0 <= c0 && c0 < c1 && c1 <= lay.width)) | |
| throw new Error(`crop ${r0} ${r1} ${c0} ${c1} is outside the grid of ${lay.height} rows and ${lay.width} columns`); | |
| const ch = await Promise.all(['x', 'y', 'z'].map(async (k) => { | |
| const l = k === 'x' ? lay : await tiffLayout(`${base}/${k}.tif`, signal); | |
| return tiffRows(`${base}/${k}.tif`, l, r0, r1, c0, c1, signal); | |
| })); | |
| const H0 = r1 - r0, W0 = c1 - c0, h = Math.ceil(H0 / stride), w = Math.ceil(W0 / stride); | |
| const xyz = new Float64Array(h * w * 3), valid = new Uint8Array(h * w); | |
| for (let i = 0; i < h; i++) for (let j = 0; j < w; j++) { | |
| const s = i * stride * W0 + j * stride, k = i * w + j; | |
| const x = ch[0][s], y = ch[1][s], z = ch[2][s]; | |
| xyz[3 * k] = x; xyz[3 * k + 1] = y; xyz[3 * k + 2] = z; | |
| valid[k] = Number.isFinite(x + y + z) && x > -0.5 && y > -0.5 && z > -0.5 && (Math.abs(x) + Math.abs(y) + Math.abs(z)) > 0 ? 1 : 0; | |
| } | |
| const m = /-(\d+(?:\.\d+)?)um\.tifxyz$/.exec(base); | |
| return { xyz, valid, h, w, meta, voxelUM: m ? +m[1] : null, grid: [lay.height, lay.width], crop: [r0, r1, c0, c1] }; | |
| } | |
| // Bilinear refinement by an integer factor; a new node is valid when its cell's 4 corners are (kit sheetview's rule). | |
| export function upsample(seg, f) { | |
| if (f <= 1) return seg; | |
| const { xyz, valid, h, w } = seg, H = (h - 1) * f + 1, W = (w - 1) * f + 1; | |
| const out = new Float64Array(H * W * 3), ok = new Uint8Array(H * W); | |
| for (let i = 0; i < H; i++) { | |
| const r = i / f, i0 = Math.min(Math.floor(r), h - 2), fr = r - i0; | |
| for (let j = 0; j < W; j++) { | |
| const c = j / f, j0 = Math.min(Math.floor(c), w - 2), fc = c - j0; | |
| const a = i0 * w + j0, b = a + 1, cc = a + w, d = cc + 1, k = i * W + j; | |
| ok[k] = valid[a] & valid[b] & valid[cc] & valid[d]; | |
| for (let t = 0; t < 3; t++) { | |
| const q = (n) => (valid[n] ? xyz[3 * n + t] : 0); | |
| out[3 * k + t] = (1 - fr) * (1 - fc) * q(a) + (1 - fr) * fc * q(b) + fr * (1 - fc) * q(cc) + fr * fc * q(d); | |
| } | |
| } | |
| } | |
| return { ...seg, xyz: out, valid: ok, h: H, w: W }; | |
| } | |
| // Unit normals, column step cross row step (numpy.gradient: central inside, one-sided at the edges); NaN at holes. | |
| export function normals(seg) { | |
| const { xyz, valid, h, w } = seg, n = new Float64Array(h * w * 3).fill(NaN); | |
| const P = (i, j, t) => (valid[i * w + j] ? xyz[3 * (i * w + j) + t] : NaN); | |
| const grad = (i, j, t, alongCols) => { | |
| const L = alongCols ? w : h, p = alongCols ? j : i; | |
| const at = (q) => (alongCols ? P(i, q, t) : P(q, j, t)); | |
| if (L < 2) return NaN; | |
| if (p === 0) return at(1) - at(0); | |
| if (p === L - 1) return at(L - 1) - at(L - 2); | |
| return (at(p + 1) - at(p - 1)) / 2; | |
| }; | |
| for (let i = 0; i < h; i++) for (let j = 0; j < w; j++) { | |
| const u = [0, 1, 2].map((t) => grad(i, j, t, true)), v = [0, 1, 2].map((t) => grad(i, j, t, false)); | |
| const c = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]]; | |
| const L = Math.hypot(c[0], c[1], c[2]), k = 3 * (i * w + j); | |
| for (let t = 0; t < 3; t++) n[k + t] = c[t] / L; | |
| } | |
| return n; | |
| } | |
| // ------------------------------------------------------------------ zarr v2 | |
| let bloscCodec = null; | |
| async function blosc() { | |
| if (!bloscCodec) bloscCodec = import(BLOSC_URL).then((m) => new m.Blosc()); | |
| return bloscCodec; | |
| } | |
| const DTYPES = { '|u1': Uint8Array, '<u1': Uint8Array, '|i1': Int8Array, '<u2': Uint16Array, '<i2': Int16Array, | |
| '<u4': Uint32Array, '<i4': Int32Array, '<f4': Float32Array, '<f8': Float64Array }; | |
| // One level of an OME-Zarr (v2) volume: { shape, chunks, factor [z, y, x], chunk(cz, cy, cx) -> typed array or null }. | |
| export async function openVolume(root, level = 0, { signal } = {}) { | |
| root = root.replace(/\/$/, ''); | |
| const attrs = await getJSON(`${root}/.zattrs`, signal); | |
| const ds = attrs?.multiscales?.[0]?.datasets; | |
| let path = '', factor = [1, 1, 1]; | |
| if (ds) { | |
| if (!(level >= 0 && level < ds.length)) throw new Error(`${root} has levels 0 to ${ds.length - 1}`); | |
| const sc = (d) => (d.coordinateTransformations || []).find((t) => t.type === 'scale')?.scale; | |
| const s0 = sc(ds[0]), sl = sc(ds[level]); | |
| factor = s0 && sl ? sl.slice(-3).map((v, i) => v / s0.slice(-3)[i]) : [2 ** level, 2 ** level, 2 ** level]; | |
| path = ds[level].path; | |
| } else if (level) throw new Error(`${root} is one array, not a pyramid`); | |
| const meta = await getJSON(`${root}/${path ? path + '/' : ''}.zarray`, signal); | |
| if (!meta) throw new Error(`${root}: no zarr v2 array at level ${level} (zarr v3 volumes are not supported here yet)`); | |
| if (meta.shape.length !== 3) throw new Error(`${root}: level ${level} has ${meta.shape.length} axes; a scan volume has 3`); | |
| const T = DTYPES[meta.dtype]; | |
| if (!T) throw new Error(`${root}: dtype ${meta.dtype} is not supported`); | |
| if (meta.filters?.length) throw new Error(`${root}: zarr filters are not supported`); | |
| const cid = meta.compressor?.id || null; | |
| if (cid && cid !== 'blosc') throw new Error(`${root}: compressor ${cid} is not supported (raw and blosc are)`); | |
| const sep = meta.dimension_separator || '.', pre = `${root}/${path ? path + '/' : ''}`; | |
| const n = meta.chunks[0] * meta.chunks[1] * meta.chunks[2]; | |
| const stats = { chunks: 0, bytes: 0, absent: 0 }; | |
| const chunk = async (cz, cy, cx) => { | |
| const r = await fetch(pre + [cz, cy, cx].join(sep), { signal }); | |
| if (!r.ok) { stats.absent++; return null; } // a missing chunk is the fill value | |
| let buf = new Uint8Array(await r.arrayBuffer()); | |
| stats.chunks++; stats.bytes += buf.length; | |
| if (cid) buf = await (await blosc()).decode(buf); | |
| const a = new T(buf.buffer, buf.byteOffset, buf.byteLength / T.BYTES_PER_ELEMENT); | |
| if (a.length !== n) throw new Error(`chunk ${cz} ${cy} ${cx}: ${a.length} values, expected ${n}`); | |
| return a; | |
| }; | |
| return { root, level, shape: meta.shape, chunks: meta.chunks, factor, dtype: meta.dtype, fill: meta.fill_value || 0, | |
| chunk, stats }; | |
| } | |
| // ------------------------------------------------------------------ sampling | |
| const rint = (x) => { // numpy.rint: halves go to the even neighbor | |
| const f = Math.floor(x), d = x - f; | |
| return d > 0.5 ? f + 1 : d < 0.5 ? f : (f % 2 === 0 ? f : f + 1); | |
| }; | |
| // Values on the sheet: Float32Array h*w, NaN at holes and where every sample is outside the volume. | |
| export async function sampleSheet(seg, vol, { depth = 0, reduce = 'max', onProgress, concurrency = 12 } = {}) { | |
| if (!(depth >= 0)) throw new Error(`depth must be 0 or more voxels (got ${depth})`); | |
| if (reduce !== 'max' && reduce !== 'mean') throw new Error(`reduce must be max or mean (got ${reduce})`); | |
| const { xyz, valid, h, w } = seg, nrm = normals(seg), N = h * w; | |
| const step = Math.max(Math.max(...vol.factor), 1), K = Math.floor(depth / step + 1e-9); | |
| const offs = []; for (let k = -K; k <= K; k++) offs.push(k * step); | |
| const S = offs.length, [CZ, CY, CX] = vol.chunks, [SZ, SY, SX] = vol.shape, [fz, fy, fx] = vol.factor; | |
| const idx = new Int32Array(N * S * 3).fill(-1), groups = new Map(); | |
| for (let p = 0; p < N; p++) { | |
| if (!valid[p]) continue; | |
| const nx = Number.isFinite(nrm[3 * p]) ? nrm[3 * p] : 0, ny = Number.isFinite(nrm[3 * p + 1]) ? nrm[3 * p + 1] : 0; | |
| const nz = Number.isFinite(nrm[3 * p + 2]) ? nrm[3 * p + 2] : 0; | |
| for (let s = 0; s < S; s++) { | |
| const z = rint((xyz[3 * p + 2] + offs[s] * nz) / fz), y = rint((xyz[3 * p + 1] + offs[s] * ny) / fy); | |
| const x = rint((xyz[3 * p] + offs[s] * nx) / fx); | |
| if (z < 0 || y < 0 || x < 0 || z >= SZ || y >= SY || x >= SX) continue; | |
| const q = 3 * (p * S + s); idx[q] = z; idx[q + 1] = y; idx[q + 2] = x; | |
| const key = `${Math.floor(z / CZ)},${Math.floor(y / CY)},${Math.floor(x / CX)}`; | |
| let g = groups.get(key); if (!g) groups.set(key, (g = [])); | |
| g.push(p * S + s); | |
| } | |
| } | |
| const vals = new Float32Array(N * S).fill(NaN), keys = [...groups.keys()]; | |
| let done = 0, next = 0; | |
| const worker = async () => { | |
| while (next < keys.length) { | |
| const key = keys[next++], [cz, cy, cx] = key.split(',').map(Number); | |
| const a = await vol.chunk(cz, cy, cx); | |
| for (const t of groups.get(key)) { | |
| const lz = idx[3 * t] - cz * CZ, ly = idx[3 * t + 1] - cy * CY, lx = idx[3 * t + 2] - cx * CX; | |
| vals[t] = a ? a[(lz * CY + ly) * CX + lx] : vol.fill; | |
| } | |
| groups.delete(key); // the chunk is not needed again | |
| onProgress?.(++done, keys.length); | |
| } | |
| }; | |
| await Promise.all(Array.from({ length: Math.min(concurrency, keys.length) }, worker)); | |
| const out = new Float32Array(N).fill(NaN); | |
| for (let p = 0; p < N; p++) { | |
| if (!valid[p]) continue; | |
| let m = reduce === 'max' ? -Infinity : 0, c = 0; | |
| for (let s = 0; s < S; s++) { | |
| const v = vals[p * S + s]; if (Number.isNaN(v)) continue; | |
| if (reduce === 'max') { if (v > m) m = v; } else m += v; | |
| c++; | |
| } | |
| if (c) out[p] = reduce === 'max' ? m : m / c; | |
| } | |
| return { values: out, chunks: keys.length, samplesPerVertex: S }; | |
| } | |
| // ------------------------------------------------------------------ mesh | |
| // Triangles of the valid grid (two per cell whose 4 corners are valid) and compact positions and values. | |
| export function sheetMesh(seg, values) { | |
| const { xyz, valid, h, w } = seg, index = new Int32Array(h * w).fill(-1); | |
| let nv = 0; | |
| for (let p = 0; p < h * w; p++) if (valid[p]) index[p] = nv++; | |
| const positions = new Float32Array(nv * 3), vals = new Float32Array(nv), uv = new Float32Array(nv * 2); | |
| for (let p = 0; p < h * w; p++) { | |
| const k = index[p]; if (k < 0) continue; | |
| positions.set(xyz.subarray(3 * p, 3 * p + 3), 3 * k); | |
| vals[k] = values[p]; uv[2 * k] = (p % w) / Math.max(w - 1, 1); uv[2 * k + 1] = Math.floor(p / w) / Math.max(h - 1, 1); | |
| } | |
| const tri = []; | |
| for (let i = 0; i < h - 1; i++) for (let j = 0; j < w - 1; j++) { | |
| const a = index[i * w + j], b = index[i * w + j + 1], c = index[(i + 1) * w + j], d = index[(i + 1) * w + j + 1]; | |
| if (a >= 0 && b >= 0 && c >= 0 && d >= 0) tri.push(a, c, b, b, c, d); | |
| } | |
| return { positions, values: vals, uv, indices: Uint32Array.from(tri), vertices: nv, triangles: tri.length / 3 }; | |
| } | |
| // ASCII VTK polydata with one point attribute: what Neuroglancer's vtk:// source reads (as kit sheetview writes). | |
| export function toVTK(mesh, name = 'value') { | |
| const L = ['# vtk DataFile Version 3.0', 'unroll live sheet', 'ASCII', 'DATASET POLYDATA', `POINTS ${mesh.vertices} float`]; | |
| for (let k = 0; k < mesh.vertices; k++) | |
| L.push(`${mesh.positions[3 * k].toFixed(2)} ${mesh.positions[3 * k + 1].toFixed(2)} ${mesh.positions[3 * k + 2].toFixed(2)}`); | |
| L.push(`POLYGONS ${mesh.triangles} ${4 * mesh.triangles}`); | |
| for (let t = 0; t < mesh.triangles; t++) L.push(`3 ${mesh.indices[3 * t]} ${mesh.indices[3 * t + 1]} ${mesh.indices[3 * t + 2]}`); | |
| L.push(`POINT_DATA ${mesh.vertices}`, `SCALARS ${name} float 1`, 'LOOKUP_TABLE default'); | |
| for (let k = 0; k < mesh.vertices; k++) L.push(String(Number.isFinite(mesh.values[k]) ? +mesh.values[k].toPrecision(6) : 0)); | |
| return L.join('\n') + '\n'; | |
| } | |
| // Percentiles of the finite values (for a default contrast). | |
| export function percentiles(values, ps) { | |
| const a = Float32Array.from(values.filter(Number.isFinite)).sort(); | |
| if (!a.length) return ps.map(() => 0); | |
| return ps.map((p) => { | |
| const x = (p / 100) * (a.length - 1), i = Math.floor(x), f = x - i; | |
| return i + 1 < a.length ? a[i] * (1 - f) + a[i + 1] * f : a[i]; | |
| }); | |
| } | |