Пример #1
0
class filterForm(baseForm):

    defaultRadius = 10
    defaultTone = 100.0
    defaultAmount = 50.0

    @classmethod
    def getNewWindow(cls,
                     targetImage=None,
                     axeSize=500,
                     layer=None,
                     parent=None):
        wdgt = filterForm(targetImage=targetImage,
                          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)
        # 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

    def setDefaults(self):
        self.enableSliders()
        try:
            self.dataChanged.disconnect()
        except RuntimeError:
            pass
        self.sliderRadius.setValue(self.defaultRadius)
        self.sliderAmount.setValue(self.defaultAmount)
        self.sliderTone.setValue(self.defaultTone)
        self.dataChanged.connect(self.updateLayer)

    def updateLayer(self):
        """
        dataChanged Slot
        """
        self.enableSliders()
        for key in self.listWidget1.options:
            if self.listWidget1.options[key]:
                self.kernelCategory = self.filterDict[key]
                break
        self.layer.applyToStack()
        self.layer.parentImage.onImageChanged()

    def enableSliders(self):
        opt = self.listWidget1.options
        useRadius = opt[self.optionList[0]] or opt[self.optionList[2]] or opt[
            self.optionList[3]]
        useAmount = opt[self.optionList[0]] or opt[self.optionList[2]]
        useTone = opt[self.optionList[3]]
        self.sliderRadius.setEnabled(useRadius)
        self.sliderAmount.setEnabled(useAmount)
        self.sliderTone.setEnabled(useTone)
        self.radiusValue.setEnabled(self.sliderRadius.isEnabled())
        self.amountValue.setEnabled(self.sliderAmount.isEnabled())
        self.toneValue.setEnabled(self.sliderTone.isEnabled())
        self.radiusLabel.setEnabled(self.sliderRadius.isEnabled())
        self.amountLabel.setEnabled(self.sliderAmount.isEnabled())
        self.toneLabel.setEnabled(self.sliderTone.isEnabled())
Пример #2
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)