To extract the color, we have this function
# define range of blue color in HSV
lower_blue = np.array([110,50,50])
upper_blue = np.array([130,255,255])
# Threshold the HSV image to get only blue colors
mask = cv2.inRange(hsv, lower_blue, upper_blue)
How do we actually visualize the range(lower_blue,upper_blue) I define on hsv space? Also How do I actually plot a hsv color,but it is not working...? I have this code:
upper = np.array([60, 255, 255])
upper = cv2.cvtColor(upper, cv2.COLOR_HSV2BGR)
upper = totuple(upper/-255)
print(upper)
plt.imshow([[upper]])
HSV, like HSL (or in OpenCV, HLS), is one of the cylindrical colorspaces.
The name is somewhat descriptive of how their values are referenced.
The hue is represented as degrees from 0 to 360 (in OpenCV, to fit into the an 8-bit unsigned integer format, they degrees are divided by two to get a number from 0 to 179; so 110 in OpenCV is 220 degrees). If you were to take a "range" of hue values, it's like cutting a slice from a cake. You're just taking some angle chunk of the cake.
The saturation channel is how far from the center you are---the radius you're at. The center has absolutely no saturation---only gray colors from black to white. If you took a range of these values, it is akin to shaving off the outside of the cylinder, or cutting out a circle from the center. For example, if the range is 0 to 255, then the range 0 to 127 would be a cylinder only extending to half the radius; the range 127 to 255 would be cutting an inner cylinder with half the radius out.
The value channel is a slightly confusing name; it's not exactly darkness-to-brightness because the highest value represents the direct color, while the lowest value is black. This is the height of the cylinder. Not too hard to imagine cutting a slice of the cylinder vertically.
The function cv2.inRange(image, lower_bound, upper_bound)
finds all values of the image between lower_bound
and upper_bound
. For instance, if your image was a 3x3 image (just for simple demonstration purposes) with 3-channels, it might look something like this:
# h channel # s channel # v channel
100 150 250 150 150 100 50 75 225
50 100 125 75 25 50 255 100 50
0 255 125 100 200 250 50 75 100
If we wanted to select hues between 100 and 200, then our lower_b
should be [100, 0, 0]
and upper_b
should be [200, 255, 255]
. That way our mask would only take into account values in the hue channel, and not be affected by the saturation and value. That's why HSV is so popular---you can select colors by hue regardless of their brightness or darkness, so a dark red and bright red can be selected just by specifying the min and max of the hue channel.
But say we only wanted to select bright white colors. Take a look back at the cylinder model---we see that white is given at the top-center of the cylinder, so where s
values are low, and v
values are high, and the color angle doesn't matter. So the lower_b
would look something like [0, 0, 200]
and upper_b
would look something like [255, 50, 255]
. That means all H
values will be included and won't affect our mask. But then only S
values between 0 and 50 would be included (towards the center of the cylinder) and only V
values from 200 to 255 will be included (towards the top of the cylinder).
One way to visualize all the colors in a range is to create gradients going the length of both directions for each of two channels, and then animate over the changing third channel.
For instance, you could create a gradient of values from left to right for the range of S
values, from top to bottom for the range of V
values, and then loop over each H
value. This whole program could look something like this:
import numpy as np
import cv2
lower_b = np.array([110,50,50])
upper_b = np.array([130,255,255])
s_gradient = np.ones((500,1), dtype=np.uint8)*np.linspace(lower_b[1], upper_b[1], 500, dtype=np.uint8)
v_gradient = np.rot90(np.ones((500,1), dtype=np.uint8)*np.linspace(lower_b[1], upper_b[1], 500, dtype=np.uint8))
h_array = np.arange(lower_b[0], upper_b[0]+1)
for hue in h_array:
h = hue*np.ones((500,500), dtype=np.uint8)
hsv_color = cv2.merge((h, s_gradient, v_gradient))
rgb_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)
cv2.imshow('', rgb_color)
cv2.waitKey(250)
cv2.destroyAllWindows()
Now this gif shows a new H
value every frame. And from left to right we have the min to max S
values, and from top to bottom we have the min to max V
values. Every single one of the colors showing up in this animation will be selected from your image to be part of your mask
.
To fully understand the OpenCV function, the easiest way is just to make your own function to complete the task. It's not difficult at all, and not very much code.
The idea behind the function is simple: find where the values of each channel fall between min
and max
, and then &
all the channels together.
def inRange(img, lower_b, upper_b):
ch1, ch2, ch3 = cv2.split(img)
ch1m = (lower_b[0] <= ch1) & (ch1 <= upper_b[0])
ch2m = (lower_b[1] <= ch2) & (ch2 <= upper_b[1])
ch3m = (lower_b[2] <= ch3) & (ch3 <= upper_b[2])
mask = ch1m & ch2m & ch3m
return mask.astype(np.uint8)*255
You can read the OpenCV docs to see that this is indeed the formula used. And we can verify it too.
lower_b = np.array([200,200,200])
upper_b = np.array([255,255,255])
mask = cv2.inRange(img, lower_b, upper_b) # OpenCV function
mask2 = inRange(img, lower_b, upper_b) # above defined function
print((mask==mask2).all()) # checks that the masks agree on all values
# True
It can be a little tricky to find the correct values to use for a particular image. There is an easy way to experiment, though. You can create trackbars in OpenCV and use them to control the min and max for each channel and have the Python program update your mask every time you change the values. I made a program for this which you can grab on GitHub here. Here's an animated .gif
of it being used, to demonstrate:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With