【実践、OpenCV画像処理】検査対象領域の視点補正

February 11, 2022

OpenCV外観検査python

目次

はじめに

おうちにある身近な題材でpython+OpenCVによる外観検査の画像処理を楽しむ本企画。今回はお手軽に手持ちスマホで撮った画像を検査用の正立画像に変換することを題材とします。

検査対象が2次元平面の場合、通常、検査対象と垂直な方向からカメラを向けて正立撮影した画像を使用するのがベストですが、照明の関係やカメラ設置スペースの関係とかで、それがかなわない場合があります。そんなとき、視点をずらしてちょっと斜めから撮った画像を使用することになります。斜めから撮った画像を正立撮影した画像に変換すること(視点の補正)が本記事の目的です。

本記事では、pythonとOpenCVを使用しますので、動作確認する場合は、お手元のpython環境にOpenCVをインストールしておいてください。

$ pip install opencv-python
$ pip install opencv-contrib-python

いつものようにおうちに転がっている素材で進めていきます。今回はSDカードを長方形の白い紙の上にバラけていますが、この紙を検査対象が載ったトレイと見立て、トレイ領域を検査対象とします。

最初に結論を書きますが、本記事ではこのように撮った画像を、

sd_cards_orig2.jpg

最終的にはこのように変換するスクリプトを組んでいきます。

sd_cards_upright.jpg

尚、本記事に掲載する画像は、リンクをクリックするとフル解像度の画像に切り替わりますので、それをローカルにダウンロードしすると、記事中のサンプルプログラムで使用可能です。

トレイ領域抽出

今回は、黒っぽい場所にトレイに見立てた白い紙を配置した環境なんで、画像の2値化でトレイ領域を分離抽出し、OpenCVのコーナー検出機能を使ってトレイ領域の四隅を特定するという手順で進めます。

より実践的には、トレイに見立てた紙の四隅に十字マークのようなアライメントマークを描いておいて、そのアライメントマークを画像処理で検出して精度良く安定して四隅を特定するというのが現実的かもしれませんが、今回はやめておきます。

トレイ領域を抽出するために画像を2値化しますが、単純に2値化しただけではトレイ領域だけでなくノイズや他の映り込み物体も抽出されてしまいます。

sd_cards_bin.jpg

そこで、2値化後ラベリングし、サイズフィルターで最も大きな領域だけを残します。

sd_cards_tray_orig.jpg

最終的にはトレイに空いた穴を埋める必要がありますが、OpenCVで穴を埋めるためにはfloodFill()関数を使用しますが、このとき穴毎に穴の座標を指定しなければなりません。これでは自動化できませんので、トレイの外側だけを穴埋めし、それと元画像のxor論理演算でトレイだけを残すようにしています。

sd_cards_tray.jpg

トレイの台形領域が抽出できました。

ここまでのスクリプトが以下です。

extract_tray.py
import cv2
import numpy as np
img_orig = cv2.imread('sd_cards_orig2.jpg')
img_gray = cv2.cvtColor(img_orig, cv2.COLOR_BGR2GRAY)
img_gray = cv2.medianBlur(img_gray,5)
ret,img_bin = cv2.threshold(img_gray,110,255,cv2.THRESH_BINARY)
cv2.imwrite('sd_cards_bin.jpg', img_bin)

# トレイ領域抽出
n, label, data, center = cv2.connectedComponentsWithStats(img_bin)
sizes = data[1:, -1]
height, width = img_gray.shape[:2]
img_tray_orig = np.zeros((height, width, 3), np.uint8)
# サイズフィルター
for i in range(1, n):
    if 1000000<sizes[i-1]:
        img_tray_orig[label==i] = 255
img_tray_orig = cv2.cvtColor(img_tray_orig, cv2.COLOR_BGR2GRAY)
cv2.imwrite('sd_cards_tray_orig.jpg', img_tray_orig)
img_tray_temp = img_tray_orig.copy()
mask = np.zeros((height+2, width+2), dtype=np.uint8)
retval, img_tray_temp2, mask_tmp, rect_tmp = cv2.floodFill(img_tray_temp, mask=mask,
                    seedPoint=(0, 0),newVal=(255, 255, 255),loDiff=0,upDiff=0)
cv2.imwrite('sd_cards_tray_temp2.jpg', img_tray_temp2)
img_tray = cv2.bitwise_xor(img_tray_orig, img_tray_temp2)
img_tray = cv2.bitwise_not(img_tray)
cv2.imwrite('sd_cards_tray.jpg', img_tray)

画像の台形補正

