Repository navigation
Add zarr codec support for JP2K/HTJ2K with Python writer implementation #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
3344607
Add codec fixture demo and store-backed OME-Zarr loading
xinaesthete ac32070
spatialdata recompress util first pass
xinaesthete 76007f6
add fizarrita and worker-pool dependencies to zarrextra
xinaesthete 1420f36
implement worker based chunk decoding, change registration mechanism …
xinaesthete f6c22a3
spatialdata writer can add encoded images as siblings rather than rep…
xinaesthete 662b474
python type tweaks
xinaesthete bdbd56d
add openjph dependency
xinaesthete 3d41c8f
htj2k encode in python script, needs conda env
xinaesthete 4b19606
remove python artefacts from git
xinaesthete 9df3f27
register HTJ2K codec in frontend
xinaesthete 1018bcd
HTJ2K encoding with wasm, used by python recompress script and fixtures
xinaesthete ac4a981
fix quality param in HTJK encoding, don't support imagecodecs version…
xinaesthete 25b4976
tweak demos, add synthetic images stub test-fixture
xinaesthete afcb098
quality callibration and cli to more easily control it
xinaesthete 897e253
rearrange codec-writer scripts to be more suitable for publish and mo…
xinaesthete a361de8
recompress all images if --image-key is omitted
xinaesthete 71b66da
add version to spatialdata_attrs so schema doesn't fail
xinaesthete 58673a8
change zarrextra build target to es2022 for top-level await in module
xinaesthete c0f0a45
Implement abort handling for promises in chunk decoding and improve e…
xinaesthete e9ff737
Update codec fixtures and improve error handling in spatialdata codec…
xinaesthete 9110960
fix synthetic_images import
xinaesthete 38ee67e
add basedpyright config for scripts so imports aren't flagged in edit…
xinaesthete 4839a47
better axis dimension understanding in omeZarr
xinaesthete d38e12b
add mandelbulb 4d volume generation to synthetic_images script
xinaesthete 7020e63
add mandelbulb to test fixtures and demo gui
xinaesthete 58425bd
synthetic data cli
xinaesthete 0fc32f0
synthetic image cli raises error if invalid pattern is passed
xinaesthete e82f023
add changeset
xinaesthete File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| --- | ||
| sidebar_position: 6 | ||
| --- | ||
|
|
||
| # Codec fixture guide | ||
|
|
||
| SpatialData codec fixtures live under `test-fixtures/codecs/` and are generated | ||
| separately from the versioned `blobs()` fixtures. The reference fixtures are | ||
| small OME-Zarr/SpatialData images using: | ||
|
|
||
| - `imagecodecs_jpeg2k` (registered JP2K codec id) | ||
| - `experimental.openjph_htj2k` (OpenJPH WASM HTJ2K codec id) | ||
|
|
||
| Older fixtures may use `experimental.imagecodecs_htj2k`; the frontend decodes both. | ||
|
|
||
| ## Try it in the browser | ||
|
|
||
| From the repo root: | ||
|
|
||
| ```bash | ||
| pnpm test:fixtures:generate:codecs | ||
| pnpm --filter @spatialdata/vis dev | ||
| ``` | ||
|
|
||
| Open [http://127.0.0.1:5173/codec](http://127.0.0.1:5173/codec). | ||
|
|
||
| The route calls `enableWorkerChunkDecode()` from `zarrextra/workers`, which uses | ||
| [`@fideus-labs/fizarrita`](https://www.npmjs.com/package/@fideus-labs/fizarrita) | ||
| with a custom codec worker that registers JP2K and experimental HTJ2K support | ||
| inside the worker before decode. Use the codec selector on the page to switch | ||
| between `/test-fixtures/codecs/jpeg2k.zarr` and `/test-fixtures/codecs/htj2k-demo.zarr`. | ||
| When HTJ2K is selected, the demo loads a single `htj2k-demo.zarr` store containing | ||
| `mandelbrot_lossless`, `mandelbrot_balanced`, and `mandelbrot_small` image layers | ||
| (512×512 Mandelbrot, two pyramid levels). Use the `SpatialCanvas` layer controls to | ||
| toggle between presets. Manifests are at | ||
| `/test-fixtures/codecs/jpeg2k.manifest.json` and | ||
| `/test-fixtures/codecs/htj2k-encode-demo.manifest.json`. | ||
|
|
||
| The vis dev script starts the fixture server on port `38473` and proxies | ||
| `/test-fixtures` through Vite on `5173`. Override the fixture server port with | ||
| `SPATIALDATA_FIXTURE_PORT` if needed. | ||
|
|
||
| ## Python reference writer | ||
|
|
||
| The **publishable** package (`spatialdata-codec-writer`) exposes recompression | ||
| only. Codec test fixtures are generated by repo-local scripts under | ||
| `python/spatialdata-codec-writer/scripts/`. | ||
|
|
||
| Useful commands: | ||
|
|
||
| ```bash | ||
| pnpm test:fixtures:generate:codecs | ||
| uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer inspect ../../test-fixtures/codecs/jpeg2k.manifest.json | ||
| ``` | ||
|
|
||
| ### Fixture provenance | ||
|
|
||
| Dev fixtures stamp root metadata with an experimental marker rather than a fake | ||
| `spatialdata` library version: | ||
|
|
||
| ```json | ||
| "spatialdata_attrs": { | ||
| "experimental_codec_writer": "0.1.0", | ||
| "note": "codec test fixture; not a spatialdata.write() artifact" | ||
| } | ||
| ``` | ||
|
|
||
| The frontend image schema treats `spatialdata_attrs` as optional on rasters and | ||
| does not use `version` for OME format detection. We intend to propose a | ||
| standardized provenance field to the spatialdata community; feedback welcome. | ||
|
|
||
| The sidecar manifest records shape, dtype, chunk shape, codec id, package | ||
| versions, and representative decoded/encoded checksums. The fixture scripts | ||
| validate representative chunks after writing so JS tests can compare decoded | ||
| samples against Python. | ||
|
|
||
| ### Whole-object recompression | ||
|
|
||
| For larger or real examples, use the `recompress` command. It copies the source | ||
| SpatialData store, rewrites configured image rasters with JP2K or experimental | ||
| HTJ2K, and writes labels with Blosc/zstd by default. | ||
|
|
||
| ```bash | ||
| uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress input.sdata.zarr output-jp2k.zarr --image-key morphology_focus --preset balanced --chunks auto --overwrite | ||
| ``` | ||
|
|
||
| Custom HTJ2K quality (instead of a preset name): | ||
|
|
||
| ```bash | ||
| uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress \ | ||
| input.sdata.zarr output-htj2k.zarr \ | ||
| --image-key morphology_focus \ | ||
| --codec experimental.openjph_htj2k \ | ||
| --quality 0.001 \ | ||
| --chunks auto \ | ||
| --sibling \ | ||
| --overwrite | ||
| ``` | ||
|
|
||
| `--quality` requires `--image-key` and `--codec experimental.openjph_htj2k`. | ||
| Lower values preserve more detail. With `--sibling`, the output image is named | ||
| like `morphology_focus:htj2k_q0.001`. | ||
|
|
||
| For repeatable per-raster settings, use JSON: | ||
|
|
||
| ```json | ||
| { | ||
| "default_image": { "codec": "imagecodecs_jpeg2k", "preset": "lossless", "chunks": "auto" }, | ||
| "images": { | ||
| "morphology_focus": { "preset": "balanced" }, | ||
| "morphology_focus_htj2k_tuned": { | ||
| "codec": "experimental.openjph_htj2k", | ||
| "quality": 0.001, | ||
| "chunks": "auto" | ||
| }, | ||
| "he_image": { "preset": "small" }, | ||
| "fast_preview": { | ||
| "codec": "experimental.openjph_htj2k", | ||
| "preset": "lossless", | ||
| "chunks": "auto" | ||
| } | ||
| }, | ||
| "default_labels": { "codec": "blosc", "clevel": 5 } | ||
| } | ||
| ``` | ||
|
|
||
| JP2K and HTJ2K share preset names (`lossless`, `balanced`, `small`). HTJ2K encode | ||
| uses OpenJPH WASM (`experimental.openjph_htj2k`) via | ||
| `HTJ2KEncoder.setQuality(reversible, quality)`. The quality argument is a float | ||
| quantization factor (lower = higher fidelity, larger output) — not JP2K-style | ||
| 0–100. Presets map to `balanced: 0.0002`, `small: 0.001` (calibrated roughly on | ||
| Xenium morphology; see `python/spatialdata-codec-writer/docs/htj2k-wasm-encode-design.md`). | ||
| Override with CLI `--quality 0.001` or per-image JSON `"quality": 0.001` (implies | ||
| lossy unless `"reversible": true`). A future codec-demo UI may add interactive `q` | ||
| exploration on sample regions. | ||
|
|
||
| `generate_codec_fixtures.py --experimental-htj2k` also writes | ||
| `htj2k-quality-sweep.manifest.json`, encoding the same Mandelbrot plane at several | ||
| qualities so you can confirm compression responds on detail-rich imagery, and | ||
| `htj2k-encode-demo.manifest.json` with three browser-viewable multiscale image | ||
| layers in one store (`htj2k-demo.zarr`: `mandelbrot_lossless`, `mandelbrot_balanced`, | ||
| `mandelbrot_small`) at 512×512 with 64×64 chunks. The main `htj2k.zarr` fixture | ||
| remains a small 64×64 Mandelbrot raster for fast CI smoke tests. | ||
|
|
||
| Browser-targeted image codec output is limited to `uint8`, `int8`, `uint16`, and | ||
| `int16`. Labels are not image-codec-compressed in v1; they stay lossless through | ||
| Blosc/zstd because integer IDs and wider dtypes such as `uint32` are not | ||
| considered supported by the current JavaScript decoder paths. | ||
|
|
||
| Useful Xenium morphology experiment: | ||
|
|
||
| ```bash | ||
| uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress /Users/ptodd/data/spatialdata/sdata_inputs/xenium_rep1_io_spatialdata_0.7.1.zarr /private/tmp/xenium-rep1-morphology-focus-jp2k.zarr --image-key morphology_focus --preset balanced --chunks auto --overwrite | ||
| ``` | ||
|
|
||
| When serving recompressed stores for browser experiments, disable HTTP caching | ||
| or use a fresh output path after each rewrite. `http-server` defaults to | ||
| cacheable responses, so overwriting a Zarr store in place can leave the browser | ||
| with stale array metadata that points at the old codec while the chunk bytes have | ||
| changed. | ||
|
|
||
| ```bash | ||
| bunx http-server --cors -c-1 /private/tmp | ||
| ``` | ||
|
|
||
| Repo scripts may set `UV_CACHE_DIR=.tmp/uv-cache` for sandbox or CI cache | ||
| isolation, but normal user-facing `uv run` commands do not need it. | ||
|
|
||
| ## JS codec flow | ||
|
|
||
| `zarrextra` exposes: | ||
|
|
||
| - `registerJpeg2kCodec()` for the registered `imagecodecs_jpeg2k` id (Node/CI). | ||
| - `enableWorkerChunkDecode()` from `zarrextra/workers` for browser apps (fizarrita | ||
| worker pool + custom codec worker with JP2K and experimental HTJ2K registration). | ||
| - `registerExperimentalHtj2kCodec()` for OpenJPH HTJ2K decode (`experimental.openjph_htj2k` | ||
| and legacy `experimental.imagecodecs_htj2k`) in Node/CI smoke tests. | ||
| - `loadOmeZarrMultiscalesFromStore()` for loading multiscales from a | ||
| `zarr.Readable` store without going back through Viv's URL loader. | ||
|
|
||
| `@spatialdata/vis` now prefers raster element stores (`images/<key>` and | ||
| `labels/<key>`) and keeps Viv URL loading as a compatibility fallback. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,28 @@ | ||
| import { loadOmeZarr } from '@hms-dbmi/viv'; | ||
| import { loadOmeZarrMultiscalesFromStore } from 'zarrextra'; | ||
|
|
||
| export type OmeZarrMultiscalesSource = | ||
| | string | ||
| | { | ||
| url?: string; | ||
| store?: unknown; | ||
| }; | ||
|
|
||
| /** OME-Zarr multiscales pixel sources (no caching). */ | ||
| export async function loadOmeZarrMultiscalesData( | ||
| source: OmeZarrMultiscalesSource | ||
| ): Promise<unknown> { | ||
| if (typeof source !== 'string' && source.store) { | ||
| return await loadOmeZarrMultiscalesFromStore( | ||
| source.store as Parameters<typeof loadOmeZarrMultiscalesFromStore>[0] | ||
| ); | ||
| } | ||
|
|
||
| const url = typeof source === 'string' ? source : source.url; | ||
| if (!url) { | ||
| throw new Error('OME-Zarr loading requires either a Zarrita store or a URL.'); | ||
| } | ||
|
|
||
| /** OME-Zarr multiscales pixel sources for a URL (no caching). */ | ||
| export async function loadOmeZarrMultiscalesData(url: string): Promise<unknown> { | ||
| const loader = await loadOmeZarr(url, { type: 'multiscales' }); | ||
| return loader.data; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.