allow pygame.surfarray to create alpha channels (1244)
Issue №1244 opened by robertpfeiffer at 2019-08-13 14:19:47
Reported by Discord user JoKing# 4832:
Is there a way to add RGBA value of color to pygame.surfarray.make_surface(). If I have array with only RGB values (for example [0, 0, 0]) it works just fine, but if I try to do something like [0, 0, 0, 128] I get ValueError: must be a valid 2d or 3d array
The shape of the numpy array is explicitly checked in pixelcopy.c; Either it can be a 2D array of 32bit ints (which are interpreted as RGB only, the byte corresponding to the alpha channel is ingored), or a 3D array where the first two dimensions are x any y, and the third is the different colours and has to be exactly size 3.
At the very least, we will need to change the error message to explain that not all 3D arrays are accepted, only those where the third dimension has size 3. This is a one-line fix.
But we could also create an alpha channel, at least when the input is a 3D array with size 4 in the 3rd dimension. In that case, the behaviour of 2D and 3D arrays would be inconsistent with each other, but backwards compatibility would be maintained. That would be more than a quick fix and require additional unit tests and probably a new, different code path.
Related Docs: https://www.pygame.org/docs/ref/surfarray.html
Comments
# # MyreMylar commented at 2020-05-15 20:00:46
Merging info from 1513:
I tried passing a numpy.ndarray to pygame.surfarray.make_surface() that had shape = (200, 200, 4) and dtype = np.uint8. However this raises an exception
ValueError: must be a valid 2d or 3d array
which I tracked down to pixelcopy.c
It looks like this is not designed to take an RGBA array, which seems like an unnecessary limitation. The error is also poor in that I did pass a 3D array. But the documentation for this function just says:
Create a new Surface that best resembles the data and format on the array. The array can be 2D or 3D with any sized integer values.
(I also tried to construct a surface and access its pixels as an RGBA view, but pygame.surfarray.pixels3d() returns an RGB array of shape (w, h, 3))
# # carlosgmartin commented at 2020-10-08 06:41:40
@MyreMylar What's the right way to proceed when the array has an alpha channel? How can one create the corresponding surface?
# # llindstrom commented at 2021-01-17 07:25:26
Support for (w, h, 4) arrays was omitted from surfarray because 32 bit source alpha surfaces have differing alpha channel placement within a pixel. Depending on various factors such as hardware and operating system the channel order may be RGBA or ARGB, making indexing inconsistent for pixels3d arrays. However, other surfarray functions could be made to accept, or return, (w, h, 4) arrays.
# # jiss2891 commented at 2021-03-08 20:06:38
Hi guys, are there any plans to fix this? I'm needing this so bad..., also, is there a viable workaround to emulate this feature? I tried to use Mask but without luck, due to lack of knowledge...
I'm greyscaling an image using surfarray to manipulate the rgb values but the image has an alpha channel (it's a mob sprite), so when restored back to a surface I lost that alpha values...
# # jiss2891 commented at 2021-03-09 15:24:05
Hi again, I finally came up with a workaround to this, maybe it helps others with the same issue:
def greyscale(surface: pygame.Surface):
surface_copy = surface.copy() # I want to use the original surface as is.
arr = pygame.surfarray.pixels3d(surface_copy)
mean_arr = np.dot(arr, [0.216, 0.587, 0.144])
arr[:, :, 0] = mean_arr
arr[:, :, 1] = mean_arr
arr[:, :, 2] = mean_arr
return surface_copy
# # llindstrom commented at 2021-03-09 23:46:58
Here is a version of pygame.pixelcopy.make_surface for RGBA numpy arrays. Like make_surface, it only handles integer arrays.
import numpy
import pygame.pixelcopy
def make_surface_rgba(array):
"""Returns a surface made from a [w, h, 4] numpy array with per-pixel alpha
"""
shape = array.shape
if len(shape) != 3 and shape[2] != 4:
raise ValueError("Array not RGBA")
# Create a surface the same width and height as array and with
# per-pixel alpha.
surface = pygame.Surface(shape[0:2], pygame.SRCALPHA, 32)
# Copy the rgb part of array to the new surface.
pygame.pixelcopy.array_to_surface(surface, array[:,:,0:3])
# Copy the alpha part of array to the surface using a pixels-alpha
# view of the surface.
surface_alpha = numpy.array(surface.get_view('A'), copy=False)
surface_alpha[:,:] = array[:,:,3]
return surface
This could be done more cleanly using pygame.surfarray instead, and it would also work with floating point arrays.
# # jiss2891 commented at 2021-03-10 03:05:45
That's great! I think in my case it's cheaper to access the pixel's reference, keeping the alpha intact. :)
Will surfarray.make_surface support create alpha channel finally? Since this issue is still open and has an [enhancement] tag.
@llindstrom Thanks you have rescued my project. I had been through the docs and could not see any way to set alpha values for a surface (except setting the values all to same integer). The only way I could find was saving an array with transparency values as a png using opencv and then reading that in as a surface.
I would like to see a method for blitting numpy arrays onto a surface. At the moment you can only do this if the array has same dimensions as surface and alpha values are not supported.
It would also be great to see a convenience method for converting from opencv numpy formats to pygame ones, but that should probably be a separate request.
@Haperth I think these are what you're looking for? pygame.image.tobytes pygame.image.frombytes
Or maybe the surfarray API?
@oddbookworm Thanks, I had missed those methods. In case anybody else needs this, an opencv format numpy image with transparency can be converted to a pygame surface as follows:
pygame.image.frombytes(array.tobytes(), (array.shape[1], array.shape[0]), 'BGRA')
edit: @llindstrom This method is a few times faster than your make_surface_rgba() method above. can just use
surface = pygame.image.frombytes(array.tobytes(), (array.shape[1], array.shape[0]), 'RGBA')
Can be used inline as it does not lock the surface.