Chris Sprance

Character Technical Director

Dilation in PIL

Recently I wanted to try and figure out how dilation works and if I could replicate it inside of python using nothing but the PIL library. It's a pretty straightforward bit of code.

I found a tutorial online by Ryan Brucks on how to do Dilation Inside of Unreal and applied that to Python.

First lets check out the code

dilate.py
from PIL import Image

OPERATIONS = (
    lambda idx, width: idx - 1,  # left pixel
    lambda idx, width: idx - width,  # top pixel
    lambda idx, width: idx + 1,  # right pixel
    lambda idx, width: idx + width,  # bottom pixel
    lambda idx, width: idx - width - 1,  # top left pixel
    lambda idx, width: idx - width + 1,  # top right pixel
    lambda idx, width: idx + width - 1,  # botom right pixel
    lambda idx, width: idx + width + 1,  # bottom left pixel
)


def dilate_texture(img: Image, iterations: int = 1):
    """
    This function goes through each pixel and samples it's 8 neighbors and finds a non 0 neighbor to
    replace the current pixels color with
    :param img: Image to dilate
    :param iterations: How many times to run the dilate filter
    :return: Image the dilated image
    """
    width = img.width
    original_alpha = img.split()[-1]
    for i in range(iterations):
        pixels = img.getdata()  # create the pixel map
        data = list()
        for idx, pixel in enumerate(pixels):  # for every pixel:
            # add our data in
            data.append(pixel)
            alpha = pixel[-1]
            # if alpha is 0 we want to search for a neighbor with alpha
            if alpha == 0:
                for op in OPERATIONS:
                    try:
                        # find neighbor pixels
                        r, g, b, a = pixels[op(idx, width)]
                        # find the first non 0 alpha neighbor
                        if a != 0:
                            # replace the color we set earlier with neighbors
                            data[idx] = (r, g, b, a)
                            # bail out and move on to next pixel if we find a neighbor
                            break
                    except IndexError:
                        pass
        img.putdata(data)
    img.putalpha(original_alpha)
    return img


img = Image.open("dilate_test_undilated.png")
img = dilate_texture(img, 10)
img.save("dilate_test_dilated.tga")

The undilated image on the left and the image dilated with one iteration on the right.

It should be noted that this is certainly not fast by any means. A roughly 128x128 image dilated 64 times takes around 33 seconds, but you know, it's python what can you do.

If you have ideas to make it faster please let me know. I ended up making this in c++ as well and it's much faster! Link here.