Artikel

Bildigenkänning - hitta ansiktet i bilden

Introduktion

Att automatiskt identifiera ett ansikte i en digital bild eller video kallas ansiktsigenkänning. Sofistikerade metoder för ansiktsigenkänning använder sig av utvalda ansiktsdrag som jämförs med områden i bilden. En sådan metod använder sig av ett egenansikte som är ett standardansikte skapat utifrån en stor mängd ansikten. Egenansiktet fångar de viktigaste dragen i ett ansikte som alltså används för att identifiera ansikten i en bild. I ett senare skede kan man även matcha identifierade ansikten mot en databas för att dra slutsatser om vem personen är.

Här skall vi använda oss av en rudimentär metod för ansiktsigenkänning. Ansikten identifieras i två steg:

  1. Pixlar som troligtvis är hudfärgade klassificeras som hudfärgade.

  2. Den största ansamlingen med hudfärgade pixlar antas vara ett ansikte.

Kort teori

Genom att skapa en modell över en hudfärgad pixel får vi ett sätt att klassificera pixlar utifrån. En pixel som stämmer tillräckligt bra överens med vår modell klassificerar vi som hudfärgad och annars som ej hudfärgad.

I bland annat signalbehandling och bildbehandling används ofta normalfördelningar för att skapa modeller i den här typen av tillämpningar. Att normalfördelningen är väldigt betydelsefull kommer av centrala gränsvärdessatsen (CGS) som säger

Medelvärdet av ett tillräckligt stort antal upprepningar av oberoende stokastiska (slumpmässiga) variabler kommer att kunna approximeras med en normalfördelning

Antag att vi skall göra en undersökning av medelåldern hos biobesökare till en viss film men vi har inte möjlighet att kontrollera alla. Vi gör ett stickprov där vi frågar fem personer och beräknar medelvärdet för detta urvalet. Vi upprepar detta tillräckligt många gånger och noterar medelvärdet för varje urval. Om resultatet förs in i ett diagram där x-axeln representerar medelvärdet och y-axeln representerar antalet stickprov med ett visst medelvärdet så kommer vi finna själva kärnan i nyttan med normalfördelningen

En normalfördelad variabel antar ofta värden som ligger nära medelvärdet och mycket sällan värden som har en stor avvikelse.

Att känna till detta gör att vi kan skapa approximeringar av fenomen som annars kräver omfattande uträkningar. Tänk dig följande problem i en tid där miniräknare inte existerade:

En tärning kastas 100 gånger, vad är sannolikheten att få fler än 40 treor? 

För att lösa problemet beräknas sannolikheten för 40 treor, 41 treor, 43 treor etc. och därefter summeras alla dessa sannolikheter.  Resultatet är ett exempel på en binomialfördelning: en diskret fördelning som uppkommer genom upprepade försök där en speciell händelse har samma sannolikhet i varje försök. Istället kan vi alltså utnyttja CGS och utgå från att resultatet går att approximera med en normalfördelning. Historiskt var CGS viktigt just för möjligheten att approximera bionomialfördelningar med normalfördelningar. Vi kan alltså direkt beräkna tätheten (höjden på kurvan för ett givet x-värde) för en normalfördelad stokastisk variabel med medelvärdet (egentligen väntevärdet) μ och standardavvikelse σ med följande funktion:

\begin{equation} g(x)=\frac{1}{\sigma\sqrt{2\pi}}e^{-\frac{1}{2}(\frac{x-\mu}{\sigma })^{2}} \end{equation}

Medelvärdet avgör helt enkel var längs x-axeln som fördelningen har sitt centrum medan standardavvikelser anger hur bred kurvan är:

Normalfördelning
Normalfördelning

Tätheten är ett mått på hur sannolika olika resultat är i förhållande till varandra. Vilket i det här fallet innebär att positiva och negativa avvikelser kring medelvärdet är lika troliga samt att små avvikelser är mer troliga än stora avvikelser.

