Multi-channel image: uniform (constant) channels render silently black
Unexpressed markers, background channels, and synthetic test images all hit this.
Environment
spatialdata-plot 0.3.4.dev (main, commit 5cfedc7)
spatialdata 0.5.0
- Python 3.13
Problem
When render_images composites multiple channels, each channel is individually normalized via matplotlib.colors.Normalize(vmin=None, vmax=None). For a channel with a constant value (min == max), this normalization computes (v - min) / (max - min) = 0/0, which numpy masks to 0. The channel contributes no color to the composite and renders completely black. No warning is emitted.
# render.py ~line 1425
layers[ch] = ch_norm(layers[ch]) # ch_norm = Normalize(vmin=None, vmax=None)
# result: all-zero array when layer is constant, no warning
This is inconsistent with the single-channel path, which passes the image directly to ax.imshow() and allows matplotlib to handle the constant case gracefully.
Practical impact: In fluorescence spatial omics data, channels representing unexpressed markers or tissue autofluorescence are frequently constant (or near-constant) within a region of interest. These silently vanish in composite overlays. Users see a dark image and have no diagnostic pointing to the actual cause.
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
import warnings
# Two-channel image where both channels have a constant (non-zero) value
stacked = np.stack([
np.full((20, 20), 200, dtype=np.uint8), # "DAPI" channel, all 200
np.full((20, 20), 150, dtype=np.uint8), # "GFP" channel, all 150
])
image = Image2DModel.parse(stacked, dims=["c","y","x"], c_coords=["DAPI","GFP"])
sdata = sd.SpatialData(images={"img": image})
fig, ax = plt.subplots()
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
sdata.pl.render_images("img").pl.show(ax=ax)
user_warns = [str(w.message) for w in caught if issubclass(w.category, UserWarning)]
arr = np.array(ax.images[0].get_array())
print(f"Image max value: {arr.max()}") # 0 — entirely black
print(f"Warnings emitted: {user_warns}") # []
Expected behaviour
- Constant channel → emit a
UserWarning such as "Channel 'DAPI' has a constant value (200); it will appear black in the composite. Pass explicit norm= or vmin/vmax to control its display."
- Ideally: map the constant value to a mid-grey (0.5) so the channel contributes a visible baseline, matching single-channel behaviour.
Actual behaviour
Image max value: 0
Warnings emitted: []
The composite image is entirely black. No diagnostic is produced.
Fix sketch
After ch_norm(layers[ch]), check whether the channel had zero variance:
ch_data = layers[ch]
if ch_data.min() == ch_data.max():
logger.warning(
f"Channel '{ch}' has a constant value ({ch_data.min()!r}); "
"it will appear black in the composite. "
"Pass an explicit norm= or vmin/vmax to control its display."
)
layers[ch] = np.full_like(ch_data, 0.5) # visible grey instead of black
The zero-variance check before normalization is the minimal guard. The grey fallback is optional but matches the single-channel path's behaviour more closely.
Related
finding_multichannel_uniform_channel_black_no_warning.md
Triage tier: Tier 1
Multi-channel image: uniform (constant) channels render silently black
Unexpressed markers, background channels, and synthetic test images all hit this.
Environment
spatialdata-plot0.3.4.dev(main, commit5cfedc7)spatialdata0.5.0Problem
When
render_imagescomposites multiple channels, each channel is individually normalized viamatplotlib.colors.Normalize(vmin=None, vmax=None). For a channel with a constant value (min == max), this normalization computes(v - min) / (max - min) = 0/0, which numpy masks to 0. The channel contributes no color to the composite and renders completely black. No warning is emitted.This is inconsistent with the single-channel path, which passes the image directly to
ax.imshow()and allows matplotlib to handle the constant case gracefully.Practical impact: In fluorescence spatial omics data, channels representing unexpressed markers or tissue autofluorescence are frequently constant (or near-constant) within a region of interest. These silently vanish in composite overlays. Users see a dark image and have no diagnostic pointing to the actual cause.
Minimal reproducible example
Expected behaviour
UserWarningsuch as"Channel 'DAPI' has a constant value (200); it will appear black in the composite. Pass explicit norm= or vmin/vmax to control its display."Actual behaviour
The composite image is entirely black. No diagnostic is produced.
Fix sketch
After
ch_norm(layers[ch]), check whether the channel had zero variance:The zero-variance check before normalization is the minimal guard. The grey fallback is optional but matches the single-channel path's behaviour more closely.
Related
finding_multichannel_uniform_channel_black_no_warning.mdTriage tier: Tier 1