Randomizing Algorithm for a Pointillism painting in Python - python

NOTE: This is a school-related assignment and I am in no capacity looking for a direct answer. Looking for support on coming up with algorithms as this is my first time with such a question
Programs Intended Purpose: Take a command line provided image and scale it up by a unit of 5. Use the RGB values from the original image to re-create it in a randomized fashion.
Algorithm Attempts:
Test image is 250x250 scaling up to 1250x1250. I've tried to break this into sections of 50(original image side divided by 5) and then use (r + g + b)/5 number of circles to generate the color needed. Ex: Color: (100,50,5) would use 20 red circles, 10 green circles, 1 blue circle in the 50x50 space, (100+50+5)/5 = 31, 20 + 10 + 1 = 31. The x and y coordinates of these circles inside the 50x50 space should be random.
My main issue here has been putting this into code.
Code Attempt 1: Is not related to algorithm, is simply attempt to print image using pygame.draw.circle(this is what I am required to use to make the circle)
import pygame
import sys
import random
image_name = sys.argv[1]
#Import Image
src_image = pygame.image.load(image_name)
(w,h) = src_image.get_size()
window = pygame.display.set_mode((w*5,h*5))
#Nested Loop To Iterate Through Rows And Columns Of Pixels
for y in range(h):
for x in range(w):
(r,g,b,_) = src_image.get_at((x,y))
print(f"{r},{g},{b},x:{x},y:{y}")
pygame.draw.circle(window,(r,g,b),(x*5,y*5),2)
pygame.display.update()
pygame.time.delay(5000)

Possible Solution, works as intended but is still a bit messy due to the randomization:
By dividing the r, g and b values by the scaling value, in this case 5, we get the number of circles needed to be drawn of each color in each block which is broken down to increments of 15. (reduced from 50 to allow more curves and edges of images to be shown as they were hidden with a randomization area of 50). Further luminance calculation is used to find any dark spots, specifically black, and prevent color from being printed in that section. 3 while loops are used to draw each colors separately as they are all drawn a different amount of times.
import pygame
import sys
import random
pygame.init()
image_name = sys.argv[1]
#Import Image
src_image = pygame.image.load(image_name)
#Get image size
(w,h) = src_image.get_size()
#Scale up and display window
window = pygame.display.set_mode((w*5,h*5))
#Nested Loop To Iterate Through Rows And Columns Of Pixels
for y in range(h):
for x in range(w):
#Get rgb values at x and y of image
(r,g,b,_) = src_image.get_at((x,y))
#chck if area is black
lum = (0.2126 * r + 0.7152 * g + 0.0788 * b)*255
#Calculate required number of circles
a = int(r/5)
k = int(g/5)
d = int(b/5)
#draw required number of circles
while(a > 0):
if(lum > 0.625):
pygame.draw.circle(window,(255,0,0),(random.randint((x*5)-15,(x*5)),random.randint((y*5)-15,(y*5))),1)
a-=1
while(k > 0):
if(lum > 0.625):
pygame.draw.circle(window,(0,255,0),(random.randint((x*5)-15,(x*5)),random.randint((y*5)-15,(y*5))),1)
k-=1
while(d > 0):
if(lum > 0.625):
pygame.draw.circle(window,(0,0,255),(random.randint((x*5)-15,(x*5)),random.randint((y*5)-15,(y*5))),1)
d-=1
#update screen
pygame.display.update()
#Keep window open until closed by user
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()

You have to initialize pygame, before you can use any pygame feature:
pygame.init()
src_image = pygame.image.load(image_name)
You have to handle the events in the application loop. See pygame.event.get() respectively pygame.event.pump():
For each frame of your game, you will need to make some sort of call to the event queue. This ensures your program can internally interact with the rest of the operating system.
It is recommended to use an application loop.
Create a list of colors and shuffle it with random.shuffle:
import pygame
import sys
import random
pygame.init()
#Import Image
image_name = sys.argv[1]
src_image = pygame.image.load(image_name)
(w,h) = src_image.get_size()
window = pygame.display.set_mode((w*5,h*5))
for y in range(h):
for x in range(w):
(r, g, b, _) = src_image.get_at((x,y))
lum = (0.2126 * r + 0.7152 * g + 0.0788 * b)*255
if lum > 0.625:
a, k, d = r // 5, g // 5, b // 5
colors = [(255,0,0) for _ in range(a)] + [(0,255,0) for _ in range(k)] + [(0,0,255) for _ in range(d)]
random.shuffle(colors)
for c in colors:
pygame.draw.circle(window,c,(random.randint((x*5)-15,(x*5)),random.randint((y*5)-15,(y*5))),1)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.display.update()
pygame.time.delay(100)
pygame.quit()

Related

pygame pixel processing is slow [duplicate]

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.

Pygame doesn't draw rectangles on the screen after a specific threshold

