Edited at

Watson Visual Recognition でみそ煮込み分類器をつくろう


はじめに


  • 画像認識がはじめての方でも分類器がつくれるレベルを目指します

  • HTML/CSSの基礎知識が必要です

  • プログラミングはjavascriptをやったことがあれば充分です。経験なければがんばろう!

  • 作成するアプリはVue.jsを利用していますが、この記事で詳細については触れません


これは何の写真かわかりますか?



人は経験から写真に写っているモノを認識して無意識に分類できます。



「認識する」とは「分類する」



画像認識とは?

【事例付き】様々なサービスに活用!画像認識技術とは

この記事がとてもわかりやすいので、簡単に記事の内容を紹介していきます。



画像認識の基本


  • 画像認識とは、画像や動画から特徴をつかみ、対象物を識別するパターン認識技術の1つ

  • コンピューターは画像に何が写っているかを「理解」することができない

  • 対象物が何であるかを「確率」として表現することができる



ディープラーニング技術


2012年に開催された「ILSVRC2012」という画像認識の大会です。この大会では、ImageNetという大量のラベル付き画像(画像と写っている物の名前のセット)を基に画像認識率を競い会います。前年度までの正解率は、高くても70%台前半でした。ところが、一気に約85%の正解率を叩き出します。翌年からは、ディープラーニングを使ったモデル同士が競い合うようになり、正解率もどんどん向上して行きました。現在では正解率95%以上、人間を超えるレベルにまで到達しています。




画像認識の仕組み



画像認識の前処理

ピクセル単位の色しかわからないコンピュータに学習させるために通常は画像データに前処理をします。


  • 画像のノイズや歪みなどを取り除く

  • オブジェクトの輪郭を強調したり、明るさや色合いを調整することで、オブジェクトを抽出しやすくする

  • 画像からオブジェクトの領域を切り出す(領域抽出)



Watsonでできること



Visual Recognition (画像認識)

Watson Visual Recognitionは画像認識のためのツール群です。すでに学習済みのモデルも用意されており、2019年7月時点で以下の機能があります。


  • General (画像からタグ抽出)

  • Faces (顔認識)

  • Food (料理認識)

  • Explicit (不適切表現の判断)

  • Classify Images (カスタム分類器)

  • Detect Objects (物体検出)private beta

  • Text (文字認識)private beta



みそ煮込み分類器をつくろう



何故「みそ煮込み分類器」か?

名古屋人にとってソウルフードともいえる「みそ煮込み」。そこには2つの名店の存在があります。

名前も似てるし、観光にきた方に説明するにも大変です。みそ煮込みを食べようとした時もどっちがどっちなのか分からなくなってしまいます。そこでWatsonにどっちのみそ煮込みなのか判断してもらいましょう!



画像データを収集する前に

AIと著作権について理解しておきましょう。AIの著作権については2つの切り口があります。


  • AIが作成したモノの著作権

  • AIを作成するために利用するデータの著作権

利用するデータの著作権については2019年1月に改正された「著作権法30条の4」が当てはまります。基本的に学習のための画像データをネットから集めて作ることは違法ではないことになります。


第三十条の四

著作物は、次に掲げる場合その他の当該著作物に表現された思想又は感情を自ら享受し又は他人に享受させることを目的としない場合には、その必要と認められる限度において、いずれの方法によるかを問わず、利用することができる。ただし、当該著作物の種類及び用途並びに当該利用の態様に照らし著作権者の利益を不当に害することとなる場合は、この限りでない。

(中略)

二 情報解析(多数の著作物その他の大量の情報から、当該情報を構成する言語、音、影像その他の要素に係る情報を抽出し、比較、分類その他の解析を行うことをいう。第四十七条の五第一項第二号において同じ。)の用に供する場合

(以下略)




画像データを収集しよう

Watsonは一つのモデルに画像データが10枚以上あれば分類器をつくれます。2つのお店を混同しないように画像を集めてください。オリジナルの画像そのものよりもPCのキャプチャツールで切り抜きをした画像の方がデータ量が少なく済みます。

<キャプチャツール>

- Windows10 の場合はWin + Shift + S

