pythonpygame

How to decrease the brightness of an image in PyGame?


I know there's a similar question here - How can I change the brightness of an image in pygame? - but the answer only explains how to increase the brightness, without referring to how to decrease the brightness. I want to create an animation where an image increases in brightness, but then decreases in the same manner. I've tried using this code:

running = True
brightness = 1
brightstep = 1
while running:
    colour = (brightness, brightness, brightness)
    canvas.fill((255, 255, 255))
    canvas.blit(bg, (0, 0))
    canvas.blit(img, (0, 0))
    if brightness == 51:
        brightstep = -1
        brightness = 50
    elif brightstep == 1:
        brightness += brightstep
        img.fill(colour, special_flags=pygame.BLEND_RGB_ADD)
    elif brightstep == -1:
        brightness += brightstep
        img.fill(colour, special_flags=pygame.BLEND_RGB_SUB)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    pygame.display.update()

The images have already been loaded so that's not causing the error. I know it's something to do with the special_flags when I'm decreasing the brightness.


Solution

  • Alright, updates!

    The Problem

    The problem is that you are performing a destructive modification to your images. You can't go backwards.

    In simple terms, you can only add increasing whiteness to a color before that color is pure white (or close to it). Once you have a pixel that is that washed out, there is no way to get back to what the color originally was -- that information is lost!

    The correct way to manage brightness is not to touch the original image data at all. Use another solid-color image over top of the original image to mix in color.

    Pure PyGame Method

    Here is an example of how to do that in pure PyGame. It uses a couple of images that I snagged off of the internet.

    background.jpg background.jpg Beggin, by Sonja Cristoph @BlenderArtists.org
    Resized to 600×328 pixels

    sprite.png
    sprite.png
    AI-generated image snagged from freepik.com
    Resized to 200×219 pixels

    Now for the code! Wave the sprite (get it?) around and see her turn from shade to pixie (get it?). If you hold the mouse button down you keep her mood stable.

    import pygame, sys
    from pygame.locals import *
    from pygame.math import Vector2
    
    pygame.init()
    pygame.mouse.set_visible( False )
    
    # Load images
    background    = pygame.image.load( "background.jpg" );
    sprite        = pygame.image.load( "sprite.png" )
    sprite_offset = Vector2( sprite.get_width() / 2, sprite.get_height() / 2 )
    
    # Create main window
    display       = pygame.display.set_mode( background.get_size() )
    
    # Shade zones
    edge_width    = 25
    dark_min      =                           edge_width
    dark_max      = display.get_width() / 2 - edge_width
    light_min     = display.get_width() / 2 + edge_width
    light_max     = display.get_width()     - edge_width
    
    # The image we actually display
    shaded_sprite = sprite
    
    
    def darken( percent ):
        # A simple linear function looks good here
        L = int( 255 * percent )
        shaded_sprite.fill( (L,L,L), special_flags=pygame.BLEND_RGB_MULT )
    
        
    def lighten( percent ):
        # A decelerator function gets us a little more color before total whiteness
        # (Exponent found by a little experimentation)
        L = int( 255 - 255 * (1 - percent) ** .5 )
        shaded_sprite.fill( (L,L,L), special_flags=pygame.BLEND_RGB_ADD )
    
    
    # Main loop
    while True:
    
        # If (Window closed) or (Esc key pressed)
        for event in pygame.event.get():
            if ((event.type == QUIT)
            or ((event.type == KEYDOWN) and (event.key == 27))):
                pygame.quit()
                sys.exit()
    
        # Update the sprite's shade (unless the mouse button is held down)
        if not pygame.mouse.get_pressed()[0]:
            x = pygame.mouse.get_pos()[0]
            shaded_sprite = sprite.copy() 
            if   x < dark_min:  darken ( 0 )
            elif x < dark_max:  darken ( (x - dark_min) / (dark_max - dark_min) )
            elif x < light_min: pass
            elif x < light_max: lighten( (x - light_min) / (light_max - light_min) )
            else:               lighten( 1 )
    
        # Draw everything
        display.blit( background, (0, 0) )
        display.blit( shaded_sprite, pygame.mouse.get_pos() - sprite_offset )
        pygame.display.update()
    

    The trick is to:

    1. Create a copy of the original image, to which we
    2. Apply a quick transform by blitting a color on top of it.

    To darken the image a simple linear multiplication works just fine. You see her eyes to the very end.

    To lighten the image a simple linear addition washes things out too quickly, so I used an interpolation function called a decelerator, which has the form f(t, n) := 1 - (1 - t)**n. That gives us a nicer curve that goes slowly toward 1.0 until near the very end, where it quickly rises to 1.0. I found the value n=0.5 by a little experimentation. This produces the same effect as the darken — you see her eyes until the very end. Try dragging back and forth to explore that aspect of it.

    Notice the PyGame blit flags:

    Caveats

    This is the pure PyGame (CPU-bound) method for doing it. The consequence is that this is relatively slow, meaning that you should avoid doing this to large numbers of sprites at once.

    Notice also how the code is designed to update the shaded sprite only as needed. (The program kind of does it anyway, unless you hold the mouse button down, but the design is to make it only necessary to update iff the shade changes.)

    If you wish to do more than this, or to just be blindingly fast anyway because you profiled it and discovered that this is a bottleneck, you may wish to consider a solution using the pygame_shader module, which allows pygame to use GPU-bound OpenGL stuff.

    In particular, you could use a GLES shader fragment to do this kind of stuff, and even more awesome things — all without bogging down your CPU with a bazillion sprite manipulations every frame.