pygame-community / pygame-ce

🐍🎮 pygame - Community Edition is a FOSS Python library for multimedia applications (like games). Built on top of the excellent SDL library.
https://pyga.me
822 stars 131 forks source link

allow pygame.surfarray to create alpha channels (1244) #740

Open GalacticEmperor1 opened 1 year ago

GalacticEmperor1 commented 1 year ago

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. :)

feiyuhuahuo commented 3 months ago

Will surfarray.make_surface support create alpha channel finally? Since this issue is still open and has an [enhancement] tag.

Haperth commented 4 days ago

@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.

oddbookworm commented 4 days ago

@Haperth I think these are what you're looking for? pygame.image.tobytes pygame.image.frombytes

Or maybe the surfarray API?

Haperth commented 1 day ago

@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')