この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
複数の手書き文字(日本語)が書かれている1つの画像があるとします。 この画像について、Paragraph毎に画像を分割してみました。
S3バケットに画像を置くとLambdaが起動し、Lambda内でGCPのCloud Vision APIを使ってOCRを行います。 OCR結果に従ってParagraph毎に画像を切り出し、S3バケットに保存します。
なお、画像の切り出しにはPillowを使います。
おすすめの方
- S3にオブジェクトを置くと、SNSトピックを発行する方法を知りたい方
- LambdaでGCPのCloud Vision APIを使ってみたい方
- LambdaからS3にファイルをアップロードしたい方
- AWS SAMを使ってみたい方
GCPのプロジェクトを作成する
こちらにアクセスして、プロジェクトの設定を行います。 GCPコンソールからプロジェクト作成を行ってもOKです。
そのままCloud Vision API
を有効にします。
続いてJSONのプライベートキーをダウンロードしておきます。
サーバーレスアプリを作成する
SAM Init
sam init \
--runtime python3.7 \
--name GCP-OCR-Crop-Sample \
--app-template hello-world
VisionクライアントライブラリとPillowを使う
GCPのVisionクライアントライブラリとPillowを使うため、requirements.txtに記載します。
requirements.txt
google-cloud-vision
pillow
認証情報JSONファイルを格納する
GCPのプロジェクト作成時にダウンロードした認証情報(JSONファイル)をLambdaハンドラーと同じ場所に格納します。
├── hello_world
│ ├── __init__.py
│ ├── app.py
│ ├── gcp.json
│ └── requirements.txt
└── template.yaml
SAMテンプレートファイル
認証情報(JSONファイル)のファイル位置をLambdaの環境変数で設定しています。また、S3バケットに対して、SNSトピックをPublishする権限を与える必要があります。
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: GCP-OCR-Crop-Sample
Resources:
ImageSourceBucket:
DeletionPolicy: Retain
Type: AWS::S3::Bucket
Properties:
BucketName: cm-fujii-genki-ocr-crop-src-sample-bucket
NotificationConfiguration:
TopicConfigurations:
- Event: s3:ObjectCreated:*
Topic: !Ref OcrNotifyTopic
ImageOutputBucket:
DeletionPolicy: Retain
Type: AWS::S3::Bucket
Properties:
BucketName: cm-fujii-genki-ocr-crop-output-sample-bucket
OcrNotifyTopic:
Type: AWS::SNS::Topic
OcrNotifyTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
Topics:
- !Ref OcrNotifyTopic
PolicyDocument:
Id: !Ref OcrNotifyTopic
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: s3.amazonaws.com
Action: SNS:Publish
Resource: !Ref OcrNotifyTopic
Condition:
ArnLike:
aws:SourceArn: arn:aws:s3:::cm-fujii-genki-ocr-crop-src-sample-bucket
OcrSampleFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.7
Timeout: 30
MemorySize: 3008
Policies:
- arn:aws:iam::aws:policy/AmazonS3FullAccess
Environment:
Variables:
GOOGLE_APPLICATION_CREDENTIALS: gcp.json
OUTPUT_S3_BUCKET_NAME: !Ref ImageOutputBucket
Events:
S3Event:
Type: SNS
Properties:
Topic: !Ref OcrNotifyTopic
OcrSampleFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${OcrSampleFunction}
Lambdaコード
S3バケットから画像ファイルをダウンロードし、GCPのCloud Vision APIでOCRを行い、Paragraph領域を切り取ってS3バケットにアップロードしています。
app.py
import json
import os
import boto3
from PIL import Image
s3 = boto3.client('s3')
OUTPUT_S3_BUCKET_NAME = os.environ['OUTPUT_S3_BUCKET_NAME']
def lambda_handler(event, context):
message = json.loads(event['Records'][0]['Sns']['Message'])
s3_event = message['Records'][0]['s3']
bucket_name = s3_event['bucket']['name']
key_name = s3_event['object']['key']
print(f'target: {bucket_name}, {key_name}')
tmp_file_path = f'/tmp/{key_name}'
s3.download_file(bucket_name, key_name, tmp_file_path)
with open(tmp_file_path, 'rb') as f:
body = f.read()
paragraph_box = detect_document(body)
crop_and_upload(tmp_file_path, paragraph_box)
def crop_and_upload(file_path, paragraph_box):
image = Image.open(file_path)
for index, bound in enumerate(paragraph_box):
# 指定領域を切り取る(トリミング)
crop_image = image.crop((
bound.vertices[0].x, bound.vertices[0].y, # 左上の座標
bound.vertices[2].x, bound.vertices[2].y, # 右下の座標
))
file_name, extension = os.path.splitext(file_path)
output_file_path = f'{file_name}_crop_{index:03}{extension}'
crop_image.save(output_file_path)
s3.upload_file(
output_file_path,
OUTPUT_S3_BUCKET_NAME,
os.path.basename(output_file_path)
)
def detect_document(body):
# https://cloud.google.com/vision/docs/handwriting
from google.cloud import vision
client = vision.ImageAnnotatorClient()
image = vision.Image(content=body)
response = client.document_text_detection(
image=image,
image_context={'language_hints': ['ja']}
)
paragraph_box = []
for page in response.full_text_annotation.pages:
for block in page.blocks:
print('+ Block confidence: {}\n'.format(block.confidence))
for paragraph in block.paragraphs:
paragraph_box.append(paragraph.bounding_box)
print('-- Paragraph confidence: {}'.format(
paragraph.confidence))
for word in paragraph.words:
word_text = ''.join([
symbol.text for symbol in word.symbols
])
print('**** Word text: {} (confidence: {})'.format(
word_text, word.confidence))
for symbol in word.symbols:
print('>>>>>> Symbol: {} (confidence: {})'.format(
symbol.text, symbol.confidence))
if response.error.message:
raise Exception(
'{}\nFor more info on error messages, check: '
'https://cloud.google.com/apis/design/errors'.format(
response.error.message))
return paragraph_box
デプロイ
sam build --use-container
sam package \
--output-template-file packaged.yaml \
--s3-bucket cm-fujii.genki-deploy
sam deploy \
--template-file packaged.yaml \
--stack-name GCP-OCR-Crop-Sample-Stack \
--capabilities CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset
さっそく動かす
iPhoneのメモアプリで手書きし、3種類の画像を用意しました。
にんじん・大根
S3にアップロードした画像
以前も使った画像ですが、画像サイズ(余白部分)が異なります。
OCR結果
にんじんさんが……。
分割結果
大根部分が少し欠けました。
牛肉・はくさい・れんこん
S3にアップロードした画像
OCR結果
はくさい部分が惜しいですね。
分割結果
牛肉とはくさいが同じParagraphと認識されました。意図的に改行時の空白を大きくすると、3分割になるかもしれません。
枝豆・じゃがいも・ブロッコリー・レタス
S3にアップロードした画像
OCR結果
じゃがいもとブロッコリーが駄目でした。アルファベットに似ている縦棒?あたりが怪しいのでしょうか。 それにしても、以下は「言われてみれば……」と少し納得です。
手書き文字 | OCR結果 |
---|---|
じ | L' |
ゃ | th |
フ | 7 |
ロッ | Diy |
分割結果
枝豆とじゃがいもは分割されていますが、両端が切れてしまっています。
さいごに
人間の認識能力ってすごいですね……。
参考
- Vision AI | ML から画像情報を引き出す | Cloud Vision API | Google Cloud
- Cloud Vision のドキュメント | Cloud Vision API | Google Cloud
- 画像内の手書き入力を検出する | Cloud Vision API | Google Cloud
- googleapis/python-vision
- Pillow — Pillow (PIL Fork) 7.2.0 documentation
- S3 — Boto3 Docs 1.15.16 documentation
- PIL/Pillow チートシート - Qiita
- Cloud Vision APIをLambdaで使って、手書き文字(日本語)のOCRをやってみた | Developers.IO