pygame pixel processing is slow [duplicate] - python

This question already has answers here:
Pygame: Draw single pixel
(6 answers)
Closed 4 months ago.
I'm trying to iterate through the numpy array and assigning a 0 - 255 value based on the distance to the mouse.
WIDTH and HEIGHT are in this case set to 400 and GRID[] is a numpy matrix with WIDTH and HEIGHT dimensions.
I'm using the window.set_at() function to draw each pixel on the screen with the color stored in the numpy matrix, I'm getting about 5 FPS.
Is there a more efficient way to handle this type of pixel processing, or should I switch to something like c++ & SFML
#update pixels
for y in range(HEIGHT):
for x in range(WIDTH):
#get color based on distance to mouse; 0 -> 255
mousePosition = pg.mouse.get_pos()
dx = mousePosition[0] - x
dy = mousePosition[1] - y
d = math.sqrt(abs(dx ** 2 + dy ** 2))
#constraining the distance value between 0 - 255
c = min(max(d, 0), 255)
GRID[x,y] = c
#draw pixels
for y in range(HEIGHT):
for x in range(WIDTH):
c = GRID[x,y]
window.set_at((x, y), (c, c, c))

What constitutes fast enough?
Tidying up your code to create a minimal example with your 400x400 resolution:
import math
import time
import pygame
width, height = 400, 400
pygame.init()
screen = pygame.display.set_mode((width, height))
screen.fill(pygame.Color("black"))
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Graphics
# update pixels
start = time.time()
mx, my = pygame.mouse.get_pos()
for y in range(height):
for x in range(width):
distance = math.sqrt( (mx - x)**2 + (my - y)**2 ) # 2.2 FPS 0.455s
#constrain the distance value between 0 - 255
c = min(max(distance, 0), 255)
screen.set_at((x, y), (c, c, c))
# Update Screen
pygame.display.set_caption(f"FPS: {clock.get_fps():.1f} Pixel Processing {time.time() - start:.3f} s")
pygame.display.update()
pygame.quit()
This results in 2.2 FPS on my PC.
The Python math module has a function that calculates the hypotenuse, so it's probably a little more optimised. Change the calculation to:
distance = math.hypot(mx - x, my - y)
This increases my frame rate by 50% to 3, probably still too slow.
We can be smarter about the pixels we modify, instead of clamping the distance to 255, fill the screen with white and then if the distance is greater than 255, don't change the pixel.
screen.fill(pygame.Color("white"))
for y in range(height):
for x in range(width):
distance = math.hypot(mx - x, my - y)
if distance <= 255: # don't set far pixels
c = round(distance)
screen.set_at((x, y), (c, c, c))
This increases my frame rate to 12 when the mouse is in the corner, 8 in the middle. Perhaps this is approaching usable.
If you look at the documentation for surface.set_at(), it says that using get_at() and set_at is too slow and recommends using PixelArray or SurfArray. So we can create a SurfArray:
surfarray = pygame.surfarray.pixels3d(screen)
Then to set the pixel values, we replace screen.set_at():
surfarray[x, y] = (c,c,c)
Surprisingly and unfortunately this doesn't change the frame rate significantly. Maybe this requires hardware acceleration.
I also tried manually locking the surface before iterating through the pixels as suggested in the docs, but this made no significant improvement.
So lets consider what we're doing, drawing the same circle wherever the mouse is every frame. It will be faster if we draw the circle once, and then blit it every frame centered on the mouse position. To create the circle, it's similar to what's already been done:
size = 255 * 2
dist_image = pygame.Surface((size, size), pygame.SRCALPHA)
for y in range(size):
for x in range(size):
distance = math.hypot(255 - x, 255 - y)
if distance <= 255:
c = round(distance)
dist_image.set_at((x, y), (c, c, c))
Then our graphics update logic becomes:
screen.fill(pygame.Color("white"))
dist_rect = dist_image.get_rect(center=pygame.mouse.get_pos())
screen.blit(dist_image, dist_rect)
This runs at 60 FPS (max) and takes almost no processing time:

