Search code examples
pythonpython-3.xuser-interfacepyqt5paint

Implementing flood fill in PyQt5


I'm trying to implement the equivalent of Paint, and for this I need to make a filling. Can anyone tell how to use PyQt5 to find out the color of a pixel, and use the width search to find similar pixels. And then change all these pixels to a new color. Just in tkinter for this were getpixel and putpixel. I'm wondering if PyQt5 does this. If there is, then I ask to show some example of the implementation of this.  

P.s. You can without looking for pixels, just show how to take and replace pixels.

P.s.s. I apologize for my English, if something is wrong с:


Solution

  • I have an implementation of a Paint program here which includes an example of a flood fill.

    Unfortunately, it's a little more complicated than you might imagine. Reading a pixel from a QImage in Qt is possible you can do it as follows —

    QImage.pixel(x, y)       # returns a QRgb object
    QImage.pixelColor(x, y)  # returns a QColor object
    

    Basic algorithm

    The basic Forest Fire fill algorithm using QImage.pixel(x,y) is shown below. We start by converting our pixmap to a QImage (if neccessary).

        image = self.pixmap().toImage()
        w, h = image.width(), image.height()
        x, y = e.x(), e.y()
    
        # Get our target color from origin.
        target_color = image.pixel(x,y)
    

    Then we define a function which, for a given position looks at all surrounding positions — if they haven't been looked at yet — and tests whether it is a hit or a miss. If it's a hit, we store that pixel to fill later.

        def get_cardinal_points(have_seen, center_pos):
            points = []
            cx, cy = center_pos
            for x, y in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
                xx, yy = cx + x, cy + y
                if (xx >= 0 and xx < w and
                    yy >= 0 and yy < h and
                    (xx, yy) not in have_seen):
    
                    points.append((xx, yy))
                    have_seen.add((xx, yy))
    
            return points
    

    To perform the fill we create a QPainter to write to our original pixmap. Then, starting at our initial x,y we iterate, checking cardinal points, and — if we have a match — pushing those new squares onto our queue. We fill any matching points as we go.

        # Now perform the search and fill.
        p = QPainter(self.pixmap())
        p.setPen(QPen(self.active_color))
    
        have_seen = set()
        queue = [(x, y)]
    
        while queue:
            x, y = queue.pop()
            if image.pixel(x, y) == target_color:
                p.drawPoint(QPoint(x, y))
                queue.extend(get_cardinal_points(have_seen, (x, y)))
    
        self.update()
    

    Performance

    The QImage.pixel() can be slow, so the above implementation reading/writing directly on the QImage isn't really feasible for very large images. After that point it will start to take > a few seconds to fill the area.

    The solution I've used is to convert the area to be filled into bytes. There are 4 bytes per pixel (RGBA). This gives us a data structure that's far quicker to interact with.

        image = self.pixmap().toImage() # Convert to image if you have a QPixmap
        w, h = image.width(), image.height()
        s = image.bits().asstring(w * h * 4)
    

    Next we need to find our current location's 3-byte (RGB) value. With our data structure, we create a custom function to retrieve our hit/miss bytes.

        # Lookup the 3-byte value at a given location.
        def get_pixel(x, y):
            i = (x + (y * w)) * 4
            return s[i:i+3]
    
        x, y = e.x(), e.y()
        target_color = get_pixel(x, y)
    

    The actual loop to perform the search for all points in our input and write them out to the QPixmap if we find a match.

        # Now perform the search and fill.
        p = QPainter(self.pixmap())
        p.setPen(QPen(self.active_color))
    
        have_seen = set()
        queue = [(x, y)]
    
        while queue:
            x, y = queue.pop()
            if get_pixel(x, y) == target_color:
                p.drawPoint(QPoint(x, y))
                queue.extend(get_cardinal_points(have_seen, (x, y)))
    
        self.update()