Help us understand the problem. What is going on with this article?

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

はじめに:やりたいこと

  • 来年に迫った2020年 東京オリンピック・パラリンピックですが、少し前にピクトグラムが発表されました。 (公式サイト
  • とてもわかりやすいですが、馴染みがないスポーツだとパッとわかないものもあるかもしれません。 1.jpg

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

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

というのをLine Botでやってみようと思いました。

前提

使用技術

実際のもの

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

実際の画面
3.JPG 4.JPG

QRコード(教えてピクトグラムTokyo2020)
2.jpg
よかったら使ってみてください。

実現方法

準備:カスタム分類器の作成+画像のアップロード+トレイニングの実施

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

watson.jpg

分類器のトレーニングに関するガイドライン

下記のコマンドを実行すると、クラス名とスコアーが返ってきます。(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 の使い方 機械学習モデルを作ってみよう

参考URL

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした