July 18, 2009 will

Making tileable images with Python

Here's an interesting bit of Python code I hacked together – it's a script that takes an image and warps it so that it is tileable (making it suitable for a repeating backgound or a texture in a game).

A fractal

A Mandlebrot fractal

If you use it on a photograph, it will come out looking like a fair-ground mirror. But it works well when applied to a pattern, or something more abstract, such as the fractal image on the left.

The code is public domain – use it for whatever the heck you want!

Example Output

Update: Here's another, more interesting example, The original is here.

The Code

import Image
from math import *

def maketilable(src_path, dst_path):
    src = Image.open(src_path)
    src = src.convert('RGB')
    src_w, src_h = src.size

    dst = Image.new('RGB', (src_w, src_h))
    w, h = dst.size

    def warp(p, l, dl):
        i = float(p) / l
        i = sin(i*pi*2 + pi)
        i = i / 2.0 + .5
        return abs(i * dl)

    warpx = [warp(x, w-1, src_w-1) for x in range(w)]
    warpy = [warp(y, h-1, src_h-1) for y in range(h)]

    get = src.load()
    put = dst.load()

    def getpixel(x, y):

        frac_x = x - floor(x)
        frac_y = y - floor(y)

        x1 = (x+1)%src_w
        y1 = (y+1)%src_h

        a = get[x, y]
        b = get[x1, y]
        c = get[x, y1]
        d = get[x1, y1]

        area_d = frac_x * frac_y
        area_c = (1.-frac_x) * frac_y
        area_b = frac_x * (1. - frac_y)
        area_a = (1.-frac_x) * (1. - frac_y)

        a = [n*area_a for n in a]
        b = [n*area_b for n in b]
        c = [n*area_c for n in c]
        d = [n*area_d for n in d]

        return tuple(int(sum(s)) for s in zip(a,b,c,d))


    old_status_msg = None
    status_msg = ''
    for y in xrange(h):

        status_msg = '%2d%% complete' % ((float(y) / h)*100.0)
        if status_msg != old_status_msg:
            print status_msg
        old_status_msg = status_msg

        for x in xrange(w):
            put[x, y] = getpixel(warpx[x], warpy[y])

    dst.save(dst_path)


if __name__ == "__main__":

    import sys
    try:
        src_path = sys.argv[1]
        dst_path = sys.argv[2]
    except IndexError:
        print "<source image path>, <destination image path>"
    else:
        maketilable(src_path, dst_path)
Use Markdown for formatting
*Italic* **Bold** `inline code` Links to [Google](http://www.google.com) > This is a quote > ```python import this ```
your comment will be previewed here
gravatar
Pauli Virtanen
Smells like a job for Numpy (http://www.scipy.org):
 import Image
import numpy as np
def maketilable(src_path, dst_path, w=None, h=None):
    src_img = Image.open(src_path)
    src_img = src_img.convert('RGB')
    src = np.asarray(src_img)
    src_w, src_h, ncolors = src.shape
    if w is None:
        w = src_w
    if h is None:
        h = src_h
    def warp(p, l, dl):
        i = p * 1.0 / l
        i = np.sin(i*np.pi*2 + np.pi)
        i = i / 2.0 + .5
        return abs(i * dl)
    xp = warp(np.arange(w), w-1, src_w-1)[:,np.newaxis]
    yp = warp(np.arange(h), h-1, src_h-1)[np.newaxis,:]
    x = xp.astype(int)
    y = yp.astype(int)
    x1 = (x + 1) % src_w
    y1 = (y + 1) % src_h
    a = src[x, y]
    b = src[x1, y]
    c = src[x, y1]
    d = src[x1, y1]
    frac_x = (xp - np.floor(xp))[...,np.newaxis]
    frac_y = (yp - np.floor(yp))[...,np.newaxis]
    
    d *= frac_x * frac_y
    c *= (1.-frac_x) * frac_y
    b *= frac_x * (1. - frac_y)
    a *= (1.-frac_x) * (1. - frac_y)
    dst = a + b + c + d
    dst_img = Image.fromarray(dst, 'RGB')
    dst_img.save(dst_path)
if __name__ == "__main__":
    import sys
    try:
        src_path = sys.argv[1]
        dst_path = sys.argv[2]
    except IndexError:
        print "<source image path>, <destination image path>"
    else:
        maketilable(src_path, dst_path)
gravatar
Will McGugan
Pauli, Nice! I think it is a job for Numpy.
gravatar
Pauli Virtanen
Once more, this time demonstrating the use of scipy.ndimage for the interpolation:
 import Image
import numpy as np
import scipy.ndimage
def maketilable(src_path, dst_path, w=None, h=None):
    src_img = Image.open(src_path)
    src_img = src_img.convert('RGB')
    src = np.asarray(src_img)
    src_w, src_h, ncolors = src.shape
    if w is None:
        w = src_w
    if h is None:
        h = src_h
    def warp(p, l, dl):
        i = p * 1.0 / l
        i = np.sin(i*np.pi*2 + np.pi)
        i = i / 2.0 + .5
        return abs(i * dl)
    x = warp(np.arange(w), w-1, src_w-1)
    y = warp(np.arange(h), h-1, src_h-1)
    coords = np.broadcast_arrays(*np.ix_(x, y))
    dst = np.empty((w, h, 3), src.dtype)
    for j in xrange(3):
        dst[:,:,j] = \
            scipy.ndimage.map_coordinates(src[:,:,j], 
                                          coords,
                                          mode='wrap',
                                          order=3)
    dst_img = Image.fromarray(dst, 'RGB')
    dst_img.save(dst_path)
if __name__ == "__main__":
    import sys
    try:
        src_path = sys.argv[1]
        dst_path = sys.argv[2]
    except IndexError:
        print "<source image path>, <destination image path>"
    else:
        maketilable(src_path, dst_path)