Skip to content

Fix multi-channel compositing and add per-channel norm support#451

Draft
timtreis wants to merge 11 commits intomainfrom
bugfix/issue450-feature-pca-aggregation-for-n-4-channels-in-render_images
Draft

Fix multi-channel compositing and add per-channel norm support#451
timtreis wants to merge 11 commits intomainfrom
bugfix/issue450-feature-pca-aggregation-for-n-4-channels-in-render_images

Conversation

@timtreis
Copy link
Member

@timtreis timtreis commented Apr 22, 2025

Summary

  • Signal-based multi-channel compositing: replaces 4 inconsistent compositing paths in _render_images with a shared _additive_blend helper. For each channel, the "signal" (deviation from cmap(0)) is summed onto a canvas colored by the mean zero-value. This handles both Napari-style additive blending (black-to-color LUTs) and ImageJ Composite Invert (white-to-color LUTs), and produces reasonable results for arbitrary colormaps.
  • Per-channel normalization: render_images(norm=...) now accepts a list of Normalize objects for independent contrast control per channel (e.g. bright DAPI vs dim cytokine stain).
  • Removes dead percentiles_for_norm field and its deprecation alias.

Bug fixes

Bug Fix
Averaging instead of additive sum in multi-cmap paths Signal-based blend via _additive_blend
Missing clip to [0, 1] in 2-3 channel path np.clip in _additive_blend
Double-alpha application in 4+ channel path (baked into RGBA and passed to imshow) Alpha only via imshow
Redundant isinstance(c, str) filter in palette path Removed (validated upstream)
White saturation with non-black-based cmaps (e.g. Reds, seismic) Signal subtraction handles any cmap zero-value
na_color alpha compared via internal hex string Uses Color.get_alpha_as_float() public API

Per-channel norm usage

from matplotlib.colors import Normalize

sdata.pl.render_images(
    "morphology_focus",
    palette=["#0f73e6", "#a4a400", "#f300a5", "#008a00"],
    channel=["DAPI", "18S", "ATP1A1/CD45/E-Cadherin", "AlphaSMA/Vimentin"],
    norm=[Normalize(0, 5000), Normalize(0, 2000), Normalize(0, 8000), Normalize(0, 3000)],
).pl.show()

Test plan

  • 5 existing baseline images updated (signal-based output differs from averaging)
  • 4 new visual tests with CI-generated baselines (5-channel compositing, per-channel norm)
  • 3 error tests for norm validation (length mismatch, empty list, tuple rejection)

Closes #416, #534. Addresses #370. Partially addresses #460 (per-channel norm, but not arbitrary preprocessing callables).

Note: #450 (PCA aggregation) is not addressed by this PR.

@timtreis timtreis linked an issue Apr 22, 2025 that may be closed by this pull request
@timtreis
Copy link
Member Author

@Sonja-Stockhaus did we discuss this at one point? I've been ignoring that warning for a long time because it's thrown my matplotlib and doesn't fail

@timtreis

This comment was marked as outdated.

@timtreis timtreis marked this pull request as draft April 23, 2025 12:30
@Sonja-Stockhaus
Copy link
Collaborator

@Sonja-Stockhaus did we discuss this at one point? I've been ignoring that warning for a long time because it's thrown my matplotlib and doesn't fail

Nope. I also don't remember ever trying to render sth with 3+ channels

@codecov-commenter
Copy link

codecov-commenter commented May 11, 2025

Codecov Report

❌ Patch coverage is 88.37209% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.50%. Comparing base (2893343) to head (11dbd79).

Files with missing lines Patch % Lines
src/spatialdata_plot/pl/utils.py 62.50% 1 Missing and 2 partials ⚠️
src/spatialdata_plot/pl/render.py 93.75% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #451      +/-   ##
==========================================
+ Coverage   73.68%   74.50%   +0.82%     
==========================================
  Files           9        9              
  Lines        2744     2750       +6     
  Branches      651      656       +5     
