Source code for uwsift.view.rgb_config

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""UI objects for configuring RGB layers."""
import logging
import uuid
from functools import partial
from typing import Optional, Tuple

from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QDoubleValidator
from PyQt5.QtWidgets import QComboBox, QLineEdit

from uwsift.common import DEFAULT_GAMMA_VALUE, Info, Kind
from uwsift.model.composite_recipes import RGBA2IDX, CompositeRecipe
from uwsift.model.layer_item import LayerItem
from uwsift.model.layer_model import LayerModel

LOG = logging.getLogger(__name__)


[docs] class RGBLayerConfigPane(QObject): """Configures RGB channel selection and ranges on behalf of document. Document in turn generates update signals which cause the SceneGraph to refresh. """ # Recipe, Channel (RGB), layer uuid, color limits, gamma didChangeRGBInputLayers = pyqtSignal(CompositeRecipe, str, object, tuple, float) # Recipe, Channel (RGB), color limits didChangeRGBColorLimits = pyqtSignal(CompositeRecipe, str, tuple) # Recipe, Channel (RGB), gamma didChangeRGBGamma = pyqtSignal(CompositeRecipe, str, float) didChangeRecipeName = pyqtSignal(CompositeRecipe, str) _rgb = None # combo boxes in r,g,b order; cache _sliders = None # sliders in r,g,b order; cache _edits = None _gamma_boxes = None # tuple of each component's gamma spin boxes def __init__(self, ui, parent, model: LayerModel): super(RGBLayerConfigPane, self).__init__(parent) self.ui = ui self._valid_ranges = [(None, None), (None, None), (None, None)] # tuples of each component's c-limits self._layer_uuids: list = [] self.recipe: Optional[CompositeRecipe] = None self.model = model self._slider_steps = 100 self.ui.slideMinRed.setRange(0, self._slider_steps) self.ui.slideMaxRed.setRange(0, self._slider_steps) self.ui.slideMinGreen.setRange(0, self._slider_steps) self.ui.slideMaxGreen.setRange(0, self._slider_steps) self.ui.slideMinBlue.setRange(0, self._slider_steps) self.ui.slideMaxBlue.setRange(0, self._slider_steps) self._double_validator = qdoba = QDoubleValidator() self.ui.editMinRed.setValidator(qdoba) self.ui.editMinRed.setText("0.0") self.ui.editMaxRed.setValidator(qdoba) self.ui.editMaxRed.setText("0.0") self.ui.editMinGreen.setValidator(qdoba) self.ui.editMinGreen.setText("0.0") self.ui.editMaxGreen.setValidator(qdoba) self.ui.editMaxGreen.setText("0.0") self.ui.editMinBlue.setValidator(qdoba) self.ui.editMinBlue.setText("0.0") self.ui.editMaxBlue.setValidator(qdoba) self.ui.editMaxBlue.setText("0.0") [ x.currentIndexChanged.connect(partial(self._combo_changed, combo=x, color=rgb)) for rgb, x in zip(("b", "g", "r"), (self.ui.comboBlue, self.ui.comboGreen, self.ui.comboRed)) ] [ x.valueChanged.connect(partial(self._slider_changed, slider=x, color=rgb, is_max=False)) for rgb, x in zip(("b", "g", "r"), (self.ui.slideMinBlue, self.ui.slideMinGreen, self.ui.slideMinRed)) ] [ x.valueChanged.connect(partial(self._slider_changed, slider=x, color=rgb, is_max=True)) for rgb, x in zip(("b", "g", "r"), (self.ui.slideMaxBlue, self.ui.slideMaxGreen, self.ui.slideMaxRed)) ] [ x.editingFinished.connect(partial(self._edit_changed, line_edit=x, color=rgb, is_max=False)) for rgb, x in zip(("b", "g", "r"), (self.ui.editMinBlue, self.ui.editMinGreen, self.ui.editMinRed)) ] [ x.editingFinished.connect(partial(self._edit_changed, line_edit=x, color=rgb, is_max=True)) for rgb, x in zip(("b", "g", "r"), (self.ui.editMaxBlue, self.ui.editMaxGreen, self.ui.editMaxRed)) ] [ x.valueChanged.connect(self._gamma_changed) for rgb, x in zip( ("b", "g", "r"), (self.ui.blueGammaSpinBox, self.ui.greenGammaSpinBox, self.ui.redGammaSpinBox) ) ] self.ui.nameEdit.textEdited.connect(self._rgb_name_edit_changed) self._rgb_name_edit = self.ui.nameEdit # initialize the combo boxes self.set_combos_to_layer_names() # disable all UI elements to start self._show_settings_for_layer() @property def rgb(self): if self._rgb is None: self._rgb = [self.ui.comboRed, self.ui.comboGreen, self.ui.comboBlue] return self._rgb else: return self._rgb @property def sliders(self): if self._sliders is None: self._sliders = [ (self.ui.slideMinRed, self.ui.slideMaxRed), (self.ui.slideMinGreen, self.ui.slideMaxGreen), (self.ui.slideMinBlue, self.ui.slideMaxBlue), ] return self._sliders @property def line_edits(self): if self._edits is None: self._edits = [ (self.ui.editMinRed, self.ui.editMaxRed), (self.ui.editMinGreen, self.ui.editMaxGreen), (self.ui.editMinBlue, self.ui.editMaxBlue), ] return self._edits @property def rgb_name_edit(self): return self._rgb_name_edit @property def gamma_boxes(self): if self._gamma_boxes is None: self._gamma_boxes = ( self.ui.redGammaSpinBox, self.ui.greenGammaSpinBox, self.ui.blueGammaSpinBox, ) return self._gamma_boxes
[docs] def layer_added(self, layer: LayerItem): if layer.kind in [Kind.IMAGE, Kind.COMPOSITE]: self._layer_uuids.append(layer.uuid) self.set_combos_to_layer_names()
[docs] def layer_removed(self, layer_uuid): if layer_uuid in self._layer_uuids: idx = self._layer_uuids.index(layer_uuid) del self._layer_uuids[idx] self.set_combos_to_layer_names() self._show_settings_for_layer(self.recipe)
def _gamma_changed(self, value): gamma = tuple(x.value() for x in self.gamma_boxes) recipe_gamma = self.recipe.gammas channels = ["r", "g", "b"] changed_channel = "" for idx in range(len(channels)): if gamma[idx] != recipe_gamma[idx]: changed_channel = channels[idx] self.didChangeRGBGamma.emit(self.recipe, changed_channel, value) def _combo_changed(self, index, combo: QComboBox, color): layer_uuid = combo.itemData(index) if not layer_uuid: # we use None as no-selection value, not empty string layer_uuid = None LOG.debug("RGB: user selected %s for %s" % (repr(layer_uuid), color)) # reset slider position to min and max for layer self._set_minmax_slider(color, layer_uuid) layer = self.model.get_layer_by_uuid(layer_uuid) if not layer: clim = (None, None) else: valid_range = layer.valid_range # TODO: is the actual range really used correctly here? actual_range = layer.get_actual_range_from_layer() assert actual_range != (None, None) # nosec B101 clim = valid_range if valid_range else actual_range self.didChangeRGBInputLayers.emit(self.recipe, color, layer_uuid, clim, DEFAULT_GAMMA_VALUE) self._show_settings_for_layer(self.recipe) def _rgb_name_edit_changed(self, text): self.didChangeRecipeName.emit(self.recipe, text) def _display_to_data(self, color: str, values): """Convert display value to data value.""" if self.recipe is None: # No recipe has been set yet return values layer_uuid = self.recipe.input_layer_ids[RGBA2IDX[color]] if layer_uuid is None: return values layer = self.model.get_layer_by_uuid(layer_uuid) assert layer is not None # nosec B101 # suppress mypy [union-attr] layer_info = layer.info return ( layer_info[Info.UNIT_CONVERSION][1](values, inverse=True) if layer_info.get(Info.UNIT_CONVERSION) else values ) def _data_to_display(self, color: str, values): "convert data value to display value" if self.recipe is None: # No recipe has been set yet return values layer_uuid = self.recipe.input_layer_ids[RGBA2IDX[color]] if layer_uuid is None: return values layer = self.model.get_layer_by_uuid(layer_uuid) assert layer is not None # nosec B101 # suppress mypy [union-attr] layer_info = layer.info return layer_info[Info.UNIT_CONVERSION][1](values) if layer_info.get(Info.UNIT_CONVERSION) else values def _get_slider_value(self, valid_min, valid_max, slider_val): return (slider_val / self._slider_steps) * (valid_max - valid_min) + valid_min def _create_slider_value(self, valid_min, valid_max, channel_val): # return int((channel_val - valid_min) / (valid_max - valid_min)) * self._slider_steps return int((channel_val - valid_min) / (valid_max - valid_min) * self._slider_steps) def _update_line_edits(self, color: str, n: Optional[float] = None, x: Optional[float] = None): """ update edit controls to match non-None values provided if called with just color, returns current min and max implicitly convert data values to and from display values :param color: in 'rgba' :param n: minimum data value or None :param x: max data value or None :return: new min, new max """ idx = RGBA2IDX[color] edn, edx = self.line_edits[idx] edn.blockSignals(True) edx.blockSignals(True) if n is not None: ndis = self._data_to_display(color, n) edn.setText("%f" % ndis) else: ndis = float(edn.text()) n = self._display_to_data(color, ndis) if x is not None: xdis = self._data_to_display(color, x) edx.setText("%f" % xdis) else: xdis = float(edx.text()) x = self._display_to_data(color, xdis) edn.blockSignals(False) edx.blockSignals(False) return n, x def _signal_color_changing_range(self, color: str, n: float, x: float): new_limits = (n, x) self.didChangeRGBColorLimits.emit(self.recipe, color, new_limits) def _slider_changed(self, value, slider, color: str, is_max: bool): """ handle slider update event from user :param slider: control :param color: char in 'rgba' :param is_max: whether slider's value represents the max or the min :return: """ idx = RGBA2IDX[color] valid_min, valid_max = self._valid_ranges[idx] if value is None: value = slider.value() value = self._get_slider_value(valid_min, valid_max, value) LOG.debug("slider %s %s => %f" % (color, "max" if is_max else "min", value)) n, x = self._update_line_edits(color, value if not is_max else None, value if is_max else None) self._signal_color_changing_range(color, n, x) def _edit_changed(self, line_edit: QLineEdit, color: str, is_max: bool): # pragma: no cover """ update relevant slider value, propagate to the document :param line_edit: field that got a new value :param color: in 'rgba' :param is_max: whether the min or max edit field was changed :return: """ idx = RGBA2IDX[color] vn, vx = self._valid_ranges[idx] vdis = float(line_edit.text()) val = self._display_to_data(color, vdis) LOG.debug("line edit %s %s => %f => %f" % (color, "max" if is_max else "min", vdis, val)) sv = self._create_slider_value(vn, vx, val) slider = self.sliders[idx][1 if is_max else 0] slider2 = self.sliders[idx][0 if is_max else 1] current_slider_max = slider.maximum() current_slider_min = slider.minimum() slider.blockSignals(True) slider2.blockSignals(True) if current_slider_max < sv: slider.setMaximum(sv) slider2.setMaximum(sv) if current_slider_min > sv: slider.setMinimum(sv) slider2.setMinimum(sv) slider.setValue(sv) slider.blockSignals(False) slider2.blockSignals(False) self._signal_color_changing_range(color, *self._update_line_edits(color))
[docs] def selection_did_change(self, layers: Tuple[LayerItem]): """Change UI elements to reflect the provided recipe.""" if layers is not None and len(layers) == 1: layer = layers[0] self.recipe = layer.recipe if isinstance(layer.recipe, CompositeRecipe) else None self._show_settings_for_layer(self.recipe)
def _show_settings_for_layer(self, recipe=None): if not isinstance(recipe, CompositeRecipe): self.ui.rgbScrollAreaWidgetContents.setDisabled(True) return else: self.ui.rgbScrollAreaWidgetContents.setDisabled(False) for widget in self.rgb: # block signals so an existing RGB layer doesn't get overwritten with new layer selections widget.blockSignals(True) # update the combo boxes self._select_components_for_recipe(recipe) self._set_minmax_sliders(recipe) self._set_gamma_boxes(recipe) self._set_rgb_name_edit(recipe) for widget in self.rgb: # block signals so an existing RGB layer doesn't get overwritten with new layer selections widget.blockSignals(False) def _set_rgb_name_edit(self, recipe): if recipe is not None: self.rgb_name_edit.setText(recipe.name) else: self.rgb_name_edit.setText("") def _set_minmax_slider(self, color: str, layer_uuid: uuid.UUID, clims: Optional[Tuple[float, float]] = None): idx = RGBA2IDX[color] slider = self.sliders[idx] editn, editx = self.line_edits[idx] if layer_uuid not in self._layer_uuids: LOG.debug("Could not find {} in layer_uuids {}".format(repr(layer_uuid), self._layer_uuids)) # block signals so the changed sliders don't trigger updates slider[0].blockSignals(True) slider[1].blockSignals(True) if clims is None or clims == (None, None) or layer_uuid not in self._layer_uuids: self._valid_ranges[idx] = (None, None) slider[0].setSliderPosition(0) slider[1].setSliderPosition(0) editn.blockSignals(True) editx.blockSignals(True) editn.setText("0.0") editx.setText("0.0") editn.blockSignals(False) editx.blockSignals(False) slider[0].setDisabled(True) slider[1].setDisabled(True) editn.setDisabled(True) editx.setDisabled(True) else: slider[0].setDisabled(False) slider[1].setDisabled(False) editn.setDisabled(False) editx.setDisabled(False) layer = self.model.get_layer_by_uuid(layer_uuid) assert layer is not None # nosec B101 # suppress mypy [union-attr] valid_range = layer.valid_range # TODO: is the actual range really used correctly here? actual_range = layer.get_actual_range_from_layer() assert actual_range != (None, None) # nosec B101 layer_range = valid_range if valid_range else actual_range self._valid_ranges[idx] = layer_range slider_val = self._create_slider_value(layer_range[0], layer_range[1], clims[0]) slider[0].setSliderPosition(max(slider_val, 0)) slider_val = self._create_slider_value(layer_range[0], layer_range[1], clims[1]) slider[1].setSliderPosition(min(slider_val, self._slider_steps)) self._update_line_edits(color, *clims) slider[0].blockSignals(False) slider[1].blockSignals(False) def _set_minmax_sliders(self, recipe): if recipe: for idx, (color, clim) in enumerate(zip("rgb", self.recipe.color_limits)): layer_uuid = self.recipe.input_layer_ids[idx] self._set_minmax_slider(color, layer_uuid, clim) else: self._set_minmax_slider("r", None) self._set_minmax_slider("g", None) self._set_minmax_slider("b", None) def _select_components_for_recipe(self, recipe=None): if recipe is not None: for layer_uuid, widget in zip(recipe.input_layer_ids, self.rgb): if not layer_uuid: widget.setCurrentIndex(0) else: # Qt can't handle item data being tuples dex = widget.findData(layer_uuid) if dex <= 0: widget.setCurrentIndex(0) LOG.error("Layer with uuid '%s' not available to" " be selected" % (layer_uuid,)) else: widget.setCurrentIndex(dex) else: for widget in self.rgb: widget.setCurrentIndex(0)
[docs] def set_combos_to_layer_names(self): """ update combo boxes with the list of layer names and then select the right r,g,b,a layers if they're not None :return: """ # Get the current selected families so we can reselect them when we # rebuild the lists. current_layers = [x.itemData(x.currentIndex()) for x in self.rgb] for widget in self.rgb: # block signals so an existing RGB layer doesn't get overwritten with new layer selections widget.blockSignals(True) # clear out the current lists for widget in self.rgb: widget.clear() widget.addItem("None", None) # fill up our lists of layers for widget, selected_layer_uuid in zip(self.rgb, current_layers): if not selected_layer_uuid or selected_layer_uuid not in self._layer_uuids: # if the selection is None or the current layer was removed # if the current layer was removed by the document then the # document should have updated the recipe widget.setCurrentIndex(0) for idx, layer_uuid in enumerate(self._layer_uuids): layer: LayerItem = self.model.get_layer_by_uuid(layer_uuid) display_name = layer.descriptor widget.addItem(display_name, layer_uuid) widget.findData(uuid) if layer_uuid == selected_layer_uuid: widget.setCurrentIndex(idx + 1) for widget in self.rgb: # block signals so an existing RGB layer doesn't get overwritten with new layer selections widget.blockSignals(False)
def _set_gamma_boxes(self, recipe=None): if recipe is not None: for idx, sbox in enumerate(self.gamma_boxes): sbox.setDisabled(recipe.input_layer_ids[idx] is None) sbox.setValue(recipe.gammas[idx]) else: for sbox in self.gamma_boxes: sbox.setDisabled(True) sbox.setValue(1.0)