MiniMiloe
MiniMiloe

Reputation: 75

Count different colour pixels - Python

I found this code on here but all it does is count black and red, and this would only work with a black and red image.

from PIL import Image
im = Image.open('oh.png')

black = 0
red = 0

for pixel in im.getdata():
    if pixel == (0, 0, 0, 255): # if your image is RGB (if RGBA, (0, 0, 0, 255) or so
        black += 1
    else:
        red += 1
print('black=' + str(black)+', red='+str(red))

How would I be able to check different colours and not have it so precise, for example black could be (0, 0, 0) to (40,40,40).

This may be too much to ask, just let me know. Thanks :)

Upvotes: 4

Views: 8273

Answers (4)

Mark Setchell
Mark Setchell

Reputation: 208003

Paul's answer is more elegant, but basically I think you can solve it by defining a function that gives you a "distance" between any two RGB colours something like this:

def distance(col1, col2):
    (r1,g1,b1) = col1
    (r2,g2,b2) = col2
    return (r1 - r2)**2 + (g1 - g2) ** 2 + (b1 - b2) **2

Now, all you need to do (in pseudo-code) is:

load your pre-existing reference colours into a list
load your new image
for each pixel in new image
    # Calculate distance from this pixel to first one in reference list
    mindist=distance(this pixel, first pixel in reference colours)
    nearest=first pixel in reference colours
    # Now see if any other colour in reference list is closer
    for index=1 to number of colours in reference list
        d=distance(this pixel, reference list[index])
        if d < mindist:
           mindist=d
           nearest=reference list[index]
    end
    replace pixel with nearest one from reference list as determined above
end

I am still learning Python, so my translation of the above may not be optimal but it works!

#!/usr/local/bin/python3
from PIL import Image
import numpy as np

# Calculate distance in RGB space between two RGB pixels
def distance(col1, col2):
    r1,g1,b1 = col1
    r2,g2,b2 = col2
    return (r1 - r2)**2 + (g1 - g2) ** 2 + (b1 - b2) **2

# All colours in the image will be forced to nearest one in this list
refColours=(
      [[255,   0,   0],    # red
       [  0, 255,   0],    # green
       [  0,   0, 255],    # blue
       [255, 255,   0],    # yellow
       [  0, 255, 255],    # cyan
       [255,   0, 255],    # magenta
       [  0,   0,   0],    # black
       [255, 255, 255]])   # white

# Load your new image and note its width and height
im = Image.open('colorwheel.png')
imrgb = im.convert("RGB")
(w,h)=im.size[0],im.size[1]

# Make a buffer for our output pixels
px=np.zeros(w*h,dtype=np.uint8)
idx=0

for pixel in list(imrgb.getdata()):
   # Calculate distance from this pixel to first one in reference list
   mindist = distance([pixel[0],pixel[1],pixel[2]],refColours[0])
   nearest = 0
   # Now see if any other colour in reference list is closer
   for index in range(1,len(refColours)):
       d=distance([pixel[0],pixel[1],pixel[2]],refColours[index])
       if d < mindist:
          mindist=d
          nearest=index
   # Set output pixel to nearest
   px[idx]=nearest
   idx += 1

# Reshape our output pixels to match input image
px=px.reshape(w,h)
# Make output image from our pixels
outimg=Image.fromarray(px).convert('P')
# Put our palette of favourite colours into the output image
palette=[item for sublist in refColours for item in sublist]
outimg.putpalette(palette)
outimg.save("result.png")

So, I start with this as colorwheel.png:

enter image description here

and end up with this:

enter image description here


Of course, the easier solution, like I suggested in the comments is to use a tool like ImageMagick to remap the colours in your new image to those in your "reference" image, which you do like this on the command-line:

convert colorwheel.png +dither -remap colormap.png result.png

as shown in my other answer here. So in Python, you could do that with the system() call or using the subprocess module:

cmd="https://stackoverflow.com/a/38328879/2836621"
system(cmd)

Upvotes: 1

Paul Panzer
Paul Panzer

Reputation: 53099

Here is an approach using a KDTree for efficient nearest color lookup. Note that while KDTrees may be pretty advanced, using them is actually quite simple.

import numpy as np
from matplotlib import colors
from scipy.spatial import cKDTree as KDTree
from scipy.misc import face

REDUCED_COLOR_SPACE = True

