Rendering Pipeline#

This page documents how WISER turns a loaded raster dataset into the pixels you see on screen: how display bands are chosen, how contrast stretches are built and applied, how results are cached, and how all of this is driven by RasterView. It is the entry point for three closely related subsystems, each documented in depth on its own page:

  • Band Chooser — selecting which dataset band(s) map to red, green, blue, or grayscale.

  • Stretch Builder — building and applying the per-channel contrast stretches that map raw values to display brightness.

  • Data Caching — the three-tier cache that avoids recomputing band data, rendered images, and histograms.

For how RasterView fits into the larger display system (panes, zoom, linked scrolling, coordinate systems), see Viewport System. This page covers only the data → image path.


Overview#

RasterView owns the rendering pipeline. Its core method, update_display_image() (src/wiser/gui/rasterview.py), is the single place where a RasterDataSet, a set of display bands, a list of stretches, and an optional colormap are combined into the QPixmap shown on screen.

The three inputs are held as attributes on RasterView and changed through setters that invalidate cached state and re-render:

Attribute

Set by

Source subsystem

_display_bands

set_display_bands()

Band Chooser

_stretches

set_stretches()

Stretch Builder

_colormap

set_display_bands()

Band Chooser

Everything else — normalized band data, the final image, and histograms used by the Stretch Builder — flows through the shared data cache.


The Pipeline, End to End#

        flowchart TD
    DS["RasterDataSet"]
    BANDS["_display_bands<br/>(1 or 3 indices)"]
    NORM["get_band_data_normalized()<br/>float values in [0, 1]"]
    JOINT{"Joint stretch?<br/>(_detect_joint_stretch)"}
    PERBAND["Per-band path:<br/>make_channel_image()<br/>conditioner then stretch"]
    JOINTPATH["Joint path:<br/>_render_joint_channels()<br/>apply_multi() on (H,W,3)"]
    U8["per-channel uint8<br/>(0..255)"]
    RGB["make_rgb_image() /<br/>make_grayscale_image()<br/>packed uint32 ARGB"]
    QIMG["QImage / QPixmap<br/>(_image_pixmap)"]

    CC[("Computation cache<br/>normalized bands")]
    RC[("Render cache<br/>final uint32 image")]

    DS --> NORM
    BANDS --> NORM
    NORM -.->|lookup / store| CC
    NORM --> JOINT
    JOINT -->|no| PERBAND
    JOINT -->|yes| JOINTPATH
    PERBAND --> U8
    JOINTPATH --> U8
    U8 --> RGB
    RGB -.->|lookup / store| RC
    RGB --> QIMG
    

Stage-by-stage#

Stage

Responsible code

Cache used

Check for a fully-rendered image

update_display_image()

Render cache

Read + normalize each band to [0, 1]

RasterDataSet.get_band_data_normalized() (src/wiser/raster/dataset.py)

Computation cache

Decide per-band vs joint stretch

RasterView._detect_joint_stretch()

Apply stretch, scale to uint8

make_channel_image() / _render_joint_channels()

Joint result cache (in-view)

Pack channels into ARGB uint32

make_rgb_image() / make_grayscale_image()

Store rendered image

update_display_image()

Render cache

Wrap as QImageQPixmap

update_display_image()

The render cache is checked first: if a uint32 image already exists for the exact (dataset, bands, stretches, colormap) combination, all of the intermediate work is skipped. See Data Caching for the cache keys and an important known issue affecting whether these lookups currently hit.


Per-band vs Joint Stretches#

Most stretches operate on one band at a time (StretchBase.apply() mutates a 2-D array in place). A few — notably the decorrelation stretch — are joint: they must see all three display bands together. RasterView distinguishes the two cases with _detect_joint_stretch(), which inspects each channel’s stretch and returns a joint stretch only if every channel agrees on the same one (otherwise it logs a warning and falls back to per-band).

  • Per-band path — for each dirty channel, make_channel_image() applies the optional conditioner then the main stretch, clips to [0, 1], and scales to uint8.

  • Joint path_render_joint_channels() gathers all bands into one (H, W, 3) float32 buffer, applies per-band conditioners, runs apply_multi() once, then clips and scales each channel. The joint result is memoized in RasterView._joint_render_cache so re-renders that don’t change the bands or conditioners can skip the expensive transform.

See Stretch Builder for the stretch model and the conditioner/stretch composition that this detection relies on.


How Changes Reach the Screen#

The Band Chooser and Stretch Builder are dialogs; they never touch RasterView directly. Changes flow through ApplicationState (the state holder) and the App class (the signal broker), then down to every RasterView showing the affected dataset. This mirrors the broker pattern described in Viewport System.

        sequenceDiagram
    participant SB as StretchBuilderDialog
    participant App as App (broker)
    participant AS as ApplicationState
    participant RP as RasterPane
    participant RV as RasterView

    Note over SB,RV: Stretch changed
    SB->>App: stretch_changed(ds_id, bands, stretches)
    App->>AS: set_stretches(ds_id, bands, stretches)
    AS->>RP: stretch_changed(ds_id, bands)
    RP->>AS: get_stretches(ds_id, bands)
    RP->>RV: set_stretches(stretches)
    RV->>RV: update_display_image()

    Note over SB,RV: Bands / colormap changed (global)
    Note right of RP: BandChooserDialog returns, RasterPane emits display_bands_change
    RP->>App: display_bands_change(ds_id, bands, colormap, is_global)
    App->>RP: set_display_bands() on every pane
    RP->>RV: set_display_bands(bands, stretches, colormap)
    RV->>RV: update_display_image()
    

Stretch change pathStretchBuilderDialog.stretch_changedApp._on_stretch_changedApplicationState.set_stretches (stores the stretch per (ds_id, band_index) and re-emits stretch_changed) → RasterPane._on_stretch_changedRasterView.set_stretches.

Band/colormap change pathBandChooserDialog returns from exec_()RasterPane._on_band_chooser emits display_bands_changeApp._on_display_bands_change calls set_display_bands on all panes (when global) → RasterView.set_display_bands. A non-global change is applied only to the originating pane.

Both setters null out RasterView._joint_render_cache and call update_display_image(), so the next paint reflects the change.