P5
und ML5: Face-Tracking
(Konturen mit Emotionseinschätzung)
Über
ML5 kann P5 auf Machine-Learning-Modelle zugeifen, die im Bereich der
Mustererkennung Dinge ermöglichen, die vor einigen Jahren für
einfach Scriptsprachen wie Javascript oder P5 noch undenkbar waren. Einer
dieser Hauptbereiche ist die Erkennung von menschlichen Posen und Bewegungen,
so dass nun via Javascript/P5 Bewegungen und Posen erfasst und in Zahlen
umgesetzt werden können, die dann mit anderen Zahlenwerten (z.B.
aus physiologischen Messungen, Timbre Feature Analysen, EEG, Eye-Tracking
etc.) in Verbindung gebracht werden können.
Um auf Möglichkeiten
der Emotionserkennung zugreifen zu können, muss im Header der HTML-Seite
neben der P5-Library
auch eine frühere ML5-Library
eingebunden werden, die auch unter header/ml5.min_expressions.js
in dieser Seite eingebunden ist (mit späteren ML5-Libraries funktioniert
die Emotionseinschätzung nicht mehr, da sie einen Verstoß gegen
den Code of Conduct von ML5 darstellt),
so dass es im Header der Seite folgendermaßen heisst:
<script src="header/ml5.min_expressions.js"></script>
Im Script
wird nach der Deklarierung der für die Verbindung mit der Gesichtserkennung
via faceApi und die Videodarstellung notwendigen Variablen ...
var faceapi; //
Array für das von faceApi zurückgelieferte Ergebnis
var detections = []; // Array für
die erfassten Gesichtskonturen
var video; // Variable für das von
der Webcam erfasste Video
... die setup-Funktion
gestartet, in der sowohl der Canvas zur Darstellung von WebCam-Video und
Gesichtsserkennung erstellt wird als auch die Verbindung mit dem Modell
für die Gesichtsserkennungserkennung hergestellt wird:
function setup() {
var container = createCanvas(1024, 768); //
Canvas erstellen
container.parent('p5container'); // an
DIV-Container anhängen
video = createCapture(VIDEO); // Videoeingang
aktivieren
video.size(width, height); // Video der
Größe des Canvas anpassen
const faceOptions = { withLandmarks: true, withExpressions: true, withDescriptors:
false }; // Einstellung der Optionen für
die Gesichtserkennung
faceapi = ml5.faceApi(video, faceOptions, faceReady); //
Verbindung zu faceApi
// herstellen, um Gesichter zu erkennen und das Ergebnis in der Variablen
faceapi zu speichern.
video.hide(); // verstecke das Video (es
würde sonst neben dem Canvas zusätzlich zum
// verarbeiteten Video erscheinen)
}
Sobald die
Verbindung zu faceApi hergestellt ist, wird über die Funktion faceReady()
die Gesichtserkennung gestartet ...
function faceReady()
{
faceapi.detect(gotFaces); // erkenne das
Gesicht im Video
}
... und -
unter Einbindung des Abfangens von eventuellen Fehlermeldungen - ein Array
mit Zahlen gefüllt, die markante Punkte des Gesichts beschreiben:
function gotFaces(error,
result) {
if (error) { console.log(error); return;}
// Fehlermeldungen abfangen, damit das
// Script bei einem Fehler nicht gestoppt wird
detections = result; // fülle ein
Array mit einer zahlenmäßigen Beschreibung des erkannten
// Gesichts
faceapi.detect(gotFaces); // und starte
den Erkennungsvorgang erneut.
}
In der draw-Funktion
werden dann die Funktionen drawLandmarks()
und drawExpressions() gestartet,
mit denen die einzelnen markanten Punkte des Gesichts ausgegeben sowie
die dazu gehörenden Emotionen eingeschätzt werden:
function draw() {
background(255,255,255,125); // Hintergrund
halb durchsichtig gestalten, damit mit der
// Maus zwischen Gesichtskontur und Video hin- und hergeblendet werden
kann
cursor(HAND); // Mauszeiger in eine
Hand umwandeln
image(video, 0, 0, width, height); // zeige
das Video in der Höhe und Weite des Canvas
// an
tint(255, 255, 255, map(mouseY, 0, height, 0, 255), ); //
Transparenz des
// Videos mit der Y-Position der Maus verknüpfen
drawLandmarks(detections); // gebe die
markannten Gesichtspunkte aus
drawExpressions(detections); // gebe
die dazu eingeschätzten Ausdrücke/Emotionen aus
}
Sobald ein
Gesicht erkannt wird, werden die dazu gehörenden landmarks
mit ihren Positionen in ein Array (points)
geschrieben, das dann beliebig weiter verarbeitet/ausgegeben werden kann:
function drawLandmarks(){
if (detections.length
> 0) { // sobald ein Gesicht erkannt
wurde ...
var points = detections[0].landmarks.positions; //
... schreibe die dazu
// gehörenden Punkte in das Array points
stroke(184, 0, 0); noFill(); strokeWeight(4); //
Strichfarbe auf dunkelrot setzen
// und die Strichweite auf 4 Pixel erhöhen.
//
Gesicht: aus den Punkten 0-16 wird ein Shape gebildet:
beginShape();
for (let i = 0; i < 17; i++) {vertex(points[i]._x, points[i]._y);}
endShape();
// Augenbraue links: aus
den Punkten 17-21 wird ein Shape gebildet:
beginShape(); for (let i = 17; i < 22; i++) {vertex(points[i]._x, points[i]._y);}
endShape();
// Augenbraue rechts:
aus
den Punkten 22-26 wird ein Shape gebildet:
beginShape(); for (let i = 22; i < 27; i++) {vertex(points[i]._x, points[i]._y);}
endShape();
// Nase:
aus
den Punkten 27-35 wird ein Shape gebildet:
beginShape(); for (let i = 27; i < 36; i++) {vertex(points[i]._x, points[i]._y);}
endShape();
// Auge links: aus
den Punkten 36-41 wird ein Shape gebildet:
beginShape(); for (let i = 36; i < 42; i++) {vertex(points[i]._x, points[i]._y);}
endShape();
// Auge rechts: aus
den Punkten 42-47 wird ein Shape gebildet:
beginShape(); for (let i = 42; i < 48; i++) {vertex(points[i]._x, points[i]._y);}
endShape();
// Mund:
aus den Punkten 48-67 wird ein Shape gebildet:
beginShape(); for (let i = 48; i < 68; i++) {vertex(points[i]._x, points[i]._y);}
endShape();
// Alle Gesichtspunkte (0-67) werden ausgegeben:
stroke(184, 184, 184); // Strichfarbe auf
grau setzen
for (let i = 0; i < points.length; i++) {point(points[i]._x, points[i]._y);}
// alle Punkte aus dem Array mit ihren
X- und Y-Koordinaten ausgeben.
}
}
Die Art der
Gesichtskontur wird in der Reihenfolge der points
codiert. So kann man über folgende Variablen auf die jeweiligen Körperteile/Sinnesorgane
zugreifen:
- points[0] bis
points[16]
= Umriss des Gesichts
- points[17] bis
points[21]
= Augenbraue links
- points[22] bis
points[26]
=
Augenbraue rechts
- points[27] bis
points[35]
= Nase
- points[36] bis
points[41]
= Auge links
- points[42] bis
points[47]
= Auge rechts
- points[48] bis
points[67]
= Mund
In der Funktion
drawExpressions() werden
die aus den Gesichtsausdrücken eingeschätzten Emotionen in prozentualen
Anteilen ausgegeben:
function drawExpressions(detections){
fill(184, 0, 0); noStroke(); // Füllfarbe
auf dunkelrot setzen
text("Neutral: ", 850, 10); //
Textausgabe für neutral
text("Freude: ", 850, 30); //
Textausgabe für Freude
text("Wut: ", 850, 50); // Textausgabe
für Wut
text("Trauer: ", 850, 70); //
Textausgabe für Trauer
text("Ekel: ", 850, 90); // Textausgabe
für Ekel
text("Ueberraschung: ", 850, 110); //
Textausgabe für Überraschung
text("Angst: ", 850, 130); //
Textausgabe für Angst
if(detections.length > 0){ // sobald
ein Gesicht entdeckt wird ...
var {neutral, happy, angry, sad, disgusted, surprised, fearful} = detections[0].expressions;
// ... weise die Zahlen aus dem Array detections[0].expressions
den einzelnen Variablen für die Emotionen zu
text(round(neutral*10000)/100+"%", 950, 10);
// Prozentausgabe für neutral
text(round(happy*10000)/100+"%", 950, 30);
// Prozentausgabe für Freude
text(round(angry*10000)/100+"%", 950, 50);
// Prozentausgabe für Wut
text(round(sad*10000)/100+"%", 950, 70);
// Prozentausgabe für Trauer
text(round(disgusted*10000)/100+"%", 950, 90);
// Prozentausgabe für Ekel
text(round(surprised*10000)/100+"%", 950, 110);
// Prozentausgabe für
// Überraschung
text(round(fearful*10000)/100+"%", 950, 130);
// Prozentausgabe für Angst
}
}
Insgesamt
sieht das Script dann folgendermaßen aus:
<script src="header/ml5.min_expressions.js"></script>
<script src="header/p5.js"></script>
<script>
var faceapi;
var detections = [];
var video;
function setup() {
var container = createCanvas(1024, 768);
container.parent('p5container');
video = createCapture(VIDEO);
video.size(width, height);
const faceOptions = { withLandmarks: true, withExpressions: true, withDescriptors:
false, minConfidence: 0.2 };
faceapi = ml5.faceApi(video, faceOptions, faceReady);
video.hide();
}
function faceReady()
{
faceapi.detect(gotFaces);
}
function gotFaces(error,
result) {
if (error) {
console.log(error);
return;
}
detections = result;
faceapi.detect(gotFaces);
}
function draw() {
background(255,255,255,125);
cursor(HAND);
image(video, 0, 0, width, height);
tint(255, 255, 255, map(mouseY, 0, height, 0, 255), );
drawLandmarks(detections);
drawExpressions(detections);
}
function drawLandmarks(){
if (detections.length > 0) {
var points = detections[0].landmarks.positions;
stroke(184, 0, 0); noFill(); strokeWeight(4);
//Gesicht
beginShape(); for (let i = 0; i < 17; i++) {vertex(points[i]._x,
points[i]._y);} endShape();
//Augenbraue links
beginShape(); for (let i = 17; i < 22; i++) {vertex(points[i]._x,
points[i]._y);} endShape();
//Augenbraue rechts <br>
beginShape(); for (let i = 22; i < 27; i++) {vertex(points[i]._x,
points[i]._y);} endShape();
//Nase
beginShape(); for (let i = 27; i < 36; i++) {vertex(points[i]._x,
points[i]._y);} endShape();
//Auge links
beginShape(); for (let i = 36; i < 42; i++) {vertex(points[i]._x,
points[i]._y);} endShape();
//Auge rechts
beginShape(); for (let i = 42; i < 48; i++) {vertex(points[i]._x,
points[i]._y);} endShape();
//Mund
beginShape(); for (let i = 48; i < 68; i++) {vertex(points[i]._x,
points[i]._y);} endShape();
//Alle Gesichtspunkte
stroke(184, 184, 184); for (let i = 0; i < points.length; i++) {point(points[i]._x,
points[i]._y);}
}
}
function drawExpressions(detections){
fill(184, 0, 0); noStroke();
text("Neutral: ", 850, 10);
text("Freude: ", 850, 30);
text("Wut: ", 850, 50);
text("Trauer: ", 850, 70);
text("Ekel: ", 850, 90);
text("Ueberraschung: ", 850, 110);
text("Angst: ", 850, 130);
if(detections.length > 0){
var {neutral, happy, angry, sad, disgusted, surprised, fearful} = detections[0].expressions;
text(round(neutral*10000)/100+"%", 950, 10);
text(round(happy*10000)/100+"%", 950, 30);
text(round(angry*10000)/100+"%", 950, 50);
text(round(sad*10000)/100+"%", 950, 70);
text(round(disgusted*10000)/100+"%", 950, 90);
text(round(surprised*10000)/100+"%", 950, 110);
text(round(fearful*10000)/100+"%", 950, 130);
}
}
</script>
<DIV id="p5container"
style="width:1024;border: 1px solid #333;box-shadow: 8px 8px 5px
#444;padding: 8px 12px;background-color:ffffff"></DIV>
|