How do I change the colour of an image in Pygame without changing its transparency?

Question:

It sounds like a simple thing to do , but I’m struggling.
I have a png image of a rectangle with a transparent centre called “zero”. I want to change the colour of the visible part of the image. To try and change it, I have tried using “zero.fill” but no option I try changes only the non-transparent part, or it merges the new colour with the old and stays like that.
I’m not keen on installing numpy as I wish to take the finished program to a friend who doesn’t have it. Any simple suggestions welcome.

Asked By: John_F

||

Answers:

I’ve got an example that works with per pixel alpha surfaces that can also be translucent. The fill function just loops over the surface’s pixels and sets them to the new color, but keeps their alpha value. It’s probably not recommendable to do this every frame with many surfaces. (Press f, g, h to change the color.)

import sys
import pygame as pg


def fill(surface, color):
    """Fill all pixels of the surface with color, preserve transparency."""
    w, h = surface.get_size()
    r, g, b, _ = color
    for x in range(w):
        for y in range(h):
            a = surface.get_at((x, y))[3]
            surface.set_at((x, y), pg.Color(r, g, b, a))


def main():
    screen = pg.display.set_mode((640, 480))
    clock = pg.time.Clock()

    # Uncomment this for a non-translucent surface.
    # surface = pg.Surface((100, 150), pg.SRCALPHA)
    # pg.draw.circle(surface, pg.Color(40, 240, 120), (50, 50), 50)
    surface = pg.image.load('bullet2.png').convert_alpha()
    surface = pg.transform.rotozoom(surface, 0, 2)

    done = False

    while not done:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
            if event.type == pg.KEYDOWN:
                if event.key == pg.K_f:
                    fill(surface, pg.Color(240, 200, 40))
                if event.key == pg.K_g:
                    fill(surface, pg.Color(250, 10, 40))
                if event.key == pg.K_h:
                    fill(surface, pg.Color(40, 240, 120))

        screen.fill(pg.Color('lightskyblue4'))
        pg.draw.rect(screen, pg.Color(40, 50, 50), (210, 210, 50, 90))
        screen.blit(surface, (200, 200))

        pg.display.flip()
        clock.tick(30)


if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()
    sys.exit()

bullet2.png

Answered By: skrx

In this version, the visible and transparent parts are the other way round to the original question as per Ankur’s suggestion.
Here is the essential working code:

import pygame, sys
from pygame.locals import *
pygame.init()

def load_image(name):
    image = pygame.image.load(name).convert()
    return image

def resize(obj, w, h):
    global scale
    return pygame.transform.scale(obj, (int(w * scale), int(h * scale)))


pink = (255, 0, 160)
red = (255, 0, 0)
peach = (255, 118, 95)
blue = (0, 0, 255)
blue_1 = (38, 0, 160)
dark_yellow = (255, 174, 0)
green = (38, 137, 0)
orange = (255, 81, 0)
colour = [pink, red, peach, blue, blue_1, dark_yellow, green, orange, green]
clock = pygame.time.Clock()
scale = 4
screen = pygame.display.set_mode((292 * scale, 240 * scale),0, 32)
banner = load_image("banner.png") #292x35
zero = load_image("zero.png") #5x7
c = 0
while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
    rgb = colour[c]
    c = c + 1
    if c > 7:
        c = 0
    pygame.draw.line(banner, rgb, (53, 21), (53, 27), 5) #line the same size as zero image
    banner.blit(zero, (51, 21)) #blit image with transparency over line
    large_banner = resize(banner, 292, 35)
    screen.blit(large_banner, (0, 0))
    clock.tick(120)
    pygame.display.flip()

pygame.quit()
sys.exit()
Answered By: John_F

This code for drop all color brightness

    def drop(surface):
        w, h = surface.get_size()
        for x in range(w):
            for y in range(h):
                r = surface.get_at((x, y))[0]
                if r>150:
                   r-=50
                g = surface.get_at((x, y))[1]
                if g>150:
                   g-=50
                b = surface.get_at((x, y))[2]
                if b>150:
                   b-=50
                a = surface.get_at((x, y))[3]
                surface.set_at((x, y), pygame.Color(r, g, b, a)) 

    img = pygame.image.load("image.png").convert_alpha()
    drop(img)

I solved this problem a little bit differently than others appear to, but I was trying to figure this out and the answers listed here did not give me exactly what I was looking for. I’ve slowly built off of what I’ve found to to accomplish my goals.

Firstly, I just wanted to change the colour of my animation frames to a random colour chosen from a list. The method I discovered used pygame.PixelAray() and .replace() to swap one colour for another:

self.fire_frame_list = glob.glob("pictures\Boom_Pow\Colour_Flame\*.png")
self.fire_frame_list.sort()
self.fire_frame_anim = [pygame.PixelArray(pygame.image.load(path)) for path in self.fire_frame_list]
...
self.fire_img = self.fire_frame_anim[self.frame_index]
self.fire_img.replace(self.colour, self.new_colour)
...
self.colour = self.new_colour
self.new_colour = randRGB()

In this code I gather the paths of my animation frames, sort them, and then load and convert the images into PixelArays. I seclect my current image using a variable called frame_index, and then change the colour for that frame. However, when I decided to change the colour midway through the animation, I encountered a problem; the second half of my animation – the frames that came after I had changed the desired colour – were their original colour – white.

In order to remedy this, I had to ensure that all the animation frames had their colour changed upon creation.

self.fire_frame_list = glob.glob("pictures\Boom_Pow\Colour_Flame\*.png")
self.fire_frame_list.sort()
self.fire_frame_anim = [pygame.PixelArray(pygame.image.load(path)) for path in self.fire_frame_list]
for img in self.fire_frame_anim:
    img.replace((255,255,255), self.new_colour)

For a while this worked perfectly, until I tried using this code with a translucent image. I made sure to use .convert_alpha() on my image so as to keep my base colour as white and have the alpha value be the changed value. This worked, but not with my PixelArrays.

Although printing a list of my pixels with each RGBA value listed showed the correct values, .replace() only changed the colour of the pixels with full opacity, leaving the rest white. After much experimenting, I realized I needed to create a list of my pizel alpha values using the same method I had used to check that .convert_alpha() had given me the values I wanted.

self.img = pygame.image.load("pictures\Boom_Pow\Bubbloid.png").convert_alpha()
self.pix = {self.img.get_at((i, j))[3] for j in range(10) for i in range(10)}
self.img = pygame.PixelArray(self.img)

for a in self.pix:
    self.img.replace((255, 255, 255, a), self.colour + (a,))

To start, I loaded my image and converted it to its alpha values. Next, I created a dictionary where each key was a possible pixel alpha value – the [3] is the index number for the alpha values in the colour list. Using dictionary keys ensured that I would not have any duplicate items. I then converted my images into PixelArrays and ran a loop that changed the colour for each of my possible alpha values.

This has been working the way that I wish, however I believe it is important to state that I only had 4 levels of transparency, and I do not know how python would handle having to run through tens or even hundreds of levels of transparency. However, this method works for the simple images I have created, and I hope I was able to help someone else as well.
Many of my little bubbles, filled with a random colour
My lantern, colour change midway through animation

Answered By: Amaranthiora
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.