def __init__(self, parent, text, lastDir): """ @param parent: @type parent: QObject @param text: @type text: str @param lastDir: @type lastDir:str """ # QDialog __init__ super().__init__() self.setWindowTitle(text) # File Dialog self.dlg = QFileDialog(caption=text, directory=lastDir) # sliders self.sliderComp = QbLUeSlider(Qt.Horizontal) self.sliderComp.setTickPosition(QSlider.TicksBelow) self.sliderComp.setRange(0, 9) self.sliderComp.setSingleStep(1) self.sliderComp.setValue(5) self.sliderQual = QbLUeSlider(Qt.Horizontal) self.sliderQual.setTickPosition(QSlider.TicksBelow) self.sliderQual.setRange(0, 100) self.sliderQual.setSingleStep(10) self.sliderQual.setValue(90) self.dlg.setVisible(True) l = QVBoxLayout() h = QHBoxLayout() l.addWidget(self.dlg) h.addWidget(QLabel("Quality")) h.addWidget(self.sliderQual) h.addWidget(QLabel("Compression")) h.addWidget(self.sliderComp) l.addLayout(h) self.setLayout(l) # file dialog close event handler def f(): self.close() self.dlg.finished.connect(f)
def __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()
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) def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) outStream.writeQString(self.listWidget1.selectedItems()[0].text()) outStream.writeInt32(self.sliderExp.value()) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() temp = inStream.readInt32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderExp.setValue(temp) self.update() return inStream
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 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, -axeSize // 2) 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> x-axes represent hue values from 0 to 360. The upper curve shows brightness multiplicative shifts (initially 1) and the lower curve hue additive shifts (initially 0). All pixel colors are changed by the specific shifts corresponding to their hue. 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 __init__(self, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): super().__init__(parent=parent) self.tempCorrection = 6500 self.tintCorrection = 1.0 self.setStyleSheet( 'QRangeSlider * {border: 0px; padding: 0px; margin: 0px}') self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) # link back to image layer self.layer = weakProxy(layer) """ # using weak ref for back links if type(layer) in weakref.ProxyTypes: self.layer = layer else: self.layer = weakref.proxy(layer) """ self.defaultTemp = sRGBWP # ref temperature D65 self.defaultTint = 0 # options optionList, optionNames = ['Photo Filter', 'Chromatic Adaptation' ], ['Photo Filter', 'Chromatic Adaptation'] self.listWidget1 = optionsWidget( options=optionList, optionNames=optionNames, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[1]) self.options = self.listWidget1.options # 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) tempLabel = QbLUeLabel() tempLabel.setMaximumSize(150, 30) tempLabel.setText("Color temperature") tempLabel.doubleClicked.connect(lambda: self.sliderTemp.setValue( self.temp2Slider(self.defaultTemp))) self.tempValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("0000") 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) tintLabel = QbLUeLabel() tintLabel.setMaximumSize(150, 30) tintLabel.setText("Tint") 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())))) # temp change event handler def tempUpdate(value): 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 self.sliderTemp.valueChanged.disconnect() self.sliderTemp.sliderReleased.disconnect() self.tempCorrection = self.slider2Temp(value) self.dataChanged.emit() self.sliderTemp.valueChanged.connect(tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: tempUpdate(self.sliderTemp.value())) # tint change event handler def tintUpdate(value): 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 self.sliderTint.valueChanged.disconnect() self.sliderTint.sliderReleased.disconnect() self.tintCorrection = self.slider2Tint(value) self.dataChanged.emit() self.sliderTint.valueChanged.connect(tintUpdate) self.sliderTint.sliderReleased.connect( lambda: tintUpdate(self.sliderTint.value())) self.sliderTemp.valueChanged.connect(tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: tempUpdate(self.sliderTemp.value())) self.sliderTint.valueChanged.connect(tintUpdate) self.sliderTint.sliderReleased.connect( lambda: tintUpdate(self.sliderTint.value())) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(self.listWidget1) l.addWidget(tempLabel) hl = QHBoxLayout() hl.addWidget(self.tempValue) hl.addWidget(self.sliderTemp) l.addLayout(hl) l.addWidget(tintLabel) hl1 = QHBoxLayout() hl1.addWidget(self.tintValue) hl1.addWidget(self.sliderTint) l.addLayout(hl1) self.setLayout(l) self.adjustSize() self.dataChanged.connect( self.updateLayer) # TODO move to setDefaults 3/12/18 self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") self.setDefaults() self.setWhatsThis("""<b>Color Temperature</b><br> <b>Photo Filter</b> uses the multiply blending mode to mimic a color filter in front of the camera lens.<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
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()) def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) outStream.writeQString(self.listWidget1.selectedItems()[0].text()) outStream.writeFloat32(self.sliderRadius.value()) outStream.writeFloat32(self.sliderAmount.value()) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() radius = inStream.readFloat32() amount = inStream.readFloat32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderRadius.setValue(radius) self.sliderAmount.setValue(amount) self.repaint() return inStream
class noiseForm(baseForm): dataChanged = QtCore.Signal(bool) @classmethod def getNewWindow(cls, axeSize=500, layer=None, parent=None): wdgt = noiseForm(axeSize=axeSize, layer=layer, parent=parent) wdgt.setWindowTitle(layer.name) return wdgt def __init__(self, axeSize=500, layer=None, parent=None): super(noiseForm, self).__init__(parent=parent) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) # link back to image layer # using weak ref for back links if type(layer) in weakref.ProxyTypes: self.layer = layer else: self.layer = weakref.proxy(layer) # attribute initialized in setDefaults # defined here for the sake of correctness self.noiseCorrection = 0 # options optionList = ['Wavelets', 'Bilateral', 'NLMeans'] self.listWidget1 = optionsWidget( options=optionList, exclusive=True, changed=lambda: self.dataChanged.emit(True)) self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.options = self.listWidget1.options # threshold slider self.sliderThr = QbLUeSlider(Qt.Horizontal) self.sliderThr.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderThr.setTickPosition(QSlider.TicksBelow) self.sliderThr.setRange(0, 10) self.sliderThr.setSingleStep(1) self.sliderThr.valueChanged.connect(self.thrUpdate) self.sliderThr.sliderReleased.connect(lambda: self.thrUpdate( self.sliderThr.value())) # signal has no parameter) self.thrLabel = QLabel() self.thrLabel.setMaximumSize(150, 30) self.thrLabel.setText("level") self.thrValue = QLabel() font = self.thrValue.font() metrics = QFontMetrics(font) w = metrics.width("0000") h = metrics.height() self.thrValue.setMinimumSize(w, h) self.thrValue.setMaximumSize(w, h) self.thrValue.setText( str("{:.0f}".format(self.slider2Thr(self.sliderThr.value())))) # self.dataChanged.connect(self.updateLayer) # self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") # layout l = QVBoxLayout() #l.setAlignment(Qt.AlignBottom) l.addWidget(self.listWidget1) hl1 = QHBoxLayout() hl1.addWidget(self.thrLabel) hl1.addWidget(self.thrValue) hl1.addWidget(self.sliderThr) l.addLayout(hl1) l.setContentsMargins(20, 0, 20, 25) # left, top, right, bottom #l.setContentsMargins(10, 10, 10, 10) # left, top, right, bottom self.setLayout(l) self.adjustSize() self.setDefaults() self.setWhatsThis("""<b>Noise Reduction</b><br> <b>Bilateral Filtering</b> is the fastest method.<br> <b>NLMeans</b> (Non Local Means) and <b>Wavelets</b> are slower, but they usually give better results.<br> It is possible to <b>limit the application of all methods to a rectangular region of the image</b> by drawing a selection rectangle on the layer with the marquee tool.<br> Ctrl-Click to <b>clear the selection</b><br> """) # end of setWhatsThis def setDefaults(self): self.listWidget1.unCheckAll() self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.noiseCorrection = 0 # prevent multiple updates try: self.dataChanged.disconnect() except RuntimeError: pass self.sliderThr.setValue(round(self.thr2Slider(self.noiseCorrection))) self.dataChanged.connect(self.updateLayer) self.dataChanged.emit(True) def updateLayer(self, invalidate): """ data changed event handler """ #if invalidate: #self.layer.postProcessCache = None # TODO 2/11/18 unused validate #self.enableSliders() self.layer.applyToStack() self.layer.parentImage.onImageChanged() def slider2Thr(self, v): return v def thr2Slider(self, t): return t def thrUpdate(self, value): self.thrValue.setText( str("{:.0f}".format(self.slider2Thr(self.sliderThr.value())))) # move not yet terminated or value not modified if self.sliderThr.isSliderDown() or self.slider2Thr( value) == self.noiseCorrection: return self.sliderThr.valueChanged.disconnect() self.sliderThr.sliderReleased.disconnect() self.noiseCorrection = self.slider2Thr(self.sliderThr.value()) self.thrValue.setText(str("{:+d}".format(self.noiseCorrection))) self.dataChanged.emit(False) self.sliderThr.valueChanged.connect( self.thrUpdate) # send new value as parameter self.sliderThr.sliderReleased.connect(lambda: self.thrUpdate( self.sliderThr.value())) # signal has no parameter def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) outStream.writeQString(self.listWidget1.selectedItems()[0].text()) outStream.writeInt32(self.sliderTemp.value() * 100) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() temp = inStream.readInt32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderTemp.setValue(temp // 100) self.update() return inStream
class QLayerView(QTableView): """ Display the stack of image layers. """ def __init__(self, parent): super(QLayerView, self).__init__(parent) self.img = None # graphic form to show : it # should correspond to the currently selected layer self.currentWin = None # mouse click event self.clicked.connect(self.viewClicked) # set behavior and styles self.setSelectionBehavior(QAbstractItemView.SelectRows) delegate = itemDelegate(parent=self) self.setItemDelegate(delegate) ic1 = QImage(":/images/resources/eye-icon.png") ic2 = QImage(":/images/resources/eye-icon-strike.png") delegate.px1 = QPixmap.fromImage(ic1) delegate.px2 = QPixmap.fromImage(ic2) ic1.invertPixels() ic2.invertPixels() delegate.inv_px1 = QPixmap.fromImage(ic1) delegate.inv_px2 = QPixmap.fromImage(ic2) self.setIconSize(QSize(20, 15)) self.verticalHeader().setMinimumSectionSize(-1) self.verticalHeader().setDefaultSectionSize( self.verticalHeader().minimumSectionSize()) self.horizontalHeader().setMinimumSectionSize(40) self.horizontalHeader().setDefaultSectionSize(40) # drag and drop self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setDragDropOverwriteMode(False) self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) ################################ # layer property GUI : # preview, blending mode, opacity, mask color ################################ # Preview option # We should use a QListWidget or a custom optionsWidget # (cf. utils.py) : adding it to QVBoxLayout with mode # Qt.AlignBottom does not work. self.previewOptionBox = QCheckBox('Preview') self.previewOptionBox.setMaximumSize(100, 30) # View/Preview changed event handler def m(state): # state : Qt.Checked Qt.UnChecked if self.img is None: return self.img.useThumb = (state == Qt.Checked) window.updateStatus() self.img.cacheInvalidate() for layer in self.img.layersStack: layer.autoclone = True # auto update cloning layers layer.knitted = False try: QApplication.setOverrideCursor( Qt.WaitCursor ) # TODO 18/04/18 waitcursor is called by applytostack? QApplication.processEvents() # update the whole stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() # TODO added 30/11/18 validate finally: for layer in self.img.layersStack: layer.autoclone = False # reset flags layer.knitted = False QApplication.restoreOverrideCursor() QApplication.processEvents() # window.label.repaint() # TODO removed 30/11/18 replaced by onImageChange above self.previewOptionBox.stateChanged.connect(m) self.previewOptionBox.setChecked(True) # m is not triggered # title titleLabel = QLabel('Layer') titleLabel.setMaximumSize(100, 30) # opacity slider self.opacitySlider = QbLUeSlider(Qt.Horizontal) self.opacitySlider.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.opacitySlider.setTickPosition(QSlider.TicksBelow) self.opacitySlider.setRange(0, 100) self.opacitySlider.setSingleStep(1) self.opacitySlider.setSliderPosition(100) self.opacityValue = QLabel() font = self.opacityValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.opacityValue.setMinimumSize(w, h) self.opacityValue.setMaximumSize(w, h) self.opacityValue.setText('100 ') # opacity value changed event handler def f1(): self.opacityValue.setText(str('%d ' % self.opacitySlider.value())) # opacity slider released event handler def f2(): try: layer = self.img.getActiveLayer() layer.setOpacity(self.opacitySlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.opacitySlider.valueChanged.connect(f1) self.opacitySlider.sliderReleased.connect(f2) # mask color slider maskSlider = QbLUeSlider(Qt.Horizontal) maskSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) maskSlider.setTickPosition(QSlider.TicksBelow) maskSlider.setRange(0, 100) maskSlider.setSingleStep(1) maskSlider.setSliderPosition(100) self.maskSlider = maskSlider self.maskValue = QLabel() font = self.maskValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.maskValue.setMinimumSize(w, h) self.maskValue.setMaximumSize(w, h) self.maskValue.setText('100 ') # mask value changed event handler def g1(): self.maskValue.setText(str('%d ' % self.maskSlider.value())) # mask slider released event handler def g2(): try: layer = self.img.getActiveLayer() layer.setColorMaskOpacity(self.maskSlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.maskSlider.valueChanged.connect(g1) self.maskSlider.sliderReleased.connect(g2) # blending mode combo box compLabel = QLabel() compLabel.setText("Blend") self.compositionModeDict = OrderedDict([ ('Normal', QPainter.CompositionMode_SourceOver), ('Plus', QPainter.CompositionMode_Plus), ('Multiply', QPainter.CompositionMode_Multiply), ('Screen', QPainter.CompositionMode_Screen), ('Overlay', QPainter.CompositionMode_Overlay), ('Darken', QPainter.CompositionMode_Darken), ('Lighten', QPainter.CompositionMode_Lighten), ('Color Dodge', QPainter.CompositionMode_ColorDodge), ('Color Burn', QPainter.CompositionMode_ColorBurn), ('Hard Light', QPainter.CompositionMode_HardLight), ('Soft Light', QPainter.CompositionMode_SoftLight), ('Difference', QPainter.CompositionMode_Difference), ('Exclusion', QPainter.CompositionMode_Exclusion) ]) self.blendingModeCombo = QComboBox() for key in self.compositionModeDict: self.blendingModeCombo.addItem(key, self.compositionModeDict[key]) # combo box item chosen event handler def g(ind): s = self.blendingModeCombo.currentText() try: layer = self.img.getActiveLayer() layer.compositionMode = self.compositionModeDict[str(s)] layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.blendingModeCombo.currentIndexChanged.connect(g) # self.blendingModeCombo.activated.connect(g) # TODO activated changed to currentIndexChanged 08/10/18 validate #layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) hl0 = QHBoxLayout() hl0.addWidget(titleLabel) hl0.addStretch(1) hl0.addWidget(self.previewOptionBox) l.addLayout(hl0) hl = QHBoxLayout() hl.addWidget(QLabel('Opacity')) hl.addWidget(self.opacityValue) hl.addWidget(self.opacitySlider) l.addLayout(hl) hl1 = QHBoxLayout() hl1.addWidget(QLabel('Mask Color')) hl1.addWidget(self.maskValue) hl1.addWidget(self.maskSlider) l.addLayout(hl1) l.setContentsMargins(0, 0, 10, 0) # left, top, right, bottom hl2 = QHBoxLayout() hl2.addWidget(compLabel) hl2.addWidget(self.blendingModeCombo) l.addLayout(hl2) for layout in [hl, hl1, hl2]: layout.setContentsMargins(5, 0, 0, 0) # this layout must be added to the propertyWidget object loaded from blue.ui : # we postpone it after loading of the main form, in blue.py. self.propertyLayout = l # shortcut actions self.actionDup = QAction('Duplicate layer', None) self.actionDup.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_J)) self.addAction(self.actionDup) def dup(): row = self.selectedIndexes()[0].row() # Stack index index = len(self.img.layersStack) - row - 1 layer = self.img.layersStack[index] if layer.isAdjustLayer(): return # add new layer to stack and set it to active self.img.dupLayer(index=index) # update layer view self.setLayers(self.img) self.actionDup.triggered.connect(dup) self.setWhatsThis("""<b>Layer Stack</b> To <b>toggle the layer visibility</b> click on the Eye icon.<br> To <b>add a mask</b> use the context menu to enable it and paint pixels with the Mask/Unmask tools in the left pane.<br> For <b>color mask<b/b>: <br> green pixels are masked,<br> red pixels are unmasked.<br> Note that upper visible layers slow down mask edition.<br> """) # end of setWhatsThis """ def setEnabled(self, value): # TODO removed 30/11/18 super(QLayerView, self).setEnabled(value) if not self.isEnabled(): self.setStatusTip('Close adjustment form %s to enable Layers' % self.currentWin.windowTitle()) else: self.setStatusTip('') """ def closeAdjustForms(self, delete=False): """ Close all layer forms. If delete is True (default False), the forms and their dock containers are deleted. @param delete: @type delete: boolean """ if self.img is None: return stack = self.img.layersStack for layer in stack: if hasattr(layer, "view"): if layer.view is not None: dock = layer.view if delete: form = dock.widget() # remove back link form.layer = None # QtGui1.window.removeDockWidget(dock) form.setAttribute(Qt.WA_DeleteOnClose) form.close() dock.setAttribute(Qt.WA_DeleteOnClose) dock.close() layer.view = None elif not TABBING: # tabbed forms should not be closed dock.close() if delete: self.currentWin = None gc.collect() def clear(self, delete=True): """ Reset LayerView and clear the back links to image. """ self.closeAdjustForms(delete=delete) self.img = None self.currentWin = None model = layerModel() model.setColumnCount(3) self.setModel(None) def setLayers(self, mImg, delete=False): """ Displays the layer stack of mImg @param mImg: image @type mImg: mImage """ # close open adjustment windows #self.closeAdjustForms() self.clear(delete=delete) mImg.layerView = self # back link to image self.img = weakProxy(mImg) model = layerModel() model.setColumnCount(3) l = len(mImg.layersStack) # dataChanged event handler : enables edition of layer name def f(index1, index2): # index1 and index2 should be equal # only layer name should be editable # dropEvent emit dataChanged when setting item values. f must # return immediately from these calls, as they are possibly made with unconsistent data : # dragged rows are already removed from layersStack # and not yet removed from model. if l != self.model().rowCount(): return # only name is editable if index1.column() != 1: return row = index1.row() stackIndex = l - row - 1 mImg.layersStack[stackIndex].name = index1.data() model.dataChanged.connect(f) for r, lay in enumerate(reversed(mImg.layersStack)): items = [] # col 0 : visibility icon if lay.visible: item_visible = QStandardItem( QIcon(":/images/resources/eye-icon.png"), "") else: item_visible = QStandardItem( QIcon(":/images/resources/eye-icon-strike.png"), "") items.append(item_visible) # col 1 : image icon (for non-adjustment layeronly) and name if len(lay.name) <= 30: name = lay.name else: name = lay.name[:28] + '...' if hasattr(lay, 'inputImg'): item_name = QStandardItem(name) else: # icon with very small dim causes QPainter error # QPixmap.fromImage bug ? smallImg = lay.resize(50 * 50) w, h = smallImg.width(), smallImg.height() if w < h / 5 or h < w / 5: item_name = QStandardItem(name) else: item_name = QStandardItem( QIcon(QPixmap.fromImage(smallImg)), name) # set tool tip to full name item_name.setToolTip(lay.name) items.append(item_name) item_mask = QStandardItem('m') items.append(item_mask) model.appendRow(items) self.setModel(model) self.horizontalHeader().hide() self.verticalHeader().hide() header = self.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeToContents) header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # select active layer self.selectRow(len(mImg.layersStack) - 1 - mImg.activeLayerIndex) layerview = mImg.getActiveLayer().view # TODO added 25/11/18 if layerview is not None: layerview.show() if TABBING: layerview.raise_() self.updateForm() for item in self.img.layersStack: if hasattr(item, 'sourceIndex'): combo = item.getGraphicsForm().sourceCombo currentText = combo.currentText() combo.clear() for i, x in enumerate(self.img.layersStack): item.view.widget().sourceCombo.addItem(x.name, i) combo.setCurrentIndex(combo.findText(currentText)) def updateForm(self): activeLayer = self.img.getActiveLayer() if hasattr(activeLayer, 'view'): self.currentWin = activeLayer.view if self.currentWin is not None: self.currentWin.show() self.currentWin.activateWindow() def updateRow(self, row): minInd, maxInd = self.model().index(row, 0), self.model().index(row, 3) self.model().dataChanged.emit(minInd, maxInd) def dropEvent(self, event): """ drop event handler : moving layer @param event: @type event: Qevent """ if event.source() is not self: return # get selected rows and layers rows = set([mi.row() for mi in self.selectedIndexes()]) rStack = self.img.layersStack[::-1] layers = [rStack[i] for i in rows] linked = any(l.group for l in layers) if linked and len(rows) > 1: return # get target row and layer targetRow = self.indexAt(event.pos()).row() targetLayer = rStack[targetRow] if linked: if layers[0].group is not targetLayer.group: return if bool(targetLayer.group) != linked: return # remove target from selection if targetRow in rows: rows.discard(targetRow) rows = sorted(rows) if not rows: return # if target is below last row insert at the last position if targetRow == -1: targetRow = self.model().rowCount() # mapping of src (row) indices to target indices rowMapping = dict() for idx, row in enumerate(rows): if row < targetRow: rowMapping[row] = targetRow + idx else: rowMapping[row + len(rows)] = targetRow + idx # update layerStack using rowMapping # insert None items for _ in range(len(rows)): rStack.insert(targetRow, None) # copy moved items to their final place for srcRow, tgtRow in sorted( rowMapping.items()): # python 3 iteritems->items rStack[tgtRow] = rStack[srcRow] # remove moved items from their initial place for row in reversed(sorted( rowMapping.keys())): # python 3 iterkeys -> keys rStack.pop(row) self.img.layersStack = rStack[::-1] # update model # insert empty rows for _ in range(len(rows)): result = self.model().insertRow(targetRow, QModelIndex()) # copy moved rows to their final place colCount = self.model().columnCount() for srcRow, tgtRow in sorted( rowMapping.items()): # python 3 iteritems->items for col in range(0, colCount): # CAUTION : setItem calls the data changed event handler (cf. setLayers above) self.model().setItem(tgtRow, col, self.model().takeItem(srcRow, col)) # remove moved rows from their initial place and keep track of moved items movedDict = rowMapping.copy() for row in reversed(sorted( rowMapping.keys())): # python 3 iterkeys -> keys self.model().removeRow(row) for s, t in rowMapping.items(): if t > row: movedDict[s] -= 1 ######################################### sanity check for r in range(self.model().rowCount()): id = self.model().index(r, 1) if id.data() != rStack[r].name: raise ValueError('Drop Error') ######################################## # reselect moved rows sel = sorted(movedDict.values()) selectionModel = QtCore.QItemSelectionModel(self.model()) self.setSelectionModel(selectionModel) index1 = self.model().index(sel[0], 1) index2 = self.model().index(sel[-1], 1) itemSelection = QtCore.QItemSelection(index1, index2) self.selectionModel().select( itemSelection, QtCore.QItemSelectionModel.Rows | QtCore.QItemSelectionModel.Select) # multiple selection : display no window if len(sel) > 1: self.currentWin.hide() self.currentWin = None elif len(sel) == 1: self.img.setActiveLayer(len(self.img.layersStack) - sel[0] - 1) # update stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() def select(self, row, col): """ select item in view @param row: @type row: @param col: @type col: @return: @rtype: """ model = self.model() self.viewClicked(model.index(row, col)) def viewClicked(self, clickedIndex): """ Mouse clicked event handler. @param clickedIndex: @type clickedIndex: QModelIndex """ row = clickedIndex.row() rows = set([mi.row() for mi in self.selectedIndexes()]) #multiple selection : go to top of selection m = min(rows) if row != m: clickedIndex = self.model().index(m, clickedIndex.column()) layer = self.img.layersStack[-1 - row] self.actionDup.setEnabled(not layer.isAdjustLayer()) # toggle layer visibility if clickedIndex.column() == 0: # background layer is always visible if row == len(self.img.layersStack) - 1: return #layer.visible = not(layer.visible) layer.setVisible(not (layer.visible)) if self.currentWin is not None: self.currentWin.setVisible(layer.visible) if not layer.visible: self.currentWin = None if layer.tool is not None: layer.tool.setVisible(layer.visible) # update stack if layer.visible: layer.applyToStack() else: i = layer.getUpperVisibleStackIndex() if i >= 0: layer.parentImage.layersStack[i].applyToStack() else: # top layer : update only the presentation layer layer.parentImage.prLayer.execute(l=None, pool=None) self.img.onImageChanged() # update displayed window and active layer activeStackIndex = len(self.img.layersStack) - 1 - row activeLayer = self.img.setActiveLayer(activeStackIndex) if self.currentWin is not None: if not self.currentWin.isFloating(): #self.currentWin.hide() self.currentWin = None if hasattr(self.img.layersStack[activeStackIndex], "view"): self.currentWin = self.img.layersStack[activeStackIndex].view if self.currentWin is not None and activeLayer.visible: self.currentWin.show() self.currentWin.raise_() # make self.currentWin the active window self.currentWin.activateWindow() # update opacity and composition mode for current layer opacity = int(layer.opacity * 100) self.opacityValue.setText(str('%d ' % opacity)) self.opacitySlider.setSliderPosition(opacity) compositionMode = layer.compositionMode ind = self.blendingModeCombo.findData(compositionMode) self.blendingModeCombo.setCurrentIndex(ind) # draw the right rectangle window.label.repaint() def initContextMenu(self): """ return the context menu @return: @rtype: QMenu """ menu = QMenu() menu.actionReset = QAction('Reset To Default', None) menu.actionLoadImage = QAction('Load New Image', None) menu.actionGroupSelection = QAction('Group Selection', None) menu.actionAdd2Group = QAction('Add to Group', None) # Active layer is not in a group or right clicked layer is in a group menu.actionUnGroup = QAction('Ungroup', None) # multiple selections menu.actionMerge = QAction('Merge Lower', None) # merge only adjustment layer with image layer # don't dup adjustment layers menu.actionUnselect = QAction('Unselect All', None) menu.actionRepositionLayer = QAction('Reposition Layer(s)', None) menu.actionColorMaskEnable = QAction('Color Mask', None) menu.actionOpacityMaskEnable = QAction('Opacity Mask', None) menu.actionClippingMaskEnable = QAction('Clipping Mask', None) menu.actionMaskDisable = QAction('Disable Mask', None) menu.actionMaskInvert = QAction('Invert Mask', None) menu.actionMaskReset = QAction('Clear Mask', None) menu.actionMaskCopy = QAction('Copy Mask to Clipboard', None) menu.actionImageCopy = QAction('Copy Image to Clipboard', None) menu.actionMaskPaste = QAction('Paste Mask', None) menu.actionImagePaste = QAction('Paste Image', None) menu.actionMaskDilate = QAction('Dilate Mask', None) menu.actionMaskErode = QAction('Erode Mask', None) menu.actionColorMaskEnable.setCheckable(True) menu.actionOpacityMaskEnable.setCheckable(True) menu.actionClippingMaskEnable.setCheckable(True) menu.actionMaskDisable.setCheckable(True) #################### # Build menu ################### # group/ungroup menu.addAction(menu.actionAdd2Group) menu.addAction(menu.actionGroupSelection) menu.addAction(menu.actionUnGroup) menu.addSeparator() menu.addAction(menu.actionUnselect) menu.addSeparator() menu.addAction(menu.actionRepositionLayer) menu.addSeparator() # layer menu.addAction(menu.actionImageCopy) menu.addAction(menu.actionImagePaste) menu.addSeparator() # mask menu.subMenuEnable = menu.addMenu('Mask...') menu.subMenuEnable.addAction(menu.actionColorMaskEnable) menu.subMenuEnable.addAction(menu.actionOpacityMaskEnable) menu.subMenuEnable.addAction(menu.actionClippingMaskEnable) menu.subMenuEnable.addAction(menu.actionMaskDisable) menu.addAction(menu.actionMaskInvert) menu.addAction(menu.actionMaskReset) menu.addAction(menu.actionMaskCopy) menu.addAction(menu.actionMaskPaste) menu.addAction(menu.actionMaskDilate) menu.addAction(menu.actionMaskErode) menu.addSeparator() # miscellaneous menu.addAction(menu.actionLoadImage) # to link actionDup with a shortcut, # it must be set in __init__ menu.addAction(self.actionDup) menu.addAction(menu.actionMerge) menu.addAction(menu.actionReset) return menu def contextMenuEvent(self, event): """ context menu handler @param event @type event: QContextMenuEvent """ selection = self.selectedIndexes() if not selection: return # get fresh context menu self.cMenu = self.initContextMenu() # get current selection rows = set([mi.row() for mi in selection]) rStack = self.img.layersStack[::-1] layers = [rStack[r] for r in rows] group = [] # TODO added 5/11/18 validate if layers: group = layers[0].group for l in layers: # different groups if l.group and group: if l.group is not group: dlgWarn("Select a single group") return # get current position index = self.indexAt(event.pos()) layerStackIndex = len(self.img.layersStack) - 1 - index.row() layer = self.img.layersStack[layerStackIndex] lowerVisible = self.img.layersStack[layer.getLowerVisibleStackIndex()] lower = self.img.layersStack[layerStackIndex - 1] # case index == 0 doesn't matter # toggle actions self.cMenu.actionGroupSelection.setEnabled(not (len(rows) < 2 or any( l.group for l in layers))) self.cMenu.actionAdd2Group.setEnabled(not (group or layer.group)) self.cMenu.actionUnGroup.setEnabled(bool(layer.group)) self.cMenu.actionMerge.setEnabled(not ( hasattr(layer, 'inputImg') or hasattr(lowerVisible, 'inputImg'))) self.actionDup.setEnabled(not layer.isAdjustLayer()) self.cMenu.actionColorMaskEnable.setChecked(layer.maskIsSelected and layer.maskIsEnabled) self.cMenu.actionOpacityMaskEnable.setChecked( (not layer.maskIsSelected) and layer.maskIsEnabled) self.cMenu.actionClippingMaskEnable.setChecked( layer.isClipping and (layer.maskIsSelected or layer.maskIsEnabled)) self.cMenu.actionMaskDisable.setChecked(not ( layer.isClipping or layer.maskIsSelected or layer.maskIsEnabled)) self.cMenu.actionUnselect.setEnabled(layer.rect is None) self.cMenu.subMenuEnable.setEnabled(len(rows) == 1) self.cMenu.actionMaskPaste.setEnabled( not QApplication.clipboard().image().isNull()) self.cMenu.actionImagePaste.setEnabled( not QApplication.clipboard().image().isNull()) # Event handlers def f(): self.opacitySlider.show() def unselectAll(): layer.rect = None def RepositionLayer(): layer.xOffset, layer.yOffset = 0, 0 layer.Zoom_coeff = 1.0 layer.AltZoom_coeff = 1.0 layer.xAltOffset, layer.yAltOffset = 0, 0 layer.updatePixmap() self.img.onImageChanged() def loadImage(): return # TODO 26/06/18 action to remove from menu? replaced by new image layer filename = openDlg(window) img = QImage(filename) layer.thumb = None layer.setImage(img) def add2Group(): layer.group = group layer.mask = group[0].mask layer.maskIsEnabled = True layer.maskIsSelected = True def groupSelection(): layers = [rStack[i] for i in sorted(rows)] if any(l.group for l in layers): dlgWarn("Some layers are already grouped. Ungroup first") return mask = layers[0].mask for l in layers: l.group = layers l.mask = mask l.maskIsEnabled = True l.maskIsSelected = False def unGroup(): group = layer.group.copy() for l in group: l.unlinkMask() def merge(): layer.merge_with_layer_immediately_below() def testUpperVisibility(): pos = self.img.getStackIndex(layer) upperVisible = False for i in range(len(self.img.layersStack) - pos - 1): if self.img.layersStack[pos + 1 + i].visible: upperVisible = True break if upperVisible: dlgWarn("Upper visible layers slow down mask edition") return True return False def colorMaskEnable(): testUpperVisibility() layer.maskIsEnabled = True layer.maskIsSelected = True layer.applyToStack() self.img.onImageChanged() def opacityMaskEnable(): testUpperVisibility() layer.maskIsEnabled = True layer.maskIsSelected = False layer.applyToStack() self.img.onImageChanged() def clippingMaskEnable(): layer.maskIsEnabled = True layer.maskIsSelected = False layer.isClipping = True layer.applyToStack() self.img.onImageChanged() def maskDisable(): layer.maskIsEnabled = False layer.maskIsSelected = False layer.isClipping = False # TODO added 28/11/18 layer.applyToStack() self.img.onImageChanged() def maskInvert(): layer.invertMask() # update mask stack layer.applyToStack() #for l in self.img.layersStack: #l.updatePixmap(maskOnly=True) self.img.onImageChanged() def maskReset(): layer.resetMask() # update mask stack for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.onImageChanged() def maskCopy(): QApplication.clipboard().setImage(layer.mask) def imageCopy(): QApplication.clipboard().setImage(layer.getCurrentMaskedImage()) def maskPaste(): """ Pastes clipboard to mask and updates the stack. The clipboard image is scaled if its size does not match the size of the mask """ cb = QApplication.clipboard() if not cb.image().isNull(): img = cb.image() if img.size() == layer.mask.size(): layer.mask = img else: layer.mask = img.scaled(layer.mask.size()) layer.applyToStack() self.img.onImageChanged() def imagePaste(): """ Pastes clipboard to mask and updates the stack. The clipboard image is scaled if its size does not match the size of the mask """ cb = QApplication.clipboard() if not cb.image().isNull(): srcImg = cb.image() if srcImg.size() == layer.size(): layer.setImage(srcImg) else: layer.setImage(srcImg.scaled(layer.size())) layer.applyToStack() self.img.onImageChanged() def maskDilate(): kernel = np.ones((5, 5), np.uint8) buf = QImageBuffer(layer.mask) # CAUTION erode decreases values (min filter), so it extends the masked part of the image buf[:, :, 2] = cv2.erode(buf[:, :, 2], kernel, iterations=1) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.onImageChanged() def maskErode(): kernel = np.ones((5, 5), np.uint8) buf = QImageBuffer(layer.mask) # CAUTION dilate increases values (max filter), so it reduces the masked part of the image buf[:, :, 2] = cv2.dilate(buf[:, :, 2], kernel, iterations=1) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.onImageChanged() def layerReset(): view = layer.getGraphicsForm() if hasattr(view, 'reset'): view.reset() self.cMenu.actionRepositionLayer.triggered.connect(RepositionLayer) self.cMenu.actionUnselect.triggered.connect(unselectAll) self.cMenu.actionLoadImage.triggered.connect(loadImage) self.cMenu.actionAdd2Group.triggered.connect(add2Group) self.cMenu.actionGroupSelection.triggered.connect(groupSelection) self.cMenu.actionUnGroup.triggered.connect(unGroup) self.cMenu.actionMerge.triggered.connect(merge) self.cMenu.actionColorMaskEnable.triggered.connect(colorMaskEnable) self.cMenu.actionOpacityMaskEnable.triggered.connect(opacityMaskEnable) self.cMenu.actionClippingMaskEnable.triggered.connect( clippingMaskEnable) self.cMenu.actionMaskDisable.triggered.connect(maskDisable) self.cMenu.actionMaskInvert.triggered.connect(maskInvert) self.cMenu.actionMaskReset.triggered.connect(maskReset) self.cMenu.actionMaskCopy.triggered.connect(maskCopy) self.cMenu.actionMaskPaste.triggered.connect(maskPaste) self.cMenu.actionImageCopy.triggered.connect(imageCopy) self.cMenu.actionImagePaste.triggered.connect(imagePaste) self.cMenu.actionMaskDilate.triggered.connect(maskDilate) self.cMenu.actionMaskErode.triggered.connect(maskErode) self.cMenu.actionReset.triggered.connect(layerReset) self.cMenu.exec_(event.globalPos()) # update table for row in rows: self.updateRow(row)
class savingDialog(QDialog): """ File dialog with quality and compression sliders. We use a standard QFileDialog as a child widget and we forward its methods to the top level. """ def __init__(self, parent, text, lastDir): """ @param parent: @type parent: QObject @param text: @type text: str @param lastDir: @type lastDir:str """ # QDialog __init__ super().__init__() self.setWindowTitle(text) # File Dialog self.dlg = QFileDialog(caption=text, directory=lastDir) # sliders self.sliderComp = QbLUeSlider(Qt.Horizontal) self.sliderComp.setTickPosition(QSlider.TicksBelow) self.sliderComp.setRange(0, 9) self.sliderComp.setSingleStep(1) self.sliderComp.setValue(5) self.sliderQual = QbLUeSlider(Qt.Horizontal) self.sliderQual.setTickPosition(QSlider.TicksBelow) self.sliderQual.setRange(0, 100) self.sliderQual.setSingleStep(10) self.sliderQual.setValue(90) self.dlg.setVisible(True) l = QVBoxLayout() h = QHBoxLayout() l.addWidget(self.dlg) h.addWidget(QLabel("Quality")) h.addWidget(self.sliderQual) h.addWidget(QLabel("Compression")) h.addWidget(self.sliderComp) l.addLayout(h) self.setLayout(l) # file dialog close event handler def f(): self.close() self.dlg.finished.connect(f) def exec_(self): # QDialog exec_ super().exec_() # forward file dialog result return self.dlg.result() def selectFile(self, fileName): self.dlg.selectFile(fileName) def selectedFiles(self): return self.dlg.selectedFiles() def directory(self): return self.dlg.directory()
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): super().__init__(parent=parent) #self.targetImage = targetImage self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) # link back to image layer # using weak ref for back links if type(layer) in weakref.ProxyTypes: self.layer = layer else: self.layer = weakref.proxy(layer) # options self.options = None # exposure slider self.sliderExp = QbLUeSlider(Qt.Horizontal) self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderExp.setTickPosition(QSlider.TicksBelow) 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 done event handler def f(): self.sliderExp.setEnabled(False) self.expValue.setText( str("{:+.1f}".format(self.sliderExp.value() * self.DefaultStep))) self.onUpdateExposure(self.layer, self.sliderExp.value() * self.DefaultStep) self.sliderExp.setEnabled(True) # exp value changed slot def g(): self.expValue.setText( str("{:+.1f}".format(self.sliderExp.value() * self.DefaultStep))) self.sliderExp.valueChanged.connect(g) self.sliderExp.sliderReleased.connect(f) self.sliderExp.setValue(self.defaultExpCorrection / self.DefaultStep) self.expValue.setText(str("{:+.1f}".format(self.defaultExpCorrection))) #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 #l.addStretch(1) self.setLayout(l) self.adjustSize() self.setWhatsThis("""<b>Exposure Correction</b> Multiplicative correction in the linear sRGB color space.<br> """) # end setWhatsThis
class ExpForm(baseForm): defaultExpCorrection = 0.0 DefaultStep = 0.1 @classmethod def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): wdgt = ExpForm(axeSize=axeSize, layer=layer, parent=parent, mainForm=mainForm) wdgt.setWindowTitle(layer.name) return wdgt def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): super().__init__(parent=parent) #self.targetImage = targetImage self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) # link back to image layer # using weak ref for back links if type(layer) in weakref.ProxyTypes: self.layer = layer else: self.layer = weakref.proxy(layer) # options self.options = None # exposure slider self.sliderExp = QbLUeSlider(Qt.Horizontal) self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderExp.setTickPosition(QSlider.TicksBelow) 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 done event handler def f(): self.sliderExp.setEnabled(False) self.expValue.setText( str("{:+.1f}".format(self.sliderExp.value() * self.DefaultStep))) self.onUpdateExposure(self.layer, self.sliderExp.value() * self.DefaultStep) self.sliderExp.setEnabled(True) # exp value changed slot def g(): self.expValue.setText( str("{:+.1f}".format(self.sliderExp.value() * self.DefaultStep))) self.sliderExp.valueChanged.connect(g) self.sliderExp.sliderReleased.connect(f) self.sliderExp.setValue(self.defaultExpCorrection / self.DefaultStep) self.expValue.setText(str("{:+.1f}".format(self.defaultExpCorrection))) #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 #l.addStretch(1) self.setLayout(l) self.adjustSize() self.setWhatsThis("""<b>Exposure Correction</b> Multiplicative correction in the linear sRGB color space.<br> """) # end setWhatsThis def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) outStream.writeQString(self.listWidget1.selectedItems()[0].text()) outStream.writeInt32(self.sliderExp.value()) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() temp = inStream.readInt32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderExp.setValue(temp) self.update() return inStream
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.defaultTemp = sRGBWP # ref temperature D65 self.defaultTint = 0 # options optionList, optionNames = ['Photo Filter', 'Chromatic Adaptation' ], ['Photo Filter', 'Chromatic Adaptation'] self.listWidget1 = optionsWidget( options=optionList, optionNames=optionNames, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[1]) self.options = self.listWidget1.options # 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) tempLabel = QbLUeLabel() tempLabel.setMaximumSize(150, 30) tempLabel.setText("Filter Temperature") 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())))) # temp change slot def tempUpdate(value): 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(tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: tempUpdate(self.sliderTemp.value())) # tint change slot def tintUpdate(value): 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(tintUpdate) self.sliderTint.sliderReleased.connect( lambda: tintUpdate(self.sliderTint.value())) self.sliderTemp.valueChanged.connect(tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: tempUpdate(self.sliderTemp.value())) self.sliderTint.valueChanged.connect(tintUpdate) self.sliderTint.sliderReleased.connect( lambda: tintUpdate(self.sliderTint.value())) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(self.listWidget1) l.addWidget(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 Temperature</b><br> <b>Photo Filter</b> uses 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
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.defaultTemp = sRGBWP # ref temperature D65 self.defaultTint = 0 # options optionList, optionNames = ['Photo Filter', 'Chromatic Adaptation' ], ['Photo Filter', 'Chromatic Adaptation'] self.listWidget1 = optionsWidget( options=optionList, optionNames=optionNames, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[1]) self.options = self.listWidget1.options # 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) tempLabel = QbLUeLabel() tempLabel.setMaximumSize(150, 30) tempLabel.setText("Filter Temperature") 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())))) # temp change slot def tempUpdate(value): 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(tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: tempUpdate(self.sliderTemp.value())) # tint change slot def tintUpdate(value): 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(tintUpdate) self.sliderTint.sliderReleased.connect( lambda: tintUpdate(self.sliderTint.value())) self.sliderTemp.valueChanged.connect(tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: tempUpdate(self.sliderTemp.value())) self.sliderTint.valueChanged.connect(tintUpdate) self.sliderTint.sliderReleased.connect( lambda: tintUpdate(self.sliderTint.value())) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(self.listWidget1) l.addWidget(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 Temperature</b><br> <b>Photo Filter</b> uses 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): self.sliderTemp.setEnabled(True) 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) def updateLayer(self): """ data changed slot """ self.enableSliders() self.layer.applyToStack() self.layer.parentImage.onImageChanged() @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) def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) outStream.writeQString(self.listWidget1.selectedItems()[0].text()) outStream.writeInt32(self.sliderTemp.value() * 100) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() temp = inStream.readInt32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderTemp.setValue(temp // 100) self.update() return inStream
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): super().__init__(parent=parent) defaultRadius = 10 defaultTone = 100.0 defaultAmount = 50.0 self.radius = defaultRadius self.tone = defaultTone self.amount = defaultAmount self.targetImage = targetImage self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) self.img = targetImage # link back to image layer self.layer = weakProxy(layer) """ # using weak ref for back links if type(layer) in weakref.ProxyTypes: self.layer = layer else: self.layer = weakref.proxy(layer) """ self.mainForm = mainForm 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)) self.listWidget1 = optionsWidget(options=self.optionList, exclusive=True, changed=self.dataChanged) # set initial selection to unsharp mask item = self.listWidget1.checkOption(self.optionList[0]) # sliders self.sliderRadius = QbLUeSlider(Qt.Horizontal) #self.sliderRadius.setTickPosition(QSlider.TicksBelow) 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.setTickPosition(QSlider.TicksBelow) 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.setTickPosition(QSlider.TicksBelow) 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) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignBottom) 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) # value changed event handler def sliderUpdate(): self.radiusValue.setText(str('%d ' % self.sliderRadius.value())) self.amountValue.setText(str('%d ' % self.sliderAmount.value())) self.toneValue.setText(str('%d ' % self.sliderTone.value())) # value done event handler def formUpdate(): sR, sA, sT = self.sliderRadius.isEnabled( ), self.sliderAmount.isEnabled(), self.sliderTone.isEnabled() self.sliderRadius.setEnabled(False) self.sliderAmount.setEnabled(False) self.sliderTone.setEnabled(False) self.tone = self.sliderTone.value() self.radius = self.sliderRadius.value() self.amount = self.sliderAmount.value() sliderUpdate() self.dataChanged.emit() self.sliderRadius.setEnabled(sR) self.sliderAmount.setEnabled(sA) self.sliderTone.setEnabled(sT) self.sliderRadius.valueChanged.connect(sliderUpdate) self.sliderRadius.sliderReleased.connect(formUpdate) self.sliderAmount.valueChanged.connect(sliderUpdate) self.sliderAmount.sliderReleased.connect(formUpdate) self.sliderTone.valueChanged.connect(sliderUpdate) self.sliderTone.sliderReleased.connect(formUpdate) self.dataChanged.connect(self.updateLayer) # init self.sliderRadius.setValue(defaultRadius) self.sliderAmount.setValue(defaultAmount) self.sliderTone.setValue(defaultTone) self.enableSliders() sliderUpdate() self.setWhatsThis(""" <b>Unsharp Mask</b> and <b>Sharpen Mask</b> are used to sharpen an image.<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.<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 tool.<br> Ctrl-Click to <b>clear the selection</b><br> """) # end setWhatsThis
class filterForm(baseForm): # dataChanged = QtCore.Signal() @classmethod def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): wdgt = filterForm(targetImage=targetImage, axeSize=axeSize, layer=layer, parent=parent, mainForm=mainForm) wdgt.setWindowTitle(layer.name) return wdgt def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): super().__init__(parent=parent) defaultRadius = 10 defaultTone = 100.0 defaultAmount = 50.0 self.radius = defaultRadius self.tone = defaultTone self.amount = defaultAmount self.targetImage = targetImage self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) self.img = targetImage # link back to image layer self.layer = weakProxy(layer) """ # using weak ref for back links if type(layer) in weakref.ProxyTypes: self.layer = layer else: self.layer = weakref.proxy(layer) """ self.mainForm = mainForm 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)) self.listWidget1 = optionsWidget(options=self.optionList, exclusive=True, changed=self.dataChanged) # set initial selection to unsharp mask item = self.listWidget1.checkOption(self.optionList[0]) # sliders self.sliderRadius = QbLUeSlider(Qt.Horizontal) #self.sliderRadius.setTickPosition(QSlider.TicksBelow) 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.setTickPosition(QSlider.TicksBelow) 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.setTickPosition(QSlider.TicksBelow) 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) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignBottom) 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) # value changed event handler def sliderUpdate(): self.radiusValue.setText(str('%d ' % self.sliderRadius.value())) self.amountValue.setText(str('%d ' % self.sliderAmount.value())) self.toneValue.setText(str('%d ' % self.sliderTone.value())) # value done event handler def formUpdate(): sR, sA, sT = self.sliderRadius.isEnabled( ), self.sliderAmount.isEnabled(), self.sliderTone.isEnabled() self.sliderRadius.setEnabled(False) self.sliderAmount.setEnabled(False) self.sliderTone.setEnabled(False) self.tone = self.sliderTone.value() self.radius = self.sliderRadius.value() self.amount = self.sliderAmount.value() sliderUpdate() self.dataChanged.emit() self.sliderRadius.setEnabled(sR) self.sliderAmount.setEnabled(sA) self.sliderTone.setEnabled(sT) self.sliderRadius.valueChanged.connect(sliderUpdate) self.sliderRadius.sliderReleased.connect(formUpdate) self.sliderAmount.valueChanged.connect(sliderUpdate) self.sliderAmount.sliderReleased.connect(formUpdate) self.sliderTone.valueChanged.connect(sliderUpdate) self.sliderTone.sliderReleased.connect(formUpdate) self.dataChanged.connect(self.updateLayer) # init self.sliderRadius.setValue(defaultRadius) self.sliderAmount.setValue(defaultAmount) self.sliderTone.setValue(defaultTone) self.enableSliders() sliderUpdate() self.setWhatsThis(""" <b>Unsharp Mask</b> and <b>Sharpen Mask</b> are used to sharpen an image.<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.<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 tool.<br> Ctrl-Click to <b>clear the selection</b><br> """) # end setWhatsThis 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()) def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) outStream.writeQString(self.listWidget1.selectedItems()[0].text()) outStream.writeFloat32(self.sliderRadius.value()) outStream.writeFloat32(self.sliderAmount.value()) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() radius = inStream.readFloat32() amount = inStream.readFloat32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderRadius.setValue(radius) self.sliderAmount.setValue(amount) self.repaint() return inStream
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() def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) for item in self.listWidget1.selectedItems(): outStream.writeQString(item.text()) outStream.writeInt32(self.sliderContrast.value()) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() temp = inStream.readInt32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderContrast.setValue(temp) self.update() return inStream
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 __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) # TODO 130 changed to 100 12/11/18 validate 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, self.dngDict) # 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 # self.dataChanged.connect(self.updateLayer) # TODO 30/10/18 moved to base class self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") # 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) self.listWidget2.setStyleSheet( "QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 20px;}" ) vl1 = QVBoxLayout() vl1.addWidget(QLabel('White Balance')) vl1.addWidget(self.listWidget2) gb1 = QGroupBox() gb1.setStyleSheet( "QGroupBox {border: 1px solid gray; border-radius: 4px}") 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 __init__(self, parent): super(QLayerView, self).__init__(parent) self.img = None # graphic form to show : it # should correspond to the currently selected layer self.currentWin = None # mouse click event self.clicked.connect(self.viewClicked) # set behavior and styles self.setSelectionBehavior(QAbstractItemView.SelectRows) delegate = itemDelegate(parent=self) self.setItemDelegate(delegate) ic1 = QImage(":/images/resources/eye-icon.png") ic2 = QImage(":/images/resources/eye-icon-strike.png") delegate.px1 = QPixmap.fromImage(ic1) delegate.px2 = QPixmap.fromImage(ic2) ic1.invertPixels() ic2.invertPixels() delegate.inv_px1 = QPixmap.fromImage(ic1) delegate.inv_px2 = QPixmap.fromImage(ic2) self.setIconSize(QSize(20, 15)) self.verticalHeader().setMinimumSectionSize(-1) self.verticalHeader().setDefaultSectionSize( self.verticalHeader().minimumSectionSize()) self.horizontalHeader().setMinimumSectionSize(40) self.horizontalHeader().setDefaultSectionSize(40) # drag and drop self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setDragDropOverwriteMode(False) self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) ################################ # layer property GUI : # preview, blending mode, opacity, mask color ################################ # Preview option # We should use a QListWidget or a custom optionsWidget # (cf. utils.py) : adding it to QVBoxLayout with mode # Qt.AlignBottom does not work. self.previewOptionBox = QCheckBox('Preview') self.previewOptionBox.setMaximumSize(100, 30) # View/Preview changed event handler def m(state): # state : Qt.Checked Qt.UnChecked if self.img is None: return self.img.useThumb = (state == Qt.Checked) window.updateStatus() self.img.cacheInvalidate() for layer in self.img.layersStack: layer.autoclone = True # auto update cloning layers layer.knitted = False try: QApplication.setOverrideCursor( Qt.WaitCursor ) # TODO 18/04/18 waitcursor is called by applytostack? QApplication.processEvents() # update the whole stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() # TODO added 30/11/18 validate finally: for layer in self.img.layersStack: layer.autoclone = False # reset flags layer.knitted = False QApplication.restoreOverrideCursor() QApplication.processEvents() # window.label.repaint() # TODO removed 30/11/18 replaced by onImageChange above self.previewOptionBox.stateChanged.connect(m) self.previewOptionBox.setChecked(True) # m is not triggered # title titleLabel = QLabel('Layer') titleLabel.setMaximumSize(100, 30) # opacity slider self.opacitySlider = QbLUeSlider(Qt.Horizontal) self.opacitySlider.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.opacitySlider.setTickPosition(QSlider.TicksBelow) self.opacitySlider.setRange(0, 100) self.opacitySlider.setSingleStep(1) self.opacitySlider.setSliderPosition(100) self.opacityValue = QLabel() font = self.opacityValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.opacityValue.setMinimumSize(w, h) self.opacityValue.setMaximumSize(w, h) self.opacityValue.setText('100 ') # opacity value changed event handler def f1(): self.opacityValue.setText(str('%d ' % self.opacitySlider.value())) # opacity slider released event handler def f2(): try: layer = self.img.getActiveLayer() layer.setOpacity(self.opacitySlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.opacitySlider.valueChanged.connect(f1) self.opacitySlider.sliderReleased.connect(f2) # mask color slider maskSlider = QbLUeSlider(Qt.Horizontal) maskSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) maskSlider.setTickPosition(QSlider.TicksBelow) maskSlider.setRange(0, 100) maskSlider.setSingleStep(1) maskSlider.setSliderPosition(100) self.maskSlider = maskSlider self.maskValue = QLabel() font = self.maskValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.maskValue.setMinimumSize(w, h) self.maskValue.setMaximumSize(w, h) self.maskValue.setText('100 ') # mask value changed event handler def g1(): self.maskValue.setText(str('%d ' % self.maskSlider.value())) # mask slider released event handler def g2(): try: layer = self.img.getActiveLayer() layer.setColorMaskOpacity(self.maskSlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.maskSlider.valueChanged.connect(g1) self.maskSlider.sliderReleased.connect(g2) # blending mode combo box compLabel = QLabel() compLabel.setText("Blend") self.compositionModeDict = OrderedDict([ ('Normal', QPainter.CompositionMode_SourceOver), ('Plus', QPainter.CompositionMode_Plus), ('Multiply', QPainter.CompositionMode_Multiply), ('Screen', QPainter.CompositionMode_Screen), ('Overlay', QPainter.CompositionMode_Overlay), ('Darken', QPainter.CompositionMode_Darken), ('Lighten', QPainter.CompositionMode_Lighten), ('Color Dodge', QPainter.CompositionMode_ColorDodge), ('Color Burn', QPainter.CompositionMode_ColorBurn), ('Hard Light', QPainter.CompositionMode_HardLight), ('Soft Light', QPainter.CompositionMode_SoftLight), ('Difference', QPainter.CompositionMode_Difference), ('Exclusion', QPainter.CompositionMode_Exclusion) ]) self.blendingModeCombo = QComboBox() for key in self.compositionModeDict: self.blendingModeCombo.addItem(key, self.compositionModeDict[key]) # combo box item chosen event handler def g(ind): s = self.blendingModeCombo.currentText() try: layer = self.img.getActiveLayer() layer.compositionMode = self.compositionModeDict[str(s)] layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.blendingModeCombo.currentIndexChanged.connect(g) # self.blendingModeCombo.activated.connect(g) # TODO activated changed to currentIndexChanged 08/10/18 validate #layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) hl0 = QHBoxLayout() hl0.addWidget(titleLabel) hl0.addStretch(1) hl0.addWidget(self.previewOptionBox) l.addLayout(hl0) hl = QHBoxLayout() hl.addWidget(QLabel('Opacity')) hl.addWidget(self.opacityValue) hl.addWidget(self.opacitySlider) l.addLayout(hl) hl1 = QHBoxLayout() hl1.addWidget(QLabel('Mask Color')) hl1.addWidget(self.maskValue) hl1.addWidget(self.maskSlider) l.addLayout(hl1) l.setContentsMargins(0, 0, 10, 0) # left, top, right, bottom hl2 = QHBoxLayout() hl2.addWidget(compLabel) hl2.addWidget(self.blendingModeCombo) l.addLayout(hl2) for layout in [hl, hl1, hl2]: layout.setContentsMargins(5, 0, 0, 0) # this layout must be added to the propertyWidget object loaded from blue.ui : # we postpone it after loading of the main form, in blue.py. self.propertyLayout = l # shortcut actions self.actionDup = QAction('Duplicate layer', None) self.actionDup.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_J)) self.addAction(self.actionDup) def dup(): row = self.selectedIndexes()[0].row() # Stack index index = len(self.img.layersStack) - row - 1 layer = self.img.layersStack[index] if layer.isAdjustLayer(): return # add new layer to stack and set it to active self.img.dupLayer(index=index) # update layer view self.setLayers(self.img) self.actionDup.triggered.connect(dup) self.setWhatsThis("""<b>Layer Stack</b> To <b>toggle the layer visibility</b> click on the Eye icon.<br> To <b>add a mask</b> use the context menu to enable it and paint pixels with the Mask/Unmask tools in the left pane.<br> For <b>color mask<b/b>: <br> green pixels are masked,<br> red pixels are unmasked.<br> Note that upper visible layers slow down mask edition.<br> """) # end of setWhatsThis
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) # TODO 130 changed to 100 12/11/18 validate 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, self.dngDict) # 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 # self.dataChanged.connect(self.updateLayer) # TODO 30/10/18 moved to base class self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") # 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) self.listWidget2.setStyleSheet( "QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 20px;}" ) vl1 = QVBoxLayout() vl1.addWidget(QLabel('White Balance')) vl1.addWidget(self.listWidget2) gb1 = QGroupBox() gb1.setStyleSheet( "QGroupBox {border: 1px solid gray; border-radius: 4px}") 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.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 dockT = 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.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 dockC = 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) # TODO added 15/07/18 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, self.dngDict) ] 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, self.dngDict) ] 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] # convert multipliers to White Point RGB coordinates, modulo tint green correction (mult[1] = tint*WP_G) # invMultipliers = [self.daylight[i] / self.rawMultipliers[i] for i in range(3)] invMultipliers = [1 / self.rawMultipliers[i] for i in range(3)] # TODO modified 11/11/18 validate 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.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 # TODO change 5.0 to 0.0 6/11/2018 validate # 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): """ # for each item, text is the filename and data is the corresponding dict @return: the currently selected item data @rtype: dict """ self.cameraProfilesCombo = QComboBox() files = [self.targetImage.filename] files.extend(getDngProfileList(self.targetImage.cameraModel())) # load profiles items = OrderedDict([ (basename(f)[:-4] if i > 0 else 'Embedded Profile', getDngProfileDict(f)) for i, f in enumerate(files) ]) # add 'None' and all found profiles for the current camera model: 'None' will be the default selection # filter items to eliminate empty entries and # add non empty dicts to cameraProfileCombo for key in items: # filter items[key] d = {k: items[key][k] for k in items[key] if items[key][k] != ''} if d: self.cameraProfilesCombo.addItem(key, d) self.cameraProfilesCombo.addItem('None', {}) self.cameraProfilesCombo.setSizeAdjustPolicy( QComboBox.SizeAdjustPolicy.AdjustToContents) self.cameraProfilesCombo.setMaximumWidth(150) self.cameraProfilesCombo.setStyleSheet( "QComboBox QAbstractItemView { min-width: 250px;}") # return the currently selected item return self.cameraProfilesCombo.itemData(0) def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) outStream.writeQString(self.listWidget1.selectedItems()[0].text()) outStream.writeInt32(self.sliderExp.value()) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() temp = inStream.readInt32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderExp.setValue(temp) self.update() return inStream
def __init__(self, axeSize=500, layer=None, parent=None): super(noiseForm, self).__init__(parent=parent) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) # link back to image layer # using weak ref for back links if type(layer) in weakref.ProxyTypes: self.layer = layer else: self.layer = weakref.proxy(layer) # attribute initialized in setDefaults # defined here for the sake of correctness self.noiseCorrection = 0 # options optionList = ['Wavelets', 'Bilateral', 'NLMeans'] self.listWidget1 = optionsWidget( options=optionList, exclusive=True, changed=lambda: self.dataChanged.emit(True)) self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.options = self.listWidget1.options # threshold slider self.sliderThr = QbLUeSlider(Qt.Horizontal) self.sliderThr.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderThr.setTickPosition(QSlider.TicksBelow) self.sliderThr.setRange(0, 10) self.sliderThr.setSingleStep(1) self.sliderThr.valueChanged.connect(self.thrUpdate) self.sliderThr.sliderReleased.connect(lambda: self.thrUpdate( self.sliderThr.value())) # signal has no parameter) self.thrLabel = QLabel() self.thrLabel.setMaximumSize(150, 30) self.thrLabel.setText("level") self.thrValue = QLabel() font = self.thrValue.font() metrics = QFontMetrics(font) w = metrics.width("0000") h = metrics.height() self.thrValue.setMinimumSize(w, h) self.thrValue.setMaximumSize(w, h) self.thrValue.setText( str("{:.0f}".format(self.slider2Thr(self.sliderThr.value())))) # self.dataChanged.connect(self.updateLayer) # self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") # layout l = QVBoxLayout() #l.setAlignment(Qt.AlignBottom) l.addWidget(self.listWidget1) hl1 = QHBoxLayout() hl1.addWidget(self.thrLabel) hl1.addWidget(self.thrValue) hl1.addWidget(self.sliderThr) l.addLayout(hl1) l.setContentsMargins(20, 0, 20, 25) # left, top, right, bottom #l.setContentsMargins(10, 10, 10, 10) # left, top, right, bottom self.setLayout(l) self.adjustSize() self.setDefaults() self.setWhatsThis("""<b>Noise Reduction</b><br> <b>Bilateral Filtering</b> is the fastest method.<br> <b>NLMeans</b> (Non Local Means) and <b>Wavelets</b> are slower, but they usually give better results.<br> It is possible to <b>limit the application of all methods to a rectangular region of the image</b> by drawing a selection rectangle on the layer with the marquee tool.<br> Ctrl-Click to <b>clear the selection</b><br> """) # end of setWhatsThis
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 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, -axeSize // 2) 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> x-axes represent hue values from 0 to 360. The upper curve shows brightness multiplicative shifts (initially 1) and the lower curve hue additive shifts (initially 0). All pixel colors are changed by the specific shifts corresponding to their hue. 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 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): """ sets black/white points (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, -self.axeSize // 2) self.markerLabel.setText("%d" % h) self.update() def writeToStream(self, outStream): """ @param outStream: @type outStream: QDataStream @return: @rtype: QDataStream """ graphicsScene = self.scene() layer = graphicsScene.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) if layer.actionName in [ 'actionBrightness_Contrast', 'actionCurves_HSpB', 'actionCurves_Lab' ]: outStream.writeQString(self.listWidget1.selectedItems()[0].text()) graphicsScene.cubicRGB.writeToStream(outStream) graphicsScene.cubicR.writeToStream(outStream) graphicsScene.cubicG.writeToStream(outStream) graphicsScene.cubicB.writeToStream(outStream) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() graphicsScene = self.scene() graphicsScene.cubicRGB.readFromStream(inStream) graphicsScene.cubicR.readFromStream(inStream) graphicsScene.cubicG.readFromStream(inStream) graphicsScene.cubicB.readFromStream(inStream) return inStream
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
class temperatureForm(baseForm): # dataChanged = QtCore.Signal() @classmethod def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): wdgt = temperatureForm(axeSize=axeSize, layer=layer, parent=parent, mainForm=mainForm) wdgt.setWindowTitle(layer.name) return wdgt def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): super().__init__(parent=parent) self.tempCorrection = 6500 self.tintCorrection = 1.0 self.setStyleSheet( 'QRangeSlider * {border: 0px; padding: 0px; margin: 0px}') self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) # link back to image layer self.layer = weakProxy(layer) """ # using weak ref for back links if type(layer) in weakref.ProxyTypes: self.layer = layer else: self.layer = weakref.proxy(layer) """ self.defaultTemp = sRGBWP # ref temperature D65 self.defaultTint = 0 # options optionList, optionNames = ['Photo Filter', 'Chromatic Adaptation' ], ['Photo Filter', 'Chromatic Adaptation'] self.listWidget1 = optionsWidget( options=optionList, optionNames=optionNames, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[1]) self.options = self.listWidget1.options # 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) tempLabel = QbLUeLabel() tempLabel.setMaximumSize(150, 30) tempLabel.setText("Color temperature") tempLabel.doubleClicked.connect(lambda: self.sliderTemp.setValue( self.temp2Slider(self.defaultTemp))) self.tempValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("0000") 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) tintLabel = QbLUeLabel() tintLabel.setMaximumSize(150, 30) tintLabel.setText("Tint") 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())))) # temp change event handler def tempUpdate(value): 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 self.sliderTemp.valueChanged.disconnect() self.sliderTemp.sliderReleased.disconnect() self.tempCorrection = self.slider2Temp(value) self.dataChanged.emit() self.sliderTemp.valueChanged.connect(tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: tempUpdate(self.sliderTemp.value())) # tint change event handler def tintUpdate(value): 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 self.sliderTint.valueChanged.disconnect() self.sliderTint.sliderReleased.disconnect() self.tintCorrection = self.slider2Tint(value) self.dataChanged.emit() self.sliderTint.valueChanged.connect(tintUpdate) self.sliderTint.sliderReleased.connect( lambda: tintUpdate(self.sliderTint.value())) self.sliderTemp.valueChanged.connect(tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: tempUpdate(self.sliderTemp.value())) self.sliderTint.valueChanged.connect(tintUpdate) self.sliderTint.sliderReleased.connect( lambda: tintUpdate(self.sliderTint.value())) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(self.listWidget1) l.addWidget(tempLabel) hl = QHBoxLayout() hl.addWidget(self.tempValue) hl.addWidget(self.sliderTemp) l.addLayout(hl) l.addWidget(tintLabel) hl1 = QHBoxLayout() hl1.addWidget(self.tintValue) hl1.addWidget(self.sliderTint) l.addLayout(hl1) self.setLayout(l) self.adjustSize() self.dataChanged.connect( self.updateLayer) # TODO move to setDefaults 3/12/18 self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") self.setDefaults() self.setWhatsThis("""<b>Color Temperature</b><br> <b>Photo Filter</b> uses the multiply blending mode to mimic a color filter in front of the camera lens.<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): self.sliderTemp.setEnabled(True) self.sliderTint.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.dataChanged.emit() # removed 30/10/18 def updateLayer(self): """ data changed event handler. """ self.enableSliders() self.layer.applyToStack() self.layer.parentImage.onImageChanged() def slider2Temp(self, v): return v * 100 def temp2Slider(self, v): return int(v / 100) def sliderTemp2User(selfself, v): return v * 100 def slider2Tint(self, v): return (v - 50) / 50 def tint2Slider(self, v): return int((1.0 + v) * 50.0) def sliderTint2User(selfself, v): return int((v - 50) / 5.0) def writeToStream(self, outStream): layer = self.layer outStream.writeQString(layer.actionName) outStream.writeQString(layer.name) outStream.writeQString(self.listWidget1.selectedItems()[0].text()) outStream.writeInt32(self.sliderTemp.value() * 100) return outStream def readFromStream(self, inStream): actionName = inStream.readQString() name = inStream.readQString() sel = inStream.readQString() temp = inStream.readInt32() for r in range(self.listWidget1.count()): currentItem = self.listWidget1.item(r) if currentItem.text() == sel: self.listWidget.select(currentItem) self.sliderTemp.setValue(temp // 100) self.update() return inStream