【実践、おうちで外観検査】欠陥認識編

January 20, 2022

OpenCV外観検査python

目次

はじめに

外観検査とは何か?については、例えばキーエンスのサイトとかLINXのサイトとかを見れば事例がたくさん載ってます。自分がなにかの生産工場の品質担当者で、製品の外観検査が必要になればこれらの会社(ここにあげたのはいわば頂点です)にお願いして検査システムを構築してもらうのが最善でしょうが、それではお金がいくらあっても足りません。

精度とか安定性はとりあえず目をつむって、お手軽におうちにある身近な題材で外観検査を試してみるのが本記事の目的です。開発環境はpython上のOpenCVを利用します。OpenCV通常の画像処理から機械学習までもカバーするコンピュータビジョン用の超巨大ライブラリです。

本記事では、例としてスマホの外観検査を試みます。

pythonとOpenCVを使用しますので、お手元のpython環境にOpenCVをインストールしておいてください。

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

典型的な外観検査のための画像処理

検査対象物が何であっても、だいたい次のような手順で検査をすることになるでしょう。

[検査対象の生画像]
      ↓
検査対象領域の欠陥を強調するための前画像処理
      ↓
[対象領域の欠陥が強調された画像]
      ↓
画像をラベリングして欠陥候補を分離
      ↓
[ラベリング画像]
      ↓
欠陥候補毎に真の欠陥か否かを判定
      ↓
[検査結果]

スマホの外観検査を例に、この手順を実装していきます。

前画像処理

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

cellphone_orig.jpg

普通にスマホのカメラで検査対象を撮りました。けっこう汚いスマホですね。ちなみにGoodleのNexus5です。筐体が樹脂製なんで傷がつきやすい。このスマホに傷がないかを撮った画像から検査していきます。

画像を2値化し、反転すると…

step1.py
import cv2
img = cv2.imread('cellphone_orig.jpg', 0)
ret,img_bin = cv2.threshold(img,60,255,cv2.THRESH_BINARY)
img_bin = cv2.bitwise_not(img_bin) # 白黒反転
cv2.imwrite('cellphone_bin.jpg', img_bin)

cellphone_bin.jpg

これだけでも欠陥やゴミを浮き出させることはできますが…

人の目にはスマホ表面は同じ色に見えていても実際は濃淡があり、欠陥を分離するしきい値1つだけでは表面の暗い領域も採れてしまいました。

OpenCVには「適応的しきい値処理」という機能があります。小さく区切った領域に応じて2値化しきい値を変えて2値化するというもので、結果として階調が大きく変化するエッジが抽出されます。

適応的しきい値処理を使って2値化してみます。

step2.py
import cv2
img = cv2.imread('cellphone_orig.jpg', 0)
img_adpth = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,\
                cv2.THRESH_BINARY,31,5)
cv2.imwrite('cellphone_adpth1.jpg', img_adpth)

cellphone_adpth1.jpg

画像のエッジがきれいに抽出され、遠目には欠陥やゴミのエッジも見えるようになりました。スマホ表面の濃淡の影響もありません。しかし、拡大してみると、無数の黒い点が…

cellphone_adpth1_zoom.png

元画像のミクロな濃淡が最終的に強調されて微細なエッジになってしまった訳で、こうなってしまいました。

適応的しきい値処理の前に、medianフィルターで画像の近傍ピクセルを平均化し、なだらかにボカしておくと、

step3.py
import cv2
img = cv2.imread('cellphone_orig.jpg', 0)
img = cv2.medianBlur(img,5)
img_adpth = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,\
                cv2.THRESH_BINARY,31,5)
cv2.imwrite('cellphone_adpth2.jpg', img_adpth)

スマホ表面の無数の黒い点は消え、傷とほこりだけが浮かび上がりました。

cellphone_adpth2.jpg

しかし、このままでは背景が無数の黒点として写り込んでいます。背景を除去するために、元の画像からスマホ領域だけのマスクを作成しておきます。

具体的には、スマホ領域が抽出できるように2値化し、モノクロ画像のモルフォロジー演算で白領域の収縮/膨張処理を行い、黒領域中の白ノイズと、白領域中の黒ノイズを除去します。

