2018-03-30

LINEから送った画像を文字起こししてくれるアプリを作るときのメモ①

ユーザーのニーズ


・卒論書く時とか、電子化されていない書物から引用したいけど、写すの糞めんどうくさい
・書評を書きたいけど、紙の本だと結構面倒くさい。
・文字起こしをしてくれるAPIのお試しページからそれをやるのも面倒くさい

実装したいもの


・Lineのインターフェイスから画像を送信したときに、その情報をGoogle Vision APIにかませて、文字おこししてかえしてくれる処理

仕様


正常系
・画像を送信すると、その画像に含まれている文字を返してくれる

異常系
・画像以外のデータ(テキスト・位置情報)を送ると、invalid dataみたいなレスポンスがちゃんと帰ってくるようにする
・文字が含まれていないもの、もしくは検出できないものに関しても、invalid imageみたいな値がちゃんと帰ってくるようにする
・APIのレスポンスを超えてしまった場合は、献金ページに移動する
・一秒間に10リクエスト以上gああると止まる

実装方法


・画像はLINEのエンドポイントからいったんbase64方式に変換して、Google Vision APIに投げると、リクエストで文字起こしした結果が返ってくる




思考ログ

・Google Vision APIのリファレンスを読む

・あと先人の知恵とか。いろいろ情報収集してみる。
・Vision APIの実装記事。顔認識のやつ。QIita。アクセストークンが必要っぽい

Google Cloud Vision APIで顔検出してみた - Qiita

顔パーツのマークアップ googole cloud vision API 使った顔のパーツ検出を、phpのお勉強がてらwebで結果表示するやつやってみた。 加えて、検出された感情も雑に表示。 face_expression.php <?php //APIキー $api_key = "XXXXX"; //リファラー //$referer = "各自設定してください"; //画像へのパス $image_path ...

・文字認識はこちらで紹介しているっぽい

google vision apiで文字検出 - Qiita

Text Detection using the Vision API https://github.com/GoogleCloudPlatform/cloud-vision/tree/master/python/text vision apiのわかりやすい記事 http://qiita.com/t-fujiwara/items/7e1f7c52a73887519ac1 初期設定 git clone https://github.com/GoogleCl...


認証キーが必要っぽい。これから仕組みを学ぶ必要がありそう。英語見るの面倒なので誰かが書いてくれていると嬉しい。と思ったけど、下の日本語のドキュメントを見れば、Credentialから見れば普通にAPIキー発行できそうですわ。

Authentication Overview | Documentation | Google Cloud

You can use a service account by providing its private key to your application, or by using the built-in service accounts available when running on Google Cloud Functions, Google App Engine, Google Compute Engine, or Google Kubernetes Engine.


Authentication Overview | Documentation | Google Cloud

You can use a service account by providing its private key to your application, or by using the built-in service accounts available when running on Google Cloud Functions, Google App Engine, Google Compute Engine, or Google Kubernetes Engine.

・APIキーが創れたらどういうリクエストを送ればいいかわかればできのでリファレンスを見る。


API リファレンス | Google Cloud Vision API ドキュメント | Google Cloud

null

・リファレンスにはいろいろあるけど、言語ごとのリファレンスはあるので、それは使わないでおく。で、普通にREST APIのリファレンスを見てやる。

Google Cloud Vision API | Google Cloud Vision API | Google Cloud

Integrates Google Vision features, including image labeling, face, logo, and landmark detection, optical character recognition (OCR), and detection of explicit content, into applications.

・OCRのリファレンスを見つけた。日本語

光学式文字認識(OCR) | Google Cloud Vision API ドキュメント | Google Cloud

