OpenCV_轮廓的查找、表达、绘制、特性及匹配
2011-10-14 14:49
351 查看
虽然Canny之类的边缘检测算法可以根据像素间的差异检测出轮廓边界的像素,但是它并没有将轮廓作为一个整体。下一步是要将这些边缘像素组装成轮廓。
轮廓是构成任何一个形状的边界或外形线。直方图对比和模板匹配根据色彩及色彩的分布来进行匹配,以下包括:轮廓的查找、表达方式、组织方式、绘制、特性、匹配。
首先回忆下几个结构体:
首先是图像本身的结构体:
typedef struct CvMat
{
int type; /* CvMat 标识 (CV_MAT_MAGIC_VAL), 元素类型和标记 */
int step; /* 以字节为单位的行数据长度*/
int* refcount; /* 数据引用计数 */
union
{
uchar* ptr;
short* s;
int* i;
float* fl;
double* db;
} data;
union
{
int rows;
int height;
};
union
{
int cols;
int width;
};
这个结构体是最基础的矩阵,而图像本身就是一个复杂的矩阵,所以图像是对这个结构体的继承:
typedef struct _IplImage
{
int nSize; /* IplImage大小 */
int ID; /* 版本 (=0)*/
int nChannels; /* 大多数OPENCV函数支持1,2,3 或 4 个通道 */
int alphaChannel; /* 被OpenCV忽略 */
int depth; /* 像素的位深度: IPL_DEPTH_8U, IPL_DEPTH_8S, IPL_DEPTH_16U,
IPL_DEPTH_16S, IPL_DEPTH_32S, IPL_DEPTH_32F and IPL_DEPTH_64F 可支持 */
char colorModel[4]; /* 被OpenCV忽略 */
char channelSeq[4]; /* 同上 */
int dataOrder; /* 0 - 交叉存取颜色通道, 1 - 分开的颜色通道.
cvCreateImage只能创建交叉存取图像 */
int origin; /* 0 - 顶—左结构,1 - 底—左结构 (Windows bitmaps 风格) */
int align; /* 图像行排列 (4 or 8). OpenCV 忽略它,使用 widthStep 代替 */
int width; /* 图像宽像素数 */
int height; /* 图像高像素数*/
struct _IplROI *roi;/* 图像感兴趣区域. 当该值非空只对该区域进行处理 */
struct _IplImage *maskROI; /* 在 OpenCV中必须置NULL */
void *imageId; /* 同上*/
struct _IplTileInfo *tileInfo; /*同上*/
int imageSize; /* 图像数据大小(在交叉存取格式下imageSize=image->height*image->widthStep),单位字节*/
char *imageData; /* 指向排列的图像数据 */
int widthStep; /* 排列的图像行大小,以字节为单位 */
int BorderMode[4]; /* 边际结束模式, 被OpenCV忽略 */
int BorderConst[4]; /* 同上 */
char *imageDataOrigin; /* 指针指向一个不同的图像数据结构(不是必须排列的),是为了纠正图像内存分配准备的 */
}IplImage;
值得注意的地方:首先是origin这个,当有些图像复制或者视频播放时候,由于原点坐标位置未定,很容造成图片倒置。这时就得用void cvFlip( const CvArr* src, CvArr* dst=NULL, int flip_mode=0)函数或者直接设定origin来改变坐标原点;widthstep就是CvMat的step;
构造方法:IplImage* cvCreateImage( CvSize size, int depth, int channels );
直方图结构:
typedef struct CvHistogram
{
int type;
CvArr* bins;
float thresh[CV_MAX_DIM][2]; /* 对于标准直方图,bins的值有左边界+右边界=2 */
float** thresh2; /* 对于非标准直方图 */
CvMatND mat; /* embedded matrix header for array histograms */
}CvHistogram;
因此,由于直方图的复杂性,得到一个图片的直方图的步骤就不是一个函数完成的:
1,分割图片通道
2,求出bins数量及范围
3,CvHistogram* cvCreateHist( int dims, int* sizes, int type,float** ranges=NULL, int uniform=1 );
创建直方图
4,void cvCalcHist( IplImage** image, CvHistogram* hist,int accumulate=0, const CvArr* mask=NULL );
计算直方图
下面开始轮廓的学习。
查找轮廓
首先是如何在图像中找到轮廓,可以利用OpenCV提供的方法cvFindContours()可以很方便的查找轮廓。
cvFindContours()方法从二值图像中寻找轮廓。因此此方法处理的图像可以是从cvCanny()函数得到的有边缘像素的图像,或者从cvThreshold()及cvAdaptiveThreshold()得到的图像,这时的边缘是正和负区域之间的边界。
既然在查找之前,我们需要将彩色图像转换成灰度图像,然后再将灰度图像转换成二值图像。代码如下所示:
轮廓的表达方式
使用上面的代码可以得到图像的默认轮廓,但是轮廓在电脑中是如何表达的呢?在OpenCv中提供了两类表达轮廓的方式:顶点的序列、Freeman链码。
首先介绍下内存存储器的概念,这是OpenCV在创建动态对象时存取内存的技术。
CvMemStorage* cvCreateMemStorage( int block_size=0 );//创建默认值大小的内存空间
void cvReleaseMemStorage( CvMemStorage** storage );//释放内存空间
void cvClearMemStorage( CvMemStorage* storage );//清空内存块,可以用于重复使用,将内存返还给存储器,而不是返回给系统
void *cvMemStorageAlloc(CvMemStorage *storage,size_t size);//开辟内存空间
序列
序列是内存存储器中可以存储的一种对象。序列是某种结构的链表。序列在内存中被实现为一个双端队列,因此序列可以实习快速的随机访问,以及快速删除顶端的元素,但是从中间删除元素则稍慢些。
序列结构:
CvSeq
可动态增长元素序列(OpenCV_1.0已发生改变,详见cxtypes.h) Growable sequence of elements
#define CV_SEQUENCE_FIELDS() /
int flags; /* micsellaneous flags */ /
int header_size; /* size of sequence header */ /
struct CvSeq* h_prev; /* previous sequence */ /
struct CvSeq* h_next; /* next sequence */ /
struct CvSeq* v_prev; /* 2nd previous sequence */ /
struct CvSeq* v_next; /* 2nd next sequence */ /
int total; /* total number of elements */ /
int elem_size;/* size of sequence element in bytes */ /
char* block_max;/* maximal bound of the last block */ /
char* ptr; /* current write pointer */ /
int delta_elems; /* how many elements allocated when the sequence grows (sequence granularity) */ /
CvMemStorage* storage; /* where the seq is stored */ /
CvSeqBlock* free_blocks; /* free blocks list */ /
CvSeqBlock* first; /* pointer to the first sequence block */
typedef struct CvSeq
{
CV_SEQUENCE_FIELDS()
} CvSeq;
相关操作就不重复列出(排序,查找,逆序,拆分,复制,读取,写入切片的复制,移除,插入,),可以查找相关文档。
1.顶点的序列
用多个顶点(或各点间的线段)来表达轮廓。假设要表达一个从(0,0)到(2,2)的矩形,
(1)如果用点来表示,那么依次存储的可能是:(0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,1);
(2)如果用点间的线段来表达轮廓,那么依次存储的可能是:(0,0),(2,0),(2,2),(0,2)。
2.Freeman链码
Freeman链码需要一个起点,以及从起点出发的一系列位移。每个位移有8个方向,从0~7分别指向从正北开始的8个方向。假设要用Freeman链码表达从(0,0)到(2,2)的矩形,可能的表示方法是:起点(0,0),方向链2,2,4,4,6,6,0,0。
轮廓之间的组织方式
在查找到轮廓之后,不同轮廓是怎么组织的呢?根据不同的选择,它们可能是:(1)列表;(2)双层结构;(3)树型结构。
从纵向上来看,列表只有一层,双层结构有一或者两层,树型结构可能有一层或者多层。
如果要遍历所有的轮廓,可以使用递归的方式。
轮廓的绘制
轮廓的绘制比较简单,用上面提到的方法取得轮廓的所有点,然后把这些点连接成一个多边形即可。
轮廓的一个例子为:OpenCV_轮廓例子
上例中检测出输入图像的轮廓,然后逐个绘制每个轮廓。下个例子为:
在输入图像上寻找并绘制轮廓
具体代码为:
轮廓的匹配源码
轮廓是构成任何一个形状的边界或外形线。直方图对比和模板匹配根据色彩及色彩的分布来进行匹配,以下包括:轮廓的查找、表达方式、组织方式、绘制、特性、匹配。
首先回忆下几个结构体:
首先是图像本身的结构体:
typedef struct CvMat
{
int type; /* CvMat 标识 (CV_MAT_MAGIC_VAL), 元素类型和标记 */
int step; /* 以字节为单位的行数据长度*/
int* refcount; /* 数据引用计数 */
union
{
uchar* ptr;
short* s;
int* i;
float* fl;
double* db;
} data;
union
{
int rows;
int height;
};
union
{
int cols;
int width;
};
这个结构体是最基础的矩阵,而图像本身就是一个复杂的矩阵,所以图像是对这个结构体的继承:
typedef struct _IplImage
{
int nSize; /* IplImage大小 */
int ID; /* 版本 (=0)*/
int nChannels; /* 大多数OPENCV函数支持1,2,3 或 4 个通道 */
int alphaChannel; /* 被OpenCV忽略 */
int depth; /* 像素的位深度: IPL_DEPTH_8U, IPL_DEPTH_8S, IPL_DEPTH_16U,
IPL_DEPTH_16S, IPL_DEPTH_32S, IPL_DEPTH_32F and IPL_DEPTH_64F 可支持 */
char colorModel[4]; /* 被OpenCV忽略 */
char channelSeq[4]; /* 同上 */
int dataOrder; /* 0 - 交叉存取颜色通道, 1 - 分开的颜色通道.
cvCreateImage只能创建交叉存取图像 */
int origin; /* 0 - 顶—左结构,1 - 底—左结构 (Windows bitmaps 风格) */
int align; /* 图像行排列 (4 or 8). OpenCV 忽略它,使用 widthStep 代替 */
int width; /* 图像宽像素数 */
int height; /* 图像高像素数*/
struct _IplROI *roi;/* 图像感兴趣区域. 当该值非空只对该区域进行处理 */
struct _IplImage *maskROI; /* 在 OpenCV中必须置NULL */
void *imageId; /* 同上*/
struct _IplTileInfo *tileInfo; /*同上*/
int imageSize; /* 图像数据大小(在交叉存取格式下imageSize=image->height*image->widthStep),单位字节*/
char *imageData; /* 指向排列的图像数据 */
int widthStep; /* 排列的图像行大小,以字节为单位 */
int BorderMode[4]; /* 边际结束模式, 被OpenCV忽略 */
int BorderConst[4]; /* 同上 */
char *imageDataOrigin; /* 指针指向一个不同的图像数据结构(不是必须排列的),是为了纠正图像内存分配准备的 */
}IplImage;
值得注意的地方:首先是origin这个,当有些图像复制或者视频播放时候,由于原点坐标位置未定,很容造成图片倒置。这时就得用void cvFlip( const CvArr* src, CvArr* dst=NULL, int flip_mode=0)函数或者直接设定origin来改变坐标原点;widthstep就是CvMat的step;
构造方法:IplImage* cvCreateImage( CvSize size, int depth, int channels );
直方图结构:
typedef struct CvHistogram
{
int type;
CvArr* bins;
float thresh[CV_MAX_DIM][2]; /* 对于标准直方图,bins的值有左边界+右边界=2 */
float** thresh2; /* 对于非标准直方图 */
CvMatND mat; /* embedded matrix header for array histograms */
}CvHistogram;
因此,由于直方图的复杂性,得到一个图片的直方图的步骤就不是一个函数完成的:
1,分割图片通道
2,求出bins数量及范围
3,CvHistogram* cvCreateHist( int dims, int* sizes, int type,float** ranges=NULL, int uniform=1 );
创建直方图
4,void cvCalcHist( IplImage** image, CvHistogram* hist,int accumulate=0, const CvArr* mask=NULL );
计算直方图
下面开始轮廓的学习。
查找轮廓
首先是如何在图像中找到轮廓,可以利用OpenCV提供的方法cvFindContours()可以很方便的查找轮廓。
cvFindContours()方法从二值图像中寻找轮廓。因此此方法处理的图像可以是从cvCanny()函数得到的有边缘像素的图像,或者从cvThreshold()及cvAdaptiveThreshold()得到的图像,这时的边缘是正和负区域之间的边界。
既然在查找之前,我们需要将彩色图像转换成灰度图像,然后再将灰度图像转换成二值图像。代码如下所示:
CvSeq *contours = 0; cvCvtColor(src,dst,CV_BGR2GRAY);//将源图像进行灰度化 cvThreshold(dst,dst,f_thresh,255,CV_THRESH_BINARY);//二值化阈值 虽然第一个参数是const,但仍可以更改dst cvFindContours(dst,f_storage,&contours); //查找轮廓 cvZero(dst);
轮廓的表达方式
使用上面的代码可以得到图像的默认轮廓,但是轮廓在电脑中是如何表达的呢?在OpenCv中提供了两类表达轮廓的方式:顶点的序列、Freeman链码。
首先介绍下内存存储器的概念,这是OpenCV在创建动态对象时存取内存的技术。
CvMemStorage* cvCreateMemStorage( int block_size=0 );//创建默认值大小的内存空间
void cvReleaseMemStorage( CvMemStorage** storage );//释放内存空间
void cvClearMemStorage( CvMemStorage* storage );//清空内存块,可以用于重复使用,将内存返还给存储器,而不是返回给系统
void *cvMemStorageAlloc(CvMemStorage *storage,size_t size);//开辟内存空间
序列
序列是内存存储器中可以存储的一种对象。序列是某种结构的链表。序列在内存中被实现为一个双端队列,因此序列可以实习快速的随机访问,以及快速删除顶端的元素,但是从中间删除元素则稍慢些。
序列结构:
CvSeq
可动态增长元素序列(OpenCV_1.0已发生改变,详见cxtypes.h) Growable sequence of elements
#define CV_SEQUENCE_FIELDS() /
int flags; /* micsellaneous flags */ /
int header_size; /* size of sequence header */ /
struct CvSeq* h_prev; /* previous sequence */ /
struct CvSeq* h_next; /* next sequence */ /
struct CvSeq* v_prev; /* 2nd previous sequence */ /
struct CvSeq* v_next; /* 2nd next sequence */ /
int total; /* total number of elements */ /
int elem_size;/* size of sequence element in bytes */ /
char* block_max;/* maximal bound of the last block */ /
char* ptr; /* current write pointer */ /
int delta_elems; /* how many elements allocated when the sequence grows (sequence granularity) */ /
CvMemStorage* storage; /* where the seq is stored */ /
CvSeqBlock* free_blocks; /* free blocks list */ /
CvSeqBlock* first; /* pointer to the first sequence block */
typedef struct CvSeq
{
CV_SEQUENCE_FIELDS()
} CvSeq;
相关操作就不重复列出(排序,查找,逆序,拆分,复制,读取,写入切片的复制,移除,插入,),可以查找相关文档。
1.顶点的序列
用多个顶点(或各点间的线段)来表达轮廓。假设要表达一个从(0,0)到(2,2)的矩形,
(1)如果用点来表示,那么依次存储的可能是:(0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,1);
(2)如果用点间的线段来表达轮廓,那么依次存储的可能是:(0,0),(2,0),(2,2),(0,2)。
2.Freeman链码
Freeman链码需要一个起点,以及从起点出发的一系列位移。每个位移有8个方向,从0~7分别指向从正北开始的8个方向。假设要用Freeman链码表达从(0,0)到(2,2)的矩形,可能的表示方法是:起点(0,0),方向链2,2,4,4,6,6,0,0。
轮廓之间的组织方式
在查找到轮廓之后,不同轮廓是怎么组织的呢?根据不同的选择,它们可能是:(1)列表;(2)双层结构;(3)树型结构。
从纵向上来看,列表只有一层,双层结构有一或者两层,树型结构可能有一层或者多层。
如果要遍历所有的轮廓,可以使用递归的方式。
轮廓的绘制
轮廓的绘制比较简单,用上面提到的方法取得轮廓的所有点,然后把这些点连接成一个多边形即可。
轮廓的一个例子为:OpenCV_轮廓例子
上例中检测出输入图像的轮廓,然后逐个绘制每个轮廓。下个例子为:
在输入图像上寻找并绘制轮廓
具体代码为:
轮廓的匹配源码
#include "gesrec.h" #include <stdio.h>////////////////////////////////////////// #define PI 3.14159f //轮廓面积比较函数 static int gesContourCompFunc(const void* _a, const void* _b, void* userdata) { int retval; double s1, s2; CvContour* a = (CvContour*)_a; CvContour* b = (CvContour*)_b; s1 = fabs(cvContourArea(a)); s2 = fabs(cvContourArea(b)); //s1 = a->rect.height * a->rect.width; //s2 = b->rect.height * b->rect.width; if(s1 < s2) { retval = 1; } else if(s1 == s2) { retval = 0; } else { retval = -1; } return retval; } //src:BGR dst: void gesFindContours(IplImage* src, IplImage* dst, CvSeq** templateContour, CvMemStorage* templateStorage, int flag) { int count;//轮廓数 IplImage* gray; CvMemStorage* first_sto; CvMemStorage* all_sto; CvSeq* first_cont; CvSeq* all_cont; CvSeq* cur_cont; //初始化动态内存 first_sto = cvCreateMemStorage(0); first_cont = cvCreateSeq(CV_SEQ_ELTYPE_POINT, sizeof(CvSeq), sizeof(CvPoint), first_sto); all_sto = cvCreateMemStorage(0); all_cont = cvCreateSeq(0, sizeof(CvSeq), sizeof(CvSeq), all_sto); //创建源图像对应的灰度图像 gray = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1); cvCvtColor(src, gray, CV_BGR2GRAY); //得到图像的外层轮廓 count = cvFindContours(gray, first_sto, &first_cont, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); //如果没有检测到轮廓则返回 if(first_sto == NULL) { return; } //将所有的轮廓都放到first_cont中 for(;first_cont != 0;first_cont = first_cont->h_next) { if(((CvContour* )first_cont)->rect.height * ((CvContour* )first_cont)->rect.width >= 625) cvSeqPush(all_cont, first_cont); } //对轮廓按照面积进行排序 cvSeqSort(all_cont, gesContourCompFunc, 0); //在dst中画出轮廓 cvZero(dst); for(int i = 0;i < min(all_cont->total, 3);i++)///////////////////////次数待改 { cur_cont = (CvSeq* )cvGetSeqElem(all_cont, i); if(flag != 0 && i == 0) { *templateContour = cvCloneSeq(cur_cont, templateStorage); } CvScalar color = CV_RGB(rand()&255, rand()&255, rand()&255); cvDrawContours(dst, (CvSeq* )cur_cont, color, color, -1, 1, 8); } //判断原点位置以确定是否需要反转图像 if(src->origin == 1) { cvFlip(dst); } //释放内存 cvReleaseMemStorage(&first_sto); cvReleaseMemStorage(&all_sto); cvReleaseImage(&gray); } void gesMatchContoursTemplate(IplImage* src, IplImage* dst, CvSeq** templateContour) { CvSeq* contour; CvMemStorage* storage; //初始化动态内存 storage = cvCreateMemStorage(0); contour = cvCreateSeq(CV_SEQ_ELTYPE_POINT, sizeof(CvSeq), sizeof(CvPoint), storage); //得到轮廓并进行匹配 gesFindContours(src, dst, &contour, storage, 1); if(contour->total != 0)//如果得到的轮廓不为空 { double result = cvMatchShapes((CvContour* )contour, (CvContour* )(*templateContour), CV_CONTOURS_MATCH_I3); printf("%.2f\n", result);///////////////////////////////////////////// } //释放内存 cvReleaseMemStorage(&storage); } //模版匹配法的完整实现 int gesMatchContoursTemplate2(IplImage* src, IplImage* dst, CvSeq* templateContour) { CvSeq* contour; CvSeq* cur_cont; CvMemStorage* storage; double minValue, tempValue; int i, minIndex; //初始化动态内存 storage = cvCreateMemStorage(0); contour = cvCreateSeq(CV_SEQ_ELTYPE_POINT, sizeof(CvSeq), sizeof(CvPoint), storage); //得到轮廓并进行匹配 minIndex = -1; gesFindContours(src, dst, &contour, storage, 1); if(contour->total != 0)//如果得到的轮廓不为空 { if(templateContour->total != 0) { cur_cont = (CvSeq* )cvGetSeqElem(templateContour, 0); minValue = cvMatchShapes((CvContour* )contour, (CvContour* )cur_cont, CV_CONTOURS_MATCH_I3); minIndex = 0; printf("0:%.2f\n", minValue); } for(i = 1;i < templateContour->total;i++) { cur_cont = (CvSeq* )cvGetSeqElem(templateContour, i); tempValue = cvMatchShapes((CvContour* )contour, (CvContour* )cur_cont, CV_CONTOURS_MATCH_I3); if(tempValue < minValue) { minValue = tempValue; minIndex = i; } printf("%d:%.2f\n", i, tempValue); } if(minValue >= 0.3) { minIndex = -1; } } //打印匹配结果 printf("the result is %d\n", minIndex); //释放内存 cvReleaseMemStorage(&storage); return minIndex; } //找出轮廓最大的5个极大值点 void gesFindContourMaxs(CvSeq* contour) { int i; CvScalar center;//重心位置 CvPoint* p; CvMat max;//存储5个极大值的数组 double initMax[] = {-1, -1, -1, -1, -1};//初始极大值设置为-1 double minValue, maxValue;//5个极大值中的最大值与最小值 CvPoint minLoc;//最小值的位置 double preDistance = 0; bool isCandidate = false;//是否是候选的极大值点 //初始化重心位置 center = cvScalarAll(0); //初始化极大值矩阵 max = cvMat(1, 5, CV_64FC1, initMax); //首先求出轮廓的重心 for(i = 0;i < contour->total;i++) { p = (CvPoint* )cvGetSeqElem(contour, i); center.val[0] += p->x; center.val[1] += p->y; } center.val[0] /= contour->total; center.val[1] /= contour->total; //遍历轮廓,找出所有的极大值点 for(i = 0;i < contour->total;i++) { p = (CvPoint* )cvGetSeqElem(contour, i); double distance = sqrt(pow(center.val[0] - p->x, 2) + pow(center.val[1] - p->y, 2)); if(distance > preDistance) { isCandidate = true; } else if(distance < preDistance && isCandidate == true) { cvMinMaxLoc(&max, &minValue, &maxValue, &minLoc); if(distance > minValue) { cvmSet(&max, minLoc.y, minLoc.x, distance); } isCandidate = false; } else { isCandidate = false; } preDistance = distance; } //打印5个极大值 printf("%.2f %.2f %.2f %.2f %.2f\n", cvmGet(&max, 0, 0), cvmGet(&max, 0, 1), cvmGet(&max, 0, 2), cvmGet(&max, 0, 3), cvmGet(&max, 0, 4)); } //计算轮廓的pair-wise几何直方图 CvHistogram* gesCalcContoursPGH(CvSeq* contour) { CvHistogram* hist;//成对几何直方图 CvContour* tempCont; //得到成对几何直方图第二个维度上的范围 tempCont = (CvContour* )contour; cvBoundingRect(tempCont, 1); int sizes[2] = {60, 200}; float ranges[2][2] = {{0,PI}, {0,200}}; float** rangesPtr = new float* [2]; rangesPtr[0] = ranges[0]; rangesPtr[1] = ranges[1]; //初始化几何直方图 hist = cvCreateHist(2, sizes, CV_HIST_ARRAY, rangesPtr, 1); //计算轮廓的成对几何直方图 cvCalcPGH(contour, hist); return hist; } //对轮廓的pair-wise几何直方图进行匹配 void gesMatchContoursPGH(CvSeq* contour, CvHistogram* templateHist) { CvHistogram* hist; //得到轮廓的成对几何直方图 hist = gesCalcContoursPGH(contour); //归一化直方图 cvNormalizeHist(templateHist, 1); cvNormalizeHist(hist, 1); //直方图匹配 double result = cvCompareHist(hist, templateHist, CV_COMP_INTERSECT); printf("result:%.2f\n", result); //释放内存 cvReleaseHist(&hist); }
相关文章推荐
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- opencv学习篇(8)轮廓的查找、表达、绘制、特性及匹配
- OpenCV_轮廓的查找、表达、绘制、特性及匹配
- 轮廓的查找、表达、绘制、特性及匹配 c#版
- 轮廓的查找、表达、绘制、特性及匹配(How to Use Contour? Find, Component, Construct, Features & Match)
- 轮廓的查找、表达、绘制、特性及匹配
- 轮廓的查找、表达、绘制、特性及匹配
- 轮廓的查找、表达、绘制、特性及匹配(How to Use Contour? Find, Component, Construct, Features & Match)
- 轮廓的查找、表达、绘制、特性及匹配(How to Use Contour? Find, Component, Construct, Features & Match)
- C#实现图像轮廓的查找、表达、绘制、特性及匹配