Sunteți pe pagina 1din 77

Universitatea POLITEHNICA din București

Facultatea de Electronică, Telecomunicații și Tehnologia Informației

Analiza facială automată cu rețele convoluționale pe


Raspberry Pi

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

Conducători științifici Absolvent


Conf. dr. ing. Corneliu FLOREA Radu-Alexandru BULAI
As. drd. ing. Mihai BADEA

Anul 2020
Universitatea “Politehnica” din București Anexa 1
Facultatea de Electronică, Telecomunicații și Tehnologia Informației
Departamentul EAII

TEMA PROIECTULUI DE DIPLOMĂ


a studentului BULAI S. Radu-Alexandru , 441A

1. Titlul temei: Analiza facială automată cu rețele convoluționale pe Raspberry Pi

2. Descrierea contribuției originale a studentului (în afara părții de documentare) și


specificații de proiectare:
Se va considera o soluție de estimare a genului și de predicție a vârstei bazată pe rețele
convoluționale adânci. Într-un prim pas se urmărește optimizarea performanței soluțiilor pe o
bază de date cu adnotări. Într-un al doilea pas se va implementa soluția pe un sistem fără
accelerare grafică de tip Raspberry Pi 3. Drept intrare se vor folosi imagini și secvențe video
achiziționate de camera de pe Raspberry Pi. În condițiile resurselor limitate se vor căuta
arhitecturi de rețele și valori ale hiperparametrilor care sa producă efecte optime. Soluția este
prezentata prin intermediul unei interfețe grafice și implementată în Python.

3. Resurse folosite la dezvoltarea proiectului:


Python, OpenCV, TensorFlow Lite, Thonny

4. Proiectul se bazează pe cunoștințe dobândite în principal la următoarele 3-4


discipline:
DEPI, RFIA, PI

5. Proprietatea intelectuală asupra proiectului aparține: U.P.B.

6. Data înregistrării temei: 2020-02-17 16:58:12

Conducător(i) lucrare,
Student,
Prof. dr. ing. Corneliu FLOREA

Mihai Badea

Director departament, Decan,


Prof. dr. ing Sever PAȘCA Prof. dr. ing. Mihnea UDREA

Cod Validare: f5352e0c4f


Cuprins

Lista figurilor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii

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

4. Concluzii și viitoare implementări . . . . . . . . . . . . . . . . . . . . . . . . . . 44


Bibliografie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

Anexa A. Organizarea bazei de date . . . . . . . . . . . . . . . . . . . . . . . . . . 49


A.1. Script pentru organizarea bazei de date pe foldere separate pe fiecare categorie
de vârstă . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
A.2. Script pentru detecția chipurilor umane din baza de date și salvarea pozelor
decupate rezultate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
A.3. Parcurgerea imaginilor și salvarea acestora în liste Python, respectiv fișiere Nu-
mpy .npz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

Anexa B. Modificarea, antrenarea și ajustarea rețelei . . . . . . . . . . . . . . . . 53


B.1. Preluarea și modificarea rețelei convoluționale . . . . . . . . . . . . . . . . . . . 53
B.2. Antrenarea rețelei convoluționale . . . . . . . . . . . . . . . . . . . . . . . . . . 53
B.3. Determinarea acurateței de test extinzând limitele intervalelor . . . . . . . . . . 54

Anexa C. Întreaga aplicație pe Raspberry Pi, înfășurată într-o interfață grafică 55

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

1.1. Structura rețelei MobileNet, unde Conv dw = deepth-wise convolutional layer


(Sursa: [7]) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.2. Exemplu de resurse utilizate pentru o convoluție standard tipică CNN și o convo-
luție de separare în adâncime, pentru un strat intern din rețea cu DK = 3, M =
512, N = 512, DF = 14 (Sursa: [7]) . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.3. Comparație privind arhitecturile CNN . . . . . . . . . . . . . . . . . . . . . . . 16
3.1. Codarea binară a claselor folosind tehnica Hot One Encoding . . . . . . . . . . . 26
3.2. Rezultate ale antrenărilor pentru diverși parametri (clasificarea vârstelor) . . . . 33
3.3. Rezultate ale antrenărilor pentru diverși parametri (clasificarea genurilor) . . . . 33

v
Lista acronimelor

AI = Artificial Intelligence (Inteligență artificială)


ANN = Artificial Neural Networks (Rețea neuronală artificială)
API = Application Programming Interface (Interfață de programare a aplicațiilor)

CNN = Convolutional Neural Network (Rețea neuronală convoluțională)


CPU = Central Processing Unit (Unitate centrală de procesare)
CSI = Camera Serial Interface (Interfață serială a camerei)
CUDA = Compute Unified Device Architecture (Arhitectură NVidia)

Dw-Conv = Depth-wise convolution (Convoluție în adâncime)

FPS = Frames per Second (Cadre pe secundă)

GPU = Graphics Processing Unit (Unitate de procesare grafică)


GUI = Graphical User Interface (Interfață grafică pentru utilizator)

IoT = Internet of Things (Internetul obiectelor)

LR = Learning Rate (Rată de învățare)

NN = Neural Network (Rețea neuronală)

OpenCV = Open Source Computer Vision Library


OS = Operating System (Sistem de operare)

RAM = Random Acces Memory (Memorie cu acces aleator)


RGB = Red Green Blue (Roșu, Verde, Albastru: sistemul primar de reprezentare a culorilor)

SBC = Single Board Computer (Calculator pe o singură placă de dezvoltare)


SGD = Stochastic gradient descent (Gradient descendent stocastic)

TF = TensorFlow
TFLite = TensorFlow Lite

Wi-Fi = Wireless Fidelity (Fidelitate wireless)

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. Analizarea și compararea rețelelor neuronale convoluționale pentru a alege o arhitectură


optimă pentru sistemul Raspberry Pi 4.

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.

3. Organizarea și preprocesarea unei baze de date conținând chipuri umane etichetate pe


vârstă și gen.

4. Modificarea rețelei neuronale convoluționale alese pentru a îndeplini cerințele aplicației.

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.

6. Convertirea modelului rezultat în urma antrenării într-un model ce va avea o dimensiune


mai mică din punct de vedere al stocării pe disc.

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 3 cuprinde toate etapele detaliate în dezvoltarea aplicației, precum și observații în


luarea unor decizii alternative privind organizarea bazei de date și ajustarea parametrilor rețelei
convoluționale înainte de antrenare. Tot în cadrul acestui capitol vor fi comentate rezultatele
experimentale obținute, iar la în final se va prezenta aplicația completă.

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

1.1 Imagini digitale și prelucrarea imaginilor


O imagine este definită ca o funcție bidimensională, F (x, y), unde x și y sunt coordonate
spațiale, iar amplitudinea F reprezintă intensitatea acelei imagini în punctul (x, y). Un caz
particular al imaginii este imaginea digitală, unde valorile coordonatelor spațiale (x, y), respec-
tiv amplitudinea F sunt definite pe un domeniu finit[13]. Cu alte cuvinte, o imagine poate fi
definită printr-un tablou bidimensional special aranjat în rânduri și coloane. Imaginea digitală
este compusă dintr-un număr finit de elemente, fiecare dintre ele având o valoare particulară
într-o anumită locație. Aceste elemente sunt denumite pixeli (cuvânt preluat din engleza, care
provine de la picture element).

