Reconocimiento de dígitos simple OCR en OpenCV-Python


380

Estoy tratando de implementar un "Reconocimiento de dígitos OCR" en OpenCV-Python (cv2). Es solo para fines de aprendizaje. Me gustaría aprender las funciones KNearest y SVM en OpenCV.

Tengo 100 muestras (es decir, imágenes) de cada dígito. Me gustaría entrenar con ellos.

Hay una muestra letter_recog.pyque viene con la muestra OpenCV. Pero todavía no podía entender cómo usarlo. No entiendo cuáles son las muestras, las respuestas, etc. Además, al principio carga un archivo txt, que no entendí primero.

Más adelante, buscando un poco, pude encontrar un letter_recognition.data en muestras de cpp. Lo usé e hice un código para cv2.KNearest en el modelo de letter_recog.py (solo para probar):

import numpy as np
import cv2

fn = 'letter-recognition.data'
a = np.loadtxt(fn, np.float32, delimiter=',', converters={ 0 : lambda ch : ord(ch)-ord('A') })
samples, responses = a[:,1:], a[:,0]

model = cv2.KNearest()
retval = model.train(samples,responses)
retval, results, neigh_resp, dists = model.find_nearest(samples, k = 10)
print results.ravel()

Me dio una variedad de tamaño 20000, no entiendo lo que es.

Preguntas:

1) ¿Qué es el archivo letter_recognition.data? ¿Cómo construir ese archivo desde mi propio conjunto de datos?

2) ¿Qué results.reval()denota?

3) ¿Cómo podemos escribir una herramienta de reconocimiento de dígitos simple usando el archivo letter_recognition.data (ya sea KNearest o SVM)?

Respuestas:


528

Bueno, decidí entrenarme en mi pregunta para resolver el problema anterior. Lo que quería era implementar un OCR simple usando las funciones KNearest o SVM en OpenCV. Y a continuación es lo que hice y cómo. (es solo para aprender a usar KNearest para propósitos simples de OCR).

1) Mi primera pregunta fue sobre el archivo letter_recognition.data que viene con muestras de OpenCV. Quería saber qué hay dentro de ese archivo.

Contiene una letra, junto con 16 características de esa letra.

Y this SOFme ayudó a encontrarlo. Estas 16 características se explican en el documento Letter Recognition Using Holland-Style Adaptive Classifiers. (Aunque no entendí algunas de las características al final)

2) Como sabía, sin comprender todas esas características, es difícil hacer ese método. Intenté algunos otros papeles, pero todos fueron un poco difíciles para un principiante.

So I just decided to take all the pixel values as my features. (No estaba preocupado por la precisión o el rendimiento, solo quería que funcionara, al menos con la menor precisión)

Tomé la imagen de abajo para mis datos de entrenamiento:

ingrese la descripción de la imagen aquí

(Sé que la cantidad de datos de entrenamiento es menor. Pero, dado que todas las letras son de la misma fuente y tamaño, decidí probar esto).

Para preparar los datos para el entrenamiento, hice un pequeño código en OpenCV. Hace las siguientes cosas:

  1. Carga la imagen.
  2. Selecciona los dígitos (obviamente, buscando el contorno y aplicando restricciones en el área y la altura de las letras para evitar detecciones falsas).
  3. Dibuja el rectángulo delimitador alrededor de una letra y espera key press manually. Esta vez presionamos la tecla del dígito correspondiente a la letra en el cuadro.
  4. Una vez que se presiona la tecla del dígito correspondiente, cambia el tamaño de este cuadro a 10x10 y guarda valores de 100 píxeles en una matriz (aquí, muestras) y el dígito correspondiente ingresado manualmente en otra matriz (aquí, respuestas).
  5. Luego guarde ambas matrices en archivos txt separados.

Al final de la clasificación manual de dígitos, todos los dígitos en los datos del tren (train.png) están etiquetados manualmente por nosotros mismos, la imagen se verá a continuación:

ingrese la descripción de la imagen aquí

A continuación se muestra el código que utilicé para el propósito anterior (por supuesto, no tan limpio):

import sys

import numpy as np
import cv2

im = cv2.imread('pitrain.png')
im3 = im.copy()

gray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray,(5,5),0)
thresh = cv2.adaptiveThreshold(blur,255,1,1,11,2)

#################      Now finding Contours         ###################

contours,hierarchy = cv2.findContours(thresh,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)

samples =  np.empty((0,100))
responses = []
keys = [i for i in range(48,58)]

for cnt in contours:
    if cv2.contourArea(cnt)>50:
        [x,y,w,h] = cv2.boundingRect(cnt)

        if  h>28:
            cv2.rectangle(im,(x,y),(x+w,y+h),(0,0,255),2)
            roi = thresh[y:y+h,x:x+w]
            roismall = cv2.resize(roi,(10,10))
            cv2.imshow('norm',im)
            key = cv2.waitKey(0)

            if key == 27:  # (escape to quit)
                sys.exit()
            elif key in keys:
                responses.append(int(chr(key)))
                sample = roismall.reshape((1,100))
                samples = np.append(samples,sample,0)

responses = np.array(responses,np.float32)
responses = responses.reshape((responses.size,1))
print "training complete"

np.savetxt('generalsamples.data',samples)
np.savetxt('generalresponses.data',responses)

Ahora entramos en la parte de entrenamiento y prueba.

Para probar la parte que usé debajo de la imagen, que tiene el mismo tipo de letras que solía entrenar.

ingrese la descripción de la imagen aquí

Para el entrenamiento hacemos lo siguiente :

  1. Cargue los archivos txt que ya guardamos anteriormente
  2. crear una instancia de clasificador que estamos usando (aquí, es KNearest)
  3. Luego usamos la función KNearest.train para entrenar los datos

Para fines de prueba, hacemos lo siguiente:

  1. Cargamos la imagen utilizada para probar
  2. procesar la imagen como antes y extraer cada dígito usando métodos de contorno
  3. Dibuje un cuadro delimitador para él, luego cambie el tamaño a 10x10 y almacene sus valores de píxeles en una matriz como se hizo anteriormente.
  4. Luego usamos la función KNearest.find_nearest () para encontrar el artículo más cercano al que le dimos. (Si tiene suerte, reconoce el dígito correcto).

Incluí los últimos dos pasos (entrenamiento y prueba) en el código único a continuación:

import cv2
import numpy as np

#######   training part    ############### 
samples = np.loadtxt('generalsamples.data',np.float32)
responses = np.loadtxt('generalresponses.data',np.float32)
responses = responses.reshape((responses.size,1))

model = cv2.KNearest()
model.train(samples,responses)

############################# testing part  #########################

im = cv2.imread('pi.png')
out = np.zeros(im.shape,np.uint8)
gray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
thresh = cv2.adaptiveThreshold(gray,255,1,1,11,2)

contours,hierarchy = cv2.findContours(thresh,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)

for cnt in contours:
    if cv2.contourArea(cnt)>50:
        [x,y,w,h] = cv2.boundingRect(cnt)
        if  h>28:
            cv2.rectangle(im,(x,y),(x+w,y+h),(0,255,0),2)
            roi = thresh[y:y+h,x:x+w]
            roismall = cv2.resize(roi,(10,10))
            roismall = roismall.reshape((1,100))
            roismall = np.float32(roismall)
            retval, results, neigh_resp, dists = model.find_nearest(roismall, k = 1)
            string = str(int((results[0][0])))
            cv2.putText(out,string,(x,y+h),0,1,(0,255,0))

cv2.imshow('im',im)
cv2.imshow('out',out)
cv2.waitKey(0)

Y funcionó, a continuación se muestra el resultado que obtuve:

ingrese la descripción de la imagen aquí


Aquí funcionó con 100% de precisión. Supongo que esto se debe a que todos los dígitos son del mismo tipo y del mismo tamaño.

Pero de cualquier manera, este es un buen comienzo para principiantes (espero que sí).


67
+1 Post largo, pero muy educativo. Esto debería ir a la información de etiqueta
opencv

12
en caso de que alguien esté interesado, hice un motor OO adecuado a partir de este código, junto con algunas campanas y silbatos: github.com/goncalopp/simple-ocr-opencv
goncalopp

10
Tenga en cuenta que no es necesario usar SVM y KNN cuando tiene una fuente perfecta bien definida. Por ejemplo, los dígitos 0, 4, 6, 9 forman un grupo, los dígitos 1, 2, 3, 5, 7 forman otro y 8 otro. Este grupo viene dado por el número euler. Entonces "0" no tiene puntos finales, "4" tiene dos, y "6" y "9" se distinguen por la posición del centroide. "3" es el único, en el otro grupo, con 3 puntos finales. "1" y "7" se distinguen por la longitud del esqueleto. Cuando se considera el casco convexo junto con el dígito, "5" y "2" tienen dos agujeros y se pueden distinguir por el centroide del agujero más grande.
mmgp

44
Tengo el problema ... gracias. Fue un gran tutorial. Estaba cometiendo un pequeño error. Si alguien más se enfrenta al mismo problema en esto como yo y @rash, entonces es porque está presionando la tecla incorrecta. Para cada número en el cuadro, debe ingresar ese no para que se capacite en él. Espero que ayude.
shalki

19
Un tutorial estelar. ¡Gracias! Se necesitan algunos cambios para que esto funcione con la última versión (3.1) de OpenCV: contornos, jerarquía = cv2.findContours (thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) => _, contornos, jerarquía = cv2.findContours (thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE), model = cv2.KNearest () => model = cv2.ml.KNearest_create (), model.train (samples, answers) => model.train (samples, cv2.ml .ROW_SAMPLE, respuestas), retval, resultados, neigh_resp, dists = model.find_nearest (roismall, k = 1) => retval, results, neigh_resp, dists = model.find_nearest (roismall, k = 1)
Johannes Brodwall

53

Para aquellos que estén interesados ​​en el código C ++, pueden consultar el siguiente código. Gracias Abid Rahman por la buena explicación.


El procedimiento es el mismo que el anterior pero, la búsqueda de contorno utiliza solo el primer nivel de nivel jerárquico, de modo que el algoritmo usa solo el contorno exterior para cada dígito.

Código para crear datos de muestra y etiqueta

//Process image to extract contour
Mat thr,gray,con;
Mat src=imread("digit.png",1);
cvtColor(src,gray,CV_BGR2GRAY);
threshold(gray,thr,200,255,THRESH_BINARY_INV); //Threshold to find contour
thr.copyTo(con);

// Create sample and label data
vector< vector <Point> > contours; // Vector for storing contour
vector< Vec4i > hierarchy;
Mat sample;
Mat response_array;  
findContours( con, contours, hierarchy,CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE ); //Find contour

for( int i = 0; i< contours.size(); i=hierarchy[i][0] ) // iterate through first hierarchy level contours
{
    Rect r= boundingRect(contours[i]); //Find bounding rect for each contour
    rectangle(src,Point(r.x,r.y), Point(r.x+r.width,r.y+r.height), Scalar(0,0,255),2,8,0);
    Mat ROI = thr(r); //Crop the image
    Mat tmp1, tmp2;
    resize(ROI,tmp1, Size(10,10), 0,0,INTER_LINEAR ); //resize to 10X10
    tmp1.convertTo(tmp2,CV_32FC1); //convert to float
    sample.push_back(tmp2.reshape(1,1)); // Store  sample data
    imshow("src",src);
    int c=waitKey(0); // Read corresponding label for contour from keyoard
    c-=0x30;     // Convert ascii to intiger value
    response_array.push_back(c); // Store label to a mat
    rectangle(src,Point(r.x,r.y), Point(r.x+r.width,r.y+r.height), Scalar(0,255,0),2,8,0);    
}

// Store the data to file
Mat response,tmp;
tmp=response_array.reshape(1,1); //make continuous
tmp.convertTo(response,CV_32FC1); // Convert  to float

FileStorage Data("TrainingData.yml",FileStorage::WRITE); // Store the sample data in a file
Data << "data" << sample;
Data.release();

FileStorage Label("LabelData.yml",FileStorage::WRITE); // Store the label data in a file
Label << "label" << response;
Label.release();
cout<<"Training and Label data created successfully....!! "<<endl;

imshow("src",src);
waitKey();

Código para entrenamiento y pruebas

Mat thr,gray,con;
Mat src=imread("dig.png",1);
cvtColor(src,gray,CV_BGR2GRAY);
threshold(gray,thr,200,255,THRESH_BINARY_INV); // Threshold to create input
thr.copyTo(con);


// Read stored sample and label for training
Mat sample;
Mat response,tmp;
FileStorage Data("TrainingData.yml",FileStorage::READ); // Read traing data to a Mat
Data["data"] >> sample;
Data.release();

FileStorage Label("LabelData.yml",FileStorage::READ); // Read label data to a Mat
Label["label"] >> response;
Label.release();


KNearest knn;
knn.train(sample,response); // Train with sample and responses
cout<<"Training compleated.....!!"<<endl;

vector< vector <Point> > contours; // Vector for storing contour
vector< Vec4i > hierarchy;

//Create input sample by contour finding and cropping
findContours( con, contours, hierarchy,CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE );
Mat dst(src.rows,src.cols,CV_8UC3,Scalar::all(0));

for( int i = 0; i< contours.size(); i=hierarchy[i][0] ) // iterate through each contour for first hierarchy level .
{
    Rect r= boundingRect(contours[i]);
    Mat ROI = thr(r);
    Mat tmp1, tmp2;
    resize(ROI,tmp1, Size(10,10), 0,0,INTER_LINEAR );
    tmp1.convertTo(tmp2,CV_32FC1);
    float p=knn.find_nearest(tmp2.reshape(1,1), 1);
    char name[4];
    sprintf(name,"%d",(int)p);
    putText( dst,name,Point(r.x,r.y+r.height) ,0,1, Scalar(0, 255, 0), 2, 8 );
}

imshow("src",src);
imshow("dst",dst);
imwrite("dest.jpg",dst);
waitKey();

Resultado

En el resultado, el punto en la primera línea se detecta como 8 y no hemos entrenado para el punto. También estoy considerando cada contorno en el primer nivel de jerarquía como entrada de muestra, el usuario puede evitarlo calculando el área.

Resultados


1
Estoy cansado de ejecutar este código. Pude crear datos de muestra y etiqueta. Pero cuando ejecuto el archivo de entrenamiento de prueba, se ejecuta con un error *** stack smashing detected ***:y, por lo tanto, no obtengo una imagen final correcta como está obteniendo arriba (dígitos en color verde)
skm

1
Cambié char name[4];su código a char name[7];y no obtuve el error relacionado con la pila, pero aún no obtengo los resultados correctos. Recibo una imagen como aquí < i.imgur.com/qRkV2B4.jpg >
skm

@skm Asegúrese de obtener la misma cantidad de contorno que la cantidad de dígitos en la imagen, también intente imprimir el resultado en la consola.
Haris

1
Hola, ¿podríamos cargar una red entrenada para usar?
yode

14

Si está interesado en el estado del arte en Machine Learning, debe buscar en Deep Learning. Debe tener una GPU compatible con CUDA o, alternativamente, utilizar la GPU en los servicios web de Amazon.

Google Udacity tiene un buen tutorial sobre esto usando Tensor Flow . Este tutorial le enseñará cómo entrenar su propio clasificador en dígitos escritos a mano. Obtuve una precisión de más del 97% en el conjunto de pruebas usando Redes Convolucionales.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.