"responses": [ { "textAnnotations": [ { "locale": "en", "description": "O Google Cloud Platform\n", "boundingPoly": { "vertices": [ { "x": 14, "y": 11 }, { "x": 279, "y": 11 }, { "x": 279, "y": 37 }, { "x": 14, "y": 37 } ] } }, ], "fullTextAnnotation": { "pages": [ {

・エンドポイント、リクエストパラメーターにAPIキーを入れる

POST https://vision.googleapis.com/v1/images:annotate?key=YOUR_API_KEY

・リクエストボディには、base64方式の画像ファイルと、テキストのタイプを指定している
・ドキュメントからとってくる場合は、こうしろとのこと

ドキュメント テキスト検出の場合は、上記のリクエストの機能を "type": "DOCUMENT_TEXT_DETECTION" に置き換えてください。

・リクエストパラメーターはこんな感じらしい。
{
  "requests": [
    {
      "image": {
        "content": "/9j/7QBEUGhvdG9zaG9...base64-encoded-image-content...fXNWzvDEeYxxxzj/Coa6Bax//Z"
      },
      "features": [
        {
          "type": "TEXT_DETECTION"
        }
      ]
    }
  ]
}
・送信できる画像の種類は3種類あるらしい。

画像は 3 種類の方法(base64 エンコード文字列(前述)、Google Cloud Storage URI、ウェブ URL)で渡すことができます。詳しくは、リクエストの作成をご覧ください。

・かえってくるレスポンスは以下のようになっている

{
  "responses": [
    {
      "textAnnotations": [
        {
          "locale": "en",
          "description": "ABBEY\nROAD NW8\nCITY OF WESTMINSTER\n",
          "boundingPoly": {
            "vertices": [
              {
                "x": 45,
                "y": 43
              },
              {
                "x": 269,
                "y": 43
              },
              {
                "x": 269,
                "y": 178
              },
              {
                "x": 45,
                "y": 178
              }
            ]
          }
        },
        {
          "description": "ABBEY",
          "boundingPoly": {
            "vertices": [
              {
                "x": 45,
                "y": 50
              },
              {
                "x": 181,
                "y": 43
              },
              {
                "x": 183,
                "y": 80
              },
              {
                "x": 47,
                "y": 87
              }
            ]
          }
        },
        {
          "description": "ROAD",
          "boundingPoly": {
            "vertices": [
              {
                "x": 48,
                "y": 96
              },
              {
                "x": 155,
                "y": 96
              },
              {
                "x": 155,
                "y": 132
              },
              {
                "x": 48,
                "y": 132
              }
            ]
          }
        },
        {
          "description": "NW8",
          "boundingPoly": {
            "vertices": [
              {
                "x": 182,
                "y": 95
              },
              {
                "x": 269,
                "y": 95
              },
              {
                "x": 269,
                "y": 130
              },
              {
                "x": 182,
                "y": 130
              }
            ]
          }
        },
        {
          "description": "CITY",
          "boundingPoly": {
            "vertices": [
              {
                "x": 51,
                "y": 162
              },
              {
                "x": 85,
                "y": 161
              },
              {
                "x": 85,
                "y": 177
              },
              {
                "x": 51,
                "y": 178
              }
            ]
          }
        },
        {
          "description": "OF",
          "boundingPoly": {
            "vertices": [
              {
                "x": 95,
                "y": 162
              },
              {
                "x": 111,
                "y": 162
              },
              {
                "x": 111,
                "y": 176
              },
              {
                "x": 95,
                "y": 176
              }
            ]
          }
        },
        {
          "description": "WESTMINSTER",
          "boundingPoly": {
            "vertices": [
              {
                "x": 124,
                "y": 162
              },
              {
                "x": 249,
                "y": 160
              },
              {
                "x": 249,
                "y": 174
              },
              {
                "x": 124,
                "y": 176
              }
            ]
          }
        }
      ]
    }
  ]
}

・"responses" > "textAnnotations" > "description" でJSON形式で文字を取得すればできそう

・ということで、とりあえずbase64方式で画像を取得できることを前提にした場合は、画像を送信することができるらしい。

・問題はLINEでとってきた画像を、base64方式に変換して、Google Apps Scriptで取得する方法がわかればよい。もしくは、別にどこかのサーバーに画像を一時保存することがもしできるのであれば、一度保存処理をかませることができるとよい。

・で、画像を保存するという処理をとるのは、あまり良い方法とは言えない。プライバシーとかセキュリティとか、そういう問題もあるし、そもそもストレージが圧迫するので、やっぱりLINEの画像のエンドポイントからとってくるのがよさそうだなぁ。

・ということで、LINEの画像のエンドポイントから、画像を保存する方向で行こうかと思っている。ここ資料あるかなぁ。

・過去にnode jsでやってる人がいた

LINE Botに投稿した画像をLambdaで受けてS3に保存する - Qiita

前置き LINE Botに投稿した画像をS3に保存しようと思いぐぐったものの 『lambda 画像 s3』だと死ぬほどサムネとかリサイズとか系のが引っかかる。 ちゃうねん!LINE Botに投稿した画像を本当にそのまま単純にS3に保存したいだけやねん!その後の加工は別処理でしたいねん! 調べ方が悪いのかなかなかストライクな記事が見つからなかったので、とりあえずいろんな記事のやってる事を継ぎ接ぎした事のまとめ。 Lambdaはnode.js。 流れ ①LINE...
・こちらはRails。一度ローカルに保存しているっぽい。

【Rails】LINE Messaging APIでデータを保存してecho bot作る - Qiita

More than 1 year has passed since last update. 2016年10月2日現在のLINE Messaging APIを送信、受信のAPIを大体試します。 Echo BotをRuby, Railsで実装し、全てのメッセージタイプの保存してエコーする実装を、テストまで含め紹介します。

・画像ファイルをbase64形式のデータで送るのはこの辺が役に立ちそう

formular_bot.gs

function Formular_bot(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("config");//configという名前のシートを取得
  var last_id = sheet.getRange("last_id").getValue();//最後に取得したリプライのIDをシートから取得
  var retobj = {};
  retobj = getNewReply(last_id); //リプライを取得する
  last_id = retobj.last_id;

  retobj['replies'].forEach(function(value,index,array){
    if(value['in_reply_to_screen_name']!=value['user']['screen_name']){//リプライの送信者と受信者が違うことを確かめる

      var intext = value.text;
      intext = intext.slice(intext.indexOf(value.in_reply_to_screen_name)+value.in_reply_to_screen_name.length+1);//文章部分だけを抜き出す
      var option = {
        'cht':'tx',
        'chl':''+intext+''
      };
      var URL = getEncordURL("https://chart.googleapis.com/chart",option);//Google Charts Tool用のURLにエンコード
      var resp = UrlFetchApp.fetch(URL,{'method':'get'});

      var resp_blob = resp.getBlob();
      var resp_64 = Utilities.base64Encode(resp_blob.getBytes());//TwitterAPI送信用にbase64にエンコード
      var form_data = {
         'media':resp_blob
      }
      var img_option = {
        'method':"POST",
        'payload':{'media_data':resp_64}
      };
      /*
      /  media/uploadに画像をbase64にエンコードしてPOSTし
      /  statusesのin_replay_to_status_idパラメータに戻ってきたJSONのmedia_id_stringを指定する
      /
      */
      var image_upload = JSON.parse(Twitter.oauth.service().fetch("https://upload.twitter.com/1.1/media/upload.json",img_option));
      var sendmsg = "@"+value['user']['screen_name'];
      var sendoption = {
        'status':sendmsg,
        'media_ids':image_upload['media_id_string'],
        'in_reply_to_status_id':value.id_str
      }
      Twitter.api('statuses/update',sendoption);
    }
  });

  sheet.getRange("last_id").setValue(last_id);
}

function getRateLimit(){
  Logger.log(JSON.stringify(Twitter.api('application/rate_limit_status'))); 
}
function getEncordURL(URL,param){
  URL += "?"
  Object.keys(param).forEach(function(key){
    URL+=key+"="+encodeURIComponent(param[key])+"&";
  }); 
  return URL.slice(0,URL.length-1);
}
function getNewReply(last_id){
  resp = {}
  resp['replies'] = Twitter.api('statuses/mentions_timeline',{'since_id':last_id})
  if(resp['replies'].length>0&&'id_str' in resp['replies'][0]){
    last_id = resp['replies'][0]['id_str']//最新のIDを保存しておく
  }
  resp['last_id'] = last_id;
  return resp;
}



・この辺の知識を使って明日さくっと実装してみる

続き








注目の投稿

 PythonのTweepyを利用して、Twitter APIを利用している。 その中で、ハマったポイントをメモしておく。 まず、Searchに関して。 Twitter検索は、クライアントアプリ側では、全期間の検索が可能になっている。 一方で、APIを利用する際は、過去1週間しか...