# 📏 Canny 邊緣檢測(Canny Edge Detection) --- ## 📘 1. 題目說明 Canny 邊緣檢測是一種經典且廣泛應用的圖像處理技術,用於檢測圖像中的邊緣特徵。其核心步驟包含: 1. **高斯模糊(Gaussian Blur)** 減少圖像中的雜訊與細節,避免誤檢邊緣。 2. **梯度計算(Computing Gradients)** 使用 Sobel 遮罩計算水平與垂直方向的梯度。 3. **非極大值抑制(Non-Maximum Suppression)** 保留梯度方向上的局部極大值,去除非邊緣像素。 4. **雙閾值檢測(Double Thresholding)** 根據梯度強度區分:強邊緣、弱邊緣與非邊緣。 5. **邊緣追蹤(Edge Tracking by Hysteresis)** 將與強邊緣連通的弱邊緣保留,其餘濾除。 --- ## 🔁 2. 流程圖 ![圖片13](https://hackmd.io/_uploads/Sy-dGjiAJl.png) --- ## 💻 3. 程式碼邏輯與流程說明 ### 🔧 A. 高斯低通濾波器 ```cpp= void gaussianLowPassFilter(int inputR[][MaxBMPSizeY], int outputR[][MaxBMPSizeY], int width, int height, float sigma) { // 計算高斯遮罩 int maskSize = 5; float mask[maskSize][maskSize]; float sum = 0.0; // 計算高斯遮罩的每個元素 for (int y = 0; y < maskSize; y++) { for (int x = 0; x < maskSize; x++) { mask[y][x] = exp(-(pow(x - maskSize / 2, 2) + pow(y - maskSize / 2, 2)) / (2 * sigma * sigma)); sum += mask[y][x]; } } //正規化 for (int y = 0; y < maskSize; y++) { for (int x = 0; x < maskSize; x++) { mask[y][x] /= sum; } } for (int j = maskSize / 2; j < height - maskSize / 2; j++) { for (int i = maskSize / 2; i < width - maskSize / 2; i++) { float sumR = 0.0; // 在遮罩範圍內計算像素值總和 for (int y = -maskSize / 2; y <= maskSize / 2; y++) { for (int x = -maskSize / 2; x <= maskSize / 2; x++) { sumR += inputR[i + x][j + y] * mask[y + maskSize / 2][x + maskSize / 2]; } } // 將相加結果設定為輸出影像的像素值 outputR[i][j] = (int)sumR; } } } ``` ![圖片14](https://hackmd.io/_uploads/H1kwEijRJe.png) - 使用 5×5 高斯遮罩 - 遮罩根據 `sigma` 產生並正規化(總和為 1) - 每個像素套用遮罩後,完成平滑處理以降低雜訊 --- ### B. 梯度計算(Sobel 遮罩) - 使用 Sobel 遮罩計算圖像在 x 和 y 方向的梯度 ```cpp= void calculateGradient(int inputR[][MaxBMPSizeY], int outputMagnitude[][MaxBMPSizeY], int outputDirection[][MaxBMPSizeY], int width, int height) { // Sobel遮罩 int sobelX[3][3] = { {-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1} }; int sobelY[3][3] = { {-1, -2, -1}, { 0, 0, 0}, { 1, 2, 1} }; for (int j = 1; j < height - 1; j++) { for (int i = 1; i < width - 1; i++) { // 計算水平和垂直梯度 int gradientX = 0; int gradientY = 0; for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { gradientX += inputR[i + x][j + y] * sobelX[x + 1][y + 1]; gradientY += inputR[i + x][j + y] * sobelY[x + 1][y + 1]; } } // 計算梯度大小和方向 int magnitude = (int)sqrt(gradientX * gradientX + gradientY * gradientY); double direction = atan2((double)gradientY, (double)gradientX) * 180 / M_PI; // 將角度分成4等分 int quantizedDirection; if ((direction >= -22.5 && direction < 22.5) || (direction <= -157.5 && direction >= -180)) { quantizedDirection = 0; } else if (direction >= 22.5 && direction < 67.5) { quantizedDirection = 45; } else if (direction >= 67.5 && direction < 112.5) { quantizedDirection = 90; } else if (direction >= 112.5 && direction < 157.5) { quantizedDirection = 135; } outputMagnitude[i][j] = magnitude; //梯度大小 outputDirection[i][j] = quantizedDirection; //梯度方向 } } } ``` ### C. 非極大值抑制 - 對梯度大小圖像進行精細處理,保留局部最大值 - 根據梯度方向分為:0°、45°、90°、135° ![圖片15](https://hackmd.io/_uploads/B13wrosCyl.png) ```cpp= void nonMaxSuppression(int inputMagnitude[][MaxBMPSizeY], int inputDirection[][MaxBMPSizeY], int outputR[][MaxBMPSizeY], int width, int height) { for (int j = 1; j < height - 1; j++) { for (int i = 1; i < width - 1; i++) { int magnitude = inputMagnitude[i][j]; // 當前像素的梯度強度 int direction = inputDirection[i][j]; // 當前像素的梯度方向 // 比較前後兩個方向的梯度強度,若不是最大值則設置為0 if ((direction == 0 && (magnitude < inputMagnitude[i][j + 1] || magnitude < inputMagnitude[i][j - 1])) || (direction == 45 && (magnitude < inputMagnitude[i - 1][j + 1] || magnitude < inputMagnitude[i + 1][j - 1])) || (direction == 90 && (magnitude < inputMagnitude[i + 1][j] || magnitude < inputMagnitude[i - 1][j])) || (direction == 135 && (magnitude < inputMagnitude[i + 1][j + 1] || magnitude < inputMagnitude[i - 1][j - 1]))) { outputR[i][j] = 0; // 將非最大值設置為0 } else { outputR[i][j] = inputMagnitude[i][j]; // 保留最大值 } } } } ``` ### D. 雙閾值分類 - 高於高閾值 → 強邊緣 - 低於低閾值 → 非邊緣 - 介於兩者之間 → 弱邊緣 ![圖片16](https://hackmd.io/_uploads/r1lSLjiCyx.png) ```cpp= void doubleThreshold(int inputMagnitude[][MaxBMPSizeY], int outputR[][MaxBMPSizeY], int width, int height, int th, int tl) { //雙閾值判斷 for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { int magnitude = inputMagnitude[i][j]; // 將梯度大小高於高閾值(th)的像素設定為強邊緣 if (magnitude > th) { outputR[i][j] = 255; } // 將梯度大小低於低閾值(tl)的像素設定為非邊緣 else if (magnitude < tl) { outputR[i][j] = 0; } // 將梯度大小介於高閾值和低閾值之間的像素標記為可能的邊緣 else { outputR[i][j] = 128; // 使用128作為標記值,之後會進行邊緣追蹤 } } } ``` ### E. 邊緣追蹤 - 若弱邊緣與強邊緣連接 → 視為邊緣 - 其餘弱邊緣 → 濾除 ![螢幕擷取畫面 2025-04-15 172514](https://hackmd.io/_uploads/HkdL8ioAkl.png) ```cpp= //邊緣追蹤(edge tracking) for (int j = 1; j < height - 1; j++) { for (int i = 1; i < width - 1; i++) { // 檢查標記值為128的像素(可能的邊緣) if (outputR[i][j] == 128) { // 檢查周圍像素是否有強邊緣(標記值為255) bool hasStrongEdge = false; for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { if (outputR[i + x][j + y] == 255) { hasStrongEdge = true; break; } } if (hasStrongEdge) { break; } } // 如果周圍像素中有強邊緣,則將該像素設定為強邊緣(255),否則設定為非邊緣(0) outputR[i][j] = hasStrongEdge ? 255 : 0; } } } } ``` ### ✅ 3. 實驗結果與分析 --- #### 📷 處理前後影像對比 | 原始圖像 | Canny 邊緣偵測後 | |----------|------------------| |![圖片17](https://hackmd.io/_uploads/Hk-SPsoCke.png) |![圖片18](https://hackmd.io/_uploads/ByALDijC1g.png) --- #### 📊 分析說明 - **精準定位邊緣:** 相較於其他邊緣偵測演算法,Canny 能更準確地描繪出物體輪廓,提升邊緣偵測的精度。 - **雜訊抑制效果佳:** 結合高斯濾波與雙閾值處理,有效濾除影像中的隨機雜訊,降低誤偵測率。 - **邊緣連續性良好:** 透過弱邊緣追蹤機制(Hysteresis Thresholding),確保邊緣在偵測結果中連續不斷裂,使影像輪廓更自然清晰。 --- #### 📈 結論 - Canny 邊緣偵測法在保留物體結構的同時,有效去除雜訊。 - 適合應用於需要高邊緣解析度的圖像前處理階段。 - 若搭配形態學處理(如膨脹、腐蝕)可進一步強化邊緣輪廓。