Map small surface onto larger surface in PyGame - python

I'm using PyGame's graphics library to display graphics on an LED display.
My LED display is 25x7. For testing without hardware, I've been able to use PyGame's display to view a 40x amplified version:
screen = pygame.display.set_mode((1000,280))
I'd like to actually draw boxes, rects, etc on a 25x7 grid and then project it onto a larger 1000x280 window on my computer, which will look like a 25x7 grid of 40x40 "pixels". That way, if I start working with hardware, I can skip the large projection, and just write 25x7=175 pixels worth of data out to my LEDs.
I came up with a solution, but it's prohibitively slow:
xDim = 25
yDim = 7
pixelMultiplier = 40
surf = pygame.Surface((xDim,yDim))
bigSurf = pygame.Surface((pixelMultiplier*xDim,pixelMultiplier*yDim))
# ... manipulate surf ...
# map surf to bigSurf
for x in range(xDim):
for y in range(yDim):
# grab color of pixel
tmpColor = surf.get_at((x,y))
# write to all pixels in larger surface
for i in range(pixelMultiplier):
for j in range(pixelMultiplier):
bigSurf.set_at((pixelMultiplier*x+i,pixelMultiplier*y+j),tmpColor)
Is there some built-in PyGame function that does this efficiently?

Right after writing the post, this dawned on me:
Create a surface to represent the pixel, and just blit that to the larger surface. Instead of running through 1600 set_at() functions for all 175 pixels, do it with a built-in function that already does that.
# map surf to larger version
for x in range(xDim):
for y in range(yDim):
# grab color of pixel
tmpColor = surf.get_at((x,y))
# create square surface to represent pixel
pixelSurf = pygame.Surface((pixelMultiplier,pixelMultiplier))
pixelSurf.fill(tmpColor)
# blit big pixel onto bigSurf
bigSurf.blit(pixelSurf,(pixelMultiplier*x,pixelMultiplier*y))
This runs much faster, but I'm still not sure it's the optimal solution.
Edit - Best Method
PyGame does have a built-in function!
pygame.transform.scale(surface,(newX,newY))

Related

How to draw a circle on image given float (subpixel) coordinates of it center

