本稿は「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);
}