なにをしたいか
ドキュメントをスキャンor撮影した画像があるとする。それぞれの画像はドキュメントの正しい向きに対してバラバラの向きになってしまっている。これをGCPのVision APIで取得できる情報を使って正しい向きに回転してやりたい。
ちなみに、フリーのOCRであるTesseract OCRではPSM(Page Segmentation Model)に0
や2
を設定することでテキストの傾斜角度を調べることが可能。一方、Google Cloud Vision APIでは調べた限りそのようなオプションは見つからなかったので、自前で実装することにする。
※なお、本記事では横書きのドキュメントのみを対象とし、縦書きのドキュメントは考慮しない。
目次
Google Cloud Vision APIで得られる情報
テキスト検出機能としてDOCUMENT_TEXT_DETECTION
を使った場合、以下のような階層構造をもったテキスト情報が得られる。
(Vision API OCR事始め(2):検出されたテキストの階層構造(fullTextAnnotation)より引用)
Googleのドキュメントによれば、それぞれの階層は以下のような意味をもつようだ。
fullTextAnnotation は、画像から抽出された UTF-8 テキストを階層構造で表現したレスポンスで、ページ→ブロック→段落→語→記号のように編成されています。
Page は、ブロックの集まりに、ページについてのメタ情報、つまりサイズ、解像度(X 解像度と Y 解像度が違う場合がある)を付加したものです。
Block は、ページの 1 つの「論理的」要素を表します。たとえば、テキストで埋め尽くされている領域や、列と列の間にある図や区切りなどです。テキスト ブロックとテーブルブロックには、テキスト抽出に必要な主要な情報が含まれています。
Paragraph は、順序付けられた単語列を表すテキストの構造単位です。デフォルトで単語は、単語区切りで区切られているものとみなされます。
Word は、テキストの最小単位です。これは、記号の配列として表記されます。
Symbol は、文字または句読点記号を表します。
例えば、今回使う画像だと以下のような階層構造に分類される(青枠がBlock、緑枠がParagraph、橙色の枠がWord)
またBlock, Paragraph, Word, Symbolの各オブジェクトは画像内での位置を示すbounding_box
の情報を持っている。つまりWordのbounding_box
は単語毎(ex. 毎日, 暮らし, ステキ, アイデア)の位置を示し、Symbolのbounding_box
は文字毎(ex. 毎, 日, 暮, ら, し, ...)の位置を示す。
向き判定アルゴリズム
上記の階層構造のうち、WordとSymbolが今回は重要な要素となる。すなわち、word内で文字がどの方向で読まれたかを以下の方法で推定する。
-
文書内の各word内の最初のsymbolの
bounding_box
$B_0\hspace{0.1ex}$と、最後のsymbolのbounding_box
$B_{-1}\hspace{0.1ex}$を取得する -
得られた
bounding_box
の4つの頂点の座標を平均して$B_0, B_{-1}$の重心$C_0=(C_{0x}, C_{0y}), C_{-1}=(C_{-1x}, C_{-1y})$を算出する -
$C_0, C_{-1}$の位置の差分${\delta x=C_{-1x}-C_{0x}, \delta y=C_{-1y}-C_{0y}}\hspace{0.1ex}$を計算し、以下のルールで各wordの読まれた向きを判定
- $|\delta x|>|\delta y|\hspace{0.1ex}$であって、かつ$\delta x>0\hspace{0.1ex}$である場合: 0° (横書きのドキュメントにおいて正しい向き)
- $|\delta x|>|\delta y|\hspace{0.1ex}$であって、かつ$\delta x\leq0\hspace{0.1ex}$である場合: 180°
- $|\delta x|\leq|\delta y|\hspace{0.1ex}$であって、かつ$\delta y >0\hspace{0.1ex}$である場合: 90°
- $|\delta x|\leq|\delta y|\hspace{0.1ex}$であって、かつ$\delta y\leq0\hspace{0.1ex}$である場合: -90°
実装
import os
from io import BytesIO
from tqdm import tqdm
import numpy as np
from google.cloud import vision
from PIL import Image
import matplotlib.pyplot as plt
os.chdir(os.path.dirname(__file__))
GOOGLE_CREDENTIALS_PATH = '/path/to/credential'
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = GOOGLE_CREDENTIALS_PATH
vision_client = vision.ImageAnnotatorClient()
impath = '/path/to/image'
with open(impath, "rb") as f :
content = f.read()
img = Image.open(BytesIO(content))
inp = vision.Image(content=content)
response = vision_client.document_text_detection(
image=inp,
image_context={'language_hints': ['ja']}
)
document = response.full_text_annotation
text_blocks = document.pages[0].blocks
# [rotate 0, rotate -90, rotate +90, rotate +180]
rotate_flgs = [0, 0, 0, 0]
rotate_angles = [0, -90, 90, 180]
for i, bl in enumerate(tqdm(text_blocks)):
paragraph = bl.paragraphs
for p in paragraph:
for w in p.words:
symbol_centroids = []
for s in w.symbols:
symbol_bbox = np.array([[b.x, b.y]
for b in s.bounding_box.vertices])
symbol_centroid = np.mean(symbol_bbox, axis=0)
symbol_centroids.append(symbol_centroid)
symbol_centroids = np.array(symbol_centroids)
if len(symbol_centroids) > 1:
c_delta = symbol_centroids[-1, :] - symbol_centroids[0, :]
if abs(c_delta[0]) > abs(c_delta[1]):
if c_delta[0] > 0:
rotate_flgs[0] += 1
elif c_delta[0] < 0:
rotate_flgs[3] += 1
elif abs(c_delta[0]) < abs(c_delta[1]):
if c_delta[1] > 0:
rotate_flgs[2] += 1
elif c_delta[1] < 0:
rotate_flgs[1] += 1
rotate_angle = rotate_angles[np.argmax(rotate_flgs)]
print(rotate_angle)
plt.figure(dpi=200)
plt.title(f'inferred angle : {rotate_angle}')
plt.imshow(img)