OCR de reconhecimento de dígitos simples em OpenCV-Python

380

Estou tentando implementar um "OCR de reconhecimento de dígitos" no OpenCV-Python (cv2). É apenas para fins de aprendizado. Gostaria de aprender os recursos do KNearest e SVM no OpenCV.

Eu tenho 100 amostras (ou seja, imagens) de cada dígito. Eu gostaria de treinar com eles.

Há uma amostra letter_recog.pyque acompanha a amostra OpenCV. Mas ainda não consegui descobrir como usá-lo. Eu não entendo quais são as amostras, respostas etc. Além disso, ele carrega um arquivo txt no começo, o que eu não entendi primeiro.

Mais tarde, pesquisando um pouco, pude encontrar um letter_recognition.data nos exemplos do cpp. Eu usei e fiz um código para cv2.KNearest no modelo de letter_recog.py (apenas para teste):

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()

Deu-me uma matriz de tamanho 20000, não entendo o que é.

Questões:

1) O que é o arquivo letter_recognition.data? Como criar esse arquivo a partir do meu próprio conjunto de dados?

2) O que results.reval()denota?

3) Como podemos escrever uma ferramenta simples de reconhecimento de dígitos usando o arquivo letter_recognition.data (KNearest ou SVM)?

Abid Rahman K
fonte

Respostas:

527

Bem, eu decidi me exercitar na minha pergunta para resolver o problema acima. O que eu queria era implementar um OCR simples usando os recursos KNearest ou SVM no OpenCV. E abaixo está o que eu fiz e como. (é apenas para aprender a usar o KNearest para fins simples de OCR).

1) Minha primeira pergunta foi sobre o arquivo letter_recognition.data que acompanha as amostras do OpenCV. Eu queria saber o que há dentro desse arquivo.

Ele contém uma carta, juntamente com 16 recursos dessa carta.

E this SOFme ajudou a encontrá-lo. Esses 16 recursos são explicados no documento Letter Recognition Using Holland-Style Adaptive Classifiers. (Embora eu não tenha entendido alguns dos recursos no final)

2) Como eu sabia, sem entender todos esses recursos, é difícil fazer esse método. Tentei alguns outros papéis, mas todos eram um pouco difíceis para iniciantes.

So I just decided to take all the pixel values as my features. (Eu não estava preocupado com precisão ou desempenho, só queria que funcionasse, pelo menos com a menor precisão)

Tirei a imagem abaixo para os meus dados de treinamento:

insira a descrição da imagem aqui

(Eu sei que a quantidade de dados de treinamento é menor. Mas, como todas as letras têm a mesma fonte e tamanho, decidi tentar isso).

Para preparar os dados para o treinamento, criei um pequeno código no OpenCV. Faz as seguintes coisas:

  1. Carrega a imagem.
  2. Seleciona os dígitos (obviamente, encontrando contornos e aplicando restrições na área e altura das letras para evitar detecções falsas).
  3. Desenha o retângulo delimitador em torno de uma letra e aguarde key press manually. Desta vez, pressionamos a tecla do dígito correspondente à letra na caixa.
  4. Uma vez pressionada a tecla do dígito correspondente, ela redimensiona essa caixa para 10x10 e salva os valores de 100 pixels em uma matriz (aqui, amostras) e o dígito digitado manualmente correspondente em outra matriz (aqui, respostas).
  5. Em seguida, salve as duas matrizes em arquivos txt separados.

No final da classificação manual dos dígitos, todos os dígitos nos dados do trem (train.png) são rotulados manualmente por nós mesmos, a imagem será semelhante a seguir:

insira a descrição da imagem aqui

Abaixo está o código que usei para a finalidade acima (é claro, não tão limpo):

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)

Agora entramos na parte de treinamento e teste.

Para testar a parte, usei a imagem abaixo, que tem o mesmo tipo de letras que eu costumava treinar.

insira a descrição da imagem aqui

Para o treinamento, fazemos o seguinte :

  1. Carregue os arquivos txt que já salvamos anteriormente
  2. crie uma instância do classificador que estamos usando (aqui, é o KNearest)
  3. Em seguida, usamos a função KNearest.train para treinar os dados

Para fins de teste, fazemos o seguinte:

  1. Carregamos a imagem usada para teste
  2. processe a imagem como anteriormente e extraia cada dígito usando métodos de contorno
  3. Desenhe uma caixa delimitadora para ela, redimensione para 10x10 e armazene seus valores de pixel em uma matriz, como feito anteriormente.
  4. Então usamos a função KNearest.find_nearest () para encontrar o item mais próximo ao que fornecemos. (Se tiver sorte, ele reconhece o dígito correto.)

