How to detect color of balls in the given image using python-opencv?
-
that depends on how specific "color" is for your task. if it is **exactly** the same RGB value, one option would be [converting image to an array](https://stackoverflow.com/questions/7762948/how-to-convert-an-rgb-image-to-numpy-array) and create a set of colors, sorting by each pixel RGB value – jsofri Nov 14 '21 at 08:27
1 Answers
Introduction
I will dismantle the question in the following three sections
- Obtain the English name of a color from a RGB or Hex value
- Locate the circles on the image
- Obtain the English name on per circle
Obtain color name from RGB or Hex
Using the following answer:
We are almost done, except for the small change that cv2 uses BGR instead of RGB, therefore we take RGB[2] (the blue channel) to match the red channel of the webcolors.
def color_rgb_to_name(rgb: tuple[int, int, int]) -> str:
"""
Translates an rgb value to the closest English color name known
Args:
rgb: The rgb value that has to be translated to the color name.
Returns:
The name of the colors that most closely defines the rgb value in CSS3.
"""
min_colours = {}
for key, name in webcolors.CSS3_HEX_TO_NAMES.items():
r_c, g_c, b_c = webcolors.hex_to_rgb(key)
rd = (r_c - rgb[2]) ** 2
gd = (g_c - rgb[1]) ** 2
bd = (b_c - rgb[0]) ** 2
min_colours[(rd + gd + bd)] = name
return min_colours[min(min_colours.keys())]
Which is already enough to solve the question if you only care about the colors that are used in the image.
image = cv2.imread('image.jpg')
colors = set([color_rgb_to_name(val) for val in np.unique(image.reshape(-1, 3), axis=0)])
Colors:
{'firebrick', 'cadetblue', 'peru', 'indianred', 'darkturquoise', 'cyan', 'darkviolet', 'darkorange', 'midnightblue', 'indigo', 'lightseagreen', 'mediumturquoise', 'blue', 'brown', 'chocolate', 'saddlebrown', 'mediumblue', 'darkslateblue', 'turquoise', 'blueviolet', 'sienna', 'black', 'orangered', 'slateblue'}
Notes:
- This uses the
webcolors
package, but you can create your own dictionary. This gives you a higher control on the colors that you allow / disallow.
Locate the Circles
The colors that we found above are all the unique colors that are contained in the image. This is often not really what we want. Instead we want to find the color that is most commonly used inside the circle.
In order to define the color in a circle there are several sources that we can use:
- https://www.tutorialspoint.com/find-circles-in-an-image-using-opencv-in-python
- https://www.pyimagesearch.com/2014/07/21/detecting-circles-images-using-opencv-hough-circles/
- How to find the circle in the given images using opencv python (hough circles )?
Which combines to the following code:
def locate_circles(img: np.ndarray, vmin=10, vmax=30) -> np.ndarray:
"""
Locates circles on a gray image.
Args:
img: a gray image with black background.
vmin: The minimum radius value of the circles.
vmax: The maximum radius value of the circles.
Returns:
A numpy array containing the center location of the circles and the radius.
"""
img = cv2.medianBlur(img, 5)
circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 20, param1=50, param2=20, minRadius=vmin, maxRadius=vmax)
circles = np.round(circles[0, :]).astype("int")
return circles
I added the medianBlur to increase the consistency in locating the circles, alternatively you could play a bit more with the param
values or radius sizes.
Test code:
image = cv2.imread('image.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
for (x, y, r) in locate_circles(gray, vmin=10, vmax=30):
print(x, y, r)
Answers:
262 66 12
186 74 12
136 60 12
Obtain the English name per circle
Now that we know where the circle is located, we can get the average color per circle and combine this with the above code obtain the final result.
The following code locates all x and y values that are inside the circle.
def coordinates(x: int, y: int, r: int, width: int, height: int) -> np.ndarray:
"""
Locates all valid x and y coordinates inside a circle.
Args:
x: Center column position.
y: Center row position.
r: Radius of the circle.
width: the maximum width value that is still valid (in bounds)
height: the maximum height values that is still valid (in bounds)
Returns:
A numpy array with all valid x and y coordinates that fall within the circle.
"""
indices = []
for dx in range(-r, r):
for dy in range(-r, r):
if 0 <= x + dx < width and 0 <= y + dy < height:
indices.append([x + dx, y + dy])
return np.array(indices).T.reshape(2, -1)
Which can then be used to obtain the average color value per circle.
image = cv2.imread('image.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
for (x, y, r) in locate_circles(gray, vmin=10, vmax=30):
columns, rows = coordinates(x, y, r, *gray.shape[:2])
color = np.average(image[rows, columns], axis=0).astype(np.uint8)
name = color_rgb_to_name(color)
# Draw the information on the screen
cv2.putText(image, name, (x - 20, y - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 0, 0), 1)
Answer:
indigo
firebrick
darkturquoise
TL;DR
import cv2
import numpy as np
import webcolors
def imshow(img, delay=0):
cv2.imshow('Test', img)
cv2.waitKey(delay)
def locate_circles(img: np.ndarray, vmin=10, vmax=30) -> np.ndarray:
"""
https://www.tutorialspoint.com/find-circles-in-an-image-using-opencv-in-python
https://www.pyimagesearch.com/2014/07/21/detecting-circles-images-using-opencv-hough-circles/
https://stackoverflow.com/questions/67764821/how-to-find-the-circle-in-the-given-images-using-opencv-python-hough-circles
Locates circles on a gray image.
Args:
img: a gray image with black background.
vmin: The minimum radius value of the circles.
vmax: The maximum radius value of the circles.
Returns:
A numpy array containing the center location of the circles and the radius.
"""
img = cv2.medianBlur(img, 5)
circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 20, param1=50, param2=20, minRadius=vmin, maxRadius=vmax)
circles = np.round(circles[0, :]).astype("int")
return circles
def coordinates(x: int, y: int, r: int, width: int, height: int) -> np.ndarray:
"""
Locates all valid x and y coordinates inside a circle.
Args:
x: Center column position.
y: Center row position.
r: Radius of the circle.
width: the maximum width value that is still valid (in bounds)
height: the maximum height values that is still valid (in bounds)
Returns:
A numpy array with all valid x and y coordinates that fall within the circle.
"""
indices = []
for dx in range(-r, r):
for dy in range(-r, r):
if 0 <= x + dx < width and 0 <= y + dy < height:
indices.append([x + dx, y + dy])
return np.array(indices).T.reshape(2, -1)
def draw_circles(img: np.ndarray, x: int, y: int, r: int):
"""
draw the circle in the output image, then draw a rectangle corresponding to the center of the circle
Args:
img: Image on which to draw the circle location and center.
x: Center column position.
y: Center row position.
r: Radius of the circle.
Modifies:
The input image by drawing a circle on it and a rectangle on the image.
"""
cv2.circle(img, (x, y), r, (0, 255, 0), 4)
cv2.rectangle(img, (x - 2, y - 2), (x + 2, y + 2), (0, 128, 255), -1)
def color_rgb_to_name(rgb: tuple[int, int, int]) -> str:
"""
https://stackoverflow.com/questions/9694165/convert-rgb-color-to-english-color-name-like-green-with-python
Translates an rgb value to the closest English color name known
Args:
rgb: The rgb value that has to be translated to the color name.
Returns:
The name of the colors that most closely defines the rgb value in CSS3.
"""
min_colours = {}
for key, name in webcolors.CSS3_HEX_TO_NAMES.items():
r_c, g_c, b_c = webcolors.hex_to_rgb(key)
rd = (r_c - rgb[2]) ** 2
gd = (g_c - rgb[1]) ** 2
bd = (b_c - rgb[0]) ** 2
min_colours[(rd + gd + bd)] = name
return min_colours[min(min_colours.keys())]
if __name__ == '__main__':
image = cv2.imread('image.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
for (x, y, r) in locate_circles(gray, vmin=10, vmax=30):
columns, rows = coordinates(x, y, r, *gray.shape[:2])
color = np.average(image[rows, columns], axis=0).astype(np.uint8)
name = color_rgb_to_name(color)
print(name)
# Draw extra information on the screen
# draw_circles(image, x, y, r)
cv2.putText(image, name, (x - 20, y - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 0, 0), 1)
# show the output image
imshow(image)

- 2,089
- 1
- 9
- 13