Automatic Warhol-Style Serigraph Generation
Posted: August 26, 2012 Filed under: Python | Tags: Andy Warhol, Python Imaging Library Comments Off on Automatic Warhol-Style Serigraph GenerationWe recently had a logo drawn up for a research project that I’ve been involved with for a couple of years. I’ve been planning on having a poster-size version printed, but I thought that simply printing a very large logo would be too easy. I’m a fan of pop-art, so I thought that an Andy Warhol-style serigraph of our logo would make a cool poster.
My first thought was that this should be pretty easy in Photoshop. I hit the Internet to find tutorials for making Warhol-style graphics from an image and started Photoshop. I quickly abandoned that idea; I don’t know Photoshop well at all, and the entire process appeared painfully manual. So, naturally, I decided to do it programmatically with the Python Imaging Library.
I used Warhol’s Che Guevara serigraph as a model.

I used the colors in the original by manually picked them out from the original piece and saving them in a list of dictionaries:
# colors here are taken directly from Warhol's Che Guevara serigraph # (in order left to right, top to bottom) colorset = [ {'bg' : (255,255,0,255), 'fg' : (50,9,125,255), 'skin': (118,192,0,255)}, {'bg' : (0,122,240,255), 'fg' : (255,0,112,255), 'skin': (255,255,0,255)}, {'bg' : (50,0,130,255),'fg' : (255,0,0,255),'skin': (243,145,192,255)}, {'bg' : (255,126,0,255),'fg' : (134,48,149,255),'skin': (111,185,248,255)}, {'bg' : (255,0,0,255),'fg' : (35,35,35,255),'skin': (255,255,255,255)}, {'bg' : (122,192,0,255),'fg' : (255,89,0,255),'skin': (250,255,160,255)}, {'bg' : (0,114,100,255),'fg' : (252,0,116,255),'skin': (250,250,230,255)}, {'bg' : (250,255,0,255),'fg' : (254,0,0,255),'skin': (139,198,46,255)}, {'bg' : (253,0,118,255),'fg' : (51,2,126,255),'skin': (255,105,0,255)} ]
Automatically Warholifying images is not straightforward for general pictures or images, but working on vector-style images with a couple of a assumptions is not very difficult. The assumptions I made for this were that the “face” color is white and the background is transparent.
The image I’ll use for this example is a vector version of the classic photo of Che Guevara from Wikimedia Commons. I modified this image by using a bucket fill tool to make the face area white, but did not have to make any other modifications.

First, we change the transparent background to be the bg_color specified and change everything else to be fg_color.
def color_bg_fg(image, bg_color, fg_color): '''change transparent background to bg_color and change everything non-transparent to fg_color''' fg_layer = Image.new('RGBA', image.size, fg_color) bg_layer = Image.new('RGBA', image.size, bg_color) masked_image = Image.composite(fg_layer, bg_layer, image) return masked_image bg_fg_layer = color_bg_fg(image, bg_color, fg_color)
Output:

Next we create a mask of the skin area. The first step here is to change the background from transparent to something non-transparent (I chose black):
def darken_bg(image, color): '''composite image on top of a single-color image, effectively turning all transparent parts to that color''' color_layer = Image.new('RGBA', image.size, color) masked_image = Image.composite(image, color_layer, image) return masked_image temp_dark_image = darken_bg(image, (0,0,0,255))
Output:

We then change all of the white-ish areas of the image to be transparent.
def white_to_color(image, color): '''change all colors close to white and non-transparent (alpha > 0) to be color.''' threshold=50 dist=10 arr=np.array(np.asarray(image)) r,g,b,a=np.rollaxis(arr,axis=-1) mask=((r>threshold) & (g>threshold) & (b>threshold) & (np.abs(r-g)<dist) & (np.abs(r-b)<dist) & (np.abs(g-b)<dist) & (a>0) ) arr[mask]=color image=Image.fromarray(arr,mode='RGBA') return image skin_mask = white_to_color(temp_dark_image,(0,0,0,0))
Output:

We then create an image the same size as our source file that is nothing but skin_color.
skin_layer = Image.new('RGBA', image.size, skin_color)
Output:

Finally, we composite bg_gf_layer with skin_layer, using skin_mask as a mask.
out = Image.composite(bg_fg_layer, skin_layer, skin_mask)
Output:

Here is all the steps put together:
def make_warhol_single(image, bg_color, fg_color, skin_color): '''create a single warhol-serigraph-style image''' bg_fg_layer = color_bg_fg(image, bg_color, fg_color) temp_dark_image = darken_bg(image, (0,0,0,255)) skin_mask = white_to_color(temp_dark_image,(0,0,0,0)) skin_layer = Image.new('RGBA', image.size, skin_color) out = Image.composite(bg_fg_layer, skin_layer, skin_mask) return out
Finally, we can call the make_warhol_single function with multiple color combinations and create a single image containing all of them.
def warholify(image_file): im = Image.open(image_file).convert('RGBA') warhols = [] for colors in colorset: bg = colors['bg'] fg = colors['fg'] skin = colors['skin'] warhols.append(make_warhol_single(im, bg, fg, skin)) x = im.size[0] y = im.size[1] blank_image = Image.new("RGB", (x*3, y*3)) blank_image.paste(warhols[0], (0,0)) blank_image.paste(warhols[1], (x,0)) blank_image.paste(warhols[2], (x*2,0)) blank_image.paste(warhols[3], (0,y)) blank_image.paste(warhols[4], (x,y)) blank_image.paste(warhols[5], (x*2,y)) blank_image.paste(warhols[6], (0,y*2)) blank_image.paste(warhols[7], (x,y*2)) blank_image.paste(warhols[8], (x*2,y*2)) blank_image.save('out.png')
Output:

The full source code for this program is available on GitHub.