Example #1
0
    def __init__(self, parent, text, lastDir):
        """

        @param parent:
        @type parent: QObject
        @param text:
        @type text: str
        @param lastDir:
        @type lastDir:str
        """
        # QDialog __init__
        super().__init__()
        self.setWindowTitle(text)
        # File Dialog
        self.dlg = QFileDialog(caption=text, directory=lastDir)
        self.dlg.setOption(QFileDialog.DontUseNativeDialog)
        self.metaOption = QCheckBox('Remove Meta')
        # sliders
        self.sliderComp = QbLUeSlider(Qt.Horizontal)
        self.sliderComp.setTickPosition(QSlider.TicksBelow)
        self.sliderComp.setRange(0, 9)
        self.sliderComp.setSingleStep(1)
        self.sliderComp.setValue(
            3
        )  # 3 = default opencv imwrite value  TODO changed 5 to 3 20/02/20 validate
        self.sliderQual = QbLUeSlider(Qt.Horizontal)
        self.sliderQual.setTickPosition(QSlider.TicksBelow)
        self.sliderQual.setRange(0, 100)
        self.sliderQual.setSingleStep(10)
        self.sliderQual.setValue(
            95
        )  # 95 = default opencv imwrite value  # TODO changed 90 to 95 20/02/20 validate
        self.dlg.setVisible(True)
        l = QVBoxLayout()
        h = QHBoxLayout()
        l.addWidget(self.dlg)
        h.addWidget(self.metaOption)
        h.addWidget(QLabel("Quality"))
        h.addWidget(self.sliderQual)
        h.addWidget(QLabel("Compression"))
        h.addWidget(self.sliderComp)
        l.addLayout(h)
        self.setLayout(l)

        # file dialog close event handler
        def f():
            self.close()

        self.dlg.finished.connect(f)
Example #2
0
    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(layer=layer, targetImage=targetImage, parent=parent)
        # options
        self.options = None
        # exposure slider
        self.sliderExp = QbLUeSlider(Qt.Horizontal)
        self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.sliderExp.setRange(-20, 20)
        self.sliderExp.setSingleStep(1)

        expLabel = QbLUeLabel()
        expLabel.setMaximumSize(150, 30)
        expLabel.setText("Exposure Correction")
        expLabel.doubleClicked.connect(
            lambda: self.sliderExp.setValue(self.defaultExpCorrection))

        self.expValue = QbLUeLabel()
        font = self.expValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("1000 ")
        h = metrics.height()
        self.expValue.setMinimumSize(w, h)
        self.expValue.setMaximumSize(w, h)

        # exp change/released slot
        def f():
            self.expValue.setText(
                str("{:+.1f}".format(self.sliderExp.value() *
                                     self.defaultStep)))
            if self.sliderExp.isSliderDown() or (self.expCorrection
                                                 == self.sliderExp.value() *
                                                 self.defaultStep):
                return
            try:
                self.sliderExp.valueChanged.disconnect()
                self.sliderExp.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.expCorrection = self.sliderExp.value() * self.defaultStep
            self.dataChanged.emit()
            self.sliderExp.valueChanged.connect(f)
            self.sliderExp.sliderReleased.connect(f)

        self.sliderExp.valueChanged.connect(f)
        self.sliderExp.sliderReleased.connect(f)

        # layout
        l = QVBoxLayout()
        l.setAlignment(Qt.AlignTop)
        l.addWidget(expLabel)
        hl = QHBoxLayout()
        hl.addWidget(self.expValue)
        hl.addWidget(self.sliderExp)
        l.addLayout(hl)
        l.setContentsMargins(20, 0, 20, 25)  # left, top, right, bottom
        self.setLayout(l)
        self.adjustSize()
        self.setWhatsThis("""<b>Exposure Correction</b>
                        Multiplicative correction in the linear sRGB color space.<br>
                        Unit is the diaphragm stop.<br>
                        """)  # end setWhatsThis

        self.setDefaults()
    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(layer=layer, targetImage=targetImage, parent=parent)
        # connect layer selectionChanged signal
        self.layer.selectionChanged.sig.connect(self.updateLayer)
        self.kernelCategory = filterIndex.UNSHARP
        # options
        self.optionList = [
            'Unsharp Mask', 'Sharpen', 'Gaussian Blur', 'Surface Blur'
        ]
        filters = [
            filterIndex.UNSHARP, filterIndex.SHARPEN, filterIndex.BLUR1,
            filterIndex.SURFACEBLUR
        ]
        self.filterDict = dict(
            zip(self.optionList,
                filters))  # filters is not a dict: don't use UDict here

        self.listWidget1 = optionsWidget(options=self.optionList,
                                         exclusive=True,
                                         changed=self.dataChanged)
        # set initial selection to unsharp mask
        self.listWidget1.checkOption(self.optionList[0])

        # sliders
        self.sliderRadius = QbLUeSlider(Qt.Horizontal)
        self.sliderRadius.setRange(1, 50)
        self.sliderRadius.setSingleStep(1)
        self.radiusLabel = QLabel()
        self.radiusLabel.setMaximumSize(150, 30)
        self.radiusLabel.setText("Radius")

        self.radiusValue = QLabel()
        font = self.radiusValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("1000 ")
        h = metrics.height()
        self.radiusValue.setMinimumSize(w, h)
        self.radiusValue.setMaximumSize(w, h)

        self.sliderAmount = QbLUeSlider(Qt.Horizontal)
        self.sliderAmount.setRange(0, 100)
        self.sliderAmount.setSingleStep(1)
        self.amountLabel = QLabel()
        self.amountLabel.setMaximumSize(150, 30)
        self.amountLabel.setText("Amount")
        self.amountValue = QLabel()
        font = self.radiusValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("1000 ")
        h = metrics.height()
        self.amountValue.setMinimumSize(w, h)
        self.amountValue.setMaximumSize(w, h)

        self.toneValue = QLabel()
        self.toneLabel = QLabel()
        self.toneLabel.setMaximumSize(150, 30)
        self.toneLabel.setText("Sigma")
        self.sliderTone = QbLUeSlider(Qt.Horizontal)
        self.sliderTone.setRange(0, 100)
        self.sliderTone.setSingleStep(1)
        font = self.radiusValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("1000 ")
        h = metrics.height()
        self.toneValue.setMinimumSize(w, h)
        self.toneValue.setMaximumSize(w, h)

        # value change/done slot
        def formUpdate():
            self.radiusValue.setText(str('%d ' % self.sliderRadius.value()))
            self.amountValue.setText(str('%d ' % self.sliderAmount.value()))
            self.toneValue.setText(str('%d ' % self.sliderTone.value()))
            if self.sliderRadius.isSliderDown(
            ) or self.sliderAmount.isSliderDown(
            ) or self.sliderTone.isSliderDown():
                return
            try:
                for slider in [
                        self.sliderRadius, self.sliderAmount, self.sliderTone
                ]:
                    slider.valueChanged.disconnect()
                    slider.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.tone = self.sliderTone.value()
            self.radius = self.sliderRadius.value()
            self.amount = self.sliderAmount.value()
            self.dataChanged.emit()
            for slider in [
                    self.sliderRadius, self.sliderAmount, self.sliderTone
            ]:
                slider.valueChanged.connect(formUpdate)
                slider.sliderReleased.connect(formUpdate)

        for slider in [self.sliderRadius, self.sliderAmount, self.sliderTone]:
            slider.valueChanged.connect(formUpdate)
            slider.sliderReleased.connect(formUpdate)

        # layout
        l = QVBoxLayout()
        l.addWidget(self.listWidget1)
        hl = QHBoxLayout()
        hl.addWidget(self.radiusLabel)
        hl.addWidget(self.radiusValue)
        hl.addWidget(self.sliderRadius)
        l.addLayout(hl)
        hl = QHBoxLayout()
        hl.addWidget(self.amountLabel)
        hl.addWidget(self.amountValue)
        hl.addWidget(self.sliderAmount)
        l.addLayout(hl)
        hl = QHBoxLayout()
        hl.addWidget(self.toneLabel)
        hl.addWidget(self.toneValue)
        hl.addWidget(self.sliderTone)
        l.addLayout(hl)
        l.setContentsMargins(20, 0, 20, 25)  # left, top, right, bottom
        self.setLayout(l)

        self.setDefaults()
        self.setWhatsThis("""
                       <b>Unsharp Mask</b> and <b>Sharpen Mask</b> are used to sharpen an image.
                       Unsharp Mask usually gives best results.<br>
                       <b>Gaussian Blur</b> and <b>Surface Blur</b> are used to blur an image.<br>
                       In contrast to Gaussian Blur, Surface Blur preserves edges and reduces noise,
                       but it may be slow.<br>
                       It is possible to <b>limit the effect of a filter to a rectangular region of the image</b> by
                       drawing a selection rectangle on the layer with the marquee (rectangle) tool.<br>
                       Ctrl Click <b>clears the selection</b><br>
                       
                    """)  # end setWhatsThis
Example #4
0
    def __init__(self, parent):
        super().__init__(parent)
        self.img = None
        # graphic form to show : it
        # should correspond to the currently selected layer
        self.currentWin = None
        # mouse click event
        self.clicked.connect(self.viewClicked)

        # set behavior and styles
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        delegate = itemDelegate(parent=self)
        self.setItemDelegate(delegate)
        ic1 = QImage(":/images/resources/eye-icon.png")
        ic2 = QImage(":/images/resources/eye-icon-strike.png")
        delegate.px1 = QPixmap.fromImage(ic1)
        delegate.px2 = QPixmap.fromImage(ic2)
        ic1.invertPixels()
        ic2.invertPixels()
        delegate.inv_px1 = QPixmap.fromImage(ic1)
        delegate.inv_px2 = QPixmap.fromImage(ic2)
        self.setIconSize(QSize(20, 15))
        self.verticalHeader().setMinimumSectionSize(-1)
        self.verticalHeader().setDefaultSectionSize(
            self.verticalHeader().minimumSectionSize())
        self.horizontalHeader().setMinimumSectionSize(40)
        self.horizontalHeader().setDefaultSectionSize(40)

        # drag and drop
        self.setDragDropMode(QAbstractItemView.DragDrop)
        self.setDefaultDropAction(Qt.MoveAction)
        self.setDragDropOverwriteMode(False)
        self.setDragEnabled(True)
        self.setAcceptDrops(True)
        self.setDropIndicatorShown(True)

        ################################
        # layer property GUI :
        # preview, blending mode, opacity, mask color
        ################################
        # Preview option
        # We should use a QListWidget or a custom optionsWidget
        # (cf. utils.py) :  adding it to QVBoxLayout with mode
        # Qt.AlignBottom does not work.
        self.previewOptionBox = QCheckBox('Preview')
        self.previewOptionBox.setMaximumSize(100, 30)

        # View/Preview changed slot
        def m(state):  # state : Qt.Checked Qt.UnChecked
            if self.img is None:
                return
            useThumb = (state == Qt.Checked)
            if useThumb == self.img.useThumb:
                return
            self.img.useThumb = useThumb
            window.updateStatus()
            self.img.cacheInvalidate()
            try:
                QApplication.setOverrideCursor(
                    Qt.WaitCursor
                )  # TODO 18/04/18 waitcursor already called by applytostack
                QApplication.processEvents()
                # update the whole stack
                self.img.layersStack[0].applyToStack()
                self.img.onImageChanged()
            finally:
                QApplication.restoreOverrideCursor()
                QApplication.processEvents()

        self.previewOptionBox.stateChanged.connect(m)
        self.previewOptionBox.setChecked(True)  # m is not triggered

        # title
        titleLabel = QLabel('Layer')
        titleLabel.setMaximumSize(100, 30)

        # opacity slider
        self.opacitySlider = QbLUeSlider(Qt.Horizontal)
        self.opacitySlider.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.opacitySlider.setTickPosition(QSlider.TicksBelow)
        self.opacitySlider.setRange(0, 100)
        self.opacitySlider.setSingleStep(1)
        self.opacitySlider.setSliderPosition(100)

        self.opacityValue = QLabel()
        font = self.opacityValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100 ")
        h = metrics.height()
        self.opacityValue.setMinimumSize(w, h)
        self.opacityValue.setMaximumSize(w, h)
        self.opacityValue.setText('100 ')

        # opacity value changed event handler
        def f1():
            self.opacityValue.setText(str('%d ' % self.opacitySlider.value()))

        # opacity slider released event handler
        def f2():
            try:
                layer = self.img.getActiveLayer()
                layer.setOpacity(self.opacitySlider.value())
                layer.applyToStack()
                self.img.onImageChanged()
            except AttributeError:
                return

        self.opacitySlider.valueChanged.connect(f1)
        self.opacitySlider.sliderReleased.connect(f2)

        # mask color slider
        self.maskLabel = QLabel('Mask Color')
        maskSlider = QbLUeSlider(Qt.Horizontal)
        maskSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)
        maskSlider.setTickPosition(QSlider.TicksBelow)
        maskSlider.setRange(0, 100)
        maskSlider.setSingleStep(1)
        maskSlider.setSliderPosition(100)
        self.maskSlider = maskSlider

        self.maskValue = QLabel()
        font = self.maskValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100 ")
        h = metrics.height()
        self.maskValue.setMinimumSize(w, h)
        self.maskValue.setMaximumSize(w, h)
        self.maskValue.setText('100 ')
        # masks are disbled by default
        self.maskLabel.setEnabled(False)
        self.maskSlider.setEnabled(False)
        self.maskValue.setEnabled(False)

        # mask value changed event handler
        def g1():
            self.maskValue.setText(str('%d ' % self.maskSlider.value()))

        # mask slider released event handler
        def g2():
            try:
                layer = self.img.getActiveLayer()
                layer.setColorMaskOpacity(self.maskSlider.value() * 255.0 /
                                          100.0)
                layer.applyToStack()
                self.img.onImageChanged()
            except AttributeError:
                return

        self.maskSlider.valueChanged.connect(g1)
        self.maskSlider.sliderReleased.connect(g2)

        # blending mode combo box
        compLabel = QLabel()
        compLabel.setText("Blend")

        self.compositionModeDict = OrderedDict([
            ('Normal', QPainter.CompositionMode_SourceOver),
            ('Plus', QPainter.CompositionMode_Plus),
            ('Multiply', QPainter.CompositionMode_Multiply),
            ('Screen', QPainter.CompositionMode_Screen),
            ('Overlay', QPainter.CompositionMode_Overlay),
            ('Darken', QPainter.CompositionMode_Darken),
            ('Lighten', QPainter.CompositionMode_Lighten),
            ('Color Dodge', QPainter.CompositionMode_ColorDodge),
            ('Color Burn', QPainter.CompositionMode_ColorBurn),
            ('Hard Light', QPainter.CompositionMode_HardLight),
            ('Soft Light', QPainter.CompositionMode_SoftLight),
            ('Difference', QPainter.CompositionMode_Difference),
            ('Exclusion', QPainter.CompositionMode_Exclusion),
            # Type of previous modes is QPainter.CompositionMode (Shiboken enum-type).
            # Next additional modes are not implemented by QPainter:
            ('Luminosity', -1),
            ('color', -2)
        ])

        self.blendingModeCombo = QComboBox()
        for key in self.compositionModeDict:
            self.blendingModeCombo.addItem(key, self.compositionModeDict[key])

        # combo box item changed slot
        def g(ind):
            layer = self.img.getActiveLayer()
            s = self.blendingModeCombo.currentText()
            newMode = self.compositionModeDict[str(s)]
            if newMode == layer.compositionMode:
                return
            layer.compositionMode = newMode
            layer.applyToStack()
            self.img.onImageChanged()

        self.blendingModeCombo.currentIndexChanged.connect(g)

        # layout
        l = QVBoxLayout()
        l.setAlignment(Qt.AlignTop)
        hl0 = QHBoxLayout()
        hl0.addWidget(titleLabel)
        hl0.addStretch(1)
        hl0.addWidget(self.previewOptionBox)
        l.addLayout(hl0)
        hl = QHBoxLayout()
        hl.addWidget(QLabel('Opacity'))
        hl.addWidget(self.opacityValue)
        hl.addWidget(self.opacitySlider)
        l.addLayout(hl)
        hl1 = QHBoxLayout()
        hl1.addWidget(self.maskLabel)
        hl1.addWidget(self.maskValue)
        hl1.addWidget(self.maskSlider)
        l.addLayout(hl1)
        l.setContentsMargins(0, 0, 10, 0)  # left, top, right, bottom
        hl2 = QHBoxLayout()
        hl2.addWidget(compLabel)
        hl2.addWidget(self.blendingModeCombo)
        l.addLayout(hl2)
        for layout in [hl, hl1, hl2]:
            layout.setContentsMargins(5, 0, 0, 0)
        # this layout must be added to the propertyWidget object loaded from blue.ui :
        # we postpone it to in blue.py, after loading the main form.
        self.propertyLayout = l
        # shortcut actions
        self.actionDup = QAction('Duplicate layer', None)
        self.actionDup.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_J))
        self.addAction(self.actionDup)

        def dup():
            row = self.selectedIndexes()[0].row()
            # Stack index
            index = len(self.img.layersStack) - row - 1
            layer = self.img.layersStack[index]
            if layer.isAdjustLayer():
                return
            # add new layer to stack and set it to active
            self.img.dupLayer(index=index)
            # update layer view
            self.setLayers(self.img)

        self.actionDup.triggered.connect(dup)
        self.setWhatsThis("""<b>Layer Stack</b><br>
To <b>toggle the layer visibility</b> click on the Eye icon.<br>
To <b>add a mask</b> use the context menu to enable it and paint pixels with the Mask/Unmask tools in the left pane.<br>
For <b>color mask<b/b>: <br>
    &nbsp; green pixels are masked,<br>
    &nbsp; red pixels are unmasked.<br>
    &nbsp; Other colors correspond to partially masked pixels.<br>
Note that upper visible layers slow down mask edition.<br>
""")  # end of setWhatsThis
Example #5
0
class QLayerView(QTableView):
    """
    Display the stack of image layers.
    """
    def __init__(self, parent):
        super().__init__(parent)
        self.img = None
        # graphic form to show : it
        # should correspond to the currently selected layer
        self.currentWin = None
        # mouse click event
        self.clicked.connect(self.viewClicked)

        # set behavior and styles
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        delegate = itemDelegate(parent=self)
        self.setItemDelegate(delegate)
        ic1 = QImage(":/images/resources/eye-icon.png")
        ic2 = QImage(":/images/resources/eye-icon-strike.png")
        delegate.px1 = QPixmap.fromImage(ic1)
        delegate.px2 = QPixmap.fromImage(ic2)
        ic1.invertPixels()
        ic2.invertPixels()
        delegate.inv_px1 = QPixmap.fromImage(ic1)
        delegate.inv_px2 = QPixmap.fromImage(ic2)
        self.setIconSize(QSize(20, 15))
        self.verticalHeader().setMinimumSectionSize(-1)
        self.verticalHeader().setDefaultSectionSize(
            self.verticalHeader().minimumSectionSize())
        self.horizontalHeader().setMinimumSectionSize(40)
        self.horizontalHeader().setDefaultSectionSize(40)

        # drag and drop
        self.setDragDropMode(QAbstractItemView.DragDrop)
        self.setDefaultDropAction(Qt.MoveAction)
        self.setDragDropOverwriteMode(False)
        self.setDragEnabled(True)
        self.setAcceptDrops(True)
        self.setDropIndicatorShown(True)

        ################################
        # layer property GUI :
        # preview, blending mode, opacity, mask color
        ################################
        # Preview option
        # We should use a QListWidget or a custom optionsWidget
        # (cf. utils.py) :  adding it to QVBoxLayout with mode
        # Qt.AlignBottom does not work.
        self.previewOptionBox = QCheckBox('Preview')
        self.previewOptionBox.setMaximumSize(100, 30)

        # View/Preview changed slot
        def m(state):  # state : Qt.Checked Qt.UnChecked
            if self.img is None:
                return
            useThumb = (state == Qt.Checked)
            if useThumb == self.img.useThumb:
                return
            self.img.useThumb = useThumb
            window.updateStatus()
            self.img.cacheInvalidate()
            try:
                QApplication.setOverrideCursor(
                    Qt.WaitCursor
                )  # TODO 18/04/18 waitcursor already called by applytostack
                QApplication.processEvents()
                # update the whole stack
                self.img.layersStack[0].applyToStack()
                self.img.onImageChanged()
            finally:
                QApplication.restoreOverrideCursor()
                QApplication.processEvents()

        self.previewOptionBox.stateChanged.connect(m)
        self.previewOptionBox.setChecked(True)  # m is not triggered

        # title
        titleLabel = QLabel('Layer')
        titleLabel.setMaximumSize(100, 30)

        # opacity slider
        self.opacitySlider = QbLUeSlider(Qt.Horizontal)
        self.opacitySlider.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.opacitySlider.setTickPosition(QSlider.TicksBelow)
        self.opacitySlider.setRange(0, 100)
        self.opacitySlider.setSingleStep(1)
        self.opacitySlider.setSliderPosition(100)

        self.opacityValue = QLabel()
        font = self.opacityValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100 ")
        h = metrics.height()
        self.opacityValue.setMinimumSize(w, h)
        self.opacityValue.setMaximumSize(w, h)
        self.opacityValue.setText('100 ')

        # opacity value changed event handler
        def f1():
            self.opacityValue.setText(str('%d ' % self.opacitySlider.value()))

        # opacity slider released event handler
        def f2():
            try:
                layer = self.img.getActiveLayer()
                layer.setOpacity(self.opacitySlider.value())
                layer.applyToStack()
                self.img.onImageChanged()
            except AttributeError:
                return

        self.opacitySlider.valueChanged.connect(f1)
        self.opacitySlider.sliderReleased.connect(f2)

        # mask color slider
        self.maskLabel = QLabel('Mask Color')
        maskSlider = QbLUeSlider(Qt.Horizontal)
        maskSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)
        maskSlider.setTickPosition(QSlider.TicksBelow)
        maskSlider.setRange(0, 100)
        maskSlider.setSingleStep(1)
        maskSlider.setSliderPosition(100)
        self.maskSlider = maskSlider

        self.maskValue = QLabel()
        font = self.maskValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100 ")
        h = metrics.height()
        self.maskValue.setMinimumSize(w, h)
        self.maskValue.setMaximumSize(w, h)
        self.maskValue.setText('100 ')
        # masks are disbled by default
        self.maskLabel.setEnabled(False)
        self.maskSlider.setEnabled(False)
        self.maskValue.setEnabled(False)

        # mask value changed event handler
        def g1():
            self.maskValue.setText(str('%d ' % self.maskSlider.value()))

        # mask slider released event handler
        def g2():
            try:
                layer = self.img.getActiveLayer()
                layer.setColorMaskOpacity(self.maskSlider.value() * 255.0 /
                                          100.0)
                layer.applyToStack()
                self.img.onImageChanged()
            except AttributeError:
                return

        self.maskSlider.valueChanged.connect(g1)
        self.maskSlider.sliderReleased.connect(g2)

        # blending mode combo box
        compLabel = QLabel()
        compLabel.setText("Blend")

        self.compositionModeDict = OrderedDict([
            ('Normal', QPainter.CompositionMode_SourceOver),
            ('Plus', QPainter.CompositionMode_Plus),
            ('Multiply', QPainter.CompositionMode_Multiply),
            ('Screen', QPainter.CompositionMode_Screen),
            ('Overlay', QPainter.CompositionMode_Overlay),
            ('Darken', QPainter.CompositionMode_Darken),
            ('Lighten', QPainter.CompositionMode_Lighten),
            ('Color Dodge', QPainter.CompositionMode_ColorDodge),
            ('Color Burn', QPainter.CompositionMode_ColorBurn),
            ('Hard Light', QPainter.CompositionMode_HardLight),
            ('Soft Light', QPainter.CompositionMode_SoftLight),
            ('Difference', QPainter.CompositionMode_Difference),
            ('Exclusion', QPainter.CompositionMode_Exclusion),
            # Type of previous modes is QPainter.CompositionMode (Shiboken enum-type).
            # Next additional modes are not implemented by QPainter:
            ('Luminosity', -1),
            ('color', -2)
        ])

        self.blendingModeCombo = QComboBox()
        for key in self.compositionModeDict:
            self.blendingModeCombo.addItem(key, self.compositionModeDict[key])

        # combo box item changed slot
        def g(ind):
            layer = self.img.getActiveLayer()
            s = self.blendingModeCombo.currentText()
            newMode = self.compositionModeDict[str(s)]
            if newMode == layer.compositionMode:
                return
            layer.compositionMode = newMode
            layer.applyToStack()
            self.img.onImageChanged()

        self.blendingModeCombo.currentIndexChanged.connect(g)

        # layout
        l = QVBoxLayout()
        l.setAlignment(Qt.AlignTop)
        hl0 = QHBoxLayout()
        hl0.addWidget(titleLabel)
        hl0.addStretch(1)
        hl0.addWidget(self.previewOptionBox)
        l.addLayout(hl0)
        hl = QHBoxLayout()
        hl.addWidget(QLabel('Opacity'))
        hl.addWidget(self.opacityValue)
        hl.addWidget(self.opacitySlider)
        l.addLayout(hl)
        hl1 = QHBoxLayout()
        hl1.addWidget(self.maskLabel)
        hl1.addWidget(self.maskValue)
        hl1.addWidget(self.maskSlider)
        l.addLayout(hl1)
        l.setContentsMargins(0, 0, 10, 0)  # left, top, right, bottom
        hl2 = QHBoxLayout()
        hl2.addWidget(compLabel)
        hl2.addWidget(self.blendingModeCombo)
        l.addLayout(hl2)
        for layout in [hl, hl1, hl2]:
            layout.setContentsMargins(5, 0, 0, 0)
        # this layout must be added to the propertyWidget object loaded from blue.ui :
        # we postpone it to in blue.py, after loading the main form.
        self.propertyLayout = l
        # shortcut actions
        self.actionDup = QAction('Duplicate layer', None)
        self.actionDup.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_J))
        self.addAction(self.actionDup)

        def dup():
            row = self.selectedIndexes()[0].row()
            # Stack index
            index = len(self.img.layersStack) - row - 1
            layer = self.img.layersStack[index]
            if layer.isAdjustLayer():
                return
            # add new layer to stack and set it to active
            self.img.dupLayer(index=index)
            # update layer view
            self.setLayers(self.img)

        self.actionDup.triggered.connect(dup)
        self.setWhatsThis("""<b>Layer Stack</b><br>
To <b>toggle the layer visibility</b> click on the Eye icon.<br>
To <b>add a mask</b> use the context menu to enable it and paint pixels with the Mask/Unmask tools in the left pane.<br>
For <b>color mask<b/b>: <br>
    &nbsp; green pixels are masked,<br>
    &nbsp; red pixels are unmasked.<br>
    &nbsp; Other colors correspond to partially masked pixels.<br>
Note that upper visible layers slow down mask edition.<br>
""")  # end of setWhatsThis

    def closeAdjustForms(self, delete=False):
        """
        Close all windows associated with image layers.
        If delete is True (default False), the windows and
        their dock containers are deleted.
        @param delete:
        @type delete: boolean
        """
        if self.img is None:
            return
        stack = self.img.layersStack
        for layer in stack:
            layer.closeView(delete=delete)
        if delete:
            self.currentWin = None
            gc.collect()

    def clear(self, delete=True):
        """
        Reset LayerView and clear the back
        links to image.
        """
        self.closeAdjustForms(delete=delete)
        self.img = None
        self.currentWin = None
        # model = layerModel()
        # model.setColumnCount(3)  # TODO removed 21/01/20 validate
        self.setModel(None)

    def setLayers(self, mImg, delete=False):
        """
        Displays the layer stack of a mImage instance.
        @param mImg: image
        @type mImg: mImage
        @param delete:
        @type delete:
        """
        # close open adjustment windows
        # self.closeAdjustForms()
        self.clear(delete=delete)
        mImg.layerView = self
        # back link to image
        self.img = weakProxy(mImg)
        model = layerModel()
        model.setColumnCount(3)
        l = len(mImg.layersStack)

        # dataChanged event handler : enables edition of layer name
        def f(index1, index2):
            # index1 and index2 should be equal
            # only layer name should be editable
            # dropEvent emit dataChanged when setting item values. f must
            # return immediately from these calls, as they are possibly made with unconsistent data :
            # dragged rows are already removed from layersStack
            # and not yet removed from model.
            if l != self.model().rowCount():
                return
            # only name is editable
            if index1.column() != 1:
                return
            row = index1.row()
            stackIndex = l - row - 1
            mImg.layersStack[stackIndex].name = index1.data()

        model.dataChanged.connect(f)
        for r, lay in enumerate(reversed(mImg.layersStack)):
            try:
                lay.maskSettingsChanged.sig.disconnect()
            except RuntimeError:
                pass
            lay.maskSettingsChanged.sig.connect(self.updateRows)
            items = []
            # col 0 : visibility icon
            if lay.visible:
                item_visible = QStandardItem(
                    QIcon(":/images/resources/eye-icon.png"), "")
            else:
                item_visible = QStandardItem(
                    QIcon(":/images/resources/eye-icon-strike.png"), "")
            items.append(item_visible)
            # col 1 : image icon (for non-adjustment layer only) and name
            if len(lay.name) <= 30:
                name = lay.name
            else:
                name = lay.name[:28] + '...'
            if hasattr(lay, 'inputImg'):
                item_name = QStandardItem(name)
            else:
                # icon with very small dim causes QPainter error
                # QPixmap.fromImage bug ?
                smallImg = lay.resized(50, 50)
                w, h = smallImg.width(), smallImg.height()
                if w < h / 5 or h < w / 5:
                    item_name = QStandardItem(name)
                else:
                    item_name = QStandardItem(
                        QIcon(QPixmap.fromImage(smallImg)), name)
            # set tool tip to full name
            item_name.setToolTip(lay.name)
            items.append(item_name)
            item_mask = QStandardItem('m')
            items.append(item_mask)
            model.appendRow(items)
        self.setModel(model)
        self.horizontalHeader().hide()
        self.verticalHeader().hide()
        header = self.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
        # lay out  the graphic forms into right pane
        forms = [
            item.view for item in mImg.layersStack
            if getattr(item, 'view', None) is not None
        ]
        for dock in forms:
            if TABBING:
                dockedForms = [item for item in forms if not item.isFloating()]
                dock.show()  # needed to get working tabbing
                if dock not in dockedForms and dock.tabbed:
                    if dockedForms:
                        window.tabifyDockWidget(dockedForms[-1], dock)
                    else:
                        window.addDockWidget(Qt.RightDockWidgetArea, dock)
            dock.setFloating(False)
        # select active layer
        self.selectRow(len(mImg.layersStack) - 1 - mImg.activeLayerIndex)
        activeLayer = mImg.getActiveLayer()
        layerview = activeLayer.view
        if layerview is not None:
            layerview.show()
            if TABBING:
                layerview.raise_()
            # lay out subcontrols of activeLayer
            form = layerview.widget()
            for dk in form.subControls:
                dk.setVisible(form.options[dk.widget().optionName])
                # clean up: we (re)dock all sucontrols
                dk.setFloating(False)  # emit topLevelChanged signal
        self.opacitySlider.setSliderPosition(int(activeLayer.opacity * 100))
        self.maskSlider.setSliderPosition(
            int(activeLayer.colorMaskOpacity * 100.0 / 255.0))
        ind = self.blendingModeCombo.findData(activeLayer.compositionMode)
        self.blendingModeCombo.setCurrentIndex(ind)
        self.previewOptionBox.setChecked(activeLayer.parentImage.useThumb)
        #activeLayer.maskColor
        self.updateForm()
        """                                                   # TODO removed 25/01/20 useless validate
        for item in self.img.layersStack:
            if hasattr(item, 'sourceIndex'):
                combo = item.getGraphicsForm().sourceCombo
                currentText = combo.currentText()
                combo.clear()
                for i, x in enumerate(self.img.layersStack):
                    item.view.widget().sourceCombo.addItem(x.name, i)
                combo.setCurrentIndex(combo.findText(currentText))
        """

    def updateForm(self):
        activeLayer = self.img.getActiveLayer()
        if hasattr(activeLayer, 'view'):
            self.currentWin = activeLayer.view
        if self.currentWin is not None:
            self.currentWin.show()
            self.currentWin.activateWindow()

    def updateRow(self, row):
        minInd, maxInd = self.model().index(row, 0), self.model().index(row, 3)
        self.model().dataChanged.emit(minInd, maxInd)

    def updateRows(self):
        """
        Update all rows.
        """
        count = self.model().rowCount()
        minInd, maxInd = self.model().index(0, 0), self.model().index(
            count - 1, 3)
        self.model().dataChanged.emit(minInd, maxInd)

    def dropEvent(self, event):
        """
        drop event handler : moving layer
        @param event:
        @type event: Qevent
        """
        if event.source() is not self:
            return
        # get selected rows and layers
        rows = set([mi.row() for mi in self.selectedIndexes()])
        rStack = self.img.layersStack[::-1]
        layers = [rStack[i] for i in rows]
        # get target row and layer
        targetRow = self.indexAt(event.pos()).row()
        targetLayer = rStack[targetRow]
        # remove target from selection
        if targetRow in rows:
            rows.discard(targetRow)
        rows = sorted(rows)
        if not rows:
            return
        # if target is below last row insert at the last position
        if targetRow == -1:
            targetRow = self.model().rowCount()
        # mapping of src (row) indices to target indices
        rowMapping = dict()
        for idx, row in enumerate(rows):
            if row < targetRow:
                rowMapping[row] = targetRow + idx
            else:
                rowMapping[row + len(rows)] = targetRow + idx
        # update layerStack using rowMapping
        # insert None items
        for _ in range(len(rows)):
            rStack.insert(targetRow, None)
        # copy moved items to their final place
        for srcRow, tgtRow in sorted(
                rowMapping.items()):  # python 3 iteritems->items
            rStack[tgtRow] = rStack[srcRow]
        # remove moved items from their initial place
        for row in reversed(sorted(
                rowMapping.keys())):  # python 3 iterkeys -> keys
            rStack.pop(row)
        self.img.layersStack = rStack[::-1]
        # update model
        # insert empty rows
        for _ in range(len(rows)):
            result = self.model().insertRow(targetRow, QModelIndex())
        # copy moved rows to their final place
        colCount = self.model().columnCount()
        for srcRow, tgtRow in sorted(
                rowMapping.items()):  # python 3 iteritems->items
            for col in range(0, colCount):
                # CAUTION : setItem calls the data changed event handler (cf. setLayers above)
                self.model().setItem(tgtRow, col,
                                     self.model().takeItem(srcRow, col))
        # remove moved rows from their initial place and keep track of moved items
        movedDict = rowMapping.copy()
        for row in reversed(sorted(
                rowMapping.keys())):  # python 3 iterkeys -> keys
            self.model().removeRow(row)
            for s, t in rowMapping.items():
                if t > row:
                    movedDict[s] -= 1
        ######################################### sanity check
        for r in range(self.model().rowCount()):
            id = self.model().index(r, 1)
            if id.data() != rStack[r].name:
                raise ValueError('Drop Error')
        ########################################
        # reselect moved rows
        sel = sorted(movedDict.values())
        selectionModel = QtCore.QItemSelectionModel(self.model())
        self.setSelectionModel(selectionModel)
        index1 = self.model().index(sel[0], 1)
        index2 = self.model().index(sel[-1], 1)
        itemSelection = QtCore.QItemSelection(index1, index2)
        self.selectionModel().select(
            itemSelection, QtCore.QItemSelectionModel.Rows
            | QtCore.QItemSelectionModel.Select)
        # multiple selection : display no window
        if len(sel) > 1:
            self.currentWin.hide()
            self.currentWin = None
        elif len(sel) == 1:
            self.img.setActiveLayer(len(self.img.layersStack) - sel[0] - 1)
        # update stack
        self.img.layersStack[0].applyToStack()
        self.img.onImageChanged()

    def select(self, row, col):
        """
        select item in view
        @param row:
        @type row:
        @param col:
        @type col:
        @return:
        @rtype:
        """
        model = self.model()
        self.viewClicked(model.index(row, col))

    def viewClicked(self, clickedIndex):
        """
        Mouse clicked event handler.
        @param clickedIndex: 
        @type clickedIndex: QModelIndex
        """
        row = clickedIndex.row()
        rows = set([mi.row() for mi in self.selectedIndexes()])
        # multiple selection : go to top of selection
        m = min(rows)
        if row != m:
            clickedIndex = self.model().index(m, clickedIndex.column())
        layer = self.img.layersStack[-1 - row]
        self.actionDup.setEnabled(not layer.isAdjustLayer())
        # toggle layer visibility
        if clickedIndex.column() == 0:
            # background layer is always visible
            if row == len(self.img.layersStack) - 1:
                return
            # layer.visible = not(layer.visible)
            layer.setVisible(not layer.visible)
            if self.currentWin is not None:
                self.currentWin.setVisible(layer.visible)
                if not layer.visible:
                    self.currentWin = None
            if layer.tool is not None:
                layer.tool.setVisible(layer.visible)
            # update stack
            if layer.visible:
                layer.applyToStack()
            else:
                i = layer.getUpperVisibleStackIndex()
                if i >= 0:
                    layer.parentImage.layersStack[i].applyToStack()
                else:
                    # top layer : update only the presentation layer
                    layer.parentImage.prLayer.execute(l=None, pool=None)
            self.img.onImageChanged()
        # update displayed window and active layer
        activeStackIndex = len(self.img.layersStack) - 1 - row
        activeLayer = self.img.setActiveLayer(activeStackIndex)
        # update color mask slider and label
        self.maskLabel.setEnabled(layer.maskIsSelected)
        self.maskSlider.setEnabled(activeLayer.maskIsSelected)
        self.maskValue.setEnabled(activeLayer.maskIsSelected)
        if self.currentWin is not None:
            # hide sucontrols
            for dk in self.currentWin.widget().subControls:
                dk.hide()
            if self.currentWin.isFloating():
                self.currentWin.hide()
        self.currentWin = None
        if hasattr(activeLayer, "view"):
            self.currentWin = activeLayer.view
        if self.currentWin is not None and activeLayer.visible:
            self.currentWin.show()
            self.currentWin.raise_()
            # display subcontrols
            for dk in self.currentWin.widget().subControls:
                dk.setVisible(
                    self.currentWin.widget().options[dk.widget().optionName])
            # make self.currentWin the active window
            self.currentWin.activateWindow()
        # update opacity and composition mode for current layer
        opacity = int(layer.opacity * 100)
        self.opacityValue.setText(str('%d ' % opacity))
        self.opacitySlider.setSliderPosition(opacity)
        compositionMode = layer.compositionMode
        ind = self.blendingModeCombo.findData(compositionMode)
        self.blendingModeCombo.setCurrentIndex(ind)

        # draw the right rectangle
        window.label.repaint()

    def initContextMenu(self):
        """
        Context menu initialization
        @return:
        @rtype: QMenu
        """
        menu = QMenu()
        # menu.actionReset = QAction('Reset To Default', None)
        # menu.actionLoadImage = QAction('Load New Image', None)
        # multiple selections
        menu.actionMerge = QAction('Merge Lower', None)
        # merge only adjustment layer with image layer
        menu.actionRepositionLayer = QAction('Reposition Layer(s)', None)
        menu.actionColorMaskEnable = QAction('Color', None)
        menu.actionOpacityMaskEnable = QAction('Opacity', None)
        menu.actionClippingMaskEnable = QAction('Clipping', None)
        menu.actionMaskDisable = QAction('Disabled', None)
        menu.actionMaskUndo = QAction('Undo Mask', None)
        menu.actionMaskRedo = QAction('Redo Mask', None)
        menu.actionMaskInvert = QAction('Invert Mask', None)
        menu.actionMaskReset_UM = QAction('Unmask All', None)
        menu.actionMaskReset_M = QAction('Mask All', None)
        menu.actionMaskCopy = QAction('Copy Mask to Clipboard', None)
        menu.actionImageCopy = QAction('Copy Image to Clipboard', None)
        menu.actionMaskPaste = QAction('Paste Mask', None)
        menu.actionImagePaste = QAction('Paste Image', None)
        menu.actionMaskDilate = QAction('Dilate Mask', None)
        menu.actionMaskErode = QAction('Erode Mask', None)
        menu.actionMaskSmooth = QAction('Smooth Mask', None)
        menu.actionMaskBright1 = QAction('Bright 1 Mask', None)
        menu.actionMaskBright2 = QAction('Bright 2 Mask', None)
        menu.actionMaskBright3 = QAction('Bright 3 Mask', None)
        menu.actionMaskDark1 = QAction('Dark 1 Mask', None)
        menu.actionMaskDark2 = QAction('Dark 2 Mask', None)
        menu.actionMaskDark3 = QAction('Dark 3 Mask', None)
        menu.actionMaskMid1 = QAction('Mid 1 Mask', None)
        menu.actionMaskMid2 = QAction('Mid 2 Mask', None)
        menu.actionMaskMid3 = QAction('Mid 3 Mask', None)
        menu.actionMergingFlag = QAction('Merged Layer', None)
        menu.actionMergingFlag.setCheckable(True)
        menu.actionColorMaskEnable.setCheckable(True)
        menu.actionOpacityMaskEnable.setCheckable(True)
        menu.actionClippingMaskEnable.setCheckable(True)
        menu.actionMaskDisable.setCheckable(True)
        ####################
        # Build menu
        ###################
        menu.addAction(menu.actionRepositionLayer)
        menu.addSeparator()
        # layer
        menu.addAction(menu.actionImageCopy)
        menu.addAction(menu.actionImagePaste)
        menu.addAction(menu.actionMergingFlag)
        menu.addSeparator()
        # mask
        menu.subMenuEnable = menu.addMenu('Mask...')
        menu.subMenuEnable.addAction(menu.actionColorMaskEnable)
        menu.subMenuEnable.addAction(menu.actionOpacityMaskEnable)
        menu.subMenuEnable.addAction(menu.actionClippingMaskEnable)
        menu.subMenuEnable.addAction(menu.actionMaskDisable)
        menu.addAction(menu.actionMaskUndo)
        menu.addAction(menu.actionMaskRedo)
        menu.addAction(menu.actionMaskInvert)
        menu.subMenuLum = menu.addMenu('Luminosity Mask...')
        for a in [
                menu.actionMaskBright1, menu.actionMaskBright2,
                menu.actionMaskBright3, menu.actionMaskDark1,
                menu.actionMaskDark2, menu.actionMaskDark3,
                menu.actionMaskMid1, menu.actionMaskMid2, menu.actionMaskMid3
        ]:
            menu.subMenuLum.addAction(a)
        menu.addAction(menu.actionMaskReset_UM)
        menu.addAction(menu.actionMaskReset_M)
        menu.addAction(menu.actionMaskCopy)
        menu.addAction(menu.actionMaskPaste)
        menu.addAction(menu.actionMaskDilate)
        menu.addAction(menu.actionMaskErode)
        menu.addAction(menu.actionMaskSmooth)
        menu.addSeparator()
        # miscellaneous
        # menu.addAction(menu.actionLoadImage)
        # to link actionDup with a shortcut,
        # it must be set in __init__
        menu.addAction(self.actionDup)
        menu.addAction(menu.actionMerge)
        # menu.addAction(menu.actionReset)
        return menu

    def contextMenuEvent(self, event):
        """
        context menu handler
        @param event
        @type event: QContextMenuEvent
        """
        selection = self.selectedIndexes()
        if not selection:
            return
        # get a fresh context menu without connected actions
        # and with state corresponding to the currently clicked layer
        self.cMenu = self.initContextMenu()
        # get current selection
        rows = set([mi.row() for mi in selection])
        rStack = self.img.layersStack[::-1]
        layers = [rStack[r] for r in rows]
        # get current position
        index = self.indexAt(event.pos())
        layerStackIndex = len(self.img.layersStack) - 1 - index.row()
        layer = self.img.layersStack[layerStackIndex]
        lowerVisible = self.img.layersStack[layer.getLowerVisibleStackIndex()]
        lower = self.img.layersStack[layerStackIndex -
                                     1]  # case index == 0 doesn't matter
        # toggle actions
        self.cMenu.actionMergingFlag.setChecked(layer.mergingFlag)
        self.cMenu.actionMerge.setEnabled(not (
            hasattr(layer, 'inputImg') or hasattr(lowerVisible, 'inputImg')))
        self.actionDup.setEnabled(not layer.isAdjustLayer())
        self.cMenu.actionColorMaskEnable.setChecked(layer.maskIsSelected
                                                    and layer.maskIsEnabled)
        self.cMenu.actionOpacityMaskEnable.setChecked(
            (not layer.maskIsSelected) and layer.maskIsEnabled)
        self.cMenu.actionClippingMaskEnable.setChecked(
            layer.isClipping and (layer.maskIsSelected or layer.maskIsEnabled))
        self.cMenu.actionMaskDisable.setChecked(not (
            layer.isClipping or layer.maskIsSelected or layer.maskIsEnabled))
        self.cMenu.actionMaskUndo.setEnabled(layer.historyListMask.canUndo())
        self.cMenu.actionMaskRedo.setEnabled(layer.historyListMask.canRedo())
        self.cMenu.subMenuEnable.setEnabled(len(rows) == 1)
        self.cMenu.actionMaskPaste.setEnabled(
            not QApplication.clipboard().image().isNull())
        self.cMenu.actionImagePaste.setEnabled(
            not QApplication.clipboard().image().isNull())
        self.cMenu.actionMergingFlag.setEnabled(layer.isImageLayer())

        # Event handlers

        def RepositionLayer():
            layer.xOffset, layer.yOffset = 0, 0
            layer.Zoom_coeff = 1.0
            layer.AltZoom_coeff = 1.0
            layer.xAltOffset, layer.yAltOffset = 0, 0
            layer.updatePixmap()
            self.img.onImageChanged()

        def merge():
            layer.merge_with_layer_immediately_below()

        def testUpperVisibility():
            pos = self.img.getStackIndex(layer)
            upperVisible = False
            for i in range(len(self.img.layersStack) - pos - 1):
                if self.img.layersStack[pos + 1 + i].visible:
                    upperVisible = True
                    break
            if upperVisible:
                dlgWarn("Upper visible layers slow down mask edition")
                return True
            return False

        def colorMaskEnable():
            testUpperVisibility()
            layer.maskIsEnabled = True
            layer.maskIsSelected = True
            self.maskLabel.setEnabled(layer.maskIsSelected)
            self.maskSlider.setEnabled(layer.maskIsSelected)
            self.maskValue.setEnabled(layer.maskIsSelected)
            layer.applyToStack()
            self.img.onImageChanged()

        def opacityMaskEnable():
            testUpperVisibility()
            layer.maskIsEnabled = True
            layer.maskIsSelected = False
            self.maskLabel.setEnabled(layer.maskIsSelected)
            self.maskSlider.setEnabled(layer.maskIsSelected)
            self.maskValue.setEnabled(layer.maskIsSelected)
            layer.applyToStack()
            self.img.onImageChanged()

        def clippingMaskEnable():
            layer.maskIsEnabled = True
            layer.maskIsSelected = False
            self.maskLabel.setEnabled(layer.maskIsSelected)
            self.maskSlider.setEnabled(layer.maskIsSelected)
            self.maskValue.setEnabled(layer.maskIsSelected)
            layer.isClipping = True
            layer.applyToStack()
            self.img.onImageChanged()

        def maskDisable():
            layer.maskIsEnabled = False
            layer.maskIsSelected = False
            self.maskLabel.setEnabled(layer.maskIsSelected)
            self.maskSlider.setEnabled(layer.maskIsSelected)
            self.maskValue.setEnabled(layer.maskIsSelected)
            layer.isClipping = False
            layer.applyToStack()
            self.img.onImageChanged()

        def undoMask():
            try:
                layer.mask = layer.historyListMask.undo(
                    saveitem=layer.mask.copy())
                layer.applyToStack()
                self.img.onImageChanged()
            except ValueError:
                pass

        def redoMask():
            try:
                layer.mask = layer.historyListMask.redo()
                layer.applyToStack()
                self.img.onImageChanged()
            except ValueError:
                pass

        def maskInvert():
            layer.invertMask()
            # update mask stack
            layer.applyToStack()
            self.img.onImageChanged()

        def maskReset_UM():
            layer.resetMask(maskAll=False)
            # update mask stack
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.execute(l=None, pool=None)
            self.img.onImageChanged()

        def maskReset_M():
            layer.resetMask(maskAll=True)
            # update mask stack
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.execute(l=None, pool=None)
            self.img.onImageChanged()

        def maskCopy():
            QApplication.clipboard().setImage(layer.mask)

        def imageCopy():
            QApplication.clipboard().setImage(layer.getCurrentMaskedImage())

        def maskPaste():
            """
            Pastes clipboard to mask and updates the stack. The clipboard image
            is scaled if its size does not match the size of the mask
            """
            cb = QApplication.clipboard()
            if not cb.image().isNull():
                img = cb.image()
                if img.size() == layer.mask.size():
                    layer.mask = img
                else:
                    layer.mask = img.scaled(layer.mask.size())
            layer.applyToStack()
            self.img.prLayer.execute(l=None, pool=None)
            self.img.onImageChanged()

        def imagePaste():
            """
            Pastes clipboard to mask and updates the stack. The clipboard image
            is scaled if its size does not match the size of the mask
            """
            cb = QApplication.clipboard()
            if not cb.image().isNull():
                srcImg = cb.image()
                if srcImg.size() == layer.size():
                    layer.setImage(srcImg)
                else:
                    layer.setImage(srcImg.scaled(layer.size()))
            layer.applyToStack()
            self.img.onImageChanged()

        def maskDilate():
            """
            Increase the masked part of the image
            """
            buf = QImageBuffer(layer.mask)
            buf[:, :, 2] = vImage.maskDilate(buf[:, :, 2])
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskErode():
            """
            Reduce the masked part of the image
            """
            buf = QImageBuffer(layer.mask)
            buf[:, :, 2] = vImage.maskErode(buf[:, :, 2])
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskSmooth():
            """
            Smooth the mask boundary
            """
            buf = QImageBuffer(layer.mask)
            buf[:, :, 2] = vImage.maskSmooth(buf[:, :, 2])
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskBright1():
            layer.setMaskLuminosity(min=128, max=255)
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskBright2():
            layer.setMaskLuminosity(min=192, max=255)
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskBright3():
            layer.setMaskLuminosity(min=224, max=255)
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskDark1():
            layer.setMaskLuminosity(min=0, max=128)

        def maskDark2():
            layer.setMaskLuminosity(min=0, max=64)
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskDark3():
            layer.setMaskLuminosity(min=0, max=32)
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskMid1():
            layer.setMaskLuminosity(min=64, max=192)
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskMid2():
            layer.setMaskLuminosity(min=96, max=160)
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def maskMid3():
            layer.setMaskLuminosity(min=112, max=144)
            for l in self.img.layersStack:
                l.updatePixmap(maskOnly=True)
            self.img.prLayer.update()
            self.img.onImageChanged()

        def mergingFlag(flag):
            layer.mergingFlag = flag

        self.cMenu.actionRepositionLayer.triggered.connect(RepositionLayer)
        # self.cMenu.actionLoadImage.triggered.connect(loadImage)
        self.cMenu.actionMerge.triggered.connect(merge)
        self.cMenu.actionColorMaskEnable.triggered.connect(colorMaskEnable)
        self.cMenu.actionOpacityMaskEnable.triggered.connect(opacityMaskEnable)
        self.cMenu.actionClippingMaskEnable.triggered.connect(
            clippingMaskEnable)
        self.cMenu.actionMaskDisable.triggered.connect(maskDisable)
        self.cMenu.actionMaskUndo.triggered.connect(undoMask)
        self.cMenu.actionMaskRedo.triggered.connect(redoMask)
        self.cMenu.actionMaskInvert.triggered.connect(maskInvert)
        self.cMenu.actionMaskReset_UM.triggered.connect(maskReset_UM)
        self.cMenu.actionMaskReset_M.triggered.connect(maskReset_M)
        self.cMenu.actionMaskCopy.triggered.connect(maskCopy)
        self.cMenu.actionMaskPaste.triggered.connect(maskPaste)
        self.cMenu.actionImageCopy.triggered.connect(imageCopy)
        self.cMenu.actionImagePaste.triggered.connect(imagePaste)
        self.cMenu.actionMaskDilate.triggered.connect(maskDilate)
        self.cMenu.actionMaskErode.triggered.connect(maskErode)
        self.cMenu.actionMaskSmooth.triggered.connect(maskSmooth)
        self.cMenu.actionMaskBright1.triggered.connect(maskBright1)
        self.cMenu.actionMaskBright2.triggered.connect(maskBright2)
        self.cMenu.actionMaskBright3.triggered.connect(maskBright3)
        self.cMenu.actionMaskDark1.triggered.connect(maskDark1)
        self.cMenu.actionMaskDark2.triggered.connect(maskDark2)
        self.cMenu.actionMaskDark3.triggered.connect(maskDark3)
        self.cMenu.actionMaskMid1.triggered.connect(maskMid1)
        self.cMenu.actionMaskMid2.triggered.connect(maskMid2)
        self.cMenu.actionMaskMid3.triggered.connect(maskMid3)
        self.cMenu.actionMergingFlag.toggled.connect(mergingFlag)
        self.cMenu.exec_(event.globalPos() - QPoint(400, 0))
        # update table
        for row in rows:
            self.updateRow(row)
    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(layer=layer, targetImage=targetImage, parent=parent)
        self.setMinimumSize(axeSize, axeSize+100)
        # contrast spline viewer
        self.contrastForm = None
        # options
        optionList1, optionNames1 = ['Multi-Mode', 'CLAHE'], ['Multi-Mode', 'CLAHE']
        self.listWidget1 = optionsWidget(options=optionList1, optionNames=optionNames1, exclusive=True,
                                         changed=lambda: self.dataChanged.emit())
        self.listWidget1.checkOption(self.listWidget1.intNames[0])
        self.listWidget1.setStyleSheet("QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}")
        optionList2, optionNames2 = ['High', 'manualCurve'], ['Preserve Highlights', 'Show Contrast Curve']

        def optionList2Change(item):
            if item.internalName == 'High':
                # force to recalculate the spline
                self.layer.autoSpline = True
            self.dataChanged.emit()
        self.listWidget2 = optionsWidget(options=optionList2, optionNames=optionNames2, exclusive=False,
                                         changed=optionList2Change)
        self.listWidget2.checkOption(self.listWidget2.intNames[0])
        self.listWidget2.setStyleSheet("QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}")
        self.options = UDict((self.listWidget1.options, self.listWidget2.options))

        # contrast slider
        self.sliderContrast = QbLUeSlider(Qt.Horizontal)
        self.sliderContrast.setStyleSheet(QbLUeSlider.bLueSliderDefaultIBWStylesheet)
        self.sliderContrast.setRange(0, 10)
        self.sliderContrast.setSingleStep(1)

        contrastLabel = QbLUeLabel()
        contrastLabel.setMaximumSize(150, 30)
        contrastLabel.setText("Contrast Level")
        contrastLabel.doubleClicked.connect(lambda: self.sliderContrast.setValue(self.contrast2Slider(self.contrastDefault)))

        self.contrastValue = QLabel()
        font = self.contrastValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.contrastValue.setMinimumSize(w, h)
        self.contrastValue.setMaximumSize(w, h)
        self.contrastValue.setText(str("{:d}".format(self.sliderContrast.value())))

        # contrast changed  event handler.
        def contrastUpdate(value):
            self.contrastValue.setText(str("{:d}".format(self.sliderContrast.value())))
            # move not yet terminated or value not modified
            if self.sliderContrast.isSliderDown() or self.slider2Contrast(value) == self.contrastCorrection:
                return
            self.sliderContrast.valueChanged.disconnect()
            self.sliderContrast.sliderReleased.disconnect()
            self.contrastCorrection = self.slider2Contrast(self.sliderContrast.value())
            # force to recalculate the spline
            self.layer.autoSpline = True
            self.dataChanged.emit()
            self.sliderContrast.valueChanged.connect(contrastUpdate)
            self.sliderContrast.sliderReleased.connect(lambda: contrastUpdate(self.sliderContrast.value()))

        self.sliderContrast.valueChanged.connect(contrastUpdate)
        self.sliderContrast.sliderReleased.connect(lambda: contrastUpdate(self.sliderContrast.value()))

        # saturation slider
        self.sliderSaturation = QbLUeSlider(Qt.Horizontal)
        self.sliderSaturation.setStyleSheet(QbLUeSlider.bLueSliderDefaultColorStylesheet)
        self.sliderSaturation.setRange(0, 100)
        self.sliderSaturation.setSingleStep(1)

        saturationLabel = QbLUeLabel()
        saturationLabel.setMaximumSize(150, 30)
        saturationLabel.setText("Saturation")
        saturationLabel.doubleClicked.connect(lambda: self.sliderSaturation.setValue(self.saturation2Slider(self.saturationDefault)))

        self.saturationValue = QLabel()
        font = self.saturationValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.saturationValue.setMinimumSize(w, h)
        self.saturationValue.setMaximumSize(w, h)
        self.saturationValue.setText(str("{:+d}".format(self.sliderContrast.value())))

        # saturation changed  event handler
        def saturationUpdate(value):
            self.saturationValue.setText(str("{:+d}".format(int(self.slidersaturation2User(self.sliderSaturation.value())))))
            # move not yet terminated or value not modified
            if self.sliderSaturation.isSliderDown() or self.slider2Saturation(value) == self.satCorrection:
                return
            self.sliderSaturation.valueChanged.disconnect()
            self.sliderSaturation.sliderReleased.disconnect()
            self.satCorrection = self.slider2Saturation(self.sliderSaturation.value())
            self.dataChanged.emit()
            self.sliderSaturation.valueChanged.connect(saturationUpdate)
            self.sliderSaturation.sliderReleased.connect(lambda: saturationUpdate(self.sliderSaturation.value()))
        self.sliderSaturation.valueChanged.connect(saturationUpdate)
        self.sliderSaturation.sliderReleased.connect(lambda: saturationUpdate(self.sliderSaturation.value()))

        # brightness slider
        self.sliderBrightness = QbLUeSlider(Qt.Horizontal)
        self.sliderBrightness.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.sliderBrightness.setRange(0, 100)
        self.sliderBrightness.setSingleStep(1)

        brightnessLabel = QbLUeLabel()
        brightnessLabel.setMaximumSize(150, 30)
        brightnessLabel.setText("Brightness")
        brightnessLabel.doubleClicked.connect(lambda: self.sliderBrightness.setValue(self.brightness2Slider(self.brightnessDefault)))

        self.brightnessValue = QLabel()
        font = self.brightnessValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.brightnessValue.setMinimumSize(w, h)
        self.brightnessValue.setMaximumSize(w, h)
        self.brightnessValue.setText(str("{:+d}".format(self.sliderContrast.value())))

        # brightness changed  event handler
        def brightnessUpdate(value):
            self.brightnessValue.setText(str("{:+d}".format(int(self.sliderBrightness2User(self.sliderBrightness.value())))))
            # move not yet terminated or value not modified
            if self.sliderBrightness.isSliderDown() or self.slider2Brightness(value) == self.brightnessCorrection:
                return
            self.sliderBrightness.valueChanged.disconnect()
            self.sliderBrightness.sliderReleased.disconnect()
            self.brightnessCorrection = self.slider2Brightness(self.sliderBrightness.value())
            self.dataChanged.emit()
            self.sliderBrightness.valueChanged.connect(brightnessUpdate)
            self.sliderBrightness.sliderReleased.connect(lambda: brightnessUpdate(self.sliderBrightness.value()))

        self.sliderBrightness.valueChanged.connect(brightnessUpdate)
        self.sliderBrightness.sliderReleased.connect(lambda: brightnessUpdate(self.sliderBrightness.value()))

        # attributes initialized in setDefaults, declared here
        # for the sake of correctness
        self.contrastCorrection = None    # range
        self.satCorrection = None         # range -0.5..0.5
        self.brightnessCorrection = None  # range -0.5..0.5

        # layout
        l = QVBoxLayout()
        l.setAlignment(Qt.AlignTop)
        gb1 = QGroupBox()
        gb1.setStyleSheet("QGroupBox {border: 1px solid gray; border-radius: 4px}")
        l1 = QVBoxLayout()
        ct = QLabel()
        ct.setText('Contrast')
        l.setAlignment(Qt.AlignTop)
        l1.addWidget(ct)
        l1.addWidget(self.listWidget1)
        gb1.setLayout(l1)
        l.addWidget(gb1)
        l.addWidget(self.listWidget2)
        l.addWidget(contrastLabel)
        hl = QHBoxLayout()
        hl.addWidget(self.contrastValue)
        hl.addWidget(self.sliderContrast)
        l.addLayout(hl)
        l.addWidget(brightnessLabel)
        hl3 = QHBoxLayout()
        hl3.addWidget(self.brightnessValue)
        hl3.addWidget(self.sliderBrightness)
        l.addLayout(hl3)
        l.addWidget(saturationLabel)
        hl2 = QHBoxLayout()
        hl2.addWidget(self.saturationValue)
        hl2.addWidget(self.sliderSaturation)
        l.addLayout(hl2)
        self.setLayout(l)
        self.adjustSize()
        self.setStyleSheet("QListWidget, QLabel {font : 7pt;}")
        self.setDefaults()
        self.setWhatsThis(
            """<b>Contrast Brightness Saturation</b><br>
            <b>Contrast</b> is enhanced using one of these two methods:<br>
              - <b>CLAHE</b> : increases the local contrast.<br>
              - <b>Multi-Mode</b> : increases the local contrast and the contrast between regions of the image.<br>
            For both methods the contrast slider controls the level of the correction.<br>
            With Multi-Mode enabled, use the option <b>Show Contrast Curve</b> to edit the correction curve and check
            <b>Preserve Highlights</b> for softer highlights.<br>
            <b>Brightness</b> and <b>Saturation</b> corrections are non linear to limit clipping.<br>
            Sliders are <b>reset</b> to their default value by double clicking the name of the slider.<br>
            """
                        )  # end setWhatsThis
class CoBrSatForm(baseForm):
    """
    Contrast, Brightness, Saturation adjustment form
    """
    layerTitle = "Cont/Bright/Sat"
    contrastDefault = 0.0
    brightnessDefault = 0.0
    saturationDefault = 0.0

    @classmethod
    def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None):
        wdgt = CoBrSatForm(targetImage=targetImage, axeSize=axeSize, layer=layer, parent=parent)
        wdgt.setWindowTitle(layer.name)
        return wdgt

    @classmethod
    def slider2Contrast(cls, v):
        return v / 10

    @classmethod
    def contrast2Slider(cls, v):
        return v * 10

    @classmethod
    def slider2Saturation(cls, v):
        return v / 100 - 0.5

    @classmethod
    def saturation2Slider(cls, v):
        return v * 100 + 50

    @classmethod
    def slidersaturation2User(cls, v):
        return v - 50

    @classmethod
    def slider2Brightness(cls, v):
        return v / 100 - 0.5

    @classmethod
    def brightness2Slider(cls, v):
        return v * 100 + 50

    @classmethod
    def sliderBrightness2User(cls, v):
        return v - 50

    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(layer=layer, targetImage=targetImage, parent=parent)
        self.setMinimumSize(axeSize, axeSize+100)
        # contrast spline viewer
        self.contrastForm = None
        # options
        optionList1, optionNames1 = ['Multi-Mode', 'CLAHE'], ['Multi-Mode', 'CLAHE']
        self.listWidget1 = optionsWidget(options=optionList1, optionNames=optionNames1, exclusive=True,
                                         changed=lambda: self.dataChanged.emit())
        self.listWidget1.checkOption(self.listWidget1.intNames[0])
        self.listWidget1.setStyleSheet("QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}")
        optionList2, optionNames2 = ['High', 'manualCurve'], ['Preserve Highlights', 'Show Contrast Curve']

        def optionList2Change(item):
            if item.internalName == 'High':
                # force to recalculate the spline
                self.layer.autoSpline = True
            self.dataChanged.emit()
        self.listWidget2 = optionsWidget(options=optionList2, optionNames=optionNames2, exclusive=False,
                                         changed=optionList2Change)
        self.listWidget2.checkOption(self.listWidget2.intNames[0])
        self.listWidget2.setStyleSheet("QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}")
        self.options = UDict((self.listWidget1.options, self.listWidget2.options))

        # contrast slider
        self.sliderContrast = QbLUeSlider(Qt.Horizontal)
        self.sliderContrast.setStyleSheet(QbLUeSlider.bLueSliderDefaultIBWStylesheet)
        self.sliderContrast.setRange(0, 10)
        self.sliderContrast.setSingleStep(1)

        contrastLabel = QbLUeLabel()
        contrastLabel.setMaximumSize(150, 30)
        contrastLabel.setText("Contrast Level")
        contrastLabel.doubleClicked.connect(lambda: self.sliderContrast.setValue(self.contrast2Slider(self.contrastDefault)))

        self.contrastValue = QLabel()
        font = self.contrastValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.contrastValue.setMinimumSize(w, h)
        self.contrastValue.setMaximumSize(w, h)
        self.contrastValue.setText(str("{:d}".format(self.sliderContrast.value())))

        # contrast changed  event handler.
        def contrastUpdate(value):
            self.contrastValue.setText(str("{:d}".format(self.sliderContrast.value())))
            # move not yet terminated or value not modified
            if self.sliderContrast.isSliderDown() or self.slider2Contrast(value) == self.contrastCorrection:
                return
            self.sliderContrast.valueChanged.disconnect()
            self.sliderContrast.sliderReleased.disconnect()
            self.contrastCorrection = self.slider2Contrast(self.sliderContrast.value())
            # force to recalculate the spline
            self.layer.autoSpline = True
            self.dataChanged.emit()
            self.sliderContrast.valueChanged.connect(contrastUpdate)
            self.sliderContrast.sliderReleased.connect(lambda: contrastUpdate(self.sliderContrast.value()))

        self.sliderContrast.valueChanged.connect(contrastUpdate)
        self.sliderContrast.sliderReleased.connect(lambda: contrastUpdate(self.sliderContrast.value()))

        # saturation slider
        self.sliderSaturation = QbLUeSlider(Qt.Horizontal)
        self.sliderSaturation.setStyleSheet(QbLUeSlider.bLueSliderDefaultColorStylesheet)
        self.sliderSaturation.setRange(0, 100)
        self.sliderSaturation.setSingleStep(1)

        saturationLabel = QbLUeLabel()
        saturationLabel.setMaximumSize(150, 30)
        saturationLabel.setText("Saturation")
        saturationLabel.doubleClicked.connect(lambda: self.sliderSaturation.setValue(self.saturation2Slider(self.saturationDefault)))

        self.saturationValue = QLabel()
        font = self.saturationValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.saturationValue.setMinimumSize(w, h)
        self.saturationValue.setMaximumSize(w, h)
        self.saturationValue.setText(str("{:+d}".format(self.sliderContrast.value())))

        # saturation changed  event handler
        def saturationUpdate(value):
            self.saturationValue.setText(str("{:+d}".format(int(self.slidersaturation2User(self.sliderSaturation.value())))))
            # move not yet terminated or value not modified
            if self.sliderSaturation.isSliderDown() or self.slider2Saturation(value) == self.satCorrection:
                return
            self.sliderSaturation.valueChanged.disconnect()
            self.sliderSaturation.sliderReleased.disconnect()
            self.satCorrection = self.slider2Saturation(self.sliderSaturation.value())
            self.dataChanged.emit()
            self.sliderSaturation.valueChanged.connect(saturationUpdate)
            self.sliderSaturation.sliderReleased.connect(lambda: saturationUpdate(self.sliderSaturation.value()))
        self.sliderSaturation.valueChanged.connect(saturationUpdate)
        self.sliderSaturation.sliderReleased.connect(lambda: saturationUpdate(self.sliderSaturation.value()))

        # brightness slider
        self.sliderBrightness = QbLUeSlider(Qt.Horizontal)
        self.sliderBrightness.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.sliderBrightness.setRange(0, 100)
        self.sliderBrightness.setSingleStep(1)

        brightnessLabel = QbLUeLabel()
        brightnessLabel.setMaximumSize(150, 30)
        brightnessLabel.setText("Brightness")
        brightnessLabel.doubleClicked.connect(lambda: self.sliderBrightness.setValue(self.brightness2Slider(self.brightnessDefault)))

        self.brightnessValue = QLabel()
        font = self.brightnessValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.brightnessValue.setMinimumSize(w, h)
        self.brightnessValue.setMaximumSize(w, h)
        self.brightnessValue.setText(str("{:+d}".format(self.sliderContrast.value())))

        # brightness changed  event handler
        def brightnessUpdate(value):
            self.brightnessValue.setText(str("{:+d}".format(int(self.sliderBrightness2User(self.sliderBrightness.value())))))
            # move not yet terminated or value not modified
            if self.sliderBrightness.isSliderDown() or self.slider2Brightness(value) == self.brightnessCorrection:
                return
            self.sliderBrightness.valueChanged.disconnect()
            self.sliderBrightness.sliderReleased.disconnect()
            self.brightnessCorrection = self.slider2Brightness(self.sliderBrightness.value())
            self.dataChanged.emit()
            self.sliderBrightness.valueChanged.connect(brightnessUpdate)
            self.sliderBrightness.sliderReleased.connect(lambda: brightnessUpdate(self.sliderBrightness.value()))

        self.sliderBrightness.valueChanged.connect(brightnessUpdate)
        self.sliderBrightness.sliderReleased.connect(lambda: brightnessUpdate(self.sliderBrightness.value()))

        # attributes initialized in setDefaults, declared here
        # for the sake of correctness
        self.contrastCorrection = None    # range
        self.satCorrection = None         # range -0.5..0.5
        self.brightnessCorrection = None  # range -0.5..0.5

        # layout
        l = QVBoxLayout()
        l.setAlignment(Qt.AlignTop)
        gb1 = QGroupBox()
        gb1.setStyleSheet("QGroupBox {border: 1px solid gray; border-radius: 4px}")
        l1 = QVBoxLayout()
        ct = QLabel()
        ct.setText('Contrast')
        l.setAlignment(Qt.AlignTop)
        l1.addWidget(ct)
        l1.addWidget(self.listWidget1)
        gb1.setLayout(l1)
        l.addWidget(gb1)
        l.addWidget(self.listWidget2)
        l.addWidget(contrastLabel)
        hl = QHBoxLayout()
        hl.addWidget(self.contrastValue)
        hl.addWidget(self.sliderContrast)
        l.addLayout(hl)
        l.addWidget(brightnessLabel)
        hl3 = QHBoxLayout()
        hl3.addWidget(self.brightnessValue)
        hl3.addWidget(self.sliderBrightness)
        l.addLayout(hl3)
        l.addWidget(saturationLabel)
        hl2 = QHBoxLayout()
        hl2.addWidget(self.saturationValue)
        hl2.addWidget(self.sliderSaturation)
        l.addLayout(hl2)
        self.setLayout(l)
        self.adjustSize()
        self.setStyleSheet("QListWidget, QLabel {font : 7pt;}")
        self.setDefaults()
        self.setWhatsThis(
            """<b>Contrast Brightness Saturation</b><br>
            <b>Contrast</b> is enhanced using one of these two methods:<br>
              - <b>CLAHE</b> : increases the local contrast.<br>
              - <b>Multi-Mode</b> : increases the local contrast and the contrast between regions of the image.<br>
            For both methods the contrast slider controls the level of the correction.<br>
            With Multi-Mode enabled, use the option <b>Show Contrast Curve</b> to edit the correction curve and check
            <b>Preserve Highlights</b> for softer highlights.<br>
            <b>Brightness</b> and <b>Saturation</b> corrections are non linear to limit clipping.<br>
            Sliders are <b>reset</b> to their default value by double clicking the name of the slider.<br>
            """
                        )  # end setWhatsThis

    def setContrastSpline(self, a, b, d, T):
        """
        Updates and displays the contrast spline viewer.
        The form is created only once.
        (Cf also rawForm.setCoBrSat.setContrastSpline).
        @param a: x_coordinates
        @type a:
        @param b: y-coordinates
        @type b:
        @param d: tangent slopes
        @type d:
        @param T: spline
        @type T: ndarray dtype=float
        """
        axeSize = 200
        if self.contrastForm is None:
            form = graphicsSplineForm.getNewWindow(targetImage=None, axeSize=axeSize, layer=self.layer, parent=None)
            form.setAttribute(Qt.WA_DeleteOnClose, on=False)
            form.setWindowTitle('Contrast Curve')
            form.setMaximumSize(300, 400)
            self.contrastForm = form
            window = self.parent().parent()
            dock = stateAwareQDockWidget(self.parent())
            dock.setWidget(form)
            dock.setWindowFlags(form.windowFlags())
            dock.setWindowTitle(form.windowTitle())
            dock.setStyleSheet("QGraphicsView{margin: 10px; border-style: solid; border-width: 1px; border-radius: 1px;}")
            window.addDockWidget(Qt.LeftDockWidgetArea, dock)
            self.dock = dock

            # curve changed slot
            def f():
                self.layer.applyToStack()
                self.layer.parentImage.onImageChanged()
            form.scene().quadricB.curveChanged.sig.connect(f)
        else:
            form = self.contrastForm
        # update the curve
        form.scene().setSceneRect(-25, -axeSize-25, axeSize+50, axeSize+50)
        form.scene().quadricB.setCurve(a*axeSize, b*axeSize,d, T*axeSize)
        self.dock.showNormal()

    def updateHists(self):
        """
        Update the histogram displayed under
        the contrast spline.
        """
        if self.contrastForm is not None:
            self.contrastForm.updateHists()

    def enableSliders(self):
        self.sliderContrast.setEnabled(True)
        self.sliderSaturation.setEnabled(True)
        self.sliderBrightness.setEnabled(True)

    def setDefaults(self):
        try:
            self.dataChanged.disconnect()
        except RuntimeError:
            pass
        self.listWidget1.unCheckAll()
        self.listWidget1.checkOption(self.listWidget1.intNames[0])
        self.listWidget2.unCheckAll()
        self.listWidget2.checkOption(self.listWidget2.intNames[0])
        self.enableSliders()
        self.contrastCorrection = self.contrastDefault
        self.sliderContrast.setValue(round(self.contrast2Slider(self.contrastCorrection)))
        self.satCorrection = self.saturationDefault
        self.sliderSaturation.setValue(round(self.saturation2Slider(self.satCorrection)))
        self.brightnessCorrection = self.brightnessDefault
        self.sliderBrightness.setValue(round(self.brightness2Slider(self.brightnessCorrection)))
        self.dataChanged.connect(self.updateLayer)

    def updateLayer(self):
        """
        data changed slot.
        """
        self.enableSliders()
        self.layer.applyToStack()
        self.layer.parentImage.onImageChanged()
        # enable/disable options relative to multi-mode
        for intname in ['High', 'manualCurve']:
            item = self.listWidget2.items[intname]
            if self.options['Multi-Mode']:
                item.setFlags(item.flags() | Qt.ItemIsEnabled)
            else:
                item.setFlags(item.flags() & ~Qt.ItemIsEnabled)
        # show/hide contrast curve
        cf = getattr(self, 'dock', None)
        if cf is None:
            return
        if self.options['manualCurve'] and self.options['Multi-Mode']:
            cf.showNormal()
        else:
            cf.hide()
