pax_global_header00006660000000000000000000000064130470131650014512gustar00rootroot0000000000000052 comment=18a099ba3d9f649406ab1cfa75990df1cf658f03 fengsp-color-thief-py-18a099b/000077500000000000000000000000001304701316500161725ustar00rootroot00000000000000fengsp-color-thief-py-18a099b/.gitignore000066400000000000000000000000771304701316500201660ustar00rootroot00000000000000.DS_Store *.pyc *.pyo *.egg-info dist build docs/_build .tox/* fengsp-color-thief-py-18a099b/CHANGES000066400000000000000000000004151304701316500171650ustar00rootroot00000000000000Color 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/LICENSE000066400000000000000000000027621304701316500172060ustar00rootroot00000000000000Copyright (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.in000066400000000000000000000000251304701316500177250ustar00rootroot00000000000000include LICENSE *.py fengsp-color-thief-py-18a099b/Makefile000066400000000000000000000004201304701316500176260ustar00rootroot00000000000000clean-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.rst000066400000000000000000000042031304701316500176600ustar00rootroot00000000000000Color 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.py000066400000000000000000000323201304701316500207020ustar00rootroot00000000000000# -*- 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/000077500000000000000000000000001304701316500200105ustar00rootroot00000000000000fengsp-color-thief-py-18a099b/examples/demo.py000066400000000000000000000006241304701316500213100ustar00rootroot00000000000000# -*- 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.cfg000066400000000000000000000000261304701316500200110ustar00rootroot00000000000000[wheel] universal = 1 fengsp-color-thief-py-18a099b/setup.py000066400000000000000000000017321304701316500177070ustar00rootroot00000000000000""" 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', ], )