PROYECTO INTEGRADOR DE VISIÓN ARTIFICIAL-COMPLEMENTOS DE INFORMÁTICA SISTEMA DE VISIÓN PARA EL RECONOCIMINETO DE MATRICULAS ALUMNO: MORENO LUCAS FEDERICO REG: 22589 AÑO:2013 Introducción El presente trabajo usa las potencialidades del lenguaje de programación de alto nivel C++ y las librerías de OpenCV (para el tratamiento de imágenes), para reconocer matriculas de vehículos a partir de fotografías debidamente tomadas, seguido luego de un sistema de reconocimiento óptico de caracteres (OCR) para reconocer el dominio de la matriucla. Este programa es capaz de reconocer patentes de automóviles, mediante la búsqueda de contornos del algoritmo de Canny, junto con otras herramientas presentes en las librerías de OpenCV. Por ende puede ser implementado sobre cualquier tipo de patente, sin embargo aquí el desarrollo está realizado sobre patentes argentinas. Para el reconocimiento de caracteres se ha utilizado el software “tesseract”, desarrollado por Hewlett Packard en 1995. Actualmente continúa su desarrollo Google y lo distribuye bajo licencia Apache. Organización del programa El programa consta de dos clases, una de ellas realiza el pre-procesamiento de la imagen de entrada, llamada “PreProcess” y la clase “Regions” tiene todos los métodos necesarios para trabajar con contornos. La interface de la clase “PreProcess” está organizada en: El constructor de esta clase está declarado como: PreProcess::PreProcess(const cv::Mat img_input, double canyThresh ,cv::Size blurSize, Size resize) ,donde “Mat img_input” representa la imagen de entrada ingresada en línea de comandos, “double canyThresh” es el valor de histéresis inferior utilizado por el algoritmo de Canny, “cv::Size blurSize” indica el tamaño de la ventana, y por último “resize” indica el tamaño a la cual se redimensionará la imagen de entrada. Esta clase no tiene un constructor sin parámetros, dado que los atributos que son creados con la llamada al constructor, son utilzados frozosamente en el resto del programa. Los métodos Show(), y Write(), permiten mostrar y guardar las imágenes pre-procesadas. La interface de la clase “Regions” está organizada en : Donde se dispone solo de un constructor sin parámetros de la forma Regions::Regions() Desarrollo del programa El programa puede ser subdividido en tres etapas, las cuales permitirán obtener los caracteres de una patente válida. La primera etapa, se encarga de realizar un tratamiento de imagen, lo cual permite obtener un mejor gradiente de bordes, necesario para potenciar la capacidad de búsqueda de bordes en la etapa siguiente, tal como se muestra en figura 1. La segunda etapa consiste en la búsqueda de contornos mediante el algoritmo de Canny, estos contornos luego serán guardados en una clase vector, que serán tratados con posterioridad para su clasificación según las dimensiones de una patente prototipo asignada en el programa. La última etapa, se encarga de obtener la imagen a la cual pertenece dicho contorno o bien aquella que está contenida en este, para luego ser clasificada en una imagen de patente valida o no, previo a un entrenamiento realizado con anterioridad. Lo anteriormente expuesto, se encuentra esquematizado de la siguiente manera. Figura 1 1ra Etapa Como se mencionó anteriormente, la primera etapa del programa es la encargada del procesamiento de la imagen de entrada, la cual a su vez esta subdividida en: Figura 2 A) Adquisición de imagen: En este paso se ha utilizado la función cv::imread(argv[1]) ,donde argv[1] es una cadena de caracteres e indica la ruta de archivo de la imagen del automóvil. Para esto en el programa principal se ha usado: // //Open file if (!argv[1]) { return 1; } else { try { img=cv::imread(argv[1]); //imshow("img",img); } catch(...) { cout<<"Existió un error durante la apertura del archivo"<<endl; } } PreProcess imgPrep(img); Se utiliza la sentencia Try en este caso para capturar y advertir cualquier error que pudiese ocurrir durante la apertura de la imagen. Las imágenes en este caso son fotografías de automóviles obtenidas de la red, algunos ejemplos de ellas son: Figura 3 B) Conversión de escala de grises: En primer lugar la imagen es convertida a escala de grises dentro del constructor “PreProcess” de la siguiente manera; cv::cvtColor(img_resize,img_gray,CV_RGB2GRAY) ,donde el último parámetro indica el tipo de conversión a realizar, e ”img_gray” es la imagen de entrada convertida en grises. En el resto del programa se trabaja con la imagen tratada en escala de grises, dado que implica una menor cantidad de cálculo, y además reduce la complejidad del programa. C) Ecualización de la imagen: Debido a los distintos niveles de intensidad presentes en una imagen, se ecualiza su histograma, en este caso sobre “img_gray” (obtenida del paso anterior) para esto se utiliza la función; cv::equalizeHist(img_gray,img_equ) , donde “img_equ”, es la imagen de destino. D) Filtro de Gauss: En este último paso se ha realizado un “Gaussian Filter”, para reducir elevados gradientes de intensidad, en otras palabras se reduce el ruido presente en la imagen. Para esto utilizamos la función; cv::GaussianBlur(img_equ,img_blur,blurSize,0,0,1) , donde “img_blur” es la imagen filtrada, “blurSize” indica el tamaño de Kernel o ventana, donde se utiliza una ventana de “3x3”, tamaño obtenido mediante prueba y error. 2da Etapa Aquí se busca encontrar todos los contornos presentes en la imagen, que luego permitirá encontrar la patente sobre la imagen. Figura 4 A) Búsqueda de contornos. En este apartado y como última función utilizada dentro del constructor “PreProcess”, se utiliza la búsqueda de contornos con el algoritmo de Canny, utilizando la función; cv::Canny(img_equ, img_canny, canyThresh, canyThresh*5, 3 ) , donde “img_canny” es la imagen de salida, canyThresh es el valor de histéresis inferior utilizado para el cálculo del algoritmo, el cual es proporcionado por el constructor como parámetro, y el tercero y último parámetro indican el límite superior de histéresis y el tamaño de la venta del operador de Sobel respectivamente. Estos parámetros se obtuvieron mediante prueba y error con distintos tipos de imágenes. B) Clasificación El arreglo de contornos es obtenido en el programa principal “main()”, utilizando la función cv::findContours(imgPrep.img_canny, contours, hierarchy ,CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) ), ,donde “imgPrep.img_canny” es la imagen de bordes contenida en el objeto “imgPrep” creado con anterioridad, “contours” es un arreglo de arrelgos de la clase vector que almacenan datos del tipo cv::Point, donde quedarán almacenadas las coordenas “x” e “y” del los puntos que constituyen el contorno, “hierarchy” es un arreglo de vectores de 4 dimensiones enteros, que en el presente programa no se utiliza, “CV_RETR_LIST” es una constante entera e indica que todos los contornos son retornados sin alguna relación con el arreglo hierarchy, “CV_CHAIN_APPROX_NONE” le indica a la función que debe almacenar todos los puntos “cv::Points”, del contorno y por ultimo “Point(0, 0)”, indica que no se ha considerado en este caso offset alguno para los puntos del contorno. Una vez obtenidos los contornos se procede a clasificarlos, para esto se realiza en el programa principal lo siguiente; for( int i=0; i<(int) contours.size(); i++ ) { RotatedRect rotRect= minAreaRect(contours[i]); Regions reg; if(reg.Check(rotRect,i)) { vrotRect.push_back(rotRect); cRegions.push_back(reg); } } Es decir, mediante este bucle intentamos aquí chequear, o bien verificar (con dimensiones preestablecidas en el programa) los tamaños de los contornos obtenidos en la etapa anterior. Aquí “vrotRect” es un arreglo de objetos de la clase “RotatedRect” que almacena las dimensiones de los contornos, y por ultimo “cRegions” es un arreglo de clase “Regions” que luego permitirá trabajar con cada contorno por separado. Para la clasificación en sí, se llama la función Check(rotRect,i) del objeto reg de la clase “Regions”, el cual utiliza como parámetro el tamaño mínimo posible de un rectángulo, que encierra al contorno dado, lo cual se realiza mediante la función “cv::minAreaRect” que devuelve un objeto de la clase RotatedRect. En esta función check(), la clasificación se realiza de la forma; if ( rotRect.size.height < _teoricHeight*_factorPlate >_teoricHeight/_factorPlate) && rotRect.size.height if (rotRect.size.width < _teoricWidth*_factorPlate >_teoricWidth/_factorPlate) && rotRect.size.width check=true; donde “_teoricHeight”,” “_teoricWidth”,”_ factorPlate”, son atributos privados de la clase “Regions”, definidos por defecto cuando se llama al constructor de la clase y son dimensiones estimativas de una matrícula prototipo sobre la imagen del automóvil en un caso ideal, y “_factorPlate”, es un factor de tamaño para considerar un amplio rango de posibles contornos. Estos parámetros son inicializados en la llamada al constructor como: Regions::Regions() { _teoricHeight=90; _teoricWidth=60; _factorPlate=4; } Recortes: En este paso se realiza un corte sobre la imagen de entrada para cada uno de los contornos verificados, en otras palabras, se obtiene la imagen contenida para este contorno a partir de la imagen original. Para esto se utiliza el método Cut de la clase “Regions” que utiliza la función “cv::getRotationMatrix2D” para obtener el grado de rotación del rectángulo contenido en el contorno, obteniendo como resultado una matriz de rotación homogénea, luego mediante “cv::warpAffine”, se obtiene la imagen de entrada rotada que luego podrá ser cortada mediante la función “cv::getRectSubPix” y luego redimensionada al tamaño indicado por el último parámetro de la función “Cut”. Esta función es llamada desde el programa principal como: for(int i=0;i<(int)cRegions.size();i++) { Mat img_chkregion; img_chkregion = cRegions[i].Cut(imgPrep.img_blur,vrotRect[i]); img_chkregion = cRegions[i].Adjust(img_chkregion,3); cRegions[i].Write(argv[1],i,img_chkregion); } Ajuste de intensidad La función “Adjust” de la clase “Regions” hace un ajuste de contraste de la imagen entrada, donde se incorpora más intensidad a los colores claros y se reducen los de menor intensidad. Esta función está definida como: cv::Mat Regions::Adjust(cv::Mat img_input, int itr ) { Mat img_adj = Mat::zeros( img_input.size(), img_input.type() ); img_input.copyTo(img_adj); for(int i=0;i<itr;i++) { for( int y = 0; y < img_input.rows; y++ ) for( int x = 0; x < img_input.cols; x++ ) { img_adj.at<uchar>(y,x)= saturate_cast<uchar>( 1.5* img_adj.at<uchar>(y, x)); img_adj.at<uchar>(y,x) =saturate_cast<uchar>( img_adj.at<uchar>(y, x) 50 ); } } return img_adj; } Donde 1.5 y 50, e itr, se han obtenido por prueba y error luego de probar con distintas imágenes. 3ra Etapa Esta etapa es la encargada de clasificar las imagenes recortadas de la etapa anterior, y puede subdividirse en: Figura 5. A) Clasificador SVM Para seleccionar la imagen correcta dentro de todas las imágenes posibles contenidas en los contornos, utilizamos un clasificador SVM (Support Vector Machines) el cual separa por medio de un hiperplano a las dos clases, que en este caso son matriculas y no matriculas. Las matriculas obtenidas tiene la forma; Figura 6. Aquellas que no lo son, pero son regiones válidas, es decir, cumplen con las dimensiones de bordes anteriormente mencionadas son de la forma: Figura 7. Con imágenes de este tipo se construyó “TrainingData” y “SVM_Classes” que no son más que las mismas imágenes organizadas en una “Mat” de [1,170x70x(N+n)] , donde “N” es la cantidad de matriculas entrenadas y “n” corresponde a la cantidad de imágenes que no son matriculas, “SVM_Classe” una “Mat” de [1,N+n] , donde se clasifica con uno “1” y con un “0” las imágenes que no corresponden a la imagen de una matrícula válida. Luego en el programa principal se construye el clasificador, de siguiente manera: CvSVM svmClassifier(SVM_TrainingData, SVM_Classes, Mat(), Mat(), SVM_params); ,donde “TrainingData” y “SVM_Classes” son los parámetros anteriormente mencionados, y “SVM_params” se encuentran definidas las características del clasificador, declaradas y definidas en la cabecera del programa principal. Posteriormente para cada una de las imágenes contenidas en los contornos válidos, se procede a clasificarlas mediante el método “predict” de la clase “CvSVM”. Luego de obtener la/las matriculas válidas (solo se considera una, esto se debe a que mas de un contorno encierra a la misma matricula), se utiliza: img_plate = cRegions[i].Adjust(img_plate); img_plate= cRegions[i].AddBorder(img_plate,40); Donde se llama al método “Adjust” el cual mediante una operación algebraica aplicada a cada pixel, se mejora el contraste de la patente, que permitirá luego obtener mejores resultados cuando esta, es procesada por el OCR. A modo de ejemplo se muestra la siguiente figura. Figura 8. Por otro lado el método “AddBorder” no es otra cosa que una función que permite agregar un borde de color negro para que la imagen sea aceptada por el OCR. Posteriormente ”img_plate” es guardada en el directorio raíz del programa para que luego sea trabajada por el sistema de OCR. Ejecución del OCR En este apartado se utiliza el software tesseract en línea de comandos, tal como se muestra mas abajo. system("tesseract plate.jpg plate_ocr -l esp") ,donde se llama al método “system” para ejecutar en línea de comandos a “tesseract”, donde recibe como parámetros la ubicación de la imagen de entrada, en este caso se encuentra en el directorio raíz del programa, por otro lado “plate_ocr” es el archivo de texto de salida donde se guardan los caracteres y por ultimo –l esp, indica que tesseract utilizará el idioma “esp”, creado durante la fase de entrenamiento. [Entrenamiento] Durante la fase de entrenamiento, se utilizó una seria de programas adjuntos a “tesseract” mediante el siguiente conjunto de imágenes (todo este proceso puede ser consultado en la bibliografía con numero [3] de la ultima página) Conjunto de caracteres (letras) Figura 9. Conjunto de caracteres (números) Figura 10. Para finalizar con el entrenamiento fue necesario editar y crear los siguiente archivos, mediante el mismo software en línea de comandos y otras herramientas auxiliares como “jTessBoxEditor”. Imprimir patente La última tarea del programa es leer el archivo de texto donde se encuentra el código de la matricula. Para esto se utiliza el objeto “in” de la clase ifstream, donde en la llamada al constructor se pasa como parámetro el archivo de lectura y el modo de operación. Por último se llama al método getline para obtener la primera línea de texto del archivo. ifstream in("plate_ocr.txt", ios::in); in.getline(str_plate, sizeof(str_plate),'/n'); Estadísticas Se analizó 30 imágenes con una resolución de 1200x900 px, y se obtuvieron 24 imágenes de patentes válidas y el programa OCR ha reconocido de ellas solo 19. Por ende en definitiva son reconocidas solo el 63% de las imágenes de entrada en caracteres imprimibles. Conclusión: En el último apartado concluimos que el programa reconoce para esta muestra solo el 63% de los casos, este resultado está directamente afectado por tres factores importantes. El primer factor que incide en este resultado es la calidad de la imagen entrada, la posición de la patente respecto del frente de la cámara y por otro lado la capacidad del algoritmo de Canny para detectar los bordes. El segundo ítem a tener en cuenta que influye y en menor medida que el anterior, es la cantidad de muestras de entrenamiento necesarias para clasificar correctamente. Por último este factor, y de mayor importancia, la capacidad de detección del OCR no es optima, y esto se debe principalmente a que en este caso se entrenó con una sola copia de cada carácter posible en la matricula, esto es así dado que la tarea de conseguir caracteres de patentes de matriculas es un trabajo absolutamente manual, además el entrenamiento es sumamente tedioso, donde es necesario indicarle a “tesseract” donde se encuentra y con qué dimensiones cada carácter dentro de una plantilla de caracteres, que en algunas situaciones según la calidad del carácter este puede ser reconocido o no, lo cual hace aún más ardua la tarea. El software tesseract en su manual de usuario indica al menos entre 6-10 muestras de cada carácter para obtener buenos resultados, es decir que aquí los resultados obtenidos son bastante buenos considerando que se ha utilizado una sola copia. Anexo: A continuación se detallan los archivos utilizados y desarrollados para este programa, donde además del programa principal se desarrolláron otros programas para poder entrenar el clasificador, y otros que permitiéron encontrar los parámetros adecuados a cada función, estos son: “main.cpp” -> Contiene el programa principal “Regions.h” y “Regions.cpp” ->Contiene la clase “Regions” “PreProcess.h” y “PreProcess.cpp” ->Contiene la clase “PreProcess” “ToolContrast.cpp” -> Permite comprobar distintos ajustes en la imagen “ToolTrainning.cpp” -> Contiene el programa necesario para entrenar Bibliografía [1] Mastering OpenCV with Practical Computer Vision Projects . Daniel Lélis Baggio .2012 [2] Learning OpenCV. Gary Bradski and Adrian Kaehler. O’Really.2008 [3] http://opencv.org/documentation.html [4] http://iaci.unq.edu.ar/materias/vision/apuntes.htm [5] http://hackingtelevision.blogspot.com.ar/2011/04/ocr-con-opencv-usando-canny.html [6] https://code.google.com/p/tesseract-ocr/ [7] http://blog.damiles.com/ [8] https://code.google.com/p/lime-ocr/
© Copyright 2024