【Python】画像を領域ごとに分割することを学習した記録

完全に将来の自分が思い出すためのメモ記事です。

目的

一般的なフォーマットの書類を普通にOCRすると

請求書
A社御中
A社の郵便番号      発行:B社
A社の住所        B社の住所 社
A社担当者名       B社担当者名判


こんな感じのテキスト(実際はさらにぐちゃぐちゃ)になっちまうので、「宛先情報エリア」「発行主情報エリア」「金額記載エリア」と分割して読み取らないと二進も三進もいかねぇやってなったので、なんか良い感じに書類を分割する方法を探る。

参考:

こちらがわかりやすそうだったので、まずはこちらを参考に進める。

とりあえずopenCVで画像をRGB値の配列として読み込んでみる

サンプル画像は前回作ったPDFをjpeg化したもの。

import cv2, matplotlib
img = cv2.imread("image/ocr_test_pdf_1.jpeg")
print(img)

おりゃ。

[[[255 255 255]
 [255 255 255]
 [255 255 255]
 ...
 [255 255 255]
 [255 255 255]
 [255 255 255]]
[[255 255 255]
 [255 255 255]
 [255 255 255]
 ...
 [255 255 255]
 [255 255 255]
 [255 255 255]]
[[255 255 255]
 [255 255 255]
 [255 255 255]

 ...
 [255 255 255]
 [255 255 255]
 [255 255 255]]]

よしよし。全部真っ白だけど、読んだの書類だから、左端のピクセルは全部白よな。そりゃな。

……あんまりなんで別の画像を読ませて確認。

[[[153 204 102]
 [153 204 102]
 [153 204 102]
 ...
 [153 204 102]
 [153 204 102]
 [153 204 102]]

よしよし。

グレースケールに変換する

gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
print(gray_img)

これだけ

本当にグレスケになったか確認

cv2.imshow("image", gray_img)
cv2.waitKey()

cv2.imshowで画像を表示できる。第一引数はウィンドウ名なので適当に。第二引数はcv2で読み込んで二次元配列化した画像を渡す。

そのまま実行すると一瞬で画面消えるので、waitKeyを追加。

こうして関数化しておくと後が楽。

def display_img(mat):
   cv2.namedWindow("image", cv2.WINDOW_NORMAL) # 可変ウインドウ「image」を開く
   cv2.imshow("image", mat) # imageウインドウの中に受け取った画像を展開する
   cv2.waitKey()

画像の平均色を求める

import numpy as np

avarage_color_per_row = np.average(img, axis=0)
avarage_color = np.average(avarage_color_per_row, axis=0)
avarage_rgb = np.uint8(avarage_color)

numpyを使って行ごとの平均値出して、行ごと平均全体の平均を出して、その結果をunit8(8ビット値、0~255の範囲で表される数値)に戻して平均値を出す。

指定の色の100px四方のベタ塗り画像を作る

avarage_color_img = np.array([[avarage_rgb]*100]*100, np.uint8)

上記で求めた平均値が100個並んだ行が100列ある二次元配列を作る。

できた二次元配列を、cv2.imgshow()に渡せば表示できる。

画像を二値化する

_, threshold_img = cv2.threshold(gray_img, 180, 255, cv2.THRESH_BINARY)
display_img(threshold_img)

グレスケ化した画像をcv2.thresholdに渡すと二値化してくれる。

cv2.thresholdの戻り値が2つあるので、使わない1つ目は_(アンダースコア)で受け取って抹消する。display_imgはさっき自作したプレビュー表示用の関数。

cv2.thresholdの第一引数は元画像、第二引数がしきい値、第三引数が最大値、第四引数は処理モード。よくわかんない。

白黒の書類をより鮮明にするには180位が適量だったけど、色々な条件の書類を良い感じに受け取るにはどうしたらいいかなー。悩ましいなー。

ガウスぼかす

画像全体にガウスぼかしを掛けてから外接四角形を抽出すれば良い感じに範囲分けができるんじゃない?って思ったんですよ。

img_blur = cv2.GaussianBlur(gray_img, (51,51), 0)
display_img(img_blur)

これだけでガウスぼかしを掛けられます。便利ねぇ……

cv2.GaussianBlurの第一引数が元画像を読み込んだ配列、第二引数がぼかす半径。このとき、半径は必ず奇数で入力しないといけない。奇数よ奇数。

ガウスぼかして二値化

で、ぼかしを掛けた書類相手に二値化を試して見ると

img_blur = cv2.GaussianBlur(gray_img, (81,81), 0)
display_img(img_blur)
_, threshold_img = cv2.threshold(img_blur, 248, 255, cv2.THRESH_BINARY)
display_img(threshold_img)

画像1

こうなって

画像2


こう!

いいんじゃないの、近づいてきたんじゃないの?

白黒反転

この後の輪郭検出をするとき、「明るいところが関心ごと」として判断されるので、背景黒、抽出したいエリアが白になるよう反転する。

二値化した画像(threshold_img)を用意しておいて

reversal_img = cv2.bitwise_not(threshold_img)

これも一行。便利な世の中ねぇ……

画像3

よしよし。

輪郭を検出する

contours, _ = cv2.findContours(reversal_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

第一引数は二次元配列化した画像。「背景が黒、抽出したいものが白」くなっている画像を渡す。

第二引数は輪郭検出方法のタイプ。「cv2.RETR_EXTERNAL」で外周の輪郭だけ抽出できる。

第三引数は、パスを単純化するかどうか。「cv2.CHAIN_APPROX_SIMPLE」で単純化する。単純化しておいた方が動作が軽い。精度は落ちる。

これまた戻り値が2つあるので、2つめは_で受け取って抹殺。

輪郭を描画する

with_contours_img = cv2.drawContours(img, contours, -1, (0, 255 ,0), 5)
display_img(with_contours_img)

drawContoursに、輪郭を書き足したい画像と、輪郭抽出で取得した輪郭のリストを渡す。

第三引数は、輪郭リストのうちどれを描画すんのかい?という指定。0ならリスト内の一番最初の輪郭、1ならリストの2番目……って具合。-1を指定すると全部描画される。

第四引数は、輪郭の色をRGBで指定するんだけど、このとき、第一引数に渡した画像がモノクロの場合、最初の0~255だけがグレースケール値として反映されてしまうので注意。(第一引数には一番大元のカラー画像を渡すのがいい)

第五引数は描画する輪郭の太さ。

※ただし、この後元画像をさらに何か別の処理に使いたい場合、drawContoursの引数に渡した時点で上書きされてしまうので、コピーを渡すといい。

取得した領域の外接四角形を取得する

今回は四角く分割してOCRに繋げたいので、領域の外接四角形を取得する。

bounding_img = np.copy(img)
for contour in contours:
   x, y, w, h = cv2.boundingRect(contour)
   bounding_img = cv2.rectangle(bounding_img, (x, y), (x + w , y + h), (0,255, 0), 3)
display_img(bounding_img)

cv2.boundingRectに領域を渡すと、「左上のX座標、左上のY座標、幅、高さ」の4つが帰ってくる。

それをcv2.rectangleに、(書き込む画像、(左上のX座標、左上のY座標)、(右下のX、右下のY)」の順番に渡す。あと、色と太さ。

画像4

おーしおーし。

とりあえずここまで。今度はこの四角形で画像を切り抜いて行きたいぞっと。

この記事が気に入ったらサポートをしてみませんか?