MENU

python はがきから郵便番号の輪郭取得

f:id:hanamichi_sukusuku:20201228160314p:plain

実行結果(左側元画像、右側出力結果)

f:id:hanamichi_sukusuku:20201228162142p:plain

まずif __name__ == '__main__':について

これは別ファイルからこのファイルがimportされた時と同時に実行されないようにしているものである。

hello.py

def main():
    print("Hello")

main()

if _ _ name _ _ == '_ _main_ _':がない場合。

>>> import hello  # この時点で「Hello」が表示されてしまう
Hello

if _ _ name _ _ == '_ _main_ _':がある場合。

>>> import hello
>>>

 

今回の処理はほとんどdetect_zipno()関数内で処理しているのでこの関数に関して細かく見ていく。

 

画像読み込み。無駄な領域を省く。

img = cv2.imread(fname)
# 画像のサイズを求める
h, w = img.shape[:2]
# ハガキ画像の右上のみ抽出する --- (*1)
img = img[0:h//2, w//3:]

画像ファイル.shapeで画像の高さ、幅、色を取得できる。

[:2]を指定することで取得した配列の最初の要素からインデックス番号が1の要素までを取得し変数に格納。[:2]を指定しなければ色の情報も取得してしまうので指定している。

img[0:h//2, w//3:]では画像の右上のみを抽出しており、今回ははがきの郵便番号を扱うことが分かっているので無駄な領域を省いていく。

画像の切り抜きは[y1:y2, x1:x2]のように指定する。

今回はw//3:とx2となる部分は記述はないのでx2は一番後ろまでを意味しており画像の幅を指定しているのと同じ結果になる。[0:h//2, w//3:w]でも同じ結果。

 

#リストについて補足

スライスという機能を使うとリストの一部を取得することができる。

リスト名[開始インデックス:終了インデックス] 開始インデックスから終了インデックスの手前までの要素を取得する。 開始インデックスと終了インデックスには負の値を指定することも可能。 開始インデックスを省略すると最初の要素から取得し、終了インデックスを省略すると最後の要素まで取得する。

【使用例】
list_number = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print list_number[1:5]
print list_number[1:-5]
print list_number[-5:9]
print list_number[:5]
print list_number[5:]
print list_number[:-5]
print list_number[-5:]
print list_number[:]

【実行結果】
[1, 2, 3, 4]
[1, 2, 3, 4]
[5, 6, 7, 8]
[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9]
[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

 

画像の二値化

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (3, 3), 0)
im2 = cv2.threshold(gray, 140, 255, cv2.THRESH_BINARY_INV)[1]

グレースケールに変更。

 

cv2.GaussianBlur()関数で平滑化(ぼかし処理)

引数は(画像ファイル, カーネルサイズ, 標準偏差)

カーネルサイズサイズ、標準偏差が大きくなるとぼかし具合が強くなる。

標準偏差に0を指定するとカーネルサイズから自動で標準偏差を指定してくれるので細かな調整が必要ない場合は0で十分。

 

cv2.threshold()で二値化。

(画像ファイル, しきい値, しきい値以上時の割り当てる値,どのように二値化を行うかの指定)

そして、返される値が二次元配列で二値化したデータはインデックス番号が1の部分にあるのでthreshold()[1]のように指定している。

 

輪郭の抽出

cnts = cv2.findContours(im2,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)[1]

 

cv2.findContours()関数で輪郭の抽出。

引数は(画像データ, 抽出方法, 近似手法)の順で指定。

これも必要なデータはインデックス番号1にあるので[1]を指定。

 

第三引数のcv2.CHAIN_APPROX_SIMPLEでは不必要な点を削除し、必要最低限の点のみを返している。cv2.CHAIHN_APPROX_NONEを指定すると輪郭上の全ての点を返す。

 

抽出したリストから外接する長方形のリストに変換

result =
for pt in cnts:
 x, y, w, h = cv2.boundingRect(pt)
 # 大きすぎる小さすぎる領域を除去 --- (*5)
  if not(50 < w < 70): continue
 result.append([x, y, w, h])

 

cv2.boundingRect()で輪郭に外接する長方形のタプルを返す

返り値は(左上からのx座標,y座標,幅,高さ)

appendメソッドはリストの末尾に要素を追加する。

 

抽出した輪郭が左側から並ぶようにソート(並び替え)

result = sorted(result, key=lambda x: x[0])

これを行うことで左から郵便番号の情報を扱えるようになる。

sorted()は並び替えの処理をするのに使用。

デフォルトは昇順、第二引数にreverse=Trueを指定すると降順になる。

二次元配列では今回のように第二引数のkeyに関数を指定するとソートされる前に各要素に適用させることができる。指定しないとそれぞれのリストの先頭の要素の小さいもの順にソートされる。

今回指定しているlamda関数は無名関数であり、xで要素を一つずつ受け取りx[0]の要素を返す処理をしている。つまり各要素の先頭の要素で比較しソートしている。

(この場合はkeyを指定しなくても同じ結果を得られる)

 

 

抽出した輪郭が近すぎるものを除去

result2 =
lastx = -100
for x, y, w, h in result:
  if (x - lastx) < 10: continue
 result2.append([x, y, w, h])
 lastx = x

抽出したデータを見ると枠線の内側と外側で別の輪郭として抽出している部分があった。

そのため要素のx座標の距離が近すぎるものを除去して新しいリストを作成。

このfor文ではその要素のxから一つ前の要素のxを引いた時10より小さい時、新しいリストに加えないようにしている。

一番最初に定義している-100の値は一番最初の要素のxをから引いた値が10以上になればなんでもいいと思う。

 

緑の枠を描画

for x, y, w, h in result2:
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 3)
return result2, img

cv2.rectangle()で描画。

returnで値を呼び出し元に返す。