How to takeout the average colour of a screnshot taken using OpenCV?

Question:

I am trying to develop a device that changes the RGB led strips according to the colour of my display. To this, I am planning on screenshotting the screen and normalising/taking the mean of the colours of individual pixels in the display. I am having trouble normalising the image and taking out the average colour of the image. Here’s the code I am using.

import numpy as np
import cv2
import mss
import time  


def getAverageColor(frame):
    (B, G, R) = 0, 0, 0
    for i in frame:
        for j in i:
            B += j[0]
            G += j[1]
            R += j[2]
    B /= len(frame) * len(frame[0])
    G /= len(frame) * len(frame[0])
    R /= len(frame) * len(frame[0])
    return (B, G, R)



with mss.mss() as sct:
    # Grab frames in an endless lopp until q key is pressed
        time.sleep(2)
        # Iterate the list of monitors, and grab one frame from each monitor (ignore index 0)
        for monitor_number, mon in enumerate(sct.monitors[1:]):
            monitor = {"top": mon["top"], "left": mon["left"], "width": mon["width"], "height": mon["height"], "mon": monitor_number}  # Not used in the example

            # Grab the data
            img = np.array(sct.grab(mon)) # BGRA Image (the format BGRA, at leat in Wiqndows 10).
            print(getAverageColor(img))
            # Show down-scaled image for testing
            # The window name is img0, img1... applying different monitors.
            cv2.imshow(f'img{monitor_number}', cv2.resize(img, (img.shape[1]//4, img.shape[0]//4)))
            key = cv2.waitKey(1)
            if key == ord('q'):
                break

cv2.destroyAllWindows()

The program works fine but I would like to ask if there is any way o take out the average colour in openCV itself as my method is not very well recommended as it can be very slow in processing. Not to add this but the code is not very accurate as well.

Asked By: Tanay Upreti

||

Answers:

I have updated the function getAverageColor, idea is to find the dominant color.
I believe this gives better color options. Time is an issue, I will update in case I could find a way to make it faster.

import pandas as pd
from scipy.cluster.vq import whiten
from scipy.cluster.vq import kmeans
# import matplotlib.pyplot as plt


def getAverageColor(frame):
  r = []
  g = []
  b = []
  print("Frames")
  for row in frame:
      for temp_r, temp_g, temp_b, temp in row:
          r.append(temp_r)
          g.append(temp_g)
          b.append(temp_b)
  
  df = pd.DataFrame({'r' : r, 'g' : g, 'b' : b})
    
  df['scaled_r'] = whiten(df['r'])
  df['scaled_b'] = whiten(df['b'])
  df['scaled_g'] = whiten(df['g'])
    
  cluster_centers, _ = kmeans(df[['scaled_r', 'scaled_b', 'scaled_g']], 3)
    
  dominant_colors = []
    
  r_std, g_std, b_std = df[['r', 'g', 'b']].std()
    
  for cluster_center in cluster_centers:
      red_scaled, green_scaled, blue_scaled = cluster_center
      dominant_colors.append((red_scaled * r_std / 255, green_scaled * g_std / 255, blue_scaled * b_std / 255))
  
  print("Dominant", dominant_colors)
  return(dominant_colors[0])
  # plt.imshow([dominant_colors])
  # plt.show()
Answered By: Cryptc_Slaughtr

Image processing with lists and for loops is inefficient, slow, and error-prone in Python. try to use Numpy, or a vectorised library such as OpenCV, scikit-image, wand or PIL/Pillow.

Make a sample gradient image image from lime green to yellow, i.e. with no blue, solid green and a gradient in red which will give us simple, easily checked means:

magick -size 256x256 gradient:lime-yellow png24:start.png

enter image description here

Now get means of all three channels with Numpy:

import cv2
import numpy as np

# Load image
im = cv2.imread('start.png')

print(im.shape)       # prints (256, 256, 3)

meanBlue  = np.mean(im[...,0])     # gets value=0
meanGreen = np.mean(im[...,1])     # gets value=255.0
meanRed   = np.mean(im[...,2])     # gets value=127.5
Answered By: Mark Setchell

I want to answer this question with a simple solution I have come up with. This might not be the best solution to this problem but it works fast enough for my use case. If anyone can improve my solution it’ll be more than appreciated.

 def getAverageColor(frame):
        r = []
        g = []
        b = []
        print("Frames")
        for row in frame:
            for pixel in row:
                r.append(pixel[0])
                g.append(pixel[1])
                b.append(pixel[2])

        r_mean = np.mean(r)
        g_mean = np.mean(g)
        b_mean = np.mean(b)
    
        return(r_mean, g_mean, b_mean)

This takes out the mean of all the pixels and is not like the previously defined solution that just returns an average colour which is just a shade of brown in 90% of cases.

Answered By: Tanay Upreti