Det är precis detta vi vill utnyttja i vår ansiktsigenkännare. De som har små avvikelser mot medelvärdet vill vi klassificera som hudfärgade medan övriga skall klassificeras som icke hudfärgade. Om vi kan bestämma ett medelvärde på en hudfärgad pixel så kan vi jämföra andra pixlar mot detta medelvärde. 

Det data som vi utgår ifrån är en pixels färg. En pixel representeras med sitt rgb-värde, alltså en vektor, vilket gör att vi behöver använda normalfördelningens flerdimensionella, eller multivariata, motsvarighet:

\begin{equation} f(\vec{x})=\frac{1}{\sqrt{(2\pi)^{k}\left|\Sigma\right |}}e^{(-\frac{1}{2}(\vec{x}-\vec{\mu})^{T}\Sigma^{-1}(\vec{x}-\vec{\mu}))} \end{equation}

där μ är en vektor med medelvärden och Σ är en kovarians-matris. Första steget är att "träna" vår modell genom att bestämma μ och Σ.

Del 1: träna den flerdimensionella Gaussiska

För träningen behöver vi en stor mängd data med hudfärgade pixlar så att vi får med vissa variationer i färgen på huden. Som tur är finns det folk som har satt ihop sådana arkiv. Exempelvis finns det ett arkiv på George W Bush som du laddar ner här: http://vis-www.cs.umass.edu/lfw/lfw-bush.tgz. På samtliga bilder i det här arktivet är ansiktet centrerat i bilden så om vi väljer ut ett litet område i mitten av bilden kommer vi garanterat få enbart pixlar som är hudfärgade, vilket är precis vad vi behöver.

Hämta träningsdata

Vi börjar med att skapa en funktion som hämtar ett kvadratiskt område i mitten av bilden. Funktionen TaCentrumPixlar tar följande argument

  • bi_namn (textsträng) är filnamnet på bilden
  • p (flyttal mellan 0 och 1) anger hur stort område som skall extraheras
function bic = TaCentrumPixlar(bi_fnamn, p) 
bi = double(imread(bi_fnamn)); 
[H W] = size(bi); 
start = (1-p)/2; 
sx = floor(start*W); 
sy = floor(start*H); 
lx = floor(p*W); 
ly = floor(p*H); 
bic = im(sy:sy+ly-1, sx:sx+lx-1, :); 
end

På rad 2 läser vi in bilden till variabeln bi, inga konstigheter. På rad 4 och 5 bestämmer vi vilket element, i x- och y-led, som är början på det kvadratiska området. Avslutningsvis, på rad 9, sker själva beskärningen.

Testfall för träningsdata

Skapa en m-fil, döp den till exempelvis test_del1_traeningsdata.m, och lägg in följande rader:

% en godtycklig bild
bi_fnamn = 'lfw/George_W_Bush_0001.jpg';
% läs in bilden i matrisen bi
bi = double(imread(bi_fnamn));
% hämta ett kvadratiskt område till matrisen bic
bic = TaCentrumPixlar(bi_fnamn,0.5);
% visa i en plot
subplot(121)
imshow(bi/255);
subplot(122)
imshow(bic/255);
axis equal

Se figuren nedan för att förstå hur TaCentrumPixlar arbetar. Originalet är till vänster och det område som TaCentrumPixlar har valt ut är till höger. 

Träningsdata
Träningsdata

Bestäm parametrarna i modellen

Nu är vi redo att skriva en funktion som beräknar parametrarna μ och Σ. Den är återgiven i listan nedan i sin helhet. 

