miércoles, 26 de julio de 2017

Breve introducción a Python + Numpy + OpenCV

En esta entrada trataremos el tema de instalación y primeros pasos de un entorno básico para crear programas relacionados con la Visión Artificial basado en el lenguaje de programación Python y la biblioteca OpenCV.

OpenCV es una biblioteca de código abierto muy utilizada para el desarrollo de aplicaciones de visión artificial. En OpenCV encontraremos muchos algoritmos de tratamiento de imagen y de clasificación de patrones que nos servirán para crear aplicaciones de visión artificial. OpenCV está desarrollada originalmente en C++. Sin embargo, dado la dificultad de aprendizaje de C++ se ha desarrollado un recubrimiento de OpenCV para que pueda ser utilizado desde Python. En este tutorial nosotros usaremos ese recubrimiento.

Junto con OpenCV utilizaremos otras bibliotecas que forman parte del ecosistema ScyPy. ScyPy es un conjunto de bibliotecas de código abierto para construir aplicaciones matemáticas, científicas e ingenieriles.

Instalación

La instalación de Python y todas las bibliotecas necesarias la realizaremos instalando el ecosistema Anaconda. Este ecosistema facilita la instalación, la actualización  y la compatibilidad entre los diferentes componentes instalados. Hay versiones para Windows, para Mac y para Linux. Si la instalación se realiza sobre un ordenador del 2012 o posterior seguro que la versión de 64 bits es la adecuada.

Al instalar Anaconda debemos tener cuidado para elegir una versión compatible con la última versión de OpenCV. La versión de OpenCV más actual, cuando se escribió esta entrada, era OpenCV 3.2. Dicha versión de OpenCV es compatible con Python 3.X, pero no con Python 2.X. Por ello, desde aquí se recomienda instalar una versión de Anaconda basada en Python 3.X. En particular , este tutorial utiliza:
  • Python 3.6. 
  • OpenCV 3.2 
La instalación es sencilla. basta con descargar un archivo y ordenar su ejecución. Durante la instalación conviene apuntar el directorio en el que se instala Anaconda. Habitualmente, tras la instalación, Anaconda incluye en la variable PATH del sistema dicha ruta, así cuando desde un terminal se escriben comandos como “conda, anaconda o python” se abre la versión Anaconda de los mismos. En Linux y Mac este cambio se suele hacer de manera automática en el archivo local “.bashrc”. En Windows, suele instalarse una aplicación que se llama consola de Anaconda que será necesario abrir para lanzar la versión instalada de “conda, anaconda y python”.

Tras la instalación por defecto de Anaconda encontraremos ya algunas de las principales bibliotecas que usaremos para el desarrollo de aplicaciones de Visión Artificial. Por ejemplo, dispondremos de Numpy y MatPlotLib. Numpy es una biblioteca de cálculo numérico que se integra con OpenCV en numerosos aspectos. Por ejemplo, las imágenes de OpenCV se tratan como matrices de Numpy. Por otro lado, MatPlotLib es una biblioteca de presentación gráfica de resultados para Numpy que resulta útil por motivos obvios. En este tutorial se usa Numpy 1.11 y MatPlotLib 1.15, aunque Anaconda siempre instala las versiones compatibles más modernas.

Las bibliotecas que incluye Anaconda tienen interrelaciones complejas. Por ejemplo, Matplotlib tiene dependencia de las bibliotecas six, dateutil y py, pero Anaconda suele encargarse de instalar todo lo necesario sin que tengamos que ocuparnos de estos detalles.

Una vez instalado Anaconda hay que instalar OpenCV. Para instalar una biblioteca desde Anaconda hay que abrir un terminal (en el caso de Windows una consola de Anaconda) y teclear:

conda search opencv

Este comando mostrará las versiones de Opencv disponibles en los repositorios oficiales de Anaconda. Cuando se escribio este blog, OpenCV 3.2 no aparecía en los respositorios oficiales. Por ello, se utilizó el siguiente comando para buscar en los repositorios no oficiales.

anaconda search opencv

