Canny的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是:
为了满足这些要求Canny使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测使用四个指数函数项的和表示,但是它非常近似于高斯函数的一阶导数。
【注1】:高斯滤波器是一个模为奇数的矩阵,大小为 (2k+1)∗(2k+1) ( 2 k + 1 ) ∗ ( 2 k + 1 ) 的高斯滤波器的生成方程式如下:
Hij=12πσ2exp((i−(k+1))2+(j−(k+1))22σ2) H i j = 1 2 π σ 2 exp ( ( i − ( k + 1 ) ) 2 + ( j − ( k + 1 ) ) 2 2 σ 2 ) ;
1≤i,j≤(2k+1) 1 ≤ i , j ≤ ( 2 k + 1 )
下面是一个 σ=1.4,k=1 σ = 1.4 , k = 1 的高斯卷积核(需要归一化:即每个元素除以元素和,使最后元素和为一):
H=⎡⎣⎢0.09240.11920.09240.11920.15380.11920.09240.11920.0924⎤⎦⎥ H = [ 0.0924 0.1192 0.0924 0.1192 0.1538 0.1192 0.0924 0.1192 0.0924 ]
3、计算梯度强度和方向
图像中的边缘可以指向各个方向,因此Canny算法使用四个算子来检测图像中的水平、垂直和对角边缘。边缘检测的算子(如Roberts,Prewitt,Sobel等)返回水平Gx和垂直Gy方向的一阶导数值,由此便可以确定像素点的梯度G和方向theta 。
G=G2x+G2y−−−−−−−√ G = G x 2 + G y 2
θ=arctanGyGx θ = arctan G y G x
其中G为梯度强度, θ θ 表示梯度方向, arctan arctan 为反正切函数。下面以Sobel算子为例讲述如何计算梯度强度和方向。
x和y方向的Sobel算子分别为
Sx=⎡⎣⎢−1−2−1000121⎤⎦⎥ S x = [ − 1 0 1 − 2 0 2 − 1 0 1 ]
Sy=⎡⎣⎢10−120−210−1⎤⎦⎥ S y = [ 1 2 1 0 0 0 − 1 − 2 − 1 ]
其中Sx表示x方向的Sobel算子,用于检测y方向的边缘; Sy表示y方向的Sobel算子,用于检测x方向的边缘。
若图像中一个3x3的窗口为A,要计算梯度的像素点为e,则和Sobel算子进行卷积之后,像素点e在x和y方向的梯度值分别为:
Gx=Sx∗A=⎡⎣⎢−1−2−1000121⎤⎦⎥∗⎡⎣⎢adgbehcfi⎤⎦⎥=sum⎡⎣⎢−a−2d−g000c2fi⎤⎦⎥ G x = S x ∗ A = [ − 1 0 1 − 2 0 2 − 1 0 1 ] ∗ [ a b c d e f g h i ] = s u m [ − a 0 c − 2 d 0 2 f − g 0 i ]
Gy=Sy∗A=⎡⎣⎢10−120−210−1⎤⎦⎥∗⎡⎣⎢adgbehcfi⎤⎦⎥=sum⎡⎣⎢a0−g2b0−2hc0−i⎤⎦⎥ G y = S y ∗ A = [ 1 2 1 0 0 0 − 1 − 2 − 1 ] ∗ [ a b c d e f g h i ] = s u m [ a 2 b c 0 0 0 − g − 2 h − i ]
这样我们就从原始图像生成了图像中每个点亮度梯度图以及亮度梯度的方向。
4、非极大值抑制
1)将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较。
2)如果当前像素的梯度强度与另外两个像素相比最大,则该像素点保留为边缘点,否则该像素点将被抑制。
通常为了更加精确的计算,在跨越梯度方向的两个相邻像素之间使用线性插值来得到要比较的像素梯度。
如图所示,将梯度分为8个方向,分别为E、NE、N、NW、W、SW、S、SE,其中0代表0°~45°,1代表45°~90°,2代表-90°~-45°,3代表-45°~0°。像素点P的梯度方向为θ,则像素点P1和P2的梯度线性插值为:
tanθ=GyGx tan θ = G y G x
Gp1=(1−tan(θ))∗E+tan(θ)∗NE G p 1 = ( 1 − t a n ( θ ) ) ∗ E + t a n ( θ ) ∗ N E
Gp2=(1−tan(θ))∗W+tan(θ)∗SW G p 2 = ( 1 − t a n ( θ ) ) ∗ W + t a n ( θ ) ∗ S W
5、双阈值检测
选择两个阈值,即高阈值和低阈值。
如果边缘像素的梯度值高于高阈值,则标记为强边缘像素;
如果边缘像素的梯度值高于低阈值,低于高阈值,则标记为弱边缘像素;
如果边缘像素的梯度值低于低阈值,则抑制该像素。
6、抑制孤立低阈值点
强边缘像素一定是真实边缘;
若边缘像素中周围八连通的像素中如果存在强边缘像素,则保留为真实边缘;
否则,抑制该像素。
void MainWindow::on_pushButton_gray_clicked()
{
if(ui->label->text().isEmpty()){
QMessageBox::critical(this,u8"提示",u8"原图未加载");
return;
}
//img:原图
//img_gray:存储灰度处理后的图像
img_gray=new QImage(img.width(),img.height(),QImage::Format_ARGB32);
QColor old_color;
int average;
for(int i=0;i<img_gray->width();++i){
for(int j=0;j<img_gray->height();++j){
old_color=img.pixel(i,j);
average=(old_color.red()+old_color.green()+old_color.blue())/3;
img_gray->setPixel(i,j,qRgb(average,average,average));
}
}
ui->label_gray->resize(img_gray->width(),img_gray->height());
ui->label_gray->setPixmap(QPixmap::fromImage(*img_gray));
ui->label_gray1->setText(u8"灰度效果图");
this->update();
}
void generic_guess(double kernel[10][10],int size,double thelt){
double pi=acos(-1);
double sum=0;
int mk=size/2;
for(int i=0;ifor(int j=0;jexp(-sqrt(pow(i-mk,2)+pow(j-mk,2))/(2*thelt*thelt));
kernel[i][j]/=2*pi*thelt*thelt;
sum+=kernel[i][j];
}
}
for(int i=0;ifor(int j=0;jvoid MainWindow::on_pushButton_guess_clicked()
{
//img_gray:存储灰度处理后的图像
//img_guass:存储高斯平滑处理后的图像
//t_img:将灰度图填充到长宽都加上高斯滤波器的尺寸大小
if(ui->label_gray1->text().isEmpty()){
QMessageBox::critical(this,u8"提示",u8"原图未经过灰度处理");
return;
}
int k=5;//高斯滤波器规模
double kernel[10][10];
generic_guess(kernel,k,1.4);
QImage * t_img=new QImage(img_gray->width()+k-1,img_gray->height()+k-1,QImage::Format_ARGB32);
img_guass=new QImage(img_gray->width(),img_gray->height(),QImage::Format_ARGB32);
//lefttop
for(int i=0;i2;++i){
for(int j=0;j2;++j){
t_img->setPixel(i,j,img_gray->pixel(0,0));
}
}
//righttop
for(int i=0;i2;++i){
for(int j=0;j2;++j){
t_img->setPixel(i,j,img_gray->pixel(img_gray->width()-1,0));
}
}
//rightbottom
for(int i=img_gray->width()+k/2;iwidth();++i){
for(int j=0;j2;++j){
t_img->setPixel(i,j,img_gray->pixel(img_gray->width()-1,img_gray->height()-1));
}
}
//leftbottom
for(int i=0;i2;++i){
for(int j=img_gray->height()+k/2;jheight();++j){
t_img->setPixel(i,j,img_gray->pixel(0,img_gray->height()-1));
}
}
//top
for(int i=0;iwidth();++i){
for(int j=0;j2;++j){
t_img->setPixel(i+k/2,j,img_gray->pixel(i,0));
}
}
//right
for(int i=t_img->width()-k/2;iwidth();++i){
for(int j=0;jheight();++j){
t_img->setPixel(i,j+k/2,img_gray->pixel(img_gray->width()-1,j));
}
}
//bottom
for(int i=0;iwidth();++i){
for(int j=t_img->height()-k/2;jheight();++j){
t_img->setPixel(i+k/2,j,img_gray->pixel(i,img_gray->height()-1));
}
}
//left
for(int i=0;i2;++i){
for(int j=0;jheight();++j){
t_img->setPixel(i,j+k/2,img_gray->pixel(0,j));
}
}
for(int i=0;iwidth();++i){
for(int j=0;jheight();++j){
t_img->setPixel(i+k/2,j+k/2,img_gray->pixel(i,j));
}
}
for(int i=k/2;iwidth()-k/2;++i){
for(int j=k/2;jheight()-k/2;++j){
double temp=0;
for(int ti=0;tifor(int tj=0;tjpixel(i-k/2+ti,j-k/2+tj));
}
}
img_guass->setPixel(i-k/2,j-k/2,qRgb(temp,temp,temp));
}
}
ui->label_guass->resize(img_guass->width(),img_guass->height());
ui->label_guass->setPixmap(QPixmap::fromImage(*img_guass));
ui->label_guass1->setText(u8"高斯平滑效果图");
}
void MainWindow::on_pushButton_calculate_clicked()
{
if(ui->label_guass1->text().isEmpty()){
QMessageBox::critical(this,u8"提示",u8"未高斯平滑处理");
return;
}
gradx=new double*[img.width()];
for(int i=0;inew double[img.height()];
grady=new double*[img.width()];
for(int i=0;inew double[img.height()];
grad=new double*[img.width()];
for(int i=0;inew double[img.height()];
dir=new double*[img.width()];
for(int i=0;inew double[img.height()];
int k=3;//sobel算子规模
double kernelx[3][3]={-1,0,1,-2,0,2,-1,0,1};
double kernely[3][3]={1,2,1,0,0,0,-1,-2,-1};
QImage * t_img=new QImage(img_gray->width()+k-1,img_gray->height()+k-1,QImage::Format_ARGB32);
//lefttop
for(int i=0;i2;++i){
for(int j=0;j2;++j){
t_img->setPixel(i,j,img_gray->pixel(0,0));
}
}
//righttop
for(int i=0;i2;++i){
for(int j=0;j2;++j){
t_img->setPixel(i,j,img_gray->pixel(img_gray->width()-1,0));
}
}
//rightbottom
for(int i=img_gray->width()+k/2;iwidth();++i){
for(int j=0;j2;++j){
t_img->setPixel(i,j,img_gray->pixel(img_gray->width()-1,img_gray->height()-1));
}
}
//leftbottom
for(int i=0;i2;++i){
for(int j=img_gray->height()+k/2;jheight();++j){
t_img->setPixel(i,j,img_gray->pixel(0,img_gray->height()-1));
}
}
//top
for(int i=0;iwidth();++i){
for(int j=0;j2;++j){
t_img->setPixel(i+k/2,j,img_gray->pixel(i,0));
}
}
//right
for(int i=t_img->width()-k/2;iwidth();++i){
for(int j=0;jheight();++j){
t_img->setPixel(i,j+k/2,img_gray->pixel(img_gray->width()-1,j));
}
}
//bottom
for(int i=0;iwidth();++i){
for(int j=t_img->height()-k/2;jheight();++j){
t_img->setPixel(i+k/2,j,img_gray->pixel(i,img_gray->height()-1));
}
}
//left
for(int i=0;i2;++i){
for(int j=0;jheight();++j){
t_img->setPixel(i,j+k/2,img_gray->pixel(0,j));
}
}
for(int i=0;iwidth();++i){
for(int j=0;jheight();++j){
t_img->setPixel(i+k/2,j+k/2,img_gray->pixel(i,j));
}
}
for(int i=k/2;iwidth()-k/2;++i){
for(int j=k/2;jheight()-k/2;++j){
double tempx=0;
double tempy=0;
for(int ti=0;tifor(int tj=0;tjpixel(i-k/2+ti,j-k/2+tj));
tempy+=kernely[ti][tj]*qRed(t_img->pixel(i-k/2+ti,j-k/2+tj));
}
}
gradx[i-k/2][j-k/2]=tempx;
grady[i-k/2][j-k/2]=tempy;
grad[i-k/2][j-k/2]=sqrt(pow(tempx,2)+pow(tempy,2));
double theta=atan(tempy/tempx)+90;
if (theta >= 0 && theta < 45)
dir[i-k/2][j-k/2] = 2;
else if (theta >= 45 && theta < 90)
dir[i-k/2][j-k/2] = 3;
else if (theta >= 90 && theta < 135)
dir[i-k/2][j-k/2] = 0;
else
dir[i-k/2][j-k/2] = 1;
}
}
calcu=true;
QMessageBox::information(this,u8"提示",u8"边缘强度和方向计算完成");
}
void MainWindow::on_pushButton_nms_clicked()
{
if(!calcu){
QMessageBox::critical(this,u8"提示",u8"未计算边缘强度和方向");
return;
}
img_nms=new QImage(img_guass->width(),img_guass->height(),QImage::Format_ARGB32);
double temp=0;
for(int i=0;iwidth();++i){
for(int j=0;jheight();++j){
temp+=grad[i][j];
}
}
temp/=img_nms->width()*img_nms->height();
highthresh*=temp;
lowthresh*=highthresh;
double N,NE,E,SW,W,SE,S,NW;
double grad1=0,grad2=0,tantheta=0;
for(int i=1;iwidth()-1;++i){
for(int j=1;jheight();++j){
N=grad[i][j-1];
NE=grad[i+1][j-1];
E=grad[i+1][j];
SW=grad[i-1][j+1];
W=grad[i-1][j];
SE=grad[i+1][j+1];
S=grad[i][j+1];
NW=grad[i-1][j-1];
if(dir[i][j]==0){
tantheta=abs(grady[i][j]/gradx[i][j]);
grad1=E*(1-tantheta)+NE*tantheta;
grad2=W*(1-tantheta)+SW*tantheta;
}
else if(dir[i][j]==1){
tantheta=abs(gradx[i][j]/grady[i][j]);
grad1=N*(1-tantheta)+NE*tantheta;
grad2=S*(1-tantheta)+SW*tantheta;
}
else if(dir[i][j]==2){
tantheta=abs(gradx[i][j]/grady[i][j]);
grad1=N*(1-tantheta)+NW*tantheta;
grad2=S*(1-tantheta)+SE*tantheta;
}
else if(dir[i][j]==3){
tantheta=abs(grady[i][j]/gradx[i][j]);
grad1=W*(1-tantheta)+NW*tantheta;
grad2=E*(1-tantheta)+SE*tantheta;
}
else{
grad1=highthresh;
grad2=highthresh;
}
if(grad[i][j]>grad1&&grad[i][j]>grad2){
img_nms->setPixel(i,j,edge);//black边缘
// gradcp[i][j]=highthresh;
}
else{
img_nms->setPixel(i,j,notedge);//white
grad[i][j]=0;
}
}
}
ui->label_nms->resize(img_nms->width(),img_nms->height());
ui->label_nms->setPixmap(QPixmap::fromImage(*img_nms));
ui->label_nms1->setText(u8"非极大值抑制效果图");
}
void MainWindow::on_pushButton_dt_clicked()
{
if(ui->label_nms1->text().isEmpty()){
QMessageBox::critical(this,u8"提示",u8"未经过非极大值抑制");
return;
}
img_dt=new QImage(img_nms->width(),img_nms->height(),QImage::Format_ARGB32);
gradcp=new double*[img.width()];
for(int i=0;inew double[img.height()];
for(int i=0;iwidth();++i){
for(int j=0;jheight();++j){
if(grad[i][j]>highthresh){//强边缘
gradcp[i][j]=highthresh;
img_dt->setPixel(i,j,edge);
}
else if(grad[i][j]>lowthresh){//弱边缘
gradcp[i][j]=lowthresh;
img_dt->setPixel(i,j,edge);
}
else{//非边缘
gradcp[i][j]=0;
img_dt->setPixel(i,j,notedge);//抑制
}
}
}
ui->label_dt->resize(img_dt->width(),img_dt->height());
ui->label_dt->setPixmap(QPixmap::fromImage(*img_dt));
ui->label_dt1->setText(u8"双阙值检测效果图");
}
void MainWindow::on_pushButton_st_clicked()
{
if(ui->label_dt1->text().isEmpty()){
QMessageBox::critical(this,u8"提示",u8"未经过双阙值检测");
return;
}
img_st=new QImage(img_dt->width(),img_dt->height(),QImage::Format_ARGB32);
int frac[8][2]={{1,0},{1,1},{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1}};
for(int i=1;iwidth()-1;++i){
for(int j=1;jheight()-1;++j){
if(gradcp[i][j]==highthresh){//强边缘
img_st->setPixel(i,j,edge);
}
else if(gradcp[i][j]==lowthresh){//弱边缘
for(int p=0;p<8;++p){
if(gradcp[i+frac[p][0]][j+frac[p][1]]==highthresh){
img_st->setPixel(i,j,edge);//边缘
break;
}
img_st->setPixel(i,j,notedge);//非边缘
}
}
else//非边缘
img_st->setPixel(i,j,notedge);
}
}
ui->label_st->resize(img_st->width(),img_st->height());
ui->label_st->setPixmap(QPixmap::fromImage(*img_st));
ui->label_st1->setText(u8"抑制孤立弱边缘效果图");
}
高斯滤波器的大小
第一步所用的平滑滤波器将会直接影响Canny算法的结果。较小的滤波器产生的模糊效果也较少,这样就可以检测较小、变化明显的细线。较大的滤波器产生的模糊效果也较多,将较大的一块图像区域涂成一个特定点的颜色值。这样带来的结果就是对于检测较大、平滑的边缘更加有用,例如彩虹的边缘。
阈值
使用两个阈值比使用一个阈值更加灵活,但是它还是有阈值存在的共性问题。设置的阈值过高,可能会漏掉重要信息;阈值过低,将会把枝节信息看得很重要。很难给出一个适用于所有图像的通用阈值。目前还没有一个经过验证的实现方法。