2017-09-27 10:59:08 xiao_lxl 阅读数 2248
  • Qt项目实战之网络电子白板

    本课程使用Qt技术实现了网络电子白板,支持直线、矩形、椭圆、三角形、涂鸦等图形元素。本课程实现的电子白板,可以在多人之间共享,每个人都可以进行任意绘制,每个人的绘制都可以同步显示在其它人的白板上。服务器端使用Qt Network开发,客户端使用Qt Network和Qt Graphics View Framework开发,数据传输使用JSON数据格式。

    11506 人正在学习 去看看 安晓辉

以前整理的文档,现在贴出来吧,方便后期需要时查看参考。

图像处理——椭圆肤色模型
这里参数的说明参考《基于分裂式K均值聚类的肤色检测方法 》 http://download.csdn.net/detail/wobuaishangdiao/4378026
《皮肤检测技术的研究及改进》

皮肤检测—-肤色椭圆模型
肤色区域的颜色与亮度成非线性函数关系,在低亮度条件下,YCbCr 空间中色度的聚类性会随Y 呈非线性变换降低。为了使肤色聚类不受亮度Y 的影响并将YCbCr 颜色空间中的色度Cb、Cr进行非线性变换,在研究YCbCr 颜色空间的肤色聚类情况的基础上,去掉高光阴影部分(即 Y 的最大最小值),YCbCr 空间色度非线性变换过程中,用 Cb·· Y 、 Cr·· Y 表示肤色区域的中轴线,肤色区域的宽度分别用 Vcb、Vcr 表示。
即将图像转化到YCbCr 空间并且在CbCr平面进行投影,因此我们采集了肤色的样本点,将其投影到此平面,并且投影后,我们进行了相应的非线性变换K-L变换进而形成的的统计椭圆模型

这里写图片描述

这里写图片描述

    void cvSkinSegment(IplImage* img, IplImage* mask)   //原始图像及目标图像   
    {  
        CvSize imageSize = cvSize(img->width, img->height);    
        IplImage *imgY = cvCreateImage(imageSize, IPL_DEPTH_8U, 1);  
        IplImage *imgCr = cvCreateImage(imageSize, IPL_DEPTH_8U, 1);  
        IplImage *imgCb = cvCreateImage(imageSize, IPL_DEPTH_8U, 1);  


        IplImage *imgYCrCb = cvCreateImage(imageSize, img->depth, img->nChannels);  
        cvCvtColor(img,imgYCrCb,CV_BGR2YCrCb);  
        cvSplit(imgYCrCb, imgY, imgCr, imgCb, 0);   //得到每一通道的数据   
        int y, cr, cb, l, x1, y1, value;  
        unsigned char *pY, *pCr, *pCb, *pMask;  

        pY = (unsigned char *)imgY->imageData;  //Y通道的第一行数据的指针   
        pCr = (unsigned char *)imgCr->imageData;  
        pCb = (unsigned char *)imgCb->imageData;  
        pMask = (unsigned char *)mask->imageData;   //目标图像的第一行数据的指针   
        cvSetZero(mask);   //目标图像全部为零   
        l = img->height * img->width;   //图像的元素个数   
        for (int i = 0; i < l; i++)  
        {  
            y  = *pY;  
            cr = *pCr;  
            cb = *pCb;  
            cb -= 109;  
            cr -= 152;  
            x1 = (819*cr-614*cb)/32 + 51;  
            y1 = (819*cr+614*cb)/32 + 77;  
            x1 = x1*41/1024;  
            y1 = y1*73/1024;  
            value = x1*x1+y1*y1;    //构造椭圆的模型   
            if(y<100)    (*pMask)=(value<700) ? 255:0;    //对齐进行值得选择,要不255,要不0  //在不同的亮度下呈现出不同的亮度   
            else        (*pMask)=(value<850)? 255:0;    //255 is the skin, 0 is the background   //255 纯白色 0 纯黑色   
            pY++;  
            pCr++;  
            pCb++;  
            pMask++;  
        }  

    //cvSaveImage("example.jpg",mask);   

        //cvErode(mask, mask, NULL, 1);    
        //cvDilate(mask, mask, NULL, 1);    

        //cvSmooth(mask, mask, CV_GAUSSIAN, 21, 0, 0);    

        //cvThreshold(mask, mask,130, 255, CV_THRESH_BINARY);    
        cvReleaseImage(&imgY);  
        cvReleaseImage(&imgCr);  
        cvReleaseImage(&imgCb);  
        cvReleaseImage(&imgYCrCb);  
    }  
2019-05-16 10:11:18 cheng_xing_ 阅读数 804
  • Qt项目实战之网络电子白板

    本课程使用Qt技术实现了网络电子白板,支持直线、矩形、椭圆、三角形、涂鸦等图形元素。本课程实现的电子白板,可以在多人之间共享,每个人都可以进行任意绘制,每个人的绘制都可以同步显示在其它人的白板上。服务器端使用Qt Network开发,客户端使用Qt Network和Qt Graphics View Framework开发,数据传输使用JSON数据格式。

    11506 人正在学习 去看看 安晓辉

经典椭圆检测方法

椭圆检测算法经过多年的研究发展,已经基本形成一个较完整的体系。它们大致可以分为三类即投票(聚类)、最优化、基于弧段的方法。

投票(聚类)方法

椭圆因为有中心位置坐标、长短轴长度、倾斜角五个参数,标准霍夫变换有较强的鲁棒性,但对内存要求高,运算效率低,不太现实。霍夫变换类算法以霍夫变换为算法基础,经过不同国家研究人员多年的不懈努力研究,如今已衍生出很多改进算法,它们各有优劣。随机霍夫变换算法相对标准霍夫变换计算速度有较大提升,但检测相互遮挡的椭圆时准确度低。

