【Azure／Computer Vision】VBAだってクラウドできる！

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

なお、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")


リクエストは WinHttp.WinHttpRequest でやれ！
---
VBAでWeb APIへリクエストする場合、`MSXML2.XMLHTTP`オブジェクトを使うのが一般的です。ですが、今回のComputer Vision APIへのリクエストのテストにおいて、JSONのポストでは問題ありませんでしたが、バイナリのポストで以下のエラーとなりリクエストが失敗します。

![GEnGyUzakAA-vuj.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/68faef47-093d-8acc-4ce4-4d4c8a1d3efc.jpeg)

「パラメーターが間違っています。」:question::question::question: いやいや間違ってはいないだろう:frowning2: 何か不足しているのか、あるいはバイナリの作り方に問題があるのか、目を凝らしてコードを眺めたり色々と試行錯誤しましたが、結局のところ`MSXML2.XMLHTTP`を諦めて`WinHttp.WinHttpRequest`を使うことで解決に至りました。
以下、VBAでのComputer Vision API利用の一例です。

```vb:Module1.bas

Public Sub AnalyzeLocalImage()
    On Error GoTo Catch
    ' MSXML2.XMLHTTPはボツ
'    Dim httpReq As Object: Set httpReq = CreateObject("MSXML2.XMLHTTP")
    Dim httpReq As Object: Set httpReq = CreateObject("WinHttp.WinHttpRequest.5.1")
    
    ' Azure Computer Vision APIのエンドポイントとキー
    Dim endpoint As String: endpoint = "https://{エンドポイントの名前}.cognitiveservices.azure.com/vision/v3.2/read/analyze"
    Dim subscKey As String: subscKey = "{キー}"

    ' ローカルの画像ファイルパス
    Dim imagePath As String: imagePath = "C:\{ファイルパス}\{ファイル名}.pdf"

    ' 画像ファイルをバイナリデータとして読み込む
    Dim imageBytes() As Byte: imageBytes = GetBinaryDataFromImage(imagePath)
    
    ' リクエストの実行
    httpReq.Open "POST", endpoint, False
    httpReq.SetRequestHeader "Content-Type", "application/octet-stream"
    httpReq.SetRequestHeader "Ocp-Apim-Subscription-Key", subscKey
    httpReq.Send imageBytes

    ' レスポンスの確認
    If httpReq.Status = 202 Then
        ' 成功した場合の処理
        Dim requestUrl As String: requestUrl = httpReq.GetResponseHeader("Operation-Location")
        Do
            ' 少し待機（例えば2秒）
            Call Application.Wait(Now + TimeValue("0:00:02"))
            
            ' 同じURLに繰り返しGETリクエストを行う（status が running の間）
            httpReq.Open "GET", requestUrl, False
            httpReq.SetRequestHeader "Ocp-Apim-Subscription-Key", subscKey
            httpReq.Send
            If httpReq.Status <> 200 Then
                Debug.Print "Error: " & httpReq.Status & " - " & httpReq.StatusText & " - " & httpReq.ResponseText
                GoTo Finally
            End If
            ' レスポンスを取得
            Dim jsonRes As String: jsonRes = httpReq.ResponseText
        
            ' レスポンスが running 以外ならループを抜ける
            If InStr(jsonRes, """status"":""running""") = 0 Then
                Exit Do
            End If
        Loop
        ' JSON Converterを使用してJSONをパース
        Dim json As Object
        Set json = JsonConverter.ParseJson(jsonRes)
        
        ' analyzeResultフィールドを取得
        Dim readResults As Object
        If json.Exists("analyzeResult") Then
            
            Set readResults = json("analyzeResult")("readResults")
            Dim fPage As Object, fLine As Object
            ' すべてのページをループ
            For Each fPage In readResults
                ' JSONデータ内のテキストデータをループ
                For Each fLine In fPage("lines")
                    Debug.Print fLine("text")
                Next
            Next
        Else
            ' analyzeResultフィールドが存在しない場合の処理
            Debug.Print "analyzeResult not found"
        End If
    Else
        ' エラーの場合の処理
        Debug.Print "Error: " & httpReq.Status & " - " & httpReq.StatusText & " - " & httpReq.ResponseText
    End If
    GoTo Finally
Catch:
    MsgBox Err.Description, vbExclamation
Finally:
    Set httpReq = Nothing
End Sub

Private Function GetBinaryDataFromImage(imagePath As String) As Byte()

    Dim sm As Object: Set sm = CreateObject("ADODB.Stream")
    
    ' Stream オブジェクトの設定
    With sm
        .Open
        .Type = 1 ' adTypeBinary
        .Position = 0
        .LoadFromFile imagePath ' ファイルを読み込む
        GetBinaryDataFromImage = .Read ' バイナリデータとして読み出す
        .Close
    End With
    
    Set sm = Nothing

End Function

```
あくまでも一例なので、キーやエンドポイントが丸見え（:see_no_evil:）なのはご愛嬌です。実用する場合はこんな書き方は止めましょう:no_good:
エンドポイントの書き方は以下を参考にしました。

https://learn.microsoft.com/ja-jp/azure/ai-services/computer-vision/how-to/call-read-api#submit-data-to-the-service

レスポンスのJSONのパースには、毎度おなじみの`JsonConverter`を使用しています。
`JsonConverter`は、**Microsoft Scripting Runtime** への参照設定が必須となることに注意してください。

https://github.com/VBA-tools/VBA-JSON

精度が高くて大満足
---
とにかく読み取り精度が高いことに感動しました。手書きも見事に読み取ります。
パラメータとして`?language=ja`のように読み取る言語を指定したり、PDFファイルなら`?pages=1-2,5`のようにページ指定も可能です。
読み取ったすべての文字列は`boundingBox`というフィールドに四隅の座標を持っています。これを解析すれば、読み取り結果を適切にレイアウトする助けになります。
さて、ここからは利用者の腕次第ということになりますね:sunglasses:

おわりに
---
クラウドに傾倒している世の中ですが、使い方次第でクラウド環境も活用することができるVBA。あらためてお手軽で本当に便利だと思いました:laughing: