安装教程与调试显示成功 不得不说,opencv的安装需要很久,也会出现很多的错误,也是参考了很多的安装教程,综合了好几个全面可靠,最后显示图像的时候,还是很开心的。先来一个调试成功,图像显示的界面。
想学opencv的应该都有c++基础,至于怎么在vs2010中建立工程,这里应该就不用多说了,不会的可以百度一下 vs2010怎么建立工程。
这里提供一篇文章参考建立程http://jingyan.baidu.com/article/5552ef473d44f5518ffbc9fd.html
测试代码如下: #include <opencv2/opencv.hpp> using namespacestd; using namespacecv; int main(intargc, char* argv[]) { const char* imagename = "lena.jpg"; //从文件中读入图像 Mat img = imread(imagename); //如果读入图像失败 if (img.empty()) { fprintf(stderr, "Can not load image %s\n", imagename); return -1; } //显示图像 imshow("image", img); //此函数等待按键,按键盘任意键就返回 waitKey(); return 0; } 注意:唯一你要注意的是对于这个代码你要显示的图片要放对位置,看下图的位置,相应打开你的文件夹,至于为什么会在后面的图像显示教程中显示将函数和路径,这里只要安装好,能显示就可以了。(想多了也没用)
一、解压 下载完后得到文件,解压到D:\Program Files 二、.配置环境变量
配置方法:右击我的电脑——属性——高级——环境变量——系统变量
变量名为PATH 变量值为D:\Program Files\opencv\build\x64\vc10\bin(我是64位+vs2010,如果你的是32位,选x86)
三.配置工程(很重要)
建立好一个工程,不会的上面有连接vs2010建立工程,然后菜单栏中找到属性管理器,点击项目->Debug|Win32->Microsoft.Cpp.Win32.userDirectories(右键属性,或者双击)即可打开属性页面----通用属性 ---VC++目录 ---包含目录中添加上
D:\Program Files\opencv\build\include D:\Program Files\opencv\build\include\opencv D:\Program Files\opencv\build\include\opencv2 这三个目录。
在库目录中,添加上D:\Program Files\opencv\build\x86\vc10\lib (这个路径,无论是32位还是64位电脑都选x86)
通用属性-- -链接器----输入->---附加的依赖项
opencv_ml248d.lib opencv_calib3d248d.lib opencv_contrib248d.lib opencv_core248d.lib opencv_features2d248d.lib opencv_flann248d.lib opencv_gpu248d.lib opencv_highgui248d.lib opencv_imgproc248d.lib opencv_legacy248d.lib opencv_objdetect248d.lib opencv_ts248d.lib opencv_video248d.lib opencv_nonfree248d.lib opencv_ocl248d.lib opencv_photo248d.lib opencv_stitching248d.lib opencv_superres248d.lib opencv_videostab248d.lib
opencv_objdetect248.lib opencv_ts248.lib opencv_video248.lib opencv_nonfree248.lib opencv_ocl248.lib opencv_photo248.lib opencv_stitching248.lib opencv_superres248.lib opencv_videostab248.lib opencv_calib3d248.lib opencv_contrib248.lib opencv_core248.lib opencv_features2d248.lib opencv_flann248.lib opencv_gpu248.lib opencv_highgui248.lib opencv_imgproc248.lib opencv_legacy248.lib
opencv_ml248.lib
OK,这样你可以看到最开始的那个图了。如果出现图像显示问题,不是配置问题的话么可以看第三节的图像显示,会具体的给出路径了,和图像显示长出现的问题和错误!
准备知识 首先你安装好了,然后用一个测试文件(没有测试文件可以找后面教程中的图像显示的代码粘贴),可以正常的运行。
然后还不要着急去学习怎么图像处理,因为还要知道一些常识。
准备知识也是很重要
模块 #include namespace argc argv Mat
(1) 模块
在这个文件下D:\ProgramFiles\opencv\build\include\opencv2会看到很多东西,这些都是需要的模块,里面有很多的要用的东西
介绍这个就不说了
因为是图像处理,先不用管那么多的模块,先去认识core imgproc highgui这三个,其他的用到的时候再去看吧
为 什么要看这个三个呢?
一、Core 核心功能模块(核心所以必须要啊),主要包含opencv基本数据结构,动态数据结构,绘图函数,数组操作相关函数,听听这些名字就知道必须要用了
二、Imgproc 这是图像处理模块,既然是处理图像,那也是没的说了
三、Highgui GUI图形用户界面,简单的说,你要用窗口,用界面,什么输入输出了,那你就用他的模块了
(2)#include
正如你经常看到的,每个代码的前面都会有这养的include,包含才可用,学过c++的应该很懂
#include <opencv2/core/core.hpp> #include<opencv2/highgui/highgui.hpp>
(3) namespace
还有一个需要注意的是这样的话
using namespace cv; using namespace std;
因为OpenCV中的C++类和函数都是定义在命名空间cv之内的,想要用,就的提前给人家打声招呼,所以要加上using namespacecv;这个方法比较好,只要一句话就可以,但是如果你加这一句,那么后面如果你要用类和函数的话,你就的这样写,很麻烦,std同理,std是一个类(输入输出标准),它包括了cin成员和cout成员,usingname space std ;以后才能使用它的成员。
image=cv::imread(“lena.jpg”); cv::nameWindow(“original image”); cv::imshow(“original image”,image);
(4) argc argv
对于一个程序来说,都会有一个main()函数
例如我们常用的一句话
main(int argv char **argv)
这里的int argc 为整型,用来统计程序运行时,发送给main函数的命令行参数;char **argv 是字符串数组用来存放指向字符串参数的指针数组,每一个元素指向一个参数
imread(argv[1],1),//意思是读取字符串名为argv[1]的图片,argv[1]指向在DOS命令行中执行程序名后的第一个字符串
(5)Mat
自从版本2.0,OpenCV采用了新的数据结构,用Mat类结构取代了之前用extended C写的cvMat和lplImage(在学习代码的时候,会经常看到IplImage),更加好用啦,最大的好处就是更加方便的进行内存管理,对写更大的程序是很好的消息。
如果光说Mat其中就可以是一篇很长的文章,因为用法太多,Mat是OpenCV里最基本的一个类,它用来表示图像,有部分组成,一个是矩阵头,一个是指向存储所有像素值的矩阵的指针,其中幅值与复制只是复制了信息头。下面给出超具体的例子,可以慢慢体会每一个Mat的应用,还有以前一篇写的复制问题可以参考http://blog.csdn.net/qq_20823641/article/details/51452939 #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <cv.h> #include <iostream>
using namespace std; using namespace cv;
int main() {
//Mat() Constructor Mat M(2, 2, CV_8UC3, Scalar(0, 0, 255)); cout << "M = " << endl << " " << M << endl << endl;
//C/C++ arrays and initialize via constructor int sz[3] = { 2,2,2 };//创建一个三维矩阵 Mat L(3, sz, CV_8UC1, Scalar::all(0)); //cout<<"L = "<<endl<<" "<<L<<endl<<endl;
//create a header for an already existing IplImage pointer IplImage* img = cvLoadImage("D:\\lena.bmp", 1); Mat mtx(img);//把IplImage*转换为Mat
//create()函数 M.create(4, 4, CV_8UC(2)); cout << "M = " << endl << " " << M << endl << endl;
//MATLAB风格初始化:zero(),ones(),:eyes(). Mat E = Mat::eye(4, 4, CV_64F); cout << "E = " << endl << " " << E << endl << endl; Mat O = Mat::ones(2, 2, CV_32F); cout << "O = " << endl << " " << O << endl << endl; Mat Z = Mat::zeros(3, 3, CV_8UC1); cout << "Z = " << endl << " " << Z << endl << endl;
//小矩阵用逗号初始化 Mat C = (Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); cout << "C = " << endl << " " << C << endl << endl;
//对已经存在的Mat对象创建新的头,并克隆或复制 Mat RowClone = C.row(1).clone(); cout << "RowClone = " << endl << " " << RowClone << endl << endl;
//利用随机函数创建Mat矩阵 Mat R = Mat(3, 2, CV_8UC3); randu(R, Scalar::all(0), Scalar::all(255)); cout << "R = " << endl << " " << R << endl << endl;
//打印输出格式化 //default格式 cout << "R(default) = " << endl << R << endl << endl; //Python格式 cout << "R(Python) = " << endl << format(R, "python") << endl << endl; //comma separated values(CSV) cout << "R(csv) = " << endl << format(R, "csv") << endl << endl; //numpy cout << "R(numpy) = " << endl << format(R, "numpy") << endl << endl; //C cout << "R(c) = " << endl << format(R, "C") << endl << endl;
//2D Point Point2f p(5, 1); cout << "Point(2D) = " << p << endl << endl; //3D Point Point3f P3f(2, 6, 7); cout << "Point (3D) = " << P3f << endl << endl; //std::vector via cv::Mat vector<float> v; v.push_back((float)CV_PI); v.push_back(2); v.push_back(3.01f); cout << "Vector of floats via Mat = " << Mat(v) << endl << endl; //std::vector of points vector<Point2f> vPoints(20); for (size_t E = 0; E < vPoints.size(); ++E) vPoints[E] = Point2f((float)(E * 5), (float)(E % 7)); cout << "A Vector of 2D POINTS = " << vPoints << endl << endl; return 0; }
除此之外呢,Mat类还有很多的属性,方便我们理解图像矩阵,例如高度、宽度等 Mat img(3, 4, CV_16UC4, Scalar_<uchar>(1, 2, 3, 4));
cout << "img is:" << img << endl;
cout << "dims:" << img.dims << endl; cout << "rows:" << img.rows << endl; cout << "cols:" << img.cols << endl; cout << "channels:" << img.channels() << endl; cout << "type:" << img.type() << endl; cout << "depth:" << img.depth() << endl; cout << "elemSize:" << img.elemSize() << endl; cout << "elemSize1:" << img.elemSize1() << endl;
单图像显示和多图像显示
其实,对于图像显示,对于我们在安装调试opencv的时候,就是作为一个例子,相信看到第三节的话,应该有了正确的配置环境,还有就是对opencv的结构有了一定的理解,那么下面就是进去图像处理的阶段,但是为了更好的学习,所以用已经见过而且能够实现的例子来说问题,相信更加让你感觉到亲近吧。 说实话,即使显示成功了一张图片,但是你对里面的东西也不知道的,只有好好的理解,那么到了出现错误的时候才可以更快的找到解决的办法,这样慢慢养成习惯,到了以后的项目中,就不会在小问题上大费周章,下面正式进去图像显示。 一、图像显示的代码 简要看一下,这三个显示图像的例子 第一个是最简洁最方便的 第二个是官方文档的例子 第三个是老版的例子 #include<opencv2/core/core.hpp> #include<opencv2/highgui/highgui.hpp> using namespace cv; int main(int argc, char** argv[]) { Mat srcimage; srcimage = imread("lena.jpg"); // srcimage=imread("../lena.jpg"); //cvnameWindow("原图显示"); imshow("原图显示", srcimage); waitKey(0);
}
#include<opencv2/core/core.hpp> #include<opencv2/highgui/highgui.hpp> #include<iostream> using namespace cv; using namespace std; int main(int argc, char** argv) { if (argc != 2) { cout << "display image imagetoload and display" << endl; return -1; } Mat image; image = imread(argv[1], 1); // Read the file
if (!image.data) // Check for invalid input { cout << "Could not open or find the image" << endl; return -1; }
imshow("Display window", image); // Show our image inside it.
waitKey(0); // Wait for a keystroke in the window return 0; }
#include "cv.h" #include "highgui.h" int main(int argc, char** argv) { IplImage* pImg; //argv[1] = " F:\\opencvprojects\\0 argc argv\\argcargv\\argcargv\\lena.jpg";
if (argc == 2 && (pImg = cvLoadImage(argv[1], 1)) != 0) { cvNamedWindow("Image", 1); //创建窗口 cvShowImage("Image", pImg); //显示图像
cvWaitKey(0); //等待按键
cvDestroyWindow("Image"); //销毁窗口 cvReleaseImage(&pImg); //释放图像 return 0;
} } 二、整体功能说明 看到上面的三个例子,要分别说一下
(1)第一个,显然让你回忆到了上一篇说的Mat,这里也是可以写成Mat
srcimage=imread("lena.jpg");对于这个看上一篇的Mat
就明白了,那么对于这个路径问题,一般对于入门来说,都会出现问题,官方说: this is unless you give a full,
explicit path as parameter for the I/O functions. Before stating up the
application
make sure you place you place the image file in your current working
directory.就是如果我们没有写全路径,只是写了一个图像的名字的话,那么这个图像要放在我们当前我们工作的路径下,刚开始可能找不到,但是你可以记住,这个图像在和.cpp文件是要在一起的。如下图
(2)第二个例子,我们发现了一个不一样的地方就是出现了argv【1】,举出这个例子是为了相应第二节中出现的准备知识,理论可以看上一篇,那么这个在这里是什么意思呢,怎么用呢?其实你要知道,这一个路径,是引用了一个常字符串,简单说,你需要自己配置一个命令参数,说明一下这个argv[1]代表了什么,具体的方法就是对于项目右击--属性----配置属性---调试---命令参数---lena.jpg
(3)第三个其实用opencv2的人经常用,但是现在学的话,一般都是用Mat,但是还是要说一下,因为可能在看例子或者其他的资料会出现, 主要是这句 IplImage* pImg; 这个是声明IplImage指针 三、常见的问题 (1)当我们刚开始接触的时候,一个是路径问题,那么上面已经用三个例子来说明了,这里就不重复了。 (2)还有一个问题就是,一闪而过 end far too quickly的问题,这个问题在你自己写代码,不是复制粘贴别人的代码的时候,刚开始都会遇到,是因为少了 waitKey(0),zero means to wait forever,这个可以等待鼠标键盘敲击然后退出,也就是让结果显示一会,然后等待下一个命令。 (3) 还有一个问题就是出现下图的
解决的方法就是把Release改成Debug,这个两个的调试原理和区别可以自搜一下,这里简略
四、类与函数 (1)Mat imread(const string& filename, intflags=1)
enum { /* 8bit, color or not */ CV_LOAD_IMAGE_UNCHANGED = -1, /* 8bit, gray */ CV_LOAD_IMAGE_GRAYSCALE = 0, /* ?, color */ CV_LOAD_IMAGE_COLOR = 1, /* any depth, ? */ CV_LOAD_IMAGE_ANYDEPTH = 2, /* ?, any color */ CV_LOAD_IMAGE_ANYCOLOR = 4 }; (2) void nameWindow(const string& winname, int flags=WINDOUW_AUTOSIZE)
for this you need to specity its name and how it should handle
the change of the image it contains from a size point of view. WINDOW_NORMAL设置了这个值,用户便可以改变窗口的大小(没有限制) WINDOW_AUTOSIZE如果设置了这个值,窗口大小会自动调整以适应所显示的图像,并且不能手动改变窗口大小。 WINDOW_OPENGL 如果设置了这个值的话,窗口创建的时候便会支持OpenGL。
(3)void imshow(const string& winname, InputArray mat); specify the opencv window name to update and the image to use during this operation 五、多图像显示
有时候在做图像显示的时候,会进行很多张的处理,特别是训练或者模版匹配的时候,我们要是一张一张的处理实在是太慢了,于是我们需要一次读取多张图,你可以一张一张的
imread(),但是明显不是什么好方法,再没有深入学习访问像素的时候,我们可以通过一个简单的方法来看,就是循环for,代码如 #include <iostream> #include <stdio.h> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp>
int main() { const int num = 2; char fileName[10]; char windowName[10]; cv::Mat srcImage; for (int i = 1; i <= num; i++) {
sprintf_s(fileName, "%d.jpg", i); sprintf_s(windowName, "1-%d", i);
srcImage = cv::imread(fileName); cv::namedWindow(windowName); cv::imshow(windowName, srcImage); std::cout << "NO" << i << std::endl; /* 以后想处理,或者保存都在这里添加,这里只是显示*/
} cv::waitKey(0); return 0;
}
六、Matlab 辅助
以前用的matlab比较多,最后会来一个matlab实现,matlab比较直观的表示,其实opencv也是可以的,下载一个Image
watch就可以了,但是文档说要vs2012以上吧,所以我就没有用,安装他的效果就是你可以看到这个图像是600*800* Uint8
这样矩阵的形式,和matlab的感觉就一样了。还是不多说了,来看看matlab怎么实现的 I=imread('lena.jpg');figureimshow(I) 有没有发现框架是一样的呢,这个就是opencv升级以后很多的函数都是通用的感觉一样,不像以前CV_LOAD_IMAGE什么的,很长很不通用,看到这里是不是感觉,慢慢的走进了图像识别的世界,有了很大的兴趣呢?
cMake与源代码与image watch
其实在学习opencv图像的时候,不是那么需要看源代码。但是还是有想学习一下的人,所以就写出来如何去看源代码,其实名字应该是代码追踪,就是我们在设置断点调试的时候,可以看到内部的定义,一堆一堆,在后面会送上imshow()与imread()的源代码可以用来欣赏学习。 自己看了好多教程,重载了一次软件还,终于能够F11到自己想要的源代码了 vs2010, cmake2.8, opencv2.4.9,这是需要的东西,估计大家都有,没有或者下不到的可以私信或者留言索要,这里就不多说了。 第一步要生产源代码 (1) 运行解压的camke文件,在bin文件夹下运行cmake-gui.exe ( 2) 在where is the source code:选择路径为D:\Program Files\opencv\sources(就是你解压opencv的路径,一般教 程都是这个位置) 下面的路径,就自己选择了,《随意》 ,我的是F:\opencv (3)先点一次configure,然后选择编译器,我的是vs2010,所以选visual
studio 10(如果不是自己根据实际选 择),等出现configuring done 然后再点一次configure
再等出现configuring done,然后点击generate,出现 configuring done. 这个完成就可以看到很多的源代码了,文章就是 《 随意》那里的路径,我的就在F:\opencv,然后打开这个文件夹,找到opencv.sln打开看到下图
左边那些点开,找到src文件夹,里面就是源文件,那么如何对应呢?例如上一篇的imread(),他的源代码在higngui模块下的,loadsave.cpp,估计要是从这么多的模块和代码下面找,需要很大的功夫,那么怎么实现快速的方法吗?这个也是折磨死我了,方式就是需要再配置一点东西。 (1)首先要设置一个启动项,为什么呢?其实目的不是太大,主要是因为如果你不设置的话,默认的是ALL_BUILD作为启动项,要选中install这个,因为后面后用这个文件夹,然后你就弹出错误提示了,可以尝试,看到错误了,然后再设置一个启动,看上图右击,选择设为启动项目 (2)分别在Dudug 和Release模式下进行调试,这个时间挺长的,耐心等待,知道出现 xxxx个成功,0个失败,0个最新,2个模式下都是这样的 (3)设置环境变量,F:\opencv\bin\Debug 和 F:\opencv\bin\Release,这里因为我压缩到F:\opencv所以参考着写,添加环境变量的方法和第一节中方法一样,可以回顾看一下 (4)下面也是差不多,就是要添加包含目录和库目录 VC++ 目录 -> 包含目录, VC++ 目录 -> 库目录进行设置: F:\opencv\install\include F;\opencv\install\include\opencv F:opencv\install\include\opencv2 VC++ 目录 ->库目录:F:opencv\lib\Debug (5)链接器->输入->附加依赖项处添加:一堆的东西,这个和第一节中加的东西是一样的很长,这些省略,可以去第一节那里复制进行粘贴 下面设置断点就可以看到imread(),其实imread()没有什么主要是imread_() 先看一下imread() 然后是Imread_() 就是这样一层一层的拨开你的心,哈哈 上图和源代码 static void* imread_(const string& filename, int flags, int hdrtype, Mat* mat = 0) { IplImage* image = 0; CvMat* matrix = 0; Mat temp, * data = &temp;
ImageDecoder decoder = findDecoder(filename); if (decoder.empty()) return 0; decoder->setSource(filename); if (!decoder->readHeader()) return 0;
CvSize size; size.width = decoder->width(); size.height = decoder->height();
int type = decoder->type(); if (flags != -1) { if ((flags & CV_LOAD_IMAGE_ANYDEPTH) == 0) type = CV_MAKETYPE(CV_8U, CV_MAT_CN(type));
if ((flags & CV_LOAD_IMAGE_COLOR) != 0 || ((flags & CV_LOAD_IMAGE_ANYCOLOR) != 0 && CV_MAT_CN(type) > 1)) type = CV_MAKETYPE(CV_MAT_DEPTH(type), 3); else type = CV_MAKETYPE(CV_MAT_DEPTH(type), 1); }
if (hdrtype == LOAD_CVMAT || hdrtype == LOAD_MAT) { if (hdrtype == LOAD_CVMAT) { matrix = cvCreateMat(size.height, size.width, type); temp = cvarrToMat(matrix); } else { mat->create(size.height, size.width, type); data = mat; } } else { image = cvCreateImage(size, cvIplDepth(type), CV_MAT_CN(type)); temp = cvarrToMat(image); }
if (!decoder->readData(*data)) { cvReleaseImage(&image); cvReleaseMat(&matrix); if (mat) mat->release(); return 0; }
return hdrtype == LOAD_CVMAT ? (void*)matrix : hdrtype == LOAD_IMAGE ? (void*)image : (void*)mat; }
想学习源代码的,以后就可以方便的看了,深刻理解更为重要。最后看一个又一个实用的工具,Image
watch,是让你感觉到matlab的形象感觉,下面图片一看就明白
Image Watch可进行的操作包括: 1. 放大、缩小图像; 2. 将图像保存到指定的目录; 3. 显示图像大小、通道数; 4. 拖拽图像; 5. 可以查看指定坐标的像素值(按照在内存中的顺序显示); 6. Link Views:所有相同尺寸的图像共享一个视图; 7. 像素值以十六进制显示还是十进制显示; 8. 在Watch窗口可对图像进行的操作包括(Image Watch包括Locals和Watch两个窗口): (1)、提取指定通道图像:@band(img, number); (2)、对指定图像进行阈值化:@thresh(img, threshold); (3)、对图像像素值进行取绝对值操作:@abs(img); (4)、对图像像素值进行缩放操作:@scale(img, factor); (5)、通过1/255方式缩放像素值操作:@norm8(img); (6)、沿y轴水平flip:@fliph(img); (7)、沿x轴垂直flip:@flipv(img); (8)、对图像进行矩阵转置操作:@flipd(img); (9)、对图像进行顺时针90、180、270度操作:@rot90(img)、@rot180(img)、@rot270(img); (10)、计算两幅图像的像素差值:@diff(img0, img1); (11)、载入图像:@file(path),如 @file(“d:\1.jpg”); (12)、将指定的内存地址内容按照指定的图像格式显示出来:@mem(address,
type, channels, width, height, stride),如@mem(0x00000000003d1050,UINT8,
3, 256, 256, 768 ); Image Watch的更详细介绍可参考:http://research.microsoft.com/en-us/um/redmond/groups/ivm/imagewatchhelp/imagewatchhelp.htm
Mat_ROI、颜色转换、多图显示、保存输出 其实在看到Mat类的时候,感觉总是怎么那么多功能,没办法就是那么头疼,不过功能多,那么用法也就多,相对的会在图像处理中有很大的重要,所以后面不知不觉中就会回去看看他,这里用ROI来进步说一下Mat,看看实例的应用,这样更舒服一些。
然后再说一下颜色转化,因为在图像中,我们会看到彩色图像和灰度图像,他们有处理的共同的方法,也有自己的方法,每种类型都有自己的特征,所以在他们之间的转化是很重要的,这里用彩色和灰度转化,其他的看函数定义也是一样的,注意opencv的彩色是BGR不是RGB,这里在下一节的图像遍历中会用实例来说明。 最后是显示图像的升级,可以在一个窗口显示多张图片并保存输出,和工程代码、结果图像 一、Mat_ROI Mat(int _rows, int _cols, int _type, constScalar& _s); 这个构造函数。 IplImage*是C语言操作OpenCV的数据结构,在当时C操纵OpenCV的时候,地位等同于Mat,OpenCV为其提供了一个接口,很方便的直接将IplImage转化为Mat,即使用构造函数 Mat(const IplImage* img, boolcopyData=false); 上面程序中的第二种方法就是使用的这个构造函数。 关于Mat数据复制:前面说过Mat包括头和数据指针,当使用Mat的构造函数初始化的时候,会将头和数据指针复制(注意:只是指针复制,指针指向的地址不会复制),若要将数据也复制,则必须使用copyTo或clone函数 再来一张图更好的理解
二、cvtcolor CV_EXPORTS_W void cvtColor( InputArray src, OutputArray dst, int code, int dstCn=0 ); 第一个参数是输入图像,第二个是输出图像,第三个是颜色空间转换的标识符,第四个是参数为目标图像的通道 下面是颜色空间种类,很多 COLOR_BGR2BGRA = 0, COLOR_RGB2RGBA = COLOR_BGR2BGRA, COLOR_BGRA2BGR = 1, COLOR_RGBA2RGB = COLOR_BGRA2BGR, COLOR_BGR2RGBA = 2, COLOR_RGB2BGRA = COLOR_BGR2RGBA, COLOR_RGBA2BGR = 3, COLOR_BGRA2RGB = COLOR_RGBA2BGR, COLOR_BGR2RGB = 4, COLOR_RGB2BGR = COLOR_BGR2RGB, COLOR_BGRA2RGBA = 5, COLOR_RGBA2BGRA = COLOR_BGRA2RGBA, COLOR_BGR2GRAY = 6, COLOR_RGB2GRAY = 7, COLOR_GRAY2BGR = 8, COLOR_GRAY2RGB = COLOR_GRAY2BGR, COLOR_GRAY2BGRA = 9, COLOR_GRAY2RGBA = COLOR_GRAY2BGRA, COLOR_BGRA2GRAY = 10, COLOR_RGBA2GRAY = 11, COLOR_BGR2BGR565 = 12, COLOR_RGB2BGR565 = 13, COLOR_BGR5652BGR = 14, COLOR_BGR5652RGB = 15, COLOR_BGRA2BGR565 = 16, COLOR_RGBA2BGR565 = 17, COLOR_BGR5652BGRA = 18, COLOR_BGR5652RGBA = 19 三、多图显示和保存CV_EXPORTS_W bool imwrite( const string& filename, InputArray img, const vector<int>& params=vector<int>()); 第一个参数是文件名,第二个参数mat类型的图像数据,第三个暗示表示特定格式保存的参数编码
四、工程代码和结果展示 #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include<iostream> #include<opencv2\imgproc\imgproc.hpp>
using namespace cv; using namespace std;
void showManyImages(const std::vector<cv::Mat>& srcImages) { int nNumImages = srcImages.size(); cv::Size nSizeWindows;
nSizeWindows = cv::Size(2, 2);
int nShowImageSize = 200; int nSplitLineSize = 15; int nAroundLineSize = 50; const int imagesHeight = nShowImageSize * nSizeWindows.width + nAroundLineSize + (nSizeWindows.width - 1) * nSplitLineSize; const int imagesWidth = nShowImageSize * nSizeWindows.height + nAroundLineSize + (nSizeWindows.height - 1) * nSplitLineSize; std::cout << imagesWidth << " " << imagesHeight << std::endl; cv::Mat showWindowImages(imagesWidth, imagesHeight, CV_8UC3, cv::Scalar(0, 0, 0)); int posX = (showWindowImages.cols - (nShowImageSize * nSizeWindows.width + (nSizeWindows.width - 1) * nSplitLineSize)) / 2; int posY = (showWindowImages.rows - (nShowImageSize * nSizeWindows.height + (nSizeWindows.height - 1) * nSplitLineSize)) / 2; std::cout << posX << " " << posY << std::endl; int tempPosX = posX; int tempPosY = posY; for (int i = 0; i < nNumImages; i++) {
if ((i % nSizeWindows.width == 0) && (tempPosX != posX)) { tempPosX = posX; tempPosY += (nSplitLineSize + nShowImageSize); } cv::Mat tempImage = showWindowImages(cv::Rect(tempPosX, tempPosY, nShowImageSize, nShowImageSize)); resize(srcImages[i], tempImage, cv::Size(nShowImageSize, nShowImageSize)); tempPosX += (nSplitLineSize + nShowImageSize); } cv::imshow("showWindowImages", showWindowImages); }
int main() {
Mat image_lena; image_lena = imread("lena.jpg", CV_LOAD_IMAGE_COLOR); namedWindow("原图", CV_WINDOW_AUTOSIZE); imshow("原图", image_lena); //这段可以选择不看 if (image_lena.empty()) { // error handling
std::cout << "Error reading image..." << std::endl; return 0; } //显示出来看一下图像的大小,这样后面就好设置ROI的大小 //如果看了上一篇的image watch 它可以自动告诉你就不用写下面的话了 std::cout << "This image is " << image_lena.rows << " x " << image_lena.cols << std::endl; //显示图像左上角1/4 Mat imagelenaROI = image_lena(Rect(0, 0, image_lena.cols / 2, image_lena.rows / 2)); namedWindow("ROI图"); imshow("ROI图", imagelenaROI);
Mat graylena_image; cvtColor(imagelenaROI, graylena_image, CV_BGR2GRAY); //保存 imwrite("ROI灰度图片.jpg", graylena_image); imwrite("ROI图.jpg", imagelenaROI); std::vector<cv::Mat> srcImages(3); srcImages[0] = cv::imread("lena.jpg"); srcImages[1] = cv::imread("ROI图.jpg"); srcImages[2] = cv::imread("ROI灰度图片.jpg"); showManyImages(srcImages);
waitKey();
return 0; }
结果图像:
五、辅助Matlab matlab中的图像格式转化也是类似的,提供了很多的函数 %数据类型im2uint8()%将图像转化成uint8类型im2uint16()%将图像转化成uint16类型Im2double()%将图像转化成double类型 gray2ind()%灰度图转索引图im2bw()%图像阈值转二值图rgb2gray()%彩色转灰度[I,MAP]=rgb2gray()%其中X是图像的数据,MAP是颜色表 写入函数的定义: imwrite(A,FILENAME,FMT); FILENAME参数指定文件名 FMT参数指定保存所采用的格式
对于matlab中查看数据,比较方便,一个是数据会显示,另外可以用whos I(I=imread('lena.jpg')),还可以用imfinfo()函数读取图像文件中的某些属性信息,比如修改日期、大小、格式、高度、宽度、色深、颜色空间、存储方式等
访问图像像素
图像处理,从开始我们就接触了Mat类,这一个图像容器类,同时也是个矩阵类,那么如何访问图像的像素呢?或者说如何去操作这个矩阵呢?普遍上是说有暗中方法,一个是指针ptr,一个是AT,一个是迭代器,这个是一一来说,主要是从不同的角度说指针访问,因为这个最快,个人认为最重要。其中有vc6.0和matlab的辅助因为比较长,所以就穿插在里面,不单独说了。首先,在进行访问前,要知道像素的存储方式,下面来一张图,是最好的解释,这个是基础,因为后面在对行列进行访问的时候,你不知道存储方式,就一定会出现。 我们认为的矩阵形式是左图,计算机认识的是右图
不同维度的数组在内存的存储方式为
一、灰度图像,单通道
二、彩色图像,三通道图像
对于彩色图像来说,一般我们都说RGB,但是这里要强调一个是BGR,这个我用下面的代码来看一下,更好的理解 #include<opencv2\core\core.hpp> #include<opencv2\highgui\highgui.hpp> using namespace cv; int main() { Mat srcimage = imread("red.jpg"); //Mat srcimage1=imread("green.jpg"); Mat srcimage2 = imread("blue.jpg"); //if(!srcimage.data) // return 1; Mat tempimage = srcimage.clone(); Mat tempimage1 = srcimage2.clone();
int watch11, watch12, watch13, watch21, watch22, watch23; watch11 = tempimage.at<Vec3b>(0, 0)[0]; watch12 = tempimage.at<Vec3b>(0, 0)[1]; watch13 = tempimage.at<Vec3b>(0, 0)[2]; watch21 = tempimage1.at<Vec3b>(0, 0)[0]; watch22 = tempimage1.at<Vec3b>(0, 0)[1]; watch23 = tempimage1.at<Vec3b>(0, 0)[2]; waitKey(0); return 0; } 对面下面的图可以看到,(0 0 254)一张红色的图,只有BGR的red有数值,同时也可以看到矩阵的数值显示 0 0 254
基础也说的差不多了,那我们看看像素是怎么访问的 三、指针方式 我喜欢从指针方式开始,因为自己以前用VC6.0的,感觉很相同 void colorReduce(const Mat & image, Mat & outImage, int div) { // 创建与原图像等尺寸的图像 outImage.create(image.size(), image.type()); int nr = image.rows; // 将3通道转换为1通道 int nl = image.cols * image.channels(); for (int k = 0; k < nr; k++) { // 每一行图像的指针 const uchar* inData = image.ptr<uchar>(k); uchar* outData = outImage.ptr<uchar>(k); for (int i = 0; i < nl; i++) { outData[i] = inData[i] / div * div + div / 2; //这里也可以用*outData下面的例子就是可以参考 } } } 一般来说图像行与行之间往往存储是不连续的,但是有些图像可以是连续的,Mat提供了一个检测图像是否连续的函数isContinuous()。当图像连通时,我们就可以把图像完全展开,看成是一行,所以用进一步的提高了效率。
void colorReduce(const Mat & image, Mat & outImage, int div) { int nr = image.rows; int nc = image.cols; outImage.create(image.size(), image.type()); if (image.isContinuous() && outImage.isContinuous()) { nr = 1; nc = nc * image.rows * image.channels(); } for (int i = 0; i < nr; i++) { const uchar* inData = image.ptr<uchar>(i); uchar* outData = outImage.ptr<uchar>(i); for (int j = 0; j < nc; j++) { *outData++ = *inData++ / div * div + div / 2; } } } 上面说的两种是最常见的,也是最重要的,以后的访问中会经常看到,所以要好好的看 为了进一步学习ptr指针访问,找了2个方法,都是用指针来访问,略有一点不同,可以作为参考 //三通道图像,at(y , x)索引是先行(y轴) , 后列(x轴) //第一种方法 for (int h = 0; h < image.rows; ++h) { for (int w = 0; w < image.cols / 2; ++w) { uchar* ptr = image.ptr<uchar>(h, w); ptr[0] = 255; ptr[1] = 0; ptr[2] = 0; } } imshow("color1", image); //第二种方法 for (int h = 0; h < image.rows; ++h) { for (int w = 0; w < image.cols / 2; ++w) { Vec3b* ptr = image.ptr<Vec3b>(h, w); ptr->val[0] = 0; ptr->val[1] = 255; ptr->val[2] = 0; } } imshow("color2", image); 为了加深印象我对照了一下vc6.0c++的程序,发现很相似,但是明显简介的多,但是思路和方法是一样的,下面这个就是vc6.0里面的一个小程序void HuidubianhuanDib::Fei0() { LPBYTE p_data; int wide, height; p_data = this->GetData; wide = this->GetWidth; height = this->Getheight; for (int j = 0; j < height; j++) for (int i = 0; i < wide; i++) { if (*p_data != 0) * p_data = 255; p_data++; } }
显然可以看到用是对data进行操作,相应的opencv中也可以用同样的方式,和上面的代码一个思路,so看看下面的代码是不是更加的清楚 #include <highgui.h> using namespace std; using namespace cv; int main() { Mat image = imread("forest.jpg"); imshow("image", image); //三通道 uchar* data = image.data; for (int h = 0; h < image.rows; ++h) { for (int w = 0; w < image.cols / 2; ++w) { *data++ = 128; *data++ = 128; *data++ = 128; } } imshow("data", image); //单通道 image = imread("forest.jpg", 0); imshow("image", image);
data = image.data; for (int h = 0; h < image.rows; ++h) { for (int w = 0; w < image.cols / 2; ++w) { *data++ = 128; } } imshow("data1", image); waitKey(0); return 0; } 其中ptr是成员函数,data是成员变量,单独一个用法,看下面的代码,和上面的对比就会名ptr和data了 void colorReduce2(const Mat& image, Mat& result, int div = 64) { int n1 = image.rows; //int nc = image.cols * image.channels(); int nc = image.cols; for (int j = 0; j < n1; j++) { //uchar *data = image.ptr(j); //uchar *data_in = image.data + j * image.step; //uchar *data_out = result.data + j * result.step; for (int i = 0; i < nc; i++) { uchar* data = image.data + j * image.step + i * image.elemSize(); // 这种方式不推荐使用,一方面容易出错,还不适用于带有"感兴趣区域" //data_out[i] = data_in[i]/div *div + div/2; data[0] = 0; data[1] = 0; data[2] = 0; } } } 四、动态地址at
Mat类提供了一个at的方法用于取得图像上的点,它是一个模板函数,可以取到任何类型的图像上的点。下面我们通过一个图像处理中的实际来说明它的用法。 最经典的用法就是m.at<Vec3b>(i,j)[m] void colorReduce(Mat& image, int div) { for (int i = 0; i < image.rows; i++) { for (int j = 0; j < image.cols; j++) { image.at<Vec3b>(i, j)[0] = image.at<Vec3b>(i, j)[0] / div * div + div / 2; image.at<Vec3b>(i, j)[1] = image.at<Vec3b>(i, j)[1] / div * div + div / 2; image.at<Vec3b>(i, j)[2] = image.at<Vec3b>(i, j)[2] / div * div + div / 2; } } } 看到at,很多的时候叫访问下标,因为类似与image(i,j),这个就和矩阵的访问一样,Matlab经常出现的就是这样的,例如I = zeros(m, n); for (i = 0; i < m; i++) { for (j = 0; j < n; j++) I(i, j) = 0; } 五、迭代器访问 这个这是你没有看出来什么优点,目前就是因为指针直接访问可能出现越界问题,而迭代器是非常安全的方法,用法是通过获得图像矩阵的开始和结束,然后增加迭代直至从开始到结束。 cv::Mat tempImage = srcImage.clone(); // 初始化源图像迭代器 cv::MatConstIterator_<cv::Vec3b> srcIterStart = srcImage.begin<cv::Vec3b>(); cv::MatConstIterator_<cv::Vec3b> srcIterEnd = srcImage.end<cv::Vec3b>(); // 初始化输出图像迭代器 cv::MatIterator_<cv::Vec3b> resIterStart = tempImage.begin<cv::Vec3b>(); cv::MatIterator_<cv::Vec3b> resIterEnd = tempImage.end<cv::Vec3b>(); // 遍历图像反色处理 while (srcIterStart != srcIterEnd) { (*resIterStart)[0] = 255 - (*srcIterStart)[0]; (*resIterStart)[1] = 255 - (*srcIterStart)[1]; (*resIterStart)[2] = 255 - (*srcIterStart)[2]; // 迭代器递增 srcIterStart++; resIterStart++; }
官方比较流行的是这样的代码,其中是一样的,只是个人写法的习惯Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table) { // accept only char type matrices CV_Assert(I.depth() != sizeof(uchar)); const int channels = I.channels(); switch (channels) { case 1: { MatIterator_<uchar> it, end; for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it) * it = table[*it]; break; } case 3: { MatIterator_<Vec3b> it, end; for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it) { (*it)[0] = table[(*it)[0]]; (*it)[1] = table[(*it)[1]]; (*it)[2] = table[(*it)[2]]; } } } return I; } 六、LUT函数 Look up table与计时函数getTickFrequency() LuT用于批量进行图像像素查找、扫描、操作像素 cv::Mat inverseColor6(cv::Mat srcImage) { int row = srcImage.rows; int col = srcImage.cols; cv::Mat tempImage = srcImage.clone(); // 建立LUT 反色table uchar LutTable[256]; for (int i = 0; i < 256; ++i) LutTable[i] = 255 - i; cv::Mat lookUpTable(1, 256, CV_8U); uchar* pData = lookUpTable.data; // 建立映射表 for (int i = 0; i < 256; ++i) pData[i] = LutTable[i]; // 应用索引表进行查找 cv::LUT(srcImage, lookUpTable, tempImage); return tempImage; } 对于计算时间的问题,opencv提供了2个比较方便的函数getTickCount()与getTickFrequency(), 其中getTickCount()函数返回CPU自某个事件以来走过的时钟周期, getTickFrequency()函数返回CPU一秒钟所走过的时钟周期数 用法 double time0 = static_cast<double>(getTickCount()); time0 = ((double)getTickCount() - time0) / getTickFrequency(); cout << ''时间为:"<<time0<<"秒"<<endl;
图像平移、旋转、镜像
根据vc6.0c++的学习经验,如果可以很好的自己编程,让图像进行平移旋转这些操作,那么就好像能够清楚的看见图像的内部结构当然这里你怎么访问像素,这个可以自己选一种适合的,最多的是ptr指针,at也是挺多的。看着很简单的变换,可以对图像处理上手的更快,当然对于旋转可能就稍微i难了一点,不过opencv提供了resize(0,remap()等这样的函数,可以方便的让我们进行学习-特别是旋转的时候,有很多的变换,你可以任意旋转一个角度,也可能一直旋转,当然还可以保持图像大小不变的旋转和大小变换的旋转。好了,这些后面都会慢慢的接触到。这里不过多的说理论的知识,不过相应的会加上对应的重要的公式或者图,如果对平移,旋转,镜像的概念或者公式不了解的话,可以百度一下,下面的主要是实践为主,代码和结果的应用。
首先,要简单的介绍一下两个函数,就是remap()和resize() void remap(InputArray src, OutputArraydst, InputArray map1, InputArray map2, int interpolation, intborderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位或者浮点型图像。 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,即这个参数用于存放函数调用后的输出结果,需和源图片有一样的尺寸和类型。 第三个参数,InputArray类型的map1,里面存储着源图像中各像素点的x坐标在目标图像中的x坐标,x坐标就是代表列号 第四个参数,InputArray类型的map2,里面存储着源图像中各像素点的y坐标在目标图像中的y坐标,y坐标就是代表行号 第五个参数,int类型的interpolation,插值方式,可选的插值方式如下: INTER_NEAREST - 最近邻插值 INTER_LINEAR – 双线性插值(默认值) INTER_CUBIC – 双三次样条插值(逾4×4像素邻域内的双三次插值) INTER_LANCZOS4 -Lanczos插值(逾8×8像素邻域的Lanczos插值)
第六个参数,int类型的borderMode,边界模式,有默认值BORDER_CONSTANT,表示目标图像中“离群点(outliers)”的像素值不会被此函数修改。具体什么叫离群点我现在也不清楚! 第七个参数,const Scalar&类型的borderValue,当有常数边界时使用的值,其有默认值Scalar( ),即默认值为0。具什么叫有常数边界,我现在也不清楚!
void resize(InputArray src, OutputArray dst, Size dsize, double fx = 0, double fy = 0, int interpolation = INTER_LINEAR)
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
其中,dsize,fx,fy都不能为0。
可选的插值方式如下:
INTER_LANCZOS4 -Lanczos插值(超过8×8像素邻域的Lanczos插值 一-、图像的平移 根据上面的公式,就可以编写代码了,平移还是很简单的,基础也是可以升级的,因为你会发现平移后,图像的大小少了一部分,在同样的大小的窗口中是不能打开的,所以你要改进,方法如下 #include<opencv2/core/core.hpp> #include<opencv2/highgui/highgui.hpp> using namespace cv; //平移后大小不变 void translateTransform(cv::Mat const& src, cv::Mat& dst, int dx, int dy) { CV_Assert(src.depth() == CV_8U); const int rows = src.rows; const int cols = src.cols; dst.create(rows, cols, src.type()); Vec3b* p; for (int i = 0; i < rows; i++) { p = dst.ptr<Vec3b>(i); for (int j = 0; j < cols; j++) { //平移后坐标映射到原图像 int x = j - dx; int y = i - dy; //保证映射后的坐标在原图像范围内 if (x >= 0 && y >= 0 && x < cols && y < rows) p[j] = src.ptr<Vec3b>(y)[x]; } } } //平移后大小变化 void translateTransformSize(cv::Mat const& src, cv::Mat& dst, int dx, int dy) { CV_Assert(src.depth() == CV_8U); const int rows = src.rows + abs(dy); //输出图像的大小 const int cols = src.cols + abs(dx); dst.create(rows, cols, src.type()); Vec3b* p; for (int i = 0; i < rows; i++) { p = dst.ptr<Vec3b>(i); for (int j = 0; j < cols; j++) { int x = j - dx; int y = i - dy; if (x >= 0 && y >= 0 && x < src.cols && y < src.rows) p[j] = src.ptr<Vec3b>(y)[x]; } } } int main(int argc, char** argv[]) { Mat srcimage, dst, dst1; srcimage = imread("lena.jpg"); namedWindow("src_window"); imshow("src_window", srcimage); translateTransform(srcimage, dst, 50, 50); namedWindow("dst_window"); imshow("dst_window", dst); translateTransformSize(srcimage, dst1, 50, 50); namedWindow("dst_window1"); imshow("dst_window1", dst1); waitKey(0); }
二、镜像 镜像分为水平镜像和垂直镜像,分别是关于y轴和x轴的对称翻转,所以也会看到翻转的说法,原理都是差不多的,左为水平,右为垂直 #include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main() { Mat src = imread("lena.jpg", CV_LOAD_IMAGE_UNCHANGED); imshow("src", src); Mat dst; dst.create(src.size(), src.type()); Mat map_x; Mat map_y; map_x.create(src.size(), CV_32FC1); map_y.create(src.size(), CV_32FC1); for (int i = 0; i < src.rows; ++i) { for (int j = 0; j < src.cols; ++j) { map_x.at<float>(i, j) = (float)(src.cols - j); map_y.at<float>(i, j) = (float)i; //水平 //map_x.at<float>(i, j) = (float) j ; //map_y.at<float>(i, j) = (float) (src.rows - i) ; //垂直 } } remap(src, dst, map_x, map_y, CV_INTER_LINEAR); imshow("dst", dst); imwrite("invert2.jpg", dst); waitKey(0); system("pause"); return 0; }
三、旋转 这个我找了一张说的比较简洁来介绍,旋转的时候可以有旋转后图像不被掩盖的(就是图像窗变大),常见的是掩盖的 原理如下: #include "cv.h" #include "highgui.h" #include "math.h"
// clockwise 为true则顺时针旋转,否则为逆时针旋转 IplImage* rotateImage(IplImage* src, int angle, bool clockwise) { angle = abs(angle) % 180; if (angle > 90) { angle = 90 - (angle % 90); } IplImage* dst = NULL; int width = (double)(src->height * sin(angle * CV_PI / 180.0)) + (double)(src->width * cos(angle * CV_PI / 180.0)) + 1; int height = (double)(src->height * cos(angle * CV_PI / 180.0)) + (double)(src->width * sin(angle * CV_PI / 180.0)) + 1; int tempLength = sqrt((double)src->width * src->width + src->height * src->height) + 10; int tempX = (tempLength + 1) / 2 - src->width / 2; int tempY = (tempLength + 1) / 2 - src->height / 2; int flag = -1;
dst = cvCreateImage(cvSize(width, height), src->depth, src->nChannels); cvZero(dst); IplImage* temp = cvCreateImage(cvSize(tempLength, tempLength), src->depth, src->nChannels); cvZero(temp);
cvSetImageROI(temp, cvRect(tempX, tempY, src->width, src->height)); cvCopy(src, temp, NULL); cvResetImageROI(temp);
if (clockwise) flag = 1;
float m[6]; int w = temp->width; int h = temp->height; m[0] = (float)cos(flag * angle * CV_PI / 180.); m[1] = (float)sin(flag * angle * CV_PI / 180.); m[3] = -m[1]; m[4] = m[0]; // 将旋转中心移至图像中间 m[2] = w * 0.5f; m[5] = h * 0.5f; // CvMat M = cvMat(2, 3, CV_32F, m); cvGetQuadrangleSubPix(temp, dst, &M); cvReleaseImage(&temp); return dst; }
int main(int argc, char** argv) { IplImage* src = 0; IplImage* dst = 0; // 旋转角度 int angle = 90;
src = cvLoadImage("lena.jpg", CV_LOAD_IMAGE_COLOR); cvNamedWindow("src", 1); cvShowImage("src", src);
dst = rotateImage(src, angle, false); cvNamedWindow("dst", 2); cvShowImage("dst", dst); cvWaitKey(0);
cvReleaseImage(&src); cvReleaseImage(&dst); return 0; }
四、matlab辅助 本来想添加vc6.0的框架的,因为两者可以更好的比较,但是会带来篇幅过长,所以这里用matlab,来实现简单高效 i=imread('D:\lena.jpg'); %读一幅图像 j=imrotate(i,30);%图像旋转30度 k=imresize(i,2);%图像放大两倍 t=imresize(i,2,'bilinear');%采用双线性插值法进行放大两倍 m=imresize(i,0.8);%图像缩小到0.8倍 p=translate(strel(1), [25 25]);%图像平移 img=imdilate(i,p); figure; subplot(231);imshow(i);title('原图'); subplot(232);imshow(j);title('旋转'); subplot(233);imshow(k);title('放大'); subplot(234);imshow(t);title('双线性插值'); subplot(235);imshow(m);title('缩小'); subplot(236);imshow(img);title('平移');
灰度直方图
其实刚开始的时候,看很多的书和教程讲绘图和彩色图像等,但是我觉得还是先学会灰度直方图,因为灰度的dims是1,如果dims是3的就是彩色,同时知道前面将的彩色图像的像素访问,相信很快就可以迁移过去的。 一、换个角度认识图像(直方图) 第一个就是当我们面对图像的时候,我们面对的是抽象的矩阵,如下图,下面是0-255的灰度图像的表示,密密麻麻的 那么我们做的直方图,其实就是对这些像素值的统计,看下图,其中Bin是条数,数据和范围是对图的解释,一看就懂
二、准备知识 如果想绘制出来直方图,先要知道几个函数 (1) Point类数据结构表示了二维坐标系下的点 Point point=Point(1,2); (2)calcHist()绘制直方图 void calcHist(const Mat* arrays, intnarrays, const int* channels, InputArray mask, OutputArray hist, int dims, const int* histSize, const float** ranges, bool uniform = true, boolaccumulate = false); 参数解释: arrays:输入的图像的指针,可以是多幅图像,所有的图像必须有同样的深度(CV_8U orCV_32F)。同时一副图像可以有多个channes。 narrays:输入的图像的个数。
channels:用来计算直方图的channes的数组。比如输入是2副图像,第一副图像有0,1,2共三个channel,第二幅图像只有0一个channel,那么输入就一共有4个channes,如果int
channels[3] = {3, 2, 0},那么就表示是使用第二副图像的第一个通道和第一副图像的第2和第0个通道来计算直方图。 mask:掩码。如果mask不为空,那么它必须是一个8位(CV_8U)的数组,并且它的大小的和arrays[i]的大小相同,值为1的点将用来计算直方图。 hist:计算出来的直方图 dims:计算出来的直方图的维数。 histSize:在每一维上直方图的个数。简单把直方图看作一个一个的竖条的话,就是每一维上竖条的个数。
ranges:用来进行统计的范围。比如 float rang1[] = {0, 20};float rang2[] = {30,
40}; const float*rangs[] = {rang1, rang2};那么就是对0,20和30,40范围的值进行统计。 uniform:每一个竖条的宽度是否相等。 accumulate: 是否累加。如果为true,在下次计算的时候不会首先清空hist。
画直线,在图像img中画一条颜色为color,粗细为thickness,类型为lineType的直线 (3)line() rectangle()画出直方图 void line(Mat& img, Point pt1, Pointpt2, const Scalar& color, int thickness = 1, int lineType = 8, int shift = 0) //两点确认一条直线。 //lineType:直线类型 //shift:坐标小数点维数
//画一个单一的实矩形 void rectangle(Mat& img, Point pt1, Point pt2, const Scalar& color, int thickness = 1, int lineType = 8, int shift = 0) //一条对角线的两个顶点可确定一个矩形 //pt1和pt2互为对顶点 //thickness为负值表示矩形为实矩形 三、绘制一维灰度直方图 #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace cv; using namespace std;
void Help() { printf("\n\n\t\t\t欢迎来到直方图的世界!\n"); printf("\n\n---------------------------------------------------------\n"); }
int main() { Mat srcImage = imread("lena.jpg", 0); imshow("原图", srcImage); if (!srcImage.data) { cout << "fail to load image" << endl; return 0; } system("color 1F"); Help();
MatND dstHist; // 在cv中用CvHistogram *hist = cvCreateHist int dims = 1; float hranges[2] = { 0, 255 }; const float* ranges[1] = { hranges }; // 这里需要为const类型 int size = 256; int channels = 0; //计算图像的直方图 calcHist(&srcImage, 1, &channels, Mat(), dstHist, dims, &size, ranges); // cv 中是cvCalcHist int scale = 1; Mat dstImage(size * scale, size, CV_8U, Scalar(0)); //获取最大值和最小值 double minValue = 0; double maxValue = 0; minMaxLoc(dstHist, &minValue, &maxValue, 0, 0); // 在cv中用的是cvGetMinMaxHistValue //绘制出直方图 int hpt = saturate_cast<int>(0.9 * size); for (int i = 0; i < 256; i++) { float binValue = dstHist.at<float>(i); // 注意hist中是float类型 int realValue = saturate_cast<int>(binValue * hpt / maxValue); //rectangle(dstImage,Point(i*scale, size - 1), Point((i+1)*scale - 1, size - realValue), Scalar(255)); line(dstImage, Point(i * scale, size - 1), Point((i + 1) * scale - 1, size - realValue), Scalar(255)); } imshow("一维直方图", dstImage); waitKey(0); return 0; } 其实我们有时候想改变量化,上面的表示是256,我们可以50 100都可以,这里用过滑块的知识,所以不想多讲,只是提供一个别人写的参考,尊重原作者,写的很好
#include "cv.h" #include "highgui.h" #include <stdio.h> #include <ctype.h> using namespace std; using namespace cv;
IplImage* src = 0; IplImage* histimg = 0; CvHistogram* hist = 0;
int hdims = 50; // 划分HIST的初始个数,越高越精确
//滚动条函数 void HIST(int t) { float hranges_arr[] = { 0,255 }; float* hranges = hranges_arr; int bin_w; int bin_u; float max; int i; char string[10]; CvFont font; cvInitFont(&font, CV_FONT_HERSHEY_PLAIN, 1, 1, 0, 1, 8);//字体结构初始化 if (hdims == 0) { printf("直方图条数不能为零!\n"); } else { hist = cvCreateHist(1, &hdims, CV_HIST_ARRAY, &hranges, 1); // 创建直方图 histimg = cvCreateImage(cvSize(800, 512), 8, 3); cvZero(histimg); cvCalcHist(&src, hist, 0, 0); // 计算直方图 cvGetMinMaxHistValue(hist, NULL, &max, NULL, NULL);//寻找最大值及其位置 //printf("max_val:%f \n",max_val); cvZero(histimg);
double bin_w = (double)histimg->width / hdims; // hdims: 条的个数,则 bin_w 为条的宽度 double bin_u = (double)histimg->height / max; //// max: 最高条的像素个数,则 bin_u 为单个像素的高度
// 画直方图 for (int i = 0; i < hdims; i++) { CvPoint p0 = cvPoint(i * bin_w, histimg->height); int val = cvGetReal1D(hist->bins, i); CvPoint p1 = cvPoint((i + 1) * bin_w, histimg->height - cvGetReal1D(hist->bins, i) * bin_u); cvRectangle(histimg, p0, p1, cvScalar(0, 255), 1, 8, 0); } //画纵坐标刻度(像素个数) int kedu = 0; for (int i = 1; kedu < max; i++) { kedu = i * max / 10; itoa(kedu, string, 10);//把一个整数转换为字符串 //在图像中显示文本字符串 cvPutText(histimg, string, cvPoint(0, histimg->height - kedu * bin_u), &font, CV_RGB(0, 255, 255)); } //画横坐标刻度(像素灰度值) kedu = 0; for (int i = 1; kedu < 256; i++) { kedu = i * 20; itoa(kedu, string, 10);//把一个整数转换为字符串 //在图像中显示文本字符串 cvPutText(histimg, string, cvPoint(kedu * (histimg->width / 256), histimg->height), &font, CV_RGB(255, 0, 0)); }
cvShowImage("Histogram", histimg); } }
int main(int argc, char** argv) { argc = 2; argv[1] = "lena.jpg";
if (argc != 2 || (src = cvLoadImage(argv[1], 0)) == NULL) // force to gray image return -1;
cvNamedWindow("src", 1); cvShowImage("src", src); cvNamedWindow("Histogram", 1);
cvCreateTrackbar("hdims", "src", &hdims, 256, HIST); HIST(0); cvWaitKey(0);
cvDestroyWindow("src"); cvDestroyWindow("Histogram"); cvReleaseImage(&src); cvReleaseImage(&histimg); cvReleaseHist(&hist);
return 0; }
三、彩色直方图 这里不想过多的介绍,以后讲彩色图像的时候会具体的说 四、matlab辅助 一个imhist()函数就搞定了 clear; %%读入图像 a=imread('cameraman.tif'); imhist(a); title('原始cameraman图像的直方图');
对比度亮度改变 一张图像来说,会有不同的亮暗程度,很多时候都要增强一下,增强的方法有很多,从大量可以说是线性变换和非线性变换,当然这是说空间域的,频率域的暂时不考虑。 线性变换增强,也是对点的操作,如下图 一、点操作,线性增强 两种常用的点过程(即点算子),是用常数对点进行 乘法 和 加法 运算: 两个参数 和 一般称作 增益 和 偏置 参数。我们往往用这两个参数来分别控制 对比度 和 亮度 。 你可以把 看成源图像像素,把 看成输出图像像素。这样一来,上面的式子就能写得更清楚些: 其中, 和 表示像素位于 第i行 和 第j列
让我看看这个应用,核心就是上面的公式 #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream>
using namespace std; using namespace cv;
double alpha; /**< 控制对比度 */ int beta; /**< 控制亮度 */
int main(int argc, char** argv) { /// 读入用户提供的图像 Mat image = imread("lena.jpg"); Mat new_image = Mat::zeros(image.size(), image.type());
/// 初始化 //cout << " Basic Linear Transforms " << endl; //cout << "-------------------------" << endl; //cout << "* Enter the alpha value [1.0-3.0]: "; //cin >> alpha; //cout << "* Enter the beta value [0-100]: "; //cin >> beta; int alpha = 2; int beta = 20; /// for循环执行运算 new_image(i,j) = alpha*image(i,j) + beta for (int y = 0; y < image.rows; y++) { for (int x = 0; x < image.cols; x++) { for (int c = 0; c < 3; c++) { new_image.at<Vec3b>(y, x)[c] = saturate_cast<uchar>(alpha * (image.at<Vec3b>(y, x)[c]) + beta); } } } /// 创建窗口 namedWindow("Original Image", 1); namedWindow("New Image", 1);
/// 显示图像 imshow("Original Image", image); imshow("New Image", new_image);
/// 等待用户按键 waitKey(); return 0; } 二、带滑动条的图像对比度增强 int createTrackbar(conststring& trackbarname, conststring& winname, int* value, int count, TrackbarCallback onChange = 0, void* userdata = 0); 第一个参数,const string&类型的trackbarname,表示轨迹条的名字,用来代表我们创建的轨迹条。 第二个参数,const string&类型的winname,填窗口的名字,表示这个轨迹条会依附到哪个窗口上,即对应namedWindow()创建窗口时填的某一个窗口名。 第三个参数,int* 类型的value,一个指向整型的指针,表示滑块的位置。并且在创建时,滑块的初始位置就是该变量当前的值。 第四个参数,int类型的count,表示滑块可以达到的最大位置的值。PS:滑块最小的位置的值始终为0。 第五个参数,TrackbarCallback类型的onChange,首先注意他有默认值0。这是一个指向回调函数的指针,每次滑块位置改变时,这个函数都会进行回调。并且这个函数的原型必须为void
XXXX(int,void*);其中第一个参数是轨迹条的位置,第二个参数是用户数据(看下面的第六个参数)。如果回调是NULL指针,表示没有回调函数的调用,仅第三个参数value有变化。 第六个参数,void*类型的userdata,他也有默认值0。这个参数是用户传给回调函数的数据,用来处理轨迹条事件。如果使用的第三个参数value实参是全局变量的话,完全可以不去管这个userdata参数。
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include "opencv2/imgproc/imgproc.hpp" #include <iostream>
using namespace std; using namespace cv;
static void ContrastAndBright(int, void*);
int g_nContrastValue; //对比度值 int g_nBrightValue; //亮度值 Mat g_srcImage, g_dstImage;
int main() {
g_srcImage = imread("lena.jpg"); g_dstImage = Mat::zeros(g_srcImage.size(), g_srcImage.type()); g_nContrastValue = 80; g_nBrightValue = 80; namedWindow("【效果图窗口】", 1); //创建轨迹条 createTrackbar("对比度:", "【效果图窗口】", &g_nContrastValue, 300, ContrastAndBright); createTrackbar("亮 度:", "【效果图窗口】", &g_nBrightValue, 200, ContrastAndBright); //调用回调函数 ContrastAndBright(g_nContrastValue, 0); ContrastAndBright(g_nBrightValue, 0); //输出一些帮助信息 cout << endl << "\t运行成功,请调整滚动条观察图像效果\n\n" << "\t按下“q”键时,程序退出\n"; //按下“q”键时,程序退出 while (char(waitKey(1)) != 'q') {} return 0; }
static void ContrastAndBright(int, void*) {
namedWindow("【原始图窗口】", 1); // 三个for循环,执行运算 g_dstImage(i,j) = a*g_srcImage(i,j) + b for (int y = 0; y < g_srcImage.rows; y++) { for (int x = 0; x < g_srcImage.cols; x++) { for (int c = 0; c < 3; c++) { g_dstImage.at<Vec3b>(y, x)[c] = saturate_cast<uchar>((g_nContrastValue * 0.01) * (g_srcImage.at<Vec3b>(y, x)[c]) + g_nBrightValue); } } } // 显示图像 imshow("【原始图窗口】", g_srcImage); imshow("【效果图窗口】", g_dstImage); }
三、Matlab辅助 maltab中主要用的函数是imadjust() J = imadjust(I,[low_in high_in],[low_out high_out]) clear all; I = imread('pout.tif'); J = imadjust(I); subplot(1,4,1);imshow(I), xlabel('(a)原始图像') subplot(1,4,2), imshow(J) xlabel('(b)增强对比度所得图像') K=imadjust(I,[0.4,0.8],[]); %对指定的灰度范围进行图像增强处理 subplot(1,4,3), imshow(K) xlabel('(c)指定对比度范围增强图像') K1=imadjust(I,[],[0.4 0.6]); %对指定的灰度范围进行图像增强处理 subplot(1,4,4), imshow(K1) xlabel('(d)指定对比度范围增强图像') K2=imadjust(I,[0.5 0.8],[],0.5); %对指定的灰度范围进行图像增强处理 figure imshow(K2)
直方图均衡化与直方图拉伸 一、直方图均衡化
直方图均衡化是灰度变换的一个重要应用,广泛应用在图像增强处理中,它是以累计分布函数变换为基础的直方图修正法,可以产生一幅灰度级分布具有均匀概率密度的图像,扩展了像素的取值动态范围。许多图像的灰度值是非均匀分布的,其中灰度值集中在一个小区间内的图像是很常见的,直方图均衡化是一种通过重新均匀地分布各灰度值来增强图像对比度的方法,经过直方图均衡化的图像对二值化阈值选取十分有利。一般来说,直方图修正能提高图像的主观质量,因此在处理艺术图像时非常有用。直方图均衡化处理的中心思想是把原始图像的灰度直方图从比较集中的某个灰度区间变成在全部灰度范围内的均匀分布。
opencv中的调用就是下面这个函数,很方便 void equalizeHist(InputArray src,OutputArraydst ); (1)通常用来增加许多图像的全局对比度,尤其是当图像的有用数据的对比度相当接近的时候。 (2)亮度可以更好地在直方图上分布。这样就可以用于增强局部的对比度而不影响整体的对比度,直方图均衡化通过有效地扩展常用的亮度来实现这种功能。 (3)对于背景和前景都太亮或者太暗的图像非常有用,这种方法尤其是可以带来X光图像中更好的骨骼结构显示以及曝光过度或者曝光不足照片中更好的细节。 (4)一个主要优势是它是一个相当直观的技术并且是可逆操作,如果已知均衡化函数,那么就可以恢复原始的直方图,并且计算量也不大。 (5)一个缺点是它对处理的数据不加选择,它可能会增加背景噪声的对比度并且降低有用信号的对比度。 单独使用直方图均衡化得到图像 #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace cv; using namespace std; int main(int argc, const char** argv) { Mat img = imread("lena.jpg", CV_LOAD_IMAGE_COLOR); //open and read the image if (img.empty()) { cout << "Image cannot be loaded..!!" << endl; return -1; } cvtColor(img, img, CV_BGR2GRAY); //change the color image to grayscale image Mat img_hist_equalized; equalizeHist(img, img_hist_equalized); //equalize the histogram //create windows namedWindow("Original Image", CV_WINDOW_AUTOSIZE); namedWindow("Histogram Equalized", CV_WINDOW_AUTOSIZE); //show the image imshow("Original Image", img); imshow("Histogram Equalized", img_hist_equalized); waitKey(0); //wait for key press destroyAllWindows(); //destroy all open windows return 0; }
因为还是直方图均衡化,所以会用到直方图,如果单独使用直方图均衡化的话,就只有一个函数,所以这里可以处理图像后显示直方图和均衡化图像 #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> #include<opencv2\core\core.hpp> using namespace cv; using namespace std; int img_Hist(Mat& image) { if (!image.data) { cout << "fail to load image" << endl; return 0; } Mat img_gray; //GRAY if (image.channels() == 3) { cvtColor(image, img_gray, CV_BGR2GRAY); } else { image.copyTo(img_gray); } cv::imwrite("img_gray.jpg", img_gray);
MatND hist; int dims = 1; float hranges[] = { 0, 255 }; const float* ranges[] = { hranges }; // 这里需要为const类型 int size = 256; int channels = 0; // 计算图像的直方图 calcHist(&img_gray, 1, &channels, Mat(), hist, dims, &size, ranges); // cv 中是cvCalcHist int scale = 1; Mat imageShow(size * scale, size, CV_8U, Scalar(0)); // 获取最大值和最小值 double minVal = 0; double maxVal = 0; minMaxLoc(hist, &minVal, &maxVal, 0, 0); // cv中用的是cvGetMinMaxHistValue //显示直方图的图像 int hpt = saturate_cast<int>(0.9 * size);
for (int i = 0; i < 256; i++) { float value = hist.at<float>(i); // 注意hist中是float类型, cv中用cvQueryHistValue_1D int realValue = saturate_cast<int>(value * hpt / maxVal); rectangle(imageShow, Point(i * scale, size - 1), Point((i + 1) * scale - 1, size - realValue), Scalar(255)); } namedWindow("Hist"); imshow("Hist", imageShow); cv::imwrite("hist.jpg", imageShow); Mat equalize_Hist; cv::equalizeHist(img_gray, equalize_Hist);
namedWindow("equalize_Hist"); imshow("equalize_Hist", equalize_Hist); cv::imwrite("equalize_Hist.jpg", equalize_Hist); // 计算图像的直方图 calcHist(&equalize_Hist, 1, &channels, Mat(), hist, dims, &size, ranges); // cv 中是cvCalcHist Mat imageShow_equal(size * scale, size, CV_8U, Scalar(0)); // 获取最大值和最小值 minMaxLoc(hist, &minVal, &maxVal, 0, 0); // cv中用的是cvGetMinMaxHistValue //显示直方图的图像 hpt = saturate_cast<int>(0.9 * size); for (int i = 0; i < 256; i++) { float value = hist.at<float>(i); // 注意hist中是float类型, cv中用cvQueryHistValue_1D int realValue = saturate_cast<int>(value * hpt / maxVal); rectangle(imageShow_equal, Point(i * scale, size - 1), Point((i + 1) * scale - 1, size - realValue), Scalar(255)); }
namedWindow("Hist_equalize"); imshow("Hist_equalize", imageShow_equal); cv::imwrite("Hist_equalize.jpg", imageShow_equal); waitKey(0); return 0; } int main(int args, char** argv) { Mat image = imread("lena.jpg", 1); // 这里也可以是BGR 但是想想提取轮廓 效果是一样的 imshow("original", image); img_Hist(image); waitKey(); return 0; }
二、直方图拉伸 变换函数:将图像的一种灰度值经过变换得到另一个灰度。 直方图变换的核心就是变换函数,s=T(r),r是变换前的灰度值,s是变换后的灰度值,如要我们想将[a,b]区间的灰度变换到[0,255]范围内,则变换函数是:T(r)=255*(r-a)/(b-a)。
先要找到imin imax int imax, imin; for (imin = 0; imin < 256; imin++) { if (hist.at<uchar>(imin) > minValue) break; } for (imax = 255; imax > -1; imax--) { if (hist.at<uchar>(imax) > minValue) break; } 然后有一个映射函数 Mat lookup(1, 256, CV_8U); for (int i = 0; i < 256; i++) { if (lut.at<uchar>(i) < imin) lut.at<uchar>(i) = 0; else if (lut.at<uchar>(i) > imax) lut.at<uchar>(i) = 255; else lut.at<uchar>(i) = static_cast<uchar>( 255.0 * (i - imin) / (imax - imin) + 0.5); }
最后用LUT LUT(image,lut,result); 好文章分享http://blog.csdn.net/cv_ronny/article/details/17507671
三、Matlab辅助 clear all; I = imread('pout.tif'); subplot(2,2,1);imshow(I); title('原始图像'); J = histeq(I); subplot(2,2,2);imshow(J); title('图像均衡化') subplot(2,2,3);imhist(I) title('原始图像直方图') subplot(2,2,4);imhist(J) title('均衡化图像直方图')
一个窗口多图显示 前面介绍了如何批量的读取图片,从而也会有批量的显示一堆图片,那么在平时我们显示图片的时候,会发现都是一个图片一个窗口,会出来很多,这时候就会想到matlab中我们经常会使用subplot显示多张图片在一个窗口,之前http://blog.csdn.net/qq_20823641/article/details/51910066这篇文章也提供了一种方法,可以参考一下,经过进一步学习,从简单到复杂,再一次认为subplot(),感觉越来越有意思,虽然不是最好的,但是是入门比较好的,其中很多部分都可以进行优化,以后会在精通部分进行展示,这里已入门为主。 一、版本1 int main(void) { vector<Mat> imgs(2); imgs[0] = imread("cm.png"); imgs[1] = imread("cm.png"); Mat dispImg; int x, y; x = imgs[0].cols; y = imgs[0].rows; int max; max = (x > y) ? x : y; int dstsize = max; dispImg.create(Size(dstsize * (1 + 1) + 100, dstsize), CV_8UC3); int nImg = (int)imgs.size(); for (int i = 0; i < nImg; i++) {
int m = 20 + i * max; int n = 20; Mat imgROI = dispImg(Rect(m, n, (int)x, (int)y)); resize(imgs[i], imgROI, Size((int)x, (int)y));
} namedWindow("winName"); imshow("winName", dispImg); waitKey(); return 0; }
二、版本2 int main(void) { vector<Mat> imgs(6); imgs[0] = imread("cm.png"); imgs[1] = imread("cm.png"); imgs[2] = imread("lina.png"); imgs[3] = imread("dr.png"); imgs[4] = imread("pom.png"); imgs[5] = imread("qop.png"); imshowMany("DOTA2_Hero", imgs); waitKey(); return 0; } void imshowMany(const std::string& _winName, const vector<Mat>& _imgs) { int nImg = (int)_imgs.size(); Mat dispImg; int size; int x, y; // w - Maximum number of images in a row // h - Maximum number of images in a column int w, h; // scale - How much we have to resize the image float scale; int max; if (nImg <= 0) { printf("Number of arguments too small....\n"); return; } else if (nImg > 12) { printf("Number of arguments too large....\n"); return; } else if (nImg == 1) { w = h = 1; size = 300; } else if (nImg == 2) { w = 2; h = 1; size = 300; } else if (nImg == 3 || nImg == 4) { w = 2; h = 2; size = 300; } else if (nImg == 5 || nImg == 6) { w = 3; h = 2; size = 200; } else if (nImg == 7 || nImg == 8) { w = 4; h = 2; size = 200; } else { w = 4; h = 3; size = 150; } dispImg.create(Size(100 + size * w, 60 + size * h), CV_8UC3); for (int i = 0, m = 20, n = 20; i < nImg; i++, m += (20 + size)) { x = _imgs[i].cols; y = _imgs[i].rows; max = (x > y) ? x : y; scale = (float)((float)max / size); if (i % w == 0 && m != 20) { m = 20; n += 20 + size; } Mat imgROI = dispImg(Rect(m, n, (int)(x / scale), (int)(y / scale))); resize(_imgs[i], imgROI, Size((int)(x / scale), (int)(y / scale))); } namedWindow(_winName); imshow(_winName, dispImg); }
http://blog.csdn.net/yang_xian521/article/details/7915396 http://kanwoerzi./blog/1304073 三、Matlab辅助 matlab就一个subplot()就搞定了,参考这个最后给出了matlab的代码和图片http://blog.csdn.net/qq_20823641/article/details/51910066
滑动条控制直方图、对比度、亮度、图像相加
经过前面的学习,有了对比度,直方图的基础,所以就想着用这滑动条做一个综合的实例,用滑动条去控制直方图,去滑动条控制对比度和亮度,用滑动条控制融合,也是对基础的一点提高,其中还有很多值得改进的,可以动态显示结果之类的,这里就不多介绍。以后有机会再优化。 学习之前,要知道几个函数的使用方法,其他的出现的函数或者用法,请参考前面的基础例子 一、滑动条的综合示例 int createTrackbar(conststring& trackbarname, conststring& winname,int* value, int count, TrackbarCallback onChange=0,void* userdata=0); 第一个参数,const string&类型的trackbarname,表示轨迹条的名字,用来代表我们创建的轨迹条。 第二个参数,const string&类型的winname,填窗口的名字,表示这个轨迹条会依附到哪个窗口上,即对应namedWindow()创建窗口时填的某一个窗口名。 第三个参数,int* 类型的value,一个指向整型的指针,表示滑块的位置。并且在创建时,滑块的初始位置就是该变量当前的值。 第四个参数,int类型的count,表示滑块可以达到的最大位置的值。PS:滑块最小的位置的值始终为0。 第五个参数,TrackbarCallback类型的onChange,首先注意他有默认值0。这是一个指向回调函数的指针,每次滑块位置改变时,这个函数都会进行回调。并且这个函数的原型必须为void
XXXX(int,void*);其中第一个参数是轨迹条的位置,第二个参数是用户数据(看下面的第六个参数)。如果回调是NULL指针,表示没有回调函数的调用,仅第三个参数value有变化。 第六个参数,void*类型的userdata,他也有默认值0。这个参数是用户传给回调函数的数据,用来处理轨迹条事件。如果使用的第三个参数value实参是全局变量的话,完全可以不去管这个userdata参数。
void addWeighted(InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1);
第一个参数,InputArray类型的src1,表示需要加权的第一个数组,常常填一个Mat。 第二个参数,alpha,表示第一个数组的权重 第三个参数,src2,表示第二个数组,它需要和第一个数组拥有相同的尺寸和通道数。 第四个参数,beta,表示第二个数组的权重值。 第五个参数,dst,输出的数组,它和输入的两个数组拥有相同的尺寸和通道数。 第六个参数,gamma,一个加到权重总和上的标量值。看下面的式子自然会理解。 第七个参数,dtype,输出阵列的可选深度,有默认值-1。;当两个输入数组具有相同的深度时,这个参数设置为-1(默认值),即等同于src1.depth()。 下面就是综合示例#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace std; using namespace cv; #define winname "混合滚动条" Mat src; Mat src1; Mat src2; Mat src3; Mat src4; Mat dst; Mat dispImg1; int sidlervalue = 10; int sidlervalue1 = 10; const int g_max = 100; const int g_max1 = 100; void ontracker(int, void*) {
double alpha = (double)sidlervalue / g_max; double beta = (1 - alpha); addWeighted(src1, alpha, src2, beta, 0.0, dst);
MatND dstHist; float hranges[2] = { 0, 255 }; const float* ranges[1] = { hranges }; int channels = 0; calcHist(&dst, 1, &channels, Mat(), dstHist, 1, &sidlervalue, ranges); Mat dstImage(256, 256, CV_8U, Scalar(0)); double minValue = 0; double maxValue = 0; minMaxLoc(dstHist, &minValue, &maxValue, 0, 0); int hpt = saturate_cast<int>(0.9 * 256); for (int i = 0; i < sidlervalue; i++) { float binValue = dstHist.at<float>(i); int realValue = saturate_cast<int>(binValue * hpt / maxValue); line(dstImage, Point(i, 256 - 1), Point((i + 1) - 1, 256 - realValue), Scalar(255)); } namedWindow(winname); imshow(winname, dst); namedWindow("一维直方图"); imshow("一维直方图", dstImage);
} void ontracker2(int, void*) { for (int i = 0; i < src.rows; i++) { for (int j = 0; j < src.cols; j++) { dst.at<uchar>(i, j) = saturate_cast<uchar>(sidlervalue1 + dst.at<uchar>(i, j)); } } MatND dstHist; float hranges[2] = { 0, 255 }; const float* ranges[1] = { hranges }; int channels = 0; calcHist(&dst, 1, &channels, Mat(), dstHist, 1, &sidlervalue1, ranges); Mat dstImage(256, 256, CV_8U, Scalar(0)); double minValue = 0; double maxValue = 0; minMaxLoc(dstHist, &minValue, &maxValue, 0, 0); int hpt = saturate_cast<int>(0.9 * 256); for (int i = 0; i < sidlervalue1; i++) { float binValue = dstHist.at<float>(i); int realValue = saturate_cast<int>(binValue * hpt / maxValue); line(dstImage, Point(i, 256 - 1), Point((i + 1) - 1, 256 - realValue), Scalar(255)); } namedWindow(winname); imshow(winname, dst); namedWindow("一维直方图"); imshow("一维直方图", dstImage); } int main() { vector<Mat> imgs(2); imgs[0] = imread("peppers.png"); imgs[1] = imread("pillsetc.png"); src4 = imgs[1]; src = imgs[0]; cvtColor(src4, src3, COLOR_BGR2GRAY); cvtColor(src, src2, COLOR_BGR2GRAY); src1 = src3; Mat dispImg; int x, y; x = imgs[0].cols; y = imgs[0].rows; int max; max = (x > y) ? x : y; int dstsize = max; dispImg.create(Size(dstsize * (1 + 1) + 100, dstsize), CV_8UC3); int nImg = (int)imgs.size(); for (int i = 0; i < nImg; i++) {
int m = 20 + i * max; int n = 20; Mat imgROI = dispImg(Rect(m, n, (int)x, (int)y)); resize(imgs[i], imgROI, Size((int)x, (int)y));
} namedWindow("winName"); imshow("winName", dispImg); sidlervalue = 10; namedWindow(winname); char barname[50]; sprintf(barname, "融合 %d", g_max); createTrackbar(barname, winname, &sidlervalue, g_max, ontracker); ontracker(sidlervalue, 0); sidlervalue1 = 1; createTrackbar("对比度", winname, &sidlervalue1, g_max1, ontracker2); ontracker2(sidlervalue1, 0); waitKey(0); return 0; } 结果如下 (1)多图显示
(2)滑动条界面
(3)实时动态显示直方图
二、Matlab辅助 对于matlab的按钮滑块,是GUI里面的内容,这个在前面我写了好几篇,有基础入门,也有图像的进阶链接如下 http://blog.csdn.net/qq_20823641/article/category/6310257
点线圆矩形与鼠标事件 图像中不可少的元素就是点、线、圆、椭圆、矩形,多边形,同时这些也是物体的特征组成单位,在图像识别中必不可少。所以要首先去认识这个元素怎么定义和使用,同时鼠标是电脑的窗口,我们很多的处理都会用到鼠标。本文主要有下面三个部分: (1) 点、线、圆、椭圆、矩形的基础应用 (2)点、线、圆、椭圆、矩形的进阶应用 (3)鼠标事件 一、点、线、圆、椭圆、矩形的基础应用 绘制点的函数:
Point a = Point (600,600); 文字函数putText()函数 void putText( CvArr* img, const char* text, CvPoint org, const CvFont* font,CvScalar color ); img: 输入图像 text: 要显示的字符串 org: 第一个字母左下角的坐标 font: 指向字体结构的指针 color:[1] 文本的颜色. 绘制直线
CV_EXPORTS_W void line(CV_IN_OUT Mat&img, Point pt1, Point pt2, const Scalar& color,int thickness=1, intlineType=8, int shift=0); 绘制椭圆 CV_EXPORTS_W void ellipse(CV_IN_OUTMat& img, Point center, Size axes,double angle, doublestartAngle, double endAngle,const Scalar&color, int thickness=1,int lineType=8, intshift=0);
img: 要绘制椭圆的图像。 center: 椭圆中心点坐标。 axes: 椭圆位于该Size决定的矩形内。(即定义长轴和短轴)。 angle: 椭圆旋转角度。 startAngle: 椭圆开始绘制时角度。 endAngle: 椭圆绘制结束时角度。(若绘制一个完整的椭圆,则startAngle=0, endAngle = 360)。 color: 椭圆的颜色。 thickness: 绘制椭圆线粗。负数表示全部填充。 lineType,shift:同上。
绘制矩形 CV_EXPORTS_W void rectangle(CV_IN_OUTMat& img, Point pt1, Point pt2,const Scalar&color, int thickness=1,int lineType=8, intshift=0);
pt1: 矩形的左上角坐标。 pt2: 矩阵的右下角坐标。
绘制圆 CV_EXPORTS_W void circle(CV_IN_OUT Mat&img, Point center, int radius,const Scalar& color,int thickness=1,int lineType=8, int shift=0); center: 圆心坐标。 radius: 半径。 其余同上。
填充多边形 CV_EXPORTS void fillPoly(Mat& img,const Point** pts,const int* npts, intncontours,const Scalar&color, int lineType=8, int shift=0,Point offset=Point()); pts: 多边形定点集。 npts: 多边形的顶点数目。 ncontours: 要绘制多边形的数量。 offset: 所有点轮廓的可选偏移。 #include<opencv2/core/core.hpp> #include<opencv2/highgui/highgui.hpp> using namespace cv; int main() {
Mat src(500, 500, CV_8UC3, Scalar(0, 0, 255)); system("color 5F"); string words = "A simple example"; putText(src, words, Point(src.rows / 2, src.cols / 4), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(255, 0, 0)); namedWindow("显示字"); imshow("显示字", src); Point center = Point(300, 200); int r = 50; circle(src, center, r, Scalar(0, 0, 0)); ellipse(src, center, Size(250, 100), 0, 30, 360, Scalar(0, 0, 0)); Point a = Point(600, 600); line(src, a, center, Scalar(255, 0, 0)); rectangle(src, a, center, Scalar(255, 0, 0)); imshow("底板", src); waitKey(0); return 0; }
二、点、线、圆、椭圆、矩形的进阶应用#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> using namespace cv; #include <opencv2/imgproc/imgproc.hpp> #define WINDOW_NAME1 "【绘制图1】" //为窗口标题定义的宏 #define WINDOW_NAME2 "【绘制图2】" //为窗口标题定义的宏 #define WINDOW_WIDTH 600//定义窗口大小的宏 void DrawEllipse(Mat img, double angle);//绘制椭圆 void DrawFilledCircle(Mat img, Point center);//绘制圆 void DrawPolygon(Mat img);//绘制多边形 void DrawLine(Mat img, Point start, Point end);//绘制线段
int main(void) { Mat atomImage = Mat::zeros(WINDOW_WIDTH, WINDOW_WIDTH, CV_8UC3); Mat rookImage = Mat::zeros(WINDOW_WIDTH, WINDOW_WIDTH, CV_8UC3);
DrawEllipse(atomImage, 90); DrawEllipse(atomImage, 0); DrawEllipse(atomImage, 45); DrawEllipse(atomImage, -45);
DrawFilledCircle(atomImage, Point(WINDOW_WIDTH / 2, WINDOW_WIDTH / 2));
DrawPolygon(rookImage); rectangle(rookImage, Point(0, 7 * WINDOW_WIDTH / 8), Point(WINDOW_WIDTH, WINDOW_WIDTH), Scalar(0, 255, 255), -1, 8); DrawLine(rookImage, Point(0, 15 * WINDOW_WIDTH / 16), Point(WINDOW_WIDTH, 15 * WINDOW_WIDTH / 16)); DrawLine(rookImage, Point(WINDOW_WIDTH / 4, 7 * WINDOW_WIDTH / 8), Point(WINDOW_WIDTH / 4, WINDOW_WIDTH)); DrawLine(rookImage, Point(WINDOW_WIDTH / 2, 7 * WINDOW_WIDTH / 8), Point(WINDOW_WIDTH / 2, WINDOW_WIDTH)); DrawLine(rookImage, Point(3 * WINDOW_WIDTH / 4, 7 * WINDOW_WIDTH / 8), Point(3 * WINDOW_WIDTH / 4, WINDOW_WIDTH)); imshow(WINDOW_NAME1, atomImage); moveWindow(WINDOW_NAME1, 0, 200); imshow(WINDOW_NAME2, rookImage); moveWindow(WINDOW_NAME2, WINDOW_WIDTH, 200); waitKey(0); return(0); }
void DrawEllipse(Mat img, double angle) { int thickness = 2; int lineType = 8;
ellipse(img, Point(WINDOW_WIDTH / 2, WINDOW_WIDTH / 2), Size(WINDOW_WIDTH / 4, WINDOW_WIDTH / 16), angle, 0, 360, Scalar(255, 129, 0), thickness, lineType); }
void DrawFilledCircle(Mat img, Point center) { int thickness = -1; int lineType = 8;
circle(img, center, WINDOW_WIDTH / 32, Scalar(0, 0, 255), thickness, lineType); }
void DrawPolygon(Mat img) { int lineType = 8;
//创建一些点 Point rookPoints[1][20]; rookPoints[0][0] = Point(WINDOW_WIDTH / 4, 7 * WINDOW_WIDTH / 8); rookPoints[0][1] = Point(3 * WINDOW_WIDTH / 4, 7 * WINDOW_WIDTH / 8); rookPoints[0][2] = Point(3 * WINDOW_WIDTH / 4, 13 * WINDOW_WIDTH / 16); rookPoints[0][3] = Point(11 * WINDOW_WIDTH / 16, 13 * WINDOW_WIDTH / 16); rookPoints[0][4] = Point(19 * WINDOW_WIDTH / 32, 3 * WINDOW_WIDTH / 8); rookPoints[0][5] = Point(3 * WINDOW_WIDTH / 4, 3 * WINDOW_WIDTH / 8); rookPoints[0][6] = Point(3 * WINDOW_WIDTH / 4, WINDOW_WIDTH / 8); rookPoints[0][7] = Point(26 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 8); rookPoints[0][8] = Point(26 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 4); rookPoints[0][9] = Point(22 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 4); rookPoints[0][10] = Point(22 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 8); rookPoints[0][11] = Point(18 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 8); rookPoints[0][12] = Point(18 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 4); rookPoints[0][13] = Point(14 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 4); rookPoints[0][14] = Point(14 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 8); rookPoints[0][15] = Point(WINDOW_WIDTH / 4, WINDOW_WIDTH / 8); rookPoints[0][16] = Point(WINDOW_WIDTH / 4, 3 * WINDOW_WIDTH / 8); rookPoints[0][17] = Point(13 * WINDOW_WIDTH / 32, 3 * WINDOW_WIDTH / 8); rookPoints[0][18] = Point(5 * WINDOW_WIDTH / 16, 13 * WINDOW_WIDTH / 16); rookPoints[0][19] = Point(WINDOW_WIDTH / 4, 13 * WINDOW_WIDTH / 16);
const Point* ppt[1] = { rookPoints[0] }; int npt[] = { 20 };
fillPoly(img, ppt, npt, 1, Scalar(255, 255, 255), lineType); }
void DrawLine(Mat img, Point start, Point end) { int thickness = 2; int lineType = 8; line(img, start, end, Scalar(0, 0, 0), thickness, lineType); } 这只猫一定是哪个很有爱心的博主画的,很是喜欢,所以就推荐它,当然还有示例中给出的,图片是下图,不过上面那个猫更加简洁让人吸收的块,官方例子太长了
三、鼠标事件 setMouseCallback()函数讲解: 函数原型为 void setMouseCallback(conststring& winname, MouseCallback onMouse, void* userdata = 0); 这个函数的讲解见下图: #include <opencv2/opencv.hpp> using namespace cv; #define WINDOW_NAME "【程序窗口】" void on_MouseHandle(int event, int x, int y, int flags, void* param); void DrawRectangle(cv::Mat& img, cv::Rect box); Rect g_rectangle; bool g_bDrawingBox = false;//是否进行绘制 RNG g_rng(12345); int main(int argc, char** argv) {
system("color 9F"); g_rectangle = Rect(-1, -1, 0, 0); Mat srcImage(600, 800, CV_8UC3), tempImage; srcImage.copyTo(tempImage); g_rectangle = Rect(-1, -1, 0, 0); srcImage = Scalar::all(0); namedWindow(WINDOW_NAME); setMouseCallback(WINDOW_NAME, on_MouseHandle, (void*)& srcImage); while (1) { srcImage.copyTo(tempImage); if (g_bDrawingBox) DrawRectangle(tempImage, g_rectangle); imshow(WINDOW_NAME, tempImage); if (waitKey(10) == 27) break; } return 0; }
void on_MouseHandle(int event, int x, int y, int flags, void* param) {
Mat& image = *(cv::Mat*) param; switch (event) {
case EVENT_MOUSEMOVE: { if (g_bDrawingBox) { g_rectangle.width = x - g_rectangle.x; g_rectangle.height = y - g_rectangle.y; } } break;
//左键按下消息 case EVENT_LBUTTONDOWN: { g_bDrawingBox = true; g_rectangle = Rect(x, y, 0, 0);//记录起始点 } break;
//左键抬起消息 case EVENT_LBUTTONUP: { g_bDrawingBox = false;//置标识符为false //对宽和高小于0的处理 if (g_rectangle.width < 0) { g_rectangle.x += g_rectangle.width; g_rectangle.width *= -1; }
if (g_rectangle.height < 0) { g_rectangle.y += g_rectangle.height; g_rectangle.height *= -1; } //调用函数进行绘制 DrawRectangle(image, g_rectangle); } break;
} }
void DrawRectangle(cv::Mat& img, cv::Rect box) {
cv::rectangle(img, box.tl(), box.br(), cv::Scalar(g_rng.uniform(0,
255), g_rng.uniform(0, 255), g_rng.uniform(0, 255)));//随机颜色 }
线性滤波和非线性滤波 一,噪声的介绍和卷积 二、各个滤波函数的解读,定义与源代码 三、综合所有的滤波,加滑动条控制核大小来blur 四、Matlab 辅助表达 一,噪声的介绍
图像噪声是图像在摄取或传输时所受的随机信号干扰,是图像中各种妨碍人们对其信息接受的因素。很多时候将图像噪声看成是多维随机过程,因而描述噪声的方法完全可以借用随机过程的描述,即用其概率分布函数和概率密度分布函数。我们常看到的就是淑艳噪声
salt&pepper,这里对噪声有理论的介绍http://blog.csdn.net/qq_20823641/article/details/51513567,可以学习一下。 二、各个滤波函数的解读,定义与源代码
这里通说噪声分为线性和非线性,官方中给了BoxBlur,Blur,GaussianBlur,medianBlur,bilateralBlur,其中方框滤波和均值滤波有相同有不同,准备说均值滤波是方框滤波的特殊,从函数BoxBlur与Blur也可以看出,下面的图也是一种解释
滤波器:可以说滤波器是一个包含加权系数的窗口,当使用这个滤波器平滑处理图像时,就把这个窗口放到图像上,透过这个窗口来看图像,有时候也叫做核函数 kernel,相信经常看见这个词语。 其中在看到这个窗口的时候我建议大家看看卷积的概念,http://baike.baidu.com/link?url=lQxRSr-C41IoYNqYyOwXE1FfsufcuptxFhBE2AJexoL9bdhSVOaRppD7f4r8T8C5DAR8ggGgQVXrgMmHr-PfJq void boxFilter(InputArray src,OutputArray dst, int ddepth, Size ksize,
Point anchor=Point(-1,-1), boolnormalize=true, int borderType=BORDER_DEFAULT)
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。该函数对通道是独立处理的,且可以处理任意通道数的图片,但需要注意,待处理的图片深度应该为CV_8U,
CV_16U, CV_16S, CV_32F 以及 CV_64F之一。 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。 第三个参数,int类型的ddepth,输出图像的深度,-1代表使用原图深度,即src.depth()。 第四个参数,Size类型(对Size类型稍后有讲解)的ksize,内核的大小。一般这样写Size( w,h )来表示内核的大小( 其中,w 为像素宽度, h为像素高度)。Size(3,3)就表示3x3的核大小,Size(5,5)就表示5x5的核大小 第五个参数,Point类型的anchor,表示锚点(即被平滑的那个点),注意他有默认值Point(-1,-1)。如果这个点坐标是负值的话,就表示取核的中心为锚点,所以默认值Point(-1,-1)表示这个锚点在核的中心。 第六个参数,bool类型的normalize,默认值为true,一个标识符,表示内核是否被其区域归一化(normalized)了。 第七个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT,我们一般不去管它
看到上面的方框滤波的时候,不用太多想,因为我们一般用的是均值滤波,方框滤波是一个过度,同时看下面的均值滤波的时候,还会看到它是调用了boFilter的 void cv::blur(InputArray src, OutputArray dst, Size ksize, Point anchor, int borderType) { boxFilter(src, dst, -1, ksize, anchor, true, borderType); } 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。该函数对通道是独立处理的,且可以处理任意通道数的图片,但需要注意,待处理的图片深度应该为CV_8U,
CV_16U, CV_16S, CV_32F 以及 CV_64F之一。 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。比如可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。 第三个参数,Size类型(对Size类型稍后有讲解)的ksize,内核的大小。一般这样写Size( w,h )来表示内核的大小( 其中,w 为像素宽度, h为像素高度)。Size(3,3)就表示3x3的核大小,Size(5,5)就表示5x5的核大小 第四个参数,Point类型的anchor,表示锚点(即被平滑的那个点),注意他有默认值Point(-1,-1)。如果这个点坐标是负值的话,就表示取核的中心为锚点,所以默认值Point(-1,-1)表示这个锚点在核的中心。 第五个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT,我们一般不去管它。
cv::Ptr<cv::FilterEngine> cv::createBoxFilter(int srcType, int dstType, Size ksize, Point anchor, bool normalize, int borderType) { int sdepth = CV_MAT_DEPTH(srcType); int cn = CV_MAT_CN(srcType), sumType = CV_64F; if (sdepth <= CV_32S && (!normalize || ksize.width * ksize.height <= (sdepth == CV_8U ? (1 << 23) : sdepth == CV_16U ? (1 << 15) : (1 << 16)))) sumType = CV_32S; sumType = CV_MAKETYPE(sumType, cn); Ptr<BaseRowFilter> rowFilter = getRowSumFilter(srcType, sumType, ksize.width, anchor.x); Ptr<BaseColumnFilter> columnFilter = getColumnSumFilter(sumType, dstType, ksize.height, anchor.y, normalize ? 1. / (ksize.width * ksize.height) : 1);
return Ptr<FilterEngine>(new FilterEngine(Ptr<BaseFilter>(0), rowFilter, columnFilter, srcType, dstType, sumType, borderType)); } 高斯函数主要就是看下面这个函数,高斯函数,我只是想说它可能是图像处理一辈子离不开的函数,因为他太好用,在以后的频域学习也会那么好用 void GaussianBlur(InputArray src,OutputArray dst, Size ksize,
double sigmaX, double sigmaY=0, intborderType=BORDER_DEFAULT)
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。它可以是单独的任意通道数的图片,但需要注意,图片深度应该为CV_8U,CV_16U, CV_16S, CV_32F 以及 CV_64F之一。 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。比如可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。 第三个参数,Size类型的ksize高斯内核的大小。其中ksize.width和ksize.height可以不同,但他们都必须为正数和奇数。或者,它们可以是零的,它们都是由sigma计算而来。 第四个参数,double类型的sigmaX,表示高斯核函数在X方向的的标准偏差。 第五个参数,double类型的sigmaY,表示高斯核函数在Y方向的的标准偏差。若sigmaY为零,就将它设为sigmaX,如果sigmaX和sigmaY都是0,那么就由ksize.width和ksize.height计算出来。 为了结果的正确性着想,最好是把第三个参数Size,第四个参数sigmaX和第五个参数sigmaY全部指定到。 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT,我们一般不去管它
高斯的源代码还是很长的,因为后面还有很多的东西,所以这里不展示,想看的可以到这里去http://blog.csdn.net/xiaowei_cqu/article/details/7785365 void medianBlur(InputArray src,OutputArray dst, int ksize) 第一个参数,InputArray类型的src,函数的输入参数,填1、3或者4通道的Mat类型的图像;当ksize为3或者5的时候,图像深度需为CV_8U,CV_16U,或CV_32F其中之一,而对于较大孔径尺寸的图片,它只能是CV_8U。 第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。我们可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。 第三个参数,int类型的ksize,孔径的线性尺寸(aperture linear size),注意这个参数必须是大于1的奇数,比如:3,5,7,9 ...
源代码可以到这里看,也可以到\opencv\sources\modules\imgproc\src\smooth.cpp的第1653行开始,不过看了也没用,精华没有在源代码里面写出来,还要再深入,有兴趣的可以看看http://blog.csdn.net/poem_qianmo/article/details/23184547 双边滤波我是很喜欢的,因为我们都知道smoothing的时候很blur,同时丢失了很多的细节,特别是边缘,那这个以后我对提取边缘轮廓的时候就会感到很不好,但是双边滤波同时达到了要求,这里是有关i他的理论介绍http://blog.csdn.net/qq_20823641/article/details/51533420 void bilateralFilter(InputArray src, OutputArraydst, int d,
double sigmaColor, double sigmaSpace, int borderType=BORDER_DEFAULT)
第一个参数,InputArray类型的src,输入图像,即源图像,需要为8位或者浮点型单通道、三通道的图像。 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。 第三个参数,int类型的d,表示在过滤过程中每个像素邻域的直径。如果这个值我们设其为非正数,那么OpenCV会从第五个参数sigmaSpace来计算出它来。 第四个参数,double类型的sigmaColor,颜色空间滤波器的sigma值。这个参数的值越大,就表明该像素邻域内有更宽广的颜色会被混合到一起,产生较大的半相等颜色区域。 第五个参数,double类型的sigmaSpace坐标空间中滤波器的sigma值,坐标空间的标注方差。他的数值越大,意味着越远的像素会相互影响,从而使更大的区域足够相似的颜色获取相同的颜色。当d>0,d指定了邻域大小且与sigmaSpace无关。否则,d正比于sigmaSpace。 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。
三、综合所有的滤波,加滑动条控制核大小来blur
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <iostream> using namespace std; using namespace cv; Mat g_srcImage, g_dstImage1, g_dstImage2, g_dstImage3, g_dstImage4, g_dstImage5; int g_nBoxFilterValue = 6; //方框滤波内核值 int g_nMeanBlurValue = 10; //均值滤波内核值 int g_nGaussianBlurValue = 6; //高斯滤波内核值 int g_nMedianBlurValue = 10; //中值滤波参数值 int g_nBilateralFilterValue = 10; //双边滤波参数值 static void on_BoxFilter(int, void*); //方框滤波 static void on_MeanBlur(int, void*); //均值块滤波器 static void on_GaussianBlur(int, void*); //高斯滤波器 static void on_MedianBlur(int, void*); //中值滤波器 static void on_BilateralFilter(int, void*); //双边滤波器 int main() { g_srcImage = imread("lena.jpg", 1); g_dstImage1 = g_srcImage.clone(); g_dstImage2 = g_srcImage.clone(); g_dstImage3 = g_srcImage.clone(); g_dstImage4 = g_srcImage.clone(); g_dstImage5 = g_srcImage.clone(); namedWindow("【<0>原图窗口】", 1); imshow("【<0>原图窗口】", g_srcImage); namedWindow("【<1>方框滤波】", 1); createTrackbar("内核值:", "【<1>方框滤波】", &g_nBoxFilterValue, 50, on_BoxFilter); on_MeanBlur(g_nBoxFilterValue, 0); imshow("【<1>方框滤波】", g_dstImage1); namedWindow("【<2>均值滤波】", 1); createTrackbar("内核值:", "【<2>均值滤波】", &g_nMeanBlurValue, 50, on_MeanBlur); on_MeanBlur(g_nMeanBlurValue, 0); namedWindow("【<3>高斯滤波】", 1); createTrackbar("内核值:", "【<3>高斯滤波】", &g_nGaussianBlurValue, 50, on_GaussianBlur); on_GaussianBlur(g_nGaussianBlurValue, 0); namedWindow("【<4>中值滤波】", 1); createTrackbar("参数值:", "【<4>中值滤波】", &g_nMedianBlurValue, 50, on_MedianBlur); on_MedianBlur(g_nMedianBlurValue, 0); namedWindow("【<5>双边滤波】", 1); createTrackbar("参数值:", "【<5>双边滤波】", &g_nBilateralFilterValue, 50, on_BilateralFilter); on_BilateralFilter(g_nBilateralFilterValue, 0); return 0; } static void on_BoxFilter(int, void*) { boxFilter(g_srcImage, g_dstImage1, -1, Size(g_nBoxFilterValue + 1, g_nBoxFilterValue + 1)); imshow("【<1>方框滤波】", g_dstImage1); }
static void on_MeanBlur(int, void*) { blur(g_srcImage, g_dstImage2, Size(g_nMeanBlurValue + 1, g_nMeanBlurValue + 1), Point(-1, -1)); imshow("【<2>均值滤波】", g_dstImage2);
}
static void on_GaussianBlur(int, void*) { GaussianBlur(g_srcImage, g_dstImage3, Size(g_nGaussianBlurValue * 2 + 1, g_nGaussianBlurValue * 2 + 1), 0, 0); imshow("【<3>高斯滤波】", g_dstImage3); }
static void on_MedianBlur(int, void*) { medianBlur(g_srcImage, g_dstImage4, g_nMedianBlurValue * 2 + 1); imshow("【<4>中值滤波】", g_dstImage4); }
static void on_BilateralFilter(int, void*) { bilateralFilter(g_srcImage, g_dstImage5, g_nBilateralFilterValue, g_nBilateralFilterValue * 2, g_nBilateralFilterValue / 2); imshow("【<5>双边滤波】", g_dstImage5); }
四、matlab辅助 h=imread('d:\lena.jpg'); A=fspecial('average',3); output1 = imfilter(h, A, 'conv', 'replicate'); A=fspecial('gaussian',3); output2 = imfilter(h, A, 'conv', 'replicate'); output3 = medfilt2(h, [3, 3]); clear all; close all; clc; img=imread('lena.jpg'); img=mat2gray(img); [m n]=size(img); imshow(img); r=10; imgn=zeros(m+2*r+1,n+2*r+1); imgn(r+1:m+r,r+1:n+r)=img; imgn(1:r,r+1:n+r)=img(1:r,1:n); imgn(1:m+r,n+r+1:n+2*r+1)=imgn(1:m+r,n:n+r); imgn(m+r+1:m+2*r+1,r+1:n+2*r+1)=imgn(m:m+r,r+1:n+2*r+1); imgn(1:m+2*r+1,1:r)=imgn(1:m+2*r+1,r+1:2*r); sigma_d=2; sigma_r=0.1; [x,y] = meshgrid(-r:r,-r:r); w1=exp(-(x.^2+y.^2)/(2*sigma_d^2)); for i=r+1:m+r for j=r+1:n+r w2=exp(-(imgn(i-r:i+r,j-r:j+r)-imgn(i,j)).^2/(2*sigma_r^2)); w=w1.*w2; s=imgn(i-r:i+r,j-r:j+r).*w; imgn(i,j)=sum(sum(s))/sum(sum(w)); end end figure; imshow(mat2gray(imgn(r+1:m+r,r+1:n+r))); 还有另外一个函数调用的方法可以参考这里http://blog.csdn.net/abcjennifer/article/details/7616663
阈值分割、固定阈值Threshold、自适应阈值分割adaptiveThreshold、OSTU大津法
当我们用图像技术来找到图像特征的时候,其中很重要的一个步骤就是分割,不区分出来我们想要的就没有办法得到其中的参数,而这里首先介绍阈值分割,也是最基础的分割方式,一共有下面三个部分组成。本来想介绍一个函数与定义然后再跟着例子,但是感觉有点乱,然后就按现在这样来说了,当后面看到不懂的在前面的函数与定义都会提及到,希望对大家有用。
(1)、函数与定义 (2)、代码应用
(3)、matlab辅助 一、函数与定义 阈值分割是一种区域分割技术,将灰度根据主观愿望分成两个或者多个灰度区间,利用图像中背景和目标物体的灰度上的差异,旋转一个合适的阈值进行分割。 一般的分割有全局分割和局部分割,这是不同的思路,在Opencv中给出了threshold(),adapativeThreshold(),这两个函数中的tpye会给出很多的方法 double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type) 参数信息:第一个参数,InputArray类型的src,输入数组,填单通道 , 8或32位浮点类型的Mat即可。 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,即这个参数用于存放输出结果,且和第一个参数中的Mat变量有一样的尺寸和类型。 第三个参数,double类型的thresh,阈值的具体值。 第四个参数,double类型的maxval,当第五个参数阈值类型type取 THRESH_BINARY 或THRESH_BINARY_INV阈值类型时的最大值. 第五个参数,int类型的type,阈值类型,。
第五个参数,第五参数有以下几种类型 0: THRESH_BINARY 当前点值大于阈值时,取Maxval,也就是第四个参数,下面再不说明,否则设置为0 1: THRESH_BINARY_INV 当前点值大于阈值时,设置为0,否则设置为Maxval 2: THRESH_TRUNC 当前点值大于阈值时,设置为阈值,否则不改变 3: THRESH_TOZERO 当前点值大于阈值时,不改变,否则设置为0 4: THRESH_TOZERO_INV 当前点值大于阈值时,设置为0,否则不改变
源代码中的定义 enum {
CV_THRESH_BINARY = 0, /* value = value > threshold ? max_value : 0 */
CV_THRESH_BINARY_INV = 1, /* value = value > threshold ? 0 : max_value */
CV_THRESH_TRUNC = 2, /* value = value > threshold ? threshold : value */
CV_THRESH_TOZERO = 3, /* value = value > threshold ? value : 0 */
CV_THRESH_TOZERO_INV = 4, /* value = value > threshold ? 0 : value */
CV_THRESH_MASK = 7,
CV_THRESH_OTSU = 8 /* use Otsu algorithm to choose the optimal threshold value; combine the flag with one of the above CV_THRESH_* values */
}; void cvAdaptiveThreshold(const CvArr * src, CvArr * dst, double max_value, int adaptive_method = CV_ADAPTIVE_THRESH_MEAN_C, int threshold_type = CV_THRESH_BINARY, int block_size = 3, double param1 = 5); src 输入图像.dst 输出图像.max_value使用 CV_THRESH_BINARY 和 CV_THRESH_BINARY_INV 的最大值.adaptive_method自适应阈值算法使用:CV_ADAPTIVE_THRESH_MEAN_C 或 CV_ADAPTIVE_THRESH_GAUSSIAN_C (见讨论).threshold_type取阈值类型:必须是下者之一 CV_THRESH_BINARY, CV_THRESH_BINARY_INV
block_size用来计算阈值的象素邻域大小: 3, 5, 7, ...param1与方法有关的参数。对方法 CV_ADAPTIVE_THRESH_MEAN_C 和 CV_ADAPTIVE_THRESH_GAUSSIAN_C, 它是一个从均值或加权均值提取的常数(见讨论), 尽管它可以是负数。 二、代码应用 (1) #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using namespace std; Mat dst, gray, bluriamge; int thresh, thresh1; void on_thresh(int, void*) { threshold(bluriamge, dst, thresh, 255, thresh1); imshow("threshold二值化灰图", dst); }
int main() { Mat src; src = imread("lena.jpg"); cvtColor(src, gray, CV_BGR2GRAY); blur(gray, bluriamge, Size(3, 3)); imshow("threshold二值化灰图", src); namedWindow("threshold二值化灰图"); createTrackbar("阈值:", "threshold二值化灰图", &thresh, 255, on_thresh); createTrackbar("type:", "threshold二值化灰图", &thresh1, 4, on_thresh); on_thresh(0, 0); waitKey(0); return 0; } (2)adaptiveThreshold #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using namespace std; #include <iostream>
int main(int argc, char** argv) { cv::Mat image = cv::imread("lena.jpg", CV_LOAD_IMAGE_GRAYSCALE); if (image.empty()) { std::cout << "read image failure" << std::endl; return -1; }
int th = 100; cv::Mat global; cv::threshold(image, global, th, 255, CV_THRESH_BINARY_INV);
int blockSize = 25; int constValue = 10; cv::Mat local; cv::adaptiveThreshold(image, local, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY_INV, blockSize, constValue);
cv::imshow("globalThreshold", global); cv::imshow("localThreshold", local); cv::waitKey(0);
return 0; } (3)OSTU关于他是什么回事,想看理论的可以到我以前的写一篇文章中,很清晰明了http://blog.csdn.net/qq_20823641/article/details/51480651 #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using namespace std; #include <iostream> int piexlSum; int thresh; float w1, u1, w2, u2, g, gmax;
int main(int argc, uchar* argv[]) { vector<int> hist(256); Mat img = imread("lena.jpg", 0); Mat dst1;
for (auto it = img.begin<uchar>(); it != img.end<uchar>(); ++it) hist[*it]++;
piexlSum = img.cols * img.rows;
for (int i = 0; i != hist.size(); i++) { w1 = w2 = u1 = u2 = 0;
for (int j = 0; j != hist.size(); j++) { float k = (float)hist[j] / piexlSum;
if (j <= i) { w1 += k; u1 += j * k;
} else { w2 += k; u2 += j * k; } }
g = w1 * w2 * (u1 - u2) * (u1 - u2); if (g > gmax) { gmax = g; thresh = i; }
}
threshold(img, dst1, thresh, 255, CV_THRESH_OTSU); imshow("otsu1", dst1); waitKey(0); return 0; } 三、Matlab辅助
I=imread('d:\lena.jpg'); I=rgb2gray(I); figure subplot(1,2,1) imshow(I); title('原图') [width,height]=size(I); level=graythresh(I); BW=im2bw(I,level); subplot(1,2,2) imshow(BW); title('otsu算法阈值分割效果图');
膨胀腐蚀一、形态学操作
就是基于形状的一系列图像处理操作。有很多的,这里先看最简单的操作。 膨胀与腐蚀(Dilation与Erosion)。能实现多种多样的功能,主要如下: 二、记住下面红体字 (1)腐蚀操作描述为:扫描图像的每一个像素,用结构元素与其覆盖的二值图像做“与”操作:如果都为1,结果图像的该像素为1,否则为0。 (2)膨胀操作描述为:扫描图像的每一个像素,用结构元素与其覆盖的二值图像做“与”操作:如果都为0,结果图像的该像素为0,否则为1。 (1)腐蚀运算是由结构元素确定的邻域块中选取图像值与结构元素值的差的最小值。 (2)膨胀运算是由结构元素确定的邻域块中选取图像值与结构元素值的和的最大值。 三、图解: 四、函数与定义 void dilate( InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1, -1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar & borderValue = morphologyDefaultBorderValue() ); 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U,CV_16U,CV_16S,CV_32F或 CV_64F其中之一。 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。 第三个参数,InputArray类型的kernel,膨胀操作的核。若为NULL时,表示的是使用参考点位于中心3x3的核。
第四个参数,Point类型的anchor,锚的位置,其有默认值(-1,-1),表示锚位于中心。 第五个参数,int类型的iterations,迭代使用erode()函数的次数,默认值为1。 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。 第七个参数,const
Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般我们不用去管他。需要用到它时,可以看官方文档中的createMorphologyFilter()函数得到更详细的解释。
对于核来说有三个 矩形: MORPH_RECT 交叉形: MORPH_CROSS 椭圆形: MORPH_ELLIPSE 基础用法如下
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸//获取自定义核Mat element = getStructuringElement(MORPH_RECT, Size(2 * g_nStructElementSize + 1, 2 * g_nStructElementSize + 1), Point(g_nStructElementSize, g_nStructElementSize)); 对于腐蚀和膨胀其实差不多,定义如下,具体的解释裤和上面差不多,这里不再重复介绍这些参数了 下面来看一下具体的例子 #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include "highgui.h" #include <stdlib.h> #include <stdio.h> using namespace cv; Mat src, erosion_dst, dilation_dst; int erosion_elem = 0; int erosion_size = 0; int dilation_elem = 0; int dilation_size = 0; int const max_elem = 2; int const max_kernel_size = 21; void Erosion(int, void*); void Dilation(int, void*); int main(int argc, char** argv) { src = imread("1.png"); if (!src.data) { return -1; } namedWindow("Erosion Demo", CV_WINDOW_AUTOSIZE); namedWindow("Dilation Demo", CV_WINDOW_AUTOSIZE); cvMoveWindow("Dilation Demo", src.cols, 0); createTrackbar("Element:\n 0: Rect \n 1: Cross \n 2: Ellipse", "Erosion Demo", &erosion_elem, max_elem, Erosion); createTrackbar("Kernel size:\n 2n +1", "Erosion Demo", &erosion_size, max_kernel_size, Erosion); createTrackbar("Element:\n 0: Rect \n 1: Cross \n 2: Ellipse", "Dilation Demo", &dilation_elem, max_elem, Dilation); createTrackbar("Kernel size:\n 2n +1", "Dilation Demo", &dilation_size, max_kernel_size, Dilation); Erosion(0, 0); Dilation(0, 0); waitKey(0); return 0; } void Erosion(int, void*) { int erosion_type; if (erosion_elem == 0) { erosion_type = MORPH_RECT; } else if (erosion_elem == 1) { erosion_type = MORPH_CROSS; } else if (erosion_elem == 2) { erosion_type = MORPH_ELLIPSE; }
Mat element = getStructuringElement(erosion_type, Size(2 *
erosion_size + 1, 2 * erosion_size + 1), Point(erosion_size,
erosion_size)); erode(src, erosion_dst, element); imshow("Erosion Demo", erosion_dst); }
void Dilation(int, void*) { int dilation_type; if (dilation_elem == 0) { dilation_type = MORPH_RECT; } else if (dilation_elem == 1) { dilation_type = MORPH_CROSS; } else if (dilation_elem == 2) { dilation_type = MORPH_ELLIPSE; }
Mat element = getStructuringElement(dilation_type, Size(2 *
dilation_size + 1, 2 * dilation_size + 1), Point(dilation_size,
dilation_size)); dilate(src, dilation_dst, element); imshow("Dilation Demo", dilation_dst); } 五、Matlab辅助 I=imread('d:\1.png');SE=strel('rectangle',[3 3]);I2=imdilate(I,SE);I3=imerode(I,SE);figuresubplot(121),imshow(I2),title('膨胀')subplot(122),imshow(I3),title('腐蚀')
开运算、闭运算、顶帽、黑帽、形态学梯度、形态学角点、细化、填充一、全体成员 经过了上一篇的膨胀、腐蚀以后,我们就可以用他们组合起来,形成了更多的形态效果,这样就不会太多的改变原来图像的大小,总结了一下,主要包含开运算、闭运算、顶帽、黑帽、形态学梯度、形态学角点、细化、填充这些方面。 1.开运算 对图像进行先腐蚀后膨胀的操作就是图像的开运算。 它的功能是有利于移走黑色前景下的白色小物体。 2.闭运算 对图像进行先膨胀后腐蚀的操作就是图像的闭运算。 它的功能是有利于移走黑色区域小洞。 3.形态学梯度 形态学梯度是一幅图像腐蚀和膨胀的差值。 它有利于查找图像的轮廓。 4.Top Hat Top Hat是输入图像和它开运算的差值。 5.Black Hat Black Hat是输入图像和它闭运算的差值。 6.形态学角点dst=open-open 7细化 这个比较的复杂,讲原理的话估计要还几页http://www.cnblogs.com/mikewolf2002/p/3321732.html,可以看看这篇文章说的很多,也很好
二、morphologyEx函数 其实,说了那么多大多数都是和一个函数有关系那就是morphologyEx函数,这里面给出了多种形态学类型的定义 void cv::morphologyEx(InputArray _src, OutputArray _dst, int op, InputArray kernel, Pointanchor, int iterations, int borderType, constScalar & borderValue) { //拷贝Mat数据到临时变量 Mat src = _src.getMat(), temp; _dst.create(src.size(), src.type()); Mat dst = _dst.getMat();
//一个大switch,根据不同的标识符取不同的操作 switch (op) { case MORPH_ERODE: //腐蚀 erode(src, dst, kernel, anchor, iterations, borderType, borderValue); break; case MORPH_DILATE: //膨胀 dilate(src, dst, kernel, anchor, iterations, borderType, borderValue); break; case MORPH_OPEN: //开运算 erode(src, dst, kernel, anchor, iterations, borderType, borderValue); dilate(dst, dst, kernel, anchor, iterations, borderType, borderValue); break; case CV_MOP_CLOSE: //闭运算 dilate(src, dst, kernel, anchor, iterations, borderType, borderValue); erode(dst, dst, kernel, anchor, iterations, borderType, borderValue); break; case CV_MOP_GRADIENT: //形态学梯度 erode(src, temp, kernel, anchor, iterations, borderType, borderValue); dilate(src, dst, kernel, anchor, iterations, borderType, borderValue); dst -= temp; break; case CV_MOP_TOPHAT: //顶帽 if (src.data != dst.data) temp = dst; erode(src, temp, kernel, anchor, iterations, borderType, borderValue); dilate(temp, temp, kernel, anchor, iterations, borderType, borderValue); dst = src - temp; break; case CV_MOP_BLACKHAT: //黑帽 if (src.data != dst.data) temp = dst; dilate(src, temp, kernel, anchor, iterations, borderType, borderValue); erode(temp, temp, kernel, anchor, iterations, borderType, borderValue); dst = temp - src; break; default: CV_Error(CV_StsBadArg, "unknown morphological operation"); } } 三、开运算小例子 为了好理解,简单说明一个例子,其他的参考,改变参数就可以,下面是使用开运算作为例子 int main() { Mat image = imread("1.jpg"); namedWindow("【原始图】开运算"); namedWindow("【效果图】开运算"); imshow("【原始图】开运算", image); Mat element = getStructuringElement(MORPH_RECT, Size(15, 15)); morphologyEx(image, image, MORPH_OPEN, element); imshow("【效果图】开运算", image); waitKey(0); return 0; }
四、综合成员 相信学习那么长时间,上面的一看就明白了,那么就可以进去综合代码 #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> using namespace cv; Mat src, dst; int morph_elem = 0; int morph_size = 0; int morph_operator = 0; int const max_operator = 4; int const max_elem = 2; int const max_kernel_size = 21; char* window_name = "Morphology Transformations Demo"; void Morphology_Operations(int, void*); int main(int argc, char** argv) { src = imread("lena.jpg"); if (!src.data) { return -1; } namedWindow(window_name, CV_WINDOW_AUTOSIZE); createTrackbar("Operator:\n 0: Opening - 1: Closing \n 2: Gradient - 3: Top Hat \n 4: Black Hat", window_name, &morph_operator, max_operator, Morphology_Operations); createTrackbar("Element:\n 0: Rect - 1: Cross - 2: Ellipse", window_name, &morph_elem, max_elem, Morphology_Operations); createTrackbar("Kernel size:\n 2n +1", window_name, &morph_size, max_kernel_size, Morphology_Operations); Morphology_Operations(0, 0); waitKey(0); return 0; } void Morphology_Operations(int, void*) { int operation = morph_operator + 2; Mat element = getStructuringElement(morph_elem, Size(2 * morph_size + 1, 2 * morph_size + 1), Point(morph_size, morph_size)); morphologyEx(src, dst, operation, element); imshow(window_name, dst); }
再看来看看角点检测,其实也是膨胀腐蚀的变形,只是没有一个好名字 Mat result2; dilate(image, result2, x); erode(result2, result2, square); imshow("result2", result2); absdiff(result2, result, result); imshow("result3", result); threshold(result, result, 40, 255, THRESH_BINARY);
细化,这个比较的复杂,需要好好的理解一下 图像细化(Image Thinning),一般指二值图像的骨架化(Image Skeletonization) 的一种操作运算。细化就是经过一层层的剥离,从原来的图中去掉一些点,但仍要保持原来的形状,直到得到图像的骨架。骨架,可以理解为图象的中轴。 #include <opencv2/opencv.hpp> #include <opencv2/core/core.hpp> #include <iostream> #include <vector> cv::Mat thinImage(const cv::Mat & src, const int maxIterations = -1) { assert(src.type() == CV_8UC1); cv::Mat dst; int width = src.cols; int height = src.rows; src.copyTo(dst); int count = 0; while (true) { count++; if (maxIterations != -1 && count > maxIterations) break; std::vector<uchar*> mFlag; //用于标记需要删除的点 //对点标记 for (int i = 0; i < height; ++i) { uchar* p = dst.ptr<uchar>(i); for (int j = 0; j < width; ++j) { //如果满足四个条件,进行标记 // p9 p2 p3 // p8 p1 p4 // p7 p6 p5 uchar p1 = p[j]; if (p1 != 1) continue; uchar p4 = (j == width - 1) ? 0 : *(p + j + 1); uchar p8 = (j == 0) ? 0 : *(p + j - 1); uchar p2 = (i == 0) ? 0 : *(p - dst.step + j); uchar p3 = (i == 0 || j == width - 1) ? 0 : *(p - dst.step + j + 1); uchar p9 = (i == 0 || j == 0) ? 0 : *(p - dst.step + j - 1); uchar p6 = (i == height - 1) ? 0 : *(p + dst.step + j); uchar p5 = (i == height - 1 || j == width - 1) ? 0 : *(p + dst.step + j + 1); uchar p7 = (i == height - 1 || j == 0) ? 0 : *(p + dst.step + j - 1); if ((p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) >= 2 && (p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) <= 6) { int ap = 0; if (p2 == 0 && p3 == 1) ++ap; if (p3 == 0 && p4 == 1) ++ap; if (p4 == 0 && p5 == 1) ++ap; if (p5 == 0 && p6 == 1) ++ap; if (p6 == 0 && p7 == 1) ++ap; if (p7 == 0 && p8 == 1) ++ap; if (p8 == 0 && p9 == 1) ++ap; if (p9 == 0 && p2 == 1) ++ap; if (ap == 1 && p2 * p4 * p6 == 0 && p4 * p6 * p8 == 0) { mFlag.push_back(p + j); } } } } //将标记的点删除 for (std::vector<uchar*>::iterator i = mFlag.begin(); i != mFlag.end(); ++i) { **i = 0; } if (mFlag.empty()) { break; } else { mFlag.clear();//将mFlag清空 } for (int i = 0; i < height; ++i) { uchar* p = dst.ptr<uchar>(i); for (int j = 0; j < width; ++j) { uchar p1 = p[j]; if (p1 != 1) continue; uchar p4 = (j == width - 1) ? 0 : *(p + j + 1); uchar p8 = (j == 0) ? 0 : *(p + j - 1); uchar p2 = (i == 0) ? 0 : *(p - dst.step + j); uchar p3 = (i == 0 || j == width - 1) ? 0 : *(p - dst.step + j + 1); uchar p9 = (i == 0 || j == 0) ? 0 : *(p - dst.step + j - 1); uchar p6 = (i == height - 1) ? 0 : *(p + dst.step + j); uchar p5 = (i == height - 1 || j == width - 1) ? 0 : *(p + dst.step + j + 1); uchar p7 = (i == height - 1 || j == 0) ? 0 : *(p + dst.step + j - 1); if ((p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) >= 2 && (p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) <= 6) { int ap = 0; if (p2 == 0 && p3 == 1) ++ap; if (p3 == 0 && p4 == 1) ++ap; if (p4 == 0 && p5 == 1) ++ap; if (p5 == 0 && p6 == 1) ++ap; if (p6 == 0 && p7 == 1) ++ap; if (p7 == 0 && p8 == 1) ++ap; if (p8 == 0 && p9 == 1) ++ap; if (p9 == 0 && p2 == 1) ++ap; if (ap == 1 && p2 * p4 * p8 == 0 && p2 * p6 * p8 == 0) { //标记 mFlag.push_back(p + j); } } } } for (std::vector<uchar*>::iterator i = mFlag.begin(); i != mFlag.end(); ++i) { **i = 0; } if (mFlag.empty()) { break; } else { mFlag.clear();//将mFlag清空 } } return dst; } int main(int argc, char* argv[]) { if (argc != 2) { std::cout << "参数个数错误!" << std::endl; return -1; } cv::Mat src = cv::imread(argv[1], cv::IMREAD_GRAYSCALE); if (src.empty()) { std::cout << "读取文件失败!" << std::endl; return -1; } cv::threshold(src, src, 128, 1, cv::THRESH_BINARY); //图像细化 cv::Mat dst = thinImage(src); //显示图像 dst = dst * 255; cv::namedWindow("src1", CV_WINDOW_AUTOSIZE); cv::namedWindow("dst1", CV_WINDOW_AUTOSIZE); cv::imshow("src1", src); cv::imshow("dst1", dst); cv::waitKey(0); }
填充可以参考这篇文章http://lib.csdn.net/article/opencv/28355 #include "cv.h" #include "highgui.h"
void FillInternalContours(IplImage * pBinary, double dAreaThre) { double dConArea; CvSeq* pContour = NULL; CvSeq* pConInner = NULL; CvMemStorage* pStorage = NULL; // 执行条件 if (pBinary) { // 查找所有轮廓 pStorage = cvCreateMemStorage(0); cvFindContours(pBinary, pStorage, &pContour, sizeof(CvContour), CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE); // 填充所有轮廓 cvDrawContours(pBinary, pContour, CV_RGB(255, 255, 255), CV_RGB(255, 255, 255), 2, CV_FILLED, 8, cvPoint(0, 0)); // 外轮廓循环 int wai = 0; int nei = 0; for (; pContour != NULL; pContour = pContour->h_next) { wai++; // 内轮廓循环 for (pConInner = pContour->v_next; pConInner != NULL; pConInner = pConInner->h_next) { nei++; // 内轮廓面积 dConArea = fabs(cvContourArea(pConInner, CV_WHOLE_SEQ)); printf("%f\n", dConArea); if (dConArea <= dAreaThre) { cvDrawContours(pBinary, pConInner, CV_RGB(255, 255, 255), CV_RGB(255, 255, 255), 0, CV_FILLED, 8, cvPoint(0, 0)); } } } printf("wai = %d, nei = %d", wai, nei); cvReleaseMemStorage(&pStorage); pStorage = NULL; } } int Otsu(IplImage* src) { int height = src->height; int width = src->width;
//histogram float histogram[256] = { 0 }; for (int i = 0; i < height; i++) { unsigned char* p = (unsigned char*)src->imageData + src->widthStep * i; for (int j = 0; j < width; j++) { histogram[*p++]++; } } //normalize histogram int size = height * width; for (int i = 0; i < 256; i++) { histogram[i] = histogram[i] / size; }
//average pixel value float avgValue = 0; for (int i = 0; i < 256; i++) { avgValue += i * histogram[i]; //整幅图像的平均灰度 }
int threshold; float maxVariance = 0; float w = 0, u = 0; for (int i = 0; i < 256; i++) { w += histogram[i]; //假设当前灰度i为阈值, 0~i 灰度的像素(假设像素值在此范围的像素叫做前景像素) 所占整幅图像的比例 u += i * histogram[i]; // 灰度i 之前的像素(0~i)的平均灰度值: 前景像素的平均灰度值
float t = avgValue * w - u; float variance = t * t / (w * (1 - w)); if (variance > maxVariance) { maxVariance = variance; threshold = i; } }
return threshold; }
int main() { IplImage* img = cvLoadImage("lena.jpg", 0); IplImage* bin = cvCreateImage(cvGetSize(img), 8, 1);
int thresh = Otsu(img); cvThreshold(img, bin, thresh, 255, CV_THRESH_BINARY);
FillInternalContours(bin, 200);
cvNamedWindow("img"); cvShowImage("img", img);
cvNamedWindow("result"); cvShowImage("result", bin);
cvWaitKey(-1);
cvReleaseImage(&img); cvReleaseImage(&bin);
return 0; }
五、matlab辅助 I=imread('d:\22.png'); SE=strel('rectangle',[3 3]); I2=imopen(I,SE); I3=imclose(I,SE); I4=im2bw(I); I5=bwmorph(I4,'thin',inf) figure subplot(131),imshow(I2),title('开运算') subplot(132),imshow(I3),title('闭运算') subplot(133),imshow(I5),title('细化')
击中击不中 在我们学习了膨胀腐蚀和基于膨胀腐蚀的变化之后,我比较喜欢的一个是击中击不中,因为喜欢所以就要单独列出来,心里总是觉得他可以有很多的用处,以后模版匹配,特征检测都会用,更深入的是,他会加深对膨胀腐蚀的理解,是一个很好的例子。 下面先看一个算法步骤和原理: Hit-miss算法步骤: 击中击不中变换是形态学中用来检测特定形状所处位置的一个基本工具。它的原理就是使用腐蚀;如果要在一幅图像A上找到B形状的目标,我们要做的是: 首先,建立一个比B大的模板W;使用此模板对图像A进行腐蚀,得到图像假设为Process1; 其次,用B减去W,从而得到V模板(W-B);使用V模板对图像A的补集进行腐蚀,得到图像假设为Process2; 然后,Process1与Process2取交集;得到的结果就是B的位置。这里的位置可能不是B的中心位置,要视W-B时对齐的位置而异;
其实很简单,两次腐蚀,然后交集,结果就出来了。 Hit-miss原理: 基于腐蚀运算的一个特性:腐蚀的过程相当于对可以填入结构元素的位置作标记的过程。 腐蚀中,虽然标记点取决于原点在结构元素中的相对位置,但输出图像的形状与此无关,改变原点的位置,只会导致输出结果发生平移。 既然腐蚀的过程相当于对可以填入结构元素的位置作标记的过程,可以利用腐蚀来确定目标的位置。 进行目标检测,既要检测到目标的内部,也要检测到外部,即在一次运算中可以同时捕获内外标记。 由于以上两点,采用两个结构基元H、M,作为一个结构元素对B=(H,M),一个探测目标内部,一个探测目标外部。当且仅当H平移到某一点可填入X的内部,M平移到该点可填入X的外部时,该点才在击中击不中变换的输出中。 opencv应用 Mat src; src = imread("2312.png", 0); Mat input_image = src; Mat Kernel_S1 = imread("2311.png"); cvtColor(Kernel_S1, Kernel_S1, CV_RGB2GRAY); int threhold = 180; threshold(input_image, input_image, threhold, 255, CV_THRESH_BINARY); threshold(Kernel_S1, Kernel_S1, threhold, 255, CV_THRESH_BINARY); imshow("二值化图像", input_image); Mat BigBlankimage = Mat::ones(input_image.size(), input_image.type()); input_image = BigBlankimage * 255 - input_image; imshow("反置图像", input_image); Mat Blankimage = Mat::ones(Kernel_S1.rows, Kernel_S1.cols, CV_8UC1); Mat Kernel_S2 = Blankimage * 255 - Kernel_S1; imshow("核1", Kernel_S1); imshow("核2", Kernel_S2); Mat hit_result, hit_result1, hit_result2; erode(input_image, hit_result1, Kernel_S1, Point(-1, -1), 1, BORDER_DEFAULT, 0); imshow("hit_result1", hit_result1); erode(input_image, hit_result2, Kernel_S2, Point(-1, -1), 1, BORDER_DEFAULT, 0); imshow("hit_result2", hit_result2); hit_result = hit_result1 & hit_result2; imshow("击中击不中", hit_result);
matlab应用之函数使用与自定义实现只要定义好s1 s2 调用bwhitmiss Ihm = bwhitmiss(I, S1, S2);% I为输入图像% S1、S2为结构元素 根据冈萨雷斯的书的说明,如下图,然后用matlab编写代码,最后得到图像
clear all; I = zeros(120,180); I(11:80,16:75) = 1; I(56:105,86:135) = 1; I(26:55,141:170) = 1; figure,imshow(I); se = zeros(58,58); se(5:54,5:54) = 1; figure,imshow(se); %击中击不中变换 Ie1 = imerode(I,se); figure,imshow(Ie1); Ic = 1 - I; figure,imshow(Ic); S2 = 1 - se; figure;imshow(S2); Ie2 = imerode(Ic,S2); figure,imshow(Ie2); Ihm = Ie1 & Ie2; figure,imshow(Ihm); 这里我们看一下结果
Robert,prewitt,Sobel边缘检测 图像的边缘检测,是根据灰度的突变或者说不连续来检测,对于其中的算子有一阶导数和二价导数,这里先说基础的三种方法---Robert,prewitt,Sobel边缘检测。 一、梯度
首先介绍下梯度,梯度并非是一个数值,梯度严格意义上是一个向量,这个向量指向当前位置变化最快的方向,可以这么理解,当你站在一个山上,你有360°的方向可以选择,哪个方向下降速度最快(最陡峭),便是梯度方向,梯度的长度,表示为向量的长度,表示最大的变化速率。 梯度的数学表达: 在二维中只取前两项,也就是由x方向的偏微分和y方向的偏微分组成。对于图像f中点(x,y)处的梯度,定义为:
与上面所述保持一致,图像梯度方向给出图像变化最快方向,当前点的梯度长度为:
次长度计算中有平方和开平方,所以将不再是现行操作。 为了简单计算,将上面求距离简化成:
二、RObert Roberts边缘算子是一个2x2的模板,采用的是对角方向相邻的两个像素之差。从图像处理的实际效果来看,边缘定位较准,对噪声敏感。适用于边缘明显且噪声较少的图像分割。Roberts边缘检测算子是一种利用局部差分算子寻找边缘的算子,Robert算子图像处理后结果边缘不是很平滑。经分析,由于Robert算子通常会在图像边缘附近的区域内产生较宽的响应,故采用上述算子检测的边缘图像常需做细化处理,边缘定位的精度不是很高。标准一阶差分不同,Robert采用对角线差分,边缘定位准,但是对噪声敏感。适用于边缘明显且噪声较少的图像分割。Roberts边缘检测算子是一种利用局部差分算子寻找边缘的算子,Robert算子图像处理后结果边缘不是很平滑。经分析,由于Robert算子通常会在图像边缘附近的区域内产生较宽的响应,故采用上述算子检测的边缘图像常需做细化处理,边缘定位的精度不是很高。
// roberts算子实现 cv::Mat roberts(cv::Mat srcImage) { cv::Mat dstImage = srcImage.clone(); int nRows = dstImage.rows; int nCols = dstImage.cols; for (int i = 0; i < nRows - 1; i++) { for (int j = 0; j < nCols - 1; j++) { int t1 = (srcImage.at<uchar>(i, j) - srcImage.at<uchar>(i + 1, j + 1)) * (srcImage.at<uchar>(i, j) - srcImage.at<uchar>(i + 1, j + 1)); int t2 = (srcImage.at<uchar>(i + 1, j) - srcImage.at<uchar>(i, j + 1)) * (srcImage.at<uchar>(i + 1, j) - srcImage.at<uchar>(i, j + 1)); dstImage.at<uchar>(i, j) = (uchar)sqrt(t1 + t2);
} } return dstImage; } 三、prewitt Prewitt算子是一种一阶微分算子的边缘检测,利用像素点上下、左右邻点的灰度差,在边缘处达到极值检测边缘,去掉部分伪边缘,对噪声具有平滑作用 。其原理是在图像空间利用两个方向模板与图像进行邻域卷积来完成的,这两个方向模板一个检测水平边缘,一个检测垂直边缘。 对数字图像f(x,y),Prewitt算子的定义如下: G(i)=|[f(i-1,j-1)+f(i-1,j)+f(i-1,j+1)]-[f(i+1,j-1)+f(i+1,j)+f(i+1,j+1)]| G(j)=|[f(i-1,j+1)+f(i,j+1)+f(i+1,j+1)]-[f(i-1,j-1)+f(i,j-1)+f(i+1,j-1)]| 则 P(i,j)=max[G(i),G(j)]或 P(i,j)=G(i)+G(j) 经典Prewitt算子认为:凡灰度新值大于或等于阈值的像素点都是边缘点。即选择适当的阈值T,若P(i,j)≥T,则(i,j)为边缘点,P(i,j)为边缘图像。这种判定是欠合理的,会造成边缘点的误判,因为许多噪声点的灰度值也很大,而且对于幅值较小的边缘点,其边缘反而丢失了。 Prewitt算子对噪声有抑制作用,抑制噪声的原理是通过像素平均,但是像素平均相当于对图像的低通滤波,所以Prewitt算子对边缘的定位不如Roberts算子。 因为平均能减少或消除噪声,Prewitt梯度算子法就是先求平均,再求差分来求梯度。水平和垂直梯度模板分别为: 检测水平边沿 横向模板
检测垂直平边沿 纵向模板:
该算子与Sobel算子类似,只是权值有所变化,但两者实现起来功能还是有差距的,据经验得知Sobel要比Prewitt更能准确检测图像边缘。
对噪声有抑制作用,抑制噪声的原理是通过像素平均,但是像素平均相当于对图像的低通滤波,所以Prewitt算子对边缘的定位不如Roberts算子。
// roberts算子实现 Mat prewitt(Mat imageP) { cvtColor(imageP, imageP, CV_RGB2GRAY); float prewittx[9] = { -1,0,1, -1,0,1, -1,0,1 }; float prewitty[9] = { 1,1,1, 0,0,0, -1,-1,-1 }; Mat px = Mat(3, 3, CV_32F, prewittx); cout << px << endl; Mat py = Mat(3, 3, CV_32F, prewitty); cout << py << endl; Mat dstx = Mat(imageP.size(), imageP.type(), imageP.channels()); Mat dsty = Mat(imageP.size(), imageP.type(), imageP.channels()); Mat dst = Mat(imageP.size(), imageP.type(), imageP.channels()); filter2D(imageP, dstx, imageP.depth(), px); filter2D(imageP, dsty, imageP.depth(), py); float tempx, tempy, temp; for (int i = 0; i < imageP.rows; i++) { for (int j = 0; j < imageP.cols; j++) { tempx = dstx.at<uchar>(i, j); tempy = dsty.at<uchar>(i, j); temp = sqrt(tempx * tempx + tempy * tempy); dst.at<uchar>(i, j) = temp; } } return dst; }
四、Sobel
其主要用于边缘检测,在技术上它是以离散型的差分算子,用来运算图像亮度函数的梯度的近似值, Sobel算子是典型的基于一阶导数的边缘检测算子,由于该算子中引入了类似局部平均的运算,因此对噪声具有平滑作用,能很好的消除噪声的影响。Sobel算子对于象素的位置的影响做了加权,与Prewitt算子、Roberts算子相比因此效果更好。 Sobel算子包含两组3x3的矩阵,分别为横向及纵向模板,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。实际使用中,常用如下两个模板来检测图像边缘。 检测水平边沿 横向模板 :
检测垂直平边沿 纵向模板: 图像的每一个像素的横向及纵向梯度近似值可用以下的公式结合,来计算梯度的大小。
然后可用以下公式计算梯度方向。
在以上例子中,如果以上的角度Θ等于零,即代表图像该处拥有纵向边缘,左方较右方暗。 缺点是Sobel算子并没有将图像的主题与背景严格地区分开来,换言之就是Sobel算子并没有基于图像灰度进行处理,由于Sobel算子并没有严格地模拟人的视觉生理特征,所以提取的图像轮廓有时并不能令人满意。 Sobel
算子是一个主要用作边缘检测的离散微分算子 (discrete differentiation operator)。
它Sobel算子结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。在图像的任何一点使用此算子,将会产生对应的梯度矢量或是其法矢量。因为Sobel算子结合了高斯平滑和分化(differentiation),因此结果会具有更多的抗噪性。大多数情况下,我们使用sobel函数时,取【xorder
= 1,yorder = 0,ksize = 3】来计算图像X方向的导数,【xorder = 0,yorder = 1,ksize =
3】来计算图像y方向的导数。 计算图像X方向的导数,取【xorder= 1,yorder = 0,ksize = 3】情况对应的内核: 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); 第一个参数,InputArray 类型的src,为输入图像,填Mat类型即可。 第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。 第三个参数,int类型的ddepth,输出图像的深度,支持如下src.depth()和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 第四个参数,int类型dx,x 方向上的差分阶数。 第五个参数,int类型dy,y方向上的差分阶数。 第六个参数,int类型ksize,有默认值3,表示Sobel核的大小;必须取1,3,5或7。 第七个参数,double类型的scale,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。我们可以在文档中查阅getDerivKernels的相关介绍,来得到这个参数的更多信息。 第八个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。 第九个参数, int类型的borderType,我们的老朋友了(万年是最后一个参数),边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。 #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 = "Sobel Demo - Simple Edge Detector"; int scale = 1;//默认值 int delta = 0;//默认值 int ddepth = CV_16S;//防止输出图像深度溢出 int c; src = imread("lena.jpg"); 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); //生成 grad_x and grad_y Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; // Gradient X x方向梯度 1,0:x方向计算微分即导数 //Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT ); Sobel(src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT); convertScaleAbs(grad_x, abs_grad_x); // Gradient Y y方向梯度 0,1:y方向计算微分即导数 //Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT ); 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; }
五、Matlab clear all; I=imread('d:\lena.jpg'); I=rgb2gray(I);%灰度图像 subplot(1,4,1);imshow(I); xlabel('(a)原始图像'); %sobel算子 BW2=edge(I,'sobel'); subplot(1,4,2);imshow(BW2); xlabel('(c)sobel算子') %prewitt算子 BW3=edge(I,'prewitt'); subplot(1,4,3);imshow(BW3); xlabel('(d)prewitt算子') %roberts算子 BW4=edge(I,'roberts'); subplot(1,4,4);imshow(BW4); xlabel('(e)roberts算子')
laplace LOG DOG边缘检测
经过了上一篇的简单的边缘检测,现在来看一下二阶导数的边缘检测,分别是Laplace LOG
DOG,看到他们心里还是有点遗憾,要是自己能加快一点学习的步伐,在面试的时候也许就可以轻松回答了,亲爱的你们只是来的晚了2天。希望和我一样的同学,要加快脚步,认真学习了。废话不再多说,让我看看是怎么回事。 一、Laplacian Laplacian算子定义为
表示成模板的形式就是 。Laplacian算子另外一种形式是,也经常使用。Laplace算子是一种各向同性算子,在只关心边缘的位置而不考虑其周围的象素灰度差值时比较合适。Laplace算子对孤立象素的响应要比对边缘或线的响应要更强烈,因此只适用于无噪声图象。存在噪声情况下,使用Laplacian算子检测边缘之前需要先进行低通滤波。
二、LOG 看到这里,可能觉得按照习惯上面我少了Laplace的代码, 这是因为Laplace算子对噪声很敏感,所有要进行高斯滤波然后再laplace,过度到LOG,似乎也很舒服。 对于高斯先要有个基础
这里我用图片画出来,一个是好理解,还有就是和下面的DOG进行比较
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include "opencv2/imgproc/imgproc.hpp" #include <iostream> int main() { cv::Mat srcImage = cv::imread("lena.jpg", 0); if (!srcImage.data) return -1;
// 高斯平滑 //加上高斯平滑效果边缘检测效果更好,原因未知 GaussianBlur(srcImage, srcImage, cv::Size(3, 3), 0, 0, cv::BORDER_DEFAULT); cv::Mat dstImage; // 拉普拉斯变换 Laplacian(srcImage, dstImage, CV_16S, 3); convertScaleAbs(dstImage, dstImage); cv::imshow("srcImage", srcImage); cv::imshow("dstImage", dstImage); cv::waitKey(0); return 0; }
三、DOG
高斯差分算子,看着名字就能知道是2个高斯差,因为LOG滤波器有无限长的拖尾,若取得很大尺寸,将使得计算不堪重负。但随着:的增加,LOG滤波器幅值迅速下降,当r大于一定程度时,可以忽略模板的作用,这就为节省计算量创造了条件。实际计算时,常常取n*n大小的LOG滤波器,近似n=3σ。另外,LOG滤波器可以近似为两个指数函数之差,即DOG
( Difference Of twoGaussians functions)。
图像画出来就是
GaussianBlur(img, img_G0, Size(3, 3), 0); GaussianBlur(img_G0, img_G1, Size(3, 3), 0); Mat img_DoG = img_G0 - img_G1; normalize(img_DoG, img_DoG, 255, 0, CV_MINMAX);
四、matlab辅助 I=imread("lena.jpg");
I=rgb2gray(I)I1=edge(I,'log');figure,imshow(I1)</span>
canny算子边缘检测 最后来看看canny算子,这个是被成为最好的算子,因为过程多,有准测,后面会列出来,也是边缘检测的最后一个,所以这里作为结尾,来看看各个边缘检测的效果。 边缘检测结果比较 Roberts算子检测方法对具有陡峭的低噪声的图像处理效果较好,但是利用roberts算子提取边缘的结果是边缘比较粗,因此边缘的定位不是很准确。 Sobel算子检测方法对灰度渐变和噪声较多的图像处理效果较好,sobel算子对边缘定位不是很准确,图像的边缘不止一个像素。 Prewitt算子检测方法对灰度渐变和噪声较多的图像处理效果较好。但边缘较宽,而且间断点多。 Laplacian算子法对噪声比较敏感,所以很少用该算子检测边缘,而是用来判断边缘像素视为与图像的明区还是暗区。 Canny方法不容易受噪声干扰,能够检测到真正的弱边缘。优点在于,使用两种不同的阈值分别检测强边缘和弱边缘,并且当弱边缘和强边缘相连时,才将弱边缘包含在输出图像中
canny对边缘检测质量进行分析时,有3个原则: canny边缘检测的基本思想是:首先对图像选择一定的Gauss滤波器进行平滑滤波,然后采用非极值抑制技术进行处理得到最后的边缘图像。 具体步骤: 1、用高斯滤波器平滑图像 对图像进行高斯滤波,听起来很玄乎,其实就是根据待滤波的像素点及其邻域点的灰度值按照一定的参数规则进行加权平均。这样可以有效滤去理想图像中叠加的高频噪声。
2、用一阶偏导的有限差分来计算梯度的幅值和方向 图像灰度值得梯度可使用一阶有限差分来进行近似,这样就可以得图像在x和y方向上偏导数的两个矩阵。常用的梯度算子就是Roberts.sobel,prewitt.,canny
3、对梯度幅值进行非极大值抑制 图像梯度幅值矩阵中的元素值越大,说明图像中该点的梯度值越大,但这不不能说明该点就是边缘(这仅仅是属于图像增强的过程)。在Canny算法中,非极大值抑制是进行边缘检测的重要步骤,通俗意义上是指寻找像素点局部最大值,将非极大值点所对应的灰度值置为0,这样可以剔除掉一大部分非边缘的点
4、用双阈值算法检测和连接边缘 Canny算法中减少假边缘数量的方法是采用双阈值法。选择两个阈值(关于阈值的选取方法在扩展中进行讨论),根据高阈值得到一个边缘图像,这样一个图像含有很少的假边缘,但是由于阈值较高,产生的图像边缘可能不闭合,未解决这样一个问题采用了另外一个低阈值。
通俗的来说:就是在进行边缘检测时,还是要用到滤波减小噪声,先通过在水平和垂直方向的一阶偏导,求得梯度的幅值和方向,这样每个点都可能有4中方向情况(0,45,90,135度),在局部范围内,保留在同一方向上,梯度最大的点,非最大就置零,最后使用2个阈值T1和T2(T1<T2),T2用来找到每条线段,T1用来在这些线段的两个方向上延伸寻找边缘的断裂处,并连接这些边缘。 void Canny(InputArray image, OutputArray edges, double threshold1, double threshold2, int apertureSize = 3, bool L2gradient = false) 第一个参数,InputArray类型的image,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。 第二个参数,OutputArray类型的edges,输出的边缘图,需要和源图片有一样的尺寸和类型。 第三个参数,double类型的threshold1,第一个滞后性阈值。 第四个参数,double类型的threshold2,第二个滞后性阈值。 第五个参数,int类型的apertureSize,表示应用Sobel算子的孔径大小,其有默认值3。 第六个参数,bool类型的L2gradient,一个计算图像梯度幅值的标识,有默认值false。
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> #include <iostream> using namespace cv; using namespace std; Mat src, src_gray; Mat dst, detected_edges; int edgeThresh = 1; int lowThreshold; int const max_lowThreshold = 100; int ratio = 3; int kernel_size = 3; const char* window_name = "Edge Map"; static void CannyThreshold(int, void*) {
Canny(src_gray, detected_edges, lowThreshold, lowThreshold * ratio, kernel_size); dst = Scalar::all(0); src.copyTo(dst, detected_edges); imshow(window_name, dst); } int main(int, char** argv) {
src = imread("D:\\lena.jpg", CV_LOAD_IMAGE_COLOR); if (!src.data) { return -1; } dst.create(src.size(), src.type()); cvtColor(src, src_gray, CV_BGR2GRAY); namedWindow(window_name, CV_WINDOW_AUTOSIZE); createTrackbar("Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold); CannyThreshold(0, 0); waitKey(0); return 0; } matlab I=imread('d:\lena.jpg'); I1=rgb2gray(I); img1=edge(I1,'canny',[0.03,0.08],3); subplot(121),imshow(I); subplot(122),imshow(img1)
hough变换检测直线与圆 今天要看的是霍夫变换,常用用来检测直线和圆,这里是把常见的笛卡尔坐标系转换成极坐标下,进行累计峰值的极大值,确定。HoughLines,HoughLinesP,HoughCircles,三个函数,首先先看看原理,最后会用漂亮的matlab图,来回归一下,霍夫直线变换。 霍夫线变换:众所周知, 一条直线在图像二维空间可由两个变量表示. 例如:
对于霍夫变换, 我们将用 极坐标系 来表示直线. 因此, 直线的表达式可为:
化简得: 在 笛卡尔坐标系: 可由参数: 斜率和截距表示. 在 极坐标系: 可由参数: 极径和极角表示
一般来说对于点 ,
我们可以将通过这个点的一族直线统一定义为:
这就意味着每一对 代表一条通过点 的直线. 如果对于一个给定点 我们在极坐标对极径极角平面绘出所有通过它的直线,
将得到一条正弦曲线. 例如, 对于给定点 and 我们可以绘出下图
(在平面 - ): 只绘出满足下列条件的点 and . 我们可以对图像中所有的点进行上述操作. 如果两个不同点进行上述操作后得到的曲线在平面 - 相交,
这就意味着它们通过同一条直线. 例如, 接上面的例子我们继续对点: , 和点 , 绘图,
得到下图: 这三条曲线在 - 平面相交于点 ,
坐标表示的是参数对 () 或者是说点 ,
点 和点 组成的平面内的的直线.
OpenCV实现了以下三种霍夫线变换: 标准霍夫线变换,多尺度霍夫变换
统计概率霍夫线变换 void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn = 0, double stn = 0)第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。 第二个参数,InputArray类型的lines,经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量表示,其中,是离坐标原点((0,0)(也就是图像的左上角)的距离。 是弧度线条旋转角度(0~垂直线,π/2~水平线)。 第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。PS:Latex中/rho就表示 。 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。 第六个参数,double类型的srn,有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。 第七个参数,double类型的stn,有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
void HoughLinesP(InputArray _image, OutputArray _lines, double rho, double theta, int threshold, double minLineLength, double maxGap) 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。 第二个参数,InputArray类型的lines,经过调用HoughLinesP函数后后存储了检测到的线条的输出矢量,每一条线由具有四个元素的矢量(x_1,y_1,
x_2, y_2) 表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点。 第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。 第六个参数,double类型的minLineLength,有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。 第七个参数,double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大的距离。 综合上面2个函数,来看看如何检测直线#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace cv; using namespace std; int main(int argc, char** argv) { const char* filename = argc >= 2 ? argv[1] : "lena.jpg"; Mat src = imread(filename, 0); if (src.empty()) { help(); cout << "can not open " << filename << endl; return -1; }
Mat dst, cdst; Canny(src, dst, 50, 200, 3); cvtColor(dst, cdst, CV_GRAY2BGR); #if 0 vector<Vec2f> lines; HoughLines(dst, lines, 1, CV_PI / 180, 100, 0, 0); for (size_t i = 0; i < lines.size(); i++) { float rho = lines[i][0], theta = lines[i][1]; Point pt1, pt2; double a = cos(theta), b = sin(theta); double x0 = a * rho, y0 = b * rho; pt1.x = cvRound(x0 + 1000 * (-b)); pt1.y = cvRound(y0 + 1000 * (a)); pt2.x = cvRound(x0 - 1000 * (-b)); pt2.y = cvRound(y0 - 1000 * (a)); line(cdst, pt1, pt2, Scalar(0, 0, 255), 3, CV_AA); } #else vector<Vec4i> lines; HoughLinesP(dst, lines, 1, CV_PI / 180, 50, 50, 10); for (size_t i = 0; i < lines.size(); i++) { Vec4i l = lines[i]; line(cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0, 0, 255), 3, CV_AA); } #endif imshow("source", src); imshow("detected lines", cdst);
waitKey();
return 0; }
#include <opencv2/opencv.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> using namespace std; using namespace cv; Mat g_srcImage, g_dstImage, g_midImage; vector<Vec4i> g_lines; int g_nthreshold = 100; static void on_HoughLines(int, void*); int main() { Mat g_srcImage = imread("lena.jpg"); imshow("【原始图】", g_srcImage); namedWindow("【效果图】", 1); createTrackbar("值", "【效果图】", &g_nthreshold, 200, on_HoughLines); Canny(g_srcImage, g_midImage, 50, 200, 3); cvtColor(g_midImage, g_dstImage, CV_GRAY2BGR); on_HoughLines(g_nthreshold, 0); HoughLinesP(g_midImage, g_lines, 1, CV_PI / 180, 80, 50, 10); imshow("【效果图】", g_dstImage); waitKey(0); return 0;
}
static void on_HoughLines(int, void*) {
Mat dstImage = g_dstImage.clone(); Mat midImage = g_midImage.clone(); vector<Vec4i> mylines; HoughLinesP(midImage, mylines, 1, CV_PI / 180, g_nthreshold + 1, 50, 10);
for (size_t i = 0; i < mylines.size(); i++) { Vec4i l = mylines[i]; line(dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(23, 180, 55), 1, CV_AA); }
imshow("【效果图】", dstImage); } 霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是很类似的,只是点对应的二维极径极角空间被三维的圆心点x, y还有半径r空间取代
void HoughCircles(InputArray _image, OutputArray _circles, int method, double dp, double min_dist, double param1, double param2, int minRadius, int maxRadius) 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的灰度单通道图像。 第二个参数,InputArray类型的circles,经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x, y, radius)表示。 第三个参数,int类型的method,即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为CV_HOUGH_GRADIENT,在此参数处填这个标识符即可。 第四个参数,double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。上述文字不好理解的话,来看例子吧。例如,如果dp=
1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。 第五个参数,double类型的minDist,为霍夫变换检测到的圆的圆心之间的最小距离,即让我们的算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大的话,某些圆就不能被检测出来了。 第六个参数,double类型的param1,有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。 第七个参数,double类型的param2,也有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。 第八个参数,int类型的minRadius,有默认值0,表示圆半径的最小值。 第九个参数,int类型的maxRadius,也有默认值0,表示圆半径的最大值。
过点(x1,y1)的所有圆可以表示为(a1(i),b1(i),r1(i)),过点(x2,y2)的所有圆可以表示为(a2(i),b2(i),r2(i)),过点(x3,y3)的所有圆可以表示为(a3(i),b3(i),r3(i)),如果这三个点在同一个圆上,那么存在一个值(a0,b0,r0),使得
a0 = a1(k)=a2(k)=a3(k) 且b0 = b1(k)=b2(k)=b3(k) 且r0 =
r1(k)=r2(k)=r3(k),即这三个点同时在圆(a0,b0,r0)上。 从下图可以形象的看出: 首先,分析过点(x1,y1)的所有圆(a1(i),b1(i),r1(i)),当确定r1(i)时
,(a1(i),b1(i))的轨迹是一个以(x1,y1,r1(i))为中心半径为r1(i)的圆。那么,所有圆(a1(i),b1(i),r1(i))的组成了一个以(x1,y1,0)为顶点,锥角为90度的圆锥面。 三个圆锥面的交点A 既是同时过这三个点的圆。 #include <opencv2/opencv.hpp> #include <opencv2/imgproc/imgproc.hpp> using namespace cv; int main() { Mat srcImage = imread("1.png"); Mat midImage, dstImage; imshow("【原始图】", srcImage); cvtColor(srcImage, midImage, CV_BGR2GRAY); GaussianBlur(midImage, midImage, Size(9, 9), 2, 2); vector<Vec3f> circles; HoughCircles(midImage, circles, CV_HOUGH_GRADIENT, 1.5, 10, 200, 100, 0, 0); for (size_t i = 0; i < circles.size(); i++) { Point center(cvRound(circles[i][0]), cvRound(circles[i][1])); int radius = cvRound(circles[i][2]); circle(srcImage, center, 3, Scalar(0, 255, 0), -1, 8, 0); circle(srcImage, center, radius, Scalar(155, 50, 255), 3, 8, 0); }
imshow("【效果图】", srcImage); waitKey(0); return 0; }
matlab I = imread('circuit.tif'); rotI = imrotate(I,33,'crop'); figure imshow(rotI, []) BW = edge(rotI,'canny'); [H,T,R] = hough(BW,'RhoResolution',0.5,'ThetaResolution',0.5); figure imshow(H,[],'XData',T,'YData',R,... 'InitialMagnification','fit'); xlabel('theta'), ylabel('rho'); axis on, axis normal, hold on; colormap(hot) P = houghpeaks(H,5,'threshold',ceil(0.3*max(H(:)))); x = T(P(:,2)); y = R(P(:,1)); plot(x,y,'s','color','white'); % Find lines and plot them lines = houghlines(BW,T,R,P,'FillGap',5,'MinLength',7); figure, imshow(rotI), hold on max_len = 0; for k = 1:length(lines) xy = [lines(k).point1; lines(k).point2]; plot(xy(:,1),xy(:,2),'LineWidth',2,'Color','green'); % Plot beginnings and ends of lines plot(xy(1,1),xy(1,2),'x','LineWidth',2,'Color','yellow'); plot(xy(2,1),xy(2,2),'x','LineWidth',2,'Color','red'); % Determine the endpoints of the longest line segment len = norm(lines(k).point1 - lines(k).point2); if ( len > max_len) max_len = len; xy_long = xy; end end % highlight the longest line segment plot(xy_long(:,1),xy_long(:,2),'LineWidth',2,'Color','blue'); 左边图是hough变换,右边是标记直线结果
轮廓 当看到轮廓的时候,发现没有办法具体到什么, 因为关系轮廓的东西似乎有很多,例如检测轮廓,提取轮廓,轮廓跟踪,轮廓面积,周长,标记,匹配,还有一系列的外接最小矩形,圆形,椭圆,图像矩,填充孔洞等,不得不说东西真的很好。 轮廓其实最容易和边缘检测联系到一起,有很多的相同,但是我理解的是边缘是检测,是预处理,而轮廓就可能是你要用的特征。 一、函数:一个是找,一个是画 void findContours//提取轮廓,用于提取图像的轮廓 ( InputOutputArray image,//输入图像,必须是8位单通道图像,并且应该转化成二值的 OutputArrayOfArrays contours,//检测到的轮廓,每个轮廓被表示成一个point向量 OutputArray hierarchy,//可选的输出向量,包含图像的拓扑信息。其中元素的个数和检测到的轮廓的数量相等 int mode,//说明需要的轮廓类型和希望的返回值方式 int method,//轮廓近似方法 Point offset = Point() ) 注意:mode轮廓检索模式
CV_RETR_EXTERNAL 只检测出最外轮廓即c0。图2中第一个轮廓指向最外的序列,除此之外没有别的连接。 CV_RETR_LIST 检测出所有的轮廓并将他们保存到表(list)中,图2中描绘了这个表,被找到的9条轮廓相互之间由h_prev和h_next连接。这里并没有表达出纵向的连接关系,没有使用v_prev和v_next. CV_RETR_COMP 检测出所有的轮廓并将他们组织成双层的结构,第一层是外部轮廓边界,第二层边界是孔的边界。从图2可以看到5个轮廓的边界,其中3个包含孔。最外层边界c0有两个孔,c0之间的所有孔相互间由h_prev和h_next指针连接。
CV_RETR_TREE
检测出所有轮廓并且重新建立网状的轮廓结构。图2中,根节点是最外层的边界c0,c0之下是孔h00,在同一层中与另一个孔h01相连接。同理,每个孔都有子节点(相对于c000和c010),这些子节点和父节点被垂直连接起来。这个步骤一直持续到图像最内层的轮廓,这些轮廓会成为树叶节点。 注意,method 也就是轮廓的近似方法 CV_CHAIN_CODE 用freeman链码输出轮廓,其他方法输出多边形(顶点的序列)。 CV_CHAIN_APPROX_NONE将链码编码中的所有点转换为点。 CV_CHAIN_APPROX_SIMPLE压缩水平,垂直或斜的部分,只保存最后一个点。 CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_QPPROX_TC89_KCOS使用Teh-Chin链逼近算法中的一个。 CV_LINK_RUNS与上述的算法完全不同,连接所有的水平层次的轮廓。 void drawContours//绘制轮廓,用于绘制找到的图像轮廓 ( InputOutputArray image,//要绘制轮廓的图像 InputArrayOfArrays contours,//所有输入的轮廓,每个轮廓被保存成一个point向量 int contourIdx,//指定要绘制轮廓的编号,如果是负数,则绘制所有的轮廓 const Scalar & color,//绘制轮廓所用的颜色 int thickness = 1, //绘制轮廓的线的粗细,如果是负数,则轮廓内部被填充 int lineType = 8, / 绘制轮廓的线的连通性 InputArray hierarchy = noArray(),//关于层级的可选参数,只有绘制部分轮廓时才会用到 int maxLevel = INT_MAX,//绘制轮廓的最高级别,这个参数只有hierarchy有效的时候才有效 //maxLevel=0,绘制与输入轮廓属于同一等级的所有轮廓即输入轮廓和与其相邻的轮廓 //maxLevel=1, 绘制与输入轮廓同一等级的所有轮廓与其子节点。 //maxLevel=2,绘制与输入轮廓同一等级的所有轮廓与其子节点以及子节点的子节点 Point offset = Point() ) 具体的用法#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace cv; using namespace std; #define WINDOW_NAME1 "【原始图窗口】" #define WINDOW_NAME2 "【轮廓图】" Mat g_srcImage; Mat g_grayImage; int g_nThresh = 80; int g_nThresh_max = 255; RNG g_rng(12345); Mat g_cannyMat_output; vector<vector<Point>> g_vContours; vector<Vec4i> g_vHierarchy; void on_ThreshChange(int, void*);
int main(int argc, char** argv) {
g_srcImage = imread("lena.jpg", 1); if (!g_srcImage.data) { printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n"); return false; } cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY); blur(g_grayImage, g_grayImage, Size(3, 3)); namedWindow(WINDOW_NAME1, WINDOW_AUTOSIZE); imshow(WINDOW_NAME1, g_srcImage); //创建滚动条并初始化 createTrackbar("canny阈值", WINDOW_NAME1, &g_nThresh, g_nThresh_max, on_ThreshChange); on_ThreshChange(0, 0); waitKey(0); return(0); }
void on_ThreshChange(int, void*) {
Canny(g_grayImage, g_cannyMat_output, g_nThresh, g_nThresh * 2, 3); imshow("1", g_cannyMat_output); // 寻找轮廓 findContours(g_cannyMat_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); // 绘出轮廓 Mat drawing = Mat::zeros(g_cannyMat_output.size(), CV_8UC3); for (int i = 0; i < g_vContours.size(); i++) { Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));//任意值 drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point()); }
// 显示效果图 imshow(WINDOW_NAME2, drawing); }
二、其他的函数 三、matlab regionprops统计被标记的区域的面积分布,显示区域总数。这个里面有多的不能再多的好用 函数regionprops语法规则为:STATS = regionprops(L,properties) 该函数用来测量标注矩阵L中每一个标注区域的一系列属性。 'Area' 图像各个区域中像素总个数 'BoundingBox' 包含相应区域的最小矩形 'Centroid' 每个区域的质心(重心) 'MajorAxisLength' 与区域具有相同标准二阶中心矩的椭圆的长轴长度(像素意义下) 'MinorAxisLength' 与区域具有相同标准二阶中心矩的椭圆的短轴长度(像素意义下) 'Eccentricity' 与区域具有相同标准二阶中心矩的椭圆的离心率(可作为特征) 'Orientation' 与区域具有相同标准二阶中心矩的椭圆的长轴与x轴的交角(度) 'Image' 与某区域具有相同大小的逻辑矩阵 'FilledImage' 与某区域具有相同大小的填充逻辑矩阵 'FilledArea' 填充区域图像中的on像素个数 'ConvexHull' 包含某区域的最小凸多边形 'ConvexImage' 画出上述区域最小凸多边形 'ConvexArea' 填充区域凸多边形图像中的on像素个数 'EulerNumber' 几何拓扑中的一个拓扑不变量——欧拉数 'Extrema' 八方向区域极值点 'EquivDiameter' 与区域具有相同面积的圆的直径 'Solidity' 同时在区域和其最小凸多边形中的像素比例 'Extent' 同时在区域和其最小边界矩形中的像素比例 'PixelIdxList' 存储区域像素的索引下标 'PixelList' 存储上述索引对应的像素坐标
im=imread('d:\lena.jpg'); %读取原图 figure,imshow(im,[]);title('Raw'); %显示原图 im=im2bw(im); %转二值图像 figure,imshow(im,[]),title('BW'); %显示二值图像 im2=imfill(im,'holes'); %填充 im3=bwperim(im2); %轮廓提取 figure,imshow(im2,[]); title('') %显示 figure,imshow(im3,[]);
漫水填充,种子填充,区域生长、孔洞填充
可以说从这篇文章开始,就结束了图像识别的入门基础,来到了第二阶段的学习。在平时处理二值图像的时候,除了要进行形态学的一些操作,还有有上一节讲到的轮廓连通区域的面积周长标记等,还有一个最常见的就是孔洞的填充,opencv这里成为漫水填充,其实也可以叫种子填充,或者区域生长,基本的原理是一样的,但是应用的时候需要注意一下,种子填充用递归的办法,回溯算法,漫水填充使用堆栈,提高效率,同时还提供了一种方式是扫描行。经常用来填充孔洞,现在来具体看看。 漫水填充:也就是用一定颜色填充联通区域,通过设置可连通像素的上下限以及连通方式来达到不同的填充效果;漫水填充经常被用来标记或分离图像的一部分以便对其进行进一步处理或分析,也可以用来从输入图像获取掩码区域,掩码会加速处理过程,或只处理掩码指定的像素点,操作的结果总是某个连续的区域。简单的说,就是选中点seedPoint,然后选取出它周围和它色彩差异不大的点,并将它们的值改为newVal。如果被选取的点,遇到mask掩码,则放弃对该方向的 种子填充算法,种子填充算法是从多边形区域内部的一点开始,由此出发找到区域内的所有像素。种子填充算法采用的边界定义是区域边界上所有像素具有某个特定的颜色值,区域内部所有像素均不取这一特定颜色,而边界外的像素则可具有与边界相同的颜色值。 具体算法步骤: 标记种子(x,y)的像素点 ; 检测该点的颜色,若他与边界色和填充色均不同,就用填充色填 充该点,否则不填充 ; 检测相邻位置,继续 2。这个过程延续到已经检测区域边界范围内的所有像素为止。 当然在搜索的时候有两种检测相邻像素:四向连通和八向连通。四向连通即从区域上一点出发,通过四个方向上、下、左、右来检索。而八向连通加上了左上、左下、右上、右下四个方向。这种算法的有点是算法简单,易于实现,也可以填充带有内孔的平面区域。但是此算法需要更大的存储空间以实现栈结构,同一个像素多次入栈和出栈,效率低,运算量大。
扫描线种子填充算法:该算法属于种子填充算法,它是以扫描线上的区段为单位操作。所谓区段,就是一条扫描线上相连着的若干内部象素的集合。扫描线种子填充算法思想:首先填充当前扫描线上的位于给定区域的一区段,然后确定于这一区段相邻的上下两条线上位于该区域内是否存在需要填充的新区段,如果存在,则依次把他们保存起来,反复这个过程,直到所保存的各区段都填充完毕。 FloodFill函数 int floodFill(InputOutputArray image, InputOutputArray mask, Point seedPoint, Scalar newVal, Rect * rect = 0, Scalar loDiff = Scalar(), Scalar upDiff = Scalar(), int flags = 4); InputOutputArray:输入和输出图像。 mask : 输入的掩码图像。 seedPoint: 算法开始处理的开始位置。 newVal: 图像中所有被算法选中的点,都用这个数值来填充。 rect : 最小包围矩阵。 loDiff: 最大的低亮度之间的差异。 upDiff: 最大的高亮度之间的差异。 flag: 选择算法连接方式。 根据上面的函数先看一个基础的应用#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using namespace std;
Mat g_srcImage, g_dstImage, g_grayImage, g_maskImage; int g_nFillMode = 1; int g_nLowDifference = 20, g_nUpDifference = 20; int g_nConnectivity = 4; int g_bIsColor = true; bool g_bUseMask = false; int g_nNewMaskVal = 255; static void onMouse(int event, int x, int y, int, void*) {
if (event != EVENT_LBUTTONDOWN) return; Point seed = Point(x, y); int LowDifference = g_nFillMode == 0 ? 0 : g_nLowDifference; int UpDifference = g_nFillMode == 0 ? 0 : g_nUpDifference; int flags = g_nConnectivity + (g_nNewMaskVal << 8) + (g_nFillMode == 1 ? FLOODFILL_FIXED_RANGE : 0); int b = (unsigned)theRNG() & 255; int g = (unsigned)theRNG() & 255; int r = (unsigned)theRNG() & 255; Rect ccomp; Scalar newVal = g_bIsColor ? Scalar(b, g, r) : Scalar(r * 0.299 + g * 0.587 + b * 0.114); Mat dst = g_bIsColor ? g_dstImage : g_grayImage;//目标图的赋值 int area; if (g_bUseMask) { threshold(g_maskImage, g_maskImage, 1, 128, THRESH_BINARY); area = floodFill(dst, g_maskImage, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference), Scalar(UpDifference, UpDifference, UpDifference), flags); imshow("mask", g_maskImage); } else { area = floodFill(dst, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference), Scalar(UpDifference, UpDifference, UpDifference), flags); }
imshow("效果图", dst); cout << area << " 个像素被重绘\n"; }
int main(int argc, char** argv) {
g_srcImage = imread("lena.jpg", 1);
if (!g_srcImage.data) { printf("读取图片image0错误~! \n"); return false; }
g_srcImage.copyTo(g_dstImage); cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY); g_maskImage.create(g_srcImage.rows + 2, g_srcImage.cols + 2, CV_8UC1); namedWindow("效果图", WINDOW_AUTOSIZE); createTrackbar("负差最大值", "效果图", &g_nLowDifference, 255, 0); createTrackbar("正差最大值", "效果图", &g_nUpDifference, 255, 0); setMouseCallback("效果图", onMouse, 0); while (1) { //先显示效果图 imshow("效果图", g_bIsColor ? g_dstImage : g_grayImage);
//获取键盘按键 int c = waitKey(0); //判断ESC是否按下,若按下便退出 if ((c & 255) == 27) { cout << "程序退出...........\n"; break; }
//根据按键的不同,进行各种操作 switch ((char)c) { //如果键盘“1”被按下,效果图在在灰度图,彩色图之间互换 case '1': if (g_bIsColor)//若原来为彩色,转为灰度图,并且将掩膜mask所有元素设置为0 { cout << "键盘“1”被按下,切换彩色/灰度模式,当前操作为将【彩色模式】切换为【灰度模式】\n"; cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY); g_maskImage = Scalar::all(0); //将mask所有元素设置为0 g_bIsColor = false; //将标识符置为false,表示当前图像不为彩色,而是灰度 } else//若原来为灰度图,便将原来的彩图image0再次拷贝给image,并且将掩膜mask所有元素设置为0 { cout << "键盘“1”被按下,切换彩色/灰度模式,当前操作为将【彩色模式】切换为【灰度模式】\n"; g_srcImage.copyTo(g_dstImage); g_maskImage = Scalar::all(0); g_bIsColor = true;//将标识符置为true,表示当前图像模式为彩色 } break; //如果键盘按键“2”被按下,显示/隐藏掩膜窗口 case '2': if (g_bUseMask) { destroyWindow("mask"); g_bUseMask = false; } else { namedWindow("mask", 0); g_maskImage = Scalar::all(0); imshow("mask", g_maskImage); g_bUseMask = true; } break; //如果键盘按键“3”被按下,恢复原始图像 case '3': cout << "按键“3”被按下,恢复原始图像\n"; g_srcImage.copyTo(g_dstImage); cvtColor(g_dstImage, g_grayImage, COLOR_BGR2GRAY); g_maskImage = Scalar::all(0); break; //如果键盘按键“4”被按下,使用空范围的漫水填充 case '4': cout << "按键“4”被按下,使用空范围的漫水填充\n"; g_nFillMode = 0; break; //如果键盘按键“5”被按下,使用渐变、固定范围的漫水填充 case '5': cout << "按键“5”被按下,使用渐变、固定范围的漫水填充\n"; g_nFillMode = 1; break; //如果键盘按键“6”被按下,使用渐变、浮动范围的漫水填充 case '6': cout << "按键“6”被按下,使用渐变、浮动范围的漫水填充\n"; g_nFillMode = 2; break; //如果键盘按键“7”被按下,操作标志符的低八位使用4位的连接模式 case '7': cout << "按键“7”被按下,操作标志符的低八位使用4位的连接模式\n"; g_nConnectivity = 4; break; //如果键盘按键“8”被按下,操作标志符的低八位使用8位的连接模式 case '8': cout << "按键“8”被按下,操作标志符的低八位使用8位的连接模式\n"; g_nConnectivity = 8; break; } }
return 0; }
再来看看漫水填充在填充孔洞上的应用 #include<opencv2\core\core.hpp> #include<opencv2\highgui\highgui.hpp> #include<opencv2\imgproc\imgproc.hpp> using namespace std; using namespace cv; void chao_fillHole(const cv::Mat srcimage, cv::Mat& dstimage) { Size m_Size = srcimage.size(); Mat temimage = Mat::zeros(m_Size.height + 2, m_Size.width + 2, srcimage.type()); srcimage.copyTo(temimage(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1))); floodFill(temimage, Point(0, 0), Scalar(255)); Mat cutImg; temimage(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1)).copyTo(cutImg); dstimage = srcimage | (~cutImg); } int main() { Mat src = imread("111.png"); Mat dst; chao_fillHole(src, dst); imshow("tianchong", dst); waitKey(0); return 0;
}
matlab I=imread('tire.tif');figure,imshow(I)BW=imfill(I);figure,imshow(BW)
区域分裂与合并 区域分割一般认为有漫水填充,区域分裂与合并,分水岭,这篇是中间的区域分裂和合并。 区域分裂合并算法的基本思想是先确定一个分裂合并的准则,即区域特征一致性的测度,当图像中某个区域的特征不一致时就将该区域分裂成4 个相等的子区域,当相邻的子区域满足一致性特征时则将它们合成一个大区域,直至所有区域不再满足分裂合并的条件为止. 当分裂到不能再分的情况时,分裂结束,然后它将查找相邻区域有没有相似的特征,如果有就将相似区域进行合并,最后达到分割的作用。 在一定程度上区域生长和区域分裂合并算法有异曲同工之妙,互相促进相辅相成的,区域分裂到极致就是分割成单一像素点,然后按照一定的测量准则进行合并,在一定程度上可以认为是单一像素点的区域生长方法。 区域生长比区域分裂合并的方法节省了分裂的过程,而区域分裂合并的方法可以在较大的一个相似区域基础上再进行相似合并,而区域生长只能从单一像素点出发进行生长(合并)。 反复进行拆分和聚合以满足限制条件的算法。 令R表示整幅图像区域并选择一个谓词P。对R进行分割的一种方法是反复将分割得到的结果图像再次分为四个区域,直到对任何区域Ri,有P(Ri)=TRUE。这里是从整幅图像开始。如果P(R)=FALSE,就将图像分割为4个区域。对任何区域如果P的值是FALSE.就将这4个区域的每个区域再次分别分为4个区域,如此不断继续下去。这种特殊的分割技术用所谓的四叉树形式表示最为方便(就是说,每个非叶子节点正好有4个子树),这正如图10.42中说明的树那样。注意,树的根对应于整幅图像,每个节点对应于划分的子部分。此时,只有R4进行了进一步的再细分。
如果只使用拆分,最后的分区可能会包含具有相同性质的相邻区域。这种缺陷可以通过进行拆分的同时也允许进行区域聚合来得到矫正。就是说,只有在P(Rj∪Rk)=TRUE时,两个相邻的区域Rj和Rk才能聚合。 前面的讨论可以总结为如下过程。在反复操作的每一步,我们需要做: l.对于任何区域Ri,如果P(Ri)=FALSE,就将每个区域都拆分为4个相连的象限区域。 2.将P(Rj∪Rk)=TRUE的任意两个相邻区域Rj和Rk进行聚合。 3.当再无法进行聚合或拆分时操作停止。 可以对前面讲述的基本思想进行几种变化。例如,一种可能的变化是开始时将图像拆分为一组图象块。然后对每个块进一步进行上述拆分,但聚合操作开始时受只能将4个块并为一组的限制。这4个块是四叉树表示法中节点的后代且都满足谓词P。当不能再进行此类聚合时,这个过程终止于满足步骤2的最后的区域聚合。在这种情况下,聚合的区域可能会大小不同。这种方法的主要优点是对于拆分和聚合都使用同样的四叉树,直到聚合的最后一步。 例10.17 拆分和聚合 图10.43(a)显示了一幅简单的图像。如果在区域Ri内至少有80%的像素具有zj-mi≤2σi的性质,就定义P(Ri)=TRUE,这里zj是Ri内第j个像素的灰度级,mi是区域Ri的灰度级均值,σi是区域Ri内的灰度级的标准差。如果在此条件下,P(Ri)=TRUE,则设置Ri内的所有像素的值等于mi。拆分和聚合使用前速算法的要点完成。将这种技术应用于图10.43(a)所得结果示于图10.43(b)。请注意,图像分割效果相当好。示于图10.43(c)中的图像是通过对图10.43(a)进行门限处理得到的,门限值选在直方图中两个主要的尖峰之间的中点。经过门限处理,图像中生成的阴影(和叶子的茎)被错误地消除了。 如前面的例子中所使用的属性那样,我们试图使用基于区域中像素的均值和标准差的某些特性对区域的纹理进行量化(见11.3.3节中关于纹理的讨论)。纹理分割的概念是以在谓词P(Ri)中使用有关纹理的量度为基础的。就是说,通过指定基于纹理内容的谓词,我们可以使用本节中讨论的任何方法进行纹理分割。 1. 把一幅图像分成4份,计算每一份图像的最大灰度值与最小灰度值的差, 如果差在误差范围值外,则该份图像继续分裂。 2. 对于那些不需要分裂的那些份图像可以对其进行阈值切割了,例如某一块图像的最大灰度大于某个值,则该块图像变成255,否则变为0。 // 代码 // 区域分裂合并的图像分割 // nOffSetLne是行偏移量 // 由于分裂的层数太多了, 使用递归将使内存空间堆栈溢出 // 解决方法是使用一个堆栈对要分裂的块入栈 // 使用堆栈的方法类似在"区域生长"的实现方法 #include <stack> struct SplitStruct { unsigned int nWidth; // 这一块图像的宽度 unsigned int nHeigh; // 这一块图像的高度 unsigned int nOffSetWidth; // 相对源图像数据的偏移宽度 unsigned int nOffSetHeigh; // 相对源图像数据的偏移高度 }; void AreaSplitCombineEx(BYTE* image0, // 源图像数据 unsigned int nAllWidth, // 源图像的宽度 unsigned int nAllHeigh, // 源图像的高度 unsigned int w, // 这一块图像的宽度 unsigned int h, // 这一块图像的高度 unsigned int nOffSetWidth, // 相对源图像数据的偏移宽度 unsigned int nOffSetHeigh) // 相对源图像数据的偏移高度 { std::stack<SplitStruct> nMyStack; SplitStruct splitStruct, splitStructTemp; splitStruct.nWidth = w; splitStruct.nHeigh = h; splitStruct.nOffSetWidth = nOffSetWidth; splitStruct.nOffSetHeigh = nOffSetHeigh; nMyStack.push(splitStruct); int i, j; int nValueS[2][2]; // 用于存储块图像的属性值(该属性值= 该块图像的所有像素灰度值之和除以该块图像所有像素点的数量) int nAV; int nWidthTemp[3], nHeightTemp[3], nTemp; int nWidth, nHeigh; int n, m, l; double dOver; while (!nMyStack.empty()) { splitStruct = nMyStack.top(); nMyStack.pop(); n = (splitStruct.nOffSetHeigh * nAllWidth + splitStruct.nOffSetWidth); // 该块图像的左上角 // 1. 把图像分成2 * 2 块, nWidthTemp[0] = 0; nWidthTemp[2] = (splitStruct.nWidth + 1) / 2; nWidthTemp[1] = splitStruct.nWidth - nWidthTemp[2]; nHeightTemp[0] = 0; nHeightTemp[2] = (splitStruct.nHeigh + 1) / 2; nHeightTemp[1] = splitStruct.nHeigh - nHeightTemp[2]; // 计算每一块图像的属性值 int nValue; int nValueTemp; nAV = 0; for (i = 1; i < 3; ++i) { for (j = 1; j < 3; ++j) { nValue = 0; m = (n + nAllWidth * nHeightTemp[i - 1] + nWidthTemp[j - 1]); for (nHeigh = 0; nHeigh < nHeightTemp[i]; ++nHeigh) { for (nWidth = 0; nWidth < nWidthTemp[j]; ++nWidth) { l = (m + nAllWidth * nHeigh + nWidth) * 4; nValueTemp = (0.299 * image0[l] + 0.587 * image0[l + 1] + 0.114 * image0[l + 2]); // 灰度值之和 nValue += nValueTemp; } } if (nHeightTemp[i] * nWidthTemp[j] == 0) { continue; } if (nHeightTemp[i] * nWidthTemp[j] == 1) { l = m * 4; if ((0.299 * image0[l] + 0.587 * image0[l + 1] + 0.114 * image0[l + 2]) < 125) // 这个值可以动态设定 { image0[l] = image0[l + 1] = image0[l + 2] = 0; image0[l + 3] = 255; } else { image0[l] = image0[l + 1] = image0[l + 2] = 255; image0[l + 3] = 255; } continue; } // 各块图像的灰度平均值(每一块图像的属性值) nValueS[i - 1][j - 1] = nValue / (nHeightTemp[i] * nWidthTemp[j]); // 2. 对每一块进行判断是否继续分裂(注意分裂的原则) // 我这里的分裂原则是: 图像的属性值在属性值平均值的误差范围之内就不分裂 if (nValueS[i - 1][j - 1] < 220) // 灰度平均值少于200 需要继续分裂 // 这里就是分裂准则了 { splitStructTemp.nWidth = nWidthTemp[j]; splitStructTemp.nHeigh = nHeightTemp[i]; splitStructTemp.nOffSetWidth = splitStruct.nOffSetWidth + nWidthTemp[j - 1]; splitStructTemp.nOffSetHeigh = splitStruct.nOffSetHeigh + nHeightTemp[i - 1]; nMyStack.push(splitStructTemp); } else // 合并(直接填充该块图像为黑色) { // 3. 如果不需要分裂, 则进行合并 for (nHeigh = 0; nHeigh < nHeightTemp[i]; ++nHeigh) { for (nWidth = 0; nWidth < nWidthTemp[j]; ++nWidth) { l = (m + nAllWidth * nHeigh + nWidth) * 4; image0[l] = image0[l + 1] = image0[l + 2] = 255; image0[l + 3] = 255; } } } } } } return; }
该代码的效果也不是太好,主要是分裂准则不好确 区域分裂合并中 最初使用每块图像区域中极大与极小灰度值之差是否在允许的偏差范围来作为均匀性测试准则。 后来均匀性测试准则又被不断的发展。目前,统计检验,如均方误差最小, F检测等都是最常用的均匀性测试准侧方法 看均方误差最小的情况 其中C是区域R中N个点的平均值。 相对于区域生长而言,区域分割于合并技术不再依赖于种子点的选择与生长顺序。但选用合适的均匀性测试准则P对于提高图像分割质量十分重要,当均匀性测试准则P选择不当时,很容易会引起“方块效应” 参考连接;http://blog.csdn.net/bagboy_taobao_com/article/details/5666109MATLAB matlab中给出了qtdecomp().qtsetblk(),下面看看效果 I = imread('liftingbody.png'); S = qtdecomp(I,.27);%以阈值ceil(0.27*255)=69对图像I进行四叉分解 blocks = repmat(uint8(0),size(S));%得到一个和I同尺寸的黑色背景blocks for dim = [512 256 128 64 32 16 8 4 2 1]; %分块全是2的整数次幂,注① numblocks = length(find(S==dim)); %有numblocks个尺寸为dim的分块,注③ if (numblocks > 0) values = repmat(uint8(1),[dim dim numblocks]);%产生一个dim x dim x numblocks的三维1值矩阵(或说 % numblocks个尺寸为dim x dim的1值block) values(2:dim,2:dim,:) = 0; blocks = qtsetblk(blocks,S,dim,values);%blocks保存了所有块被替换后的结果。注④ end end blocks(end,1:end) = 1; blocks(1:end,end) = 1; imshow(I), figure, imshow(blocks,[])
分水岭 分水岭是区域分割三个方法的最后一个,对于前景背景的分割有不错的效果。
分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸入水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝,即形成分水岭。 分水岭算法一般和区域生长法或聚类分析法相结合。 分水岭算法一般用于分割感兴趣的图像区域,应用如细胞边界的分割,分割出相片中的头像等等 分水岭算法主要用于图像分段,通常是把一副彩色图像灰度化,然后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线。 opencv中的算法是先把输入图像转化成梯度图(标量) 如果把梯度图看成是一个地形的话,就会发现,梯度高的地方就成了山脉,梯度低的地方就是山谷 我们经过标记为不同的区域后,就从各个标记的地方注水进去,注入的水越来越多的时候,就会出现把流过低些的山脉,从而流到别的山谷中,那么他们就连一了一片区域。 区域分割的要求是把不同的标记分割成不同的地方。所以如果一直注水,可能就会覆盖别的区域了。这时算法就采取某种方法,修大坝使标记的不同区域不会因为注水而相连 他们会互不相干的扩张领地,直到把整个领地都扩张完为止。 再看看下图,是一个图像的地形拓扑
对灰度图的地形学解释,我们我们考虑三类点: 1. 局部最小值点,该点对应一个盆地的最低点,当我们在盆地里滴一滴水的时候,由于重力作用,水最终会汇聚到该点。注意:可能存在一个最小值面,该平面内的都是最小值点。 2. 盆地的其它位置点,该位置滴的水滴会汇聚到局部最小点。 3. 盆地的边缘点,是该盆地和其它盆地交接点,在该点滴一滴水,会等概率的流向任何一个盆地。 函数声明:CV_EXPORTS_W void watershed(InputArray image, InputOutputArray markers); InputArray image 要分割的原始图片 InputOutputArray markers 标记数组,非零的32位有符号的int型数组, 用于标记出要分割的关键点,进而区域生长,扩展出感兴趣的区域。
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using namespace std;
#define WINDOW_NAME1 "【程序窗口1】" //为窗口标题定义的宏 #define WINDOW_NAME2 "【分水岭算法效果图】" //为窗口标题定义的宏
Mat g_maskImage, g_srcImage; Point prevPt(-1, -1);
static void ShowHelpText(); static void on_Mouse(int event, int x, int y, int flags, void*);
int main(int argc, char** argv) {
//【1】载入原图并显示,初始化掩膜和灰度图 g_srcImage = imread("lena.jpg", 1); imshow(WINDOW_NAME1, g_srcImage); Mat srcImage, grayImage; g_srcImage.copyTo(srcImage); cvtColor(g_srcImage, g_maskImage, COLOR_BGR2GRAY); cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR); g_maskImage = Scalar::all(0);
//【2】设置鼠标回调函数 setMouseCallback(WINDOW_NAME1, on_Mouse, 0);
//【3】轮询按键,进行处理 while (1) { //获取键值 int c = waitKey(0);
//若按键键值为ESC时,退出 if ((char)c == 27) break;
//按键键值为2时,恢复源图 if ((char)c == '2') { g_maskImage = Scalar::all(0); srcImage.copyTo(g_srcImage); imshow("image", g_srcImage); }
//若检测到按键值为1或者空格,则进行处理 if ((char)c == '1' || (char)c == ' ') { //定义一些参数 int i, j, compCount = 0; vector<vector<Point> > contours; vector<Vec4i> hierarchy;
//寻找轮廓 findContours(g_maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
//轮廓为空时的处理 if (contours.empty()) continue;
//拷贝掩膜 Mat maskImage(g_maskImage.size(), CV_32S); maskImage = Scalar::all(0);
//循环绘制出轮廓 for (int index = 0; index >= 0; index = hierarchy[index][0], compCount++) drawContours(maskImage, contours, index, Scalar::all(compCount + 1), -1, 8, hierarchy, INT_MAX);
//compCount为零时的处理 if (compCount == 0) continue;
//生成随机颜色 vector<Vec3b> colorTab; for (i = 0; i < compCount; i++) { int b = theRNG().uniform(0, 255); int g = theRNG().uniform(0, 255); int r = theRNG().uniform(0, 255);
colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r)); }
//计算处理时间并输出到窗口中 double dTime = (double)getTickCount(); watershed(srcImage, maskImage); dTime = (double)getTickCount() - dTime; printf("\t处理时间 = %gms\n", dTime * 1000. / getTickFrequency());
//双层循环,将分水岭图像遍历存入watershedImage中 Mat watershedImage(maskImage.size(), CV_8UC3); for (i = 0; i < maskImage.rows; i++) for (j = 0; j < maskImage.cols; j++) { int index = maskImage.at<int>(i, j); if (index == -1) watershedImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255); else if (index <= 0 || index > compCount) watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0); else watershedImage.at<Vec3b>(i, j) = colorTab[index - 1]; }
//混合灰度图和分水岭效果图并显示最终的窗口 watershedImage = watershedImage * 0.5 + grayImage * 0.5; imshow(WINDOW_NAME2, watershedImage); } }
return 0; }
static void on_Mouse(int event, int x, int y, int flags, void*) { //处理鼠标不在窗口中的情况 if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows) return;
//处理鼠标左键相关消息 if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) prevPt = Point(-1, -1); else if (event == EVENT_LBUTTONDOWN) prevPt = Point(x, y);
//鼠标左键按下并移动,绘制出白色线条 else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) { Point pt(x, y); if (prevPt.x < 0) prevPt = pt; line(g_maskImage, prevPt, pt, Scalar::all(255), 5, 8, 0); line(g_srcImage, prevPt, pt, Scalar::all(255), 5, 8, 0); prevPt = pt; imshow(WINDOW_NAME1, g_srcImage); } }
matlab这里给出一个最简单,不过有过度切割的现象,还有很多的好的标记分割方法,想学习的可以再深入,这里给出的是入门,效果不是太好 clear,clc%三种方法进行分水岭分割 %读入图像 filename='pears.png'; f=imread(filename); Info=imfinfo(filename); if Info.BitDepth>8 f=rgb2gray(f); end b=im2bw(f,graythresh(f));%二值化,注意应保证集水盆地的值较低(为0),否则就要对b取反 d=bwdist(b); %求零值到最近非零值的距离,即集水盆地到分水岭的距离 l=watershed(-d); %matlab自带分水岭算法,l中的零值即为风水岭 w=l==0; %取出边缘 g=b&~w; %用w作为mask从二值图像中取值 figure subplot(2,3,1), imshow(f); subplot(2,3,2), imshow(b); subplot(2,3,3), imshow(d); subplot(2,3,4), imshow(l); subplot(2,3,5), imshow(w); subplot(2,3,6), imshow(g);
grabcut 这是基于图论的分割方法,所以开始就先介绍了 Graph cuts,然后再到Grab cut 一、 Graph cuts Graph cuts是一种十分有用和流行的能量优化算法,在计算机视觉领域普遍应用于前背景分割(Image segmentation)、立体视觉(stereo
vision)、抠图(Image matting)等。 此类方法把图像分割问题与图的最小割(min cut)问题相关联。首先用一个无向图G=<V,E>表示要分割的图像,V和E分别是顶点(vertex)和边(edge)的集合。此处的Graph和普通的Graph稍有不同。普通的图由顶点和边构成,如果边的有方向的,这样的图被则称为有向图,否则为无向图,且边是有权值的,不同的边可以有不同的权值,分别代表不同的物理意义。而Graph
Cuts图是在普通图的基础上多了2个顶点,这2个顶点分别用符号”S”和”T”表示,统称为终端顶点。其它所有的顶点都必须和这2个顶点相连形成边集合中的一部分。所以Graph
Cuts中有两种顶点,也有两种边。 第一种顶点和边是:第一种普通顶点对应于图像中的每个像素。每两个邻域顶点(对应于图像中每两个邻域像素)的连接就是一条边。这种边也叫n-links。 第二种顶点和边是:除图像像素外,还有另外两个终端顶点,叫S(source:源点,取源头之意)和T(sink:汇点,取汇聚之意)。每个普通顶点和这2个终端顶点之间都有连接,组成第二种边。这种边也叫t-links。 上图就是一个图像对应的s-t图,每个像素对应图中的一个相应顶点,另外还有s和t两个顶点。上图有两种边,实线的边表示每两个邻域普通顶点连接的边n-links,虚线的边表示每个普通顶点与s和t连接的边t-links。在前后景分割中,s一般表示前景目标,t一般表示背景。 图中每条边都有一个非负的权值we,也可以理解为cost(代价或者费用)。一个cut(割)就是图中边集合E的一个子集C,那这个割的cost(表示为|C|)就是边子集C的所有边的权值的总和。 Graph Cuts中的Cuts是指这样一个边的集合,很显然这些边集合包括了上面2种边,该集合中所有边的断开会导致残留”S”和”T”图的分开,所以就称为“割”。如果一个割,它的边的所有权值之和最小,那么这个就称为最小割,也就是图割的结果。而福特-富克森定理表明,网路的最大流max
flow与最小割min cut相等。所以由Boykov和Kolmogorov发明的max-flow/min-cut算法就可以用来获得s-t图的最小割。这个最小割把图的顶点划分为两个不相交的子集S和T,其中s ∈S,t∈ T和S∪T=V 。这两个子集就对应于图像的前景像素集和背景像素集,那就相当于完成了图像分割。 二、grabcut OpenCV中的GrabCut算法是依据《"GrabCut"
- Interactive Foreground Extraction using Iterated Graph Cuts》这篇文章来实现的。该算法利用了图像中的纹理(颜色)信息和边界(反差)信息,只要少量的用户交互操作即可得到比较好的分割结果
和Graph Cut有何不同? (1)Graph Cut的目标和背景的模型是灰度直方图,Grab Cut取代为RGB三通道的混合高斯模型GMM; (2)Graph Cut的能量最小化(分割)是一次达到的,而Grab Cut取代为一个不断进行分割估计和模型参数学习的交互迭代过程; (3)Graph Cut需要用户指定目标和背景的一些种子点,但是Grab Cut只需要提供背景区域的像素集就可以了。也就是说你只需要框选目标,那么在方框外的像素全部当成背景,这时候就可以对GMM进行建模和完成良好的分割了。即Grab
Cut允许不完全的标注(incomplete labelling)。
void cv::grabCut(InputArray _img, InputOutputArray _mask, Rect rect, InputOutputArray _bgdModel, InputOutputArray _fgdModel, int iterCount, int mode) img——待分割的源图像,必须是8位3通道(CV_8UC3)图像,在处理的过程中不会被修改; mask——掩码图像,如果使用掩码进行初始化,那么mask保存初始化掩码信息;在执行分割的时候,也可以将用户交互所设定的前景与背景保存到mask中,然后再传入grabCut函数;在处理结束之后,mask中会保存结果。mask只能取以下四种值: GCD_BGD( = 0),背景; GCD_FGD( = 1),前景; GCD_PR_BGD( = 2),可能的背景; GCD_PR_FGD( = 3),可能的前景。 如果没有手工标记GCD_BGD或者GCD_FGD,那么结果只会有GCD_PR_BGD或GCD_PR_FGD; rect——用于限定需要进行分割的图像范围,只有该矩形窗口内的图像部分才被处理; bgdModel——背景模型,如果为null,函数内部会自动创建一个bgdModel;bgdModel必须是单通道浮点型(CV_32FC1)图像,且行数只能为1,列数只能为13x5; fgdModel——前景模型,如果为null,函数内部会自动创建一个fgdModel;fgdModel必须是单通道浮点型(CV_32FC1)图像,且行数只能为1,列数只能为13x5; iterCount——迭代次数,必须大于0; mode——用于指示grabCut函数进行什么操作,可选的值有: GC_INIT_WITH_RECT( = 0),用矩形窗初始化GrabCut; GC_INIT_WITH_MASK( = 1),用掩码图像初始化GrabCut; GC_EVAL( = 2),执行分割。
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream>
using namespace std; using namespace cv;
static void help() { cout << "\nThis program demonstrates GrabCut segmentation -- select an object in a region\n" "and then grabcut will attempt to segment it out.\n" "Call:\n" "./grabcut <image_name>\n" "\nSelect a rectangular area around the object you want to segment\n" << "\nHot keys: \n" "\tESC - quit the program\n" "\tr - restore the original image\n" "\tn - next iteration\n" "\n" "\tleft mouse button - set rectangle\n" "\n" "\tCTRL+left mouse button - set GC_BGD pixels\n" "\tSHIFT+left mouse button - set CG_FGD pixels\n" "\n" "\tCTRL+right mouse button - set GC_PR_BGD pixels\n" "\tSHIFT+right mouse button - set CG_PR_FGD pixels\n" << endl; }
const Scalar RED = Scalar(0, 0, 255); const Scalar PINK = Scalar(230, 130, 255); const Scalar BLUE = Scalar(255, 0, 0); const Scalar LIGHTBLUE = Scalar(255, 255, 160); const Scalar GREEN = Scalar(0, 255, 0);
const int BGD_KEY = CV_EVENT_FLAG_CTRLKEY; //Ctrl键 const int FGD_KEY = CV_EVENT_FLAG_SHIFTKEY; //Shift键
static void getBinMask(const Mat& comMask, Mat& binMask) { if (comMask.empty() || comMask.type() != CV_8UC1) CV_Error(CV_StsBadArg, "comMask is empty or has incorrect type (not CV_8UC1)"); if (binMask.empty() || binMask.rows != comMask.rows || binMask.cols != comMask.cols) binMask.create(comMask.size(), CV_8UC1); binMask = comMask & 1; //得到mask的最低位,实际上是只保留确定的或者有可能的前景点当做mask }
class GCApplication { public: enum { NOT_SET = 0, IN_PROCESS = 1, SET = 2 }; static const int radius = 2; static const int thickness = -1;
void reset(); void setImageAndWinName(const Mat& _image, const string& _winName); void showImage() const; void mouseClick(int event, int x, int y, int flags, void* param); int nextIter(); int getIterCount() const { return iterCount; } private: void setRectInMask(); void setLblsInMask(int flags, Point p, bool isPr);
const string* winName; const Mat* image; Mat mask; Mat bgdModel, fgdModel;
uchar rectState, lblsState, prLblsState; bool isInitialized;
Rect rect; vector<Point> fgdPxls, bgdPxls, prFgdPxls, prBgdPxls; int iterCount; };
/*给类的变量赋值*/ void GCApplication::reset() { if (!mask.empty()) mask.setTo(Scalar::all(GC_BGD)); bgdPxls.clear(); fgdPxls.clear(); prBgdPxls.clear(); prFgdPxls.clear();
isInitialized = false; rectState = NOT_SET; //NOT_SET == 0 lblsState = NOT_SET; prLblsState = NOT_SET; iterCount = 0; }
/*给类的成员变量赋值而已*/ void GCApplication::setImageAndWinName(const Mat& _image, const string& _winName) { if (_image.empty() || _winName.empty()) return; image = &_image; winName = &_winName; mask.create(image->size(), CV_8UC1); reset(); }
/*显示4个点,一个矩形和图像内容,因为后面的步骤很多地方都要用到这个函数,所以单独拿出来*/ void GCApplication::showImage() const { if (image->empty() || winName->empty()) return;
Mat res; Mat binMask; if (!isInitialized) image->copyTo(res); else { getBinMask(mask, binMask); image->copyTo(res, binMask); //按照最低位是0还是1来复制,只保留跟前景有关的图像,比如说可能的前景,可能的背景 }
vector<Point>::const_iterator it; /*下面4句代码是将选中的4个点用不同的颜色显示出来*/ for (it = bgdPxls.begin(); it != bgdPxls.end(); ++it) //迭代器可以看成是一个指针 circle(res, *it, radius, BLUE, thickness); for (it = fgdPxls.begin(); it != fgdPxls.end(); ++it) //确定的前景用红色表示 circle(res, *it, radius, RED, thickness); for (it = prBgdPxls.begin(); it != prBgdPxls.end(); ++it) circle(res, *it, radius, LIGHTBLUE, thickness); for (it = prFgdPxls.begin(); it != prFgdPxls.end(); ++it) circle(res, *it, radius, PINK, thickness);
/*画矩形*/ if (rectState == IN_PROCESS || rectState == SET) rectangle(res, Point(rect.x, rect.y), Point(rect.x + rect.width, rect.y + rect.height), GREEN, 2);
imshow(*winName, res); }
/*该步骤完成后,mask图像中rect内部是3,外面全是0*/ void GCApplication::setRectInMask() { assert(!mask.empty()); mask.setTo(GC_BGD); //GC_BGD == 0 rect.x = max(0, rect.x); rect.y = max(0, rect.y); rect.width = min(rect.width, image->cols - rect.x); rect.height = min(rect.height, image->rows - rect.y); (mask(rect)).setTo(Scalar(GC_PR_FGD)); //GC_PR_FGD == 3,矩形内部,为可能的前景点 }
void GCApplication::setLblsInMask(int flags, Point p, bool isPr) { vector<Point>* bpxls, * fpxls; uchar bvalue, fvalue; if (!isPr) //确定的点 { bpxls = &bgdPxls; fpxls = &fgdPxls; bvalue = GC_BGD; //0 fvalue = GC_FGD; //1 } else //概率点 { bpxls = &prBgdPxls; fpxls = &prFgdPxls; bvalue = GC_PR_BGD; //2 fvalue = GC_PR_FGD; //3 } if (flags & BGD_KEY) { bpxls->push_back(p); circle(mask, p, radius, bvalue, thickness); //该点处为2 } if (flags & FGD_KEY) { fpxls->push_back(p); circle(mask, p, radius, fvalue, thickness); //该点处为3 } }
/*鼠标响应函数,参数flags为CV_EVENT_FLAG的组合*/ void GCApplication::mouseClick(int event, int x, int y, int flags, void*) { // TODO add bad args check switch (event) { case CV_EVENT_LBUTTONDOWN: // set rect or GC_BGD(GC_FGD) labels { bool isb = (flags & BGD_KEY) != 0, isf = (flags & FGD_KEY) != 0; if (rectState == NOT_SET && !isb && !isf)//只有左键按下时 { rectState = IN_PROCESS; //表示正在画矩形 rect = Rect(x, y, 1, 1); } if ((isb || isf) && rectState == SET) //按下了alt键或者shift键,且画好了矩形,表示正在画前景背景点 lblsState = IN_PROCESS; } break; case CV_EVENT_RBUTTONDOWN: // set GC_PR_BGD(GC_PR_FGD) labels { bool isb = (flags & BGD_KEY) != 0, isf = (flags & FGD_KEY) != 0; if ((isb || isf) && rectState == SET) //正在画可能的前景背景点 prLblsState = IN_PROCESS; } break; case CV_EVENT_LBUTTONUP: if (rectState == IN_PROCESS) { rect = Rect(Point(rect.x, rect.y), Point(x, y)); //矩形结束 rectState = SET; setRectInMask(); assert(bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty()); showImage(); } if (lblsState == IN_PROCESS) //已画了前后景点 { setLblsInMask(flags, Point(x, y), false); //画出前景点 lblsState = SET; showImage(); } break; case CV_EVENT_RBUTTONUP: if (prLblsState == IN_PROCESS) { setLblsInMask(flags, Point(x, y), true); //画出背景点 prLblsState = SET; showImage(); } break; case CV_EVENT_MOUSEMOVE: if (rectState == IN_PROCESS) { rect = Rect(Point(rect.x, rect.y), Point(x, y)); assert(bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty()); showImage(); //不断的显示图片 } else if (lblsState == IN_PROCESS) { setLblsInMask(flags, Point(x, y), false); showImage(); } else if (prLblsState == IN_PROCESS) { setLblsInMask(flags, Point(x, y), true); showImage(); } break; } }
/*该函数进行grabcut算法,并且返回算法运行迭代的次数*/ int GCApplication::nextIter() { if (isInitialized) //使用grab算法进行一次迭代,参数2为mask,里面存的mask位是:矩形内部除掉那些可能是背景或者已经确定是背景后的所有的点,且mask同时也为输出 //保存的是分割后的前景图像 grabCut(*image, mask, rect, bgdModel, fgdModel, 1); else { if (rectState != SET) return iterCount;
if (lblsState == SET || prLblsState == SET) grabCut(*image, mask, rect, bgdModel, fgdModel, 1, GC_INIT_WITH_MASK); else grabCut(*image, mask, rect, bgdModel, fgdModel, 1, GC_INIT_WITH_RECT);
isInitialized = true; } iterCount++;
bgdPxls.clear(); fgdPxls.clear(); prBgdPxls.clear(); prFgdPxls.clear();
return iterCount; }
GCApplication gcapp;
static void on_mouse(int event, int x, int y, int flags, void* param) { gcapp.mouseClick(event, x, y, flags, param); }
int main(int argc, char** argv) {
string filename = "lena.jpg"; Mat image = imread(filename, 1); if (image.empty()) { cout << "\n Durn, couldn't read image filename " << filename << endl; return 1; }
help();
const string winName = "image"; cvNamedWindow(winName.c_str(), CV_WINDOW_AUTOSIZE); cvSetMouseCallback(winName.c_str(), on_mouse, 0);
gcapp.setImageAndWinName(image, winName); gcapp.showImage();
for (;;) { int c = cvWaitKey(0); switch ((char)c) { case '\x1b': cout << "Exiting ..." << endl; goto exit_main; case 'r': cout << endl; gcapp.reset(); gcapp.showImage(); break; case 'n': int iterCount = gcapp.getIterCount(); cout << "<" << iterCount << "... "; int newIterCount = gcapp.nextIter(); if (newIterCount > iterCount) { gcapp.showImage(); cout << iterCount << ">" << endl; } else cout << "rect must be determined>" << endl; break; } }
exit_main: cvDestroyWindow(winName.c_str()); return 0; }
3,matlab matlab中要与c+联合,太长了,所以就不说了。。。
Kmeans K-means算法算是个著名的聚类算法了,不仅容易实现,并且效果也不错,训练过程不需人工干预,实乃模式识别等领域的居家必备良品啊,今天就拿这个算法练练手。属于无监督学习中间接聚类方法中的动态聚类 流程: 1.随机选取样本中的K个点作为聚类中心 2.计算所有样本到各个聚类中心的距离,将每个样本规划在最近的聚类中 3.计算每个聚类中所有样本的中心,并将新的中心代替原来的中心 4.检查新老聚类中心的距离,如果距离超过规定的阈值,则重复2-4,直到小于阈值 聚类属于无监督学习,以往的回归、朴素贝叶斯、SVM等都是有类别标签y的,也就是说样例中已经给出了样例的分类。而聚类的样本中却没有给定y,只有特征x,比如假设宇宙中的星星可以表示成三维空间中的点集。聚类的目的是找到每个样本x潜在的类别y,并将同类别y的样本x放在一起。比如上面的星星,聚类后结果是一个个星团,星团里面的点相互距离比较近,星团间的星星距离就比较远了。 在聚类问题中,给我们的训练样本是,每个,没有了y。 K-means算法是将样本聚类成k个簇(cluster),具体算法描述如下: 1、 随机选取k个聚类质心点(cluster centroids)为。 2、 重复下面过程直到收敛 { 对于每一个样例i,计算其应该属于的类 对于每一个类j,重新计算该类的质心 K是我们事先给定的聚类数,代表样例i与k个类中距离最近的那个类,的值是1到k中的一个。质心代表我们对属于同一个类的样本中心点的猜测,拿星团模型来解释就是要将所有的星星聚成k个星团,首先随机选取k个宇宙中的点(或者k个星星)作为k个星团的质心,然后第一步对于每一个星星计算其到k个质心中每一个的距离,然后选取距离最近的那个星团作为,这样经过第一步每一个星星都有了所属的星团;第二步对于每一个星团,重新计算它的质心(对里面所有的星星坐标求平均)。重复迭代第一步和第二步直到质心不变或者变化很小。 下图展示了对n个样本点进行K-means聚类的效果,这里k取2。 参考;http://blog.csdn.net/holybin/article/details/22969747
Kmeans有以下几个优点: 1、是解决聚类问题的一种经典算法,算法简单、快速。 2、对处理大数据集,该算法是相对可伸缩的和高效率的,因为它的复杂度是线性的,大约是O(nkt),其中n是所有样本的数目,k是簇的数目,t是迭代的次数。通常k<<n。 3、该算法是收敛的(不会无限迭代下去)。 4、算法尝试找出使平方误差函数值最小的k个划分。当簇是密集的、球状或团状的,而簇与簇之间区别明显时,它的聚类效果很好。 Kmeans也存在如下缺点: 1、只有在簇的平均值被定义的情况下才能使用,不适用于某些应用,如涉及有分类属性的数据不适用。它的前提假设是样本数据的协方差矩阵已经归一化。 2、虽然理论证明它是一定可以收敛的,但是不能保证是全局收敛,可能是局部收敛,这样的话找到的聚类中心不一定是最佳方案。 3、要求用户必须事先给出要生成的簇的数目K。对于同一个数据样本集合,选择不同的K值,得到的结果是不一样的,甚至是不合理的。 4、对中心初值敏感,对于不同的初始值,可能会导致不同的聚类结果。 5、对于"噪声"和孤立点数据敏感,少量的该类数据能够对平均值产生极大影响。 6、不适合于发现非凸面形状的簇,或者大小差别很大的簇。 CV_EXPORTS_W double kmeans( InputArray data, int K, InputOutputArray bestLabels, TermCriteria criteria, int attempts, int flags, OutputArray centers = noArray() ); 注意,在opencv中,kmeans()函数是在core.h头文件中的,所以首先要包含这个头文件。下面分析其参数: 1 InputArray data:输入的待聚类向量,其中每一行是一个样本,有多少列,就有多少个样本。 2 int K: 要聚类的个数。 3 InputOutputArray bestLabels:行数与data是一样的,每行有一个数字,代表分类的编号。比如聚类的数目是8类,那么每行的数字在0-3。 4 TerCriteria criteria:这个变量用于控制结束条件。其中TerCriteria是一个模板类,在opencv中的定义如下: TermCriteria::TermCriteria(int _type, int _maxCount, double _epsilon) : type(_type), maxCount(_maxCount), epsilon(_epsilon) {} 其中,type的类型有三种: TermCriteria::COUNT: 代表以运行的步数为结束条件。 TermCriteria::EPS: 代表迭代到阈值时结束。 TermCriteria::COUNT + TermCriteria::EPS:当步数或阈值中有一个达到条件时终止。 _maxCount是运行的步数,_epsilon是阈值。 5 int attempts:这个变量控制kmean算法进行的次数,选择最优的结果做为最后结果。 6 int flags:这个变量可以取以下三个类型。 KMEANS_RANDOM_CENTERS:随机选择初始中心。 KMEANS_PP_CENTERS:以一种算法,确定初始中心。 KMEANS_USE_INITIAL_LABELS:用户自定义中心。 7 centers:这个变量代表具体的聚类中心。
下面来看一个例子CV_EXPORTS_W double kmeans(InputArray data, int K, InputOutputArray bestLabels, TermCriteria criteria, int attempts, int flags, OutputArray centers = noArray()); 注意,在opencv中,kmeans()函数是在core.h头文件中的,所以首先要包含这个头文件。下面分析其参数: 1 InputArray data:输入的待聚类向量,其中每一行是一个样本,有多少列,就有多少个样本。 2 int K: 要聚类的个数。 3 InputOutputArray bestLabels:行数与data是一样的,每行有一个数字,代表分类的编号。比如聚类的数目是8类,那么每行的数字在0 - 3。 4 TerCriteria criteria:这个变量用于控制结束条件。其中TerCriteria是一个模板类,在opencv中的定义如下: TermCriteria::TermCriteria(int _type, int _maxCount, double _epsilon) : type(_type), maxCount(_maxCount), epsilon(_epsilon) {} 其中,type的类型有三种: TermCriteria::COUNT: 代表以运行的步数为结束条件。 TermCriteria::EPS: 代表迭代到阈值时结束。 TermCriteria::COUNT + TermCriteria::EPS:当步数或阈值中有一个达到条件时终止。 _maxCount是运行的步数,_epsilon是阈值。 5 int attempts:这个变量控制kmean算法进行的次数,选择最优的结果做为最后结果。 6 int flags:这个变量可以取以下三个类型。 KMEANS_RANDOM_CENTERS:随机选择初始中心。 KMEANS_PP_CENTERS:以一种算法,确定初始中心。 KMEANS_USE_INITIAL_LABELS:用户自定义中心。 7 centers:这个变量代表具体的聚类中心 下面来看一个例子 #include "opencv2/highgui/highgui.hpp" #include "opencv2/core/core.hpp" #include <iostream> using namespace cv; using namespace std;
int main(int /*argc*/, char** /*argv*/) { const int MAX_CLUSTERS = 5; Scalar colorTab[] = { Scalar(0, 0, 255), Scalar(0,255,0), Scalar(255,100,100), Scalar(255,0,255), Scalar(0,255,255) };
Mat img(500, 500, CV_8UC3); RNG rng(12345);
for (;;) { int k, clusterCount = rng.uniform(2, MAX_CLUSTERS + 1); int i, sampleCount = rng.uniform(1, 1001); Mat points(sampleCount, 1, CV_32FC2), labels;
clusterCount = MIN(clusterCount, sampleCount); Mat centers(clusterCount, 1, points.type());
/* generate random sample from multigaussian distribution */ for (k = 0; k < clusterCount; k++) { Point center; center.x = rng.uniform(0, img.cols); center.y = rng.uniform(0, img.rows); Mat pointChunk = points.rowRange(k * sampleCount / clusterCount, k == clusterCount - 1 ? sampleCount : (k + 1) * sampleCount / clusterCount);
rng.fill(pointChunk, CV_RAND_NORMAL, Scalar(center.x, center.y), Scalar(img.cols * 0.05, img.rows * 0.05));
}
randShuffle(points, 1, &rng); kmeans(points, clusterCount, labels, TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 10, 1.0), 3, KMEANS_PP_CENTERS, centers);
img = Scalar::all(0);
for (i = 0; i < sampleCount; i++) { int clusterIdx = labels.at<int>(i); Point ipt = points.at<Point2f>(i); circle(img, ipt, 2, colorTab[clusterIdx], CV_FILLED, CV_AA); }
imshow("clusters", img);
char key = (char)waitKey(); if (key == 27 || key == 'q' || key == 'Q') // 'ESC' break; }
return 0; }
matlab K-means聚类算法采用的是将N*P的矩阵X划分为K个类,使得类内对象之间的距离最大,而类之间的距离最小。 使用方法: Idx=Kmeans(X,K) [Idx,C]=Kmeans(X,K) [Idx,C,sumD]=Kmeans(X,K) [Idx,C,sumD,D]=Kmeans(X,K) […]=Kmeans(…,’Param1’,Val1,’Param2’,Val2,…) 各输入输出参数介绍: X N*P的数据矩阵 K 表示将X划分为几类,为整数 Idx N*1的向量,存储的是每个点的聚类标号 C K*P的矩阵,存储的是K个聚类质心位置 sumD 1*K的和向量,存储的是类间所有点与该类质心点距离之和 D N*K的矩阵,存储的是每个点与所有质心的距离 […]=Kmeans(…,'Param1',Val1,'Param2',Val2,…) 这其中的参数Param1、Param2等,主要可以设置为如下: 1. 'Distance’(距离测度) 'sqEuclidean’ 欧式距离(默认时,采用此距离方式) 'cityblock’ 绝度误差和,又称:L1 'cosine’ 针对向量 'correlation’ 针对有时序关系的值 'Hamming’ 只针对二进制数据 2. 'Start’(初始质心位置选择方法) 'sample’ 从X中随机选取K个质心点 'uniform’ 根据X的分布范围均匀的随机生成K个质心 'cluster’ 初始聚类阶段随机选择10%的X的子样本(此方法初始使用’sample’方法) matrix 提供一K*P的矩阵,作为初始质心位置集合 3. 'Replicates’(聚类重复次数) 整数 X = [randn(100, 2) + ones(100, 2); ... randn(100, 2) - ones(100, 2)]; opts = statset('Display', 'final'); [idx, ctrs] = kmeans(X, 2, ... 'Distance', 'city', ... 'Replicates', 5, ... 'Options', opts);
plot(X(idx == 1, 1), X(idx == 1, 2), 'r.', 'MarkerSize', 12) hold on plot(X(idx == 2, 1), X(idx == 2, 2), 'b.', 'MarkerSize', 12) plot(ctrs(:, 1), ctrs(:, 2), 'kx', ... 'MarkerSize', 12, 'LineWidth', 2) plot(ctrs(:, 1), ctrs(:, 2), 'ko', ... 'MarkerSize', 12, 'LineWidth', 2) legend('Cluster 1', 'Cluster 2', 'Centroids', ... 'Location', 'NW')
图像金字塔,向上上下采样,resize插值 金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近似。我们将一层一层的图像比喻成金字塔,层级越高,则图像越小,分辨率越低
一、两个金字塔 高斯金字塔不同(DoG)又称为拉普拉斯金字塔,给出计算方式前,先加强一下定义 记得在上面我们定义了G0,G1,G2 G0下采样获得G1 G1上采样获得Upsample(G1),注意Upsample(G1)不等于G0,上采样和下采样不是可逆过程,这是因为下采样损失了图片信息 在此,给出计算拉普拉斯金字塔(DOG)的公式:L(i) = G(i) –Upsample(G(i+1)) 二、采样 对图像向上采样:pyrUp函数 <1>对图像G_i进行高斯内核卷积 <2>将所有偶数行和列去除
对图像向下采样:pyrDown函数
注意:这里的向下与向上采样,是对图像的尺寸而言的(和金字塔的方向相反),向上就是图像尺寸加倍,向下就是图像尺寸减半。而如果我们按上图中演示的金字塔方向来理解,金字塔向上图像其实在缩小,这样刚好是反过来了。 注意:PryUp和PryDown不是互逆的,即PryUp不是降采样的逆操作。这种情况下,图像首先在每个维度上扩大为原来的两倍,新增的行(偶数行)以0填充。然后给指定的滤波器进行卷积(实际上是一个在每个维度都扩大为原来两倍的过滤器)去估计“丢失”像素的近似值。 PryDown( )是一个会丢失信息的函数。为了恢复原来更高的分辨率的图像,我们要获得由降采样操作丢失的信息,这些数据就和拉普拉斯金字塔有关系了。 三、函数介绍 <span style="font-size:18px;">C++: void pyrUp(InputArray src, OutputArraydst, const Size& dstsize=Size(), int borderType=BORDER_DEFAULT ) </span>
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。 第二个参数,OutputArray类型的dst,输出图像,和源图片有一样的尺寸和类型。 第三个参数,const Size&类型的dstsize,输出图像的大小;有默认值Size(),即默认情况下,由Size(src.cols*2,src.rows*2)来进行计算,且一直需要满足下列条件:
void pyrDown(InputArray src,OutputArray dst, const Size& dstsize=Size(), int borderType=BORDER_DEFAULT)
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。 第二个参数,OutputArray类型的dst,输出图像,和源图片有一样的尺寸和类型。 第三个参数,const Size&类型的dstsize,输出图像的大小;有默认值Size(),即默认情况下,由Size Size((src.cols+1)/2, (src.rows+1)/2)来进行计算,且一直需要满足下列条件:
该pyrDown函数执行了高斯金字塔建造的向下采样的步骤。首先,它将源图像与如下内核做卷积运算: 接着,它便通过对图像的偶数行和列做插值来进行向下采样操作。 pyrUp函数执行高斯金字塔的采样操作,其实它也可以用于拉普拉斯金字塔的。 首先,它通过插入可为零的行与列,对源图像进行向上取样操作,然后将结果与pyrDown()乘以4的内核做卷积,就是这样。 void resize(InputArray src,OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR)
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。 第二个参数,OutputArray类型的dst,输出图像,当其非零时,有着dsize(第三个参数)的尺寸,或者由src.size()计算出来。 第三个参数,Size类型的dsize,输出图像的大小;如果它等于零,由下式进行计算:
其中,dsize,fx,fy都不能为0。
可选的插值方式如下:
INTER_NEAREST - 最近邻插值 INTER_LINEAR - 线性插值(默认值) INTER_AREA - 区域插值(利用像素区域关系的重采样插值) INTER_CUBIC –三次样条插值(超过4×4像素邻域内的双三次插值) INTER_LANCZOS4 -Lanczos插值(超过8×8像素邻域的Lanczos插值)
若要缩小图像,一般情况下最好用CV_INTER_AREA来插值, 而若要放大图像,一般情况下最好用CV_INTER_CUBIC(效率不高,慢,不推荐使用)或CV_INTER_LINEAR(效率较高,速度较快,推荐使用)。
四、插值介绍1、最邻近元法 这是最简单的一种插值方法,不需要计算,在待求象素的四邻象素中,将距离待求象素最近的邻象素灰度赋给待求象素。设i+u,
j+v(i, j为正整数, u, v为大于零小于1的小数,下同)为待求象素坐标,则待求象素灰度的值 f(i+u, j+v) 如下图所示: 如果(i+u, j+v)落在A区,即u<0.5, v<0.5,则将左上角象素的灰度值赋给待求象素,同理,落在B区则赋予右上角的象素灰度值,落在C区则赋予左下角象素的灰度值,落在D区则赋予右下角象素的灰度值。 最邻近元法计算量较小,但可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能出现明显的锯齿状。 2、双线性内插法 双线性内插法是利用待求象素四个邻象素的灰度在两个方向上作线性内插,如下图所示: 对于 (i, j+v),f(i, j) 到 f(i, j+1) 的灰度变化为线性关系,则有: f(i, j+v) = [f(i, j+1) - f(i, j)] * v + f(i, j) 同理对于 (i+1, j+v) 则有: f(i+1, j+v) = [f(i+1, j+1) - f(i+1, j)] * v + f(i+1, j) 从f(i, j+v) 到 f(i+1, j+v) 的灰度变化也为线性关系,由此可推导出待求象素灰度的计算式如下: f(i+u, j+v) = (1-u) * (1-v) * f(i, j) + (1-u) * v * f(i, j+1) + u * (1-v) * f(i+1, j) + u * v * f(i+1, j+1) 双线性内插法的计算比最邻近点法复杂,计算量较大,但没有灰度不连续的缺点,结果基本令人满意。它具有低通滤波性质,使高频分量受损,图像轮廓可能会有一点模糊。 3、三次内插法 该方法利用三次多项式S(x)求逼近理论上最佳插值函数sin(x)/x, 其数学表达式为: 待求像素(x, y)的灰度值由其周围16个灰度值加权内插得到,如下图: 待求像素的灰度计算式如下: f(x, y) = f(i+u, j+v) = ABC 其中: 三次曲线插值方法计算量较大,但插值后的图像效果最好。 五、综合示例 #include <opencv2/opencv.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> using namespace std; using namespace cv;
Mat g_srcImage, g_dstImage, g_tmpImage; int main() {
ShowHelpText(); g_srcImage = imread("lena.jpg"); if (!g_srcImage.data) { printf("Oh,no,读取srcImage错误~! \n"); return false; } namedWindow(WINDOW_NAME, CV_WINDOW_AUTOSIZE); imshow(WINDOW_NAME, g_srcImage); g_tmpImage = g_srcImage; g_dstImage = g_tmpImage; int key = 0; while (1) { key = waitKey(9); //根据key变量的值,进行不同的操作 switch (key) {
case 27: return 0; break;
case 'q': return 0; break;
case 'a'://按键A按下,调用pyrUp函数 pyrUp(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2)); printf(">检测到按键【A】被按下,开始进行基于【pyrUp】函数的图片放大:图片尺寸×2 \n"); break;
case 'w'://按键W按下,调用resize函数 resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2)); printf(">检测到按键【W】被按下,开始进行基于【resize】函数的图片放大:图片尺寸×2 \n"); break;
case '1'://按键1按下,调用resize函数 resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2)); printf(">检测到按键【1】被按下,开始进行基于【resize】函数的图片放大:图片尺寸×2 \n"); break;
case '3': //按键3按下,调用pyrUp函数 pyrUp(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2)); printf(">检测到按键【3】被按下,开始进行基于【pyrUp】函数的图片放大:图片尺寸×2 \n"); break; //======================【图片缩小相关键值处理】======================= case 'd': //按键D按下,调用pyrDown函数 pyrDown(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2)); printf(">检测到按键【D】被按下,开始进行基于【pyrDown】函数的图片缩小:图片尺寸/2\n"); break;
case 's': //按键S按下,调用resize函数 resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2)); printf(">检测到按键【S】被按下,开始进行基于【resize】函数的图片缩小:图片尺寸/2\n"); break;
case '2'://按键2按下,调用resize函数 resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2), (0, 0), (0, 0), 2); printf(">检测到按键【2】被按下,开始进行基于【resize】函数的图片缩小:图片尺寸/2\n"); break;
case '4': //按键4按下,调用pyrDown函数 pyrDown(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2)); printf(">检测到按键【4】被按下,开始进行基于【pyrDown】函数的图片缩小:图片尺寸/2\n"); break; }
//经过操作后,显示变化后的图 imshow(WINDOW_NAME, g_dstImage);
//将g_dstImage赋给g_tmpImage,方便下一次循环 g_tmpImage = g_dstImage; }
return 0; }
六、matlab I = imread('d:\lena.jpg'); A = imresize(I, 1.5, 'nearest'); B = imresize(I, 1.5, 'bilinear'); C = imresize(I, 1.5, 'bicubic'); subplot(2,2,1), imshow(I), title('original'); subplot(2,2,2), imshow(A), title('nearest'); subplot(2,2,3), imshow(B), title('bilinear'); subplot(2,2,4), imshow(C), title('bicubic');
重映射,仿射变换一、序言 面对图像处理的时候,我们会旋转缩放图像,例如前面所提高的resize 插值改变,也是几何变换:
几何运算需要空间变换和灰度级差值两个步骤的算法,像素通过变换映射到新的坐标位置,新的位置可能是在几个像素之间,即不一定为整数坐标。这时就需要灰度级差值将映射的新坐标匹配到输出像素之间。最简单的插值方法是最近邻插值,就是令输出像素的灰度值等于映射最近的位置像素,该方法可能会产生锯齿。这种方法也叫零阶插值,相应比较复杂的还有一阶和高阶插值。
除了插值算法感觉只要了解就可以了,图像处理中比较需要理解的还是空间变换。 空间变换对应矩阵的仿射变换。一个坐标通过函数变换的新的坐标位置:
所以在程序中我们可以使用一个2*3的数组结构来存储变换矩阵:
以最简单的平移变换为例,平移(b1,b2)坐标可以表示为:
因此,平移变换的变换矩阵及逆矩阵记为:
缩放变换:将图像横坐标放大(或缩小)sx倍,纵坐标放大(或缩小)sy倍,变换矩阵及逆矩阵为:
选择变换:图像绕原点逆时针旋转a角,其变换矩阵及逆矩阵(顺时针选择)为: 二、重映射 重映射: 把一个图像中一个位置的像素放置到另一个图片指定位置的过程. 为了完成映射过程, 有必要获得一些插值为非整数像素坐标,因为源图像与目标图像的像素坐标不是一一对应的. 简单的说就是改变图片的位置(左,右,上,下,颠倒) void remap(InputArray src, OutputArraydst, InputArray map1, InputArray map2, int interpolation, intborderMode=BORDER_CONSTANT , const Scalar& borderValue=Scalar())
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位或者浮点型图像。 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,即这个参数用于存放函数调用后的输出结果,需和源图片有一样的尺寸和类型。 第三个参数,InputArray类型的map1,它有两种可能的表示对象。
INTER_NEAREST - 最近邻插值 INTER_LINEAR – 双线性插值(默认值) INTER_CUBIC – 双三次样条插值(逾4×4像素邻域内的双三次插值) INTER_LANCZOS4 -Lanczos插值(逾8×8像素邻域的Lanczos插值)
其中要变换的模式如下
三、仿射变换 仿射变换(Affine Transformation)是空间直角坐标系的变换,从一个二维坐标变换到另一个二维坐标,仿射变换是一个线性变换,他保持了图像的“平行性”和“平直性”,即图像中原来的直线和平行线,变换后仍然保持原来的直线和平行线,仿射变换比较常用的特殊变换有平移(Translation)、缩放(Scale)、翻转(Flip)、旋转(Rotation)和剪切(Shear)。
其中,点1, 2 和 3 (在图一中形成一个三角形) 与图二中三个点是一一映射的关系, 且他们仍然形成三角形, 但形状已经和之前不一样了。我们能通过这样两组三点求出仿射变换 (可以选择自己喜欢的点),
接着就可以把仿射变换应用到图像中去。
而我们通常使用2 x 3的矩阵来表示仿射变换。
考虑到我们要使用矩阵 A 和 B 对二维向量 做变换,
所以也能表示为下列形式: 或者
即: 也可以理解是坐标系的旋转和缩放、平移
void warpAffine(InputArray src,OutputArray dst, InputArray M, Size dsize, int flags=INTER_LINEAR, intborderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,需和源图片有一样的尺寸和类型。 第三个参数,InputArray类型的M,2×3的变换矩阵。 第四个参数,Size类型的dsize,表示输出图像的尺寸。 第五个参数,int类型的flags,插值方法的标识符。此参数有默认值INTER_LINEAR(线性插值),可选的插值方式如下:
INTER_NEAREST - 最近邻插值 INTER_LINEAR - 线性插值(默认值) INTER_AREA - 区域插值 INTER_CUBIC –三次样条插值 INTER_LANCZOS4 -Lanczos插值 CV_WARP_FILL_OUTLIERS - 填充所有输出图像的象素。如果部分象素落在输入图像的边界外,那么它们的值设定为 fillval. CV_WARP_INVERSE_MAP –表示M为输出图像到输入图像的反变换,即 。因此可以直接用来做象素插值。否则, warpAffine函数从M矩阵得到反变换。
四、例子#include<opencv2/opencv.hpp> #include<iostream> #include<vector> using namespace cv; using namespace std; int main() { Mat srcImage = imread("lena.jpg", 1); imshow("【原图】", srcImage); Mat grayImage; cvtColor(srcImage, grayImage, CV_BGR2GRAY); Mat XImage, YImage; Mat dstImage; dstImage.create(srcImage.size(), srcImage.type()); XImage.create(srcImage.size(), CV_32FC1); YImage.create(srcImage.size(), CV_32FC1); for (int i = 0; i < srcImage.rows; i++) { for (int j = 0; j < srcImage.cols; j++) { XImage.at<float>(i, j) = static_cast<float>(srcImage.cols - j); YImage.at<float>(i, j) = static_cast<float>(i); } } remap(srcImage, dstImage, XImage, YImage, CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0)); imshow("【重映射后】", dstImage); waitKey(0); return 0; }
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> using namespace cv; using namespace std; char* source_window = "Source image"; char* warp_window = "Warp"; char* warp_rotate_window = "Warp + Rotate"; int main(int argc, char** argv) { Point2f srcTri[3]; Point2f dstTri[3]; Mat rot_mat(2, 3, CV_32FC1); Mat warp_mat(2, 3, CV_32FC1); Mat src, warp_dst, warp_rotate_dst; src = imread("lena.jpg", 1); warp_dst = Mat::zeros(src.rows, src.cols, src.type()); srcTri[0] = Point2f(0, 0); srcTri[1] = Point2f(src.cols - 1, 0); srcTri[2] = Point2f(0, src.rows - 1); dstTri[0] = Point2f(src.cols * 0.0, src.rows * 0.33); dstTri[1] = Point2f(src.cols * 0.85, src.rows * 0.25); dstTri[2] = Point2f(src.cols * 0.15, src.rows * 0.7); warp_mat = getAffineTransform(srcTri, dstTri); warpAffine(src, warp_dst, warp_mat, warp_dst.size()); 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()); namedWindow(source_window, CV_WINDOW_AUTOSIZE); imshow(source_window, src); namedWindow(warp_window, CV_WINDOW_AUTOSIZE); imshow(warp_window, warp_dst); namedWindow(warp_rotate_window, CV_WINDOW_AUTOSIZE); imshow(warp_rotate_window, warp_rotate_dst); waitKey(0); return 0; }
五、matlab f=imread('d:\lena.jpg'); tform=maketform('affine',[-1 0 0;0 1 0;0 0 1]); ff=imtransform(f,tform); imshow(f) figure imshow(ff)
图像修补,分离合并通道一、图像修复简介
图像修复是图像复原中的一个重要内容,其目的是利用图像现有的信息来恢复丢失的信息。可用于旧照片中丢失信息的恢复,视频文字去除以及视频错误隐藏等。简言之,图像修复就是对图像上信息缺损区域进行信息填充的过程,其目的就是为了对有信息缺损的图像进行复原,并且使得观察者无法察觉到图像曾经缺损或者已经修复 图像修复技术简单来说,就是利用那些被破坏区域的边缘,即是边缘的颜色和结构,繁殖和混合到损坏的图像中,来进行修复图像 目前存在两大类图像修复技术:一类是用于修复小尺度缺损的数字图像修补(inpainting)技术。即,利用待修补区域的边缘信息,同时采用一种由粗到精的方法来估计等照度线的方向,并采用传播机制将信息传播到待修补的区域内,以便达到较好的修补效果;另外一类是用于填充图像大块丢失信息的图像补全技术。目前,这一技术分为以下两种方法:一种是基于图像分解的修复方法,其主要思想是将图像分解为结构部分和纹理部分。其中,结构部分用inpainting的技术来修复,而纹理部分则采用纹理合成的方法来填充。另一种方法是用基于块的纹理合成技术来填充丢失的信息,其主要思想是:首先从待修补区域的边界上选取一个像素点,同时以该点为中心,根据图像的纹理特征,选择大小合适的纹理块,然后在待修补区域的周围寻找与之最相近的纹理匹配块来替代该纹理块。近几年来,利用纹理合成来修复大块丢失信息的图像合成技术得到了相当的研究,也取得了一定的成果。需要提醒的是,图像修复技术是一种对视觉感知过程的学习和理解。它是一个不确定问题,没有唯一解的存在,解的合理性取决于视觉系统的接受程度。换言之,为了达到较好的视觉效果,我们必须让修复效果更加符合视觉感知的特性,使得图像看起来浑然一体,没有修改过的痕迹。 二、原理介绍opencv提供了2中方法,这里主要说的是INPAINT_TELEA 参考文献为Alexandru Telea于2004年发表于Journal
of GraphicTools上An ImageInpainting Technique Based On the Fast Marching Method”也称为FMM算法 如何修复一个像素点的?
参考上图,Ω区域是待修复的区域;δΩ指Ω的边界);要修复Ω中的像素,就需要计算出新的像素值来代替原值。 现在假设p点是我们要修复的像素。以p为中心选取一个小邻域B(ε),该邻域中的点像素值都是已知的(只要已知的)。(这个ε就是opencv函数中参数
inpaintRadius) 现在假设p点是我们要修复的像素。以p为中心选取一个小邻域B(ε),该邻域中的点像素值都是已知的(只要已知的)。(这个ε就是opencv函数中参数
inpaintRadius) q为 Bε(p)中的一点,由q点计算P的灰度值公式如下
显然,我们需要的是用邻域Bε(p)中的所有点计算p点的新灰度值。显然,各个像素点所起的作用应该是不同的,也就引入了权值函数来决定哪些像素的值对新像素值影响更大,哪些比较小。采用下面的公式(公式2):
这里的w(p, q)就是权值函数,是用来限定邻域中各像素的贡献大小的。 w(p, q) = dir(p, q) ·dst(p, q) · lev(p, q)
其中,d0和 T0分别为距离参数和水平集参数,一般都取为
1。方向因子 dir(p,q)保证了越靠近法线方向 N = ?T的像素点对 p点的贡献最大;几何距离因子
dst(p,q)保证了离 p点越近的像素点对p点贡献越大;水平集距离因子lev(p,q)保证了离经过点
p的待修复区域的轮廓线越近的已知像素点对点 p的贡献越大。
三、图像修复应用 void inpaint(InputArray src, InputArray inpaintMask, OutputArray dst, double inpaintRadius, int flags)
第一个参数,InputArray类型的src,也就是输入图像,用Mat类对象就可以了。并且要是8位单通道或者三通道图像。 第二个参数,InputArray类型的inpaintMask,修复掩膜,为8位单通道图像。其中非0像素表示要修复的区域。 第三个参数,OutputArray类型的dst,函数调用后的运算结果保存在这里,和输入图像有着一样的大小和类型。 第四个参数,double类型的inpaintRadius,需要修补的每个点的圆形邻域,为修复算法参考的半径。 第五个参数,int类型的flags,修补方法的标识符。如下 #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/photo/photo.hpp"
#include <iostream> using namespace cv; using namespace std;
#define WINDOW_NAME1 "【原始图】" #define WINDOW_NAME2 "【修补后】"
Mat srcImage1, inpaintMask; // 原来的点坐标 Point previousPoint(-1, -1);
static void On_Mouse(int event, int x, int y, int flags, void*) { if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) previousPoint = Point(-1, -1); else if (event == EVENT_LBUTTONDOWN) previousPoint = Point(x, y); else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) { Point pt(x, y); if (previousPoint.x < 0) previousPoint = pt;
line(inpaintMask, previousPoint, pt, Scalar::all(255), 5, 8, 0); line(srcImage1, previousPoint, pt, Scalar::all(255), 5, 8, 0); previousPoint = pt; imshow(WINDOW_NAME1, srcImage1); } }
int main(int argc, char** argv) { Mat srcImage = imread("lena.jpg", -1); if (!srcImage.data) { printf("读取照片错误,请确定目录下是否有imread函数指定的图片存在!\n"); return false; } srcImage1 = srcImage.clone(); inpaintMask = Mat::zeros(srcImage1.size(), CV_8U);
// 显示原图 imshow(WINDOW_NAME1, srcImage1);
// 设置消息回调消息 setMouseCallback(WINDOW_NAME1, On_Mouse, 0);
while (1) { char c = (char)waitKey();
// 按键为ESC则退出 if (c == 27) break;
// 按键为'2’则恢复原来图像 if (c == '2') { inpaintMask = Scalar::all(0); srcImage.copyTo(srcImage1); imshow(WINDOW_NAME1, srcImage1); }
// 按键为1或者空格,则进行修补操作 if (c == '1' || c == ' ') { Mat inpaintedImage; inpaint(srcImage1, inpaintMask, inpaintedImage, 3, INPAINT_TELEA); imshow(WINDOW_NAME2, inpaintedImage); imshow("yanmo", inpaintMask); }
} return 0; }
四、通道分离与合并 void split(InputArray m,OutputArrayOfArrays mv);
void merge(InputArrayOfArrays mv,OutputArray dst); 通道的分离合并也是经常用的,因为比较简单所以就穿插在里面了,不单独作为来说,看看就好
Mat srcImage; Mat imageROI; vector<Mat> channels; srcImage = cv::imread("dota.jpg"); // 把一个3通道图像转换成3个单通道图像 split(srcImage, channels);//分离色彩通道 imageROI = channels.at(0); addWeighted(imageROI(Rect(385, 250, logoImage.cols, logoImage.rows)), 1.0, logoImage, 0.5, 0., imageROI(Rect(385, 250, logoImage.cols, logoImage.rows)));
merge(channels, srcImage4);
namedWindow("sample"); imshow("sample", srcImage);
五、matlab
图像修复,基于偏微分方程的主要有三个:经典的PDE模型有BSCB模型,TV模型和CDD模型,用了BSCB修了一下,感觉一般,也可能是设置不合理吧,读着可以自己找这个三种方法的原理和实现,这里就不多说了,
直方图对比,模版匹配,方向投影0、预备知识 归一化就是要把需要处理的数据经过处理后(通过某种算法)限制在你需要的一定范围内。 函数原型: void normalize(InputArray src,OutputArray dst, double alpha=1, doublebeta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray())
该函数归一化输入数组使它的范数或者数值范围在一定的范围内。 Parameters: src 输入数组 dst 输出数组,支持原地运算 alpha range normalization模式的最小值 beta range normalization模式的最大值,不用于norm normalization(范数归一化)模式。 normType 归一化的类型,可以有以下的取值: NORM_MINMAX:数组的数值被平移或缩放到一个指定的范围,线性归一化,一般较常用。 NORM_INF: 此类型的定义没有查到,根据OpenCV 1的对应项,可能是归一化数组的C-范数(绝对值的最大值) NORM_L1 : 归一化数组的L1-范数(绝对值的和) NORM_L2: 归一化数组的(欧几里德)L2-范数 dtype dtype为负数时,输出数组的type与输入数组的type相同; 否则,输出数组与输入数组只是通道数相同,而tpye=CV_MAT_DEPTH(dtype). mask 操作掩膜,用于指示函数是否仅仅对指定的元素进行操作
归一化公式: 1、线性函数转换,表达式如下:(对应NORM_MINMAX) ifmask(i,j)!=0 dst(i,j)=(src(i,j)-min(src))*(b'-a')/(max(src)-min(src))+ a' else dst(i,j)=src(i,j) 其中b'=MAX(a,b), a'=MIN(a,b); 2. 当norm_type!=CV_MINMAX: ifmask(i,j)!=0 dst(i,j)=src(i,j)*a/norm (src,norm_type,mask) else dst(i,j)=src(i,j) 其中,函数norm的功能是计算norm(范数)的绝对值 Thefunctions normcalculate an absolute norm of src1 (when there is no src2 ):
、将该数值存储在新的图像中(BackProjection),也可以先归一化hue直方图数值到0-255范围,这样可以直接显示BackProjection图像(单通道图像)。 mixChannels: 从输入中拷贝某通道到输出中特定的通道。 void mixChannels(const Mat*src, size_t nsrcs, Mat* dst, size_t ndsts, const int*fromTo, size_t npairs) 一、直方图对比double compareHist(InputArray H1, //直方图1 InputArray H2, //直方图2 intmethod//对比方法 );
method有CV_COMP_CORREL, CV_COMP_CHISQR,CV_COMP_INTERSECT,CV_COMP_BHATTACHARYYA四种方法,对应公式如下:
来个实例 #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" using namespace cv;
int main() {
Mat srcImage_base, hsvImage_base; Mat srcImage_test1, hsvImage_test1; Mat srcImage_test2, hsvImage_test2; Mat hsvImage_halfDown; srcImage_base = imread("1.jpg", 1); srcImage_test1 = imread("2.jpg", 1); srcImage_test2 = imread("3.jpg", 1); imshow("基准图像", srcImage_base); imshow("测试图像1", srcImage_test1); imshow("测试图像2", srcImage_test2); cvtColor(srcImage_base, hsvImage_base, COLOR_BGR2HSV); cvtColor(srcImage_test1, hsvImage_test1, COLOR_BGR2HSV); cvtColor(srcImage_test2, hsvImage_test2, COLOR_BGR2HSV); hsvImage_halfDown = hsvImage_base(Range(hsvImage_base.rows / 2, hsvImage_base.rows - 1), Range(0, hsvImage_base.cols - 1)); int h_bins = 50; int s_bins = 60; int histSize[] = { h_bins, s_bins }; float h_ranges[] = { 0, 256 }; float s_ranges[] = { 0, 180 }; const float* ranges[] = { h_ranges, s_ranges }; int channels[] = { 0, 1 }; MatND baseHist; MatND halfDownHist; MatND testHist1; MatND testHist2; calcHist(&hsvImage_base, 1, channels, Mat(), baseHist, 2, histSize, ranges, true, false); normalize(baseHist, baseHist, 0, 1, NORM_MINMAX, -1, Mat()); calcHist(&hsvImage_halfDown, 1, channels, Mat(), halfDownHist, 2, histSize, ranges, true, false); normalize(halfDownHist, halfDownHist, 0, 1, NORM_MINMAX, -1, Mat()); calcHist(&hsvImage_test1, 1, channels, Mat(), testHist1, 2, histSize, ranges, true, false); normalize(testHist1, testHist1, 0, 1, NORM_MINMAX, -1, Mat()); calcHist(&hsvImage_test2, 1, channels, Mat(), testHist2, 2, histSize, ranges, true, false); normalize(testHist2, testHist2, 0, 1, NORM_MINMAX, -1, Mat()); for (int i = 0; i < 4; i++) {
int compare_method = i; double base_base = compareHist(baseHist, baseHist, compare_method); double base_half = compareHist(baseHist, halfDownHist, compare_method); double base_test1 = compareHist(baseHist, testHist1, compare_method); double base_test2 = compareHist(baseHist, testHist2, compare_method); printf(" 方法 [%d] 的匹配结果如下:\n\n 【基准图 - 基准图】:%f, 【基准图 - 半身图】:%f,【基准图 - 测试图1】: %f, 【基准图 - 测试图2】:%f \n-----------------------------------------------------------------\n", i, base_base, base_half, base_test1, base_test2); }
printf("检测结束。"); waitKey(0); return 0; }
二、模版匹配 模板匹配是一项在一幅图像中寻找与另一幅模板图像最匹配(相似)部分的技术.模板匹配是一种用于在源图像S中寻找定位给定目标图像T(即模板图像)的技术。其原理很简单,就是通过一些相似度准则来衡量两个图像块之间的相似度Similarity(S,T)。 通过 模版滑动, 我们的意思是图像块一次移动一个像素 (从左往右,从上往下). 在每一个位置, 都进行一次度量计算来表明它是 “好” 或 “坏” 地与那个位置匹配 (或者说块图像和原图像的特定区域有多么相似). 对于 T模版覆盖在 I原图 上的每个位置,你把度量值 保存 到 结果图像矩阵 (R) 中. 在 R 中的每个位置 (x,y) 都包含匹配度量值:最白的位置代表最高的匹配. 正如您所见, 红色椭圆框住的位置很可能是结果图像矩阵中的最大数值, 所以这个区域 (以这个点为顶点,长宽和模板图像一样大小的矩阵) 被认为是匹配的.我们使用函数 minMaxLoc 来定位在矩阵 R 中的最大值点 (或者最小值, 根据函数输入的匹配参数) . 目标匹配函数: void MatchTemplate(InputArray image, InputArray temp1, OutputArray result, int method); Image 待搜索图像 Templ 模板图像 Result 匹配结果 用来存放通过以下方法计算出滑动窗口与模板的相似值 Method 计算匹配程度的方法 关于匹配方法,使用不同的方法产生的结果的意义可能不太一样,有些返回的值越大表示匹配程度越好,而有些方法返回的值越小表示匹配程度越好 关于参数 method: TM_SQDIFF平方差匹配法:该方法采用平方差来进行匹配;最好的匹配值为0;匹配越差,匹配值越大。 TM_CCORR相关匹配法:该方法采用乘法操作;数值越大表明匹配程度越好。 TM_CCOEFF相关系数匹配法:1表示完美的匹配; - 1表示最差的匹配。 TM_SQDIFF_NORMED归一化平方差匹配法 CV_TM_CCORR_NORMED归一化相关匹配法 CV_TM_CCOEFF_NORMED归一化相关系数匹配法
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> using namespace std; using namespace cv; Mat img; Mat templ; Mat result; char* image_window = "Source Image"; char* result_window = "Result window"; int match_method; int max_Trackbar = 5; void MatchingMethod(int, void*); int main(int argc, char** argv) { img = imread("1.jpg", 1); templ = imread("2.jpg", 1); namedWindow(image_window, CV_WINDOW_AUTOSIZE); namedWindow(result_window, CV_WINDOW_AUTOSIZE);
char* trackbar_label = "Method: \n 0: SQDIFF \n 1: SQDIFF NORMED \n 2:
TM CCORR \n 3: TM CCORR NORMED \n 4: TM COEFF \n 5: TM COEFF NORMED"; createTrackbar(trackbar_label, image_window, &match_method, max_Trackbar, MatchingMethod); MatchingMethod(0, 0); waitKey(0); return 0; }
void MatchingMethod(int, void*) {
Mat img_display; img.copyTo(img_display); int result_cols = img.cols - templ.cols + 1; int result_rows = img.rows - templ.rows + 1; result.create(result_cols, result_rows, CV_32FC1); matchTemplate(img, templ, result, match_method); normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat()); double minVal; double maxVal; Point minLoc; Point maxLoc; Point matchLoc; minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat()); if (match_method == CV_TM_SQDIFF || match_method == CV_TM_SQDIFF_NORMED) { matchLoc = minLoc; } else { matchLoc = maxLoc; } rectangle(img_display, matchLoc, Point(matchLoc.x + templ.cols, matchLoc.y + templ.rows), Scalar::all(0), 2, 8, 0); rectangle(result, matchLoc, Point(matchLoc.x + templ.cols, matchLoc.y + templ.rows), Scalar::all(0), 2, 8, 0); imshow(image_window, img_display); imshow(result_window, result); return; }
三、反向投影 什么是反向投影直方图呢?简单的说在灰度图像的每个点(x,y),用它对应的直方图的bin的值(就是有多少像素落在bin内)来代替它。所以·如果这个bin的值比较大,那么反向投影显示的结果会比较亮,否则就比较暗。 从统计学的角度,反输出图像象素点的值是观测数组在某个分布(直方图)下的的概率。 所以加入我们已经得到了一个物体的直方图,我们可以计算它在另一幅图像中的反向投影,来判断这幅图像中是否有该物体。 1.反向投影的作用是什么? 反向投影用于在输入图像(通常较大)中查找特定图像(通常较小或者仅1个像素,以下将其称为模板图像)最匹配的点或者区域,也就是定位模板图像出现在输入图像的位置。 2.反向投影如何查找(工作)? 查找的方式就是不断的在输入图像中切割跟模板图像大小一致的图像块,并用直方图对比的方式与模板图像进行比较。 3.反向投影的结果是什么? 反向投影的结果包含了:以每个输入图像像素点为起点的直方图对比结果。可以把它看成是一个二维的浮点型数组,二维矩阵,或者单通道的浮点型图像。backproject是直接取直方图中的值,即以灰度为例,某种灰度值在整幅图像中所占面积越大,其在直方图中的值越大,backproject时,其对应的像素的新值越大(越亮),反过来,某灰度值所占面积越小,其新值就越小。 假设我们有一张100x100的输入图像,有一张10x10的模板图像,查找的过程是这样的: (1)从输入图像的左上角(0,0)开始,切割一块(0,0)至(10,10)的临时图像; (2)生成临时图像的直方图; (3)用临时图像的直方图和模板图像的直方图对比,对比结果记为c; (4)直方图对比结果c,就是结果图像(0,0)处的像素值; (5)切割输入图像从(0,1)至(10,11)的临时图像,对比直方图,并记录到结果图像; (6)重复(1)~(5)步直到输入图像的右下角。 原图中以某点为基准,抠出来作对比的部分也转换为直方图,两个直方图作匹配,匹配的结果作为此点的值。结果会是一张概率图,概率越大的地方,代表此区域与模板的相似度越高。 利用Hue直方图解释反向投影原理: 1、获取测试图像中每个像素的hue数据 hi,j,并找到 hi,j 在hue直方图中的bin的位置。 2、查询hue直方图中对应bin的数值。 3、将该数值存储在新的图像中(BackProjection),也可以先归一化hue直方图数值到0-255范围,这样可以直接显示BackProjection图像(单通道图像)。 4、通过对测试图像每个像素采取以上步骤,可以得到最终的BackProjection图像。 void cvCalcBackProject( IplImage** image, CvArr* back_project, const CvHistogram* hist );
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" using namespace cv; #define WINDOW_NAME1 "【原始图】" Mat g_srcImage; Mat g_hsvImage; Mat g_hueImage; int g_bins = 30;//直方图组距 void on_BinChange(int, void*); int main() {
g_srcImage = imread("1.jpg", 1); if (!g_srcImage.data) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; } cvtColor(g_srcImage, g_hsvImage, COLOR_BGR2HSV); g_hueImage.create(g_hsvImage.size(), g_hsvImage.depth()); int ch[] = { 0, 0 }; mixChannels(&g_hsvImage, 1, &g_hueImage, 1, ch, 1); namedWindow(WINDOW_NAME1, WINDOW_AUTOSIZE); createTrackbar("色调组距 ", WINDOW_NAME1, &g_bins, 180, on_BinChange); on_BinChange(0, 0);//进行一次初始化 imshow(WINDOW_NAME1, g_srcImage); waitKey(0); return 0; }
void on_BinChange(int, void*) {
MatND hist; int histSize = MAX(g_bins, 2); float hue_range[] = { 0, 180 }; const float* ranges = { hue_range }; calcHist(&g_hueImage, 1, 0, Mat(), hist, 1, &histSize, &ranges, true, false); normalize(hist, hist, 0, 255, NORM_MINMAX, -1, Mat()); MatND backproj; calcBackProject(&g_hueImage, 1, 0, hist, backproj, &ranges, 1, true); imshow("反向投影图", backproj); int w = 400; int h = 400; int bin_w = cvRound((double)w / histSize); Mat histImg = Mat::zeros(w, h, CV_8UC3); for (int i = 0; i < g_bins; i++) { rectangle(histImg, Point(i * bin_w, h), Point((i + 1) * bin_w, h - cvRound(hist.at<float>(i) * h / 255.0)), Scalar(100, 123, 255), -1); } imshow("直方图", histImg); }
moravec角点、harris角点一、角
图像处理和与计算机视觉领域,兴趣点(interest points),或称作关键点(keypoints)、特征点(feature
points)
被大量用于解决物体识别,图像识别、图像匹配、视觉跟踪、三维重建等一系列的问题。我们不再观察整幅图,而是选择某些特殊的点,然后对他们进行局部有的放矢的分析。如果能检测到足够多的这种点,同时他们的区分度很高,并且可以精确定位稳定的特征,那么这个方法就有使用价值。 图像特征类型可以被分为如下三种: <1>边缘 <2>角点 (感兴趣关键点) <3>斑点(Blobs)(感兴趣区域)
其中,角点是个很特殊的存在。他们在图像中可以轻易地定位,同时,他们在人造物体场景,比如门、窗、桌等出随处可见。因为角点位于两条边缘的交点处,代表了两个边缘变化的方向上的点,,所以他们是可以精确定位的二维特征,甚至可以达到亚像素的精度。且其图像梯度有很高的变化,这种变化是可以用来帮助检测角点的。需要注意的是,角点与位于相同强度区域上的点不同,与物体轮廓上的点也不同,因为轮廓点难以在相同的其他物体上精确定位。 角点检测(Corner Detection)是计算机视觉系统中用来获得图像特征的一种方法,广泛应用于运动检测、图像匹配、视频跟踪、三维建模和目标识别等领域中。也称为特征点检测。
角点通常被定义为两条边的交点,更严格的说,角点的局部邻域应该具有两个不同区域的不同方向的边界。而实际应用中,大多数所谓的角点检测方法检测的是拥有特定特征的图像点,而不仅仅是“角点”。这些特征点在图像中有具体的坐标,并具有某些数学特征,如局部最大或最小灰度、某些梯度特征等。 现有的角点检测算法并不是都十分的健壮。很多方法都要求有大量的训练集和冗余数据来防止或减少错误特征的出现。另外,角点检测方法的一个很重要的评价标准是其对多幅图像中相同或相似特征的检测能力,并且能够应对光照变化、图像旋转等图像变化。 如果某一点在任意方向的一个微小变动都会引起灰度很大的变化,那么我们就把它称之为角点 首先我们来看三幅图片理解什么是角点: 我们在图片以某像素点为中心,取一窗口,当窗口向各个方向移动时,其内部灰度值变化不是很明显,则该点即处在平坦区域(如左边图);当其内部灰度值只在几个固定的方向上变化较为明显,那么该点则处在边缘区域(如图中间部分);当向各个方向移动,其变化都是很明显,则该点为角点(如图右)。
另外,关于角点的具体描述可以有几种: · 一阶导数(即灰度的梯度)的局部最大所对应的像素点; · 两条及两条以上边缘的交点; · 图像中梯度值和梯度方向的变化速率都很高的点; · 角点处的一阶导数最大,二阶导数为零,指示物体边缘变化不连续的方向。
二、moravvec角 Moravec 在1981年提出Moravec角点检测算子[1],并将它应用于立体匹配。
首先, 计算每个像素点的兴趣值, 即以该像素点为中心, 取一个w*w(如:5×5)的方形窗口,
计算0度、45度、90度、135度四个方向灰度差的平方和,
取其中的最小值作为该像素点的兴趣值.E就是像素的变化值。Moravec算子对四个方向进行加权求和来确定变化的大小,然和设定阈值,来确定到底是边还是角点。
图 以3×3为例 黑色窗口为I(x,y) 红色窗口为I(x+u,y+v) 其中四种移位 (u,v) = (1,0), (1,1), (0,1), (-1, 1).w(x,y)为方形二值窗口,若像素点在窗口内,则取值为1, 否则为0。 moravec角点检测步骤:
(1)对于每一个像素点,计算在E(u,v),在我们的算法中,(u,v)的取值是((1,0), (1,1),(0,1), (-1,
1).当然,你自己可以改成(1,0),(1,1),(0,1),(-1,1),(-1,0),(-1,-1),(0,-1),(1,-1)8种情况 (2)计算最小值对每个位置minValue = min{E(u,v)},其中(u,v) = (1,0),(1,1), (0,1), (-1, 1). (3)对每个位置minValue 进行判断,是不是大于设定阈值,如果是大于设定阈值,接着判断是不是局部极大,在判断角点的时候,必须判断每个方向的patch的变化。 moravec角点检测主要有两个缺: #include <opencv2/opencv.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> using namespace std; using namespace cv; // MoravecCorners角点检测 cv::Mat MoravecCorners(cv::Mat srcImage, int kSize, int threshold) { cv::Mat resMorMat = srcImage.clone(); int r = kSize / 2; const int nRows = srcImage.rows; const int nCols = srcImage.cols; int nConut = 0; CvPoint* pPoint = new CvPoint[nRows * nCols]; for (int i = r; i < srcImage.rows - r; i++) { for (int j = r; j < srcImage.cols - r; j++) { int wV1, wV2, wV3, wV4; wV1 = wV2 = wV3 = wV4 = 0; for (int k = -r; k < r; k++) wV1 += (srcImage.at<uchar>(i, j + k) - srcImage.at<uchar>(i, j + k + 1)) * (srcImage.at <uchar>(i, j + k) - srcImage.at<uchar>(i, j + k + 1)); for (int k = -r; k < r; k++) wV2 += (srcImage.at<uchar>(i + k, j) - srcImage.at<uchar>(i + k + 1, j)) * (srcImage.at <uchar>(i + k, j) - srcImage.at<uchar>(i + k + 1, j)); for (int k = -r; k < r; k++) wV3 += (srcImage.at<uchar>(i + k, j + k) - srcImage.at<uchar>(i + k + 1, j + k + 1)) * (srcImage.at <uchar>(i + k, j + k) - srcImage.at<uchar>(i + k + 1, j + k + 1)); for (int k = -r; k < r; k++) wV4 += (srcImage.at<uchar>(i + k, j - k) - srcImage.at<uchar>(i + k + 1, j - k - 1)) * (srcImage.at <uchar>(i + k, j - k) - srcImage.at<uchar>(i + k + 1, j - k - 1)); int value = min(min(wV1, wV2), min(wV3, wV4)); if (value > threshold) { pPoint[nConut] = cvPoint(j, i); nConut++; } } } for (int i = 0; i < nConut; i++) cv::circle(resMorMat, pPoint[i], 5, cv::Scalar(255, 0, 0)); return resMorMat; }
int main() { cv::Mat srcImage = imread("lena.jpg", 0); if (!srcImage.data) return -1; cv::Mat resMorMat = MoravecCorners(srcImage, 5, 10000); cv::imshow("srcImage", srcImage); cv::imshow("resMorMat", resMorMat); cv::waitKey(0); return 0; }
三、harris角点检测 在harris的角点检测中,使用的是高斯窗口,所以w(x,y)表示的是高斯窗口中的权重。此时 当u和v取两组相互垂直的值时,E(u,v)都有较大值的点。 <1>计算图像I(x,y)在x和y两个方向的梯度Ix,Iy
结果解释: 角点:最直观的印象就是在水平、竖直两个方向上变化均较大的点,即Ix、Iy都较大 边缘:仅在水平、或者仅在竖直方向有较大的变化量,即Ix和Iy只有其一较大 平坦地区:在水平、竖直方向的变化量均较小,即Ix、Iy都较小
或者说,r1 r2是特征值 r1,r2都很小,对应于图像中的平滑区域 r1,r2都很大,对应于图像中的角点 r1,r2一个很大,一个很小,对应于图像中的边缘
Harris角点检测最直观的解释是:在任意两个相互垂直的方向上,都有较大变化的点。
在moravec角点检测中,w(x,y)的取值是二元的,在窗口内部就取值为1,在窗口外部就取值为0,在harris的角点检测中,使用的是高斯窗口,所以w(x,y)表示的是高斯窗口中的权重。此时
当u和v取两组相互垂直的值时,E(u,v)都有较大值的。 Harris角点检测算法有诸多优点: 当然Harris也有许多不完善的地方:它对尺度很敏感,不具备几何尺度不变性。
cornerHarris 函数用于在OpenCV中运行Harris角点检测算子处理图像。和cornerMinEigenVal(
)以及cornerEigenValsAndVecs( )函数类似,cornerHarris
函数对于每一个像素(x,y)在邻域内,计算2x2梯度的协方差矩阵,接着它计算如下式子: 即可以找出输出图中的局部最大值,即找出了角点。 void cornerHarris(InputArraysrc,OutputArray dst, int blockSize, int ksize, double k,intborderType=BORDER_DEFAULT ) 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位或者浮点型图像。 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,即这个参数用于存放Harris角点检测的输出结果,和源图片有一样的尺寸和类型。 第三个参数,int类型的blockSize,表示邻域的大小,更多的详细信息在cornerEigenValsAndVecs()中有讲到。 第四个参数,int类型的ksize,表示Sobel()算子的孔径大小。 第五个参数,double类型的k,Harris参数。 第六个参数,int类型的borderType,图像像素的边界模式,注意它有默认值BORDER_DEFAULT。更详细的解释,参考borderInterpolate( )函数。</span>
#include <opencv2/opencv.hpp> #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" using namespace cv; using namespace std; #define WINDOW_NAME1 "【程序窗口1】" #define WINDOW_NAME2 "【程序窗口2】" Mat g_srcImage, g_srcImage1, g_grayImage; int thresh = 30; int max_thresh = 175; void on_CornerHarris(int, void*);//回调函数 int main(int argc, char** argv) { g_srcImage = imread("1.jpg", 1); if (!g_srcImage.data) { printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n"); return false; } imshow("原始图", g_srcImage); g_srcImage1 = g_srcImage.clone(); cvtColor(g_srcImage1, g_grayImage, CV_BGR2GRAY); namedWindow(WINDOW_NAME1, CV_WINDOW_AUTOSIZE); createTrackbar("阈值: ", WINDOW_NAME1, &thresh, max_thresh, on_CornerHarris); on_CornerHarris(0, 0); waitKey(0); return(0); }
void on_CornerHarris(int, void*) {
Mat dstImage; Mat normImage; Mat scaledImage; dstImage = Mat::zeros(g_srcImage.size(), CV_32FC1); g_srcImage1 = g_srcImage.clone(); cornerHarris(g_grayImage, dstImage, 2, 3, 0.04, BORDER_DEFAULT); normalize(dstImage, normImage, 0, 255, NORM_MINMAX, CV_32FC1, Mat()); convertScaleAbs(normImage, scaledImage); for (int j = 0; j < normImage.rows; j++) { for (int i = 0; i < normImage.cols; i++) { if ((int)normImage.at<float>(j, i) > thresh + 80) { circle(g_srcImage1, Point(i, j), 5, Scalar(10, 10, 255), 2, 8, 0); circle(scaledImage, Point(i, j), 5, Scalar(0, 10, 255), 2, 8, 0); } } } imshow(WINDOW_NAME1, g_srcImage1); imshow(WINDOW_NAME2, scaledImage);
}
四、知识补充 关于矩阵知识的一点补充:好长时间没看过线性代数的话,这一段比较难理解。可以看到M是实对称矩阵,这里简单温习一下实对称矩阵和二次型的一些知识点吧。 1. 关于特征值和特征向量: 特征值的特征向量的概念忘了就自己查吧,这里只说关键的。对于实对称矩阵M(设阶数为n),则一定有n个实特征值,每个特征值对应一组特征向量(这组向量中所有向量共线),不同特征值对应的特征向量间相互正交;(注意这里说的是实对称矩阵,不是所有的矩阵都满足这些条件) 2. 关于对角化: 对角化是指存在一个正交矩阵Q,使得
Q’MQ
能成为一个对角阵(只有对角元素非0),其中Q’是Q的转置(同时也是Q的逆,因为正交矩阵的转置就是其逆)。一个矩阵对角化后得到新矩阵的行列式和矩阵的迹(对角元素之和)均与原矩阵相同。如果M是n阶实对称矩阵,则Q中的第
j 列就是第 j 个特征值对应的一个特征向量(不同列的特征向量两两正交)。 3. 关于二次型: 对于一个n元二次多项式,f(x1,x2....xn)= ∑ ( aij*xi*xj ) ,其中 i 和 j 的求和区间均为 [1,n] , 可将其各次的系数 aij 写成一个n*n矩阵M,由于 aij 和 aji 的对称等价关系,一般将 aij 和 aji 设为一样的值,均为xi*xj 的系数的二分之一。这样,矩阵M就是实对称矩阵了。即二次型的矩阵默认都是实对称矩阵 4. 关于二次型的标准化(正交变换法): 二次型的标准化是指通过构造一个n阶可逆矩阵
C,使得向量 ( x1,x2...xn ) = C * (y1,y2...yn),把n维向量 x 变换成n维向量 y
,并代入f(x1,x2....xn) 后得到 g(y1,y2...yn),而后者的表达式中的二次项中不包含任何交叉二次项
yi*yj(全部都是平方项 yi^2),也即表达式g的二次型矩阵N是对角阵。用公式表示一下 f 和 g ,(下面的表达式中 x 和
y都代表向量,x' 和 y' 代表转置) f = x' * M * x ; g = f = x' * M * x = (Cy)' * M * (Cy) = y'* (C'MC) * y = y' * N * y ; 因此 C'MC = N。正交变换法,就是直接将M对角化得到N,而N中对角线的元素就是M的特征值。正交变换法中得到的 C 正好是一个正交矩阵,其每一列都是两两正交的单位向量,因此 C 的作用仅仅是将坐标轴旋转(不会有放缩)。 http://blog.csdn.net/newthinker_wei/article/details/45603583 http://www.360doc.com/content/15/1212/23/20007814_519967668.shtml http://blog.csdn.net/crzy_sparrow/article/details/7391511 http://blog.csdn.net/lu597203933/article/details/15088485 http://blog.csdn.net/poem_qianmo/article/details/29356187
SIFI一、理论知识 Scale Invariant Feature Transform,尺度不变特征变换匹配算法,对于算法的理论介绍,可以参考这篇文章http://blog.csdn.net/qq_20823641/article/details/51692415,里面很详细,可以更好的学习。这里就不多介绍。后面就挑选重点的来说 二、SIFT 主要思想 SIFT算法是一种提取局部特征的算法,在尺度空间寻找极值点,提取位置,尺度,旋转不变量。 三、SIFT算法的主要特点: a) SIFT特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换、噪声也保持一定程度的稳定性。 b) 独特性(Distinctiveness)好,信息量丰富,适用于在海量特征数据库中进行快速、准确的匹配[23]。 c) 多量性,即使少数的几个物体也可以产生大量SIFT特征向量。 d) 高速性,经优化的SIFT匹配算法甚至可以达到实时的要求。 e) 可扩展性,可以很方便的与其他形式的特征向量进行联合。
四、SIFT算法步骤: 1)检测尺度空间极值点 2)精确定位极值点 3)为每个关键点指定方向参数 4)关键点描述子的生成
五、程序过程 使用SiftFeatureDetector的detect方法检测特征存入一个向量里,并使用drawKeypoints在图中标识出来 SiftDescriptorExtractor 的compute方法提取特征描述符,特征描述符是一个矩阵 使用匹配器matcher对描述符进行匹配,匹配结果保存由DMatch的组成的向量里 设置距离阈值,使得匹配的向量距离小于最小距离的2被才能进入最终的结果,用DrawMatch可以显示
六、函数简介 SIFT::SIFT(int nfeatures=0, int nOctaveLayers=3, double contrastThreshold=0.04, double edgeThreshold= 10, double sigma=1.6)
nfeatures:特征点数目(算法对检测出的特征点排名,返回最好的nfeatures个特征点)。 nOctaveLayers:金字塔中每组的层数(算法中会自己计算这个值,后面会介绍)。 contrastThreshold:过滤掉较差的特征点的对阈值。contrastThreshold越大,返回的特征点越少。 edgeThreshold:过滤掉边缘效应的阈值。edgeThreshold越大,特征点越多(被多滤掉的越少)。 sigma:金字塔第0层图像高斯滤波系数,也就是σ。
void SIFT::operator()(InputArray img, InputArray mask, vector<KeyPoint>& keypoints, OutputArray descriptors, bool useProvidedKeypoints=false)
class keyPoint { Point2f pt; float size; float angle; float response; int octave; int class_id; } void drawMatches(const Mat & img1, const vector<KeyPoint> & keypoints1, const Mat & img2, const vector<KeyPoint> & keypoints2, const vector<DMatch> & matches1to2, Mat & outImg, const Scalar & matchColor = Scalar::all(-1), const Scalar & singlePointColor = Scalar::all(-1), const vector<char> & matchesMask = vector<char>(), intflags = DrawMatchesFlags::DEFAULT) Parameters: | img1 – 源图像1 keypoints1 –源图像1的特征点. img2 – 源图像2. keypoints2 – 源图像2的特征点 matches1to2 – 源图像1的特征点匹配源图像2的特征点[matches[i]] . outImg – 输出图像具体由flags决定. matchColor – 匹配的颜色(特征点和连线),若matchColor==Scalar::all(-1),颜色随机. singlePointColor – 单个点的颜色,即未配对的特征点,若matchColor==Scalar::all(-1),颜色随机. matchesMask – Mask决定哪些点将被画出,若为空,则画出所有匹配点. flags – Fdefined by DrawMatchesFlags.
|
---|
七、函数注意事项1.生成一个SiftFeatureDetector的对象,这个对象顾名思义就是SIFT特征的探测器,用它来探测衣服图片中SIFT点的特征,存到一个KeyPoint类型的vector中,keypoint只是保存了opencv的sift库检测到的特征点的一些基本信息,但sift所提取出来的特征向量其实不是在这个里面,特征向量通过SiftDescriptorExtractor
提取,结果放在一个Mat的数据结构中。这个数据结构才真正保存了该特征点所对应的特征向量。 2.keypoint只是达到了关键点的位置,方向等信息,并无该特征点的特征向量,要想提取得到特征向量就还要进行SiftDescriptorExtractor
的工作,建立了SiftDescriptorExtractor
对象后,通过该对象,对之前SIFT产生的特征点进行遍历,找到该特征点所对应的128维特征向量 八、示例
#include <opencv2/opencv.hpp> #include <opencv2/features2d/features2d.hpp> #include<opencv2/nonfree/nonfree.hpp> #include<opencv2/legacy/legacy.hpp> #include<vector> using namespace std; using namespace cv; int main(int argc, uchar* argv[]) { const char* imagename = "hand1.jpg"; //从文件中读入图像 Mat img = imread(imagename); Mat img2 = imread("hand3.jpg"); //如果读入图像失败 if (img.empty()) { fprintf(stderr, "Can not load image %s\n", imagename); return -1; } if (img2.empty()) { fprintf(stderr, "Can not load image %s\n", imagename); return -1; } //显示图像 imshow("image before", img); imshow("image2 before", img2); //sift特征检测 SiftFeatureDetector siftdtc; vector<KeyPoint>kp1, kp2; siftdtc.detect(img, kp1); Mat outimg1; drawKeypoints(img, kp1, outimg1); imshow("image1 keypoints", outimg1); KeyPoint kp; siftdtc.detect(img2, kp2); Mat outimg2; drawKeypoints(img2, kp2, outimg2); imshow("image2 keypoints", outimg2); SiftDescriptorExtractor extractor; Mat descriptor1, descriptor2; BruteForceMatcher<L2<float>> matcher; vector<DMatch> matches; Mat img_matches; extractor.compute(img, kp1, descriptor1); extractor.compute(img2, kp2, descriptor2); matcher.match(descriptor1, descriptor2, matches); drawMatches(img, kp1, img2, kp2, matches, img_matches); imshow("matches", img_matches); //此函数等待按键,按键盘任意键就返回 waitKey(); return 0; }
九、matlab Demo Software: SIFT Keypoint Detector 代码参考大牛的,网址如下http://www.cs./~lowe/keypoints/i1=imread('hand1.jpg'); i2=imread('hand3.jpg'); i11=rgb2gray(i1); i22=rgb2gray(i2); imwrite(i11,'v1.jpg','quality',80); imwrite(i22,'v2.jpg','quality',80); match('v1.jpg','v2.jpg');
SURF SIFT在前面已经说过了,可以说在实现过程中是精益求精,用了各种手段来删除不符合条件的特征点,同时也得到了很好的效果但是实时性不高,于是就有了SURF(speeded up robusr features).SURF是一种尺度,旋转不变的detector和descriptor.最大的特点是快!在快的基础上保证性能(repeatability,distinctiveness和robustness) 1、总体概括 首先,先对SURF算法的中特征点的提取
在SURF算法中,特征点的判据为某像素亮度的Hessian矩阵的行列式(Dxx*Dyy-Dxy*Dxy)为一个极值。由于Hessian矩阵的计算需要用到偏导数的计算,这一般通过像素点亮度值与高斯核的某一方向偏导数卷积而成;在SURF算法里,为提高算法运行速度,在精度影响很小的情况下,用近似的盒状滤波器(0,1,1组成的box
filter)代替高斯核。因为滤波器仅有0,-1,1,因此卷积的计算可以用积分图像(Integral
image)来优化(O(1)的时间复杂度),大大提高了效率。每个点需计算Dxx,Dyy,Dxy三个值,故需要三个滤波器;用它们滤波后,得到一幅图像的响应图(Response
image,其中每个像素的值为原图像素的Dxx*Dyy-Dxy*Dxy)。对图像用不同尺寸的滤波器进行滤波,得到同一图像在不同尺度的一系列响应图,构成一个金字塔(该金字塔无需像SIFT中的高斯一样进行降采样,即金字塔每组中的每层图像分辨率相同)。 特征点的检测与SIFT一致,即若某点的Dxx*Dyy-Dxy*Dxy大于其邻域的26个点(与SIFT一致)的Dxx*Dyy-Dxy*Dxy,则该点为特征点。特征点的亚像素精确定位与SIFT一致。 其次,描述子的建立。为保证特征点描述子的旋转不变性,需对每个特征点计算主方向。计算主方向的过程如下: 1. 统计以特征点为中心,正比于特征点尺度的某个数位半径,张角为60°的扇形区域内所有像素点的
sumX=(y方向小波变换响应)*(高斯函数),sumY=(x方向小波变换响应)*(高斯函数),计算合成向量角度θ=arctan(sumY/sumX),模长sqrt(sumy*sumy+sumx*sumx)。 2. 将扇形沿逆时针旋转(一般取步长为0.1个弧度),以同样方法计算合成向量。 3. 求出各方向扇形的合成向量模长最大值,其对应的角度即特征点主方向。 描述子的建立过程如下: 1. 选定以特征点为中心的一块正方形区域,将其旋转与主方向对齐。 2. 将正方形分为4x4的16个子区域,对每个区域进行Haar 小波变换(同样用积分图像加速),得到4个系数。 3. 由上述两步,生成4x4x4=64维向量,即描述子,用它可以进行匹配等工作。
算法的优点在于大量合理使用积分图像降低运输量,而且在运用的过程中并未降低精度(小波变换,Hessian矩阵行列式检测都是成熟有效的手段)。在时间上,SURF运行速度大约为SIFT的3倍;在质量上,SURF的鲁棒性很好,特征点识别率较SIFT高,在视角、光照、尺度变化等情形下,大体上都优于SIFT。 2、分开来说一、积分图 SURF是对积分图像进行操作,从而实现了加速,采用盒子滤波器计算每个像素点的Hessian矩阵行列式时,只需要几次加减法运算,而且运算量与盒子滤波器大小无关,所以能够快速的构成出SURF的尺度金字塔。 积分图像中每个像元的值,是原图像上对应位置的左上角所有元素之和
二、尺度空间的构造 2.1 DOH近似 SIFT算法建立一幅图像的金字塔,在每一层进行高斯滤波并求取图像差(DOG)进行特征点的提取,而SURF则用的是Hessian
Matrix进行特征点的提取,所以黑森矩阵是SURF算法的核心。假设函数f(x,y),Hessian矩阵H是由函数偏导数组成。首先来看看图像中某个像素点的HessianMatrix的定义为:
判别式的值是H矩阵的特征值,可以利用判定结果的符号将所有点分类,根据判别式取值正负,从来判别该点是或不是极点的值。在SURF算法中,通常用图像像素I(x,y)取代函数值f(x,y)。然后选用二阶标准高斯函数作为滤波器。通过特定核间的卷积计算二阶偏导数,这样便能计算出H矩阵的三个矩阵元素Lxx,Lxy,
Lyy,从而计算出H矩阵公式如下
2.2 构造尺度空间 算法的尺度不变性主要靠不同尺度下寻找感兴趣点。谈到不同尺度就不得不说'金字塔’。Lowe在其SIFT大作中是这样构造尺度空间的:对原图像不断地进行Gauss平滑+降采样。得到金字塔图像后,有进一步得到了DoG图,边和斑状结构就是通过DoG图得到其在原图的位置。 SURF中的做法与SIFT是有所不同的。SIFT算法在构造金字塔图层时Gauss滤波器大小不变,改变的是图像的大小;而SURF则恰恰相反:图像大小保持不变,改变的是滤波器的大小。
SURF首先采用9×9的盒子滤波器(近似等于σ=1.2时的高斯二阶微分,记为尺度s=1.2)得到的响应图像作为最底层的图像,然后逐渐增大盒子的尺寸,对原图像继续进行滤波处理。与SIFT类似,把响应图像分成若干组,每组若干层。每组都是采用逐渐增大的滤波器尺寸进行处理。层与层之间的尺度变化量是高斯二阶微分模板决定的。对于9×9的滤波器,由于要保证滤波器的结构比例不变同时要求存在滤波器模板中心,每个块最小增加量是2,由于有三个块,所以最小增加量是6,即下一个滤波器的大小为15×15,依次增加为21×21,27×27,...;利用这样的模板序列,就构造出尺度空间。
每一个octave中的filter的size可以表示如下
三、关键 3.1非极大值点位 与SIFT类似,对每层图像上的每个像素与空间邻域内和尺度邻域内的响应值比较(不包括第一层与最后一层图像),同层上有8个邻域像元,向量尺度空间共有2×9=18个,共计26个像元的值进行比较,如果是极大值则保留下来,作为候选特征点。
同时如果特征点的响应值小于Hessia行列式的阈值,也被排除。 3.2删除不符合条件的极值点然后,采用3维线性插值法得到亚像素级的特征点,同时也去掉那些值小于一定阈值的点,增加极值使检测到的特征点数量减少,最终只有几个特征最强点会被检测出来,参考SIFT的方法
四,特征点描述子 4.1主方向 为了保证旋转不变性,在SURF中,不统计其梯度直方图,而是统计特征点领域内的Harr小波特征。即以特征点为中心,计算半径为6s(S为特征点所在的尺度值)的邻域内,统计60度扇形内所有点在x(水平)和y(垂直)方向的Haar小波响应总和(Haar小波边长取4s)
并给这些响应值赋高斯权重系数(σ=2.5s),使得靠近特征点的响应贡献大,而远离特征点的响应贡献小,然后60度范围内的响应相加以形成新的矢量,遍历整个圆形区域,选择最长矢量的方向为该特征点的主方向。这样,通过特征点逐个进行计算,得到每一个特征点的主方向。该过程的示意图如下:
4.2形成特征矢量 在SURF中,也是在特征点周围取一个正方形框,框的边长为20s(s是所检测到该特征点所在的尺度)。该框带方向,方向当然就是第4步检测出来的主方向了。然后把该框分为16个子区域,每个子区域统计25个像素的水平方向和垂直方向的haar小波特征,这里的x(水平)和y(垂直)方向都是相对主方向而言的。该haar小波特征为x(水平)方向值之和,水平方向绝对值之和,垂直方向之和,垂直方向绝对值之和。该过程的示意图如下所示:
这样每个子区域携带4个信息,共有16个子区域,共64维。最后为了防止光照与对比度的影响,对特征矢量归一化处理。 五、Opencv 函数的定义看SIFT基本相同,只是SIFT换了SURF
程序的核心思想是:
使用 DescriptorExtractor 接口来寻找关键点对应的特征向量。 使用 SurfDescriptorExtractor 以及它的函数 compute 来完成特定的计算。 使用 BruteForceMatcher 来匹配特征向量。 使用函数 drawMatches 来绘制检测到的匹配点。
#include "opencv2/core/core.hpp" #include "opencv2/features2d/features2d.hpp" #include "opencv2/highgui/highgui.hpp" #include <opencv2/nonfree/nonfree.hpp> #include<opencv2/legacy/legacy.hpp> #include <iostream> using namespace cv; using namespace std;
int main() {
Mat srcImage1 = imread("hand1.jpg", 1); Mat srcImage2 = imread("hand3.jpg", 1); if (!srcImage1.data || !srcImage2.data) { printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n"); return false; } int minHessian = 700; SurfFeatureDetector detector(minHessian); std::vector<KeyPoint> keyPoint1, keyPoints2; detector.detect(srcImage1, keyPoint1); detector.detect(srcImage2, keyPoints2); SurfDescriptorExtractor extractor; Mat descriptors1, descriptors2; extractor.compute(srcImage1, keyPoint1, descriptors1); extractor.compute(srcImage2, keyPoints2, descriptors2);
BruteForceMatcher< L2<float> > matcher; std::vector< DMatch > matches; matcher.match(descriptors1, descriptors2, matches);
Mat imgMatches; drawMatches(srcImage1, keyPoint1, srcImage2, keyPoints2, matches, imgMatches);//进行绘制 imshow("匹配图", imgMatches);
waitKey(0); return 0; }
六、Matlab 程序源码由荷兰特温特大学的Dirk-Jan Kroon博士提供,可以自己去下载,比较复杂,这里提供一下结果
I1=imread('TestImages/testc1.png'); I2=imread('TestImages/testc2.png'); Options.upright=true; Options.tresh=0.0001; Ipts1=OpenSurf(I1,Options); Ipts2=OpenSurf(I2,Options); D1 = reshape([Ipts1.descriptor],64,[]); D2 = reshape([Ipts2.descriptor],64,[]); err=zeros(1,length(Ipts1)); cor1=1:length(Ipts1); cor2=zeros(1,length(Ipts1)); for i=1:length(Ipts1), distance=sum((D2-repmat(D1(:,i),[1 length(Ipts2)])).^2,1); [err(i),cor2(i)]=min(distance); end [err, ind]=sort(err); cor1=cor1(ind); cor2=cor2(ind); I = zeros([size(I1,1) size(I1,2)*2 size(I1,3)]); I(:,1:size(I1,2),:)=I1; I(:,size(I1,2)+1:size(I1,2)+size(I2,2),:)=I2; figure, imshow(I/255); hold on; for i=1:30, c=rand(1,3); plot([Ipts1(cor1(i)).x Ipts2(cor2(i)).x+size(I1,2)],[Ipts1(cor1(i)).y Ipts2(cor2(i)).y],'-','Color',c) plot([Ipts1(cor1(i)).x Ipts2(cor2(i)).x+size(I1,2)],[Ipts1(cor1(i)).y Ipts2(cor2(i)).y],'o','Color',c) end
http://blog.csdn.net/luoshixian099/article/details/47807103
http://blog.csdn.net/cxp2205455256/article/details/41311013
http://www./lib/view/open1440832074794.html
http://blog.csdn.net/poem_qianmo/article/details/33320997
DFT离散傅里叶变换 这篇就是图像的时域到频域的开始,也是信号处理中比较常见的傅立叶变换。 一、傅立叶图像
对一张图像使用傅立叶变换就是将它分解成正弦和余弦两部分。也就是将图像从空间域(spatial domain)转换到频域(frequency
domain)。 这一转换的理论基础来自于以下事实:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅立叶变换就是一个用来将函数分解的工具。
2维图像的傅立叶变换可以用以下数学公式表达: 式中
f 是空间域(spatial domain)值, F 则是频域(frequency domain)值。 转换之后的频域值是复数,
因此,显示傅立叶变换之后的结果需要使用实数图像(real image) 加虚数图像(complex image),
或者幅度图像(magitude image)加相位图像(phase image)。
在实际的图像处理过程中,仅仅使用了幅度图像,因为幅度图像包含了原图像的几乎所有我们需要的几何信息。
然而,如果你想通过修改幅度图像或者相位图像的方法来间接修改原空间图像,你需要使用逆傅立叶变换得到修改后的空间图像,这样你就必须同时保留幅度图像和相位图像了。 在此示例中,我将展示如何计算以及显示傅立叶变换后的幅度图像。由于数字图像的离散性,像素值的取值范围也是有限的。比如在一张灰度图像中,像素灰度值一般在0到255之间。 因此,我们这里讨论的也仅仅是离散傅立叶变换(DFT)。 如果你需要得到图像中的几何结构信息,那你就要用到它了。请参考以下步骤(假设输入图像为单通道的灰度图像 I) 二、函数 int getOptimalDFTSize(int vecsize) 该函数是为了获得进行DFT计算的最佳尺寸。因为在进行DFT时,如果需要被计算的数字序列长度vecsize为2的n次幂的话,那么其运行速度是非常快的。如果不是2的n次幂,但能够分解成2,3,5的乘积,则运算速度也非常快。这里的getOptimalDFTSize()函数就是为了获得满足分解成2,3,5的最小整数尺寸。很显然,如果是多维矩阵需要进行DFT,则每一维单独用这个函数获得最佳DFT尺寸。 void
copyMakeBorder(InuptArray src, OutputArray dst, int top , int bottom,
int left, int right, int borderType, const Scalar& value=Scalar()) 该函数是用来扩展一个图像的边界的,第3~6个参数分别为原始图像的上下左右各扩展的像素点的个数,第7个参数表示边界的类型,如果其为BORDER_CONSTANT,则扩充的边界像素值则用第8个参数来初始化。将src图像扩充边界后的结果保存在dst图像中。 merge()函数是把多个但通道数组连接成1个多通道数组,而split()函数则相反,把1个多通道函数分解成多个但通道函数。 Void magnitude(InputArray x, InputArray y, OutPutArray magnitude) 该函数是计算输入矩阵x和y对应该的每个像素平方求和后开根号保存在输出矩阵magnitude中。 函数log(InputArray src, OutputArray dst)是对输入矩阵src中每个像素点求log,保存在输出矩阵dst的相对应的位置上。 三、过程描述将图像延扩到最佳尺寸. 离散傅立叶变换的运行速度与图片的尺寸息息相关。当图像的尺寸是2, 3,5的整数倍时,计算速度最快。 因此,为了达到快速计算的目的,经常通过添凑新的边缘像素的方法获取最佳图像尺寸。函数 getOptimalDFTSize() 返回最佳尺寸,而函数copyMakeBorder() 填充边缘像素: Mat padded; //将输入图像延扩到最佳的尺寸 int m = getOptimalDFTSize( I.rows ); int n = getOptimalDFTSize( I.cols ); // 在边缘添加0 copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0)); 添加的像素初始化为0. 为傅立叶变换的结果(实部和虚部)分配存储空间. 傅立叶变换的结果是复数,这就是说对于每个原图像值,结果是两个图像值。 此外,频域值范围远远超过空间值范围, 因此至少要将频域储存在 float 格式中。 结果我们将输入图像转换成浮点类型,并多加一个额外通道来储存复数部分: Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)}; Mat complexI;merge(planes, 2, complexI); // 为延扩后的图像增添一个初始化为0的通道 进行离散傅立叶变换. 支持图像原地计算 (输入输出为同一图像): dft(complexI, complexI); // 变换结果很好的保存在原始矩阵中 将复数转换为幅度.复数包含实数部分(Re)和复数部分 (imaginary - Im)。 离散傅立叶变换的结果是复数,对应的幅度可以表示为:
转化为OpenCV代码:
split(complexI, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I)) magnitude(planes[0], planes[1], planes[0]); // planes[0] = magnitude Mat magI = planes[0];
对数尺度(logarithmic scale)缩放. 傅立叶变换的幅度值范围大到不适合在屏幕上显示。高值在屏幕上显示为白点,而低值为黑点,高低值的变化无法有效分辨。为了在屏幕上凸显出高低变化的连续性,我们可以用对数尺度来替换线性尺度: 转化为OpenCV代码: magI += Scalar::all(1); // 转换到对数尺度 log(magI, magI); 剪切和重分布幅度图象限.
还记得我们在第一步时延扩了图像吗?
那现在是时候将新添加的像素剔除了。为了方便显示,我们也可以重新分布幅度图象限位置(注:将第五步得到的幅度图从中间划开得到四张1/4子图像,将每张子图像看成幅度图的一个象限,重新分布即将四个角点重叠到图片中心)。
这样的话原点(0,0)就位移到图像中心。 int cx = magI.cols/2; int cy = magI.rows/2;
Mat q0(magI, Rect(0, 0, cx, cy)); // Top-Left - 为每一个象限创建ROI Mat q1(magI, Rect(cx, 0, cx, cy)); // Top-Right Mat q2(magI, Rect(0, cy, cx, cy)); // Bottom-Left Mat q3(magI, Rect(cx, cy, cx, cy)); // Bottom-Right
Mat tmp; // 交换象限 (Top-Left with Bottom-Right) q0.copyTo(tmp); q3.copyTo(q0); tmp.copyTo(q3);
q1.copyTo(tmp); // 交换象限 (Top-Right with Bottom-Left) q2.copyTo(q1);tmp.copyTo(q2) 7.归一化. 这一步的目的仍然是为了显示。 现在我们有了重分布后的幅度图,但是幅度值仍然超过可显示范围[0,1] 。我们使用 normalize()函数将幅度归一化到可显示范围。
normalize(magI, magI, 0, 1, CV_MINMAX); // 将float类型的矩阵转换到可显示图像范围 // (float [0, 1]). 四、整体代码#include "opencv2/core/core.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; int main() {
Mat srcImage = imread("lena.jpg", 0); if (!srcImage.data) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; } imshow("原始图像", srcImage); int m = getOptimalDFTSize(srcImage.rows); int n = getOptimalDFTSize(srcImage.cols); Mat padded; copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols, BORDER_CONSTANT, Scalar::all(0)); Mat planes[] = { Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F) }; Mat complexI; merge(planes, 2, complexI); dft(complexI, complexI); split(complexI, planes); magnitude(planes[0], planes[1], planes[0]); Mat magnitudeImage = planes[0]; magnitudeImage += Scalar::all(1); log(magnitudeImage, magnitudeImage);//求自然对数 magnitudeImage = magnitudeImage(Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2)); int cx = magnitudeImage.cols / 2; int cy = magnitudeImage.rows / 2; Mat q0(magnitudeImage, Rect(0, 0, cx, cy)); // ROI区域的左上 Mat q1(magnitudeImage, Rect(cx, 0, cx, cy)); // ROI区域的右上 Mat q2(magnitudeImage, Rect(0, cy, cx, cy)); // ROI区域的左下 Mat q3(magnitudeImage, Rect(cx, cy, cx, cy)); // ROI区域的右下 Mat tmp; q0.copyTo(tmp); q3.copyTo(q0); tmp.copyTo(q3); q1.copyTo(tmp); q2.copyTo(q1); tmp.copyTo(q2); normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX); imshow("频谱幅值", magnitudeImage); waitKey(); return 0; }
五、matlab I = rgb2gray(imread('d:\lena.jpg')); fcoef=fft2(double(I)); %FFT变换 tmp1 =log(1+abs(fcoef)); spectrum = fftshift(fcoef); %调整中心 tmp2 = log(1+abs(spectrum)); ifcoef = ifft2(fcoef); %逆变换 figure %显示处理结果 subplot(2,2,1), imshow(I), title('source image'); subplot(2,2,2), imshow(tmp1,[]), title('FFT image'); subplot(2,2,3), imshow(tmp2,[]), title('shift FFT image'); subplot(2,2,4), imshow(ifcoef,[]), title('IFFT image');
|