# borrow a list of named colors from matplotlib
if REDUCED_COLOR_SPACE:
    use_colors = {k: colors.cnames[k] for k in ['red', 'green', 'blue', 'black', 'yellow', 'purple']}
else:
    use_colors = colors.cnames

# translate hexstring to RGB tuple
named_colors = {k: tuple(map(int, (v[1:3], v[3:5], v[5:7]), 3*(16,)))
                for k, v in use_colors.items()}
ncol = len(named_colors)

if REDUCED_COLOR_SPACE:
    ncol -= 1
    no_match = named_colors.pop('purple')
else:
    no_match = named_colors['purple']

# make an array containing the RGB values 
color_tuples = list(named_colors.values())
color_tuples.append(no_match)
color_tuples = np.array(color_tuples)

color_names = list(named_colors)
color_names.append('no match')

# get example picture
img = face()

# build tree
tree = KDTree(color_tuples[:-1])
# tolerance for color match `inf` means use best match no matter how
# bad it may be
tolerance = np.inf
# find closest color in tree for each pixel in picture
dist, idx = tree.query(img, distance_upper_bound=tolerance)
# count and reattach names
counts = dict(zip(color_names, np.bincount(idx.ravel(), None, ncol+1)))

print(counts)

import pylab

pylab.imshow(img)
pylab.savefig('orig.png')
pylab.clf()
pylab.imshow(color_tuples[idx])
pylab.savefig('minimal.png' if REDUCED_COLOR_SPACE else 'reduced.png')

Output with full matplotlib named color space:

{'aliceblue': 315, 'antiquewhite': 0, 'aqua': 0, 'aquamarine': 0, 'azure': 0, 'beige': 27, 'bisque': 0, 'black': 88584, 'blanchedalmond': 0, 'blue': 0, 'blueviolet': 0, 'brown': 0, 'burlywood': 76, 'cadetblue': 0, 'chartreuse': 0, 'chocolate': 0, 'coral': 0, 'cornflowerblue': 0, 'cornsilk': 0, 'crimson': 0, 'cyan': 0, 'darkblue': 0, 'darkcyan': 0, 'darkgoldenrod': 0, 'darkgray': 0, 'darkgreen': 4148, 'darkgrey': 71985, 'darkkhaki': 32907, 'darkmagenta': 0, 'darkolivegreen': 90899, 'darkorange': 0, 'darkorchid': 0, 'darkred': 0, 'darksalmon': 0, 'darkseagreen': 30171, 'darkslateblue': 134, 'darkslategray': 108608, 'darkslategrey': 0, 'darkturquoise': 0, 'darkviolet': 0, 'deeppink': 0, 'deepskyblue': 0, 'dimgray': 0, 'dimgrey': 108318, 'dodgerblue': 0, 'firebrick': 0, 'floralwhite': 0, 'forestgreen': 1, 'fuchsia': 0, 'gainsboro': 10438, 'ghostwhite': 736, 'gold': 0, 'goldenrod': 0, 'gray': 0, 'green': 0, 'greenyellow': 0, 'grey': 79835, 'honeydew': 0, 'hotpink': 0, 'indianred': 0, 'indigo': 0, 'ivory': 0, 'khaki': 1056, 'lavender': 4650, 'lavenderblush': 46, 'lawngreen': 0, 'lemonchiffon': 0, 'lightblue': 3, 'lightcoral': 0, 'lightcyan': 0, 'lightgoldenrodyellow': 0, 'lightgray': 11905, 'lightgreen': 2323, 'lightgrey': 0, 'lightpink': 0, 'lightsalmon': 0, 'lightseagreen': 0, 'lightskyblue': 0, 'lightslategray': 0, 'lightslategrey': 31920, 'lightsteelblue': 3590, 'lightyellow': 0, 'lime': 0, 'limegreen': 0, 'linen': 46, 'magenta': 0, 'maroon': 0, 'mediumaquamarine': 0, 'mediumblue': 0, 'mediumorchid': 0, 'mediumpurple': 15, 'mediumseagreen': 0, 'mediumslateblue': 0, 'mediumspringgreen': 0, 'mediumturquoise': 0, 'mediumvioletred': 0, 'midnightblue': 54, 'mintcream': 0, 'mistyrose': 19, 'moccasin': 0, 'navajowhite': 0, 'navy': 0, 'oldlace': 0, 'olive': 0, 'olivedrab': 30828, 'orange': 0, 'orangered': 0, 'orchid': 0, 'palegoldenrod': 1499, 'palegreen': 285, 'paleturquoise': 0, 'palevioletred': 0, 'papayawhip': 0, 'peachpuff': 0, 'peru': 21, 'pink': 0, 'plum': 0, 'powderblue': 0, 'purple': 0, 'rebeccapurple': 0, 'red': 0, 'rosybrown': 2831, 'royalblue': 0, 'saddlebrown': 0, 'salmon': 0, 'sandybrown': 0, 'seagreen': 0, 'seashell': 0, 'sienna': 5, 'silver': 35951, 'skyblue': 0, 'slateblue': 0, 'slategray': 7836, 'slategrey': 0, 'snow': 18, 'springgreen': 0, 'steelblue': 0, 'tan': 3925, 'teal': 0, 'thistle': 10274, 'tomato': 0, 'turquoise': 0, 'violet': 0, 'wheat': 21, 'white': 3, 'whitesmoke': 834, 'yellow': 0, 'yellowgreen': 9292, 'no match': 0}