Incluí as duas últimas etapas (treinamento e teste) no código único abaixo:

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)

E funcionou, abaixo está o resultado que obtive:

insira a descrição da imagem aqui


Aqui, trabalhou com 100% de precisão. Suponho que isso ocorre porque todos os dígitos são do mesmo tipo e mesmo tamanho.

Mas de qualquer forma, este é um bom começo para iniciantes (espero que sim).

Abid Rahman K
fonte
67
+1 Postagem longa, mas muito educativa. Isso deve ir para informações da tag opencv
karlphillip
12
no caso de qualquer pessoa interessada, fiz um motor OO adequada a partir deste código, junto com alguns sinos e assobios: github.com/goncalopp/simple-ocr-opencv
goncalopp
10
Observe que não há necessidade de usar SVM e KNN quando você possui uma fonte perfeita bem definida. Por exemplo, os dígitos 0, 4, 6, 9 formam um grupo, os dígitos 1, 2, 3, 5, 7 formam outro e 8 outro. Este grupo é dado pelo número euler. Então "0" não possui pontos finais, "4" possui dois e "6" e "9" são diferenciados pela posição do centróide. "3" é o único, no outro grupo, com 3 pontos de extremidade. "1" e "7" são diferenciados pelo comprimento do esqueleto. Ao considerar o casco convexo junto com o dígito, "5" e "2" têm dois orifícios e podem ser distinguidos pelo centróide do maior orifício.
mmgp 28/01
4
Entendi o problema .. Obrigado. Foi um ótimo tutorial. Eu estava cometendo um pequeno erro. Se mais alguém enfrentar o mesmo problema, como eu e o @rash, é porque você está pressionando a tecla errada. Para cada número na caixa, é necessário inserir esse não para que ele seja treinado. Espero que ajude.
shalki
19
Um tutorial estelar. Obrigado! Existem algumas alterações necessárias para que isso funcione com a versão mais recente (3.1) do OpenCV: contornos, hierarquia = cv2.findContours (thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) => _, contornos, hierarquia = cv2.findContours (thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE), model = cv2.KNearest () => model = cv2.ml.KNearest_create (), model.train (amostras, respostas) => model.train (amostras, cv2.ml .ROW_SAMPLE, answers), retval, resultados, neigh_resp, dists = model.find_nearest (roismall, k = 1) => retval, resultados, neigh_resp, dists = model.find_nearest (roismall, k = 1)
Johannes Brodwall
53

Para quem estiver interessado no código C ++, pode consultar o código abaixo. Obrigado Abid Rahman pela boa explicação.


O procedimento é o mesmo que acima, mas a descoberta de contorno usa apenas o primeiro nível de hierarquia, de modo que o algoritmo usa apenas o contorno externo para cada dígito.

Código para criar dados de amostra e 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 treinamento e teste

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

No resultado, o ponto na primeira linha é detectado como 8 e não treinamos para o ponto. Também estou considerando todos os contornos no primeiro nível da hierarquia como entrada de amostra, o usuário pode evitá-lo calculando a área.

Resultados

Haris
fonte
11
Eu cansei de executar esse código. Consegui criar amostras e rotular dados. Mas quando eu executar o arquivo de treinamento de teste, ele é executado com um erro *** stack smashing detected ***:e, portanto, eu não estou recebendo uma imagem adequada final como você está recebendo acima (dígitos em cor verde)
skm
11
Eu mudo char name[4];no seu código char name[7];e não recebi o erro relacionado à pilha, mas ainda não estou obtendo os resultados corretos. Estou recebendo uma imagem como aqui < i.imgur.com/qRkV2B4.jpg >
skm
@skm Verifique se o número de contornos está igual ao número de dígitos na imagem; tente também imprimir o resultado no console.
Haris
11
Olá, podemos carregar uma rede treinada para usar?
yode 11/07/19
14

Se você está interessado no estado da arte do Machine Learning, deve procurar o Deep Learning. Você deve ter uma CUDA suportando GPU ou, alternativamente, usá-la no Amazon Web Services.

O Google Udacity tem um bom tutorial sobre isso usando o Tensor Flow . Este tutorial ensinará como treinar seu próprio classificador com dígitos escritos à mão. Eu obtive uma precisão de mais de 97% no conjunto de testes usando Redes Convolucionais.

Yonatan Simson
fonte