OpenCV机读卡识别

一、简单介绍

编写一个基于OpenCV的小程序,用于识别下图所示机读卡。

二、步骤回顾

(一) 图像处理

1. 图像二值化

图像识别离不开图像的处理。用相机拍摄的机读卡基本都是三通道的彩色图像,而这里需要用到的处理方法就是“图像二值化”,整个图像呈现出明显的只有黑和白的视觉效果,便于进行图像分割。现将图片先转换为灰度图,然后再进行二值化。

Mat loadImage(char *path)
{
    Mat src = imread(path);
    if (src.empty())
    {
        cout << "IMAGE LOAD FAILED!" << endl;
        exit(0);
    }
    Mat gray, binary;
    cvtColor(src, gray, CV_BGR2GRAY);
    adaptiveThreshold(gray, binary, 255, 0, 1, 101, 10);
    return binary;
}

处理效果:

2.ROI分割

想要获得数字信息和选择题答案,就要定位到ROI(region of interest,感兴趣区域)将其单独分割出来。显而易见,图1的ROI即矩形框框住的区域,也是整个图像中连通域最大的区域。获得最大连通域的方法如下:

Mat gaintComponent(Mat src)
{
    //查找连通域
    vector<vector<Point>>contours;
    findContours(src, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
    //最大连通域
    vector<Point>maxContour;
    double maxArea = 0.0;
    for (size_t i = 0; i < contours.size(); i++)
    {
        double area = cv::contourArea(contours[i]);
        if (area > maxArea)
        {
            maxArea = area;
            maxContour = contours[i];
        }
    }
    //转换为矩形框(boundingbox)
    Rect maxRect = boundingRect(maxContour);
    Mat result = src(maxRect);
    return result;
}

分割结果:

(1)选择题部分分割

根据图3的分割效果将图像反色,再次提取最大连通域即可定位到选择题方框,接着再次反色让背景变成黑色。然后对得到的图像进行简单的裁剪,去掉上方的分割线,降低识别误差。
ps:二值图中白色为1,黑色为0。处理二值图图像时计算机只看得见白色,所以需要反色操作。

//定位到选择题部分
Mat roiBox = gaintComponent(src);
Mat choiceBox = gaintComponent(~roiBox);
choiceBox = ~choiceBox;
//选择题部分边界处理
choiceBox = choiceBox(Rect(0, 15, choiceBox.cols, choiceBox.rows / 2 - 22));

分割效果:

(2)数字部分分割

//定位选择题上方数字信息
Mat roiImg = gaintComponent(src);
Mat infoBox = roiImg(Rect(0, 10, roiImg.cols, roiImg.rows / 4 - 10));
vector<vector<cv::Point>> contours, numBox;
cv::findContours(infoBox, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
for (size_t i = 0; i < contours.size(); i++)
{
    double area = cv::contourArea(contours[i]);
    if (area > 100)
        numBox.push_back(contours[i]);
}
vector<Mat>num;
for (size_t i = 0; i < numBox.size(); i++)
{
    cv::Rect rect = cv::boundingRect(numBox[i]);
    cv::Mat result = infoBox(rect);
    num.push_back(result);
}

该代码的问题:对于上方数字的分割用的是固定值的方法,只适用于图1大小的机读卡图片,不能自适应的调整分割区域

分割效果:

(三)识别

1. 选择题识别

根据图4的分割效果,可以明显的看出涂抹了选项的选项部分连通域明显大于未涂抹部分,所以这里可以很轻易的得到40道选择题的涂抹区域的相对位置。将图4分割为10x20的矩阵,每个位置对于一个区域信息,从而得到涂抹结果。

代码实现:

    //定位涂抹区间连通域
    vector<vector<cv::Point>> contours, answer;
    cv::findContours(choiceBox, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
    for (size_t i = 0; i < contours.size(); i++)
    {
        double area = cv::contourArea(contours[i]);
        if (area > 20 && area < 60)
            answer.push_back(contours[i]);
    }
    //存储涂抹中心点
    vector<Moments> mu(answer.size());
    for (int i = 0; i < answer.size(); i++)
        mu[i] = moments(answer[i], false);
    //  计算中心矩:
    vector<Point2f> mc(answer.size());
    for (int i = 0; i < answer.size(); i++)
        mc[i] = Point2f(mu[i].m10 / mu[i].m00, mu[i].m01 / mu[i].m00);
    //得到答案(使用选择题模板,涂抹部分的答案用小写字母标识)
    char answers[10][20] = {
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' },
    { ' ','A','B','C','D',' ','A','B','C','D', ' ','A','B','C','D', ' ','A','B','C','D' } };
    int x = choiceBox.cols / 20;
    int y = choiceBox.rows / 10;
    for (size_t i = 0; i < mc.size(); i++)
    {
        int x_index = mc[i].x / x;
        int y_index = mc[i].y / y;
        answers[y_index][x_index] += 32;
    }
    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; j < 20; j++)
        {
            if (answers[i][j] >= 'a'&&answers[i][j] <= 'd')
                cout << answers[i][j] << " ";
        }
        cout << endl;
    }

2. 数字识别

数字识别相较于选择题识别更加复杂一点,需要进行模板匹配,当然也可以用机器学习的相关方法实现。这里我使用的是较为简单一点的模板识别。这是我自己做的一个匹配模板:

按照选择题识别的方法,将0~9的数字放在一个图像上,通过OpenCV提供的匹配函数定位识别区域中心的位置。为了使识别更精准,现将待匹配的数字图像提取其最大连通域进行匹配。下面是代码实现的过程:

//method: 
//CV_TM_SQDIFF      =0,
//CV_TM_SQDIFF_NORMED =1,
//CV_TM_CCORR         =2,
//CV_TM_CCORR_NORMED  =3,
//CV_TM_CCOEFF        =4,
//CV_TM_CCOEFF_NORMED =5
int numMatch(Mat src, int method)
{
    //模板简单处理
    Mat model = imread("model.jpg", CV_BGR2GRAY);
    resize(model, model, Size(src.cols * 10, src.rows));//调整模板至便于匹配的大小
    //取数字部分连通域,排除干扰因素
    src = gaintComponent(~src);
    src = ~src;
    //单独识别1,因为1的连通域最小,长宽比大
    if (src.rows / src.cols > 2)
        return 1;
    //带识别图像处理(单通道变三通道)
    cv::Mat three_ch = Mat::zeros(src.rows, src.cols, CV_8UC3);
    vector<Mat>channels;
    for (int i = 0; i < 3; i++)
        channels.push_back(src);
    merge(channels, three_ch);
    //用matchTemplate()函数匹配数字
    Mat result;
    matchTemplate(model, three_ch, result, method);
    normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());
    //通过匹配的区域的中心点坐标定位数字
    double minVal, maxVal;
    Point minLoc, maxLoc;
    minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat());
    int ans = (minLoc.x + three_ch.cols / 2) / (model.cols / 10);
    return ans;
}

(四)运行结果

猜你喜欢

转载自www.cnblogs.com/linzijie1998/p/11031965.html