def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) # options optionList1, optionNames1 = ['Free', 'Rotation', 'Translation'], [ 'Free Transformation', 'Rotation', 'Translation' ] self.listWidget1 = optionsWidget(options=optionList1, optionNames=optionNames1, exclusive=True, changed=self.dataChanged) optionList2, optionNames2 = ['Transparent' ], ['Set Transparent Pixels To Black'] self.listWidget2 = optionsWidget(options=optionList2, optionNames=optionNames2, exclusive=False, changed=self.dataChanged) self.options = UDict( (self.listWidget1.options, self.listWidget2.options)) # set initial selection to Perspective self.listWidget1.checkOption(optionList1[0]) pushButton1 = QPushButton(' Reset Transformation ') pushButton1.adjustSize() pushButton1.clicked.connect(self.reset) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(self.listWidget1) l.addWidget(self.listWidget2) hl = QHBoxLayout() hl.setAlignment(Qt.AlignHCenter) hl.addWidget(pushButton1) l.addLayout(hl) self.setLayout(l) self.adjustSize() self.setDefaults() self.setWhatsThis(""" <b>Geometric transformation :</b><br> Choose a transformation type and drag either corner of the image using the small square red buttons.<br> Ctrl+Alt+Drag to change the <b>initial positions</b> of buttons. """) # end of setWhatsThis
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) self.setMinimumSize(axeSize, axeSize + 100) # contrast spline viewer self.contrastForm = None # options optionList1, optionNames1 = ['Multi-Mode', 'CLAHE'], ['Multi-Mode', 'CLAHE'] self.listWidget1 = optionsWidget( options=optionList1, optionNames=optionNames1, exclusive=True, changed=lambda: self.dataChanged.emit()) self.listWidget1.checkOption(self.listWidget1.intNames[0]) self.listWidget1.setStyleSheet( "QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}" ) optionList2, optionNames2 = ['High', 'manualCurve'], [ 'Preserve Highlights', 'Show Contrast Curve' ] def optionList2Change(item): if item.internalName == 'High': # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit() self.listWidget2 = optionsWidget(options=optionList2, optionNames=optionNames2, exclusive=False, changed=optionList2Change) self.listWidget2.checkOption(self.listWidget2.intNames[0]) self.listWidget2.setStyleSheet( "QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 0px;}" ) self.options = UDict( (self.listWidget1.options, self.listWidget2.options)) # contrast slider self.sliderContrast = QbLUeSlider(Qt.Horizontal) self.sliderContrast.setStyleSheet( QbLUeSlider.bLueSliderDefaultIBWStylesheet) self.sliderContrast.setRange(0, 10) self.sliderContrast.setSingleStep(1) contrastLabel = QbLUeLabel() contrastLabel.setMaximumSize(150, 30) contrastLabel.setText("Contrast Level") contrastLabel.doubleClicked.connect( lambda: self.sliderContrast.setValue( self.contrast2Slider(self.contrastDefault))) self.contrastValue = QLabel() font = self.contrastValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.contrastValue.setMinimumSize(w, h) self.contrastValue.setMaximumSize(w, h) self.contrastValue.setText( str("{:d}".format(self.sliderContrast.value()))) # contrast changed event handler. def contrastUpdate(value): self.contrastValue.setText( str("{:d}".format(self.sliderContrast.value()))) # move not yet terminated or value not modified if self.sliderContrast.isSliderDown() or self.slider2Contrast( value) == self.contrastCorrection: return self.sliderContrast.valueChanged.disconnect() self.sliderContrast.sliderReleased.disconnect() self.contrastCorrection = self.slider2Contrast( self.sliderContrast.value()) # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit() self.sliderContrast.valueChanged.connect(contrastUpdate) self.sliderContrast.sliderReleased.connect( lambda: contrastUpdate(self.sliderContrast.value())) self.sliderContrast.valueChanged.connect(contrastUpdate) self.sliderContrast.sliderReleased.connect( lambda: contrastUpdate(self.sliderContrast.value())) # saturation slider self.sliderSaturation = QbLUeSlider(Qt.Horizontal) self.sliderSaturation.setStyleSheet( QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderSaturation.setRange(0, 100) self.sliderSaturation.setSingleStep(1) saturationLabel = QbLUeLabel() saturationLabel.setMaximumSize(150, 30) saturationLabel.setText("Saturation") saturationLabel.doubleClicked.connect( lambda: self.sliderSaturation.setValue( self.saturation2Slider(self.saturationDefault))) self.saturationValue = QLabel() font = self.saturationValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.saturationValue.setMinimumSize(w, h) self.saturationValue.setMaximumSize(w, h) self.saturationValue.setText( str("{:+d}".format(self.sliderContrast.value()))) # saturation changed event handler def saturationUpdate(value): self.saturationValue.setText( str("{:+d}".format( int( self.slidersaturation2User( self.sliderSaturation.value()))))) # move not yet terminated or value not modified if self.sliderSaturation.isSliderDown() or self.slider2Saturation( value) == self.satCorrection: return self.sliderSaturation.valueChanged.disconnect() self.sliderSaturation.sliderReleased.disconnect() self.satCorrection = self.slider2Saturation( self.sliderSaturation.value()) self.dataChanged.emit() self.sliderSaturation.valueChanged.connect(saturationUpdate) self.sliderSaturation.sliderReleased.connect( lambda: saturationUpdate(self.sliderSaturation.value())) self.sliderSaturation.valueChanged.connect(saturationUpdate) self.sliderSaturation.sliderReleased.connect( lambda: saturationUpdate(self.sliderSaturation.value())) # brightness slider self.sliderBrightness = QbLUeSlider(Qt.Horizontal) self.sliderBrightness.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderBrightness.setRange(0, 100) self.sliderBrightness.setSingleStep(1) brightnessLabel = QbLUeLabel() brightnessLabel.setMaximumSize(150, 30) brightnessLabel.setText("Brightness") brightnessLabel.doubleClicked.connect( lambda: self.sliderBrightness.setValue( self.brightness2Slider(self.brightnessDefault))) self.brightnessValue = QLabel() font = self.brightnessValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.brightnessValue.setMinimumSize(w, h) self.brightnessValue.setMaximumSize(w, h) self.brightnessValue.setText( str("{:+d}".format(self.sliderContrast.value()))) # brightness changed event handler def brightnessUpdate(value): self.brightnessValue.setText( str("{:+d}".format( int( self.sliderBrightness2User( self.sliderBrightness.value()))))) # move not yet terminated or value not modified if self.sliderBrightness.isSliderDown() or self.slider2Brightness( value) == self.brightnessCorrection: return self.sliderBrightness.valueChanged.disconnect() self.sliderBrightness.sliderReleased.disconnect() self.brightnessCorrection = self.slider2Brightness( self.sliderBrightness.value()) self.dataChanged.emit() self.sliderBrightness.valueChanged.connect(brightnessUpdate) self.sliderBrightness.sliderReleased.connect( lambda: brightnessUpdate(self.sliderBrightness.value())) self.sliderBrightness.valueChanged.connect(brightnessUpdate) self.sliderBrightness.sliderReleased.connect( lambda: brightnessUpdate(self.sliderBrightness.value())) # attributes initialized in setDefaults, declared here # for the sake of correctness self.contrastCorrection = None # range self.satCorrection = None # range -0.5..0.5 self.brightnessCorrection = None # range -0.5..0.5 # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) gb1 = QGroupBox() gb1.setStyleSheet( "QGroupBox {border: 1px solid gray; border-radius: 4px}") l1 = QVBoxLayout() ct = QLabel() ct.setText('Contrast') l.setAlignment(Qt.AlignTop) l1.addWidget(ct) l1.addWidget(self.listWidget1) gb1.setLayout(l1) l.addWidget(gb1) l.addWidget(self.listWidget2) l.addWidget(contrastLabel) hl = QHBoxLayout() hl.addWidget(self.contrastValue) hl.addWidget(self.sliderContrast) l.addLayout(hl) l.addWidget(brightnessLabel) hl3 = QHBoxLayout() hl3.addWidget(self.brightnessValue) hl3.addWidget(self.sliderBrightness) l.addLayout(hl3) l.addWidget(saturationLabel) hl2 = QHBoxLayout() hl2.addWidget(self.saturationValue) hl2.addWidget(self.sliderSaturation) l.addLayout(hl2) self.setLayout(l) self.adjustSize() self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") self.setDefaults() self.setWhatsThis("""<b>Contrast Brightness Saturation</b><br> <b>Contrast</b> is enhanced using one of these two methods:<br> - <b>CLAHE</b> : increases the local contrast.<br> - <b>Multi-Mode</b> : increases the local contrast and the contrast between regions of the image.<br> For both methods the contrast slider controls the level of the correction.<br> With Multi-Mode enabled, use the option <b>Show Contrast Curve</b> to edit the correction curve and check <b>Preserve Highlights</b> for softer highlights.<br> <b>Brightness</b> and <b>Saturation</b> corrections are non linear to limit clipping.<br> Sliders are <b>reset</b> to their default value by double clicking the name of the slider.<br> """) # end setWhatsThis
def __init__(self, cModel, targetImage=None, axeSize=500, LUTSize=LUTSIZE, layer=None, parent=None, mainForm=None): """ @param cModel: color space used by colorPicker, slider2D and colorPicker @type cModel: cmConverter object @param axeSize: size of the color wheel @type axeSize: int @param targetImage: @type targetImage: imImage @param LUTSize: @type LUTSize: int @param layer: layer of targetImage linked to graphics form @type layer : QLayer @param parent: @type parent: """ super().__init__(parent=parent) self.mainForm = mainForm # used by saveLUT() # context help tag self.helpId = "LUT3DForm" self.cModel = cModel border = 20 self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize + 90, axeSize + 200) self.setAttribute(Qt.WA_DeleteOnClose) self.setBackgroundBrush(QBrush(Qt.black, Qt.SolidPattern)) self.currentHue, self.currentSat, self.currentPb = 0, 0, self.defaultColorWheelBr self.currentR, self.currentG, self.currentB = 0, 0, 0 self.size = axeSize # back links to image self.targetImage = weakProxy(targetImage) self.layer = weakProxy(layer) # currently selected grid node self.selected = None self.graphicsScene = QGraphicsScene() self.graphicsScene.options = None self.setScene(self.graphicsScene) # back to image layer self.graphicsScene.layer = weakProxy(layer) # init LUT freshLUT3D = LUT3D(None, size=LUTSize, alpha=True) self.graphicsScene.lut = freshLUT3D # init 2D slider QImg = hueSatPattern(axeSize, axeSize, cModel, bright=self.defaultColorWheelBr, border=border) self.graphicsScene.slider2D = colorChooser(cModel, QImg, target=self.targetImage, size=axeSize, border=border) self.graphicsScene.selectMarker = activeMarker.fromCross(parent=self.graphicsScene.slider2D) self.graphicsScene.selectMarker.setPos(axeSize / 2, axeSize / 2) # color wheel event handler def f1(p, r, g, b): h, s, br = self.cModel.rgb2cm(r, g, b) self.currentHue, self.currentSat, self.currentPb = h, s, br self.currentR, self.currentG, self.currentB = r, g, b self.bSliderUpdate() self.displayStatus() self.graphicsScene.slider2D.onMouseRelease = f1 self.graphicsScene.addItem(self.graphicsScene.slider2D) # Brightness slider self.bSliderHeight = 20 self.bSliderWidth = self.graphicsScene.slider2D.QImg.width() px = brightnessPattern(self.bSliderWidth, self.bSliderHeight, cModel, self.currentHue, self.currentSat).rPixmap self.graphicsScene.bSlider = QGraphicsPixmapItem(px, parent=self.graphicsScene.slider2D) self.graphicsScene.bSlider.setPos(QPointF(-border, self.graphicsScene.slider2D.QImg.height() - border)) bSliderCursor = activeMarker.fromTriangle(parent=self.graphicsScene.bSlider) bSliderCursor.setMoveRange(QRectF(0.0, bSliderCursor.size, self.graphicsScene.bSlider.pixmap().width(), 0.0)) bSliderCursor.setPos(self.graphicsScene.bSlider.pixmap().width() * self.defaultColorWheelBr, bSliderCursor.size) # cursor event handlers def f2(e, p, q): self.currentPb = p / float(self.bSliderWidth) QToolTip.showText(e.screenPos(), (str(int(self.currentPb * 100.0))), self) bSliderCursor.onMouseMove = f2 def f3(e, p, q): self.currentPb = p / float(self.bSliderWidth) self.graphicsScene.slider2D.QImg.setPb(self.currentPb) self.graphicsScene.slider2D.setPixmap(self.graphicsScene.slider2D.QImg.rPixmap) self.displayStatus() bSliderCursor.onMouseRelease = f3 # status bar offset = 60 self.graphicsScene.statusBar = QGraphicsTextItem() self.graphicsScene.statusBar.setPos(-20, axeSize + offset) self.graphicsScene.statusBar.setDefaultTextColor(QColor(255, 255, 255)) self.graphicsScene.statusBar.setPlainText('') self.graphicsScene.addItem(self.graphicsScene.statusBar) self.displayStatus() # self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) # grid self.grid = activeGrid(self.graphicsScene.lut.size, self.cModel, parent=self.graphicsScene.slider2D) self.graphicsScene.grid = self.grid # buttons pushButton1 = QbLUePushButton("Reset Grid") pushButton1.clicked.connect(self.onReset) pushButton2 = QbLUePushButton("Save LUT") pushButton2.clicked.connect(self.saveLUT) pushButton3 = QbLUePushButton("Smooth Grid") pushButton3.clicked.connect(self.onSmoothGrid) pushButton4 = QbLUePushButton('Set Mask') pushButton4.clicked.connect(self.setMask) # pushButton4 needs enabling/disabling self.pushButton4 = pushButton4 # options options1, optionNames1 = ['use image', 'use selection'], ['Use Image', 'Use Selection'] self.listWidget1 = optionsWidget(options=options1, optionNames=optionNames1, exclusive=True) """ def onSelect1(item): self.graphicsScene.options['use selection'] = item is self.listWidget1.items['use selection'] self.listWidget1.onSelect = onSelect1 """ self.listWidget1.setFocusPolicy(Qt.NoFocus) # set initial selection to 'use image' self.listWidget1.checkOption(self.listWidget1.intNames[0]) options2, optionNames2 = ['add node', 'remove node'], ['Add Node', 'Remove Node'] self.listWidget2 = optionsWidget(options=options2, optionNames=optionNames2, exclusive=True) """ def onSelect2(item): self.graphicsScene.options['add node'] = item is self.listWidget2.items['add node'] self.listWidget2.onSelect = onSelect2 """ # set initial selection to add node' self.listWidget2.checkOption(self.listWidget2.intNames[0]) options3 = ['select neighbors', 'reset removed nodes', 'show histogram', 'keep alpha'] optionNames3 = ['Select Neighbors', 'Reset Removed', 'Show Histogram', 'Keep Alpha'] self.listWidget3 = optionsWidget(options=options3, optionNames=optionNames3, exclusive=False) self.listWidget3.checkOption(self.listWidget3.intNames[0]) self.listWidget3.checkOption(self.listWidget3.intNames[1]) def onSelect3(item): option = item.internalName if option == 'show histogram': self.graphicsScene.slider2D.showTargetHist = self.graphicsScene.options[option] # .isChecked() self.graphicsScene.slider2D.updatePixmap() return if option == 'keep alpha': self.enableButtons() self.layer.applyToStack() self.listWidget3.onSelect = onSelect3 # set initial selection to 'select naighbors' item = self.listWidget3.items[options3[0]] item.setCheckState(Qt.Checked) self.graphicsScene.options = UDict((self.listWidget1.options, self.listWidget2.options, self.listWidget3.options)) # layouts hlButtons = QHBoxLayout() hlButtons.addWidget(pushButton1) hlButtons.addWidget(pushButton3) hlButtons.addWidget(pushButton2) hl = QHBoxLayout() vl1 = QVBoxLayout() vl1.addWidget(self.listWidget1) vl1.addWidget(pushButton4) hl.addLayout(vl1) hl.addWidget(self.listWidget2) hl.addWidget(self.listWidget3) vl = QVBoxLayout() for l in [hlButtons, hl]: vl.addLayout(l) # Init QWidget container for adding buttons and options to graphicsScene container = QWidget() container.setObjectName("container") container.setLayout(vl) ss = """QWidget#container{background: black}\ QListWidget{background-color: black; selection-background-color: black; border: none; font-size: 7pt}\ QListWidget::item{color: white;}\ QListWidget::item::selected{background: black; border: none}""" container.setStyleSheet(ss) for wdg in [self.listWidget1, self.listWidget2, self.listWidget3]: wdg.setMinimumWidth(wdg.sizeHintForColumn(0)) wdg.setMinimumHeight(wdg.sizeHintForRow(0)*len(wdg.items)) container.setGeometry(-offset//2, axeSize + offset - 20, axeSize + offset, 20) self.graphicsScene.addWidget(container) #container.setStyleSheet("QPushButton {color: white;}\ # QPushButton:pressed, QPushButton:hover {background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #0d5ca6, stop: 1 #2198c0);}") self.setWhatsThis( """ <b>2.5D LUT Editor</b><br> HSpB layers are slower than HSV, but usually they give better results.<br> <b>Select nodes</b> with mouse clicks on the image. Selected nodes are shown as small black circles on the color wheel.<br> <b>Modify the color</b> of a node by dragging it on the wheel. Several nodes can be moved simultaneously by grouping them.<br> <b>Group nodes</b> :<br> 1 - select them with the mouse : while pressing the mouse left button, drag a rubber band around the nodes to select;<br> 2 - next, right click any one of the selected nodes and choose group from the context menu<br> <b>unselect nodes</b> :<br> 1 - check the option Remove Node;<br> 2 - ungroup;<br> 3 - on the image, click the pixels to unselect.<br> <b>Caution</b> : Selecting nodes with the mouse is enabled only when the Color Chooser is closed.<br> Click the <b> Smooth Grid</b> button to smooth color transitions between neighbor nodes.<br> Check the <br>Keep Alpha</b> option to forward the alpha channel without modifications.<br> This option must be unchecked to build a mask from the 3D LUT.<br> """ ) # end of setWhatsThis
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None): super().__init__(layer=layer, targetImage=targetImage, parent=parent) ####################################### # Libraw correspondences: # rgb_xyz_matrix is libraw cam_xyz # camera_whitebalance is libraw cam_mul # daylight_whitebalance is libraw pre_mul # dng correspondences: # ASSHOTNEUTRAL tag value is (X,Y,Z) = 1 / rawpyObj.camera_whitebalance ########################################## rawpyObj = layer.parentImage.rawImage # constants and as shot values self.XYZ2CameraMatrix = rawpyObj.rgb_xyz_matrix[:3, :] self.XYZ2CameraInverseMatrix = np.linalg.inv(self.XYZ2CameraMatrix) # initial post processing multipliers (as shot) m1, m2, m3, m4 = rawpyObj.camera_whitebalance self.asShotMultipliers = ( m1 / m2, 1.0, m3 / m2, m4 / m2 ) # normalization is mandatory : for nef files white balance is around 256 self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint( *1 / np.array(self.asShotMultipliers[:3]), self.XYZ2CameraMatrix) self.rawMultipliers = self.asShotMultipliers # rawpyObj.camera_whitebalance # = 1/(dng ASSHOTNEUTRAL tag value) self.sampleMultipliers = False self.samples = [] ######################################## # XYZ-->Camera conversion matrix: # Last row is zero for RGB cameras (cf. rawpy and libraw docs). # type ndarray, shape (4,3) ######################################### # attributes initialized in setDefaults, declared here for the sake of correctness self.tempCorrection, self.tintCorrection, self.expCorrection, self.highCorrection,\ self.contCorrection, self.satCorrection, self.brCorrection = [None] * 7 # contrast spline vie (initialized by setContrastSpline) self.contrastForm = None # tone spline view (initialized by setToneSpline) self.toneForm = None # dock containers for contrast and tome forms self.dockC, self.dockT = None, None # options optionList0, optionNames0 = ['Auto Brightness', 'Preserve Highlights' ], ['Auto Expose', 'Preserve Highlights'] optionList1, optionNames1 = ['Auto WB', 'Camera WB', 'User WB' ], ['Auto', 'Camera (As Shot)', 'User'] optionList2, optionNames2 = [ 'cpLookTable', 'cpToneCurve', 'manualCurve' ], [ 'Use Camera Profile Look Table', 'Show Tone Curves', 'Show Contrast Curve' ] self.listWidget1 = optionsWidget( options=optionList0, optionNames=optionNames0, exclusive=False, changed=lambda: self.dataChanged.emit(1)) self.listWidget2 = optionsWidget( options=optionList1, optionNames=optionNames1, exclusive=True, changed=lambda: self.dataChanged.emit(1)) self.listWidget3 = optionsWidget( options=optionList2, optionNames=optionNames2, exclusive=False, changed=lambda: self.dataChanged.emit(2)) self.options = UDict( (self.listWidget1.options, self.listWidget2.options, self.listWidget3.options)) # display the 'as shot' temperature item = self.listWidget2.item(1) item.setText(item.text() + ' : %d' % self.asShotTemp) # temperature slider self.sliderTemp = QbLUeSlider(Qt.Horizontal) self.sliderTemp.setStyleSheet( QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderTemp.setRange( 0, 100) # TODO 130 changed to 100 12/11/18 validate self.sliderTemp.setSingleStep(1) self.tempLabel = QLabel() self.tempLabel.setText("Temp") self.tempValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("10000") h = metrics.height() self.tempValue.setMinimumSize(w, h) self.tempValue.setMaximumSize(w, h) self.tempValue.setText( str("{:.0f}".format(self.slider2Temp(self.sliderTemp.value())))) self.sliderTemp.valueChanged.connect( self.tempUpdate) # signal send new value as parameter self.sliderTemp.sliderReleased.connect(lambda: self.tempUpdate( self.sliderTemp.value())) # signal pass no parameter # tint slider self.sliderTint = QbLUeSlider(Qt.Horizontal) self.sliderTint.setStyleSheet( QbLUeSlider.bLueSliderDefaultIMGColorStylesheet) self.sliderTint.setRange(0, 150) self.sliderTint.setSingleStep(1) self.tintLabel = QLabel() self.tintLabel.setText("Tint") self.tintValue = QLabel() font = self.tempValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.tintValue.setMinimumSize(w, h) self.tintValue.setMaximumSize(w, h) self.tintValue.setText( str("{:.0f}".format(self.sliderTint2User( self.sliderTint.value())))) self.sliderTint.valueChanged.connect(self.tintUpdate) self.sliderTint.sliderReleased.connect(lambda: self.tintUpdate( self.sliderTint.value())) # signal pass no parameter) ###################### # From libraw and dcraw sources: # Exposure and brightness are curve transformations. # Exposure curve is y = alpha*x, with cubic root ending; it is applied before demosaicing. # Brightness is (similar to) y = x**alpha and part of gamma transformation from linear sRGB to RGB. # Exposure and brightness both dilate the histogram towards highlights. # Exposure dilatation is uniform (homothety), brightness dilataion is # maximum for the midtones and the highlghts are preserved. # As a consequence, normal workflow begins with the adjustment of exposure, # to fill the entire range of the histogram and to adjust the highlights. Next, # one adjusts the brightness to put the midtones at the level we want them to be. # Cf. https://www.cambridgeincolour.com/forums/thread653.htm ##################### # profile combo self.dngDict = self.setCameraProfilesCombo() # cameraProfilesCombo index changed event handler def cameraProfileUpdate(value): self.dngDict = self.cameraProfilesCombo.itemData(value) if self.options['cpToneCurve']: toneCurve = dngProfileToneCurve( self.dngDict.get('ProfileToneCurve', [])) self.toneForm.baseCurve = [ QPointF(x * axeSize, -y * axeSize) for x, y in zip(toneCurve.dataX, toneCurve.dataY) ] self.toneForm.update() # recompute as shot temp and tint using new profile self.asShotTemp, self.asShotTint = multipliers2TemperatureAndTint( *1 / np.array(self.asShotMultipliers[:3]), self.XYZ2CameraMatrix, self.dngDict) # display updated as shot temp item = self.listWidget2.item(1) item.setText(item.text().split(":")[0] + ': %d' % self.asShotTemp) # invalidate cache self.layer.bufCache_HSV_CV32 = None self.dataChanged.emit(2) # 2 = no postprocessing self.cameraProfilesCombo.currentIndexChanged.connect( cameraProfileUpdate) # denoising combo self.denoiseCombo = QComboBox() items = OrderedDict([('Off', 0), ('Medium', 1), ('Full', 2)]) for key in items: self.denoiseCombo.addItem(key, items[key]) # denoiseCombo index changed event handler def denoiseUpdate(value): self.denoiseValue = self.denoiseCombo.itemData(value) self.dataChanged.emit(1) self.denoiseCombo.currentIndexChanged.connect(denoiseUpdate) # overexposed area restoration self.overexpCombo = QComboBox() items = OrderedDict([('Clip', 0), ('Ignore', 1), ('Blend', 2), ('Reconstruct', 3)]) for key in items: self.overexpCombo.addItem(key, items[key]) # overexpCombo index changed event handler def overexpUpdate(value): self.overexpValue = self.overexpCombo.itemData(value) self.dataChanged.emit(1) self.overexpCombo.currentIndexChanged.connect(overexpUpdate) # exp slider self.sliderExp = QbLUeSlider(Qt.Horizontal) self.sliderExp.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderExp.setRange(0, 100) self.sliderExp.setSingleStep(1) self.expLabel = QLabel() self.expLabel.setText("Exp.") self.expValue = QLabel() font = self.expValue.font() metrics = QFontMetrics(font) w = metrics.width("+1.0") h = metrics.height() self.expValue.setMinimumSize(w, h) self.expValue.setMaximumSize(w, h) self.expValue.setText( str("{:.1f}".format(self.slider2Exp(self.sliderExp.value())))) # exp done event handler def expUpdate(value): self.expValue.setText( str("{:+.1f}".format( self.sliderExp2User(self.sliderExp.value())))) # move not yet terminated or value not modified if self.sliderExp.isSliderDown() or self.slider2Exp( value) == self.expCorrection: return try: self.sliderExp.valueChanged.disconnect() self.sliderExp.sliderReleased.disconnect() except RuntimeError: pass # rawpy: expCorrection range is -2.0...3.0, boiling down to exp_shift range 2**(-2)=0.25...2**3=8.0 self.expCorrection = self.slider2Exp(self.sliderExp.value()) self.dataChanged.emit(1) self.sliderExp.valueChanged.connect( expUpdate) # send new value as parameter self.sliderExp.sliderReleased.connect(lambda: expUpdate( self.sliderExp.value())) # signal pass no parameter self.sliderExp.valueChanged.connect( expUpdate) # send new value as parameter self.sliderExp.sliderReleased.connect(lambda: expUpdate( self.sliderExp.value())) # signal pass no parameter # brightness slider brSlider = QbLUeSlider(Qt.Horizontal) brSlider.setRange(1, 101) self.sliderExp.setSingleStep(1) brSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderBrightness = brSlider brLabel = QLabel() brLabel.setText("Bright.") self.brValue = QLabel() font = self.expValue.font() metrics = QFontMetrics(font) w = metrics.width("+99") h = metrics.height() self.brValue.setMinimumSize(w, h) self.brValue.setMaximumSize(w, h) self.brValue.setText( str("{:+d}".format( int(self.brSlider2User(self.sliderBrightness.value()))))) # brightness done event handler def brUpdate(value): self.brValue.setText( str("{:+d}".format( int(self.brSlider2User(self.sliderBrightness.value()))))) # move not yet terminated or value not modified if self.sliderBrightness.isSliderDown() or self.slider2Br( value) == self.brCorrection: return try: self.sliderBrightness.valueChanged.disconnect() self.sliderBrightness.sliderReleased.disconnect() except RuntimeError: pass self.brCorrection = self.slider2Br(self.sliderBrightness.value()) self.dataChanged.emit(1) self.sliderBrightness.sliderReleased.connect( lambda: brUpdate(self.sliderBrightness.value())) self.sliderBrightness.valueChanged.connect( brUpdate) # send new value as parameter self.sliderBrightness.valueChanged.connect( brUpdate) # send new value as parameter self.sliderBrightness.sliderReleased.connect( lambda: brUpdate(self.sliderBrightness.value())) # contrast slider self.sliderCont = QbLUeSlider(Qt.Horizontal) self.sliderCont.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.sliderCont.setRange(0, 20) self.sliderCont.setSingleStep(1) self.contLabel = QLabel() self.contLabel.setText("Cont.") self.contValue = QLabel() font = self.contValue.font() metrics = QFontMetrics(font) w = metrics.width("100") h = metrics.height() self.contValue.setMinimumSize(w, h) self.contValue.setMaximumSize(w, h) self.contValue.setText( str("{:.0f}".format(self.slider2Cont(self.sliderCont.value())))) # cont done event handler def contUpdate(value): self.contValue.setText( str("{:.0f}".format(self.slider2Cont( self.sliderCont.value())))) # move not yet terminated or value not modified if self.sliderCont.isSliderDown() or self.slider2Cont( value) == self.tempCorrection: return try: self.sliderCont.valueChanged.disconnect() self.sliderCont.sliderReleased.disconnect() except RuntimeError: pass self.contCorrection = self.slider2Cont(self.sliderCont.value()) self.contValue.setText(str("{:+d}".format(self.contCorrection))) # force to recalculate the spline self.layer.autoSpline = True self.dataChanged.emit( 3) # no postprocessing and no camera profile stuff self.sliderCont.valueChanged.connect( contUpdate) # send new value as parameter self.sliderCont.sliderReleased.connect(lambda: contUpdate( self.sliderCont.value())) # signal has no parameter self.sliderCont.valueChanged.connect( contUpdate) # send new value as parameter self.sliderCont.sliderReleased.connect(lambda: contUpdate( self.sliderCont.value())) # signal has no parameter # saturation slider self.sliderSat = QbLUeSlider(Qt.Horizontal) self.sliderSat.setStyleSheet( QbLUeSlider.bLueSliderDefaultColorStylesheet) self.sliderSat.setRange(0, 100) self.sliderSat.setSingleStep(1) satLabel = QLabel() satLabel.setText("Sat.") self.satValue = QLabel() font = self.satValue.font() metrics = QFontMetrics(font) w = metrics.width("+10") h = metrics.height() self.satValue.setMinimumSize(w, h) self.satValue.setMaximumSize(w, h) self.satValue.setText( str("{:+d}".format(self.slider2Sat(self.sliderSat.value())))) """sat done event handler""" def satUpdate(value): self.satValue.setText( str("{:+d}".format(self.slider2Sat(self.sliderSat.value())))) # move not yet terminated or value not modified if self.sliderSat.isSliderDown() or self.slider2Sat( value) == self.satCorrection: return try: self.sliderSat.valueChanged.disconnect() self.sliderSat.sliderReleased.disconnect() except RuntimeError: pass self.satCorrection = self.slider2Sat(self.sliderSat.value()) self.dataChanged.emit( 3) # no post processing and no camera profile stuff self.sliderSat.valueChanged.connect( satUpdate) # send new value as parameter self.sliderSat.sliderReleased.connect(lambda: satUpdate( self.sliderSat.value())) # signal has no parameter self.sliderSat.valueChanged.connect( satUpdate) # send new value as parameter self.sliderSat.sliderReleased.connect(lambda: satUpdate( self.sliderSat.value())) # signal has no parameter # self.dataChanged.connect(self.updateLayer) # TODO 30/10/18 moved to base class self.setStyleSheet("QListWidget, QLabel {font : 7pt;}") # layout l = QVBoxLayout() l.addWidget(self.listWidget3) hl01 = QHBoxLayout() hl01.addWidget(QLabel('Camera Profile')) hl01.addWidget(self.cameraProfilesCombo) l.addLayout(hl01) hl0 = QHBoxLayout() hl0.addWidget(QLabel('Denoising')) hl0.addWidget(self.denoiseCombo) l.addLayout(hl0) hl00 = QHBoxLayout() hl00.addWidget(QLabel('Overexp. Restoration')) hl00.addWidget(self.overexpCombo) l.addLayout(hl00) hl1 = QHBoxLayout() hl1.addWidget(self.expLabel) hl1.addWidget(self.expValue) hl1.addWidget(self.sliderExp) l.addLayout(hl1) hl8 = QHBoxLayout() hl8.addWidget(brLabel) hl8.addWidget(self.brValue) hl8.addWidget(self.sliderBrightness) l.addLayout(hl8) l.addWidget(self.listWidget1) self.listWidget2.setStyleSheet( "QListWidget {border: 0px;} QListWidget::item {border: 0px; padding-left: 20px;}" ) vl1 = QVBoxLayout() vl1.addWidget(QLabel('White Balance')) vl1.addWidget(self.listWidget2) gb1 = QGroupBox() gb1.setStyleSheet( "QGroupBox {border: 1px solid gray; border-radius: 4px}") hl2 = QHBoxLayout() hl2.addWidget(self.tempLabel) hl2.addWidget(self.tempValue) hl2.addWidget(self.sliderTemp) hl3 = QHBoxLayout() hl3.addWidget(self.tintLabel) hl3.addWidget(self.tintValue) hl3.addWidget(self.sliderTint) vl1.addLayout(hl2) vl1.addLayout(hl3) gb1.setLayout(vl1) l.addWidget(gb1) hl4 = QHBoxLayout() hl4.addWidget(self.contLabel) hl4.addWidget(self.contValue) hl4.addWidget(self.sliderCont) hl7 = QHBoxLayout() hl7.addWidget(satLabel) hl7.addWidget(self.satValue) hl7.addWidget(self.sliderSat) # separator sep = QFrame() sep.setFrameShape(QFrame.HLine) sep.setFrameShadow(QFrame.Sunken) l.addWidget(sep) l.addLayout(hl4) l.addLayout(hl7) l.addStretch(1) self.setLayout(l) self.adjustSize() self.setDefaults() self.setWhatsThis("""<b>Development of raw files</b><br> <b>Default settings</b> are a good starting point.<br> A <b>Tone Curve</b> is applied to the raw image prior to postprocessing.<br> The cuvre can be edited by checking the option <b>Show Tone Curve</b>; this option works best with manual exposure.<br> <b>Contrast</b> correction is based on an automatic algorithm well suited to multi-mode histograms.<br> <b>Brightness, Contrast</b> and <b>Saturation</b> levels</b> are adjustable with the correponding sliders.<br> The <b>Contrast Curve</b> can be edited manually by checking the option <b>Show Contrast Curve</b>.<br> Uncheck <b>Auto Expose</b> to adjust the exposure manually.<br> The <b>OverExp. Rest.</b> slider controls the mode of restoration of overexposed areas. Valid values are 0 to 3 (0=clip;1=unclip;2=blend;3=rebuild); (with Auto Exposed checked the mode is clip).<br> """) # end of setWhatsThis
def __init__(self, cModel, targetImage=None, axeSize=500, LUTSize=LUTSIZE, layer=None, parent=None, mainForm=None): """ @param cModel: color space used by colorPicker, slider2D and colorPicker @type cModel: cmConverter object @param axeSize: size of the color wheel @type axeSize: int @param targetImage: @type targetImage: imImage @param LUTSize: @type LUTSize: int @param layer: layer of targetImage linked to graphics form @type layer : QLayer @param parent: @type parent: """ super().__init__(targetImage=targetImage, layer=layer, parent=parent) self.mainForm = mainForm # used by saveLUT() # context help tag self.helpId = "LUT3DForm" self.cModel = cModel border = 20 self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize + 90, axeSize + 250) self.setAttribute(Qt.WA_DeleteOnClose) self.setBackgroundBrush(QBrush(Qt.black, Qt.SolidPattern)) self.currentHue, self.currentSat, self.currentPb = 0, 0, self.defaultColorWheelBr self.currentR, self.currentG, self.currentB = 0, 0, 0 self.size = axeSize # currently selected grid node self.selected = None # init LUT freshLUT3D = LUT3D(None, size=LUTSize, alpha=True) self.graphicsScene.lut = freshLUT3D # init 2D slider (color wheel) swatchImg = hueSatPattern(axeSize, axeSize, cModel, bright=self.defaultColorWheelBr, border=border) slider2D = colorChooser(cModel, swatchImg, target=self.targetImage, size=axeSize, border=border) ######################################################################### # CAUTION : sliedr2D has a non null offset # slider2D (and QImg) topleft corner is at scene pos -slider2D.offset() ######################################################################### self.graphicsScene.addItem(slider2D) self.graphicsScene.slider2D = slider2D ############################## # neutral and working markers ############################## offset = slider2D.offset() neutralMarker = activeMarker.fromCross(parent=slider2D) neutralMarker.setPos(swatchImg.width() / 2 + offset.x(), swatchImg.height() / 2 + offset.y()) self.workingMarker = activeMarker.fromCross(parent=slider2D) # default pos: average skin tone pt = QPointF(*swatchImg.GetPoint(*rgb2hsB(*cmyk2rgb(6, 25, 30, 0))[:2])) + offset self.workingMarker.setPos(pt.x(), pt.y()) self.workingMarker.onMouseMove = lambda e, x, y: self.displayStatus() # swatch event handler def f1(p, r, g, b): h, s, br = self.cModel.rgb2cm(r, g, b) self.currentHue, self.currentSat, self.currentPb = h, s, br self.currentR, self.currentG, self.currentB = r, g, b self.bSliderUpdate() self.displayStatus() self.graphicsScene.slider2D.onMouseRelease = f1 # Brightness slider self.bSliderHeight = 20 self.bSliderWidth = self.graphicsScene.slider2D.QImg.width() px = brightnessPattern(self.bSliderWidth, self.bSliderHeight, cModel, self.currentHue, self.currentSat).rPixmap self.graphicsScene.bSlider = QGraphicsPixmapItem(px, parent=self.graphicsScene.slider2D) self.graphicsScene.bSlider.setPos(QPointF(-border, self.graphicsScene.slider2D.QImg.height() - border)) bSliderCursor = activeMarker.fromTriangle(parent=self.graphicsScene.bSlider) bSliderCursor.setMoveRange(QRectF(0.0, bSliderCursor.size, self.graphicsScene.bSlider.pixmap().width(), 0.0)) bSliderCursor.setPos(self.graphicsScene.bSlider.pixmap().width() * self.defaultColorWheelBr, bSliderCursor.size) # bSlider event handlers def f2(e, p, q): self.currentPb = p / float(self.bSliderWidth) self.graphicsScene.slider2D.QImg.setPb(self.currentPb) self.graphicsScene.slider2D.setPixmap(self.graphicsScene.slider2D.QImg.rPixmap) QToolTip.showText(e.screenPos(), (str(int(self.currentPb * 100.0))), self) bSliderCursor.onMouseMove = f2 def f3(e, p, q): self.currentPb = p / float(self.bSliderWidth) # self.graphicsScene.slider2D.QImg.setPb(self.currentPb) # self.graphicsScene.slider2D.setPixmap(self.graphicsScene.slider2D.QImg.rPixmap) self.displayStatus() bSliderCursor.onMouseRelease = f3 # grid self.grid = activeGrid(self.graphicsScene.lut.size, self.cModel, parent=self.graphicsScene.slider2D) self.graphicsScene.grid = self.grid # buttons pushButton1 = QbLUePushButton("Reset Grid") pushButton1.clicked.connect(self.onReset) pushButton2 = QbLUePushButton("Save LUT") pushButton2.clicked.connect(self.saveLUT) pushButton3 = QbLUePushButton("Smooth Grid") pushButton3.clicked.connect(self.onSmoothGrid) pushButton4 = QbLUePushButton('Set Mask') pushButton4.clicked.connect(self.setMask) # pushButton4 needs enabling/disabling self.pushButton4 = pushButton4 # options options1, optionNames1 = ['use image', 'use selection'], ['Use Image', 'Use Selection'] self.listWidget1 = optionsWidget(options=options1, optionNames=optionNames1, exclusive=True) self.listWidget1.setFocusPolicy(Qt.NoFocus) # set initial selection to 'use image' self.listWidget1.checkOption(self.listWidget1.intNames[0]) options2, optionNames2 = ['add node', 'remove node'], ['Add Node', 'Remove Node'] self.listWidget2 = optionsWidget(options=options2, optionNames=optionNames2, exclusive=True) # set initial selection to 'add node' self.listWidget2.checkOption(self.listWidget2.intNames[0]) options3 = ['select neighbors', 'reset removed nodes', 'show histogram', 'keep alpha'] optionNames3 = ['Select Neighbors', 'Reset Removed', 'Show Histogram', 'Keep Alpha'] self.listWidget3 = optionsWidget(options=options3, optionNames=optionNames3, exclusive=False) self.listWidget3.checkOption(self.listWidget3.intNames[0]) self.listWidget3.checkOption(self.listWidget3.intNames[1]) def onSelect3(item): option = item.internalName if option == 'show histogram': self.graphicsScene.slider2D.showTargetHist = self.graphicsScene.options[option] self.graphicsScene.slider2D.updatePixmap() return if option == 'keep alpha': self.enableButtons() self.layer.applyToStack() self.listWidget3.onSelect = onSelect3 # set initial selection to 'select naighbors' item = self.listWidget3.items[options3[0]] item.setCheckState(Qt.Checked) self.graphicsScene.options = UDict((self.listWidget1.options, self.listWidget2.options, self.listWidget3.options)) for wdg in [self.listWidget1, self.listWidget2, self.listWidget3]: wdg.setMinimumWidth(wdg.sizeHintForColumn(0)) wdg.setMinimumHeight(wdg.sizeHintForRow(0)*len(wdg.items)) # color format combo infoCombo = QComboBox() oDict = OrderedDict([('RGB', 0), ('CMYK', 1), ('HSV', 2)]) for key in oDict: infoCombo.addItem(key, oDict[key]) labelFormat = QLabel() def colorInfoFormatChanged(value): self.colorInfoFormat = infoCombo.itemData(value) if self.colorInfoFormat == 0: labelFormat.setText('RGB') elif self.colorInfoFormat == 1: labelFormat.setText('CMYK') elif self.colorInfoFormat == 2: labelFormat.setText('HSV') self.displayStatus() infoCombo.currentIndexChanged.connect(colorInfoFormatChanged) # working marker position editor self.info = QLineEdit() self.info.setMaximumSize(120, 40) # returnPressed slot def infoDone(): try: token = self.info.text().split(' ') if self.colorInfoFormat == 0: # RGB r, g, b = [int(x) for x in token if x.isdigit()] pt = QPointF(*swatchImg.GetPoint(*rgb2hsB(r, g, b)[:2])) + offset elif self.colorInfoFormat == 1: # CMYK c, m, y, k = [int(x) for x in token if x.isdigit()] pt = QPointF(*swatchImg.GetPoint(*rgb2hsB(*cmyk2rgb(c, m, y, k))[:2])) + offset elif self.colorInfoFormat == 2: # HSV h, s, _ = [int(x) for x in token if x.isdigit()] if not 0 <= s <= 100: raise ValueError pt = QPointF(*swatchImg.GetPoint(h, s / 100.0)) + offset else: raise ValueError self.workingMarker.setPos(pt.x(), pt.y()) except ValueError: dlgWarn("Invalid color") self.info.returnPressed.connect(infoDone) # layout gl = QGridLayout() for i, button in enumerate([pushButton1, pushButton3, pushButton2]): gl.addWidget(button, 0, i) gl.addWidget(pushButton4, 1, 0) gl.addWidget(infoCombo, 1, 1) for i, widget in enumerate([self.listWidget1, self.listWidget2, self.listWidget3]): gl.addWidget(widget, 2 if i < 2 else 1, i, 1 if i < 2 else 2, 1, 0) hl = QHBoxLayout() hl.addWidget(labelFormat) hl.addWidget(self.info) gl.addLayout(hl, 3, 0, -1, -1) self.addCommandLayout(gl) # set defaults self.colorInfoFormat = 0 # RGB colorInfoFormatChanged(self.colorInfoFormat) # whatsthis self.setWhatsThis( """ <b>2.5D LUT Editor</b><br> <b>Select nodes</b> with mouse clicks on the image. Selected nodes are shown as small black circles on the color wheel.<br> <b>Modify the color</b> of a node by dragging it on the wheel. Several nodes can be moved simultaneously by grouping them.<br> <b>Group nodes</b> :<br> 1 - group nodes with the mouse : while pressing the mouse left button, drag a rubber band around the nodes to group;<br> 2 - next, right click any one of the selected nodes and choose group from the context menu<br> <b>Unselect nodes</b> :<br> 1 - check the option Remove Node;<br> 2 - ungroup;<br> 3 - on the image, click the pixels to unselect.<br> <b>Warning</b> : Selecting/unselecting nodes with the mouse is enabled only when the Color Chooser is closed.<br> Press the <b> Smooth Grid</b> button to smooth color transitions between neighbor nodes.<br> <b>Crosshair</b> markers indicate neutral gray tones and average skin tones. They can be moved with the mouse. The position of the second marker is reflected in the editable bottom right box. Due to inherent properties of the CMYK color model, CMYK input values may be modified silently.<br> <b>The grid can be zoomed</b> with the mouse wheel.<br> Check the <b>Keep Alpha</b> option to forward the alpha channel without modifications.<br> This option must be unchecked to build a mask from the 3D LUT.<br> HSpB layer is slower than HSV, but usually gives better results.<br> """ ) # end of setWhatsThis
def __init__(self, targetImage=None, axeSize=500, layer=None, parent=None, mainForm=None): super(transForm, self).__init__(parent=parent) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setMinimumSize(axeSize, axeSize) self.setAttribute(Qt.WA_DeleteOnClose) # back links to image self.targetImage = weakProxy(targetImage) self.img = weakProxy(targetImage) self.layer = weakProxy(layer) self.mainForm = mainForm # options optionList1, optionNames1 = ['Free', 'Rotation', 'Translation'], [ 'Free Transformation', 'Rotation', 'Translation' ] self.listWidget1 = optionsWidget(options=optionList1, optionNames=optionNames1, exclusive=True) optionList2, optionNames2 = ['Transparent' ], ['Set Transparent Pixels To Black'] self.listWidget2 = optionsWidget(options=optionList2, optionNames=optionNames2, exclusive=False) self.options = UDict( (self.listWidget1.options, self.listWidget2.options)) # set initial selection to Perspective self.listWidget1.checkOption(optionList1[0]) # option changed handler def g(item): l = self.layer l.tool.setBaseTransform() #self.layer.tool.setVisible(True) l.applyToStack() l.parentImage.onImageChanged() self.listWidget1.onSelect = g self.listWidget2.onSelect = g pushButton1 = QPushButton(' Reset Transformation ') pushButton1.adjustSize() def f(): self.layer.tool.resetTrans() pushButton1.clicked.connect(f) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) l.addWidget(self.listWidget1) l.addWidget(self.listWidget2) hl = QHBoxLayout() hl.setAlignment(Qt.AlignHCenter) hl.addWidget(pushButton1) l.addLayout(hl) self.setLayout(l) self.adjustSize() self.setWhatsThis(""" <b>Geometric transformation :</b><br> Choose a transformation type and drag either corner of the image using the small square red buttons.<br> Ctrl+Alt+Drag to change the <b>initial positions</b> of buttons. """)