6

In pygame, I have a surface:

im = pygame.image.load('foo.png').convert_alpha()
im = pygame.transform.scale(im, (64, 64))

How can I get a grayscale copy of the image, or convert the image data to grayscale? I have numpy.

Christian Mann
  • 8,030
  • 5
  • 41
  • 51

3 Answers3

5

Use a Surfarray, and filter it with numpy or Numeric:

def grayscale(self, img):
    arr = pygame.surfarray.array3d(img)
    #luminosity filter
    avgs = [[(r*0.298 + g*0.587 + b*0.114) for (r,g,b) in col] for col in arr]
    arr = numpy.array([[[avg,avg,avg] for avg in col] for col in avgs])
    return pygame.surfarray.make_surface(arr)
Christian Mann
  • 8,030
  • 5
  • 41
  • 51
  • If you use .pixels3d instead of .array3d, you can avoid copying all the data into a new surface. – geometrian Jul 11 '12 at 04:03
  • To speed this up using the same logic: `arr=arr.dot([0.298, 0.587, 0.114])[:,:,None].repeat(3,axis=2); return pygame.surfarrary.make_surface(arr)`. On my (800x600) test image this speeds up by more than 2 orders of magnitudes. – Jason Aug 06 '17 at 13:40
  • Though the images converts to grayscale, still the the shape of array has 3 at third dimension How can we get rid of channels? – Shilan Nov 09 '20 at 15:06
3

After a lot of research, I came up with this solution, because answers to this question were too slow for what I wanted this feature to:

def greyscale(surface: pygame.Surface):
    start = time.time()  # delete me!
    arr = pygame.surfarray.array3d(surface)
    # calulates the avg of the "rgb" values, this reduces the dim by 1
    mean_arr = np.mean(arr, axis=2)
    # restores the dimension from 2 to 3
    mean_arr3d = mean_arr[..., np.newaxis]
    # repeat the avg value obtained before over the axis 2
    new_arr = np.repeat(mean_arr3d[:, :, :], 3, axis=2)
    diff = time.time() - start  # delete me!
    # return the new surface
    return pygame.surfarray.make_surface(new_arr)

I used time.time() to calculate the time cost for this approach, so for a (800, 600, 3) array it takes: 0.026769161224365234 s to run.

As you pointed out, here is a variant preserving the luminiscence:

def greyscale(surface: pygame.Surface):
    arr = pygame.surfarray.pixels3d(surface)
    mean_arr = np.dot(arr[:,:,:], [0.216, 0.587, 0.144])
    mean_arr3d = mean_arr[..., np.newaxis]
    new_arr = np.repeat(mean_arr3d[:, :, :], 3, axis=2)
    return pygame.surfarray.make_surface(new_arr)
2

The easiest way is to iterate over all the pixels in your image and call .get_at(...) and .set_at(...).

This will be pretty slow, so in answer to your implicit suggestion about using NumPy, look at http://www.pygame.org/docs/tut/surfarray/SurfarrayIntro.html. The concepts and most of the code are identical.

geometrian
  • 14,775
  • 10
  • 56
  • 132