|
Download docs/CT_VIEW.md from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 8.42 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/docs/CT_VIEW.md
- Command line
-
hf download hf://spaces/SharpMeow/unroll/docs/CT_VIEW.md
-
curl -L -o CT_VIEW.md https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/docs/CT_VIEW.md
8.42 kB
| # CT tools: drag resolution, X-ray window/level, CT volume, labels, OME-Zarr streaming | |
| All code in this document is original to Unroll and licensed Apache 2.0. The feature set was inspired by | |
| [ScrollSlabViewer](https://github.com/Paul-G2/ScrollSlabViewer) by Paul-G2 (MIT); the ideas (raymarched MIP, an | |
| oriented slab clip, a label overlay, a lower resolution while the camera moves, percentile contrast) are common | |
| volume rendering techniques and no code was copied from it or other viewers. | |
| ## 1. Lower resolution while dragging | |
| `lib/dragres.js`. On OrbitControls `start` the renderer and the effect composer drop to a fraction of their | |
| pixel ratio; after `end`, once the camera has stopped changing for 150 ms (damping included), the full | |
| ratio returns. The render targets of the bloom pass shrink with it, which is where most of the saving is. | |
| | Where | What | | |
| | --- | --- | | |
| | Settings | **Lower resolution while dragging**: `off`, `light` (0.75x), `strong` (0.5x). Default `light`, `strong` on touch screens. The pixel ratio never goes below 0.5. | | |
| | URL | `?dragRes=off\|light\|strong` (this visit only, like every setting) | | |
| | API | `unrollView.dragRes.state()`, `unrollView.dragRes.set('strong')`, test hooks `begin()` and `end()`; `unrollView.settings.set('dragRes', 'off')` | | |
| | Event | ui.js dispatches `unroll:settings` on every settings change | | |
| ## 2. X-ray window/level with percentile auto contrast | |
| `lib/ctwl.js`, plus one uniform in `app.js` (`uCTWin`, applied in the surface fragment shader right after the | |
| empty-surface test, so holes stay holes). Values are on the 0..1 scale of the CT atlas: | |
| `low = level - window / 2`, `high = level + window / 2`, and each pixel shows `clamp((ct - low) / (high - low))`. | |
| Auto contrast builds a 256-bin histogram of the CT atlas (drawn once into a 512 px wide canvas, empty pixels | |
| with value 0..3 ignored) and sets low and high to the chosen percentiles (default 1 and 99). | |
| | Where | What | | |
| | --- | --- | | |
| | Panel | **X-ray contrast** group: **Window (contrast range)** and **Level (center brightness)** sliders, **Auto contrast**, **Full range** | | |
| | Settings | **X-ray auto contrast on load**, **Auto contrast low percentile** (0 to 20), **Auto contrast high percentile** (80 to 100) | | |
| | URL | `?ctwin=0.5&ctlevel=0.4`, `?ctauto=1` (Settings percentiles) or `?ctauto=2,98` | | |
| | API | `unrollView.ct.setWindow(window, level)`, `ct.setLowHigh(low, high)`, `ct.auto(pLow, pHigh)`, `ct.reset()`, `ct.state()`, `ct.histogram()`, `ct.percentiles(pLow, pHigh)` | | |
| | Actions | `ct.auto`, `ct.reset` (in `unrollView.actions.list()`) | | |
| ## 3. CT volume: MIP inside an oriented slab | |
| `lib/ctvolume.js` (view, panel, API) and `lib/ctsource.js` (where the data comes from). Two read-only hooks in | |
| `app.js`: `unrollView.scanGeom()` (the int16 mesh grids, which hold scan voxels minus the roll axis center) and | |
| `unrollView.scanPick(x, y)` (winding and UV under a screen point, from the existing float pick). | |
| How a pick becomes a cube: | |
| 1. Winding UV to grid cell through the winding's `colIdx`/`rowIdx`, bilinear between the 4 grid vertices, plus | |
| `axis.centre` gives the scan voxel (x, y, z) at level 0. The surface normal comes from the row and column | |
| tangents at that cell. The scan's zarr axes are (z, y, x), so `[z][y][x]` at level 0 is the picked voxel. | |
| 2. A cube of `size` voxels (64 to 256; 256 on desktops, 128 on phones) at detail level `L` (voxels of | |
| 9.362 x 2^L µm) is centered on it, clamped to the level's shape, read chunk by chunk and uploaded as a | |
| `THREE.Data3DTexture` (R8, linear filtering). 256³ is 16.8 MB on the GPU, 128³ is 2.1 MB. | |
| 3. The view is a unit cube drawn with back faces in its own canvas and WebGL context (so the main scroll | |
| renderer, its passes and its picking are untouched). The fragment shader marches each ray at 1.25 samples | |
| per voxel (up to 1024 steps, jittered against banding), clipped to the box and to the slab | |
| `|dot(p, n) - offset| <= thickness / 2`, and keeps the brightest sample. Window/level is applied to that | |
| maximum. It renders on demand (camera change or a control), not every frame. | |
| 4. The slab normal starts as the surface normal, so the MIP looks across the sheet; **Tilt across** and | |
| **Tilt along** turn it about two perpendicular axes, **Slab offset** moves it, **Face the slab** points the | |
| camera down the normal. | |
| | Where | What | | |
| | --- | --- | | |
| | Panel | **CT volume** group: **Pick a point** (crosshair; click the papyrus; Esc cancels), **Show volume**. The floating **CT VOLUME · MIP** panel has Slab clip, Slab thickness, Slab offset, Tilt across, Tilt along, Window, Level, Cube size, Detail level, Auto contrast, Face the slab, Pick another point | | |
| | Settings | **CT volume cube size (voxels)** (auto, 64, 128, 192, 256), **CT volume detail level** (auto, 0 to 4; auto is 1 on desktops, 2 on phones) | | |
| | URL | `?ctvol=w035,0.5,0.5` (winding, u, v) or `?ctvol=4126,3926,5478` (scan voxels x, y, z), with `&ctlod=1`, `&ctsize=128`, `&ctslab=16` (voxels), `&cttilt=10,0` (degrees) | | |
| | API | `unrollView.ct.volume.open({ winding, u, v } \| { voxel: [x, y, z] } \| { x, y } screen point, level?, size? })`, `close()`, `state()`, `setSlab({ on, thickness, offset, tiltX, tiltY, face })`, `setWindow(w, l)`, `auto()`, `setLevel(l)`, `setSize(n)`, `pick(on)`, `scanPoint(winding, u, v)`, `image()` (PNG data URL), `bench(n)` | | |
| | Actions | `ctvol.pick`, `ctvol.show`, `ctvol.close` | | |
| | Event | `unroll:ctvolume` on window (`open`, `loaded`) | | |
| Only scrolls whose meshes were traced on a known public volume can open it (PHerc0139 today, see | |
| `VOLUMES` in `lib/ctsource.js`); a manifest may name its own with `ctZarr` and `ctVoxelUM`. | |
| ## 4. Labeled segments | |
| `lib/ctlabels.js`. When the cube has loaded, every winding grid (the traced segment meshes) is walked cell by | |
| cell; cells within reach of the cube are split so samples sit under 0.7 voxels apart at the cube's detail | |
| level, and each sample writes the segment's number (1 to 255) into an R8 label volume of the cube's size, with | |
| its 6 neighbors, so a sheet is about 3 voxels thick. The label volume is a second `Data3DTexture` (nearest | |
| filtering) in the same raymarch: the first label met along a ray tints the MIP pixel in the segment's color, | |
| scaled by the MIP brightness; a highlighted segment is drawn stronger and the rest are dimmed. | |
| | Where | What | | |
| | --- | --- | | |
| | Panel | **Labeled segments** checkbox in the CT VOLUME panel, then one chip per segment in the cube (color swatch and winding id); click to highlight, click again to clear | | |
| | URL | `?ctlabels=1` (with `?ctvol=...`) | | |
| | API | `unrollView.ct.volume.labels.show(on)`, `.highlight(id)`, `.state()` (segments, colors, voxel counts, build ms), `.colors()`; `window.unroll.ct.labels(on)`, `ct.highlight(id)` | | |
| ## 5. OME-Zarr streaming from the open data bucket | |
| `lib/ctsource.js` reads the scan straight from `https://vesuvius-challenge-open-data.s3.amazonaws.com` | |
| (the public bucket `s3://vesuvius-challenge-open-data`). Only that bucket is accepted, for the built-in | |
| scrolls, for a manifest's `ctZarr` and for `?ctzarr=`. Level metadata comes from `<zarr>/<level>/.zarray` | |
| (zarr v2, OME-Zarr multiscale levels 0 to 5) and the cube is assembled from the chunks it overlaps, | |
| fetched 6 at a time (the PHerc0139 volume stores raw uint8 chunks, so no decompression is needed; a compressed level is refused with a clear error). | |
| CORS: checked on 2026-10-16, a GET of `.zarray` with an `Origin` header answers `200` with | |
| `Access-Control-Allow-Origin: *` and `Access-Control-Allow-Methods: HEAD, GET`, so no proxy is needed. If the | |
| bucket ever stops sending CORS headers, the fallback is a small read-only proxy (for example a Hugging Face | |
| Space or a Cloudflare Worker) that forwards GETs for that bucket only and adds the header. | |
| | Where | What | | |
| | --- | --- | | |
| | URL | `?ctzarr=https://vesuvius-challenge-open-data.s3.amazonaws.com/<path>.zarr&ctum=9.362` | | |
| | API | `window.unroll.ct.source()` (bucket, zarr URL, voxel size), `unrollView.ct.volume.state().source` | | |
| ## Agents: `window.unroll.ct` | |
| `dragRes(mode)`, `setWindow(w, l)`, `autoContrast(pLow, pHigh)`, `fullRange()`, `contrast()`, `open(where)`, | |
| `close()`, `state()`, `setSlab(o)`, `volumeWindow(w, l)`, `volumeAuto()`, `setDetail(level)`, `setSize(n)`, | |
| `pick(on)`, `image()`, `labels(on)`, `highlight(id)`, `source()`. Specs in `lib/ctapi.js` (`CT_SPECS`), listed in | |
| `api.json` with `extension: true`. | |