論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
物体検出
Object Detection
画像認識

🔖 キーワード索引

物体検出(Object Detection)を深く理解するために、 本ページで扱う主要キーワードを並べます。 クリックで該当セクションへジャンプ:

📍 文脈 🎨 直感 📐 IoU 定義 🔬 数式を言葉で読み解く 🧮 実値計算 🐍 Python 実装 ⚠️ 落とし穴 🌐 関連手法 🔗 関連用語 📚 関連グループ教材

💡 30秒で分かる結論

🍰 まずはやさしく

画像の中の物を探す技術です。

物の場所と名前を同時に知るために使います。

スマホで写真の人物を囲む機能に似ています。

この章では仕組みと評価の方法を学びます。

📍 文脈 — どこで使う概念か

🍰 まずはやさしく

画像認識という分野の主要な技術です。

どこに何があるかを正確に答えるために使います。

自動運転で歩行者を見つける時に役立ちます。

他の似た技術との違いについて解説します。

物体検出(Object Detection)は、 画像認識の中核タスクの 1 つ。 単純な「画像分類」(猫か犬か)より一段難しく、 「画像のどこに何があるか」を答えます。 自動運転(周辺車両・歩行者)、 監視カメラ、 製造業の品質検査など 産業応用が極めて多い領域です。

📍 文脈ボックス:あなたが今見ているもの

あなたは今、 コンピュータビジョン(CV)の中核タスクの一つである「物体検出」のページにいます。 CV のタスクは以下の階層で整理できます:

物体検出は自動運転(人・車・標識検知)、 監視カメラ、 医療画像(病変検出)、 小売(棚卸し)、 農業(果実検出)など実応用の多くで採用されています。

🎨 直感で掴む — 具体例で理解する

🍰 まずはやさしく

写真に付箋を貼るような作業です。

物の種類と自信の強さを記録するために使います。

部活の集合写真から友達を探す感覚です。

機械にとってなぜ難しいのかを考えます。

画像認識タスクの階層:

タスク出力例
画像分類1 つのラベル「この画像は猫」
物体検出複数のボックス + ラベル「(100,50,200,150) に猫、 (300,100,400,300) に犬」
セマンティックセグメンテーションピクセル単位のラベル「このピクセル群が猫」
インスタンスセグメンテーションピクセル + 個体ID「猫1、 猫2 を区別」

物体検出は「いくつあるか分からない物体を検出」する必要があり、 アルゴリズム設計が複雑。 主流は 1 段階検出器(YOLO, SSD)と 2 段階検出器(Faster R-CNN)に分かれます。

🎨 直感で掴む

物体検出を「画像にラベル付き付箋を貼る作業」と考えてみましょう。 写真の中の犬・人・自転車それぞれに長方形の付箋を貼り、 「犬 92%」「人 88%」「自転車 76%」と書き込みます。 これがまさに物体検出器の出力です。

人間は瞬時にこれを行えますが、 機械にとっては難題です。 なぜなら:

そこで近年のモデルは「① 候補矩形を多数生成 → ② 各候補をクラス分類 + 矩形微調整 → ③ 重複矩形を間引く(NMS)」という流れで処理します。 これを 1 段階で同時にやるのが YOLO、 2 段階に分けるのが Faster R-CNN です。

モデル系代表特徴速度精度
2 段階検出Faster R-CNN, Mask R-CNN候補抽出 → 分類の二段構え中高
1 段階検出YOLO v3-v10, SSD, RetinaNetgrid + anchor で一発推論高中〜高
Transformer 系DETR, Deformable DETRNMS 不要・end-to-end中高
Anchor-freeFCOS, CenterNetアンカー不要、 中心点予測高中〜高

📐 定義・数式

🍰 まずはやさしく

正解との重なり具合を測るルールです。

AIの予測がどれくらい正しいか判定するために使います。

買い物で欲しい物を正確に見つける感覚です。

計算に使う重要な指標について説明します。

【IoU(Intersection over Union)】
$$\mathrm{IoU} = \frac{|A \cap B|}{|A \cup B|}$$
予測ボックス $A$ と正解ボックス $B$ の重なり率。 0〜1 の値。 IoU > 0.5 で「正解」と判定するのが慣例
【mAP(mean Average Precision)】
$$\mathrm{mAP} = \frac{1}{C} \sum_{c=1}^{C} \mathrm{AP}_c$$
クラス $c$ ごとの AP(Precision-Recall 曲線下面積)を平均した総合指標

📐 数式または定義

物体検出を支える 4 つの中核数式を順に示します。

(1) IoU(Intersection over Union): 予測矩形 A と正解矩形 B の重なり度合いを 0〜1 で測る指標。

$$\mathrm{IoU}(A, B) = \frac{\mathrm{Area}(A \cap B)}{\mathrm{Area}(A \cup B)} = \frac{|A \cap B|}{|A| + |B| - |A \cap B|}$$

(2) Precision / Recall: 検出された矩形のうち IoU≥τ の割合が Precision、 正解矩形のうち拾えたものの割合が Recall。

$$\mathrm{Precision} = \frac{TP}{TP + FP}, \quad \mathrm{Recall} = \frac{TP}{TP + FN}$$

(3) Average Precision(AP): 信頼度スコアの閾値を変えて描いた PR 曲線の下面積。

$$\mathrm{AP} = \int_0^1 P(r)\,dr \approx \sum_{i=1}^{N} (r_i - r_{i-1}) \cdot P_i$$

(4) mean Average Precision(mAP): 全クラスの AP の平均。

$$\mathrm{mAP} = \frac{1}{C} \sum_{c=1}^{C} \mathrm{AP}_c$$

(5) Non-Maximum Suppression(NMS): 重複矩形を間引くアルゴリズム。 同クラス内で IoU が閾値(例 0.5)以上の矩形のうち最高スコア以外を捨てる。

$$\mathrm{Keep}(b_i) \iff \forall j: s_j > s_i \Rightarrow \mathrm{IoU}(b_i, b_j) < \tau_{\mathrm{nms}}$$

🔬 記号・要素の読み解き

