ml5jsで特徴抽出を用いた回帰を試す



Daniel Shiffmanさんのcodingtrainのチュートリアルを参考に
特徴抽出を用いた回帰（Regression with Feature Extractor）を試してみました。
featureExtractor()メソッドを用いて実装できます。

- ml5.js: Feature Extractor Regression
https://www.youtube.com/watch?v=aKgq0m1YjvQ&feature=youtu.be

- featureExtractor()
https://ml5js.org/docs/FeatureExtractor

- Examples
https://ml5js.org/docs/custom-regression

ファイルは以下にアップしています。
https://www.dropbox.com/s/3cgr99lwuocugwo/03_2_featureExtractorRegression-demo.zip?dl=0

コードは以下です。
試す場合は、プロジェクトフォルダに「images」フォルダを設置し、配下に「happy.png」「sad.png」を置いてください。
ライブラリはCDNを読み込んでますのでローカルサーバー上で実行してください。

``` html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Feature Extractor Regression</title>
  <style>
    body {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
    #inputContainer {
      position: absolute;
      top: 10px;
      left: 10px;
    }
    input, button {
      display: block;
      margin: 0 10px 10px 0;
    }
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.1/p5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.1/addons/p5.dom.min.js"></script>
  <script src="https://unpkg.com/ml5@0.1.1/dist/ml5.min.js"></script>
  <script src="sketch.js"></script>
</head>
<body>
  <div id="inputContainer"></div>
</body>
</html>
```

``` javascript

let mobilenet;
let predictor;
let video;
let happyIco;
let sadIco;
let value = 0;
let slider;
let addButton;
let trainButton;
// MobileNetモデルの準備が完了したときコールバック関数として呼ばれる
function modelReady() {
  console.log('モデルが準備できた!');
}
// ビデオ入力したクラス分類機の準備が完了したときコールバック関数として呼ばれる
function videoReady() {
  console.log('ビデオが準備できた!!!');
}
// トレーニング完了時に1度だけ実行される関数
function whileTraining(loss) {
  if (loss == null) {
    console.log('トレーニングが完了した！');
    // 画像の予測を取得する
    predictor.predict(gotResults);
  } else {
    console.log(loss);
  }
}
// トレーニング完了時に再帰的に実行される関数
function gotResults(error, result) {
  if (error) {
    console.error(error);
  } else {
    // 画像の予測の値を代入する
    value = result;
    // 画像の予測を取得する
    // 再帰的に実行
    predictor.predict(gotResults);
  }
}
function setup() {
  // キャンバスを生成する
  createCanvas(windowWidth, windowHeight);
  // ウェブカメラから映像をキャプチャし、Video要素を生成
  video = createCapture(VIDEO);
  // Video要素を非表示にする
  video.hide();
  happyIco = loadImage('images/happy.png');
  sadIco = loadImage('images/sad.png');
  // 背景を黒で塗りつぶす
  background(0);
  // MobileNetで事前学習された特徴を展開
  mobilenet = ml5.featureExtractor('MobileNet', modelReady);
  // ビデオを入力にして、回帰のカスタム予測子を生成する
  predictor = mobilenet.regression(video, videoReady);
  // スライダーを生成(0 ~ 1)
  slider = createSlider(0, 1, 0.5, 0.01);
  slider.parent('inputContainer');
  // 「サンプル画像追加」ボタンンを生成する
  addButton = createButton('サンプル画像を追加する');
  addButton.parent('inputContainer');
  addButton.mousePressed(function() {
    // 0 ~ 1の値に画像を紐づける
    predictor.addImage(slider.value());
  });
  // 「トレーニング開始」ボタンを生成する
  trainButton = createButton('トレーニング開始');
  trainButton.parent('inputContainer');
  // 「トレーニング開始」ボタンが押された時の処理
  trainButton.mousePressed(function() {
    // クラス分類機を再トレーニングする（Transfer Learning 転移学習）
    predictor.train(whileTraining);
  });
}
function draw() {
  // 背景を黒で塗りつぶし
  background(0);
  // ウェブカメラからの映像をキャンバスに描画
  // ミラー表示
  imageMode(CORNER)
  push(); 
    translate(width,0); 
    scale(-1.0,1.0); 
    image(video, 0, 0, width, height);
  pop();
  // 画像の直径は横幅の1/10
  let diameter = width / 10;
  // 円を描画
  imageMode(CENTER);
  // 上下左右に画像を配置
  for ( let x = 0; x < width; x += diameter) {
    for ( let y = 0; y < height; y += diameter) {
      if (value == 0) {
        break; // value = 0（初期値）のときは表示しない
      } else if (value < 0.5) {
        // 楽しい顔のアイコンを表示
        image(happyIco, x + diameter/2, y + diameter/2, diameter*Math.cos(value * Math.PI), diameter); 
      } else {
        // 悲しい顔のアイコンを表示
        image(sadIco, x + diameter/2, y + diameter/2, diameter*Math.cos(value * Math.PI), diameter);
      }
    }
  }
  // 線を描画しない
  noStroke();
  // 塗りを黒へ
  fill(0);
  // 矩形の描画モードを左上へ
  rectMode(CORNER);
  rect(0, height - 50, width, 50);
  
  // 矩形の描画モードを中央へ
  rectMode(CENTER);
  // 塗りつぶしを赤へ
  fill(255, 0, 0);
  rect(value * width, height - 25, 50, 50);
  // 塗りつぶしを白に設定
  fill(255);
  // テキストのサイズを設定
  textSize(25);
  // テキストを描画 
  text(value, 10, height - 10);
}
function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
```

ローカルサーバーを立ち上げ、ブラウザでindex.htmlを開いたら、
以下の手順でテストしてみてください。

1. スライダーを左端までドラッグし、ウェブカメラの前で前でポーズをとって、[楽しい]ボタンをクリックします。約15枚の画像を追加してみてください。 
2. スライダーを右端までドラッグし、ウェブカメラの前でポーズを変えて、[悲しい]ボタンをクリックします。約15枚の画像を追加してみてください。 
3. [トレーニング開始]をクリックして、トレーニングプロセスが完了するまで待ちます。 （コンソールログにプロセスが表示されます。）
4. トレーニングが完了したら、モデルをトレーニングした2つのポーズを切り替えます。 

※カメラのいろんな位置で、ポーズ画像を追加した方が精度が上がるかと思います。

- 楽しい
![happy.png](https://qiita-image-store.s3.amazonaws.com/0/13147/cd2e76fb-67ae-c916-edca-02211cee0c03.png)

- 悲しい
![sad.png](https://qiita-image-store.s3.amazonaws.com/0/13147/e33937f4-fed3-ffe1-ba4d-a32e90ea2f31.png)