De RGB a HSV – y de regreso de nuevo

Un concepto fundamental en Visión por computadora es comprender cómo se almacenan y representan las imágenes. En el disco, los archivos de imagen están codificados de varias maneras, desde los pérdida, comprimidos Jpeg Archivos a la pérdida Png archivos. Una vez que carga una imagen en un programa y la decodifique del formato de archivo respectivo, lo más probable es que tenga una estructura similar a una matriz que represente los píxeles en la imagen.

RGB

Cada píxel contiene algunos información de color sobre ese punto específico en la imagen. Ahora la forma más común de representar este color está en el RGB espacio, donde cada píxel tiene tres valores: rojo, verde y azul. Estos valores describen cuánto de cada color está presente y se mezclarán aditivamente. Entonces, por ejemplo, una imagen con todos los valores establecidos en cero será negra. Si los tres valores se establecen en 100%, la imagen resultante será blanca.

A veces se puede cambiar el orden de estos canales de color. Otro orden común es Bgranentonces el orden se invierte. Esto se usa comúnmente en Opencvv y el valor predeterminado al leer o mostrar imágenes.

Canal alfa

Las imágenes también pueden contener información sobre la transparencia. En ese caso, hay un canal alfa adicional (RGBA). El valor alfa indica la opacidad de cada píxel: un alfa de cero significa que el píxel es completamente transparente y un valor del 100% representa un píxel completamente opaco.

HSV

Ahora RGB (a) No es la única forma de representar los colores. De hecho, hay muchos modelos de color diferentes que representan el color. Uno de los modelos más útiles es el HSV modelo. En este modelo, cada color está representado por un matiz, saturación y valor propiedad. El tono describe el tono de color, independientemente del brillo y la saturación. A veces esto se representa en un círculo con valores entre 0 y 360 o 0 a 180, o simplemente entre 0 y 100%. Es importante destacar que es cíclico, lo que significa que los valores se envuelven. La segunda propiedad, la saturación describe cómo intenso Un tono de color es, por lo que una saturación de 0 resulta en colores grises. Finalmente, la propiedad de valor describe el brillo del color, por lo que un brillo del 0% es siempre negro.

Ahora, este modelo de color es extremadamente útil en el procesamiento de imágenes, ya que nos permite desacoplar el tono de color de la saturación y el brillo, lo cual es imposible de hacer directamente en RGB. Por ejemplo, si desea una transición entre dos colores y mantiene el mismo brillo durante la transición completa, esto será muy complejo de lograr el uso del RGB modelo de color, mientras que en el HSV Modelo Esto es sencillo simplemente interpolando el tono.

Ejemplos prácticos

Veremos tres ejemplos de cómo trabajar con estos espacios de color en Python usando OpenCV. En el primer ejemplo, extraemos partes de una imagen que son de cierto color. En la segunda parte, creamos una función de utilidad para convertir los colores entre los espacios de color. Finalmente, en la tercera aplicación, creamos una animación continua entre dos colores con brillo constante y saturación.

1 – Máscara de color

El objetivo de esta parte es encontrar una máscara que aísla los colores basados ​​en su tono en una imagen. En la siguiente imagen, hay piezas de papel de diferentes colores que queremos separar.

Piezas de papel de color

Usando OpenCV, podemos cargar la imagen y convertirla en el espacio de color HSV. Por defecto se leen las imágenes en Bgran formato, por lo tanto, necesitamos la bandera cv2.COLOR_BGR2HSV En la conversión:

Python">import cv2

img_bgr = cv2.imread("images/notes.png")
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

Ahora en el HSV imagen podemos aplicar un filtro de color utilizando el cv2.inRange Funcionar para especificar un límite inferior y superior para cada propiedad (tono, saturación, valor). Con alguna experimentación llegué a los siguientes valores para el filtro:

Propiedad Bound Tound Límite superior
Matiz 90 110
Saturación 60 100
Valor 150 200
mask = cv2.inRange(
    src=img_hsv,
    lowerb=np.array([90, 60, 150]),
    upperb=np.array([110, 100, 200]),
)

El filtro de tono aquí está limitado entre 90 y 110, que corresponde al papel azul claro en la parte inferior de la imagen. También establecemos un rango de saturación y el valor de brillo para obtener una máscara razonablemente precisa.

Para mostrar los resultados, primero necesitamos convertir la máscara de un solo canal en un Bgran forma de imagen con 3 canales. Además, también podemos aplicar la máscara a la imagen original y visualizar el resultado.

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)

Al cambiar el rango de tono, también podemos aislar otras piezas. Por ejemplo, para el papel púrpura, podemos especificar el siguiente rango:

Propiedad Bound Tound Límite superior
Matiz 160 175
Saturación 80 110
Valor 170 210

2 – Conversión de color

Si bien OpenCV proporciona una función práctica para convertir imágenes completas entre los espacios de color, no proporciona una solución lista para usar para convertir los colores individuales entre los espacios de color. Podemos escribir un envoltorio simple que cree una pequeña imagen de píxel de 1 × 1 con un color de entrada, utiliza la función OpenCV integrada para convertir a otro espacio de color y extraer el color de este único píxel nuevamente.

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)

Ahora podemos probar la función con cualquier color. Podemos verificar que si nos convertimos de RGB -> HSV -> RGB al formato original, obtenemos los mismos valores.

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 – Transición de color continuo

En este tercer ejemplo, crearemos una transición entre dos colores con un brillo constante e interpolación de saturación. Esto se comparará con una interpolación directa entre los valores RGB iniciales y finales.

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)

Ahora podemos escribir un bucle para comparar estos dos métodos de interpolación. Para crear la imagen, usamos el np.full Método para llenar todos los píxeles de la matriz de imágenes con un color especificado. Usando cv2.hconcat Podemos combinar las dos imágenes horizontalmente en una imagen. Antes de mostrarlos, necesitamos convertirnos al formato OpenCV 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()

Ahora podemos simplemente llamar a esta función con dos colores para los cuales queremos visualizar la transición. A continuación visualizo la transición de azul a amarillo.

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

La diferencia es bastante drástica. Si bien la saturación y el brillo permanecen constantes en la animación correcta, cambian considerablemente por la transición que interpola directamente en el RGB espacio.


Para obtener más detalles de implementación, consulte el código fuente completo en el repositorio de GitHub:

https://github.com/trflorian/auto-color-filter


Todas las visualizaciones en esta publicación fueron creadas por el autor.