function [my, Sigma] = GaussiskaPara(Mappnamn, n)
   minstr = [Mappnamn, '/*.jpg'];
   bi_filer = dir(minstr);
   p = 0.2;
   fnamn = cell(1, length(bi_filer));
   rgb_data = [];
   
   for i=1:length(bi_filer)
       fnamn{i} = [Mappnamn, '/', bi_filer(i).name];
   end
   
   for i=1:n
       bic = TaCentrumPixlar(fnamn{i}, p);
       data = reshape(bic, [size(bic,1)*size(bic,2), 3]);
       rgb_data = [rgb_data; data];
   end
   
   my = mean(rgb_data, 1);
   Sigma = cov(rgb_data);
end

Funktionshuvudet deklarerar två argument:

  • Mappnamn anger mappen där bilderna är lagrade
  • n anger hur många bilder som skall läsas in.

På rad 2 skapas en söksträng som används på rad 3 för att returnera samtliga filer med ändelsen .jpg. I slingan på rad 7 till 9 skapar vi en fullständig sökväg till alla våra bildfiler och sparar dessa i ett fält av typen cell. 

I nästa for-slinga använder vi funktionen TaCentrumPixlar på varje bildfil och lagrar de extraherade pixlarna i en lokal variabel, bic av storlek HxBx3, där H och B beror på p. Varje bild är alltså återgiven av tre stycken två dimensionella matriser, eller, annorlunda uttryckt: alla tre kanaler representeras av varsin tvådimensionell matris. 

På rad 13 och 14 använder vi ett enkelt trick som kräver lite förklaring. Vi sa att bic är av storlek HxBx3. På rad 13 omformar vi denna till en matris av storlek HxBx3 och lagrar resultatet i varibeln data. På det här sättet lagrar vi alltså kanalerna i varsin kolumn. Slutligen, på rad 14, lagrar vi den tvådimensionella matrisen i fältet rgb_data. 

Genom att skriva

rgb_data = [rgb_data; data];

skapar vi en automatiskt växande matris, där vi hela tiden fyller på med data radvis, efter rgb_data. Vi kan enkelt plocka ut en specifik bild i matrisen eftersom vi vet att alla bilder har samma höjd och bredd och således alltid tar upp samma antal rader i matrisen rgb_data. Om vi vill plocka ut första bilder väljer vi alltså de H*B första raderna i samtliga tre kolumner.

På rad 17 och 18 kan vi enkelt beräkna medelvärdet och kovariansen i rgb_data genom att bilderna är lagrade enligt ovan. mu är en vektor av storlek 1x3 med medelvärdet av varje kolumn och Sigma är en matris av storlek 3x3 med kovariansen av pixeldatan.

Testfall för modellparametrar

Skapa en m-fil, test_del1_parametrar.m, och lägg in följande rader:

addpath('lfw'); % lägg till underkatalogen i sökvägen
[my_h, Sigma_h] = GaussiskaPara('lfw', 20); % beräkna p(x,hudfärg)

Kör test_del1_parametrar.m för att kontrollera att resultatet på μ och Σ är följande: 

\begin{align} \mu &= \begin{bmatrix} 176,91 & 129,18 & 103,88 \end{bmatrix} \\ \Sigma &= 1,0e+003\begin{bmatrix} 1,7264 & 1,4316 & 1,4479 \\ 1,4316 & 1,4268 & 1,4498 \\ 1,4479 & 1,4498 & 1,5935 \end{bmatrix} \end{align}

Del 2: betingad sannolikhet för hudfärg

Nu behöver vi en funktion för att beräkna de betingade sannolikheterna. Alltså en funktion som beräknar den flerdimensionella gaussdistributionen för givna värden på x, μ och Σ. 

Vi skapar en funktion, GaussSannolikhet, som gör detta. Målet är alltså att få ut en bild, i samma storlek som originalbilden, men där pixelns värde hela tiden beror på hur troligt det är att pixeln är hudfärgad. 

Detta kan göras med endast ett par rader kod:

