【Line Bot】教えてピクトグラム Tokyo 2020


##はじめに：やりたいこと
- 来年に迫った2020年 東京オリンピック・パラリンピックですが、少し前にピクトグラムが発表されました。
（[公式サイト](https://tokyo2020.org/jp/news/notice/20190312-01.html)） 
- とてもわかりやすいですが、馴染みがないスポーツだとパッとわかないものもあるかもしれません。
![1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167772/1e7f1f6d-f8b3-159d-d5e4-6130f3979bad.jpeg)

そこで、画像認識APIを体感したいというのものあり、

**(1)「柔道」といったテキスト文字を送ると、ピクトグラムの写真を送り返してくれる。**
**(2)東京の街中で、会場に行く途中で、案内標識にピクトグラムを見つけたら写真をとり、それをLineに送ると競技名を教えてくれる。**
 
というのをLine Botでやってみようと思いました。


##前提
- Line message APIのアクセストークンを取得　＋　Webhook URLにPHPプログラムを指定する。
[【参考】PHPでLINEのbotを作ってみる](https://qiita.com/caique/items/50e00c4a5801e2d583a4)

- Watson APIを使用するために、IBM CouldのIDを取得し、APIキーを取得する。
[【参考】誰でもできる機械学習 Watson Visual Recognition（画像認識）の使い方](https://blog.apar.jp/web/9425/)


##使用技術
- [Line message API](https://developers.line.biz/ja/services/messaging-api/)
- [Watson API (Watson Visual Recognition (画像認識))](https://www.ibm.com/watson/jp-ja/developercloud/visual-recognition.html)
- PHP （初学者です。PHPを選んだ理由は、参考にさせていただく情報が多かったからです）


##実際のもの

画像識別には、1クラス最低の10枚の画像しか用意していないので、返答も控えめに、「この画像は高確率で、**** じゃないでしょうか」という返答にしています。

実際の画面
![3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167772/8e53f6f4-334b-3612-d40d-04a95fead3a0.jpeg)  ![4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167772/13abb78f-63b0-dafd-1911-3fa5240345eb.jpeg)


QRコード（教えてピクトグラムTokyo2020）
![2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167772/53d4216c-9427-1b0a-c6ef-b2b6ded56ff9.jpeg)
よかったら使ってみてください。

##実現方法

### 準備：カスタム分類器の作成＋画像のアップロード＋トレイニングの実施
 - Visual Recognitionのカスタム分類器に対して写真をアップし、トレイニングするがそこそこ時間がかかる。
 - 画像はクラスごとにまとめておき、ZIPファイルにて一括アップロードが可能。
 - クラス名は、日本語に対応していない（と思う）

![watson.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167772/46f1d08b-444f-795b-9602-23d5d54393ab.jpeg)



[分類器のトレーニングに関するガイドライン](https://cloud.ibm.com/docs/services/visual-recognition?topic=visual-recognition-customizing#-)

下記のコマンドを実行すると、クラス名とスコアーが返ってきます。（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 でできることまとめ](https://qiita.com/kakakaori830/items/52e52d969800de61ce28)

~~~PHP
<?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つの写真を返すようにする。（最大は「自転車」の３つ：マウンテンバイク・ロード・トラック）　

~~~PHP
// 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からは、競技名は英語で返ってくる（=分類キーのクラス名）ので、日本語に変換して返答する。

~~~PHP
// メッセージ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 の使い方 機械学習モデルを作ってみよう](https://blog.apar.jp/web/10326/)


##参考URL
 - [LINE Bot で 画像を受信してみる](https://titirobo-develop.hatenablog.jp/entry/2017/01/29/130749)
 - [PHPでJSONデータの取得の仕方](https://qiita.com/fantm21/items/603cbabf2e78cb08133e)
 - [【LINE BOT】どんな bot が作られているか Qiita で調べてみた](https://qiita.com/msh5/items/d4abd7add10a0da5e9c5)