Example #8
0
    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(targetImage=targetImage,
                         axeSize=axeSize,
                         layer=layer,
                         parent=parent)
        graphicsScene = self.scene()
        # connect layer selectionChanged signal
        self.layer.selectionChanged.sig.connect(self.updateLayer)
        # Init curves
        dSplineItem = activeBSpline(axeSize,
                                    period=axeSize,
                                    yZero=-3 * axeSize // 4)
        graphicsScene.addItem(dSplineItem)
        dSplineItem.setVisible(True)
        dSplineItem.initFixedPoints()
        self.dSplineItemB = dSplineItem
        graphicsScene.dSplineItemB = dSplineItem
        text = graphicsScene.addText('Hue (0-360)')
        text.setDefaultTextColor(Qt.white)
        text.setPos(-40, 10)
        text = graphicsScene.addText('delta H')
        text.setDefaultTextColor(Qt.white)
        text.setPos(-40, -self.axeSize // 4)
        text = graphicsScene.addText('delta B')
        text.setDefaultTextColor(Qt.white)
        text.setPos(-40, -(self.axeSize * 3) // 4)

        dSplineItem = activeBSpline(axeSize,
                                    period=axeSize,
                                    yZero=-axeSize // 4)
        graphicsScene.addItem(dSplineItem)
        dSplineItem.setVisible(True)
        dSplineItem.initFixedPoints()
        self.dSplineItemH = dSplineItem
        graphicsScene.dSplineItemH = dSplineItem

        # init 3D LUT
        self.LUT = DeltaLUT3D((34, 32, 32))

        self.marker = activeMarker.fromTriangle(parent=self.dSplineItemB)
        self.marker.setPos(0, 0)  # -(axeSize * 3) // 4)
        self.marker.setMoveRange(QRect(0, 0, axeSize, 0))
        self.scene().addItem(self.marker)

        def showPos(e, x, y):
            self.markerLabel.setText("%d" % (x * 360 // axeSize))

        self.marker.onMouseMove = showPos

        self.markerLabel = QLabel()
        font = self.markerLabel.font()
        metrics = QFontMetrics(font)
        w = metrics.width("0000")
        h = metrics.height()
        self.markerLabel.setMinimumSize(w, h)
        self.markerLabel.setMaximumSize(w, h)

        self.sliderSat = QbLUeSlider(Qt.Horizontal)
        self.sliderSat.setMinimumWidth(200)

        def satUpdate(value):
            self.satValue.setText(str("{:d}".format(value)))
            # move not yet terminated or values not modified
            if self.sliderSat.isSliderDown() or value == self.satThr:
                return
            try:
                self.sliderSat.valueChanged.disconnect()
                self.sliderSat.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.satThr = value
            self.dataChanged.emit()
            self.sliderSat.valueChanged.connect(satUpdate)
            self.sliderSat.sliderReleased.connect(
                lambda: satUpdate(self.sliderSat.value()))

        self.sliderSat.valueChanged.connect(satUpdate)
        self.sliderSat.sliderReleased.connect(
            lambda: satUpdate(self.sliderSat.value()))

        self.satValue = QLabel()
        font = self.markerLabel.font()
        metrics = QFontMetrics(font)
        w = metrics.width("0000")
        h = metrics.height()
        self.satValue.setMinimumSize(w, h)
        self.satValue.setMaximumSize(w, h)

        # layout
        gl = QGridLayout()
        gl.addWidget(QLabel('Hue '), 0, 0)
        gl.addWidget(self.markerLabel, 0, 1)
        gl.addWidget(QLabel('Sat Thr '), 1, 0)
        gl.addWidget(self.satValue, 1, 1)
        gl.addWidget(self.sliderSat, 1, 2, 4, 1)
        self.addCommandLayout(gl)

        self.setDefaults()

        self.setWhatsThis("""<b>3D LUT Shift HSV</b><br>
            All pixel colors are changed by the specific hue and brightness shifts corresponding to their hue.<br>
            x-axis represents hue values from 0 to 360. 
            The upper curve shows brightness multiplicative shifts (initially 1) and
            the lower curve hue additive shifts (initially 0). <br>
            Each curve is controlled by bump triangles.<br>
            To <b>add a bump triangle</b> to the curve click anywhere on the curve.
            To <b>remove the triangle</b> click on any vertex.<br>
            Drag the triangle vertices to move the bump along the x-axis and to change 
            its height and orientation. Use the <b> Sat Thr</b> slider 
            to preserve low saturated colors.<br>
            To <b>set the Hue Value Marker</b> Ctrl+click on the image.<br>
            To limit the shift corrections to a region of the image select the desired area
            with the rectangular marquee tool.<br>
            <b>Zoom</b> the curves with the mouse wheel.<br>
            """)
Example #9
0
class HVLUT2DForm(graphicsCurveForm):
    """
    Form for interactive HV 2D LUT
    """
    @classmethod
    def getNewWindow(cls,
                     targetImage=None,
                     axeSize=500,
                     layer=None,
                     parent=None):
        newWindow = HVLUT2DForm(targetImage=targetImage,
                                axeSize=axeSize,
                                layer=layer,
                                parent=parent)
        newWindow.setWindowTitle(layer.name)
        return newWindow

    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(targetImage=targetImage,
                         axeSize=axeSize,
                         layer=layer,
                         parent=parent)
        graphicsScene = self.scene()
        # connect layer selectionChanged signal
        self.layer.selectionChanged.sig.connect(self.updateLayer)
        # Init curves
        dSplineItem = activeBSpline(axeSize,
                                    period=axeSize,
                                    yZero=-3 * axeSize // 4)
        graphicsScene.addItem(dSplineItem)
        dSplineItem.setVisible(True)
        dSplineItem.initFixedPoints()
        self.dSplineItemB = dSplineItem
        graphicsScene.dSplineItemB = dSplineItem
        text = graphicsScene.addText('Hue (0-360)')
        text.setDefaultTextColor(Qt.white)
        text.setPos(-40, 10)
        text = graphicsScene.addText('delta H')
        text.setDefaultTextColor(Qt.white)
        text.setPos(-40, -self.axeSize // 4)
        text = graphicsScene.addText('delta B')
        text.setDefaultTextColor(Qt.white)
        text.setPos(-40, -(self.axeSize * 3) // 4)

        dSplineItem = activeBSpline(axeSize,
                                    period=axeSize,
                                    yZero=-axeSize // 4)
        graphicsScene.addItem(dSplineItem)
        dSplineItem.setVisible(True)
        dSplineItem.initFixedPoints()
        self.dSplineItemH = dSplineItem
        graphicsScene.dSplineItemH = dSplineItem

        # init 3D LUT
        self.LUT = DeltaLUT3D((34, 32, 32))

        self.marker = activeMarker.fromTriangle(parent=self.dSplineItemB)
        self.marker.setPos(0, 0)  # -(axeSize * 3) // 4)
        self.marker.setMoveRange(QRect(0, 0, axeSize, 0))
        self.scene().addItem(self.marker)

        def showPos(e, x, y):
            self.markerLabel.setText("%d" % (x * 360 // axeSize))

        self.marker.onMouseMove = showPos

        self.markerLabel = QLabel()
        font = self.markerLabel.font()
        metrics = QFontMetrics(font)
        w = metrics.width("0000")
        h = metrics.height()
        self.markerLabel.setMinimumSize(w, h)
        self.markerLabel.setMaximumSize(w, h)

        self.sliderSat = QbLUeSlider(Qt.Horizontal)
        self.sliderSat.setMinimumWidth(200)

        def satUpdate(value):
            self.satValue.setText(str("{:d}".format(value)))
            # move not yet terminated or values not modified
            if self.sliderSat.isSliderDown() or value == self.satThr:
                return
            try:
                self.sliderSat.valueChanged.disconnect()
                self.sliderSat.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.satThr = value
            self.dataChanged.emit()
            self.sliderSat.valueChanged.connect(satUpdate)
            self.sliderSat.sliderReleased.connect(
                lambda: satUpdate(self.sliderSat.value()))

        self.sliderSat.valueChanged.connect(satUpdate)
        self.sliderSat.sliderReleased.connect(
            lambda: satUpdate(self.sliderSat.value()))

        self.satValue = QLabel()
        font = self.markerLabel.font()
        metrics = QFontMetrics(font)
        w = metrics.width("0000")
        h = metrics.height()
        self.satValue.setMinimumSize(w, h)
        self.satValue.setMaximumSize(w, h)

        # layout
        gl = QGridLayout()
        gl.addWidget(QLabel('Hue '), 0, 0)
        gl.addWidget(self.markerLabel, 0, 1)
        gl.addWidget(QLabel('Sat Thr '), 1, 0)
        gl.addWidget(self.satValue, 1, 1)
        gl.addWidget(self.sliderSat, 1, 2, 4, 1)
        self.addCommandLayout(gl)

        self.setDefaults()

        self.setWhatsThis("""<b>3D LUT Shift HSV</b><br>
            All pixel colors are changed by the specific hue and brightness shifts corresponding to their hue.<br>
            x-axis represents hue values from 0 to 360. 
            The upper curve shows brightness multiplicative shifts (initially 1) and
            the lower curve hue additive shifts (initially 0). <br>
            Each curve is controlled by bump triangles.<br>
            To <b>add a bump triangle</b> to the curve click anywhere on the curve.
            To <b>remove the triangle</b> click on any vertex.<br>
            Drag the triangle vertices to move the bump along the x-axis and to change 
            its height and orientation. Use the <b> Sat Thr</b> slider 
            to preserve low saturated colors.<br>
            To <b>set the Hue Value Marker</b> Ctrl+click on the image.<br>
            To limit the shift corrections to a region of the image select the desired area
            with the rectangular marquee tool.<br>
            <b>Zoom</b> the curves with the mouse wheel.<br>
            """)

    def setDefaults(self):
        try:
            self.dataChanged.disconnect()
            self.dSplineItemB.curveChanged.sig.disconnect()
            self.dSplineItemH.curveChanged.sig.disconnect()
        except RuntimeError:
            pass
        self.satThr = 10
        self.sliderSat.setValue(self.satThr)
        self.dataChanged.connect(self.updateLayer)
        self.dSplineItemB.curveChanged.sig.connect(self.updateLayer)
        self.dSplineItemH.curveChanged.sig.connect(self.updateLayer)

    def updateLUT(self):
        """
        Updates the displacement LUT
        """
        data = self.LUT.data
        axeSize = self.axeSize
        hdivs = self.LUT.divs[0]
        # sat threshold
        sThr = int(self.LUT.divs[1] * self.satThr / 100)
        hstep = 360 / hdivs
        activeSpline = self.dSplineItemB
        sp = activeSpline.spline[activeSpline.periodViewing:]
        d = activeSpline.yZero
        # reinit the LUT
        data[...] = 0, 1, 1
        # update brightness
        for i in range(hdivs):
            pt = sp[int(i * hstep * axeSize / 360)]
            data[i, sThr:, :, 2] = 1.0 - (pt.y() - d) / 100

        activeSpline = self.dSplineItemH
        sp = activeSpline.spline[activeSpline.periodViewing:]
        d = activeSpline.yZero
        for i in range(hdivs):
            pt = sp[int(i * hstep * axeSize / 360)]
            data[i, sThr:, :, 0] = -(pt.y() - d) / 5

    def updateLayer(self):
        self.updateLUT()
        l = self.scene().layer
        l.applyToStack()
        l.parentImage.onImageChanged()

    def colorPickedSlot(self, x, y, modifiers):
        """
        Updates cursor from the hue of an image pixel.
        (x,y) coordinates are relative to the full size image.
        @param x:
        @type x:
        @param y:
        @type y:
        @param modifiers:
        @type modifiers:
        """
        color = self.scene().targetImage.getActivePixel(x, y, qcolor=True)
        h = color.hsvHue()
        if modifiers & QtCore.Qt.ControlModifier:
            self.marker.setPos(h * 300 / 360, 0)  # -(self.axeSize * 3) // 4)
            self.markerLabel.setText("%d" % h)
            self.update()
Example #10
0
class rawForm(baseForm):
    """
    Postprocessing of raw files.
    """
    dataChanged = QtCore.Signal(int)

    @classmethod
    def getNewWindow(cls,
                     targetImage=None,
                     axeSize=500,
                     layer=None,
                     parent=None):
        wdgt = rawForm(axeSize=axeSize,
                       targetImage=targetImage,
                       layer=layer,
                       parent=parent)
        wdgt.setWindowTitle(layer.name)
        return wdgt

    @staticmethod
    def slider2Temp(v):
        return 2000 + v * v

    @staticmethod
    def temp2Slider(T):
        return np.sqrt(T - 2000)

    @staticmethod
    def slider2Tint(v):
        return 0.1 + 0.0125 * v  # 0.2 + 0.0125 * v  # wanted range : 0.2...2.5
        # coeff = (self.tempCorrection / 4000 - 1) * 1.2 # experimental formula
        # eturn coeff + 0.01*v

    @staticmethod
    def tint2Slider(t):
        return (t - 0.1) / 0.0125
        # coeff = (self.tempCorrection / 4000 - 1) * 1.2 # experimental formula
        # return (t-coeff)/0.01
        # displayed value

    @staticmethod
    def sliderTint2User(v):
        return v - 75  # ((slider2Tint(v) - 1)*100)

    @staticmethod
    def slider2Exp(v):
        return 2**((v - 50) / 15.0)

    @staticmethod
    def exp2Slider(e):
        return round(15 * np.log2(e) + 50)

    @staticmethod
    def sliderExp2User(v):
        return (v - 50) / 15

    @staticmethod
    def slider2Cont(v):
        return v

    @staticmethod
    def cont2Slider(e):
        return e

    @staticmethod
    def slider2Br(v):
        return (np.power(3, v / 50) - 1) / 2

    @staticmethod
    def br2Slider(v):
        return 50 * log(2 * v + 1, 3)  # int(round(50.0 * e))

    @staticmethod
    def brSlider2User(v):
        return v - 50

    @staticmethod
    def slider2Sat(v):
        return v - 50

    @staticmethod
    def sat2Slider(e):
        return e + 50

    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(layer=layer, targetImage=targetImage, parent=parent)

        #######################################
        # Libraw correspondences:
        # rgb_xyz_matrix is libraw cam_xyz
        # camera_whitebalance is libraw cam_mul
        # daylight_whitebalance is libraw pre_mul
        # dng correspondences:
        # ASSHOTNEUTRAL tag value is (X,Y,Z) =  1 / rawpyObj.camera_whitebalance
        ##########################################
        rawpyObj = layer.parentImage.rawImage

        # constants and as shot values
        self.XYZ2CameraMatrix = rawpyObj.rgb_xyz_matrix[:3, :]
        self.XYZ2CameraInverseMatrix = np.linalg.inv(self.XYZ2CameraMatrix)
        # initial post processing multipliers (as shot)
        m1, m2, m3, m4 = rawpyObj.camera_whitebalance
        self.asShotMultipliers = (
            m1 / m2, 1.0, m3 / m2, m4 / m2
        )  # normalization is mandatory : for nef files white balance is around 256
        self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint(
            *1 / np.array(self.asShotMultipliers[:3]), self.XYZ2CameraMatrix)
        self.rawMultipliers = self.asShotMultipliers  # rawpyObj.camera_whitebalance # = 1/(dng ASSHOTNEUTRAL tag value)
        self.sampleMultipliers = False
        self.samples = []
        ########################################
        # XYZ-->Camera conversion matrix:
        # Last row is zero for RGB cameras (cf. rawpy and libraw docs).
        # type ndarray, shape (4,3)
        #########################################

        # attributes initialized in setDefaults, declared here for the sake of correctness
        self.tempCorrection, self.tintCorrection, self.expCorrection, self.highCorrection,\
                                                   self.contCorrection, self.satCorrection, self.brCorrection = [None] * 7
        # contrast spline vie (initialized by setContrastSpline)
        self.contrastForm = None
        # tone spline view (initialized by setToneSpline)
        self.toneForm = None
        # dock containers for contrast and tome forms
        self.dockC, self.dockT = None, None
        # options
        optionList0, optionNames0 = ['Auto Brightness', 'Preserve Highlights'
                                     ], ['Auto Expose', 'Preserve Highlights']
        optionList1, optionNames1 = ['Auto WB', 'Camera WB', 'User WB'
                                     ], ['Auto', 'Camera (As Shot)', 'User']
        optionList2, optionNames2 = [
            'cpLookTable', 'cpToneCurve', 'manualCurve'
        ], [
            'Use Camera Profile Look Table', 'Show Tone Curves',
            'Show Contrast Curve'
        ]
        self.listWidget1 = optionsWidget(
            options=optionList0,
            optionNames=optionNames0,
            exclusive=False,
            changed=lambda: self.dataChanged.emit(1))
        self.listWidget2 = optionsWidget(
            options=optionList1,
            optionNames=optionNames1,
            exclusive=True,
            changed=lambda: self.dataChanged.emit(1))
        self.listWidget3 = optionsWidget(
            options=optionList2,
            optionNames=optionNames2,
            exclusive=False,
            changed=lambda: self.dataChanged.emit(2))
        self.options = UDict(
            (self.listWidget1.options, self.listWidget2.options,
             self.listWidget3.options))
        # display the 'as shot' temperature
        item = self.listWidget2.item(1)
        item.setText(item.text() + ' : %d' % self.asShotTemp)

        # temperature slider
        self.sliderTemp = QbLUeSlider(Qt.Horizontal)
        self.sliderTemp.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultColorStylesheet)
        self.sliderTemp.setRange(0, 100)
        self.sliderTemp.setSingleStep(1)

        self.tempLabel = QLabel()
        self.tempLabel.setText("Temp")

        self.tempValue = QLabel()
        font = self.tempValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("10000")
        h = metrics.height()
        self.tempValue.setMinimumSize(w, h)
        self.tempValue.setMaximumSize(w, h)
        self.tempValue.setText(
            str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value()))))

        self.sliderTemp.valueChanged.connect(
            self.tempUpdate)  # signal send new value as parameter
        self.sliderTemp.sliderReleased.connect(lambda: self.tempUpdate(
            self.sliderTemp.value()))  # signal pass no parameter

        # tint slider
        self.sliderTint = QbLUeSlider(Qt.Horizontal)
        self.sliderTint.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultIMGColorStylesheet)
        self.sliderTint.setRange(0, 150)

        self.sliderTint.setSingleStep(1)

        self.tintLabel = QLabel()
        self.tintLabel.setText("Tint")

        self.tintValue = QLabel()
        font = self.tempValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.tintValue.setMinimumSize(w, h)
        self.tintValue.setMaximumSize(w, h)
        self.tintValue.setText(
            str("{:.0f}".format(self.sliderTint2User(
                self.sliderTint.value()))))

        self.sliderTint.valueChanged.connect(self.tintUpdate)
        self.sliderTint.sliderReleased.connect(lambda: self.tintUpdate(
            self.sliderTint.value()))  # signal pass no parameter)

        ######################
        # From libraw and dcraw sources:
        # Exposure and brightness are curve transformations.
        # Exposure curve is y = alpha*x, with cubic root ending; it is applied before demosaicing.
        # Brightness is (similar to) y = x**alpha and part of gamma transformation from linear sRGB to RGB.
        # Exposure and brightness both dilate the histogram towards highlights.
        # Exposure dilatation is uniform (homothety), brightness dilataion is
        # maximum for the midtones and the highlghts are preserved.
        # As a consequence, normal workflow begins with the adjustment of exposure,
        # to fill the entire range of the histogram and to adjust the highlights. Next,
        # one adjusts the brightness to put the midtones at the level we want them to be.
        # Cf. https://www.cambridgeincolour.com/forums/thread653.htm
        #####################

        # profile combo
        self.dngDict = self.setCameraProfilesCombo()

        # cameraProfilesCombo index changed event handler

        def cameraProfileUpdate(value):
            self.dngDict = self.cameraProfilesCombo.itemData(value)
            if self.options['cpToneCurve']:
                toneCurve = dngProfileToneCurve(
                    self.dngDict.get('ProfileToneCurve', []))
                self.toneForm.baseCurve = [
                    QPointF(x * axeSize, -y * axeSize)
                    for x, y in zip(toneCurve.dataX, toneCurve.dataY)
                ]
                self.toneForm.update()
            # recompute as shot temp and tint using new profile
            self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint(
                *1 / np.array(self.asShotMultipliers[:3]),
                self.XYZ2CameraMatrix,
                dngDict=self.dngDict
            )  # TODO 6/12/19 added keyword dngDict validate
            # display updated as shot temp
            item = self.listWidget2.item(1)
            item.setText(item.text().split(":")[0] + ': %d' % self.asShotTemp)
            # invalidate cache
            self.layer.bufCache_HSV_CV32 = None
            self.dataChanged.emit(2)  # 2 = no postprocessing

        self.cameraProfilesCombo.currentIndexChanged.connect(
            cameraProfileUpdate)

        # denoising combo
        self.denoiseCombo = QComboBox()
        items = OrderedDict([('Off', 0), ('Medium', 1), ('Full', 2)])
        for key in items:
            self.denoiseCombo.addItem(key, items[key])

        # denoiseCombo index changed event handler
        def denoiseUpdate(value):
            self.denoiseValue = self.denoiseCombo.itemData(value)
            self.dataChanged.emit(1)

        self.denoiseCombo.currentIndexChanged.connect(denoiseUpdate)

        # overexposed area restoration
        self.overexpCombo = QComboBox()
        items = OrderedDict([('Clip', 0), ('Ignore', 1), ('Blend', 2),
                             ('Reconstruct', 3)])
        for key in items:
            self.overexpCombo.addItem(key, items[key])

        # overexpCombo index changed event handler
        def overexpUpdate(value):
            self.overexpValue = self.overexpCombo.itemData(value)
            self.dataChanged.emit(1)

        self.overexpCombo.currentIndexChanged.connect(overexpUpdate)

        # exp slider
        self.sliderExp = QbLUeSlider(Qt.Horizontal)
        self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.sliderExp.setRange(0, 100)

        self.sliderExp.setSingleStep(1)

        self.expLabel = QLabel()
        self.expLabel.setText("Exp.")

        self.expValue = QLabel()
        font = self.expValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("+1.0")
        h = metrics.height()
        self.expValue.setMinimumSize(w, h)
        self.expValue.setMaximumSize(w, h)
        self.expValue.setText(
            str("{:.1f}".format(self.slider2Exp(self.sliderExp.value()))))

        # exp done event handler
        def expUpdate(value):
            self.expValue.setText(
                str("{:+.1f}".format(
                    self.sliderExp2User(self.sliderExp.value()))))
            # move not yet terminated or value not modified
            if self.sliderExp.isSliderDown() or self.slider2Exp(
                    value) == self.expCorrection:
                return
            try:
                self.sliderExp.valueChanged.disconnect()
                self.sliderExp.sliderReleased.disconnect()
            except RuntimeError:
                pass
            # rawpy: expCorrection range is -2.0...3.0, boiling down to exp_shift range 2**(-2)=0.25...2**3=8.0
            self.expCorrection = self.slider2Exp(self.sliderExp.value())
            self.dataChanged.emit(1)
            self.sliderExp.valueChanged.connect(
                expUpdate)  # send new value as parameter
            self.sliderExp.sliderReleased.connect(lambda: expUpdate(
                self.sliderExp.value()))  # signal pass no parameter

        self.sliderExp.valueChanged.connect(
            expUpdate)  # send new value as parameter
        self.sliderExp.sliderReleased.connect(lambda: expUpdate(
            self.sliderExp.value()))  # signal pass no parameter

        # brightness slider
        brSlider = QbLUeSlider(Qt.Horizontal)
        brSlider.setRange(1, 101)

        self.sliderExp.setSingleStep(1)

        brSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)

        self.sliderBrightness = brSlider
        brLabel = QLabel()
        brLabel.setText("Bright.")

        self.brValue = QLabel()
        font = self.expValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("+99")
        h = metrics.height()
        self.brValue.setMinimumSize(w, h)
        self.brValue.setMaximumSize(w, h)
        self.brValue.setText(
            str("{:+d}".format(
                int(self.brSlider2User(self.sliderBrightness.value())))))

        # brightness done event handler
        def brUpdate(value):
            self.brValue.setText(
                str("{:+d}".format(
                    int(self.brSlider2User(self.sliderBrightness.value())))))
            # move not yet terminated or value not modified
            if self.sliderBrightness.isSliderDown() or self.slider2Br(
                    value) == self.brCorrection:
                return
            try:
                self.sliderBrightness.valueChanged.disconnect()
                self.sliderBrightness.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.brCorrection = self.slider2Br(self.sliderBrightness.value())
            self.dataChanged.emit(1)
            self.sliderBrightness.sliderReleased.connect(
                lambda: brUpdate(self.sliderBrightness.value()))
            self.sliderBrightness.valueChanged.connect(
                brUpdate)  # send new value as parameter

        self.sliderBrightness.valueChanged.connect(
            brUpdate)  # send new value as parameter
        self.sliderBrightness.sliderReleased.connect(
            lambda: brUpdate(self.sliderBrightness.value()))

        # contrast slider
        self.sliderCont = QbLUeSlider(Qt.Horizontal)
        self.sliderCont.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.sliderCont.setRange(0, 20)

        self.sliderCont.setSingleStep(1)

        self.contLabel = QLabel()
        self.contLabel.setText("Cont.")

        self.contValue = QLabel()
        font = self.contValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.contValue.setMinimumSize(w, h)
        self.contValue.setMaximumSize(w, h)
        self.contValue.setText(
            str("{:.0f}".format(self.slider2Cont(self.sliderCont.value()))))

        # cont done event handler
        def contUpdate(value):
            self.contValue.setText(
                str("{:.0f}".format(self.slider2Cont(
                    self.sliderCont.value()))))
            # move not yet terminated or value not modified
            if self.sliderCont.isSliderDown() or self.slider2Cont(
                    value) == self.tempCorrection:
                return
            try:
                self.sliderCont.valueChanged.disconnect()
                self.sliderCont.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.contCorrection = self.slider2Cont(self.sliderCont.value())
            self.contValue.setText(str("{:+d}".format(self.contCorrection)))
            # force to recalculate the spline
            self.layer.autoSpline = True
            self.dataChanged.emit(
                3)  # no postprocessing and no camera profile stuff
            self.sliderCont.valueChanged.connect(
                contUpdate)  # send new value as parameter
            self.sliderCont.sliderReleased.connect(lambda: contUpdate(
                self.sliderCont.value()))  # signal has no parameter

        self.sliderCont.valueChanged.connect(
            contUpdate)  # send new value as parameter
        self.sliderCont.sliderReleased.connect(lambda: contUpdate(
            self.sliderCont.value()))  # signal has no parameter

        # saturation slider
        self.sliderSat = QbLUeSlider(Qt.Horizontal)
        self.sliderSat.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultColorStylesheet)
        self.sliderSat.setRange(0, 100)

        self.sliderSat.setSingleStep(1)

        satLabel = QLabel()
        satLabel.setText("Sat.")

        self.satValue = QLabel()
        font = self.satValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("+10")
        h = metrics.height()
        self.satValue.setMinimumSize(w, h)
        self.satValue.setMaximumSize(w, h)
        self.satValue.setText(
            str("{:+d}".format(self.slider2Sat(self.sliderSat.value()))))
        """sat done event handler"""

        def satUpdate(value):
            self.satValue.setText(
                str("{:+d}".format(self.slider2Sat(self.sliderSat.value()))))
            # move not yet terminated or value not modified
            if self.sliderSat.isSliderDown() or self.slider2Sat(
                    value) == self.satCorrection:
                return
            try:
                self.sliderSat.valueChanged.disconnect()
                self.sliderSat.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.satCorrection = self.slider2Sat(self.sliderSat.value())
            self.dataChanged.emit(
                3)  # no post processing and no camera profile stuff
            self.sliderSat.valueChanged.connect(
                satUpdate)  # send new value as parameter
            self.sliderSat.sliderReleased.connect(lambda: satUpdate(
                self.sliderSat.value()))  # signal has no parameter

        self.sliderSat.valueChanged.connect(
            satUpdate)  # send new value as parameter
        self.sliderSat.sliderReleased.connect(lambda: satUpdate(
            self.sliderSat.value()))  # signal has no parameter

        # layout
        l = QVBoxLayout()
        l.addWidget(self.listWidget3)
        hl01 = QHBoxLayout()
        hl01.addWidget(QLabel('Camera Profile'))
        hl01.addWidget(self.cameraProfilesCombo)
        l.addLayout(hl01)
        hl0 = QHBoxLayout()
        hl0.addWidget(QLabel('Denoising'))
        hl0.addWidget(self.denoiseCombo)
        l.addLayout(hl0)
        hl00 = QHBoxLayout()
        hl00.addWidget(QLabel('Overexp. Restoration'))
        hl00.addWidget(self.overexpCombo)
        l.addLayout(hl00)
        hl1 = QHBoxLayout()
        hl1.addWidget(self.expLabel)
        hl1.addWidget(self.expValue)
        hl1.addWidget(self.sliderExp)
        l.addLayout(hl1)
        hl8 = QHBoxLayout()
        hl8.addWidget(brLabel)
        hl8.addWidget(self.brValue)
        hl8.addWidget(self.sliderBrightness)
        l.addLayout(hl8)
        l.addWidget(self.listWidget1)
        vl1 = QVBoxLayout()
        vl1.addWidget(self.listWidget2)
        gb1 = QGroupBox()
        gb1.setTitle('White Balance')
        hl2 = QHBoxLayout()
        hl2.addWidget(self.tempLabel)
        hl2.addWidget(self.tempValue)
        hl2.addWidget(self.sliderTemp)
        hl3 = QHBoxLayout()
        hl3.addWidget(self.tintLabel)
        hl3.addWidget(self.tintValue)
        hl3.addWidget(self.sliderTint)
        vl1.addLayout(hl2)
        vl1.addLayout(hl3)
        gb1.setLayout(vl1)
        l.addWidget(gb1)
        hl4 = QHBoxLayout()
        hl4.addWidget(self.contLabel)
        hl4.addWidget(self.contValue)
        hl4.addWidget(self.sliderCont)
        hl7 = QHBoxLayout()
        hl7.addWidget(satLabel)
        hl7.addWidget(self.satValue)
        hl7.addWidget(self.sliderSat)

        # separator
        sep = QFrame()
        sep.setFrameShape(QFrame.HLine)
        sep.setFrameShadow(QFrame.Sunken)
        l.addWidget(sep)
        l.addLayout(hl4)
        l.addLayout(hl7)
        l.addStretch(1)
        self.setLayout(l)
        self.adjustSize()
        self.setDefaults()
        self.setWhatsThis("""<b>Development of raw files</b><br>
                    <b>Default settings</b> are a good starting point.<br>
                    A <b>Tone Curve</b> is applied to the raw image prior to postprocessing.<br> 
                    The cuvre can be edited by checking the option
                    <b>Show Tone Curve</b>; this option works best with manual exposure.<br>
                    <b>Contrast</b> correction is based on an automatic algorithm 
                    well suited to multi-mode histograms.<br>
                    <b>Brightness, Contrast</b> and <b>Saturation</b> levels</b> are 
                    adjustable with the correponding sliders.<br>
                    The <b>Contrast Curve</b> can be edited manually by checking 
                    the option <b>Show Contrast Curve</b>.<br>
                    Uncheck <b>Auto Expose</b> to adjust the exposure manually.<br>
                    The <b>OverExp. Rest.</b> slider controls the mode of restoration of overexposed areas. 
                    Valid values are 0 to 3 (0=clip;1=unclip;2=blend;3=rebuild); (with Auto Exposed 
                    checked the mode is clip).<br>
                    """)  # end of setWhatsThis

    def close(self):
        """
        Overrides QWidget.close to
        close toneForm and contrastForm
        @return:
        @rtype: boolean
        """
        delete = self.testAttribute(Qt.WA_DeleteOnClose)
        for attr in ['toneForm', 'contrastForm']:
            form = getattr(self, attr, None)
            if delete and form is not None:
                dock = form.parent()
                dock.setAttribute(Qt.WA_DeleteOnClose)
                form.setAttribute(Qt.WA_DeleteOnClose)
                dock.close()
                form.close()
        return super().close()

    def showToneSpline(self):
        """
        On first call, init and show the Tone Curve form.
        Otherwise, show the form.
        Return True if called for the first time, False otherwise.
        @return:
        @rtype: boolean
        """
        axeSize = 200
        if self.toneForm is None:
            form = graphicsToneForm.getNewWindow(targetImage=self.targetImage,
                                                 axeSize=axeSize,
                                                 layer=self.layer,
                                                 parent=self,
                                                 curveType='cubic')
            form.setWindowFlags(Qt.WindowStaysOnTopHint)
            form.setAttribute(Qt.WA_DeleteOnClose, on=False)
            form.setFixedHeight(axeSize + 140)
            form.setWindowTitle('Cam Tone Curve')
            form.setButtonText('Reset Curve')
            # get base curve from profile
            toneCurve = dngProfileToneCurve(
                self.dngDict.get('ProfileToneCurve', []))
            form.baseCurve = [
                QPointF(x * axeSize, -y * axeSize)
                for x, y in zip(toneCurve.dataX, toneCurve.dataY)
            ]

            def f():
                layer = self.layer
                layer.bufCache_HSV_CV32 = None
                layer.applyToStack()
                layer.parentImage.onImageChanged()

            form.scene().quadricB.curveChanged.sig.connect(f)
            self.toneForm = form
            self.toneForm.optionName = 'cpToneCurve'
            dockT = self.addSubcontrol(
                self.parent())  # )stateAwareQDockWidget(self.parent())
            dockT.setWindowFlags(form.windowFlags())
            dockT.setWindowTitle(form.windowTitle())
            dockT.setStyleSheet(
                "QGraphicsView{margin: 10px; border-style: solid; border-width: 1px; border-radius: 1px;}"
            )
            window = self.parent().parent()
            window.addDockWidget(Qt.LeftDockWidgetArea, dockT)
            self.dockT = dockT
            dockT.setWidget(form)
            showFirst = True
            form.setWhatsThis("""<b>Camera Profile Tone Curve</b><br>
                            The profile curve, if any, is applied as a starting point for user adjustments,
                            after raw post-processing.
                            Its input and output are in <b>linear</b> gamma.
                            The curve is shown in red and cannot be changed.<br>
                            A user curve, shown in black, is editable and is applied right after the
                            former.<br>         
                            """)  # end of setWhatsThis
        else:
            form = self.toneForm
            showFirst = False
        form.scene().setSceneRect(-25, -axeSize - 25, axeSize + 50,
                                  axeSize + 50)
        self.dockT.showNormal()
        return showFirst

    def setContrastSpline(self, a, b, d, T):
        """
        Updates and displays the contrast spline Form.
        The form is created if needed.
        (Cf. also CoBrStaForm setContrastSpline).
        @param a: x_coordinates
        @type a:
        @param b: y-coordinates
        @type b:
        @param d: tangent slopes
        @type d:
        @param T: spline
        @type T: ndarray dtype=float
        """
        axeSize = 200
        if self.contrastForm is None:
            form = graphicsSplineForm.getNewWindow(targetImage=None,
                                                   axeSize=axeSize,
                                                   layer=self.layer,
                                                   parent=None)
            form.setFixedHeight(axeSize + 140)
            form.setWindowFlags(Qt.WindowStaysOnTopHint)
            form.setAttribute(Qt.WA_DeleteOnClose, on=False)
            form.setWindowTitle('Contrast Curve')

            def f():
                layer = self.layer
                layer.applyToStack()
                layer.parentImage.onImageChanged()

            form.scene().quadricB.curveChanged.sig.connect(f)
            self.contrastForm = form
            self.contrastForm.optionName = 'manualCurve'
            dockC = self.addSubcontrol(
                self.parent())  # stateAwareQDockWidget(self.parent())
            dockC.setWindowFlags(form.windowFlags())
            dockC.setWindowTitle(form.windowTitle())
            dockC.setStyleSheet(
                "QGraphicsView{margin: 10px; border-style: solid; border-width: 1px; border-radius: 1px;}"
            )
            window = self.parent().parent()
            window.addDockWidget(Qt.LeftDockWidgetArea, dockC)
            self.dockC = dockC
            dockC.setWidget(form)
        else:
            form = self.contrastForm
        # update the curve
        form.scene().setSceneRect(-25, -axeSize - 25, axeSize + 50,
                                  axeSize + 50)
        form.scene().quadricB.setCurve(a * axeSize, b * axeSize, d,
                                       T * axeSize)
        self.dockC.showNormal()

    # temp changed  event handler
    def tempUpdate(self, value):
        self.tempValue.setText(
            str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value()))))
        # move not yet terminated or value not modified
        if self.sliderTemp.isSliderDown() or self.slider2Temp(
                value) == self.tempCorrection:
            return
        try:
            self.sliderTemp.valueChanged.disconnect()
            self.sliderTemp.sliderReleased.disconnect()
        except RuntimeError:
            pass
        self.tempCorrection = self.slider2Temp(self.sliderTemp.value())
        # get multipliers (temperatureAndTint2Multipliers returns the camera neutral)
        multipliers = [
            1 / m
            for m in temperatureAndTint2Multipliers(self.tempCorrection,
                                                    1.0,
                                                    self.XYZ2CameraMatrix,
                                                    dngDict=self.dngDict)
        ]  # TODO 6/12/19 added keyword dngDict validate
        multipliers[1] *= self.tintCorrection
        self.rawMultipliers = multipliers
        m = multipliers[1]
        self.rawMultipliers = [self.rawMultipliers[i] / m for i in range(4)]
        self.dataChanged.emit(1)
        self.sliderTemp.valueChanged.connect(
            self.tempUpdate)  # send new value as parameter
        self.sliderTemp.sliderReleased.connect(lambda: self.tempUpdate(
            self.sliderTemp.value()))  # signal has no parameter

    # tint change event handler
    def tintUpdate(self, value):
        self.tintValue.setText(
            str("{:.0f}".format(self.sliderTint2User(
                self.sliderTint.value()))))
        # move not yet terminated or value not modified
        if self.sliderTint.isSliderDown() or self.slider2Tint(
                value) == self.tintCorrection:
            return
        try:
            self.sliderTint.valueChanged.disconnect()
            self.sliderTint.sliderReleased.disconnect()
        except RuntimeError:
            pass
        self.tintCorrection = self.slider2Tint(self.sliderTint.value())
        # get multipliers (temperatureAndTint2Multipliers returns the camera neutral)
        multipliers = [
            1 / m
            for m in temperatureAndTint2Multipliers(self.tempCorrection,
                                                    1.0,
                                                    self.XYZ2CameraMatrix,
                                                    dngDict=self.dngDict)
        ]  # TODO 6/12/19 added keyword dngDict validate
        multipliers[1] *= self.tintCorrection
        self.rawMultipliers = multipliers
        m = multipliers[1]
        self.rawMultipliers = [self.rawMultipliers[i] / m for i in range(4)]
        self.dataChanged.emit(1)
        self.sliderTint.valueChanged.connect(self.tintUpdate)
        self.sliderTint.sliderReleased.connect(lambda: self.tintUpdate(
            self.sliderTint.value()))  # signal has no parameter)

    def setRawMultipliers(self, m0, m1, m2, sampling=True):
        mi = min(m0, m1, m2)
        m0, m1, m2 = m0 / mi, m1 / mi, m2 / mi
        self.rawMultipliers = [m0, m1, m2, m1]
        invMultipliers = [1 / self.rawMultipliers[i] for i in range(3)]
        try:
            self.sliderTemp.valueChanged.disconnect()
            self.sliderTint.valueChanged.disconnect()
        except RuntimeError:
            pass
        # get temp and tint
        temp, tint = multipliers2TemperatureAndTint(*invMultipliers,
                                                    self.XYZ2CameraMatrix)
        self.tintCorrection = tint
        self.sliderTemp.setValue(self.temp2Slider(temp))
        self.sliderTint.setValue(self.tint2Slider(tint))
        self.tempValue.setText(
            str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value()))))
        self.tintValue.setText(
            str("{:.0f}".format(self.sliderTint2User(
                self.sliderTint.value()))))
        self.sliderTemp.valueChanged.connect(self.tempUpdate)
        self.sliderTint.valueChanged.connect(self.tintUpdate)
        self.sampleMultipliers = sampling
        self.dataChanged.emit(1)

    def updateLayer(self, level):
        """
        data changed event handler.
        @param level: 3: redo contrast and saturation, 2: previous + camera profile stuff, 1: all
        @type level: int
        """
        if level == 1:
            # force all
            self.layer.bufCache_HSV_CV32 = None
            self.layer.postProcessCache = None
        elif level == 2:
            # force camera profile stuff
            self.layer.bufCache_HSV_CV32 = None
        elif level == 3:
            # keep the 2 cache buffers
            pass
        # contrast curve
        cf = getattr(self, 'dockC', None)
        if cf is not None:
            if self.options['manualCurve']:
                cf.showNormal()
            else:
                cf.hide()
        # tone curve
        ct = getattr(self, 'dockT', None)
        if ct is not None:
            if self.options['cpToneCurve']:
                ct.showNormal()
            else:
                ct.hide()
        self.enableSliders()
        self.layer.applyToStack()
        self.layer.parentImage.onImageChanged()

    def enableSliders(self):
        useUserWB = self.listWidget2.options["User WB"]
        useUserExp = not self.listWidget1.options["Auto Brightness"]
        self.sliderTemp.setEnabled(useUserWB)
        self.sliderTint.setEnabled(useUserWB)
        self.sliderExp.setEnabled(useUserExp)
        # self.sliderHigh.setEnabled(useUserExp)
        self.tempValue.setEnabled(self.sliderTemp.isEnabled())
        self.tintValue.setEnabled(self.sliderTint.isEnabled())
        self.expValue.setEnabled(self.sliderExp.isEnabled())
        # self.highValue.setEnabled(self.sliderHigh.isEnabled())
        self.tempLabel.setEnabled(self.sliderTemp.isEnabled())
        self.tintLabel.setEnabled(self.sliderTint.isEnabled())
        self.expLabel.setEnabled(self.sliderExp.isEnabled())
        # self.highLabel.setEnabled(self.sliderHigh.isEnabled())

    def setDefaults(self):
        self.dngDict = self.cameraProfilesCombo.itemData(0)
        self.listWidget1.unCheckAll()
        self.listWidget2.unCheckAll()
        self.listWidget1.checkOption(self.listWidget1.intNames[0])
        self.listWidget1.checkOption(self.listWidget1.intNames[1])
        self.listWidget2.checkOption(self.listWidget2.intNames[1])
        self.listWidget3.checkOption(self.listWidget3.intNames[0])
        self.enableSliders()
        self.denoiseValue = 0  # denoising off
        self.overexpValue = 0  # clip
        self.tempCorrection = self.asShotTemp
        self.tintCorrection = 1.0
        self.expCorrection = 1.0
        # self.highCorrection = 3.0  # restoration of overexposed highlights. 0: clip 1:unclip, 2: blend, 3...: rebuild
        self.contCorrection = 0.0
        # self.noiseCorrection = 0
        self.satCorrection = 0.0
        self.brCorrection = 1.0
        # prevent multiple updates
        try:
            self.dataChanged.disconnect()
        except RuntimeError:
            pass
        self.sliderTemp.setValue(round(self.temp2Slider(self.tempCorrection)))
        self.sliderTint.setValue(round(self.tint2Slider(self.tintCorrection)))
        self.sliderExp.setValue(self.exp2Slider(self.expCorrection))
        # self.sliderHigh.setValue(self.highCorrection)
        self.sliderCont.setValue(self.cont2Slider(self.contCorrection))
        self.sliderBrightness.setValue(self.br2Slider(self.brCorrection))
        self.sliderSat.setValue(self.sat2Slider(self.satCorrection))
        self.dataChanged.connect(self.updateLayer)

    def setCameraProfilesCombo(self):
        """
        Populates the camera profile Combo box.
        for each item, text is the filename and data is the corresponding dict.
        The function returns as soon as a first item is loaded. Remainning profiles are
        loaded asynchronously.
        @return: the currently selected item data
        @rtype: dict
        """
        self.cameraProfilesCombo = QComboBox()
        files = [self.targetImage.filename]
        files.extend(getDngProfileList(self.targetImage.cameraModel()))
        if not files:
            self.cameraProfilesCombo.addItem('None', {})
            return {}
        # load a first profile
        nextInd, found = 0, False
        while nextInd < len(files) and not found:
            f = files[nextInd]
            key = basename(f)[:-4] if nextInd > 0 else 'Embedded Profile'
            d = getDngProfileDict(f)
            # filter d
            d = {k: d[k] for k in d if d[k] != ''}
            if d:
                self.cameraProfilesCombo.addItem(key, d)
                found = True
            nextInd += 1

        def load():
            # load remaining profiles
            for i, f in enumerate(files[nextInd:]):
                key = basename(
                    f)[:-4] if i + nextInd > 0 else 'Embedded Profile'
                d = getDngProfileDict(f)
                # filter d
                d = {k: d[k] for k in d if d[k] != ''}
                if d:
                    self.cameraProfilesCombo.addItem(key, d)
            self.cameraProfilesCombo.addItem('None', {})

        threading.Thread(target=load).start()

        self.cameraProfilesCombo.setSizeAdjustPolicy(
            QComboBox.SizeAdjustPolicy.AdjustToContents)
        self.cameraProfilesCombo.setMaximumWidth(150)
        self.cameraProfilesCombo.setStyleSheet(
            "QComboBox QAbstractItemView { min-width: 250px;}")
        # return the currently selected item data
        return self.cameraProfilesCombo.itemData(0)
