Grayscaling an Image with PyGame and Numpy


If you had the chance to play The Alchemist, you probably saw the grayscaling effect applied when you pause the game, this is done at pixel level and is achieved thanks to NumPy arrays, which allows really fast operations and transformations (it takes under 0.027 seconds to process an array with shape [800, 600, 3] ).

How is this being done?

At first, you must understand how the RGB pixels color system works, the basic idea is really simple and it says that you can store the color of a pixel using an integer (from 0 to 255) array of size 3, where each entry represents one color component, Red, Green, and Blue respectively.

https://en.wikipedia.org/wiki/RGB_color_model

The important part here is that, if you set the same value for each component, then you'll get a shade of gray from black to white.

Let's say that you have an image with two colors:

ColorComponents in Hex Representation
Components in Decimal Representation
Blue-ish(00, 9A, FF)(0, 154, 255)
Green-ish(2A, BB, 85)(42, 187, 133)


If you want to convert these colors to a grayscale version, you can arbitrarily pick one component and replicate it across the array, for example, let's pick the "intermediate" value between the lowest and the highest one, and you will end up with these new RGB vectors:
Color
Original compone
Grayscaled components
Blue-ish(0, 154, 255)
(154, 154, 154)
Green-ish(42, 187, 133)(133, 133, 133)

After applying the transformation, you'll get something like this:

As you can see, the idea is pretty simple, but in this case, there's something weird with the result, this is because the luminescence factor, which refers to how much light a pixel does have. This is widely explained here:

https://en.wikipedia.org/wiki/Luma_(video)

What you need to know is that you can preserve this luminescence factor by applying this formula to each pixel:



The calculations are very simple:

luma_blueish = 0 * 0.299 + 154 * 0.587 + 255 * 0.114 = 0 + 90.398 + 29.07 = 119.46799999999999 ~ 119
luma_greenish = 42 * 0.299 + 187 * 0.587 + 133 * 0.114 = 12.558 + 109.769 + 15.162 = 137.489 ~ 137
Color Original compone Grayscaled components
Blue-ish(0, 154, 255)
(119, 119, 119)
Green-ish(42, 187, 133)(137, 137, 137)


Then you can get a result similar to this one:

Which feels more accurate to the eye.

But... how can we code all this stuff, well, here's the code I'm using within the game, commented line by line. Unfortunately, Itch does not have syntax highlight :/

def greyscale(surface: pygame.Surface):
    # Creates a NumPy array from a given surface, with shape: (width, height, 3)
    arr = pygame.surfarray.pixels3d(surface)
    # Calculate the Luma value for each pixel and replaces the RGB value with it.
    luma_arr = np.dot(arr[:,:,:], [0.216, 0.587, 0.144])
    # This last operation leaves you with an array of shape (width, height)
    # Now we need to restore the array's original shape, by inserting a new axis.
    luma_arr3d = mean_arr[..., np.newaxis]
    # Propagate the luma value through this new axis
    new_arr = np.repeat(mean_arr3d[:, :, :], 3, axis=2)
    # Return the new Surface
    return pygame.surfarray.make_surface(new_arr)

An In-game Grayscaling example:

Any questions or feedback are welcome, feel free to comment!

Get The Alchemist

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.