7 cosas que debes saber antes de usar OpenCV en proyectos (Evita errores) | Minicurso OpenCV – Parte 6

Por Administrador

Cuando empezamos a trabajar con visión por computador usando OpenCV, es común encontrarse con errores que no siempre son evidentes: códigos que “deberían funcionar”, colores raros, imágenes que cambian solas o problemas de rendimiento.

En este artículo veremos 7 aspectos claves a tomar en cuenta cuando construimos proyectos con OpenCV. Muchos de estos son causa de errores silenciosos, que pueden hacerte perder horas de trabajo. Así que, ¡empecemos!

1. Las versiones de OpenCV

Seguro te ha pasado que encuentras un tutorial o un repositorio que promete justo lo que necesitas… copias el código, lo ejecutas y no funciona, aunque no hayas cambiado nada.

Bueno… en ese momento toca activar el modo detective 🕵️‍♂️, y el principal sospechoso casi siempre es la versión de OpenCV.

OpenCV es una librería que se actualiza constantemente. Se agregan funciones nuevas, algunas cambian de comportamiento y otras incluso dejan de existir con el tiempo. Por eso es importante corroborar que la versión de OpenCV que tienes instalada sea la misma, o al menos muy parecida, a la versión con la que se creó ese tutorial o ese proyecto.

Entonces… ¿cómo sé qué versión de OpenCV tengo instalada?

Para conocer qué versión se tiene instalada se puede acceder al intérprete de Python y digitar:

import cv2
print(cv2.__version__)

Además, para profundizar en funciones, parámetros, entre otros, puedes visitar la documentación oficial de OpenCV, y en el menú superior izquierdo seleccionar la versión exacta de la que se desea obtener información.

Consejo práctico: Cuando algo no te funcione, antes de volverte loco buscando errores, revisa primero estas tres cosas:

  1. Que tengas las mismas librerías instaladas
  2. Que la versión de OpenCV sea la misma o similar a la del proyecto a replicar
  3. Que los nombres de las funciones no hayan cambiado

2. Las imágenes en OpenCV son matrices de NumPy

Algo que necesitamos entender desde ya es que, para OpenCV una imagen no es simplemente una foto, en realidad es una matriz de datos, es decir, un arreglo de filas y columnas con valores numéricos.

Por ejemplo, si leemos una imagen, como lo hicimos en el tutorial anterior, podemos usar el atributo shape para saber cómo está estructurada esa matriz. Entonces escribimos:

print(image.shape)

Para una imagen a color obtendremos algo como esto:

(458, 700, 3)

Tenemos tres valores: el número de filas, el número de columnas y el número de canales. O dicho de otra forma: el alto, el ancho y el número de canales de la imagen.

En el ejemplo la imagen tiene (458, 700, 3), significa hay 458 píxeles de alto, 700 de ancho y 3 canales de color. Y como es una matriz, también podemos extraer directamente los valores de los píxeles especificando primero la fila y luego la columna, o incluso aplicar slicing, indicando un rango de filas y luego un rango de columnas. Esto también lo podremos visualizar con ayuda de cv2.imshow(). Veamos un ejemplo:

import cv2

# Leer una imagen
image = cv2.imread("image.jpeg")

# filas, columnas, canales
print(image.shape)
# Acceder a los valores de un píxel
print(image[20, 35])
# Aplicar slicing para obtener una imagen pequeña
tiny_image = image[10:100, 300:]

# Visualizar la imagen
cv2.imshow("Imagen de entrada", image)
cv2.imshow("Imagen pequeña", tiny_image)

cv2.waitKey(0)
cv2.destroyAllWindows()
Conoce más del ebook en: Introducción a la Visión por Computador con OpenCV y Python

3. OpenCV lee las imágenes por defecto en BGR

Ahora, quizás te estés preguntando: ¿cómo que 3 canales?. Pues sí, una imagen a color suele estar representada por 3 canales. Y aquí viene un punto muy importante, OpenCV lee las imágenes por defecto en 3 canales: B (blue), G (green), R (red). En ese orden.

No importa si la imagen parece estar en escala de grises o a color, OpenCV la va a leer en formato BGR, a menos que indiquemos lo contrario usando los parámetros en la función cv2.imread(). Si no indicamos nada, se va a leer en BGR.

Si visualizas una imagen usando funciones de OpenCV, no vas a notar ningún problema, porque este interpreta correctamente ese orden de canales. Pero si lees la imagen con OpenCV y luego la visualizas con otra librería, como por ejemplo Matplotlib que trabaja en RGB, entonces los colores se van a ver cambiados o extraños, como a continuación:

Esto se puede corregir fácilmente usando la función cv2.cvtColor(), con:

image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

Veamos la programación completa para este ejemplo:

import cv2
import matplotlib.pyplot as plt

# Leer una imagen
image = cv2.imread("imagen.jpg")

image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Visualizar con OpenCV
#cv2.imshow("Imagen de entrada", image)
#cv2.waitKey(0)
#cv2.destroyAllWindows()

# Visualizar con matplotlib
plt.imshow(image_rgb)
plt.axis("off")
plt.title("Imagen cargada con OpenCV y mostrada con Matplotlib")
plt.show()

Obtenemos la imagen corregida:

¿Qué es un espacio de color?

Y aquí introducimos un nuevo término: el espacio de color.
Un espacio de color es simplemente una forma de representar los colores usando diferentes combinaciones de valores numéricos. Por ejemplo, en RGB representamos un color usando: rojo, verde y azul. Existe otro espacio de color llamado HSV, que también usa tres canales, pero representan: tono, saturación y brillo.

También podemos convertir una imagen a escala de grises, donde solo tenemos un canal que representa la intensidad de cada píxel. De hecho, existen muchos espacios de color a los que puedes convertir una imagen con OpenCV usando la función cv2.cvtColor(). Puedes revisar su documentación para conocer todas las opciones disponibles.

4. El reinado de uint8

Ahora que ya sabemos que una imagen es una matriz de números y que cada píxel tiene varios valores según sus canales, viene otra pregunta importante: ¿qué tipo de números son estos?

Si imprimimos:

print(image.dtype)

Generalmente obtenemos:

uint8

¿Qué significa uint8?

Significa entero sin signo de 8 bits. Eso quiere decir que cada valor se representa usando 8 bits, lo que nos da 2 elevado a la 8, es decir, 256 valores posibles. Por esta razón, cada valor de un píxel puede ir desde 0 hasta 255

Veamos un ejercicio para obtener distintos colores a partir de los valores de 0 a 255 por cada canal:

import numpy as np
import cv2

# Dimensiones: alto x ancho
height, width = 300, 400

# Imagen negra (3 canales)
black_image = np.zeros((height, width, 3), dtype=np.uint8)

# Imagen blanca (3 canales)
white_image = np.ones((height, width, 3), dtype=np.uint8) * 255

# Azul
blue = black_image.copy()
blue[:, :, :] = (255, 0, 0)

# Verde
verde = black_image.copy()
verde[:, :, :] = (0, 255, 0)

# Rojo
red = black_image.copy()
red[:, :, :] = (0, 0, 255)

# Visualización de imágenes
cv2.imshow("Imagen negra (BGR)", black_image)
cv2.imshow("Imagen blanca (BGR)", white_image)
cv2.imshow("Imagen azul", blue)
cv2.imshow("Imagen verde", verde)
cv2.imshow("Imagen roja", red)

cv2.waitKey(0)
cv2.destroyAllWindows()

Línea 5: Definimos el alto y ancho de las imágenes que vamos a construir.

Línea 8: Creamos una matriz de ceros del tamaño especificado, con tipo de dato np.uint8. Esta imagen será de color negro.

Línea 11: Creamos una matriz de unos del tamaño especificado, con tipo de dato np.uint8. Esta imagen será de color blanco.

Línea 14 a 23: Para crear cada imagen, copiamos la imagen de ceros black_image y modificaremos su contenido del siguiente modo:

  • blue[:, :, :] = (255, 0, 0), solo el canal Blue tendrá el valor de 255, por lo que la imagen será azul.
  • verde[:, :, :] = (0, 255, 0), solo el canal Green tendrá el valor de 255, por lo que la imagen será verde.
  • red[:, :, :] = (0, 0, 255), solo el canal Red tendrá el valor de 255, por lo que la imagen será roja.

Obtendremos:

Pero también podemos combinar valores en cada canal, siempre y cuando estén entre 0 y 255, para obtener cualquier otro color.

¿Por qué se usa uint8?

Y por cierto, se usa uint8 porque es muy eficiente en memoria y en velocidad de procesamiento, algo que es clave cuando trabajamos con imágenes o video, especialmente en tiempo real.

5. Sistema de coordenadas que usa OpenCV

Cuando navegues por la documentación de OpenCV, encontrarás que la ubicación de los píxeles suele ser representada en coordenadas (X, Y). Y sí, OpenCV cuenta con un sistema de coordenadas para ubicar cada píxel en la imagen.

El punto de origen, es decir, el punto (0, 0), se encuentra en la esquina superior izquierda de la imagen. Desde ese punto, el eje X crece hacia la derecha y el eje Y crece hacia abajo.

