5

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]])
pwan
  • 687
  • 3
  • 9
  • 16
  • Do you mean that you want to see a plot of all the colors included in the range? To see the upper and lower bounds you could simply create two images filled with those two colors to see at least what those look like. But otherwise get familiar with the [cylindrical model for color in the HSV space](https://en.wikipedia.org/wiki/HSL_and_HSV#/media/File:Hsl-hsv_models.svg). These bounds are saying "include hues between 220 and 260 degrees, saturation between 50 and 255, and a value (dark-to-vibrant) between 50 and 255". You can imagine taking that chunk of the cylinder. – alkasm Jul 13 '17 at 03:09
  • i want to plot all the posible value in the range. is it possible? – pwan Jul 13 '17 at 03:11
  • @AlexanderReynolds Can you teach me how to plot that cylinder and visualize cutting that chunk? just updated my question. – pwan Jul 13 '17 at 03:14
  • I think the probably easier thing to do for now would be this: create a gradient image which goes from left to right `s_min` to `s_max`, from top to bottom `v_min` to `v_max`, and then loop through every hue to get an animation of what that would look like. I'll cook something up for this, give me a bit. – alkasm Jul 13 '17 at 03:23
  • 1
    i really dont get it what does "range" mean here? in my case 110 ~ 130 degree is all in range. but what about "H" & "V"? – pwan Jul 13 '17 at 03:29
  • To answer the second part of your question in your updated post: `upper = np.array([[[60, 255, 255]]], dtype=np.uint8); upper = cv2.cvtColor(upper, cv2.COLOR_HSV2BGR); upper_img = np.ones((500,500,3), dtype=np.uint8)*upper; cv2.imshow('', upper_img); cv2.waitKey(0)` – alkasm Jul 13 '17 at 03:30
  • @AlexanderReynolds Can you please explain what that range mean? take a small example [0,20,30] to [10,30,50]. – pwan Jul 13 '17 at 03:31

1 Answers1

27

What are HSV colors

HSV, like HSL (or in OpenCV, HLS), is one of the cylindrical colorspaces.

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.

Ranges of HSV values

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).

Visualizing a range of colors from HSV

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()

Gif of range values

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.

Make your own inRange() function

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

How to find the right colors

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:

Gif of cspaceThresh program

alkasm
  • 22,094
  • 5
  • 78
  • 94
  • thanks! that is a very deep explain. very helpful. just 1 more silly question, is it possible to show it inline in ipython? So I can visualize many range to compare them in once. – pwan Jul 13 '17 at 04:59
  • @RobAu Both representations are correct; they're typically referenced as [cylindrical coordinates](https://en.wikipedia.org/wiki/HSL_and_HSV). With the conic or bi-conic solids you get the benefit of only a single representation of each color, whereas for HSV there are for e.g. an infinite number of black points (with value=0, any degree of H and any saturation S still produces black). But I find the cylindrical model easier to explain; a 3d section of a solid conic is a strange thing to imagine. – alkasm Jul 13 '17 at 07:05
  • @panpan you can do the same thing I did but with matplotlib, just be sure to use RGB order for displaying (i.e. convert with `COLOR_HSV2RGB`). You can loop through a `plt.imshow()` with `matplotlib` as I did here. At least I think you can with Jupyter. – alkasm Jul 13 '17 at 07:06
  • 1
    @AlexanderReynolds the cylindrical model is also easier to store in data. But it is good to realize that all the black points in the cylinder are actually the same color, despite their different values for H and S. This might make color-distance functions in the cylinder not work as expected. – Rob Audenaerde Jul 13 '17 at 07:24
  • 1
    @RobAu a good point, and taken, thanks for your input. I've known that for `v=0`, the `h` and `s` channel don't matter; now that we're having this discussion I realize that means for an 8-bit representation you have more colors in RGB (not even including the fact that the degree increments are by 2 degrees in OpenCV's HSV model). – alkasm Jul 13 '17 at 07:33
  • 2
    god, your tool is just amazing. – pwan Jul 14 '17 at 07:13
  • @pwan I'm glad it's useful! I'm currently making a web version, it should be useable within the next week. – alkasm Jul 14 '17 at 19:11
  • I just wanted to say it's a great explanation, deserving much more upvoting. Thanks for this one. – KjMag Aug 23 '17 at 07:53
  • @KjMag thanks for the encouragement, I'm glad it was helpful! – alkasm Aug 23 '17 at 09:09
  • @AlexanderReynolds where is the web version ? ahahaha – lucians Feb 18 '19 at 21:15
  • It seems, the hue channel is the most tricky one to filter because it wraps around at red, if I understand correctly. So, for example, to filter out all the red-ish objects of the image, you will have to combine both ranges around 0 and 179. Or you can shift around the hue channel and your boundaries before processing. – JustAMartin Sep 18 '20 at 14:20