台形領域を正立視点に変換するためには、抽出した台形領域の頂点の座標を知る必要があります。

まずOpenCVのコーナー検出機能を用いて台形領域の頂点の座標を抽出します。これで四隅だけ抽出できればよいのですが、多くの場合ちょっとしたノイズを拾って四隅以外の虚報も拾ってしまいます。四隅だけを拾うようにパラメーターをギリギリ調整すると、調整で使った画像ではうまくいっても、別の画像では四隅すら拾い漏れしてしまうかもしれません。そこで、今回はある程度たくさんの候補を拾うようにパラメーターを調整し、拾った候補中の最も端の点を頂点とすることにしました。

extract_corner.py
import cv2
import numpy as np
img_tray = cv2.imread('sd_cards_tray.jpg')
img_tray_gray = cv2.cvtColor(img_tray, cv2.COLOR_BGR2GRAY)
height, width = img_tray.shape[:2]
# トレイ領域の四隅を検出
img_tray_gray = np.float32(img_tray_gray)
corners  = cv2.cornerHarris(src=img_tray_gray, blockSize=50, ksize=25, k=0.04)
corners  = cv2.dilate(corners , None)

# トレイ領域の四隅座標を抽出
ret, corners = cv2.threshold(corners,0.01*corners.max(),255,0)
corners = np.uint8(corners)
ret, labels, stats, centroids = cv2.connectedComponentsWithStats(corners)
# 左上
l_lt = centroids[:,0]*centroids[:,0]+centroids[:,1]*centroids[:,1]
idx = np.argmin(l_lt)
p1 = np.array([centroids[idx,0], centroids[idx,1]])
# 右上
l_rt = (width-centroids[:,0])*(width-centroids[:,0])+centroids[:,1]*centroids[:,1]
idx = np.argmin(l_rt)
p2 = np.array([centroids[idx,0], centroids[idx,1]])
# 左下
l_lb = centroids[:,0]*centroids[:,0]+(height-centroids[:,1])*(height-centroids[:,1])
idx = np.argmin(l_lb)
p3 = np.array([centroids[idx,0], centroids[idx,1]])
# 右下
l_rb = (width-centroids[:,0])*(width-centroids[:,0])+(height-centroids[:,1])*(height-centroids[:,1])
idx = np.argmin(l_rb)
p4 = np.array([centroids[idx,0], centroids[idx,1]])

# 台形描画
img_disp = img_tray.copy()
x1, y1 = p1.astype(np.int64)
x2, y2 = p2.astype(np.int64)
x3, y3 = p3.astype(np.int64)
x4, y4 = p4.astype(np.int64)
cv2.line(img_disp,(x1, y1),(x2, y2),(0,0,255),8)
cv2.line(img_disp,(x2, y2),(x4, y4),(0,0,255),8)
cv2.line(img_disp,(x4, y4),(x3, y3),(0,0,255),8)
cv2.line(img_disp,(x3, y3),(x1, y1),(0,0,255),8)
cv2.imwrite('sd_cards_tray_region.jpg', img_disp)

実行結果はコレ。

sd_cards_tray_region.jpg

台形の頂点座標が正しいことが確認できたら、あとは台形補正を行います。四隅の点から変換行列を作成しwarpPerspective()を呼ぶだけ。このとき、抽出した台形を矩形に戻すために、トレイの実際のサイズと同じ縦横比の矩形を指定する必要があります。


   ...

# 台形補正
o_width = np.linalg.norm(p2 - p1)
o_width=int(np.floor(o_width))
o_height = np.linalg.norm(p3 - p1)
o_height=int(np.floor(o_height))
ori_cor = np.float32([p1, p2, p3, p4])
d_width = 174*20   # トレイの実際のサイズと同じ縦横比にする
d_height = 119*20  # 解像度は20pixel=1mm(508dpi)とした
dst_cor=np.float32([[0, 0],[d_width, 0],[0, d_height],[d_width, d_height]])
M = cv2.getPerspectiveTransform(ori_cor, dst_cor)
img_upright = cv2.warpPerspective(img_orig, M,(d_width, d_height))
cv2.imwrite('sd_cards_upright.jpg', img_upright)

台形補正の結果がコレ。

sd_cards_upright.jpg

まとめ

OpenCVを使用した視点補正(台形補正)の実例を示しました。台形の四隅の特定がキーになります。今回は白い紙を置きその四隅を求めましたが、アライメントマーク等を利用するのも一法でしょう。

斜視画像を入力とし、台形補正の結果画像を出力する通しのコードは以下です。

