From 61ce419615c9ca5fd7ceeff618c6af480c727a8b Mon Sep 17 00:00:00 2001 From: anon Date: Wed, 20 May 2026 18:35:01 +0200 Subject: [PATCH 1/6] Add method='datashader' to render_images for sparse images (#449) Mean-aggregating rasterization + imshow interpolation collapses very sparse images (mostly zeros, rare non-zero pixels) to near-black. Adds method='datashader' + ds_reduction kwargs mirroring the existing render_points/render_shapes API; routes the downsample step through datashader.Canvas.raster with a configurable reduction (default 'max') and forces nearest-neighbor display so the reduction is not re-smoothed. Also centralizes the _DsReduction Literal (previously duplicated across five sites) into render_params.py alongside a new _ImageDsReduction for the image-only set ('mode', 'first', 'last' added; 'sum', 'any', 'count' dropped since they're not valid Canvas.raster downsample methods). --- src/spatialdata_plot/pl/_datashader.py | 4 +- src/spatialdata_plot/pl/basic.py | 43 ++++++++- src/spatialdata_plot/pl/render.py | 33 ++++++- src/spatialdata_plot/pl/render_params.py | 8 +- src/spatialdata_plot/pl/utils.py | 60 ++++++++++++- tests/pl/test_render_images.py | 108 +++++++++++++++++++++++ 6 files changed, 242 insertions(+), 14 deletions(-) diff --git a/src/spatialdata_plot/pl/_datashader.py b/src/spatialdata_plot/pl/_datashader.py index 1d6415ed..59b58d99 100644 --- a/src/spatialdata_plot/pl/_datashader.py +++ b/src/spatialdata_plot/pl/_datashader.py @@ -17,7 +17,7 @@ from matplotlib.colors import Normalize from spatialdata_plot._logging import logger -from spatialdata_plot.pl.render_params import Color, FigParams, ShapesRenderParams +from spatialdata_plot.pl.render_params import Color, FigParams, ShapesRenderParams, _DsReduction from spatialdata_plot.pl.utils import ( _ax_show_and_transform, _convert_alpha_to_datashader_range, @@ -32,8 +32,6 @@ # Type aliases and constants # --------------------------------------------------------------------------- -_DsReduction = Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] - # Sentinel category name used in datashader categorical paths to represent # missing (NaN) values. Must not collide with realistic user category names. _DS_NAN_CATEGORY = "ds_nan" diff --git a/src/spatialdata_plot/pl/basic.py b/src/spatialdata_plot/pl/basic.py index 996161ae..dcedaa0b 100644 --- a/src/spatialdata_plot/pl/basic.py +++ b/src/spatialdata_plot/pl/basic.py @@ -7,7 +7,7 @@ from collections.abc import Callable, Sequence from copy import deepcopy from pathlib import Path -from typing import Any, Literal, cast +from typing import Any, Literal, cast, get_args import matplotlib import matplotlib.pyplot as plt @@ -29,7 +29,7 @@ from xarray import DataArray, DataTree from spatialdata_plot._accessor import register_spatial_data_accessor -from spatialdata_plot._logging import _log_context +from spatialdata_plot._logging import _log_context, logger from spatialdata_plot.pl.render import ( _draw_channel_legend, _render_graph, @@ -52,8 +52,10 @@ LegendParams, PointsRenderParams, ShapesRenderParams, + _DsReduction, _FontSize, _FontWeight, + _ImageDsReduction, ) from spatialdata_plot.pl.utils import ( _RENDER_CMD_TO_CS_FLAG, @@ -194,7 +196,7 @@ def render_shapes( shape: Literal["circle", "hex", "visium_hex", "square"] | None = None, colorbar: bool | str | None = "auto", colorbar_params: dict[str, object] | None = None, - datashader_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None, + datashader_reduction: _DsReduction | None = None, transfunc: Callable[[float], float] | None = None, ) -> sd.SpatialData: """ @@ -384,7 +386,7 @@ def render_points( gene_symbols: str | None = None, colorbar: bool | str | None = "auto", colorbar_params: dict[str, object] | None = None, - datashader_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None, + datashader_reduction: _DsReduction | None = None, transfunc: Callable[[float], float] | None = None, ) -> sd.SpatialData: """ @@ -536,6 +538,8 @@ def render_images( colorbar: bool | str | None = "auto", colorbar_params: dict[str, object] | None = None, channels_as_legend: bool = False, + method: Literal["matplotlib", "datashader"] | None = None, + ds_reduction: _ImageDsReduction | None = None, ) -> sd.SpatialData: """ Render image elements in SpatialData. @@ -616,6 +620,21 @@ def render_images( Ignored for single-channel and RGB(A) images. When multiple ``render_images`` calls use this flag on the same axes, all channel entries are combined into a single legend. + method : str | None, optional + Whether to use ``'matplotlib'`` (default) or ``'datashader'`` for + the downsampling step. When ``'datashader'`` is selected, the + rasterization-to-canvas step uses + :meth:`datashader.Canvas.raster` with ``ds_reduction`` as the + downsample method (default ``'max'``), and ``imshow`` is rendered + with ``interpolation='nearest'`` so the chosen reduction is not + re-smoothed at display time. Useful for very sparse images + (mostly zeros) where mean aggregation collapses the signal — + ``method='datashader'`` with ``ds_reduction='max'`` preserves the + rare non-zero pixels (``plt.spy``-style). + ds_reduction : {"max", "min", "mean", "mode", "first", "last", "var", "std"} | None, optional + Downsample reduction used by the datashader path. Defaults to + ``'max'`` when ``method='datashader'``. Ignored otherwise (a + warning is emitted if set without ``method='datashader'``). Notes ----- @@ -634,6 +653,20 @@ def render_images( """ if grayscale and palette is not None: raise ValueError("Cannot combine grayscale=True with palette.") + + if method is not None and not isinstance(method, str): + raise TypeError("Parameter 'method' must be a string.") + if method is not None and method not in ("matplotlib", "datashader"): + raise ValueError("Parameter 'method' must be either 'matplotlib' or 'datashader'.") + if ds_reduction is not None and not isinstance(ds_reduction, str): + raise TypeError("Parameter 'ds_reduction' must be a string.") + if ds_reduction is not None and ds_reduction not in get_args(_ImageDsReduction): + raise ValueError( + f"Parameter 'ds_reduction' must be one of {get_args(_ImageDsReduction)}, got {ds_reduction!r}." + ) + if ds_reduction is not None and method != "datashader": + logger.warning("Parameter 'ds_reduction' has no effect unless method='datashader'; ignoring.") + params_dict = _validate_image_render_params( self._sdata, element=element, @@ -699,6 +732,8 @@ def render_images( transfunc=transfunc, grayscale=grayscale, channels_as_legend=channels_as_legend, + method=method, + ds_reduction=ds_reduction, ) n_steps += 1 diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 5cd6688e..494f8333 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -39,7 +39,6 @@ _ds_aggregate, _ds_shade_categorical, _ds_shade_continuous, - _DsReduction, _render_ds_image, _render_ds_outlines, ) @@ -55,6 +54,7 @@ LegendParams, PointsRenderParams, ShapesRenderParams, + _DsReduction, ) from spatialdata_plot.pl.utils import ( _ax_show_and_transform, @@ -73,6 +73,7 @@ _prepare_cmap_norm, _prepare_transformation, _rasterize_if_necessary, + _rasterize_if_necessary_datashader, _set_color_source_vec, _validate_polygons, ) @@ -1279,7 +1280,24 @@ def _render_images( scale=scale, ) # rasterize spatial image if necessary to speed up performance - if rasterize: + use_datashader = render_params.method == "datashader" + if use_datashader: + downsample_method = render_params.ds_reduction or "max" + logger.info( + f"Using 'datashader' backend with '{downsample_method}' as downsample method. " + "Depending on the reduction, the value range of the plot might change. " + "Set method to 'matplotlib' to disable this behaviour." + ) + img = _rasterize_if_necessary_datashader( + image=img, + dpi=fig_params.fig.dpi, + width=fig_params.fig.get_size_inches()[0], + height=fig_params.fig.get_size_inches()[1], + coordinate_system=coordinate_system, + extent=extent, + downsample_method=downsample_method, + ) + elif rasterize: img = _rasterize_if_necessary( image=img, dpi=fig_params.fig.dpi, @@ -1389,6 +1407,10 @@ def _render_images( "Consider using 'palette' instead." ) + # Force nearest-neighbor at display time when the datashader reduction picked + # a non-mean aggregation; otherwise imshow's default interpolation would smear it. + _interp = "nearest" if use_datashader else None + # Detect RGB(A) images by channel names — skip when user overrides with palette/cmap is_rgb, has_alpha = _is_rgb_image(channels) has_explicit_cmap = ( @@ -1430,7 +1452,7 @@ def _render_images( render_params.alpha, ) - _ax_show_and_transform(stacked, trans_data, ax, **show_kwargs) + _ax_show_and_transform(stacked, trans_data, ax, interpolation=_interp, **show_kwargs) if render_params.channels_as_legend: logger.warning("channels_as_legend is not supported for true RGB images and will be ignored.") return @@ -1457,6 +1479,7 @@ def _render_images( cmap=cmap, zorder=render_params.zorder, norm=render_params.cmap_params.norm, + interpolation=_interp, ) wants_colorbar = _should_request_colorbar( @@ -1549,6 +1572,7 @@ def _render_images( ax, render_params.alpha, zorder=render_params.zorder, + interpolation=_interp, ) # 2B) Image has n channels, no palette/cmap info -> sample n categorical colors @@ -1613,6 +1637,7 @@ def _render_images( ax, render_params.alpha, zorder=render_params.zorder, + interpolation=_interp, ) # 2C) palette set; also covers `palette + norm=list` since synthesized @@ -1633,6 +1658,7 @@ def _render_images( ax, render_params.alpha, zorder=render_params.zorder, + interpolation=_interp, ) elif palette is None and got_multiple_cmaps: @@ -1654,6 +1680,7 @@ def _render_images( ax, render_params.alpha, zorder=render_params.zorder, + interpolation=_interp, ) # Collect channel legend entries (single point for all multi-channel paths) diff --git a/src/spatialdata_plot/pl/render_params.py b/src/spatialdata_plot/pl/render_params.py index e7232ec7..ec0459d0 100644 --- a/src/spatialdata_plot/pl/render_params.py +++ b/src/spatialdata_plot/pl/render_params.py @@ -12,6 +12,8 @@ _FontWeight = Literal["light", "normal", "medium", "semibold", "bold", "heavy", "black"] _FontSize = Literal["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large"] +_DsReduction = Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] +_ImageDsReduction = Literal["max", "min", "mean", "mode", "first", "last", "var", "std"] # replace with # from spatialdata._types import ColorLike @@ -243,7 +245,7 @@ class ShapesRenderParams: table_name: str | None = None table_layer: str | None = None shape: Literal["circle", "hex", "visium_hex", "square"] | None = None - ds_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None + ds_reduction: _DsReduction | None = None colorbar: bool | str | None = "auto" colorbar_params: dict[str, object] | None = None @@ -265,7 +267,7 @@ class PointsRenderParams: zorder: int = 0 table_name: str | None = None table_layer: str | None = None - ds_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None + ds_reduction: _DsReduction | None = None colorbar: bool | str | None = "auto" colorbar_params: dict[str, object] | None = None @@ -286,6 +288,8 @@ class ImageRenderParams: transfunc: Callable[[np.ndarray], np.ndarray] | list[Callable[[np.ndarray], np.ndarray]] | None = None grayscale: bool = False channels_as_legend: bool = False + method: Literal["matplotlib", "datashader"] | None = None + ds_reduction: _ImageDsReduction | None = None @dataclass diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 35844c6a..0324d311 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -83,6 +83,7 @@ PointsRenderParams, ScalebarParams, ShapesRenderParams, + _DsReduction, _FontSize, _FontWeight, ) @@ -2048,6 +2049,58 @@ def _rasterize_if_necessary( return image +def _rasterize_if_necessary_datashader( + image: DataArray, + dpi: float, + width: float, + height: float, + coordinate_system: str, + extent: dict[str, tuple[float, float]], + downsample_method: str, +) -> DataArray: + """Downsample to canvas resolution with a configurable datashader reduction. + + Used by ``render_images(method='datashader')`` so sparse images (mostly + zeros, rare non-zero pixels) survive the downsample step instead of + being averaged away by the default mean aggregation. + """ + has_c_dim = len(image.shape) == 3 + y_dims, x_dims = (image.shape[1], image.shape[2]) if has_c_dim else image.shape + + target_y_dims = int(dpi * height) + target_x_dims = int(dpi * width) + + if y_dims <= target_y_dims and x_dims <= target_x_dims: + return image + + # spatialdata.rasterize is invoked solely to inherit the output coords and + # spatial transformation; its mean-aggregated values are overwritten below. + world_x = float(extent["x"][1]) - float(extent["x"][0]) + world_y = float(extent["y"][1]) - float(extent["y"][0]) + target_unit_to_pixels = min(target_y_dims / world_y, target_x_dims / world_x) + base = rasterize( + image, + ("y", "x"), + [extent["y"][0], extent["x"][0]], + [extent["y"][1], extent["x"][1]], + coordinate_system, + target_unit_to_pixels=target_unit_to_pixels, + ) + + out_y, out_x = (base.shape[1], base.shape[2]) if has_c_dim else base.shape + # Materialize once: per-chunk reductions across channels would otherwise + # trigger repeated dask graph evaluations on the same source array. + src = image.compute() if hasattr(image.data, "compute") else image + cvs = ds.Canvas( + plot_width=out_x, + plot_height=out_y, + x_range=(float(extent["x"][0]), float(extent["x"][1])), + y_range=(float(extent["y"][0]), float(extent["y"][1])), + ) + base.values = np.asarray(cvs.raster(src, downsample_method=downsample_method).values).astype(base.dtype, copy=False) + return base + + def _multiscale_to_spatial_image( multiscale_image: DataTree, dpi: float, @@ -3385,6 +3438,7 @@ def _ax_show_and_transform( cmap: ListedColormap | LinearSegmentedColormap | None = None, zorder: int = 0, norm: Normalize | None = None, + interpolation: str | None = None, ) -> matplotlib.image.AxesImage: # ``extent`` uses mpl's pixel-grid convention; world placement happens via # ``set_transform(trans_data)`` afterwards. @@ -3396,6 +3450,8 @@ def _ax_show_and_transform( imshow_kwargs["alpha"] = alpha else: imshow_kwargs["cmap"] = cmap + if interpolation is not None: + imshow_kwargs["interpolation"] = interpolation im = ax.imshow(array, **imshow_kwargs) im.set_transform(trans_data) return im @@ -3508,7 +3564,7 @@ def _create_image_from_datashader_result( def _datashader_aggregate_with_function( - reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None, + reduction: _DsReduction | None, cvs: Canvas, spatial_element: GeoDataFrame | dask.dataframe.core.DataFrame, col_for_color: str | None, @@ -3572,7 +3628,7 @@ def _datashader_aggregate_with_function( def _datshader_get_how_kw_for_spread( - reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None, + reduction: _DsReduction | None, ) -> str: # Get the best input for the how argument of ds.tf.spread(), needed for numerical values reduction = reduction or "sum" diff --git a/tests/pl/test_render_images.py b/tests/pl/test_render_images.py index 7c2390ab..6a555279 100644 --- a/tests/pl/test_render_images.py +++ b/tests/pl/test_render_images.py @@ -746,3 +746,111 @@ def test_lognorm_with_zeros_suppresses_colorbar_with_warning(): sdata.pl.render_images("img", norm=LogNorm()).pl.show(ax=ax) finally: plt.close(fig) + + +def _render_sparse_image_max(**kwargs) -> float: + arr = np.zeros((1, 1024, 1024), dtype=np.float32) + arr[0, 500, 500] = 1.0 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) + fig, ax = plt.subplots(figsize=(2, 2), dpi=50) + try: + sdata.pl.render_images("img", **kwargs).pl.show(ax=ax) + return float(np.nanmax(ax.get_images()[0].get_array())) + finally: + plt.close(fig) + + +def test_render_images_datashader_preserves_sparse_max(): + # Regression test for #449. + default_max = _render_sparse_image_max() + datashader_max = _render_sparse_image_max(method="datashader", ds_reduction="max") + assert default_max < 0.1, f"default path should collapse sparse signal, got max={default_max}" + assert datashader_max == pytest.approx(1.0, abs=1e-6), ( + f"datashader should preserve sparse signal at 1.0, got {datashader_max}" + ) + + +class TestRenderImagesDatashader: + """Tests for the method='datashader' code path on render_images (issue #449).""" + + @pytest.fixture(autouse=True) + def _cleanup(self): + yield + plt.close("all") + + def test_method_invalid_type_raises(self, sdata_blobs: SpatialData): + with pytest.raises(TypeError, match="must be a string"): + sdata_blobs.pl.render_images("blobs_image", method=123) # type: ignore[arg-type] + + def test_method_invalid_value_raises(self, sdata_blobs: SpatialData): + with pytest.raises(ValueError, match="matplotlib.*datashader"): + sdata_blobs.pl.render_images("blobs_image", method="bogus") + + def test_ds_reduction_invalid_type_raises(self, sdata_blobs: SpatialData): + with pytest.raises(TypeError, match="must be a string"): + sdata_blobs.pl.render_images("blobs_image", ds_reduction=42) # type: ignore[arg-type] + + def test_ds_reduction_invalid_value_raises(self, sdata_blobs: SpatialData): + with pytest.raises(ValueError, match="ds_reduction"): + sdata_blobs.pl.render_images("blobs_image", method="datashader", ds_reduction="bogus") + + def test_ds_reduction_without_datashader_warns(self, sdata_blobs: SpatialData, caplog): + with logger_warns(caplog, logger, match="ds_reduction"): + _, ax = plt.subplots() + sdata_blobs.pl.render_images("blobs_image", ds_reduction="max").pl.show(ax=ax) + + def test_datashader_basic_renders_single_image(self): + arr = np.zeros((1, 512, 512), dtype=np.float32) + arr[0, 100, 100] = 1.0 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) + _, ax = plt.subplots(figsize=(2, 2), dpi=50) + sdata.pl.render_images("img", method="datashader").pl.show(ax=ax) + assert len(ax.get_images()) == 1 + + def test_datashader_multichannel(self): + arr = np.zeros((3, 512, 512), dtype=np.float32) + arr[0, 100, 100] = 1.0 + arr[1, 200, 200] = 1.0 + arr[2, 300, 300] = 1.0 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1", "c2", "c3"])}) + _, ax = plt.subplots(figsize=(2, 2), dpi=50) + sdata.pl.render_images("img", method="datashader", ds_reduction="max").pl.show(ax=ax) + assert len(ax.get_images()) == 1 + + def test_datashader_rgb_passthrough(self): + arr = np.zeros((3, 256, 256), dtype=np.float32) + arr[0] = 0.8 + arr[1] = 0.2 + arr[2] = 0.1 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["r", "g", "b"])}) + _, ax = plt.subplots(figsize=(2, 2), dpi=50) + sdata.pl.render_images("img", method="datashader").pl.show(ax=ax) + assert ax.get_images()[0].get_array().shape[-1] == 3 + + def test_datashader_with_transfunc(self): + arr = np.zeros((1, 512, 512), dtype=np.float32) + arr[0, 100, 100] = 1.0 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) + _, ax = plt.subplots(figsize=(2, 2), dpi=50) + sdata.pl.render_images("img", method="datashader", ds_reduction="max", transfunc=np.log1p).pl.show(ax=ax) + assert len(ax.get_images()) == 1 + + def test_datashader_with_multiscale(self, sdata_blobs: SpatialData): + _, ax = plt.subplots() + sdata_blobs.pl.render_images("blobs_multiscale_image", method="datashader", ds_reduction="max").pl.show(ax=ax) + assert len(ax.get_images()) == 1 + + def test_method_matplotlib_matches_default(self): + rng = np.random.default_rng(0) + arr = rng.random((1, 64, 64), dtype=np.float32) + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) + + def _render_and_grab(**kwargs): + fig, ax = plt.subplots(figsize=(2, 2), dpi=50) + try: + sdata.pl.render_images("img", **kwargs).pl.show(ax=ax) + return np.asarray(ax.get_images()[0].get_array()) + finally: + plt.close(fig) + + np.testing.assert_array_equal(_render_and_grab(), _render_and_grab(method="matplotlib")) From e515c7f735c8611f0ab6146d70ec967093d871b5 Mon Sep 17 00:00:00 2001 From: anon Date: Wed, 20 May 2026 18:48:17 +0200 Subject: [PATCH 2/6] Add visual regression tests for datashader image path Five test_plot_* methods covering the high-signal cases for #449: hero sparse-pixel test (default vs datashader=max side-by-side), ds_reduction grid (max/min/mean/mode), multi-channel with per-channel cmap, transfunc+cmap composition, and shapes-on-top composition. Reference PNGs to be sourced from the next CI artifact run. --- tests/pl/test_render_images.py | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/pl/test_render_images.py b/tests/pl/test_render_images.py index 6a555279..b095ae0a 100644 --- a/tests/pl/test_render_images.py +++ b/tests/pl/test_render_images.py @@ -854,3 +854,59 @@ def _render_and_grab(**kwargs): plt.close(fig) np.testing.assert_array_equal(_render_and_grab(), _render_and_grab(method="matplotlib")) + + +class TestRenderImagesDatashaderVisual(PlotTester, metaclass=PlotTesterMeta): + """Visual regression tests for render_images(method='datashader') — #449.""" + + @staticmethod + def _sparse_sdata(n_pixels: int = 50, size: int = 1024, channels: tuple[str, ...] = ("c1",)) -> SpatialData: + rng = np.random.default_rng(0) + arr = np.zeros((len(channels), size, size), dtype=np.float32) + ys = rng.integers(0, size, size=n_pixels) + xs = rng.integers(0, size, size=n_pixels) + for c in range(len(channels)): + arr[c, ys, xs] = 1.0 + return SpatialData(images={"img": Image2DModel.parse(arr, c_coords=list(channels))}) + + def test_plot_datashader_preserves_scattered_sparse_pixels(self): + sdata = self._sparse_sdata(n_pixels=50) + fig, axs = plt.subplots(1, 2, figsize=(8, 4)) + sdata.pl.render_images("img").pl.show(ax=axs[0], colorbar=False) + sdata.pl.render_images("img", method="datashader", ds_reduction="max").pl.show(ax=axs[1], colorbar=False) + axs[0].set_title("default (mean)") + axs[1].set_title("datashader (max)") + + def test_plot_datashader_reduction_grid(self): + arr = np.zeros((1, 1024, 1024), dtype=np.float32) + arr[0, ::32, :] = 1.0 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) + fig, axs = plt.subplots(2, 2, figsize=(8, 8)) + for ax, red in zip(axs.flat, ("max", "min", "mean", "mode"), strict=True): + sdata.pl.render_images("img", method="datashader", ds_reduction=red).pl.show(ax=ax, colorbar=False) + ax.set_title(red) + + def test_plot_datashader_multichannel_with_per_channel_cmap(self): + sdata = self._sparse_sdata(n_pixels=30, channels=("c1", "c2", "c3")) + fig, ax = plt.subplots() + sdata.pl.render_images( + "img", method="datashader", ds_reduction="max", cmap=["Reds", "Greens", "Blues"] + ).pl.show(ax=ax, colorbar=False) + + def test_plot_datashader_with_transfunc_log1p(self): + arr = np.zeros((1, 1024, 1024), dtype=np.float32) + arr[0, 200, 200] = 1.0 + arr[0, 800, 800] = 0.001 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) + fig, ax = plt.subplots() + sdata.pl.render_images( + "img", method="datashader", ds_reduction="max", transfunc=np.log1p, cmap="viridis" + ).pl.show(ax=ax) + + def test_plot_datashader_composes_with_shapes(self, sdata_blobs: SpatialData): + fig, ax = plt.subplots() + ( + sdata_blobs.pl.render_images("blobs_image", method="datashader", ds_reduction="max") + .pl.render_shapes("blobs_circles") + .pl.show(ax=ax) + ) From 913bc5a3c39190c230e288811b2cae849ea8906f Mon Sep 17 00:00:00 2001 From: anon Date: Wed, 20 May 2026 18:55:29 +0200 Subject: [PATCH 3/6] Fold datashader visual tests into existing TestImages class Drop the standalone TestRenderImagesDatashaderVisual class and its sparse-data helper; add two test_plot_* methods directly to TestImages. Keeps the two high-signal cases (hero side-by-side + reduction grid) and drops the multichannel-cmap, transfunc, and shapes-overlay panels whose code paths are already locked by non-visual tests. --- tests/pl/test_render_images.py | 78 ++++++++++------------------------ 1 file changed, 22 insertions(+), 56 deletions(-) diff --git a/tests/pl/test_render_images.py b/tests/pl/test_render_images.py index b095ae0a..4658ec2e 100644 --- a/tests/pl/test_render_images.py +++ b/tests/pl/test_render_images.py @@ -162,6 +162,28 @@ def test_plot_constant_channel_renders_as_midgrey(self): sdata = SpatialData(images={"img": img}) sdata.pl.render_images("img").pl.show(title="constant channel: mid-value (not black)") + def test_plot_method_datashader_preserves_sparse_pixels(self): + # #449: bright pixels in a sparse image must survive the downsample step. + arr = np.zeros((1, 1024, 1024), dtype=np.float32) + rng = np.random.default_rng(0) + arr[0, rng.integers(0, 1024, 50), rng.integers(0, 1024, 50)] = 1.0 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) + fig, axs = plt.subplots(1, 2, figsize=(8, 4)) + sdata.pl.render_images("img").pl.show(ax=axs[0], colorbar=False, title="default (mean)") + sdata.pl.render_images("img", method="datashader", ds_reduction="max").pl.show( + ax=axs[1], colorbar=False, title="datashader (max)" + ) + + def test_plot_method_datashader_reduction_grid(self): + arr = np.zeros((1, 1024, 1024), dtype=np.float32) + arr[0, ::32, :] = 1.0 + sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) + fig, axs = plt.subplots(2, 2, figsize=(8, 8)) + for ax, red in zip(axs.flat, ("max", "min", "mean", "mode"), strict=True): + sdata.pl.render_images("img", method="datashader", ds_reduction=red).pl.show( + ax=ax, colorbar=False, title=red + ) + # --------------------------------------------------------------------------- # Grayscale + transfunc visual tests @@ -854,59 +876,3 @@ def _render_and_grab(**kwargs): plt.close(fig) np.testing.assert_array_equal(_render_and_grab(), _render_and_grab(method="matplotlib")) - - -class TestRenderImagesDatashaderVisual(PlotTester, metaclass=PlotTesterMeta): - """Visual regression tests for render_images(method='datashader') — #449.""" - - @staticmethod - def _sparse_sdata(n_pixels: int = 50, size: int = 1024, channels: tuple[str, ...] = ("c1",)) -> SpatialData: - rng = np.random.default_rng(0) - arr = np.zeros((len(channels), size, size), dtype=np.float32) - ys = rng.integers(0, size, size=n_pixels) - xs = rng.integers(0, size, size=n_pixels) - for c in range(len(channels)): - arr[c, ys, xs] = 1.0 - return SpatialData(images={"img": Image2DModel.parse(arr, c_coords=list(channels))}) - - def test_plot_datashader_preserves_scattered_sparse_pixels(self): - sdata = self._sparse_sdata(n_pixels=50) - fig, axs = plt.subplots(1, 2, figsize=(8, 4)) - sdata.pl.render_images("img").pl.show(ax=axs[0], colorbar=False) - sdata.pl.render_images("img", method="datashader", ds_reduction="max").pl.show(ax=axs[1], colorbar=False) - axs[0].set_title("default (mean)") - axs[1].set_title("datashader (max)") - - def test_plot_datashader_reduction_grid(self): - arr = np.zeros((1, 1024, 1024), dtype=np.float32) - arr[0, ::32, :] = 1.0 - sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) - fig, axs = plt.subplots(2, 2, figsize=(8, 8)) - for ax, red in zip(axs.flat, ("max", "min", "mean", "mode"), strict=True): - sdata.pl.render_images("img", method="datashader", ds_reduction=red).pl.show(ax=ax, colorbar=False) - ax.set_title(red) - - def test_plot_datashader_multichannel_with_per_channel_cmap(self): - sdata = self._sparse_sdata(n_pixels=30, channels=("c1", "c2", "c3")) - fig, ax = plt.subplots() - sdata.pl.render_images( - "img", method="datashader", ds_reduction="max", cmap=["Reds", "Greens", "Blues"] - ).pl.show(ax=ax, colorbar=False) - - def test_plot_datashader_with_transfunc_log1p(self): - arr = np.zeros((1, 1024, 1024), dtype=np.float32) - arr[0, 200, 200] = 1.0 - arr[0, 800, 800] = 0.001 - sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) - fig, ax = plt.subplots() - sdata.pl.render_images( - "img", method="datashader", ds_reduction="max", transfunc=np.log1p, cmap="viridis" - ).pl.show(ax=ax) - - def test_plot_datashader_composes_with_shapes(self, sdata_blobs: SpatialData): - fig, ax = plt.subplots() - ( - sdata_blobs.pl.render_images("blobs_image", method="datashader", ds_reduction="max") - .pl.render_shapes("blobs_circles") - .pl.show(ax=ax) - ) From 09ac6697c3b577a51b4b5c75e23150d99eca8e3a Mon Sep 17 00:00:00 2001 From: anon Date: Wed, 20 May 2026 19:07:35 +0200 Subject: [PATCH 4/6] Add baseline images for datashader visual tests --- ...ethod_datashader_preserves_sparse_pixels.png | Bin 0 -> 18557 bytes .../Images_method_datashader_reduction_grid.png | Bin 0 -> 27811 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/_images/Images_method_datashader_preserves_sparse_pixels.png create mode 100644 tests/_images/Images_method_datashader_reduction_grid.png diff --git a/tests/_images/Images_method_datashader_preserves_sparse_pixels.png b/tests/_images/Images_method_datashader_preserves_sparse_pixels.png new file mode 100644 index 0000000000000000000000000000000000000000..3333600dc0d3c0912b86c839a68c45adf3f99698 GIT binary patch literal 18557 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 HS@8b=-`qkk literal 0 HcmV?d00001 diff --git a/tests/_images/Images_method_datashader_reduction_grid.png b/tests/_images/Images_method_datashader_reduction_grid.png new file mode 100644 index 0000000000000000000000000000000000000000..74849eaab8d361d55d4bb1847f3911379c21ed28 GIT binary patch literal 27811 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 literal 0 HcmV?d00001 From f5d13251291b76bfb42ce1cf54d471cc003aaad1 Mon Sep 17 00:00:00 2001 From: anon Date: Wed, 20 May 2026 19:29:37 +0200 Subject: [PATCH 5/6] Address review: rename to datashader_reduction, sharpen visual test - Rename render_images public kwarg ds_reduction -> datashader_reduction to match the existing render_points/render_shapes API; ImageRenderParams field name stays (matches the points/shapes internal field name). - Bind get_args(_ImageDsReduction) once in the validation block. - Mark the spatialdata.rasterize geometry-only call as a TODO for a future cheaper path. - Redesign the reduction-grid visual test: mid-grey background + sparse bright pixels so max/min/mean/mode produce visibly distinct panels instead of nearly-identical stripes. Reference PNG to be regenerated from the next CI artifact run. --- src/spatialdata_plot/pl/basic.py | 24 ++++++----- src/spatialdata_plot/pl/utils.py | 3 ++ ...mages_method_datashader_reduction_grid.png | Bin 27811 -> 0 bytes tests/pl/test_render_images.py | 40 +++++++++++------- 4 files changed, 40 insertions(+), 27 deletions(-) delete mode 100644 tests/_images/Images_method_datashader_reduction_grid.png diff --git a/src/spatialdata_plot/pl/basic.py b/src/spatialdata_plot/pl/basic.py index dcedaa0b..ab38b312 100644 --- a/src/spatialdata_plot/pl/basic.py +++ b/src/spatialdata_plot/pl/basic.py @@ -539,7 +539,7 @@ def render_images( colorbar_params: dict[str, object] | None = None, channels_as_legend: bool = False, method: Literal["matplotlib", "datashader"] | None = None, - ds_reduction: _ImageDsReduction | None = None, + datashader_reduction: _ImageDsReduction | None = None, ) -> sd.SpatialData: """ Render image elements in SpatialData. @@ -624,14 +624,14 @@ def render_images( Whether to use ``'matplotlib'`` (default) or ``'datashader'`` for the downsampling step. When ``'datashader'`` is selected, the rasterization-to-canvas step uses - :meth:`datashader.Canvas.raster` with ``ds_reduction`` as the + :meth:`datashader.Canvas.raster` with ``datashader_reduction`` as the downsample method (default ``'max'``), and ``imshow`` is rendered with ``interpolation='nearest'`` so the chosen reduction is not re-smoothed at display time. Useful for very sparse images (mostly zeros) where mean aggregation collapses the signal — - ``method='datashader'`` with ``ds_reduction='max'`` preserves the + ``method='datashader'`` with ``datashader_reduction='max'`` preserves the rare non-zero pixels (``plt.spy``-style). - ds_reduction : {"max", "min", "mean", "mode", "first", "last", "var", "std"} | None, optional + datashader_reduction : {"max", "min", "mean", "mode", "first", "last", "var", "std"} | None, optional Downsample reduction used by the datashader path. Defaults to ``'max'`` when ``method='datashader'``. Ignored otherwise (a warning is emitted if set without ``method='datashader'``). @@ -658,14 +658,16 @@ def render_images( raise TypeError("Parameter 'method' must be a string.") if method is not None and method not in ("matplotlib", "datashader"): raise ValueError("Parameter 'method' must be either 'matplotlib' or 'datashader'.") - if ds_reduction is not None and not isinstance(ds_reduction, str): - raise TypeError("Parameter 'ds_reduction' must be a string.") - if ds_reduction is not None and ds_reduction not in get_args(_ImageDsReduction): + _valid_image_reductions = get_args(_ImageDsReduction) + if datashader_reduction is not None and not isinstance(datashader_reduction, str): + raise TypeError("Parameter 'datashader_reduction' must be a string.") + if datashader_reduction is not None and datashader_reduction not in _valid_image_reductions: raise ValueError( - f"Parameter 'ds_reduction' must be one of {get_args(_ImageDsReduction)}, got {ds_reduction!r}." + f"Parameter 'datashader_reduction' must be one of {_valid_image_reductions}, " + f"got {datashader_reduction!r}." ) - if ds_reduction is not None and method != "datashader": - logger.warning("Parameter 'ds_reduction' has no effect unless method='datashader'; ignoring.") + if datashader_reduction is not None and method != "datashader": + logger.warning("Parameter 'datashader_reduction' has no effect unless method='datashader'; ignoring.") params_dict = _validate_image_render_params( self._sdata, @@ -733,7 +735,7 @@ def render_images( grayscale=grayscale, channels_as_legend=channels_as_legend, method=method, - ds_reduction=ds_reduction, + ds_reduction=datashader_reduction, ) n_steps += 1 diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 0324d311..ea2dd421 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -2075,6 +2075,9 @@ def _rasterize_if_necessary_datashader( # spatialdata.rasterize is invoked solely to inherit the output coords and # spatial transformation; its mean-aggregated values are overwritten below. + # TODO: this wastes a full per-channel resample pass. A future refactor can + # construct the target DataArray + transformation directly once spatialdata + # exposes a public geometry-only helper. world_x = float(extent["x"][1]) - float(extent["x"][0]) world_y = float(extent["y"][1]) - float(extent["y"][0]) target_unit_to_pixels = min(target_y_dims / world_y, target_x_dims / world_x) diff --git a/tests/_images/Images_method_datashader_reduction_grid.png b/tests/_images/Images_method_datashader_reduction_grid.png deleted file mode 100644 index 74849eaab8d361d55d4bb1847f3911379c21ed28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27811 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 diff --git a/tests/pl/test_render_images.py b/tests/pl/test_render_images.py index 4658ec2e..cf4ef89b 100644 --- a/tests/pl/test_render_images.py +++ b/tests/pl/test_render_images.py @@ -170,17 +170,21 @@ def test_plot_method_datashader_preserves_sparse_pixels(self): sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) fig, axs = plt.subplots(1, 2, figsize=(8, 4)) sdata.pl.render_images("img").pl.show(ax=axs[0], colorbar=False, title="default (mean)") - sdata.pl.render_images("img", method="datashader", ds_reduction="max").pl.show( + sdata.pl.render_images("img", method="datashader", datashader_reduction="max").pl.show( ax=axs[1], colorbar=False, title="datashader (max)" ) def test_plot_method_datashader_reduction_grid(self): - arr = np.zeros((1, 1024, 1024), dtype=np.float32) - arr[0, ::32, :] = 1.0 + # Mid-grey background with sparse bright pixels: each reduction yields a + # visibly distinct panel — max preserves spots, min/mode show the + # background only, mean shows a slightly-lifted background. + rng = np.random.default_rng(0) + arr = np.full((1, 1024, 1024), 0.3, dtype=np.float32) + arr[0, rng.integers(0, 1024, 50), rng.integers(0, 1024, 50)] = 1.0 sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) fig, axs = plt.subplots(2, 2, figsize=(8, 8)) for ax, red in zip(axs.flat, ("max", "min", "mean", "mode"), strict=True): - sdata.pl.render_images("img", method="datashader", ds_reduction=red).pl.show( + sdata.pl.render_images("img", method="datashader", datashader_reduction=red).pl.show( ax=ax, colorbar=False, title=red ) @@ -785,7 +789,7 @@ def _render_sparse_image_max(**kwargs) -> float: def test_render_images_datashader_preserves_sparse_max(): # Regression test for #449. default_max = _render_sparse_image_max() - datashader_max = _render_sparse_image_max(method="datashader", ds_reduction="max") + datashader_max = _render_sparse_image_max(method="datashader", datashader_reduction="max") assert default_max < 0.1, f"default path should collapse sparse signal, got max={default_max}" assert datashader_max == pytest.approx(1.0, abs=1e-6), ( f"datashader should preserve sparse signal at 1.0, got {datashader_max}" @@ -808,18 +812,18 @@ def test_method_invalid_value_raises(self, sdata_blobs: SpatialData): with pytest.raises(ValueError, match="matplotlib.*datashader"): sdata_blobs.pl.render_images("blobs_image", method="bogus") - def test_ds_reduction_invalid_type_raises(self, sdata_blobs: SpatialData): + def test_datashader_reduction_invalid_type_raises(self, sdata_blobs: SpatialData): with pytest.raises(TypeError, match="must be a string"): - sdata_blobs.pl.render_images("blobs_image", ds_reduction=42) # type: ignore[arg-type] + sdata_blobs.pl.render_images("blobs_image", datashader_reduction=42) # type: ignore[arg-type] - def test_ds_reduction_invalid_value_raises(self, sdata_blobs: SpatialData): - with pytest.raises(ValueError, match="ds_reduction"): - sdata_blobs.pl.render_images("blobs_image", method="datashader", ds_reduction="bogus") + def test_datashader_reduction_invalid_value_raises(self, sdata_blobs: SpatialData): + with pytest.raises(ValueError, match="datashader_reduction"): + sdata_blobs.pl.render_images("blobs_image", method="datashader", datashader_reduction="bogus") - def test_ds_reduction_without_datashader_warns(self, sdata_blobs: SpatialData, caplog): - with logger_warns(caplog, logger, match="ds_reduction"): + def test_datashader_reduction_without_datashader_warns(self, sdata_blobs: SpatialData, caplog): + with logger_warns(caplog, logger, match="datashader_reduction"): _, ax = plt.subplots() - sdata_blobs.pl.render_images("blobs_image", ds_reduction="max").pl.show(ax=ax) + sdata_blobs.pl.render_images("blobs_image", datashader_reduction="max").pl.show(ax=ax) def test_datashader_basic_renders_single_image(self): arr = np.zeros((1, 512, 512), dtype=np.float32) @@ -836,7 +840,7 @@ def test_datashader_multichannel(self): arr[2, 300, 300] = 1.0 sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1", "c2", "c3"])}) _, ax = plt.subplots(figsize=(2, 2), dpi=50) - sdata.pl.render_images("img", method="datashader", ds_reduction="max").pl.show(ax=ax) + sdata.pl.render_images("img", method="datashader", datashader_reduction="max").pl.show(ax=ax) assert len(ax.get_images()) == 1 def test_datashader_rgb_passthrough(self): @@ -854,12 +858,16 @@ def test_datashader_with_transfunc(self): arr[0, 100, 100] = 1.0 sdata = SpatialData(images={"img": Image2DModel.parse(arr, c_coords=["c1"])}) _, ax = plt.subplots(figsize=(2, 2), dpi=50) - sdata.pl.render_images("img", method="datashader", ds_reduction="max", transfunc=np.log1p).pl.show(ax=ax) + sdata.pl.render_images("img", method="datashader", datashader_reduction="max", transfunc=np.log1p).pl.show( + ax=ax + ) assert len(ax.get_images()) == 1 def test_datashader_with_multiscale(self, sdata_blobs: SpatialData): _, ax = plt.subplots() - sdata_blobs.pl.render_images("blobs_multiscale_image", method="datashader", ds_reduction="max").pl.show(ax=ax) + sdata_blobs.pl.render_images("blobs_multiscale_image", method="datashader", datashader_reduction="max").pl.show( + ax=ax + ) assert len(ax.get_images()) == 1 def test_method_matplotlib_matches_default(self): From ee32ffaca5f5c819746c3f685c20c81cf406ae00 Mon Sep 17 00:00:00 2001 From: anon Date: Wed, 20 May 2026 19:38:27 +0200 Subject: [PATCH 6/6] Update baseline for redesigned reduction-grid visual test --- .../Images_method_datashader_reduction_grid.png | Bin 0 -> 25633 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/_images/Images_method_datashader_reduction_grid.png diff --git a/tests/_images/Images_method_datashader_reduction_grid.png b/tests/_images/Images_method_datashader_reduction_grid.png new file mode 100644 index 0000000000000000000000000000000000000000..f12bd7bcbf6fb624b5f06d0ecb8e7cfee0f4ada6 GIT binary patch literal 25633 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 literal 0 HcmV?d00001