今回はVue.jsとFlaskでアヤメの分類を行って、分類結果の出力に合わせてCubismWeb(Live2D Cubism3SDK for Web)のモデルを動かしてみます。
アプリケーションの動作
1. Vueにアヤメのパラメータ(がく片の長さ、がく片の幅、花弁の長さ、花弁の幅)を入力する
2. Vueに入力されたパラメータをFlaskの分類器に渡す
3. 分類器の判定結果をVueに返す
4. 分類結果を表示してCubismWebと連動させる
使用するイラスト
CubismWebで表示するモデルです。

© Unity Technologies Japan/UCL
上記モデルには以前の記事で Live2D のテンプレート「FaceRig」を適用しています。
テンプレートを適用する手順についてはこちらで紹介しています。

動作確認サンプル
ChromeとEdgeで動作を確認しています。(下の画面はChrome)
アヤメの品種名を答えてくれるLive2Dモデルの作成
Vue.jsとFlaskでアヤメの分類を行って、分類結果をLive2Dモデル(CubismWeb)に渡すことで、アニメーションと連動させます。
1. scikit-learnでアヤメの学習モデルを作成する
機械学習のライブラリ「scikit-learn」にはサンプルとしてデータセットが用意されているので、今回はその中からアヤメの品種データを学習させます。
scikit-learnの使い方については、解説しているサイトが多数あるので省略します。
アヤメの学習モデルについてはpickleで保存します。
# save the model as pickle joblib.dump(forest, './trained-model/rfcParam.pkl', compress=True)
2. Vue.jsで入力フォームを作成する
Vue.jsでアヤメのパラメータの入力フォームと決定ボタンを作成します。
<div class="param">
<div id="paramsL">
<select v-model="selectedA">
<option v-for="option in options" v-bind:value="option.value">
[[ option.text ]]
</option>
</select>
<span>がく片の長さ SepalLength: [[ selectedA ]] cm</span>
</div>
<div id="paramsL">
<select v-model="selectedB">
<option v-for="option in options" v-bind:value="option.value">
[[ option.text ]]
</option>
</select>
<span>がく片の幅 SepalWidth: [[ selectedB ]] cm</span>
</div>
・ ・ ・
</div>
<div id="answer">
<h2>Answer</h2>
<button @click="getAnswer" id="btn">try</button>
<p>[[ irisName ]]</p>
</div>

3. 入力された値を分類器に渡す
入力フォーム関連の記述を追加します。
<script>
var ans = new Vue({
el: "#exampleA",
delimiters: ["[[", "]]"],
data: {
irisName: '// 菖蒲のパラメータが入力されたら品種名を出力します //',
selectedA: '0',
selectedB: '0',
selectedC: '0',
selectedD: '0',
options: [
{text:'0',value:'0'}, //0~10までの数値でアヤメのパラメータを選択
{text:'1',value:'1'},
{text:'2',value:'2'},
{text:'3',value:'3'},
{text:'4',value:'4'},
{text:'5',value:'5'},
{text:'6',value:'6'},
{text:'7',value:'7'},
{text:'8',value:'8'},
{text:'9',value:'9'},
{text:'10',value:'10'}
]
},
methods: {
update_name: function(str) {
this.irisName = str;
},
getAnswer:function() {
var src = [];
var SepalLength = parseFloat(this.selectedA);
var SepalWidth = parseFloat(this.selectedB);
var PetalLength = parseFloat(this.selectedC);
var PetalWidth = parseFloat(this.selectedD);
src = [SepalLength, SepalWidth, PetalLength, PetalWidth];
callback = this.update_name;
fetch('http://localhost:5000/irisCubism', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(src), //入力された値を分類器に渡す
}).then(function(res){
return res.json();
}).then(function(src) {
callback('「品種名は ' + src.irisName + ' です」'); //分類器からirisNameが返ってくる
}).catch(function(error) {
console.log(error)
})
}}
})
</script>

4. 分類器の処理
Vueから渡された値を基にして、分類器でアヤメの品種を判定します。
@app.route('/irisCubism', methods = ['GET', 'POST'])
def irisCubism():
src = request.json //Vueから送られてきた値
params = np.array(src)
if request.method == 'POST':
pred = predictIris(params) //1.で作成した学習モデルで判定を行う、内容については「機械学習」で調べて
irisName = getIrisName(pred) //0,1,2で出力される判定結果を品種名に変換する処理を行う
return make_response(jsonify({ //分類器の判定結果をVueに返す
'irisName':irisName
}))
elif request.method == 'GET':
return render_template('irisCubism.html')

5. CubismWebの画面を表示する
Vueの入力フォームの上にCubismWebの画面を配置します。
<div class="sizer" id="example">
<canvas id="SAMPLE" width="1280" height="640">
このブラウザは canvas 要素をサポートしていません。
</canvas>
</div>
live2dcubismcoreなどはページの最後尾で読み込みます。
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script> <script src="static/Core/live2dcubismcore.min.js"></script> <script src="static/dist/index.js"></script>
lappdefine.tsの相対パス ResourcesPath を作業環境に合わせて変更します。
今回は static というフォルダに必要なファイルを格納しているので、下記のように変更しました。
// 相対パス export const ResourcesPath: string = "static/Sample/JavaScript/Demo/Resources/";

6. 分類結果の表示に合わせてモデルのモーションを変更する
getAnswerのボタン(id=”btn”)がクリックされた時にモデルのモーションを更新することにしました。
モデルの更新処理及び描画処理を行うonUpdateを使って強引に更新しています。
(できればモーション更新用の処理で切り替えたい)
window.onload = () => {
let isPlay = false;
let bb = document.getElementById("btn");
bb.onclick = () => {
isPlay = true;
this.onUpdate = () => {
//onUpdateの中身と同じ//
if (isPlay === false) {
return;
}
for (let i: number = 0; i < this._models.getSize(); i++) {
//更新用のモーション
this._models.at(i).startMotion(LAppDefine.MotionGroupAdd, 0, LAppDefine.PriorityNormal);
}
isPlay = false;
}
}
}


