10_3:mobileNetを使ったWebカメラ画像のKNN分類 ml5.js JavaScript

本稿は「ml5-examples/p5js/KNNClassification/KNNClassification_Video」で公開されているサンプルコードの解説です。

次のリンクをクリックすると、実際の動作を確認することができます。「mobileNetを使ったWebカメラ画像のKNN分類

このサンプルは、コンピュータに接続したWebカメラに向かって、グーかチョキかパーを出すと、ブラウザがそれが何かを推測するというものです。学習用のデータを新たに追加することもできます。

また大きな特徴として、既存のモデル用データをクリア(空に)して、グーチョキパーとはまったく関係のない別の画像データで訓練することで、独自のクラスを推測するアプリに作り変えることもできます。

以下はHTMLファイルに記述した全コードです。上記サンプルとは細かな点で異なっているので、コメント書きなどを参考にしてください。


<html>

<head>
  <meta charset="UTF-8">
  <title>KNN Classification on Webcam Images with mobileNet. Built with p5.js</title>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.0/p5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.0/addons/p5.dom.min.js"></script>
  <!-- <script src="https://unpkg.com/ml5@0.1.3/dist/ml5.min.js" type="text/javascript"></script> -->
  <!-- 1/27現在、新しいml5.jsがCDNに反映されていないので、
    https://raw.githubusercontent.com/ml5js/ml5-library/master/dist/ml5.min.js
  から新しいml5.jsを入手して、ローカルに保存する -->
  <!-- https://github.com/ml5js/ml5-library ページの[Usage]のminifiedリンク -->
  <script src="ml5.min.js" type="text/javascript"></script>

  <style>
    button {
      width: 220px;
      margin: 6px 6px 6px 0;
      padding: 4px;
      font-size: 14px;
    }
    
    video {
      width: 300px;
      height: 220px;
    }
    
    p {
      display: inline;
      font-size: 16px;
    }
    
    .emoji {
      display: inline-block;
      /*border: 1px solid black;*/
      
      width: 35px;
      font-family: 'Segoe UI Emoji';
      font-size: 1.8em;
    }
    
    .flex {
      display: flex;
    }
    
    .emoji-buttons {
      margin-top: 0;
      margin-left: -50px;
      width: 500px;
      /*border: 1px solid black;*/
    }
    
    .reset-button {
      width: 180px;
      margin-left: 40px;
      background-color: bisque;
    }
    
    .show-info {
      background-color: antiquewhite;
      margin-left: 40px;
      width: 440px;
    }
    
    .main-controle {
      /*border: 1px solid black;*/
      
      width: 350px;
      /*display:block;*/
      
      margin-left: 40px;
    }
    
    .no-flex {
      display: block;
    }
    
    #result {
      color: crimson;
      font-weight: bold;
    }
  </style>
</head>

<body>
  <h2>mobileNetを使ったWebカメラ画像のKNN分類</h2>
  <div class="flex">

    <div>
      <div id="videoContainer"></div>
      <p id="status">モデルの読み込み中...</p>
      <div class="main-controle no-flex">

        <button id="load">データセットを読み込む</button>
        <button id="buttonPredict">推測を開始</button>
        <button id="clearAll">全クラスをクリア</button>
        <button id="save">データセットを保存</button>
      </div>
    </div>

    <div>
      <!-- emoji-buttons -->
      <div class="emoji-buttons">
        <div>
          <span class="emoji"> ✊ </span>
          <button id="addClassRock">クラス Rockにサンプルを追加</button>
          <button id="resetRock" class="reset-button">クラス Rockをリセット</button>
          <div class="show-info">
            <p>Rockサンプル数:<span id="exampleRock">0</span></p>
            <p>| 信頼度: <span id="confidenceRock">0</span></p>
          </div>
        </div>
        <div>
          <span class="emoji"> 🖐 </span>
          <button id="addClassPaper">クラス Paperサンプルを追加</button>
          <button id="resetPaper" class="reset-button">クラス Paperをリセット</button>
          <div class="show-info">
            <p>Paperサンプル数:<span id="examplePaper">0</span></p>
            <p>| 信頼度:<span id="confidencePaper">0</span></p>
          </div>
        </div>
        <div>
          <span class="emoji"> ✌️ </span>
          <button id="addClassScissor">クラス Scissorにサンプルを追加</button>
          <button id="resetScissor" class="reset-button">クラス Scissorをリセット</button>
          <div class="show-info">
            <p>Scissorサンプル数:<span id="exampleScissor">0</span></p>
            <p>| 信頼度:<span id="confidenceScissor">0</span></p>
          </div>
        </div>
      </div>
      <!-- emoji-buttons -->
      <p class="no-flex">KNN分類器は<span id="result">...</span>と分類した。</p>
      <p>信頼度:<span id="confidence">...</span></p>
    </div>
  </div>
  <div>
    <h4>使い方</h4>
    <ol>
      <li>特徴抽出器が読み込まれたら、[データセットを読み込む]ボタンをクリックする。</li>
      <li>すると、グーチョキパーのビデオ画像の分類に使用する既存のサンプルデータが16 X 3 = 48個読み込まれる。</li>
      <li>[推測を開始]ボタンをクリックする。</li>
      <li>グーチョキパーの絵文字の右に、分類結果の信頼度が%で表示される。</li>
    </ol>
    <h4>既存のデータではうまく機能しない場合:</h4>
    <ul>
      <li>自分の手でグーやチョキ、パーを作って、そのデータをサンプルデータに追加できる。</li>
      <li>例:グーを作った状態で、[クラス Rockにサンプルを追加]ボタンをクリックする。</li>
    </ul>
    <h4>自分用のデータの保存:</h4>
    <ul>
      <li>[データセットを保存する]ボタンをクリックする。</li>
      <li>myKNNDataset.jsonファイルが自動的にダウンロードされるので、これを既存のものと置き換える。</li>
    </ul>
  </div>
  <script src="sketch.js"></script>
</body>

</html>

以下はsketch.jsの全コードです。これも細かな点がサンプルと異なっています。


let video;
let knnClassifier;
let featureExtractor;

function setup() {
    // KNN分類器を作成
    // https://github.com/ml5js/ml5-library/blob/master/src/KNNClassifier/index.js
    knnClassifier = ml5.KNNClassifier();

    // MobileNetから学習済みの特徴を抽出できるfeatureExtractor(特徴抽出器)を作成
    featureExtractor = ml5.featureExtractor('MobileNet', modelReady);
    noCanvas();
    // video要素を作成
    video = createCapture(VIDEO);
    // それをvideoContainer DOM要素に追加
    video.parent('videoContainer');
}

function modelReady() {
    select('#status').html('特徴抽出器が読み込まれた');
    // 保存された分類器データセットを読み込む
    buttonSetData = select('#load');
    buttonSetData.mousePressed(loadMyKNN);
}

// データセットを分類器に読み込む
function loadMyKNN() {
    console.log('[データセットを読み込む]ボタンがクリックされた')
        // load(path, callback)
        // https://github.com/ml5js/ml5-library/blob/master/src/KNNClassifier/index.js
    knnClassifier.load('./myKNNDataset.json', updateCounts);
    createButtons();
}

// 各ラベルのサンプル数を更新する
function updateCounts() {
    console.log('各ラベルのサンプル数を更新');
    // ラベルでサンプル数を数える
    // getCountByLabel()
    // https://github.com/ml5js/ml5-library/blob/master/src/KNNClassifier/index.js
    const counts = knnClassifier.getCountByLabel();
    //console.log(counts);  // {Rock: 16, Paper: 16, Scissor: 16}というオブジェクト
    // 各クラスのサンプル数を表示する
    select('#exampleRock').html(counts['Rock'] || 0);
    select('#examplePaper').html(counts['Paper'] || 0);
    select('#exampleScissor').html(counts['Scissor'] || 0);
}


// ビデオの現在のフレームを分類器に追加する
function addExample(label) {
    console.log('サンプルを、ラベルを付けて、分類器に追加');
    // 入力ビデオの特徴を取得
    const features = featureExtractor.infer(video);
    // featureExtractor.infer()には、オプションでendpointを渡すこともできる。
    // endpointのデフォルトは'conv_preds'。
    // endpointの全リストは、次のコードで出力できる。
    // const features = featureExtractor.infer(video, 'conv_preds');
    //console.log('All endpoints: ', featureExtractor.mobilenet.endpoints);
    // https://github.com/ml5js/ml5-library/blob/master/src/FeatureExtractor/Mobilenet.js

    console.log(features);

    // サンプルを、ラベルを付けて、分類器に追加する
    knnClassifier.addExample(features, label);
    updateCounts();
}

