寫在前面
- 作者:隊友調(diào)車我吹空調(diào)
- 日期:2023/05/17
- 版權(quán):遵循CC 4.0 BY-SA版權(quán)協(xié)議
這里是后期的筆者,本文算是筆者的學(xué)習(xí)筆記,主要是在單片機(jī)中使用的,再者就是由于某些原因,筆者不想使用opencv,因此嘗試跟著原理手搓了這份代碼,筆者也盡力將代碼寫到最簡和效率最優(yōu)了。然而即使如此,Canny算子運算量依然非常大。高斯濾波的時候遍歷了一次圖像,計算梯度的時候遍歷了四次,非極大抑制的時候遍歷了一次。雙閾值和滯后邊界跟蹤總共遍歷一次。也就是說整套算法下來一共遍歷了圖像7次,完成跑下來爆內(nèi)存,筆者所使用的TC387都險些跑不下來。
所以在最開始,首先要對圖像做一些處理。
在進(jìn)行高斯濾波器前,將圖像裁剪到合適的大小,在這里筆者比較推薦30*80的大小。筆者在TC387上使用單獨一個核是可以較快運行的。如果對細(xì)節(jié)要求不高的同學(xué)可以再降采樣一次。
筆者在這里給大家提供一個圖像裁剪的函數(shù)。
/*************************************************************************
* 函數(shù)名稱:cropImage
* 功能說明:將大小為[MT9V03X_H][MT9V03X_W]的圖像裁剪到[PICTURE_H][PICTURE_W]
* 參數(shù)說明:picture_in[MT9V03X_H][MT9V03X_W] 輸入圖像
* 函數(shù)返回:void
* 修改時間:2023/05/17
* 備一一注:
*************************************************************************/
void cropImage(const uint8_t picture_in[MT9V03X_H][MT9V03X_W], uint8_t picture_out[PICTURE_H][PICTURE_W])
{
int start_row = (MT9V03X_H - PICTURE_H) / 2;
int start_col = (MT9V03X_W - PICTURE_W) / 2;
for (int i = 0; i < PICTURE_H; i++) {
for (int j = 0; j < PICTURE_W; j++) {
picture_out[i][j] = picture_in[start_row + i][start_col + j];
}
}
}
筆者在正是內(nèi)容開始前,先談一談自己對Canny算子的理解。其實認(rèn)真看了我下邊的內(nèi)容的話,是能夠感覺到Canny算子的思想非常簡單的,先高斯濾波將圖像細(xì)節(jié)處理一下,接下來計算梯度幅值、梯度方向,梯度變化最劇烈的地方就是邊界;找到邊界以后,對邊界濾一下波,除去最不可能是邊界的地方,標(biāo)出最可能是邊界的地方,再用邊界跟蹤算法處理有一點可能是邊界的地方。
Canny算子的核心思想可以看成是使用了兩次Sobel算子,但是效果和計算量與Sobel算子都是不可同日而語的。
概覽
Canny邊緣檢測是一種非常流行的邊緣檢測算法,是John Canny在1986年提出的。它是一個多階段的算法,即由多個步驟構(gòu)成。本文主要講解了Canny算子的原理及實現(xiàn)過程。通常情況下邊緣檢測的目的是在保留原有圖像屬性的情況下,顯著減少圖像的數(shù)據(jù)規(guī)模。有多種算法可以進(jìn)行邊緣檢測,雖然Canny算法年代久遠(yuǎn),但可以說它是邊緣檢測的一種標(biāo)準(zhǔn)算法,而且仍在研究中廣泛使用。
步驟
- 應(yīng)用高斯濾波來平滑圖像,目的是去除噪聲
- 找尋圖像的強度梯度(intensity gradients)
- 應(yīng)用非最大抑制(non-maximum suppression)技術(shù)來消除邊誤檢(本來不是但檢測出來是)
- 應(yīng)用雙閾值的方法來決定可能的(潛在的)邊界
- 利用滯后技術(shù)來跟蹤邊界
詳解
高斯平滑濾波
濾波是為了去除噪聲,選用高斯濾波也是因為在眾多噪聲濾波器中,高斯表現(xiàn)最好(如果嘗試其他濾波器,如均值濾波、中值濾波等等,就會發(fā)現(xiàn)高斯的效果最好)。
一個大小為(2k+1)x(2k+1)的高斯濾波器核(核一般都是奇數(shù)尺寸的)的生成方程式由下式給出:
高斯濾波
高斯濾波是一種線性平滑濾波,適用于消除高斯噪聲,廣泛應(yīng)用于圖像處理的減噪過程。
通俗的講,高斯濾波就是對整幅圖像進(jìn)行加權(quán)平均的過程,每一個像素點的值,都由其本身和鄰域內(nèi)的其他像素值經(jīng)過加權(quán)平均后得到。
高斯濾波的具體操作是:用一個模板(或稱卷積、掩模)掃描圖像中的每一個像素,用模板確定的鄰域內(nèi)像素的加權(quán)平均灰度值去替代模板中心像素點的值。
對于均值濾波和方框濾波來說,其鄰域內(nèi)每個像素的權(quán)重是相等的。而在高斯濾波中,會將中心點的權(quán)重值加大,遠(yuǎn)離中心點的權(quán)重值減小,在此基礎(chǔ)上計算鄰域內(nèi)各個像素值不同權(quán)重的和。
針對最左側(cè)的圖像內(nèi)第4行第3列位置上像素值為226的像素點進(jìn)行高斯卷積,其運算規(guī)則為將該領(lǐng)域內(nèi)的像素點按照不同的權(quán)重計算和。
在實際使用中,高斯濾波使用的可能是不同大小的卷積核。例如, 3×3、5×5、7×7 大小的卷積核。在高斯濾波中,核的寬度和高度可以不相同,但是它們都必須是奇數(shù)。
每一種尺寸的卷積核都可以有多種不同形式的權(quán)重比例。例如,同樣是5×5的卷積核,可能是兩種不同的權(quán)重比。
在實際計算中,卷積核是歸一化處理的。
————————————————
版權(quán)聲明:本文為CSDN博主「半濠春水」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議。
原文鏈接:https://blog.csdn.net/weixin_51571728/article/details/121527964
代碼實現(xiàn)
#include <stdio.h>
#include <math.h>
#define PICTURE_H 120
#define PICTURE_W 188
void gaussianBlur(unsigned char picture_in[PICTURE_H][PICTURE_W], int ksize, double sigma, unsigned char picture_out[PICTURE_H][PICTURE_W])
{
int i, j, m, n;
int radius = ksize / 2;
double weightSum;
double weightTotal;
double weightMatrix[ksize][ksize];
// 計算高斯權(quán)重矩陣
for (i = 0; i < ksize; i++)
{
for (j = 0; j < ksize; j++)
{
m = i - radius;
n = j - radius;
weightMatrix[i][j] = exp(-((m * m + n * n) / (2 * sigma * sigma)));
weightSum += weightMatrix[i][j];
}
}
// 高斯濾波處理
for (i = 0; i < PICTURE_H; i++)
{
for (j = 0; j < PICTURE_W; j++)
{
double pixelValue = 0.0;
weightTotal = 0.0;
for (m = -radius; m <= radius; m++)
{
for (n = -radius; n <= radius; n++)
{
if (i + m >= 0 && i + m < PICTURE_H && j + n >= 0 && j + n < PICTURE_W)
{
pixelValue += picture_in[i + m][j + n] * weightMatrix[m + radius][n + radius];
weightTotal += weightMatrix[m + radius][n + radius];
}
}
}
picture_out[i][j] = (unsigned char)(pixelValue / weightTotal);
}
}
}
調(diào)用示例
int main()
{
// 定義輸入和輸出圖像數(shù)組
unsigned char picture_in[PICTURE_H][PICTURE_W];
unsigned char picture_out[PICTURE_H][PICTURE_W];
// 假設(shè)已經(jīng)讀取了輸入圖像到picture_in數(shù)組中
int ksize = 3; // 濾波核大小
double sigma = 1.0; // 水平方向上的標(biāo)準(zhǔn)差
// 調(diào)用高斯濾波函數(shù)
gaussianBlur(picture_in, ksize, sigma, picture_out);
// 輸出濾波后的圖像或進(jìn)行其他處理
return 0;
}
調(diào)用效果
結(jié)果是下面兩張圖片,可以看到圖像的邊緣被明顯地模糊了,圖像的抗噪能力變強了。
總結(jié):
總結(jié)一下這一步:高斯濾波其實就是將所指像素用周圍的像素的某種均值代替(即卷積核),卷積核尺寸越大,去噪能力越強,因此噪聲越少,但圖片越模糊,canny檢測算法抗噪聲能力越強,但模糊的副作用也會導(dǎo)致定位精度不高。
高斯的卷積核大小推薦:一般情況下,尺寸5 * 5,3 * 3就行。
計算梯度的大小和方向
對于一張圖片來說,梯度能很好地反映其像素的變化情況,而梯度變化越大,說明相鄰像素之間存在著較大差異,放大到整張圖片來說,就是在某一塊區(qū)域存在邊緣,從視覺上來說就是用黑到白(灰度圖片讀入)。
梯度的計算分為大小和方向,首先需要求出各個方向上的梯度,然后求平方根和切線。
以下是x、y方向上梯度的計算方式:
計算梯度的過程包括以下幾個步驟:
- 首先,使用某種邊緣檢測算子(如Sobel算子)分別對圖像在水平和垂直方向進(jìn)行卷積運算。這將產(chǎn)生兩個梯度圖像,分別表示水平方向和垂直方向上的像素變化情況。
- 對于每個像素,可以使用求平方根的方法來計算梯度的大小。將水平方向和垂直方向上的梯度值平方,然后將它們相加,再對結(jié)果進(jìn)行開方運算。這將給出每個像素位置上的梯度大小。
- 同時,可以使用反正切函數(shù)(如
atan2
)來計算梯度的方向。通過將水平方向上的梯度值除以垂直方向上的梯度值,可以得到每個像素位置上的梯度方向。
使用Sobel算子計算圖像梯度幅值和梯度方向
Sobel算子可以分別在水平和垂直方向上對圖像進(jìn)行卷積操作,以捕捉圖像中像素值的變化情況。以下是使用Sobel算子計算梯度的詳細(xì)步驟:
灰度化:首先,將彩色圖像轉(zhuǎn)換為灰度圖像。這可以通過將彩色圖像的每個像素的紅、綠和藍(lán)通道值進(jìn)行加權(quán)平均來實現(xiàn),得到對應(yīng)的灰度值。
建立Sobel算子:Sobel算子是一個3x3的卷積核,分為水平和垂直方向上的兩個核。這兩個核分別用于檢測圖像中水平和垂直方向上的邊緣。下面是Sobel算子的示例:
水平方向Sobel算子:
-1 0 1
-2 0 2
-1 0 1
垂直方向Sobel算子:
-1 -2 -1
0 0 0
1 2 1
對圖像進(jìn)行卷積運算:將Sobel算子分別應(yīng)用于圖像的水平和垂直方向上,進(jìn)行卷積運算。對于每個像素位置,將算子與其周圍像素進(jìn)行乘法運算,并將乘積的結(jié)果相加,得到新的像素值。
計算梯度幅值和方向:對于每個像素位置,使用水平方向和垂直方向上的卷積結(jié)果,分別計算梯度的幅值和方向。幅值可以通過求平方根來得到,方向可以通過反正切函數(shù)(如atan2
)計算得到。
建立Sobel算子,計算每個像素點在四個方向上的梯度幅值
代碼實現(xiàn)
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef signed short int int16;
/****************************************************************************************************
*函數(shù)簡介 基于soble算子計算四個方向的梯度幅值
*參數(shù)說明 picture 輸入二維數(shù)組名
*參數(shù)說明 gradient_h 水平方向梯度幅值
gradient_v 垂直方向梯度幅值
gradient_d1 正對角線方向梯度幅值
gradient_d2 反對角線方向梯度幅值
*返回參數(shù) void
****************************************************************************************************/
void sobel_gradient(uint8 picture[PICTURE_H][PICTURE_W], int16 gradient_h[PICTURE_H][PICTURE_W], int16 gradient_v[PICTURE_H][PICTURE_W], int16 gradient_d1[PICTURE_H][PICTURE_W], int16 gradient_d2[PICTURE_H][PICTURE_W])
{
/*卷積核大小*/
uint8 KERNEL_SIZE = 3;
uint16 xStart = KERNEL_SIZE / 2;
uint16 xEnd = PICTURE_W - KERNEL_SIZE / 2;
uint16 yStart = KERNEL_SIZE / 2;
uint16 yEnd = PICTURE_H - KERNEL_SIZE / 2;
uint16 i, j;
for (i = yStart; i < yEnd; i++)
{
for (j = xStart; j < xEnd; j++)
{
/*計算不同方向梯度幅值*/
// 水平方向
gradient_h[i][j] = -(int16)picture[i - 1][j - 1] + (int16)picture[i - 1][j + 1]
-(int16)picture[i][j - 1] + (int16)picture[i][j + 1]
-(int16)picture[i + 1][j - 1] + (int16)picture[i + 1][j + 1];
// 垂直方向
gradient_v[i][j] = -(int16)picture[i - 1][j - 1] + (int16)picture[i + 1][j - 1]
-(int16)picture[i - 1][j] + (int16)picture[i + 1][j]
-(int16)picture[i - 1][j + 1] + (int16)picture[i + 1][j + 1];
// 對角線方向1
gradient_d1[i][j] = -(int16)picture[i - 1][j] + (int16)picture[i][j - 1]
-(int16)picture[i][j + 1] + (int16)picture[i + 1][j]
-(int16)picture[i - 1][j + 1] + (int16)picture[i + 1][j - 1];
// 對角線方向2
gradient_d2[i][j] = -(int16)picture[i - 1][j] + (int16)picture[i][j + 1]
-(int16)picture[i][j - 1] + (int16)picture[i + 1][j]
-(int16)picture[i - 1][j - 1] + (int16)picture[i + 1][j + 1];
}
}
}
調(diào)用示例
#define PICTURE_H 120
#define PICTURE_W 188
uint8 picture[PICTURE_H][PICTURE_W];
int16 gradient_h[PICTURE_H][PICTURE_W];
int16 gradient_v[PICTURE_H][PICTURE_W];
int16 gradient_d1[PICTURE_H][PICTURE_W];
int16 gradient_d2[PICTURE_H][PICTURE_W];
// 填充輸入圖像數(shù)組 picture
SOBEL(picture, gradient_h, gradient_v, gradient_d1, gradient_d2);
// 使用計算得到的梯度幅值進(jìn)行后續(xù)處理
計算圖像的梯度幅值和梯度方向
代碼實現(xiàn)
/****************************************************************************************************
* 計算圖像的梯度幅值和方向
* @param picture 輸入圖像數(shù)組,類型為 uint8,大小為 [PICTURE_H][PICTURE_W]。
* @param Gx 輸入圖像的水平方向梯度數(shù)組,類型為 int16,大小為 [PICTURE_H][PICTURE_W]。
* @param Gy 輸入圖像的垂直方向梯度數(shù)組,類型為 int16,大小為 [PICTURE_H][PICTURE_W]。
* @param G 輸出圖像的梯度幅值數(shù)組,類型為 int16,大小為 [PICTURE_H][PICTURE_W]。
* @param theta 輸出圖像的梯度方向數(shù)組,類型為 float,大小為 [PICTURE_H][PICTURE_W]。
****************************************************************************************************/
void calculate_gradient(uint8 picture[PICTURE_H][PICTURE_W], int16 Gx[PICTURE_H][PICTURE_W], int16 Gy[PICTURE_H][PICTURE_W], int16 G[PICTURE_H][PICTURE_W], float theta[PICTURE_H][PICTURE_W])
{
// 計算圖像梯度幅值和方向
for (int i = 0; i < PICTURE_H; i++)
{
for (int j = 0; j < PICTURE_W; j++)
{
G[i][j] = sqrt(pow(Gx[i][j], 2) + pow(Gy[i][j], 2));
theta[i][j] = atan2(Gy[i][j], Gx[i][j]) * 180 / PI;
}
}
}
調(diào)用示例
#include <stdio.h>
#define PICTURE_H 100 // 圖像的高度
#define PICTURE_W 100 // 圖像的寬度
void calculate_gradient(uint8 picture[PICTURE_H][PICTURE_W], int16 Gx[PICTURE_H][PICTURE_W], int16 Gy[PICTURE_H][PICTURE_W], int16 G[PICTURE_H][PICTURE_W], float theta[PICTURE_H][PICTURE_W]);
int main()
{
// 假設(shè)有一個大小為 [PICTURE_H][PICTURE_W] 的圖像和梯度數(shù)組
uint8 picture[PICTURE_H][PICTURE_W];
int16 Gx[PICTURE_H][PICTURE_W];
int16 Gy[PICTURE_H][PICTURE_W];
int16 G[PICTURE_H][PICTURE_W];
float theta[PICTURE_H][PICTURE_W];
// 填充圖像和梯度數(shù)組的數(shù)據(jù)
// 調(diào)用計算梯度函數(shù)
calculate_gradient(picture, Gx, Gy, G, theta);
// 打印梯度幅值和方向的示例
for (int i = 0; i < PICTURE_H; i++)
{
for (int j = 0; j < PICTURE_W; j++)
{
printf("G[%d][%d] = %d\n", i, j, G[i][j]);
printf("theta[%d][%d] = %f\n", i, j, theta[i][j]);
}
}
return 0;
}
調(diào)用結(jié)果
可以看到,[-1, 0, 1], [-1, 0, 1], [-1, 0, 1] 對應(yīng)了x方向上的邊緣檢測(在碰到x方向垂直的邊界的時候計算出來的數(shù)值更大,所以像素更亮),[1, 1, 1], [0, 0, 0], [-1, -1, -1] 同理對應(yīng)了y方向上的邊緣檢測。
非極大抑制
用Sobel算子或者是其他簡單的邊緣檢測方法,計算出來的邊緣太粗了,就是說原本的一條邊用幾條幾乎重疊的邊所代替了,導(dǎo)致視覺上看起來邊界很粗。非極大抑制是一種瘦邊經(jīng)典算法。它抑制那些梯度不夠大的像素點,只保留最大的梯度,從而達(dá)到瘦邊的目的。
a) 將其梯度方向近似為以下值中的一個[0,45,90,135,180,225,270,315](即上下左右和45度方向)這一步是為了方便使用梯度;
b) 比較該像素點,和其梯度方向正負(fù)方向的像素點的梯度強度,這里比較的范圍一般為像素點的八鄰域;
c) 如果該像素點梯度強度最大則保留,否則抑制(刪除,即置為0);
代碼實現(xiàn)
/****************************************************************************************************
* 對圖像的梯度幅值進(jìn)行非極大值抑制,以細(xì)化邊緣。
*
* @param G 梯度幅值數(shù)組,類型為 int16,大小為 [PICTURE_H][PICTURE_W]。
* @param theta 梯度方向數(shù)組,類型為 float,大小為 [PICTURE_H][PICTURE_W]。
* @param edge_threshold 邊緣閾值,類型為 int16。
* @param result 輸出的二值化邊緣圖像數(shù)組,類型為 uint8,大小為 [PICTURE_H][PICTURE_W]。
****************************************************************************************************/
void non_maximum_suppression(int16 G[PICTURE_H][PICTURE_W], float theta[PICTURE_H][PICTURE_W], int16 edge_threshold, uint8 result[PICTURE_H][PICTURE_W])
{
// 對梯度幅值進(jìn)行非極大值抑制
for (int i = 1; i < PICTURE_H - 1; i++)
{
for (int j = 1; j < PICTURE_W - 1; j++)
{
float angle = theta[i][j];
// 將角度映射到 0 到 180 度之間
if (angle < 0)
angle += 180;
// 判斷當(dāng)前像素點的梯度方向,并進(jìn)行非極大值抑制
if (((angle >= 0 && angle < 22.5) || (angle >= 157.5 && angle < 180)) || // 水平方向
((angle >= 22.5 && angle < 67.5) || (angle >= 202.5 && angle < 247.5)) || // 主對角線方向
((angle >= 67.5 && angle < 112.5) || (angle >= 247.5 && angle < 292.5)) || // 垂直方向
((angle >= 112.5 && angle < 157.5) || (angle >= 292.5 && angle < 337.5))) // 次對角線方向
{
if (G[i][j] > G[i][j - 1] && G[i][j] > G[i][j + 1])
{
// 梯度幅值大于相鄰兩個像素的幅值,保留邊緣
result[i][j] = (G[i][j] >= edge_threshold) ? 255 : 0;
}
else
{
result[i][j] = 0;
}
}
else
{
if (G[i][j] > G[i - 1][j] && G[i][j] > G[i + 1][j])
{
// 梯度幅值大于相鄰兩個像素的幅值,保留邊緣
result[i][j] = (G[i][j] >= edge_threshold) ? 255 : 0;
}
else
{
result[i][j] = 0;
}
}
}
}
// 將圖像邊緣的邊界像素置為 0
for (int i = 0; i < PICTURE_H; i++)
{
result[i][0] = 0;
result[i][PICTURE_W - 1] = 0;
}
for (int j = 0; j < PICTURE_W; j++)
{
result[0][j] = 0;
result[PICTURE_H - 1][j] = 0;
}
}
調(diào)用示例
// 假設(shè)有已計算好的梯度幅值和方向數(shù)組 G 和 theta
int16 G[PICTURE_H][PICTURE_W];
float theta[PICTURE_H][PICTURE_W];
// 定義邊緣閾值
int16 edge_threshold = 100;
// 定義二值化邊緣圖像數(shù)組
uint8 result[PICTURE_H][PICTURE_W];
// 執(zhí)行非極大值抑制
non_maximum_suppression(G, theta, edge_threshold, result);
示例中的邊緣閾值 edge_threshold
是一個示例值,需要根據(jù)實際情況調(diào)整該閾值以達(dá)到滿意的邊緣檢測效果。
該函數(shù)使用兩個嵌套的循環(huán)遍歷圖像像素,并根據(jù)梯度方向進(jìn)行非極大值抑制。代碼中對于不同的梯度方向使用了不同的判斷條件,并根據(jù)相鄰像素的梯度幅值決定是否保留邊緣。在循環(huán)結(jié)束后,代碼還會將圖像邊緣的邊界像素置為0。
調(diào)用結(jié)果
雙閾值(Double Thresholding)和滯后邊界跟蹤
經(jīng)過非極大抑制后圖像中仍然有很多噪聲點。Canny算法中應(yīng)用了一種叫雙閾值的技術(shù)。
使用雙閾值是常見的邊緣檢測后續(xù)處理步驟之一。雙閾值處理可以更好地區(qū)分邊緣和非邊緣像素,并進(jìn)一步提升邊緣檢測的準(zhǔn)確性。
雙閾值處理包括以下步驟:
- 高閾值和低閾值的選擇:根據(jù)你的應(yīng)用需求和圖像特性,選擇適當(dāng)?shù)母唛撝岛偷烷撝?。高閾值用于定義強邊緣像素,低閾值用于定義弱邊緣像素。
- 邊緣分類:根據(jù)閾值,將像素分類為強邊緣像素、弱邊緣像素和非邊緣像素。一般來說,高于高閾值的像素被認(rèn)為是強邊緣,低于低閾值的像素被認(rèn)為是非邊緣,介于兩者之間的像素被認(rèn)為是弱邊緣。
- 邊緣連接:對于弱邊緣像素,如果其與強邊緣像素相鄰,則將其重新分類為強邊緣。這可以通過邊緣跟蹤算法(如霍夫變換或連通組件分析)來實現(xiàn)。
通過雙閾值處理,可以獲得更準(zhǔn)確的邊緣圖像,強邊緣像素代表明顯的邊界,而弱邊緣像素代表可能的邊界。這為后續(xù)的邊緣連接和過濾提供了更好的基礎(chǔ)。
雙閾值,即設(shè)定一個閾值上界和閾值下界(通常由人為指定的),圖像中的像素點如果大于閾值上界則認(rèn)為必然是邊界(稱為強邊界,strong edge),小于閾值下界則認(rèn)為必然不是邊界,兩者之間的則認(rèn)為是候選項(稱為弱邊界,weak edge),需進(jìn)行進(jìn)一步處理。我查閱資料,了解到上界一般是下界的2-3倍,實現(xiàn)出來的效果比較好。在Canny算法中,對于弱邊緣像素,通常采用邊緣跟蹤(如霍夫變換或連通組件分析)來連接它們與強邊緣像素,形成連續(xù)的邊界線段。
雙閾值技術(shù)
代碼實現(xiàn)
/****************************************************************************************************
* 應(yīng)用雙閾值技術(shù)將梯度幅值圖像轉(zhuǎn)換為邊界圖像
*
* @param G 梯度幅值數(shù)組,二維數(shù)組,大小為 PICTURE_H × PICTURE_W
* @param edges 邊界圖像數(shù)組,二維數(shù)組,大小為 PICTURE_H × PICTURE_W
* @param high_threshold 高閾值,用于判斷強邊界的閾值
* @param low_threshold 低閾值,用于判斷強邊界和弱邊界的閾值
****************************************************************************************************/
void apply_double_threshold(int16 G[PICTURE_H][PICTURE_W], uint8 edges[PICTURE_H][PICTURE_W], int16 high_threshold, int16 low_threshold)
{
for (int i = 0; i < PICTURE_H; i++)
{
for (int j = 0; j < PICTURE_W; j++)
{
if (G[i][j] >= high_threshold)
{
// 像素強度大于等于高閾值,被認(rèn)為是強邊界
edges[i][j] = 255; // 白色
}
else if (G[i][j] >= low_threshold)
{
// 像素強度在高閾值和低閾值之間,被認(rèn)為是弱邊界
edges[i][j] = 128; // 灰色
}
else
{
// 像素強度小于低閾值,被認(rèn)為是非邊界
edges[i][j] = 0; // 黑色
}
}
}
}
這個函數(shù)接受梯度幅值數(shù)組 G、輸出的邊界數(shù)組 edges,以及高閾值 high_threshold 和低閾值 low_threshold 作為參數(shù)。它遍歷每個像素,根據(jù)其梯度幅值將其分類為強邊界、弱邊界或非邊界,并在邊界數(shù)組中相應(yīng)地設(shè)置像素值。
基于八鄰域的邊緣跟蹤算法
代碼實現(xiàn)
/****************************************************************************************************
* 使用滯后邊界跟蹤對弱邊界進(jìn)行二次判斷
*
* @param edges 邊界圖像數(shù)組,二維數(shù)組,大小為 PICTURE_H × PICTURE_W
* @param high_threshold 高閾值,用于判斷強邊界的閾值
* @param low_threshold 低閾值,用于判斷強邊界和弱邊界的閾值
****************************************************************************************************/
void apply_hysteresis_threshold(uint8 edges[PICTURE_H][PICTURE_W], int16 high_threshold, int16 low_threshold)
{
for (int i = 1; i < PICTURE_H - 1; i++)
{
for (int j = 1; j < PICTURE_W - 1; j++)
{
if (edges[i][j] == 128) // 如果當(dāng)前像素為弱邊界
{
// 檢查當(dāng)前像素周圍的8個像素
if (edges[i - 1][j - 1] == 255 || edges[i - 1][j] == 255 || edges[i - 1][j + 1] == 255 ||
edges[i][j - 1] == 255 || edges[i][j + 1] == 255 ||
edges[i + 1][j - 1] == 255 || edges[i + 1][j] == 255 || edges[i + 1][j + 1] == 255)
{
// 如果周圍任意一個像素是強邊界,則將當(dāng)前像素標(biāo)記為強邊界
edges[i][j] = 255;
}
else
{
// 否則將當(dāng)前像素標(biāo)記為非邊界
edges[i][j] = 0;
}
}
}
}
}
雙閾值+邊緣跟蹤
使用雙閾值后再調(diào)用邊緣跟蹤,遍歷兩次似乎效率太低,所以我們可以將這兩個算法合成一個函數(shù)。
代碼實現(xiàn)
/****************************************************************************************************
* 應(yīng)用雙閾值技術(shù)將梯度幅值圖像轉(zhuǎn)換為邊界圖像,并使用邊緣跟蹤算法處理弱邊界
*
* @param G 梯度幅值數(shù)組,二維數(shù)組,大小為 PICTURE_H × PICTURE_W
* @param edges 邊界圖像數(shù)組,二維數(shù)組,大小為 PICTURE_H × PICTURE_W
* @param high_threshold 高閾值,用于判斷強邊界的閾值
* @param low_threshold 低閾值,用于判斷強邊界和弱邊界的閾值
****************************************************************************************************/
void apply_double_threshold(int16 G[PICTURE_H][PICTURE_W], uint8 edges[PICTURE_H][PICTURE_W], int16 high_threshold, int16 low_threshold)
{
for (int i = 0; i < PICTURE_H-1; i++)
{
for (int j = 0; j < PICTURE_W-1; j++)
{
if (G[i][j] >= high_threshold)
{
// 像素強度大于等于高閾值,被認(rèn)為是強邊界
edges[i][j] = 255; // 白色
}
else if (G[i][j] >= low_threshold)
{
// 像素強度在高閾值和低閾值之間,被認(rèn)為是弱邊界
// 檢查當(dāng)前像素周圍的8個像素
if (edges[i - 1][j - 1] == 255 || edges[i - 1][j] == 255 || edges[i - 1][j + 1] == 255 ||
edges[i][j - 1] == 255 || edges[i][j + 1] == 255 ||
edges[i + 1][j - 1] == 255 || edges[i + 1][j] == 255 || edges[i + 1][j + 1] == 255)
{
// 如果周圍任意一個像素是強邊界,則將當(dāng)前像素標(biāo)記為強邊界
edges[i][j] = 255;
}
else
{
// 否則將當(dāng)前像素標(biāo)記為非邊界
edges[i][j] = 0;
}
}
else
{
// 像素強度小于低閾值,被認(rèn)為是非邊界
edges[i][j] = 0; // 黑色
}
}
}
}
在上述代碼中,白色方塊代表存在邊緣(包括強弱邊緣),遍歷弱邊緣中的每個像素,如果像素的八鄰域存在強邊緣對應(yīng)的像素,則將這個弱邊緣像素歸為真正的邊緣(從視覺上來理解,就是存在一條不確定的邊緣,如果這條不確定的邊緣旁存在真正的邊緣,則將這條邊歸為真邊,非則就將其刪除)
調(diào)用結(jié)果
封裝
最后我們可以將以上代碼封裝成一個Canny函數(shù)。文章來源:http://www.zghlxwxcb.cn/news/detail-859416.html
其實讀者們依次將筆者上述的所有函數(shù)復(fù)制并封裝到源文件和頭文件中使用即可
由于筆者比較懶,所以懶得封裝了。
后續(xù)有空的時候,筆者會將所有內(nèi)容封裝成一個壓縮包,放到付費文件里。
最后會將連接更新在文章的開頭。
如果覺得這篇文章寫得不錯的話,不妨點贊并收藏,謝謝閱讀。
參考文獻(xiàn)
Canny算子的原理參考了這篇文章:計算機(jī)視覺中Canny算子詳解。在本篇文章中筆者僅參考了計算公式和整體思路。C語言代碼純靠手搓。文章來源地址http://www.zghlxwxcb.cn/news/detail-859416.html
到了這里,關(guān)于Canny算子邊緣檢測原理講解及其完整C語言實現(xiàn)(不使用opencv)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!