バウンディングボックス
(x, y, w, h) または (x1, y1, x2, y2) で物体を矩形で囲む
クラス
物体のカテゴリ(人、 車、 犬 など)
信頼度(confidence)
「この検出が正しい確率」
IoU
重なり率。 マッチング判定に使う
NMS(Non-Max Suppression)
重複検出を 1 つに統合する後処理
mAP
全クラスの総合精度

🔬 数式を言葉で読み解く

「数式を言葉で読み解く」ことは、 数学アレルギーを乗り越える最良の方法です。 各記号の意味を日本語で並べ直します:

記号意味物体検出での実体
A, B2 つの矩形(集合)予測 box と正解 box
A ∩ B共通部分(重なり領域)交差矩形の面積
A ∪ B和集合(合体領域)予測と正解の総面積
IoU=1完全一致予測 = 正解
IoU=0重なりゼロ完全外れ
TP, FP, FN真陽性 / 偽陽性 / 偽陰性IoU≥0.5 で正解 → TP、 過検出 → FP、 見落とし → FN
P(r)Recall=r での PrecisionPR 曲線の高さ
Cクラス数COCO なら 80, PASCAL VOC なら 20
τ_nmsNMS の IoU 閾値通常 0.45〜0.5

読み下し例: 「IoU = (重なりの面積) ÷ (合体の面積)。 IoU が 0.5 以上なら『当たり』とみなし、 PR 曲線を作って積分すると AP、 それをクラス平均すると mAP」。

🧮 数値例・実値計算

YOLOv5 等の代表モデルの性能比較(COCO データセット):

モデルmAP速度 (FPS)パラメータ数
YOLOv5s37.41407.2M
YOLOv5x50.73287M
Faster R-CNN42.0541M
DETR44.91041M
YOLOv8x53.94068M

速度と精度はトレードオフ。 リアルタイム用途(自動運転)は YOLO 系、 精度重視は R-CNN 系。

🧮 数式に値を入れて手で計算する: mAP の計算

合成データでクラス別 AP と平均 (mAP) を計算する。

Step 1: クラス別 AP

クラスAP
person0.85
car0.78
dog0.92
cat0.65

Step 2: mAP

mAP = (0.85+0.78+0.92+0.65)/4 = 3.20/4 = 0.80

🐍 Python で再現

1
2
3
import numpy as np
ap = np.array([0.85, 0.78, 0.92, 0.65])
print(f"mAP: {ap.mean():.3f}")

📤 実行結果

mAP: 0.800

💬 手計算 (Step 2) 0.80 と Python 出力が完全一致。

🧮 IoU を手で計算する — 「重なって見えるのに不正解」になる例

上の mAP の計算はクラス別 AP が与えられた後の「平均をとる」部分だけでした。ここからは、その AP がどこから来るのかを 仮想の箱の座標(画像や実在のデータセットではなく、計算の仕組みを見るための合成データ)で 1 段ずつ手計算し、Python で同じ値になることを確かめます。まずは全ての出発点になる IoU です。

正解の箱 $A = (x_1, y_1, x_2, y_2) = (50, 40, 150, 120)$、予測の箱 $B = (70, 50, 170, 140)$ とします(単位はピクセル、左上と右下の座標)。

Step計算値
1. 面積$|A| = (150-50)(120-40) = 100 \times 80$、$|B| = (170-70)(140-50) = 100 \times 90$8,000 と 9,000
2. 重なりの幅・高さ幅 $\min(150,170) - \max(50,70) = 150-70$、高さ $\min(120,140) - \max(40,50) = 120-50$80 と 70
3. 重なりと和集合$|A \cap B| = 80 \times 70$、$|A \cup B| = 8000 + 9000 - 5600$5,600 と 11,400
4. IoU$5600 / 11400$0.4912

予測は正解の箱の 70% を覆っている(5,600 ÷ 8,000)のに、IoU は 0.4912 で閾値 0.5 にわずかに届きません。mAP@0.5 ではこの予測は FP(過検出)になり、同時に正解 A は誰にも当てられなかったので FN(見逃し)にも数えられます。「1 つの惜しい予測が FP と FN の 2 回分の失点になる」のが IoU 閾値で判定する評価の特徴です。

🎯 このコードでやること:上の Step 1〜4 を関数にして同じ箱で IoU を計算し、さらに「右に 8 ピクセル・下に 8 ピクセルずれた予測」の IoU を物体の一辺 16〜256 ピクセルで比べる。

📥 入力例 正解の箱 (50, 40, 150, 120)、予測の箱 (70, 50, 170, 140) 正方形の物体(一辺 s = 16, 32, 64, 128, 256)と、同じ大きさで (8, 8) ずれた予測
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np

def iou(a, b):
    """a, b = (x1, y1, x2, y2)。左上と右下の座標(ピクセル)"""
    iw = max(0, min(a[2], b[2]) - max(a[0], b[0]))   # 重なりの幅
    ih = max(0, min(a[3], b[3]) - max(a[1], b[1]))   # 重なりの高さ
    inter = iw * ih
    area_a = (a[2] - a[0]) * (a[3] - a[1])
    area_b = (b[2] - b[0]) * (b[3] - b[1])
    return inter, area_a + area_b - inter, inter / (area_a + area_b - inter)

gt, pred = (50, 40, 150, 120), (70, 50, 170, 140)
inter, union, v = iou(gt, pred)
print(f'重なり {inter}  和集合 {union}  IoU = {v:.4f}  → 閾値 0.5 で', 'TP' if v >= 0.5 else 'FP')

# 同じ 8 ピクセルのずれ(右に 8・下に 8)が、物体の大きさでどう効くか
print('一辺 s  IoU(8 px ずれ)')
for s in [16, 32, 64, 128, 256]:
    g = (0, 0, s, s)
    p = (8, 8, s + 8, s + 8)
    print(f'{s:>6}  {iou(g, p)[2]:.3f}')
📤 実行例(実測) 重なり 5600 和集合 11400 IoU = 0.4912 → 閾値 0.5 で FP 一辺 s IoU(8 px ずれ) 16 0.143 32 0.391 64 0.620 128 0.784 256 0.884