Esta vez, en el listado encontramos OpenCV 3.1 y 3.2. Entonces, hay que elegir un repositorio de Opencv 3.2 válido para nuestra plataforma. Por ejemplo, si nuestra plataforma es Linux, Windows o Mac de 64 bits, para instalar OpenCV 3.2 podemos utilizar el repositorio “conda-forge/opencv3”. Para instalar utilizando este repositorio escribiremos:

anaconda install -c conda-forge opencv3

Una vez instalado OpenCV podemos comprobar que funciona correctamente abriendo una consola de python e importando la biblioteca. Para ello, desde la consola de Anaconda tecleamos: python. En ese momento se presentará un mensaje de bienvenida a Python. En ese momento podremos teclear
los siguientes comandos. Si no se observan mensajes de error es que todo ha ido bien, pero si aparecen mensajes de error deberán leerse y entenderse para buscar una solución.

>>> import cv2
>>> import numpy as np
>>> import matplotlib

Instalación de Pycharm

Anaconda incluye por defecto Spyder y Jupyter Notebook. Ambos pueden utilizarse como entornos para desarrollar aplicaciones en Python. Sin embargo, dada su potencia, desde aquí se anima al lector a instalar y a utilizar PyCharm.

PyCharm es un IDE para programar en Python que nos permitirá programar con ciertas ayudas: autocompletar, sugerencias de nombre de variable, refactorización... Además nos permitirá depurar nuestro código de una manera muy sencilla.

La versión de PyCharm que vamos a utilizar es gratuita y se denomina Comunity Edition.
  • En GNU/Linux bastará con:
    1. Bajar el fichero “pcharm-community-X.X.tar.gz”.
    2. Descomprimir el fichero tar.gz. Podemos utilizar un terminal unix y ejecutar. Por ejemplo:
        > tar xzvf pycharm-community-4.0.4.tar.gz
    3. Ejecutar PyCharm desde la línea de comandos:
        > pycharm-community-4.0.4/bin/pycharm.sh
  • En MS Windows habrá que:
    1. Ejecutar el fichero pycharm-*.exe para arrancar el proceso de instalación.
    2. Seguir las instrucciones y prestar atención a las opciones de instalación.

Carga de imagen y presentación en pantalla

En esta sección vamos a proceder a utilizar Pyton y las bibliotecas que acabamos de instalar para cargar una imagen y presentarla en pantalla.

Desde la consola de comandos (terminal en Unix y CMD en Windows), utilizamos el comando pyhton (o mejor ipython) para abrir un intérprete de Python.

Nuestro primer programa OpenCV solo carga de disco y muestra una imagen por pantalla. Obsérvese que todas las funciones de OpenCV están dentro del paquete cv2. Tras ejecutarlo se puede pulsa ENTER sobre la ventana para terminar la ejecución.

import cv2

m = cv2.imread("avatar.jpg") #Carga en m la imagen de un fichero jpg
cv2.imshow("AvatarWindow",m) #Muestra la matriz en una ventana con nombre
cv2.waitKey() #Espera que se pulse una tecla sobre la ventana
cv2.destroyAllWindows() #Cierra todas las ventanas abiertas por OpenCV

Como la herramienta de visualización de OpenCV es muy básica, vamos a utilizar la de Matplotlib. Para ello tecleamos el siguiente código:

from matplotlib import pylab
from matplotlib import pyplot as plt

pylab.ion()
plt.imshow(m)

Como se puede apreciar, Matplotlib ofrece un sistema de presentación mucho más adecuado. Para la multitud de opciones que aporta Mtplotlib remitimos al alumno a su documentación. Un primer problema que presenta la presentación de imágenes es que supone RGB en vez e en BGR (que es el formato interno de OpenCV por defecto para imágenes en color). Para que la imagen se viese bien debería sustituirse la última línea del script anterior por:

plt.imshow(cv2.cvtColor(m,cv2.COLOR_BGR2RGB))

Creación de un proyecto OpenCV en PyCharm

Python es un lenguaje interpretado, por lo que no es necesario el paso de compilación de otros lenguajes como Java o C++. Como hemos visto, una de las formas más sencillas de hacer pruebas en pequeños fragmentos de código es utilizar directamente la consola con el intérprete de Python (python) o mejor ipython.

