From RGB to HSV — and Back Again
A practical introduction to color spaces with Python and OpenCV The post From RGB to HSV — and Back Again appeared first on Towards Data Science.

Introduction
A fundamental concept in Computer Vision is understanding how images are stored and represented. On disk, image files are encoded in various ways, from lossy, compressed JPEG files to lossless PNG files. Once you load an image into a program and decode it from the respective file format, it will most likely have an array-like structure that represents the pixels in the image.
RGB
Each pixel contains some color information about that specific point in the image. Now the most common way to represent this color is in the RGB space, where each pixel has three values: red, green and blue. These values describe how much of each color is present and they will be mixed additively. So for example, an image with all values set to zero will be black. If all three values are set to 100%, the resulting image will be white.
Sometimes the order of these color channels can be swapped. Another common order is BGR, so the order is reversed. This is commonly used in OpenCV and the default when reading or displaying images.
Alpha Channel
Images can also contain information about transparency. In that case, an additional alpha channel is present (RGBA). The alpha value indicates the opacity of each pixel: an alpha of zero means the pixel is fully transparent and a value of 100% represents a fully opaque pixel.
HSV
Now RGB(A) is not the only way to represent colors. In fact there are many different color models that represent color. One of the most useful models is the HSV model. In this model, each color is represented by a hue, saturation and value property. The hue describes the tone of color, irrespective of brightness and saturation. Sometimes this is represented on a circle with values between 0 and 360 or 0 to 180, or simply between 0 and 100%. Importantly, it is cyclical, meaning the values wrap around. The second property, the saturation describes how intense a color tone is, so a saturation of 0 results in gray colors. Finally the value property describes the brightness of the color, so a brightness of 0% is always black.
Now this color model is extremely helpful in image processing, as it allows us to decouple the color tone from the saturation and brightness, which is impossible to do directly in RGB. For example, if you want a transition between two colors and keep the same brightness during the full transition, this will be very complex to achieve using the RGB color model, whereas in the HSV model this is straightforward by just interpolating the hue.
Practical Examples
We will look at three examples of how to work with these color spaces in Python using OpenCV. In the first example, we extract parts of an image that are of a certain color. In the second part, we create a utility function to convert colors between the color spaces. Finally, in the third application, we create a continuous animation between two colors with constant brightness and saturation.
1 – Color Mask
The goal of this part is to find a mask that isolates colors based on their hue in an image. In the following picture, there are different colored paper pieces that we want to separate.
Using OpenCV, we can load the image and convert it to the HSV color space. By default images are read in BGR format, hence we need the flag cv2.COLOR_BGR2HSV
in the conversion:
Python">import cv2
img_bgr = cv2.imread("images/notes.png")
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
Now on the HSV image we can apply a color filter using the cv2.inRange
function to specify a lower and upper bound for each property (hue, saturation, value). With some experimentation I arrived at the following values for the filter:
Property Lower Bound Upper Bound Hue 90 110 Saturation 60 100 Value 150 200
mask = cv2.inRange(
src=img_hsv,
lowerb=np.array([90, 60, 150]),
upperb=np.array([110, 100, 200]),
)
The hue filter here is constrained between 90 and 110, which corresponds to the light blue paper at the bottom of the image. We also set a range of the saturation and the brightness value to get a reasonably accurate mask.
To show the results, we first need to convert the single-channel mask back to a BGR image shape with 3 channels. Additionally, we can also apply the mask to the original image and visualize the result.
mask_bgr = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
img_bgr_masked = cv2.bitwise_and(img_bgr, img_bgr, mask=mask)
composite = cv2.hconcat([img_bgr, mask_bgr, img_bgr_masked])
cv2.imshow("Composite", composite)

By changing the hue range, we can also isolate other pieces. For example for the purple paper, we can specify the following range:
Property Lower Bound Upper Bound Hue 160 175 Saturation 80 110 Value 170 210
2 – Color Conversion
While OpenCV provides a handy function to convert full images between color spaces, it doesn’t provide an out-of-the-box solution to convert single colors between color spaces. We can write a simple wrapper that creates a small 1×1 pixel image with an input color, uses the integrated OpenCV function to convert to another color space and extract the color of this single pixel again.
def convert_color_space(input: tuple[int, int, int], mode: int) -> tuple[int, int, int]:
"""
Converts between color spaces
Args:
input: A tuple representing the color in any color space (e.g., RGB or HSV).
mode: The conversion mode (e.g., cv2.COLOR_RGB2HSV or cv2.COLOR_HSV2RGB).
Returns:
A tuple representing the color in the target color space.
"""
px_img_hsv = np.array([[input]], dtype=np.uint8)
px_img_bgr = cv2.cvtColor(px_img_hsv, mode)
b, g, r = px_img_bgr[0][0]
return int(b), int(g), int(r)
Now we can test the function with any color. We can verify that if we convert from RGB -> HSV -> RGB back to the original format, we get the same values.
red_rgb = (200, 120, 0)
red_hsv = convert_color_space(red_rgb, cv2.COLOR_RGB2HSV)
red_bgr = convert_color_space(red_rgb, cv2.COLOR_RGB2BGR)
red_rgb_back = convert_color_space(red_hsv, cv2.COLOR_HSV2RGB)
print(f"{red_rgb=}") # (200, 120, 0)
print(f"{red_hsv=}") # (18, 255, 200)
print(f"{red_bgr=}") # (0, 120, 200)
print(f"{red_rgb_back=}") # (200, 120, 0)
3 – Continuous Color Transition
In this third example, we will create a transition between two colors with a constant brightness and saturation interpolation. This will be compared to a direct interpolation between the initial and final RGB values.
def interpolate_color_rgb(
start_rgb: tuple[int, int, int], end_rgb: tuple[int, int, int], t: float
) -> tuple[int, int, int]:
"""
Interpolates between two colors in RGB color space.
Args:
start_rgb: The starting color in RGB format.
end_rgb: The ending color in RGB format.
t: A float between 0 and 1 representing the interpolation factor.
Returns:
The interpolated color in RGB format.
"""
return (
int(start_rgb[0] + (end_rgb[0] - start_rgb[0]) * t),
int(start_rgb[1] + (end_rgb[1] - start_rgb[1]) * t),
int(start_rgb[2] + (end_rgb[2] - start_rgb[2]) * t),
)
def interpolate_color_hsv(
start_rgb: tuple[int, int, int], end_rgb: tuple[int, int, int], t: float
) -> tuple[int, int, int]:
"""
Interpolates between two colors in HSV color space.
Args:
start_rgb: The starting color in RGB format.
end_rgb: The ending color in RGB format.
t: A float between 0 and 1 representing the interpolation factor.
Returns:
The interpolated color in RGB format.
"""
start_hsv = convert_color_space(start_rgb, cv2.COLOR_RGB2HSV)
end_hsv = convert_color_space(end_rgb, cv2.COLOR_RGB2HSV)
hue = int(start_hsv[0] + (end_hsv[0] - start_hsv[0]) * t)
saturation = int(start_hsv[1] + (end_hsv[1] - start_hsv[1]) * t)
value = int(start_hsv[2] + (end_hsv[2] - start_hsv[2]) * t)
return convert_color_space((hue, saturation, value), cv2.COLOR_HSV2RGB)
Now we can write a loop to compare these two interpolation methods. To create the image, we use the np.full
method to fill all pixels of the image array with a specified color. Using cv2.hconcat
we can combine the two images horizontally into one image. Before we display them, we need to convert to the OpenCV format BGR.
def run_transition_loop(
color_start_rgb: tuple[int, int, int],
color_end_rgb: tuple[int, int, int],
fps: int,
time_duration_secs: float,
image_size: tuple[int, int],
) -> None:
"""
Runs the color transition loop.
Args:
color_start_rgb: The starting color in RGB format.
color_end_rgb: The ending color in RGB format.
time_steps: The number of time steps for the transition.
time_duration_secs: The duration of the transition in seconds.
image_size: The size of the images to be generated.
"""
img_shape = (image_size[1], image_size[0], 3)
num_steps = int(fps * time_duration_secs)
for t in np.linspace(0, 1, num_steps):
color_rgb_trans = interpolate_color_rgb(color_start_rgb, color_end_rgb, t)
color_hue_trans = interpolate_color_hsv(color_start_rgb, color_end_rgb, t)
img_rgb = np.full(shape=img_shape, fill_value=color_rgb_trans, dtype=np.uint8)
img_hsv = np.full(shape=img_shape, fill_value=color_hue_trans, dtype=np.uint8)
composite = cv2.hconcat((img_rgb, img_hsv))
composite_bgr = cv2.cvtColor(composite, cv2.COLOR_RGB2BGR)
cv2.imshow("Color Transition", composite_bgr)
key = cv2.waitKey(1000 // fps) & 0xFF
if key == ord("q"):
break
cv2.destroyAllWindows()
Now we can simply call this function with two colors for which we want to visualize the transition. Below I visualize the transition from blue to yellow.
run_transition_loop(
color_start_rgb=(0, 0, 255), # Blue
color_end_rgb=(255, 255, 0), # Yellow
fps=25,
time_duration_secs=5,
image_size=(512, 256),
)

The difference is quite drastic. While the saturation and brightness remain constant in the right animation, they change considerably for the transition that interpolates directly in the RGB space.
For more implementation details, check out the full source code in the GitHub repository:
https://github.com/trflorian/auto-color-filter
All visualizations in this post were created by the author.
The post From RGB to HSV — and Back Again appeared first on Towards Data Science.