- Mac の場合はCmd + Shift + 4



Watsonに学習させよう

集めた画像をWatsonに学習させるには以下のステップで行います。

1.IBM Cloudのアカウントを作成する

2.Watson Studio から Visual Recognition Service を作成する

3.集めた画像をアップロードする

4.モデルを作成し、アップロードした画像を学習させる

操作手順の詳細はこちらの記事を参考にしてください。(「Node-REDと接続」の手前まで)

追加するclass名は「honten」と「sohonke」にしてください。



スマホで使える「みそ煮込み分類器」をつくります

スマホで写真をとって、すぐに判定できるようWebアプリをつくります。完成のイメージはこちらです。



Node-REDでWebアプリをつくる

Node-REDでWebアプリをつくる手順は以下の通りです。コピペばかりだとつまらないのでWebアプリのつくり方を説明していきたいと思います。

1.IBM CloudでNode-REDの環境をつくる

2.Node-REDとWatson Visual Recognitionを接続する

3.Webアプリの画面をつくる

4.画像をWatson Visual Recognitionで判定する

1と2の操作手順の詳細は再度こちらの記事を参考にしてください。(「Node-REDと接続」からNode-REDフローエディターが起動するところまで)

3から説明していきます。



Node-REDでWebアプリをつくる

フローの完成のイメージはこんな感じです。

時間の無い方はこちらをコピペしてインポートしてください。

[{"id":"e8a18d31.d9ed7","type":"http in","z":"c61718a0.d3c958","name":"","url":"index","method":"get","upload":false,"swaggerDoc":"","x":140,"y":129,"wires":[["5a60e348.f0cfcc"]]},{"id":"1f1c3e2e.a9ff42","type":"http response","z":"c61718a0.d3c958","name":"","statusCode":"","headers":{},"x":530,"y":129,"wires":[]},{"id":"5a60e348.f0cfcc","type":"template","z":"c61718a0.d3c958","name":"画面","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset='utf-8'>\n    <meta http-equiv='X-UA-Compatible' content='IE=edge'>\n    <title>みそ煮込み分類器</title>\n    <meta name='viewport' content='width=device-width, initial-scale=1'>\n    <link rel=\"stylesheet\" href=\"https://unpkg.com/element-ui/lib/theme-chalk/index.css\">\n</head>\n<body>\n<div id=\"app\">\n   <!-- 画面 START -->\n    <el-row>\n        <el-col :span=\"24\" style=\"text-align:center\">\n            <h2><% result %></h2>\n            <!-- 撮影画像 -->\n            <p><video id=\"myVideo\" width=\"100%\" height=\"300\" autoplay playsinline=\"true\"></video></p>\n            <p><canvas id=\"myCanvas\" style=\"display:none\"></canvas></p>\n            <p>\n                <el-button-group>\n                    <el-button @click=\"sendImage\" :loading=\"buttonLoading\" icon=\"el-icon-camera\">Take a picture</el-button>\n                    <el-button @click=\"changeCamera\" icon=\"el-icon-refresh\"></el-button>\n                </el-button-group>\n            </p>\n            <p>\n                <el-upload\n                action=\"/upload\"\n                :on-change=\"handleChange\"\n                :on-success=\"handleSuccess\"\n                :file-list=\"fileList\"\n                list-type=\"picture\">\n                    <el-button icon=\"el-icon-picture-outline\">Click to upload</el-button>\n                </el-upload>\n            </p>\n        </el-col>\n    </el-row>\n    <!-- 画面 END -->\n</div>\n<!-- ここからはJavascript -->\n<script src=\"https://cdn.jsdelivr.net/npm/vue\"></script>\n<script src=\"https://unpkg.com/element-ui/lib/index.js\"></script>\n<script src=\"https://unpkg.com/axios/dist/axios.min.js\"></script>\n<script>\n    var app = new Vue({\n      el: '#app',\n      data: function() {\n        return {\n            buttonLoading: false,\n            result:'',\n            fileList:[],\n            cameraMode:'user'\n        }\n      },\n      delimiters: [\"<%\",\"%>\"],\n      methods: {\n        handleChange:function(file, fileList){\n            this.fileList = fileList.slice(-1);\n        },\n        changeCamera:function() {\n            this.cameraMode = (this.cameraMode === 'user') ? 'environment' : 'user';\n            this.initCamera();\n        },\n        initCamera:function() {\n            //ストリーム作成とカメラ画像のストリーミング開始\n            const constraints = {facingMode:this.cameraMode};\n            const promise = navigator.mediaDevices.getUserMedia({video: constraints,audio: false});\n            promise.then(function(stream){\n                video.srcObject = stream;\n                //video.play();\n            }).catch(function(err){\n                this.result = err;\n                console.log(err);\n            });\n        },\n        sendImage: function() {\n            this.buttonLoading = true;\n            this.result=\"...判定中...\";\n            video.pause();                                                // ビデオ停止\n            var canvas_image = ctx.drawImage(video, 0, 0, canvas.width, canvas.height);          // カメラ→imgに変換\n\n            var dataURL = canvas.toDataURL(\"image/jpeg\");    // DataURLに変換\n            var img_base64 = dataURL.replace(/^.*,/, '');    // プレフィックスを削除してBase64部分だけ取り出し\n            var param = {image:img_base64};\n            axios.post('/picture', param\n                ).then(response => {\n                    this.handleSuccess(response.data);\n                }).catch(error => {//エラー処理\n                    console.log(\"post failed\"+error);\n                });\n        },\n        handleSuccess: function(data,file,fileList) {\n            console.log(JSON.stringify(data));\n            let buf = '';\n            //クラス分類結果を表示\n            if (data.class) {\n                for (let i=0; i<data.class.length;i++) {\n                    buf += \" \" + data.class[i].class + \"(\"+data.class[i].score+\")\";   \n                }\n            }\n            buf = buf.replace(\"honten\",\"山本屋本店\").replace(\"sohonke\",\"山本屋総本家\")\n            this.result = buf;\n            this.buttonLoading = false;\n            video.play();\n        } \n      }\n    })\n    //------------------------//\n    //初期化\n    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;\n    window.URL = window.URL || window.webkitURL; \n    //カメラ入力\n    var video = document.getElementById('myVideo');\n    //中間出力\n    var canvas = document.getElementById('myCanvas');\n    var ctx = canvas.getContext('2d');\n    app.initCamera();\n\n</script>\n</body>\n</html>\n","output":"str","x":330,"y":129,"wires":[["1f1c3e2e.a9ff42"]]},{"id":"1684e49.f4c0c1b","type":"comment","z":"c61718a0.d3c958","name":"画面","info":"","x":130,"y":69,"wires":[]},{"id":"ce12d408.33c238","type":"http in","z":"c61718a0.d3c958","name":"","url":"picture","method":"post","upload":false,"swaggerDoc":"","x":150,"y":280,"wires":[["192997c8.cd9eb8"]]},{"id":"453ff247.1cd8fc","type":"visual-recognition-v3","z":"c61718a0.d3c958","name":"","vr-service-endpoint":"https://gateway-a.watsonplatform.net/visual-recognition/api","image-feature":"classifyImage","lang":"ja","x":590,"y":280,"wires":[["4ea6dec6.62d93","74d38315.15e7bc"]]},{"id":"657e8870.5aa688","type":"http response","z":"c61718a0.d3c958","name":"","statusCode":"","headers":{},"x":910,"y":280,"wires":[]},{"id":"4ea6dec6.62d93","type":"function","z":"c61718a0.d3c958","name":"後処理","func":"msg.payload = {};\n\nif (msg.result.images[0].classifiers) {\n    msg.payload.class = msg.result.images[0].classifiers[0].classes;\n}\n\nif (msg.result.images[0].faces) {\n    msg.payload.faces = msg.result.images[0].faces;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":280,"wires":[["657e8870.5aa688"]]},{"id":"6d68e42c.45980c","type":"comment","z":"c61718a0.d3c958","name":"画像認識","info":"","x":140,"y":200,"wires":[]},{"id":"74d38315.15e7bc","type":"debug","z":"c61718a0.d3c958","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"result","targetType":"msg","x":790,"y":340,"wires":[]},{"id":"192997c8.cd9eb8","type":"function","z":"c61718a0.d3c958","name":"デコード","func":"msg.payload = new Buffer(msg.payload.image, 'base64');\nreturn msg;","outputs":1,"noerr":0,"x":300,"y":280,"wires":[["49251580.d7bf6c"]]},{"id":"be94e94.b87ab18","type":"http in","z":"c61718a0.d3c958","name":"","url":"upload","method":"post","upload":true,"swaggerDoc":"","x":150,"y":340,"wires":[["d9623a99.0d5828"]]},{"id":"49251580.d7bf6c","type":"function","z":"c61718a0.d3c958","name":"前処理","func":"msg.params = {};\n//msg.params.classifier_ids = \"misomodel_336019170\";\nmsg.params.classifier_ids = \"default\";\nmsg.params.threshold = \"0\";\nreturn msg;","outputs":1,"noerr":0,"x":430,"y":280,"wires":[["453ff247.1cd8fc"]]},{"id":"d9623a99.0d5828","type":"function","z":"c61718a0.d3c958","name":"ファイル","func":"msg.payload = msg.req.files[0].buffer;\nreturn msg;","outputs":1,"noerr":0,"x":300,"y":340,"wires":[["49251580.d7bf6c"]]}]



Node-REDでWebアプリをつくる


Webアプリの画面をつくる(1/2)

フローエディターの左側にある入力から「http」と出力から「http response」をドラッグ&ドロップします。

続いて機能から「template」をドラッグ&ドロップします。

入力の「http」をダブルクリックしてプロパティを開きます。URLに「index」と入力して完了ボタンを押してください。

「template」のプロパティを開き、最初に書いてある文字を消し「みそ煮込み分類器」と入力して完了ボタンを押してください。

フローを線でつなげて右上の「デプロイ」ボタンを押します。

ブラウザのURLに

''https://XXXXX.mybluemix.net/index''

と入力して「みそ煮込み分類器」と表示されればオーケーです。(XXXXは各自のWebアプリ用に変更してください)



Node-REDでWebアプリをつくる


Webアプリの画面をつくる(2/2)

Webアプリの画面はこちらのソースを使います。VueElement UIを利用して画面を作っています。こちらをコピペして「template」に貼り付けしてください。


index.html

<!DOCTYPE html>

<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>みそ煮込み分類器</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
</head>
<body>
<div id="app">
<!-- 画面 START -->
<el-row>
<el-col :span="24" style="text-align:center">
<h2><% result %></h2>
<!-- 撮影画像 -->
<p><video id="myVideo" width="100%" height="300" autoplay playsinline="true"></video></p>
<p><canvas id="myCanvas" style="display:none"></canvas></p>
<p>
<el-button-group>
<el-button @click="sendImage" :loading="buttonLoading" icon="el-icon-camera">Take a picture</el-button>
<el-button @click="changeCamera" icon="el-icon-refresh"></el-button>
</el-button-group>
</p>
<p>
<el-upload
action="/upload"
:on-change="handleChange"
:on-success="handleSuccess"
:file-list="fileList"
list-type="picture">
<el-button icon="el-icon-picture-outline">Click to upload</el-button>
</el-upload>
</p>
</el-col>
</el-row>
<!-- 画面 END -->
</div>
<!-- ここからはJavascript -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
var app = new Vue({
el: '#app',
data: function() {
return {
buttonLoading: false,
result:'',
fileList:[],
cameraMode:'user'
}
},
delimiters: ["<%","%>"],
methods: {
handleChange:function(file, fileList){
this.fileList = fileList.slice(-1);
},
changeCamera:function() {
this.cameraMode = (this.cameraMode === 'user') ? 'environment' : 'user';
this.initCamera();
},
initCamera:function() {
//ストリーム作成とカメラ画像のストリーミング開始
const constraints = {facingMode:this.cameraMode};
const promise = navigator.mediaDevices.getUserMedia({video: constraints,audio: false});
promise.then(function(stream){
video.srcObject = stream;
//video.play();
}).catch(function(err){
this.result = err;
console.log(err);
});
},
sendImage: function() {
this.buttonLoading = true;
this.result="...判定中...";
video.pause(); // ビデオ停止
var canvas_image = ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // カメラ→imgに変換

var dataURL = canvas.toDataURL("image/jpeg"); // DataURLに変換
var img_base64 = dataURL.replace(/^.*,/, ''); // プレフィックスを削除してBase64部分だけ取り出し
var param = {image:img_base64};
axios.post('/picture', param
).then(response => {
this.handleSuccess(response.data);
}).catch(error => {//エラー処理
console.log("post failed"+error);
});
},
handleSuccess: function(data,file,fileList) {
console.log(JSON.stringify(data));
let buf = '';
//クラス分類結果を表示
if (data.class) {
for (let i=0; i<data.class.length;i++) {
buf += " " + data.class[i].class + "("+data.class[i].score+")";
}
}
buf = buf.replace("honten","山本屋本店").replace("sohonke","山本屋総本家")
this.result = buf;
this.buttonLoading = false;
video.play();
}
}
})
//------------------------//
//初期化
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
window.URL = window.URL || window.webkitURL;
//カメラ入力
var video = document.getElementById('myVideo');
//中間出力
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
app.initCamera();

</script>
</body>
</html>




Node-REDでWebアプリをつくる


画像をWatson Visual Recognitionで判定する(1/2)

部品を並べよう。「http」を2つ、「function」を4つ、「http response」を1つ。最後にIBM Watsonから「visual recognition」を1つ。

カメラで写真を撮影したときの処理をつくります。画面の以下の処理を受ける部分になります。


index.html

             var param = {image:img_base64};

axios.post('/picture', param
).then(response => {
this.handleSuccess(response.data);
}).catch(error => {//エラー処理
console.log("post failed"+error);
});

httpのプロパティを変更します。

Base64をデコードします。「function」の中身を以下に書き換えます。

msg.payload = new Buffer(msg.payload.image, 'base64');

return msg;

Base64エンコーディングとは?

画像ファイルや音声ファイル等のバイナリーデータを文字列で表現できます。文字列で扱うことができるのはWebで便利な時があります。


base64とは、64進数を意味する言葉で、すべてのデータをアルファベット(a~z, A~z)と数字(0~9)、一部の記号(+,/)の64文字で表すエンコード方式です

ただ、データ長を揃えるためにパディングとして末尾に記号の=を使用するので、厳密にはbase64は、65文字の英数字から表現されます

(URLや正規表現のなかでbase64を用いると一部の記号(+,/)は特別な意味を持つことがあるので-_などが用いられることがあります)


Watson Visual Recognitionのパラメータを設定します。「function」の中身を以下に書き換えます。

msg.params = {};

msg.params.classifier_ids = "(作成したモデルのClassifier ID)";
msg.params.threshold = "0";
return msg;

Visual Recognitionの判定結果をセットします。「function」の中身を以下に書き換えます。

msg.payload = {};

if (msg.result.images[0].classifiers) {
msg.payload.class = msg.result.images[0].classifiers[0].classes;
}
return msg;

フローをつないでデプロイします。これでカメラで写真を撮る部分は完成です。



Node-REDでWebアプリをつくる


画像をWatson Visual Recognitionで判定する(2/2)

最後に画像ファイルのアップロードに対応します。ほぼVue+Element UIでつくってあるのでやることはほとんどありません。

httpのプロパティを変更します。ファイルのアップロードにチェックを入れてください。

アップロードしたファイルをpayloadに設定します。「function」の中身を以下に書き換えます。

msg.payload = msg.req.files[0].buffer;

return msg;

これで完成です。早速、動かしてみましょう!



カスタマイズしてみよう

Watson Visual Recognitionの学習済みモデルを使えば「Food判定機」や「画像判定機」が簡単に作れます。

Node-RedのVisual Recognitionに渡すパラメータを変更してみてください。

msg.params.classifier_ids = "food"; //Food判定機

msg.params.classifier_ids = "default"; //画像判定機



凄く役立つ参考リンク


画像認識


Watson Visual Recognition + Node-RED


その他