Printing small rectangles to the screen in a for loop. (pygame) - python

I'm trying to get a code to print small rectangles all over my screen in pygame with the help of for loops, but having trouble. I have solved parts of it with this code but it looks ugly and preforms bad:
x = 0
y = 0
for y_row in range(60):
y = y + 10
pygame.draw.rect(screen, GREEN, [x, y, 5, 5], 0)
for x_row in range(70):
pygame.draw.rect(screen, GREEN, [x, y, 5, 5], 0)
x = x + 10
x = 0
To start of, I do not believe I have to assign a value to x and y if I just can figure out how to implement the value of y_row and x_row at x and y's places instead, now it increases with 1, it should increase with 10, than I can implement it instead.
Another problem with the code is that it leaves a blank row at the top, this is because I had to add the y = y + 10 above the pygame draw, otherwise it just printed one rectangle there witch made it more visible.
The template I'm using to get the code working you can find Here.

Drawing 4,200 rectangles to the screen every 60th of a second is probably a significant task for the CPU. I suspect that the pygame.draw.rect() function is fairly high-level and calls are not batched by pygame making it sub-optimal, there is a hint in the documentation (https://www.pygame.org/docs/ref/draw.html#pygame.draw.rect) that Surface.fill(color, rect=None, special_flags=0) can be hardware accelerated and may be a faster option if you're filling the rectangles.
Note: the code examples below are pseudo ... just means you need to fill in the gaps.
You only need 1 call to pygame.draw.rect per iteration of the loop not 2 as you have now, e.g.
for row in rows:
y = ...
for col in cols:
x = ...
... draw rect ...
One easy win for performance is to not draw anything that's off-screen, so test your x, y coordinates before rendering, e.g:
screen_width = 800
screen_height = 600
for ...
y = y += 10
if y > screen_height:
break
for ...
x += 10
if x > screen_width:
break
... draw block ...
The same approach could also be used (with a continue) to implement an offset (e.g a starting offset_x, offset_y value) where rectangles with negative x, y values are not rendered (the test is not x < 0 however, but x < -block_size).
There's nothing wrong with calculating the x and y values from a loop index as you are doing, it's often useful to have an index (for example the index [row][col] might give you the location of data for a tile in a 2D matrix representing game tiles). I would calculate the x, y values myself from the indexes using a multiplier (this also solves the blank first row issue):
block_size = 10
for row in ...
y = row * block_size
if y > screen_height:
break
for col in ...
x = col * block_size
if x > screen_width:
break
... draw block ...
If you're using Python2 then you might consider using xrange to predefine the loop ranges to improve performance (though I imagine only a small amount and as always with optimization testing the performance difference is key). For example:
rows = xrange(60)
cols = xrange(70)
for row in rows:
...
for cols in cols:
... draw block ...

As #bshuster13 mentioned you can use pythons range() function and pass an optional step and stop argument to create a list containing arithmetic progressions.
numberOfRows = 60
numberOfColumns = 70
stepBetweenRects = 10
for y in range(0, numberOfRows * stepBetweenRects, stepBetweenRects):
for x in range(0, numberOfColumns * stepBetweenRects, stepBetweenRects):
pygame.draw.rect(screen, GREEN, (x, y, 5, 5), 0)

Related

Scanning lists more efficiently in python

I have some code, which works as intended, however takes about 4 and a half hours to run, I understand that there are about 50 billion calculations my poor pc needs to do but I thought it would be worth asking!
This code gets an image, and wants to find every possible region of 331*331 pixels in the given image, and find how many black pixels there are in each, I will use this data to create a heatmap of black pixel density, and also a list of all of the values found:
image = Image.open(self.selectedFile)
pixels = list(image.getdata())
width, height = image.size
pixels = [pixels[i * width:(i+1) * width] for i in range(height)]
#print(pixels)
rightShifts = width - 331
downShifts = height - 331
self.totalRegionsLabel['text'] = f'Total Regions: {rightShifts * downShifts}'
self.blackList = [0 for i in range(0, rightShifts*downShifts)]
self.heatMap = [[] for i in range(0, downShifts)]
for x in range(len(self.heatMap)):
self.heatMap[x] = [0 for i in range(0, rightShifts)]
for x in range(rightShifts):
for y in range(downShifts):
blackCount = 0
for z in range(x + 331):
for w in range(y + 331):
if pixels[z][w] == 0:
blackCount += 1
self.blackList[x+1*y] = blackCount
self.heatMap[x][y] = blackCount
print(self.blackList)
You have several problems here, as I pointed out. Your z/w loops are always starting at the upper left, so by the time you get towards the end, you're summing the entire image, not just a 331x331 subset. You also have much confusion in your axes. In an image, [y] is first, [x] is second. An image is rows of columns. You need to remember that.
Here's an implementation as I suggested above. For each column, I do a full sum on the top 331x331 block. Then, for every row below, I just subtract the top row and add the next row below.
self.heatMap = [[0]*rightShifts for i in range(downShifts)]
for x in range(rightShifts):
# Sum up the block at the top.
blackCount = 0
for row in range(331):
for col in range(331):
if pixels[row][x+col] == 0:
blackCount += 1
self.heatMap[0][x] = blackCount
for y in range(1,downShifts):
# To do the next block down, we subtract the top row and
# add the bottom.
for col in range(331):
blackCount += pixels[y+330][x+col] - pixels[y-1][x+col]
self.heatMap[y][x] = blackCount
You could tweak this even more by alternating the columns. So, at the bottom of the first column, scoot to the right by subtracting the first column and adding the next new column. then scoot back up to the top. That's a lot more trouble.
The two innermost for-loops seem to be transformable to some numpy code if using this package is not an issue. It would give something like:
pixels = image.get_data() # it is probably already a numpy array
# Get an array filled with either True or False, with True whenever pixel is black:
pixel_is_black = (pixels[x:(x+331), y:(y+331)] == 0)
pixel_is_black *= 1 # Transform True and False to respectively 1 and 0. Maybe not needed
self.blackList[x+y] = pixel_is_black.sum() # self explanatory
This is the simplest optimization I can think of, you probably can do much better with clever numpy tricks.
I would recommend using some efficient vector computations through the numpy and opencv libraries.
First, binarize your image so that black pixels are set to zero, and any other color pixels (gray to white) are set to 1. Then, apply a 2D filter to the image of shape 331 x 331 where each value in the filter kernel is (1 / (331 x 331) - this will take the average of all the values in each 331x331 area and assign it to the center pixel.
This gives you a heatmap, where each pixel value is the proportion of non-black pixels in the surrounding 331 x 331 region. A darker pixel (value closer to zero) means more pixels in that region are black.
For some background, this approach uses image processing techniques called image binarization and box blur
Example code:
import cv2
import numpy as np
# setting up a fake image, with some white spaces, gray spaces, and black spaces
img_dim = 10000
fake_img = np.full(shape=(img_dim, img_dim), fill_value=255, dtype=np.uint8) # white
fake_img[: img_dim // 3, : img_dim // 3] = 0 # top left black
fake_img[2 * img_dim // 3 :, 2 * img_dim // 3 :] = 0 # bottom right black
fake_img[img_dim // 3 : 2 * img_dim // 3, img_dim // 3 : 2 * img_dim // 3] = 127 # center gray
# show the fake image
cv2.imshow("", fake_img)
cv2.waitKey()
cv2.destroyAllWindows()
# solution to your problem
binarized = np.where(fake_img == 0, 0, 1) # have 0 values where black, 1 values else
my_filter = np.full(shape=(331, 331), fill_value=(1 / (331 * 331))) # set up filter
heatmap = cv2.filter2D(fake_img, 1, my_filter) # apply filter, which takes average of values in 331x331 block
# show the heatmap
cv2.imshow("", heatmap)
cv2.waitKey()
cv2.destroyAllWindows()
I ran this on my laptop, with a huge (fake) image of 10000 x 10000 pixels, almost instantly.
Sorry I should have deleted this post before you all put the effort in, however, some of these workarounds are really smart and interesting, I ended up coming up with a solution independently that is the same as what Tim Robbers first suggested, I used the array I had and built a second one on which every item in a row is the number of black cells preceding it, and then for each row in a region instead of scanning every item, just scan the preceding value and the final value and you are good:
image = Image.open(self.selectedFile).convert('L') #convert to luminance mode as RGB information is irrelevant
pixels = list(image.getdata()) #get the value of every pixel in the image
width, height = image.size
pixels = [pixels[i * width:(i+1) * width] for i in range(height)] #split the pixels array into a two dimensional array with the dimensions to match the image
#This program scans every possible 331*331 square starting from the top left, so it will move right width - 331 pixels and down height - 331 pixels
rightShifts = width - 331
downShifts = height - 331
self.totalRegionsLabel['text'] = f'Total Regions: {rightShifts * downShifts}' #This wont update till the function has completed running
#The process of asigning new values to values in an array is faster than appending them so this is why I prefilled the arrays:
self.heatMap = [[] for i in range(0, downShifts)]
for x in range(len(self.heatMap)):
self.heatMap[x] = [0 for i in range(0, rightShifts)]
cumulativeMatrix = [] #The cumulative matrix replaces each value in each row with how many zeros precede it
for y in range(len(pixels)):
cumulativeMatrix.append([])
cumulativeMatrix[y].append(0)
count = 0
for x in range(len(pixels[y])):
if pixels[y][x] == 0:
count += 1
cumulativeMatrix[y].append(count)
regionCount = 0
maxValue = 0 #this is the lowest possible maximum value
minValue = 109561 #this is the largest possible minimum value
self.blackList = []
#loop through all possible regions
for y in range(downShifts):
for x in range(rightShifts):
blackPixels = 0
for regionY in range(y, y + 331):
lowerLimit = cumulativeMatrix[regionY][x]
upperLimit = cumulativeMatrix[regionY][x+332]
blackPixels += (upperLimit - lowerLimit)
if blackPixels > maxValue:
maxValue = blackPixels
if blackPixels < minValue:
minValue = blackPixels
self.blackList.append(blackPixels)
self.heatMap[y][x] = blackPixels
regionCount += 1
This brought run time to under a minute and thus solved my problem, however, thank you for your contributions I have learned a lot from reading them!
Try to look into the map() function. It uses C to streamline iterations.
You can speed up your for loops like this:
pixels = list(map(lambda i: x[i*width:(i+1)*width], range(height)))

How can i append multiple values to numPy array?

I am making a snake game in pygame, and i need to make an array of pygame rects. When i was testing the code to see if the basic idea works, it didn't. When it was supposed to print
[[0,0],
[10,0],
[20,0],
and so on until it got to the biggest x value, and then it would add ten to the y value, it just prints the x values when the y value is always 0. I am new to pygame and python, so any help would be appreciated.
My code:
class Grid:
def __init__(self, gridSize):
self.gridSize = gridSize
self.numX = int(screenX / gridSize)
self.numY = int(screenX / gridSize)
self.xList = []
for y in range(0, self.numY * 10, 10):
for x in range(0, self.numX * 10, 10):
self.xList.append((x,y))
if y == 0:
self.array = np.array(self.xList)
else:
np.append(self.array, self.xList)
self.xList = []
print(self.array)
Most (if not all) numpy commands don't modify their arrays in-place. They return a new array, but the old array stays as it is.
Thus, under your else, you'll need
else:
self.array = np.append(self.array, self.xList)
This will update self.array so that it holds the new, appended array.
It also explains why you're only seeing print-outs for y = 0 and not other values. (You could possibly arrive at this same conclusion by debugging and stepping through your code. Maybe next time? :-) )
For starters, you aren't iterating over the Y range:
self.numY = int(screenX / gridSize) needs to be self.numY = int(screenY/ gridSize)

Draw triangle's in pygame given only side lengths

I have three line lengths and I need to plot a triangle on the screen with them.
Say I have:
len1 = 30
len2 = 50
len3 = 70
(these are randomly generated)
I can draw the first line at the bottom like this
pygame.draw.line(screen, red, (500,500), (500+len1,500), 10)
The other two lines will start at (500,500) and (500+len1,500) respectivly and will have the same endpoint but I can't figure out the math to get that location
Converted the formula in Jody Muelaner's answer here to python:
def thirdpoint(a, b, c):
result = []
y=((a**2)+(b**2)-(c**2))/(a*2)
x = math.sqrt((b**2)-(y**2))
result.append(x)
result.append(y)
return result

Ensure that contents of list sums up to 1 for np.random.choice()

The Context
In Python 3.5, I'm making a function to generate a map with different biomes - a 2-dimensional list with the first layer representing the lines of the Y-axis and the items representing items along the X-axis.
Example:
[
["A1", "B1", "C1"],
["A2", "B2", "C2"],
["A3", "B3", "C3"]
]
This displays as:
A1 B1 C1
A2 B2 C2
A3 B3 C3
The Goal
A given position on the map should be more likely to be a certain biome if its neighbours are also that biome. So, if a given square's neighbours are all Woods, that square is almost guaranteed to be a Woods.
My Code (so far)
All the biomes are represented by classes (woodsBiome, desertBiome, fieldBiome). They all inherit from baseBiome, which is used on its own to fill up a grid.
My code is in the form of a function. It takes the maximum X and Y coordinates as parameters. Here it is:
def generateMap(xMax, yMax):
areaMap = [] # this will be the final result of a 2d list
# first, fill the map with nothing to establish a blank grid
xSampleData = [] # this will be cloned on the X axis for every Y-line
for i in range(0, xMax):
biomeInstance = baseBiome()
xSampleData.append(biomeInstance) # fill it with baseBiome for now, we will generate biomes later
for i in range(0, yMax):
areaMap.append(xSampleData)
# now we generate biomes
yCounter = yMax # because of the way the larger program works. keeps track of the y-coordinate we're on
for yi in areaMap: # this increments for every Y-line
xCounter = 0 # we use this to keep track of the x coordinate we're on
for xi in yi: # for every x position in the Y-line
biomeList = [woodsBiome(), desertBiome(), fieldBiome()]
biomeProbabilities = [0.0, 0.0, 0.0]
# biggest bodge I have ever written
if areaMap[yi-1][xi-1].isinstance(woodsBiome):
biomeProbabilities[0] += 0.2
if areaMap[yi+1][xi+1].isinstance(woodsBiome):
biomeProbabilities[0] += 0.2
if areaMap[yi-1][xi+1].isinstance(woodsBiome):
biomeProbabilities[0] += 0.2
if areaMap[yi+1][xi-1].isinstance(woodsBiome):
biomeProbabilities[0] += 0.2
if areaMap[yi-1][xi-1].isinstance(desertBiome):
biomeProbabilities[1] += 0.2
if areaMap[yi+1][xi+1].isinstance(desertBiome):
biomeProbabilities[1] += 0.2
if areaMap[yi-1][xi+1].isinstance(desertBiome):
biomeProbabilities[1] += 0.2
if areaMap[yi+1][xi-1].isinstance(desertBiome):
biomeProbabilities[1] += 0.2
if areaMap[yi-1][xi-1].isinstance(fieldBiome):
biomeProbabilities[2] += 0.2
if areaMap[yi+1][xi+1].isinstance(fieldBiome):
biomeProbabilities[2] += 0.2
if areaMap[yi-1][xi+1].isinstance(fieldBiome):
biomeProbabilities[2] += 0.2
if areaMap[yi+1][xi-1].isinstance(fieldBiome):
biomeProbabilities[2] += 0.2
choice = numpy.random.choice(biomeList, 4, p=biomeProbabilities)
areaMap[yi][xi] = choice
return areaMap
Explanation:
As you can see, I'm starting off with an empty list. I add baseBiome to it as a placeholder (up to xi == xMax and yi == 0) in order to generate a 2D grid that I can then cycle through.
I create a list biomeProbabilities with different indices representing different biomes. While cycling through the positions in the map, I check the neighbours of the chosen position and adjust a value in biomeProbabilities based on its biome.
Finally, I use numpy.random.choice() with biomeList and biomeProbabilities to make a choice from biomeList using the given probabilities for each item.
My Question
How can I make sure that the sum of every item in biomeProbabilities is equal to 1 (so that numpy.random.choice will allow a random probability choice)? There are two logical solutions I see:
a) Assign new probabilities so that the highest-ranking biome is given 0.8, then the second 0.4 and the third 0.2
b) Add or subtract equal amounts to each one until the sum == 1
Which option (if any) would be better, and how would I implement it?
Also, is there a better way to get the result without resorting to the endless if statements I've used here?
This sounds like a complex way to approach the problem. It will be difficult for you to make it work this way, because you are constraining yourself to a single forward pass.
One way you can do this is choose a random location to start a biome, and "expand" it to neighboring patches with some high probability (like 0.9).
(note that there is a code error in your example, line 10 -- you have to copy the inner list)
import random
import sys
W = 78
H = 40
BIOMES = [
('#', 0.5, 5),
('.', 0.5, 5),
]
area_map = []
# Make empty map
inner_list = []
for i in range(W):
inner_list.append(' ')
for i in range(H):
area_map.append(list(inner_list))
def neighbors(x, y):
if x > 0:
yield x - 1, y
if y > 0:
yield x, y - 1
if y < H - 1:
yield x, y + 1
if x < W - 1:
yield x + 1, y
for biome, proba, locations in BIOMES:
for _ in range(locations):
# Random starting location
x = int(random.uniform(0, W))
y = int(random.uniform(0, H))
# Remember the locations to be handled next
open_locations = [(x, y)]
while open_locations:
x, y = open_locations.pop(0)
# Probability to stop
if random.random() >= proba:
continue
# Propagate to neighbors, adding them to the list to be handled next
for x, y in neighbors(x, y):
if area_map[y][x] == biome:
continue
area_map[y][x] = biome
open_locations.append((x, y))
for y in range(H):
for x in range(W):
sys.stdout.write(area_map[y][x])
sys.stdout.write('\n')
Of course a better method, the one usually used for those kinds of tasks (such as in Minecraft), is to use a Perlin noise function. If the value for a specific area is above some threshold, use the other biome. The advantages are:
Lazy generation: you don't need to generate the whole area map in advance, you determine what type of biome is in an area when you actually need to know that area
Looks much more realistic
Perlin gives you real values as output, so you can use it for more things, like terrain height, or to blend multiple biomes (or you can use it for "wetness", have 0-20% be desert, 20-60% be grass, 60-80% be swamp, 80-100% be water)
You can overlay multiple "sizes" of noise to give you details in each biome for instance, by simply multiplying them
I'd propose:
biomeProbabilities = biomeProbabilities / biomeProbabilities.sum()
For your endless if statements I'd propose to use a preallocated array of directions, like:
directions = [(-1, -1), (0, -1), (1, -1),
(-1, 0), (1, 0),
(-1, 1), (0, 1), (1, 1)]
and use it to iterate, like:
for tile_x, tile_y in tiles:
for x, y in direction:
neighbor = map[tile_x + x][tile_y + y]
#remram did a nice answer about the algorithm you may or may not use to generate terrain, so I won't go to this subject.

What is the most efficient method for accessing and manipulating a pandas df

I am working on an agent based modelling project and have a 800x800 grid that represents a landscape. Each cell in this grid is assigned certain variables. One of these variables is 'vegetation' (i.e. what functional_types this cell posses). I have a data fame that looks like follows:
Each cell is assigned a landscape_type before I access this data frame. I then loop through each cell in the 800x800 grid and assign more variables, so, for example, if cell 1 is landscape_type 4, I need to access the above data frame, generate a random number for each functional_type between the min and max_species_percent, and then assign all the variables (i.e. pollen_loading, succession_time etc etc) for that landscape_type to that cell, however, if the cumsum of the random numbers is <100 I grab function_types from the next landscape_type (so in this example, I would move down to landscape_type 3), this continues until I reach a cumsum closer to 100.
I have this process working as desired, however it is incredibly slow - as you can imagine, there are hundreds of thousands of assignments! So far I do this (self.model.veg_data is the above df):
def create_vegetation(self, landscape_type):
if landscape_type == 4:
veg_this_patch = self.model.veg_data[self.model.veg_data['landscape_type'] <= landscape_type].copy()
else:
veg_this_patch = self.model.veg_data[self.model.veg_data['landscape_type'] >= landscape_type].copy()
veg_this_patch['veg_total'] = veg_this_patch.apply(lambda x: randint(x["min_species_percent"],
x["max_species_percent"]), axis=1)
veg_this_patch['cum_sum_veg'] = veg_this_patch.veg_total.cumsum()
veg_this_patch = veg_this_patch[veg_this_patch['cum_sum_veg'] <= 100]
self.vegetation = veg_this_patch
I am certain there is a more efficient way to do this. The process will be repeated constantly, and as the model progresses, landscape_types will change, i.e. 3 become 4. So its essential this become as fast as possible! Thank you.
As per the comment: EDIT.
The loop that creates the landscape objects is given below:
for agent, x, y in self.grid.coord_iter():
# check that patch is land
if self.landscape.elevation[x,y] != -9999.0:
elevation_xy = int(self.landscape.elevation[x, y])
# calculate burn probabilities based on soil and temp
burn_s_m_p = round(2-(1/(1 + (math.exp(- (self.landscape.soil_moisture[x, y] * 3)))) * 2),4)
burn_s_t_p = round(1/(1 + (math.exp(-(self.landscape.soil_temp[x, y] * 1))) * 3), 4)
# calculate succession probabilities based on soil and temp
succ_s_m_p = round(2 - (1 / (1 + (math.exp(- (self.landscape.soil_moisture[x, y] * 0.5)))) * 2), 4)
succ_s_t_p = round(1 / (1 + (math.exp(-(self.landscape.soil_temp[x, y] * 1))) * 0.5), 4)
vegetation_typ_xy = self.landscape.vegetation[x, y]
time_colonised_xy = self.landscape.time_colonised[x, y]
is_patch_colonised_xy = self.landscape.colonised[x, y]
# populate landscape patch with values
patch = Landscape((x, y), self, elevation_xy, burn_s_m_p, burn_s_t_p, vegetation_typ_xy,
False, time_colonised_xy, is_patch_colonised_xy, succ_s_m_p, succ_s_t_p)
self.grid.place_agent(patch, (x, y))
self.schedule.add(patch)
Then, in the object itself I call the create_vegetation function to add the functional_types from the above df. Everything else in this loop comes from a different dataset so isn't relevant.
You need to extract as many calculations as you can into a vectorized preprocessing step. For example in your 800x800 loop you have:
burn_s_m_p = round(2-(1/(1 + (math.exp(- (self.landscape.soil_moisture[x, y] * 3)))) * 2),4)
Instead of executing this line 800x800 times, just do it once, during initialization:
burn_array = np.round(2-(1/(1 + (np.exp(- (self.landscape.soil_moisture * 3)))) * 2),4)
Now in your loop it is simply:
burn_s_m_p = burn_array[x, y]
Apply this technique to the rest of the similar lines.

Categories

Resources