Vision APIのレスポンスデータとJSONやディクショナリを相互変換する(Proto Plus for Python、google.protobuf.json_format)

本記事では、Proto Plus for Pythonやgoogle.protobuf.json_formatのメソッドを利用して、主にVision APIのレスポンスデータをJSONやディクショナリと相互に変換する方法を見ていきます。

【目次】

[1]はじめに

Google Vision APIのPythonクライアントライブラリを利用して画像から情報検出を行うと、そのレスポンスデータ(検出結果)は、Protocol Buffersで定義された形式で返却されます。

型付けされたProtocol Buffersのデータは、プログラムで扱うのには便利なのですが、JSON形式やディクショナリと相互に変換したい場合もあります。

例えば、以下のようなケースです。
  • JavaやJavaScript等の他の言語でも処理しやすいように、JSON形式で保存しておきたい。
  • Vision APIのAsyncBatchAnnotateImagesとAsyncBatchAnnotateFilesのレスポンスは、Cloud StorageにJSON形式で格納される。一方で、プログラムでの処理はProtocol Buffersのオブジェクトで処理したい。
  • データの可視化ツールやライブラリを利用したり、データベースなどに格納するとき、JSONあるいはディクショナリのデータ形式に変換したい場合がある。
    • 要求される形式がJSONやディクショナリ以外であっても、JSONやディクショナリのような汎用型に変換すれば、目的の形式に変換できるツールやライブラリが使える場合もあるかもしれません。
  • プログラム処理は、Protocol BuffersかJSON形式のどちらかに統一したい。

個人的には、JSONに変換できれば、ディクショナリやその他の形式への変換が簡単になって、様々な局面でデータの取り回しが良くなることが相互変換したい理由です。

そこで、本記事では、Vision APIのレスポンスデータとJSONとディクショナリ形式を相互変換する方法を見ていきます。

ちなみに、本記事の内容は、Vision APIのレスポンスデータを題材にしていますが、Proto Plus for Pythonを利用しているGoogle APIに対しては同様に扱えると思います。

(注意)
本記事の内容は、Vision API Pythonクライアントライブラリのバージョン2.2.0で動作検証しています。
バージョン2以前(1.0.0)での相互変換については、記事『Vision API Pythonクライアントライブラリを少し深堀りする(BatchAnnotateImages編)/[9]JSON、ディクショナリとの相互変換』を参照して下さい。

[2]Vision APIのレスポンスデータ

以下のコードは、Google Vision APIのOCR(document_text_detection)を利用して、画像からテキスト抽出を行っています。
from google.cloud import vision
client = vision.ImageAnnotatorClient()
response = client.document_text_detection({'source': {'filename': imagefilename} })

document_text_detectionの抽出結果(response変数)は、AnnotateImageResponseクラスのインスタンスです(vision.AnnotateImageResponseで参照できます)。

以下では、上記コードのようなVision API Pythonクライアントライブラリを利用して取得したレスポンスデータ(response)と、JSONあるいはディクショナリと相互変換していきます。

なお、結果のクラス(上記の例ではAnnotateImageResponse)は、実際に利用するAPIに応じて読み替えてください。

(参考)
上記のコード内容については、以下の記事も参考にしてください。

[3]Proto Plus for Pythonのメソッド(to_json,from_json,to_dict)を利用した変換

(1)Proto Plus for Python

Vision APIのPythonクライアントライブラリのバージョン2.0.0以降は、Protocol Buffersで定義しているメッセージを、Proto Plus for Pythonを利用して扱っているようです。

実際に、Vision APIのPythonクライアントライブラリのソースコードを見てみると、依存関係に"proto-plus"があります。また、types以下にあるAnnotateFileResponse等のクラスは、proto.Message(Proto Plus for Pythonの型)から派生していることがわかります。
このproto.Messageのドキュメントは以下にあります。

このドキュメントのSerializationのところに、JSONやディクショナリに関する相互変換の記述があります。

以下では、この情報を元に、相互変換方法と利用例、留意事項をまとめます。

