taka.txt

なにかを書いたメモ帳です

GASで文字起こしできるAPIを作ってあそぼ(遊びたかった)

はじめに

「Sunrise Advent Calendar 2019」の11日目、わんたか (or たか)です!

無限に有益!な記事を書くことができればよかったのですが、
個人開発の進捗が「虚無」に等しいので、ちょこっと最近使ってみたものの紹介です。

概要

Google Apps Scriptを人生で使ったことがなかったのですが、色々あって簡単に試したお話」です。


実装内容は、Google Apps Scriptと諸々を組み合わせ、画像から文字を認識して取り出す
OCR(Optical character recognition)を少し触ってみたお話です。

終結

f:id:onetk:20191211230930g:plain


ただ問題があり、APIを単体で公開して実験する際に400エラーで止まっています🤔
求む:知見

 

背景・既存手法

まとめとして、

  1. OCRサービスはすごい!
  2. お金もちょくちょくかかる
  3. OCRの先行研究すごい!

という流れです。 

 

1. OCRサービス

最近文字認識してくれるサービス、すごくいっぱいありますよね!最高!
でもどれがいいのか分からない、そんな時のいつものメンバーを紹介しますね!

どれくらいの精度か気になりますよね!
とりあえず昔々にbotかどうかの判定で使われていた、reCAPTCHAの文字画像をぶつけてみます。

f:id:onetk:20191210223834p:plain
reCAPTCHAとGoogleOCR

おぉ、大体できてる。さすがっす。
他にも色々あるみたいです。

 

2. お値段

ここ個人的に重要。

今とあるものを作っているのですが、2分に一回必ず処理する必要があるので、
1日で720回(=月21600回)近くAPIを叩きます。そのためここがネックでした。


上記それぞれのおおよその料金体系ですが、以下のような形です。

  • GCPの:最初の1000回/月無料。その後は1000回ごとに$1.5
  • Azureの:最初の5000回/月無料。その後は1000回ごとに$1.5
  • AWSの:(12ヶ月だけ)最初の5000回/月無料。その後は1000回ごとに$1.0
  • free OCR APIの:freeプランは25000回/月無料。ただし、1日500回まで。


やっぱり月数千回となるとお手頃なんですが、やはり大体はお値段かかりますね。

3. 先行研究

「お金がかかるんだったら、自分で作ればいいじゃない。」

昔の人が言ったり言わなかったりした名言。心に響きます。
というところで行くと、今のOCRの研究はどうなっているのかなと思いますよね。


偉大なる先人が10日前に書かれていました。凄過ぎる。
qiita.com


す ご そ う 。
(またいつかチャレンジします。)

提案手法

実際に1からCNNを実装・学習するのは時間の制約上厳しかったので、
こちらの先人の叡智を使わさせていただきました。
qiita.com
www.initialsite.com



シンプルに3行で、

  • Google driveにアップした画像をGoogle Docsで開く
  • 文字の書き起こしをしてくれる
  • そこからテキスト引っ張ってこれば......!!

という技です。凄い!

0. 準備など

流れとしてはほとんどこの記事を参考にしています。すぐにできるのでおすすめです。

qiita.com

1. ドライブに一度保存

APIを叩かれた際に、クエリに画像の値が入った過程で進めています。


はじめにGoogle Driveに画像を保存していきます。

function doGet(e) {


  // クエリから画像部分を取ってきて、decode!
  var data = e.parameter.img;
  var decoded_uri  = decodeURIComponent(data);
  var decoded_b64 = Utilities.base64Decode(decoded_uri);
  
  // 画像の一時保存先としてGoogleDrive指定
  var pic_folder_id = PropertiesService.getScriptProperties().getProperty('pic_folder_id');
  var drive = DriveApp.getFolderById(pic_folder_id);
  
  // 指定先のドライブに保存
  var blob = Utilities.newBlob(decoded_b64, "image/png", "ScreenShot.png");
  drive.createFile(blob);

  // 2に続く...

}


いきなりでてきたこれ何?と思われると思いますが、これはGAS上で環境変数として
フォルダのidを定義、呼び出しているだけです。

var pic_folder_id = PropertiesService.getScriptProperties().getProperty('pic_folder_id');

参考
qiita.com


2. Google Docsで文字認識

1の続きを書いていきます。

function doGet(e) {

  // ~~1の内容~~ //
 
  // 先ほどの画像がアップロードされた既定のフォルダ・画像を取得
  var folder = DriveApp.getFoldersByName('img').next();
  var image = folder.getFilesByName("ScreenShot.png").next();
  var doc_name = image.getName().split("\.")[0];
  
  // 画像が挿入されたGoogle Docs作成
  var request_body = {
    title: doc_name, 
    mimeType: 'image/png'
  }
  Drive.Files.insert(request_body, image, { ocr: true });
  
  var new_file = DriveApp.getFilesByName(doc_name).next();
  folder.addFile(new_file);
  DriveApp.getRootFolder().removeFile(new_file);
  
  // 作ったDocsを探して開く→テキスト持ってくる
  var docs = folder.getFilesByType('application/vnd.google-apps.document');
  var file = docs.next();
  var doc_id = file.getId();
  var doc = DocumentApp.openById(doc_id);
  var text = doc.getBody().getText().split('\n')[1];
  
  // 使用済みのDocsファイル・保存した画像の削除
  DriveApp.getFolderById(pic_folder_id).removeFile(file);
  DriveApp.getFolderById(pic_folder_id).removeFile(image);

   // 3に続く...

}

