Skip to content

Instantly share code, notes, and snippets.

@gbishop
Created August 14, 2010 14:01
Show Gist options
  • Save gbishop/524321 to your computer and use it in GitHub Desktop.
Save gbishop/524321 to your computer and use it in GitHub Desktop.
#!/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...
'''
import sys
sys.stdout = sys.stderr = open('/tmp/Montage.log', 'w')
import Image
import GifImagePlugin
import JpegImagePlugin
import PngImagePlugin
import BmpImagePlugin
Image._initialized = 1
import math
import os
import random
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')
parser.add_option('--big', action='store', type='float', dest='big', metavar='diameter',
help='maximum dimension of biggest pictures', default=256)
parser.add_option('--small', action='store', type='float', dest='small', metavar='diameter',
help='maximum dimension of smallest pictures', default=128)
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=2)
parser.add_option('--overlap', action='store', type='float', dest='overlap',
help='fraction that images may overlap', default=0.5)
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('--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()
image_dir, result_name = args
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
import gconf
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 not options.regions:
if m == 2:
options.regions = [ (300,30,w//2,h-30), (w//2,0,w,h) ]
else:
options.regions = [ (0,30,w,h-30) ]
if options.verbose:
print 'size=', options.size
print 'regions=', options.regions
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))
out = Image.new('RGB', out_size, (0,0,0))
for i,field in enumerate(fields):
region = options.regions[i]
print 'region=', region
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