[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky]
[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Overlay images and text to make exercise numbers with Python and PIL

[This image was made with a background image, mask, text, and text mask in Python and PIL]

This is another program that I wrote for an upcoming book. I wanted an image like the one on the right but with different numbers. This presented several problems in PIL.

  • How to draw outlined text
  • How to make the numbers not mess up the background
  • How to make the numbers not cover the robot's pointer
Here's how the make_exercise_images method (which I'll show you shortly) works.

  1. Load the background image. This includes the robot, whiteboard, and pointer. We'll actually draw this on top of the numbers.
  2. Make a blank image to hold the numbers. We'll draw the numbers here.
  3. Load a fixed mask that is white only where we are allowed to draw the numbers. That includes the whiteboard area except where the pointer lies over it.
  4. Make a numbers mask that's initially black. We will white out the area covered by the numbers.
  5. Make a final mask image. Eventually this will be the same as the numbers mask but with the pointer blacked out so the numbers cannot cover it.
  6. Load the font. (I used Arial bold.)
  7. For each of the exercise numbers:
    1. Black out the numbers mask.
    2. For i in range(5):
      1. Draw the text in black, offset by a small amount, on the numbers image.
      2. Draw the text in white, offset by a small amount, on the numbers mask.
    3. Draw the text in green on the numbers image. At this point, the numbers image holds a green number with a black outline, and the numbers mask is white where the number and its outline are.
    4. Reset the final mask to all black.
    5. Paste the numbers mask onto the final mask, using the fixed mask as a mask. (Yeah, I know that's a lot of masks.) The result is the final mask, which is white in places where the numbers mask is white but only if allowed by the fixed mask (which protects the pointer).
    6. Paste the numbers image onto the background image using the final mask so it only copies in the areas allowed. Those areas are where the numbers are except as protected by the final mask (which protects the pointer).
    7. Save the resulting image.

Yes, I know that's pretty confusing. The following pictures show the background image and masks. You can see how the program combines the number mask and the fixed mask to create the final mask.


Background Image

Fixed Mask

Number Image

Number Mask

Final Mask

Final Image

Here's the method's code. Look back at the list of steps to see what it's doing.

def make_exercise_images(min_number, max_number): '''Make exercise images from min_number to max_number inclusive.''' # Load the background image. bg_image = Image.open('bg_image.jpg') # Make an image to hold the numbers. wid = bg_image.width hgt = bg_image.height fg_image = Image.new('RGBA', (wid, hgt), 'white') fg_dr = ImageDraw.Draw(fg_image) # Load the fixed mask in grayscale. fixed_mask = Image.open('fixed_mask.png').convert('L') # Make the number mask. number_mask = Image.new('L', (wid, hgt), 'black') number_mask_dr = ImageDraw.Draw(number_mask) # Make the final mask. final_mask = Image.new('L', (wid, hgt), 'black') final_mask_dr = ImageDraw.Draw(final_mask) # Define the numbers area. xmin = 40 ymin = 150 xmax = 570 ymax = 670 cx = (xmin + xmax) / 2 cy = (ymin + ymax) / 2 # Define the font. font_size = 300 font = ImageFont.truetype("arialbd.ttf", font_size) # Get the length of the largest number. digits = len(f'{max_number}') # Create the numbered images. for number in range(min_number, max_number + 1): print(number) # Clear the number mask. number_mask_dr.rectangle([0, 0, wid, hgt], fill='black') # Draw the number on the foreground and number mask images. text = f'{number}' text_color = 'lightgreen' outline_color = 'black' for i in range(5): for dx in range(-i, i + 1): for dy in range(-i, i + 1): fg_dr.text((cx+dx, cy+dy), text, font=font, fill=outline_color, anchor='mm', align='center') number_mask_dr.text((cx+dx, cy+dy), text, font=font, fill='white', anchor='mm', align='center') fg_dr.text((cx, cy), text=text, fill=text_color, font=font, anchor='mm', align='center') number_mask.save('final_mask.png') # Save for debugging. # Clear the final mask. final_mask_dr.rectangle([0, 0, wid, hgt], fill='black') # Paste the number mask onto the final mask but # only in places where the fixed mask is white. final_mask.paste(number_mask, (0, 0), fixed_mask) final_mask.save('number_mask.png') # Save for debugging. # Overlay the number picture on the background picuture but # only where the final mask is white. bg_copy = bg_image.copy() bg_copy.paste(fg_image, (0, 0), final_mask) # Save the result. bg_copy.save(f'exercise_{number:0{digits}d}.jpg')

After all that, the main program makes the following call.

make_exercise_images(1, 10)

This creates the files exercise_01.jpg through exercise_10.jpg. (For the actual book, I've created the images through exercise_110.jpg. I'm not sure how many exercises there will eventually be.)

Download the example to experiment with it and to see additional details.

© 2024 Rocky Mountain Computer Consulting, Inc. All rights reserved.