HSV Dominant Colors

The purpose of this article is to show how to find dominant colors in an image by clustering its colors in the HSV space.


This post has been inspired by the excellent work of Charles Leifer you can read here.


Beside the usual tools such as OpenCV, matplotlib and numpy, this tutorial is going to make use of Scipy's kMeans implementation in order to clusterize the image pixels.
%matplotlib inline
import cv2
from scipy.cluster.vq import vq, kmeans
from matplotlib import pyplot as plt
import numpy as np
import time as t

Hue, Saturation and Value Color Space

One of the most common spaces in which the colors of an image are represented is RGB, a three dimensional cubic discrete space whose principal axes are Red, Green and Blue amount of color. This is the most reasonable way of representing colors for a computer, but unfortunately it does not preserve the way humans perceive colors. On the other hand, HSV color space is a half-cone in which the perceptive distance is preserved. An intuition is given in the following figure, while a comprehensive description can be found on Wikipedia.

HSV Histograms

Let's load an image and plot its HSV Histograms.
target_image = 'images/lenna.png'
img = cv2.imread(target_image)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
We can convert the image by using cv2.cvtColor() with the flag cv2.COLOR_RGB2HSV
hsv_image = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
We can plot the histograms by selecting one column at the time from the image matrix representation.
hue, sat, val = hsv_image[:,:,0], hsv_image[:,:,1], hsv_image[:,:,2]
We set up the figure to be 10x8 inches, with three plots inside by calling the method plt.subplot(311) . The code 311 stands for "make a 3 rows, 1 column figure; plot in the first cell of the figure".
plt.subplot(311)                             #plot in the first cell
plt.hist(np.ndarray.flatten(hue), bins=180)
plt.subplot(312)                             #plot in the second cell
plt.hist(np.ndarray.flatten(sat), bins=128)
plt.subplot(313)                             #plot in the third cell
plt.title("Luminosity Value")
plt.hist(np.ndarray.flatten(val), bins=128)


The following procedure performs the clustering operation via kmeans and returns an image representing the found clusters. For further detail, please refer to comments in the body of the function.
def do_cluster(hsv_image, K, channels):
    # gets height, width and the number of channes from the image shape
    h,w,c = hsv_image.shape
    # prepares data for clustering by reshaping the image matrix into a (h*w) x c matrix of pixels
    cluster_data = hsv_image.reshape( (h*w,c) )
    # grabs the initial time
    t0 = t.time()
    # performs clustering
    codebook, distortion = kmeans(np.array(cluster_data[:,0:channels], dtype=np.float), K)
    # takes the final time
    t1 = t.time()
    print "Clusterization took %0.5f seconds" % (t1-t0)
    # calculates the total amount of pixels
    tot_pixels = h*w
    # generates clusters
    data, dist = vq(cluster_data[:,0:channels], codebook)
    # calculates the number of elements for each cluster
    weights = [len(data[data == i]) for i in range(0,K)]
    # creates a 4 column matrix in which the first element is the weight and the other three
    # represent the h, s and v values for each cluster
    color_rank = np.column_stack((weights, codebook))
    # sorts by cluster weight
    color_rank = color_rank[np.argsort(color_rank[:,0])]

    # creates a new blank image
    new_image =  np.array([0,0,255], dtype=np.uint8) * np.ones( (500, 500, 3), dtype=np.uint8)
    img_height = new_image.shape[0]
    img_width  = new_image.shape[1]

    # for each cluster
    for i,c in enumerate(color_rank[::-1]):
        # gets the weight of the cluster
        weight = c[0]
        # calculates the height and width of the bins
        height = int(weight/float(tot_pixels) *img_height )
        width = img_width/len(color_rank)

        # calculates the position of the bin
        x_pos = i*width

        # defines a color so that if less than three channels have been used
        # for clustering, the color has average saturation and luminosity value
        color = np.array( [0,128,200], dtype=np.uint8)
        # substitutes the known HSV components in the default color
        for j in range(len(c[1:])):
            color[j] = c[j+1]
        # draws the bin to the image
        new_image[ img_height-height:img_height, x_pos:x_pos+width] = [color[0], color[1], color[2]]
    # returns the cluster representation
    return new_image
# creates a new figure size 12x10 inches
# creates a 4-column subplot
# in the first cell draws the target image

# calculates clusters for
# * h
# * h and s
# * h, s and v

for i in range(1,4):
    plt.subplot(141 + i)
    plt.title("Channels: %i" % i)
    new_image = do_cluster(hsv_image, 5, i)
    new_image = cv2.cvtColor(new_image, cv2.COLOR_HSV2RGB)
Clusterization took 0.76545 seconds
Clusterization took 4.80713 seconds
Clusterization took 8.77994 seconds

Comparison with RGB

I would like to compare the results of this HSV implementation with RGB clustering proposed in artilce.

Test 1

Consider the following image
Akira motorcycles
The RGB dominant colors result

While in HSV the dominant colors result
Clusterization took 3.60024 seconds

Test 2

Consider the following image
Akira 3
The RGB dominant colors result
While in HSV the dominant colors result
Clusterization took 1.86138 seconds

Final Thoughts

There is a very non-surprisingly slight difference from the chromatic point of view but what makes my mind grind, on the other hand, is the different distribution of the colors in the clusters. Any ideas? Feel free to share your thoughts in the comment box below. Thank you for reading this article. Don't forget to take a look the RGB implementation by Charles Leifer