def __init__(self, parent, text, lastDir): """ @param parent: @type parent: QObject @param text: @type text: str @param lastDir: @type lastDir:str """ # QDialog __init__ super().__init__() self.setWindowTitle(text) # File Dialog self.dlg = QFileDialog(caption=text, directory=lastDir) self.dlg.setOption(QFileDialog.DontUseNativeDialog) self.metaOption = QCheckBox('Remove Meta') # sliders self.sliderComp = QbLUeSlider(Qt.Horizontal) self.sliderComp.setTickPosition(QSlider.TicksBelow) self.sliderComp.setRange(0, 9) self.sliderComp.setSingleStep(1) self.sliderComp.setValue( 3 ) # 3 = default opencv imwrite value TODO changed 5 to 3 20/02/20 validate self.sliderQual = QbLUeSlider(Qt.Horizontal) self.sliderQual.setTickPosition(QSlider.TicksBelow) self.sliderQual.setRange(0, 100) self.sliderQual.setSingleStep(10) self.sliderQual.setValue( 95 ) # 95 = default opencv imwrite value # TODO changed 90 to 95 20/02/20 validate self.dlg.setVisible(True) l = QVBoxLayout() h = QHBoxLayout() l.addWidget(self.dlg) h.addWidget(self.metaOption) h.addWidget(QLabel("Quality")) h.addWidget(self.sliderQual) h.addWidget(QLabel("Compression")) h.addWidget(self.sliderComp) l.addLayout(h) self.setLayout(l) # file dialog close event handler def f(): self.close() self.dlg.finished.connect(f)
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) # options self.options = None # exposure slider self.sliderExp = QbLUeSlider(Qt.Horizontal) self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderExp.setRange(-20, 20) self.sliderExp.setSingleStep(1) expLabel = QbLUeLabel() expLabel.setMaximumSize(150, 30) expLabel.setText("Exposure Correction") expLabel.doubleClicked.connect( lambda: self.sliderExp.setValue(self.defaultExpCorrection)) self.expValue = QbLUeLabel() font = self.expValue.font() metrics = QFontMetrics(font) w = metrics.width("1000 ") h = metrics.height() self.expValue.setMinimumSize(w, h) self.expValue.setMaximumSize(w, h) # exp change/released slot def f(): self.expValue.setText( str("{:+.1f}".format(self.sliderExp.value() * self.defaultStep))) if self.sliderExp.isSliderDown() or (self.expCorrection == self.sliderExp.value() * self.defaultStep): return try: self.sliderExp.valueChanged.disconnect() self.sliderExp.sliderReleased.disconnect() except RuntimeError: pass self.expCorrection = self.sliderExp.value() * self.defaultStep self.dataChanged.emit() self.sliderExp.valueChanged.connect(f) self.sliderExp.sliderReleased.connect(f) self.sliderExp.valueChanged.connect(f) self.sliderExp.sliderReleased.connect(f) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(expLabel) hl = QHBoxLayout() hl.addWidget(self.expValue) hl.addWidget(self.sliderExp) l.addLayout(hl) l.setContentsMargins(20, 0, 20, 25) # left, top, right, bottom self.setLayout(l) self.adjustSize() self.setWhatsThis("""<b>Exposure Correction</b> Multiplicative correction in the linear sRGB color space.<br> Unit is the diaphragm stop.<br> """) # end setWhatsThis self.setDefaults()
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) # connect layer selectionChanged signal self.layer.selectionChanged.sig.connect(self.updateLayer) self.kernelCategory = filterIndex.UNSHARP # options self.optionList = [ 'Unsharp Mask', 'Sharpen', 'Gaussian Blur', 'Surface Blur' ] filters = [ filterIndex.UNSHARP, filterIndex.SHARPEN, filterIndex.BLUR1, filterIndex.SURFACEBLUR ] self.filterDict = dict( zip(self.optionList, filters)) # filters is not a dict: don't use UDict here self.listWidget1 = optionsWidget(options=self.optionList, exclusive=True, changed=self.dataChanged) # set initial selection to unsharp mask self.listWidget1.checkOption(self.optionList[0]) # sliders self.sliderRadius = QbLUeSlider(Qt.Horizontal) self.sliderRadius.setRange(1, 50) self.sliderRadius.setSingleStep(1) self.radiusLabel = QLabel() self.radiusLabel.setMaximumSize(150, 30) self.radiusLabel.setText("Radius") self.radiusValue = QLabel() font = self.radiusValue.font() metrics = QFontMetrics(font) w = metrics.width("1000 ") h = metrics.height() self.radiusValue.setMinimumSize(w, h) self.radiusValue.setMaximumSize(w, h) self.sliderAmount = QbLUeSlider(Qt.Horizontal) self.sliderAmount.setRange(0, 100) self.sliderAmount.setSingleStep(1) self.amountLabel = QLabel() self.amountLabel.setMaximumSize(150, 30) self.amountLabel.setText("Amount") self.amountValue = QLabel() font = self.radiusValue.font() metrics = QFontMetrics(font) w = metrics.width("1000 ") h = metrics.height() self.amountValue.setMinimumSize(w, h) self.amountValue.setMaximumSize(w, h) self.toneValue = QLabel() self.toneLabel = QLabel() self.toneLabel.setMaximumSize(150, 30) self.toneLabel.setText("Sigma") self.sliderTone = QbLUeSlider(Qt.Horizontal) self.sliderTone.setRange(0, 100) self.sliderTone.setSingleStep(1) font = self.radiusValue.font() metrics = QFontMetrics(font) w = metrics.width("1000 ") h = metrics.height() self.toneValue.setMinimumSize(w, h) self.toneValue.setMaximumSize(w, h) # value change/done slot def formUpdate(): self.radiusValue.setText(str('%d ' % self.sliderRadius.value())) self.amountValue.setText(str('%d ' % self.sliderAmount.value())) self.toneValue.setText(str('%d ' % self.sliderTone.value())) if self.sliderRadius.isSliderDown( ) or self.sliderAmount.isSliderDown( ) or self.sliderTone.isSliderDown(): return try: for slider in [ self.sliderRadius, self.sliderAmount, self.sliderTone ]: slider.valueChanged.disconnect() slider.sliderReleased.disconnect() except RuntimeError: pass self.tone = self.sliderTone.value() self.radius = self.sliderRadius.value() self.amount = self.sliderAmount.value() self.dataChanged.emit() for slider in [ self.sliderRadius, self.sliderAmount, self.sliderTone ]: slider.valueChanged.connect(formUpdate) slider.sliderReleased.connect(formUpdate) for slider in [self.sliderRadius, self.sliderAmount, self.sliderTone]: slider.valueChanged.connect(formUpdate) slider.sliderReleased.connect(formUpdate) # layout l = QVBoxLayout() l.addWidget(self.listWidget1) hl = QHBoxLayout() hl.addWidget(self.radiusLabel) hl.addWidget(self.radiusValue) hl.addWidget(self.sliderRadius) l.addLayout(hl) hl = QHBoxLayout() hl.addWidget(self.amountLabel) hl.addWidget(self.amountValue) hl.addWidget(self.sliderAmount) l.addLayout(hl) hl = QHBoxLayout() hl.addWidget(self.toneLabel) hl.addWidget(self.toneValue) hl.addWidget(self.sliderTone) l.addLayout(hl) l.setContentsMargins(20, 0, 20, 25) # left, top, right, bottom self.setLayout(l) self.setDefaults() self.setWhatsThis(""" <b>Unsharp Mask</b> and <b>Sharpen Mask</b> are used to sharpen an image. Unsharp Mask usually gives best results.<br> <b>Gaussian Blur</b> and <b>Surface Blur</b> are used to blur an image.<br> In contrast to Gaussian Blur, Surface Blur preserves edges and reduces noise, but it may be slow.<br> It is possible to <b>limit the effect of a filter to a rectangular region of the image</b> by drawing a selection rectangle on the layer with the marquee (rectangle) tool.<br> Ctrl Click <b>clears the selection</b><br> """) # end setWhatsThis
def __init__(self, parent): super().__init__(parent) self.img = None # graphic form to show : it # should correspond to the currently selected layer self.currentWin = None # mouse click event self.clicked.connect(self.viewClicked) # set behavior and styles self.setSelectionBehavior(QAbstractItemView.SelectRows) delegate = itemDelegate(parent=self) self.setItemDelegate(delegate) ic1 = QImage(":/images/resources/eye-icon.png") ic2 = QImage(":/images/resources/eye-icon-strike.png") delegate.px1 = QPixmap.fromImage(ic1) delegate.px2 = QPixmap.fromImage(ic2) ic1.invertPixels() ic2.invertPixels() delegate.inv_px1 = QPixmap.fromImage(ic1) delegate.inv_px2 = QPixmap.fromImage(ic2) self.setIconSize(QSize(20, 15)) self.verticalHeader().setMinimumSectionSize(-1) self.verticalHeader().setDefaultSectionSize( self.verticalHeader().minimumSectionSize()) self.horizontalHeader().setMinimumSectionSize(40) self.horizontalHeader().setDefaultSectionSize(40) # drag and drop self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setDragDropOverwriteMode(False) self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) ################################ # layer property GUI : # preview, blending mode, opacity, mask color ################################ # Preview option # We should use a QListWidget or a custom optionsWidget # (cf. utils.py) : adding it to QVBoxLayout with mode # Qt.AlignBottom does not work. self.previewOptionBox = QCheckBox('Preview') self.previewOptionBox.setMaximumSize(100, 30) # View/Preview changed slot def m(state): # state : Qt.Checked Qt.UnChecked if self.img is None: return useThumb = (state == Qt.Checked) if useThumb == self.img.useThumb: return self.img.useThumb = useThumb window.updateStatus() self.img.cacheInvalidate() try: QApplication.setOverrideCursor( Qt.WaitCursor ) # TODO 18/04/18 waitcursor already called by applytostack QApplication.processEvents() # update the whole stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() finally: QApplication.restoreOverrideCursor() QApplication.processEvents() self.previewOptionBox.stateChanged.connect(m) self.previewOptionBox.setChecked(True) # m is not triggered # title titleLabel = QLabel('Layer') titleLabel.setMaximumSize(100, 30) # opacity slider self.opacitySlider = QbLUeSlider(Qt.Horizontal) self.opacitySlider.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.opacitySlider.setTickPosition(QSlider.TicksBelow) self.opacitySlider.setRange(0, 100) self.opacitySlider.setSingleStep(1) self.opacitySlider.setSliderPosition(100) self.opacityValue = QLabel() font = self.opacityValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.opacityValue.setMinimumSize(w, h) self.opacityValue.setMaximumSize(w, h) self.opacityValue.setText('100 ') # opacity value changed event handler def f1(): self.opacityValue.setText(str('%d ' % self.opacitySlider.value())) # opacity slider released event handler def f2(): try: layer = self.img.getActiveLayer() layer.setOpacity(self.opacitySlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.opacitySlider.valueChanged.connect(f1) self.opacitySlider.sliderReleased.connect(f2) # mask color slider self.maskLabel = QLabel('Mask Color') maskSlider = QbLUeSlider(Qt.Horizontal) maskSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) maskSlider.setTickPosition(QSlider.TicksBelow) maskSlider.setRange(0, 100) maskSlider.setSingleStep(1) maskSlider.setSliderPosition(100) self.maskSlider = maskSlider self.maskValue = QLabel() font = self.maskValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.maskValue.setMinimumSize(w, h) self.maskValue.setMaximumSize(w, h) self.maskValue.setText('100 ') # masks are disbled by default self.maskLabel.setEnabled(False) self.maskSlider.setEnabled(False) self.maskValue.setEnabled(False) # mask value changed event handler def g1(): self.maskValue.setText(str('%d ' % self.maskSlider.value())) # mask slider released event handler def g2(): try: layer = self.img.getActiveLayer() layer.setColorMaskOpacity(self.maskSlider.value() * 255.0 / 100.0) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.maskSlider.valueChanged.connect(g1) self.maskSlider.sliderReleased.connect(g2) # blending mode combo box compLabel = QLabel() compLabel.setText("Blend") self.compositionModeDict = OrderedDict([ ('Normal', QPainter.CompositionMode_SourceOver), ('Plus', QPainter.CompositionMode_Plus), ('Multiply', QPainter.CompositionMode_Multiply), ('Screen', QPainter.CompositionMode_Screen), ('Overlay', QPainter.CompositionMode_Overlay), ('Darken', QPainter.CompositionMode_Darken), ('Lighten', QPainter.CompositionMode_Lighten), ('Color Dodge', QPainter.CompositionMode_ColorDodge), ('Color Burn', QPainter.CompositionMode_ColorBurn), ('Hard Light', QPainter.CompositionMode_HardLight), ('Soft Light', QPainter.CompositionMode_SoftLight), ('Difference', QPainter.CompositionMode_Difference), ('Exclusion', QPainter.CompositionMode_Exclusion), # Type of previous modes is QPainter.CompositionMode (Shiboken enum-type). # Next additional modes are not implemented by QPainter: ('Luminosity', -1), ('color', -2) ]) self.blendingModeCombo = QComboBox() for key in self.compositionModeDict: self.blendingModeCombo.addItem(key, self.compositionModeDict[key]) # combo box item changed slot def g(ind): layer = self.img.getActiveLayer() s = self.blendingModeCombo.currentText() newMode = self.compositionModeDict[str(s)] if newMode == layer.compositionMode: return layer.compositionMode = newMode layer.applyToStack() self.img.onImageChanged() self.blendingModeCombo.currentIndexChanged.connect(g) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) hl0 = QHBoxLayout() hl0.addWidget(titleLabel) hl0.addStretch(1) hl0.addWidget(self.previewOptionBox) l.addLayout(hl0) hl = QHBoxLayout() hl.addWidget(QLabel('Opacity')) hl.addWidget(self.opacityValue) hl.addWidget(self.opacitySlider) l.addLayout(hl) hl1 = QHBoxLayout() hl1.addWidget(self.maskLabel) hl1.addWidget(self.maskValue) hl1.addWidget(self.maskSlider) l.addLayout(hl1) l.setContentsMargins(0, 0, 10, 0) # left, top, right, bottom hl2 = QHBoxLayout() hl2.addWidget(compLabel) hl2.addWidget(self.blendingModeCombo) l.addLayout(hl2) for layout in [hl, hl1, hl2]: layout.setContentsMargins(5, 0, 0, 0) # this layout must be added to the propertyWidget object loaded from blue.ui : # we postpone it to in blue.py, after loading the main form. self.propertyLayout = l # shortcut actions self.actionDup = QAction('Duplicate layer', None) self.actionDup.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_J)) self.addAction(self.actionDup) def dup(): row = self.selectedIndexes()[0].row() # Stack index index = len(self.img.layersStack) - row - 1 layer = self.img.layersStack[index] if layer.isAdjustLayer(): return # add new layer to stack and set it to active self.img.dupLayer(index=index) # update layer view self.setLayers(self.img) self.actionDup.triggered.connect(dup) self.setWhatsThis("""<b>Layer Stack</b><br> To <b>toggle the layer visibility</b> click on the Eye icon.<br> To <b>add a mask</b> use the context menu to enable it and paint pixels with the Mask/Unmask tools in the left pane.<br> For <b>color mask<b/b>: <br> green pixels are masked,<br> red pixels are unmasked.<br> Other colors correspond to partially masked pixels.<br> Note that upper visible layers slow down mask edition.<br> """) # end of setWhatsThis
class QLayerView(QTableView): """ Display the stack of image layers. """ def __init__(self, parent): super().__init__(parent) self.img = None # graphic form to show : it # should correspond to the currently selected layer self.currentWin = None # mouse click event self.clicked.connect(self.viewClicked) # set behavior and styles self.setSelectionBehavior(QAbstractItemView.SelectRows) delegate = itemDelegate(parent=self) self.setItemDelegate(delegate) ic1 = QImage(":/images/resources/eye-icon.png") ic2 = QImage(":/images/resources/eye-icon-strike.png") delegate.px1 = QPixmap.fromImage(ic1) delegate.px2 = QPixmap.fromImage(ic2) ic1.invertPixels() ic2.invertPixels() delegate.inv_px1 = QPixmap.fromImage(ic1) delegate.inv_px2 = QPixmap.fromImage(ic2) self.setIconSize(QSize(20, 15)) self.verticalHeader().setMinimumSectionSize(-1) self.verticalHeader().setDefaultSectionSize( self.verticalHeader().minimumSectionSize()) self.horizontalHeader().setMinimumSectionSize(40) self.horizontalHeader().setDefaultSectionSize(40) # drag and drop self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setDragDropOverwriteMode(False) self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) ################################ # layer property GUI : # preview, blending mode, opacity, mask color ################################ # Preview option # We should use a QListWidget or a custom optionsWidget # (cf. utils.py) : adding it to QVBoxLayout with mode # Qt.AlignBottom does not work. self.previewOptionBox = QCheckBox('Preview') self.previewOptionBox.setMaximumSize(100, 30) # View/Preview changed slot def m(state): # state : Qt.Checked Qt.UnChecked if self.img is None: return useThumb = (state == Qt.Checked) if useThumb == self.img.useThumb: return self.img.useThumb = useThumb window.updateStatus() self.img.cacheInvalidate() try: QApplication.setOverrideCursor( Qt.WaitCursor ) # TODO 18/04/18 waitcursor already called by applytostack QApplication.processEvents() # update the whole stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() finally: QApplication.restoreOverrideCursor() QApplication.processEvents() self.previewOptionBox.stateChanged.connect(m) self.previewOptionBox.setChecked(True) # m is not triggered # title titleLabel = QLabel('Layer') titleLabel.setMaximumSize(100, 30) # opacity slider self.opacitySlider = QbLUeSlider(Qt.Horizontal) self.opacitySlider.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.opacitySlider.setTickPosition(QSlider.TicksBelow) self.opacitySlider.setRange(0, 100) self.opacitySlider.setSingleStep(1) self.opacitySlider.setSliderPosition(100) self.opacityValue = QLabel() font = self.opacityValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.opacityValue.setMinimumSize(w, h) self.opacityValue.setMaximumSize(w, h) self.opacityValue.setText('100 ') # opacity value changed event handler def f1(): self.opacityValue.setText(str('%d ' % self.opacitySlider.value())) # opacity slider released event handler def f2(): try: layer = self.img.getActiveLayer() layer.setOpacity(self.opacitySlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.opacitySlider.valueChanged.connect(f1) self.opacitySlider.sliderReleased.connect(f2) # mask color slider self.maskLabel = QLabel('Mask Color') maskSlider = QbLUeSlider(Qt.Horizontal) maskSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) maskSlider.setTickPosition(QSlider.TicksBelow) maskSlider.setRange(0, 100) maskSlider.setSingleStep(1) maskSlider.setSliderPosition(100) self.maskSlider = maskSlider self.maskValue = QLabel() font = self.maskValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.maskValue.setMinimumSize(w, h) self.maskValue.setMaximumSize(w, h) self.maskValue.setText('100 ') # masks are disbled by default self.maskLabel.setEnabled(False) self.maskSlider.setEnabled(False) self.maskValue.setEnabled(False) # mask value changed event handler def g1(): self.maskValue.setText(str('%d ' % self.maskSlider.value())) # mask slider released event handler def g2(): try: layer = self.img.getActiveLayer() layer.setColorMaskOpacity(self.maskSlider.value() * 255.0 / 100.0) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.maskSlider.valueChanged.connect(g1) self.maskSlider.sliderReleased.connect(g2) # blending mode combo box compLabel = QLabel() compLabel.setText("Blend") self.compositionModeDict = OrderedDict([ ('Normal', QPainter.CompositionMode_SourceOver), ('Plus', QPainter.CompositionMode_Plus), ('Multiply', QPainter.CompositionMode_Multiply), ('Screen', QPainter.CompositionMode_Screen), ('Overlay', QPainter.CompositionMode_Overlay), ('Darken', QPainter.CompositionMode_Darken), ('Lighten', QPainter.CompositionMode_Lighten), ('Color Dodge', QPainter.CompositionMode_ColorDodge), ('Color Burn', QPainter.CompositionMode_ColorBurn), ('Hard Light', QPainter.CompositionMode_HardLight), ('Soft Light', QPainter.CompositionMode_SoftLight), ('Difference', QPainter.CompositionMode_Difference), ('Exclusion', QPainter.CompositionMode_Exclusion), # Type of previous modes is QPainter.CompositionMode (Shiboken enum-type). # Next additional modes are not implemented by QPainter: ('Luminosity', -1), ('color', -2) ]) self.blendingModeCombo = QComboBox() for key in self.compositionModeDict: self.blendingModeCombo.addItem(key, self.compositionModeDict[key]) # combo box item changed slot def g(ind): layer = self.img.getActiveLayer() s = self.blendingModeCombo.currentText() newMode = self.compositionModeDict[str(s)] if newMode == layer.compositionMode: return layer.compositionMode = newMode layer.applyToStack() self.img.onImageChanged() self.blendingModeCombo.currentIndexChanged.connect(g) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) hl0 = QHBoxLayout() hl0.addWidget(titleLabel) hl0.addStretch(1) hl0.addWidget(self.previewOptionBox) l.addLayout(hl0) hl = QHBoxLayout() hl.addWidget(QLabel('Opacity')) hl.addWidget(self.opacityValue) hl.addWidget(self.opacitySlider) l.addLayout(hl) hl1 = QHBoxLayout() hl1.addWidget(self.maskLabel) hl1.addWidget(self.maskValue) hl1.addWidget(self.maskSlider) l.addLayout(hl1) l.setContentsMargins(0, 0, 10, 0) # left, top, right, bottom hl2 = QHBoxLayout() hl2.addWidget(compLabel) hl2.addWidget(self.blendingModeCombo) l.addLayout(hl2) for layout in [hl, hl1, hl2]: layout.setContentsMargins(5, 0, 0, 0) # this layout must be added to the propertyWidget object loaded from blue.ui : # we postpone it to in blue.py, after loading the main form. self.propertyLayout = l # shortcut actions self.actionDup = QAction('Duplicate layer', None) self.actionDup.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_J)) self.addAction(self.actionDup) def dup(): row = self.selectedIndexes()[0].row() # Stack index index = len(self.img.layersStack) - row - 1 layer = self.img.layersStack[index] if layer.isAdjustLayer(): return # add new layer to stack and set it to active self.img.dupLayer(index=index) # update layer view self.setLayers(self.img) self.actionDup.triggered.connect(dup) self.setWhatsThis("""<b>Layer Stack</b><br> To <b>toggle the layer visibility</b> click on the Eye icon.<br> To <b>add a mask</b> use the context menu to enable it and paint pixels with the Mask/Unmask tools in the left pane.<br> For <b>color mask<b/b>: <br> green pixels are masked,<br> red pixels are unmasked.<br> Other colors correspond to partially masked pixels.<br> Note that upper visible layers slow down mask edition.<br> """) # end of setWhatsThis def closeAdjustForms(self, delete=False): """ Close all windows associated with image layers. If delete is True (default False), the windows and their dock containers are deleted. @param delete: @type delete: boolean """ if self.img is None: return stack = self.img.layersStack for layer in stack: layer.closeView(delete=delete) if delete: self.currentWin = None gc.collect() def clear(self, delete=True): """ Reset LayerView and clear the back links to image. """ self.closeAdjustForms(delete=delete) self.img = None self.currentWin = None # model = layerModel() # model.setColumnCount(3) # TODO removed 21/01/20 validate self.setModel(None) def setLayers(self, mImg, delete=False): """ Displays the layer stack of a mImage instance. @param mImg: image @type mImg: mImage @param delete: @type delete: """ # close open adjustment windows # self.closeAdjustForms() self.clear(delete=delete) mImg.layerView = self # back link to image self.img = weakProxy(mImg) model = layerModel() model.setColumnCount(3) l = len(mImg.layersStack) # dataChanged event handler : enables edition of layer name def f(index1, index2): # index1 and index2 should be equal # only layer name should be editable # dropEvent emit dataChanged when setting item values. f must # return immediately from these calls, as they are possibly made with unconsistent data : # dragged rows are already removed from layersStack # and not yet removed from model. if l != self.model().rowCount(): return # only name is editable if index1.column() != 1: return row = index1.row() stackIndex = l - row - 1 mImg.layersStack[stackIndex].name = index1.data() model.dataChanged.connect(f) for r, lay in enumerate(reversed(mImg.layersStack)): try: lay.maskSettingsChanged.sig.disconnect() except RuntimeError: pass lay.maskSettingsChanged.sig.connect(self.updateRows) items = [] # col 0 : visibility icon if lay.visible: item_visible = QStandardItem( QIcon(":/images/resources/eye-icon.png"), "") else: item_visible = QStandardItem( QIcon(":/images/resources/eye-icon-strike.png"), "") items.append(item_visible) # col 1 : image icon (for non-adjustment layer only) and name if len(lay.name) <= 30: name = lay.name else: name = lay.name[:28] + '...' if hasattr(lay, 'inputImg'): item_name = QStandardItem(name) else: # icon with very small dim causes QPainter error # QPixmap.fromImage bug ? smallImg = lay.resized(50, 50) w, h = smallImg.width(), smallImg.height() if w < h / 5 or h < w / 5: item_name = QStandardItem(name) else: item_name = QStandardItem( QIcon(QPixmap.fromImage(smallImg)), name) # set tool tip to full name item_name.setToolTip(lay.name) items.append(item_name) item_mask = QStandardItem('m') items.append(item_mask) model.appendRow(items) self.setModel(model) self.horizontalHeader().hide() self.verticalHeader().hide() header = self.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeToContents) header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # lay out the graphic forms into right pane forms = [ item.view for item in mImg.layersStack if getattr(item, 'view', None) is not None ] for dock in forms: if TABBING: dockedForms = [item for item in forms if not item.isFloating()] dock.show() # needed to get working tabbing if dock not in dockedForms and dock.tabbed: if dockedForms: window.tabifyDockWidget(dockedForms[-1], dock) else: window.addDockWidget(Qt.RightDockWidgetArea, dock) dock.setFloating(False) # select active layer self.selectRow(len(mImg.layersStack) - 1 - mImg.activeLayerIndex) activeLayer = mImg.getActiveLayer() layerview = activeLayer.view if layerview is not None: layerview.show() if TABBING: layerview.raise_() # lay out subcontrols of activeLayer form = layerview.widget() for dk in form.subControls: dk.setVisible(form.options[dk.widget().optionName]) # clean up: we (re)dock all sucontrols dk.setFloating(False) # emit topLevelChanged signal self.opacitySlider.setSliderPosition(int(activeLayer.opacity * 100)) self.maskSlider.setSliderPosition( int(activeLayer.colorMaskOpacity * 100.0 / 255.0)) ind = self.blendingModeCombo.findData(activeLayer.compositionMode) self.blendingModeCombo.setCurrentIndex(ind) self.previewOptionBox.setChecked(activeLayer.parentImage.useThumb) #activeLayer.maskColor self.updateForm() """ # TODO removed 25/01/20 useless validate for item in self.img.layersStack: if hasattr(item, 'sourceIndex'): combo = item.getGraphicsForm().sourceCombo currentText = combo.currentText() combo.clear() for i, x in enumerate(self.img.layersStack): item.view.widget().sourceCombo.addItem(x.name, i) combo.setCurrentIndex(combo.findText(currentText)) """ def updateForm(self): activeLayer = self.img.getActiveLayer() if hasattr(activeLayer, 'view'): self.currentWin = activeLayer.view if self.currentWin is not None: self.currentWin.show() self.currentWin.activateWindow() def updateRow(self, row): minInd, maxInd = self.model().index(row, 0), self.model().index(row, 3) self.model().dataChanged.emit(minInd, maxInd) def updateRows(self): """ Update all rows. """ count = self.model().rowCount() minInd, maxInd = self.model().index(0, 0), self.model().index( count - 1, 3) self.model().dataChanged.emit(minInd, maxInd) def dropEvent(self, event): """ drop event handler : moving layer @param event: @type event: Qevent """ if event.source() is not self: return # get selected rows and layers rows = set([mi.row() for mi in self.selectedIndexes()]) rStack = self.img.layersStack[::-1] layers = [rStack[i] for i in rows] # get target row and layer targetRow = self.indexAt(event.pos()).row() targetLayer = rStack[targetRow] # remove target from selection if targetRow in rows: rows.discard(targetRow) rows = sorted(rows) if not rows: return # if target is below last row insert at the last position if targetRow == -1: targetRow = self.model().rowCount() # mapping of src (row) indices to target indices rowMapping = dict() for idx, row in enumerate(rows): if row < targetRow: rowMapping[row] = targetRow + idx else: rowMapping[row + len(rows)] = targetRow + idx # update layerStack using rowMapping # insert None items for _ in range(len(rows)): rStack.insert(targetRow, None) # copy moved items to their final place for srcRow, tgtRow in sorted( rowMapping.items()): # python 3 iteritems->items rStack[tgtRow] = rStack[srcRow] # remove moved items from their initial place for row in reversed(sorted( rowMapping.keys())): # python 3 iterkeys -> keys rStack.pop(row) self.img.layersStack = rStack[::-1] # update model # insert empty rows for _ in range(len(rows)): result = self.model().insertRow(targetRow, QModelIndex()) # copy moved rows to their final place colCount = self.model().columnCount() for srcRow, tgtRow in sorted( rowMapping.items()): # python 3 iteritems->items for col in range(0, colCount): # CAUTION : setItem calls the data changed event handler (cf. setLayers above) self.model().setItem(tgtRow, col, self.model().takeItem(srcRow, col)) # remove moved rows from their initial place and keep track of moved items movedDict = rowMapping.copy() for row in reversed(sorted( rowMapping.keys())): # python 3 iterkeys -> keys self.model().removeRow(row) for s, t in rowMapping.items(): if t > row: movedDict[s] -= 1 ######################################### sanity check for r in range(self.model().rowCount()): id = self.model().index(r, 1) if id.data() != rStack[r].name: raise ValueError('Drop Error') ######################################## # reselect moved rows sel = sorted(movedDict.values()) selectionModel = QtCore.QItemSelectionModel(self.model()) self.setSelectionModel(selectionModel) index1 = self.model().index(sel[0], 1) index2 = self.model().index(sel[-1], 1) itemSelection = QtCore.QItemSelection(index1, index2) self.selectionModel().select( itemSelection, QtCore.QItemSelectionModel.Rows | QtCore.QItemSelectionModel.Select) # multiple selection : display no window if len(sel) > 1: self.currentWin.hide() self.currentWin = None elif len(sel) == 1: self.img.setActiveLayer(len(self.img.layersStack) - sel[0] - 1) # update stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() def select(self, row, col): """ select item in view @param row: @type row: @param col: @type col: @return: @rtype: """ model = self.model() self.viewClicked(model.index(row, col)) def viewClicked(self, clickedIndex): """ Mouse clicked event handler. @param clickedIndex: @type clickedIndex: QModelIndex """ row = clickedIndex.row() rows = set([mi.row() for mi in self.selectedIndexes()]) # multiple selection : go to top of selection m = min(rows) if row != m: clickedIndex = self.model().index(m, clickedIndex.column()) layer = self.img.layersStack[-1 - row] self.actionDup.setEnabled(not layer.isAdjustLayer()) # toggle layer visibility if clickedIndex.column() == 0: # background layer is always visible if row == len(self.img.layersStack) - 1: return # layer.visible = not(layer.visible) layer.setVisible(not layer.visible) if self.currentWin is not None: self.currentWin.setVisible(layer.visible) if not layer.visible: self.currentWin = None if layer.tool is not None: layer.tool.setVisible(layer.visible) # update stack if layer.visible: layer.applyToStack() else: i = layer.getUpperVisibleStackIndex() if i >= 0: layer.parentImage.layersStack[i].applyToStack() else: # top layer : update only the presentation layer layer.parentImage.prLayer.execute(l=None, pool=None) self.img.onImageChanged() # update displayed window and active layer activeStackIndex = len(self.img.layersStack) - 1 - row activeLayer = self.img.setActiveLayer(activeStackIndex) # update color mask slider and label self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(activeLayer.maskIsSelected) self.maskValue.setEnabled(activeLayer.maskIsSelected) if self.currentWin is not None: # hide sucontrols for dk in self.currentWin.widget().subControls: dk.hide() if self.currentWin.isFloating(): self.currentWin.hide() self.currentWin = None if hasattr(activeLayer, "view"): self.currentWin = activeLayer.view if self.currentWin is not None and activeLayer.visible: self.currentWin.show() self.currentWin.raise_() # display subcontrols for dk in self.currentWin.widget().subControls: dk.setVisible( self.currentWin.widget().options[dk.widget().optionName]) # make self.currentWin the active window self.currentWin.activateWindow() # update opacity and composition mode for current layer opacity = int(layer.opacity * 100) self.opacityValue.setText(str('%d ' % opacity)) self.opacitySlider.setSliderPosition(opacity) compositionMode = layer.compositionMode ind = self.blendingModeCombo.findData(compositionMode) self.blendingModeCombo.setCurrentIndex(ind) # draw the right rectangle window.label.repaint() def initContextMenu(self): """ Context menu initialization @return: @rtype: QMenu """ menu = QMenu() # menu.actionReset = QAction('Reset To Default', None) # menu.actionLoadImage = QAction('Load New Image', None) # multiple selections menu.actionMerge = QAction('Merge Lower', None) # merge only adjustment layer with image layer menu.actionRepositionLayer = QAction('Reposition Layer(s)', None) menu.actionColorMaskEnable = QAction('Color', None) menu.actionOpacityMaskEnable = QAction('Opacity', None) menu.actionClippingMaskEnable = QAction('Clipping', None) menu.actionMaskDisable = QAction('Disabled', None) menu.actionMaskUndo = QAction('Undo Mask', None) menu.actionMaskRedo = QAction('Redo Mask', None) menu.actionMaskInvert = QAction('Invert Mask', None) menu.actionMaskReset_UM = QAction('Unmask All', None) menu.actionMaskReset_M = QAction('Mask All', None) menu.actionMaskCopy = QAction('Copy Mask to Clipboard', None) menu.actionImageCopy = QAction('Copy Image to Clipboard', None) menu.actionMaskPaste = QAction('Paste Mask', None) menu.actionImagePaste = QAction('Paste Image', None) menu.actionMaskDilate = QAction('Dilate Mask', None) menu.actionMaskErode = QAction('Erode Mask', None) menu.actionMaskSmooth = QAction('Smooth Mask', None) menu.actionMaskBright1 = QAction('Bright 1 Mask', None) menu.actionMaskBright2 = QAction('Bright 2 Mask', None) menu.actionMaskBright3 = QAction('Bright 3 Mask', None) menu.actionMaskDark1 = QAction('Dark 1 Mask', None) menu.actionMaskDark2 = QAction('Dark 2 Mask', None) menu.actionMaskDark3 = QAction('Dark 3 Mask', None) menu.actionMaskMid1 = QAction('Mid 1 Mask', None) menu.actionMaskMid2 = QAction('Mid 2 Mask', None) menu.actionMaskMid3 = QAction('Mid 3 Mask', None) menu.actionMergingFlag = QAction('Merged Layer', None) menu.actionMergingFlag.setCheckable(True) menu.actionColorMaskEnable.setCheckable(True) menu.actionOpacityMaskEnable.setCheckable(True) menu.actionClippingMaskEnable.setCheckable(True) menu.actionMaskDisable.setCheckable(True) #################### # Build menu ################### menu.addAction(menu.actionRepositionLayer) menu.addSeparator() # layer menu.addAction(menu.actionImageCopy) menu.addAction(menu.actionImagePaste) menu.addAction(menu.actionMergingFlag) menu.addSeparator() # mask menu.subMenuEnable = menu.addMenu('Mask...') menu.subMenuEnable.addAction(menu.actionColorMaskEnable) menu.subMenuEnable.addAction(menu.actionOpacityMaskEnable) menu.subMenuEnable.addAction(menu.actionClippingMaskEnable) menu.subMenuEnable.addAction(menu.actionMaskDisable) menu.addAction(menu.actionMaskUndo) menu.addAction(menu.actionMaskRedo) menu.addAction(menu.actionMaskInvert) menu.subMenuLum = menu.addMenu('Luminosity Mask...') for a in [ menu.actionMaskBright1, menu.actionMaskBright2, menu.actionMaskBright3, menu.actionMaskDark1, menu.actionMaskDark2, menu.actionMaskDark3, menu.actionMaskMid1, menu.actionMaskMid2, menu.actionMaskMid3 ]: menu.subMenuLum.addAction(a) menu.addAction(menu.actionMaskReset_UM) menu.addAction(menu.actionMaskReset_M) menu.addAction(menu.actionMaskCopy) menu.addAction(menu.actionMaskPaste) menu.addAction(menu.actionMaskDilate) menu.addAction(menu.actionMaskErode) menu.addAction(menu.actionMaskSmooth) menu.addSeparator() # miscellaneous # menu.addAction(menu.actionLoadImage) # to link actionDup with a shortcut, # it must be set in __init__ menu.addAction(self.actionDup) menu.addAction(menu.actionMerge) # menu.addAction(menu.actionReset) return menu def contextMenuEvent(self, event): """ context menu handler @param event @type event: QContextMenuEvent """ selection = self.selectedIndexes() if not selection: return # get a fresh context menu without connected actions # and with state corresponding to the currently clicked layer self.cMenu = self.initContextMenu() # get current selection rows = set([mi.row() for mi in selection]) rStack = self.img.layersStack[::-1] layers = [rStack[r] for r in rows] # get current position index = self.indexAt(event.pos()) layerStackIndex = len(self.img.layersStack) - 1 - index.row() layer = self.img.layersStack[layerStackIndex] lowerVisible = self.img.layersStack[layer.getLowerVisibleStackIndex()] lower = self.img.layersStack[layerStackIndex - 1] # case index == 0 doesn't matter # toggle actions self.cMenu.actionMergingFlag.setChecked(layer.mergingFlag) self.cMenu.actionMerge.setEnabled(not ( hasattr(layer, 'inputImg') or hasattr(lowerVisible, 'inputImg'))) self.actionDup.setEnabled(not layer.isAdjustLayer()) self.cMenu.actionColorMaskEnable.setChecked(layer.maskIsSelected and layer.maskIsEnabled) self.cMenu.actionOpacityMaskEnable.setChecked( (not layer.maskIsSelected) and layer.maskIsEnabled) self.cMenu.actionClippingMaskEnable.setChecked( layer.isClipping and (layer.maskIsSelected or layer.maskIsEnabled)) self.cMenu.actionMaskDisable.setChecked(not ( layer.isClipping or layer.maskIsSelected or layer.maskIsEnabled)) self.cMenu.actionMaskUndo.setEnabled(layer.historyListMask.canUndo()) self.cMenu.actionMaskRedo.setEnabled(layer.historyListMask.canRedo()) self.cMenu.subMenuEnable.setEnabled(len(rows) == 1) self.cMenu.actionMaskPaste.setEnabled( not QApplication.clipboard().image().isNull()) self.cMenu.actionImagePaste.setEnabled( not QApplication.clipboard().image().isNull()) self.cMenu.actionMergingFlag.setEnabled(layer.isImageLayer()) # Event handlers def RepositionLayer(): layer.xOffset, layer.yOffset = 0, 0 layer.Zoom_coeff = 1.0 layer.AltZoom_coeff = 1.0 layer.xAltOffset, layer.yAltOffset = 0, 0 layer.updatePixmap() self.img.onImageChanged() def merge(): layer.merge_with_layer_immediately_below() def testUpperVisibility(): pos = self.img.getStackIndex(layer) upperVisible = False for i in range(len(self.img.layersStack) - pos - 1): if self.img.layersStack[pos + 1 + i].visible: upperVisible = True break if upperVisible: dlgWarn("Upper visible layers slow down mask edition") return True return False def colorMaskEnable(): testUpperVisibility() layer.maskIsEnabled = True layer.maskIsSelected = True self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(layer.maskIsSelected) self.maskValue.setEnabled(layer.maskIsSelected) layer.applyToStack() self.img.onImageChanged() def opacityMaskEnable(): testUpperVisibility() layer.maskIsEnabled = True layer.maskIsSelected = False self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(layer.maskIsSelected) self.maskValue.setEnabled(layer.maskIsSelected) layer.applyToStack() self.img.onImageChanged() def clippingMaskEnable(): layer.maskIsEnabled = True layer.maskIsSelected = False self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(layer.maskIsSelected) self.maskValue.setEnabled(layer.maskIsSelected) layer.isClipping = True layer.applyToStack() self.img.onImageChanged() def maskDisable(): layer.maskIsEnabled = False layer.maskIsSelected = False self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(layer.maskIsSelected) self.maskValue.setEnabled(layer.maskIsSelected) layer.isClipping = False layer.applyToStack() self.img.onImageChanged() def undoMask(): try: layer.mask = layer.historyListMask.undo( saveitem=layer.mask.copy()) layer.applyToStack() self.img.onImageChanged() except ValueError: pass def redoMask(): try: layer.mask = layer.historyListMask.redo() layer.applyToStack() self.img.onImageChanged() except ValueError: pass def maskInvert(): layer.invertMask() # update mask stack layer.applyToStack() self.img.onImageChanged() def maskReset_UM(): layer.resetMask(maskAll=False) # update mask stack for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.execute(l=None, pool=None) self.img.onImageChanged() def maskReset_M(): layer.resetMask(maskAll=True) # update mask stack for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.execute(l=None, pool=None) self.img.onImageChanged() def maskCopy(): QApplication.clipboard().setImage(layer.mask) def imageCopy(): QApplication.clipboard().setImage(layer.getCurrentMaskedImage()) def maskPaste(): """ Pastes clipboard to mask and updates the stack. The clipboard image is scaled if its size does not match the size of the mask """ cb = QApplication.clipboard() if not cb.image().isNull(): img = cb.image() if img.size() == layer.mask.size(): layer.mask = img else: layer.mask = img.scaled(layer.mask.size()) layer.applyToStack() self.img.prLayer.execute(l=None, pool=None) self.img.onImageChanged() def imagePaste(): """ Pastes clipboard to mask and updates the stack. The clipboard image is scaled if its size does not match the size of the mask """ cb = QApplication.clipboard() if not cb.image().isNull(): srcImg = cb.image() if srcImg.size() == layer.size(): layer.setImage(srcImg) else: layer.setImage(srcImg.scaled(layer.size())) layer.applyToStack() self.img.onImageChanged() def maskDilate(): """ Increase the masked part of the image """ buf = QImageBuffer(layer.mask) buf[:, :, 2] = vImage.maskDilate(buf[:, :, 2]) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskErode(): """ Reduce the masked part of the image """ buf = QImageBuffer(layer.mask) buf[:, :, 2] = vImage.maskErode(buf[:, :, 2]) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskSmooth(): """ Smooth the mask boundary """ buf = QImageBuffer(layer.mask) buf[:, :, 2] = vImage.maskSmooth(buf[:, :, 2]) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskBright1(): layer.setMaskLuminosity(min=128, max=255) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskBright2(): layer.setMaskLuminosity(min=192, max=255) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskBright3(): layer.setMaskLuminosity(min=224, max=255) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskDark1(): layer.setMaskLuminosity(min=0, max=128) def maskDark2(): layer.setMaskLuminosity(min=0, max=64) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskDark3(): layer.setMaskLuminosity(min=0, max=32) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskMid1(): layer.setMaskLuminosity(min=64, max=192) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskMid2(): layer.setMaskLuminosity(min=96, max=160) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskMid3(): layer.setMaskLuminosity(min=112, max=144) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def mergingFlag(flag): layer.mergingFlag = flag self.cMenu.actionRepositionLayer.triggered.connect(RepositionLayer) # self.cMenu.actionLoadImage.triggered.connect(loadImage) self.cMenu.actionMerge.triggered.connect(merge) self.cMenu.actionColorMaskEnable.triggered.connect(colorMaskEnable) self.cMenu.actionOpacityMaskEnable.triggered.connect(opacityMaskEnable) self.cMenu.actionClippingMaskEnable.triggered.connect( clippingMaskEnable) self.cMenu.actionMaskDisable.triggered.connect(maskDisable) self.cMenu.actionMaskUndo.triggered.connect(undoMask) self.cMenu.actionMaskRedo.triggered.connect(redoMask) self.cMenu.actionMaskInvert.triggered.connect(maskInvert) self.cMenu.actionMaskReset_UM.triggered.connect(maskReset_UM) self.cMenu.actionMaskReset_M.triggered.connect(maskReset_M) self.cMenu.actionMaskCopy.triggered.connect(maskCopy) self.cMenu.actionMaskPaste.triggered.connect(maskPaste) self.cMenu.actionImageCopy.triggered.connect(imageCopy) self.cMenu.actionImagePaste.triggered.connect(imagePaste) self.cMenu.actionMaskDilate.triggered.connect(maskDilate) self.cMenu.actionMaskErode.triggered.connect(maskErode) self.cMenu.actionMaskSmooth.triggered.connect(maskSmooth) self.cMenu.actionMaskBright1.triggered.connect(maskBright1) self.cMenu.actionMaskBright2.triggered.connect(maskBright2) self.cMenu.actionMaskBright3.triggered.connect(maskBright3) self.cMenu.actionMaskDark1.triggered.connect(maskDark1) self.cMenu.actionMaskDark2.triggered.connect(maskDark2) self.cMenu.actionMaskDark3.triggered.connect(maskDark3) self.cMenu.actionMaskMid1.triggered.connect(maskMid1) self.cMenu.actionMaskMid2.triggered.connect(maskMid2) self.cMenu.actionMaskMid3.triggered.connect(maskMid3) self.cMenu.actionMergingFlag.toggled.connect(mergingFlag) self.cMenu.exec_(event.globalPos() - QPoint(400, 0)) # update table for row in rows: self.updateRow(row)
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) self.setMinimumSize(axeSize, axeSize+100) # contrast spline viewer self.contrastForm = None # options optionList1, optionNames1 = ['Multi-Mode', 'CLAHE'], ['Multi-Mode', 'CLAHE'] self.listWidget1 = optionsWidget(options=optionList1, optionNames=optionNames1, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.listWidget1.setStyleSheet("QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}") optionList2, optionNames2 = ['High', 'manualCurve'], ['Preserve Highlights', 'Show Contrast Curve'] def optionList2Change(item): if item.internalName == 'High': # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit() self.listWidget2 = optionsWidget(options=optionList2, optionNames=optionNames2, exclusive=False, changed=optionList2Change) self.listWidget2.checkOption(self.listWidget2.intNames[0]) self.listWidget2.setStyleSheet("QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}") self.options = UDict((self.listWidget1.options, self.listWidget2.options)) # contrast slider self.sliderContrast = QbLUeSlider(Qt.Horizontal) self.sliderContrast.setStyleSheet(QbLUeSlider.bLueSliderDefaultIBWStylesheet) self.sliderContrast.setRange(0, 10) self.sliderContrast.setSingleStep(1) contrastLabel = QbLUeLabel() contrastLabel.setMaximumSize(150, 30) contrastLabel.setText("Contrast Level") contrastLabel.doubleClicked.connect(lambda: self.sliderContrast.setValue(self.contrast2Slider(self.contrastDefault))) self.contrastValue = QLabel() font = self.contrastValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.contrastValue.setMinimumSize(w, h) self.contrastValue.setMaximumSize(w, h) self.contrastValue.setText(str("{:d}".format(self.sliderContrast.value()))) # contrast changed event handler. def contrastUpdate(value): self.contrastValue.setText(str("{:d}".format(self.sliderContrast.value()))) # move not yet terminated or value not modified if self.sliderContrast.isSliderDown() or self.slider2Contrast(value) == self.contrastCorrection: return self.sliderContrast.valueChanged.disconnect() self.sliderContrast.sliderReleased.disconnect() self.contrastCorrection = self.slider2Contrast(self.sliderContrast.value()) # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit() self.sliderContrast.valueChanged.connect(contrastUpdate) self.sliderContrast.sliderReleased.connect(lambda: contrastUpdate(self.sliderContrast.value())) self.sliderContrast.valueChanged.connect(contrastUpdate) self.sliderContrast.sliderReleased.connect(lambda: contrastUpdate(self.sliderContrast.value())) # saturation slider self.sliderSaturation = QbLUeSlider(Qt.Horizontal) self.sliderSaturation.setStyleSheet(QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderSaturation.setRange(0, 100) self.sliderSaturation.setSingleStep(1) saturationLabel = QbLUeLabel() saturationLabel.setMaximumSize(150, 30) saturationLabel.setText("Saturation") saturationLabel.doubleClicked.connect(lambda: self.sliderSaturation.setValue(self.saturation2Slider(self.saturationDefault))) self.saturationValue = QLabel() font = self.saturationValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.saturationValue.setMinimumSize(w, h) self.saturationValue.setMaximumSize(w, h) self.saturationValue.setText(str("{:+d}".format(self.sliderContrast.value()))) # saturation changed event handler def saturationUpdate(value): self.saturationValue.setText(str("{:+d}".format(int(self.slidersaturation2User(self.sliderSaturation.value()))))) # move not yet terminated or value not modified if self.sliderSaturation.isSliderDown() or self.slider2Saturation(value) == self.satCorrection: return self.sliderSaturation.valueChanged.disconnect() self.sliderSaturation.sliderReleased.disconnect() self.satCorrection = self.slider2Saturation(self.sliderSaturation.value()) self.dataChanged.emit() self.sliderSaturation.valueChanged.connect(saturationUpdate) self.sliderSaturation.sliderReleased.connect(lambda: saturationUpdate(self.sliderSaturation.value())) self.sliderSaturation.valueChanged.connect(saturationUpdate) self.sliderSaturation.sliderReleased.connect(lambda: saturationUpdate(self.sliderSaturation.value())) # brightness slider self.sliderBrightness = QbLUeSlider(Qt.Horizontal) self.sliderBrightness.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderBrightness.setRange(0, 100) self.sliderBrightness.setSingleStep(1) brightnessLabel = QbLUeLabel() brightnessLabel.setMaximumSize(150, 30) brightnessLabel.setText("Brightness") brightnessLabel.doubleClicked.connect(lambda: self.sliderBrightness.setValue(self.brightness2Slider(self.brightnessDefault))) self.brightnessValue = QLabel() font = self.brightnessValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.brightnessValue.setMinimumSize(w, h) self.brightnessValue.setMaximumSize(w, h) self.brightnessValue.setText(str("{:+d}".format(self.sliderContrast.value()))) # brightness changed event handler def brightnessUpdate(value): self.brightnessValue.setText(str("{:+d}".format(int(self.sliderBrightness2User(self.sliderBrightness.value()))))) # move not yet terminated or value not modified if self.sliderBrightness.isSliderDown() or self.slider2Brightness(value) == self.brightnessCorrection: return self.sliderBrightness.valueChanged.disconnect() self.sliderBrightness.sliderReleased.disconnect() self.brightnessCorrection = self.slider2Brightness(self.sliderBrightness.value()) self.dataChanged.emit() self.sliderBrightness.valueChanged.connect(brightnessUpdate) self.sliderBrightness.sliderReleased.connect(lambda: brightnessUpdate(self.sliderBrightness.value())) self.sliderBrightness.valueChanged.connect(brightnessUpdate) self.sliderBrightness.sliderReleased.connect(lambda: brightnessUpdate(self.sliderBrightness.value())) # attributes initialized in setDefaults, declared here # for the sake of correctness self.contrastCorrection = None # range self.satCorrection = None # range -0.5..0.5 self.brightnessCorrection = None # range -0.5..0.5 # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) gb1 = QGroupBox() gb1.setStyleSheet("QGroupBox {border: 1px solid gray; border-radius: 4px}") l1 = QVBoxLayout() ct = QLabel() ct.setText('Contrast') l.setAlignment(Qt.AlignTop) l1.addWidget(ct) l1.addWidget(self.listWidget1) gb1.setLayout(l1) l.addWidget(gb1) l.addWidget(self.listWidget2) l.addWidget(contrastLabel) hl = QHBoxLayout() hl.addWidget(self.contrastValue) hl.addWidget(self.sliderContrast) l.addLayout(hl) l.addWidget(brightnessLabel) hl3 = QHBoxLayout() hl3.addWidget(self.brightnessValue) hl3.addWidget(self.sliderBrightness) l.addLayout(hl3) l.addWidget(saturationLabel) hl2 = QHBoxLayout() hl2.addWidget(self.saturationValue) hl2.addWidget(self.sliderSaturation) l.addLayout(hl2) self.setLayout(l) self.adjustSize() self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") self.setDefaults() self.setWhatsThis( """<b>Contrast Brightness Saturation</b><br> <b>Contrast</b> is enhanced using one of these two methods:<br> - <b>CLAHE</b> : increases the local contrast.<br> - <b>Multi-Mode</b> : increases the local contrast and the contrast between regions of the image.<br> For both methods the contrast slider controls the level of the correction.<br> With Multi-Mode enabled, use the option <b>Show Contrast Curve</b> to edit the correction curve and check <b>Preserve Highlights</b> for softer highlights.<br> <b>Brightness</b> and <b>Saturation</b> corrections are non linear to limit clipping.<br> Sliders are <b>reset</b> to their default value by double clicking the name of the slider.<br> """ ) # end setWhatsThis
class CoBrSatForm(baseForm): """ Contrast, Brightness, Saturation adjustment form """ layerTitle = "Cont/Bright/Sat" contrastDefault = 0.0 brightnessDefault = 0.0 saturationDefault = 0.0 @classmethod def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None): wdgt = CoBrSatForm(targetImage=targetImage, axeSize=axeSize, layer=layer, parent=parent) wdgt.setWindowTitle(layer.name) return wdgt @classmethod def slider2Contrast(cls, v): return v / 10 @classmethod def contrast2Slider(cls, v): return v * 10 @classmethod def slider2Saturation(cls, v): return v / 100 - 0.5 @classmethod def saturation2Slider(cls, v): return v * 100 + 50 @classmethod def slidersaturation2User(cls, v): return v - 50 @classmethod def slider2Brightness(cls, v): return v / 100 - 0.5 @classmethod def brightness2Slider(cls, v): return v * 100 + 50 @classmethod def sliderBrightness2User(cls, v): return v - 50 def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) self.setMinimumSize(axeSize, axeSize+100) # contrast spline viewer self.contrastForm = None # options optionList1, optionNames1 = ['Multi-Mode', 'CLAHE'], ['Multi-Mode', 'CLAHE'] self.listWidget1 = optionsWidget(options=optionList1, optionNames=optionNames1, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.listWidget1.setStyleSheet("QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}") optionList2, optionNames2 = ['High', 'manualCurve'], ['Preserve Highlights', 'Show Contrast Curve'] def optionList2Change(item): if item.internalName == 'High': # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit() self.listWidget2 = optionsWidget(options=optionList2, optionNames=optionNames2, exclusive=False, changed=optionList2Change) self.listWidget2.checkOption(self.listWidget2.intNames[0]) self.listWidget2.setStyleSheet("QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}") self.options = UDict((self.listWidget1.options, self.listWidget2.options)) # contrast slider self.sliderContrast = QbLUeSlider(Qt.Horizontal) self.sliderContrast.setStyleSheet(QbLUeSlider.bLueSliderDefaultIBWStylesheet) self.sliderContrast.setRange(0, 10) self.sliderContrast.setSingleStep(1) contrastLabel = QbLUeLabel() contrastLabel.setMaximumSize(150, 30) contrastLabel.setText("Contrast Level") contrastLabel.doubleClicked.connect(lambda: self.sliderContrast.setValue(self.contrast2Slider(self.contrastDefault))) self.contrastValue = QLabel() font = self.contrastValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.contrastValue.setMinimumSize(w, h) self.contrastValue.setMaximumSize(w, h) self.contrastValue.setText(str("{:d}".format(self.sliderContrast.value()))) # contrast changed event handler. def contrastUpdate(value): self.contrastValue.setText(str("{:d}".format(self.sliderContrast.value()))) # move not yet terminated or value not modified if self.sliderContrast.isSliderDown() or self.slider2Contrast(value) == self.contrastCorrection: return self.sliderContrast.valueChanged.disconnect() self.sliderContrast.sliderReleased.disconnect() self.contrastCorrection = self.slider2Contrast(self.sliderContrast.value()) # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit() self.sliderContrast.valueChanged.connect(contrastUpdate) self.sliderContrast.sliderReleased.connect(lambda: contrastUpdate(self.sliderContrast.value())) self.sliderContrast.valueChanged.connect(contrastUpdate) self.sliderContrast.sliderReleased.connect(lambda: contrastUpdate(self.sliderContrast.value())) # saturation slider self.sliderSaturation = QbLUeSlider(Qt.Horizontal) self.sliderSaturation.setStyleSheet(QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderSaturation.setRange(0, 100) self.sliderSaturation.setSingleStep(1) saturationLabel = QbLUeLabel() saturationLabel.setMaximumSize(150, 30) saturationLabel.setText("Saturation") saturationLabel.doubleClicked.connect(lambda: self.sliderSaturation.setValue(self.saturation2Slider(self.saturationDefault))) self.saturationValue = QLabel() font = self.saturationValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.saturationValue.setMinimumSize(w, h) self.saturationValue.setMaximumSize(w, h) self.saturationValue.setText(str("{:+d}".format(self.sliderContrast.value()))) # saturation changed event handler def saturationUpdate(value): self.saturationValue.setText(str("{:+d}".format(int(self.slidersaturation2User(self.sliderSaturation.value()))))) # move not yet terminated or value not modified if self.sliderSaturation.isSliderDown() or self.slider2Saturation(value) == self.satCorrection: return self.sliderSaturation.valueChanged.disconnect() self.sliderSaturation.sliderReleased.disconnect() self.satCorrection = self.slider2Saturation(self.sliderSaturation.value()) self.dataChanged.emit() self.sliderSaturation.valueChanged.connect(saturationUpdate) self.sliderSaturation.sliderReleased.connect(lambda: saturationUpdate(self.sliderSaturation.value())) self.sliderSaturation.valueChanged.connect(saturationUpdate) self.sliderSaturation.sliderReleased.connect(lambda: saturationUpdate(self.sliderSaturation.value())) # brightness slider self.sliderBrightness = QbLUeSlider(Qt.Horizontal) self.sliderBrightness.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderBrightness.setRange(0, 100) self.sliderBrightness.setSingleStep(1) brightnessLabel = QbLUeLabel() brightnessLabel.setMaximumSize(150, 30) brightnessLabel.setText("Brightness") brightnessLabel.doubleClicked.connect(lambda: self.sliderBrightness.setValue(self.brightness2Slider(self.brightnessDefault))) self.brightnessValue = QLabel() font = self.brightnessValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.brightnessValue.setMinimumSize(w, h) self.brightnessValue.setMaximumSize(w, h) self.brightnessValue.setText(str("{:+d}".format(self.sliderContrast.value()))) # brightness changed event handler def brightnessUpdate(value): self.brightnessValue.setText(str("{:+d}".format(int(self.sliderBrightness2User(self.sliderBrightness.value()))))) # move not yet terminated or value not modified if self.sliderBrightness.isSliderDown() or self.slider2Brightness(value) == self.brightnessCorrection: return self.sliderBrightness.valueChanged.disconnect() self.sliderBrightness.sliderReleased.disconnect() self.brightnessCorrection = self.slider2Brightness(self.sliderBrightness.value()) self.dataChanged.emit() self.sliderBrightness.valueChanged.connect(brightnessUpdate) self.sliderBrightness.sliderReleased.connect(lambda: brightnessUpdate(self.sliderBrightness.value())) self.sliderBrightness.valueChanged.connect(brightnessUpdate) self.sliderBrightness.sliderReleased.connect(lambda: brightnessUpdate(self.sliderBrightness.value())) # attributes initialized in setDefaults, declared here # for the sake of correctness self.contrastCorrection = None # range self.satCorrection = None # range -0.5..0.5 self.brightnessCorrection = None # range -0.5..0.5 # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) gb1 = QGroupBox() gb1.setStyleSheet("QGroupBox {border: 1px solid gray; border-radius: 4px}") l1 = QVBoxLayout() ct = QLabel() ct.setText('Contrast') l.setAlignment(Qt.AlignTop) l1.addWidget(ct) l1.addWidget(self.listWidget1) gb1.setLayout(l1) l.addWidget(gb1) l.addWidget(self.listWidget2) l.addWidget(contrastLabel) hl = QHBoxLayout() hl.addWidget(self.contrastValue) hl.addWidget(self.sliderContrast) l.addLayout(hl) l.addWidget(brightnessLabel) hl3 = QHBoxLayout() hl3.addWidget(self.brightnessValue) hl3.addWidget(self.sliderBrightness) l.addLayout(hl3) l.addWidget(saturationLabel) hl2 = QHBoxLayout() hl2.addWidget(self.saturationValue) hl2.addWidget(self.sliderSaturation) l.addLayout(hl2) self.setLayout(l) self.adjustSize() self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") self.setDefaults() self.setWhatsThis( """<b>Contrast Brightness Saturation</b><br> <b>Contrast</b> is enhanced using one of these two methods:<br> - <b>CLAHE</b> : increases the local contrast.<br> - <b>Multi-Mode</b> : increases the local contrast and the contrast between regions of the image.<br> For both methods the contrast slider controls the level of the correction.<br> With Multi-Mode enabled, use the option <b>Show Contrast Curve</b> to edit the correction curve and check <b>Preserve Highlights</b> for softer highlights.<br> <b>Brightness</b> and <b>Saturation</b> corrections are non linear to limit clipping.<br> Sliders are <b>reset</b> to their default value by double clicking the name of the slider.<br> """ ) # end setWhatsThis def setContrastSpline(self, a, b, d, T): """ Updates and displays the contrast spline viewer. The form is created only once. (Cf also rawForm.setCoBrSat.setContrastSpline). @param a: x_coordinates @type a: @param b: y-coordinates @type b: @param d: tangent slopes @type d: @param T: spline @type T: ndarray dtype=float """ axeSize = 200 if self.contrastForm is None: form = graphicsSplineForm.getNewWindow(targetImage=None, axeSize=axeSize, layer=self.layer, parent=None) form.setAttribute(Qt.WA_DeleteOnClose, on=False) form.setWindowTitle('Contrast Curve') form.setMaximumSize(300, 400) self.contrastForm = form window = self.parent().parent() dock = stateAwareQDockWidget(self.parent()) dock.setWidget(form) dock.setWindowFlags(form.windowFlags()) dock.setWindowTitle(form.windowTitle()) dock.setStyleSheet("QGraphicsView{margin: 10px; border-style: solid; border-width: 1px; border-radius: 1px;}") window.addDockWidget(Qt.LeftDockWidgetArea, dock) self.dock = dock # curve changed slot def f(): self.layer.applyToStack() self.layer.parentImage.onImageChanged() form.scene().quadricB.curveChanged.sig.connect(f) else: form = self.contrastForm # update the curve form.scene().setSceneRect(-25, -axeSize-25, axeSize+50, axeSize+50) form.scene().quadricB.setCurve(a*axeSize, b*axeSize,d, T*axeSize) self.dock.showNormal() def updateHists(self): """ Update the histogram displayed under the contrast spline. """ if self.contrastForm is not None: self.contrastForm.updateHists() def enableSliders(self): self.sliderContrast.setEnabled(True) self.sliderSaturation.setEnabled(True) self.sliderBrightness.setEnabled(True) def setDefaults(self): try: self.dataChanged.disconnect() except RuntimeError: pass self.listWidget1.unCheckAll() self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.listWidget2.unCheckAll() self.listWidget2.checkOption(self.listWidget2.intNames[0]) self.enableSliders() self.contrastCorrection = self.contrastDefault self.sliderContrast.setValue(round(self.contrast2Slider(self.contrastCorrection))) self.satCorrection = self.saturationDefault self.sliderSaturation.setValue(round(self.saturation2Slider(self.satCorrection))) self.brightnessCorrection = self.brightnessDefault self.sliderBrightness.setValue(round(self.brightness2Slider(self.brightnessCorrection))) self.dataChanged.connect(self.updateLayer) def updateLayer(self): """ data changed slot. """ self.enableSliders() self.layer.applyToStack() self.layer.parentImage.onImageChanged() # enable/disable options relative to multi-mode for intname in ['High', 'manualCurve']: item = self.listWidget2.items[intname] if self.options['Multi-Mode']: item.setFlags(item.flags() | Qt.ItemIsEnabled) else: item.setFlags(item.flags() & ~Qt.ItemIsEnabled) # show/hide contrast curve cf = getattr(self, 'dock', None) if cf is None: return if self.options['manualCurve'] and self.options['Multi-Mode']: cf.showNormal() else: cf.hide()
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(targetImage=targetImage, axeSize=axeSize, layer=layer, parent=parent) graphicsScene = self.scene() # connect layer selectionChanged signal self.layer.selectionChanged.sig.connect(self.updateLayer) # Init curves dSplineItem = activeBSpline(axeSize, period=axeSize, yZero=-3 * axeSize // 4) graphicsScene.addItem(dSplineItem) dSplineItem.setVisible(True) dSplineItem.initFixedPoints() self.dSplineItemB = dSplineItem graphicsScene.dSplineItemB = dSplineItem text = graphicsScene.addText('Hue (0-360)') text.setDefaultTextColor(Qt.white) text.setPos(-40, 10) text = graphicsScene.addText('delta H') text.setDefaultTextColor(Qt.white) text.setPos(-40, -self.axeSize // 4) text = graphicsScene.addText('delta B') text.setDefaultTextColor(Qt.white) text.setPos(-40, -(self.axeSize * 3) // 4) dSplineItem = activeBSpline(axeSize, period=axeSize, yZero=-axeSize // 4) graphicsScene.addItem(dSplineItem) dSplineItem.setVisible(True) dSplineItem.initFixedPoints() self.dSplineItemH = dSplineItem graphicsScene.dSplineItemH = dSplineItem # init 3D LUT self.LUT = DeltaLUT3D((34, 32, 32)) self.marker = activeMarker.fromTriangle(parent=self.dSplineItemB) self.marker.setPos(0, 0) # -(axeSize * 3) // 4) self.marker.setMoveRange(QRect(0, 0, axeSize, 0)) self.scene().addItem(self.marker) def showPos(e, x, y): self.markerLabel.setText("%d" % (x * 360 // axeSize)) self.marker.onMouseMove = showPos self.markerLabel = QLabel() font = self.markerLabel.font() metrics = QFontMetrics(font) w = metrics.width("0000") h = metrics.height() self.markerLabel.setMinimumSize(w, h) self.markerLabel.setMaximumSize(w, h) self.sliderSat = QbLUeSlider(Qt.Horizontal) self.sliderSat.setMinimumWidth(200) def satUpdate(value): self.satValue.setText(str("{:d}".format(value))) # move not yet terminated or values not modified if self.sliderSat.isSliderDown() or value == self.satThr: return try: self.sliderSat.valueChanged.disconnect() self.sliderSat.sliderReleased.disconnect() except RuntimeError: pass self.satThr = value self.dataChanged.emit() self.sliderSat.valueChanged.connect(satUpdate) self.sliderSat.sliderReleased.connect( lambda: satUpdate(self.sliderSat.value())) self.sliderSat.valueChanged.connect(satUpdate) self.sliderSat.sliderReleased.connect( lambda: satUpdate(self.sliderSat.value())) self.satValue = QLabel() font = self.markerLabel.font() metrics = QFontMetrics(font) w = metrics.width("0000") h = metrics.height() self.satValue.setMinimumSize(w, h) self.satValue.setMaximumSize(w, h) # layout gl = QGridLayout() gl.addWidget(QLabel('Hue '), 0, 0) gl.addWidget(self.markerLabel, 0, 1) gl.addWidget(QLabel('Sat Thr '), 1, 0) gl.addWidget(self.satValue, 1, 1) gl.addWidget(self.sliderSat, 1, 2, 4, 1) self.addCommandLayout(gl) self.setDefaults() self.setWhatsThis("""<b>3D LUT Shift HSV</b><br> All pixel colors are changed by the specific hue and brightness shifts corresponding to their hue.<br> x-axis represents hue values from 0 to 360. The upper curve shows brightness multiplicative shifts (initially 1) and the lower curve hue additive shifts (initially 0). <br> Each curve is controlled by bump triangles.<br> To <b>add a bump triangle</b> to the curve click anywhere on the curve. To <b>remove the triangle</b> click on any vertex.<br> Drag the triangle vertices to move the bump along the x-axis and to change its height and orientation. Use the <b> Sat Thr</b> slider to preserve low saturated colors.<br> To <b>set the Hue Value Marker</b> Ctrl+click on the image.<br> To limit the shift corrections to a region of the image select the desired area with the rectangular marquee tool.<br> <b>Zoom</b> the curves with the mouse wheel.<br> """)
class HVLUT2DForm(graphicsCurveForm): """ Form for interactive HV 2D LUT """ @classmethod def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None): newWindow = HVLUT2DForm(targetImage=targetImage, axeSize=axeSize, layer=layer, parent=parent) newWindow.setWindowTitle(layer.name) return newWindow def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(targetImage=targetImage, axeSize=axeSize, layer=layer, parent=parent) graphicsScene = self.scene() # connect layer selectionChanged signal self.layer.selectionChanged.sig.connect(self.updateLayer) # Init curves dSplineItem = activeBSpline(axeSize, period=axeSize, yZero=-3 * axeSize // 4) graphicsScene.addItem(dSplineItem) dSplineItem.setVisible(True) dSplineItem.initFixedPoints() self.dSplineItemB = dSplineItem graphicsScene.dSplineItemB = dSplineItem text = graphicsScene.addText('Hue (0-360)') text.setDefaultTextColor(Qt.white) text.setPos(-40, 10) text = graphicsScene.addText('delta H') text.setDefaultTextColor(Qt.white) text.setPos(-40, -self.axeSize // 4) text = graphicsScene.addText('delta B') text.setDefaultTextColor(Qt.white) text.setPos(-40, -(self.axeSize * 3) // 4) dSplineItem = activeBSpline(axeSize, period=axeSize, yZero=-axeSize // 4) graphicsScene.addItem(dSplineItem) dSplineItem.setVisible(True) dSplineItem.initFixedPoints() self.dSplineItemH = dSplineItem graphicsScene.dSplineItemH = dSplineItem # init 3D LUT self.LUT = DeltaLUT3D((34, 32, 32)) self.marker = activeMarker.fromTriangle(parent=self.dSplineItemB) self.marker.setPos(0, 0) # -(axeSize * 3) // 4) self.marker.setMoveRange(QRect(0, 0, axeSize, 0)) self.scene().addItem(self.marker) def showPos(e, x, y): self.markerLabel.setText("%d" % (x * 360 // axeSize)) self.marker.onMouseMove = showPos self.markerLabel = QLabel() font = self.markerLabel.font() metrics = QFontMetrics(font) w = metrics.width("0000") h = metrics.height() self.markerLabel.setMinimumSize(w, h) self.markerLabel.setMaximumSize(w, h) self.sliderSat = QbLUeSlider(Qt.Horizontal) self.sliderSat.setMinimumWidth(200) def satUpdate(value): self.satValue.setText(str("{:d}".format(value))) # move not yet terminated or values not modified if self.sliderSat.isSliderDown() or value == self.satThr: return try: self.sliderSat.valueChanged.disconnect() self.sliderSat.sliderReleased.disconnect() except RuntimeError: pass self.satThr = value self.dataChanged.emit() self.sliderSat.valueChanged.connect(satUpdate) self.sliderSat.sliderReleased.connect( lambda: satUpdate(self.sliderSat.value())) self.sliderSat.valueChanged.connect(satUpdate) self.sliderSat.sliderReleased.connect( lambda: satUpdate(self.sliderSat.value())) self.satValue = QLabel() font = self.markerLabel.font() metrics = QFontMetrics(font) w = metrics.width("0000") h = metrics.height() self.satValue.setMinimumSize(w, h) self.satValue.setMaximumSize(w, h) # layout gl = QGridLayout() gl.addWidget(QLabel('Hue '), 0, 0) gl.addWidget(self.markerLabel, 0, 1) gl.addWidget(QLabel('Sat Thr '), 1, 0) gl.addWidget(self.satValue, 1, 1) gl.addWidget(self.sliderSat, 1, 2, 4, 1) self.addCommandLayout(gl) self.setDefaults() self.setWhatsThis("""<b>3D LUT Shift HSV</b><br> All pixel colors are changed by the specific hue and brightness shifts corresponding to their hue.<br> x-axis represents hue values from 0 to 360. The upper curve shows brightness multiplicative shifts (initially 1) and the lower curve hue additive shifts (initially 0). <br> Each curve is controlled by bump triangles.<br> To <b>add a bump triangle</b> to the curve click anywhere on the curve. To <b>remove the triangle</b> click on any vertex.<br> Drag the triangle vertices to move the bump along the x-axis and to change its height and orientation. Use the <b> Sat Thr</b> slider to preserve low saturated colors.<br> To <b>set the Hue Value Marker</b> Ctrl+click on the image.<br> To limit the shift corrections to a region of the image select the desired area with the rectangular marquee tool.<br> <b>Zoom</b> the curves with the mouse wheel.<br> """) def setDefaults(self): try: self.dataChanged.disconnect() self.dSplineItemB.curveChanged.sig.disconnect() self.dSplineItemH.curveChanged.sig.disconnect() except RuntimeError: pass self.satThr = 10 self.sliderSat.setValue(self.satThr) self.dataChanged.connect(self.updateLayer) self.dSplineItemB.curveChanged.sig.connect(self.updateLayer) self.dSplineItemH.curveChanged.sig.connect(self.updateLayer) def updateLUT(self): """ Updates the displacement LUT """ data = self.LUT.data axeSize = self.axeSize hdivs = self.LUT.divs[0] # sat threshold sThr = int(self.LUT.divs[1] * self.satThr / 100) hstep = 360 / hdivs activeSpline = self.dSplineItemB sp = activeSpline.spline[activeSpline.periodViewing:] d = activeSpline.yZero # reinit the LUT data[...] = 0, 1, 1 # update brightness for i in range(hdivs): pt = sp[int(i * hstep * axeSize / 360)] data[i, sThr:, :, 2] = 1.0 - (pt.y() - d) / 100 activeSpline = self.dSplineItemH sp = activeSpline.spline[activeSpline.periodViewing:] d = activeSpline.yZero for i in range(hdivs): pt = sp[int(i * hstep * axeSize / 360)] data[i, sThr:, :, 0] = -(pt.y() - d) / 5 def updateLayer(self): self.updateLUT() l = self.scene().layer l.applyToStack() l.parentImage.onImageChanged() def colorPickedSlot(self, x, y, modifiers): """ Updates cursor from the hue of an image pixel. (x,y) coordinates are relative to the full size image. @param x: @type x: @param y: @type y: @param modifiers: @type modifiers: """ color = self.scene().targetImage.getActivePixel(x, y, qcolor=True) h = color.hsvHue() if modifiers & QtCore.Qt.ControlModifier: self.marker.setPos(h * 300 / 360, 0) # -(self.axeSize * 3) // 4) self.markerLabel.setText("%d" % h) self.update()
class rawForm(baseForm): """ Postprocessing of raw files. """ dataChanged = QtCore.Signal(int) @classmethod def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None): wdgt = rawForm(axeSize=axeSize, targetImage=targetImage, layer=layer, parent=parent) wdgt.setWindowTitle(layer.name) return wdgt @staticmethod def slider2Temp(v): return 2000 + v * v @staticmethod def temp2Slider(T): return np.sqrt(T - 2000) @staticmethod def slider2Tint(v): return 0.1 + 0.0125 * v # 0.2 + 0.0125 * v # wanted range : 0.2...2.5 # coeff = (self.tempCorrection / 4000 - 1) * 1.2 # experimental formula # eturn coeff + 0.01*v @staticmethod def tint2Slider(t): return (t - 0.1) / 0.0125 # coeff = (self.tempCorrection / 4000 - 1) * 1.2 # experimental formula # return (t-coeff)/0.01 # displayed value @staticmethod def sliderTint2User(v): return v - 75 # ((slider2Tint(v) - 1)*100) @staticmethod def slider2Exp(v): return 2**((v - 50) / 15.0) @staticmethod def exp2Slider(e): return round(15 * np.log2(e) + 50) @staticmethod def sliderExp2User(v): return (v - 50) / 15 @staticmethod def slider2Cont(v): return v @staticmethod def cont2Slider(e): return e @staticmethod def slider2Br(v): return (np.power(3, v / 50) - 1) / 2 @staticmethod def br2Slider(v): return 50 * log(2 * v + 1, 3) # int(round(50.0 * e)) @staticmethod def brSlider2User(v): return v - 50 @staticmethod def slider2Sat(v): return v - 50 @staticmethod def sat2Slider(e): return e + 50 def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) ####################################### # Libraw correspondences: # rgb_xyz_matrix is libraw cam_xyz # camera_whitebalance is libraw cam_mul # daylight_whitebalance is libraw pre_mul # dng correspondences: # ASSHOTNEUTRAL tag value is (X,Y,Z) = 1 / rawpyObj.camera_whitebalance ########################################## rawpyObj = layer.parentImage.rawImage # constants and as shot values self.XYZ2CameraMatrix = rawpyObj.rgb_xyz_matrix[:3, :] self.XYZ2CameraInverseMatrix = np.linalg.inv(self.XYZ2CameraMatrix) # initial post processing multipliers (as shot) m1, m2, m3, m4 = rawpyObj.camera_whitebalance self.asShotMultipliers = ( m1 / m2, 1.0, m3 / m2, m4 / m2 ) # normalization is mandatory : for nef files white balance is around 256 self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint( *1 / np.array(self.asShotMultipliers[:3]), self.XYZ2CameraMatrix) self.rawMultipliers = self.asShotMultipliers # rawpyObj.camera_whitebalance # = 1/(dng ASSHOTNEUTRAL tag value) self.sampleMultipliers = False self.samples = [] ######################################## # XYZ-->Camera conversion matrix: # Last row is zero for RGB cameras (cf. rawpy and libraw docs). # type ndarray, shape (4,3) ######################################### # attributes initialized in setDefaults, declared here for the sake of correctness self.tempCorrection, self.tintCorrection, self.expCorrection, self.highCorrection,\ self.contCorrection, self.satCorrection, self.brCorrection = [None] * 7 # contrast spline vie (initialized by setContrastSpline) self.contrastForm = None # tone spline view (initialized by setToneSpline) self.toneForm = None # dock containers for contrast and tome forms self.dockC, self.dockT = None, None # options optionList0, optionNames0 = ['Auto Brightness', 'Preserve Highlights' ], ['Auto Expose', 'Preserve Highlights'] optionList1, optionNames1 = ['Auto WB', 'Camera WB', 'User WB' ], ['Auto', 'Camera (As Shot)', 'User'] optionList2, optionNames2 = [ 'cpLookTable', 'cpToneCurve', 'manualCurve' ], [ 'Use Camera Profile Look Table', 'Show Tone Curves', 'Show Contrast Curve' ] self.listWidget1 = optionsWidget( options=optionList0, optionNames=optionNames0, exclusive=False, changed=lambda: self.dataChanged.emit(1)) self.listWidget2 = optionsWidget( options=optionList1, optionNames=optionNames1, exclusive=True, changed=lambda: self.dataChanged.emit(1)) self.listWidget3 = optionsWidget( options=optionList2, optionNames=optionNames2, exclusive=False, changed=lambda: self.dataChanged.emit(2)) self.options = UDict( (self.listWidget1.options, self.listWidget2.options, self.listWidget3.options)) # display the 'as shot' temperature item = self.listWidget2.item(1) item.setText(item.text() + ' : %d' % self.asShotTemp) # temperature slider self.sliderTemp = QbLUeSlider(Qt.Horizontal) self.sliderTemp.setStyleSheet( QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderTemp.setRange(0, 100) self.sliderTemp.setSingleStep(1) self.tempLabel = QLabel() self.tempLabel.setText("Temp") self.tempValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("10000") h = metrics.height() self.tempValue.setMinimumSize(w, h) self.tempValue.setMaximumSize(w, h) self.tempValue.setText( str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value())))) self.sliderTemp.valueChanged.connect( self.tempUpdate) # signal send new value as parameter self.sliderTemp.sliderReleased.connect(lambda: self.tempUpdate( self.sliderTemp.value())) # signal pass no parameter # tint slider self.sliderTint = QbLUeSlider(Qt.Horizontal) self.sliderTint.setStyleSheet( QbLUeSlider.bLueSliderDefaultIMGColorStylesheet) self.sliderTint.setRange(0, 150) self.sliderTint.setSingleStep(1) self.tintLabel = QLabel() self.tintLabel.setText("Tint") self.tintValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.tintValue.setMinimumSize(w, h) self.tintValue.setMaximumSize(w, h) self.tintValue.setText( str("{:.0f}".format(self.sliderTint2User( self.sliderTint.value())))) self.sliderTint.valueChanged.connect(self.tintUpdate) self.sliderTint.sliderReleased.connect(lambda: self.tintUpdate( self.sliderTint.value())) # signal pass no parameter) ###################### # From libraw and dcraw sources: # Exposure and brightness are curve transformations. # Exposure curve is y = alpha*x, with cubic root ending; it is applied before demosaicing. # Brightness is (similar to) y = x**alpha and part of gamma transformation from linear sRGB to RGB. # Exposure and brightness both dilate the histogram towards highlights. # Exposure dilatation is uniform (homothety), brightness dilataion is # maximum for the midtones and the highlghts are preserved. # As a consequence, normal workflow begins with the adjustment of exposure, # to fill the entire range of the histogram and to adjust the highlights. Next, # one adjusts the brightness to put the midtones at the level we want them to be. # Cf. https://www.cambridgeincolour.com/forums/thread653.htm ##################### # profile combo self.dngDict = self.setCameraProfilesCombo() # cameraProfilesCombo index changed event handler def cameraProfileUpdate(value): self.dngDict = self.cameraProfilesCombo.itemData(value) if self.options['cpToneCurve']: toneCurve = dngProfileToneCurve( self.dngDict.get('ProfileToneCurve', [])) self.toneForm.baseCurve = [ QPointF(x * axeSize, -y * axeSize) for x, y in zip(toneCurve.dataX, toneCurve.dataY) ] self.toneForm.update() # recompute as shot temp and tint using new profile self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint( *1 / np.array(self.asShotMultipliers[:3]), self.XYZ2CameraMatrix, dngDict=self.dngDict ) # TODO 6/12/19 added keyword dngDict validate # display updated as shot temp item = self.listWidget2.item(1) item.setText(item.text().split(":")[0] + ': %d' % self.asShotTemp) # invalidate cache self.layer.bufCache_HSV_CV32 = None self.dataChanged.emit(2) # 2 = no postprocessing self.cameraProfilesCombo.currentIndexChanged.connect( cameraProfileUpdate) # denoising combo self.denoiseCombo = QComboBox() items = OrderedDict([('Off', 0), ('Medium', 1), ('Full', 2)]) for key in items: self.denoiseCombo.addItem(key, items[key]) # denoiseCombo index changed event handler def denoiseUpdate(value): self.denoiseValue = self.denoiseCombo.itemData(value) self.dataChanged.emit(1) self.denoiseCombo.currentIndexChanged.connect(denoiseUpdate) # overexposed area restoration self.overexpCombo = QComboBox() items = OrderedDict([('Clip', 0), ('Ignore', 1), ('Blend', 2), ('Reconstruct', 3)]) for key in items: self.overexpCombo.addItem(key, items[key]) # overexpCombo index changed event handler def overexpUpdate(value): self.overexpValue = self.overexpCombo.itemData(value) self.dataChanged.emit(1) self.overexpCombo.currentIndexChanged.connect(overexpUpdate) # exp slider self.sliderExp = QbLUeSlider(Qt.Horizontal) self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderExp.setRange(0, 100) self.sliderExp.setSingleStep(1) self.expLabel = QLabel() self.expLabel.setText("Exp.") self.expValue = QLabel() font = self.expValue.font() metrics = QFontMetrics(font) w = metrics.width("+1.0") h = metrics.height() self.expValue.setMinimumSize(w, h) self.expValue.setMaximumSize(w, h) self.expValue.setText( str("{:.1f}".format(self.slider2Exp(self.sliderExp.value())))) # exp done event handler def expUpdate(value): self.expValue.setText( str("{:+.1f}".format( self.sliderExp2User(self.sliderExp.value())))) # move not yet terminated or value not modified if self.sliderExp.isSliderDown() or self.slider2Exp( value) == self.expCorrection: return try: self.sliderExp.valueChanged.disconnect() self.sliderExp.sliderReleased.disconnect() except RuntimeError: pass # rawpy: expCorrection range is -2.0...3.0, boiling down to exp_shift range 2**(-2)=0.25...2**3=8.0 self.expCorrection = self.slider2Exp(self.sliderExp.value()) self.dataChanged.emit(1) self.sliderExp.valueChanged.connect( expUpdate) # send new value as parameter self.sliderExp.sliderReleased.connect(lambda: expUpdate( self.sliderExp.value())) # signal pass no parameter self.sliderExp.valueChanged.connect( expUpdate) # send new value as parameter self.sliderExp.sliderReleased.connect(lambda: expUpdate( self.sliderExp.value())) # signal pass no parameter # brightness slider brSlider = QbLUeSlider(Qt.Horizontal) brSlider.setRange(1, 101) self.sliderExp.setSingleStep(1) brSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderBrightness = brSlider brLabel = QLabel() brLabel.setText("Bright.") self.brValue = QLabel() font = self.expValue.font() metrics = QFontMetrics(font) w = metrics.width("+99") h = metrics.height() self.brValue.setMinimumSize(w, h) self.brValue.setMaximumSize(w, h) self.brValue.setText( str("{:+d}".format( int(self.brSlider2User(self.sliderBrightness.value()))))) # brightness done event handler def brUpdate(value): self.brValue.setText( str("{:+d}".format( int(self.brSlider2User(self.sliderBrightness.value()))))) # move not yet terminated or value not modified if self.sliderBrightness.isSliderDown() or self.slider2Br( value) == self.brCorrection: return try: self.sliderBrightness.valueChanged.disconnect() self.sliderBrightness.sliderReleased.disconnect() except RuntimeError: pass self.brCorrection = self.slider2Br(self.sliderBrightness.value()) self.dataChanged.emit(1) self.sliderBrightness.sliderReleased.connect( lambda: brUpdate(self.sliderBrightness.value())) self.sliderBrightness.valueChanged.connect( brUpdate) # send new value as parameter self.sliderBrightness.valueChanged.connect( brUpdate) # send new value as parameter self.sliderBrightness.sliderReleased.connect( lambda: brUpdate(self.sliderBrightness.value())) # contrast slider self.sliderCont = QbLUeSlider(Qt.Horizontal) self.sliderCont.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderCont.setRange(0, 20) self.sliderCont.setSingleStep(1) self.contLabel = QLabel() self.contLabel.setText("Cont.") self.contValue = QLabel() font = self.contValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.contValue.setMinimumSize(w, h) self.contValue.setMaximumSize(w, h) self.contValue.setText( str("{:.0f}".format(self.slider2Cont(self.sliderCont.value())))) # cont done event handler def contUpdate(value): self.contValue.setText( str("{:.0f}".format(self.slider2Cont( self.sliderCont.value())))) # move not yet terminated or value not modified if self.sliderCont.isSliderDown() or self.slider2Cont( value) == self.tempCorrection: return try: self.sliderCont.valueChanged.disconnect() self.sliderCont.sliderReleased.disconnect() except RuntimeError: pass self.contCorrection = self.slider2Cont(self.sliderCont.value()) self.contValue.setText(str("{:+d}".format(self.contCorrection))) # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit( 3) # no postprocessing and no camera profile stuff self.sliderCont.valueChanged.connect( contUpdate) # send new value as parameter self.sliderCont.sliderReleased.connect(lambda: contUpdate( self.sliderCont.value())) # signal has no parameter self.sliderCont.valueChanged.connect( contUpdate) # send new value as parameter self.sliderCont.sliderReleased.connect(lambda: contUpdate( self.sliderCont.value())) # signal has no parameter # saturation slider self.sliderSat = QbLUeSlider(Qt.Horizontal) self.sliderSat.setStyleSheet( QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderSat.setRange(0, 100) self.sliderSat.setSingleStep(1) satLabel = QLabel() satLabel.setText("Sat.") self.satValue = QLabel() font = self.satValue.font() metrics = QFontMetrics(font) w = metrics.width("+10") h = metrics.height() self.satValue.setMinimumSize(w, h) self.satValue.setMaximumSize(w, h) self.satValue.setText( str("{:+d}".format(self.slider2Sat(self.sliderSat.value())))) """sat done event handler""" def satUpdate(value): self.satValue.setText( str("{:+d}".format(self.slider2Sat(self.sliderSat.value())))) # move not yet terminated or value not modified if self.sliderSat.isSliderDown() or self.slider2Sat( value) == self.satCorrection: return try: self.sliderSat.valueChanged.disconnect() self.sliderSat.sliderReleased.disconnect() except RuntimeError: pass self.satCorrection = self.slider2Sat(self.sliderSat.value()) self.dataChanged.emit( 3) # no post processing and no camera profile stuff self.sliderSat.valueChanged.connect( satUpdate) # send new value as parameter self.sliderSat.sliderReleased.connect(lambda: satUpdate( self.sliderSat.value())) # signal has no parameter self.sliderSat.valueChanged.connect( satUpdate) # send new value as parameter self.sliderSat.sliderReleased.connect(lambda: satUpdate( self.sliderSat.value())) # signal has no parameter # layout l = QVBoxLayout() l.addWidget(self.listWidget3) hl01 = QHBoxLayout() hl01.addWidget(QLabel('Camera Profile')) hl01.addWidget(self.cameraProfilesCombo) l.addLayout(hl01) hl0 = QHBoxLayout() hl0.addWidget(QLabel('Denoising')) hl0.addWidget(self.denoiseCombo) l.addLayout(hl0) hl00 = QHBoxLayout() hl00.addWidget(QLabel('Overexp. Restoration')) hl00.addWidget(self.overexpCombo) l.addLayout(hl00) hl1 = QHBoxLayout() hl1.addWidget(self.expLabel) hl1.addWidget(self.expValue) hl1.addWidget(self.sliderExp) l.addLayout(hl1) hl8 = QHBoxLayout() hl8.addWidget(brLabel) hl8.addWidget(self.brValue) hl8.addWidget(self.sliderBrightness) l.addLayout(hl8) l.addWidget(self.listWidget1) vl1 = QVBoxLayout() vl1.addWidget(self.listWidget2) gb1 = QGroupBox() gb1.setTitle('White Balance') hl2 = QHBoxLayout() hl2.addWidget(self.tempLabel) hl2.addWidget(self.tempValue) hl2.addWidget(self.sliderTemp) hl3 = QHBoxLayout() hl3.addWidget(self.tintLabel) hl3.addWidget(self.tintValue) hl3.addWidget(self.sliderTint) vl1.addLayout(hl2) vl1.addLayout(hl3) gb1.setLayout(vl1) l.addWidget(gb1) hl4 = QHBoxLayout() hl4.addWidget(self.contLabel) hl4.addWidget(self.contValue) hl4.addWidget(self.sliderCont) hl7 = QHBoxLayout() hl7.addWidget(satLabel) hl7.addWidget(self.satValue) hl7.addWidget(self.sliderSat) # separator sep = QFrame() sep.setFrameShape(QFrame.HLine) sep.setFrameShadow(QFrame.Sunken) l.addWidget(sep) l.addLayout(hl4) l.addLayout(hl7) l.addStretch(1) self.setLayout(l) self.adjustSize() self.setDefaults() self.setWhatsThis("""<b>Development of raw files</b><br> <b>Default settings</b> are a good starting point.<br> A <b>Tone Curve</b> is applied to the raw image prior to postprocessing.<br> The cuvre can be edited by checking the option <b>Show Tone Curve</b>; this option works best with manual exposure.<br> <b>Contrast</b> correction is based on an automatic algorithm well suited to multi-mode histograms.<br> <b>Brightness, Contrast</b> and <b>Saturation</b> levels</b> are adjustable with the correponding sliders.<br> The <b>Contrast Curve</b> can be edited manually by checking the option <b>Show Contrast Curve</b>.<br> Uncheck <b>Auto Expose</b> to adjust the exposure manually.<br> The <b>OverExp. Rest.</b> slider controls the mode of restoration of overexposed areas. Valid values are 0 to 3 (0=clip;1=unclip;2=blend;3=rebuild); (with Auto Exposed checked the mode is clip).<br> """) # end of setWhatsThis def close(self): """ Overrides QWidget.close to close toneForm and contrastForm @return: @rtype: boolean """ delete = self.testAttribute(Qt.WA_DeleteOnClose) for attr in ['toneForm', 'contrastForm']: form = getattr(self, attr, None) if delete and form is not None: dock = form.parent() dock.setAttribute(Qt.WA_DeleteOnClose) form.setAttribute(Qt.WA_DeleteOnClose) dock.close() form.close() return super().close() def showToneSpline(self): """ On first call, init and show the Tone Curve form. Otherwise, show the form. Return True if called for the first time, False otherwise. @return: @rtype: boolean """ axeSize = 200 if self.toneForm is None: form = graphicsToneForm.getNewWindow(targetImage=self.targetImage, axeSize=axeSize, layer=self.layer, parent=self, curveType='cubic') form.setWindowFlags(Qt.WindowStaysOnTopHint) form.setAttribute(Qt.WA_DeleteOnClose, on=False) form.setFixedHeight(axeSize + 140) form.setWindowTitle('Cam Tone Curve') form.setButtonText('Reset Curve') # get base curve from profile toneCurve = dngProfileToneCurve( self.dngDict.get('ProfileToneCurve', [])) form.baseCurve = [ QPointF(x * axeSize, -y * axeSize) for x, y in zip(toneCurve.dataX, toneCurve.dataY) ] def f(): layer = self.layer layer.bufCache_HSV_CV32 = None layer.applyToStack() layer.parentImage.onImageChanged() form.scene().quadricB.curveChanged.sig.connect(f) self.toneForm = form self.toneForm.optionName = 'cpToneCurve' dockT = self.addSubcontrol( self.parent()) # )stateAwareQDockWidget(self.parent()) dockT.setWindowFlags(form.windowFlags()) dockT.setWindowTitle(form.windowTitle()) dockT.setStyleSheet( "QGraphicsView{margin: 10px; border-style: solid; border-width: 1px; border-radius: 1px;}" ) window = self.parent().parent() window.addDockWidget(Qt.LeftDockWidgetArea, dockT) self.dockT = dockT dockT.setWidget(form) showFirst = True form.setWhatsThis("""<b>Camera Profile Tone Curve</b><br> The profile curve, if any, is applied as a starting point for user adjustments, after raw post-processing. Its input and output are in <b>linear</b> gamma. The curve is shown in red and cannot be changed.<br> A user curve, shown in black, is editable and is applied right after the former.<br> """) # end of setWhatsThis else: form = self.toneForm showFirst = False form.scene().setSceneRect(-25, -axeSize - 25, axeSize + 50, axeSize + 50) self.dockT.showNormal() return showFirst def setContrastSpline(self, a, b, d, T): """ Updates and displays the contrast spline Form. The form is created if needed. (Cf. also CoBrStaForm setContrastSpline). @param a: x_coordinates @type a: @param b: y-coordinates @type b: @param d: tangent slopes @type d: @param T: spline @type T: ndarray dtype=float """ axeSize = 200 if self.contrastForm is None: form = graphicsSplineForm.getNewWindow(targetImage=None, axeSize=axeSize, layer=self.layer, parent=None) form.setFixedHeight(axeSize + 140) form.setWindowFlags(Qt.WindowStaysOnTopHint) form.setAttribute(Qt.WA_DeleteOnClose, on=False) form.setWindowTitle('Contrast Curve') def f(): layer = self.layer layer.applyToStack() layer.parentImage.onImageChanged() form.scene().quadricB.curveChanged.sig.connect(f) self.contrastForm = form self.contrastForm.optionName = 'manualCurve' dockC = self.addSubcontrol( self.parent()) # stateAwareQDockWidget(self.parent()) dockC.setWindowFlags(form.windowFlags()) dockC.setWindowTitle(form.windowTitle()) dockC.setStyleSheet( "QGraphicsView{margin: 10px; border-style: solid; border-width: 1px; border-radius: 1px;}" ) window = self.parent().parent() window.addDockWidget(Qt.LeftDockWidgetArea, dockC) self.dockC = dockC dockC.setWidget(form) else: form = self.contrastForm # update the curve form.scene().setSceneRect(-25, -axeSize - 25, axeSize + 50, axeSize + 50) form.scene().quadricB.setCurve(a * axeSize, b * axeSize, d, T * axeSize) self.dockC.showNormal() # temp changed event handler def tempUpdate(self, value): self.tempValue.setText( str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value())))) # move not yet terminated or value not modified if self.sliderTemp.isSliderDown() or self.slider2Temp( value) == self.tempCorrection: return try: self.sliderTemp.valueChanged.disconnect() self.sliderTemp.sliderReleased.disconnect() except RuntimeError: pass self.tempCorrection = self.slider2Temp(self.sliderTemp.value()) # get multipliers (temperatureAndTint2Multipliers returns the camera neutral) multipliers = [ 1 / m for m in temperatureAndTint2Multipliers(self.tempCorrection, 1.0, self.XYZ2CameraMatrix, dngDict=self.dngDict) ] # TODO 6/12/19 added keyword dngDict validate multipliers[1] *= self.tintCorrection self.rawMultipliers = multipliers m = multipliers[1] self.rawMultipliers = [self.rawMultipliers[i] / m for i in range(4)] self.dataChanged.emit(1) self.sliderTemp.valueChanged.connect( self.tempUpdate) # send new value as parameter self.sliderTemp.sliderReleased.connect(lambda: self.tempUpdate( self.sliderTemp.value())) # signal has no parameter # tint change event handler def tintUpdate(self, value): self.tintValue.setText( str("{:.0f}".format(self.sliderTint2User( self.sliderTint.value())))) # move not yet terminated or value not modified if self.sliderTint.isSliderDown() or self.slider2Tint( value) == self.tintCorrection: return try: self.sliderTint.valueChanged.disconnect() self.sliderTint.sliderReleased.disconnect() except RuntimeError: pass self.tintCorrection = self.slider2Tint(self.sliderTint.value()) # get multipliers (temperatureAndTint2Multipliers returns the camera neutral) multipliers = [ 1 / m for m in temperatureAndTint2Multipliers(self.tempCorrection, 1.0, self.XYZ2CameraMatrix, dngDict=self.dngDict) ] # TODO 6/12/19 added keyword dngDict validate multipliers[1] *= self.tintCorrection self.rawMultipliers = multipliers m = multipliers[1] self.rawMultipliers = [self.rawMultipliers[i] / m for i in range(4)] self.dataChanged.emit(1) self.sliderTint.valueChanged.connect(self.tintUpdate) self.sliderTint.sliderReleased.connect(lambda: self.tintUpdate( self.sliderTint.value())) # signal has no parameter) def setRawMultipliers(self, m0, m1, m2, sampling=True): mi = min(m0, m1, m2) m0, m1, m2 = m0 / mi, m1 / mi, m2 / mi self.rawMultipliers = [m0, m1, m2, m1] invMultipliers = [1 / self.rawMultipliers[i] for i in range(3)] try: self.sliderTemp.valueChanged.disconnect() self.sliderTint.valueChanged.disconnect() except RuntimeError: pass # get temp and tint temp, tint = multipliers2TemperatureAndTint(*invMultipliers, self.XYZ2CameraMatrix) self.tintCorrection = tint self.sliderTemp.setValue(self.temp2Slider(temp)) self.sliderTint.setValue(self.tint2Slider(tint)) self.tempValue.setText( str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value())))) self.tintValue.setText( str("{:.0f}".format(self.sliderTint2User( self.sliderTint.value())))) self.sliderTemp.valueChanged.connect(self.tempUpdate) self.sliderTint.valueChanged.connect(self.tintUpdate) self.sampleMultipliers = sampling self.dataChanged.emit(1) def updateLayer(self, level): """ data changed event handler. @param level: 3: redo contrast and saturation, 2: previous + camera profile stuff, 1: all @type level: int """ if level == 1: # force all self.layer.bufCache_HSV_CV32 = None self.layer.postProcessCache = None elif level == 2: # force camera profile stuff self.layer.bufCache_HSV_CV32 = None elif level == 3: # keep the 2 cache buffers pass # contrast curve cf = getattr(self, 'dockC', None) if cf is not None: if self.options['manualCurve']: cf.showNormal() else: cf.hide() # tone curve ct = getattr(self, 'dockT', None) if ct is not None: if self.options['cpToneCurve']: ct.showNormal() else: ct.hide() self.enableSliders() self.layer.applyToStack() self.layer.parentImage.onImageChanged() def enableSliders(self): useUserWB = self.listWidget2.options["User WB"] useUserExp = not self.listWidget1.options["Auto Brightness"] self.sliderTemp.setEnabled(useUserWB) self.sliderTint.setEnabled(useUserWB) self.sliderExp.setEnabled(useUserExp) # self.sliderHigh.setEnabled(useUserExp) self.tempValue.setEnabled(self.sliderTemp.isEnabled()) self.tintValue.setEnabled(self.sliderTint.isEnabled()) self.expValue.setEnabled(self.sliderExp.isEnabled()) # self.highValue.setEnabled(self.sliderHigh.isEnabled()) self.tempLabel.setEnabled(self.sliderTemp.isEnabled()) self.tintLabel.setEnabled(self.sliderTint.isEnabled()) self.expLabel.setEnabled(self.sliderExp.isEnabled()) # self.highLabel.setEnabled(self.sliderHigh.isEnabled()) def setDefaults(self): self.dngDict = self.cameraProfilesCombo.itemData(0) self.listWidget1.unCheckAll() self.listWidget2.unCheckAll() self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.listWidget1.checkOption(self.listWidget1.intNames[1]) self.listWidget2.checkOption(self.listWidget2.intNames[1]) self.listWidget3.checkOption(self.listWidget3.intNames[0]) self.enableSliders() self.denoiseValue = 0 # denoising off self.overexpValue = 0 # clip self.tempCorrection = self.asShotTemp self.tintCorrection = 1.0 self.expCorrection = 1.0 # self.highCorrection = 3.0 # restoration of overexposed highlights. 0: clip 1:unclip, 2: blend, 3...: rebuild self.contCorrection = 0.0 # self.noiseCorrection = 0 self.satCorrection = 0.0 self.brCorrection = 1.0 # prevent multiple updates try: self.dataChanged.disconnect() except RuntimeError: pass self.sliderTemp.setValue(round(self.temp2Slider(self.tempCorrection))) self.sliderTint.setValue(round(self.tint2Slider(self.tintCorrection))) self.sliderExp.setValue(self.exp2Slider(self.expCorrection)) # self.sliderHigh.setValue(self.highCorrection) self.sliderCont.setValue(self.cont2Slider(self.contCorrection)) self.sliderBrightness.setValue(self.br2Slider(self.brCorrection)) self.sliderSat.setValue(self.sat2Slider(self.satCorrection)) self.dataChanged.connect(self.updateLayer) def setCameraProfilesCombo(self): """ Populates the camera profile Combo box. for each item, text is the filename and data is the corresponding dict. The function returns as soon as a first item is loaded. Remainning profiles are loaded asynchronously. @return: the currently selected item data @rtype: dict """ self.cameraProfilesCombo = QComboBox() files = [self.targetImage.filename] files.extend(getDngProfileList(self.targetImage.cameraModel())) if not files: self.cameraProfilesCombo.addItem('None', {}) return {} # load a first profile nextInd, found = 0, False while nextInd < len(files) and not found: f = files[nextInd] key = basename(f)[:-4] if nextInd > 0 else 'Embedded Profile' d = getDngProfileDict(f) # filter d d = {k: d[k] for k in d if d[k] != ''} if d: self.cameraProfilesCombo.addItem(key, d) found = True nextInd += 1 def load(): # load remaining profiles for i, f in enumerate(files[nextInd:]): key = basename( f)[:-4] if i + nextInd > 0 else 'Embedded Profile' d = getDngProfileDict(f) # filter d d = {k: d[k] for k in d if d[k] != ''} if d: self.cameraProfilesCombo.addItem(key, d) self.cameraProfilesCombo.addItem('None', {}) threading.Thread(target=load).start() self.cameraProfilesCombo.setSizeAdjustPolicy( QComboBox.SizeAdjustPolicy.AdjustToContents) self.cameraProfilesCombo.setMaximumWidth(150) self.cameraProfilesCombo.setStyleSheet( "QComboBox QAbstractItemView { min-width: 250px;}") # return the currently selected item data return self.cameraProfilesCombo.itemData(0)
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) ####################################### # Libraw correspondences: # rgb_xyz_matrix is libraw cam_xyz # camera_whitebalance is libraw cam_mul # daylight_whitebalance is libraw pre_mul # dng correspondences: # ASSHOTNEUTRAL tag value is (X,Y,Z) = 1 / rawpyObj.camera_whitebalance ########################################## rawpyObj = layer.parentImage.rawImage # constants and as shot values self.XYZ2CameraMatrix = rawpyObj.rgb_xyz_matrix[:3, :] self.XYZ2CameraInverseMatrix = np.linalg.inv(self.XYZ2CameraMatrix) # initial post processing multipliers (as shot) m1, m2, m3, m4 = rawpyObj.camera_whitebalance self.asShotMultipliers = ( m1 / m2, 1.0, m3 / m2, m4 / m2 ) # normalization is mandatory : for nef files white balance is around 256 self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint( *1 / np.array(self.asShotMultipliers[:3]), self.XYZ2CameraMatrix) self.rawMultipliers = self.asShotMultipliers # rawpyObj.camera_whitebalance # = 1/(dng ASSHOTNEUTRAL tag value) self.sampleMultipliers = False self.samples = [] ######################################## # XYZ-->Camera conversion matrix: # Last row is zero for RGB cameras (cf. rawpy and libraw docs). # type ndarray, shape (4,3) ######################################### # attributes initialized in setDefaults, declared here for the sake of correctness self.tempCorrection, self.tintCorrection, self.expCorrection, self.highCorrection,\ self.contCorrection, self.satCorrection, self.brCorrection = [None] * 7 # contrast spline vie (initialized by setContrastSpline) self.contrastForm = None # tone spline view (initialized by setToneSpline) self.toneForm = None # dock containers for contrast and tome forms self.dockC, self.dockT = None, None # options optionList0, optionNames0 = ['Auto Brightness', 'Preserve Highlights' ], ['Auto Expose', 'Preserve Highlights'] optionList1, optionNames1 = ['Auto WB', 'Camera WB', 'User WB' ], ['Auto', 'Camera (As Shot)', 'User'] optionList2, optionNames2 = [ 'cpLookTable', 'cpToneCurve', 'manualCurve' ], [ 'Use Camera Profile Look Table', 'Show Tone Curves', 'Show Contrast Curve' ] self.listWidget1 = optionsWidget( options=optionList0, optionNames=optionNames0, exclusive=False, changed=lambda: self.dataChanged.emit(1)) self.listWidget2 = optionsWidget( options=optionList1, optionNames=optionNames1, exclusive=True, changed=lambda: self.dataChanged.emit(1)) self.listWidget3 = optionsWidget( options=optionList2, optionNames=optionNames2, exclusive=False, changed=lambda: self.dataChanged.emit(2)) self.options = UDict( (self.listWidget1.options, self.listWidget2.options, self.listWidget3.options)) # display the 'as shot' temperature item = self.listWidget2.item(1) item.setText(item.text() + ' : %d' % self.asShotTemp) # temperature slider self.sliderTemp = QbLUeSlider(Qt.Horizontal) self.sliderTemp.setStyleSheet( QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderTemp.setRange(0, 100) self.sliderTemp.setSingleStep(1) self.tempLabel = QLabel() self.tempLabel.setText("Temp") self.tempValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("10000") h = metrics.height() self.tempValue.setMinimumSize(w, h) self.tempValue.setMaximumSize(w, h) self.tempValue.setText( str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value())))) self.sliderTemp.valueChanged.connect( self.tempUpdate) # signal send new value as parameter self.sliderTemp.sliderReleased.connect(lambda: self.tempUpdate( self.sliderTemp.value())) # signal pass no parameter # tint slider self.sliderTint = QbLUeSlider(Qt.Horizontal) self.sliderTint.setStyleSheet( QbLUeSlider.bLueSliderDefaultIMGColorStylesheet) self.sliderTint.setRange(0, 150) self.sliderTint.setSingleStep(1) self.tintLabel = QLabel() self.tintLabel.setText("Tint") self.tintValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.tintValue.setMinimumSize(w, h) self.tintValue.setMaximumSize(w, h) self.tintValue.setText( str("{:.0f}".format(self.sliderTint2User( self.sliderTint.value())))) self.sliderTint.valueChanged.connect(self.tintUpdate) self.sliderTint.sliderReleased.connect(lambda: self.tintUpdate( self.sliderTint.value())) # signal pass no parameter) ###################### # From libraw and dcraw sources: # Exposure and brightness are curve transformations. # Exposure curve is y = alpha*x, with cubic root ending; it is applied before demosaicing. # Brightness is (similar to) y = x**alpha and part of gamma transformation from linear sRGB to RGB. # Exposure and brightness both dilate the histogram towards highlights. # Exposure dilatation is uniform (homothety), brightness dilataion is # maximum for the midtones and the highlghts are preserved. # As a consequence, normal workflow begins with the adjustment of exposure, # to fill the entire range of the histogram and to adjust the highlights. Next, # one adjusts the brightness to put the midtones at the level we want them to be. # Cf. https://www.cambridgeincolour.com/forums/thread653.htm ##################### # profile combo self.dngDict = self.setCameraProfilesCombo() # cameraProfilesCombo index changed event handler def cameraProfileUpdate(value): self.dngDict = self.cameraProfilesCombo.itemData(value) if self.options['cpToneCurve']: toneCurve = dngProfileToneCurve( self.dngDict.get('ProfileToneCurve', [])) self.toneForm.baseCurve = [ QPointF(x * axeSize, -y * axeSize) for x, y in zip(toneCurve.dataX, toneCurve.dataY) ] self.toneForm.update() # recompute as shot temp and tint using new profile self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint( *1 / np.array(self.asShotMultipliers[:3]), self.XYZ2CameraMatrix, dngDict=self.dngDict ) # TODO 6/12/19 added keyword dngDict validate # display updated as shot temp item = self.listWidget2.item(1) item.setText(item.text().split(":")[0] + ': %d' % self.asShotTemp) # invalidate cache self.layer.bufCache_HSV_CV32 = None self.dataChanged.emit(2) # 2 = no postprocessing self.cameraProfilesCombo.currentIndexChanged.connect( cameraProfileUpdate) # denoising combo self.denoiseCombo = QComboBox() items = OrderedDict([('Off', 0), ('Medium', 1), ('Full', 2)]) for key in items: self.denoiseCombo.addItem(key, items[key]) # denoiseCombo index changed event handler def denoiseUpdate(value): self.denoiseValue = self.denoiseCombo.itemData(value) self.dataChanged.emit(1) self.denoiseCombo.currentIndexChanged.connect(denoiseUpdate) # overexposed area restoration self.overexpCombo = QComboBox() items = OrderedDict([('Clip', 0), ('Ignore', 1), ('Blend', 2), ('Reconstruct', 3)]) for key in items: self.overexpCombo.addItem(key, items[key]) # overexpCombo index changed event handler def overexpUpdate(value): self.overexpValue = self.overexpCombo.itemData(value) self.dataChanged.emit(1) self.overexpCombo.currentIndexChanged.connect(overexpUpdate) # exp slider self.sliderExp = QbLUeSlider(Qt.Horizontal) self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderExp.setRange(0, 100) self.sliderExp.setSingleStep(1) self.expLabel = QLabel() self.expLabel.setText("Exp.") self.expValue = QLabel() font = self.expValue.font() metrics = QFontMetrics(font) w = metrics.width("+1.0") h = metrics.height() self.expValue.setMinimumSize(w, h) self.expValue.setMaximumSize(w, h) self.expValue.setText( str("{:.1f}".format(self.slider2Exp(self.sliderExp.value())))) # exp done event handler def expUpdate(value): self.expValue.setText( str("{:+.1f}".format( self.sliderExp2User(self.sliderExp.value())))) # move not yet terminated or value not modified if self.sliderExp.isSliderDown() or self.slider2Exp( value) == self.expCorrection: return try: self.sliderExp.valueChanged.disconnect() self.sliderExp.sliderReleased.disconnect() except RuntimeError: pass # rawpy: expCorrection range is -2.0...3.0, boiling down to exp_shift range 2**(-2)=0.25...2**3=8.0 self.expCorrection = self.slider2Exp(self.sliderExp.value()) self.dataChanged.emit(1) self.sliderExp.valueChanged.connect( expUpdate) # send new value as parameter self.sliderExp.sliderReleased.connect(lambda: expUpdate( self.sliderExp.value())) # signal pass no parameter self.sliderExp.valueChanged.connect( expUpdate) # send new value as parameter self.sliderExp.sliderReleased.connect(lambda: expUpdate( self.sliderExp.value())) # signal pass no parameter # brightness slider brSlider = QbLUeSlider(Qt.Horizontal) brSlider.setRange(1, 101) self.sliderExp.setSingleStep(1) brSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderBrightness = brSlider brLabel = QLabel() brLabel.setText("Bright.") self.brValue = QLabel() font = self.expValue.font() metrics = QFontMetrics(font) w = metrics.width("+99") h = metrics.height() self.brValue.setMinimumSize(w, h) self.brValue.setMaximumSize(w, h) self.brValue.setText( str("{:+d}".format( int(self.brSlider2User(self.sliderBrightness.value()))))) # brightness done event handler def brUpdate(value): self.brValue.setText( str("{:+d}".format( int(self.brSlider2User(self.sliderBrightness.value()))))) # move not yet terminated or value not modified if self.sliderBrightness.isSliderDown() or self.slider2Br( value) == self.brCorrection: return try: self.sliderBrightness.valueChanged.disconnect() self.sliderBrightness.sliderReleased.disconnect() except RuntimeError: pass self.brCorrection = self.slider2Br(self.sliderBrightness.value()) self.dataChanged.emit(1) self.sliderBrightness.sliderReleased.connect( lambda: brUpdate(self.sliderBrightness.value())) self.sliderBrightness.valueChanged.connect( brUpdate) # send new value as parameter self.sliderBrightness.valueChanged.connect( brUpdate) # send new value as parameter self.sliderBrightness.sliderReleased.connect( lambda: brUpdate(self.sliderBrightness.value())) # contrast slider self.sliderCont = QbLUeSlider(Qt.Horizontal) self.sliderCont.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderCont.setRange(0, 20) self.sliderCont.setSingleStep(1) self.contLabel = QLabel() self.contLabel.setText("Cont.") self.contValue = QLabel() font = self.contValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.contValue.setMinimumSize(w, h) self.contValue.setMaximumSize(w, h) self.contValue.setText( str("{:.0f}".format(self.slider2Cont(self.sliderCont.value())))) # cont done event handler def contUpdate(value): self.contValue.setText( str("{:.0f}".format(self.slider2Cont( self.sliderCont.value())))) # move not yet terminated or value not modified if self.sliderCont.isSliderDown() or self.slider2Cont( value) == self.tempCorrection: return try: self.sliderCont.valueChanged.disconnect() self.sliderCont.sliderReleased.disconnect() except RuntimeError: pass self.contCorrection = self.slider2Cont(self.sliderCont.value()) self.contValue.setText(str("{:+d}".format(self.contCorrection))) # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit( 3) # no postprocessing and no camera profile stuff self.sliderCont.valueChanged.connect( contUpdate) # send new value as parameter self.sliderCont.sliderReleased.connect(lambda: contUpdate( self.sliderCont.value())) # signal has no parameter self.sliderCont.valueChanged.connect( contUpdate) # send new value as parameter self.sliderCont.sliderReleased.connect(lambda: contUpdate( self.sliderCont.value())) # signal has no parameter # saturation slider self.sliderSat = QbLUeSlider(Qt.Horizontal) self.sliderSat.setStyleSheet( QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderSat.setRange(0, 100) self.sliderSat.setSingleStep(1) satLabel = QLabel() satLabel.setText("Sat.") self.satValue = QLabel() font = self.satValue.font() metrics = QFontMetrics(font) w = metrics.width("+10") h = metrics.height() self.satValue.setMinimumSize(w, h) self.satValue.setMaximumSize(w, h) self.satValue.setText( str("{:+d}".format(self.slider2Sat(self.sliderSat.value())))) """sat done event handler""" def satUpdate(value): self.satValue.setText( str("{:+d}".format(self.slider2Sat(self.sliderSat.value())))) # move not yet terminated or value not modified if self.sliderSat.isSliderDown() or self.slider2Sat( value) == self.satCorrection: return try: self.sliderSat.valueChanged.disconnect() self.sliderSat.sliderReleased.disconnect() except RuntimeError: pass self.satCorrection = self.slider2Sat(self.sliderSat.value()) self.dataChanged.emit( 3) # no post processing and no camera profile stuff self.sliderSat.valueChanged.connect( satUpdate) # send new value as parameter self.sliderSat.sliderReleased.connect(lambda: satUpdate( self.sliderSat.value())) # signal has no parameter self.sliderSat.valueChanged.connect( satUpdate) # send new value as parameter self.sliderSat.sliderReleased.connect(lambda: satUpdate( self.sliderSat.value())) # signal has no parameter # layout l = QVBoxLayout() l.addWidget(self.listWidget3) hl01 = QHBoxLayout() hl01.addWidget(QLabel('Camera Profile')) hl01.addWidget(self.cameraProfilesCombo) l.addLayout(hl01) hl0 = QHBoxLayout() hl0.addWidget(QLabel('Denoising')) hl0.addWidget(self.denoiseCombo) l.addLayout(hl0) hl00 = QHBoxLayout() hl00.addWidget(QLabel('Overexp. Restoration')) hl00.addWidget(self.overexpCombo) l.addLayout(hl00) hl1 = QHBoxLayout() hl1.addWidget(self.expLabel) hl1.addWidget(self.expValue) hl1.addWidget(self.sliderExp) l.addLayout(hl1) hl8 = QHBoxLayout() hl8.addWidget(brLabel) hl8.addWidget(self.brValue) hl8.addWidget(self.sliderBrightness) l.addLayout(hl8) l.addWidget(self.listWidget1) vl1 = QVBoxLayout() vl1.addWidget(self.listWidget2) gb1 = QGroupBox() gb1.setTitle('White Balance') hl2 = QHBoxLayout() hl2.addWidget(self.tempLabel) hl2.addWidget(self.tempValue) hl2.addWidget(self.sliderTemp) hl3 = QHBoxLayout() hl3.addWidget(self.tintLabel) hl3.addWidget(self.tintValue) hl3.addWidget(self.sliderTint) vl1.addLayout(hl2) vl1.addLayout(hl3) gb1.setLayout(vl1) l.addWidget(gb1) hl4 = QHBoxLayout() hl4.addWidget(self.contLabel) hl4.addWidget(self.contValue) hl4.addWidget(self.sliderCont) hl7 = QHBoxLayout() hl7.addWidget(satLabel) hl7.addWidget(self.satValue) hl7.addWidget(self.sliderSat) # separator sep = QFrame() sep.setFrameShape(QFrame.HLine) sep.setFrameShadow(QFrame.Sunken) l.addWidget(sep) l.addLayout(hl4) l.addLayout(hl7) l.addStretch(1) self.setLayout(l) self.adjustSize() self.setDefaults() self.setWhatsThis("""<b>Development of raw files</b><br> <b>Default settings</b> are a good starting point.<br> A <b>Tone Curve</b> is applied to the raw image prior to postprocessing.<br> The cuvre can be edited by checking the option <b>Show Tone Curve</b>; this option works best with manual exposure.<br> <b>Contrast</b> correction is based on an automatic algorithm well suited to multi-mode histograms.<br> <b>Brightness, Contrast</b> and <b>Saturation</b> levels</b> are adjustable with the correponding sliders.<br> The <b>Contrast Curve</b> can be edited manually by checking the option <b>Show Contrast Curve</b>.<br> Uncheck <b>Auto Expose</b> to adjust the exposure manually.<br> The <b>OverExp. Rest.</b> slider controls the mode of restoration of overexposed areas. Valid values are 0 to 3 (0=clip;1=unclip;2=blend;3=rebuild); (with Auto Exposed checked the mode is clip).<br> """) # end of setWhatsThis
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) self.tempCorrection = 6500 self.tintCorrection = 1.0 self.filterColor = QColor(255, 255, 255) self.defaultTemp = sRGBWP # ref temperature D65 self.defaultTint = 0 # options optionList, optionNames = [ 'Color Filter', 'Photo Filter', 'Chromatic Adaptation' ], ['Color Filter', 'Photo Filter', 'Chromatic Adaptation'] self.listWidget1 = optionsWidget( options=optionList, optionNames=optionNames, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.options = self.listWidget1.options # link to app color dialog self.colorChooser = self.parent().colorChooser # color viewer self.colorLabel = QLabel() self.colorLabel.setMaximumSize(50, 50) # color chooser button self.colorChooserBtn = QbLUePushButton('Select Filter Color') self.colorChooserBtn.clicked.connect(self.showColorChooser) # temp slider self.sliderTemp = QbLUeSlider(Qt.Horizontal) self.sliderTemp.setStyleSheet( QbLUeSlider.bLueSliderDefaultIColorStylesheet) self.sliderTemp.setRange( 17, 100 ) # 250) # valid range for spline approximation is 1667..25000, cf. colorConv.temperature2xyWP self.sliderTemp.setSingleStep(1) self.tempLabel = QbLUeLabel() self.tempLabel.setMaximumSize(150, 30) self.tempLabel.setText("Filter Temperature") self.tempLabel.doubleClicked.connect(lambda: self.sliderTemp.setValue( self.temp2Slider(self.defaultTemp))) self.tempValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("00000") h = metrics.height() self.tempValue.setMinimumSize(w, h) self.tempValue.setMaximumSize(w, h) self.tempValue.setText( str("{:d}".format(self.sliderTemp2User(self.sliderTemp.value())))) # tint slider self.sliderTint = QbLUeSlider(Qt.Horizontal) self.sliderTint.setStyleSheet( QbLUeSlider.bLueSliderDefaultMGColorStylesheet) self.sliderTint.setRange( 0, 100 ) # 250) # valid range for spline approximation is 1667..25000, cf. colorConv.temperature2xyWP self.sliderTint.setSingleStep(1) self.tintLabel = QbLUeLabel() self.tintLabel.setMaximumSize(150, 30) self.tintLabel.setText("Tint") self.tintLabel.doubleClicked.connect(lambda: self.sliderTint.setValue( self.tint2Slider(self.defaultTint))) self.tintValue = QLabel() font = self.tintValue.font() metrics = QFontMetrics(font) w = metrics.width("0000") h = metrics.height() self.tintValue.setMinimumSize(w, h) self.tintValue.setMaximumSize(w, h) self.tintValue.setText( str("{:d}".format(self.sliderTint2User(self.sliderTint.value())))) self.sliderTemp.valueChanged.connect(self.tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: self.tempUpdate(self.sliderTemp.value())) self.sliderTint.valueChanged.connect(self.tintUpdate) self.sliderTint.sliderReleased.connect( lambda: self.tintUpdate(self.sliderTint.value())) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(self.listWidget1) hl2 = QHBoxLayout() hl2.addWidget(self.colorLabel) hl2.addWidget(self.colorChooserBtn) l.addLayout(hl2) l.addWidget(self.tempLabel) hl = QHBoxLayout() hl.addWidget(self.tempValue) hl.addWidget(self.sliderTemp) l.addLayout(hl) l.addWidget(self.tintLabel) hl1 = QHBoxLayout() hl1.addWidget(self.tintValue) hl1.addWidget(self.sliderTint) l.addLayout(hl1) self.setLayout(l) self.adjustSize() self.setDefaults() self.setWhatsThis( """<b> Color Filter</b> and <b>Photo Filter</b> use the multiply blending mode to mimic a warming or cooling filter put in front of the camera lens. The luminosity of the resulting image is corrected.<br> <b>Chromatic Adaptation</b> uses multipliers in the linear sRGB color space to adjust <b>temperature</b> and <b>tint</b>. """) # end of setWhatsThis
class temperatureForm(baseForm): @classmethod def getNewWindow(cls, targetImage=None, axeSize=500, layer=None, parent=None): wdgt = temperatureForm(axeSize=axeSize, layer=layer, parent=parent) wdgt.setWindowTitle(layer.name) return wdgt def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) self.tempCorrection = 6500 self.tintCorrection = 1.0 self.filterColor = QColor(255, 255, 255) self.defaultTemp = sRGBWP # ref temperature D65 self.defaultTint = 0 # options optionList, optionNames = [ 'Color Filter', 'Photo Filter', 'Chromatic Adaptation' ], ['Color Filter', 'Photo Filter', 'Chromatic Adaptation'] self.listWidget1 = optionsWidget( options=optionList, optionNames=optionNames, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.options = self.listWidget1.options # link to app color dialog self.colorChooser = self.parent().colorChooser # color viewer self.colorLabel = QLabel() self.colorLabel.setMaximumSize(50, 50) # color chooser button self.colorChooserBtn = QbLUePushButton('Select Filter Color') self.colorChooserBtn.clicked.connect(self.showColorChooser) # temp slider self.sliderTemp = QbLUeSlider(Qt.Horizontal) self.sliderTemp.setStyleSheet( QbLUeSlider.bLueSliderDefaultIColorStylesheet) self.sliderTemp.setRange( 17, 100 ) # 250) # valid range for spline approximation is 1667..25000, cf. colorConv.temperature2xyWP self.sliderTemp.setSingleStep(1) self.tempLabel = QbLUeLabel() self.tempLabel.setMaximumSize(150, 30) self.tempLabel.setText("Filter Temperature") self.tempLabel.doubleClicked.connect(lambda: self.sliderTemp.setValue( self.temp2Slider(self.defaultTemp))) self.tempValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("00000") h = metrics.height() self.tempValue.setMinimumSize(w, h) self.tempValue.setMaximumSize(w, h) self.tempValue.setText( str("{:d}".format(self.sliderTemp2User(self.sliderTemp.value())))) # tint slider self.sliderTint = QbLUeSlider(Qt.Horizontal) self.sliderTint.setStyleSheet( QbLUeSlider.bLueSliderDefaultMGColorStylesheet) self.sliderTint.setRange( 0, 100 ) # 250) # valid range for spline approximation is 1667..25000, cf. colorConv.temperature2xyWP self.sliderTint.setSingleStep(1) self.tintLabel = QbLUeLabel() self.tintLabel.setMaximumSize(150, 30) self.tintLabel.setText("Tint") self.tintLabel.doubleClicked.connect(lambda: self.sliderTint.setValue( self.tint2Slider(self.defaultTint))) self.tintValue = QLabel() font = self.tintValue.font() metrics = QFontMetrics(font) w = metrics.width("0000") h = metrics.height() self.tintValue.setMinimumSize(w, h) self.tintValue.setMaximumSize(w, h) self.tintValue.setText( str("{:d}".format(self.sliderTint2User(self.sliderTint.value())))) self.sliderTemp.valueChanged.connect(self.tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: self.tempUpdate(self.sliderTemp.value())) self.sliderTint.valueChanged.connect(self.tintUpdate) self.sliderTint.sliderReleased.connect( lambda: self.tintUpdate(self.sliderTint.value())) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(self.listWidget1) hl2 = QHBoxLayout() hl2.addWidget(self.colorLabel) hl2.addWidget(self.colorChooserBtn) l.addLayout(hl2) l.addWidget(self.tempLabel) hl = QHBoxLayout() hl.addWidget(self.tempValue) hl.addWidget(self.sliderTemp) l.addLayout(hl) l.addWidget(self.tintLabel) hl1 = QHBoxLayout() hl1.addWidget(self.tintValue) hl1.addWidget(self.sliderTint) l.addLayout(hl1) self.setLayout(l) self.adjustSize() self.setDefaults() self.setWhatsThis( """<b> Color Filter</b> and <b>Photo Filter</b> use the multiply blending mode to mimic a warming or cooling filter put in front of the camera lens. The luminosity of the resulting image is corrected.<br> <b>Chromatic Adaptation</b> uses multipliers in the linear sRGB color space to adjust <b>temperature</b> and <b>tint</b>. """) # end of setWhatsThis def enableSliders(self): for item in [self.colorLabel, self.colorChooserBtn]: item.setEnabled(self.options['Color Filter']) for item in [self.sliderTemp, self.tempLabel]: item.setEnabled(not self.options['Color Filter']) for item in [self.sliderTint, self.tintLabel, self.tintValue]: item.setEnabled(self.options['Chromatic Adaptation']) def setDefaults(self): # prevent multiple updates try: self.dataChanged.disconnect() except RuntimeError: pass self.listWidget1.unCheckAll() self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.enableSliders() self.sliderTemp.setValue(round(self.temp2Slider(self.tempCorrection))) self.sliderTint.setValue(round(self.tint2Slider(self.defaultTint))) self.dataChanged.connect(self.updateLayer) self.filterColor = self.colorChooser.currentColor() # set colorLabel background self.colorLabel.setAutoFillBackground(True) colorstr = ''.join('%02x' % i for i in self.filterColor.getRgb()[:3]) self.colorLabel.setStyleSheet("background:#%s" % colorstr) def colorUpdate(self, color): """ color Changed slot @param color: @type color: QColor """ self.dataChanged.emit() def tempUpdate(self, value): """ temp change slot @param value: @type value: int """ self.tempValue.setText(str("{:d}".format(self.sliderTemp2User(value)))) # move not yet terminated or values not modified if self.sliderTemp.isSliderDown() or self.slider2Temp( value) == self.tempCorrection: return try: self.sliderTemp.valueChanged.disconnect() self.sliderTemp.sliderReleased.disconnect() except RuntimeError: pass self.tempCorrection = self.slider2Temp(value) self.dataChanged.emit() self.sliderTemp.valueChanged.connect(self.tempUpdate) self.sliderTemp.sliderReleased.connect( lambda: self.tempUpdate(self.sliderTemp.value())) def tintUpdate(self, value): """ tint change slot @param value: @type value: int """ self.tintValue.setText(str("{:d}".format(self.sliderTint2User(value)))) # move not yet terminated or values not modified if self.sliderTint.isSliderDown() or self.slider2Tint( value) == self.tintCorrection: return try: self.sliderTint.valueChanged.disconnect() self.sliderTint.sliderReleased.disconnect() except RuntimeError: pass self.tintCorrection = self.slider2Tint(value) self.dataChanged.emit() self.sliderTint.valueChanged.connect(self.tintUpdate) self.sliderTint.sliderReleased.connect( lambda: self.tintUpdate(self.sliderTint.value())) def updateLayer(self): """ data changed slot """ self.enableSliders() self.layer.applyToStack() self.layer.parentImage.onImageChanged() def setFilterColor(self, color): """ currentColorChanged slot @param color: @type color: QColor """ self.filterColor = color self.colorLabel.setAutoFillBackground(True) colorstr = ''.join('%02x' % i for i in color.getRgb()[:3]) self.colorLabel.setStyleSheet("background:#%s" % colorstr) def showColorChooser(self): self.colorChooser.show() try: self.colorChooser.currentColorChanged.disconnect() except RuntimeError: pass self.colorChooser.setCurrentColor(self.filterColor) self.colorChooser.currentColorChanged.connect(self.setFilterColor) self.colorChooser.colorSelected.connect(self.colorUpdate) @staticmethod def slider2Temp(v): return v * 100 @staticmethod def temp2Slider(v): return int(v / 100) @staticmethod def sliderTemp2User(v): return v * 100 @staticmethod def slider2Tint(v): return (v - 50) / 50 @staticmethod def tint2Slider(v): return int((1.0 + v) * 50.0) @staticmethod def sliderTint2User(v): return int((v - 50) / 5.0)
class savingDialog(QDialog): """ File dialog with options. We use a standard QFileDialog as a child widget and we forward its methods to the top level. """ def __init__(self, parent, text, lastDir): """ @param parent: @type parent: QObject @param text: @type text: str @param lastDir: @type lastDir:str """ # QDialog __init__ super().__init__() self.setWindowTitle(text) # File Dialog self.dlg = QFileDialog(caption=text, directory=lastDir) self.dlg.setOption(QFileDialog.DontUseNativeDialog) self.metaOption = QCheckBox('Remove Meta') # sliders self.sliderComp = QbLUeSlider(Qt.Horizontal) self.sliderComp.setTickPosition(QSlider.TicksBelow) self.sliderComp.setRange(0, 9) self.sliderComp.setSingleStep(1) self.sliderComp.setValue( 3 ) # 3 = default opencv imwrite value TODO changed 5 to 3 20/02/20 validate self.sliderQual = QbLUeSlider(Qt.Horizontal) self.sliderQual.setTickPosition(QSlider.TicksBelow) self.sliderQual.setRange(0, 100) self.sliderQual.setSingleStep(10) self.sliderQual.setValue( 95 ) # 95 = default opencv imwrite value # TODO changed 90 to 95 20/02/20 validate self.dlg.setVisible(True) l = QVBoxLayout() h = QHBoxLayout() l.addWidget(self.dlg) h.addWidget(self.metaOption) h.addWidget(QLabel("Quality")) h.addWidget(self.sliderQual) h.addWidget(QLabel("Compression")) h.addWidget(self.sliderComp) l.addLayout(h) self.setLayout(l) # file dialog close event handler def f(): self.close() self.dlg.finished.connect(f) def exec_(self): # QDialog exec_ super().exec_() # forward file dialog result return self.dlg.result() def selectFile(self, fileName): self.dlg.selectFile(fileName) def selectedFiles(self): return self.dlg.selectedFiles() def directory(self): return self.dlg.directory()