Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ Current development version for the next ConfUSIus release.
- [`fetch_brainglobe_atlas`][confusius.datasets.fetch_brainglobe_atlas] downloads every
region mesh in one batched call on the first fetch, so meshes are available offline
afterwards ([#448](https://github.com/confusius-tools/confusius/pull/448)).
- Added [`plot_surface`][confusius.plotting.plot_surface] to display a triangular mesh as
a napari surface layer, and [`plot_atlas_mesh`][confusius.plotting.plot_atlas_mesh]
(also reachable as `ds.atlas.plot.mesh`) to display atlas regions with their mesh, name,
color, and units read from the atlas, aligned with the reference template or a
registered fUSI volume shown with `plot_napari`. Like
[`get_mesh`][confusius.atlas.AtlasAccessor.get_mesh], it accepts one region or a
sequence of them with a single hemisphere filter or one per region, mirroring
[`get_masks`][confusius.atlas.AtlasAccessor.get_masks], and merges the requested
regions into a single surface layer, each drawn in its own atlas color
([#263](https://github.com/confusius-tools/confusius/pull/263)).

### :zap: Performance

Expand Down
3 changes: 1 addition & 2 deletions docs/images/atlas/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,8 @@ def _napari_screenshot(viewer: "napari.Viewer", path: str) -> None:
_section("napari mesh")

try:
surface = atlas.atlas.get_mesh(_MESH_REGION)[_MESH_REGION]
viewer = napari.Viewer(ndisplay=3, show=False)
viewer.add_surface(surface, colormap="gray", name=f"{_MESH_REGION} mesh")
atlas.atlas.plot.mesh(_MESH_REGION, viewer=viewer, colormap="gray")
_napari_screenshot(viewer, str(HERE / "atlas-mesh-root.png"))
viewer.close()
_ok("Saved atlas-mesh-root.png")
Expand Down
8 changes: 4 additions & 4 deletions docs/user-guide/atlas.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,12 @@ dict_keys(['VISp', 'AUDp'])
>>> vertices, faces = atlas.atlas.get_mesh("root")["root"]
```

napari's `add_surface` takes exactly that pair:
To display them, [`plot_atlas_mesh`][confusius.plotting.plot_atlas_mesh] (also reachable
as `atlas.atlas.plot.mesh`) takes the same regions and draws them in napari, each in its
own atlas color:

```python
import napari

napari.Viewer(ndisplay=3).add_surface(atlas.atlas.get_mesh("root")["root"])
atlas.atlas.plot.mesh("root")
```

![Whole-brain surface mesh of the Allen mouse atlas in napari](../images/atlas/atlas-mesh-root.png)
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/visualization.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ generation**:
|---|---|---|
| [`plot_napari`][confusius.plotting.plot_napari] / [`.fusi.plot.napari()`][confusius.xarray.FUSIPlotAccessor.napari] | napari | Interactive exploration of 3D+t datasets |
| [`draw_napari_labels`][confusius.plotting.draw_napari_labels] / [`.fusi.plot.draw_napari_labels()`][confusius.xarray.FUSIPlotAccessor.draw_napari_labels] + [`labels_from_layer`][confusius.plotting.labels_from_layer] / [`.fusi.plot.labels_from_layer()`][confusius.xarray.FUSIPlotAccessor.labels_from_layer] | napari | Interactive manual ROI drawing |
| [`plot_surface`][confusius.plotting.plot_surface] / [`plot_atlas_mesh`][confusius.plotting.plot_atlas_mesh] / [`.atlas.plot.mesh()`][confusius.atlas.AtlasPlotAccessor.mesh] | napari | Overlaying meshes (e.g. atlas region surfaces) in 3D |
| [`plot_volume`][confusius.plotting.plot_volume] / [`.fusi.plot.volume()`][confusius.xarray.FUSIPlotAccessor.volume] | Matplotlib | Static slice grids |
| [`plot_contours`][confusius.plotting.plot_contours] / [`.fusi.plot.contours()`][confusius.xarray.FUSIPlotAccessor.contours] | Matplotlib | Contour-only grids (masks or atlas outlines) |
| [`plot_composite`][confusius.plotting.plot_composite] / [`.fusi.plot.composite()`][confusius.xarray.FUSIPlotAccessor.composite] | Matplotlib | Composite plots of two volumes |
Expand Down
2 changes: 2 additions & 0 deletions src/confusius/atlas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
get_atlas_mesh,
search_atlas,
)
from confusius.atlas._plot import AtlasPlotAccessor

__all__ = [
"AtlasAccessor",
"AtlasPlotAccessor",
"get_atlas_masks",
"get_atlas_mesh",
"search_atlas",
Expand Down
19 changes: 19 additions & 0 deletions src/confusius/atlas/_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import xarray as xr

from confusius._utils.atlas import build_atlas_cmap_and_norm
from confusius.atlas._plot import AtlasPlotAccessor
from confusius.atlas._structures import (
_build_lookup_df,
_get_descendant_ids,
Expand Down Expand Up @@ -175,6 +176,24 @@ def norm(self) -> "BoundaryNorm":
_, norm = build_atlas_cmap_and_norm(self.annotation.attrs["rgb_lookup"])
return norm

# ── Plotting ──────────────────────────────────────────────────────────────────────

@property
def plot(self) -> AtlasPlotAccessor:
"""Plotting namespace for the atlas.

Returns
-------
confusius.atlas.AtlasPlotAccessor
Accessor exposing atlas plotting methods, e.g.
[`mesh`][confusius.atlas.AtlasPlotAccessor.mesh].

Examples
--------
>>> atlas.atlas.plot.mesh("VISp")
"""
return AtlasPlotAccessor(self._ds)

# ── Search ────────────────────────────────────────────────────────────────────────

def search(
Expand Down
96 changes: 96 additions & 0 deletions src/confusius/atlas/_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""The `.atlas.plot` namespace: napari plotting for atlas Datasets."""

from collections.abc import Sequence
from typing import TYPE_CHECKING, Literal

import numpy as np
import numpy.typing as npt
import xarray as xr

if TYPE_CHECKING:
from napari import Viewer
from napari.layers import Surface


class AtlasPlotAccessor:
"""Plotting methods for an atlas Dataset, reached via `ds.atlas.plot`.

Parameters
----------
ds : xarray.Dataset
Atlas Dataset the plotting methods operate on.

Examples
--------
>>> import confusius as cf
>>> atlas = cf.datasets.fetch_brainglobe_atlas("allen_mouse_25um")
>>> viewer, layer = atlas.atlas.plot.mesh("VISp")
"""

def __init__(self, ds: xr.Dataset) -> None:
self._ds = ds

def mesh(
self,
regions: int | str | Sequence[int | str],
sides: (
Literal["left", "right", "both"]
| Sequence[Literal["left", "right", "both"]]
) = "both",
*,
clip: bool = True,
values: npt.NDArray[np.floating] | None = None,
viewer: "Viewer | None" = None,
show_scale_bar: bool = True,
**layer_kwargs,
) -> "tuple[Viewer, Surface]":
"""Display one or more region surface meshes in napari.

Thin wrapper around
[`plot_atlas_mesh`][confusius.plotting.plot_atlas_mesh] bound to this atlas.

Parameters
----------
regions : int or str or sequence of int or str
One or more regions to display, each given as a structure index or acronym.
All requested meshes are merged into a single surface layer.
sides : {"left", "right", "both"} or sequence thereof, default: "both"
Hemisphere filter, forwarded to
[`get_mesh`][confusius.atlas.AtlasAccessor.get_mesh]. Pass a scalar to apply
the same side to all regions, or a sequence of the same length as `regions`
for per-region control.
clip : bool, default: True
Whether to clip the mesh to the reference grid, forwarded to
[`get_mesh`][confusius.atlas.AtlasAccessor.get_mesh].
values : (N,) or (N, T) numpy.ndarray, optional
Per-vertex scalar values used to color the surface through the layer's
colormap. If not provided, each region is drawn in its atlas color.
viewer : napari.Viewer, optional
Existing napari viewer to add the layer to. If not provided, a new viewer
is created.
show_scale_bar : bool, default: True
Whether to show the scale bar.
**layer_kwargs
Additional keyword arguments passed through to
[`plot_atlas_mesh`][confusius.plotting.plot_atlas_mesh].

Returns
-------
viewer : napari.Viewer
The napari viewer instance with the surface layer added.
layer : napari.layers.Surface
The surface layer added to the viewer.
"""
# Imported here so `import confusius.atlas` does not eagerly pull in napari.
from confusius.plotting import plot_atlas_mesh

return plot_atlas_mesh(
self._ds,
regions,
sides,
clip=clip,
values=values,
viewer=viewer,
show_scale_bar=show_scale_bar,
**layer_kwargs,
)
4 changes: 4 additions & 0 deletions src/confusius/plotting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"VolumePlotter",
"draw_napari_labels",
"labels_from_layer",
"plot_atlas_mesh",
"plot_carpet",
"plot_composite",
"plot_contours",
Expand All @@ -13,6 +14,7 @@
"plot_motion_diagnostics",
"plot_napari",
"plot_stat_map",
"plot_surface",
"plot_volume",
]

Expand All @@ -33,5 +35,7 @@
from confusius.plotting.napari import (
draw_napari_labels,
labels_from_layer,
plot_atlas_mesh,
plot_napari,
plot_surface,
)
Loading