I'm trying to make a visualisation for various sorting algorithms and I was playing around with Pygame to try and draw the rectangles that I need.
In the below code, the user is given multiples inputs: the lowest value of the list to be sorted, the highest value, and the number of elements the list is going to have. The elements are going to be randomly generated.
Then I'm getting the user's screen size so that I can have an appropriate window for the visualisation. Based on the visualisation window and the user's input, I'm setting up the width and height of the rectangles, so that each rectangle has the same width and that they are scaled based on the highest value.
Almost everything is nice and fine with this approach, but there's one thing that I can't figure out. It seems that setting the number of elements (n, in the code below) too high, the rectangles are not being drawn.
My asumption is that after a specific threshold, RECT_W, which is the width of the rectangles, becomes to small for Pygame to draw it.
What options do I have to solve it, except of having the number of elements smaller than a specific value?
import random
import pygame
import color_constants as colors
import ctypes
import copy
from pygame.locals import *
# Read data based on user's input
def readData():
listOfNumbers = []
data = dict()
print("Lowest Value: ")
numLow = int(input())
print("Highest Value: ")
numHigh = int(input())
print("Length of list: ")
n = int(input())
for i in range(0, n):
listOfNumbers.append(random.randint(numLow, numHigh))
origLst = copy.copy(listOfNumbers)
data.update({'lst': origLst})
data.update({'numLow': numLow})
data.update({'numHigh': numHigh})
data.update({'n': n})
data.update({'sorted': listOfNumbers})
return data
if __name__ == "__main__":
data = readData()
# Getting the user's screen size
user32 = ctypes.windll.user32
SCREENSIZE = user32.GetSystemMetrics(0)-100, user32.GetSystemMetrics(1)-100
SCREEN_W = SCREENSIZE[0]
SCREEN_H = SCREENSIZE[1]
# Setting and scaling the size of rectangle based on the number of elements (n)
# and the highest number (numHigh)
RECT_W = SCREEN_W // data['n']
RECT_H = SCREEN_H / (data['numHigh'])
# Setting up the color literals
RED = (255, 255, 255)
GRAY = (0, 0, 0)
pygame.init()
screen = pygame.display.set_mode(SCREENSIZE)
running = True
while running:
for event in pygame.event.get():
if event.type == QUIT:
running = False
screen.fill(GRAY)
for i in range(data['n']):
rect = Rect(i*RECT_W, 0, RECT_W, RECT_H * data['lst'][i])
rect.bottom = SCREEN_H
pygame.draw.rect(screen, RED, rect)
pygame.display.flip()
If data['n'] is greater than SCREEN_W, RECT_W is 0. A coordinate is truncated when drawing. You cannot draw a fraction of a pixel. The size of the rectangle can only be integral (0, 1, 2 ...). Hence, you cannot draw a rectangle with a size less than 1.
You can draw the rectangles on a large surface and scale down the surface. However, the rectangles get blurred. So, this is no good option.

Why are pixels fuzzy in Pygame?

I am just getting started with Pygame, and I did a little test of just printing pixels in random spots. However, I noticed that the pixels don't seem to be single pixels at all, but 'fuzzy' blobs of a few pixels, as shown in the image. Here's the code I used to draw the pixels:
Is there any way to just display single pixels?
Edit: Here's the whole code I used:
import pygame.gfxdraw
import pygame
import random
width = 1000
height = 1000
screen = pygame.display.set_mode((width, height))
clock = pygame.time.Clock()
running = True
while running:
x = random.randint(0,1000)
y = random.randint(0,1000)
pygame.gfxdraw.pixel(screen, x, y, (225,225,225))
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
clock.tick(240)
more fuzzy pixels
pygame.gfxdraw.pixel(surface, x, y, color) will draw a single pixel on the given surface.
Also you will need to add import pygame.gfxdraw.
EDIT: Full code:
import pygame.gfxdraw
import pygame
import random
width = 1680
height = 1050
screen = pygame.display.set_mode((width, height))
clock = pygame.time.Clock()
running = True
x = [i for i in range(width - 10)]
x_full = [i for i in range(width)]
y = 100
y_full = [i for i in range(height // 2)]
while running:
for i in x:
for j in y_full:
pygame.gfxdraw.pixel(screen, i, j, pygame.Color("red"))
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
clock.tick(1)
Try to test it this way, I set the window size to fit my monitor resolution so it goes fullscreen. x_full and y should give you horizontal line. And if you subtract, for example, 10 you will get slightly shorter line, and vice-versa with y_full and some random x. Also using (width//2, height//2) will cover exactly quarter of the screen. I think it is accurate and that pygame.gfxdraw.pixel(screen, i, j, pygame.Color("red")) displays only single pixel as it should.
In your code you are using random to display pixels and it adds them 240 per second so you are very fast ending up with bunch of pixels at random positions resulting to have pixels close to each other looking as a "bigger one". I think this is what was happening here. Please someone correct me if I am wrong.
Also make small window e.g. (100, 100) and draw one pixel at (50, 50) this way it can be more easily seen. If you are on windows use magnifier to test it.
IMPORTANT:
While testing this with huge number of pixels do it OUTSIDE of the loop because it will consume much processor power to display them.
Hope this answers your question

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)]

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