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 с:
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
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()
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()