Programmatically divide screen into N-rectangles with pygame - python

I have a stream of input data from multiple "channels" and I want to represent each channel's data stream within a section of the screen using pygame.
The streaming data is captured in a pandas data.frame and summarized every few seconds. Right now I've prototyped this using only the first channel of data and fullscreen mode in pygame (i.e., scr = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)).
The way I'm trying to divide up the screen is a lot like this but instead of having a fixed number of sub-screens to create, I'm hoping to reference the number of columns in the pandas data.frame to divide up the screen programmatically.
Should this be done using Surface and some sort of loop? Just looking for some guidance as this type of programming is new to me. I've seen something vaguely similar with geographic data (like this) but the context of working with displays and pygame is enough to make me a bit uncertain.

I would indeed work with pygame.Surfaces and a loop.
I took a primer on pygame (my go to is: https://realpython.com/pygame-a-primer/) and added how I would implement multiple sub_screens (or surfs in my script below). I hope my code and comments are clear enough to get what's going on, otherwise just ask!
For now I just fill the sub-screens with random colors, but you can blit anything you want on the sub-screens (in or outside the while-loop), as they are just good old pygame.Surfaces.
import pygame
import random
# init pygame
pygame.init()
# set up parameters
n_rows = 5
n_cols = 3
screen_width = 800
screen_height = 500
# Create a dict with the row and column as key,
# and a tuple containing a Surface and its topleft coordinate as value
surf_width = round(screen_width / n_cols)
surf_height = round(screen_height / n_rows)
surfaces_dct = {}
for row in range(n_rows):
for col in range(n_cols):
# create a surface with given size
surf = pygame.Surface(size=(surf_width, surf_height))
# get its top left coordinate
top_left = (col*surf_width, row*surf_height)
# put it in the dict as a tuple
surfaces_dct[(row, col)] = (surf, top_left)
# Here you can blit anything to each surface/sub-screen (defined by it's row and column):
for key, value in surfaces_dct.items():
# unpack the key, value pairs
(row, col) = key
(surf, top_left) = value
# I just fill each surface with a random color for demonstration
(r, g, b) = [random.randint(0, 255) for i in range(3)]
surf.fill((r, g, b))
# Set up the drawing window
screen = pygame.display.set_mode([screen_width, screen_height])
# Run until the user asks to quit
running = True
while running:
# Did the user click the window close button?
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# You can also run this for-loop here, inside the main while-loop
# to dynamically change what's on each surf
# as you see you can also unpack the key and value like this
for (row, col), (surf, top_left) in surfaces_dct.items():
# I'll leave it blank here
pass
# Finally place(/blit) the surfaces on the screen given
screen.blits(list(surfaces_dct.values()))
#### the above is the same as doing:
# for surf, top_left in surfaces_dct.values():
# screen.blit(surf, top_left)
# Update the display
pygame.display.update()

Related

pygame surface.get_at() not returning RGB integers anymore?

I just started playing around with python a bit and tried to do some silly grafix stuff to see how it worked. Now I got stuck with one precise problem: I am trying to read the color value of a pixel from a pygame surface - but I can't. The below code - which I found in several (perhaps outdated) manuals and samples - throws an error I don't understand:
(Line 38 is the one containing the "screen.get_at()" part)
File "/home/mark/devel/python/./test.py", line 38, in
color = screen.get_at((x, y))
TypeError: 'list' object cannot be interpreted as an integer
What I guess from the stuff I found online: pygame.surface changed the type of the return value for get_at() a short while ago. Now it doesn't return four integers for R, G, B and alpha. but it returns a type "color". However, I was unable to find an explanation what this type "color" actually is or how it works, resp. how I get just the RGB values out of it.
A shortened sample of my code which throws the above error is as follows:
#!/usr/bin/python3
import pygame
from pygame.locals import *
import numpy as np
WIDTH = 1200
HEIGHT = 800
WHITE = (255, 255, 255)
pygame.init()
# Set width and height of the screen
screen = pygame.display.set_mode((WIDTH,HEIGHT))
pygame.display.set_caption("Ant")
pygame.draw.rect(screen, WHITE, (0,0,WIDTH,HEIGHT))
pos = ([0],[0])
# Loop until the user clicks the close button.
done = False
clock = pygame.time.Clock()
# Loop as long as done == False
while not done:
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT or event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: # If user wants to exit
done = True # Flag that we are done so we exit this loop
x = pos[0] # horizontal position
y = pos[1] # vertical position
color = screen.get_at((x, y))
print (color)
pygame.display.flip()
# This limits the while loop to a max of 60 times per second.
# Leave this out and we will use all CPU we can.
clock.tick(600)
# Be IDLE friendly
pygame.quit()
I'd really appreciate if someone could give me a hint how to get the RGB (integer) values from this "color" type get_at() returns.
thx,
Mark

I'm trying to make a bot with pyautogui that shoots targets based on their pixel color but the cursor gets stuck once the bot finds that pixel

I'm attempting to scan through screenshots and find a specific pixel color location where the bot must move the cursor to and then click it, however, it seems like as soon as the bot finds the pixel the cursor gets stuck in a loop where it keeps moving a very short distance up and down. The site I'm using for this bot is aimtrainer.io.
import pyautogui
import keyboard
import win32api, win32con
import time
# defining a function that moves the mouse to a specific 'x, y' position and then performs a click
def win_click(x_axis, y_axis):
win32api.SetCursorPos((x_axis, y_axis))
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0)
time.sleep(0.2)
target_rgb = (255, 87, 34) # this is the color that the bot will look for
x_fourth = 1366 / 4
y_fourth = 768 / 4
scr_shot_size = ((x_fourth, 3 * x_fourth), (y_fourth, 3 * y_fourth)) # this is cutting the screen such that it's 1/4 of
# distance from the border on all its sides
last_pixel = None # this is just a placeholder
while not keyboard.is_pressed('q'):
scr_shot = pyautogui.screenshot()
# these 2 for loops will scan the cut screen with a pace of 5 pixels
# such that for each 'x', it will look for its 'y's
for x in range(int(scr_shot_size[0][0]), int(scr_shot_size[0][1]), 5):
for y in range(int(scr_shot_size[1][0]), int(scr_shot_size[1][1]), 5):
curr_pixel_rgb = scr_shot.getpixel((x, y)) # gets the pixel in the current 'x, y' coordinate
if curr_pixel_rgb == target_rgb:
# the below if statements tries to prevent the bot from clicking twice on the same position
if (x, y) != last_pixel:
win_click(x, y)
else:
time.sleep(0.1)
last_pixel = (x, y) # stores the value of the current pixel that will later be compared with the value
# of the current pixel
First I would recommend, you add a Keypress check inside the nested loop, so you don't need to wait for the whole loop to terminate before you can exit.
The problem with the stuck moving cursor is that you jump each 5 pixels.
Let's imagine you have a Rectangle of the size 150x300px with its top left corner positioned at (100,100) and with the correct color.
As you can see, your cursor would repeatedly jump column by column inside of the rectangle.
(I'm sorry for this awful drawing)
You can try to fix this by checking the whole color area and place the cursor centered inside of it once.
Let me know, if you need help with that!

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

How to show an Image with pillow and update it?

I want to show an image recreated from an img-vector, everything fine.
now I edit the Vector and want to show the new image, and that multiple times per second.
My actual code open tons of windows, with the new picture in it.
loop
{
rearr0 = generateNewImageVector()
reimg0 = Image.fromarray(rearr0, 'RGB')
reimg0.show()
}
What can I do to create just one Window and always show just the new image?
Another way of doing this is to take advantage of OpenCV's imshow() function which will display a numpy image in a window and redraw it each time you update the data.
Note that OpenCV is quite a beast of an installation, but maybe you use it already. So, the code is miles simpler than my pygame-based answer, but the installation of OpenCV could take many hours/days...
#!/usr/local/bin/python3
import numpy as np
import cv2
def sin2d(x,y):
"""2-d sine function to plot"""
return np.sin(x) + np.cos(y)
def getFrame():
"""Generate next frame of simulation as numpy array"""
# Create data on first call only
if getFrame.z is None:
xx, yy = np.meshgrid(np.linspace(0,2*np.pi,w), np.linspace(0,2*np.pi,h))
getFrame.z = sin2d(xx, yy)
getFrame.z = cv2.normalize(getFrame.z,None,alpha=0,beta=1,norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_32F)
# Just roll data for subsequent calls
getFrame.z = np.roll(getFrame.z,(1,2),(0,1))
return getFrame.z
# Frame size
w, h = 640, 480
getFrame.z = None
while True:
# Get a numpy array to display from the simulation
npimage=getFrame()
cv2.imshow('image',npimage)
cv2.waitKey(1)
That looks like this:
It is dead smooth and has no "banding" effects in real life, but there is a 2MB limit on StackOverflow, so I had to decrease the quality and frame rate to keep the size down.
You can do that pretty simply and pretty fast with pygame.
You write a function called getFrame() that returns a numpy array containing an image that is calculated by your simulation.
By way of example, I create a 2-d sine wave on the first pass then roll that 1 pixel down and 2 pixels across on subsequent calls to simulate movement.
#!/usr/local/bin/python3
import numpy as np
import pygame
h,w=480,640
border=50
N=0
getFrame.z = None
def sin2d(x,y):
"""2-d sine function to plot"""
return np.sin(x) + np.cos(y)
def getFrame():
"""Generate next frame of simulation as numpy array"""
# Create data on first call only
if getFrame.z is None:
xx, yy = np.meshgrid(np.linspace(0,2*np.pi,h), np.linspace(0,2*np.pi,w))
getFrame.z = sin2d(xx, yy)
getFrame.z = 255*getFrame.z/getFrame.z.max()
# Just roll data for subsequent calls
getFrame.z = np.roll(getFrame.z,(1,2),(0,1))
return getFrame.z
pygame.init()
screen = pygame.display.set_mode((w+(2*border), h+(2*border)))
pygame.display.set_caption("Serious Work - not games")
done = False
clock = pygame.time.Clock()
# Get a font for rendering the frame number
basicfont = pygame.font.SysFont(None, 32)
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# Clear screen to white before drawing
screen.fill((255, 255, 255))
# Get a numpy array to display from the simulation
npimage=getFrame()
# Convert to a surface and splat onto screen offset by border width and height
surface = pygame.surfarray.make_surface(npimage)
screen.blit(surface, (border, border))
# Display and update frame counter
text = basicfont.render('Frame: ' + str(N), True, (255, 0, 0), (255, 255, 255))
screen.blit(text, (border,h+border))
N = N + 1
pygame.display.flip()
clock.tick(60)
That looks like this. In real life it is very fast and very smooth, but there is a 2MB size limit for videos on StackOverflow, so I have generated a GIF with a low-ish frame rate and small-ish size just to keep it under 2MB.
Obviously you can add in detection of Up and Down arrows to speed up or slow down the animation, and you could detect left/right arrow and Spacebar to go backwards/forwards or pause the animation.
According to Pillow documentation the method Image.show() is mainly intended for debugging purposes. On Windows, it saves the image to a temporary BMP file, and uses the standard BMP display utility to show it.
You should take a look at the matplotlib image library.
You can try:
import matplotlib.pyplot as plt
imgplot = plt.imshow(rearr0) # "plot" the image.
For each iteration, you can call imgplot.clf(), it will clear the plot keeping the axis. I have not tried it, but it should work

Creating a grid in pygame using for loops

This is different to the other questions as it uses another method. I have the following code and need to change it so that it produces a grid (all rows and columns filled in) as per Figure 16.7 on this link: http://programarcadegames.com/index.php?chapter=array_backed_grids
The following code produces a full row and a full column, but I can't quite work out how to extend it to fill the whole screen with rectangles with the appropriate margin built in.
Code:
"""
Create a grid with rows and colums
"""
import pygame
# Define some colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
pygame.init()
# Set the width and height of the screen [width, height]
size = (255, 255)
screen = pygame.display.set_mode(size)
pygame.display.set_caption("My Game")
# Loop until the user clicks the close button.
done = False
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
width=20
height=20
margin=5
# -------- Main Program Loop -----------
while not done:
# --- Main event loop
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# --- Game logic should go here
# --- Screen-clearing code goes here
# Here, we clear the screen to white. Don't put other drawing commands
# above this, or they will be erased with this command.
# If you want a background image, replace this clear with blit'ing the
# background image.
screen.fill(BLACK)
# --- Drawing code should go here
#for column (that is along the x axis) in range (0 = starting position, 100=number to go up to, width+margin =step by (increment by this number)
#adding the 255 makes it fill the entire row, as 255 is the size of the screen (both ways)
for column in range(0+margin,255,width+margin):
pygame.draw.rect(screen,WHITE, [column,0+margin,width,height])
for row in range(0+margin,255,width+margin):
pygame.draw.rect(screen,WHITE,[0+margin,row,width,height])
#This simply draws a white rectangle to position (column)0,(row)0 and of size width(20), height(20) to the screen
# --- Go ahead and update the screen with what we've drawn.
pygame.display.flip()
# --- Limit to 60 frames per second
clock.tick(60)
# Close the window and quit.
pygame.quit()
The problem lies in the inner loop (for row in...),
where the rect is drawn with:
pygame.draw.rect(screen,WHITE,[0+margin,row,width,height])
Note that the x coordinate always is 0+margin,
no matter which column is currently drawn. So
the code draws 10 columns on top of each other.
As a simple fix, change the line to:
pygame.draw.rect(screen,WHITE,[column,row,width,height])
You might then notice that the other call of the draw method in the outer loop is completely unnecessary. After all, the inner call now draws a rectangle for each row in each column. So you can reduce the loop code to:
for column in range(0+margin, 255, width+margin):
for row in range(0+margin, 255, height+margin):
pygame.draw.rect(screen, WHITE, [column,row,width,height])

Categories

Resources