Sin embargo, con la ayuda de un entorno como PyCharm, también es fácil crear un proyecto para realizar programas más complejos. Cabe decir que, para pruebas rápidas, en PyCharm también tenemos acceso a una consola de Python (Tools/Windows/Python Console) que se situara en la parte inferior izquierda de la ventana de PyCharm (ver Figura 1).



Para crear un proyecto Python desde PyCharm se debe:
  1. Crear un proyecto desde el menú file.
  2. Seleccionar el intérprete 3.6.X.
  3. Elegir una carpeta vacía para la localización del proyecto (quizás creando una carpeta nueva).
  4. Con el menú de contexto, sobre el árbol del proyecto, crear un fichero llamado “Practica1.py”.
  5. Añadir al fichero recién creado la línea: print “Hola mundo”
Tras estos sencillos pasos llegamos ya podemos ejecutar nuestro primer programa mediante el menú Run/Run (ver Figura 2).


Introducción a las matrices de numpy

En Visión Artificial, una parte muy importante de los algoritmos se centra en estudiar los patrones que aparecen en las matrices que corresponden a imágenes digitalizadas. Es por ello que la matriz es el elemento central de la biblioteca OpenCV. OpenCV está programada en C++ y las matrices que maneja originalmente son exclusivas de OpenCV. Sin embargo, en el recubrimiento que se ha creado de OpenCV para Python esto no es así.

Para facilitar la interoperabilidad del módulo de OpenCV con otros paquetes (como matplotlib o scipy), las funciones de OpenCV en Python utilizan los arrays multidimensionales de Numpy. Cada uno de estos arrays es una tabla de elementos (usualmente números), todos del mismo tipo, indexados por una tupla de números positivos (filas, columnas y canal de color en el caso de imágenes en color). En adelante, por comodidad, utilizaremos el término matriz para referirnos a estos arrays multidimensionales.

Para crear una matriz que sea interpretada como imagen se debe utilizar los comandos que Numpy proporciona para crear arrays multidimensionales. El siguiente fragmento crea una matriz inicializada a cero, con 3 planos de 100x100 elementos de tipo byte (ver Figura 3).

import cv2
import numpy as np
depth=np.uint8
rows=100
cols=200
channels=3
blank_image = np.zeros((rows,cols,channels), depth)
cv2.imshow("BlackWindow",blank_image)
cv2.waitKey()

Python es un lenguaje completamente orientado a objetos con tipado implícito. Es por ello que en el ejemplo anterior la variable “blank_image” representa a un objeto de tipo array multidensional sin que se haya realizado ninguna declaración previa (las variables adquieren el tipo del objeto que se les asigna).

Funciones para creación de arrays de numpy

Numpy dispone de muchas opciones para crear una matriz a partir de listas de Python (elementos separados por comas y entre []):

# Vector fila. Dimensión (1 x 3). vec.shape = (3,)
vec = np.array([1., 2., 1.])

# Matriz (2 x 3). mat.shape = (2,3)
mat = np.array([[1., 2., 3.], [4., 5., 6.]])

Muchas veces se conocen las dimensiones de la matriz que queremos crear pero no los valores de sus elementos. En términos de imágenes, conocemos las dimensiones de la imagen en píxeles pero no los valores de gris de la misma.
Numpy nos ofrece varias formas de inicializar los arrays a partir de las dimensiones:


# Matriz rellena de ceros de 3 filas x 4 columnas.
# En este caso el tipo de los elementos es float64.
m_0 = np.zeros( (3,4) )

# Matriz rellena de unos de 2 filas x 3 columnas x 4 planos.
# En este caso el tipo de los elementos es entero de 16 bits.
m_1 = np.ones( (2,3,4), dtype=np.int16 )

# Matriz con números aleatorios de 4 filas x 3 columnas.
# En este caso el tipo de los elementos es float64
m_2 = np.random.rand(4,3)

También podemos crear matrices a partir de otras ya creadas:

# Apuntar a una parte de la matriz, desde la fila 0 a la 2 (inclusive)
# y desde la columna 1 a la 2 (inclusive)
roi = m_2[0:3, 1:3]
roi[:,:] = -1 # asignar -1 a todos los elementos de la roi en M2.

