請撰寫一個 Python 程式,能夠辨識圖片中的紅綠燈,偵測紅、黃、綠三種燈號的狀態,並在畫面上標示出當前亮起的燈號(例如:「紅燈」、「綠燈」等)。本任務的核心在於顏色分割與形狀分析。
人類辨識紅綠燈的方式:
1. 顏色識別:看到「紅色」的圓形燈亮起 → 判斷為紅燈
2. 形狀識別:確認是圓形的燈號(而非方形招牌)
3. 位置判斷:通常在特定高度、特定位置
電腦視覺的做法與人類相似,但更加結構化:
核心問題:紅綠燈是發光體,不是普通物體!
普通物體(反射光) 發光體(自發光)
┌─────────┐ ┌─────────┐
│ 紅色 │ 反射 │ 紅燈 │ 發光
│ 蘋果 │ ←── 光源 │ ●○○ │ 自己發光
└─────────┘ └─────────┘
特性: 特性:
• 依賴環境光 • 不依賴環境光
• 陰影影響大 • 亮度穩定
• 顏色受光線影響 • 顏色一致性高
紅綠燈的特殊挑戰:
BGR 值: BGR 值:
紅燈: (50, 50, 200) 紅燈: (0, 0, 255)
背景: (180,180,180) 背景: (20, 20, 20)
難以用固定範圍偵測!
```
光暈效應(Bloom)
過曝或模糊時:
┌─────────┐
│ ╱●╲ │ ← 紅燈周圍有光暈
│ ╱ ● ╲ │ BGR 值變化劇烈
│ ●●●●● │ 難以定義邊界
└─────────┘
色溫變化
```
不同燈泡/LED:
暖紅 (255, 0, 0) ← 傳統燈泡
冷紅 (255, 30, 50) ← LED 燈
橙紅 (255, 50, 0) ← 老舊燈泡
RGB 值差異大!
```
HSV 的解決方案:
| 問題 | BGR 的困境 | HSV 的優勢 |
|---|---|---|
| 亮度變化 | 所有通道都變 (R↓G↓B↓) | 只有 V 變化,H 和 S 穩定 |
| 光暈效應 | 邊緣 RGB 混亂 | H (色相) 仍保持紅色範圍 |
| 色溫差異 | RGB 值完全不同 | H (色相) 都在紅色範圍內 |
實際數值對比:
# 場景 1:亮紅燈(白天)
bright_red_bgr = (50, 50, 200) # BGR
bright_red_hsv = (0, 180, 200) # HSV → H=0 (紅), S=180, V=200
# 場景 2:暗紅燈(夜晚)
dark_red_bgr = (0, 0, 100) # BGR 差很多!
dark_red_hsv = (0, 255, 100) # HSV → H=0 (紅), S=255, V=100
# H 完全一樣!只有 V 變化
# 場景 3:過曝紅燈(逆光)
bloom_red_bgr = (80, 100, 255) # BGR 差很多!
bloom_red_hsv = (0, 156, 255) # HSV → H=0 (紅), S=156, V=255
# H 還是一樣!
# 用 HSV 可以統一處理:
red_h_range = 0-10 # 色相範圍固定
red_s_range = 100-255 # 只要夠鮮豔
red_v_range = 50-255 # 亮度可以很寬鬆
答案:視情況而定
| 場景類型 | 是否需要 HSV | 原因 |
|---|---|---|
| 發光體 | ✅ 強烈建議 | LED 燈、霓虹燈、螢幕、燈號 |
| 室外物體 | ✅ 建議使用 | 陽光變化大(早中晚、陰晴) |
| 室內物體 | ⚠️ 可選 | 光線穩定時 BGR 也可以 |
| 固定光源 | ⚠️ 可選 | 實驗室、攝影棚等控制環境 |
| 灰階物體 | ❌ 不需要 | 黑白、灰色物體用亮度即可 |
實例分析:
情境 1:偵測紅色蘋果(室外)
┌─────────────────────┐
│ 早上 8 點:陽光斜射 │ → 需要 HSV ✅
│ 中午 12 點:直射強光 │ 光線變化太大
│ 傍晚 6 點:夕陽橙光 │
└─────────────────────┘
情境 2:偵測紅色蘋果(室內)
┌─────────────────────┐
│ 固定 LED 白光燈 │ → BGR 可用 ⚠️
│ 蘋果固定在同一位置 │ 但 HSV 更穩健
│ 攝影機不移動 │
└─────────────────────┘
情境 3:偵測手機螢幕亮點
┌─────────────────────┐
│ 白色背光 LCD │ → 必須用 HSV ✅
│ OLED 自發光像素 │ 發光體特性
│ 亮度變化極大 │
└─────────────────────┘
情境 4:工業產品顏色檢測
┌─────────────────────┐
│ 固定光箱 │ → BGR 即可 ⚠️
│ 標準化光源 │ 環境完全控制
│ 塑膠產品(不反光) │
└─────────────────────┘
實際測試對比:
import cv2
import numpy as np
# 測試:同一個紅色物體在不同光線下
# === 場景 1:室外紅色交通錐(陽光變化) ===
def test_outdoor_red_cone():
# 早上(偏藍光)
morning_bgr = (40, 30, 180)
# 中午(強烈白光)
noon_bgr = (100, 100, 255)
# 傍晚(偏黃光)
evening_bgr = (60, 80, 200)
# BGR 範圍難以統一
bgr_lower = np.array([30, 20, 150])
bgr_upper = np.array([120, 120, 255]) # 範圍很寬,容易誤判
# 轉換為 HSV
morning_hsv = cv2.cvtColor(np.uint8([[morning_bgr]]), cv2.COLOR_BGR2HSV)[0][0]
noon_hsv = cv2.cvtColor(np.uint8([[noon_bgr]]), cv2.COLOR_BGR2HSV)[0][0]
evening_hsv = cv2.cvtColor(np.uint8([[evening_bgr]]), cv2.COLOR_BGR2HSV)[0][0]
print("室外紅色交通錐:")
print(f"早上 BGR: {morning_bgr} → HSV: {morning_hsv}") # H 值相近!
print(f"中午 BGR: {noon_bgr} → HSV: {noon_hsv}")
print(f"傍晚 BGR: {evening_bgr} → HSV: {evening_hsv}")
# HSV 範圍容易統一
hsv_lower = np.array([0, 100, 100]) # H: 紅色, S: 鮮豔, V: 不太暗
hsv_upper = np.array([10, 255, 255]) # 範圍緊緻,準確度高
return "HSV 優勢明顯 ✅"
# === 場景 2:室內紅色檔案夾(固定光源) ===
def test_indoor_red_folder():
# 室內 LED 白光下的紅色
indoor_bgr = (50, 50, 220)
# BGR 就能穩定偵測
bgr_lower = np.array([40, 40, 200])
bgr_upper = np.array([60, 60, 240])
# 但 HSV 仍然更好(對不同紅色物體容錯性高)
hsv_lower = np.array([0, 150, 150])
hsv_upper = np.array([10, 255, 255])
return "BGR 可用,但 HSV 更穩健 ⚠️"
# === 場景 3:紅色 LED 燈(發光體) ===
def test_red_led():
# LED 燈在不同亮度下
dim_led_bgr = (0, 0, 80) # 暗
bright_led_bgr = (0, 0, 255) # 亮
# BGR 差異大
print("\nRED LED:")
print(f"暗 LED BGR: {dim_led_bgr}")
print(f"亮 LED BGR: {bright_led_bgr}")
# HSV 差異主要在 V
dim_led_hsv = cv2.cvtColor(np.uint8([[dim_led_bgr]]), cv2.COLOR_BGR2HSV)[0][0]
bright_led_hsv = cv2.cvtColor(np.uint8([[bright_led_bgr]]), cv2.COLOR_BGR2HSV)[0][0]
print(f"暗 LED HSV: {dim_led_hsv}") # H 相同!
print(f"亮 LED HSV: {bright_led_hsv}")
return "HSV 必須使用 ✅"
test_outdoor_red_cone()
test_indoor_red_folder()
test_red_led()
結論:
| 情況 | 建議 |
|---|---|
| 發光物體(如紅綠燈) | 必須用 HSV ✅ |
| 室外顏色辨識 | 強烈建議用 HSV ✅ |
| 室內顏色辨識 | 建議用 HSV(更穩健)⚠️ |
| 完全控制環境 | BGR 可用(但 HSV 更好)⚠️ |
| 灰階/形狀偵測 | 不需要 HSV ❌ |
HSV 的三個維度:
HSV 色彩圓柱體模型:
V (明度)
↑
│ ╱╲
│ ╱ ╲ ← 高飽和度(鮮豔)
│ ╱ ╲
│ │ H │ ← H (色相) 圓環:0°=紅, 120°=綠, 240°=藍
│ ╲ ╱
│ ╲ ╱ ← 低飽和度(灰白)
│ ╲╱
└────────→ S (飽和度)
1. H (Hue, 色相) - 顏色種類
色相環:想像彩虹圍成一個圓圈
0° / 180°
紅色
↑
黃 │ 紫
30° │ 150°
╲ │ ╱
╲ │ ╱
綠色 ← ─┼─ → 藍色
60° │ 120°
╱ │ ╲
╱ │ ╲
青色 │ 橙色
90° 30°
OpenCV 中的 H 值範圍:0-180(為了符合 8-bit 儲存)
- 0-10°:紅色(偏橙紅)
- 20-35°:黃色
- 40-80°:綠色
- 100-130°:藍色
- 170-180°:紅色(偏紫紅)
2. S (Saturation, 飽和度) - 顏色鮮豔程度
S = 0 (灰色) S = 128 (中等) S = 255 (鮮豔)
┌─────────┐ ┌─────────┐ ┌─────────┐
│ │ │ │ │ │
│ 灰 │ → │ 淡紅 │ → │ 鮮紅 │
│ │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘
範圍:0-255
- 0 = 完全無色(灰階)
- 255 = 最鮮豔
3. V (Value, 明度) - 亮度
V = 0 (黑色) V = 128 (中等亮度) V = 255 (最亮)
┌─────────┐ ┌─────────┐ ┌─────────┐
│ │ │ │ │ │
│ 黑 │ → │ 暗紅 │ → │ 亮紅 │
│ │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘
範圍:0-255
- 0 = 完全黑暗
- 255 = 最明亮
紅燈的 HSV 定義:
紅色 H: 0-10° (或 170-180°) ← 色相:紅色
高飽和度 S: 150-255 ← 鮮豔的紅
高明度 V: 150-255 ← 亮的紅燈
視覺化:
BGR (0, 0, 255) → HSV (0, 255, 255)
┌──────┐ ┌──────┐
│ 鮮紅 │ 轉換 │ 鮮紅 │
│ 燈號 │ ───→ │ 燈號 │
└──────┘ └──────┘
但 HSV 更容易設定範圍!
黃燈的 HSV 定義:
黃色 H: 20-35°
高飽和度 S: 150-255
高明度 V: 200-255
BGR (0, 255, 255) → HSV (30, 255, 255)
綠燈的 HSV 定義:
綠色 H: 40-80°
中高飽和度 S: 100-255 ← 綠燈可能略淡
高明度 V: 150-255
BGR (0, 255, 0) → HSV (60, 255, 255)
import cv2
import numpy as np
from google.colab.patches import cv2_imshow
# 建立 HSV 色彩空間的視覺化圖表
def visualize_hsv_space():
"""繪製 HSV 色相環"""
img = np.zeros((400, 400, 3), dtype=np.uint8)
# 繪製色相環
for angle in range(0, 360, 1):
h = int(angle / 2) # OpenCV H 範圍是 0-180
color_hsv = np.uint8([[[h, 255, 255]]])
color_bgr = cv2.cvtColor(color_hsv, cv2.COLOR_HSV2BGR)
color = tuple(map(int, color_bgr[0, 0]))
# 計算位置(圓形排列)
x = int(200 + 150 * np.cos(np.radians(angle)))
y = int(200 + 150 * np.sin(np.radians(angle)))
cv2.circle(img, (x, y), 15, color, -1)
# 標註主要顏色
cv2.putText(img, "RED", (350, 210), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(img, "YELLOW", (280, 100), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(img, "GREEN", (50, 130), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(img, "BLUE", (80, 320), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
return img
# 建立飽和度漸變圖
def visualize_saturation():
"""展示飽和度對紅色的影響"""
img = np.zeros((100, 256, 3), dtype=np.uint8)
for s in range(256):
color_hsv = np.uint8([[[0, s, 255]]]) # H=0(紅), V=255(亮)
color_bgr = cv2.cvtColor(color_hsv, cv2.COLOR_HSV2BGR)
img[:, s] = color_bgr[0, 0]
return img
# 建立明度漸變圖
def visualize_value():
"""展示明度對紅色的影響"""
img = np.zeros((100, 256, 3), dtype=np.uint8)
for v in range(256):
color_hsv = np.uint8([[[0, 255, v]]]) # H=0(紅), S=255(鮮豔)
color_bgr = cv2.cvtColor(color_hsv, cv2.COLOR_HSV2BGR)
img[:, v] = color_bgr[0, 0]
return img
# 顯示視覺化結果
print("HSV 色相環:")
cv2_imshow(visualize_hsv_space())
print("\n飽和度漸變 (S: 0→255, 紅色):")
cv2_imshow(visualize_saturation())
print("\n明度漸變 (V: 0→255, 紅色):")
cv2_imshow(visualize_value())
核心函數:cv2.inRange(hsv, lower_bound, upper_bound)
原理:
- 檢查每個像素的 HSV 值是否在指定範圍內
- 如果在範圍內 → 標記為白色 (255)
- 如果不在範圍內 → 標記為黑色 (0)
- 輸出:二值化遮罩 (Binary Mask)
視覺化範例:
原始圖片 (BGR) HSV 圖片 紅色遮罩 (Mask)
┌─────────┐ ┌─────────┐ ┌─────────┐
│ [灰][紅]│ │ [X][H=5]│ │ [0][255]│
│ [綠][黃]│ → │[H=60][30]│ → │ [0][0] │
└─────────┘ └─────────┘ └─────────┘
形態學處理是一種基於形狀的影像處理技術,就像用「橡皮擦」和「畫筆」來修改圖片中物體的形狀。
想像你在黑板上畫了一個圓圈,但畫得不夠完美:
- 圓圈內有些小缺口 → 用「粉筆填補」(膨脹)
- 圓圈邊緣有些毛邊 → 用「板擦擦掉」(侵蝕)
- 組合使用 → 得到平滑完美的圓圈
在詳細說明之前,先了解形態學處理的核心元件:
Kernel 是形態學操作的「工具」,就像一個「刷子」或「印章」。
基本定義:
kernel = np.ones((5, 5), np.uint8)
視覺化 Kernel:
5×5 Kernel 矩陣:
[[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]]
表示為:
●●●●●
●●●●●
●●☆●● ← ☆ 是中心點 (2, 2)
●●●●●
●●●●●
Kernel 參數說明:
| 參數 | 說明 | 範例 |
|---|---|---|
shape |
Kernel 的大小 | (3, 3), (5, 5), (7, 7) |
dtype |
資料型別 | np.uint8 (0-255) |
value |
矩陣中的值 | 1 = 有效, 0 = 無效 |
不同 Kernel 形狀:
# 1. 方形 kernel (最常用)
kernel_square = np.ones((5, 5), np.uint8)
# 2. 圓形 kernel (更平滑)
kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# 3. 十字形 kernel
kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
Kernel 大小的影響:
| Kernel 大小 | 處理強度 | 適用場景 | 副作用 |
|---|---|---|---|
| 3×3 | 輕微 | 細微雜訊 | 幾乎沒有 |
| 5×5 | 中等 ✓ | 一般使用 | 輕微圓角 |
| 7×7 | 強烈 | 嚴重雜訊 | 明顯圓角 |
| 9×9 | 非常強烈 | 極端情況 | 物體變形 |
形態學處理有四個核心操作,分為兩個基礎操作和兩個組合操作:
基礎操作 (Building Blocks):
| 操作 | 英文 | 效果 | 比喻 | 函數 |
|---|---|---|---|---|
| 膨脹 | Dilation | 白色區域向外擴張 | 「長胖」 | cv2.dilate() |
| 侵蝕 | Erosion | 白色區域向內收縮 | 「減肥」 | cv2.erode() |
組合操作 (Compound Operations):
| 操作 | 英文 | 組合方式 | 用途 | 函數 |
|---|---|---|---|---|
| 開運算 | Opening | 先侵蝕 → 再膨脹 | 去除外部雜點 | cv2.morphologyEx(..., cv2.MORPH_OPEN, ...) |
| 閉運算 | Closing | 先膨脹 → 再侵蝕 | 填補內部黑洞 | cv2.morphologyEx(..., cv2.MORPH_CLOSE, ...) |
操作流程圖:
1. 基礎操作函數:
# 膨脹 (Dilation)
cv2.dilate(
src, # 輸入圖片 (二值化遮罩)
kernel, # 結構元素
iterations=1 # 迭代次數 (預設 1)
)
# 侵蝕 (Erosion)
cv2.erode(
src, # 輸入圖片 (二值化遮罩)
kernel, # 結構元素
iterations=1 # 迭代次數 (預設 1)
)
2. 組合操作函數 (推薦使用):
# 通用形態學函數
cv2.morphologyEx(
src, # 輸入圖片 (二值化遮罩)
op, # 操作類型:
# cv2.MORPH_OPEN → 開運算
# cv2.MORPH_CLOSE → 閉運算
# cv2.MORPH_DILATE → 膨脹
# cv2.MORPH_ERODE → 侵蝕
kernel # 結構元素
)
在紅綠燈辨識中,我們使用「先閉後開」的策略:
# 第 1 步:建立 Kernel
kernel = np.ones((5, 5), np.uint8)
# 第 2 步:閉運算 - 填補燈號內部的小黑洞
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
# 第 3 步:開運算 - 去除背景的小雜點
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
為什麼是「先閉後開」?
原始問題 閉運算解決 開運算解決
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ●●● ● ● │ │ ●●●● ● ● │ │ ●●●● │
│ ●●○ ● │ → │ ●●●● ● │ → │ ●●●● │
│ ●●● │ │ ●●●● │ │ ●●●● │
│ ● ● │ │ ● ● │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
問題1:燈內有黑洞 (○) ✓ 黑洞被填補 ✓ 保持完整
問題2:背景有雜點 (●) ✗ 雜點還在 ✓ 雜點去除
| 步驟 | 操作 | 目標 | 解決的問題 |
|---|---|---|---|
| 第 1 步 | 閉運算 | 完善燈號本體 | 填補燈內反光造成的小黑洞 |
| 第 2 步 | 開運算 | 清理背景雜訊 | 去除招牌、車燈等紅色雜點 |
快速參考表:
| 想要達成的效果 | 使用的操作 | 程式碼 |
|---|---|---|
| 填補物體內的小黑洞 | 閉運算 | cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) |
| 去除背景的小白點 | 開運算 | cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) |
| 讓物體變大/連接斷裂 | 膨脹 | cv2.dilate(mask, kernel) |
| 讓物體變小/分離黏連 | 侵蝕 | cv2.erode(mask, kernel) |
現在我們詳細說明每個操作的原理與應用。
原理:讓白色區域向外擴張
工作方式:
- Kernel 在遮罩上滑動
- 如果 Kernel 覆蓋區域內有任何白色像素
- 則中心點標記為白色
效果:
- ✅ 填補小的黑色缺口
- ✅ 連接斷裂的線條
- ⚠️ 物體變大
視覺化示例:
原始圖 膨脹後
●●○●● ●●●●●
●○○○● → ●●●●●
●●○●● ●●●●●
(黑色的小缺口被填補了)
程式碼:
kernel = np.ones((5, 5), np.uint8)
dilated = cv2.dilate(mask, kernel, iterations=1)
應用場景:
- 連接斷裂的文字或線條
- 增強物體的邊緣
- 填補物體內部的小洞
原理:讓白色區域向內收縮
工作方式:
- Kernel 在遮罩上滑動
- 如果 Kernel 覆蓋區域內全部是白色像素
- 則中心點標記為白色
- 否則標記為黑色
效果:
- ✅ 去除小的白色雜點
- ✅ 分離黏連的物體
- ⚠️ 物體變小
視覺化示例:
原始圖 侵蝕後
●●●●● ●●○●●
●●●●● → ●○○○●
●●●●● ●●○●●
(白色區域收縮了)
程式碼:
kernel = np.ones((5, 5), np.uint8)
eroded = cv2.erode(mask, kernel, iterations=1)
應用場景:
- 去除細小的雜訊點
- 分離相連的物體
- 細化物體邊緣
組合方式:
# 等價於:
mask_eroded = cv2.erode(mask, kernel)
mask_opened = cv2.dilate(mask_eroded, kernel)
# 簡化寫法 (推薦):
mask_opened = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
用途:去除小的白色雜點(噪點)
視覺化示例:
原始圖 開運算後
████████ ● ████████
████████ → ████████
████████ ● ████████
(小白點被去除,主要物體保持原樣)
特點:
- ✅ 去除雜訊
- ✅ 主要物體大小幾乎不變
- ✅ 平滑邊緣
程式碼:
kernel = np.ones((5, 5), np.uint8)
opened = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
詳細運作過程:
假設背景中有紅色的小雜點(可能是招牌、車燈):
步驟 1:原始 mask
┌─────────────────┐
│ │
│ ●●●● ● │ ← 主要紅燈 + 背景小雜點
│ ●●●● ● │
│ ●●●● │
│ ●●●● ● │
│ │
└─────────────────┘
步驟 1:侵蝕 (Erosion)
侵蝕後 (小雜點消失)
┌─────────────────┐
│ │
│ ●●● │ ← 小雜點被去除!
│ ●● │ 主要物體變小
│ ●●● │
│ ●● │
│ │
└─────────────────┘
步驟 2:膨脹 (Dilation)
膨脹後 (恢復大小,雜點已去除)
┌─────────────────┐
│ │
│ ●●●● │ ← 乾淨的紅燈,沒有背景雜點!
│ ●●●● │
│ ●●●● │
│ ●●●● │
│ │
└─────────────────┘
組合方式:
# 等價於:
mask_dilated = cv2.dilate(mask, kernel)
mask_closed = cv2.erode(mask_dilated, kernel)
# 簡化寫法 (推薦):
mask_closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
用途:填補物體內的小黑洞
視覺化示例:
原始圖 閉運算後
████ ████ █████████
████ ████ → █████████
████ ████ █████████
(中間的小缺口被填補)
特點:
- ✅ 填補缺口
- ✅ 連接鄰近物體
- ✅ 主要物體大小幾乎不變
程式碼:
kernel = np.ones((5, 5), np.uint8)
closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
詳細運作過程:
假設我們有一個紅燈的遮罩,但內部有小黑洞:
步驟 1:原始 mask (紅燈偵測結果)
┌─────────┐
│ │
│ ●●●● │
│ ●●○● │ ← 紅燈內有一個小黑洞 (可能是反光造成)
│ ●●●● │
│ ●●●● │
│ │
└─────────┘
步驟 1:膨脹 (Dilation)
膨脹後 (白色區域變大)
┌─────────┐
│ │
│ ●●●●●● │
│ ●●●●●● │ ← 小黑洞被填滿了!
│ ●●●●●● │
│ ●●●●●● │
│ ●●●●●● │
└─────────┘
步驟 2:侵蝕 (Erosion)
侵蝕後 (恢復大小,但小洞已填補)
┌─────────┐
│ │
│ ●●●● │
│ ●●●● │ ← 乾淨的紅燈區域,沒有黑洞了!
│ ●●●● │
│ ●●●● │
│ │
└─────────┘
為什麼先做閉運算?
在紅綠燈辨識中,常見問題:
1. 燈號內部有反光 → 造成小黑洞
2. 燈號邊緣不完整 → 造成小缺口
3. 鏡頭畫質不佳 → 造成雜訊
閉運算解決這些問題:填補紅燈內部的小缺陷,讓燈號更完整。
讓我們逐行解析這段關鍵程式碼:
# (可選) 形態學操作:先閉合再開,幫助去除小雜訊點
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
kernel = np.ones((5, 5), np.uint8)
Kernel(結構元素) 就像一個「刷子」或「印章」,定義了形態學操作的影響範圍和形狀。
視覺化 Kernel:
kernel = np.ones((5, 5), np.uint8)
# 實際上建立了這樣的矩陣:
[[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]]
# 視覺化表示:
●●●●●
●●●●●
●●☆●● ← ☆ 是中心點 (2, 2)
●●●●●
●●●●●
Kernel 的工作原理:
想像 kernel 是一個「滑動窗口」,在遮罩圖上移動:
遮罩圖 (Mask) Kernel 掃描過程
┌─────────────┐
│○○●●●○○○○│ 第1步:Kernel 在左上角
│○●●●●●○○○│ ┌─────┐
│●●●●●●●○○│ → │●●●●●│
│●●○●●●●○○│ │●●●●●│ 檢查這 5×5 區域
│●●●●●●●○○│ │●●●●●│
│○○○●●●○○○│ │●●●●●│
└─────────────┘ │●●●●●│
└─────┘
第2步:Kernel 向右移動一格
┌─────┐
│●●●●●│
│●●●●●│
│●●●●●│
│●●●●●│
│●●●●●│
└─────┘
... 依此類推掃描整張圖
np.ones() 的意義:
np.ones((5, 5), np.uint8)
np.ones():建立全為 1 的矩陣(5, 5):5 列 × 5 行 = 25 個元素np.uint8:資料型別為 8-bit 無符號整數 (0-255)為什麼用全 1 矩陣?
- 1 表示「有效」,這個位置會參與運算
- 0 表示「無效」,這個位置會被忽略
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
函數參數說明:
cv2.morphologyEx(
mask, # 輸入圖片(二值化遮罩)
cv2.MORPH_CLOSE, # 操作類型:閉運算
kernel # 結構元素
)
閉運算做什麼?
閉運算 = 先膨脹,再侵蝕
# 等價於:
mask_dilated = cv2.dilate(mask, kernel)
mask_closed = cv2.erode(mask_dilated, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
開運算做什麼?
開運算 = 先侵蝕,再膨脹
# 等價於:
mask_eroded = cv2.erode(mask, kernel)
mask_opened = cv2.dilate(mask_eroded, kernel)
原始 mask (問題多多) 閉運算後 開運算後 (完美)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ ●●● ● ● │ │ ●●●● ● ● │ │ ●●●● │
│ ●●○ ● │ → │ ●●●● ● │ → │ ●●●● │
│ ●●● │ │ ●●●● │ │ ●●●● │
│ ● ● │ │ ● ● │ │ │
│ │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
問題: 閉運算效果: 開運算效果:
1. 紅燈內有黑洞 (○) 1. 填補黑洞 ✓ 1. 保持完整 ✓
2. 背景有小雜點 (●) 2. 雜點還在 ✗ 2. 雜點去除 ✓
實驗:不同 Kernel 大小的效果
# 小 kernel (3×3) - 處理細微雜訊
kernel_small = np.ones((3, 3), np.uint8)
mask_1 = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_small)
mask_1 = cv2.morphologyEx(mask_1, cv2.MORPH_OPEN, kernel_small)
# 中 kernel (5×5) - 一般使用 ✓
kernel_medium = np.ones((5, 5), np.uint8)
mask_2 = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_medium)
mask_2 = cv2.morphologyEx(mask_2, cv2.MORPH_OPEN, kernel_medium)
# 大 kernel (9×9) - 處理嚴重雜訊
kernel_large = np.ones((9, 9), np.uint8)
mask_3 = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_large)
mask_3 = cv2.morphologyEx(mask_3, cv2.MORPH_OPEN, kernel_large)
效果對比表:
| Kernel 大小 | 填補黑洞能力 | 去除雜點能力 | 副作用 |
|---|---|---|---|
| 3×3 | ★☆☆ | ★☆☆ | 幾乎沒有 |
| 5×5 | ★★★ | ★★★ | 輕微圓角 ✓ 推薦 |
| 7×7 | ★★★★ | ★★★★ | 明顯圓角 |
| 9×9 | ★★★★★ | ★★★★★ | 物體變形 |
import cv2
import numpy as np
from google.colab.patches import cv2_imshow
# 建立測試遮罩 (模擬偵測到的紅燈區域,有雜訊)
mask = np.zeros((200, 200), dtype=np.uint8)
cv2.circle(mask, (100, 100), 40, 255, -1) # 主要紅燈
cv2.circle(mask, (95, 95), 5, 0, -1) # 內部小黑洞 (反光)
cv2.circle(mask, (150, 80), 8, 255, -1) # 背景小雜點 (招牌)
print("原始遮罩 (有問題):")
cv2_imshow(mask)
# 測試不同 kernel 大小
kernel_sizes = [3, 5, 7]
for size in kernel_sizes:
kernel = np.ones((size, size), np.uint8)
# 先閉運算 → 再開運算
result = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
result = cv2.morphologyEx(result, cv2.MORPH_OPEN, kernel)
print(f"\nKernel {size}×{size} 處理後:")
cv2_imshow(result)
在步驟 5 的特徵篩選中,我們使用圓形度 (Circularity) 來判斷偵測到的物體是否為圓形。
圓形度公式:
圓形度 = 4π × 面積 / 周長²
讓我們理解這個公式的數學推導:
核心概念:在所有相同周長的形狀中,圓形的面積最大。
數學推導:
1. 圓形的情況
已知:
- 半徑 = r
- 面積 Area = πr²
- 周長 Perimeter = 2πr
代入公式:
圓形度 = 4π × Area / Perimeter²
= 4π × (πr²) / (2πr)²
= 4π × πr² / 4π²r²
= 4π²r² / 4π²r²
= 1.0
結論:完美的圓形,圓形度 = 1.0 ✓
2. 正方形的情況
已知:
- 邊長 = a
- 面積 Area = a²
- 周長 Perimeter = 4a
代入公式:
圓形度 = 4π × Area / Perimeter²
= 4π × (a²) / (4a)²
= 4π × a² / 16a²
= 4πa² / 16a²
= π / 4
≈ 0.785
結論:正方形的圓形度 ≈ 0.785
3. 其他形狀
| 形狀 | 圓形度值 | 計算 |
|---|---|---|
| 圓形 | 1.0 | 4π × πr² / (2πr)² = 1.0 |
| 正方形 | ~0.785 | 4π × a² / (4a)² ≈ 0.785 |
| 長方形 (2:1) | ~0.698 | 4π × 2a² / (6a)² ≈ 0.698 |
| 三角形 (等邊) | ~0.605 | 4π × (√3/4)a² / (3a)² ≈ 0.605 |
| 不規則形狀 | < 0.5 | 視形狀而定 |
在程式碼中:
# 計算輪廓的面積和周長
area = cv2.contourArea(contour)
perimeter = cv2.arcLength(contour, True)
# 計算圓形度
if perimeter > 0:
circularity = 4 * np.pi * area / (perimeter ** 2)
# 設定閾值:只接受圓形度 > 0.7 的物體
if circularity > 0.7:
# 這是一個接近圓形的物體,可能是紅綠燈!
...
閾值選擇:
圓形度閾值 > 0.7:
┌──────────────────────────┐
│ ✓ 圓形 (1.0) │ ← 紅綠燈 ✓
│ ✓ 略微變形的圓 (0.85) │ ← 拍攝角度偏斜的燈 ✓
│ ✓ 正方形 (0.785) │ ← 可能是方形燈號 ✓
│ ✗ 長方形 (0.698) │ ← 招牌 ✗
│ ✗ 三角形 (0.605) │ ← 交通標誌 ✗
│ ✗ 不規則形狀 (< 0.5) │ ← 雜訊 ✗
└──────────────────────────┘
視覺化範例:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 建立不同形狀並計算圓形度
shapes = []
# 1. 圓形
circle = np.zeros((200, 200), dtype=np.uint8)
cv2.circle(circle, (100, 100), 50, 255, -1)
shapes.append(("圓形", circle))
# 2. 正方形
square = np.zeros((200, 200), dtype=np.uint8)
cv2.rectangle(square, (50, 50), (150, 150), 255, -1)
shapes.append(("正方形", square))
# 3. 長方形
rectangle = np.zeros((200, 200), dtype=np.uint8)
cv2.rectangle(rectangle, (50, 75), (150, 125), 255, -1)
shapes.append(("長方形", rectangle))
# 4. 三角形
triangle = np.zeros((200, 200), dtype=np.uint8)
pts = np.array([[100, 50], [50, 150], [150, 150]], np.int32)
cv2.fillPoly(triangle, [pts], 255)
shapes.append(("三角形", triangle))
# 計算並顯示圓形度
for name, shape in shapes:
contours, _ = cv2.findContours(shape, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contour = contours[0]
area = cv2.contourArea(contour)
perimeter = cv2.arcLength(contour, True)
circularity = 4 * np.pi * area / (perimeter ** 2)
print(f"{name}: 圓形度 = {circularity:.3f}")
# 判斷是否通過閾值
if circularity > 0.7:
print(f" ✓ 接受 (圓形度 > 0.7)")
else:
print(f" ✗ 拒絕 (圓形度 < 0.7)")
輸出結果:
圓形: 圓形度 = 1.000
✓ 接受 (圓形度 > 0.7)
正方形: 圓形度 = 0.785
✓ 接受 (圓形度 > 0.7)
長方形: 圓形度 = 0.698
✗ 拒絕 (圓形度 < 0.7)
三角形: 圓形度 = 0.605
✗ 拒絕 (圓形度 < 0.7)
結論:
- 圓形度公式利用等周不等式的數學特性
- 圓形的圓形度恆為 1.0
- 其他形狀的圓形度都 < 1.0
- 設定適當閾值(如 0.7)可以有效篩選圓形物體
核心函數:cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
原理:
- 在二值化圖上尋找連續的白色區域邊界
- 每個連續白色區塊 = 一個輪廓 (Contour)
參數說明:
- cv2.RETR_EXTERNAL:只偵測最外層輪廓(忽略嵌套)
- cv2.CHAIN_APPROX_SIMPLE:壓縮輪廓,只保留端點(節省記憶體)
視覺化:
遮罩圖 偵測到的輪廓
████████ ┌──────┐
████████ → │ C1 │ (Contour 1: 大圓形)
████████ └──────┘
███ │ (Contour 2: 小圓形)
███ ▼
兩大核心特徵:
python
area = cv2.contourArea(contour)
if area < 200: continue # 過濾太小的雜訊過大 → 可能是整個招牌、建築物
圓形度 (Circularity):
python
circularity = 4 * π * area / (perimeter²)
圓形度公式推導:
圓形:Area = πr², Perimeter = 2πr
→ 圓形度 = 4π(πr²)/(2πr)² = 4π²r²/4π²r² = 1.0 ✓
正方形:Area = a², Perimeter = 4a
→ 圓形度 = 4π(a²)/(4a)² = 4πa²/16a² ≈ 0.785
if 偵測到 0 個燈:
狀態 = "未知"
elif 偵測到 1 個燈:
狀態 = 該燈的狀態 (STOP/CAUTION/GO)
elif 偵測到多個燈:
狀態 = 面積最大的燈 (最可能是主要目標)
| 步驟 | 時間複雜度 | 說明 |
|---|---|---|
| BGR→HSV 轉換 | O(W×H) | 每個像素處理一次 |
| 顏色分割 | O(W×H) | 每個像素比較一次 |
| 形態學處理 | O(W×H×K²) | K = kernel 大小 |
| 輪廓偵測 | O(W×H) | 邊界追蹤 |
| 特徵計算 | O(N×M) | N = 輪廓數, M = 輪廓點數 |
| 總體 | O(W×H) | 線性時間,效率高 |
清晰的紅綠燈照片
✅ 白天拍攝的紅綠燈(光線充足)
✅ 燈號清晰發光(紅/黃/綠其中一個亮)
✅ 紅綠燈在畫面中央、佔比適中
✅ 背景相對簡單(天空、建築物)直視角度拍攝
單一紅綠燈為主
光線充足的照片
說明:匯入必要套件,並建立一個理想化的模擬紅綠燈圖片,用於初步驗證演算法的正確性。
程式碼提示:
import cv2
import numpy as np
from google.colab.patches import cv2_imshow
print("="*60)
print("Traffic Light Detection System")
print("="*60)
# 建立模擬紅綠燈測試圖片
def create_traffic_light(light_on='red'):
img = np.ones((600, 400, 3), dtype=np.uint8) * 100
cv2.rectangle(img, (150, 100), (250, 450), (30, 30, 30), -1)
red_pos, yellow_pos, green_pos = (200, 180), (200, 300), (200, 420)
radius = 35
cv2.circle(img, red_pos, radius, (60, 60, 60), -1)
cv2.circle(img, yellow_pos, radius, (60, 60, 60), -1)
cv2.circle(img, green_pos, radius, (60, 60, 60), -1)
if light_on == 'red':
cv2.circle(img, red_pos, radius, (0, 0, 255), -1)
elif light_on == 'yellow':
cv2.circle(img, yellow_pos, radius, (0, 255, 255), -1)
elif light_on == 'green':
cv2.circle(img, green_pos, radius, (0, 255, 0), -1)
return img
img = create_traffic_light('red')
print("✓ Test image created")
print(f"Image size: {img.shape[1]} x {img.shape[0]} pixels")
print("\nOriginal test image:")
cv2_imshow(img)
img_result = img.copy()
說明:定義紅、黃、綠三種燈號的 HSV 顏色範圍。HSV(色相、飽和度、亮度)比 BGR 更適合根據顏色來辨識物體,因為它將顏色與光照強度分開。
重要提示:在 HSV 色彩空間中,紅色橫跨了 0° 和 360° 的邊界。因此,要完整偵測紅色,通常需要設定兩個範圍(例如 0-10 和 170-180)。為簡化初次練習,此處我們先使用一個主要範圍,並在除錯技巧中提供完整方案。
程式碼提示:
# 定義紅綠燈顏色範圍字典
# 格式:'名稱': {'lower': HSV下限, 'upper': HSV上限, ...}
traffic_light_colors = {
'RED': {
'lower': np.array([0, 150, 150]),
'upper': np.array([10, 255, 255]),
'display_color': (0, 0, 255),
'status': 'STOP'
},
'YELLOW': {
'lower': np.array([20, 150, 200]),
'upper': np.array([35, 255, 255]),
'display_color': (0, 255, 255),
'status': 'CAUTION'
},
'GREEN': {
'lower': np.array([40, 100, 150]),
'upper': np.array([80, 255, 255]),
'display_color': (0, 255, 0),
'status': 'GO'
}
}
print("✓ Color ranges configured")
說明:將 BGR 格式的圖片轉換為 HSV 格式,以便使用我們定義的顏色範圍。
程式碼提示:
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
print("✓ Image converted to HSV color space\n")
說明:這一步是核心。我們將遍歷三種燈號,對每一種顏色:
1. 建立遮罩 (Mask):使用 cv2.inRange 篩選出在指定 HSV 範圍內的像素。
2. 尋找輪廓 (Contour):在遮罩上使用 cv2.findContours 找到所有獨立的白色區域邊界。
3. 篩選輪廓:計算輪廓的面積和圓形度,過濾掉太小或形狀不對的雜訊。
程式碼提示:
detected_lights = []
print("="*60 + "\nDetecting traffic lights\n" + "="*60)
for light_name, light_info in traffic_light_colors.items():
print(f"\nDetecting {light_name}...")
# 步驟 1: 建立顏色遮罩
mask = cv2.inRange(hsv, light_info['lower'], light_info['upper'])
# (可選) 形態學操作:先閉合再開,幫助去除小雜訊點
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# 步驟 2: 尋找輪廓
# cv2.RETR_EXTERNAL: 只偵測最外層的輪廓
# cv2.CHAIN_APPROX_SIMPLE: 壓縮水平、垂直和對角線段,只保留其端點
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f" Found {len(contours)} potential contours")
# 步驟 3: 篩選有效輪廓
for contour in contours:
area = cv2.contourArea(contour)
if area < 200: continue # 過濾面積太小的雜訊
perimeter = cv2.arcLength(contour, True)
if perimeter == 0: continue
# 核心篩選條件:圓形度
# 公式:4π * 面積 / (周長^2)。完美圓形的值為 1.0
circularity = 4 * np.pi * area / (perimeter * perimeter)
if 0.7 < circularity < 1.3: # 接受接近圓形的輪廓
x, y, w, h = cv2.boundingRect(contour)
center_x, center_y = x + w // 2, y + h // 2
# 繪製標記
radius = max(w, h) // 2 + 5
cv2.circle(img_result, (center_x, center_y), radius, light_info['display_color'], 3)
label = f"{light_name} ({light_info['status']})"
cv2.putText(img_result, label, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, light_info['display_color'], 2)
detected_lights.append({'name': light_name, 'status': light_info['status'], 'position': (center_x, center_y), 'area': area})
print(f" ✓ Valid contour: pos({center_x},{center_y}), area={area:.0f}px, circularity={circularity:.2f}")
print("\n" + "="*60)
說明:根據偵測到的燈號數量進行最終判斷,並將所有資訊繪製到結果圖上。
程式碼提示:
print("Traffic Light Status\n" + "="*60)
if not detected_lights:
current_status = "Unknown (No light detected)"
elif len(detected_lights) == 1:
light = detected_lights[0]
current_status = f"{light['name']} - {light['status']}"
else: # 偵測到多個燈亮,可能是誤判或場景複雜
main_light = max(detected_lights, key=lambda x: x['area']) # 以面積最大的為準
current_status = f"{main_light['name']} - {main_light['status']} (primary)"
print("⚠️ Warning: Multiple lights detected, using largest one")
print(f"✓ Final status: {current_status}")
print("="*60 + "\n")
# 將狀態資訊繪製到圖片上
cv2.putText(img_result, f"Status: {current_status}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
cv2.putText(img_result, f"Detected: {len(detected_lights)} light(s)", (10, 75), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
# 顯示原圖與結果圖
print("Original image:")
cv2_imshow(img)
print("\nResult with annotations:")
cv2_imshow(img_result)
# 儲存結果
output_filename = 'result_traffic_light.jpg'
cv2.imwrite(output_filename, img_result)
print(f"\n✓ Result saved as {output_filename}")
# ========================================
# Traffic Light Detection System (v1 - English)
# ========================================
import cv2
import numpy as np
from google.colab.patches import cv2_imshow
# === 1. Create/Load Image ===
def create_traffic_light(light_on='red'):
img = np.ones((600, 400, 3), dtype=np.uint8) * 100
cv2.rectangle(img, (150, 100), (250, 450), (30, 30, 30), -1)
positions = {'red': (200, 180), 'yellow': (200, 300), 'green': (200, 420)}
radius = 35
for color, pos in positions.items():
cv2.circle(img, pos, radius, (60, 60, 60), -1)
if light_on in positions:
bgr_color = {'red': (0,0,255), 'yellow': (0,255,255), 'green': (0,255,0)}
cv2.circle(img, positions[light_on], radius, bgr_color[light_on], -1)
return img
img = create_traffic_light('red') # Can be replaced with 'yellow', 'green', or cv2.imread('your_image.jpg')
img_result = img.copy()
print("✓ Image prepared\n")
# === 2. Configure Color Ranges ===
traffic_light_colors = {
'RED': {'lower': np.array([0, 150, 150]), 'upper': np.array([10, 255, 255]), 'display_color': (0, 0, 255), 'status': 'STOP'},
'RED2': {'lower': np.array([170, 150, 150]), 'upper': np.array([180, 255, 255]), 'display_color': (0, 0, 255), 'status': 'STOP'}, # Handle red wraparound
'YELLOW': {'lower': np.array([20, 150, 200]), 'upper': np.array([35, 255, 255]), 'display_color': (0, 255, 255), 'status': 'CAUTION'},
'GREEN': {'lower': np.array([40, 100, 150]), 'upper': np.array([80, 255, 255]), 'display_color': (0, 255, 0), 'status': 'GO'}
}
print("✓ Color ranges configured\n")
# === 3. Convert Color Space ===
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# === 4. Detect Lights ===
detected_lights = []
print("="*60 + "\nDetecting...\n" + "="*60)
for light_name, light_info in traffic_light_colors.items():
mask = cv2.inRange(hsv, light_info['lower'], light_info['upper'])
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
area = cv2.contourArea(contour)
if area < 200: continue
perimeter = cv2.arcLength(contour, True)
if perimeter == 0: continue
circularity = 4 * np.pi * area / (perimeter * perimeter)
if 0.7 < circularity < 1.3:
x, y, w, h = cv2.boundingRect(contour)
center_x, center_y = x + w // 2, y + h // 2
detected_lights.append({'name': light_name.replace('2',''), 'status': light_info['status'], 'position': (center_x, center_y), 'area': area, 'display_color': light_info['display_color']})
# === 5. Judge and Display ===
# Remove duplicates caused by dual red range detection
unique_lights = []
if detected_lights:
detected_lights.sort(key=lambda l: l['area'], reverse=True)
positions_seen = set()
for light in detected_lights:
pos_key = (light['position'][0]//20, light['position'][1]//20) # Treat nearby points as the same
if pos_key not in positions_seen:
unique_lights.append(light)
positions_seen.add(pos_key)
print(f"Found {len(unique_lights)} unique light(s)")
for light in unique_lights:
cv2.circle(img_result, light['position'], 30, light['display_color'], 3)
label = f"{light['name']} ({light['status']})"
cv2.putText(img_result, label, (light['position'][0]-30, light['position'][1]-40), cv2.FONT_HERSHEY_SIMPLEX, 0.7, light['display_color'], 2)
if not unique_lights:
current_status = "Unknown"
else:
main_light = max(unique_lights, key=lambda x: x['area'])
current_status = f"{main_light['name']} - {main_light['status']}"
cv2.putText(img_result, f"Status: {current_status}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
print(f"\nFinal result: {current_status}\n" + "="*60)
print("\nAnnotated result:")
cv2_imshow(img_result)
cv2.imwrite('result_traffic_light_en.jpg', img_result)
# ========================================
# 題目二:紅綠燈辨識系統 (v2 - 中文版)
# ========================================
import cv2
import numpy as np
from google.colab.patches import cv2_imshow
from PIL import Image, ImageDraw, ImageFont
# === 下載中文字型 (在 Colab 中執行一次) ===
# !wget -O NotoSansTC.ttf https://github.com/google/fonts/raw/main/ofl/notosanstc/NotoSansTC-Regular.ttf
# === 中文文字繪製函數 ===
def put_chinese_text(img, text, position, font_path, font_size=30, color=(255, 255, 255)):
"""
在 OpenCV 圖片上繪製中文文字
參數:
img: OpenCV 圖片 (BGR 格式)
text: 要顯示的文字
position: 文字位置 (x, y)
font_path: 字型檔案路徑
font_size: 字型大小
color: 文字顏色 (BGR 格式)
返回:
繪製後的圖片 (BGR 格式)
"""
# 轉換為 PIL 格式 (RGB)
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
# 載入字型
try:
font = ImageFont.truetype(font_path, font_size)
except:
print(f"⚠️ 無法載入字型 {font_path},使用預設字型")
font = ImageFont.load_default()
# 繪製文字 (PIL 使用 RGB,所以要轉換 BGR 到 RGB)
draw.text(position, text, font=font, fill=(color[2], color[1], color[0]))
# 轉回 OpenCV 格式 (BGR)
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
# === 1. 建立/讀取圖片 ===
def create_traffic_light(light_on='red'):
img = np.ones((600, 400, 3), dtype=np.uint8) * 100
cv2.rectangle(img, (150, 100), (250, 450), (30, 30, 30), -1)
positions = {'red': (200, 180), 'yellow': (200, 300), 'green': (200, 420)}
radius = 35
for color, pos in positions.items():
cv2.circle(img, pos, radius, (60, 60, 60), -1)
if light_on in positions:
bgr_color = {'red': (0,0,255), 'yellow': (0,255,255), 'green': (0,255,0)}
cv2.circle(img, positions[light_on], radius, bgr_color[light_on], -1)
return img
img = create_traffic_light('red') # 可替換為 'yellow', 'green', 或使用 cv2.imread('your_image.jpg')
img_result = img.copy()
print("✓ 圖片準備完成\n")
# === 2. 設定顏色範圍 ===
traffic_light_colors = {
'紅燈': {'lower': np.array([0, 150, 150]), 'upper': np.array([10, 255, 255]), 'display_color': (0, 0, 255), 'status': '停止'},
'紅燈2': {'lower': np.array([170, 150, 150]), 'upper': np.array([180, 255, 255]), 'display_color': (0, 0, 255), 'status': '停止'}, # 處理紅色環繞問題
'黃燈': {'lower': np.array([20, 150, 200]), 'upper': np.array([35, 255, 255]), 'display_color': (0, 255, 255), 'status': '注意'},
'綠燈': {'lower': np.array([40, 100, 150]), 'upper': np.array([80, 255, 255]), 'display_color': (0, 255, 0), 'status': '通行'}
}
print("✓ 顏色範圍設定完成\n")
# === 3. 轉換色彩空間 ===
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# === 4. 偵測燈號 ===
detected_lights = []
print("="*60 + "\n開始偵測...\n" + "="*60)
for light_name, light_info in traffic_light_colors.items():
mask = cv2.inRange(hsv, light_info['lower'], light_info['upper'])
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
area = cv2.contourArea(contour)
if area < 200: continue
perimeter = cv2.arcLength(contour, True)
if perimeter == 0: continue
circularity = 4 * np.pi * area / (perimeter * perimeter)
if 0.7 < circularity < 1.3:
x, y, w, h = cv2.boundingRect(contour)
center_x, center_y = x + w // 2, y + h // 2
detected_lights.append({'name': light_name.replace('2',''), 'status': light_info['status'], 'position': (center_x, center_y), 'area': area, 'display_color': light_info['display_color']})
# === 5. 判斷與顯示 ===
# 移除因雙紅色範圍檢測導致的重複項
unique_lights = []
if detected_lights:
detected_lights.sort(key=lambda l: l['area'], reverse=True)
positions_seen = set()
for light in detected_lights:
pos_key = (light['position'][0]//20, light['position'][1]//20) # 將鄰近點視為同一個
if pos_key not in positions_seen:
unique_lights.append(light)
positions_seen.add(pos_key)
print(f"找到 {len(unique_lights)} 個獨立燈號")
# 使用中文字型繪製標記
FONT_PATH = 'NotoSansTC.ttf' # 請確保已下載字型檔案
for light in unique_lights:
cv2.circle(img_result, light['position'], 30, light['display_color'], 3)
label = f"{light['name']} ({light['status']})"
# 使用 PIL 繪製中文
img_result = put_chinese_text(img_result, label,
(light['position'][0]-40, light['position'][1]-50),
FONT_PATH, 24, light['display_color'])
if not unique_lights:
current_status = "未知"
else:
main_light = max(unique_lights, key=lambda x: x['area'])
current_status = f"{main_light['name']} - {main_light['status']}"
# 繪製狀態資訊(使用中文)
img_result = put_chinese_text(img_result, f"狀態: {current_status}",
(10, 40), FONT_PATH, 28, (255, 255, 255))
img_result = put_chinese_text(img_result, f"偵測到: {len(unique_lights)} 個燈號",
(10, 80), FONT_PATH, 20, (255, 255, 255))
print(f"\n最終判斷: {current_status}\n" + "="*60)
print("\n標記後的結果:")
cv2_imshow(img_result)
cv2.imwrite('result_traffic_light_zh.jpg', img_result)
print("✓ 結果已儲存")
PIL (Python Imaging Library) 是 Python 中經典的影像處理函式庫,Pillow 是它的現代分支和繼任者。
| 特性 | OpenCV | PIL/Pillow |
|---|---|---|
| 主要用途 | 電腦視覺、即時處理 | 影像編輯、圖片生成 |
| 顏色格式 | BGR (藍綠紅) | RGB (紅綠藍) |
| 中文支援 | ❌ 不支援 | ✅ 完整支援 |
| 效能 | 極快 (C++ 實作) | 較慢 (Python 為主) |
| 適用場景 | 影像分析、物體偵測 | 文字繪製、圖片合成 |
from PIL import Image
# 開啟圖片
img = Image.open('photo.jpg')
# 從 NumPy 陣列建立 (與 OpenCV 互轉)
img_pil = Image.fromarray(cv_image_rgb)
# 轉回 NumPy 陣列
cv_image = np.array(img_pil)
# 基本屬性
print(img.size) # (寬, 高)
print(img.mode) # 'RGB', 'RGBA', 'L' (灰階)
print(img.format) # 'JPEG', 'PNG', etc.
# 基本操作
img_resized = img.resize((800, 600))
img_cropped = img.crop((100, 100, 400, 400)) # (left, top, right, bottom)
img_rotated = img.rotate(45)
img_flipped = img.transpose(Image.FLIP_LEFT_RIGHT)
from PIL import ImageDraw
# 建立繪圖物件
draw = ImageDraw.Draw(img)
# 繪製各種形狀
draw.rectangle([(100, 100), (200, 200)], outline='red', fill='blue', width=3)
draw.ellipse([(50, 50), (150, 150)], outline='green', width=2)
draw.line([(0, 0), (100, 100)], fill='yellow', width=5)
draw.polygon([(100, 100), (150, 50), (200, 100)], outline='orange', fill='pink')
# 繪製文字 (關鍵功能!)
draw.text((50, 50), "Hello 你好", fill='white', font=font)
from PIL import ImageFont
# 載入 TrueType 字型
font = ImageFont.truetype('/path/to/font.ttf', size=30)
# 載入預設字型 (無中文支援)
font_default = ImageFont.load_default()
# 取得文字尺寸 (用於對齊排版)
bbox = draw.textbbox((0, 0), "測試文字", font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
import cv2
import numpy as np
from PIL import Image
# === OpenCV → PIL ===
def cv2pil(cv_img):
"""
OpenCV (BGR) → PIL (RGB)
"""
# 方法 1: 使用 cv2.cvtColor
rgb_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
pil_img = Image.fromarray(rgb_img)
# 方法 2: 使用 NumPy 切片 (更快)
# pil_img = Image.fromarray(cv_img[:, :, ::-1])
return pil_img
# === PIL → OpenCV ===
def pil2cv(pil_img):
"""
PIL (RGB) → OpenCV (BGR)
"""
# 轉為 NumPy 陣列
rgb_array = np.array(pil_img)
# 轉換顏色通道
bgr_img = cv2.cvtColor(rgb_array, cv2.COLOR_RGB2BGR)
return bgr_img
# === 使用範例 ===
cv_image = cv2.imread('photo.jpg')
pil_image = cv2pil(cv_image)
# ... PIL 操作 (如繪製中文)
cv_result = pil2cv(pil_image)
cv2.imwrite('result.jpg', cv_result)
from PIL import Image, ImageDraw, ImageFont
import numpy as np
# 建立空白畫布
img = Image.new('RGB', (600, 400), color='white')
draw = ImageDraw.Draw(img)
# 載入中文字型
font = ImageFont.truetype('NotoSansTC.ttf', 40)
# 繪製文字
draw.text((50, 50), "紅綠燈辨識系統", fill='black', font=font)
draw.text((50, 120), "Traffic Light Detection", fill='blue', font=font)
img.show()
def draw_text_with_shadow(draw, position, text, font, text_color, shadow_color):
"""繪製帶陰影的文字"""
x, y = position
# 繪製陰影 (偏移 2 像素)
draw.text((x+2, y+2), text, fill=shadow_color, font=font)
# 繪製主文字
draw.text((x, y), text, fill=text_color, font=font)
# 使用範例
img = Image.new('RGB', (600, 200), color='white')
draw = ImageDraw.Draw(img)
font = ImageFont.truetype('NotoSansTC.ttf', 50)
draw_text_with_shadow(draw, (50, 50), "紅燈停", font, 'red', 'black')
img.show()
def draw_centered_text(img, text, font, color):
"""繪製置中文字"""
draw = ImageDraw.Draw(img)
# 計算文字尺寸
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 計算置中位置
img_width, img_height = img.size
x = (img_width - text_width) // 2
y = (img_height - text_height) // 2
# 繪製
draw.text((x, y), text, fill=color, font=font)
return img
# 使用範例
img = Image.new('RGB', (600, 300), color='lightblue')
font = ImageFont.truetype('NotoSansTC.ttf', 60)
img = draw_centered_text(img, "偵測成功", font, 'darkblue')
img.show()
import cv2
from PIL import Image, ImageDraw, ImageFont
import numpy as np
def annotate_with_chinese(cv_img, contours, labels):
"""
在 OpenCV 偵測結果上標註中文標籤
參數:
cv_img: OpenCV 圖片 (BGR)
contours: 輪廓列表
labels: 對應的中文標籤
"""
# 1. OpenCV 繪製輪廓
cv2.drawContours(cv_img, contours, -1, (0, 255, 0), 2)
# 2. 轉換為 PIL 格式
pil_img = Image.fromarray(cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_img)
font = ImageFont.truetype('NotoSansTC.ttf', 25)
# 3. PIL 繪製中文標籤
for i, contour in enumerate(contours):
x, y, w, h = cv2.boundingRect(contour)
draw.text((x, y-30), labels[i], fill='red', font=font)
# 4. 轉回 OpenCV 格式
result = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
return result
# 使用範例
img = cv2.imread('traffic_light.jpg')
# ... (進行輪廓偵測)
result = annotate_with_chinese(img, contours, ['紅燈', '黃燈', '綠燈'])
cv2.imwrite('result.jpg', result)
from pathlib import Path
def batch_add_watermark(input_dir, output_dir, watermark_text):
"""批次加入浮水印"""
font = ImageFont.truetype('NotoSansTC.ttf', 30)
for img_path in Path(input_dir).glob('*.jpg'):
img = Image.open(img_path)
draw = ImageDraw.Draw(img)
# 右下角浮水印
w, h = img.size
draw.text((w-200, h-50), watermark_text, fill='white', font=font)
img.save(Path(output_dir) / img_path.name)
def concat_images_horizontal(img_list):
"""水平拼接圖片"""
widths, heights = zip(*(img.size for img in img_list))
total_width = sum(widths)
max_height = max(heights)
result = Image.new('RGB', (total_width, max_height))
x_offset = 0
for img in img_list:
result.paste(img, (x_offset, 0))
x_offset += img.width
return result
| 錯誤現象 | 原因 | 解決方法 |
|---|---|---|
| 中文顯示為方塊 | 字型不支援中文 | 使用 truetype() 載入中文字型 |
| 顏色錯誤 | BGR/RGB 混淆 | 轉換前檢查色彩空間 |
OSError: cannot open resource |
字型路徑錯誤 | 檢查字型檔案存在且路徑正確 |
| 文字位置偏移 | 座標系統不同 | PIL 左上角為 (0,0),與 OpenCV 相同 |
原因:在 HSV 色彩圓環中,紅色位於 0/360 度的交界處。
完整解法:建立兩個範圍的遮罩(例如 0-10 和 170-180),然後將兩個遮罩合併。
# 紅燈需要兩個 HSV 範圍
red_lower1 = np.array([0, 150, 150])
red_upper1 = np.array([10, 255, 255])
red_lower2 = np.array([170, 150, 150])
red_upper2 = np.array([180, 255, 255])
# 建立兩個遮罩並用 cv2.bitwise_or 合併
mask_red1 = cv2.inRange(hsv, red_lower1, red_upper1)
mask_red2 = cv2.inRange(hsv, red_lower2, red_upper2)
mask_red = cv2.bitwise_or(mask_red1, mask_red2)
# 後續輪廓分析使用合併後的 mask_red
contours, _ = cv2.findContours(mask_red, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
(註:上方完整程式碼已包含此邏輯的簡化實現。)
解法:
1. 提高面積閾值:if area < 500: continue,過濾更大的雜訊。
2. 收緊圓形度範圍:if 0.8 < circularity < 1.2:,要求更接近正圓。
3. 限制偵測區域 (ROI):如果紅綠燈總是在畫面上半部,可以只對上半部進行偵測,忽略下半部的干擾。
解決方案:
缺點:需要額外的字型檔案
使用英文標籤(最簡單)
缺點:不支援中文顯示
字型檔案來源:
wget -O NotoSansTC.ttf https://github.com/google/fonts/raw/main/ofl/notosanstc/NotoSansTC-Regular.ttf/System/Library/Fonts/PingFang.ttcC:/Windows/Fonts/msjh.ttc (微軟正黑體)/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttcVersion b07 更新內容:
1. 新增完整的形態學處理程式碼深度解析章節:
- 逐行解釋三行關鍵程式碼
- Kernel 建立與工作原理的詳細說明
- 閉運算與開運算的完整流程視覺化
- 不同 Kernel 大小的影響分析
- 完整演示程式碼與效果對比
- 進階理解(為什麼先閉後開、可以只用一種嗎)
實際數值對比(不同場景下的 BGR vs HSV)
新增「一般物體的顏色辨識需要 HSV 嗎?」完整分析:
發光物體 vs 一般物體的使用建議
優化視覺化呈現:
實務建議總結
增強實用性: