Omega
Omega

Reputation: 871

Python - finding the most used colour in an image - script seems to find inaccurate colours

I wrote a script that finds the top 3 most common colours used in a jpg file. It works for the most part, but I noticed that I have some outliers.

The image in question is this one: https://public.tableau.com/s/sites/default/files/media/votd_02_08_13_snip.png

I have downloaded and saved it as jpg.

My script detects the following colours, which are completely off:

  1. 255, 176, 213
  2. 197, 255, 255
  3. 174, 187, 120

My script:

from PIL import Image
from collections import Counter

def most_frequent_colour2(img):
    width, height = img.size

    r_total = []
    g_total = []
    b_total = []

    for x in range(0, width):
        for y in range(0, height):

            # Define r, g, b for the rgb colour space and fetch the RGB colour for each pixel
            r, g, b = img.getpixel((x,y))

            r_total.append(r)
            g_total.append(g)
            b_total.append(b)       

    # Count the most common colours
    r_dict, g_dict, b_dict = Counter(r_total), Counter(g_total), Counter(b_total)

    #pick the top 3 most frequent colours
    return r_dict.most_common(3), g_dict.most_common(3), b_dict.most_common(3)

file = r"Filepath\https://public.tableau.com/s/sites/default/files/media/votd_02_08_13_snip.jpg"
img = Image.open(file)
img = img.convert('RGB')
colour = most_frequent_colour2(img)

# colour has the following format: 
#([(Most frequent R value, number of occurences), (2nd most frequent R value, number of occurences)],
# [(Most frequent G value, number of occurences), (2nd most frequent G value, number of occurences)]
# [(Most frequent B value, number of occurences), (2nd most frequent B value, number of occurences)])
print(colour)

The script works really well for this image: https://public.tableau.com/s/sites/default/files/media/tourdefrance1.jpg where it returns 85, 85, 85 as dominant colour.

Could the issue be the conversion from png to jpg?

Upvotes: 0

Views: 1067

Answers (4)

user10637953
user10637953

Reputation: 353

Here is the example pixel-array program I promised. It is grounded in the popular pygame package. I work with the Windows operating system, so if you work with some other OS, you will need to comment or strip out the Window-specific commands. Pixel Arrays are native to Pygame, so this program should work on other systems that are pygame compatible once the windows stuff is switched off. Windows can mess with the unsuspecting coder unless the quirks are dealt with, so forgive me those lines of Windows code if you use a well behaved OS. I tried to keep the Windows stuff at the top of the program.

from os import system
import ctypes
from time import sleep
import pygame
from pygame import Color
pygame.init()

system('cls') # Windows-specific command to clear the Command Console Window.

# Windows can play weird tricks related to scaling when you try to position or show images with pygame.
# The following will disable the Windows Auto-scaling feature.
# Query DPI Awareness (Windows 10 and 8)
awareness = ctypes.c_int()
errorCode = ctypes.windll.shcore.GetProcessDpiAwareness(0, ctypes.byref(awareness))
# Set DPI Awareness  (Windows 10 and 8)
errorCode = ctypes.windll.shcore.SetProcessDpiAwareness(2) # the argument is the awareness level, which can be 0, 1 or 2:
                                                       # (for 1-to-1 pixel control it seems to need a non-zero value.)

pygame.mouse.set_visible(False) # Hide the mouse

# Use a Rectangle object to find your system's maximum screen size.
screen_rec = pygame.display.set_mode((0,0),pygame.FULLSCREEN,32).get_rect()

# Create a light blue screen and show it.
screen = pygame.display.set_mode(screen_rec.size,pygame.FULLSCREEN,32)
screen.fill(Color("CornflowerBlue"))
pygame.display.flip()
sleep(2) # Pause for effect.

# Now let's create an example image using a pygame surface.
# The image will be our stand-in for an image loaded from a .bmp or .png
# file from disk. (You can also think of the image as a sprite.) We will 
# display the image on the screen, but we will work on the image surface
# using a pixel array. The image could be created using a pixelarray too,
# but pygame's draw methods are much faster so we will create the image
# using those. Then, once the image is created, we will use
# a pixel array to sample the colors and then to modify that image.

image = pygame.Surface((300,100))
image.fill(Color("IndianRed"))
image_rec = image.get_rect()  # Pygame Rectangle Objects are So useful!

pygame.draw.rect(image,Color("GhostWhite"),image_rec,7)
pygame.draw.line(image,Color("GhostWhite"),image_rec.topleft,image_rec.bottomright,4)

image_rec.center = screen_rec.center
screen.blit(image,image_rec)
pygame.display.update()
sleep(2) # Another dramatic pause!

# We now have an example image in 'image' which we will pretent was
# loaded from an image file. We want to sample the colors using 
# a pixel array.

''' ================================================================='''
#  The key thing to remember about pixel arrays is that the image is
#  locked out when the array is created, and is only releasod when
#  the array is deleted. You are working on the image at the pixel
#  level through the array and will not be able to do anything else
#  with the image while the pixel array exists.
''' ================================================================='''
my_pixel_array = pygame.PixelArray(image)  # The image is locked out.

color_log = {}  # Create a dictionary object. It will contain each unique
                # color and the pixel count having that color. 

for row in my_pixel_array:
    for pix in row:
        a,r,g,b = Color(pix) # ignore 'a', the 'alpha' value. It is always '0'
        color = (r,g,b)      # Create a new color tuple for our log. (We do this only for the sake of esthetics in the final output to the CCW.) 

        cnt = color_log.setdefault(color,0) # If the color is not in our log, add it, with a count value of 0. Always returns the count value. 
        color_log[color] = cnt+1 # Add one to the pixel count for that color


# Finished! But before we exit pygame and show the final counts,
# let's see how the pixel array can be used to change individual pixels.
# We could draw images or set random pixels to random colors, but let's
# change all of the GhostWhite pixels to black, then move the image
# across the screen.

for row in range(len(my_pixel_array)):
    for pix in range(len(my_pixel_array[row])):
        a,r,g,b = Color(my_pixel_array[row][pix])
        color = (r,g,b)
        if color == Color("GhostWhite"):
            my_pixel_array[row][pix] = Color("BLACK")

del my_pixel_array  # <-- THIS releases our image back to us!

screen.fill(Color("CornflowerBlue")) # Start with a fresh screen.

for c in range(-100,screen_rec.height+100,2): # Let's move image diagonally
    pygame.draw.rect(screen,Color("CornflowerBlue"),image_rec,0) # Erase previous image.
    image_rec.topleft = (c,c)    # Use rectangle to move the image.
    screen.blit(image,image_rec) # Draw image in new location.
    pygame.display.update()      # Show image in new location.

pygame.quit()

print("\n\n Finished.\n\n Here is the final tally:\n")

# Now show the results of our earlier pixel count and exit program.
for key,value in color_log.items():
    print(" Color: "+str(key).ljust(20," "),"Number of Pixels with that color:",value)


input("\n Press [Enter] to Quit: ")        
print("\n\n ",end="")
exit()

A final quirk you should know about. Sometimes if you have some coding error within the block that is using the pixel array, the block may abort out with your original image still locked. Then, the next time you try to display or use your image you will get a baffling error message; something like "You cannot display a locked surface." Go back and check your code dealing with the pixel array. This is likely what happened.

I hope you find my example interesting. Happy Coding!!

-Science_1

Upvotes: 1

Paul M.
Paul M.

Reputation: 10809

Incidentally, PIL has a method for counting unique colors in an image.

from PIL import Image

image = Image.open("votd_02_08_13_snip.png")

most_common = sorted(image.getcolors(maxcolors=2**16), key=lambda t: t[0], reverse=True)[0:3]
print(most_common)

