Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cython support #11

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ dist/
node_modules/
.cache/
.vscode/
.idea
.DS_Store
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
include LICENSE
include readme.rst
global-include *.pyx
global-include *.pxd
2 changes: 1 addition & 1 deletion colorgram/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

from __future__ import absolute_import

from .colorgram import extract, Color
from .colorgram import *
126 changes: 30 additions & 96 deletions colorgram/colorgram.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,38 @@

from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import

import logging

import array
from collections import namedtuple
from PIL import Image

import sys
if sys.version_info[0] <= 2:
range = xrange
ARRAY_DATATYPE = b'l'
else:
ARRAY_DATATYPE = 'l'

logger = logging.getLogger(__name__)


try:
import cython
import pyximport
import sys

pyximport.install(language_level=sys.version_info[0])
import colorgram.utils_c as utils
logger.debug('c-boosted version will be used')
except ImportError:
import colorgram.utils_p as utils
logger.debug('pure python version will be used')


__all__ = [
'Color', 'extract'
]

Rgb = namedtuple('Rgb', ('r', 'g', 'b'))
Hsl = namedtuple('Hsl', ('h', 's', 'l'))


class Color(object):
def __init__(self, r, g, b, proportion):
self.rgb = Rgb(r, g, b)
Expand All @@ -31,68 +48,21 @@ def hsl(self):
try:
return self._hsl
except AttributeError:
self._hsl = Hsl(*hsl(*self.rgb))
self._hsl = Hsl(*utils.hsl(*self.rgb))
return self._hsl


def extract(f, number_of_colors):
image = f if isinstance(f, Image.Image) else Image.open(f)
if image.mode not in ('RGB', 'RGBA', 'RGBa'):
image = image.convert('RGB')

samples = sample(image)

pixels = list(image.getdata())
samples = utils.sample(pixels)
used = pick_used(samples)
used.sort(key=lambda x: x[0], reverse=True)
return get_colors(samples, used, number_of_colors)

def sample(image):
top_two_bits = 0b11000000

sides = 1 << 2 # Left by the number of bits used.
cubes = sides ** 7

samples = array.array(ARRAY_DATATYPE, (0 for _ in range(cubes)))
width, height = image.size

pixels = image.load()
for y in range(height):
for x in range(width):
# Pack the top two bits of all 6 values into 12 bits.
# 0bYYhhllrrggbb - luminance, hue, luminosity, red, green, blue.

r, g, b = pixels[x, y][:3]
h, s, l = hsl(r, g, b)
# Standard constants for converting RGB to relative luminance.
Y = int(r * 0.2126 + g * 0.7152 + b * 0.0722)

# Everything's shifted into place from the top two
# bits' original position - that is, bits 7-8.
packed = (Y & top_two_bits) << 4
packed |= (h & top_two_bits) << 2
packed |= (l & top_two_bits) << 0

# Due to a bug in the original colorgram.js, RGB isn't included.
# The original author tries using negative bit shifts, while in
# fact JavaScript has the stupidest possible behavior for those.
# By uncommenting these lines, "intended" behavior can be
# restored, but in order to keep result compatibility with the
# original the "error" exists here too. Add back in if it is
# ever fixed in colorgram.js.

# packed |= (r & top_two_bits) >> 2
# packed |= (g & top_two_bits) >> 4
# packed |= (b & top_two_bits) >> 6
# print "Pixel #{}".format(str(y * width + x))
# print "h: {}, s: {}, l: {}".format(str(h), str(s), str(l))
# print "R: {}, G: {}, B: {}".format(str(r), str(g), str(b))
# print "Y: {}".format(str(Y))
# print "Packed: {}, binary: {}".format(str(packed), bin(packed)[2:])
# print
packed *= 4
samples[packed] += r
samples[packed + 1] += g
samples[packed + 2] += b
samples[packed + 3] += 1
return samples

def pick_used(samples):
used = []
Expand All @@ -102,6 +72,7 @@ def pick_used(samples):
used.append((count, i))
return used


def get_colors(samples, used, number_of_colors):
pixels = 0
colors = []
Expand All @@ -122,43 +93,6 @@ def get_colors(samples, used, number_of_colors):
color.proportion /= pixels
return colors

def hsl(r, g, b):
# This looks stupid, but it's way faster than min() and max().
if r > g:
if b > r:
most, least = b, g
elif b > g:
most, least = r, g
else:
most, least = r, b
else:
if b > g:
most, least = b, r
elif b > r:
most, least = g, r
else:
most, least = g, b

l = (most + least) >> 1

if most == least:
h = s = 0
else:
diff = most - least
if l > 127:
s = diff * 255 // (510 - most - least)
else:
s = diff * 255 // (most + least)

