The problem isn't with the "bloom" algorithm, but with the way you blit
the surface to the target.
Not just the alpha channel is blurred, also the color channels are blurred. Therefore you have to blit
the Surface with the special flag BLEND_PREMULTIPLIED
:
bloom_surf = Bloom(source_surf)
screen.blit(bloom_surf, (x, y), pygame.BLEND_PREMULTIPLIED)
It is also possible to cancel this effect in the Bloom
function by dividing the color channels by the alpha channel:
r' = r / a
g' = g / a
b' = b / a
a' = a
Each color channel is coded in a byte in the range [0, 255]. That makes the conversion a little trickier:
def BloomNoPremultipliedAlpha(canvas: pygame.Surface):
size = canvas.get_size()
canvas_color = pygame.surfarray.array2d(canvas)
canvas_rgba = canvas_color.view(dtype=np.uint8).reshape((*canvas_color.shape, 4))
newCanvas = pygame.Surface(size, pygame.SRCALPHA)
cv2.blur(canvas_rgba, ksize=(25, 25), dst=canvas_rgba)
canvas_rgba[:,:,0:3] = canvas_rgba[:,:,0:3] * 255.0 / canvas_rgba[:,:,[3,3,3]]
pygame.surfarray.blit_array(newCanvas, canvas_color)
return newCanvas
See the following minimal example that demonstrates the difference:

left: Bloom
; center: Bloom
+ BLEND_PREMULTIPLIED
; right: BloomNoPremultipliedAlpha
import pygame, cv2
import numpy as np
def Bloom(canvas: pygame.Surface):
size = canvas.get_size()
canvas_color = pygame.surfarray.array2d(canvas)
canvas_rgba = canvas_color.view(dtype=np.uint8).reshape((*canvas_color.shape, 4))
newCanvas = pygame.Surface(size, pygame.SRCALPHA)
cv2.blur(canvas_rgba, ksize=(25, 25), dst=canvas_rgba)
pygame.surfarray.blit_array(newCanvas, canvas_color)
return newCanvas
def BloomNoPremultipliedAlpha(canvas: pygame.Surface):
size = canvas.get_size()
canvas_color = pygame.surfarray.array2d(canvas)
canvas_rgba = canvas_color.view(dtype=np.uint8).reshape((*canvas_color.shape, 4))
newCanvas = pygame.Surface(size, pygame.SRCALPHA)
cv2.blur(canvas_rgba, ksize=(25, 25), dst=canvas_rgba)
canvas_rgba[:,:,0:3] = canvas_rgba[:,:,0:3] * 255.0 / canvas_rgba[:,:,[3,3,3]]
pygame.surfarray.blit_array(newCanvas, canvas_color)
return newCanvas
pygame.init()
window = pygame.display.set_mode((800, 300))
clock = pygame.time.Clock()
background = pygame.Surface(window.get_size())
ts, w, h, c1, c2 = 100, *window.get_size(), (160, 160, 160), (96, 96, 96)
tiles = [((x*ts, y*ts, ts, ts), c1 if (x+y) % 2 == 0 else c2) for x in range((w+ts-1)//ts) for y in range((h+ts-1)//ts)]
[pygame.draw.rect(background, color, rect) for rect, color in tiles]
surface = pygame.Surface((250, 250), pygame.SRCALPHA)
pygame.draw.circle(surface, (255, 255, 255), surface.get_rect().center, 100)
surface1 = Bloom(surface)
surface2 = Bloom(surface)
surface3 = BloomNoPremultipliedAlpha(surface)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.blit(background, (0, 0))
window.blit(surface1, surface1.get_rect(center = (150, 150)))
window.blit(surface2, surface2.get_rect(center = (400, 150)), special_flags = pygame.BLEND_PREMULTIPLIED)
window.blit(surface3, surface1.get_rect(center = (650, 150)))
pygame.display.flip()
clock.tick(60)
pygame.quit()
exit()