P5 und ML5: Face-Tracking in Bildern (hochaufgelöst) mit Triangulation


Ü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 werden nach der Deklarierung der für die Verbindung mit der Gesichtserkennung via facemesh und die Bilddarstellung notwendigen Variablen ...

var facemesh; // Array für das von faceMesh zurückgelieferte Ergebnis
var predictions = []; // Array für die erfassten Gesichtskonturen
var bild; // Variable für das zu analysierende Bild
var bildweite = 563; // Weite des zu untersuchenden Bilds
var bildhoehe = 479; // Höhe des zu untersuchenden Bilds

... die Punkte als Arrays definiert, die die jeweils markanten Gesichtszüge ausmachen:

var gesicht = [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288, 397, 365, 379, 378, 400, 377, 152, 148, 149, 176, 136, 150, 58, 172, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109, 10];

var rechtes_auge_oben0 = [246, 161, 160, 159, 158, 157, 173];
var rechtes_auge_unten0 = [33, 7, 163, 144, 145, 153, 154, 155, 133];
var rechtes_auge_oben1 = [247, 30, 29, 27, 28, 56, 190];
var rechtes_auge_unten1 = [130, 25, 110, 24, 23, 22, 26, 112, 243];
var rechtes_auge_oben2 = [113, 225, 224, 223, 222, 221, 189];
var rechtes_auge_unten2 = [226, 31, 228, 229, 230, 231, 232, 233, 244];
var rechtes_auge_unten3 = [143, 111, 117, 118, 119, 120, 121, 128, 245];
var rechte_augenbraue_oben = [156, 70, 63, 105, 66, 107, 55, 193];
var rechte_augenbraue_unten = [35, 124, 46, 53, 52, 65];

var linkes_auge_oben0 = [466, 388, 387, 386, 385, 384, 398];
var linkes_auge_unten0 = [263, 249, 390, 373, 374, 380, 381, 382, 362];
var linkes_auge_oben1 = [467, 260, 259, 257, 258, 286, 414];
var linkes_auge_unten1 = [359, 255, 339, 254, 253, 252, 256, 341, 463];
var linkes_auge_oben2 = [342, 445, 444, 443, 442, 441, 413];
var linkes_auge_unten2 = [446, 261, 448, 449, 450, 451, 452, 453, 464];
var linkes_auge_unten3 = [372, 340, 346, 347, 348, 349, 350, 357, 465];
var linke_augenbraue_oben = [383, 300, 293, 334, 296, 336, 285, 417];
var linke_augenbraue_unten = [265, 353, 276, 283, 282, 295];

var nase = [8, 417, 465, 343, 437, 420, 279, 331, 294, 460, 326, 2, 97, 240, 64, 102, 49, 198, 217, 114, 245, 193, 8];

var oberlippe_aussen = [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291];
var unterlippe_aussen = [146, 91, 181, 84, 17, 314, 405, 321, 375, 291];
var oberlippe_innen = [78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308];
var unterlippe_innen = [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308];

Daraufhin wird die setup-Funktion gestartet, in der sowohl der Canvas zur Darstellung des Bilds und der Gesichtsserkennung erstellt wird. Der Canvas sollte dabei mindestens so groß wie die Maße des Bilds sein:

function setup() {
var container = createCanvas(800,500); // Canvas erstellen
container.parent('p5container'); // an DIV-Container anhängen
bild = createImg("bilder/gesicht.png", imageReady); // Bild laden und die Funktion imageReady() aufrufen, sobald das Bild geladen ist.
bild.hide(); // verstecke das Bild (es würde sonst neben dem Canvas zusätzlich zum
// verarbeiteten Bild erscheinen)

}

Sobald das Bild geladen ist, wird die Verbindung zum FaceMesh-Model hergestellt, das die erkannten Punkte des Gesichts (keypoints) als Ergebnis (results) herausgibt, sobald das Modell fertig geladen ist

function imageReady() {
facemesh = ml5.facemesh(modelReady); // Verbindung zu faceMesh
// herstellen, um Gesichter zu erkennen und das Ergebnis in der Variablen facemesh zu speichern.

facemesh.on("predict", results => { // sobald ein Gesicht erkannt wird
predictions = results; // fülle ein Array (results) mit einer zahlenmäßigen Beschreibung des erkannten Gesichts und weise es der Variablen predictions zu
});

Sobald die Verbindung zu faceMesh hergestellt ist, kann die Gesichtserkennung gestartet werden

function modelReady() {
facemesh.predict(bild); // sobald das facemesh-Modell fertig geladen ist, beginne mit der Gesichtserkennung
}

In der draw-Funktion werden dann die einzelnen markanten Punkte des Gesichts (keypoints) 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
Bild hin- und hergeblendet werden kann
cursor(HAND); // Mauszeiger in eine Hand umwandeln
image(bild, 0, 0, bildweite, bildhoehe); // zeige das Bild in seiner Höhe und Weite
// an

tint(255, 255, 255, map(mouseY, 0, bildhoehe, 0, 255)); // Transparenz des
// Bilds mit der Y-Position der Maus verknüpfen

if (predictions.length > 0) { // Sobald ein Gesicht erkannt wird
drawKeypoints(); // einzelne Gesichtspunkte ausgeben
}
}

 

Am Ende der draw-Funktion werden in der Funktion drawKeypoints() die 468 Punkte (keypoints) ausgegeben, in die das Gesicht für die Erkennung aufgelöst wurde:

function drawKeypoints() {
for (let i = 0; i < predictions.length; i += 1) { // für jedes erkannte Gesicht // ...
keypoints = predictions[i].scaledMesh;
// schreibe die erkannten Punkte ins Array
// keypoints.

for (let j = 0; j < keypoints.length; j += 1) { // für jeden Keypoint ...
const [x, y] = keypoints[j]; // ermittele die X- und Y-Koordinate ...
fill(150, 150, 150, map(mouseY, 0, height, 255, 0)); // gebe den Koordinaten
// eine graue Farbe, deren Transparenz von der Mausposition abhängig ist und ...

ellipse(x, y, 5, 5); //text(j, x,y); // zeichne einen kleinen grauen Kreis an jeden
// erkannten Gesichtspunkt

}

Wenn aus der Vielzahl der Punkte einzelne Konturen zur Verdeutlichung von Augen, Mund, Nase etc. hervorgehoben werden sollen, kann man sich an den zuvor erstellten - für alle 468-Punkte-Face-Meshes gültigen - Arrays orientieren (s.o.), so dass man für jedes Sinnesorgan/für jede Gesichtskontur via beginShape() - vertex(x,y) - und endShape() eine Form erstellen und diese einfärben kann, z.B. für die Umrisse des rechten Auges:

noFill();
stroke(50,150,150, map(mouseX, 0, width, 255, 0)); // türkise Strichfarbe erstellen
strokeWeight(3); // Strichdicke auf 3 setzen
beginShape(); // Form beginnen
for(k=0; k<rechtes_auge_oben0.length; k +=1){ // für alle Elmente des Arrays
// rechtes_auge_oben0:

index=rechtes_auge_oben0[k]; // hole den Wert für den Punkt des inneren Umrisses für das
// rechte Auge oben und schreibe ihn in die Variable index

vertex(keypoints[index][0],keypoints[index][1]); // setze einen Punkt mit den X- und
// Y-Koordinanten dieses keypoints.

}
endShape(); // schließe die Form und verbinde die Punkte mit Strichen.
beginShape(); // Form beginnen
for(k=0; k<rechtes_auge_unten0.length; k += 1){ // für alle Elmente des Arrays
// rechtes_auge_oben0:

index=rechtes_auge_unten0[k]; / hole den Wert für den Punkt des inneren Umrisses für das
// rechte Auge oben und schreibe ihn in die Variable index
vertex(keypoints[index][0],keypoints[index][1]); // setze einen Punkt mit den X- und
// Y-Koordinanten dieses keypoints.

}
endShape();
// schließe die Form und verbinde die Punkte mit Strichen.

Will man die einzelnen Punkte (keypoints) zu Dreiecken verbinden (Triangulation), so bietet sich hier eine auf die 468-Punkte Meshes ausgelegte Javascript-Library (triangulation.js) an, in der alle Dreiecksverbindungen schon in einem Array (TRIANGULATION) antizipiert sind. Diese Library wird in den Header via

<script src="header/triangulation.js"></script>

eingebunden. Am Ende der draw-Funktion kann man dann eine Funktion zum Dreiecke-Zeichnen aufrufen (z.B. drawTriangles(predictions); ), in der die Punkte aller erkannten Gesichter (predictions) als Array übergeben werden. In dieser Funktion werden jeweils drei aufeinanderfolgende Punkte des jeweils erkannten Gesichts über ihren Index aufgerufen und via beginShape() - vertex(x,y) - und endShape() miteinander verbunden. Wenn man mit endShape(CLOSE); abschließt, kann man diese Dreiecke auch beliebig mit einer bzw. mehreren Farben füllen, also z.B.:

function drawTriangles(predictions){
stroke(150,150,150, map(mouseY, 0, bildhoehe, 0, 255)); // Strichfarbe
// hellgrau auswählen, wobei die Transparenz abhängig ist von der Y-Postiion der Maus.

if(predictions.length > 0) { // sobald mindestens ein Gesicht erkannt wurde
predictions.forEach(prediction => { // mache folgendes für jedes Gesicht
const keypoints = prediction.scaledMesh; // Hole das Array der erkannten Punkte
// (Keypoints) aus dem jeweiligen erkannten Gesicht

for(var i = 0; i < TRIANGULATION.length; i+=3) { // für jede Dreieckskombination
// im Array TRIANGUALTION aus der Javascript-Library triangulation.js hole drei aufeinanderfolgende
// keypoints:

var i1 = TRIANGULATION[i]; // Keypoint 1 mit x- und y-Koordinate [0] und [1]
var i2 = TRIANGULATION[i + 1]; // Keypoint 2 mit x- und y-Koordinate [0] und [1]
var i3 = TRIANGULATION[i + 2]; // Keypoint 3 mit x- und y-Koordinate [0] und [1]
fill(255-i/10,0,i/10, map(mouseY, 0, bildhoehe, 0, 255)); // optional: gebe
// jedem Dreieck eine beliebige Farbe

beginShape(); // beginne eine Form
vertex(keypoints[i1][0], keypoints[i1][1]); // erster Punkt des Dreiecks
vertex(keypoints[i2][0], keypoints[i2][1]); // zweiter Punkt des Dreiecks
vertex(keypoints[i3][0], keypoints[i3][1]); // dritter Punkt des Dreiecks
endShape(CLOSE); // schließe das Dreieck, so dass es mit einer Farbe gefüllt werden kann
}
});
}
}

 

 

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 src="header/triangulation.js"></script>

<script>
var gesicht = [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288, 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136, 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109, 10];
var rechtes_auge_oben0 = [246, 161, 160, 159, 158, 157, 173];
var rechtes_auge_unten0 = [33, 7, 163, 144, 145, 153, 154, 155, 133];
var rechtes_auge_oben1 = [247, 30, 29, 27, 28, 56, 190];
var rechtes_auge_unten1 = [130, 25, 110, 24, 23, 22, 26, 112, 243];
var rechtes_auge_oben2 = [113, 225, 224, 223, 222, 221, 189];
var rechtes_auge_unten2 = [226, 31, 228, 229, 230, 231, 232, 233, 244];
var rechtes_auge_unten3 = [143, 111, 117, 118, 119, 120, 121, 128, 245];
var rechte_augenbraue_oben = [156, 70, 63, 105, 66, 107, 55, 193];
var rechte_augenbraue_unten = [35, 124, 46, 53, 52, 65];
//var rechtes_auge_iris = [473, 474, 475, 476, 477];
var linkes_auge_oben0 = [466, 388, 387, 386, 385, 384, 398];
var linkes_auge_unten0 = [263, 249, 390, 373, 374, 380, 381, 382, 362];
var linkes_auge_oben1 = [467, 260, 259, 257, 258, 286, 414];
var linkes_auge_unten1 = [359, 255, 339, 254, 253, 252, 256, 341, 463];
var linkes_auge_oben2 = [342, 445, 444, 443, 442, 441, 413];
var linkes_auge_unten2 = [446, 261, 448, 449, 450, 451, 452, 453, 464];
var linkes_auge_unten3 = [372, 340, 346, 347, 348, 349, 350, 357, 465];
var linke_augenbraue_oben = [383, 300, 293, 334, 296, 336, 285, 417];
var linke_augenbraue_unten = [265, 353, 276, 283, 282, 295];
//var linkes_auge_iris = [468, 469, 470, 471, 472];
var nase = [8, 417, 465, 343, 437, 420, 279, 331, 294, 460, 326, 2, 97, 240, 64, 102, 49, 198, 217, 114, 245, 193, 8];
var oberlippe_aussen = [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291];
var unterlippe_aussen = [146, 91, 181, 84, 17, 314, 405, 321, 375, 291];
var oberlippe_innen = [78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308];
var unterlippe_innen = [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308];

// var zwischen_den_augen = 168;
// var nasenspitze = 1;
// var nase_unterseite = 2;
// var nase_rechte_ecke = 98;
// var nase_linkee_ecke = 327;
// var rechte_wange = 205;
// var linke_wange = 425;

var facemesh;
var predictions = [];
var bild;
var bildweite = 563;
var bildhoehe = 479;

function setup() {
var container = createCanvas(800,400);
container.parent('p5container');
bild = createImg("bilder/gesicht.png", imageReady);
bild.hide();
}

// when the image is ready, then load up poseNet
function imageReady() {
facemesh = ml5.facemesh(modelReady);
facemesh.on("predict", results => {
predictions = results;
});
}

// when poseNet is ready, do the detection
function modelReady() {
console.log("Model ready!");
facemesh.predict(bild);
}

function draw() {
background(255,255,255,125);
image(bild, 0, 0, bildweite, bildhoehe);
tint(255, 255, 255, map(mouseY, 0, bildhoehe, 0, 255));
if (predictions.length > 0) {
drawKeypoints();
drawTriangles(predictions);
}
}

function drawKeypoints() {
noStroke();
for (let i = 0; i < predictions.length; i += 1) {
keypoints = predictions[i].scaledMesh;

for (let j = 0; j < keypoints.length; j += 1) {
const [x, y] = keypoints[j];
fill(150, 150, 150, map(mouseY, 0, bildhoehe, 255, 0));
ellipse(x, y, 2, 2);
}
noFill();
stroke(50,150,150, map(mouseX, 0, bildweite, 255, 0));strokeWeight(3);
beginShape();for(k=0; k<rechtes_auge_oben0.length; k += 1){index=rechtes_auge_oben0[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<rechtes_auge_unten0.length; k += 1){index=rechtes_auge_unten0[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
stroke(50,150,150, map(mouseX, 0, bildweite, 255, 0));strokeWeight(2);
beginShape();for(k=0; k<rechtes_auge_oben1.length; k += 1){index=rechtes_auge_oben1[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<rechtes_auge_unten1.length; k += 1){index=rechtes_auge_unten1[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
stroke(50,150,150, map(mouseX, 0, bildweite, 255, 0));strokeWeight(1);
beginShape();for(k=0; k<rechtes_auge_oben2.length; k += 1){index=rechtes_auge_oben2[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<rechtes_auge_unten2.length; k += 1){index=rechtes_auge_unten2[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<rechtes_auge_unten3.length; k += 1){index=rechtes_auge_unten3[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
stroke(150,150,50, map(mouseX, 0, bildweite, 255, 0));strokeWeight(2);
beginShape();for(k=0; k<rechte_augenbraue_oben.length; k += 1){index=rechte_augenbraue_oben[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<rechte_augenbraue_unten.length; k += 1){index=rechte_augenbraue_unten[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();

stroke(50,150,150, map(mouseX, 0, bildweite, 255, 0));strokeWeight(2);
beginShape();for(k=0; k<linkes_auge_oben0.length; k += 1){index=linkes_auge_oben0[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<linkes_auge_unten0.length; k += 1){index=linkes_auge_unten0[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
stroke(50,150,150, map(mouseX, 0, bildweite, 255, 0));strokeWeight(2);
beginShape();for(k=0; k<linkes_auge_oben1.length; k += 1){index=linkes_auge_oben1[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<linkes_auge_unten1.length; k += 1){index=linkes_auge_unten1[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
stroke(50,150,150, map(mouseX, 0, bildweite, 255, 0));strokeWeight(1);
beginShape();for(k=0; k<linkes_auge_oben2.length; k += 1){index=linkes_auge_oben2[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<linkes_auge_unten2.length; k += 1){index=linkes_auge_unten2[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<linkes_auge_unten3.length; k += 1){index=linkes_auge_unten3[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
stroke(150,150,50, map(mouseX, 0, bildweite, 255, 0));strokeWeight(2);
beginShape();for(k=0; k<linke_augenbraue_oben.length; k += 1){index=linke_augenbraue_oben[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<linke_augenbraue_unten.length; k += 1){index=linke_augenbraue_unten[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();

stroke(150,50,150, map(mouseX, 0, bildweite, 255, 0));strokeWeight(2);
beginShape();for(k=0; k<nase.length; k += 1){index=nase[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();

stroke(150,50,50, map(mouseX, 0, bildweite, 255, 0));strokeWeight(2);
beginShape();for(k=0; k<oberlippe_aussen.length; k += 1){index=oberlippe_aussen[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<oberlippe_innen.length; k += 1){index=oberlippe_innen[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<unterlippe_aussen.length; k += 1){index=unterlippe_aussen[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
beginShape();for(k=0; k<unterlippe_innen.length; k += 1){index=unterlippe_innen[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();

stroke(50,50,50, map(mouseX, 0, bildweite, 255, 0));strokeWeight(1);
beginShape();for(k=0; k<gesicht.length; k += 1){index=gesicht[k]; vertex(keypoints[index][0],keypoints[index][1]);} endShape();
noStroke();
}
}

function drawTriangles(predictions){
stroke(150,150,150, map(mouseY, 0, bildhoehe, 0, 255));
if(predictions.length > 0) {
predictions.forEach(prediction => {
const keypoints = prediction.scaledMesh;
for(var i = 0; i < TRIANGULATION.length; i+=3) {
var i1 = TRIANGULATION[i];
var i2 = TRIANGULATION[i + 1];
var i3 = TRIANGULATION[i + 2];
fill(255-i/20,0,i/20, map(mouseY, 0, bildhoehe, 0, 255));
beginShape();
vertex(keypoints[i1][0], keypoints[i1][1]);
vertex(keypoints[i2][0], keypoints[i2][1]);
vertex(keypoints[i3][0], keypoints[i3][1]);
endShape(CLOSE);
}
});
}
}

</script>

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