Kursy • Poradniki • Inspirujące DIY • Forum
« Poprzedni artykuł z seriiNastępny artykuł z serii »
Na początek napiszemy prosty program, który będzie wykrywał twarz, później zajmiemy się rozpoznawaniem jej na obrazach. Postaram się także wytłumaczyć w jaki sposób można budować własne bazy twarzy, które posłużą nam do trenowania naszego programu. Dodatkowo w trakcie kursu pojawi się kilka nowych funkcji, służących do przetwarzania obrazów. Zaczynamy!
Detekcja twarzy
Na pierwszy ogień pójdzie prosty program, którego jedynym zadaniem jest wykrycie twarzy na wczytanym z pliku obrazku. Dzięki temu zobaczymy jakie to proste w OpenCV i szybko załapiemy o co chodzi, bez zbędnego przebijania się przez tony kodu. Program następnie rozbudujemy tak by wygrywał bardziej szczegóły obiekty twarzy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#include <opencv2/core/core.hpp> //Sposób #include <opencv2/highgui/highgui.hpp> //dokładny #include "opencv2/objdetect/objdetect.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <string> #include <iostream> using namespace cv; using namespace std; string face_cascade_name = "haarcascade_frontalface_alt.xml"; //Nazwa kaskady którą wykorzystujemy do rozpoznania twarzy CascadeClassifier face_cascade; //Utworzenie obiektu cascady twarzy string window_name = "Hello Face !"; const string img_name = "cotillard.jpg"; void detectFace( Mat img ); int main( int argc, char** argv ) { Mat img; //Utworzenie nowej macierzy na nasz obrazek img = imread( img_name ); //Wczytanie obrazka z dysku if ( !img.data ) //Sprawdzenie czy ładowanie obrazka przebiegło pomyslnie { cout << "Nie znaleziono pliku " << img_name << "."; return -1; } if( !face_cascade.load( face_cascade_name ) ) //Ładowanie pliku ze sprawdzeniem poprawnoci { cout << "Nie znaleziono pliku " << face_cascade_name << "."; return -2; } namedWindow(window_name, CV_WINDOW_AUTOSIZE); //Utworzenie okna (nazwa okna, ustalenie rozmiaru) detectFace(img); waitKey(0); //Odczekanie na wcisnięce klawisza z opóźnieniem 0ms return 0; } void detectFace( Mat img ) { vector<Rect> faces; //Utworzenie bufora na twarze Mat img_gray; //Utworzenie bufora na obrazek w odcieniach szarosci cvtColor(img, img_gray, CV_BGR2GRAY ); //Konwersja obrazu do odcieni szarosci //equalizeHist(img_gray, img_gray); //Zaaplikowanie do obrazu jego historogramu, poprawa kontrastu face_cascade.detectMultiScale(img_gray, faces, 1.1, 3, 0|CV_HAAR_SCALE_IMAGE, Size(50, 50) ); for( unsigned i = 0; i < faces.size(); i++ ) { Rect rect_face( faces[i] ); //Kwadrat okreslający twarz //ellipse( img, center, Size( faces[i].width*0.5, faces[i].height*0.5), 0, 0, 360, Scalar( 255, 120, 12 ), 2, 2, 0 ); rectangle(img, rect_face, Scalar( 120, 5, 86 ), 2, 2, 0 ); } imshow(window_name, img); //Pokazanie obrazka w oknmie o nazwie "Hello Face !" } |
Należy umieścić obrazek z twarzą w folderze głównym programu. Zmieniając łańcuch można dostosować nazwę wczytywanego pliku do własnych potrzeb.
1 |
const string img_name = "cotillard.jpg"; |
Efekt działania programu:
Żeby dobrze zrozumieć, o co chodzi w naszym programie, wypada opisać pokrótce metodę, której używa OpenCV do wykrywanie twarzy. Metoda ta wykorzystuje do wykrywanie obiektów klasyfikator kaskadowy „Haar Feature-based Cascade Classifier”.
Aby móc wykrywać obiekty takie jak twarz, nos, usta należy wytrenować taki klasyfikator. Nie musimy jednak tego robić sami, gdyż openCV dostarcza kilka gotowych, wytrenowanych już klasyfikatorów zapisanych w postaci plików xml.
Trenowanie klasyfikatora odbywa się poprzez dostarczenie kilkuset różnych próbek, na których znajduję się dany obiekt np. twarz, samochód, wyskalowanymi do tego samego rozmiaru, powiedzmy 50x50. Po wytrenowani klasyfikatora możemy przeszukiwać nasz obraz.
Bierzemy pewien wycinek obrazu taki sam jak rozmiar próbek przy trenowaniu, czyli np. 50x50 i skanujemy cały obraz. Funkcja zwraca 1 jeżeli wycinek obrazu z dużym podobieństwem przedstawia wyszukiwany obiekt (np. twarz) i 0 w przeciwnym przypadku.
Klasyfikator jest tak zaprojektowany by łatwo było go przeskalować co pozwala na znajdowanie obiektów bez względu na ich rozmiary i jest szybsze niż przeskalowywanie obrazu na którym przeprowadzamy detekcje obiektów. Więc, aby znaleźć obiekty o nieznanych wymiarach należy powtórzyć procedurę skanowania obrazu kilkakrotnie.
To tyle podstaw, po więcej odsyłam do Wikipedii lub dokumentacji OpenCV, zawsze można również zadawać pytania pod artykułem, jeśli kogoś bardzo to zainteresuje.
Z powyższego wynika, że aby móc wykrywać obiekty musimy dysponować wytrenowanym klasyfikatorem, w tym kursie skorzystamy z gotowych, wytrenowanych już klasyfikatorów dostarczonych wraz z biblioteką OpenCV.
Potrzebne pliki odnajdujemy, przechodząc do folderu w którym zainstalowaliśmy OpenCV i kolejno przez foldery data/haarcascades. Widzimy kilka dostępnych klasyfikatorów w formie plików .xml. Do wykrycia twarzy możemy wykorzystać plik haarcascade_frontalface_alt.xml.
W programie nazwę pliku mamy zapisaną w zmiennej:
1 |
string face_cascade_name = "haarcascade_frontalface_alt.xml"; |
Następnie tworzymy obiekt, klasyfikatora:
1 |
CascadeClassifier face_cascade; |
W funkcji main() wczytujemy zawartość pliku z klasyfikatorem do naszego obiektu:
1 2 3 4 5 |
if( !face_cascade.load( face_cascade_name ) ) { cout << "Nie znaleziono pliku " << face_cascade_name << "."; return -2; } |
Następnie wywołujemy funkcję wykrywającą twarz na obrazie:
1 |
detectFace(img); |
Z racji że możemy na jednym obrazie znaleźć więcej niż jedną twarz, dane o nich będziemy przechowywać w wektorze kwadratów. Zostaną do niego wpisane współrzędne kwadratów otaczających twarze na obrazie.
1 |
vector<Rect> faces; |
Następnie konwertujemy nasz obraz wejściowy do odcieni szarości, gdyż zmniejszona liczba kolorów nie przeszkadza w rozróżnieniu twarzy na obrazie, ani nam ani komputerowi, a znacznie przyspiesza obliczenia.
1 2 |
Mat img_gray; cvtColor(img, img_gray, CV_BGR2GRAY ); |
Następnie, mamy metodę, gwóźdź programu. Odwala ona za nas "brudną robotę" i wykrywa twarz na obrazie. Jako argumenty podajemy obraz, na którym ma być szukany obiekt. Z racji, że funkcja zwraca wynik w postaci kwadratów otaczających miejsca na obrazie, w których znajdują się twarze, kolejnym argumentem jest wektor kwadratów.
Dalsze parametry określają o ile będzie skalowany obszar przeszukiwania przy kolejnych skanowaniach. Na koniec podajemy minimalny rozmiar twarzy, można dodać jeszcze jeden argument określający maksymalny możliwy rozmiar twarzy.
1 |
face_cascade.detectMultiScale(img_gray, faces, 1.1, 2, 0|CV_HAAR_SCALE_IMAGE, Size(50, 50) ); |
Następna funkcja rysuje kwadraty otaczające twarze na obrazku.
1 |
rectangle(img, rect_face, Scalar( 120, 5, 86 ), 2, 2, 0 ); |
To by było na tyle.
Zaznaczanie ust, nosa i oczu
Teraz możemy nieco rozbudować nasz program, aby oprócz twarzy wykrywał usta, nos oraz oczy. Jest to zadanie bardzo proste i sprowadza się właściwie do wczytania kilku innych plików .xml z klasyfikatorami dla oczu, uszu i nosa. Na początek powinniśmy skopiować te pliki tak jak poprzednio z data/haarcascades do folderu z naszym programem. Poniżej zamieszczam kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
#include <opencv2/core/core.hpp> //Sposób #include <opencv2/highgui/highgui.hpp> //dokładny #include "opencv2/objdetect/objdetect.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <string> #include <iostream> using namespace cv; using namespace std; string face_cascade_name = "haarcascade_frontalface_alt.xml"; //Nazwa kaskady którą wykorzystujemy do rozpoznania twarzy string eyes_cascade_name = "haarcascade_eye_tree_eyeglasses.xml"; //Nazwa kaskady którą wykorzystujemy do rozpoznania oczu string mouth_cascade_name = "haarcascade_mcs_mouth.xml"; //Nazwa kaskady wykorzystywanej przy rozpoznawaniu ust string nose_cascade_name = "haarcascade_mcs_nose.xml"; //Nazwa kaskady wykorzystywanej przy rozpoznawaniu nosa CascadeClassifier face_cascade; //Utworzenie obiektu cascady twarzy CascadeClassifier eyes_cascade; //Utworzenie obiektu cascady oczu CascadeClassifier mouth_cascade; //Utworzenie obiektu cascady ust CascadeClassifier nose_cascade; //Utworzenie obiektu cascady nosa string window_name = "Hello Face !"; string window_name2 = "Face !"; string window_name3 = "Face/2 !"; string img_name = "cotillard.jpg"; RNG rng(12345); void detectFace( Mat img ); int main( int argc, char** argv ) { Mat img; //Utworzenie nowej macierzy na nasz obrazek img = imread( img_name ); //Wczytanie obrazka z dysku if ( !img.data ) //Sprawdzenie czy ładowanie obrazka przebiegło pomyslnie { cout << "Nie znaleziono pliku " << img_name << "."; return -1; } if( !face_cascade.load( face_cascade_name ) ) //Ładowanie pliku ze sprawdzeniem poprawnoci { cout << "Nie znaleziono pliku " << face_cascade_name << "."; return -2; } if( !eyes_cascade.load(eyes_cascade_name) ) //Ładowanie pliku ze sprawdzeniem poprawnoci { cout << "Nie znaleziono pliku " << eyes_cascade_name << "."; return -2; } if( !mouth_cascade.load(mouth_cascade_name) ) //Ładowanie pliku ze sprawdzeniem poprawnoci { cout << "Nie znaleziono pliku " << mouth_cascade_name << "."; return -2; } if( !nose_cascade.load(nose_cascade_name) ) //Ładowanie pliku ze sprawdzeniem poprawnoci { cout << "Nie znaleziono pliku " << nose_cascade_name << "."; return -2; } namedWindow(window_name, CV_WINDOW_AUTOSIZE); //Utworzenie okna (nazwa okna, ustalenie rozmiaru) namedWindow(window_name2, CV_WINDOW_AUTOSIZE); namedWindow(window_name3, CV_WINDOW_AUTOSIZE); detectFace(img); waitKey(0); //Odczekanie na wcinięce klawisza z opóxnieniem 0ms return 0; } void detectFace( Mat img ) { vector<Rect> faces; //Utworzenie bufora na twarze Mat img_gray; //Utworzenie bufora na obrazek w odcieniach szarosci Mat img_face; Mat img_bottom_face; cvtColor(img, img_gray, CV_BGR2GRAY ); //Konwersja obrazu do odcieni szarosci //equalizeHist(img_gray, img_gray); //Zaaplikowanie do obrazu jego historogramu, poprawa kontrastu face_cascade.detectMultiScale(img_gray, faces, 1.1, 2, 0|CV_HAAR_SCALE_IMAGE, Size(50, 50) ); for( unsigned i = 0; i < faces.size(); i++ ) { Rect rect_face( faces[i] ); //Kwadrat okreslający twarz //ellipse( img, center, Size( faces[i].width*0.5, faces[i].height*0.5), 0, 0, 360, Scalar( 255, 120, 12 ), 2, 2, 0 ); rectangle(img, rect_face, Scalar( 120, 5, 86 ), 2, 2, 0 ); img_face = img_gray( faces[i] ); vector<Rect> eyes; vector<Rect> mouth; vector<Rect> nose; eyes_cascade.detectMultiScale(img_face, eyes, 1.1, 2, 0 |CV_HAAR_SCALE_IMAGE, Size(30, 30) ); for( unsigned j = 0; j < eyes.size(); j++ ) { cv::Rect rect_eye( faces[i].x + eyes[j].x, faces[i].y + eyes[j].y, eyes[j].width, eyes[j].height ); rectangle(img, rect_eye, Scalar( 0, 120, 12 ), 2, 2, 0 ); } Rect rect_bottom_face( 0, faces[i].height / 2, faces[i].width, faces[i].height/2); //rectangle(img_face, rect_bottom_face, Scalar( 125, 120, 12 ), 2, 2, 0); img_bottom_face = img_face( rect_bottom_face ); mouth_cascade.detectMultiScale(img_bottom_face, mouth, 1.1, 2, 0 |CV_HAAR_SCALE_IMAGE, Size(40, 40)); for( unsigned j = 0; j < mouth.size(); j++ ) { Rect rect_mouth( faces[i].x + mouth[j].x, faces[i].y + faces[i].height/2 + mouth[j].y, mouth[j].width, mouth[j].height ); rectangle(img, rect_mouth, Scalar( 56, 23, 129 ), 2, 2, 0 ); } nose_cascade.detectMultiScale( img_bottom_face, nose, 1.1, 2, 0 |CV_HAAR_SCALE_IMAGE, Size(40, 40) ); for( unsigned j = 0; j < nose.size(); j++ ) { Rect rect_nose( faces[i].x + nose[j].x, faces[i].y + faces[i].height/2 + nose[j].y, nose[j].width, nose[j].height ); rectangle(img, rect_nose, Scalar( 100, 100, 129 ), 2, 2, 0 ); } } imshow(window_name2, img_face); imshow(window_name3, img_bottom_face); //Pokazanie twarzy imshow(window_name, img); //Pokazanie obrazka w onkie o nazwie "Hello Face !" } |
Myślę, że większa część kodu nie nastarcza problemów, gdyż jest to właściwie powtarzanie tych samych czynności, co przy wykrywaniu twarzy.
Wczytujemy pliki z klasyfikatorami dla poszczególnych obiektów, sprawdzamy poprawność odczytu danych. Następnie przechodzimy do detekcji. Najpierw wykrywamy całą twarz. Potem tworzymy miejsce na kwadraty otaczające oczy, nos, usta.
1 2 3 |
vector<Rect> eyes; vector<Rect> mouth; vector<Rect> nose; |
Jak widać program nie jest optymalny, biorąc pod uwagę, że ludzie mają nos i usta w liczbie jeden. Jednakowoż lepiej utworzyć wektor, w razie gdyby np. komputer pomylił się i wykrył nam oczy jako usta, co się zdarza, albo badany miał szminkę odbitą na twarzy, co też się czasem zdarza.
W naszym programie na uwagę zasługuje poniższy fragment, za pomocą tej instrukcji tworzymy nowy obrazek, jest on mniejszy niż oryginał, a przedstawia wycięta z obrazu wykrytą twarz, pozwala to zoptymalizować wyszukiwanie oczu itp. gdyż będziemy ich od tej chili szukać w obrębie wykrytych twarzy.
1 |
img_face = img_gray( faces[i] ); |
OpenCV - Wykrycie oczu
Poniższy kod wykrywa oczy, metoda ta sama, co w przypadku wykrywania twarzy tyle, że wykorzystujemy kaskadę dla oczu. Następnie w pętli przechodzimy przez wszystkie wykryte oczy, normalnie dwa, i obrysowujemy je kwadratami.
1 2 3 4 5 6 |
eyes_cascade.detectMultiScale(img_face, eyes, 1.1, 2, 0 |CV_HAAR_SCALE_IMAGE, Size(30, 30) ); for( unsigned j = 0; j < eyes.size(); j++ ) { cv::Rect rect_eye( faces[i].x + eyes[j].x, faces[i].y + eyes[j].y, eyes[j].width, eyes[j].height ); rectangle(img, rect_eye, Scalar( 0, 120, 12 ), 2, 2, 0 ); } |
W funkcji:
1 |
cv::Rect rect_eye( faces[i].x + eyes[j].x, faces[i].y + eyes[j].y, eyes[j].width, eyes[j].height ); |
Musimy przesunąć kwadrat do współrzędnych twarzy, gdyż obraz, na którym wykrywamy oczy jest wycinkiem oryginalnego obrazu zawierającym tą jego część na której wykryto twarz. Musimy więc do współrzędnych wyrycia oczu dodać współrzędne wykrycia twarzy, aby wszystko było na sowim miejscu.
OpenCV - wykrycie ust
Przy wykrywaniu ust okazało się, że były one także wykrywane w miejscu oczu, ze względy na kształt powiek. Aby uniknąć takich pomyłek, mając na uwadze, że usta umiejscowione są w obszarze dolnej połowy twarzy, będziemy szukać ich na obrazku, który będzie wycinkiem dolnej połowy twarzy:
1 2 3 4 5 6 7 8 9 |
Rect rect_bottom_face( 0, faces[i].height / 2, faces[i].width, faces[i].height/2); //rectangle(img_face, rect_bottom_face, Scalar( 125, 120, 12 ), 2, 2, 0); img_bottom_face = img_face( rect_bottom_face ); mouth_cascade.detectMultiScale(img_bottom_face, mouth, 1.1, 2, 0 |CV_HAAR_SCALE_IMAGE, Size(40, 40)); for( unsigned j = 0; j < mouth.size(); j++ ) { Rect rect_mouth( faces[i].x + mouth[j].x, faces[i].y + faces[i].height/2 + mouth[j].y, mouth[j].width, mouth[j].height ); rectangle(img, rect_mouth, Scalar( 56, 23, 129 ), 2, 2, 0 ); } |
Dlatego też przy ustalaniu współrzędnych kwadratu otaczającego usta powinniśmy wprowadzić odpowiednie przesunięcie.
OpenCV - Wykrycie nosa
Na koniec wykrywamy nos, którego szukamy także w dolnej części twarzy.
1 2 3 4 5 6 |
nose_cascade.detectMultiScale( img_bottom_face, nose, 1.1, 2, 0 |CV_HAAR_SCALE_IMAGE, Size(40, 40) ); for( unsigned j = 0; j < nose.size(); j++ ) { Rect rect_nose( faces[i].x + nose[j].x, faces[i].y + faces[i].height/2 + nose[j].y, nose[j].width, nose[j].height ); rectangle(img, rect_nose, Scalar( 100, 100, 129 ), 2, 2, 0 ); } |
Program oprócz obrazu, na który zaznaczone są poszczególne obiekty, wyświetla również, obrazki będące wycinkami twarzy oraz jej dolnej części. Efekt działania jest następujący:
OpenCV - Rozpoznawanie twarzy
Teraz, kiedy potrafimy już wykryć twarze, możemy przystąpić do ich rozpoznawania. Choć temat wydaje się być o wiele bardziej skomplikowany, w OpenCV można go również prosto rozwiązać, dlatego że od wersji 2.4 mamy zaimplementowane funkcję wykonującą najcięższą robotę za nas.
Jak już wspomniałem od wersji 2.4 w OpenCV dostępna jest klasa FaceRecognizer, wspomagająca tworzenie aplikacji rozpoznających twarze.
Zaimplementowano w niej metody służące do trenowania algorytmu za pomocą bazy danych z twarzami, przewidywania (odgadywania) największego podobieństwa rozpoznawanej twarzy do tych z bazy danych, ładowania/zapisywania wytrenowanego modelu do pliku XML lub YAML. Obecnie biblioteka dysponuje, aż 3 różnymi algorytmami rozpoznawania twarzy:
- Eigenfaces
- Fisherfaces
- LBPH
Bez zbędnego wnikania w działanie algorytmów, gdyż nie jest to celem tego kursu, postaram się pokazać jak można w praktyce rozpoznawać twarze w OpenCV. Postaram się to zilustrować prostym przykładem, tak aby zrozumienie najważniejszych jego części nie nastarczyło problemów.
Będzie to jedynie wprowadzenie do rozpoznawania twarzy. Być może natchnie ono kogoś by wgryźć się głębiej w problem. Po dodatkowe informacje odsyłam do dokumentacji OpenCV 2.4, gdzie można znaleźć przydatne linki i przykładowe programy, ten kurs bazuje zresztą na informacjach tam zawartych. Kod programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
#include "opencv2/core/core.hpp" #include "opencv2/contrib/contrib.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/objdetect/objdetect.hpp" #include <string> #include <fstream> #include <iostream> using namespace cv; using namespace std; void read_csv( const string & filename, vector<Mat> & images, vector<int> & labels ) { ifstream file(filename.c_str(), ifstream::in ); if ( !file ) { cout << "Otwarcie pliku " << filename << " nie powiodlo sie."; return; } string line, path, classlabel; while ( getline(file, line) ) { stringstream liness(line); getline(liness, path, ';'); getline(liness, classlabel); classlabel.erase(0, 1); path.erase(path.length()-1, path.length()); if ( !path.empty() && !classlabel.empty() ) { images.push_back(imread(path, 0)); labels.push_back(atoi(classlabel.c_str())); cout << path << "=" << atoi(classlabel.c_str()) << endl; } } } int main() { string test_name = "test_3.pgm"; Mat test = imread(test_name, 0); if ( !test.data ) { cout << "Nie odnaleziono " << test_name << "."; return -2; } vector<Mat> images; vector<int> labels; string named_window_s = "Sample"; string named_window_p = "Predicted"; read_csv("C:/Users/fingolfin/workspace/Face_Recognizer/orl_faces/face.csv", images, labels); namedWindow(named_window_s, CV_WINDOW_AUTOSIZE); namedWindow(named_window_p, CV_WINDOW_AUTOSIZE); /*int i = 0; while ( i < 25 ) { imshow(named_window, images[i]); waitKey(0); i++; }*/ Ptr<FaceRecognizer> model = createEigenFaceRecognizer(); model->train(images, labels); int plabel = model->predict(test); cout << plabel; imshow(named_window_s, test); int i = 0; imshow(named_window_p, images[5*plabel + (i % 5) ]); while ( waitKey(0) != 27 ) { i++; imshow(named_window_p, images[5*plabel + (i % 5) ]); } return 0; } |
Zanim uruchomimy program powinniśmy dysponować bazą danych z twarzami, za pomocą której będziemy trenować naszą klasę FaceRecignizer. Taką bazę danych możemy pobrać ze strony w postaci archiwum .zip/.tar.
Następnie przechodzimy do folderu z naszym programem, tworzymy folder powiedzmy „orl_faces” i rozpakowujemy do niego pobrany plik, możemy zobaczyć w jaki sposób zorganizowana jest taka baza. Jest podzielona na foldery, w każdym z nich znajduje się po 10 zdjęć tej samej osoby. Różniących się min. oświetleniem, kątem skierowania twarzy itp.
Dla celów naszego programu wystarczy, jeśli zostawimy 5 pierwszych folderów s1-s5, pozostałe można usunąć. Następnie w każdym z folderów usuwamy 4 ostatnie zdjęcia tak by zostały pliki 1-6. Pięć pierwszych plików z każdego folderu będzie tworzyć dane do trenowania, szósty plik należy wyciąć z folderu i skopiować go do folderu głównego z naszym programem, zmieniając jego nazwę na test1 dla s1, test2 dla s2 itd. za ich pomocą sprawdzimy poprawność działania programu.
Mając tak spreparowaną bazę danych musimy znaleźć jakiś sposób na jej „automatyczne” wczytywanie do naszego programu. Użyjemy do tego pliku csv, plik ten będzie miał następującą budowę:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
C:\Users\ja\workspace\Face_Recognizer\orl_faces\s1/1.pgm ; 0 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s1/2.pgm ; 0 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s1/3.pgm ; 0 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s1/4.pgm ; 0 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s1/5.pgm ; 0 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s2/1.pgm ; 1 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s2/2.pgm ; 1 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s2/3.pgm ; 1 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s2/4.pgm ; 1 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s2/5.pgm ; 1 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s3/1.pgm ; 2 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s3/2.pgm ; 2 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s3/3.pgm ; 2 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s3/4.pgm ; 2 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s3/5.pgm ; 2 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s4/1.pgm ; 3 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s4/2.pgm ; 3 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s4/3.pgm ; 3 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s4/4.pgm ; 3 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s4/5.pgm ; 3 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s5/1.pgm ; 4 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s5/2.pgm ; 4 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s5/3.pgm ; 4 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s5/4.pgm ; 4 C:\Users\ja\workspace\Face_Recognizer\orl_faces\s5/5.pgm ; 4 |
Jak widać, pojedyncza linia składa się ze ścieżki do pliku z obrazkiem oraz nr klasy do której należy obrazek, jego etykiety. Etykiety są po to by wskazać, że dana grupa obrazków (5 w tym przypadku) z jednego folderu reprezentuje twarz tej samej osoby.
Każdej osobie przypisywana jest numer, kiedy mamy już wytrenowany model i podajemy do niego obrazek w celu rozpoznania twarzy, metoda predykcji zwróci nam numer. Porównując go z poszczególnymi etykietami możemy stwierdzić, że dana twarz należy do tej lub innej osoby z bazy.
Z względy na to, że pojedyncza linia zawiera ścieżkę do pliku z obrazkiem w bazie danych, a ścieżki te będą się różnić na różnych komputerach, każdy musimy samemu wygenerować u siebie taki plik. Można to zrobić ręcznie, nie jest jednak to najlepszy pomysł.
Lepiej wykorzystać do tego skrypt, który zrobi to za nas. Można znaleźć go na stronie z dokumentacją OpenCV. Skrypt napisany jest w Python'ie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import sys import os.path # This is a tiny script to help you creating a CSV file from a face # database with a similar hierarchie: # # philipp@mango:~/facerec/data/at$ tree # . # |-- README # |-- s1 # | |-- 1.pgm # | |-- ... # | |-- 10.pgm # |-- s2 # | |-- 1.pgm # | |-- ... # | |-- 10.pgm # ... # |-- s40 # | |-- 1.pgm # | |-- ... # | |-- 10.pgm # if __name__ == "__main__": if len(sys.argv) != 2: print("usage: create_csv <base_path>") sys.exit(1) BASE_PATH=sys.argv[1] SEPARATOR=";" label = 0 for dirname, dirnames, filenames in os.walk(BASE_PATH): for subdirname in dirnames: subject_path = os.path.join(dirname, subdirname) for filename in os.listdir(subject_path): abs_path = "%s/%s" % (subject_path, filename) print(abs_path, SEPARATOR, label) label = label + 1 |
Jest to wersja skryptu dla Python'a 3.0 (lub wyższej), na stronach dokumentacji OpenCV można znaleźć skrypty dla starszych wersji.
Aby uruchomić skrypt należy przejść do folderu z naszą bazą danych, zapisać go tam jako create_csv.py otworzyć cmd, przejść do folderu z naszym skryptem i wywołać go komendą
python create_csv.py C:\Users\ja\workspace\Face_Recognizer\orl_faces
Gdzie zamiast „C:\Users\ja\workspace\Face_Recognizer\orl_faces” podajemy nasz własny link do bazy danych. Po wykonaniu skryptu zostanie wygenerowana w konsoli zawartość pliku csv dla naszej bazy danych, należy ją zaznaczyć, skopiować i zapisać z rozszerzenie .csv np. „face.csv”.
Na początku zajmiemy się funkcją read_csv. Funkcja ta wczytuje utworzony wcześniej plik csv w moim przypadku face.csv. Następnie z pliku pobierane są ścieżki do kolejnych obrazków oraz ich etykiety. Obrazki są wczytywane z plików i umieszczane w wektorze images natomiast odpowiadające im etykiety umieszczane są w wektorze labels.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void read_csv( const string & filename, vector<Mat> & images, vector<int> & labels ) { ifstream file(filename.c_str(), ifstream::in ); if ( !file ) { cout << "Otwarcie pliku " << filename << " nie powiodlo sie."; return; } string line, path, classlabel; while ( getline(file, line) ) { stringstream liness(line); getline(liness, path, ';'); getline(liness, classlabel); classlabel.erase(0, 1); path.erase(path.length()-1, path.length()); if ( !path.empty() && !classlabel.empty() ) { images.push_back(imread(path, 0)); labels.push_back(atoi(classlabel.c_str())); cout << path << "=" << atoi(classlabel.c_str()) << endl; } } } |
Czas na funkcję main, w której na początku wczytujemy obrazek z twarzą, którą postaramy się rozpoznać. Następnie tworzymy klasę rozpoznającą twarz, przy użyciu algorytmu Eigenface, później trenujemy klasę przekazując jako parametry wektory z danymi uczącymi.
1 2 |
Ptr<FaceRecognizer> model = createEigenFaceRecognizer(); model->train(images, labels); |
W kolejnym kroku dokonujemy już właściwego rozpoznania twarzy za pomocą metody predict, do której przekazujemy obraz z twarzą którą chcemy rozpoznać. Jako wynik zwrócona zostanie liczba odpowiadająca etykiecie zbiorowi obrazków twarzy jednej z osób, która zgodnie z algorytmem wykazuje największe podobieństwo do testowanej.
Tzn. jeżeli np. zwróconą liczbą będzie 1 to znaczy, że zgodnie z algorytmem testowana twarz należy do osoby z folderu s2 gdyż wszystkie obrazki z tego folderu mają etykietę 1
C:\Users\ja\workspace\Face_Recognizer\orl_faces\s2/1.pgm ; 1
1 |
int plabel = model->predict(test); |
W dalszej części programu znajdziemy jeszcze kod odpowiedzialny z pokazanie wszystkich obrazków. Kolejne zdjęcia oglądamy naciskając spację.
Efekt działania jest następujący, po lewej mamy obrazek testowy, po prawej odpowiedź programu, czyli jeden z obrazków z bazy danych tej osoby którą komputer uważa za właściciela twarzy testowej. Przewijając spację możemy podglądać pozostałe obrazy twarzy tej osoby w bazie. Oto wynik działania dla dwóch obrazów testowych.
W powyższym przykładzie używaliśmy plików z jednej bazy danych. Miały one takie same rozmiary i liczbę kanałów. Jednak w większości przypadków, rozmiary obrazków są różne, dzieje się tak np. wtedy gdy w bazie danych mamy próbki uczące o rozmiarach załóżmy 200x200, a twarz chcemy rozpoznawać na obrazie przechwytywanym z kamery. Wówczas można posłużyć się opisaną już wcześniej metodą detekcji twarzy i jako obraz testowy podawać do metody predict wycinek obrazu kamery, ograniczony do wykrytej twarzy.
W taki wypadku, wycinek może mieć różne rozmiary w zależności od tego jak daleko od kamery znajduje się osoba. Aby zapewnić poprawne działanie programu i uniknąć błędów powinniśmy więc odpowiednio przeskalować nasz wycinek twarzy oraz dokonać odpowiedniej konwersji kolorów, gdyż obrazy trenujące i testowe muszą być ze sobą zgodne. Konwersji dokonujmy funkcją:
1 |
resize(img_cut, img_resized, Size(im_width, im_height), 1.0, 1.0, INTER_CUBIC); |
Gdzie parametrami są, obrazek skalowany, obrazek wynikowy, rozmiar obrazka po skalowaniu. Następnie podajemy pewne współczynniki skalowania, na koniec podajemy metodę interpolacji.
Na koniec zamieszczam prosty program, który może okazać się pomocny przy tworzeniu własnej bazy z twarzami.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
#include "opencv2/core/core.hpp" #include "opencv2/contrib/contrib.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/objdetect/objdetect.hpp" #include <string> #include <fstream> #include <iostream> using namespace cv; using namespace std; void detectFace( Mat img, string file_name, int im_width, int im_height ); int read_csv( const string & filename, vector<Mat> & images, vector<string> & file_names); string face_cascade_name = "haarcascade_frontalface_alt.xml"; CascadeClassifier face_cascade; string named_window = "ImgMaker"; const int r_width = 200; const int r_height = 200; int main() { vector<Mat> images; vector<string> file_names; int img_num = read_csv("C:/Users/fingolfin/workspace/Database_maker/Twarze/database.csv", images, file_names); namedWindow(named_window, CV_WINDOW_AUTOSIZE); if( !face_cascade.load( face_cascade_name ) ) //Ładowanie pliku ze sprawdzeniem poprawnoci { cout << "Nie znaleziono pliku " << face_cascade_name << "."; return -2; } int i = 0; while ( waitKey(0) != 27 ) { detectFace(images[i%img_num], file_names[i%img_num], r_width, r_height); i++; } waitKey(0); cout << img_num << endl; return 0; } void detectFace( Mat img, string file_name, int im_width, int im_height ) { vector<Rect> faces; //Utworzenie bufora na twarze Mat img_gray; //Utworzenie bufora na obrazek w odcieniach szarosci Mat img_cut; Mat img_resized; Mat resized_gray; vector<int> compression_params; compression_params.push_back(CV_IMWRITE_JPEG_QUALITY); compression_params.push_back(100); cvtColor(img, img_gray, CV_BGR2GRAY ); //Konwersja obrazu do odcieni szarosci face_cascade.detectMultiScale(img_gray, faces, 1.1, 2, 0|CV_HAAR_SCALE_IMAGE, Size(50, 50) ); for( unsigned i = 0; i < faces.size(); i++ ) { img_cut = img(faces[i]); resize(img_cut, img_resized, Size(im_width, im_height), 1.0, 1.0, INTER_CUBIC); cvtColor(img_resized, resized_gray, CV_BGR2GRAY ); } //Pokazanie obrazka w onkie o nazwie "Hello Face !" //putText(resized_gray, file_name, Point(50, 100), FONT_HERSHEY_SIMPLEX, 0.5, 2, 0 ); imwrite(file_name.c_str(), resized_gray, compression_params); imshow(named_window, resized_gray); } int read_csv( const string & filename, vector<Mat> & images, vector<string> & file_names ) { int i = 0; ifstream file(filename.c_str(), ifstream::in ); if ( !file ) { cout << "Otwarcie pliku " << filename << " nie powiodlo sie."; return -1; } string line, path, classlabel; while ( getline(file, line) ) { stringstream liness(line); getline(liness, path, ';'); getline(liness, classlabel); classlabel.erase(0, 1); path.erase(path.length()-1, path.length()); if ( !path.empty() && !classlabel.empty() ) { images.push_back(imread(path)); //labels.push_back(atoi(classlabel.c_str())); //cout << path << "=" << atoi(classlabel.c_str()) << endl; size_t slash_position; slash_position = path.find('/'); path.erase(0,slash_position + 1); file_names.push_back(path); i++; } } return i; } |
Działanie programu jest następujące, należy dostarczyć dla niego plik z klasyfikatorem dla twarzy, oraz bazę zdjęć. Zdjęcia mogą mieć dowolne rozmiary. Podobnie jak wcześniej należy utworzyć plik csv przy użyciu tego samego skryptu pythona, postępując analogicznie jak przedtem.
Obrazy każdej z osób powinny być zgrupowane w osobnych folderach. Po wygenerowaniu plik csv powinniśmy umieścić go tam gdzie znajduje się nasz program główny. Po uruchomieniu programu, będą wczytywane kolejne zdjęcia i konwertowane do rozmiarów ustalonych w zmiennych.
1 2 |
const int r_width = 200; const int r_height = 200; |
Wciskając spację przechodzimy przez kolejne zdjęcia, które będą pokazywać się nam w przeskalowanej formie, w tym samym czasie przeskalowane zdjęcia będą zapisywane w folderze z programem pod takimi samymi nazwami jakie miały oryginały.
Należy więc pamiętać by nie nadawać zdjęciom w różnych folderach takich samych nazw bo potem będą się nawzajem nadpisywać, najlepiej nazywać je zgodnie z regułą nazwiskoosoby_nr.
Podsumowanie - OpenCV i wykrywanie twarzy
Mam nadzieję, że ten artykuł rozjaśnił co nieco w kwestii rozpoznawania i detekcji twarzy. W kolejnych częściach serii zajmiemy się segmentacją skóry, spróbujemy poszukać lepszych metod wykrywanie obiektów po kolorach.
Postaram się też w miarę możliwości przedstawić sposób rozpoznawania pisma przy użyciu OpenCV. Tymczasem odsyłam do dokumentacji: http://opencv.itseez.com/index.html
Gdyby ktoś utkną przy tworzeniu bazy z twarzami albo modyfikacji tej ze strony AT&T to proszę dać znać w komentarzach, postaram się pomóc.
« Poprzedni artykuł z seriiNastępny artykuł z serii »
To nie koniec, sprawdź również
Przeczytaj powiązane artykuły oraz aktualnie popularne wpisy lub losuj inny artykuł »
detekcja, kurs, obraz, OpenCV, rozpoznawanie, twarze
Trwa ładowanie komentarzy...