SpringBoot + KotlinでGoogleのVisionAIを使う
今趣味でつくっているWebサービスでGoogleCloudのVisionAIを使用したので備忘録として書いておきます。
目次
実行環境
言語: Kotlin
Framework: SpringBoot 2.3.1 RELEASE
ビルドツール: Maven
プロジェクトでMysqlも使っているので Docker上で実行しています
(Java Image: openjdk:14-jdk-alpine
)
Vision AI
VisionAIはGoogleの提供する画像解析ツールです。今回はAPIとして提供されているVision APIを使用します。
VIsionAPI: Google Cloud の Vision API は REST API や RPC API を介して強力な事前トレーニング済み機械学習モデルを提供します。画像にラベルを割り当てることで、事前定義済みの数百万のカテゴリに画像を高速に分類できます。オブジェクトや顔を検出し、印刷テキストや手書き入力を読み取り、有用なメタデータを画像カタログに作成します。
参考: Cloud VisionAPI REST API Document https://cloud.google.com/vision/docs/reference/rest?hl=ja
参考: Spring FrameworkでVisionを使用すr https://cloud.google.com/vision/docs/adding-spring?hl=ja
VisionAPIをSpringで使用する
準備
1. 依存情報を追加(pom.xml)
dependencyManagementとdependencyの両方に追記する必要があります。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-dependencies</artifactId> <version>1.2.4.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-starter-vision</artifactId> </dependency>
GCP プロジェクトの作成・サービスアカウントのSpringプロジェクトへの適用
記事が多いので省略します。
GCPプロジェクトを作成する(アカウント登録・プロジェクト登録など)
サービスアカウントの作成
下記リンクを参考に。
https://cloud.google.com/docs/authentication/getting-started?hl=ja
3.サービスアカウントキーをプロジェクトの環境変数に設定します。
export GOOGLE_APPLICATION_CREDENTIALS="path/credentials.json"
実装
Controller, Service, Repositoryを書きます。
今回は簡単のためInterfaceは挟みません。
今回実装するAPIの応答時間は開発環境で1.3sぐらいです。
VisionAPIのレスポンス速度が早くてびっくり。
Controller
今回はクライアント側から次のようなRequestBodyを受け取ることにします。 画像をBase64でエンコードした文字列をPostします。
VisionOcrBody
data class VisionOcrBody ( /** IMG -> Decode from Base64 */ @field:NotNull(message = "dataは必須項目です") val data: String )
VisionController.kt
import project.backend.model.requests.vision.VisionOcrBody import project.backend.model.responses.ResponseVision import project.backend.service.VisionService import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.* import javax.validation.Valid @RestController @RequestMapping("/vision") class VisionController { @Autowired lateinit var visionService: VisionService @GetMapping("ocr") fun ocr(@Valid @RequestBody visionOcrBody: VisionOcrBody): ResponseVision { return visionService.ocr( data = visionOcrBody.data) } }
Service
今回は例として書いているのでほとんど記載はないですが、取得したデータの加工は主にService Layerで行います。(今回の場合だとextract function)
VisionService.kt
import project.backend.model.responses.ResponseVision import project.backend.repository.VisionRepository import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @Service class VisionService { @Autowired lateinit var visionRepository: VisionRepository fun ocr(extension: String, data: String): ResponseVision { val result: ArrayList<String> = visionRepository.analyzeByBase64String(base64String = data) return extract(result) } fun extract(result: ArrayList<String>): ResponseVision { // 取得したデータの加工 } }
Repository
cloudVisionTemplate
を使用してVisionAPIを叩きます。
具体的には、下記の部分です。
cloudVisionTemplate.analyzeImage( resource, Feature.Type.TEXT_DETECTION )
Resourceの実装クラスとresource
画像解析のTypeを指定します。
今回は文字抽出をしたかったのでFeature.Type.TEXT_DETECTION
を使用しました。
実施する画像解析によって引数が変わります。
参考: https://cloud.google.com/vision/docs/features-list?hl=ja
VisionRepository.kt
import com.google.cloud.vision.v1.AnnotateImageResponse import com.google.cloud.vision.v1.Feature import org.apache.commons.codec.binary.Base64 import org.springframework.beans.factory.annotation.Autowired import org.springframework.cloud.gcp.vision.CloudVisionTemplate import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.Resource import org.springframework.stereotype.Repository @Repository class VisionRepository { @Autowired lateinit var cloudVisionTemplate: CloudVisionTemplate fun analyzeByBase64String(base64String: String): List<String> { val byteArrayData: ByteArray = Base64.decodeBase64(base64String) var resource: Resource = ByteArrayResource(byteArrayData) val response: AnnotateImageResponse = cloudVisionTemplate.analyzeImage( resource, Feature.Type.TEXT_DETECTION ) val ret = ArrayList<String>() for(item in response.textAnnotationsList){ ret.add(item.description.toString()) // item.boundingPoly.toString() } return ret } }
Response
VisionAPIのResponseBodyは次のようなものです。
response.textAnnotationList
: 検出した文字列の一覧
response.textAnnotationList[index].description
: 検出された文字
response.textAnnotationList[index].boundingPoly
: 検出した文字の位置情報など
GoogleのRepositoryサンプル
VisionAPIのチュートリアルのコードではURLを指定し、画像を取得 -> VisionAPIにリクエストという流れでしたが、レスポンスは10sぐらいかかりました。
(画像サイズにもよりますが、resourceLoader.getResource(url)
で相当時間かかってそう)
fun analyzeByUrl(url: String): List<String> { val resource: Resource = resourceLoader.getResource(url) val response: AnnotateImageResponse = cloudVisionTemplate.analyzeImage( resource, Feature.Type.TEXT_DETECTION ) val ret = ArrayList<String>() for(item in response.textAnnotationsList){ ret.add(item.description.toString()) // description // println(item.boundingPoly) // position } return ret }
使ってみた感想
実装もかなり簡単なので、凄く使いやすいと感じました。
ただし、APIで文字列が抽出できて終わり!のようなプロジェクトでは別ですが、今回は抽出した文字一覧のうち必要な情報を抽出し、クライアントに返さないと行けなかったのでその部分が少し大変でした。
クライアント側に必要な情報だけを提供するテクニックを3つ紹介します。
- 正規表現: シンプル
- boundingPoly(位置座標)から推定
- ある程度同じフォーマットの画像なら、返ってくるListに含まれる文字列の順序(検知順)が同じなのでListのIndexで推測