Imaginile digitale pot fi de mai multe tipuri, spre exemplu:


• Imagini binare, în care pixelii pot avea doar două valori posibile: 0 și 1, unde de regulă
valoarea de 0 se referă la un pixel negru, iar valoarea de 1 la un pixel alb în imagine.
Acest tip mai poartă denumirea de imagini monocrome.
• Imaginile cu nivele de gri pe 8 biți, unde pixelii pot lua valori în plaja [0, 255], unde 0
reprezintă culoarea neagră, 127 culoarea gri iar 255 culoarea albă. Acestea mai poartă
denumirea tradițională de imagini alb-negru.
• Imaginile color pe 8 biți, ce fac parte din imaginile vectoriale[14], unde fiecare componentă
este un vector de 3 elemente pentru cele trei canale de culoare RGB.

Figura 1.1: Reprezentarea imaginii color în formă matriceală (figură preluată din [1])

Prelucrarea imaginilor reprezintă o metodă de a efectua anumite operații pe o imagine, pentru


a obține o imagine îmbunătățită sau pentru a extrage informații utile din aceasta. Metoda
poate fi privită ca un sistem care are la intrare un semnal (o imagine) iar la ieșire poate fi o altă
imagine sau caracteristici asociate cu acea imagine[15]. Prelucrarea imaginilor include, practic,
următorii trei pași:
4

• Importarea imaginii prin instrumente de achiziție de imagini;

• Analiza și manipularea imaginii;

• Ieșirea sistemului în care rezultatul poate fi o imagine modificată sau un raport care se
bazează pe analiza imaginii.

1.2 Rețele neuronale artificiale

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.

1.2.1 Arhitectura de tip feed-forward

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

1.2.2 Perceptronul simplu


Perceptronul este unul dintre cei mai simpli și mai vechi1 algoritmi de învățare supervizată2
pentru clasificarea binară. Este un tip de clasificator liniar, adică un algoritm de clasificare
care își face predicțiile bazate pe o funcție de predictor liniar care combină un set de ponderi
(weights) cu un vector al caracteristicilor[3], altfel spus, putem clasifica setul de date desenând
o linie dreaptă simplă. Mai intuitiv, este ca o rețea neuronală cu un singur neuron.

Figura 1.3: Principiul matematic al perceptronului (Imagine preluată din [3])

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

unde X = (1, x1 , ..., xn ), xi ∈ R reprezintă intrările în neuron și W = (w0 , w1 , ..., wn ), wi ∈ R


reprezintă ponderile neuronului.

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.2.3 Perceptronul multi-strat (MLP)


Perceptronul multi-strat reprezintă o rețea neuronală artificială compusă din mai mulți percep-
troni. Spre deosebire de perceptronii cu un singur strat, rețeaua MLP este capabilă să calculeze

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.

1.2.4 Funcții de activare


Funcțiile de activare a rețelei neuronale reprezintă o componentă esențială a învățării profunde
(din eng. Deep Learning). Acestea determină rezultatul unui model de învățare profundă,
anume acuratețea, precum și eficiența computațională. Funcțiile de activare au un efect major
asupra capacității și a vitezei de convergență a rețelei neuronale, iar în unele cazuri, ar putea
chiar împiedica convergența.

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.

Există trei tipuri de funcții de activare, și anume:

• 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ă.

f (x) = a ∗ x, unde a este o constantă, a ∈ R (1.3)

• 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ă.

Câteva dintre funcțiile neliniare des utilizate sunt următoarele:

• Funcția tangentă hiperbolică:

e2x − 1
f (x) = tanh(x) = (1.4)
e2x + 1

• Funcția sigmoidă:
1
f (x) = (1.5)
1 + (1 + e−x )

• Funcția unitate liniară rectificată (ReLU - Rectified Linear Unit):


{
0 , dacă x < 0
f (x) = max(0, x) = (1.6)
x , dacă x ≥ 0

• Funcția Softmax:

ezi
σ(z)i = ∑K pentru i = 1, . . . , K și z = (z1 , . . . , zK ) ∈ RK (1.7)
j=1 ezj

unde zi reprezintă vectorul de la intrare, de dimensiune K.

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

1.3 Rețele neuronale convoluționale

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.

Comparativ cu CNN, rețeaua MLP prezentată în secțiunea anterioară, prezintă următoarele


dezavantaje:

• 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

Arhitectura rețelelor convoluționale


CNN este formată dintr-un strat de intrare, ieșire și mai multe straturi ascunse care constau
într-o serie de straturi convolutive, care transformă un nivel de activări în altul. Fiecărui strat
convolutiv i se atașează straturi suplimentare precum straturile de grupare (pooling), complet
conectate (fully-connected) și de normalizare (normalization).

1.3.1 Stratul convoluțional


Stratul convoluțional este blocul principal care realizează cea mai parte a operațiunilor de cal-
culare. Parametrii acestui strat constau într-un set de filtre care pot fi învățate.

Î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.

Figura 1.7: Exemplu de operație de convoluție bidimensională între imaginea de la intrare și


masca de filtrare (Figură preluată din [5])

Î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.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])

1.3.2 Stratul pooling

Stratul de pooling este responsabil de reducerea dimensiunii spațiale a caracteristicilor în urma


convoluției și scade astfel puterea de calcul necesară procesării datelor. Stratul este util pentru
extragerea caracteristicilor dominante care sunt invariante la rotație și translație, menținând
astfel procesul de instruire a modelului eficient. Există două tipuri de pooling: Max Pooling
și Average Pooling[4]. Max Pooling returnează valoarea maximă din porțiunea de imagine
acoperită de nucleu, iar Average Pooling returnează media tuturor valorilor din porțiunea de
imagine acoperită de nucleu. Max Pooling funcționează ca filtru de înlăturare a zgomotului,
înlăturând activările redundante. Pe de altă parte, Average Pooling realizează pur și simplu
reducerea dimensionalității. În practică, Max Pooling are performanțe mult mai bune decât
Average Pooling, preferându-se folosirea acestuia în rețelele convoluționale.

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

1.3.3 Stratul fully-connected


Stratul complet conectat este o modalitate convenabilă de a învăța combinații neliniare ale
caracteristicilor la nivel înalt, așa cum este reprezentată de ieșirea stratului convolutiv. Stratul
complet conectat învață o funcție posibil neliniară în spațiul respectiv.

1.3.4 Aplicarea funcției de activare Softmax


După ce imaginea de intrare a fost transformată într-o formă adecvată pentru perceptronul cu
mai multe nivele (anume au fost extrase și filtrate trăsăturile), se transformă imaginea într-un
vector coloană. Ieșirea aplatizată este alimentată într-o rețea neuronală tip feed-forward și
se aplică la fiecare iterație de antrenament. Pentru o serie de epoci, modelul este capabil să
distingă între imagini dominante pentru anumite caracteristici și să le clasifice folosind funcția
de activare Softmax.

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].

1.4.1 Tipuri de convoluții. Comparare cu CNN tipice


