Converting an image in pygame to an 2D array of RGB values - python

How can I convert a surface object in pygame to a 2-dimensional array of RGB values (one value for every pixel)? I have read the documentation on PixelArrays and Surfarrays and I cannot seem to find an answer to my question. Examples are more than welcome.

The pygame documentation says that given a surface object, you can create a PixelArray wrapper to provide direct 2D array access to its pixels by calling the module's PixelArray() method like this:
pxarray = pygame.PixelArray(surface)
Logically a PixelArray object is a 2-dimensional array of RGB values stored as integers.
A PixelArray pixel item can be assigned a raw integer value, a
pygame.Color instance (an object for color representations), or a (r, g, b[, a]) tuple.
pxarray[x, y] = 0xFF00FF
pxarray[x, y] = pygame.Color(255, 0, 255)
pxarray[x, y] = (255, 0, 255)
It also mentions:
However, only a pixel’s integer value is returned. So, to compare a pixel to a
particular color, the color needs to be first mapped using the Surface.map_rgb()
method of the Surface object for which the PixelArray was created.
Which means you'll need to use the Surface.map_rgb() method to get RGB tuples from PixelArray integer values whenever you're not doing an assignment to the array, i.e. when reading a pixel's value, as is being done in the following conditional:
# Check, if the first pixel at the topleft corner is blue
if pxarray[0, 0] == surface.map_rgb((0, 0, 255)):
...
Hope this helps.

After wrapping a Surface in a PixelArray, the unmap_rgb function can convert the pixels into pygame.Color objects.
For example:
import pygame
pygame.init()
image = pygame.image.load('example.png')
pixel = pygame.PixelArray(image)
center = int(image.get_width()/2), int(image.get_height()/2)
colour = image.unmap_rgb(pixel[center])
print(str(colour))
Edit: To do the complete conversion of all these pygame.Color objects into a two-dimensional array of RGB tuples, you could use this code:
import pygame
pygame.init()
image = pygame.image.load('example.png')
pixel = pygame.PixelArray(image)
color_array = [[image.unmap_rgb(pixel[x, y]) for x in range(0, image.get_width())] for y in range(0, image.get_height())]
rgb_array = [[(column.r, column.g, column.b) for column in row] for row in color_array]
print(str(rgb_array))
Note that if you were to use map_rgb (as suggested in the other answer), you might have to test every pixel against over 16 million map_rgb calls (one for each of 256 * 256 * 256 possible RGB values). If you wanted the alpha values as well, that would add another multiplication of possible values by 256. Another strategy using map_rgb might be to create a lookup table from the (256 ** n) possible values, and check each pixel against the table. While it is possible to do that, the code presented here seems much more efficient.

Related

use a color lookup table with pygame

I created a lookup color table in pygame (here just gray level):
LUT = np.empty([256,3],np.uint8)
for gg in range(0,256):
LUT[gg,:] = [gg,gg,gg]
Then I want to call each value of this lookup table to fill a 3D pixel array from pygame from a 2D matrix, for example with noise:
screenWidth = 800
screenHeight = 480
win = pygame.display.set_mode((screenWidth,screenHeight), 32)
buffer = pygame.surfarray.pixels3d(win)
I = (255*np.random.rand(screenWidth,screenHeight)).astype(int)
buffer = LUT[I,:]
In appearance everything seems right, I get a 3D pixel array, each value is what I expect. But this pixel array is not being displayed in the window "win" I created (it remains black). What puzzles me is that if I fill the same pixel array element by element:
for rr in range(0,screenWidth):
for cc in range(0,screenHeight):
buffer[rr,cc,:] = LUT[I[rr,cc],:]
Then it works fine, but it's a lot slower. I can find no difference between the pixel arrays filled by the 2 techniques (both are regular (800,480,3) unit8 array as I would expect). And if I display them with matplotlib imshow the image look fine in both cases. Does anyone know why?
The nested loop
for rr in range(0,screenWidth):
for cc in range(0,screenHeight):
buffer[rr,cc,:] = LUT[I[rr,cc],:]
can be substituted by:
buffer[:,:,:] = LUT[I,:]
buffer = LUT[I,:] doesn't changes the element of buffer. The statement generates an new buffer and assigns the new array object to the variable buffer. The elements of buffer have to be set by slicing (see numpy - Basic Slicing and Indexing):
lutBuffer = LUT[I,:]
buffer[:,:,:] = lutBuffer

PyCairo and modifying pixel data

I'm using PyCairo to draw some vector images programmatically from a Python script. It works fine. But now I'd like to access the pixel data and do some further processing on them at the pixel level (things like blur or other raster effects) and then continue using that image surface with PyCairo to draw some more vector shapes.
I found the get_data() method in cairo.ImageSurface class, but I'm not sure how to use it, because the documentation is very cryptic about it. It just says that it returns something called a "Python buffer", but there are no code examples of how this can actually be used in a real aplication.
Can anyone provide an example code of how to get the grip of those pixels in that "Python buffer" thingamajig? (preferably without the need of copying the entire image back and forth from/to PyCairo surfaces).
The data is the raw pixel data. It's a memoryview representing the underlying ImageSurface. The data has a different interpretation depending on the format.
Let's consider only the RGB24 pixel format for simplicity sake. Each pixel is stored as four bytes. Red, green, and blue respectively. The fourth byte is ignored, and is only there for performance reasons.
The pixels are then stored row by row, the first row coming first and the second row coming after and so on and so forth.
The might be additional padding at the end of the row as well, therefore the stride of the data is a crucial property. To get the byte index of a specific row y we thus need to compute y * stride. To this we add a x coordinate times the pixel byte width 4.
This is all illustrated in the following small python program that draws a white rectangle on a black background.
import cairo
width, height = 100, 100
surface = cairo.ImageSurface(cairo.Format.RGB24, width, height)
data = surface.get_data()
for y in range(50 - 20, 50 + 20):
for x in range(50 - 20, 50 + 20):
index = y * surface.get_stride() + x * 4
data[index] = 255 # red
data[index + 1] = 255 # green
data[index + 2] = 255 # blue
surface.write_to_png("im.png")