Output:

[(7488, (197, 176, 213, 255)), (4320, (255, 255, 255, 255)), (3488, (255, 187, 120, 255))]

PIL.Image.getcolors returns a list of tuples, where the first element is the count, and the second element is a tuple containing band / color channel information (RGBA in this case). The list is unsorted, which is why we sort it based on the first element (the count) of each tuple. The sorting is done in descending order, so that the items with the highest count come first. I arbitrarily sliced the resulting list to yield the three most common colors.

EDIT - forgot to mention, PIL.Image.getcolors takes an optional keyword argument maxcolors, which is 256 by default. This is the value for the maximum number of unique colors to consider. If your image contains more unique colors than this value, PIL.Image.getcolors will return None. Since this particular image has a bitdepth of 32, my first impulse was to pass 2**32 as maxcolors, but this resulted in a OverflowError on my system. The average image won't contain anywhere near the number of colors that its bitdepth could represent anyways, so I just halved that number (again, arbitrarily).

Time shouldn't really be a concern, unless you have a huge image I guess:

from timeit import Timer

setup_method_1 = """
from PIL import Image
image = Image.open("votd_02_08_13_snip.png")
def get_most_common(image):
    most_common = sorted(image.getcolors(maxcolors=2**16), key=lambda t: t[0], reverse=True)[0:3]
    return most_common
"""

setup_method_2 = """
from PIL import Image
from collections import Counter
image = Image.open("votd_02_08_13_snip.png")
def get_most_common(image):
    width, height = image.size

    rgb_total = []

    for x in range(0, width):
        for y in range(0, height):

            r, g, b, _ = image.getpixel((x, y))
            rgb_total.append(f"r:{r}g:{g}b:{b}")
    rgb_dict = Counter(rgb_total)
    return rgb_dict.most_common(3)
"""

time_method_1 = min(Timer("get_most_common(image)", setup=setup_method_1).repeat(3, 10))
time_method_2 = min(Timer("get_most_common(image)", setup=setup_method_2).repeat(3, 10))

print(time_method_1)
print(time_method_2)

Output:

0.016010588999999964
0.6741786089999999
>>> 

Alternative method I just came up with using collections.Counter:

from timeit import Timer

setup = """

from PIL import Image
from collections import Counter

image = Image.open("votd_02_08_13_snip.png")

def get_most_common(image):
    return Counter(image.getdata()).most_common(3)

"""

time = min(Timer("get_most_common(image)", setup=setup).repeat(3, 10))
print(time)

Output:

0.05364039000000004

Upvotes: 2

BrendanOtherwhyz
BrendanOtherwhyz

Reputation: 481

I think the problem is more likely with the structure of you program. You should be appending the rgb values together not separately. The most common values you are producing are for each colour group, not the colours themselves.

def most_frequent_colour2(img):
width, height = img.size

rgb_total = []

for x in range(0, width):
    for y in range(0, height):

        # Define r, g, b for the rgb colour space and fetch the RGB colour for each pixel
        r, g, b = img.getpixel((x,y))

        rgb_total.append("r:"+str(r)+"g:"+str(g)+"b:"+str(b))     

# Count the most common colours
rgb_dict = Counter(rgb_total)

#pick the top 3 most frequent colours
return rgb_dict.most_common(3)
#[('r:197g:176b:213', 7488), ('r:255g:255b:255', 4320), ('r:255g:187b:120', 3488)]

Upvotes: 3

user10637953
user10637953

Reputation: 353

An alternate method if the images are small or if you are not concerned about speed might be to use a pixel array. It is my understanding that the colors discovered by Python are sampled approximations of the colors only; but pixel arrays have worked well for me in the past and I haven't run into inaccuracy issues with the colors returned. Pixel arrays have a couple of quirks to be aware of when you use them. Let me know if you are interested and need a working example. I could whip something up.

Upvotes: 0

Related Questions