Modelul MobileNets se bazează pe convoluții separabile în adâncime, care sunt o formă de
convoluții care separă o convoluție standard într-o convoluție în adâncime și o convoluție 1 × 1
numită convoluție punctuală (fig. 1.12). Pentru MobileNets, convoluția în adâncime aplică un
singur filtru pentru fiecare canal de intrare. Apoi, convoluția punctuală aplică o convoluție 1×1
pentru a combina ieșirile convoluției în adâncime[7]. Spre deosebire de o convoluție standard
care filtrează și combină intrările într-un nou set de ieșiri într-o singură etapă, această separare
are ca efect reducerea drastică a calculului și dimensiunea modelului.
În continuare, se vor folosi următoarele notații:

• DF = lățimea și înălțimea hărții de caracteristici la intrare (de formă pătrată)

• DG = lățimea și înălțimea hărții de caracteristici la ieșire (de formă pătrată)

• M = Adâncimea (Numărul de canale) hărții la intrare (de exemplu, cele 3 canale RGB)

• N = Adâncimea (Numărul de canale) hărții la ieșire

• DK = Dimensiunea nucleului de convoluție (de formă pătrată)


13

Figura 1.12: Separarea convoluțiilor standard în convoluții în adâncime și convoluții punctuale


(figură preluată din [7])

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)

care reprezintă suma dintre convoluția în adâncime și cea punctuală[7].

Prin exprimarea convoluției standard ca un proces în două etape, de filtrare și combinare,


obținem ca raport o reducere a calculului de:

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

Rețeaua MobileNets utilizează convoluții separabile în adâncime cu nuclee de dimensiune 3 × 3,


care reduc astfel volumul computațional de 8-9 ori față de convoluțiile standard, singurul cost
fiind o mică reducere a preciziei.
14

1.4.2 Arhitectura rețelei


Structura MobileNet este construită pe convoluții separabile în adâncime, cu excepția primului
strat, care este o convoluție completă. Toate straturile sunt urmate de o normalizare a lotului
(batch normalization) și neliniaritate ReLU, cu excepția stratului final complet conectat, care
nu are neliniaritate și este urmat de o funcție de activare Softmax pentru clasificare. Struc-
tura completă a rețelei se poate vedea în tabelul 1.1. Numărând convoluțiile în adâncime și
convoluțiile punctuale ca straturi separate, rezultă că rețeaua MobileNets are 28 de straturi.

Type / Stride Filter Shape Input Size


Conv / s2 3 × 3 × 3 × 32 224 × 224 × 3
Conv dw / s1 3 × 3 × 32 dw 112 × 112 × 32
Conv / s1 1 × 1 × 32 × 64 112 × 112 × 32
Conv dw / s2 3 × 3 × 64 dw 112 × 112 × 64
Conv / s1 1 × 1 × 64 × 128 56 × 56 × 64
Conv dw / s1 3 × 3 × 128 dw 56 × 56 × 128
Conv / s1 1 × 1 × 128 × 128 56 × 56 × 128
Conv dw / s2 3 × 3 × 128 dw 56 × 56 × 128
Conv / s1 1 × 1 × 128 × 256 28 × 28 × 128
Conv dw / s1 3 × 3 × 256 dw 28 × 28 × 256
Conv / s1 1 × 1 × 256 × 256 28 × 28 × 256
Conv dw / s2 3 × 3 × 256 dw 28 × 28 × 256
Conv / s1 1 × 1 × 256 × 512 14 × 14 × 256
5 x Conv dw / s1 3 × 3 × 512 dw 14 × 14 × 512
5 x Conv / s1 1 × 1 × 512 × 512 14 × 14 × 512
Conv dw / s2 3 × 3 × 512 dw 14 × 14 × 512
Conv / s1 1 × 1 × 512 × 1024 7 × 7 × 512
Conv dw / s2 3 × 3 × 1024 dw 7 × 7 × 1024
Conv / s1 1 × 1 × 1024 × 1024 7 × 7 × 1024
Avg Pool / s1 Pool 7 × 7 7 × 7 × 1024
FC / s1 1024 × 1000 1 × 1 × 1024
Softmax / s1 Classifier 1 × 1 × 1000

Tabela 1.1: Structura rețelei MobileNet, unde Conv dw = deepth-wise convolutional layer
(Sursa: [7])

1.4.3 Parametri ajustabili. Multiplicatorii de lățime și rezoluție


Deși arhitectura MobileNets are deja o latență mică și scăzută, de multe ori un caz de utilizare
specific sau o aplicație poate necesita ca modelul să fie mai mic și mai rapid. Astfel, pe lângă
separarea convoluției standard în convoluții optimizate ce reduc volumul de calcul, rețeaua Mo-
bileNets dispune de încă doi parametri: multiplicatorul de lățime și multiplicatorul de rezoluție.

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

multiplicatorul de lățime α devine acum:

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.

Tabelul 1.2 prezintă numărul de operații de adunare-înmulțire și numărul de parametri necesari


pentru un strat, pe măsură ce metodele de micșorare ale arhitecturii sunt aplicate secvențial.
Primul rând prezintă numărul de operații și parametrii pentru un strat convolutiv complet cu o
hartă a caracteristicilor de intrare cu dimensiunea 14 × 14 × 512 cu un nucleu K de dimensiuni
3 × 3 × 512 × 512.
Layer/Modification Million Mult-Adds Million Parameters
Standard Convolution 462 2.36
Depthwise Separable Conv 52.3 0.27
Dw conv and α = 0.75 29.6 0.15
Dw conv, α = 0.75 and ρ = 0.714 15.1 0.15

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

1.4.4 Concluzii privind alegerea arhitecturii


Vom analiza pe scurt și alte arhitecturi de rețele CNN, într-o ordine cronologică:
1. AlexNet (2012): arhitectură cu 60 milioane de parametri, ce conține 8 straturi dintre
care 5 sunt convoluționale și 3 sunt complet conectate. Rețeaua are o arhitectură foarte
asemănătoare cu LeNet (Yann LeCun), dar este mai profundă, cu mai multe filtre pe
strat și cu straturi convolutive suprapuse.
2. VGG-16 (2014): arhitectură cu 138 milioane de parametri, care este formată din 16
straturi convoluționale și 3 complet conectate. Această rețea stochează mai multe straturi
pe baza arhitecturii AlexNet și folosește filtre de dimensiuni mai mici (2 × 2 și 3 × 3).
Aceasta ocupă aproximativ 500MB ca spațiu de stocare.
16

3. GoogLeNet/Inception-v1 (2014): se bazează pe mai multe convoluții de dimensiuni mici


pentru a reduce numărul de parametri. Arhitectura constă într-un CNN cu adâncime de
22 de straturi, dar a redus numărul de parametri de la 60 de milioane (AlexNet) la 4
milioane.

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.

ARHITECTURĂ TOP 1 ACCURACY NUMĂR DE PARAMETRI DIMENSIUNE AN


MobileNet-224 0.706 4M 16 MB 2017
Inception V1 0.698 5M 2014
Inception V2 0.748 11M 2015
Xception 0.790 22M 88 MB 2016
Inception V3 0.779 23M 92 MB 2015
ResNet-50 0.721 26M 98 MB 2015
AlexNet - 7CNNs 0.633 60M 2012
VGG-16 0.744 138M 528 MB 2014
VGG-19 0.745 144M 549 MB 2014

Tabela 1.3: Comparație privind arhitecturile CNN


17

Capitolul 2
Resurse utilizate

2.1 Resurse Hardware

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].

Figura 2.1: Placa de dezvoltare Raspberry Pi 4 (imagine preluată din [8])

Specificațiile Raspberry Pi 4 sunt următoarele:

• CPU: Broadcom BCM2711, Quad core Cortex-A72 @ 1.5GHz, cu posibil overclock până
la 2.1GHz

• GPU: Broadcom VideoCore VI @ 500Mhz


1
• RAM: 2GB/4GB/8GB LPDDR4 @ 3200MHz

• Stocare: 1 slot pentru card Micro-SD

• Conexiuni: 1 Gigabit Ethernet, 2.4GHz/5.0GHz 802.11ac Wi-fi, Bluetooth 5.0

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

• Dimensiuni: 88mm × 58mm × 19.5mm, 46g

• Putere maximă: 7.5W

• OS: Raspbian 32bit (bazat pe Debian Linux 10)

2.1.2 Modul Raspberry Pi Camera V2


Raspberry Pi Camera Module v2 are un senzor Sony IMX219 de 8 megapixeli, care poate fi
folosit pentru a realiza videoclipuri de înaltă definiție, precum și fotografii statice. Aceasta se
atașează printr-un cablu cu panglică de 15 cm la portul CSI de pe Raspberry Pi[24].

Greutatea modulului de cameră este de aproximativ 3g și dimensiuni de 25mm x 20mm x


9mm. Astfel întregul pachet cu placa de dezvoltare Raspberry Pi și modulul de cameră va
fi alegerea ideală în ceea ce privește dimensiunea și greutatea, ceea ce este foarte important
pentru aplicațiile mobile.

Figura 2.2: Placa Raspberry Pi 4 cu modul cameră atașat ce va fi folosită în această lucrare

2.2 Resurse Software

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.

• Extensibilitate: Componentele noi sunt ușor de adăugat și de utilizat în proiect.

• 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].

2.2.4 TensorFlow Lite

Figura 2.3: Etapele de la modelul antrenat TF la modelul TFLite utilizabil pe dispozitive


mobile (imagine preluată din [9])

TensorFlow Lite este un set de instrumente pentru a ajuta dezvoltatorii să ruleze modele Ten-
sorFlow pe dispozitive mobile, integrate și IoT 3 [30].

TensorFlow Lite este format din două componente principale:

• 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.

2.2.6 Google Colab


Google Colaboratory este un produs din Google Research. Colab permite oricui utilizator să
scrie și să execute cod Python prin intermediul browserului și este de potrivit pentru învățarea
algoritmilor de ML, analiza datelor sau pentru educație. Mai tehnic, Colab este un serviciu de
notebook-uri Jupyter5 găzduit, care nu necesită nicio configurație de utilizare, oferind în ace-
lași timp acces gratuit la resurse de calcul[33]. Cea mai importantă caracteristică care distinge
Colab de alte servicii cloud este disponibilitatea unei plăci grafice (GPU) pentru utilizatorii cu
abonament gratuit.

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]:

• Arhitectura GPU: Nvidia Pascal

• Frecvență: 1190 MHz

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

• NVIDIA CUDA® Cores: 3584

• Putere maximă: 250W

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

3.1 Organizarea bazelor de date

3.1.1 Organizarea și preprocesarea imaginilor în directoare separate


pe clase

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)

3.1.2 Încărcarea imaginilor în liste Python și exportarea acestora ca


fișiere .npz
Pentru a încărca imaginile sub formă de tablouri tridimensionale (așa cum am văzut în fig 1.1
din subcapitolul 1.1 - Reprezentarea imaginii digitale color), vom parcurge toate directoarele pe
categorii de vârstă precum și subdirectoarele pe genuri. Reamintim că fiecare imagine (conform
bazei de date UTKFace și a extragerii de informații din fișierele .csv pentru Appa-Real) conține
în denumirea sa ”VarstaReala_Genul_DenumireImagine”. Astfel, pentru fiecare imagine vom
salva matricea ei corespunzătoare (citită folosind Librăria OpenCV), vârsta reală (pentru care
vom putea atașa o etichetă imaginii - label) și genul.

Clasele vor fi împărțite în următorul mod:

• Clasa 0: 04-06 ani (copilărie timpurie - early childhood)

• Clasa 1: 07-08 ani (copilărie mijlocie - middle childhood)

• Clasa 2: 09-11 ani (copilărie târzie - late childhood)

• Clasa 3: 12-19 ani (adolescență - adolescence)

• Clasa 4: 20-27 ani (vârstă adultă timpurie - early adulthood)

• Clasa 5: 28-35 ani (vârstă adultă mijlocie - middle adulthood)

• Clasa 6: 36-45 ani (vârstă de mijloc - midlife)

• Clasa 7: 46-60 ani (maturitate adultă - mature adulthood)


25

• Clasa 8: 61-75 ani (vârstă adultă târzie - late adulthood)

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']

return (train, train_labels), (test, test_labels)

Clasă Codare numerică Hot One Encoding


04-06 ani 0 100000000
07-08 ani 1 010000000
09-11 ani 2 001000000
12-19 ani 3 000100000
20-27 ani 4 000010000
28-35 ani 5 000001000
36-45 ani 6 000000100
46-60 ani 7 000000010
61-75 ani 8 000000001
Feminin 0 10
Masculin 1 01

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

from keras.utils import to_categorical


(x_train, y_train), (x_test, y_test) = load_data_training_and_test("AgeClass")

# Codare binară folosind One Hot Encoding


y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# Convertirea valorilor imaginii în float și normalizarea acestora


# din intervalul valorilor culorilor pe 8 biți [0,255] în [0,1]
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

3.2 Pregătirea rețelei neuronale convoluționale


Pentru a crea și ajusta rețeaua MobileNets, ne vom folosi de modulul Applications, integrat
direct în Keras. Modulul de aplicații din Keras conține modele de învățare profundă alături
de ponderi ale rețelei pre-antrenate. Aceste modele pot fi utilizate pentru predicție, extrage-
rea caracteristicilor și reglarea fină. Vom importa și vom vedea ce conține rețeaua folosind
următoarele linii de cod:

from keras import applications


mobile = applications.mobilenet.MobileNet()
print(mobile.summary())

Observăm că ultimele 6 straturi (pre-antrenate) au fost folosite pentru clasificarea celor 1000
de categorii a setului de date ImageNet:

