From 6a1340257f6395e92dce65593962dd837680dd61 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Fri, 22 May 2026 00:32:28 +0200 Subject: [PATCH] Add interactive region annotation tutorial (skeleton) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds examples/interactive_annotate.ipynb covering the new `sdata.pl.annotate()` API from scverse/spatialdata-plot#684. Uses the same `squidpy.datasets.visium_hne_sdata()` dataset as visium_mouse_brain.ipynb so the download cache is shared. Structure follows the existing examples/ pattern: - Intro + dataset citation + install hint for the [interactive] extra - Load → render the image so the reader sees what to annotate - Widget invocation shown in a markdown fenced block (the static docs build can't execute the anywidget JS runtime; live-kernel users copy the snippet) - GIF placeholder cell — replace interactive_annotate.gif with a real recording before merging - "What the widget produces": a code cell that builds the equivalent ShapesModel directly via shapely + ShapesModel.parse, so downstream cells reproduce in the docs build without the widget - Overlay with render_shapes; polygon-crop via sd.polygon_query - Watermark block matching the other tutorials Wires the new entry into examples/index.md alongside the Visium card. Outstanding before merge: - Replace the hippocampus polygon coordinates with values from an actual annotation pass against visium_hne_sdata - Record interactive_annotate.gif (~3-5 s of drawing/saving) - Add interactive_annotate.png thumbnail for the gallery grid card Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/index.md | 10 ++ examples/interactive_annotate.ipynb | 260 ++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 examples/interactive_annotate.ipynb diff --git a/examples/index.md b/examples/index.md index 84f4cc7..19b2c3b 100644 --- a/examples/index.md +++ b/examples/index.md @@ -14,6 +14,15 @@ Render H&E tissue, overlay spots, color by gene expression and by cluster, and finish with a publication-style figure. ::: +:::{grid-item-card} Interactive region annotation +:link: interactive_annotate +:link-type: doc +:img-top: interactive_annotate.png + +Draw regions of interest directly on a `spatialdata-plot` canvas with +`sdata.pl.annotate(...)` and persist them as a `ShapesModel` element. +::: + :::: ```{toctree} @@ -21,4 +30,5 @@ and finish with a publication-style figure. :maxdepth: 1 visium_mouse_brain +interactive_annotate ``` diff --git a/examples/interactive_annotate.ipynb b/examples/interactive_annotate.ipynb new file mode 100644 index 0000000..a786041 --- /dev/null +++ b/examples/interactive_annotate.ipynb @@ -0,0 +1,260 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "anno-intro", + "metadata": {}, + "source": [ + "# Interactive region annotation\n", + "\n", + "This tutorial shows how to use `sdata.pl.annotate()` to draw regions of interest directly on a `spatialdata-plot` canvas inside a notebook and persist them as a `ShapesModel` element. The widget is a custom [anywidget] that draws client-side, so it works over SSH (Jupyter or VSCode-Remote-SSH) without streaming PNG frames per mouse-move.\n", + "\n", + "**Dataset**: a Visium H&E mouse brain section, downloaded by `squidpy.datasets.visium_hne_sdata` from the scverse example data host. The download (~400 MB) is cached after the first run.\n", + "\n", + "**Requires the `interactive` extra:**\n", + "\n", + "```bash\n", + "pip install 'spatialdata-plot[interactive]'\n", + "```\n", + "\n", + "[anywidget]: https://anywidget.dev" + ] + }, + { + "cell_type": "markdown", + "id": "anno-load-hdr", + "metadata": {}, + "source": [ + "## Loading the dataset\n", + "\n", + "`squidpy.datasets.visium_hne_sdata()` returns a `SpatialData` object with the multi-resolution H&E image (`'hne'`) and the spot polygons (`'spots'`), both aligned in the `'global'` coordinate system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "anno-load", + "metadata": {}, + "outputs": [], + "source": [ + "import squidpy as sq\n", + "from shapely.geometry import Polygon\n", + "\n", + "import spatialdata as sd\n", + "import spatialdata_plot # noqa: F401 (registers the .pl accessor)\n", + "from spatialdata.models import ShapesModel\n", + "from spatialdata.transformations.transformations import Identity\n", + "\n", + "sdata = sq.datasets.visium_hne_sdata()\n", + "sdata" + ] + }, + { + "cell_type": "markdown", + "id": "anno-inspect-hdr", + "metadata": {}, + "source": [ + "## Inspect what we'll annotate\n", + "\n", + "Before drawing, let's render the image so we know what to outline." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "anno-inspect", + "metadata": {}, + "outputs": [], + "source": [ + "sdata.pl.render_images(\"hne\").pl.show()" + ] + }, + { + "cell_type": "markdown", + "id": "anno-launch-hdr", + "metadata": {}, + "source": [ + "## Launching the widget\n", + "\n", + "Call `sdata.pl.annotate(coordinate_system, element)` to open the drawing canvas. The image is rendered once via the standard `render_images` pipeline, exported to PNG, and laid under a client-side SVG drawing surface; all interaction happens in the browser, and only the final shape geometry round-trips to the kernel on Save.\n", + "\n", + "```python\n", + "sdata.pl.annotate(\n", + " coordinate_system=\"global\",\n", + " element=\"hne\",\n", + " max_width=880, # display hint; underlying render is 840×840\n", + " persist=True, # shows a \"Write to disk\" button\n", + ")\n", + "```\n", + "\n", + "> The code above is intentionally a Markdown block, not a code cell — the widget needs a live JS runtime, which the static docs build can't provide. Run the line in your own notebook (Jupyter Lab or VSCode-Remote-SSH) to see the canvas.\n", + "\n", + "**Drawing tools**: rectangle (drag), polygon (click vertices, snap-to-first or Enter to close), lasso (drag freehand). \n", + "**Shortcuts**: `R` / `P` / `L` switch tool · wheel zoom · Shift+drag pan · Alt+click a shape to delete · Ctrl+Z undo · `F` fit · `Esc` cancel in-progress shape.\n", + "\n", + "When you click **Save** the shapes on the canvas are committed to `sdata.shapes[]` as a single `ShapesModel` (multiple rows if you drew multiple shapes). The optional **Write to disk** button calls `sdata.write_element()` to persist to the backing zarr." + ] + }, + { + "cell_type": "markdown", + "id": "anno-gif", + "metadata": {}, + "source": [ + "![Drawing a region with sdata.pl.annotate](interactive_annotate.gif)\n", + "\n", + "*Live recording of the widget. Replace this file with your own capture before publishing the tutorial.*" + ] + }, + { + "cell_type": "markdown", + "id": "anno-simulate-hdr", + "metadata": {}, + "source": [ + "## What the widget produces\n", + "\n", + "To keep the rest of this notebook reproducible without a live kernel, the next cell creates the same `ShapesModel` the widget would have written if you'd drawn a polygon around the hippocampus and clicked Save with the name `'tumor_region'`. After this cell, downstream code is identical regardless of whether you ran the widget or this simulated commit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "anno-simulate", + "metadata": {}, + "outputs": [], + "source": [ + "# Pretend the user drew this polygon in the widget and clicked Save with\n", + "# name=\"tumor_region\". Coordinates are in the 'global' coordinate system\n", + "# of the visium_hne_sdata dataset.\n", + "hippocampus_polygon = Polygon(\n", + " [\n", + " (3200, 4800),\n", + " (4800, 4400),\n", + " (5600, 5200),\n", + " (5400, 6400),\n", + " (4200, 6600),\n", + " (3200, 6000),\n", + " ]\n", + ")\n", + "\n", + "import geopandas as gpd\n", + "\n", + "sdata.shapes[\"tumor_region\"] = ShapesModel.parse(\n", + " gpd.GeoDataFrame({\"geometry\": [hippocampus_polygon]}),\n", + " transformations={\"global\": Identity()},\n", + ")\n", + "sdata.shapes[\"tumor_region\"]" + ] + }, + { + "cell_type": "markdown", + "id": "anno-overlay-hdr", + "metadata": {}, + "source": [ + "## Working with the saved region\n", + "\n", + "The committed element is a normal `ShapesModel` — every downstream API in `spatialdata` and `spatialdata-plot` treats it like any other shapes layer. Here we overlay it on the H&E to confirm placement." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "anno-overlay", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " sdata\n", + " .pl.render_images(\"hne\")\n", + " .pl.render_shapes(\"tumor_region\", outline_color=\"#22d3ee\", fill_alpha=0.2)\n", + " .pl.show()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "anno-query-hdr", + "metadata": {}, + "source": [ + "## Cropping the dataset to the region\n", + "\n", + "Because the region is a registered `ShapesModel`, `sdata.query.polygon` can subset every element down to what falls inside it. Useful for focused analyses or quick QC on a single anatomical structure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "anno-query", + "metadata": {}, + "outputs": [], + "source": [ + "subset = sd.polygon_query(\n", + " sdata,\n", + " sdata[\"tumor_region\"],\n", + " target_coordinate_system=\"global\",\n", + ")\n", + "subset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "anno-query-plot", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " subset\n", + " .pl.render_images(\"hne\")\n", + " .pl.render_shapes(\"spots\", fill_alpha=0.4)\n", + " .pl.show()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "anno-repro-hdr", + "metadata": {}, + "source": [ + "## For reproducibility" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "anno-repro", + "metadata": {}, + "outputs": [], + "source": [ + "# ruff: noqa: F401, F811, I001, E402\n", + "# fmt: off\n", + "import spatialdata_plot\n", + "\n", + "%load_ext watermark\n", + "# fmt: on\n", + "\n", + "%watermark -v -m -p spatialdata,spatialdata_plot,squidpy,anywidget,matplotlib,numpy" + ] + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "display_name": "Python 3", + "language": "python" + }, + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}