pygame pixel processing is slow
Yes it is. So don't process pixels.
Given your colour resolution of 256, this means we're really dealing with a bunch of coloured circles around the mouse cursor. By considering only these circles, you're specifically not processing all those other pixels that can never be anything other than colour-zero.
The code below implements the program by drawing circles about the mouse cursor where the circle radius is the known distance. So we're drawing a circle in the same "distance-colour". Assuming the circle is drawn using the midpoint circle algorithm, this means that it only needs to calculate 1/8 of the pixels, and the rest are just quadrant (octant?) reflected about an axis of circle-symmetry.
import pygame
import random
# Window size
WINDOW_WIDTH = 600
WINDOW_HEIGHT = 600
###
### MAIN
###
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ) )
pygame.display.set_caption("Mouse Distance")
# Main loop
clock = pygame.time.Clock()
running = True
while running:
time_now = pygame.time.get_ticks()
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
running = False
# Paint the screen in a gradient centred about the mouse
window.fill( ( 0, 0, 0 ) ) # max distance
mouse_pos = pygame.mouse.get_pos()
for distance in range( 255 ):
colour = ( 255-distance, 255-distance, 255-distance )
pygame.draw.circle( window, colour, mouse_pos, distance, 2 ) # use a width of 2 so there's no "holes"
pygame.display.flip()
# Clamp FPS
clock.tick(60)
pygame.quit()
I don't have time to make the change right now, but this example should draw to a surface, and then blit() that surface to the window for painting. That way we only need to re-compute the surface when the mouse moves.

Related

Making my own grid in python

I'm new one in coding, so I expect that this is easy one, but I still can't figure this out.
So I'm trying to make a program that animates a diagram, but the problem is when am making animation with equation (0,0) obviously in upper left corner.
I need to make a grid that will represent this, so I cant input my equation and get right animation depends on this grid's numbers Grid here
UPD:
Sorry for being less specific than I should.
The goal is to make a program that animates HR diagram, outputs luminosity and temperature after you inputs solar mass.
I didn't input equation so far because I'm trying to figure out how pygame animation works.
What I got so far trying to animate diagonal line:
import pygame
from pygame.locals import *
pygame.init()
width = 600
height = 600
screen = pygame.display.set_mode((width, height))
background = pygame.image.load("background.png")
point = pygame.image.load("point.png")
clock = pygame.time.Clock()
speed = 100
x = 0
y = 0
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
quit()
screen.blit(background, (0,0))
screen.blit(point, (x,y),)
milli = clock.tick()
second = milli/1000.
dm=second * speed
x += dm
y = x
print (x, y)
if x > 600 or y > 600:
x = 0
y = 0
pygame.display.update()
So I need to make axis like in diagram, so I can just type down the equation and make correct animation and correct outputs
Use a 2 dimensional array to represent a grid structure in python.
w, h = 10000, 50000;
grid = [[0 for x in range(w)] for y in range(h)]

Drawing pixel to pixel python 2.7 [duplicate]