Get all pixels different from a color and paint them in other image

I have image A with dimension (512, 512, 3).
I want to find all the pixels which != [255,255,255].
Given that pixels, I want to color these coordinates in another image B.
What am I doing wrong?
indices = np.where(imgA!= [255,255,255])
imgB[indices] = [0,0,0]
This template should get you on the right path:
from PIL import image
picture = Image.open(path_to_picture)
width, height = picture.size
for x in range(width):
for y in range(height):
current_color = picture.getpixel( (x,y) )
if current_color[0:3]!=(255,255,255):
picture.putpixel( (x,y), (***, ***,***) + (current_color[-1],))
picture.save(path_to_new_picture)
Note here that getpixel() will return a tuple that contains the RGBA values for the given pixel. In this example, I am assuming that you are retaining the alpha value and simply modifying the RGB values of the current pixel.
you need to loop over each pixel in the image.
... imgA!= [255,255,255] will always return true, because you are comparing a (512,512,3) nd.array to a (3,) nd.array
Even if your images are not built from numpy matricies, this point still applies. If you run into performance issues, use cython for faster for loops.

How do you map RGB color values onto a color wheel?

I have small set of color data that I want to investigate. It is in the form of a list of RGB data.
[(255, 255, 255), (124, 144, 231), ...]
The image uses a restricted palette, and I would like to see how these colors are "distributed" by plotting them along the color wheel. As alternative, I tried histogram of individual channels, but this did not give me the insight I am interested in.
I googled and learned that HSL color more accurately maps to color wheel. However, after converting to HSL, I'm still confused. 3 pieces of data still make up the color: Hue, saturation, and luminosity. How do you map 3 piece of data onto a 2d plane?
I read about polar coordinates here: https://www.mathsisfun.com/polar-cartesian-coordinates.html. I try ignoring luminosity and plotting by treating HSL data as Polar coordinate (hue = angle, saturation = length of radius (scaled by some factor))
def polar2cartesian(hsl):
color_circle_radius = 100
radius = hsl.saturation * color_circle_radius
x = radius * math.cos(math.radians(hsl.hue))
y = radius * math.sin(math.radians(hsl.hue))
return x, y
...
for hsl in colors:
x, y = polar2cartesian(hsl)
im.point(x, y, hsl.to_rgb())
This is not correct result. As it shows same red color hue in multiple places like this example:
bad chart
What is the correct way to translate from RGB to a position on color wheel?
The problem of mapping a 3D (H, S, V) colour onto a 2D plane is a tough one to solve objectively, so I thought I'd give a crack at it and come up with results that I find pleasing.
My approach is as follows:
For every (R, G, B) pixel in the image, convert it to (H, S, V).
Convert the (H, S, V) colour to a 2D vector using the H value as the angle and the S value as the magnitude.
Find the position of that vector in our 2D output image, and only write the pixel if the value (V) is greater than the value of what was previously in that pixel. (My reasoning is that since an image is likely to have multiple pixels of similar enough colours that they appear in the same place on our colour wheel, since we are not plotting using the value, we should give higher value pixels precedence to be shown.)
Now, in code: (Entire file)
Create a table to store the largest value in every particular position
highest_value = numpy.zeros((image_size, image_size))
Convert RGB to HSV
def rgb_to_point(rgb):
hsv = colorsys.rgb_to_hsv(*rgb)
Convert that to a vector
rads = math.tau * hsv[0] - math.pi
mag = hsv[1] * (image_size/2) - 1
Convert that to a point on our image
x = int(math.cos(rads) * mag + (image_size/2))
y = int(math.sin(rads) * mag + (image_size/2))
If the value is higher, return the point, otherwise None
if(hsv[2] > highest_value[x][y]):
highest_value[x][y] = hsv[2]
return (x, y)
I called all that the rgb_to_point function, now we will use it for every pixel in our image:
for pixel in img.getdata():
c = rgb_to_point(pixel)
if(c):
imgo.putpixel(c, pixel)
if(c) determines whether the value was higher, since c is None when it wasn't.
Here's the results:
Note: Part of the reason I am dealing with value like this is because the alternatives I thought of were not as good. Ignoring value completely lead to darker pixels turning up on the output image, which usually lead to an ugly wheel. Turning the value up to 1 for every output pixel lead to very generic looking wheels that didn't really give a good idea of the original input image.

Tiling an array in place with numpy

I have an image stored in RGBA format as a 3d numpy array in python, i.e.
image = np.zeros((500, 500, 4), dtype=np.int16)
would be a transparent, black 500x500 square.
I would like to be able to quickly fill the image with a uniform color. For instance fill_img(some_instance_with_img, (255, 0, 0, 255)) would fill the image stored in some_instance_with_img with opaque red. The following code does the trick, assuming self is an instance that contains an image stored as image:
def fill_img(self, color):
color = np.array(color)
shape = self.image.shape
self.image = np.tile(color, (shape[0] * shape[1])).reshape(shape)
However, it creates a brand new array and simply reassigns self.image to this new array. What I would like to do is avoid this intermediate array. If np.tile had an out argument, it would look like:
def fill_img(self, color):
color = np.array(color)
shape = self.image.shape
np.tile(color, (shape[0] * shape[1]), out=self.image)
self.image.reshape(shape)
but np.tile does not support an out parameter. It feels like I am just missing something, although it is possible that this behavior doesn't exist. Any help would be appreciated. Thanks.

Categories