💬 IoU は手計算の 5,600 ÷ 11,400 = 0.4912 と一致し、閾値 0.5 で FP と判定されます。後半の表が物体検出の評価でいちばん誤解されやすい点で、同じ 8 ピクセルのずれでも、一辺 256 ピクセルの物体なら IoU 0.884 で余裕の TP、64 ピクセルなら 0.620 で mAP@0.5 は TP でも mAP@0.75 では FP、32 ピクセルで 0.391、16 ピクセルでは 0.143 と完全な外れ扱いになります。COCO の「小物体(面積 32² 未満)の AP が大物体よりずっと低い」という傾向の一部はモデルの力不足ではなく、この IoU の物差しが小さい物体ほど厳しいことから来ています。小物体が主役の用途(遠方の歩行者、ドローン画像の果実など)では、サイズ別に AP を分けて報告し、閾値の意味をそろえて比べる必要があります。

一辺 8〜256 ピクセルの正方形の正解と、縦横に 2・4・8・16 ピクセルずれた予測の IoU の曲線。8 ピクセルのずれでは一辺 44 ピクセル以上で IoU 0.5、108 ピクセル以上で 0.75 を超える。一辺 32 ピクセル未満(COCO の小物体)の範囲を灰色で示す

図の読み方: 上のコードの計算を、ずれ δ = 2・4・8・16 ピクセルについて一辺 8〜256 ピクセルで連続に描いたもの(IoU = (s−δ)² ÷ (2s² − (s−δ)²)、乱数なしの計算例)。8 ピクセルのずれで IoU 0.5 を超えるには一辺 44 ピクセル以上、0.75 を超えるには 108 ピクセル以上が必要です。わずか 2 ピクセルのずれでも、一辺 11 ピクセル未満では IoU 0.5 に届きません。灰色の帯は COCO が「小物体」と呼ぶ面積 32² 未満の範囲で、ここでは 4 ピクセルずれるだけで IoU が 0.620 以下になり、mAP@[0.5:0.95] の高い閾値ではほぼ FP として数えられます。画像を縮小して入力すると物体の一辺も同じ割合で縮むので、入力解像度を下げたときに小物体の AP が真っ先に落ちるのもこの曲線の左側に移るためです。図の作成スクリプトは code/glossary_figs/object-detection.py。

🧮 NMS を手で回す — 閾値 τ を下げすぎると隣の物体まで消える

検出器は 1 つの物体のまわりに少しずつずれた箱を何本も出すので、NMS(Non-Maximum Suppression)で「信頼度の最も高い箱を残し、それと IoU が τ を超える箱を消す」を繰り返して 1 本にまとめます。隣り合って立つ 2 人の人物を想定した仮想の候補箱 5 本で、手順を追います。

箱座標 (x1, y1, x2, y2)信頼度想定
a(100, 100, 200, 300)0.95人物 1
b(110, 105, 210, 305)0.90人物 1 の重複
c(180, 100, 280, 300)0.85人物 2(人物 1 と 20 px 重なる)
d(185, 110, 285, 310)0.60人物 2 の重複
e(400, 400, 450, 450)0.30離れた小さい箱
Step計算(τ = 0.5)結果
1. 最高信頼度を残すa(0.95)を確定残り b, c, d, e
2. a との IoUb: 重なり 90 × 195 = 17,550、和集合 20,000 + 20,000 − 17,550 = 22,450 → 0.782
c: 重なり 20 × 200 = 4,000、和集合 36,000 → 0.111
d: 重なり 15 × 190 = 2,850、和集合 37,150 → 0.077
e: 重なり 0 → 0
0.782 > 0.5 の b だけ削除
3. 残りの最高を残すc(0.85)を確定残り d, e
4. c との IoUd: 重なり 95 × 190 = 18,050、和集合 40,000 − 18,050 = 21,950 → 0.822d を削除
5. 最後e を確定残るのは a, c, e

🎯 このコードでやること:手計算と同じ 5 本の候補箱に、IoU 閾値 τ = 0.5 と τ = 0.1 で NMS を実行し、どの箱がどの箱に消されたかを表示する。

📥 入力例 候補箱 5 本(上の表の a〜e)と信頼度 0.95, 0.90, 0.85, 0.60, 0.30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import numpy as np

boxes = np.array([[100, 100, 200, 300],   # a: 人物 1
                  [110, 105, 210, 305],   # b: 人物 1 の重複
                  [180, 100, 280, 300],   # c: 人物 2(人物 1 と隣り合う)
                  [185, 110, 285, 310],   # d: 人物 2 の重複
                  [400, 400, 450, 450]])  # e: 離れた小さい箱
scores = np.array([0.95, 0.90, 0.85, 0.60, 0.30])
names = np.array(list('abcde'))

def iou_1_to_n(box, others):
    iw = np.clip(np.minimum(box[2], others[:, 2]) - np.maximum(box[0], others[:, 0]), 0, None)
    ih = np.clip(np.minimum(box[3], others[:, 3]) - np.maximum(box[1], others[:, 1]), 0, None)
    inter = iw * ih
    area = lambda b: (b[..., 2] - b[..., 0]) * (b[..., 3] - b[..., 1])
    return inter / (area(box) + area(others) - inter)

def nms(boxes, scores, tau):
    order = np.argsort(-scores)
    keep = []
    while order.size:
        i = order[0]
        keep.append(i)
        rest = order[1:]
        ious = iou_1_to_n(boxes[i], boxes[rest])
        for j, v in zip(rest, ious):
            if v > tau:
                print(f'  {names[i]} を残す → {names[j]} は IoU {v:.3f} > {tau} で削除')
        order = rest[ious <= tau]
    return keep

for tau in [0.5, 0.1]:
    print(f'τ = {tau}')
    print('  残った箱:', ', '.join(names[nms(boxes, scores, tau)]))
📤 実行例(実測) τ = 0.5 a を残す → b は IoU 0.782 > 0.5 で削除 c を残す → d は IoU 0.822 > 0.5 で削除 残った箱: a, c, e τ = 0.1 a を残す → b は IoU 0.782 > 0.1 で削除 a を残す → c は IoU 0.111 > 0.1 で削除 残った箱: a, d, e