global_average_pooling2d_1 ( (None, 1024) 0


_________________________________________________________________
reshape_1 (Reshape) (None, 1, 1, 1024) 0
_________________________________________________________________
dropout (Dropout) (None, 1, 1, 1024) 0
_________________________________________________________________
conv_preds (Conv2D) (None, 1, 1, 1000) 1025000
_________________________________________________________________
reshape_2 (Reshape) (None, 1000) 0
_________________________________________________________________
act_softmax (Activation) (None, 1000) 0
=================================================================
Total params: 4,253,864
Trainable params: 4,231,976
Non-trainable params: 21,888

Putem să înlocuim aceste straturi cu un strat Dropout, urmat de o funcție de activare ce


va avea la ieșire cele 9 clase de categorii de vârstă (cod disponibil în Anexa B.1). Stratul
Dropout reprezintă o metodă de regularizare care se referă la ignorarea unor anumitor seturi
de neuroni alese la întâmplare în faza de antrenare. Astfel, dacă unitățile nu sunt luate în
considerare, funcția de cost devine mai sensibilă la neuronii vecini care schimbă modul în
care ponderile vor fi actualizate în timpul procesului de backpropagation, prevenind în final
supra-dimensionarea (overfitting) rețelei. Rețeaua rezultată ce va fi folosită pentru antrenarea
modelului este prezentată în figura 3.4.
28

Figura 3.4: Rețeaua MobileNet după înlocuirea ultimelor 6 straturi cu straturile Dropout și
Dense

3.3 Antrenarea rețelei și ajustarea parametrilor


Pentru antrenarea rețelei, vom folosi ca optimizator gradientul descendent stocastic (SGD).
Spre deosebire de gradient descendent, la fiecare pas, algoritmul SGD va alege în mod alea-
tor un număr mic de puncte (lot) de eșantionare pentru a calcula derivatele, reducând astfel
volumul computațional. SGD poate totodată să efectueze actualizări frecvente cu o varianță
ridicată care determină ca funcția obiectiv să fluctueze foarte mult, permițând algoritmului să
sară în minime locale posibile mai bune, fără să rămână blocat în acestea. Cu toate că nu
mai este nevoie să efectueze calcule redundante (anume recalcularea gradienților pentru puncte
similare din alt lot, cum se întâmplă la gradient descendent), apare dezavantajul convergenței
la un minim local mai bun[39]. Dar, acest dezavantaj poate fi eliminat cu ajutorul parametrului
de rată de învățare (Learning Rate). Dacă scădem lent rata de învățare, SGD va avea același
comportament de convergență ca gradient descendent, convergând aproape sigur la un nivel
local mai mic.

Biblioteca Keras are încorporată un modul de optimizatori, cuprinzând algoritmi populari


29

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

from keras.optimizers import SGD


optim = SGD(learning_rate=0.0001, momentum=0.9, nesterov=True)

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:

from keras.callbacks import ModelCheckpoint, EarlyStopping


callbacks_list = [
EarlyStopping(monitor='val_loss', patience=20),
ModelCheckpoint(filepath='AgeClass_best.h5', verbose=1,
monitor='val_loss', save_best_only=True)
]
epochs = 100
batch_size = 2

Î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'

3.4 Rezultatele antrenării pentru estimarea vârstei

Vom prezenta în continuare câteva etape din antrenarea rețelei:

Observăm că modelul a încetat din a se îmbunătăți în a generaliza încă de la epoca 7. Conti-


nuând, modelul își scade valoarea funcției de cost pentru imaginile de antrenare. Acest lucru
din urmă reprezintă fenomenul de supra-dimensionare (overfitting), modelul începe de-acum
să memoreze trăsăturile caracteristice prezente în imaginile date pentru antrenare, dar nu își
poate îmbunătăți performanța pe imaginile de testare. Putem trasa graficele din figura 3.6
pentru a analiza mai bine ce se întâmplă.

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

3.4.1 Determinarea acurateței de test extinzând limitele intervalelor


După cum s-a văzut la finalul antrenării, acuratețea pentru cele 2178 de imagini de test (din
10903 în total) se situează la 49.63%. Putem totodată să calculăm o nouă acuratețe, extinzând
limitele superioare și inferioare ale fiecărui interval cu până la 2 ani. Bineînțeles, nu vom extinde
intervalele pentru primele categorii de clase (cele de la 4 la 11 ani) întrucât intervalele de vârstă
corespunzătoare sunt prea mici, și vom suprapune clasele între ele. Noile clase de vârstă vor fi
următoarele:

• Clasa 3: [12, 19] ani va avea noul interval de [10, 21] ani

• Clasa 4: [20, 27] ani va avea intervalul [18, 29] ani

• Clasa 5: [28, 35] ani va avea intervalul [26, 37] ani

• Clasa 6: [36, 45] ani va avea intervalul [34, 47] ani

• Clasa 7: [46, 60] ani va avea intervalul [44, 49] ani

• Clasa 8: [61, 75] ani va avea intervalul [58, 75] 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%.

3.4.2 Rezultate ale antrenării pentru setarea diferită a parametrilor


Putem ajusta acum parametri de antrenare precum dimensiunea lotului, rata de învățare, mo-
mentum și numărul de epoci după care să se oprească antrenarea (folosind EarlyStopping).
Pentru ușurință, întocmim tabelul 3.2 ce va cuprinde direct rezultatele fiecărei dintre antre-
nări, în funcție de parametri aleși.

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)

3.5 Rezultatele antrenării pentru determinarea genului


Efectuând toți pașii precizați anterior, anume parcurgerea imaginilor și încărcarea lor în fișiere
NumPy (de data acesta împărțite pe cele 2 categorii de gen feminin și masculin), definirea și
folosirea funcției pentru a încărca aceste fișiere în platforma Google Colab, crearea și modifica-
rea rețelei astfel încât la ieșire din funcția de activare să ne rezulte 2 clase, precum și ajustarea
parametrilor înainte de antrenare, obținem, într-un final, următoarele rezultate prezente în ta-
belul 3.3.

Nr crt Antrenare 01 Antrenare 02 Antrenare 03 Antrenare 04


Data - ora antrenării 02iun-20:08 03iun-21:01 02iun-22:18 03iun-23:22
BatchSize 8 8 4 4
LearningRate 0.01 0.001 0.001 0.01
Momentum 0.8 0.9 0.99 0.9
Nr epoci 33 14 13 11
EarlyStopping (epoci) 30 10 5 5
ms / pas 21ms/step 21ms/step 18ms/step 16ms/step
s / epoca 179s 179s 153s 154s
Durată antrenare 1h38min 42min 34min 28min
val_loss 0.202 0.172 0.234 0.332
val_acc 0.955 0.952 0.93 0.915

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

3.6 Convertirea și utilizarea modelului TFLite


Pentru a converti modelul din formatul inițial .HDF5 (Hierarchical Data Format version 5),
un format de fișier conceput pentru a stoca și organiza cantități mari de date, într-un model
.tflite (prezentat anterior în subcapitolul 2.2.4), ne vom folosi următoarele funcții integrate în
biblioteca TensorFlow:

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)

Metoda TFLiteConverter.from_keras_model(model) creează un obiect de tipul TFLiteConver-


ter dintr-un model Keras, încărcat în variabila model prin metoda load_model(). TFLiteCon-
verter conține la rândul lui metoda convert() care va converti datele modelului într-un format
serializat, de tipul FlatBuffer[41]. După ce am realizat convertirea modelelor, dimensiunea
acestora pe disc a scăzut de la 25MB la 12MB (figura 3.10)

Figura 3.10: Reprezentarea dimensiunii de stocare a modelelor în format h5/hdf5 și tflite

Î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

3.7 Interfața aplicației pe Raspberry Pi

Figura 3.11: Meniul principal al aplicației

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.

3.8 Captarea și clasificarea imaginilor în timp real