==========================================
+ Hits         2022     2049      +27     
+ Misses        447      419      -28     
- Partials      275      282       +7     
Files with missing lines Coverage Δ
src/spatialdata_plot/pl/basic.py 82.29% <100.00%> (+0.06%) ⬆️
src/spatialdata_plot/pl/render_params.py 86.22% <100.00%> (ø)
src/spatialdata_plot/pl/render.py 86.44% <93.75%> (+2.44%) ⬆️
src/spatialdata_plot/pl/utils.py 65.71% <62.50%> (+0.41%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timtreis timtreis changed the title Feature: PCA aggregation for n >= 4 channels in render_images Fix multi-channel handling May 11, 2025
…nel norm

Replace inconsistent compositing formulas in _render_images with a shared
_additive_blend helper that implements standard additive blending with
clamping, matching Napari/ImageJ/FIJI behavior.

Fixes:
- Averaging bug in paths 2A-cmap and 2D (divided by n_channels)
- Missing clip in paths 2B (2-3ch) and 2C
- Double-alpha bug in path 2B (4+ch)
- Redundant isinstance(c, str) filter in path 2C

Adds per-channel normalization support: render_images() now accepts
norm as a list of Normalize objects (one per channel), enabling
proper contrast control for multi-channel fluorescence data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@timtreis timtreis force-pushed the bugfix/issue450-feature-pca-aggregation-for-n-4-channels-in-render_images branch from 2e991c5 to 92cb5eb Compare March 19, 2026 14:24
@timtreis timtreis changed the title Fix multi-channel handling Fix multi-channel compositing and add per-channel norm support Mar 19, 2026
timtreis and others added 10 commits March 19, 2026 15:40
The `percentiles_for_norm` field on `ImageRenderParams` and its
`quantiles_for_norm` deprecation alias were never wired into the
rendering pipeline. Per-channel normalization is now handled via
the `norm` parameter accepting a list of Normalize objects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updates 5 baseline images that changed due to the switch from
averaging to additive blending in multi-channel image compositing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The naive additive blend (sum RGB + clip) only works with black-to-color
LUTs.  For white-to-color cmaps (e.g. Reds, Greens) it saturates to
white; for diverging cmaps (e.g. seismic) it washes out.

Signal-based blending subtracts each cmap's zero-value before summing,
then composites onto a canvas colored by the mean zero-value.  This
reduces to standard additive for black-to-color LUTs (Napari/ImageJ)
and to invert-add-invert for white-to-color LUTs (ImageJ Composite
Invert), while producing reasonable results for arbitrary cmaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 5-channel auto-color composite (path 2B, n>3): untested path
- 5-channel fluorescence palette (DAPI + 4 markers): path 2C with n>3
- Per-channel norm list: new feature, independent contrast per channel
- Single-element norm list: verifies list[Normalize] works for 1 channel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reuse pre-computed zero_colors array instead of calling cmap(0) twice
- Add tests for norm list length mismatch and empty norm list
- Remove unconditional multi-cmap warning (signal-based blend handles it)
- Clarify norms field naming with inline comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace stringly-typed `_na.alpha == "00"` with the public accessor
`_na.get_alpha_as_float() == 0.0` in all three groups-filtering guards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ards

- Reject tuple/sequence norms in _type_check_params with a clear
  TypeError pointing users to use a list instead
- Extract na_is_transparent local in all three groups-filtering guards
  (shapes, points, labels) for consistent style
- Add test_norm_tuple_raises

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updates 5 existing baselines affected by the signal-based blending
change and adds 4 new baselines for per-channel normalization and
5-channel compositing tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@LucaMarconato
Copy link
Member

Could this PR help with #534?

@timtreis
Copy link
Member Author

Could this PR help with #534?

I think so 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: PCA aggregation for n >= 4 channels in render_images Clipping warning when plotting images

4 participants