HOWTO · Python

Enmascaramiento de imágenes en OpenCV

En este artículo, veremos cómo realizar el enmascaramiento de imágenes usando Bitwise AND en OpenCV Python.

En esta página

El enmascaramiento de imágenes permite conservar una parte de una imagen y combinarla con otra imagen. En OpenCV, el enmascaramiento se basa en la operación bit a bit AND: la función cv.bitwise_and() calcula el AND bit a bit elemento a elemento de dos matrices, y una máscara opcional decide qué píxeles de ese resultado llegan a la salida.

Este artículo recorre el flujo de trabajo estándar de enmascaramiento en Python. Con una foto de un coche y un archivo de logotipo, preparamos una región de interés (ROI), construimos una máscara binaria y su inversa a partir del logotipo, aplicamos ambas máscaras y componemos el logotipo en la esquina superior izquierda de la imagen del coche.

Cómo cv.bitwise_and() aplica una máscara

cv.bitwise_and() es una función a nivel de módulo del paquete cv2, no un método de un objeto de imagen. Su firma en Python es la siguiente:

cv.bitwise_and(src1, src2[, dst[, mask]])

  • src1 y src2 son las dos matrices de entrada. Deben tener el mismo tamaño y el mismo tipo de datos. Para enmascarar una sola imagen, pasa la misma matriz en ambos argumentos.
  • dst es una matriz de salida opcional del mismo tamaño y tipo; cuando la omites, la función reserva y devuelve el resultado.
  • mask es una matriz opcional de 8 bits y un solo canal, del mismo tamaño que las fuentes. Donde la máscara contiene un valor distinto de cero, el píxel de salida es src1 & src2; donde la máscara contiene 0, el píxel de salida es 0. La máscara, por tanto, selecciona qué píxeles de la operación aparecen en el resultado: esa selección es la idea central del enmascaramiento de imágenes.

Ejecuta este ejemplo, que llama a cv.bitwise_and() cuando OpenCV está instalado y, en caso contrario, realiza el mismo AND elemento a elemento con listas simples de Python. El código y los mensajes visibles del ejemplo se mantienen en inglés:

import importlib.util

if importlib.util.find_spec("cv2") is not None:
    import cv2 as cv
    import numpy as np

    pixels = np.array([[200, 120, 60], [30, 90, 250]], dtype=np.uint8)
    mask = np.array([[255, 255, 0], [0, 255, 255]], dtype=np.uint8)
    result = cv.bitwise_and(pixels, pixels, mask=mask).tolist()
else:
    pixels = [[200, 120, 60], [30, 90, 250]]
    mask = [[255, 255, 0], [0, 255, 255]]
    result = [[v & m for v, m in zip(pr, mr)] for pr, mr in zip(pixels, mask)]

print(result)
[[200, 120, 0], [0, 90, 250]]

Cada píxel bajo un valor de máscara 255 se conserva; cada píxel bajo un valor 0 se elimina. En una máscara real, los valores se generan mediante umbralización en lugar de escribirse a mano, que es el siguiente paso.

Preparar la región de interés de la imagen

El objetivo es colocar el logotipo en la esquina superior izquierda de la imagen del coche, por lo que el código lee ambos archivos y recorta una región coincidente de la imagen del coche:

  • car = cv.imread("car.jpg") y logo = cv.imread("logo.png") cargan las imágenes como matrices BGR. cv.imread devuelve None cuando un archivo no se carga, así que comprueba ambos resultados antes de continuar.
  • logo.shape devuelve (rows, cols, channels); las dos primeras entradas dan las dimensiones del logotipo.
  • roi = car[0:rows, 0:cols] toma una vista de NumPy de la esquina superior izquierda de la imagen del coche que coincide exactamente con el tamaño del logotipo. Al ser una vista, escribir el compuesto de vuelta en car[0:rows, 0:cols] actualiza la imagen original.

El logotipo debe caber dentro de la imagen del coche. Si el logotipo es más ancho o más alto que la foto, el recorte se reduce silenciosamente y cv.bitwise_and(roi, roi, mask=mask_inv) falla después con un desajuste de tamaño, porque mask_inv tiene el tamaño del logotipo y no el de la ROI reducida, así que redimensiona primero el logotipo o elige una ubicación que encaje. Ten en cuenta también que cv.imread carga un PNG como imagen BGR de tres canales por defecto (el canal alfa se descarta), de modo que el logotipo y la ROI terminan con el mismo número de canales.

Crear una máscara binaria para el logotipo

Del logotipo salen dos máscaras: mask, que selecciona los píxeles del propio logotipo, y mask_inv, que selecciona todo lo demás.

  • gray_logo = cv.cvtColor(logo, cv.COLOR_BGR2GRAY) convierte el logotipo a escala de grises; consulta la función cv.cvtColor() para más información sobre la conversión de color.
  • _, mask = cv.threshold(gray_logo, 10, 255, cv.THRESH_BINARY) produce la máscara binaria: los píxeles más brillantes que 10 pasan a 255 y los más oscuros a 0. Para un logotipo sobre un fondo oscuro o negro, esto aísla la forma del logotipo; ajusta el 10 según tu propio fondo.
  • mask_inv = cv.bitwise_not(mask) invierte cada bit, lo que da la máscara complementaria que selecciona la región de fondo.

Combinar el logotipo y la región de interés

Con ambas máscaras listas, dos AND enmascarados reparten el trabajo: cv.bitwise_and(roi, roi, mask=mask_inv) conserva el fondo del coche y ennegrece la forma del logotipo, mientras que cv.bitwise_and(logo, logo, mask=mask) conserva solo los píxeles del logotipo. cv.add() suma entonces los dos resultados en el compuesto, que se escribe de vuelta en la imagen del coche.

El script completo que sigue realiza cada paso. Cuando OpenCV o los archivos de imagen no están presentes, imprime un mensaje breve y se detiene sin traceback. El código y los mensajes visibles del script se mantienen en inglés:

import importlib.util

if importlib.util.find_spec("cv2") is None:
    print("This example needs OpenCV installed: pip install opencv-python.")
    raise SystemExit

import cv2 as cv

car = cv.imread("car.jpg")
logo = cv.imread("logo.png")
if car is None or logo is None:
    print("car.jpg and logo.png must sit next to this script.")
    raise SystemExit

rows, cols = logo.shape[:2]
if rows > car.shape[0] or cols > car.shape[1]:
    print("The logo is larger than the car image; resize it first.")
    raise SystemExit

roi = car[0:rows, 0:cols]

gray_logo = cv.cvtColor(logo, cv.COLOR_BGR2GRAY)
_, mask = cv.threshold(gray_logo, 10, 255, cv.THRESH_BINARY)
mask_inv = cv.bitwise_not(mask)

background = cv.bitwise_and(roi, roi, mask=mask_inv)
foreground = cv.bitwise_and(logo, logo, mask=mask)

composite = cv.add(background, foreground)
car[0:rows, 0:cols] = composite

cv.imwrite("car-with-logo.png", car)
print("Wrote car-with-logo.png")

El script guarda la imagen terminada con cv.imwrite() para que también funcione en máquinas sin pantalla; las llamadas de visualización aparecen en la siguiente sección.

Inspeccionar el resultado del enmascaramiento

El compuesto coloca el logotipo sobre la esquina superior izquierda del coche mientras el resto de la foto permanece intacto:

Imagen del coche con el logotipo compuesto en su esquina superior izquierda

Para ver lo que produjo cada etapa, muestra o guarda los intermedios: cv.imshow("mask", mask), cv.imshow("mask_inv", mask_inv), cv.imshow("background", background) y cv.imshow("foreground", foreground) seguidos de cv.waitKey(0) los muestran en pantalla, mientras que cv.imwrite() los escribe en disco en máquinas sin entorno gráfico. cv.imshow() necesita una pantalla con interfaz gráfica, razón por la cual el script anterior guarda archivos.

En mask deberías ver la silueta blanca del logotipo sobre negro, y mask_inv es su imagen especular. background muestra la esquina del coche con la forma del logotipo recortada, y foreground muestra solo el logotipo. Ese es todo el patrón de enmascaramiento: umbraliza una vez, aplica cv.bitwise_and() dos veces, suma los resultados y la ROI contiene la imagen fusionada.