盒子
盒子
文章目录
  1. Yet Another PhotoMosaic Generator
    1. Search the Internet
    2. Write My Own PhotoMosaic Generator
      1. Frame, Shadow and Rotate
    3. Calculate Images And Compare
    4. Final Work
    5. Demo
    6. Thanks

Yet Another PhotoMosaic Generator

Yet Another PhotoMosaic Generator

Update: Mon 25 Feb 2013 11:38:47 AM CST add classic style. More refer to github

Python是面向对象的?没有对象面向毛对象。

——Anonymous

Several weeks ago, I saw a poster of presidential campaign for Obama, in which Obama’s portrait was made up of many voter’s photos. It really attracted me, somedays later, I want to make one myself.

The completed code host here. It is much more functional than object-oriented…

Search the Internet

First of all, I searched the Google to find out how others achieve it, then I found many interesting implement and post on it.Along with them, there are pretty demos around.One of the demo of Foto-Mosaik-Edda striked me.It declaims as follows in their site:

The Chaos Mosaic Picture is a new form of photo mosaic which can, at present, only be created by Foto-Mosaik-Edda.

Chaos Mosaic Picture

Uhm…Foto-Mosaic-Edda is an open-source project that really impressive.But it was an C# project. Linux users don’t like it however.I don’t like mono.

I searched other open-source implement on photomosaic. I get some simple programs only use gray photos, and some complex ones can make beautiful classic photomosaic(like metapixel, even chaos style which it calls collage style), But none has as beautiful demos as Foto-Mosaic-Edda.(metapixel really amazing, it is robust and quickly.)

However, I saw many enthusiastic people write one themselves, it really looks interesting for me. I’ve used PIL for processing images when I tried to decode captchas several days ago, so I believe with the help of PIL, someone can achieve photomosaic simply.

So I just read the documentation of PIL, then start my hack.

Write My Own PhotoMosaic Generator

It’s not hard, however, what you should do is clear and simple:

  • analyse the image to be made mosaic, get a dict in which position as key and color as value.
  • use a bunch of images to get a dict, in which image name as key and colors as value.
  • thumbnail bunches of images and paste it in the right position, so that the big image looks like it consists of many small one.

I’d like to got the chaos style, so some other requirements:

  • frame and shadow for small images
  • random paste small images onto large one

Now, let’s go.

Frame, Shadow and Rotate

first add frame, shadow to small images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def add_frame(image):
'''Add frame for image.'''
im = ImageOps.expand(image, border=int(0.01 * max(image.size)), fill=0xffffff)
return im


def drop_shadow(image, offset, border=0, shadow_color=0x444444):
"""Add shadows for image"""
# Caclulate size
fullWidth = image.size[0] + abs(offset[0]) + 2 * border
fullHeight = image.size[1] + abs(offset[1]) + 2 * border
# Create shadow, hardcode color
shadow = Image.new('RGBA', (fullWidth, fullHeight), (0, 0, 0))
# Place the shadow, with required offset
shadowLeft = border + max(offset[0], 0) # if <0, push the rest of the image right
shadowTop = border + max(offset[1], 0) # if <0, push the rest of the image down
shadow.paste(shadow_color, [shadowLeft, shadowTop, shadowLeft + image.size[0], shadowTop + image.size[1]])
shadow_mask = shadow.convert("L")
# Paste the original image on top of the shadow
imgLeft = border - min(offset[0], 0) # if the shadow offset was <0, push right
imgTop = border - min(offset[1], 0) # if the shadow offset was <0, push down
shadow.putalpha(shadow_mask)
shadow.paste(image, (imgLeft, imgTop))
return shadow

Then a function to rotate images.

1
2
3
4
5
6
def rotate_image(image, degree):
'''Rotate images for specific degree. Expand to show all'''
if image.mode != 'RGBA':
image = image.convert('RGBA')
im = image.rotate(degree, expand=1)
return im

‘RGBA’ mode is to support transparency. What’s matter here is that jpeg/jpg does not support transparency. So you can’t get transparency shadows and rotate pictures if you just use jpg/jpeg images.So, write a function to process images with jpg/jpeg format, transpose it into png.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def process_image(filename, newname):
'''convert image to png to support transparency'''
if filename.split('.')[-1] != 'png':
im = Image.open(filename)
im.save(newname + '.png')
print "processing image file %s" % filename
return 1


def process_directory(path):
os.chdir(path)
count = 1
for filename in os.listdir(path):
ext = filename.split('.')[-1]
if ext == 'jpeg' or ext == 'jpg':
process_image(filename, str(count))
os.remove(filename)
count += 1
return 1

Really poor work… But it works for me: )

We have to thumnail bunches of images, It’s easy to thumbnail with PIL:

1
2
3
4
def thumbnail(im, size):
"""thumnail the image"""
im.thumbnail(size, Image.ANTIALIAS)
return im

Let’s have a fun with them. To get heaps of images randomly on the desktop, I hardcoded these parameters to get my photos work, you HAVE TO find yours:

1
2
3
4
5
6
7
8
9
10
# Just for fun
def chao_image(path, size=(800, 800), thumbnail_size=(50, 50), shadow_offset=(10, 10), backgroud_color=0xffffff):
image_all = Image.new('RGB', size, backgroud_color)
for image in os.listdir(path):
if image.split('.')[-1] == 'png':
im = Image.open(image)
degree = random.randint(-30, 30)
im = thumbnail(rotate_image(drop_shadow(add_frame(im), shadow_offset), degree), thumbnail_size)
image_all.paste(im, (random.randint(-thumbnail_size[0], size[0]), random.randint(-thumbnail_size[1], size[1])), im)
return image_all

Calculate Images And Compare

Get average colors of an image

1
2
3
4
def average_image(im):
"""return average (r,g,b) for image"""
color_vector = [int(x) for x in ImageStat.Stat(im).mean]
return color_vector

to compare images? Compare the (r,g,b) value of them.

1
2
3
4
5
6
7
8
9
def compare_vectors(v1, v2):
"""compare image1 and image2, return relations"""
if len(v1) == len(v2):
distance = 0
for i in xrange(len(v1)):
distance += (v1[i] - v2[i]) ** 2
return distance
else:
print "vector not match in dimensions"

I just use distance in (R, G, B) space to calculate similarity, someone advice compare in other space, you can change it just like the example in PIL’s documentation:

1
2
3
4
5
6
# May not useful
def rgb2xyz(im):
"""rgb to xyz"""
rgb2xyz = (0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, 0.019334, 0.119193, 0.950227, 0)
out = im.convert("RGB", rgb2xyz)
return out

But I find many implements just use R,G,B, and it works well.

Next, get a dict of image in current path, in which filename as key, average (R,G,B) colors as value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def tile_dict(path):
"""Return list of average (R,G,B) for image in this path as dict."""
dic = {}
for image in os.listdir(path):
if image.split('.')[-1] == 'png':
try:
im = Image.open(image)
except:
print "image file %s cannot open" % image
continue
if im.mode != 'RGB':
im = im.convert('RGB')
dic[image] = average_image(im)
return dic

We don’t need to calculate every pixel of the large picture, just thumbnail it to get a nearest color of different regions.

1
2
3
4
5
6
def thumbnail_background(im, scale):
"""thumbnail backgroud image"""
newsize = im.size[0] / scale, im.size[1] / scale
im.thumbnail(newsize)
print 'thumbnail size and the number of tiles %d X %d' % im.size
return im.size

For every pixel in the thumbnailed large image, find most similar small image filenames.(top ten):

1
2
3
4
5
6
7
8
9
10
def find_similar(lst, dic):
"""for lst([R, G, B], Calculate which key-value in dic has the most similarity.Return first 10)"""
similar = {}
for k, v in dic.items():
similar[k] = compare_vectors(v, lst)
# if len(v) != len(lst):
# print v, len(v), lst, len(lst)
similar = [(v, k) for k, v in similar.items()] # Not good, lost the same Score
similar.sort()
return similar[:10]

Poor hack, but it really works…

Final Work

Now it’s the final magic.

Get the small image in order, the order imply where it should be. Then rotate, add shadows and frames for small images, finally paste it onto the large one randomly in the right position:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def get_image_list(im, dic):
"""receive a thumbnail image and a dict of image to be mosaic, return tiles(filename) in order(as a list)"""
lst = list(im.getdata())
tiles = []
for i in range(len(lst)):
#print find_similar(lst[i], dic)[random.randrange(10)][1]
tiles.append(find_similar(lst[i], dic)[random.randrange(10)][1])
return tiles


def paste_chaos(image, tiles, size, shadow_off_set=(30, 30)):
"""size is thumbnail of backgroud size that is how many tiles per line and row"""
# image_all = Image.new('RGB', image.size, 0xffffff)
image_all = image
lst = range(len(tiles))
random.shuffle(lst)
fragment_size = (image.size[0] / size[0], image.size[1] / size[1])
print 'tiles size %d X %d' % fragment_size
print 'number of tiles one iteration: %d' % len(lst)
for i in lst:
im = Image.open(tiles[i])
degree = random.randint(-20, 20)
im = thumbnail(rotate_image(drop_shadow(add_frame(im), shadow_off_set), degree), (fragment_size[0] * 3 / 2, fragment_size[1] * 3 / 2))
x = i % size[0] * fragment_size[0] + random.randrange(-fragment_size[0] / 2, fragment_size[0] / 2)
y = i / size[0] * fragment_size[1] + random.randrange(-fragment_size[1] / 2, fragment_size[1] / 2)
# print x, y
image_all.paste(im, (x, y), im)
return image_all

I try it like this, I know parameter n is tricky, it was the scale it thumbnail the large image. Maybe I’ll change it to something more clear later…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def main(filename, n, scale, iteration, path='./'):
# 0. select an big image for mosaic
print "open %s" % filename
im = Image.open(filename)
# 1. process image as png to support transparency
print "process directory %s" % path
process_directory(path)
# 2. get a dict for path
print "get tile dict for path `%s`" % path
try:
with open('dic.txt', 'r') as f:
dic = p.load(f)
except:
dic = tile_dict(path)
with open('dic.txt', 'wb') as f:
p.dump(dic, f)
# 3. thumbnail the big image for compare
print "thumbnail background for compare"
# n = 30 # 原始图片缩为多少分之一
# scale = 3 # 原始图片放大倍数
big_size = im.size[0] * scale, im.size[1] * scale
im_chao = Image.new('RGB', big_size, 0xffffff)
imb_t_size = thumbnail_background(im, n)
print "how may tiles: %d X %d" % imb_t_size
print 'number of iterations: %d' % iteration
for i in range(iteration):
print 'iteration: %d' % (i + 1)
# 4. get a list of smail image for mosaic
print "get pic list"
im_tiles = get_image_list(im, dic)
# 5. paste in chaos style
print "generate final image"
im_chao = paste_chaos(im_chao, im_tiles, imb_t_size)
return im_chao


if __name__ == '__main__':
im = main('../mm.jpg', 15, 5, 2)
im.save('../final3.png')
im.show()

Demo

Do you like it?

Demo

Can you see it?

Demo Download(45.8M)

More examples here(Chinese)

Thanks

My family, It supports me.Never let me down, never pour cold water, never scold for insignificance.