💬 τ = 0.5 では手計算どおり b(a との IoU 0.782)と d(c との IoU 0.822)が消え、a・c・e の 3 本が残ります。人物 1 と人物 2 はそれぞれ 1 本ずつになり、NMS が意図どおりに働いています。τ = 0.1 に下げると、人物 2 の本命の箱 c が a との IoU 0.111 で消されてしまいます。この例では偶然 d(a との IoU 0.077)が生き残るので人物 2 はかろうじて検出されますが、残るのは信頼度 0.60 の、正解からずれた箱です。τ を下げるほど重複は確実に消えますが、混み合った場面(群衆、商品棚、隣接する車)では別の物体まで消すことになり、再現率が落ちます。逆に τ を上げすぎると重複が残って FP が増えるので、τ は検証データで AP を見ながら決め、混雑が本質的な用途では Soft-NMS(消さずに信頼度を下げる)を検討します。

🧮 AP を検出の順位表から手で計算する — 全点補間と 11 点補間

AP は「信頼度の高い順に検出を並べ、1 件ずつ TP/FP を判定しながら適合率と再現率を更新し、その曲線の下の面積を求めたもの」です。正解が 6 個ある 1 クラスの画像群に対して、検出器が 10 件の箱を出したとします(仮想の結果)。各検出には「最もよく重なる正解の番号」と「その IoU」が付いています。

順位信頼度重なる正解IoUIoU 0.5 での判定
10.98g10.88TP
20.95g20.81TP
30.91g10.62FP(g1 は順位 1 で当てられ済み=重複)
40.87g30.72TP
50.80なし—FP(背景を検出)
60.74g40.55TP
70.66g50.47FP(IoU 不足)
80.58なし—FP
90.45g60.79TP
100.30g50.53TP(g5 はまだ誰にも当てられていない)
Step計算値
1. 累積の TP と FP順位 k までの TP 数・FP 数を数える(例: 順位 5 で TP 3・FP 2)下の出力の表
2. 適合率と再現率適合率 = 累積 TP ÷ k、再現率 = 累積 TP ÷ 6(例: 順位 5 で 3/5 = 0.600、3/6 = 0.500)下の出力の表
3. 包絡線各再現率で「それ以上の再現率での最大の適合率」に置き換える。再現率 0.167, 0.333, 0.500, 0.667, 0.833, 1.000 で 1.000, 1.000, 0.750, 0.667, 0.600, 0.600ギザギザを除いた階段
4. 全点補間の AP再現率は 1/6 ずつ増えるので $\tfrac{1}{6}(1.000 + 1.000 + 0.750 + 0.667 + 0.600 + 0.600) = \tfrac{4.617}{6}$0.7694
5. 11 点補間の AP再現率 0, 0.1, …, 1.0 の 11 点で包絡線を読む: 1, 1, 1, 1, 0.75, 0.75, 0.667, 0.6, 0.6, 0.6, 0.6 の平均 $= 8.567 / 11$0.7788

🎯 このコードでやること:手計算と同じ 10 件の検出を信頼度順に TP/FP 判定し、累積の適合率・再現率の表と、全点補間・11 点補間の AP を計算する。

📥 入力例 検出 10 件: (信頼度, 重なる正解の番号, IoU) の組。正解は g1〜g6 の 6 個(-1 は正解と重ならない検出)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import numpy as np

# 仮想の検出結果 10 件(1 クラス、正解の箱は 6 個)。
# gt = 一番よく重なる正解の番号(無ければ -1)、iou = その正解との IoU
det = [(0.98, 1, 0.88), (0.95, 2, 0.81), (0.91, 1, 0.62), (0.87, 3, 0.72), (0.80, -1, 0.0),
       (0.74, 4, 0.55), (0.66, 5, 0.47), (0.58, -1, 0.0), (0.45, 6, 0.79), (0.30, 5, 0.53)]
N_GT = 6

def ap_at(thr, verbose=False):
    det_s = sorted(det, key=lambda d: -d[0])      # 信頼度の高い順
    used, tp = set(), []
    for s, g, v in det_s:
        ok = g != -1 and v >= thr and g not in used   # 同じ正解への 2 回目は FP
        if ok:
            used.add(g)
        tp.append(int(ok))
    tp = np.array(tp)
    ctp, cfp = np.cumsum(tp), np.cumsum(1 - tp)
    prec, rec = ctp / (ctp + cfp), ctp / N_GT
    if verbose:
        print(' 順位 信頼度 判定 累積TP 累積FP  適合率  再現率')
        for k in range(len(tp)):
            print(f'{k+1:>4}  {det_s[k][0]:.2f}  {"TP" if tp[k] else "FP"}  {ctp[k]:>5}  {cfp[k]:>5}   {prec[k]:.3f}   {rec[k]:.3f}')
    # 全点補間(COCO・VOC2010 以降): 右側の最大の適合率で包絡線をとって面積を足す
    r = np.concatenate([[0], rec, [1]]); p = np.concatenate([[1], prec, [0]])
    for i in range(len(p) - 2, -1, -1):
        p[i] = max(p[i], p[i + 1])
    idx = np.flatnonzero(r[1:] != r[:-1])
    ap_all = float(np.sum((r[idx + 1] - r[idx]) * p[idx + 1]))
    # 11 点補間(VOC2007): 再現率 0, 0.1, ..., 1.0 で「それ以上の再現率での最大適合率」を平均
    ap11 = float(np.mean([prec[rec >= t].max() if (rec >= t).any() else 0 for t in np.linspace(0, 1, 11)]))
    return tp, ap_all, ap11

tp, a, a11 = ap_at(0.5, verbose=True)
print(f'IoU 0.5: AP(全点補間) = {a:.4f}   AP(11 点補間) = {a11:.4f}')
📤 実行例(実測) 順位 信頼度 判定 累積TP 累積FP 適合率 再現率 1 0.98 TP 1 0 1.000 0.167 2 0.95 TP 2 0 1.000 0.333 3 0.91 FP 2 1 0.667 0.333 4 0.87 TP 3 1 0.750 0.500 5 0.80 FP 3 2 0.600 0.500 6 0.74 TP 4 2 0.667 0.667 7 0.66 FP 4 3 0.571 0.667 8 0.58 FP 4 4 0.500 0.667 9 0.45 TP 5 4 0.556 0.833 10 0.30 TP 6 4 0.600 1.000 IoU 0.5: AP(全点補間) = 0.7694 AP(11 点補間) = 0.7788

