边缘检测之Canny算法_Qt实现(C++)
Canny边缘检测算法的简单介绍
Canny的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是:
- 好的检测 - 算法能够尽可能多地标识出图像中的实际边缘。
- 好的定位 - 标识出的边缘要与实际图像中的实际边缘尽可能接近。
- 最小响应 - 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。
为了满足这些要求Canny使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测使用四个指数函数项的和表示,但是它非常近似于高斯函数的一阶导数。
Canny算法的步骤
- 1、灰度处理
- 2、降噪,使用高斯滤波器,以平滑图像,滤除噪声。
- 3、计算图像中每个像素的亮度梯度和方向。
- 4、应用非极大值抑制(Non-Maximum Suppression),以消除边缘检测带来的杂散响应。
- 5、应用双阙值检测(Double-Threshold)来确定真实的和潜在的边缘。
- 6、通过抑制孤立的弱边缘最终完成边缘检测。
Canny算法详细步骤
- 1、灰度处理
边缘检测算法处理的是灰度图像,如果是rgb图像,需要先转化为灰度图像。 - 2、高斯平滑滤波
任何边缘检测算法都不可能在未经处理的原始数据上很好的处理,所以第一步是对原始数据与高斯滤波器做卷积,得到的图像与原始图像相比有些轻微的模糊。使得单独的一个像素噪声在经过高斯平滑的图像上变得几乎没有影响。
若图像中一个3*3的窗口为A,要滤波的像素点为e,则经过高斯滤波后,像素点e的亮度值为:
其中*为卷积符号,sum表示矩阵中所有元素相加求和。
【注1】:高斯滤波器是一个模为奇数的矩阵,大小为 的高斯滤波器的生成方程式如下:
;
下面是一个 的高斯卷积核(需要归一化:即每个元素除以元素和,使最后元素和为一):
3、计算梯度强度和方向
图像中的边缘可以指向各个方向,因此Canny算法使用四个算子来检测图像中的水平、垂直和对角边缘。边缘检测的算子(如Roberts,Prewitt,Sobel等)返回水平Gx和垂直Gy方向的一阶导数值,由此便可以确定像素点的梯度G和方向theta 。
其中G为梯度强度, 表示梯度方向, 为反正切函数。下面以Sobel算子为例讲述如何计算梯度强度和方向。
x和y方向的Sobel算子分别为
其中Sx表示x方向的Sobel算子,用于检测y方向的边缘; Sy表示y方向的Sobel算子,用于检测x方向的边缘。
若图像中一个3x3的窗口为A,要计算梯度的像素点为e,则和Sobel算子进行卷积之后,像素点e在x和y方向的梯度值分别为:
这样我们就从原始图像生成了图像中每个点亮度梯度图以及亮度梯度的方向。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的梯度线性插值为:
5、双阈值检测
选择两个阈值,即高阈值和低阈值。
如果边缘像素的梯度值高于高阈值,则标记为强边缘像素;
如果边缘像素的梯度值高于低阈值,低于高阈值,则标记为弱边缘像素;
如果边缘像素的梯度值低于低阈值,则抑制该像素。6、抑制孤立低阈值点
强边缘像素一定是真实边缘;
若边缘像素中周围八连通的像素中如果存在强边缘像素,则保留为真实边缘;
否则,抑制该像素。
Canny算法详细步骤代码
- 1、灰度处理
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();
}
- 2、高斯平滑滤波
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;i<size;++i){
for(int j=0;j<size;++j){
kernel[i][j]=exp(-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;i<size;++i){
for(int j=0;j<size;++j){
kernel[i][j]/=sum;
}
}
}
void 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;i<k/2;++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i,j,img_gray->pixel(0,0));
}
}
//righttop
for(int i=0;i<k/2;++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i,j,img_gray->pixel(img_gray->width()-1,0));
}
}
//rightbottom
for(int i=img_gray->width()+k/2;i<t_img->width();++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i,j,img_gray->pixel(img_gray->width()-1,img_gray->height()-1));
}
}
//leftbottom
for(int i=0;i<k/2;++i){
for(int j=img_gray->height()+k/2;j<t_img->height();++j){
t_img->setPixel(i,j,img_gray->pixel(0,img_gray->height()-1));
}
}
//top
for(int i=0;i<img_gray->width();++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i+k/2,j,img_gray->pixel(i,0));
}
}
//right
for(int i=t_img->width()-k/2;i<t_img->width();++i){
for(int j=0;j<img_gray->height();++j){
t_img->setPixel(i,j+k/2,img_gray->pixel(img_gray->width()-1,j));
}
}
//bottom
for(int i=0;i<img_gray->width();++i){
for(int j=t_img->height()-k/2;j<t_img->height();++j){
t_img->setPixel(i+k/2,j,img_gray->pixel(i,img_gray->height()-1));
}
}
//left
for(int i=0;i<k/2;++i){
for(int j=0;j<img_gray->height();++j){
t_img->setPixel(i,j+k/2,img_gray->pixel(0,j));
}
}
for(int i=0;i<img_gray->width();++i){
for(int j=0;j<img_gray->height();++j){
t_img->setPixel(i+k/2,j+k/2,img_gray->pixel(i,j));
}
}
for(int i=k/2;i<t_img->width()-k/2;++i){
for(int j=k/2;j<t_img->height()-k/2;++j){
double temp=0;
for(int ti=0;ti<k;++ti){
for(int tj=0;tj<k;++tj){
temp+=kernel[ti][tj]*qRed(t_img->pixel(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"高斯平滑效果图");
}
- 3、计算梯度强度和方向
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;i<img.width();++i)
gradx[i]=new double[img.height()];
grady=new double*[img.width()];
for(int i=0;i<img.width();++i)
grady[i]=new double[img.height()];
grad=new double*[img.width()];
for(int i=0;i<img.width();++i)
grad[i]=new double[img.height()];
dir=new double*[img.width()];
for(int i=0;i<img.width();++i)
dir[i]=new 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;i<k/2;++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i,j,img_gray->pixel(0,0));
}
}
//righttop
for(int i=0;i<k/2;++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i,j,img_gray->pixel(img_gray->width()-1,0));
}
}
//rightbottom
for(int i=img_gray->width()+k/2;i<t_img->width();++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i,j,img_gray->pixel(img_gray->width()-1,img_gray->height()-1));
}
}
//leftbottom
for(int i=0;i<k/2;++i){
for(int j=img_gray->height()+k/2;j<t_img->height();++j){
t_img->setPixel(i,j,img_gray->pixel(0,img_gray->height()-1));
}
}
//top
for(int i=0;i<img_gray->width();++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i+k/2,j,img_gray->pixel(i,0));
}
}
//right
for(int i=t_img->width()-k/2;i<t_img->width();++i){
for(int j=0;j<img_gray->height();++j){
t_img->setPixel(i,j+k/2,img_gray->pixel(img_gray->width()-1,j));
}
}
//bottom
for(int i=0;i<img_gray->width();++i){
for(int j=t_img->height()-k/2;j<t_img->height();++j){
t_img->setPixel(i+k/2,j,img_gray->pixel(i,img_gray->height()-1));
}
}
//left
for(int i=0;i<k/2;++i){
for(int j=0;j<img_gray->height();++j){
t_img->setPixel(i,j+k/2,img_gray->pixel(0,j));
}
}
for(int i=0;i<img_gray->width();++i){
for(int j=0;j<img_gray->height();++j){
t_img->setPixel(i+k/2,j+k/2,img_gray->pixel(i,j));
}
}
for(int i=k/2;i<t_img->width()-k/2;++i){
for(int j=k/2;j<t_img->height()-k/2;++j){
double tempx=0;
double tempy=0;
for(int ti=0;ti<k;++ti){
for(int tj=0;tj<k;++tj){
tempx+=kernelx[ti][tj]*qRed(t_img->pixel(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"边缘强度和方向计算完成");
}
- 4、非极大值抑制
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;i<img_nms->width();++i){
for(int j=0;j<img_nms->height();++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;i<img_nms->width()-1;++i){
for(int j=1;j<img_nms->height();++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"非极大值抑制效果图");
}
- 5、双阈值检测
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;i<img.width();++i)
gradcp[i]=new double[img.height()];
for(int i=0;i<img_dt->width();++i){
for(int j=0;j<img_dt->height();++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"双阙值检测效果图");
}
- 6、抑制孤立低阈值点
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;i<img_st->width()-1;++i){
for(int j=1;j<img_st->height()-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算法的结果。较小的滤波器产生的模糊效果也较少,这样就可以检测较小、变化明显的细线。较大的滤波器产生的模糊效果也较多,将较大的一块图像区域涂成一个特定点的颜色值。这样带来的结果就是对于检测较大、平滑的边缘更加有用,例如彩虹的边缘。阈值
使用两个阈值比使用一个阈值更加灵活,但是它还是有阈值存在的共性问题。设置的阈值过高,可能会漏掉重要信息;阈值过低,将会把枝节信息看得很重要。很难给出一个适用于所有图像的通用阈值。目前还没有一个经过验证的实现方法。