Skip to content

Instantly share code, notes, and snippets.

@kernalphage
Created February 22, 2016 04:29
Show Gist options
  • Save kernalphage/1d53647ca16af246fbbf to your computer and use it in GitHub Desktop.
Save kernalphage/1d53647ca16af246fbbf to your computer and use it in GitHub Desktop.
given an image, generates a palette of n colors
import collections
from functools import *
import argparse
from PIL import Image
from colour import Color
def interesting_colors(filename, num_colors, steps=32, sample_scale=(200, 200)):
"""
:rtype: List(Color)
:type num_colors: picks the top n colors
:type steps: Lower steps, more duplicate colors. Higher steps, more washed out colors.
distance from a color that a sample can be before it is considered a different color
:type sample_scale: (width,height): Scale down the image to speed up processing time
"""
im = Image.open(filename).resize(sample_scale)
px = im.load()
# Shove the pixels into buckets
buckets = collections.defaultdict(list)
for x in range(im.size[0]):
for y in range(im.size[1]):
cur = px[x, y]
bk = bucket(cur, steps)
buckets[bk].append(cur)
# get the top n most filled buckets (aka, most common colors)
bucket_length = lambda k: len(buckets[k])
topn_keys = sorted(buckets, key=bucket_length, reverse=True)[:num_colors]
topn_colors = [color_average(buckets[k]) for k in topn_keys]
#
topn_colors = sorted(topn_colors, key=lambda c: c.hue)
return topn_colors
def hue_dist(c1, c2):
a = min(c1.hue, c2.hue)
b = max(c1.hue, c2.hue)
dist1 = b - a
dist2 = ((a+1) - b)
return min(dist1, dist2)
def named_colors( colors ):
#lets' make the background 'dark' and unsaturated
background = min(colors, key=lambda c: c.saturation + c.luminance * .5)
colors.remove(background)
#Contrast, baby
foreground = max(colors, key=lambda c: c.saturation)
colors.remove(foreground)
#totally color science
accent = max(colors, key=lambda c: hue_dist(foreground,c) + abs(foreground.luminance - c.luminance) * .5)
colors.remove(accent)
rest = sorted(colors, key=lambda c: c.hue)
return {"background": background, "foreground": foreground, "accent": accent, "rest": rest}
#### Below are helper functions
def rgb_clamp(x):
return max(0, min(x, 255))
def hx(r, g, b):
return "#{0:02x}{1:02x}{2:02x}".format(rgb_clamp(r), rgb_clamp(g), rgb_clamp(b))
def color_dist(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1]) + abs(a[2] - b[2])
def color_sum(c1, c2):
return c1[0] + c2[0], c1[1] + c2[1], c1[2] + c2[2]
def color_div(c1, s):
return c1[0] // s, c1[1] // s, c1[2] // s
# get the 'average' color: sum the (r,g,b) components and then divide by number of elements
# I'm no color scientist, but this is a good place for improvement
def color_average(l):
return Color(hx(*color_div(reduce(color_sum, l), len(l))))
def bucket(color, steps):
bucket_sz = 256 // steps
r = color[0] // steps
g = color[1] // steps
b = color[2] // steps
return r + g * bucket_sz + b * bucket_sz * bucket_sz
def bounds(s):
"""
:rtype: (width,height)
"""
try:
x, y = map(int, s.split(','))
return x, y
except:
raise argparse.ArgumentTypeError("Coordinates must be x,y")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('file', help="the image to parse")
parser.add_argument('--samples', help="The number of resulting colors", default=5, type=int)
parser.add_argument('--fidelity', help="the size of the buckets to use (advanced, untested with non-power of two)", default=16)
parser.add_argument('--size', help="resize image before sampling", default="100,100", type=bounds)
args = parser.parse_args()
colors = interesting_colors(args.file, args.samples, args.fidelity, args.size)
for c in colors:
print(c.hex)
print(named_colors(colors))
@kernalphage
Copy link
Author

Example Output:
2016-02-21 12_50_58-tk

Take an image and create a color palette. Could be used for accent colors, stylized thumnails, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment