2

I need to find the average color of a specified part of the image. Specifically I need to know If the color in there is red, green, brown, black or blue.

The images are 640x640, and the region I need to analyze is the rectangle between pixel x1=20 y1=600 and pixel x2=620 y2=640. (where x0y0 is top left corner)

I found several examples, like this one How to find the average colour of an image in Python with OpenCV? but they all deal with the whole image.

How can I get the average color of only apecified area?

A must is that it has to be as quick as possible, below 5 ms.

my aproach would be to go over each pixel in the range and do the maths, but I have the feeling that open CV or similar libraries already can do this.

Any help appreciated.

sharkyenergy
  • 3,842
  • 10
  • 46
  • 97

2 Answers2

3

As your region of interest (ROI) is only a simple rectangle, I think you just want to use Numpy slicing to identify it.

So, I have made a test image that is green where you want to measure:

enter image description here

Then the code would go like this:

import cv2
import numpy as np

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

# Calculate mean of green area
A = np.mean(im[600:640, 20:620], axis=(0,1))

That gets green, unsurprisingly:

array([  0., 255.,   0.])

Now include some of the black area above the green to reduce the mean "greenness"

B = np.mean(im[500:640, 20:620], axis=(0,1))

That gives... "a bit less green":

aarray([ 0.        , 72.85714286,  0.        ])

The full sampling of every pixel in the green area takes 214 microsecs on my Mac, as follows:

IIn [5]: %timeit A = np.mean(im[600:640, 20:620], axis=(0,1))
214 µs ± 150 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Note that you could almost certainly sample every 4th pixel down and every 4th pixel across as follows in 50.6 microseconds and still get a very indicative result:

In [11]: %timeit A = np.mean(im[500:640:4, 20:620:4], axis=(0,1))
50.6 µs ± 29.3 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

You can make every pixel you are sampling into a red dot like this - look carefully:

im[600:640:4, 20:620:4] = [255,0,0]

enter image description here


As suggested by Fred (@fmw42), it is even faster if you replace np.mean() with cv2.mean():

So, 11.4 microseconds with cv2.mean() versus 214 microseconds with np.mean():

In [22]: %timeit cv2.mean(im[600:640, 20:620])
11.4 µs ± 11.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

And 7.85 microseconds with cv2.mean() versus 50.6 microseconds with np.mean() if sampling every 4th pixel:

In [23]: %timeit cv2.mean(im[600:640:4, 20:620:4])
7.85 µs ± 6.42 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • What is the speed difference when using cv2.mean() rather than np.mean()? – fmw42 May 12 '22 at 17:04
  • One could resize the image down using INTER_AREA to one pixel and then just read the pixel value. Perhaps OpenCV CUDA can do that very fast? – fmw42 May 12 '22 at 17:24
  • @fmw42 Yes, miles faster with `cv2.mean()` – Mark Setchell May 12 '22 at 17:33
  • @fmw42 I'll leave you to try your CUDA suggestion on your new Mac – Mark Setchell May 12 '22 at 17:34
  • You wrote "So, 11.4 microseconds with cv2.mean() versus 214 microseconds with `cv2.mean()`:". Did you mean np.mean() for the 214 rather than cv2.mean()? – fmw42 May 12 '22 at 17:38
  • What about the resize without CUDA? Would that be faster than cv2.mean()? – fmw42 May 12 '22 at 17:38
  • @fmw42 No, that seems slower at 25 microseconds if I do `cv2.resize(im[600:640, 20:620], (1,1), interpolation=cv2.INTER_AREA)` – Mark Setchell May 12 '22 at 17:45
  • Thanks. Very interesting. cv2.resize is faster than Numpy. But I thought that might be the case. But slower than cv2.mean. Nice study of efficiency! – fmw42 May 12 '22 at 17:47
  • 1
    @MarkSetchell sorry for the late reply, got covid. This looks reallly good. Now i could simply convert the RGB values to HSL to get the hue and define a range for every color... – sharkyenergy May 16 '22 at 04:51
2

Here is one way to do that in Python/OpenCV/Numpy.

  • Read the input
  • Threshold on color of region using cv2.inRange()
  • Optionally, apply morphology to clean up small regions
  • Optionally get the largest contour and draw it as white filled on a black background as final mask
  • Compute BGR component mean values for region using mask
  • Define select test colors with color names as Numpy arrays
  • Define an array of the color arrays
  • Loop over each array in the array of arrays and separate out the BGR color components of the colors
  • Compute RMSE between mean color and test color
  • Search for the minimum RMSE and its corresponding color name
  • Print the results

Input:

enter image description here

Test for Yellow Region:

import cv2
import numpy as np
import math

# load image
img = cv2.imread("sailboat.png")
hh, ww = img.shape[:2]

# threshold
lower = (0,200,200)
upper = (50,255,255)
thresh = cv2.inRange(img, lower, upper)

# apply open morphology
#kernel = np.ones((5,5), np.uint8)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
morph = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)

# get bounding box coordinates from largest external contour
contours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)

# draw white filled contour on black background
mask = np.zeros((hh,ww), dtype=np.uint8)
cv2.drawContours(mask, [big_contour], 0, 255, cv2.FILLED)

# compute mean color for region
mean = cv2.mean(img, mask=mask)[0:3]
blue_mean = mean[0]
green_mean = mean[1]
red_mean = mean[2]
print("region_mean_color:", mean)
print("")

# define colors
red = np.array(["red",(0,0,255)],dtype=object)
green = np.array(["green",(0,255,0)],dtype=object)
brown = np.array(["brown",(20,70,140)],dtype=object)
black = np.array(["black",(0,0,0)],dtype=object)
blue = np.array(["blue",(255,0,0)],dtype=object)
yellow = np.array(["yellow",(0,255,255)],dtype=object)

min_rmse = 1000000
colors = np.array([red, green, brown, black, blue, yellow])
print("colorname", "rmse")
for color in colors:
    bb = color[1][0]
    gg = color[1][1]
    rr = color[1][2]
    rmse = math.sqrt( ( (red_mean-rr)*(red_mean-rr) + (green_mean-gg)*(green_mean-gg) + (blue_mean-bb)*(blue_mean-bb) )/3 )
    colorname = color[0]
    print(colorname,rmse)
    if rmse < min_rmse:
        min_rmse = rmse
        match_color = color[0]
print("")
print("match_color:", match_color)
print("rmse:", min_rmse)

# write result to disk
cv2.imwrite("sailboat_thresh.jpg", thresh)
cv2.imwrite("sailboat_morph.jpg", morph)
cv2.imwrite("sailboat_mask.jpg", mask)

# display results
cv2.imshow("THRESH", thresh)
cv2.imshow("MORPH", morph)
cv2.imshow("MASK", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()

Mask Image:

enter image description here

Textual Information:

region_mean_color: (0.0, 254.65158457426497, 254.65158457426497)

colorname rmse
red 147.02329851590747
green 147.02329851590747
brown 126.01745055239607
black 207.92214813271502
blue 254.7677759924176
yellow 0.2844800038551275

match_color: yellow
rmse: 0.2844800038551275
fmw42
  • 46,825
  • 10
  • 62
  • 80
  • Love the sample image – Mark Setchell May 12 '22 at 16:21
  • @MarkSetchell Is there a more efficient way of doing this? – fmw42 May 12 '22 at 16:39
  • I don't think so, you are bang in the money, as usual, AFAIK. My interpretation of the question is different though... I understood OP simply wants the mean of a rectangular region of interest. I assume you understood he wants the mean of a region of a specified colour and your answer does that perfectly. So, I guess we'll have to wait and see what OP actually wanted. You already have my vote – Mark Setchell May 12 '22 at 16:54
  • @MarkSetchell You are correct. I overlooked his specification for the region. I see now that he wants a rectangle. Feel free to post a solution for that. – fmw42 May 12 '22 at 17:00
  • thank you for your answer, sorry for the late reply i got covid. yes as @MarkSetchell pointed out I actually need the mean of a specified region, non the less your answer is extremly interesting and a good example for a newbie like me so please leave it here. +1 from me too.. – sharkyenergy May 16 '22 at 04:55