影像模糊 Kernel (Image Blur)
重點總覽 (Overview)
| 項目 | 內容 |
|---|---|
| 任務 | 每個輸出像素 = 輸入影像中以該像素為中心的 N×N patch 平均值 |
| 數學本質 | 簡化版 convolution(box filter / 等權重平均,無 Gaussian 加權) |
| Thread→Data 映射 | 一個 thread 算一個 output pixel(與 colorToGrayscaleConversion 相同) |
| patch 半徑 | BLUR_SIZE(每一邊的像素數);一維寬度 = 2*BLUR_SIZE+1 |
| 核心結構 | 巢狀 for 迴圈掃 patch + 內層 if 過濾出界像素 |
| 邊界處理 | 用 pixels 計數實際累加的合法像素,再除以它求平均 |
| 累加像素數 (3×3) | 內部 9、邊 6、角 4 |
| 新觀念 | thread 需做較複雜運算並讀取鄰近多個元素(非一對一獨立運算) |
Important
這是本書第一個 thread 需要讀取一片鄰域 (neighborhood) 而非單一元素的 kernel,是邁向 Convolution 與 Stencil pattern 的前奏。
影像模糊的概念 (Image Blurring Concept)
- 目的:平滑像素值的劇烈變化,同時保留辨識特徵所需的邊緣。
- 應用:降噪 / 去顆粒、讓 edge detection 與物件辨識聚焦主體、刻意模糊背景以凸顯重點。
- 本章簡化:取 patch 的簡單平均 (simple average),不依距離加權。
- 真實做法(如 Gaussian blur)會依距離給權重 → 屬於完整 convolution。
| 模式 | 權重 | 章節 |
|---|---|---|
| 本章 box blur | 全部像素等權重(平均) | Ch.3 |
| Gaussian / weighted blur | 依與中心距離加權 | Ch.7 Convolution |
BLUR_SIZE 與 Patch 幾何 (Radius & Patch Geometry)
BLUR_SIZE= patch 每一邊的像素數 (radius)。- patch 一維寬度 =
2*BLUR_SIZE + 1(含中心像素)。
BLUR_SIZE |
patch 尺寸 | 總像素數 |
|---|---|---|
| 1 | 3×3 | 9 |
| 2 | 5×5 | 25 |
| 3 | 7×7 | 49 |
patch 以 (row, col) 為中心,列範圍 row-BLUR_SIZE … row+BLUR_SIZE,行範圍 col-BLUR_SIZE … col+BLUR_SIZE。
col-1 col col+1 (BLUR_SIZE = 1 → 3x3 patch)
+-------+-------+-------+
row-1 | r-1 | r-1 | r-1 |
| c-1 | c | c+1 |
+-------+-------+-------+
row | r | (row, | r | <-- 中心 = 要計算的 output pixel
| c-1 | col) | c+1 |
+-------+-------+-------+
row+1 | r+1 | r+1 | r+1 |
| c-1 | c | c+1 |
+-------+-------+-------+
output[row][col] = average(以上所有「合法」像素)
Tip
對 output pixel (25,50) 且 BLUR_SIZE=1:外層迴圈 curRow 走 24→25→26,內層 curCol 走 49→50→51,恰好掃過 9 個像素。
Blur Kernel 程式碼 (The blurKernel)
__global__ void blurKernel(unsigned char *in, unsigned char *out, int w, int h) {
int col = blockIdx.x * blockDim.x + threadIdx.x; // 行索引 (line 03)
int row = blockIdx.y * blockDim.y + threadIdx.y; // 列索引 (line 04)
if (col < w && row < h) { // 外層邊界檢查 (line 05)
int pixVal = 0;
int pixels = 0;
// 掃過 (2*BLUR_SIZE+1) x (2*BLUR_SIZE+1) 的 patch
for (int blurRow = -BLUR_SIZE; blurRow < BLUR_SIZE+1; ++blurRow) { // line 10
for (int blurCol = -BLUR_SIZE; blurCol < BLUR_SIZE+1; ++blurCol) { // line 11
int curRow = row + blurRow;
int curCol = col + blurCol;
// 內層邊界檢查:patch 可能超出影像範圍 (line 15)
if (curRow >= 0 && curRow < h && curCol >= 0 && curCol < w) {
pixVal += in[curRow * w + curCol]; // 累加合法像素 (line 16)
++pixels; // 計數 (line 17)
}
}
}
out[row * w + col] = (unsigned char)(pixVal / pixels); // 寫回平均 (line 22)
}
}
兩層 if 的分工(這是本 kernel 的關鍵):
| 位置 | 條件 | 防止的事 |
|---|---|---|
| 外層 if (line 05) | col < w && row < h |
thread 對應到影像外(grid 多生的 thread)→ 不計算任何 output pixel |
| 內層 if (line 15) | curRow/curCol 都在 [0, h)/[0, w) |
patch 超出邊界的鄰居像素 → 跳過累加、不計數 |
- 記憶體用 row-major linearization:
in[curRow*w + curCol]、out[row*w + col]。詳見 03-Multidimensional-Grids-And-Data/02-Mapping-Threads-to-Multidimensional-Data。
Warning
平均一定要除以 pixels(實際累加數),不可直接除以 (2*BLUR_SIZE+1)^2。否則角落/邊緣像素會把不存在的「黑邊」算進去,導致邊緣偏暗。
邊界條件處理 (Boundary Conditions)
當 output pixel 靠近影像邊緣,patch 會延伸到影像外,內層 if 把這些不存在的像素直接略過,並讓 pixels 只計入合法像素。
3×3 (BLUR_SIZE=1) 三種情形:
角落 (corner) 邊緣 (edge) 內部 (interior)
patch 中心=(0,0) patch 中心=(0,c) patch 中心=(r,c)
X X X X X X o o o
X o o 合法=4 o o o 合法=6 o o o 合法=9
X o o o o o o o o
X = 出界(被 if 跳過) o = 合法像素累加
| 位置 | 合法像素數 (3×3) | 一般式 |
|---|---|---|
| 內部 (interior) | 9 | (2B+1)^2,B=BLUR_SIZE |
| 邊 (edge, 非角) | 6 | (2B+1)*(B+1) 之類,依鄰近邊數遞減 |
| 角 (corner) | 4 | (B+1)^2 |
Warning
角落範例:output (0,0) 的 9 次迭代中,curRow/curCol 會出現 -1,內層 if 用 curRow >= 0 && curCol >= 0 攔截,最終只累加 4 個合法像素、pixels==4。
Tip
多數 thread 的 patch 完全落在影像內(走 fast path),只有邊與角的少數 thread 走進「跳過」分支 → 形成 control divergence,但比例極小。對照 halo cells 的進階處理方式。
與其他 Kernel 的對照 (Comparison)
| 特性 | colorToGrayscaleConversion |
blurKernel |
|---|---|---|
| Thread→Data 映射 | 1 thread → 1 output pixel | 相同 |
| 每 thread 讀取量 | 單一像素 (r,g,b) | 整片 N×N patch |
| 運算複雜度 | O(1) 固定 | O((2·BLUR_SIZE+1)²) |
| 邊界 if | 只有外層 (col/row 範圍) | 外層 + 內層 patch 邊界 |
是否需 pixels 計數 |
否 | 是(邊角才能正確平均) |
考試/面試重點 (Exam / Test Patterns)
| 情境 / 關鍵字 | 答案 / 技巧 |
|---|---|
BLUR_SIZE 代表什麼? |
patch 半徑(每邊像素數);patch 一維寬度 = 2*BLUR_SIZE+1 |
| 7×7 patch 的 BLUR_SIZE | 3(因 2*3+1 = 7) |
| 為何需要兩個 if? | 外層擋「thread 對應影像外」;內層擋「patch 鄰居出界」 |
為何用 pixels 而不用固定 patch 面積除? |
邊/角 patch 部分出界,合法像素數不同;除以實際 pixels 才正確 |
| 3×3 時,角落像素累加幾個? | 4;邊緣 6;內部 9 |
| 記憶體索引怎麼寫? | row-major:in[curRow*w + curCol]、out[row*w + col] |
| 與 grayscale kernel 的差別? | thread 需讀一片鄰域並做較複雜運算(非一對一獨立) |
| 這屬於哪種 parallel pattern? | 簡化的 convolution(等權重 box filter) |
| 邊界 thread 造成什麼效能議題? | control divergence(少數 thread 走 skip 分支) |
| Gaussian blur 與本章差別? | Gaussian 依距離加權;本章等權重平均 |
Related Notes
- 03-Multidimensional-Grids-And-Data/02-Mapping-Threads-to-Multidimensional-Data
- 03-Multidimensional-Grids-And-Data/04-Matrix-Multiplication-Kernel
- 03-Multidimensional-Grids-And-Data/01-Multidimensional-Grid-Organization
- 07-Convolution/01-Convolution-Fundamentals-and-Basic-Kernel
- 07-Convolution/03-Tiled-Convolution-and-Halo-Handling
- 04-Compute-Architecture-And-Scheduling/02-Warps-SIMD-and-Control-Divergence