With the help of crackanddie, I was able to improve my solution.
If anybody is looking to count pixel like me very fast, here is my new code :
def countColors(self, image):
start_time = time.time()
colors_Count = {}
for i in range(len(self.colors)):
tmp = cv.inRange(image, self.colors[i], self.colors[i])
count = cv.countNonZero(tmp)
colors_Count[str(self.colors[i])] = count
print(time.time() - start_time, "seconds")
return colors_Count
Thank you again crackanddie, I was so stuck ahah
Answer from Marie on Stack OverflowWith the help of crackanddie, I was able to improve my solution.
If anybody is looking to count pixel like me very fast, here is my new code :
def countColors(self, image):
start_time = time.time()
colors_Count = {}
for i in range(len(self.colors)):
tmp = cv.inRange(image, self.colors[i], self.colors[i])
count = cv.countNonZero(tmp)
colors_Count[str(self.colors[i])] = count
print(time.time() - start_time, "seconds")
return colors_Count
Thank you again crackanddie, I was so stuck ahah
PIL package has a function that counts all colors really fast. if you care only to count specific colors, just filter the response. As for run time, on small images both function (PIL and yours) are almost the same but on big images (I checked 1000,1000,3) PIL is a bit faster. The big advantage of using PIL is that there's no need to specify colors.
def count_colors_2(cv_img: np.array) -> list: # no need to give colors
from PIL import Image
start_time = time.time()
pil_image = Image.fromarray(cv_img)
colors_count_list = pil_image.getcolors()
print('count_colors_2 time elapsed: {:.10f}s'.format(time.time() - start_time))
for count, c_bgr in colors_count_list:
print('\tcolor {} appeared {} times'.format(c_bgr, count))
return colors_count_list
time and output check (yours is count_colors_1 and PIL is 2)
image dims (1000, 1000, 3)
count_colors_1 time elapsed: 0.0039894581s
color [0 0 0] appeared 500000 times
color [255 255 255] appeared 250000 times
color [ 0 0 255] appeared 250000 times
count_colors_2 time elapsed: 0.0029892921s
color (255, 255, 255) appeared 250000 times
color (0, 0, 0) appeared 500000 times
color (0, 0, 255) appeared 250000 times
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:

Reduced color version:

Basic color versioin:

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:

