Azure Computer Vision でOCRする

はじめに
---
この記事では、Azure Computer Vision のOCR機能を利用する方法を書きます。

なお、Azureに限らずクラウドサービスは思わぬ課金発生を招くことがありますので、ご利用は自己責任でお願いいたします。

Computer Vision とは
---
Computer Vision とは、AzureのAI サービスの一つで、その中に画像ファイルやPDFファイルから文字列を読み取るOCR機能が含まれています。
利用にはAzureアカウントの作成が必要になります。以下、Azureアカウントは作成済みという前提で話を進めます。

Computer Visionリソースの作成
---
まずはAzureポータルへ行き、**vision**で検索します。すると **Computer Vision** が現れるのでクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/5b070c43-adab-12e3-33e6-e6f3fe38fbe1.png)

<br>

画面下部の **Computer Visionの作成** をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/cad017bf-38e3-825d-eca2-4e62197d36a1.png)

<br>
リソースグループは既存のものがあればそれでもよし。ここでは新規作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/b76a5c83-4a79-1e3a-55ef-6372618bc1d7.png)

<br>

**新規作成** をクリックしてリソースグループの名前を決めて入力。ここでは、Japan Eastリージョンの意味で`jp-e`を頭につけて`jp-e-webapp`としました。**OK**をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/b62b57d4-8e71-91e4-3d6b-e0a87cec8059.png)
<br>
リージョンを選び、自分で決めたリソースの名前を入力します。価格レベルは、ここでは`Free F0`を選択します。英語で書いてありますが、1 分あたり 20 件のトランザクションかつ月当たり5,000 無料トランザクション使えます。1トランザクションはPDFファイル 1ページが目安のようです。
「このボックスをオン...」にチェックを入れて**確認と作成**をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/3929dbb1-e873-6c44-d67e-541795ce4312.png)


https://azure.microsoft.com/ja-jp/pricing/details/cognitive-services/computer-vision/

<br>

**作成**をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/b592f7c7-6ec9-6a73-e420-efe3eb03fd63.png)
<br>
**リソースに移動**をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/06fd8d5e-efc9-2f5a-e166-5cbd192787c5.png)
<br>
**キー1**と**エンドポイント**を控えておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/0e844888-f16c-92b1-d840-e238f4420bd5.png)

大まかな流れ
---
上で得たエンドポイントに画像をPOSTします。画像はクラウド上のものであればURLをJSONで送ります。ローカルからであればファイルをバイナリに変換して送ります。

リクエストが成功すると読み取り結果ではなく、読み取り結果が格納された`Operation Id`（URLのこと）を返してきます。このURLにGETすることで読み取り結果が収められたJSONを得られます。
以下のリンクは、Computer Visionの中でもテキストを多用する画像や複数ページ・複数言語混在のPDF文書などから文字列を読み取ることに最適化されている **Readオペレーション** のリファレンスです。

[Computer Vision API - POST Read のリファレンス](https://westus.dev.cognitive.microsoft.com/docs/services/computer-vision-v3-2/operations/5d986960601faab4bf452005 "POST Read")

[Computer Vision API - GET Read Result のリファレンス](https://westus.dev.cognitive.microsoft.com/docs/services/computer-vision-v3-2/operations/5d9869604be85dee480c8750 "GET Read Result")

テキストの抽出とログの出力
---
以下のコードは、Microsoft Learnの「[**クイック スタート: Azure AI Vision v3.2 GA Read**](https://learn.microsoft.com/ja-jp/azure/ai-services/computer-vision/quickstarts-sdk/client-library?tabs=windows%2Cvisual-studio&pivots=programming-language-csharp#read-printed-and-handwritten-text)」を参考にして書きました。
読み取ったテキストの他にリクエストやレスポンスの内容も同時に出力させています。

```C#:Program.cs
// トップレベルステートメント
using Microsoft.Azure.CognitiveServices.Vision.ComputerVision;
using Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models;

﻿﻿// Computer Vision のキーとエンドポイントを環境変数から取得
string? key = Environment.GetEnvironmentVariable("COMPUTER_VISION_KEY", EnvironmentVariableTarget.User);
string? endpoint = Environment.GetEnvironmentVariable("COMPUTER_VISION_ENDPOINT", EnvironmentVariableTarget.User);

// サービスクライアントの作成。クライアントパイプラインに LoggingHandler を追加
ComputerVisionClient client = new(new ApiKeyServiceClientCredentials(key),
                                  new DelegatingHandler[] { new LoggingHandler() })
{
    Endpoint = endpoint
};

// テキスト抽出対象のローカルファイルのパス
string localFilePath = @"C:\{パス}\{ファイル名}.pdf";

// Read API を使ってローカルファイルからテキストを抽出
ReadFileBinary(client, localFilePath).Wait();


// Read API を使ってローカルファイルからテキストを抽出
static async Task ReadFileBinary(ComputerVisionClient client, string filePath)
{
    // ファイルをバイト配列に読み込む
    byte[] byteData = await File.ReadAllBytesAsync(filePath);
    using MemoryStream stream = new(byteData);

    // バイナリのボディと共にパラメータを指定してリクエストをPOSTする
    // ここでは language=ja を設定
    ReadInStreamHeaders textHeaders = await client.ReadInStreamAsync(stream, "ja");

    // レスポンスからOperation IDを取得
    string operationLocation = textHeaders.OperationLocation;

    // Operation-LocationヘッダーからIDだけ取得。完全なURLは必要ない
    string operationId = operationLocation[(operationLocation.LastIndexOf('/') + 1)..];

    // テキストを抽出する
    ReadOperationResult results;
    Console.WriteLine($"テキスト抽出 (ファイル名: {Path.GetFileName(filePath)})");
    do
    {
        // 2秒待機
        Thread.Sleep(2000);
        // ステータスがRunningまたはNotStartedの間、繰り返しGETする
        results = await client.GetReadResultAsync(Guid.Parse(operationId));
    }
    while (results.Status == OperationStatusCodes.Running ||
           results.Status == OperationStatusCodes.NotStarted);

    // 見つかったテキストを表示する
    Console.WriteLine();
    IList<ReadResult> readResults = results.AnalyzeResult.ReadResults;
    foreach (ReadResult page in readResults)
    {
        foreach (Line line in page.Lines)
        {
            Console.WriteLine(line.Text);
        }
    }
    Console.WriteLine();
}

```
以下は上述コードの最初に登場する`ComputerVisionClient`クラスのコンストラクタに渡しているカスタムハンドラークラスです。これでリクエストの前やレスポンスの後にログ出力の処理を挟み込んでいます。

```C#:LoggingHandler.cs
public class LoggingHandler : DelegatingHandler
{
    public LoggingHandler() : base() { }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // リクエストの内容を出力
        Console.WriteLine($"Request: {request}\n");
        if (request.Content != null)
        {
            // リクエストのボディが人の目で読めるメディアタイプであれば出力
            Console.WriteLine("Body:");
            string mediaType = request.Content.Headers.ContentType?.MediaType ?? "";
            if (mediaType != "")
            {
                if (IsReadableMediaType(mediaType))
                {
                    string requestContent = await request.Content.ReadAsStringAsync(cancellationToken);
                    Console.WriteLine(requestContent);
                }
                else
                {
                    Console.WriteLine($"メディアタイプ '{mediaType}' は表示しない。");
                }
            }
        }
        Console.WriteLine();

        // レスポンスを取得
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

        // レスポンスの内容を出力
        Console.WriteLine($"Response: {response}\n");
        Console.WriteLine("Body:");
        if (response.Content != null)
        {
            string responseContent = await response.Content.ReadAsStringAsync();
            Console.WriteLine(responseContent);
        }
        Console.WriteLine();

        return response;
    }

    // メディアタイプが人の目で読めるものかどうか判定
    private bool IsReadableMediaType(string mediaType)
    {
        List<string> readableMediaTypes =
        [
            "application/json",
            "application/xml",
            "application/csv",
            "application/x-yaml",
            "application/xhtml+xml",
            "text/xml",
            "text/csv",
            "text/html",
        ];
        return readableMediaTypes.Contains(mediaType);
    }
}

```
本題から逸れますが、このパイプラインはASP.NETの根幹をなす仕組みで、理解しておきたいポイントです。（クライアント側のみならず、サーバー側も同様の仕組みを持ちます）

https://learn.microsoft.com/ja-jp/aspnet/web-api/overview/advanced/httpclient-message-handlers

`ComputerVisionClient`クラスは`ServiceClient<T>`クラスを継承しています。`ServiceClient<T>`クラスは、AzureサービスとのHTTP通信のメカニズムをカプセル化し、リクエストの作成やレスポンスの解析に関する詳細を気にすることなく、サービスを簡単に利用することを目的としたものです。しかし、以下のリファレンスを見てもメンバが列挙されているだけなので、利用目的がハッキリしていないと溺れてしまいそうです:sweat_smile:

https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.azure.cognitiveservices.vision.computervision.computervisionclient?view=azure-dotnet

このサンプルを動作させるには、プロジェクトに**Microsoft.Azure.CognitiveServices.Vision.ComputerVision**をインストールする必要があります。

https://www.nuget.org/packages/Microsoft.Azure.CognitiveServices.Vision.ComputerVision/7.0.1?_src=template

精度が高くて大満足
---
色々なファイルで試してみましたが、とにかく読み取り精度が高いことに感動しました。手書きも見事に読み取ります。
上記のコードでは`ReadInStreamAsync`メソッドの引数として`ja`を設定して読み取る言語を日本語に指定しています。PDFファイルなら第3引数でページ指定も可能です。
ログを見れば分かりますが、読み取ったすべての文字列は`boundingBox`というフィールドに四隅の座標を持っています。これを解析すれば、読み取り結果を適切にレイアウトする助けになります。

おわりに
---
制限こそあれ、無料でこの様な機能が使えるAzureは、お手軽で本当に便利ですね。気がつけば無料では飽き足らず、サービスにどっぷりと浸かってしまうのでしょうか:laughing: