はじめに
こんにちは、ハニカムラボのdoshitaです。
先日仕事でUnityのスマホアプリを作る際に、画像の文字をOCRで取得するということを行いました。
ただ、調べていて自分の実現したいことに対してドンピシャの記事が見つからなかったので、備忘録と共有のために今回の実装内容をまとめてみました。
やりたかったこと
Unityで開発したスマホアプリ内で、AndroidとiOSの両方で写真に映っている文字を取得する
開発環境
- Unity
- 2022.3.13f1
- VisualStudio
- 2019
使用したツール
Azure AI Vision
- 選定理由
- 精度が良かった
- 案件内で使用するには余裕があるほど無料枠が提供されていた
- 案件で既にAzureアカウントを作成していた
実装方法
クラウド側の実装
Computer Visionリソースの作成までの部分をこちらの記事を参考に行いました。
Unity側の実装
1. とりあえず楽に実装したい(iOS非対応)
Nugetを介してライブラリをインストールする
とても簡単に実装できましたが、残念ながらiOSでは動作しませんでした。
WindowsやAndroid環境だけであればこの方法で問題ありません。
2. REST APIを使って実装する
今回はiOSとAndroidで動作するアプリを開発していたため、前述の方法は採用できませんでした。
他の方法を調査した結果APIが公開されていたため、REST APIで実装することにしました。
全体のコード
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
public class ComputerVisionOCR
{
string _key = "";
string _uri = "";
public ComputerVisionOCR(string endpoint, string key)
{
if (string.IsNullOrEmpty(key))
{
throw new Exception("key missing.");
}
_key = key;
InitiallizeURI(endpoint);
}
private void InitiallizeURI(string endpoint)
{
if (string.IsNullOrEmpty(endpoint))
{
throw new Exception("endpoint missing.");
}
string requestParameters = "language=ja&detectOrientation=true";
_uri = $"{endpoint}/vision/v3.2/read/analyze?{requestParameters}";
}
public async Task<string> GetOcrData(Texture2D texture)
{
return await ReadTextFromImage(texture);
}
/// <summary>
/// OCR実行
/// </summary>
/// <param name="texture"></param>
/// <returns></returns>
private async Task<string> ReadTextFromImage(Texture2D texture)
{
byte[] imageBytes = texture.EncodeToJPG();
string responseJson = "";
try
{
using (UnityWebRequest request = new UnityWebRequest(_uri, "POST"))
{
RequestSetting(request, imageBytes);
await SendWebRequestAsync(request);
UnityWebRequest resultRequest = ResultRequestSetting(request);
int requestCount = 0;
bool requestSucceeded = false;
//結果の取得(2秒ほど待たなければならない)
while (!requestSucceeded && requestCount < 3)
{
await Task.Delay(2000); // 2秒待機
await SendWebRequestAsync(resultRequest);
requestCount++;
if (GetJsonStatus(resultRequest.downloadHandler.text) == "succeeded")
{
requestSucceeded = true;
}
}
if (!requestSucceeded)
{
throw new Exception("Request failed after 3 attempts.");
}
responseJson = resultRequest.downloadHandler.text;
}
}
catch (Exception e)
{
Debug.LogError("OCRError: " + e.Message);
responseJson = null;
}
return responseJson;
}
/// <summary>
/// POSTのリクエスト設定
/// </summary>
/// <param name="request"></param>
/// <param name="imageBytes"></param>
/// <returns></returns>
private UnityWebRequest RequestSetting(UnityWebRequest request, byte[] imageBytes)
{
if (request == null)
{
throw new Exception("UnityWebRequest missing.");
}
if (imageBytes == null)
{
throw new Exception("Image missing.");
}
request.uploadHandler = new UploadHandlerRaw(imageBytes);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Ocp-Apim-Subscription-Key", _key);
request.SetRequestHeader("Content-Type", "application/octet-stream");
return request;
}
/// <summary>
/// GETのリクエスト設定
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private UnityWebRequest ResultRequestSetting(UnityWebRequest request)
{
string operationLocation = request.GetResponseHeader("Operation-Location");
if (string.IsNullOrEmpty(operationLocation))
{
throw new Exception("Operation-Location header missing.");
}
UnityWebRequest resultRequest = UnityWebRequest.Get(operationLocation);
resultRequest.SetRequestHeader("Ocp-Apim-Subscription-Key", _key);
return resultRequest;
}
/// <summary>
/// APIリクエスト
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private async Task SendWebRequestAsync(UnityWebRequest request)
{
var completionSource = new TaskCompletionSource<bool>();
request.SendWebRequest().completed += _ => completionSource.SetResult(true);
await completionSource.Task;
if (request.result != UnityWebRequest.Result.Success)
{
throw new Exception(request.error);
}
}
/// <summary>
/// Json変換
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private string GetJsonStatus(string json)
{
if (string.IsNullOrEmpty(json))
{
throw new Exception("Json is null or empty");
}
ResponseData responseData = JsonUtility.FromJson<ResponseData>(json);
return responseData.status;
}
}
public class ResponseData
{
public string status;
public string resultText;
}
解説
コンストラクタ
コンストラクタで呼び出す際にAzure Computer Visionのリソースを作成後に表示されるエンドポイントとキー1を引数として使用する(クラウド側の実装の記事参照)
エンドポイントはURI作成に使用
public ComputerVisionOCR(string endpoint, string key)
{
if (string.IsNullOrEmpty(key))
{
throw new Exception("key missing.");
}
_key = key;
InitiallizeURI(endpoint);
}
InitiallizeURI
リクエストパラメーターを設定して、リクエストURIを作成
private void InitiallizeURI(string endpoint)
{
if (string.IsNullOrEmpty(endpoint))
{
throw new Exception("endpoint missing.");
}
string requestParameters = "language=ja&detectOrientation=true";
_uri = $"{endpoint}/vision/v3.2/read/analyze?{requestParameters}";
}
GetOcrData
外部から呼び出す関数
テクスチャを実行クラスに渡す
public async Task<string> GetOcrData(Texture2D texture)
{
return await ReadTextFromImage(texture);
}
ReadTextFromImage
OCRの実行関数
各参照関数は後述
private async Task<string> ReadTextFromImage(Texture2D texture)
{
byte[] imageBytes = texture.EncodeToJPG();
string responseJson = "";
try
{
using (UnityWebRequest request = new UnityWebRequest(_uri, "POST"))
{
RequestSetting(request, imageBytes);
await SendWebRequestAsync(request);
UnityWebRequest resultRequest = ResultRequestSetting(request);
int requestCount = 0;
bool requestSucceeded = false;
//結果の取得(2秒ほど待たなければならない)
while (!requestSucceeded && requestCount < 3)
{
await Task.Delay(2000); // 2秒待機
await SendWebRequestAsync(resultRequest);
requestCount++;
if (GetJsonStatus(resultRequest.downloadHandler.text) == "succeeded")
{
requestSucceeded = true;
}
}
if (!requestSucceeded)
{
throw new Exception("Request failed after 3 attempts.");
}
responseJson = resultRequest.downloadHandler.text;
}
}
catch (Exception e)
{
Debug.LogError("OCRError: " + e.Message);
responseJson = null;
}
return responseJson;
}
RequestSetting
POSTリクエストの設定
private UnityWebRequest RequestSetting(UnityWebRequest request, byte[] imageBytes)
{
if (request == null)
{
throw new Exception("UnityWebRequest missing.");
}
if (imageBytes == null)
{
throw new Exception("Image missing.");
}
request.uploadHandler = new UploadHandlerRaw(imageBytes);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Ocp-Apim-Subscription-Key", _key);
request.SetRequestHeader("Content-Type", "application/octet-stream");
return request;
}
ResultRequestSetting
GETリクエストの設定
private UnityWebRequest ResultRequestSetting(UnityWebRequest request)
{
string operationLocation = request.GetResponseHeader("Operation-Location");
if (string.IsNullOrEmpty(operationLocation))
{
throw new Exception("Operation-Location header missing.");
}
UnityWebRequest resultRequest = UnityWebRequest.Get(operationLocation);
resultRequest.SetRequestHeader("Ocp-Apim-Subscription-Key", _key);
return resultRequest;
}
SendWebRequestAsync
APIリクエスト実行
private async Task SendWebRequestAsync(UnityWebRequest request)
{
var completionSource = new TaskCompletionSource<bool>();
request.SendWebRequest().completed += _ => completionSource.SetResult(true);
await completionSource.Task;
if (request.result != UnityWebRequest.Result.Success)
{
throw new Exception(request.error);
}
}
GetJsonStatus
GETリクエストのステータスを取得
private string GetJsonStatus(string json)
{
if (string.IsNullOrEmpty(json))
{
throw new Exception("Json is null or empty");
}
ResponseData responseData = JsonUtility.FromJson<ResponseData>(json);
return responseData.status;
}
※ハマりポイント
(1) 一部のコードにはURIにocrと書かれたものもあるが、こちらは古いコードで精度も低いので間違えないように注意
(2) POSTリクエストの後すぐにGETリクエストをすると空のまま返ってきてしまうため、POSTリクエスト後1~2秒空けてからGETリクエストを行う
succeeded の値が返されるまで、この操作を対話形式で呼び出します。 1 秒あたりの要求数 (RPS) を超えないようにするために、間隔は 1、2 秒あけてください。
(公式ドキュメントより)
まとめ
この方法で試したところ、掠れたレシートの文字もしっかり読み取ってくれたので大満足でした。
また無料枠でのAPI呼び出し制限が1か月5000回とのことで、小規模であれば無料でも十分使えるのが魅力的ですね。
今後はこれ使って何ができるか考えてみたいと思います。