Skip to content

Fix RGBA images rendered with categorical coloring#563

Open
timtreis wants to merge 10 commits intomainfrom
fix/issue-406-rgba-image-rendering
Open

Fix RGBA images rendered with categorical coloring#563
timtreis wants to merge 10 commits intomainfrom
fix/issue-406-rgba-image-rendering

Conversation

@timtreis
Copy link
Copy Markdown
Member

@timtreis timtreis commented Mar 27, 2026

Summary

  • Images with channel names r, g, b (and optionally a) are now detected case-insensitively and rendered as proper RGB(A) instead of falling into the multi-channel compositing path that applies categorical coloring.
  • When the user explicitly sets alpha, it overrides the per-pixel alpha from the data (with an info message). When alpha is default (1.0), the image's own alpha channel is used.
  • Detection is skipped when the user provides palette, a cmap list, or an explicit single cmap, preserving existing behavior for all other code paths.
  • Follows the same detection approach as napari-spatialdata (Change RGB detection to be based on channel names napari-spatialdata#153), but with case-insensitive matching.

timtreis and others added 3 commits March 27, 2026 14:47
Two additional performance fixes on top of the datashader speedups:

1. Replace .assign() + .rename() with direct column assignment when
   attaching the color column to the transformed element. Avoids two
   full DataFrame copies (~320MB saved for 10M points).

2. Add preloaded_color_data parameter to _set_color_source_vec so
   _render_points can pass already-loaded color data from get_values()
   instead of triggering a redundant second load from the table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The default datashader reduction for shapes was "sum", causing overlapping
shapes to inflate the colorbar beyond the true data maximum. Changed the
default to "max" which preserves the actual data range and closely matches
the matplotlib rendering.

Also: extract _default_reduction to prevent log/aggregation drift, add
logger_no_warns test helper, short-circuit _want_decorations for diverse
color vectors.

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

Images with channel names r, g, b (and optionally a) are now detected
case-insensitively and rendered as RGB(A) instead of falling into the
multi-channel compositing path. When the user explicitly sets alpha,
it overrides the per-pixel alpha from the data with an info message.

Detection is skipped when the user provides palette, cmap list, or an
explicit single cmap, preserving existing behavior for all other cases.

Closes #406

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@timtreis timtreis linked an issue Mar 27, 2026 that may be closed by this pull request
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 85.36585% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.05%. Comparing base (f2bff29) to head (994d52e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/spatialdata_plot/_logging.py 71.42% 4 Missing and 2 partials ⚠️
src/spatialdata_plot/pl/render.py 89.83% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #563      +/-   ##
==========================================
+ Coverage   74.05%   75.05%   +0.99%     
==========================================
  Files          10       10              
  Lines        2802     2870      +68     
  Branches      649      662      +13     
==========================================
+ Hits         2075     2154      +79     
+ Misses        453      431      -22     
- Partials      274      285      +11     
Files with missing lines Coverage Δ
src/spatialdata_plot/pl/basic.py 82.22% <100.00%> (ø)
src/spatialdata_plot/pl/render_params.py 86.22% <ø> (-0.07%) ⬇️
src/spatialdata_plot/pl/utils.py 65.91% <100.00%> (+0.32%) ⬆️
src/spatialdata_plot/_logging.py 83.33% <71.42%> (-4.39%) ⬇️
src/spatialdata_plot/pl/render.py 86.02% <89.83%> (+3.33%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

timtreis and others added 7 commits March 27, 2026 20:33
- Normalize uint8/uint16 RGB(A) images to [0, 1] before passing to imshow
- Add length check to _is_rgb_image to prevent false positives with
  duplicate channel names
- Remove .squeeze() calls that could drop spatial dimensions
- Add tests for uint8, uint16, and duplicate channel name edge cases

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

The shared Normalize instance auto-ranges on the first channel, so
subsequent channels with different value ranges can exceed [0, 1].
This causes matplotlib's "Clipping input data to the valid range for
imshow with RGB data" warning.

Added np.clip(array, 0, 1) before _ax_show_and_transform in all
multi-channel compositing paths (2A, 2B, 2C, 2D).

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

The root cause of matplotlib's "Clipping input data" warning was that a
single Normalize instance was shared across all channels. It auto-ranged
on the first channel's min/max, causing subsequent channels with different
value ranges to normalize outside [0, 1].

Fix: when the shared norm has auto-ranging (vmin=None or vmax=None), copy
it per channel so each channel normalizes independently. This replaces
the previous approach of clipping the final array.

Additive compositing paths (palette, categorical) still clip after
summing, since summing RGBA values can inherently exceed [0, 1].

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

- Extract _normalize_dtype_to_float helper to deduplicate dtype
  normalization between RGB and alpha channels
- Warn when user-provided norm is silently ignored for RGB(A) images
- Handle signed integer dtypes safely by clipping negative values after
  division by dtype max

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a user passes norm= with explicit vmin/vmax to an RGB(A) image,
apply it per channel for contrast adjustment. Fall back to dtype-based
normalization only when no explicit norm is provided.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Copy norm in _prepare_cmap_norm so each CmapParams gets an
   independent instance — fixes shared-norm bug when cmap is a list
   and cross-element state sharing in chained render_images calls.

2. Move per-channel auto-ranging norm copy outside if/else so it
   covers both list and single-cmap branches.

3. Global auto-range for float RGB data outside [0,1]: instead of
   clipping (which destroys contrast), scale using global min/max
   across all RGB channels to preserve color balance.

4. Explicit np.clip in Path 2A-default after stacking normalized
   channels, instead of relying on matplotlib's silent clipping.

5. Remove dead percentiles_for_norm field from ImageRenderParams
   and the quantiles_for_norm deprecation alias.

6. Remove unreachable ch_norm-is-not-None guard since
   _prepare_cmap_norm always creates a Normalize.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Log when float RGB data outside [0, 1] triggers auto-ranging
- Document _normalize_dtype_to_float as RGB-display-oriented
- Add test for float RGB outside [0, 1] auto-ranging
- Add test for explicit cmap overriding RGBA detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

Images with alpha channel being plotted incorrectly

2 participants