GASで文字起こしできるAPIを作ってあそぼ(遊びたかった)
はじめに
「Sunrise Advent Calendar 2019」の11日目、わんたか (or たか)です!
無限に有益!な記事を書くことができればよかったのですが、
個人開発の進捗が「虚無」に等しいので、ちょこっと最近使ってみたものの紹介です。
概要
「Google Apps Scriptを人生で使ったことがなかったのですが、色々あって簡単に試したお話」です。
実装内容は、Google Apps Scriptと諸々を組み合わせ、画像から文字を認識して取り出す
OCR(Optical character recognition)を少し触ってみたお話です。
最終結果
ただ問題があり、APIを単体で公開して実験する際に400エラーで止まっています🤔
求む:知見
背景・既存手法
まとめとして、
という流れです。
1. OCRサービス
最近文字認識してくれるサービス、すごくいっぱいありますよね!最高!
でもどれがいいのか分からない、そんな時のいつものメンバーを紹介しますね!
どれくらいの精度か気になりますよね!
とりあえず昔々にbotかどうかの判定で使われていた、reCAPTCHAの文字画像をぶつけてみます。
おぉ、大体できてる。さすがっす。
他にも色々あるみたいです。
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回まで。
やっぱり月数千回となるとお手頃なんですが、やはり大体はお値段かかりますね。
提案手法
実際に1からCNNを実装・学習するのは時間の制約上厳しかったので、
こちらの先人の叡智を使わさせていただきました。
qiita.com
www.initialsite.com
シンプルに3行で、
- Google driveにアップした画像をGoogle Docsで開く
- 文字の書き起こしをしてくれる
- そこからテキスト引っ張ってこれば......!!
という技です。凄い!
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); }
6. 代わりに
今回は代わりにGASをちょっとしたwebページとして使えるようになる、
HTML Serviceを使ってみました。
そろそろ退屈になられてきた頃かと思うので、全速力で進みます。
HTML Serviceのやり方は簡単。
先程の画面の
ここを
こうして
こうじゃ
ほぼ空の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を作ってあそぼ」を絶対に完成させたいので、原因と解決策が分かり次第更新します。