![[Amazon Bedrock] 検査プロンプトを VLM 自身に書かせて外観検査をしてみました 〜欠陥の位置は座標ではなくグリッドのセル名で〜](https://devio2024-media.developers.io/image/upload/f_auto,q_auto,w_3840/v1787352184/user-gen-eyecatch/cx7kn02bbnltrkitl4m5.png)
[Amazon Bedrock] 検査プロンプトを VLM 自身に書かせて外観検査をしてみました 〜欠陥の位置は座標ではなくグリッドのセル名で〜
1 はじめに
製造ビジネステクノロジー部の平内(SIN)です。
VLM(Vision Language Model)で外観検査を試すとき、最初に手が止まるのは「何を検査キーワードにするか」だと思います。
以前 Nova 2 Lite で金属部品の欠陥検出 を試した際、この点で分かりやすい結果が出ました。鋼板表面欠陥のデータセット(NEU-DET)に対して、分類名である patch を検出対象に指定すると 0 件、見た目をそのまま表す dark spot に変えると 2 件検出される、という具合です。inclusion と dark streak でも同じことが起きました。
つまり、同じ画像・同じモデルでも、指定する言葉次第で結果が変わります。そして現場の欠陥を的確な言葉に置き換える作業は、そこそこ悩ましいものです。
そこで今回は、この工程ごと VLM に肩代わりさせてみました。異常サンプルと正常サンプルを数枚渡すと、欠陥の説明・検査プロンプト・出力 JSON スキーマまでを VLM 自身に生成させる、という仕組みです。
もう 1 つ、前記事では欠陥の位置をバウンディングボックスの座標(0〜1000 の正規化座標)で返させていましたが、位置がずれやすいという課題が残っていました。今回は座標をやめ、画像にグリッドを焼き込んで「どのセルか」(A1、B2 …)で答えさせています。
この 2 つ、検査プロンプトの自動生成とグリッドのセル名による位置指定が本稿の柱です。
本稿は、本番ラインの全数検査を置き換えるものではありません。PoC や目視検査の一次スクリーニングを、少ない準備で立ち上げることを狙っています。
検証にあたって考慮した点は以下です。
- 検査キーワードを人が決めない
- 位置は座標ではなくグリッドのセル名で受け取る
- 判定は 2 値ではなく OK / 要目視 / NG の 3 値にする
- 出力が非決定的である前提で組む

2 仕組みと設計方針
処理は 2 段に分かれます。
| 段 | 実行頻度 | モデル | 役割 |
|---|---|---|---|
| 仕様書の生成 | 検査対象ごとに 1 回だけ | Claude Opus 5 | 欠陥仕様書・検査プロンプト・JSON スキーマを作る |
| 検査 | 画像ごとに毎回 | Nova 2 Lite / Claude Haiku 4.5 | OK / REVIEW / NG とセル名を返す |
前段は 1 回しか動かないので、多少時間がかかっても強いモデルを使います。後段は枚数分だけ動くので、速くて安いモデルを使います。
検査の実行方法は 2 通り用意しました。構成図の (1)〜(6) が手元のスクリプトから動かすルート、(A)〜(C) がブラウザから動かすルートです。
| ルート | グリッドを焼き込む場所 | Bedrock を呼ぶ場所 |
|---|---|---|
| スクリプト | 手元の Python(Pillow) | 手元の Python |
| Web UI | ブラウザ(Canvas) | Lambda |
どちらも最後は同じ Bedrock を呼びますが、グリッドを焼き込む場所が違います。Web UI 側はブラウザで焼き込んでから送るので、Lambda に Pillow を入れる必要がありません。
(1) 検証環境
| バージョン | |
|---|---|
| Python | 3.10.13 |
| boto3 | 1.42.91 |
| Pillow | 11.0.0 |
| リージョン | ap-northeast-1 |
Web UI(CDK)側のバージョンは 4 章 (2) に記載します。
(2) 使用したモデル
| 役割 | モデル |
|---|---|
| 仕様書の生成 | global.anthropic.claude-opus-5 |
| 検査 | jp.amazon.nova-2-lite-v1:0 |
| 検査 | jp.anthropic.claude-haiku-4-5-20251001-v1:0 |
なお、モデルの選定で 1 点補足します。ap-northeast-1 で jp.(国内ルーティング)の推論プロファイルが用意されているのは、確認した時点では Claude Opus 4.8 までです。Claude Opus 5 / Sonnet 5 は global. のみでした。
$ aws bedrock list-inference-profiles --region ap-northeast-1 \
--query 'inferenceProfileSummaries[].inferenceProfileId' --output text | tr '\t' '\n' | grep '^jp\.'
# =>
jp.amazon.nova-2-lite-v1:0
jp.anthropic.claude-haiku-4-5-20251001-v1:0
jp.anthropic.claude-sonnet-4-5-20250929-v1:0
jp.anthropic.claude-sonnet-4-6
jp.anthropic.claude-opus-4-7
jp.anthropic.claude-opus-4-8
データを国内で処理したい要件がある場合、仕様書の生成は jp.anthropic.claude-opus-4-8 に置き換えることになります。ここは「最新のモデルを取るか、国内ルーティングを取るか」の判断になるかと思います。
(3) 使用した画像
テスト画像は 2 種類使いました。ライセンスと出典は以下にまとめています。
Github scripts/images/SOURCES.md
- NEU-DET(東北大学の熱間圧延鋼帯の表面欠陥データセット / Roboflow 再配布版 CC BY 4.0)
200x200 のグレースケールなので、3 倍にアップスケールして 600x600 で使っています - Wikimedia Commons の金属の写真(CC0 ほか)
1 点、正常画像について補足します。NEU-DET は全ファイルが欠陥画像で、参照に使える正常画像がありません。そのため既存の欠陥画像から欠陥が写っていない領域を切り出して正常サンプルとしました。拡大縮小はしていません。倍率が変わると検査画像と条件が揃わなくなるためです。
実運用では、同じカメラ・同じ照明で撮った本物の正常品画像を使うべき箇所です。
3 検査プロンプトを VLM 自身に書かせる
1 本目の柱です。
Github scripts/generate_spec.py
異常サンプルと正常サンプルを Claude Opus 5 に渡し、以下の 4 つを JSON で出力させます。
defect_types… 欠陥の種類(名前・見た目・誤認しやすいもの)normal_characteristics… 正常品がどう見えるかinspection_prompt… そのまま使える検査プロンプトoutput_schema… 検査結果の JSON スキーマ
(1) メタプロンプトの要点
要点抜粋です。鍵になるのは、欠陥の命名を明示的に縛っている箇所です。
# 要点抜粋
META_PROMPT = """あなたは製造業の外観検査の専門家です。
...
1. defect_types: 見つかった欠陥の種類。各要素は以下を含むこと
- name: 欠陥の呼び名。**分類の専門用語ではなく、見た目をそのまま表す平易な語**にすること
(例:「介在物」ではなく「濃い色の筋」、「パッチ」ではなく「暗い斑点」)
- appearance: 色・形状・テクスチャ・輪郭の性質を具体的に
- confusable_with: 正常な模様・光沢・映り込みのうち、この欠陥と誤認しやすいもの
...
3. inspection_prompt: 上記を踏まえた検査用プロンプト。**そのまま別のモデルに渡して使える完成品**
にすること。以下を必ず含めること
- この製品で何を探すべきか
- 何を欠陥と見なさないか
- グリッドのセル名で位置を答えること
- 判定は OK / REVIEW / NG の 3 値であること
- 迷ったら NG ではなく REVIEW にすること
"""
「分類の専門用語ではなく、見た目をそのまま表す平易な語」という指定は、冒頭に書いた patch と dark spot の差から来ています。人間が試行錯誤していた言い換えを、最初から指示として渡している形です。
(2) 実行する
$ python generate_spec.py \
--defect images/defect/neu_scratches_1.jpg \
images/defect/neu_scratches_2.jpg \
images/defect/neu_inclusion_1.jpg \
--normal images/normal/neu_normal_1.jpg \
images/normal/neu_normal_2.jpg \
--cols 4 --rows 4 --out spec.json
# =>
saved: spec.json
model : global.anthropic.claude-opus-5
elapsed : 56.55s
tokens : in=2929 out=3230
defects : ['細く光る縦の白い筋', '太く光る横一直線の白い帯', 'うすい暗色の縦の流れ模様']
生成された欠陥名は「細く光る縦の白い筋」「太く光る横一直線の白い帯」「うすい暗色の縦の流れ模様」でした。scratches・inclusion といった分類名ではなく、指示どおり見た目の語になっています。
appearance も具体的でした(抜粋)。
背景の灰色地に対して明るく光る、非常に細い縦方向の線状の欠陥。上下方向に長く伸び、途中で太くなったり細くなったりして紡錘形(膨らんだ部分)を作り、その中に特に白い点状の輝点を含むことがある。
confusable_with(誤認しやすいもの)には、こう書かれていました。
正常品にも見られる圧延方向(縦方向)のわずかな明暗の流れ目や、照明の縦方向の反射ムラ。これらは幅が広くぼやけており、白く鋭く光る芯を持たない点で区別する。
(3) 生成された検査プロンプト
そのまま使える形で出てきました(抜粋)。
あなたは金属板(圧延材)表面のグレースケール検査画像を判定する外観検査AIです。入力画像には
4x4 のグリッドが焼き込まれており、列は左から A, B, C, D、行は上から 1, 2, 3, 4 で、
左上セルが A1 です。位置は必ず A1〜D4 の 16 個のセル名のみで表現し、ピクセル座標・
パーセント・上下左右などの表現は使わないでください。
【探すべき欠陥】
1. 細く光る縦の白い筋: 地肌より明るく光る細い縦線。途中で紡錘状に太くなったり、
内部に白い輝点を含むことがある。長さは1セルを越えて複数セルにまたがることが多い。
...
【欠陥と見なさないもの】
- 全面に一様に分布する細かい粒状ノイズ(ざらつき)。
- 照明由来の緩やかな明暗グラデーション(画面周辺がなだらかに暗い/明るい)。
- 線状・帯状に連ならない、孤立した微小な白点(ダスト・画素ノイズ)。
...
【判定ルール】
- OK: 上記の欠陥がまったく見られず、表面が均一な地肌とノイズ・緩やかな明暗変化のみで説明できる。
- NG: 明らかな欠陥がある。正常テクスチャでは説明できない構造が明確に確認できる場合。
- REVIEW: 欠陥かもしれないが確信が持てない場合。
- 迷った場合は必ず NG ではなく REVIEW を選ぶこと。
「欠陥と見なさないもの」の節まで自分で書いている点が、個人的には一番の収穫でした。ここは人が書くと抜けやすい箇所だと思います。
output_schema も、セル名を enum で列挙した厳密なものが生成されました。
4 欠陥の位置は座標ではなくグリッドのセル名で
2 本目の柱です。
前記事では、VLM に 0〜1000 の正規化座標で bbox を返させていました。ただし小さく低コントラストな画像では位置がずれやすく、temperature=0 でも呼び出しごとに座標が変動する、という結果でした。
そこで今回は座標をやめ、画像にグリッドとセルラベルを焼き込んでから渡しています。
Github scripts/add_grid.py
# 要点抜粋
def add_grid(image, cols=3, rows=3):
image = image.convert("RGB").copy()
w, h = image.size
draw = ImageDraw.Draw(image)
# ラベルを大きくしすぎると欠陥そのものを覆い隠すため、セル幅の 1/6 程度に抑える
line_w = max(2, round(min(w, h) / 300))
font_size = max(12, round(min(w, h) / (max(cols, rows) * 6)))
font = ImageFont.load_default(size=font_size)
...
列は左から A, B, C …、行は上から 1, 2, 3 …。左上が A1 です。分割数は引数にしてあります。

コメントに書いたとおり、ラベルのサイズには注意が必要です。最初はフォントサイズを min(w,h) / (分割数 * 4) にしていたのですが、600x600 の 3x3 では 50px になり、A1 のラベルが検出対象の傷の上端を覆ってしまいました。/6 に変更しています。グリッドを焼き込む方式に固有の注意点かと思います。
(1) 3 値判定と多数決
検査側は inspect_image.py です。
Github scripts/inspect_image.py
判定は OK / REVIEW / NG の 3 値にしています。判断がつかないものを REVIEW にして人の目視に回すことで、「全自動検査」ではなく「目視検査の一次スクリーニング」という位置づけにできます。
加えて、既定で 3 回投げて多数決を取っています。
# 要点抜粋
def vote(results, runs):
"""多数決。過半数に届かない判定は安全側の REVIEW に倒す"""
verdicts = [r["verdict"] for r in results if r]
counts = Counter(verdicts)
top, n = counts.most_common(1)[0]
verdict = top if n > runs / 2 else "REVIEW"
# セルは過半数の試行で挙がったものだけ採用する
cell_counts = Counter(
c for r in results if r for f in r.get("findings", []) for c in f.get("cells", [])
)
cells = sorted(c for c, n in cell_counts.items() if n > len(verdicts) / 2)
...
なお、多数決の理由については当初の想定を修正しました。
はじめは「temperature を 0 に固定したうえで、それでも残るブレを多数決で均す」と考えていました。しかし前記事の実測で temperature=0 でも座標が変動していますし、後述するとおり新しい Claude では temperature の指定自体ができません。
そこで、そもそも VLM の出力は非決定的である前提で多数決する、という整理にしています。
(2) Web UI
CDK で Web UI もデプロイできるようにしました。画像をドラッグ&ドロップすると、グリッドを焼き込んで検査し、検出されたセルを塗って返します。
Github cdk/lib/stack.ts
構成は Lambda + API Gateway + S3 + CloudFront(OAC)です。

グリッドの分割数・推論モデル・多数決の回数を画面から切り替えられるようにしました。欠陥仕様書は generate_spec.py が出力した JSON をテキストエリアに貼り付けます。
結果は OK / REVIEW / NG を色分けして表示し、検出されたセルを半透明の赤で塗っています。判定の内訳とセルの得票も出しているので、3 回のうち何回そのセルが挙がったかが分かります。
実装の方針は前記事と同じで、グリッドの焼き込みをブラウザの Canvas 側で済ませてから送っています。こうすると Lambda はランタイム同梱の boto3 だけで動くので、Pillow のレイヤーも Docker ビルドも不要です。
デプロイは以下です。
$ cd cdk
$ pnpm install
$ pnpm exec cdk deploy
# =>
VlmVisualInspectionStack.ApiUrl = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/inspect
VlmVisualInspectionStack.WebsiteUrl = https://xxxxxxxxxxxxxx.cloudfront.net
常時稼働するリソースはありませんが、検証が終わったら cdk destroy で削除できます。
検証時のバージョンは以下です。
| バージョン | |
|---|---|
| Node.js | 22.14.0 |
| pnpm | 10.24.0 |
| aws-cdk(CLI) | 2.1135.0 |
| aws-cdk-lib | 2.263.0 |
| constructs | 10.8.1 |
| TypeScript | 5.9.3 |
| Lambda ランタイム | Python 3.13 |
5 動作確認
(1) NEU-DET の傷
左端に縦の傷がある画像です。4x4 のグリッドを焼き込んで検査しました。
$ python inspect_image.py --spec spec.json --image images/defect/neu_scratches_1.jpg
# =>
VERDICT: NG cells=['A1', 'A2', 'A3', 'A4']

傷は左端の A 列を縦に貫いているので、A1 から A4 まで挙がるのが期待どおりです。
(2) 別の被写体に適用してみる
NEU-DET は 600x600 のグレースケールです。高解像度のカラー写真でも成立するのか確認しました。
ここでは仕様書の生成に使っていない画像でテストしています。錆びた鋼板と車体の錆から仕様書を作り、それを別の部品写真に適用する、という形です。
$ python generate_spec.py \
--defect images/defect/sample_rust.jpg images/defect/sample_car_rust.jpg \
--normal images/normal/normal_sheet.jpg \
--cols 4 --rows 4 --out spec_rust.json
# =>
defects : ['茶色いざらつき(広がったサビ)',
'塗装のふくれ・めくれと下からのぞく茶色い地肌',
'縁に沿った茶色い変色ライン']
この仕様書で、生成に使っていない部品写真(1280x853)を検査します。

Nova 2 Lite と Claude Haiku 4.5 が、まったく同じセル(A2, A3, A4, B4)を返しました。左のブラケットに腐食が集中しているので、A 列が挙がるのは妥当です。
ただし正確に言うと、B4 は少しずれています。下側のバーにある黒い剥離片は、実際には B3 の範囲にあります。グリッドにしても 1 セル程度のずれは残るということかと思います。とはいえ、bbox で数百ピクセルずれるのとは違い、ずれても隣のセルに収まります。
なお、この検証は条件としては甘い点があるので補足します。仕様書に渡した 3 枚は、正常サンプルも含めて検査対象とは別の被写体です。
| 被写体 | |
|---|---|
| 仕様書の異常サンプル | 錆びた鋼板 / 車のボディ |
| 仕様書の正常サンプル | 無傷の亜鉛メッキ鋼板 |
| 検査した画像 | カセットホルダーの部品 |
つまり「この製品の正常な状態」を見せたわけではなく、「きれいな金属とはこう見えるものだ」という一般例を渡したにすぎません。被写体をまたいで通用した点は収穫ですが、実運用では同じ製品の正常品写真を使うべき箇所です。
(3) 錆の仕様書は傷に反応しない
同じ錆の仕様書で、傷だらけだが錆はない金属板と、無傷の金属板を検査しました。

どちらも両モデルで OK でした。
sample_scratch.jpg は傷そのものは大量にある画像です。それでも OK が返るということは、「何かおかしいものを探す」のではなく「この仕様書に書かれた欠陥だけを探す」挙動になっている、と言えそうです。欠陥仕様書が特異性として機能している、と考えています。
裏を返せば、検査対象が変われば仕様書を作り直す必要があります。
6 つまずいた点
3 点あります。いずれも設計に影響したので共有します。
(1) 画像の直前に 1 行入れるかどうかで結果が変わる
Lambda 版とローカルスクリプト版で、同じ画像なのに結果が食い違いました。調べたところ、違いは Converse API に渡す content 配列の先頭に区切りテキストが 1 つあるかないかだけでした。
Converse API の content は、テキストや画像のブロックを並べた配列です。まず、食い違っていた 2 つを並べます。
食い違いの原因になっていた側(デリミタ無し)
content = [
{"image": {"format": "png", "source": {"bytes": image_bytes}}},
{"text": inspection_prompt}, # generate_spec.py が生成した検査プロンプト
]
もう一方(デリミタ有り)
content = [
{"text": "=== ここから検査画像です。この画像だけを判定してください(4x4 グリッド焼き込み済み)==="}, # この 1 行だけ多い
{"image": {"format": "png", "source": {"bytes": image_bytes}}},
{"text": inspection_prompt},
]
差は先頭の 1 ブロックだけです。画像もプロンプト本文もモデルも同じです。
これで条件を揃えて各 8 回投げました(neu_scratches_1.jpg / 4x4 / Nova 2 Lite / temperature=0)。返ってきた JSON はこうなりました。
デリミタ無し … 8 回とも欠陥なし
{ "verdict": "OK", "findings": [] }
デリミタ有り … 8 回とも正しいセル
{
"verdict": "NG",
"findings": [
{ "defect_type": "細く光る縦の白い筋", "cells": ["A1", "A2", "A3", "A4"], ... }
]
}
この画像は左端の A 列を縦に傷が貫いているので、後者が正解です。前者は見逃しです。
| 条件 | content の組み方 |
NG 検出 | 検出セル |
|---|---|---|---|
| デリミタ無し | [画像, プロンプト] |
0 / 8 | (毎回 空) |
| デリミタ有り | [区切りテキスト, 画像, プロンプト] |
8 / 8 | 毎回 A1, A2, A3, A4 |
8 回中 8 回で完全に反転したので、偶然ではないと思います。
理由としては、content が先頭から順に読まれることが関係していると考えています。検査プロンプトは画像の後ろにあるので、デリミタが無いと「モデルが画像を見た時点では、何をすべきか何も言われていない」状態になります。
コストは数十トークンです。画像を渡す前に、その画像が何なのかを一言書いておくのが良さそうです。
(2) 参照用に「欠陥サンプル」を渡すと誤検知が増える
当初の設計では、検査画像だけでなく「お手本」として正常サンプルと欠陥サンプルも一緒に渡すつもりでした。人が検査を教わるときと同じで、良品と不良品を見せたほうが精度が上がるだろう、という発想です。
しかし実測すると、欠陥サンプルを一緒に渡したときだけ誤検知が増えました。
検査したのは neu_scratches_1.jpg で、実際にある欠陥は左端の縦傷 1 種類だけです。参照として渡したのは以下の 5 枚でした。
| 役割 | ファイル | 写っている欠陥 |
|---|---|---|
| 正常サンプル | neu_normal_1.jpg / neu_normal_2.jpg |
なし |
| 欠陥サンプル | neu_scratches_1.jpg |
縦の傷(=検査画像と同じもの) |
| 欠陥サンプル | neu_scratches_2.jpg |
横に走る白い帯 |
| 欠陥サンプル | neu_inclusion_1.jpg |
縦の濃いにじみ模様 |
この状態で Claude Haiku 4.5 に検査させると、こう返ってきました。
{
"verdict": "NG",
"findings": [
{ "defect_type": "縦に伸びた白い細い筋", "cells": ["A1","A2","A3"], "confidence": 0.92 },
{ "defect_type": "横に走る白く光る帯とその下のざらついた影", "cells": ["B2","C2","B3","C3"], "confidence": 0.88 },
{ "defect_type": "縦方向に流れる濃い色のにじみ模様", "cells": ["B1","B2","B3"], "confidence": 0.75 }
]
}
1 つ目は正解です。しかし 2 つ目と 3 つ目は、検査画像には写っていません。参照として渡した neu_scratches_2.jpg と neu_inclusion_1.jpg に実在する欠陥です。しかも確信度 0.88 / 0.75 と、それなりに自信を持って答えています。
参照画像に写っているものを、検査画像にもあると混同している形です。参照画像の前に「これは検査対象ではありません」というラベルを付けても防げませんでした。
渡し方を 3 通りに変えて比べたのが以下です。
| 参照画像 | 入力トークン | 検出セル | 判定 |
|---|---|---|---|
| 正常+欠陥 | 4,023 | A1, A2, A3, B1, B2, B3, C2, C3 | 誤検知 5 セル |
| 正常のみ | 2,573 | A1, A2, A3 | 正解 |
| 参照なし | 2,122 | A1, A2, A3 | 正解 |
そのため、既定では参照画像を渡さない設定にしています。欠陥の情報は画像ではなく、仕様書というテキストで渡すほうが正確で、しかも入力トークンは約半分で済みました。
人に教えるときは実物を見せたほうが早いのですが、VLM に対しては逆でした。
(3) 新しい Claude では temperature が使えない
モデルの疎通確認をしていて気づいた点です。
ValidationException: The model returned the following errors:
`temperature` is deprecated for this model.
Claude Opus 4.7 以降 / Opus 5 / Sonnet 5 では temperature が廃止されており、渡すとエラーになります。Nova 2 Lite と Claude Haiku 4.5 では従来どおり使えます。
コード側では、受け付けるモデルにだけ渡すようにしました。
# 要点抜粋
NO_TEMPERATURE = re.compile(r"anthropic\.claude-(opus-4-7|opus-4-8|opus-5|sonnet-5|fable-5)")
cfg = {"maxTokens": max_tokens}
if temperature is not None and not _NO_TEMPERATURE.search(model_id):
cfg["temperature"] = temperature
なお、Claude Fable 5 は 30 日のデータ保持設定が必要で、今回のアカウントでは利用できませんでした(data retention mode 'default' is not available for this model)。
(4) 逆に、JSON は壊れなくなりました
こちらは想定と違って良かった点です。
前記事では、Nova 2 Lite が temperature=0 でも壊れた JSON を返すことがありました(数値の後ろにだけ引用符が付く、など)。公式サンプルにも多段のフォールバックが実装されている既知の挙動です。
今回はグリッド方式にしたところ、評価を通してパース失敗はゼロでした。座標という数値の配列を返させるのをやめたことが効いたのだと思います。フォールバックのパーサーはコードに残していますが、一度も発動していません。
7 定量評価とコスト
(1) 3x3 と 4x4 の比較
グリッドの分割数はどちらが良いか、正解セルを目視で決めたうえで測りました。NEU-DET の欠陥 4 枚 + 正常 2 枚、3 回多数決、Nova 2 Lite です。
最初は仕様書 1 本ずつで比較し、「3x3 は見逃し 2 枚、4x4 は見逃し 0 枚」という結果になりました。ただしこれはグリッドの分割数と仕様書の文面が同時に変わっている比較です。
そこで、同じ入力・同じ設定で仕様書を 3 本ずつ生成し、それぞれで評価し直しました。
| グリッド | spec A | spec B | spec C | 平均 recall | 見逃し合計 |
|---|---|---|---|---|---|
| 3x3 | 50%(見逃し 2) | 42%(2) | 42%(2) | 45% | 6 / 12 |
| 4x4 | 100%(0) | 69%(1) | 44%(2) | 71% | 3 / 12 |
読み取れたことは 3 点です。
- 4x4 のほうが平均は良い(recall 45% → 71%、見逃し 6 → 3)。方向としては再現します
- ただし 4x4 のばらつきが大きい(44%〜100%)。最初に出た「見逃し 0」は 3 回のうちの 1 本で、グリッドを変えた効果として提示するのは正確ではありませんでした
- 支配的な要因は、グリッドの分割数よりも「どの仕様書が生成されたか」 でした
3x3 は 3 本とも同じ 2 枚(inclusion と patches)を見逃しています。こちらは安定して悪い、という結果でした。
この点から、実運用では 仕様書を生成して終わりにせず、既知のサンプルで評価してから採用する工程が要ると考えています。仕様書を複数本生成して、手持ちの正解付きサンプルで比較し、良いものを採る、という流れです。リポジトリの evaluate.py がその用途です。
(2) モデル間の比較
同じ条件で Claude Haiku 4.5 とも比較しました(仕様書 1 本ずつ)。
| 画像 | 正解 | Nova 2 Lite | Claude Haiku 4.5 |
|---|---|---|---|
| 縦の傷 | NG @ A1,A2,A3 | NG @ A1,A2,A3 | NG @ A1,A2,A3 |
| 横の傷 | NG @ A2,B2,C2 | NG @ A2,B2,C2 | NG @ A2,B2,C2 |
| 濃い縦の筋 | NG @ B1,B2,B3 | OK(見逃し) | REVIEW @ B1,B2,B3 |
| 暗い斑点 | NG @ C 列ほか | OK(見逃し) | NG @ C1,C2,C3 |
| 正常 1 | OK | OK | OK |
| 正常 2 | OK | OK | OK |
高コントラストの傷は Nova 2 Lite でも問題なく当たりますが、低コントラストの欠陥では見逃しが出ました。外観検査で見逃しは避けたいものなので、「安いモデルで十分」とは言いにくい結果です。
一方、Claude Haiku 4.5 が「濃い縦の筋」で REVIEW を返した点は、3 値判定が意図どおり効いた例だと思いました。判断がつかないものを NG でも OK でもなく人に回す、という設計が機能しています。
(3) 処理時間とコスト
| 条件 | 1 枚あたりの秒数 | 入力トークン | 出力トークン |
|---|---|---|---|
| Nova 2 Lite | 1.1 〜 1.5 秒 | 約 4,500 | 約 400 |
| Claude Haiku 4.5 | 3.4 〜 4.9 秒 | 約 6,100 | 約 1,000 |
トークンは 3 回多数決の合計です。
単価は AWS Pricing API から取得しました。
$ aws pricing get-products --service-code AmazonBedrock --region us-east-1 \
--filters "Type=TERM_MATCH,Field=regionCode,Value=ap-northeast-1" \
"Type=TERM_MATCH,Field=model,Value=Nova 2.0 Lite"
# =>
APN1-Nova2.0Lite-input-tokens $0.000396 per 1K tokens
APN1-Nova2.0Lite-output-tokens $0.003311 per 1K tokens
1 ドル 150 円で換算すると、Nova 2 Lite の 3 回多数決で 1 枚あたり 0.34 〜 0.54 円でした。
なお、Claude Haiku 4.5 と Claude Opus 5 は、確認した時点では ap-northeast-1 の Pricing API に登録がありませんでした。推測で数字を出すのは避けたいので、コードでは単価を引数で渡す形にしています。
仕様書の生成(Claude Opus 5)は、検査対象ごとに 1 回だけで 47 〜 111 秒、入力 2,900 / 出力 2,900 〜 3,300 トークンでした。
8 使いどころと限界
(1) よくある 3 つの懸念
VLM で外観検査という話をすると、だいたい次の 3 点を聞かれます。今回の実測を踏まえた回答です。
| 懸念 | 一般的な指摘 | 今回の設計での回答 |
|---|---|---|
| 判定が遅い | 毎秒何十個も流れるラインに追いつかない | 全数リアルタイム検査は狙いません。一次スクリーニングや抜き取り検査に用途を限定します。実測は 1 枚 1.1 〜 4.9 秒でした |
| 見逃す・幻覚 | ない傷を「ある」と言い、不良を「合格」と誤判定する | 3 値判定でグレーを人に回します。ただし低コントラストの欠陥は実際に見逃しました。見逃しが許されない用途では、モデル選定と事前評価が必須です |
| コストが高い | 高性能 GPU が必要で費用が跳ねる | マネージド API なので GPU の調達は不要です。1 枚 0.34 〜 0.54 円でした |
2 つ目については、正直に書いておきます。今回の範囲では、安いモデルで低コントラストの欠陥を確実に拾うことはできませんでした。
(2) 物体検出モデルとの使い分け
| VLM(今回の方式) | 物体検出モデル(YOLO 等) | |
|---|---|---|
| 事前準備 | 画像数枚 | アノテーション済みデータセット |
| 立ち上げまでの作業 | 仕様書の生成 1 回(実測 47〜111 秒) | 収集・アノテーション・学習 |
| 位置精度 | グリッドのセル単位 | ピクセル単位 |
| 判定理由 | 文章で返る | ヒートマップ等 |
| 精度 | 欠陥の種類に依存 | 学習させれば安定 |
| 向いている場面 | PoC、多品種少量、一次スクリーニング | 品種が固定された本番ライン |
「まず何か動くものを見せたい」段階では VLM、本番ラインに載せる段階では物体検出モデル、という住み分けかと思います。
(3) お客さまへの画像依頼テンプレート
今回の仕組みを試すために必要な画像は、次のようにお願いすると揃います。
- 欠陥のある写真を 2〜3 枚
- 欠陥のない(正常な)写真も 2〜3 枚
- できれば同じカメラ・同じ照明・同じ距離で
- 欠陥の種類が複数あるなら、種類ごとに 1 枚以上
- スマートフォンの手持ち撮影で構いません
正常な写真も依頼する点がポイントです。異常だけだと「何が正常か」の基準がないため、仕様書の confusable_with(誤認しやすいもの)が書けません。
(4) 分かっている限界
- 生成される仕様書にはばらつきがあり、検査精度を左右します。同じ入力で 3 本生成したところ、recall は 44%〜100% まで振れました
- 低コントラストの欠陥は見逃されやすく、安いモデルほどその傾向が出ます
- 位置は 1 セル程度ずれることがあります
- 検査対象が変われば仕様書を作り直す必要があります
9 最後に
今回分かったことを 3 点にまとめます。
- 検査プロンプトは VLM に書かせられます。 「分類の専門用語ではなく見た目の語で」と指示するだけで、
scratchesではなく「細く光る縦の白い筋」という命名が出てきました。「欠陥と見なさないもの」の節まで自分で書いてくれます - 座標をやめてグリッドのセル名にすると、JSON が壊れなくなりました。 位置は 1 セル程度ずれますが、bbox のように大きく外すことはありませんでした
- 一番効いたのは、画像の直前に「これが検査対象です」と 1 行入れることでした。 同じ条件で 8 回中 0 回だった検出が 8 回中 8 回になりました。マルチモーダルでは
contentの順番と区切りが効くのだと思います
一方で、生成される仕様書のばらつきが精度を左右する点は、そのまま残っています。仕様書を生成して終わりにせず、既知のサンプルで評価してから採用する工程を組み込むのが現実的かと思いました。
お客さまから写真を数枚いただくところから外観検査の PoC を始める、という場面のたたき台になれば嬉しいです。
次は、VLM を教師役にして NG 画像を自動生成し、物体検出モデルの学習データを作る方向も試してみたいと考えています。
この記事で使用したコードは、以下に置きました。