Pentru a folosi captarea continuă a cadrelor disponibilă în librăria OpenCV, în cadrul inter-
feței PyQt5, vom avea nevoie de un timer pentru care să apeleze o funcție update la un
interval cât se poate se mic de timp (spre exemplu, 20msec), precum și o funcție de tip on-off ,
asociată unui buton, pentru a opri sau porni acest timer. În cadrul clasei UI_CameraWindow
(anume, prima fereastră ce se deschide în urma apăsării primului buton cu logo de ”cameră” din
meniul principal), vom inițializa o variabilă timer căreia îi vom ”atașa” funcțiile menționate:

# Creează un obiect tip timer


self.timer = QTimer()
# Setează funcția pentru care să se facă update constant
self.timer.timeout.connect(self.Webcam_update)
# Setează funcția pentru a opri/porni timer (on-off)
self.Btn_camera_on_off.clicked.connect(self.controlTimer)
37

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

Funcția de oprire/pornire a timer-ului va apela exact funcțiile de inițializare, respectiv închidere


a camerei web atașate din librăria OpenCV. Pentru inițializare, vom declara un obiect în cadrul
clasei curente numit self.webcam care va accesa camera principală (0) a sistemului apelând
cv2.VideoCapture(0). Pentru închiderea camerei, vom apela funcția release(). Funcția completă
de pornire/oprire timer, care totodată va deschide/închide camera web, va fi definită astfel:

# 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]

# Verifica dacă există chipuri umane in cadrul curent:


if input_im is not None:
# Pre-procesare imagine pentru formatul cerut la intrarea modelului
input_im = cv2.resize(input_im, (224, 224))
input_im = input_im.astype('float')
input_im = input_im / 255
input_im = img_to_array(input_im)
input_im = np.expand_dims(input_im, axis=0)

# 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

3.9 Încărcarea imaginilor locale și clasificarea acestora


Următoarele 2 butoane din meniul principal al aplicației (figura 3.11), vor permite selecta-
rea unei singure imagini, respectiv al unui folder ce conține imagini aflate în memoria lo-
cală. Pentru a efectua aceste operații în cadrul claselor UI_SelectPic, UI_SelectGallery, ne
vom folosi de metodele librăriei PyQt5 QFileDialog.getOpenFileName(), respectiv QFileDia-
log.getExistingDirectory(). În urma apelării, vom obține calea pentru imagine, respectiv pentru
folder.
• În cazul meniului de selecție a unei singure imagini, vom citi imaginea într-o funcție
separată (classifyImage()) folosind cv2.imread(cale_imagine), și vom efectua aceeași pași
pentru clasificare menționați la captarea în timp real.
41

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

# Procesare imagine, clasificare


pixmap, outputs_str = classifyImage(filePath)

• Î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

# Aici depinde de sistemul de operare: Windows (\), Linux (/)


# Deci folosim normpath()
if self.imageName is not None:
full_path = os.path.join(os.path.normpath(
self.folderPath), self.imageName)

# ## Procesare imagine, clasificare


pixmap, outputs_str = classifyImage(full_path)

Î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

șiruri de caractere. Toate acestea ar putea fi precompilate înainte rescriind sintaxele în


Cython, pentru a îmbunătăți performanțele.

• MobileNetV2. MobileNetV2 este o arhitectură ce utilizează rețele neuronale convoluțio-


nale, care reprezintă o îmbunătățire semnificativă față de MobileNetV1 și împinge stadiul
tehnicii pentru recunoașterea vizuală mobilă, incluzând clasificarea, detectarea obiectelor
și segmentarea semantică. Aceasta ar putea oferi o acuratețe mai bună modelului de
clasificare a vârstei și a genului, reducând de asemenea numărul de parametri. Alte arhi-
tecturi eficiente din punct de vedere computațional ce ar putea fi încercate sunt: ResNeXt
și ShuffleNet.

• 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ă.

• Overclocking. Overclocking este reprezintă o practică de a crește frecvența de ceas a


unui calculator pentru a o depăși pe cea certificată de producător. În cazul calculatorului
Raspberry Pi 4, putem crește frecvența de la 1.5GHz la 2.1GHz, în condițiile în care putem
asigura o răcire eficientă a procesorului, folosind un radiator pasiv sau un ventilator, dar
trebuie asigurată și o sursă stabilă de tensiune (în acest caz nu vom mai putea folosi o
baterie externă pentru alimentare). Realizând această practică, putem crește performanța
clasificării în timp real, obținând un timp mai mic de procesare a unui cadru captat.
46

Bibliografie

[1] RGB Image Representation.


https://www.geeksforgeeks.org/matlab-rgb-image-representation/. [Online; ac-
cesat 6-Iun-2020].
[2] Feedforward neural networks.
https://brilliant.org/wiki/feedforward-neural-networks/, 2020. [Online; accesat
10-Iun-2020].
[3] What exactly is a perceptron?
https://riptutorial.com/machine-learning/example/22617/what-exactly-is-a-
perceptron-. [Online; accesat 10-Iun-2020].
[4] Sumit Saha. A Comprehensive Guide to Convolutional Neural Networks.
https://towardsdatascience.com/a-comprehensive-guide-to-convolutional-
neural-networks-the-eli5-way-3bd2b1164a53, 2018. [Online; accesat 11-Iun-2020].
[5] Jason Brownlee. How Do Convolutional Layers Work in Deep Learning Neural Networks?
https://machinelearningmastery.com/convolutional-layers-for-deep-
learning-neural-networks/, 2019. [Online; accesat 11-Iun-2020].
[6] Andrej Karpathy. Cs231n convolutional neural networks for visual recognition.
https://cs231n.github.io/convolutional-networks/ [Online; accesat 11-Iun-2020].
[7] Andrew G Howard, Menglong Zhu, Bo Chen, Dmitry Kalenichenko, Weijun Wang, Tobias
Weyand, Marco Andreetto, and Hartwig Adam. Mobilenets: Efficient convolutional neural
networks for mobile vision applications. 2017.
[8] Raspberry Pi 4.
https://www.raspberrypi.org/products/raspberry-pi-4-model-b/. [Online; acce-
sat 8-Iun-2020].
[9] Tensorflow. Using TensorFlow Lite on Android.
https://medium.com/tensorflow/using-tensorflow-lite-on-android-
9bbc9cb7d69d, 2018. [Online; accesat 7-Iun-2020].
[10] Vitaly Bushaev. Stochastic Gradient Descent with momentum.
https://towardsdatascience.com/stochastic-gradient-descent-with-momentum-
a84097641a5d, 2017. [Online; accesat 16-Iun-2020].
[11] Eran Eidinger, Roee Enbar, and Tal Hassner. Age and gender estimation of unfiltered
faces. IEEE Transactions on Information Forensics and Security, 9(12), 2014.
[12] Afshin Dehghan, Enrique G Ortiz, Guang Shu, and Syed Zain Masood. Dager: Deep age,
gender and emotion recognition using convolutional neural network. 2017.
[13] Nishant Kumar. Digital Image Processing Basics.
https://www.geeksforgeeks.org/digital-image-processing-basics/. [Online; ac-
cesat 6-Iun-2020].
47

[14] Constantin Vertan. Prelucrarea și Analiza Imaginilor. Editura Printech Bucuresti, 1999.

[15] Gholamreza Anbarjafari. Digital Image Processing.


https://sisu.ut.ee/imageprocessing/book/1. [Online; accesat 6-Iun-2020].

[16] Wikipedia contributors. Perceptron — Wikipedia, the free encyclopedia.