Example #11
0
    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(layer=layer, targetImage=targetImage, parent=parent)

        #######################################
        # Libraw correspondences:
        # rgb_xyz_matrix is libraw cam_xyz
        # camera_whitebalance is libraw cam_mul
        # daylight_whitebalance is libraw pre_mul
        # dng correspondences:
        # ASSHOTNEUTRAL tag value is (X,Y,Z) =  1 / rawpyObj.camera_whitebalance
        ##########################################
        rawpyObj = layer.parentImage.rawImage

        # constants and as shot values
        self.XYZ2CameraMatrix = rawpyObj.rgb_xyz_matrix[:3, :]
        self.XYZ2CameraInverseMatrix = np.linalg.inv(self.XYZ2CameraMatrix)
        # initial post processing multipliers (as shot)
        m1, m2, m3, m4 = rawpyObj.camera_whitebalance
        self.asShotMultipliers = (
            m1 / m2, 1.0, m3 / m2, m4 / m2
        )  # normalization is mandatory : for nef files white balance is around 256
        self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint(
            *1 / np.array(self.asShotMultipliers[:3]), self.XYZ2CameraMatrix)
        self.rawMultipliers = self.asShotMultipliers  # rawpyObj.camera_whitebalance # = 1/(dng ASSHOTNEUTRAL tag value)
        self.sampleMultipliers = False
        self.samples = []
        ########################################
        # XYZ-->Camera conversion matrix:
        # Last row is zero for RGB cameras (cf. rawpy and libraw docs).
        # type ndarray, shape (4,3)
        #########################################

        # attributes initialized in setDefaults, declared here for the sake of correctness
        self.tempCorrection, self.tintCorrection, self.expCorrection, self.highCorrection,\
                                                   self.contCorrection, self.satCorrection, self.brCorrection = [None] * 7
        # contrast spline vie (initialized by setContrastSpline)
        self.contrastForm = None
        # tone spline view (initialized by setToneSpline)
        self.toneForm = None
        # dock containers for contrast and tome forms
        self.dockC, self.dockT = None, None
        # options
        optionList0, optionNames0 = ['Auto Brightness', 'Preserve Highlights'
                                     ], ['Auto Expose', 'Preserve Highlights']
        optionList1, optionNames1 = ['Auto WB', 'Camera WB', 'User WB'
                                     ], ['Auto', 'Camera (As Shot)', 'User']
        optionList2, optionNames2 = [
            'cpLookTable', 'cpToneCurve', 'manualCurve'
        ], [
            'Use Camera Profile Look Table', 'Show Tone Curves',
            'Show Contrast Curve'
        ]
        self.listWidget1 = optionsWidget(
            options=optionList0,
            optionNames=optionNames0,
            exclusive=False,
            changed=lambda: self.dataChanged.emit(1))
        self.listWidget2 = optionsWidget(
            options=optionList1,
            optionNames=optionNames1,
            exclusive=True,
            changed=lambda: self.dataChanged.emit(1))
        self.listWidget3 = optionsWidget(
            options=optionList2,
            optionNames=optionNames2,
            exclusive=False,
            changed=lambda: self.dataChanged.emit(2))
        self.options = UDict(
            (self.listWidget1.options, self.listWidget2.options,
             self.listWidget3.options))
        # display the 'as shot' temperature
        item = self.listWidget2.item(1)
        item.setText(item.text() + ' : %d' % self.asShotTemp)

        # temperature slider
        self.sliderTemp = QbLUeSlider(Qt.Horizontal)
        self.sliderTemp.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultColorStylesheet)
        self.sliderTemp.setRange(0, 100)
        self.sliderTemp.setSingleStep(1)

        self.tempLabel = QLabel()
        self.tempLabel.setText("Temp")

        self.tempValue = QLabel()
        font = self.tempValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("10000")
        h = metrics.height()
        self.tempValue.setMinimumSize(w, h)
        self.tempValue.setMaximumSize(w, h)
        self.tempValue.setText(
            str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value()))))

        self.sliderTemp.valueChanged.connect(
            self.tempUpdate)  # signal send new value as parameter
        self.sliderTemp.sliderReleased.connect(lambda: self.tempUpdate(
            self.sliderTemp.value()))  # signal pass no parameter

        # tint slider
        self.sliderTint = QbLUeSlider(Qt.Horizontal)
        self.sliderTint.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultIMGColorStylesheet)
        self.sliderTint.setRange(0, 150)

        self.sliderTint.setSingleStep(1)

        self.tintLabel = QLabel()
        self.tintLabel.setText("Tint")

        self.tintValue = QLabel()
        font = self.tempValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.tintValue.setMinimumSize(w, h)
        self.tintValue.setMaximumSize(w, h)
        self.tintValue.setText(
            str("{:.0f}".format(self.sliderTint2User(
                self.sliderTint.value()))))

        self.sliderTint.valueChanged.connect(self.tintUpdate)
        self.sliderTint.sliderReleased.connect(lambda: self.tintUpdate(
            self.sliderTint.value()))  # signal pass no parameter)

        ######################
        # From libraw and dcraw sources:
        # Exposure and brightness are curve transformations.
        # Exposure curve is y = alpha*x, with cubic root ending; it is applied before demosaicing.
        # Brightness is (similar to) y = x**alpha and part of gamma transformation from linear sRGB to RGB.
        # Exposure and brightness both dilate the histogram towards highlights.
        # Exposure dilatation is uniform (homothety), brightness dilataion is
        # maximum for the midtones and the highlghts are preserved.
        # As a consequence, normal workflow begins with the adjustment of exposure,
        # to fill the entire range of the histogram and to adjust the highlights. Next,
        # one adjusts the brightness to put the midtones at the level we want them to be.
        # Cf. https://www.cambridgeincolour.com/forums/thread653.htm
        #####################

        # profile combo
        self.dngDict = self.setCameraProfilesCombo()

        # cameraProfilesCombo index changed event handler

        def cameraProfileUpdate(value):
            self.dngDict = self.cameraProfilesCombo.itemData(value)
            if self.options['cpToneCurve']:
                toneCurve = dngProfileToneCurve(
                    self.dngDict.get('ProfileToneCurve', []))
                self.toneForm.baseCurve = [
                    QPointF(x * axeSize, -y * axeSize)
                    for x, y in zip(toneCurve.dataX, toneCurve.dataY)
                ]
                self.toneForm.update()
            # recompute as shot temp and tint using new profile
            self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint(
                *1 / np.array(self.asShotMultipliers[:3]),
                self.XYZ2CameraMatrix,
                dngDict=self.dngDict
            )  # TODO 6/12/19 added keyword dngDict validate
            # display updated as shot temp
            item = self.listWidget2.item(1)
            item.setText(item.text().split(":")[0] + ': %d' % self.asShotTemp)
            # invalidate cache
            self.layer.bufCache_HSV_CV32 = None
            self.dataChanged.emit(2)  # 2 = no postprocessing

        self.cameraProfilesCombo.currentIndexChanged.connect(
            cameraProfileUpdate)

        # denoising combo
        self.denoiseCombo = QComboBox()
        items = OrderedDict([('Off', 0), ('Medium', 1), ('Full', 2)])
        for key in items:
            self.denoiseCombo.addItem(key, items[key])

        # denoiseCombo index changed event handler
        def denoiseUpdate(value):
            self.denoiseValue = self.denoiseCombo.itemData(value)
            self.dataChanged.emit(1)

        self.denoiseCombo.currentIndexChanged.connect(denoiseUpdate)

        # overexposed area restoration
        self.overexpCombo = QComboBox()
        items = OrderedDict([('Clip', 0), ('Ignore', 1), ('Blend', 2),
                             ('Reconstruct', 3)])
        for key in items:
            self.overexpCombo.addItem(key, items[key])

        # overexpCombo index changed event handler
        def overexpUpdate(value):
            self.overexpValue = self.overexpCombo.itemData(value)
            self.dataChanged.emit(1)

        self.overexpCombo.currentIndexChanged.connect(overexpUpdate)

        # exp slider
        self.sliderExp = QbLUeSlider(Qt.Horizontal)
        self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.sliderExp.setRange(0, 100)

        self.sliderExp.setSingleStep(1)

        self.expLabel = QLabel()
        self.expLabel.setText("Exp.")

        self.expValue = QLabel()
        font = self.expValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("+1.0")
        h = metrics.height()
        self.expValue.setMinimumSize(w, h)
        self.expValue.setMaximumSize(w, h)
        self.expValue.setText(
            str("{:.1f}".format(self.slider2Exp(self.sliderExp.value()))))

        # exp done event handler
        def expUpdate(value):
            self.expValue.setText(
                str("{:+.1f}".format(
                    self.sliderExp2User(self.sliderExp.value()))))
            # move not yet terminated or value not modified
            if self.sliderExp.isSliderDown() or self.slider2Exp(
                    value) == self.expCorrection:
                return
            try:
                self.sliderExp.valueChanged.disconnect()
                self.sliderExp.sliderReleased.disconnect()
            except RuntimeError:
                pass
            # rawpy: expCorrection range is -2.0...3.0, boiling down to exp_shift range 2**(-2)=0.25...2**3=8.0
            self.expCorrection = self.slider2Exp(self.sliderExp.value())
            self.dataChanged.emit(1)
            self.sliderExp.valueChanged.connect(
                expUpdate)  # send new value as parameter
            self.sliderExp.sliderReleased.connect(lambda: expUpdate(
                self.sliderExp.value()))  # signal pass no parameter

        self.sliderExp.valueChanged.connect(
            expUpdate)  # send new value as parameter
        self.sliderExp.sliderReleased.connect(lambda: expUpdate(
            self.sliderExp.value()))  # signal pass no parameter

        # brightness slider
        brSlider = QbLUeSlider(Qt.Horizontal)
        brSlider.setRange(1, 101)

        self.sliderExp.setSingleStep(1)

        brSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet)

        self.sliderBrightness = brSlider
        brLabel = QLabel()
        brLabel.setText("Bright.")

        self.brValue = QLabel()
        font = self.expValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("+99")
        h = metrics.height()
        self.brValue.setMinimumSize(w, h)
        self.brValue.setMaximumSize(w, h)
        self.brValue.setText(
            str("{:+d}".format(
                int(self.brSlider2User(self.sliderBrightness.value())))))

        # brightness done event handler
        def brUpdate(value):
            self.brValue.setText(
                str("{:+d}".format(
                    int(self.brSlider2User(self.sliderBrightness.value())))))
            # move not yet terminated or value not modified
            if self.sliderBrightness.isSliderDown() or self.slider2Br(
                    value) == self.brCorrection:
                return
            try:
                self.sliderBrightness.valueChanged.disconnect()
                self.sliderBrightness.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.brCorrection = self.slider2Br(self.sliderBrightness.value())
            self.dataChanged.emit(1)
            self.sliderBrightness.sliderReleased.connect(
                lambda: brUpdate(self.sliderBrightness.value()))
            self.sliderBrightness.valueChanged.connect(
                brUpdate)  # send new value as parameter

        self.sliderBrightness.valueChanged.connect(
            brUpdate)  # send new value as parameter
        self.sliderBrightness.sliderReleased.connect(
            lambda: brUpdate(self.sliderBrightness.value()))

        # contrast slider
        self.sliderCont = QbLUeSlider(Qt.Horizontal)
        self.sliderCont.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultBWStylesheet)
        self.sliderCont.setRange(0, 20)

        self.sliderCont.setSingleStep(1)

        self.contLabel = QLabel()
        self.contLabel.setText("Cont.")

        self.contValue = QLabel()
        font = self.contValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("100")
        h = metrics.height()
        self.contValue.setMinimumSize(w, h)
        self.contValue.setMaximumSize(w, h)
        self.contValue.setText(
            str("{:.0f}".format(self.slider2Cont(self.sliderCont.value()))))

        # cont done event handler
        def contUpdate(value):
            self.contValue.setText(
                str("{:.0f}".format(self.slider2Cont(
                    self.sliderCont.value()))))
            # move not yet terminated or value not modified
            if self.sliderCont.isSliderDown() or self.slider2Cont(
                    value) == self.tempCorrection:
                return
            try:
                self.sliderCont.valueChanged.disconnect()
                self.sliderCont.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.contCorrection = self.slider2Cont(self.sliderCont.value())
            self.contValue.setText(str("{:+d}".format(self.contCorrection)))
            # force to recalculate the spline
            self.layer.autoSpline = True
            self.dataChanged.emit(
                3)  # no postprocessing and no camera profile stuff
            self.sliderCont.valueChanged.connect(
                contUpdate)  # send new value as parameter
            self.sliderCont.sliderReleased.connect(lambda: contUpdate(
                self.sliderCont.value()))  # signal has no parameter

        self.sliderCont.valueChanged.connect(
            contUpdate)  # send new value as parameter
        self.sliderCont.sliderReleased.connect(lambda: contUpdate(
            self.sliderCont.value()))  # signal has no parameter

        # saturation slider
        self.sliderSat = QbLUeSlider(Qt.Horizontal)
        self.sliderSat.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultColorStylesheet)
        self.sliderSat.setRange(0, 100)

        self.sliderSat.setSingleStep(1)

        satLabel = QLabel()
        satLabel.setText("Sat.")

        self.satValue = QLabel()
        font = self.satValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("+10")
        h = metrics.height()
        self.satValue.setMinimumSize(w, h)
        self.satValue.setMaximumSize(w, h)
        self.satValue.setText(
            str("{:+d}".format(self.slider2Sat(self.sliderSat.value()))))
        """sat done event handler"""

        def satUpdate(value):
            self.satValue.setText(
                str("{:+d}".format(self.slider2Sat(self.sliderSat.value()))))
            # move not yet terminated or value not modified
            if self.sliderSat.isSliderDown() or self.slider2Sat(
                    value) == self.satCorrection:
                return
            try:
                self.sliderSat.valueChanged.disconnect()
                self.sliderSat.sliderReleased.disconnect()
            except RuntimeError:
                pass
            self.satCorrection = self.slider2Sat(self.sliderSat.value())
            self.dataChanged.emit(
                3)  # no post processing and no camera profile stuff
            self.sliderSat.valueChanged.connect(
                satUpdate)  # send new value as parameter
            self.sliderSat.sliderReleased.connect(lambda: satUpdate(
                self.sliderSat.value()))  # signal has no parameter

        self.sliderSat.valueChanged.connect(
            satUpdate)  # send new value as parameter
        self.sliderSat.sliderReleased.connect(lambda: satUpdate(
            self.sliderSat.value()))  # signal has no parameter

        # layout
        l = QVBoxLayout()
        l.addWidget(self.listWidget3)
        hl01 = QHBoxLayout()
        hl01.addWidget(QLabel('Camera Profile'))
        hl01.addWidget(self.cameraProfilesCombo)
        l.addLayout(hl01)
        hl0 = QHBoxLayout()
        hl0.addWidget(QLabel('Denoising'))
        hl0.addWidget(self.denoiseCombo)
        l.addLayout(hl0)
        hl00 = QHBoxLayout()
        hl00.addWidget(QLabel('Overexp. Restoration'))
        hl00.addWidget(self.overexpCombo)
        l.addLayout(hl00)
        hl1 = QHBoxLayout()
        hl1.addWidget(self.expLabel)
        hl1.addWidget(self.expValue)
        hl1.addWidget(self.sliderExp)
        l.addLayout(hl1)
        hl8 = QHBoxLayout()
        hl8.addWidget(brLabel)
        hl8.addWidget(self.brValue)
        hl8.addWidget(self.sliderBrightness)
        l.addLayout(hl8)
        l.addWidget(self.listWidget1)
        vl1 = QVBoxLayout()
        vl1.addWidget(self.listWidget2)
        gb1 = QGroupBox()
        gb1.setTitle('White Balance')
        hl2 = QHBoxLayout()
        hl2.addWidget(self.tempLabel)
        hl2.addWidget(self.tempValue)
        hl2.addWidget(self.sliderTemp)
        hl3 = QHBoxLayout()
        hl3.addWidget(self.tintLabel)
        hl3.addWidget(self.tintValue)
        hl3.addWidget(self.sliderTint)
        vl1.addLayout(hl2)
        vl1.addLayout(hl3)
        gb1.setLayout(vl1)
        l.addWidget(gb1)
        hl4 = QHBoxLayout()
        hl4.addWidget(self.contLabel)
        hl4.addWidget(self.contValue)
        hl4.addWidget(self.sliderCont)
        hl7 = QHBoxLayout()
        hl7.addWidget(satLabel)
        hl7.addWidget(self.satValue)
        hl7.addWidget(self.sliderSat)

        # separator
        sep = QFrame()
        sep.setFrameShape(QFrame.HLine)
        sep.setFrameShadow(QFrame.Sunken)
        l.addWidget(sep)
        l.addLayout(hl4)
        l.addLayout(hl7)
        l.addStretch(1)
        self.setLayout(l)
        self.adjustSize()
        self.setDefaults()
        self.setWhatsThis("""<b>Development of raw files</b><br>
                    <b>Default settings</b> are a good starting point.<br>
                    A <b>Tone Curve</b> is applied to the raw image prior to postprocessing.<br> 
                    The cuvre can be edited by checking the option
                    <b>Show Tone Curve</b>; this option works best with manual exposure.<br>
                    <b>Contrast</b> correction is based on an automatic algorithm 
                    well suited to multi-mode histograms.<br>
                    <b>Brightness, Contrast</b> and <b>Saturation</b> levels</b> are 
                    adjustable with the correponding sliders.<br>
                    The <b>Contrast Curve</b> can be edited manually by checking 
                    the option <b>Show Contrast Curve</b>.<br>
                    Uncheck <b>Auto Expose</b> to adjust the exposure manually.<br>
                    The <b>OverExp. Rest.</b> slider controls the mode of restoration of overexposed areas. 
                    Valid values are 0 to 3 (0=clip;1=unclip;2=blend;3=rebuild); (with Auto Exposed 
                    checked the mode is clip).<br>
                    """)  # end of setWhatsThis
