Updated code to support uniform scaling:
#particleMosaic.py
#PIL
import Image
from ImageOps import fit
#builtin
import os
import sys
import getopt
from math import sqrt, ceil, floor
def boxUnion(A, B):
axmin, aymin, axmax, aymax = A
bxmin, bymin, bxmax, bymax = B
xmin = min( axmin, bxmin )
ymin = min( aymin, bymin )
xmax = max( axmax, bxmax )
ymax = max( aymax, bymax )
return ( xmin, ymin, xmax, ymax )
def getNextPowerOfTwo( upper ):
last = 0
exp = 0
count = 0
while( last < upper ):
exp = count
val = 2 ** exp
last = val
count = count + 1
return ( exp, last )
def totalToTile( count ):
exponent, powerOfTwo = getNextPowerOfTwo( count )
half = exponent / 2.0
x = int(ceil(half))
y = int(floor(half))
return ( 2 ** x, 2 ** y )
def generateTiles( counts, sizes ):
boxes = []
for y in range(counts[1]):
for x in range(counts[0]):
minx = x * sizes[0]
miny = y * sizes[1]
maxx = (x + 1) * sizes[0]
maxy = (y + 1) * sizes[1]
boxes.append( (minx, miny, maxx, maxy) )
return boxes
def getScaledUniform( targetSize, region ):
size = ( region[2] - region[0], region[3] - region[1] )
idealRatio = targetSize[0] / float(targetSize[1])
actualRatio = size[0] / float(size[1])
dims = targetSize
if idealRatio > actualRatio:
# Y Major
dims = ( int(dims[1] * actualRatio), dims[1] )
else:
# X Major
dims = ( dims[0], int(dims[0] * ( 1 / actualRatio ) ) )
return dims
def boxFromCenterSize( center, size ):
half = ( size[0] / 2.0, size[1] / 2.0 )
minx = int( center[0] - half[0] )
miny = int( center[1] - half[1] )
maxx = int( center[0] + half[0] )
maxy = int( center[1] + half[1] )
return ( minx, miny, maxx, maxy )
def MosaicFolder( folder, output, tileDim=(128,128), uniform=False ):
images = []
for infile in os.listdir( folder ):
try:
path = os.path.join( folder, infile )
im = Image.open( path )
images.append( im )
print ( "Atlasing: " + path )
except IOError, e:
#Not an image file we can read
pass
return Mosaic( images, output, tileDim, uniform )
def Mosaic( images, output, tileDim=(128,128), uniform=False ):
# Get all Bounding boxes
boxes = [ im.getbbox() for im in images ]
# Get the Bounding box that encompasses all other bounding boxes
extents = reduce( boxUnion, boxes )
# Pull cropped sections into memory
regions = [ im.crop( extents ) for im in images ]
# Get Atlas-Tiles out of the regions
if uniform:
collector = []
cropBox = getScaledUniform( tileDim, extents )
half = ( tileDim[0] / 2.0, tileDim[1] / 2.0 )
pasteBox = boxFromCenterSize( half, cropBox )
for region in regions:
blank = Image.new( region.mode, tileDim )
scaledRegion = region.resize( cropBox, Image.ANTIALIAS )
blank.paste( scaledRegion, pasteBox )
collector.append( blank )
regions = collector
else:
regions = [ im.resize( tileDim, Image.ANTIALIAS ) for im in regions ]
# X and Y number of Tiles. This will be a power of two
tileCount = totalToTile( len( regions ) )
# Bounding boxes for every tile in the atlas.
tiles = generateTiles( tileCount, tileDim )
# Generate the Atlas
atlasDim = [ count * dim for count, dim in zip( tileCount, tileDim ) ]
atlas = Image.new( images[0].mode, atlasDim )
# Copy cropped images into proper slot in the atlas
for i in range( len(regions) ):
atlas.paste( regions[i], tiles[i] )
# Make sure the directory exists to save
if not os.path.exists( os.path.dirname( output ) ):
os.path.makedirs( os.path.dirname( output ) )
# Write
atlas.save(output)
print ( "Saved Atlas to " + output )
def printHelp():
print '''
This Python Script will take a folder full of images,
and composite them into a single large "atlas". If no
overrides are provided, every tile is 128x128 pixels.
The flags this program excepts are:
-f "Folder/Path" ( Required )
-o "output/File.format" ( Required )
-x The x dimension of each tile in the atlas
-y The y dimension of each tile in the atlas
-u forces uniform scaling instead of scale to fit.
-h shows this dialog
A typical call looks like:
python particleMosaic.py -f "c:/folder" -o "c:/atlas.png" -u -x 256 -y 256
'''
if __name__ == "__main__":
try:
opts, args = getopt.getopt( sys.argv[1:], "f:o:x:y:uh" )
except getopt.GetoptError, err:
print str(err)
sys.exit(2)
if ( len(opts) == 0 ):
printHelp()
sys.exit(2)
inFolder = None
outFile = None
uniform = False
x = 128
y = 128
for flag, value in opts:
if flag == "-f":
inFolder = str(value)
elif flag == "-o":
outFile = str(value)
elif flag == "-x":
x = int(value)
elif flag == "-y":
y = int(value)
elif flag == "-u":
uniform = True
print value
elif flag == "-h":
printHelp()
sys.exit(2)
else:
assert False, "unhandled option"
if not inFolder:
assert False, "No input folder provided."
if not outFile:
assert False, "No output file specified."
MosaicFolder( inFolder, outFile, ( x, y ), uniform )
usage now looks like:
python particleMosaic.py -f "c:/folder" -o "c:/atlas.png" -u -x 256 -y 256
I guess I should pyDoc it better, but this does practically nothing. Fun little script though. I feel like there was a more elegant way to do uniform scaling, ( Maybe create a crop region, force it to be within the bounds of the image… etc… but that might not work in the case of the tile being a shape that is out of the range source image ). Similarly, there is little safeguarding against images being of different sizes. I leave that as an exercise to the reader. (Obvious choices are to pre-scale everything to a uniform size, or change all operations to be based on percentages) Given the problem-space this was originally created for, that is a bit extraneous. It would be fun to try and create a general module for all your atlasing needs though.