https://en.wikipedia.org/w/index.php?title=Perceptron&oldid=961536136, 2020.
[Online; accesat 10-Iun-2020].

[17] Types of neural network activation functions: How to choose?


https://missinglink.ai/guides/neural-network-concepts/7-types-neural-
network-activation-functions-right/. [Online; accesat 10-Iun-2020].

[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.

[20] Uniqtech. Multilayer Perceptron (MLP) vs Convolutional Neural Network in Deep


Learning.
https://medium.com/data-science-bootcamp/multilayer-perceptron-mlp-vs-
convolutional-neural-network-in-deep-learning-c890f487a8f1#, 2018. [Online;
accesat 11-Iun-2020].

[21] Fransiska. Differences Between Inception Resnet and MobileNet. https:


//medium.com/@fransiska26/the-differences-between-inception-resnet-and-
mobilenet-e97736a709b0, 2019. [Online; accesat 12-Iun-2020].

[22] Image Classification on ImageNet - Leaderboard.


https://paperswithcode.com/sota/image-classification-on-imagenet, 2020. [On-
line; accesat 13-Iun-2020].

[23] Wikipedia contributors. Raspberry pi — Wikipedia, the free encyclopedia.


https://en.wikipedia.org/w/index.php?title=Raspberry_Pi&oldid=961425494,
2020. [Online; accesat 8-Iun-2020].

[24] Raspberry Pi 4 Camera Module V2.


https://www.raspberrypi.org/products/camera-module-v2/. [Online; accesat 8-Iun-
2020].

[25] What is Python? Executive Summary.


https://www.python.org/doc/essays/blurb/. [Online; accesat 6-Iun-2020].

[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

[29] Wikipedia contributors. Keras — Wikipedia, the free encyclopedia.


https://en.wikipedia.org/w/index.php?title=Keras&oldid=954618107, 2020. [On-
line; accesat 7-Iun-2020].

[30] Tensorflow. TensorFlow Lite guide.


https://www.tensorflow.org/lite/guide, 2020. [Online; accesat 7-Iun-2020].

[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].

[33] Google Colab Free GPU Tutorial.


https://medium.com/deep-learning-turkey/google-colab-free-gpu-tutorial-
e113627b9f5d, 2018. [Online; accesat 9-Iun-2020].

[34] NVIDIA Tesla P100 PCIe 16 GB Specifications.


https://www.techpowerup.com/gpu-specs/tesla-p100-pcie-16-gb.c2888, 2016.
[Online; accesat 9-Iun-2020].

[35] Yang Song, Zhifei Zhang. Large Scale Face Dataset.


https://susanqq.github.io/UTKFace/, 2017. [Online; accesat 30-Mai-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.

[38] Thomas Armstrong. The 12 Stages of Life.


https://www.institute4learning.com/resources/articles/the-12-stages-of-
life/. [Online; accesat 15-Iun-2020].

[39] Sebastian Ruder. An overview of gradient descent optimization algorithms.


https://ruder.io/optimizing-gradient-descent/, 2016. [Online; accesat 16-Iun-
2020].

[40] Abhishek Sharma. Confusion Matrix in Machine Learning.


https://www.geeksforgeeks.org/confusion-matrix-machine-learning/. [Online;
accesat 17-Iun-2020].

[41] Tensorflow. Converter Python API guide.


https://www.tensorflow.org/lite/convert/python_api, 2020. [Online; accesat 18-
Iun-2020].
49

Anexa A
Organizarea bazei de date

A.1 Script pentru organizarea bazei de date pe foldere separate pe


fiecare categorie de vârstă
''' author: Radu
Muta fiecare imagine din folder-ul cu imagini amestecate apartinand tuturor claselor
a bazei de date Appa-Real in foldere separate pe clase de varste.
Redenumeste fiecare imagine cu varsta persoanei din imaginea respectiva
'''

import os
import shutil

# SCHIMBA NUMELE .CSV si dir_target pentru fiecare categorie varsta


# - numele .CSV pentru train/test/valid
# - dir_start -> train/test/valid

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)

# Pastrez doar denumirea fisierelor in lista


lista_names = [x[0:6] for x in lista_names]
lista_ages = [str(int(x[0:2])) for x in lista_ages]
print(lista_names)

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

print("Au fost mutate {} poze.".format(count))

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

print("Au fost redenumite {} poze.".format(count))

if __name__ == "__main__":
Move_images()
Rename_images()

A.2 Script pentru detecția chipurilor umane din baza de date și


salvarea pozelor decupate rezultate

''' author: Radu


Detecteaza si decupeaza figurile umane din fiecare poză
aparținând dosarului curent și salvează imaginile decupate
'''
import cv2
import os

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]

# Ignoră imaginile cu chipuri rezultate dacă au


# o rezoluție mai mică de 90 x 90
if sub_face.shape[0] > 90 and sub_face.shape[1] > 90:
resized = cv2.resize(sub_face, (200, 200))
face_file_name = filename[:-4] + "_crop2.jpg"
cv2.imwrite(face_file_name, resized)

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

A.3 Parcurgerea imaginilor și salvarea acestora în liste Python, res-


pectiv fișiere Numpy .npz

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']

histo = [0] * len(lista_foldere)


lista_imag_label = [[], [], [], [], [], [], [], [], []]
# lista_imag_label va fi o matrice tridimenionala ce contine cele 9 clase,
# fiecare clasa avand [imaginea, label, real_age]

# lista_imag_label[i][j][0/1/2] unde i = clasa [0,1,2,3,4,5,6,7,8 sau 9]


# j = numărul imaginii din clasă, [0...1500]
# 0 = imaginea matrice, 1 = label imagine, 2 = varsta reala

for nume_folder in lista_foldere:


for nume_subfolder in lista_subfoldere:
path_imagini = "{}/{}/{}".format(path_imagini_main,
nume_folder, nume_subfolder)
for dirname, subdirnames, filenames in os.walk(path_imagini):
for filename in filenames:
if dirname == "{}/04-06/{}".format(path_imagini_main, nume_subfolder):
image = cv2.imread("{}/{}".format(dirname, filename))
image = cv2.resize(image, (224, 224))
histo[0] += 1
real_age = filename[:2].replace('_', '')
lista_imag_label[0].append([image, 0, real_age])

. . .

elif dirname == "{}/61-75/{}".format(path_imagini_main, nume_subfolder):


image = cv2.imread("{}/{}".format(dirname, filename))
image = cv2.resize(image, (224, 224))
histo[8] += 1
real_age = filename[:2]
lista_imag_label[8].append([image, 8, real_age])

# ## Histograma mod distribuire a claselor


plt.figure()
plt.bar(lista_foldere, histo)
plt.show()

# ## 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

for j in range(len_train[i][0], len_train[i][1]):


train_images.append(lista_imag_label[i][j][0])
train_labels.append(lista_imag_label[i][j][1])
train_ages.append(lista_imag_label[i][j][2])
print(len(train_images))

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

# # Shuffle 3 lists simultaneosly


from sklearn.utils import shuffle
train_images, train_labels, train_ages = shuffle(
train_images, train_labels, train_ages)
test_images, test_labels, test_ages = shuffle(
test_images, test_labels, test_ages)

print("Au fost incarcate {} pentru antrenare.".format(len(train_images)))


