【異常検知】食品画像を攻略してみる

今回は、**実例があまり出てこない**食品画像を攻略してみたいと思います。（といっても、精度100％は出ていません(^^；）

![ダウンロード (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/9ebc7a1e-745d-9973-7444-600d5bf5ebc3.png)
（右端「Padim + YOLOv5」が提案手法）

# 食品画像の難しさ
食品画像の検査は、工業製品と違って難しいと言われています。個人的には

+ 見た目に多様性がある
+ 位置が決まっていない

が理由だと思っております。

見た目の多様性は、例えばミカンでいうと、色やサイズ、形が様々で、一つとして同じものはありません。つまり、正常品の範囲が広く、正常/異常の境界線があいまいになりがちです。一方、工業製品は多様性が少なく、正常品の範囲が狭いです。このため、正常/異常の境界線がはっきりしています。

二点目、「位置が決まっていない」は、食品という特性上、位置が多少ズレることがあります。さらに、2つの食品があったとして、2つの位置が入れ替わったとしても正常となることがあります。つまり、位置ベースの異常検知手法が使えなくなります([Padim](https://github.com/xiahaifeng1995/PaDiM-Anomaly-Detection-Localization-master)とか)。

# データセット
今回は新しいデータセットである[LOCO](https://www.mvtec.com/company/research/datasets/mvtec-loco)を使います。

LOCOの詳細は[こちらの記事](https://qiita.com/bezilla/items/ea274d081284d83d7843)に書かれている通り、「構造的な異常」と「論
理的な異常」が含まれるデータセットです。そして、ターゲットとなるのは"breakfast_box"です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/77123e4f-e256-08d8-7238-f96e86b021cf.png)

# ベースライン

本稿で使う異常検知手法は、特に明記がない限り**全て**[マハラノビスAD法](https://qiita.com/shinmura0/items/5f2c363812f7cdcc8771)を使います。
まずは、マハラノビスAD法でポン！をしてみます。
実験条件と結果は以下。

||backbone|level|解像度|ROAUC↑|正常品誤判定率@recall1.0↓|
|---|---|---|---|---|---|
|baseline|tf_efficientnetv2_xl_in21k|8|700x700|0.891|0.931|

ROAUCは異常検知の論文で頻出するAUCです。このスコアが大きいほど強い手法といえます。

### 正常品誤判定率@recall1.0
一方、正常品誤判定率@recall1.0は製造業でよくあるケースを想定しています。日本の製造業では（多分、食品業界でも）、**外観検査した後に異常品を含んでいることは許されず**、閾値は厳しめに設定されています。このとき、正常品を誤判定（過検知）するわけですが、この誤判定率は低くなければ使い物になりません。

そして、この誤判定率こそ、正常品誤判定率@recall1.0であり、定義としては以下です。

「異常品を100％弾く（recall1.0）くらいの厳しい閾値にしたときの、正常品の誤判定率」

もちろん、このスコアは小さいほど良いものです。論文ではROAUCが重宝されますが、個人的に、日本の製造業では、正常品誤判定率@recall1.0が重要な数値と考えています。上記の表の誤判定率0.931という数値は、正常品が100個あったとしたら、93個が誤判定されるという意味です。

# データをよく見る

ベースラインではディープでポン！を行ってみました。
ROAUCが0.95に到達していないので、あまり良い異常検知器とは言えません。

そこで、Kaggleで教わった流儀で、データをよく見てみます。
Kaggleに限らず、異常検知においてもデータをよく見るのは大事です。
正常品は以下。
![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/379c876c-283e-6673-061c-a05c7a6be341.png)

正常品の特徴として
+ それぞれの食品の外観が多様
+ ミカンとスモモの位置が入れ替わることがある

が挙げられます。続いて、異常品。
![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/163f1169-816d-cc2f-925b-2db34210d9fe.png)

異常品の特徴として
+ 小さい異常（傷など）もあれば、大きい異常（シリアルが入っていないなど）もある
+ シリアルの量が多すぎるなど、量的な異常がある

があります。一点目（異常の大小）について、一般論では、大きい異常は全体画像で検査すれば容易に検出できますが、小さい異常は全体画像だけでは検出できず、拡大する必要があります。そこで、各食品を切り出して、画像を拡大する方法があります。これにより、小さい異常も必然的に大きくなり、目立つようになります。

二点目（量的な異常）は全体画像の検査（ディープでポン！）でも検出できそうです。ただ、ここでも各食品を拡大することにより、量が敏感に分かるような画像となるため、前述した各食品の画像拡大が有効そうです。

# 解法
本稿の解法は以下です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/2c8fe461-9d8f-7982-9301-cf1e5af8cbcf.png)

+ 全体画像で異常スコアAを出す
+ 各食品をYOLOv5で切り出し、各食品の異常スコアBを出す
+ 異常スコアAとBをマージ（加算）し、総合スコアを出す

### 異常スコアのマージ方法
この解法はシンプルですが、異常スコアのマージ（加算）方法が結構やっかいです。ここで、計算例を書いてみます。例えば、正常品が「ミカン2個の画像」と定義します。そして、正常品の異常スコアAとBが共に1だったとして、普通に足すと

+ 正常品（ミカン2個 ）の総合スコア
 = 異常スコアA+異常スコアB1+異常スコアB2
 = **1**+1+1 = 3
+ 異常品（ミカン1個 ）の総合スコア
 = 異常スコアA+異常スコアB1
 = **2**+1 = 3

で、総合スコアが同じになってしまいました。このからくりは、異常品において、本来加算するはずだったミカンの異常スコアB2（=1）がないことです。結果的に、異常品では、ミカンが欠落しているため、全体画像のスコア（異常スコアA）が2と大きくなっています（good!）が、ミカンの個数が少ないため、総合スコアが同じになってしまったパターンです。このように、異常スコアを普通に加算すると、**欠品している画像の総合スコアが低くなりがち**になります。

そこで、本稿では、マージ方法で良く使われる標準化を使います。標準化は、平均値で引いて標準偏差で割る（(score - average) / std）操作です。上記の例で、単純化のために異常スコアAとBの平均値が1、標準偏差が1だったとして、各スコアを標準化し加算します。

+ 正常品（ミカン2個 ）の総合スコア
 = 異常スコアA+異常スコアB1+異常スコアB2
 = **(1-1)/1**+(1-1)/1+(1-1)/1 = 0
+ 異常品（ミカン1個 ）の総合スコア
 = 異常スコアA+異常スコアB1
 = **(2-1)/1**+(1-1)/1 = 1

計算式がやや煩雑ですが、結果的に**異常品の総合スコアが大きくなりました**（good!）。この操作は、雑な言い方をすると「正常品の異常スコアをいくら足しても総合的には0になり、異常品の異常スコアは顕著に大きな数値となる」です。

先にネタバレですが、"breakfast box"で異常スコアを単純に加算すると、ディープでポン！よりもスコアが悪化します。一方、この標準化を使うことで、ディープでポン！よりもスコアが改善します。

### 可視化
可視化の説明は分かりにくいです。そのため、読み飛ばしていただいた結構です。


異常部分の可視化には、[Padim](https://github.com/xiahaifeng1995/PaDiM-Anomaly-Detection-Localization-master)を使用しました。PadimはマハラノビスAD法とほぼ同じ手法ですが、空間情報を残しているため、ピクセル単位の異常検知が可能となります。直感的な説明は以下のとおりです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/b7ed04e6-51f7-0750-297b-0c0932790f2b.png)

冒頭で述べた通り、Padimは位置ベースの異常検知手法です。そのため、ミカンとスモモが入れ替わる"breakfast_box"は大の苦手です。そこで、ここでも、異常マップをマージして改善を図ります。比較対象（ベースライン）として、全体画像に適用したPadimを後ほど掲載します。

異常マップのマージ方法は、以下のとおりです。
+ 全体画像にPadimを適用し、異常マップ$M$（ベースラインと等しい）を取得します。
+ YOLOv5で各物体を切り出し、切り出した画像にもPadimを適用して異常マップ$m$を取得します。
  + ただし、切り出す座標$p_{left},p_{right},p_{top},p_{bottom} $を取得しておきます。
+ さらに、切り出した画像にマハラノビスAD法を適用し、**前述した標準化を適用した**異常スコアB($s_b$)を取得しておきます。
+ 最後に、$M$の$p_{left},p_{right},p_{top},p_{bottom} $に$m$と$s_b$を加算します。

疑似コードで書くと、こんな感じ。
```python
# M :画像全体の異常マップ（ベースラインに等しい）,shape(700,700)
# m :切り出した画像の異常マップ,例えば shape(100,50)
# p_left, p_right, p_top, p_bottom, s_bはスカラー

M[p_top:p_bottom, p_left:p_right] = M[p_top:p_bottom, p_left:p_right] + m + s_b
```

最後に異常スコアB($s_b$)を足すのが重要なところで、これが無いとあまり改善しません。この意図は、たとえ異常部位を的確に抽出できなくても、異常部位を含んでいる物体（bounding box）の異常度が高くなり、視認性が改善されるところにあります。前述した通り、$s_b$は正常品では、ほとんど0に張り付いています。ところが、異常品の$s_b$は顕著に大きな値なので、異常部位を含むbounding boxの異常度が高くなる仕組みです。

# 結果
### YOLOv5
物体検出はYOLOv5を使いました。train_dataでアノテーションを行い、テストデータに適用したところ、recallは100％近い印象です。以下の画像はテストデータの画像です。

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/d3ba344d-38e5-9cfa-041f-636c8c5ab765.png)
![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/12d90de5-fc31-50a4-6f67-f13cedeb7970.png)

### スコア
マージで使う平均値と標準偏差は、validationデータを用いて算出しました。validationデータは、正常品であるものの、学習データとして使っていないデータを指します（LOCOでは、validationデータが別に用意されています）。実験条件と結果は以下。

||Backbone|Level|解像度|ROAUC↑|正常品誤判定率@recall1.0↓|
|---|---|---|---|---|---|
|ミカン|tf_efficientnet_b5|6|400x400|||
|スモモ|〃|7|500x500|||
|シリアル|〃|5|300x300|||
|バナナチップ|〃|6|600x600|||
|Baseline|tf_efficientnetv2_xl_in21k|8|700x700|0.891|0.931|
|Score merge||||**0.924**|**0.833**|

異常スコアB（ミカン、スモモ・・・）は各Backboneによって出しました。また、異常スコアAはBaselineの異常スコアを使用しました。

結果的に、score mergeによって、**ベースラインの誤判定率を10％改善しました**。各食品をYOLOで切り出し、異常部位を拡大する狙いは成功したようです。最終的な誤判定率が0.833なので、正常品の10個に8個は誤判定されます。しかし、2/10個は正確な判定ができるすので、工場の状況によっては、これを導入するところもあるのではないでしょうか。

### 可視化
以下は、**全て異常品**の可視化結果です。

![ダウンロード (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/1c5d67a6-2357-e097-2cbb-c14b4e73c2ff.png)
![ダウンロード (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/b0eeb58c-af08-0050-cde9-5af797f781e1.png)
![ダウンロード (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/2bf53682-01cc-5aaf-8d58-dccce83bcdb9.png)
![ダウンロード (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/bac42d2d-ff58-02e8-3b27-e31d83243658.png)
![ダウンロード (10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/242cc6dc-9fe3-f90d-6c0b-aadc0e7cbebf.png)
![ダウンロード (11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/a711fe03-3220-77bf-f9ad-61fa3f99aff3.png)
![ダウンロード (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/9ebc7a1e-745d-9973-7444-600d5bf5ebc3.png)
![ダウンロード (16).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/8f4700a8-e012-6544-9fb8-ca427cf2d747.png)
![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/ada3ed74-894d-f26d-20d5-d6ce07080540.png)
![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/84cfcb9c-7002-cb21-9b27-5f497d7efd8c.png)
![ダウンロード (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/9989a8d7-8352-c03c-9575-d8e368e2b79f.png)
![ダウンロード (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/91243940-3902-175a-3ab3-b84d15d943c3.png)
![ダウンロード (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/888a08fa-3077-1d8f-a468-e6c88a4c8799.png)

### 災い転じて・・・
ここまで見てきて、「YOLOv5」や「可視化」の結果に疑問を持った方がいるかもしれません。なぜなら、以下の可視化は矛盾があるからです。
![ダウンロード (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/1c5d67a6-2357-e097-2cbb-c14b4e73c2ff.png)
![ダウンロード (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/b0eeb58c-af08-0050-cde9-5af797f781e1.png)
![ダウンロード (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/2bf53682-01cc-5aaf-8d58-dccce83bcdb9.png)

これらの画像はYOLOv5を使うことで、視認性が改善されています。つまり、YOLOv5で異物を物体（ミカン、スモモ、シリアル、バナナチップ）として誤検出してことを示しています。そして、YOLOv5の検出は以下。

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/5a4e6639-6a64-eef4-48ea-14d0c054a65a.png)

確かに、ピンポン玉とミカンの皮はミカンとして、鍵はバナナチップとして検出されています。これにより、この画像は、ロジック的に画像単位の異常度も上がりますし、異常マップにおける異常部位の異常度も上がり、精度が改善されます。普段は嬉しくない、YOLOv5の誤検出ですが、今回は「災い転じて福となす」の効果があったようです。

# まとめ
今回は、実例があまり出てこない食品画像で異常検知を行ってみました。
分かったことは以下です。
+ 食品画像はやっぱり難しい
  + 特に、多様性を吸収するのが難しい
+ ただ、工夫すれば精度を改善でき、実用に耐えうる精度を出せる気がする
  + 本稿で使った技術は食品だけではなく、工業製品でも精度改善に寄与できる
+ 今回はマハラノビスAD法を使ったが、PatchCoreやfastflowに変えることでスコア改善が期待できる

実は、まだまだ改善のアイデアがありますが、冗長になるので、ここまでとさせていただきます。