Band Chooser#
The Band Chooser is the dialog that lets the user pick which dataset band(s) are displayed and how: three bands for RGB, or one band for grayscale (optionally colorized with a matplotlib colormap). This page covers the dialog itself, the automatic band-selection helpers it relies on, and how a selection propagates into the rendering pipeline.
Overview#
A raster dataset can have anywhere from one to hundreds of bands. The renderer, however, only ever shows 1 band (grayscale) or 3 bands (RGB). The Band Chooser is the UI that maps dataset bands onto those display slots.
GUI:
BandChooserDialog(src/wiser/gui/band_chooser.py)Auto-selection helpers:
find_truecolor_bands()/find_display_bands()(src/wiser/raster/dataset.py)Consumed by:
RasterView.set_display_bands()(src/wiser/gui/rasterview.py)
BandChooserDialog#
File: src/wiser/gui/band_chooser.py
Purpose: A modal QDialog for choosing display bands and colormap for one
dataset. It is purely a chooser — it reads the dataset to populate its controls
and reports the user’s choice back to the caller; it does not apply anything
itself.
Constructor:
BandChooserDialog(
app_state: ApplicationState,
dataset: RasterDataSet,
display_bands: List[int], # current bands: 1-tuple or 3-tuple
colormap: Optional[str] = None,
can_apply_global: bool = True,
parent=None,
)
On construction the dialog:
Populates the red/green/blue/gray band combo boxes from
dataset.band_list(), labeling each entry withdataset.get_band_label(i)(which includes a(bad)marker for bad bands) and right-aligning the text when the datasethas_wavelengths().Fills the colormap combo box from
matplotlib.pyplot.colormaps().Selects RGB or grayscale mode based on the length of
display_bandsand shows the matching page of the config stack.Configures the “choose defaults” / “choose visible-light” buttons based on dataset capabilities (
_configure_buttons()).
Public API (read by the caller after exec_()):
Method |
Returns |
Description |
|---|---|---|
|
|
|
|
|
Colormap name if grayscale + colormap enabled, else |
|
|
True when grayscale mode and the “use colormap” box is checked |
|
|
Whether the change should apply to all panes (the “apply globally” checkbox) |
Controls:
RGB vs grayscale mode toggle and the corresponding band combo boxes.
Colormap selection and a live preview gradient strip (
_on_grayscale_choose_colormap, connected to the combo box’sactivatedsignal, samples the selected colormap at 256 points into a 256×24QImageshown onlbl_colormap_display).The “apply globally” checkbox (disabled when
can_apply_global=False).
Does not control:
Applying the selection (the caller does this — see the Propagation section below).
Stretches or rendering.
Which dataset is shown.
Automatic Band Selection#
Two module-level helpers in src/wiser/raster/dataset.py decide
sensible defaults so the user rarely has to choose bands manually.
find_truecolor_bands(dataset, red, green, blue)#
Returns a (red_index, green_index, blue_index) triple of the bands closest to
the given visible-light wavelengths, or None if the dataset has no wavelength
metadata or a match cannot be found for any channel. The Band Chooser calls this
via _get_truecolor_bands(), passing the configured red/green/blue wavelengths
from app config (general.red_wavelength_nm, etc.).
find_display_bands(dataset, red, green, blue)#
Picks the bands to display when a dataset is first shown, in priority order:
The dataset’s own
default_display_bands()(may be a 3-tuple or 1-tuple).Otherwise,
find_truecolor_bands()if wavelengths are available.Otherwise,
(0, 1, 2)if the dataset has ≥ 3 bands, else(0,)(grayscale).
These helpers are also wired to the dialog’s buttons: “choose visible-light
bands” calls find_truecolor_bands(), and “choose defaults” reads
dataset.default_display_bands().
Propagation#
The key thing to understand is that BandChooserDialog is passive: it never
calls into the RasterPane or RasterView, and it changes no application state.
It is a modal data-collection widget. RasterPane._on_band_chooser()
(src/wiser/gui/rasterpane.py) constructs it, blocks on exec_(), and then —
once the user clicks OK — pulls the user’s choices back out of the dialog
with its getters and decides what to do with them.
sequenceDiagram
participant U as User
participant RP as RasterPane
participant BC as BandChooserDialog
participant App as App (broker)
participant RV as RasterView
U->>RP: click "Band Chooser"
RP->>BC: construct + exec_() (modal, blocks)
U->>BC: pick bands / colormap, click OK
Note over BC: passive - only records the choice in its own widgets, no app state
BC-->>RP: exec_() returns QDialog.Accepted
RP->>BC: get_display_bands() / get_colormap_name() / apply_globally()
BC-->>RP: bands, colormap, is_global
alt is_global is True (Apply globally checked)
RP->>App: display_bands_change(ds_id, bands, colormap, is_global)
App->>RP: set_display_bands(ds_id, bands, colormap) on every pane
RP->>RV: set_display_bands(bands, stretches, colormap)
else local (this pane only)
RP->>RV: set_display_bands(bands, colormap=colormap)
end
RV->>RV: update_display_image()
You’re right that the dashed return line is mostly getters — that is the whole point. The dialog holds no application state, so nothing it does affects the pane until the pane reads these values after the modal closes:
Getter |
Returns |
Role |
|---|---|---|
|
new band tuple (1 or 3 indices) |
the new data to apply |
|
colormap name or |
the new data to apply |
|
|
routing decision only — not data |
So apply_globally() is the only value that changes the pane’s behavior
(global vs local); the other two carry the new bands/colormap. The pane then
routes them (_on_band_chooser, src/wiser/gui/rasterpane.py, the
if dialog.exec_() == QDialog.Accepted: block):
Global (
apply_globally()isTrue): emitdisplay_bands_change(ds_id, bands, colormap, is_global).Appreceives it and callsset_display_bandson every pane (context, main, zoom) so they stay in sync.Local: call this pane’s own
set_display_bands(ds_id, bands, colormap)directly, skipping the broadcast.
Both routes converge on RasterPane.set_display_bands(ds_id, bands, colormap),
which loops over the pane’s rasterviews and, for each one showing this dataset,
calls RasterView.set_display_bands(bands, stretches, colormap) — forwarding the
exact bands tuple that BandChooserDialog.get_display_bands() returned. (The
stretches are looked up from ApplicationState by the pane at this point so the
view only re-renders once.)
The same BandChooserDialog is reused elsewhere (e.g.
similarity_transform_pane.py, geo_reference_pane.py, and a plugin-facing
instance held by ApplicationState).
RasterView.set_display_bands()#
File: src/wiser/gui/rasterview.py
This is the method that connects the Band Chooser to an actual render. As traced
above, the display_bands argument it receives is exactly the tuple the user
chose in the dialog (BandChooserDialog.get_display_bands() →
RasterPane.set_display_bands() → here); colormap is
BandChooserDialog.get_colormap_name(), and stretches are the ones the pane
looked up from ApplicationState. The dialog itself is never referenced here —
by the time this runs, its values have already been extracted and passed in.
Its key behavior is a
dirty-flag optimization: it compares the old and new band tuples and builds
an ImageColors flag (RED, GREEN, BLUE, or all) describing which channels
actually changed, so update_display_image() only regenerates those channels.
def set_display_bands(self, display_bands, stretches=None, colormap=None):
# ... validate length is 1 or 3 ...
changed = ImageColors.NONE
# ... set RED/GREEN/BLUE flags for each band index that differs ...
self._display_bands = display_bands
self._colormap = colormap
self._joint_render_cache = None # band selection feeds joint stretch input
self.update_display_image(colors=changed)
ImageColors (src/wiser/gui/rasterview.py) is an IntFlag
with NONE=0, RED=1, GREEN=2, BLUE=4, RGB=7. Switching the number of
display bands (RGB ↔ grayscale) or changing the single grayscale band marks all
channels dirty.
RGB vs grayscale rendering#
RGB (3 bands): each channel is stretched and scaled independently (
make_channel_image), then the threeuint8channels are packed into auint32ARGB image bymake_rgb_image.Grayscale (1 band): the single stretched channel is duplicated across R, G, B, then
make_grayscale_imageeither packs it as gray or, if a colormap is set, maps eachuint8value through a 256-entry colormap lookup table.
For how the stretch portion of this works (per-band vs joint), see the Stretch Builder. For how results are cached, see Data Caching.