はじめに:やりたいこと
- 来年に迫った2020年 東京オリンピック・パラリンピックですが、少し前にピクトグラムが発表されました。
(公式サイト) - とてもわかりやすいですが、馴染みがないスポーツだとパッとわかないものもあるかもしれません。
そこで、画像認識APIを体感したいというのものあり、
(1)「柔道」といったテキスト文字を送ると、ピクトグラムの写真を送り返してくれる。
(2)東京の街中で、会場に行く途中で、案内標識にピクトグラムを見つけたら写真をとり、それをLineに送ると競技名を教えてくれる。
というのをLine Botでやってみようと思いました。
前提
-
Line message APIのアクセストークンを取得 + Webhook URLにPHPプログラムを指定する。
【参考】PHPでLINEのbotを作ってみる -
Watson APIを使用するために、IBM CouldのIDを取得し、APIキーを取得する。
【参考】誰でもできる機械学習 Watson Visual Recognition(画像認識)の使い方
使用技術
- Line message API
- Watson API (Watson Visual Recognition (画像認識))
- PHP (初学者です。PHPを選んだ理由は、参考にさせていただく情報が多かったからです)
実際のもの
画像識別には、1クラス最低の10枚の画像しか用意していないので、返答も控えめに、「この画像は高確率で、**** じゃないでしょうか」という返答にしています。
QRコード(教えてピクトグラムTokyo2020)
よかったら使ってみてください。
実現方法
準備:カスタム分類器の作成+画像のアップロード+トレイニングの実施
- Visual Recognitionのカスタム分類器に対して写真をアップし、トレイニングするがそこそこ時間がかかる。
- 画像はクラスごとにまとめておき、ZIPファイルにて一括アップロードが可能。
- クラス名は、日本語に対応していない(と思う)
下記のコマンドを実行すると、クラス名とスコアーが返ってきます。(test.pngという画像を指定)
$curl -X POST -u "apikey:APIキーを指定" --form "images_file=@test.png" --form "classifier_ids=分類キーのIDを指定" "https://gateway.watsonplatform.net/visual-recognition/api/v3/classify?version=2018-03-19"
{
"images": [
{
"classifiers": [
{
"classifier_id": "***カスタム分類器ID***",
"name": "***カスタム分類器名称***",
"classes": [
{
"class": "basketball3x3",
"score": 0.993
}
]
}
],
"image": "test.png"
}
],
"images_processed": 1,
"custom_classes": 50
}
手順①Lineで返答する
・Line Message APIの基本です。相手の問いかけに対して、返答するところです。
【参考】LINE Messaging API でできることまとめ
<?php
// Lineアクセストークン設定
$channelAccessToken = 'キーを設定';
// ユーザーからのメッセージ取得
$inputData = file_get_contents('php://input');
// 受信したJSON文字列をデコード
$jsonObj = json_decode($inputData);
// イベントタイプの取得
$eventType = $jsonObj->{"events"}[0]->{"type"};
// メッセージイベントだった場合(テキスト、画像、スタンプなどの場合「message」になる)
if ($eventType == 'message') {
// メッセージタイプ取得
$messageType = $jsonObj->{"events"}[0]->{"message"}->{"type"};
// ReplyToken取得
$replyToken = $jsonObj->{"events"}[0]->{"replyToken"};
// メッセージタイプがtextの場合は、画像検索を実施
if ($messageType == 'text') {
// **** ここに(1)テキストが送られてきたときの処理を記述=>手順② ****
// メッセージタイプがimageの場合、画像のマッチングを実施
} elseif ($messageType == 'image') {
// **** ここに(2)画像が送られてきたときの処理を記述=>手順③ ****
//メッセージタイプがtext, imageではない場合
} else {
$response_format_text = [
"type" => "text",
"text" => "競技名かピクトグラムの画像を送ってください!"
];
$post_data = [
"replyToken" => $replyToken,
"messages" => [$response_format_text]
];
}
}
//Reply message用のURLに対して HTTP requestを行う
$ch = curl_init("https://api.line.me/v2/bot/message/reply");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json; charser=UTF-8',
'Authorization: Bearer ' . $channelAccessToken
));
$result = curl_exec($ch);
curl_close($ch);
?>
手順② テキストが送られてきたら、写真を送り返す
- DB(MySQLなど)に、候補となるテキスト文字と画像を置いてあるURLをセットしておく。(例:スポーツクライミング であれば、「すぽーつくらいみんぐ」・「クライミング」・「くらいみんぐ」・「ロッククライミング」くらいのテキストには対応できるようにする)
- 例えば、カヌーは、「スラローム」・「スプリント」があるので、カヌーと問いかけられたら、2つの写真を返すようにする。(最大は「自転車」の3つ:マウンテンバイク・ロード・トラック)
// DB接続情報
$host = "host情報";
$dbname = "db名称";
$user = "ユーザ";
$pass = "パスワード";
// DB接続情報設定
$db = new PDO('mysql:host=' . $host . 'dbname=' . $dbname . 'charset=utf8', $user, $pass);
// 相手から送られてきたメッセージを取得
$messageText = $jsonObj->{"events"}[0]->{"message"}->{"text"};
// SQL文の実行
$sql = 'SELECT * from tokyo2020';
$stmt = $db->query($sql);
$stmt->execute();
// 取得した情報をループして、配列に格納
foreach ($stmt as $row) {
// テキストがnameにある場合
if($row['name1'] == $messageText || $row['name2'] == $messageText || $row['name3'] == $messageText || $row['name4'] == $messageText || $row['name5'] == $messageText || $row['name6'] == $messageText){
$imgUrl[] = [
'imageurl' => $row['url'],
'name' => $row['name1'],
];
};
}
// 格納した情報が空白の場合、識別できない旨のメッセージを返す
if(empty($imgUrl)) {
$response_format_image = [
"type" => "text",
"text" => "競技が識別できませんでした。問いかけ直してみてください"
];
$post_data = [
"replyToken" => $replyToken,
"messages" => [$response_format_image]
];
// 格納した情報が空白ではない場合は、画像を返信する処理を実施
} else {
$cnt = count($imgUrl);
switch ($cnt) {
// データが1件の場合
case 1:
foreach ($imgUrl as $row1) {
$response_format_image = [
"type" => "image",
"originalContentUrl" => $row1['imageurl'],
"previewImageUrl" => $row1['imageurl']
];
}
$post_data = [
"replyToken" => $replyToken,
"messages" => [$response_format_image]
];
break;
// データが2件の場合
case 2:
$i = 1;
foreach ($imgUrl as $row1) {
if($i == 1) {
$response_format_image1 = [
"type" => "image",
"originalContentUrl" => $row1['imageurl'],
"previewImageUrl" => $row1['imageurl']
];
} else {
$response_format_image2 = [
"type" => "image",
"originalContentUrl" => $row1['imageurl'],
"previewImageUrl" => $row1['imageurl']
];
}
$i++;
}
$post_data = [
"replyToken" => $replyToken,
"messages" => [$response_format_image1, $response_format_image2]
];
break;
// データが3件の場合
case 3:
$i = 1;
foreach ($imgUrl as $row1) {
if($i == 1) {
$response_format_image1 = [
"type" => "image",
"originalContentUrl" => $row1['imageurl'],
"previewImageUrl" => $row1['imageurl']
];
} elseif($i == 2) {
$response_format_image2 = [
"type" => "image",
"originalContentUrl" => $row1['imageurl'],
"previewImageUrl" => $row1['imageurl']
];
} else {
$response_format_image3 = [
"type" => "image",
"originalContentUrl" => $row1['imageurl'],
"previewImageUrl" => $row1['imageurl']
];
}
$i++;
}
$post_data = [
"replyToken" => $replyToken,
"messages" => [$response_format_image1, $response_format_image2, $response_format_image3]
];
break;
};
}
手順③ 写真が送られてきたら、Watson APIで画像を判定し、競技名を返す
- 画像認識スコアーは、0.90よりも大きい数字の場合は、認識できたとして、競技名を返答する。
- スコアーがよくないときは、別の写真を送ってください、というメッセージを返答する。
- Watsonからは、競技名は英語で返ってくる(=分類キーのクラス名)ので、日本語に変換して返答する。
// メッセージIDの取得・格納
$messageId = $jsonObj->{"events"}[0]->{"message"}->{"id"};
// 画像ファイルのバイナリ取得
$ch = curl_init("https://api.line.me/v2/bot/message/" . $messageId . "/content");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json; charser=UTF-8',
'Authorization: Bearer ' . $channelAccessToken
));
$result = curl_exec($ch);
curl_close($ch);
// 画像ファイルの作成
$fp = fopen('./tokyo2020/test.jpg', 'wb');
if ($fp){
if (flock($fp, LOCK_EX)){
if (fwrite($fp, $result ) === FALSE){
print('ファイル書き込みに失敗しました');
} else {
print( $data . 'をファイルに書き込みました');
}
flock($fp, LOCK_UN);
} else {
print('ファイルロックに失敗しました');
}
}
fclose($fp);
// Watson Visual Recognition API(画像認識)による解析処理
$base_url = 'https://gateway.watsonplatform.net/visual-recognition/api/v3/classify?version=2018-03-19';
$key = 'API keyを設定';
$img_url = '画像のURLを指定';
$ch = curl_init();
$cfile = curl_file_create($img_url, 'image/jpeg', 'test_name');
$data = array(
'images_file' => $cfile,
'classifier_ids' => '分類キーのIDを設定'
);
curl_setopt($ch, CURLOPT_USERPWD, 'apikey:' . $key);
curl_setopt($ch, CURLOPT_URL, $base_url);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$result = curl_exec($ch);
curl_close($ch);
// 取得した結果についてJSON文字列をデコード
$jsonResult = json_decode($result, true);
// 返ってきたclass名とスコアーを取得する
$name = $jsonResult["images"][0]["classifiers"][0]["classes"][0]["class"];
$score = $jsonResult["images"][0]["classifiers"][0]["classes"][0]["score"];
// 名称が入っていない場合
if($name === null){
$reply_text = "ごめんなさい。画像判別に失敗しました(泣)";
// スコアーがよくなかった場合(識別確度が低かった場合)
} elseif($score < 0.90 ) {
$reply_text = "ごめんなさい。画像識別確度不足です。別の画像を送ってください(" . $score . ")";
// それ以外の場合
} else {
// SQL文の格納・実行
$sqlname = 'SELECT name1 from tokyo2020 where eng = "' . $name . '"';
$stmt = $db->prepare($sqlname);
$stmt->execute();
$name1 = $stmt->fetchColumn();
$reply_text = "この画像は高確率で、" . $name1 . " じゃないでしょうか(" . $score . ")";
}
$response_format_text = [
"type" => "text",
"text" => $reply_text
];
$post_data = [
"replyToken" => $replyToken,
"messages" => [$response_format_text]
];
まとめ
- 今回一番大変だったのは、アップロードする写真を揃えることでした。
- 1クラスで最低でも10枚必要で、50種類のピクトグラムがありましたので、最低でも500枚の写真が必要でした。
- この写真の枚数が増えれば増えるほど、回答確度があがると思いますので、10枚程度では、写真がうまくとれていないと、途端に回答率が下がることが体感できました。
- 今回は、Watson APIを使用しまいたが、Google Cloud AutoML Visionでもよいと思います。どちらが、回答確度がよいのか試してもおもしろいのかな、と思います。
【参考】Google Cloud AutoML Vision の使い方 機械学習モデルを作ってみよう