m_5 = m_2[1,:] # apunta a la parte de la matriz formada por la segunda fila.
m_6 = m_2[:,2] # apunta a la parte de la matriz formada por la tercera columna

Es interesante la existencia de algunas funciones que permiten crear una matriz de ceros (np.zeros()), de unos (np.ones()) o matriz identidad (np.eye()).

Referencias a objetos.

En Python (casi como en Java) todos las variables son referencias a objetos. Por ello, si A apunta a un objeto y hacemos B=A, lo que tenemos es que A y B apunten al mismo objeto.

a=np.zeros((10,20)) #Matriz de 10 filas y 20 columnas

b = a #Matriz b comparte memoria con matriz a

a[0,0]=23 #Al poner el elemento (0,0) a 23

cambia_b = b[0,0]==23 #vale true, porque a y b apuntan a los mismos datos

Si se desea que A y B apunten a objetos distintos (pero con los mismos valores) deberemos hacer B = A.copy().

b = a.copy() #Matriz b es igual a a, pero no comparte memoria.

a[0,0]=2 #Al poner el elemento (0,0) a 23

cambia_b = b[0,0]==2 #vale false, porque a y b apuntan a datos distintos

cambia = (b==a).all() #vale false, porque a y b apuntan a datos distintos

Cuando se desea que una referencia deje de apuntar a un objeto basta con asignarla a otro o a None. Si se desea borrar definitivamente una variable se puede utilizar la palabra reservada “del”.

Propiedades y métodos descriptivos de las matrices