// 現在のフレームを推測する
function classify() {
    console.log('分類を実行');
    // knnClassifierからラベルの総数を得る => クラスの数と同じ(グーチョキパーの3種類)
    // getNumLabels()
    const numLabels = knnClassifier.getNumLabels();
    //console.log(numLabels); // 3
    if (numLabels <= 0) {
        console.error('サンプルがない');
        return;
    }
    // 入力ビデオの特徴を取得
    const features = featureExtractor.infer(video);
    //console.log(features.shape); // [1, 1, 1, 1000]
    // featuresの実体は、[-3.9986913204193115, -1.448075294494629,...]といった数値を1000個持つ配列
    //console.log(features.dataSync())  // Float32Array(1000) [-3.9986913204193115, -1.448075294494629,...]

    // knnClassifierを使って、特徴がどのラベルに属しているかを分類する
    // knnClassifier.classify()には、コールバック関数'gotResults'を渡すことができる。
    // async classify(input, kOrCallback, cb) { let k = 3;...
    // https://github.com/ml5js/ml5-library/blob/master/src/KNNClassifier/index.js
    knnClassifier.classify(features, gotResults);
    // オプションのK値を渡すこともできる。Kのデフォルトは3。
    // knnClassifier.classify(features, 3, gotResults);

    // またknnClassifier.classify()の呼び出しには、次のasync/await functionも使用できる。
    // ただし、function predictClass()の前に忘れずにasyncを追加する。
    // const res = await knnClassifier.classify(features);
    // gotResults(null, res);
}

// 結果を表示する
function gotResults(err, result) {
    console.log('分類結果が出たので、結果を表示');
    // エラーを表示する
    if (err) {
        console.error(err);
    }

    console.log('classIndex :' + result.classIndex);
    console.log('confidences :' + result.confidences);
    console.log('label :' + result.label);
    console.log('confidencesByLabel :' + result.confidencesByLabel);

    // 推測がチョキ100%の場合
    // result.classIndex: 2
    // result.confidences: {0: 0, 1: 0, 2: 1}
    // result.label Scissor
    // result.confidencesByLabel: {Rock: 0, Paper: 0, Scissor: 1}

    // 推測が有効であることを確認してから、
    if (result.confidencesByLabel) {
        const confidencesByLabel = result.confidencesByLabel;
        // result.labelは最も高い信頼度を持つラベル
        if (result.label) {
            select('#result').html(result.label);
            select('#confidence').html(`${(confidencesByLabel[result.label] * 100).toFixed(2)} %`);
        }

        select('#confidenceRock').html(`${confidencesByLabel['Rock'] ? (confidencesByLabel['Rock'] * 100).toFixed(5) : 0} %`);
        select('#confidencePaper').html(`${confidencesByLabel['Paper'] ? (confidencesByLabel['Paper'] * 100).toFixed(5) : 0} %`);
        select('#confidenceScissor').html(`${confidencesByLabel['Scissor'] ? (confidencesByLabel['Scissor'] * 100).toFixed(5) : 0} %`);
    }
    // ビデオなので、つづけて推測できる
    classify();
}


// 1つのラベルのサンプルをクリアする
function clearLabel(label) {
    //  clearLabel(labelIndex) => 1つのクラスをクリアするのと同じこと
    knnClassifier.clearLabel(label);
    updateCounts();
}

// 全ラベルの全サンプルをクリアする
function clearAllLabels() {
    // clearAllLabels() => すべてのクラスをクリアするのと同じこと
    knnClassifier.clearAllLabels();
    updateCounts();
}

// データセットをmyKNNDataset.jsonとして保存する
function saveMyKNN() {
    // save(name) => io.saveFile(fileName, JSON.stringify({ dataset, tensors }));
    // const saveFile = (name, data)
    // https://github.com/ml5js/ml5-library/blob/master/src/utils/io.js
    knnClassifier.save('myKNNDataset');
}

// UIボタンを作成するユーティリティ関数
function createButtons() {
    // buttonAが押されたら、ビデオの現在のフレームを
    // "Rock"というラベルを付けて、分類器に追加する
    const buttonA = select('#addClassRock');
    buttonA.mousePressed(function() {
        addExample('Rock');
    });

    // buttonBが押されたら、ビデオの現在のフレームを
    // "Paper"というラベルを付けて、分類器に追加する
    const buttonB = select('#addClassPaper');
    buttonB.mousePressed(function() {
        addExample('Paper');
    });

    // buttonCが押されたら、ビデオの現在のフレームを
    // "Scissor"というラベルを付けて、分類器に追加する
    const buttonC = select('#addClassScissor');
    buttonC.mousePressed(function() {
        addExample('Scissor');
    });

    // リセットボタン
    const resetBtnA = select('#resetRock');
    resetBtnA.mousePressed(function() {
        clearLabel('Rock');
    });

    const resetBtnB = select('#resetPaper');
    resetBtnB.mousePressed(function() {
        clearLabel('Paper');
    });

    const resetBtnC = select('#resetScissor');
    resetBtnC.mousePressed(function() {
        clearLabel('Scissor');
    });

    // 推測ボタン
    const buttonPredict = select('#buttonPredict');
    buttonPredict.mousePressed(classify);

    // 全クラスをクリアするボタン
    const buttonClearAll = select('#clearAll');
    buttonClearAll.mousePressed(clearAllLabels);

    // 分類器データセットを保存する
    const buttonGetData = select('#save');
    buttonGetData.mousePressed(saveMyKNN);
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA