P5 und ML5: Face-Tracking via Webcam (Konturen)

 

Über ML5 kann P5 auf Machine-Learning-Modelle zugeifen, die im Bereich der Mustererkennung Dinge ermöglichen, die vor einigen Jahren für einfache 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 diese Möglichkeiten zugreifen zu können, muss im Header der HTML-Seite neben der P5-Library auch die ML5-Library eingebunden werden, so dass es im Header der Seite folgendermaßen heisst:

<script src="header/ml5.min_0.12.2.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: false, withDescriptors: false }; // Einstellung der Optionen für die Gesichtserkennung
// (withExpressions wurde leider wieder aus dem Gesichtserkennungs-Paket entfernt, da es nicht dem
// Code of Conduct von ML5 entsprach....).

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 einzelnen markanten Punkte des Gesichts ausgegeben, die dann beliebig weiter verarbeitet werden können, z.B.:

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

filter(THRESHOLD); // Schwarz-Weiß-Filter einbauen (sieht schöner aus :->)
tint(255, 255, 255, map(mouseY, 0, height, 0, 255), ); // Transparenz des
// Videos mit der Y-Position der Maus verknüpfen

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

 

Insgesamt sieht das Script dann folgendermaßen aus:

 

<script src="header/ml5.min_0.12.2.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: false, withDescriptors: false };
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);
filter(THRESHOLD);
tint(255, 255, 255, map(mouseY, 0, height, 0, 255), );

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);}
}
}

</script>

<DIV id="p5container" style="width:1024;border: 1px solid #333;box-shadow: 8px 8px 5px #444;padding: 8px 12px;background-color:ffffff"></DIV>