(2)JSONとの相互変換

①to_json:レスポンスデータ=>JSON 変換

JSONデータ = クラスの型.to_json( レスポンスデータ [,オプション引数])

to_jsonには以下のオプション引数も指定できます。
  • use_integers_for_enums
    • Falseの場合、列挙値を文字列で表します。Trueの場合は整数で表します。
    • デフォルト値はTrueです。
  • including_default_value_fields
    • Falseの場合、空でないフィールドのみシリアル化されます。Trueの場合は単一のプリミティブフィールド、繰り返しフィールド、およびマップフィールドは常にシリアル化されます。
    • デフォルト値はTrueです。

②from_json:JSON=>レスポンスデータ 変換

レスポンスデータ = クラスの型.from_json( JSONデータ  [,オプション引数])

from_jsonには以下のオプション引数も指定できます。
  • ignore_unknown_fields
    • Falseの場合、JSONデータに不明なフィールドがあるとエラーが発生しますが、Trueの場合はエラーになりません。
    • デフォルト値はFalseです。

③利用例

以下のコードで、[2]で取得したAnnotateImageResponseクラスのレスポンスデータとJSONを相互変換できます。
# JSONへ変換
js_data = vision.AnnotateImageResponse.to_json(response)
# JSONから再現
pb_from_js = vision.AnnotateImageResponse.from_json(js_data)

なお、レスポンスデータからJSONに変換する場合は、以下のように、クラス名を明示せず、レスポンスデータからクラスを取得して to_json メソッドを利用できます。
# レスポンスからクラスを取得してJSONへ変換
js_data = response.__class__.to_json(response)

この方法は、コードにレスポンスのクラスを明示しないため、利用ケースによっては汎用性が高いと思います。

上記例では to_json、from_jsonともにオプション引数を指定していませんが、必要に応じてオプション引数を指定します。

④to_jsonメソッドの注意事項

to_jsonメソッドでJSONに変換すると、Protocol BuffersのMessageで定義されるフィールド名とJSONのフィールド名が異る場合がありますので注意してください。いわゆるスネークケースとキャメルケースの違いです。

例えば、Protocol Buffersのフィールド名はスネークケースで text_annotations なのに対し、JSONのフィールド名はキャメルケースで textAnnotations になります。
これは、Vision APIのRPCとRESTの定義の違いに対応しています。

このフィールド名の変換が問題になる場合の回避策は、「[4]google.protobuf.json_formatを利用した変換」を参照して下さい。

(3)ディクショナリとの相互変換

①to_dict:レスポンスデータ=>ディクショナリ 変換

ディクショナリデータ = クラスの型.to_dict( レスポンスデータ  [,オプション引数] )

以下のオプション引数も指定できます。
  • use_integers_for_enums
    • Falseの場合、列挙値を文字列で表します。Trueの場合は整数で表します。
    • デフォルト値はTrueです。

②ディクショナリ=>レスポンスデータ 変換

レスポンスデータ = クラスの型( ディクショナリデータ )

クラスのコンストラクタを利用して変換します。(Vision APIの呼び出しパラメータをディクショナリで指定できるのと同じ方法です。)
このため、from_dictのような変換メソッドはありません。

③利用例

以下のコードで、[2]で取得したAnnotateImageResponseクラスのレスポンスデータとディクショナリを相互変換できます。
# ディクショナリへ変換
dict_data = vision.AnnotateImageResponse.to_dict(response)
# ディクショナリから再現
pb_from_dict = vision.AnnotateImageResponse(dict_data)

JSON変換と違って、Protocol Buffersのフィールド名規則であるスネークケースのまま変換されます。(text_annotations フィールドは text_annotations フィールドのままです。)

また、to_jsonと同様に、レスポンスデータからJSONに変換する場合は、以下のように、クラス名を明示せず、レスポンスデータからクラスを取得する方法もあります。
# レスポンスからクラスを取得してディクショナリへ変換
dict_data = response.__class__.to_dict(response)

上記例では to_dictにオプション引数を指定していませんが、必要に応じてオプション引数を指定します。

[4]google.protobuf.json_formatを利用した変換

(1)google.protobuf.json_format

もともとProtocol Buffersのライブラリには、JSONやディクショナリと相互変換する機能を持っています。
Proto Plus for Pythonにあるto_jsonメソッドのコードを見ると、内部的にjson_formatのMessageToJsonを呼び出していることが分かります。

json_format を利用すると、Proto Plusより詳細な制御を行うことができます。
例えば、to_jsonメソッドでは、フィールド名の変換が強制的に行われますが、json_formatを用いると変換を制御することができます。また、ディクショナリ変換でもフィールド名の変換を制御できます。

なお、Proto Plus for Python は、protocol buffers のラッパーなのに対して、json_format は protocol buffers を扱うため、Proto Plus for Python のオブジェクトを扱う場合は、 protocol buffers のオブジェクトへの変換(取り出し)が必要になります。

以下では個々のオプション引数はドキュメントを参照して頂くことにして割愛し、 json_format にある MessageToJson/Parse/MessageToDict/ParseDict メソッドの基本的な利用方法を見ていきます。

(参考)
json_format を含むProtocol Buffersのライブラリの情報は以下になります。

(2)MessageToJson:レスポンスデータ=>JSON 変換

以下のコードでレスポンスデータをJSONに変換できます。
from google.protobuf.json_format import MessageToJson
js_data = MessageToJson( response.__class__.pb( response ) )

引数には、Proto Plusのインスタンスではなく、Protocol Buffersのメッセージのインスタンスを指定する必要があります。そこで、Proto PlusのMessageクラスのpbメソッドを利用してインスタンスを取り出します。これを行っているのが response.__class__.pb になります。
ちなみに、これは以下のように具体的なクラス(今回の例ではvision.AnnotateImageResponse)を利用しても同じです。
from google.protobuf.json_format import MessageToJson
js_data = MessageToJson( vision.AnnotateImageResponse.pb( response ))

また、以下のオプション引数を指定することができます。
  • preserving_proto_field_name
    • True の場合、キャメルケースへの変換は行われず、Protocol Buffersで定義されたフィールド名のままJSON文字列に変換されます。Falseの場合はキャメルケースへの変換が行われます。
    • デフォルト値はFalseです。
  • use_integers_for_enums
    • Falseの場合、列挙値を文字列で表します。Trueの場合は整数で表します。
    • デフォルト値はFalseです。
  • including_default_value_fields
    • Falseの場合、空でないフィールドのみシリアル化されます。Trueの場合は単一のプリミティブフィールド、繰り返しフィールド、およびマップフィールドは常にシリアル化されます。
    • デフォルト値はFalseです。
  • indent、sort_keysなど、その他のオプション引数についてはドキュメントを参照して下さい。

(注意)
to_json と MessageToJson のオプション引数である use_integers_for_enumsとincluding_default_value_fieldsのデフォルト値の違いに注意してください。
to_jsonではデフォルト値がTrueであるのに対して、MessageToJsonではFalseとなっています。

以下のようにMessageToJsonの引数を指定するとto_jsonと同じ結果になります。
js_data_1 = response.__class__.to_json(response)
js_data_2 = MessageToJson( response.__class__.pb( response ),
                   use_integers_for_enums=True, including_default_value_fields=True )

(3)Parse:JSON=>レスポンスデータ 変換

以下のコードでJSONからレスポンスデータの型に変換できます。
from google.protobuf.json_format import Parse
pb_from_js = vision.AnnotateImageResponse()
Parse( js_data, pb_from_js._pb )

Parseの第一引数にはJSON文字列、第二引数にProtocol Buffersのクラスのインスタンスを指定することで、インスタンスにJSONの内容を取り込んでくれます。

このため、pb_form_jsにProto Plusのメッセージのインスタンスを作成して、その _pb属性を第二引数に渡しています。なお、上記コード例は、 from_jsonのコードを参考にしました。

なお、JSONのフィールド名が、キャメルケースとスネークケースのどちらでも取り込んでくれます。

また、以下のオプション引数を指定することができます。
  • ignore_unknown_fields
    • Falseの場合、JSONデータに不明なフィールドがあるとエラーが発生しますが、Trueの場合はエラーになりません。
    • デフォルト値はFalseです。

(4)MessageToDict:レスポンスデータ=>ディクショナリ 変換

以下のコードでレスポンスデータをディクショナリに変換できます。
from google.protobuf.json_format import MessageToDict
dict_data = MessageToDict( response.__class__.pb( response ) )

引数には、Proto Plusのインスタンスではなく、Protocol Buffersのメッセージのインスタンスを指定する必要があります。そこで、Proto PlusのMessageクラスのpbメソッドを利用してインスタンスを取り出します。これを行っているのが response.__class__.pb になります。

ちなみに、これは以下のように具体的なクラス(今回の例ではvision.AnnotateImageResponse)を利用しても同じです。
from google.protobuf.json_format import MessageToDict
dict_data = MessageToDict( vision.AnnotateImageResponse.pb( response ) )

また、以下のオプション引数を指定することができます。
  • preserving_proto_field_name
    • True の場合、キャメルケースへの変換は行われず、Protocol Buffersで定義されたフィールド名のままJSON文字列に変換されます。Falseの場合はキャメルケースへの変換が行われます。
    • デフォルト値はFalseです。
  • use_integers_for_enums
    • Falseの場合、列挙値を文字列で表します。Trueの場合は整数で表します。
    • デフォルト値はFalseです。
  • including_default_value_fields
    • Falseの場合、空でないフィールドのみシリアル化されます。Trueの場合は単一のプリミティブフィールド、繰り返しフィールド、およびマップフィールドは常にシリアル化されます。
    • デフォルト値はFalseです。
  • その他のオプション引数についてはドキュメントを参照して下さい。

to_dictと違って、MessageToDictはオプション引数を指定するとフィールド名の変換の制御が可能です。

(注意)
to_dict と MessageToDict のオプション引数である use_integers_for_enumsとincluding_default_value_fieldsのデフォルト値の違いに注意してください。
to_dictではデフォルト値がTrueであるのに対して、MessageToJsonではFalseとなっています。

また、to_dict は MessageToDict の preserving_proto_field_name 引数をTrueで呼び出しているのに対して、MessageToDictのデフォルト値はFalseです。
つまり、to_dict ではフィールド名がスネークケースのままでしたが、MessageToDictで引数を指定しない場合は、キャメルケースに変換されます。

以下のようにMessageToDictの引数を指定するとto_jsonと同じ結果になります。
dict_data_1 = response.__class__.to_dict(response)
dict_data_2 = MessageToDict( response.__class__.pb( response ),
                   use_integers_for_enums=True, including_default_value_fields=True,
                   preserving_proto_field_name=True )

(5)ParseDict:ディクショナリ=>レスポンスデータ 変換

以下のコードでディクショナリからレスポンスデータの型に変換できます。
from google.protobuf.json_format import ParseDict
pb_from_dict = vision.AnnotateImageResponse()
ParseDict( dict_data, pb_from_dict._pb )

ParseDictの第一引数にはディクショナリ、第二引数にProtocol Buffersのクラスのインスタンスを指定することで、インスタンスにディクショナリの内容を取り込んでくれます。

このため、pb_form_dictにProto Plusのメッセージのインスタンスを作成して、その _pb属性を第二引数に渡しています。

なお、ディクショナリのキー文字列が、キャメルケースとスネークケースのどちらでも取り込んでくれます。

また、以下のオプション引数を指定することができます。
  • ignore_unknown_fields
    • Falseの場合、JSONデータに不明なフィールドがあるとエラーが発生しますが、Trueの場合はエラーになりません。
    • デフォルト値はFalseです。

同じことは、クラスのコンストラクタを利用して変換できますが、ParseDictを利用するメリットは、オプション引数 ignore_unknown_fieldsを利用して、不明なフィールドを無視した変換ができることかと思います。

[5](参考?)V1.0.0との違いなど

json_format を使ったレスポンスデータと、JSONやディクショナリの相互変換については、既に2020年9月の記事『Vision API Pythonクライアントライブラリを少し深堀りする(BatchAnnotateImages編)/[9]JSON、ディクショナリとの相互変換』で書いていました。

しかし、久しぶりに Python クライアントライブラリの最新版(v2.2.0)を使って json_formatを使ったら、以下のようなエラーが出ました(涙)。

<実行コード>
from google.protobuf.json_format import MessageToJson
js = MessageToJson( response )
<エラー内容>
KeyError                                  Traceback (most recent call last)
/usr/local/lib/python3.7/dist-packages/proto/message.py in __getattr__(self, key)
    559         try:
--> 560             pb_type = self._meta.fields[key].pb_type
    561             pb_value = getattr(self._pb, key)

KeyError: 'DESCRIPTOR'

During handling of the above exception, another exception occurred:

AttributeError                            Traceback (most recent call last)

4 frames

/usr/local/lib/python3.7/dist-packages/proto/message.py in __getattr__(self, key)
    563             return marshal.to_python(pb_type, pb_value, absent=key not in self)
    564         except KeyError as ex:
--> 565             raise AttributeError(str(ex))
    566 
    567     def __ne__(self, other):

AttributeError: 'DESCRIPTOR'

2020年10月に、記事『Vision API Pythonクライアントライブラリ v2.0.0リリース(BREAKING CHANGES 有り)』で、Python クライアントライブラリの大きな変更があったことは書いていたのですが、google.protobuf.json_format のことはすっかり抜けていました。

そこで、再度以下のドキュメントを読んでみました。

ここに、next-gen code generatorに基づく重要なアップグレードがあると書いてあり、以下の情報にリンクされています。

これは「protocol buffersで定義されたAPIに対するクライアントライブラリのジェネレータ」である旨の記載があります。
さらに上記ドキュメントによると、client library generators specificationを実装するライブラリのジェネレータであるとのことです。

なかなか興味深いので読んでみようとは思ったものの、奥が深すぎて調べるのに時間がかかりそうだったので(ライブラリのビルド方法に興味が無いわけではありませんが、今はそれを知りたかったわけではないし)、まずは実際のエラーとクライアントライブラリのソースコードから対策を考えることにしました。


エラーの場所は、proto/message.py を指していました。
また、MessageToJson に渡したresponseは AnnotateImageResponse 型なので、このクラスのソースコードを見ると、
  class AnnotateImageResponse(proto.Message):
と定義されています。

実際に、以下のコードでクラス階層を調べてみました。
import inspect
for item in inspect.getmro(type(response)):
  print(item)
<実行結果>
<class 'google.cloud.vision_v1.types.image_annotator.AnnotateImageResponse'>
<class 'proto.message.Message'>
<class 'object'>

さらに、setup.pyを見ると、dependenciesに"proto-plus >= 1.4.0"とありました。

これらのことから Proto Plus for Python に行きついて、本記事の内容を書きました。

クライアントライブラリの v2.0.0 から、Protocol Buffers に Proto Plus というラッパーがかぶさったことで、json_formatをそのまま利用できなかったということのようです。

ところで、Vision API Python Client Library のChangeLogによると、V1.0.0が2020年の3月、V2.0.0が2020年の9月と1年もたたないうちにBREAKING CHANGESです(涙)。
Vision APIに限らず、またGoogleのサービスに限ったものでもありませんが、いろいろな技術が相互に影響して、凄い速度で進化(変化?)していることを実感します。

コメント

このブログの人気の投稿

VirtualBoxのスナップショット機能

Google Document AIで画像から表形式データを抽出する(Vision API OCRとの違い)

Ubuntu/Colab環境でPDFファイルのページを画像化する(pdf2image、pdftoppm、pdftocairo)