c – 使用OpenCV进行硬币的模板匹配

前端之家收集整理的这篇文章主要介绍了c – 使用OpenCV进行硬币的模板匹配前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
我正在进行一个项目,它将自动计算输入图像中的硬币值.到目前为止,我已经使用边缘检测和使用霍夫变换的一些预处理来分割硬币.

我的问题是如何从这里开始?我需要根据以前存储的一些功能对分割的图像进行一些模板匹配.我怎么能这样做呢.

我也读过一些叫做K-Nearest Neighbours的东西,我觉得这是我应该使用的东西.但我不太清楚如何使用它.

我遵循的研究文章

> Coin
Detector

> Coin
Recognition

解决方法

进行模式匹配的一种方法是使用cv :: matchTemplate.

这需要输入图像和作为模板的较小图像.它将模板与重叠图像区域进行比较,计算模板与重叠区域的相似性.有几种计算比较的方法.
方法不直接支持比例或方向不变性.但是可以通过将候选者缩放到参考大小并通过针对几个旋转模板进行测试来克服这一点.

示出了该技术的详细示例以检测50c硬币的存在和位置.相同的程序可以应用于其他硬币.
将建立两个计划.一个用于从50c硬币的大图像模板创建模板.另一个将输入这些模板以及带有硬币的图像作为输入,并将输出50c硬币被标记的图像.

模板制作者

#define TEMPLATE_IMG "50c.jpg"
#define ANGLE_STEP 30
int main()
{
    cv::Mat image = loadImage(TEMPLATE_IMG);
    cv::Mat mask = createMask( image );
    cv::Mat loc = locate( mask );
    cv::Mat imageCS;
    cv::Mat maskCS;
    centerAndScale( image,mask,loc,imageCS,maskCS);
    saveRotatedTemplates( imageCS,maskCS,ANGLE_STEP );
    return 0;
}

在这里,我们加载将用于构建模板的图像.
将其分段以创建蒙版.
找到所述面罩的质量中心.
然后我们重新缩放并复制那个面具和硬币,使它们形成一个固定大小的正方形,正方形的边缘触及面具和硬币的圆形.也就是说,正方形的边具有与缩放的掩模或硬币图像的直径相同的长度像素.
最后,我们保存硬币的缩放和居中图像.并且我们保存了它以固定角度增量旋转的更多副本.

cv::Mat loadImage(const char* name)
{
    cv::Mat image;
    image = cv::imread(name);
    if ( image.data==NULL || image.channels()!=3 )
    {
        std::cout << name << " could not be read or is not correct." << std::endl;
        exit(1);
    }
    return image;
}

loadImage使用cv :: imread来读取图像.验证数据已被读取,图像有三个通道并返回读取的图像.

#define THRESHOLD_BLUE  130
#define THRESHOLD_TYPE_BLUE  cv::THRESH_BINARY_INV
#define THRESHOLD_GREEN 230
#define THRESHOLD_TYPE_GREEN cv::THRESH_BINARY_INV
#define THRESHOLD_RED   140
#define THRESHOLD_TYPE_RED   cv::THRESH_BINARY
#define CLOSE_ITERATIONS 5
cv::Mat createMask(const cv::Mat& image)
{
    cv::Mat channels[3];
    cv::split( image,channels);
    cv::Mat mask[3];
    cv::threshold( channels[0],mask[0],THRESHOLD_BLUE,255,THRESHOLD_TYPE_BLUE );
    cv::threshold( channels[1],mask[1],THRESHOLD_GREEN,THRESHOLD_TYPE_GREEN );
    cv::threshold( channels[2],mask[2],THRESHOLD_RED,THRESHOLD_TYPE_RED );
    cv::Mat compositeMask;
    cv::bitwise_and( mask[0],compositeMask);
    cv::bitwise_and( compositeMask,compositeMask);
    cv::morphologyEx(compositeMask,compositeMask,cv::MORPH_CLOSE,cv::Mat(),cv::Point(-1,-1),CLOSE_ITERATIONS );

    /// Next three lines only for debugging,may be removed
    cv::Mat filtered;
    image.copyTo( filtered,compositeMask );
    cv::imwrite( "filtered.jpg",filtered);

    return compositeMask;
}

