1

I'm trying to apply gaussian blur to a PIL.Image which has transparency, but blurring doesn't seem to be applied to the alpha channel.

Here's my code:

import wx
from PIL import Image, ImageFilter
import io

class HaloButton(wx.Button):
    def __init__(self, parent, image, id=wx.ID_ANY, label='', pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=wx.ButtonNameStr):
        super().__init__(parent, id, label, pos, size, style, validator, name)
        self._image = image.copy()
        self.Bind(wx.EVT_PAINT, self._OnPaint)

    def _OnPaint(self, event):
        dc = wx.PaintDC(self)
        dc.Clear()
        pil_image = self._image.copy()
        blurred_image = pil_image.filter(ImageFilter.GaussianBlur(radius=15))
        bitmap = wx.Bitmap.FromBufferRGBA(blurred_image.size[0], blurred_image.size[1], blurred_image.tobytes())
        dc.DrawBitmap(bitmap, 0, 0)


if __name__ == '__main__':
    app = wx.App()

    frame = wx.Frame(parent=None, title='Halo Button Example', size=(400, 400), style=wx.DEFAULT_FRAME_STYLE)
    frame.SetBackgroundColour((255, 192, 203))  # Rose

    panel = wx.Panel(frame, size=(400, 400), style=wx.SUNKEN_BORDER)
    pil_image = Image.open("image.png")
    button = HaloButton(panel, pil_image, size=(100, 100))
    button.SetPosition((150, 150))

    frame.Show()
    app.MainLoop()

image.png (which is transparent around the yellow circle):
enter image description here

and the resulting frame:
enter image description here

I see two issues. Obviously, the background color of the image is not the same as the frame, even though the original image has transparent background. The second is that where there is blur, it doesn't seem to merge the yellow from the circle, neither with the pink of the window nor the white background of the image. Instead, there is some kind of a dark component.

What am I doing wrong?

EDIT:
What I expected was something like this:
enter image description here

which was made with GIMP by applying a gaussian blur to the image and putting a pink layer underneath.

3 Answers 3

0

With blur:

with blur

import wx
from PIL import Image, ImageFilter
import io

class HaloButton(wx.Button):
    def __init__(self, parent, image, id=wx.ID_ANY, label='', pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=wx.ButtonNameStr):
        super().__init__(parent, id, label, pos, size, style, validator, name)
    
        self._image = image.copy()

        self.Bind(wx.EVT_PAINT, self.OnPaint)
    
    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        #dc.Clear()
        pil_image = self._image.copy()
        blurred_image = pil_image.filter(ImageFilter.GaussianBlur(radius=15))
        bitmap = wx.Bitmap.FromBufferRGBA(blurred_image.size[0], blurred_image.size[1], blurred_image.tobytes())
        dc.DrawBitmap(bitmap, 0, 0)

class Frame(wx.Frame):
    def __init__(self, parent, id, title):
        super().__init__(parent, -1, title, size=(400, 400), style=wx.DEFAULT_FRAME_STYLE | wx.FULL_REPAINT_ON_RESIZE)

        self.SetBackgroundColour((255, 192, 203))  # Rose
        self.panel = wx.Panel(self, size=(400, 400), style=wx.SUNKEN_BORDER| wx.TRANSPARENT_WINDOW)
        self.Bind(wx.EVT_BUTTON, self.OnClick)
        self.panel.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
        pil_image = Image.open("image.png").convert('RGBA')
        button = HaloButton(self.panel, pil_image, size=(100, 100))
        button.SetPosition((150, 150))

    def OnEnter(self, event):   # setHover
        print("Refresh")
        self.Refresh()

    def OnClick(self, event):
        print("Click! (%d)\n" % event.GetId())
        self.Refresh()
    
class App(wx.App):
    def OnInit(self):
        frame = Frame(None, -1, "Halo Button Example")
        self.SetTopWindow(frame)
        frame.Centre()
        frame.Show(True)

        return True

def main():
    app = App(False)
    app.MainLoop()

if __name__ == "__main__" :
    main()

and without blur:

without blur

import wx
from PIL import Image, ImageFilter
import io

class HaloButton(wx.Button):
    def __init__(self, parent, image, id=wx.ID_ANY, label='', pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=wx.ButtonNameStr):
        super().__init__(parent, id, label, pos, size, style, validator, name)

        self._image = image.copy()
    
        self.Bind(wx.EVT_PAINT, self.OnPaint)

    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        #dc.Clear()
        pil_image = self._image.copy()
        blurred_image = pil_image.filter(ImageFilter.GaussianBlur(radius=15))
        blurred_image.putalpha(pil_image.getchannel('A'))  # Preserve transparency
        bitmap = wx.Bitmap.FromBufferRGBA(blurred_image.size[0], blurred_image.size[1], blurred_image.tobytes())
        dc.DrawBitmap(bitmap, 0, 0)

class Frame(wx.Frame):
    def __init__(self, parent, id, title):
        super().__init__(parent, -1, title, size=(400, 400), style=wx.DEFAULT_FRAME_STYLE | wx.FULL_REPAINT_ON_RESIZE)

        self.SetBackgroundColour((255, 192, 203))  # Rose
        self.panel = wx.Panel(self, size=(400, 400), style=wx.SUNKEN_BORDER| wx.TRANSPARENT_WINDOW)
        self.Bind(wx.EVT_BUTTON, self.OnClick)
        self.panel.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
        pil_image = Image.open("image.png").convert('RGBA')
        button = HaloButton(self.panel, pil_image, size=(100, 100))
        button.SetPosition((150, 150))

    def OnEnter(self, event):   # setHover
        print("Refresh")
        self.Refresh()

    def OnClick(self, event):
        print("Click! (%d)\n" % event.GetId())
        self.Refresh()
    
class App(wx.App):
    def OnInit(self):
        frame = Frame(None, -1, "Halo Button Example")
        self.SetTopWindow(frame)
        frame.Centre()
        frame.Show(True)

        return True

def main():
    app = App(False)
    app.MainLoop()

if __name__ == "__main__" :
    main()
2
  • As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
    – Community Bot
    Commented Jul 7 at 6:34
  • Thanks for your answer. It seems that the only difference between the two solutions you provided is this instruction: blurred_image.putalpha(pil_image.getchannel('A')) # Preserve transparency But the result is not what I expected, since there is still this dark component is the blurred color mixing yellow with pink. What I expected is this kind of image: ibb.co/HTzzGDS wich was made by applying a gaussian blur to the image and putting a pink layer underneath.
    – Benco
    Commented Jul 7 at 9:23
0

Two things.

First, everywhere your image is completely transparent the color has been set to black. That black is getting mixed into the blurred edges. Since your image is a constant color (255,255,0), that's easy to fix:

pix = pil_image.load()
for y in range(pil_image.size[1]):
    for x in range(pil_image.size[0]):
        if pix[x,y][3] == 0:
            pix[x,y] = (255, 255, 0, 0)

You'll need to make your image bigger because it will grow when you blur it.

The second problem is that apparently wx doesn't do blending when drawing the button image. I don't know how to fix that, but you can blend the image with your background color before you pass it to the button.

0

Result displayed image

Tested:

  • Python 3.11.6
  • wxPython 4.2.2a1
  • wxWidgets 3.2.3
  • Windows 10

imageForTest

"""

The image obtained is the one sought. However, the button code does not work perfectly. 
Refreshing the image is hazardous and sometimes only the focus is displayed !-(

"""

import wx
from PIL import Image, ImageFilter
import io

class HaloButton(wx.Button):
    def __init__(self, parent, image, id=wx.ID_ANY, label='', pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=wx.ButtonNameStr):
        super().__init__(parent, id, label, pos, size, style, validator, name)
    
        self._image = image.copy()

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        
    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        #pil_image = self._image.copy()
        blurred_image = self._image.filter(ImageFilter.GaussianBlur(radius=15))
        pix = self._image.load()
        for y in range(self._image.size[1]):
            for x in range(self._image.size[0]):
                if pix[x,y][3] == 0:
                    pix[x,y] = (255, 255, 0, 0)
                
        bitmap = wx.Bitmap.FromBufferRGBA(blurred_image.size[0], blurred_image.size[1], blurred_image.tobytes())
        dc.DrawBitmap(bitmap, 0, 0)

class Frame(wx.Frame):
    def __init__(self, parent, id, title):
        super().__init__(parent, -1, title, size=(400, 400),
                         style=wx.DEFAULT_FRAME_STYLE | wx.FULL_REPAINT_ON_RESIZE)

        self.panel = wx.Panel(self, size=(400, 400), style=wx.SUNKEN_BORDER)
        self.panel.SetBackgroundColour((255, 192, 203))  # Rose

        self.Bind(wx.EVT_BUTTON, self.OnClick)
        self.panel.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
    
        pil_image = Image.open("image.png").convert('RGBA')
        button = HaloButton(self.panel, pil_image, size=(100, 100))
        button.SetPosition((150, 150))
        button.SetBackgroundColour((255, 192, 203))  # Rose
    
    def OnEnter(self, event):   # setHover
        print("Refresh")
        self.Refresh()

    def OnClick(self, event):
        print("Click! (%d)\n" % event.GetId())
        self.Refresh()
    
class App(wx.App):
    def OnInit(self):
        frame = Frame(None, -1, "Halo Button Example")
        self.SetTopWindow(frame)
        frame.Centre()
        frame.Show(True)

    return True

def main():
    app = App(False)
    app.MainLoop()

if __name__ == "__main__" :
    main()
1
  • I tried to modify your code in order to not compute bitmap each time the frame refreshes. I just save the blurred bitmap as an attribute that is displayed each time the frame refreshes. Oddly, even though your code produces the right image, this one doesn't, it still has the black background issue. And I really don't understand why...
    – Benco
    Commented Jul 11 at 15:17

Not the answer you're looking for? Browse other questions tagged or ask your own question.