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