and end up with this:

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)
One possible solution (I encoded the PNG image in Base64):
from PIL import Image
from io import BytesIO
import base64
data = '''iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+ACCAAXMEa+fpcAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAx9JREFUeNrt3MtNA1EMQFGMpqWIgpAiakJKA6mBBrJOL2wfBYQVDrI9c85+8pnPlWfxXrxQbSWPD6eQv3p1CkAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEAurKWPC+1nv/z/S315efLzT2ICQAQAEAAAAEABAAQAEAAAAEABAAEABAAQAAAAQAEABAAYH+sxc4r3Q8g6+vju/T7r6e7e9gEAAgAIACAAAACAAgAIACAAAACAAgAIACAAAACAAgAIADAo236H1jJ45+wmDz1EefLbfR+ApgAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAD4F5tTcGzX0736J4SrYAIABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAOBB+X4AK3l85D+C2bdAtdH7GZgAwCsAIACAAAACAAgAIACAAAACAAgAIACAAAACAAgAIABAWx3WMq+yg12A2Rd/H9ev9BYwAYBXAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAgLaesRb50Ov5ofgBTH2ECQC8AgACAAgAIACAAAACAAgAIACAAAACAAgAIACAAAACALS1OQWMtop3lIgYffpMAOAVABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAoC37ATBbdj1+9X4CJgBAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAH5nPwCOLbufgAkAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAKBKNPgNq+xgqH+ASp9BEwB4BQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEAGgrdvAf7CdA5QMw+hkyAYBXAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAgLbCKbAlgGfABAAIACAAgAAAAgAIACAAgAAAAgAIACAAgAAAAgAIACAAQDs/+5skuT18qwIAAAAASUVORK5CYII='''
img = Image.open(BytesIO(base64.b64decode(data)))
unique_colors = set()
for i in range(img.size[0]):
for j in range(img.size[1]):
pixel = img.getpixel((i, j))
unique_colors.add(pixel)
print('Image info = ', img)
print('Unique color count = ', len(unique_colors))
Prints:
Image info = <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=256x256 at 0x7FFFF670A400>
Unique color count = 7
Is it possible to list the pixel RGBs and scan through that? As John Anderson said, you can use Image.getcolors to get that. But to get the number of unique variables, you can convert it to a set, which removes duplicates, then get the length of that, with len(set(list)).
The getcolors method should do the trick. See the docs.
Edit: That link is broken. Pillow seems to be the go-to lib now, forked from PIL. New Docs
Image.open('file.jpg').getcolors() => a list of (count, color) tuples or None
I have used something like the following a few times to analyze graphs:
>>> from PIL import Image
>>> im = Image.open('polar-bear-cub.jpg')
>>> from collections import defaultdict
>>> by_color = defaultdict(int)
>>> for pixel in im.getdata():
... by_color[pixel] += 1
>>> by_color
defaultdict(<type 'int'>, {(11, 24, 41): 8, (53, 52, 58): 8, (142, 147, 117): 1, (121, 111, 119): 1, (234, 228, 216): 4
Ie, there are 8 pixels with rbg value (11, 24, 41), and so on.
You need to use Numpy, or OpenCV, for fast image processing in Python. I made a 9-colour version of Paddington:

from PIL import Image
import numpy as np
# Open Paddington and make sure he is RGB - not palette
im = Image.open('paddington.png').convert('RGB')
# Make into Numpy array
na = np.array(im)
# Arrange all pixels into a tall column of 3 RGB values and find unique rows (colours)
colours, counts = np.unique(na.reshape(-1,3), axis=0, return_counts=1)
print(colours)
print(counts)
Results
[[ 14 48 84]
[ 19 21 30]
[ 33 108 163]
[ 33 152 190]
[ 72 58 58]
[ 96 154 210]
[180 89 64]
[205 210 200]
[208 151 99]]
[20389 40269 12820 1488 17185 25371 17050 16396 9032]
That means there are 20,389 pixels of RGB(14,48,84), and so on.
That takes 125ms on my Mac for a 400x400 image, which will give you 8 fps, so you better have at least 4 CPU cores and use all of them to get 25+ fps.
Update
I think you can actually go significantly faster than this. If you take the dot-product of each of the pixels with [1,256,65536], you will get a single 24-bit number for each pixel, rather than 3 8-bit numbers. It is then a lot faster to find the unique values. That looks like this:
# Open Paddington and make sure he is RGB - not palette
im = Image.open('paddington.png').convert('RGB')
# Make into Numpy array
na = np.array(im)
# Make a single 24-bit number for each pixel
f = np.dot(na.astype(np.uint32),[1,256,65536])
nColours = len(np.unique(f)) # prints 9
That takes 4ms rather than 125ms on my Mac :-)
Keywords: Python, Numpy, PIL/Pillow, image processing, count unique colours, count colors.
This function returns a pretty DataFrame with all the information you need and is lightning fast - 10 times faster than colours, counts = np.unique(...)
import numexpr # Import the mighty mighty 'numexpr' library for efficient numerical expression evaluation - up to 10x faster than numpy
import numpy as np
import pandas as pd
def count_colors(img, sort=True):
"""
Count the unique colors in an image and return a DataFrame with color counts.
Parameters:
img (numpy.ndarray): Input image as a NumPy array with shape (height, width, 3).
sort (bool, optional): Whether to sort the resulting DataFrame by color count. Default is True.
Returns:
pandas.DataFrame: A DataFrame containing color information with columns 'a' ,'r', 'g', 'b', and 'c' (count).
"""
r, g, b = (
img[..., 2],
img[..., 1],
img[..., 0],
) # Extract the red, green, and blue color channels from the image.
absnum = numexpr.evaluate(
# Use 'numexpr' to efficiently calculate a unique numerical representation for each color.
"(r << 16) + (g << 8) + b",
global_dict={},
local_dict={"r": r, "g": g, "b": b},
)
npunique = np.unique(
absnum, return_counts=True
) # Find unique values and their counts in the 'absnum' array.
return pd.merge( # Merge DataFrames to combine color information and color counts.
pd.DataFrame(
{
"r": r.ravel(),
"g": g.ravel(),
"b": b.ravel(),
"a": absnum.ravel(),
}
)
.drop_duplicates() # Remove duplicate rows based on the unique numerical representation 'a'.
.reset_index(drop=True) # Reset the DataFrame index after dropping duplicates.
.set_index("a"), # Set the 'a' column as the index of the DataFrame.
pd.DataFrame({"a": npunique[0], "c": npunique[1]}).set_index("a"),
# Create another DataFrame with 'a' and 'c' columns.
left_index=True, # Merge DataFrames using the index of the left DataFrame ('a' column).
right_index=True, # Merge DataFrames using the index of the right DataFrame ('a' column).
sort=sort, # Sort the resulting DataFrame by color count if 'sort' is True.
).reset_index(
drop=False
) # Reset the index of the merged DataFrame and return it.
# df
# Out[3]:
# a r g b c
# 0 929876 14 48 84 20389
# 1 1250590 19 21 30 40269
# 2 2190499 33 108 163 12820
# 3 2201790 33 152 190 1488
# 4 4733498 72 58 58 17185
# 5 6331090 96 154 210 25371
# 6 11819328 180 89 64 17050
# 7 13488840 205 210 200 16396
# 8 13670243 208 151 99 9032
# %timeit colours, counts = np.unique(pic.reshape(-1,3), axis=0, return_counts=1)
# %timeit df = count_colors(pic)
# 127 ms ± 3.81 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# 12.8 ms ± 130 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
You could do it with GIMP which is free, open source, and works on Mac, Windows, and Linux.
Open an image in GIMP, using File > Open
In the main menu click Windows > Dockable Dialogs > Histogram
Choose the Select by Color tool Shift+O, and set the Threshold to 0, in the Tool Options, under the Toolbox

- Click on a colour to select it, and the number of pixels will show in the Histogram. Repeat for other colours

Note: If your image is very small, you might want to zoom in. Use + and - to zoom in/out. Holding down Space while you move the mouse allows panning around the image. Also note that the grey and white checkerboard in the above example is to show the transparent background. If your image has a solid background you won't see these.
Drawing heavily on Billy Kerr's answer using Gimp, this is how you can achieve the same in Affinity Photo:
First ensure you have the histogram panel active and visible using the View > Studio > Histogram menu option. (You might need to set the Advanced option for the panel, using the small burger menu top right, in order to view the statistics associated with the histogram.) Ensure that the "Marquee" option is selected, to limit the scope to selected pixels.

Then use the Select > Select Sampled Colour... option. A small panel will be shown:

Click in your image to choose the colour to be sampled. There's a Threshold slider to determine how closely each pixel must match to be selected. Adjust the slider to include/exclude the pixels you need. Then click Apply. The histogram and statistics will be updated to reflect the selection:

In my example you can see 136 pixels were selected. If you see a small warning triangle top right of the histogram (as in my first example), click it to force a full refresh of the data.
Repeat for the other colours. It's slightly tiresome because you will need to use the Select > Select Sampled Colour... menu option each time.
I would suggest using a range of hues in HSV color space in Python/OpenCV to get a mask. Then count the number of non-zero values in the mask.
Input:

import cv2
import numpy as np
# load image
img = cv2.imread('blue_bag.png')
# convert to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)
# create mask for blue color in hsv
# blue is 240 in range 0 to 360, so for opencv it would be 120
lower = (100,100,100)
upper = (160,255,255)
mask = cv2.inRange(hsv, lower, upper)
# count non-zero pixels in mask
count=np.count_nonzero(mask)
print('count:', count)
# save output
cv2.imwrite('blue_bag_mask.png', mask)
# Display various images to see the steps
cv2.imshow('mask',mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
Mask:

count: 34231
You can use:
blue = sum(np.all(np.array(im.getdata()) == np.array([30,144,255]), axis=1))
Note that the blue value is more likely to be zero since there is a slight chance for the image to have an exact row that is equal to [30,144,255]