Example #12
0
    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(layer=layer, targetImage=targetImage, parent=parent)
        self.tempCorrection = 6500
        self.tintCorrection = 1.0
        self.filterColor = QColor(255, 255, 255)
        self.defaultTemp = sRGBWP  # ref temperature D65
        self.defaultTint = 0

        # options
        optionList, optionNames = [
            'Color Filter', 'Photo Filter', 'Chromatic Adaptation'
        ], ['Color Filter', 'Photo Filter', 'Chromatic Adaptation']
        self.listWidget1 = optionsWidget(
            options=optionList,
            optionNames=optionNames,
            exclusive=True,
            changed=lambda: self.dataChanged.emit())
        self.listWidget1.checkOption(self.listWidget1.intNames[0])
        self.options = self.listWidget1.options
        # link to app color dialog
        self.colorChooser = self.parent().colorChooser
        # color viewer
        self.colorLabel = QLabel()
        self.colorLabel.setMaximumSize(50, 50)
        # color chooser button
        self.colorChooserBtn = QbLUePushButton('Select Filter Color')
        self.colorChooserBtn.clicked.connect(self.showColorChooser)

        # temp slider
        self.sliderTemp = QbLUeSlider(Qt.Horizontal)
        self.sliderTemp.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultIColorStylesheet)
        self.sliderTemp.setRange(
            17, 100
        )  # 250)  # valid range for spline approximation is 1667..25000, cf. colorConv.temperature2xyWP
        self.sliderTemp.setSingleStep(1)

        self.tempLabel = QbLUeLabel()
        self.tempLabel.setMaximumSize(150, 30)
        self.tempLabel.setText("Filter Temperature")
        self.tempLabel.doubleClicked.connect(lambda: self.sliderTemp.setValue(
            self.temp2Slider(self.defaultTemp)))

        self.tempValue = QLabel()
        font = self.tempValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("00000")
        h = metrics.height()
        self.tempValue.setMinimumSize(w, h)
        self.tempValue.setMaximumSize(w, h)
        self.tempValue.setText(
            str("{:d}".format(self.sliderTemp2User(self.sliderTemp.value()))))

        # tint slider
        self.sliderTint = QbLUeSlider(Qt.Horizontal)
        self.sliderTint.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultMGColorStylesheet)
        self.sliderTint.setRange(
            0, 100
        )  # 250) # valid range for spline approximation is 1667..25000, cf. colorConv.temperature2xyWP
        self.sliderTint.setSingleStep(1)

        self.tintLabel = QbLUeLabel()
        self.tintLabel.setMaximumSize(150, 30)
        self.tintLabel.setText("Tint")
        self.tintLabel.doubleClicked.connect(lambda: self.sliderTint.setValue(
            self.tint2Slider(self.defaultTint)))

        self.tintValue = QLabel()
        font = self.tintValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("0000")
        h = metrics.height()
        self.tintValue.setMinimumSize(w, h)
        self.tintValue.setMaximumSize(w, h)
        self.tintValue.setText(
            str("{:d}".format(self.sliderTint2User(self.sliderTint.value()))))

        self.sliderTemp.valueChanged.connect(self.tempUpdate)
        self.sliderTemp.sliderReleased.connect(
            lambda: self.tempUpdate(self.sliderTemp.value()))
        self.sliderTint.valueChanged.connect(self.tintUpdate)
        self.sliderTint.sliderReleased.connect(
            lambda: self.tintUpdate(self.sliderTint.value()))

        # layout
        l = QVBoxLayout()
        l.setAlignment(Qt.AlignTop)
        l.addWidget(self.listWidget1)
        hl2 = QHBoxLayout()
        hl2.addWidget(self.colorLabel)
        hl2.addWidget(self.colorChooserBtn)
        l.addLayout(hl2)
        l.addWidget(self.tempLabel)
        hl = QHBoxLayout()
        hl.addWidget(self.tempValue)
        hl.addWidget(self.sliderTemp)
        l.addLayout(hl)
        l.addWidget(self.tintLabel)
        hl1 = QHBoxLayout()
        hl1.addWidget(self.tintValue)
        hl1.addWidget(self.sliderTint)
        l.addLayout(hl1)
        self.setLayout(l)
        self.adjustSize()
        self.setDefaults()
        self.setWhatsThis(
            """<b> Color Filter</b> and <b>Photo Filter</b> use the multiply blending mode
                        to mimic a warming or cooling filter put in front of the camera lens. 
                        The luminosity of the resulting image is corrected.<br>
                        <b>Chromatic Adaptation</b> uses multipliers in the linear sRGB
                        color space to adjust <b>temperature</b> and <b>tint</b>.
                        """)  # end of setWhatsThis