随机hough变换椭圆检测算法

随机椭圆检测结合使用了了最小二乘法和Hough变换算法。第一步预处理,获得较理想的边缘图。第二步随机选取三个点,取这三点为中心相同大小的邻域中所有点,用最小二乘法把它们拟合成一个椭圆。如图2-3所示。第三步从边缘点中再随机选取第四个点,判断此点是否在拟合出的圆上。若是,则认为该椭圆是真实椭圆的可能性较大,接着收集证据,验证该椭圆的真实性。

图2-3 随机选点示意图
算法具体过程如下(从第二步开始):
1.把边缘检测得到的点收进集合V中,失败计数器f初始值设为0。设定5个阈值,分别是能容忍的失败次数最大值Tf,检测进行时对V中边缘点数量的要求阈值Tem,随机选取的三点之间两两距离最小值Ta,随机选取的第四点到可能椭圆边界距离的最大值Td,以及椭圆残缺比率阈值Tr。
2.np表示集合V中剩余的点的数量,当np小于Tem时或当失败次数f大于Tf时停止检测,算法终止;否则从V中随机取四点,并从V中删除这四点。
3.若用来求解椭圆参数的三个点两两之间距离都大于Ta,拟合出椭圆,计算第四个点到该椭圆边界的距离,若距离小于Td,执行第4步;若不满足两者之一,将这四个点返回到V中,失败次数加一,回到第2步执行。
4.设E为第3步拟合出来的椭圆,初始化满足阈值Td的点的个数num。遍历V中点,计算并判断它们到椭圆E的边界的距离是否小于Td,若是则num=num+1,并将该点从V中除去,直到遍历完成。
5.若num>=Tr*K,其中K为椭圆E的周长,那么跳转到第6步;否则认为椭圆E不是真实的椭圆,将第4步和第2步中删除的num+4个点返回V中,并跳转到第2步。
6.认为椭圆E是一个真实存在的椭圆,f置0,并跳转到第2步。
随机hough变换的优缺点如下:
第一,由于该算法是基于最小二乘法,所以一方面检测结果往往比真实椭圆小而且对噪声敏感,但是另一方面当预处理效果较好时检测精度很高。
第二,由于该算法基于随机采样,所以一方面可能会有所选点距离较近的情况造成拟合出的椭圆偏差较大,但是另一方面因为随机采样的灵活性检测速度提升了。
第三,一方面当参数选取的较好时检测又快有准确;另一方面,由于该算法严格由参数Ta,Td,Tr控制而且这些参数不易取到合适值,所以会出现不合适的参数不仅增加计算量,而且增加误检机会的情况。

最优化方法

最优化类方法优点在其精度上,缺点是其一次只能处理一个图形,即此前要对图像信息进行分类分离。AndrewFitzgibb等人提出了直接最小二乘法椭圆拟合算法。该方法能保证拟合出来的一定是椭圆。但该方法受到孤立点和噪声点的影响。目前最优化算法多与其他算法一起结合使用。

基于弧段的方法

基于边界聚类的椭圆检测方法结合使用了基于弧段的方法和最小二乘法。从边界图提取圆弧,再经过过滤、聚类,最终用最小二乘法拟合出椭圆。该方法能有效应对多个椭圆、椭圆相互遮挡和椭圆部分缺损等复杂情况,因而引起了广泛的注意。

边界聚类算法流程

边界聚类算法属于从下往上结构的算法。算法步骤主要分为三步,分别是预处理,边界聚类和直接最小二乘法拟合椭圆三个过程。流程图如下所示:
在这里插入图片描述

预处理

预处理第一步是进行灰度变换。灰度化常用的方法也就是依据亮度方程来实现的,即依据人眼对不同颜色的敏感度不同,对RGB分量以不同系数的加权平均。
在这里插入图片描述
第二是降噪。去噪手段对应于噪声的两种分类主要有两种。噪声功率谱符合高斯函数时用可以用高斯平滑模板平滑。由于脉冲干扰等产生的噪声(即椒盐噪声)可以采用中值滤波去除。
第三步是边界检测。通常图像中边界点都是图像中亮度梯度比较大的点,这些点包含了我们检测要用到的图像特征信息。边界检测最常用的方法是Canny算法。该算法主要分四个部分。一、降噪。方法是让原始图像和所用的高斯模板作卷积,模板在使用前指定了标准差。 二、寻找亮度梯度。Canny算法使用4个模板检测边缘的方向,,它们分别是水平、垂直、主对角线和副对角线方向;遍历圆图上的每个像素点,让原始图像中以该点为中心以该模板为窗口内的所有点与该模板作卷积,我们就从原图获得了各个点亮度梯度图和亮度梯度的方向。三、非极大值抑制。该过程目的是获得单像素的候选边缘,主要操作是将非零像素点所在的区域进行细化。具体过程如下:对于图3-2中一点P(x,y),计算P点梯度方向与其8-连通邻域点所组成的正方形的交点 (x1,y1)和(x2,y2)。如图2-5,交点坐标通过插值法得到。如果中间点的值大于这两个交点值,那么P点值不变,如若不然置零。四、滞后阈值操作。它需要设置两个阈值t1与t2。t1等于边界像素数除以总像素数,这些点称之为强边缘像素。t2等于t1除以2, t2和 t1之间的点称之为弱边缘像素。最后通过将8-连通的弱像素集成到强像素,再把它们连接起来,得到边界图。
在这里插入图片描述
第四步是二值化。二值化的效果几乎完全取决于分割阈值的选取。所以自动寻找最佳分割阈值的方法就显得十分关键。找到图片二值化的一个合适的分割阈值的一种方法是按图像的灰度特性,将图像分成背景和目标两部分,背景和目标之间的类间方差最大的分割意味着错分概率最小。MATLAB 的Graythresh函数就是使用该方法来获得一个自适应阈值作为二值化的分割依据。
第五步,二值化后,因为椭圆弧附近的非相关像素会严重影响检测结果,因此为了大幅度减少非相关像素,本文用形态学的腐蚀操作来得到细化的边界。

