Documente Academic
Documente Profesional
Documente Cultură
Lucrare de licență
Prezentată ca cerință parțială pentru obținerea
titlului de Inginer
în domeniul Calculatoare și Tehnologia Informației
programul de studii Ingineria Informației
Anul 2020
Universitatea “Politehnica” din București Anexa 1
Facultatea de Electronică, Telecomunicații și Tehnologia Informației
Departamentul EAII
Conducător(i) lucrare,
Student,
Prof. dr. ing. Corneliu FLOREA
Mihai Badea
Lista tabelelor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v
Lista acronimelor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi
Introducere . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Obiectivul lucrării . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Organizarea lucrării . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1. Noțiuni teoretice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1. Imagini digitale și prelucrarea imaginilor . . . . . . . . . . . . . . . . . . . . . . 3
1.2. Rețele neuronale artificiale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.1. Arhitectura de tip feed-forward . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.2. Perceptronul simplu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2.3. Perceptronul multi-strat (MLP) . . . . . . . . . . . . . . . . . . . . . . . 5
1.2.4. Funcții de activare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3. Rețele neuronale convoluționale . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.1. Stratul convoluțional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.2. Stratul pooling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3.3. Stratul fully-connected . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.4. Aplicarea funcției de activare Softmax . . . . . . . . . . . . . . . . . . . 11
1.4. MobileNets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4.1. Tipuri de convoluții. Comparare cu CNN tipice . . . . . . . . . . . . . . 12
1.4.2. Arhitectura rețelei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4.3. Parametri ajustabili. Multiplicatorii de lățime și rezoluție . . . . . . . . 14
1.4.4. Concluzii privind alegerea arhitecturii . . . . . . . . . . . . . . . . . . . 15
2. Resurse utilizate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.1. Resurse Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.1.1. Raspberry Pi 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.1.2. Modul Raspberry Pi Camera V2 . . . . . . . . . . . . . . . . . . . . . . 18
2.2. Resurse Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2.1. Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2.2. OpenCV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2.3. Keras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2.4. TensorFlow Lite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2.5. PyQt5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.2.6. Google Colab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3. Etape în implementarea aplicației . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.1. Organizarea bazelor de date . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.1.1. Organizarea și preprocesarea imaginilor în directoare separate pe clase . 23
3.1.2. Încărcarea imaginilor în liste Python și exportarea acestora ca fișiere .npz 24
3.2. Pregătirea rețelei neuronale convoluționale . . . . . . . . . . . . . . . . . . . . . 27
3.3. Antrenarea rețelei și ajustarea parametrilor . . . . . . . . . . . . . . . . . . . . 28
3.4. Rezultatele antrenării pentru estimarea vârstei . . . . . . . . . . . . . . . . . . . 30
3.4.1. Determinarea acurateței de test extinzând limitele intervalelor . . . . . . 32
3.4.2. Rezultate ale antrenării pentru setarea diferită a parametrilor . . . . . . 32
3.5. Rezultatele antrenării pentru determinarea genului . . . . . . . . . . . . . . . . 33
3.6. Convertirea și utilizarea modelului TFLite . . . . . . . . . . . . . . . . . . . . . 35
3.7. Interfața aplicației pe Raspberry Pi . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.8. Captarea și clasificarea imaginilor în timp real . . . . . . . . . . . . . . . . . . . 36
3.9. Încărcarea imaginilor locale și clasificarea acestora . . . . . . . . . . . . . . . . . 40
ii
Lista figurilor
1.1. Reprezentarea imaginii color în formă matriceală (figură preluată din [1]) . . . . 3
1.2. Exemplu de rețea neuronală de tip feed-forward (Imagine preluată din [2]) . . . 4
1.3. Principiul matematic al perceptronului (Imagine preluată din [3]) . . . . . . . . 5
1.4. Funcție de activare ce poate fi plasată între intrarea neuronului curent și ieșirea
acestuia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5. Reprezentarea funcțiilor de activare tanh, sigmoidă și ReLU. Reprezentare folo-
sind biblioteca Matplotlib în Python . . . . . . . . . . . . . . . . . . . . . . . . 7
1.6. Exemplu vizual de rețea convoluțională (Imagine preluată din [4]) . . . . . . . . 8
1.7. Exemplu de operație de convoluție bidimensională între imaginea de la intrare
și masca de filtrare (Figură preluată din [5]) . . . . . . . . . . . . . . . . . . . . 9
1.8. Exemplu de operație de convoluție între o imagine de dimensiune M × N × 3 și
un nucleu de dimensiune 3 × 3 × 3 (Imagine preluată din [4]) . . . . . . . . . . . 10
1.9. Exemplu de operație Max Pooling pe o hartă de caracteristici (rezultată în urma
operației de convoluție) și un filtru 2 × 2 cu pasul (stride) de 2 (Imagine preluată
din [6]) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.10. Exemplu de aplicare a stratului fully-connected și obținere rezultate ale clasifi-
cării utilizând funcția Softmax (Sursa: [4]) . . . . . . . . . . . . . . . . . . . . . 11
1.11. Exemple de aplicații mobile folosind rețeaua MobileNets (imagine preluată din
[7]) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.12. Separarea convoluțiilor standard în convoluții în adâncime și convoluții punctu-
ale (figură preluată din [7]) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1. Placa de dezvoltare Raspberry Pi 4 (imagine preluată din [8]) . . . . . . . . . . 17
2.2. Placa Raspberry Pi 4 cu modul cameră atașat ce va fi folosită în această lucrare 18
2.3. Etapele de la modelul antrenat TF la modelul TFLite utilizabil pe dispozitive
mobile (imagine preluată din [9]) . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4. Aflarea sistemului de operare și a plăcii video disponibile pe platforma Google
Colab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.1. Organizarea directoarelor cu imaginile bazelor de date UTKFace, respectiv Appa-
Real (vizualizare folosind aplicația WinDirStat) . . . . . . . . . . . . . . . . . . 24
3.2. Histogramă cu numărul total de imagini pentru fiecare clasă de vârstă corespun-
zătoare (a), respectiv a genului corespunzător (b) . . . . . . . . . . . . . . . . . 25
3.3. Preluarea a 80% din imaginile fiecărei clase pentru antrenare, de la mijlocul
intervalului . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.4. Rețeaua MobileNet după înlocuirea ultimelor 6 straturi cu straturile Dropout și
Dense . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.5. Diferența dintre SGD cu momentum și Nesterov momentum (Sursa: [10]) . . . . 29
3.6. Grafice ce reprezintă evoluția funcției de cost (Loss), respectiv a acurateței (Ac-
curacy) pentru imaginile de antrenare și testare . . . . . . . . . . . . . . . . . . 31
3.7. Matricea de confuzie asociată modelului antrenat, reprezentare sub formă nu-
merică (a) și sub formă procentuală (b) . . . . . . . . . . . . . . . . . . . . . . . 31
3.8. Grafice ce reprezintă evoluția funcției de cost (Loss), respectiv a acurateței (Ac-
curacy) pentru imaginile de antrenare și testare, pentru clasificatorul de gen . . 34
iii
3.9. Matricea de confuzie asociată modelului antrenat, reprezentare sub formă nu-
merică (a) și sub formă procentuală (b), unde Clasa 0 = genul feminin iar Clasa
1 = genul masculin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.10. Reprezentarea dimensiunii de stocare a modelelor în format h5/hdf5 și tflite . . 35
3.11. Meniul principal al aplicației . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.12. Interfața pentru fiecare din cele 3 submeniuri: camera în timp real (a), selectarea
unei imagini (b), selectarea unui director (c) . . . . . . . . . . . . . . . . . . . . 37
3.13. Rezultate ale clasificării în timp real folosind camera web atașată . . . . . . . . 39
3.14. Rezultate ale clasificării în timp real, fără a folosi o interfață grafică . . . . . . . 40
3.15. Rezultate ale clasificării în timp real, pentru o rezoluție de intrare de 320 × 240 40
3.16. Rezultatul clasificării unei singure imagini. Aceste poze de validare au fost pre-
luate de pe ThisPersonDoesNotExist . . . . . . . . . . . . . . . . . . . . . . . . 42
3.17. Clasificare a mai multor imagini aflate într-un dosar, afișare în cascadă. Aceste
poze de validare au fost preluate de pe ThisPersonDoesNotExist . . . . . . . . . 43
iv
Lista tabelelor
v
Lista acronimelor
TF = TensorFlow
TFLite = TensorFlow Lite
vi
1
Introducere
Recunoașterea atributelor faciale precum vârsta, genul și emoția reprezintă, de peste două dece-
nii, un subiect de interes în rândul cercetărilor de viziune computerizată. Unul dintre motivele
cheie constă în numeroasele aplicații ale acestei probleme provocatoare. Datorită lansării unor
seturi mari de date etichetate, precum și a progreselor înregistrate în proiectarea rețelelor ne-
uronale convoluționale (CNN), ratele de eroare au scăzut semnificativ. Astfel, prin utilizarea
CNN, se poate obține o creștere semnificativă a performanței în aceste sarcini. În ultimii ani,
acuratețea clasificării vârstelor persoanelor a crescut de la 41.5% în 2014 [11] la 71% în 2017[12],
iar pentru estimarea genului, acuratețea se situează la 91%[12], acestea fiind obținute pe baza
de date de referință Adience1 .
Clasificarea automată a vârstei și a genului poate fi utilizată într-un număr mare de aplicații,
în special în interfețele inteligente om-calculator, precum și în biometrie, supraveghere vizuală,
acces limitat la conținut multi-media pe diferite platforme online și pentru aplicații comerciale
de tip self-checkout folosite în magazinele alimentare la cumpărarea diverselor produse.
Obiectivul lucrării
Sistemele de recunoaștere a trăsăturilor figurilor faciale bazate pe CNN au demonstrat perfor-
manțe impresionante, obținând o precizie comparabilă cu cea a oamenilor. Cu toate acestea,
există o problemă care ar putea împiedica adoptarea lor pe dispozitive cu consum redus de ener-
gie (ce pot fi alimentate de la o baterie), precum telefoanele inteligente și sistemele integrate de
tip Single Board Computer (SBC). Arhitecturile convoluționale sunt de obicei pretențioase din
punct de vedere al resurselor de calcul necesare, mai ales atunci când adâncimea rețelei este
crescută pentru a maximiza acuratețea.
Această lucrare are ca obiectiv găsirea unei soluții optime pentru clasificarea genului și vârstei
pe o platformă cu resurse limitate, anume sistemul integrat Raspberry Pi 4, care nu dispune
de accelerare grafică. Astfel, se va căuta o arhitectură neuronală și se va ajuta astfel încât
să permită rularea unei aplicații de clasificare în timp real, ce are la intrare secvențe video
achiziționate de camera conectată la acest sistem. Întreaga aplicație va fi apoi înfășurată într-o
interfață grafică simplă, cu aspect modern, pentru a facilita interacțiunea cu utilizatorii.
Pe scurt, se vor efectua următorii pași în scopul realizării acestei aplicații. Acești pași vor
reprezenta totodată contribuțiile personale în cadrul proiectului:
1
Aceste valori ale acurateței au fost obținute pe o bază de date pentru care fiecărei categorii de vârstă îi sunt
excluse limitele inferioare și superioare. Spre exemplu categoriile sunt împărțite în (0-2, 4-6, 8-13, 15-20, 25-32,
38-43, 48-53, 60-) ani, necuprinzând toate valorile din intervalul [0, 80]. Se va analiza ulterior în lucrare acest
caz, pentru care se va calcula o nouă acuratețe.
2
2. Căutarea și analizarea resurselor software optime, care utilizează librării scrise în C/C++
și folosesc toate nucleele disponibile pe Raspberry Pi.
5. Antrenarea rețelei și ajustarea parametrilor pentru a obține o acuratețe cât mai mare pe
imagini de test, nefolosite în cadrul antrenării.
7. Crearea unei interfețe grafice pe Raspberry Pi, prin intermediul căreia să se realizeze
clasificarea secvențelor video captate de camera atașată sistemului, precum și a imaginilor
aflate în memoria locală.
Organizarea lucrării
Pe parcursul capitolului următor, se vor descrie noțiunile teoretice privind imaginile digitale, o
scurtă istorie despre rețelele neuronale artificiale precum și descrierea tipurilor acestora. În con-
tinuare, capitolul prezintă detalii privind rețelele neuronale convoluționale, necesitatea acestora
în dezvoltarea aplicațiilor de viziune computerizată, modul de funcționare precum și analizarea
fiecărui strat intermediar din componența lor. În finalul capitolului se va analiza arhitectura
MobileNets, menită să reducă volumul computațional al rețelelor convoluționale clasice.
Capitolul 2 este dedicat resurselor hardware și software utilizate în cadrul alcătuirii aplicației
din această lucrare. Se vor descrie pe scurt motivațiile și specificațiile plăcii de dezvoltare
folosite. Pentru fiecare resursă software prezentată, se vor preciza avantajele și eventual deza-
vantajele acesteia, ca argumente pentru folosirea ei în conceperea aplicației.
Capitolul 4 prezintă concluzia personală după dezvoltarea acestei aplicații, precum și prezenta-
rea pe scurt a problemelor întâmpinate. Totodată vor fi propuse câteva idei pentru dezvoltarea
în continuare a aplicației.
3
Capitolul 1
Noțiuni teoretice
Figura 1.1: Reprezentarea imaginii color în formă matriceală (figură preluată din [1])
• Ieșirea sistemului în care rezultatul poate fi o imagine modificată sau un raport care se
bazează pe analiza imaginii.
Rețelele neuronale artificiale sunt unele dintre instrumentele principale utilizate în învățarea
automată (Machine Learning). Aceste sisteme sunt inspirate din anatomia creierului uman,
reproducând modul în care oamenii învață lucruri noi. Rețelele neuronale constau din straturi
de intrare și ieșire, precum și unul sau mai multe straturi ascunse formate din unități care
transformă intrarea într-o reprezentare utilă stratului de ieșire. Ele sunt instrumente excelente
pentru a găsi modele care sunt mult prea complexe sau numeroase pentru care un programator
uman să producă un algoritm.
Rețeaua neuronală de tip feed-forward a fost prima și cea mai simplă tipologie de rețea neu-
ronală artificială concepută. În această rețea, informația se deplasează într-o singură direcție,
înainte, de la nodurile de intrare, prin nodurile ascunse (dacă există) și către nodurile de ieșire.
Nu există cicluri sau bucle în rețea[2]. Din acest tip de rețea derivă perceptronul cu un singur
strat și perceptronul cu mai multe straturi, prezentate în următoarele subsecțiuni.
Figura 1.2: Exemplu de rețea neuronală de tip feed-forward (Imagine preluată din [2])
5
Principiul perceptronului este următorul: preia mai multe intrări binare, le procesează și pro-
duce o singură ieșire. Din punct de vedere matematic, perceptronul poate fi privit ca o funcție
de prag care mapează intrarea sa x (un vector cu valori reale) la o valoare de ieșire f (x) (o
singură valoare binară), precum în figura 1.3. Relația se poate scrie astfel:
{ ∑
1 , dacă w0 + ni=1 wi ∗ xi > prag
f (x) = (1.1)
0 , în rest
Deși inițial perceptronul părea promițător, s-a dovedit rapid că acesta nu poate fi antrenat să
recunoască mai mult de două tipuri de clase. Acest lucru a făcut ca domeniul cercetării rețelelor
neuronale să stagneze mulți ani până la descoperirea că o rețea neuronală avansată cu două sau
mai multe straturi (numită și perceptron multi-strat) ar avea o putere de procesare mai mare
decât perceptronii cu un strat[16].
1
Algoritmul perceptron a fost inventat în 1958 de către Frank Rosenblat la Laboratorul Aeronautic Cornell.
2
Învățarea supervizată (din eng. Supervised Learning) presupune existența unui lot de antrenare etichetat,
unde etichetele asociate vectorilor de la intrare reprezintă codificarea clasei căreia îi aparțin.
6
și să învețe funcții neliniar separabile. Deoarece un MLP poate învăța funcții neliniare, acesta
devine una dintre tehnicile principale de învățare automată atât pentru regresie cât și pentru
clasificare în învățarea supervizată[2]. MLP-urile sunt de obicei organizate în straturi, respec-
tiv au în componență un strat de intrare, cel puțin un strat ascuns (compus din mai mulți
perceptroni) și un strat de ieșire. Prelucrarea semnalului de la intrare se face doar în straturile
intermediare și în cel de ieșire.
Figura 1.4: Funcție de activare ce poate fi plasată între intrarea neuronului curent și ieșirea
acestuia
O funcție de activare reprezintă o ecuație matematică care determină ieșirea unei rețele neu-
ronale. Funcția este atașată la fiecare neuron din rețea și stabilește dacă acesta ar trebui să
fie activat sau nu, în funcție de cât de relevant este pentru predicția modelului. Funcțiile de
activare pot ajuta la normalizarea ieșirii fiecărui neuron într-un interval cuprins între 0 și 1 sau
între -1 și 1[17]. Totodată, funcțiile pot servi ca o ”poartă” între intrarea care alimentează ne-
uronul curent și ieșirea sa care servește drept intrare la următorul strat (precum în figura 1.4).
Astfel, acestea pot fi aplicate pe mii sau chiar milioane de neuroni pentru fiecare eșantion de
date, deci, trebuie să fie eficiente din punct de vedere al calculului. Rețelele neuronale moderne
folosesc o tehnică de propagare înapoi prin rețea (backpropagation) pentru a antrena modelul,
care pune o tensiune de calcul crescută asupra funcției de activare.
• Funcția treaptă binară, care este bazată pe un prag ales. Dacă valoarea de intrare este
peste un anumit prag, neuronul este activat și trimite semnalul de 1 către următorul strat,
iar în caz contrar trimite semnalul 0. Relația matematică este următoarea:
{
1 , dacă x ≥ prag
f (x) = (1.2)
0 , dacă x < prag
• Funcție de activare liniară, care preia intrările înmulțite cu ponderile pentru fiecare ne-
uron și creează un semnal de ieșire proporțional cu intrarea. Astfel, spre deosebire de
7
funcția treaptă binară, poate permite ieșiri multiple. Totuși, cu această funcție nu este
posibilă utilizarea metodei de propagare înapoi folosind gradient descendent (metodă ne-
cesară pentru a antrena modelul), deoarece derivata funcției este o constantă și se pierde
legătura cu intrarea x[17]. Deci nu este posibilă întoarcerea pentru a afla care ponderi
ale neuronilor de intrare pot oferi o predicție mai bună.
• Funcții de activare neliniare, care permit modelului să creeze mapări complexe între
intrările și ieșirile rețelei. Sunt esențiale pentru învățarea și modelarea datelor complexe,
cum ar fi imaginile, capturile video, audio sau seturile de date care nu sunt liniare sau au
o dimensionalitate ridicată.
e2x − 1
f (x) = tanh(x) = (1.4)
e2x + 1
• Funcția sigmoidă:
1
f (x) = (1.5)
1 + (1 + e−x )
• Funcția Softmax:
ezi
σ(z)i = ∑K pentru i = 1, . . . , K și z = (z1 , . . . , zK ) ∈ RK (1.7)
j=1 ezj
De obicei funcția Softmax este utilizată doar pentru stratul de ieșire, pentru rețelele neuronale
care trebuie să clasifice intrările în mai multe categorii. Aceasta normalizează ieșirile pentru
fiecare clasă între 0 și 1[17], oferind astfel gradele de apartenență a intrării la fiecare dintre
clasele de ieșire.
Figura 1.5: Reprezentarea funcțiilor de activare tanh, sigmoidă și ReLU. Reprezentare folosind
biblioteca Matplotlib în Python
8
Rețelele neuronale convoluționale (CNN) sunt o clasă de rețele neuronale artificiale, deci, sunt
formate din neuroni, care iau o sumă ponderată a intrărilor și produc un nivel de activări
(respectiv activările fiecărui neuron). Acestea au fost introduse pentru prima dată în 1989,
dovedindu-se eficiente în clasificarea imaginilor care conțineau cifre scrise de mână[18]. În
urma acestui succes, interesul pentru CNN a crescut și s-a dovedit că obține performanțe mult
mai bune în sarcini mai dificile de recunoaștere a imaginii, depășind alte modele de ML[19].
Succesul se poate datora disponibilității seturilor mari de date, creșterii puterii de calcul din
ultimii ani precum și a tehnicilor de regularizare îmbunătățite.
Figura 1.6: Exemplu vizual de rețea convoluțională (Imagine preluată din [4])
Arhitecturile CNN fac presupunerea explicită că intrările sunt imagini, ceea ce permite codi-
ficarea anumitor proprietăți. Acestea fac apoi ca funcția forward să fie mai eficientă pentru
a implementa și a reduce considerabil numărul de parametri în rețea[6]. Astfel, înmulțirea se
realizează punct la punct între un tablou de date de intrare și un tablou de valori ale ponderi-
lor, numit filtru sau nucleu (kernel), ambele tabele bidimensionale. Acest filtru este mult mai
mic decât datele de intrare permițând să fie înmulțit de mai multe ori cu tabloul de intrare în
diferite puncte. Inovația rețelelor neuronale convoluționale este abilitatea de a învăța automat
un număr mare de filtre specific unui set de date de instruire, sub constrângerile unei probleme
specifice, cum ar fi clasificarea imaginilor[5]. Rezultatul constă în caracteristici specifice care
pot fi detectate oriunde apoi pe imaginile de intrare.
• Fiecare perceptron este conectat cu fiecare alt perceptron și astfel numărul de parametri
devine foarte mare, creându-se o redundanță în dimensiunile foarte mari[20]. În cazul
CNN, fiecare nod nu se conectează la orice alt nod, straturile interne rețelei fiind astfel
parțial conectate între ele.
• Rețeaua ignoră informațiile spațiale și este nevoie de vectori aplatizați ca intrări, în timp
ce CNN poate accepta ambele tipuri de date la intrare (vectori sau matrici), iar fiecare
filtru este împărțit în jurul întregii imagini și permite găsirea tiparelor indiferent unde
sunt amplasate acestea.
9
Într-un prim pas se înmulțește tabloul de la intrare cu un filtru. Se plasează, pe rând, originea
ferestrei de filtrare în fiecare punct al imaginii de intrare. Pentru fiecare poziție se face suma
produselor punct cu punct între coeficienții din fereastră și valorile pixelilor din acea regiune a
imaginii. Practic, se realizează operația de convoluție bidimensională, între imagine și masca
(fereastra) de filtrare. Dacă filtrul este proiectat pentru a detecta un anumit tip de caracteris-
tică, atunci aplicarea acestui filtru în mod sistematic pe întreaga imagine de intrare permite
filtrului posibilitatea de a descoperi acea caracteristică oriunde în imagine3 . Odată creată o
hartă a funcțiilor, putem trece fiecare valoare din harta caracteristică printr-o funcție neliniară,
precum ReLU[5]. Un exemplu vizual al acestei operații se poate vedea în figura 1.7.
În cazul imaginilor cu mai multe canale, nucleul are aceeași adâncime ca imaginea de intrare (de
exemplu, pentru imagini cu 3 canale - RGB, nucleul va avea adâncimea egală cu 3). Înmulțirea
matricei se realizează între fiecare canal al imaginii și masca nucleului și toate rezultatele sunt
însumate apoi cu un prag (bias). Operația este exemplificată în figura 1.8.
3
Găsirea unei trăsături oriunde în imagine poartă de obicei denumirea de invarianță la translație.
10
Figura 1.9: Exemplu de operație Max Pooling pe o hartă de caracteristici (rezultată în urma
operației de convoluție) și un filtru 2 × 2 cu pasul (stride) de 2 (Imagine preluată din [6])
11
Figura 1.10: Exemplu de aplicare a stratului fully-connected și obținere rezultate ale clasificării
utilizând funcția Softmax (Sursa: [4])
1.4 MobileNets
MobileNets este o arhitectură ce derivă din rețelele convoluționale, concepută special pentru
aplicațiile mobile și integrate (fig 1.11), unde lipsește puterea mare de calcul. Aceasta a apărut
ca soluție pentru aplicațiile care necesitau conectarea la un server extern pentru a trimite date
de intrare și a primi rezultate de ieșire procesate de către server, clasificarea datelor făcându-se
la distanță. Folosind MobileNets, aplicațiile mobile pot procesa și clasifica intrările direct pe
platforma hardware proprie și astfel nu mai au nevoie de conectare la Internet.
Ideea din spatele MobileNets este de a utiliza convoluții separabile în adâncime (depthwise
separable convolutions). În stratul convoluțional obișnuit, nucleul filtrului de convoluție este
aplicat pe toate canalele imaginii de intrare, făcând suma ponderată a pixelilor de intrare cu
nucleul. MobileNets folosește această convoluție regulată numai în primul strat. Următoarele
12
Figura 1.11: Exemple de aplicații mobile folosind rețeaua MobileNets (imagine preluată din
[7])
straturi sunt convoluțiile separabile în adâncime, care sunt o combinație dintre convoluția în
adâncime (depthwise convolution) și convoluția punctuală (pointwise convolution). Pe scurt,
convoluția în adâncime face separat operația pe fiecare canal, iar convoluția punctuală, este
similară cu convoluția obișnuită, dar se folosește un filtru de dimensiune 1 × 1. Procedând
astfel, cantitatea de calcul necesară este mai mică decât rețelele convoluționale obișnuite[21].
• M = Adâncimea (Numărul de canale) hărții la intrare (de exemplu, cele 3 canale RGB)
Un strat convolutiv standard din interiorul rețelei are ca intrare o hartă a caracteristicilor F
de dimensiuni DF × DF × M din care rezultă o hartă G de dimensiuni DG × DG × N . Acest
strat este caracterizat de nucleul de convoluție K de dimensiunea DK × DK × M × N . Astfel,
convoluțiile standard au costul de calcul de:
DK × DK × M × N × DF × DF (1.8)
Costul de calcul depinde deci de numărul de canale de intrare, ieșire, de dimensiunea nucleului
și de harta de caracteristici.
Modelele MobileNets abordează fiecare dintre acești termeni și interacțiunile dintre ei. Etapele
de filtrare și combinație pot fi împărțite în două etape prin utilizarea convoluțiilor separabile în
adâncime, obținând mai întâi un cost de calcul de DK ×DK ×M ×DF ×DF în urma convoluției în
adâncime. Se observă că această operație este mai eficientă în raport cu convoluția standard,
dar, aceasta filtrează doar canalele de intrare, deci nu le combină pentru a crea funcții noi.
Așadar, este nevoie de un strat suplimentar, care să calculeze o combinație liniară a ieșirii
convoluției în adâncime printr-o convoluție 1 × 1 (convoluție punctuală) pentru generarea de
noi caracteristici. Prin aceasta, convoluțiile separabile în adâncime vor avea costul de:
DK × DK × M × DF × DF + M × N × DF × DF (1.9)
DK × DK × M × N × DF × DF D2 × N
= K
DK × DK × M × DF × DF + M × N × DF × DF DK2
+N
( )−1 (1.10)
1 1
= + 2
N DK
Tabela 1.1: Structura rețelei MobileNet, unde Conv dw = deepth-wise convolutional layer
(Sursa: [7])
Multiplicatorul de lățime
Rolul multiplicatorului de lățime α este să subțieze uniform rețeaua la fiecare strat. Pentru
un strat dat și un multiplicator scalar α, numărul de canale ale hărții de intrare M și ieșire
N devin αM , respectiv αN . Costul de calcul al unei convoluții separabile în adâncime cu
15
DK × DK × αM × DF × DF + αM × αN × DF × DF (1.11)
unde α ∈ (0, 1], iar valorile tipice pentru acesta sunt 1, 0.75, 0.25[7]. Multiplicatorul de lățime
are ca efect reducerea costului de calcul și a numărului de parametri de aproximativ α2 ori.
Multiplicatorul de lățime poate fi aplicat oricărei structuri de model pentru a defini un nou
model mai mic, cu o precizie, latență cu un compromis rezonabil.
Multiplicatorul de rezoluție
Al doilea hiper-parametru se aplică imaginilor de intrare în rețea și este echivalentul stabilirii
rezoluției imaginilor de intrare. Practic, dimensiunile fiecărei imagini de intrare vor fi înmulțite
cu multiplicatorul de rezoluție ρ. Astfel, costul de calcul pentru straturile de bază ale rețelei cu
convoluții separabile în adâncime cu multiplicatorul de lățime α și multiplicatorul de rezoluție
ρ devin:
DK × DK × αM × ρDF × ρDF + αM × αN × ρDF × ρDF (1.12)
unde ρ ∈ (0, 1]. Implicit, acest parametru este setat ρ = 1[7], rezoluțiile imaginilor de la intrare
fiind adesea stabilite de către utilizator, valorile cele mai tipice pentru acestea fiind de 224, 192,
160 sau 128 pixeli. Și în acest caz, multiplicatorul de rezoluție are ca efect reducerea costului
de calcul de aproximativ ρ2 ori.
Tabela 1.2: Exemplu de resurse utilizate pentru o convoluție standard tipică CNN și o convoluție
de separare în adâncime, pentru un strat intern din rețea cu DK = 3, M = 512, N = 512, DF =
14 (Sursa: [7])
4. ResNet-50 (2015): a dovedit proiectarea rețelelor și mai profunde (până la 152 de straturi)
fără a compromite puterea de generalizare a modelului, cu un număr de 26 de milioane
de parametri. ResNet este una dintre arhitecturile care a adoptat timpuriu normalizarea
lotului.
Pe baza datelor din ImageNet Leaderboard [22] putem întocmi următorul tabel 1.3, pentru care
observăm că din punct de vedere a resurselor utilizate (dimensiunea rețelei precum și numărul
de parametri) în compromis cu acuratețea obținută pe setul de date ImageNet, arhitectura
MobileNets pare a fi cea mai optimă pentru implementarea aplicației de analiză facială pe
Raspberry Pi 4.
Capitolul 2
Resurse utilizate
2.1.1 Raspberry Pi 4
Raspberry Pi este o serie de calculatoare pe o singură placă de bază (SBC), dezvoltate în Re-
gatul Unit de către fundația Raspberry Pi pentru a promova predarea informaticii de bază în
școli și în țările în curs de dezvoltare. Modelul original a devenit mult mai popular decât cel
anticipat, vânzându-se în afara pieței vizate pentru utilizări precum robotică, centre media,
servere de fișiere, console de jocuri retro și routere. Acum este utilizat pe scară largă chiar și
în proiecte de cercetare, cum ar fi monitorizarea vremii sau proiecte de ML, acestea datorită
costurilor reduse, consumului mic de energie și a portabilității sale[23].
• CPU: Broadcom BCM2711, Quad core Cortex-A72 @ 1.5GHz, cu posibil overclock până
la 2.1GHz
1
În această lucrare se va folosi modelul de 4GB RAM. Acest aspect însă nu este important, deoarece într-un
final aplicația nu va utiliza mai mult de 500MB RAM, putând fi utilizată și pe plăci Raspberry cu mai puțină
memorie, cum ar fi Raspberry Pi 3B+.
18
Figura 2.2: Placa Raspberry Pi 4 cu modul cameră atașat ce va fi folosită în această lucrare
2.2.1 Python
Python este un limbaj de programare la nivel înalt, orientat pe obiecte, cu semantică dinamică.
Este un limbaj de scripting, ceea ce înseamnă că este interpretat, și nu compilat, economisind
astfel mult timp în procesul de dezvoltare și de debugging a aplicațiilor. Sintaxa clară și foarte
simplă subliniază lizibilitatea și reduce costurile de întreținere a programelor. Python acceptă
module și pachete, încurajând modularitatea programului și reutilizarea codului. Modulele cu-
prind o gamă foarte largă de funcționalități, de la cele de bază pentru lucrul cu șiruri și fișiere,
până la cele pentru lucrul cu procese, threaduri, sockets, serializare. Multe dintre aceste mo-
dule oferă o interfață foarte similară programării la nivel de sistem din C, astfel încât funcțiile
Python au același nume si aproximativ aceiași parametrii ca cei pentru funcțiile din C pentru
19
apeluri de sistem[25]. Există o varietate de API-uri2 , cele mai populare la momentul actual
fiind cele pentru aplicații web, roboți AI, știința datelor (data science) și grafice, de exemplu:
Flask, NumPy, SciPy, Pandas, Plotly, Matplotlib.
Unul dintre dezavantajele principale ale acestui limbaj însă, este viteza de execuție. Python
este mult mai lent decât Fortran și C. Fiind un limbaj necompilat, acest lucru înseamnă că la
momentul executării programului, interpretorul nu cunoaște tipul variabilelor care sunt definite,
acesta trebuie să inspecteze header-ul obiect pentru fiecare variabilă pentru a găsi informațiile
despre tip și apoi să apeleze rutina operației corespunzătoare pentru interacțiunile dintre tipurile
de obiecte. În cele din urmă, trebuie să creeze și să inițializeze un nou obiect Python pentru a
reține valoarea returnată a operațiilor dintre variabile[26]. Astfel, spre deosebire de interpretor,
un compilator poate privi înainte și optimiza operațiunile repetate sau redundante, ceea ce
poate duce la accelerații mari în viteză.
2.2.2 OpenCV
OpenCV (Open Source Computer Vision Library) este o bibliotecă cu sursă deschisă (open-
source) de viziune computerizată. Biblioteca este scrisă în limbajele C și C++, funcționează
pe sistemele de operare Linux, Windows și Mac OS X și se află în continuă dezvoltare și pe
interfețele scrise în limbaje precum Python, Ruby și Matlab. Aceasta a fost realizată pentru
aplicațiile de imagistică computerizată, cuprinzând algoritmi pentru detecția recunoașterea a
obiectelor (incluzând figuri faciale), clasifica acțiuni, urmări obiecte din capturi video și chiar
extragerea diverselor tipuri de obiecte dintr-un fundal. Totodată biblioteca OpenCV a fost
proiectată pentru eficiența calculelor cu un accent puternic pe aplicațiile în timp real. OpenCV
este scris în C optimizat și poate profita de procesoare multi nucleu[27].
Astfel, biblioteca OpenCV poate fi alegerea optimă pentru implementarea aplicației de analiză
facială pe Raspberry Pi 4, aplicație ce va prelua cadre în timp real și va folosi toate cele 4
nuclee disponibile pentru procesarea cât mai rapidă acestor cadre.
2.2.3 Keras
Keras este o bibliotecă open-source de rețele neuronale, scrisă în Python pentru învățare pro-
fundă, care are la bază, la alegerea utilizatorului, una din bibliotecile Theano sau Tensorflow[28].
A fost dezvoltat pentru a permite experimentarea rapidă cu rețele neuronale profunde și se
concentrează pe a fi intuitiv și ușor de utilizat. Keras a fost construit ghidându-se pe patru
principii:
• Modularitate: Un model poate fi ușor înțeles dacă este reprezentat ca o secvență sau un
grafic.
• Simplitate: biblioteca oferă suficient de multe resurse pentru a obține un rezultat, fără a
sacrifica din lizibilitatea codului.
• Python: Modelele pot fi definite scriind direct cod Python, fără a fi nevoie personalizarea
sau configurarea altor tipuri de fișiere.
2
API = Application Programming Interface: set de proceduri care permit crearea de aplicații care accesează
caracteristicile sau datele unui sistem de operare, serviciu sau o altă aplicație.
20
Keras conține numeroase implementări ale elementelor de rețea neuronală utilizate frecvent,
cum ar fi straturi neuronale, funcții de activare, optimizatoare, normalizarea lotului precum și
o serie de instrumente pentru a ușura lucrul cu datele de imagine și text pentru a simplifica
codificarea necesară[29].
TensorFlow Lite este un set de instrumente pentru a ajuta dezvoltatorii să ruleze modele Ten-
sorFlow pe dispozitive mobile, integrate și IoT 3 [30].
• Interpretorul TFLite, care rulează modele special optimizate pentru mai multe tipuri de
platforme hardware, inclusiv telefoane mobile, dispozitive Linux încorporate și microcon-
trolere.
• Convertorul TFLite, care transformă modelele TF într-o formă eficientă pentru utilizarea
interpretorului și poate introduce optimizări pentru îmbunătățirea dimensiunii de stocare
și a performanței.
Convertorul TFLite preia un model TF antrenat (sub forma unui fișier .hdf5) și produce un
fișier TFLite (cu extensie .tflite) care este bazat pe FlatBuffers4 și conține o reprezentare binară
3
IoT = Internet of Things: sistem de dispozitive de calcul inter-relaționate, prevăzute cu identificatori unici
și capacitatea de a transfera date printr-o rețea fără a necesita interacțiune om-mașină.
4
FlatBuffers este o bibliotecă software gratuită de serializare eficientă ce poate fi folosită pe platforme mul-
tiple. Formatul unui fișier obținut după serializarea folosind FlatBuffers poate fi citit de limbaje precum C,
C++, Java, JavaScript, Kotlin, Lua, PHP și Python
21
redusă a modelului original. FlatBuffers joacă un rol esențial în serializarea eficientă a datelor
modelului și în asigurarea accesului rapid la aceste date, menținând în același timp o dimensiune
de stocare mică[31]. Acest lucru este util în special pentru fișierele ce conțin un model (rezultate
în urma antrenării unei rețele) cu foarte multe ponderi, a căror valori sunt foarte mari din punct
de vedere al stocării (de exemplu, ponderi ce au tipul de date float pe 32 sau chiar 64 de biți)
și citirea acestora ar rezulta într-o latență ridicată.
2.2.5 PyQt5
Qt este un set de instrumente open-source pentru crearea de interfețe grafice și aplicații care
rulează pe diverse platforme cum ar fi Linux, Windows, MacOS, Android. PyQt5 este o bi-
bliotecă Python care se bazează pe Qt și include clase care acoperă interfețe grafice pentru
utilizatori, precum și manipulare XML, comunicare în rețea, expresii regulate, thread-uri, baze
de date SQL, multimedia, navigare web și alte tehnologii disponibile în Qt. PyQt5 implemen-
tează peste o mie din aceste clase Qt într-un set de module Python, toate fiind incluse într-un
pachet Python de nivel superior numit PyQt5[32]. Totodată, librăria are o colecție bogată și
modernă de widget-uri care servesc mai multor scopuri. Unele dintre cele mai comune și utile
widget-uri sunt: butoanele, etichetele ce pot servi ca înlocuitor pentru alte obiecte (placehol-
der), modificări de linie (input), cutii combo și butoane radio.
Spre deosebire de Tkinter, o altă librărie Python pentru interfețe grafice, PyQt5 oferă flexibili-
tate în codare, anume programarea GUI cu Qt este concepută în jurul conceptului de semnale și
sloturi pentru a stabili comunicarea între obiecte. Aceasta permite adaptare ușoară a funcțiilor
atunci când trebuie stabilite evenimente (spre exemplu apăsări de butoane).
Având cod C/C++ scris în spate, performanțele nu vor fi afectate dacă funcțiile folosite pentru
clasificarea vârstelor și a genului vor fi înfășurate în această interfață grafică. Totodată, modulul
PyQt5, precum Tkinter, este preinstalat în Python versiunea 3 existentă pe mașinile Linux,
inclusiv pe sistemul de operare Raspbian de pe Raspberry Pi.
Pentru a vedea placa video pusă la dispoziție de Google Colab, vom rula comenzile Bash din
figura 2.4. Observăm că avem la dispoziție o placă video Nvidia P100, pentru care găsim
următoarele specificații[34]:
5
Jupyter Notebook este o aplicație web open-source care permite crearea și partajarea de documente care
conțin cod scris în oricare din limbajele Julia, Python sau R, ecuații, vizualizări grafice și text narativ.
22
• Memorie: 16 GB
Figura 2.4: Aflarea sistemului de operare și a plăcii video disponibile pe platforma Google
Colab
În această lucrare se vor folosi resursele puse la dispoziție de platforma Google Colab, întrucât
placa video dedicată Nvidia P100 va reduce semnificativ timpul de antrenare a rețelei.
23
Capitolul 3
Etape în implementarea aplicației
Pentru antrenarea rețetei, se vor combina două baze de date cu sursă deschisă conținând chi-
puri umane, anume UTKFace și Appa-Real.
UTKFace este un set de date de figuri faciale la scară largă, cu un interval de vârste cuprins
între 1 și 116 ani. Setul de date constă în peste 20.000 de imagini cu adnotări de vârstă, gen
și etnie. Imaginile acoperă variații mari ale plasării spațiale a persoanelor, expresiei faciale,
iluminare și rezoluție. Acest set de date poate fi utilizat într-o varietate de aplicații precum
detectarea feței, estimarea vârstei, progresia / regresia vârstei sau localizarea unor trăsături fa-
ciale. Setul de date UTKFace este disponibil numai în scopuri de cercetare non-comerciale[35].
Baza de date Appa-Real conține 7.591 de imagini cu etichete reale și aparente asociate. Vâr-
stele reale și clasificările de vârstă aparentă sunt furnizate în fișierele .CSV, cu un rând separat
pentru fiecare evaluare[36].
Pentru combinarea și organizarea celor două baze de date în directoare pe categorii de vârste,
respectiv subdirectoare pentru genurile masculin și feminin, voi folosi anumite scripturi Python
pentru a asocia numele fiecărui fișier imagine cu linia din fișierele .CSV1 ce conține informațiile
asupra imaginii (precum vârsta reală și genul). Codul este disponibil în Anexa A.1.
Într-o următoare etapă, pentru imaginile ce conțin persoane integral, se va aplica un script
Python de detecție facială a persoanelor (folosind algoritmul Viola-Jones, integrat în biblioteca
OpenCV) pentru a localiza și decupa chipurile umane din poze, urmând ca acestea să fie salvate
drept imagini separate în același director. Menționez că multe dintre pozele ce conțin chipuri
umane vor avea o rezoluție mai mică de 100 × 100 pixeli după localizarea chipului, astfel am
pus o condiție pentru a ignora acest tip de poze (cod disponibil în Anexa A.2). Într-un final
voi obține o bază de date mult mai mică, dar care va conține în majoritar poze clare. În urma
mutării imaginilor în directoare separate pe categorii de vârste, vom avea o structură precum
cea din figura 3.1.
1
CSV = Comma-Separated values: fișier text delimitat care folosește o virgulă pentru a separa valorile
24
Figura 3.1: Organizarea directoarelor cu imaginile bazelor de date UTKFace, respectiv Appa-
Real (vizualizare folosind aplicația WinDirStat)
Această distribuire a claselor a fost realizată urmărind indicațiile din articolele [37] și [38].
După citirea și încărcarea imaginilor, putem trasa histograma din figura 3.2a. Putem alege
acum o metodă prin care să împărțim imaginile în două categorii: imagini ce vor fi folosite
pentru antrenarea rețelei, respectiv testarea rețelei, ținând cont că momentan imaginile sunt
aranjate în ordine crescătoare de la 4 ani la 75 ani. Astfel:
• Fie vom randomiza lista ce conține toate imaginile și vom prelua 80% dintre acestea
pentru antrenare și 20% pentru testare (dar, în acest mod riscăm ca într-o clasă să avem
foarte puține imagini de antrenare comparativ cu numărul de imagini de testare, sau
invers, în funcție de generarea aleatoare)
• Fie vom prelua 80% pentru antrenare, 20% pentru testare din imaginile fiecărei clase,
începând de la prima imagine din clasa respectivă
• Fie vom prelua imaginile 80% din mijlocul fiecărei clase de imagini, practic, imaginile din
intervalul 10%-90% dintr-o clasă vor reprezenta imaginile de antrenare, iar restul 20% vor
reprezenta imaginile de test. (ilustrare în figura 3.3)
(a) (b)
Figura 3.2: Histogramă cu numărul total de imagini pentru fiecare clasă de vârstă corespunză-
toare (a), respectiv a genului corespunzător (b)
Pentru clasificatorul de estimare a vârstei, vom folosi ultima metodă menționată. Pentru clasi-
ficatorul de estimare a genului, vom folosi prima metodă, întrucât cele două clase (gen masculin
și feminin), au un număr echilibrat de poze în baza de date folosită (figura 3.2b).
Vom salva acum imaginile de antrenare, testare, precum și etichetele corespunzătoare acestora
în fișiere NumPy .npz, pentru a le încărca mai departe pe platforma Google Colab. Vom folosi
funcția savez integrată în librăria NumPy, astfel:
np.savez('AgeClass_train_data_224.npz', np.array(train_images))
np.savez('AgeClass_train_labels_224.npz', np.array(train_labels))
np.savez('AgeClass_test_data_224.npz', np.array(test_images))
np.savez('AgeClass_test_labels_224.npz', np.array(test_labels))
26
Figura 3.3: Preluarea a 80% din imaginile fiecărei clase pentru antrenare, de la mijlocul inter-
valului
Pentru a încărca aceste date în platforma Google Colab, vom defini următoarea funcție:
def load_data_training_and_test(datasetname):
npzfile = np.load(datasetname + "_train_data_224.npz")
train = npzfile['arr_0']
npzfile = np.load(datasetname + "_train_labels_224.npz")
train_labels = npzfile['arr_0']
npzfile = np.load(datasetname + "_test_data_224.npz")
test = npzfile['arr_0']
npzfile = np.load(datasetname + "_test_labels_224.npz")
test_labels = npzfile['arr_0']
Tabela 3.1: Codarea binară a claselor folosind tehnica Hot One Encoding
În sfârșit, putem codifica etichetele pentru fiecare clasă ca tablouri binare, folosind tehnica
”Hot One Encoding” (tabelul 3.1). Această codare permite unui algoritm de învățare automată
să utilizeze informațiile conținute într-o categorie, fără confuzia cauzată de ordinalitate. Tot-
odată, vom normaliza imaginile pentru a ne asigura că fiecare parametru de intrare (pixel, în
acest caz) are o distribuție similară a datelor. Acest lucru face convergența să fie mai rapidă
în timpul în antrenării rețelei.
27
Observăm că ultimele 6 straturi (pre-antrenate) au fost folosite pentru clasificarea celor 1000
de categorii a setului de date ImageNet:
Figura 3.4: Rețeaua MobileNet după înlocuirea ultimelor 6 straturi cu straturile Dropout și
Dense
precum SGD, Adam (derivat din adaptive moment estimation), RMSprop (Root Mean Square
Propagation), Adagrad (adaptive gradient algorithm) și Adadelta. Vom importa algoritmul
SGD, pentru care stabilim ca parametru, conform celor menționate anterior, o rată de învățare
mică (lr = 10−4 ):
SGD cu momentum este o metodă care ajută la accelerarea vectorilor gradienților în direcțiile
corecte, ducând astfel la o convergență mai rapidă. Nesterov Momentum este o versiune ușor
diferită de momentum, care a câștigat recent popularitate[10]. În această versiune, ne uităm
mai întâi la un punct în care impulsul curent pointează și calculează gradienții din acel punct
(figura 3.5).
Figura 3.5: Diferența dintre SGD cu momentum și Nesterov momentum (Sursa: [10])
Într-un pas următor vom inițializa modelul cu funcția de cost, optimizatorul propus și ponderile
inițiale (ponderile sunt preluate de la rețeaua pre-antrenată pe baza de date ImageNet, pe care
le vom reajusta folosind tehnica Fine-Tuning). Altfel spus, vom compila modelul:
model.compile(optimizer=optim,
loss='categorical_crossentropy',
metrics=['accuracy'])
Putem defini înainte de antrenare o listă cu puncte de verificare (checkpoints). Lista callbacks
va putea efectua acțiuni în diferite etape de antrenare. Spre exemplu, vom alege ca modelul
să se oprească din antrenare (EarlyStopping) dacă valoarea funcției de cost nu continuă să
descrească după un număr ales de epoci (în acest caz, ales 20). ModelCheckpoint este folosit
pentru a salva modelul cu cea mai bună performanță până la epoca curentă. Totodată vom
stabili numărul de epoci limită pentru antrenare precum și dimensiunea lotului de antrenare:
Într-un final putem începe antrenarea. Ținând cont că serverul platformei Google Colab ar
putea întrerupe execuția, putem crea o excepție în cazul unei întreruperi în timpul antrenării,
astfel încât modelul antrenat până în acel moment să nu fie pierdut:
30
try:
history = model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(x_test, y_test),
shuffle=True,
callbacks=callbacks_list)
model.save("AgeClass.h5")
except:
print("\nAntrenare întreruptă. Se salvează modelul curent...")
model.save("AgeClass.h5")
print("Copiere modele în Google Drive...)
%cp 'AgeClass.h5' './drive/My Drive/Colab_Notebooks/Licenta/AgeClass.h5'
%cp 'AgeClass_best.h5' './drive/My Drive/Colab_Notebooks/Licenta/AgeClass_best.h5'
Pentru funcția de cost, pe măsură ce numărul de epoci crește, valoarea acesteia scade continuu
pe setul de antrenare, dar de la epoca 7 încetează a se mai îmbunătăți pe setul de testare,
ajungând chiar ca valoarea ei să crească. Echivalent, din punct de vedere al acurateței, aceasta
continuă să crească pe setul de antrenare, dar atinge o valoare de la care nu se mai îmbunătă-
țește. Algoritmul s-a oprit din antrenare însă la epoca 26, anume de la epoca 6 + numărul de
epoci prestabilit în funcția EarlyStopping, menționată anterior.
31
Figura 3.6: Grafice ce reprezintă evoluția funcției de cost (Loss), respectiv a acurateței (Accu-
racy) pentru imaginile de antrenare și testare
Putem să reprezentăm și matricea de confuzie asociată acestui model antrenat (figura 3.7).
Matricea de confuzie reprezintă o măsurare a performanței în cazul unei probleme de clasificare,
unde fiecare rând al matricei reprezintă instanțele dintr-o clasă prezisă, în timp ce fiecare coloană
reprezintă instanțele dintr-o clasă reală (sau invers). Altfel spus, aceasta reprezintă un rezumat
al rezultatelor predicțiilor, unde numărul de predicții corecte și incorecte sunt rezumate cu
valorile numărate și separate pe fiecare clasă[40]. În mod ideal, ne dorim ca această matrice să
aibă valori nenule doar pe diagonala principală, iar în rest doar valori nule.
(a) (b)
Figura 3.7: Matricea de confuzie asociată modelului antrenat, reprezentare sub formă numerică
(a) și sub formă procentuală (b)
Din matricea de confuzie în reprezentare numerică (figura 3.7a) observăm că majoritatea ima-
ginilor au fost clasificate în mod corect, sau cel puțin au fost clasificate într-o clasă vecină.
Totodată, există câteva imagini care au fost clasificate complet greșit. Acest lucru poate apă-
rea datorită bazei de date alese, care conține și poze etichetate într-un mod greșit, fiind necesară
o inspectare a fiecărei clase și o ștergere manuală a pozelor. Totodată, clasele de la 0 până la 3
inclusiv (anume vârstele cuprinse între 4 și 11 ani) au fost etichetate de mai multe ori în mod
eronat, în clasele vecine. Acest lucru se poate datora trăsăturilor faciale foarte asemănătoare
pentru categoriile de vârstă respective, ceea ce ar fi fost mai ușor pentru algoritm dacă cele 3-4
clase ar fi reprezentat o singură clasă.
32
• Clasa 3: [12, 19] ani va avea noul interval de [10, 21] ani
Având la dispoziție etichetele cu vârsta reală (numerică, extrasă din imagini în etapa de pregătire
a bazei de date) pentru fiecare din imaginile de test, putem însuma numărul de cazuri corecte
inițiale, la care adăugăm ca fiind corecte orice estimare a unei categorii de vârstă apropriată
de categoria reală de vârstă (cod disponibil în Anexa B.3). Incluzând aceste limite, obținem o
nouă acuratețe de 68.82%.
Pe măsură ce scădem rata de învățare, valoarea funcției de cost ajunge mai greu într-un minim
local, durata de antrenare fiind mult mai mare. Totodată, minimul local la care ajungem
de fiecare dată se situează în jurul valorii [1.23, 1.35]. Mai observăm că, făcând diferența
dintre numărul total de epoci parcurse și numărul impus de epoci pentru care antrenarea să se
oprească, modelul încetează de fiecare dată să se oprească din a-și îmbunătăți valoarea funcției
de cost după aproximativ [4, 6] epoci.
33
Nr crt. Antrenare 01 Antrenare 02 Antrenare 03 Antrenare 04 Antrenare 05 Antrenare 06
Data-ora antrenării 02iun-13:55 02iun-14:50 02iun-16:02 02iun-17:53 02iun-23:44 11iun-01:33
BatchSize 8 4 2 2 64 2
LearningRate 0.01 0.01 0.001 0.001 0.00001 0.00001
Momentum 0.9 0.9 0.9 0.99 0.9 0.9
EarlyStopping (epoci) 60 10 10 10 100 20
Nr epoci parcurse 66 14 14 15 125 26
ms / pas 8ms/step 15ms/step 26ms/step 41ms/step 18ms/step 21ms/step
s / epocă 70s 133s 227s 358s 157s 185s
Durată antrenare 1h17min 32min 54min 1h30min 5h26min 1h20min
val_loss 1.35 1.26 1.23 1.29 1.47 1.26
val_acc 0.489 0.484 0.491 0.478 0.448 0.496
val_acc(cu limite) 0.676 0.695 0.716 0.704 0.64 0.688
Tabela 3.2: Rezultate ale antrenărilor pentru diverși parametri (clasificarea vârstelor)
Tabela 3.3: Rezultate ale antrenărilor pentru diverși parametri (clasificarea genurilor)
Vom analiza mai detaliat modelul obținut în urma antrenării 02, a cărei valori pentru funcția
de cost (val_loss) este cea mai mică dintre cele 4 încercări. Prezentăm mai întâi câteva etape
din antrenarea rețelei și trasăm graficele din figura 3.8.
34
Figura 3.8: Grafice ce reprezintă evoluția funcției de cost (Loss), respectiv a acurateței (Accu-
racy) pentru imaginile de antrenare și testare, pentru clasificatorul de gen
Se observă că modelul atinge o valoare minimă a funcției de cost pentru setul de test la epoca
a 4-a, apoi rămâne constantă și începe să crească, în timp ce valoarea funcției pentru setul de
antrenare continuă să scadă la fiecare epocă, într-un final acuratețea tinzând către 100%. Și în
acest caz apare fenomenul de supra-dimensionare.
În continuare, reprezentăm matricea de confuzie pentru acest model (figura 3.9). Având doar
2 clase de clasificat, matricea de confuzie va avea 2 linii și 2 coloane. Deducem din această
matrice că majoritatea imaginilor de test au fost clasificate corect, cu excepția unui procentaj
foarte mic dintre acestea. Și aici, acest lucru poate fi datorat bazei de date cu care a fost
antrenată și testată rețeaua, existând posibile imagini cu etichete greșite ce trebuie reetichetate
sau șterse manual.
(a) (b)
Figura 3.9: Matricea de confuzie asociată modelului antrenat, reprezentare sub formă numerică
(a) și sub formă procentuală (b), unde Clasa 0 = genul feminin iar Clasa 1 = genul masculin
35
import tensorflow as tf
model = tf.keras.models.load_model('AgeClass_best_06_11-01-33.h5')
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open('AgeClass_best.tflite', 'wb') as f:
f.write(tflite_model)
În continuare, pentru a utiliza modelul pe Rasberry Pi, se va utiliza mai întâi tf.lite.Interpreter()
pentru a încărca modelul și allocate_tensors() pentru a aloca memorie tensorilor2 . Totodată, se
vor reține în variabile separate detalii (precum numele, indecșii și dimensiunea tensorilor) despre
intrările și ieșirile modelului folosind funcțiile get_input_details() și get_output_details(). Vom
încărca și pregăti cele două modele pentru estimarea vârstei și clasificarea genului astfel:
interpreter_age = tf.lite.Interpreter(
model_path="AgeClass_best.tflite")
interpreter_age.allocate_tensors()
interpreter_gender = tf.lite.Interpreter(
model_path="GenderClass.tflite")
interpreter_gender.allocate_tensors()
# Get input and output tensors
input_details_age = interpreter_age.get_input_details()
output_details_age = interpreter_age.get_output_details()
input_shape_age = input_details_age[0]['shape']
input_details_gender = interpreter_gender.get_input_details()
output_details_gender = interpreter_gender.get_output_details()
input_shape_gender = input_details_gender[0]['shape']
2
Tensorii în librăria TensorFlow reprezintă matrici multidimensionale cu un tip uniform de date. Aceștia
pot fi comparați cu un tip asemănător de date precum matricile din librăria NumPy (np.arrays)
36
Vom prezenta interfața aplicației, realizată în PyQt5 (care se poate vizualiza în figura 3.11).
Aceasta este simplă de utilizat, conținând doar 3 butoane în meniul principal, și anume:
• pentru deschiderea camerei atașate pe Raspberry Pi, ce captează cadre în timp real, care
în același timp localizează chipurile umane desenând un pătrat în jurul acesteia și clasifică
persoana după categoria de vârstă și gen (figura 3.12a).
• pentru deschiderea unei singure poze aflată într-un director local, pentru care se va face
automat clasificarea de vârstă și gen dacă există o persoană în poză (figura 3.12b).
• pentru selectarea unui director local ce conține doar imagini, urmând ca utilizatorul să
vizualizeze în cascadă imaginile clasificate (figura 3.12c).
În interfața PyQt5, fiecare dintre aceste butoane vor duce către ferestre separate în aplica-
ție. Pentru fiecare fereastră, am implementat o clasă aferentă ce derivă din clasa QWidget,
inclusă în librăria PyQt5. Clasele vor purta denumirile: UI_CameraWindow, UI_SelectPic și
UI_SelectGallery.
(a) (b)
(c)
Figura 3.12: Interfața pentru fiecare din cele 3 submeniuri: camera în timp real (a), selectarea
unei imagini (b), selectarea unui director (c)
# start/stop timer
def controlTimer(self):
if not self.timer.isActive():
self.webcam = cv2.VideoCapture(0)
self.webcam.set(cv2.CAP_PROP_FRAME_WIDTH, 640) # 640x480, 320x240
self.webcam.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
# start timer
self.timer.start(20)
# update Btn_camera_on_off text (label)
self.Btn_camera_on_off.setText("Stop Camera")
else:
# stop timer
self.timer.stop()
# release video webcam
self.webcam.release()
# update Btn_camera_on_off text
self.Btn_camera_on_off.setText("Start")
38
În sfârșit, putem defini și funcția de actualizare a camerei, unde vom procesa și cadrele în timp
real (localizare chipuri umane, preprocesare imagine decupată, clasificare și desenare rezultate).
În această funcție vom apela self.webcam.read() pentru a prelua cadrul curent cu ajutorul
librăriei OpenCV, vom converti temporar (într-o variabilă separată) cadrul curent în nuanțe de
gri pentru a putea aplica algoritmul Viola-Jones de detecție a chipurilor umane și vom salva
coordonatele unde există acestea:
def Webcam_update(self):
time_start = time() # (opțional) pentru afișare FPS la fiecare frame
_, frame = self.webcam.read()
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
faces = face_cascade.detectMultiScale(
gray_frame, scaleFactor=1.2, minNeighbors=5)
for x, y, w, h in faces:
input_im = frame[y:y + h, x:x + w]
Dacă există chipuri umane detectate în cadrul curent, putem pre-procesa imaginea în formatul
cerut la intrarea modelului, anume: vom redimensiona imaginea la rezoluția 224 × 224, converti
valorile în tipul de date f loat, normaliza plaja valorilor pixelilor din [0, 255] în [0, 1] și vectoriza
imaginea. Putem acum să clasificăm imaginea apelând metodele set_tensor() și get_tensor()
pentru fiecare din cele 2 modele pentru estimarea vârstei și clasificarea genului.
for x, y, w, h in faces:
input_im = frame[y:y + h, x:x + w]
# Prezicere
input_data = np.array(input_im, dtype=np.float32)
interpreter_age.set_tensor(
input_details_age[0]['index'], input_data)
interpreter_age.invoke()
interpreter_gender.set_tensor(
input_details_gender[0]['index'], input_data)
interpreter_gender.invoke()
output_data_age = interpreter_age.get_tensor(
output_details_age[0]['index'])
output_data_gender = interpreter_gender.get_tensor(
output_details_gender[0]['index'])
index_pred_age = int(np.argmax(output_data_age))
index_pred_gender = int(np.argmax(output_data_gender))
prezic_age = string_pred_age[index_pred_age]
prezic_gender = string_pred_gen[index_pred_gender]
În figura 3.13 am prezentat câteva rezultate în cadrul clasificării imaginilor captate în timp real
de la camera web atașată plăcii. Se observă că avem un număr de aproximativ 2.3 cadre pe
secundă (FPS) pentru clasificarea unei singure persoane. Astfel, Raspberry Pi 4, reușește să
detecteze și să analizeze un chip uman în aproximativ 430 ms. În figura 3.13c se observă cum
performanța scade drastic pentru detecția și analizarea a două persoane simultan (1.32 FPS).
39
(a) (b)
(c)
Figura 3.13: Rezultate ale clasificării în timp real folosind camera web atașată
Mai putem încerca câteva teste ce țin de performanță. Vom crea acum o aplicație care utilizează
doar camera web prin intermediul librăriei OpenCV, fără a avea o interfață grafică. Observăm
acum în figura 3.14 că obținem aproximativ 2.6 cadre pe secundă, adică un timp de procesare
de 380 ms per cadru, deci, avem o îmbunătățire puțin semnificativă de 50 ms. Putem aduce
altă modificare și anume scăderea rezoluției de la 640 × 480 la 320 × 240 în cadrul aplicației
(figura 3.15). În acest caz vom avea în jur de 2.9 cadre pe secundă, 340 ms per cadru.
40
(a) (b)
Figura 3.14: Rezultate ale clasificării în timp real, fără a folosi o interfață grafică
Figura 3.15: Rezultate ale clasificării în timp real, pentru o rezoluție de intrare de 320 × 240
class UI_SelectPic(QWidget):
def __init__(self):
# Partea de UI Setup
. . .
def browseImage(self):
filePath, _ = QFileDialog.getOpenFileName(
self, 'Open an Image', '/home/pi/Desktop/Documents',
'Image files (*.jpg *.bmp *.png)')
• În cazul meniului de selecție a unui folder, vom salva denumirile tuturor imaginilor din
acesta într-o listă pe care o vom transforma ulterior într-un obiect Python de tip generator.
Cu acest generator vom putea apela funcția next() care ne va returna câte un element
din lista generator (care conține denumirile fișierelor). Această funcție next() putem să
o asociem mai departe unui buton din interfața PyQt5, astfel încât la fiecare apăsare a
butonului Next să obținem o nouă cale către imaginea din dosarul selectat, să o citim
folosind aceeași funcție classifyImage() apoi să întoarcem rezultatele clasificării. Funcția
classifyImage() definită separat poate fi găsită, alături de întregul cod al aplicației, în
Anexa C.
class UI_SelectGallery(QWidget):
def __init__(self):
# Partea de UI Setup
. . .
def GetFolderPath(self):
self.image_nr = 1
self.imageNames = []
self.folderPath = QFileDialog.getExistingDirectory(
self, "Select Directory", '/home/pi/Desktop/Documents')
class Found(Exception):
pass
try:
for dirname, dirnames, filenames in os.walk(self.folderPath):
for filename in filenames:
# Verifica daca folderul contine imagini
if filename[-4:] not in ['.jpg', '.png', '.bmp']:
raise Found
self.imageNames.append(filename)
except Found:
error_msg = QMessageBox()
error_msg.setIcon(QMessageBox.Critical)
error_msg.setText("Eroare:")
error_msg.setInformativeText(
'Directorul nu contine imagini!')
error_msg.setWindowTitle("Error")
error_msg.exec_()
# print(self.imageNames)
self.imageNames_gen = (_ for _ in self.imageNames) # Creez generator
def NextImage(self):
if self.imageNames_gen is not None:
self.imageName = next(self.imageNames_gen, None)
else:
self.PlaceHolder_Label.setText(
"Momentan niciun folder nu este selectat!!!".upper())
42
return
În figura 3.16 am prezentat rezultatul clasificării unei singure imagini. În figura 3.17 am prezen-
tat rezultatele clasificării mai multor imagini aflate într-un dosar. Totodată, nefiind necesară o
procesare continuă a cadrelor (procesorul revine înapoi la starea de idle după clasificarea unei
imagini), am atașat și un string cu gradele de apartenență a imaginii curente la fiecare clasă.
(a) (b)
Figura 3.16: Rezultatul clasificării unei singure imagini. Aceste poze de validare au fost preluate
de pe ThisPersonDoesNotExist
43
(a) (b)
(c)
Figura 3.17: Clasificare a mai multor imagini aflate într-un dosar, afișare în cascadă. Aceste
poze de validare au fost preluate de pe ThisPersonDoesNotExist
44
Capitolul 4
Concluzii și viitoare implementări
Principalul scop al acestei lucrări a fost găsirea unei arhitecturi convoluționale care să ofere
funcționalități de analiză a atributelor faciale, unde întreaga aplicație a fost optimizată pentru a
rula pe calculatoarele de tip Raspberry Pi. Acest scop a fost atins în cele din urmă, prezentând
totodată rezultate în cazul alegerii altor decizii de proiectare (precum parametri de antrenare
a rețelei). În starea actuală, aplicația este capabilă să efectueze detectarea și clasificarea chi-
purilor umane în funcție de categorie de vârstă și gen. Aplicația poate realiza aceste clasificări
folosind secvențe video captate în timp real sau imagini salvate local în memorie. Cu toate că
performanța dobândita (de 2.3 cadre pe secundă, 430 ms timp de procesare per cadru), nu este
extraordinară, există suficiente metode pentru îmbunătățire, ce vor fi enunțate în continuare.
Clasificarea persoanelor în funcție de vârstă nu este o sarcină însă foarte ușoară, întrucât aceasta
este afectată de mulți factori precum mediul, stilul de viață ales, existența problemelor medicale
sau chiar prezența produselor cosmetice. Acești factori pot îngreuna clasificarea vârstelor (sau
a genului) în cadrul învățării automate a mașinii, uneori punând în dificultate și estimarea din
perspectiva umană. De asemenea, multe dintre bazele de date etichetate ce pot fi folosite gra-
tuit de pe Internet pot conține astfel imagini etichetate în mod eronat, în alte clase decât cele
corespunzătoare. În cazul acestui proiect s-au efectuat în plus ștergeri manuale ale imaginilor
ce nu corespundeau categoriilor vârstă și gen.
Revenind la partea de rezultate, am observat în această lucrare că oricât de mult au fost schim-
bați parametri de antrenare precum rata de învățare, dimensiunea lotului, numărul de epoci și
ajustarea optimizatorului gradient descendent stocastic, ajungeam la o valoare a acurateței de
test ce nu putea să depășească 50%. Aceasta deoarece, clasele nu sunt separate corespunzător,
algoritmul fiind penalizat pentru clasificarea unei persoane într-o categorie de vârstă extrem
de apropriată (la diferențe chiar de 1-2 ani) față de categoria reală. Totodată, fenomenul de
supra-dimensionare (overfitting) a modelului apărea de fiecare dată în antrenări, datorită uti-
lizării unei baze de date relativ mici (de aproximativ 10 000 de imagini, comparativ cu setul
de date ImageNet ce conține 14 milioane de imagini), chiar dacă s-a favorizat reglarea fină a
ponderilor cu valori inițiale deja stabilite în locul unor valori generate aleator la fiecare nouă
antrenare.
Viitoare implementări
Câteva dintre ideile pentru dezvoltarea acestei aplicații pe viitor, vor cuprinde tehnologii pre-
cum:
• Cython. Cython este un limbaj de programare compilat care își propune să fie un superset
al limbajului de programare Python, conceput pentru a oferi performanțe asemănătoare
limbajului C sau C++, eliminând astfel dezavantajele menționate în subcapitolul 2.2.1.
Spre exemplu, în această aplicație, cel puțin pentru realizarea interfeței grafice dar și
pentru clasificare, s-au folosit multe variabile ce conțineau liste sau diferite informații în
45
• Caffe. Caffe este un framework de Deep Learning care acceptă multe tipuri diferite de
arhitecturi orientate spre clasificarea și segmentarea imaginilor. Caffe este orientat către
telefoanele mobile și alte platforme restrânse din punct de vedere computațional. Folosind
acest framework, este posibil să antrenăm și să ajustăm parametri într-un timp mult mai
scurt, iar modelul final în forma comprimată (cu extensia .caffemodel) ce conține toată
structura și ponderile rețelei antrenate, ar putea avea o dimensiune pe disc chiar mai
mică.
Bibliografie
[14] Constantin Vertan. Prelucrarea și Analiza Imaginilor. Editura Printech Bucuresti, 1999.
[18] Grace Lindsay. Convolutional neural networks as a model of the visual system: past,
present, and future. Journal of Cognitive Neuroscience, 2020.
[19] Matthew D Zeiler and Rob Fergus. Visualizing and understanding convolutional networks.
European conference on computer vision, 2014.
[26] Jake VanderPlas. Why Python is Slow: Looking Under the Hood.
https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/. [Online; acce-
sat 6-Iun-2020].
[27] Gary Bradski and Adrian Kaehler. Learning OpenCV: Computer vision with the OpenCV
library. ” O’Reilly Media, Inc.”, 2008.
[28] Jason Brownlee. Deep learning with Python: develop deep learning models on Theano and
TensorFlow using Keras. Machine Learning Mastery, 2016.
48
[31] Airen Surzyn. How TensorFlow Lite Optimizes Neural Networks for Mobile Machine
Learning.
https://heartbeat.fritz.ai/how-tensorflow-lite-optimizes-neural-networks-
for-mobile-machine-learning-e6ffa7f8ee12, 2019. [Online; accesat 7-Iun-2020].
[32] Leodanis Pozo Ramos. Python and PyQt: Building a GUI Desktop Calculator.
https://realpython.com/python-pyqt-gui-calculator/#understanding-pyqt,
2019. [Online; accesat 8-Iun-2020].
[36] E Agustsson, R Timofte, S Escalera, X Baro, I Guyon, R Rothe. Apparent and real age
estimation in still images with deep residual regressors on APPA-REAL database. 2017.
[37] Wen-Bing Horng, Cheng-Ping Lee, and Chun-Wen Chen. Classification of age groups
based on facial features. Tamkang Journal of Science and Engineering, 2001.
Anexa A
Organizarea bazei de date
import os
import shutil
train_test = "train"
varsta = "20-27"
# Citesc prima coloana din fisierele .CSV care contine numele imaginii jpeg
# Citesc a 5-a coloana din fiserele .CSV care contine varsta imaginii respective
lista_names = []
lista_ages = []
with open("gt_avg_{} - {}ani.csv".format(train_test, varsta)) as f:
for row in f:
lista_names.append(row.split()[0])
lista_ages.append(row.split(',')[4])
fullpath = os.path.join
dir_start = "./{}".format(train_test)
dir_target = "./imagini_appa/{}".format(varsta) # !
if not os.path.exists(dir_target):
os.makedirs(dir_target)
def Move_images():
count = 0
for dirname, dirnames, filenames in os.walk(dir_start):
for filename in filenames:
source = fullpath(dirname, filename)
# print(filename[0:6])
if filename[0:6] in lista_names:
shutil.move(source, fullpath(dir_target, filename))
count += 1
50
def Rename_images():
count = 0
for dirname, dirnames, filenames in os.walk(dir_target):
for filename in filenames:
for i in range(len(lista_names)):
if filename[0:6] == lista_names[i]:
os.rename(dir_target + '/' + filename,
dir_target + '/' + lista_ages[i] + '_' + filename)
count += 1
if __name__ == "__main__":
Move_images()
Rename_images()
facedata = "haarcascade_frontalface_default.xml"
cascade = cv2.CascadeClassifier(facedata)
def face_crop(filename):
img = cv2.imread(filename)
faces = cascade.detectMultiScale(img)
for f in faces:
x, y, w, h = [v for v in f]
sub_face = img[y:y + h, x:x + w]
if __name__ == '__main__':
print("Se proceseaza...")
for dirname, dirnames, filenames in os.walk('./'):
for filename in filenames:
if filename.endswith('.jpg'):
face_crop(filename)
print(".", end='')
51
import os
import cv2
import matplotlib.pyplot as plt
path_imagini_main = "./imagini_VGG_cu_appa"
lista_foldere = ["04-06", "07-08", "09-11", "12-19",
"20-27", "28-35", "36-45", "46-60", "61-75"]
lista_subfoldere = ['f', 'm']
. . .
# ## Met3: Preluam 80% din MIJLOCUL fiecarei clase (folosesc o matrice tridimensionala):
# ## Anume, voi lua intre 10% si 90% din fiecare clasa (trebuie sa retin cele 2 margini)
len_total = (len(lista_imag_label))
len_train = []
for i in range(len(lista_imag_label)):
len_train.append([int(0.1 * len(lista_imag_label[i])),
int(0.9 * len(lista_imag_label[i]))])
train_images = []
train_labels = []
train_ages = []
for i in range(len(lista_imag_label)):
52
test_images = []
test_labels = []
test_ages = []
for i in range(len(lista_imag_label)):
for j in range(0, len_train[i][0]):
test_images.append(lista_imag_label[i][j][0])
test_labels.append(lista_imag_label[i][j][1])
test_ages.append(lista_imag_label[i][j][2])
for j in range(len_train[i][1], len(lista_imag_label[i])):
test_images.append(lista_imag_label[i][j][0])
test_labels.append(lista_imag_label[i][j][1])
test_ages.append(lista_imag_label[i][j][2])
np.savez('AgeClass_train_data_224.npz', np.array(train_images))
np.savez('AgeClass_train_labels_224.npz', np.array(train_labels))
np.savez('AgeClass_test_data_224.npz', np.array(test_images))
np.savez('AgeClass_test_labels_224.npz', np.array(test_labels))
53
Anexa B
Modificarea, antrenarea și ajustarea rețelei
mobile = applications.mobilenet.MobileNet()
x = mobile.layers[-6].output
x = Dropout(0.5)(x)
x = datetime.now(); print(x)
try:
history = model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(x_test, y_test),
shuffle=True,
callbacks=callbacks_list)
model.save("AgeClass.h5")
except:
print("\nAntrenare intrerupta. Se salveaza modelul curent...")
model.save("AgeClass.h5")
54
y = datetime.now(); print(y)
print("Durata totala de antrenare: {}".format(abs(y-x)))
Anexa C
Întreaga aplicație pe Raspberry Pi, înfășurată într-o
interfață grafică
1 import sys
2 import os
3 from PyQt5.QtCore import *
4 from PyQt5.QtWidgets import *
5 from PyQt5.QtGui import QPainter, QPixmap, QImage
6
7 import tensorflow as tf # version 1.15.0
8 from tensorflow.keras.preprocessing.image import img_to_array
9 import numpy as np
10 import cv2
11 from time import time
12
13 string_pred_age = ['04 - 06 ani', '07 - 08 ani', '09 - 11 ani', '12 - 19 ani',
14 '20 - 27 ani', '28 - 35 ani', '36 - 45 ani', '46 - 60 ani', '61 - 75 ani']
15 string_pred_gen = ['Feminin', 'Masculin']
16
17 # Load TFLite model and allocate tensors. Load Face Cascade
18 face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
19
20 interpreter_age = tf.lite.Interpreter(
21 model_path="AgeClass_best_06_02-16-02.tflite")
22 interpreter_age.allocate_tensors()
23
24 interpreter_gender = tf.lite.Interpreter(
25 model_path="GenderClass_06_03-20-08.tflite")
26 interpreter_gender.allocate_tensors()
27
28 # # Get input and output tensors
29 input_details_age = interpreter_age.get_input_details()
30 output_details_age = interpreter_age.get_output_details()
31 input_shape_age = input_details_age[0]['shape']
32
33 input_details_gender = interpreter_gender.get_input_details()
34 output_details_gender = interpreter_gender.get_output_details()
35 input_shape_gender = input_details_gender[0]['shape']
36
37 class PicButton(QAbstractButton):
38 def __init__(self, pixmap, pixmap_hover, pixmap_pressed, parent=None):
39 super(PicButton, self).__init__(parent)
40 self.pixmap = pixmap
41 self.pixmap_hover = pixmap_hover
42 self.pixmap_pressed = pixmap_pressed
43
44 self.pressed.connect(self.update)
45 self.released.connect(self.update)
46
47 def paintEvent(self, event):
48 pix = self.pixmap_hover if self.underMouse() else self.pixmap
49 if self.isDown():
50 pix = self.pixmap_pressed
51
52 painter = QPainter(self)
53 painter.drawPixmap(event.rect(), pix)
54
55 def enterEvent(self, event):
56 self.update()
57
58 def leaveEvent(self, event):
59 self.update()
60
61 def sizeHint(self):
56
137
138 CameraWindow_Layout = QVBoxLayout()
139
140 # Adauga buton deschidere camera web
141 self.Btn_camera_on_off = QPushButton(self)
142 self.Btn_camera_on_off.setStyleSheet("background-color: white")
143 CameraWindow_Layout.addWidget(self.Btn_camera_on_off)
144
145 # Adauga placeholder pentru a afisa continutul webcam
146 self.PlaceHolder_CameraLabel = QLabel()
147 CameraWindow_Layout.addWidget(self.PlaceHolder_CameraLabel)
148
149 # Adauga placeholder pentru a afisa FPS (numar cadre pe secunda)
150 self.PlaceHolder_FPS = QLabel()
151 self.PlaceHolder_FPS.setStyleSheet("color: white")
152 self.PlaceHolder_FPS.resize(20, 100)
153 CameraWindow_Layout.addWidget(self.PlaceHolder_FPS)
154
155 self.setLayout(CameraWindow_Layout)
156
157 self.retranslateUI()
158
159 def retranslateUI(self):
160 _translate = QCoreApplication.translate
161 self.setWindowTitle(_translate(
162 "App", "Age Classification: Live Webcam"))
163 self.setStyleSheet("background-color: black;")
164
165 self.PlaceHolder_CameraLabel.setText(
166 _translate("App", ""))
167 self.Btn_camera_on_off.setText(_translate("App", "Start Camera"))
168
169 # ## Partea de OpenCV Camera
170 # create a timer
171 self.timer = QTimer()
172 # set timer timeout callback function
173 self.timer.timeout.connect(self.Webcam_update)
174 # set Btn_camera_on_off callback clicked function
175 self.Btn_camera_on_off.clicked.connect(self.controlTimer)
176
177 def Webcam_update(self):
178 time_start = time() # pentru afișare FPS la fiecare frame
179
180 _, frame = self.webcam.read()
181 gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
182 faces = face_cascade.detectMultiScale(
183 gray_frame, scaleFactor=1.2, minNeighbors=5)
184
185 for x, y, w, h in faces:
186 input_im = frame[y:y + h, x:x + w]
187
188 # Verifica dacă există chipuri umane in cadrul curent:
189 if input_im is not None:
190 # Pre-procesare imagine pentru formatul cerut la intrarea modelului
191 input_im = cv2.resize(input_im, (224, 224))
192 input_im = input_im.astype('float')
193 input_im = input_im / 255
194 input_im = img_to_array(input_im)
195 input_im = np.expand_dims(input_im, axis=0)
196
197 # Prezicere
198 input_data = np.array(input_im, dtype=np.float32)
199 interpreter_age.set_tensor(
200 input_details_age[0]['index'], input_data)
201 interpreter_age.invoke()
202 interpreter_gender.set_tensor(
203 input_details_gender[0]['index'], input_data)
204 interpreter_gender.invoke()
205
206 output_data_age = interpreter_age.get_tensor(
207 output_details_age[0]['index'])
208 output_data_gender = interpreter_gender.get_tensor(
209 output_details_gender[0]['index'])
210 index_pred_age = int(np.argmax(output_data_age))
211 index_pred_gender = int(np.argmax(output_data_gender))
58
212
213 prezic_age = string_pred_age[index_pred_age]
214 prezic_gender = string_pred_gen[index_pred_gender]
215
216 font = cv2.FONT_HERSHEY_PLAIN
217 cv2.putText(frame, prezic_age + ', ' + prezic_gender,
218 (x, y), font, 1, (255, 255, 255), 1, cv2.LINE_AA)
219 cv2.rectangle(frame, (x, y), (x + w, y + h),
220 (255, 255, 255), 1)
221
222 # Frame info for PyQT
223 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
224 frame_height, frame_width, frame_channel = frame.shape
225 bytesPerLine = frame_channel * frame_width
226
227 # create QImage from frame
228 qImg = QImage(frame.data, frame_width, frame_height,
229 bytesPerLine, QImage.Format_RGB888)
230 # show frame in img_label
231 self.PlaceHolder_CameraLabel.setPixmap(QPixmap.fromImage(qImg))
232
233 fps = "FPS: " + str(round(1.0 / (time() - time_start), 2))
234 self.PlaceHolder_FPS.setText(fps)
235
236 # start/stop timer
237 def controlTimer(self):
238
239 if not self.timer.isActive():
240 self.webcam = cv2.VideoCapture(0)
241 self.webcam.set(cv2.CAP_PROP_FRAME_WIDTH, 640) # 640x480, 320x240
242 self.webcam.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
243
244 # start timer
245 self.timer.start(20)
246 # update Btn_camera_on_off text
247 self.Btn_camera_on_off.setText("Stop Camera")
248
249 else:
250 # stop timer
251 self.timer.stop()
252 # release video webcam
253 self.webcam.release()
254 # update Btn_camera_on_off text
255 self.Btn_camera_on_off.setText("Start")
256
257
258 class UI_SelectPic(QWidget):
259 def __init__(self):
260 # ## Partea de UI Setup
261 super(UI_SelectPic, self).__init__()
262
263 self.setObjectName("SelectPic_Window")
264 self.setWindowTitle("Age Classification: Select an Image")
265 self.setStyleSheet("background-color: black;")
266 self.resize(400, 400)
267
268 SelectPic_Layout = QVBoxLayout()
269
270 # Adauga buton Browse for image
271 self.Btn_BrowseImage = QPushButton(self)
272 self.Btn_BrowseImage.setStyleSheet("background-color: white")
273 self.Btn_BrowseImage.setText("Browse an image!")
274 self.Btn_BrowseImage.clicked.connect(self.browseImage)
275 SelectPic_Layout.addWidget(self.Btn_BrowseImage)
276
277 # Adauga placeholder pentru a afisa imaginea
278 self.PlaceHolder_Pic = QLabel()
279 SelectPic_Layout.addWidget(self.PlaceHolder_Pic)
280
281 # Adauga placeholder label pentru a afisa probabilitatile
282 self.PlaceHolder_Outputs = QLabel()
283 self.PlaceHolder_Outputs.adjustSize()
284 self.PlaceHolder_Outputs.setStyleSheet("color: white")
285 SelectPic_Layout.addWidget(self.PlaceHolder_Outputs)
286
59
287 self.setLayout(SelectPic_Layout)
288
289 def browseImage(self):
290 filePath, _ = QFileDialog.getOpenFileName(
291 self, 'Open an Image', '/home/pi/Desktop/Documents', 'Image files (*.jpg *.bmp *.png)')
292
293 # ## Procesare imagine, clasificare
294 pixmap, outputs_str = classifyImage(filePath)
295
296 # ## Display image
297 if pixmap.width() > 500 or pixmap.height() > 500:
298 pixmap = pixmap.scaled(
299 500, 500, Qt.KeepAspectRatio, Qt.SmoothTransformation)
300 self.PlaceHolder_Pic.setPixmap(QPixmap(pixmap))
301 self.PlaceHolder_Outputs.setText(outputs_str)
302
303
304 class UI_SelectGallery(QWidget):
305 def __init__(self):
306 # ## Partea de UI Setup
307 super(UI_SelectGallery, self).__init__()
308
309 self.setObjectName("SelectGallery_Window")
310 self.setWindowTitle("Age Classification: Select a Folder with Images")
311 self.setStyleSheet("background-color: black;")
312 self.resize(400, 400)
313
314 self.folderPath = None
315 self.imageNames = []
316 self.imageNames_gen = None
317 self.image_nr = 1
318
319 SelectPic_Layout = QVBoxLayout()
320
321 # Adauga buton Browse for image
322 self.Btn_SelectFolder = QPushButton(self)
323 self.Btn_SelectFolder.setStyleSheet("background-color: white")
324 self.Btn_SelectFolder.setText("Select a folder with images!")
325 self.Btn_SelectFolder.clicked.connect(self.GetFolderPath)
326 SelectPic_Layout.addWidget(self.Btn_SelectFolder)
327
328 # Adauga placeholder pentru a afisa imaginea
329 self.PlaceHolder_Pic2 = QLabel()
330 SelectPic_Layout.addWidget(self.PlaceHolder_Pic2)
331
332 # Adauga buton pentru afisare Next Image
333 self.Btn_NextImage = QPushButton(self)
334 self.Btn_NextImage.setStyleSheet("background-color: white")
335 self.Btn_NextImage.setText("Next image")
336 self.Btn_NextImage.clicked.connect(self.NextImage)
337 SelectPic_Layout.addWidget(self.Btn_NextImage)
338
339 # Adauga label pentru afisare nr imagine
340 self.PlaceHolder_Label = QLabel(
341 "Momentan niciun folder nu este selectat!")
342 self.PlaceHolder_Label.setStyleSheet("color:white")
343 self.PlaceHolder_Label.adjustSize()
344 SelectPic_Layout.addWidget(self.PlaceHolder_Label)
345
346 self.setLayout(SelectPic_Layout)
347
348 def GetFolderPath(self):
349 self.image_nr = 1
350 self.imageNames = []
351 self.folderPath = QFileDialog.getExistingDirectory(
352 self, "Select Directory", '/home/pi/Desktop/Documents')
353
354 class Found(Exception):
355 pass
356 try:
357 for dirname, dirnames, filenames in os.walk(self.folderPath):
358 for filename in filenames:
359 # Verifica daca folderul contine imagini
360 if filename[-4:] not in ['.jpg', '.png', '.bmp']:
361 raise Found
60
362 self.imageNames.append(filename)
363 except Found:
364 error_msg = QMessageBox()
365 error_msg.setIcon(QMessageBox.Critical)
366 error_msg.setText("Eroare:")
367 error_msg.setInformativeText(
368 'Directorul nu contine imagini!')
369 error_msg.setWindowTitle("Error")
370 error_msg.exec_()
371
372 # print(self.imageNames)
373 self.imageNames_gen = (_ for _ in self.imageNames) # Creez generator
374
375 def NextImage(self):
376 if self.imageNames_gen is not None:
377 self.imageName = next(self.imageNames_gen, None)
378 else:
379 self.PlaceHolder_Label.setText(
380 "Momentan niciun folder nu este selectat!!!".upper())
381 return
382
383 # Aici depinde de sistemul de operare, Windows (\), Linux (/), deci folosim normpath()
384 if self.imageName is not None:
385 full_path = os.path.join(os.path.normpath(
386 self.folderPath), self.imageName)
387
388 # ## Procesare imagine, clasificare
389 pixmap, outputs_str = classifyImage(full_path)
390
391 # ## Display image
392 if pixmap.width() > 500 or pixmap.height() > 500:
393 pixmap = pixmap.scaled(
394 500, 500, Qt.KeepAspectRatio, Qt.SmoothTransformation)
395 self.PlaceHolder_Pic2.setPixmap(QPixmap(pixmap))
396
397 self.PlaceHolder_Label.setText(
398 "Imaginea {} din {}\n{}".format(self.image_nr, len(self.imageNames), outputs_str))
399 self.image_nr += 1
400 else:
401 self.PlaceHolder_Label.setText(
402 "S-au afisat toate cele {} imagini!\n".format(len(self.imageNames))+
403 "Selectati un folder nou sau inchideti!")
404
405
406 def classifyImage(filePath):
407 frame = cv2.imread(filePath)
408 gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
409
410 # Detectie fata (preluare coordonate) cu HaarCascade:
411 faces = face_cascade.detectMultiScale(
412 gray_frame, scaleFactor=1.01, minNeighbors=5)
413 for x, y, w, h in faces:
414 input_im = frame[y:y + h, x:x + h]
415 input_im = cv2.resize(input_im, (224, 224))
416 input_im = input_im.astype('float')
417 input_im = input_im / 255
418 input_im = img_to_array(input_im)
419 input_im = np.expand_dims(input_im, axis=0)
420
421 # Predict
422 input_data = np.array(input_im, dtype=np.float32)
423 interpreter_age.set_tensor(input_details_age[0]['index'], input_data)
424 interpreter_age.invoke()
425 interpreter_gender.set_tensor(
426 input_details_gender[0]['index'], input_data)
427 interpreter_gender.invoke()
428
429 output_data_age = interpreter_age.get_tensor(
430 output_details_age[0]['index'])
431 output_data_gender = interpreter_gender.get_tensor(
432 output_details_gender[0]['index'])
433 index_pred_age = int(np.argmax(output_data_age))
434 index_pred_gender = int(np.argmax(output_data_gender))
435 prezic_age = string_pred_age[index_pred_age]
436 prezic_gender = string_pred_gen[index_pred_gender]
61
437
438 font = cv2.FONT_HERSHEY_PLAIN
439 cv2.putText(frame, "{}, {}".format(prezic_age, prezic_gender),
440 (x, y), font, 2, (255, 255, 255), 1, cv2.LINE_AA)
441
442 # Creeaza o lista cu grad apartenenta la fiecare clasa [%]
443 outputs_prob = output_data_age.tolist()[0]
444 outputs_prob = [[string_pred_age[i] + ':',
445 round(outputs_prob[i], 4)] for i in range(len(outputs_prob))]
446 outputs_prob.sort(key=lambda c: -c[1])
447 outputs_str = prezic_gender + '\n'
448 for i in range(len(outputs_prob)):
449 outputs_str += " ".join(str(_) for _ in outputs_prob[i]) + '\n'
450 # print(outputs_str)
451
452 # Convertire obiect imagine OpenCV într-un obiect imagine PyQt (QImage)
453 # Frame info for PyQT:
454 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
455 frame_height, frame_width, frame_channel = frame.shape
456 bytesPerLine = frame_channel * frame_width
457
458 # Create QImage from frame:
459 qImg = QImage(frame.data, frame_width, frame_height,
460 bytesPerLine, QImage.Format_RGB888)
461
462 return qImg, outputs_str
463
464
465 def main():
466 app = QCoreApplication.instance()
467 if app is None:
468 app = QApplication(sys.argv)
469 main = UI_MainWindow()
470 main.show()
471 sys.exit(app.exec())
472
473
474 if __name__ == '__main__':
475 main()