createMask执行模板的分段.它将每个BGR通道二值化,对这三个二值化图像进行AND,并执行CLOSE形态操作以生成掩模.
三个调试行使用计算的掩码作为复制操作的掩码将原始图像复制为黑色图像.这有助于为阈值选择合适的值.

在这里,我们可以看到由createMask中创建的掩码过滤的50c图像

cv::Mat locate( const cv::Mat& mask )
{
  // Compute center and radius.
  cv::Moments moments = cv::moments( mask,true);
  float area = moments.m00;
  float radius = sqrt( area/M_PI );
  float xCentroid = moments.m10/moments.m00;
  float yCentroid = moments.m01/moments.m00;
  float m[1][3] = {{ xCentroid,yCentroid,radius}};
  return cv::Mat(1,3,CV_32F,m);
}

locate计算掩模的质心及其半径.以{x,y,radius}的形式在单行mat中返回这3个值.
它使用cv :: moments来计算多边形或栅格化形状的三阶之前的所有时刻.在我们的案例中是一个光栅化的形状.我们对所有这些时刻都不感兴趣.但其中三个在这里很有用. M00是面具的区域.质心可以从m00,m10和m01计算出来.

void centerAndScale(const cv::Mat& image,const cv::Mat& mask,const cv::Mat& characteristics,cv::Mat& imageCS,cv::Mat& maskCS)
{
    float radius = characteristics.at<float>(0,2);
    float xCenter = characteristics.at<float>(0,0);
    float yCenter = characteristics.at<float>(0,1);
    int diameter = round(radius*2);
    int xOrg = round(xCenter-radius);
    int yOrg = round(yCenter-radius);
    cv::Rect roiOrg = cv::Rect( xOrg,yOrg,diameter,diameter );
    cv::Mat roiImg = image(roiOrg);
    cv::Mat roiMask = mask(roiOrg);
    cv::Mat centered = cv::Mat::zeros( diameter,CV_8UC3);
    roiImg.copyTo( centered,roiMask);
    cv::imwrite( "centered.bmp",centered); // debug
    imageCS.create( TEMPLATE_SIZE,TEMPLATE_SIZE,CV_8UC3);
    cv::resize( centered,cv::Size(TEMPLATE_SIZE,TEMPLATE_SIZE),0 );
    cv::imwrite( "scaled.bmp",imageCS); // debug

    roiMask.copyTo(centered);
    cv::resize( centered,0 );
}

centerAndScale使用由locate计算的质心和半径来获得输入图像的感兴趣区域和掩模的感兴趣区域,使得这些区域的中心也是硬币和掩模的中心以及边长.区域等于硬币/掩模的直径.
稍后将这些区域缩放到固定的TEMPLATE_SIZE.这个缩放区域将是我们的参考模板.当稍后在匹配程序中我们想要检查检测到的候选硬币是否是该硬币时,我们还将采用候选硬币的区域,在执行模板匹配之前以相同的方式居中并缩放该候选硬币.这样我们就可以实现尺度不变性.

void saveRotatedTemplates( const cv::Mat& image,int stepAngle )
{
    char name[1000];
    cv::Mat rotated( TEMPLATE_SIZE,CV_8UC3 );
    for ( int angle=0; angle<360; angle+=stepAngle )
    {
        cv::Point2f center( TEMPLATE_SIZE/2,TEMPLATE_SIZE/2);
        cv::Mat r = cv::getRotationMatrix2D(center,angle,1.0);

        cv::warpAffine(image,rotated,r,TEMPLATE_SIZE));
        sprintf( name,"template-%03d.bmp",angle);
        cv::imwrite( name,rotated );

        cv::warpAffine(mask,"templateMask-%03d.bmp",rotated );
    }
}

saveRotatedTemplates保存以前的计算模板.
但它保存了几个副本,每个副本旋转一个角度,在ANGLE_STEP中定义.这样做的目的是提供方向不变性.我们定义stepAngle越低,我们得到的方向不变性越好,但它也意味着更高的计算成本.

您可以下载整个模板制作程序here.
当使用ANGLE_STEP作为30运行时,我得到以下12个模板:
here7003

模板匹配.

#define INPUT_IMAGE "coins.jpg"
#define LABELED_IMAGE "coins_with50cLabeled.bmp"
#define LABEL "50c"
#define MATCH_THRESHOLD 0.065
#define ANGLE_STEP 30
int main()
{
    vector<cv::Mat> templates;
    loadTemplates( templates,ANGLE_STEP );
    cv::Mat image = loadImage( INPUT_IMAGE );
    cv::Mat mask = createMask( image );
    vector<Candidate> candidates;
    getCandidates( image,candidates );
    saveCandidates( candidates ); // debug
    matchCandidates( templates,candidates );
    for (int n = 0; n < candidates.size( ); ++n)
        std::cout << candidates[n].score << std::endl;
    cv::Mat labeledImg = labelCoins( image,candidates,MATCH_THRESHOLD,false,LABEL );
    cv::imwrite( LABELED_IMAGE,labeledImg );
    return 0;
}

这里的目标是读取模板和要检查的图像,并确定与我们的模板匹配的硬币的位置.

首先,我们读入了我们在前一个程序中生成的所有模板图像的图像矢量.
然后我们读取要检查的图像.
然后我们使用与模板制作者完全相同的功能对要检查的图像进行二值化.
getCandidates定位要形成多边形的点组.这些多边形中的每一个都是硬币的候选者.并且所有这些都被重新缩放并且以与我们的模板大小相等的方块为中心,以便我们可以以不变的方式执行匹配以进行缩放.
我们保存为调试和调整目的而获得的候选图像.
matchCandidates将每个候选者与为最佳匹配结果存储的所有模板匹配.由于我们有多个方向的模板,因此可以提供方向的不变性.
打印每个候选人的分数,以便我们可以决定将50c硬币与非50c硬币分开的阈值.
labelCoins复制原始图像,并在得分大于(或小于某些方法)MATCH_THRESHOLD中定义的阈值的标签上绘制标签.
最后我们将结果保存在.BMP中

void loadTemplates(vector<cv::Mat>& templates,int angleStep)
{
    templates.clear( );
    for (int angle = 0; angle < 360; angle += angleStep)
    {
        char name[1000];
        sprintf( name,angle );
        cv::Mat templateImg = cv::imread( name );
        if (templateImg.data == NULL)
        {
            std::cout << "Could not read " << name << std::endl;
            exit( 1 );
        }
        templates.push_back( templateImg );
    }
}

loadTemplates类似于loadImage.但它加载了几个图像而不是一个,并将它们存储在std :: vector中.

loadImage与模板制作者完全相同.

createMask也与tempate制作者完全相同.这次我们用几个硬币将它应用于图像.应该注意的是,选择二值化阈值来对50c进行二值化,并且这些阈值将无法正确地对图像中的所有硬币进行二值化.但这并不重要,因为该计划的目标只是识别50c硬币.只要这些被正确分割,我们就可以了.如果在这个分段中丢失了一些硬币,它实际上对我们有利,因为我们将节省评估它们的时间(只要我们只丢失不是50c的硬币).

typedef struct Candidate
{
    cv::Mat image;
    float x;
    float y;
    float radius;
    float score;
} Candidate;

void getCandidates(const cv::Mat& image,vector<Candidate>& candidates)
{
    vector<vector<cv::Point> > contours;
    vector<cv::Vec4i> hierarchy;
    /// Find contours
    cv::Mat maskCopy;
    mask.copyTo( maskCopy );
    cv::findContours( maskCopy,contours,hierarchy,CV_RETR_TREE,CV_CHAIN_APPROX_SIMPLE,cv::Point( 0,0 ) );
    cv::Mat maskCS;
    cv::Mat imageCS;
    cv::Scalar white = cv::Scalar( 255 );
    for (int nContour = 0; nContour < contours.size( ); ++nContour)
    {
        /// Draw contour
        cv::Mat drawing = cv::Mat::zeros( mask.size( ),CV_8UC1 );
        cv::drawContours( drawing,nContour,white,-1,8,cv::Point( ) );

        // Compute center and radius and area.
        // Discard small areas.
        cv::Moments moments = cv::moments( drawing,true );
        float area = moments.m00;
        if (area < CANDIDATES_MIN_AREA)
            continue;
        Candidate candidate;
        candidate.radius = sqrt( area / M_PI );
        candidate.x = moments.m10 / moments.m00;
        candidate.y = moments.m01 / moments.m00;
        float m[1][3] = {
            { candidate.x,candidate.y,candidate.radius}
        };
        cv::Mat characteristics( 1,m );
        centerAndScale( image,drawing,characteristics,maskCS );
        imageCS.copyTo( candidate.image );
        candidates.push_back( candidate );
    }
}

getCandidates的核心是cv :: findContours,它可以找到输入图像中存在的区域的轮廓.这是以前计算过的掩码.
findContours返回轮廓矢量.每个轮廓本身是点的矢量,其形成检测到的多边形的外线.
每个多边形限定每个候选硬币的区域.
对于每个轮廓,我们使用cv :: drawContours在黑色图像上绘制填充的多边形.
使用此绘制的图像,我们使用前面解释的相同过程来计算多边形的质心和半径.
我们使用与模板制作者中使用的相同功能的centerAndScale,将该poligon中包含的图像居中并缩放到与我们的模板大小相同的图像中.通过这种方式,我们以后甚至可以对来自不同比例的照片的硬币进行适当的匹配.
这些候选硬币中的每一个都以候选结构复制,其中包含:

>候选人形象
> x和y表示质心
>半径
>得分

getCandidates计算除分数之外的所有这些值.
在编写候选人之后,将其放入候选人矢量中,这是我们从getCandidates获得的结果.

这些是获得的4名候选人:

void saveCandidates(const vector<Candidate>& candidates)
{
    for (int n = 0; n < candidates.size( ); ++n)
    {
        char name[1000];
        sprintf( name,"Candidate-%03d.bmp",n );
        cv::imwrite( name,candidates[n].image );
    }
}

saveCandidates保存计算的候选者以调试purpouses.并且我也可以在这里发布这些图像.

void matchCandidates(const vector<cv::Mat>& templates,vector<Candidate>& candidates)
{
    for (auto it = candidates.begin( ); it != candidates.end( ); ++it)
        matchCandidate( templates,*it );
}

matchCandidates只为每个候选人调用matchCandidate.完成后,我们将计算所有候选人的分数.

void matchCandidate(const vector<cv::Mat>& templates,Candidate& candidate)
{
    /// For SQDIFF and SQDIFF_NORMED,the best matches are lower values. For all the other methods,the higher the better
    candidate.score;
    if (MATCH_METHOD == CV_TM_SQDIFF || MATCH_METHOD == CV_TM_SQDIFF_NORMED)
        candidate.score = FLT_MAX;
    else
        candidate.score = 0;
    for (auto it = templates.begin( ); it != templates.end( ); ++it)
    {
        float score = singleTemplateMatch( *it,candidate.image );
        if (MATCH_METHOD == CV_TM_SQDIFF || MATCH_METHOD == CV_TM_SQDIFF_NORMED)
        {
            if (score < candidate.score)
                candidate.score = score;
        }
        else
        {
            if (score > candidate.score)
                candidate.score = score;
        }
    }
}

matchCandidate具有单个候选项和所有模板作为输入.它的目标是将每个模板与候选人匹配.该工作被委托给singleTemplateMatch.
我们存储获得的最佳分数,其中CV_TM_SQDIFF和CV_TM_SQDIFF_NORMED是最小的,而其他匹配方法是最大的分数.

