Skip to content

Add per-channel norm support for render_images#572

Merged
timtreis merged 9 commits intomainfrom
feature/issue-460-per-channel-norm
Apr 1, 2026
Merged

Add per-channel norm support for render_images#572
timtreis merged 9 commits intomainfrom
feature/issue-460-per-channel-norm

Conversation

@timtreis
Copy link
Copy Markdown
Member

@timtreis timtreis commented Mar 31, 2026

Summary

  • Closes Document how to perform on-the-fly normalization for channels. #460render_images now accepts a list of Normalize objects for per-channel normalization
  • Essential for multi-channel protein/fluorescence data with vastly different intensity ranges per channel
  • The rendering pipeline already read per-channel norms from CmapParams — this change only widens validation and routes per-channel norms into the existing CmapParams creation loop

Changes

  • basic.py: Widen norm type to list[Normalize] | Normalize | None, update docstring, zip norm list with cmap list
  • utils.py: Accept list[Normalize] in validation, reject empty lists and non-Normalize elements
  • test_render_images.py: 5 new tests (per-channel norm, backward compat, length mismatch, empty list, invalid element)

Usage

from matplotlib.colors import Normalize

norms = [
    Normalize(vmin=0, vmax=0.05, clip=True),  # dim channel
    Normalize(vmin=0, vmax=1.0, clip=True),    # full range
    Normalize(vmin=0, vmax=0.5, clip=True),    # medium
]
sdata.pl.render_images(
    channel=[0, 1, 2],
    norm=norms,
    # cmaps are optiopnal
    cmap=[plt.cm.gray] * 3,
).pl.show()

timtreis and others added 3 commits March 31, 2026 13:53
Accept a list of Normalize objects in render_images so each channel
can be normalized independently — essential for multi-channel protein
data with vastly different intensity ranges.

The rendering pipeline already reads per-channel norms from CmapParams,
so this change only widens the input validation and routes per-channel
norms into the existing CmapParams creation loop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s only

- Use resolved per-element cmap for list branching so norm list works
  correctly when cmap is auto-replicated
- Raise ValueError when norm is a list but cmap is not (no silent
  pass-through to _prepare_cmap_norm)
- Restrict list norm validation to element_type="images" only — labels
  has no list-norm support
- Add test for norm list without explicit cmap list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Using the resolved (replicated) cmap for branching broke the scalar
cmap path — e.g. grayscale=True with cmap="viridis" entered the list
branch because the cmap was internally replicated to match channels.

Now only use the resolved cmap when norm is a list (the new feature
path); otherwise preserve the original user-supplied cmap for branching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 77.77778% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.17%. Comparing base (367204d) to head (f12292b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/spatialdata_plot/pl/utils.py 50.00% 1 Missing and 2 partials ⚠️
src/spatialdata_plot/pl/render.py 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #572      +/-   ##
==========================================
- Coverage   75.61%   75.17%   -0.44%     
==========================================
  Files          10       10              
  Lines        2908     2929      +21     
  Branches      672      681       +9     
==========================================
+ Hits         2199     2202       +3     
- Misses        428      445      +17     
- Partials      281      282       +1     
Files with missing lines Coverage Δ
src/spatialdata_plot/pl/basic.py 83.72% <100.00%> (+0.55%) ⬆️
src/spatialdata_plot/pl/render.py 86.69% <0.00%> (-0.17%) ⬇️
src/spatialdata_plot/pl/utils.py 65.32% <50.00%> (-0.85%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

timtreis and others added 6 commits April 1, 2026 12:12
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Improve error message when norm list + cmap resolution fails —
   now mentions cmap/channel mismatch as possible cause instead of
   the misleading "multiple colormaps are used"

2. Unwrap length-1 cmap_params lists to scalar so single-channel
   images still take the fast path (proper norm in imshow + colorbar)

3. Make Normalize copy unconditional in the multi-channel compositing
   loop — eliminates fragile conditional that only copied auto-ranging
   norms, preventing any future cross-channel mutation bugs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…selected

When user specifies `channel=[0]` on a 3-channel image with
`cmap=['gray']`, the cmap matched the selected channel count but was
nullified because it didn't match the full image channel count. Now
the comparison uses the selected channel count (or full count when no
channel is specified).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Simplify error: "When 'norm' is a list, you must also pass a list
  of colormaps via 'cmap' with matching length"
- Add test locking the cmap validation fix: cmap=['gray'] with
  channel=[0] on a 3-channel image should succeed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructure branching so scalar cmap_params is built directly when
there's only one cmap (len <= 1), instead of building a list and
unwrapping after. Also handles norm=[single_Normalize] gracefully
by extracting the scalar. Shortens the effective_cmap comment to
one line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When norm is a list but no cmap is provided, auto-generate a default
cmap list (one per norm) so the per-channel rendering path works.
Users shouldn't need to write cmap=[plt.cm.gray]*3 just to use
per-channel normalization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@timtreis timtreis merged commit e54c74f into main Apr 1, 2026
8 checks passed
@timtreis timtreis deleted the feature/issue-460-per-channel-norm branch April 1, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants