LoginSignup
19
14

More than 1 year has passed since last update.

【Processing 2021】 #p5js で #MediaPipe (JavaScript版)を使った高精度な認識を利用する

Last updated at Posted at 2021-12-15

この記事は、2021年の Processing のアドベントカレンダー(@Adventar) の 15日目の記事です。

あと 2021年のアドベントカレンダーについて、Qiita の p5.js のアドベントカレンダー もあったので、そちらは以下の別記事を登録しているので、よろしければこちらもご覧いただけると幸いです。

この記事の内容

内容は、p5.js(Processing を JavaScript で扱えるライブラリというような感じのもの)と、Googleさんが提供している MediaPipe(その中の JavaScript版)を組み合わせる話です。

あれこれ説明を書いてしまっているので、「とりあえず実装の中身だけ知りたい」とか、「今回の内容をサクッと体験したい」・「実装した例を見たい」という場合は、目的によって、記事内の以下のいずれかの項目からお読みください。

  • MediaPipe のサンプルの構成(MediaPipe Hands を例に)
  • p5.js と組み合わせるために簡素化する
  • サンプルを体験する
  • MediaPipe + p5.js で試作したもの(試作の事例集)

記事に出てくるライブラリ等

p5.js とは?

p5.js とは、以下の公式ページの説明を引用すると、「クリエイティブ・コーディングのための JavaScriptライブラリ」です。
home___p5_js.jpg

クリエイティブ・コーディングの説明や例について、気になる方は以下をご覧ください。
 ●趣味としてのクリエイティブ・コーディング|deconbatch|note
  https://note.com/deconbatch/n/n6e6dbcb2b452

技術的なところでは、p5.js は描画系ライブラリの 1つとして紹介されたりもします。
ライブラリを用いなくても、HTML+JavaScript の構成で canvasタグと素の JavaScript を使った描画を行うことはできますが、それを、より便利にしてくれるもの、というのがざっくりなイメージです。
そして、WebGL を用いた 3D系の描画も扱いやすくしてくれる仕組みがあります。

MediaPipe とは?

MediaPipeでできること.jpg
MediaPipe は、Googleさんが提供している仕組みで、今回扱う JavaScript版では、主に人を対象にした認識処理(機械学習ベースの処理)を利用できるものです。
JavaScript版以外にも、以下のとおり複数の開発言語に対応していて、開発言語によって使える機能・使えない機能があったりします。
対応言語.jpg

JavaScript版は、「Googleさんが提供する機械学習ライブラリの JavaScript版、TensorFlow.js」で動いているので、ブラウザ上で動作します。
以下の公式ページ内のリンクから、ブラウザ上で体験できるデモページに行って、簡単に体験することもできます。
 ●MediaPipe in JavaScript - mediapipe
  https://google.github.io/mediapipe/getting_started/javascript.html
MediaPipe_in_JavaScript_-_mediapipe.jpg

デモページを開くと、例えば以下のような処理結果を、実際に確認できます(要カメラ)。

  • 顔全体だけでなく、顔の中の複数の場所の位置を検出
  • 手の指先や手首など、手の中の複数の場所の位置を検出
  • 画像中の人の領域と人ではない背景などの領域とを分離する(ための領域の情報を得られる)
  • いわゆる姿勢推定ができる(顔や手だけでなく、肩・腰・足など全身の複数の箇所の位置を検出できる)
  • 特定の物体の 3D空間内でのおおよその位置(それを囲うボックスの情報)を得られる

なぜ MediaPipe を使いたいのか?

ここで書いた JavaScript版の MediaPipe でできることの一部は、別の JavaScriptライブラリでも対応できるものがあります。p5.js とセットでよく用いられるもので ml5.js が有名どころであったりもします。

自分も、実際に MediaPipe以外の画像認識系ライブラリを p5.js と組み合わせて、お試しをしたことも何度もあります。

「なぜ、MediaPipe なのか?」というところは、その精度が優れているから、というのがあります。
例えば、画像からの手の認識で、「手が画面端に近いと認識しづらい」・「開いた手が真正面に向いてると良く認識するが、手を傾けたりすると認識しづらくなる」等ということがありますが、MediaPipe はそのあたりがダントツで優秀なイメージです。
ちなみに、手の認識の話だと「MediaPipe の仕組みは MediaPipe Hands」、他のライブラリだと「ml5.js の Handpose」、「Handtrack.js」などを p5.js と組み合わせて試し、使い比べていました。

「MediaPipe と p5.js で実際に何ができるか?」という話は、この記事の最後(「おわりに」の後の部分)に、自分が組み合わせて試したものの一部を掲載しますので、よろしければそちらをご参照ください。
(精度についても、その動画でおおよそ分かる部分があるかと思います)

MediaPipe と p5.js を組み合わせる

MediaPipe のサンプルの構成(MediaPipe Hands を例に)

MediaPipe と p5.js を組み合わせる話をする前に、MediaPipe 単体での構成を補足します。
いくつもある中の「MediaPipe Hands」を例にして、話を進めていきます。その際、簡単化のために、同時に検出できる手の数を 1つのみという制約をつけて進めていきます(機能としては2つよりも多くの手を同時に検出できます)。
MediaPipe Hands.jpg

基本的な実装については、公式ページの「JavaScript Solution API」の部分をもとにします。
公式ページの説明(Hands).jpg

具体的には、ライブラリの読み込みと、JavaScript の実装が以下のようになっています。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
</head>

<body>
  <div class="container">
    <video class="input_video"></video>
    <canvas class="output_canvas" width="1280px" height="720px"></canvas>
  </div>
</body>
</html>

HTML について、bodyタグ部分は「videoタグとcanvasタグ」という、カメラ入力+描画を扱うもので、よく見かける構成です。

<script type="module">
const videoElement = document.getElementsByClassName('input_video')[0];
const canvasElement = document.getElementsByClassName('output_canvas')[0];
const canvasCtx = canvasElement.getContext('2d');

function onResults(results) {
  canvasCtx.save();
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
  canvasCtx.drawImage(
      results.image, 0, 0, canvasElement.width, canvasElement.height);
  if (results.multiHandLandmarks) {
    for (const landmarks of results.multiHandLandmarks) {
      drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS,
                     {color: '#00FF00', lineWidth: 5});
      drawLandmarks(canvasCtx, landmarks, {color: '#FF0000', lineWidth: 2});
    }
  }
  canvasCtx.restore();
}

const hands = new Hands({locateFile: (file) => {
  return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
}});
hands.setOptions({
  maxNumHands: 2,
  modelComplexity: 1,
  minDetectionConfidence: 0.5,
  minTrackingConfidence: 0.5
});
hands.onResults(onResults);

const camera = new Camera(videoElement, {
  onFrame: async () => {
    await hands.send({image: videoElement});
  },
  width: 1280,
  height: 720
});
camera.start();
</script>

JavaScript の実装は、主に以下の処理で構成されています。

  • MediaPipe Hands の設定
    • パラメータ設定
  • カメラでの画像取得
    • カメラのパラメータ設定
    • 画像処理部への受け渡し
  • カメラ画像に検出したキーポイント等の描画(緑や赤の丸や線のところ)を重畳表示
    • カメラ画像の描画
    • キーポイント等の重畳描画
    • (キーポイントの座標データの保持)

p5.js と組み合わせるために簡素化する

p5.js と組み合わせるにあたり、重複した部分が出てくるので、それを削ります。
上記の HTML と JavaScript を扱う際、p5.js側で受け持てば良さそうなものは削ってしまうという方針です。

また、手の上に重畳しているキーポイント等の情報(赤や緑の丸や線)は、デバッグ用にあっても良いかもしれないですが、最終的には不要になるので削ってしまいます。

その結果、以下のような役割分担となります。

  • MediaPipe Hands で担当
    • MediaPipe Hands のパラメータ設定
    • カメラでの画像取得
    • カメラのパラメータ設定
    • 画像処理部への受け渡し
  • p5.js で担当
    • カメラ画像の描画
    • キーポイントの座標情報を使った処理(手の位置に応じた処理をする座標計算)
    • カメラ画像上への図形等の重畳描画

HTML について

ここから、p5.js の実装の話も入ってくるのですが、プログラムを p5.js Web Editor の上で作っていきます。

