pax_global_header 0000666 0000000 0000000 00000000064 13047013165 0014512 g ustar 00root root 0000000 0000000 52 comment=18a099ba3d9f649406ab1cfa75990df1cf658f03
fengsp-color-thief-py-18a099b/ 0000775 0000000 0000000 00000000000 13047013165 0016172 5 ustar 00root root 0000000 0000000 fengsp-color-thief-py-18a099b/.gitignore 0000664 0000000 0000000 00000000077 13047013165 0020166 0 ustar 00root root 0000000 0000000 .DS_Store
*.pyc
*.pyo
*.egg-info
dist
build
docs/_build
.tox/*
fengsp-color-thief-py-18a099b/CHANGES 0000664 0000000 0000000 00000000415 13047013165 0017165 0 ustar 00root root 0000000 0000000 Color Thief Changelog
=====================
Version 0.1
-----------
First public preview release.
Version 0.2
-----------
Released on Oct 14th 2015
- Added Python3.x support
Version 0.2.1
-------------
Released on Feb 9th 2017
- Removed useless list creation
fengsp-color-thief-py-18a099b/LICENSE 0000664 0000000 0000000 00000002762 13047013165 0017206 0 ustar 00root root 0000000 0000000 Copyright (c) 2015 by Shipeng Feng.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
fengsp-color-thief-py-18a099b/MANIFEST.in 0000664 0000000 0000000 00000000025 13047013165 0017725 0 ustar 00root root 0000000 0000000 include LICENSE *.py
fengsp-color-thief-py-18a099b/Makefile 0000664 0000000 0000000 00000000420 13047013165 0017626 0 ustar 00root root 0000000 0000000 clean-pyc:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
lines:
find . -name "*.py"|xargs cat|wc -l
release:
python setup.py register
python setup.py sdist upload
python setup.py bdist_wheel upload
fengsp-color-thief-py-18a099b/README.rst 0000664 0000000 0000000 00000004203 13047013165 0017660 0 ustar 00root root 0000000 0000000 Color Thief
===========
A Python module for grabbing the color palette from an image.
.. image:: http://img3.douban.com/view/photo/large/public/p2239120853.jpg
:width: 400 px
:align: center
Installation
------------
::
$ pip install colorthief
Usage
-----
.. code:: python
from colorthief import ColorThief
color_thief = ColorThief('/path/to/imagefile')
# get the dominant color
dominant_color = color_thief.get_color(quality=1)
# build a color palette
palette = color_thief.get_palette(color_count=6)
API
---
.. code:: python
class ColorThief(object):
def __init__(self, file):
"""Create one color thief for one image.
:param file: A filename (string) or a file object. The file object
must implement `read()`, `seek()`, and `tell()` methods,
and be opened in binary mode.
"""
pass
def get_color(self, quality=10):
"""Get the dominant color.
:param quality: quality settings, 1 is the highest quality, the bigger
the number, the faster a color will be returned but
the greater the likelihood that it will not be the
visually most dominant color
:return tuple: (r, g, b)
"""
pass
def get_palette(self, color_count=10, quality=10):
"""Build a color palette. We are using the median cut algorithm to
cluster similar colors.
:param color_count: the size of the palette, max number of colors
:param quality: quality settings, 1 is the highest quality, the bigger
the number, the faster the palette generation, but the
greater the likelihood that colors will be missed.
:return list: a list of tuple in the form (r, g, b)
"""
pass
Thanks
------
Thanks to Lokesh Dhakar for his `original work
`_.
Better
------
If you feel anything wrong, feedbacks or pull requests are welcome.
fengsp-color-thief-py-18a099b/colorthief.py 0000664 0000000 0000000 00000032320 13047013165 0020702 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
colorthief
~~~~~~~~~~
Grabbing the color palette from an image.
:copyright: (c) 2015 by Shipeng Feng.
:license: BSD, see LICENSE for more details.
"""
__version__ = '0.2.1'
import math
from PIL import Image
class cached_property(object):
"""Decorator that creates converts a method with a single
self argument into a property cached on the instance.
"""
def __init__(self, func):
self.func = func
def __get__(self, instance, type):
res = instance.__dict__[self.func.__name__] = self.func(instance)
return res
class ColorThief(object):
"""Color thief main class."""
def __init__(self, file):
"""Create one color thief for one image.
:param file: A filename (string) or a file object. The file object
must implement `read()`, `seek()`, and `tell()` methods,
and be opened in binary mode.
"""
self.image = Image.open(file)
def get_color(self, quality=10):
"""Get the dominant color.
:param quality: quality settings, 1 is the highest quality, the bigger
the number, the faster a color will be returned but
the greater the likelihood that it will not be the
visually most dominant color
:return tuple: (r, g, b)
"""
palette = self.get_palette(5, quality)
return palette[0]
def get_palette(self, color_count=10, quality=10):
"""Build a color palette. We are using the median cut algorithm to
cluster similar colors.
:param color_count: the size of the palette, max number of colors
:param quality: quality settings, 1 is the highest quality, the bigger
the number, the faster the palette generation, but the
greater the likelihood that colors will be missed.
:return list: a list of tuple in the form (r, g, b)
"""
image = self.image.convert('RGBA')
width, height = image.size
pixels = image.getdata()
pixel_count = width * height
valid_pixels = []
for i in range(0, pixel_count, quality):
r, g, b, a = pixels[i]
# If pixel is mostly opaque and not white
if a >= 125:
if not (r > 250 and g > 250 and b > 250):
valid_pixels.append((r, g, b))
# Send array to quantize function which clusters values
# using median cut algorithm
cmap = MMCQ.quantize(valid_pixels, color_count)
return cmap.palette
class MMCQ(object):
"""Basic Python port of the MMCQ (modified median cut quantization)
algorithm from the Leptonica library (http://www.leptonica.com/).
"""
SIGBITS = 5
RSHIFT = 8 - SIGBITS
MAX_ITERATION = 1000
FRACT_BY_POPULATIONS = 0.75
@staticmethod
def get_color_index(r, g, b):
return (r << (2 * MMCQ.SIGBITS)) + (g << MMCQ.SIGBITS) + b
@staticmethod
def get_histo(pixels):
"""histo (1-d array, giving the number of pixels in each quantized
region of color space)
"""
histo = dict()
for pixel in pixels:
rval = pixel[0] >> MMCQ.RSHIFT
gval = pixel[1] >> MMCQ.RSHIFT
bval = pixel[2] >> MMCQ.RSHIFT
index = MMCQ.get_color_index(rval, gval, bval)
histo[index] = histo.setdefault(index, 0) + 1
return histo
@staticmethod
def vbox_from_pixels(pixels, histo):
rmin = 1000000
rmax = 0
gmin = 1000000
gmax = 0
bmin = 1000000
bmax = 0
for pixel in pixels:
rval = pixel[0] >> MMCQ.RSHIFT
gval = pixel[1] >> MMCQ.RSHIFT
bval = pixel[2] >> MMCQ.RSHIFT
rmin = min(rval, rmin)
rmax = max(rval, rmax)
gmin = min(gval, gmin)
gmax = max(gval, gmax)
bmin = min(bval, bmin)
bmax = max(bval, bmax)
return VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo)
@staticmethod
def median_cut_apply(histo, vbox):
if not vbox.count:
return (None, None)
rw = vbox.r2 - vbox.r1 + 1
gw = vbox.g2 - vbox.g1 + 1
bw = vbox.b2 - vbox.b1 + 1
maxw = max([rw, gw, bw])
# only one pixel, no split
if vbox.count == 1:
return (vbox.copy, None)
# Find the partial sum arrays along the selected axis.
total = 0
sum_ = 0
partialsum = {}
lookaheadsum = {}
do_cut_color = None
if maxw == rw:
do_cut_color = 'r'
for i in range(vbox.r1, vbox.r2+1):
sum_ = 0
for j in range(vbox.g1, vbox.g2+1):
for k in range(vbox.b1, vbox.b2+1):
index = MMCQ.get_color_index(i, j, k)
sum_ += histo.get(index, 0)
total += sum_
partialsum[i] = total
elif maxw == gw:
do_cut_color = 'g'
for i in range(vbox.g1, vbox.g2+1):
sum_ = 0
for j in range(vbox.r1, vbox.r2+1):
for k in range(vbox.b1, vbox.b2+1):
index = MMCQ.get_color_index(j, i, k)
sum_ += histo.get(index, 0)
total += sum_
partialsum[i] = total
else: # maxw == bw
do_cut_color = 'b'
for i in range(vbox.b1, vbox.b2+1):
sum_ = 0
for j in range(vbox.r1, vbox.r2+1):
for k in range(vbox.g1, vbox.g2+1):
index = MMCQ.get_color_index(j, k, i)
sum_ += histo.get(index, 0)
total += sum_
partialsum[i] = total
for i, d in partialsum.items():
lookaheadsum[i] = total - d
# determine the cut planes
dim1 = do_cut_color + '1'
dim2 = do_cut_color + '2'
dim1_val = getattr(vbox, dim1)
dim2_val = getattr(vbox, dim2)
for i in range(dim1_val, dim2_val+1):
if partialsum[i] > (total / 2):
vbox1 = vbox.copy
vbox2 = vbox.copy
left = i - dim1_val
right = dim2_val - i
if left <= right:
d2 = min([dim2_val - 1, int(i + right / 2)])
else:
d2 = max([dim1_val, int(i - 1 - left / 2)])
# avoid 0-count boxes
while not partialsum.get(d2, False):
d2 += 1
count2 = lookaheadsum.get(d2)
while not count2 and partialsum.get(d2-1, False):
d2 -= 1
count2 = lookaheadsum.get(d2)
# set dimensions
setattr(vbox1, dim2, d2)
setattr(vbox2, dim1, getattr(vbox1, dim2) + 1)
return (vbox1, vbox2)
return (None, None)
@staticmethod
def quantize(pixels, max_color):
"""Quantize.
:param pixels: a list of pixel in the form (r, g, b)
:param max_color: max number of colors
"""
if not pixels:
raise Exception('Empty pixels when quantize.')
if max_color < 2 or max_color > 256:
raise Exception('Wrong number of max colors when quantize.')
histo = MMCQ.get_histo(pixels)
# check that we aren't below maxcolors already
if len(histo) <= max_color:
# generate the new colors from the histo and return
pass
# get the beginning vbox from the colors
vbox = MMCQ.vbox_from_pixels(pixels, histo)
pq = PQueue(lambda x: x.count)
pq.push(vbox)
# inner function to do the iteration
def iter_(lh, target):
n_color = 1
n_iter = 0
while n_iter < MMCQ.MAX_ITERATION:
vbox = lh.pop()
if not vbox.count: # just put it back
lh.push(vbox)
n_iter += 1
continue
# do the cut
vbox1, vbox2 = MMCQ.median_cut_apply(histo, vbox)
if not vbox1:
raise Exception("vbox1 not defined; shouldn't happen!")
lh.push(vbox1)
if vbox2: # vbox2 can be null
lh.push(vbox2)
n_color += 1
if n_color >= target:
return
if n_iter > MMCQ.MAX_ITERATION:
return
n_iter += 1
# first set of colors, sorted by population
iter_(pq, MMCQ.FRACT_BY_POPULATIONS * max_color)
# Re-sort by the product of pixel occupancy times the size in
# color space.
pq2 = PQueue(lambda x: x.count * x.volume)
while pq.size():
pq2.push(pq.pop())
# next set - generate the median cuts using the (npix * vol) sorting.
iter_(pq2, max_color - pq2.size())
# calculate the actual colors
cmap = CMap()
while pq2.size():
cmap.push(pq2.pop())
return cmap
class VBox(object):
"""3d color space box"""
def __init__(self, r1, r2, g1, g2, b1, b2, histo):
self.r1 = r1
self.r2 = r2
self.g1 = g1
self.g2 = g2
self.b1 = b1
self.b2 = b2
self.histo = histo
@cached_property
def volume(self):
sub_r = self.r2 - self.r1
sub_g = self.g2 - self.g1
sub_b = self.b2 - self.b1
return (sub_r + 1) * (sub_g + 1) * (sub_b + 1)
@property
def copy(self):
return VBox(self.r1, self.r2, self.g1, self.g2,
self.b1, self.b2, self.histo)
@cached_property
def avg(self):
ntot = 0
mult = 1 << (8 - MMCQ.SIGBITS)
r_sum = 0
g_sum = 0
b_sum = 0
for i in range(self.r1, self.r2 + 1):
for j in range(self.g1, self.g2 + 1):
for k in range(self.b1, self.b2 + 1):
histoindex = MMCQ.get_color_index(i, j, k)
hval = self.histo.get(histoindex, 0)
ntot += hval
r_sum += hval * (i + 0.5) * mult
g_sum += hval * (j + 0.5) * mult
b_sum += hval * (k + 0.5) * mult
if ntot:
r_avg = int(r_sum / ntot)
g_avg = int(g_sum / ntot)
b_avg = int(b_sum / ntot)
else:
r_avg = int(mult * (self.r1 + self.r2 + 1) / 2)
g_avg = int(mult * (self.g1 + self.g2 + 1) / 2)
b_avg = int(mult * (self.b1 + self.b2 + 1) / 2)
return r_avg, g_avg, b_avg
def contains(self, pixel):
rval = pixel[0] >> MMCQ.RSHIFT
gval = pixel[1] >> MMCQ.RSHIFT
bval = pixel[2] >> MMCQ.RSHIFT
return all([
rval >= self.r1,
rval <= self.r2,
gval >= self.g1,
gval <= self.g2,
bval >= self.b1,
bval <= self.b2,
])
@cached_property
def count(self):
npix = 0
for i in range(self.r1, self.r2 + 1):
for j in range(self.g1, self.g2 + 1):
for k in range(self.b1, self.b2 + 1):
index = MMCQ.get_color_index(i, j, k)
npix += self.histo.get(index, 0)
return npix
class CMap(object):
"""Color map"""
def __init__(self):
self.vboxes = PQueue(lambda x: x['vbox'].count * x['vbox'].volume)
@property
def palette(self):
return self.vboxes.map(lambda x: x['color'])
def push(self, vbox):
self.vboxes.push({
'vbox': vbox,
'color': vbox.avg,
})
def size(self):
return self.vboxes.size()
def nearest(self, color):
d1 = None
p_color = None
for i in range(self.vboxes.size()):
vbox = self.vboxes.peek(i)
d2 = math.sqrt(
math.pow(color[0] - vbox['color'][0], 2) +
math.pow(color[1] - vbox['color'][1], 2) +
math.pow(color[2] - vbox['color'][2], 2)
)
if d1 is None or d2 < d1:
d1 = d2
p_color = vbox['color']
return p_color
def map(self, color):
for i in range(self.vboxes.size()):
vbox = self.vboxes.peek(i)
if vbox['vbox'].contains(color):
return vbox['color']
return self.nearest(color)
class PQueue(object):
"""Simple priority queue."""
def __init__(self, sort_key):
self.sort_key = sort_key
self.contents = []
self._sorted = False
def sort(self):
self.contents.sort(key=self.sort_key)
self._sorted = True
def push(self, o):
self.contents.append(o)
self._sorted = False
def peek(self, index=None):
if not self._sorted:
self.sort()
if index is None:
index = len(self.contents) - 1
return self.contents[index]
def pop(self):
if not self._sorted:
self.sort()
return self.contents.pop()
def size(self):
return len(self.contents)
def map(self, f):
return list(map(f, self.contents))
fengsp-color-thief-py-18a099b/examples/ 0000775 0000000 0000000 00000000000 13047013165 0020010 5 ustar 00root root 0000000 0000000 fengsp-color-thief-py-18a099b/examples/demo.py 0000664 0000000 0000000 00000000624 13047013165 0021310 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import sys
if sys.version_info < (3, 0):
from urllib2 import urlopen
else:
from urllib.request import urlopen
import io
from colorthief import ColorThief
fd = urlopen('http://lokeshdhakar.com/projects/color-thief/img/photo1.jpg')
f = io.BytesIO(fd.read())
color_thief = ColorThief(f)
print(color_thief.get_color(quality=1))
print(color_thief.get_palette(quality=1))
fengsp-color-thief-py-18a099b/setup.cfg 0000664 0000000 0000000 00000000026 13047013165 0020011 0 ustar 00root root 0000000 0000000 [wheel]
universal = 1
fengsp-color-thief-py-18a099b/setup.py 0000664 0000000 0000000 00000001732 13047013165 0017707 0 ustar 00root root 0000000 0000000 """
Color Thief
-----------
A module for grabbing the color palette from an image.
Links
`````
* `github `_
* `development version
`_
"""
from setuptools import setup
setup(
name='colorthief',
version='0.2.1',
url='https://github.com/fengsp/color-thief-py',
license='BSD',
author='Shipeng Feng',
author_email='fsp261@gmail.com',
description='A module for grabbing the color palette from an image.',
long_description=__doc__,
py_modules=['colorthief'],
install_requires=[
'Pillow'
],
zip_safe=False,
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
],
)