边界像素连接

采用Kovesi的边界连接算法,以8-邻域连通准则从上至下,从左至右扫描二值图像,将边界像素连接为有向边界列。然后采用边界列中像素数阈值条件去除像素数较小的集合。因为若边界列像素数少于阈值数,则很有可能是噪声或背景,应当删除。具体步骤如下:
1.以8-邻域连通准则从上至下,从左至右扫描二值图像,按连通区域对图像中的像素点聚类。
2.寻找每一个连通域中边界像素中所有的结束点和分叉点(分叉点是三条以上曲线的交点)并存储。
3.以这些结束点和分叉点为结束标志,让每一个连通域中的像素点集合分割为遇到结束点和连接点就断开的小集合。
4.删除这些集合中像素数小于某一阈值的部分。

线段列提取

因为图像光栅化难以获得准确的切线,而后续过程需要用到圆弧的切线,所以要进行线段拟合,即用多段折线代替原来的圆弧。具体步骤如下:
1.取边界像素连接成的第i条有向边界列,判断是否超过边界列总数目total,若不是进行步骤2,若是终止算法。
2.判断该边界列是否已经完成处理,若为否则进行第3步;若是则i=i+1,重新进行步骤2.
3.从其中第三个点开始,计算第一个点到这个点(记为点j)的连线方程,并依次判断第一个点和该点之间的所有点到该连线的距离,若所有距离均小于某一阈值,则j=j+1,重新进行步骤3,否则该有向边界列从该处断开,前面部分只保留第一个点和第j个点,前面j个点构成的连线用第一点和第j点之间的直线连线代替;后面部分仍然记为边界列i,
4.判断步骤3中断开的有向边界列后面部分像素数是否小于某阈值,若是则删除掉,否则不处理。最后跳转到步骤1。
完成这一个过程后一个连通域的的像素点构成的曲线就变成了其中部分像素点构成的一条折线。经过这个过程虽然像素信息损失了一部分,但是求取圆弧切线的精度从某种意义上说提高了,因为没有了光栅化效应。而且数据少了处理变得简单。再采用线段数阈值条件去除较短的线段列。若线段数数少于阈值数,则很有可能是噪声或背景或者进行拟合时误差过大,因此须删除。

线段列旋转方向统一

本文将所有线段列旋转方向统一为逆时针方向。
假设图3-4中的黑点为线段列中的点,箭头代表线段列的方向,P1(x1,y1),P2(x2,y2),P3(x3,y3)为线段列中连续的三个像素,像素都引入z坐标,且令其为0,则P1(x1,y1,0),P2(x2,y2,0),P3(x3,y3,0),空间向量
P1P2=(x2-x1,y2-y1,0)         (3-2)P2P3=(x3-x2,y3-y2,0)         (3-3)
向量积
P1P2×P2P3=|■(i ⃗&j ⃗&k ⃗@x2-x1&y2-y1&0@x3-x2&y3-y2 &0)|=(0,0,(x2-x1)(y3-y2)-(x3-x2)(y2-y1))         (3-4)
对一个线段列中除去首尾两个点的所有点像P2点一样计算并判别和存储,若小于0的次数最多,则认为线段列的旋转方向是顺时针,将线段列中的点逆序处理;若大于0的次数最多,则认为线段列的旋转方向是逆时针。
在这里插入图片描述

凹点和角点检测

在确定线段列的旋转方向为逆时针方向后,检测凹点和角点方法同前面统一线段列旋转方向类似,对一个线段列中除去首尾两个点的所有点计算P1P2×P2P3并判断向量积第三个分量的大小,若小于0,则P2为凹点。因为边界波动可能引入冗余凹点也即因边界检测误差可能错判一些正常点为凹点而进修分割会导致检测率下降,所以增加一个角度判断过程,即前面向量积为0并且P1P2和P2P3的夹角大于某阈值,才为凹点,这样选择合适的阈值可保证凹点检测的准确性。若向量积大于0且P1P2和P2P3的夹角大于另一阈值,则认为该点角度变化过大,是角点,线段列有很大可能性不是椭圆弧,而有可能是三角形、矩形等图形的边角,因此从该点分割该线段列。分割完成后,过滤掉含点数较少的线段列,即可除掉部分非椭圆弧。留下的线段列认为是椭圆弧,参加后续的聚类。如图3-5所示,左上角的P2很可能是凹点,右下角的P2很可能是角点。
在这里插入图片描述

圆弧聚类

