The pygame drawing functions leave pixel-wide gaps. Why? - python

After converting a piece of code (that animates a pattern of rectangles) from Java to Python, I noticed that the animation that the code produced seemed quite glitchy. I managed to reproduce the problem with a minimal example as follows:
import pygame
SIZE = 200
pygame.init()
DISPLAYSURF = pygame.display.set_mode((SIZE, SIZE))
D = 70.9
xT = 0.3
yT = 0
#pygame.draw.rect(DISPLAYSURF, (255,0,0), (0, 0, SIZE, SIZE))
pygame.draw.rect(DISPLAYSURF, (255,255,255), (xT, yT, D, D))
pygame.draw.rect(DISPLAYSURF, (255,255,255), (xT+D, yT+D, D, D))
pygame.draw.rect(DISPLAYSURF, (0,0,0), (xT, yT+D, D, D))
pygame.draw.rect(DISPLAYSURF, (0,0,0), (xT+D, yT, D, D))
pygame.display.update()
This code generates the following image:
Notice that the squares don't line up perfectly in the middle. Uncommenting the commented line in the code above results in the following image, which serves to illuminate the problem further:
It seems that there are pixel-wide gaps in the black and white pattern, even though it can be seen in the code (by the data that is passed in the calls to pygame.draw.rect()) that this shouldn't be the case. What is the reason for this behaviour, and how can I fix it?
(This didn't happen in Java, here is a piece of Java code corresponding to the Python code above).

Looking at the rendered picture in an image editor, the pixel distances can be confirmed as such:
Expanding the function calls (i.e. performing the additions manually), one can see that the input arguments to draw the white rectangles are of the form
pygame.draw.rect(DISPLAYSURF, (255,255,255), ( 0.3, 0, 70.9, 70.9))
pygame.draw.rect(DISPLAYSURF, (255,255,255), (71.2, 70.9, 70.9, 70.9))
Since fractions of pixels do not make sense screen-wise, the input must be discretized in some way. Pygame (or SDL, as mentioned in the comments to the question) seems to choose truncating, which in practice transforms the drawing commands to:
pygame.draw.rect(DISPLAYSURF, (255,255,255), ( 0, 0, 70, 70))
pygame.draw.rect(DISPLAYSURF, (255,255,255), (71, 70, 70, 70))
which corresponds to the dimensions in the rendered image. If AWT draws it differently, my guess is that it uses rounding (of some sort) instead of truncating. This could be investigated by trying different rendering inputs, or by digging in the documentation.
If one wants pixel perfect rendering, using floating points as input is not well defined. If one keeps to the integers, the result should be independent of renderer, though.
EDIT: I expand a bit if anyone else finds this, since I couldn't find much info on this behavior apart from the source code.
The function call in question takes the following input arguments (documentation):
pygame.draw.rect(Surface, color, Rect, width=0)
where Rect is a specific object defined by a top-left coordinate, a width and a height. By design it only handles integer attributes, since it is meant as a low-level "this is what you see on the screen" data type. The data type handles floats by truncating:
>>> import pygame
>>> r = pygame.Rect((1, 1, 8, 12))
>>> r.bottomright
(9, 13)
>>> r.bottomright = (9.9, 13.5)
>>> r.bottomright
(9, 13)
>>> r.bottomright = (11.9, 13.5)
>>> r.bottomright
(11, 13)
i.e., a regular (int) cast is done.
The Rect object is not meant as a "store the coordinates for my sprite" object, but as a "this is what the screen will represent" object. Floating points are certainly useful for the former purpose, and the designer would probably want to keep an internal list of floats to store this information. Otherwise, incrementing a screen position by e.g. r.left += 0.8 (where r is the Rect object) would never move r at all.
The problem in the question comes from (quite reasonably) assuming that the right x coordinate of the rectangle will at least be calculated as something like x₂ = int(x₁ + width), but since the function call implicitly transforms the input tuple to a Rect object before proceeding, and since Rect will truncate its input arguments, it will instead calculate it as x₂ = int(x₁) + int(width), which is not always the same for float input.
To create a Rect using rounding rules, one could e.g. define a wrapper like:
def rect_round(x1, y1, w, h):
"""Returns pygame.Rect object after applying sane rounding rules.
Args:
x1, y1, w, h:
(x1, y1) is the top-left coordinate of the rectangle,
w is width,
h is height.
Returns:
pygame.Rect object.
"""
r_x1 = round(x1)
r_y1 = round(y1)
r_w = round(x1 - r_x1 + w)
r_h = round(y1 - r_y1 + h)
return pygame.Rect(map(int, (r_x1, r_y1, r_w, r_h)))
(or modified for other rounding rules) and then call the draw function as e.g.
pygame.draw.rect(DISPLAYSURF, (255,255,255), rect_round(71.2, 70.9, 70.9, 70.9))
One will never bypass the fact that the pixel by definition is the smallest addressable unit on the screen, though, so this solution might also have its quirks.
Related thread on the Pygame mailing list from 2005: Suggestion: make Rect use float coordinates

Related

Why does the ellipse position not similar to my mouse position?

I have a code here that will add an ellipse and line when mouse is clicked.
class Viewer(QtWidgets.QGraphicsView):
def __init__(self, parent):
super(leftImagePhotoViewer, self).__init__(parent)
self._zoom = 0
self._empty = True
self._scene = QtWidgets.QGraphicsScene(self)
self.setGeometry(QtCore.QRect(20, 90, 451, 421))
self.setSceneRect(20, 90, 451, 421)
I have an MouseRelase Event
def mouseReleaseEvent(self,event):
pos = self.mapToScene(event.pos())
point = self._scene.addEllipse(self._size/2, self._size/2, 10, 10, QPen(Qt.black), QBrush(Qt.green))
point.setPos(QPointF(pos.x(),pos.y()))
self._scene.addLine(pos.x(),pos.y(), self.posprev.x(), self.posprev.y(), QPen(Qt.green))
When I clicked the line, its position is similar to the mouse positon, but the ellipse positon has few gap or difference to the exact mouse position.The center of the ellipse should be the endpoitn of the line or where the mouse position is.
See image here:
Can someone help me what is wrong why the ellipse will not add on the exact position to the mouse?
As the documentation of addEllipse() explains:
Note that the item's geometry is provided in item coordinates, and its position is initialized to (0, 0).
This is actually valid for all QGraphicsScene functions that add basic shapes, and the initialized position is always (0, 0) for all QGraphicsItems in general.
Consider the following:
point = scene.addEllipse(5, 5, 10, 10)
The above will create an ellipse enclosed in a rectangle that starts at (5, 5) relative to its position. Since we've not moved it yet, that position is the origin point of the scene.
The ellipse as it as soon as it's created, with the rectangle shown as a reference of its boundaries.
Then, we set its position (assuming the mouse is at 20, 20 of the scene):
point.setPos(QPointF(20, 20))
The result will be an ellipse enclosed in a rectangle that has its top left corner at (25, 25), which is the rectangle position relative to the item position: (5, 5) + (20, 20).
Note that the above shows both the ellipse in the original position and the result of setPos().
If you want an ellipse that will be centered on its position, you must create one with negative x and y coordinates that are half of the width and height of its rectangle.
Considering the case above, the following will properly show the ellipse centered at (20, 20):
point = scene.addEllipse(-5, -5, 10, 10)
point.setPos(QPointF(20, 20))
Notes:
as the documentation shows, mapToScene() already returns a QPointF, there's no point in doing setPos(QPointF(pos.x(), pos.y())): just do setPos(pos);
remember what said above: all items have a starting position at (0, 0); this is valid also for the line you're creating after that point, which will be drawn between pos and self.posprev, but will still be at (0, 0) in scene coordinates;
the view and the scene might need mouse events, especially if you're going to add movable items; you should always call the base implementation (in your case, super().mouseReleaseEvent(event)) when you override functions, unless you really know what you're doing;
as already suggested to you, it is of utmost importance that you read and understand the whole graphics view documentation, especially how its coordinate system works; the graphics view framework is as much powerful as it is complex, and cannot be learnt just by trial and error: being able to use it requires a lot of patience in understanding how it works by carefully studying the documentation of each of its classes and all functions you are going to use;

How to circle moving objects using opencv?

I have a video file and I need to circle all moving objects in a certain frame I select. My idea of a solution to this problem is:
Circle all moving objects (white areas) on a video on which was applied motion detector and circle the same areas on the original frame.
I am using BackgroundSubtractorGMG() from cv2 to detect movement
Below I show the way I expect this program to work(I used to paint, so I am now sure this is correct, but I hope it is good enough to demonstrate the concept)
As others have said in comments:
Get the mask from you background subtraction algorithm
use cv.findContours(mask, ...) to find contours
(optional) select which contours you want to keep (something like ((x, y), radius) = cv.minEnclosingCircle(contour) or a, b, w, h = cv.boundingRect(c)
and if radius > 5
use drawing functions like cv.rectangle or similar to draw the shape around the contour (like so: cv.rectangle(img, (a, b), (a + w, b + h), (0, 255, 0), 2))

Ways to reduce recursion depth in Python

I am currently making a program which makes use of floodfill recursion (such as filling a white circle with black borders into a different color). When I click on my image to floodfill, only part of the circle will get filled into the different color, then I get the Recursion Error. The only part of my code that has recursion is this.
def floodfill(x,y):
floodfill(x+1,y)
floodfill(x-1,y)
floodfill(x, y+1)
floodfill(x, y-1)
You do so by not using recursion; you'd run into the recursion limit for circles with a radius matching the recursion limit, which defaults to 1000, and the limit can't arbitrarily be raised. Use an iterative approach instead. You can do so here with a queue:
from collections import deque
def floodfill(x, y, _directions=((-1, 0), (0, -1), (1, 0), (0, 1))):
queue = deque([(r, c)])
handled = {(r, c)}
while queue:
r, c = queue.popleft()
for dr, dc in _directions:
nr, nc = r + dr, c + dc
if (nr, nc) not in handled:
handled.add((nr, nc))
queue.append((nr, nc))
# do something with these coordinates, like filling
# this position in an image.
I used a set to track what coordinates have not yet been handled here; there may be simpler way for your application to do the same (like simply testing that the floodfill colour is already applied to that pixel).
You also would test for the boundary conditions in the same if test location. If the pixel is already black there, you'd also ignore those coordinates.

How does one stretch an image to the shape of a polygon in Pygame?

Alternatively, how does one fill a polygon in Pygame with an image?
I've tried searching documentation but didn't turn up much, so I'm asking here to see if I missed out anything. I can use transformation matrices if needed.
Here's an example of what I want to achieve:
You might be looking for pygame's alternative drawing functions from pygame.gfxdraw, what your looking is textured polygon
Note: You need to import it separately as gfxdraw doesn't import as default so you need to import it separately e.g.
import pygame
import pygame.gfxdraw
I think the concept you are looking for is a bitmap mask. Using a special blending mode, pygame.BLEND_RGBA_MULT, we can blend the contents of two surfaces together. Here, by blending, I mean that we can simply multiply the color values of two corresponding pixels together to get the new color value. This multiplication is done on normalized RGB colors, so white (which is (255, 255, 255) would actually be converted to (1.0, 1.0, 1.0), then multiplication between the masking surface and the masked surface's pixels would be performed, and then you would convert back to the non-normalized RGB colors.
In this case, all that means is that we need to draw the polygon to one surface, draw the image of interest to another surface, and blend the two together and render the final result to the screen. Here is the code that does just that:
import pygame
import sys
(width, height) = (800, 600)
pygame.init()
screen = pygame.display.set_mode((width, height))
image = pygame.image.load("test.png").convert_alpha()
masked_result = image.copy()
white_color = (255, 255, 255)
polygon = [(0, 0), (800, 600), (0, 600)]
mask_surface = pygame.Surface((width, height))
pygame.draw.polygon(mask_surface, white_color, polygon)
pygame.draw.aalines(mask_surface, white_color, True, polygon)#moderately helps with ugly aliasing
masked_result.blit(mask_surface, (0, 0), None, pygame.BLEND_RGBA_MULT)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screen.blit(masked_result, (0, 0))
pygame.display.update()
The result looks like this using a simple triangle as the mask.
Some things to note:
This answer is based off of this, which you might be interested in if you want more complicated masks than polygons (such as pretty bitmap masks).
This solution is not using the pygame.mask module!
I have not fully tested this code yet, but I suspect that this will only work for convex polygons, which may be a significant limitation. I am not sure if pygame can draw concave polygons, but I know PyOpenGL can using stencil buffer tricks or tesselation. Let me know if you are interested in that approach instead.
While I have attempted to get rid of aliasing, the results are still not completely satisfactory.

Efficiently masking a surface in pygame

I need to draw a circle filled with random gray colors and a black outline using pygame. This is what it should look like:
The radius increases by expansion_speed * dt every frame and the surface is updated 60 times per second, so however this is achieved (if even possible) needs to be fast. I tried masking an stored texture but that was too slow. My next idea was to read the pixels from this stored texture and only replace the difference between the last and current surfaces. I tried this too but was unable to translate the idea to code.
So how can this be done?
See my update to your previous related question. It has some info about performance. You could try to enable hardware acceleration in fullscreen mode, but I never personally tried it, so can't give good advice how to do it properly. Just use two differnt colorkeys for extracting circle from noise and putting the whole surface to the display. Note that if your Noise surface has pixels same as colorkey color then they also become transparent.
This example I think is what you are trying to get, move the circle with mouse and hold CTRL key to change radius.
Images:
import os, pygame
pygame.init()
w = 800
h = 600
DISP = pygame.display.set_mode((w, h), 0, 24)
clock = pygame.time.Clock( )
tile1 = pygame.image.load("2xtile1.png").convert()
tile2 = pygame.image.load("2xtile2.png").convert()
tw = tile1.get_width()
th = tile1.get_height()
Noise = pygame.Surface ((w,h))
Background = pygame.Surface ((w,h))
for py in range(0, h/th + 2) :
for px in range(0, w/tw + 2):
Noise.blit(tile1, (px*(tw-1), py*(th-1) ) )
Background.blit(tile2, (px*(tw-1), py*(th-1) ) )
color_key1 = (0, 0, 0)
color_key2 = (1, 1, 1)
Circle = pygame.Surface ((w,h))
Circle.set_colorkey(color_key1)
Mask = pygame.Surface ((w,h))
Mask.fill(color_key1)
Mask.set_colorkey(color_key2)
strokecolor = (10, 10, 10)
DISP.blit(Background,(0,0))
def put_circle(x0, y0, r, stroke):
pygame.draw.circle(Mask, strokecolor, (x0,y0), r, 0)
pygame.draw.circle(Mask, color_key2, (x0,y0), r - stroke, 0)
Circle.blit(Noise,(0,0))
Circle.blit(Mask,(0,0))
dirtyrect = (x0 - r, y0 - r, 2*r, 2*r)
Mask.fill(color_key1, dirtyrect)
DISP.blit(Circle, (0,0))
X = w/2
Y = h/2
R = 100
stroke = 2
FPS = 25
MainLoop = True
pygame.mouse.set_visible(False)
pygame.event.set_grab(True)
while MainLoop :
clock.tick(FPS)
pygame.event.pump()
Keys = pygame.key.get_pressed()
MR = pygame.mouse.get_rel() # get mouse shift
if Keys [pygame.K_ESCAPE] :
MainLoop = False
if Keys [pygame.K_LCTRL] :
R = R + MR[0]
if R <= stroke : R = stroke
else :
X = X + MR[0]
Y = Y + MR[1]
DISP.blit(Background,(0,0))
put_circle(X, Y, R, stroke)
pygame.display.flip( )
pygame.mouse.set_visible(True)
pygame.event.set_grab(False)
pygame.quit( )
Many years ago we had a font rendering challenge with the Pygame project.
Someone created an animated static text for the contest but it was far too slow.
We put our heads together and made a much quicker version. Step one was to create a smallish image with random noise. Something like 64x64. You may need a bigger image if your final image is large enough to notice the tiling.
Every frame you blit the tiled noise using a random offset. Then you take an image with the mask, in your case an inverted circle, and draw that on top. That should give you a final image containing just the unmasked noise.
The results were good. In our case it was not noticeable that the noise was just jittering around. That may be because the text did not have a large unobstrcted area. I'd be concerned your large circle would make the trick appear obvious. i guess if you really had a large enough tiled image it would still work.
The results and final source code are still online at the Pygame website,
http://www.pygame.org/pcr/static_text/index.php

Categories

Resources