print("Au fost incarcate {} pentru testare.".format(len(test_images)))

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

B.1 Preluarea și modificarea rețelei convoluționale


from keras import applications
from keras.layers import Dense, GlobalAveragePooling2D, Flatten, Dropout
from keras.models import Model
from keras.utils import plot_model

mobile = applications.mobilenet.MobileNet()
x = mobile.layers[-6].output
x = Dropout(0.5)(x)

# Cele 9 clase la ieșire vor reprezenta categoriile de vârstă


predictions = Dense(9, activation='softmax')(x)
model = Model(inputs=mobile.input, output=predictions)

plot_model(model, to_file='model.png', show_shapes=True, show_layer_names=True)


print(model.summary())

B.2 Antrenarea rețelei convoluționale


from datetime import datetime
from keras.optimizers import SGD
from keras.callbacks import ModelCheckpoint, EarlyStopping

x = datetime.now(); print(x)

optim = SGD(learning_rate=0.0001, momentum=0.9, nesterov=True)


model.compile(optimizer=optim,
loss='categorical_crossentropy',
metrics=['accuracy'])
callbacks_list = [
EarlyStopping(monitor='val_loss', patience=20),
ModelCheckpoint(filepath='AgeClass_best.h5', verbose=1,
monitor='val_loss', save_best_only=True)
]
epochs = 100
batch_size = 2

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

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'

scores = model.evaluate(x_test, y_test, verbose=1)


print('Test loss: {}'.format(scores[0]))
print('Test accuracy: {} \n'.format(scores[1]))

y = datetime.now(); print(y)
print("Durata totala de antrenare: {}".format(abs(y-x)))

B.3 Determinarea acurateței de test extinzând limitele intervalelor


import numpy as np
from keras.models import load_model
classifier = load_model("AgeClass_best.h5")
(train_ages, test_ages) = load_age_training_and_test("AgeClass")
nr_cazuri_corecte = 0
nr_cazuri_corecte_cu_limite = 0
nr_cazuri_totale = len(x_test)
for i in range(0, nr_cazuri_totale):
input_im = x_test[i]
input_im = input_im.reshape(1, 224, 224, 3)
real_label = int(np.argmax(y_test[i]))
real_age = int(test_ages[i])
# Get Prediction
res = list(classifier.predict(input_im, 1, verbose=0)[0])
predict_label = int(np.argmax(res))
if predict_label == real_label:
nr_cazuri_corecte += 1
nr_cazuri_corecte_cu_limite += 1
elif predict_label == 3: # 12-19 ani
if real_age in [10, 11, 20, 21]:
nr_cazuri_corecte_cu_limite += 1
elif predict_label == 4: # 20-27 ani
if real_age in [18, 19, 28, 29]:
nr_cazuri_corecte_cu_limite += 1
elif predict_label == 5: # 28-35 ani)
if real_age in [26, 27, 36, 37]:
nr_cazuri_corecte_cu_limite += 1
elif predict_label == 6: # 36-45 ani
if real_age in [34, 35, 46, 47]:
nr_cazuri_corecte_cu_limite += 1
elif predict_label == 7: # 46-60 ani
if real_age in [44, 45, 61, 62]:
nr_cazuri_corecte_cu_limite += 1
elif predict_label == 8: # 61-75 ani
if real_age in [58, 59, 60]:
nr_cazuri_corecte_cu_limite += 1
print("{} corecte din {} in total".format(nr_cazuri_corecte, nr_cazuri_totale))
print(nr_cazuri_corecte / nr_cazuri_totale)
print("{} corecte (cu limite) din {} in total".format(
nr_cazuri_corecte_cu_limite, nr_cazuri_totale))
print(nr_cazuri_corecte_cu_limite / nr_cazuri_totale)
print("{} poze clasificate in plus in mod corect (cu limita)".format(
nr_cazuri_corecte_cu_limite - nr_cazuri_corecte))
55

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

62 return QSize(200, 200)


63
64
65 class UI_MainWindow(QWidget):
66 def __init__(self):
67 super(UI_MainWindow, self).__init__()
68 self.title = "Age Classification App"
69 self.top = 200
70 self.left = 500
71 self.width = 400
72 self.height = 300
73
74 self.UI_MainWindow_InitSetup()
75
76 def UI_MainWindow_InitSetup(self):
77 self.setWindowTitle(self.title)
78 self.setGeometry(self.left, self.top, self.width, self.height)
79 self.setStyleSheet("background-color: black;")
80
81 MainWindow_Layout = QHBoxLayout()
82
83 # ## Adaugare Buton1 pentru Camera Live
84 self.Btn_Camera = PicButton(QPixmap("./Icons/Btn_Camera.jpg"),
85 QPixmap("./Icons/Btn_Camera_hover.jpg"),
86 QPixmap("./Icons/Btn_Camera_pressed.jpg"))
87 self.Btn_Camera.clicked.connect(self.Open_UI_Camera)
88 MainWindow_Layout.addWidget(self.Btn_Camera)
89
90 # ## Adauga Spacer intre 2 butoane
91 spacerItem = QSpacerItem(
92 40, 40, QSizePolicy.Minimum, QSizePolicy.Preferred)
93 MainWindow_Layout.addItem(spacerItem)
94
95 # ## Adaugare Buton2 pentru Selectare 1 Imagine
96 self.Btn_SelectPic = PicButton(QPixmap("./Icons/Btn_SelectPic.jpg"),
97 QPixmap("./Icons/Btn_SelectPic_hover.jpg"),
98 QPixmap("./Icons/Btn_SelectPic_pressed.jpg"))
99 self.Btn_SelectPic.clicked.connect(self.Open_UI_SelectPic)
100 MainWindow_Layout.addWidget(self.Btn_SelectPic)
101
102 # ## Adauga Spacer intre 2 butoane
103 spacerItem = QSpacerItem(
104 40, 40, QSizePolicy.Minimum, QSizePolicy.Preferred)
105 MainWindow_Layout.addItem(spacerItem)
106
107 # ## Adaugare Buton3 pentru Selectare Galerie Imagini
108 self.Btn_SelectGallery = PicButton(QPixmap("./Icons/Btn_SelectGallery.jpg"),
109 QPixmap("./Icons/Btn_SelectGallery_hover.jpg"),
110 QPixmap("./Icons/Btn_SelectGallery_pressed.jpg"))
111 self.Btn_SelectGallery.clicked.connect(self.Open_UI_SelectGallery)
112 MainWindow_Layout.addWidget(self.Btn_SelectGallery)
113
114 self.setLayout(MainWindow_Layout)
115 self.show()
116
117 def Open_UI_Camera(self):
118 self.ui = UI_CameraWindow()
119 self.ui.show()
120
121 def Open_UI_SelectPic(self):
122 self.ui = UI_SelectPic()
123 self.ui.show()
124
125 def Open_UI_SelectGallery(self):
126 self.ui = UI_SelectGallery()
127 self.ui.show()
128
129
130 class UI_CameraWindow(QWidget):
131 def __init__(self):
132 # ## Partea de UI Setup
133 super(UI_CameraWindow, self).__init__()
134
135 self.setObjectName("Camera_Window")
136 self.resize(700, 480)
57

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

S-ar putea să vă placă și