Custom Visionとは?
Custom Vision Service とは https://docs.microsoft.com/ja-jp/azure/cognitive-services/custom-vision-service/home
Custom Vision Service は、カスタム画像分類子の構築を支援する Microsoft Cognitive Services です。 画像の分類子を簡単に素早く構築、デプロイし、その性能を向上させることができます。 Custom Vision Service には、画像をアップロードして分類子をトレーニングするための REST API や Web インターフェイスが用意されています。
ざっくりいうと、画像認識のためのロジックは全部Microsoftがやってくれるので、ユーザーは学習データの投入・タグ付けをやるだけでAPI経由で推論結果を得られるというサービスです。 サービスイン当初は分類器のみでしたが、5/7のアップデートで物体検出(特定部分の座標取得)ができるようになりました。仕事で軽くプロトタイプを作ってみたところ、そこそこ使えそうな精度になったのでプライベート向けの題材で学習して公開してみることにしました。
東方Projectのキャラ判定機を作ろう
物体検出の題材として、学習データの分量を用意できることが必須かつ「分類することの意味」にもこだわってみたかったので東方キャラを題材に選んでみました。学習材料は自宅にある薄い本+公式書籍+αとなっております。 作業を簡略化するため、下記の条件に当てはまる箇所を撮影して学習データを用意します。
- 表紙
- 裏表紙
- フルカラー
コードサンプルはこちら
GitHub : gensobunya/try-customvision
1.学習データを準備
ログイン後、規約に同意してプロジェクト作成を選択すると、下のような画像アップロード画面に移動します。
学習データ(薄い本)を用意して1枚づつ裏表紙を撮影していきます。
アップロードします
ドラッグ&ドロップでエリア選択をして、タグ付けします
基本的にすべてマニュアル作業となりますが、編集画面で物体検出を自動でやってくれたりして結構スマートです。点線は自動検出部分。
2.学習させる
右上の「Train」ボタンを押すだけ…なのですが1タグあたり15枚以上の画像が必須となっているので、微妙に足りないキャラの学習データをフルカラーイラスト集から追加したり、全く足りないキャラのタグを削除したりします。
そもそも50タグが無料版の限界なので、東方キャラ全員の検知は不可能ですね…
初期状態で残ったキャラは以下の通りです。
- アリス・マーガトロイド
- クラウンピース
- ドレミー・スイート
- パチュリー・ノーレッジ
- フランドール・スカーレット
- マエリベリー・ハーン
- ミスティア・ローレライ
- レミリア・スカーレット
- 二ッ岩マミゾウ
- 八雲紫
- 十六夜咲夜
- 博麗霊夢
- 古明地こいし
- 古明地さとり
- 姫海棠はたて
- 宇佐見蓮子
- 射命丸文
- 本居小鈴
- 東風谷早苗
- 犬走椛
- 稀神サグメ
- 稗田阿求
- 茨木華扇
- 霧雨魔理沙
- 魂魄妖夢
学習が完了すると、試験結果も表示してくれます。特に何もしていないのでバランスが悪いですね… 学習結果と学習内容はIteration単位で保存されていますので、バージョン管理に活用できます。
検出の確信度閾値は左上のスライダーで調整できます。 よほど閾値を下げない限り、Precision>>>Recallとなっているので、そもそもキャラを拾い出すことに苦労している感じですね。
APIで結果を得ることもできるのですが、まずは画面上のQuick Testを使って、学習に使っていない電撃Playstationの表紙を判定させてみます。
3.学習内容を改善する
まだまだ使いものにならないのでもうちょっとマシな学習内容を目指します。 閾値を0にして確認すると、霊夢は12%の確率で魔理沙ですが魔理沙は1.9%の確率でしか魔理沙と思われていません。
学習ロジックはいじれないので、学習データを下記の通りいじって再度Trainingします。だいたい5分から10分くらいでモデルが出来上がるので爆速と言って過言ではないでしょう。
- データの少ないキャラタグを削除
- 衣装変更(コート着・社会派ルポライターあや・他)を削除
- 学習データ追加
結果、下記のキャラ(数字はデータ枚数)が残りました。主人公sと書籍キャラが多く残りましたね…
- アリス・マーガトロイド17
- クラウンピース19
- ドレミー・スイート18
- パチュリー・ノーレッジ19
- フランドール・スカーレット19
- マエリベリー・ハーン22
- レミリア・スカーレット33
- 十六夜咲夜28
- 博麗霊夢54
- 古明地こいし18
- 古明地さとり17
- 姫海棠はたて19
- 宇佐見蓮子23
- 射命丸文45
- 本居小鈴24
- 東風谷早苗28
- 稀神サグメ25
- 稗田阿求22
- 茨木華扇18
- 霧雨魔理沙46
- 魂魄妖夢18
もう1度、電撃Playstationの表紙でテストします。
未だ魔理沙発見できず。0%時の候補からも消えてしまいました。 他の画像でも霊夢が射命丸になったりするので、あまりいいモデルとは言えません。
仕事で作ったときのように、全タグの学習枚数を揃えると精度があがるかもしれませんが、時間がかかるので今回はここまでにします。
4.WEBアプリ化する
APIを用いてWEBアプリ化します。PredictionURLをクリックして、必要な情報を確認できます。
Vue.jsとAxiosを使って適当に実装します。公開するときはAPIキーを隠すべきですがとりあえずローカルで動かしたいだけなので気にせず直接書きます。 APIがJSON形式で検出したエリアを返してくるので、確信度でフィルタした後に画像にオーバーレイしてループしてボックス表示します。
<template> <div id="app"> <h2>SanteMedical Detector(Proto)</h2> <div> <input type="file" name="file" @change="onFileChange" class="waves-effect waves-light btn"> </div> <div class="buttonwrapper" v-if="image"> <button @click="removeImage" class="waves-effect waves-light btn">Remove image</button> <button @click="submitImage" class="waves-effect waves-light btn">Submit image <i class="material-icons right">send</i> </button> </div> <div v-if="image"> <div class="imagewrapper"> <img :src="image" /> <div v-if="predictionData" v-for="prediction in predictionData" :key="prediction.tagID" class="detectionBox" :style="{width:prediction.boundingBox.width*100+'%',height:prediction.boundingBox.height*100+'%',left:prediction.boundingBox.left*100+'%',top:prediction.boundingBox.top*100+'%'}"></div> </div> </div> <div v-if="predictionData" class="tagwrapper"> <ul class="collection"> <li class="collection-header"> <h4>Detected Tags</h4> </li> <li v-for="prediction in predictionData" :key="prediction.tagID" class="collection-item"> <b>{{prediction.tagName}}</b>:{{prediction.probability}} </li> </ul> </div> </div> </template> <script> import loadImage from 'blueimp-load-image' import axios from 'axios' const projectId = "YOUR PROJECT ID"; const predictionKey = "YOUR PREDICTION KEY"; const postURL = "https://southcentralus.api.cognitive.microsoft.com/customvision/v2.0/Prediction/"+projectId+"/image"; const probabilityLine = 0.15; //確信度閾値 export default { name: 'app', data: function() { return { image: '', imgName: '', imgHeight: '', imgWidth:'', uploadFile: '', predictionData:'' } }, methods: { onFileChange: function(e){ let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } if (!files[0].type.match('image.*')) { return; } this.createImage(files[0]); this.uploadFile = files[0]; this.predictionData = ''; this.occupancyRate =''; console.log(postURL); }, createImage: function(file) { let reader = new FileReader(); reader.onload = (e) => { //ローテーション loadImage.parseMetaData(file, (data) => { const options = { canvas: true }; if (data.exif) { options.orientation = data.exif.get('Orientation'); } loadImage(e.target.result, (canvas) => { const dataUri = canvas.toDataURL('image/jpeg'); this.image = dataUri; this.imgHeight = canvas.height; this.imgWidth = canvas.width; }, options); }); //ローテーションここまで } reader.readAsDataURL(file); this.imgName = file.name; }, removeImage: function() { this.image = ''; this.imgName = ''; this.predictionData = ''; this.occupancyRate =''; }, submitImage:function() { let formData = new FormData(); formData.append('shelfImage', this.uploadFile); let config = { method : 'post', headers: { 'content-type': "multipart/form-data", 'Prediction-Key': predictionKey } }; axios.post(postURL, formData, config) .then(response => { //response 処理 //確信度フィルタリング this.predictionData = response.data.predictions.filter( (items) => Number(items.probability) > probabilityLine); }) .catch(error => { // error 処理 this.predictionData = error; }) } } }; </script> <style> #app { text-align: center; } img { width: 100%; margin: auto; display: block; margin-bottom: 10px; } button{ margin: 10px; } .imagewrapper{ display: inline-block; width: 30%; position: relative; margin: 0px; padding: 0px; align-self: center; } .buttonwrapper{ padding: 1em; } .detectionBox{ position: absolute; border-width: 2px; border-style: solid; border-color: red; } </style>
Materializeを使って適当に装飾して、このように表示されます。