製造業の画像検査 AI の使い方と作り方:撮像・手法選び・しきい値・運用まで
工場では、できあがった製品に傷・打痕・汚れ・欠け・異物がないか、部品が正しく付いているかを、人が目で見て確かめる 外観検査が今も多く行われています。人の目は柔軟ですが、疲れると見逃しが増え、検査員によって判定がぶれ、人手も足りません。そこで、カメラで撮った画像を AI に判定させる 画像検査 AI の導入が進んでいます。
この記事では、画像検査 AI を「何に使えるか」と「どう作るか」を、機械学習に初めて触れる人向けにまとめます。結論を先に書くと、次のとおりです。
- 位置も見た目もそろっている製品なら、従来のルールベースの画像処理で十分なことが多い。製品の置き位置・模様・形にばらつきがあり、ルールが書き切れないところで AI が効く
- 精度の大半は、AI より前の撮像(カメラ・レンズ・照明)で決まる。写っていない欠陥は、どんなモデルでも見つけられない
- 不良品の画像はほとんど集まらないので、良品だけで学ぶ 異常検知から始めることが多い。不良の種類まで分けたいときや、不良品の画像が十分にあるときは、分類・検出・セグメンテーションを使う
- 見逃し(不良品を良品と判定)と過検出(良品を不良品と判定)は、しきい値で片方を減らすと、もう片方が増える。見逃しの許容値から決め、判断に迷うものは人に回す
- 照明の劣化や材料の変更で、良品の見た目は少しずつ変わる。前処理で吸収できるものは吸収し、点数の分布を監視して学び直す
途中の計算例は、Python と NumPy で実際に動かした結果です。
環境
計算例を動かした環境です。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| NumPy | 2.5 |
パッチの距離の計算は画像 1 枚ごとに行列の掛け算をするので、全部を続けて動かすと数分かかります。
画像検査で何を自動化できるか
製造業の画像検査は、目的によって次のように分けられます。
| 検査 | 確かめること | 例 |
|---|---|---|
| 外観検査 | 傷・打痕・汚れ・欠け・異物・色むらがないか | 金属部品の表面、樹脂の成形品、塗装、食品 |
| 有無・組立検査 | 部品が正しい位置に、正しい向きで付いているか | 基板の電子部品、ねじ・コネクター・ラベル |
| 寸法・形状の計測 | 長さ・穴の位置・すき間が規格内か | 加工部品、シール材の塗布の幅 |
| 文字・コードの読み取り | 印字された文字やバーコードが正しく読めるか | 賞味期限・ロット番号の印字 |
| 数量・選別 | 数が合っているか、等級ごとに分ける | 青果の等級分け、異物の除去 |
寸法の計測や、きれいに印字された文字・コードの読み取りは、昔からある画像処理(エッジの検出・パターンマッチング・決まった OCR)が得意で、AI の出番はあまりありません。AI が力を発揮するのは、「何が不良か」を数式やルールで書き切れない検査、とくに外観検査です。
ルールベースと AI の使い分け
良品の手本との差で判定する
AI を使わない外観検査の代表は、良品の画像(ゴールデン画像)を手本にして、検査する画像との差が大きいところを不良とする方法です。位置がそろっていれば、とても強力です。
縦じまの加工模様がある、32 × 32 画素の製品の表面の画像で試します。不良品には、斜めの細い傷(明るい)か、小さな打痕(暗い)が付いています。良品 20 枚の平均をゴールデン画像にし、差の最大値を「不良らしさ」の点数にします。製品の置き位置のずれが 0 画素のときと、左右に最大 2 画素ずれるときを比べます。
import numpy as np
rng = np.random.default_rng(0)
N = 32
yy, xx = np.mgrid[0:N, 0:N]
def make(shift=0, defect=None):
"""製品の表面の画像。縦じまの加工模様に、撮影のノイズが乗る"""
img = 0.5 + 0.2 * np.sin(2 * np.pi * (xx + shift) / 8) + 0.03 * rng.standard_normal((N, N))
mask = np.zeros((N, N), bool)
if defect == "scratch": # 斜めの細い傷(明るい)
r, c = rng.integers(6, 22, 2)
for t in range(8):
mask[r + t, c + t] = True
img[mask] += 0.25
elif defect == "dent": # 小さな打痕(暗い)
r, c = rng.integers(6, 26, 2)
mask = (yy - r) ** 2 + (xx - c) ** 2 <= 2
img[mask] -= 0.25
return img, mask
def auroc(ok, ng):
"""良品の点数 ok と不良品の点数 ng から AUROC を計算する"""
s = np.r_[ok, ng]; r = s.argsort().argsort() + 1
return (r[len(ok):].sum() - len(ng) * (len(ng) + 1) / 2) / (len(ok) * len(ng))
DEFECTS = ["scratch", "dent"] * 25 # 不良品 50 枚(傷 25・打痕 25)
# 1. 良品の手本(ゴールデン画像)との差の最大値を点数にする
for jitter in [0, 2]:
sh = lambda: rng.integers(-jitter, jitter + 1) # 製品の置き位置のずれ(画素)
golden = np.mean([make(sh())[0] for _ in range(20)], axis=0)
ok = np.array([np.abs(make(sh())[0] - golden).max() for _ in range(200)])
ng = np.array([np.abs(make(sh(), d)[0] - golden).max() for d in DEFECTS])
print(jitter, round(auroc(ok, ng), 3), round((ok >= ng.min()).mean(), 3))
実行結果:
0 1.0 0.0
2 0.911 0.79
1 列目が位置のずれ、2 列目が AUROC(不良品の点数が良品の点数より高くなっている組の割合。1.0 なら完全に分けられる。評価指標のしくみ を参照)、3 列目が「不良品 50 枚をすべて不良と判定できるしきい値にしたとき、良品のうち何割を不良と判定してしまうか」です。
位置がそろっていれば、AUROC は 1.0 で、良品を 1 枚も誤らずに不良品をすべて見つけられます。ところが、置き位置が 2 画素ずれるだけで、縦じまの山と谷が手本とずれ、良品でも差が大きくなります。不良品をすべて見つけようとすると、良品の 79% を不良と判定してしまいます。
AI が効くのは「ばらつき」があるところ
実際の工場では、位置合わせ(パターンマッチングで位置を補正する処理)を入れることで、この程度のずれは吸収できます。ルールベースの検査が苦しくなるのは、次のように、良品どうしの見た目のばらつきが、不良との差と同じくらい大きいときです。
- 木目・鋳物の肌・織物のように、良品でも 1 つ 1 つ模様が違う
- 青果・食品・溶接のビードのように、形そのものがばらつく
- 「浅い傷は良品、深い傷は不良」のように、検査員が経験で判断している
- 不良の種類が多く、それぞれにルールを書いて調整し続けるのがたいへん
このような検査では、たくさんの画像から「良品のばらつきの範囲」を学べる AI が効きます。逆に、位置も見た目もそろっていて、ルールで判定できている検査を、わざわざ AI に置き換える必要はありません。ルールベースは判定の理由が説明しやすく、学習データもいらないからです。
撮像:精度の大半はカメラと照明で決まる
画像検査 AI の精度を上げようとして、モデルばかり工夫するのはよくある失敗です。画像に写っていない欠陥は、どんなモデルでも見つけられません。最初に、欠陥が「はっきり写る」撮像を作ります。
解像度:最小の欠陥が何画素で写るか
まず、見つけたい いちばん小さな欠陥が、画像の上で何画素になるかを計算します。目安として、最小の欠陥が少なくとも 3〜4 画素にまたがるようにします。1〜2 画素では、ノイズや画素の境目との区別がつきません。
たとえば、幅 60 mm の製品全体を 1 枚で写し、0.1 mm の傷を見つけたいとします。
- 幅 2,000 画素のカメラなら、1 画素は 60 ÷ 2,000 = 0.03 mm で、0.1 mm の傷は約 3.3 画素
- 幅 1,000 画素のカメラなら、1 画素は 0.06 mm で、傷は約 1.7 画素しかない
足りないときは、高解像度のカメラにするか、製品を何枚かに分けて写します。ただし、画素が増えるほど、1 枚の推論にかかる時間も増えます。製品 1 個を何秒で検査しなければならないか(タクトタイム)と合わせて決めます。
照明:欠陥を目立たせる
照明は、欠陥と良品の部分の明るさの差(コントラスト)を作る、いちばん大事な部品です。欠陥の種類によって、向いている当て方が違います。
| 照明 | 当て方 | 見やすくなるもの |
|---|---|---|
| 同軸照明 | カメラと同じ向きから光を当てる | 鏡のような金属面の、へこみ・傷・汚れ |
| ローアングル照明 | 横から浅い角度で光を当てる | 表面の小さな凹凸・打痕・浅い傷(影ができる) |
| ドーム照明 | あらゆる方向から光を回り込ませる | 曲面や光沢面の、色や汚れ(てかりを消す) |
| バックライト(透過照明) | 製品の裏から光を当てる | 輪郭・欠け・穴・透明な材料の中の異物 |
照明の色(波長)を変えると、特定の色の汚れが目立つこともあります。どの照明がよいかは、実際の不良品を並べて、照明を替えながら撮り比べて決めるのが確実です。
また、検査の画像は いつも同じ条件で撮れることが大切です。周りの窓からの外光は遮り、製品の置き位置はガイドで固定し、カメラの露出やゲインの自動調整は切っておきます。条件がぶれると、その分だけ良品の見た目のばらつきが増え、AI が見分けにくくなります。
手法の選び方
画像検査に使う AI は、どんな答えがほしいか、不良品の画像がどれだけあるかで選びます。
| 手法 | 出す答え | 学習に必要なデータ | 向いている場面 |
|---|---|---|---|
| 画像分類 | 画像 1 枚ごとの「良品・傷・打痕」 | 種類ごとのラベルを付けた画像 | 不良の種類で後工程を変えたい |
| 物体検出 | 欠陥の位置を四角で囲み、種類を付ける | 欠陥を四角で囲んだ画像 | 欠陥の数や位置を記録したい・部品の有無を見たい |
| セグメンテーション | 欠陥の形を画素単位で塗る | 欠陥を画素単位で塗った画像 | 欠陥の面積や長さで合否を決めたい |
| 異常検知 | 「いつもと違う」度合いと、その場所 | 良品の画像だけ | 不良品がめったに出ない・見たことのない不良にも気付きたい |
製造の現場では、不良品はめったに出ませんし、ラインが安定するほどさらに減ります。不良の種類ごとに何百枚も集めるのは、ふつうは無理です。また、過去に出たことのない種類の不良にも気付きたいところです。そのため、良品の画像だけで学べる異常検知から始めることがよくあります。不良品の画像が集まってきたら、不良の種類を分けたり、欠陥の大きさを測ったりするために、分類・検出・セグメンテーションを組み合わせます。
良品のパッチを覚えて、似ていないところを探す
外観検査の異常検知でよく使われるのが、良品の画像の小さな切れ端(パッチ)をたくさん覚えておき、検査する画像のパッチごとに「いちばん似ている良品のパッチまでの距離」を測る方法です。どの良品のパッチとも似ていない場所が、欠陥の候補になります。2022 年の PatchCore は、この考え方で、学習済みの画像認識モデルが取り出した特徴をパッチにして使います。
ここでは、特徴のかわりに、5 × 5 画素の画素の値をそのままパッチとして使います。製品の置き位置は、さっきゴールデン画像がうまくいかなかった、最大 2 画素のずれです。上のコードの続きです。
# 2. 良品の小さな切れ端(パッチ)を覚えておき、どれにも似ていないパッチを探す
P = 5
def patches(img):
return np.lib.stride_tricks.sliding_window_view(img, (P, P)).reshape(-1, P * P)
sh = lambda: rng.integers(-2, 3)
bank = np.vstack([patches(make(sh())[0]) for _ in range(20)]) # 良品 20 枚のパッチ
def score_map(img, bank=bank):
"""パッチごとに、良品のパッチの中でいちばん近いものまでの距離を返す"""
q = patches(img)
d2 = (q ** 2).sum(1)[:, None] - 2 * q @ bank.T + (bank ** 2).sum(1)[None]
return np.sqrt(np.maximum(d2.min(1), 0)).reshape(N - P + 1, N - P + 1)
ok = np.array([score_map(make(sh())[0]).max() for _ in range(200)])
ng, hit = [], 0
for d in DEFECTS:
img, mask = make(sh(), d)
m = score_map(img)
ng.append(m.max())
r, c = np.unravel_index(m.argmax(), m.shape)
hit += mask[r:r + P, c:c + P].any() # 点数がいちばん高いパッチが不良の場所に重なったか
ng = np.array(ng)
print(bank.shape, round(auroc(ok, ng), 3), round((ok >= ng.min()).mean(), 3), int(hit))
実行結果:
(15680, 25) 1.0 0.0 50
良品 20 枚から、25 画素のパッチを 15,680 個覚えました。画像の点数は、パッチの距離のいちばん大きな値です。同じ 2 画素のずれでも、AUROC は 1.0 で、不良品をすべて見つけるしきい値にしても、良品を 1 枚も誤りません。さらに、不良品 50 枚すべてで、点数がいちばん高いパッチが実際の傷や打痕の場所に重なりました。
うまくいくのは、覚えたパッチが 画像のあらゆる場所から取ったものだからです。縦じまが 2 画素ずれても、「2 画素ずれた縦じまのパッチ」は、ほかの場所や、ほかの良品の画像のどこかに必ずあります。良品のばらつきを、手本 1 枚ではなく、たくさんの例で覚えておくのが、ゴールデン画像との違いです。パッチごとの距離をそのまま画像に並べると、欠陥の場所が明るく光る 異常マップになり、検査員が「どこが悪いと判定されたか」を確かめるのにも使えます。
本物の製品の画像では、画素の値をそのまま比べると、照明のわずかなむらや製品の個体差にも反応してしまいます。PatchCore のように、学習済みの画像認識モデルの特徴を使うと、「模様として似ているか」で比べられるので、この影響を受けにくくなります。
しきい値:見逃しと過検出
見逃しと過検出は釣り合いで決まる
異常検知のモデルが出すのは点数で、どこから不良とするかの しきい値は人が決めます。検査で起きる間違いは 2 種類です。
| 間違い | 意味 | 困ること |
|---|---|---|
| 見逃し | 不良品を良品と判定する | 不良品がお客さまに届き、クレームやリコールになる |
| 過検出(過剰検出) | 良品を不良品と判定する | 良品を捨てる・人が見直す手間が増える |
ふつう、見逃しのほうがずっと困るので、まず見逃しの許容値を決め、その中で過検出をできるだけ減らす順に考えます。ここでは、検証用の良品 200 枚の点数のいちばん大きな値をしきい値にして、別の良品 200 枚と不良品 50 枚で確かめます。
# 3. 検証用の良品でしきい値を決め、別の画像で確かめる
th = max(score_map(make(sh())[0]).max() for _ in range(200))
test_ok = np.array([score_map(make(sh())[0]).max() for _ in range(200)])
test_ng = np.array([score_map(make(sh(), d)[0]).max() for d in DEFECTS])
print(round(th, 3), round((test_ok > th).mean(), 3), round((test_ng <= th).mean(), 3))
実行結果:
0.205 0.0 0.0
しきい値は 0.205 で、新しい画像でも、過検出率(2 列目)も見逃し率(3 列目)も 0 でした。この例は欠陥がはっきりしているので、しきい値の選び方に余裕があります。実際の検査では、浅い傷と良品の点数が重なり、しきい値を下げて見逃しを減らすほど、過検出が増えます。その場合は、点数を 2 つのしきい値で 3 つに分け、はっきり良品・はっきり不良・判断に迷うもののうち、迷うものだけを人が見る運用にすると、人の手間を減らしながら見逃しを抑えられます。
見逃しが 0 件でも、見逃し率が 0 とは言えない
検証で不良品 50 枚をすべて見つけられても、「見逃し率 0%」とは言えません。50 枚で 1 件も見逃さないことは、本当の見逃し率が数 % あっても、偶然起こりうるからです。
本当の見逃し率が p のとき、n 枚すべてを見つける確率は (1 − p)ⁿ です。これが 5% 以下になる p を求めると、「n 枚で見逃し 0 件なら、本当の見逃し率は 95% の確からしさで 1 − 0.05^(1/n) 以下」と言えます。この値はおよそ 3 / n になるので、3 の法則(rule of three)と呼ばれます。
for n in [50, 100, 300, 1000]:
print(n, round(1 - 0.05 ** (1 / n), 4), round(3 / n, 4))
実行結果:
50 0.0582 0.06
100 0.0295 0.03
300 0.0099 0.01
1000 0.003 0.003
不良品 50 枚で見逃し 0 件なら、言えるのは「見逃し率は約 6% 以下」までです。「見逃し率 1% 以下」と言うには、見逃し 0 件で約 300 枚の不良品が必要です。不良品の画像が少ないうちは、AI だけで出荷を決めず、人の検査と並べて動かしながら、不良品を集めて確かめていくのが安全です。
作り方の流れ
ここまでの内容を、画像検査 AI を作って導入する流れに並べます。
1. 何を不良とするかを決める
- 不良の種類と、それぞれの 合否の基準(長さ何 mm 以上の傷は不良、など)を書き出す。検査員の間で判断が分かれるものは、良品と不良品の境目の見本(限度見本)を作って合わせる
- 見逃しと過検出の許容値、タクトタイム、判定のあと不良品をどう取り除くかを決める
- 人の検査員の今の見逃し率・過検出率を測っておく。AI の目標を決めるときの比べる相手になる
人どうしで判定が分かれる画像には、AI も正解を学べません。基準をそろえる作業が、ラベル付けの品質を決めます。
2. 撮像系を作る
- 最小の欠陥が 3〜4 画素以上になる解像度とレンズを選ぶ
- 実物の不良品で照明を撮り比べ、欠陥がいちばん目立つ当て方を選ぶ
- 外光を遮り、置き位置・露出・ゲインを固定する
3. 画像を集めてラベルを付ける
- 良品は、ばらつきをすべて含むように集める。材料のロット・設備・季節・昼夜・作業者が違う日の画像を混ぜる。数日分だけで学ぶと、それ以外の日の良品を不良と判定しやすい
- 不良品は、出るたびに画像とラベルを残す仕組みを、早いうちに作る
- 分類・検出・セグメンテーションを使うなら、欠陥の種類・位置・形のラベルを付ける。迷ったラベルには印を付けて、あとで基準を見直す
4. 学習して評価する
- 学習用・検証用・評価用に分けるときは、製造日やロットごとに分ける。同じ日の画像を学習と評価の両方に入れると、よく似た画像で答え合わせをすることになり、精度が高く出すぎる(過学習と正則化 を参照)
- 正解率ではなく、見逃し率と過検出率で評価する。不良品はめったにないので、すべて良品と答えるだけで正解率は 99% を超えてしまう
- 異常マップを検査員に見せて、AI が欠陥そのものに反応しているか、背景の汚れやジグの映り込みに反応していないかを確かめる(機械学習の判断を説明するしくみ も参考になる)
5. 現場に入れる
- 工場の中では、ネットワークの遅れや切断を避けるため、ライン横の PC や エッジ端末で推論することが多い。タクトタイムに収まるか、実機で測る
- 最初は人の検査と 並行稼働させ、AI と人の判定が食い違った画像を集めて見直す。ここで見つかった見逃しや過検出が、いちばん役に立つ学習データになる
- 判定結果・点数・画像を、製品のシリアル番号やロットと紐づけて残す。不良が後で見つかったとき、どの判定が誤っていたかをたどれるようにする
6. 運用しながら学び直す
導入したあとも、良品の見た目は少しずつ変わります。照明の劣化、カメラの汚れ、材料の仕入れ先の変更、金型の摩耗などです(ドリフト)。照明が少し暗くなったとき、さっきのモデルがどうなるかを試します。
# 4. 照明が暗くなり、画像全体の明るさが 85% になると
dark_ok = np.array([score_map(make(sh())[0] * 0.85).max() for _ in range(200)])
dark_ng = np.array([score_map(make(sh(), d)[0] * 0.85).max() for d in DEFECTS])
print(round((dark_ok > th).mean(), 3), round((dark_ng <= th).mean(), 3))
実行結果:
1.0 0.0
不良品はすべて見つけていますが、良品の 100% を不良と判定しました。暗くなった良品のパッチは、覚えた明るさのパッチのどれとも似ていないからです。ラインは止まり、検査員は警報を無視するようになってしまいます。
明るさの変化のように、あらかじめ分かっている変化は、前処理で吸収できます。画像ごとに平均と標準偏差をそろえてから、パッチを覚えて比べます。
# 5. 画像ごとに明るさをそろえて(平均 0・標準偏差 1 にして)から比べる
norm = lambda img: (img - img.mean()) / img.std()
bank_n = np.vstack([patches(norm(make(sh())[0])) for _ in range(20)])
score = lambda img: score_map(norm(img), bank_n).max()
th_n = max(score(make(sh())[0]) for _ in range(200))
for k in [1.0, 0.85]:
ok = np.array([score(make(sh())[0] * k) for _ in range(200)])
ng = np.array([score(make(sh(), d)[0] * k) for d in DEFECTS])
print(k, round((ok > th_n).mean(), 3), round((ng <= th_n).mean(), 3))
実行結果:
1.0 0.005 0.0
0.85 0.005 0.0
1 列目が明るさです。明るさをそろえると、暗くなっても過検出は 0.5%(200 枚のうち 1 枚)のままで、見逃しも 0 でした。この 1 枚は、検証用の 200 枚の最大値をしきい値にしたため、それより少し高い点数の良品が新しく出たものです。200 枚から決めたしきい値では、良品の 0.5% 程度が超えることは起こりえます。
ただし、前処理で吸収できるのは、あらかじめ予想できた変化だけです。運用では次を続けます。
- 良品の点数の分布(平均や上位 1% の値)を日ごとに記録し、じわじわ上がってきたら、照明やカメラを点検する
- 照明やカメラを交換したとき、材料や設備が変わったときは、新しい良品の画像を加えて学び直す
- 学び直したモデルは、過去の不良品の画像をすべて見つけられるかを確かめてから入れ替える
画像検査 AI の流れ
この記事に出てきた手法とデータセットを年代順に並べます。
| 年 | 手法・出来事 | ポイント |
|---|---|---|
| 1983 | 3 の法則(医学の論文で紹介) | 0 件の観測から、起こる確率の上限を見積もる |
| 2012 | AlexNet | 深層学習の画像分類が実用の精度に届き、外観検査への応用が広がる |
| 2019 | MVTec AD | 15 種類の製品・素材の外観検査の異常検知のデータセット |
| 2020 | PaDiM | 学習済みモデルの特徴の分布を、パッチの位置ごとに学ぶ |
| 2022 | PatchCore | 良品のパッチの特徴を覚え、近傍距離で欠陥の場所を見つける |
| 2022 | VisA | 基板・カプセルなど、複雑な形の製品を含む異常検知のデータセット |
まとめ
- 位置も見た目もそろっていればルールベースで十分。良品どうしのばらつきが大きく、ルールで書き切れない外観検査で AI が効く
- 精度の大半は撮像で決まる。最小の欠陥が 3〜4 画素以上になる解像度にし、欠陥が目立つ照明を撮り比べて選び、条件を固定する
- 不良品はめったに出ないので、良品だけで学ぶ異常検知から始めることが多い。良品のパッチを覚えて近傍距離を測る方法は、位置のずれや模様のばらつきに強く、欠陥の場所も示せる
- 見逃しと過検出はしきい値の釣り合いで決まる。見逃しの許容値から決め、迷うものは人に回す。見逃し 0 件でも、言えるのは「約 3 / n 以下」まで
- 学習と評価はロットや製造日で分け、見逃し率と過検出率で評価する。導入は人との並行稼働から始める
- 照明の劣化や材料の変更で良品の見た目は変わる。前処理で吸収できる変化は吸収し、点数の分布を監視して学び直す
参考
- J. A. Hanley and A. Lippman-Hand, "If Nothing Goes Wrong, Is Everything All Right? Interpreting Zero Numerators", JAMA 1983
- A. Krizhevsky, I. Sutskever and G. E. Hinton, "ImageNet Classification with Deep Convolutional Neural Networks", NeurIPS 2012
- P. Bergmann et al., "MVTec AD — A Comprehensive Real-World Dataset for Unsupervised Anomaly Detection", CVPR 2019
- T. Defard et al., "PaDiM: a Patch Distribution Modeling Framework for Anomaly Detection and Localization", ICPR 2020 Workshops(arXiv:2011.08785)
- K. Roth et al., "Towards Total Recall in Industrial Anomaly Detection", CVPR 2022(arXiv:2106.08265)
- Y. Zou et al., "SPot-the-Difference Self-supervised Pre-training for Anomaly Detection and Segmentation", ECCV 2022(arXiv:2207.14315)