💬 表の適合率・再現率は Step 1〜2 の手計算と一致し、全点補間の AP 0.7694、11 点補間の AP 0.7788 も Step 4・5 と一致します。同じ検出結果でも補間の仕方で AP が 0.0094 違うので、論文や記事の AP を比べるときは「VOC2007 の 11 点」か「全点(VOC2010 以降・COCO は 101 点)」かをそろえる必要があります。表を見ると、適合率は順位 3(重複の FP)と順位 5・7・8 で下がり、順位 9・10 の TP で少し戻ります。包絡線をとるのはこのギザギザを消して「その再現率を達成できる最良の適合率」で評価するためです。なお順位 10 の検出は信頼度 0.30 と低いのに TP で、これがあるから再現率 1.000 に届いています。後処理で信頼度 0.5 未満を捨てていれば、この 1 件と順位 9 の TP を失い、AP の計算に入る再現率の上限は 0.667 で止まります。

🧮 IoU 閾値を 0.5 から 0.95 まで動かす — mAP@0.5 と mAP@[0.5:0.95] が大きく違う理由

同じ 10 件の検出でも、TP と認める IoU の閾値を上げると TP は減っていきます。COCO の主指標 mAP@[0.5:0.95] は閾値 0.50, 0.55, …, 0.95 の 10 段階で AP を計算して平均したものです。あわせて、sklearn.metrics.average_precision_score に TP/FP の列を渡して「AP」を計算してしまう、よくある間違いも並べます。

🎯 このコードでやること:IoU 閾値 0.50〜0.95 の 10 段階それぞれで TP 数・再現率の上限・全点補間の AP を計算し、mAP@[0.5:0.95] を出す。同じ TP/FP の列を sklearn の average_precision_score に渡した値と比べる。

📥 入力例 上のブロックと同じ検出 10 件と正解 6 個
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import numpy as np
from sklearn.metrics import average_precision_score

# 上のブロックと同じ仮想の検出 10 件(信頼度, 正解の番号, IoU)と正解 6 個
det = [(0.98, 1, 0.88), (0.95, 2, 0.81), (0.91, 1, 0.62), (0.87, 3, 0.72), (0.80, -1, 0.0),
       (0.74, 4, 0.55), (0.66, 5, 0.47), (0.58, -1, 0.0), (0.45, 6, 0.79), (0.30, 5, 0.53)]
N_GT = 6
scores = np.array([d[0] for d in det])          # すでに信頼度の高い順

def tp_flags(thr):
    used, tp = set(), []
    for s, g, v in det:
        ok = g != -1 and v >= thr - 1e-12 and g not in used
        used |= {g} if ok else set()
        tp.append(int(ok))
    return np.array(tp)

def ap_all_point(tp):
    ctp = np.cumsum(tp)
    prec, rec = ctp / np.arange(1, len(tp) + 1), ctp / N_GT
    r = np.r_[0, rec, 1]; p = np.r_[1, prec, 0]
    p = np.maximum.accumulate(p[::-1])[::-1]      # 右側の最大値で包絡線
    i = np.flatnonzero(r[1:] != r[:-1])
    return float(np.sum((r[i + 1] - r[i]) * p[i + 1]))

aps = []
print(' IoU閾値  TP数  再現率の上限    AP     sklearn の AP')
for t in np.round(np.arange(0.50, 0.96, 0.05), 2):
    tp = tp_flags(t)
    a = ap_all_point(tp); aps.append(a)
    sk = average_precision_score(tp, scores) if tp.sum() else float('nan')
    print(f'  {t:.2f}     {tp.sum()}      {tp.sum() / N_GT:.3f}      {a:.4f}   {sk:.4f}')
print(f'AP@0.5 = {aps[0]:.4f}   AP@0.75 = {aps[5]:.4f}   AP@[0.5:0.95] = {np.mean(aps):.4f}')
📤 実行例(実測) IoU閾値 TP数 再現率の上限 AP sklearn の AP 0.50 6 1.000 0.7694 0.7620 0.55 5 0.833 0.6620 0.7944 0.60 4 0.667 0.5324 0.7986 0.65 4 0.667 0.5324 0.7986 0.70 4 0.667 0.5324 0.7986 0.75 3 0.500 0.3889 0.7778 0.80 2 0.333 0.3333 1.0000 0.85 1 0.167 0.1667 1.0000 0.90 0 0.000 0.0000 nan 0.95 0 0.000 0.0000 nan AP@0.5 = 0.7694 AP@0.75 = 0.3889 AP@[0.5:0.95] = 0.3918

💬 IoU 0.5 では 6 個の正解すべてに TP があり AP 0.7694 ですが、0.55 で順位 10(IoU 0.53)、0.60 で順位 6(0.55)が FP に変わり、0.75 では TP 3 個・AP 0.3889 と半分になります。0.90 以上ではどの検出も届かず AP 0 です。10 段階の平均 mAP@[0.5:0.95] = 0.3918 は AP@0.5 の約半分で、COCO の表で mAP@[0.5:0.95] が mAP@0.5 より大きく低いのは、この「箱の位置の精密さ」まで点数に入れているからです。右端の列が落とし穴で、sklearn の average_precision_score は渡された検出の中だけで再現率を計算するので、一度も検出されなかった正解(FN)を知りません。IoU 0.80 では正解 6 個のうち 2 個しか当たっていない(再現率の上限 0.333)のに 1.0000 を返し、0.75 でも 0.7778 と正しい AP 0.3889 の 2 倍になります。0.5 でも補間をしないため 0.7620 と少しずれます。物体検出の AP は pycocotools や torchmetrics の MeanAveragePrecision など、正解の総数を分母にする実装で計算します。

🧮 件数が合っても検出が合っているとは限らない — 数える用途の落とし穴

果実の数や来店者数のように「検出した箱の数」をそのまま件数として使う用途では、信頼度の下限(これ未満の箱を捨てる値)の選び方で件数が変わります。同じ 10 件の検出で、下限を 4 通りに変えて件数の誤差と中身(TP・FP・FN)を並べます。

🎯 このコードでやること:信頼度の下限を 0.9 / 0.7 / 0.5 / 0.3 に変えたときの検出数・TP・FP・FN・件数の誤差(検出数 − 正解数)・適合率・再現率を表にする(IoU 閾値は 0.5)。

📥 入力例 上のブロックと同じ検出 10 件と正解 6 個
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np

# 上のブロックと同じ仮想の検出 10 件(信頼度, 正解の番号, IoU)と正解 6 個。IoU 閾値は 0.5
det = [(0.98, 1, 0.88), (0.95, 2, 0.81), (0.91, 1, 0.62), (0.87, 3, 0.72), (0.80, -1, 0.0),
       (0.74, 4, 0.55), (0.66, 5, 0.47), (0.58, -1, 0.0), (0.45, 6, 0.79), (0.30, 5, 0.53)]
N_GT = 6

print('信頼度の下限  検出数  TP  FP  FN  件数の誤差  適合率  再現率')
for cut in [0.9, 0.7, 0.5, 0.3]:
    kept = [d for d in det if d[0] >= cut]
    used, tp = set(), 0
    for s, g, v in kept:
        if g != -1 and v >= 0.5 and g not in used:
            used.add(g); tp += 1
    fp, fn = len(kept) - tp, N_GT - tp
    print(f'    {cut:.1f}        {len(kept):>2}    {tp}   {fp}   {fn}    {len(kept) - N_GT:+d}({(len(kept) - N_GT) / N_GT:+.0%})'
          f'   {tp / len(kept):.3f}  {tp / N_GT:.3f}')
📤 実行例(実測) 信頼度の下限 検出数 TP FP FN 件数の誤差 適合率 再現率 0.9 3 2 1 4 -3(-50%) 0.667 0.333 0.7 6 4 2 2 +0(+0%) 0.667 0.667 0.5 8 4 4 2 +2(+33%) 0.500 0.667 0.3 10 6 4 0 +4(+67%) 0.600 1.000

💬 下限 0.7 では検出が 6 件で、正解の 6 個と件数の誤差は 0 です。しかし中身は TP 4・FP 2・FN 2 で、見逃し 2 件と背景・重複の誤検出 2 件が打ち消し合って件数だけが合っている状態です。下限を 0.5 にすると FP が 4 に増えて +33%、0.3 では見逃しは 0 になるものの +67% の過大計上になり、0.9 では −50% の過小計上です。検証データで件数の誤差だけを見て下限を決めると、別の画像では FP と FN の釣り合いが崩れて誤差が一気に出ることがあります。数える用途でも、適合率と再現率を別々に確認し、必要なら「検出数 ÷ 適合率 × 再現率」の形で件数を補正するか、密度推定型の数え方と比べるのが安全です。

🧮 mAP は「クラスの単純平均」 — 少ないクラスの AP が同じ重みで効く

冒頭の手計算で mAP = (0.85 + 0.78 + 0.92 + 0.65) ÷ 4 = 0.80 としたように、mAP はクラスごとの AP を同じ重みで平均します(マクロ平均)。評価セットの正解箱が person 500 個・car 300 個・dog 40 個・cat 20 個だったと仮定して、箱の数で重みづけした平均と比べます。

Step計算値
1. mAP(単純平均)(0.85 + 0.78 + 0.92 + 0.65) ÷ 4 = 3.20 ÷ 40.8000
2. 箱数で重みづけ(0.85×500 + 0.78×300 + 0.92×40 + 0.65×20) ÷ 860 = (425 + 234 + 36.8 + 13) ÷ 860 = 708.8 ÷ 8600.8242
3. cat が 0.30 に下がるとmAP = 2.85 ÷ 4、重みづけ = (425 + 234 + 36.8 + 6) ÷ 860 = 701.8 ÷ 8600.7125 と 0.8160

🎯 このコードでやること:手計算と同じクラス別 AP と、仮定した正解箱の数で、mAP(単純平均)と箱数で重みづけした平均を計算し、cat の AP だけが下がった場合と比べる。

📥 入力例 クラス別 AP: person 0.85, car 0.78, dog 0.92, cat 0.65 正解箱の数(仮定): person 500, car 300, dog 40, cat 20
1
2
3
4
5
6
7
8
9
10
import numpy as np

cls = ['person', 'car', 'dog', 'cat']
ap = np.array([0.85, 0.78, 0.92, 0.65])          # 上の手計算のクラス別 AP
n_gt = np.array([500, 300, 40, 20])              # 仮定: 評価セットの正解箱の数

for label, a in [('元の AP', ap), ('cat だけ 0.30 に下がった', np.array([0.85, 0.78, 0.92, 0.30]))]:
    macro = a.mean()                             # mAP(クラスを同じ重みで平均)
    weighted = (a * n_gt).sum() / n_gt.sum()     # 正解箱の数で重みづけ
    print(f'{label:<16} mAP = {macro:.4f}   箱数で重みづけ = {weighted:.4f}')
📤 実行例(実測) 元の AP mAP = 0.8000 箱数で重みづけ = 0.8242 cat だけ 0.30 に下がった mAP = 0.7125 箱数で重みづけ = 0.8160

💬 mAP 0.8000 と重みづけ 0.8242、cat が下がった場合の 0.7125 と 0.8160 は Step 1〜3 と一致します。正解箱が全体の 2.3%(20 ÷ 860)しかない cat の AP が 0.65 → 0.30 に落ちると、mAP は 0.0875 下がるのに、箱数で重みづけした平均は 0.0082 しか動きません。mAP は「どのクラスも同じくらい大事」という立場の指標なので、珍しいクラスの失敗がはっきり表に出ます。逆に、件数の多いクラスだけが大事な用途では mAP が実感より悪く見えることもあるので、mAP 1 つで判断せず、クラス別 AP の表を一緒に見るのが基本です。

🧪 理解度チェック — 手計算の数値で確かめる

