P5 und ML5: Hand-Tracking in Videos


Ü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 Handerkennung via handPose und für die Bilddarstellung notwendigen Variablen ...

var handpose; // Array für das von handPose zurückgelieferte Ergebnis
var video; // Variable für das zu analysierende Video
var videoIsPlaying = false; // Variable für den Zustand des Videos
var hands = []; // Array für die erfassten Handkonturen
var bildweite = 800; // Weite des zu untersuchenden Bilds
var bildhoehe = 480; // Höhe des zu untersuchenden Bilds

... die setup-Funktion gestartet, in der sowohl der Canvas zur Darstellung von Video und Handerkennung erstellt wird als auch die Verbindung mit dem Modell für die Handerkennung gestartet wird Der Canvas sollte dabei mindestens so groß wie die Maße des Videos sein:

function setup() {
var container = createCanvas(800,500); // Canvas erstellen
container.parent('p5container'); // an DIV-Container anhängen
container.mouseClicked(togglePlay); // auf Klick auf Canvas reagieren
video = createVideo("bilder/bertsch.mp4"); // Video laden
video.size(bildweite, bildhoehe); // Videogröße einstellen
handpose = ml5.handpose(video, modelReady); // Verbindung zu handPose
// herstellen, um Hände zu erkennen und das Ergebnis in der Variablen handpose zu speichern.

handpose.on("hand", results => { // sobald eine Hand erkannt wird
hands = results; // fülle ein Array mit einer zahlenmäßigen Beschreibung der
// erkannten Hand

});
video.hide(); // verstecke das Video (es würde sonst neben dem Canvas zusätzlich zum
// verarbeiteten Video erscheinen)

}

Sobald die Verbindung zu handPose 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 der Hand 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 Handkontur und Video hin- und hergeblendet werden kann
cursor(HAND); // Mauszeiger in eine Hand umwandeln
image(video, 0, 0, bildweite, bildhoehe);// zeige das Video in seiner Höhe und
// Weite an

tint(255, 255, 255, map(mouseY, 0, height, 0, 255), ); // Transparenz des
// Videos mit der Y-Position der Maus verknüpfen
drawKeypoints(); // einzelne Handpunkte ausgeben
}

Am Ende der draw-Funktion werden in der Funktion drawKeypoints() die 21 Punkte (landmarks) ausgegeben, in die die Hand für die Erkennung der Posen aufgelöst wird (ML5 kann bis jetzt leider nur eine Hand erkennen), z.B.:

function drawKeypoints() {
for (var i = 0; i < hands.length; i += 1) { // sobald eine Hand erkannt wird...
handgelenk = hands[i].landmarks[0]; // weise den nullten Landmark dem Handgelenk zu

daumen1 = hands[i].landmarks[1]; // weise die Landmarks 1-4 den Gliedern des Daumens
// zu...

daumen2 = hands[i].landmarks[2]; //...
daumen3 = hands[i].landmarks[3]; //...
daumen4 = hands[i].landmarks[4]; //...
noStroke();fill(150, 50, 50); //... wähle dunkelrot und ...
ellipse(daumen4[0], daumen4[1],15,15); //... markiere die Daumenspitze mit einem
// roten Kreis

zeigefinger1 = hands[i].landmarks[5]; // weise die Landmarks 5-8 den Gliedern des
// Zeigefingers zu...

zeigefinger2 = hands[i].landmarks[6]; //...
zeigefinger3 = hands[i].landmarks[7]; //...
zeigefinger4 = hands[i].landmarks[8]; //...
noStroke();fill(150, 150, 50); //... wähle grüngelb und ...
ellipse(zeigefinger4[0], zeigefinger4[1],15,15); //... markiere die
// Zeigefingerspitze mit einem grüngelben Kreis

mittelfinger1 = hands[i].landmarks[9]; // weise die Landmarks 9-12 den Gliedern des
// Mittelfingers zu...

mittelfinger2 = hands[i].landmarks[10]; //...
mittelfinger3 = hands[i].landmarks[11]; //...
mittelfinger4 = hands[i].landmarks[12]; //...
noStroke();fill(50, 150, 50); //... wähle grün und ...
ellipse(mittelfinger4[0], mittelfinger4[1],15,15); //... markiere die
// Mittelfingerspitze mit einem grünen Kreis


ringfinger1 = hands[i].landmarks[13]; // weise die Landmarks 13-16 den Gliedern des
// Mittelfingers zu...

ringfinger2 = hands[i].landmarks[14]; //...
ringfinger3 = hands[i].landmarks[15]; //...
ringfinger4 = hands[i].landmarks[16]; //...
noStroke();fill(50, 150, 150); //... wähle türkis und ...
ellipse(ringfinger4[0], ringfinger4[1],15,15); //... markiere die
// Mittelfingerspitze mit einem türkisen Kreis

kleinerfinger1 = hands[i].landmarks[17]; // weise die Landmarks 17-20 den Gliedern
// des kleinen Fingers zu...

kleinerfinger2 = hands[i].landmarks[18]; //...
kleinerfinger3 = hands[i].landmarks[19]; //...
kleinerfinger4 = hands[i].landmarks[20]; //...
noStroke();fill(50, 50, 150); //... wähle dunkelblau und ...
ellipse(kleinerfinger4[0], kleinerfinger4[1],15,15); //... markiere die
// Spitze des kleinen Fingers mit einem blauen Kreis

noStroke();fill(150, 150, 150); //... wähle grau und ...
ellipse(handgelenk[0], handgelenk[1],15,15); //... markiere das Handgelenk mit
// einem grauen Kreis

Wenn die Punkte zur Verdeutlichung der Hand für eine Handkontur miteinander verbunden werden sollen, kann man für jeden Finger via beginShape() - vertex(x,y) - und endShape() eine Form erstellen und diese entsprechend einfärben, z.B.:

 

noFill(); strokeWeight(2); // wähle als Strichstärke 2 Pixel und ...
stroke(150, 50, 50); //... wähle dunkelrot als Strichfarbe und ...
beginShape(); // ... verbinde die Daumenglieder miteinander
vertex(handgelenk[0], handgelenk[1]);
vertex(daumen1[0], daumen1[1]);
vertex(daumen2[0], daumen2[1]);
vertex(daumen3[0], daumen3[1]);
vertex(daumen4[0], daumen4[1]);
endShape(); // schließe die Form und verbinde die Punkte mit Strichen.

stroke(150, 150, 50); //... wähle gelbgrün als Strichfarbe und ...
beginShape(); // ... verbinde die Zeigefingerglieder miteinander
vertex(handgelenk[0], handgelenk[1]);
vertex(zeigefinger1[0], zeigefinger1[1]);
vertex(zeigefinger2[0], zeigefinger2[1]);
vertex(zeigefinger3[0], zeigefinger3[1]);
vertex(zeigefinger4[0], zeigefinger4[1]);
endShape(); // schließe die Form und verbinde die Punkte mit Strichen.

stroke(50, 150, 50); //... wähle grün als Strichfarbe und ...
beginShape(); // ... verbinde die Mittelfingerglieder miteinander
vertex(handgelenk[0], handgelenk[1]);
vertex(mittelfinger1[0], mittelfinger1[1]);
vertex(mittelfinger2[0], mittelfinger2[1]);
vertex(mittelfinger3[0], mittelfinger3[1]);
vertex(mittelfinger4[0], mittelfinger4[1]);
endShape(); // schließe die Form und verbinde die Punkte mit Strichen.

stroke(50, 150, 150); //... wähle türkis als Strichfarbe und ...
beginShape(); // ... verbinde die Ringfingerglieder miteinander
vertex(handgelenk[0], handgelenk[1]);
vertex(ringfinger1[0], ringfinger1[1]);
vertex(ringfinger2[0], ringfinger2[1]);
vertex(ringfinger3[0], ringfinger3[1]);
vertex(ringfinger4[0], ringfinger4[1]);
endShape(); // schließe die Form und verbinde die Punkte mit Strichen.

stroke(50, 50, 150); //... wähle dunkelblau als Strichfarbe und ...
beginShape(); // ... verbinde die Glieder des kleinen Fingers miteinander
vertex(handgelenk[0], handgelenk[1]);
vertex(kleinerfinger1[0], kleinerfinger1[1]);
vertex(kleinerfinger2[0], kleinerfinger2[1]);
vertex(kleinerfinger3[0], kleinerfinger3[1]);
vertex(kleinerfinger4[0], kleinerfinger4[1]);
endShape(); // schließe die Form und verbinde die Punkte mit Strichen.

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 handpose;
var video;
var videoIsPlaying = false;
var hands = [];
var bildweite = 800;
var bildhoehe = 480;


function setup() {
var container = createCanvas(800,500);
container.parent('p5container');
container.mouseClicked(togglePlay);
video = createVideo("bilder/bertsch.mp4");
video.size(bildweite, bildhoehe);
handpose = ml5.handpose(video, modelReady);
handpose.on("hand", results => {
hands = results;
});
video.hide();
}

function modelReady() {
console.log("Model ready!");
}

function draw() {
background(255,255,255);
cursor(HAND);
image(video, 0, 0, bildweite, bildhoehe);
tint(255, 255, 255, map(mouseY, 0, bildhoehe, 0, 255));
if (hands.length > 0) {
drawKeypoints();
}
}


function drawKeypoints() {
console.log("Keypoints zeichnen");
for (var i = 0; i < hands.length; i += 1) {
handgelenk = hands[i].landmarks[0];

daumen1 = hands[i].landmarks[1];
daumen2 = hands[i].landmarks[2];
daumen3 = hands[i].landmarks[3];
daumen4 = hands[i].landmarks[4];noStroke();fill(150, 50, 50); ellipse(daumen4[0], daumen4[1],15,15);

zeigefinger1 = hands[i].landmarks[5];
zeigefinger2 = hands[i].landmarks[6];
zeigefinger3 = hands[i].landmarks[7];
zeigefinger4 = hands[i].landmarks[8];noStroke();fill(150, 150, 50); ellipse(zeigefinger4[0], zeigefinger4[1],15,15);

mittelfinger1 = hands[i].landmarks[9];
mittelfinger2 = hands[i].landmarks[10];
mittelfinger3 = hands[i].landmarks[11];
mittelfinger4 = hands[i].landmarks[12];noStroke();fill(50, 150, 50); ellipse(mittelfinger4[0], mittelfinger4[1],15,15);

ringfinger1 = hands[i].landmarks[13];
ringfinger2 = hands[i].landmarks[14];
ringfinger3 = hands[i].landmarks[15];
ringfinger4 = hands[i].landmarks[16];noStroke();fill(50, 150, 150); ellipse(ringfinger4[0], ringfinger4[1],15,15);

kleinerfinger1 = hands[i].landmarks[17];
kleinerfinger2 = hands[i].landmarks[18];
kleinerfinger3 = hands[i].landmarks[19];
kleinerfinger4 = hands[i].landmarks[20];noStroke();fill(50, 50, 150); ellipse(kleinerfinger4[0], kleinerfinger4[1],15,15);

noFill();fill(150, 150, 150); ellipse(handgelenk[0], handgelenk[1],15,15);

noFill();
strokeWeight(2);
stroke(150, 50, 50);
beginShape();
vertex(handgelenk[0], handgelenk[1]);
vertex(daumen1[0], daumen1[1]);
vertex(daumen2[0], daumen2[1]);
vertex(daumen3[0], daumen3[1]);
vertex(daumen4[0], daumen4[1]);
endShape();

stroke(150, 150, 50);
beginShape();
vertex(handgelenk[0], handgelenk[1]);
vertex(zeigefinger1[0], zeigefinger1[1]);
vertex(zeigefinger2[0], zeigefinger2[1]);
vertex(zeigefinger3[0], zeigefinger3[1]);
vertex(zeigefinger4[0], zeigefinger4[1]);
endShape();

stroke(50, 150, 50);
beginShape();
vertex(handgelenk[0], handgelenk[1]);
vertex(mittelfinger1[0], mittelfinger1[1]);
vertex(mittelfinger2[0], mittelfinger2[1]);
vertex(mittelfinger3[0], mittelfinger3[1]);
vertex(mittelfinger4[0], mittelfinger4[1]);
endShape();

stroke(50, 150, 150);
beginShape();
vertex(handgelenk[0], handgelenk[1]);
vertex(ringfinger1[0], ringfinger1[1]);
vertex(ringfinger2[0], ringfinger2[1]);
vertex(ringfinger3[0], ringfinger3[1]);
vertex(ringfinger4[0], ringfinger4[1]);
endShape();

stroke(50, 50, 150);
beginShape();
vertex(handgelenk[0], handgelenk[1]);
vertex(kleinerfinger1[0], kleinerfinger1[1]);
vertex(kleinerfinger2[0], kleinerfinger2[1]);
vertex(kleinerfinger3[0], kleinerfinger3[1]);
vertex(kleinerfinger4[0], kleinerfinger4[1]);
endShape();
}
}

function togglePlay(){
if (videoIsPlaying==true) {
video.pause(); videoIsPlaying = false;
} else {
video.loop(); videoIsPlaying = true;
}
}

</script>

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