diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 99dd227a..54a832fd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,12 @@ # # Scope: Runs on push to main (deploys dev alias) and version tags (deploys stable). # Uses the pixi docs environment for reproducible builds. -# Jobs: deploy (mike + mkdocs) +# Jobs: deploy (mike + zensical) +# +# Note: The docs are built by Zensical, and versioned with the Zensical maintainers' +# fork of mike (installed from git via the pixi docs feature, since it is not +# published to PyPI). The mike CLI and the gh-pages layout are unchanged, so +# already-published versions stay browsable. name: Docs on: @@ -37,6 +42,12 @@ jobs: with: environments: docs + # `mike deploy` builds the site itself, but its internal build is not strict: + # `zensical build` exits 0 even when a markdown-exec code block raises, which + # would publish tracebacks. Build once up front with --strict to fail fast. + - name: 🔍 Verify docs build + run: pixi run -e docs build_docs + - name: Deploy docs env: GH_TOKEN: ${{ github.token }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f9cdd83..1a3cce82 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,10 @@ ci: autofix_commit_msg: "style(pre-commit.ci): auto fixes [...]" autoupdate_commit_msg: "ci(pre-commit.ci): autoupdate" +# Vendored third-party files, kept byte-identical to what upstream ships. The SIL OFL +# text in particular should not be rewritten by the whitespace hooks. +exclude: ^brand/fonts/ + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -14,6 +18,10 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + # mkdocs.yml uses `!!python/name:` tags for the pymdownx.emoji generator/index, + # which the safe YAML loader cannot construct. Excluded here rather than passing + # --unsafe, so every other YAML file (CI workflows included) stays fully checked. + exclude: ^mkdocs\.yml$ - id: check-toml - id: check-merge-conflict - id: debug-statements diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f2f2ecf..9c5e7f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,11 @@ - Apply the `io_retry` policy to the IO paths that bypass the zarr store: the pyarrow table backend's dataset load/write, the AnnData backend's direct local/fsspec writes, and the `fractal_fsspec_store` metadata probe (where 401 auth failures are translated to `NgioValueError` inside the retried call, so they are never retried — even in blanket mode). ### Documentation +- Rebuild the docs on [Zensical](https://zensical.org) instead of MkDocs + Material, and move every executed code block out of the Markdown into standalone scripts under `docs/snippets/`, included via `pymdownx.snippets` and run at build time. The five tutorial notebooks become Markdown pages, and CI builds with `--strict`. +- Apply a new design system to the docs and the README. The full theme in `docs/stylesheets/ngio.css`. +- Rewrite the copy to the design system's content conventions across every page, the nav and `llms.txt`, and add a landing page, a glossary, and API cross-references. +- Correct what the docs claim against what ngio actually does, most substantially on the table specification pages. +- Add `CODE_OF_CONDUCT.md` and `CITATION.cff`, and move `CONTRIBUTING.md` to the repository root, single-sourced into the docs so GitHub's community widgets pick them up. - Add a "Configuration" getting-started page documenting the config file location (`~/.ngio/ngio_config.json` / `NGIO_CONFIG_PATH`), the `io_retry` policy (fields, backoff strategies, marker matching, snapshot-at-open vs read-at-call semantics), and its relationship to the lower-level `s3fs.custom_retry_markers` mechanism. `NgioConfig`, `RetryConfig`, and `get_config` are now listed in the top-level API reference. ### Fix diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000..1be36615 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,27 @@ +cff-version: 1.2.0 +title: "ngio: a Python library for OME-Zarr bioimage analysis" +message: "If you use this software, please cite it using these metadata." +type: software +authors: + - given-names: Lorenzo + family-names: Cerrone + affiliation: "BioVisionCenter, University of Zurich" + alias: lorenzocerrone + - given-names: Joel + family-names: Luethi + affiliation: "BioVisionCenter, University of Zurich" + alias: jluethi +repository-code: "https://github.com/BioVisionCenter/ngio" +url: "https://biovisioncenter.github.io/ngio/" +abstract: >- + ngio is a Python library that simplifies bioimage analysis workflows on OME-Zarr data. + It provides an object-based API for images, labels, tables, regions of interest and + high-content screening plates. +keywords: + - OME-Zarr + - OME-NGFF + - bioimage analysis + - microscopy + - Python + - zarr +license: BSD-3-Clause diff --git a/CLAUDE.md b/CLAUDE.md index f74d1e60..8af24aaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,9 +18,16 @@ test13 pytest # 3.13 dev lint # lint/format (pre-commit hooks) dev ty check # type check (Ruff ty) docs serve_docs # docs preview -test_nb # run notebooks +docs build_docs # build the site +docs test_snippets # run every docs snippet script standalone +docs clean_docs_data # drop generated ./data/*.zarr stores ``` +## Docs +Snippet mechanics and build traps: see `docs/CLAUDE.md`. Snippet scripts repeat their +imports so each rendered block stands alone (hence the `docs/snippets/**` ruff +per-file-ignores). + ## Config - Python: 3.11–3.14 - Versioning: VCS via `hatch-vcs` (git tags, no hardcoded versions) @@ -34,7 +41,6 @@ test_nb # run notebooks - Don't restate types in prose — they live in the signature (`channel: The channel to load.`, not `channel (int): ...`) - Sections: `Args`, `Returns`, `Raises`, `Example`, `Note` - One-line summary, blank line, then body - - Code examples in fenced ` ```python ` blocks, not `>>>` doctests - Terse: behavior and edge cases only, don't restate the signature - Type checking via `ty` - Internal modules prefixed with `_` @@ -43,12 +49,5 @@ test_nb # run notebooks ## Changelog -- Follow the format in `CHANGELOG.md` -- **Always** update `CHANGELOG.md` when making code changes — add entries under the current `## [vX.Y.Z]` section (or create one if missing). -- Use these subsections (omit empty ones): - - `### Features` — new user-visible behaviour - - `### Fix` — bug fixes - - `### API Breaking Changes` — anything that breaks existing call sites (include before/after example) - - `### Chores` — internal refactors, dependency bumps, CI changes - - `### Documentation` — doc-only changes -- One bullet per logical change; use backticks for identifiers. +- Follow the format in `CHANGELOG.md`. +- **Always** update `CHANGELOG.md` when making code changes — add entries under the current `## [Unreleased]` section (or create one if missing). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..566c5fde --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,59 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in the ngio +community a harassment-free experience for everyone, regardless of age, body size, +visible or invisible disability, ethnicity, sex characteristics, gender identity and +expression, level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, +inclusive, and healthy community. + +## Our Standards + +Examples of behaviour that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility, apologising to those affected by our mistakes, and learning + from the experience +- Focusing on what is best for the overall community + +Examples of unacceptable behaviour: + +- The use of sexualised language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without + their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional + setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing these standards, and will +take appropriate and fair corrective action in response to any behaviour that they deem +inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all community spaces — the issue tracker, pull +requests, discussions, and any other project channel — and also applies when an +individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported to +the maintainers at the [BioVisionCenter](https://www.biovisioncenter.uzh.ch/en.html), +University of Zurich, or by opening a confidential report with a maintainer directly. All +complaints will be reviewed and investigated promptly and fairly. Community leaders are +obligated to respect the privacy and security of the reporter of any incident. + +## Attribution + +This Code of Conduct is adapted from the [Contributor +Covenant](https://www.contributor-covenant.org/), version 2.1, available at +. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..aa3f2a92 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# Contributing + +Contributions are welcome! Please open an issue to discuss significant changes before opening a PR. + +## Prerequisites + +- [Pixi](https://pixi.sh) — manages all environments and dependencies +- Git + +Install Pixi, then clone and set up: + +```bash +git clone https://github.com/BioVisionCenter/ngio +cd ngio +pixi install +``` + +## Development + +Work in the `dev` environment, which includes linters, type checker, and test dependencies: + +```bash +pixi shell -e dev # activate shell +# or prefix individual commands: +pixi run -e dev +``` + +## Running tests + +```bash +pixi run -e test pytest # single run (Python 3.11) +pixi run -e test13 pytest # a specific Python version: test11, test12, test13, test14 +``` + +Coverage is reported automatically. Pull requests run a reduced matrix — Linux on Python 3.11 and 3.13 — while `main` and tags run the full one: `test11`–`test14` across Linux, macOS, and Windows. + +## Linting and formatting + +```bash +pixi run -e dev lint # run all hooks on all files +``` + +This runs Ruff (lint + format), `typos` (spell check), YAML/TOML validation, and notebook output stripping. Hooks also run automatically on `git commit`. + +## Commit conventions + +Please follow [Conventional Commits](https://www.conventionalcommits.org/) — this is not enforced by a hook (yet), but helps maintain a clean history and enables automated changelog generation. + +Examples: + +``` +feat: add support for multiscale labels +fix: correct axis order in NgffImage +docs: update contributing guide +``` + +## Opening a pull request + +1. Fork the repo and create a branch from `main`. +2. Make your changes with tests where relevant. +3. Run `pixi run -e dev lint` and ensure all checks pass. +4. Open a PR against `main` with a clear description of what and why. + +CI runs the linters and the test matrix automatically. + +## Releasing *(maintainers only)* + +Versions are derived from git tags via `hatch-vcs`. Use the Pixi bump tasks in the `dev` environment: + +```bash +pixi run -e dev bump-patch # 0.5.7 → 0.5.8 +pixi run -e dev bump-minor # 0.5.7 → 0.6.0 +pixi run -e dev bump-major # 0.5.7 → 1.0.0 +pixi run -e dev bump-alpha # → 0.6.0a1 (pre-release) +``` + +Append `-- --dry-run` to preview without creating a tag. Once tagged, CI builds and publishes to PyPI automatically. diff --git a/README.md b/README.md index 3a7c4a78..4135a2e5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# ngio - Next Generation file format IO +

+ + + + +

[![License](https://img.shields.io/pypi/l/ngio.svg?color=green)](https://github.com/BioVisionCenter/ngio/raw/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/ngio.svg?color=green)](https://pypi.org/project/ngio) @@ -6,69 +11,66 @@ [![CI](https://github.com/BioVisionCenter/ngio/actions/workflows/ci.yml/badge.svg)](https://github.com/BioVisionCenter/ngio/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/BioVisionCenter/ngio/graph/badge.svg?token=FkmF26FZki)](https://codecov.io/gh/BioVisionCenter/ngio) -ngio is a Python library designed to simplify bioimage analysis workflows, offering an intuitive interface for working with OME-Zarr files. +**Next generation file format IO — a Python library for OME-Zarr bioimage analysis.** -## What is ngio? - -ngio is built for the [OME-Zarr](https://ngff.openmicroscopy.org/) file format, a modern, cloud-optimized format for biological imaging data. OME-Zarr stores large, multi-dimensional microscopy images and metadata in an efficient and scalable way. - -ngio's mission is to streamline working with OME-Zarr files by providing a simple, object-based API for opening, exploring, and manipulating OME-Zarr images and high-content screening (HCS) plates. It also offers comprehensive support for labels, tables and regions of interest (ROIs), making it easy to extract and analyze specific regions in your data. - -## Key Features - -### 🔍 Simple Object-Based API - -- Easily open, explore, and manipulate OME-Zarr images and HCS plates -- Create and derive new images and labels with minimal boilerplate code - -### 📊 Rich Tables and Regions of Interest (ROI) Support - -- Tight integration with [tabular data](https://biovisioncenter.github.io/ngio/stable/table_specs/overview/) -- Extract and analyze specific regions of interest -- Store measurements and other metadata in the OME-Zarr container -- Extensible & modular allowing users to define custom table schemas and on disk serialization - -### 🔄 Scalable Data Processing - -- Powerful iterators for building scalable and generalizable image processing pipelines -- Extensible mapping mechanism for custom parallelization strategies +ngio is built for [OME-Zarr](https://ngff.openmicroscopy.org/), a cloud-optimised format +that stores large, multi-dimensional microscopy images and their metadata in an efficient, +scalable way. It provides an object-based API for opening, exploring and manipulating +OME-Zarr images and high-content screening (HCS) plates, along with labels, tables and +regions of interest (ROIs) for extracting and analysing specific regions of your data. ## Installation -You can install ngio via pip: +To install ngio, use whichever package manager you already work with — it is published on +both PyPI and conda-forge. ```bash -pip install ngio +pip install ngio # pip +uv add ngio # uv project (or: uv pip install ngio) +pixi add ngio # pixi, from conda-forge (--pypi for PyPI) +mamba install -c conda-forge ngio # mamba/conda ``` -To get started check out the [Quickstart Guide](https://BioVisionCenter.github.io/ngio/stable/getting_started/0_quickstart/). - -## Supported OME-Zarr versions - -ngio supports OME-Zarr v0.4/v0.5. Support for version 0.6 and higher is planned for future releases. +Then work through the +[quickstart](https://biovisioncenter.github.io/ngio/stable/getting_started/0_quickstart/). -## Development Status +## Key features -ngio is under active development and is not yet stable. The API is subject to change, and bugs and breaking changes are expected. -We follow [Semantic Versioning](https://semver.org/). Which means for 0.x releases potentially breaking changes can be introduced in minor releases. +- **Object-based API** — open, explore and manipulate OME-Zarr images and HCS plates; + derive new images and labels with minimal boilerplate. +- **Tables and ROIs** — tight integration with [tabular + data](https://biovisioncenter.github.io/ngio/stable/table_specs/overview/), extensible + table schemas, and measurements stored alongside the image. +- **Scalable processing** — iterators for building pipelines that generalise from a single + ROI to a full plate, with a pluggable mapping mechanism for parallelisation. +- **Remote stores** — stream from S3 and other fsspec-backed sources, with a configurable + IO retry policy. +- **Supported OME-Zarr versions** — ngio supports OME-Zarr v0.4 and v0.5, backed by either Zarr v2 or v3 storage. Support for + v0.6 and later is planned. -### Available Features +## Versioning -- ✅ OME-Zarr metadata handling and validation -- ✅ Image and label access across pyramid levels -- ✅ ROI and table support -- ✅ Image processing iterators -- ✅ Streaming from remote sources -- ✅ Documentation and examples +ngio follows [semantic versioning](https://semver.org/): from 1.0 onwards the public API +is stable, and breaking changes are reserved for major releases. -### Upcoming Features +## Documentation -- Enhanced performance optimizations (parallel iterators, optimized io strategies) +Full documentation, including guides, tutorials and the API reference, is at +[biovisioncenter.github.io/ngio](https://biovisioncenter.github.io/ngio/). The worked +examples are executed when the site is built, so the code and the output you read are what +actually ran. -## Contributors +## Citing ngio -ngio is developed at the [BioVisionCenter](https://www.biovisioncenter.uzh.ch/en.html), University of Zurich, by [@lorenzocerrone](https://github.com/lorenzocerrone) and [@jluethi](https://github.com/jluethi). +If ngio contributes to work you publish, please cite it. See +[`CITATION.cff`](https://github.com/BioVisionCenter/ngio/blob/main/CITATION.cff) for the +current citation metadata. -## License +## Project -ngio is released under the BSD-3-Clause License. See [LICENSE](https://github.com/BioVisionCenter/ngio/blob/main/LICENSE) for details. +ngio is developed at the [BioVisionCenter](https://www.biovisioncenter.uzh.ch/en.html), +University of Zurich, by [@lorenzocerrone](https://github.com/lorenzocerrone) and +[@jluethi](https://github.com/jluethi). It is released under the BSD-3-Clause +[licence](https://github.com/BioVisionCenter/ngio/blob/main/LICENSE), and developed in the +open on [GitHub](https://github.com/BioVisionCenter/ngio) — issues and contributions +welcome. diff --git a/brand/fonts/OFL.txt b/brand/fonts/OFL.txt new file mode 100644 index 00000000..cb512b9a --- /dev/null +++ b/brand/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/brand/fonts/SpaceGrotesk[wght].ttf b/brand/fonts/SpaceGrotesk[wght].ttf new file mode 100644 index 00000000..a1b2e6c2 Binary files /dev/null and b/brand/fonts/SpaceGrotesk[wght].ttf differ diff --git a/brand/logo-dark.svg b/brand/logo-dark.svg new file mode 100644 index 00000000..cc0a9acf --- /dev/null +++ b/brand/logo-dark.svg @@ -0,0 +1,16 @@ + + ngio + + + + + + + + + + + + + + diff --git a/brand/logo-lockup-dark.svg b/brand/logo-lockup-dark.svg new file mode 100644 index 00000000..66d5f786 --- /dev/null +++ b/brand/logo-lockup-dark.svg @@ -0,0 +1,23 @@ + + ngio + ngio horizontal lockup. Wordmark is Space Grotesk SemiBold, outlined, tracking -0.03em. Groups: #mark, #wordmark. + + + + + + + + + + + + + + + + + + + + diff --git a/brand/logo-lockup-stacked-dark.svg b/brand/logo-lockup-stacked-dark.svg new file mode 100644 index 00000000..22a6502b --- /dev/null +++ b/brand/logo-lockup-stacked-dark.svg @@ -0,0 +1,23 @@ + + ngio + ngio stacked lockup. Mark centred over the wordmark, 0.28em of separation. + + + + + + + + + + + + + + + + + + + + diff --git a/brand/logo-lockup-stacked.svg b/brand/logo-lockup-stacked.svg new file mode 100644 index 00000000..259ebc75 --- /dev/null +++ b/brand/logo-lockup-stacked.svg @@ -0,0 +1,23 @@ + + ngio + ngio stacked lockup. Mark centred over the wordmark, 0.28em of separation. + + + + + + + + + + + + + + + + + + + + diff --git a/brand/logo-lockup.svg b/brand/logo-lockup.svg new file mode 100644 index 00000000..c29a513e --- /dev/null +++ b/brand/logo-lockup.svg @@ -0,0 +1,23 @@ + + ngio + ngio horizontal lockup. Wordmark is Space Grotesk SemiBold, outlined, tracking -0.03em. Groups: #mark, #wordmark. + + + + + + + + + + + + + + + + + + + + diff --git a/brand/logo-mono.svg b/brand/logo-mono.svg new file mode 100644 index 00000000..121af638 --- /dev/null +++ b/brand/logo-mono.svg @@ -0,0 +1,7 @@ + + ngio + + + + + diff --git a/brand/logo.svg b/brand/logo.svg new file mode 100644 index 00000000..f0748369 --- /dev/null +++ b/brand/logo.svg @@ -0,0 +1,16 @@ + + ngio + + + + + + + + + + + + + + diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 100644 index 00000000..390a8f2f --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1,33 @@ +# Docs + +Executed code lives in scripts under `docs/snippets/`, included by `pymdownx.snippets` +(`--8<-- "docs/snippets/.py:name"`, delimited by `# --8<-- [start:name]`). Use +`source="material-block"`, and `html="1"` for figures. One script per session, each +runnable standalone from the repo root. Figures and tables go through +`docs/snippets/_render.py` (house style, percentile windows, scale bars, the +colour-to-CSS-variable swap); each script imports it in a hidden `plot_helpers` / +`table_helpers` section. + +Five traps, none visible from the sources: + +- markdown-exec does not redirect `sys.stdout`. It injects its own `print` into the + globals of the block it executes, so only a `print` **written in the block** is + captured; one inside an imported module resolves to the builtin, prints to the build's + terminal, and leaves the block rendering as empty — with the build still exiting 0. + Hence `print(figure_html(fig))` rather than a `print_figure(fig)` that prints, and + hence the per-block duplication this replaced. + +- Build with `--clean --strict` (what `build_docs` does). Plain `zensical build` exits 0 + and reports "No issues found" even when a code block raised, and serves cached HTML. +- Each page gets a fresh markdown-exec session, so a page cannot use a variable bound on + another. Hence the silent `reopen_*` sections atop getting-started pages 2 and 3. +- Print tables with the `table_html` helper and `html="1"`, never `.to_markdown()` — + block-level Markdown is not run over markdown-exec output, so a pipe table stays literal + `|---|`. The helper also strips pandas' `class`/`border` attributes, which every theme + table rule is gated against (`table:not([class])`). +- Every `.md` under `docs/` becomes a page, this file included (`/CLAUDE/`). Zensical does + not read `exclude_docs`, and neither an underscore prefix nor burying the file under + `snippets/` prevents it; a post-build `rm` would not help either, because `mike deploy` + builds its own copy of the site rather than publishing `site/`. The page is absent from + `sitemap.xml` but present in the site search index. Accepted deliberately: the split + between this file and the root `CLAUDE.md` is worth more than the stray page. diff --git a/docs/_glossary.md b/docs/_glossary.md new file mode 100644 index 00000000..c34d3e95 --- /dev/null +++ b/docs/_glossary.md @@ -0,0 +1,14 @@ +*[OME-Zarr]: Cloud-optimised file format for bioimaging data, built on Zarr and the OME-NGFF specification +*[NGFF]: Next Generation File Format — the OME specification that OME-Zarr implements +*[ROI]: Region of Interest — a rectangular region of an image, defined in world or pixel coordinates +*[ROIs]: Regions of Interest — rectangular regions of an image, defined in world or pixel coordinates +*[FOV]: Field of View — the area captured in a single microscope acquisition +*[FOVs]: Fields of View — the areas captured in single microscope acquisitions +*[HCS]: High-Content Screening — plate-based imaging where each well holds one or more images +*[masking ROI]: A ROI table indexed by label id, mapping each segmented object to its bounding region +*[pyramid level]: One resolution of the multiscale image pyramid; level 0 is the highest resolution +*[acquisition]: One imaging round of a plate; a well may contain images from several acquisitions +*[backend]: The on-disk serialisation used for a table (anndata, parquet, csv or json) +*[index_key]: The table column used as the row index when a table is written to disk +*[consolidate]: Propagate a write made at one pyramid level to all the other levels +*[derive]: Create a new image or label that inherits metadata and geometry from an existing one diff --git a/docs/api/hcs.md b/docs/api/hcs.md index 28bf5cd9..7a46911f 100644 --- a/docs/api/hcs.md +++ b/docs/api/hcs.md @@ -1,17 +1,27 @@ -# HCS API Documentation +--- +description: API reference for OmeZarrPlate and OmeZarrWell. +--- -## Open a Plate +# HCS API reference + +## Open a plate ::: ngio.open_ome_zarr_plate -## ngio.OmeZarrPlate Class Reference +## OmeZarrPlate ::: ngio.OmeZarrPlate -## Open a Well +## Open a well ::: ngio.open_ome_zarr_well -## ngio.OmeZarrWell Class Reference +## OmeZarrWell ::: ngio.OmeZarrWell + +## Create a plate or a well + +::: ngio.create_empty_plate + +::: ngio.create_empty_well diff --git a/docs/api/images.md b/docs/api/images.md index e0bf91c4..bd3cc97f 100644 --- a/docs/api/images.md +++ b/docs/api/images.md @@ -1,17 +1,21 @@ -# Images Like: API Documentation +--- +description: API reference for the Image and Label objects. +--- -## Open an Image +# Images API reference + +## Open an image ::: ngio.open_image -## ngio.Image Class Reference +## Image ::: ngio.Image -## Open a Label +## Open a label ::: ngio.open_label -## ngio.Label Class Reference +## Label ::: ngio.Label diff --git a/docs/api/iterators.md b/docs/api/iterators.md index 9e8a0052..8348c91c 100644 --- a/docs/api/iterators.md +++ b/docs/api/iterators.md @@ -1,4 +1,13 @@ -# Iterators API Reference +--- +description: API reference for the ngio processing iterators. +--- + +# Iterators API reference + +!!! warning "Experimental API" + + These classes live in `ngio.experimental.iterators` and may change or be removed in a + future release without notice. ## ImageProcessingIterator diff --git a/docs/api/ngio/common.md b/docs/api/ngio/common.md index 1bcd1571..1d3820cb 100644 --- a/docs/api/ngio/common.md +++ b/docs/api/ngio/common.md @@ -1,3 +1,3 @@ -# ngio.common API documentation +# ngio.common API reference ::: ngio.common diff --git a/docs/api/ngio/hcs.md b/docs/api/ngio/hcs.md index 8a4b66c1..4fc18c7e 100644 --- a/docs/api/ngio/hcs.md +++ b/docs/api/ngio/hcs.md @@ -1,3 +1,3 @@ -# ngio.hcs API documentation +# ngio.hcs API reference ::: ngio.hcs diff --git a/docs/api/ngio/images.md b/docs/api/ngio/images.md index 11675c19..b94a755f 100644 --- a/docs/api/ngio/images.md +++ b/docs/api/ngio/images.md @@ -1,3 +1,3 @@ -# ngio.images API documentation +# ngio.images API reference ::: ngio.images diff --git a/docs/api/ngio/io_pipes.md b/docs/api/ngio/io_pipes.md index 8829da76..6fabac5d 100644 --- a/docs/api/ngio/io_pipes.md +++ b/docs/api/ngio/io_pipes.md @@ -1,3 +1,3 @@ -# ngio.io_pipes API documentation +# ngio.io_pipes API reference ::: ngio.io_pipes diff --git a/docs/api/ngio/iterators.md b/docs/api/ngio/iterators.md index 263ae024..a1fec52d 100644 --- a/docs/api/ngio/iterators.md +++ b/docs/api/ngio/iterators.md @@ -1,3 +1,3 @@ -# ngio.iterators API documentation +# ngio.experimental.iterators API reference ::: ngio.experimental.iterators diff --git a/docs/api/ngio/ngio.md b/docs/api/ngio/ngio.md index 37d5ca01..5dd82a0d 100644 --- a/docs/api/ngio/ngio.md +++ b/docs/api/ngio/ngio.md @@ -1,4 +1,4 @@ -# ngio API documentation +# ngio API reference ::: ngio options: diff --git a/docs/api/ngio/tables.md b/docs/api/ngio/tables.md index b77e2e0a..e42ee2e0 100644 --- a/docs/api/ngio/tables.md +++ b/docs/api/ngio/tables.md @@ -1,3 +1,3 @@ -# ngio.tables API documentation +# ngio.tables API reference ::: ngio.tables diff --git a/docs/api/ngio/transforms.md b/docs/api/ngio/transforms.md index 4f4100b8..430b4927 100644 --- a/docs/api/ngio/transforms.md +++ b/docs/api/ngio/transforms.md @@ -1,3 +1,3 @@ -# ngio.transforms API documentation +# ngio.transforms API reference ::: ngio.transforms diff --git a/docs/api/ngio/utils.md b/docs/api/ngio/utils.md index 01aeccad..11dff8f3 100644 --- a/docs/api/ngio/utils.md +++ b/docs/api/ngio/utils.md @@ -1,3 +1,3 @@ -# ngio.utils +# ngio.utils API reference ::: ngio.utils diff --git a/docs/api/ome_zarr_container.md b/docs/api/ome_zarr_container.md index 62c4083e..3840495e 100644 --- a/docs/api/ome_zarr_container.md +++ b/docs/api/ome_zarr_container.md @@ -1,14 +1,18 @@ -# OmeZarrContainer: API Documentation +--- +description: API reference for opening and creating OME-Zarr containers. +--- -## Open an OME-Zarr Container +# OME-Zarr container API reference + +## Open a container ::: ngio.open_ome_zarr_container -## Create an OME-Zarr Container +## Create a container ::: ngio.create_empty_ome_zarr ::: ngio.create_ome_zarr_from_array -## OmeZarrContainer Class +## OmeZarrContainer ::: ngio.OmeZarrContainer diff --git a/docs/api/tables.md b/docs/api/tables.md index 71a4b756..7309deea 100644 --- a/docs/api/tables.md +++ b/docs/api/tables.md @@ -1 +1,48 @@ -# Ngio Tables API Documentation +--- +description: API reference for ngio tables — ROI, masking ROI, feature, condition and generic tables, plus the tables container and backends. +--- + +# Tables API reference + +For the on-disk format of each table type, see the +[Table Specifications](../table_specs/overview.md). + +## Opening tables + +::: ngio.tables.open_table + +::: ngio.tables.open_table_as + +::: ngio.tables.open_tables_container + +## Tables container + +::: ngio.tables.TablesContainer + +## Table types + +### ROI tables + +::: ngio.tables.RoiTable + +::: ngio.tables.MaskingRoiTable + +::: ngio.tables.GenericRoiTable + +### Feature tables + +::: ngio.tables.FeatureTable + +### Condition tables + +::: ngio.tables.ConditionTable + +### Generic tables + +::: ngio.tables.GenericTable + +## Backends + +::: ngio.tables.TableBackend + +::: ngio.tables.ImplementedTableBackends diff --git a/docs/assets/logo-dark.svg b/docs/assets/logo-dark.svg new file mode 100644 index 00000000..cc0a9acf --- /dev/null +++ b/docs/assets/logo-dark.svg @@ -0,0 +1,16 @@ + + ngio + + + + + + + + + + + + + + diff --git a/docs/assets/logo-lockup-dark.svg b/docs/assets/logo-lockup-dark.svg new file mode 100644 index 00000000..66d5f786 --- /dev/null +++ b/docs/assets/logo-lockup-dark.svg @@ -0,0 +1,23 @@ + + ngio + ngio horizontal lockup. Wordmark is Space Grotesk SemiBold, outlined, tracking -0.03em. Groups: #mark, #wordmark. + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/logo-lockup-stacked-dark.svg b/docs/assets/logo-lockup-stacked-dark.svg new file mode 100644 index 00000000..22a6502b --- /dev/null +++ b/docs/assets/logo-lockup-stacked-dark.svg @@ -0,0 +1,23 @@ + + ngio + ngio stacked lockup. Mark centred over the wordmark, 0.28em of separation. + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/logo-lockup-stacked.svg b/docs/assets/logo-lockup-stacked.svg new file mode 100644 index 00000000..259ebc75 --- /dev/null +++ b/docs/assets/logo-lockup-stacked.svg @@ -0,0 +1,23 @@ + + ngio + ngio stacked lockup. Mark centred over the wordmark, 0.28em of separation. + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/logo-lockup.svg b/docs/assets/logo-lockup.svg new file mode 100644 index 00000000..c29a513e --- /dev/null +++ b/docs/assets/logo-lockup.svg @@ -0,0 +1,23 @@ + + ngio + ngio horizontal lockup. Wordmark is Space Grotesk SemiBold, outlined, tracking -0.03em. Groups: #mark, #wordmark. + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/logo-mono.svg b/docs/assets/logo-mono.svg new file mode 100644 index 00000000..121af638 --- /dev/null +++ b/docs/assets/logo-mono.svg @@ -0,0 +1,7 @@ + + ngio + + + + + diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 00000000..f0748369 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,16 @@ + + ngio + + + + + + + + + + + + + + diff --git a/docs/changelog.md b/docs/changelog.md index cf425851..786b75d5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1 @@ -{% - include-markdown "../CHANGELOG.md" -%} +--8<-- "CHANGELOG.md" diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md index 3f89b323..a3e93551 100644 --- a/docs/code_of_conduct.md +++ b/docs/code_of_conduct.md @@ -1,4 +1,5 @@ -# Code of Conduct +--- +description: The ngio community Code of Conduct, adapted from the Contributor Covenant v2.1. +--- -!!! warning - The library is still in the early stages of development, the code of conduct is not yet established. +--8<-- "CODE_OF_CONDUCT.md" diff --git a/docs/contributing.md b/docs/contributing.md index f8604923..a33f37ff 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,91 +1,5 @@ -# Contributing - -Contributions are welcome! Please open an issue to discuss significant changes before opening a PR. - ---- - -## Prerequisites - -- [Pixi](https://pixi.sh) — manages all environments and dependencies -- Git - -Install Pixi, then clone and set up: - -```bash -git clone https://github.com/BioVisionCenter/ngio -cd ngio -pixi install -``` - ---- - -## Development - -Work in the `dev` environment, which includes linters, type checker, and test dependencies: - -```bash -pixi shell -e dev # activate shell -# or prefix individual commands: -pixi run -e dev -``` - ---- - -## Running Tests - -```bash -pixi run -e test pytest # single run (Python 3.11) -pixi run -e test13 pytest # specific Python version (3.11–3.14) -``` - -Coverage is reported automatically. The full CI matrix covers `test11`, `test12`, `test13`, `test14` across Linux, macOS, and Windows. - --- - -## Linting & Formatting - -```bash -pixi run lint # run all hooks on all files -``` - -This runs Ruff (lint + format), `typos` (spell check), YAML/TOML validation, and notebook output stripping. Hooks also run automatically on `git commit`. - ---- - -## Commit Conventions - -Please follow [Conventional Commits](https://www.conventionalcommits.org/) — this is not enforced by a hook (yet), but helps maintain a clean history and enables automated changelog generation. - -Examples: - -``` -feat: add support for multiscale labels -fix: correct axis order in NgffImage -docs: update contributing guide -``` - +description: How to set up a development environment for ngio, run the tests and linters, and open a pull request. --- -## Opening a Pull Request - -1. Fork the repo and create a branch from `main`. -2. Make your changes with tests where relevant. -3. Run `pixi run lint` and ensure all checks pass. -4. Open a PR against `main` with a clear description of what and why. - -CI will run the full test matrix and linters automatically. - ---- - -## Releasing *(maintainers only)* - -Versions are derived from git tags via `hatch-vcs`. Use the Pixi bump tasks in the `dev` environment: - -```bash -pixi run bump-patch # 0.5.7 → 0.5.8 -pixi run bump-minor # 0.5.7 → 0.6.0 -pixi run bump-major # 0.5.7 → 1.0.0 -pixi run bump-alpha # → 0.6.0a1 (pre-release) -``` - -Append `-- --dry-run` to preview without creating a tag. Once tagged, CI builds and publishes to PyPI automatically. +--8<-- "CONTRIBUTING.md" diff --git a/docs/getting_started/0_quickstart.md b/docs/getting_started/0_quickstart.md index 59866472..e7cf4c14 100644 --- a/docs/getting_started/0_quickstart.md +++ b/docs/getting_started/0_quickstart.md @@ -1,10 +1,18 @@ +--- +description: Install ngio and open your first OME-Zarr container in a few lines of Python. +--- + # Quickstart -Ngio is a Python package that provides a simple and intuitive API for reading and writing data to and from OME-Zarr. This guide will walk you through the basics of using `ngio` to read and write data. +**Install ngio and open your first OME-Zarr container.** + +In a few lines of Python you can open an OME-Zarr store, see what is inside it, and reach +the images, labels and tables it contains. ## Installation -`ngio` can be installed from PyPI, conda-forge, or from source. +To install `ngio`, use whichever package manager you already work with — it is published on +PyPI and conda-forge, and can also be installed from source. - `ngio` requires Python `>=3.11` @@ -16,6 +24,27 @@ Ngio is a Python package that provides a simple and intuitive API for reading an pip install ngio ``` +=== "uv" + + Inside a uv project: + + ```bash + uv add ngio + ``` + + Or into an existing environment: + + ```bash + uv pip install ngio + ``` + +=== "pixi" + + ```bash + pixi add ngio # from conda-forge + pixi add --pypi ngio # from PyPI + ``` + === "mamba/conda" Alternatively, you can install `ngio` using mamba: @@ -34,7 +63,7 @@ Ngio is a Python package that provides a simple and intuitive API for reading an 1. Clone the repository: ```bash - git clone https://github.com/fractal-analytics-platform/ngio.git + git clone https://github.com/BioVisionCenter/ngio.git cd ngio ``` @@ -45,55 +74,53 @@ Ngio is a Python package that provides a simple and intuitive API for reading an ### Troubleshooting -Please report installation problems by opening an issue on our [GitHub repository](https://github.com/fractal-analytics-platform/ngio). +Please report installation problems by opening an issue on the [ngio GitHub repository](https://github.com/BioVisionCenter/ngio). -## Setup some test data +## Set up test data -Let's start by downloading a sample OME-Zarr dataset to work with. +Download a sample OME-Zarr dataset to work with. ```python exec="true" source="material-block" session="quickstart" -from pathlib import Path -from ngio.utils import download_ome_zarr_dataset - -# Download a sample dataset -download_dir = Path("./data") -download_dir = Path(".").absolute() / "data" # markdown-exec: hide -hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) -image_path = hcs_path / "B" / "03" / "0" +--8<-- "docs/snippets/getting_started/quickstart.py:setup" ``` ## Open an OME-Zarr image -Let's start by opening an OME-Zarr file and inspecting its contents. +Open an OME-Zarr file and inspect its contents. + +```python exec="true" source="material-block" session="quickstart" +--8<-- "docs/snippets/getting_started/quickstart.py:open_container" +``` + +The pixels are one call away — here is the DAPI channel of that container, read from a +lower pyramid level: -```pycon exec="true" source="console" session="quickstart" ->>> from ngio import open_ome_zarr_container ->>> ome_zarr_container = open_ome_zarr_container(image_path) ->>> ome_zarr_container ->>> print(ome_zarr_container) # markdown-exec: hide +```python exec="true" session="quickstart" +--8<-- "docs/snippets/getting_started/quickstart.py:plot_helpers" +``` +```python exec="true" html="1" session="quickstart" +--8<-- "docs/snippets/getting_started/quickstart.py:plot_quickstart_image" ``` ### What is the OME-Zarr container? -The `OME-Zarr Container` is the core of ngio and the entry point to working with OME-Zarr images. It provides high-level access to the image metadata, images, labels, and tables. +The OME-Zarr container is the core of ngio and the entry point to working with OME-Zarr images. It provides high-level access to the image metadata, images, labels, and tables. The [next section](1_ome_zarr_containers.md) goes into more detail: inspecting and editing metadata, opening remote stores, and deriving new images and labels. ### What is the OME-Zarr container not? -The `OME-Zarr Container` object does not allow the user to interact with the image data directly. For that, we need to use the `Image`, `Label`, and `Table` objects. +The OME-Zarr container does not give you access to the image data directly. For that, use the `Image`, `Label`, and `Table` objects. ## Next steps -To learn how to work with the `OME-Zarr Container` object, but also with the image, label, and table data, check out the following guides: - -- [OME-Zarr Container](1_ome_zarr_containers.md): An overview on how to use the OME-Zarr Container object and how to create new images and labels. -- [Images/Labels](2_images.md): To know more on how to work with image data. -- [Tables](3_tables.md): To know more on how to work with table data, and how you can combine tables with image data. -- [Masked Images/Labels](4_masked_images.md): To know more on how to work with masked image data. -- [HCS Plates](5_hcs.md): To know more on how to work with HCS plate data. +- [OME-Zarr containers](1_ome_zarr_containers.md) — inspect and modify metadata, and create new images and labels. +- [Images and labels](2_images.md) — read and write pixel data. +- [Tables](3_tables.md) — ROIs, features and measurements stored alongside the image. +- [Masked images and labels](4_masked_images.md) — work object-by-object using a segmentation. +- [HCS plates](5_hcs.md) — scale up from a single image to a whole plate. -Also, checkout our jupyer notebook tutorials for more examples: +For worked end-to-end examples, see the tutorials: -- [Image Processing](../tutorials/image_processing.ipynb): Learn how to perform simple image processing operations. -- [Image Segmentation](../tutorials/image_segmentation.ipynb): Learn how to create new labels from images. -- [Feature Extraction](../tutorials/feature_extraction.ipynb): Learn how to extract features from images. -- [HCS Exploration](../tutorials/hcs_exploration.ipynb): Learn how to explore high-content screening data using ngio. +- [Image processing](../tutorials/image_processing.md) — apply a processing step across an image. +- [Image segmentation](../tutorials/image_segmentation.md) — create new labels from images. +- [Feature extraction](../tutorials/feature_extraction.md) — measure objects and store the results. +- [HCS exploration](../tutorials/hcs_exploration.md) — navigate high-content screening data. diff --git a/docs/getting_started/1_ome_zarr_containers.md b/docs/getting_started/1_ome_zarr_containers.md index daf6b5fe..ec4a56f2 100644 --- a/docs/getting_started/1_ome_zarr_containers.md +++ b/docs/getting_started/1_ome_zarr_containers.md @@ -1,38 +1,124 @@ -# 1. OME-Zarr Container - -Let's see how to open and explore an OME-Zarr image using `ngio`: +--- +description: "The OME-Zarr container object: inspect and modify metadata, derive and create images, and open remote stores." +--- + +# 1. OME-Zarr containers + +**Open an OME-Zarr image and explore what it holds.** + +The OME-Zarr container is your entry point to working with OME-Zarr images. It gives you +high-level access to the metadata, images, labels and tables in a store. + + +
+ + The objects inside an OME-Zarr container + An OmeZarrContainer branches into an images container made of one Image per resolution level, a labels container holding named multiscale labels, and a tables container holding typed tables. + + + + + + + + + + OmeZarrContainer + the store, opened + + + + + + + IMAGE · PIXEL DATA + LABELS · SEGMENTATIONS + TABLES · MEASUREMENTS AND ROIS + + + + + + + + + ImagesContainer + several resolutions + + + + + + + LabelsContainer + masks, by name + + + + + TablesContainer + tables, by name + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Image · level 0 + Image · level 1 + Image · level 2 … + Label · nuclei + Label · cells + RoiTable + FeatureTable + MaskingRoiTable + + … any number of labels + +
```python exec="true" source="material-block" session="get_started" -from pathlib import Path -from ngio import open_ome_zarr_container -from ngio.utils import download_ome_zarr_dataset - -# Download a sample dataset -download_dir = Path("./data") -download_dir = Path(".").absolute() / "data" # markdown-exec: hide -hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) -image_path = hcs_path / "B" / "03" / "0" - -# Open the OME-Zarr container -ome_zarr_container = open_ome_zarr_container(image_path) +--8<-- "docs/snippets/getting_started/get_started.py:setup" ``` -The `OME-Zarr Container` in is your entry point to working with OME-Zarr images. It provides high-level access to the image metadata, images, labels, and tables. - -```pycon exec="true" source="console" session="get_started" ->>> ome_zarr_container ->>> print(ome_zarr_container) # markdown-exec: hide +```python exec="true" source="material-block" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:print_container" ``` -The `OME-Zarr Container` will be the starting point for all your image processing tasks. +The OME-Zarr container will be the starting point for all your image processing tasks. ## Main concepts ### What is the OME-Zarr container? -The `OME-Zarr Container` in ngio is your entry point to working with OME-Zarr images. - -It provides: +The OME-Zarr container gives you: - **OME-Zarr overview**: get an overview of the OME-Zarr file, including the number of image levels, list of labels, and tables available. - **Image access**: get access to the images at different resolution levels and pixel sizes. @@ -42,95 +128,96 @@ It provides: ### What is the OME-Zarr container not? -The `OME-Zarr Container` object does not allow the user to interact with the image data directly. For that, we need to use the `Image`, `Label`, and `Table` objects. +The OME-Zarr container does not give you access to the image data directly. For that, use the `Image`, `Label`, and `Table` objects. ## OME-Zarr overview -Examples of the OME-Zarr metadata access: +Examples of accessing the OME-Zarr metadata: -=== "Number of Resolution Levels" +=== "Number of resolution levels" Show the number of resolution levels: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.levels # Show the number of resolution levels - >>> print(ome_zarr_container.levels) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:levels" ``` -=== "Available Paths" +=== "Available paths" Show the paths to all available resolution levels: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.level_paths # Show the paths to all available images - >>> print(ome_zarr_container.level_paths) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:level_paths" ``` === "Dimensionality" Show if the image is 2D or 3D: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.is_3d # Get if the image is 3D - >>> print(ome_zarr_container.is_3d) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:is_3d" ``` or if the image is a time series: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.is_time_series # Get if the image is a time series - >>> print(ome_zarr_container.is_time_series) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:is_time_series" ``` -=== "Full Metadata Object" - ```pycon exec="true" source="console" session="get_started" - >>> metadata = ome_zarr_container.meta - >>> print(metadata) # markdown-exec: hide +=== "Full metadata object" + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:metadata" ``` The metadata object contains all the information about the image, for example, the channel labels: - ```pycon exec="true" source="console" session="get_started" - >>> metadata.channels_meta.channel_labels - >>> print(metadata.channels_meta.channel_labels) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:channel_labels" + ``` + And those three channels, read from a lower pyramid level: + ```python exec="true" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:plot_helpers" + ``` + ```python exec="true" html="1" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:plot_container_channels" ``` ## Modifying metadata -ngio provides methods to modify the image metadata, such as channel labels, colors, display windows, axes names, and units. +ngio provides methods to modify the image metadata, such as channel labels, colours, display windows, axes names, and units. ### Channel metadata -You can update channel labels, colors, and display windows: +You can update channel labels, colours, and display windows: -=== "Channel Labels" +=== "Channel labels" Update the labels (names) of the channels: ```python - >>> ome_zarr_container.set_channel_labels(["DAPI", "GFP", "RFP"]) + ome_zarr_container.set_channel_labels(["DAPI", "GFP", "RFP"]) ``` -=== "Channel Colors" - Update the display colors of the channels (hex format): +=== "Channel colours" + Update the display colours of the channels (hex format): ```python - >>> ome_zarr_container.set_channel_colors(["0000FF", "00FF00", "FF0000"]) + ome_zarr_container.set_channel_colors(["0000FF", "00FF00", "FF0000"]) ``` -=== "Channel Windows" +=== "Channel windows" Update the display windows (start/end values) for each channel: ```python - >>> ome_zarr_container.set_channel_windows([(0, 255), (0, 1000), (0, 500)]) + ome_zarr_container.set_channel_windows([(0, 255), (0, 1000), (0, 500)]) ``` -=== "Channel Windows from Percentiles" +=== "Channel windows from percentiles" Automatically compute display windows based on data percentiles: ```python - >>> ome_zarr_container.set_channel_windows_with_percentiles(percentiles=(0.1, 99.9)) + ome_zarr_container.set_channel_windows_with_percentiles(percentiles=(0.1, 99.9)) ``` ### Axes metadata You can update the axes names and units: -=== "Axes Names" +=== "Axes names" Rename the axes in the metadata: ```python - >>> ome_zarr_container.set_axes_names(["t", "c", "z", "y", "x"]) + ome_zarr_container.set_axes_names(["t", "c", "z", "y", "x"]) ``` -=== "Axes Units" +=== "Axes units" Set the space and time units: ```python - >>> ome_zarr_container.set_axes_units(space_unit="micrometer", time_unit="second") + ome_zarr_container.set_axes_units(space_unit="micrometer", time_unit="second") ``` ### Image name @@ -138,7 +225,7 @@ You can update the axes names and units: You can set the name of the image in the metadata: ```python ->>> ome_zarr_container.set_name("My Processed Image") +ome_zarr_container.set_name("My Processed Image") ``` !!! note @@ -146,9 +233,9 @@ You can set the name of the image in the metadata: ## Accessing images / labels / tables -To access images, labels, and tables, you can use the `get_image`, `get_label`, and `get_table` methods of the `OME-Zarr Container` object. +To access images, labels, and tables, you can use the `get_image`, `get_label`, and `get_table` methods of the OME-Zarr container. -A variety of examples and additional information can be found in the [Images and Labels](./2_images.md), and [Tables](./3_tables.md) sections. +A variety of examples and additional information can be found in the [Images and labels](./2_images.md), and [Tables](./3_tables.md) sections. ## Creating derived images @@ -193,7 +280,7 @@ new_ome_zarr_image = create_ome_zarr_from_array( ) ``` -Alternatively, if you wanto to create an a empty OME-Zarr image, you can use the `create_empty_ome_zarr` function: +Alternatively, if you want to create an empty OME-Zarr image, you can use the `create_empty_ome_zarr` function: ```python from ngio import create_empty_ome_zarr @@ -211,11 +298,12 @@ This will create an empty OME-Zarr image with the specified shape and pixel size ## Opening remote OME-Zarr containers You can use `ngio` to open remote OME-Zarr containers. -For publicly available OME-Zarr containers, you can just use the `open_ome_zarr_container` function with a URL. +For publicly available OME-Zarr containers, use the `open_ome_zarr_container` function with a URL. -For example, to open a remote OME-Zarr container hosted on a github repository: +For example, to open a remote OME-Zarr container hosted on a GitHub repository: ```python +from ngio import open_ome_zarr_container from ngio.utils import fractal_fsspec_store url = ( @@ -229,12 +317,18 @@ store = fractal_fsspec_store(url=url) ome_zarr_container = open_ome_zarr_container(store) ``` -For fractal users, the `fractal_fsspec_store` function can be used to open private OME-Zarr containers. -In this case we need to provide a `fractal_token` to authenticate the user. +For Fractal users, the `fractal_fsspec_store` function can be used to open private OME-Zarr containers. +In this case you need to provide a `fractal_token` to authenticate. ```python +from ngio import open_ome_zarr_container from ngio.utils import fractal_fsspec_store store = fractal_fsspec_store(url="https://fractal_url...", fractal_token="**your_secret_token**") ome_zarr_container = open_ome_zarr_container(store) ``` + +## Next steps + +- [Images and labels](2_images.md) — read and write pixel data. +- [Tables](3_tables.md) — ROIs, features and measurements stored alongside the image. diff --git a/docs/getting_started/2_images.md b/docs/getting_started/2_images.md index ef8e473b..91fd2844 100644 --- a/docs/getting_started/2_images.md +++ b/docs/getting_started/2_images.md @@ -1,213 +1,286 @@ -# 2. Images and Labels +--- +description: "Read and write OME-Zarr pixel data: resolution levels, numpy and dask access, slicing, and labels." +--- + +# 2. Images and labels + +**Read and write the pixel data.** + +An [`Image`][ngio.Image] gives you the data of one resolution level: as numpy or dask, sliced +by axis or by a region of interest in world coordinates. A [`Label`][ngio.Label] is a +segmentation stored the same way, and behaves the same way. + + +
+ + A multiscale pyramid + Every level covers the same physical region of the sample. Each step doubles the pixel size, so the same area is stored with a quarter of the pixels. + + + + + + + + + + + + + + + + + + + + + + + + + + + + level 0 + level 1 + level 2 + + + full resolution + half in each axis + a quarter in each axis + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4320 × 5120 px + 2160 × 2560 px + 1080 × 1280 px + + + 0.325 µm per pixel + 0.65 µm per pixel + 1.3 µm per pixel + + the physical region never changes — only how finely it is sampled + +
## Images -In order to start working with the image data, we need to instantiate an `Image` object. +To start working with the image data, instantiate an [`Image`][ngio.Image] object. ngio provides a high-level API to access the image data at different resolution levels and pixel sizes. ### Getting an image -=== "Highest Resolution Image" +```python exec="true" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:reopen_container" +``` +```python exec="true" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:plot_helpers" +``` + +=== "Highest resolution image" By default, the `get_image` method returns the highest resolution image: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.get_image() # Get the highest resolution image - >>> print(ome_zarr_container.get_image()) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:get_image_default" ``` -=== "Specific Pyramid Level" +=== "Specific pyramid level" To get a specific pyramid level, you can use the `path` parameter: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.get_image(path="1") # Get a specific pyramid level - >>> print(ome_zarr_container.get_image(path="1")) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:get_image_by_path" ``` This will return the image at the specified pyramid level. -=== "Specific Resolution" +=== "Specific resolution" If you want to get an image with a specific pixel size, you can use the `pixel_size` parameter: - ```pycon exec="true" source="console" session="get_started" - >>> from ngio import PixelSize - >>> pixel_size = PixelSize(x=0.65, y=0.65, z=1.0) - >>> ome_zarr_container.get_image(pixel_size=pixel_size) - >>> image = ome_zarr_container.get_image(pixel_size=pixel_size) # markdown-exec: hide - >>> print(image) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:get_image_by_pixel_size" ``` -=== "Nearest Resolution" - By default the pixels must match exactly the requested pixel size. If you want to get the nearest resolution, you can use the `strict` parameter: - ```pycon exec="true" source="console" session="get_started" - >>> from ngio import PixelSize - >>> pixel_size = PixelSize(x=0.60, y=0.60, z=1.0) - >>> ome_zarr_container.get_image(pixel_size=pixel_size, strict=False) - >>> image = ome_zarr_container.get_image(pixel_size=pixel_size, strict=False) # markdown-exec: hide - >>> print(image) # markdown-exec: hide +=== "Nearest resolution" + ngio returns the level whose pixel size is nearest to the one you ask for. That is the default, `strict=False`, spelled out here: + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:get_image_nearest" ``` - This will return the image with the nearest resolution to the requested pixel size. + Pass `strict=True` instead to require an exact match, and raise `NgioValueError` when no level has that pixel size. The module-level [`open_image`][ngio.open_image] and [`open_label`][ngio.open_label] functions default the other way, to `strict=True`. -Similarly to the `OME-Zarr Container`, the `Image` object provides a high-level API to access the image metadata. +Similarly to the OME-Zarr container, the `Image` object provides a high-level API to access the image metadata. === "Dimensions" - ```pycon exec="true" source="console" session="get_started" - >>> image.dimensions - >>> print(image.dimensions) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:image_dimensions" ``` - The `dimensions` attribute returns a object with the image dimensions for each axis. + The `dimensions` attribute returns an object with the image dimensions for each axis. -=== "Pixel Size" - ```pycon exec="true" source="console" session="get_started" - >>> image.pixel_size - >>> print(image.pixel_size) # markdown-exec: hide +=== "Pixel size" + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:image_pixel_size" ``` The `pixel_size` attribute returns the pixel size for each axis. -=== "On disk array infos" - ```pycon exec="true" source="console" session="get_started" - >>> image.shape, image.dtype, image.chunks - >>> print(image.shape, image.dtype, image.chunks) # markdown-exec: hide +=== "On-disk array info" + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:image_array_info" ``` - The `axes` attribute returns the order of the axes in the image. + `shape`, `dtype` and `chunks` come straight from the underlying Zarr array; `axes` gives the order those dimensions are stored in. ### Working with image data Once you have the `Image` object, you can access the image data as a: -=== "Numpy Array" - ```pycon exec="true" source="console" session="get_started" - >>> data = image.get_as_numpy() # Get the image as a numpy array - >>> data.shape, data.dtype - >>> print(data.shape, data.dtype) # markdown-exec: hide +=== "Numpy array" + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:image_as_numpy" ``` -=== "Dask Array" - ```pycon exec="true" source="console" session="get_started" - >>> dask_array = image.get_as_dask() # Get the image as a dask array - >>> dask_array - >>> print(dask_array) # markdown-exec: hide +=== "Dask array" + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:image_as_dask" ``` -=== "Legacy" - A generic `get_array` method is still available for backwards compatibility. +=== "Either, by mode" + `get_array` is the generic form of the two above: one entry point that picks the backend from a `mode` argument. Reach for it when the backend is decided at runtime; otherwise prefer the explicit `get_as_numpy` / `get_as_dask`. - ```pycon exec="true" source="console" session="get_started" - >>> data = image.get_array(mode="numpy") # Get the image as a numpy or dask or delayed object - >>> data.shape, data.dtype - >>> print(data.shape, data.dtype) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:image_get_array_legacy" ``` -The `get_as_*` can also be used to slice the image data, and query specific axes in specific orders: +The `get_as_*` methods can also slice the image data, and return the axes in an order you choose: -```pycon exec="true" source="console" session="get_started" ->>> image_slice = image.get_as_numpy(channel_selection="DAPI", x=slice(0, 128), axes_order=["t", "z", "y", "x", "c"]) # Get a specific channel and axes order ->>> image_slice.shape ->>> print(image_slice.shape) # markdown-exec: hide +```python exec="true" source="material-block" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:image_slice" ``` -If you want to edit the image data, you can use the `set_array` method: +To write pixel data back, use the `set_array` method: ```python ->>> image.set_array(data) # Set the image data +image.set_array(data) ``` -The `set_array` method can be used to set the image data from a numpy array, dask array, or dask delayed object. +It accepts a numpy array or a dask array, and takes the same slicing and `axes_order` +arguments as the getters, so you can write back exactly the region you read. -A minimal example of how to use the `get_array` and `set_array` methods: +A minimal read-modify-write example: ```python exec="true" source="material-block" session="get_started" -# Get the image data as a numpy array -data = image.get_as_numpy(channel_selection="DAPI", x=slice(0, 128), y=slice(0, 128), axes_order=["z", "y", "x", "c"]) - -# Modify the image data -some_function = lambda x: x # markdown-exec: hide -data = some_function(data) - -# Set the modified image data -image.set_array(data, channel_selection="DAPI", x=slice(0, 128), y=slice(0, 128), axes_order=["z", "y", "x", "c"]) -image.consolidate() # Consolidate the changes to all resolution levels, see below for more details +--8<-- "docs/snippets/getting_started/get_started.py:set_array_example" ``` !!! important - The `set_array` method will overwrite the image data at single resolution level. After you have finished editing the image data, you need to `consolidate` the changes to the OME-Zarr file at all resolution levels: + `set_array` writes to one resolution level only. Once you have finished editing, consolidate the changes so the rest of the pyramid is rebuilt from it: ```python - >>> image.consolidate() # Consolidate the changes + image.consolidate() ``` - This will write the changes to the OME-Zarr file at all resolution levels. ### World coordinates slicing -To read or write a specific region of the image defined in world coordinates, you can use the `Roi` object. +To read or write a specific region of the image defined in world coordinates, you can use the [`Roi`][ngio.Roi] object. -```pycon exec="true" source="console" session="get_started" ->>> from ngio import Roi ->>> roi = Roi.from_values(slices={"x": (34.1, 321.6), "y": (10, 330)}, name=None) # Define a ROI in world coordinates ->>> image.get_roi_as_numpy(roi) # Get the image data in the ROI as a numpy array ->>> print(image.get_roi_as_numpy(roi).shape) # markdown-exec: hide +```python exec="true" source="material-block" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:roi_slicing" +``` + +The ROI is defined in micrometres, so it names the same region whatever pyramid level you +read it from — on the left it is outlined on the whole image, on the right it is the region +that came back: + +```python exec="true" html="1" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:plot_roi_slicing" ``` ## Labels -`Labels` represent segmentation masks that identify objects in the image. In ngio `Labels` are similar to `Images` and can -be accessed and manipulated in the same way. +A label is a segmentation mask that identifies objects in the image. In ngio a [`Label`][ngio.Label] +behaves like an [`Image`][ngio.Image], and is accessed and manipulated the same way. ### Getting a label -Now let's see what labels are available in our image: +See which labels are available in the image: -```pycon exec="true" source="console" session="get_started" -# List all available labels ->>> ome_zarr_container.list_labels() # Available labels ->>> print(ome_zarr_container.list_labels()) # markdown-exec: hide ->>> print("") # markdown-exec: hide +```python exec="true" source="material-block" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:list_labels" ``` -We have `4` labels available in our image. Let's see how to access them: +Here is how to reach one of them: -=== "Highest Resolution Label" +=== "Highest resolution label" By default, the `get_label` method returns the highest resolution label: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.get_label("nuclei") # Get the highest resolution label - >>> print(ome_zarr_container.get_label("nuclei")) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:get_label_default" ``` -=== "Specific Pyramid Level" +=== "Specific pyramid level" To get a specific pyramid level, you can use the `path` parameter: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.get_label("nuclei", path="1") # Get a specific pyramid level - >>> print(ome_zarr_container.get_label("nuclei", path="1")) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:get_label_by_path" ``` This will return the label at the specified pyramid level. -=== "Specific Resolution" +=== "Specific resolution" If you want to get a label with a specific pixel size, you can use the `pixel_size` parameter: - ```pycon exec="true" source="console" session="get_started" - >>> from ngio import PixelSize - >>> pixel_size = PixelSize(x=0.65, y=0.65, z=1.0) - >>> ome_zarr_container.get_label("nuclei", pixel_size=pixel_size) - >>> label_nuclei = ome_zarr_container.get_label("nuclei", pixel_size=pixel_size) # markdown-exec: hide - >>> print(label_nuclei) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:get_label_by_pixel_size" ``` -=== "Nearest Resolution" - By default the pixels must match exactly the requested pixel size. If you want to get the nearest resolution, you can use the `strict` parameter: - ```pycon exec="true" source="console" session="get_started" - >>> from ngio import PixelSize - >>> pixel_size = PixelSize(x=0.60, y=0.60, z=1.0) - >>> ome_zarr_container.get_label("nuclei", pixel_size=pixel_size, strict=False) - >>> label_nuclei = ome_zarr_container.get_label("nuclei", pixel_size=pixel_size, strict=False) # markdown-exec: hide - >>> print(label_nuclei) # markdown-exec: hide +=== "Nearest resolution" + As with images, the nearest level wins unless you ask for an exact match with `strict=True`: + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:get_label_nearest" ``` - This will return the label with the nearest resolution to the requested pixel size. + +Each object in a label carries its own id, drawn here in its own colour over the channel it +was segmented from: + +```python exec="true" html="1" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:plot_label_overlay" +``` ### Working with label data -Data access and manipulation for `Labels` is similar to `Images`. You can use the `get_array` and `set_array` methods to access and modify the label data. +Reading and writing label data works exactly as it does for images: `get_as_numpy`, `get_as_dask`, `get_roi_as_numpy` and `set_array` are all available on a `Label`. ### Deriving a label Often, you might want to create a new label based on an existing image. You can do this using the `derive_label` method: -```pycon exec="true" source="console" session="get_started" ->>> new_label = ome_zarr_container.derive_label("new_label", overwrite=True) # Derive a new label ->>> print(new_label) # markdown-exec: hide +```python exec="true" source="material-block" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:derive_label" ``` This will create a new label with the same dimensions as the original image (without channels) and compatible metadata. -If you want to create a new label with slightly different metadata see [API Reference](../api/images.md). +If you want to create a new label with slightly different metadata see the [images API reference](../api/images.md). + +## Next steps + +- [Tables](3_tables.md) — use ROIs to slice the image data you just learned to read. +- [Masked images and labels](4_masked_images.md) — work object-by-object using a segmentation. +- [Images API reference](../api/images.md) — every method on `Image` and `Label`. diff --git a/docs/getting_started/3_tables.md b/docs/getting_started/3_tables.md index 921f4ffd..ce323dd0 100644 --- a/docs/getting_started/3_tables.md +++ b/docs/getting_started/3_tables.md @@ -1,221 +1,127 @@ +--- +description: "Load and create ngio tables: ROI, masking ROI, feature and generic tables." +--- + # 3. Tables -Tables are not part of the core OME-Zarr specification but can be used in ngio to store measurements, features, regions of interest (ROIs), and other tabular data. Ngio follows the [Fractal's Table Spec](https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/). +**Keep ROIs, features and measurements alongside the image.** + +Tables are not part of the core OME-Zarr specification, but ngio uses them to store regions +of interest (ROIs), per-object measurements and other tabular data next to the pixel data. +The on-disk layout follows ngio's [table specifications](../table_specs/overview.md). It was +originally defined as part of [Fractal](https://fractal-analytics-platform.github.io/); ngio +is now where the spec lives and is maintained. ## Getting a table -We can list all available tables and load a specific table: +List all available tables and load a specific one: + +```python exec="true" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:reopen_container" +``` + +```python exec="true" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:reopen_image" +``` -```pycon exec="true" source="console" session="get_started" -# List all available tables ->>> ome_zarr_container.list_tables() ->>> list_tables = ome_zarr_container.list_tables() # markdown-exec: hide ->>> print(list_tables) # markdown-exec: hide +```python exec="true" source="material-block" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:list_tables" ``` -Ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_roi_table`, as well as untyped `generic_table`. +ngio recognises four typed tables — `roi_table`, `masking_roi_table`, `feature_table` and `condition_table` — plus the untyped `generic_table`, which is what anything it cannot classify is loaded as. The three you will meet most often are below; see the [table specifications](../table_specs/overview.md) for the rest. -=== "ROI Table" +=== "ROI table" ROI tables can be used to store arbitrary regions of interest (ROIs) in the image. - Here for example we will load the `FOV_ROI_table` that contains the microscope field of view (FOV) ROIs: - ```pycon exec="true" source="console" session="get_started" - >>> roi_table = ome_zarr_container.get_table("FOV_ROI_table") # Get a ROI table - >>> roi_table.get("FOV_1") - >>> print(roi_table.get("FOV_1")) # markdown-exec: hide - ``` - ```python exec="1" html="1" session="get_started" - from io import StringIO - import matplotlib.pyplot as plt - import numpy as np - # Create a random colormap for labels - from matplotlib.colors import ListedColormap - from matplotlib.patches import Rectangle - np.random.seed(0) - cmap_array = np.random.rand(1000, 3) - cmap_array[0] = 0 - cmap = ListedColormap(cmap_array) - image_3 = ome_zarr_container.get_image(path="3") - image_data = image_3.get_as_numpy(c=0) - image_data = np.squeeze(image_data) - roi = roi_table.get("FOV_1") - roi = roi.to_pixel(pixel_size=image_3.pixel_size) - x_slice = roi.get("x") - y_slice = roi.get("y") - #label_3 = ome_zarr_container.get_label("nuclei", pixel_size=image_3.pixel_size) - #label_data = label_3.get_as_numpy() - #label_data = np.squeeze(label_data) - fig, ax = plt.subplots(figsize=(8, 4)) - ax.set_title("FOV_1 ROI") - ax.imshow(image_data, cmap='gray') - ax.add_patch(Rectangle((x_slice.start, y_slice.start), x_slice.length, y_slice.length, edgecolor='red', facecolor='none', lw=2)) - #ax.imshow(label_data, cmap=cmap, alpha=0.6) - # make sure the roi is centered - ax.axis('off') - fig.tight_layout() - buffer = StringIO() - plt.savefig(buffer, format="svg") - print(buffer.getvalue()) - ``` - This will return all the ROIs in the table. - ROIs can be used to slice the image data: - ```pycon exec="true" source="console" session="get_started" - >>> roi = roi_table.get("FOV_1") - >>> roi_data = image.get_roi_as_numpy(roi) - >>> roi_data.shape - >>> print(roi_data.shape) # markdown-exec: hide + For example, load the `FOV_ROI_table`, which contains the microscope field of view (FOV) ROIs: + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:roi_table_get" + ``` + ```python exec="true" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:plot_helpers" + ``` + ```python exec="true" html="1" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:plot_fov_roi_on_image" + ``` + `get` returns the single ROI with that name; `rois()` returns them all as a list. + A ROI can then be used to slice the image data: + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:roi_table_slice_image" ``` This will return the image data for the specified ROI. - ```python exec="1" html="1" session="get_started" - from io import StringIO - import matplotlib.pyplot as plt - import numpy as np - # Create a random colormap for labels - from matplotlib.colors import ListedColormap - from matplotlib.patches import Rectangle - np.random.seed(0) - cmap_array = np.random.rand(1000, 3) - cmap_array[0] = 0 - cmap = ListedColormap(cmap_array) - roi = roi_table.get("FOV_1") - image_3 = ome_zarr_container.get_image(path="3") - image_data = image_3.get_roi_as_numpy(roi, c=0) - image_data = np.squeeze(image_data) - #label_3 = ome_zarr_container.get_label("nuclei", pixel_size=image_3.pixel_size) - #label_data = label_3.get_as_numpy() - #label_data = np.squeeze(label_data) - fig, ax = plt.subplots(figsize=(8, 4)) - ax.set_title("FOV_1 ROI") - ax.imshow(image_data, cmap='gray') - #ax.imshow(label_data, cmap=cmap, alpha=0.6) - # make sure the roi is centered - ax.axis('off') - fig.tight_layout() - buffer = StringIO() - plt.savefig(buffer, format="svg") - print(buffer.getvalue()) - ``` - -=== "Masking ROI Table" + ```python exec="true" html="1" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:plot_fov_roi_crop" + ``` + +=== "Masking ROI table" Masking ROIs are a special type of ROIs that can be used to store ROIs for masked objects in the image. The `nuclei_ROI_table` contains the masks for the `nuclei` label in the image, and is indexed by the label id. - ```pycon exec="true" source="console" session="get_started" - >>> masking_table = ome_zarr_container.get_table("nuclei_ROI_table") # Get a mask table - >>> masking_table.get_label(1) - >>> print(masking_table.get_label(100)) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:masking_table_get" ``` ROIs can be used to slice the image data: - ```pycon exec="true" source="console" session="get_started" - >>> roi = masking_table.get_label(100) - >>> roi_data = image.get_roi_as_numpy(roi) - >>> roi_data.shape - >>> print(roi_data.shape) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:masking_table_slice_image" ``` This will return the image data for the specified ROI. - ```python exec="1" html="1" session="get_started" - from io import StringIO - import matplotlib.pyplot as plt - import numpy as np - # Create a random colormap for labels - from matplotlib.colors import ListedColormap - from matplotlib.patches import Rectangle - np.random.seed(0) - cmap_array = np.random.rand(1000, 3) - cmap_array[0] = 0 - cmap = ListedColormap(cmap_array) - roi = masking_table.get_label(100) - image_3 = ome_zarr_container.get_image(path="2") - image_data = image_3.get_roi_as_numpy(roi, c=0) - image_data = np.squeeze(image_data) - label_3 = ome_zarr_container.get_label("nuclei", pixel_size=image_3.pixel_size) - label_data = label_3.get_roi_as_numpy(roi) - label_data = np.squeeze(label_data) - fig, ax = plt.subplots(figsize=(8, 4)) - ax.set_title("Label 1 ROI") - ax.imshow(image_data, cmap='gray') - ax.imshow(label_data, cmap=cmap, alpha=0.6) - # make sure the roi is centered - ax.axis('off') - fig.tight_layout() - buffer = StringIO() - plt.savefig(buffer, format="svg") - print(buffer.getvalue()) - ``` - See [4. Masked Images and Labels](./4_masked_images.md) for more details on how to use the masking ROIs to load masked data. - -=== "Features Table" - Features tables are used to store measurements and are indexed by the label id - ```pycon exec="true" source="material-block" session="get_started" - >>> feature_table = ome_zarr_container.get_table("regionprops_DAPI") # Get a feature table - >>> feature_table.dataframe.head(5) # only show the first 5 rows - >>> print(feature_table.dataframe.head(5).to_markdown()) # markdown-exec: hide + ```python exec="true" html="1" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:plot_masking_roi_crop" + ``` + See [4. Masked images and labels](./4_masked_images.md) for more details on how to use the masking ROIs to load masked data. + +=== "Feature table" + Feature tables are used to store measurements and are indexed by the label id + ```python exec="true" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:table_helpers" + ``` + ```python exec="true" html="1" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:feature_table" ``` ## Creating a table -Tables (differently from Images and Labels) can be purely in memory objects, and don't need to be saved on disk. +Tables (unlike images and labels) can be purely in-memory objects, and don't need to be saved on disk. -=== "Creating a ROI Table" - ```pycon exec="true" source="console" session="get_started" - >>> from ngio.tables import RoiTable - >>> from ngio import Roi - >>> roi = Roi.from_values(slices={"x": (0, 128), "y": (0, 128)}, name="FOV_1") - >>> roi_table = RoiTable(rois=[roi]) - >>> print(roi_table) # markdown-exec: hide +=== "Creating a ROI table" + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:create_roi_table" ``` If you would like to create on-the-fly a ROI table for the whole image: - ```pycon exec="true" source="console" session="get_started" - >>> roi_table = ome_zarr_container.build_image_roi_table("whole_image") - >>> roi_table - >>> print(ome_zarr_container.build_image_roi_table("whole_image")) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:build_image_roi_table" ``` The `build_image_roi_table` method will create a ROI table with a single ROI that covers the whole image. This table is not associated with the image and is purely in memory. - If we want to save it to disk, we can use the `add_table` method: - ```pycon exec="true" source="console" session="get_started" - >>> ome_zarr_container.add_table("new_roi_table", roi_table, overwrite=True) - >>> roi_table = ome_zarr_container.get_table("new_roi_table") - >>> print(roi_table) # markdown-exec: hide + To save it to disk, use the `add_table` method: + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:add_roi_table" ``` -=== "Creating a Masking ROI Table" - Similarly to the ROI table, we can create a masking ROI table on-the-fly: - Let's for example create a masking ROI table for the `nuclei` label: - ```pycon exec="true" source="console" session="get_started" - >>> masking_table = ome_zarr_container.build_masking_roi_table("nuclei") - >>> masking_table - >>> print(ome_zarr_container.build_masking_roi_table("nuclei")) # markdown-exec: hide +=== "Creating a masking ROI table" + As with the ROI table, you can create a masking ROI table on the fly, here for the `nuclei` label: + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:build_masking_roi_table" ``` -=== "Creating a Feature Table" +=== "Creating a feature table" Feature tables can be created from a pandas `Dataframe`: - ```pycon exec="true" source="console" session="get_started" - >>> from ngio.tables import FeatureTable - >>> import pandas as pd - >>> example_data = pd.DataFrame({"label": [1, 2, 3], "area": [100, 200, 300]}) - >>> feature_table = FeatureTable(table_data=example_data) - >>> feature_table - >>> print(feature_table) # markdown-exec: hide + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:create_feature_table" ``` -=== "Creating a Generic Table" +=== "Creating a generic table" Sometimes you might want to create a table that doesn't fit into the `ROI`, `Masking ROI`, or `Feature` categories. - In this case, you can use the `GenericTable` class, which allows you to store any tabular data. + In this case, you can use the [`GenericTable`][ngio.tables.GenericTable] class, which allows you to store any tabular data. It can be created from a pandas `Dataframe`: - ```pycon exec="true" source="console" session="get_started" - >>> from ngio.tables import GenericTable - >>> import pandas as pd - >>> example_data = pd.DataFrame({"area": [100, 200, 300], "perimeter": [50, 60, 70]}) - >>> generic_table = GenericTable(table_data=example_data) - >>> generic_table - >>> print(generic_table) # markdown-exec: hide - ``` - Or from an "AnnData" object: - ```pycon exec="true" source="console" session="get_started" - >>> from ngio.tables import GenericTable - >>> import anndata as ad - >>> adata = ad.AnnData(X=np.random.rand(10, 5), obs=pd.DataFrame({"cell_type": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]})) - >>> generic_table = GenericTable(table_data=adata) - >>> generic_table - >>> print(generic_table) # markdown-exec: hide - ``` - The `GenericTable` class allows you to store any tabular data, and is a flexible way to work with tables in ngio. + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:create_generic_table" + ``` + Or from an `AnnData` object: + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:generic_table_from_anndata" + ``` + +## Next steps + +- [Masked images and labels](4_masked_images.md) — use masking ROI tables to read per-object data. +- [Table specifications](../table_specs/overview.md) — the on-disk format behind these tables. +- [Tables API reference](../api/tables.md) — the table classes and backends. diff --git a/docs/getting_started/4_masked_images.md b/docs/getting_started/4_masked_images.md index e25e33bd..0a49dd2f 100644 --- a/docs/getting_started/4_masked_images.md +++ b/docs/getting_started/4_masked_images.md @@ -1,148 +1,153 @@ -# 4. Masked Images and Labels - -Masked images (or labels) are images that are masked by an instance segmentation mask. - -In this section we will show how to create a `MaskedImage` object and how to use it to get the data of the image. - -```python exec="true" session="masked_images" -from pathlib import Path -from ngio import open_ome_zarr_container -from ngio.utils import download_ome_zarr_dataset - -# Download a sample dataset -download_dir = Path(".").absolute() / "data" -hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) -image_path = hcs_path / "B" / "03" / "0" - -# Open the OME-Zarr container -ome_zarr_container = open_ome_zarr_container(image_path) +--- +description: Read and write image data object-by-object using a segmentation as a mask. +--- + +# 4. Masked images and labels + +**Read and write image data one object at a time.** + +A masked image is an image paired with an instance segmentation. Instead of slicing by +coordinates, you address the data by label id, and you can restrict reads and writes to +the pixels belonging to that object. + + +
+ + From a label image to a single masked object + Each object in a label image gets one row in a masking ROI table. Selecting a row returns just that object's bounding box, with the pixels outside the object masked out. + + LABEL IMAGE + MASKING ROI TABLE + ONE OBJECT + + + + + + + + + + + + + + + + + + + 1 + 2 + 4 + + 3 + each object carries an integer id + + + + + + + + + + + label + bounding box + + + 187 × 53 + 395 × 65 + 459 × 45 + + one row per object, in world units + + + + + label 3 + its own small array + +
+ +```python exec="true" source="material-block" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:setup" ``` -Similar to the `Image` and `Label` objects, the `MaskedImage` can be initialized from an `OME-Zarr Container` object using the `get_masked_image` method. +Like the `Image` and `Label` objects, a `MaskedImage` is initialised from an OME-Zarr container, using the `get_masked_image` method. -Let's create a masked image from the `nuclei` label: +Create a masked image from the `nuclei` label: -```pycon exec="true" source="console" session="masked_images" ->>> masked_image = ome_zarr_container.get_masked_image("nuclei") ->>> masked_image ->>> print(masked_image) # markdown-exec: hide +```python exec="true" source="material-block" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:get_masked_image" ``` -Since the `MaskedImage` is a subclass of `Image`, we can use all the methods available for `Image` objects. +Since `MaskedImage` is a subclass of `Image`, you can use every method available on `Image` objects. -The two most notable exceptions are the `get_roi_as_numpy` (or `get_roi_as_dask`) and `set_roi` which now instead of requiring a `roi` object, require an integer `label`. +The two most notable exceptions are `get_roi_as_numpy` (or `get_roi_as_dask`) and `set_roi`, which now take an integer `label` instead of a `roi` object. -```pycon exec="true" source="console" session="masked_images" ->>> roi_data = masked_image.get_roi_as_numpy(label=1009, c=0) ->>> roi_data.shape ->>> print(roi_data.shape) # markdown-exec: hide +```python exec="true" source="material-block" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:masked_roi_numpy" ``` -```python exec="1" html="1" session="masked_images" -from io import StringIO -import matplotlib.pyplot as plt -import numpy as np -# Create a random colormap for labels -from matplotlib.colors import ListedColormap -from matplotlib.patches import Rectangle -np.random.seed(0) -cmap_array = np.random.rand(1000, 3) -cmap_array[0] = 0 -cmap = ListedColormap(cmap_array) - -image_data = masked_image.get_roi_as_numpy(label=1009, c=0) -image_data = np.squeeze(image_data) - -fig, ax = plt.subplots(figsize=(8, 4)) -ax.set_title("Label 1009 ROI") -ax.imshow(image_data, cmap='gray') - -ax.axis('off') -fig.tight_layout() -buffer = StringIO() -plt.savefig(buffer, format="svg") -print(buffer.getvalue()) +```python exec="true" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:plot_helpers" ``` -Additionally we can used the `zoom_factor` argument to get more context around the ROI. -For example we can zoom out the ROI by a factor of `2`: - -```pycon exec="true" source="console" session="masked_images" ->>> roi_data = masked_image.get_roi_as_numpy(label=1009, c=0, zoom_factor=2) ->>> roi_data.shape ->>> print(roi_data.shape) # markdown-exec: hide +```python exec="true" html="1" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:plot_masked_roi" ``` -```python exec="1" html="1" session="masked_images" -image_data = masked_image.get_roi_as_numpy(label=1009, c=0, zoom_factor=2) -image_data = np.squeeze(image_data) +You can also use the `zoom_factor` argument to get more context around the ROI. +For example, zoom out the ROI by a factor of `2`: -fig, ax = plt.subplots(figsize=(8, 4)) -ax.set_title("Label 1009 ROI - Zoomed out") -ax.imshow(image_data, cmap='gray') +```python exec="true" source="material-block" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:masked_roi_zoom" +``` -ax.axis('off') -fig.tight_layout() -buffer = StringIO() -plt.savefig(buffer, format="svg") -print(buffer.getvalue()) +```python exec="true" html="1" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:plot_masked_roi_zoom" ``` ## Masked operations -In addition to the `get_roi_as_numpy` method, the `MaskedImage` class also provides a masked operation method that allows you to perform reading and writing only on the masked pixels. +`get_roi_as_numpy` returns the object's whole bounding box, neighbouring objects included. +To read or write only the pixels that belong to the object, use the masked operations: +`get_roi_masked_as_numpy`, `get_roi_masked_as_dask` and `set_roi_masked`. Everything +outside the mask comes back zeroed, and on write is left untouched. -For these operations we can use the `get_roi_masked` and `set_roi_masked` methods. -For example, we can use the `get_roi_masked` method to get the masked data for a specific label: +For example, read the masked data for one label: -```pycon exec="true" source="console" session="masked_images" ->>> masked_roi_data = masked_image.get_roi_masked(label=1009, c=0, zoom_factor=2) ->>> masked_roi_data.shape ->>> print(masked_roi_data.shape) # markdown-exec: hide +```python exec="true" source="material-block" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:get_roi_masked" ``` -```python exec="1" html="1" session="masked_images" -masked_roi_data = masked_image.get_roi_masked(label=1009, c=0, zoom_factor=2) -masked_roi_data = np.squeeze(masked_roi_data) -fig, ax = plt.subplots(figsize=(8, 4)) -ax.set_title("Masked Label 1009 ROI") -ax.imshow(masked_roi_data, cmap='gray') -ax.axis('off') -fig.tight_layout() -buffer = StringIO() -plt.savefig(buffer, format="svg") -print(buffer.getvalue()) +```python exec="true" html="1" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:plot_get_roi_masked" ``` -We can also use the `set_roi_masked` method to set the masked data for a specific label: +And write it back with `set_roi_masked`, which only touches the pixels inside the mask: -```pycon exec="true" source="console" session="masked_images" ->>> masked_data = masked_image.get_roi_masked(label=1009, c=0) ->>> masked_data = np.random.randint(0, 255, masked_data.shape, dtype=np.uint8) ->>> masked_image.set_roi_masked(label=1009, c=0, patch=masked_data) +```python exec="true" source="material-block" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:set_roi_masked" ``` -```python exec="1" html="1" session="masked_images" -masked_data = masked_image.get_roi_as_numpy(label=1009, c=0, zoom_factor=2) -masked_data = np.squeeze(masked_data) -fig, ax = plt.subplots(figsize=(8, 4)) -ax.set_title("Masked Label 1009 ROI - After setting") -ax.imshow(masked_data, cmap='gray') -ax.axis('off') -fig.tight_layout() -buffer = StringIO() -plt.savefig(buffer, format="svg") -print(buffer.getvalue()) +```python exec="true" html="1" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:plot_after_set_roi_masked" ``` -## Masked Labels +## Masked labels -The `MaskedLabel` class is a subclass of `Label` and provides the same functionality as the `MaskedImage` class. +The `MaskedLabel` class is a subclass of [`Label`][ngio.Label] and provides the same functionality as the `MaskedImage` class. -The `MaskedLabel` class can be used to create a masked label from an `OME-Zarr Container` object using the `get_masked_label` method. +Create a masked label from an OME-Zarr container using the `get_masked_label` method. -```pycon exec="true" source="console" session="masked_images" ->>> masked_label = ome_zarr_container.get_masked_label(label_name = "wf_2_labels", masking_label_name = "nuclei") ->>> masked_label ->>> print(masked_label) # markdown-exec: hide +```python exec="true" source="material-block" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:get_masked_label" ``` + +## Next steps + +- [HCS plates](5_hcs.md) — scale up from a single image to a whole plate. +- [Iterators](6_iterators.md) — process every object or region without writing the loop yourself. diff --git a/docs/getting_started/5_hcs.md b/docs/getting_started/5_hcs.md index 184e6f56..0267ab31 100644 --- a/docs/getting_started/5_hcs.md +++ b/docs/getting_started/5_hcs.md @@ -1,124 +1,185 @@ -# 5. HCS Plates - -Ngio provides a simple interface for high-content screening (HCS) plates. An HCS plate is a collection of OME-Zarr images organized in a grid-like structure. Each plates contains columns and rows, and each well in the plate is identified by its row and column indices. Each well can contain multiple images, and each image can belong to a different acquisition. - -The HCS plate is represented by the `OmeZarrPlate` class. - -Let's open an `OmeZarrPlate` object. - -```pycon exec="true" source="console" session="hcs_plate" ->>> from pathlib import Path # markdown-exec: hide ->>> from ngio.utils import download_ome_zarr_dataset ->>> from ngio import open_ome_zarr_plate ->>> download_dir = Path(".").absolute() / "data" # markdown-exec: hide ->>> hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) ->>> ome_zarr_plate = open_ome_zarr_plate(hcs_path) ->>> ome_zarr_plate ->>> print(ome_zarr_plate) # markdown-exec: hide +--- +description: "Work with high-content screening plates: rows, columns, acquisitions, wells and images." +--- + +# 5. HCS plates + +**Navigate a whole high-content screening plate.** + +An HCS plate is a grid of wells, each holding one or more images, possibly from different +acquisitions. The [`OmeZarrPlate`][ngio.OmeZarrPlate] class gives you the rows, columns and +acquisitions of the plate, and the images inside each well. + + +
+ + Plate, well, field of view + A plate is a grid of wells addressed by row letter and column number. A well holds one image per acquisition. Each image is an OME-Zarr container of its own. + + + + + + + + plate.zarr + well B / 03 + image B / 03 / 0 + + + rows × columns of wells + one image per acquisition + pixels, labels and ROIs + + + + + 123456 + ABCD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Open an `OmeZarrPlate` object. + +```python exec="true" source="material-block" session="hcs_plate" +--8<-- "docs/snippets/getting_started/hcs.py:setup" ``` This example plate is very small and contains only a single well. ## Plate overview -The `OmeZarrPlate` object provides a high-level overview of the plate, including rows, columns, and acquisitions. The following methods are available: +The `OmeZarrPlate` object gives you a high-level overview of the plate through three properties: === "Columns" Show the columns in the plate: - ```pycon exec="true" source="console" session="hcs_plate" - >>> ome_zarr_plate.columns - >>> print(ome_zarr_plate.columns) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:plate_columns" ``` === "Rows" Show the rows in the plate: - ```pycon exec="true" source="console" session="hcs_plate" - >>> ome_zarr_plate.rows - >>> print(ome_zarr_plate.rows) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:plate_rows" ``` === "Acquisitions" - Show the acquisitions ids: - ```pycon exec="true" source="console" session="hcs_plate" - >>> ome_zarr_plate.acquisition_ids - >>> print(ome_zarr_plate.acquisition_ids) # markdown-exec: hide + Show the acquisition ids: + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:plate_acquisitions" ``` ## Retrieving the path to the images The `OmeZarrPlate` object provides multiple methods to retrieve the path to the images in the plate. -=== "All Images Paths" +=== "All image paths" This will return the paths to all images in the plate: - ```pycon exec="true" source="console" session="hcs_plate" - >>> ome_zarr_plate.images_paths() - >>> print(ome_zarr_plate.images_paths()) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:images_paths" ``` -=== "All Wells Paths" +=== "All well paths" This will return the paths to all wells in the plate: - ```pycon exec="true" source="console" session="hcs_plate" - >>> ome_zarr_plate.wells_paths() - >>> print(ome_zarr_plate.wells_paths()) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:wells_paths" ``` -=== "All Images Paths in a Well" +=== "All image paths in a well" This will return the paths to all images in a well: - ```pycon exec="true" source="console" session="hcs_plate" - >>> ome_zarr_plate.well_images_paths(row="B", column=3) - >>> print(ome_zarr_plate.well_images_paths(row="B", column=3)) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:well_images_paths" ``` ## Getting the images -The `OmeZarrPlate` object provides a method to get the image objects in a well. The method `get_well_images` takes the row and column indices of the well and returns a list of `OmeZarrContainer` objects. +`get_well_images` takes the row and column of a well and returns a dictionary mapping each image path to its [`OmeZarrContainer`][ngio.OmeZarrContainer]. -=== "All Images" +=== "All images" Get all images in the plate: - ```pycon exec="true" source="console" session="hcs_plate" - >>> ome_zarr_plate.get_images() - >>> ome_zarr_plate - >>> print(ome_zarr_plate.get_images()) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:get_images" ``` This dictionary contains the path to the images and the corresponding `OmeZarrContainer` object. -=== "All Images in a Well" +=== "All images in a well" Get all images in a well: - ```pycon exec="true" source="console" session="hcs_plate" - >>> well_images = ome_zarr_plate.get_well_images(row="B", column=3) - >>> well_images - >>> print(well_images) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:get_well_images" ``` This dictionary contains the path to the images and the corresponding `OmeZarrContainer` object. -=== "Specific Image" +=== "Specific image" Get a specific image in a well: - ```pycon exec="true" source="console" session="hcs_plate" - >>> ome_zarr_plate.get_image(row="B", column=3, image_path="0") - >>> print(ome_zarr_plate.get_image(row="B", column=3, image_path="0")) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:get_image" ``` This will return the `OmeZarrContainer` object for the image in the well. -=== "Filter by Acquisition" +=== "Filter by acquisition" In these methods, you can also filter the images by acquisition. When available, the `acquisition` parameter can be used to filter the images by acquisition id. - ```pycon exec="true" source="console" session="hcs_plate" - >>> well_images = ome_zarr_plate.get_well_images(row="B", column=3, acquisition=0) - >>> well_images - >>> print(well_images) # markdown-exec: hide + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:get_well_images_by_acquisition" ``` - The `acquisition` is not required, and if not provided, an empty dictionary will be returned. + `acquisition` is optional: omit it and every image in the well is returned. Pass an acquisition id that the plate does not define — as on this example plate, which carries no acquisition metadata — and you get an empty dictionary back. ## Creating a plate -Ngio provides a utility function to create a plate. +ngio provides a utility function to create a plate. The first step is to create a list of `ImageInWellPath` objects. Each `ImageInWellPath` object contains the path to the image and the corresponding well. -```python exec="true" source="console" session="hcs_plate" -from ngio import ImageInWellPath -list_of_images = [ImageInWellPath(path="0", row="A", column=0), - ImageInWellPath(path="0", row="B", column=1), - ImageInWellPath(path="0", row="C", column=1), - ImageInWellPath(path="1", row="A", column=0, acquisition_id=1, acquisition_name="acquisition_1"), -] +```python exec="true" source="material-block" session="hcs_plate" +--8<-- "docs/snippets/getting_started/hcs.py:image_in_well_paths" ``` !!! note @@ -126,25 +187,20 @@ list_of_images = [ImageInWellPath(path="0", row="A", column=0), Then, you can create the plate using the `create_empty_plate` function. -```pycon exec="true" source="console" session="hcs_plate" ->>> from ngio import create_empty_plate ->>> plate = create_empty_plate(store="new_plate.zarr", name="test_plate", images=list_of_images, overwrite=True) ->>> plate ->>> print(plate) # markdown-exec: hide +```python exec="true" source="material-block" session="hcs_plate" +--8<-- "docs/snippets/getting_started/hcs.py:create_empty_plate" ``` This has created a new empty plate with the metadata correctly set. But no images have been added yet. ### Modifying the plate -You can add images or remove images +You can add or remove images. -=== "Add Images" - To add images to the plate, you can use the `add_image` method. This method takes the row and column indices of the well and the path to the image. - ```pycon exec="true" source="console" session="hcs_plate" - >>> print(f"Before adding images: {plate.rows} rows, {plate.columns} columns") - >>> plate.add_image(row="D", column=0, image_path="0") - >>> print(f"After adding images: {plate.rows} rows, {plate.columns} columns") +=== "Add images" + To add images to the plate, use the `add_image` method. It takes the row and column of the well and the path to the image within it. + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:plate_add_image" ``` This will add a new image to the plate and well metadata. !!! note @@ -152,15 +208,19 @@ You can add images or remove images !!! warning This function is not multiprocessing safe. If you are using multiprocessing, you should use the `atomic_add_image` method instead. -=== "Remove Images" - To remove images from the plate, you can use the `remove_image` method. This method takes the row and column indices of the well and the path to the image. - ```pycon exec="true" source="console" session="hcs_plate" - >>> print(f"Before removing images: {plate.wells_paths()} wells") - >>> plate.remove_image(row="D", column=0, image_path="0") - >>> print(f"After removing images: {plate.wells_paths()} wells") +=== "Remove images" + To remove images from the plate, use the `remove_image` method. It takes the same arguments as `add_image`. + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:plate_remove_image" ``` This will remove the image metadata from the plate and well metadata. !!! warning No data will be removed from the store. If an image is saved in the store it will remain there. Also the metadata will only be removed from the plate.well metadata. The number of columns and rows will not be updated. This function is not multiprocessing safe. If you are using multiprocessing, you should use the `atomic_remove_image` method instead. + +## Next steps + +- [Iterators](6_iterators.md) — build pipelines that scale across a plate. +- [HCS exploration tutorial](../tutorials/hcs_exploration.md) — a worked example on real plate data. +- [HCS API reference](../api/hcs.md) — `OmeZarrPlate` and `OmeZarrWell`. diff --git a/docs/getting_started/6_iterators.md b/docs/getting_started/6_iterators.md index 3b80205b..2a581f41 100644 --- a/docs/getting_started/6_iterators.md +++ b/docs/getting_started/6_iterators.md @@ -1,14 +1,221 @@ +--- +description: The four ngio iterators for building scalable image-processing pipelines. +--- + # 6. Iterators -When building image processing pipelines it is often useful to iterate over specific regions of the image, for example to process the image in smaller tiles or to process only specific regions of interest (ROIs). +**Process an image region by region without writing the loop.** + +When building image processing pipelines it is often useful to iterate over specific regions of the image, for example to process the image in smaller tiles or to process only specific regions of interest (ROIs). Iterators also let you set broadcasting rules for the iteration, for example to iterate over all z-planes or over all timepoints. + +!!! warning "Experimental API" + + The iterators live in `ngio.experimental.iterators`, outside the stability guarantee + that covers the rest of ngio: they may change or be removed in a future release + without notice. Everything on this page works today, but pin your ngio version if you + depend on it. + + +
+ + How an iterator walks an image + A ROI table names the regions. For each region the iterator reads that part of the input, applies your function, and writes the result into the output, one region at a time. + + + + + + + + 1234 + + + ROI TABLE + INPUT + YOUR FUNCTION + OUTPUT + + + + + + + region 1 + region 2 + region 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + process() + + + + + + + + + + + + + + + + + + + + + + repeat for every region the table names + +
+ +ngio provides four basic `Iterator` classes, all imported from +`ngio.experimental.iterators`: + + +
+ + The four iterators, by what they take and return + Segmentation takes an image and returns a label. Masked segmentation takes an image and a label and returns a label. Image processing takes an image and returns an image. Feature extraction takes an image and a label and returns a table. + + + + + SegmentationIterator + MaskedSegmentationIterator + ImageProcessingIterator + FeatureExtractorIterator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + an image in, a new label out + the same, restricted to one mask + an image in, a new image out + read only — measurements out + + + + + + + + + image + labels + table + + +
+ + +* The `SegmentationIterator` is designed to build segmentation pipelines, where an input image is processed to produce a segmentation mask. For a worked example, see the [image segmentation tutorial](../tutorials/image_segmentation.md). +* The `MaskedSegmentationIterator` is similar to the `SegmentationIterator`, but it uses a masking ROI table to restrict the segmentation to masks. This is useful when you want to segment only specific regions of the image, for example, segmenting cells only within a specific tissue region. For a worked example, see the [image segmentation tutorial](../tutorials/image_segmentation.md). +* The `ImageProcessingIterator` is designed to build image processing pipelines, where an input image is processed to produce a new image. For a worked example, see the [image processing tutorial](../tutorials/image_processing.md). +* The `FeatureExtractorIterator` is a read-only iterator designed to iterate over pairs of images and labels to extract features from the image based on the labels. For a worked example, see the [feature extraction tutorial](../tutorials/feature_extraction.md). + +## Building one + +Every iterator is constructed from the images it reads and writes, then narrowed. A fresh +iterator covers the whole image as a single region: + +```python exec="true" source="material-block" session="iterators" +--8<-- "docs/snippets/getting_started/iterators.py:setup" +``` + +```python exec="true" source="material-block" session="iterators" +--8<-- "docs/snippets/getting_started/iterators.py:build" +``` + +`product` replaces that single region with the ones a ROI table names — here the +microscope fields of view: + +```python exec="true" source="material-block" session="iterators" +--8<-- "docs/snippets/getting_started/iterators.py:product" +``` + +The regions are ordinary [`Roi`][ngio.Roi] objects, so you can inspect them before +processing anything: + +```python exec="true" source="material-block" session="iterators" +--8<-- "docs/snippets/getting_started/iterators.py:inspect" +``` -Moreover, when working with OME-Zarr Images it is often useful to set specific broadcasting rules for the iteration, for example to iterate over all z-planes or iterate over all timepoints. +From here you would call `map_as_numpy` or iterate with `iter_as_numpy` to do the work; +the [image processing tutorial](../tutorials/image_processing.md) carries this through to +a written result. -Ngio provides a set of `Iterator` classes that can be used for this purpose. We provide iterators four basic iterators: +More complete examples can be found in the [Fractal tasks template](https://github.com/fractal-analytics-platform/fractal-tasks-template). -* The `SegmentationIterator` is designed to build segmentation pipelines, where an input image is processed to produce a segmentation mask. An example use case on how to use the `SegmentationIterator` can be found in the [Image Segmentation Tutorial](../tutorials/image_segmentation.ipynb). -* The `MaskedSegmentationIterator` is similar to the `SegmentationIterator`, but it uses a masking roi table to restrict the segmentation to masks. This is useful when you want to segment only specific regions of the image, for example, segmenting cells only within a specific tissue region. An example use case on how to use the `MaskedSegmentationIterator` can be found in the [Image Segmentation Tutorial](../tutorials/image_segmentation.ipynb). -* The `ImageProcessingIterator` is designed to build image processing pipelines, where an input image is processed to produce a new image. An example use case on how to use the `ImageProcessingIterator` can be found in the [Image Processing Tutorial](../tutorials/image_processing.ipynb). -* The `FeatureExtractionIterator` is read-only iterator designed to iterate over pairs of images and labels to extract features from the image based on the labels. An example use case on how to use the `FeatureExtractionIterator` can be found in the [Feature Extraction Tutorial](../tutorials/feature_extraction.ipynb). +## Next steps -A set of more complete example can be found in the [Fractal Tasks Template](https://github.com/fractal-analytics-platform/fractal-tasks-template). +- [Image processing tutorial](../tutorials/image_processing.md) — an iterator applied end to end. +- [Image segmentation tutorial](../tutorials/image_segmentation.md) — segmentation and masked segmentation. +- [Iterators API reference](../api/iterators.md) — the full iterator API. diff --git a/docs/getting_started/7_configuration.md b/docs/getting_started/7_configuration.md index c614c884..adb24442 100644 --- a/docs/getting_started/7_configuration.md +++ b/docs/getting_started/7_configuration.md @@ -1,10 +1,16 @@ -# Configuration +--- +description: Configure ngio via ngio_config.json, including the io_retry policy. +--- -Ngio has a small global configuration object that controls cross-cutting IO behavior. It is loaded once at import time from a JSON file and is accessible programmatically. +# 7. Configuration + +**Tune the cross-cutting IO behaviour.** + +ngio has a small global configuration object, read from a JSON file and also reachable programmatically. It is loaded once, during `import ngio`, and cached for the life of the process. ## The config file -By default ngio looks for `~/.ngio/ngio_config.json`. You can point it somewhere else with the `NGIO_CONFIG_PATH` environment variable. Only `.json` files are supported. A missing file simply means all defaults. +By default ngio looks for `~/.ngio/ngio_config.json`. You can point it somewhere else with the `NGIO_CONFIG_PATH` environment variable — set it **before** you import ngio, since the file is read during import. Only `.json` files are supported. A missing file means all defaults. ```json { @@ -19,7 +25,7 @@ By default ngio looks for `~/.ngio/ngio_config.json`. You can point it somewhere } ``` -You can also inspect or change the configuration at runtime: +`get_config()` returns that object — an `NgioConfig` — so you can inspect or change the configuration at runtime: ```python from ngio import get_config @@ -44,20 +50,25 @@ Fields: - `constant`: wait `delay_s` between retries. - `linear`: wait `delay_s * attempt`. - `exponential` (default): wait `delay_s * 2 ** (attempt - 1)`. - Delays are capped at `max_delay_s`; `jitter` multiplies the delay by a random factor in `[0.5, 1.5]`. + `jitter` multiplies the delay by a random factor in `[0.5, 1.5]`; the result is capped at `max_delay_s` both before and after jitter is applied. - `retry_on`: a list of substrings matched against `"ExceptionName: message"`. An error is retried only if at least one marker matches, so you can match either an exception class name (`"TimeoutError"`) or a message fragment (`"RequestTimeTooSkewed"`). - `retry_all_errors`: retry every error. This is **discouraged** — it also retries errors that will never succeed (permissions, missing keys, bugs), multiplying the time to failure. Enabling it emits an `NgioUserWarning`, and it is mutually exclusive with `retry_on`. Prefer narrowing `retry_on` to the specific transient errors you observe. -Ngio's own errors (`NgioError` subclasses, e.g. validation errors) are never retried, in any mode. +ngio's own errors (`NgioError` subclasses, e.g. validation errors) are never retried, in any mode. ### Semantics worth knowing - **Zarr IO snapshots the policy at open time.** Every group ngio opens is backed by a store that copies the current `io_retry` at construction. The snapshot travels with the store — including into pickled dask task graphs, so workers retry with the policy that was active on the driver. Changing `get_config().io_retry` afterwards does not affect already-open containers. - **Non-zarr IO reads the policy at call time.** The table backends and store probes check the current global config on every call, so runtime changes apply immediately there. -- Retries are logged as warnings on the `ngio` logger, including the error, attempt count, and sleep time. +- Retries are logged as warnings, including the error, attempt count, and sleep time. ngio names its loggers `ngio:` (here `ngio:ngio.utils._retry`) — note the colon, which means they are not children of a `ngio` logger in Python's dot-separated hierarchy, so attach handlers to the full name. ## s3fs retry markers (`s3fs`) `s3fs.custom_retry_markers` is a separate, lower-level mechanism: it registers an error handler inside `s3fs` itself, making s3fs's internal request loop retry any botocore error whose message contains one of the markers (the motivating case is AWS clock-skew `RequestTimeTooSkewed` errors). Apply changes at runtime with `ngio.utils.refresh_s3fs_config(get_config())`. The two mechanisms are complementary and independent: `s3fs` retries individual S3 requests inside a single ngio IO call, while `io_retry` retries the whole ngio IO call. If both are enabled and their triggers overlap, an error can be retried at both layers, so the effective number of attempts is multiplicative — keep the two configurations narrow. + +## Next steps + +- [Quickstart](0_quickstart.md) — if you have not opened a container yet. +- [Contributing](../contributing.md) — set up a development environment. diff --git a/docs/index.md b/docs/index.md index 2f5b7aa5..0a46b8d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,66 +1,150 @@ -ngio is a Python library designed to simplify bioimage analysis workflows, offering an intuitive interface for working with OME-Zarr files. +--- +description: ngio is a Python library for OME-Zarr bioimage analysis, with an object-based API for images, labels, tables, ROIs and HCS plates. +--- -## What is Ngio? +# ngio { .ngio-visually-hidden } -Ngio is built for the [OME-Zarr](https://ngff.openmicroscopy.org/) file format, a modern, cloud-optimized format for biological imaging data. OME-Zarr stores large, multi-dimensional microscopy images and metadata in an efficient and scalable way. +![](assets/logo-lockup.svg#only-light){ .ngio-hero-logo } +![](assets/logo-lockup-dark.svg#only-dark){ .ngio-hero-logo } -Ngio's mission is to streamline working with OME-Zarr files by providing a simple, object-based API for opening, exploring, and manipulating OME-Zarr images and high-content screening (HCS) plates. It also offers comprehensive support for labels, tables and regions of interest (ROIs), making it easy to extract and analyze specific regions in your data. +**Next generation file format IO — a Python library for OME-Zarr bioimage analysis.** -## Key Features +ngio is built for [OME-Zarr](https://ngff.openmicroscopy.org/), a cloud-optimised format +that stores large, multi-dimensional microscopy images and their metadata in an efficient, +scalable way. It provides an object-based API for opening, exploring and manipulating +OME-Zarr images and high-content screening (HCS) plates, along with labels, tables and +regions of interest (ROIs) for extracting and analysing specific regions of your data. -### 🔍 Simple Object-Based API +## Key features -- Easily open, explore, and manipulate OME-Zarr images and HCS plates -- Create and derive new images and labels with minimal boilerplate code +- **Object-based API** — open, explore and manipulate OME-Zarr images and HCS + plates; derive new images and labels with minimal boilerplate. +- **Tables and ROIs** — tight integration with [tabular + data](table_specs/overview.md), extensible table schemas, and measurements stored + alongside the image. +- **Scalable processing** — iterators for building pipelines that generalise from a + single ROI to a full plate, with a pluggable mapping mechanism for parallelisation. +- **Remote stores** — stream from S3 and other fsspec-backed sources, with a + [configurable IO retry policy](getting_started/7_configuration.md). +- **Supported OME-Zarr versions** — ngio supports OME-Zarr v0.4 and v0.5, backed by either Zarr v2 or v3 storage. Support for + v0.6 and later is planned. -### 📊 Rich Tables and Regions of Interest (ROI) Support +## Installation -- Tight integration with [tabular data](https://biovisioncenter.github.io/ngio/stable/table_specs/overview/) -- Extract and analyze specific regions of interest -- Store measurements and other metadata in the OME-Zarr container -- Extensible & modular allowing users to define custom table schemas and on disk serialization +To install ngio, use whichever package manager you already work with — it is published on +both PyPI and conda-forge. To install from source, see the +[quickstart](getting_started/0_quickstart.md). -### 🔄 Scalable Data Processing +=== "pip" -- Powerful iterators for building scalable and generalizable image processing pipelines -- Extensible mapping mechanism for custom parallelization strategies + ```bash + pip install ngio + ``` -## Getting Started +=== "uv" -Refer to the [Getting Started](getting_started/0_quickstart.md) guide to integrate ngio into your workflows. We also provide a collection of [Tutorials](tutorials/image_processing.ipynb) to help you get up and running quickly. -For more advanced usage and API documentation, see our [API Reference](api/ome_zarr_container.md). + Inside a uv project: -## Supported OME-Zarr versions + ```bash + uv add ngio + ``` -Ngio supports OME-Zarr v0.4 and v0.5, with Zarr v2 and v3 storage formats. + Or into an existing environment: -## Development Status + ```bash + uv pip install ngio + ``` -!!! warning - Ngio is under active development and is not yet stable. The API is subject to change, and bugs and breaking changes are expected. - We follow [Semantic Versioning](https://semver.org/). Which means for 0.x releases potentially breaking changes can be introduced in minor releases. +=== "pixi" -### Available Features + ```bash + pixi add ngio # from conda-forge + pixi add --pypi ngio # from PyPI + ``` -- ✅ OME-Zarr metadata handling and validation -- ✅ Image and label access across pyramid levels -- ✅ ROI and table support -- ✅ Image processing iterators -- ✅ Streaming from remote sources -- ✅ Documentation and examples +=== "mamba/conda" -### Upcoming Features + ```bash + mamba install -c conda-forge ngio + ``` -- Enhanced performance optimizations (parallel iterators, optimized io strategies) +## ngio in 30 seconds -## Contributors +Opening a container, inspecting it and slicing out a region of interest take a couple of +lines each. The example below uses a placeholder path; the +[quickstart](getting_started/0_quickstart.md) walks through the same steps on a dataset you +can download. -Ngio is developed at the [BioVisionCenter](https://www.biovisioncenter.uzh.ch/en.html), University of Zurich, by [@lorenzocerrone](https://github.com/lorenzocerrone) and [@jluethi](https://github.com/jluethi). +```python +from ngio import open_ome_zarr_container -## License +# Open a container and inspect what is inside +ome_zarr = open_ome_zarr_container("path/to/image.zarr") +print(ome_zarr) # levels, labels and tables at a glance -Ngio is released under the BSD-3-Clause License. See [LICENSE](https://github.com/BioVisionCenter/ngio/blob/main/LICENSE) for details. +# Grab the highest-resolution image and read a channel as numpy +image = ome_zarr.get_image() +data = image.get_as_numpy(channel_selection="DAPI") -## Repository +# Slice by a region of interest, in world coordinates +roi = ome_zarr.get_table("FOV_ROI_table").get("FOV_1") +patch = image.get_roi_as_numpy(roi) +``` -Visit our [GitHub repository](https://github.com/BioVisionCenter/ngio) for the latest code, issues, and contributions. +## Where to go next + +
+ +- :material-rocket-launch:{ .lg .middle } **Getting started** + + --- + + Install ngio and work through the core objects: containers, images and labels, + tables, masked images and HCS plates. + + [:octicons-arrow-right-24: Quickstart](getting_started/0_quickstart.md) + +- :material-school:{ .lg .middle } **Tutorials** + + --- + + End-to-end walkthroughs: create an OME-Zarr, process and segment images, extract + features, and explore a plate. For hands-on notebooks, see the + [ngio workshop](https://github.com/BioVisionCenter/ngio-workshop). + + [:octicons-arrow-right-24: Browse tutorials](tutorials/create_ome_zarr.md) + +- :material-table:{ .lg .middle } **Table specifications** + + --- + + The on-disk spec for ROI, masking ROI, feature, condition and generic tables, and + the backends that store them. + + [:octicons-arrow-right-24: Read the spec](table_specs/overview.md) + +- :material-api:{ .lg .middle } **API reference** + + --- + + Generated reference for every public class and function, with type annotations and + source links. + + [:octicons-arrow-right-24: Open the reference](api/ome_zarr_container.md) + +
+ +## Citing ngio + +If ngio contributes to work you publish, please cite it. See +[`CITATION.cff`](https://github.com/BioVisionCenter/ngio/blob/main/CITATION.cff) in the +repository for the current citation metadata. + +## Project + +ngio is developed at the [BioVisionCenter](https://www.biovisioncenter.uzh.ch/en.html), +University of Zurich, by [@lorenzocerrone](https://github.com/lorenzocerrone) and +[@jluethi](https://github.com/jluethi). It is released under the BSD-3-Clause +[licence](https://github.com/BioVisionCenter/ngio/blob/main/LICENSE), and developed in the +open on [GitHub](https://github.com/BioVisionCenter/ngio) — issues and contributions +welcome. diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 00000000..cdc4e207 --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,58 @@ +# ngio + +> ngio (next generation file format IO) is a Python library for bioimage analysis on +> OME-Zarr data. It provides an object-based API for opening and manipulating OME-Zarr +> images, labels, tables, regions of interest (ROIs) and high-content screening (HCS) +> plates. It supports OME-Zarr v0.4 and v0.5 over Zarr v2 and v3 storage, and is developed +> at the BioVisionCenter, University of Zurich, under the BSD-3-Clause licence. + +Install with `pip install ngio` or `mamba install -c conda-forge ngio`. The central object +is the OME-Zarr container, obtained with `open_ome_zarr_container(store)`; from it you +reach images and labels (`get_image`, `get_label`), tables (`get_table`), masked images +(`get_masked_image`) and, for plates, `open_ome_zarr_plate`. The worked examples in the +guides and tutorials are executed when the site is built, so the outputs shown are real. + +## Getting started + +- [Quickstart](https://biovisioncenter.github.io/ngio/stable/getting_started/0_quickstart/): Installation and opening your first OME-Zarr container. +- [OME-Zarr containers](https://biovisioncenter.github.io/ngio/stable/getting_started/1_ome_zarr_containers/): The container object, metadata inspection and modification, deriving and creating images, and remote stores. +- [Images and labels](https://biovisioncenter.github.io/ngio/stable/getting_started/2_images/): Selecting resolution levels by path or pixel size, reading as numpy or dask, slicing, writing with set_array and consolidate, and working with labels. +- [Tables](https://biovisioncenter.github.io/ngio/stable/getting_started/3_tables/): Listing and loading ROI, masking ROI and feature tables, and creating new tables in memory or on disk. +- [Masked images and labels](https://biovisioncenter.github.io/ngio/stable/getting_started/4_masked_images/): Label-indexed access to image regions, zoom_factor, and masked read/write operations. +- [HCS plates](https://biovisioncenter.github.io/ngio/stable/getting_started/5_hcs/): Plate structure, rows, columns and acquisitions, retrieving images, and creating plates. +- [Iterators](https://biovisioncenter.github.io/ngio/stable/getting_started/6_iterators/): The four iterators for building scalable processing pipelines. +- [Configuration](https://biovisioncenter.github.io/ngio/stable/getting_started/7_configuration/): The ngio config file and the io_retry policy. + +## Tutorials + +- [Create an OME-Zarr image](https://biovisioncenter.github.io/ngio/stable/tutorials/create_ome_zarr/): Convert a numpy array to OME-Zarr and add a ROI table. +- [Image processing](https://biovisioncenter.github.io/ngio/stable/tutorials/image_processing/): Gaussian blur applied eagerly, lazily with dask, and via an iterator. +- [Image segmentation](https://biovisioncenter.github.io/ngio/stable/tutorials/image_segmentation/): Otsu segmentation per field of view, and masked segmentation. +- [Feature extraction](https://biovisioncenter.github.io/ngio/stable/tutorials/feature_extraction/): regionprops features written back as a feature table. +- [HCS exploration](https://biovisioncenter.github.io/ngio/stable/tutorials/hcs_exploration/): Aggregating tables across a plate and creating an empty plate. + +## Table specifications + +- [Overview](https://biovisioncenter.github.io/ngio/stable/table_specs/overview/): The table architecture — backends, in-memory objects and type specs — and the on-disk group layout. +- [Table backends](https://biovisioncenter.github.io/ngio/stable/table_specs/backend/): The anndata, parquet, csv and json backends and their metadata. +- [ROI table](https://biovisioncenter.github.io/ngio/stable/table_specs/table_types/roi_table/): Required and optional columns for ROI tables. +- [Masking ROI table](https://biovisioncenter.github.io/ngio/stable/table_specs/table_types/masking_roi_table/): ROI tables indexed by label id. +- [Feature table](https://biovisioncenter.github.io/ngio/stable/table_specs/table_types/feature_table/): Measurement tables indexed by label id. +- [Condition table](https://biovisioncenter.github.io/ngio/stable/table_specs/table_types/condition_table/): Experimental condition metadata. +- [Generic table](https://biovisioncenter.github.io/ngio/stable/table_specs/table_types/generic_table/): Untyped fallback tables. +- [Add a custom table](https://biovisioncenter.github.io/ngio/stable/table_specs/table_types/custom_table/): Why generic tables are the extension point today. + +## API reference + +- [OmeZarrContainer](https://biovisioncenter.github.io/ngio/stable/api/ome_zarr_container/): open_ome_zarr_container, create_empty_ome_zarr, create_ome_zarr_from_array. +- [Images](https://biovisioncenter.github.io/ngio/stable/api/images/): Image and Label objects and their accessors. +- [HCS](https://biovisioncenter.github.io/ngio/stable/api/hcs/): open_ome_zarr_plate, OmeZarrPlate, open_ome_zarr_well, OmeZarrWell, create_empty_plate, create_empty_well. +- [Tables](https://biovisioncenter.github.io/ngio/stable/api/tables/): Table classes, the tables container and backends. +- [Iterators](https://biovisioncenter.github.io/ngio/stable/api/iterators/): ImageProcessingIterator, SegmentationIterator, MaskedSegmentationIterator, FeatureExtractorIterator — experimental, from ngio.experimental.iterators. +- [ngio top-level API](https://biovisioncenter.github.io/ngio/stable/api/ngio/ngio/): Roi, PixelSize, Dimensions, NgioConfig and other core types. + +## Optional + +- [Changelog](https://biovisioncenter.github.io/ngio/stable/changelog/): Release history. +- [Contributing](https://biovisioncenter.github.io/ngio/stable/contributing/): Development setup, tests and PR workflow. +- [Code of Conduct](https://biovisioncenter.github.io/ngio/stable/code_of_conduct/): Contributor Covenant v2.1. diff --git a/docs/snippets/_render.py b/docs/snippets/_render.py new file mode 100644 index 00000000..ce9bf62e --- /dev/null +++ b/docs/snippets/_render.py @@ -0,0 +1,376 @@ +"""Rendering helpers shared by every executed docs snippet. + +Figures and tables reach the site through this module: `figure_html` and `table_html` +build the markup, and the rcParams applied at import give every figure one house style. +Snippet scripts pull it in from their `plot_helpers` / `table_helpers` sections, which +the pages include hidden (`exec="true"` with no `source=`), so none of this plumbing is +reader-facing — but `docs/tutorials/*.md` do show their own plotting code, so keep what +is used there to `figure_html` alone and leave plain matplotlib in view. + +Nothing here prints, and that is load-bearing rather than a style choice. markdown-exec +does not redirect `sys.stdout`; it injects its own `print` into the globals of the code +block it executes. A `print` inside this module resolves to the builtin instead, so its +output would land on the build's terminal and the block would render as empty, silently: +the build still exits 0. Hence `print(figure_html(fig))` at every call site: the `print` +has to happen in the block. + +Zensical ignores `exclude_docs`, so this file is copied into the built site as a static +asset at `site/snippets/_render.py`, like the snippet scripts themselves. Harmless: a +`.py` does not become a page. +""" + +import math +from html import escape +from io import StringIO +from typing import TYPE_CHECKING, Any + +import matplotlib +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from matplotlib.colors import Colormap, ListedColormap +from matplotlib.figure import Figure +from matplotlib.image import AxesImage +from matplotlib.patches import Rectangle +from matplotlib.patheffects import withStroke + +if TYPE_CHECKING: + import pandas as pd + + from ngio import PixelSize, Roi + +matplotlib.use("Agg") + +# Every colour below is the light-scheme value of the matching custom property in +# docs/stylesheets/ngio.css, and doubles as the sentinel `print_figure` swaps for that +# property. A figure therefore degrades to the documented light-mode colour if the swap +# ever stops firing, rather than to black. +INK = "#5b6569" # --md-default-fg-color--light: titles, ticks, spines +BLUE = "#2e6fd6" # --ngio-blue: image data +GREEN = "#4cae4f" # --ngio-green: labels +MAGENTA = "#c2185b" # --ngio-magenta: tables and ROIs +ACCENT = "#22a699" # --ngio-accent: ngio itself + +# Scale bars are drawn over pixels, so they are fixed white with a dark halo: they have +# to survive any pixel value, not any page colour. Deliberately not a token, and +# deliberately absent from _THEME_VARS below. +ON_IMAGE = "#ffffff" + +_THEME_VARS = { + INK: "var(--md-default-fg-color--light)", + BLUE: "var(--ngio-blue)", + GREEN: "var(--ngio-green)", + MAGENTA: "var(--ngio-magenta)", + ACCENT: "var(--ngio-accent)", +} + +# The house style. Applied once at import, so it reaches figures built by reader-facing +# code that calls nothing from this module. +# +# `figure.figsize` is a starting point, not a house rule. Figures carry their intrinsic +# size into the page (the stylesheet only caps them at the column width) and +# `savefig.bbox: "tight"` trims the canvas back to the panels, so a figure showing a +# whole image should be sized to fill the ~8.6in content column — otherwise it lands at +# whatever width its aspect ratio leaves. A single object cropped to its ROI wants the +# opposite: small, because there is nothing there to enlarge. +# +# `svg.fonttype: "none"` keeps labels as `` rather than outlined paths, so the +# inline SVG resolves the site's own webfont and figure labels match the hand-authored +# diagrams. Text metrics still come from the local fallback, so nothing here may depend +# on exact text width — titles stay single-line and left-aligned, and the scale-bar +# label is anchored at its left edge. +HOUSE_STYLE: dict[str, Any] = { + "figure.figsize": (8.0, 4.0), + "figure.facecolor": "none", + "axes.facecolor": "none", + "savefig.transparent": True, + "savefig.bbox": "tight", + "savefig.pad_inches": 0.02, + "svg.fonttype": "none", + "font.family": "monospace", + "font.monospace": ["JetBrains Mono", "DejaVu Sans Mono"], + "font.size": 8.5, + "axes.titlesize": 8.5, + "axes.titlelocation": "left", + "axes.titlecolor": INK, + # "medium" would print a `findfont: Failed to find font weight` line on every build. + "axes.titleweight": "normal", + "axes.titlepad": 5.0, + "axes.edgecolor": INK, + "axes.labelcolor": INK, + "text.color": INK, + "xtick.color": INK, + "ytick.color": INK, + "xtick.labelcolor": INK, + "ytick.labelcolor": INK, + "xtick.labelsize": 8.0, + "ytick.labelsize": 8.0, + "legend.frameon": False, + "legend.fontsize": 8.0, + "lines.linewidth": 1.4, +} + +plt.style.use(HOUSE_STYLE) + + +def random_label_cmap(n_labels: int = 1000, seed: int = 0) -> ListedColormap: + """Build a reproducible random colormap for label images.""" + rng = np.random.default_rng(seed) + colors = rng.random((n_labels, 3)) + colors[0] = 0.0 + return ListedColormap(colors) + + +def stretch_limits( + data: np.ndarray, + percentiles: tuple[float, float] = (1.0, 99.8), + ignore_zeros: bool = False, +) -> tuple[float, float]: + """Return the display window for an intensity image. + + Microscopy data rarely fills its dtype — a uint16 MIP shown on the full 0-65535 + range is nearly black — so figures window on percentiles instead. + + Args: + data: The intensity array. + percentiles: Lower and upper percentile bounding the window. + ignore_zeros: Compute the window over non-zero values only. For masked data, + where the zeros outside the mask would otherwise dominate. + + Returns: + The `(vmin, vmax)` pair, widened to a unit range if the data is constant. + """ + values = np.asarray(data) + if ignore_zeros: + non_zero = values[values > 0] + if non_zero.size: + values = non_zero + vmin, vmax = np.percentile(values, percentiles) + if vmax <= vmin: + vmax = vmin + 1.0 + return float(vmin), float(vmax) + + +def show_image( + ax: Axes, + data: np.ndarray, + *, + title: str | None = None, + cmap: str | Colormap = "gray", + limits: tuple[float, float] | None = None, + percentiles: tuple[float, float] = (1.0, 99.8), + ignore_zeros: bool = False, + alpha: float | None = None, + mask_zeros: bool = False, + pixel_size: "PixelSize | None" = None, +) -> AxesImage: + """Draw one image panel in the house style. + + Args: + ax: The axes to draw on. + data: The array to show; squeezed first, so singleton `c`/`z`/`t` axes are fine. + title: Panel title. + cmap: A colormap name for intensity data, or a `Colormap` for labels — a + `Colormap` also turns off the intensity window and any interpolation. + limits: An explicit `(vmin, vmax)`, bypassing `stretch_limits`. Pass the same + pair to both panels of a before/after figure: two independently stretched + panels would misrepresent the change between them. + percentiles: Forwarded to `stretch_limits`. + ignore_zeros: Forwarded to `stretch_limits`. + alpha: Opacity, for drawing an overlay over an earlier panel. + mask_zeros: Hide zero-valued pixels. For a label overlay, so the image below + shows through at full contrast instead of being dimmed by the background. + pixel_size: Draw a scale bar from this pixel size. + + Returns: + The `AxesImage`, so a caller can add a colorbar or overlay another array. + """ + array = np.squeeze(np.asarray(data)) + if mask_zeros: + array = np.ma.masked_where(array == 0, array) + + kwargs: dict[str, Any] = {"cmap": cmap} + if isinstance(cmap, str): + vmin, vmax = ( + limits + if limits is not None + else stretch_limits(array, percentiles, ignore_zeros) + ) + kwargs["vmin"], kwargs["vmax"] = vmin, vmax + else: + # A label colormap indexes into its own colours: windowing would remap the ids, + # and smoothing would blend them into colours no object has. + kwargs["interpolation"] = "nearest" + if alpha is not None: + kwargs["alpha"] = alpha + + mappable = ax.imshow(array, **kwargs) + if title is not None: + ax.set_title(title) + ax.axis("off") + if pixel_size is not None: + add_scale_bar(ax, pixel_size) + return mappable + + +def _nice_length(target: float) -> float: + """Round a length to the nearest 1, 2 or 5 per decade.""" + if target <= 0: + return 1.0 + candidates = [m * 10.0**e for e in range(-4, 8) for m in (1.0, 2.0, 5.0)] + return min(candidates, key=lambda c: abs(math.log10(c / target))) + + +def add_scale_bar(ax: Axes, pixel_size: "PixelSize", *, fraction: float = 0.22) -> None: + """Draw a scale bar in the lower right of an image panel. + + The bar length is the round number nearest `fraction` of the panel width, so it + stays legible whatever the crop. Both artists are placed in axes coordinates: the + y axis of an `imshow` is inverted, and axes fractions are not. + + Args: + ax: The axes holding the image. + pixel_size: The pixel size the image was read at. + fraction: Target bar length as a fraction of the panel width. + """ + x_low, x_high = sorted(ax.get_xlim()) + width_px = abs(x_high - x_low) + if not width_px: + return + + length = _nice_length(width_px * fraction * pixel_size.x) + length_frac = min(length / pixel_size.x / width_px, 0.8) + + unit = pixel_size.space_unit + symbol = "µm" if unit == "micrometer" else str(unit) if unit else "px" + halo = [withStroke(linewidth=1.6, foreground="#000000")] + + right, bottom = 0.96, 0.055 + ax.add_patch( + Rectangle( + (right - length_frac, bottom), + length_frac, + 0.018, + transform=ax.transAxes, + facecolor=ON_IMAGE, + edgecolor="none", + path_effects=halo, + zorder=5, + ) + ) + ax.text( + right - length_frac, + bottom + 0.055, + f"{length:g} {symbol}", + transform=ax.transAxes, + ha="left", + va="bottom", + fontsize=7.5, + color=ON_IMAGE, + path_effects=halo, + zorder=6, + ) + + +def add_roi_rectangle( + ax: Axes, + roi: "Roi", + pixel_size: "PixelSize", + *, + color: str = MAGENTA, + lw: float = 1.6, +) -> Rectangle: + """Outline a ROI on an image panel. + + Takes the ROI in world coordinates and converts it, so the caller does not repeat + the `to_pixel` arithmetic. + + Args: + ax: The axes holding the image. + roi: The ROI, in world coordinates. + pixel_size: The pixel size the image was read at. + color: Outline colour. Magenta, the docs' semantic colour for tables and ROIs. + lw: Outline width. + + Returns: + The rectangle that was added. + + Raises: + ValueError: If the ROI is unbounded in x or y, and so has no rectangle to draw. + """ + pixel_roi = roi.to_pixel(pixel_size=pixel_size) + x_slice = pixel_roi.get("x") + y_slice = pixel_roi.get("y") + # A `RoiSlice` bound may be None, meaning "to the edge of the image". Nothing here + # knows where that edge is, so refuse rather than guess. + if x_slice is None or y_slice is None: + raise ValueError(f"ROI {roi.name!r} has no x/y extent to outline.") + x_start, y_start = x_slice.start, y_slice.start + width, height = x_slice.length, y_slice.length + if x_start is None or y_start is None or width is None or height is None: + raise ValueError(f"ROI {roi.name!r} is unbounded in x or y.") + rectangle = Rectangle( + (x_start, y_start), + width, + height, + edgecolor=color, + facecolor="none", + lw=lw, + ) + ax.add_patch(rectangle) + return rectangle + + +def figure_html(fig: Figure, alt: str | None = None) -> str: + """Render a figure as inline SVG, for markdown-exec `html` blocks. + + Swaps the brand colours the figure was drawn with for the matching theme variables, + so the figure follows the light/dark toggle rather than baking one scheme into the + page. This only works because the SVG is inline: an `` would be a separate + document and would not see the site's custom properties. Colours inside a raster — + a label overlay, the pixels themselves — are baked into a base64 PNG and cannot + follow the toggle, which is why greyscale is the rule for pixel data. + + The `.ngio-figure` wrapper is what the stylesheet keys on to strip the `OUT` + terminal-output treatment that `.result` applies by default. + + Args: + fig: The figure to render. Closed before returning. + alt: A short description of what the figure shows, for screen readers. + + Returns: + The markup to print from the code block. + """ + buffer = StringIO() + fig.savefig(buffer, format="svg") + plt.close(fig) + + # Drop matplotlib's XML declaration and DOCTYPE — they are meaningless inside an + # HTML body, and the figure is being embedded, not served. + svg = buffer.getvalue() + svg = svg[svg.index("{svg}' + + +def table_html(df: "pd.DataFrame") -> str: + """Render a DataFrame as HTML that the docs theme will style. + + Markdown is not an option here: Zensical does not run block-level Markdown over + markdown-exec output, so a pipe table would stay literal text. The theme styles + only `table:not([class])` — and its JS only wraps such tables in a horizontal + scroll container — while pandas tags its output `class="dataframe"`, so the class + and the presentational border are stripped. + + Returns: + The markup to print from the code block. + """ + # A named index (here the label id) is real data, so promote it to a column: pandas + # otherwise renders it as a second, near-empty header row. + if df.index.name is not None: + df = df.reset_index() + html = df.to_html(index=False, border=0, float_format="{:.2f}".format) + return html.replace(' class="dataframe"', "") diff --git a/docs/snippets/getting_started/get_started.py b/docs/snippets/getting_started/get_started.py new file mode 100644 index 00000000..0e16705e --- /dev/null +++ b/docs/snippets/getting_started/get_started.py @@ -0,0 +1,487 @@ +"""Snippets for docs/getting_started/1_ome_zarr_containers.md, 2_images.md, 3_tables.md. + +These three pages share the `get_started` markdown-exec session, so they share this +one script and it stays runnable on its own: + + python docs/snippets/getting_started/get_started.py + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into a page by `pymdownx.snippets` and executed by `markdown-exec`. Sections follow +page order, and several of them rebind names used by later sections, so the order +here is load-bearing. +""" + +# --8<-- [start:plot_helpers] +import sys + +# markdown-exec execs this block, so `__file__` does not exist; a standalone run puts +# only this script's own directory on sys.path. Both run from the repo root, which is +# what the rest of the snippets already assume (`Path("./data")`), so resolve the +# shared module against that. +sys.path.append("docs/snippets") + +from matplotlib import pyplot as plt + +from _render import add_roi_rectangle, figure_html, random_label_cmap, show_image +# --8<-- [end:plot_helpers] + +# --8<-- [start:table_helpers] +import sys + +sys.path.append("docs/snippets") + +from _render import table_html +# --8<-- [end:table_helpers] + +# --------------------------------------------------------------------------- +# 1. OME-Zarr Container +# --------------------------------------------------------------------------- + +# --8<-- [start:setup] +from pathlib import Path + +from ngio import open_ome_zarr_container +from ngio.utils import download_ome_zarr_dataset + +# Download a sample dataset +download_dir = Path("./data").absolute() +hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) +image_path = hcs_path / "B" / "03" / "0" + +# Open the OME-Zarr container +ome_zarr_container = open_ome_zarr_container(image_path) +# --8<-- [end:setup] + +# Zensical gives every page its own markdown-exec session, so a page cannot see state +# bound by an earlier page. The two sections below are included (hidden, no `source=`) +# at the top of 2_images.md and 3_tables.md to re-bind what those pages need. They +# print nothing, so they render as empty. `re_unzip=False` reuses the already-extracted +# store rather than re-extracting it, which would race with the other pages. + +# --8<-- [start:reopen_container] +from pathlib import Path + +from ngio import open_ome_zarr_container +from ngio.utils import download_ome_zarr_dataset + +download_dir = Path("./data").absolute() +hcs_path = download_ome_zarr_dataset( + "CardiomyocyteSmallMip", download_dir=download_dir, re_unzip=False +) +ome_zarr_container = open_ome_zarr_container(hcs_path / "B" / "03" / "0") +# --8<-- [end:reopen_container] + +# --8<-- [start:reopen_image] +from ngio import PixelSize + +image = ome_zarr_container.get_image( + pixel_size=PixelSize(x=0.60, y=0.60, z=1.0), strict=False +) +# --8<-- [end:reopen_image] + +# --8<-- [start:print_container] +print(ome_zarr_container) +# --8<-- [end:print_container] + +# --8<-- [start:levels] +print(ome_zarr_container.levels) # Show the number of resolution levels +# --8<-- [end:levels] + +# --8<-- [start:level_paths] +print(ome_zarr_container.level_paths) # Show the paths to all available images +# --8<-- [end:level_paths] + +# --8<-- [start:is_3d] +print(ome_zarr_container.is_3d) # Get if the image is 3D +# --8<-- [end:is_3d] + +# --8<-- [start:is_time_series] +print(ome_zarr_container.is_time_series) # Get if the image is a time series +# --8<-- [end:is_time_series] + +# --8<-- [start:metadata] +metadata = ome_zarr_container.meta +print(metadata) +# --8<-- [end:metadata] + +# --8<-- [start:channel_labels] +print(metadata.channels_meta.channel_labels) +# --8<-- [end:channel_labels] + +# --8<-- [start:plot_container_channels] +image_3 = ome_zarr_container.get_image(path="3") + +fig, axs = plt.subplots(1, 3, figsize=(8.6, 3.4)) +# Each channel is windowed on its own percentiles: the three stains have unrelated +# intensity ranges, and one shared window would render the dimmest as an empty panel. +# One scale bar is enough — the three panels are the same image at the same level. +for ax, channel_label in zip(axs, image_3.channel_labels, strict=True): + show_image( + ax, + image_3.get_as_numpy(channel_selection=channel_label), + title=channel_label, + pixel_size=image_3.pixel_size if ax is axs[0] else None, + ) +fig.tight_layout() +print(figure_html(fig, alt="The three channels of the container, side by side.")) +# --8<-- [end:plot_container_channels] + +# --------------------------------------------------------------------------- +# 2. Images and Labels +# --------------------------------------------------------------------------- + +# --8<-- [start:get_image_default] +print(ome_zarr_container.get_image()) # Get the highest resolution image +# --8<-- [end:get_image_default] + +# --8<-- [start:get_image_by_path] +print(ome_zarr_container.get_image(path="1")) # Get a specific pyramid level +# --8<-- [end:get_image_by_path] + +# --8<-- [start:get_image_by_pixel_size] +from ngio import PixelSize + +pixel_size = PixelSize(x=0.65, y=0.65, z=1.0) +image = ome_zarr_container.get_image(pixel_size=pixel_size) +print(image) +# --8<-- [end:get_image_by_pixel_size] + +# --8<-- [start:get_image_nearest] +from ngio import PixelSize + +pixel_size = PixelSize(x=0.60, y=0.60, z=1.0) +image = ome_zarr_container.get_image(pixel_size=pixel_size, strict=False) +print(image) +# --8<-- [end:get_image_nearest] + +# --8<-- [start:image_dimensions] +print(image.dimensions) +# --8<-- [end:image_dimensions] + +# --8<-- [start:image_pixel_size] +print(image.pixel_size) +# --8<-- [end:image_pixel_size] + +# --8<-- [start:image_array_info] +print(image.shape, image.dtype, image.chunks, image.axes) +# --8<-- [end:image_array_info] + +# --8<-- [start:image_as_numpy] +data = image.get_as_numpy() # Get the image as a numpy array +print(data.shape, data.dtype) +# --8<-- [end:image_as_numpy] + +# --8<-- [start:image_as_dask] +dask_array = image.get_as_dask() # Get the image as a dask array +print(dask_array) +# --8<-- [end:image_as_dask] + +# --8<-- [start:image_get_array_legacy] +# One entry point for both, selected with mode="numpy" or mode="dask" +data = image.get_array(mode="numpy") +print(data.shape, data.dtype) +# --8<-- [end:image_get_array_legacy] + +# --8<-- [start:image_slice] +# Get a specific channel and axes order +image_slice = image.get_as_numpy( + channel_selection="DAPI", + x=slice(0, 128), + axes_order=["t", "z", "y", "x", "c"], +) +print(image_slice.shape) +# --8<-- [end:image_slice] + +# --8<-- [start:set_array_example] +import numpy as np + + +def process(patch: np.ndarray) -> np.ndarray: + """Placeholder for your own processing step. + + Replace the body with the operation you want to apply to the patch. + """ + return patch + + +# Get the image data as a numpy array +data = image.get_as_numpy( + channel_selection="DAPI", + x=slice(0, 128), + y=slice(0, 128), + axes_order=["z", "y", "x", "c"], +) + +# Modify the image data +data = process(data) + +# Set the modified image data +image.set_array( + data, + channel_selection="DAPI", + x=slice(0, 128), + y=slice(0, 128), + axes_order=["z", "y", "x", "c"], +) + +# Consolidate the changes to all resolution levels, see below for more details +image.consolidate() +# --8<-- [end:set_array_example] + +# --8<-- [start:roi_slicing] +from ngio import Roi + +# Define a ROI in world coordinates +roi = Roi.from_values(slices={"x": (34.1, 321.6), "y": (10, 330)}, name=None) +# Get the image data in the ROI as a numpy array +print(image.get_roi_as_numpy(roi).shape) +# --8<-- [end:roi_slicing] + +# --8<-- [start:plot_roi_slicing] +image_3 = ome_zarr_container.get_image(path="3") + +fig, axs = plt.subplots(1, 2, figsize=(8, 4.1)) +show_image( + axs[0], + image_3.get_as_numpy(c=0), + title="Whole image", + pixel_size=image_3.pixel_size, +) +add_roi_rectangle(axs[0], roi, image_3.pixel_size) +show_image( + axs[1], + image_3.get_roi_as_numpy(roi, c=0), + title="The ROI", + pixel_size=image_3.pixel_size, +) +fig.tight_layout() +print( + figure_html( + fig, alt="The ROI outlined on the whole image, and the region it returns." + ) +) +# --8<-- [end:plot_roi_slicing] + +# --8<-- [start:list_labels] +print(ome_zarr_container.list_labels()) # Available labels +# --8<-- [end:list_labels] + +# --8<-- [start:get_label_default] +# Get the highest resolution label +print(ome_zarr_container.get_label("nuclei")) +# --8<-- [end:get_label_default] + +# --8<-- [start:get_label_by_path] +# Get a specific pyramid level +print(ome_zarr_container.get_label("nuclei", path="1")) +# --8<-- [end:get_label_by_path] + +# --8<-- [start:get_label_by_pixel_size] +from ngio import PixelSize + +pixel_size = PixelSize(x=0.65, y=0.65, z=1.0) +label_nuclei = ome_zarr_container.get_label("nuclei", pixel_size=pixel_size) +print(label_nuclei) +# --8<-- [end:get_label_by_pixel_size] + +# --8<-- [start:get_label_nearest] +from ngio import PixelSize + +pixel_size = PixelSize(x=0.60, y=0.60, z=1.0) +label_nuclei = ome_zarr_container.get_label( + "nuclei", pixel_size=pixel_size, strict=False +) +print(label_nuclei) +# --8<-- [end:get_label_nearest] + +# --8<-- [start:plot_label_overlay] +image_3 = ome_zarr_container.get_image(path="3") +label_3 = ome_zarr_container.get_label( + "nuclei", pixel_size=image_3.pixel_size, strict=False +) + +fig, ax = plt.subplots(figsize=(5.5, 5.5)) +show_image( + ax, + image_3.get_as_numpy(c=0), + title="nuclei over DAPI", + pixel_size=image_3.pixel_size, +) +# `mask_zeros` drops the label background, so the image below stays at full contrast +# instead of being dimmed by a semi-transparent black. +show_image( + ax, + label_3.get_as_numpy(), + cmap=random_label_cmap(), + alpha=0.6, + mask_zeros=True, +) +fig.tight_layout() +print( + figure_html( + fig, alt="The nuclei label, coloured by object id, over the DAPI channel." + ) +) +# --8<-- [end:plot_label_overlay] + +# --8<-- [start:derive_label] +# Derive a new label +new_label = ome_zarr_container.derive_label("new_label", overwrite=True) +print(new_label) +# --8<-- [end:derive_label] + +# --------------------------------------------------------------------------- +# 3. Tables +# --------------------------------------------------------------------------- + +# --8<-- [start:list_tables] +# List all available tables +print(ome_zarr_container.list_tables()) +# --8<-- [end:list_tables] + +# --8<-- [start:roi_table_get] +roi_table = ome_zarr_container.get_table("FOV_ROI_table") # Get a ROI table +print(roi_table.get("FOV_1")) +# --8<-- [end:roi_table_get] + +# --8<-- [start:plot_fov_roi_on_image] +image_3 = ome_zarr_container.get_image(path="3") + +fig, ax = plt.subplots(figsize=(6.5, 6.5)) +show_image( + ax, + image_3.get_as_numpy(c=0), + title="FOV_1 ROI", + pixel_size=image_3.pixel_size, +) +add_roi_rectangle(ax, roi_table.get("FOV_1"), image_3.pixel_size) +fig.tight_layout() +print(figure_html(fig, alt="One field of view outlined on the whole well image.")) +# --8<-- [end:plot_fov_roi_on_image] + +# --8<-- [start:roi_table_slice_image] +roi = roi_table.get("FOV_1") +roi_data = image.get_roi_as_numpy(roi) +print(roi_data.shape) +# --8<-- [end:roi_table_slice_image] + +# --8<-- [start:plot_fov_roi_crop] +roi = roi_table.get("FOV_1") +image_3 = ome_zarr_container.get_image(path="3") + +fig, ax = plt.subplots(figsize=(5.5, 5.5)) +show_image( + ax, + image_3.get_roi_as_numpy(roi, c=0), + title="FOV_1 ROI", + pixel_size=image_3.pixel_size, +) +fig.tight_layout() +print(figure_html(fig, alt="The pixels of one field of view, read through its ROI.")) +# --8<-- [end:plot_fov_roi_crop] + +# --8<-- [start:masking_table_get] +# Get a mask table +masking_table = ome_zarr_container.get_table("nuclei_ROI_table") +print(masking_table.get_label(100)) +# --8<-- [end:masking_table_get] + +# --8<-- [start:masking_table_slice_image] +roi = masking_table.get_label(100) +roi_data = image.get_roi_as_numpy(roi) +print(roi_data.shape) +# --8<-- [end:masking_table_slice_image] + +# --8<-- [start:plot_masking_roi_crop] +roi = masking_table.get_label(100) +image_2 = ome_zarr_container.get_image(path="2") +label_2 = ome_zarr_container.get_label("nuclei", pixel_size=image_2.pixel_size) + +fig, ax = plt.subplots(figsize=(4.5, 4.5)) +show_image( + ax, + image_2.get_roi_as_numpy(roi, c=0), + title="Label 100 ROI", + pixel_size=image_2.pixel_size, +) +show_image( + ax, + label_2.get_roi_as_numpy(roi), + cmap=random_label_cmap(), + alpha=0.6, + mask_zeros=True, +) +fig.tight_layout() +print( + figure_html( + fig, alt="One nucleus, cropped to its masking ROI, with its label on top." + ) +) +# --8<-- [end:plot_masking_roi_crop] + +# --8<-- [start:feature_table] +# Get a feature table +feature_table = ome_zarr_container.get_table("regionprops_DAPI") +# only show the first 5 rows +print(table_html(feature_table.dataframe.head(5))) +# --8<-- [end:feature_table] + +# --8<-- [start:create_roi_table] +from ngio import Roi +from ngio.tables import RoiTable + +roi = Roi.from_values(slices={"x": (0, 128), "y": (0, 128)}, name="FOV_1") +roi_table = RoiTable(rois=[roi]) +print(roi_table) +# --8<-- [end:create_roi_table] + +# --8<-- [start:build_image_roi_table] +roi_table = ome_zarr_container.build_image_roi_table("whole_image") +print(roi_table) +# --8<-- [end:build_image_roi_table] + +# --8<-- [start:add_roi_table] +ome_zarr_container.add_table("new_roi_table", roi_table, overwrite=True) +roi_table = ome_zarr_container.get_table("new_roi_table") +print(roi_table) +# --8<-- [end:add_roi_table] + +# --8<-- [start:build_masking_roi_table] +masking_table = ome_zarr_container.build_masking_roi_table("nuclei") +print(masking_table) +# --8<-- [end:build_masking_roi_table] + +# --8<-- [start:create_feature_table] +import pandas as pd + +from ngio.tables import FeatureTable + +example_data = pd.DataFrame({"label": [1, 2, 3], "area": [100, 200, 300]}) +feature_table = FeatureTable(table_data=example_data) +print(feature_table) +# --8<-- [end:create_feature_table] + +# --8<-- [start:create_generic_table] +import pandas as pd + +from ngio.tables import GenericTable + +example_data = pd.DataFrame({"area": [100, 200, 300], "perimeter": [50, 60, 70]}) +generic_table = GenericTable(table_data=example_data) +print(generic_table) +# --8<-- [end:create_generic_table] + +# --8<-- [start:generic_table_from_anndata] +import anndata as ad +import numpy as np +import pandas as pd + +from ngio.tables import GenericTable + +adata = ad.AnnData( + X=np.random.rand(10, 5), + obs=pd.DataFrame({"cell_type": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]}), +) +generic_table = GenericTable(table_data=adata) +print(generic_table) +# --8<-- [end:generic_table_from_anndata] diff --git a/docs/snippets/getting_started/hcs.py b/docs/snippets/getting_started/hcs.py new file mode 100644 index 00000000..04f0ed7d --- /dev/null +++ b/docs/snippets/getting_started/hcs.py @@ -0,0 +1,103 @@ +"""Snippets for docs/getting_started/5_hcs.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/getting_started/hcs.py +""" + +# --8<-- [start:setup] +from pathlib import Path + +from ngio import open_ome_zarr_plate +from ngio.utils import download_ome_zarr_dataset + +download_dir = Path("./data").absolute() +hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) +ome_zarr_plate = open_ome_zarr_plate(hcs_path) +print(ome_zarr_plate) +# --8<-- [end:setup] + +# --8<-- [start:plate_columns] +print(ome_zarr_plate.columns) +# --8<-- [end:plate_columns] + +# --8<-- [start:plate_rows] +print(ome_zarr_plate.rows) +# --8<-- [end:plate_rows] + +# --8<-- [start:plate_acquisitions] +print(ome_zarr_plate.acquisition_ids) +# --8<-- [end:plate_acquisitions] + +# --8<-- [start:images_paths] +print(ome_zarr_plate.images_paths()) +# --8<-- [end:images_paths] + +# --8<-- [start:wells_paths] +print(ome_zarr_plate.wells_paths()) +# --8<-- [end:wells_paths] + +# --8<-- [start:well_images_paths] +print(ome_zarr_plate.well_images_paths(row="B", column=3)) +# --8<-- [end:well_images_paths] + +# --8<-- [start:get_images] +print(ome_zarr_plate.get_images()) +# --8<-- [end:get_images] + +# --8<-- [start:get_well_images] +well_images = ome_zarr_plate.get_well_images(row="B", column=3) +print(well_images) +# --8<-- [end:get_well_images] + +# --8<-- [start:get_image] +print(ome_zarr_plate.get_image(row="B", column=3, image_path="0")) +# --8<-- [end:get_image] + +# --8<-- [start:get_well_images_by_acquisition] +well_images = ome_zarr_plate.get_well_images(row="B", column=3, acquisition=0) +print(well_images) +# --8<-- [end:get_well_images_by_acquisition] + +# --8<-- [start:image_in_well_paths] +from ngio import ImageInWellPath + +list_of_images = [ + ImageInWellPath(path="0", row="A", column=0), + ImageInWellPath(path="0", row="B", column=1), + ImageInWellPath(path="0", row="C", column=1), + ImageInWellPath( + path="1", + row="A", + column=0, + acquisition_id=1, + acquisition_name="acquisition_1", + ), +] +# --8<-- [end:image_in_well_paths] + +# --8<-- [start:create_empty_plate] +from ngio import create_empty_plate + +plate = create_empty_plate( + store="data/new_plate.zarr", + name="test_plate", + images=list_of_images, + overwrite=True, +) +print(plate) +# --8<-- [end:create_empty_plate] + +# --8<-- [start:plate_add_image] +print(f"Before adding images: {plate.rows} rows, {plate.columns} columns") +plate.add_image(row="D", column=0, image_path="0") +print(f"After adding images: {plate.rows} rows, {plate.columns} columns") +# --8<-- [end:plate_add_image] + +# --8<-- [start:plate_remove_image] +print(f"Before removing images: {plate.wells_paths()} wells") +plate.remove_image(row="D", column=0, image_path="0") +print(f"After removing images: {plate.wells_paths()} wells") +# --8<-- [end:plate_remove_image] diff --git a/docs/snippets/getting_started/iterators.py b/docs/snippets/getting_started/iterators.py new file mode 100644 index 00000000..4b5a9d66 --- /dev/null +++ b/docs/snippets/getting_started/iterators.py @@ -0,0 +1,41 @@ +"""Snippets for docs/getting_started/6_iterators.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/getting_started/iterators.py +""" + +# --8<-- [start:setup] +from pathlib import Path + +from ngio import open_ome_zarr_container +from ngio.experimental.iterators import ImageProcessingIterator +from ngio.utils import download_ome_zarr_dataset + +download_dir = Path("./data").absolute() +hcs_path = download_ome_zarr_dataset( + "CardiomyocyteSmallMip", download_dir=download_dir, re_unzip=False +) +ome_zarr = open_ome_zarr_container(hcs_path / "B" / "03" / "0") +image = ome_zarr.get_image() +# --8<-- [end:setup] + +# --8<-- [start:build] +# A new iterator covers the whole image as a single region +iterator = ImageProcessingIterator(input_image=image, output_image=image) +print(iterator) +# --8<-- [end:build] + +# --8<-- [start:product] +# Narrow it to the regions named by a ROI table +iterator = iterator.product(ome_zarr.get_roi_table("FOV_ROI_table")) +print(iterator) +# --8<-- [end:product] + +# --8<-- [start:inspect] +# The regions are plain Roi objects, so you can look before you process +for roi in iterator.rois[:2]: + print(roi) +# --8<-- [end:inspect] diff --git a/docs/snippets/getting_started/masked_images.py b/docs/snippets/getting_started/masked_images.py new file mode 100644 index 00000000..c48120b5 --- /dev/null +++ b/docs/snippets/getting_started/masked_images.py @@ -0,0 +1,129 @@ +"""Snippets for docs/getting_started/4_masked_images.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/getting_started/masked_images.py +""" + +# --8<-- [start:plot_helpers] +import sys + +# markdown-exec execs this block, so `__file__` does not exist; a standalone run puts +# only this script's own directory on sys.path. Both run from the repo root, which is +# what the rest of the snippets already assume (`Path("./data")`), so resolve the +# shared module against that. +sys.path.append("docs/snippets") + +from matplotlib import pyplot as plt + +from _render import figure_html, show_image +# --8<-- [end:plot_helpers] + +# --8<-- [start:setup] +from pathlib import Path + +from ngio import open_ome_zarr_container +from ngio.utils import download_ome_zarr_dataset + +# Download a sample dataset +download_dir = Path("./data").absolute() +hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) +image_path = hcs_path / "B" / "03" / "0" + +# Open the OME-Zarr container +ome_zarr_container = open_ome_zarr_container(image_path) +# --8<-- [end:setup] + +# --8<-- [start:get_masked_image] +masked_image = ome_zarr_container.get_masked_image("nuclei") +print(masked_image) +# --8<-- [end:get_masked_image] + +# --8<-- [start:masked_roi_numpy] +roi_data = masked_image.get_roi_as_numpy(label=1009, c=0) +print(roi_data.shape) +# --8<-- [end:masked_roi_numpy] + +# --8<-- [start:plot_masked_roi] +fig, ax = plt.subplots(figsize=(4.5, 4.5)) +show_image( + ax, + masked_image.get_roi_as_numpy(label=1009, c=0), + title="Label 1009 ROI", + pixel_size=masked_image.pixel_size, +) +fig.tight_layout() +print(figure_html(fig, alt="One nucleus, cropped to the bounding box of its label.")) +# --8<-- [end:plot_masked_roi] + +# --8<-- [start:masked_roi_zoom] +roi_data = masked_image.get_roi_as_numpy(label=1009, c=0, zoom_factor=2) +print(roi_data.shape) +# --8<-- [end:masked_roi_zoom] + +# --8<-- [start:plot_masked_roi_zoom] +fig, ax = plt.subplots(figsize=(4.5, 4.5)) +show_image( + ax, + masked_image.get_roi_as_numpy(label=1009, c=0, zoom_factor=2), + title="Label 1009 ROI - Zoomed out", + pixel_size=masked_image.pixel_size, +) +fig.tight_layout() +print(figure_html(fig, alt="The same nucleus with twice the surrounding context.")) +# --8<-- [end:plot_masked_roi_zoom] + +# --8<-- [start:get_roi_masked] +masked_roi_data = masked_image.get_roi_masked_as_numpy(label=1009, c=0, zoom_factor=2) +print(masked_roi_data.shape) +# --8<-- [end:get_roi_masked] + +# --8<-- [start:plot_get_roi_masked] +fig, ax = plt.subplots(figsize=(4.5, 4.5)) +show_image( + ax, + masked_image.get_roi_masked_as_numpy(label=1009, c=0, zoom_factor=2), + title="Masked Label 1009 ROI", + # Everything outside the mask is zero here, and would otherwise take the low end of + # the window with it, leaving the nucleus washed out. + ignore_zeros=True, + pixel_size=masked_image.pixel_size, +) +fig.tight_layout() +print( + figure_html(fig, alt="The same nucleus with every pixel outside its mask zeroed.") +) +# --8<-- [end:plot_get_roi_masked] + +# --8<-- [start:set_roi_masked] +import numpy as np + +masked_data = masked_image.get_roi_masked_as_numpy(label=1009, c=0) +masked_data = np.random.randint(0, 255, masked_data.shape, dtype=np.uint8) +masked_image.set_roi_masked(label=1009, c=0, patch=masked_data) +# --8<-- [end:set_roi_masked] + +# --8<-- [start:plot_after_set_roi_masked] +fig, ax = plt.subplots(figsize=(4.5, 4.5)) +show_image( + ax, + masked_image.get_roi_as_numpy(label=1009, c=0, zoom_factor=2), + title="Masked Label 1009 ROI - After setting", + pixel_size=masked_image.pixel_size, +) +fig.tight_layout() +print( + figure_html( + fig, alt="The nucleus replaced by random values, its surroundings intact." + ) +) +# --8<-- [end:plot_after_set_roi_masked] + +# --8<-- [start:get_masked_label] +masked_label = ome_zarr_container.get_masked_label( + label_name="wf_2_labels", masking_label_name="nuclei" +) +print(masked_label) +# --8<-- [end:get_masked_label] diff --git a/docs/snippets/getting_started/quickstart.py b/docs/snippets/getting_started/quickstart.py new file mode 100644 index 00000000..3d9ca48e --- /dev/null +++ b/docs/snippets/getting_started/quickstart.py @@ -0,0 +1,54 @@ +"""Snippets for docs/getting_started/0_quickstart.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/getting_started/quickstart.py +""" + +# --8<-- [start:setup] +from pathlib import Path + +from ngio.utils import download_ome_zarr_dataset + +# Download a sample dataset +download_dir = Path("./data").absolute() +hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) +image_path = hcs_path / "B" / "03" / "0" +# --8<-- [end:setup] + +# --8<-- [start:open_container] +from ngio import open_ome_zarr_container + +ome_zarr_container = open_ome_zarr_container(image_path) +print(ome_zarr_container) +# --8<-- [end:open_container] + +# --8<-- [start:plot_helpers] +import sys + +# markdown-exec execs this block, so `__file__` does not exist; a standalone run puts +# only this script's own directory on sys.path. Both run from the repo root, which is +# what the rest of the snippets already assume (`Path("./data")`), so resolve the +# shared module against that. +sys.path.append("docs/snippets") + +from matplotlib import pyplot as plt + +from _render import figure_html, show_image +# --8<-- [end:plot_helpers] + +# --8<-- [start:plot_quickstart_image] +image = ome_zarr_container.get_image(path="3") + +fig, ax = plt.subplots(figsize=(6.5, 6.5)) +show_image( + ax, + image.get_as_numpy(channel_selection="DAPI"), + title="DAPI · level 3", + pixel_size=image.pixel_size, +) +fig.tight_layout() +print(figure_html(fig, alt="A field of cardiomyocyte nuclei, stained with DAPI.")) +# --8<-- [end:plot_quickstart_image] diff --git a/docs/snippets/tutorials/create_ome_zarr.py b/docs/snippets/tutorials/create_ome_zarr.py new file mode 100644 index 00000000..707fdcc1 --- /dev/null +++ b/docs/snippets/tutorials/create_ome_zarr.py @@ -0,0 +1,49 @@ +"""Snippets for docs/tutorials/create_ome_zarr.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/tutorials/create_ome_zarr.py +""" + +# --8<-- [start:plot_helpers] +import sys + +# markdown-exec execs this block, so `__file__` does not exist; a standalone run puts +# only this script's own directory on sys.path. Both run from the repo root, which is +# what the rest of the snippets already assume (`Path("./data")`), so resolve the +# shared module against that. +sys.path.append("docs/snippets") + +from matplotlib import pyplot as plt + +from _render import figure_html +# --8<-- [end:plot_helpers] + +# --8<-- [start:plot_input_image] +import skimage + +fig, ax = plt.subplots(figsize=(6, 6)) +ax.imshow(skimage.data.human_mitosis(), cmap="gray") +ax.axis("off") +print(figure_html(fig)) +# --8<-- [end:plot_input_image] + +# --8<-- [start:create] +from ngio import create_ome_zarr_from_array + +ome_zarr = create_ome_zarr_from_array( + store="./data/human_mitosis.zarr", + array=skimage.data.human_mitosis(), + pixelsize=0.1, # Just a guess + overwrite=True, +) +print(ome_zarr) +# --8<-- [end:create] + +# --8<-- [start:add_roi_table] +# create a roi for the whole image +roi_table = ome_zarr.build_image_roi_table(name="image_roi") +ome_zarr.add_table("image_roi_table", roi_table, overwrite=True) +# --8<-- [end:add_roi_table] diff --git a/docs/snippets/tutorials/feature_extraction.py b/docs/snippets/tutorials/feature_extraction.py new file mode 100644 index 00000000..c717ffb5 --- /dev/null +++ b/docs/snippets/tutorials/feature_extraction.py @@ -0,0 +1,109 @@ +"""Snippets for docs/tutorials/feature_extraction.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/tutorials/feature_extraction.py +""" + +# --8<-- [start:table_helpers] +import sys + +# markdown-exec execs this block, so `__file__` does not exist; a standalone run puts +# only this script's own directory on sys.path. Both run from the repo root, which is +# what the rest of the snippets already assume (`Path("./data")`), so resolve the +# shared module against that. +sys.path.append("docs/snippets") + +from _render import table_html +# --8<-- [end:table_helpers] + + +# --8<-- [start:extract_features] +import numpy as np +import pandas as pd +from skimage import measure + + +def extract_features(image: np.ndarray, label: np.ndarray) -> pd.DataFrame: + """Basic feature extraction using skimage.measure.regionprops_table.""" + label = label.squeeze(-1) # Remove the channel axis if present + roi_feat_table = measure.regionprops_table( + label_image=label, + intensity_image=image, + properties=[ + "label", + "area", + "mean_intensity", + "max_intensity", + "min_intensity", + ], + ) + return pd.DataFrame(roi_feat_table) + + +# --8<-- [end:extract_features] + +# --8<-- [start:open_container] +from pathlib import Path + +from ngio import open_ome_zarr_container +from ngio.utils import download_ome_zarr_dataset + +# Download the dataset +download_dir = Path("./data").absolute() +hcs_path = download_ome_zarr_dataset("CardiomyocyteTinyMip", download_dir=download_dir) +image_path = hcs_path / "B" / "03" / "0" + +# Open the OME-Zarr container +ome_zarr = open_ome_zarr_container(image_path) +# --8<-- [end:open_container] + +# --8<-- [start:setup_transform] +from ngio.transforms import ZoomTransform + +# Take the image to measure +image = ome_zarr.get_image() + +# Get the nuclei label +nuclei = ome_zarr.get_label("nuclei") + +# Here the image is stored at a higher resolution than the nuclei label +print(f"Image dimensions: {image.dimensions}, pixel size: {image.pixel_size}") +print(f"Nuclei dimensions: {nuclei.dimensions}, pixel size: {nuclei.pixel_size}") + +# So resample the label up to the image resolution with a transform +zoom_transform = ZoomTransform( + input_image=nuclei, + target_image=image, + order="nearest", # Nearest-neighbour interpolation, so label ids stay intact +) +# --8<-- [end:setup_transform] + +# --8<-- [start:extract] +from ngio.experimental.iterators import FeatureExtractorIterator +from ngio.tables import FeatureTable + +iterator = FeatureExtractorIterator( + input_image=image, + input_label=nuclei, + label_transforms=[zoom_transform], + axes_order=["y", "x", "c"], +) + +feat_table = [] +for image_data, label_data, roi in iterator.iter_as_numpy(): + print(f"Processing ROI: {roi}") + roi_feat_table = extract_features(image=image_data, label=label_data) + feat_table.append(roi_feat_table) + +# Concatenate the per-region frames into one table +feat_table = pd.concat(feat_table) +feat_table = FeatureTable(table_data=feat_table, reference_label="nuclei") +ome_zarr.add_table("nuclei_regionprops", feat_table, overwrite=True) +# --8<-- [end:extract] + +# --8<-- [start:read_table_back] +print(table_html(ome_zarr.get_table("nuclei_regionprops").dataframe.head())) +# --8<-- [end:read_table_back] diff --git a/docs/snippets/tutorials/hcs_exploration.py b/docs/snippets/tutorials/hcs_exploration.py new file mode 100644 index 00000000..5a659758 --- /dev/null +++ b/docs/snippets/tutorials/hcs_exploration.py @@ -0,0 +1,71 @@ +"""Snippets for docs/tutorials/hcs_exploration.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/tutorials/hcs_exploration.py +""" + +# --8<-- [start:table_helpers] +import sys + +# markdown-exec execs this block, so `__file__` does not exist; a standalone run puts +# only this script's own directory on sys.path. Both run from the repo root, which is +# what the rest of the snippets already assume (`Path("./data")`), so resolve the +# shared module against that. +sys.path.append("docs/snippets") + +from _render import table_html +# --8<-- [end:table_helpers] + + +# --8<-- [start:open_plate] +from pathlib import Path + +from ngio import open_ome_zarr_plate +from ngio.utils import download_ome_zarr_dataset + +# Download the dataset +download_dir = Path("./data").absolute() + +hcs_path = download_ome_zarr_dataset("CardiomyocyteTinyMip", download_dir=download_dir) +hcs_zarr = open_ome_zarr_plate(hcs_path) +print(hcs_zarr) +print(f"Rows: {hcs_zarr.rows}, Columns: {hcs_zarr.columns}") + +# Get all the images in the plate +print(hcs_zarr.get_images()) +# --8<-- [end:open_plate] + +# --8<-- [start:concatenate_tables] +# Aggregate all table across all images +table = hcs_zarr.concatenate_image_tables(name="nuclei") +print(table_html(table.dataframe.head())) +# --8<-- [end:concatenate_tables] + +# --8<-- [start:save_table] +# Save the table in the HCS plate +hcs_zarr.add_table(name="nuclei", table=table, overwrite=True) + +# Read the table back for sanity check +print(table_html(hcs_zarr.get_table("nuclei").dataframe.head())) +# --8<-- [end:save_table] + +# --8<-- [start:create_plate] +from ngio import ImageInWellPath, create_empty_plate + +test_plate = create_empty_plate( + store="./data/empty_plate.zarr", + name="Test Plate", + images=[ + ImageInWellPath(row="A", column="01", path="0"), + ImageInWellPath(row="A", column="02", path="0"), + ImageInWellPath(row="A", column="02", path="1", acquisition_id=1), + ], + overwrite=True, +) + +print(test_plate) +print(f"Rows: {test_plate.rows}, Columns: {test_plate.columns}") +# --8<-- [end:create_plate] diff --git a/docs/snippets/tutorials/image_processing.py b/docs/snippets/tutorials/image_processing.py new file mode 100644 index 00000000..76a44c45 --- /dev/null +++ b/docs/snippets/tutorials/image_processing.py @@ -0,0 +1,154 @@ +"""Snippets for docs/tutorials/image_processing.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/tutorials/image_processing.py +""" + +# --8<-- [start:plot_helpers] +import sys + +# markdown-exec execs this block, so `__file__` does not exist; a standalone run puts +# only this script's own directory on sys.path. Both run from the repo root, which is +# what the rest of the snippets already assume (`Path("./data")`), so resolve the +# shared module against that. +sys.path.append("docs/snippets") + +from matplotlib import pyplot as plt + +from _render import figure_html +# --8<-- [end:plot_helpers] + +# --8<-- [start:gaussian_blur] +import numpy as np +import skimage + + +def gaussian_blur(image: np.ndarray, sigma: float) -> np.ndarray: + """Apply gaussian blur to an image.""" + original_type = image.dtype + image = skimage.filters.gaussian( + image, sigma=sigma, channel_axis=0, preserve_range=True + ) + # Convert the image back to the original type + image = image.astype(original_type) + return image + + +# --8<-- [end:gaussian_blur] + +# --8<-- [start:open_container] +from pathlib import Path + +from ngio import open_ome_zarr_container +from ngio.utils import download_ome_zarr_dataset + +# Download the dataset +download_dir = Path("./data").absolute() + +hcs_path = download_ome_zarr_dataset("CardiomyocyteTiny", download_dir=download_dir) +image_path = hcs_path / "B" / "03" / "0" + +# Open the OME-Zarr container +ome_zarr = open_ome_zarr_container(image_path) +# --8<-- [end:open_container] + +# --8<-- [start:derive_image] +# Take the image to read from +image = ome_zarr.get_image() + +# Derive a new OME-Zarr container to store the processed image in + +blurred_omezarr_path = image_path.parent / "0_blurred" +blurred_omezarr = ome_zarr.derive_image( + store=blurred_omezarr_path, name="Blurred Image", overwrite=True +) +blurred_image = blurred_omezarr.get_image() +# --8<-- [end:derive_image] + +# --8<-- [start:apply_blur] +# `axes_order` sets the order the array comes back in. Here it is +# ["c", "z", "y", "x"], to match the blur function, which expects the +# channel axis first. +image_data = image.get_as_numpy(axes_order=["c", "z", "y", "x"]) +# Apply gaussian blur to the image +sigma = 5.0 +blurred_image_data = gaussian_blur(image_data, sigma=sigma) + +# Write the processed data back to the OME-Zarr image +blurred_image.set_array(patch=blurred_image_data, axes_order=["c", "z", "y", "x"]) + +# `set_array` wrote to one resolution level only, so the rest of the pyramid is +# still empty. `consolidate` rebuilds the other levels from it. +blurred_image.consolidate() +# --8<-- [end:apply_blur] + +# --8<-- [start:plot_blur] +original = image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]) +blurred = blurred_image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]) + +# The data does not fill its uint16 range, so window it on percentiles. One window for +# both panels: stretching them separately would misrepresent the difference. +vmin, vmax = np.percentile(original, (1, 99.8)) + +fig, axs = plt.subplots(2, 1, figsize=(8, 6)) +axs[0].set_title("Original image") +axs[0].imshow(original, cmap="gray", vmin=vmin, vmax=vmax) +axs[1].set_title("Blurred image") +axs[1].imshow(blurred, cmap="gray", vmin=vmin, vmax=vmax) +for ax in axs: + ax.axis("off") +fig.tight_layout() +print(figure_html(fig)) +# --8<-- [end:plot_blur] + +# --8<-- [start:dask_blur] +from dask import array as da + + +def dask_gaussian_blur(image: da.Array, sigma: float) -> da.Array: + """Apply gaussian blur to a dask array.""" + # This introduces edge artefacts at chunk boundaries. In a real application, + # use map_overlap with a depth chosen from sigma to avoid them. + return da.map_blocks(gaussian_blur, image, dtype=image.dtype, sigma=sigma) + + +image_dask = image.get_as_dask(axes_order=["c", "z", "y", "x"]) +blurred_image_dask = dask_gaussian_blur(image_dask, sigma=sigma) +print(blurred_image_dask) +# --8<-- [end:dask_blur] + +# --8<-- [start:iterators] +from ngio.experimental.iterators import ImageProcessingIterator + +iterator = ImageProcessingIterator( + input_image=image, + output_image=blurred_image, + axes_order=["c", "z", "y", "x"], +) + +# A freshly built iterator covers the entire image in one region. +print(f"Iterator over the whole image: {iterator}") + +# Narrow it to an arbitrary ROI table. `product` takes the cartesian product +# of the iterator's regions and the table's. +table = ome_zarr.get_roi_table("FOV_ROI_table") +iterator = iterator.product(table) +print(f"Iterator after product with table: {iterator}") + +# Set the broadcasting explicitly. `by_zyx` splits the time axis, so each step +# yields one whole ZYX volume rather than the full time series at once. +iterator = iterator.by_zyx() + +# Optionally assert the regions do not overlap each other... +iterator.require_no_regions_overlap() +# ...nor share chunks, which is what makes parallel writes safe. +iterator.require_no_chunks_overlap() + +# Map the blur across every region +iterator.map_as_numpy(lambda x: gaussian_blur(x, sigma=sigma)) + +# No need to consolidate: the iterator does it once every region is processed +# --8<-- [end:iterators] diff --git a/docs/snippets/tutorials/image_segmentation.py b/docs/snippets/tutorials/image_segmentation.py new file mode 100644 index 00000000..3eea6cbd --- /dev/null +++ b/docs/snippets/tutorials/image_segmentation.py @@ -0,0 +1,194 @@ +"""Snippets for docs/tutorials/image_segmentation.md. + +Each section between `--8<-- [start:name]` / `--8<-- [end:name]` markers is included +into the page by `pymdownx.snippets` and executed by `markdown-exec`. The whole file +is also runnable on its own: + + python docs/snippets/tutorials/image_segmentation.py +""" + +# --8<-- [start:plot_helpers] +import sys + +# markdown-exec execs this block, so `__file__` does not exist; a standalone run puts +# only this script's own directory on sys.path. Both run from the repo root, which is +# what the rest of the snippets already assume (`Path("./data")`), so resolve the +# shared module against that. +sys.path.append("docs/snippets") + +from matplotlib import pyplot as plt + +from _render import figure_html, random_label_cmap +# --8<-- [end:plot_helpers] + +# --8<-- [start:segmentation_fn] +# Setup a simple segmentation function +import numpy as np +import skimage + + +def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray: + """Simple segmentation using Otsu thresholding.""" + threshold = skimage.filters.threshold_otsu(image) + binary = image > threshold + label_image = skimage.measure.label(binary) + label_image += max_label + label_image = np.where(binary, label_image, 0) + return label_image.astype(np.uint32) + + +# --8<-- [end:segmentation_fn] + +# --8<-- [start:open_container] +from pathlib import Path + +from ngio import open_ome_zarr_container +from ngio.utils import download_ome_zarr_dataset + +# Download the dataset +download_dir = Path("./data").absolute() +hcs_path = download_ome_zarr_dataset("CardiomyocyteTiny", download_dir=download_dir) +image_path = hcs_path / "B" / "03" / "0" + +# Open the OME-Zarr container +ome_zarr = open_ome_zarr_container(image_path) +# --8<-- [end:open_container] + +# --8<-- [start:segment] +from ngio.experimental.iterators import SegmentationIterator + +# Take the image to read from, and the FOV table naming the regions to walk +image = ome_zarr.get_image() +roi_table = ome_zarr.get_roi_table("FOV_ROI_table") + +# Derive an empty label image to write the segmentation into +label = ome_zarr.derive_label("new_label", overwrite=True) + +# Setup the segmentation iterator +seg_iterator = SegmentationIterator( + input_image=image, + output_label=label, + channel_selection="DAPI", + axes_order=["z", "y", "x"], +) +seg_iterator = seg_iterator.product(roi_table) + +# Split any remaining time axis, so each step yields one whole ZYX volume +seg_iterator = seg_iterator.by_zyx() + +max_label = 0 # Carried across regions so the label ids never collide +for image_data, label_writer in seg_iterator.iter_as_numpy(): + roi_segmentation = otsu_threshold_segmentation( + image_data, max_label + ) # Segment the image + + max_label = roi_segmentation.max() # Get the max label for the next iteration + + label_writer(patch=roi_segmentation) # Write the segmentation back to the label + +# No need to consolidate, the iterator does it automatically after the last write +# --8<-- [end:segment] + +# --8<-- [start:plot_segmentation] +rand_cmap = random_label_cmap() +original = image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]) + +# The data does not fill its uint16 range, so window it on percentiles. +vmin, vmax = np.percentile(original, (1, 99.8)) + +fig, axs = plt.subplots(2, 1, figsize=(8, 6)) +axs[0].set_title("Original image") +axs[0].imshow(original, cmap="gray", vmin=vmin, vmax=vmax) +axs[1].set_title("Final segmentation") +axs[1].imshow( + label.get_as_numpy(z=1, axes_order=["y", "x"]), + cmap=rand_cmap, + interpolation="nearest", +) +for ax in axs: + ax.axis("off") +fig.tight_layout() +print(figure_html(fig)) +# --8<-- [end:plot_segmentation] + +# --8<-- [start:create_mask] +# Create a basic mask for illustration purposes +mask = ome_zarr.derive_label("mask", overwrite=True) +mask_data = mask.get_as_numpy(axes_order=["z", "y", "x"]) +mask_data[:, 200:-200, 500:2000] = 1 +mask_data[:, 200:-200, 3000:-500] = 2 +mask_data[:, 600:-600, 1200:-1000] = 0 +mask_data[:, 700:-700, 1600:-1500] = 3 +mask.set_array(mask_data, axes_order=["z", "y", "x"]) +mask.consolidate() +# --8<-- [end:create_mask] + +# --8<-- [start:plot_mask] +fig, axs = plt.subplots(2, 1, figsize=(8, 6)) +axs[0].set_title("Original image") +axs[0].imshow(original, cmap="gray", vmin=vmin, vmax=vmax) +axs[1].set_title("Mask") +axs[1].imshow( + mask.get_as_numpy(z=1, axes_order=["y", "x"]), + cmap=rand_cmap, + interpolation="nearest", +) +for ax in axs: + ax.axis("off") +fig.tight_layout() +print(figure_html(fig)) +# --8<-- [end:plot_mask] + +# --8<-- [start:masked_segment] +from ngio.experimental.iterators import MaskedSegmentationIterator + +# Take a masked image, which carries its masking ROI table with it +image = ome_zarr.get_masked_image(masking_label_name="mask") + +# Derive an empty label image to write the segmentation into +label = ome_zarr.derive_label("masked_new_label", overwrite=True) + +# Setup the masked segmentation iterator +seg_iterator = MaskedSegmentationIterator( + input_image=image, + output_label=label, + channel_selection="DAPI", + axes_order=["z", "y", "x"], +) + +# Split any remaining time axis, so each step yields one whole ZYX volume +seg_iterator = seg_iterator.by_zyx() + +max_label = 0 # Carried across regions so the label ids never collide +for image_data, label_writer in seg_iterator.iter_as_numpy(): + roi_segmentation = otsu_threshold_segmentation( + image_data, max_label + ) # Segment the image + + max_label = roi_segmentation.max() # Get the max label for the next iteration + + label_writer(patch=roi_segmentation) # Write the segmentation back to the label + +# No need to consolidate, the iterator does it automatically after the last write +# --8<-- [end:masked_segment] + +# --8<-- [start:plot_masked_segmentation] +fig, axs = plt.subplots(2, 1, figsize=(8, 6)) +axs[0].set_title("Original image") +axs[0].imshow( + image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]), + cmap="gray", + vmin=vmin, + vmax=vmax, +) +axs[1].set_title("Final segmentation") +axs[1].imshow( + label.get_as_numpy(z=1, axes_order=["y", "x"]), + cmap=rand_cmap, + interpolation="nearest", +) +for ax in axs: + ax.axis("off") +fig.tight_layout() +print(figure_html(fig)) +# --8<-- [end:plot_masked_segmentation] diff --git a/docs/stylesheets/ngio.css b/docs/stylesheets/ngio.css new file mode 100644 index 00000000..f703d17a --- /dev/null +++ b/docs/stylesheets/ngio.css @@ -0,0 +1,730 @@ +/* ══════════════════════════════════════════════════════════════════════════ + ngio — theme layer for Zensical + Lives at docs/stylesheets/ngio.css, registered via extra_css in mkdocs.yml + (Zensical reads mkdocs.yml natively). + + Nothing here touches Zensical's HTML. It re-points the theme's own CSS + variables and adds a small number of component-level rules. Everything + degrades to the stock theme if a rule stops matching after an upgrade. + ══════════════════════════════════════════════════════════════════════════ */ + +/* Space Grotesk is the display face; theme.font in mkdocs.yml only manages the + text and code families (IBM Plex Sans / JetBrains Mono), so it loads here. */ +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap"); + +/* ── 1. Typography ────────────────────────────────────────────────────────── + Text and code families are set via theme.font in mkdocs.yml so the theme + preloads them; the :root fallback keeps this stylesheet self-contained. */ +:root { + --md-text-font: "IBM Plex Sans"; + --md-code-font: "JetBrains Mono"; + --ngio-display-font: "Space Grotesk", "IBM Plex Sans", sans-serif; + + /* Motion. Short and understated — a card lifts 2px on hover and that is the + extent of it. Routed through a variable so the reduce block below can + neutralise every transition in one place. */ + --ngio-duration: 180ms; + --ngio-ease: cubic-bezier(0.2, 0, 0.2, 1); + + /* Measure. Body copy is capped narrow on purpose; at these widths ragged + last lines show, hence `text-wrap: pretty` wherever prose runs. */ + --ngio-measure-lede: 42ch; + --ngio-measure-prose: 66ch; +} + +@media (prefers-reduced-motion: reduce) { + :root { + --ngio-duration: 1ms; + } +} + +/* ── 2. Palette ───────────────────────────────────────────────────────────── + These are Zensical's own variable names (see its palette.css, per scheme). + NB: do not write that path with a glob — the `*` followed by `/` closes this + comment early, and everything after it becomes a garbage selector that eats + the whole light-scheme block below. That is exactly what used to happen. + Overriding them re-colours the whole site — header, nav, links, admonitions, + footer, search — without any selector surgery. */ + +[data-md-color-scheme="default"] { + /* Light teal #22a699 — sits between the green ngio's own site runs on and + the teal the BioVisionCenter's converters-tools docs use, so it reads as + the same family as both. This is NOT a stock Material palette entry, so + the values below are what carry the brand; theme.palette in config only + needs to pick the nearest stock name for the toggle to behave. */ + --ngio-accent: #22a699; + --ngio-accent-ink: #16695f; /* clears AA as body-copy link colour */ + --ngio-accent-soft: #e4f4f2; + --ngio-accent-2: #94dad4; /* the mark's aqua apex */ + --ngio-accent-2-ink: #0f4c45; /* hover / active */ + + /* semantic fills */ + --ngio-blue: #2e6fd6; + --ngio-green: #4cae4f; + --ngio-magenta: #c2185b; + --ngio-amber: #ffaa00; + /* AA-safe text variants for code and inline use on light surfaces */ + --ngio-blue-t: #2559b0; + --ngio-green-t: #357a38; + --ngio-magenta-t: #a81050; + --ngio-amber-t: #8a5d00; + + --md-default-bg-color: #f9fafa; + --md-default-fg-color: #101619; + --md-default-fg-color--light: #5b6569; + --md-default-fg-color--lighter: #a3abad; + --md-default-fg-color--lightest: #e3e6e6; + + --md-typeset-color: #253034; + --md-typeset-a-color: var(--ngio-accent-ink); + --md-typeset-table-color: #e3e6e6; + + --md-primary-fg-color: var(--ngio-accent); + --md-primary-fg-color--light: #4cc0b3; + --md-primary-fg-color--dark: var(--ngio-accent-ink); + --md-primary-bg-color: #f9fafa; + + --md-accent-fg-color: var(--ngio-accent-2-ink); + --md-accent-fg-color--transparent: #22a6991a; + + --md-code-bg-color: #f2f4f4; + --md-code-fg-color: #253034; + --md-code-hl-comment-color: #5b6569; + --md-code-hl-keyword-color: var(--ngio-magenta-t); + --md-code-hl-string-color: var(--ngio-green-t); + --md-code-hl-function-color: var(--ngio-blue-t); + --md-code-hl-number-color: var(--ngio-amber-t); + --md-code-hl-constant-color: var(--ngio-amber-t); + --md-code-hl-name-color: #253034; + --md-code-hl-operator-color: #5b6569; + --md-code-hl-punctuation-color: #5b6569; + + --ngio-line: #e3e6e6; + --ngio-line-strong: #cfd4d4; + --ngio-surface: #ffffff; + --ngio-sunk: #eff1f1; + --ngio-on-accent: #ffffff; + --ngio-accent-fill: var(--ngio-accent-ink); + + /* Chrome — the header, the tabs and the footer meta bar. It is deliberately + NOT the page colour: content is paper, chrome is a different level, and the + page is framed by the same tone top and bottom. + Light sinks below the page; dark lifts above it. See the slate block. */ + --ngio-chrome: var(--ngio-sunk); + --ngio-chrome-field: var(--ngio-surface); /* search pill, inverted vs chrome */ + + /* Aliases for parity with the design system's portable token file. Nothing + here uses them; they exist so a stylesheet written against those names + resolves instead of computing to the guaranteed-invalid value — a missing + --ngio-code-bg silently blanked a search field during the header work. */ + --ngio-paper: var(--md-default-bg-color); + --ngio-ink: var(--md-default-fg-color); + --ngio-faint: var(--md-default-fg-color--light); + --ngio-code-bg: var(--md-code-bg-color); +} + +[data-md-color-scheme="slate"] { + --ngio-accent: #6cc8be; + --ngio-accent-ink: #8fd8d0; + --ngio-accent-soft: #123330; + --ngio-accent-2: #abe4dc; + --ngio-accent-2-ink: #abe4dc; + + --ngio-blue: #7fa6ff; + --ngio-green: #71c174; + --ngio-magenta: #f06090; + --ngio-amber: #ffc845; + --ngio-blue-t: var(--ngio-blue); + --ngio-green-t: var(--ngio-green); + --ngio-magenta-t: var(--ngio-magenta); + --ngio-amber-t: var(--ngio-amber); + + --md-default-bg-color: #0b1113; + --md-default-fg-color: #e6edee; + --md-default-fg-color--light: #808d90; + --md-default-fg-color--lighter: #58656a; + --md-default-fg-color--lightest: #1d272a; + + --md-typeset-color: #c2ccce; + --md-typeset-a-color: var(--ngio-accent); + --md-typeset-table-color: #1d272a; + + --md-primary-fg-color: var(--ngio-accent); + --md-primary-fg-color--light: #8fd8d0; + --md-primary-fg-color--dark: #1f9389; + --md-primary-bg-color: #0b1113; + + --md-accent-fg-color: var(--ngio-accent-2); + --md-accent-fg-color--transparent: #6cc8be1a; + + --md-code-bg-color: #0f1618; + --md-code-fg-color: #c2ccce; + --md-code-hl-comment-color: #808d90; + --md-code-hl-keyword-color: var(--ngio-magenta); + --md-code-hl-string-color: var(--ngio-green); + --md-code-hl-function-color: var(--ngio-blue); + --md-code-hl-number-color: var(--ngio-amber); + --md-code-hl-constant-color: var(--ngio-amber); + --md-code-hl-name-color: #c2ccce; + --md-code-hl-operator-color: #808d90; + --md-code-hl-punctuation-color: #808d90; + + --ngio-line: #1d272a; + --ngio-line-strong: #2a3639; + --ngio-surface: #131b1e; + --ngio-sunk: #0f1618; + --ngio-on-accent: #06211e; + --ngio-accent-fill: var(--ngio-accent); + + /* Chrome inverts here, and the reason is magnitude rather than direction: + --ngio-sunk (#0f1618) sits only four or five RGB steps off the page + (#0b1113), which is not a visible level change, whereas --ngio-surface + (#131b1e) is. Note sunk is *lighter* than the page in this scheme — the + rule is "chrome is a different level", not "chrome is darker". */ + --ngio-chrome: var(--ngio-surface); + --ngio-chrome-field: var(--ngio-sunk); + + --ngio-paper: var(--md-default-bg-color); + --ngio-ink: var(--md-default-fg-color); + --ngio-faint: var(--md-default-fg-color--light); + --ngio-code-bg: var(--md-code-bg-color); +} + +/* ── 3. Display type ─────────────────────────────────────────────────────── */ +.md-typeset h1, +.md-typeset h2, +.md-typeset h3, +.md-typeset h4, +.md-header__title, +.md-nav__title { + font-family: var(--ngio-display-font); + letter-spacing: -0.022em; +} + +/* The header title is the wordmark, not a heading — it takes the wordmark's + own tracking, which is tighter than the heading scale's. */ +.md-header__title { + letter-spacing: -0.025em; +} + +.md-typeset h1 { + font-weight: 600; + font-size: 2.6rem; + line-height: 1.08; + letter-spacing: -0.035em; + /* The theme's bottom margin is 1.25em of the h1's own size, so the 2.6rem + above opens a 65px gap over the lede. Pinned in rem so it stays put. */ + margin: 0 0 1.1rem; + color: var(--md-default-fg-color); +} + +/* Section rules: every h2 opens with a hairline, which is what gives the + long landing page its rhythm. */ +.md-typeset h2 { + font-weight: 600; + font-size: 1.5rem; + margin-top: 2.6rem; + padding-top: 1.1rem; + border-top: 1px solid var(--ngio-line); +} + +.md-typeset h3 { + font-weight: 600; + font-size: 1.05rem; +} + +/* First paragraph after the h1 is the page lede. */ +.md-typeset h1 + p { + font-size: 1.1rem; + line-height: 1.5; + color: var(--md-default-fg-color); + max-width: var(--ngio-measure-lede); + text-wrap: pretty; +} + +.md-typeset p, +.md-typeset li { + text-wrap: pretty; +} + +/* Prose runs to a narrow measure. Scoped to top-level paragraphs so it does + not squeeze copy that is already inside a constrained box — cards, table + cells, admonitions — or push figures and tables off their own width. */ +.md-typeset > p { + max-width: var(--ngio-measure-prose); +} + +/* ── 4. Header and tabs ────────────────────────────────────────────────────── + Chrome sits on its own level, not on the page colour. The step between the + two surfaces is small by design (about 1.08:1), so the edge is carrying most + of the separation — which is why it takes --ngio-line-strong rather than the + hairline. `box-shadow: none` is load-bearing: it defeats the theme's + .md-header--shadow, and this system has exactly one shadow, on card hover. */ +.md-header { + background: var(--ngio-chrome); + color: var(--md-default-fg-color); + box-shadow: none; + border-bottom: 1px solid var(--ngio-line-strong); +} + +.md-header__button.md-logo :is(img, svg) { + height: 1.5rem; + width: 1.5rem; +} + +/* theme.logo is one file for both schemes, and it is an ``, so it has + the same problem as the lockup in 6c: it cannot see the palette. The chrome + is near-white under default and dark under slate, where the colour mark's + deepest level (#1c7c69) goes muddy — hence the swap to the dark mark. + Done as a background on the anchor rather than `content: url()`, whose + support on replaced elements is uneven; the img is left in place, only made + invisible, so it keeps holding the 1.5rem box open. Paths here resolve + against this stylesheet, i.e. /stylesheets/ → /assets/. */ +[data-md-color-scheme="slate"] .md-header__button.md-logo { + background: url("../assets/logo-dark.svg") no-repeat center / 1.5rem 1.5rem; +} + +[data-md-color-scheme="slate"] .md-header__button.md-logo :is(img, svg) { + visibility: hidden; +} + +/* ── The header wordmark ────────────────────────────────────────────────── + Zensical renders `site_name` as the header title, which is the descriptive + string we want in and in search results but not in the lockup. The + lockup is the wordmark: lowercase `ngio` with an accent `i`. + + That needs three separately-coloured pieces out of one text node, and the + only ways to get markup are a `custom_dir` override — which means vendoring + a 69-line generated partial against an unpinned pre-1.0 dependency — or + pseudo-elements. `.md-header__topic` is a flex container and `.md-ellipsis` + a flex item, so between them they give four ordered slots; three is enough. + + The site name is collapsed with `font-size: 0` rather than hidden, because + `visibility: hidden` and `opacity: 0` both keep the string's full width and + push the trailing `o` to the far end of the flex line. Zeroing the size also + keeps the name in the accessibility tree, so screen readers still get the + real site name; the cost is that the decorative `ngio` may be announced + alongside it, which is the lesser of the two problems. + + Scoped to :first-child on purpose. The second .md-header__topic is the page + title that slides in on scroll; restyling it breaks the scrolled state. */ +.md-header__topic:first-child .md-ellipsis { + font-size: 0; + flex-grow: 0; +} + +.md-header__topic:first-child .md-ellipsis::before, +.md-header__topic:first-child .md-ellipsis::after, +.md-header__topic:first-child::before { + font-family: var(--ngio-display-font); + font-size: 0.9rem; + font-weight: 600; + letter-spacing: -0.025em; + color: var(--md-default-fg-color); +} + +.md-header__topic:first-child::before { + content: "ng"; +} + +.md-header__topic:first-child .md-ellipsis::before { + content: "i"; + /* --ngio-accent-fill already switches per scheme: the darkened ink in light, + the raw accent in dark. Using it keeps the `i` legible in both. */ + color: var(--ngio-accent-fill); +} + +.md-header__topic:first-child .md-ellipsis::after { + content: "o"; +} + +.md-tabs { + background: var(--ngio-chrome); + color: var(--md-default-fg-color--light); + border-bottom: 1px solid var(--ngio-line-strong); +} + +.md-tabs__link { + font-size: 0.72rem; + font-weight: 500; + opacity: 1; + color: var(--md-default-fg-color--light); +} + +/* The active tab's 2px underline sits on the chrome/content boundary, so the + edge is doing navigation work rather than just being a line. */ +.md-tabs__item--active .md-tabs__link { + color: var(--md-default-fg-color); + box-shadow: inset 0 -2px 0 var(--md-primary-fg-color); +} + +/* Zensical draws its own `border-bottom: .05rem solid var(--md-default-fg-color)` + on the active tab, which stacks under the inset above — two underlines, the + lower one near-black in light and near-white in dark. The box-shadow is the + underline here. */ +.md-tabs__item--active { + border-bottom: 0; +} + +/* ── 5. Sidebar ──────────────────────────────────────────────────────────── */ +.md-nav { + font-size: 0.72rem; +} + +.md-nav__link { + border-radius: 7px; + padding: 0.3rem 0.6rem; +} + +.md-nav__link--active { + background: var(--ngio-accent-soft); + color: var(--md-accent-fg-color); +} + +/* Top-level section labels read as eyebrows, not links. */ +.md-nav--primary > .md-nav__list > .md-nav__item--nested > .md-nav__link { + font-family: var(--md-code-font), monospace; + font-size: 0.6rem; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--md-default-fg-color--light); +} + +/* Table of contents: hairline rail. */ +.md-nav--secondary .md-nav__link { + border-left: 2px solid var(--ngio-line); + border-radius: 0; + padding-left: 0.6rem; +} + +.md-nav--secondary .md-nav__link--active { + border-left-color: var(--md-primary-fg-color); + background: none; +} + +/* ── 6. Code blocks ──────────────────────────────────────────────────────── */ +.md-typeset pre > code, +.md-typeset .highlight { + border-radius: 10px; +} + +.md-typeset .highlight { + border: 1px solid var(--ngio-line); + overflow: hidden; +} + +.md-typeset code { + border: 1px solid var(--ngio-line); + border-radius: 4px; + padding: 0.05em 0.35em; + font-size: 0.84em; +} + +.md-typeset pre > code { + border: 0; + line-height: 1.75; +} + +/* The `result` blocks produced by the executed snippets read as terminal + output rather than as more prose. */ +.md-typeset .result { + margin-top: -0.6rem; + border: 1px solid var(--ngio-line); + border-top: 0; + border-radius: 0 0 10px 10px; + background: var(--ngio-sunk); + font-family: var(--md-code-font), monospace; + font-size: 0.72rem; + padding: 0.7rem 0.9rem; +} + +.md-typeset .result::before { + content: "OUT"; + display: block; + margin-bottom: 0.4rem; + font-size: 0.58rem; + letter-spacing: 0.09em; + color: var(--md-default-fg-color--light); +} + +/* A rendered figure is not terminal output, even though markdown-exec delivers + it through the same `.result` wrapper. Strip the terminal treatment back off + when the wrapper holds a figure, so the image reads as an image. + Where :has() is unsupported the figure simply keeps the OUT label — the old + behaviour, and the reason this is written as a subtraction. */ +.md-typeset .result:has(> .ngio-figure) { + margin-top: 0; + border: 0; + border-radius: 0; + background: none; + padding: 0; +} + +.md-typeset .result:has(> .ngio-figure)::before { + content: none; +} + +/* The SVG carries its own intrinsic size from matplotlib; cap it to the column + and let it scale down on narrow viewports. */ +.md-typeset .ngio-figure svg { + max-width: 100%; + height: auto; +} + +/* ── 6b. Diagrams ──────────────────────────────────────────────────────────── + The explanatory diagrams are authored as inline SVG in the Markdown rather + than as files in docs/assets/. That is deliberate: an `<img src>` is a + separate document and cannot see these custom properties, so it would need + two files and would still not follow the palette toggle. Inline, the shapes + reference --ngio-blue / --ngio-green / --ngio-magenta and re-colour with the + scheme for free. + + The semantic mapping is load-bearing and consistent across all six figures: + blue = image data, green = labels, magenta = tables and ROIs, teal = ngio + itself. A greyscale cell field on a dark ground means real pixel data — that + field is deliberately fixed rather than tokenised, because a micrograph does + not invert with the site theme. + + The figures set their own type inline, so there are no text rules here; only + the margin and the width, which the SVGs depend on since they carry a + viewBox and no intrinsic size. */ +.md-typeset .ngio-diagram { + margin: 1.6em 0; +} + +.md-typeset .ngio-diagram svg { + display: block; + width: 100%; + height: auto; +} + +/* ── 6c. The landing-page lockup ────────────────────────────────────────────── + The one place the inline-SVG trick of 6b does not apply. The lockup's + wordmark is outlined path data rather than live text — an SVG used as an + image cannot load the webfont — so it ships as two files with baked-in + fills, one per scheme, and takes the two-file route 6b avoids. + + Nothing here performs the swap: Zensical's own stylesheets hide + `img[src$="#only-light"]` under slate and `img[src$="#only-dark"]` under + default, so the pair in index.md only needs the URL fragments. What is left + is the size, which the SVG cannot supply on its own at this width. + + The heading the lockup replaced is still in the page, just moved off-screen: + Zensical takes the page title from the first h1, the table of contents wants + a root entry, and a screen reader wants a heading — none of which an image + provides. The images are therefore marked decorative (empty alt) so the name + is announced once, by the h1. */ +.md-typeset .ngio-hero-logo { + display: block; + width: 15rem; + max-width: 100%; + height: auto; + margin: 0 0 1.2rem; +} + +.md-typeset .ngio-visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +/* ── 7. Content tabs (pip / mamba / source) ──────────────────────────────── */ +.md-typeset .tabbed-set > .tabbed-labels { + box-shadow: none; + border-bottom: 1px solid var(--ngio-line); +} + +.md-typeset .tabbed-labels > label { + font-family: var(--md-code-font), monospace; + font-size: 0.68rem; + font-weight: 400; +} + +.md-typeset .tabbed-labels > label > [href]:first-child { + color: inherit; +} + +/* ── 8. Grid cards ───────────────────────────────────────────────────────── */ +.md-typeset .grid.cards > ul > li { + border: 1px solid var(--ngio-line); + border-radius: 12px; + background: var(--ngio-surface); + padding: 1.1rem; + transition: + border-color var(--ngio-duration) var(--ngio-ease), + box-shadow var(--ngio-duration) var(--ngio-ease), + transform var(--ngio-duration) var(--ngio-ease); +} + +/* The one shadow in the system, and the only place elevation appears. */ +.md-typeset .grid.cards > ul > li:hover { + border-color: var(--md-primary-fg-color); + box-shadow: + 0 1px 2px rgb(16 22 25 / 5%), + 0 8px 24px -12px rgb(16 22 25 / 16%); + transform: translateY(-2px); +} + +/* Shortening the duration is not enough — an instant 2px jump is exactly the + movement the preference asks us not to make. The border and shadow still + change, so the card is still legibly hovered. */ +@media (prefers-reduced-motion: reduce) { + .md-typeset .grid.cards > ul > li:hover { + transform: none; + } +} + +.md-typeset .grid.cards > ul > li > hr { + display: none; +} + +.md-typeset .grid.cards > ul > li > p:first-child strong { + font-family: var(--ngio-display-font); + font-weight: 600; + font-size: 0.86rem; +} + +.md-typeset .grid.cards .twemoji.lg svg, +.md-typeset .grid.cards .twemoji svg { + fill: var(--md-primary-fg-color); +} + +/* ── 9. Admonitions ────────────────────────────────────────────────────────── + Admonitions are deliberately NOT colour-coded by severity: every kind gets + the same accent left border. If something is dangerous, the title says so. + + The doubled [class] is load-bearing, not a typo. Zensical colours each type + with `.md-typeset .admonition.note` (0,3,0) and + `.md-typeset .note > .admonition-title::before` (0,3,1), both of which + outrank a plain `.md-typeset .admonition` (0,2,0) no matter what order the + files load in. Without the bump the translucent blue tint and blue icon + survive — which also breaks the "no transparency anywhere" rule. Repeating + the attribute selector clears their specificity for every type at once, + instead of enumerating a dozen type classes that Zensical may add to. */ +.md-typeset .admonition[class][class], +.md-typeset details[class][class] { + border: 1px solid var(--ngio-line); + border-left: 3px solid var(--md-primary-fg-color); + border-radius: 0 10px 10px 0; + box-shadow: none; + background: var(--ngio-surface); + font-size: 0.76rem; +} + +.md-typeset .admonition-title, +.md-typeset summary { + font-family: var(--ngio-display-font); + font-weight: 600; + background: transparent; +} + +.md-typeset [class][class] > .admonition-title::before, +.md-typeset [class][class] > summary::before { + background-color: var(--md-primary-fg-color); +} + +/* ::after is the disclosure chevron on a collapsible `details`, and Zensical + colours it per type as well. */ +.md-typeset [class][class] > .admonition-title::after, +.md-typeset [class][class] > summary::after { + color: var(--md-primary-fg-color); +} + +/* ── 10. mkdocstrings API reference ──────────────────────────────────────── */ +.md-typeset .doc-heading { + font-family: var(--md-code-font), monospace; + font-weight: 500; + border-top: 1px solid var(--ngio-line); + padding-top: 1rem; +} + +.md-typeset .doc-symbol { + border-radius: 5px; + background: var(--ngio-accent-soft); + color: var(--md-accent-fg-color); +} + +.md-typeset .doc-signature { + border: 1px solid var(--ngio-line); + border-radius: 10px; +} + +.md-typeset .doc-contents .autorefs-internal { + border-bottom: 1px dotted var(--md-primary-fg-color); + text-decoration: none; +} + +.md-typeset details.mkdocstrings-source { + border-left-width: 1px; + border-left-color: var(--ngio-line); + border-radius: 10px; + background: var(--ngio-sunk); +} + +/* ── 11. Glossary abbreviations ──────────────────────────────────────────── */ +.md-typeset abbr[title] { + border-bottom: 1px dotted var(--md-default-fg-color--light); + cursor: help; + text-decoration: none; +} + +/* ── 12. Footer ────────────────────────────────────────────────────────────── + The meta bar takes the same chrome tone as the header, which is what closes + the frame: chrome top and bottom, paper between. Matching edge weight too. */ +.md-footer { + border-top: 1px solid var(--ngio-line-strong); +} + +.md-footer-meta { + background: var(--ngio-chrome); + color: var(--md-default-fg-color--light); +} + +.md-footer__link { + border: 1px solid var(--ngio-line); + border-radius: 12px; + padding: 0.9rem 1rem; + opacity: 1; +} + +.md-footer__link:hover { + border-color: var(--md-primary-fg-color); +} + +.md-footer__title { + font-family: var(--ngio-display-font); + background: none; +} + +.md-copyright { + color: var(--md-default-fg-color--light); +} + +/* ── 13. Search button ─────────────────────────────────────────────────────── + The field is inverted against the chrome it sits on — surface on a sunk bar + in light, sunk on a lifted bar in dark — so it reads as something you can + type into rather than as a hole in the header. Zensical's modern theme + renders this control as a <button>; it emits no .md-search__form/__input. */ +.md-search__button { + border: 1px solid var(--ngio-line-strong); + border-radius: 8px; + background: var(--ngio-chrome-field); + color: var(--md-default-fg-color--light); + font-size: 0.7rem; +} + +/* Not optional. Zensical's own :hover is (0,2,0) and beats the rule above + whatever the source order, flipping the pill to a mid-grey slab. Match its + specificity to keep the field a field; the accent border is the hover cue. */ +.md-search__button:hover, +.md-search__button:focus { + background: var(--ngio-chrome-field); + border-color: var(--ngio-accent); + color: var(--md-default-fg-color); +} diff --git a/docs/table_specs/backend.md b/docs/table_specs/backend.md index bbc0c50d..2a3a360b 100644 --- a/docs/table_specs/backend.md +++ b/docs/table_specs/backend.md @@ -1,55 +1,72 @@ -# Table Backends +--- +description: "The on-disk table backends: AnnData, Parquet, CSV and JSON." +--- -In ngio we implemented four different table backends. Each table backend is a python class that can serialize tabular data into OME-Zarr containers. +# Table backends + +ngio has four on-disk table formats — AnnData, Parquet, CSV and JSON — each implemented by a Python class that serialises tabular data into an OME-Zarr container. These backends are wrappers around existing tooling implemented in `anndata`, `pandas`, and `polars`. -Currently, we provide a thin layer of metadata and table normalization to ensure that tables are serialized/deserialized in a consistent way across the different backends and across different table objects. +On top of that, ngio adds a thin layer of metadata and table normalisation, so that tables are serialised and deserialised consistently across the different backends and across different table objects. + +In particular, the metadata describes the intended index key and type of the table for each backend. + +!!! note "Backend names" -In particular, we provide the metadata that describes the intended index key and type of the table for each backend. + The `backend` attribute records the name of the backend that wrote the table. The + convention is `{backend_name}_v{version}`, so a backend that declares a version is + recorded under it — `anndata_v1` — and one that does not under its plain name. + A table saved without an explicit `backend=` is written by whichever backend + `DefaultTableBackend` names. -## AnnData Backend + `experimental_json_v1`, `experimental_csv_v1` and `experimental_parquet_v1` are legacy + aliases for the JSON, CSV and Parquet backends, kept so that tables written by older + ngio releases still load. They are accepted wherever a backend name is, and resolve to + the same backends; passing one stores that backend's current name. -AnnData is a widely used format in single-cell genomics, and can natively store complex tabular data in a Zarr group. The AnnData backend in ngio is a wrapper around the `anndata` library, which performs some table normalization for consistency and compatibility with the ngio table specifications. +## AnnData backend -The following normalization steps are applied to each table before saving it to the AnnData backend: +AnnData is a widely used format in single-cell genomics, and can natively store complex tabular data in a Zarr group. The AnnData backend in ngio is a wrapper around the `anndata` library, and applies some table normalisation for consistency and compatibility with the ngio table specifications. -- We separate the table in two parts: The floating point columns are casted to `float32` and stored as `X` in the AnnData object, while the categorical, boolean, and integer columns are stored as `obs`. +The following normalisation steps are applied to each table before saving it to the AnnData backend: + +- The table is split in two: numeric columns — floats and booleans — become `X`, while categorical and integer columns become `obs`. If the numeric columns do not share a dtype, they are cast to a common `float64`; a homogeneous set is stored as it is. - The index column is cast to a string, and is stored in the `obs` index. - The index column name must match the `index_key` specified in the metadata. AnnData backend metadata: -```json +```json5 { // Backend metadata - "backend": "annadata", // the backend used to store the table, e.g. "annadata", "parquet", etc.. + "backend": "anndata", // the backend used to store the table, e.g. "anndata", "parquet", etc.. "index_key": "index", // The default index key for the table, which is used to identify each row. - "index_type": "str", // Either "int" or "str" + "index_type": "str" // Either "int" or "str" } ``` Additionally, the AnnData package will write some additional metadata to the group attributes -```json +```json5 { "encoding-type": "anndata", - "encoding-version": "0.1.0", + "encoding-version": "0.1.0" } ``` -## Parquet Backend +## Parquet backend -The Parquet backend is a high-performance columnar storage format that is widely used in big data processing. It is designed to efficiently store large datasets and can be used with various data processing frameworks. -Another advantage of the Parquet backend is that it can be used lazily, meaning that the data is not loaded into memory until it is needed. This can be useful for working with large datasets that do not fit into memory. +The Parquet backend is a high-performance columnar storage format that is widely used in big data processing. It is designed to store large datasets efficiently and can be used with various data processing frameworks. +Another advantage of the Parquet backend is that it can be read lazily: the data is not loaded into memory until it is needed. That helps when working with datasets that do not fit into memory. Parquet backend metadata: -```json +```json5 { // Backend metadata - "backend": "parquet", // the backend used to store the table, e.g. "annadata", "parquet", etc.. + "backend": "parquet", // the backend used to store the table, e.g. "anndata", "parquet", etc.. "index_key": "index", // The default index key for the table, which is used to identify each row. - "index_type": "int", // Either "int" or "str" + "index_type": "int" // Either "int" or "str" } ``` @@ -62,18 +79,18 @@ table.zarr # Zarr group for the table └── .zgroup # Zarr group metadata ``` -## CSV Backend +## CSV backend -The CSV backend is a plain text format that is widely used for tabular data. It is easy to read and write, and can be used across many different tools. +The CSV backend is a plain text format that is widely used for tabular data. It can be read and written by hand, and across many different tools. The CSV backend in ngio follows closely the same specifications as the Parquet backend, with the following metadata: -```json +```json5 { // Backend metadata - "backend": "csv", // the backend used to store the table, e.g. "annadata", "parquet", etc.. + "backend": "csv", // the backend used to store the table, e.g. "anndata", "parquet", etc.. "index_key": "index", // The default index key for the table, which is used to identify each row. - "index_type": "int", // Either "int" or "str" + "index_type": "int" // Either "int" or "str" } ``` @@ -86,28 +103,36 @@ table.zarr # Zarr group for the table └── .zgroup # Zarr group metadata ``` -## JSON Backend +## JSON backend -The JSON backend serializes the table data into the Zarr group attributes as a JSON object. This backend is useful for tiny tables. +The JSON backend serialises the table data into the Zarr group attributes as a JSON object. This backend is useful for tiny tables. JSON backend metadata: -```json +```json5 { // Backend metadata - "backend": "json", // the backend used to store the table, e.g. "annadata", "parquet", etc.. + "backend": "json", // the backend used to store the table, e.g. "anndata", "parquet", etc.. "index_key": "index", // The default index key for the table, which is used to identify each row. "index_type": "int" // Either "int" or "str" } ``` -The table will be stored in a subgroup of the Zarr group, and the metadata will be stored in the group attributes. Storing the table in a subgroup instead of a standalone json file allows for easier access via the Zarr API. +The table is stored in a subgroup of the Zarr group, and the metadata is stored in the group attributes. Storing the table in a subgroup rather than a standalone JSON file keeps it accessible through the Zarr API. ```bash table.zarr # Zarr group for the table +├── .zattrs # Zarr group attributes containing the metadata +├── .zgroup # Zarr group metadata └── table # Zarr subgroup containing the table data - ├── .zattrs # the json table data serialized as a JSON object + ├── .zattrs # the table data serialised as a JSON object └── .zgroup # Zarr group metadata -├── .zattrs # Zarr group attributes containing the metadata -└── .zgroup # Zarr group metadata ``` + +!!! note "Zarr v2 and v3 layouts" + + The trees on this page show the Zarr v2 layout, which is what OME-Zarr 0.4 uses. Under + OME-Zarr 0.5, backed by Zarr v3, the separate `.zattrs` and `.zgroup` files are + replaced by a single `zarr.json` per group. The attributes themselves — and everything + the [table type specifications](table_types/generic_table.md) require — are unchanged; + only the file they live in differs. diff --git a/docs/table_specs/overview.md b/docs/table_specs/overview.md index ebe36927..af744bea 100644 --- a/docs/table_specs/overview.md +++ b/docs/table_specs/overview.md @@ -1,47 +1,59 @@ -# Tables Overview +--- +description: "The ngio table architecture: backends, in-memory objects and type specifications." +--- -Ngio's architecture is designed to tightly integrate image and tabular data. For this purpose we developed custom specifications for serializing and deserializing tabular data into OME-Zarr containers, and semantically typed tables derived from the [fractal table specification](https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/). +# Tables overview + +ngio's architecture tightly integrates image and tabular data. To do that, ngio defines custom specifications for serialising and deserialising tabular data into OME-Zarr containers, together with semantically typed tables. These table types were originally defined as part of [Fractal](https://fractal-analytics-platform.github.io/) and are now specified and maintained here. ## Architecture -The ngio tables architectures is composed of three main components: +The ngio tables architecture is composed of three main components: -### 1. Table Backends +### 1. Table backends -A backend module is a class that can serialize tabular data into OME-Zarr containers. We currently support four on-disk file formats: +A backend module is a class that can serialise tabular data into OME-Zarr containers. ngio supports four on-disk file formats: -- **AnnData**: Commonly used in single-cell genomics and was the standard table for the initial Fractal table spec. -- **Parquet**: A columnar storage file format optimized for large datasets. -- **CSV**: A simple text format for tabular data, easily human readable and writable. -- **JSON**: A lightweight data interchange format that both readable and efficient for small tables. +- **AnnData**: Commonly used in single-cell genomics, and the standard table for the initial Fractal table spec. +- **Parquet**: A columnar storage file format optimised for large datasets. +- **CSV**: A plain text format for tabular data, readable and writable by hand. +- **JSON**: A lightweight data interchange format that is both readable and efficient for small tables. -A more detailed description of the backend module can be found in the [Table Backends documentation](backend.md). +For a detailed description of the backend module, see the [table backends documentation](backend.md). -### 2. In-Memory Table Objects +### 2. In-memory table objects -These are Python objects that represent the tabular data in memory. They provide a convenient interface for manipulating and analyzing the data without needing to interact directly with the underlying file format. We support the following in-memory table objects: +These are Python objects that represent the tabular data in memory. They give you an interface for manipulating and analysing the data without interacting directly with the underlying file format. ngio supports the following in-memory table objects: - **Pandas DataFrame**: The most commonly used data structure for tabular data in Python. - **Polars LazyFrame**: A fast DataFrame implementation that allows for lazy evaluation and efficient computation on large datasets. -- **AnnData**: A specialized data structure for single-cell genomics data, which goes beyond simple tabular data. +- **AnnData**: A specialised data structure for single-cell genomics data, which goes beyond plain tabular data. + +ngio also provides utilities to convert between these in-memory representations in a standardised way, based on the table type specifications and metadata. + +### 3. Table type specifications -We also provide utilities to convert between these in-memory representations in a standardized way based on the table type specifications/metadata. +These specifications define structured tables that standardise common table types used in image analysis. ngio defines five table types so far: -### 3. Table Type Specifications +- **Generic tables**: A flexible table type that can represent any tabular data. See more in the [generic tables documentation](table_types/generic_table.md). +- **ROI tables**: A table type designed for representing Regions of Interest (ROIs) in images. See more in the [ROI tables documentation](table_types/roi_table.md). +- **Masking ROI tables**: A specialised table type for representing ROIs that are associated with specific labels in an OME-Zarr label image. See more in the [masking ROI tables documentation](table_types/masking_roi_table.md). +- **Feature tables**: A table type for representing features extracted from images. This table is also associated with a specific label image. See more in the [feature tables documentation](table_types/feature_table.md). +- **Condition tables**: A table to represent experimental conditions or metadata associated with images or experiments. See more in the [condition tables documentation](table_types/condition_table.md). -These specifications define structured tables that standardize common table types used in image analysis. We have defined five table types so far: +Of these, four are recognised automatically when a table is read: ROI, masking ROI, feature +and condition. Anything else — including a table written by another tool — is loaded as a +generic table. -- **Generic Tables**: A flexible table type that can represent any tabular data. See more in the [Generic Tables documentation](table_types/generic_table.md). -- **ROI Tables**: A table type specifically designed for representing Regions of Interest (ROIs) in images. See more in the [ROI Tables documentation](table_types/roi_table.md). -- **Masking ROI Tables**: A specialized table type for representing ROIs that are associated with specific labels in a OME-Zarr label image. See more in the [Masking ROI Tables documentation](table_types/masking_roi_table.md). -- **Feature Tables**: A table type for representing features extracted from images. This table is also associated with a specific label image. See more in the [Feature Tables documentation](table_types/feature_table.md). -- **Condition Tables**: A table to represent experimental conditions or metadata associated with images or experiments. See more in the [Condition Tables documentation](table_types/condition_table.md). +There is also [`GenericRoiTable`][ngio.tables.GenericRoiTable], a ROI table without the +naming and indexing conventions of the standard one. It has no spec page, and is not +auto-detected on read: reach it explicitly with `get_generic_roi_table`. -## Tables Groups +## Table groups -Tables in OME-Zarr images are organized into groups of tables. Each group is saved in a Zarr group, and can be associated with a specific image or plate. The tables groups are: +Tables in OME-Zarr images are organised into groups of tables. Each group is saved in a Zarr group, and can be associated with a specific image or plate. The table groups are: -- **Image Tables**: These tables are a sub group of the OME-Zarr image group and contain metadata or features related only to that specific image. The `.zarr` hierarchy is based on image [specification in NGFF 0.4](https://ngff.openmicroscopy.org/0.4/index.html#image-layout). The subgroup structure is based on the approach of the OME-Zarr `labels` group. +- **Image tables**: These tables are a sub group of the OME-Zarr image group and contain metadata or features related only to that specific image. The `.zarr` hierarchy is based on image [specification in NGFF 0.4](https://ngff.openmicroscopy.org/0.4/index.html#image-layout). The subgroup structure is based on the approach of the OME-Zarr `labels` group. ```bash image.zarr # Zarr group for a OME-Zarr image @@ -61,16 +73,16 @@ image.zarr # Zarr group for a OME-Zarr image └── ... ``` -- **Plate Tables**: These tables are a sub group of the OME-Zarr plate group and contain metadata or features related only to that specific plate. +- **Plate tables**: These tables are a sub group of the OME-Zarr plate group and contain metadata or features related only to that specific plate. ```bash plate.zarr # Zarr group for a OME-Zarr HCS plate | ├── A # Row A of the plate -| ├── 1 # Column 0 of row A -| | ├── 0 # Acquisition 0 of column A1 -| | ├── 1 # Acquisition 1 of column A1 -| | └── ... # Other acquisitions of column A1 +| ├── 1 # Column 1 of row A, i.e. well A1 +| | ├── 0 # Image 0 in well A1 +| | ├── 1 # Image 1 in well A1 +| | └── ... # Other images in well A1, one per field and acquisition ... ├── tables # Zarr subgroup with a list of tables associated to this plate | ├── table_1 # Zarr subgroup for a given table @@ -79,11 +91,11 @@ plate.zarr # Zarr group for a OME-Zarr HCS plate └── ... ``` -If a plate table contains per image information, the table should contain a `row`, `column`, and `path_in_well` columns. +If a plate table contains per-image information, the table should contain `row`, `column`, and `path_in_well` columns. -## Tables Group Attributes +## Table group attributes -The Zarr attributes of the tables group must include the key tables, pointing to the list of all tables (this simplifies discovery of tables associated to the current OME-Zarr image or plate), as in +The Zarr attributes of the tables group must include the key tables, pointing to the list of all tables. This makes it easier to discover the tables associated with the current OME-Zarr image or plate. ```json { diff --git a/docs/table_specs/table_types/condition_table.md b/docs/table_specs/table_types/condition_table.md index e6f14d2c..e139ac7b 100644 --- a/docs/table_specs/table_types/condition_table.md +++ b/docs/table_specs/table_types/condition_table.md @@ -1,10 +1,14 @@ -# Condition Table +--- +description: "Condition table: a flexible table type for experimental conditions and metadata." +--- -A condition table is a simple table that can be used to represent experimental conditions or metadata associated with images or experiments. It is a flexible table type that can be used to store any kind of metadata related to the images or experiments. +# Condition table + +A condition table represents experimental conditions or metadata associated with images or experiments. It is a flexible table type, so it can hold any kind of metadata about them. Example condition table: -| Cell Type | Drug | Dose | +| Cell type | Drug | Dose | |-----------|-----------|------| | A | Drug A | 10 | | A | Drug B | 20 | @@ -15,14 +19,19 @@ Example condition table: A condition table must include the following metadata fields in the group attributes: -```json +```json5 { // Condition table metadata "type": "condition_table", "table_version": "1", // Backend metadata - "backend": "csv", // the backend used to store the table, e.g. "annadata", "parquet", etc.. - "index_key": "index", // The default index key for the condition table, which is used to identify each row. - "index_type": "int" // Either "int" or "str" + "backend": "csv", // the backend used to store the table, e.g. "anndata", "parquet", etc.. + "index_key": "condition_id", // Optional. The column used as the row index. + "index_type": "str" // Optional. Either "int" or "str" } ``` + +As with [generic tables](generic_table.md), a condition table has no default index: +`index_key` and `index_type` appear only when you set one. + +In ngio this table type is the [`ConditionTable`][ngio.tables.ConditionTable] class. diff --git a/docs/table_specs/table_types/custom_table.md b/docs/table_specs/table_types/custom_table.md index c1996b6a..5283e129 100644 --- a/docs/table_specs/table_types/custom_table.md +++ b/docs/table_specs/table_types/custom_table.md @@ -1,6 +1,31 @@ -# Add a Custom Table +--- +description: How custom table types fit into the ngio table architecture, and what to use until the extension API is documented. +--- -Ngio allows users to define custom tables that can be used to store any kind of tabular data. Custom tables are flexible and can be used to represent any kind of data that does not fit into the predefined table types. +# Add a custom table -!!! warning - The library is still in the early stages and full documentation for custom tables is not yet available. +A custom table is a table type you define yourself, for data that does not fit any of the +predefined types. + +!!! note "No public extension API yet" + + The registry that maps a `type` string to a table class is internal, so there is no + supported way to register your own table type today. Until there is, + [generic tables](generic_table.md) are the way to store arbitrary tabular data: they + accept a pandas `DataFrame`, a polars `DataFrame` or `LazyFrame`, or an `AnnData` + object. + + One caveat when choosing a [backend](../backend.md): only the AnnData backend can + write an `AnnData` payload. If your generic table holds one, saving it through the + Parquet, CSV or JSON backend raises `NotImplementedError` — convert it to a + `DataFrame` first, or keep it on the AnnData backend. + + If you need a genuinely new table *type* — with its own validation and metadata — open + an issue on [GitHub](https://github.com/BioVisionCenter/ngio/issues) describing your + use case. + +## See also + +- [Tables overview](../overview.md) — the three-component table architecture. +- [Generic tables](generic_table.md) — the untyped fallback. +- [Tables API reference](../../api/tables.md) — `TablesContainer` and the table classes. diff --git a/docs/table_specs/table_types/feature_table.md b/docs/table_specs/table_types/feature_table.md index 75d2992f..06de4cb7 100644 --- a/docs/table_specs/table_types/feature_table.md +++ b/docs/table_specs/table_types/feature_table.md @@ -1,18 +1,26 @@ -# Feature Tables +--- +description: "Feature table: per-object measurements tied to a label image." +--- -A feature table is a table type for representing per object features in an image. Each row in a feature table corresponds to a specific label in the label image. +# Feature table -Feature tables can optionally include metadata to specify the type of features stored in each column: +A feature table is a table type for representing per-object features in an image. Each row in a feature table corresponds to a specific label in the label image. + +A feature table can also declare what kind of feature each column holds: - `measurement`: A quantitative measurement of the object, such as area, perimeter, or intensity. - `categorical`: A categorical feature of the object, such as a classification label or a type. -- `metadata`: Additional free-from columns that can be used to store any other information about the object, but that should not be used for analysis/classification purposes. +- `metadata`: Additional free-form columns that can be used to store any other information about the object, but that should not be used for analysis/classification purposes. + +The declaration is there so that downstream tools can select subsets of features without +guessing from dtypes. -These feature types inform casting of the values when serialising a table and can be used in downstream analysis to select specific subsets of features. The feature type can be explicitly specified in the feature table metadata. Alternatively, if a column is not specified, we apply the following casting rules: +!!! warning "Declarative only" -- If the column contains only numeric values, it is considered a `measurement`. -- If the column contains string or boolean values, it is considered a `categorical`. -- The index column is considered a `categorical` feature. + ngio writes these three lists but does not yet read them back: they do not influence + how a table is serialised. Casting is decided by dtype alone, as described under + [table backends](../backend.md). Treat the lists as an annotation for your own + tooling, not as a contract ngio enforces. ## Specifications @@ -20,26 +28,27 @@ These feature types inform casting of the values when serialising a table and ca A feature table must include the following metadata fields in the group attributes: -```json +```json5 { // Feature table metadata "type": "feature_table", "table_version": "1", "region": {"path": "../labels/label_DAPI"}, // Path to the label image associated with this feature table // Backend metadata - "backend": "annadata", // the backend used to store the table, e.g. "annadata", "parquet", etc.. + "backend": "anndata", // the backend used to store the table, e.g. "anndata", "parquet", etc.. "index_key": "label", "index_type": "int", // Either "int" or "str" + "instance_key": "label" // Mirrors index_key; identifies the label each row describes } ``` -Additionally, it can include feature type information such as: +ngio also always writes the three feature-type lists, empty when you have not set them: -```json +```json5 { "categorical_columns": [ "label", - "cell_type", + "cell_type" ], "measurement_columns": [ "area", @@ -48,7 +57,7 @@ Additionally, it can include feature type information such as: "intensity_std" ], "metadata_columns": [ - "description", - ], + "description" + ] } ``` diff --git a/docs/table_specs/table_types/generic_table.md b/docs/table_specs/table_types/generic_table.md index 66af2e97..27949cd1 100644 --- a/docs/table_specs/table_types/generic_table.md +++ b/docs/table_specs/table_types/generic_table.md @@ -1,8 +1,12 @@ -# Generic Tables +--- +description: "Generic table: the untyped table for arbitrary tabular data." +--- -A generic table is a flexible table type that can represent any tabular data. It is not tied to any specific domain or use case, making it suitable for a wide range of custom applications. +# Generic table -Generic tables can used as a safe fallback when trying to read a table that does not match any other specific table type. +A generic table is a flexible table type that can represent any tabular data. It is not tied to any specific domain or use case, which makes it suitable for a wide range of custom applications. + +Generic tables are also the safe fallback when you read a table that does not match any other table type. ## Specifications @@ -10,14 +14,18 @@ Generic tables can used as a safe fallback when trying to read a table that does A generic table should include the following metadata fields in the group attributes: -```json +```json5 { // Generic table metadata "type": "generic_table", "table_version": "1", // Backend metadata - "backend": "annadata", // the backend used to store the table, e.g. "annadata", "parquet", etc.. - "index_key": "index", // The default index key for the generic table, which is used to identify each row. - "index_type": "int" // Either "int" or "str" + "backend": "anndata", // the backend used to store the table, e.g. "anndata", "parquet", etc.. + "index_key": "my_id", // Optional. The column used as the row index. + "index_type": "int" // Optional. Either "int" or "str" } ``` + +Generic tables have no default index. `index_key` and `index_type` are written only when +you set one, so a generic table saved without an index key carries just `type`, +`table_version` and `backend`. diff --git a/docs/table_specs/table_types/masking_roi_table.md b/docs/table_specs/table_types/masking_roi_table.md index 60d5e43a..f49c9747 100644 --- a/docs/table_specs/table_types/masking_roi_table.md +++ b/docs/table_specs/table_types/masking_roi_table.md @@ -1,29 +1,34 @@ -# Masking ROI Tables +--- +description: "Masking ROI table: bounding boxes tied to the labels of a label image." +--- -A masking ROI table is a specialized table type for representing Regions of Interest (ROIs) that are associated with specific labels in a label image. +# Masking ROI table + +A masking ROI table is a specialised table type for representing Regions of Interest (ROIs) that are associated with specific labels in a label image. Each row in a masking ROI table corresponds to a specific label in the label image. -Masking ROI tables can be used for several purposes, such as: +Masking ROI tables serve several purposes, such as: - Feature extraction from specific regions in the image. -- Masking specific regions in the image for further processing. For example a masking ROI table could store the ROIs for specific tissues, and for each of these ROIs we would like to perform cell segmentation. +- Masking specific regions in the image for further processing. For example, a masking ROI table could store the ROIs for specific tissues, and you would like to perform cell segmentation within each of them. ## Specifications ### V1 -A ROI table must include the following metadata fields in the group attributes: +A masking ROI table must include the following metadata fields in the group attributes: -```json +```json5 { // ROI table metadata "type": "masking_roi_table", "table_version": "1", "region": {"path": "../labels/label_DAPI"}, // Path to the label image associated with this masking ROI table // Backend metadata - "backend": "annadata", // the backend used to store the table, e.g. "annadata", "parquet", etc.. + "backend": "anndata", // the backend used to store the table, e.g. "anndata", "parquet", etc.. "index_key": "label", // The default index key for the ROI table, which is used to identify each ROI. "index_type": "int", // Either "int" or "str" + "instance_key": "label" // Mirrors index_key; identifies the label each ROI belongs to } ``` @@ -34,4 +39,5 @@ Moreover the ROI table must include the following columns: - `label`: An integer column label associated with the ROI, which corresponds to a specific label in the label image. This can also be the table index key. - (Optional) `t_second` and `len_t_second`: the time coordinate of the ROI in seconds, and the length of the time coordinate in seconds. This is useful for multiplexing acquisitions. -Additionally, each ROI can include the following optional columns: see [ROI Table](./roi_table.md). +The optional columns of a [ROI table](./roi_table.md) — time, original coordinates, +registration translations and the plate-location columns — are recognised here too. diff --git a/docs/table_specs/table_types/roi_table.md b/docs/table_specs/table_types/roi_table.md index 00d2ac66..f42a8bbe 100644 --- a/docs/table_specs/table_types/roi_table.md +++ b/docs/table_specs/table_types/roi_table.md @@ -1,12 +1,16 @@ -# ROI Table +--- +description: "ROI table: axis-aligned bounding boxes in the image space." +--- -A ROI table defines regions of space which are axes-aligned bounding boxes in the image space. +# ROI table -ROI tables can be used for several purposes, such as: +A ROI table defines regions of space which are axis-aligned bounding boxes in the image space. -- Storing information about the Microscope Field of View (FOV). +ROI tables serve several purposes, such as: + +- Storing information about the microscope field of view (FOV). - Storing arbitrary regions of interest (ROIs). -- Use them as masks for other processes, such as segmentation or feature extraction. +- Acting as masks for other processes, such as segmentation or feature extraction. ## Specifications @@ -14,15 +18,15 @@ ROI tables can be used for several purposes, such as: A ROI table must include the following metadata fields in the group attributes: -```json +```json5 { // ROI table metadata "type": "roi_table", "table_version": "1", // Backend metadata - "backend": "annadata", // the backend used to store the table, e.g. "annadata", "parquet", etc.. + "backend": "anndata", // the backend used to store the table, e.g. "anndata", "parquet", etc.. "index_key": "FieldIndex", // The default index key for the ROI table, which is used to identify each ROI. - "index_type": "str", // Either "int" or "str" + "index_type": "str" // Either "int" or "str" } ``` @@ -36,5 +40,9 @@ Additionally, each ROI can include the following optional columns: - `t_second` and `len_t_second`: the time coordinate of the ROI in seconds, and the length of the time coordinate in seconds. This is useful for multiplexing acquisitions. - `x_micrometer_original`, `y_micrometer_original` and `z_micrometer_original` which are the original coordinates of the ROI in micrometers. These are typically used when the data is saved in different coordinates during conversion, e.g. to avoid overwriting data from overlapping ROIs. - `translation_x`, `translation_y` and `translation_z`, which are used during registration of multiplexing acquisitions. +- `plate_name`, `row`, `column`, `path_in_well`, `path_in_plate`, `acquisition_id` and `acquisition_name`, which record where in a plate the ROI came from. These matter when ROI tables from many images are concatenated into one plate-level table. +- `FieldIndex` and `label`, the two column names ngio treats as index keys. -The user can also add additional columns to the ROI table, but these columns will not be exposed in the ROI table API. +You can add further columns. They are carried through unchanged — read back as attributes +on the `Roi` object and written out again on save — but ngio does not interpret them, and +it logs a warning for each column it does not recognise. diff --git a/docs/tutorials/create_ome_zarr.ipynb b/docs/tutorials/create_ome_zarr.ipynb deleted file mode 100644 index a9dc74ae..00000000 --- a/docs/tutorials/create_ome_zarr.ipynb +++ /dev/null @@ -1,101 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# OME-Zarr Creation\n", - "\n", - "This is a minimal example of how to create an OME-Zarr image using `ngio`.\n", - "\n", - "This example is just a simple demonstration but for more complex conversion tasks please refer \n", - "to the converter tooling library [ome-zarr-converters-tools](https://github.com/BioVisionCenter/ome-zarr-converters-tools).\n", - "\n", - "Let's start by converting a sample image from `skimage` to OME-Zarr format." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1", - "metadata": {}, - "outputs": [], - "source": [ - "import skimage\n", - "from matplotlib import pyplot as plt\n", - "\n", - "plt.imshow(skimage.data.human_mitosis(), cmap=\"gray\")\n", - "plt.axis(\"off\")\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2", - "metadata": {}, - "outputs": [], - "source": [ - "from ngio import create_ome_zarr_from_array\n", - "\n", - "ome_zarr = create_ome_zarr_from_array(\n", - " store=\"./data/human_mitosis.zarr\",\n", - " array=skimage.data.human_mitosis(),\n", - " pixelsize=0.1, # Just a guess\n", - ")\n", - "ome_zarr" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Adding a ROI table to an OME-Zarr image\n", - "\n", - "Often, is useful to add ROIs to OME-Zarr images to be able to retrieve them later. \n", - "This can be done using the `ngio` library as follows." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4", - "metadata": {}, - "outputs": [], - "source": [ - "# create a roi for the whole image\n", - "roi_table = ome_zarr.build_image_roi_table(name=\"image_roi\")\n", - "ome_zarr.add_table(\"image_roi_table\", roi_table)" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "dev", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/tutorials/create_ome_zarr.md b/docs/tutorials/create_ome_zarr.md new file mode 100644 index 00000000..1cd75e81 --- /dev/null +++ b/docs/tutorials/create_ome_zarr.md @@ -0,0 +1,43 @@ +--- +description: Convert a numpy array into an OME-Zarr image and attach a ROI table. +--- + +# Create an OME-Zarr image + +**Convert a numpy array into an OME-Zarr image.** + +Convert a numpy array into an OME-Zarr image with `ngio`, then attach a ROI table to it. +By the end you will have an on-disk container that the other tutorials read from. + +For larger conversion jobs — vendor formats, multi-file acquisitions, whole plates — reach +for the converter tooling library [ome-zarr-converters-tools](https://github.com/BioVisionCenter/ome-zarr-converters-tools). + +## Step 1: convert an array to OME-Zarr + +Start by converting a sample image from `skimage` to OME-Zarr format. + +```python exec="true" session="create_ome_zarr" +--8<-- "docs/snippets/tutorials/create_ome_zarr.py:plot_helpers" +``` + +```python exec="true" html="1" source="material-block" session="create_ome_zarr" +--8<-- "docs/snippets/tutorials/create_ome_zarr.py:plot_input_image" +``` + +```python exec="true" source="material-block" session="create_ome_zarr" +--8<-- "docs/snippets/tutorials/create_ome_zarr.py:create" +``` + +## Step 2: add a ROI table + +Attaching ROIs to an OME-Zarr image lets you retrieve those regions later. Add them with +`ngio` as follows. + +```python exec="true" source="material-block" session="create_ome_zarr" +--8<-- "docs/snippets/tutorials/create_ome_zarr.py:add_roi_table" +``` + +## Next steps + +- [Image processing](image_processing.md) — process the image you just created. +- [OME-Zarr containers](../getting_started/1_ome_zarr_containers.md) — the container API in depth. diff --git a/docs/tutorials/feature_extraction.ipynb b/docs/tutorials/feature_extraction.ipynb deleted file mode 100644 index 26d04d3b..00000000 --- a/docs/tutorials/feature_extraction.ipynb +++ /dev/null @@ -1,175 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Feature Extraction\n", - "\n", - "This sections will cover how to extract regionprops features from an image using `ngio`, `skimage`. Moreover we will also write the features to a table in the ome-zarr container.\n", - "\n", - "# Step 1: Open the OME-Zarr Container" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "from skimage import measure\n", - "\n", - "\n", - "def extract_features(image: np.ndarray, label: np.ndarray) -> pd.DataFrame:\n", - " \"\"\"Basic feature extraction using skimage.measure.regionprops_table.\"\"\"\n", - " label = label.squeeze(-1) # Remove the channel axis if present\n", - " roi_feat_table = measure.regionprops_table(\n", - " label_image=label,\n", - " intensity_image=image,\n", - " properties=[\n", - " \"label\",\n", - " \"area\",\n", - " \"mean_intensity\",\n", - " \"max_intensity\",\n", - " \"min_intensity\",\n", - " ],\n", - " )\n", - " return pd.DataFrame(roi_feat_table)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "\n", - "from ngio import open_ome_zarr_container\n", - "from ngio.utils import download_ome_zarr_dataset\n", - "\n", - "# Download the dataset\n", - "download_dir = Path(\".\").absolute().parent.parent / \"data\"\n", - "hcs_path = download_ome_zarr_dataset(\"CardiomyocyteTinyMip\", download_dir=download_dir)\n", - "image_path = hcs_path / \"B\" / \"03\" / \"0\"\n", - "\n", - "# Open the ome-zarr container\n", - "ome_zarr = open_ome_zarr_container(image_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 2: Setup the inputs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from ngio.transforms import ZoomTransform\n", - "\n", - "# First we will need the image object and the FOVs table\n", - "image = ome_zarr.get_image()\n", - "\n", - "# Get the nuclei label\n", - "nuclei = ome_zarr.get_label(\"nuclei\")\n", - "\n", - "# In this example we the image is available at an higher resolution than the nuclei\n", - "print(f\"Image dimensions: {image.dimensions}, pixel size: {image.pixel_size}\")\n", - "print(f\"Nuclei dimensions: {nuclei.dimensions}, pixel size: {nuclei.pixel_size}\")\n", - "\n", - "# We need to setup a transform to resample the nuclei to the image resolution\n", - "zoom_transform = ZoomTransform(\n", - " input_image=nuclei,\n", - " target_image=image,\n", - " order=\"nearest\", # Nearest neighbor interpolation for labels\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 3: Use the FeatureExtractorIterator to create a feature table" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from ngio.experimental.iterators import FeatureExtractorIterator\n", - "from ngio.tables import FeatureTable\n", - "\n", - "iterator = FeatureExtractorIterator(\n", - " input_image=image,\n", - " input_label=nuclei,\n", - " label_transforms=[zoom_transform],\n", - " axes_order=[\"y\", \"x\", \"c\"],\n", - ")\n", - "\n", - "feat_table = []\n", - "for image_data, label_data, roi in iterator.iter_as_numpy():\n", - " print(f\"Processing ROI: {roi}\")\n", - " roi_feat_table = extract_features(image=image_data, label=label_data)\n", - " feat_table.append(roi_feat_table)\n", - "\n", - "# Concatenate all the dataframes into a single one\n", - "feat_table = pd.concat(feat_table)\n", - "feat_table = FeatureTable(table_data=feat_table, reference_label=\"nuclei\")\n", - "ome_zarr.add_table(\"nuclei_regionprops\", feat_table)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Sanity Check: Read the Table back" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ome_zarr.get_table(\"nuclei_regionprops\").lazy_frame.collect()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "dev", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/tutorials/feature_extraction.md b/docs/tutorials/feature_extraction.md new file mode 100644 index 00000000..3d4ff610 --- /dev/null +++ b/docs/tutorials/feature_extraction.md @@ -0,0 +1,53 @@ +--- +description: Extract regionprops features and store them as an ngio feature table. +--- + +# Feature extraction + +**Measure per-label features and store them as a table.** + +Measure regionprops features from a segmented image with `ngio` and `skimage`, and write +them back as a feature table in the OME-Zarr container. By the end the container holds a +table with one row per label, ready to be read back or aggregated across a plate. + +## Step 1: write the measurement function + +Start with the function that does the measuring — here a thin wrapper around +`skimage.measure.regionprops_table`, taking one image patch and one label patch. + +```python exec="true" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:extract_features" +``` + +## Step 2: open the OME-Zarr container + +```python exec="true" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:open_container" +``` + +## Step 3: set up the inputs + +```python exec="true" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:setup_transform" +``` + +## Step 4: use the FeatureExtractorIterator to create a feature table + +```python exec="true" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:extract" +``` + +### Sanity check: read the table back + +```python exec="true" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:table_helpers" +``` + +```python exec="true" html="1" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:read_table_back" +``` + +## Next steps + +- [HCS exploration](hcs_exploration.md) — aggregate feature tables across a plate. +- [Table specifications](../table_specs/overview.md) — how feature tables are stored. diff --git a/docs/tutorials/hcs_exploration.ipynb b/docs/tutorials/hcs_exploration.ipynb deleted file mode 100644 index 87daf1ad..00000000 --- a/docs/tutorials/hcs_exploration.ipynb +++ /dev/null @@ -1,135 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# HCS Plates\n", - "\n", - "This is a minimal example of how to work with OME-Zarr Plates using `ngio`.\n", - "\n", - "## Show what's in the plate" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "\n", - "from ngio import open_ome_zarr_plate\n", - "from ngio.utils import download_ome_zarr_dataset\n", - "\n", - "# Download the dataset\n", - "download_dir = Path(\".\").absolute().parent.parent / \"data\"\n", - "\n", - "hcs_path = download_ome_zarr_dataset(\"CardiomyocyteTinyMip\", download_dir=download_dir)\n", - "hcs_zarr = open_ome_zarr_plate(hcs_path)\n", - "print(hcs_zarr)\n", - "print(f\"Rows: {hcs_zarr.rows}, Columns: {hcs_zarr.columns}\")\n", - "\n", - "# Get all the images in the plate\n", - "hcs_zarr.get_images()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Aggregate tables across all images" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Aggregate all table across all images\n", - "\n", - "table = hcs_zarr.concatenate_image_tables(name=\"nuclei\")\n", - "table.dataframe" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Save the table in the HCS plate" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Save the tabvle in the HCS plate\n", - "hcs_zarr.add_table(name=\"nuclei\", table=table)\n", - "\n", - "# Read the table back for sanity check\n", - "hcs_zarr.get_table(\"nuclei\").dataframe" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create a new empty Plate" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from ngio import ImageInWellPath, create_empty_plate\n", - "\n", - "test_plate = create_empty_plate(\n", - " store=\"./data/empty_plate.zarr\",\n", - " name=\"Test Plate\",\n", - " images=[\n", - " ImageInWellPath(row=\"A\", column=\"01\", path=\"0\"),\n", - " ImageInWellPath(row=\"A\", column=\"02\", path=\"0\"),\n", - " ImageInWellPath(row=\"A\", column=\"02\", path=\"1\", acquisition_id=1),\n", - " ],\n", - " overwrite=True,\n", - ")\n", - "\n", - "print(test_plate)\n", - "print(f\"Rows: {test_plate.rows}, Columns: {test_plate.columns}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "dev", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/tutorials/hcs_exploration.md b/docs/tutorials/hcs_exploration.md new file mode 100644 index 00000000..4def7073 --- /dev/null +++ b/docs/tutorials/hcs_exploration.md @@ -0,0 +1,44 @@ +--- +description: Explore an HCS plate, aggregate tables across images, and create a new plate. +--- + +# HCS exploration + +**Explore a plate and aggregate tables across it.** + +Open an OME-Zarr plate with `ngio`, see what it contains, aggregate a table across every +image in it, and write the result back to the plate. The last section creates a new empty +plate from scratch. + +## Step 1: show what's in the plate + +```python exec="true" source="material-block" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:open_plate" +``` + +## Step 2: aggregate tables across all images + +```python exec="true" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:table_helpers" +``` + +```python exec="true" html="1" source="material-block" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:concatenate_tables" +``` + +## Step 3: save the table in the plate + +```python exec="true" html="1" source="material-block" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:save_table" +``` + +## Step 4: create a new empty plate + +```python exec="true" source="material-block" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:create_plate" +``` + +## Next steps + +- [HCS plates](../getting_started/5_hcs.md) — the plate API in depth. +- [HCS API reference](../api/hcs.md) — `OmeZarrPlate` and `OmeZarrWell`. diff --git a/docs/tutorials/image_processing.ipynb b/docs/tutorials/image_processing.ipynb deleted file mode 100644 index 9eff03c4..00000000 --- a/docs/tutorials/image_processing.ipynb +++ /dev/null @@ -1,274 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Image Processing\n", - "\n", - "This is a minimal example of how to use the `ngio` library for applying some basic image processing techniques.\n", - "\n", - "For this example we will apply gaussian blur to an image.\n", - "\n", - "\n", - "## Step 1: Setup\n", - "\n", - "We will first create a simple function to apply gaussian blur to an image. This function will take an image and a sigma value as input and return the blurred image." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import skimage\n", - "\n", - "\n", - "def gaussian_blur(image: np.ndarray, sigma: float) -> np.ndarray:\n", - " \"\"\"Apply gaussian blur to an image.\"\"\"\n", - " original_type = image.dtype\n", - " image = skimage.filters.gaussian(\n", - " image, sigma=sigma, channel_axis=0, preserve_range=True\n", - " )\n", - " # Convert the image back to the original type\n", - " image = image.astype(original_type)\n", - " return image" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 2: Open the OmeZarr container" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "\n", - "from ngio import open_ome_zarr_container\n", - "from ngio.utils import download_ome_zarr_dataset\n", - "\n", - "# Download the dataset\n", - "download_dir = Path(\".\").absolute().parent.parent / \"data\"\n", - "\n", - "hcs_path = download_ome_zarr_dataset(\"CardiomyocyteTiny\", download_dir=download_dir)\n", - "image_path = hcs_path / \"B\" / \"03\" / \"0\"\n", - "\n", - "# Open the ome-zarr container\n", - "ome_zarr = open_ome_zarr_container(image_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 3: Create a new empty omeZarr container\n", - "\n", - "ngio provide a simple way to \"derive\" a new container from an existing one. This is useful when you want to apply some processing to an image and save the results in a new container that \n", - "preserves the original metadata and dimensions (unless explicitly changed when deriving)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# First we will need the image object\n", - "image = ome_zarr.get_image()\n", - "\n", - "# Second we need to derive a new ome-zarr image where we will store\n", - "# the processed image\n", - "\n", - "blurred_omezarr_path = image_path.parent / \"0_blurred\"\n", - "blurred_omezarr = ome_zarr.derive_image(\n", - " store=blurred_omezarr_path, name=\"Blurred Image\", overwrite=True\n", - ")\n", - "blurred_image = blurred_omezarr.get_image()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 4: Apply the gaussian blur and consolidate the processed image" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can use the axes order to specify how we query the image data.\n", - "# Here we will reorder the axes to be [\"c\", \"z\", \"y\", \"x\"].\n", - "# So that it will be compatible with the gaussian blur function\n", - "# which expects the channel axis to be the first one.\n", - "image_data = image.get_as_numpy(axes_order=[\"c\", \"z\", \"y\", \"x\"])\n", - "# Apply gaussian blur to the image\n", - "sigma = 5.0\n", - "blurred_image_data = gaussian_blur(image_data, sigma=sigma)\n", - "\n", - "# Set the processed image data back to the ome-zarr image\n", - "blurred_image.set_array(patch=blurred_image_data, axes_order=[\"c\", \"z\", \"y\", \"x\"])\n", - "\n", - "# The `set_array` method only saved the blurred image to the container at a specific\n", - "# resolution level. So all other resolution levels are still empty.\n", - "# To propagate the changes to all resolution levels,\n", - "# we can use the `consolidate` method.\n", - "blurred_image.consolidate()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Plot the results\n", - "\n", - "Finally, we can visualize the original and blurred images using `matplotlib`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from matplotlib.colors import ListedColormap\n", - "\n", - "rand_cmap = np.random.rand(1000, 3)\n", - "rand_cmap[0] = 0\n", - "rand_cmap = ListedColormap(rand_cmap)\n", - "\n", - "fig, axs = plt.subplots(2, 1, figsize=(8, 4))\n", - "axs[0].set_title(\"Original image\")\n", - "axs[0].imshow(image.get_as_numpy(c=0, z=1, axes_order=[\"y\", \"x\"]), cmap=\"gray\")\n", - "axs[1].set_title(\"Blurred image\")\n", - "axs[1].imshow(blurred_image.get_as_numpy(c=0, z=1, axes_order=[\"y\", \"x\"]), cmap=\"gray\")\n", - "for ax in axs:\n", - " ax.axis(\"off\")\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 5: Out of memory processing\n", - "\n", - "Sometimes we want to apply some simple processing to larger than memory images. In this case, we can use the `dask` library to process the image in chunks. In `ngio` we can simply query the data as a `dask` array and apply the desired processing function to it." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from dask import array as da\n", - "\n", - "\n", - "def dask_gaussian_blur(image: da.Array, sigma: float) -> da.Array:\n", - " \"\"\"Apply gaussian blur to a dask array.\"\"\"\n", - " # This will itroduce some edge artifacts at chunk boundaries\n", - " # In a real application, consider using map_overlap to mitigate this\n", - " # With appropriate depth based on sigma\n", - " return da.map_blocks(gaussian_blur, image, dtype=image.dtype, sigma=sigma)\n", - "\n", - "\n", - "image_dask = image.get_as_dask(axes_order=[\"c\", \"z\", \"y\", \"x\"])\n", - "blurred_image_dask = dask_gaussian_blur(image_dask, sigma=sigma)\n", - "blurred_image_dask" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 6. Image Processing Iterators\n", - "\n", - "`ngio` provides an alternative way to process large images using iterators. This API is not meant to replace `dask` but to provide a simple way to iterate over arbitrary regions, moreover it provides a simple way to implement default broadcasting behaviors." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from ngio.experimental.iterators import ImageProcessingIterator\n", - "\n", - "iterator = ImageProcessingIterator(\n", - " input_image=image,\n", - " output_image=blurred_image,\n", - " axes_order=[\"c\", \"z\", \"y\", \"x\"],\n", - ")\n", - "\n", - "# After initializing the iterator, the iterator will have created\n", - "# will iterate over the entire image.\n", - "print(f\"Iterator after initialization: {iterator}\")\n", - "\n", - "# Iterate over an arbitrary region of interest table\n", - "# We can use the product method that performs a cartesian product\n", - "# between the iterator and the table.\n", - "table = ome_zarr.get_roi_table(\"FOV_ROI_table\")\n", - "iterator = iterator.product(table)\n", - "print(f\"Iterator after product with table: {iterator}\")\n", - "\n", - "# We can explicitly set a broadcasting behavior\n", - "# For example we can iterate over all zyx planes, and broadcast all the other\n", - "# spatial dimensions\n", - "iterator = iterator.by_zyx()\n", - "\n", - "# Finally (if needed) we can check if the regions are not-overlapping\n", - "iterator.require_no_regions_overlap()\n", - "# We can also check if the regions lay on non-overlapping chunks\n", - "iterator.require_no_chunks_overlap()\n", - "\n", - "# Now we can map the gaussian blur function to the iterator\n", - "iterator.map_as_numpy(lambda x: gaussian_blur(x, sigma=sigma))\n", - "\n", - "# No need to consolidate, the iterator takes care of that\n", - "# after all the regions have been processed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "dev", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/tutorials/image_processing.md b/docs/tutorials/image_processing.md new file mode 100644 index 00000000..15278b56 --- /dev/null +++ b/docs/tutorials/image_processing.md @@ -0,0 +1,75 @@ +--- +description: Apply a Gaussian blur eagerly, lazily with dask, and through an ngio iterator. +--- + +# Image processing + +**Apply a Gaussian blur three ways.** + +Apply a Gaussian blur to an OME-Zarr image three ways with `ngio`: eagerly on a numpy +array, lazily with `dask`, and through an ngio iterator. Along the way you derive a new +container that keeps the metadata of the original and write the processed image into it. + +## Step 1: set up + +Start with a function that applies a Gaussian blur to an image. It takes an image and a +sigma value as input, and returns the blurred image. + +```python exec="true" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:plot_helpers" +``` + +```python exec="true" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:gaussian_blur" +``` + +## Step 2: open the OME-Zarr container + +```python exec="true" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:open_container" +``` + +## Step 3: create a new empty OME-Zarr container + +ngio can "derive" a new container from an existing one. Use this when you want to apply +processing to an image and save the results in a new container that preserves the original +metadata and dimensions (unless you change them explicitly when deriving). + +```python exec="true" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:derive_image" +``` + +## Step 4: apply the Gaussian blur and consolidate the processed image + +```python exec="true" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:apply_blur" +``` + +### Plot the results + +Finally, visualise the original and blurred images with `matplotlib`. + +```python exec="true" html="1" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:plot_blur" +``` + +## Step 5: out-of-memory processing + +Some images are larger than memory. In that case, use the `dask` library to process the image in chunks: with `ngio` you query the data as a `dask` array and apply the processing function to it. + +```python exec="true" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:dask_blur" +``` + +## Step 6: image processing iterators + +`ngio` also processes large images with iterators. This API is not meant to replace `dask`: it lets you iterate over arbitrary regions, and it supplies default broadcasting behaviour. + +```python exec="true" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:iterators" +``` + +## Next steps + +- [Image segmentation](image_segmentation.md) — turn images into labels. +- [Iterators](../getting_started/6_iterators.md) — the iterator concepts behind step 6. diff --git a/docs/tutorials/image_segmentation.ipynb b/docs/tutorials/image_segmentation.ipynb deleted file mode 100644 index 21be734f..00000000 --- a/docs/tutorials/image_segmentation.ipynb +++ /dev/null @@ -1,266 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Image Segmentation\n", - "\n", - "This is a minimal tutorial on how to use ngio for image segmentation.\n", - "\n", - "## Step 1: Setup\n", - "\n", - "We will first implement a very simple function to segment an image. We will use skimage to do this. \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Setup a simple segmentation function\n", - "import numpy as np\n", - "import skimage\n", - "\n", - "\n", - "def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray:\n", - " \"\"\"Simple segmentation using Otsu thresholding.\"\"\"\n", - " threshold = skimage.filters.threshold_otsu(image)\n", - " binary = image > threshold\n", - " label_image = skimage.measure.label(binary)\n", - " label_image += max_label\n", - " label_image = np.where(binary, label_image, 0)\n", - " return label_image.astype(np.uint32)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 2: Open the OmeZarr container" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "\n", - "from ngio import open_ome_zarr_container\n", - "from ngio.utils import download_ome_zarr_dataset\n", - "\n", - "# Download the dataset\n", - "download_dir = Path(\".\").absolute().parent.parent / \"data\"\n", - "hcs_path = download_ome_zarr_dataset(\"CardiomyocyteTiny\", download_dir=download_dir)\n", - "image_path = hcs_path / \"B\" / \"03\" / \"0\"\n", - "\n", - "# Open the ome-zarr container\n", - "ome_zarr = open_ome_zarr_container(image_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 3: Segment the image\n", - "\n", - "For this example, we will not segment the image all at once. Instead we will iterate over the image FOVs and segment them one by one." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from ngio.experimental.iterators import SegmentationIterator\n", - "\n", - "# First we will need the image object and the FOVs table\n", - "image = ome_zarr.get_image()\n", - "roi_table = ome_zarr.get_roi_table(\"FOV_ROI_table\")\n", - "\n", - "# Second we need to derive a new label image to use as target for the segmentation\n", - "\n", - "label = ome_zarr.derive_label(\"new_label\", overwrite=True)\n", - "\n", - "# Setup the segmentation iterator\n", - "seg_iterator = SegmentationIterator(\n", - " input_image=image,\n", - " output_label=label,\n", - " channel_selection=\"DAPI\",\n", - " axes_order=[\"z\", \"y\", \"x\"],\n", - ")\n", - "seg_iterator = seg_iterator.product(roi_table)\n", - "\n", - "# Make sure that if other axes are present they are iterated over\n", - "seg_iterator = seg_iterator.by_zyx()\n", - "\n", - "max_label = 0 # We will use this to avoid label collisions\n", - "for image_data, label_writer in seg_iterator.iter_as_numpy():\n", - " roi_segmentation = otsu_threshold_segmentation(\n", - " image_data, max_label\n", - " ) # Segment the image\n", - "\n", - " max_label = roi_segmentation.max() # Get the max label for the next iteration\n", - "\n", - " label_writer(patch=roi_segmentation) # Write the segmentation back to the label\n", - "\n", - "# No need to consolidate, the iterator does it automatically after the last write" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Plot the segmentation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from matplotlib.colors import ListedColormap\n", - "\n", - "rand_cmap = np.random.rand(1000, 3)\n", - "rand_cmap[0] = 0\n", - "rand_cmap = ListedColormap(rand_cmap)\n", - "\n", - "fig, axs = plt.subplots(2, 1, figsize=(8, 4))\n", - "axs[0].set_title(\"Original image\")\n", - "axs[0].imshow(image.get_as_numpy(c=0, z=1, axes_order=[\"y\", \"x\"]), cmap=\"gray\")\n", - "axs[1].set_title(\"Final segmentation\")\n", - "axs[1].imshow(label.get_as_numpy(z=1, axes_order=[\"y\", \"x\"]), cmap=rand_cmap)\n", - "for ax in axs:\n", - " ax.axis(\"off\")\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 4: Masked image segmentation\n", - "\n", - "In this example we will use a mask to restrict the segmentation to certain areas of the image.\n", - "In this case we will create a simple mask for illustration purposes, but in a real case scenario the mask could come\n", - "from another segmentation mask." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a basic mask for illustration purposes\n", - "mask = ome_zarr.derive_label(\"mask\", overwrite=True)\n", - "mask_data = mask.get_as_numpy(axes_order=[\"z\", \"y\", \"x\"])\n", - "mask_data[:, 200:-200, 500:2000] = 1\n", - "mask_data[:, 200:-200, 3000:-500] = 2\n", - "mask_data[:, 600:-600, 1200:-1000] = 0\n", - "mask_data[:, 700:-700, 1600:-1500] = 3\n", - "mask.set_array(mask_data, axes_order=[\"z\", \"y\", \"x\"])\n", - "mask.consolidate()\n", - "\n", - "fig, axs = plt.subplots(2, 1, figsize=(8, 4))\n", - "axs[0].set_title(\"Original image\")\n", - "axs[0].imshow(image.get_as_numpy(c=0, z=1, axes_order=[\"y\", \"x\"]), cmap=\"gray\")\n", - "axs[1].set_title(\"Mask\")\n", - "axs[1].imshow(mask.get_as_numpy(z=1, axes_order=[\"y\", \"x\"]), cmap=rand_cmap)\n", - "for ax in axs:\n", - " ax.axis(\"off\")\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from ngio.experimental.iterators import MaskedSegmentationIterator\n", - "\n", - "# First we will need the masked image object\n", - "# (that contains the masking table information inside)\n", - "image = ome_zarr.get_masked_image(masking_label_name=\"mask\")\n", - "\n", - "# Second we need to derive a new label image to use as target for the segmentation\n", - "label = ome_zarr.derive_label(\"masked_new_label\", overwrite=True)\n", - "\n", - "# Setup the masked segmentation iterator\n", - "seg_iterator = MaskedSegmentationIterator(\n", - " input_image=image,\n", - " output_label=label,\n", - " channel_selection=\"DAPI\",\n", - " axes_order=[\"z\", \"y\", \"x\"],\n", - ")\n", - "\n", - "# Make sure that if other axes are present they are iterated over\n", - "seg_iterator = seg_iterator.by_zyx()\n", - "\n", - "max_label = 0 # We will use this to avoid label collisions\n", - "for image_data, label_writer in seg_iterator.iter_as_numpy():\n", - " roi_segmentation = otsu_threshold_segmentation(\n", - " image_data, max_label\n", - " ) # Segment the image\n", - "\n", - " max_label = roi_segmentation.max() # Get the max label for the next iteration\n", - "\n", - " label_writer(patch=roi_segmentation) # Write the segmentation back to the label\n", - "\n", - "# No need to consolidate, the iterator does it automatically after the last write" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from matplotlib.colors import ListedColormap\n", - "\n", - "fig, axs = plt.subplots(2, 1, figsize=(8, 4))\n", - "axs[0].set_title(\"Original image\")\n", - "axs[0].imshow(image.get_as_numpy(c=0, z=1, axes_order=[\"y\", \"x\"]), cmap=\"gray\")\n", - "axs[1].set_title(\"Final segmentation\")\n", - "axs[1].imshow(label.get_as_numpy(z=1, axes_order=[\"y\", \"x\"]), cmap=rand_cmap)\n", - "for ax in axs:\n", - " ax.axis(\"off\")\n", - "plt.tight_layout()\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "dev", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/tutorials/image_segmentation.md b/docs/tutorials/image_segmentation.md new file mode 100644 index 00000000..7c06f30b --- /dev/null +++ b/docs/tutorials/image_segmentation.md @@ -0,0 +1,80 @@ +--- +description: Segment an OME-Zarr image per field of view, then repeat within a mask. +--- + +# Image segmentation + +**Segment an image one field of view at a time.** + +Segment an OME-Zarr image with `ngio` and `skimage`, one field of view at a time, and +write the result back as a label. The second half repeats the segmentation inside a mask, +so it only runs where you want it to. + +## Step 1: set up + +Start with a function that segments an image, using `skimage` to do the work. + +```python exec="true" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:plot_helpers" +``` + +```python exec="true" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:segmentation_fn" +``` + +## Step 2: open the OME-Zarr container + +```python exec="true" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:open_container" +``` + +## Step 3: segment the image + +Rather than segmenting the image all at once, iterate over its FOVs and segment them one by one. + +```python exec="true" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:segment" +``` + +### Plot the segmentation + +```python exec="true" html="1" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:plot_segmentation" +``` + +## Step 4: masked image segmentation + +Now use a mask to restrict the segmentation to certain areas of the image. Here you create +the mask by hand for illustration, but in a real pipeline it would usually come from +another segmentation. + +```python exec="true" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:create_mask" +``` + +```python exec="true" html="1" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:plot_mask" +``` + +Note that the next step rebinds `image` to the *masked* image, so the plot below shows +the masked image rather than the original one. + +```python exec="true" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:masked_segment" +``` + +```python exec="true" html="1" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:plot_masked_segmentation" +``` + +## Next steps + +- [Feature extraction](feature_extraction.md) — measure the objects you just segmented. +- [Masked images and labels](../getting_started/4_masked_images.md) — read data object-by-object. + +## Beyond the tutorials + +The [ngio workshop](https://github.com/BioVisionCenter/ngio-workshop) has hands-on marimo +notebooks covering containers, images, labels and tables, and the processing iterators. Run +them locally with `uv`, in the browser via molab, or read them as +[static pages](https://biovisioncenter.github.io/ngio-workshop/). diff --git a/mkdocs.yml b/mkdocs.yml index f0c88055..0c3ec10a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,60 +1,98 @@ -site_name: "NGIO: Next Generation File Format I/O" -site_url: "https://github.com/BioVisionCenter/ngio.git" -site_description: "A Python library for processing OME-Zarr images" +# The name is an acronym: n(ext) g(eneration file format) IO. Lowercased, because the +# project styles itself lowercase everywhere else. What the library actually does is +# spelled out in `site_description` and on the landing page. +site_name: "ngio: next generation file format IO" +# The published docs root (mike serves versions beneath it, e.g. /ngio/stable/). +# This was previously the git clone URL, which produced wrong canonical links and, under +# Zensical, a bogus /BioVisionCenter/ngio.git/ base path when serving locally. +site_url: "https://biovisioncenter.github.io/ngio/" +site_description: "A Python library for OME-Zarr bioimage analysis" repo_name: "ngio" repo_url: "https://github.com/BioVisionCenter/ngio" -copyright: "Copyright © 2024-, BioVisionCenter UZH" +copyright: "Copyright © 2024-2026, BioVisionCenter UZH" +# GitHub's default branch here is `main`. Zensical would otherwise derive +# `edit/master/docs` (config.py:439-453) and every edit link would 404. +edit_uri: edit/main/docs/ + +# The snippet scripts are executed into the docs via pymdownx.snippets, which reads them +# from the filesystem rather than from the page tree. +# NOTE: Zensical does not honour `exclude_docs`, so the .py files are still copied into +# the built site as static assets (they are not rendered as pages). Harmless — they are +# already public in the repo — but that is why they show up under site/snippets/. +exclude_docs: | + snippets/ theme: name: material - favicon: images/favicon.ico - #logo: logos/logo_white.png + logo: assets/logo.svg + favicon: assets/logo.svg icon: repo: fontawesome/brands/github + # Space Grotesk (display face) is loaded from ngio.css; theme.font only + # manages the text and code families. + font: + text: IBM Plex Sans + code: JetBrains Mono + # The real brand palette lives in stylesheets/ngio.css as variable overrides; + # config only picks the nearest stock name so the toggle behaves. palette: # Palette toggle for light mode - scheme: default - # primary: green + primary: teal + accent: teal toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - scheme: slate - # primary: teal - # accent: light-green + primary: teal + accent: teal toggle: icon: material/brightness-4 name: Switch to light mode + # Only flags Zensical's templates actually read are listed. Previously enabled but + # absent from zensical/templates/, i.e. dead config: navigation.instant, toc.follow, + # search.suggest, search.share. features: - - content.tooltips - - content.tabs.link + - announce.dismiss + - content.action.edit + - content.action.view - content.code.annotate - - navigation.instant + - content.tabs.link + - content.tooltips + - navigation.footer + - navigation.indexes - navigation.instant.progress - - navigation.sections - navigation.path - - navigation.indexes - - navigation.footer - #- toc.integrate - - toc.follow - - search.suggest - - search.share + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + +extra_css: + - stylesheets/ngio.css extra: + homepage: https://biovisioncenter.github.io/ngio/ version: provider: mike social: - icon: fontawesome/brands/github link: "https://github.com/BioVisionCenter/ngio" - name: NGIO on GitHub + name: ngio on GitHub + - icon: fontawesome/brands/python + link: "https://pypi.org/project/ngio/" + name: ngio on PyPI + - icon: fontawesome/solid/microscope + link: "https://www.biovisioncenter.uzh.ch/en.html" + name: BioVisionCenter, University of Zurich plugins: - search - autorefs - markdown-exec - - include-markdown - mkdocstrings: handlers: python: @@ -83,57 +121,93 @@ plugins: signature_crossrefs: true show_symbol_type_heading: true show_symbol_type_toc: true - - git-revision-date-localized: - enable_creation_date: true - - git-committers: - repository: BioVisionCenter/ngio - branch: main - - mkdocs-jupyter: - execute: true +# IMPORTANT: declaring this key REPLACES Zensical's ~30 defaults rather than extending +# them (zensical/config.py:656 — `config.get("markdown_extensions", DEFAULT_...)`), so +# everything we rely on must be listed here explicitly. Omitting `tables` is what once +# made pipe tables render as literal `|---|` text; omitting `attr_list` silently broke +# `content.code.annotate`, and omitting `abbr` broke `content.tooltips`. +# Deliberately NOT restored from Zensical's defaults: +# pymdownx.smartsymbols — rewrites `<--` to an arrow, and every snippet include +# contains `--8<--`; not worth the risk for the gain. +# pymdownx.caret / tilde — turn `^x^` / `~x~` into super/subscript, a latent trap in +# prose about array axes and dtypes. +# pymdownx.magiclink — changes how bare URLs render across existing pages. +# pymdownx.arithmatex — no maths in these docs. markdown_extensions: + - abbr # glossary tooltips (with content.tooltips) - admonition - - pymdownx.details - - pymdownx.superfences + - attr_list # grid cards, code annotations, link attributes + - def_list + - footnotes - md_in_html + - tables + - pymdownx.details + # NB: zensical.extensions.emoji, not material.extensions.emoji — mkdocs-material is + # not installed since the Zensical migration. + - pymdownx.emoji: + emoji_generator: !!python/name:zensical.extensions.emoji.to_svg + emoji_index: !!python/name:zensical.extensions.emoji.twemoji + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.snippets: + base_path: ["."] + check_paths: true + auto_append: + - docs/_glossary.md + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid - pymdownx.tabbed: alternate_style: true + combine_header_slug: true + - pymdownx.tasklist: + custom_checkbox: true - toc: permalink: true toc_depth: 3 +# Nav labels are sentence case, like every other heading and title on the site. +# Initialisms (OME-Zarr, HCS, ROI, API) keep their capitals; the common nouns +# beside them do not. "Code of Conduct" is the exception: that is the proper +# name of the Contributor Covenant document, not a description. nav: - - "NGIO: Streamlined OME-Zarr Image Analysis": index.md - - Getting Started: - - getting_started/0_quickstart.md - - getting_started/1_ome_zarr_containers.md - - getting_started/2_images.md - - getting_started/3_tables.md - - getting_started/4_masked_images.md - - getting_started/5_hcs.md - - getting_started/6_iterators.md - - getting_started/7_configuration.md + - Home: index.md + - Getting started: + - "Quickstart": getting_started/0_quickstart.md + - "OME-Zarr containers": getting_started/1_ome_zarr_containers.md + - "Images and labels": getting_started/2_images.md + - "Tables": getting_started/3_tables.md + - "Masked images and labels": getting_started/4_masked_images.md + - "HCS plates": getting_started/5_hcs.md + - "Iterators": getting_started/6_iterators.md + - "Configuration": getting_started/7_configuration.md - Tutorials: - - tutorials/create_ome_zarr.ipynb - - tutorials/image_processing.ipynb - - tutorials/image_segmentation.ipynb - - tutorials/feature_extraction.ipynb - - tutorials/hcs_exploration.ipynb + - "Create an OME-Zarr image": tutorials/create_ome_zarr.md + - "Image processing": tutorials/image_processing.md + - "Image segmentation": tutorials/image_segmentation.md + - "Feature extraction": tutorials/feature_extraction.md + - "HCS exploration": tutorials/hcs_exploration.md - - Table Specifications: + - Table specifications: - "Overview": table_specs/overview.md - - "Table Backends": table_specs/backend.md - - "Table Types": - - "Generic Table": table_specs/table_types/generic_table.md - - "ROI Table": table_specs/table_types/roi_table.md - - "Masking ROI Table": table_specs/table_types/masking_roi_table.md - - "Feature Table": table_specs/table_types/feature_table.md - - "Condition Table": table_specs/table_types/condition_table.md - - "Add Custom Table": table_specs/table_types/custom_table.md + - "Table backends": table_specs/backend.md + - "Table types": + - "Generic table": table_specs/table_types/generic_table.md + - "ROI table": table_specs/table_types/roi_table.md + - "Masking ROI table": table_specs/table_types/masking_roi_table.md + - "Feature table": table_specs/table_types/feature_table.md + - "Condition table": table_specs/table_types/condition_table.md + - "Add a custom table": table_specs/table_types/custom_table.md - - API Reference: + - API reference: - "OmeZarrContainer": api/ome_zarr_container.md - "Images": api/images.md - "HCS": api/hcs.md @@ -143,7 +217,7 @@ nav: - "ngio": api/ngio/ngio.md - "ngio.common": api/ngio/common.md - "ngio.io_pipes": api/ngio/io_pipes.md - - "ngio.iterators": api/ngio/iterators.md + - "ngio.experimental.iterators": api/ngio/iterators.md - "ngio.images": api/ngio/images.md - "ngio.tables": api/ngio/tables.md - "ngio.hcs": api/ngio/hcs.md @@ -152,5 +226,5 @@ nav: - changelog.md - Contributing: - - "Contributing Guide": contributing.md + - "Contributing guide": contributing.md - "Code of Conduct": code_of_conduct.md diff --git a/pixi.lock b/pixi.lock index a91041a7..734e4618 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1144,13 +1144,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: ./ + - pypi: git+https://github.com/squidfunk/mike.git#2d4ad799442f4592db8ad53b179bfb33db8c69ac - pypi: https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl @@ -1158,60 +1157,38 @@ environments: - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/5b/058db09c45ba58a7321bdf2294cae651b37d6fec68117265af90cde043b0/legacy_api_wrap-1.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/53/f4/f0b4f9ba7ec14a7af8151f3ad71ecfe3561e6ba38cfab1db3681ba4ca112/matplotlib-3.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/57/d0/cbe85158dc091219fd5134bf6d724d30b1f2005ee1d0dabaaa41416bee78/mkdocs_git_revision_date_localized_plugin-1.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/fc/7650df09dce0edfbaa1f2898c5eb0e0523ae5964dadb83855beb52166505/ome_zarr_models-1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/10/ffb85fa380bc9c9000dc35f40f44954dde49023018501c54faab94b3a39e/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5e/d7/6893c9c2a52e4bcbeca2a2bf2aee970a686cc7bf555f97db13b00f35250e/session_info2-0.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/e9/e2ae007456069dbe01865c69a4203a7ada6f7e337b78fc2f12e51bd3fae7/jupytext-1.19.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -1219,36 +1196,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/80/ab/11a76c1e2126084fde2639514f24e6111b789b0bfa4fc6264a8975c7e1f1/zict-3.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/84/c0109c5991b89cbf028b8597f73fa7bd6a6b092407be2369a354b52737ab/mkdocs_include_markdown_plugin-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/86/16/1a8fd2b19544b84575cf84ef7aa3ad4c173b756d5f087c91f85d1b295777/array_api_compat-1.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a2/d3/e0ce94d247ccc5240c6433f44155c3acdc54e812418f7d88c596bde845a3/anndata-0.12.18-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b6/1ae53367e28b9cffa3be7574e13fbe4589694272fd47710fbdbafd3d63c6/pymdown_extensions-11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/62/674ba5ada6de5615b458d768c0f0fb1a7b53299486fb38a36e0694f3c617/distributed-2026.6.0-py3-none-any.whl @@ -1260,16 +1220,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/bd/3e/9dad188e00d08f5381b95dd1f8f7b1dc9eb244250d93fea3e9cb7d0e61f0/scverse_misc-0.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/6a/edd939cc6fa04b6415aaa9bf19720fc74ead81234b3d38542e0005816d4d/polars-1.42.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/73/aef4aaf16b8d785762e2b14cf321a9178cc84a1bf4c40f58320f499a2d65/wcmatch-10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl @@ -1279,27 +1231,22 @@ environments: - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/17/8306a0bcd8c88d7761c2e73e831b0be026cd6873ce1f12beb3b4c9a03ffa/pygments_ansi_color-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ea/22/7b684ddb01b423b79eaba9726954bbe559540d510abc7a72a84d8eee1b26/markdown_exec-1.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/6f/91bbf78f704d5fd4c0c9be27d6bce3b6e4c2c339e4dcd6e7cf19ecda643c/zensical-0.0.51-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda @@ -1315,14 +1262,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - pypi: ./ + - pypi: git+https://github.com/squidfunk/mike.git#2d4ad799442f4592db8ad53b179bfb33db8c69ac - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/8e/4f8296fcfd1347f1351342fecf13bf2430d7efbae2f1f45964ec7930a99e/polars_runtime_32-1.42.1-cp310-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl @@ -1332,96 +1277,58 @@ environments: - pypi: https://files.pythonhosted.org/packages/0e/f4/93a623ab5d2e1c20de0b43e60d6bd8df7704b39e88f5424b4191391a40d9/pydantic_zarr-0.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/5b/058db09c45ba58a7321bdf2294cae651b37d6fec68117265af90cde043b0/legacy_api_wrap-1.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/57/d0/cbe85158dc091219fd5134bf6d724d30b1f2005ee1d0dabaaa41416bee78/mkdocs_git_revision_date_localized_plugin-1.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5b/fc/7650df09dce0edfbaa1f2898c5eb0e0523ae5964dadb83855beb52166505/ome_zarr_models-1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5e/92/044f1de43901310202f4c79acf4f141be53b2ca8d8380e2fcefb3d523a75/matplotlib-3.11.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5e/d7/6893c9c2a52e4bcbeca2a2bf2aee970a686cc7bf555f97db13b00f35250e/session_info2-0.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/e9/e2ae007456069dbe01865c69a4203a7ada6f7e337b78fc2f12e51bd3fae7/jupytext-1.19.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/ab/11a76c1e2126084fde2639514f24e6111b789b0bfa4fc6264a8975c7e1f1/zict-3.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/84/c0109c5991b89cbf028b8597f73fa7bd6a6b092407be2369a354b52737ab/mkdocs_include_markdown_plugin-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/86/16/1a8fd2b19544b84575cf84ef7aa3ad4c173b756d5f087c91f85d1b295777/array_api_compat-1.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/35/b0d96f58253514cb3d08f5779020ab01ee5472334fb984b92e3fc9e9c9ac/zensical-0.0.51-cp310-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a2/d3/e0ce94d247ccc5240c6433f44155c3acdc54e812418f7d88c596bde845a3/anndata-0.12.18-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/af/b6/1ae53367e28b9cffa3be7574e13fbe4589694272fd47710fbdbafd3d63c6/pymdown_extensions-11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/62/674ba5ada6de5615b458d768c0f0fb1a7b53299486fb38a36e0694f3c617/distributed-2026.6.0-py3-none-any.whl @@ -1430,18 +1337,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/bd/3e/9dad188e00d08f5381b95dd1f8f7b1dc9eb244250d93fea3e9cb7d0e61f0/scverse_misc-0.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/6a/edd939cc6fa04b6415aaa9bf19720fc74ead81234b3d38542e0005816d4d/polars-1.42.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/73/aef4aaf16b8d785762e2b14cf321a9178cc84a1bf4c40f58320f499a2d65/wcmatch-10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl @@ -1453,19 +1352,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/17/8306a0bcd8c88d7761c2e73e831b0be026cd6873ce1f12beb3b4c9a03ffa/pygments_ansi_color-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/22/7b684ddb01b423b79eaba9726954bbe559540d510abc7a72a84d8eee1b26/markdown_exec-1.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl @@ -1489,11 +1382,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda - pypi: ./ + - pypi: git+https://github.com/squidfunk/mike.git#2d4ad799442f4592db8ad53b179bfb33db8c69ac - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl @@ -1501,55 +1393,34 @@ environments: - pypi: https://files.pythonhosted.org/packages/0f/5b/af02c417954f46e5c7bd5163ac251f535877d909fce54861c99ae197f6f6/numcodecs-0.16.5-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/85/aa827c244ed4f404e99a91c3ecf5e5adb62eca806a9e9c8e3333bbad8660/zensical-0.0.51-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/5b/058db09c45ba58a7321bdf2294cae651b37d6fec68117265af90cde043b0/legacy_api_wrap-1.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/57/d0/cbe85158dc091219fd5134bf6d724d30b1f2005ee1d0dabaaa41416bee78/mkdocs_git_revision_date_localized_plugin-1.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/fc/7650df09dce0edfbaa1f2898c5eb0e0523ae5964dadb83855beb52166505/ome_zarr_models-1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/d7/6893c9c2a52e4bcbeca2a2bf2aee970a686cc7bf555f97db13b00f35250e/session_info2-0.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/94/6660de2f2d7bf388f229335ba4637646eebabdbf38564cb439a95a9193c9/debugpy-1.8.21-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/e9/e2ae007456069dbe01865c69a4203a7ada6f7e337b78fc2f12e51bd3fae7/jupytext-1.19.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl @@ -1557,34 +1428,20 @@ environments: - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/85/84/c0109c5991b89cbf028b8597f73fa7bd6a6b092407be2369a354b52737ab/mkdocs_include_markdown_plugin-7.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/86/16/1a8fd2b19544b84575cf84ef7aa3ad4c173b756d5f087c91f85d1b295777/array_api_compat-1.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/d3/e0ce94d247ccc5240c6433f44155c3acdc54e812418f7d88c596bde845a3/anndata-0.12.18-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/af/b6/1ae53367e28b9cffa3be7574e13fbe4589694272fd47710fbdbafd3d63c6/pymdown_extensions-11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/62/674ba5ada6de5615b458d768c0f0fb1a7b53299486fb38a36e0694f3c617/distributed-2026.6.0-py3-none-any.whl @@ -1594,22 +1451,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/bd/3e/9dad188e00d08f5381b95dd1f8f7b1dc9eb244250d93fea3e9cb7d0e61f0/scverse_misc-0.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/6a/edd939cc6fa04b6415aaa9bf19720fc74ead81234b3d38542e0005816d4d/polars-1.42.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/9f/970fcbf381e82ec66fdf5da8ea76e2e9240f61a24011ce9fd1d42c37ac2d/matplotlib-3.11.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d1/0e/51db89361668fe077a835fc579277f824ba526e7daf7b94d23d25439e0d0/polars_runtime_32-1.42.1-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d4/73/aef4aaf16b8d785762e2b14cf321a9178cc84a1bf4c40f58320f499a2d65/wcmatch-10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl @@ -1621,20 +1471,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/17/8306a0bcd8c88d7761c2e73e831b0be026cd6873ce1f12beb3b4c9a03ffa/pygments_ansi_color-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/22/7b684ddb01b423b79eaba9726954bbe559540d510abc7a72a84d8eee1b26/markdown_exec-1.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl @@ -4999,19 +4842,14 @@ packages: - griffe-typingdoc ; extra == 'docs' - markdown-exec[ansi] ; extra == 'docs' - matplotlib ; extra == 'docs' - - mike ; extra == 'docs' - mkdocs-autorefs ; extra == 'docs' - - mkdocs-git-committers-plugin-2 ; extra == 'docs' - - mkdocs-git-revision-date-localized-plugin ; extra == 'docs' - - mkdocs-include-markdown-plugin ; extra == 'docs' - - mkdocs-jupyter ; extra == 'docs' - - mkdocs-material ; extra == 'docs' - - mkdocs<2.0 ; extra == 'docs' - mkdocstrings[python] ; extra == 'docs' + - pymdown-extensions ; extra == 'docs' - rich ; extra == 'docs' - ruff ; extra == 'docs' - scikit-image ; extra == 'docs' - tabulate ; extra == 'docs' + - zensical ; extra == 'docs' - aiomoto[pandas] ; extra == 'test' - boto3 ; extra == 'test' - devtools ; extra == 'test' @@ -5021,6 +4859,17 @@ packages: - pytest-xdist ; extra == 'test' - scikit-image ; extra == 'test' requires_python: '>=3.11,<3.15' +- pypi: git+https://github.com/squidfunk/mike.git#2d4ad799442f4592db8ad53b179bfb33db8c69ac + name: mike + version: 2.2.0+zensical.0.1.0 + requires_dist: + - jinja2>=2.7 + - pyparsing>=3.0 + - verspec + - zensical>=0.0.30 + - ruff ; extra == 'dev' + - shtab ; extra == 'dev' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl name: graphql-core version: 3.2.11 @@ -5683,18 +5532,6 @@ packages: - cryptography>=45.0.1 - pycryptodome ; extra == 'drafts' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - name: mkdocs-jupyter - version: 0.26.3 - sha256: cd6644fb578131157194d750fd4d10fc2fd8f1e84e00036ee62df3b5b4b84c82 - requires_dist: - - ipykernel>6.0.0,<8 - - jupytext>1.13.8,<2 - - mkdocs-material>9.0.0 - - mkdocs>=1.4.0,<2 - - nbconvert>=7.2.9,<8 - - pygments>2.12.0 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: propcache version: 0.5.2 @@ -6265,6 +6102,20 @@ packages: - typing-extensions>=4.13.2 ; python_full_version < '3.11' - bcrypt>=3.1.5 ; extra == 'ssh' requires_python: '!=3.9.0,>=3.9,!=3.9.1' +- pypi: https://files.pythonhosted.org/packages/1f/85/aa827c244ed4f404e99a91c3ecf5e5adb62eca806a9e9c8e3333bbad8660/zensical-0.0.51-cp310-abi3-win_amd64.whl + name: zensical + version: 0.0.51 + sha256: 12529d3d3991b63820952111dc1d1edc29b2c9b3a3abb16c243bcb649631ebf2 + requires_dist: + - click>=8.1.8 + - deepmerge>=2.0 + - jinja2>=3.1 + - markdown>=3.7 + - pygments>=2.20 + - pymdown-extensions>=10.21.3 + - pyyaml>=6.0.2 + - tomli>=2.4.0 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl name: cryptography version: 49.0.0 @@ -6274,28 +6125,6 @@ packages: - typing-extensions>=4.13.2 ; python_full_version < '3.11' - bcrypt>=3.1.5 ; extra == 'ssh' requires_python: '!=3.9.0,>=3.9,!=3.9.1' -- pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - name: gitpython - version: 3.1.50 - sha256: d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 - requires_dist: - - gitdb>=4.0.1,<5 - - typing-extensions>=3.10.0.2 ; python_full_version < '3.10' - - coverage[toml] ; extra == 'test' - - ddt>=1.1.1,!=1.4.3 ; extra == 'test' - - mock ; python_full_version < '3.8' and extra == 'test' - - mypy==1.18.2 ; python_full_version >= '3.9' and extra == 'test' - - pre-commit ; extra == 'test' - - pytest>=7.3.1 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-instafail ; extra == 'test' - - pytest-mock ; extra == 'test' - - pytest-sugar ; extra == 'test' - - typing-extensions ; python_full_version < '3.11' and extra == 'test' - - sphinx>=7.4.7,<8 ; extra == 'doc' - - sphinx-rtd-theme ; extra == 'doc' - - sphinx-autodoc-typehints ; extra == 'doc' - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: aiohttp version: 3.14.1 @@ -6844,30 +6673,6 @@ packages: version: 1.8.0 sha256: f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - name: mkdocs-material - version: 9.7.6 - sha256: 71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba - requires_dist: - - babel>=2.10 - - backrefs>=5.7.post1 - - colorama>=0.4 - - jinja2>=3.1 - - markdown>=3.2 - - mkdocs-material-extensions>=1.3 - - mkdocs>=1.6,<2 - - paginate>=0.5 - - pygments>=2.16 - - pymdown-extensions>=10.2 - - requests>=2.30 - - mkdocs-git-committers-plugin-2>=1.1 ; extra == 'git' - - mkdocs-git-revision-date-localized-plugin>=1.2.4 ; extra == 'git' - - cairosvg>=2.6 ; extra == 'imaging' - - pillow>=10.2 ; extra == 'imaging' - - mkdocs-minify-plugin>=0.7 ; extra == 'recommended' - - mkdocs-redirects>=1.2 ; extra == 'recommended' - - mkdocs-rss-plugin>=1.6 ; extra == 'recommended' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl name: virtualenv version: 21.5.1 @@ -7558,6 +7363,11 @@ packages: version: 0.5.2 sha256: 44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl + name: tomli + version: 2.4.1 + sha256: 5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/42/6f/286e3e49d3b6b181473fefa5d9fc02e10d98ccc417e0de74e396db951fd9/aws_sam_translator-1.110.0-py3-none-any.whl name: aws-sam-translator version: 1.110.0 @@ -7706,6 +7516,11 @@ packages: version: 3.4.7 sha256: 92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: tomli + version: 2.4.1 + sha256: 5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/49/55/5bb1a2d918e9f02f131e47a59032bae70e48050e986e941511fd737a935c/flask_cors-6.0.5-py3-none-any.whl name: flask-cors version: 6.0.5 @@ -7933,6 +7748,22 @@ packages: version: 1.2.1 sha256: d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl + name: deepmerge + version: 2.1.0 + sha256: 8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75 + requires_dist: + - typing-extensions ; python_full_version < '3.10' + - validate-pyproject[all] ; extra == 'dev' + - pyupgrade ; extra == 'dev' + - black ; extra == 'dev' + - mypy ; extra == 'dev' + - pytest ; extra == 'dev' + - build ; extra == 'dev' + - twine ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl name: h5py version: 3.16.0 @@ -8070,16 +7901,6 @@ packages: version: 1.2.1 sha256: 5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/57/d0/cbe85158dc091219fd5134bf6d724d30b1f2005ee1d0dabaaa41416bee78/mkdocs_git_revision_date_localized_plugin-1.5.3-py3-none-any.whl - name: mkdocs-git-revision-date-localized-plugin - version: 1.5.3 - sha256: cd96e432de6a7e59b31c7041574b22f84179c8636835419ff458877ecfaaaf05 - requires_dist: - - babel>=2.7.0 - - gitpython>=3.1.44 - - mkdocs>=1.0,<2 - - tzdata>=2023.3 ; sys_platform == 'win32' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: rpds-py version: 2026.6.3 @@ -8321,11 +8142,6 @@ packages: - moto[wafv2]>=5.1.5,<=5.2.1 ; extra == 'wafv2' - moto[xray]>=5.1.5,<=5.2.1 ; extra == 'xray' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl - name: mkdocs-material-extensions - version: 1.3.1 - sha256: adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/5b/5b/d65adfbd02f32212431bca1f06d1e2eb763a20b12978b454bafaf23dacb7/regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: regex version: 2026.6.28 @@ -9027,6 +8843,11 @@ packages: - importlib-metadata ; extra == 'test-extras' - crc32c ; extra == 'test-extras' requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl + name: tomli + version: 2.4.1 + sha256: 4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl name: mkdocstrings version: 1.0.4 @@ -9042,84 +8863,6 @@ packages: - mkdocstrings-python-legacy>=0.2.1 ; extra == 'python-legacy' - mkdocstrings-python>=1.16.2 ; extra == 'python' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/6e/e9/e2ae007456069dbe01865c69a4203a7ada6f7e337b78fc2f12e51bd3fae7/jupytext-1.19.4-py3-none-any.whl - name: jupytext - version: 1.19.4 - sha256: 032d4ef4bd2e96addcac780b9b1d6b5a266ca39beceaaca95bfb4f06e0b77029 - requires_dist: - - markdown-it-py>=1.0 - - mdit-py-plugins - - nbformat - - packaging - - pyyaml - - tomli ; python_full_version < '3.11' - - autopep8 ; extra == 'dev' - - black ; extra == 'dev' - - flake8 ; extra == 'dev' - - gitpython ; extra == 'dev' - - ipykernel ; extra == 'dev' - - isort ; extra == 'dev' - - jupyter-fs[fs]>=1.0 ; extra == 'dev' - - jupyter-server!=2.11 ; extra == 'dev' - - marimo>=0.17.6,<=0.19.4 ; extra == 'dev' - - nbconvert ; extra == 'dev' - - pre-commit ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-asyncio ; extra == 'dev' - - pytest-cov>=2.6.1 ; extra == 'dev' - - pytest-randomly ; extra == 'dev' - - pytest-xdist ; extra == 'dev' - - sphinx ; extra == 'dev' - - sphinx-gallery>=0.8 ; extra == 'dev' - - myst-parser ; extra == 'docs' - - sphinx ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - - pytest ; extra == 'test' - - pytest-asyncio ; extra == 'test' - - pytest-randomly ; extra == 'test' - - pytest-xdist ; extra == 'test' - - black ; extra == 'test-cov' - - ipykernel ; extra == 'test-cov' - - jupyter-server!=2.11 ; extra == 'test-cov' - - nbconvert ; extra == 'test-cov' - - pytest ; extra == 'test-cov' - - pytest-asyncio ; extra == 'test-cov' - - pytest-cov>=2.6.1 ; extra == 'test-cov' - - pytest-randomly ; extra == 'test-cov' - - pytest-xdist ; extra == 'test-cov' - - autopep8 ; extra == 'test-external' - - black ; extra == 'test-external' - - flake8 ; extra == 'test-external' - - gitpython ; extra == 'test-external' - - ipykernel ; extra == 'test-external' - - isort ; extra == 'test-external' - - jupyter-fs[fs]>=1.0 ; extra == 'test-external' - - jupyter-server!=2.11 ; extra == 'test-external' - - marimo>=0.17.6,<=0.19.4 ; extra == 'test-external' - - nbconvert ; extra == 'test-external' - - pre-commit ; extra == 'test-external' - - pytest ; extra == 'test-external' - - pytest-asyncio ; extra == 'test-external' - - pytest-randomly ; extra == 'test-external' - - pytest-xdist ; extra == 'test-external' - - sphinx ; extra == 'test-external' - - sphinx-gallery>=0.8 ; extra == 'test-external' - - black ; extra == 'test-functional' - - pytest ; extra == 'test-functional' - - pytest-asyncio ; extra == 'test-functional' - - pytest-randomly ; extra == 'test-functional' - - pytest-xdist ; extra == 'test-functional' - - black ; extra == 'test-integration' - - ipykernel ; extra == 'test-integration' - - jupyter-server!=2.11 ; extra == 'test-integration' - - nbconvert ; extra == 'test-integration' - - pytest ; extra == 'test-integration' - - pytest-asyncio ; extra == 'test-integration' - - pytest-randomly ; extra == 'test-integration' - - pytest-xdist ; extra == 'test-integration' - - bash-kernel ; extra == 'test-ui' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl name: frozenlist version: 1.8.0 @@ -9365,27 +9108,6 @@ packages: - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - name: mike - version: 2.2.0 - sha256: e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040 - requires_dist: - - jinja2>=2.7 - - mkdocs~=1.0 - - pyparsing>=3.0 - - pyyaml>=5.1 - - pyyaml-env-tag - - verspec - - importlib-metadata ; python_full_version < '3.10' - - importlib-resources ; python_full_version < '3.10' - - coverage ; extra == 'dev' - - flake8-quotes ; extra == 'dev' - - flake8>=3.0 ; extra == 'dev' - - shtab ; extra == 'dev' - - coverage ; extra == 'test' - - flake8-quotes ; extra == 'test' - - flake8>=3.0 ; extra == 'test' - - shtab ; extra == 'test' - pypi: https://files.pythonhosted.org/packages/76/93/32f1bfca6cdd34259c8af42820a034b7a28dfb44969a13ed38c17e0ba5b0/coverage-7.14.3-cp311-cp311-macosx_11_0_arm64.whl name: coverage version: 7.14.3 @@ -9905,15 +9627,6 @@ packages: version: 2026.6.3 sha256: 0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/85/84/c0109c5991b89cbf028b8597f73fa7bd6a6b092407be2369a354b52737ab/mkdocs_include_markdown_plugin-7.3.0-py3-none-any.whl - name: mkdocs-include-markdown-plugin - version: 7.3.0 - sha256: 5b5c99b5d3c9b9ce0114a9e60353bbafb6be53a26c2d3b74ec6b767a7a8e55ca - requires_dist: - - mkdocs>=1.4 - - wcmatch - - platformdirs ; extra == 'cache' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl name: s3transfer version: 0.17.1 @@ -10333,15 +10046,6 @@ packages: - xlsxwriter>=3.0.5 ; extra == 'all' - zstandard>=0.19.0 ; extra == 'all' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl - name: mkdocs-git-committers-plugin-2 - version: 2.5.0 - sha256: 1778becf98ccdc5fac809ac7b62cf01d3c67d6e8432723dffbb823307d1193c4 - requires_dist: - - mkdocs>=1.0.3 - - requests - - gitpython - requires_python: '>=3.8,<4' - pypi: https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl name: scikit-image version: 0.26.0 @@ -10427,14 +10131,6 @@ packages: - wrapt>=1.10.10,<3.0.0 - httpx>=0.25.1,<0.29 ; extra == 'httpx' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - name: paginate - version: 0.5.7 - sha256: b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591 - requires_dist: - - pytest ; extra == 'dev' - - tox ; extra == 'dev' - - black ; extra == 'lint' - pypi: https://files.pythonhosted.org/packages/90/c0/5467967d95378b2cfce312e09cbd0c9ab64354a0922379b734f793edd04f/openapi_schema_validator-0.9.0-py3-none-any.whl name: openapi-schema-validator version: 0.9.0 @@ -11145,13 +10841,6 @@ packages: version: 2.5.0 sha256: 39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03 requires_python: '>=3.12' -- pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - name: gitdb - version: 4.0.12 - sha256: 67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf - requires_dist: - - smmap>=3.0.1,<6 - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl name: nest-asyncio version: 1.6.0 @@ -11211,6 +10900,20 @@ packages: - pytest>=7.1.0 ; extra == 'dev' - hypothesis>=6.70.0 ; extra == 'dev' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a2/35/b0d96f58253514cb3d08f5779020ab01ee5472334fb984b92e3fc9e9c9ac/zensical-0.0.51-cp310-abi3-macosx_11_0_arm64.whl + name: zensical + version: 0.0.51 + sha256: e97ab39668ae3b452c550634e921a0336443743aae5e1fe031c7bb57d049e535 + requires_dist: + - click>=8.1.8 + - deepmerge>=2.0 + - jinja2>=3.1 + - markdown>=3.7 + - pygments>=2.20 + - pymdown-extensions>=10.21.3 + - pyyaml>=6.0.2 + - tomli>=2.4.0 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/a2/d3/e0ce94d247ccc5240c6433f44155c3acdc54e812418f7d88c596bde845a3/anndata-0.12.18-py3-none-any.whl name: anndata version: 0.12.18 @@ -11452,21 +11155,6 @@ packages: - pytest-faulthandler ; extra == 'test' - pytest-doctestplus>=1.6.0 ; extra == 'test' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - name: mdit-py-plugins - version: 0.6.1 - sha256: 214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d - requires_dist: - - markdown-it-py>=2.0.0,<5.0.0 - - pre-commit ; extra == 'code-style' - - myst-parser ; extra == 'rtd' - - sphinx-book-theme ; extra == 'rtd' - - coverage ; extra == 'testing' - - pytest ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-regressions ; extra == 'testing' - - pytest-timeout ; extra == 'testing' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl name: pandas version: 2.3.3 @@ -12475,11 +12163,6 @@ packages: requires_dist: - typing-extensions>=4.14.1 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl - name: smmap - version: 5.0.3 - sha256: c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl name: executing version: 2.2.1 @@ -12610,13 +12293,6 @@ packages: version: 6.5.7 sha256: de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl - name: backrefs - version: '7.0' - sha256: a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475 - requires_dist: - - regex ; extra == 'extras' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl name: pytest-xdist version: 3.8.0 @@ -12958,13 +12634,6 @@ packages: - pytest-faulthandler ; extra == 'test' - pytest-doctestplus>=1.6.0 ; extra == 'test' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/d4/73/aef4aaf16b8d785762e2b14cf321a9178cc84a1bf4c40f58320f499a2d65/wcmatch-10.2-py3-none-any.whl - name: wcmatch - version: '10.2' - sha256: f1a79e80ccbe296907b7eaf57d8d3bc49eab0b428d35f7d09986b5079b6e4a5d - requires_dist: - - bracex>=2.1.1 - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl name: frozenlist version: 1.8.0 @@ -14082,11 +13751,6 @@ packages: - pytest-faulthandler ; extra == 'test' - pytest-doctestplus>=1.6.0 ; extra == 'test' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl - name: bracex - version: '3.0' - sha256: 3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5 - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/f5/6c/86644987505dcb90ba6d627d6989c27bafb0699f9fd00187e06d05ea8594/numcodecs-0.16.5-cp312-cp312-macosx_11_0_arm64.whl name: numcodecs version: 0.16.5 @@ -14395,3 +14059,17 @@ packages: - pytest-cov ; extra == 'test' - pytest-subtests ; extra == 'test' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fe/6f/91bbf78f704d5fd4c0c9be27d6bce3b6e4c2c339e4dcd6e7cf19ecda643c/zensical-0.0.51-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: zensical + version: 0.0.51 + sha256: 67f813a1514a90890ca86248a8d54b81b2164bcbff11a6bcf11b01e1c01a1454 + requires_dist: + - click>=8.1.8 + - deepmerge>=2.0 + - jinja2>=3.1 + - markdown>=3.7 + - pygments>=2.20 + - pymdown-extensions>=10.21.3 + - pyyaml>=6.0.2 + - tomli>=2.4.0 + requires_python: '>=3.10' diff --git a/pyproject.toml b/pyproject.toml index 90971a66..a87e8397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,22 +89,21 @@ dev = [ docs = [ "ruff", - "mkdocs<2.0", - "mkdocs-include-markdown-plugin", - "mkdocs-material", + "zensical", "mkdocstrings[python]", - "mkdocs-jupyter", "mkdocs-autorefs", - "mkdocs-git-revision-date-localized-plugin", - "mkdocs-git-committers-plugin-2", "markdown-exec[ansi]", + # Was only transitive via mkdocs-material, which Zensical replaces. + "pymdown-extensions", "griffe-typingdoc", "scikit-image", "matplotlib", "tabulate", "rich", - "mike", ] +# `mike` is deliberately NOT listed above: Zensical needs the maintainers' fork, which is +# only installable from git, and a PEP 508 direct reference in optional-dependencies would +# make ngio unpublishable to PyPI. It lives in the pixi docs feature instead. [project.urls] @@ -148,6 +147,9 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = ["D"] +# Doc snippets are split into sections that are rendered as standalone code blocks, +# so each section repeats the imports it needs. +"docs/snippets/**/*.py" = ["D", "E402", "F811", "I001"] [tool.ruff.lint.pydocstyle] convention = "google" @@ -261,10 +263,48 @@ python = ">=3.13,<3.14" [tool.pixi.feature.py14.dependencies] python = ">=3.14,<3.15" +[tool.pixi.feature.docs.pypi-dependencies] +# Zensical's versioning story is a fork of mike that is not published to PyPI. +# Kept here rather than in [project.optional-dependencies] so ngio itself stays +# publishable (PyPI rejects direct URL references). +mike = { git = "https://github.com/squidfunk/mike.git" } + [tool.pixi.feature.docs.tasks] -serve_docs = "mkdocs serve" -clean_nb_data = "rm -rf ./docs/notebooks/**/*.zarr" -test_nb = { cmd = "jupyter-execute ./docs/notebooks/*.ipynb" } +serve_docs = "zensical serve" +# --strict matters: without it `zensical build` exits 0 and prints "No issues found" +# even when markdown-exec code blocks raised, silently publishing tracebacks. +# --clean avoids reusing cached HTML from a previous (possibly broken) build. +build_docs = "zensical build --clean --strict" +# Drops the generated stores but keeps the downloaded archives cached in ./data. +# `data/tmp` is the shared staging dir `download_ome_zarr_dataset` extracts through; an +# interrupted build can leave a half-extracted dataset there, and every later build then +# fails with "Expected one directory to be extracted, got 2". Clearing it is the fix. +clean_docs_data = "rm -rf ./data/tmp ./data/*.zarr" +snip_quickstart = "python docs/snippets/getting_started/quickstart.py > /dev/null" +snip_get_started = "python docs/snippets/getting_started/get_started.py > /dev/null" +snip_masked_images = "python docs/snippets/getting_started/masked_images.py > /dev/null" +snip_iterators = "python docs/snippets/getting_started/iterators.py > /dev/null" +snip_hcs = "python docs/snippets/getting_started/hcs.py > /dev/null" +snip_create_ome_zarr = "python docs/snippets/tutorials/create_ome_zarr.py > /dev/null" +snip_image_processing = "python docs/snippets/tutorials/image_processing.py > /dev/null" +snip_image_segmentation = "python docs/snippets/tutorials/image_segmentation.py > /dev/null" +snip_feature_extraction = "python docs/snippets/tutorials/feature_extraction.py > /dev/null" +snip_hcs_exploration = "python docs/snippets/tutorials/hcs_exploration.py > /dev/null" +# Must stay sequential: `download_ome_zarr_dataset` stages every extraction through a +# single shared `<download_dir>/tmp` that it wipes on entry, so two scripts downloading +# at once clobber each other. (`depends-on` would run them in parallel.) +test_snippets = """ +python docs/snippets/getting_started/quickstart.py > /dev/null && +python docs/snippets/getting_started/get_started.py > /dev/null && +python docs/snippets/getting_started/masked_images.py > /dev/null && +python docs/snippets/getting_started/iterators.py > /dev/null && +python docs/snippets/getting_started/hcs.py > /dev/null && +python docs/snippets/tutorials/create_ome_zarr.py > /dev/null && +python docs/snippets/tutorials/image_processing.py > /dev/null && +python docs/snippets/tutorials/image_segmentation.py > /dev/null && +python docs/snippets/tutorials/feature_extraction.py > /dev/null && +python docs/snippets/tutorials/hcs_exploration.py > /dev/null +""" [tool.pixi.environments]