問題解答
Q1. 正解の箱 (0, 0, 100, 100) と予測 (20, 0, 120, 100) の IoU は? mAP@0.75 で TP になるか。重なり 80 × 100 = 8,000、和集合 10,000 + 10,000 − 8,000 = 12,000、IoU = 0.667。0.75 未満なので FP(mAP@0.5 なら TP)。
Q2. 一辺 16 ピクセルの物体に対して、予測が右に 4 ピクセルだけずれた(縦のずれ 0)。IoU は?重なり 12 × 16 = 192、和集合 256 + 256 − 192 = 320、IoU = 0.600。4 ピクセルでも小物体では 0.75 の閾値を満たさない。
Q3. 上の順位表で、順位 3 の検出(g1 に IoU 0.62)が無かったら AP@0.5(全点補間)はいくつか。検出は 9 件になり、適合率は順位順に 1, 1, 1, 0.75, 0.8, 0.667, 0.571, 0.625, 0.667。包絡線は再現率 1/6〜3/6 で 1.0、4/6 で 0.8、5/6 と 6/6 で 0.667。AP = (1 + 1 + 1 + 0.8 + 0.667 + 0.667) ÷ 6 = 0.856。重複を 1 本消すだけで 0.769 から上がるので、NMS の τ が AP に効くことが分かる。
Q4. NMS の例で τ = 0.15 にすると、残る箱は?a を残すと b(0.782)が消え、c(0.111)は 0.15 以下なので残る。c を残すと d(0.822)が消える。残るのは a, c, e で τ = 0.5 と同じ。τ = 0.1 との違いは、a と c の IoU 0.111 を境にしている。
Q5. 「AP@0.5 = 0.77、AP@[0.5:0.95] = 0.39」のモデルについて言えることは?物体の有無と大まかな位置はよく当てているが、箱の位置は正解から少しずれていることが多い(IoU 0.75 を超える TP が半分しかない)。箱の精密さが要る用途(計測・ロボットの把持など)では位置回帰の改善が必要。
Q6. 上の mAP の重みづけの例で、dog(正解箱 40 個)の AP が 0.92 → 0.50 に下がったら、mAP と箱数で重みづけした平均はいくつになるか。mAP = (0.85 + 0.78 + 0.50 + 0.65) ÷ 4 = 2.78 ÷ 4 = 0.695。重みづけ = (425 + 234 + 0.50×40 + 13) ÷ 860 = 692 ÷ 860 = 0.805。mAP は 0.105 下がるが、重みづけは 0.020 しか下がらない。
Q7. 数える用途の例で、信頼度の下限 0.7 は件数の誤差 0 だった。この下限を「最適」と言ってよいか。言えない。TP 4・FP 2・FN 2 で、見逃しと誤検出が打ち消し合っているだけ。画像の条件が変わって FP と FN の釣り合いが崩れれば誤差が出るので、適合率 0.667・再現率 0.667 を別々に報告し、別の検証データでも件数の誤差を確かめる。

🐍 Python 実装例

最小コードで動かしてみる例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from ultralytics import YOLO

# 事前学習済み YOLOv8 で物体検出
model = YOLO('yolov8n.pt')

# 画像で推論
results = model('image.jpg')

# 検出結果(バウンディングボックス + クラス + 信頼度)
for r in results:
    print(r.boxes.xyxy)  # 座標
    print(r.boxes.cls)   # クラスID
    print(r.boxes.conf)  # 信頼度

⚠️ よくある落とし穴

❌ 小さい物体の検出
高解像度画像での小物体は見逃しやすい。 マルチスケール検出や FPN を使う。
❌ クラス不均衡
「人」が大量、 「飛行機」が少しのデータだと学習が偏る。 Focal Loss や重み付け。
❌ アノテーションの質
境界ボックスの定義が人間でも揺れる。 アノテーションガイドラインを厳密に。
❌ 照明・角度変化
学習データと違う環境では性能が落ちる。 データ増強で頑健化。
❌ リアルタイム性
車両等の用途では遅延が致命的。 速度 vs 精度のバランスを慎重に。

⚠️ 落とし穴 - 拡張版

上記の基本的な落とし穴に加え、 現場で実際に遭遇する 10 個の追加注意点を解説。

  1. train/val リーク: 同じシーンの連続フレームを train と val に分けると擬似的に val 精度が高くなる。 シーン単位で分割すべし。
  2. 長尾分布の罠: COCO でも少数の頻出クラス(特に person)にインスタンスが大きく偏る。 LVIS では極端な long-tail のため class-balanced sampling 必須。
  3. アノテーション仕様の揺れ: 「自動車」を「車体のみ」とするか「窓・ミラー含む」とするかで box が変わる。 ガイドライン文書化が必須。
  4. 重複物体カウントエラー: 群衆検出で 1 人を 2 つに数える / 2 人を 1 つに数えるエラー。 Soft-NMS や Crowd-NMS で改善。
  5. 動的シーンへの過適合: 学習データが特定の街路パターンに偏ると未知シーンで一気に精度低下。 ドメイン多様性を確保。
  6. ラベルノイズ: アノテーターのミスで 5-10% のラベルが誤っているのは珍しくない。 Label cleaning, co-teaching 等で対応。
  7. 計算リソース過大評価: 「YOLO は 200 FPS」とあるが入力サイズ・バッチ・前処理を含めると実 30 FPS のことも。 自前環境での測定必須。
  8. 動画の時間整合性問題: 単フレーム検出を動画に適用すると、 同じ物体の box が小刻みにブレる。 時間平滑化 / トラッキング統合で対策。
  9. カラー空間問題: BGR / RGB の取り違えで精度激減。 OpenCV と PIL の違いに注意。
  10. 適切なライセンス確認: YOLOv5/v8 は GPL-3.0、 商用利用には別途ライセンス購入が必要。 プロジェクト初期に確認。

🗺 概念マップ:物体検出を取り巻く知識マップ

物体検出を中心にした関連概念のツリーマップ:

物体検出 (Object Detection)
├── 入力
│ ├── 静止画 (RGB / RGB-D / グレースケール / マルチスペクトル)
│ ├── 動画 (連続フレーム)
│ └── 点群 (LiDAR / ステレオ / RGB-D 深度)
├── アーキテクチャ
│ ├── 2 段階 (Faster R-CNN, Mask R-CNN, Cascade R-CNN)
│ ├── 1 段階 anchor-based (SSD, RetinaNet, YOLO v1-v5)
│ ├── 1 段階 anchor-free (FCOS, CenterNet, YOLOX, YOLOv8)
│ ├── Transformer (DETR, Deformable DETR, DINO, Co-DETR)
│ └── Open-vocab (OWL-ViT, Grounding DINO, GLIP)
├── 構成要素
│ ├── Backbone (ResNet, EfficientNet, Swin, ViT)
│ ├── Neck (FPN, PAN, BiFPN)
│ ├── Head (classification + regression)
│ └── Post-processing (NMS, Soft-NMS, score thresholding)
├── 学習
│ ├── 損失 (CE, Focal, Smooth L1, GIoU, CIoU)
│ ├── 対応付け (Max-IoU, ATSS, OTA, TAL)
│ ├── データ拡張 (Mosaic, MixUp, Copy-Paste, RandAugment)
│ └── 事前学習 (COCO pretrain, self-supervised)
├── 評価
│ ├── IoU / GIoU / DIoU / CIoU
│ ├── Precision / Recall / F1
│ ├── AP (11-point / 101-point / continuous)
│ └── mAP (@0.5, @0.75, @[0.5:0.95], AP_S/M/L)
├── デプロイ
│ ├── クラウド (GPU/TPU)
│ ├── エッジ (Jetson, Coral, Hailo)
│ ├── モバイル (CoreML, TFLite, ONNX Runtime Mobile)
│ └── 最適化 (TensorRT, INT8 量子化, pruning, distillation)
└── 応用
├── 自動運転 (人・車・標識・信号)
├── 監視 (侵入・異常行動)
├── 医療 (病変・組織)
├── 小売 (商品・客)
├── 農業 (果実・雑草)
├── スポーツ (選手・ボール)
├── 製造 (欠陥・部品)
└── 環境 (野生動物・植生)

🎯 まとめ

物体検出(Object Detection)は CV の中核タスクであり、 画像 1 枚から複数の (矩形, クラス, 信頼度) を出力する。 評価は IoU と mAP が中心で、 代表モデルは Faster R-CNN(2 段階)、 YOLO(1 段階)、 DETR(Transformer)の 3 系統。 予測ボックスと正解ボックスの IoU が 0.5 を超えれば正解 (TP) と判定され、 位置ずれが大きくなるほど mAP は急激に低下する。 実応用では精度と速度のトレードオフ、 アノテーションコスト、 ドメインギャップに注意。 産業導入では自動運転・医療・小売・農業・スポーツなど幅広い分野で実用化されている。

📑 クイックリファレンス(チートシート)

現場で素早く参照できる物体検出チートシート:

状況推奨モデル理由
リアルタイム必須 (60+ FPS)YOLOv8n/s, YOLOv10n軽量・高速
精度最優先DINO-Swin-L, Co-DETRCOCO mAP 60+
バランス重視YOLOv8m/l, Faster R-CNN-FPN速度・精度両立
少データ (1 クラス<100 枚)転移学習 + 強力なデータ拡張pre-trained 重要
小物体多数FPN/PAN 付きモデル, 高解像度入力multi-scale が鍵
クラス不均衡Focal Loss + class-balanced samplinglong-tail 対策
未知クラス検出Grounding DINO, OWL-ViTopen-vocab
エッジ展開YOLOv8n + INT8 量子化省メモリ・低遅延
医療画像Faster R-CNN + Focal Lossrecall 重視
産業欠陥検査独自学習 YOLOv5 + ハードネガ採掘false alarm 削減

▶ よく使うコマンド・スニペット

# YOLOv8 ワンライナー推論
yolo detect predict model=yolov8n.pt source='path/to/image.jpg'

# YOLOv8 学習
yolo detect train data=dataset.yaml model=yolov8n.pt epochs=100 imgsz=640

# YOLOv8 検証
yolo detect val model=runs/detect/train/weights/best.pt data=dataset.yaml

# YOLOv8 → ONNX 変換
yolo export model=best.pt format=onnx

# TensorRT 変換
trtexec --onnx=best.onnx --saveEngine=best.engine --fp16

# Detectron2 (Faster R-CNN) 推論例
python demo/demo.py --config-file configs/COCO-Detection/faster_rcnn_R_50_FPN_3x.yaml \\
  --input image.jpg --output output.jpg \\
  --opts MODEL.WEIGHTS detectron2://COCO-Detection/faster_rcnn_R_50_FPN_3x/137849458/model_final_280758.pkl
object detection セマンティックセグメンテーシ インスタンスセグメンテーショ キーポイント検出 3D 物体検出 Tracking-by-De Open-vocabular

🔗 隣接手法への橋渡し

「物体検出」は単独で完結せず、 前後の手法と組み合わさって価値が発揮される。 入力データの準備 (上流)・同目的の代替手法との比較 (並列)・結果の活用 (下流) という 3 軸で隣接領域を整理する。

この上流・並列・下流の対応を地図化することで、 「物体検出」を中核に据えた分析パイプライン (データ準備 → 手法選択 → 結果の検証と展開) の全体像が見えてくる。

🌳 手法選択フロー

「物体検出」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。

  1. 位置まで要るのか、 有無だけでよいか
    「写っているか」だけなら画像分類で足りる。 「どこに、 いくつ」が要るときに初めて物体検出を使う。 検出は学習データの作成コストが桁違いに高い。
  2. アノテーションを用意できるか
    矩形を 1 つずつ描く作業が要る。 数千枚なら数百時間規模。 用意できないなら、 学習済みモデルで検出できる一般物体に問題を寄せられないか検討する。
  3. 速さと精度のどちらが要るか
    映像をリアルタイムで処理するなら 1 段階検出(YOLO 系)。 精度を優先し 1 枚ずつ処理してよいなら 2 段階検出。
  4. 評価をどう定義したか
    検出は「どれくらい重なれば正解か」(IoU の閾値)で数値が大きく変わる。 閾値を明記しないと mAP は比較できない。

物体検出は分類より一段コストが高い。 「位置が本当に必要か」を最初に確かめると、 分類で済んで工数が数分の一になることがある。