Scripting Labeled Intentions With Python PIL

Date: 2021-07-11 | labeled-intentions | python | pil | art |

Overview

  • I released new art project Labeled Intentions last week
  • I built the design with a script written in Python, leveraging the PIL library
  • In this post, I'll go through why I chose Python for this and walk through the code I used to build it

Context

View this post on Instagram

A post shared by HAMY (@hamy.art)

Labeled Intentions

Labeled Intentions is my newest art project which explores the meaning of labels. It does this by creating text labels (which we'll be talking about here) and then producing shirts with those labels on them (which we won't talk about here though you can read how I do this in: How I make and sell clothes online).

Python PIL

I chose Python over a traditional image editor (like GIMP, Inkscape, Photoshop, etc.) for this project because I saw that it would be way more efficient. Labeled Intentions is a set of several labels, each requiring several outputs. Doing this by hand, with an image editor, would require me to do the same operation multiple times. Plus if I had to change something (the font type, the size, the background) etc. it would require lots of changes to all the different designs.

With code, I can just set new rules and make the computer regenerate everything to the new specs. I wanted to reduce the cost of iteration so I chose code over a traditional image editor.

For those that don't know PIL is an image manipulation library for Python. There are many image manipulation libraries out there but I like PIL because:

  • Good documentation
  • Widely used -> battle hardened
  • Wide functionality
  • Continuously supported and updated

Code

Code Overview

Essentially the code flows like this:

  • A set of labels to be created
  • Foreach label
    • Create 3 images
      • transparent
      • white
      • black
    • Add centered text on each image
    • Save the image

Source Code

Here's the source code.

Note: I run this snippet of code in a generation framework I created so there are a few imports that won't make sense by itself but this still has all the important logic for how I created this. generate_async returns a list of images which I then save using PIL.

File: LabeledIntentionsGenerationRunner.py

  • This is the driver for Labeled Intentions generation
rom typing import Any, List

from PIL import Image

from Domain.GenerationRunners.IGenerationRunner import IGenerationRunner
from Domain.GenerationRunners.IGenerationResult import IGenerationResult
from Domain.GenerationRunners.GenerationResult import GenerationResult
from Domain.Models.Point import Point
from Domain.Models.RGBA import RGBA
from Domain.Utilities.TextUtilities import draw_centered_text_on_image

class LabeledIntentionsGenerationRunner(IGenerationRunner):
    image_size_px: int = 2000
    font_path: str = './resources/HelveticaNeue-Bold.ttf'
    font_size: int = 200

    def __init__(self):
        self.labeled_intentions: List[str] = [
            'HAMBLOH',
            'LABEL',
            'INTENTION',
            # NY
            'HEALING',
            '!DEAD',
            'THRIVING',
            # Purpose
            'ART',
            'BUSINESS',
            'PLEASURE',
            # Color
            'BLACK',
            'RED',
            'WHITE'
        ]

    async def generate_async(
        self,
    ) -> List[IGenerationResult]:
        all_results: List[IGenerationResult] = []

        transparent = RGBA((0,0,0), 0)
        black = RGBA((0,0,0))
        white = RGBA((255, 255, 255))
        center_point = Point(self.image_size_px / 2, self.image_size_px /2) 

        for label in self.labeled_intentions:
            transparent_image = Image.new(
                'RGBA',
                (LabeledIntentionsGenerationRunner.image_size_px, LabeledIntentionsGenerationRunner.image_size_px),
                transparent.to_tuple_alpha())
            black_image = Image.new(
                'RGBA',
                (LabeledIntentionsGenerationRunner.image_size_px, LabeledIntentionsGenerationRunner.image_size_px),
                black.to_tuple_alpha())
            white_image = Image.new(
                'RGBA', 
                (LabeledIntentionsGenerationRunner.image_size_px, LabeledIntentionsGenerationRunner.image_size_px), 
                white.to_tuple_alpha())

            transparent_words_image = draw_centered_text_on_image(
                transparent_image,
                center_point,
                LabeledIntentionsGenerationRunner._get_hambloh_string_from_label(label),
                LabeledIntentionsGenerationRunner.font_path,
                LabeledIntentionsGenerationRunner.font_size,
                white
            )
            black_words_image = draw_centered_text_on_image(
                black_image,
                center_point,
                LabeledIntentionsGenerationRunner._get_hambloh_string_from_label(label),
                LabeledIntentionsGenerationRunner.font_path,
                LabeledIntentionsGenerationRunner.font_size,
                white
            )
            white_words_image = draw_centered_text_on_image(
                white_image,
                center_point,
                LabeledIntentionsGenerationRunner._get_hambloh_string_from_label(label),
                LabeledIntentionsGenerationRunner.font_path,
                LabeledIntentionsGenerationRunner.font_size,
                black
            )

            all_results.append(
                GenerationResult(
                    LabeledIntentionsGenerationRunner._get_file_name(label, 'transparent'),
                    transparent_words_image
                )
            )
            all_results.append(
                GenerationResult(
                    LabeledIntentionsGenerationRunner._get_file_name(label, 'white'),
                    black_words_image
                )
            )
            all_results.append(
                GenerationResult(
                    LabeledIntentionsGenerationRunner._get_file_name(label, 'black'),
                    white_words_image
                )
            )
        
        return all_results

    @staticmethod
    def _get_file_name(
        label: str,
        descriptor: str
    ) -> str:
        return f'{label}-{descriptor}'

    @staticmethod
    def _get_hambloh_string_from_label(
        label: str
    ) -> str:
        return f'\"{label}\"'

File: TextUtilities.py

  • This is how I draw the centered text on each image
from Domain.Models.Point import Point
from Domain.Models.RGBA import RGBA
from PIL import Image, ImageDraw, ImageFont

def draw_centered_text_on_image(
    image: Image,
    center_point: Point,
    text: str,
    font_path: str,
    font_size: int,
    color_rgb: RGBA
) -> Image:
    """
    Draws centered text on image
    """
    text_image = Image.new('RGBA', image.size, (255,255,255,0))

    font = ImageFont.truetype(
        font=font_path,
        size=font_size,
    )
    image_draw = ImageDraw.Draw(text_image)

    text_width, text_height = image_draw.textsize(text, font)

    draw_point = Point(
        (center_point.x - (text_width / 2)),
        (center_point.y - (text_height / 2))
    )

    image_draw.text(
        draw_point.to_tuple_int(),
        text,
        font=font,
        fill=color_rgb.to_bgr_tuple_alpha()
    )

    out_image = Image.alpha_composite(image, text_image)

    return out_image

How I save images:

async def export_image_async(
        self,
        original_image: Image,
        image_name: str
    ) -> None:
        print('exporting image')

        if not os.path.isdir(self.folder_path):
            os.mkdir(self.folder_path)
        
        save_file_path = FileImageExporter._construct_output_file_path(
            self.folder_path,
            image_name,
            self._get_file_ending_for_file_image_type(self.file_image_type)
        )
        original_image.save(save_file_path)
        print(f'image saved to: {save_file_path}')

Next Steps

[] Let me know what you create in the comments below!

Further Reading

Happy generating!

-HAMY.OUT

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.