現實中的視覺任務,往往遠比我們前幾天所做的圖像分類還複雜,例如物件偵測:不只要知道圖片有一隻貓,還要知道貓在哪裡。物件偵測的目標是同時在一張圖片完成兩件事
定位:找出所有物體的所在位置,並用一個框框標起來。
分類:判斷框框內的物體是什麼。
如何將我們已經非常擅長的 CNN 分類器,應用到偵測任務上呢?一個最直觀的想法是設計不同尺寸、不同長寬比的滑動窗口,然後讓這些窗口在整張圖片上,從左到右、從上到下地滑動。每滑動到一個位置,我們就把窗口內的圖像裁切下來,送入我們的 CNN 分類器進行判斷。
理論上這方法可行,實際上因為計算成本過大,一張圖要進行成千上萬次前向傳播,是不可能接受的。因此 2014 年的 R-CNN 提出了一個方法:先找出可能包含物體的地方,再只對這些地方應用 CNN,這個方法被稱為獲取候選區域 (region proposal)。 R-CNN 使用了一種名為選擇性搜索 (selective search) 的傳統演算法。選擇性搜索會基於顏色、紋理、尺寸等低階特徵,快速地在圖片中合併相似的區域,從而生成大約 2000 個高品質的候選框。
完整流程如下
獲取候選區域:對輸入圖片,使用「選擇性搜索」演算法,生成約 2000 個候選的物體邊界框 (Region of Interest, RoI)。
特徵提取:將這 2000 個候選框,不論其原始尺寸和長寬比,全部扭曲成一個固定的大小(例如 227×227),然後逐一送入一個預訓練好的 CNN 模型(例如 AlexNet)中,進行前向傳播,提取出特徵向量。
分類:將每個候選框的特徵向量,送入一組為每個類別專門訓練的線性 SVM 分類器中,判斷該候選框屬於哪個類別。
邊界框回歸 (bounding box regression):對於被 SVM 判斷為包含物體的候選框,再使用一個線性回歸模型,對其位置和尺寸進行微調,使其能更精準地框住物體。
雖然 R-CNN 精度非常高,但同時卻速度極慢又訓練複雜。由於需要對 2000 個候選框獨立、重複地進行 CNN 的前向傳播,原本人馬針對此改良出 Fast R-CNN,流程如下
先對整張圖做卷積:不再是對每個候選框單獨做 CNN,而是先將整張原始圖片,只做一次 CNN 前向傳播,得到一個整體的特徵圖 (feature map)。
RoI 池化 (RoI Pooling):將選擇性搜索生成的候選框,直接映射到這個整體的特徵圖上。然後,使用「RoI 池化層」這個巧妙的結構,從特徵圖上對應的區域中,提取出一個固定大小的特徵向量。
統一的頭部:最後,將這個特徵向量送入一個統一的「頭部網路」,這個頭部網路會並行地輸出兩個結果:物體的分類(使用 Softmax)和邊界框的回歸。
Fast R-CNN 將分類器從 SVM 換成了 Softmax,並將邊界框回歸器也整合進了神經網路,使得整個偵測流程(除了候選區域)可以進行端到端的聯合訓練。其訓練速度比 R-CNN 快了近 9 倍,測試速度快了近 200 倍。
雖然 Fast R-CNN 已經大幅度改進了 R-CNN 的速度,但是候選區域仍然是獨立運行於 CPU 上的選擇性搜索演算法完成的。因此 Faster R-CNN 改成使用一個神經網路來取代這個傳統演算法,引入候選區域網路 (Region Proposal Network, RPN) 這個結構。
RPN 是一個小型的、全卷積的網路,它直接被接在 Fast R-CNN 的主幹 CNN 網路的最後一個共享卷積層之上。
RPN 會在整張特徵圖上滑動,並在每個位置,預測 K 個不同尺寸和長寬比的錨框 (anchor boxes) 是否包含物體(前景/背景),並對其位置進行初步的微調。
RPN 輸出的高品質候選框,會直接送入後續的 Fast R-CNN 檢測頭部,進行最終的精確分類和定位。
import torch
import torchvision
import torchvision.transforms as transforms
from PIL import Image, ImageDraw
import requests
from io import BytesIO
import numpy as np
# --- 1. 載入預訓練的 Faster R-CNN 模型 ---
# COCO 數據集是一個大規模的目標偵測、分割數據集
print("正在下載並載入在 COCO 數據集上預訓練的 Faster R-CNN 模型...")
# weights=torchvision.models.detection.FasterRCNN_ResNet50_FPN_Weights.DEFAULT
model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True)
model.eval() # 設為評估模式
print("模型載入完成!")
# --- 2. 準備 COCO 數據集的類別標籤 ---
COCO_INSTANCE_CATEGORY_NAMES = [
'__background__', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus',
'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'N/A', 'stop sign',
'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
'elephant', 'bear', 'zebra', 'giraffe', 'N/A', 'backpack', 'umbrella', 'N/A', 'N/A',
'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket',
'bottle', 'N/A', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl',
'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'N/A', 'dining table',
'N/A', 'N/A', 'toilet', 'N/A', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'N/A', 'book',
'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'
]
# --- 3. 準備輸入圖片 ---
image_url = "https://images.pexels.com/photos/37337/cat-silhouette-cats-silhouette-cat-s-eyes.jpg" # 一隻貓
response = requests.get(image_url)
img_pil = Image.open(BytesIO(response.content)).convert("RGB")
# 將圖片轉換為 Tensor
transform = transforms.Compose([transforms.ToTensor()])
img_tensor = transform(img_pil)
# --- 4. 進行預測 ---
with torch.no_grad():
# 模型需要一個 list of tensors 作為輸入
prediction = model([img_tensor])
# prediction 是一個 list,其中包含一個 dict,包含了 'boxes', 'labels', 'scores'
# boxes: [N, 4] 的張量,N 是偵測到的物體數量,4 代表 (x1, y1, x2, y2)
# labels: [N] 的張量,每個物體的類別索引
# scores: [N] 的張量,每個物體的置信度分數
# --- 5. 視覺化結果 ---
def draw_predictions(image, prediction, threshold=0.7):
draw = ImageDraw.Draw(image)
boxes = prediction[0]['boxes']
labels = prediction[0]['labels']
scores = prediction[0]['scores']
# 檢查是否有符合閾值的檢測結果
valid_detections = sum(1 for score in scores if score > threshold)
print(f"找到 {valid_detections} 個信心度高於 {threshold} 的檢測結果")
for i in range(len(scores)):
if scores[i] > threshold:
box = boxes[i].numpy().astype(int)
label_id = labels[i].item()
score = scores[i].item()
# 獲取類別名稱
class_name = COCO_INSTANCE_CATEGORY_NAMES[label_id]
# 繪製邊界框
draw.rectangle([(box[0], box[1]), (box[2], box[3])], outline="red", width=3)
# 計算文字位置,確保不超出圖片範圍
text_y = max(10, box[1] - 10) # 避免文字超出頂部
# 繪製標籤和分數,使用較大的字體
text = f"{class_name}: {score:.2f}"
# 設定字體大小(需要 PIL 字體)
try:
from PIL import ImageFont
font = ImageFont.truetype("arial.ttf", 80) # Windows
except:
try:
font = ImageFont.truetype("/System/Library/Fonts/Arial.ttf", 80) # macOS
except:
font = ImageFont.load_default() # 預設字體
# 繪製文字背景(讓文字更清楚)
text_bbox = draw.textbbox((box[0], text_y), text, font=font)
draw.rectangle(text_bbox, fill="red")
# 繪製白色文字
draw.text((box[0], text_y), text, fill="white", font=font)
return image
result_image = draw_predictions(img_pil, prediction, threshold=0.8)
result_image.show()
結果