spatialdata-plot icon indicating copy to clipboard operation
spatialdata-plot copied to clipboard

Document how to perform on-the-fly normalization for channels.

Open LucaMarconato opened this issue 6 months ago • 1 comments

Original comment from @MeyerBender

  • normalization of channels: see differences between the two notebooks when plotting all channels but DAPI. I guess it is intentional that no normalization is performed by default, and it should be possible to normalize channels using the “norm” argument, but the documentation isn’t very clear on how to do this properly.

The comment refers to the different between making plot with spatialproteomics and making plot with spatialdata-plot.

In spatialmuon we had a preprocessing: Callable argument. Maybe we need something like this here instead of norm? @timtreis

LucaMarconato avatar May 13 '25 22:05 LucaMarconato

Example code snippet:

import spatialdata as sd
import spatialdata_plot
from spatialdata.datasets import blobs
from matplotlib.colors import Normalize

sdata_blobs = blobs()
# making the first (red) channel have a different range (0-0.05) than the other two (0-1)
img = sdata_blobs.images['blobs_image'].values.copy()
img[0] /= 20
image = sd.models.Image2DModel.parse(
    img, transformations=None, dims=("c", "x", "y"), c_coords=[0, 1, 2]
)
sdata = sd.SpatialData(images={"image": image})

fig, ax = plt.subplots(1, 3)
sdata_blobs.pl.render_images(channel=[0, 1, 2]).pl.show(ax=ax[0])
sdata.pl.render_images(channel=[0, 1, 2]).pl.show(ax=ax[1])

# global normalization
sdata.pl.render_images(channel=[0, 1, 2], norm=Normalize(vmin=img.min(), vmax=img.max())).pl.show(ax=ax[2])

# TODO: what about channel-wise normalization?
Image

It would be nice to have the option to normalize so that the final plot for sdata looks the same as for sdata_blobs.

MeyerBender avatar May 14 '25 07:05 MeyerBender

I would also be very much interested in normalization per channel. Protein data is super noisy and has vastly different ranges.

This could maybe be done by supplying a dictionary of channel names and norms? I would also like to have an option to specify percentiles a la ("p20","p98") per channel similar to scanpy.pl.umap()

I will try to see if I can get this to work

pakiessling avatar Jul 26 '25 19:07 pakiessling

Would something like this help for your usecase?

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize

# pick channels and compute per-channel percentiles from the data
channels = [0, 1, 2]
arr = sdata.images["image"]  # xarray DataArray with dims ("c", "x", "y")

norms = []
for c in channels:
    a = arr.sel(c=c).values
    lo = np.nanpercentile(a, 2)
    hi = np.nanpercentile(a, 98)
    norms.append(Normalize(vmin=lo, vmax=hi, clip=True))

sdata.pl.render_images(channel=channels, norm=norms, cmap=[plt.cm.gray]*len(channels)).pl.show()

I think this should work, it's just less elegant since the order of the normalisation objects is only implicitly aligned with the order of the channels, not by name or anything like that

timtreis avatar Jul 28 '25 18:07 timtreis

Thanks @timtreis ,

however this does not seem to be currently supported:


import matplotlib.pyplot as plt
from matplotlib.colors import Normalize
from spatialdata.datasets import blobs
import spatialdata_plot
spatialdata_plot.__version__
>'0.2.10'

sdata = blobs()

# pick channels and compute per-channel percentiles from the data
channels = [0, 1, 2]

arr = sdata.images["blobs_image"]  # xarray DataArray with dims ("c", "x", "y")
norms = []
for c in channels:
    a = arr.sel(c=c).values
    lo = np.nanpercentile(a, 2)
    hi = np.nanpercentile(a, 98)
    norms.append(Normalize(vmin=lo, vmax=hi, clip=True))

sdata.pl.render_images(channel=channels, norm=norms, cmap=[plt.cm.gray] * len(channels)).pl.show()


--> 524 params_dict = _validate_image_render_params(
    525     self._sdata,
    526     element=element,
    527     channel=channel,
    528     alpha=alpha,
    529     palette=palette,
...
-> 1621         raise TypeError("Parameter 'norm' must be of type Normalize.")
   1622     if element_type in ["shapes", "points"] and not isinstance(norm, bool | Normalize):
   1623         raise TypeError("Parameter 'norm' must be a boolean or a mpl.Normalize.")

TypeError: Parameter 'norm' must be of type Normalize.

pakiessling avatar Aug 04 '25 11:08 pakiessling