Output with only basic colors:

{'red': 0, 'green': 403561, 'blue': 3262, 'black': 153782, 'yellow': 225827, 'no match': 0}

Original picture:

enter image description here

Reduced color version:

enter image description here

Basic color versioin:

enter image description here

Upvotes: 2

Aaron Krajeski
Aaron Krajeski

Reputation: 747

You'll have to define your colour buckets. I'd recommend using hsv colour space.

from PIL import Image
import colorsys
im = Image.open('download.png')

NUM_BUCKETS = 6 # Just for example
colour_counts = [0] * NUM_BUCKETS

for pixel in im.getdata():
    hue, saturation, value = colorsys.hsv_to_rgb(pixel[0], pixel[1], pixel[2])
    hue_bucket = hue * NUM_BUCKETS // 255 # Using python3 to get an int
    colour_counts[hue_bucket] += 1

colour_names = ["red", "yellow", "green", "cyan", "blue", "magenta"]
for name, count in [x for x in zip(colour_names, colour_counts)]:
    print("{n} = {c}".format(n=name, c=count))

So, this just partitions the colour space into 6, but you can use any number you'd like (you'll just have to think of names for all of them). Black and white don't work well, because we're looking at hues. To capture black and white as well use the "value" and "saturation", i.e:

for pixel in im.getdata():
        hue, saturation, value = colorsys.hsv_to_rgb(pixel[0], pixel[1], pixel[2])
        if value < 32: # It's very dark
            blacks += 1
        elif saturation < 32 and value > 224: # it's basically white
            whites += 1
        else: # if it fails both of those, we can call it a colour
            hue_bucket = hue * NUM_BUCKETS // 255 # Using python3 to get an int
            colour_counts[hue_bucket] += 1

Black is characterized by a low value. White has a high value and a low saturation. Low saturation colours are grey. I generally find hsv to be way more comprehensible than rgb.

Upvotes: 0

Doyousketch2
Doyousketch2

Reputation: 2145

If it's only doing red & black, you can just check if the red value is less than or equal to 40.

if pixel[0] <= 40:

Edit:

colors = {}

for pixel in im.getdata():
    r = pixel[0]
    g = pixel[1]
    b = pixel[2]

    color = ''
    brightness = ''
    avg = (r + g + b) / 3

    if avg < 40 then brightness = 'black'
    else if avg < 80 then brightness = 'dark'
    else if avg > 220 then brightness = 'white'
    else if avg > 150 then brightness = 'light'

    if avg / r > 0.9 then hue = 'red'
    else if avg / r > 0.8 and avg / g > 0.6 then hue = 'orange'
    else if avg / r > 0.7 and avg / g > 0.7 then hue = 'yellow'
    else if avg / g > 0.8 and avg / r > 0.6 then hue = 'lime'
    else if avg / g > 0.9 then hue = 'green'
    else if avg / g > 0.7 and avg / b > 0.7 then hue = 'cyan'
    else if avg / b > 0.9 then hue = 'blue'
    else if avg / b > 0.8 and avg / r > 0.6 then hue = 'indigo'
    else if avg / b > 0.7 and avg / r > 0.7 then hue = 'magenta'

    color = brightness + hue
    if color not in colors:
        colors[color] = 1
    else
        colors[color] = colors[color] + 1

Upvotes: 0

Related Questions