P5
und ML5: Face-Tracking
via Webcam (hochaufgelöst)
Ü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 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 Videodarstellung notwendigen Variablen ...
var facemesh; //
Array für das von faceMesh zurückgelieferte Ergebnis
var video; // Variable für das von
der Webcam erfasste Video
var predictions = []; // Array für
die erfassten Gesichtskonturen
var keypoints; // Variable für die
einzelnen Gesichtspunkte
... 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 von WebCam-Video und
Gesichtsserkennung erstellt wird als auch die Verbindung mit dem Modell
für die Gesichtsserkennungserkennung gestartet wird:
function setup() {
var container = createCanvas(640, 480); //
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
facemesh = ml5.facemesh(video, 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 mit einer zahlenmäßigen Beschreibung des
// erkannten Gesichts
});
video.hide(); // verstecke das Video (es
würde sonst neben dem Canvas zusätzlich zum
// verarbeiteten Video erscheinen)
}
Sobald die
Verbindung zu faceMesh hergestellt ist, können weitere Funktionen
gestartet werden
function modelReady()
{
console.log("Model ready!"); //
sende die Nachricht, dass das Modell geladen und bereit
// ist
}
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
tint(255, 255, 255, map(mouseY, 0, height, 0, 255), ); //
Transparenz des
// Videos mit der Y-Position der Maus verknüpfen
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.
An einzelne
Punkte des Gesichts lassen sich auch Bilder o.ä. anhängen (bzw.
diese auch beliebig anders weiter verarbeiten). Hier z.B. können
an das linke und rechte Auge jeweils ein Kullerauge als gif angehängt
werden. Dieses wird in einer preload-Funktion
als Bild vorausgeladen ...
function preload() {
auge=loadImage("bilder/auge.gif");
}
und an die
Koordinaten der entsprechenden keypoints
angehängt
image(auge, keypoints[441][0],
keypoints[441][1], 40, 40); // positioniere
das
// Kullerauge über das linke Auge
image(auge, keypoints[223][0]-15, keypoints[223][1], 40, 40); //
positioniere das
// Kullerauge über das rechte Auge
tint(255, map(mouseY, 0, height, 0, 255)); //
hänge die Transparenz der Kulleraugen an
// die Y-Maus-Koordinate
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 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 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];
// 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 video;
var predictions = [];
var keypoints;
function preload()
{
auge=loadImage("bilder/auge.gif");
}
function setup() {
var container = createCanvas(640, 480);
container.parent('p5container');
video = createCapture(VIDEO);
video.size(width, height);
facemesh = ml5.facemesh(video, modelReady);
facemesh.on("predict", results => {
predictions = results;
});
video.hide();
}
function modelReady()
{
console.log("Model ready!");
}
function draw() {
background(255,255,255,125);
cursor(HAND);
image(video, 0, 0, width, height);
tint(255, map(mouseX, 0, width, 0, 255));
drawKeypoints();
}
function drawKeypoints()
{
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, height, 255, 0));
ellipse(x, y, 5, 5); //text(j, x,y);
}
noFill();
stroke(50,150,150, map(mouseX, 0, width, 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, width, 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, width, 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, width, 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, width, 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, width, 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, width, 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, width, 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, width, 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, width, 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, width, 255, 0));strokeWeight(1);
beginShape();for(k=0; k<gesicht.length; k += 1){index=gesicht[k];
vertex(keypoints[index][0],keypoints[index][1]);} endShape();
image(auge, keypoints[441][0], keypoints[441][1], 40, 40);
image(auge, keypoints[223][0]-15, keypoints[223][1], 40, 40);
tint(255, 255, 255, map(mouseY, 0, height, 0, 255));
noStroke();
}
}
</script>
<DIV id="p5container"
style="width:800;border: 1px solid #333;box-shadow: 8px 8px 5px
#444;padding: 8px 12px;background-color:ffffff"></DIV>
|