【问题标题】:Robust card detection/persecutive correction OpenCV强大的卡片检测/精确校正 OpenCV
【发布时间】:2014-05-17 00:27:44
【问题描述】:

我目前有一种方法可以检测图像中的卡片,并且在大多数情况下,当照明相当一致且背景非常平静时,它就可以工作。

这是我用来执行此操作的代码:

    Mat img = inImg.clone();
    outImg = Mat(inImg.size(), CV_8UC1);
    inImg.copyTo(outImg);

    Mat img_fullRes = img.clone();

    pyrDown(img, img);

    Mat imgGray;
    cvtColor(img, imgGray, CV_RGB2GRAY);
    outImg_gray = imgGray.clone();
    // Find Edges //

    Mat detectedEdges = imgGray.clone();

    bilateralFilter(imgGray, detectedEdges, 0, 185, 3, 0);
    Canny( detectedEdges, detectedEdges, 20, 65, 3 );

    dilate(detectedEdges, detectedEdges, Mat::ones(3,3,CV_8UC1));
    Mat cdst = img.clone();

    vector<Vec4i> lines;
    HoughLinesP(detectedEdges, lines, 1, CV_PI/180, 60, 50, 3 );
    for( size_t i = 0; i < lines.size(); i++ )
    {
        Vec4i l = lines[i];
        // For debug
        //line( cdst, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), Scalar(0,0,255), 1);
    }
    //cdst.copyTo(inImg);

//    // Find points of intersection //

    cv::Rect imgROI;
    int ext = 10;
    imgROI.x = ext;
    imgROI.y = ext;
    imgROI.width = img.size().width - ext;
    imgROI.height = img.size().height - ext;

    int N = lines.size();
    // Creating N amount of points // N == lines.size()
    cv::Point** poi = new cv::Point*[N];
    for( int i = 0; i < N; i++ )
        poi[i] = new cv::Point[N];
    vector<cv::Point> poiList;
    for( int i = 0; i < N; i++ )
    {
        poi[i][i] = cv::Point(-1,-1);
        Vec4i line1 = lines[i];
        for( int j = i + 1; j < N; j++ )
        {
            Vec4i line2 = lines[j];
            cv::Point p = computeIntersect(line1, line2, imgROI);

            if( p.x != -1 )
            {
                //line(cdst, p-cv::Point(2,0), p+cv::Point(2,0), Scalar(0,255,0));
                //line(cdst, p-cv::Point(0,2), p+cv::Point(0,2), Scalar(0,255,0));

                poiList.push_back(p);
            }

            poi[i][j] = p;
            poi[j][i] = p;
        }
    }

    cdst.copyTo(inImg);

    if(poiList.size()==0)
    {
        outImg = inImg.clone();
        //circle(outImg, cv::Point(100,100), 50, Scalar(255,0,0), -1);
        return;
    }

    convexHull(poiList, poiList, false, true);

    for( int i=0; i<poiList.size(); i++ )
    {
        cv::Point p = poiList[i];
        //circle(cdst, p, 3, Scalar(255,0,0), 2);
    }
     //Evaluate all possible quadrilaterals

    cv::Point cardCorners[4];
    float metric_max = 0;
    int Npoi = poiList.size();
    for( int p1=0; p1<Npoi; p1++ )
    {
        cv::Point pts[4];
        pts[0] = poiList[p1];

        for( int p2=p1+1; p2<Npoi; p2++ )
        {
            pts[1] = poiList[p2];
            if( isCloseBy(pts[1],pts[0]) )
                continue;

            for( int p3=p2+1; p3<Npoi; p3++ )
            {
                pts[2] = poiList[p3];
                if( isCloseBy(pts[2],pts[1]) || isCloseBy(pts[2],pts[0]) )
                    continue;


                for( int p4=p3+1; p4<Npoi; p4++ )
                {
                    pts[3] = poiList[p4];
                    if( isCloseBy(pts[3],pts[0]) || isCloseBy(pts[3],pts[1])
                       || isCloseBy(pts[3],pts[2]) )
                        continue;


                    // get the metrics
                    float area = getArea(pts);

                    cv::Point a = pts[0]-pts[1];
                    cv::Point b = pts[1]-pts[2];
                    cv::Point c = pts[2]-pts[3];
                    cv::Point d = pts[3]-pts[0];
                    float oppLenDiff = abs(a.dot(a)-c.dot(c)) + abs(b.dot(b)-d.dot(d));

                    float metric = area - 0.35*oppLenDiff;
                    if( metric > metric_max )
                    {
                        metric_max = metric;
                        cardCorners[0] = pts[0];
                        cardCorners[1] = pts[1];
                        cardCorners[2] = pts[2];
                        cardCorners[3] = pts[3];
                    }

                }
            }
        }
    }

    // find the corners corresponding to the 4 corners of the physical card
    sortPointsClockwise(cardCorners);

    // Calculate Homography //

    vector<Point2f> srcPts(4);
    srcPts[0] = cardCorners[0]*2;
    srcPts[1] = cardCorners[1]*2;
    srcPts[2] = cardCorners[2]*2;
    srcPts[3] = cardCorners[3]*2;


    vector<Point2f> dstPts(4);
    cv::Size outImgSize(1400,800);

    dstPts[0] = Point2f(0,0);
    dstPts[1] = Point2f(outImgSize.width-1,0);
    dstPts[2] = Point2f(outImgSize.width-1,outImgSize.height-1);
    dstPts[3] = Point2f(0,outImgSize.height-1);

    Mat Homography = findHomography(srcPts, dstPts);

    // Apply Homography
    warpPerspective( img_fullRes, outImg, Homography, outImgSize, INTER_CUBIC );
    outImg.copyTo(inImg);

其中computeIntersect定义为:

cv::Point computeIntersect(cv::Vec4i a, cv::Vec4i b, cv::Rect ROI)
{
    int x1 = a[0], y1 = a[1], x2 = a[2], y2 = a[3];
    int x3 = b[0], y3 = b[1], x4 = b[2], y4 = b[3];

    cv::Point  p1 = cv::Point (x1,y1);
    cv::Point  p2 = cv::Point (x2,y2);
    cv::Point  p3 = cv::Point (x3,y3);
    cv::Point  p4 = cv::Point (x4,y4);
    // Check to make sure all points are within the image boundrys, if not reject them.
    if( !ROI.contains(p1) || !ROI.contains(p2)
       || !ROI.contains(p3) || !ROI.contains(p4) )
        return cv::Point (-1,-1);

    cv::Point  vec1 = p1-p2;
    cv::Point  vec2 = p3-p4;

    float vec1_norm2 = vec1.x*vec1.x + vec1.y*vec1.y;
    float vec2_norm2 = vec2.x*vec2.x + vec2.y*vec2.y;
    float cosTheta = (vec1.dot(vec2))/sqrt(vec1_norm2*vec2_norm2);

    float den = ((float)(x1-x2) * (y3-y4)) - ((y1-y2) * (x3-x4));
    if(den != 0)
    {
        cv::Point2f pt;
        pt.x = ((x1*y2 - y1*x2) * (x3-x4) - (x1-x2) * (x3*y4 - y3*x4)) / den;
        pt.y = ((x1*y2 - y1*x2) * (y3-y4) - (y1-y2) * (x3*y4 - y3*x4)) / den;

        if( !ROI.contains(pt) )
            return cv::Point (-1,-1);

        // no-confidence metric
        float d1 = MIN( dist2(p1,pt), dist2(p2,pt) )/vec1_norm2;
        float d2 = MIN( dist2(p3,pt), dist2(p4,pt) )/vec2_norm2;

        float no_confidence_metric = MAX(sqrt(d1),sqrt(d2));

        // If end point ratios are greater than .5 reject
        if( no_confidence_metric < 0.5 && cosTheta < 0.707 )
            return cv::Point (int(pt.x+0.5), int(pt.y+0.5));
    }

    return cv::Point(-1, -1);
}

sortPointsClockWise 定义为:

void sortPointsClockwise(cv::Point a[])
{
    cv::Point b[4];

    cv::Point ctr = (a[0]+a[1]+a[2]+a[3]);
    ctr.x /= 4;
    ctr.y /= 4;
    b[0] = a[0]-ctr;
    b[1] = a[1]-ctr;
    b[2] = a[2]-ctr;
    b[3] = a[3]-ctr;

    for( int i=0; i<4; i++ )
    {
        if( b[i].x < 0 )
        {
            if( b[i].y < 0 )
                a[0] = b[i]+ctr;
            else
                a[3] = b[i]+ctr;
        }
        else
        {
            if( b[i].y < 0 )
                a[1] = b[i]+ctr;
            else
                a[2] = b[i]+ctr;
        }
    }

}

getArea 定义为:

float getArea(cv::Point arr[])
{
    cv::Point  diag1 = arr[0]-arr[2];
    cv::Point  diag2 = arr[1]-arr[3];

    return 0.5*(diag1.cross(diag2));
}

isCloseBy 定义为:

bool isCloseBy( cv::Point p1, cv::Point p2 )
{
    int D = 10;
    // Checking that X values are within 10, same for Y values.
    return ( abs(p1.x-p2.x)<=D && abs(p1.y-p2.y)<=D );
}

最后是dist2:

float dist2( cv::Point p1, cv::Point p2 )
{
    return float((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y));
}

以下是几张测试图像及其结果:

很抱歉,这篇文章很长,但我希望有人能提出一种方法,让我从图像中提取卡片的方法更加健壮。一种可以更好地处理破坏性背景以及不一致的照明。

当卡片放置在具有良好照明的对比背景上时,我的方法几乎 90% 的时间都有效。但很明显,我需要一种更稳健的方法。

有人有什么建议吗?

谢谢。

dhanushka 独奏的尝试

Mat gray, bw;     pyrDown(inImg, inImg);
cvtColor(inImg, gray, CV_RGB2GRAY);
int morph_size = 3;
Mat element = getStructuringElement( MORPH_ELLIPSE, cv::Size( 4*morph_size + 1, 2*morph_size+1 ), cv::Point( morph_size, morph_size ) );

morphologyEx(gray, gray, 2, element);
threshold(gray, bw, 160, 255, CV_THRESH_BINARY);

vector<vector<cv::Point> > contours;
vector<Vec4i> hierarchy;
findContours( bw, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0) );

int largest_area=0;
int largest_contour_index=0;
cv::Rect bounding_rect;

for( int i = 0; i< contours.size(); i++ )
{
    double a=contourArea( contours[i],false);  //  Find the area of contour
    if(a>largest_area){
        largest_area=a;
        largest_contour_index=i;                //Store the index of largest contour
        bounding_rect=boundingRect(contours[i]);
    }
}

//Scalar color( 255,255,255);
rectangle(inImg, bounding_rect,  Scalar(0,255,0),1, 8,0);
Mat biggestRect = inImg(bounding_rect);
Mat card1 = biggestRect.clone();

【问题讨论】:

标签: c++ opencv image-processing edge-detection


【解决方案1】:

图像处理的艺术(根据我 10 多年的经验)就是:一门艺术。没有单一的答案存在,而且总是有不止一种方法可以做到这一点。而且它在某些情况下肯定会失败。

根据我在医学图像中自动检测特征的经验,构建可靠的算法需要很长时间,但事后看来,最好的结果是使用相对简单的算法获得的。但是,了解这个简单的算法需要很多时间。

为此,一般方法始终相同:

  • 开始是建立一个大型的测试图像数据库(至少 100 个)。这定义了应该工作的“正常”图像。通过收集图像,您已经开始思考问题。
  • 对图像进行注释以构建一种“基本事实”。在这种情况下,“基本事实”应该包含卡片的 4 个角,因为这些是有趣的点。
  • 创建一个应用程序,在这些图像上运行算法并将结果与​​基本事实进行比较。在这种情况下,“与地面实况比较”将采用找到的 4 个角点与地面实况角点的平均距离。
  • 输出一个以制表符分隔的文件,称为 .xls,因此可以通过双击在 Excel 中打开(在 Windows 上)。很高兴快速了解这些案例。首先看最坏的情况。然后手动打开这些案例,尝试了解它们为什么不起作用。
  • 现在您可以更改算法了。改变一些东西,然后重新运行。将新 Excel 工作表与旧 Excel 工作表进行比较。现在您开始意识到必须做出的权衡。

话虽如此,我认为在算法的调优过程中你需要回答这些问题:

  • 你允许一些折叠的牌吗?所以没有完全直线?如果是这样,请更多地关注角落而不是线条/边缘。
  • 您是否允许灯光逐渐变化?如果是这样,本地对比度拉伸过滤器可能会有所帮助。
  • 是否允许卡片与背景颜色相同?如果是这样,您必须专注于卡片的内容而不是卡片的边框。
  • 是否允许使用不完美的镜头?如果有,具体到什么程度?
  • 您允许轮换卡片吗?如果有,具体到什么程度?
  • 背景的颜色和/或纹理是否应该统一?
  • 相对于图像尺寸,最小的可检测卡片应该有多小?如果您假设应覆盖至少 80% 的宽度或高度,那么您将恢复稳健性。
  • 如果图像中可见多张卡片,算法应该是稳健的并且只选择一张,还是任何输出都可以?
  • 如果看不到卡,是否应该检测到这种情况?内置检测这种情况将使其更加用户友好(“未找到卡”),但也不太可靠。

这些将对要获取的图像提出要求和假设。您可以依赖的假设非常强大:如果您选择正确的假设,它们会使算法快速、稳健且简单。也让这些要求和假设成为测试数据库的一部分。

那么我会选择什么?根据您提供的三张图片,我将从以下内容开始:

  • 假设卡片将图像从 50% 填充到 100%。
  • 假设卡片最多旋转 10 度左右。
  • 假设角落清晰可见。
  • 假设卡片的纵横比(高度除以宽度)在 1/3 到 3 之间。
  • 假设背景中没有类似卡片的对象

算法如下所示:

  • 使用角过滤器在图像的每个象限中检测特定角。所以在图像的左上象限是卡片的左上角。例如查看 http://www.ee.surrey.ac.uk/CVSSP/demos/corners/results3.html ,或使用 OpenCV 函数,如 cornerHarris
  • 为了更加稳健,每个象限计算多个角。
  • 尝试通过组合每个象限的点来构建每个象限一个角的平行四边形。创建一个适应度函数,为以下各项提供更高的分数:

    • 内角接近 90 度
    • 变大
    • (可选)根据照明或其他特征比较卡片的角。

    这个适应度函数在以后提供了很多调整的可能性。

  • 返回得分最高的平行四边形。

那么为什么要使用角检测而不是霍夫变换来进行线检测呢?在我看来,霍夫变换(除了很慢)对背景中的图案非常敏感(这是您在第一张图像中看到的 - 它检测到背景中的线条比卡片的更强),并且它不能处理好一点曲线,除非你使用更大的 bin 大小,这会恶化检测。

祝你好运!

【讨论】:

    【解决方案2】:

    一个更通用的方法肯定会像 Rutger Nijlunsing 在他的回答中建议的那样。但是,在您的情况下,至少对于提供的示例图像,一种非常简单的方法(例如形态学打开,然后是阈值处理、轮廓处理和凸包)会产生您想要的结果。使用缩小版本的图像进行处理,这样您就不必使用大内核进行形态学操作。以下是经过这种处理的图像。

        pyrDown(large, rgb0);
        pyrDown(rgb0, rgb0);
        pyrDown(rgb0, rgb0);
    
        Mat small;
        cvtColor(rgb0, small, CV_BGR2GRAY);
    
        Mat morph;
        Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(11, 11));
        morphologyEx(small, morph, MORPH_OPEN, kernel);
    
        Mat bw;
        threshold(morph, bw, 0, 255.0, CV_THRESH_BINARY | CV_THRESH_OTSU);
    
        Mat bdry;
        kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
        erode(bw, bdry, kernel);
        subtract(bw, bdry, bdry);
    
        // do contour processing on bdry
    

    这种方法通常不起作用,所以我强烈推荐 Rutger 建议的方法。

    【讨论】:

    • 我几乎可以在所有背景上使用白卡,但无法使用彩色卡。你能提供你使用的代码吗?我已经用我的尝试更新了我的问题。
    • 我已经编辑了我的帖子给你一些建议。您需要对代码进行少量更改。
    猜你喜欢
    • 1970-01-01
    • 2021-06-05
    • 1970-01-01
    • 1970-01-01
    • 2023-04-10
    • 2018-08-30
    • 1970-01-01
    • 1970-01-01
    • 2020-03-05
    相关资源
    最近更新 更多