CairX / extract-colors-py

Extract colors from an image. Colors are grouped based on visual similarities using the CIE76 formula.
https://pypi.python.org/pypi/extcolors
MIT License
67 stars 20 forks source link

Handle alpha values #3

Open CairX opened 7 years ago

arod40 commented 4 years ago

Hello,

I dont know if this is exactly what you are looking for here, but I got a use case when I had to get rid of background(alpha=0) pixels, a.k.a not to count their color, and I solved easily. I could make a PR if you want. Im gonna leave the code here in any case.


import collections

from PIL import Image, ImageDraw

from extcolors import conversion
from extcolors import difference

DEFAULT_TOLERANCE = 32

class Color:
    def __init__(self, rgb=None, lab=None, count=0):
        self.rgb = rgb
        self.lab = lab
        self.count = count
        self.compressed = False

    def __lt__(self, other):
        return self.count < other.count

def extract_from_image(img, tolerance=DEFAULT_TOLERANCE, limit=None, transparence=False):
    pixels = load(img, transparence)
    colors = count_colors(pixels)
    colors = compress(colors, tolerance)

    if limit:
        limit = min(int(limit), len(colors))
        colors = colors[:limit]

    colors = [(color.rgb, color.count) for color in colors]

    return colors, len(pixels)

def extract_from_path(path, tolerance=DEFAULT_TOLERANCE, limit=None, transparence=False):
    img = Image.open(path)
    return extract_from_image(img, tolerance, limit, transparence)

def load(img, transparence):
    if transparence:
        img = img.convert("RGBA")
        return list(filter(lambda x: x[3] > 0, img.getdata()))
    else:
        img = img.convert("RGB")
        return list(img.getdata())

def count_colors(pixels):
    counter = collections.defaultdict(int)
    for color in pixels:
        counter[color] += 1

    colors = []
    for rgb, count in counter.items():
        lab = conversion.rgb_lab(rgb)
        colors.append(Color(rgb=rgb, lab=lab, count=count))

    return colors

def compress(colors, tolerance):
    colors.sort(reverse=True)

    if tolerance <= 0:
        return colors

    i = 0
    while i < len(colors):
        larger = colors[i]

        if not larger.compressed:
            j = i + 1
            while j < len(colors):
                smaller = colors[j]

                if not smaller.compressed and difference.cie76(
                        larger.lab, smaller.lab) < tolerance:
                    larger.count += smaller.count
                    smaller.compressed = True

                j += 1
        i += 1

    colors = [color for color in colors if not color.compressed]
    colors.sort(reverse=True)

    return colors

This is the full code in the modified __init__.py file. Its quite silly actually, its just about adding a boolean transparent parameter and filtering the pixels.

CairX commented 4 years ago

Thank you for sharing your solution, much appreciated. I like the idea to remove anything with zero alpha, it feels intuitive. I will use you idea to setup some tests and investigate further, hopefully I will have some time during the weekend.

I haven't had a use case for images with transparency yet and not sure what would be a desired way to handle alpha that isn't either zero or full. I imagine a blend option would be of interest to be able to decide how the result should be affected. Is this something you encounter in your scenario?

CairX commented 4 years ago

@arod40 Sorry for not getting back to this sooner. Have now submitted 47f4ee9799298079302b8e85f0c0f293e15bf78d, that strips out pixels that are fully transparent which prevents the skew towards black in the result. The documentation has also been updated to explain that current status of transparency support.