if most == r:
h = (g - b) * 255 // diff + (1530 if g < b else 0)
elif most == g:
h = (b - r) * 255 // diff + 510
else:
h = (r - g) * 255 // diff + 1020
h //= 6

return h, s, l

# Useful snippet for testing values:
# print "Pixel #{}".format(str(y * width + x))
Expand Down
173 changes: 173 additions & 0 deletions colorgram/utils_c.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import cython

# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False
# cython: cdivision=True
# cython: nonecheck=False
# cython: language_level=3

@cython.wraparound(False)
@cython.boundscheck(False)
@cython.nonecheck(False)
@cython.cdivision(True)
@cython.optimize.unpack_method_calls(True)
cdef int rgb_to_pack(int r, int g, int b):
# declare variables
cdef int most = 0
cdef int least = 0
cdef int diff = 0
cdef int h = 0
cdef int s = 0
cdef int l = 0
cdef int Y = 0

cdef int top_two_bits = 0b11000000
cdef int result = 0

# extract HSL+Y - This looks stupid, but it's way faster than min() and max().
if r > g:
if b > r:
most, least = b, g
elif b > g:
most, least = r, g
else:
most, least = r, b
else:
if b > g:
most, least = b, r
elif b > r:
most, least = g, r
else:
most, least = g, b

l = (most + least) >> 1

if most == least:
h = s = 0
else:
diff = most - least
if l > 127:
s = diff * 255 // (510 - most - least)
else:
s = diff * 255 // (most + least)

if most == r:
h = (g - b) * 255 // diff + (1530 if g < b else 0)
elif most == g:
h = (b - r) * 255 // diff + 510
else:
h = (r - g) * 255 // diff + 1020
h //= 6

Y = int(r * 0.2126 + g * 0.7152 + b * 0.0722)

# return packed info
# result = (Y & top_two_bits) << 4
# result |= (h & top_two_bits) << 2
# result |= (l & top_two_bits) << 0
#
# result *= 4

return (((Y & top_two_bits) << 4) + ((h & top_two_bits) << 2) + (l & top_two_bits)) * 4
# return result

@cython.wraparound(False)
@cython.boundscheck(False)
cpdef list sample(list pixels):
cdef int top_two_bits = 0b11000000

cdef int sides = 1 << 2 # 4 - Left by the number of bits used.

cdef int cubes = sides ** 7

cdef list samples = [0] * cubes

for item in pixels:
r = item[0]
g = item[1]
b = item[2]
# Pack the top two bits of all 6 values into 12 bits.
# 0bYYhhllrrggbb - luminance, hue, luminosity, red, green, blue.

# Standard constants for converting RGB to relative luminance.
# Y = int(r * 0.2126 + g * 0.7152 + b * 0.0722)

# Everything's shifted into place from the top two
# bits' original position - that is, bits 7-8.
# packed = (Y & top_two_bits) << 4
# packed |= (h & top_two_bits) << 2
# packed |= (l & top_two_bits) << 0

# Due to a bug in the original colorgram.js, RGB isn't included.
# The original author tries using negative bit shifts, while in
# fact JavaScript has the stupidest possible behavior for those.
# By uncommenting these lines, "intended" behavior can be
# restored, but in order to keep result compatibility with the
# original the "error" exists here too. Add back in if it is
# ever fixed in colorgram.js.

# packed |= (r & top_two_bits) >> 2
# packed |= (g & top_two_bits) >> 4
# packed |= (b & top_two_bits) >> 6

packed = rgb_to_pack(r,g,b)

# packed = 0
samples[packed] += r
samples[packed + 1] += g
samples[packed + 2] += b
samples[packed + 3] += 1
return samples


@cython.wraparound(False)
@cython.boundscheck(False)
@cython.nonecheck(False)
@cython.cdivision(True)
@cython.optimize.unpack_method_calls(True)
cpdef (int, int, int) hsl(int r, int g, int b):
# declare variables
cdef int most = 0
cdef int least = 0
cdef int diff = 0
cdef int h = 0
cdef int s = 0
cdef int l = 0

# This looks stupid, but it's way faster than min() and max().
if r > g:
if b > r:
most, least = b, g
elif b > g:
most, least = r, g
else:
most, least = r, b
else:
if b > g:
most, least = b, r
elif b > r:
most, least = g, r
else:
most, least = g, b

l = (most + least) >> 1

if most == least:
h = s = 0
else:
diff = most - least
if l > 127:
s = diff * 255 // (510 - most - least)
else:
s = diff * 255 // (most + least)

if most == r:
h = (g - b) * 255 // diff + (1530 if g < b else 0)
elif most == g:
h = (b - r) * 255 // diff + 510
else:
h = (r - g) * 255 // diff + 1020
h //= 6

return h, s, l
Loading