pctroll { jorge palacios }

desarrollador de juegos, programador, investigador de IA

Postgrado: Reconocimiento de Iris con Python y OpenCV

Este trimestre inscribí la materia Visión Artificial (Computer Vision) y la primera asignación fue leer un paper de biometría, el cual teníamos que implementar hasta cierto punto.

En caso que les de fastidio descargar y leer el paper, la actividad consistió en tomar un puñado de imágenes de ojos y de ellas poder aislar el iris. ¿Motivo? El iris tiene propiedades únicas de cada persona y pueden servir como objetos de identificación, así como las huellas dactilares (eso sale mejor explicado en el paper -léanlo-).

Como comenté antes, la idea era llegar hasta cierto punto y fue la “normalización” del mismo. En el conexto del paper, se entiende como normalización del iris la transformación de las coordenadas polares de la imagen del ojo, a coordenadas en el plano Cartesiano. Esto es, transformar la circunferencia entre la pupila y el iris, y hacer una cinta. De esa forma se obtiene el mapa del iris que funcionará como la huella dactilar de un sistema de identificación, por ejemplo (¿vieron recientemente The Avengers? entonces entienden el punto ;)).

Al final, la cosa quedó dividida en 4 grandes pasos:

  1. Detectar la pupila y el centro de ella.
  2. Detectar el Iris y aislarlo.
  3. Retirar los restos de párpado.
  4. Normalizar la imagen.

Detectar la pupila

Con ayuda de las funciones InRangeS, FindContours pude detectar la pupila. InRangeS hace una división binaria de los píxeles. Si están en el rango especificado, los pinta de blanco; sino, los pinta de negro. Al aplicar FindContours, obtengo todos los contornos a partir de la imagen binaria anterior.

pupila y centro

Pupila con máscara y centro visible

Como las pestañas pueden estar dentro de ése rango de valor (negro a gris oscuro), yo voy a tomar el contorno cuya área sea mayor a 50 y lo tomo como la pupila. Calculo el centro y pinto todo el contorno de negro (sin considerar los espacios vacíos que quedan a causa de los led de la cámara).

def getPupil(frame):
	pupilImg = cv.CreateImage(cv.GetSize(frame), 8, 1)
	cv.InRangeS(frame, (30,30,30), (80,80,80), pupilImg)
	contours = cv.FindContours(pupilImg, cv.CreateMemStorage(0), mode = cv.CV_RETR_EXTERNAL)
	del pupilImg
	pupilImg = cv.CloneImage(frame)
	while contours:
		moments = cv.Moments(contours)
		area = cv.GetCentralMoment(moments,0,0)
		if (area > 50):
			pupilArea = area
			x = cv.GetSpatialMoment(moments,1,0)/area
			y = cv.GetSpatialMoment(moments,0,1)/area
			pupil = contours
			global centroid
			centroid = (int(x),int(y))
			cv.DrawContours(pupilImg, pupil, (0,0,0), (0,0,0), 2, cv.CV_FILLED)
			break
		contours = contours.h_next()
	return (pupilImg)

Detectar el iris

borde del iris

Detección del borde con transformada de Hough

Aquí estaba el punto más importante de la asignación, creo. Para encontrar el iris se aplica la transformada de Hough con HoughCircles, pero previamente se aplica la función Canny a fin de encontrar filos. A partir de los filos es que trabaja la transformada de Hough. Sin embargo, puede que la función no tenga la precisión que queremos. Para ese caso, utilizo el radio del resultado de la transformada, aplicado al centro de la pupila que obtuve en el paso anterior.

El truco, como me explicaron en StackOverflow, es probar con varios valores para el parámetro de umbral acumulador (accumulator threshold), que varía para cada imagen. Este valor es clave; mientras más pequeño, más probabilidades de tener falsos círculos. Lo que hice fue “ayudar” a la función indicando los valores mínimos y máximos de radio de círculos a buscar y crear una función que va probando con varios acumuladores hasta que el número de círculos encontrados es uno.

iris aislado

Aislamiento con máscara de círculo

De ahí aislo la pupila creando una máscara con el círculo obtenido y aplicando NOT sobre la imagen. Y finalmente tomo la región de interés con SetImageROI creando un cuadrado con el centro y el radio calculados anteriormente.

