P5 und ML5: Hand-Tracking in Bildern


Ü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 bild; // Variable für das zu analysierende Bild
var hands = []; // Array für die erfassten Handkonturen
var bildweite = 550; // Weite des zu untersuchenden Bilds
var bildhoehe = 384; // Höhe des zu untersuchenden Bilds

... zunächst das gewünschte Bild vorausgeladen ...

function preload(){
bild = loadImage("bilder/renaissance2.png"); // gewünschtes Bild vorausladen
}

... und in der setup-Funktion der Canvas hergestellt, in dem das Bild dargestellt wird. Gleichzeitig wird das Modell für die Handposen geladen und die Funktion modelReady() aufgerufen, sobald das Modell fertig geladen ist. Der Canvas sollte dabei mindestens so groß wie die Maße des Bilds sein.

function setup() {
var container = createCanvas(800,600); // Canvas erstellen
container.parent('p5container'); // an DIV-Container anhängen
handpose = ml5.handpose(modelReady); // Verbindung zu handPose herstellen, um
// Hände zu erkennen und das Ergebnis in der Variablen handpose zu speichern.

}

Sobald das handPose-Modell fertig geladen ist, wird es zur Erkennung der Hand eingesetzt und speichert die erkannten Punkte im Array hands.

function modelReady() {
handpose.on("predict", results => { // sobald eine Hand erkannt wird
hands = results; // fülle ein Array (results) mit einer zahlenmäßigen Beschreibung der erkannten Hand und weise es der Variablen hands zu
});
handpose.predict(bild); // sobald das handPose-Modell fertig geladen ist, beginne mit der Handposenerkennung
}

In der draw-Funktion werden dann die einzelnen markanten Punkte der Hand (keypoints) ausgegeben, die dann beliebig weiter verarbeitet werden können, z.B.:

function draw() {
background(255,255,255); // Hintergrund halb durchsichtig gestalten, damit mit der
// Maus zwischen Handkontur 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 (hands.length > 0) { // Sobald eine Hand erkannt wird
drawKeypoints(); // einzelne Punkte der Hand 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 bild;
var hands = [];
var bildweite = 550;
var bildhoehe = 384;

function preload(){
bild = loadImage("bilder/renaissance2.png");
}

function setup() {
var container = createCanvas(800,500);
container.parent('p5container');
handpose = ml5.handpose(modelReady);
}

function modelReady() {
console.log("Model ready!");
handpose.on("predict", results => {
hands = results;
});
handpose.predict(bild);
}

function draw() {
background(255,255,255);
cursor(HAND);
image(bild, 0, 0, bildweite, bildhoehe);
tint(255, 255, 255, map(mouseY, 0, bildhoehe, 0, 255));
if (hands.length > 0) {
drawKeypoints();
console.log("Keypoints zeichnen");
}
}

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

</script>

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