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