step4.py
import cv2
img = cv2.imread('cellphone_orig.jpg', 0)
ret,img_bin = cv2.threshold(img,110,255,cv2.THRESH_BINARY)
img_bin = cv2.bitwise_not(img_bin) # 白黒反転
# カーネルを作成
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# 膨張
img_bin = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN, kernel, iterations=3)
# 収縮
img_bin = cv2.morphologyEx(img_bin, cv2.MORPH_CLOSE, kernel, iterations=20)
cv2.imwrite('cellphone_mask.jpg', img_bin)

完成したマスク画像がこれ。

cellphone_mask.jpg

このマスクを適用すると、背景が除去されます。

step5.py
import cv2
img_obj = cv2.imread('cellphone_adpth2.jpg', 0)
img_mask = cv2.imread('cellphone_mask.jpg', 0)
img_mask_inv = cv2.bitwise_not(img_mask)
img_obj = cv2.bitwise_or(img_obj, img_mask_inv)
img_obj = cv2.bitwise_not(img_obj)
cv2.imwrite('cellphone_obj.jpg', img_obj)

検査対象が限定されました。

cellphone_obj.jpg

ラベリング

この時点では、画像を人の目で見ると傷とほこりが見えていますが、傷とほこりをコンピュータが認識できた訳ではありません。

傷とほこりを認識させるためには、ラベリング処理を行います。ラベリング処理とは、モノクロ2値画像中の独立した白領域を分離することです。

ラベリングの結果を可視化するために、領域毎に色分けしてみます。

step6.py
import cv2
import numpy as np
import random
# 画像をグレースケールで読み込み
img_gray = cv2.imread('cellphone_obj.jpg', 0)
# ラベリング処理
ret, markers = cv2.connectedComponents(img_gray)
# ラベリング結果書き出し準備
img_labeled = cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)
height, width = img_gray.shape[:2]
colors = []
# ランダム色生成
for i in range(1, ret + 1):
    colors.append(np.array([random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)]))
# ラベリング結果を色分け
for y in range(0, height): 
    for x in range(0, width):
        if markers[y, x] > 0:
            img_labeled[y, x] = colors[markers[y, x]]
        else:
            img_labeled[y, x] = [0, 0, 0]
# 結果画像を書き出し
cv2.imwrite('cellphone_labeled.jpg', img_labeled)

結果はコレ。

cellphone_labeled.jpg

欠陥やほこりだけでなくスマホ本体のエッジやカメラや刻まれたロゴのエッジまで抽出されています。また、数ピクセル未満の微小な領域が虚報としてあらわれています。

欠陥判定

前回の結果から、スマホ本体のエッジやカメラや刻まれたロゴのエッジを取り除き、数ピクセル未満の微小な領域が虚報としてでてくるのを抑制するため、領域のサイズでフィルタリングし、欠陥やほこりだけを残します。

step7.py
import cv2
# 元画像をグレースケールで読み込み
img_orig = cv2.imread('cellphone_orig.jpg', 0)

# 前処理画像をグレースケールで読み込み
img_gray = cv2.imread('cellphone_obj.jpg', 0)
# ラベリング処理
n, label, data, center = cv2.connectedComponentsWithStats(img_gray)
sizes = data[1:, -1]
# ラベリング結果書き出し準備
img_result = cv2.cvtColor(img_orig, cv2.COLOR_GRAY2BGR)
# サイズフィルター
for i in range(1, n):
    if 100<sizes[i-1] and sizes[i-1]<3000:
        #img_result[label==i] = 255
        # 各オブジェクトの外接矩形を赤枠で表示
        x0 = data[i][0]-2
        y0 = data[i][1]-2
        x1 = x0 + data[i][2] +4
        y1 = y0 + data[i][3] +4
        cv2.rectangle(img_result, (x0, y0), (x1, y1), color=(0, 0, 255), thickness=3)
# 結果画像を書き出し
cv2.imwrite('cellphone_recog_result.jpg', img_result)

結果はコレ。

cellphone_recog_result.jpg

拡大してみると…

cellphone_recog_result_zoom.png

ちゃんととれているのもあるし、見逃しもあるし…いまいちですね。

でも、検査の流れは掴めたかと思います。

まとめ

pythonとOpenCVで、スマホの検査を例に、外観検査の画像処理の流れを追ってみました。

今回の外観検査実験は、いってみれば工学部の低学年の実習レベルの話で、今回のようなopencvで高々数十ステップのスクリプトで実用的な検査ができる訳ではありません。これでいいなら高価なキーエンスもLINXも不要ですもんね。そんな訳ありません。

opencvでやるならここから先地獄が待っているのです。


Written by questions6768 who lives in Uji, Kyoto.