🤖

異常検知技術を比較してみた(ルールベース, HLAC, PatchCore)

2022/09/20に公開

はじめに

異常検知AIソフトウェア・クラウドサービスを提供しているアダコテックでエンジニアをしているshin-ueです!

今回は、弊社内で保有する電子部品を撮像した画像データに対して、異常検知を実施してみます。


そもそも異常検知って?

異常検知における異常とは、みなさん何を思い浮かべますか?

例えば、毎日休まず周期的な信号を出力してくれるシステムが一家に一台あったとします。
機械とよくケンカする僕みたいな人間は「ほんとうに周期的な信号だしてるのか!?」と疑いをかけてモニタリングしてしまいます。

t=9~11の出力値がいつもと違う変化をしているぞ・・・不良品だコイツこのやろう!(過激派)

という感じで、「いつもの正常状態とは異なる状態」を異常と表現しています。

定期的に状態を監視し異常傾向を予兆してあげたり、異常状態を検出するような仕組みを作成し、異常発生したら関連システムにアラート信号を送ってあげたり、異常に対して適用される策は色々と存在しています。これによって、人と機械の平和が訪れるんですね~。完

・・・はい、茶番でした。

弊社アダコテックがターゲットとしている製造業の「外観検査工程」では、その名の通り、製品外観にキズや汚れがないことを人の目やカメラ画像データ等から判断し、製品の品質をチェックしています。後者のカメラ画像データについては、製品の正常特徴・異常特徴をしっかりと可視化させる必要があります。これについては、私のnoteのほうに記載しているので、お時間ある時に読んでみてください!
https://note.com/shin_ue626/n/n4afc48e73d25
本記事では、取得した画像データに対して、以下のような手法で異常検知をしてみようと思います。

  1. OpenCV関数でゴリゴリ異常検知アルゴリズムを構築 (画像処理ルールベース)
  2. 弊社アダコテック技術で異常検知モデルを作成 (HLAC特徴量を利用)
  3. 最新AI論文技術を適用 (PatchCore)

対象画像データについて

積層セラミックコンデンサ(=MLCC)です。実物は小指の爪先くらいに小さいです。これの良品と不良品を弊社の優秀なメンバーが撮像してくれました\( 'ω')/やったぜ

画像枚数は以下の通り。

ラベル 枚数
良品 110枚
不良品 18枚

画像データの条件は以下の通り。

  • 解像度954×1181pixel
  • グレー画像

画像中にうつっているMLCCの特徴としては以下の通り。

  • 端子電極部のサイズが個体ごとにバラついている
  • 端子電極部の輝度値は255に近く、ほぼサチっている
  • セラミック部は輝度値は80~140で個体間でバラついている
  • 欠陥は6種存在(ワレ、カケ、スリキズ、キズ、打痕、異物)
  • 正常状態と比較して輝度が局所的に変化している欠陥もあれば、広域で変化しているものもある

画像処理ルールベース

画像処理をするうえで有名なライブラリといえばOpenCVですね!製造業ではMVTecのHALCONというライブラリも有名だったりもしますね。

このOpenCVのもつ関数を利用して、異常検知プログラムを構築してみます。
なお、以降のHLACやPatchCoreでの検証条件にあわせるため、以下のようなデータセット構成としています。

ラベル 枚数 詳細
train 50 平均画像作成に利用する良品画像。
validation_ok 10 アルゴリズム構築、およびパラメータ調整に利用する良品画像。
validation_ng 5 アルゴリズム構築、およびパラメータ調整に利用する不良品画像。
test_ok 50 構築したルールベースアルゴリズムで検査するテスト用良品画像。
test_ng 13 構築したルールベースアルゴリズムで検査するテスト用不良品画像。

まずは、明らか不良部位を検出するための一番シンプルな手法として、平均差分法があります。
train=50枚の画像から平均画像を作り出してみましょう。

