Proste rozpoznawanie cyfr OCR w OpenCV-Python

380

Usiłuję zaimplementować „OCR rozpoznawania cyfr” w OpenCV-Python (cv2). To jest tylko do celów edukacyjnych. Chciałbym nauczyć się zarówno funkcji KNearest, jak i SVM w OpenCV.

Mam 100 próbek (tj. Zdjęć) każdej cyfry. Chciałbym z nimi trenować.

Jest próbka letter_recog.pydostarczana z próbką OpenCV. Ale nadal nie mogłem wymyślić, jak go używać. Nie rozumiem, jakie są próbki, odpowiedzi itp. Ponadto ładuje najpierw plik txt, którego najpierw nie rozumiałem.

Później, szukając trochę, mogłem znaleźć letter_recognition.data w próbkach cpp. Użyłem go i stworzyłem kod dla cv2.KNearest w modelu letter_recog.py (tylko do testowania):

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

Dał mi tablicę o wielkości 20000, nie rozumiem co to jest.

Pytania:

1) Co to jest plik letter_recognition.data? Jak zbudować ten plik z własnego zestawu danych?

2) Co results.reval()oznacza?

3) Jak możemy napisać proste narzędzie do rozpoznawania cyfr za pomocą pliku letter_recognition.data (KNearest lub SVM)?

Abid Rahman K.
źródło

Odpowiedzi:

527

Cóż, postanowiłem sam poćwiczyć na moim pytaniu, aby rozwiązać powyższy problem. Chciałem zaimplementować prosty OCR przy użyciu funkcji KNearest lub SVM w OpenCV. A poniżej jest to, co zrobiłem i jak. (służy tylko do nauki korzystania z KNearest do prostych celów OCR).

1) Moje pierwsze pytanie dotyczyło pliku letter_recognition.data, który jest dostarczany z próbkami OpenCV. Chciałem wiedzieć, co jest w tym pliku.

Zawiera list wraz z 16 cechami tego listu.

I this SOFpomógł mi to znaleźć. Te 16 funkcji wyjaśniono w artykule Letter Recognition Using Holland-Style Adaptive Classifiers. (Chociaż na końcu nie rozumiałem niektórych funkcji)

2) Ponieważ wiedziałem, nie rozumiejąc wszystkich tych cech, trudno jest zastosować tę metodę. Próbowałem innych artykułów, ale dla początkujących wszystkie były trochę trudne.

So I just decided to take all the pixel values as my features. (Nie martwiłem się o dokładność ani wydajność, chciałem tylko, żeby działała, przynajmniej z najmniejszą dokładnością)

Zrobiłem poniższe zdjęcie dla moich danych treningowych:

wprowadź opis zdjęcia tutaj

(Wiem, że ilość danych treningowych jest mniejsza. Ale ponieważ wszystkie litery mają tę samą czcionkę i rozmiar, postanowiłem spróbować).

Aby przygotować dane do szkolenia, stworzyłem mały kod w OpenCV. Robi następujące rzeczy:

  1. Ładuje obraz.
  2. Wybiera cyfry (oczywiście poprzez wyszukiwanie konturu i stosowanie ograniczeń dotyczących obszaru i wysokości liter, aby uniknąć fałszywych detekcji).
  3. Rysuje prostokąt ograniczający wokół jednej litery i czeka na key press manually. Tym razem sami naciskamy klawisz cyfry odpowiadający literze w polu.
  4. Po naciśnięciu odpowiedniego przycisku cyfry zmienia rozmiar tego pola na 10x10 i zapisuje wartości 100 pikseli w tablicy (tutaj próbki) i odpowiadającą ręcznie wprowadzoną cyfrę w innej tablicy (tutaj odpowiedzi).
  5. Następnie zapisz obie tablice w osobnych plikach txt.

Pod koniec ręcznej klasyfikacji cyfr wszystkie cyfry w danych pociągu (train.png) są przez nas samodzielnie oznaczone ręcznie, obraz będzie wyglądał jak poniżej:

wprowadź opis zdjęcia tutaj

Poniżej znajduje się kod, którego użyłem do powyższego celu (oczywiście nie tak czysty):

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)

Teraz przystępujemy do części szkoleniowej i testowej.

Do testowania użyłem poniższego obrazu, który ma ten sam typ liter, którego użyłem do treningu.

wprowadź opis zdjęcia tutaj

W przypadku szkoleń wykonujemy następujące czynności :

  1. Załaduj wcześniej pliki txt
  2. utwórz instancję klasyfikatora, którego używamy (tutaj KNearest)
  3. Następnie używamy funkcji KNearest.train do trenowania danych

Do celów testowych wykonujemy następujące czynności:

  1. Ładujemy obraz używany do testowania
  2. przetworzyć obraz jak wcześniej i wyodrębnić każdą cyfrę za pomocą metod konturu
  3. Narysuj obwiednię, a następnie zmień rozmiar na 10x10 i zapisz wartości pikseli w tablicy, jak wcześniej.
  4. Następnie używamy funkcji KNearest.find_nearest (), aby znaleźć element najbliższy podanemu. (Przy odrobinie szczęścia rozpoznaje prawidłową cyfrę.)

