虽然Canny之类的边缘检测算法可以根据像素间的差异检测出轮廓边界的像素,但是它并没有将轮廓作为一个整体。下一步是要把这些边缘像素组装成轮廓。而对于这一步的实现,OpenCV自带了函数cvFindContours()和cvDrawContours(),这两个函数主要的作用就是检测轮廓,并把检测到的轮廓画出来。
下面先给出个完整的轮廓检测示例,然后我们再结合这个程序示例,解释相关的函数。
#include<opencv2/opencv.hpp>
using namespace std;
IplImage *g_pGrayImage = NULL;
const char *windows1 = "二值图";
const char *windows2 = "轮廓图";
CvSeq *g_pcvSeq = NULL;
//进度条回调函数
void on_trackbar(int pos)
{
IplImage *pBinaryImage = cvCreateImage(cvGetSize(g_pGrayImage), IPL_DEPTH_8U, 1);
cvThreshold(g_pGrayImage, pBinaryImage, pos, 255, CV_THRESH_BINARY);//转化为二值化图像
cvShowImage(windows1, pBinaryImage);//显示二值化图像
CvMemStorage *cvMStorage = cvCreateMemStorage();//创建内存存储器
cvFindContours(pBinaryImage, cvMStorage, &g_pcvSeq);//检测轮廓并返回检测到的轮廓的个数
IplImage *pOutlineImage = cvCreateImage(cvGetSize(g_pGrayImage), IPL_DEPTH_8U, 3);
int levels = 5;
cvZero(pOutlineImage);
//画出检测到的轮廓
cvDrawContours(pOutlineImage, g_pcvSeq, CV_RGB(255, 0, 0), CV_RGB(0, 255, 0), levels);
cvShowImage(windows2, pOutlineImage);//显示轮廓
cvReleaseMemStorage(&cvMStorage);
cvReleaseImage(&pBinaryImage);
cvReleaseImage(&pOutlineImage);
}
int main(int argc, char **argv)
{
const char *windowsSrcTitle = "原图";
const char *BarName = "二值化";
IplImage *pSrcImage = cvLoadImage("3.jpg", CV_LOAD_IMAGE_UNCHANGED);//载入原图
cvNamedWindow(windowsSrcTitle, CV_WINDOW_AUTOSIZE);
cvShowImage(windowsSrcTitle, pSrcImage);//显示原图
g_pGrayImage = cvCreateImage(cvGetSize(pSrcImage), IPL_DEPTH_8U, 1);
cvCvtColor(pSrcImage, g_pGrayImage,CV_BGR2GRAY);//转化为灰度图
cvNamedWindow(windows1, CV_WINDOW_AUTOSIZE);
cvNamedWindow(windows2, CV_WINDOW_AUTOSIZE);
int mThreshold = 0;
//调用滑动条函数
cvCreateTrackbar(BarName, windows1, &mThreshold, 254, on_trackbar);
on_trackbar(1);
cvWaitKey(0);
cvDestroyWindow(windows1);
cvDestroyWindow(windows2);
cvDestroyWindow(windowsSrcTitle);
cvReleaseImage(&pSrcImage);
cvReleaseImage(&g_pGrayImage);
return 0;
}
这个示例程序的运行结果如下:
关于上面的程序示例,我们可以看出程序的大致思路:
输入预检测图像(一般来说是RGB图像)->RGB转化为灰度图->灰度图转化为二值图像->创建内存存储器(这个是轮廓检测函数的参数之一)->调用轮廓检测函数cvFindContours(),得到轮廓->调用画轮廓函数cvDrawContours(),画出轮廓->显示图像->例行的释放指针。
在这个程序中,我们使用了滑动条回调函数,这个函数非常简单,有兴趣的同学可以去查阅下OpenCV函数手册。下面我们重点介绍下cvFindContours()函数和cvDrawContours()函数。
int cvFindContours(
IplImage *img,//输入图像,图像必须是8位单通道的二值图像
CvMemStorage* storage,//内存存储器,cvFindContours()找到的轮廓记录在此内存里,这个存储空间的创立需要cvCreateMemStorage()分配
CvSeq** firstContour,//这个指针cvFindContours()函数可以自动分配
int headerSize=sizeof(CvContour),//存储轮廓链表的表头大小,当第六个参数传入CV_CHAIN_CODE时,要设置成sizeof(CvChain),其它情况统一设置成sizeof(CvContour)
//mode和method参数,它们分别指定计算的方法和如何计算
CvContourRetrievalMode mode=CV_RETR_LIST,
CvChainApproxMethod method=CV_CHAIN_APPROX_SIMPLE
);
mode是轮廓检测的模式,可以为以下几个选择:
CV_RETR_EXTERNAL:只检测最外面的轮廓
CV_RETR_LIST:检索所有的轮廓,并将其保存到一条链表当中;
CV_RETR_CCOMP:检索所有的轮廓,并将他们组织为两层:顶层是各部分的外部边界,第二层是空洞的边界;
CV_RETR_TREE:检索所有的轮廓,并重构嵌套轮廓的整个层次。
method是轮廓如何被近似,常用的取值如下:
CV_CHAIN_CODE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列)。
CV_CHAIN_APPROX_SIMPLE:压缩水平的、垂直的和斜的部分,也就是,函数只保留他们的终点部分。
method其实还可以有别的取值,具体的大家如果感兴趣的话,可以查阅相关函数手册。
介绍完cvFindContours()函数,下面来介绍下cvDrawContour()函数
void cvDrawContours(
CvArr* img,//表示输入图像,也就是要在这幅图像上绘制轮廓
CvSeq* contour,//表示指向轮廓链表的指针
CvScalar external_color,
CvScalar hole_color,//表示颜色,绘制时会根据轮廓的层次来交替使用这二种颜色
int max_level,//表示绘制轮廓的最大层数,如果是0,只绘制contour;如果是1,追加绘制和contour同层的所有轮廓;如果是2,追加绘制比contour低一层的轮廓,以此类推;如果值是负值,则函数并不绘制contour后的轮廓,但是将画出其子轮廓,一直到abs(max_level) - 1层。
int thickness=1,//表示轮廓线的宽度
int line_type=8,//表示轮廓线的线型
CvPoint offset=CvPoint(0,0)//表示偏移量,如果传入(10,20),那绘制将从图像的(10,20)处开始。
)
上面的示例程序中,我们可以看到cvFindContours()函数和cvDrawContours()函数是轮廓检测的主要函数,但是这里面也涉及到一些别的东西,如CvMemStorage,CvSeq。下面我针对这个简要的介绍下。
OpenCV使用内存存储器来同一管理各种动态对象的内存。内存存储器在底层被实现为一个有许多相同大小的内存块组成的双向链表,通过这种结构,OpenCV可以从内存存储器中快速的分配内存或将内存返回给内存存储器。OpenCV中基于内存存储器实现的函数,经常需要向内存存储器申请内存空间。内存存储器可以创建,释放和清空,分别为
CvMemStorage* cvCreateMemStorage(
int block_size=0
);
void cvReleaseMemStorage(
CvMemStorage** storage
);
void cvClearMemStorage(
CvMemStorage** storage
);
序列是内存存储器中可以存储的一种对象。序列是某种结构的链表。所以对序列的操作很多都是类似于数据结构中对链表的操作一样,比如,创建序列,删除序列,直接访问序列中的元素,复制和移动序列中的数据,插入和删除元素等。具体的大家可以去参阅下OpenCV这本书,在这里就不多介绍。