PrimuS
PrimuS

Reputation: 2683

Most dominant color in RGB image - OpenCV / NumPy / Python

I have a python image processing function, that uses tries to get the dominant color of an image. I make use of a function I found here https://github.com/tarikd/python-kmeans-dominant-colors/blob/master/utils.py

It works, but unfortunately I don't quite understand what it does and I learned that np.histogram is rather slow and I should use cv2.calcHist since it's 40x faster according to this: https://docs.opencv.org/trunk/d1/db7/tutorial_py_histogram_begins.html

I'd like to understand how I have to update the code to use cv2.calcHist, or better, which values I have to input.

My function

def centroid_histogram(clt):
    # grab the number of different clusters and create a histogram
    # based on the number of pixels assigned to each cluster
    num_labels = np.arange(0, len(np.unique(clt.labels_)) + 1)
    (hist, _) = np.histogram(clt.labels_, bins=num_labels)

    # normalize the histogram, such that it sums to one
    hist = hist.astype("float")
    hist /= hist.sum()

    # return the histogram
    return hist

The pprint of clt is this, not sure if this helps

KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
    n_clusters=1, n_init=10, n_jobs=1, precompute_distances='auto',
    random_state=None, tol=0.0001, verbose=0)

My code can be found here: https://github.com/primus852/python-movie-barcode

I am a very beginner, so any help is highly appreciated.

As per request:

Sample Image

Sample

Most dominant color:

rgb(22,28,37)

Computation time for the Histogram:

0.021515369415283203s

Upvotes: 24

Views: 32844

Answers (5)

Hans
Hans

Reputation: 536

Awesome answer from @Divakar

With bit shifting instead of multiplication, it might be even a little faster. It seems to have a bigger effect on large pictures with more colors

import numexpr as ne
def get_dom_color(x):
    def get_color_back(code):
        blue = int(code % 256)
        green = int((code % (256 * 256) - blue) / 256)
        red = int((code - blue - green * 256) / (256 * 256))
        return red, green, blue
    arr = x.reshape(-1,x.shape[-1])
    outa=ne.evaluate('(c1 << 16) + (c2 << 8) + c3',global_dict={},local_dict={'c1':arr[...,0],'c2':arr[...,1],'c3':arr[...,2]})
    return get_color_back(np.bincount(outa).argmax())
def bincount_numexpr_app(a):
    a2D = a.reshape(-1,a.shape[-1])
    col_range = (256, 256, 256) # generically : a2D.max(0)+1
    eval_params = {'a0':a2D[:,0],'a1':a2D[:,1],'a2':a2D[:,2],
                   's0':col_range[0],'s1':col_range[1]}
    a1D = ne.evaluate('a0*s0*s1+a1*s0+a2',eval_params)
    return np.unravel_index(np.bincount(a1D).argmax(), col_range)
np.random.seed(0)
x = np.random.randint(0,255,(4000,4000,3))
%timeit bincount_numexpr_app(x)
%timeit get_dom_color(x)
427 ms ± 14.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
412 ms ± 7.91 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Upvotes: 0

zoltron
zoltron

Reputation: 107

Improved version of @Divakar's answer. Separate solution for RGBA image. Fully transparent pixels will not be counted for the RGBA image.

import numpy as np


def most_common_color_RGB(image: np.ndarray):
    """input image ndarray shape should be RGB shape, for example: (512, 512, 3)"""
    a2D = image.reshape(-1, image.shape[-1])

    col_range = (256, 256, 256)  # generically : a2D.max(0)+1
    a1D = np.ravel_multi_index(a2D.T, col_range)
    return np.unravel_index(np.bincount(a1D).argmax(), col_range)


def most_common_color_RGBA(image_RGBA: np.ndarray):
    """input image ndarray shape should be RGBA shape, for example: (512, 512, 4)"""
    RGB_pixels = image_RGBA.reshape(-1, 4)
    # remove transparent pixels
    just_non_alpha = RGB_pixels[RGB_pixels[:, 3] != 0]
    if just_non_alpha.shape[0] == 0:
        return False
    # delete alpha channel
    just_non_alpha = np.delete(just_non_alpha, 3, axis=1)
    col_range = (256, 256, 256)  # generically : a2D.max(0)+1
    a1D = np.ravel_multi_index(just_non_alpha.T, col_range)
    return np.unravel_index(np.bincount(a1D).argmax(), col_range)

Upvotes: 0

Divakar
Divakar

