def to_qimage(im, color_table=grey, copy=True): if color_table is not None: im = to_gray(im) if len(im.shape) == 3: if im.shape[2] == 3: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_RGB888) elif im.shape[2] == 4: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_ARGB32) elif im.shape[2] == 1: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_Indexed8) qim.setColorTable(color_table) elif len(im.shape) == 2: if im.dtype == numpy.uint8: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_Indexed8) if color_table: qim.setColorTable(color_table) elif im.dtype == numpy.uint16: im = (im/16) im = numpy.require(im, dtype=numpy.uint8, requirements='C') qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_Indexed8) if color_table: qim.setColorTable(color_table) else: raise TypeError("Unsupported data type") else: raise TypeError("Wrong image shape {}".format(im.shape)) return qim.copy() if copy else qim
def capture( self, region=None, selector=None, format=None, ): """Returns snapshot as QImage. :param region: An optional tuple containing region as pixel coodinates. :param selector: A selector targeted the element to crop on. :param format: The output image format. """ if format is None: format = QImage.Format_ARGB32_Premultiplied self.main_frame.setScrollBarPolicy( Qt.Vertical, Qt.ScrollBarAlwaysOff, ) self.main_frame.setScrollBarPolicy( Qt.Horizontal, Qt.ScrollBarAlwaysOff, ) frame_size = self.main_frame.contentsSize() max_size = 23170 * 23170 if frame_size.height() * frame_size.width() > max_size: self.logger.warning("Frame size is too large.") default_size = self.page.viewportSize() if default_size.height() * default_size.width() > max_size: return None else: self.page.setViewportSize(self.main_frame.contentsSize()) self.logger.info("Frame size -> %s", str(self.page.viewportSize())) image = QImage(self.page.viewportSize(), format) painter = QPainter(image) if region is None and selector is not None: region = self.region_for_selector(selector) if region: x1, y1, x2, y2 = region w, h = (x2 - x1), (y2 - y1) reg = QRegion(x1, y1, w, h) self.main_frame.render(painter, reg) else: self.main_frame.render(painter) painter.end() if region: x1, y1, x2, y2 = region w, h = (x2 - x1), (y2 - y1) image = image.copy(x1, y1, w, h) return image
def to_QImage(im, copy=False): if im is None: return QImage() if im.dtype == np.uint8: if len(im.shape) == 2: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_Indexed8) qim.setColorTable(gray_color_table) return qim.copy() if copy else qim elif len(im.shape) == 3: if im.shape[2] == 3: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_RGB888); return qim.copy() if copy else qim elif im.shape[2] == 4: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_ARGB32); return qim.copy() if copy else qim raise NotImplementedException
def toQImage(im, copy=False): gray_color_table = [qRgb(i, i, i) for i in range(256)] if im is None: return QImage() if im.dtype == np.uint8: if len(im.shape) == 2: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_Indexed8) qim.setColorTable(gray_color_table) return qim.copy() if copy else qim elif len(im.shape) == 3: if im.shape[2] == 3: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_RGB888) return qim.copy() if copy else qim elif im.shape[2] == 4: qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_ARGB32) return qim.copy() if copy else qim
def __init__(self, title: str, cover: qtg.QImage): super().__init__() cover = cover.scaledToHeight(300, qtc.Qt.TransformationMode.SmoothTransformation) cover = cover.copy(self._rect) icon = qtg.QIcon(qtg.QPixmap.fromImage(cover)) self.setToolButtonStyle(qtc.Qt.ToolButtonIconOnly) self.setIcon(icon) self.setIconSize(qtc.QSize(300, 300)) self.setToolTip(title) self.setToolTipDuration(0)
def __init__(self, object_set: int, palette_index: int): png = QImage(str(data_dir.joinpath("gfx.png"))) png.convertTo(QImage.Format_RGB888) rows_per_object_set = 256 // 64 y_offset = 12 * rows_per_object_set * Block.HEIGHT self.png_data = png.copy(QRect(0, y_offset, png.width(), png.height() - y_offset)) self.palette_group = load_palette_group(object_set, palette_index)
def _draw_mario(self, painter: QPainter, level: Level): mario_actions = QImage(str(data_dir / "mario.png")) mario_actions.convertTo(QImage.Format_RGBA8888) mario_position = QPoint(*level.header.mario_position()) * self.block_length x_offset = 32 * level.start_action mario_cutout = mario_actions.copy(QRect(x_offset, 0, 32, 32)).scaled( 2 * self.block_length, 2 * self.block_length ) painter.drawImage(mario_position, mario_cutout)
def _writeFrame(self, surface: QImage): w = self.stream.width h = self.stream.height surface = surface.scaled(w, h) if self.scale else surface.copy() # Draw the mouse pointer. Render mouse clicks? p = QPainter(surface) p.setBrush(QColor.fromRgb(255, 255, 0, 180)) (x, y) = self.mouse p.drawEllipse(x, y, 5, 5) p.end() # Output frame. frame = av.VideoFrame.from_image(ImageQt.fromqimage(surface)) for packet in self.stream.encode(frame): if self.progress: self.progress() self.mp4.mux(packet)
class QMoon(QWidget): """Small widget displays today's moon.""" def __init__(self, pos=(0, 0), parent=None, size=216, web=False, save=False, debug=0): super(QMoon, self).__init__(parent) self.total_images = 8760 self.moon_domain = "https://svs.gsfc.nasa.gov" self.moon_path_2021 = "/vis/a000000/a004800/a004874/" # https://svs.gsfc.nasa.gov/vis/a000000/a004900/a004955/frames/216x216_1x1_30p/moon.8597.jpg self.moon_path = "/vis/a000000/a004900/a004955/" self.debug = debug self.size = size self.get_from_web = web self.save = save # self.setGeometry(pos[0], pos[1], self.size, self.size) self.moon = QLabel(self) self.moon.setGeometry(pos[0], pos[1], self.size, self.size) self.update() self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(3600 * 1000) self.image = None self.pixmap = None self.moon_image_number = 1 @Slot() def update(self): if self.debug > 0: print("Updating the Moon Phase pixmap.") self.pixmap = self.get_moon_image() self.moon.setPixmap(self.pixmap) def get_moon_image_number(self): # # Conversion from the jscript. # now = datetime.utcnow() janone = datetime(now.year, 1, 1, 0, 0, 0) self.moon_image_number = round( (datetime.utcnow() - janone).total_seconds() / 3600) return self.moon_image_number <= self.total_images def get_moon_image(self): if not self.get_moon_image_number(): print("Could not get the moon. Are we in a new year?") if self.debug: print(f"We are using moon image number: {self.moon_image_number}") if self.size > 500 or self.get_from_web: if self.size > 2160: url = self.moon_domain+self.moon_path+"/frames/5760x3240_16x9_30p/" \ f"plain/moon.{self.moon_image_number:04d}.tif" elif self.size > 216: url = self.moon_domain+self.moon_path+"/frames/3840x2160_16x9_30p/" \ f"plain/moon.{self.moon_image_number:04d}.tif" else: url = self.moon_domain + self.moon_path + "/frames/216x216_1x1_30p/" \ f"moon.{self.moon_image_number:04d}.jpg" if self.debug: print(f"Getting image from url: {url}") req = requests.get(url) self.image = QImage() self.image.loadFromData(req.content, "tiff") size = self.image.size() if self.debug: print("Image size: ", size) if self.save: self.image.save(f"moon/moon.{self.moon_image_number:04d}.tiff") offset = (size.width() - size.height()) / 2 rect = QRect(offset, 0, size.height(), size.height()) self.image = self.image.copy(rect) pix = QPixmap.fromImage(self.image) pix = pix.scaled(self.size, self.size, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) return pix else: moon_file = f"moon/moon.{self.moon_image_number:04d}.jpg" pix = QPixmap(moon_file) pix = pix.scaled(self.size, self.size, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) return pix
class ScribbleArea(QWidget): def __init__(self, parent=None): super(ScribbleArea, self).__init__(parent) self.myPenColors = [ QColor("green"), QColor("purple"), QColor("red"), QColor("blue"), QColor("yellow"), QColor("pink"), QColor("orange"), QColor("brown"), QColor("grey"), QColor("black") ] self.setAttribute(Qt.WA_AcceptTouchEvents) self.setAttribute(Qt.WA_StaticContents) self.image = QImage() self.modified = False def s_print(self): pass def clearImage(self): self.image.fill(qRgb(255, 255, 255)) self.modified = True self.update() def openImage(self, fileName): loadedImage = QImage() if not loadedImage.load(fileName): return False newSize = loadedImage.size().expandedTo(self.size()) self.resizeImage(loadedImage, newSize) self.image = loadedImage self.modified = False self.update() return True def saveImage(self, fileName, fileFormat): visibleImage = self.image.copy() self.resizeImage(visibleImage, self.size()) if visibleImage.save(fileName, fileFormat): self.modified = False return True else: return False def paintEvent(self, event): painter = QPainter(self) rect = event.rect() painter.drawImage(rect.topLeft(), self.image, rect) def resizeEvent(self, event): if self.width() > self.image.width() or self.height( ) > self.image.height(): newWidth = max(self.width() + 128, self.image.width()) newHeight = max(self.height() + 128, self.image.height()) self.resizeImage(self.image, QSize(newWidth, newHeight)) self.update() super(ScribbleArea, self).resizeEvent(event) def resizeImage(self, image, newSize): if self.image.size() == newSize: return newImage = QImage(newSize, QImage.Format_RGB32) newImage.fill(qRgb(255, 255, 255)) painter = QPainter(newImage) painter.drawImage(QPoint(0, 0), self.image) self.image = newImage # # def print(self, ) # { # #ifndef QT_NO_PRINTER: # QPrinter printer(QPrinter.HighResolution) # # QPrintDialog *printDialog = new QPrintDialog(&printer, this) # if (printDialog.exec() == QDialog.Accepted) { # QPainter painter(&printer) # QRect rect = painter.viewport() # QSize size = self.image.size() # size.scale(rect.size(), Qt.KeepAspectRatio) # painter.setViewport(rect.x(), rect.y(), size.width(), size.height()) # painter.setWindow(self.image.rect()) # painter.drawImage(0, 0, image) # } # #endif // QT_NO_PRINTER # } # def event(self, event): if event.type() == QEvent.TouchBegin or event.type( ) == QEvent.TouchUpdate or event.type() == QEvent.TouchEnd: touchPoints = event.touchPoints() for touchPoint in touchPoints: if touchPoint.state() == Qt.TouchPointStationary: continue else: rect = touchPoint.rect() if rect.isEmpty(): diameter = 50 * touchPoint.pressure() rect.setSize(QSizeF(diameter, diameter)) painter = QPainter(self.image) painter.setPen(Qt.NoPen) painter.setBrush(self.myPenColors[touchPoint.id() % len(self.myPenColors)]) painter.drawEllipse(rect) painter.end() self.modified = True rad = 2 self.update(rect.toRect().adjusted(-rad, -rad, +rad, +rad)) else: return super(ScribbleArea, self).event(event) return True
def run(self): # next() raises a StopIteration exception when the generator ends. # If this exception is unhandled by run(), it causes thread termination. # If wdg internal C++ object was destroyed by main thread (form closing) # a RuntimeError exception is raised and causes thread termination too. # Thus, no further synchronization is needed. import exiftool with exiftool.ExifTool() as e: while True: try: filename = next(self.fileListGen) # get orientation try: # read metadata from sidecar (.mie) if it exists, otherwise from image file. profile, metadata = e.get_metadata( filename, tags=("colorspace", "profileDescription", "orientation", "model", "rating", "FileCreateDate"), createsidecar=False) except ValueError: metadata = {} # get image info tmp = [ value for key, value in metadata.items() if 'orientation' in key.lower() ] orientation = tmp[ 0] if tmp else 1 # metadata.get("EXIF:Orientation", 1) # EXIF:DateTimeOriginal seems to be missing in many files tmp = [ value for key, value in metadata.items() if 'date' in key.lower() ] date = tmp[ 0] if tmp else '' # metadata.get("EXIF:ModifyDate", '') tmp = [ value for key, value in metadata.items() if 'rating' in key.lower() ] rating = tmp[ 0] if tmp else 0 # metadata.get("XMP:Rating", 5) rating = ''.join(['*'] * int(rating)) transformation = exiftool.decodeExifOrientation( orientation) # get thumbnail img = e.get_thumbNail(filename, thumbname='thumbnailimage') # no thumbnail found : try preview if img.isNull(): img = e.get_thumbNail( filename, thumbname='PreviewImage' ) # the order is important : for jpeg PreviewImage is full sized ! # all failed : open image if img.isNull(): img = QImage(filename) # remove possible black borders, except for .NEF if filename[-3:] not in ['nef', 'NEF']: bBorder = 7 img = img.copy( QRect(0, bBorder, img.width(), img.height() - 2 * bBorder)) pxm = QPixmap.fromImage(img) if not transformation.isIdentity(): pxm = pxm.transformed(transformation) # set item caption and tooltip item = QListWidgetItem( QIcon(pxm), basename(filename)) # + '\n' + rating) item.setToolTip( basename(filename) + ' ' + date + ' ' + rating) # set item mimeData to get filename=item.data(Qt.UserRole)[0] transformation=item.data(Qt.UserRole)[1] item.setData(Qt.UserRole, (filename, transformation)) self.wdg.addItem(item) # for clean exiting we catch all exceptions and force break except OSError: continue except: break
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
class Block: SIDE_LENGTH = 2 * Tile.SIDE_LENGTH WIDTH = SIDE_LENGTH HEIGHT = SIDE_LENGTH PIXEL_COUNT = WIDTH * HEIGHT tsa_data = bytes() _block_cache = {} def __init__( self, block_index: int, palette_group: List[List[int]], pattern_table: PatternTable, tsa_data: bytes, mirrored=False, ): self.index = block_index palette_index = (block_index & 0b1100_0000) >> 6 self.bg_color = QColor(*NESPalette[palette_group[palette_index][0]]) self._block_id = (block_index, self.bg_color.toTuple(), pattern_table.graphics_set) lu = tsa_data[TSA_BANK_0 + block_index] ld = tsa_data[TSA_BANK_1 + block_index] ru = tsa_data[TSA_BANK_2 + block_index] rd = tsa_data[TSA_BANK_3 + block_index] self.lu_tile = Tile(lu, palette_group, palette_index, pattern_table) self.ld_tile = Tile(ld, palette_group, palette_index, pattern_table) if mirrored: self.ru_tile = Tile(lu, palette_group, palette_index, pattern_table, mirrored=True) self.rd_tile = Tile(ld, palette_group, palette_index, pattern_table, mirrored=True) else: self.ru_tile = Tile(ru, palette_group, palette_index, pattern_table) self.rd_tile = Tile(rd, palette_group, palette_index, pattern_table) self.image = QImage(Block.WIDTH, Block.HEIGHT, QImage.Format_RGB888) painter = QPainter(self.image) painter.drawImage(QPoint(0, 0), self.lu_tile.as_image()) painter.drawImage(QPoint(Tile.WIDTH, 0), self.ru_tile.as_image()) painter.drawImage(QPoint(0, Tile.HEIGHT), self.ld_tile.as_image()) painter.drawImage(QPoint(Tile.WIDTH, Tile.HEIGHT), self.rd_tile.as_image()) painter.end() if _image_only_one_color(self.image) and self.image.pixelColor( 0, 0) == QColor(*MASK_COLOR): self._whole_block_is_transparent = True else: self._whole_block_is_transparent = False def draw(self, painter: QPainter, x, y, block_length, selected=False, transparent=False): block_attributes = (self._block_id, block_length, selected, transparent) if block_attributes not in Block._block_cache: image = self.image.copy() if block_length != Block.WIDTH: image = image.scaled(block_length, block_length) # mask out the transparent pixels first mask = image.createMaskFromColor( QColor(*MASK_COLOR).rgb(), Qt.MaskOutColor) image.setAlphaChannel(mask) if not transparent: # or self._whole_block_is_transparent: image = self._replace_transparent_with_background(image) if selected: apply_selection_overlay(image, mask) Block._block_cache[block_attributes] = image painter.drawImage(x, y, Block._block_cache[block_attributes]) def _replace_transparent_with_background(self, image): # draw image on background layer, to fill transparent pixels background = image.copy() background.fill(self.bg_color) _painter = QPainter(background) _painter.drawImage(QPoint(), image) _painter.end() return background
class QLayer(vImage): """ Base class for image layers """ @classmethod def fromImage(cls, mImg, parentImage=None): """ Returns a QLayer object initialized with mImg. @param mImg: @type mImg: QImage @param parentImage: @type parentImage: mImage @return: @rtype: Qlayer """ layer = QLayer(QImg=mImg, parentImage=parentImage) layer.parentImage = parentImage return layer def __init__(self, *args, **kwargs): ############################################################ # Signals # Making QLayer inherit from QObject leads to # a bugged behavior of hasattr and getattr. # So, we don't add signals as first level class attributes. # Instead, we use instances of ad hoc signal containers. ############################################################ self.visibilityChanged = baseSignal_bool() self.colorPicked = baseSignal_Int2() self.selectionChanged = baseSignal_No() self.maskSettingsChanged = baseSignal_No() ########################################################### # when a geometric transformation is applied to the whole image # each layer must be replaced with a transformed layer, recorded in tLayer # and tLayer.parentLayer keeps a reference to the original layer. ########################################################### self.tLayer = self self.parentLayer = self self.modified = False self.name = 'noname' self.visible = True self.isClipping = False self.role = kwargs.pop('role', '') self.tool = None # back link to parent image parentImage = kwargs.pop('parentImage', None) self.parentImage = weakProxy(parentImage) super().__init__(*args, **kwargs) # don't move backwards # mask init, must be done after after calling super().__init__ if type(self) not in [QPresentationLayer]: self.mask = QImage(self.width(), self.height(), QImage.Format_ARGB32) # default : unmask all self.mask.fill(self.defaultColor_UnMasked) # layer opacity, range 0.0...1.0 self.opacity = 1.0 self.compositionMode = QPainter.CompositionMode_SourceOver # The next two attributes are used by adjustment layers only. self.execute = lambda l=None, pool=None: l.updatePixmap( ) if l is not None else None self.options = {} # actionName is used by methods graphics***.writeToStream() self.actionName = 'actionNull' # view is the dock widget containing # the graphics form associated with the layer self.view = None # undo/redo mask history self.historyListMask = historyList(size=5) # consecutive layers can be grouped. # A group is a list of QLayer objects self.group = [] # layer offsets self.xOffset, self.yOffset = 0, 0 self.Zoom_coeff = 1.0 # clone dup layer shift and zoom relative to current layer self.xAltOffset, self.yAltOffset = 0, 0 self.AltZoom_coeff = 1.0 self.updatePixmap() def getGraphicsForm(self): """ Return the graphics form associated with the layer @return: @rtype: QWidget """ if self.view is not None: return self.view.widget() return None def isActiveLayer(self): if self.parentImage.getActiveLayer() is self: return True return False def getMmcSpline(self): """ Returns the spline used for multimode contrast correction if it is initialized, and None otherwise. @return: @rtype: activeSpline """ # get layer graphic form grf = self.getGraphicsForm() # manual curve form if grf.contrastForm is not None: return grf.contrastForm.scene().cubicItem return None def addTool(self, tool): """ Adds tool to layer @param tool: @type tool: rotatingTool """ self.tool = tool tool.modified = False tool.layer = self try: tool.layer.visibilityChanged.sig.disconnect( ) # TODO signals removed 30/09/18 validate except RuntimeError: pass tool.layer.visibilityChanged.sig.connect( tool.setVisible) # TODO signals removed 30/09/18 validate tool.img = self.parentImage w, h = tool.img.width(), tool.img.height() for role, pos in zip( ['topLeft', 'topRight', 'bottomRight', 'bottomLeft'], [QPoint(0, 0), QPoint(w, 0), QPoint(w, h), QPoint(0, h)]): tool.btnDict[role].posRelImg = pos tool.btnDict[role].posRelImg_ori = pos tool.btnDict[role].posRelImg_frozen = pos tool.moveRotatingTool() def setVisible(self, value): """ Sets self.visible to value and emit visibilityChanged.sig @param value: @type value: bool """ self.visible = value self.visibilityChanged.sig.emit( value) # TODO signals removed 30/09/18 validate def bTransformed(self, transformation, parentImage): """ Apply transformation to a copy of layer. Returns the transformed copy. @param transformation: @type transformation: QTransform @param parentImage: @type parentImage: vImage @return: transformed layer @rtype: QLayer """ # init a new layer from transformed image : # all static attributes (caches...) are reset to default, but thumb tLayer = QLayer.fromImage(self.transformed(transformation), parentImage=parentImage) # copy dynamic attributes from old layer for a in self.__dict__.keys(): if a not in tLayer.__dict__.keys(): tLayer.__dict__[a] = self.__dict__[a] tLayer.name = self.name tLayer.actionName = self.actionName tLayer.view = self.view tLayer.visible = self.visible # TODO added 25/09/18 validate # cut link from old layer to graphic form # self.view = None # TODO 04/12/17 validate tLayer.execute = self.execute tLayer.mask = self.mask.transformed(transformation) tLayer.maskIsEnabled, tLayer.maskIsSelected = self.maskIsEnabled, self.maskIsSelected return tLayer def initThumb(self): """ Override vImage.initThumb, to set the parentImage attribute """ super().initThumb() self.thumb.parentImage = self.parentImage def initHald(self): """ Build a hald image (as a QImage) from identity 3D LUT. """ if not self.cachesEnabled: return s = int(LUT3DIdentity.size**(3.0 / 2.0)) + 1 buf0 = LUT3DIdentity.toHaldArray(s, s).haldBuffer # self.hald = QLayer(QImg=QImage(QSize(190,190), QImage.Format_ARGB32)) self.hald = QImage(QSize(s, s), QImage.Format_ARGB32) buf1 = QImageBuffer(self.hald) buf1[:, :, :3] = buf0 buf1[:, :, 3] = 255 self.hald.parentImage = self.parentImage def getHald(self): if not self.cachesEnabled: s = int(LUT3DIdentity.size**(3.0 / 2.0)) + 1 buf0 = LUT3DIdentity.toHaldArray(s, s).haldBuffer # self.hald = QLayer(QImg=QImage(QSize(190,190), QImage.Format_ARGB32)) hald = QImage(QSize(s, s), QImage.Format_ARGB32) buf1 = QImageBuffer(hald) buf1[:, :, :3] = buf0 buf1[:, :, 3] = 255 hald.parentImage = self.parentImage return hald if self.hald is None: self.initHald() return self.hald def getCurrentImage(self): """ Returns current (full, preview or hald) image, according to the value of the flags useThumb and useHald. The thumbnail and hald are computed if they are not initialized. Otherwise, they are not updated unless self.thumb is None or purgeThumb is True. Overrides vImage method @return: current image @rtype: QLayer """ if self.parentImage.useHald: return self.getHald() if self.parentImage.useThumb: return self.getThumb() else: return self def inputImg(self, redo=True): """ return maskedImageContainer/maskedThumbContainer. If redo is True(default), containers are updated. layer.applyToStack() always calls inputImg() with redo=True. So, to keep the containers up to date we only have to follow each layer modification with a call to layer.applyToStack(). @param redo: @type redo: boolean @return: @rtype: bImage """ lower = self.parentImage.layersStack[self.getLowerVisibleStackIndex()] container = lower.maskedThumbContainer if self.parentImage.useThumb else lower.maskedImageContainer if redo or container is None: container = lower.getCurrentMaskedImage() container.rPixmap = None # invalidate and don't update else: if container.rPixmap is None: container.rPixmap = QPixmap.fromImage( container) # will probably be reused : update return container def full2CurrentXY(self, x, y): """ Maps x,y coordinates of pixel in the full image to coordinates in current image. @param x: @type x: int or float @param y: @type y: int or float @return: @rtype: 2uple of int """ if self.parentImage.useThumb: currentImg = self.getThumb() x = (x * currentImg.width()) / self.width() y = (y * currentImg.height()) / self.height() return int(x), int(y) 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 applyToStack(self): """ Apply new layer parameters and propagate changes to upper layers. """ # recursive function def applyToStack_(layer, pool=None): # apply transformation if layer.visible: start = time() layer.execute(l=layer) layer.cacheInvalidate() print("%s %.2f" % (layer.name, time() - start)) stack = layer.parentImage.layersStack lg = len(stack) ind = layer.getStackIndex() + 1 # update histograms displayed # on the layer form, if any if ind < lg: grForm = stack[ind].getGraphicsForm() if grForm is not None: grForm.updateHists() # get next upper visible layer while ind < lg: if stack[ind].visible: break ind += 1 if ind < lg: layer1 = stack[ind] applyToStack_(layer1, pool=pool) try: QApplication.setOverrideCursor(Qt.WaitCursor) QApplication.processEvents() applyToStack_(self, pool=None) # update the presentation layer self.parentImage.prLayer.execute(l=None, pool=None) finally: self.parentImage.setModified(True) QApplication.restoreOverrideCursor() QApplication.processEvents() """ def applyToStackIter(self): #iterative version of applyToStack stack = self.parentImage.layersStack ind = self.getStackIndex() + 1 try: QApplication.setOverrideCursor(Qt.WaitCursor) QApplication.processEvents() self.execute() for layer in stack[ind:]: if layer.visible: layer.cacheInvalidate() # for hald friendly layer compute output hald, otherwise compute output image layer.execute() finally: QApplication.restoreOverrideCursor() QApplication.processEvents() """ def isAdjustLayer(self): return self.view is not None # hasattr(self, 'view') def isSegmentLayer(self): return 'SEGMENT' in self.role def isCloningLayer(self): return 'CLONING' in self.role def isGeomLayer(self): return 'GEOM' in self.role def is3DLUTLayer(self): return '3DLUT' in self.role def isRawLayer(self): return 'RAW' in self.role def updatePixmap(self, maskOnly=False): """ Synchronize rPixmap with the layer image and mask. if maskIsEnabled is False, the mask is not used. If maskIsEnabled is True, then - if maskIsSelected is True, the mask is drawn over the layer as a color mask. - if maskIsSelected is False, the mask is drawn as an opacity mask, setting the image opacity to that of the mask (mode DestinationIn). @param maskOnly: not used : for consistency with overriding method signature @type maskOnly: boolean """ rImg = self.getCurrentImage() # apply layer transformation. Missing pixels are set to QColor(0,0,0,0) if self.xOffset != 0 or self.yOffset != 0: x, y = self.full2CurrentXY(self.xOffset, self.yOffset) rImg = rImg.copy( QRect(-x, -y, rImg.width() * self.Zoom_coeff, rImg.height() * self.Zoom_coeff)) if self.maskIsEnabled: rImg = vImage.visualizeMask(rImg, self.mask, color=self.maskIsSelected, clipping=True) # self.isClipping) self.rPixmap = QPixmap.fromImage(rImg) self.setModified(True) def getStackIndex(self): """ Returns layer index in the stack, len(stack) - 1 if the layer is not in the stack. @return: @rtype: int """ i = -1 # TODO added 5/11/18 validate for i, l in enumerate(self.parentImage.layersStack): if l is self: break return i def setMaskEnabled(self, color=False): self.maskIsEnabled = True self.maskIsSelected = color self.maskSettingsChanged.sig.emit() def getTopVisibleStackIndex(self): """ Returns the index of the top visible layer @return: @rtype: """ stack = self.parentImage.layersStack lg = len(stack) for i in range(lg - 1, -1, -1): if stack[i].visible: return i return -1 def getLowerVisibleStackIndex(self): """ Returns the index of the next lower visible layer, -1 if it does not exists @return: @rtype: int """ ind = self.getStackIndex() stack = self.parentImage.layersStack for i in range(ind - 1, -1, -1): if stack[i].visible: return i return -1 def getUpperVisibleStackIndex(self): """ Returns the index of the next upper visible layer, -1 if it does not exists @return: @rtype: int """ ind = self.getStackIndex() stack = self.parentImage.layersStack lg = len(stack) for i in range(ind + 1, lg, 1): if stack[i].visible: return i return -1 def getLowerClippingStackIndex(self): """ Returns the index of the next lower clipping layer, -1 if it does not exists @return: @rtype: int """ ind = self.getStackIndex() for i in range(ind + 1, len(self.parentImage.layersStack), 1): if self.parentImage.layersStack[i].isClipping: return i return -1 def linkMask2Lower(self): """ share mask with next lower layer @return: @rtype: """ ind = self.getStackIndex() if ind == 0: return lower = self.parentImage.layersStack[ind - 1] # don't link two groups if self.group and lower.group: return if not self.group and not lower.group: self.group = [self, lower] lower.group = self.group elif not lower.group: if not any(o is lower for o in self.group): self.group.append(lower) lower.group = self.group elif not self.group: if not any(item is self for item in lower.group): lower.group.append(self) self.group = lower.group self.mask = lower.mask def unlinkMask(self): self.mask = self.mask.copy() # remove self from group for i, item in enumerate(self.group): if item is self: self.group.pop(i) # don't keep group with length 1 if len(self.group) == 1: self.group.pop(0) break self.group = [] 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 reset(self): """ reset layer to inputImg """ self.setImage(self.inputImg()) def setOpacity(self, value): """ set layer opacity to value/100.0 @param value: @type value: int in range 0..100 """ self.opacity = value / 100.0 def setColorMaskOpacity(self, value): """ Set mask alpha channel to value @param value: @type value: int in range 0..255 """ self.colorMaskOpacity = value buf = QImageBuffer(self.mask) buf[:, :, 3] = np.uint8(value) def readFromStream(self, dataStream): grForm = self.getGraphicsForm() if grForm is not None: grForm.readFromStream(dataStream) return dataStream
class Canvas(QWidget): content_changed = Signal() _background_color = QColor.fromRgb(0, 0, 0) _foreground_color = QColor.fromRgb(255, 255, 255) def __init__(self, parent, w, h, pen_width, scale): super().__init__(parent) self.w = w self.h = h self.scaled_w = scale * w self.scaled_h = scale * h self.scale = scale # Set size self.setFixedSize(self.scaled_w, self.scaled_h) # Create image self.small_image = QImage(self.w, self.h, QImage.Format_RGB32) self.small_image.fill(self._background_color) self.large_image = QImage(self.scaled_w, self.scaled_h, QImage.Format_RGB32) self.large_image.fill(self._background_color) # Create pen self.pen = QPen() self.pen.setColor(self._foreground_color) self.pen.setJoinStyle(Qt.RoundJoin) self.pen.setCapStyle(Qt.RoundCap) self.pen.setWidthF(scale * pen_width) # There is currently no path self.currentPath = None self.content_changed.connect(self.repaint) def _get_painter(self, paintee): painter = QPainter(paintee) painter.setPen(self.pen) painter.setRenderHint(QPainter.Antialiasing, True) return painter def _derive_small_image(self, large_image=None): if large_image is None: large_image = self.large_image # Downsample image self.small_image = large_image.scaled(self.w, self.h, mode=Qt.SmoothTransformation) self.content_changed.emit() def _current_path_updated(self, terminate_path=False): # Determine whether to draw on the large image directly or whether to make a temporary copy paintee = self.large_image if terminate_path else self.large_image.copy( ) # Draw path on the large image of choice painter = self._get_painter(paintee) if self.currentPath.elementCount() != 1: painter.drawPath(self.currentPath) else: painter.drawPoint(self.currentPath.elementAt(0)) painter.end() # Optionally terminate the path if terminate_path: self.currentPath = None # Downsample image self._derive_small_image(paintee) def _clear_image(self): self.large_image.fill(self._background_color) self._derive_small_image() def get_content(self): return np.asarray(self.small_image.constBits()).reshape( (self.h, self.w, -1)) def set_content(self, image_rgb): for row in range(image_rgb.shape[0]): for col in range(image_rgb.shape[1]): self.small_image.setPixel(col, row, image_rgb[row, col]) self.large_image = self.small_image.scaled( self.scaled_w, self.scaled_h, mode=Qt.SmoothTransformation) self._derive_small_image() self.content_changed.emit() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: # Create new path self.currentPath = QPainterPath() self.currentPath.moveTo(event.pos()) self._current_path_updated() def mouseMoveEvent(self, event): if (event.buttons() & Qt.LeftButton) and self.currentPath is not None: # Add point to current path self.currentPath.lineTo(event.pos()) self._current_path_updated() def mouseReleaseEvent(self, event): if (event.button() == Qt.LeftButton) and self.currentPath is not None: # Add terminal point to current path self.currentPath.lineTo(event.pos()) self._current_path_updated(terminate_path=True) elif event.button() == Qt.RightButton: self._clear_image() def paintEvent(self, event): paint_rect = event.rect() # Only paint the surface that needs painting painter = self._get_painter(self) # Draw image painter.scale(self.scale, self.scale) painter.drawImage(paint_rect, self.small_image, paint_rect) painter.end() painter = self._get_painter(self) #if self.currentPath is not None: # painter.drawPath(self.currentPath) @Slot() def repaint(self): super().repaint()
class Map(QQuickPaintedItem): def __init__(self, parent = None): super(Map, self).__init__(parent) self.viewport = None self.setAcceptedMouseButtons(Qt.AllButtons) self.generate() clicked = Signal(QPoint) @Slot() def generate(self): # QImage does not work with bits, only with bytes self.image = QImage(300, 300, QImage.Format_Grayscale8) self.oldImage = None self.generateMap() self._percentage = 0 self._pressClick = QPoint() def pixel(self, x: int, y: int, image = None) -> bool: if not image: image = self.image # This solves: # QImage::pixelIndex: Not applicable for 8-bpp images (no palette) return image.bits()[int(x) + int(y) * image.bytesPerLine()] & 0xff def setPixel(self, x: int, y: int, value: int, image = None): if not image: image = self.image # This solves: # QImage::pixelIndex: Not applicable for 8-bpp images (no palette) image.bits()[int(x) + int(y) * image.bytesPerLine()] = value def createRandomMap(self): for i in range(self.image.byteCount()): if random.random() < 0.35: self.image.bits()[i] = 255 else: self.image.bits()[i] = 0 def countNeighbors(self, x: int, y: int, image = None, n = 1) -> int: if not image: image = self.image count = 0 for i in range(-n , n + 1): for u in range(-n , n + 1): if not i and not u: continue #TODO: pixel function is poor # need to use bits() if x + i < 0 or y + u < 0 or \ x + i >= image.width() or y + u >= image.height(): count += 1 continue if not self.pixel(x + i, y + u, image): count += 1 return count @Slot(int) def doStep(self, n = 1): if self.image.format() != QImage.Format_Grayscale8: print("Wrong file format, generate map again.") return deathLimit = 14 self._percentage = 0 for _ in range(n): _image = self.image.copy() for x in range(self.image.width()): self._percentage += 1.0/(self.image.width()*n) if x%10 == 0: # Update percentage self.percentageChanged.emit() # processEvent is necessary QEventLoop().processEvents() # Update map self.update() for y in range(self.image.height()): if self.countNeighbors(x, y, _image, 2) > deathLimit or \ x == 0 or y == 0 or x == _image.width() - 1 or y == _image.height() - 1: self.setPixel(x, y, 0) else: self.setPixel(x, y, 255) # Update percentage self.update() self.oldImage = self.image.copy() self.percentageChanged.emit() QEventLoop().processEvents() def generateMap(self): self.createRandomMap() self.update() self.oldImage = self.image.copy() def paint(self, painter): painter.drawImage(QRect(0, 0, self.width(), self.height()), self.image) self.viewport = painter.viewport() def keyPressEvent(self, event): if event.key() == Qt.Key_Space: print('Update..') start = time.time() self.doStep() print('Took: %.2fs' % (time.time() - start)) event.accept() def mousePressEvent(self, event): a, b = event.pos().x()*self.image.width()/self.width(), event.pos().y()*self.image.height()/self.height() self.clicked.emit(QPoint(a, b)) def mouseMoveEvent(self, event): a, b = event.pos().x()*self.image.width()/self.width(), event.pos().y()*self.image.height()/self.height() self.clicked.emit(QPoint(a, b)) def percentage(self): return self._percentage percentageChanged = Signal() percentage = Property(float, percentage, notify=percentageChanged) @Slot() def addVehicle(self): self.image = self.image.convertToFormat(QImage.Format_RGBA8888) painter = QPainter(self.image) painter.drawImage(QRect(50, 50, 13, 15), QImage("imgs/turtle.png")) painter.end() self.update() @Slot(str) def saveMap(self, path: str): # Check image encoder if(self.image.format() != QImage.Format_Grayscale8): print('Wrong map pixel format, create map without vehicle added.') return ok = self.image.save(path) if(not ok): print('It was not possible to save Map in:', path) @Slot(str) def loadMap(self, path: str): # Check image encoder image = QImage(path) if(image.format() != QImage.Format_Grayscale8): print('Wrong map pixel format, map should be Grayscale8.') return self.image = image self.oldImage = self.image.copy() self.update()