Example #13
0
class temperatureForm(baseForm):
    @classmethod
    def getNewWindow(cls,
                     targetImage=None,
                     axeSize=500,
                     layer=None,
                     parent=None):
        wdgt = temperatureForm(axeSize=axeSize, layer=layer, parent=parent)
        wdgt.setWindowTitle(layer.name)
        return wdgt

    def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None):
        super().__init__(layer=layer, targetImage=targetImage, parent=parent)
        self.tempCorrection = 6500
        self.tintCorrection = 1.0
        self.filterColor = QColor(255, 255, 255)
        self.defaultTemp = sRGBWP  # ref temperature D65
        self.defaultTint = 0

        # options
        optionList, optionNames = [
            'Color Filter', 'Photo Filter', 'Chromatic Adaptation'
        ], ['Color Filter', 'Photo Filter', 'Chromatic Adaptation']
        self.listWidget1 = optionsWidget(
            options=optionList,
            optionNames=optionNames,
            exclusive=True,
            changed=lambda: self.dataChanged.emit())
        self.listWidget1.checkOption(self.listWidget1.intNames[0])
        self.options = self.listWidget1.options
        # link to app color dialog
        self.colorChooser = self.parent().colorChooser
        # color viewer
        self.colorLabel = QLabel()
        self.colorLabel.setMaximumSize(50, 50)
        # color chooser button
        self.colorChooserBtn = QbLUePushButton('Select Filter Color')
        self.colorChooserBtn.clicked.connect(self.showColorChooser)

        # temp slider
        self.sliderTemp = QbLUeSlider(Qt.Horizontal)
        self.sliderTemp.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultIColorStylesheet)
        self.sliderTemp.setRange(
            17, 100
        )  # 250)  # valid range for spline approximation is 1667..25000, cf. colorConv.temperature2xyWP
        self.sliderTemp.setSingleStep(1)

        self.tempLabel = QbLUeLabel()
        self.tempLabel.setMaximumSize(150, 30)
        self.tempLabel.setText("Filter Temperature")
        self.tempLabel.doubleClicked.connect(lambda: self.sliderTemp.setValue(
            self.temp2Slider(self.defaultTemp)))

        self.tempValue = QLabel()
        font = self.tempValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("00000")
        h = metrics.height()
        self.tempValue.setMinimumSize(w, h)
        self.tempValue.setMaximumSize(w, h)
        self.tempValue.setText(
            str("{:d}".format(self.sliderTemp2User(self.sliderTemp.value()))))

        # tint slider
        self.sliderTint = QbLUeSlider(Qt.Horizontal)
        self.sliderTint.setStyleSheet(
            QbLUeSlider.bLueSliderDefaultMGColorStylesheet)
        self.sliderTint.setRange(
            0, 100
        )  # 250) # valid range for spline approximation is 1667..25000, cf. colorConv.temperature2xyWP
        self.sliderTint.setSingleStep(1)

        self.tintLabel = QbLUeLabel()
        self.tintLabel.setMaximumSize(150, 30)
        self.tintLabel.setText("Tint")
        self.tintLabel.doubleClicked.connect(lambda: self.sliderTint.setValue(
            self.tint2Slider(self.defaultTint)))

        self.tintValue = QLabel()
        font = self.tintValue.font()
        metrics = QFontMetrics(font)
        w = metrics.width("0000")
        h = metrics.height()
        self.tintValue.setMinimumSize(w, h)
        self.tintValue.setMaximumSize(w, h)
        self.tintValue.setText(
            str("{:d}".format(self.sliderTint2User(self.sliderTint.value()))))

        self.sliderTemp.valueChanged.connect(self.tempUpdate)
        self.sliderTemp.sliderReleased.connect(
            lambda: self.tempUpdate(self.sliderTemp.value()))
        self.sliderTint.valueChanged.connect(self.tintUpdate)
        self.sliderTint.sliderReleased.connect(
            lambda: self.tintUpdate(self.sliderTint.value()))

        # layout
        l = QVBoxLayout()
        l.setAlignment(Qt.AlignTop)
        l.addWidget(self.listWidget1)
        hl2 = QHBoxLayout()
        hl2.addWidget(self.colorLabel)
        hl2.addWidget(self.colorChooserBtn)
        l.addLayout(hl2)
        l.addWidget(self.tempLabel)
        hl = QHBoxLayout()
        hl.addWidget(self.tempValue)
        hl.addWidget(self.sliderTemp)
        l.addLayout(hl)
        l.addWidget(self.tintLabel)
        hl1 = QHBoxLayout()
        hl1.addWidget(self.tintValue)
        hl1.addWidget(self.sliderTint)
        l.addLayout(hl1)
        self.setLayout(l)
        self.adjustSize()
        self.setDefaults()
        self.setWhatsThis(
            """<b> Color Filter</b> and <b>Photo Filter</b> use the multiply blending mode
                        to mimic a warming or cooling filter put in front of the camera lens. 
                        The luminosity of the resulting image is corrected.<br>
                        <b>Chromatic Adaptation</b> uses multipliers in the linear sRGB
                        color space to adjust <b>temperature</b> and <b>tint</b>.
                        """)  # end of setWhatsThis

    def enableSliders(self):
        for item in [self.colorLabel, self.colorChooserBtn]:
            item.setEnabled(self.options['Color Filter'])
        for item in [self.sliderTemp, self.tempLabel]:
            item.setEnabled(not self.options['Color Filter'])
        for item in [self.sliderTint, self.tintLabel, self.tintValue]:
            item.setEnabled(self.options['Chromatic Adaptation'])

    def setDefaults(self):
        # prevent multiple updates
        try:
            self.dataChanged.disconnect()
        except RuntimeError:
            pass
        self.listWidget1.unCheckAll()
        self.listWidget1.checkOption(self.listWidget1.intNames[0])
        self.enableSliders()
        self.sliderTemp.setValue(round(self.temp2Slider(self.tempCorrection)))
        self.sliderTint.setValue(round(self.tint2Slider(self.defaultTint)))
        self.dataChanged.connect(self.updateLayer)

        self.filterColor = self.colorChooser.currentColor()
        # set colorLabel background
        self.colorLabel.setAutoFillBackground(True)
        colorstr = ''.join('%02x' % i for i in self.filterColor.getRgb()[:3])
        self.colorLabel.setStyleSheet("background:#%s" % colorstr)

    def colorUpdate(self, color):
        """
        color Changed slot
        @param color:
        @type color: QColor
        """
        self.dataChanged.emit()

    def tempUpdate(self, value):
        """
        temp change slot
        @param value:
        @type value: int
        """
        self.tempValue.setText(str("{:d}".format(self.sliderTemp2User(value))))
        # move not yet terminated or values not modified
        if self.sliderTemp.isSliderDown() or self.slider2Temp(
                value) == self.tempCorrection:
            return
        try:
            self.sliderTemp.valueChanged.disconnect()
            self.sliderTemp.sliderReleased.disconnect()
        except RuntimeError:
            pass
        self.tempCorrection = self.slider2Temp(value)
        self.dataChanged.emit()
        self.sliderTemp.valueChanged.connect(self.tempUpdate)
        self.sliderTemp.sliderReleased.connect(
            lambda: self.tempUpdate(self.sliderTemp.value()))

    def tintUpdate(self, value):
        """
        tint change slot
        @param value:
        @type value: int
        """
        self.tintValue.setText(str("{:d}".format(self.sliderTint2User(value))))
        # move not yet terminated or values not modified
        if self.sliderTint.isSliderDown() or self.slider2Tint(
                value) == self.tintCorrection:
            return
        try:
            self.sliderTint.valueChanged.disconnect()
            self.sliderTint.sliderReleased.disconnect()
        except RuntimeError:
            pass
        self.tintCorrection = self.slider2Tint(value)
        self.dataChanged.emit()
        self.sliderTint.valueChanged.connect(self.tintUpdate)
        self.sliderTint.sliderReleased.connect(
            lambda: self.tintUpdate(self.sliderTint.value()))

    def updateLayer(self):
        """
        data changed slot
        """
        self.enableSliders()
        self.layer.applyToStack()
        self.layer.parentImage.onImageChanged()

    def setFilterColor(self, color):
        """
        currentColorChanged slot
        @param color:
        @type color: QColor
        """
        self.filterColor = color
        self.colorLabel.setAutoFillBackground(True)
        colorstr = ''.join('%02x' % i for i in color.getRgb()[:3])
        self.colorLabel.setStyleSheet("background:#%s" % colorstr)

    def showColorChooser(self):
        self.colorChooser.show()
        try:
            self.colorChooser.currentColorChanged.disconnect()
        except RuntimeError:
            pass
        self.colorChooser.setCurrentColor(self.filterColor)
        self.colorChooser.currentColorChanged.connect(self.setFilterColor)
        self.colorChooser.colorSelected.connect(self.colorUpdate)

    @staticmethod
    def slider2Temp(v):
        return v * 100

    @staticmethod
    def temp2Slider(v):
        return int(v / 100)

    @staticmethod
    def sliderTemp2User(v):
        return v * 100

    @staticmethod
    def slider2Tint(v):
        return (v - 50) / 50

    @staticmethod
    def tint2Slider(v):
        return int((1.0 + v) * 50.0)

    @staticmethod
    def sliderTint2User(v):
        return int((v - 50) / 5.0)