Reputation: 221714

Two approaches using np.unique and np.bincount to get the most dominant color could be suggested. Also, in the linked page, it talks about bincount as a faster alternative, so that could be the way to go.

Approach #1

def unique_count_app(a):
    colors, count = np.unique(a.reshape(-1,a.shape[-1]), axis=0, return_counts=True)
    return colors[count.argmax()]

Approach #2

def bincount_app(a):
    a2D = a.reshape(-1,a.shape[-1])
    col_range = (256, 256, 256) # generically : a2D.max(0)+1
    a1D = np.ravel_multi_index(a2D.T, col_range)
    return np.unravel_index(np.bincount(a1D).argmax(), col_range)

Verification and timings on 1000 x 1000 color image in a dense range [0,9) for reproducible results -

In [28]: np.random.seed(0)
    ...: a = np.random.randint(0,9,(1000,1000,3))
    ...: 
    ...: print unique_count_app(a)
    ...: print bincount_app(a)
[4 7 2]
(4, 7, 2)

In [29]: %timeit unique_count_app(a)
1 loop, best of 3: 820 ms per loop

In [30]: %timeit bincount_app(a)
100 loops, best of 3: 11.7 ms per loop

Further boost

Further boost upon leveraging multi-core with numexpr module for large data -

import numexpr as ne

def bincount_numexpr_app(a):
    a2D = a.reshape(-1,a.shape[-1])
    col_range = (256, 256, 256) # generically : a2D.max(0)+1
    eval_params = {'a0':a2D[:,0],'a1':a2D[:,1],'a2':a2D[:,2],
                   's0':col_range[0],'s1':col_range[1]}
    a1D = ne.evaluate('a0*s0*s1+a1*s0+a2',eval_params)
    return np.unravel_index(np.bincount(a1D).argmax(), col_range)

Timings -

In [90]: np.random.seed(0)
    ...: a = np.random.randint(0,9,(1000,1000,3))

In [91]: %timeit unique_count_app(a)
    ...: %timeit bincount_app(a)
    ...: %timeit bincount_numexpr_app(a)
1 loop, best of 3: 843 ms per loop
100 loops, best of 3: 12 ms per loop
100 loops, best of 3: 8.94 ms per loop

Upvotes: 24

Timo
Timo

Reputation: 9835

The equivalent code for cv2.calcHist() is to replace:

(hist, _) = np.histogram(clt.labels_, bins=num_labels)  

with

dmin, dmax, _, _ = cv2.minMaxLoc(clt.labels_)

if np.issubdtype(data.dtype, 'float'): dmax += np.finfo(data.dtype).eps
else: dmax += 1

hist = cv2.calcHist([clt.labels_], [0], None, [num_labels], [dmin, dmax]).flatten()

Note that cv2.calcHist only accepts uint8 and float32 as element type.

Update

It seems like opencv's and numpy's binning differs from each other as the histograms differ if the number of bins doesn't map the value range:

import numpy as np
from matplotlib import pyplot as plt
import cv2

#data = np.random.normal(128, 1, (100, 100)).astype('float32')
data = np.random.randint(0, 256, (100, 100), 'uint8')
BINS = 20

np_hist, _ = np.histogram(data, bins=BINS)

dmin, dmax, _, _ = cv2.minMaxLoc(data)
if np.issubdtype(data.dtype, 'float'): dmax += np.finfo(data.dtype).eps
else: dmax += 1

cv_hist = cv2.calcHist([data], [0], None, [BINS], [dmin, dmax]).flatten()

plt.plot(np_hist, '-', label='numpy')
plt.plot(cv_hist, '-', label='opencv')
plt.gcf().set_size_inches(15, 7)
plt.legend()
plt.show()

Upvotes: 4

zindarod
zindarod

Reputation: 6468

@Divakar has given a great answer. But if you want to port your own code to OpenCV, then:

    img = cv2.imread('image.jpg',cv2.IMREAD_UNCHANGED)

    data = np.reshape(img, (-1,3))
    print(data.shape)
    data = np.float32(data)

    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    flags = cv2.KMEANS_RANDOM_CENTERS
    compactness,labels,centers = cv2.kmeans(data,1,None,criteria,10,flags)

    print('Dominant color is: bgr({})'.format(centers[0].astype(np.int32)))

Result for your image:

Dominant color is: bgr([41 31 23])

Time it took: 0.10798478126525879 secs

Upvotes: 13

Related Questions