class ExpForm(baseForm): defaultExpCorrection = 0.0 defaultStep = 0.1 @classmethod def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None): wdgt = ExpForm(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) # 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 updateLayer(self): self.layer.applyToStack() self.layer.parentImage.onImageChanged() def setDefaults(self): try: self.dataChanged.disconnect() except RuntimeError: pass self.sliderExp.setValue(self.defaultExpCorrection) self.expValue.setText(str("{:+.1f}".format(self.defaultExpCorrection))) self.expCorrection = self.defaultExpCorrection * self.defaultStep self.dataChanged.connect(self.updateLayer) """
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())
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()
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()
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)
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)