Source code for image_slicer.main

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
from math import sqrt, ceil, floor

from PIL import Image

from .helpers import get_basename


[docs]class Tile(object): """Represents a single tile.""" def __init__(self, image, number, position, coords, filename=None): self.image = image self.number = number self.position = position self.coords = coords self.filename = filename @property def row(self): return self.position[0] @property def column(self): return self.position[1] @property def basename(self): """Strip path and extension. Return base filename.""" return get_basename(self.filename)
[docs] def generate_filename( self, directory=os.getcwd(), prefix="tile", format="png", path=True ): """Construct and return a filename for this tile.""" filename = prefix + "_{col:02d}_{row:02d}.{ext}".format( col=self.column, row=self.row, ext=format.lower().replace("jpeg", "jpg") ) if not path: return filename return os.path.join(directory, filename)
[docs] def save(self, filename=None, format="png"): if not filename: filename = self.generate_filename(format=format) self.image.save(filename, format) self.filename = filename
def __repr__(self): """Show tile number, and if saved to disk, filename.""" if self.filename: return "<Tile #{} - {}>".format( self.number, os.path.basename(self.filename) ) return "<Tile #{}>".format(self.number)
[docs]def calc_columns_rows(n): """ Calculate the number of columns and rows required to divide an image into ``n`` parts. Return a tuple of integers in the format (num_columns, num_rows) """ num_columns = int(ceil(sqrt(n))) num_rows = int(ceil(n / float(num_columns))) return (num_columns, num_rows)
[docs]def get_combined_size(tiles): """Calculate combined size of tiles.""" # TODO: Refactor calculating layout to avoid repetition. columns, rows = calc_columns_rows(len(tiles)) tile_size = tiles[0].image.size return (tile_size[0] * columns, tile_size[1] * rows)
[docs]def join(tiles, width=0, height=0): """ @param ``tiles`` - Tuple of ``Image`` instances. @param ``width`` - Optional, width of combined image. @param ``height`` - Optional, height of combined image. @return ``Image`` instance. """ # Don't calculate size if width and height are provided # this allows an application that knows what the # combined size should be to construct an image when # pieces are missing. if width > 0 and height > 0: im = Image.new("RGBA", (width, height), None) else: im = Image.new("RGBA", get_combined_size(tiles), None) columns, rows = calc_columns_rows(len(tiles)) for tile in tiles: try: im.paste(tile.image, tile.coords) except IOError: # do nothing, blank out the image continue return im
[docs]def validate_image(image, number_tiles): """Basic sanity checks prior to performing a split.""" TILE_LIMIT = 99 * 99 try: number_tiles = int(number_tiles) except BaseException: raise ValueError("number_tiles could not be cast to integer.") if number_tiles > TILE_LIMIT or number_tiles < 2: raise ValueError( "Number of tiles must be between 2 and {} (you \ asked for {}).".format( TILE_LIMIT, number_tiles ) )
[docs]def validate_image_col_row(image, col, row): """Basic checks for columns and rows values""" SPLIT_LIMIT = 99 try: col = int(col) row = int(row) except BaseException: raise ValueError("columns and rows values could not be cast to integer.") if col < 1 or row < 1 or col > SPLIT_LIMIT or row > SPLIT_LIMIT: raise ValueError( f"Number of columns and rows must be between 1 and" f"{SPLIT_LIMIT} (you asked for rows: {row} and col: {col})." ) if col == 1 and row == 1: raise ValueError("There is nothing to divide. You asked for the entire image.")
[docs]def slice( filename, number_tiles=None, col=None, row=None, save=True, DecompressionBombWarning=True, ): """ Split an image into a specified number of tiles. Args: filename (str): The filename of the image to split. number_tiles (int): The number of tiles required. Kwargs: save (bool): Whether or not to save tiles to disk. DecompressionBombWarning (bool): Whether to suppress Pillow DecompressionBombWarning Returns: Tuple of :class:`Tile` instances. """ if DecompressionBombWarning is False: Image.MAX_IMAGE_PIXELS = None im = Image.open(filename) im_w, im_h = im.size columns = 0 rows = 0 if number_tiles: validate_image(im, number_tiles) columns, rows = calc_columns_rows(number_tiles) else: validate_image_col_row(im, col, row) columns = col rows = row tile_w, tile_h = int(floor(im_w / columns)), int(floor(im_h / rows)) tiles = [] number = 1 for pos_y in range(0, im_h - rows, tile_h): # -rows for rounding error. for pos_x in range(0, im_w - columns, tile_w): # as above. area = (pos_x, pos_y, pos_x + tile_w, pos_y + tile_h) image = im.crop(area) position = (int(floor(pos_x / tile_w)) + 1, int(floor(pos_y / tile_h)) + 1) coords = (pos_x, pos_y) tile = Tile(image, number, position, coords) tiles.append(tile) number += 1 if save: save_tiles( tiles, prefix=get_basename(filename), directory=os.path.dirname(filename) ) return tuple(tiles)
[docs]def save_tiles(tiles, prefix="", directory=os.getcwd(), format="png"): """ Write image files to disk. Create specified folder(s) if they don't exist. Return list of :class:`Tile` instance. Args: tiles (list): List, tuple or set of :class:`Tile` objects to save. prefix (str): Filename prefix of saved tiles. Kwargs: directory (str): Directory to save tiles. Created if non-existant. Returns: Tuple of :class:`Tile` instances. """ for tile in tiles: tile.save( filename=tile.generate_filename( prefix=prefix, directory=directory, format=format ), format=format, ) return tuple(tiles)
[docs]def get_image_column_row(filename): """Determine column and row position for filename.""" row, column = os.path.splitext(filename)[0][-5:].split("_") return (int(column) - 1, int(row) - 1)
[docs]def open_images_in(directory): """Open all images in a directory. Return tuple of Tile instances.""" files = [ filename for filename in os.listdir(directory) if "_" in filename and not filename.startswith("joined") ] tiles = [] if len(files) > 0: i = 0 for file in files: pos = get_image_column_row(file) im = Image.open(os.path.join(directory, file)) position_xy = [0, 0] count = 0 for a, b in zip(pos, im.size): position_xy[count] = a * b count = count + 1 tiles.append( Tile( image=im, position=pos, number=i + 1, coords=position_xy, filename=file, ) ) i = i + 1 return tiles