function tvaerden = GaussSannolikhet(x, my, Sigma)
[r c] = size(my);
% my är en vektor med tre element som vi gör till en matris av 
% samma storlek som bilden i x
my = repmat(my, size(x,1), 1);
% bestäm differenserna till exp(...)
x = x - my;
prob = sum(x' .* (inv(Sigma)*x'),1);
n = ((2*pi)^(r/2)) * (det(Sigma)^(0.5));
t = exp(-0.5*prob);
tvaerden = t / n;
end

Jämför med funktionen för multivariata normalfördelningen som gavs tidigare. Med GaussSannolikhet kan vi beräkna en betingad sannolikhet, exempelvis p(x|hudfarg).

Test del 2

Skapa en ny m-fil, test_del2.m, och lägg in följande rader

bi = double(imread('kungen.jpg'));
%omforma till tre kolumner
bi_eval = reshape(bi, [size(bi,1)*size(bi,2),3]);
[my_h, Sigma_h] = GaussiskaPara('lfw', 20);
tvaerden_hud = GaussSannolikhet(bi_eval, my_h, Sigma_h);
% omforma till en bild
bi_tvaerden_hud = reshape(tvaerden_hud, [size(bi, 1), size(bi, 2)]);
% normalisera bilderna och visa resultatet
imshow(bi/255)
figure
imshow(bi_tvaerden_hud/max(bi_tvaerden_hud(:)))

På rad 1 läser vi in originalbilden. På rad 3 bestämmer vi μ och Σ. På rad 4 använder vi μ och Σ för att beställa ett nytt värde på varje pixel i bilden bi. Detta nya värde beror alltså på hur troligt det är att just den aktuella pixeln är hudfärgad. Utdatat, de nya pixelvärdena, sparas i tvaerden_hud.

I figuren nedan syns originalbilden till vänster. Till höger är den bild som kommer av tvaerden_hud. Detta är den betingade sannolikheten för hudfärgade pixlar.

Betingad sannolikhet
Betingad sannolikhet

Del 3: klassificera pixlar

Nu skall vi bestämma om färgen på en viss pixel är hudfärgad eller inte. Enklast är att beräkna förhållandet mellan p(x|hudfärg) och p(x|icke hudfärg). Om kvoten är >1 är täljaren större än nämnaren och således är den betingade sannolikheten för att den aktuella pixeln är hudfärgad större än att den inte är hudfärgad. 

Vi väljer att representera den på samma sätt som p(x|hudfärg), alltså med en flerdimensionell Gaussisk. Börja med att skapa en funktion som kan klassificera pixlarna i en godtycklig bild:

function bik = HudKlassificering(bi, mer)

% omforma till tre kolumner
bi_eval = reshape(bi, [size(bi,1)*size(bi,2),3]);

[my_hud, Sigma_hud] = GaussiskaPara('lfw', 20);
[my_ej_hud, Sigma_ej_hud] = GaussiskaPara('Bakgrundsbilder', 20);

tvaerden_hud = GaussSannolikhet(bi_eval, my_hud, Sigma_hud);
tvaerden_ej_hud = GaussSannolikhet(bi_eval, my_ej_hud, Sigma_ej_hud);

% omforma till en bild
bi_tvaerden_hud = reshape(tvaerden_hud, [size(bi, 1), size(bi, 2)]);
bi_tvaerden_ej_hud = reshape(tvaerden_ej_hud, [size(bi, 1), size(bi, 2)]);

% beräkna trolighets-förhållandet
bi_tvaerden_ratio = bi_tvaerden_hud ./ bi_tvaerden_ej_hud;
bik = zeros(size(bi_tvaerden_hud));
bik(bi_tvaerden_ratio > 1) = 1;

if mer==1
% visa original
imshow(bi/255);
figure
% visa delresultat
imshow([bi_tvaerden_hud/max(bi_tvaerden_hud(:)), bi_tvaerden_ej_hud/max(bi_tvaerden_ej_hud(:)), bi_tvaerden_ratio/max(bi_tvaerden_ratio(:)), bik]);
end

end

Uppgiften för HudKlassificering är att ta emot en bild, som vi vill identifiera ansiktet i, och returnera en bild med samtliga pixlar klassificerade som antingen hudfärg eller ej hudfärg. Utdatan, bik, kommer alltså vara en bild med vita pixlar där vi har identifierat hudfärgade pixlar.

På rad 11 och 12 beräknar vi μ och Σ med samma funktion som tidigare, GaussiskaPara, fast den här gången med bilderna i mappen Bakgrundsbilder som träningsdata. Dubbelkolla gärna att värdena på μ och Σ är:


Bakgrundsbilder

Förhållandet mellan de betingade sannoliheterna beräknar vi på rad 16. Vi utför en punktvis, alltså elementvis, division mellan matriserna så att vi får en matris av samma storlek i bi_tvaerden_ratio. På rad 18 utför vi själva klassificering: på de platser där förhållandet är >1 vill vi klassificera pixeln som en hudpixel.

I bik kommer vi ha enbart nollor och ettor där ettor representerar hud. Vi kan visa den här matrisen som en bild för att illustrera var vi har hud och inte hud. Detta sker på rad 21.

Test del 3

Skapa test_del3.m och lägg in följande:

addpath('lfw');
addpath('Bakgrundsbilder');
bi = double(imread('kungen.jpg'));
bik = HudKlassificering(bi, 1);

Nu är vi nära målet. Nedan är originalbilden längst till vänster och sannolikhetsberäkningarna i de två i mitten. Bilden näst längst till vänster känner vi igen. I bilden näst längst till höger har vi beräknat hur sannolikt det är att pixlarna ej är hudfärgade (tvaerden_ej_hud).

Den mest intressanta just nu är bilden längst till höger. Här är värdet på varje enskild pixel antingen 0 eller 1. Detta är resultatet av vår klassificering av pixlarna.

Klassificering
Klassificering

Del 4: hitta ansiktet i bilden

Nu utgår vi alltså från bilden längst till höger från del 3. Vi antar att den största klump av vita pixlar i bik är ansiktet. Till vår hjälp finns den Matlabs inbyggda funktion bwlabeln. Skapa en funktion, HittaViktigasteKomp, med följande innehåll:

function [X, Y] = HittaViktigasteKomp(bik)
[L, nl] = bwlabeln(bik);
sz = zeros(1, nl);
for i=1:nl
 sz(i) = sum(find(L(:) == i));
end
[~, ind] = max(sz);
[I, J] = find(L == ind);
X = [min(J), min(J), max(J), max(J)];
Y = [min(I), max(I), max(I), min(I)];

HittaViktigasteKomp returnerar en vektor med x- och y-koordinater för hörnen i en box. Vi kan visa originalbilden och rita en fyrkant runt ansiktet ovanpå originalet, utifrån dessa koordinater. 

Test del 4

Nu är vi redo att testa hela systemet. Skapa ytterligare en ny m-fil som du kallar för test_del4.m och lägg in följande rader:

bi = double(imread('kungen.jpg'));
bik = HudKlassificering(bi, 0);
[X, Y] = HittaViktigasteKomp(bik);
figure
montage({bi/256});
axis equal
axis off
% håll kvar figuren så att vi kan rita en fyrkant i samma bild
hold on
% lägg till första punkten igen så att vi får en fyrkant
line([X, X(1)],[Y, Y(1)]); 

Ansiktet borde nu vara inom den blå rektangeln:

Ansiktet är hittat!
Ansiktet är hittat!

Hämta källkoden

Mot en avgift kan du hämta hem källkoden som används i artikeln så att du kan testa funktionen direkt! Gå till vår butik för att lägga till i varukorg och betala.

Varför PROSAPIO?

PROSAPIO vill bidra till att unga människor intresserar sig för ingenjörsvetenskap genom att tillgängliggöra kunskap inom intelligenta och autonoma system.

Läs mer om PROSAPIO

Genvägar