本稿は「ml5-examples/p5js/PoseNet/PoseNet_image_single」で公開されているサンプルの解説です。
次のリンクをクリックすると、実際の動作が確認できます。「画像に対するPoseNetサンプル(1人の姿勢検出)」
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/p5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/addons/p5.dom.min.js"></script>
<script src="https://unpkg.com/ml5@0.1.2/dist/ml5.min.js" type="text/javascript"></script>
...
<h1>画像に対するPoseNetサンプル(1人の姿勢検出)</h1>
<p id='status'>モデルの読み込み中...</p>
<p>使用画像:<a href="https://www.pexels.com/photo/topless-man-wearing-grey-and-black-shorts-sprinting-on-concrete-road-1401796/" target="_blank">Pexels</a></p>
<script src="sketch.js"></script>
HTMLでは、p5.jsやml5.jsを読み込むだけで、DOM要素は実際には必要ありません。使用する<canvas>や<img>要素はJavaScriptコードから作成します。
sketch.js
let img;
let poseNet;
let poses = [];
function setup() {
console.log('セットアップ');
// ドキュメント内にcanvas要素を作成し、サイズをピクセル単位で設定する。
// https://p5js.org/reference/#/p5/createCanvas
createCanvas(640, 360);
// p5 domライブラリを使ってイメージを作成し、読み込まれたらmodelReady()を呼び出す。
// 与えられたsrcと代替テキストでDOMに<img>要素を作成して、コンテナノードが指定されている場合にはそれに追加し、
// 指定されていない場合には、bodyに追加する。
// createImg(src, successCallback)
// https://p5js.org/reference/#/p5/createImg
img = createImg('data/runner.jpg', imageReady);
// イメージのサイズをキャンバスのサイズに設定する。
// 要素の幅と高さを設定する。
// size(w, [h])
// https://p5js.org/reference/#/p5.Element/size
// width:描画するキャンバスの幅を保持するシステム変数。
// この値はcreateCanvas()関数の最初のパラメータによって設定される。
// heigthも同様。
// https://p5js.org/reference/#/p5/width
img.size(width, height);
img.hide(); // イメージを隠す
// 毎秒表示するフレーム数を指定する。
// https://p5js.org/reference/#/p5/frameRate
// フレームレートの設定はsetup()内が好ましい。
frameRate(1); // 高速で実行する必要はないので、フレームレートを1にする。
}
// イメージの準備ができたらposeNetを読み込む。
function imageReady() {
console.log('イメージの準備完了');
// オプションを設定する。
let options = {
imageScaleFactor: 1,
minConfidence: 0.1
}
// poseNetを割り当てる。
poseNet = ml5.poseNet(modelReady, options);
// poseイベントを監視するリスナーを設定する。
poseNet.on('pose', function(results) {
poses = results;
});
}
// poseNetの準備ができたら、検出を行う
function modelReady() {
console.log('モデルの読み込み完了');
select('#status').html('モデルが読み込まれた');
// モデルの準備ができたら、イメージの単一姿勢検出を実行する。
// poseNet.on('pose', ...)で検出結果を監視しているので、姿勢が検出されると、
// draw()ループでposesが存在する場合のみ、drawSkeleton()とdrawKeypoints()が実行される。
poseNet.singlePose(img)
}
// draw()は、posesが見つかるまで何も描画しない
// setup()の直後呼び出され、draw()関数は、プログラムが停止されるかnoLoop()が呼び出されるまで、
// そのブロックに含まれるコード行を連続して実行する。
// https://p5js.org/reference/#/p5/draw
function draw() {
if (poses.length > 0) {
for (let i = 0; i < poses.length; i++) {
// poseが持つ情報を出力
let pose = poses[i].pose;
console.log('全体の精度' + pose.score);
for (let j = 0; j < pose.keypoints.length; j++) {
let keypoint = pose.keypoints[j];
console.log('部位名:' + keypoint.part);
console.log('精度:' + keypoint.score);
console.log('x位置:' + keypoint.position.x);
console.log('y位置:' + keypoint.position.y);
console.log('-----------------------');
}
}
for (let i = 0; i < poses.length; i++) {
// skeletonが持つ情報を出力
let skeleton = poses[i].skeleton;
for (let j = 0; j < skeleton.length; j++) {
console.log(j + 1 + '回め');
console.log('部位名:' + skeleton[j][0].part);
console.log('精度' + skeleton[j][0].score);
console.log('x位置:' + skeleton[j][0].position.x);
console.log('y位置:' + skeleton[j][0].position.y);
console.log('-----------------------');
console.log('部位名:' + skeleton[j][1].part);
console.log('精度:' + skeleton[j][1].score);
console.log('x位置:' + skeleton[j][1].position.x);
console.log('y位置:' + skeleton[j][1].position.y);
console.log('-----------------------');
}
}
// イメージをp5.jsのキャンバスに描画する。<= createCanvas(640, 360)で作成したキャンバス
// image(img, x, y, [width], [height])
// https://p5js.org/reference/#/p5/image
image(img, 0, 0, width, height);
drawSkeleton();
drawKeypoints();
// p5.jsがdraw()内のコードの連続的な実行を行うのを停める
// https://p5js.org/reference/#/p5/noLoop
noLoop(); // posesの推定時はループを停める
}
}
// 以下はhttps://ml5js.org/docs/posenet-webcamからのコード
// 検出されたキーポイントの上に円を描く
function drawKeypoints() {
// 検出されたすべての姿勢を走査する
for (let i = 0; i < poses.length; i++) {
// 検出された各姿勢について、すべてのキーポイントを走査する。
let pose = poses[i].pose;
for (let j = 0; j < pose.keypoints.length; j++) {
// keypointは、部位を表すオブジェクト(rightArmやleftShoulderなど)
let keypoint = pose.keypoints[j];
// 姿勢の確率が0.2より大きいものだけ円を描く。
if (keypoint.score > 0.2) {
// シェイプの塗りに使用するカラーを設定する。
// https://p5js.org/reference/#/p5/fill
fill(255);
// 線とシェイプの枠線の描画に使用するカラーを設定する。
// https://p5js.org/reference/#/p5/stroke
stroke(20);
// 線や点、シェイプの枠線に使用する線幅を設定する。
// https://p5js.org/reference/#/p5/strokeWeight
strokeWeight(4);
// スクリーンに楕円を描画する。
// ellipse(x, y, w, [h])
// https://p5js.org/reference/#/p5/ellipse
// パラメータnに最も近い整数を計算する。
// round(n)
// https://p5js.org/reference/#/p5/round
ellipse(round(keypoint.position.x), round(keypoint.position.y), 8, 8);
}
}
}
}
// 骨格を描く
function drawSkeleton() {
// 検出されたすべての骨格(skeleton)を走査する。
for (let i = 0; i < poses.length; i++) {
let skeleton = poses[i].skeleton;
// すべてのskeletonに関し、部位の接続を走査する。
for (let j = 0; j < skeleton.length; j++) {
let partA = skeleton[j][0];
let partB = skeleton[j][1];
stroke(255);
strokeWeight(1);
// スクリーンに線(2点を結ぶ直線)を描画する。
// line(x1, y1, x2, y2)
// https://p5js.org/reference/#/p5/line
line(partA.position.x, partA.position.y, partB.position.x, partB.position.y);
}
}
}
このサンプルでは、dataフォルダにあるrunner.jpgという名前の画像に写っている人体の姿勢を検出したいので、まず検出対象の画像を読み込む<img>要素が必要になります。また検出結果を表示する、つまり人体の姿勢の上に部位の場所を示す円や接続を表す線を描画するので、<canvas>要素も必要です。
PoseNetを使用すると、たとえば左肩などの部位が、対象画像のどの位置にあるかをその画像の座標値で得ることができます。今の場合、<canvas>要素に対象画像を描画し、その上に部位の円や接続の線を描くので、<canvas>要素のサイズは読み込む画像のサイズに合わせ、<img>要素のサイズも画像のサイズに合わせておく方が、後で面倒な計算をしなくて済みます。
これらの作業を行っているのが、p5.jsで最初に自動的に呼び出されるsetup()関数です。画像の読み込みが終わると、createImg()に指定したimageReady()関数が呼び出されます。
imageReady()関数では、次の手順として、ml5.poseNet()メソッドを使ってposeNetオブジェクトを作成し、そのon()メソッドでイベントリスナーを設定しています。これにより、poseNetのモデルが姿勢を検出すると’pose’イベントが発生し、リスナー関数が呼び出され、変数posesにモデルからの検出結果が割り当てられるようになります。
ml5.poseNet()ではコールバック関数としてmodelReadyを指定しているので、姿勢検出モデルが準備できたときにmodelReady()関数が呼び出されます。ここではposeNet.singlePose()メソッドに<img>要素を渡して、単一姿勢の検出を実行しています。
その後、poseNet.singlePose()によって<img>要素の姿勢が検出されると、’pose’イベントが発生し、リスナー関数が呼び出され、変数posesが扱えるようになります。
ここまでをまとめると、下図のように示すことができます。
変数posesが手に入れば、後は欲しい情報を取り出して処理するだけです。posesは配列で、’pose’イベント発生時の姿勢情報を持ったオブジェクトが含まれています。オブジェクトは次の構造をしています。
上記コードではdraw()関数でposesを処理しています。draw()はsetup()の後に呼び出されるp5.jsの関数で、プログラムが停止されるかnoLoop()が呼び出されるまで、関数内のコードを連続して実行します。draw()内のコードは、poses.length > 0 の場合のみ実行され、最後。noLoop()が呼び出されているので、poses配列に姿勢情報が入ったときに1回だけ実行されることになります。
posesが持つ姿勢情報には次のようにアクセスできます。
function draw() {
if (poses.length > 0) {
for (let i = 0; i < poses.length; i++) {
// poseが持つ情報を出力
let pose = poses[i].pose;
console.log('全体の精度' + pose.score);
for (let j = 0; j < pose.keypoints.length; j++) {
let keypoint = pose.keypoints[j];
console.log('部位名:' + keypoint.part);
console.log('精度:' + keypoint.score);
console.log('x位置:' + keypoint.position.x);
console.log('y位置:' + keypoint.position.y);
console.log('-----------------------');
}
}
以下はその出力結果です。
noseやleftEye、rightEyeというのは人体の部位名で、下図に示すように全部で17個あります。
poses配列の姿勢情報は、含まれるオブジェクトのskeletonプロパティからも得ることができます。
for (let i = 0; i < poses.length; i++) {
// skeletonが持つ情報を出力
let skeleton = poses[i].skeleton;
for (let j = 0; j < skeleton.length; j++) {
console.log(j + 1 + '回め');
console.log('部位名:' + skeleton[j][0].part);
console.log('精度' + skeleton[j][0].score);
console.log('x位置:' + skeleton[j][0].position.x);
console.log('y位置:' + skeleton[j][0].position.y);
console.log('-----------------------');
console.log('部位名:' + skeleton[j][1].part);
console.log('精度:' + skeleton[j][1].score);
console.log('x位置:' + skeleton[j][1].position.x);
console.log('y位置:' + skeleton[j][1].position.y);
console.log('-----------------------');
}
}
skeletonでは、接続させたい部位が2つずつ連続しているので、部位と部位を線で結びたい場合に便利です。
function drawSkeleton() {
// 検出されたすべての骨格(skeleton)を走査する。
for (let i = 0; i < poses.length; i++) {
let skeleton = poses[i].skeleton;
// すべてのskeletonに関し、部位の接続を走査する。
for (let j = 0; j < skeleton.length; j++) {
let partA = skeleton[j][0];
let partB = skeleton[j][1];
stroke(255);
strokeWeight(1);
// スクリーンに線(2点を結ぶ直線)を描画する。
// line(x1, y1, x2, y2)
// https://p5js.org/reference/#/p5/line
line(partA.position.x, partA.position.y, partB.position.x, partB.position.y);
}
}
}