ものすっごく簡単でした。すご過ぎる。

ちなみに、Google Drive をより扱いやすくする Drive APIについてはこちらを参照しています。
Google Apps Scriptで画像の文字列を抜き出す - Qiita


4. ちょこっと全体の修正

あとは返り値を書いて完了です。

全体としては以下のようになりました。


function doGet(e) {

 // ~~1の内容~~ //
 // ~~2の内容~~ //

  return ContentService.createTextOutput(text);
}
5. 公開、使ってみる


ここで進捗ストップ!!!!!!


f:id:onetk:20191211215739p:plain

このコードを公開してAPIとしてを使おうとするとこのエラーで止まりました。。。
判明次第更新します。🙇‍♂️🙇‍♂️🙇‍♂️

6. 代わりに

今回は代わりにGASをちょっとしたwebページとして使えるようになる、
HTML Serviceを使ってみました。


そろそろ退屈になられてきた頃かと思うので、全速力で進みます。



HTML Serviceのやり方は簡単。

先程の画面の

f:id:onetk:20191211220504p:plain

ここを

f:id:onetk:20191211220508p:plain

こうして

f:id:onetk:20191211220518p:plain

こうじゃ

f:id:onetk:20191211220522p:plain


ほぼ空のindex.htmlが生成されたので、ここに画像を受け取るフォームや諸々を書きます。
参考はこちらの偉大な先人「Google Apps ScriptのHTML Serviceでファイルアップロードを行う」です!

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script>
      // Prevent forms from submitting.
      function preventFormSubmit() {
        var forms = document.querySelectorAll('form');
        for (var i = 0; i < forms.length; i++) {
          forms[i].addEventListener('submit', function(event) {
            event.preventDefault();
          });
        }
      }
      window.addEventListener('load', preventFormSubmit);

      function handleFormSubmit(formObject) {
        google.script.run.withSuccessHandler(updateUrl).processForm(formObject);
      }
      function updateUrl(result) {
        var img_url = "http://drive.google.com/uc?export=view&id="+ result[0].split('/')[5]
        var div_in = document.getElementById('input');
        div_in.innerHTML = '<img src="' + img_url + '">';
        var div_out = document.getElementById('output');
        div_out.innerHTML = '<a>▶︎ ' + result[1] + '</a>';
      }
      
    </script>
  </head>
  <body>
    <div style="text-align: center; padding-top: 100px;">
      <form id="myForm" onsubmit="handleFormSubmit(this)">
        <input name="myFile" type="file" />
        <input type="submit" value="Submit" />
      </form>
      <img id="file-preview">
      <div id="input"></div>
      <div id="output" style="padding-top:30px;"></div>
    </div>
 </body>
</html>

先程のgasのファイルの方も書き換えます。


/* ------------------------------ 入力フォームの処理関連 -------------------------------- */

function processForm(formObject) {
  var formBlob = formObject.myFile;
  var picFolderID = PropertiesService.getScriptProperties().getProperty('pic_folder_id');
  var drive = DriveApp.getFolderById(picFolderID);
  var driveFile = drive.createFile(formBlob);
  var ocr_res = processOcr(driveFile);
  return [driveFile.getUrl(), ocr_res];
}

function processOcr(Name) {

  var res = {status: null, message: ''};
  
  try{
    // 既定の画像がアップロードされたフォルダ取得
    var picFolderID = PropertiesService.getScriptProperties().getProperty('pic_folder_id');
    var folder = DriveApp.getFoldersByName('img').next();
    var image = folder.getFilesByName(Name.getName()).next();

    // imgフォルダに保存した画像にocrを行う
    var docName = image.getName().split("\.")[0];    
    var Request_body = {
      title: docName, 
      mimeType: 'image/png'
    }
    Drive.Files.insert(Request_body, image, { ocr: true });
    
    var newFile = DriveApp.getFilesByName(docName).next();
    folder.addFile(newFile);
    DriveApp.getRootFolder().removeFile(newFile);
    
    var docs = folder.getFilesByType('application/vnd.google-apps.document');
    var file = docs.next();
    var docId = file.getId();
    var doc = DocumentApp.openById(docId);
    var text = doc.getBody().getText().split('\n')[1];
  
    // 使用済みのdocファイル・画像の削除
    DriveApp.getFolderById(picFolderID).removeFile(file);
    //DriveApp.getFolderById(picFolderID).removeFile(image);
    Logger.log(text)
    res['status'] = 200;
    res['message'] = text;
  
  } catch (err) {
    res['status'] = 500;
    res['message'] = "Error:" + err;  
  }
  
  return text
}

/* ------------------------------- 表示HTMLの処理関連 --------------------------------- */

function doGet() {
  var htmlOutput = HtmlService.createTemplateFromFile("index").evaluate();
  htmlOutput
    .setTitle('OCR sample')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
  return htmlOutput;
}


すると一応の完成です!!!!!
ありがとうございました!!!!!!!!!!!!!

終わりに

ここまで読んでいただきありがとうございました!

次回はもう少し進捗がある状態で出直します💪



あとがき。
「~~APIを作ってあそぼ」を絶対に完成させたいので、原因と解決策が分かり次第更新します。