From RGB to HSV, then return

A basic concept Computer Vision Understanding how to store and represent images. On disk, image files are encoded in various ways, from lossy, compressed jpeg Lossless file PNG document. After loading the image into a program and decoding from the corresponding file format, it is likely to have an array-like structure representing the pixels in the image.
RGB
Each pixel contains some Color information About specific points in the image. The most common way to represent this color now is RGB Space, each pixel has three values: red, green, and blue. These values describe the presence of each color and they will be mixed. So, for example, images that set all values to zero are black. If you set all three values to 100%, the resulting image will be white.
Sometimes the order of these color channels can be swapped. Another common order is BGRso the order was reversed. This is usually OPENCV and the default value when reading or displaying images.
alpha channel
The image can also contain information about transparency. In this case, there is another alpha channel (RGBA). The alpha value indicates opacity per pixel: a zero alpha means that the pixel is completely transparent, and a value of 100% indicates a completely opaque pixel.

HSV
Now RGB(a) Not the only way to represent color. In fact, there are many different color models representing colors. One of the most useful models is HSV Model. In this model, each color is tone,,,,, saturation and value property. Hue describes the tone of a color, regardless of brightness and saturation. Sometimes this is represented on a circle with values between 0 and 360 or 0 and 180, or just between 0 and 100%. Importantly, it is periodic, which means value entanglement. The second attribute, saturation, describes how Fierce The hue is, so 0 saturation leads to gray. Finally, the value attribute describes the brightness of the color, so 0% brightness is always black.

Now this color model is very helpful in image processing because it allows us to free saturation and brightness from color tint, which is impossible to do directly RGB. For example, if you want a transition between two colors and keep the same brightness, using that color will be very complicated RGB color model, and HSV The model is directly by interpolation of tones only.
Practical examples
We’ll look at three examples of how to use these color spaces in Python using OpenCV. In the first example, we extract a portion of the image with a certain color. In the second part, we create a utility function to convert colors between color spaces. Finally, in the third application, we create a continuous animation between two colors with constant brightness and saturation.
1 – Colored Masks
The purpose of this section is to find a mask that isolates the color according to the tone in the image. In the picture below, we want to separate different colored paper sheets.

With OpenCV we can load the image and convert it to HSV color space. The default image has been read BGR Format, so we need flags 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 HSV Images we can use color filters cv2.inRange
Specifies a lower and upper limit function for each attribute (hue, saturation, value). Through some experiments, I came up with the following filter values:
property | Lower limit | Upper limit |
---|---|---|
tone | 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 a constraint between 90 and 110, which corresponds to the light blue paper at the bottom of the image. We also set a range of saturation and brightness values to get a reasonable and accurate mask.

To display the results, we first need to convert the single channel mask back BGR Image shape with 3 channels. In addition, we can apply masks to the original image and visualize the results.
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 tone range, we can also isolate other parts. For example, for purple paper, we can specify the following range:
property | Lower limit | Upper limit |
---|---|---|
tone | 160 | 175 |
saturation | 80 | 110 |
value | 170 | 210 |

2 – Color conversion
While OpenCV provides convenient functionality to convert full images between color spaces, it does not provide an out-of-the-box solution to convert a single color between color spaces. We can write a simple wrapper that uses the input color to create a small 1×1 pixel image which uses the integrated OpenCV function to convert it to another color space and extract the color of that 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 use any color testing feature. We can verify that if we convert from RGB->HSV->RGB back to the original format, we will get the same value.
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 transitions
In the third example, we will create a transition between two colors with constant brightness and saturation interpolation. Compare this to the direct interpolation between the initial RGB value and the final RGB value.
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 the two interpolation methods. To create an image, we use np.full
Method to fill all pixels of an image array with a specified color. use cv2.hconcat
We can combine two images horizontally into one image. Before displaying them, we need to convert to 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 to visualize the transition. I’ll visualize it from Blue arrive 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),
)

There is a big difference. While saturation and brightness remain constant in the correct animation, their transitions directly in the interpolation have changed a lot RGB space.
For more implementation details, check out the full source code in the GitHub repository:
All visualizations in this post were created by the author.