この記事は前回の続き見たいなものです。
ちょうど前回の記事を書いたあたりで、自社のアドベントカレンダーがAI関係なことに気が付き、まとめで書いていた「AWSのTextract使ったら精度よくなるかも」という点を、実際に利用しながら確認してみたいと思います。
また、今回は同じくAWSのサービスであるCodeGuru
を使ってコードレビューもやってみたいと思います。
Amazon Textractってなんぞや
とても簡単にOCRすることができるサービスになっています。
現在 パリ(eu-west-3)、ロンドン(eu-west-2)、シンガポール(ap-southeast-1)、ムンバイ(ap-south-1) のリージョンでのみ利用できるようです。コンソール上、またはSDKを用いて利用することができます。
まずはコンソール上から使ってみましょう。
Amazon Textractをコンソール上から使ってみる
では精度がどんなものかの確認もしたいので、使ってみましょう。
こちらがコンソールの画面です。
サンプルの精度を見てみるとかなりよさそうに見えます。手書きなのにここまで読めるのはすごいと思います。
better app
って書かれてるところとか、tt
はH
に見えるし、a
はo
に見えますもん。よく読み取れるなと思います。
では、今度はリングフィットの画像を投げて確認してみましょう。画像をドラッグ&ドロップするだけでOCRしてくれます。
んーどうやら日本語には対応していないようですね…。ですが、数値やそれ以外の部分はちゃんと読み取れているみたいです。
後処理をちゃんとしてあげれば前回よりも精度よくデータを作れるかもしれません。
pythonからAmazon Textractを使ってみる
ではpythonからTextractを使ってみたいと思います。
pythonで利用する為にawscliとboto3をインストールします
pip install awscli
pip install boto3
awscliに利用するiamユーザーの設定をします。
iamからユーザーを作成した際に発行されるアクセスキーとシークレットアクセスキーを利用します。
aws configure
AWS Access Key ID [None]: your access key
AWS Secret Access Key [None]: your secret access key
Default region name [None]: your region
Default output format [None]: your format
region と formatは設定しなくてもいいかもしれません。
コードはドキュメントを参考にしました→Boto3 Docs1.16.37ドキュメント-Textract
コードの内容は以下になります。
- textractを利用する準備
- 画像の読み込み
- textractでOCRを行う(同期処理)
- 返ってきたデータから結果を表示する
import boto3
# Amazon Textract client
textract = boto3.client('textract', region_name="ap-southeast-1")
# read image to bytes
with open('get_data/2020-09-28.png', 'rb') as f:
data = f.read()
# Call Amazon Textract
response = textract.detect_document_text(
Document={
'Bytes': data
}
)
# Print detected text
for item in response["Blocks"]:
if item["BlockType"] == "LINE":
print ('\033[94m' + item["Text"] + '\033[0m')
では実際に実行してみましょう。
私の環境では約2秒ほどかかりました。
python .\textract.py
R
Oti
10+29
38.40kcal
0.89km
Oxian
実行結果を見ると、どうやらコンソールの実行と同じ結果になっているようです。
実行結果って何が返ってくるの?
先ほどは、ドキュメントのままでresponseの中から実行結果を抜き出して表示していましたが、そもそもどのような内容が返ってくるのでしょうが。responseの中身を確認してみたいと思います。
{
"DocumentMetadata": {
"Pages": 1
},
"Blocks": [
{
"BlockType": "PAGE",
"Geometry": {
"BoundingBox": {
"Width": 1.0,
"Height": 0.9992592334747314,
"Left": 0.0,
"Top": 0.0
},
"Polygon": [
{
"X": 6.888638380355447e-17,
"Y": 0.0
},
{
"X": 1.0,
"Y": 0.0
},
{
"X": 1.0,
"Y": 0.9992592334747314
},
{
"X": 0.0,
"Y": 0.9992592334747314
}
]
},
"Id": "33a0a9cd-0569-44ed-9f0f-7e88ede1d3d3",
"Relationships": [
{
"Type": "CHILD",
"Ids": [
"b9b8fd8e-1f13-4b9a-8bfa-8c8ca4750ae0",
"3b71c094-0bac-496e-9e26-1d311b89a66c",
"366cdb0a-5d10-4f64-b88b-c1ad79013fc2",
"232492f4-3137-49df-ad21-0369622cc56e",
"738b30df-4472-4a25-90fe-eaed85e74566",
"a73953ed-6038-49fb-af64-bad77e0d1e8f"
]
}
]
},
{
"BlockType": "LINE",
"Confidence": 87.06179809570312,
"Text": "R",
"Geometry": {
"BoundingBox": {
"Width": 0.008603394031524658,
"Height": 0.018224462866783142,
"Left": 0.7822862863540649,
"Top": 0.1344471424818039
},
"Polygon": [
{
"X": 0.7822862863540649,
"Y": 0.1344471424818039
},
{
"X": 0.7908896803855896,
"Y": 0.1344471424818039
},
{
"X": 0.7908896803855896,
"Y": 0.15267160534858704
},
{
"X": 0.7822862863540649,
"Y": 0.15267160534858704
}
]
},
"Id": "b9b8fd8e-1f13-4b9a-8bfa-8c8ca4750ae0",
"Relationships": [
{
"Type": "CHILD",
"Ids": [
"1efd9875-d6a4-45e4-8fb4-63e68c668ff1"
]
}
]
},
...
{
"BlockType": "WORD",
"Confidence": 87.06179809570312,
"Text": "R",
"TextType": "PRINTED",
"Geometry": {
"BoundingBox": {
"Width": 0.008603399619460106,
"Height": 0.018224479630589485,
"Left": 0.7822862863540649,
"Top": 0.1344471424818039
},
"Polygon": [
{
"X": 0.7822862863540649,
"Y": 0.1344471424818039
},
{
"X": 0.7908896803855896,
"Y": 0.1344471424818039
},
{
"X": 0.7908896803855896,
"Y": 0.15267162024974823
},
{
"X": 0.7822862863540649,
"Y": 0.15267162024974823
}
]
},
"Id": "1efd9875-d6a4-45e4-8fb4-63e68c668ff1"
},
{
"BlockType": "WORD",
"Confidence": 37.553348541259766,
"Text": "Oti",
"TextType": "HANDWRITING",
"Geometry": {
"BoundingBox": {
"Width": 0.03588677942752838,
"Height": 0.031930990517139435,
"Left": 0.4896482229232788,
"Top": 0.2779926359653473
},
"Polygon": [
{
"X": 0.4896482229232788,
"Y": 0.2779926359653473
},
{
"X": 0.525534987449646,
"Y": 0.2779926359653473
},
{
"X": 0.525534987449646,
"Y": 0.30992361903190613
},
{
"X": 0.4896482229232788,
"Y": 0.30992361903190613
}
]
},
"Id": "4e07e16b-f78b-4564-bb30-c0e48f6610c6"
},
...
],
"DetectDocumentTextModelVersion": "1.0",
"ResponseMetadata": {
"RequestId": "87f05420-f6d9-4e67-911e-64deadd207fb",
"HTTPStatusCode": 200,
"HTTPHeaders": {
"x-amzn-requestid": "87f05420-f6d9-4e67-911e-64deadd207fb",
"content-type": "application/x-amz-json-1.1",
"content-length": "6693",
"date": "Thu, 17 Dec 2020 00:36:14 GMT"
},
"RetryAttempts": 0
}
}
上記が実際の中身です。ドキュメントを見ながら確認したいと思います。
key | val |
---|---|
DocumentMetadata | ドキュメントのメタデータ。今回はページ数1が返ってきてます。 |
Blocks | AnalyzeDocumentによって検出および分析されるアイテム。OCRの結果が入ってきてます。 |
BlockType | 認識されるテキストアイテムのタイプ。いくつか種類があるようです。今回出た内容だけまとめます。 Page:検出されたLINEブロックオブジェクトのリスト。認識した文字のIDなどを保管していました。 Word:検出された単語。手書きとかプリントとかの判定も書いてありました。 LINE:検出されたタブ区切りの連続した単語の文字列。一文のデータが入るのかと思います。 |
必用なデータはこのくらいでしょうか。
今回はBlocks
の中のBlockType
がWord
のデータから必要なものを取り出していけばいいのかなと思います。
ではどうやって取り出していきましょうか。
必用なデータのみ取り出してみる。
返ってきた値を見ると、読み取ったデータの位置が書かれているようです。
リングフィットのデータは全部同じ書式になるので、読み取る文字の範囲はほとんど同じになるはずです。
リングフィットのデータは右下揃えなので、右下の座標は大体一緒になるはずです。
なので特定の座標付近のデータを取得してみたいと思います。
下記の手順を行います。
- 先ほどのデータから文字のデータと対応する右下の座標のデータを作成する
- 取得したいデータの座標位置のデータを取得する
- 各データに格納する
特定座標のデータですが、ずれることを考えて、誤差0.01まで許容してみました。
実行時に読み込んでるjsonは上記した、Textractのresponseデータです。
import json
# 文字と右下座標のみのデータに整形
def get_word(data: dict) -> dict:
words = []
for item in data["Blocks"]:
if item["BlockType"] == "WORD":
words.append({
"word":item["Text"],
"right_bottom":item["Geometry"]["Polygon"][2]
})
return words
# 右下座標が特定付近(ずれ0.01まで許容)かの判定
def point_check(x: float, y: float) -> dict:
origin_point = {
"time":{"x":0.71,"y":0.46},
"kcal":{"x":0.73,"y":0.63},
"km":{"x":0.73,"y":0.78}
}
for k, v in origin_point.items():
if abs(x-v["x"])<0.01 and abs(y-v["y"])<0.01:
return k
def get_point_data(data: dict) -> dict:
prepro_data = get_word(data)
some_data = {}
for v in prepro_data:
tmp = point_check(v["right_bottom"]["X"], v["right_bottom"]["Y"])
if tmp:
some_data[tmp] = v["word"]
return some_data
if __name__ == '__main__':
with open("j.json") as f:
data = json.load(f)
d = get_point_data(data)
print(d)
実行してみると…
python .\textract.py
{'time': '10+29', 'kcal': '38.40kcal', 'km': '0.89km'}
ちゃんと取れてるみたいです。
複数の画像で試してみる
それでは、いくつかリングフィットの画像があるので試してみたいと思います。
画像の読み込み部分を複数回実行し、複数の画像をOCRした結果を確認してみます。(コードは省略)
結果が以下になります。
[
{
"time": "27",
"kcal": "48kcal",
"km": "0.71km"
},
{
"time": "11>12*",
"kcal": "37.79kcal",
"km": "O.65km"
},
{
"kcal": "36.62kcal",
"km": "0.23km"
},
...
]
0とoの区別だったり、timeが読み取れてないデータがありますが、おおむね読み取れているようです。(時間データは日本語を含んだものなので仕方ないですね。)
時間のないデータは0で埋めて、それ以外は先頭2桁の値を利用したいと思います。
0とoの置換も含めた後処理を行いたいと思います。
時間のデータはうまく処理できないことがあるので外れ値(40分以上)は1/10の値にしてます。
ついでに、前回のグラフ作成に使えるように、日付データも追加しました。(コード外)
def post_processing(word_point_list: list):
for data in word_point_list:
if "time" not in data:
data["time"] = "0"
re_data = re.sub('[^0-9]','', data["time"])
if len(re_data) < 2:
re_data = re_data[:1]
else:
re_data = re_data[:2]
data["time"] = float(re_data) if float(re_data) < 40 else float(re_data)/10
data["kcal"] = float(data["kcal"].replace("o","0").replace("O","0").replace("k","").replace("c","").replace("a","").replace("l",""))
data["km"] = float(data["km"].replace("o","0").replace("O","0").replace("k","").replace("m",""))
return word_point_list
これで実行すると...
[
{
"time": 27.0,
"kcal": 48.0,
"km": 0.71,
"date": "2020-11-09.png"
},
{
"time": 11.0,
"kcal": 37.79,
"km": 0.65,
"date": "2020-11-15.png"
},
{
"kcal": 36.62,
"km": 0.23,
"date": "2020-11-16.png",
"time": 0.0
},
...
]
ちゃんときれいになってるようですね!
実際に動作させてみる
それでは画像をOCRして前処理、ついでにグラフの表示までを行ってみたいと思います。
利用している関数は前回の記事を参考にしてください。
import json
import os
from src.textract import do_ocr, get_point_data, post_processing
from src.graph import create_graph
IMPORT_FILE_PATH = "output/ocr_result.json"
OUTPUT_FILE_PATH = "output/graph2.png"
if __name__ == "__main__":
data = do_ocr("./get_data")
word_point_list = []
for word_dict in data:
word_point_list.append(get_point_data(word_dict))
word_point_list = post_processing(word_point_list)
with open("./output/j.json", "w") as f:
json.dump(word_point_list, f)
# DLした画像ファイルからデータ作成、出力
try:
os.makedirs(IMPORT_FILE_PATH.replace(IMPORT_FILE_PATH.split("/")[-1], ""))
except:
pass
with open(IMPORT_FILE_PATH, "w") as f:
json.dump(word_point_list, f, indent=2)
create_graph(IMPORT_FILE_PATH, OUTPUT_FILE_PATH)
今回作成したグラフと前回作成したグラフを比較してみたいと思います。
前回に比べ、時間やkcalの外れ値が減っていることが見て取れます。
やはり時間データに外れ値が見られるので、前処理かゲームの言語を変えることで対応したほうがいいのかもしれないですね。
しかし、kcalのデータはほとんど正しくとれているので、やはり有用ではあると思います。
それに、今回は画像に対して前処理を実行せずにこの精度で処理してくれているので、とても簡単に利用できると感じました。
前回 | |
---|---|
今回 |
以上で本題は終了します。
CodeGuruでコードレビューしてみる
AWSのサービスでCodeGuruというものがあります。コードのレビューを行ってくれるサービスなのですが、この度Pythonが対応言語になったので、試してみたいと思います。
まずレビューしたいコードを連携します。私はGitHubから行いました。
追加しましたら、「リポジトリの分析を作成」から分析したいリポジトリ、ブランチを選択します。
実行には数分かかりました。(体感10分くらいかな?)
実行結果を見てみたいと思います。
最初の一つ目だけ見てみたと思います。
どうやら例外処理を詳しく書いてないため、具体的に書いたほうがいいよ、とのことです。
実際にコードを見に行くと下記のようにexcept
だけでエラー内容の指定がありませんね。
# 取得した画像URLからDL(ファイル名はツイート日時)
for data in image_url_list:
try:
os.mkdir("get_data")
except:
pass
dst_path = f"get_data/{data['created_at'].strftime('%Y-%m-%d')}.png"
download_file(data['img_url'],dst_path)
このように、CodeGuruを使うことで、バグが起こりそうな箇所だったり、デバッグしやすい環境にする手助けをしてくれるみたいです。
pythonはいろんなところに使われていると思うので、使える機会は多いと思います。共同開発やしっかりとしたコードを書きたいときはCodeGuruを使うといいのかもしれません。
まとめ
今回はTextractとCodeGuruを使って前回の手直しのようなことを行いました。
数回試していますが、Textractは最初の3か月は月1000ページまで無料で利用可能なので一切の費用をかけずに作成することができました。
スタートアップにはとても助かりますね。
CodeGuruも3か月間の利用が無料で、それ以降も毎月 1,500,000 行のコードの分析までは、コード 100 行ごとに 0.50USDになっているようです。
ちなみに今回私が書いたコード量は250程度で、コードレビューされた行が187と表示されていました。
必用な箇所だけ読んでくれてるのかもしれないですね。
Textractは日本語対応してほしいですね…してくれたらもっと簡単にできるんだけど、、、今後に期待ですね!