在图像上检测硬币(并拟合椭圆)

45

我目前正在一个项目中尝试检测放在平面上(比如桌子上)的几个硬币。硬币不会重叠或被其他物体覆盖,但是可能会有其他可见物体并且光照条件可能不完美...基本上,可以把它想象成拍摄桌子上有硬币的情况。

因此每个硬币应该作为一个椭圆形可见。由于我不知道相机的位置,椭圆形的形状可能会因拍摄角度而异,从圆形(俯视图)到扁平椭圆形。

我的问题是我不确定如何提取硬币,并最终拟合椭圆形(我需要进行进一步计算)。

目前,我仅尝试了在OpenCV中设置阈值值,使用findContours()获取轮廓线并拟合椭圆形的第一次尝试。不幸的是,轮廓线很少给我硬币的形状(反射,糟糕的光照等),而且这种方法也不理想,因为我不希望用户设置任何阈值。

另一个想法是在图像上使用椭圆形的模板匹配方法,但是由于我不知道相机的角度和椭圆形的大小,我认为这种方法可能效果不佳...

因此,我想请问是否有人能告诉我在我的情况下会起作用的方法。

是否有一种快速从图像中提取三个硬币的方法?计算应该实时进行在移动设备上,并且该方法不应该对不同或变化的光照或背景颜色过于敏感。

如果有人能给我任何关于哪种方法适合我的提示就太好了。


1
你为什么想要“拟合椭圆”?只“找到椭圆”不就足够了吗? - Dr. belisarius
3个回答

49

这里是一些实现传统方法的C99源代码(基于OpenCV doco):

#include "cv.h"
#include "highgui.h"

#include <stdio.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

//
// We need this to be high enough to get rid of things that are too small too
// have a definite shape.  Otherwise, they will end up as ellipse false positives.
//
#define MIN_AREA 100.00    
//
// One way to tell if an object is an ellipse is to look at the relationship
// of its area to its dimensions.  If its actual occupied area can be estimated
// using the well-known area formula Area = PI*A*B, then it has a good chance of
// being an ellipse.
//
// This value is the maximum permissible error between actual and estimated area.
//
#define MAX_TOL  100.00

int main( int argc, char** argv )
{
    IplImage* src;
    // the first command line parameter must be file name of binary (black-n-white) image
    if( argc == 2 && (src=cvLoadImage(argv[1], 0))!= 0)
    {
        IplImage* dst  = cvCreateImage( cvGetSize(src), 8, 3 );
        CvMemStorage* storage = cvCreateMemStorage(0);
        CvSeq* contour = 0;    
        cvThreshold( src, src, 1, 255, CV_THRESH_BINARY );
        //
        // Invert the image such that white is foreground, black is background.
        // Dilate to get rid of noise.
        //
        cvXorS(src, cvScalar(255, 0, 0, 0), src, NULL);
        cvDilate(src, src, NULL, 2);    
        cvFindContours( src, storage, &contour, sizeof(CvContour), CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE, cvPoint(0,0));
        cvZero( dst );

        for( ; contour != 0; contour = contour->h_next )
        {
            double actual_area = fabs(cvContourArea(contour, CV_WHOLE_SEQ, 0));
            if (actual_area < MIN_AREA)
                continue;

            //
            // FIXME:
            // Assuming the axes of the ellipse are vertical/perpendicular.
            //
            CvRect rect = ((CvContour *)contour)->rect;
            int A = rect.width / 2; 
            int B = rect.height / 2;
            double estimated_area = M_PI * A * B;
            double error = fabs(actual_area - estimated_area);    
            if (error > MAX_TOL)
                continue;    
            printf
            (
                 "center x: %d y: %d A: %d B: %d\n",
                 rect.x + A,
                 rect.y + B,
                 A,
                 B
            );

            CvScalar color = CV_RGB( rand() % 255, rand() % 255, rand() % 255 );
            cvDrawContours( dst, contour, color, color, -1, CV_FILLED, 8, cvPoint(0,0));
        }

        cvSaveImage("coins.png", dst, 0);
    }
}

给定 Carnieri 提供的二进制图像,这是输出结果:
./opencv-contour.out coin-ohtsu.pbm
center x: 291 y: 328 A: 54 B: 42
center x: 286 y: 225 A: 46 B: 32
center x: 471 y: 221 A: 48 B: 33
center x: 140 y: 210 A: 42 B: 28
center x: 419 y: 116 A: 32 B: 19

这是输出图像:

coins

你可以改进的地方:

  • 处理不同的椭圆方向(目前,我假设轴是垂直/水平的)。使用图像矩很容易做到这一点。
  • 检查对象的凸性(查看cvConvexityDefects

区分硬币和其他物体的最佳方法可能是通过形状。我想不出任何其他低级图像特征(颜色显然不行)。所以,我能想到两种方法:

传统目标检测

您的第一个任务是将对象(硬币和非硬币)与背景分离。如Carnieri建议的Ohtsu方法在这里效果很好。您似乎担心图像是二分图,但我认为这不会成为问题。只要桌面上有足够多的可见区域,您就保证有一个峰值在直方图中。只要桌面上有几个视觉上可区分的物体,您就保证有第二个峰值。

膨胀您的二值图像几次,以消除阈值处理留下的噪声。硬币相对较大,因此它们应该经受得起这种形态学操作。

使用区域生长将白色像素分组为对象--只需迭代地连接相邻的前景像素。在此操作结束时,您将拥有一系列不相交的对象,并且您将知道每个对象占用哪些像素。

从这些信息中,您可以了解到物体的宽度和高度(来自上一步)。因此,现在您可以估计包围物体的椭圆的大小,然后看看这个特定的物体与椭圆匹配程度如何。也许只使用宽度与高度比率更容易。
或者,您可以使用以更精确的方式确定物体的形状。

这听起来非常不错!我将尝试第一种方法,看看它的效果如何...如果结果不够理想,我会尝试另一种方法...所以你说的模板匹配是指垂直缩放图像,希望椭圆变成圆形,然后可以尝试模板匹配或甚至是霍夫变换在圆上...到目前为止谢谢! - florianbaethge
感谢您的代码...您发布的示例看起来非常棒...我会尝试以这种方式处理...如果相机保持水平,轴仅为水平/垂直...但也许我可以从手机传感器中获得它...谢谢! - florianbaethge
不用谢。我删除了解释中的图像金字塔部分,因为我认为轮廓检测会更好。但是,总体思路是通过缩放来将椭圆变成圆形,然后进行模板匹配。不确定是否要使用霍夫变换——可能需要更多时间才能实现。 - mpenkov
太棒了!计算机视觉的一个伟大之处在于我们可以轻松地可视化结果,就像你的输出图像所展示的那样。有一点需要注意:我同意 evident 的观点,即椭圆的方向始终大致相同,因为它们位于同一平面上。然而,透视畸变可能会因摄像机与硬币之间的距离而显著不同。 - carnieri
正确,它们应该大多数是对齐的...因为硬币彼此之间距离不太远,相机也不会出现透视失真,所以这种失真可能是可以接受的。目前唯一的反驳是当相机倾斜时,但我可以通过使用手机传感器来纠正这个问题。谢谢大家! - florianbaethge
最后,不要忘记并非所有的硬币都是圆形的:http://lunaticg.blogspot.com/2010/03/kids-project-different-shape-coin.html - mpenkov

6

如果以后还有人像我一样遇到这个问题,但是使用C++:

一旦你使用findContours找到轮廓(就像Misha的答案一样),你可以很容易地使用fitEllipse来拟合椭圆,例如:

    vector<vector<Point> > contours;

    findContours(img, contours, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0,0));

    RotatedRect rotRecs[contours.size()];

    for (int i = 0; i < contours.size(); i++) {
        rotRecs[i] = fitEllipse(contours[i]);
    }

6

我不知道你的问题最佳解决方案是什么。但是,关于阈值化,你可以使用Otsu算法,它会根据图像直方图的分析自动找到最优阈值。使用OpenCV的threshold方法,将参数ThresholdType设置为THRESH_OTSU

需要注意的是,Otsu算法只在具有双峰直方图(例如,亮物体在暗背景上的图像)的图像中有效。

你可能已经看过了,但是还有一种方法可以拟合一个椭圆到一组二维点上(例如,一个连通组件)。

编辑:应用Otsu算法到示例图像:

灰度图像: grayscale image

应用Otsu算法后的结果: Otsu image


啊,对了,我听说过大津算法。看起来很不错,但像你说的,它只能用于二分直方图,在大多数情况下,我不会有这种情况,因为通常还有其他物体可见。我已经在这张图片上尝试了一种分水岭算法,效果不错,但相应的问题是要追踪硬币。我可以让用户在开始时选择硬币和桌子,然后自动检测硬币。因此,使用卡尔曼滤波器或粒子滤波器跟踪标记点可能不起作用,因为运动可能不稳定。您有其他想法或提示吗? - florianbaethge
你之前没有提到过跟踪。你需要跟踪它们是因为它们在移动还是相机在移动?我猜是后者。尝试使用OpenCV附带的blobtrack_sample.cpp。我用它来跟踪移动车辆得到了不错的结果,但在那种情况下有很多好的特征(角落)可以跟踪,对于硬币可能会更困难一些。 - carnieri
跟踪只是一个想法,如果我使用分水岭算法。用户首先选择硬币,然后它们被跟踪,以便我可以在每个时间步骤使用分水岭算法来分割硬币。但由于相机移动不稳定,我怀疑跟踪效果不佳。如果没有跟踪的方法能够很好地工作,我仍然更喜欢这种方法... - florianbaethge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接