I'm looking for method that allow me to draw single pixel on display screen. For example when I click mouse, I want the position of clicked pixel to change color. I know how to read mouse pos, but I could not find simple pixel draw ( there is screen.fill method but it's not working as I want).
You can do this with surface.set_at():
surface.set_at((x, y), color)
You can also use pygame.gfxdraw.pixel():
from pygame import gfxdraw
gfxdraw.pixel(surface, x, y, color)
Do note, however, the warning:
EXPERIMENTAL!: meaning this api may change, or dissapear in later
pygame releases. If you use this, your code will break with the next
pygame release.
You could use surface.fill() to do the job too:
def pixel(surface, color, pos):
surface.fill(color, (pos, (1, 1)))
You can also simply draw a line with the start and end points as the same:
def pixel(surface, color, pos):
pygame.draw.line(surface, color, pos, pos)
The usual method of drawing a point on a Surface or the display is to use [`pygame.Surface.set_at']:
window_surface.set_at((x, y), my_color)
However, this function is very slow and leads to a massive lack of performance if more than 1 point is to be drawn.
Minimal example where each pixel is set separately: repl.it/#Rabbid76/PyGame-DrawPixel-1
import pygame
pygame.init()
window = pygame.display.set_mode((300, 300))
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.fill(0)
rect = pygame.Rect(window.get_rect().center, (0, 0)).inflate(*([min(window.get_size())//2]*2))
for x in range(rect.width):
u = x / (rect.width - 1)
color = (round(u*255), 0, round((1-u)*255))
for y in range(rect.height):
window.set_at((rect.left + x, rect.top + y), color)
pygame.display.flip()
pygame.quit()
exit()
Another option is to use a "pygame.PixelArray" object. This object enables direct pixel access to Surface objects. A PixelArray pixel item can be assigned directly. The pixel can be accessed by subscription. The PixelArray locks the Surface, You have to close() it when you have changed the pixel:
pixel_array = pygame.PixelArray(window_surface)
pixel_array[x, y] = my_color
pixel_array[start_x:end_x, start_y:end_y] = my_color
pixel_array.close()
Minimal example that set one line of pixels at once: repl.it/#Rabbid76/PyGame-DrawPixel-2
import pygame
pygame.init()
window = pygame.display.set_mode((300, 300))
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.fill(0)
rect = pygame.Rect(window.get_rect().center, (0, 0)).inflate(*([min(window.get_size())//2]*2))
pixel_array = pygame.PixelArray(window)
for x in range(rect.width):
u = x / (rect.width - 1)
color = (round(u*255), 0, round((1-u)*255))
pixel_array[rect.left + x, rect.top:rect.bottom] = color
pixel_array.close()
pygame.display.flip()
pygame.quit()
exit()
For those who are interested in a more modern answer to the question you can use pygame.draw.circle() to draw a single pixel at a given position (or center).
pygame.draw.circle(surface, color, center, 0)
The documentation specifically says:
radius (int or float) -- radius of the circle, measured from the center parameter, a radius of 0 will only draw the center pixel
One way of doing that is to draw a line staring and ending at the same point.
pygame.draw.line(surface, (255,255,255), (x,y), (x,y))
draw a single coloured pixel
def drawPoint(x,y,color):
s = pygame.Surface((1,1)) # the object surface 1 x 1 pixel (a point!)
s.fill(color) # color as (r,g,b); e.g. (100,20,30)
# now get an object 'rectangle' from the object surface and place it at position x,y
r,r.x,r.y = s.get_rect(),x,y
screen.blit(s,r) # link the object rectangle to the object surface
of course you have to call: pygame.display.update() once you have
drawn all the points you need, don't call update at every single point.
# with this function, you can draw points and change the yer size
def point(surface, color, x, y, size):
'''the surface need the information of the pygame window'''
for i in range(0, size):
pygame.draw.line(surface, color, (x, y-1), (x, y+2), abs(size))

Anti-aliased Arc Pygame

I am trying to code a simple circle timer in Python using Pygame.
At the moment it looks like this:
As you can see, the blue line is very wavy and has white dots in it. I am achieving this blue line by using pygame.draw.arc() function, but it is not anti-aliased and looks bad. I would like it to be anti-aliased, but gfxdraw module which should let me achieve this, doesn't support arc width selection. Here's code snippet:
pygame.draw.arc(screen, blue, [center[0] - 120, center[1] - 120, 240, 240], pi/2, pi/2+pi*i*koef, 15)
pygame.gfxdraw.aacircle(screen, center[0], center[1], 105, black)
pygame.gfxdraw.aacircle(screen, center[0], center[1], 120, black)
I did it creating the arc with a polygon.
def drawArc(surface, x, y, r, th, start, stop, color):
points_outer = []
points_inner = []
n = round(r*abs(stop-start)/20)
if n<2:
n = 2
for i in range(n):
delta = i/(n-1)
phi0 = start + (stop-start)*delta
x0 = round(x+r*math.cos(phi0))
y0 = round(y+r*math.sin(phi0))
points_outer.append([x0,y0])
phi1 = stop + (start-stop)*delta
x1 = round(x+(r-th)*math.cos(phi1))
y1 = round(y+(r-th)*math.sin(phi1))
points_inner.append([x1,y1])
points = points_outer + points_inner
pygame.gfxdraw.aapolygon(surface, points, color)
pygame.gfxdraw.filled_polygon(surface, points, color)
The for loop could certainly be created more elegantly with a generator, but I am not very sophisticated with python.
The arc definitely looks nicer than pygame.draw.arc, but when I compare it to the screen rendering on my mac, there is room for improvement.
I am not aware of any pygame function that would solve this problem, meaning you basically have to program a solution yourself (or use something other than pygame), since draw is broken as you've noted and gfxdraw won't give you the thickness.
One very ugly but simple solution is to draw multiple times over the arc segments, always slightly shifted to "fill in" the missing gaps. This will still leave some aliasing at the very front of the timer arc, but the rest will be filled in.
import pygame
from pygame.locals import *
import pygame.gfxdraw
import math
# Screen size
SCREEN_HEIGHT = 350
SCREEN_WIDTH = 500
# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREY = (150, 150, 150)
RED = (255,0,0)
# initialisation
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
done = False
clock = pygame.time.Clock()
# We need this if we want to be able to specify our
# arc in degrees instead of radians
def degreesToRadians(deg):
return deg/180.0 * math.pi
# Draw an arc that is a portion of a circle.
# We pass in screen and color,
# followed by a tuple (x,y) that is the center of the circle, and the radius.
# Next comes the start and ending angle on the "unit circle" (0 to 360)
# of the circle we want to draw, and finally the thickness in pixels
def drawCircleArc(screen,color,center,radius,startDeg,endDeg,thickness):
(x,y) = center
rect = (x-radius,y-radius,radius*2,radius*2)
startRad = degreesToRadians(startDeg)
endRad = degreesToRadians(endDeg)
pygame.draw.arc(screen,color,rect,startRad,endRad,thickness)
# fill screen with background
screen.fill(WHITE)
center = [150, 200]
pygame.gfxdraw.aacircle(screen, center[0], center[1], 105, BLACK)
pygame.gfxdraw.aacircle(screen, center[0], center[1], 120, BLACK)
pygame.display.update()
step = 10
maxdeg = 0
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
maxdeg = maxdeg + step
for i in range(min(0,maxdeg-30),maxdeg):
drawCircleArc(screen,RED,(150,200),119,i+90,max(i+10,maxdeg)+90,14)
#+90 will shift it from starting at the right to starting (roughly) at the top
pygame.display.flip()
clock.tick(2) # ensures a maximum of 60 frames per second
pygame.quit()
Note that I have copied degreesToRadians and drawCircleArc from https://www.cs.ucsb.edu/~pconrad/cs5nm/08F/ex/ex09/drawCircleArcExample.py
I do not generally recommend this solution, but it might do in a pinch.
You are right, some pygame rendering functions do indeed suck, so you can achieve something like this with PIL instead.
pie_size = (40, 40) # defining constants
pil_img = PIL.Image.new("RGBA", pie_size) # PIL template image
pil_draw = PIL.ImageDraw.Draw(pil_img) # drawable image
pil_draw.pieslice((0, 0, *[ps - 1 for ps in pie_size]), -90, 180, fill=(0, 0, 0)) # args: (x0, y0, x1, y1), start, end, fill
This will create a PIL shape. Now we can convert it to pygame.
data = pil_img.tobytes()
size = pil_img.size
mode = pil_img.mode
pygame_img = pygame.image.fromstring(data, size, mode).convert_alpha()
But don't forget to pip install pillow and
import PIL.Image
import PIL.ImageDraw
Ok, this is really old, but why not try to draw pies instead. For example draw a pie, then an unfilled circle as the outside ring and then a filled circle as the inside and another unfilled circle as the inside ring.
So pie -> unfilled circle -> filled circle -> unfilled.
The order is somewhat arbitrary but if u still have this problem give it a try. (Btw I haven't tried it but I think it will work)
For my own uses, I wrote a simple wrapper function, and to deal with the spotty arc drawing, I used an ugly loop to draw the same arc several times.
def DrawArc(surface, color, center, radius, startAngle, stopAngle, width=1):
width -= 2
for i in range(-2, 3):
# (2pi rad) / (360 deg)
deg2Rad = 0.01745329251
rect = pygame.Rect(
center[0] - radius + i,
center[1] - radius,
radius * 2,
radius * 2
)
pygame.draw.arc(
surface,
color,
rect,
startAngle * deg2Rad,
stopAngle * deg2Rad,
width
)
I'm aware this is not a great solution, but it works alright for my uses.
An important note is I added that "width -= 2" to hopefully preserve the intended size of the arc at least a little more accurately, but this results in increasing the minimum width by 2.
In your case, you might want to consider doing something more to fix the issues this results in.
If the start and end aren't all that important, one can create many circles following an arc trajectory and when done ie small circles drawn 360 time, you finally have a big circle with no wavy effect:
MWE:
#!/usr/bin/env python3
import pygame
import math
# Initialize pygame
pygame.init()
# Set the screen size
screen = pygame.display.set_mode((400, 300))
# Set the center point of the arc
center_x = 200
center_y = 150
arc_radius = 100
circle_radius = 6
# Set the start and stop angles of the arc
start_angle = 0
stop_angle = 360
angle_step = 1
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Clear the screen
screen.fill((0, 0, 0))
# Draw the overlapping circles
for i in range(start_angle, stop_angle, angle_step):
angle = math.radians(i)
x = center_x + arc_radius * math.cos(angle)
y = center_y + arc_radius * math.sin(angle)
pygame.draw.circle(screen, "red", (int(x), int(y)), circle_radius)
# Update the display
pygame.display.flip()
pygame.quit()
Having a start_angle and stop_angle of 0 to 360 respectively yields a fill circle with an output:
To change it to a 1/3 of a circle, one would change the stop_angle from 360 to 120 (1/3 x 360 = 120) and this would then yield:

Draw a map based on values from the source point

I want to make a map. So I have values in an array and I want to use those values to make a map. So the values are from 1 to 250 and there are 120 of them (one for 3 degrees). So how do I calculate where should I put my points based on the values in the right order (from 0 to 360 degrees)?
Here is my code so far:
import pygame
white = (255, 255, 255)
black = (0, 0, 0)
import sys
pygame.init()
screen = pygame.display.set_caption("Drawing is fun!")
screen = pygame.display.set_mode((500, 500))
screen.fill(white)
pygame.draw.circle(screen, black, (250, 250), 250, 1)
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit();
sys.exit();
pygame.display.update()
clock.tick(60)
I don't have the array yet but I will have it in the near future. Let's say that the array is named values.
For those who didn't understand my question:
- I have a 500 x 500 pygame canvas.
- I have 120 values which represent 3 degrees each. We got them by spinning an ultra sonic sensor around and reading the values.
- I want to create an image on the canvas that reads those values and puts points in the image and then it connects them forming lines.
- It will be like a map generated by an ultrasonic sensor.
So full size of your field is 2w=500, and half-size is w=250
Center of field has coordinates (cx, cy) = (w, w)
Distance are already normalized in you case (max value 250 = w)
If your objects are point in the center angle of the sector, then screen coordinates of object at i-th index are
central_angle = (1.5 + i * 3) * Pi / 180 //in radians
x = cx + Round(values[i] * Cos(central_angle)) //round float value to get integer coordinate
y = cy + Round(values[i] * Sin(central_angle))

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