导航索引帖
前置文章,课设第三篇
上一章的主要完成了灰度化、二值化、3×3均值滤波三种图像处理功能并介绍了图像处理原理,这一章将开始进行伽马变换和边缘检测处理,到这章为止,课设必做部分基本功能就已经全部完成了,恭喜跟着一起做下来的各位。
用通俗的话来说,伽马变换就是,利用图像处理手段改变图像曝光度,难度并不大。
伽玛变换的表达式 Γ ( S ) = c ∗ r γ \Gamma(S)=c*r^γ Γ(S)=c∗rγ
其中r为输入的灰度值,[0,1],C为灰度缩放系数,γ 为伽玛因子大小, Γ ( S ) \Gamma(S) Γ(S)为经过变换以后的灰度值。
对于伽马变换的具体趋势示意图网上有很多,我这里借用一下他人的给大家看一下。
从这个示意图可以容易得知,
● γ >1时,图像的高灰度区城对比度得到增强,图像看起来变暗了;
● γ <1时,图像的低灰度区域对比度得到增强,图像看起来变亮了;
● γ =1时,这一灰度变换是线性的,即不改变原图像。
伽马变换的代码实现思路简单来说就是通过获取每个像素点的当前颜色RGB值,将其R、G、B三个值分别作γ 次方后将值放回原处生成图像。
//伽马变换
QImage MainWindow::Gamma(QImage image,int value){
QImage GammaImage =image.convertToFormat(QImage::Format_ARGB32);
QColor oldColor;
for(int y = 0; y < image.height(); y++)
{
for(int x = 0; x < image.width(); x++)
{
oldColor = QColor(image.pixel(x,y));
double red=oldColor.red();
double green=oldColor.green();
double blue=oldColor.blue();
int r=qBound(0,(int)pow(red,value),255);
int g=qBound(0,(int)pow(green,value),255);
int b=qBound(0,(int)pow(blue,value),255);
GammaImage.setPixel(x,y, qRgb(r, g, b));
}
}
return GammaImage;
}
这里用了一个qBound函数来防止做完乘方后的rgb值过大造成的溢出。
qBound(min,mid,max)函数用于取三个参数的中间值。
要注意的是,qBound函数要求三个参数都必须是const对象,所以不能直接将变量对象放入。
这里我贴一段QT中帮助文档对qBound函数的解释。
const T &qBound(const T &min, const T &val, const T &max)
Returns val bounded by min and max. This is equivalent to qMax(min, qMin(val, max)).
有过前面写二值化调节的经历,这边其实是一模一样的操作,包括代码部分也只需要修改图像处理函数就行。
需要注意的是伽马变换的伽马因子区域范围不建议过大,我这里用的maximum值为25。
//调节条伽马变换
void MainWindow::on_GammaSlider_valueChanged(int value)
{
if(srcDirPathList.isEmpty()){
QMessageBox::information(this,tr("请先选择图片"),
tr("请先选择图片!"));
return;
}
else{
QImage image=QImage(srcDirPathList.at(imagenum));//读取当前图片
QImage GammaImage=Gamma(image,value);//伽马变换
ui->GammaLineEdit->setText(QString::number(value));//改变文本框内值为伽马因子
ui->piclabel->setPixmap(QPixmap::fromImage(ImageSetSize(GammaImage,ui->piclabel)));//显示伽马变换图像
}
}
//文本框伽马变换
void MainWindow::on_GammaLineEdit_textChanged(const QString &arg1)
{
if(srcDirPathList.isEmpty()){
QMessageBox::information(this,tr("请先选择图片"),
tr("请先选择图片!"));
return;
}
else{
int value=arg1.toInt();
if (value>=0 && value<=25)
{
QImage image=QImage(srcDirPathList.at(imagenum));
QImage GammaImage=Gamma(image,value);//都是和上面一样的
ui->GammaSlider->setValue(value);//当文本框内数值改变时,动态变化调节条位置
ui->piclabel->setPixmap(QPixmap::fromImage(ImageSetSize(GammaImage,ui->piclabel)));
}
else
{
QMessageBox::information(this,tr("请输入正确数值"),
tr("请输入0-25!"));
return;
}
}
}
代码部分也是几乎没有差别,这里就直接贴上来,不作解释了,如果有疑问的可以评论或者私聊找我。
边缘检测这块相对而言也比较复杂,通常使用边缘滤波器,这些滤波器通过寻找较亮和较暗的区域边界像素点的方式提取边缘,滤波器寻找图像中梯度变化明显的部分。这些梯度一般描述为边缘的振幅和方向。将边缘振幅高的所有像素选择出来,就完成了区域的边缘轮廓提取。
常用的算子主要有Sobel算子、Laplace算子、Canny算子;详细的分析可以回头在其他帖中详述,链接之后会补充在这里。
但应该不太会全都去尝试实现了。。。。。
这里我们将用到Canny算子进行边缘检测。
Canny的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是:
好的检测 - 算法能够尽可能多地标识出图像中的实际边缘。
好的定位 - 标识出的边缘要与实际图像中的实际边缘尽可能接近。
最小响应 - 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。
为了满足这些要求Canny使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测使用四个指数函数项的和表示,但是它非常近似于高斯函数的一阶导数。
因此,Canny算法进行边缘检测需要先进行灰度处理、高斯平滑滤波、计算梯度强度和方向、非极大值抑制、双阈值检测、抑制孤立低阈值点等步骤,这里这样看可能会比较难看懂,我会将这部分内容细分小标题,暂时看不懂的同学可以先跳转到实操部分。
灰度图像前面已经处理过了,不需要多描述了
这一步是对原始数据与高斯滤波器做卷积,得到的图像与原始图像相比有些轻微的模糊。使得单独的一个像素噪声在经过高斯平滑的图像上变得几乎没有影响。
若图像中一个33的窗口为A,要滤波的像素点为e,则经过高斯滤波后,像素点e的亮度值为:
其中为卷积符号,sum表示矩阵中所有元素相加求和。
【注1】:高斯滤波器是一个模为奇数的矩阵,大小为(2k+1)∗(2k+1)的高斯滤波器的生成方程式如下:
图像中的边缘可以指向各个方向,因此Canny算法使用四个算子来检测图像中的水平、垂直和对角边缘。边缘检测的算子(如Roberts,Prewitt,Sobel等)返回水平Gx和垂直Gy方向的一阶导数值,由此便可以确定像素点的梯度G和方向theta 。
G= G x 2 + G y 2 \sqrt{G_x^2+G_y^2} Gx2+Gy2
其中G为梯度强度,θ表示梯度方向,arctan为反正切函数。下面以Sobel算子为例讲述如何计算梯度强度和方向。
x和y方向的Sobel算子分别为
其中Sx表示x方向的Sobel算子,用于检测y方向的边缘; Sy表示y方向的Sobel算子,用于检测x方向的边缘。
若图像中一个3x3的窗口为A,要计算梯度的像素点为e,则和Sobel算子进行卷积之后,像素点e在x和y方向的梯度值分别为:
这样我们就从原始图像生成了图像中每个点亮度梯度图以及亮度梯度的方向。
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的梯度线性插值为:
选择两个阈值,即高阈值和低阈值。
如果边缘像素的梯度值高于高阈值,则标记为强边缘像素;
如果边缘像素的梯度值高于低阈值,低于高阈值,则标记为弱边缘像素;
如果边缘像素的梯度值低于低阈值,则抑制该像素。
强边缘像素一定是真实边缘;
若边缘像素中周围八连通的像素中如果存在强边缘像素,则保留为真实边缘;
否则,抑制该像素。
//高斯平滑滤波器
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;
}
}
}
首先准备一个用于抑制部分噪声的滤波器(符合正态分布的噪声)
//高斯滤波
QImage MainWindow::guass(QImage grayimg){
QImage guassimg;
int k=5;//高斯滤波器规模
double kernel[10][10];
generic_guess(kernel,k,1.4);//设置好高斯滤波器
//t_img用于将灰度图填充到长款都加上高斯滤波器的尺寸大小
QImage * t_img=new QImage(grayimg.width()+k-1,grayimg.height()+k-1,QImage::Format_ARGB32);
guassimg=QImage(grayimg.width(),grayimg.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,grayimg.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,grayimg.pixel(grayimg.width()-1,0));
}
}
//rightbottom
for(int i=grayimg.width()+k/2;i<t_img->width();++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i,j,grayimg.pixel(grayimg.width()-1,grayimg.height()-1));
}
}
//leftbottom
for(int i=0;i<k/2;++i){
for(int j=grayimg.height()+k/2;j<t_img->height();++j){
t_img->setPixel(i,j,grayimg.pixel(0,grayimg.height()-1));
}
}
//top
for(int i=0;i<grayimg.width();++i){
for(int j=0;j<k/2;++j){
t_img->setPixel(i+k/2,j,grayimg.pixel(i,0));
}
}
//right
for(int i=t_img->width()-k/2;i<t_img->width();++i){
for(int j=0;j<grayimg.height();++j){
t_img->setPixel(i,j+k/2,grayimg.pixel(i,grayimg.height()-1));
}
}
//left
for(int i=0;i<k/2;++i){
for(int j=0;j<grayimg.height();++j){
t_img->setPixel(i,j+k/2,grayimg.pixel(0,j));
}
}
for(int i=0;i<grayimg.width();++i){
for(int j=0;j<grayimg.height();++j){
t_img->setPixel(i+k/2,j+k/2,grayimg.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));
}
}
guassimg.setPixel(i-k/2,j-k/2,qRgb(temp,temp,temp));
}
}
return guassimg;
}
通过高斯平滑滤波器,将原图像的灰度图像做出高斯模糊作用,对此处高斯滤波器感兴趣的同学可以尝试把guassimg输出到Label观看。
原图为这只鹅的时候,高斯滤波图像呈现出的效果为下图鹅,可以较只管看出,鹅的灰度图变得相对模糊了。
这一步的目的在于去除尖锐噪点,即降低明显噪点,将图像边缘线中更为明显的部分提取出来。
QImage MainWindow::calculate(QImage img,QImage img_gray,QImage img_guass){
}
后续部分的算法都写在这个函数内
//计算梯度强度和方向
double * gradx[img.width()];
for(int i=0;i<img.width();++i)
gradx[i]=new double[img.height()];
double * grady[img.width()];
for(int i=0;i<img.width();++i)
grady[i]=new double[img.height()];
double * grad[img.width()];
for(int i=0;i<img.width();++i)
grad[i]=new double[img.height()];
double * dir[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;
}
}
这一部分算法内容本身没有输出图像,是获取原始图像中亮度梯度图以便于检测边缘,具体原理需要看上文,如果不深究原理的话,在模仿代码的时候请注意代码中变量名等不要搞错,这里的代码相对比较复杂,请仔细检查。
//非极大值抑制图像制作
QImage img_nms=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();
double highthresh=temp;
double 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,qRgb(255,255,255));//black边缘
// gradcp[i][j]=highthresh;
}
else{
img_nms.setPixel(i,j,qRgb(0,0,0));//white
grad[i][j]=0;
}
}
}
通过多个方向的颜色亮度梯度比对来初步判断当前像素点是否为边缘,这里用了N,NE,E,SW,W,SE,S,NW共八个方向,八个方向相对存在亮度梯度变换较大的像素点则会被认为是边缘。
详细原理需要看上文理解,但结合代码查看相对而言也不难理解。
在处理后可以将图像 img_nms 取出查看。
这个时候可以发现,图像已经产生了初步的大鹅边缘雏形,但总体来说图像中噪点比较多,看起来并不清晰。
//双阈值检测
QImage img_dt= QImage(img_nms.width(),img_nms.height(),QImage::Format_ARGB32);
double * gradcp[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,qRgb(255,255,255));
}
else if(grad[i][j]>lowthresh){//弱边缘
gradcp[i][j]=lowthresh;
img_dt.setPixel(i,j,qRgb(255,255,255));
}
else{//非边缘
gradcp[i][j]=0;
img_dt.setPixel(i,j,qRgb(0,0,0));//抑制
}
}
}
对于刚才的图像,在这里进行检查。
选择两个阈值,分别为高阈值和低阈值。
如果边缘像素的梯度值高于高阈值,则标记为强边缘像素;
如果边缘像素的梯度值高于低阈值,低于高阈值,则标记为弱边缘像素;
如果边缘像素的梯度值低于低阈值,则抑制该像素。
在这样处理之后,除了相对明显的边缘线外将会被处理掉。
效果如下图所示:
QImage img_st=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,qRgb(255,255,255));
}
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,qRgb(255,255,255));//边缘
break;
}
img_st.setPixel(i,j,qRgb(0,0,0));//非边缘
}
}
else//非边缘
img_st.setPixel(i,j,qRgb(0,0,0));
}
}
这一步的目的在于判断是否存在孤立弱边缘标记点,如果存在这样的标记点,我们需要将这个点判断成非边缘点,以达到进一步降噪目的。
这里的大鹅由于本身边缘清晰,几乎不存在弱边缘标记点,所以与上一步对比效果不明显,感兴趣的同学可以自己尝试对比其他边缘线。
最后,别忘记返回最后的图像以便于输出。
return img_st;
void MainWindow::on_SideBtn_clicked()
{
if(srcDirPathList.isEmpty())
{
QMessageBox::information(this,tr("请先选择图片"),
tr("请先选择图片!"));
return;
}
else{
QImage image=QImage(srcDirPathList.at(imagenum));
QImage grayimage=gray(image);
QImage guassimage=guass(image);
QImage stimage=calculate(image,grayimage,guassimage);
ui->piclabel->setPixmap(QPixmap::fromImage(ImageSetSize(stimage,ui->piclabel)));
}
}
按钮事件本身应该在前面几章出现了很多次了,没什么好解释的了吧。
至此为止,基础项图像处理功能要求已经全部完成了。