前回の第0回では、YOLOで果物の写真を1枚認識し、種類・信頼度・位置を表示しました。今回は、その結果を業務で使える形に整えます。
画像認識で最初に迷うのが、「信頼度のしきい値をいくつにするか」です。数字を少し変えるだけで、見つかる件数が大きく変わります。この回では、しきい値を変えると何が起きるかを実際の写真で確かめ、結果をJSON(プログラム同士でやり取りしやすいデータの形式)で出力できるようにします。
この回で作るもの
- 認識結果を、信頼度の高い順に並べて表示する
--jsonを付けると、結果をJSONで出力する- しきい値を変えたときの検出件数を、表で比べるスクリプト
信頼度としきい値とは
AIは、見つけた物体ごとに「これはorangeだと思う。自信は0.87」のように信頼度(0〜1の数字)を付けます。1に近いほど自信がある、という意味です。
しきい値は、「この数字以上のものだけを結果として採用する」という線引きです。
- しきい値を高くする:確かなものだけ残る。誤検出(違うものを見つける)は減るが、見逃し(写っているのに見つからない)が増える
- しきい値を低くする:見逃しは減るが、誤検出が増える
どちらが正しいということはなく、業務で「見逃しと誤検出のどちらがより困るか」で決めます。
実装
1. 結果を整える app/results.py
結果を信頼度の高い順に並べ、辞書・JSONに変える関数を用意します。
# app/results.py(抜粋)
def sort_by_confidence(detections: list[Detection]) -> list[Detection]:
"""信頼度の高い順に並べる(同じ信頼度なら左上にあるものを先に)。"""
return sorted(detections, key=lambda d: (-d.confidence, d.box[1], d.box[0]))
def to_dict(image_path: str | Path, confidence: float, detections: list[Detection]) -> dict[str, object]:
"""画像1枚分の結果を、JSONにできる辞書にする。"""
return {
"image": Path(image_path).name,
"confidence_threshold": confidence,
"count": len(detections),
"detections": [
{"label": d.label, "confidence": round(d.confidence, 3), "box": list(d.box)}
for d in sort_by_confidence(detections)
],
}
def to_json(image_path: str | Path, confidence: float, detections: list[Detection]) -> str:
"""日本語をそのまま読める形の JSON 文字列にする。"""
return json.dumps(to_dict(image_path, confidence, detections), ensure_ascii=False, indent=2)
ensure_ascii=False:日本語のファイル名などを、文字化けした記号(写...)ではなくそのまま出します- 画像のパスは、ファイル名だけを出します。フォルダ名に社内の情報が含まれても、結果に残さないためです
2. しきい値ごとの件数を数える関数
しきい値を変えるたびにAIを再実行すると時間がかかります。そこで、いちばん低いしきい値で1回だけ認識し、あとから絞り込む方法にします。
# app/results.py(抜粋)
def counts_by_threshold(detections: list[Detection], thresholds: list[float]) -> dict[float, int]:
"""しきい値ごとに「何件残るか」を数える。低いしきい値で検出した結果を、後から絞り込む前提。"""
return {t: sum(1 for d in detections if d.confidence >= t) for t in thresholds}
3. コマンドに --json を追加
app/main.py に、JSON出力の選択肢を足します。
# app/main.py(変更部分)
parser.add_argument("--json", action="store_true", help="結果を JSON で出力する(ほかのプログラムに渡す用)")
args = parser.parse_args(argv)
detections = Detector(confidence=args.conf).detect(args.image)
if args.json:
print(to_json(args.image, args.conf, detections))
return 0
print(f"{len(detections)} 件見つかりました(しきい値 {args.conf})")
for d in sort_by_confidence(detections):
print(f"- {d.label} 信頼度 {d.confidence:.2f} 位置 {d.box}")
4. しきい値を比べるスクリプト
# tools/conf_sweep.py(抜粋)
THRESHOLDS = [0.10, 0.25, 0.50, 0.75, 0.90]
def main(image: str) -> None:
# いちばん低いしきい値で一度だけ推論し、あとは件数を数え直す(推論を何度も走らせない)
detections = Detector(confidence=min(THRESHOLDS)).detect(image)
print(f"{image}: しきい値ごとの件数")
print("しきい値 件数")
for threshold, count in counts_by_threshold(detections, THRESHOLDS).items():
print(f"{threshold:>6.2f} {count:>4}")
動かして確かめる
しきい値ごとの件数
python tools/conf_sweep.py samples/fruits.jpg
執筆時の開発環境での結果です。
samples/fruits.jpg: しきい値ごとの件数
しきい値 件数
0.10 8
0.25 4
0.50 2
0.75 1
0.90 0
同じ写真でも、しきい値が0.10なら8件、0.90なら0件です。しきい値の決め方だけで、結果が大きく変わることが分かります。
しきい値0.10で増える結果の中身
しきい値を0.10にして、増えた分の中身を見てみます。
python -m app.main samples/fruits.jpg --conf 0.1
8 件見つかりました(しきい値 0.1)
- orange 信頼度 0.87 位置 (317, 247, 512, 477)
- orange 信頼度 0.72 位置 (69, 44, 348, 470)
- orange 信頼度 0.47 位置 (0, 277, 130, 477)
- orange 信頼度 0.40 位置 (321, 121, 512, 305)
- orange 信頼度 0.21 位置 (0, 95, 146, 241)
- orange 信頼度 0.19 位置 (0, 1, 158, 105)
- dining table 信頼度 0.13 位置 (5, 3, 512, 480)
- orange 信頼度 0.10 位置 (0, 230, 76, 294)
増えた4件の枠を写真に重ねて確認すると、次のようでした(枠の位置と写真を照らし合わせて、人が判断しています)。
- 信頼度0.19(左上):写真の左上に写っている本物のオレンジで、しきい値0.25だと見逃していたもの
- 信頼度0.21(左の中ほど):バナナの上側を「orange」と誤って答えている
- 信頼度0.13:写真全体を「dining table(食卓)」と答えている。果物とは無関係な誤検出
- 信頼度0.10(左の下寄り):これもバナナの下側で、「orange」の誤検出
つまり、しきい値を下げると本物の見逃しは減るが、誤検出も増える、という関係が実際の写真で確認できました。
JSON出力
python -m app.main samples/fruits.jpg --json
{
"image": "fruits.jpg",
"confidence_threshold": 0.25,
"count": 4,
"detections": [
{
"label": "orange",
"confidence": 0.867,
"box": [317, 247, 512, 477]
}
]
}
(実際の出力は4件分が続きます。紙面の都合で1件に短縮しています)
このJSONは、次の回以降で画像に描く処理や、集計・APIで再利用します。
テスト
pytest -q # 8件成功
ruff check . # 問題なし
結果の並び替え・JSON化・しきい値ごとの件数は、モデルを読み込まなくても確かめられるので、テストが速く済みます。
つまずきやすい点
- しきい値を「精度」と混同しない:しきい値0.25は「25%の確率で正しい」という意味ではありません。信頼度は、モデルの内部の目安で、そのまま正解率を表すわけではありません(正しく測る方法は第6回で扱います)
- しきい値は1つの正解がない:対象物や写真の撮り方が変わると、ちょうどよい値も変わります。実際の業務の写真で決めます
- JSONの
boxの順序:左・上・右・下の順です。開発会社ごとに「左上と幅・高さ」形式などがあるため、連携するときは形式を確認します
発注者向けメモ:「精度○%」の前に決めること
画像認識の相談では、「精度は何%出ますか」と聞かれがちですが、しきい値の決め方だけで結果が変わるように、「精度」という1つの数字だけでは判断できません。
発注者がやること チェックリスト
- ☐ 見逃しと誤検出のどちらがより困るかを、業務の言葉で決めた(例:不良品の見逃しは許されないが、誤検出は人が確認すればよい)
- ☐ 誤検出・見逃しが起きたときに、人が確認・修正する手順を決めた
- ☐ 判定結果を、どのシステムにどんな形(JSON・CSV等)で渡すかを決めた
- ☐ 実際の現場写真で、しきい値の違いによる結果を見比べた
開発会社への質問例
- 「しきい値は、どの写真を使って、どう決めますか。見逃しと誤検出のどちらを優先する設定ですか」
- 「精度の数字を出す場合、どのテスト画像で、何枚で測りますか」
- 「信頼度が低い結果は、自動で採用せず、人の確認に回す設計にできますか」
- 「結果を当社の既存システムに渡すときのデータ形式(項目・座標の表し方)は、どう決めますか」
まとめと次回予告
第1回では、次のことを行いました。
- 信頼度としきい値の関係を、実際の写真で確かめた(0.10で8件、0.90で0件)
- しきい値を下げると、見逃しは減るが誤検出が増えることを確認した
- 結果を信頼度順に並べ、JSONで出力できるようにした
- しきい値ごとの件数を比べるスクリプトを作った
次回の第2回では、認識した位置を画像に枠とラベルで描いて、目で確認できるようにします。
この連載の記事一覧
この記事は連載「YOLOで作る画像認識ツール」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【YOLOで作る画像認識ツール 第0回】全体像と環境づくり:YOLOで最初の1枚を認識する
- 【YOLOで作る画像認識ツール 第1回】信頼度のしきい値とJSON出力:認識結果を「使える形」にする(この記事)
- 【YOLOで作る画像認識ツール 第2回】認識結果を画像に描いて確認する:枠とラベルで「間違い」を見つけやすくする
- 【YOLOで作る画像認識ツール 第3回】種類ごとに何個あるか数えて、CSVに集計する
- 【YOLOで作る画像認識ツール 第4回】FastAPIで画像認識を「API」にして、ほかのシステムから使えるようにする
- 【YOLOで作る画像認識ツール 第5回】ブラウザから写真をアップロードして、認識結果をその場で見る
- 【YOLOで作る画像認識ツール 第6回】精度を数字で測る:正解データと、適合率・再現率
- 【YOLOで作る画像認識ツール 第7回】学習済みモデルにない対象を覚えさせる:追加学習の手順と、結果の正しい読み方
- 【YOLOで作る画像認識ツール 第8回】自信の低い結果だけ人が確認する:AIと人の役割分担を設計する
- 【YOLOで作る画像認識ツール 第9回】速く・軽くする:ONNXに変換してCPUで動かし、変換前後を比べる
- 【YOLOで作る画像認識ツール 第10回】Dockerで動かす:設定・ログ・ヘルスチェックを運用に近い形にする
- 【YOLOで作る画像認識ツール 第11回(最終回)】運用の注意点とライセンス(AGPL-3.0):精度の低下に気づく仕組みと、連載のまとめ