圆弧聚类是将属于同一椭圆但是分开的两条或多条椭圆弧进行聚类。在进行聚类前,首先要判断弧段的完整度。一般用弧段的首尾端点P1,P3与中点P2构成的两向量P2P1,P2P3的夹角的大小来进行判断。夹角越小,一般该椭圆弧越完整,夹角越大,一般认为椭圆弧缺损越严重。设定一个阈值,当夹角小于该阈值时认为该弧段已经足够完整,仅仅靠该弧段上的点就可以较准确地拟合出真实存在的椭圆,因此该弧段不需要参与后面的聚类过程。如果希望该阈值自适应,在划分待聚类椭圆弧和不须聚类的弧(较完整弧)之前,先要确定该阈值。用直接最小二乘法拟合该弧所在的椭圆,若较圆,为了减小的误差,应使阈值夹角稍微大一些,如90度;若该弧所在的椭圆较扁,应使阈值夹角稍微小一些,如60度。接下来才根据该自适应阈值进行对圆弧判断。当大于阈值时认为该弧段上的点过少,不足以拟合出准确的椭圆,需要找到和该弧段属于同一椭圆的弧段然后用它们所有的点一起拟合出一个椭圆。经过此判断过程,椭圆弧就被分成两组。把需要参加聚类的椭圆弧按照含点数的数目由多到少进行排列,下面的过程都按照数目多的弧段优先的顺序进行。
对于待聚类的椭圆弧,先要定义其搜索区域,由于椭圆是封闭图形,所以整个椭圆可以确定是在其任何一部分弧和弧两端点的切线所在的射线包围起来的区域里面,属于该椭圆的其他弧以确定是在该弧对应的弦和弧两端点的切线所在的射线包围起来的区域里面。这就是我们搜索的区域。在图中a1的搜索区域也就是射线l1,l2,弦l3和图像边缘范围内的区域,在这个区域里面找弧,可以缩小搜寻的范围,提高效率。判断一条弧是否在待聚类椭圆弧的搜索区域里面我们只需取这条弧的首末端点j3,j4是否在搜索区域。方法是分别求过这两点同时平行于待聚类椭圆弧对应弦l3的直线和切线的交点,若交点分别有两个,交点都在射线上且这两个端点在对应两个交点之间则该弧段在搜索区域内。图中明显a2,a3,a4在a1的搜索区域内而a5不在。
在这里插入图片描述
待聚类椭圆弧找到待配对的圆弧后用两种约束条件判断它们到底是否属于同一椭圆。约束一是利用一个椭圆任意两段弧弧中点之间的距离大于一个弧中点到另一个弧首末端点连线的中点之间的距离,在图中即为
在这里插入图片描述
用来去除图中a2类型的椭圆弧。右图(b)不满足,左图(a)同时满足这两个关系,进入下一步,再用约束二进行判断。
在这里插入图片描述
约束二是点到拟合椭圆边界距离条件。我们只需要让两条线段列中的点一起参与椭圆拟合,按照下面公式计算所有这些点到拟合出椭圆边界的距离。设置一距离阈值,当d_i小于该阈值认为该点落在该椭圆上,否则该点不在这个椭圆上。统计d_i中小于某一阈值的点的数量,若大于某一比例(比例阈值),则认为这两条弧属于同一椭圆,否则不属于同一椭圆。判定后将属于同一椭圆的弧段聚类到一起。
在这里插入图片描述
其中
其中:x^'=(x_i-x_0)cos⁡〖θ+(y_i-y_0)sin⁡θ 〗,y^'=-(x_i-x_0)sin⁡〖θ+(y_i-y_0)cos⁡θ 〗。
在这里插入图片描述
如上图(a)中的参与拟合的点较多都落在拟合的椭圆上,所以有较大可能满足约束二条件;(b)中大多数拟合点点离拟合出的椭圆边界有一定距离,有较大可能不满足约束条件二。
如果希望这两个阈值改为自适应的,方法是先拟合出椭圆,判断椭圆大小。若椭圆较小,应适当降低限制,即增大距离阈值,减小比例阈值;若椭圆较大,应适当提高限制,即减小距离阈值,增大比例阈值。本算法中选取的是,若椭圆的短轴小于50则距离阈值为0.05,比例阈值设为0.7;否则前者取0.03,后者取0.8。

再配对

聚类后的弧和较完整弧或者两个较完整弧可能属于同一椭圆但是在前面的步骤它们只是被分开了并没有配对,所以有必要增加再匹配过程,增加检测准确度。匹配方法还是约束条件二的方法。因为该方法和原算法的去伪过程相似,所以经过该方法后无需再去伪。

直接最小二乘法椭圆拟合

前面的很多步骤都删除了像素点较少的集合,或者用少数点代替了边界列中的很多点,或者是分割后再删除点数较少的集合的,这些操作到椭圆拟合这一步实际上基本上去除了所有的背景和噪声,甚至包括一部分有用信息。所以即使对噪声和孤立点敏感的直接最小二乘法也可以用来拟合椭圆,而且因为该方法对椭圆缺损不敏感,所以非常适合。

实验效果

边界聚类算法检测结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

边界聚类算法和随机霍夫变换算法比较

用时比较在这里插入图片描述
检测结果比较
左图是选点情况;右图是拟合的椭圆和真实椭圆的差异
在这里插入图片描述
随机霍夫变换算法检测结果
在这里插入图片描述
边界聚类算法检测结果

2019-03-16 14:32:13 weixin_44540503 阅读数 945
  • Qt项目实战之网络电子白板

    本课程使用Qt技术实现了网络电子白板,支持直线、矩形、椭圆、三角形、涂鸦等图形元素。本课程实现的电子白板,可以在多人之间共享,每个人都可以进行任意绘制,每个人的绘制都可以同步显示在其它人的白板上。服务器端使用Qt Network开发,客户端使用Qt Network和Qt Graphics View Framework开发,数据传输使用JSON数据格式。

    11506 人正在学习 去看看 安晓辉

