一边缘检测
边缘检测的一般步骤:
[1]滤波
边缘检测的算法主要基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感。常见的滤波方法有高斯滤波。
[2]增强
增强边缘的基础是确定图像各点邻域强度的变化值。具体编程实现时,可通过计算梯度幅值来确定。
[3]检测
通过设定阈值,找到特定梯度的边缘点
1,Canny
1>canny边缘检测步骤
[1]消除噪声
一般使用高斯平滑滤波器卷积降噪
[2]计算梯度幅值和方向(调用Sobel())
Gx,Gy分别为作用于x,y方向卷积
梯度方向可能为0,45,90,135度
[3]非极大值抑制
排除非边缘像素,仅仅保留细线条
[4]滞后阈值
为高阈值和低阈值
若高于高阈值,则保留,低于低阈值,则舍弃,在两者中间,则仅仅连接到高于高阈值的像素时被保留
2>
void Canny(InputArray image, OutputArray edges,
double threshold1, double threshold2,
int apertureSize=3, bool L2gradient=false )
//image,单通道8-bit
//threshold1,threshold2分别为滞后过程的第一个阈值和第二个阈值,
//两者的比例一般为1:3或1:2
//apertureSize,Sobel()操作的孔径大小
//L2gradient,为true时,调用L2norm=sqrt((dI/dx)^2+(dI/dy)^2)计算
//(精度高),false时,调用L1norm=|dI/dx|+|dI/dy|(精度低)
使用步骤:
//先进行滤波
cv::Size kel_size(3,3);
cv::blur(Image,result,kel_size);
//保证threshold1/threshold2=1/3,result为单通道8-bit
cv::Canny(result,result,25,25*3,3);
效果图:
2,Sobel
1>Sobel边缘检测步骤
[1]分别在x和y方向求导
水平变化:将图像I与奇数大小的核卷积,exp,
垂直变化:将图像I与奇数大小的核卷积,exp,
[2]在图像的每一点,结合以上两个结果求出近似梯度
也可近似为:
2>
void Sobel(InputArray src, OutputArray dst, int ddepth,
int dx, int dy, int ksize=3, double scale=1,
double delta=0, int borderType=BORDER_DEFAULT )
/*ddepth(输出图像的深度):
*src.depth() = CV_8U, ddepth = -1/CV_16S/CV_32F/CV_64F
*src.depth() = CV_16U/CV_16S, ddepth = -1/CV_32F/CV_64F
*src.depth() = CV_32F, ddepth = -1/CV_32F/CV_64F
*src.depth() = CV_64F, ddepth = -1/CV_64F
*dx,为x方向的差分阶数,dy,为y方向的差分阶数
*ksize,卷积核大小,只能为1,3,5,7
*scale,计算导数时可选的缩放因子,默认为1,表示没有缩放
*delta,表示存入dst之前,可选的delta
*/
当ksize=3时,由于取近似,Sobel的误差较大,而Scharr仅仅作用于ksize=3时,而且效果更加精确,更快,它的运算核为:
Sobel应用步骤:
//Image为灰度图
cv::Mat ImageX;
//求X方向梯度,目标图深度为CV_16S cv::Sobel(Image,ImageX,CV_16S,1,0,3,1,1,cv::BORDER_DEFAULT);
//求ImageX每个元素的绝对值,并且转化为8-bit
cv::convertScaleAbs(ImageX,ImageX);
cv::Mat ImageY;
//求Y方向梯度,目标图深度为CV_16S
cv::Sobel(Image,ImageY,CV_16S,0,1,3,1,1,cv::BORDER_DEFAULT);
//求ImageY每个元素的绝对值,并且转化为8-bit
cv::convertScaleAbs(ImageY,ImageY);
//两梯度的近似绝对值相加
cv::addWeighted(ImageX,0.5,ImageY,0.5,0,result);
效果图:
3,Scharr
Scharr检测步骤,与Sobel相同,运算核子为(只有这一种情况):
void Scharr(InputArray src, OutputArray dst, int ddepth,
int dx, int dy, double scale=1, double delta=0,
int borderType=BORDER_DEFAULT )
//ddepth,见Sobel()
//dx,为x方向的差分阶数,dy,为y方向的差分阶数
//scale,计算导数时可选的缩放因子,默认为1,表示没有缩放
//delta,表示存入dst之前,可选的delta
Scharr应用步骤:
//Image为灰度图
cv::Mat ImageX;
//ImageX深度为CV_16S
cv::Scharr(Image,ImageX,CV_16S,1,0);
//求ImageX每个元素的绝对值,并且转化为8-bit
cv::convertScaleAbs(ImageX,ImageX);
cv::Mat ImageY;
//ImageY深度为CV_16S
cv::Scharr(Image,ImageY,CV_16S,0,1);
//求ImageY每个元素的绝对值,并且转化为8-bit
cv::convertScaleAbs(ImageY,ImageY);
//两个方向梯度绝对值,近似相加
cv::addWeighted(ImageX,0.5,ImageY,0.5,0,result);
效果图:
4,Laplace
Laplace算子的定义:
内部代码调用Sobel
void Laplacian(InputArray src, OutputArray dst, int ddepth,
int ksize=1, double scale=1, double delta=0,
int borderType=BORDER_DEFAULT )
//ddepth目标图像的深度
//ksize,计算二阶导数的核大小,必须为正奇数
//scale,影响因子,默认为1表示无影响
//delta,表示存入目标图之前,可选的delta值
当ksize=1时,Laplacian采用3*3的核
Laplace应用步骤:
[1]得到laplace图像
//转化成灰度图
cv::cvtColor(image,imageGray,CV_BGR2GRAY);
//得到laplace,CV_32F深度
cv::Laplacian(imageGray,laplace,CV_32F,aperture);
//缩放因子
if(scale<0)
{
double lapmin,lapmax;
cv::minMaxLoc(laplace,&lapmin,&lapmax);
scale=127/std::max(-lapmin,lapmax);
}
//深度转换
cv::Mat laplaceImage;
laplace.convertTo(laplaceImage,CV_8U,scale,128);
[2]边缘显示
在[1]得到的laplace有的点为正,有的点为负,通过查找laplace过零点的曲线得到边缘
cv::Mat getZeroCrossings(cv::Mat laplace)
{
//阈值为0,负数为黑色,正数用白色
cv::Mat signImage;
cv::threshold(laplace,signImage,0,255,cv::THRESH_BINARY);
//深度转化
cv::Mat binary;
signImage.convertTo(binary,CV_8U);
//对图像进行膨胀
cv::Mat dilated;
cv::dilate(binary,dilated,cv::Mat());
//返回过零点的轮廓
return dilated-binary;
}
效果图:
5,高斯差分
高斯滤波器可以提取图像的低频部分,用两个不同带宽的高斯滤波器对一个图像做滤波,然后用这两个图像相减,得到较高频率构成的图像,这些频率被一个滤波器保留,被另一个滤波器丢弃,这种运算叫做高斯差分
cv::Mat gauss20;
cv::GaussianBlur(image,gauss20,cv::Size(),2.0);
cv::Mat gauss22;
cv::GaussianBlur(image,gauss22,cv::Size(),2.2);
//计算高斯差分
cv::Mat dog;
cv::subtract(gauss20,gauss22,dog,cv::Mat(),CV_32F);
//计算完的高斯差分图像像素值,有正有负,通过查找过零点曲线
//得到边缘
cv::Mat zero=getZeroCrossings(dog);
效果图:
二霍夫变换
1,霍夫线变换
1>原理
[1]一条直线可以有两种表达形式
a,在笛卡尔坐标系: 可由斜率和截距表示(m,b)
b,在极坐标系:可由极径和极角表示(r, θ)
直线可以表示为:
化简为:
[2]对于点(x 0 , y 0 ),我们可以认为
每一对(r θ , θ)代表通过(x 0 , y 0 )的直线
[3]如果给定一点,在极坐标平面绘制出所有通过它的平面,得到正弦曲线
exp:x0=8,y0=6
[4]可以对图像中所有点进行上述操作,如果两个不同点进行上述操作后得到的曲线在极坐标平面内相交,则意味着它们通过同一条直线
表明这三点组成一条直线
[5]一条直线通过在极坐标平面交于一点的曲线数量来检测。我们可以设定阈值,来检测直线
2>标准霍夫变换
void HoughLines(InputArray image, OutputArray lines,
double rho, double theta, int threshold,
double srn=0, double stn=0 )
/*image,单通道8-bit,二值图像
*lines,存储线的vector,存储了两元素vector, (r, θ),用vector<Vec2f>
*rho直线搜索时,进步尺寸的单位半径
*theta,以弧度为单位,直线搜索时,进步尺寸的单位角度
*threshold,阈值,只有交点大于threshold时,才可以输出
*srn,对于多尺度霍夫变换,srn是rho的除数距离,当src=0时,调用经典霍夫变
*换,进步尺寸为rho,srn>0时,为rho/srn
*stn,与srn相同,应用于theta
*srn,stn应该同时为0,或者同时为正
*/
使用步骤:
//Image为灰度图,进行Canny边缘检测,将result转化成BGR用于画线
cv::GaussianBlur(Image,Image,cv::Size(5,5),0);
cv::Canny(Image,Image,50,150,3);
cv::cvtColor(Image,result,CV_GRAY2BGR);
//进行霍夫线变换
std::vector<cv::Vec2f> lines;
cv::HoughLines(Image,lines,1.5,CV_PI/180,150);
//将得到的线进行标记
for(int i=0;i<lines.size();i++)
{
//将(r, θ),转化成直角坐标
float rho=lines[i][0],theta=lines[i][1];
cv::Point pt1,pt2;
double a=std::cos(theta),b=std::sin(theta);
double x=a*rho,y=b*rho;
pt1.x=cvRound(x+1000*(-b));
pt1.y=cvRound(y+1000*(a));
pt2.x=cvRound(x-1000*(-b));
pt2.y=cvRound(y-1000*(a));
cv::line(result,pt1,pt2,cv::Scalar(55,100,195),1,CV_AA);
}
效果图:
2>累计概率霍夫变换
void HoughLinesP(InputArray image, OutputArray lines,
double rho, double theta, int threshold,
double minLineLength=0, double maxLineGap=0 )
//image,单通道8-bit,二值图像
//lines,线的输出向量,有四元素,(x0,y0,x1,y1),分别为线段的起始点和结束
//点,用vector<Vec4i>
//rho直线搜索时,进步尺寸的单位半径
//theta,以弧度为单位,直线搜索时,进步尺寸的单位角度
//threshold,阈值,只有交点大于threshold时,才可以输出
//minLineLength,输出线段的最短距离
//maxLineGap,允许同一行点与点连接起来的最大距离
使用步骤:
//Image为灰度图,进行Canny边缘检测,将result转化成BGR用于画线
cv::GaussianBlur(Image,Image,cv::Size(5,5),0);
cv::Canny(Image,Image,50,150,3); cv::cvtColor(Image,result,CV_GRAY2BGR);
//进行霍夫线变换
std::vector<cv::Vec4i> lines;
cv::HoughLinesP(Image,lines,1,CV_PI/180,80,50,10);
//画线
for(int i=0;i<lines.size();i++)
{
cv::Vec4i l=lines[i];
cv::line(result,cv::Point(l[0],l[1]),
cv::Point(l[2],l[3]),cv::Scalar(0,0,255),2,CV_AA);
}
效果图:
2,霍夫圆变换
原理:
[1]先对图像应用边缘检测
[2]对图像上每一个非0点考虑局部梯度,利用Sobel(),计算x和y方向的Sobel一阶导数得到梯度
[3]利用得到的梯度,由斜率指定直线上的每一个点都在累加器中累加,斜率为从一个指定最小值到指定的最大值的距离
[4]同时,标记边缘图像中每一个非0像素的位置
[5]从二维累加器中选择候选点的中心,这些中心都大于给定阈值并且大于其所有近邻,这些候选中心按照累加值降序排列
[6]对每一中心考虑所有非零像素
[7]像素按照,与中心的距离排序,从最大半径的最小距离算起,选择非0像素最支持的一条半径
缺点:
[1]Sobel计算局部梯度,可能会产生噪声
[2]如果有同心圆,只选择一个,如果阈值偏低,则算法执行的时间较长
[3]有同心圆,偏向于保存更大的圆
void HoughCircles(InputArray image, OutputArray circles,
int method, double dp, double minDist,
double param1=100, double param2=100,
int minRadius=0, int maxRadius=0 )
/*image为单通道8位灰度图
*circles,圆的输出矢量,包含(x,y,radius),用vector<Vec3f>
*method,只有CV_HOUGH_GRADIENT
*dp,用来检测圆心的累加器图像分辨率和输入图像之比的导数,允许创建
*一个比输入图像分辨率低的累加器,exp:dp=1,则尺寸相同,dp=2,则
*只有输入图像的一半的高度和宽度
*minDist两个圆之间的最小距离,太小,则多个相邻的圆被当成一个,太
*大,则很多圆检测不到
*param1,在CV_HOUGH_GRADIENT中代表传给Canny的高阈值,而低阈
*值为高阈值的一半
*param2,在CV_HOUGH_GRADIENT中代表圆心累加器的阈值,越大,则检
*测的圆越完美
*minRadius,maxRadius分别为圆的最小,最大半径
使用步骤:
//将result转化成BGR便于画圆
cv::cvtColor(Image,result,CV_GRAY2BGR);
cv::GaussianBlur(Image,Image,cv::Size(3,3),0);
//霍夫圆变换
std::vector<cv::Vec3f> circles;
cv::HoughCircles(Image,circles,CV_HOUGH_GRADIENT,1.5,10, 200,155);
//画圆
for(int i=0;i<circles.size();i++)
{
cv::Point center(cvRound(circles[i[0]),cvRound(circles[i][1]));
int radius=cvRound(circles[i][2]);
//先画圆心,再画半径 cv::circle(result,center,3,cv::Scalar(0,0,255),-1,8,0);
cv::circle(result,center,radius,cv::Scalar(0,0,255),3,8,0);
}
效果图:
二几何变换
1,重映射
把一幅图像中相应位置的像素放置到另一张图片指定位置的过程
(x,y)表示每个像素的位置:
g(x, y) = f(h(x, y))
g()是目标图像,f()是源图像,h(x,y)是作用于(x,y)的映射方法函数
void remap(InputArray src, OutputArray dst, InputArray map1,
InputArray map2, int interpolation,
int borderMode=BORDER_CONSTANT,
const Scalar& borderValue=Scalar())
//此函数不支持就地操作
//map1(两种可能的表达形式):
//表示点(x,y)的第一个形式
//CV_16SC2,CV_32FC1,CV_32FC2类型的X
//map2:
//当map1表示(x,y)时,不代表任何值
//CV_16UC1 , CV_32FC1类型的Y
//interpolation,resize中插值方法,在此不支持,INTER_AREA
使用方法:
//定义map1,map2
cv::Mat map_x(image.size(),CV_32FC1);
cv::Mat map_y(image.size(),CV_32FC1);
result.create(image.size(),image.type());
//确定remap的方法,为图像向中心聚拢
for(int j=0;j<image.rows;j++)
for(int i=0;i<image.cols;i++)
{
map_x.at<float>(j,i) = 2*( i - image.cols*0.25f ) + 0.5f;
map_y.at<float>(j,i) = 2*( j - image.rows*0.25f ) + 0.5f;
}
//重映射
cv::remap(image,result,map_x,map_y,CV_INTER_LINEAR);
效果图:
2,仿射变换
几何中,一个向量空间进行一次线性变换并接上一个平移,变换成另一个向量空间的过程
可以表示为:
乘以一个矩阵(线性变换)再加上一个向量(平移)的形式
可以有三种变换形式:
旋转,平移,缩放
使用2*3矩阵表示仿射变换
我们使用矩阵A和B对二维向量做变换
也可以表示为:
或者
仿射变换的求法:
[1]已知X和T,而且已知它们是有联系,接下来求M
[2]已知M和X,用T=M*X
矩阵M联系着两张图片:
我们可以通过两组三点求出变换矩阵M,之后就可以应用
1>实现变换
void warpAffine(InputArray src, OutputArray dst,
InputArray M, Size dsize,
int flags=INTER_LINEAR,
int borderMode=BORDER_CONSTANT,
const Scalar& borderValue=Scalar())
//M变换矩阵,2*3的size,旋转和缩放操作通过getRotationMatrix2D()得
//到,平移和缩放通过getAffineTransform()得到
//dsize,输出图像的尺寸
//flags,插值方法的标识符(见resize()),又增加了
//WARP_INVERSE_MAP,代表反变换(dst到src)
通过以下方式进行变换
2>计算转换矩阵
Mat getAffineTransform(InputArray src, InputArray dst)
//通过三组点得到,转换矩阵
应用:
//定义两组三对点
Point2f srcTri[3];
Point2f dstTri[3];
//确定三对点之间的转换关系
srcTri[0] = Point2f( 0,0 );
srcTri[1] = Point2f( src.cols - 1.f, 0 );
srcTri[2] = Point2f( 0, src.rows - 1.f );
dstTri[0] = Point2f( src.cols*0.0f, src.rows*0.33f );
dstTri[1] = Point2f( src.cols*0.85f, src.rows*0.25f );
dstTri[2] = Point2f( src.cols*0.15f, src.rows*0.7f );
//得到转换矩阵M
Mat warp_dst;
warp_mat = getAffineTransform( srcTri, dstTri );
//进行转换
warp_dst = Mat::zeros( src.rows, src.cols, src.type() );
warpAffine( src, warp_dst, warp_mat, warp_dst.size() );
效果图:
3>计算二维旋转矩阵
Mat getRotationMatrix2D(Point2f center, double angle,
double scale)
//center源图像的旋转中心,变换会将旋转中心映射到它自身
//angle,旋转角度,角度为正表示逆时针旋转(坐标原点为左上角)
//scale,缩放系数
通过以下公式计算:
应用:
Point center = Point( warp_dst.cols/2, warp_dst.rows/2 );
double angle = -50.0;
double scale = 0.6;
//得到旋转矩阵
rot_mat = getRotationMatrix2D( center, angle, scale );
//变换
warpAffine( warp_dst, warp_rotate_dst, rot_mat,
warp_dst.size() );
效果图:
4>invertAffineTransform
通过M,可以得到反向转换矩阵iM,这样通过dst,iM,就可以得到源图像
void invertAffineTransform(InputArray M, OutputArray iM)
//iM也为2*3的矩阵