def merge_with_layer_immediately_below(self): """ Merges a layer with the next lower visible layer. Does nothing if mode is preview or the target layer is an adjustment layer. """ if not hasattr(self, 'inputImg'): return ind = self.getLowerVisibleStackIndex() if ind < 0: # no visible layer found return target = self.parentImage.layersStack[ind] if hasattr(target, 'inputImg') or self.parentImage.useThumb: info = "Uncheck Preview first" if self.parentImage.useThumb else "Target layer must be background or image" dlgWarn("Cannot Merge layers", info=info) return # update stack self.parentImage.layersStack[0].applyToStack() # merge # target.setImage(self) qp = QPainter(target) qp.setCompositionMode(self.compositionMode) qp.setOpacity(self.opacity) qp.drawImage(QRect(0, 0, self.width(), self.height()), self) target.updatePixmap() self.parentImage.layerView.clear(delete=False) currentIndex = self.getStackIndex() self.parentImage.activeLayerIndex = ind self.parentImage.layersStack.pop(currentIndex) self.parentImage.layerView.setLayers(self.parentImage)
def run(self): image = QImage() image.load(str(self.filename)) mask = QImage(image.size(), QImage.Format_RGB32) mask.fill(Qt.black) maskfile = self.filename.parent / (self.filename.stem + ".mask") if maskfile.exists(): bitmap = QImage(str(maskfile)) if bitmap.size() != image.size(): raise Exception("Mask %s doesn't match image size" % maskfile) mask.fill(QColor.fromRgbF(1.0, 0.0, 1.0)) p = QPainter(mask) p.setCompositionMode(QPainter.CompositionMode_Multiply) p.drawImage(mask.rect(), bitmap) p.end() self.view.imagefile = self.filename self.view.image = image self.view.mask = mask self.view.maskfile = maskfile self.view.path = list() self.view.changed = False self.view.update()
def paintEvent(self, event): painter = QPainter(self) painter.fillRect(self.rect(), QBrush(self._color_srgb)) painter.setCompositionMode(QPainter.RasterOp_SourceXorDestination) pen = QPen(QBrush(Qt.white), 2., Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin) painter.setPen(pen) painter.drawRect(self.rect().adjusted(1, 1, -1, -1))
def get_icon(name, color=None): pixmap = QPixmap(f'icons-fontawesome/{name}.svg') if color != None: painter = QPainter(pixmap) painter.setCompositionMode(QPainter.CompositionMode_SourceIn) painter.fillRect(pixmap.rect(), color) painter.end() return QIcon(pixmap)
def pix_add_blurry(pix, transparency: float): temp_pix = QPixmap(pix.size()) temp_pix.fill(Qt.transparent) painter = QPainter(temp_pix) painter.setCompositionMode(QPainter.CompositionMode_Source) painter.drawPixmap(0, 0, pix) painter.setCompositionMode(QPainter.CompositionMode_DestinationIn) painter.fillRect(temp_pix.rect(), QColor(0, 0, 0, int(255 * transparency))) painter.end() return temp_pix
def set_rop2(op: int, painter: QPainter): """ Configure a QPainter with the required binary raster operation. :parma op: The operation identifier. :param painter: The painter being used. :returns: The operation that will be processed. """ if op < 0 or op >= len(_rop2): return None mode = _rop2[op] painter.setCompositionMode(mode) return mode
def make_compose_image(self): self.make_layer_image() self.compose_qimg = QImage(self.org_img_width, self.org_img_height, QImage.Format_RGBA8888) painter = QPainter(self.compose_qimg) painter.setCompositionMode(QPainter.CompositionMode_SourceOver) painter.drawImage(0, 0, self.org_qimg) painter.setCompositionMode(QPainter.CompositionMode_SourceOver) painter.drawImage(0, 0, self.layer_qimg) painter.end()
def erase_cursor_rect(self, pos: Tuple[int, int]): painter = QPainter(self.cursors_pixmap) cursor_rect = QRect(self.byte_rect) h = cursor_rect.height() i, j = pos cursor_rect.moveTopLeft(QPoint(i * self.byte_advance, j * h)) painter.setCompositionMode(QPainter.CompositionMode_Source) painter.setPen(Qt.NoPen) painter.setBrush(Qt.transparent) painter.drawRect(cursor_rect) painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
def paintEvent(self, event): painter = QPainter(self) painter.drawPixmap(0, 0, self._pixmap) painter.save() painter.setCompositionMode(QPainter.RasterOp_SourceXorDestination) pen = QPen(QBrush(Qt.white), 2., Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin) painter.setPen(pen) s = self._scale painter.drawRect( QRect(self._pos.x() * s - 1, (self._size.height() - 1 - self._pos.y()) * s - 1, s + 2, s + 2)) painter.restore()
def join_pixmap(self, p1, p2): result = QPixmap(p1.size()) result.fill(QtCore.Qt.transparent) painter = QPainter(result) painter.setRenderHint(QPainter.Antialiasing) painter.setCompositionMode(QPainter.CompositionMode_Source) painter.drawPixmap(QtCore.QPoint(), p1) painter.setCompositionMode(QPainter.CompositionMode_Overlay) painter.drawPixmap(QtCore.QPoint(), p2) painter.end() return result
def drawCircle(self, point): painter = QPainter(self.image) painter.setCompositionMode(QPainter.CompositionMode_Source) painter.setRenderHint(QPainter.Antialiasing) brush = QBrush(QColor(0, 0, 255, 255), Qt.SolidPattern) pen = QPen(brush, 0.2 * self.myPenWidth / self.scaleFactor, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) painter.setPen(pen) radius = 1.5 * self.myPenWidth / self.scaleFactor rectangle = QRectF((point.x() / self.scaleFactor) - (radius / 2), (point.y() / self.scaleFactor) - (radius / 2), radius, radius) painter.drawEllipse(rectangle) self.update()
def paintEvent(self, event): p = QPainter() p.begin(self) self._normalMap.render(p, event.rect()) p.setPen(Qt.black) p.drawText( self.rect(), Qt.AlignBottom | Qt.TextWordWrap, "GridCal, Map data CCBYSA 2009 " "OpenStreetMap.org contributors") p.end() if self.invert: p = QPainter(self) p.setCompositionMode(QPainter.CompositionMode_Difference) p.fillRect(event.rect(), Qt.white) p.end()
def checkeredImage(format=QImage.Format_ARGB32): """ Returns a 20x20 checker @param format: @type format: @return: checker @rtype: QImage """ base = QImage(20, 20, format) qp = QPainter(base) qp.setCompositionMode(QPainter.CompositionMode_Source) qp.fillRect(0, 0, 10, 10, Qt.gray) qp.fillRect(10, 0, 10, 10, Qt.white) qp.fillRect(0, 10, 10, 10, Qt.white) qp.fillRect(10, 10, 10, 10, Qt.gray) qp.end() return base
def drawLineTo(self, endPoint): if not self.penMoved: endPoint.setX( endPoint.x() + 1 ) # ensures a dot is being drawn if there was just a click and no mouse move painter = QPainter(self.image) painter.setCompositionMode(QPainter.CompositionMode_Source) painter.setRenderHint(QPainter.Antialiasing) brush = QBrush(self.myPenColor, Qt.SolidPattern) pen = QPen(brush, self.myPenWidth / self.scaleFactor, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) # pen = QPen(self.myPenColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) painter.setPen(pen) painter.drawLine(self.lastPoint / self.scaleFactor, endPoint / self.scaleFactor) self.update() self.lastPoint = endPoint
def paintEvent(self, event): p = QPainter(self.mask) for (mode, p1, p2, weight) in self.path: if mode == 'add': p.setPen( QPen(QColor.fromRgbF(1.0, 0.0, 1.0), (weight * 10.0)**2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) else: p.setPen( QPen(QColor.fromRgbF(0.0, 0.0, 0.0), (weight * 10.0)**2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) p.drawLine(realCoords(p1, self.mask.rect()), realCoords(p2, self.mask.rect())) self.changed = True self.path = list() p.end() p = QPainter(self) p.setCompositionMode(QPainter.CompositionMode_SourceOver) if not self.maskOnly: p.drawImage(self.rc, self.image) p.setCompositionMode(QPainter.CompositionMode_Plus) p.drawImage(self.rc, self.mask) p.end()
def paintEvent(self, event): painter = QPainter(self) brush = self.palette().window().color() painter.fillRect(self.rect(), brush) for y in range(self._rows): for x in range(self._cols): i = y * self._cols + x c = self._colors[i][1] r = QRect(self._size * x, self._size * y, self._size - 2, self._size - 2) painter.fillRect(r, QBrush(c)) # Draw index painter.setCompositionMode(QPainter.RasterOp_SourceXorDestination) pen = QPen(QBrush(Qt.white), 2., Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin) painter.setPen(pen) row = int(self._index / self._cols) col = int(self._index - row * self._cols) r = QRect(self._size * col, self._size * row, self._size - 2, self._size - 2) painter.drawRect(r.adjusted(1, 1, -1, -1))
def getCurrentMaskedImage(self): """ Reduces the layer stack up to self (included), taking into account the masks. if self.isClipping is True self.mask applies to all lower layers and to self only otherwise. The method uses the non color managed rPixmaps to build the masked image. For convenience, mainly to be able to use its color space buffers, the built image is of type QLayer. It is drawn on a container image, created only once. @return: masked image @rtype: QLayer """ # init containers if needed if self.parentImage.useHald: return self.getHald() if self.maskedThumbContainer is None: self.maskedThumbContainer = QLayer.fromImage( self.getThumb(), parentImage=self.parentImage) if self.maskedImageContainer is None: self.maskedImageContainer = QLayer.fromImage( self, parentImage=self.parentImage) if self.parentImage.useThumb: img = self.maskedThumbContainer else: img = self.maskedImageContainer # no thumbnails for containers img.getThumb = lambda: img # draw lower stack qp = QPainter(img) top = self.parentImage.getStackIndex(self) bottom = 0 for i, layer in enumerate(self.parentImage.layersStack[bottom:top + 1]): if layer.visible: if i == 0: qp.setCompositionMode(QPainter.CompositionMode_Source) else: qp.setOpacity(layer.opacity) qp.setCompositionMode(layer.compositionMode) if layer.rPixmap is not None: qp.drawPixmap(QRect(0, 0, img.width(), img.height()), layer.rPixmap) else: qp.drawImage(QRect(0, 0, img.width(), img.height()), layer.getCurrentImage()) # clipping if layer.isClipping and layer.maskIsEnabled: #TODO modified 23/06/18 # draw mask as opacity mask # mode DestinationIn (set dest opacity to source opacity) qp.setCompositionMode( QPainter.CompositionMode_DestinationIn) omask = vImage.color2OpacityMask(layer.mask) qp.drawImage(QRect(0, 0, img.width(), img.height()), omask) qp.end() return img
def getCurrentMaskedImage(self): """ Blend the layer stack up to self (included), taking into account the masks. The method uses the non color managed rPixmap to build the masked image. For convenience, mainly to be able to use its color space buffers, the built image is of type bImage. It is drawn on a container image, instantiated only once. @return: masked image @rtype: bImage """ # init containers if needed. They are instantiated only # once and updated by drawing. if self.parentImage.useHald: return self.getHald() if self.maskedThumbContainer is None: self.maskedThumbContainer = bImage.fromImage( self.getThumb(), parentImage=self.parentImage) if self.maskedImageContainer is None: self.maskedImageContainer = bImage.fromImage( self, parentImage=self.parentImage) if self.parentImage.useThumb: img = self.maskedThumbContainer else: img = self.maskedImageContainer # draw lower stack qp = QPainter(img) top = self.parentImage.getStackIndex(self) bottom = 0 for i, layer in enumerate(self.parentImage.layersStack[bottom:top + 1]): if layer.visible: if i == 0: qp.setCompositionMode(QPainter.CompositionMode_Source) else: qp.setOpacity(layer.opacity) qp.setCompositionMode(layer.compositionMode) if layer.rPixmap is None: layer.rPixmap = QPixmap.fromImage(layer.getCurrentImage( )) # TODO modified 9/12/18 validate qp.drawPixmap(QRect(0, 0, img.width(), img.height()), layer.rPixmap) # clipping if layer.isClipping and layer.maskIsEnabled: # draw mask as opacity mask # mode DestinationIn (set dest opacity to source opacity) qp.setCompositionMode( QPainter.CompositionMode_DestinationIn) omask = vImage.color2OpacityMask(layer.mask) qp.drawImage(QRect(0, 0, img.width(), img.height()), omask) qp.end() return img
def brushStrokePoly(pixmap, poly, brush): """ Draws the brush stroke defined by a QPolygon @param layer: @type layer: @param x: @type x: @param y: @type y: @param r: @type r: """ # draw the stroke if brush['name'] == 'eraser': return # drawing into stroke intermediate layer pxmp_temp = pixmap.copy() qp = QPainter() qp.begin(pxmp_temp) qp.setCompositionMode(qp.CompositionMode_SourceOver) # draw lines x_last, y_last = poly.first().x(), poly.first().y() for i in range(poly.length() - 1): x0, y0 = poly.at(i).x(), poly.at(i).y() x, y = poly.at(i + 1).x(), poly.at(i + 1).y() x_last, y_last = brushFamily.brushStrokeSeg( qp, x_last, y_last, x, y, brush) qp.end() # draw texture aligned with image strokeTex = pxmp_temp p = brush['pattern'] if p is not None: if p.pxmp is not None: strokeTex = pxmp_temp.copy() qp1 = QPainter(strokeTex) qp1.setCompositionMode(qp.CompositionMode_DestinationIn) qp1.setBrush(QBrush(p.pxmp)) qp1.fillRect( QRect(0, 0, strokeTex.width(), strokeTex.height()), QBrush(p.pxmp)) qp1.end() # restore source image and paint # the whole stroke with current brush opacity qp.begin(pixmap) # qp.setCompositionMode(qp.CompositionMode_Source) # qp.drawImage(QPointF(), layer.strokeDest) qp.setOpacity(brush['opacity']) qp.setCompositionMode(qp.CompositionMode_SourceOver) qp.drawPixmap(QPointF(), strokeTex) # pxmp_temp) qp.end()
def update_scene(self, meta_frame): """ Updates the image_label with a new opencv image. Args: meta_frame: MetaFrame Object """ # Get frame and other data from meta_frame frame = meta_frame.frame peaks = meta_frame.peaks tracks = meta_frame.tracks # Get display_options from main_window display_options = self.main_window.display_options # Get display dimensions self.display_height = self.image_label.height() self.display_width = self.image_label.width() # Adjust the scale factor scale_factor = self.display_height / 256 # Convert frame to qt image pm_img = convert_cv_qt(frame, self.display_width, self.display_height) # Start painter painter = QPainter() painter.begin(pm_img) # Create pen pen = QtGui.QPen() pen.setWidth(2) pen.setColor(QtGui.QColor(204, 0, 0)) # r, g, b painter.setPen(pen) if display_options["show_tracks"] == True: # Plot peaks with centered for point in peaks: painter.drawEllipse((point[0] * scale_factor) - 4, (point[1] * scale_factor) - 4, 8, 8) pen.setWidth(2) pen.setColor(QtGui.QColor(0, 0, 255)) if display_options["show_labels"] == True: # Draw tracked objects labels for label, trace in tracks.items(): if len(trace) > 1: # Assign random color for different tracks. color = get_random_color(int(label)) qcolor = QColor() qcolor.setNamedColor(color) pen.setColor(qcolor) painter.setPen(pen) label = label label_pos_x = trace[-1][0][0] * scale_factor + 10 label_pos_y = trace[-1][0][1] * scale_factor + 10 painter.drawText(label_pos_x, label_pos_y, label) if display_options["show_traces"] == True: # For identified object tracks draw tracking line # Use various colors to indicate different track_id for label, trace in tracks.items(): if len(trace) > 1: # Assign random color for different tracks. color = get_random_color(int(label)) qcolor = QColor() qcolor.setNamedColor(color) pen.setColor(qcolor) painter.setPen(pen) limit = 0 if len(trace) > 200: limit = len(trace) - 200 for j in range(limit, len(trace) - 1): # Draw trace line x1 = trace[j][0][0] * scale_factor y1 = trace[j][0][1] * scale_factor x2 = trace[j + 1][0][0] * scale_factor y2 = trace[j + 1][0][1] * scale_factor painter.drawLine(int(x1), int(y1), int(x2), int(y2)) painter.setCompositionMode(QPainter.CompositionMode_SourceOver) # painter.drawImage(0, 0, pix_map) painter.end() # label = QLabel() self.image_label.setPixmap(QPixmap.fromImage(pm_img))
def paintEvent(self, event): p = QPainter() p.begin(self) self._normalMap.render(p, event.rect()) p.setPen(Qt.black) # p.drawText(self.rect(), Qt.AlignBottom | Qt.TextWordWrap, "Map data CCBYSA 2009 OpenStreetMap.org contributors") p.end() if self.zoomed: dim = min(self.width(), self.height()) magnifierSize = min(MAX_MAGNIFIER, dim * 2 / 3) radius = magnifierSize / 2 ring = radius - 15 box = QSize(magnifierSize, magnifierSize) # reupdate our mask if self.maskPixmap.size() != box: self.maskPixmap = QPixmap(box) self.maskPixmap.fill(Qt.transparent) g = QRadialGradient() g.setCenter(radius, radius) g.setFocalPoint(radius, radius) g.setRadius(radius) g.setColorAt(1.0, QColor(255, 255, 255, 0)) g.setColorAt(0.5, QColor(128, 128, 128, 255)) mask = QPainter(self.maskPixmap) mask.setRenderHint(QPainter.Antialiasing) mask.setCompositionMode(QPainter.CompositionMode_Source) mask.setBrush(g) mask.setPen(Qt.NoPen) mask.drawRect(self.maskPixmap.rect()) mask.setBrush(QColor(Qt.transparent)) mask.drawEllipse(g.center(), ring, ring) mask.end() center = self.dragPos - QPoint(0, radius) center += QPoint(0, radius / 2) corner = center - QPoint(radius, radius) xy = center * 2 - QPoint(radius, radius) # only set the dimension to the magnified portion if self.zoomPixmap.size() != box: self.zoomPixmap = QPixmap(box) self.zoomPixmap.fill(Qt.lightGray) if True: p = QPainter(self.zoomPixmap) p.translate(-xy) self._largeMap.render(p, QRect(xy, box)) p.end() clipPath = QPainterPath() clipPath.addEllipse(QPointF(center), ring, ring) p = QPainter(self) p.setRenderHint(QPainter.Antialiasing) p.setClipPath(clipPath) p.drawPixmap(corner, self.zoomPixmap) p.setClipping(False) p.drawPixmap(corner, self.maskPixmap) p.setPen(Qt.gray) p.drawPath(clipPath) if self.invert: p = QPainter(self) p.setCompositionMode(QPainter.CompositionMode_Difference) p.fillRect(event.rect(), Qt.white) p.end()
def __strokePaint(self, layer, x, y, r): """ Private drawing function. Should be called only by the mouse event handler @param layer: @type layer: @param x: @type x: @param y: @type y: @param r: @type r: """ img = self.img State = self.State qp = self.qp # get image coordinates x_img = (x - img.xOffset) // r y_img = (y - img.yOffset) // r # draw the stroke if self.window.btnValues['brushButton']: # drawing onto stroke intermediate layer qp.begin(layer.stroke) qp.setCompositionMode(qp.CompositionMode_SourceOver) # draw move State['x_imagePrecPos'], State[ 'y_imagePrecPos'] = brushFamily.brushStrokeSeg( qp, State['x_imagePrecPos'], State['y_imagePrecPos'], x_img, y_img, State['brush']) qp.end() # draw texture aligned with image strokeTex = layer.stroke p = State['brush']['pattern'] if p is not None: if p.pxmp is not None: strokeTex = layer.stroke.copy() qp1 = QPainter(strokeTex) qp1.setCompositionMode(qp.CompositionMode_DestinationIn) qp1.setBrush(QBrush(p.pxmp)) qp1.fillRect( QRect(0, 0, strokeTex.width(), strokeTex.height()), QBrush(p.pxmp)) qp1.end() # restore source image and paint # the whole stroke with current brush opacity. # Restoring source image enables iterative calls showing # stroke progress qp.begin(layer.sourceImg) qp.setCompositionMode(qp.CompositionMode_Source) qp.drawImage(QPointF(), layer.strokeDest) qp.setOpacity(State['brush']['opacity']) qp.setCompositionMode(qp.CompositionMode_SourceOver) qp.drawImage(QPointF(), strokeTex) # layer.stroke) qp.end() elif self.window.btnValues['eraserButton']: qp.begin(layer.sourceImg) qp.setCompositionMode(qp.CompositionMode_DestinationIn) State['x_imagePrecPos'], State[ 'y_imagePrecPos'] = brushFamily.brushStrokeSeg( qp, State['x_imagePrecPos'], State['y_imagePrecPos'], x_img, y_img, State['brush']) qp.end() # update layer - should be layer.applyToStack() if any upper layer visible : too slow ! layer.execute() img.prLayer.update() self.window.label.repaint()
def getBrush(self, size, opacity, color, hardness, flow, spacing=1.0, jitter=0.0, orientation=0, pattern=None): """ initializes and returns a brush as a dictionary @param size: brush size @type size: int @param opacity: brush opacity, range 0..1 @type opacity: float @param color: @type color: QColor @param hardness: brush hardness, range 0..1 @type hardness: float @param flow: brush flow, range 0..1 @type flow: float @return: @rtype: dict """ s = float(self.baseSize) / 2 # set brush color if self.name == 'eraser': color = QColor(0, 0, 0, 0) else: op_max = 255 # 64 color = QColor(color.red(), color.green(), color.blue(), int(op_max * flow)) gradient = QRadialGradient(QPointF(s, s), s) gradient.setColorAt(0, color) gradient.setColorAt(hardness, color) if hardness < 1.0: # fade action to 0, starting from hardness to 1 if self.name == 'eraser': gradient.setColorAt(1, QColor(0, 0, 0, 255)) else: gradient.setColorAt(1, QColor(0, 0, 0, 0)) pxmp = self.basePixmap.copy() qp = QPainter(pxmp) # fill brush contour with gradient (pxmp color is (0,0,0,0) # outside of contourPath) qp.setCompositionMode(qp.CompositionMode_Source) qp.fillPath(self.contourPath, QBrush(gradient)) if self.preset is not None: ################################################ # we adjust the preset pixmap to pxmp size while keeping # its aspect ratio and we center it into pxmp ################################################ w, h = self.preset.width(), self.preset.height() # get the bounding rect of the scaled and centered preset # and the 2 complementary rects if w > h: rh = int(self.baseSize * h / w) # height of bounding rect m = int((self.baseSize - rh) / 2.0) # top and bottom margins r = QRect(0, m, self.baseSize, rh) r1 = QRect(0, 0, self.baseSize, m) r2 = QRect(0, rh + m, self.baseSize, m) else: rw = int(self.baseSize * w / h) # width of bounding rect m = int((self.baseSize - rw) / 2.0) # left and right margins r = QRect(m, 0, rw, self.baseSize) r1 = QRect(0, 0, m, self.baseSize) r2 = QRect(rw + m, 0, m, self.baseSize) # set opacity of r to that of preset qp.setCompositionMode(QPainter.CompositionMode_DestinationIn) qp.drawPixmap(r, self.preset) # paint the outside of r with transparent color pxmp1 = QPixmap(pxmp.size()) pxmp1.fill(QColor(0, 0, 0, 0)) qp.drawPixmap(r1, pxmp1) qp.drawPixmap(r2, pxmp1) qp.end() s = size / self.baseSize self.pxmp = pxmp.transformed(QTransform().scale( s, s).rotate(orientation)) # pxmp.scaled(size, size) pattern = pattern return { 'family': self, 'name': self.name, 'pixmap': self.pxmp, 'size': size, 'color': color, 'opacity': opacity, 'hardness': hardness, 'flow': flow, 'spacing': spacing, 'jitter': jitter, 'orientation': orientation, 'pattern': pattern, 'cursor': self.baseCursor }
def histogram(self, size=QSize(200, 200), bgColor=Qt.white, range=(0, 255), chans=channelValues.RGB, chanColors=Qt.gray, mode='RGB', addMode=''): """ Plot the image histogram with the specified color mode and channels. Histograms are smoothed using a Savisky-Golay filter and curves are scaled individually to fit the height of the plot. @param size: size of the histogram plot @type size: int or QSize @param bgColor: background color @type bgColor: QColor @param range: plot data range @type range: 2-uple of int or float @param chans: channels to plot b=0, G=1, R=2 @type chans: list of indices @param chanColors: color or 3-uple of colors @type chanColors: QColor or 3-uple of QColor @param mode: color mode ((one among 'RGB', 'HSpB', 'Lab', 'Luminosity') @type mode: str @param addMode: @type addMode: @return: histogram plot @rtype: QImage """ # convert size to QSize if type(size) is int: size = QSize(size, size) # alert threshold for clipped areas clipping_threshold = 0.02 # clipping threshold for black and white points # scaling factor for the bin edges spread = float(range[1] - range[0]) scale = size.width() / spread # per channel histogram function def drawChannelHistogram(painter, hist, bin_edges, color): # Draw the (smoothed) histogram for a single channel. # param painter: QPainter # param hist: histogram to draw # smooth the histogram (first and last bins excepted) for a better visualization of clipping. hist = np.concatenate(([hist[0]], SavitzkyGolayFilter.filter(hist[1:-1]), [hist[-1]])) M = max(hist[1:-1]) # draw histogram imgH = size.height() for i, y in enumerate(hist): try: h = int(imgH * y / M) except (ValueError, ArithmeticError): # don't draw the channel histogram if M is too small: # It may happen when channel values are concentrated # on the first and/or last bins. return h = min(h, imgH - 1) # height of rect must be < height of img, otherwise fillRect does nothing rect = QRect(int((bin_edges[i] - range[0]) * scale), max(img.height() - h, 0), int((bin_edges[i + 1] - bin_edges[i]) * scale+1), h) painter.fillRect(rect, color) # clipping indicators if i == 0 or i == len(hist)-1: left = bin_edges[0 if i == 0 else -1] if range[0] < left < range[1]: continue left = left - (10 if i > 0 else 0) percent = hist[i] * (bin_edges[i+1]-bin_edges[i]) if percent > clipping_threshold: # calculate the color of the indicator according to percent value nonlocal gPercent gPercent = min(gPercent, np.clip((0.05 - percent) / 0.03, 0, 1)) painter.fillRect(left, 0, 10, 10, QColor(255, 255*gPercent, 0)) # green percent for clipping indicators gPercent = 1.0 bufL = cv2.cvtColor(QImageBuffer(self)[:, :, :3], cv2.COLOR_BGR2GRAY)[..., np.newaxis] # returns Y (YCrCb) : Y = 0.299*R + 0.587*G + 0.114*B buf = None # TODO added 5/11/18 validate if mode == 'RGB': buf = QImageBuffer(self)[:, :, :3][:, :, ::-1] # RGB elif mode == 'HSV': buf = self.getHSVBuffer() elif mode == 'HSpB': buf = self.getHspbBuffer() elif mode == 'Lab': buf = self.getLabBuffer() elif mode == 'Luminosity': chans = [] img = QImage(size.width(), size.height(), QImage.Format_ARGB32) img.fill(bgColor) qp = QPainter(img) try: if type(chanColors) is QColor or type(chanColors) is Qt.GlobalColor: chanColors = [chanColors]*3 # compute histograms # bins='auto' sometimes causes a huge number of bins ( >= 10**9) and memory error # even for small data size (<=250000), so we don't use it. # This is a numpy bug : in the module function_base.py # a reasonable upper bound for bins should be chosen to prevent memory error. if mode == 'Luminosity' or addMode == 'Luminosity': hist, bin_edges = np.histogram(bufL, bins=100, density=True) drawChannelHistogram(qp, hist, bin_edges, Qt.gray) hist_L, bin_edges_L = [0]*len(chans), [0]*len(chans) for i, ch in enumerate(chans): buf0 = buf[:, :, ch] hist_L[i], bin_edges_L[i] = np.histogram(buf0, bins=100, density=True) # to prevent artifacts, the histogram bins must be drawn # using the composition mode source_over. So, we use # a fresh QImage for each channel. tmpimg = QImage(size, QImage.Format_ARGB32) tmpimg.fill(bgColor) tmpqp = QPainter(tmpimg) try: drawChannelHistogram(tmpqp, hist_L[i], bin_edges_L[i], chanColors[ch]) finally: tmpqp.end() # add the channnel hist to img qp.drawImage(QPoint(0,0), tmpimg) # subsequent images are added using composition mode Plus qp.setCompositionMode(QPainter.CompositionMode_Plus) finally: qp.end() buf = QImageBuffer(img) # if len(chans) > 1, clip gray area to improve the aspect of the histogram if len(chans) > 1: buf[:, :, :3] = np.where(np.min(buf, axis=-1)[:, :, np.newaxis] >= 100, np.array((100, 100, 100))[np.newaxis, np.newaxis, :], buf[:, :, :3]) return img
class ImageSegmenterView(QGraphicsView): photoClicked = Signal(QPoint) def __init__(self, parent): super(ImageSegmenterView, self).__init__(parent) self._zoom = 0 self.empty = True self._scene = QGraphicsScene(self) self._photo = QGraphicsPixmapItem() self.image_hidden = False self._seglayer = QGraphicsPixmapItem() self._seglayer.setOpacity(0.5) self._scene.addItem(self._photo) self._scene.addItem(self._seglayer) self.setScene(self._scene) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) # self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) # self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setBackgroundBrush(QBrush(QtCore.Qt.darkGray)) self.setFrameShape(QFrame.NoFrame) self.seg_image = None self.start = False self.prev_point = None self.painter = None self.segmenter_pen = QPen(QtCore.Qt.green, 30, QtCore.Qt.SolidLine) self.segmenter_pen.setCapStyle(QtCore.Qt.RoundCap) self.segmenter_pen.setJoinStyle(QtCore.Qt.RoundJoin) self.erase = False self.changed = False self.history = collections.deque(maxlen=10) self.future = collections.deque(maxlen=10) def hasPhoto(self): return not self.empty def fitInView(self, scale=True): rect = QtCore.QRectF(self._photo.pixmap().rect()) if not rect.isNull(): self.setSceneRect(rect) if self.hasPhoto(): unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1)) self.scale(1 / unity.width(), 1 / unity.height()) viewrect = self.viewport().rect() scenerect = self.transform().mapRect(rect) factor = min(viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height()) self.scale(factor, factor) self._zoom = 0 def setPhoto(self, pixmap=None): self._zoom = 0 if pixmap and not pixmap.isNull(): self.empty = False self.changed = False self._photo.setPixmap(pixmap) self.seg_image = QImage(pixmap.width(), pixmap.height(), QImage.Format_ARGB32_Premultiplied) self.seg_image.fill(QtCore.Qt.transparent) self._seglayer.setPixmap(QPixmap.fromImage(self.seg_image)) else: self._empty = True self._photo.setPixmap(QPixmap()) self.fitInView() def save_state(self): if self.future is not None: while len(self.future) > 0: present = self.future.pop() del present if self.seg_image is not None: self.history.append(self.seg_image.copy()) def clear_history(self): while len(self.history) > 0: present = self.history.pop() del present def undo(self): if len(self.history) > 0: self.future.append(self.seg_image) present = self.history.pop() self.seg_image = present self._seglayer.setPixmap(QPixmap.fromImage(self.seg_image)) def redo(self): if len(self.future) > 0: self.history.append(self.seg_image) present = self.future.pop() self.seg_image = present self._seglayer.setPixmap(QPixmap.fromImage(self.seg_image)) def setSegLayer(self, pixmap=None): if not self._photo.pixmap().isNull(): self.save_state() self.seg_image = QImage(pixmap.toImage()) self._seglayer.setPixmap(QPixmap.fromImage(self.seg_image)) def resetSegLayer(self): if not self._photo.pixmap().isNull(): self.changed = True self.save_state() del self.seg_image self.seg_image = QImage(self._photo.pixmap().width(), self._photo.pixmap().height(), QImage.Format_ARGB32_Premultiplied) self.seg_image.fill(QtCore.Qt.transparent) self._seglayer.setPixmap(QPixmap.fromImage(self.seg_image)) def wheelEvent(self, event): if self.hasPhoto() and not self.start: if event.angleDelta().y() > 0: factor = 1.25 self._zoom += 1 else: factor = 0.8 self._zoom -= 1 if self._zoom > 0: self.scale(factor, factor) elif self._zoom == 0: self.fitInView() else: self._zoom = 0 def mousePressEvent(self, event): if not self._photo.pixmap().isNull(): if event.button() == QtCore.Qt.LeftButton: self.save_state() self.start = True self.painter = QPainter(self.seg_image) if self.erase: self.painter.setCompositionMode( QPainter.CompositionMode_Clear) self.painter.setPen(self.segmenter_pen) self.paint_point(event.pos()) elif event.button() == QtCore.Qt.RightButton: if not self._photo.pixmap().isNull(): self.setDragMode(QGraphicsView.ScrollHandDrag) self.scroll_origin = self.mapToScene(event.pos()) # if self._photo.isUnderMouse(): # self.photoClicked.emit(self.mapToScene(event.pos()).toPoint()) super(ImageSegmenterView, self).mousePressEvent(event) def mouseMoveEvent(self, event): if not self._photo.pixmap().isNull(): if self.start: self.paint_point(event.pos()) if event.buttons() & QtCore.Qt.RightButton: newpoint = self.mapToScene(event.pos()) translation = newpoint - self.scroll_origin self.translate(translation.x(), translation.y()) self.scroll_origin = self.mapToScene(event.pos()) super(ImageSegmenterView, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): if not self._photo.pixmap().isNull(): if self.start: self.start = False self.prev_point = None self.painter.end() if self.dragMode() == QGraphicsView.ScrollHandDrag: self.setDragMode(QGraphicsView.NoDrag) def paint_point(self, pos): self.changed = True pos = self.mapToScene(pos).toPoint() if self.prev_point is not None: self.painter.drawLine(self.prev_point, pos) else: self.painter.drawPoint(pos) self.prev_point = pos self._seglayer.setPixmap(QPixmap.fromImage(self.seg_image)) def set_foreground(self): self.erase = False self.segmenter_pen.setColor(QtCore.Qt.green) def set_possible_foreground(self): self.erase = False self.segmenter_pen.setColor(QtCore.Qt.blue) def set_possible_background(self): self.erase = True self.segmenter_pen.setColor(QtCore.Qt.transparent) def set_background(self): self.erase = False self.segmenter_pen.setColor(QtCore.Qt.red) def set_pen_size(self, size): self.segmenter_pen.setWidth(size) def set_opacity(self, value): self._seglayer.setOpacity(value / 100) def hide_image(self): if self.image_hidden: self._photo.setOpacity(1) self.image_hidden = False else: self._photo.setOpacity(0) self.image_hidden = True