perspective_correction.py
import cv2
import numpy as np
img_orig = cv2.imread('sd_cards_orig2.jpg')
img_gray = cv2.cvtColor(img_orig, cv2.COLOR_BGR2GRAY)
img_gray = cv2.medianBlur(img_gray,5)
ret,img_bin = cv2.threshold(img_gray,110,255,cv2.THRESH_BINARY)

# トレイ領域抽出
n, label, data, center = cv2.connectedComponentsWithStats(img_bin)
sizes = data[1:, -1]
height, width = img_gray.shape[:2]
img_tray_orig = np.zeros((height, width, 3), np.uint8)
# サイズフィルター
for i in range(1, n):
    if 1000000<sizes[i-1]:
        img_tray_orig[label==i] = 255
img_tray_orig = cv2.cvtColor(img_tray_orig, cv2.COLOR_BGR2GRAY)
#cv2.imwrite('sd_cards_tray_orig.jpg', img_tray_orig)
img_tray_temp = img_tray_orig.copy()
mask = np.zeros((height+2, width+2), dtype=np.uint8)
retval, img_tray_temp2, mask_tmp, rect_tmp = cv2.floodFill(img_tray_temp, mask=mask,
                    seedPoint=(0, 0),newVal=(255, 255, 255),loDiff=0,upDiff=0)
#cv2.imwrite('sd_cards_tray_temp2.jpg', img_tray_temp2)
img_tray = cv2.bitwise_xor(img_tray_orig, img_tray_temp2)
img_tray = cv2.bitwise_not(img_tray)
#cv2.imwrite('sd_cards_tray.jpg', img_tray)

# トレイ領域の四隅を検出
img_tray_gray = img_tray.copy()
img_tray_gray = np.float32(img_tray_gray)
corners  = cv2.cornerHarris(src=img_tray_gray, blockSize=50, ksize=25, k=0.04)
corners  = cv2.dilate(corners , None)

# トレイ領域の四隅座標を抽出
ret, corners = cv2.threshold(corners,0.01*corners.max(),255,0)
corners = np.uint8(corners)
ret, labels, stats, centroids = cv2.connectedComponentsWithStats(corners)
# 左上
l_lt = centroids[:,0]*centroids[:,0]+centroids[:,1]*centroids[:,1]
idx = np.argmin(l_lt)
p1 = np.array([centroids[idx,0], centroids[idx,1]])
# 右上
l_rt = (width-centroids[:,0])*(width-centroids[:,0])+centroids[:,1]*centroids[:,1]
idx = np.argmin(l_rt)
p2 = np.array([centroids[idx,0], centroids[idx,1]])
# 左下
l_lb = centroids[:,0]*centroids[:,0]+(height-centroids[:,1])*(height-centroids[:,1])
idx = np.argmin(l_lb)
p3 = np.array([centroids[idx,0], centroids[idx,1]])
# 右下
l_rb = (width-centroids[:,0])*(width-centroids[:,0])+(height-centroids[:,1])*(height-centroids[:,1])
idx = np.argmin(l_rb)
p4 = np.array([centroids[idx,0], centroids[idx,1]])

# 台形描画
img_disp = img_orig.copy()
x1, y1 = p1.astype(np.int64)
x2, y2 = p2.astype(np.int64)
x3, y3 = p3.astype(np.int64)
x4, y4 = p4.astype(np.int64)
cv2.line(img_disp,(x1, y1),(x2, y2),(0,0,255),8)
cv2.line(img_disp,(x2, y2),(x4, y4),(0,0,255),8)
cv2.line(img_disp,(x4, y4),(x3, y3),(0,0,255),8)
cv2.line(img_disp,(x3, y3),(x1, y1),(0,0,255),8)
)
cv2.imwrite('sd_cards_tray_region.jpg', img_disp)

# 台形補正
o_width = np.linalg.norm(p2 - p1)
o_width=int(np.floor(o_width))
o_height = np.linalg.norm(p3 - p1)
o_height=int(np.floor(o_height))
ori_cor = np.float32([p1, p2, p3, p4])
d_width = 174*20   # トレイの実際のサイズと同じ縦横比にする
d_height = 119*20  # 解像度は20pixel=1mm(508dpi)とした
dst_cor=np.float32([[0, 0],[d_width, 0],[0, d_height],[d_width, d_height]])
M = cv2.getPerspectiveTransform(ori_cor, dst_cor)

img_upright = cv2.warpPerspective(img_orig, M,(d_width, d_height))
cv2.imwrite('sd_cards_upright.jpg', img_upright)

Written by questions6768 who lives in Uji, Kyoto.