利用opencv进行图像处理,提取椭圆圆心处理

写这个是因为项目正好在做这个,所以简单写写提取椭圆圆心坐标的代码,用的软件是VS。
首先介绍一下步骤,直接从图像处理开始
1,二值化处理(threhold())
2,高斯滤波(GaussianBlur())
3,轮廓提取(canny算子)
4,寻找闭合轮廓(findContours())

上面介绍的是提取椭圆轮廓过程,下面介绍的是过滤干扰图像的过程,分三步,不过,这三部也不都是必须的,看个人需求:
1,像素点数量过滤(size)
2,面积过滤
3,长宽比过滤
代码如下,比较简单,就不过多介绍了,欢迎留言:

#include<iostream>
#include"opencv.hpp"

using namespace std;
using namespace cv;


void drawCross(Mat &img, Point2f point, Scalar color, int size, int thickness /*= 1*/)
{
	//绘制横线  
	line(img, cvPoint(point.x - size / 2, point.y), cvPoint(point.x + size / 2, point.y), color, thickness, 8, 0);
	//绘制竖线  	
	line(img, cvPoint(point.x, point.y - size / 2), cvPoint(point.x, point.y + size / 2), color, thickness, 8, 0);
	return;
}
 

//提取单幅图像的特征点
void FeaturePoint(Mat &img)
{
	Point2f center; //定义变量
	vector<Point2f> ellipsecenterleft;
	Mat edges;
	threshold(img, img, 80, 255, CV_THRESH_BINARY_INV);
	//imshow("threshold", img);
	////此函数等待按键,按键盘任意键就返回
	//waitKey(0);
	GaussianBlur(img, edges, Size(5, 5), 0, 0);
	//imshow("GaussianBlur", edges);
	////此函数等待按键,按键盘任意键就返回
	//waitKey(0);
	Canny(edges, edges, 40, 120, 3);
	//imshow("Canny", edges);
	////此函数等待按键,按键盘任意键就返回
	//waitKey(0);
	vector<vector<Point> > contours;// 创建容器,存储轮廓
	vector<Vec4i> hierarchy;// 寻找轮廓所需参数


	findContours(edges, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
	//cout << "conours size in "<<serialNumber <<" :" << contours.size() << endl;

	//if (contours.size() == 1)
	for (int i = 0; i < contours.size();i++) 
	{
		RotatedRect m_ellipsetemp;  // fitEllipse返回值的数据类型
    	if (contours[i].size() <= 200){
			continue;
		}
		if (contourArea(contours[i]) < 100 && contourArea(contours[i]) > 1000)
		{
			continue;
		}
		m_ellipsetemp = fitEllipse(contours[i]);  //找到的第一个轮廓,放置到m_ellipsetemp


		ellipse(img, m_ellipsetemp, cv::Scalar(255,0,0));   //在图像中绘制椭圆

		if (m_ellipsetemp.size.width / m_ellipsetemp.size.height <0.3)
		{
			continue;
		}
		center = m_ellipsetemp.center;//读取椭圆中心

		drawCross(img, center, Scalar(255,0,0), 30, 2);
		cout << center.x << ends << center.y << endl;
	}
	
	/*imshow("image", img);
	waitKey(0);*/
	//return center;//返回椭圆中心坐标
	
}



int main(int argc, char* argv[])
{
	const char* imagename = "pixmap1.png";

	//从文件中读入图像
	Mat img = imread(imagename, 1);

	//如果读入图像失败
	if (img.empty())
	{
		fprintf(stderr, "Can not load image %s\n", imagename);
		return -1;
	}
	FeaturePoint(img);
	//显示图像
	imshow("image", img);

	//此函数等待按键,按键盘任意键就返回
	waitKey();
	return 0;
}
2014-03-30 21:18:14 zhiyuanzhe007 阅读数 8599
  • Qt项目实战之网络电子白板

    本课程使用Qt技术实现了网络电子白板,支持直线、矩形、椭圆、三角形、涂鸦等图形元素。本课程实现的电子白板,可以在多人之间共享,每个人都可以进行任意绘制,每个人的绘制都可以同步显示在其它人的白板上。服务器端使用Qt Network开发,客户端使用Qt Network和Qt Graphics View Framework开发,数据传输使用JSON数据格式。

    11506 人正在学习 去看看 安晓辉

        图像处理中的椭圆检测用处还是挺多的,找到这里来的同学大多是想用椭圆检测来解决某些实际问题吧,所以我就不做介绍,直奔主题。我研究这块也有一段时间了,也查找了挺多资料,貌似通用的椭圆算法还没有,不排除我孤陋寡闻了。前辈提出的算法适用范围比较有限,这个比较有限是相对直线检测来说的。但直接用Hough变换来找椭圆几乎是不可能的事,在5维空间里做投票,想想都觉得可怕。于是有人想到用随机Hough变换。这是一种很合理的方法,我就是这么做的,不过这种方法有个不足之处,后面会讲到。这里先介绍这方法的流程。

        二次曲线的一般方程为:,其中(x,y)为图像的坐标空间,BCDEF是二次曲线的参数。当满足时二次曲线为椭圆。方程中需要求解的参数有5个,在随机Hough变换过程中要至少采集5个点,得到5个方程以求得这5个参数。若方程有解且满足约束条件,则将解加入参数空间进行累积。思路是比较简单的,下面边贴代码边解释(P.S. 代码仅供参考)。

void FindEllipse(TImage* OrgGrayImg)
{
int ImgHeight = OrgGrayImg->nHeight;
	int ImgWidth = OrgGrayImg->nWidth;
	unsigned char * Img = OrgGrayImg->pImage; // 输入图像确保是二值图像

srand((unsigned)time(NULL));

	int totalPt = 0;// 用于统计样本点的个数

	for (i = 0; i < ImgHeight - 0; i++)
	{
		unsigned char *imgdata = Img + i * ImgWidth;
		for (j = 0; j < ImgWidth - 0; j++)
		{
			if (!imgdata[j])
				totalPt ++;
		}
	}

	if (totalPt < 5)
		return;

	POINT * seq;
	seq = new POINT [totalPt];

	int count = 0;
	for (i = 0; i < ImgHeight; i++)
	{
		unsigned char *data = Img + i * ImgWidth;
		for (j = 0; j < ImgWidth; j++)
		{
			if (!data[j])
			{
				seq[count].x = j;
				seq[count].y = i;
				count ++;
			}
		}
	}

	double Para[5];	// 存放结果(5个参数A,B,C,D,E)的数组
	int Angle_V[360]={0};// 椭圆倾斜角参数空间
	int *Center_XV = new int[ImgWidth];// 椭圆中心点x坐标参数空间
	int *Center_YV = new int[ImgHeight];// 椭圆中心点y坐标参数空间
	int *A_axis_V = new int[max(ImgWidth,ImgHeight)/2];// 椭圆长轴参数空间
	int *B_axis_V = new int[max(ImgWidth,ImgHeight)/2];// 椭圆短轴参数空间
	
	memset(Center_XV,0,sizeof(int)*ImgWidth);
	memset(Center_YV,0,sizeof(int)*ImgHeight);
	memset(A_axis_V,0,sizeof(int)*max(ImgWidth,ImgHeight)/2);
	memset(B_axis_V,0,sizeof(int)*max(ImgWidth,ImgHeight)/2);

	double Theta,X_c,Y_c,A_axis,B_axis;

int loop = 1;// 成功求出参数的迭代次数
	int looptop = loop * 1;// 总的迭代次数(也就是控制计算时间的上限,以免陷入无限循环)
while(loop > 0 && looptop > 0)
{
looptop --;
	int idx;
	for (count = totalPt; count > 0; count--)// 打乱样本点排列的顺序
	{
		POINT ptrtmp;
		idx = rand() % count;
		
		ptrtmp = seq[idx];
		seq[idx] = seq[count-1];
		seq[count-1] = ptrtmp;		
	}

	double PioMatrix[5*5];
	for (i = 0; i < 5; i++)
	{
		PioMatrix[i*5] = seq[i].x * seq[i].x;
		PioMatrix[i*5 + 1] = 2 * seq[i].x * seq[i].y;
		PioMatrix[i*5 + 2] = seq[i].y * seq[i].y;
		PioMatrix[i*5 + 3] = 2 * seq[i].x;
		PioMatrix[i*5 + 4] = 2 * seq[i].y;
	}

	if (GaussJordanInv(PioMatrix,5) == false)// Gauss-Jordan求逆
		continue;
	double sum;
	for (i = 0; i < 5; i++)
	{
		sum = 0;
		for (j = 0; j < 5; j++)
		{
			sum +=  -(PioMatrix[i*5 + j]);
		}
		Para[i] = sum;
	}

	if (pow(Para[1],2) - Para[0] * Para[2] > 0)
			continue;

		if (fabs(Para[0] - Para[2]) < 1e-20)
			Theta = 1.570796326;
		else if (Para[0] > Para[2])
			Theta = 0.5 * (atan(2.0 * Para[1] / (Para[0] - Para[2])) + PI);
		else
			Theta = 0.5 * (atan(2.0 * Para[1] / (Para[0] - Para[2])));

		X_c = (4.0 * Para[1] * Para[4] - 4.0 * Para[2] * Para[3]) / (4.0 * Para[0] * Para[2] - 4.0 * Para[1] * Para[1]);
		Y_c = (4.0 * Para[1] * Para[3] - 4.0 * Para[0] * Para[4]) / (4.0 * Para[0] * Para[2] - 4.0 * Para[1] * Para[1]);
		A_axis = 2 * (Para[0] * pow(X_c,2) + Para[2] * pow(Y_c,2) + 2 * Para[1] * X_c * Y_c - 1) 
						/ (Para[0] + Para[2] - sqrt(pow(Para[0] - Para[2],2) + pow(2.0 * Para[1],2)));
		B_axis = 2 * (Para[0] * pow(X_c,2) + Para[2] * pow(Y_c,2) + 2 * Para[1] * X_c * Y_c - 1) 
						/ (Para[0] + Para[2] + sqrt(pow(Para[0] - Para[2],2) + pow(2.0 * Para[1],2)));
		
		A_axis = sqrt(A_axis);	//长轴
		B_axis = sqrt(B_axis);	//短轴

		int AngleTmp = (int)(Theta * 180 / PI + 360 + 0.5) % 360;
		Angle_V[AngleTmp]++;
		
		if (X_c < 0 || Y_c < 0 || A_axis < 0 || B_axis < 0)
			continue;
		if (X_c >= ImgWidth || Y_c >= ImgHeight || A_axis > max(ImgWidth,ImgHeight)/2 || B_axis > max(ImgWidth,ImgHeight)/2)
			continue;

		if (X_c >= 0 && X_c < ImgWidth)
			Center_XV[(int)X_c]++;
		if (Y_c >= 0 && Y_c < ImgHeight)
			Center_YV[(int)Y_c]++;
		if (A_axis >= 0 && A_axis < max(ImgWidth,ImgHeight)/2)
			A_axis_V[(int)A_axis]++;
		if (B_axis >= 0 && B_axis < max(ImgWidth,ImgHeight)/2)
			B_axis_V[(int)B_axis]++;		
		loop--;
}
	
	int Angle,Ai,Bi,Cx,Cy;
	//	Angle
	int MaxPara = 0;
	for (i = 0; i < 360; i++)
	{
		if (MaxPara < Angle_V[i])
		{
			MaxPara = Angle_V[i];
			Angle = i;
		}
	}
	//	Cy
	MaxPara = 0;
	for (i = 0; i < ImgHeight; i++)
	{
		if (MaxPara < Center_YV[i])
		{
			MaxPara = Center_YV[i];
			Cy = i;
		}
	}
	//	Cx
	MaxPara = 0;
	for (i = 0; i < ImgWidth; i++)
	{
		if (MaxPara < Center_XV[i])
		{
			MaxPara = Center_XV[i];
			Cx = i;
		}
	}
	//	Ai
	MaxPara = 0;
	for (i = 0; i < max(ImgWidth,ImgHeight)/2; i++)
	{
		if (MaxPara < A_axis_V[i])
		{
			MaxPara = A_axis_V[i];
			Ai = i;
		}
	}
	//	Bi
	MaxPara = 0;
	for (i = 0; i < max(ImgWidth,ImgHeight)/2; i++)
	{
		if (MaxPara < B_axis_V[i])
		{
			MaxPara = B_axis_V[i];
			Bi = i;
		}
	}

	delete[] Center_XV;
	delete[] Center_YV;
	delete[] A_axis_V;
	delete[] B_axis_V;


	double sma = SinMem[Angle];
	double cma = CosMem[Angle];
	for (int n = 0; n < 360; n++)
	{
		i = (Bi) * CosMem[360 - n];
		j = (Ai) * SinMem[360 - n];
		
		int x,y;
		x = (j * cma - i * sma) + Cx;
		y = (i * cma + j * sma) + Cy;
		
		Mask[y * ImgWidth + x] = 0;
	}
	delete[] Mask;
	delete[] seq;
}

测试结果:

原图:

拟合结果(虚线为拟合的椭圆):

前面说到这种方法有缺陷,请看下面的情形:

原图:

拟合结果:

       当样本点只集中在椭圆的一边时,随机5点的hough变换总会拟合错误,实际应用中往往会发生这样的情况。这是因为公式错了吗?于是我单独提取出五点做测试,即只做一次迭代。测试结果如下图所示。图中实线为实际椭圆,我是用画图工具拖出来的“完美椭圆”,用橡皮擦擦掉一大半部分,最后再做一点旋转。打交叉的是取样的5点,虚线是用这5点代入公式求得的拟合椭圆。可见求得的椭圆穿过了5个点,表明不是求解错了,可就是跟实际的有很大差别。唯一的解释是取样点不在我们想要的椭圆上,也就是说即使是用画图工具拖出来看似完美的椭圆并不完美,这是因为样本点的坐标是整型,精度很低。所以随机5点的hough变换存在很严重的系统误差,当取样点分散在椭圆上下左右时,这种误差会比较小,当集中在某个区域时,误差就会非常大。

   

        解决办法就是多采几个点,然后用最小二乘法求解。

下图是10点随机采样的结果:

下图是将所有点一起计算的结果:

可见,采样点越多,拟合度越好。但是一次取样的点越多,这些点落入相同椭圆的概率就越小。这就需要一些手段把椭圆的边缘从噪声中提取出来。至于最小二乘法的椭圆检测算法我将另开一贴来讨论。

 

 

 


 

2019-08-23 09:43:27 weixin_41887615 阅读数 368
  • Qt项目实战之网络电子白板

    本课程使用Qt技术实现了网络电子白板,支持直线、矩形、椭圆、三角形、涂鸦等图形元素。本课程实现的电子白板,可以在多人之间共享,每个人都可以进行任意绘制,每个人的绘制都可以同步显示在其它人的白板上。服务器端使用Qt Network开发,客户端使用Qt Network和Qt Graphics View Framework开发,数据传输使用JSON数据格式。

    11506 人正在学习 去看看 安晓辉

椭圆拟合之最小二乘法

原理:可能以后的篇幅再详细写了,

代码:

QImage inputImage : 输入图像;
float
ellipse : 输出椭圆的各个参数
bool Vertical : 是否垂直
bool total : 椭圆是否有上下部分,还是只是椭圆的一段曲线

void Ellipse(QImage *inputImage,float* ellipse,bool Vertical,bool total)
{
    vector<vector<float>> XX_mats;
   // double x[2];
    vector<float> x_mats;
    vector<float> y_mats;

    int Min_x = 100000,Max_x = 0;
    float b = 100000;
    float Max_a = 0;
    float a;
    int y_Min = 0, y_Max = 0;
    float X0,Y0;



   //初始值

    float sum_x = 0;
    float sum_y = 0;
    int width = inputImage->width();
    int height = inputImage->height();

    for(int i = 25; i<height-10; i++)
    {
        for(int j = 25; j<width-10; j++)
        {
            if(QColor(inputImage->pixel(j,i)).red() == 255)
            {
                if(j>Max_x)
                {
                    Max_x = j;
                    y_Max = i;
                }
                if(j<Min_x)
                {
                    Min_x = j;
                    y_Min = i;
                }
                sum_y += i;
                sum_x += j;
                x_mats.push_back(j);
                y_mats.push_back(i);

            }
        }

     }

    if(total)
    {
        X0 = sum_x /x_mats.size();
        Y0 = sum_y / y_mats.size();
    }
    else {
        X0 = ((Max_x + Min_x) / 2);
        Y0 = ((y_Max + y_Min) / 2);
    }


    cout<<"X0:"<<X0<<"Y0:"<<Y0<<endl;
    a = sqrtf(powf((Max_x-Min_x),2)+powf((y_Max - y_Min),2)) / 2.0 ;

    for(int k=0; k<x_mats.size(); k++ )
    {
        float juli = sqrtf(powf((x_mats[k]-X0),2) + powf((y_mats[k] - Y0),2));
        if(juli<b)
        {
            b = juli;
        }
        if(juli>Max_a)
        {
            Max_a = juli;
        }
    }


     //a=Max_a;

    float tem;
    if(Vertical)
    {
        tem = a;
        a = b;
        b = tem;
    }

    float A = a*a;
    float B = b*b;


    float sum_0 = 0;
    for(int j = 0; j<x_mats.size(); j++)
    {
        float y =  powf((y_mats[j] - Y0),2) / B;
        float x =  powf((x_mats[j] - X0),2) / A;
        sum_0 += powf((1-x-y),2);
    }
    cout<<"sum_0:"<<sum_0<<endl;


    float theta =0;
    float s1 = 1.0f / x_mats.size();
    float learn = 1.0f;

    float* hmatrix = new float[x_mats.size()];
    memset(hmatrix,0,x_mats.size() * sizeof(float));



    //梯度下降
    float sum_min = 1000.0;
    int countes = 0;

    for(int i = 0;i<13140;i++)
    {
        bool kaiguang  = true;
        float sum_1 = 0,sum_2 = 0,sum_3 = 0,sum_4 = 0,sum_5 = 0,sum = 0;
        for(int j = 0; j<x_mats.size(); j++)
        {
            float cx = x_mats[j] - X0;
            float cy = y_mats[j] - Y0;
            float x = cx * cos(theta) - cy * sin(theta);
            float y = cx * sin(theta) + cy * cos(theta);


            x = (x * x) / powf(a,2);
            y = (y * y) / powf(b,2);
            //float y =  powf((y_mats[j] - Y0),2) / B;
            //float x =  powf((x_mats[j] - X0),2) / A;
            hmatrix[j] = 1 - x -y;
        }
        for(int j = 0; j<x_mats.size(); j++)
        {
            float cx = x_mats[j] - X0;
            float cy = y_mats[j] - Y0;
            float x = cx * cos(theta) - cy * sin(theta);
            float y = cx * sin(theta) + cy * cos(theta);


            sum_1 += hmatrix[j] * (2) * (x*cos(theta) / powf(a,2) + y*sin(theta) / powf(b,2));
            sum_2 += hmatrix[j] * (2) * (x*sin(theta) / powf(a,2) + y*cos(theta) / powf(b,2));
            sum_3 += hmatrix[j] * (2) * (powf(x,2) / powf(a,3));
            sum_4 += hmatrix[j] * (2) * (powf(y,2) / powf(b,3));
            sum_5 += hmatrix[j] * (2) * ((x*(-y)) / powf(a,2) + (y*x) / powf(b,2));


            sum += powf(hmatrix[j],2);
        }


        if(sum <sum_min)
        {
            sum_min = sum;
            kaiguang = false;
        }

        if(kaiguang)
        {
            if(fabs(sum - sum_min)<= 0.00001)
            {
                countes +=1;
                if(countes >= 11)
                {
                    break;
                }
            }
        }

        sum_1 = learn*s1*sum_1;
        sum_2 = learn*s1*sum_2;
        sum_3 = learn*s1*sum_3;
        sum_4 = learn*s1*sum_4;
        sum_5 = learn*s1*sum_5;

        // 更新 参数theta

        X0 = X0 - sum_1;
        Y0 = Y0 - sum_2;
        a = a - sum_3;
        b = b - sum_4;
        theta = theta + sum_5;



        cout<<"sum:"<<sum<<endl;


    }


    /**/


    for(int i = 0; i<inputImage->height()/2;i++)
    {
        for(int j = 25; j<inputImage->width()-10; j++)
        {

            /*
            float  y = sqrtf(b*b - ((b*b)/(a*a))*(j-X0)*(j-X0)) + Y0;
            y1 = Y0 - sqrtf(b*b - ((b*b)/(a*a))*(j-X0)*(j-X0));

            sum_0 += sqrtf(powf((y_mats[j] - y),2.0));
            if(y>0&&y<890)
            {
                inputImage->setPixel(j,int(y+0.3),qRgb(0,255,0));

            }
            if(y1>0&&y1<900)
                inputImage->setPixel(j,int(y1),qRgb(0,255,0));

            */
            float cx = j - X0;
            float cy = i - Y0;
            float x = cx * cos(theta) - cy * sin(theta);
            float y = cx * sin(theta) + cy * cos(theta);
            x = (x * x) / powf(a,2);
            y = (y * y) / powf(b,2);


            float result = x + y;


            if(abs(result -1)<0.01)
            {
                inputImage->setPixel(j,i,qRgb(0,255,0));
            }


        }
    }


    Show_Image(*inputImage);
    /*
     */




    ellipse[0] = X0;
    ellipse[1] = Y0;
    ellipse[2] = a;
    ellipse[3] = b;
    ellipse[4] = theta;


}

效果图:
在这里插入图片描述
误差值:
在这里插入图片描述

没有更多推荐了,返回首页