Ostatnie dwa kroki (szkolenie i testy) umieściłem w jednym kodzie poniżej:

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)

I zadziałało, poniżej mam wynik:

wprowadź opis zdjęcia tutaj


Tutaj działało ze 100% dokładnością. Zakładam, że dzieje się tak, ponieważ wszystkie cyfry są tego samego rodzaju i mają taki sam rozmiar.

Ale w każdym razie jest to dobry początek dla początkujących (mam nadzieję).

Abid Rahman K.
źródło
67
+1 Długi post, ale bardzo edukacyjny. To powinno przejść do informacji tagu
opencv
12
na wypadek, gdyby ktoś był zainteresowany, zrobiłem odpowiedni silnik OO z tego kodu, wraz z kilkoma dzwonkami i gwizdkami
goncalopp
10
Pamiętaj, że nie ma potrzeby używania SVM i KNN, jeśli masz dobrze zdefiniowaną idealną czcionkę. Na przykład cyfry 0, 4, 6, 9 tworzą jedną grupę, cyfry 1, 2, 3, 5, 7 tworzą inną grupę, a 8 inną. Ta grupa jest podana przez numer eulera. Wtedy „0” nie ma punktów końcowych, „4” ma dwa, a „6” i „9” są rozróżniane przez pozycję środka ciężkości. „3” jest jedynym, w drugiej grupie, z 3 punktami końcowymi. „1” i „7” różnią się długością szkieletu. W przypadku kadłuba wypukłego wraz z cyfrą cyfry „5” i „2” mają dwa otwory i można je odróżnić od środka ciężkości największej dziury.
mmgp
4
Masz problem .. Dziękuję. To był świetny samouczek. Popełniłem mały błąd. Jeśli ktoś ma ten sam problem, jak ja i @ rash, to dlatego, że naciskasz zły klawisz. Dla każdego numeru w polu musisz wpisać to nie, aby został na nim wyszkolony. Mam nadzieję, że to pomaga.
shalki
19
Samouczek gwiezdny. Dziękuję Ci! Jest kilka zmian potrzebnych, aby działało z najnowszą wersją OpenCV (3.1): kontury, hierarchia = cv2.findContours (thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) => _, kontury, hierarchia = cv2.findContours (thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE), model = cv2.KNearest () => model = cv2.ml.KNearest_create (), model.train (próbki, odpowiedzi) => model.train (próbki, cv2.ml .ROW_SAMPLE, odpowiedzi), retval, wyniki, neigh_resp, dists = model.find_nearest (roismall, k = 1) => retval, wyniki, neigh_resp, dists = model.find_nearest (roismall, k = 1)
Johannes Brodwall
53

Dla zainteresowanych kodem C ++ można zapoznać się z poniższym kodem. Dziękuję Abidowi Rahmanowi za miłe wyjaśnienie.


Procedura jest taka sama jak powyżej, ale przy wyszukiwaniu konturów używany jest tylko kontur pierwszego poziomu hierarchii, tak że algorytm używa tylko zewnętrznego konturu dla każdej cyfry.

Kod do tworzenia danych próbki i etykiety

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

Kod szkolenia i testów

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

Wynik

W rezultacie kropka w pierwszym wierszu jest wykrywana jako 8 i nie ćwiczyliśmy kropki. Rozważam również każdy kontur na pierwszym poziomie hierarchii jako przykładowe dane wejściowe, użytkownik może tego uniknąć, obliczając obszar.

Wyniki

Haris
źródło
1
Zmęczyłem się uruchomieniem tego kodu. Udało mi się stworzyć przykładowe dane i etykiety. Ale kiedy uruchamiam plik szkolenia testowego, działa on z błędem *** stack smashing detected ***:i dlatego nie otrzymuję ostatecznego poprawnego obrazu, gdy jesteś wyżej (cyfry w kolorze zielonym)
skm
1
zmieniłem char name[4];twój kod na char name[7];i nie otrzymałem błędu związanego ze stosem, ale nadal nie otrzymuję poprawnych wyników. Otrzymuję obraz jak tutaj < i.imgur.com/qRkV2B4.jpg >
skm
@skm Upewnij się, że otrzymujesz liczbę konturów taką samą jak liczba cyfr na obrazie, spróbuj także, drukując wynik na konsoli.
Haris,
1
Witam, czy możemy załadować przeszkoloną sieć do użycia?
yode
14

Jeśli interesuje Cię najnowszy stan uczenia maszynowego, powinieneś przyjrzeć się głębokiemu uczeniu się. Powinieneś mieć procesor graficzny obsługujący CUDA lub alternatywnie korzystać z GPU w Amazon Web Services.

Google Udacity ma fajny samouczek na ten temat przy użyciu Tensor Flow . Ten samouczek nauczy Cię, jak trenować własny klasyfikator na podstawie odręcznych cyfr. Mam dokładność ponad 97% na zestawie testowym przy użyciu Convolutional Networks.

Yonatan Simson
źródło