float singleTemplateMatch(const cv::Mat& templateImg,const cv::Mat& candidateImg)
{
    cv::Mat result( 1,1,CV_8UC1 );
    cv::matchTemplate( candidateImg,templateImg,result,MATCH_METHOD );
    return result.at<float>( 0,0 );
}

singleTemplateMatch执行匹配.
cv :: matchTemplate使用两个输入图像,第二个小于或等于第一个.
常见的用例是将一个小模板(第二个参数)与一个较大的图像(第一个参数)进行匹配,结果是浮动的二维Mat,其中模板沿着图像匹配.找到此浮点数Mat的最大值(或最小值取决于方法),我们在第一个参数的图像中获得模板的最佳候选位置.
但是我们对在图像中定位模板不感兴趣,我们已经有了候选人的坐标.
我们想要的是获得候选人和模板之间的相似度.这就是为什么我们以不太常见的方式使用cv :: matchTemplate;我们这样做的第一个参数图像的大小等于第二个参数模板.在这种情况下,结果是尺寸为1×1的垫子.而Mat中的单个值是我们的相似(或不相称)得分.

for (int n = 0; n < candidates.size( ); ++n)
    std::cout << candidates[n].score << std::endl;

我们打印每个候选人获得的分数.
在此表中,我们可以看到cv :: matchTemplate可用的每种方法的分数.最好的成绩是绿色.

CCORR和CCOEFF给出了错误的结果,因此丢弃了这两个结果.在剩余的4种方法中,两种SQDIFF方法是在最佳匹配(50c)和第二最佳(不是50c)之间具有较高相对差异的方法.这就是我选择它们的原因.
我选择了SQDIFF_NORMED,但没有充分的理由.为了真正选择一种方法,我们应该用更高的样本进行测试,而不仅仅是一种.
对于该方法,工作阈值可以是0.065.选择合适的阈值也需要许多样本.

bool selected(const Candidate& candidate,float threshold)
{
    /// For SQDIFF and SQDIFF_NORMED,the higher the better
    if (MATCH_METHOD == CV_TM_SQDIFF || MATCH_METHOD == CV_TM_SQDIFF_NORMED)
        return candidate.score <= threshold;
    else
        return candidate.score>threshold;
}

void drawLabel(const Candidate& candidate,const char* label,cv::Mat image)
{
    int x = candidate.x - candidate.radius;
    int y = candidate.y;
    cv::Point point( x,y );
    cv::Scalar blue( 255,128,128 );
    cv::putText( image,label,point,CV_FONT_HERSHEY_SIMPLEX,1.5f,blue,2 );
}

cv::Mat labelCoins(const cv::Mat& image,const vector<Candidate>& candidates,float threshold,bool inverseThreshold,const char* label)
{
    cv::Mat imageLabeled;
    image.copyTo( imageLabeled );

    for (auto it = candidates.begin( ); it != candidates.end( ); ++it)
    {
        if (selected( *it,threshold ))
            drawLabel( *it,imageLabeled );
    }

    return imageLabeled;
}

labelCoins在候选位置绘制一个标签字符串,其分数大于(或小于取决于方法)阈值.
最后,labelCoins的结果保存了

cv::imwrite( LABELED_IMAGE,labeledImg );

结果是:

硬币匹配器的整个代码可以在here下载.

这是一个好方法吗?
这很难说.
方法是一致的.它正确检测样品的50c硬币和提供的输入图像.
但我们不知道该方法是否稳健,因为它没有经过适当的样本大小测试.更重要的是针对在编码程序时不可用的样本进行测试,这是在使用足够大的样本大小完成时的稳健性的真实度量.
我对没有银币误报的方法很有信心.但我不太确定像20c这样的其他铜币.从我们可以从所获得的分数中看出,20c硬币获得的分数与50c非常相似.
在不同的光照条件下也会发生假阴性.如果我们能够控制照明条件,例如当我们设计机器拍摄硬币照片并计算它们时,哪些是可以而且应该避免的.

如果该方法起作用,则可以对每种类型的硬币重复相同的方法,从而导致所有硬币的完全检测.

猜你在找的C&C++相关文章