def calc_average(files):
    img = cv2.imread(files[0], cv2.IMREAD_GRAYSCALE)
    height, width = img.shape
    
    # 各画像を読み込み加算
    ave_img = np.zeros((height, width), dtype=np.uint32)
    for file in files:
        img = cv2.imread(file, cv2.IMREAD_GRAYSCALE)
        ave_img = ave_img+img
    
    # 読み込んだファイル数で各pixelの輝度値を割る
    ave_img = ave_img/len(files)
    return ave_img.astype(np.uint8)


learn50枚の平均画像
そして、入力画像と平均画像の絶対値差分をcv2.absdiff関数を利用して取得してみましょう。

validation_ngの各データと平均画像の差分結果
さらに任意閾値による二値化処理を適用してみると以下のように。

validation_ngの各データと平均画像の差分画像を二値化
(各画像の不良部位の抽出具合については後述します) 二値化画像を見ると、どうやら外部電極の端部が残ってしまっていますね。これは、前述した通り、個体ごとに外部電極のサイズにバラツキがあるため、平均画像との差分をとったときにバラツキ分が残ってしまったのです。
であれば、外部電極部のエッジ領域を抽出し、それをマスクとしてさらに引いてしまえば良いかも!という思いつきで、いざ実装!

エッジ領域の抽出といえば、SobelやLaplacian、Cannyといったフィルタがありますが、今回は比較的キレイに抽出できるDoG(Difference of Gaussian)を利用しようと思います。DoGは、σが異なる2種のGaussianフィルタを適用し、それぞれで差分を計算するといったアルゴリズムです。

def difference_of_gaussian(img, ksize, sigma, k):
    # ガウシアンフィルタ適用
    gaussian_1 = cv2.GaussianBlur(img, (ksize, ksize), sigma)
    gaussian_2 = cv2.GaussianBlur(img, (ksize, ksize), sigma*k)

    # 2種のガウシアンフィルタ適用画像の差分
    dog = gaussian_1-gaussian_2

    return dog

得られた結果に対して膨張フィルタcv2.dilateを適用して、以下のようなマスクが完成します。

端部マスク画像
先ほどの二値化画像に対して、このマスクを差分cv2.subtractすると以下のような結果となりました。

validation_ngの各データと平均画像の差分画像を二値化(端部マスク適用)
端子電極部にあるカケや、セラミック部のダコンといった、サイズも輝度コントラストも大きい欠陥については二値化処理で不良部位を抽出できてそうですね。(一応、キズ②についても一部分だけは抽出できている)

一方で、キズ①や異物については差分画像を見ても不良部位はハッキリとわからず・・・。この原因としては、個体ごとのセラミック部の輝度バラツキでした。

平均画像を作成するのに利用したlearn=50枚についてはセラミック部の輝度は130~150でしたが、validationデータについては80~130と輝度分布が異なっていました。
ゆえに、平均画像(learn=50枚)との絶対値差分をとったときに、キズとイブツの不良部位輝度よりもその他領域輝度との差が大きくなってしまいました。

製品生産時ロットや母材による表情差や、装置号機による撮像系のバラツキによって、同じ製品でも画像上で見た目がばらつくことがあり、これを加味したルールベースを構築しないといけないのが中々に大変ですね・・・。(製造業で外観検査アルゴリズムを構築した人なら頷くはず)

さて、平均差分法では個体ごとの輝度バラツキに影響されてしまい、一部の不良部位を検出することができませんでした。そこで、個体ごとの輝度バラツキに影響されず、局所的な輝度変化領域を抽出する手法である動的閾値法(適応的閾値処理) を利用してみます。

この手法は、任意サイズの局所領域ごとに平均値(または中央値)を計算し、局所領域内の注目画素との差分をとり、二値化処理を適用していくようなアルゴリズムとなっています。これによって、画像内で輝度ムラがあったとしても、それの影響を受けずにきれいな二値化処理を適用することが可能となります。

OpenCVにはcv2.adaptiveThresholdといった適用的閾値処理の関数が用意されていますが、ここではアルゴリズムに沿って実装してみようと思います。

def dynamic_threshold(img, ksize, threshold):
    # 中央値フィルタ適用
    median = cv2.medianBlur(img, ksize)

    # フィルタ適用画像と元画像の絶対値差分を行い、任意閾値の二値化処理を実施
    diff = cv2.absdiff(img, median)
    _, dynth = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY)

    return dynth 

平均差分法と同様に、最後に外部電極マスクとの差分を行った結果が以下となります。

validation_ngの各データに動的閾値法を適用(端部マスク適用)
平均差分法で不良部位の抽出ができていなかったキズ①と異物が見事に抽出できました!(相変わらずキズ②については、一部のみの検出ですが・・・)

ただ一方で、カケ全体とダコン中央部がとれていません。これは局所領域の平均値(または中央値)をとって差分をするといったアルゴリズムであるがゆえに、大きい面積の不良部位になると、平均値(または中央値)と元の輝度値はほぼ同等なので、差分結果=0となってしまうのです。

これが「ルールベース手法」の特徴で、欠陥の特徴に合わせて画像処理のアルゴリズムを構築していくという開発者泣かせところがあります。さらには制御するパラメータが多くなりすぎたり、謎のマジックナンバーがあったり、実装者のみ理解できるプログラムが出来上がってしまうという・・・。

とりあえず、平均差分法と動的閾値法の2種を組み合わせたものを「ルールベース手法」のアルゴリズムとします!

これでテスト画像を検査してみましょう。
評価方法としては、以下のようにしました。

  • アルゴリズムを適用した結果画像(二値化画像)の抽出画素の数を画像の異常値とする。
  • 横軸False Positive Rate、縦軸True Positive RateとしたROCカーブからAUCを計算し、それをアルゴリズムの性能とする。

結果は以下の通り。

全てのtest_ng画像を異常判定させようとすると、test_ok画像の約4割を過検出してしまう結果がわかりました。実際に検査結果画像を見てみましょう。
どこを不良部位として抽出したのかわかるように、元画像に異常箇所を赤色で重ねたヒートマップとしました。

test_ngの各データをルールベース手法で検査した結果
輝度コントラストが低めのキズが苦手のようですね・・・。
また、過検出したtest_ok画像は以下のように、外部電極のサイズ差によるものが主でした。

ここからさらに精度を上げようと思うと、以下のような処理が追加で必要になると思います。

  • 外部電極部のみを動的に検出し、その範囲内で欠陥検出のアルゴリズム適用。
  • セラミック部の輝度分布を任意分布に変換(オフセットするか、ヒストグラムマッチングという手法を適用するか)し、欠陥検出アルゴリズム適用。

このように欠陥種が多く、かつ良品でも表情差があるような製品になると適用すべきアルゴリズム(そしてそれを制御するパラメータ数)が膨大となっていきます。さらに、製品の種類も多く存在していると考えたら・・・( ノД`)
ルールベース手法はアルゴリズムの管理は大変ではありますが、個別最適化しやすく、この後紹介する機械学習モデルよりも高精度になる可能性を秘めています。
また、不良部位の詳細解析(輝度の統計量を取得したり、寸法計測したり)をするのも得意だったりします!


長々とルールベース手法について紹介してきましたが、ここからはサクっとHLAC特徴量を利用した手法の結果と、最新AI技術PatchCoreを利用した結果を紹介していきます!

HLAC特徴量を利用

弊社アダコテックで利用しているHLAC特徴量を利用した異常検知を試してみます。
HLAC特徴量について知りたい方は以下の記事をご確認ください!
https://zenn.dev/kotaro_inoue/articles/f0cbbca962313b
https://zenn.dev/kei1978/articles/3191203ece3a74
複数の良品画像からHLAC特徴量を取得し、製品の正常空間を定義した異常検知モデルを作成しています。学習に未使用のデータを入力としたとき、この正常空間からの乖離度によって良品なのか不良品なのかを判断するような仕組みとなっています。
もっと詳細を知りたい/使ってみたいという方は是非弊社にお問い合わせください!(拙い営業トーク)

データセットは、前述のルールベース手法と同様です。

ラベル 枚数 詳細
train 50 学習(正常空間を定義)に利用する良品画像。
validation_ok 10 学習時のパラメータを最適化するために利用する良品画像。
validation_ng 5 学習時のパラメータを最適化するために利用する不良品画像。
test_ok 50 作成したモデルで検査するテスト用良品画像。
test_ng 13 作成したモデルで検査するテスト用不良品画像。

作成したモデルで検査した結果は以下の通り。

AUC=1(不良流出および過検出が無い)を叩き出してしまいました。出来レースだと思われてしまう
ヒートマップ画像も見てみましょう!なお、任意サイズのパッチごと異常値の計算を行っており、設定した閾値以上のパッチが赤色系で描画されています。

test_ngの各データをHLAC特徴量利用手法で検査した結果

PatchCore

こちらでDetection AUROCランキング堂々の1位となっている手法PatchCoreを利用してみました。
最近流行り?の学習済みNNを利用して特徴量を抽出する手法ですね。ここでは、詳細については割愛しますが、詳しく知りたい方はumapyoi様の記事、わっしー様の記事がわかりやすいと思います!
https://qiita.com/umapyoi/items/7c3e9b42388d576057b1
https://zenn.dev/kwashizzz/articles/ml-anomaly-det-patchcore
データセットは、前述のルールベース手法と同様です。

ラベル 枚数 詳細
train 50 学習(正常空間を定義)に利用する良品画像。
validation_ok 10 不使用。
validation_ng 5 不使用。
test_ok 50 作成したモデルで検査するテスト用良品画像。
test_ng 13 作成したモデルで検査するテスト用不良品画像。

設定したパラメータはbackborn=WideResNet50, sampler=1%としています。
作成したモデルで検査した結果は以下の通り。

Detection AUROC=0.997という中々素晴らしい結果。

test_ngの各データをPatchCore利用手法で検査した結果
ヒートマップも良い感じですね!


さいごに

以下に結果をまとめます。

手法 DetectionAUROC 実行環境 処理時間
画像処理ルールベース 0.925 CPU 0.150msec
HLAC特徴量利用 1.0 CPU 0.090msec
PatchCore利用 0.997 GPU 0.110msec

今回はHLAC特徴量を利用した手法が最も優れていました。短時間の中でパパっと検証した結果なので、各々しっかりと調整することでより良い結果が出ると思います!

また、それぞれの処理時間は、1枚当たり数十msec~100msec代でした。ただ、各手法で実行環境が異なっています。それぞれ環境の最適化を行うことで、安定して数十msecの処理時間になるはずです。ちなみにHLAC特徴量を利用した手法はCPUのみで高速に駆動するので、GPU不要というお財布に優しい手法でもあります!w

さらに対象画像を確認してから検査アルゴリズム(モデル)が構築できるまでに要した時間は、HLACとPatchCoreは20分以内、画像処理ルールベースについては約1時間でした。ルールベースについては、まだまだ要調整部分が多いのでさらに時間かかりそうですね。一方で、HLACやPatchCoreのように学習的にモデルを構築する手法は「データセットを用意して学習実行ターンッ!」だけなのでめちゃくちゃ楽々です!

以上のようにルールベース手法だけ微妙な結果ではありますが、前述した通り、最適化することで高い精度を出すことができますし、HLACやPatchCoreでは難しい検出領域の詳細な寸法計測もできたりします!

餅は餅屋と言いますが、怪しき箇所はHLACやPatchCoreに任せて、詳細な解析はルールベースで行うというハイブリッド型のシステムとすることで、職人の目による外観検査に負けない異常検知システムを構築することだってできます。

  • ルールベースは構築に時間はかかるが、最適化次第では高い精度を出す可能性有り
  • HLAC、PatchCoreのような学習手法は、容易に高精度モデルを構築できる
  • ルールベースとのハイブリッド型で、良否判定と詳細解析の組み合わせが可能

人の感覚的な作業をシステム化するのは非常に大変ですが、弊社含め様々な機関で最適な手法が提案されて続けています!異常検知に興味がわいた方、ぜひ一緒に製造業課題を解決していきましょう!
アダコテックではメンバーを絶賛募集中です!
https://speakerdeck.com/adacotech/adacotech-company-profile
https://note.com/shin_ue626/n/n6b70248328da?from=notice
https://herp.careers/v1/adacotech

Discussion