まずは、そこでデフォルトで準備されている index.html に手を加えていきます。
主にやることは、「MediaPipe Hands用のライブラリの読み込み」と「MediaPipe で使うための videoタグの追加」です (手のキーポイント検出までは、MediaPipe Hands側の処理で担ってもらう、という切り分けが良さそうだったため)。

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>

    <link rel="stylesheet" type="text/css" href="style.css" />
    <meta charset="utf-8" />
  </head>
  <body>
    <video class="input_video"></video>
    <script src="sketch.js"></script>
  </body>
</html>

JavaScript について

JavaScript については、以下の内容にしました。
元から改変している部分や、お試しのために設定した内容などは、コメントを入れています。

const isFlipped = true;

let keypointsHand = [];

const videoElement = document.getElementsByClassName("input_video")[0];
videoElement.style.display = "none";

function onHandsResults(results) {
  keypointsHand = results.multiHandLandmarks;
}

const hands = new Hands({
  locateFile: (file) => {
    return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
  },
});

hands.setOptions({
  selfieMode: isFlipped,
  maxNumHands: 1, // 今回、簡単化のため検出数の最大1つまでに制限
  modelComplexity: 1,
  minDetectionConfidence: 0.5,
  minTrackingConfidence: 0.5,
});
hands.onResults(onHandsResults);

const camera = new Camera(videoElement, {
  onFrame: async () => {
    await hands.send({ image: videoElement });
  },
  width: 1280,
  height: 720,
});
camera.start();

let videoImage;

function setup() {
  const canvas = createCanvas(500, 400);
  videoImage = createGraphics(320, 180);
}

function draw() {
  clear();
  background("rgba(100, 100, 255, 0.2)");

  videoImage.drawingContext.drawImage(
    videoElement,
    0,
    0,
    videoImage.width,
    videoImage.height
  );

  push();
  if (isFlipped) {
    translate(width, 0);
    scale(-1, 1);
  }
  displayWidth = width;
  displayHeight = (width * videoImage.height) / videoImage.width;
  image(videoImage, 0, 0, displayWidth, displayHeight);
  pop();

  if (keypointsHand.length > 0) {
    console.log(keypointsHand); // 結果を得る

    const indexTip = keypointsHand[0][8];
    console.log(indexTip);

    ellipse(indexTip.x * displayWidth, indexTip.y * displayHeight, 10);
  }
}

今回作ったものを実行すると、手の 21箇所のキーポイントの情報が得られます。
そして、その中の人差し指の先の位置座標を使って、その位置に円を描画します。この後にあるリンクをたどることで、実際にブラウザ上で体験できるものも用意しています。

「drawingContext」を用いる部分について

今回、「あらかじめ用意した videoタグ + MediaPipe側のカメラ処理で得られた画像を p5.js で取得する」のは、少しだけ対応が必要です。
具体的には、以下の Qiita の記事で書いた「drawingContext を用いる方法」で対応可能です。
 ●p5.js の処理で既存の videoタグに表示されたカメラ画像を取り込む【drawingContext を利用】 - Qiita
  https://qiita.com/youtoy/items/ad9372a1aa895b1a0fb1

サンプルを体験する

今回の記事に書いたもの

今回、p5.js Web Editor上で作ったものを、以下で実際に体験いただけます。
カメラに手をかざすと、人差し指の先に小さな円がついてくる、という簡素な内容になります。

●p5.js Web Editor | 【2021年のProcessingのアドベントカレンダー】MediaPipeとの組み合わせ
 https://editor.p5js.org/toyota_ref/sketches/7caxAKaEp

別の事例

また、今回の内容をさらに発展させ、某マンガ・アニメの悪役が使う呪文っぽものを体験できる、というコンテンツを試作しました。以下より、情報の参照や体験を行っていただけます。

●yo-to/magic-mediapipe_hands_p5js
 https://github.com/yo-to/magic-mediapipe_hands_p5js
●某アニメの悪役が使う「とっておきの手品」っぽい禁呪が使える(気がするかもしれない)Webアプリ V2 | ProtoPedia
 https://protopedia.net/prototype/2734

おわりに

MediaPipe + p5.js を組み合わせた実装をいろいろやる中で、できるだけシンプルにプログラムを組む、ということを模索してきました。
まずは、「これがベストかは分からないけれど、かなりコンパクトになっただろう」と思える状態になったので、アドベントカレンダー登録も絡めて、記事化してみました。

MediaPipe + p5.js で試作したもの(試作の事例集)

以下は、試作したものが動作している様子の動画を、ひたすら掲載していきます。

また、動画サムネイルが大量に並んだ表示になるのを避けるため、一部を除いて動画表示部分を折りたたんでいます。
「★ 動画の表示部分を折りたたんでいます (クリックすると展開できます)」と書かれた以下の部分をクリックすると、隠れていた動画が表示されます
折り畳み、この部分をクリック.jpg

MediaPipe Hands(手の認識)

【1】 手の動きで音を奏でる

【2】 某アニメの悪役の禁呪を使える感じがする(かもしれない)やつ

【3】 カメラ映像の中にカメラ映像を表示

=================================
   ★ 動画の表示部分を折りたたんでいます
   (クリックすると展開できます)
=================================

【4】 p5.js の WEBGLモードとの組み合わせを試すためのもの(ボックスを回転表示させる)

=================================
   ★ 動画の表示部分を折りたたんでいます
   (クリックすると展開できます)
=================================

【5】 MediaPipe + p5.js の組み合わせを最初に試したもの(透過された矩形を動的に描画)

=================================
   ★ 動画の表示部分を折りたたんでいます
   (クリックすると展開できます)
=================================

MediaPipe Holistic(姿勢推定)

【6】 ネコ画像で VTuber っぽい何か

MediaPipe Selfie Segmentation(背景と人の分離)

以下のデモ映像で、画面上部に 3つほど小さいウィンドウがありますが、これは見た目に分かりやすくするためにデバッグ用情報を残しているものです。
左から順に「人物領域の情報(マスクとして使うもの)」⇒「カメラ画像そのまま」⇒「カメラ映像にマスク処理をしたもの(人物のみ表示させたもの)」となっています。

【7】 人と背景の間での描画1(バーチャル背景的な表示や、p5.js の動的な表示)

=================================
   ★ 動画の表示部分を折りたたんでいます
   (クリックすると展開できます)
=================================

【8】 人と背景の間での描画2(p5.js の動的な表示)

=================================
   ★ 動画の表示部分を折りたたんでいます
   (クリックすると展開できます)
=================================

MediaPipe Face Mesh

【9】 目と口の動き(位置・開閉)と目の虹彩の動き(位置)を利用した描画

顔の中のたくさんの点を検出できる「MediaPipe Facemesh」が、目の中の領域での「虹彩検出」ができるようになったという機能追加があった際、それを試すために作ったものです(目の位置の白い円は、目の中の虹彩n位置・動きに連動しています)。

=================================
   ★ 動画の表示部分を折りたたんでいます
   (クリックすると展開できます)
=================================

MediaPipe で試作したもの(p5.js は使っていない事例)

以下は p5.js は絡まないですが、MediaPipe の JavaScript版をブラウザ上で使っている形の試作です。

【10】 手で 6台のロボットトイ(toio)を操る

ロボットトイの toio をブラウザから制御しており、そのきっかけの部分に MediaPipe を使いました。
toio とブラウザの通信は、Web Bluetooth API という仕組みを活用して実装しています。

=================================
   ★ 動画の表示部分を折りたたんでいます
   (クリックすると展開できます)
=================================

MediaPipe 以外のものを p5.js と組み合わせた事例

【11】 Handtrack.js を使ったもの(物理演算エンジンとの組み合わせ)

=================================
   ★ 動画の表示部分を折りたたんでいます
   (クリックすると展開できます)
=================================

【2022/1 追記】

p5.js を使わない MediaPipe の解説記事ですが、すごく詳しいもので良いなと思ったのでメモ。

●HTMLファイル1つで試す手の形状認識「MediaPipe Hands」の完全チュートリアル! - paiza開発日誌
 https://paiza.hatenablog.com/entry/2022/01/21/150000

19
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
14