A continuación se presentan algunas de las propiedades más utilizadas de los arrays numpy:
  • shape: tupla con las dimensiones de la matriz (filas, columnas, planos color)
  • ndim: número de dimensiones en la tupla shape (imágenes en grises ndims=2, y en imágenes en color ndims=3).
  • dtype: tipo de los elementos de la matriz (int8, float64, etc.)
  • “[]” para acceder a los elementos de la matriz. Por ejemplo: m[0,2]=3
  • “:” para acceder a una fila o una columna. Por ejemplo: m[:,0] =0
  • i:j:k para acceder a todos los elementos (slicing) desde el índice i al j (j no incluido) de k en k elementos. Por ejemplo: m[0:2,1:5:2] (submatriz con las filas de 0 a 1 y las columnas 1 y 3.

Tipos matrices e imágenes

Las imágenes en OpenCV con Python son arrays de numpy. Por tanto, utilizaremos las facilidades de ese paquete, incluyendo los tipos definidos en el mismo, para crear imágenes con los formatos más habituales:
  • dtype=np.uint8 y ndim=2, para imágenes en niveles de gris [0-255] o bitonales {0,255}.
i_8 = np.array([[1, 2, 3],[4, 5, 6]], dtype=np.uint8)
  • dtype=np.uint8 y ndim=3, para imágenes en color, con valores en [0-255], y con tres canales de color, imágenes con 24 bits por píxel (ver Figura 3). En OpenCV por defecto las imágenes son BGR, esto es, el primer canal es el azul, el segundo el verde y el tercero el rojo. Esto es importante cuando queremos usar funciones de OpenCV (cv2) mezcladas con las de otros paquetes como scipy o matplotlib (que necesita los canales ordenados como RGB). Para crear una imagen en color de 24 bits por píxel (3 planos de color) podemos hacer:
      i_24 = np.array([[[1, 2, 3], [4, 5, 6]],
      [[7, 8, 9], [10, 11, 12]],
      [[13, 14, 15], [16, 17, 18]]], dtype=np.uint8)

  • dtype=np.int32 y ndim=2, para matrices de enteros de 32 bits (int64 para 64 bits): 
m_int = np.array([[1, 2, 3],[4, 5, 6]], dtype=np.int32)
  • dtype=np.float32 y ndim=2, para matrices de reales de 32 bits (o float16, float64, float128):

      m_float = np.array([[1, 2, 3],[4, 5, 6]], dtype=np.float32)

Operaciones entre matrices

La clase matriz tiene una serie de métodos que permiten manipularlas. En los siguientes puntos se exploran algunos de estos métodos.

Operaciones aritméticas: Suma, resta, producto y división de matrices

Los operadores +,-,* y / están sobrecargados. De manera que siempre que 2 matrices tengan exactamente las mismas dimensiones y tipo realizar operaciones aritméticas entre ellas.

Operaciones lógicas: and y or

Los operadores |, & están sobrecargados. De manera que siempre que 2 matrices tengan exactamente las mismas dimensiones y tipo realizar operaciones aritméticas entre ellas.

Operaciones de comparación y negación

Los operadores ~, >, <, >= y <= están sobrecargados. De esta forma se pueden obtener todos los píxeles de una imagen que cumplen cierta comparación con una simple llamada a esta función.

# Devuelve una imagen con valores cierto (255) y falso (0), en la que tienen
# valor cierto los píxeles cuyo valor es superior a 200.
pixeles_con_valor_alto = (imagen_gris > 200) #Máscara true false
pixeles_con_valor_alto = pixeles_con_valor_alto * 255 #Mascará en rango [0, 255]

Operaciones básicas de dibujo

OpenCV dispone de las siguientes funciones para realizar dibujos de líneas y áreas:
  • line.- Dibuja lineas.
  • rectangle.- Dibujar rectángulos.
  • circle.- Dibuja círculos.
  • ellipse.- Dibuja elipses.
  • polylines.- Dibuja lineas poligonales.
  • fillpoly.- Dibuja polígonos rellenos.
  • putText.- Dibuja textos sobre las imágenes.

Otros tipos de datos

Además de las matrices, cuando se programa en Python se suelen manejar los siguientes tipos de datos:
  • String.- Se puede crear utilizando comillas simples o dobles. El acceso a los elementos de un String se realiza utilizando los corchetes y permitiendo los dos puntos para definir subrangos. En los Strings se permiten caracteres de escape similares a los de Java y C. También se permite el uso del % para formatear números desde variables. Las triples comillas se utilizan para definir Strings de varias líneas. String, como clase tiene multitud de métodos útiles (len, max, isnumeric, capitalize...). Además hay muchas funciones core que operan sobre Strings.
    S = “Hola”
  • List.- Se crea utilizando corchetes. Como clase tiene multitud de métodos útiles (sort, count, pop, remove, reverse, insert, append). La compresión de listas es una forma eficiente de definir listas (squares = [x**2 for x in range(10)])
    l = [1, 2, 5, -3]
  • Tuple.- Las tuplas son listas inmutables. Se definen utilizando paréntesis. Por ejemplo:
    t = (5,2)
    Se pueden definir tuplas de un número ilimitado de elementos. Por ejemplo:
    • (x,y) = (3,2)
    • a,b,c = (“a”,”b”,”c”)
    • t = (3,2,1,2,1,23,4,45,23,2)
    También se pueden definir tuplas mixtas, en las que cada elemento es de un tipo. Por ejemplo:

    t = (5,2,”Hola”,x)
    Incluso se pueden definir tuplas de un solo elemento, aunque en este caso hay que añadir una coma al final para distinguirla de una expresión matemática común. Por ejemplo a = (2,)
  • Set.- Es una colección de elementos no duplicados en los que el programador no controla el orden. Su uso básico incluye: inserción, pertenencia, eliminación, unión, intersección y diferencia. Al igual que las listas también soporta compresión. Para su declaración se pueden usar las llaves o el constructor “set”.
    cartera = {“lapiz”,”cuaderno”,”papel”,”Libro}
  • Dictionary.- Python también permite la declaración de diccionarios en los que se insertan pares clave valor. Las claves deben ser objetos inmutables. Al igual que en los Sets, en la creación de diccionarios también se utilizan las llaves, aunque en este caso los elementos se componen de dos partes, la clave y el valor, separados por dos puntos.
    agenda = {“Jose”:916512123,”Andres”:916198212, “Emergencias”:112}

Tipado opcional

Como se habrá observado, el tipado en Python es implicito. Esto quiere decir que todas las variables se asignan a objetos que tienen un tipo concreto. Dicho tipo determina las operaciones que se pueden realizar sobre el objeto.
Sin embargo, recientemente (desde la versión 3.5) se ha incorporado al lenguaje la posibilidad de anotar el tipo de las variables que se definan. Esta característica es especialmente recomendada, ya que aporta mucha facilidad para entender el código. Además facilita a los editores la posibilidad de mostrar las posibles acciones a realizar sobre los objetos. Las siguientes lineas muestran algunos ejemplos:

def factorial(valor : int) -> int :
if valor == 0:
return 1
return valor * factorial(valor - 1)

En el caso de tipos complejos (como las listas) se ha incluido el paquete typing que es necesario incluir.

a = [] : List[int]
a.append(3)

Instrucciones de control en Python

Quizás la característica más diferenciadora de Python respecto a otros lenguajes es que la indentación es obligatoria para definir el ámbito de las instrucciones de control. Mientras en Pascal se utilizan las palabras begin y end, y en C se utilizan las llaves {}, en Python se utiliza el indentado. Por ello hay que cuidarlo especialmente, ya que de otra forma el programa dará error.
Python dispone de la habitual sentencia if.

if value < 0 :
print "The value is negative."

En Python también se pueden utilizar la habitual instrucción while en una forma muy similar a la de otros lenguajes.

while i <= 100:
print i

En Python, la instrucción for se utiliza para iterar sobre listas. El siguiente fragmento muestra un ejemplo típico:

string = "Hello World"
for x in string:
print x

Para usar for de la manera en que se hace en otros lenguajes, Python aporta la función xrange. Esta función crea un generador de números sobre el que se puede iterar:

for i in xrange( 1, 11 ) : # desde 1 hasta 10
print i

Declaración de funciones

Aunque Python en un lenguaje en el que todo son objetos, es posible definir funciones sin más que declararlas. Estas funciones no pertenecerán a ningún objeto, pero son objetos en si mismas.
La indentación también es obligatoria para definir el ámbito de las funciones.

def sum_range(first, last):
total = 0
i = first
while i <= last:
total = total + i
i += 1
return total

Python tiene funciones como filter, map y reduce que permiten una programación cercana al paradigma funcional. El siguiente fragmento de código ilustra este aspecto.

def f(x): return x % 3 == 0 or x % 5 == 0
...
>>> filter(f, xrange(2, 25))
[3, 5, 6, 9, 10, 12, 15, 18, 20, 21, 24]

Creación de clases

La creación de clases en Python es similar a la que encontramos en otros lenguajes. Como características destacables podemos destacar:
  • Es obligatorio el uso de self para hacer referencia a las propiedades de la clase. En otro caso se consideran variables locales al método.
  • En la declaración de métodos es obligatorio el paso de self como primer parámetro. Sin embargo, en la invocación no hace falta pasar dicho parámetro, pues la sintaxis es “objeto.método(parámetros)”
  • Si una propiedad de clase comienza por barra baja se considera privada.
  • De nuevo, la indentación es obligatoria para definir el ámbito de las clases.
  • El constructor se llama __init__

class Point:
def __init__(self, x = 0, y = 0):
self.x_coord = x
self.y_coord = y

def __str__(self):
return "(" + str(self.y_coord) + ", " +
str(self.y_coord) + ")"

def get_x(self):
return self.x_coord

def get_y(self):
return self.y_coord

def shift(self, x_inc, y_inc):
self.x_coord += x_inc
self.y_coord += y_inc


Estilo del código

En Python se utilizan las siguientes reglas de estilo:
  • Las clases utilizan la notación CamelCase.
  • Los nombre de ficheros, variables, propiedades, funciones y métodos se escriben en minúsculas separadas por barras bajas. Por ejemplo: nombre_metodo, nombre_variable, nombre_fichero.
  • Las constantes se escriben en mayúsculas. Por ejemplo: PI, HEIGHT.
  • Los comentarios que explican lo que hace una función se escriben dentro de la función justo tras la cabecera. Para este tipo de comentario en vez de utilizar el carácter de comentario en línea se utiliza el marcador de cadena multilínea (triples comillas).

def factorial(valor : int) -> int :
"""
Función que calcula el factorial de un número.
"""
if valor == 0:
return 1
return valor * factorial(valor - 1)

  • Se ponen espacios antes y después de los operadores.
  • Se pone espacio detrás de las comas.

Bibliografia