OpenCv NDK 银行卡/身份证号识别(3) 银行卡/身份证图像处理和卡号区域剪切

通过前两篇我们已经对Opencv有所了解了,接下来就要真正的来处理我们的图像,然后把卡号给提取出来。首先我们先简单分析以下银行卡然后把处理流程列出来:

 由上图我们很容易知道既然我们要找到卡号,银行卡的外边轮廓,然后根据比例找到卡号的位置,处理流程:

  1. 把采集到的图片根据银行卡边缘进行剪切,得到银行卡的区域
  2. 根据比例把卡号区域剪切出来,得到卡号的区域(具体怎么截取自己可以想不同的算法识别卡号的位置)
  3. 处理得到的卡号区域(降噪/二值化/膨胀/腐蚀等等)
  4. 分割每一个卡号(用于后期识别和训练模型使用)

基本流程就是这些,接下来就让我们从查找银行卡边缘开始:

1.对银行卡边缘进行裁剪

图像处理:

    // 首先降噪
    Mat blur;
    GaussianBlur(mat, blur, Size(5, 5), BORDER_DEFAULT, BORDER_DEFAULT);

    // 梯度增强 , x 轴和 y 轴
    Mat grad_x, grad_y;
    Scharr(blur, grad_x, CV_32F, 1, 0);
    Scharr(blur, grad_y, CV_32F, 0, 1);
    Mat grad_abs_x, grad_abs_y;
    convertScaleAbs(grad_x, grad_abs_x);
    convertScaleAbs(grad_y, grad_abs_y);
    Mat grad;
    addWeighted(grad_abs_x, 0.5, grad_abs_y, 0.5, 0, grad);

    // 把图片转成灰度图,减少图片的信息
    Mat gray;
    cvtColor(grad, gray, COLOR_BGRA2GRAY);
    // 二值化,待进行轮廓查找
    Mat binary;
    threshold(gray, binary, 40, 255, THRESH_BINARY);

效果图:

通过梯度增强把银行卡的边缘更加突出,为我们查找更准确的边缘轮廓矩形。这里二值化主要是去除一些背影信息,减少不必要的轮廓,方便查找。图片处理好之后接下来就是查找边缘轮廓了。

轮廓查找:

轮廓查找我们要筛选出符合要求的论然后返回

    // 轮廓查找
    vector<vector<Point> > contours;
    //查找轮廓
    findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    for (int i = 0; i < contours.size(); ++i) {
        Rect rect = boundingRect(contours[i]);
        //绘制轮廓
        drawContours(mat,contours,i,Scalar(0, 0, 255),1);
        // 是不是符合规则
        if (rect.width > mat.cols / 2 && rect.width != mat.cols && rect.height > mat.rows / 2) {
            card_rect = rect;
            break;
        }
        //防止轮廓都是不合尺寸的
        if (i == contours.size() - 1) {
            card_rect.x = 0;
            card_rect.y = 0;
            card_rect.width = binary.cols;
            card_rect.height = binary.rows;
        }
    }

剪切后的效果图红色是轮廓:

可以看到边缘已经根据外接矩形给剪切了。然后就是寻找卡号区域

2.剪切卡号区域

我这里是根据比例来剪切卡号区域的,你可以想其他办法去实现,那样剪切的区域跟准确,这里可以做持续优化,方法方式不限,只要能找到合适的卡号区域就行。

首先对剪切后的银行卡图片进行缩放,因为边框位置的不同剪切后银行卡区域可能图片大小不一致,所以先缩放到一定大小之后,按照比例剪切卡号区域

    //缩放
    int srcRows = 1388;
    int srcCols = 2201;
    Mat srcScale(srcRows, srcCols, srcImg.type());
    resize(srcImg, srcScale, srcScale.size(), 0, 0, INTER_LINEAR);
    //剪切矩形
    Rect cR;
    cR.x = srcScale.cols / 20;
    cR.y = srcScale.rows / 2;
    cR.width = srcScale.cols * 18 / 20;
    cR.height = srcScale.rows / 6;
    //剪切
    Mat rectMat(srcScale, cR);

可能不同的银行卡,卡号位置不一样,其实银行卡识别为了能达到更高的识别效率,需要写多套不同的图像处理代码,单单写一套代码识别率还是有限的,比较我们拍摄的角度/拍摄的光照/银行卡的样式多种多样,银行卡卡号有印刷的有突出的,银行卡背景又有很多不一样,所以在处理中其实还是要多写几套代码去适配。而对对于身份证的识别就非常简单了,因为身份证都是国家统一的。

3.处理卡号区域图像

得到卡号区域之后我们需要做的就是把卡号和背景分开,避免背景对识别产生影响,导致识别失败。所以我们要做一些图像处理:

    // 首先降噪 噪点可以选大一点
    Mat blur;
    GaussianBlur(rectMat, blur, Size(9, 9), BORDER_DEFAULT, BORDER_DEFAULT);
    //转灰度图
    Mat g;
    cvtColor(blur, g, COLOR_BGRA2GRAY); //转为灰度图
    //获取自定义核
    //第一个参数MORPH_RECT表示矩形的卷积核,当然还可以选择椭圆形的、交叉型的
    Mat element = getStructuringElement(MORPH_RECT, Size(5, 5));
    Mat erodeOut;
    //腐蚀操作
    erode(g, erodeOut, element);
    //二值化 THRESH_OTSU自动阀值
    Mat th;
    threshold(erodeOut, th, 39, 255, THRESH_OTSU);
    //膨胀
    Mat dilate_element = getStructuringElement(MORPH_RECT, Size(8, 8));
    Mat dilate_erodeOut;
    dilate(th, dilate_erodeOut, dilate_element);

从图像可以看出,还有些是干扰区域(注意这里可能你处理后的数字直接有粘练的问题,这个在这里就不说了,可以自己去研究),然后就是通过获取这个灰度图的所有矩形。

 vector<vector<Point> > contours;
    vector<Vec4i> hierarcy;
    findContours(dilate_erodeOut, contours, hierarcy, RETR_TREE, CHAIN_APPROX_NONE); //查找所有轮廓
    vector<Rect> boundRect(contours.size()); //定义外接矩形集合
    int x0 = 0, y0 = 0, w0 = 0, h0 = 0;
    int dilate_area = dilate_erodeOut.cols * dilate_erodeOut.rows;

    for (int i = 0; i < contours.size(); i++) {
        boundRect[i] = boundingRect(contours[i]); //查找每个轮廓的外接矩形
        x0 = boundRect[i].x;
        y0 = boundRect[i].y;
        w0 = boundRect[i].width;
        h0 = boundRect[i].height;
        //绘制第i个外接矩形
        rectangle(dilate_erodeOut, Point(x0, y0), Point(x0 + w0, y0 + h0), Scalar(0, 255, 0), 2, 8);

        int area = boundRect[i].area();
        if (h0 < dilate_erodeOut.rows * 2 / 5) {
            drawContours(dilate_erodeOut, contours, i, Scalar(0), 1);
            contours.erase(contours.begin() + i);
        } else if (area < dilate_area / 200) {
            drawContours(dilate_erodeOut, contours, i, Scalar(0), 1);
            contours.erase(contours.begin() + i);
        }
        // TODO : 根据高度和面积 处理腐蚀,伐值化,膨胀 不掉的干扰点。大面积的干扰点。
    }

但是我们会发现有很多的干扰矩形,就是除了我们数字区域之外的其他噪点区域,所以我们对得到的全部矩形做了“面积”,“高度”,大小的过滤。当然这里仅仅做了高度,面积的过滤其实还不够,还应该根据x,y坐标的特点进行再次过滤排除等等,这里可以自由发挥,根据自己的算法,可以做个更加优化,识别率更高。

过滤之后:

把我们的区域都找到了,把干扰区域都过滤掉了,生下来的就是我们的数字了。接下来就是把数字提取,分割出来,为识别和训练作数据采集。

4.分割每一个卡号

上面我们已经通过过滤得到了所有的卡号区域,下面就让我们把卡号区域给截取出来,然后保存到本地,为我们下一章节的训练使用:注意我们得到的矩形集合,他并不是有序的,就是如果我们直接按照集合內矩形的顺序截取,卡号是乱的,所有在截取之前需要根据x坐标排序。

    //排序
    for (int i = 0; i < split_mat.size() - 1; ++i) {

        for (int j = 0; j < split_mat.size() - 1 - i; ++j) {

            if (split_mat[j].x > split_mat[j + 1].x) {
                swapRect = split_mat[j];
                split_mat[j] = split_mat[j + 1];
                split_mat[j + 1] = swapRect;
            }
        }
    }

    //分割
  for (int i = 0; i < split_mat.size(); ++i) {
        //分割
        Mat sp(dilate_erodeOut, split_mat[i]);
        //缩放
        int nRows = 11;
        int nCols = 7;
        Mat dst(nRows, nCols, sp.type());
        resize(sp, dst, dst.size(), 0, 0, INTER_LINEAR);
        Mat bit;
        bitwise_not(dst, bit);
        //输出------训练数据提取样本时候使用
        char name[50];
        //坑 保存的时候图片尺寸不要太大(由于刚开始图片保存的是110:70的导致怎么训练都不成功),最好格式是png.
        sprintf(name,"/storage/emulated/0/dstImg_%d.png",i);
        imwrite(name, bit);
}

需要注意这里对分割的结果进行了缩放,起初我是直接保存的原始尺寸是110:70,但是在训练的时候始终训练失败,通过查找原因,一步一步的排除错误,最终发现图片太大。

下一篇就是拿到我们分割后的数字进行训练,这里你可以多分割几个卡号,多采集一些数据。

发布了119 篇原创文章 · 获赞 140 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/WangRain1/article/details/97394465