Example #14
0
class savingDialog(QDialog):
    """
    File dialog with options.
    We use a standard QFileDialog as a child widget and we
    forward its methods to the top level.
    """
    def __init__(self, parent, text, lastDir):
        """

        @param parent:
        @type parent: QObject
        @param text:
        @type text: str
        @param lastDir:
        @type lastDir:str
        """
        # QDialog __init__
        super().__init__()
        self.setWindowTitle(text)
        # File Dialog
        self.dlg = QFileDialog(caption=text, directory=lastDir)
        self.dlg.setOption(QFileDialog.DontUseNativeDialog)
        self.metaOption = QCheckBox('Remove Meta')
        # sliders
        self.sliderComp = QbLUeSlider(Qt.Horizontal)
        self.sliderComp.setTickPosition(QSlider.TicksBelow)
        self.sliderComp.setRange(0, 9)
        self.sliderComp.setSingleStep(1)
        self.sliderComp.setValue(
            3
        )  # 3 = default opencv imwrite value  TODO changed 5 to 3 20/02/20 validate
        self.sliderQual = QbLUeSlider(Qt.Horizontal)
        self.sliderQual.setTickPosition(QSlider.TicksBelow)
        self.sliderQual.setRange(0, 100)
        self.sliderQual.setSingleStep(10)
        self.sliderQual.setValue(
            95
        )  # 95 = default opencv imwrite value  # TODO changed 90 to 95 20/02/20 validate
        self.dlg.setVisible(True)
        l = QVBoxLayout()
        h = QHBoxLayout()
        l.addWidget(self.dlg)
        h.addWidget(self.metaOption)
        h.addWidget(QLabel("Quality"))
        h.addWidget(self.sliderQual)
        h.addWidget(QLabel("Compression"))
        h.addWidget(self.sliderComp)
        l.addLayout(h)
        self.setLayout(l)

        # file dialog close event handler
        def f():
            self.close()

        self.dlg.finished.connect(f)

    def exec_(self):
        # QDialog exec_
        super().exec_()
        # forward file dialog result
        return self.dlg.result()

    def selectFile(self, fileName):
        self.dlg.selectFile(fileName)

    def selectedFiles(self):
        return self.dlg.selectedFiles()

    def directory(self):
        return self.dlg.directory()