blz - 4 months ago 6
Python Question

# How can I transform this (100, 100) numpy array into a grayscale sprite in pygame?

I'm trying to make a special kind of grating called a Gabor patch, an example of which can be found at the bottom of this tutorial whose code I ported to python.

Using matplotlib's

imshow
function, I obtain the following patch.

While the coloring is different, I suspect that this has to do with how matplotlib displays numerical values. In essence, this image is a 2D, 100-by-100 pixel array containing values from
-1.0
to
1.0
(inclusive). If anybody would like to try manipulating the array in question, I've saved it as a pickle object here.

My question is as follows: How can I transfer this array to a pygame surface while ensuring that the following conditions are met?

1. The coloring is converted to grayscale coloring (c.f.: the last image in the first link)

2. The solution must employ pygame version
1.9.1release
. For some inexplicable reason, I can't find a way to install
1.9.2
on my OS (Ubuntu 13.04). There appear to be no PPAs and pygame is evidently not on PIP.

Thank you very much in advance, and please let me know if I can provide additional information!

Edit

Regarding @Veedrac's solution (which is remarkably similar to my own), here is what my patch looks like when using the grayscale colormap in matplotlib's
imshow
. This is what I would like to have:

from matplotlib.pyplot import *
import matplotlib.cm as cm

figure()
imshow(g, cm=cm.Greys_r)
show()

Answer
import numpy
import pickle
import pygame

surface = pygame.Surface((100, 100))

Get the pixels, convert to RGBA. Using Joe Kington's reminder that the data ranges from -1 to 1:

base = (pickle.load(open("g.pickle"))+1)/2 * 255
base = base[..., numpy.newaxis].repeat(4, -1).astype("uint8")

Copy the data across

numpy_surface = numpy.frombuffer(surface.get_buffer())
numpy_surface[...] = numpy.frombuffer(base)
del numpy_surface

Show it with:

screen = pygame.display.set_mode((100, 100))
screen.blit(surface, (0, 0))
pygame.display.flip()

and you get

And simplified, once again thanks to Joe Kington's input, using make_surface:

import numpy
import pickle
import pygame

base = (pickle.load(open("g.pickle"))+1) * 128
base = base[..., None].repeat(3, -1).astype("uint8")

surface = pygame.surfarray.make_surface(base)

screen = pygame.display.set_mode((100, 100))
screen.blit(surface, (0, 0))
pygame.display.flip()

The base[..., None] is normally spelt base[..., numpy.newaxis], but seeing as that was the only instance of numpy I just "expanded the constant" so as to not need numpy. It didn't work, though, as the code breaks if you don't import numpy with a IndexError: bytes to write exceed buffer size. Thanks, numpy.

The ... means "the whole of all of the axis before this point", so you can replace [3:2], [:, 3:2] and [:, :, :, 3:2] with [..., 3:2]. In fact, ... was introduced to Python for this very reason.

The None, or numpy.newaxis, slices a new axis (duh). This will transform [a, b, c] into [[a], [b], [c]], for example. This is needed because we then repeat along this new axis.

Basically, looking at one row, we have

114, 202, 143, ...

and we want

[114, 114, 114], [202, 202, 202], [143, 143, 143], ...

so our [..., None] got us to

[114], [202], [143], ...

and we just repeat 3 times in axis -1. Axis -1 is, of course, the last axis, which is the numpy.newaxis.

Comments