4

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 :)

MiniMiloe
  • 77
  • 1
  • 5
  • What are you actually trying to do? There might be 16 million colours in an image... – Mark Setchell May 26 '18 at 17:17
  • I want it to look at a range of options, for example when looking for black it looks for pixels between (0, 0 ,0) and (40, 40, 40). It may be too complicated to do in python im not sure – MiniMiloe May 26 '18 at 17:20
  • It can be done in Python, I just wanted to understand what you plan to do with the list. Are there 32 distinct colours that interest you and you want to know which of them are present, for example? Or do you want to find the statistical variations in your image? Or are you trying to tell if some tone is present? – Mark Setchell May 26 '18 at 17:24
  • There will be a couple groups of data with example data already in it, the data that is collected from that point on will be sorted into the correct group depending on what its most similar to. Using it for machine learning. It will be on a very basic level for example guessing the fruit – MiniMiloe May 26 '18 at 17:28
  • So you want to map every pixel in each new image to the nearest colour in some pre-existing *"reference"* image? Or at least calculate the distance to the nearest pre-existing colour? – Mark Setchell May 26 '18 at 17:33
  • Exactly, sorry I might have been wording it weirdly – MiniMiloe May 26 '18 at 17:36
  • Have a look at this. You would put your new image in `colorwheel.png` and your reference image in `colormap.png`. Is that close to what you want? https://stackoverflow.com/a/38328879/2836621 – Mark Setchell May 26 '18 at 17:42
  • Oh I just realised thats not in python is it – MiniMiloe May 26 '18 at 17:46
  • It can be done in Python... I am just trying to understand what you want so that folks don't go spending hours to write code and calculate the number of pixels and when they say "There are 34,879", you don't then say "Great! How can I now find the nearest to each in this other image please?". It happens often :-) – Mark Setchell May 26 '18 at 17:51
  • Yes that's exactly how I wanted it, should I move this discussion to a chat? – MiniMiloe May 26 '18 at 17:52
  • No, it's useful for others to clearly understand. When you compare a new image with your reference image, is there some scope for it causing a new reference colour to become introduced to your reference colours? Or should all pixels just be forced to the nearest existing colour even if it's miles away? – Mark Setchell May 26 '18 at 17:55
  • I don't think there would be any need for that as there is a fairly limited amount of colours a fruit can be, if that makes sense? Changing a photo into one that looks like the second one on stackoverflow.com/a/38328879/2836621 would be perfect really – MiniMiloe May 26 '18 at 18:00
  • Have a look at this too... https://stackoverflow.com/a/50496934/2836621 – Mark Setchell May 26 '18 at 18:05
  • Don't quite get that one, isn't that just making a bar with different colours on it? – MiniMiloe May 26 '18 at 18:10
  • Anyway to mix matlab and python or something, that matlab example would have been perfect – MiniMiloe May 26 '18 at 18:31

4 Answers4

2

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

Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
1

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)
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
0

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
Doyousketch2
  • 2,060
  • 1
  • 11
  • 11
0

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.

Aaron Krajeski
  • 757
  • 5
  • 18