NaN pixels in multi-channel images silently render black instead of na_color
Environment: spatialdata-plot 0.3.4.dev (main, commit 5cfedc7), Python 3.13
Problem
NaN handling is inconsistent between single-channel and multi-channel image rendering:
- Single-channel: NaN pixels are handled by the colormap (
cmap.set_bad(na_color)), so they display as na_color.
- Multi-channel: NaN pixels are silently converted to 0 by
Normalize()(layer) at render.py:1425 before additive compositing. They appear black, regardless of any na_color setting. No warning is emitted.
NaN values are common in real spatial transcriptomics data: stitched images, padded crops, masked tissue regions, and segmentation-derived images all produce NaN pixels. The inconsistency between one-channel and multi-channel paths means the same data is rendered differently depending on whether the user selects a single channel or all channels.
Minimal reproducible example
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import dask; dask.config.set({"dataframe.query-planning": False})
import spatialdata as sd
from spatialdata.models import Image2DModel
import spatialdata_plot
H, W = 8, 8
gradient = np.linspace(0, 200, H*W).reshape(H, W).astype(np.float32)
nan_gradient = gradient.copy()
nan_gradient[0:3, 0:3] = np.nan
# Single-channel: NaN region shows na_color (handled by colormap set_bad)
img_1ch = Image2DModel.parse(np.stack([nan_gradient]),
dims=["c","y","x"], c_coords=["ch0"])
# Multi-channel: NaN region becomes black, no warning
img_2ch = Image2DModel.parse(np.stack([nan_gradient, gradient]),
dims=["c","y","x"], c_coords=["ch0","ch1"])
for name, sdata in [
("1ch", sd.SpatialData(images={"img": img_1ch})),
("2ch", sd.SpatialData(images={"img": img_2ch})),
]:
fig, ax = plt.subplots()
sdata.pl.render_images("img").pl.show(ax=ax)
arr = np.array(ax.images[0].get_array())
print(f"{name}: NaN_pixel_value={arr[0,0]}")
plt.close(fig)
Expected behaviour
NaN pixels in multi-channel images should either:
- Render with
na_color (matching single-channel behaviour)
- Or emit a
UserWarning noting that NaN values will appear black
Actual behaviour
1ch: NaN_pixel_value=nan ← correct: propagates, colormap handles na_color
2ch: NaN_pixel_value=[0. 0. 0.] ← incorrect: silently black, no warning
No warning is raised in either case.
Fix sketch
In the multi-channel compositing path (render.py:1413–1425):
- Before normalizing, record a combined NaN mask:
nan_mask = np.isnan(layer).any(axis=0)
- After compositing all channels, set pixels where
nan_mask is True to na_color
- Emit
logger.warning(f"Channel data contains NaN pixels; they will appear as na_color in the composite.") if any NaN was found
This matches the existing single-channel behaviour where cmap.set_bad(na_color) handles NaN pixels.
Triage tier: Tier 3
NaN pixels in multi-channel images silently render black instead of
na_colorEnvironment:
spatialdata-plot0.3.4.dev(main, commit5cfedc7), Python 3.13Problem
NaN handling is inconsistent between single-channel and multi-channel image rendering:
cmap.set_bad(na_color)), so they display asna_color.Normalize()(layer)atrender.py:1425before additive compositing. They appear black, regardless of anyna_colorsetting. No warning is emitted.NaN values are common in real spatial transcriptomics data: stitched images, padded crops, masked tissue regions, and segmentation-derived images all produce NaN pixels. The inconsistency between one-channel and multi-channel paths means the same data is rendered differently depending on whether the user selects a single channel or all channels.
Minimal reproducible example
Expected behaviour
NaN pixels in multi-channel images should either:
na_color(matching single-channel behaviour)UserWarningnoting that NaN values will appear blackActual behaviour
No warning is raised in either case.
Fix sketch
In the multi-channel compositing path (
render.py:1413–1425):nan_mask = np.isnan(layer).any(axis=0)nan_maskis True tona_colorlogger.warning(f"Channel data contains NaN pixels; they will appear as na_color in the composite.")if any NaN was foundThis matches the existing single-channel behaviour where
cmap.set_bad(na_color)handles NaN pixels.Triage tier: Tier 3