I want to visualize results of keypoint tracking algorithm in python. I have a sequence of (Image, Keypoint) pairs (video basically). Tracking algorithm is strong enough to give me subpixel accuracy. But i have no idea, how to visualize it properly.
I tried to round my coordinates and draw a circle by cv2.circle(image, (int(round(x)), int(round(y)))), but it leads to visual jittering of my keypoints due to small image resolution.
I checked OpenCV, Pillow, skimage, Pygame (pygame.draw.circle). All of them cannot properly draw circle with float coordinates.
DIPlib has the function DrawBandlimitedBall(), which draws a disk or a circle with smooth transitions and with floating-point origin coordinates (disclosure: I'm one of the authors). You might need to draw the circle in an empty image, then blend it in to get the effect you are looking for. Code would look something like this:
import diplib as dip
img = dip.ImageRead('/Users/cris/dip/images/flamingo.tif')
p = [366.4, 219.1]
# Create an empty image and draw a circle in it
circle = dip.Image(img.Sizes(), 1, 'SFLOAT')
circle.Fill(0)
dip.DrawBandlimitedBall(circle, diameter=22.3, origin=p, value=1, mode='empty')
circle /= dip.Maximum(circle)
# Blend: img * (1-circle) + circle * color
img *= 1 - circle
img += circle * dip.Create0D([0,255,0]) # we make the circle green here
img.Show()
dip.ImageWrite(img, 'so.jpg')
(Note that the circle actually looks better without the JPEG compression artifacts.)
You could draw the circle directly in the image, but this function adds the circle values to the image, it doesn't attempt to blend, and so you'd get a much worse look for this particular application.

Pygame Rotation size issues

I am trying to rotate an image so it is facing the mouse at all times and I am noticing that the image size is changing. I am trying to troubleshoot but am having no luck. I would love some advice.
Here is what I am using:
ang = 360 - math.atan2(mousey - 540, mousex - 960) * 180 / math.pi
rotcircle = pygame.transform.scale(pygame.transform.rotate(redcircle,ang), [100, 100])
rect = rotcircle.get_rect(center=(960,540))
screen.blit(rotcircle,rect)
Just think about it. If you rotated an image 45 degrees, for example, the surface would naturally have to be larger in order to accommodate the corners, which would otherwise stick out of the original surface's bounds. So, if you rotate the image, it's going to be larger, just as the documentation says:
Unless rotating by 90 degree increments, the image will be padded larger to hold the new size. If the image has pixel alphas, the padded area will be transparent. Otherwise pygame will pick a color that matches the Surface colorkey or the topleft pixel value.
You're explicitly forcing the surface to be 100x100, meaning that the closer the image gets to being rotated by 45 degrees (again, for example), the smaller it'll appear. The easy solution is to stop resizing the image, or use another surface that's large enough to hold the rotated image and then blit the rotated image onto it.

Best way to render pixels to the screen in python?

I'm writing an interactive (zoom/pan) mandelbrot set viewer in python, and I'm having some performance issues. I'm currently using pyglet and PyOpenGL to render the pixels since I like how it handles mouse events. I generate the pixel values using numpy, and after some searching on stack exchange/docs/other places, I'm currently using glDrawPixels to draw the pixels. The application is horribly slow, taking ~1.5s to draw. I've heard that using textures is much faster, but I have no experience with them, and learning that much OpenGL seems like it should be unnecessary. Another approach I have considered is using vertex lists and batched rendering with pyglet, but it seems wrong to created a new GL_POINT at every single pixel on the screen. Am I going about this all wrong? Is there a better way to render something to the screen when pixels change so frequently? Code below:
# this code all is in a class that subclasses pyglet.window.Window
# called every 1/10.0 seconds, update 10 frames
def update_region(self):
# this code just computes new mandelbrot detail
if self.i < self.NUM_IT:
for _ in range(10): # do 10 iterations every update, can be customizable
self.z = np.where(np.absolute(self.z) < self.THRESHOLD,
self.z ** 2 + self.reg, self.z)
self.pixels = np.where(
(self.pixels == self.NUM_IT) & (np.absolute(self.z) >
self.THRESHOLD), self.i, self.pixels)
self.i = self.i + 1
def update_frame(self, x, y):
self.update_region()
# color_pixels is what will actually be rendered
self.color_pixels = self.cmap.to_rgba(self.pixels).flatten()
def on_draw(self): # draw method called every update (set to .1s)
start = time.time()
glClear(GL_COLOR_BUFFER_BIT)
glDrawPixels(2 * self.W, 2 * self.H, GL_RGBA, GL_FLOAT, (GLfloat * len(self.color_pixels))(*self.color_pixels))
glutSwapBuffers()
print('on_draw took {0} seconds'.format(time.time() - start))
Are you sure it's the glDrawPixels slowing you down? In your code for update_frame there's a cmap.to_rgba() which I assume is mapping the single value calculated by Mandelbrot into an RGB triple, and then whatever .flatten() does. Copying the entire image, twice, won't help.
For drawing raster images that don't need 3D scaling, pyglet has the image module and .blit()
You are right that a vertex list of points would not help.
Loading the image into a texture would be a bit more OpenGL code, but not too much. You could then zoom in OpenGL, and do the Mandelbrot -> RGB conversion on the GPU as it is drawn in a fragment shader.

Negative coordinates in pygame

What would be the best way to use negative coordinates in pygame?
At the moment I have a surface that is 1.5 times the original surface then everything that needs to be drawn is shifted up by a certain amount (to ensure the negative coordinates become positive) and drawn.
Is there an easier/alternate way of doing this?
A simple solution is to write a linear mapping function from world coordinates to pygame screen coordinates
def coord(x,y):
"Convert world coordinates to pixel coordinates."
return 320+170*x, 400-170*y
and use it when drawing all world objects. Have a look here for a complete example.
There is no way to move the origin of a surface from 0,0.
Implement your own drawing class which transforms all the coordinates passed in into the space of the surface.
If it's similar to an RPG map situation, where you have world coordinates and screen coordinates:
use a function that translates world to local, and vice versa.
But I wasn't sure I'd you were looking for Rect's properties?
rect.bottomright = (width, height) # bottom right at window corner
If you want to use center coordinates to blit, vs top left being (0,0)
ship.rect.center = (20, 30) # don't need to translate by adding w/2 to topleft
See also: Rect.move_ip()

how to fill a part of a circle using PIL?

I'm trying to use PIL for a task but the result is very dirty.
What I'm doing is trying to fill a part of a piece of a circle, as you can see on the image.
Here is my code:
def gen_image(values):
side = 568
margin = 47
image = Image.open(settings.MEDIA_ROOT + "/i/promo_circle.jpg")
draw = ImageDraw.Draw(image)
draw.ellipse((margin, margin, side-margin, side-margin), outline="white")
center = side/2
r = side/2 - margin
cnt = len(values)
for n in xrange(cnt):
angle = n*(360.0/cnt) - 90
next_angle = (n+1)*(360.0/cnt) - 90
nr = (r * values[n] / 5)
max_r = r
min_r = nr
for cr in xrange(min_r*10, max_r*10):
cr = cr/10.0
draw.arc((side/2-cr, side/2-cr, side/2+cr, side/2+cr), angle, next_angle, fill="white")
return image
It's been a while since I used PIL, but in various other graphics libraries, there's often an aliasing problem when drawing arcs.
Have you tried enabling anti-aliasing or drawing with thicker lines?
[Edit] Having a quick look over the PIL library, I think you're right about line width etc.
Sounds like the easiest thing to do here is to build up a polygon which covers each area. So a pair of points at each end and then a load round the middle to stop the edges looking jagged. Does that make sense?
Instead of erasing with white, consider drawing a mask of just the areas you want to show. Here's an example of this for a circular mask.
How do I generate circular thumbnails with PIL?
You draw a circle on a separate image and then remove the slice of the circle you don't want by creating a triangle over that area and removing it from the image (make it transparent). Then you copy this circle segment into the image where you want it.
If you want a partial ring, draw a circle and then draw a smaller circle to cut from the first, then use radial lines to make a triangle to remove the parts you don't need in the same way.

Categories