feat(compositing): add reusable alpha "over" array primitive - #333
Open
MAfarrag wants to merge 9 commits into
Open
feat(compositing): add reusable alpha "over" array primitive#333MAfarrag wants to merge 9 commits into
MAfarrag wants to merge 9 commits into
Conversation
Add `cleopatra.glyphs.base.compositing.alpha_over`, a NumPy-only Porter-Duff "over" operator with the two edge cases that make private copies drift -- un-premultiplying the RGBA blend and guarding the divide-by-zero where the output alpha is zero -- so the leaf render package owns the formula everything else composites through. - channel-last (H, W, C): an RGB background gives a 3-band result, an RGBA background a 4-band one; this matches cleopatra's matplotlib/PIL image layout rather than the issue's band-first (C, H, W) sketch, and lets the watermark halo delegate directly (band-first callers transpose at their own boundary) - replace the private watermark _alpha_over copy with the shared primitive and drop its forward-referencing TODO - validate array shapes, naming the offending array in the error Closes #306
Add scenarios that line/branch coverage does not capture: alpha_over leaves its inputs unmutated, coerces array-like (list) inputs through np.asarray, and an opaque foreground over an RGBA canvas reports full coverage on the RGBA branch's alpha extreme.
Coerce inputs to floating point without forcing float64: a float32 foreground/background now composites to float32 instead of doubling the working set to float64, which matters for a primitive meant to be reused across the render stack on large rasters. Only non-floating inputs (int, bool) are promoted, to float64. Documents the dtype behaviour in Returns and pins it with a float32 test.
The purity test only exercised the RGBA-background branch; add the matching check for the 3-band RGB path so both code paths are pinned as non-mutating.
- correct the RGB-branch formula, which named an undefined `bg_rgb`; the code multiplies the whole `background` array on that path - document that the module must stay NumPy-only to keep `styling` and `glyphs` free of an import cycle - state that the [0, 1] input range is a trusted, unvalidated precondition the divide-by-zero guard relies on - make the shape-error doctest robust to NumPy's tuple repr via +ELLIPSIS
Bring the touched files to the repo's pre-commit-canonical state (ruff 0.15.22 format + check --fix): - wrap long assert messages in test_compositing.py to the 88-col ruler - drop now-unnecessary quotes from watermark's annotations (the module already carries `from __future__ import annotations`) No behavioural change; assertion values and code paths are untouched.
NumPy's arithmetic operators are typed as returning Any, so the RGB branch tripped mypy's no-any-return while the RGBA branch (via np.concatenate) was already clean. Bind the blend to an ndarray-annotated local before returning so both branches type-check; mypy is clean on the module.
The Returns note said only "integer inputs are promoted"; boolean inputs are promoted to float64 too. Also reword "keep their precision" to "keep their own width -- never upcast", so a float16 pair is understood to blend at float16 precision rather than reading as a promise of more.
Add explicit cases for the dtype-coercion behaviour the docstring now describes -- float16 preserved, mixed float32/float64 resolving to float64, bool promoted to float64 -- and for a zero-sized (0, 5, 4) image compositing to an empty result without raising.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Description
Adds
cleopatra.glyphs.base.compositing.alpha_over, a NumPy-only Porter-Duff "over" compositing primitive, so theleaf render package owns the alpha-"over" formula that higher layers (
pyramids-eo,digital-earth) currentlyre-derive privately. The operator carries the two edge cases those copies get wrong: un-premultiplying the RGBA
blend, and guarding the divide-by-zero where the output alpha is zero.
Arrays are channel-last
(H, W, C)— matplotlib's and Pillow's image layout, and cleopatra's throughout:(H, W, 3)→ 3-band resultfg_rgb * fg_a + bg * (1 - fg_a)(no alpha channel).(H, W, 4)→ 4-band result without_a = fg_a + bg_a * (1 - fg_a), un-premultiplied anddivide-by-zero guarded.
Deviation from the issue's sketch: the issue proposed a band-first
(4, H, W)/(3, H, W)signature. Thisimplements channel-last instead, because that is cleopatra's native image convention and it lets the existing
watermark halo delegate directly. Band-first callers (the GDAL/rasterio convention) transpose at their own
boundary, e.g.
np.moveaxis(arr, 0, -1).Real in-repo consumer:
styling/watermark.pyheld a private_alpha_overcopy of this exact operator (addedin #314) with a TODO to adopt the centralised version once it landed. This PR deletes that copy and points the halo
compositing at the shared
alpha_over, so there is one implementation rather than two.No new dependencies and no change to the public API surface beyond the new submodule, which is imported directly
(
from cleopatra.glyphs.base.compositing import alpha_over), matching the package convention that the rootsre-export nothing.
Issues
Type of change
Check relevant points.
How Has This Been Tested?
New
tests/test_compositing.pycovers the RGB-over and RGBA-over paths, opaque/transparent foreground,per-pixel alpha broadcast, integer inputs, the un-premultiplied partial-over-partial blend, the divide-by-zero
guard (including a mixed covered/uncovered image), and every shape/ndim validation branch.
pytest tests/test_compositing.py tests/test_watermark.py --cov=cleopatra.glyphs.base.compositing --cov-branch→ 100% line + branch on the new module; the watermark tests are unchanged and green, proving thedelegation is behavior-preserving.
pytest --doctest-modules src/cleopatra/glyphs/base/compositing.py→ doctests pass.pytest -m "not e2e"→ 2717 passed.Checklist: