LoginSignup
7
6

More than 1 year has passed since last update.

Google Cloud Vision APIでドキュメントの向き判定をする

Last updated at Posted at 2022-08-01

なにをしたいか

ドキュメントをスキャンor撮影した画像があるとする。それぞれの画像はドキュメントの正しい向きに対してバラバラの向きになってしまっている。これをGCPのVision APIで取得できる情報を使って正しい向きに回転してやりたい。
ちなみに、フリーのOCRであるTesseract OCRではPSM(Page Segmentation Model)に02を設定することでテキストの傾斜角度を調べることが可能。一方、Google Cloud Vision APIでは調べた限りそのようなオプションは見つからなかったので、自前で実装することにする。
※なお、本記事では横書きのドキュメントのみを対象とし、縦書きのドキュメントは考慮しない。
image.png

目次

  1. Google Cloud Vision APIで得られる情報
  2. 向き判定アルゴリズム
  3. 実装
  4. 参考文献

Google Cloud Vision APIで得られる情報

テキスト検出機能としてDOCUMENT_TEXT_DETECTIONを使った場合、以下のような階層構造をもったテキスト情報が得られる。
image.png
(Vision API OCR事始め(2):検出されたテキストの階層構造(fullTextAnnotation)より引用)

Googleのドキュメントによれば、それぞれの階層は以下のような意味をもつようだ。

fullTextAnnotation は、画像から抽出された UTF-8 テキストを階層構造で表現したレスポンスで、ページ→ブロック→段落→語→記号のように編成されています。

  • Page は、ブロックの集まりに、ページについてのメタ情報、つまりサイズ、解像度(X 解像度と Y 解像度が違う場合がある)を付加したものです。

  • Block は、ページの 1 つの「論理的」要素を表します。たとえば、テキストで埋め尽くされている領域や、列と列の間にある図や区切りなどです。テキスト ブロックとテーブルブロックには、テキスト抽出に必要な主要な情報が含まれています。

  • Paragraph は、順序付けられた単語列を表すテキストの構造単位です。デフォルトで単語は、単語区切りで区切られているものとみなされます。

  • Word は、テキストの最小単位です。これは、記号の配列として表記されます。

  • Symbol は、文字または句読点記号を表します。

例えば、今回使う画像だと以下のような階層構造に分類される(青枠がBlock、緑枠がParagraph、橙色の枠がWord)
image.png

またBlock, Paragraph, Word, Symbolの各オブジェクトは画像内での位置を示すbounding_boxの情報を持っている。つまりWordのbounding_boxは単語毎(ex. 毎日, 暮らし, ステキ, アイデア)の位置を示し、Symbolのbounding_boxは文字毎(ex. 毎, 日, 暮, ら, し, ...)の位置を示す。
image.png

向き判定アルゴリズム

上記の階層構造のうち、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の読まれた向きを判定

    1. $|\delta x|>|\delta y|\hspace{0.1ex}$であって、かつ$\delta x>0\hspace{0.1ex}$である場合: (横書きのドキュメントにおいて正しい向き)
    2. $|\delta x|>|\delta y|\hspace{0.1ex}$であって、かつ$\delta x\leq0\hspace{0.1ex}$である場合: 180°
    3. $|\delta x|\leq|\delta y|\hspace{0.1ex}$であって、かつ$\delta y >0\hspace{0.1ex}$である場合: 90°
    4. $|\delta x|\leq|\delta y|\hspace{0.1ex}$であって、かつ$\delta y\leq0\hspace{0.1ex}$である場合: -90°
  • 各wordについて上記の処理を実行し、[0°, 90°, -90°, 180°]のうち最も多かった向きを採用する
    image.png

実装

向き判定コード
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)

image.png
image.png
image.png
image.png

参考文献

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6