scverse / scverse/spatialdata-plot
Feature: PCA aggregation for n >= 4 channels in render_images
Personne n'a encore pris cette issue.
- #451 par @timtreis — fermée sans fusion
- Langage dominant
- Python
- Étoiles
- 86
- Forks
- 21
- Merge moyen
- 14 h 50 min
- PR mergées (30 j)
- 3
Description
Problem
For images with n ≥ 4 channels, render_images falls back to an additive-RGB "stack" strategy (render.py:1597–1630): each channel gets a categorical seed color, channels are mapped through cmap → black linear maps, then summed and clipped to [0, 1].
This works for n ≤ 3 (it's the standard fluorescence-microscopy idiom) but degrades sharply at n ≥ 4:
- Additive blending saturates fast — dense co-expression regions collapse to white, and individual-channel signal becomes hard to attribute.
- Seed colors are picked by channel index, not by data variation — uninformative channels claim equal visual real estate alongside the structural ones.
- For multiplexed assays (CODEX, IMC, MERFISH/Xenium morphology, CycIF) with 4–60 channels, the result is essentially unreadable — see #534 for a real user reproduction (Xenium Explorer-style multi-stain panel).
The in-code TODO at render.py:1630 ("update when pca is added as strategy") points at the same gap.
What is not the gap
Per-channel norm support (a list of Normalize objects, one per channel) is already in main — see tests/pl/test_render_images.py:474. That fixes the dynamic-range half of #534 but does not address muddy composition: a recent verification on a synthetic 4-channel Xenium-morphology stand-in found that tuning per-channel norm to each peak reduced saturation but the additive overlap still produces blob-soup. The two problems are independent; this issue tracks only the compositing half.
Proposed solution: PCA reduction strategy
Add a multichannel_strategy: Literal[\"stack\", \"pca\"] | None = None kwarg to render_images. Default to \"stack\" (today's behavior) when n_channels ≤ 3; default to \"pca\" when n_channels ≥ 4. Log the chosen strategy (one line, like the existing stack log).
Algorithm for \"pca\":
- Stack
(c, y, x)→(c, h·w)after per-channel norm has been applied (so the reduction sees normalized intensities, not raw counts). - Run
sklearn.decomposition.PCA(n_components=3, random_state=0)on the transposed matrix. - Reshape
(h·w, 3)→(3, y, x)and rescale each component to[0, 1]independently. - Stack as RGB and render through the existing 3-channel path.
The 3-component cap is intentional: PCA → RGB is the standard multiplex-visualization recipe (used by napari plugins, MCMICRO, Steinbock). For interpretability, log the explained-variance ratio per component.
API sketch
sdata.pl.render_images(
\"morphology_focus\",
channel=[\"DAPI\", \"ATP1A1/CD45/E-Cadherin\", \"18S\", \"AlphaSMA/Vimentin\"],
norm=[Normalize(0, p, clip=True) for p in (5000, 8000, 50000, 3000)],
multichannel_strategy=\"pca\", # explicit; default auto-switches at n≥4
).pl.show()
Edge cases
- Interaction with
cmap/palette: PCA produces 3 abstract components without per-channel identity, so per-channelcmapandpalettelists do not apply. Either silently ignore with a warning, or error on the combination. Lean: warn + ignore. - Channel legend:
channels_as_legend=Truecannot map back to source channels under PCA. Either ignore with a warning or instead emit a small bar of explained-variance ratios. - Dask-backed sources: PCA needs a materialized matrix. Compute once after rasterization / multiscale-best-scale selection so we work on the canvas-size array, not the raw source.
- NaN propagation: error early (consistent with current
render_imagesNaN policy). - Reproducibility:
random_state=0and sign-normalize each component (deterministic sign by max-abs convention) so the rendered colors are stable across runs. - Fewer than 3 channels:
\"pca\"is meaningless; raise. transfuncinteraction: applied before PCA, same as it runs before the existing rasterize/composite.- n_components < 3 (rank-deficient input): zero-pad missing components so the RGB stack still has 3 channels.
Out of scope
- UMAP/t-SNE/ICA alternatives — defer to follow-up issues if requested.
-
3-component visualizations (channel grouping, small-multiples). Track separately.
- The render_shapes/render_labels analog (multi-column blending) — tracked in #677.
References
- TODO comment in code:
src/spatialdata_plot/pl/render.py:1630 - User report: #534 (Xenium morphology multi-stain panel; remaining muddiness after per-channel norm)
- Related (closed): #460 (per-channel norm documentation — landed)
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Piste de recherche
Commencez dans src/spatialdata_plot/pl/render.py, au niveau de la stratégie de stack existante et du TODO autour des lignes 1597–1630, puis examinez tests/pl/test_render_images.py autour de la ligne 474 pour le comportement de normalisation. Suivez les interactions entre la rastérisation, la normalisation et le compositing avant de décider comment les cas limites de PCA listés s'intègrent à l'API. La tâche est terminée lorsque la stratégie, le logging, les cas limites et la couverture de régression décrits dans l'issue sont implémentés et que les tests render_images passent.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- python, scikit-learn
- Domaine
- data-visualization
- Type d'issue
- Fonctionnalité
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Activité
- À l'abandon
- Clarté
- Plutôt claire
- Accessibilité débutants
- 38/100