GASとGoogle Cloud Vision APIで認識精度を検証してみた


最近、あまりコードが書けていないのですが、仕事の方があまりにも非効率極まりなくなってきたので、ToDo管理システムを構築してやろうと思っているのですが、既存のツールだけだとどうしても「手書きメモ」のリマインドがもれるので、どうにかしてやろうと思ったのです。

# 作ろうと思っているシステム
GoodNotesでメモを書き出す→Google Driveに保存→Google Apps Scriptで定期的にGoogle Vision APIを叩いて、メモを取得する→ToDoを登録
という野望を持っているわけです。

# 参考にした資料
Google Vision APIはGoogle Apps Scriptには標準で用意されていないので、APIを直接叩きます。既に同様のことが記事になっていました。
（参考記事：[Google Apps ScriptでCloud Vision APIを使ってみる](http://qiita.com/wezardnet/items/ebd0c98f3fc04bcc53a0)）

# 本日のコード.gs
ほぼ、参考サイトのコピペです。なんかすみません…。特定のフォルダにある画像ファイルを順番にVision APIに通しています。

```コード.gs
function doGet(e) {  
  var url = "https://accounts.google.com/o/oauth2/auth";
  var client_id = PropertiesService.getScriptProperties().getProperty("client_id");
  
  var accessToken = PropertiesService.getScriptProperties().getProperty("access_token");
  if(accessToken == null) {    
    //必要なパラメータ郡
    var param = {
      "response_type" : "code",
      "client_id" : client_id,
      "redirect_uri" : getCallbackURL_(),  //←は↓に書いてあります
      "state" : ScriptApp.newStateToken().withMethod("callback").withArgument("name", "value").withTimeout(2000).createToken(),  //←の指定で/usercallbackが呼び出された後、callback関数を呼び出し、その際nameというパラメータでvalueという値を渡し 2000秒でタイムアウトになるという設定が可能になります。
      "scope" : "https://www.googleapis.com/auth/cloud-vision",
      "access_type" : "offline",
      "approval_prompt": 'force'
    };
    var params = [];
    for(var name in param){ 
      params.push(name + "=" + encodeURIComponent(param[name]));
    }
    
    url = url + "?" + params.join("&");
    
    Logger.log(url);
    return HtmlService.createHtmlOutput('<a href="' + url + '" target="_blank">認証</a>') ;
  } else {
    return HtmlService.createHtmlOutput('<p>設定済みです。</p>');
  }
}

function getCallbackURL_(){
  var url = ScriptApp.getService().getUrl() ;
  if ( url.indexOf('/exec') >= 0 ) return url.slice(0, -4) + 'usercallback' ;
  return url.slice(0, -3) + 'usercallback' ;
}

function callback(e) {
  var credentials = fetchAccessToken_(e.parameter.code) ;
  var scriptProperties = PropertiesService.getScriptProperties() ;
  Logger.log(credentials);
  scriptProperties.setProperty('access_token', credentials.access_token) ;
  scriptProperties.setProperty('refresh_token', credentials.refresh_token) ;
  
  return HtmlService.createHtmlOutput('<p>設定しました。</p>');
}

function fetchAccessToken_(code) {
  var prop = PropertiesService.getScriptProperties();
  var res = UrlFetchApp.fetch("https://accounts.google.com/o/oauth2/token", {
    "method" : "POST",
    payload : {
      "code" : code,
      "client_id" : prop.getProperty("client_id"),
      "client_secret" : prop.getProperty("client_secret"),
      "redirect_uri" : getCallbackURL_(),
      "grant_type" : "authorization_code"
    },
    muteHttpExceptions : true
  });

  return JSON.parse(res.getContentText());
}

function todoDetection() {
  var targetFolder = PropertiesService.getScriptProperties().getProperty("target_folder");
  var folder = Drive.Children.list(targetFolder, {q: "trashed=false"});
  for(var i = 0; i < folder.items.length; i++) {
    var target = Drive.Files.get(folder.items[i].id);
    var file = DriveApp.getFileById(target.id);
    
    var result = imageAnnotate(file);
    if(!result) {
      Logger.log("target file name = " + target.title);
    }
  }
}

function imageAnnotate(file){
  var accessToken = PropertiesService.getScriptProperties().getProperty("access_token");
  
  var payload = JSON.stringify({
    "requests":[
      {
        "image": {
          "content": Utilities.base64Encode(file.getBlob().getBytes())
        }, 
        "features": [
          {
            "type": "TEXT_DETECTION",
            "maxResults": 100
          }
        ],
      }
    ]
  });
  
  try {
    var res = UrlFetchApp.fetch("https://vision.googleapis.com/v1/images:annotate", 
                                {
                                  method : 'post',
                                  headers: {
                                    authorization: 'Bearer ' + accessToken
                                  }, 
                                  contentType: 'application/json', 
                                  payload : payload
                                });
  } catch(e) {
    // 失敗
    Logger.log(e);
    return false;
  }
  
  var obj = JSON.parse(res.getContentText());
  var res = obj.responses;
    
  for(var i = 0; i < res.length; i++) {
//    Logger.log(res[i].textAnnotations);
    var textAnnotations = res[i].textAnnotations;
    for(var j = 0; j < textAnnotations.length; j++) {
      Logger.log(textAnnotations[j].description);
    }
  }
  
  return true;
}
```

# 結果
iPadのGoodNotesで出力した画像では、正しく認識しませんでした。文章の一部が文字に起こせたものもありましたが、実用には不十分でした。紙に書いたものをスキャナで取れば、もう少し精度があがるんでしょうか。そもそも、字が汚いという話もありそうですが…。今後の課題です。

# 参考資料
http://qiita.com/soundTricker/items/b4df3072088fcbd0e36d
http://qiita.com/wezardnet/items/ebd0c98f3fc04bcc53a0