Processes the given image by breaking it down into tiles of the given
    size and applying a triangular effect to each tile. Returns the processed
    image as a PIL Image object.

    The image can be given as anything suitable for passing to `Image.open`
    (ie, the path to an image or as a file-like object containing image data).

    If tile_size is 0, the tile size will be guessed based on the image
    size. It will also be adjusted to be divisible by 2 if it is not already.

def triangulize(image, tile_size):
    """Processes the given image by breaking it down into tiles of the given
    size and applying a triangular effect to each tile. Returns the processed
    image as a PIL Image object.

    The image can be given as anything suitable for passing to `Image.open`
    (ie, the path to an image or as a file-like object containing image data).

    If tile_size is 0, the tile size will be guessed based on the image
    size. It will also be adjusted to be divisible by 2 if it is not already.
    """
    if isinstance(image, basestring) or hasattr(image, 'read'):
        image = Image.open(image)
    assert isinstance(tile_size, int)

    # Make sure we have a usable tile size, by guessing based on image size
    # and making sure it's a multiple of two.
    if tile_size == 0:
        tile_size = guess_tile_size(image)
    if tile_size % 2 != 0:
        tile_size = (tile_size / 2) * 2

    logging.info('Input image size: %r', image.size)
    logging.info('Tile size: %r', tile_size)

    # Preprocess image to make sure it's at a size we can handle
    image = prep_image(image, tile_size)
    logging.info('Prepped image size: %r', image.size)

    # Get pixmap (for direct pixel access) and draw objects for the image.
    pix = image.load()
    draw = ImageDraw.Draw(image)

    # Process the image, tile by tile
    for x, y in iter_tiles(image, tile_size):
        process_tile(x, y, tile_size, pix, draw, image)
    return image