void cvSmooth( const CvArr* src, CvArr* dst, int smoothtype=CV_GAUSSIAN, int param1=3, int param2=0, double param3=0, double param4=0 );
-
src
- 输入图像. dst
- 输出图像. smoothtype
-
平滑方法:
- CV_BLUR_NO_SCALE (简单不带尺度变换的模糊) - 对每个象素的 param1×param2 领域求和。如果邻域大小是变化的,可以事先利用函数 cvIntegral 计算积分图像。
- CV_BLUR (simple blur) - 对每个象素param1×param2邻域 求和并做尺度变换 1/(param1•param2).
- CV_GAUSSIAN (gaussian blur) - 对图像进行核大小为 param1×param2 的高斯卷积
- CV_MEDIAN (median blur) - 对图像进行核大小为param1×param1 的中值滤波 (i.e. 邻域是方的).
- CV_BILATERAL (双向滤波) - 应用双向 3x3 滤波,彩色 sigma=param1,空间 sigma=param2. 关于双向滤波,可参考http://www.dai.ed.ac.uk/CVonline/LOCAL_COPIES/MANDUCHI1/Bilateral_Filtering.html
-
param1
- 平滑操作的第一个参数. param2
- 平滑操作的第二个参数. 对于简单/非尺度变换的高斯模糊的情况,如果param2的值 为零,则表示其被设定为param1。 param3
- 对应高斯参数的 Gaussian sigma (标准差). 如果为零,则标准差由下面的核尺寸计算:
sigma = (n/2 - 1)*0.3 + 0.8, 其中 n=param1 对应水平核, n=param2 对应垂直核.
对小的卷积核 (3×3 to 7×7) 使用如上公式所示的标准 sigma 速度会快。如果 param3 不为零,而 param1 和 param2 为零,则核大小有 sigma 计算 (以保证足够精确的操作).
函数 cvSmooth 可使用上面任何一种方法平滑图像。每一种方法都有自己的特点以及局限。
没有缩放的图像平滑仅支持单通道图像,并且支持8位到16位的转换(与cvSobel和cvaplace相似)和32位浮点数到32位浮点数的变换格式。
简单模糊和高斯模糊支持 1- 或 3-通道, 8-比特 和 32-比特 浮点图像。这两种方法可以(in-place)方式处理图像。
中值和双向滤波工作于 1- 或 3-通道, 8-位图像,但是不能以 in-place 方式处理图像.
-
中值滤波
- 中值滤波法是一种非线性平滑技术,它将每一象素点的灰度值设置为该点某邻域窗口内的所有象素点灰度值的中值。实现方法:
- 通过从图像中的某个采样窗口取出奇数个数据进行排序
- 用排序后的中值取代要处理的数据即可
中值滤波法对消除椒盐噪音非常有效,在光学测量条纹图象的相位分析处理方法中有特殊作用,但在条纹中心分析方法中作用不大。中值滤波在图像处理中,常用于用来保护边缘信息,是经典的平滑噪声的方法
-
中值滤波原理
中值滤波是基于排序统计理论的一种能有效抑制噪声的非线性信号处理技术,中值滤波的基本原理是把数字图像或数字序列中一点的值用该点的一个拎域中各点值的中值代替,让周围的像素值接近的值,从而消除孤立的噪声点。方法是去某种结构的二维滑动模板,将板内像素按照像素值的大小进行排序,生成单调上升(或下降)的为二维数据序列。二维中值滤波输出为g(x,y)=med{f(x-k,y-l),(k,l∈W)} ,其中,f(x,y),g(x,y)分别为原始图像和处理后图像。W为二维模板,通常为2*2,3*3区域,也可以是不同的的形状,如线状,圆形,十字形,圆环形等。
-
高斯滤波
高斯滤波实质上是一种信号的滤波器,其用途是信号的平滑处理,我们知道数字图像用于后期应用,其噪声是最大的问题,由于误差会累计传递等原因,很多图像处理教材会在很早的时候介绍Gauss滤波器,用于得到信噪比SNR较高的图像(反应真实信号)。于此相关的有Gauss-Lapplace变换,其实就是为了得到较好的图像边缘,先对图像做Gauss平滑滤波,剔除噪声,然后求二阶导矢,用二阶导的过零点确定边缘,在计算时也是频域乘积=>空域卷积。
滤波器就是建立的一个数学模型,通过这个模型来将图像数据进行能量转化,能量低的就排除掉,噪声就是属于低能量部分
其实编程运算的话就是一个模板运算,拿图像的八连通区域来说,中间点的像素值就等于八连通区的像素值的均值,这样达到平滑的效果
若使用理想滤波器,会在图像中产生振铃现象。采用高斯滤波器的话,系统函数是平滑的,避免了振铃现象。
例程序:
#include <stdafx.h> #include <highgui.h> #include <cv.h> int main() { IplImage *p = cvLoadImage("D:\\1234.jpg", 1); IplImage *out = cvCreateImage(cvGetSize(p), IPL_DEPTH_8U, 3); cvNamedWindow("cv_in", 1); cvNamedWindow("cv_out", 1); cvShowImage("cv_in", p); cvWaitKey(1024); cvSmooth(p, out, CV_GAUSSIAN, 3, 3);//from head file <cv.h>. cvShowImage("cv_out", out); cvWaitKey(1024); //cvReleaseImage(&p); cvReleaseImage(&out); //加上上述两行就会报错,原因: /* 在写相关程序的时候,在程序的结尾处释放前段自定义的变量。使用函数cvReleaseImage(&pFrame);时程序在调试结束后报错, 指明是在释放内存时出错。经过看前辈们遇到的相同的问题。先总结如下: cvReleaseImage()和cvCreateImage()相对应的。在程序中如果没有“创建”就不能“释放”。 所以,如果我们使用的指针不是用cvCreateImage()创建的。 如果使用cvReleaseImage()就会出现错误。如果大家担心指针不释放会引发内存的错误。 可以使用pFrame = NULL;来释放内存。 因为cvReleaseImage()的作用就是将作为参数的那个指针设置为NULL; 如修改后,只ReleaseOUT就不会出错. */ cvDestroyAllWindows(); return 0; }
CvCapture
视频获取结构
cvCreateFileCapture
CvCapture* cvCreateFileCapture( const char* filename );
-
filename
- 视频文件名。
函数cvCreateFileCapture给指定文件中的视频流分配和初始化CvCapture结构。
当分配的结构不再使用的时候,它应该使用cvReleaseCapture函数释放掉。
cvReleaseCapture
void cvReleaseCapture( CvCapture** capture );
-
capture
- 视频获取结构指针。
函数cvReleaseCapture释放由函数cvCreateFileCapture或者cvCreateCameraCapture分配的CvCapture结构。
注:若从capture中使用cvQueryFrame获取图像指针,在releaseCapture的时候同时函数释放图像指针,用户不用再自己释放。
例程序:播放AVI文件:#include <stdafx.h> #include <highgui.h> int main() { IplImage *p; CvCapture *capture = cvCreateFileCapture("D:\\123.avi"); cvNamedWindow("Image", 1); while (1) { p = cvQueryFrame(capture); if (!p) break;//if can't load image,p is NULL,and break. cvShowImage("Image", p); char c = cvWaitKey(1024); if (c == 27) break;//if you press Esc,c will get 27. } cvReleaseCapture(&capture); cvReleaseImage(&p); cvDestroyWindow(Image); return 0; } }
在循环中还可以增加存取图像的操作来进行截图。
图像像素的获取:
// 访问图像像素.cpp : 定义控制台应用程序的入口点。 //way: //IplImage *Img = cvCreateImage(cvSize(640, 580), IPL_DEPTH_8U, 3); //1.间接访问:get*D,set*D... //2.直接访问:((*uchar)ImageData+i*Image->width)[j]=111; /* 单通道: IplImage*img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,1); int height =img->height; int width=img->width; int step =img->widthStep/sizeof(uchar); uchar* data =(uchar*)img->imageData; data[i*step+j]=111; 多通道: IplImage*img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,3); int height =img->height; int width =img->width; int step =img->widthStep/sizeof(uchar); int channels =img->nChannels; uchar* data =(uchar*)img->imageData; data[i*step+j*channels+k]=111; 如果为浮点型矩阵,而非字节型,那就把uchar改为float. */ //3.利用宏:CV_IMAGE_ELEM //单通道:CV_IMAGE_ELEM(img,uchar,i,j)=111; img为IplImage指针 //多通道:CV_IMAGE_ELEM(srcimg,uchar,i,j*3+0)=111; #include "stdafx.h" #include <highgui.h> #include <cv.h> int main() { IplImage *Image = cvCreateImage(cvSize(640,480), IPL_DEPTH_8U, 3); CvScalar color; int i, j; for (i = 0; i < 50; i++)//循环使i*j区域内的点全部变为黑色. { for (j = 50; j < 100; j++) { color = cvGet2D(Image, i, j);//get(x,y)'s color.cal[0],color.cal[1]. //printf("cal0:%d\ncal1:%d\ncal2:%d\n", color.val[0], color.val[1], color.val[2]); //若为单通道图像,只修改color.val[0]即可. color.val[0] = 0; color.val[1] = 0; color.val[2] = 0; cvSet2D(Image, i, j, color); } } cvNamedWindow("ImageA", 1); cvShowImage("ImageA", Image); cvWaitKey(0); cvDestroyAllWindows(); return 0; }
其中间接访问里,cvGetXD中固定参数为IplImage指针,之后X为几,就有几个Int类型的参数用来控制图上的坐标。
而cvSetXD中,固定参数为IplImage指针,X个Int类型的参数表示图上的坐标,以及可以将每个坐标的颜色分配修改的Scalar类。
图像的边缘检测(根据像素梯度)
#include <stdafx.h> #include <highgui.h> #include <cv.h> int main() { IplImage *Image; IplImage *out; Image = cvLoadImage("D:\\1234.jpg", 0); out = cvCreateImage(cvGetSize(Image), IPL_DEPTH_8U, 1); cvNamedWindow("ImageA", 1); cvNamedWindow("ImageB", 1); cvCanny(Image, out, 50, 75, 3); //cvCanny的第一个第二个参数均为指向单通道图片的IplImage. //第三,第四个参数为阀值,一个大一个小,应当根据图像的梯度设定. //第五个参数为canny算子3. cvShowImage("ImageB", out); cvWaitKey(0); cvDestroyAllWindows(); return 0; }
canny算子实质:如果一个像素的梯度大与上限值,则被认为是边缘像素,如果小于下限阈值,则被抛弃,那么如果该点的梯度位于两者之间,则当其与高于上限值的像素点连接时我们才保留,否则删除。另:当两个阀值等比例同时增大时(比如从50,150增加到100,300,比例并未改变),则图像上的杂点变少(边缘更加清晰)。
关于梯度的计算:
引用自:http://www.cnblogs.com/justany/archive/2012/11/23/2782660.html
图像的边缘
图像的边缘从数学上是如何表示的呢?
图像的边缘上,邻近的像素值应当显著地改变了。而在数学上,导数是表示改变快慢的一种方法。梯度值的大变预示着图像中内容的显著变化了。
用更加形象的图像来解释,假设我们有一张一维图形。下图中灰度值的“跃升”表示边缘的存在:
使用一阶微分求导我们可以更加清晰的看到边缘“跃升”的存在(这里显示为高峰值):
由此我们可以得出:边缘可以通过定位梯度值大于邻域的相素的方法找到。
卷积
卷积可以近似地表示求导运算。
那么卷积是什么呢?
卷积是在每一个图像块与某个算子(核)之间进行的运算。
核?!
核就是一个固定大小的数值数组。该数组带有一个锚点 ,一般位于数组中央。
可是这怎么运算啊?
假如你想得到图像的某个特定位置的卷积值,可用下列方法计算:
- 将核的锚点放在该特定位置的像素上,同时,核内的其他值与该像素邻域的各像素重合;
- 将核内各值与相应像素值相乘,并将乘积相加;
- 将所得结果放到与锚点对应的像素上;
- 对图像所有像素重复上述过程。
用公式表示上述过程如下:
在图像边缘的卷积怎么办呢?
计算卷积前,OpenCV通过复制源图像的边界创建虚拟像素,这样边缘的地方也有足够像素计算卷积了。
近似梯度
比如内核为3时。
首先对x方向计算近似导数:
然后对y方向计算近似导数:
然后计算梯度:
当然你也可以写成:
求梯度的例程序:
<span style="font-size:18px;">#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> using namespace cv; int main( int argc, char** argv ){ Mat src, src_gray; Mat grad; char* window_name = "求解梯度"; int scale = 1; int delta = 0; int ddepth = CV_16S; int c; src = imread( argv[1] ); if( !src.data ){ return -1; } //高斯模糊 GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT ); //转成灰度图 cvtColor( src, src_gray, CV_RGB2GRAY ); namedWindow( window_name, CV_WINDOW_AUTOSIZE ); Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_x, abs_grad_x ); Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_y, abs_grad_y ); addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad ); imshow( window_name, grad ); waitKey(0); return 0; }</span>
Sobel函数
索贝尔算子(Sobel operator)计算。
C++: 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 )
参数
- src – 输入图像。
- dst – 输出图像,与输入图像同样大小,拥有同样个数的通道。
- 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
当 ddepth为-1时, 输出图像将和输入图像有相同的深度。输入8位图像则会截取顶端的导数。
- xorder – x方向导数运算参数。
- yorder – y方向导数运算参数。
- ksize – Sobel内核的大小,可以是:1,3,5,7。
- scale – 可选的缩放导数的比例常数。
- delta – 可选的增量常数被叠加到导数中。
- borderType – 用于判断图像边界的模式。
代码注释:
//在x方向求图像近似导数 Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT ); //在y方向求图像近似导数 Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
如果我们打印上面两个输出矩阵,可以看到grad_x和grad_y中的元素有正有负。
当然,正方向递增就是正的,正方向递减则是负值。
这很重要,我们可以用来判断梯度方向。
convertScaleAbs函数
线性变换转换输入数组元素成8位无符号整型。
C++: void convertScaleAbs (InputArray src, OutputArray dst, double alpha=1, double beta=0 )
参数
- src – 输入数组。
- dst – 输出数组。
- alpha – 可选缩放比例常数。
- beta – 可选叠加到结果的常数。
对于每个输入数组的元素函数convertScaleAbs 进行三次操作依次是:缩放,得到一个绝对值,转换成无符号8位类型。
对于多通道矩阵,该函数对各通道独立处理。如果输出不是8位,将调用Mat::convertTo 方法并计算结果的绝对值,例如:
Mat_<float> A(30,30); randu(A, Scalar(-100), Scalar(100)); Mat_<float> B = A*5 + 3; B = abs(B);为了能够用图像显示,提供一个直观的图形,我们利用该方法,将-256 — 255的导数值,转成0 — 255的无符号8位类型。
addWeighted函数
计算两个矩阵的加权和。
C++: void addWeighted (InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1 )
参数
- src1 – 第一个输入数组。
- alpha – 第一个数组的加权系数。
- src2 – 第二个输入数组,必须和第一个数组拥有相同的大小和通道。
- beta – 第二个数组的加权系数。
- dst – 输出数组,和第一个数组拥有相同的大小和通道。
- gamma – 对所有和的叠加的常量。
- dtype – 输出数组中的可选的深度,当两个数组具有相同的深度,此系数可设为-1,意义等同于选择与第一个数组相同的深度。
函数addWeighted 两个数组的加权和公式如下:
在多通道情况下,每个通道是独立处理的,该函数可以被替换成一个函数表达式:
dst = src1*alpha + src2*beta + gamma;
利用convertScaleAbs和addWeighted,我们可以对梯度进行一个可以用图像显示的近似表达。
这样我们就可以得到下面的效果:
梯度方向
但有时候边界还不够,我们希望得到图片色块之间的关系,或者研究样本的梯度特征来对机器训练识别物体时候,我们还需要梯度的方向。
二维平面的梯度定义为:
这很好理解,其表明颜色增长的方向与x轴的夹角。
但Sobel算子对于沿x轴和y轴的排列表示的较好,但是对于其他角度表示却不够精确。这时候我们可以使用Scharr滤波器。
Scharr滤波器的内核为:
这样能提供更好的角度信息,现在我们修改原程序,改为使用Scharr滤波器进行计算:
<span style="font-size:18px;">#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> using namespace cv; int main( int argc, char** argv ){ Mat src, src_gray; Mat grad; char* window_name = "梯度计算"; int scale = 1; int delta = 0; int ddepth = CV_16S; int c; src = imread( argv[1] ); if( !src.data ){ return -1; } GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT ); cvtColor( src, src_gray, CV_RGB2GRAY ); namedWindow( window_name, CV_WINDOW_AUTOSIZE ); Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; //改为Scharr滤波器计算x轴导数 Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_x, abs_grad_x ); //改为Scharr滤波器计算y轴导数 Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_y, abs_grad_y ); addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad ); imshow( window_name, grad ); waitKey(0); return 0; }</span><strong style="font-size:24px;"> </strong>
Scharr函数接受参数与Sobel函数相似,这里就不叙述了。
下面我们通过divide函数就能得到一个x/y的矩阵。
对两个输入数组的每个元素执行除操作。
C++: void divide (InputArray src1, InputArray src2, OutputArray dst, double scale=1, int dtype=-1 )
C++: void divide (double scale, InputArray src2, OutputArray dst, int dtype=-1 )
参数
- src1 – 第一个输入数组。
- src2 – 第二个输入数组,必须和第一个数组拥有相同的大小和通道。
- scale – 缩放系数。
- dst – 输出数组,和第二个数组拥有相同的大小和通道。
- dtype – 输出数组中的可选的深度,当两个数组具有相同的深度,此系数可设为-1,意义等同于选择与第一个数组相同的深度。
该函数对两个数组进行除法:
或则只是缩放系数除以一个数组:
这种情况如果src2是0,那么dst也是0。不同的通道是独立处理的。