pythonpython-3.xtextpygameglow

Bloom Effect in Pygame so that text glows


I want to copy the effect shown in the Watson-scott test, where the text seems to glow. Link to the example: https://www.youtube.com/watch?v=2ySNm4gltkE

Skip to 11:17 where the text seems to glow; how do I replicate that effect with pygame? I tried adding a greyish rectangle in the background of the text but it just looks awful. I also tried shadowing the text like this example but nothing works.

Also I'm using Python 3.7.4. Thanks for any help I really need it!!


Solution

  • Well sometimes we can say it is not possible, but often times it is just not the main goal of that package. Nonetheless, let's see if we can solve the problem.

    I am taking the liberty of assuming that other packages besides pygame are allowed, but that the end result should be visible in pygame. In order to create the blooming / glowing effect I use the packages opencv-python (cv2) and numpy (np).

    The first part of the solution will talk about creating a glowing border and some glowing text. The second part will talk about how this can be rendered upon a pygame surface.

    TL;DR; Skip to the Summary part below and copy the code in their respective files.

    Part 1

    Blooming

    In order to get some nice glowing borders and text, we can use the blurring functionality of opencv, which is also called smoothing. Since we want to create varying intensity of glowing, we first apply the GaussianBlur, to create some random blurriness around the image, and then extend that blurriness with the normal blur.

    def apply_blooming(image: np.ndarray) -> np.ndarray:
        # Provide some blurring to image, to create some bloom.
        cv2.GaussianBlur(image, ksize=(9, 9), sigmaX=10, sigmaY=10, dst=image)
        cv2.blur(image, ksize=(5, 5), dst=image)
        return image
    

    Note: the values for the kernel sizes (ksize), and the sigmas (sigmaX and sigmaY) have been chosen empirically, you play a bit around with those values, until you get what you want.

    Colors

    A small intermezzo, since we will need to provide some very nice scary coloring, the following class holds some (scary) colors.

    class Colors:
        WHITE_ISH = (246, 246, 246)
        YELLOW_ISH = (214, 198, 136)
        RED_ISH = (156, 60, 60)
    

    Glowing border

    In order to get a glowing border, a helper function was made, that will draw a rectangle with some predefined properties. The chosen properties are:

    def create_border(image: np.ndarray, margin: int, thickness: int, color: Colors) -> np.ndarray:
        """
        Create a normal border around an image, with specified colors.
    
        Args:
            image: The image, that requires a border.
            margin: The border distance from the sides of the image.
            thickness: The thickness of the border.
            color: The border color, by default a slightly yellow color.
    
        Modifies:
            The input image, will be modified with a border.
    
        Returns:
            The same image, with a border inserted.
    
        """
    
        # Numpy uses the convention `rows, columns`, instead of `x, y`.
        # Therefore height, has to be before width.
        height, width = image.shape[:2]
        cv2.rectangle(image, (margin, margin), (width - margin, height - margin), color, thickness=thickness)
        return image
    

    The final border can then be drawn using the apply_blooming and create_border functions.

    def glowing_border(image: np.ndarray, margin=20, thickness=10, color: Colors = Colors.WHITE_ISH):
        """
    
        Create a glowing border around an image.
    
        Args:
            image: The image, that requires a border.
            margin: The border distance from the sides of the image.
            thickness: The thickness of the border.
            color: The border color, by default a slightly yellow color.
    
        Modifies:
            The input image, will be modified with a blooming border.
    
        Returns:
            The same image, with a blooming border inserted.
        """
    
        # Generate yellowish colored box
        image = create_border(image, margin, thickness, color)
    
        # Apply the blooming.
        image = apply_blooming(image)
    
        # Reassert the original border, to get a clear outline.
        # Similar to the Watson-Scott test, two borders were added here.
        image = create_border(image, margin - 1, 1, color)
        image = create_border(image, margin + 1, 1, color)
        return image
    

    Testing code

    In order to test the glowing border, we can use cv2.imshow, to display the image. Since we are going to use this functionality later on, a small function was created. This function will take as input the image, and a displaying time (waiting time before code execution continues).

    
    def show(image, delay=0):
        """
        Display an image using cv2.
    
        Notes:
            By default cv2 uses the BGR coloring, instead RGB.
            Hence image shown by cv2, which are meant to be RGB,
            has to be transformed using `cvtColor`.
    
        Args:
            image: Input image to be displayed
            delay: Time delay before continuing running.
                When 0, The program will wait until a key stroke or window is closed.
                When 1, The program will continue as quickly as possible.
    
        Returns:
            Nothing, it displays the image.
    
        """
        cv2.imshow('Test', cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
        cv2.waitKey(delay)
    

    Actual test code:

    image = np.zeros((480, 640, 3), dtype=np.uint8)
    border = glowing_border(image.copy(), color=Colors.YELLOW_ISH)
    show(border, delay=0)
    

    Glowing text

    A similar approach can be used for the glowing text, by using cv2.putText.

    def glowing_text(image: np.ndarray, text: str, org: Tuple[int, int], color: Colors) -> np.ndarray:
        """
    
        Args:
            image: The image, that requires a border.
            text: The text to be placed on the image.
            org: The starting location of the text.
            color: The color of the text.
    
    
        Modifies:
            The input image, will be modified with a blooming text.
    
        Returns:
            The same image, with a blooming text inserted.
        """
    
        image = cv2.putText(image, text, org, cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale=.7, color=color, thickness=1)
        image = apply_blooming(image)
        image = cv2.putText(image, text, org, cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale=.7, color=color, thickness=1)
        return image
    

    With test code

    image = np.zeros((480, 640, 3), dtype=np.uint8)
    text = glowing_text(image.copy(), text="Welcome to this game", org=(50, 70), color=Colors.YELLOW_ISH)
    show(text, delay=0)
    

    Intermezzo

    Before I go on and show how this can be displayed in pygame, I will throw in a bonus and show how the text can appear on the screen, as if a human was typing it in slowly. The reason that the following code works, is because we separately draw the border and the text, and then combine the results using the np.bitwise_or.

    image = np.zeros((480, 640, 3), dtype=np.uint8)
    
    # Create the glowing border, and a copy of the image, for the text, that will be placed on it later.
    border = glowing_border(image.copy(), color=Colors.YELLOW_ISH)
    text = image.copy()
    
    # This message will be incrementally written
    message = "Welcome to this game. Don't be scared :)."
    
    for idx in range(len(message) + 1):
        text = glowing_text(image.copy(), text=message[:idx], org=(50, 70), color=Colors.YELLOW_ISH)
    
        # We use a random time delay between keystrokes, to simulate a human.
        show(np.bitwise_or(border, text), delay=np.random.randint(1, 250))
    
    # Pause the screen after the full message.
    show(np.bitwise_or(border, text), delay=0)
    

    Note: Alternatively we could first generate the border and text on the same image, and then apply the blooming filter. Just keep in mind that we then have to redraw the border and text again, to give them a solid basis.

    Part 2

    Now that we can generate a canvas with the right blooming border and text, it has to be inserted into pygame. Let's put all the previous functions into a file called blooming.py, and reference it in the new file game.py.

    The following code is a minimal working example of how to put a numpy array into pygame.

    import contextlib
    from typing import Tuple
    
    # This suppresses the `Hello from pygame` message.
    with contextlib.redirect_stdout(None):
        import pygame
    
    import numpy as np
    import blooming
    
    
    def image_generator(size: Tuple[int, int], color: blooming.Colors):
        image = np.zeros((*size[::-1], 3), dtype=np.uint8)
    
        # Create the glowing border, and a copy of the image, for the text, that will be placed on it later.
        border = blooming.glowing_border(image.copy(), color=color)
        text = image.copy()
    
        # This message will be incrementally written
        message = "Welcome to this game. Don't be scared :)."
    
        for idx in range(len(message) + 1):
            text = blooming.glowing_text(image.copy(), text=message[:idx], org=(50, 70), color=color)
            yield np.bitwise_or(border, text)
        return np.bitwise_or(border, text)
    
    
    if __name__ == '__main__':
        pygame.init()
        screen = pygame.display.set_mode((640, 480))
        clock = pygame.time.Clock()
        running = True
    
        while running:
            for image in image_generator(screen.get_size(), color=blooming.Colors.YELLOW_ISH):
                screen.fill((0, 0, 0))
    
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        running = False
    
                    if event.type == pygame.KEYDOWN:
                        if event.key == pygame.K_ESCAPE:
                            running = False
    
                # This is where we insert the numpy array.
                # Because pygame and numpy use different coordinate systems,
                # the numpy image has to be flipped and rotated, before being blit.
                img = pygame.surfarray.make_surface(np.fliplr(np.rot90(image, k=-1)))
                screen.blit(img, (0, 0))
    
                pygame.display.flip()
                clock.tick(np.random.randint(10, 30))
    
        pygame.quit()
    

    Summary (TL;DR;)

    from typing import Tuple
    
    import cv2
    import numpy as np
    
    
    class Colors:
        WHITE_ISH = (246, 246, 246)
        YELLOW_ISH = (214, 198, 136)
        RED_ISH = (156, 60, 60)
    
    
    def create_border(image: np.ndarray, margin: int, thickness: int, color: Colors) -> np.ndarray:
        """
        Create a normal border around an image, with specified colors.
    
        Args:
            image: The image, that requires a border.
            margin: The border distance from the sides of the image.
            thickness: The thickness of the border.
            color: The border color, by default a slightly yellow color.
    
        Modifies:
            The input image, will be modified with a border.
    
        Returns:
            The same image, with a border inserted.
    
        """
    
        # Numpy uses the convention `rows, columns`, instead of `x, y`.
        # Therefore height, has to be before width.
        height, width = image.shape[:2]
        cv2.rectangle(image, (margin, margin), (width - margin, height - margin), color, thickness=thickness)
        return image
    
    
    def apply_blooming(image: np.ndarray) -> np.ndarray:
        # Provide some blurring to image, to create some bloom.
        cv2.GaussianBlur(image, ksize=(9, 9), sigmaX=10, sigmaY=10, dst=image)
        cv2.blur(image, ksize=(5, 5), dst=image)
        return image
    
    
    def glowing_border(image: np.ndarray, margin=20, thickness=10, color: Colors = Colors.WHITE_ISH):
        """
    
        Create a glowing border around an image.
    
        Args:
            image: The image, that requires a border.
            margin: The border distance from the sides of the image.
            thickness: The thickness of the border.
            color: The border color, by default a slightly yellow color.
    
        Modifies:
            The input image, will be modified with a blooming border.
    
        Returns:
            The same image, with a blooming border inserted.
        """
    
        # Generate yellowish colored box
        image = create_border(image, margin, thickness, color)
    
        # Apply the blooming.
        image = apply_blooming(image)
    
        # Reassert the original border, to get a clear outline.
        # Similar to the Watson-Scott test, two borders were added here.
        image = create_border(image, margin - 1, 1, color)
        image = create_border(image, margin + 1, 1, color)
        return image
    
    
    def glowing_text(image: np.ndarray, text: str, org: Tuple[int, int], color: Colors) -> np.ndarray:
        """
    
        Args:
            image: The image, that requires a border.
            text: The text to be placed on the image.
            org: The starting location of the text.
            color: The color of the text.
    
    
        Modifies:
            The input image, will be modified with a blooming text.
    
        Returns:
            The same image, with a blooming text inserted.
        """
    
        image = cv2.putText(image, text, org, cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale=.7, color=color, thickness=1)
        image = apply_blooming(image)
        image = cv2.putText(image, text, org, cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale=.7, color=color, thickness=1)
        return image
    
    
    def show(image, delay=0):
        """
        Display an image using cv2.
    
        Notes:
            By default cv2 uses the BGR coloring, instead RGB.
            Hence image shown by cv2, which are meant to be RGB,
            has to be transformed using `cvtColor`.
    
        Args:
            image: Input image to be displayed
            delay: Time delay before continuing running.
                When 0, The program will wait until a key stroke or window is closed.
                When 1, The program will continue as quickly as possible.
    
        Returns:
            Nothing, it displays the image.
    
        """
        cv2.imshow('Test', cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
        cv2.waitKey(delay)
    
    
    if __name__ == '__main__':
        image = np.zeros((480, 640, 3), dtype=np.uint8)
    
        # Create the glowing border, and a copy of the image, for the text, that will be placed on it later.
        border = glowing_border(image.copy(), color=Colors.YELLOW_ISH)
        text = image.copy()
    
        # This message will be incrementally written
        message = "Welcome to this game. Don't be scared :)." + " " * 10
    
        for idx in range(len(message) + 1):
            text = glowing_text(image.copy(), text=message[:idx], org=(50, 70), color=Colors.YELLOW_ISH)
    
            # We use a random time delay between keystrokes, to simulate a human.
            show(np.bitwise_or(border, text), delay=np.random.randint(1, 250))
    
        # Pause the screen after the full message.
        show(np.bitwise_or(border, text), delay=0)
    
    import contextlib
    from typing import Tuple
    
    # This suppresses the `Hello from pygame` message.
    with contextlib.redirect_stdout(None):
        import pygame
    
    import numpy as np
    import blooming
    
    
    def image_generator(size: Tuple[int, int], color: blooming.Colors):
        image = np.zeros((*size[::-1], 3), dtype=np.uint8)
    
        # Create the glowing border, and a copy of the image, for the text, that will be placed on it later.
        border = blooming.glowing_border(image.copy(), color=color)
        text = image.copy()
    
        # This message will be incrementally written
        message = "Welcome to this game. Don't be scared :)." + " " * 10
    
        for idx in range(len(message) + 1):
            text = blooming.glowing_text(image.copy(), text=message[:idx], org=(50, 70), color=color)
            yield np.bitwise_or(border, text)
        return np.bitwise_or(border, text)
    
    
    if __name__ == '__main__':
        pygame.init()
        screen = pygame.display.set_mode((640, 480))
        clock = pygame.time.Clock()
        running = True
    
        while running:
            for image in image_generator(screen.get_size(), color=blooming.Colors.YELLOW_ISH):
                screen.fill((0, 0, 0))
    
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        running = False
    
                    if event.type == pygame.KEYDOWN:
                        if event.key == pygame.K_ESCAPE:
                            running = False
    
                # This is where we insert the numpy array.
                # Because pygame and numpy use different coordinate systems,
                # the numpy image has to be flipped and rotated, before being blit.
                img = pygame.surfarray.make_surface(np.fliplr(np.rot90(image, k=-1)))
                screen.blit(img, (0, 0))
    
                pygame.display.flip()
                clock.tick(np.random.randint(10, 30))
    
        pygame.quit()
    

    Result

    Please note that the real thing looks a lot sharper than this image. Also playing around with the thickness of the text and the sizes of the blurring filters will influence the result quite a bit. For this image, the ksize of the GaussianBlur has been increased to (17, 17), and the sigmaX and sigmaY have both been put to 100.

    enter image description here