Skip to content

feat(compositing): add reusable alpha "over" array primitive (alpha_over) #306

Description

@MAfarrag

Context

cleopatra is the ecosystem's render engine, but there is currently no shared primitive for alpha compositing
— stacking a semi-transparent image over another (the Porter-Duff "over" operator). As a result the same small
formula is being written independently in higher layers:

  • pyramids-eo implements it privately in composites/overlay.py (alpha_overlay) for its day/night composite.
  • digital-earth assembles RGB/HSV composites (scene/maps/raster.py) and will need the same "over" math.

These copies will drift over time (edge cases: RGBA-over-RGBA premultiplication, divide-by-zero on zero output
alpha). Since "over" is a pure drawing operation with no GIS/EO knowledge, cleopatra — the leaf render package
everything else depends on — is its natural home.

Problem / Current Behaviour

cleopatra has no array-level compositing helper. glyphs/base/ holds glyph, animation, and hillshade, but
nothing for combining two image arrays by alpha. Downstream packages therefore reinvent it.

Affected locations

File Symbol Notes
src/cleopatra/glyphs/base/ new compositing.py module goes here
src/cleopatra/glyphs/base/__init__.py module docstring list the new submodule

Steps to Reproduce / Motivation Example

# Desired: one shared primitive any glyph or downstream compositor can call.
import numpy as np
from cleopatra.glyphs.base.compositing import alpha_over

fg = np.array([[[1.0]], [[0.0]], [[0.0]], [[0.5]]])  # red, alpha 0.5  -> (4, H, W)
bg = np.array([[[0.0]], [[0.0]], [[1.0]]])           # blue            -> (3, H, W)
alpha_over(fg, bg).round(2).ravel().tolist()
# [0.5, 0.0, 0.5]   # 50/50 red-over-blue

Proposed Solution

Add src/cleopatra/glyphs/base/compositing.py with a NumPy-only alpha_over:

def alpha_over(foreground: np.ndarray, background: np.ndarray) -> np.ndarray:
    """Composite a (4, H, W) RGBA foreground over a (3|4, H, W) RGB(A) background (Porter-Duff "over").

    - RGB background  -> 3-band result: fg_rgb * fg_a + bg_rgb * (1 - fg_a)
    - RGBA background -> 4-band result with out_a = fg_a + bg_a * (1 - fg_a),
      premultiplied blend, divide-by-zero guarded where out_a == 0.
    """

Import convention follows the package (no re-export at package roots):
from cleopatra.glyphs.base.compositing import alpha_over.

Once this lands, pyramids-eo and digital-earth can delegate their array-level "over" to it (pyramids-eo keeps
only the thin Dataset-metadata wrapper, which cleopatra cannot host since it has no pyramids dependency).

Out of Scope

  • Any pyramids Dataset / geotransform-aware wrapper (stays in pyramids-eo).
  • Other blend modes (multiply, screen, etc.) — this issue is only the "over" operator.
  • Refactoring digital-earth / pyramids-eo to consume it (follow-up in those repos).

Effort Estimate

Size: XS
Rationale: one small pure-NumPy function plus a focused unit test; no dependencies or API-surface changes.

Definition of Done

  • cleopatra.glyphs.base.compositing.alpha_over implemented (RGB and RGBA background paths)
  • divide-by-zero guarded where output alpha is 0; input validation on array shapes
  • unit tests cover RGB-over, RGBA-over, half-transparent blend, and shape-error cases
  • glyphs/base/__init__.py docstring lists the new submodule
  • all existing tests continue to pass

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions