Skip to content

Instantly share code, notes, and snippets.

@parente
Created March 13, 2010 02:06
Show Gist options
  • Save parente/331040 to your computer and use it in GitHub Desktop.
Save parente/331040 to your computer and use it in GitHub Desktop.
montage.py
#!/usr/bin/python
'''Yet another Montage experiment for desktop wallpaper
This version works with a variant on the "Pseudo-Poisson" dart throwing algorithms
that were popular in stochastic ray-tracing years ago. It throws darts at a rectangular
region ensuring that no darts are closer together than a minimum distance. It allows the
minimum distance to scale down so you can have some big pictues and some smaller pictures.
The help text was intended to be self explanatory...
@author Gary Bishop <gb@cs.unc.edu>
'''
import Image
import GifImagePlugin
import JpegImagePlugin
import PngImagePlugin
import BmpImagePlugin
Image._initialized = 1
import math
import os
import random
import sys
from optparse import OptionParser
class Disk(object):
'''A circular region'''
def __init__(self, center, radius):
'''Create a disk from a complex center and float radius.'''
self.center = center
self.radius = radius
def range(self, other):
'''Compute the distance between two disks.'''
return abs(self.center - other.center)
def overlaps(self, other):
'''True if one disk overlaps the other'''
return self.range(other) < self.radius + other.radius
def __str__(self):
return 'Disk((%.1f, %.1f), %.1f)' % (self.center.real, self.center.imag, self.radius)
class Field(object):
'''A rectangular field of non-overlapping disks.'''
def __init__(self, width, height):
'''Initialize a field with integer width and height'''
self.width = width
self.height = height
self.disks = []
def insert(self, new_disk):
'''Insert a disk if it does not overlap others, return True for insertion.'''
for disk in self.disks:
if new_disk.overlaps(disk):
return False
self.disks.append(new_disk)
return True
def insert_random(self, radius):
'''Try inserting a disk at a random location within the field'''
x = random.uniform(radius, self.width-radius)
y = random.uniform(radius, self.height-radius)
return self.insert(Disk(complex(x,y), radius))
def len(self):
return len(self.disks)
def __getitem__(self, ndx):
return self.disks[ndx]
def add_border(im, picture_border):
isize = im.size
osize = (isize[0]+2*picture_border, isize[1]+2*picture_border)
om = Image.new('RGB', osize, (255,255,255))
om.paste(im, (picture_border,picture_border))
return om
def add_shadow(im, shadow_offset):
isize = im.size
osize = (isize[0]+abs(shadow_offset), isize[1]+abs(shadow_offset))
om = Image.new('RGBA', osize, (255,255,255,0))
shadow = Image.new('RGBA', isize, (0,0,0,120))
om.paste(shadow, (max(0,shadow_offset), max(0,shadow_offset)))
om.paste(im, (-min(0,shadow_offset),-min(0,shadow_offset)))
return om
def rotate(im, angle):
isize = im.size
big = int(math.sqrt(isize[0]**2 + isize[1]**2))
osize = (big,big)
om = Image.new('RGBA', osize, (0,0,0,0))
om.paste(im, ((big-isize[0])//2, (big-isize[1])//2))
om = om.rotate(angle, Image.BICUBIC)
return om.crop(om.getbbox())
parser = OptionParser(usage='usage: %prog [options] src_folder output_image')
parser.add_option('-r', '--region', action='append', type='int', nargs=4, dest='regions',
metavar='x_min y_min x_max y_max', help='limits of a rectangular image region, you can have several of these. Useful for avoiding the seam between multiple displays.')
parser.add_option('-s', '--size', action='store', type='int', nargs=2, dest='size',
help='size of the output image', metavar='width height', default=[1440, 900])
parser.add_option('--big', action='store', type='float', dest='big', metavar='diameter',
help='maximum dimension of biggest pictures', default=400)
parser.add_option('--small', action='store', type='float', dest='small', metavar='diameter',
help='maximum dimension of smallest pictures', default=400)
parser.add_option('--steps', action='store', type='int', dest='steps',
help='number of steps between BIG and SMALL', default=2)
parser.add_option('--maxtrys', action='store', type='int', dest='maxtrys',
help='maximum number of attempts to place an image', default=10000)
parser.add_option('--scale', action='store', type='float', dest='scale',
help='scale factor for output image', default=1)
parser.add_option('--aafactor', action='store', type='float', dest='aafactor',
help='scale factor for antialiasing output image', default=1)
parser.add_option('--overlap', action='store', type='float', dest='overlap',
help='fraction that images may overlap', default=0.1)
parser.add_option('--tilt', action='store', type='float', dest='tilt',
help='maximum angle of tilt', metavar='ANGLE', default=15)
parser.add_option('--border', action='store', type='int', dest='border',
help='width of white border on pictures', default=5)
parser.add_option('--shadow', action='store', type='int', dest='shadow',
help='width of shadow under pictures', default=5)
parser.add_option('--background', action='store', dest='background',
help='background image behind pictures', default='purple\ cloud.jpg')
parser.add_option('--set', action='store_true', dest="set_background",
help='set the background image', default=False)
parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
help='produce verbose output', default=False)
parser.add_option('--seed', action='store', dest='seed', default=None)
parser.add_option('--console-only', action='store_true', dest='console', default=False)
(options, args) = parser.parse_args()
try:
image_dir, result_name = args
except ValueError:
image_dir, result_name = '../baby', 'baby.jpg'
random.seed(options.seed)
if options.set_background and options.console:
disp = os.getenv('DISPLAY')
if disp not in [ ':0', ':0.0' ]:
sys.exit(0)
if options.set_background and not options.size:
# hack specific to my setups
import gtk
d = gtk.gdk.Display(os.getenv('DISPLAY'))
s = d.get_screen(0)
m = s.get_n_monitors()
w = gtk.gdk.screen_width()
h = gtk.gdk.screen_height()
options.size = (w,h)
if m == 2:
options.regions = [ (0,30,w//2,h-30), (w//2,0,w,h) ]
else:
options.regions = [ (0,30,w,h-30) ]
if options.regions is None:
options.regions = [ (0,0,options.size[0],options.size[1]) ]
fields = [ Field(region[2]-region[0], region[3]-region[1])
for region in options.regions ]
# place disks into the fields to allocate space
if options.verbose:
print 'Placing disks'
for field in fields:
scale = (float(options.small) / options.big) ** (1.0/options.steps)
radius = options.big / (2 * (1 + options.overlap))
for step in xrange(options.steps+1):
inserted = 0
for i in xrange(options.maxtrys):
if field.insert_random(radius):
inserted += 1
radius *= scale
if options.verbose:
print field.len(), 'disks placed'
# load up some images
image_names = []
for dir_path, dirnames, filenames in os.walk(image_dir):
for filename in filenames:
base, ext = os.path.splitext(filename)
if ext.lower() in ['.jpg', '.gif', '.bmp' ]:
image_names.append(os.path.join(dir_path, filename))
if options.verbose:
print len(image_names), 'images found'
random.shuffle(image_names)
scaleup = options.scale * options.aafactor
scaledown = options.aafactor
out_size = (int(options.size[0]*scaleup), int(options.size[1]*scaleup))
if options.background is None:
out = Image.new('RGBA', out_size, (0,0,0,0))
else:
out = Image.open(options.background)
for i,field in enumerate(fields):
region = options.regions[i]
for disk in field:
try:
name = image_names.pop()
im = Image.open(name)
except IndexError:
break
# check for exif data indicating the camera was rotated and correct for that
base_angle = 0
exif = im._getexif()
if exif:
r = exif.get(274, 1)
if r == 6:
base_angle = -90
elif r == 8:
base_angle = 90
scale = 2*disk.radius*(1+options.overlap)*scaleup/math.hypot(im.size[0], im.size[1])
im = im.resize((int(im.size[0]*scale),int(im.size[1]*scale)), Image.ANTIALIAS)
angle = base_angle + random.uniform(-options.tilt, options.tilt)
im = rotate(add_shadow(add_border(im,
int(options.border*scaleup)),
int(options.shadow*scaleup)),
angle)
isize = (im.size[0]/scaleup, im.size[1]/scaleup)
x = disk.center.real - isize[0]/2
y = disk.center.imag - isize[1]/2
if x < 0:
x = 0
elif x + isize[0] > field.width:
x = field.width - isize[0]
if y < 0:
y = 0
elif y + isize[1] > field.height:
y = field.height - isize[1]
x += region[0]
y += region[1]
if options.verbose:
print 'placing %s at %d,%d' % (name, x, y)
x *= scaleup
y *= scaleup
out.paste(im, (int(x),int(y)), im)
if scaledown != 1:
if options.verbose:
print 'antialiasing'
out = out.resize((int(out.size[0]/scaledown), int(out.size[1]/scaledown)),
Image.ANTIALIAS)
if options.verbose:
print 'saving'
result_name = result_name % os.environ
out.save(result_name)
'''
if options.set_background:
if options.verbose:
print 'setting background image'
client = gconf.client_get_default()
client.set_string('/desktop/gnome/background/picture_filename', result_name)
'''
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment