実践型AIプログラミング特講#16

実践型AIプログラミング特講#16

今回も前回に引き続いて数字の分類問題について取り扱った記事を皆様に共有させていただこうと思います。

前回の数字の分類問題についての記事をご覧になっていない方はこちらからご確認ください。

OpenCVと機械学習:画像動画入門

論郭抽出 – はがきの郵便番号認識に挑戦しよう

大節では、郵便はがきの郵便番号を自動認識するプログラムを作ってみましょう。OnenCV による物体認識と、前回の手書き数字の判定のプログラムの組み合わせを行います。

郵便はがきから郵便番号を読み取ろう

郵便はがきに書き込まれている郵便番号の認識に挑戦します。その際、事前にはがきの画像から数字の書かれている部分を抽出する必要があります。手順としては、数字の書かれている領域を抽出し、その後で個別に数字を判定するというものです。機械学習では、データ学習の前処理として任意の領域を抽出しなくてはならないケースも多いものです。たとえば、複数の写真から人間の顔だけを取り出して学習させたい場合、当然ながら顔データだけを取り出しておく必要があります。そうした場合も、本節で行う領域の検出手法が役立つでしょう。

 OpenCV,で輪郭抽出

郵便番号に取りかかる前に、まずは画像のなかから輪郭を抽出するプログラムを作ってみましょう。輪郭を抽出するには、OpenCV のfindContours0 関数を利用します。たとえば、以下のような花の写真から大きな花を抽出してみましょう。以下のプログラムをJupyter Notebookで試してみましょう。

import cv2

import matplotlib.pyplot as plt

# 画像を読み込んでリサイズ(* 1)

img= cv2.imread(“flower.jpg”)

img = cv2.resize(img, (300, 169))

# 色空間を二値化

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

gray = cv2.GaussianBlur (gray, (7, 7), 0)

im2 =cv2. threshold(gray, 140, 240, cv2.THRESH_BINARY_INV) [1]

# 画面左側に二値化した画像を描画(* 3)

plt.subplot (1, 2, 1)

plt.imshow(im2, cmap=”gray”)

# 輪郭を抽出(*4)

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

# 抽出した枠を描面(* 5)

for pt in cnts:

. Y, w, h = cv2.boundingRect (pt)

# 大きすぎたり小さすぎる領域を除去

if w < 30 or w > 200: continue

print(x,y,w,h)# 結果を出力

cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, O), 2)

# 画面右側に抽出結果を描画(* 6)

plt.subplot (1, 2, 2)

plt.imshow (cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

plt.savefig(“find_contours.png”, dpi=200)

plt.show ()

すると、以下の2つの領域が抽出されます。これは、物体の輪郭を抽出したものです。そして、抽出した領域に赤線の枠を描画したものが表示されます。左側の画像は領域を検出するために用意した白黒二値画像で、右側の画像が抽出した領域に赤枠を描画したものです。うまく花の輪郭を抽出することができています。それでは、プログラムを確認してみましょう。プログラムの(※1)の部分では、花の画像を読み込む。続く(#2)以降の部分で輪郭抽出を行います。輪郭抽出の手順としては、画像の色空間を白と黒のを平滑化します。これにより、複雑で細かい模様などを検出しないように、画像がぼかされます。実みます。そして、画像を300 × 169 ピクセルにリサイズします。二値化します。そのために、まず画像をグレースケールに変換し、GaussianBlur0 関数を利用して画像際に画像を二値化するのが、threshold0 関数です。(※3)の部分では、先ほどぼかして二値化した画像を画面に出力します。そして、プログラムの(※4)の部分でfindContours) 関数を利用して、輪郭を抽出します。輪郭を抽出したら、(※5)の部分で抽出した領域を画像に描き込みます。最後に(※6)の部分で(※5)で作成した画像を画面の右側に出力します。改めて輪郭抽出の手順をまとめてみましょう。

(1) 画像を読み込む

(2) 画像を二値化する

(3) 輪郭抽出を行う

輪郭抽出を行う処理で重要なのは、画像を白黒二値化する作業でしょう。フルカラーの画像をグレースケールに変換して、平滑化して、二値化するのです。いくつもの処理を行うので面倒に感じますが、いずれの処理を省略しても、なかなか良い結果が出ません。ここで利用した関数の使い方を除認してみましょう。

画像の平滑化(ぼかし処理)について

OpenCV には、画像をぼかすために cv2.blur0 関数や、cv2.GaussianBlur) 関数、cv2.medianBlur0 関数、cv2.bilateralFilter0関数など、さまざまな平滑化関数が用意されています。このうち、ガウシアンフィルターの cv2.GaussianBlur) 関数はホワイトノイズの除去に適しています。以下の書式で利用します。

