I am trying to create a annotation tool for segmentation. My idea is to create QGrahicsView and QGraphicsScene, then additem
the image and the mask to the scene. So far I was able to overlay the mask on the image, but have not figured out yet to paint on the mask.
My code for testing:
from PySide6.QtWidgets import (
QWidget,
QMainWindow,
QApplication,
QFileDialog,
QStyle,
QColorDialog,
QGraphicsView,
QGraphicsScene,
QGraphicsPixmapItem,
)
from PySide6.QtCore import Qt, Slot, QStandardPaths, QObject
from PySide6.QtGui import (
QMouseEvent,
QPaintEvent,
QPen,
QAction,
QPainter,
QColor,
QPixmap,
QIcon,
QKeySequence,
)
from PySide6.QtUiTools import QUiLoader
import sys
loader = QUiLoader() #set up loader object
image_path =r"image.jpg"
mask_path=r"mask.png"
class UserInterface(QObject): #An object wrapping around our ui
def __init__(self):
super().__init__()
self.ui = loader.load("paint3.ui", None)
self.ui.setWindowTitle("Painter3")
self.ui.button1.clicked.connect(self.do_something)
self.mask_opcity = 0.8
def show(self):
self.ui.show()
def do_something(self):
scene = QGraphicsScene(self)
image = QPixmap(image_path)
img_item = QGraphicsPixmapItem(image)
scene.addItem(img_item)
mask = QPixmap(mask_path)
msk_item = QGraphicsPixmapItem(mask)
msk_item.setOpacity(self.mask_opcity)
scene.addItem(msk_item)
self.ui.grapView.setScene(scene)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = UserInterface()
w.show()
sys.exit(app.exec())
Example of what I am trying to achieve:
Manually paint the puppy with mouse.
This is how the exported mask should look like. Exported format should be *.png.
While it is possible to use a QGraphicsPixmapItem for this purpose, it may not be efficient, since it would require to continuously draw on a new QPixmap based on the existing one whenever the mask changes.
Instead, a QGraphicsPathItem subclass may be preferred, since it offers some benefits that can also improve performance at some level. Most importantly, it supports QRegion, which are pixel-based areas that are simpler and more appropriate for image editing.
A simple implementation requires that mousePressEvent()
and mouseMoveEvent()
handlers are implemented in that item, but this also requires to consider another important aspect: like most shape-based items, its boundaries are only within the visible, drawn areas, so we need to ensure that both its boundingRect()
and shape()
functions return the full rectangle that represents the mask, even if it's empty.
The subclass will then create a QPainterPath based on the actual regions that are going to be drawn by using the mouse. In order to achieve this, we start with an empty QRegion()
and use its functions to add (or remove) new areas to it; then, every time the overall region is updated, we create a new QPainterPath, add the overall region, and call setPath()
on the item.
By using the operators and helper functions of QRegion we can easily add new areas: the |
operator merges the original region with a new one, while the subtracted
function does the opposite.
In order to set an existing mask, we can replace the current region with a new one generated by QPixmap functions:
pixmap = QPixmap('mask.png')
mask = pixmap.createHeuristicMask()
# alternatively, createMaskFromColor(<some QColor>)
path = QPainterPath()
path.addRegion(QRegion(mask))
self.setPath(path)
To get the mask, we just create a new QPixmap based on the current size, clear it (important!), create a QPainter for it, set the current region as the clip region and fill the full rectangle with the color (which will be clipped to the mask).
Here is a basic implementation that also allows to "erase" the current mask by using the Ctrl modifier.
class MaskItem(QGraphicsPathItem):
_showMask = False
def __init__(self, size):
self.size = size
self._boundingRect = QRectF(QPointF(), QSizeF(size))
self._shape = QPainterPath()
self._shape.addRect(self._boundingRect)
self.region = QRegion()
super().__init__()
self.setPen(QPen(Qt.PenStyle.NoPen))
self.setBrush(QColor(255, 80, 255, 127))
def boundingRect(self):
return self._boundingRect
def shape(self): # required for mouse event detection
return self._shape
def getMask(self):
pm = QPixmap(self.size)
pm.fill(Qt.GlobalColor.black)
qp = QPainter(pm)
qp.setClipRegion(self.region)
qp.fillRect(pm.rect(), Qt.GlobalColor.magenta)
qp.end()
return pm
def addRegionAtPos(self, pos):
pos = pos.toPoint()
rect = QRect(pos.x() - 10, pos.y() - 10, 20, 20)
self.region |= QRegion(rect, QRegion.RegionType.Ellipse)
self.rebuild()
def removeRegionAtPos(self, pos):
pos = pos.toPoint()
rect = QRect(pos.x() - 10, pos.y() - 10, 20, 20)
self.region = self.region.subtracted(
QRegion(rect, QRegion.RegionType.Ellipse))
self.rebuild()
def rebuild(self):
path = QPainterPath()
path.addRegion(self.region)
self.setPath(path)
def setMask(self, pixmap):
if not pixmap.isNull():
self.region = QRegion(pixmap.createHeuristicMask())
self.rebuild()
def showMask(self, show):
if self._showMask != show:
self._showMask = show
if show:
self.setBrush(QColor(255, 255, 255, 127))
else:
self.setBrush(QColor(255, 80, 255, 127))
def mousePressEvent(self, event):
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
self.removeRegionAtPos(event.pos())
else:
self.addRegionAtPos(event.pos())
def mouseMoveEvent(self, event):
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
self.removeRegionAtPos(event.pos())
else:
self.addRegionAtPos(event.pos())
def paint(self, qp, opt, widget=None):
if self._showMask:
negative = QRegion(QRect(QPoint(), self.size)).subtracted(self.region)
qp.save()
qp.setClipRegion(negative)
qp.fillRect(opt.rect, Qt.GlobalColor.black)
qp.restore()
super().paint(qp, opt, widget)
class MaskMaker(QWidget):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.view = QGraphicsView(self.scene)
pixmap = QPixmap('base.jpg')
self.pixmap = self.scene.addPixmap(pixmap)
self.maskItem = MaskItem(pixmap.size())
self.scene.addItem(self.maskItem)
self.toggleButton = QPushButton('Show mask', checkable=True)
self.openButton = QPushButton('Open mask')
self.exportButton = QPushButton('Export mask')
layout = QVBoxLayout(self)
toolLayout = QHBoxLayout()
layout.addLayout(toolLayout)
toolLayout.addWidget(self.toggleButton)
toolLayout.addWidget(self.openButton)
toolLayout.addWidget(self.exportButton)
layout.addWidget(self.view)
self.toggleButton.toggled.connect(self.maskItem.showMask)
self.openButton.clicked.connect(self.open)
self.exportButton.clicked.connect(self.export)
def open(self):
path, _ = QFileDialog.getOpenFileName(self, 'Open mask', '', '*.png')
if path:
self.maskItem.setMask(QPixmap(path))
def export(self):
path, _ = QFileDialog.getSaveFileName(self, 'Export mask', '', '*.png')
if path:
self.maskItem.getMask().save(path)