From b6d409ffd22329bf2b5e906e3528d9d2e9ae6755 Mon Sep 17 00:00:00 2001 From: lorenzo Date: Tue, 28 Jul 2026 19:16:27 +0200 Subject: [PATCH 01/14] remove notebooks and cleanup scripts --- CHANGELOG.md | 7 + CLAUDE.md | 20 +- docs/changelog.md | 4 +- docs/getting_started/0_quickstart.md | 24 +- docs/getting_started/1_ome_zarr_containers.md | 48 +-- docs/getting_started/2_images.md | 121 ++---- docs/getting_started/3_tables.md | 191 ++------- docs/getting_started/4_masked_images.md | 121 ++---- docs/getting_started/5_hcs.md | 92 ++-- docs/getting_started/6_iterators.md | 8 +- docs/index.md | 2 +- docs/snippets/getting_started/get_started.py | 398 ++++++++++++++++++ docs/snippets/getting_started/hcs.py | 103 +++++ .../snippets/getting_started/masked_images.py | 128 ++++++ docs/snippets/getting_started/quickstart.py | 26 ++ docs/snippets/tutorials/create_ome_zarr.py | 55 +++ docs/snippets/tutorials/feature_extraction.py | 96 +++++ docs/snippets/tutorials/hcs_exploration.py | 58 +++ docs/snippets/tutorials/image_processing.py | 162 +++++++ docs/snippets/tutorials/image_segmentation.py | 191 +++++++++ docs/tutorials/create_ome_zarr.ipynb | 101 ----- docs/tutorials/create_ome_zarr.md | 29 ++ docs/tutorials/feature_extraction.ipynb | 175 -------- docs/tutorials/feature_extraction.md | 31 ++ docs/tutorials/hcs_exploration.ipynb | 135 ------ docs/tutorials/hcs_exploration.md | 27 ++ docs/tutorials/image_processing.ipynb | 274 ------------ docs/tutorials/image_processing.md | 62 +++ docs/tutorials/image_segmentation.ipynb | 266 ------------ docs/tutorials/image_segmentation.md | 60 +++ mkdocs.yml | 22 +- pyproject.toml | 31 +- 32 files changed, 1651 insertions(+), 1417 deletions(-) create mode 100644 docs/snippets/getting_started/get_started.py create mode 100644 docs/snippets/getting_started/hcs.py create mode 100644 docs/snippets/getting_started/masked_images.py create mode 100644 docs/snippets/getting_started/quickstart.py create mode 100644 docs/snippets/tutorials/create_ome_zarr.py create mode 100644 docs/snippets/tutorials/feature_extraction.py create mode 100644 docs/snippets/tutorials/hcs_exploration.py create mode 100644 docs/snippets/tutorials/image_processing.py create mode 100644 docs/snippets/tutorials/image_segmentation.py delete mode 100644 docs/tutorials/create_ome_zarr.ipynb create mode 100644 docs/tutorials/create_ome_zarr.md delete mode 100644 docs/tutorials/feature_extraction.ipynb create mode 100644 docs/tutorials/feature_extraction.md delete mode 100644 docs/tutorials/hcs_exploration.ipynb create mode 100644 docs/tutorials/hcs_exploration.md delete mode 100644 docs/tutorials/image_processing.ipynb create mode 100644 docs/tutorials/image_processing.md delete mode 100644 docs/tutorials/image_segmentation.ipynb create mode 100644 docs/tutorials/image_segmentation.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd29f4e..fef825a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ - 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 +- Move every executed docs code block out of the markdown and into real Python scripts under `docs/snippets/`, included via `pymdownx.snippets` (`--8<-- "path.py:section"`) and executed by `markdown-exec`. The snippet scripts are linted and formatted by ruff and each one runs standalone from the repo root (`pixi run -e docs test_snippets`). This prepares the docs for the migration to Zensical, which does not support `mkdocs-jupyter`'s `execute: true`. +- Convert the five tutorial notebooks (`docs/tutorials/*.ipynb`) to markdown pages backed by snippet scripts, and drop the `mkdocs-jupyter` plugin. Fixes carried over in the conversion: the notebooks computed `download_dir` relative to their own directory (which resolved outside the repo once executed from the repo root), and several `create_ome_zarr_from_array`/`add_table` calls lacked `overwrite=True`, so a second build failed. +- Convert all `pycon` console blocks to plain `python` blocks. The old `>>> expr` plus hidden `>>> print(expr)` pair collapses to a single visible `print(expr)`, so each expression is evaluated once instead of twice and no lines are hidden from the reader. +- Replace `mkdocs-include-markdown-plugin` in `docs/changelog.md` with a `pymdownx.snippets` include, and drop the plugin. +- Fix `3_tables.md` showing `masking_table.get_label(1)` while executing `get_label(100)`; the page now shows the call it runs. +- Remove the dangling `favicon: images/favicon.ico` from `mkdocs.yml` (`docs/images/` does not exist). +- Replace the stale `clean_nb_data` / `test_nb` pixi tasks, which pointed at a nonexistent `docs/notebooks/`, with `build_docs`, `clean_docs_data`, and per-script `snip_*` tasks aggregated by `test_snippets`. - 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/CLAUDE.md b/CLAUDE.md index f74d1e60..dbca5c44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,9 +18,27 @@ 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 snippets +Executed code on the docs pages lives in real Python scripts under `docs/snippets/`, +not in the markdown. A page includes a named section via `pymdownx.snippets`: + +````markdown +```python exec="true" source="material-block" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:levels" +``` +```` + +- Sections are delimited by `# --8<-- [start:name]` / `# --8<-- [end:name]`. +- Use `source="material-block"` (not `block`) and `html="1"` for figure blocks. +- One script per session; each must run standalone from the repo root. +- Sections 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) 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/getting_started/0_quickstart.md b/docs/getting_started/0_quickstart.md index 59866472..022d8837 100644 --- a/docs/getting_started/0_quickstart.md +++ b/docs/getting_started/0_quickstart.md @@ -52,25 +52,15 @@ Please report installation problems by opening an issue on our [GitHub repositor Let's start by downloading 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. -```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" source="material-block" session="quickstart" +--8<-- "docs/snippets/getting_started/quickstart.py:open_container" ``` ### What is the OME-Zarr container? @@ -93,7 +83,7 @@ To learn how to work with the `OME-Zarr Container` object, but also with the ima Also, checkout our jupyer notebook tutorials for more examples: -- [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): Learn how to perform simple image processing operations. +- [Image Segmentation](../tutorials/image_segmentation.md): Learn how to create new labels from images. +- [Feature Extraction](../tutorials/feature_extraction.md): Learn how to extract features from images. +- [HCS Exploration](../tutorials/hcs_exploration.md): Learn how to explore high-content screening data using ngio. diff --git a/docs/getting_started/1_ome_zarr_containers.md b/docs/getting_started/1_ome_zarr_containers.md index daf6b5fe..9b9d802a 100644 --- a/docs/getting_started/1_ome_zarr_containers.md +++ b/docs/getting_started/1_ome_zarr_containers.md @@ -3,25 +3,13 @@ Let's see how to open and explore an OME-Zarr image using `ngio`: ```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. @@ -50,39 +38,33 @@ Examples of the OME-Zarr metadata access: === "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" 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 + ```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" ``` ## Modifying metadata diff --git a/docs/getting_started/2_images.md b/docs/getting_started/2_images.md index ef8e473b..b21148d4 100644 --- a/docs/getting_started/2_images.md +++ b/docs/getting_started/2_images.md @@ -9,60 +9,47 @@ ngio provides a high-level API to access the image data at different resolution === "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" 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" 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 + ```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. 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. === "Pixel Size" - ```pycon exec="true" source="console" session="get_started" - >>> image.pixel_size - >>> print(image.pixel_size) # markdown-exec: hide + ```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 + ```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. @@ -71,34 +58,26 @@ Similarly to the `OME-Zarr Container`, the `Image` object provides a high-level 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 + ```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 + ```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. - ```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: -```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: @@ -112,16 +91,7 @@ The `set_array` method can be used to set the image data from a numpy array, das A minimal example of how to use the `get_array` and `set_array` methods: ```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 @@ -135,11 +105,8 @@ image.consolidate() # Consolidate the changes to all resolution levels, see belo To read or write a specific region of the image defined in world coordinates, you can use the `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" ``` ## Labels @@ -151,48 +118,35 @@ be accessed and manipulated in the same way. Now let's see what labels are available in our 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: === "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" 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" 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 + ```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. @@ -204,9 +158,8 @@ Data access and manipulation for `Labels` is similar to `Images`. You can use th 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. diff --git a/docs/getting_started/3_tables.md b/docs/getting_started/3_tables.md index 921f4ffd..d9cf9020 100644 --- a/docs/getting_started/3_tables.md +++ b/docs/getting_started/3_tables.md @@ -6,11 +6,8 @@ Tables are not part of the core OME-Zarr specification but can be used in ngio t We can list all available tables and load a specific table: -```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`. @@ -18,136 +15,45 @@ Ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_ === "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()) + ```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" ``` 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 + ```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()) + ```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()) + ```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. === "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" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:feature_table" ``` ## Creating a table @@ -155,67 +61,42 @@ Ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_ Tables (differently from 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 + ```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 + ```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 + ```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" 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" 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. 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 + ```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: - ```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 + ```python exec="true" source="material-block" session="get_started" + --8<-- "docs/snippets/getting_started/get_started.py:generic_table_from_anndata" ``` The `GenericTable` class allows you to store any tabular data, and is a flexible way to work with tables in ngio. diff --git a/docs/getting_started/4_masked_images.md b/docs/getting_started/4_masked_images.md index e25e33bd..9f69e45a 100644 --- a/docs/getting_started/4_masked_images.md +++ b/docs/getting_started/4_masked_images.md @@ -4,88 +4,43 @@ Masked images (or labels) are images that are masked by an instance segmentation 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) +```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. Let's 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. 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`. -```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="true" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:plot_helpers" ``` -```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" html="1" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:plot_masked_roi" ``` 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" source="material-block" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:masked_roi_zoom" ``` -```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) - -fig, ax = plt.subplots(figsize=(8, 4)) -ax.set_title("Label 1009 ROI - Zoomed out") -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" html="1" session="masked_images" +--8<-- "docs/snippets/getting_started/masked_images.py:plot_masked_roi_zoom" ``` ## Masked operations @@ -95,44 +50,22 @@ In addition to the `get_roi_as_numpy` method, the `MaskedImage` class also provi 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: -```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: -```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 @@ -141,8 +74,6 @@ The `MaskedLabel` class is a subclass of `Label` and provides the same functiona The `MaskedLabel` class can be used to create a masked label from an `OME-Zarr Container` object 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" ``` diff --git a/docs/getting_started/5_hcs.md b/docs/getting_started/5_hcs.md index 184e6f56..ab7742b5 100644 --- a/docs/getting_started/5_hcs.md +++ b/docs/getting_started/5_hcs.md @@ -6,15 +6,8 @@ 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 +```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. @@ -25,21 +18,18 @@ The `OmeZarrPlate` object provides a high-level overview of the plate, including === "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 + ```python exec="true" source="material-block" session="hcs_plate" + --8<-- "docs/snippets/getting_started/hcs.py:plate_acquisitions" ``` ## Retrieving the path to the images @@ -48,23 +38,20 @@ The `OmeZarrPlate` object provides multiple methods to retrieve the path to the === "All Images 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" 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" 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 @@ -73,36 +60,29 @@ The `OmeZarrPlate` object provides a method to get the image objects in a well. === "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" 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" 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" 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. @@ -112,13 +92,8 @@ 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,11 +101,8 @@ 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. @@ -141,10 +113,8 @@ You can add images 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") + ```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 @@ -154,10 +124,8 @@ You can add images or remove images === "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") + ```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 diff --git a/docs/getting_started/6_iterators.md b/docs/getting_started/6_iterators.md index 3b80205b..421ca2b2 100644 --- a/docs/getting_started/6_iterators.md +++ b/docs/getting_started/6_iterators.md @@ -6,9 +6,9 @@ Moreover, when working with OME-Zarr Images it is often useful to set specific b Ngio provides a set of `Iterator` classes that can be used for this purpose. We provide iterators four basic iterators: -* 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). +* 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.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. An example use case on how to use the `MaskedSegmentationIterator` can be found in 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. An example use case on how to use the `ImageProcessingIterator` can be found in the [Image Processing Tutorial](../tutorials/image_processing.md). +* 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.md). A set of more complete example can be found in the [Fractal Tasks Template](https://github.com/fractal-analytics-platform/fractal-tasks-template). diff --git a/docs/index.md b/docs/index.md index 2f5b7aa5..b5e77edb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,7 +27,7 @@ Ngio's mission is to streamline working with OME-Zarr files by providing a simpl ## Getting Started -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. +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.md) to help you get up and running quickly. For more advanced usage and API documentation, see our [API Reference](api/ome_zarr_container.md). ## Supported OME-Zarr versions diff --git a/docs/snippets/getting_started/get_started.py b/docs/snippets/getting_started/get_started.py new file mode 100644 index 00000000..4ea79956 --- /dev/null +++ b/docs/snippets/getting_started/get_started.py @@ -0,0 +1,398 @@ +"""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] +from io import StringIO + +import matplotlib +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import ListedColormap +from matplotlib.figure import Figure + +matplotlib.use("Agg") + + +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 print_figure(fig: Figure) -> None: + """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + buffer = StringIO() + fig.savefig(buffer, format="svg") + plt.close(fig) + print(buffer.getvalue()) + + +# --8<-- [end:plot_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] + +# --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] + +# --------------------------------------------------------------------------- +# 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) +# --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] +# Get the image as a numpy or dask or delayed object +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: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: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] +from matplotlib.patches import Rectangle + +image_3 = ome_zarr_container.get_image(path="3") +image_data = np.squeeze(image_3.get_as_numpy(c=0)) + +roi = roi_table.get("FOV_1").to_pixel(pixel_size=image_3.pixel_size) +x_slice = roi.get("x") +y_slice = roi.get("y") + +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.axis("off") +fig.tight_layout() +print_figure(fig) +# --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") +image_data = np.squeeze(image_3.get_roi_as_numpy(roi, c=0)) + +fig, ax = plt.subplots(figsize=(8, 4)) +ax.set_title("FOV_1 ROI") +ax.imshow(image_data, cmap="gray") +ax.axis("off") +fig.tight_layout() +print_figure(fig) +# --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] +cmap = random_label_cmap() + +roi = masking_table.get_label(100) +image_2 = ome_zarr_container.get_image(path="2") +image_data = np.squeeze(image_2.get_roi_as_numpy(roi, c=0)) + +label_2 = ome_zarr_container.get_label("nuclei", pixel_size=image_2.pixel_size) +label_data = np.squeeze(label_2.get_roi_as_numpy(roi)) + +fig, ax = plt.subplots(figsize=(8, 4)) +ax.set_title("Label 100 ROI") +ax.imshow(image_data, cmap="gray") +ax.imshow(label_data, cmap=cmap, alpha=0.6) +ax.axis("off") +fig.tight_layout() +print_figure(fig) +# --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(feature_table.dataframe.head(5).to_markdown()) +# --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/masked_images.py b/docs/snippets/getting_started/masked_images.py new file mode 100644 index 00000000..9930fa73 --- /dev/null +++ b/docs/snippets/getting_started/masked_images.py @@ -0,0 +1,128 @@ +"""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] +from io import StringIO + +import matplotlib +from matplotlib import pyplot as plt +from matplotlib.figure import Figure + +matplotlib.use("Agg") + + +def print_figure(fig: Figure) -> None: + """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + buffer = StringIO() + fig.savefig(buffer, format="svg") + plt.close(fig) + print(buffer.getvalue()) + + +# --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] +import numpy as np + +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() +print_figure(fig) +# --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] +image_data = masked_image.get_roi_as_numpy(label=1009, c=0, zoom_factor=2) +image_data = np.squeeze(image_data) + +fig, ax = plt.subplots(figsize=(8, 4)) +ax.set_title("Label 1009 ROI - Zoomed out") +ax.imshow(image_data, cmap="gray") +ax.axis("off") +fig.tight_layout() +print_figure(fig) +# --8<-- [end:plot_masked_roi_zoom] + +# --8<-- [start:get_roi_masked] +masked_roi_data = masked_image.get_roi_masked(label=1009, c=0, zoom_factor=2) +print(masked_roi_data.shape) +# --8<-- [end:get_roi_masked] + +# --8<-- [start:plot_get_roi_masked] +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() +print_figure(fig) +# --8<-- [end:plot_get_roi_masked] + +# --8<-- [start:set_roi_masked] +import numpy as np + +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) +# --8<-- [end:set_roi_masked] + +# --8<-- [start:plot_after_set_roi_masked] +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() +print_figure(fig) +# --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..9bb57051 --- /dev/null +++ b/docs/snippets/getting_started/quickstart.py @@ -0,0 +1,26 @@ +"""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] diff --git a/docs/snippets/tutorials/create_ome_zarr.py b/docs/snippets/tutorials/create_ome_zarr.py new file mode 100644 index 00000000..b6f95647 --- /dev/null +++ b/docs/snippets/tutorials/create_ome_zarr.py @@ -0,0 +1,55 @@ +"""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] +from io import StringIO + +import matplotlib +from matplotlib import pyplot as plt +from matplotlib.figure import Figure + +matplotlib.use("Agg") + + +def print_figure(fig: Figure) -> None: + """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + buffer = StringIO() + fig.savefig(buffer, format="svg") + plt.close(fig) + print(buffer.getvalue()) + + +# --8<-- [end:plot_helpers] + +# --8<-- [start:plot_input_image] +import skimage + +fig, ax = plt.subplots() +ax.imshow(skimage.data.human_mitosis(), cmap="gray") +ax.axis("off") +print_figure(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..88c23d61 --- /dev/null +++ b/docs/snippets/tutorials/feature_extraction.py @@ -0,0 +1,96 @@ +"""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: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 + +# First we will need the image object and the FOVs table +image = ome_zarr.get_image() + +# Get the nuclei label +nuclei = ome_zarr.get_label("nuclei") + +# In this example we the image is available at an higher resolution than the nuclei +print(f"Image dimensions: {image.dimensions}, pixel size: {image.pixel_size}") +print(f"Nuclei dimensions: {nuclei.dimensions}, pixel size: {nuclei.pixel_size}") + +# We need to setup a transform to resample the nuclei to the image resolution +zoom_transform = ZoomTransform( + input_image=nuclei, + target_image=image, + order="nearest", # Nearest neighbor interpolation for labels +) +# --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 all the dataframes into a single one +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(ome_zarr.get_table("nuclei_regionprops").dataframe.head().to_markdown()) +# --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..5a54be81 --- /dev/null +++ b/docs/snippets/tutorials/hcs_exploration.py @@ -0,0 +1,58 @@ +"""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: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.dataframe.head().to_markdown()) +# --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(hcs_zarr.get_table("nuclei").dataframe.head().to_markdown()) +# --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..701faf88 --- /dev/null +++ b/docs/snippets/tutorials/image_processing.py @@ -0,0 +1,162 @@ +"""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] +from io import StringIO + +import matplotlib +from matplotlib import pyplot as plt +from matplotlib.figure import Figure + +matplotlib.use("Agg") + + +def print_figure(fig: Figure) -> None: + """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + buffer = StringIO() + fig.savefig(buffer, format="svg") + plt.close(fig) + print(buffer.getvalue()) + + +# --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] +# First we will need the image object +image = ome_zarr.get_image() + +# Second we need to derive a new ome-zarr image where we will store +# the processed image + +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] +# We can use the axes order to specify how we query the image data. +# Here we will reorder the axes to be ["c", "z", "y", "x"]. +# So that it will be compatible with the gaussian blur function +# which expects the channel axis to be the first one. +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) + +# Set the processed image data back to the ome-zarr image +blurred_image.set_array(patch=blurred_image_data, axes_order=["c", "z", "y", "x"]) + +# The `set_array` method only saved the blurred image to the container at a specific +# resolution level. So all other resolution levels are still empty. +# To propagate the changes to all resolution levels, +# we can use the `consolidate` method. +blurred_image.consolidate() +# --8<-- [end:apply_blur] + +# --8<-- [start:plot_blur] +fig, axs = plt.subplots(2, 1, figsize=(8, 4)) +axs[0].set_title("Original image") +axs[0].imshow(image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]), cmap="gray") +axs[1].set_title("Blurred image") +axs[1].imshow(blurred_image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]), cmap="gray") +for ax in axs: + ax.axis("off") +fig.tight_layout() +print_figure(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 will introduce some edge artifacts at chunk boundaries + # In a real application, consider using map_overlap to mitigate this + # With appropriate depth based on sigma + 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"], +) + +# After initializing the iterator, the iterator will have created +# will iterate over the entire image. +print(f"Iterator after initialization: {iterator}") + +# Iterate over an arbitrary region of interest table +# We can use the product method that performs a cartesian product +# between the iterator and the table. +table = ome_zarr.get_roi_table("FOV_ROI_table") +iterator = iterator.product(table) +print(f"Iterator after product with table: {iterator}") + +# We can explicitly set a broadcasting behavior +# For example we can iterate over all zyx planes, and broadcast all the other +# spatial dimensions +iterator = iterator.by_zyx() + +# Finally (if needed) we can check if the regions are not-overlapping +iterator.require_no_regions_overlap() +# We can also check if the regions lay on non-overlapping chunks +iterator.require_no_chunks_overlap() + +# Now we can map the gaussian blur function to the iterator +iterator.map_as_numpy(lambda x: gaussian_blur(x, sigma=sigma)) + +# No need to consolidate, the iterator takes care of that +# after all the regions have been 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..4ae37977 --- /dev/null +++ b/docs/snippets/tutorials/image_segmentation.py @@ -0,0 +1,191 @@ +"""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] +from io import StringIO + +import matplotlib +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import ListedColormap +from matplotlib.figure import Figure + +matplotlib.use("Agg") + + +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 print_figure(fig: Figure) -> None: + """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + buffer = StringIO() + fig.savefig(buffer, format="svg") + plt.close(fig) + print(buffer.getvalue()) + + +# --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 + +# First we will need the image object and the FOVs table +image = ome_zarr.get_image() +roi_table = ome_zarr.get_roi_table("FOV_ROI_table") + +# Second we need to derive a new label image to use as target for the segmentation + +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) + +# Make sure that if other axes are present they are iterated over +seg_iterator = seg_iterator.by_zyx() + +max_label = 0 # We will use this to avoid label collisions +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() + +fig, axs = plt.subplots(2, 1, figsize=(8, 4)) +axs[0].set_title("Original image") +axs[0].imshow(image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]), cmap="gray") +axs[1].set_title("Final segmentation") +axs[1].imshow(label.get_as_numpy(z=1, axes_order=["y", "x"]), cmap=rand_cmap) +for ax in axs: + ax.axis("off") +fig.tight_layout() +print_figure(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, 4)) +axs[0].set_title("Original image") +axs[0].imshow(image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]), cmap="gray") +axs[1].set_title("Mask") +axs[1].imshow(mask.get_as_numpy(z=1, axes_order=["y", "x"]), cmap=rand_cmap) +for ax in axs: + ax.axis("off") +fig.tight_layout() +print_figure(fig) +# --8<-- [end:plot_mask] + +# --8<-- [start:masked_segment] +from ngio.experimental.iterators import MaskedSegmentationIterator + +# First we will need the masked image object +# (that contains the masking table information inside) +image = ome_zarr.get_masked_image(masking_label_name="mask") + +# Second we need to derive a new label image to use as target for the segmentation +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"], +) + +# Make sure that if other axes are present they are iterated over +seg_iterator = seg_iterator.by_zyx() + +max_label = 0 # We will use this to avoid label collisions +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, 4)) +axs[0].set_title("Original image") +axs[0].imshow(image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]), cmap="gray") +axs[1].set_title("Final segmentation") +axs[1].imshow(label.get_as_numpy(z=1, axes_order=["y", "x"]), cmap=rand_cmap) +for ax in axs: + ax.axis("off") +fig.tight_layout() +print_figure(fig) +# --8<-- [end:plot_masked_segmentation] 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..40322d3b --- /dev/null +++ b/docs/tutorials/create_ome_zarr.md @@ -0,0 +1,29 @@ +# OME-Zarr Creation + +This is a minimal example of how to create an OME-Zarr image using `ngio`. + +This example is just a simple demonstration but for more complex conversion tasks please refer +to the converter tooling library [ome-zarr-converters-tools](https://github.com/BioVisionCenter/ome-zarr-converters-tools). + +Let's 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" +``` + +## Adding a ROI table to an OME-Zarr image + +Often, is useful to add ROIs to OME-Zarr images to be able to retrieve them later. +This can be done using the `ngio` library as follows. + +```python exec="true" source="material-block" session="create_ome_zarr" +--8<-- "docs/snippets/tutorials/create_ome_zarr.py:add_roi_table" +``` 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..ed1f2504 --- /dev/null +++ b/docs/tutorials/feature_extraction.md @@ -0,0 +1,31 @@ +# Feature Extraction + +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. + +## Step 1: Open the OME-Zarr Container + +```python exec="true" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:extract_features" +``` + +```python exec="true" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:open_container" +``` + +## Step 2: Setup the inputs + +```python exec="true" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:setup_transform" +``` + +## Step 3: 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" source="material-block" session="feature_extraction" +--8<-- "docs/snippets/tutorials/feature_extraction.py:read_table_back" +``` 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..74ed74aa --- /dev/null +++ b/docs/tutorials/hcs_exploration.md @@ -0,0 +1,27 @@ +# HCS Plates + +This is a minimal example of how to work with OME-Zarr Plates using `ngio`. + +## Show what's in the plate + +```python exec="true" source="material-block" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:open_plate" +``` + +## Aggregate tables across all images + +```python exec="true" source="material-block" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:concatenate_tables" +``` + +## Save the table in the HCS plate + +```python exec="true" source="material-block" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:save_table" +``` + +## Create a new empty Plate + +```python exec="true" source="material-block" session="hcs_exploration" +--8<-- "docs/snippets/tutorials/hcs_exploration.py:create_plate" +``` 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..564fe50b --- /dev/null +++ b/docs/tutorials/image_processing.md @@ -0,0 +1,62 @@ +# Image Processing + +This is a minimal example of how to use the `ngio` library for applying some basic image processing techniques. + +For this example we will apply gaussian blur to an image. + +## Step 1: Setup + +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. + +```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 OmeZarr 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 omeZarr container + +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 +preserves the original metadata and dimensions (unless explicitly changed 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, we can visualize the original and blurred images using `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 + +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. + +```python exec="true" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:dask_blur" +``` + +## Step 6. Image Processing Iterators + +`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. + +```python exec="true" source="material-block" session="image_processing" +--8<-- "docs/snippets/tutorials/image_processing.py:iterators" +``` 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..5da6c04e --- /dev/null +++ b/docs/tutorials/image_segmentation.md @@ -0,0 +1,60 @@ +# Image Segmentation + +This is a minimal tutorial on how to use ngio for image segmentation. + +## Step 1: Setup + +We will first implement a very simple function to segment an image. We will use skimage to do this. + +```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 OmeZarr container + +```python exec="true" source="material-block" session="image_segmentation" +--8<-- "docs/snippets/tutorials/image_segmentation.py:open_container" +``` + +## Step 3: Segment the image + +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. + +```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 + +In this example we will use a mask to restrict the segmentation to certain areas of the image. +In this case we will create a simple mask for illustration purposes, but in a real case scenario the mask could come +from another segmentation mask. + +```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" +``` diff --git a/mkdocs.yml b/mkdocs.yml index f0c88055..81fc2b40 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,9 +5,13 @@ repo_name: "ngio" repo_url: "https://github.com/BioVisionCenter/ngio" copyright: "Copyright © 2024-, BioVisionCenter UZH" +# The snippet scripts are executed into the docs via pymdownx.snippets, which reads +# them from the filesystem. They are not pages, so keep them out of the built site. +exclude_docs: | + snippets/ + theme: name: material - favicon: images/favicon.ico #logo: logos/logo_white.png icon: repo: fontawesome/brands/github @@ -54,7 +58,6 @@ plugins: - search - autorefs - markdown-exec - - include-markdown - mkdocstrings: handlers: python: @@ -88,13 +91,14 @@ plugins: - git-committers: repository: BioVisionCenter/ngio branch: main - - mkdocs-jupyter: - execute: true markdown_extensions: - admonition - pymdownx.details - pymdownx.superfences + - pymdownx.snippets: + base_path: ["."] + check_paths: true - md_in_html - pymdownx.tabbed: alternate_style: true @@ -116,11 +120,11 @@ nav: - 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 + - tutorials/create_ome_zarr.md + - tutorials/image_processing.md + - tutorials/image_segmentation.md + - tutorials/feature_extraction.md + - tutorials/hcs_exploration.md - Table Specifications: - "Overview": table_specs/overview.md diff --git a/pyproject.toml b/pyproject.toml index dd80f1ac..f38c3176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,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" @@ -262,8 +265,32 @@ python = ">=3.14,<3.15" [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" } +build_docs = "mkdocs build" +# Only drops the generated stores; the downloaded archives stay cached in ./data. +clean_docs_data = "rm -rf ./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_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 `/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/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] From 1606248b8fcacf5a89294a00adc51f9d29f84076 Mon Sep 17 00:00:00 2001 From: lorenzo Date: Wed, 29 Jul 2026 09:21:23 +0200 Subject: [PATCH 02/14] move to zensical --- .github/workflows/docs.yml | 13 +- CHANGELOG.md | 8 + CLAUDE.md | 13 + docs/getting_started/2_images.md | 4 + docs/getting_started/3_tables.md | 13 +- docs/snippets/getting_started/get_started.py | 52 +- docs/snippets/tutorials/feature_extraction.py | 26 +- docs/snippets/tutorials/hcs_exploration.py | 28 +- docs/tutorials/feature_extraction.md | 6 +- docs/tutorials/hcs_exploration.md | 8 +- mkdocs.yml | 22 +- pixi.lock | 580 ++++-------------- pyproject.toml | 33 +- 13 files changed, 333 insertions(+), 473 deletions(-) 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/CHANGELOG.md b/CHANGELOG.md index fef825a0..7f50a468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ - 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 +- Build the documentation with [Zensical](https://zensical.org) instead of MkDocs + Material. MkDocs 1.x is unmaintained and MkDocs 2.0 drops the plugin system; Zensical is the successor from the Material team and reads the existing `mkdocs.yml`. `serve_docs` is now `zensical serve` and `build_docs` is `zensical build --clean --strict`. Versioning keeps working through the Zensical maintainers' fork of `mike`, which is installed from git via the pixi `docs` feature (it is not on PyPI) rather than from `[project.optional-dependencies]`, so ngio itself stays publishable. The gh-pages layout is unchanged, so already-published versions stay browsable. +- **Each docs page now binds its own state.** Zensical gives every page a fresh `markdown-exec` session, so a page can no longer use a variable defined on another page. `2_images.md` and `3_tables.md` include two new silent snippet sections (`reopen_container`, `reopen_image`) that re-open the sample container; they render nothing and reuse the already-extracted store (`re_unzip=False`) instead of re-extracting it. +- Fix tables printed by exec blocks rendering as literal `|---|` text. Zensical does not run block-level Markdown over `markdown-exec` output (static pipe tables in `.md` sources are unaffected), so those blocks now emit HTML through a `print_table` snippet helper. Because every theme table rule — and the JavaScript that adds the horizontal-scroll wrapper — is gated on `table:not([class])`, the helper strips the `class="dataframe"` and `border` attributes pandas adds, which would otherwise leave the table completely unstyled and unable to scroll. Values are rounded to two decimals instead of printing six. +- Add the `tables` markdown extension to `mkdocs.yml`. MkDocs implicitly enables `toc`, `tables` and `fenced_code`; Zensical does not, so the static pipe tables in the table-spec pages need it declared. +- Fix `site_url`, which pointed at the git clone URL (`https://github.com/BioVisionCenter/ngio.git`) rather than the published docs. That produced wrong `rel="canonical"` links and, under Zensical, a bogus `/BioVisionCenter/ngio.git/` base path when serving locally. It is now `https://biovisioncenter.github.io/ngio/`. +- `clean_docs_data` now also removes `data/tmp`. That is the shared staging directory `download_ome_zarr_dataset` extracts through, and an interrupted build leaves a half-extracted dataset there, after which every later build fails with `Expected one directory to be extracted, got 2`. +- Drop the `git-revision-date-localized` and `git-committers` plugins (Tier 2 on Zensical's compatibility backlog), losing the page revision-date footer and committer avatars for now. mkdocstrings backlinks are likewise not yet supported; cross-references and the inventory still work. +- Build the docs with `--strict` in CI before deploying: plain `zensical build` exits 0 and reports "No issues found" even when a `markdown-exec` code block raised, which would otherwise publish tracebacks. - Move every executed docs code block out of the markdown and into real Python scripts under `docs/snippets/`, included via `pymdownx.snippets` (`--8<-- "path.py:section"`) and executed by `markdown-exec`. The snippet scripts are linted and formatted by ruff and each one runs standalone from the repo root (`pixi run -e docs test_snippets`). This prepares the docs for the migration to Zensical, which does not support `mkdocs-jupyter`'s `execute: true`. - Convert the five tutorial notebooks (`docs/tutorials/*.ipynb`) to markdown pages backed by snippet scripts, and drop the `mkdocs-jupyter` plugin. Fixes carried over in the conversion: the notebooks computed `download_dir` relative to their own directory (which resolved outside the repo once executed from the repo root), and several `create_ome_zarr_from_array`/`add_table` calls lacked `overwrite=True`, so a second build failed. - Convert all `pycon` console blocks to plain `python` blocks. The old `>>> expr` plus hidden `>>> print(expr)` pair collapses to a single visible `print(expr)`, so each expression is evaluated once instead of twice and no lines are hidden from the reader. diff --git a/CLAUDE.md b/CLAUDE.md index dbca5c44..96d5dbcf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,19 @@ not in the markdown. A page includes a named section via `pymdownx.snippets`: - Sections are delimited by `# --8<-- [start:name]` / `# --8<-- [end:name]`. - Use `source="material-block"` (not `block`) and `html="1"` for figure blocks. - One script per session; each must run standalone from the repo root. +- **Every page must bind its own state.** The site is built by Zensical, which gives each + page a fresh markdown-exec session — a page cannot use a variable bound on another page. + Pages 2 and 3 of the getting-started guide therefore include the silent + `reopen_container` / `reopen_image` sections at the top (hidden, no `source=`). +- **Printing a table? Call the `print_table` helper, with `html="1"` on the fence.** + `.to_markdown()` does not work: Zensical does not run block-level Markdown over + markdown-exec output, so a pipe table stays literal `|---|` text (static pipe tables in + `.md` sources are fine). `print_table` emits `to_html()` and strips the + `class="dataframe"` / `border` attributes pandas adds, because every theme table rule — + and the JS that adds the horizontal-scroll wrapper — is gated on `table:not([class])`. +- Always 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 it will serve + cached HTML from a previous broken build. - Sections repeat their imports so each rendered block stands alone (hence the `docs/snippets/**` ruff per-file-ignores). diff --git a/docs/getting_started/2_images.md b/docs/getting_started/2_images.md index b21148d4..dc44c0a5 100644 --- a/docs/getting_started/2_images.md +++ b/docs/getting_started/2_images.md @@ -7,6 +7,10 @@ ngio provides a high-level API to access the image data at different resolution ### Getting an image +```python exec="true" session="get_started" +--8<-- "docs/snippets/getting_started/get_started.py:reopen_container" +``` + === "Highest Resolution Image" By default, the `get_image` method returns the highest resolution image: ```python exec="true" source="material-block" session="get_started" diff --git a/docs/getting_started/3_tables.md b/docs/getting_started/3_tables.md index d9cf9020..18262d6e 100644 --- a/docs/getting_started/3_tables.md +++ b/docs/getting_started/3_tables.md @@ -6,6 +6,14 @@ Tables are not part of the core OME-Zarr specification but can be used in ngio t We can list all available tables and load a specific table: +```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" +``` + ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:list_tables" ``` @@ -52,7 +60,10 @@ Ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_ === "Features Table" Features tables are used to store measurements and are indexed by the label id - ```python exec="true" source="material-block" session="get_started" + ```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" ``` diff --git a/docs/snippets/getting_started/get_started.py b/docs/snippets/getting_started/get_started.py index 4ea79956..1a8ff6fb 100644 --- a/docs/snippets/getting_started/get_started.py +++ b/docs/snippets/getting_started/get_started.py @@ -41,6 +41,29 @@ def print_figure(fig: Figure) -> None: # --8<-- [end:plot_helpers] +# --8<-- [start:table_helpers] +import pandas as pd + + +def print_table(df: pd.DataFrame) -> None: + """Print 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. + """ + # 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) + print(html.replace(' class="dataframe"', "")) + + +# --8<-- [end:table_helpers] + # --------------------------------------------------------------------------- # 1. OME-Zarr Container # --------------------------------------------------------------------------- @@ -60,6 +83,33 @@ def print_figure(fig: Figure) -> None: 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] @@ -334,7 +384,7 @@ def process(patch: np.ndarray) -> np.ndarray: # Get a feature table feature_table = ome_zarr_container.get_table("regionprops_DAPI") # only show the first 5 rows -print(feature_table.dataframe.head(5).to_markdown()) +print_table(feature_table.dataframe.head(5)) # --8<-- [end:feature_table] # --8<-- [start:create_roi_table] diff --git a/docs/snippets/tutorials/feature_extraction.py b/docs/snippets/tutorials/feature_extraction.py index 88c23d61..438a2d8a 100644 --- a/docs/snippets/tutorials/feature_extraction.py +++ b/docs/snippets/tutorials/feature_extraction.py @@ -7,6 +7,30 @@ python docs/snippets/tutorials/feature_extraction.py """ +# --8<-- [start:table_helpers] +import pandas as pd + + +def print_table(df: pd.DataFrame) -> None: + """Print 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. + """ + # 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) + print(html.replace(' class="dataframe"', "")) + + +# --8<-- [end:table_helpers] + + # --8<-- [start:extract_features] import numpy as np import pandas as pd @@ -92,5 +116,5 @@ def extract_features(image: np.ndarray, label: np.ndarray) -> pd.DataFrame: # --8<-- [end:extract] # --8<-- [start:read_table_back] -print(ome_zarr.get_table("nuclei_regionprops").dataframe.head().to_markdown()) +print_table(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 index 5a54be81..4c080a16 100644 --- a/docs/snippets/tutorials/hcs_exploration.py +++ b/docs/snippets/tutorials/hcs_exploration.py @@ -7,6 +7,30 @@ python docs/snippets/tutorials/hcs_exploration.py """ +# --8<-- [start:table_helpers] +import pandas as pd + + +def print_table(df: pd.DataFrame) -> None: + """Print 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. + """ + # 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) + print(html.replace(' class="dataframe"', "")) + + +# --8<-- [end:table_helpers] + + # --8<-- [start:open_plate] from pathlib import Path @@ -28,7 +52,7 @@ # --8<-- [start:concatenate_tables] # Aggregate all table across all images table = hcs_zarr.concatenate_image_tables(name="nuclei") -print(table.dataframe.head().to_markdown()) +print_table(table.dataframe.head()) # --8<-- [end:concatenate_tables] # --8<-- [start:save_table] @@ -36,7 +60,7 @@ hcs_zarr.add_table(name="nuclei", table=table, overwrite=True) # Read the table back for sanity check -print(hcs_zarr.get_table("nuclei").dataframe.head().to_markdown()) +print_table(hcs_zarr.get_table("nuclei").dataframe.head()) # --8<-- [end:save_table] # --8<-- [start:create_plate] diff --git a/docs/tutorials/feature_extraction.md b/docs/tutorials/feature_extraction.md index ed1f2504..f27a5d08 100644 --- a/docs/tutorials/feature_extraction.md +++ b/docs/tutorials/feature_extraction.md @@ -26,6 +26,10 @@ This sections will cover how to extract regionprops features from an image using ### Sanity Check: Read the Table back -```python exec="true" source="material-block" session="feature_extraction" +```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" ``` diff --git a/docs/tutorials/hcs_exploration.md b/docs/tutorials/hcs_exploration.md index 74ed74aa..c47848cf 100644 --- a/docs/tutorials/hcs_exploration.md +++ b/docs/tutorials/hcs_exploration.md @@ -10,13 +10,17 @@ This is a minimal example of how to work with OME-Zarr Plates using `ngio`. ## Aggregate tables across all images -```python exec="true" source="material-block" session="hcs_exploration" +```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" ``` ## Save the table in the HCS plate -```python exec="true" source="material-block" session="hcs_exploration" +```python exec="true" html="1" source="material-block" session="hcs_exploration" --8<-- "docs/snippets/tutorials/hcs_exploration.py:save_table" ``` diff --git a/mkdocs.yml b/mkdocs.yml index 81fc2b40..e8d75b70 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,12 +1,18 @@ site_name: "NGIO: Next Generation File Format I/O" -site_url: "https://github.com/BioVisionCenter/ngio.git" +# 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 processing OME-Zarr images" repo_name: "ngio" repo_url: "https://github.com/BioVisionCenter/ngio" copyright: "Copyright © 2024-, BioVisionCenter UZH" -# The snippet scripts are executed into the docs via pymdownx.snippets, which reads -# them from the filesystem. They are not pages, so keep them out of the built site. +# 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/ @@ -86,13 +92,13 @@ 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 markdown_extensions: + # MkDocs silently enabled `tables` (and toc/fenced_code) by default; Zensical does not, + # so it must be listed explicitly or the static pipe tables in docs/table_specs/** + # render as literal `|---|` text. Note this does NOT help exec-block output: Zensical + # runs no block-level Markdown over it, which is why those blocks emit HTML instead. + - tables - admonition - pymdownx.details - pymdownx.superfences diff --git a/pixi.lock b/pixi.lock index 83865dac..b17f852d 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1,8 +1,20 @@ version: 7 platforms: - name: linux-64 + virtual-packages: + - __unix=0=0 + - __linux=4.18 + - __glibc=2.28 + - __archspec=0=x86_64 - name: osx-arm64 + virtual-packages: + - __unix=0=0 + - __osx=13.0 + - __archspec=0=m1 - name: win-64 + virtual-packages: + - __win=10.0 + - __archspec=0=x86_64 environments: default: channels: @@ -32,7 +44,7 @@ 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/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - pypi: . + - pypi: ./ - 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/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl @@ -113,7 +125,7 @@ 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 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - - pypi: . + - pypi: ./ - 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 @@ -196,7 +208,7 @@ 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 - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - - pypi: . + - pypi: ./ - 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/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl @@ -290,7 +302,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/72/2067f28fd0ae87978f3b61e8ec30c1d085bbed03f64eb58e43949d526b3a/napari_console-0.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/b9/748c3cbcdba20ef01c055a35905cad5c771cc1df29be11b9a82da6fd8e0d/superqt-0.8.2-py3-none-any.whl @@ -562,7 +574,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h0c9c016_1_cpython.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/72/2067f28fd0ae87978f3b61e8ec30c1d085bbed03f64eb58e43949d526b3a/napari_console-0.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl @@ -837,7 +849,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/72/2067f28fd0ae87978f3b61e8ec30c1d085bbed03f64eb58e43949d526b3a/napari_console-0.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/b9/748c3cbcdba20ef01c055a35905cad5c771cc1df29be11b9a82da6fd8e0d/superqt-0.8.2-py3-none-any.whl @@ -1125,14 +1137,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - 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: ./ + - 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 @@ -1140,60 +1151,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 @@ -1201,36 +1190,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 @@ -1242,16 +1214,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 @@ -1261,27 +1225,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 @@ -1296,15 +1255,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h0c9c016_1_cpython.conda - 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: ./ + - 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 @@ -1314,96 +1271,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 @@ -1412,18 +1331,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 @@ -1435,19 +1346,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 @@ -1470,12 +1375,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda - 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: ./ + - 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 @@ -1483,55 +1387,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 @@ -1539,34 +1422,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 @@ -1576,22 +1445,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 @@ -1603,20 +1465,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 @@ -1654,7 +1509,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl - 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 @@ -1797,7 +1652,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h0c9c016_1_cpython.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl @@ -1942,7 +1797,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -2102,7 +1957,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl - 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 @@ -2245,7 +2100,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h0c9c016_1_cpython.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl @@ -2390,7 +2245,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -2550,7 +2405,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -2691,7 +2546,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.13-h8561d8f_0_cpython.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -2834,7 +2689,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -2991,7 +2846,7 @@ 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/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - pypi: . + - pypi: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -3134,7 +2989,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.14-h448ec07_100_cp313.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl @@ -3279,7 +3134,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda - 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: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -3436,7 +3291,7 @@ 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/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - pypi: . + - pypi: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -3580,7 +3435,7 @@ 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 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - - pypi: . + - pypi: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -3726,7 +3581,7 @@ 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 - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - - pypi: . + - pypi: ./ - pypi: https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.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 @@ -4915,7 +4770,7 @@ packages: - zstd >=1.5.7,<1.6.0a0 size: 388453 timestamp: 1764777142545 -- pypi: . +- pypi: ./ name: ngio requires_dist: - aiohttp @@ -4951,19 +4806,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' @@ -4972,6 +4822,17 @@ packages: - pytest-httpserver ; 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 @@ -5634,18 +5495,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 @@ -6216,6 +6065,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 @@ -6225,28 +6088,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 @@ -6795,30 +6636,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 @@ -7509,6 +7326,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 @@ -7657,6 +7479,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 @@ -7884,6 +7711,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 @@ -8021,16 +7864,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 @@ -8272,11 +8105,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 @@ -8978,6 +8806,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 @@ -8993,84 +8826,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 @@ -9316,27 +9071,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 @@ -9856,15 +9590,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 @@ -10284,15 +10009,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 @@ -10378,14 +10094,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 @@ -11096,13 +10804,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 @@ -11162,6 +10863,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 @@ -11403,21 +11118,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 @@ -12416,11 +12116,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 @@ -12551,13 +12246,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/d0/7920b8ba919b90173a80f4428d66e05f4d219a3ae3805cebe86924f9cfa1/vispy-0.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: vispy version: 0.16.2 @@ -12888,13 +12576,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 @@ -14012,11 +13693,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 @@ -14325,3 +14001,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 f38c3176..f5a1dbc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,22 +88,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] @@ -263,11 +262,23 @@ 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" -build_docs = "mkdocs build" -# Only drops the generated stores; the downloaded archives stay cached in ./data. -clean_docs_data = "rm -rf ./data/*.zarr" +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" From 5ba2132cd44a0ce990fd9e74f4f803d832fcc025 Mon Sep 17 00:00:00 2001 From: lorenzo Date: Wed, 29 Jul 2026 14:20:24 +0200 Subject: [PATCH 03/14] stylize docs --- .pre-commit-config.yaml | 4 + CHANGELOG.md | 7 + CITATION.cff | 27 + CODE_OF_CONDUCT.md | 59 +++ CONTRIBUTING.md | 91 ++++ README.md | 4 +- docs/_glossary.md | 14 + docs/api/hcs.md | 4 + docs/api/images.md | 4 + docs/api/iterators.md | 4 + docs/api/ome_zarr_container.md | 4 + docs/api/tables.md | 49 +- docs/assets/logo-dark.svg | 16 + docs/assets/logo-mono.svg | 7 + docs/assets/logo.svg | 16 + docs/code_of_conduct.md | 7 +- docs/contributing.md | 90 +--- docs/getting_started/0_quickstart.md | 12 +- docs/getting_started/1_ome_zarr_containers.md | 15 +- docs/getting_started/2_images.md | 16 +- docs/getting_started/3_tables.md | 16 +- docs/getting_started/4_masked_images.md | 13 +- docs/getting_started/5_hcs.md | 18 +- docs/getting_started/6_iterators.md | 14 +- docs/getting_started/7_configuration.md | 13 +- docs/index.md | 133 +++-- docs/llms.txt | 56 +++ docs/stylesheets/ngio.css | 460 ++++++++++++++++++ docs/table_specs/backend.md | 14 +- docs/table_specs/overview.md | 10 +- .../table_types/condition_table.md | 2 +- docs/table_specs/table_types/custom_table.md | 26 +- docs/table_specs/table_types/feature_table.md | 4 +- docs/table_specs/table_types/generic_table.md | 4 +- .../table_types/masking_roi_table.md | 2 +- docs/table_specs/table_types/roi_table.md | 2 +- docs/tutorials/create_ome_zarr.md | 11 +- docs/tutorials/feature_extraction.md | 11 +- docs/tutorials/hcs_exploration.md | 9 + docs/tutorials/image_processing.md | 17 +- docs/tutorials/image_segmentation.md | 11 +- mkdocs.yml | 131 +++-- 42 files changed, 1209 insertions(+), 218 deletions(-) create mode 100644 CITATION.cff create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/_glossary.md create mode 100644 docs/assets/logo-dark.svg create mode 100644 docs/assets/logo-mono.svg create mode 100644 docs/assets/logo.svg create mode 100644 docs/llms.txt create mode 100644 docs/stylesheets/ngio.css diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f9cdd83..a17c1a2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,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 7f50a468..b90beb1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ - 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 +- Polish the documentation for the 1.0 release. Restore the Markdown extensions Zensical had silently dropped: declaring `markdown_extensions:` in `mkdocs.yml` **replaces** Zensical's ~30 defaults rather than extending them (`zensical/config.py:656`), so the site had been running without `attr_list`, `abbr`, `pymdownx.emoji`, `pymdownx.highlight` and superfences' mermaid fence. As a result `content.code.annotate` and `content.tooltips` were enabled but non-functional, grid cards were impossible, emoji rendered as raw Unicode and code blocks had no line anchors. The list is now explicit and commented, and deliberately omits `smartsymbols`/`caret`/`tilde`/`magiclink`/`arithmatex`. +- Give the docs a visual identity, from a commissioned design system: a new logo ("mark G", three extruded slabs of a multiscale image pyramid in isometric projection, plus `logo-dark.svg` and `logo-mono.svg` variants) and a full theme layer in `docs/stylesheets/ngio.css` — a contrast-audited teal palette for both colour schemes, Space Grotesk display type with IBM Plex Sans body and JetBrains Mono code (via `theme.font`), and component styling for the header, sidebar, code blocks, executed-snippet `OUT` result blocks, content tabs, grid cards, admonitions, mkdocstrings pages, footer, and search. Replaces the interim three-bar `currentColor` logo, green/teal stock palette, and accent-only `extra.css`. Also add navigation tabs, a back-to-top button, and working "edit this page" links — the latter needs an explicit `edit_uri: edit/main/docs/`, since Zensical would otherwise derive `edit/master/...` and every link would 404. +- Rewrite the landing page: an H1, an install snippet, a runnable 30-second example, and grid cards into the four sections. Remove the "not yet stable / 0.x releases" warning, which contradicts a 1.0 release. +- Add a glossary of 14 recurring terms, appended to every page via `pymdownx.snippets`' `auto_append` and surfaced as tooltips; add "Next steps" footers to all twelve guides and tutorials, which previously ended as dead ends; and introduce mkdocstrings symbol cross-references (`[Image][ngio.Image]`), of which there were previously none despite `autorefs` being enabled. +- Make the docs LLM-friendly: add a curated `llms.txt` at the site root following the llmstxt.org convention, and `description:` front matter on the main pages (rendered as ``). +- Fix content defects: the quickstart still pointed at the old `fractal-analytics-platform/ngio` repository and advertised "jupyer notebook tutorials" that are no longer notebooks; `6_iterators.md` named a non-existent `FeatureExtractionIterator`; the landing page linked table specs through an absolute `/stable/` URL; two remote-store examples called `open_ome_zarr_container` without importing it; `"annadata"` appeared in normative JSON on six table-spec pages; and `docs/api/tables.md` was an H1-only file rendering as a blank page in the nav. Plus ~15 grammar fixes and consistent `ngio` / `OME-Zarr` casing. +- Move `CONTRIBUTING.md` and add `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1) at the repository root, single-sourced into the docs via `--8<--` so GitHub's community widgets pick them up, and add `CITATION.cff`. - Build the documentation with [Zensical](https://zensical.org) instead of MkDocs + Material. MkDocs 1.x is unmaintained and MkDocs 2.0 drops the plugin system; Zensical is the successor from the Material team and reads the existing `mkdocs.yml`. `serve_docs` is now `zensical serve` and `build_docs` is `zensical build --clean --strict`. Versioning keeps working through the Zensical maintainers' fork of `mike`, which is installed from git via the pixi `docs` feature (it is not on PyPI) rather than from `[project.optional-dependencies]`, so ngio itself stays publishable. The gh-pages layout is unchanged, so already-published versions stay browsable. - **Each docs page now binds its own state.** Zensical gives every page a fresh `markdown-exec` session, so a page can no longer use a variable defined on another page. `2_images.md` and `3_tables.md` include two new silent snippet sections (`reopen_container`, `reopen_image`) that re-open the sample container; they render nothing and reuse the already-extracted store (`re_unzip=False`) instead of re-extracting it. - Fix tables printed by exec blocks rendering as literal `|---|` text. Zensical does not run block-level Markdown over `markdown-exec` output (static pipe tables in `.md` sources are unaffected), so those blocks now emit HTML through a `print_table` snippet helper. Because every theme table rule — and the JavaScript that adds the horizontal-scroll wrapper — is gated on `table:not([class])`, the helper strips the `class="dataframe"` and `border` attributes pandas adds, which would otherwise leave the table completely unstyled and unable to scroll. Values are rounded to two decimals instead of printing six. 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/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..f8604923 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# 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 +``` + +--- + +## 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. diff --git a/README.md b/README.md index 3a7c4a78..8a443599 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ ngio supports OME-Zarr v0.4/v0.5. Support for version 0.6 and higher is planned ## Development Status -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. +ngio follows [Semantic Versioning](https://semver.org/): from 1.0 onwards the public API is +stable, and breaking changes are reserved for major releases. ### Available Features 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..15f20220 100644 --- a/docs/api/hcs.md +++ b/docs/api/hcs.md @@ -1,3 +1,7 @@ +--- +description: API reference for OmeZarrPlate and OmeZarrWell. +--- + # HCS API Documentation ## Open a Plate diff --git a/docs/api/images.md b/docs/api/images.md index e0bf91c4..49693f4d 100644 --- a/docs/api/images.md +++ b/docs/api/images.md @@ -1,3 +1,7 @@ +--- +description: API reference for the Image and Label objects. +--- + # Images Like: API Documentation ## Open an Image diff --git a/docs/api/iterators.md b/docs/api/iterators.md index 9e8a0052..3a839161 100644 --- a/docs/api/iterators.md +++ b/docs/api/iterators.md @@ -1,3 +1,7 @@ +--- +description: API reference for the ngio processing iterators. +--- + # Iterators API Reference ## ImageProcessingIterator diff --git a/docs/api/ome_zarr_container.md b/docs/api/ome_zarr_container.md index 62c4083e..040d3a9a 100644 --- a/docs/api/ome_zarr_container.md +++ b/docs/api/ome_zarr_container.md @@ -1,3 +1,7 @@ +--- +description: API reference for opening and creating OME-Zarr containers. +--- + # OmeZarrContainer: API Documentation ## Open an OME-Zarr Container diff --git a/docs/api/tables.md b/docs/api/tables.md index 71a4b756..5b714e5b 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 Documentation + +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..b1b625cc --- /dev/null +++ b/docs/assets/logo-dark.svg @@ -0,0 +1,16 @@ + + ngio + + + + + + + + + + + + + + diff --git a/docs/assets/logo-mono.svg b/docs/assets/logo-mono.svg new file mode 100644 index 00000000..afa28cec --- /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/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 022d8837..50689140 100644 --- a/docs/getting_started/0_quickstart.md +++ b/docs/getting_started/0_quickstart.md @@ -1,6 +1,10 @@ +--- +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. +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. ## Installation @@ -34,7 +38,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,7 +49,7 @@ 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 our [GitHub repository](https://github.com/BioVisionCenter/ngio). ## Setup some test data @@ -81,7 +85,7 @@ To learn how to work with the `OME-Zarr Container` object, but also with the ima - [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. -Also, checkout our jupyer notebook tutorials for more examples: +For worked end-to-end examples, see the tutorials: - [Image Processing](../tutorials/image_processing.md): Learn how to perform simple image processing operations. - [Image Segmentation](../tutorials/image_segmentation.md): Learn how to create new labels from images. diff --git a/docs/getting_started/1_ome_zarr_containers.md b/docs/getting_started/1_ome_zarr_containers.md index 9b9d802a..e2e164ed 100644 --- a/docs/getting_started/1_ome_zarr_containers.md +++ b/docs/getting_started/1_ome_zarr_containers.md @@ -1,3 +1,7 @@ +--- +description: "The OME-Zarr container object: inspect and modify metadata, derive and create images, and open remote stores." +--- + # 1. OME-Zarr Container Let's see how to open and explore an OME-Zarr image using `ngio`: @@ -6,7 +10,7 @@ Let's see how to open and explore an OME-Zarr image using `ngio`: --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. +The `OME-Zarr Container` is your entry point to working with OME-Zarr images. It provides high-level access to the image metadata, images, labels, and tables. ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:print_container" @@ -175,7 +179,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 @@ -198,6 +202,7 @@ For publicly available OME-Zarr containers, you can just use the `open_ome_zarr_ 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 = ( @@ -215,8 +220,14 @@ For fractal users, the `fractal_fsspec_store` function can be used to open priva In this case we need to provide a `fractal_token` to authenticate the user. ```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 dc44c0a5..f2847970 100644 --- a/docs/getting_started/2_images.md +++ b/docs/getting_started/2_images.md @@ -1,8 +1,12 @@ +--- +description: "Read and write OME-Zarr pixel data: resolution levels, numpy and dask access, slicing, and labels." +--- + # 2. Images and Labels ## Images -In order to start working with the image data, we need to instantiate an `Image` object. +In order to start working with the image data, we need to 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 @@ -43,7 +47,7 @@ Similarly to the `OME-Zarr Container`, the `Image` object provides a high-level ```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" ```python exec="true" source="material-block" session="get_started" @@ -107,7 +111,7 @@ A minimal example of how to use the `get_array` and `set_array` methods: ### 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. ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:roi_slicing" @@ -168,3 +172,9 @@ Often, you might want to create a new label based on an existing image. You can 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). + +## 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 18262d6e..5f292c50 100644 --- a/docs/getting_started/3_tables.md +++ b/docs/getting_started/3_tables.md @@ -1,6 +1,10 @@ +--- +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/). +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/). ## Getting a table @@ -18,7 +22,7 @@ We can list all available tables and load a specific table: --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 supports three types of tables: `roi_table`, `feature_table`, and `masking_roi_table`, as well as untyped `generic_table`. === "ROI Table" ROI tables can be used to store arbitrary regions of interest (ROIs) in the image. @@ -101,7 +105,7 @@ Tables (differently from Images and Labels) can be purely in memory objects, and === "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`: ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:create_generic_table" @@ -111,3 +115,9 @@ Tables (differently from Images and Labels) can be purely in memory objects, and --8<-- "docs/snippets/getting_started/get_started.py:generic_table_from_anndata" ``` The `GenericTable` class allows you to store any tabular data, and is a flexible way to work with tables in ngio. + +## 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 9f69e45a..9044d8b7 100644 --- a/docs/getting_started/4_masked_images.md +++ b/docs/getting_started/4_masked_images.md @@ -1,3 +1,7 @@ +--- +description: Read and write image data object-by-object using a segmentation as a mask. +--- + # 4. Masked Images and Labels Masked images (or labels) are images that are masked by an instance segmentation mask. @@ -32,7 +36,7 @@ The two most notable exceptions are the `get_roi_as_numpy` (or `get_roi_as_dask` --8<-- "docs/snippets/getting_started/masked_images.py:plot_masked_roi" ``` -Additionally we can used the `zoom_factor` argument to get more context around the ROI. +Additionally we can use the `zoom_factor` argument to get more context around the ROI. For example we can zoom out the ROI by a factor of `2`: ```python exec="true" source="material-block" session="masked_images" @@ -70,10 +74,15 @@ We can also use the `set_roi_masked` method to set the masked data for a specifi ## 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. ```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 ab7742b5..d4bb3cfb 100644 --- a/docs/getting_started/5_hcs.md +++ b/docs/getting_started/5_hcs.md @@ -1,8 +1,12 @@ +--- +description: "Work with high-content screening plates: rows, columns, acquisitions, wells and images." +--- + # 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. +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 plate 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. +The HCS plate is represented by the [`OmeZarrPlate`][ngio.OmeZarrPlate] class. Let's open an `OmeZarrPlate` object. @@ -56,7 +60,7 @@ The `OmeZarrPlate` object provides multiple methods to retrieve the path to the ## 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. +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`][ngio.OmeZarrContainer] objects. === "All Images" Get all images in the plate: @@ -88,7 +92,7 @@ The `OmeZarrPlate` object provides a method to get the image objects in a well. ## 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. @@ -132,3 +136,9 @@ You can add images or remove images 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 421ca2b2..6200cb88 100644 --- a/docs/getting_started/6_iterators.md +++ b/docs/getting_started/6_iterators.md @@ -1,14 +1,24 @@ +--- +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). 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. -Ngio provides a set of `Iterator` classes that can be used for this purpose. We provide iterators four basic iterators: +ngio provides a set of `Iterator` classes that can be used for this purpose. We provide four basic iterators: * 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.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. An example use case on how to use the `MaskedSegmentationIterator` can be found in 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. An example use case on how to use the `ImageProcessingIterator` can be found in the [Image Processing Tutorial](../tutorials/image_processing.md). -* 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.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. An example use case on how to use the `FeatureExtractorIterator` can be found in the [Feature Extraction Tutorial](../tutorials/feature_extraction.md). A set of more complete example can be found in the [Fractal Tasks Template](https://github.com/fractal-analytics-platform/fractal-tasks-template). + +## Next steps + +- [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..f774373a 100644 --- a/docs/getting_started/7_configuration.md +++ b/docs/getting_started/7_configuration.md @@ -1,6 +1,10 @@ +--- +description: Configure ngio via ngio_config.json, including the io_retry policy. +--- + # Configuration -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. +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. ## The config file @@ -48,7 +52,7 @@ Fields: - `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 @@ -61,3 +65,8 @@ Ngio's own errors (`NgioError` subclasses, e.g. validation errors) are never ret `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 b5e77edb..ae5d28ba 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,66 +1,119 @@ -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 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. +**A Python library for OME-Zarr bioimage analysis.** -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. +ngio gives you an object-based API for [OME-Zarr](https://ngff.openmicroscopy.org/) — the +cloud-optimised format for large, multi-dimensional microscopy data. Open an image, +reach for the resolution level you need, work with labels, tables and regions of +interest, and scale the same code from one field of view to a whole plate. -## Key Features +## Installation -### 🔍 Simple Object-Based API +=== "pip" -- Easily open, explore, and manipulate OME-Zarr images and HCS plates -- Create and derive new images and labels with minimal boilerplate code + ```bash + pip install ngio + ``` -### 📊 Rich Tables and Regions of Interest (ROI) Support +=== "mamba/conda" -- 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 + ```bash + mamba install -c conda-forge ngio + ``` -### 🔄 Scalable Data Processing +## ngio in 30 seconds -- Powerful iterators for building scalable and generalizable image processing pipelines -- Extensible mapping mechanism for custom parallelization strategies +```python +from ngio import open_ome_zarr_container -## Getting Started +# 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 -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.md) to help you get up and running quickly. -For more advanced usage and API documentation, see our [API Reference](api/ome_zarr_container.md). +# Grab the highest-resolution image and read a channel as numpy +image = ome_zarr.get_image() +data = image.get_as_numpy(channel_selection="DAPI") -## Supported OME-Zarr versions +# 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) +``` + +Every code block in these docs is executed when the site is built, so what you read is +what actually runs. + +## 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** -Ngio supports OME-Zarr v0.4 and v0.5, with Zarr v2 and v3 storage formats. + --- -## Development Status + End-to-end walkthroughs: create an OME-Zarr, process and segment images, extract + features, and explore a plate. -!!! 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. + [:octicons-arrow-right-24: Browse tutorials](tutorials/create_ome_zarr.md) -### Available Features +- :material-table:{ .lg .middle } **Table Specifications** -- ✅ 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 + --- -### Upcoming Features + The on-disk spec for ROI, masking ROI, feature, condition and generic tables, and + the backends that store them. -- Enhanced performance optimizations (parallel iterators, optimized io strategies) + [:octicons-arrow-right-24: Read the spec](table_specs/overview.md) -## Contributors +- :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) + +
+ +## Key features + +- **Simple object-based API** — open, explore and manipulate OME-Zarr images and HCS + plates; derive new images and labels with minimal boilerplate. +- **Rich tables and ROI support** — 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. + +## Supported OME-Zarr versions -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). +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. -## License +## Citing ngio -Ngio is released under the BSD-3-Clause License. See [LICENSE](https://github.com/BioVisionCenter/ngio/blob/main/LICENSE) for details. +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. -## Repository +## Project -Visit our [GitHub repository](https://github.com/BioVisionCenter/ngio) for the latest code, issues, and contributions. +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..0af1c8a3 --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,56 @@ +# ngio + +> ngio 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`. Every code example in these +docs is 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](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. + +## 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/): OmeZarrPlate, OmeZarrWell and the plate helpers. +- [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. +- [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. diff --git a/docs/stylesheets/ngio.css b/docs/stylesheets/ngio.css new file mode 100644 index 00000000..3221c8db --- /dev/null +++ b/docs/stylesheets/ngio.css @@ -0,0 +1,460 @@ +/* ══════════════════════════════════════════════════════════════════════════ + 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; +} + +/* ── 2. Palette ───────────────────────────────────────────────────────────── + These are Zensical's own variable names (see assets/stylesheets/*/palette.css). + 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; + + --md-footer-bg-color: #f9fafa; + --md-footer-bg-color--dark: #eff1f1; + + --ngio-line: #e3e6e6; + --ngio-line-strong: #cfd4d4; + --ngio-surface: #ffffff; + --ngio-sunk: #eff1f1; + --ngio-on-accent: #ffffff; + --ngio-accent-fill: var(--ngio-accent-ink); +} + +[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; + + --md-footer-bg-color: #0b1113; + --md-footer-bg-color--dark: #0f1618; + + --ngio-line: #1d272a; + --ngio-line-strong: #2a3639; + --ngio-surface: #131b1e; + --ngio-sunk: #0f1618; + --ngio-on-accent: #06211e; + --ngio-accent-fill: var(--ngio-accent); +} + +/* ── 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; +} + +.md-typeset h1 { + font-weight: 600; + font-size: 2.6rem; + line-height: 1.08; + letter-spacing: -0.035em; + 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: 42ch; + text-wrap: pretty; +} + +.md-typeset p, +.md-typeset li { + text-wrap: pretty; +} + +/* ── 4. Header and tabs ──────────────────────────────────────────────────── */ +.md-header { + background: var(--md-default-bg-color); + color: var(--md-default-fg-color); + box-shadow: none; + border-bottom: 1px solid var(--ngio-line); +} + +.md-header__button.md-logo :is(img, svg) { + height: 1.5rem; + width: 1.5rem; +} + +.md-tabs { + background: var(--md-default-bg-color); + color: var(--md-default-fg-color--light); + border-bottom: 1px solid var(--ngio-line); +} + +.md-tabs__link { + font-size: 0.72rem; + font-weight: 500; + opacity: 1; + color: var(--md-default-fg-color--light); +} + +.md-tabs__item--active .md-tabs__link { + color: var(--md-default-fg-color); + box-shadow: inset 0 -2px 0 var(--md-primary-fg-color); +} + +/* ── 5. Sidebar ──────────────────────────────────────────────────────────── */ +.md-nav { + font-size: 0.72rem; +} + +.md-nav__link { + border-radius: 7px; + padding: 0.3rem 0.6rem; +} + +.md-nav__link--active, +.md-nav__link[data-md-state="blur"]:hover { + 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); +} + +/* ── 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 180ms, + box-shadow 180ms, + transform 180ms; +} + +.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); +} + +.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 ──────────────────────────────────────────────────────── */ +.md-typeset .admonition, +.md-typeset details { + 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 .admonition-title::before, +.md-typeset summary::before { + background-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 ──────────────────────────────────────────────────────────── */ +.md-footer { + border-top: 1px solid var(--ngio-line); +} + +.md-footer-meta { + background: var(--md-footer-bg-color); + 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 ───────────────────────────────────────────────────── */ +.md-search__button { + border: 1px solid var(--ngio-line-strong); + border-radius: 8px; + background: var(--ngio-surface); + color: var(--md-default-fg-color--light); + font-size: 0.7rem; +} diff --git a/docs/table_specs/backend.md b/docs/table_specs/backend.md index bbc0c50d..46b7aa5a 100644 --- a/docs/table_specs/backend.md +++ b/docs/table_specs/backend.md @@ -1,3 +1,7 @@ +--- +description: "On-disk table backends: anndata, parquet, csv and json." +--- + # Table Backends In ngio we implemented four different table backends. Each table backend is a python class that can serialize tabular data into OME-Zarr containers. @@ -13,7 +17,7 @@ AnnData is a widely used format in single-cell genomics, and can natively store The following normalization steps are applied to each table before saving it to the AnnData backend: -- 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`. +- We separate the table in two parts: The floating point columns are cast to `float32` and stored as `X` in the AnnData object, while the categorical, boolean, and integer columns are stored as `obs`. - 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. @@ -22,7 +26,7 @@ AnnData backend metadata: ```json { // 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" } @@ -47,7 +51,7 @@ Parquet backend metadata: ```json { // 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" } @@ -71,7 +75,7 @@ The CSV backend in ngio follows closely the same specifications as the Parquet b ```json { // 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" } @@ -95,7 +99,7 @@ JSON backend metadata: ```json { // 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" } diff --git a/docs/table_specs/overview.md b/docs/table_specs/overview.md index ebe36927..20b5b312 100644 --- a/docs/table_specs/overview.md +++ b/docs/table_specs/overview.md @@ -1,10 +1,14 @@ +--- +description: "The ngio table architecture: backends, in-memory objects and type specifications." +--- + # Tables Overview -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/). +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/). ## Architecture -The ngio tables architectures is composed of three main components: +The ngio tables architecture is composed of three main components: ### 1. Table Backends @@ -13,7 +17,7 @@ A backend module is a class that can serialize tabular data into OME-Zarr contai - **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. +- **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). diff --git a/docs/table_specs/table_types/condition_table.md b/docs/table_specs/table_types/condition_table.md index e6f14d2c..fdebef13 100644 --- a/docs/table_specs/table_types/condition_table.md +++ b/docs/table_specs/table_types/condition_table.md @@ -21,7 +21,7 @@ A condition table must include the following metadata fields in the group attrib "type": "condition_table", "table_version": "1", // 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 condition table, which is used to identify each row. "index_type": "int" // Either "int" or "str" } diff --git a/docs/table_specs/table_types/custom_table.md b/docs/table_specs/table_types/custom_table.md index c1996b6a..92016a1a 100644 --- a/docs/table_specs/table_types/custom_table.md +++ b/docs/table_specs/table_types/custom_table.md @@ -1,6 +1,26 @@ +--- +description: How custom table types fit into the ngio table architecture, and what to use until the extension API is documented. +--- + # Add a Custom Table -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. +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. + +!!! note "Extension API not yet documented" + + The mechanism for registering a custom table type is public but not yet documented in + full. Until it is, [Generic Tables](generic_table.md) are the supported way to store + arbitrary tabular data — they accept any pandas `DataFrame` or `AnnData` object and + round-trip through every [backend](../backend.md). + + 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 -!!! warning - The library is still in the early stages and full documentation for custom tables is not yet available. +- [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..88616eda 100644 --- a/docs/table_specs/table_types/feature_table.md +++ b/docs/table_specs/table_types/feature_table.md @@ -6,7 +6,7 @@ Feature tables can optionally include metadata to specify the type of features s - `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. 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: @@ -27,7 +27,7 @@ A feature table must include the following metadata fields in the group attribut "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" } diff --git a/docs/table_specs/table_types/generic_table.md b/docs/table_specs/table_types/generic_table.md index 66af2e97..0dbc42e1 100644 --- a/docs/table_specs/table_types/generic_table.md +++ b/docs/table_specs/table_types/generic_table.md @@ -2,7 +2,7 @@ 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 tables can used as a safe fallback when trying to read a table that does not match any other specific table type. +Generic tables can be used as a safe fallback when trying to read a table that does not match any other specific table type. ## Specifications @@ -16,7 +16,7 @@ A generic table should include the following metadata fields in the group attrib "type": "generic_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": "index", // The default index key for the generic table, which is used to identify each row. "index_type": "int" // Either "int" or "str" } diff --git a/docs/table_specs/table_types/masking_roi_table.md b/docs/table_specs/table_types/masking_roi_table.md index 60d5e43a..60d394de 100644 --- a/docs/table_specs/table_types/masking_roi_table.md +++ b/docs/table_specs/table_types/masking_roi_table.md @@ -21,7 +21,7 @@ A ROI table must include the following metadata fields in the group attributes: "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" } diff --git a/docs/table_specs/table_types/roi_table.md b/docs/table_specs/table_types/roi_table.md index 00d2ac66..c456f1bf 100644 --- a/docs/table_specs/table_types/roi_table.md +++ b/docs/table_specs/table_types/roi_table.md @@ -20,7 +20,7 @@ A ROI table must include the following metadata fields in the group attributes: "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" } diff --git a/docs/tutorials/create_ome_zarr.md b/docs/tutorials/create_ome_zarr.md index 40322d3b..1d09874c 100644 --- a/docs/tutorials/create_ome_zarr.md +++ b/docs/tutorials/create_ome_zarr.md @@ -1,3 +1,7 @@ +--- +description: Convert a numpy array into an OME-Zarr image and attach a ROI table. +--- + # OME-Zarr Creation This is a minimal example of how to create an OME-Zarr image using `ngio`. @@ -21,9 +25,14 @@ Let's start by converting a sample image from `skimage` to OME-Zarr format. ## Adding a ROI table to an OME-Zarr image -Often, is useful to add ROIs to OME-Zarr images to be able to retrieve them later. +It is often useful to add ROIs to OME-Zarr images to be able to retrieve them later. This can be done using the `ngio` library 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.md b/docs/tutorials/feature_extraction.md index f27a5d08..3666d1c5 100644 --- a/docs/tutorials/feature_extraction.md +++ b/docs/tutorials/feature_extraction.md @@ -1,6 +1,10 @@ +--- +description: Extract regionprops features and store them as an ngio feature table. +--- + # Feature Extraction -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. +This tutorial covers extracting regionprops features from an image with `ngio` and `skimage`, and writing them back as a table in the OME-Zarr container. Moreover we will also write the features to a table in the ome-zarr container. ## Step 1: Open the OME-Zarr Container @@ -33,3 +37,8 @@ This sections will cover how to extract regionprops features from an image using ```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.md b/docs/tutorials/hcs_exploration.md index c47848cf..c5d064d7 100644 --- a/docs/tutorials/hcs_exploration.md +++ b/docs/tutorials/hcs_exploration.md @@ -1,3 +1,7 @@ +--- +description: Explore an HCS plate, aggregate tables across images, and create a new plate. +--- + # HCS Plates This is a minimal example of how to work with OME-Zarr Plates using `ngio`. @@ -29,3 +33,8 @@ This is a minimal example of how to work with OME-Zarr Plates using `ngio`. ```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.md b/docs/tutorials/image_processing.md index 564fe50b..a6e2ba80 100644 --- a/docs/tutorials/image_processing.md +++ b/docs/tutorials/image_processing.md @@ -1,3 +1,7 @@ +--- +description: Apply a Gaussian blur eagerly, lazily with dask, and through an ngio iterator. +--- + # Image Processing This is a minimal example of how to use the `ngio` library for applying some basic image processing techniques. @@ -16,15 +20,15 @@ We will first create a simple function to apply gaussian blur to an image. This --8<-- "docs/snippets/tutorials/image_processing.py:gaussian_blur" ``` -## Step 2: Open the OmeZarr container +## 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 omeZarr container +## Step 3: Create a new empty OME-Zarr container -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 +ngio provides 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 preserves the original metadata and dimensions (unless explicitly changed when deriving). ```python exec="true" source="material-block" session="image_processing" @@ -53,10 +57,15 @@ Sometimes we want to apply some simple processing to larger than memory images. --8<-- "docs/snippets/tutorials/image_processing.py:dask_blur" ``` -## Step 6. Image Processing Iterators +## Step 6: Image Processing Iterators `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. ```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.md b/docs/tutorials/image_segmentation.md index 5da6c04e..7cb6fe60 100644 --- a/docs/tutorials/image_segmentation.md +++ b/docs/tutorials/image_segmentation.md @@ -1,3 +1,7 @@ +--- +description: Segment an OME-Zarr image per field of view, then repeat within a mask. +--- + # Image Segmentation This is a minimal tutorial on how to use ngio for image segmentation. @@ -14,7 +18,7 @@ We will first implement a very simple function to segment an image. We will use --8<-- "docs/snippets/tutorials/image_segmentation.py:segmentation_fn" ``` -## Step 2: Open the OmeZarr container +## 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" @@ -58,3 +62,8 @@ the masked image rather than the original one. ```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. diff --git a/mkdocs.yml b/mkdocs.yml index e8d75b70..7a2d1001 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,10 @@ site_url: "https://biovisioncenter.github.io/ngio/" site_description: "A Python library for processing OME-Zarr images" 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. @@ -18,47 +21,70 @@ exclude_docs: | theme: name: material - #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 @@ -93,44 +119,75 @@ plugins: show_symbol_type_heading: true show_symbol_type_toc: 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: - # MkDocs silently enabled `tables` (and toc/fenced_code) by default; Zensical does not, - # so it must be listed explicitly or the static pipe tables in docs/table_specs/** - # render as literal `|---|` text. Note this does NOT help exec-block output: Zensical - # runs no block-level Markdown over it, which is why those blocks emit HTML instead. - - tables + - abbr # glossary tooltips (with content.tooltips) - admonition + - attr_list # grid cards, code annotations, link attributes + - def_list + - footnotes + - md_in_html + - tables - pymdownx.details - - pymdownx.superfences + # 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 - - md_in_html + 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: - - "NGIO: Streamlined OME-Zarr Image Analysis": index.md + - Home: 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 + - "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.md - - tutorials/image_processing.md - - tutorials/image_segmentation.md - - tutorials/feature_extraction.md - - tutorials/hcs_exploration.md + - "Create an OME-Zarr": 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: - "Overview": table_specs/overview.md From 8011a9afbfb26c2de364f6621108992325097e29 Mon Sep 17 00:00:00 2001 From: lorenzo Date: Wed, 29 Jul 2026 17:42:53 +0200 Subject: [PATCH 04/14] style docs --- README.md | 93 +++--- docs/api/hcs.md | 10 +- docs/api/images.md | 10 +- docs/api/iterators.md | 2 +- docs/api/ngio/utils.md | 2 +- docs/api/ome_zarr_container.md | 8 +- docs/api/tables.md | 2 +- docs/assets/logo-dark.svg | 8 +- docs/assets/logo-mono.svg | 2 +- docs/getting_started/0_quickstart.md | 35 +-- docs/getting_started/1_ome_zarr_containers.md | 150 ++++++++-- docs/getting_started/2_images.md | 125 ++++++-- docs/getting_started/3_tables.md | 42 +-- docs/getting_started/4_masked_images.md | 97 +++++-- docs/getting_started/5_hcs.md | 120 ++++++-- docs/getting_started/6_iterators.md | 178 +++++++++++- docs/getting_started/7_configuration.md | 4 +- docs/index.md | 10 +- docs/llms.txt | 38 +-- docs/snippets/getting_started/get_started.py | 40 ++- .../snippets/getting_started/masked_images.py | 37 ++- docs/snippets/tutorials/create_ome_zarr.py | 37 ++- docs/snippets/tutorials/image_processing.py | 37 ++- docs/snippets/tutorials/image_segmentation.py | 37 ++- docs/stylesheets/ngio.css | 271 ++++++++++++++++-- docs/table_specs/backend.md | 36 +-- docs/table_specs/overview.md | 52 ++-- .../table_types/condition_table.md | 10 +- docs/table_specs/table_types/custom_table.md | 13 +- docs/table_specs/table_types/feature_table.md | 10 +- docs/table_specs/table_types/generic_table.md | 10 +- .../table_types/masking_roi_table.md | 16 +- docs/table_specs/table_types/roi_table.md | 16 +- docs/tutorials/create_ome_zarr.md | 19 +- docs/tutorials/feature_extraction.md | 18 +- docs/tutorials/hcs_exploration.md | 10 +- docs/tutorials/image_processing.md | 38 +-- docs/tutorials/image_segmentation.md | 28 +- mkdocs.yml | 51 ++-- 39 files changed, 1308 insertions(+), 414 deletions(-) diff --git a/README.md b/README.md index 8a443599..8b910495 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ngio - Next Generation file format IO +# ngio [![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 +6,68 @@ [![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. +**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 gives you an object-based API for [OME-Zarr](https://ngff.openmicroscopy.org/) — the +cloud-optimised format for large, multi-dimensional microscopy data. Open an image, reach +for the resolution level you need, work with labels, tables and regions of interest, and +scale the same code from one field of view to a whole plate. ## Installation -You can install ngio via pip: - ```bash pip install ngio ``` -To get started check out the [Quickstart Guide](https://BioVisionCenter.github.io/ngio/stable/getting_started/0_quickstart/). +or -## Supported OME-Zarr versions +```bash +mamba install -c conda-forge ngio +``` + +Then work through the +[quickstart](https://biovisioncenter.github.io/ngio/stable/getting_started/0_quickstart/). -ngio supports OME-Zarr v0.4/v0.5. Support for version 0.6 and higher is planned for future releases. +## Key features -## Development Status +- **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 follows [Semantic Versioning](https://semver.org/): from 1.0 onwards the public API is -stable, and breaking changes are reserved for major releases. +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/). Every code +block in the docs is executed when the site is built, so what you read is what actually +runs. -## 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/docs/api/hcs.md b/docs/api/hcs.md index 15f20220..17ec1425 100644 --- a/docs/api/hcs.md +++ b/docs/api/hcs.md @@ -2,20 +2,20 @@ description: API reference for OmeZarrPlate and OmeZarrWell. --- -# HCS API Documentation +# HCS API documentation -## Open a Plate +## Open a plate ::: ngio.open_ome_zarr_plate -## ngio.OmeZarrPlate Class Reference +## ngio.OmeZarrPlate class reference ::: ngio.OmeZarrPlate -## Open a Well +## Open a well ::: ngio.open_ome_zarr_well -## ngio.OmeZarrWell Class Reference +## ngio.OmeZarrWell class reference ::: ngio.OmeZarrWell diff --git a/docs/api/images.md b/docs/api/images.md index 49693f4d..5a45bd27 100644 --- a/docs/api/images.md +++ b/docs/api/images.md @@ -2,20 +2,20 @@ description: API reference for the Image and Label objects. --- -# Images Like: API Documentation +# Images API documentation -## Open an Image +## Open an image ::: ngio.open_image -## ngio.Image Class Reference +## ngio.Image class reference ::: ngio.Image -## Open a Label +## Open a label ::: ngio.open_label -## ngio.Label Class Reference +## ngio.Label class reference ::: ngio.Label diff --git a/docs/api/iterators.md b/docs/api/iterators.md index 3a839161..87c5053c 100644 --- a/docs/api/iterators.md +++ b/docs/api/iterators.md @@ -2,7 +2,7 @@ description: API reference for the ngio processing iterators. --- -# Iterators API Reference +# Iterators API documentation ## ImageProcessingIterator diff --git a/docs/api/ngio/utils.md b/docs/api/ngio/utils.md index 01aeccad..2d43eb32 100644 --- a/docs/api/ngio/utils.md +++ b/docs/api/ngio/utils.md @@ -1,3 +1,3 @@ -# ngio.utils +# ngio.utils API documentation ::: ngio.utils diff --git a/docs/api/ome_zarr_container.md b/docs/api/ome_zarr_container.md index 040d3a9a..14077149 100644 --- a/docs/api/ome_zarr_container.md +++ b/docs/api/ome_zarr_container.md @@ -2,17 +2,17 @@ description: API reference for opening and creating OME-Zarr containers. --- -# OmeZarrContainer: API Documentation +# OmeZarrContainer: API documentation -## Open an OME-Zarr Container +## Open an OME-Zarr container ::: ngio.open_ome_zarr_container -## Create an OME-Zarr Container +## Create an OME-Zarr container ::: ngio.create_empty_ome_zarr ::: ngio.create_ome_zarr_from_array -## OmeZarrContainer Class +## OmeZarrContainer class ::: ngio.OmeZarrContainer diff --git a/docs/api/tables.md b/docs/api/tables.md index 5b714e5b..eb513aa4 100644 --- a/docs/api/tables.md +++ b/docs/api/tables.md @@ -2,7 +2,7 @@ description: API reference for ngio tables — ROI, masking ROI, feature, condition and generic tables, plus the tables container and backends. --- -# Tables API Documentation +# Tables API documentation For the on-disk format of each table type, see the [Table Specifications](../table_specs/overview.md). diff --git a/docs/assets/logo-dark.svg b/docs/assets/logo-dark.svg index b1b625cc..cc0a9acf 100644 --- a/docs/assets/logo-dark.svg +++ b/docs/assets/logo-dark.svg @@ -1,15 +1,15 @@ ngio - - + + - + - + diff --git a/docs/assets/logo-mono.svg b/docs/assets/logo-mono.svg index afa28cec..121af638 100644 --- a/docs/assets/logo-mono.svg +++ b/docs/assets/logo-mono.svg @@ -1,6 +1,6 @@ ngio - + diff --git a/docs/getting_started/0_quickstart.md b/docs/getting_started/0_quickstart.md index 50689140..a340a5d6 100644 --- a/docs/getting_started/0_quickstart.md +++ b/docs/getting_started/0_quickstart.md @@ -4,7 +4,10 @@ description: Install ngio and open your first OME-Zarr container in a few lines # 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 @@ -49,11 +52,11 @@ 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/BioVisionCenter/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" --8<-- "docs/snippets/getting_started/quickstart.py:setup" @@ -61,7 +64,7 @@ Let's start by downloading a sample OME-Zarr dataset to work with. ## 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" @@ -73,21 +76,19 @@ The `OME-Zarr Container` is the core of ngio and the entry point to working with ### 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` object 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. For worked end-to-end examples, see the tutorials: -- [Image Processing](../tutorials/image_processing.md): Learn how to perform simple image processing operations. -- [Image Segmentation](../tutorials/image_segmentation.md): Learn how to create new labels from images. -- [Feature Extraction](../tutorials/feature_extraction.md): Learn how to extract features from images. -- [HCS Exploration](../tutorials/hcs_exploration.md): 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 e2e164ed..40b7fa79 100644 --- a/docs/getting_started/1_ome_zarr_containers.md +++ b/docs/getting_started/1_ome_zarr_containers.md @@ -2,16 +2,112 @@ description: "The OME-Zarr container object: inspect and modify metadata, derive and create images, and open remote stores." --- -# 1. OME-Zarr Container - -Let's see how to open and explore an OME-Zarr image using `ngio`: +# 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 a multiscale image 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 + + + + + + + + + MultiscaleImage + 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" --8<-- "docs/snippets/getting_started/get_started.py:setup" ``` -The `OME-Zarr Container` is your entry point to working with OME-Zarr images. It provides high-level access to the image metadata, images, labels, and tables. - ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:print_container" ``` @@ -22,9 +118,7 @@ The `OME-Zarr Container` will be the starting point for all your image processin ### 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. @@ -34,19 +128,19 @@ 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` object 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: ```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: ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:level_paths" @@ -62,7 +156,7 @@ Examples of the OME-Zarr metadata access: --8<-- "docs/snippets/getting_started/get_started.py:is_time_series" ``` -=== "Full Metadata Object" +=== "Full metadata object" ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:metadata" ``` @@ -73,31 +167,31 @@ Examples of the OME-Zarr metadata access: ## 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"]) ``` -=== "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"]) ``` -=== "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)]) ``` -=== "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)) @@ -107,13 +201,13 @@ You can update channel labels, colors, and display windows: 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"]) ``` -=== "Axes Units" +=== "Axes units" Set the space and time units: ```python >>> ome_zarr_container.set_axes_units(space_unit="micrometer", time_unit="second") @@ -134,7 +228,7 @@ You can set the name of the image in the metadata: To access images, labels, and tables, you can use the `get_image`, `get_label`, and `get_table` methods of the `OME-Zarr Container` object. -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 @@ -197,9 +291,9 @@ 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 @@ -216,8 +310,8 @@ 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 @@ -229,5 +323,5 @@ ome_zarr_container = open_ome_zarr_container(store) ## Next steps -- [Images and Labels](2_images.md) — read and write pixel data. +- [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 f2847970..f8f94a18 100644 --- a/docs/getting_started/2_images.md +++ b/docs/getting_started/2_images.md @@ -2,11 +2,100 @@ description: "Read and write OME-Zarr pixel data: resolution levels, numpy and dask access, slicing, and labels." --- -# 2. Images 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`][ngio.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 @@ -15,26 +104,26 @@ ngio provides a high-level API to access the image data at different resolution --8<-- "docs/snippets/getting_started/get_started.py:reopen_container" ``` -=== "Highest Resolution Image" +=== "Highest resolution image" By default, the `get_image` method returns the highest resolution image: ```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: ```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: ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:get_image_by_pixel_size" ``` -=== "Nearest Resolution" +=== "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: ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:get_image_nearest" @@ -49,13 +138,13 @@ Similarly to the `OME-Zarr Container`, the `Image` object provides a high-level ``` The `dimensions` attribute returns an object with the image dimensions for each axis. -=== "Pixel Size" +=== "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" +=== "On-disk array info" ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:image_array_info" ``` @@ -65,12 +154,12 @@ Similarly to the `OME-Zarr Container`, the `Image` object provides a high-level Once you have the `Image` object, you can access the image data as a: -=== "Numpy Array" +=== "Numpy array" ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:image_as_numpy" ``` -=== "Dask Array" +=== "Dask array" ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:image_as_dask" ``` @@ -124,34 +213,34 @@ be accessed and manipulated in 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: ```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: +There are `4` labels available in this image. Here is how to access them: -=== "Highest Resolution Label" +=== "Highest resolution label" By default, the `get_label` method returns the highest resolution label: ```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: ```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: ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:get_label_by_pixel_size" ``` -=== "Nearest Resolution" +=== "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: ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:get_label_nearest" @@ -171,10 +260,10 @@ Often, you might want to create a new label based on an existing image. You can ``` 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. +- [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 5f292c50..51b60579 100644 --- a/docs/getting_started/3_tables.md +++ b/docs/getting_started/3_tables.md @@ -4,11 +4,15 @@ description: "Load and create ngio tables: ROI, masking ROI, feature and generic # 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 [Fractal's table spec](https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/). ## 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" @@ -24,9 +28,9 @@ We can list all available tables and load a specific table: ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_roi_table`, as well as untyped `generic_table`. -=== "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: + 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" ``` @@ -46,7 +50,7 @@ ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_ --8<-- "docs/snippets/getting_started/get_started.py:plot_fov_roi_crop" ``` -=== "Masking ROI Table" +=== "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. ```python exec="true" source="material-block" session="get_started" @@ -60,10 +64,10 @@ ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_ ```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. + 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 +=== "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" ``` @@ -73,9 +77,9 @@ ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_ ## 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" +=== "Creating a ROI table" ```python exec="true" source="material-block" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:create_roi_table" ``` @@ -85,39 +89,37 @@ Tables (differently from Images and Labels) can be purely in memory objects, and ``` 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: + 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: +=== "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`: ```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`][ngio.tables.GenericTable] class, which allows you to store any tabular data. It can be created from a pandas `Dataframe`: ```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: + 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" ``` - The `GenericTable` class allows you to store any tabular data, and is a flexible way to work with tables in ngio. ## 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. +- [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 9044d8b7..9db38e3f 100644 --- a/docs/getting_started/4_masked_images.md +++ b/docs/getting_started/4_masked_images.md @@ -2,27 +2,90 @@ description: Read and write image data object-by-object using a segmentation as a mask. --- -# 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. +# 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` object, using the `get_masked_image` method. -Let's create a masked image from the `nuclei` label: +Create a masked image from the `nuclei` label: ```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. ```python exec="true" source="material-block" session="masked_images" --8<-- "docs/snippets/getting_started/masked_images.py:masked_roi_numpy" @@ -36,8 +99,8 @@ The two most notable exceptions are the `get_roi_as_numpy` (or `get_roi_as_dask` --8<-- "docs/snippets/getting_started/masked_images.py:plot_masked_roi" ``` -Additionally we can use the `zoom_factor` argument to get more context around the ROI. -For example we can zoom out the ROI by a factor of `2`: +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`: ```python exec="true" source="material-block" session="masked_images" --8<-- "docs/snippets/getting_started/masked_images.py:masked_roi_zoom" @@ -51,8 +114,8 @@ For example we can zoom out the ROI by a factor of `2`: 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. -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 these operations, use the `get_roi_masked` and `set_roi_masked` methods. +For example, use `get_roi_masked` to get the masked data for a specific label: ```python exec="true" source="material-block" session="masked_images" --8<-- "docs/snippets/getting_started/masked_images.py:get_roi_masked" @@ -62,7 +125,7 @@ For example, we can use the `get_roi_masked` method to get the masked data for a --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: +Use the `set_roi_masked` method to set the masked data for a specific label: ```python exec="true" source="material-block" session="masked_images" --8<-- "docs/snippets/getting_started/masked_images.py:set_roi_masked" @@ -72,11 +135,11 @@ We can also use the `set_roi_masked` method to set the masked data for a specifi --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`][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` object using the `get_masked_label` method. ```python exec="true" source="material-block" session="masked_images" --8<-- "docs/snippets/getting_started/masked_images.py:get_masked_label" @@ -84,5 +147,5 @@ The `MaskedLabel` class can be used to create a masked label from an `OME-Zarr C ## Next steps -- [HCS Plates](5_hcs.md) — scale up from a single image to a whole plate. +- [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 d4bb3cfb..94a7a617 100644 --- a/docs/getting_started/5_hcs.md +++ b/docs/getting_started/5_hcs.md @@ -2,13 +2,95 @@ description: "Work with high-content screening plates: rows, columns, acquisitions, wells and images." --- -# 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 plate 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`][ngio.OmeZarrPlate] class. - -Let's open an `OmeZarrPlate` object. +# 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" @@ -31,7 +113,7 @@ The `OmeZarrPlate` object provides a high-level overview of the plate, including --8<-- "docs/snippets/getting_started/hcs.py:plate_rows" ``` === "Acquisitions" - Show the acquisitions ids: + Show the acquisition ids: ```python exec="true" source="material-block" session="hcs_plate" --8<-- "docs/snippets/getting_started/hcs.py:plate_acquisitions" ``` @@ -40,19 +122,19 @@ The `OmeZarrPlate` object provides a high-level overview of the plate, including 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: ```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: ```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: ```python exec="true" source="material-block" session="hcs_plate" --8<-- "docs/snippets/getting_started/hcs.py:well_images_paths" @@ -62,28 +144,28 @@ The `OmeZarrPlate` object provides multiple methods to retrieve the path to the 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`][ngio.OmeZarrContainer] objects. -=== "All Images" +=== "All images" Get all images in the plate: ```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: ```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: ```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. ```python exec="true" source="material-block" session="hcs_plate" --8<-- "docs/snippets/getting_started/hcs.py:get_well_images_by_acquisition" @@ -113,9 +195,9 @@ This has created a new empty plate with the metadata correctly set. But no image ### Modifying the plate -You can add images or remove images +You can add or remove images. -=== "Add 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. ```python exec="true" source="material-block" session="hcs_plate" --8<-- "docs/snippets/getting_started/hcs.py:plate_add_image" @@ -126,7 +208,7 @@ 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" +=== "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. ```python exec="true" source="material-block" session="hcs_plate" --8<-- "docs/snippets/getting_started/hcs.py:plate_remove_image" @@ -140,5 +222,5 @@ You can add images or remove images ## 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 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 6200cb88..bf2e6a34 100644 --- a/docs/getting_started/6_iterators.md +++ b/docs/getting_started/6_iterators.md @@ -4,21 +4,179 @@ description: The four ngio iterators for building scalable image-processing pipe # 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.** -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. +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. -ngio provides a set of `Iterator` classes that can be used for this purpose. We provide four basic iterators: + +
+ + 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 + -* 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.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. An example use case on how to use the `MaskedSegmentationIterator` can be found in 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. An example use case on how to use the `ImageProcessingIterator` can be found in 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. An example use case on how to use the `FeatureExtractorIterator` can be found in the [Feature Extraction Tutorial](../tutorials/feature_extraction.md). + + + + + region 1 + region 2 + region 3 + + -A set of more complete example can be found in the [Fractal Tasks Template](https://github.com/fractal-analytics-platform/fractal-tasks-template). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + process() + + + + + + + + + + + + + + + + + + + + + + repeat for every region the table names + +
+ +ngio provides four basic `Iterator` classes: + + +
+ + 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). + +More complete examples can be found in the [Fractal tasks template](https://github.com/fractal-analytics-platform/fractal-tasks-template). ## Next steps -- [Image Processing tutorial](../tutorials/image_processing.md) — an iterator applied end to end. -- [Image Segmentation tutorial](../tutorials/image_segmentation.md) — segmentation and masked segmentation. +- [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 f774373a..44b2df31 100644 --- a/docs/getting_started/7_configuration.md +++ b/docs/getting_started/7_configuration.md @@ -4,11 +4,11 @@ description: Configure ngio via ngio_config.json, including the io_retry policy. # Configuration -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. +ngio has a small global configuration object that controls cross-cutting IO behaviour. It is loaded once at import time from a JSON file and is accessible programmatically. ## 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. Only `.json` files are supported. A missing file means all defaults. ```json { diff --git a/docs/index.md b/docs/index.md index ae5d28ba..7579dc1c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ what actually runs.
-- :material-rocket-launch:{ .lg .middle } **Getting Started** +- :material-rocket-launch:{ .lg .middle } **Getting started** --- @@ -68,7 +68,7 @@ what actually runs. [:octicons-arrow-right-24: Browse tutorials](tutorials/create_ome_zarr.md) -- :material-table:{ .lg .middle } **Table Specifications** +- :material-table:{ .lg .middle } **Table specifications** --- @@ -77,7 +77,7 @@ what actually runs. [:octicons-arrow-right-24: Read the spec](table_specs/overview.md) -- :material-api:{ .lg .middle } **API Reference** +- :material-api:{ .lg .middle } **API reference** --- @@ -90,9 +90,9 @@ what actually runs. ## Key features -- **Simple object-based API** — open, explore and manipulate OME-Zarr images and HCS +- **Object-based API** — open, explore and manipulate OME-Zarr images and HCS plates; derive new images and labels with minimal boilerplate. -- **Rich tables and ROI support** — tight integration with [tabular +- **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 diff --git a/docs/llms.txt b/docs/llms.txt index 0af1c8a3..28319174 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -12,36 +12,36 @@ reach images and labels (`get_image`, `get_label`), tables (`get_table`), masked (`get_masked_image`) and, for plates, `open_ome_zarr_plate`. Every code example in these docs is executed when the site is built, so the outputs shown are real. -## Getting Started +## 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. +- [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. +- [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](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. +- [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 +## 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. - -## API Reference +- [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. + +## 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. diff --git a/docs/snippets/getting_started/get_started.py b/docs/snippets/getting_started/get_started.py index 1a8ff6fb..3f9df443 100644 --- a/docs/snippets/getting_started/get_started.py +++ b/docs/snippets/getting_started/get_started.py @@ -32,11 +32,42 @@ def random_label_cmap(n_labels: int = 1000, seed: int = 0) -> ListedColormap: def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + """Print a figure as inline SVG, for markdown-exec `html` blocks. + + Recolours every bit of chrome to one sentinel, then swaps that sentinel for + a theme variable in the emitted markup. The figure therefore follows the + light/dark toggle rather than baking black text on white 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. + + The `.ngio-figure` wrapper is what the stylesheet keys on to strip the + `OUT` terminal-output treatment that `.result` applies by default. + """ + ink = "#5b6569" + for ax in fig.axes: + ax.tick_params(colors=ink, which="both") + for spine in ax.spines.values(): + spine.set_edgecolor(ink) + for text in (ax.title, ax.xaxis.label, ax.yaxis.label): + text.set_color(ink) + for label in ax.get_xticklabels() + ax.get_yticklabels(): + label.set_color(ink) + if ax.get_legend() is not None: + for text in ax.get_legend().get_texts(): + text.set_color(ink) + for text in fig.texts: + text.set_color(ink) + buffer = StringIO() - fig.savefig(buffer, format="svg") + fig.savefig(buffer, format="svg", transparent=True) plt.close(fig) - print(buffer.getvalue()) + + # 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}
') # --8<-- [end:plot_helpers] @@ -320,7 +351,8 @@ def process(patch: np.ndarray) -> np.ndarray: (x_slice.start, y_slice.start), x_slice.length, y_slice.length, - edgecolor="red", + # Magenta is the docs' semantic colour for tables and ROIs. + edgecolor="#c2185b", facecolor="none", lw=2, ) diff --git a/docs/snippets/getting_started/masked_images.py b/docs/snippets/getting_started/masked_images.py index 9930fa73..d75cd385 100644 --- a/docs/snippets/getting_started/masked_images.py +++ b/docs/snippets/getting_started/masked_images.py @@ -18,11 +18,42 @@ def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + """Print a figure as inline SVG, for markdown-exec `html` blocks. + + Recolours every bit of chrome to one sentinel, then swaps that sentinel for + a theme variable in the emitted markup. The figure therefore follows the + light/dark toggle rather than baking black text on white 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. + + The `.ngio-figure` wrapper is what the stylesheet keys on to strip the + `OUT` terminal-output treatment that `.result` applies by default. + """ + ink = "#5b6569" + for ax in fig.axes: + ax.tick_params(colors=ink, which="both") + for spine in ax.spines.values(): + spine.set_edgecolor(ink) + for text in (ax.title, ax.xaxis.label, ax.yaxis.label): + text.set_color(ink) + for label in ax.get_xticklabels() + ax.get_yticklabels(): + label.set_color(ink) + if ax.get_legend() is not None: + for text in ax.get_legend().get_texts(): + text.set_color(ink) + for text in fig.texts: + text.set_color(ink) + buffer = StringIO() - fig.savefig(buffer, format="svg") + fig.savefig(buffer, format="svg", transparent=True) plt.close(fig) - print(buffer.getvalue()) + + # 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}') # --8<-- [end:plot_helpers] diff --git a/docs/snippets/tutorials/create_ome_zarr.py b/docs/snippets/tutorials/create_ome_zarr.py index b6f95647..209dfd63 100644 --- a/docs/snippets/tutorials/create_ome_zarr.py +++ b/docs/snippets/tutorials/create_ome_zarr.py @@ -18,11 +18,42 @@ def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + """Print a figure as inline SVG, for markdown-exec `html` blocks. + + Recolours every bit of chrome to one sentinel, then swaps that sentinel for + a theme variable in the emitted markup. The figure therefore follows the + light/dark toggle rather than baking black text on white 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. + + The `.ngio-figure` wrapper is what the stylesheet keys on to strip the + `OUT` terminal-output treatment that `.result` applies by default. + """ + ink = "#5b6569" + for ax in fig.axes: + ax.tick_params(colors=ink, which="both") + for spine in ax.spines.values(): + spine.set_edgecolor(ink) + for text in (ax.title, ax.xaxis.label, ax.yaxis.label): + text.set_color(ink) + for label in ax.get_xticklabels() + ax.get_yticklabels(): + label.set_color(ink) + if ax.get_legend() is not None: + for text in ax.get_legend().get_texts(): + text.set_color(ink) + for text in fig.texts: + text.set_color(ink) + buffer = StringIO() - fig.savefig(buffer, format="svg") + fig.savefig(buffer, format="svg", transparent=True) plt.close(fig) - print(buffer.getvalue()) + + # 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}') # --8<-- [end:plot_helpers] diff --git a/docs/snippets/tutorials/image_processing.py b/docs/snippets/tutorials/image_processing.py index 701faf88..19f56274 100644 --- a/docs/snippets/tutorials/image_processing.py +++ b/docs/snippets/tutorials/image_processing.py @@ -18,11 +18,42 @@ def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + """Print a figure as inline SVG, for markdown-exec `html` blocks. + + Recolours every bit of chrome to one sentinel, then swaps that sentinel for + a theme variable in the emitted markup. The figure therefore follows the + light/dark toggle rather than baking black text on white 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. + + The `.ngio-figure` wrapper is what the stylesheet keys on to strip the + `OUT` terminal-output treatment that `.result` applies by default. + """ + ink = "#5b6569" + for ax in fig.axes: + ax.tick_params(colors=ink, which="both") + for spine in ax.spines.values(): + spine.set_edgecolor(ink) + for text in (ax.title, ax.xaxis.label, ax.yaxis.label): + text.set_color(ink) + for label in ax.get_xticklabels() + ax.get_yticklabels(): + label.set_color(ink) + if ax.get_legend() is not None: + for text in ax.get_legend().get_texts(): + text.set_color(ink) + for text in fig.texts: + text.set_color(ink) + buffer = StringIO() - fig.savefig(buffer, format="svg") + fig.savefig(buffer, format="svg", transparent=True) plt.close(fig) - print(buffer.getvalue()) + + # 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}') # --8<-- [end:plot_helpers] diff --git a/docs/snippets/tutorials/image_segmentation.py b/docs/snippets/tutorials/image_segmentation.py index 4ae37977..6d28819f 100644 --- a/docs/snippets/tutorials/image_segmentation.py +++ b/docs/snippets/tutorials/image_segmentation.py @@ -28,11 +28,42 @@ def random_label_cmap(n_labels: int = 1000, seed: int = 0) -> ListedColormap: def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks.""" + """Print a figure as inline SVG, for markdown-exec `html` blocks. + + Recolours every bit of chrome to one sentinel, then swaps that sentinel for + a theme variable in the emitted markup. The figure therefore follows the + light/dark toggle rather than baking black text on white 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. + + The `.ngio-figure` wrapper is what the stylesheet keys on to strip the + `OUT` terminal-output treatment that `.result` applies by default. + """ + ink = "#5b6569" + for ax in fig.axes: + ax.tick_params(colors=ink, which="both") + for spine in ax.spines.values(): + spine.set_edgecolor(ink) + for text in (ax.title, ax.xaxis.label, ax.yaxis.label): + text.set_color(ink) + for label in ax.get_xticklabels() + ax.get_yticklabels(): + label.set_color(ink) + if ax.get_legend() is not None: + for text in ax.get_legend().get_texts(): + text.set_color(ink) + for text in fig.texts: + text.set_color(ink) + buffer = StringIO() - fig.savefig(buffer, format="svg") + fig.savefig(buffer, format="svg", transparent=True) plt.close(fig) - print(buffer.getvalue()) + + # 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}') # --8<-- [end:plot_helpers] diff --git a/docs/stylesheets/ngio.css b/docs/stylesheets/ngio.css index 3221c8db..1a59a306 100644 --- a/docs/stylesheets/ngio.css +++ b/docs/stylesheets/ngio.css @@ -19,10 +19,30 @@ --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 assets/stylesheets/*/palette.css). + 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. */ @@ -79,15 +99,28 @@ --md-code-hl-operator-color: #5b6569; --md-code-hl-punctuation-color: #5b6569; - --md-footer-bg-color: #f9fafa; - --md-footer-bg-color--dark: #eff1f1; - --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"] { @@ -136,15 +169,25 @@ --md-code-hl-operator-color: #808d90; --md-code-hl-punctuation-color: #808d90; - --md-footer-bg-color: #0b1113; - --md-footer-bg-color--dark: #0f1618; - --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 ─────────────────────────────────────────────────────── */ @@ -158,6 +201,12 @@ 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; @@ -186,7 +235,7 @@ font-size: 1.1rem; line-height: 1.5; color: var(--md-default-fg-color); - max-width: 42ch; + max-width: var(--ngio-measure-lede); text-wrap: pretty; } @@ -195,12 +244,24 @@ text-wrap: pretty; } -/* ── 4. Header and tabs ──────────────────────────────────────────────────── */ +/* 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(--md-default-bg-color); + background: var(--ngio-chrome); color: var(--md-default-fg-color); box-shadow: none; - border-bottom: 1px solid var(--ngio-line); + border-bottom: 1px solid var(--ngio-line-strong); } .md-header__button.md-logo :is(img, svg) { @@ -208,10 +269,60 @@ width: 1.5rem; } +/* ── 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(--md-default-bg-color); + background: var(--ngio-chrome); color: var(--md-default-fg-color--light); - border-bottom: 1px solid var(--ngio-line); + border-bottom: 1px solid var(--ngio-line-strong); } .md-tabs__link { @@ -221,11 +332,21 @@ 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; @@ -236,8 +357,7 @@ padding: 0.3rem 0.6rem; } -.md-nav__link--active, -.md-nav__link[data-md-state="blur"]:hover { +.md-nav__link--active { background: var(--ngio-accent-soft); color: var(--md-accent-fg-color); } @@ -308,6 +428,57 @@ 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; +} + /* ── 7. Content tabs (pip / mamba / source) ──────────────────────────────── */ .md-typeset .tabbed-set > .tabbed-labels { box-shadow: none; @@ -331,11 +502,12 @@ background: var(--ngio-surface); padding: 1.1rem; transition: - border-color 180ms, - box-shadow 180ms, - transform 180ms; + 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: @@ -344,6 +516,15 @@ 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; } @@ -359,9 +540,20 @@ fill: var(--md-primary-fg-color); } -/* ── 9. Admonitions ──────────────────────────────────────────────────────── */ -.md-typeset .admonition, -.md-typeset details { +/* ── 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; @@ -377,11 +569,18 @@ background: transparent; } -.md-typeset .admonition-title::before, -.md-typeset summary::before { +.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; @@ -420,13 +619,15 @@ text-decoration: none; } -/* ── 12. Footer ──────────────────────────────────────────────────────────── */ +/* ── 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); + border-top: 1px solid var(--ngio-line-strong); } .md-footer-meta { - background: var(--md-footer-bg-color); + background: var(--ngio-chrome); color: var(--md-default-fg-color--light); } @@ -450,11 +651,25 @@ color: var(--md-default-fg-color--light); } -/* ── 13. Search button ───────────────────────────────────────────────────── */ +/* ── 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-surface); + 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 46b7aa5a..b0820a26 100644 --- a/docs/table_specs/backend.md +++ b/docs/table_specs/backend.md @@ -1,23 +1,23 @@ --- -description: "On-disk table backends: anndata, parquet, csv and json." +description: "The on-disk table backends: AnnData, Parquet, CSV and JSON." --- -# Table Backends +# Table backends -In ngio we implemented four different table backends. Each table backend is a python class that can serialize tabular data into OME-Zarr containers. +ngio has four table backends. Each one is a Python class that can serialise tabular data into OME-Zarr containers. 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, we provide the metadata that describes the intended index key and type of the table for each backend. +In particular, the metadata describes the intended index key and type of the table for each backend. -## AnnData Backend +## 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, which performs some table normalization for consistency and compatibility with the ngio table specifications. +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. -The following normalization steps are applied to each table before saving it to the AnnData backend: +The following normalisation steps are applied to each table before saving it to the AnnData backend: -- We separate the table in two parts: The floating point columns are cast to `float32` and stored as `X` in the AnnData object, while the categorical, boolean, and integer columns are stored as `obs`. +- The table is separated in two parts: the floating point columns are cast to `float32` and stored as `X` in the AnnData object, while the categorical, boolean, and integer columns are stored as `obs`. - 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. @@ -41,10 +41,10 @@ Additionally, the AnnData package will write some additional metadata to the gro } ``` -## 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: @@ -66,9 +66,9 @@ 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: @@ -90,9 +90,9 @@ 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: @@ -105,12 +105,12 @@ JSON backend metadata: } ``` -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 └── 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 diff --git a/docs/table_specs/overview.md b/docs/table_specs/overview.md index 20b5b312..6ba90a4b 100644 --- a/docs/table_specs/overview.md +++ b/docs/table_specs/overview.md @@ -2,50 +2,50 @@ description: "The ngio table architecture: backends, in-memory objects and type specifications." --- -# Tables Overview +# Tables overview -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/). +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 derived from the [fractal table specification](https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/). ## Architecture 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. +- **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. -We also provide utilities to convert between these in-memory representations in a standardized way based on the table type specifications/metadata. +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 +### 3. Table type specifications -These specifications define structured tables that standardize common table types used in image analysis. We have defined five table types so far: +These specifications define structured tables that standardise common table types used in image analysis. ngio defines five table types so far: -- **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). +- **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). -## 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 @@ -65,7 +65,7 @@ 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 @@ -83,11 +83,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 fdebef13..72422df8 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 | diff --git a/docs/table_specs/table_types/custom_table.md b/docs/table_specs/table_types/custom_table.md index 92016a1a..adbdf583 100644 --- a/docs/table_specs/table_types/custom_table.md +++ b/docs/table_specs/table_types/custom_table.md @@ -2,16 +2,15 @@ description: How custom table types fit into the ngio table architecture, and what to use until the extension API is documented. --- -# Add a Custom Table +# Add a custom table -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. +A custom table is a table type you define yourself, for data that does not fit any of the +predefined types. !!! note "Extension API not yet documented" The mechanism for registering a custom table type is public but not yet documented in - full. Until it is, [Generic Tables](generic_table.md) are the supported way to store + full. Until it is, [generic tables](generic_table.md) are the supported way to store arbitrary tabular data — they accept any pandas `DataFrame` or `AnnData` object and round-trip through every [backend](../backend.md). @@ -21,6 +20,6 @@ not fit into the predefined table types. ## See also -- [Tables Overview](../overview.md) — the three-component table architecture. -- [Generic Tables](generic_table.md) — the untyped fallback. +- [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 88616eda..5b805fce 100644 --- a/docs/table_specs/table_types/feature_table.md +++ b/docs/table_specs/table_types/feature_table.md @@ -1,6 +1,10 @@ -# 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 tables + +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 tables can optionally include metadata to specify the type of features stored in each column: @@ -8,7 +12,7 @@ Feature tables can optionally include metadata to specify the type of features s - `categorical`: A categorical feature of the object, such as a classification label or a type. - `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. -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: +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, ngio applies the following casting rules: - 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`. diff --git a/docs/table_specs/table_types/generic_table.md b/docs/table_specs/table_types/generic_table.md index 0dbc42e1..0f5c5d88 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 tables -Generic tables can be 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 diff --git a/docs/table_specs/table_types/masking_roi_table.md b/docs/table_specs/table_types/masking_roi_table.md index 60d394de..dee77435 100644 --- a/docs/table_specs/table_types/masking_roi_table.md +++ b/docs/table_specs/table_types/masking_roi_table.md @@ -1,18 +1,22 @@ -# 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 tables + +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 { @@ -34,4 +38,4 @@ 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). +Additionally, each ROI can include the following optional columns: see [ROI table](./roi_table.md). diff --git a/docs/table_specs/table_types/roi_table.md b/docs/table_specs/table_types/roi_table.md index c456f1bf..2ffa7dba 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 @@ -37,4 +41,4 @@ Additionally, each ROI can include the following optional columns: - `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. -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 to the ROI table, but they are not exposed in the ROI table API. diff --git a/docs/tutorials/create_ome_zarr.md b/docs/tutorials/create_ome_zarr.md index 1d09874c..b3c4679c 100644 --- a/docs/tutorials/create_ome_zarr.md +++ b/docs/tutorials/create_ome_zarr.md @@ -2,14 +2,15 @@ description: Convert a numpy array into an OME-Zarr image and attach a ROI table. --- -# OME-Zarr Creation +# Create an OME-Zarr image -This is a minimal example of how to create an OME-Zarr image using `ngio`. +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. -This example is just a simple demonstration but for more complex conversion tasks please refer -to the converter tooling library [ome-zarr-converters-tools](https://github.com/BioVisionCenter/ome-zarr-converters-tools). +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). -Let's start by converting a sample image from `skimage` to OME-Zarr format. +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" @@ -25,8 +26,8 @@ Let's start by converting a sample image from `skimage` to OME-Zarr format. ## Adding a ROI table to an OME-Zarr image -It is often useful to add ROIs to OME-Zarr images to be able to retrieve them later. -This can be done using the `ngio` library as follows. +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" @@ -34,5 +35,5 @@ This can be done using the `ngio` library as follows. ## 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. +- [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.md b/docs/tutorials/feature_extraction.md index 3666d1c5..4709649d 100644 --- a/docs/tutorials/feature_extraction.md +++ b/docs/tutorials/feature_extraction.md @@ -2,11 +2,13 @@ description: Extract regionprops features and store them as an ngio feature table. --- -# Feature Extraction +# Feature extraction -This tutorial covers extracting regionprops features from an image with `ngio` and `skimage`, and writing them back as a table in the OME-Zarr container. Moreover we will also write the features to a table in the ome-zarr container. +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: Open the OME-Zarr Container +## Step 1: open the OME-Zarr container ```python exec="true" source="material-block" session="feature_extraction" --8<-- "docs/snippets/tutorials/feature_extraction.py:extract_features" @@ -16,19 +18,19 @@ This tutorial covers extracting regionprops features from an image with `ngio` a --8<-- "docs/snippets/tutorials/feature_extraction.py:open_container" ``` -## Step 2: Setup the inputs +## Step 2: set up the inputs ```python exec="true" source="material-block" session="feature_extraction" --8<-- "docs/snippets/tutorials/feature_extraction.py:setup_transform" ``` -## Step 3: Use the FeatureExtractorIterator to create a feature table +## Step 3: 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 +### Sanity check: read the table back ```python exec="true" session="feature_extraction" --8<-- "docs/snippets/tutorials/feature_extraction.py:table_helpers" @@ -40,5 +42,5 @@ This tutorial covers extracting regionprops features from an image with `ngio` a ## Next steps -- [HCS Exploration](hcs_exploration.md) — aggregate feature tables across a plate. -- [Table Specifications](../table_specs/overview.md) — how feature tables are stored. +- [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.md b/docs/tutorials/hcs_exploration.md index c5d064d7..49a7bdcc 100644 --- a/docs/tutorials/hcs_exploration.md +++ b/docs/tutorials/hcs_exploration.md @@ -2,9 +2,11 @@ description: Explore an HCS plate, aggregate tables across images, and create a new plate. --- -# HCS Plates +# HCS exploration -This is a minimal example of how to work with OME-Zarr Plates using `ngio`. +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. ## Show what's in the plate @@ -28,7 +30,7 @@ This is a minimal example of how to work with OME-Zarr Plates using `ngio`. --8<-- "docs/snippets/tutorials/hcs_exploration.py:save_table" ``` -## Create a new empty Plate +## Create a new empty plate ```python exec="true" source="material-block" session="hcs_exploration" --8<-- "docs/snippets/tutorials/hcs_exploration.py:create_plate" @@ -36,5 +38,5 @@ This is a minimal example of how to work with OME-Zarr Plates using `ngio`. ## Next steps -- [HCS Plates](../getting_started/5_hcs.md) — the plate API in depth. +- [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.md b/docs/tutorials/image_processing.md index a6e2ba80..e8f9d895 100644 --- a/docs/tutorials/image_processing.md +++ b/docs/tutorials/image_processing.md @@ -2,15 +2,16 @@ description: Apply a Gaussian blur eagerly, lazily with dask, and through an ngio iterator. --- -# Image Processing +# Image processing -This is a minimal example of how to use the `ngio` library for applying some basic image processing techniques. +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. -For this example we will apply gaussian blur to an image. +## Step 1: set up -## Step 1: Setup - -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. +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" @@ -20,22 +21,23 @@ We will first create a simple function to apply gaussian blur to an image. This --8<-- "docs/snippets/tutorials/image_processing.py:gaussian_blur" ``` -## Step 2: Open the OME-Zarr container +## 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 +## Step 3: create a new empty OME-Zarr container -ngio provides 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 -preserves the original metadata and dimensions (unless explicitly changed when deriving). +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 +## 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" @@ -43,23 +45,23 @@ preserves the original metadata and dimensions (unless explicitly changed when d ### Plot the results -Finally, we can visualize the original and blurred images using `matplotlib`. +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 +## Step 5: out-of-memory processing -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. +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 +## Step 6: image processing iterators -`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. +`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" @@ -67,5 +69,5 @@ Sometimes we want to apply some simple processing to larger than memory images. ## Next steps -- [Image Segmentation](image_segmentation.md) — turn images into labels. -- [Iterators](../getting_started/6_iterators.md) — the iterator concepts behind Step 6. +- [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.md b/docs/tutorials/image_segmentation.md index 7cb6fe60..6b3b9dc0 100644 --- a/docs/tutorials/image_segmentation.md +++ b/docs/tutorials/image_segmentation.md @@ -2,13 +2,15 @@ description: Segment an OME-Zarr image per field of view, then repeat within a mask. --- -# Image Segmentation +# Image segmentation -This is a minimal tutorial on how to use ngio for image segmentation. +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: Setup +## Step 1: set up -We will first implement a very simple function to segment an image. We will use skimage to do this. +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" @@ -18,15 +20,15 @@ We will first implement a very simple function to segment an image. We will use --8<-- "docs/snippets/tutorials/image_segmentation.py:segmentation_fn" ``` -## Step 2: Open the OME-Zarr container +## 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 +## Step 3: segment the image -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. +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" @@ -38,11 +40,11 @@ For this example, we will not segment the image all at once. Instead we will ite --8<-- "docs/snippets/tutorials/image_segmentation.py:plot_segmentation" ``` -## Step 4: Masked image segmentation +## Step 4: masked image segmentation -In this example we will use a mask to restrict the segmentation to certain areas of the image. -In this case we will create a simple mask for illustration purposes, but in a real case scenario the mask could come -from another segmentation mask. +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" @@ -65,5 +67,5 @@ the masked image rather than the original one. ## 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. +- [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. diff --git a/mkdocs.yml b/mkdocs.yml index 7a2d1001..568fde3d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,7 @@ -site_name: "NGIO: Next Generation File Format I/O" +# Describes the library, not the format. The old value expanded NGFF ("next +# generation file format"), which names OME-Zarr rather than ngio, and title-cased +# a project that styles itself lowercase everywhere else. +site_name: "ngio: OME-Zarr bioimage analysis for Python" # 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. @@ -170,37 +173,41 @@ markdown_extensions: 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: - Home: index.md - - Getting Started: + - 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 + - "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 + - "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: - - "Create an OME-Zarr": 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 + - "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 @@ -219,5 +226,5 @@ nav: - changelog.md - Contributing: - - "Contributing Guide": contributing.md + - "Contributing guide": contributing.md - "Code of Conduct": code_of_conduct.md From 15ac90f2c017697605103e78d838f143d11ee77b Mon Sep 17 00:00:00 2001 From: lorenzo <lorenzo.cerrone@uzh.ch> Date: Wed, 29 Jul 2026 17:51:03 +0200 Subject: [PATCH 05/14] Update CHANGELOG.md --- CHANGELOG.md | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b90beb1b..cdeca75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,28 +8,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 -- Polish the documentation for the 1.0 release. Restore the Markdown extensions Zensical had silently dropped: declaring `markdown_extensions:` in `mkdocs.yml` **replaces** Zensical's ~30 defaults rather than extending them (`zensical/config.py:656`), so the site had been running without `attr_list`, `abbr`, `pymdownx.emoji`, `pymdownx.highlight` and superfences' mermaid fence. As a result `content.code.annotate` and `content.tooltips` were enabled but non-functional, grid cards were impossible, emoji rendered as raw Unicode and code blocks had no line anchors. The list is now explicit and commented, and deliberately omits `smartsymbols`/`caret`/`tilde`/`magiclink`/`arithmatex`. -- Give the docs a visual identity, from a commissioned design system: a new logo ("mark G", three extruded slabs of a multiscale image pyramid in isometric projection, plus `logo-dark.svg` and `logo-mono.svg` variants) and a full theme layer in `docs/stylesheets/ngio.css` — a contrast-audited teal palette for both colour schemes, Space Grotesk display type with IBM Plex Sans body and JetBrains Mono code (via `theme.font`), and component styling for the header, sidebar, code blocks, executed-snippet `OUT` result blocks, content tabs, grid cards, admonitions, mkdocstrings pages, footer, and search. Replaces the interim three-bar `currentColor` logo, green/teal stock palette, and accent-only `extra.css`. Also add navigation tabs, a back-to-top button, and working "edit this page" links — the latter needs an explicit `edit_uri: edit/main/docs/`, since Zensical would otherwise derive `edit/master/...` and every link would 404. -- Rewrite the landing page: an H1, an install snippet, a runnable 30-second example, and grid cards into the four sections. Remove the "not yet stable / 0.x releases" warning, which contradicts a 1.0 release. -- Add a glossary of 14 recurring terms, appended to every page via `pymdownx.snippets`' `auto_append` and surfaced as tooltips; add "Next steps" footers to all twelve guides and tutorials, which previously ended as dead ends; and introduce mkdocstrings symbol cross-references (`[Image][ngio.Image]`), of which there were previously none despite `autorefs` being enabled. -- Make the docs LLM-friendly: add a curated `llms.txt` at the site root following the llmstxt.org convention, and `description:` front matter on the main pages (rendered as `<meta name="description">`). -- Fix content defects: the quickstart still pointed at the old `fractal-analytics-platform/ngio` repository and advertised "jupyer notebook tutorials" that are no longer notebooks; `6_iterators.md` named a non-existent `FeatureExtractionIterator`; the landing page linked table specs through an absolute `/stable/` URL; two remote-store examples called `open_ome_zarr_container` without importing it; `"annadata"` appeared in normative JSON on six table-spec pages; and `docs/api/tables.md` was an H1-only file rendering as a blank page in the nav. Plus ~15 grammar fixes and consistent `ngio` / `OME-Zarr` casing. -- Move `CONTRIBUTING.md` and add `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1) at the repository root, single-sourced into the docs via `--8<--` so GitHub's community widgets pick them up, and add `CITATION.cff`. -- Build the documentation with [Zensical](https://zensical.org) instead of MkDocs + Material. MkDocs 1.x is unmaintained and MkDocs 2.0 drops the plugin system; Zensical is the successor from the Material team and reads the existing `mkdocs.yml`. `serve_docs` is now `zensical serve` and `build_docs` is `zensical build --clean --strict`. Versioning keeps working through the Zensical maintainers' fork of `mike`, which is installed from git via the pixi `docs` feature (it is not on PyPI) rather than from `[project.optional-dependencies]`, so ngio itself stays publishable. The gh-pages layout is unchanged, so already-published versions stay browsable. -- **Each docs page now binds its own state.** Zensical gives every page a fresh `markdown-exec` session, so a page can no longer use a variable defined on another page. `2_images.md` and `3_tables.md` include two new silent snippet sections (`reopen_container`, `reopen_image`) that re-open the sample container; they render nothing and reuse the already-extracted store (`re_unzip=False`) instead of re-extracting it. -- Fix tables printed by exec blocks rendering as literal `|---|` text. Zensical does not run block-level Markdown over `markdown-exec` output (static pipe tables in `.md` sources are unaffected), so those blocks now emit HTML through a `print_table` snippet helper. Because every theme table rule — and the JavaScript that adds the horizontal-scroll wrapper — is gated on `table:not([class])`, the helper strips the `class="dataframe"` and `border` attributes pandas adds, which would otherwise leave the table completely unstyled and unable to scroll. Values are rounded to two decimals instead of printing six. -- Add the `tables` markdown extension to `mkdocs.yml`. MkDocs implicitly enables `toc`, `tables` and `fenced_code`; Zensical does not, so the static pipe tables in the table-spec pages need it declared. -- Fix `site_url`, which pointed at the git clone URL (`https://github.com/BioVisionCenter/ngio.git`) rather than the published docs. That produced wrong `rel="canonical"` links and, under Zensical, a bogus `/BioVisionCenter/ngio.git/` base path when serving locally. It is now `https://biovisioncenter.github.io/ngio/`. -- `clean_docs_data` now also removes `data/tmp`. That is the shared staging directory `download_ome_zarr_dataset` extracts through, and an interrupted build leaves a half-extracted dataset there, after which every later build fails with `Expected one directory to be extracted, got 2`. -- Drop the `git-revision-date-localized` and `git-committers` plugins (Tier 2 on Zensical's compatibility backlog), losing the page revision-date footer and committer avatars for now. mkdocstrings backlinks are likewise not yet supported; cross-references and the inventory still work. -- Build the docs with `--strict` in CI before deploying: plain `zensical build` exits 0 and reports "No issues found" even when a `markdown-exec` code block raised, which would otherwise publish tracebacks. -- Move every executed docs code block out of the markdown and into real Python scripts under `docs/snippets/`, included via `pymdownx.snippets` (`--8<-- "path.py:section"`) and executed by `markdown-exec`. The snippet scripts are linted and formatted by ruff and each one runs standalone from the repo root (`pixi run -e docs test_snippets`). This prepares the docs for the migration to Zensical, which does not support `mkdocs-jupyter`'s `execute: true`. -- Convert the five tutorial notebooks (`docs/tutorials/*.ipynb`) to markdown pages backed by snippet scripts, and drop the `mkdocs-jupyter` plugin. Fixes carried over in the conversion: the notebooks computed `download_dir` relative to their own directory (which resolved outside the repo once executed from the repo root), and several `create_ome_zarr_from_array`/`add_table` calls lacked `overwrite=True`, so a second build failed. -- Convert all `pycon` console blocks to plain `python` blocks. The old `>>> expr` plus hidden `>>> print(expr)` pair collapses to a single visible `print(expr)`, so each expression is evaluated once instead of twice and no lines are hidden from the reader. -- Replace `mkdocs-include-markdown-plugin` in `docs/changelog.md` with a `pymdownx.snippets` include, and drop the plugin. -- Fix `3_tables.md` showing `masking_table.get_label(1)` while executing `get_label(100)`; the page now shows the call it runs. -- Remove the dangling `favicon: images/favicon.ico` from `mkdocs.yml` (`docs/images/` does not exist). -- Replace the stale `clean_nb_data` / `test_nb` pixi tasks, which pointed at a nonexistent `docs/notebooks/`, with `build_docs`, `clean_docs_data`, and per-script `snip_*` tasks aggregated by `test_snippets`. +- 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` (a plain build exits 0 even when a code block raised). +- Apply the commissioned design system: the mark-G logo set, a contrast-audited teal palette and full theme layer in `docs/stylesheets/ngio.css`, and six explanatory figures authored as inline SVG so they follow the light/dark toggle. Site chrome — header, tabs and footer — now sits on its own tone, framing the page rather than bleeding into it. +- Rewrite the copy to the design system's content conventions: sentence case, British spelling, second person, and openers that lead with what the reader does. Covers every page, the nav and `llms.txt`, and adds a landing page, a 14-term glossary, "Next steps" footers and API cross-references. +- 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. +- Fix defects found along the way: Markdown extensions Zensical had silently dropped (declaring `markdown_extensions` replaces its defaults rather than extending them), YAML front matter invalidated by unquoted colons on six pages, `site_url` pointing at the clone URL, tables rendering as literal `|---|`, and per-page snippet state now that each page gets a fresh `markdown-exec` session. - 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 From be4c68185292c416911920d1f12692c7110118ee Mon Sep 17 00:00:00 2001 From: lorenzo <lorenzo.cerrone@uzh.ch> Date: Thu, 30 Jul 2026 08:35:03 +0200 Subject: [PATCH 06/14] Restore the expansion of the ngio name in the docs ngio is an acronym: n(ext) g(eneration file format) IO, after the OME-NGFF specification the library reads and writes. The site title expands it again, while the descriptive "a Python library for OME-Zarr bioimage analysis" moves to the site description and the landing-page, README and llms.txt openers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- CHANGELOG.md | 1 + README.md | 14 ++++++++------ docs/index.md | 14 ++++++++------ docs/llms.txt | 5 +++-- mkdocs.yml | 10 +++++----- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b05cd19..62d53ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - Rewrite the copy to the design system's content conventions: sentence case, British spelling, second person, and openers that lead with what the reader does. Covers every page, the nav and `llms.txt`, and adds a landing page, a 14-term glossary, "Next steps" footers and API cross-references. - 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. - Fix defects found along the way: Markdown extensions Zensical had silently dropped (declaring `markdown_extensions` replaces its defaults rather than extending them), YAML front matter invalidated by unquoted colons on six pages, `site_url` pointing at the clone URL, tables rendering as literal `|---|`, and per-page snippet state now that each page gets a fresh `markdown-exec` session. +- Restore the expansion of the name across the docs: the site title is `ngio: next generation file format IO` again (n-g-IO, after the OME next generation file format the library reads and writes), with the descriptive "a Python library for OME-Zarr bioimage analysis" kept as the site description and in the landing-page, README and `llms.txt` openers. - 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/README.md b/README.md index 8b910495..69df3b5b 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,14 @@ [![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) -**A Python library for OME-Zarr bioimage analysis.** - -ngio gives you an object-based API for [OME-Zarr](https://ngff.openmicroscopy.org/) — the -cloud-optimised format for large, multi-dimensional microscopy data. Open an image, reach -for the resolution level you need, work with labels, tables and regions of interest, and -scale the same code from one field of view to a whole plate. +**Next generation file format IO — a Python library for OME-Zarr bioimage analysis.** + +The name says what the library does: IO for the OME next generation file format (NGFF), +which [OME-Zarr](https://ngff.openmicroscopy.org/) implements. ngio gives you an +object-based API for that format — the cloud-optimised one for large, multi-dimensional +microscopy data. Open an image, reach for the resolution level you need, work with labels, +tables and regions of interest, and scale the same code from one field of view to a whole +plate. ## Installation diff --git a/docs/index.md b/docs/index.md index 7579dc1c..4a359763 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,17 @@ --- -description: ngio is a Python library for OME-Zarr bioimage analysis, with an object-based API for images, labels, tables, ROIs and HCS plates. +description: ngio — next generation file format IO — is a Python library for OME-Zarr bioimage analysis, with an object-based API for images, labels, tables, ROIs and HCS plates. --- # ngio -**A Python library for OME-Zarr bioimage analysis.** +**Next generation file format IO — a Python library for OME-Zarr bioimage analysis.** -ngio gives you an object-based API for [OME-Zarr](https://ngff.openmicroscopy.org/) — the -cloud-optimised format for large, multi-dimensional microscopy data. Open an image, -reach for the resolution level you need, work with labels, tables and regions of -interest, and scale the same code from one field of view to a whole plate. +The name says what the library does: IO for the OME next generation file format (NGFF), +which [OME-Zarr](https://ngff.openmicroscopy.org/) implements. ngio gives you an +object-based API for that format — the cloud-optimised one for large, multi-dimensional +microscopy data. Open an image, reach for the resolution level you need, work with +labels, tables and regions of interest, and scale the same code from one field of view to +a whole plate. ## Installation diff --git a/docs/llms.txt b/docs/llms.txt index 28319174..d3e0fbc0 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,7 +1,8 @@ # ngio -> ngio 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 +> ngio (short for "next generation file format IO", after the OME-NGFF specification it +> reads and writes) 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. diff --git a/mkdocs.yml b/mkdocs.yml index 568fde3d..929bee88 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,12 +1,12 @@ -# Describes the library, not the format. The old value expanded NGFF ("next -# generation file format"), which names OME-Zarr rather than ngio, and title-cased -# a project that styles itself lowercase everywhere else. -site_name: "ngio: OME-Zarr bioimage analysis for Python" +# 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 processing OME-Zarr images" +site_description: "A Python library for OME-Zarr bioimage analysis" repo_name: "ngio" repo_url: "https://github.com/BioVisionCenter/ngio" copyright: "Copyright © 2024-2026, BioVisionCenter UZH" From 5a4b66968f1f661414ebe9a99f9f9e17c35b253a Mon Sep 17 00:00:00 2001 From: lorenzo <lorenzo.cerrone@uzh.ch> Date: Thu, 30 Jul 2026 08:49:22 +0200 Subject: [PATCH 07/14] Restore the declarative opener on the landing page and README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rewritten opener led with second-person imperatives ("Open an image, reach for the resolution level you need"), a register that belongs to task pages rather than to a project's front door — zarr, napari, anndata, spatialdata and scikit-image all open with "X is a Y that does Z". Restore that form, tightened to drop the "mission is to streamline" and "comprehensive support" framing. Also drop the sentence explaining where the name comes from (the title already carries the acronym) and the changelog entry for the title, which reverted to what main already had. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- CHANGELOG.md | 1 - README.md | 11 +++++------ docs/index.md | 13 ++++++------- docs/llms.txt | 11 +++++------ 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d53ac9..7b05cd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,6 @@ - Rewrite the copy to the design system's content conventions: sentence case, British spelling, second person, and openers that lead with what the reader does. Covers every page, the nav and `llms.txt`, and adds a landing page, a 14-term glossary, "Next steps" footers and API cross-references. - 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. - Fix defects found along the way: Markdown extensions Zensical had silently dropped (declaring `markdown_extensions` replaces its defaults rather than extending them), YAML front matter invalidated by unquoted colons on six pages, `site_url` pointing at the clone URL, tables rendering as literal `|---|`, and per-page snippet state now that each page gets a fresh `markdown-exec` session. -- Restore the expansion of the name across the docs: the site title is `ngio: next generation file format IO` again (n-g-IO, after the OME next generation file format the library reads and writes), with the descriptive "a Python library for OME-Zarr bioimage analysis" kept as the site description and in the landing-page, README and `llms.txt` openers. - 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/README.md b/README.md index 69df3b5b..bb2c51a1 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,11 @@ **Next generation file format IO — a Python library for OME-Zarr bioimage analysis.** -The name says what the library does: IO for the OME next generation file format (NGFF), -which [OME-Zarr](https://ngff.openmicroscopy.org/) implements. ngio gives you an -object-based API for that format — the cloud-optimised one for large, multi-dimensional -microscopy data. Open an image, reach for the resolution level you need, work with labels, -tables and regions of interest, and scale the same code from one field of view to a whole -plate. +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 diff --git a/docs/index.md b/docs/index.md index 4a359763..f0d000fd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,16 @@ --- -description: ngio — next generation file format IO — is a Python library for OME-Zarr bioimage analysis, with an object-based API for images, labels, tables, ROIs and HCS plates. +description: ngio is a Python library for OME-Zarr bioimage analysis, with an object-based API for images, labels, tables, ROIs and HCS plates. --- # ngio **Next generation file format IO — a Python library for OME-Zarr bioimage analysis.** -The name says what the library does: IO for the OME next generation file format (NGFF), -which [OME-Zarr](https://ngff.openmicroscopy.org/) implements. ngio gives you an -object-based API for that format — the cloud-optimised one for large, multi-dimensional -microscopy data. Open an image, reach for the resolution level you need, work with -labels, tables and regions of interest, and scale the same code from one field of view to -a whole plate. +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 diff --git a/docs/llms.txt b/docs/llms.txt index d3e0fbc0..94bc32f8 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,11 +1,10 @@ # ngio -> ngio (short for "next generation file format IO", after the OME-NGFF specification it -> reads and writes) 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. +> 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 From 6a7d1d7cf4d5f42038698c350edda694f4821499 Mon Sep 17 00:00:00 2001 From: lorenzo <lorenzo.cerrone@uzh.ch> Date: Thu, 30 Jul 2026 09:07:10 +0200 Subject: [PATCH 08/14] cleanup the claude.md --- CLAUDE.md | 44 ++++++-------------------------------------- docs/CLAUDE.md | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 38 deletions(-) create mode 100644 docs/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 96d5dbcf..8af24aaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,34 +23,10 @@ docs test_snippets # run every docs snippet script standalone docs clean_docs_data # drop generated ./data/*.zarr stores ``` -## Docs snippets -Executed code on the docs pages lives in real Python scripts under `docs/snippets/`, -not in the markdown. A page includes a named section via `pymdownx.snippets`: - -````markdown -```python exec="true" source="material-block" session="get_started" ---8<-- "docs/snippets/getting_started/get_started.py:levels" -``` -```` - -- Sections are delimited by `# --8<-- [start:name]` / `# --8<-- [end:name]`. -- Use `source="material-block"` (not `block`) and `html="1"` for figure blocks. -- One script per session; each must run standalone from the repo root. -- **Every page must bind its own state.** The site is built by Zensical, which gives each - page a fresh markdown-exec session — a page cannot use a variable bound on another page. - Pages 2 and 3 of the getting-started guide therefore include the silent - `reopen_container` / `reopen_image` sections at the top (hidden, no `source=`). -- **Printing a table? Call the `print_table` helper, with `html="1"` on the fence.** - `.to_markdown()` does not work: Zensical does not run block-level Markdown over - markdown-exec output, so a pipe table stays literal `|---|` text (static pipe tables in - `.md` sources are fine). `print_table` emits `to_html()` and strips the - `class="dataframe"` / `border` attributes pandas adds, because every theme table rule — - and the JS that adds the horizontal-scroll wrapper — is gated on `table:not([class])`. -- Always 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 it will serve - cached HTML from a previous broken build. -- Sections repeat their imports so each rendered block stands alone (hence the - `docs/snippets/**` ruff per-file-ignores). +## 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 @@ -65,7 +41,6 @@ not in the markdown. A page includes a named section via `pymdownx.snippets`: - 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 `_` @@ -74,12 +49,5 @@ not in the markdown. A page includes a named section via `pymdownx.snippets`: ## 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/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 100644 index 00000000..dc925d70 --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1,17 @@ +# Docs + +Executed code lives in scripts under `docs/snippets/`, included by `pymdownx.snippets` +(`--8<-- "docs/snippets/<path>.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. + +Three traps, none visible from the sources: + +- 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 `print_table` 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])`). From e508cadb996d46255ecc59ecc314ef72e4681e59 Mon Sep 17 00:00:00 2001 From: lorenzo <lorenzo.cerrone@uzh.ch> Date: Thu, 30 Jul 2026 10:37:52 +0200 Subject: [PATCH 09/14] cleanup docs tests --- CHANGELOG.md | 5 ++ CONTRIBUTING.md | 40 +++++---------- README.md | 13 ++--- docs/CLAUDE.md | 8 ++- docs/api/hcs.md | 12 +++-- docs/api/images.md | 6 +-- docs/api/iterators.md | 7 ++- docs/api/ngio/common.md | 2 +- docs/api/ngio/hcs.md | 2 +- docs/api/ngio/images.md | 2 +- docs/api/ngio/io_pipes.md | 2 +- docs/api/ngio/iterators.md | 2 +- docs/api/ngio/ngio.md | 2 +- docs/api/ngio/tables.md | 2 +- docs/api/ngio/transforms.md | 2 +- docs/api/ngio/utils.md | 2 +- docs/api/ome_zarr_container.md | 8 +-- docs/api/tables.md | 2 +- docs/getting_started/0_quickstart.md | 4 +- docs/getting_started/1_ome_zarr_containers.md | 28 +++++----- docs/getting_started/2_images.md | 39 +++++++------- docs/getting_started/3_tables.md | 6 +-- docs/getting_started/4_masked_images.md | 14 ++--- docs/getting_started/5_hcs.md | 10 ++-- docs/getting_started/6_iterators.md | 41 ++++++++++++++- docs/getting_started/7_configuration.md | 14 ++--- docs/index.md | 32 +++++------- docs/llms.txt | 10 ++-- docs/snippets/getting_started/get_started.py | 4 +- docs/snippets/getting_started/iterators.py | 41 +++++++++++++++ .../snippets/getting_started/masked_images.py | 6 +-- docs/snippets/tutorials/feature_extraction.py | 12 ++--- docs/snippets/tutorials/image_processing.py | 51 ++++++++----------- docs/snippets/tutorials/image_segmentation.py | 20 ++++---- docs/table_specs/backend.md | 47 ++++++++++++----- docs/table_specs/overview.md | 16 ++++-- .../table_types/condition_table.md | 11 ++-- docs/table_specs/table_types/custom_table.md | 16 ++++-- docs/table_specs/table_types/feature_table.md | 29 ++++++----- docs/table_specs/table_types/generic_table.md | 12 +++-- .../table_types/masking_roi_table.md | 8 +-- docs/table_specs/table_types/roi_table.md | 10 ++-- docs/tutorials/create_ome_zarr.md | 4 +- docs/tutorials/feature_extraction.md | 11 ++-- docs/tutorials/hcs_exploration.md | 8 +-- mkdocs.yml | 2 +- pyproject.toml | 2 + 47 files changed, 383 insertions(+), 244 deletions(-) create mode 100644 docs/snippets/getting_started/iterators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b05cd19..83edb66c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,11 @@ - Rewrite the copy to the design system's content conventions: sentence case, British spelling, second person, and openers that lead with what the reader does. Covers every page, the nav and `llms.txt`, and adds a landing page, a 14-term glossary, "Next steps" footers and API cross-references. - 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. - Fix defects found along the way: Markdown extensions Zensical had silently dropped (declaring `markdown_extensions` replaces its defaults rather than extending them), YAML front matter invalidated by unquoted colons on six pages, `site_url` pointing at the clone URL, tables rendering as literal `|---|`, and per-page snippet state now that each page gets a fresh `markdown-exec` session. +- Proofreading pass over the refactored docs. Corrections: the `strict` parameter of `get_image`/`get_label` defaults to `False` (nearest level), not `True`, and the page said the opposite; `set_array` takes a numpy or dask array, not a dask delayed object; `RoiTable.get` returns one ROI, not all of them; `get_well_images` returns a dict, and omitting `acquisition` returns every image in the well rather than an empty one; the object model figure labelled `ImagesContainer` as a non-existent `MultiscaleImage`; `by_zyx` splits the time axis rather than iterating z-planes; retry records go to a `ngio:<module>` logger, which is not a child of `ngio`; and the "every code block is executed" claim in `index.md`, `README.md` and `llms.txt` is now scoped to the worked examples, 15 blocks being static. +- Correct the table specifications against what ngio actually writes: document how the `backend` attribute is named (the `{backend_name}_v{version}` convention, and the `experimental_{json,csv,parquet}_v1` legacy aliases kept for older tables), the AnnData backend casts to `float64` only for heterogeneous dtypes and stores booleans in `X` rather than `obs`, `instance_key` is always written for feature and masking ROI tables, `index_key`/`index_type` are omitted entirely for generic and condition tables, extra ROI columns do round-trip (via `Roi` extras) and the plate-location and index columns were missing from the optional list, only four of the five table types are auto-detected on read (`GenericRoiTable` is reachable only via `get_generic_roi_table`), condition tables were missing from the getting-started list, and the feature-type column lists are declarative only — ngio writes but never reads them. Also fixes the malformed JSON-backend directory tree, notes the Zarr v3 `zarr.json` layout, corrects the plate tree's column and image labels, and makes every spec block parse (trailing commas removed, fences retagged `json5` so the `//` comments highlight instead of reading as syntax errors). +- Flag `ngio.experimental.iterators` as experimental on the iterators guide and API page, retitle the `ngio.iterators` reference page and nav entry to the real module path, and add an executed example to the iterators page, which previously had no runnable code. +- Consistency pass: `>>>` prompts removed so every example is copy-pasteable, `OME-Zarr Container` no longer code-formatted as if it were a class, `get_array` presented as the mode-dispatch form rather than a back-compat shim, uniform "API reference" titles, table-spec page titles matched to their nav labels, numbered steps across all five tutorials, and tutorial snippet comments moved to second person and British spelling (they render into the page). +- `CONTRIBUTING.md`: `lint` and the `bump-*` tasks need `-e dev`; PRs run a reduced CI matrix, not the full one; sentence-case headings. - 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/CONTRIBUTING.md b/CONTRIBUTING.md index f8604923..aa3f2a92 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,6 @@ 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 @@ -17,8 +15,6 @@ cd ngio pixi install ``` ---- - ## Development Work in the `dev` environment, which includes linters, type checker, and test dependencies: @@ -29,30 +25,24 @@ pixi shell -e dev # activate shell pixi run -e dev <cmd> ``` ---- - -## Running Tests +## 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) +pixi run -e test13 pytest # a specific Python version: test11, test12, test13, test14 ``` -Coverage is reported automatically. The full CI matrix covers `test11`, `test12`, `test13`, `test14` across Linux, macOS, and Windows. - ---- +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 & Formatting +## Linting and formatting ```bash -pixi run lint # run all hooks on all files +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 +## 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. @@ -64,28 +54,24 @@ fix: correct axis order in NgffImage docs: update contributing guide ``` ---- - -## Opening 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. +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 will run the full test matrix and linters automatically. - ---- +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 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) +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 bb2c51a1..9cfa3406 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,8 @@ Then work through the 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. +- **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. ## Versioning @@ -54,9 +51,9 @@ is stable, and breaking changes are reserved for major releases. ## Documentation Full documentation, including guides, tutorials and the API reference, is at -[biovisioncenter.github.io/ngio](https://biovisioncenter.github.io/ngio/). Every code -block in the docs is executed when the site is built, so what you read is what actually -runs. +[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. ## Citing ngio diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index dc925d70..3f207796 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -5,7 +5,7 @@ Executed code lives in scripts under `docs/snippets/`, included by `pymdownx.sni `source="material-block"`, and `html="1"` for figures. One script per session, each runnable standalone from the repo root. -Three traps, none visible from the sources: +Four traps, none visible from the sources: - 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. @@ -15,3 +15,9 @@ Three traps, none visible from the sources: 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/api/hcs.md b/docs/api/hcs.md index 17ec1425..7a46911f 100644 --- a/docs/api/hcs.md +++ b/docs/api/hcs.md @@ -2,13 +2,13 @@ description: API reference for OmeZarrPlate and OmeZarrWell. --- -# HCS API documentation +# HCS API reference ## Open a plate ::: ngio.open_ome_zarr_plate -## ngio.OmeZarrPlate class reference +## OmeZarrPlate ::: ngio.OmeZarrPlate @@ -16,6 +16,12 @@ description: API reference for OmeZarrPlate and OmeZarrWell. ::: 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 5a45bd27..bd3cc97f 100644 --- a/docs/api/images.md +++ b/docs/api/images.md @@ -2,13 +2,13 @@ description: API reference for the Image and Label objects. --- -# Images API documentation +# Images API reference ## Open an image ::: ngio.open_image -## ngio.Image class reference +## Image ::: ngio.Image @@ -16,6 +16,6 @@ description: API reference for the Image and Label objects. ::: ngio.open_label -## ngio.Label class reference +## Label ::: ngio.Label diff --git a/docs/api/iterators.md b/docs/api/iterators.md index 87c5053c..8348c91c 100644 --- a/docs/api/iterators.md +++ b/docs/api/iterators.md @@ -2,7 +2,12 @@ description: API reference for the ngio processing iterators. --- -# Iterators API documentation +# 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 2d43eb32..11dff8f3 100644 --- a/docs/api/ngio/utils.md +++ b/docs/api/ngio/utils.md @@ -1,3 +1,3 @@ -# ngio.utils API documentation +# ngio.utils API reference ::: ngio.utils diff --git a/docs/api/ome_zarr_container.md b/docs/api/ome_zarr_container.md index 14077149..3840495e 100644 --- a/docs/api/ome_zarr_container.md +++ b/docs/api/ome_zarr_container.md @@ -2,17 +2,17 @@ description: API reference for opening and creating OME-Zarr containers. --- -# OmeZarrContainer: API documentation +# OME-Zarr container API reference -## Open an OME-Zarr container +## 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 eb513aa4..7309deea 100644 --- a/docs/api/tables.md +++ b/docs/api/tables.md @@ -2,7 +2,7 @@ description: API reference for ngio tables — ROI, masking ROI, feature, condition and generic tables, plus the tables container and backends. --- -# Tables API documentation +# Tables API reference For the on-disk format of each table type, see the [Table Specifications](../table_specs/overview.md). diff --git a/docs/getting_started/0_quickstart.md b/docs/getting_started/0_quickstart.md index a340a5d6..c04f34df 100644 --- a/docs/getting_started/0_quickstart.md +++ b/docs/getting_started/0_quickstart.md @@ -72,11 +72,11 @@ Open an OME-Zarr file and inspect its contents. ### 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. ### What is the OME-Zarr container not? -The `OME-Zarr Container` object does not give you access to the image data directly. For that, 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 diff --git a/docs/getting_started/1_ome_zarr_containers.md b/docs/getting_started/1_ome_zarr_containers.md index 40b7fa79..6ace586e 100644 --- a/docs/getting_started/1_ome_zarr_containers.md +++ b/docs/getting_started/1_ome_zarr_containers.md @@ -6,14 +6,14 @@ description: "The OME-Zarr container object: inspect and modify metadata, derive **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 +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. <!-- Figure 01 — the object model --> <div class="ngio-diagram"> <svg viewBox="0 0 640 412" style="display:block;width:100%;height:auto" role="img" aria-labelledby="f1t f1d"> <title id="f1t">The objects inside an OME-Zarr container - An OmeZarrContainer branches into a multiscale image made of one Image per resolution level, a labels container holding named multiscale labels, and a tables container holding typed tables. + 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. @@ -42,7 +42,7 @@ high-level access to the metadata, images, labels and tables in a store. - MultiscaleImage + ImagesContainer several resolutions @@ -112,13 +112,13 @@ high-level access to the metadata, images, labels and tables in a store. --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` gives you: +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. @@ -128,7 +128,7 @@ The `OME-Zarr Container` gives you: ### What is the OME-Zarr container not? -The `OME-Zarr Container` object does not give you access to the image data directly. For that, 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 @@ -176,25 +176,25 @@ You can update channel labels, colours, and display windows: === "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 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" 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" 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 @@ -204,13 +204,13 @@ You can update the axes names and units: === "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" 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 @@ -218,7 +218,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 @@ -226,7 +226,7 @@ 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. diff --git a/docs/getting_started/2_images.md b/docs/getting_started/2_images.md index f8f94a18..a99dc723 100644 --- a/docs/getting_started/2_images.md +++ b/docs/getting_started/2_images.md @@ -124,13 +124,13 @@ ngio provides a high-level API to access the image data at different resolution ``` === "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: + 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" ```python exec="true" source="material-block" session="get_started" @@ -148,7 +148,7 @@ Similarly to the `OME-Zarr Container`, the `Image` object provides a high-level ```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 @@ -164,39 +164,39 @@ Once you have the `Image` object, you can access the image data as a: --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`. ```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: ```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" --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 @@ -208,8 +208,8 @@ To read or write a specific region of the image defined in world coordinates, yo ## 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 @@ -219,7 +219,7 @@ See which labels are available in the image: --8<-- "docs/snippets/getting_started/get_started.py:list_labels" ``` -There are `4` labels available in this image. Here is how to access them: +Here is how to reach one of them: === "Highest resolution label" By default, the `get_label` method returns the highest resolution label: @@ -241,15 +241,14 @@ There are `4` labels available in this image. Here is how to access them: ``` === "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: + 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. ### 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 diff --git a/docs/getting_started/3_tables.md b/docs/getting_started/3_tables.md index 51b60579..e12eeeaf 100644 --- a/docs/getting_started/3_tables.md +++ b/docs/getting_started/3_tables.md @@ -26,7 +26,7 @@ List all available tables and load a specific one: --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 tables can be used to store arbitrary regions of interest (ROIs) in the image. @@ -40,8 +40,8 @@ ngio supports three types of tables: `roi_table`, `feature_table`, and `masking_ ```python exec="true" html="1" session="get_started" --8<-- "docs/snippets/getting_started/get_started.py:plot_fov_roi_on_image" ``` - This will return all the ROIs in the table. - ROIs can be used to slice the image data: + `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" ``` diff --git a/docs/getting_started/4_masked_images.md b/docs/getting_started/4_masked_images.md index 9db38e3f..0a49dd2f 100644 --- a/docs/getting_started/4_masked_images.md +++ b/docs/getting_started/4_masked_images.md @@ -75,7 +75,7 @@ the pixels belonging to that object. --8<-- "docs/snippets/getting_started/masked_images.py:setup" ``` -Like the `Image` and `Label` objects, a `MaskedImage` is initialised 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. Create a masked image from the `nuclei` label: @@ -112,10 +112,12 @@ For example, zoom out the ROI by a factor of `2`: ## 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, use the `get_roi_masked` and `set_roi_masked` methods. -For example, use `get_roi_masked` to get the masked data for a specific label: +For example, read the masked data for one label: ```python exec="true" source="material-block" session="masked_images" --8<-- "docs/snippets/getting_started/masked_images.py:get_roi_masked" @@ -125,7 +127,7 @@ For example, use `get_roi_masked` to get the masked data for a specific label: --8<-- "docs/snippets/getting_started/masked_images.py:plot_get_roi_masked" ``` -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: ```python exec="true" source="material-block" session="masked_images" --8<-- "docs/snippets/getting_started/masked_images.py:set_roi_masked" @@ -139,7 +141,7 @@ Use the `set_roi_masked` method to set the masked data for a specific label: The `MaskedLabel` class is a subclass of [`Label`][ngio.Label] and provides the same functionality as the `MaskedImage` class. -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. ```python exec="true" source="material-block" session="masked_images" --8<-- "docs/snippets/getting_started/masked_images.py:get_masked_label" diff --git a/docs/getting_started/5_hcs.md b/docs/getting_started/5_hcs.md index 94a7a617..0267ab31 100644 --- a/docs/getting_started/5_hcs.md +++ b/docs/getting_started/5_hcs.md @@ -100,7 +100,7 @@ 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: @@ -142,7 +142,7 @@ The `OmeZarrPlate` object provides multiple methods to retrieve the path to the ## 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`][ngio.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" Get all images in the plate: @@ -170,7 +170,7 @@ The `OmeZarrPlate` object provides a method to get the image objects in a well. ```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 @@ -198,7 +198,7 @@ This has created a new empty plate with the metadata correctly set. But no image 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. + 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" ``` @@ -209,7 +209,7 @@ You can add or remove images. 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. + 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" ``` diff --git a/docs/getting_started/6_iterators.md b/docs/getting_started/6_iterators.md index bf2e6a34..2a581f41 100644 --- a/docs/getting_started/6_iterators.md +++ b/docs/getting_started/6_iterators.md @@ -8,6 +8,13 @@ description: The four ngio iterators for building scalable image-processing pipe 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. +
@@ -93,7 +100,8 @@ When building image processing pipelines it is often useful to iterate over spec
-ngio provides four basic `Iterator` classes: +ngio provides four basic `Iterator` classes, all imported from +`ngio.experimental.iterators`:
@@ -173,6 +181,37 @@ ngio provides four basic `Iterator` classes: * 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" +``` + +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. + More complete examples can be found in the [Fractal tasks template](https://github.com/fractal-analytics-platform/fractal-tasks-template). ## Next steps diff --git a/docs/getting_started/7_configuration.md b/docs/getting_started/7_configuration.md index 44b2df31..adb24442 100644 --- a/docs/getting_started/7_configuration.md +++ b/docs/getting_started/7_configuration.md @@ -2,13 +2,15 @@ description: Configure ngio via ngio_config.json, including the io_retry policy. --- -# Configuration +# 7. Configuration -ngio has a small global configuration object that controls cross-cutting IO behaviour. It is loaded once at import time from a JSON file and is accessible programmatically. +**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 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 { @@ -23,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 @@ -48,7 +50,7 @@ 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. @@ -58,7 +60,7 @@ ngio's own errors (`NgioError` subclasses, e.g. validation errors) are never ret - **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`) diff --git a/docs/index.md b/docs/index.md index f0d000fd..67eaa4d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,20 @@ scalable way. It provides an object-based API for opening, exploring and manipul 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. +## Key features + +- **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. + ## Installation === "pip" @@ -44,9 +58,6 @@ roi = ome_zarr.get_table("FOV_ROI_table").get("FOV_1") patch = image.get_roi_as_numpy(roi) ``` -Every code block in these docs is executed when the site is built, so what you read is -what actually runs. - ## Where to go next
@@ -89,21 +100,6 @@ what actually runs.
-## Key features - -- **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. - -## 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. - ## Citing ngio If ngio contributes to work you publish, please cite it. See diff --git a/docs/llms.txt b/docs/llms.txt index 94bc32f8..cdc4e207 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -9,8 +9,8 @@ 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`. Every code example in these -docs is executed when the site is built, so the outputs shown are real. +(`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 @@ -40,17 +40,19 @@ docs is executed when the site is built, so the outputs shown are real. - [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/): OmeZarrPlate, OmeZarrWell and the plate helpers. +- [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. +- [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/getting_started/get_started.py b/docs/snippets/getting_started/get_started.py index 3f9df443..2082b113 100644 --- a/docs/snippets/getting_started/get_started.py +++ b/docs/snippets/getting_started/get_started.py @@ -207,7 +207,7 @@ def print_table(df: pd.DataFrame) -> None: # --8<-- [end:image_pixel_size] # --8<-- [start:image_array_info] -print(image.shape, image.dtype, image.chunks) +print(image.shape, image.dtype, image.chunks, image.axes) # --8<-- [end:image_array_info] # --8<-- [start:image_as_numpy] @@ -221,7 +221,7 @@ def print_table(df: pd.DataFrame) -> None: # --8<-- [end:image_as_dask] # --8<-- [start:image_get_array_legacy] -# Get the image as a numpy or dask or delayed object +# 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] 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 index d75cd385..3b43edac 100644 --- a/docs/snippets/getting_started/masked_images.py +++ b/docs/snippets/getting_started/masked_images.py @@ -115,12 +115,12 @@ def print_figure(fig: Figure) -> None: # --8<-- [end:plot_masked_roi_zoom] # --8<-- [start:get_roi_masked] -masked_roi_data = masked_image.get_roi_masked(label=1009, c=0, zoom_factor=2) +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] -masked_roi_data = masked_image.get_roi_masked(label=1009, c=0, zoom_factor=2) +masked_roi_data = masked_image.get_roi_masked_as_numpy(label=1009, c=0, zoom_factor=2) masked_roi_data = np.squeeze(masked_roi_data) fig, ax = plt.subplots(figsize=(8, 4)) @@ -134,7 +134,7 @@ def print_figure(fig: Figure) -> None: # --8<-- [start:set_roi_masked] import numpy as np -masked_data = masked_image.get_roi_masked(label=1009, c=0) +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] diff --git a/docs/snippets/tutorials/feature_extraction.py b/docs/snippets/tutorials/feature_extraction.py index 438a2d8a..43a71c70 100644 --- a/docs/snippets/tutorials/feature_extraction.py +++ b/docs/snippets/tutorials/feature_extraction.py @@ -67,28 +67,28 @@ def extract_features(image: np.ndarray, label: np.ndarray) -> pd.DataFrame: hcs_path = download_ome_zarr_dataset("CardiomyocyteTinyMip", download_dir=download_dir) image_path = hcs_path / "B" / "03" / "0" -# Open the ome-zarr container +# 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 -# First we will need the image object and the FOVs table +# Take the image to measure image = ome_zarr.get_image() # Get the nuclei label nuclei = ome_zarr.get_label("nuclei") -# In this example we the image is available at an higher resolution than the 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}") -# We need to setup a transform to resample the nuclei to the image resolution +# So resample the label up to the image resolution with a transform zoom_transform = ZoomTransform( input_image=nuclei, target_image=image, - order="nearest", # Nearest neighbor interpolation for labels + order="nearest", # Nearest-neighbour interpolation, so label ids stay intact ) # --8<-- [end:setup_transform] @@ -109,7 +109,7 @@ def extract_features(image: np.ndarray, label: np.ndarray) -> pd.DataFrame: roi_feat_table = extract_features(image=image_data, label=label_data) feat_table.append(roi_feat_table) -# Concatenate all the dataframes into a single one +# 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) diff --git a/docs/snippets/tutorials/image_processing.py b/docs/snippets/tutorials/image_processing.py index 19f56274..1c691606 100644 --- a/docs/snippets/tutorials/image_processing.py +++ b/docs/snippets/tutorials/image_processing.py @@ -88,16 +88,15 @@ def gaussian_blur(image: np.ndarray, sigma: float) -> np.ndarray: hcs_path = download_ome_zarr_dataset("CardiomyocyteTiny", download_dir=download_dir) image_path = hcs_path / "B" / "03" / "0" -# Open the ome-zarr container +# Open the OME-Zarr container ome_zarr = open_ome_zarr_container(image_path) # --8<-- [end:open_container] # --8<-- [start:derive_image] -# First we will need the image object +# Take the image to read from image = ome_zarr.get_image() -# Second we need to derive a new ome-zarr image where we will store -# the processed 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( @@ -107,22 +106,19 @@ def gaussian_blur(image: np.ndarray, sigma: float) -> np.ndarray: # --8<-- [end:derive_image] # --8<-- [start:apply_blur] -# We can use the axes order to specify how we query the image data. -# Here we will reorder the axes to be ["c", "z", "y", "x"]. -# So that it will be compatible with the gaussian blur function -# which expects the channel axis to be the first one. +# `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) -# Set the processed image data back to the ome-zarr image +# Write the processed data back to the OME-Zarr image blurred_image.set_array(patch=blurred_image_data, axes_order=["c", "z", "y", "x"]) -# The `set_array` method only saved the blurred image to the container at a specific -# resolution level. So all other resolution levels are still empty. -# To propagate the changes to all resolution levels, -# we can use the `consolidate` method. +# `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] @@ -144,9 +140,8 @@ def gaussian_blur(image: np.ndarray, sigma: float) -> np.ndarray: def dask_gaussian_blur(image: da.Array, sigma: float) -> da.Array: """Apply gaussian blur to a dask array.""" - # This will introduce some edge artifacts at chunk boundaries - # In a real application, consider using map_overlap to mitigate this - # With appropriate depth based on sigma + # 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) @@ -164,30 +159,26 @@ def dask_gaussian_blur(image: da.Array, sigma: float) -> da.Array: axes_order=["c", "z", "y", "x"], ) -# After initializing the iterator, the iterator will have created -# will iterate over the entire image. -print(f"Iterator after initialization: {iterator}") +# A freshly built iterator covers the entire image in one region. +print(f"Iterator over the whole image: {iterator}") -# Iterate over an arbitrary region of interest table -# We can use the product method that performs a cartesian product -# between the iterator and the table. +# 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}") -# We can explicitly set a broadcasting behavior -# For example we can iterate over all zyx planes, and broadcast all the other -# spatial dimensions +# 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() -# Finally (if needed) we can check if the regions are not-overlapping +# Optionally assert the regions do not overlap each other... iterator.require_no_regions_overlap() -# We can also check if the regions lay on non-overlapping chunks +# ...nor share chunks, which is what makes parallel writes safe. iterator.require_no_chunks_overlap() -# Now we can map the gaussian blur function to the iterator +# Map the blur across every region iterator.map_as_numpy(lambda x: gaussian_blur(x, sigma=sigma)) -# No need to consolidate, the iterator takes care of that -# after all the regions have been processed +# 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 index 6d28819f..bc464e4d 100644 --- a/docs/snippets/tutorials/image_segmentation.py +++ b/docs/snippets/tutorials/image_segmentation.py @@ -97,19 +97,18 @@ def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray hcs_path = download_ome_zarr_dataset("CardiomyocyteTiny", download_dir=download_dir) image_path = hcs_path / "B" / "03" / "0" -# Open the ome-zarr container +# 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 -# First we will need the image object and the FOVs table +# 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") -# Second we need to derive a new label image to use as target for the segmentation - +# Derive an empty label image to write the segmentation into label = ome_zarr.derive_label("new_label", overwrite=True) # Setup the segmentation iterator @@ -121,10 +120,10 @@ def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray ) seg_iterator = seg_iterator.product(roi_table) -# Make sure that if other axes are present they are iterated over +# Split any remaining time axis, so each step yields one whole ZYX volume seg_iterator = seg_iterator.by_zyx() -max_label = 0 # We will use this to avoid label collisions +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 @@ -178,11 +177,10 @@ def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray # --8<-- [start:masked_segment] from ngio.experimental.iterators import MaskedSegmentationIterator -# First we will need the masked image object -# (that contains the masking table information inside) +# Take a masked image, which carries its masking ROI table with it image = ome_zarr.get_masked_image(masking_label_name="mask") -# Second we need to derive a new label image to use as target for the segmentation +# 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 @@ -193,10 +191,10 @@ def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray axes_order=["z", "y", "x"], ) -# Make sure that if other axes are present they are iterated over +# Split any remaining time axis, so each step yields one whole ZYX volume seg_iterator = seg_iterator.by_zyx() -max_label = 0 # We will use this to avoid label collisions +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 diff --git a/docs/table_specs/backend.md b/docs/table_specs/backend.md index b0820a26..2a3a360b 100644 --- a/docs/table_specs/backend.md +++ b/docs/table_specs/backend.md @@ -4,40 +4,53 @@ description: "The on-disk table backends: AnnData, Parquet, CSV and JSON." # Table backends -ngio has four table backends. Each one is a Python class that can serialise tabular data into OME-Zarr containers. +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`. 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" + + 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. + + `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 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. The following normalisation steps are applied to each table before saving it to the AnnData backend: -- The table is separated in two parts: the floating point columns are cast to `float32` and stored as `X` in the AnnData object, while the categorical, boolean, and integer columns are stored as `obs`. +- 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": "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" } ``` @@ -48,12 +61,12 @@ Another advantage of the Parquet backend is that it can be read lazily: the data Parquet backend metadata: -```json +```json5 { // Backend metadata "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" } ``` @@ -72,12 +85,12 @@ The CSV backend is a plain text format that is widely used for tabular data. It 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. "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" } ``` @@ -96,7 +109,7 @@ The JSON backend serialises the table data into the Zarr group attributes as a J JSON backend metadata: -```json +```json5 { // Backend metadata "backend": "json", // the backend used to store the table, e.g. "anndata", "parquet", etc.. @@ -109,9 +122,17 @@ The table is stored in a subgroup of the Zarr group, and the metadata is stored ```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 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 6ba90a4b..05d65f22 100644 --- a/docs/table_specs/overview.md +++ b/docs/table_specs/overview.md @@ -41,6 +41,14 @@ These specifications define structured tables that standardise common table type - **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). +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. + +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`. + ## Table groups 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: @@ -71,10 +79,10 @@ image.zarr # Zarr group for a OME-Zarr image 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 diff --git a/docs/table_specs/table_types/condition_table.md b/docs/table_specs/table_types/condition_table.md index 72422df8..e139ac7b 100644 --- a/docs/table_specs/table_types/condition_table.md +++ b/docs/table_specs/table_types/condition_table.md @@ -19,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. "anndata", "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" + "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 adbdf583..5283e129 100644 --- a/docs/table_specs/table_types/custom_table.md +++ b/docs/table_specs/table_types/custom_table.md @@ -7,12 +7,18 @@ description: How custom table types fit into the ngio table architecture, and wh A custom table is a table type you define yourself, for data that does not fit any of the predefined types. -!!! note "Extension API not yet documented" +!!! note "No public extension API yet" - The mechanism for registering a custom table type is public but not yet documented in - full. Until it is, [generic tables](generic_table.md) are the supported way to store - arbitrary tabular data — they accept any pandas `DataFrame` or `AnnData` object and - round-trip through every [backend](../backend.md). + 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 diff --git a/docs/table_specs/table_types/feature_table.md b/docs/table_specs/table_types/feature_table.md index 5b805fce..06de4cb7 100644 --- a/docs/table_specs/table_types/feature_table.md +++ b/docs/table_specs/table_types/feature_table.md @@ -2,21 +2,25 @@ description: "Feature table: per-object measurements tied to a label image." --- -# Feature tables +# Feature table 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 tables can optionally include metadata to specify the type of features stored in each column: +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-form columns that can be used to store any other information about the object, but that should not be used for analysis/classification purposes. -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, ngio applies the following casting rules: +The declaration is there so that downstream tools can select subsets of features without +guessing from dtypes. -- 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. +!!! warning "Declarative only" + + 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 @@ -24,7 +28,7 @@ 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", @@ -34,16 +38,17 @@ A feature table must include the following metadata fields in the group attribut "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", @@ -52,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 0f5c5d88..27949cd1 100644 --- a/docs/table_specs/table_types/generic_table.md +++ b/docs/table_specs/table_types/generic_table.md @@ -2,7 +2,7 @@ description: "Generic table: the untyped table for arbitrary tabular data." --- -# Generic tables +# Generic table 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. @@ -14,14 +14,18 @@ Generic tables are also the safe fallback when you read a table that does not ma 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": "anndata", // the backend used to store the table, e.g. "anndata", "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" + "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 dee77435..f49c9747 100644 --- a/docs/table_specs/table_types/masking_roi_table.md +++ b/docs/table_specs/table_types/masking_roi_table.md @@ -2,7 +2,7 @@ description: "Masking ROI table: bounding boxes tied to the labels of a label image." --- -# Masking ROI tables +# 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. @@ -18,7 +18,7 @@ Masking ROI tables serve several purposes, such as: A masking ROI table must include the following metadata fields in the group attributes: -```json +```json5 { // ROI table metadata "type": "masking_roi_table", @@ -28,6 +28,7 @@ A masking ROI table must include the following metadata fields in the group attr "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 } ``` @@ -38,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 2ffa7dba..f42a8bbe 100644 --- a/docs/table_specs/table_types/roi_table.md +++ b/docs/table_specs/table_types/roi_table.md @@ -18,7 +18,7 @@ ROI tables serve 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", @@ -26,7 +26,7 @@ A ROI table must include the following metadata fields in the group attributes: // Backend metadata "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" } ``` @@ -40,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. -You can add further columns to the ROI table, but they are not 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.md b/docs/tutorials/create_ome_zarr.md index b3c4679c..00b086d2 100644 --- a/docs/tutorials/create_ome_zarr.md +++ b/docs/tutorials/create_ome_zarr.md @@ -10,6 +10,8 @@ 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" @@ -24,7 +26,7 @@ Start by converting a sample image from `skimage` to OME-Zarr format. --8<-- "docs/snippets/tutorials/create_ome_zarr.py:create" ``` -## Adding a ROI table to an OME-Zarr image +## 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. diff --git a/docs/tutorials/feature_extraction.md b/docs/tutorials/feature_extraction.md index 4709649d..59d8f6f7 100644 --- a/docs/tutorials/feature_extraction.md +++ b/docs/tutorials/feature_extraction.md @@ -8,23 +8,28 @@ Measure regionprops features from a segmented image with `ngio` and `skimage`, a 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: open the OME-Zarr container +## 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 2: set up the inputs +## 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 3: use the FeatureExtractorIterator to create a feature table +## 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" diff --git a/docs/tutorials/hcs_exploration.md b/docs/tutorials/hcs_exploration.md index 49a7bdcc..3493f0f2 100644 --- a/docs/tutorials/hcs_exploration.md +++ b/docs/tutorials/hcs_exploration.md @@ -8,13 +8,13 @@ Open an OME-Zarr plate with `ngio`, see what it contains, aggregate a table acro image in it, and write the result back to the plate. The last section creates a new empty plate from scratch. -## Show what's in the plate +## 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" ``` -## Aggregate tables across all images +## Step 2: aggregate tables across all images ```python exec="true" session="hcs_exploration" --8<-- "docs/snippets/tutorials/hcs_exploration.py:table_helpers" @@ -24,13 +24,13 @@ plate from scratch. --8<-- "docs/snippets/tutorials/hcs_exploration.py:concatenate_tables" ``` -## Save the table in the HCS plate +## 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" ``` -## Create a new empty plate +## 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" diff --git a/mkdocs.yml b/mkdocs.yml index 929bee88..0c3ec10a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -217,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 diff --git a/pyproject.toml b/pyproject.toml index e735c5dd..a87e8397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -283,6 +283,7 @@ 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" @@ -296,6 +297,7 @@ 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 && From f6b9c334788740770a73365de2807fd8c5356082 Mon Sep 17 00:00:00 2001 From: lorenzo Date: Thu, 30 Jul 2026 11:15:51 +0200 Subject: [PATCH 10/14] minor refinements --- README.md | 12 +++++----- docs/getting_started/0_quickstart.md | 26 ++++++++++++++++++++-- docs/getting_started/3_tables.md | 4 +++- docs/index.md | 33 +++++++++++++++++++++++++++- docs/table_specs/overview.md | 2 +- docs/tutorials/image_segmentation.md | 7 ++++++ 6 files changed, 73 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9cfa3406..2d2a603e 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ regions of interest (ROIs) for extracting and analysing specific regions of your ## Installation -```bash -pip install ngio -``` - -or +To install ngio, use whichever package manager you already work with — it is published on +both PyPI and conda-forge. ```bash -mamba install -c conda-forge 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 ``` Then work through the diff --git a/docs/getting_started/0_quickstart.md b/docs/getting_started/0_quickstart.md index c04f34df..30bb1278 100644 --- a/docs/getting_started/0_quickstart.md +++ b/docs/getting_started/0_quickstart.md @@ -11,7 +11,8 @@ 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` @@ -23,6 +24,27 @@ the images, labels and tables it contains. 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: @@ -72,7 +94,7 @@ Open an OME-Zarr file and inspect its contents. ### 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? diff --git a/docs/getting_started/3_tables.md b/docs/getting_started/3_tables.md index e12eeeaf..ce323dd0 100644 --- a/docs/getting_started/3_tables.md +++ b/docs/getting_started/3_tables.md @@ -8,7 +8,9 @@ description: "Load and create ngio tables: ROI, masking ROI, feature and generic 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 [Fractal's table spec](https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/). +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 diff --git a/docs/index.md b/docs/index.md index 67eaa4d6..0f761089 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,12 +28,37 @@ regions of interest (ROIs) for extracting and analysing specific regions of your ## Installation +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). + === "pip" ```bash 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" ```bash @@ -42,6 +67,11 @@ regions of interest (ROIs) for extracting and analysing specific regions of your ## ngio in 30 seconds +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. + ```python from ngio import open_ome_zarr_container @@ -76,7 +106,8 @@ patch = image.get_roi_as_numpy(roi) --- End-to-end walkthroughs: create an OME-Zarr, process and segment images, extract - features, and explore a plate. + 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) diff --git a/docs/table_specs/overview.md b/docs/table_specs/overview.md index 05d65f22..af744bea 100644 --- a/docs/table_specs/overview.md +++ b/docs/table_specs/overview.md @@ -4,7 +4,7 @@ description: "The ngio table architecture: backends, in-memory objects and type # 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 derived from the [fractal table specification](https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/). +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 diff --git a/docs/tutorials/image_segmentation.md b/docs/tutorials/image_segmentation.md index 6b3b9dc0..cc881a06 100644 --- a/docs/tutorials/image_segmentation.md +++ b/docs/tutorials/image_segmentation.md @@ -69,3 +69,10 @@ the masked image rather than the original one. - [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/). From 7d9dcb4ac75e87ad67ea1b76508a15b666c3bd04 Mon Sep 17 00:00:00 2001 From: lorenzo Date: Thu, 30 Jul 2026 11:39:43 +0200 Subject: [PATCH 11/14] add logo to docs --- CHANGELOG.md | 1 + README.md | 7 +- brand/fonts/OFL.txt | 93 +++++++++++++++++++++++ brand/fonts/SpaceGrotesk[wght].ttf | Bin 0 -> 136676 bytes brand/logo-dark.svg | 16 ++++ brand/logo-lockup-dark.svg | 23 ++++++ brand/logo-lockup-stacked-dark.svg | 23 ++++++ brand/logo-lockup-stacked.svg | 23 ++++++ brand/logo-lockup.svg | 23 ++++++ brand/logo-mono.svg | 7 ++ brand/logo.svg | 16 ++++ docs/assets/logo-lockup-dark.svg | 23 ++++++ docs/assets/logo-lockup-stacked-dark.svg | 23 ++++++ docs/assets/logo-lockup-stacked.svg | 23 ++++++ docs/assets/logo-lockup.svg | 23 ++++++ docs/index.md | 5 +- docs/stylesheets/ngio.css | 52 +++++++++++++ 17 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 brand/fonts/OFL.txt create mode 100644 brand/fonts/SpaceGrotesk[wght].ttf create mode 100644 brand/logo-dark.svg create mode 100644 brand/logo-lockup-dark.svg create mode 100644 brand/logo-lockup-stacked-dark.svg create mode 100644 brand/logo-lockup-stacked.svg create mode 100644 brand/logo-lockup.svg create mode 100644 brand/logo-mono.svg create mode 100644 brand/logo.svg create mode 100644 docs/assets/logo-lockup-dark.svg create mode 100644 docs/assets/logo-lockup-stacked-dark.svg create mode 100644 docs/assets/logo-lockup-stacked.svg create mode 100644 docs/assets/logo-lockup.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 83edb66c..8c1a7eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ - Flag `ngio.experimental.iterators` as experimental on the iterators guide and API page, retitle the `ngio.iterators` reference page and nav entry to the real module path, and add an executed example to the iterators page, which previously had no runnable code. - Consistency pass: `>>>` prompts removed so every example is copy-pasteable, `OME-Zarr Container` no longer code-formatted as if it were a class, `get_array` presented as the mode-dispatch form rather than a back-compat shim, uniform "API reference" titles, table-spec page titles matched to their nav labels, numbered steps across all five tutorials, and tutorial snippet comments moved to second person and British spelling (they render into the page). - `CONTRIBUTING.md`: `lint` and the `bump-*` tasks need `-e dev`; PRs run a reduced CI matrix, not the full one; sentence-case headings. +- Put the ngio logo on the README and the docs landing page in place of the bare `# ngio` heading, using the horizontal lockup (mark plus outlined wordmark). Both surfaces carry a light and a dark variant and follow the reader's colour scheme — `` with `prefers-color-scheme` on GitHub, the theme's `#only-light`/`#only-dark` fragments in the docs — and the header mark now swaps to `logo-dark.svg` under the slate palette. The landing page keeps its `# ngio` heading off-screen so the page title, the table of contents and screen readers are unaffected. The logo design-system source, including the Space Grotesk font file, moved out of `docs/assets/` to a top-level `brand/` directory, since Zensical copies everything under `docs/` into the published site. - 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/README.md b/README.md index 2d2a603e..50ef58dc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# ngio +

+ + + ngio + +

[![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) 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 0000000000000000000000000000000000000000..a1b2e6c26093066510a31147e7aec9abdc8d6c5e GIT binary patch literal 136676 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 IH;+H=zZFcdV*mgE literal 0 HcmV?d00001 diff --git a/brand/logo-dark.svg b/brand/logo-dark.svg new file mode 100644 index 00000000..bf170eb6 --- /dev/null +++ b/brand/logo-dark.svg @@ -0,0 +1,16 @@ + + ngio + + + + + + + + + + + + + + \ No newline at end of file diff --git a/brand/logo-lockup-dark.svg b/brand/logo-lockup-dark.svg new file mode 100644 index 00000000..43b961d3 --- /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. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/brand/logo-lockup-stacked-dark.svg b/brand/logo-lockup-stacked-dark.svg new file mode 100644 index 00000000..f31f530a --- /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. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/brand/logo-lockup-stacked.svg b/brand/logo-lockup-stacked.svg new file mode 100644 index 00000000..0a019b33 --- /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. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/brand/logo-lockup.svg b/brand/logo-lockup.svg new file mode 100644 index 00000000..49ed56cb --- /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. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/brand/logo-mono.svg b/brand/logo-mono.svg new file mode 100644 index 00000000..ed2e22ba --- /dev/null +++ b/brand/logo-mono.svg @@ -0,0 +1,7 @@ + + ngio + + + + + \ No newline at end of file diff --git a/brand/logo.svg b/brand/logo.svg new file mode 100644 index 00000000..905bcf3f --- /dev/null +++ b/brand/logo.svg @@ -0,0 +1,16 @@ + + ngio + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/logo-lockup-dark.svg b/docs/assets/logo-lockup-dark.svg new file mode 100644 index 00000000..43b961d3 --- /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. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/logo-lockup-stacked-dark.svg b/docs/assets/logo-lockup-stacked-dark.svg new file mode 100644 index 00000000..f31f530a --- /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. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/logo-lockup-stacked.svg b/docs/assets/logo-lockup-stacked.svg new file mode 100644 index 00000000..0a019b33 --- /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. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/logo-lockup.svg b/docs/assets/logo-lockup.svg new file mode 100644 index 00000000..49ed56cb --- /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. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 0f761089..ba08de42 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,10 @@ description: ngio is a Python library for OME-Zarr bioimage analysis, with an object-based API for images, labels, tables, ROIs and HCS plates. --- -# ngio +# ngio { .ngio-visually-hidden } + +![](assets/logo-lockup.svg#only-light){ .ngio-hero-logo } +![](assets/logo-lockup-dark.svg#only-dark){ .ngio-hero-logo } **Next generation file format IO — a Python library for OME-Zarr bioimage analysis.** diff --git a/docs/stylesheets/ngio.css b/docs/stylesheets/ngio.css index 1a59a306..01ace7da 100644 --- a/docs/stylesheets/ngio.css +++ b/docs/stylesheets/ngio.css @@ -269,6 +269,22 @@ 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 @@ -479,6 +495,42 @@ 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; From c871a3d4db4baf9ea6c1a076ffc82ae74106c563 Mon Sep 17 00:00:00 2001 From: lorenzo <lorenzo.cerrone@uzh.ch> Date: Thu, 30 Jul 2026 11:54:17 +0200 Subject: [PATCH 12/14] add logo to docs --- .pre-commit-config.yaml | 4 ++++ README.md | 4 ++-- brand/logo-dark.svg | 10 +++++----- brand/logo-lockup-dark.svg | 2 +- brand/logo-lockup-stacked-dark.svg | 2 +- brand/logo-lockup-stacked.svg | 2 +- brand/logo-lockup.svg | 2 +- brand/logo-mono.svg | 4 ++-- brand/logo.svg | 10 +++++----- docs/assets/logo-lockup-dark.svg | 2 +- docs/assets/logo-lockup-stacked-dark.svg | 2 +- docs/assets/logo-lockup-stacked.svg | 2 +- docs/assets/logo-lockup.svg | 2 +- docs/index.md | 2 +- 14 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a17c1a2e..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 diff --git a/README.md b/README.md index 50ef58dc..4135a2e5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -<p align="center"> +<p> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/BioVisionCenter/ngio/main/docs/assets/logo-lockup-dark.svg"> - <img alt="ngio" width="320" src="https://raw.githubusercontent.com/BioVisionCenter/ngio/main/docs/assets/logo-lockup.svg"> + <img alt="" width="320" src="https://raw.githubusercontent.com/BioVisionCenter/ngio/main/docs/assets/logo-lockup.svg"> </picture> </p> diff --git a/brand/logo-dark.svg b/brand/logo-dark.svg index bf170eb6..cc0a9acf 100644 --- a/brand/logo-dark.svg +++ b/brand/logo-dark.svg @@ -1,16 +1,16 @@ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="ngio"> <title>ngio - - + + - + - + - \ No newline at end of file + diff --git a/brand/logo-lockup-dark.svg b/brand/logo-lockup-dark.svg index 43b961d3..66d5f786 100644 --- a/brand/logo-lockup-dark.svg +++ b/brand/logo-lockup-dark.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/brand/logo-lockup-stacked-dark.svg b/brand/logo-lockup-stacked-dark.svg index f31f530a..22a6502b 100644 --- a/brand/logo-lockup-stacked-dark.svg +++ b/brand/logo-lockup-stacked-dark.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/brand/logo-lockup-stacked.svg b/brand/logo-lockup-stacked.svg index 0a019b33..259ebc75 100644 --- a/brand/logo-lockup-stacked.svg +++ b/brand/logo-lockup-stacked.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/brand/logo-lockup.svg b/brand/logo-lockup.svg index 49ed56cb..c29a513e 100644 --- a/brand/logo-lockup.svg +++ b/brand/logo-lockup.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/brand/logo-mono.svg b/brand/logo-mono.svg index ed2e22ba..121af638 100644 --- a/brand/logo-mono.svg +++ b/brand/logo-mono.svg @@ -1,7 +1,7 @@ ngio - + - \ No newline at end of file + diff --git a/brand/logo.svg b/brand/logo.svg index 905bcf3f..f0748369 100644 --- a/brand/logo.svg +++ b/brand/logo.svg @@ -1,16 +1,16 @@ ngio - - + + - + - + - \ No newline at end of file + diff --git a/docs/assets/logo-lockup-dark.svg b/docs/assets/logo-lockup-dark.svg index 43b961d3..66d5f786 100644 --- a/docs/assets/logo-lockup-dark.svg +++ b/docs/assets/logo-lockup-dark.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/docs/assets/logo-lockup-stacked-dark.svg b/docs/assets/logo-lockup-stacked-dark.svg index f31f530a..22a6502b 100644 --- a/docs/assets/logo-lockup-stacked-dark.svg +++ b/docs/assets/logo-lockup-stacked-dark.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/docs/assets/logo-lockup-stacked.svg b/docs/assets/logo-lockup-stacked.svg index 0a019b33..259ebc75 100644 --- a/docs/assets/logo-lockup-stacked.svg +++ b/docs/assets/logo-lockup-stacked.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/docs/assets/logo-lockup.svg b/docs/assets/logo-lockup.svg index 49ed56cb..c29a513e 100644 --- a/docs/assets/logo-lockup.svg +++ b/docs/assets/logo-lockup.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/docs/index.md b/docs/index.md index ba08de42..0a46b8d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,7 +26,7 @@ regions of interest (ROIs) for extracting and analysing specific regions of your 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 +- **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. ## Installation From bc676a361895136083fd187a2d6e01acd4f1e413 Mon Sep 17 00:00:00 2001 From: lorenzo Date: Thu, 30 Jul 2026 13:31:12 +0200 Subject: [PATCH 13/14] add more figures --- CHANGELOG.md | 2 + docs/CLAUDE.md | 16 +- docs/getting_started/0_quickstart.md | 10 + docs/getting_started/1_ome_zarr_containers.md | 7 + docs/getting_started/2_images.md | 18 + docs/snippets/_render.py | 376 ++++++++++++++++++ docs/snippets/getting_started/get_started.py | 235 +++++------ .../snippets/getting_started/masked_images.py | 128 +++--- docs/snippets/getting_started/quickstart.py | 28 ++ docs/snippets/tutorials/create_ome_zarr.py | 57 +-- docs/snippets/tutorials/feature_extraction.py | 27 +- docs/snippets/tutorials/hcs_exploration.py | 29 +- docs/snippets/tutorials/image_processing.py | 68 +--- docs/snippets/tutorials/image_segmentation.py | 108 ++--- 14 files changed, 711 insertions(+), 398 deletions(-) create mode 100644 docs/snippets/_render.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1a7eb7..6269711a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ - Consistency pass: `>>>` prompts removed so every example is copy-pasteable, `OME-Zarr Container` no longer code-formatted as if it were a class, `get_array` presented as the mode-dispatch form rather than a back-compat shim, uniform "API reference" titles, table-spec page titles matched to their nav labels, numbered steps across all five tutorials, and tutorial snippet comments moved to second person and British spelling (they render into the page). - `CONTRIBUTING.md`: `lint` and the `bump-*` tasks need `-e dev`; PRs run a reduced CI matrix, not the full one; sentence-case headings. - Put the ngio logo on the README and the docs landing page in place of the bare `# ngio` heading, using the horizontal lockup (mark plus outlined wordmark). Both surfaces carry a light and a dark variant and follow the reader's colour scheme — `` with `prefers-color-scheme` on GitHub, the theme's `#only-light`/`#only-dark` fragments in the docs — and the header mark now swaps to `logo-dark.svg` under the slate palette. The landing page keeps its `# ngio` heading off-screen so the page title, the table of contents and screen readers are unaffected. The logo design-system source, including the Space Grotesk font file, moved out of `docs/assets/` to a top-level `brand/` directory, since Zensical copies everything under `docs/` into the published site. +- Give every executed figure one house style, and show real pixels on the three getting-started pages that had none. New figures: the DAPI channel on the quickstart page (which previously ended on a repr), the three channels side by side under the channel metadata, and on the images page a world-coordinate ROI outlined next to the region it returns, plus the `nuclei` label coloured by object id over the channel it was segmented from. Every intensity panel is now windowed on percentiles rather than the full dtype range — a uint16 MIP shown on 0-65535 is nearly black — and carries a scale bar derived from `pixel_size`. Titles are left-aligned and set in the site's mono face (`svg.fonttype: "none"` keeps figure text as ``, so the inline SVG resolves the site's webfont), and the magenta of a ROI outline is now swapped for `var(--ngio-magenta)` like the rest of the chrome, so it follows the light/dark toggle instead of baking the light value into the page. +- Replace the eight near-verbatim copies of the figure and table helpers (`print_figure` in five snippet scripts, `print_table` in three) with `docs/snippets/_render.py`, imported from each script's hidden `plot_helpers` / `table_helpers` section. The helpers return markup rather than printing it: markdown-exec does not redirect `sys.stdout`, it injects a `print` into the executed block's globals, so a `print` inside an imported module lands on the build's terminal and the block renders empty — with the build still exiting 0. The tutorials keep plain `plt.subplots` / `imshow` in their reader-facing code and pick up the style from the shared rcParams. - 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/docs/CLAUDE.md b/docs/CLAUDE.md index 3f207796..390a8f2f 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -3,15 +3,25 @@ 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. +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. -Four traps, none visible from the sources: +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 `print_table` helper and `html="1"`, never `.to_markdown()` — +- 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])`). diff --git a/docs/getting_started/0_quickstart.md b/docs/getting_started/0_quickstart.md index 30bb1278..e7cf4c14 100644 --- a/docs/getting_started/0_quickstart.md +++ b/docs/getting_started/0_quickstart.md @@ -92,6 +92,16 @@ Open an OME-Zarr file and inspect its contents. --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: + +```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 [next section](1_ome_zarr_containers.md) goes into more detail: inspecting and editing metadata, opening remote stores, and deriving new images and labels. diff --git a/docs/getting_started/1_ome_zarr_containers.md b/docs/getting_started/1_ome_zarr_containers.md index 6ace586e..ec4a56f2 100644 --- a/docs/getting_started/1_ome_zarr_containers.md +++ b/docs/getting_started/1_ome_zarr_containers.md @@ -164,6 +164,13 @@ Examples of accessing the OME-Zarr metadata: ```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 diff --git a/docs/getting_started/2_images.md b/docs/getting_started/2_images.md index a99dc723..91fd2844 100644 --- a/docs/getting_started/2_images.md +++ b/docs/getting_started/2_images.md @@ -103,6 +103,9 @@ ngio provides a high-level API to access the image data at different resolution ```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: @@ -206,6 +209,14 @@ To read or write a specific region of the image defined in world coordinates, yo --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 A label is a segmentation mask that identifies objects in the image. In ngio a [`Label`][ngio.Label] @@ -246,6 +257,13 @@ Here is how to reach one of them: --8<-- "docs/snippets/getting_started/get_started.py:get_label_nearest" ``` +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 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`. 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 index 2082b113..0e16705e 100644 --- a/docs/snippets/getting_started/get_started.py +++ b/docs/snippets/getting_started/get_started.py @@ -12,87 +12,25 @@ """ # --8<-- [start:plot_helpers] -from io import StringIO - -import matplotlib -import numpy as np -from matplotlib import pyplot as plt -from matplotlib.colors import ListedColormap -from matplotlib.figure import Figure - -matplotlib.use("Agg") - - -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) +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") -def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks. - - Recolours every bit of chrome to one sentinel, then swaps that sentinel for - a theme variable in the emitted markup. The figure therefore follows the - light/dark toggle rather than baking black text on white 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. - - The `.ngio-figure` wrapper is what the stylesheet keys on to strip the - `OUT` terminal-output treatment that `.result` applies by default. - """ - ink = "#5b6569" - for ax in fig.axes: - ax.tick_params(colors=ink, which="both") - for spine in ax.spines.values(): - spine.set_edgecolor(ink) - for text in (ax.title, ax.xaxis.label, ax.yaxis.label): - text.set_color(ink) - for label in ax.get_xticklabels() + ax.get_yticklabels(): - label.set_color(ink) - if ax.get_legend() is not None: - for text in ax.get_legend().get_texts(): - text.set_color(ink) - for text in fig.texts: - text.set_color(ink) - - buffer = StringIO() - fig.savefig(buffer, format="svg", transparent=True) - 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}') - +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 pandas as pd - - -def print_table(df: pd.DataFrame) -> None: - """Print 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. - """ - # 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) - print(html.replace(' class="dataframe"', "")) +import sys +sys.path.append("docs/snippets") +from _render import table_html # --8<-- [end:table_helpers] # --------------------------------------------------------------------------- @@ -170,6 +108,24 @@ def print_table(df: pd.DataFrame) -> None: 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 # --------------------------------------------------------------------------- @@ -281,6 +237,31 @@ def process(patch: np.ndarray) -> np.ndarray: 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] @@ -313,6 +294,36 @@ def process(patch: np.ndarray) -> np.ndarray: 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) @@ -334,32 +345,18 @@ def process(patch: np.ndarray) -> np.ndarray: # --8<-- [end:roi_table_get] # --8<-- [start:plot_fov_roi_on_image] -from matplotlib.patches import Rectangle - image_3 = ome_zarr_container.get_image(path="3") -image_data = np.squeeze(image_3.get_as_numpy(c=0)) - -roi = roi_table.get("FOV_1").to_pixel(pixel_size=image_3.pixel_size) -x_slice = roi.get("x") -y_slice = roi.get("y") - -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, - # Magenta is the docs' semantic colour for tables and ROIs. - edgecolor="#c2185b", - facecolor="none", - lw=2, - ) + +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, ) -ax.axis("off") +add_roi_rectangle(ax, roi_table.get("FOV_1"), image_3.pixel_size) fig.tight_layout() -print_figure(fig) +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] @@ -371,14 +368,16 @@ def process(patch: np.ndarray) -> np.ndarray: # --8<-- [start:plot_fov_roi_crop] roi = roi_table.get("FOV_1") image_3 = ome_zarr_container.get_image(path="3") -image_data = np.squeeze(image_3.get_roi_as_numpy(roi, c=0)) -fig, ax = plt.subplots(figsize=(8, 4)) -ax.set_title("FOV_1 ROI") -ax.imshow(image_data, cmap="gray") -ax.axis("off") +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(fig) +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] @@ -394,29 +393,37 @@ def process(patch: np.ndarray) -> np.ndarray: # --8<-- [end:masking_table_slice_image] # --8<-- [start:plot_masking_roi_crop] -cmap = random_label_cmap() - roi = masking_table.get_label(100) image_2 = ome_zarr_container.get_image(path="2") -image_data = np.squeeze(image_2.get_roi_as_numpy(roi, c=0)) - label_2 = ome_zarr_container.get_label("nuclei", pixel_size=image_2.pixel_size) -label_data = np.squeeze(label_2.get_roi_as_numpy(roi)) -fig, ax = plt.subplots(figsize=(8, 4)) -ax.set_title("Label 100 ROI") -ax.imshow(image_data, cmap="gray") -ax.imshow(label_data, cmap=cmap, alpha=0.6) -ax.axis("off") +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(fig) +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(feature_table.dataframe.head(5)) +print(table_html(feature_table.dataframe.head(5))) # --8<-- [end:feature_table] # --8<-- [start:create_roi_table] diff --git a/docs/snippets/getting_started/masked_images.py b/docs/snippets/getting_started/masked_images.py index 3b43edac..c48120b5 100644 --- a/docs/snippets/getting_started/masked_images.py +++ b/docs/snippets/getting_started/masked_images.py @@ -8,54 +8,17 @@ """ # --8<-- [start:plot_helpers] -from io import StringIO +import sys -import matplotlib -from matplotlib import pyplot as plt -from matplotlib.figure import Figure - -matplotlib.use("Agg") - - -def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks. - - Recolours every bit of chrome to one sentinel, then swaps that sentinel for - a theme variable in the emitted markup. The figure therefore follows the - light/dark toggle rather than baking black text on white 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. - - The `.ngio-figure` wrapper is what the stylesheet keys on to strip the - `OUT` terminal-output treatment that `.result` applies by default. - """ - ink = "#5b6569" - for ax in fig.axes: - ax.tick_params(colors=ink, which="both") - for spine in ax.spines.values(): - spine.set_edgecolor(ink) - for text in (ax.title, ax.xaxis.label, ax.yaxis.label): - text.set_color(ink) - for label in ax.get_xticklabels() + ax.get_yticklabels(): - label.set_color(ink) - if ax.get_legend() is not None: - for text in ax.get_legend().get_texts(): - text.set_color(ink) - for text in fig.texts: - text.set_color(ink) - - buffer = StringIO() - fig.savefig(buffer, format="svg", transparent=True) - 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}') +# 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] @@ -84,17 +47,15 @@ def print_figure(fig: Figure) -> None: # --8<-- [end:masked_roi_numpy] # --8<-- [start:plot_masked_roi] -import numpy as np - -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, 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(fig) +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] @@ -103,15 +64,15 @@ def print_figure(fig: Figure) -> None: # --8<-- [end:masked_roi_zoom] # --8<-- [start:plot_masked_roi_zoom] -image_data = masked_image.get_roi_as_numpy(label=1009, c=0, zoom_factor=2) -image_data = np.squeeze(image_data) - -fig, ax = plt.subplots(figsize=(8, 4)) -ax.set_title("Label 1009 ROI - Zoomed out") -ax.imshow(image_data, cmap="gray") -ax.axis("off") +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(fig) +print(figure_html(fig, alt="The same nucleus with twice the surrounding context.")) # --8<-- [end:plot_masked_roi_zoom] # --8<-- [start:get_roi_masked] @@ -120,15 +81,20 @@ def print_figure(fig: Figure) -> None: # --8<-- [end:get_roi_masked] # --8<-- [start:plot_get_roi_masked] -masked_roi_data = masked_image.get_roi_masked_as_numpy(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, 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(fig) +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] @@ -140,15 +106,19 @@ def print_figure(fig: Figure) -> None: # --8<-- [end:set_roi_masked] # --8<-- [start:plot_after_set_roi_masked] -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, 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(fig) +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] diff --git a/docs/snippets/getting_started/quickstart.py b/docs/snippets/getting_started/quickstart.py index 9bb57051..3d9ca48e 100644 --- a/docs/snippets/getting_started/quickstart.py +++ b/docs/snippets/getting_started/quickstart.py @@ -24,3 +24,31 @@ 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 index 209dfd63..707fdcc1 100644 --- a/docs/snippets/tutorials/create_ome_zarr.py +++ b/docs/snippets/tutorials/create_ome_zarr.py @@ -8,63 +8,26 @@ """ # --8<-- [start:plot_helpers] -from io import StringIO +import sys -import matplotlib -from matplotlib import pyplot as plt -from matplotlib.figure import Figure - -matplotlib.use("Agg") - - -def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks. - - Recolours every bit of chrome to one sentinel, then swaps that sentinel for - a theme variable in the emitted markup. The figure therefore follows the - light/dark toggle rather than baking black text on white 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. - - The `.ngio-figure` wrapper is what the stylesheet keys on to strip the - `OUT` terminal-output treatment that `.result` applies by default. - """ - ink = "#5b6569" - for ax in fig.axes: - ax.tick_params(colors=ink, which="both") - for spine in ax.spines.values(): - spine.set_edgecolor(ink) - for text in (ax.title, ax.xaxis.label, ax.yaxis.label): - text.set_color(ink) - for label in ax.get_xticklabels() + ax.get_yticklabels(): - label.set_color(ink) - if ax.get_legend() is not None: - for text in ax.get_legend().get_texts(): - text.set_color(ink) - for text in fig.texts: - text.set_color(ink) - - buffer = StringIO() - fig.savefig(buffer, format="svg", transparent=True) - 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}') +# 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() +fig, ax = plt.subplots(figsize=(6, 6)) ax.imshow(skimage.data.human_mitosis(), cmap="gray") ax.axis("off") -print_figure(fig) +print(figure_html(fig)) # --8<-- [end:plot_input_image] # --8<-- [start:create] diff --git a/docs/snippets/tutorials/feature_extraction.py b/docs/snippets/tutorials/feature_extraction.py index 43a71c70..c717ffb5 100644 --- a/docs/snippets/tutorials/feature_extraction.py +++ b/docs/snippets/tutorials/feature_extraction.py @@ -8,26 +8,15 @@ """ # --8<-- [start:table_helpers] -import pandas as pd - - -def print_table(df: pd.DataFrame) -> None: - """Print 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. - """ - # 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) - print(html.replace(' class="dataframe"', "")) +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] @@ -116,5 +105,5 @@ def extract_features(image: np.ndarray, label: np.ndarray) -> pd.DataFrame: # --8<-- [end:extract] # --8<-- [start:read_table_back] -print_table(ome_zarr.get_table("nuclei_regionprops").dataframe.head()) +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 index 4c080a16..5a659758 100644 --- a/docs/snippets/tutorials/hcs_exploration.py +++ b/docs/snippets/tutorials/hcs_exploration.py @@ -8,26 +8,15 @@ """ # --8<-- [start:table_helpers] -import pandas as pd - - -def print_table(df: pd.DataFrame) -> None: - """Print 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. - """ - # 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) - print(html.replace(' class="dataframe"', "")) +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] @@ -52,7 +41,7 @@ def print_table(df: pd.DataFrame) -> None: # --8<-- [start:concatenate_tables] # Aggregate all table across all images table = hcs_zarr.concatenate_image_tables(name="nuclei") -print_table(table.dataframe.head()) +print(table_html(table.dataframe.head())) # --8<-- [end:concatenate_tables] # --8<-- [start:save_table] @@ -60,7 +49,7 @@ def print_table(df: pd.DataFrame) -> None: hcs_zarr.add_table(name="nuclei", table=table, overwrite=True) # Read the table back for sanity check -print_table(hcs_zarr.get_table("nuclei").dataframe.head()) +print(table_html(hcs_zarr.get_table("nuclei").dataframe.head())) # --8<-- [end:save_table] # --8<-- [start:create_plate] diff --git a/docs/snippets/tutorials/image_processing.py b/docs/snippets/tutorials/image_processing.py index 1c691606..76a44c45 100644 --- a/docs/snippets/tutorials/image_processing.py +++ b/docs/snippets/tutorials/image_processing.py @@ -8,54 +8,17 @@ """ # --8<-- [start:plot_helpers] -from io import StringIO +import sys -import matplotlib -from matplotlib import pyplot as plt -from matplotlib.figure import Figure - -matplotlib.use("Agg") - - -def print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks. - - Recolours every bit of chrome to one sentinel, then swaps that sentinel for - a theme variable in the emitted markup. The figure therefore follows the - light/dark toggle rather than baking black text on white 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. - - The `.ngio-figure` wrapper is what the stylesheet keys on to strip the - `OUT` terminal-output treatment that `.result` applies by default. - """ - ink = "#5b6569" - for ax in fig.axes: - ax.tick_params(colors=ink, which="both") - for spine in ax.spines.values(): - spine.set_edgecolor(ink) - for text in (ax.title, ax.xaxis.label, ax.yaxis.label): - text.set_color(ink) - for label in ax.get_xticklabels() + ax.get_yticklabels(): - label.set_color(ink) - if ax.get_legend() is not None: - for text in ax.get_legend().get_texts(): - text.set_color(ink) - for text in fig.texts: - text.set_color(ink) - - buffer = StringIO() - fig.savefig(buffer, format="svg", transparent=True) - 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}') +# 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] @@ -123,15 +86,22 @@ def gaussian_blur(image: np.ndarray, sigma: float) -> np.ndarray: # --8<-- [end:apply_blur] # --8<-- [start:plot_blur] -fig, axs = plt.subplots(2, 1, figsize=(8, 4)) +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(image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]), cmap="gray") +axs[0].imshow(original, cmap="gray", vmin=vmin, vmax=vmax) axs[1].set_title("Blurred image") -axs[1].imshow(blurred_image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]), cmap="gray") +axs[1].imshow(blurred, cmap="gray", vmin=vmin, vmax=vmax) for ax in axs: ax.axis("off") fig.tight_layout() -print_figure(fig) +print(figure_html(fig)) # --8<-- [end:plot_blur] # --8<-- [start:dask_blur] diff --git a/docs/snippets/tutorials/image_segmentation.py b/docs/snippets/tutorials/image_segmentation.py index bc464e4d..3eea6cbd 100644 --- a/docs/snippets/tutorials/image_segmentation.py +++ b/docs/snippets/tutorials/image_segmentation.py @@ -8,64 +8,17 @@ """ # --8<-- [start:plot_helpers] -from io import StringIO +import sys -import matplotlib -import numpy as np -from matplotlib import pyplot as plt -from matplotlib.colors import ListedColormap -from matplotlib.figure import Figure - -matplotlib.use("Agg") - - -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 print_figure(fig: Figure) -> None: - """Print a figure as inline SVG, for markdown-exec `html` blocks. - - Recolours every bit of chrome to one sentinel, then swaps that sentinel for - a theme variable in the emitted markup. The figure therefore follows the - light/dark toggle rather than baking black text on white 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. - - The `.ngio-figure` wrapper is what the stylesheet keys on to strip the - `OUT` terminal-output treatment that `.result` applies by default. - """ - ink = "#5b6569" - for ax in fig.axes: - ax.tick_params(colors=ink, which="both") - for spine in ax.spines.values(): - spine.set_edgecolor(ink) - for text in (ax.title, ax.xaxis.label, ax.yaxis.label): - text.set_color(ink) - for label in ax.get_xticklabels() + ax.get_yticklabels(): - label.set_color(ink) - if ax.get_legend() is not None: - for text in ax.get_legend().get_texts(): - text.set_color(ink) - for text in fig.texts: - text.set_color(ink) - - buffer = StringIO() - fig.savefig(buffer, format="svg", transparent=True) - 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}') +# 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] @@ -138,16 +91,24 @@ def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray # --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, 4)) +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") +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) +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(fig) +print(figure_html(fig)) # --8<-- [end:plot_segmentation] # --8<-- [start:create_mask] @@ -163,15 +124,19 @@ def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray # --8<-- [end:create_mask] # --8<-- [start:plot_mask] -fig, axs = plt.subplots(2, 1, figsize=(8, 4)) +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") +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) +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(fig) +print(figure_html(fig)) # --8<-- [end:plot_mask] # --8<-- [start:masked_segment] @@ -208,13 +173,22 @@ def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray # --8<-- [end:masked_segment] # --8<-- [start:plot_masked_segmentation] -fig, axs = plt.subplots(2, 1, figsize=(8, 4)) +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") +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) +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(fig) +print(figure_html(fig)) # --8<-- [end:plot_masked_segmentation] From c7d1ee999b75b4f219590e12016b356df4c37689 Mon Sep 17 00:00:00 2001 From: lorenzo Date: Thu, 30 Jul 2026 13:53:34 +0200 Subject: [PATCH 14/14] fix spacing and font size in tutorials --- CHANGELOG.md | 16 ++++------------ docs/stylesheets/ngio.css | 3 +++ docs/tutorials/create_ome_zarr.md | 2 ++ docs/tutorials/feature_extraction.md | 2 ++ docs/tutorials/hcs_exploration.md | 2 ++ docs/tutorials/image_processing.md | 2 ++ docs/tutorials/image_segmentation.md | 2 ++ 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6269711a..9c5e7f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,19 +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` (a plain build exits 0 even when a code block raised). -- Apply the commissioned design system: the mark-G logo set, a contrast-audited teal palette and full theme layer in `docs/stylesheets/ngio.css`, and six explanatory figures authored as inline SVG so they follow the light/dark toggle. Site chrome — header, tabs and footer — now sits on its own tone, framing the page rather than bleeding into it. -- Rewrite the copy to the design system's content conventions: sentence case, British spelling, second person, and openers that lead with what the reader does. Covers every page, the nav and `llms.txt`, and adds a landing page, a 14-term glossary, "Next steps" footers and API cross-references. +- 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. -- Fix defects found along the way: Markdown extensions Zensical had silently dropped (declaring `markdown_extensions` replaces its defaults rather than extending them), YAML front matter invalidated by unquoted colons on six pages, `site_url` pointing at the clone URL, tables rendering as literal `|---|`, and per-page snippet state now that each page gets a fresh `markdown-exec` session. -- Proofreading pass over the refactored docs. Corrections: the `strict` parameter of `get_image`/`get_label` defaults to `False` (nearest level), not `True`, and the page said the opposite; `set_array` takes a numpy or dask array, not a dask delayed object; `RoiTable.get` returns one ROI, not all of them; `get_well_images` returns a dict, and omitting `acquisition` returns every image in the well rather than an empty one; the object model figure labelled `ImagesContainer` as a non-existent `MultiscaleImage`; `by_zyx` splits the time axis rather than iterating z-planes; retry records go to a `ngio:` logger, which is not a child of `ngio`; and the "every code block is executed" claim in `index.md`, `README.md` and `llms.txt` is now scoped to the worked examples, 15 blocks being static. -- Correct the table specifications against what ngio actually writes: document how the `backend` attribute is named (the `{backend_name}_v{version}` convention, and the `experimental_{json,csv,parquet}_v1` legacy aliases kept for older tables), the AnnData backend casts to `float64` only for heterogeneous dtypes and stores booleans in `X` rather than `obs`, `instance_key` is always written for feature and masking ROI tables, `index_key`/`index_type` are omitted entirely for generic and condition tables, extra ROI columns do round-trip (via `Roi` extras) and the plate-location and index columns were missing from the optional list, only four of the five table types are auto-detected on read (`GenericRoiTable` is reachable only via `get_generic_roi_table`), condition tables were missing from the getting-started list, and the feature-type column lists are declarative only — ngio writes but never reads them. Also fixes the malformed JSON-backend directory tree, notes the Zarr v3 `zarr.json` layout, corrects the plate tree's column and image labels, and makes every spec block parse (trailing commas removed, fences retagged `json5` so the `//` comments highlight instead of reading as syntax errors). -- Flag `ngio.experimental.iterators` as experimental on the iterators guide and API page, retitle the `ngio.iterators` reference page and nav entry to the real module path, and add an executed example to the iterators page, which previously had no runnable code. -- Consistency pass: `>>>` prompts removed so every example is copy-pasteable, `OME-Zarr Container` no longer code-formatted as if it were a class, `get_array` presented as the mode-dispatch form rather than a back-compat shim, uniform "API reference" titles, table-spec page titles matched to their nav labels, numbered steps across all five tutorials, and tutorial snippet comments moved to second person and British spelling (they render into the page). -- `CONTRIBUTING.md`: `lint` and the `bump-*` tasks need `-e dev`; PRs run a reduced CI matrix, not the full one; sentence-case headings. -- Put the ngio logo on the README and the docs landing page in place of the bare `# ngio` heading, using the horizontal lockup (mark plus outlined wordmark). Both surfaces carry a light and a dark variant and follow the reader's colour scheme — `` with `prefers-color-scheme` on GitHub, the theme's `#only-light`/`#only-dark` fragments in the docs — and the header mark now swaps to `logo-dark.svg` under the slate palette. The landing page keeps its `# ngio` heading off-screen so the page title, the table of contents and screen readers are unaffected. The logo design-system source, including the Space Grotesk font file, moved out of `docs/assets/` to a top-level `brand/` directory, since Zensical copies everything under `docs/` into the published site. -- Give every executed figure one house style, and show real pixels on the three getting-started pages that had none. New figures: the DAPI channel on the quickstart page (which previously ended on a repr), the three channels side by side under the channel metadata, and on the images page a world-coordinate ROI outlined next to the region it returns, plus the `nuclei` label coloured by object id over the channel it was segmented from. Every intensity panel is now windowed on percentiles rather than the full dtype range — a uint16 MIP shown on 0-65535 is nearly black — and carries a scale bar derived from `pixel_size`. Titles are left-aligned and set in the site's mono face (`svg.fonttype: "none"` keeps figure text as ``, so the inline SVG resolves the site's webfont), and the magenta of a ROI outline is now swapped for `var(--ngio-magenta)` like the rest of the chrome, so it follows the light/dark toggle instead of baking the light value into the page. -- Replace the eight near-verbatim copies of the figure and table helpers (`print_figure` in five snippet scripts, `print_table` in three) with `docs/snippets/_render.py`, imported from each script's hidden `plot_helpers` / `table_helpers` section. The helpers return markup rather than printing it: markdown-exec does not redirect `sys.stdout`, it injects a `print` into the executed block's globals, so a `print` inside an imported module lands on the build's terminal and the block renders empty — with the build still exiting 0. The tutorials keep plain `plt.subplots` / `imshow` in their reader-facing code and pick up the style from the shared rcParams. - 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/docs/stylesheets/ngio.css b/docs/stylesheets/ngio.css index 01ace7da..f703d17a 100644 --- a/docs/stylesheets/ngio.css +++ b/docs/stylesheets/ngio.css @@ -212,6 +212,9 @@ 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); } diff --git a/docs/tutorials/create_ome_zarr.md b/docs/tutorials/create_ome_zarr.md index 00b086d2..1cd75e81 100644 --- a/docs/tutorials/create_ome_zarr.md +++ b/docs/tutorials/create_ome_zarr.md @@ -4,6 +4,8 @@ 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. diff --git a/docs/tutorials/feature_extraction.md b/docs/tutorials/feature_extraction.md index 59d8f6f7..3d4ff610 100644 --- a/docs/tutorials/feature_extraction.md +++ b/docs/tutorials/feature_extraction.md @@ -4,6 +4,8 @@ description: Extract regionprops features and store them as an ngio feature tabl # 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. diff --git a/docs/tutorials/hcs_exploration.md b/docs/tutorials/hcs_exploration.md index 3493f0f2..4def7073 100644 --- a/docs/tutorials/hcs_exploration.md +++ b/docs/tutorials/hcs_exploration.md @@ -4,6 +4,8 @@ description: Explore an HCS plate, aggregate tables across images, and create a # 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. diff --git a/docs/tutorials/image_processing.md b/docs/tutorials/image_processing.md index e8f9d895..15278b56 100644 --- a/docs/tutorials/image_processing.md +++ b/docs/tutorials/image_processing.md @@ -4,6 +4,8 @@ description: Apply a Gaussian blur eagerly, lazily with dask, and through an ngi # 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. diff --git a/docs/tutorials/image_segmentation.md b/docs/tutorials/image_segmentation.md index cc881a06..7c06f30b 100644 --- a/docs/tutorials/image_segmentation.md +++ b/docs/tutorials/image_segmentation.md @@ -4,6 +4,8 @@ description: Segment an OME-Zarr image per field of view, then repeat within a m # 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.