[書式]ガウシアンフィルター(画像のぼかし処理)

img = cv2.GaussianBlur(img, (ax, ay), sigma x)

この関数は、OpenCVで読み込んだ画像 img に対して、ガウシアンフィルターを適用して返します。(ax, ay) には平滑化する画素の周囲のサイズをピクセル単位で指定します。このとき、値には奇数を指定する必要があります。sigma_xは、横方向の標準偏差を指定します。0にした場合、カーネルのサイズから自動的に計算されます。ちなみに、平滑化関数を適用すると画像全体がぼかされますが、バイラテラルフィルターのcv2.bilateralFiter) 関数を使うと、エッジを残したまま画像をぼかすことができます。ただし、処理速度は遅くなります。

画像の二値化(しきい値処理)について

画像を白黒に変換する処理には、cv2.threshold0 関数を使います。この関数は、画像の二値化(しきい値処理)を行います。画像の画素が指定のしきい値より大きければ白、小さければ黒を割り当てる処理を行います。

[書式]画像の二値化

ret, img = cv2.threshold(Img, thresh, maxval, type)

この関数は、画像を二値化して返します。その際、第1引数には、グレースケール画像を指定します。第2引数は、しきい値を指定します。そして、第3引数には、しきい値以上の値を持つ値に対して割り当てる値を指定します。第4引数には、どのように二値化を行うのかを指定します。THRESH_BINARY_INV を指定した場合、しきい値よりも大きな値であれば 0、それ以外は maxvalの値にします。

輪郭の抽出について

輪郭を抽出するには、findContours() 関数を利用します。以下の書式で行います。

[書式]輪郭を抽出

contours, hierarchy =cv2.findContours(image, mode, method)

第1引数は入力画像、第2引数は抽出モード、第3引数は近似手法を指定します。戻り値は、輪郭リスト、階層情報が返されます。第2引数は輪郭の抽出方法を指定します。以下の定数を指定できます。第3引数のmethod には、輪郭の近似手法を指定します。以下の値を指定します。CHAIN_APPROX_NONEを指定すると輪郭のすべての点を検出し、CHAIN_APPROX_SIMPLE を指定すると不必要な点を削除し必要最低限の点だけを返します。そのため、一般的には、CHAIN_APPROX_SIMPLE を指定することになるでしょう。

 はがきから郵便番号の領域を抽出しよう

輪郭抽出の基本がわかったところで、はがきから郵便番号の番号部分の領域を抽出してみましょう。それでは、郵便番号の領域抽出に挑戦してみましょう。以下のようなプログラムを作ります。

Import cv2

Import matplotlib.pyplot as plt

#はがき画像から郵便番号領域を抽出する関数

def detect_zipno (fname):

# 画像を読み込む

img = cv2. imread (fname)

# 画像のサイズを求める

h, w = img.shape [:2]

# はがき画像の右上のみ抽出する( * 1)

Img=img [0:h//2, w//3:]

# 画像を二値化(* 2)

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]

#輪郭を抽出(*4)

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

# 抽出した輪郭を単純なリストに変換(* 4)

result = []

for pt in cnts:

x, y, w, h = cv2.boundingRect(pt)

# 大きすぎる小さすぎる領域を除去

if not (50 < w < 70): continue

result.append([x, y, w, h])

# 抽出した輪郭が左側から並ぶようソート(* 6)

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

# 抽出した輪郭が近すぎるものを除去(* 7)

result2 =[]

lastx = -100

for x, y, w, h in result:

if (x- lastx) < 10: continue

 

result2. append ( [x, y, w, h])

lastx = x

# 緑色の枠を描画(*8)

for x, y, w, h in result2:

cv2.rectangle (img, (x, y), (x+w, y+h), (0, 255, 0), 3)

return result2, img

if__name__==’__main__’:

# はがき画像を指定して領域を抽出

cnts, img=detect_zipno(“hagaki1.png”)

# 画面に抽出結果を描画

plt.imshow(cv2.cvtColor (img, cv2.COLOR_BGR2RGB))

plt.savefig(“detect-zip.png”, dpi=200)

plt.show ()

プログラムを実行すると、以下のように、郵便番号のある領域に緑色の枠線が描画され、正しく抽出できていることがわかります。プログラムを確認してみましょう。プログラムの(※1)の部分では、はがき画像象の上半分かつ右側3分の2の範囲を抽出します。というのも、郵便番号ははがきの右上に位置していることがわかっています。画像すべてに対して輪郭抽出を行うと、切手や左下の枠など不要なゴミ領域を拾ってしまいます。そこで、最初からだいたいの位置に見当をつけて、必要な領域を抽出します。プログラムの(※ 2)の部分では、画像を二値化します。先ほど紹介したように、グレースケール→ぼかし処理→二値化の処理を行います。続いて、(※3)の部分では、輪郭を抽出します。(※4)の部分では、抽出した輪郭を単純なX,Y,幅,高さ]のリストに変換します。その際、(※ 5)の部分のように、大きすぎたり小さすぎたりする領域を除去します。(※6)の部分では、抽出した領域をX方向にソートします。これによって、左側から順に領域を取得できます。ここまでの部分で、基本的な輪郭が抽出できますが、抽出したデータを見ると、郵便番号の枠線の外側と内側で別の輪郭として抽出している部分がありました。そこで、(※7)の部分では、重複している輪郭を除去します。そして最後に、(※ 8)の部分で、緑色の枠を描画します。

抽出した数字画像を判定しよう

ここまでの部分で領域を抽出できたので、数字を読み取りましょう。この部分は、前節で紹介した手き数字の判定がそのまま利用できます。

from detect zip import *

import matplotlib.pyplot as plt

「# 学習済み手書き数字のデータを読み込む

vith open(“digits.pkl”, “rb”) as fp:

clf = pickle.load(fp)

# 画像から領域を読み込む

cnts, img = detect_zipno(“hagaki1.png”)

# 読み込んだデータをプロット

for i, pt in enumerate(cnts):

х, у, w, h = pt

# 枠線の輪郭分だけ小さくする

X += 8

y += 8

w -=16

h-=16

# 画像データを取り出す

im2 = img[y:y+h, x:x+w]

# データを学習済みデータに合わせる

im2gray =cv2.cvtColor(im2, cv2.COLOR_BGR2GRAY)# グレースケールに変換

im2gray = cv2.resize (im2gray, (8, 8)) # リサイズ

im2gray=15 – im2gray /// 白黒反転

im2gray = im2gray.reshape ((-1, 64)) # 一次元に変換

# データ予測する

res = clf.predict(im2gray)

# 画面に出力

plt.subplot(1, 7, i + 1)

plt.imshow (im2)

plt.axis(“off”)

plt.title(str (res))

plt.show()

 

すると、以下のように表示されます。郵便番号の該当する部分を抽出し、それを手書き文字判定して数字の上に判定した数字を書き込んでいます。

改良のヒント

ここでは、基本的な輪郭抽出の手法について紹介しました。今回見たように、はがきの郵便番号など、一定のフォーマットに沿ったデータであれば、画像から特定の領域を抽出できます。また、ここでは試していませんが、はがきの郵便番号であれば、枠線が赤色であることが多いので、赤色の線だけを残し、その部分だけを抽出すると、より精度が高くなることでしょう。

応用のヒント

ここで紹介した輪郭抽出を行えば、画像のなかにある特定の部分を取り出せます。テストの答案用紙の自動採点をしたり、レシートに書かれている価格情報を取り出すなど、さまざまな用途に応用できるでしょう。

この節のまとめ

・ OpenCV を使うと手軽に輪郭抽出を行うことができる

・輪郭抽出するには、前処理としてグレースケールに変換し、ぼかし処理を行い、白黒二値化処理を行う

・はがきの郵便番号など、一定のフォーマットがある画像データであれば、場所やサイズの見当をつけて正確な領域を抽出できる

 

プログラミングカテゴリの最新記事