简体   繁体   中英

Interactive Drawing in Python

First of all. I already have some code running on python and PyQt in which the user draws an image and the program returns the drawed image. What I would like to do is to let the user modify the drawing once he has finished. For example he can click on a point he has drawed and drag it around modifying the painting.

Can anyone give me ideas or libraries to do so?

This is the existing code I have:

import sys 
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import cv2

##
# MAIN WINDOW LAYOUT
##
class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.initUI()

    def initUI(self):
        self.view = View(self)

        # Button to clear both image and drawing
        self.button = QPushButton('Clear Drawing', self)            
        self.button.clicked.connect(self.handleClearView)

        # 'Load image' button
        self.btnLoad = QToolButton(self)
        self.btnLoad.setText('Load image')
        self.btnLoad.clicked.connect(self.loadImage)

        # Save
        self.btnSave = QToolButton(self)
        self.btnSave.setText('Save image')
        self.btnSave.clicked.connect(self.file_save)

        # Save as
        self.btnSaveAs = QToolButton(self)
        self.btnSaveAs.setText('Save as...')
        self.btnSaveAs.clicked.connect(self.file_save_as)

        # Arrange Layout
        self.layout = QVBoxLayout(self)                         
        self.layout.addWidget(self.view)        # Drawing
        self.layout.addWidget(self.button)      # Clear view
        self.layout.addWidget(self.btnLoad)     # Load photo
        self.layout.addWidget(self.btnSave)     # Save
        self.layout.addWidget(self.btnSaveAs)   # Save as...


        self.setGeometry(0, 25, 1365, 700)
        self.setWindowTitle('Processed Slices')
        self.show()

    def file_save_as(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        filename, _ = QFileDialog.getSaveFileName(self,"QFileDialog.getSaveFileName()","","All Files (*);;Text Files (*.txt)", options=options)
        cv2.imwrite(filename + '.png', self.view.cvImage)

    def file_save(self):
        cv2.imwrite(self.view._filename, self.view.cvImage)

    def openFileNameDialog(self):    
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        fileName, _ = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","All Files (*);;Python Files (*.py)", options=options)
        if fileName:
            self.view._filename = fileName

    def loadImage(self):
        self.view._empty = False
        # Load Image to pixmap
        self.openFileNameDialog()
        self.view.cvImage = cv2.imread(self.view._filename)
        self.view.height, self.view.width , self.view.bytesPerComponent= self.view.cvImage.shape
        self.view.bytesPerLine = self.view.bytesPerComponent * self.view.width
        cv2.cvtColor(self.view.cvImage, cv2.COLOR_BGR2RGB, self.view.cvImage)
        self.view.mQImage = QImage(self.view.cvImage.data, self.view.width, self.view.height, self.view.bytesPerLine, QImage.Format_RGB888)        
        self.pixmap = QPixmap(self.view.mQImage) 

        # Include pixmap on the drawing scene
        self.view.setScene(QGraphicsScene(self))
        self.view.setSceneRect(QRectF(0,0,self.view.width, self.view.height))   # Scene has same dimension as image so that we can map the segmented area to the cv2 image
        self.view.scene().addPixmap(self.pixmap)
        self.view.fitInView()

    def handleClearView(self):
        self.view.scene().clear()
        self.view.scene().addPixmap(self.pixmap)
        self.view.contour = []
        self.view.fitInView()



##
# DRAWING AND ZOOMING
##
class View(QGraphicsView):
    def __init__(self, parent):

        super().__init__()

        # Attributes
        self._zoom = 0
        self._empty = True
        self._scene = QGraphicsScene(self)
        self._photo = QGraphicsPixmapItem()
        self._scene.addItem(self._photo)
        self._filename = ""
        # Resettings for Zooming
        self.setScene(self._scene)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setBackgroundBrush(QBrush(QColor(30, 30, 30)))
        self.setFrameShape(QFrame.NoFrame)

        self.contour = []                   # Contains points of the contour for cv2 drawing
        self.first = QPointF(0,0)
        self.ii = 0

    """ PAINTING """
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._start = event.pos()                                                                               # Get point where we pressed
            self.contour.append(QPointF(self.mapToScene(self._start)))              
            if self.first == QPointF(0,0):                                                           # If it is the first we press, save the point which will be used to close 
                                                                                                     # the shape whenever the right button is pressed
                self.first = QPointF(self.mapToScene(self._start))
                self.ii = 0
            else:                                                                                   # If it is not the first point draw a line joining the point we previously 
                                                                                                    # pressed and the one we have just clicked
                self.scene().addItem(QGraphicsLineItem(QLineF(self.contour[self.ii+1], self.contour[self.ii]))) 
                self.ii += 1


    def mouseReleaseEvent(self, event):
        # RIGHT BUTTON
        if event.button() == Qt.RightButton:
            self.scene().addItem(QGraphicsLineItem(QLineF(self.contour[-1], self.first)))  # close contour
            self.first = QPointF(0,0)
            self.contour.append((self.contour[0]))

            #Drawing CV_IMAGE
            for i in range(len(self.contour) - 1):
                A = ( int( self.contour[i].x() ), int( self.contour[i].y() ) )
                B = ( int( self.contour[i+1].x() ), int( self.contour[i+1].y() ))
                cv2.line(self.cvImage, A, B, (0,0,255), 2)
            cv2.imshow('drawed slice', self.cvImage)
            cv2.waitKey()


    """ ZOOMING """
    def hasPhoto(self):
        return not self._empty

    def fitInView(self, scale=True):
        rect = QRectF(0, 0, self.width, self.height)
        if not rect.isNull():
            self.setSceneRect(rect)
            if self.hasPhoto():
                unity = self.transform().mapRect(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 wheelEvent(self, event):
        if self.hasPhoto():
            if event.angleDelta().y() > 0:              # event.angleDelta() returns the distance that the wheel is rotated, in eighths of a degree.
                factor = 1.25                           # Zooming in
                self._zoom += 1
            else:
                factor = 0.8                            # Zooming out
                self._zoom -= 1

            if self._zoom > 0:
                self.scale(factor, factor)
            elif self._zoom == 0:
                self.fitInView()
            else:                                       # Cannot zoom out from the original size
                self._zoom = 0


##
# RUN PROGRAM
##
if __name__ == '__main__':

    import sys
    app = QApplication(sys.argv)
    ex = Window()
    sys.exit(app.exec_()) 

Just copy paste the program, click the load image and draw a figure by multiple left-clicking, to end the drawing and see the results you have to right-click.

So there is no easy answer to this. I've done this, but it was many years ago. You need to implement some kind of hit-test mechanism and "drag handles". It's a simple architecture, but Qt doesn't have one built in. Basically you have to look at your scene as the product of a sequence of steps which create items. Your line self.scene().addItem(...) is key. You need to either go back into the scene and look at the items present, (I forget what the scene is able to tell you about the items it has) or enable your own external tracking.

But once you know what items are in the scene, you need to modify your mousePress event to see if they are clicking on something that already exists, and if so, modify that object in the manner desired. This will invariably require some thresholding, so you'll have to look for items around 1 to 5 pixels around the mouse click, and include them as well. Once you decide that the user has clicked on something valid, you apply the events. So you'll need to add a test to mousePress, unless you have a toggle (mode, tool, etc) for editing instead of creating.

None of this is specific to PyQt, just Qt in general.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM