P5
und ML5: Pose-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="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>
Im Script
wird nach der Deklarierung der für die Verbindung mit dem Posen-/Bewegungsmodell
von poseNet und die Videodarstellung notwendigen Variablen ...
var poseNet; //
Array für das von poseNet zurückgelieferte Ergebnis
var poses = []; // Array für die erfassten
Posen = Personen
var bild; // Variable für das zu untersuchende
Bild
var bildweite =1232; // Weite des zu untersuchenden
Bilds
var bildhoehe =720; // Höhe des zu
untersuchenden Bilds
... über
die preload-Funktion das
zu untersuchende Bild vorausgeladen:
function preload(){
bild = loadImage("bilder/renaissance.png"); //
Das zu untersuchende Bild
// vorausladen
}
Danach wird
die
setup-Funktion gestartet,
in der sowohl der Canvas zur Darstellung des Bildes und der Posenerkennung
erstellt wird als auch die Verbindung mit dem Modell für die Posen/Bewegungserkennung
hergestellt wird:
function setup() {
var container = createCanvas(800, 500); //
Canvas erstellen
container.parent('p5container'); // an
DIV-Container anhängen
poseNet = ml5.poseNet(bild, {imageScaleFactor: 1, minConfidence: 0.1,
multiplier: 1.0, quantBytes:4, maxPoseDetections:3}, modelReady);
// Verbindung zu poseNet starten, um Pose/Bewegung zu erkennen und das
Ergebnis in der Variablen
// poseNet zu speichern.
}
In der
ml5.poseNet()-Funktion können in den geschweiften Klammern
{...} folgende Optionen für die Genauigkeit des Pose/Motion-Trackings
eingestellt werden:
- imageScaleFactor:
= um welchen Faktor
das Bild vergrößert oder verkleinert werden soll.
- outputStride:
= wie kleinschrittig
die Ausgabe des Ergebnisses sein soll. Je kleiner der Wert, desto genauer
die Abbildung, aber desto langsamer die Ausgabe
(kann 8, 16 oder 32 sein).
- flipHorizontal:
= horizontale Spiegelung
der Ausgabe (kann true
oder false sein)
- maxPoseDetections:
= maximale Anzahl
der entdeckten Personen/Posen (default bei 5)
- scoreThreshold:
= Schwelle, ab welcher
Entdeckungswahrscheinlichkeit zwischen 0 und 1 ein Körperteil berücksichtigt
werden soll (default bei 0,5).
- nmsRadius:
= bei mehreren Posen,
wie groß der Radius sein soll, ab dem sich zwei Posen gegenseitig
verdecken (default ist bei 20).
- detectionType:
= ob einzelne ('single')
oder mehrere Personen erkannt werden sollen ('multiple')(für
die Entdeckung von Einzelposen empfiehlt es sich das 'single' außerhalb
der Optionen in die PoseNet-Funktion zu schreiben, also z.B.:
poseNet = ml5.poseNet(video,
{scoreThreshold: 0.2, multiplier:0.75}, 'single', modelReady);)
- inputResolution:
= Eingabe-Auflösung
auf die das Bild reduziert wird, bevor es ins poseNet-Modell gespeist
wird. Je größer die Auflösung, desto genauer die Erkennung,
desto langsamer die Ausgabe (kann 161, 193, 257, 289, 321, 353, 385,
417, 449, 481, 513 und 801 sein, default ist bei 257).
- multiplier:
= Tiefe der Entdeckung,
je höher die Zahl, desto genauer die Abbildung, desto langsamer
die Ausgabegeschwindigkeit (kann 1.01, 1.0, 0.75 oder 0.5 sein).
- quantBytes:
= Quantisierung des
Bilds, je höher die Auflösung desto genauer die Abbildung,
desto langsamer die Ausgabegeschwindigkeit (kann 1 oder 4 sein).
Sobald das
Modell geladen ist, kann es in der Funktion modelReady()
gestartet werden:
function modelReady()
{
console.log("Model ready!"); //
Rückmeldung, dass das PoseNet-Model geladen und bereit
// ist.
poseNet.on('pose', function(results) {
//sobald eine Pose entdeckt wird ...
poses = results; // fülle ein Array
mit einer zahlenmäßigen Beschreibung dieser Pose
});
poseNet.multiPose(bild); // tracke viele
Personen in einem Bild. Wenn nur eine Person im
// Bild getrackt werden soll, muss es heissen: poseNet.singlePose(bild);
}
In der draw-Funktion
von P5 werden dann neben der Darstellung des Bilds nur noch zwei weitere
Funktionen aufgerufen: drawKeypoints(),
um markante Punkte = Körperteile der erfassten Personen wiederzugeben,
und drawSkeleton(), um
diese Punkte über Linien zu einem Skelett bzw. einer Strichfigur
zu verbinden:
function draw() {
background(255,255,255,125); // Hintergrund
halb durchsichtig gestalten, damit mit der
// Maus zwischen Drahtgitterfigur 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 nach den oben definierten Variablen an
tint(255, 255, 255, map(mouseY, 0, video_hoehe, 0, 255), );
// Transparenz
// des Bildes mit der Y-Position der Maus verknüpfen
drawKeypoints(); // rufe eine Funktion
auf, in der das von PoseNet zurückgelieferte Array
// als Punktfigur ausgegeben wird
drawSkeleton(); // rufe eine Funktion auf,
in der die markanten Punkte der Punktfigur zu
// einem Skelett/einer Strichfigur verbunden werden
}
In beiden
Funktionen wird zuerst geschaut wie viele Posen d.h. Personen pro Frame
erkannt wurden und pro Person wird dann geschaut, welche keypoints
d.h. Körperteile dazu entdeckt wurden und wie sie sich zu einer Strichfigur
verbinden lassen:
function drawKeypoints()
{ // Zeichne die markanten Punkte einer/mehrerer
Personen/Posen
for (let i = 0; i < poses.length; i++) { //zähle
das zurückgegebene Array durch,
// wie viele Posen = Personen pro Frame gefunden wurden
var pose = poses[i].pose; // übergebe
der Reihe nach jede gefundene Pose = Person an die
// Variable pose (= wiederum ein Array mit einzelnen Keypoints = Körperteilen)
for (let j = 0; j < pose.keypoints.length; j++) { //
zähle die zurückgegebenen
// Keypoints = Körperteile pro Person durch ...
let keypoint = pose.keypoints[j]; //...
und weise sie der Variablen keypoint zu.
if (keypoint.score > 0.2) { //... wenn
die Wahrscheinlichkeit für ein gefundenes
// Körperteil größer als 0,2 ist ...
noStroke(); fill(184, 0, 0); //... setze
die Füllfarbe auf dunkelrot und ...
ellipse(keypoint.position.x, keypoint.position.y, 10, 10); //
... gebe den
// Punkt als kleinen Kreis aus ...
text(round(keypoint.position.x, 2), keypoint.position.x, keypoint.position.y);
//.. und mit ihm seine X- ...
text(round(keypoint.position.y, 2), keypoint.position.x, keypoint.position.y+10);
// ... und Y-Koordinaten.
}
}
}
function drawSkeleton()
{ // Zeichne Strichfiguren auf Grundlage
der markanten Punkte
// einer/mehrerer Personen/Posen
for (var i = 0; i < poses.length; i++) { //zähle
das zurückgegebene Array durch,
// wie viele Posen = Personen pro Frame gefunden wurden
var skeleton = poses[i].skeleton; // übergebe
der Reihe nach jede gefundene Pose =
// Person an die Variable skeleton (= wiederum ein Array mit einzelnen
Linien = Bestandteilen des
// Skeletts)
for (var j = 0; j < skeleton.length; j++) { //
zähle die zurückgegebenen Linien =
// Skelettbestandteile pro Person durch ...
var partA = skeleton[j][0]; // weise den
Anfangspunkt einer Skelettlinie mit zwei
// Koordinaten der Variablen partA zu ...
var partB = skeleton[j][1]; // und den
Endpunkt der jeweiligen Skelettlinie mit zwei
// Koordinaten der Variablen partB.
stroke(184, 0, 0); // setze die Strichfarbe
auf dunkelrot
line(partA.position.x, partA.position.y, partB.position.x, partB.position.y);
// zeichne die Linie aus den Koordinaten
von partA und partB
}
}
}
Die Art der
Körperteile wird in der Reihenfolge der keypoints
codiert, so kann man über folgende Variablen auf die jeweiligen Körperteile
zugreifen:
- pose.keypoints[0]
= Nase
- pose.keypoints[1]
= linkes Auge
- pose.keypoints[2]
= rechtes Auge
- pose.keypoints[3]
= linkes Ohr
- pose.keypoints[4]
= rechtes Ohr
- pose.keypoints[5]
= linke Schulter
- pose.keypoints[6]
= rechte Schulter
- pose.keypoints[7]
= linker Ellenbogen
- pose.keypoints[8]
= rechter Ellenbogen
- pose.keypoints[9]
= linkes Handgelenk
- pose.keypoints[10]
= rechtes Handgelenk
- pose.keypoints[11]
= linke Hüfte
- pose.keypoints[12]
= rechte Hüfte
- pose.keypoints[13]
= linkes Knie
- pose.keypoints[14]
= rechtes Knie
- pose.keypoints[15]
= linkes Fußgelenk
- pose.keypoints[16]
= rechtes Fußgelenk
Zu jedem
Punkt wird neben seiner X- und Y-Koordinate auch ein score
mitgegegeben, bei dem sich zwischen 0 und 1 erkennen lässt, wie hoch
die Wahrscheinlichkeit ist, dass es sich tatsächlich um das entsprechende
Körperteil handelt.
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 poseNet;
var poses = [];
var bild;
var bildweite = 391;
var bildhoehe = 463;
function preload(){
bild = loadImage("bilder/renaissance.png");
}
function setup() {
var container = createCanvas(800, 500);
container.parent('p5container');
poseNet = ml5.poseNet(bild, {imageScaleFactor: 1, minConfidence: 0.1,
multiplier: 1.0, quantBytes:4, maxPoseDetections:3}, modelReady);
}
function modelReady()
{
console.log("Model ready!");
poseNet.on('pose', function(results) {
poses = results;
});
poseNet.multiPose(bild);
}
function draw() {
background(255,255,255,125);
image(bild, 0, 0, bildweite, bildhoehe);
tint(255, 255, 255, map(mouseY, 0, bildhoehe, 0, 255), );
drawKeypoints();
drawSkeleton();
}
function drawKeypoints()
{
for (let i = 0; i < poses.length; i++) {
var pose = poses[i].pose;
for (let j = 0; j < pose.keypoints.length; j++) {
let keypoint = pose.keypoints[j];
if (keypoint.score > 0.2) {
noStroke(); fill(184, 0, 0);
ellipse(keypoint.position.x, keypoint.position.y, 10, 10);
text(round(keypoint.position.x, 2), keypoint.position.x, keypoint.position.y);
text(round(keypoint.position.y, 2), keypoint.position.x, keypoint.position.y+10);
}
}
//Bedeutung der Keypoints:
if (pose.keypoints[0].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Nase: ', 10, 20);
text('X: '+ round(pose.keypoints[0].position.x), 120, 20);
text('Y: '+ round(pose.keypoints[0].position.y), 170, 20);
text('Score: '+ round(pose.keypoints[0].score, 2), 220, 20);
if (pose.keypoints[1].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Auge links: ', 10, 30);
text('X: '+ round(pose.keypoints[1].position.x), 120, 30);
text('Y: '+ round(pose.keypoints[1].position.y), 170, 30);
text('Score: '+ round(pose.keypoints[1].score, 2), 220, 30);
if (pose.keypoints[2].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Auge rechts: ', 10, 40);
text('X: '+ round(pose.keypoints[2].position.x), 120, 40);
text('Y: '+ round(pose.keypoints[2].position.y), 170, 40);
text('Score: '+ round(pose.keypoints[2].score, 2), 220, 40);
if (pose.keypoints[3].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Ohr links: ', 10, 50);
text('X: '+ round(pose.keypoints[3].position.x), 120, 50);
text('Y: '+ round(pose.keypoints[3].position.y), 170, 50);
text('Score: '+ round(pose.keypoints[3].score, 2), 220, 50);
if (pose.keypoints[4].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Ohr rechts: ', 10, 60);
text('X: '+ round(pose.keypoints[4].position.x), 120, 60);
text('Y: '+ round(pose.keypoints[4].position.y), 170, 60);
text('Score: '+ round(pose.keypoints[4].score, 2), 220, 60);
if (pose.keypoints[5].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Schulter links: ', 10, 70);
text('X: '+ round(pose.keypoints[5].position.x), 120, 70);
text('Y: '+ round(pose.keypoints[5].position.y), 170, 70);
text('Score: '+ round(pose.keypoints[5].score, 2), 220, 70);
if (pose.keypoints[6].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Schulter rechts: ', 10, 80);
text('X: '+ round(pose.keypoints[6].position.x), 120, 80);
text('Y: '+ round(pose.keypoints[6].position.y), 170, 80);
text('Score: '+ round(pose.keypoints[6].score, 2), 220, 80);
if (pose.keypoints[7].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Ellenbogen links: ', 10, 90);
text('X: '+ round(pose.keypoints[7].position.x), 120, 90);
text('Y: '+ round(pose.keypoints[7].position.y), 170, 90);
text('Score: '+ round(pose.keypoints[7].score, 2), 220, 90);
if (pose.keypoints[8].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Ellenbogen rechts: ', 10, 100);
text('X: '+ round(pose.keypoints[8].position.x), 120, 100);
text('Y: '+ round(pose.keypoints[8].position.y), 170, 100);
text('Score: '+ round(pose.keypoints[8].score, 2), 220, 100);
if (pose.keypoints[9].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Handgelenk links: ', 10, 110);
text('X: '+ round(pose.keypoints[9].position.x), 120, 110);
text('Y: '+ round(pose.keypoints[9].position.y), 170, 110);
text('Score: '+ round(pose.keypoints[9].score, 2), 220, 110);
if (pose.keypoints[10].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Handgelenk rechts: ', 10, 120);
text('X: '+ round(pose.keypoints[10].position.x), 120, 120);
text('Y: '+ round(pose.keypoints[10].position.y), 170, 120);
text('Score: '+ round(pose.keypoints[10].score, 2), 220, 120);
if (pose.keypoints[11].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Huefte links: ', 10, 130);
text('X: '+ round(pose.keypoints[11].position.x), 120, 130);
text('Y: '+ round(pose.keypoints[11].position.y), 170, 130);
text('Score: '+ round(pose.keypoints[11].score, 2), 220, 130);
if (pose.keypoints[12].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Huefte rechts: ', 10, 140);
text('X: '+ round(pose.keypoints[12].position.x), 120, 140);
text('Y: '+ round(pose.keypoints[12].position.y), 170, 140);
text('Score: '+ round(pose.keypoints[12].score, 2), 220, 140);
if (pose.keypoints[13].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Knie links: ', 10, 150);
text('X: '+ round(pose.keypoints[13].position.x), 120, 150);
text('Y: '+ round(pose.keypoints[13].position.y), 170, 150);
text('Score: '+ round(pose.keypoints[13].score, 2), 220, 150);
if (pose.keypoints[14].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Knie rechts: ', 10, 160);
text('X: '+ round(pose.keypoints[14].position.x), 120, 160);
text('Y: '+ round(pose.keypoints[14].position.y), 170, 160);
text('Score: '+ round(pose.keypoints[14].score, 2), 220, 160);
if (pose.keypoints[15].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Fussgelenk links: ', 10, 170);
text('X: '+ round(pose.keypoints[15].position.x), 120, 170);
text('Y: '+ round(pose.keypoints[15].position.y), 170, 170);
text('Score: '+ round(pose.keypoints[15].score, 2), 220, 170);
if (pose.keypoints[16].score > 0.2){fill(184, 0, 0);} else {fill(184,
184, 184);}
text('Fussgelenk rechts: ', 10, 180);
text('X: '+ round(pose.keypoints[16].position.x), 120, 180);
text('Y: '+ round(pose.keypoints[16].position.y), 170, 180);
text('Score: '+ round(pose.keypoints[16].score, 2), 220, 180);
}
text('Anzahl der gefundenen Personen: '+ poses.length, 10, 200);
}
function drawSkeleton()
{
for (var i = 0; i < poses.length; i++) {
var skeleton = poses[i].skeleton;
for (var j = 0; j < skeleton.length; j++) {
var partA = skeleton[j][0];
var partB = skeleton[j][1];
stroke(184, 0, 0);
line(partA.position.x, partA.position.y, partB.position.x, partB.position.y);
}
}
}
</script>
<DIV id="p5container"
style="width:1024;border: 1px solid #333;box-shadow: 8px 8px 5px
#444;padding: 8px 12px;background-color:ffffff"></DIV>
|