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!!
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.
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.
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)
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
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)
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)
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.
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()
blooming.py
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)
game.py
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()
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.