Google App Script LINE データ管理

【GAS】画像のテキスト抽出ーOCR(5) LINE UI実装

はじめに

今回は、LINEをOCRのUI(ユーザ・インタフェース)としてOCRが使えるようになるまでを作ります。

以下の流れで処理が行われます。

  • 画像がLINEの特定チャンネルに投稿される(ユーザ操作)
  • LINEのWebhook設定よりOCRを行うGASスクリプトにPOSTされる
  • GASスクリプトからLINE Messaging APIを呼び出して投稿画像をGoogle Driveに保存する
  • GASスクリプトで保存した投稿画像をOCRにかけて文字を抽出する
  • 抽出した文字をLINEの応答メッセージとして送信する

今日の課題は前回と同様です。

今日の課題

  • LINEで画像を送ったら画像中の文字をテキストとして抽出した結果をメッセージで取得したい。

では、実現方法を見ていきましょう。

おさらい

初めてこの記事を見る方は、以下の4つの記事を確認することをお勧めします。

LINEとGASの連携

LINEにはメッセージを投稿すると、URLで指定される特定のサービスに情報を転送するWebhookの仕組みがあります。

このWebhookで呼び出されるサービスに、GASで作成するWebアプリを指定し、GASにて以下の処理を行います。

  • LINEからメッセージを取得
  • LINEから画像を取得
  • OCRで得られたテキストをLINE応答メッセージ送信

これらの処理をGASで実装する方法について解説します。

また、Webhookの設定方法はこちらで説明します。

LINEからメッセージの取得

LINEのWebhookにより、以下に示すコードのdoPost()関数が呼び出されます。

function doPost(e){
  try{
    if (typeof e === "undefined"){
      return;
    } 

    //JSON文字列解析
    var json = JSON.parse(e.postData.contents);
    var replyToken = json.events[0].replyToken;
    var messageId = json.events[0].message.id;
    var messageType = json.events[0].message.type;

    //画像以外は動作終了
    if(messageType !== "image"){
      sendMessage(replyToken, '画像を送信してください');
      return;
    }

    // ここで、以下の処理を行います。
    // ・LINEから画像取得してGoogle Driveに保存
    var LINE_END_POINT = "https://api-data.line.me/v2/bot/message/" + messageId + "/content";
    var fileId = saveImage(LINE_END_POINT);

    // ・保存した画像から文字を抽出(OCR)
    var text = ocr(fileId);

    // ・LINEに抽出した文字を応答メッセージとして送信
    sendMessage(replyToken, text);

  }
  catch(e){
    logOut(PRJ_NAME, 'doPost():' + e.message);
  }
}

doPost()関数の引数である「e」にPOSTされるデータが含まれるので、JSON.parse()によりJSON形式に変換します。

その後、応答に必要となるトークンを「replyToken」に格納し、画像を取得するために必要となるメッセージIDを「messageId」に格納します。

画像のみ受け付けることにするので、messageTypeの内容が「image」のみを処理します。画像以外の場合は、「画像を送信してください」というメッセージを出力して終了します。

インターネット間のデータ通信となるので、例外が発生しても良いようにtryで括り、例外発生時はcatch出来るようにします。

GASのWebアプリでは、Logger.log()で出力した内容は実行ログに残りません。そのため、新たにlogOut()関数を作成し、スプレッドシートにログを出力するようにします。これについては、便利なログ関数で説明します。

LINEから画像の取得

以下にLINEから画像を取得してGoogle Driveに保存するコードを示します。

var LINE_END_POINT = "https://api-data.line.me/v2/bot/message/" + messageId + "/content";
var fileId = saveImage(LINE_END_POINT);

// Blob形式で画像取得
function saveImage(LINE_END_POINT){
  var now = new Date();
  var formattedDate = Utilities.formatDate(now, 'Asia/Tokyo', 'yyyyMMdd_hhmmss')

  try {
    var url = LINE_END_POINT;
    var headers = {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN
    };

    var options = {
      "method" : "get",
      "headers" : headers,
    };

    // 画像ファイル保存
    var res = UrlFetchApp.fetch(url, options);
    var imageBlob = res.getBlob().getAs("image/png").setName(formattedDate + ".png");
    var folder = DriveApp.getFolderById(GOOGLE_DRIVE_FOLDER_ID);
    var file = folder.createFile(imageBlob);

    return file.getId();
  } 
  catch(e) {
    logOut(PRJ_NAME, 'saveImage(): ' + e.message);
  }
}

取得したmessageIdをもとにLINEから画像を取得するURLを作り、saveImage()関数で画像取得から保存までを行います。

headersとoptionsを設定し、UrlFetchApp.fetch()にて画像を要求します。

戻り値を使って、imageBlobに画像ファイルを格納します。この時の画像の名前は処理した日時をもとに、yyyyMMdd_hhmmss.pngとします。

その後、Google Driveの保存するフォルダをDriveApp.getFolderById()で取得し、folder.createFile()に取得したimageBlobを引数に与えて画像ファイルを保存します。

画像ファイルはOCRをかけるために必要なので、画像ファイルのIDをfile.getId()で取得して戻り値とします。

OCR処理

OCR処理はこれまでに見てきた内容と同様です。

Drive APIを使用してOCRを行う設定でファイルをコピーし、抽出した文字列を戻り値として返します。

function ocr(srcId){
  try{
    // 一時ファイル名
    var resource = {
      title: "tmp"
    };

    // OCR設定
    var option = {
      "ocr": true,        // OCRを行う
      "ocrLanguage": "ja",// OCRの言語設定
    }

    // OCR処理を行う
    var image = Drive.Files.copy(resource, srcId, option);      // ファイルコピー(OCR処理実施)
    var text = DocumentApp.openById(image.id).getBody().getText();  // OCRテキスト取得
    Drive.Files.remove(image.id);  // 一時ファイル削除

    return text;
  }
  catch(e){
    logOut(PRJ_NAME, 'ocr(): ' + e.message);
  }
}

LINEに応答メッセージを送信

以下にLINEに応答メッセージを送信する関数を示します。

// メッセージ送信
function sendMessage(replyToken, text){
  var replyUrl = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type": "application/json; charset=UTF-8",
    "Authorization": "Bearer " + LINE_ACCESS_TOKEN
  }; 

  var postData = {
    "replyToken": replyToken,
    "messages": [{
                  "type": "text",
                  "text": text
                  }]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  UrlFetchApp.fetch(replyUrl, options);
}

replyTokenとOCRで抽出した文字列を指定してpostDataを作り、それをJSON形式のpayloadに設定したPOSTデータoptionを作り、UrlFetchApp.cetch()を呼び出すことで、応答メッセージを送信します。

便利なログ関数

logOut()関数

Google Apps Scriptをウェブアプリとしてデプロイし、ウェブ経由で呼び出すと、コード中にLogger.log()でログ出力を記述していても実行ログとして残らなくなります。

これでは意図しない動きをする場合に、ログを出力させた原因解析ができません。

これを解決するために、以下のようにスプレッドシートにログを追記するlogOut()関数を作成し、要所要所にログを出力させて動作を確認できるようにします。

function logOut(prjName, msg) {
  var id = '<ログ出力用スプレッドシートのIDを指定>';
  var spreadsheet = SpreadsheetApp.openById(id);
  var sheet = spreadsheet.getSheets()[0];
  sheet.appendRow([new Date(), prjName, msg]);
}

引数のprjNameはプロジェクト名を入れるのが良いでしょう。msgが実際に記録されるログメッセージとなります。

あらかじめログ出力用のスプレッドシートを作成し、スプレッドシートのIDを上記コード中の「<ログ出力用スプレッドシートのIDを指定>」に記載します。

IDは以下の画像に示す部分となります。

スプレッドシートに出力されるログはこのようになります。実際にLINEに画像を投稿して文字が抽出された際に記録したログ内容です。

古い人は知っている、print文デバックと考え方は同じです。

LINE UIによるOCR処理の実装

全てのコード

以下にGoogle Apps Scriptの全てのコードを示します。

コード中の3か所で指定している以下のIDはご自分の環境に合わせて置き換えてください。

  • <チャネルアクセストークンを指定>
  • <Google Driveの画像保存フォルダIDを指定>
  • <ログ出力用スプレッドシートのIDを指定>

チャネルアクセストークンについては次の章で説明します。

// LINE Messaging API チャネルアクセストークン
var LINE_ACCESS_TOKEN = '<チャネルアクセストークンを指定>';

// 画像保存フォルダID
var GOOGLE_DRIVE_FOLDER_ID = '<Google Driveの画像保存フォルダIDを指定>';

// logOut用
var PRJ_NAME = 'LineOCR';

//LINE Messaging APIからのPOST受信処理
function doPost(e){
  try{
    if (typeof e === "undefined"){
      return;
    } 

    //JSON文字列解析
    var json = JSON.parse(e.postData.contents);
    var replyToken = json.events[0].replyToken;
    var messageId = json.events[0].message.id;
    var messageType = json.events[0].message.type;

    //画像以外は動作終了
    if(messageType !== "image"){
      sendMessage(replyToken, '画像を送信してください');
      return;
    }

    var LINE_END_POINT = "https://api-data.line.me/v2/bot/message/" + messageId + "/content";
    var fileId = saveImage(LINE_END_POINT);
    var text = ocr(fileId);
    logOut(PRJ_NAME, 'ocr text: ' + text);

    // メッセージ送信
    sendMessage(replyToken, text);

    // 保存画像削除
    Drive.Files.remove(fileId);
  }
  catch(e){
    logOut(PRJ_NAME, 'doPost():' + e.message);
  }
}

// Blob形式で画像取得
function saveImage(LINE_END_POINT){
  var now = new Date();
  var formattedDate = Utilities.formatDate(now, 'Asia/Tokyo', 'yyyyMMdd_hhmmss')

  try {
    var url = LINE_END_POINT;
    var headers = {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN
    };

    var options = {
      "method" : "get",
      "headers" : headers,
    };

    // 画像ファイル保存
    var res = UrlFetchApp.fetch(url, options);
    var imageBlob = res.getBlob().getAs("image/png").setName(formattedDate + ".png");
    var folder = DriveApp.getFolderById(GOOGLE_DRIVE_FOLDER_ID);
    var file = folder.createFile(imageBlob);

    return file.getId();
  } 
  catch(e) {
    logOut(PRJ_NAME, 'saveImage(): ' + e.message);
  }
}

// OCR処理
function ocr(srcId){
  try{
    // 一時ファイル名
    var resource = {
      title: "tmp"
    };

    // OCR設定
    var option = {
      "ocr": true,        // OCRを行う
      "ocrLanguage": "ja",// OCRの言語設定
    }

    // OCR処理を行う
    var image = Drive.Files.copy(resource, srcId, option);      // ファイルコピー(OCR処理実施)
    var text = DocumentApp.openById(image.id).getBody().getText();  // OCRテキスト取得
    Drive.Files.remove(image.id);  // 一時ファイル削除

    return text;
  }
  catch(e){
    logOut(PRJ_NAME, 'ocr(): ' + e.message);
  }
}

// メッセージ送信
function sendMessage(replyToken, text){
  var replyUrl = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type": "application/json; charset=UTF-8",
    "Authorization": "Bearer " + LINE_ACCESS_TOKEN
  }; 

  var postData = {
    "replyToken": replyToken,
    "messages": [{
                  "type": "text",
                  "text": text
                  }]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  UrlFetchApp.fetch(replyUrl, options);
}

function logOut(prjName, msg) {
  var id = '<ログ出力用スプレッドシートのIDを指定>';
  var spreadsheet = SpreadsheetApp.openById(id);
  var sheet = spreadsheet.getSheets()[0];
  sheet.appendRow([new Date(), prjName, msg]);
}

また、以下に注意してください。

コードを実行する前に、サービスに「Drive API」を追加することを忘れないようにしましょう。

チャネルアクセストークン

LINE Messaging APIを呼び出すときに 必要となるチャネルアクセストークンを取得します。

LINE Developersで作成したプロバイダーのチャネルを指定し、「Messaging API 設定」画面からチャネルアクセストークンを確認します。

「全てのコード」の中で指定している「<チャネルアクセストークンを指定>」の部分を、LINE Developersで作成したチャネルのチャネルアクセストークンで置き換えましょう。

Webアプリ公開

作成したGASコードをLINEから呼び出せるようにするには、以下に示す2つの設定を行う必要があります。

  • GAS側でWebアプリとして公開する「デプロイ」を行う。
  • LINEのチャネル設定にて、Webhook設定からGASを呼び出すように設定する。

これらを以下に解説します。

Google Apps Script デプロイ

作成したGASスクリプトをWebアプリとして公開します。

「デプロイ」ボタンを押して表示された画面で、アクセスできるユーザを「全員」とします。

「デプロイ」ボタンを押すと以下の画面になります。

ここで表示されているウェブアプリのURLは、LINE Develperサイトのチャネル設定でWebhookに使用するURLです。

ウェブアプリのURLをコピーしておきましょう。

LINE Webhook設定

Google Apps Scriptでデプロイして得られたウェブアプリのURLを、WebhookのURLとして設定します。

Message API設定ページのWebhook URLにある編集ボタンを押します。

Webhook URLにデプロイしたWeb URLを貼り付けて、更新ボタンを押します。

最後にWebhookの利用をONにします。

実行

以下の画像をLINEに投稿して文字列が応答メッセージとして返ってくるか確認します。

投稿してから数秒待つと、以下のように抽出した文字を応答メッセージとして得ることができます。

まとめ

いかがでしたでしょうか。

手順が多いため、ちょっと難しかったかもしれませんが、一つ一つ順番に実装を進めることで実現できます。

そして、LINEに文字抽出させたい画像を投稿すると、抽出した文字が応答メッセージとして得られるようになったかと思います。

ちょっとしたメモや、研究調査に基づいた写真、旅行時に撮影した説明書きや、スクリーンショットをLINEに投稿することで、必要な文字を編集可能な文字として得ることができるようになります。

LINEそのものをメモとして使うこともできるので、このままでも応用範囲は広いと思いますので、活用いただけたら幸いです。

次回はOCR編の最後として、LINE UIの改造を行い、OCRにより抽出した文字をLINEのメッセージとして得るだけでなく、登録したメールアドレスに送信し、Evernoteにも登録できるようにします。

では、今日もよい一日を。

-Google App Script, LINE, データ管理
-, , ,