少々古い記事となるのですが日経ソフトウェア 2018年1月号を読んでいたら、「人工知能API」で画像認識プログラミング、という記事がありまして、Google Vision API や Microsoft Cognitive Service, Amazon Rekognition を利用してみる、というものでした。
記事を読んでみると、人工知能APIと言いつつも、やっていることは REST API に画像を投げて、クラウド側の機械学習モデルを使って判定させているだけでしたので、これは Delphi/C++Builder でもできそうです。
そこで、この記事を参考にしつつ Google Vision API を Delphi で利用する例をご紹介します。
お手元に書籍がございましたら、比較しつつ記事を読んでみてください。
なお Google は Vision API の既存の機械学習モデルをベースにカスタマイズ可能なサービス、Cloud AutoML を発表していますが、この記事には Cloud AutoML の内容は含めていません。
Table of Contents
REST API を叩く方法は?
REST API を利用する方法には2つの方法、HTTP クライアントを使う方法と、RESTクライアントライブラリを使う方法があります。
ただし HTTP クライアントを利用する方法では、URLの組み立てや受け取ったJSONデータの取扱いが若干不便です。
このために本記事では RESTクライアントライブラリを利用します。
JSON はどのように取り扱う?
Delphi では JSON を取り扱うためのフレームワークとして下記2種類を提供しています。
- JSONオブジェクトフレームワーク
- JSONリーダー/ライターフレームワーク
JSONオブジェクトフレームワークを用いると、データ構造の最も深い部分から浅い部分に向けてボトムアップ的にJSONデータを作ることができますし、JSONリーダー/ライターフレームワークでは浅い階層から掘り下げていくような形でデータを作ることができます。
これについては http://docwiki.embarcadero.com/RADStudio/Tokyo/ja/JSON を見ていただくとよいでしょう。
ただし、これらのフレームワークを使わずともJSONのデータを作ることも可能です。JSONは {“key”:”value”, “key2”:”value2”, … } のような形式で構成されるテキストデータですので、形式が論理的に正しい限り、どのような方法で作っても構いません。
また、データの読み出しについても、JSONオブジェクトフレームワークとJSONリーダーフレームワークでは差異があります。
そこでこの記事では、JSONフレームワークを使わずにリクエストデータを作成する方法、JSONリーダー/ライターフレームワークを使ってリクエストデータを作成する方法を例示してみます。
REST API に送信するデータを作る
今回のデータ形式
Google Vision API に投げるリクエストのJSONはこんな形で投げます。画像データはBase64エンコードしたものを下記のデータ内に差し込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "<span class="hljs-attribute">requests</span>": <span class="hljs-value">[ { "<span class="hljs-attribute">image</span>": <span class="hljs-value">{ "<span class="hljs-attribute">content</span>": <span class="hljs-value"><span class="hljs-string" style="color:#0000ff;font-weight:bold;">"[画層のBase64データ]"</span> </span>}</span>, "<span class="hljs-attribute">features</span>": <span class="hljs-value">[ { "<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string" style="color: #0000ff; font-weight: bold;">"LANDMARK_DETECTION"</span> </span>} ] </span>} ] </span>} |
取り扱う内容はシンプルですが、入れ子の階層は案外深いかもしれません。
この形式でデータを投げると、Google Vison API で判定が成功した場合は以下のような形式で緯度経度の情報を含むJSONが返されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
{ "<span class="hljs-attribute">responses</span>": <span class="hljs-value">[ { "<span class="hljs-attribute">landmarkAnnotations</span>": <span class="hljs-value">[ { "<span class="hljs-attribute">mid</span>": <span class="hljs-value"><span class="hljs-string" style="color:#0000ff;font-weight:bold;">"/m/03k987"</span></span>, "<span class="hljs-attribute">description</span>": <span class="hljs-value"><span class="hljs-string" style="color:#0000ff;font-weight:bold;">"Hōzōmon"</span></span>, "<span class="hljs-attribute">score</span>": <span class="hljs-value"><span class="hljs-number">0.95834094</span></span>, "<span class="hljs-attribute">boundingPoly</span>": <span class="hljs-value">{ "<span class="hljs-attribute">vertices</span>": <span class="hljs-value">[ { "<span class="hljs-attribute">x</span>": <span class="hljs-value"><span class="hljs-number">186</span></span>, "<span class="hljs-attribute">y</span>": <span class="hljs-value"><span class="hljs-number">31</span> </span>}, { "<span class="hljs-attribute">x</span>": <span class="hljs-value"><span class="hljs-number">881</span></span>, "<span class="hljs-attribute">y</span>": <span class="hljs-value"><span class="hljs-number">31</span> </span>}, { "<span class="hljs-attribute">x</span>": <span class="hljs-value"><span class="hljs-number">881</span></span>, "<span class="hljs-attribute">y</span>": <span class="hljs-value"><span class="hljs-number">389</span> </span>}, { "<span class="hljs-attribute">x</span>": <span class="hljs-value"><span class="hljs-number">186</span></span>, "<span class="hljs-attribute">y</span>": <span class="hljs-value"><span class="hljs-number">389</span> </span>} ] </span>}</span>, "<span class="hljs-attribute">locations</span>": <span class="hljs-value">[ { "<span class="hljs-attribute">latLng</span>": <span class="hljs-value">{ "<span class="hljs-attribute">latitude</span>": <span class="hljs-value"><span class="hljs-number">35.713804054916366</span></span>, "<span class="hljs-attribute">longitude</span>": <span class="hljs-value"><span class="hljs-number">139.796659</span> </span>} </span>} ] </span>} ] </span>} ] </span>} |
ちなみに上記のデータは浅草寺の宝蔵門の写真を判定した場合のレスポンスです。
JSONフレームワークを使わずにJSONデータを作ってみる
以下のような実装にすると、Google Vision API で判定したい画像を含むJSONデータをJSONフレームワークを使わずに作成できます。ここでは TStringStream を用いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="hljs-comment" style="color:#008000;font-weight:bold;">// Google Vision API で判定したいファイルを MemoryStream 経由で TImage に入れる</span> OpenDialog1.Execute; oInStr := TMemoryStream.Create; oInStr.LoadFromFile(OpenDialog1.FileName); Image1.Bitmap.LoadFromStream(oInStr); <span class="hljs-comment" style="color:#008000;font-weight:bold;">// TStringStream を作成して、JSONデータを書き込んでいる。</span> oOutStr := TStringStream.Create; oOutStr.WriteString(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'{"requests": [{"image": {"content": "'</span>); Encoder := TBase64Encoding.Create; oInStr.Seek(<span class="hljs-number">0</span>,soBeginning); Encoder.Encode(oInStr,oOutStr); oOutStr.WriteString(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'"},"features": [{"type": "LANDMARK_DETECTION"}]}]}'</span>); |
今回のリクエストでは JSON の形式がシンプルなので、このような方法もアリといえばアリです。
ただしこの方法は静的に用いる文字列部分を間違って毀損してしまった場合にコンパイルエラーは出ないけれど実行時にAPI呼び出しでエラーが生じます。そのような問題を内包した実装ですので、できれば割けたほうがよい実装です。
JSONリーダー/ライターフレームワークのTJSONObjectBuilderで作る
まずは実際のコード例から。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
StringBuilder := TStringBuilder.Create; StringWriter := TStringWriter.Create(StringBuilder); Writer := TJsonTextWriter.Create(StringWriter); Writer.Formatting := TJsonFormatting.Indented; MyJSONBuilder := TJSONObjectBuilder.Create(Writer); MyJSONBuilder .BeginObject <span class="hljs-comment" style="color:#008000;font-weight:bold;">// {</span> .BeginArray(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'requests'</span>) <span class="hljs-comment" style="color:#008000;font-weight:bold;">// "requests": [</span> .BeginObject <span class="hljs-comment" style="color:#008000;font-weight:bold;">// {</span> .BeginObject(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'image'</span>) <span class="hljs-comment" style="color:#008000;font-weight:bold;">// "image": {</span> .Add( <span class="hljs-comment" style="color:#008000;font-weight:bold;">// "content","...."</span> <span class="hljs-string" style="color:#0000ff;font-weight:bold;">'content'</span>, <span class="hljs-comment" style="color:#008000;font-weight:bold;">//</span> oOutStr.ReadString(oOutStr.Size) <span class="hljs-comment" style="color:#008000;font-weight:bold;">//</span> ) <span class="hljs-comment" style="color:#008000;font-weight:bold;">//</span> .EndObject <span class="hljs-comment" style="color:#008000;font-weight:bold;">// },</span> .BeginArray(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'features'</span>) <span class="hljs-comment" style="color:#008000;font-weight:bold;">// "features":[</span> .BeginObject <span class="hljs-comment" style="color: #008000; font-weight: bold;">// {</span> .Add(<span class="hljs-string" style="color: #0000ff; font-weight: bold;">'type'</span>,<span class="hljs-string" style="color: #0000ff; font-weight: bold;">'LANDMARK_DETECTION'</span>) <span class="hljs-comment" style="color: #008000; font-weight: bold;">// "type":"LANDMARK_DETECTION"</span> .EndObject <span class="hljs-comment" style="color: #008000; font-weight: bold;">// }</span> .EndArray <span class="hljs-comment" style="color: #008000; font-weight: bold;">// ]</span> .EndObject <span class="hljs-comment" style="color:#008000;font-weight:bold;">// }</span> .EndArray <span class="hljs-comment" style="color:#008000;font-weight:bold;">// ]</span> .EndObject; <span class="hljs-comment" style="color:#008000;font-weight:bold;">// }</span> |
今回のように、シンプルだけど配列を含むデータをJSONリーダー/ライターフレームワークで作ろうとすると、単に文字列処理で作る場合に比べて少々冗長に見えるかもしれません。
しかしJSONデータをコメントとして併記したものと比べていただければ一目瞭然ですが、JSONリーダー/ライターフレームワークの記述と実際のJSONデータの構造は完全に等価です。そういう意味ではJSONリーダー/ライターフレームワークは構造と一対一で分かりやすいといえるでしょう。
なお、この方法は MyJSONBuilder の最初の BeginObject から最後の EndObject までの内容が文法上は1行であることにご注意ください。
JSONリーダー/ライターフレームワークのTJsonTextWriterで作る
TJsonTextWriter でこんな風に作ることもできます。TJSONObjectBuilder と同様で、TJsonTextWriterのメソッド実行とJSONデータの構造は等価です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
StringBuilder := TStringBuilder.Create; StringWriter := TStringWriter.Create(StringBuilder); Writer := TJsonTextWriter.Create(StringWriter); Writer.Formatting := TJsonFormatting.Indented; Writer.WriteStartObject; <span class="hljs-comment" style="color:#008000;font-weight:bold;">// {</span> Writer.WritePropertyName(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'requests'</span>); <span class="hljs-comment" style="color:#008000;font-weight:bold;">// "requests": [</span> Writer.WriteStartArray; <span class="hljs-comment" style="color:#008000;font-weight:bold;">//</span> Writer.WriteStartObject; <span class="hljs-comment" style="color:#008000;font-weight:bold;">// {</span> Writer.WritePropertyName(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'image'</span>); <span class="hljs-comment" style="color:#008000;font-weight:bold;">// "image":{</span> Writer.WriteStartObject; <span class="hljs-comment" style="color:#008000;font-weight:bold;">//</span> Writer.WritePropertyName(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'content'</span>); <span class="hljs-comment" style="color:#008000;font-weight:bold;">// "content":"..."</span> Writer.WriteValue(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'test'</span>); <span class="hljs-comment" style="color:#008000;font-weight:bold;">//</span> Writer.WriteEndObject; <span class="hljs-comment" style="color:#008000;font-weight:bold;">// },</span> Writer.WritePropertyName(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'features'</span>); <span class="hljs-comment" style="color:#008000;font-weight:bold;">// "features":[</span> Writer.WriteStartArray; <span class="hljs-comment" style="color: #008000; font-weight: bold;">//</span> Writer.WriteStartObject; <span class="hljs-comment" style="color: #008000; font-weight: bold;">// {</span> Writer.WritePropertyName(<span class="hljs-string" style="color: #0000ff; font-weight: bold;">'type'</span>); <span class="hljs-comment" style="color: #008000; font-weight: bold;">// "type":"LANDMARK_DETECTION'</span> Writer.WriteValue(<span class="hljs-string" style="color: #0000ff; font-weight: bold;">'LANDARK_DETECTION'</span>); <span class="hljs-comment" style="color: #008000; font-weight: bold;">//</span> Writer.WriteEndObject; <span class="hljs-comment" style="color: #008000; font-weight: bold;">// }</span> Writer.WriteEndArray; <span class="hljs-comment" style="color: #008000; font-weight: bold;">// ]</span> Writer.WriteEndObject; <span class="hljs-comment" style="color:#008000;font-weight:bold;">// }</span> Writer.WriteEndArray; <span class="hljs-comment" style="color:#008000;font-weight:bold;">// ]</span> Writer.WriteEndObject; <span class="hljs-comment" style="color:#008000;font-weight:bold;">// }</span> |
TJSONObjectBuilder と違って、TJsonTextWriter は一つ一つのメソッドで1行です。途中の配列作成部分をループさせることもできます。
JSONオブジェクトフレームワークで作る?
参考までにJSONオブジェクトフレームワークで作る、という方法も紹介しようと準備したのですが、あまり楽しい結果にならなかったのでコードは省略します。
ざっくりとした流れは以下のような感じですので、JSONリーダーライターフレームワークとは作る順番が全然違います。
{ "image": { "content":"..." } }
を作る{”type":"LANDMARK_DETECTION"}
を作る"features"[ ... ]"
に type を差し込む- 最後に
{ "requests": [ ... ] }
に image と features を入れる
REST API から受け取ったデータを読む
JSONオブジェクトフレームワークで読む
レスポンスには緯度経度以外の情報も含まれていますが、それらを使わない場合はJSONオブジェクトフレームワークを用いると、欲しいデータのパスを直接して参照できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<span class="hljs-keyword" style="color:#000080;font-weight:bold;">var</span> JSONValue: TJSONValue; GoogleMapURL: <span class="hljs-keyword" style="color:#000080;font-weight:bold;">string</span>; <span class="hljs-keyword" style="color:#000080;font-weight:bold;">begin</span> JSONValue := TJSONObject.ParseJSONValue(Memo1.Lines.Text); GoogleMapURL := Format( <span class="hljs-string" style="color:#0000ff;font-weight:bold;">'https://maps.google.com/maps?q=%s,%s'</span>, [ JSONValue.GetValue<<span class="hljs-keyword" style="color:#000080;font-weight:bold;">String</span>>(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'responses[0].landmarkAnnotations[0].locations[0].latLng.latitude'</span>), JSONValue.GetValue<<span class="hljs-keyword" style="color:#000080;font-weight:bold;">String</span>>(<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'responses[0].landmarkAnnotations[0].locations[0].latLng.longitude'</span>) ] ); <span class="hljs-keyword" style="color:#000080;font-weight:bold;">end</span>; |
RESTResponse コンポーネントで RootElement を指定してみる
RESTResponse コンポーネントで RootElement に “responses[0].landmarkAnnotations[0].locations[0]” を設定すると、 JSONValue.GetValue の箇所を以下のように書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<span class="hljs-keyword" style="color:#000080;font-weight:bold;">var</span> JSONValue: TJSONValue; GoogleMapURL: <span class="hljs-keyword" style="color:#000080;font-weight:bold;">string</span>; <span class="hljs-keyword" style="color:#000080;font-weight:bold;">begin</span> RESTResponse1.RootElement=<span class="hljs-string" style="color:#0000ff;font-weight:bold;">'responses[0].landmarkAnnotations[0].locations[0]'</span>; JSONValue := TJSONObject.ParseJSONValue(Memo1.Lines.Text); GoogleMapURL := Format( <span class="hljs-string" style="color:#0000ff;font-weight:bold;">'https://maps.google.com/maps?q=%s,%s'</span>, [ JSONValue.GetValue<<span class="hljs-keyword" style="color: #000080; font-weight: bold;">String</span>>(<span class="hljs-string" style="color: #0000ff; font-weight: bold;">'latLng.latitude'</span>), JSONValue.GetValue<<span class="hljs-keyword" style="color: #000080; font-weight: bold;">String</span>>(<span class="hljs-string" style="color: #0000ff; font-weight: bold;">'latLng.longitude'</span>) ] ); <span class="hljs-keyword" style="color:#000080;font-weight:bold;">end</span>; |
RootElement を指定したほうがコードはシンプルになります。
JSONリーダーライターフレームワークで読む?
JSONリーダーライターフレームワークは、JSON配列として複数のデータが渡される場合に while や Iterator を用いてデータを読むことができます。ただし今回のケースでは locations[0].latLng を参照したいだけなので、今回はこの方法の紹介は省略します。
取得できた緯度経度情報を表示する
いずれの処理でも GoogleMapURL には判定できた場所の地図を表示できるURLが生成されていますので、ブラウザで開くなりして表示可能です。
Windows 向けアプリの場合は ShellExecute により、デフォルトのブラウザで GoogleMap URL を閲覧できます。
1 2 |
ShellExecute(<span class="hljs-number">0</span>, <span class="hljs-string" style="color:#0000ff;font-weight:bold;">'open'</span>, PChar(GoogleMapURL), <span class="hljs-keyword" style="color:#000080;font-weight:bold;">nil</span>, <span class="hljs-keyword" style="color:#000080;font-weight:bold;">nil</span>, SW_SHOWNORMAL); |
あるいはアプリ内で TWebBrowser で表示してもよいでしょう。
2018年4月23日~5月4日までの月~金曜に毎日ブログを更新。Delphi / C++Builderに関する技術記事からエンジニアの日常まで、さまざまな話題を投稿します。お楽しみに! 日本人スタッフブログを一覧表示できる、こちらのページをブックマークしてください。 |
Design. Code. Compile. Deploy.
Start Free Trial Upgrade Today
Free Delphi Community Edition Free C++Builder Community Edition