Real-World On-Vehicle Evaluation of Embedding-Based Anomaly Detection を、机の上の小物検出という形で試した記事です。

参考にしたもの

  • Real-World On-Vehicle Evaluation of Embedding-Based Anomaly Detection DINOv3の画像埋め込みを使い、1枚の正常参照画像と入力画像を比較して異常マップを作る論文です。論文では、実車のROS2ノードとしても評価しています。

  • DINOv3 / Meta AI 今回の論文で使われている画像基盤モデルです。画像を小さな領域ごとの特徴ベクトルに変換できるため、分類ではなく「どの領域が見慣れないか」を見る用途に使えます。

  • facebookresearch/dinov3
    DINOv3の公式実装です。

  • 今回のコード: desk-anomaly-dino 自分で試したJupyter Notebook実装です。reference_normal に正常状態の机画像、observed に検査画像を置き、DINOv3で異常ヒートマップ、マスク、bbox、summary.csvを出力します。


まずは試した結果

論文で提案されている「正常な画像と今の画像を比べて、いつもと違う場所を見つける」という考え方を、机の上で試してみました。

1枚目に、何も追加していない正常な机の画像を用意します。 2枚目に、机の上に小物を追加した画像を用意します。 その2枚をDINOv3に通して、どこが「いつもの机と違う」のかを見ます。

今回の入力画像は次の2枚です。

正常状態の机画像

これは reference_normal に置いた正常状態の机画像です。 この画像を「いつもの状態」として扱います。

小物を追加した机画像

正常画像にはなかった小物が机の上に追加されています。

この2枚を比べると、DINOv3の異常ヒートマップは次のようになりました。

手元実装の異常ヒートマップ

明るく出ている場所ほど、DINOv3の特徴として「正常画像に似た場所が見つかりにくい」場所です。

元画像に重ねると、こうなります。

手元実装のOverlay画像

小物の周辺が反応していることが分かります。

さらに、異常候補として採用された領域にbboxを付けると、次のようになります。

手元実装のbbox付き異常候補

今回は、3つの候補が取れました。

手元実装の二値マスク

白くなっている場所が、しきい値を超えて「異常候補」として採用された領域です。

出力された候補は次のような値でした。

候補1: center=(279.1, 92.3), area=503, max_score=0.2139
候補2: center=(304.9, 149.3), area=786, max_score=0.2136
候補3: center=(364.6, 114.3), area=689, max_score=0.1735

この max_score=0.2139 は、「21.39%の確率で異常」という意味ではないです。

この値は、DINOv3が出した画像特徴を使って、正常画像とどれくらい似ていないかを数値化したものです。 この記事ではこれを分かりやすく 違和感スコア と呼ぼうと思います。


画像の処理方法

今回の方法では、画像を1枚まるごと比べていません。小さい領域に分け、処理を行います。

DINOv3のようなVision Transformer系のモデルは、画像を小さなタイルのような領域に分けて処理します。 この小さなタイルのような領域を patch と呼んでいます。

たとえば今回の実装では、画像サイズを448×448にそろえ、DINOv3のpatch sizeは16です。

つまり、画像はだいたい次のように分けられます。

448 / 16 = 28

28 × 28 = 784個のpatch

1枚の画像が、784個の小さな領域に分けられ、DINOv3がそれぞれのpatchについて特徴ベクトルを作ります。 画像の小さな領域ごとに「見た目や意味の情報を含んだ数値のまとまり」を作っているイメージです。


違和感スコアとはどういう意味で使ったか

ここで、ようやく「違和感スコア」の意味を説明できます。

まず、正常画像をDINOv3に渡します。 すると、正常画像の784個のpatchそれぞれに特徴ベクトルができます。

これは、正常な机に含まれる見た目の部品表のようなものです。

正常画像の机の木目
正常画像の影
正常画像のキーボード
正常画像の背景
正常画像の机の端

次に、検査画像もDINOv3に通します。 こちらも784個のpatch特徴になります。

そして、検査画像の各patchについて、こう調べます。

このpatchに似た場所は、正常画像の中にあるか?

似ていれば、「これはいつもの机にありそう」と考えます。 似ていなければ、「これはいつもの机ではなさそう」と考えます。

この「いつもの机ではなさそう」の強さを数値にしたものが、ここで言う違和感スコアです。

論文の表現に寄せると、入力画像のpatch特徴を t_i、正常参照画像のpatch特徴を r_j として、

s_i = max_j cos(t_i, r_j)

を計算します。

これは、検査画像のある領域 (t_i) が、正常画像の中で一番似ている領域とどれくらい似ているかを表します。

そして、この似ている度合いを反転させて、

a_i = (1 - s_i) / 2

のように異常スコアにします。

つまり、似ていればスコアは小さくなり、似ていなければスコアは大きくなります。 ざっくり言えば コサイン類似度 で見ています。

コサイン類似度は、2つの特徴ベクトルがどれくらい同じ方向を向いているかを見る値です。 画像そのもののピクセル差ではなく、DINOv3が作った「その場所らしさ」の特徴同士を比べます。

この記事では、この a_i を分かりやすく 違和感スコア と呼びます。


元論文では何をしているのか

今回参考にした論文は、Real-World On-Vehicle Evaluation of Embedding-Based Anomaly Detection です。

タイトルの通り、自動運転の車載カメラで異常検出を評価しています。

自動運転では、普通の物体検出なら「車」「人」「信号」「自転車」のようなクラスを見つけます。 しかし、現実の道路では、あらかじめ決めたクラスに収まらないものが出てきます。

道路に落ちたタイヤ。 玩具のような小型物体。 膨らませた物体。 想定外の障害物。 人が持っている普通ではない物体。

このような異常物体は、種類が多く、事前に全部集めて学習するのが難しいです。

そこでこの論文は、異常物体を「分類」せず、正常な参照画像と比較して意味的に見慣れない場所を探します。

論文のFigure 1では、処理の流れが次のように示されています。

Reference image
Input image
↓
Pre-trained DINO encoder
↓
Reference embeddings / Input embeddings
↓
Feature comparison
↓
Anomaly map

つまり、正常画像と入力画像を同じDINO encoderに通し、それぞれのpatch特徴を比較して、異常マップを作るという流れです。

論文の実車評価では、入力画像、PCA可視化、異常マップ、二値異常マップを並べて表示しています。 以下は論文中の可視化例です。

論文中の入力画像例

これは車載カメラの入力画像です。

論文中の異常マップ例

異常マップでは、通常の道路風景から外れている対象の周辺が反応しています。

論文中の二値マスク例

二値マスクでは、しきい値を超えた領域が白く出ています。

この論文側の流れと、今回の机上で試した実装はこのように対応しています。

論文:
車載カメラ画像
↓
DINOv3特徴
↓
異常マップ
↓
二値マスク

今回:
机の画像
↓
DINOv3特徴
↓
異常ヒートマップ
↓
二値マスク
↓
bbox付き候補

今回の実装では、最後にbboxと中心座標も出しています。 ここは論文そのものではなく、手元実験用に追加しています。


論文通りにできている部分と、手元実装で足した部分

今回のNotebookの中心処理は、論文の方法と基本的に同じです。 対応している部分は次です。

DINOv3を固定特徴抽出器として使う
正常画像を1枚のreferenceとして使う
画像をpatchに分ける
各patchの特徴ベクトルを取り出す
L2正規化してcos類似度で比較する
検査画像の各patchについて、正常画像側で一番似ているpatchを探す
似ていない場所ほど異常スコアを高くする
異常マップを作る
しきい値で二値マスクを作る

一方で、手元実装では次の処理を追加しています。

ROIで机の中央付近だけを見る
小さなノイズ領域を除外する
connected componentsで異常候補を分ける
bbox、中心座標、面積、最大スコアを出す
summary.csvに保存する

ここは論文の主張そのものではありません。 机上実験で見やすくしたり、ロボット制御に使いやすくしたりするための追加処理です。

特にROIで見る場所を絞ったのは結果の再現性に重要でした。 絞ったことで画像端、キーボード、影、背景境界に反応しない結果を出し、見てほしいところに絞った確認ができるようにしています。 これは次で書きますが、論文でも、単一reference画像を使う設定では、referenceと見た目が違う正常要素にも反応し得ると述べられています。

机上実験では、これがカメラ位置のずれ、画像端、キーボード、影、背景境界などへの反応として出やすくなるため、ROIで見る範囲を絞りました。


元論文の評価結果

論文では、標準的な異常検出ベンチマークでも評価しています。

BenchmarkAPFPR95AUROC
Fishyscapes L&F26.4392.7661.95
Fishyscapes Static41.1581.7074.62
Road Anomaly70.8339.8292.83

Road AnomalyではAUROCが高く出ています。 一方で、Fishyscapes系ではFPR95が高めです。

これは、正常ではあるけれど参照画像と見た目が違う場所にも反応してしまうことがある、という性質を表していると思います。

論文も「単一reference画像だけを使う設定は導入しやすいが、referenceと見た目が違う正常要素にも反応し得る」という制約を述べています。

この制約は、机上実験でもそのまま出ます。

正常画像と検査画像でカメラ位置がずれる。 光が変わる。 影が変わる。 キーボードの見え方が変わる。

こうしたものにも反応する可能性があります。


まとめ

論文では、自動運転の実車環境上で 正常なreference imageと入力画像をDINOv3に渡し、patchごとの特徴を比較して、似ていない領域を異常として出します。

手元の実装として、それを机の上で試しました。

論文:
正常な道路画像
↓
道路上の異常物体を検出

今回:
正常な机画像
↓
机の上に追加された小物を検出

これだけの比較でも、かなりそれらしい異常検出が作れるなと思いました。