Por ejemplo, si deseamos dibujar círculos sobre una imagen debemos usar la función cv2.circle(). Allí debemos especificar la ubicación del centro del círculo en (X, Y):

Entonces veamos el código para dibujar el círculo sobre la imagen:

import cv2

# Leer una imagen
image = cv2.imread("image.jpeg")

# Dibujar un círculo en la imagen
cv2.circle(image, (150, 250), 10, (0, 255, 255), -1)

# Visualizar la imagen
cv2.imshow("Imagen de entrada", image)

cv2.waitKey(0)
cv2.destroyAllWindows()

Línea 7: Primero especificamos la imagen donde se va a dibujar el círculo, luego las coordenadas X e Y. En este caso vamos a usar X igual a 150 y Y igual a 250. Después indicamos el radio, por ejemplo 10 píxeles, luego el color en formato BGR, y finalmente usamos -1 para que el círculo se rellene completamente.

De esta forma estamos usando coordenadas dentro de las funciones de OpenCV para dibujar directamente sobre la imagen.

6. El tamaño de la imagen

Una imagen es importante en el desarrollo de nuestros proyectos de visión artificial, ya que es nuestra materia prima. Pero en esta ocasión no nos vamos a centrar en su contenido, sino en su tamaño.

Y es que no es lo mismo procesar una imagen grande que una imagen pequeña.
Pensemos en esto: una imagen de 640 por 480 tiene alrededor de 307 mil píxeles, pero una imagen Full HD de 1920 por 1080 tiene más de dos millones de píxeles. Procesar una imagen tan grande puede ser bastante costoso para el procesador, y todavía más si se trata de un video, donde tenemos que procesar muchas imágenes por segundo.

Ahora bien, si tenemos una imagen muy grande, que incluso es difícil de visualizar en pantalla, podemos usar la función cv2.resize() para redimensionar su tamaño, veamos:

import cv2

# Leer una imagen
image = cv2.imread("playa.jpg")
print(image.shape)

# Redimensionar la imagen
resized_image1 = cv2.resize(image, (640, 480))
resized_image2 = cv2.resize(image, (0, 0), fx=0.5, fy=0.5)

# Visualizar la imagen
cv2.imshow("Imagen de entrada", image)
cv2.imshow("Imagen redimensionada 1", resized_image1)
cv2.imshow("Imagen redimensionada 2", resized_image2)

cv2.waitKey(0)
cv2.destroyAllWindows()

Línea 8: Usamos cv2.resize() para redimensionar la imagen a un ancho y alto específicos. Entonces en primer lugar indicamos la imagen que se desea redimensionar y luego entre paréntesis el ancho y el alto.

Línea 9: Ahora redimensionaremos la imagen a partir de un factor de escala, entonces especificamos la imagen que deseamos redimensionar, seguido de (0, 0), esto ya que no vamos a redimensionar a un ancho y alto específico. En los parámetros fx y fy asignamos 0.5 para que la imagen se redimensione a la mitad.

Eso sí, hay que tomar en cuenta que, al redimensionar una imagen, se pierden detalles, así que siempre hay que buscar un balance entre calidad y rendimiento.

¿A qué tamaño debo redimensionar una imagen?

Entonces, te estarás preguntando: ¿a qué tamaño debo redimensionar mis imágenes?
Y la respuesta es que no hay una medida universal, porque todo depende del tipo de proyecto que estés haciendo.

Por ejemplo:

  • Si necesitas que tu programa reaccione en tiempo real, porque estás capturando video en vivo, probablemente te convenga trabajar con imágenes más pequeñas y probar el rendimiento de tu computador.
  • Si en cambio estás analizando imágenes donde necesitas mucho detalle, como en reconocimiento óptico de caracteres o en imágenes médicas, tal vez no te convenga reducir el tamaño, y más bien necesites un procesador más potente.
  • Otra estrategia muy usada es capturar la imagen en alta resolución, crear una versión más pequeña para procesarla, obtener los resultados y luego dibujarlos sobre la imagen original de alta calidad.

7. Copiar vs referenciar imágenes

Este punto puede causar muchos bugs difíciles de detectar.

Si deseas crear una imagen igual a otra, y usas:

image2 = image

Al usar image = image2, no se estará creando una nueva imagen, sino una referencia a la misma matriz en memoria. Esto ocasiona que cualquier cambio en una, afectará a ambas.

Para crear una copia real se debe realizar lo siguiente:

image2 = image.copy()

Ahora sí, ambas imágenes son independientes.

Gracias por haber llegado hasta aquí. Espero que este contenido te haya sido útil. Si te quedaste con alguna duda o te gustaría que profundicemos en algún punto, puedes dejarlo en los comentarios.

Nos vemos en el siguiente artículo. 😊