Created
March 13, 2010 02:06
-
-
Save parente/331040 to your computer and use it in GitHub Desktop.
montage.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 <[email protected]> | |
''' | |
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