¡Saludos, estudiantes!. Continuando con nuestro (somerísimo) repaso de las posibilidades de Open CV en Robótica, hoy quiero hablaros de las transformaciones y de las máscaras. Así, en general. A lo loco. Como si supiéramos de algo.
Y es que éste es un mundo muy, muy complejo, que implica el trabajo con Matemáticas de alto nivel. Y esta humilde web es de un aprendiz para aprendices. Yo os voy explicando lo que voy descubriendo, y qué conclusiones saco, pero eso no quiere decir que tenga necesariamente razón. Así que es mi deber avisarte que todo lo que estás viendo en esta serie de artículos es siempre una aproximación (en muchos casos, hasta grosera) a las poderosas posibilidades que ofrece esta librería.
Pero dejémonos de monsergas: hoy quiero hablarte de filtros y máscaras. Para ello, es necesaria una primera explicación: Open CV es una herramienta matemática que trata a las imágenes como matrices de píxeles. Permite que utilice una imagen para ilustrarte:
Esta foto está en escala de grises. A cada píxel le corresponde un valor de 0 a 255 que indica la luminosidad: 0 para negro, 255 para blanco. Si queremos imágenes más complejas, necesitamos añadir varias matrices, una para cada canal. Por ejemplo, si queremos trabajar con colores RGB e indicar el canal Alfa:
Lo que hace la librería Open CV es tratar matemáticamente estos conjuntos de valores. Por ejemplo,supongamos que cargamos una foto llamada fotoAntonio.jpg (recuerda que debe estar en el mismo directorio que Python, por defecto en Windows C:/Python27/) y la pasamos por una ventana que vamos a titular, en mi honor, Antonio:
import cv2
import numpy as np
foto=cv2.imread('fotoAntonio.jpg')
cv2.imshow('Antonio',foto)
while(1):
k = cv2.waitKey(5) & 0xFF
if k == 27:
break
cv2.destroyAllWindows()
Una imagen en color RGB (Red, Green, Blue) es descompuesta en la mezcla de tres matrices de píxeles que indican en cada uno la cantidad de color rojo, verde o azul que se detecta en cada punto:
Una de las posibilidades de Open Cv es, por ejemplo, simplificar la imagen convirtiendo estas ternas de valores en un solo valor de luminosidad (es decir, pasamos la imagen a escala de grises):
import cv2
import numpy as np
foto=cv2.imread('fotoAntonio.jpg')
fotoGris=cv2.cvtColor(foto,cv2.COLOR_RGB2GRAY)
cv2.imshow("Antonio en Blanco y Negro",fotoGris)
cv2.imshow('Antonio',foto)
while(1):
k = cv2.waitKey(5) & 0xFF
if k == 27:
break
cv2.destroyAllWindows()
Como ya mencionaba en el artículo anterior, trabajamos con objetos: uno es foto, que es la imagen cargada del disco duro; otra es FotoGris, que es el objeto obtenido después de haber transformado el modo en que Python va a mostrar el color de Foto. Es por eso por lo que puedo mostrar dos ventanas con el procedimiento imshow().
Vamos ahora con los filtros. Sabiendo que cada píxel es un conjunto de valores enteros, es más fácil entender como funciona, por ejemplo, threshold (umbral):
import cv2
import numpy as np
foto=cv2.imread('fotoAntonio.jpg')
fotoGris=cv2.cvtColor(foto,cv2.COLOR_RGB2GRAY)
ret,filtro=cv2.threshold(fotoGris,200,255,cv2.THRESH_BINARY)
cv2.imshow("Antonio en Blanco y Negro",filtro)
cv2.imshow('Antonio',foto)
while(1):
k = cv2.waitKey(5) & 0xFF
if k == 27:
break
cv2.destroyAllWindows()
En las dos líneas que he resaltado, verás que la diferencia con el código anterior ha sido asignar al par de objetos ret y filtro el resultado de aplicar el filtro threshold al objeto fotoGris. Aprovechando que ahora sólo tenemos una matriz de valores de luminosidad para cada píxel, este procedimiento va buscando todos aquellos valores que sobrepasen el valor 200, asignándoles directamente el valor 255 (puedes cambiar ambos valores). El resultado es que los puntos que superen esa luminosidad se dibujarán directamente en blanco en filtro y ret.
Umm... Quizás me haya pasado con el filtro. La banda negra de arriba no me gusta. Voy a bajar el valor de filtro de luminosidad a 200 a 150, a ver qué pasa:
import cv2
import numpy as np
foto=cv2.imread('fotoAntonio.jpg')
fotoGris=cv2.cvtColor(foto,cv2.COLOR_RGB2GRAY)
ret,filtro=cv2.threshold(fotoGris,200,150,cv2.THRESH_BINARY)
cv2.imshow("Antonio en Blanco y Negro",filtro)
cv2.imshow('Antonio',foto)
while(1):
k = cv2.waitKey(5) & 0xFF
if k == 27:
break
cv2.destroyAllWindows()
A ver ahora...
¡Mucho mejor, hombre!. ¡Dónde va a parar!. Hablemos ahora de las operaciones booleanas. Voy a utilizar el objeto filtro como una máscara que me guíe para recortar las zonas en blanco (el cielo, la mitad de mi cara y un par de rayas de mi jersey). En esa zona, aparecerá parte del logotipo de mi IES, el Eduardo Valencia de Calzada de Calatrava.
En primer lugar, debo tener en cuenta que si voy a sumar matrices de puntos, éstas deben tener el mismo tamaño. Eso quiere decir que debo asegurarme de que tengan exactamente las mismas dimensiones, lo que no suele ser fácil, o bien hacer que el programa adapte el tamaño del archivo mayor (en mi caso, el del logo de mi centro) al más pequeño. Las palabras mágicas serán:
foto=cv2.imread('fotoAntonio.jpg')
logo=cv2.imread('logo.jpg')
rows,cols,channels = foto.shape
roi = logo[0:rows, 0:cols ]
Como el objeto foto es más pequeño que el objeto logo (576*768 frente a 1024*800) utilizamos el procedimiento shape para obtener el número de filas, columnas y canales del primero, de modo que transformamos dichas filas y canales del segundo para que se adapten a las del primero. No utilizaremos channels.
Para que veas la diferencia de tamaños, fíjate en este código:
import cv2
import numpy as np
foto=cv2.imread('fotoAntonio.jpg')
logo=cv2.imread('logo.jpg')
rows,cols,channels = foto.shape
roi = logo[0:rows, 0:cols ]
cv2.imshow("Antonio",foto)
cv2.imshow("Logo original",logo)
cv2.imshow("Logo adaptado", roi)
while(1):
k = cv2.waitKey(5) & 0xFF
if k == 27:
break
cv2.destroyAllWindows()
Y fíjate como la ventana "Logo adaptado" tiene el mismo tamaño que "Antonio" (aunque a costa de comernos parte de la imagen; otra cosa sería redimensionar su contenido, pero no es nuestro propósito):
Vamos a explicar ahora lo que queremos hacer, para no aburrir demasiado con el código y poner sólo la solución final. Utilizaremos las expresiones bitwise_and, bitwise_not (booleanas ambas) y add:
- El procedimiento add SUMA DIRECTAMENTE LOS PÍXELES DE LAS IMÁGENES QUE INDIQUEMOS. Eso quiere decir que iremos poniendo un píxel encima de otro para cada posición de la matriz (en uno o varios canales).
- La operación bitwise_and compara punto por punto los píxeles de dos imágenes y pondrá un 1 en cada posición si existe un 1 en ambos píxeles de origen.
- La operación bitwise_not toma píxel por píxel de la imagen que indiquemos y cambia los 1 por 0 y viceversa.
El código que vamos a probar hará lo siguiente:
- Carga los archivos "fotoAntonio.jpg" y "logo.jpg" (ambos, repito, en el mismo directorio que el ejecutable Python) y se los asigna a los objetos foto y logo.
- El procedimiento shape mide las filas y columnas del objeto más pequeño (foto) y recorta logo al mismo tamaño, asignándoselo al objeto roi
- Mediante cvtColor creamos fotoGris a la que aplicamos el procedimiento threshold (asignado al objeto filtro)
- filtro es una matriz de 0 (negro) y 1 (255,blanco). Mediante la operación bitwise_not crearemos el objeto filtroInverso que contendrá los valores contrarios.
- La operación bitwise_and con el objeto foto y el objeto filtro como máscara recortará la parte más blanca que hemos conseguido antes de mi foto. Es decir, esa zona desaparecerá de la foto. Asignamos el resultado a fotoRecortada.
- La operación bitwise_and con el objeto roi y el objeto filtroInverso como máscara recortará del logo transformado la zona contraria y se lo asignará a logoRecortado.
- Tengo ahora dos imágenes complementarias. El objeto fotoFinal las funde mediante la operación cv2.add()
Éste es el código:
import cv2
import numpy as np
foto=cv2.imread('fotoAntonio.jpg')
logo=cv2.imread('logo.jpg')
rows,cols,channels = foto.shape
roi = logo[0:rows, 0:cols ]
fotoGris=cv2.cvtColor(foto,cv2.COLOR_RGB2GRAY)
ret,filtro=cv2.threshold(fotoGris,100,255,cv2.THRESH_BINARY)
filtroInvertido=cv2.bitwise_not(filtro)
fotoRecortada=cv2.bitwise_and(foto,foto,mask=filtroInvertido)
logoRecortado=cv2.bitwise_and(roi,roi,mask=filtro)
fotoFinal=cv2.add(fotoRecortada,logoRecortado)
cv2.imshow("Antonio en Blanco y Negro",filtro)
cv2.imshow('Antonio',fotoFinal)
while(1):
k = cv2.waitKey(5) & 0xFF
if k == 27:
break
cv2.destroyAllWindows()
Y éste sería el resultado final:
Como decía, aunque chapuceramente (como pasa siempre en los comienzos), hemos aprendido a utilizar filtros, a comprender el enfoque matricial que Open CV hace de nuestras imágenes, a utilizar dichos filtros como máscaras y a recortar imágenes siguiendo dichas máscaras como patrón. En el próximo artículo, volveremos al vídeo y hablaremos de la detección de movimiento mediante distintas técnicas, que se basarán en parte en lo que comentamos hoy. ¡Eso es todo, amigos makers!. ¡Seguid disfrutando!. ¡Seguid creando!