def getIris(frame):
	iris = []
	copyImg = cv.CloneImage(frame)
	resImg = cv.CloneImage(frame)
	grayImg = cv.CreateImage(cv.GetSize(frame), 8, 1)
	mask = cv.CreateImage(cv.GetSize(frame), 8, 1)
	storage = cv.CreateMat(frame.width, 1, cv.CV_32FC3)
	cv.CvtColor(frame,grayImg,cv.CV_BGR2GRAY)
	cv.Canny(grayImg, grayImg, 5, 70, 3)
	cv.Smooth(grayImg,grayImg,cv.CV_GAUSSIAN, 7, 7)
	circles = getCircles(grayImg)
	iris.append(resImg)
	for circle in circles:
		rad = int(circle[0][2])
		global radius
		radius = rad
		cv.Circle(mask, centroid, rad, cv.CV_RGB(255,255,255), cv.CV_FILLED)
		cv.Not(mask,mask)
		cv.Sub(frame,copyImg,resImg,mask)
		x = int(centroid[0] - rad)
		y = int(centroid[1] - rad)
		w = int(rad * 2)
		h = w
		cv.SetImageROI(resImg, (x,y,w,h))
		cropImg = cv.CreateImage((w,h), 8, 3)
		cv.Copy(resImg,cropImg)
		cv.ResetImageROI(resImg)
		return(cropImg)
	return (resImg)

# Aqui esta el "truco" de ir buscando el acumulador para cada ojo y
# regresar el circulo cuando solo exista uno.
def getCircles(image):
	i = 80
	while i < 151:
		storage = cv.CreateMat(image.width, 1, cv.CV_32FC3)
		cv.HoughCircles(image, storage, cv.CV_HOUGH_GRADIENT, 2, 100.0, 30, i, 100, 140)
		circles = np.asarray(storage)
		if (len(circles) == 1):
			return circles
		i +=1
	return ([])

Retirar los restos del párpado

aislar iris de los párpados

Ejemplo de la idea en Photoshop®

Esto sí me costó un poco más y la verdad no me senté a terminar de implementarlo porque quería trabajar en otras cosas. La idea que tengo es aplicar de nuevo la transformada con otros valores de acumulador y de radio mínimo y máximo para encontrar a lo sumo 3 círculos (el que ya tengo, más los 2 para los párpados), y aplicar AND sobre las máscaras de los mismos, a fin de obtener una máscara final que me elimine los párpados.

No parece una idea agarrada de los pelos. Para ello, sería ideal parametrizar la función getCircles y ajustar con unas pocas pruebas los valores de entrada.

Normalizar la imagen

Aquí no hubo mucho que hacer, más que sentarme un rato a buscar la forma de hacerlo y encontrar la función LogPolar. La función ya hace casi todo el trabajo a partir de entregarle el centro y ajustar la magnitud. Lo que me quedó por hacer es aislar el rectángulo del iris como región de interés, pero realmente no pude encontrar la relación existente entre el valor de magnitud y el mapeo de coordenadas en la imagen resultante.

def getPolar2CartImg(image, rad):
	imgSize = cv.GetSize(image)
	c = (float(imgSize[0]/2.0), float(imgSize[1]/2.0))
	imgRes = cv.CreateImage((rad*3, int(360)), 8, 3)
	cv.LogPolar(image,imgRes,c,60.0, cv.CV_INTER_LINEAR+cv.CV_WARP_FILL_OUTLIERS)
	return (imgRes)
resultado final

Resultado final: Imagen delimitada por el radio del iris y luego normalizada.

Eso es básicamente lo que hice. Adicionalmente, todo el programa corre en un ciclo que hace pausa y va iterando entre todas las imágenes del directorio; pero eso es más carpintería que otra cosa, aunque ayuda a corroborar que la función que busca el acumulador está funcionando . Lo pueden ver a detalle en el código fuente.

En GitHub está el repositorio con el código, las imágenes y el informe. En el informe se explica con un poco más de detalle, pero tampoco es mucha la diferencia porque la entrada la elaboré a partir del mismo. Sólo que aquí no escribo tan formal.

Ahora, lo importante de la asignación (además de mejorar mis habilidades con Python y esquematización de un pipeline para el procesamiento de imágenes), es que me dio la oportunidad de hacer algo distinto a lo que normalmente estoy acostumbrado a hacer. Sí, OpenCV ya tiene un montón de funciones implementadas y eso alivia un poco la carga de código. Sin embargo, hay que saber qué hace la función por detrás para saber qué significan los parámetros de la misma y cómo eso influirá en el resultado de lo que obtendré.

El mayor aprendizaje que me está dando la materia es que hay que tener claro qué se quiere obtener a fin de poder dar con los pasos necesarios para llegar a ello. Pareciera obvio, pero creo que a todos nos pasa que tenemos una idea de lo que hay que hacer y a mitad de camino perdemos el rumbo; pudiendo terminar implementando cosas que no hacían falta o que nos hacía falta implementar cosas que no habíamos considerado, por ejemplo.

La próxima asignación será clasificación de hojas, y por supuesto iré montando los avances en el repositorio (es el mismo, computer-vision) y por Twitter con algún screenshot.

Imagen de previsualización de YouTube

2 Comentarios

  1. exelente tutorial, gracias por compartir tus conocimientos

  2. buen material, se agradece

Deja un comentario