身份证号码图像提取--基于canny边缘检测的连通域检测算法
Posted 小田田_XOW
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了身份证号码图像提取--基于canny边缘检测的连通域检测算法相关的知识,希望对你有一定的参考价值。
在之前扫描二维码提取任务之后,工作中又需要将身份证图像中的身份证号码提取出来,然后给同事调用进行识别。之前的连通域检测算法比较“蛮力”,因为它一旦检测出一个大的区域,那么这区域中的所有内部区域都将不复存在了。所以在连通域检测时,需要第一步去掉周围可能存在的白边,否则就会失败。后来笔者换了一个思路,如果检测一个区域时保存对应生成该区域的点,该区域不符合要求的话就将这些点擦掉,从而就不会影响到内部的区域了。于是就有了一下算法的诞生:
(1)从左上角开始,从碰到的第一个白点开始探测最大的连通域,获取离该点小于max_dis的所有点,放到一个list中。
(2)然后遍历该列表,并将离每一个点距离小于max_dis的点都放到该list中。
(3)遍历结束后,计算包含list中所有点的最小rect区域。
(4)根据设定的目标区域特点,如长宽、长宽比等,来判断该区域是否满足要求,如果满足,则放到rectlist中。然后将该list中的所有点都置黑。转到(1)执行。
(5)如果rectlist为空,则没有获取到目标rect。如果>=1 则将之按照一个规则进行排序(应该是那个最主要的特征),然后输出最可能的那个rect。
算法过程演示如下:
原图:
色彩过滤(为了得到效果好一点的canny图):
canny图:
检测画框与擦除:
第一次 画框:
第一次擦除:
第二次画框:
第二次擦除
第n次画框:
第n次擦除:
最后的什么都没剩下:
得出结果:
详细算法代码如下:
FindIdCode.h
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc_c.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include < io.h>
#include <algorithm>
#include <stdio.h>
#include "opencv/cv.h"
#include "opencv/cxcore.h"
#include "opencv2/highgui/highgui_c.h"
#include "direct.h"
using namespace cv;
using namespace std;
class CGetIDCOde
{
public:
CGetIDCOde();
//删除文件 并返回string 值
string getFilePath( const char * szBuf);
//获取文件长度
long GetFileLength(const char * filepath);
//过滤颜色
void FilterColor(string strImgFileName);
//找到目标连通域
RECT FindTargetConnectedDomain();
//将list中的点都设置成某一个颜色
void SetPointListColor(Mat & srcImg, std::vector<cv::Point> pointList, int nColor);
//根据点列表获取最小包含区域
void GetRectFromPointList(std::vector<cv::Point>& pointList, RECT & rtRect);
//获取与该点临近的点
void GetNearPoint(Mat & srcImg,cv::Point currentPoint, std::vector<cv::Point> & pointList);
//将一个box框画成某一个颜色
void DrowBoxColor(Mat &srcImg, std::vector<RECT> &boxList, int nColor);
//获取一个联通区域
BOOL GetOneConnectedDomain(Mat & srcImg, std::vector<cv::Point>& pointList, RECT &rect);
//将图像的某一个区域保存为图像
void SavePicWithDestRect(string strSource, string strDest, RECT destRect);
//获取身份证号图像区域
RECT GetIdCode(const char * szSourceFile);
//边缘检测
int outLinePic2();
char szCurrentPath[MAX_PATH];
string strOrigin;
string strSave1;
string strSave1_1;
string strSave2;
string strSave3;
string strSave4;
string strSave5;
string strSave3_0;
string strSave3_1;
string strSave3_2;
string strSave3_3;
string strSave6;
string strSave7;
string strSave8;
};
FindIdCode.cpp
#include "FindIdCode.h"
int mMAX_DIS = 0;
double fScale = 0.0;
#define BOX_WIDTH 50
#define BLACK 0
#define MID_BLACK_WHITE 128
#define WHITE 255
#define RATE 0.2
//按照框的宽度排序
BOOL SortByM5(RECT &v1, RECT &v2)
{
int nWidth1 = v1.right - v1.left;
int nHeight1 = v1.bottom - v1.top;
int nWidth2 = v2.right - v2.left;
int nHeight2 = v2.bottom - v2.top;
float fRate1 = 1.0 * nWidth1 / nHeight1;
float fRate2 = 1.0 * nWidth2 / nHeight2;
if (fRate1 > fRate2)
{
return TRUE;
}
else
{
return FALSE;
}
}
string CGetIDCOde::getFilePath( const char * szBuf)
{
string str;
str = szCurrentPath;
str += "\\\\";
str += szBuf;
//删除已经存在的文件
DeleteFile(str.c_str());
return str;
}
long CGetIDCOde::GetFileLength(const char * filepath)
{
FILE* file = fopen(filepath, "rb");
if (file)
{
long size = filelength(fileno(file));
return size;
}
else
{
return 0;
}
}
//颜色过滤
void CGetIDCOde::FilterColor(string strImgFileName)
{
uchar uDifferMax = 80;
uchar rMax = 100;
uchar bMax = 150;
uchar gMax = 150;
uchar uWhite = 255;
uchar r,b,g;
IplImage *workImg = cvLoadImage(strImgFileName.c_str(), CV_LOAD_IMAGE_UNCHANGED);
//像素太高的进行缩放
if (workImg->width > 900)
{
int nTargetWidth = 600;
fScale = 1.0 * workImg->width / nTargetWidth;
CvSize czSize;
//计算目标图像大小
czSize.width = nTargetWidth;
czSize.height = workImg->height / fScale;
//IplImage *pSrcImage = cvLoadImage(strSave2.c_str(), CV_LOAD_IMAGE_UNCHANGED);
IplImage *pDstImage = cvCreateImage(czSize, workImg->depth, workImg->nChannels);
cvResize(workImg, pDstImage, CV_INTER_AREA);
cvReleaseImage(&workImg);
cvSaveImage(strSave1_1.c_str(),pDstImage);
workImg = pDstImage;
}
for(int x=0;x<workImg->height;x++)
{
for(int y=0;y<workImg->width;y++)
{
b=((uchar*)(workImg->imageData+x*workImg->widthStep))[y*3+0];
g=((uchar*)(workImg->imageData+x*workImg->widthStep))[y*3+1];
r=((uchar*)(workImg->imageData+x*workImg->widthStep))[y*3+2];
//偏色比较严重的
//uchar uMax = max(max(b,g),r);
//uchar uMin = min(min(b,g),r);
//if ( uMax - uMin > uDifferMax)
int nAbove = 0;
if (b >= uDifferMax)
{
nAbove ++;
}
if (g >= uDifferMax)
{
nAbove ++;
}
if (r >= uDifferMax)
{
nAbove ++;
}
//有两个大于80
if(nAbove >= 2 || b > bMax || g > gMax || r > rMax)
{
((uchar*)(workImg->imageData+x*workImg->widthStep))[y*3+0] = uWhite;
((uchar*)(workImg->imageData+x*workImg->widthStep))[y*3+1] = uWhite;
((uchar*)(workImg->imageData+x*workImg->widthStep))[y*3+2] = uWhite;
}
}
}
cvSaveImage(strSave1.c_str(), workImg);
}
int CGetIDCOde::outLinePic2()
{
Mat src = imread(strSave1.c_str());
Mat dst;
if (!src.empty())
{
//输入图像
//输出图像
//输入图像颜色通道数
//x方向阶数
//y方向阶数
Sobel(src,dst,src.depth(),1,1);
//imwrite("sobel.jpg",dst);
//输入图像
//输出图像
//输入图像颜色通道数
Laplacian(src,dst,src.depth());
imwrite("laplacian.jpg",dst);
//输入图像
//输出图像
//彩色转灰度
cvtColor(src,src,CV_BGR2GRAY); //canny只处理灰度图
//输入图像
//输出图像
//低阈值
//高阈值,opencv建议是低阈值的3倍
//内部sobel滤波器大小
//threshold1和threshold2 当中的小阈值用来控制边缘连接,大的阈值用来控制强边缘的初始分割。50 150
Canny(src,dst,220,240,3);
imwrite(strSave2.c_str(),dst);
return 0;
}
else
{
cout<< "IMG is not exist!";
return -1;
}
}
void CGetIDCOde::SetPointListColor(Mat & srcImg, std::vector<cv::Point> pointList, int nColor)
{
for (int i = 0; i < pointList.size(); i ++)
{
int x = pointList[i].x;
int y = pointList[i].y;
*(srcImg.data + srcImg.step[0] * y + srcImg.step[1] * x) = nColor;
}
}
RECT CGetIDCOde::FindTargetConnectedDomain()
{
Mat srcImg = imread(strSave2.c_str(), CV_LOAD_IMAGE_GRAYSCALE);
//设定最大的距离
mMAX_DIS = srcImg.cols * (1.0 * 9 / 400) + 1;
int nMaxWidth = 0.6 * srcImg.cols;
int nMaxHeight = 1.0 * 5 * srcImg.rows / 36 ;
std::vector<cv::Point> pointList;
//探测一个矩形连通域,判断是否符合目标特征,不符合删除找下一个。
//找到一个放入vector中。
std::vector<RECT> targetRectList;
while(TRUE)
{
RECT rect;
GetOneConnectedDomain(srcImg, pointList,rect);
//判断该rect是否符合要求。
int nWidth = rect.right - rect.left;
int nHeight = rect.bottom - rect.top;
// 300 20
float fRate = 1.0 * nWidth / nHeight;
if (nHeight > 5 && nHeight < nMaxHeight && nWidth > 100 && nWidth < nMaxWidth && fRate > 8 && fRate < 20)
{
//SavePicWithDestRect(strOrigin, strSave8, rect);
targetRectList.push_back(rect);
//break;
}
else
{
if (pointList.empty())
{
break;
}
}
//置黑然后找下一个
SetPointListColor(srcImg, pointList, BLACK);
imwrite(strSave3_3.c_str(),srcImg);
pointList.clear();
}
//有多个排序
if (targetRectList.size() > 0)
{
sort(targetRectList.begin(), targetRectList.end(), SortByM5);
//找到 提取图像 保存。
RECT rect = targetRectList[0];
rect.left -= mMAX_DIS;
if (rect.left < 0)
{
rect.left = 0;
}
rect.top -= mMAX_DIS;
if (rect.top < 0)
{
rect.top = 0;
}
rect.right += mMAX_DIS;
if (rect.right > srcImg.cols)
{
rect.right = srcImg.cols;
}
rect.bottom += mMAX_DIS;
if (rect.bottom > srcImg.rows)
{
rect.bottom = srcImg.rows;
}
if (fScale > 0.0)
{
rect.left *= fScale;
rect.right*= fScale;
rect.bottom *= fScale;
rect.top *= fScale;
}
return rect;
//SavePicWithDestRect(strOrigin, strSave8, rect);
}
else
{
//cout<< "find no numbers!";
//getchar();
RECT rect;
rect.bottom = rect.top = rect.left = rect.right = 0;
return rect;
}
}
//保存图像
void CGetIDCOde::SavePicWithDestRect(string strSource, string strDest, RECT destRect)
{
IplImage* src;
IplImage* dst;
src = cvLoadImage(strSource.c_str(),1);
if(!src)
{
return ;
}
cvSetImageROI(src,cvRect(destRect.left,destRect.top ,destRect.right - destRect.left, destRect.bottom - destRect.top));
dst = cvCreateImage(cvSize(destRect.right - destRect.left, destRect.bottom - destRect.top),
IPL_DEPTH_8U,
src->nChannels);
cvCopy(src,dst,0);
cvResetImageROI(src);
cvSaveImage(strDest.c_str(), dst);
cvReleaseImage(&dst);
cvReleaseImage(&src);
}
BOOL CGetIDCOde::GetOneConnectedDomain(Mat & srcImg, std::vector<cv::Point>& pointList, RECT &rect)
{
int nWidth = srcImg.cols;
int nHeight = srcImg.rows;
int nXStart = 0;
int nYStart = 0;
BOOL bBlack = TRUE;
BOOL bBreak = FALSE;
int nWhite = 0;
//找到第一个最上角的白点
for (int y = 0; y < nHeight; y ++)
{
for (int x = 0; x < nWidth; x++)
{
int nPixel = (int)(*(srcImg.data + srcImg.step[0] * y + srcImg.step[1] * x));
if (nPixel > MID_BLACK_WHITE)
{
nXStart = x;
nYStart = y;
cv::Point tempPint(nXStart,nYStart);
pointList.push_back(tempPint);
bBreak = TRUE;
break;
}
}
if (bBreak)
{
break;
}
}
int nSize = pointList.size();
//探测下一个点。
for (int i = 0; i < nSize; i ++)
{
cv::Point currentPoint = pointList[i];
GetNearPoint(srcImg, currentPoint, pointList);
nSize = pointList.size();
//如果超过4000个点则删除后重新再来
if (nSize > 3000)
{
break;
}
}
//对该pointList求最小包含的矩形框。
GetRectFromPointList(pointList, rect);
std::vector<RECT> tempTect;
tempTect.push_back(rect);
DrowBoxColor(srcImg,tempTect, WHITE);
imwrite(strSave3_2.c_str(),srcImg);
DrowBoxColor(srcImg,tempTect, BLACK);
return TRUE;
}
void CGetIDCOde::GetRectFromPointList(std::vector<cv::Point>& pointList, RECT & rtRect)
{
int nLeft = 0;
int nTop = 0;
int nRight = 0;
int nBottom = 0;
for(int i = 0; i < pointList.size(); i ++)
{
cv::Point tempPoint = pointList[i];
if (i == 0)
{
nLeft = nRight = tempPoint.x;
nTop = nBottom = tempPoint.y;
}
else
{
if (tempPoint.x < nLeft)
{
nLeft = tempPoint.x;
}
if (tempPoint.x > nRight)
{
nRight = tempPoint.x;
}
if (tempPoint.y < nTop)
{
nTop = tempPoint.y;
}
if (tempPoint.y > nBottom)
{
nBottom = tempPoint.y;
}
}
}
rtRect.left = nLeft;
rtRect.top = nTop;
rtRect.right = nRight;
rtRect.bottom = nBottom;
}
void CGetIDCOde::GetNearPoint(Mat & srcImg,cv::Point currentPoint, std::vector<cv::Point> & pointList)
{
//探测以该点为中心的 20 * 20范围的点。
for (int y = max(0, currentPoint.y - mMAX_DIS); y < min(srcImg.rows, currentPoint.y + mMAX_DIS); y ++)
{
for (int x = max(currentPoint.x - mMAX_DIS, 0); x < min(srcImg.cols, currentPoint.x + mMAX_DIS); x ++)
{
int nPixel = (int)(*(srcImg.data + srcImg.step[0] * y + srcImg.step[1] * x));
if (nPixel > MID_BLACK_WHITE)
{
cv::Point tempPint(x, y);
//看该点是否已经放入list
std::vector<cv::Point>::iterator itFind = find( pointList.begin(), pointList.end(),tempPint);
if (itFind == pointList.end())
{
pointList.push_back(tempPint);
}
}
}
}
}
//画框线为一个颜色
void CGetIDCOde::DrowBoxColor(Mat &srcImg, std::vector<RECT> &boxList, int nColor)
{
int nResultSize = boxList.size();
for (int i = 0; i < nResultSize; i ++)
{
RECT tempRect = boxList[i];
//上下边线
int y1 = tempRect.top;
int y2 = tempRect.bottom;
for (int x = tempRect.left; x <= tempRect.right; x ++)
{
*(srcImg.data + srcImg.step[1] * x + srcImg.step[0] * y1) = nColor;
*(srcImg.data + srcImg.step[1] * x + srcImg.step[0] * y2) = nColor;
}
//左右边线
int x1 = tempRect.left;
int x2 = tempRect.right;
for (int y = tempRect.top; y <= tempRect.bottom; y ++)
{
*(srcImg.data + srcImg.step[1] * x1 + srcImg.step[0] * y) = nColor;
*(srcImg.data + srcImg.step[1] * x2 + srcImg.step[0] * y) = nColor;
}
}
}
RECT CGetIDCOde::GetIdCode(const char * szSourceFile)
{
CopyFile(szSourceFile, strOrigin.c_str(), FALSE);
//文件大小 过小则不进行图像过滤
RECT rect;
rect.bottom = rect.top = rect.left = rect.right = 0;
long nFileLen = GetFileLength(strOrigin.c_str());
if (nFileLen == 0)
{
return rect;
}
else if (nFileLen > 7000 )
{
FilterColor(strOrigin);
}
else
{
CopyFile(strOrigin.c_str(), strSave1.c_str(),FALSE );
}
if (outLinePic2() == -1)
{
return rect;
}
return FindTargetConnectedDomain();
}
CGetIDCOde::CGetIDCOde()
{
_getcwd(szCurrentPath,MAX_PATH);
strOrigin = getFilePath("imageText.jpg");
strSave1 = getFilePath("imageText_D.jpg");
strSave1_1 = getFilePath("imageText_ReSize.jpg");
strSave2 = getFilePath("canny.jpg");
strSave3 = getFilePath("imageText_Clear0.jpg");
strSave4 = getFilePath("imageText_Clear1.jpg");
strSave5 = getFilePath("imageText_Clear2.jpg");
strSave3_0 = getFilePath("imageText_Clear3_0.jpg");
strSave3_1 = getFilePath("imageText_Clear3_1.jpg");
strSave3_2 = getFilePath("imageText_Clear3_2.jpg");
strSave3_3 = getFilePath("imageText_Clear3_3.jpg");
strSave6 = getFilePath("imageText_Clear3.jpg");
strSave7 = getFilePath("imageText_D.jpg");
strSave8 = getFilePath("imageText_Clear4.jpg");
}
类的测试代码:
#include "../FindIdCode/FindIdCode.h"
using namespace std;
#ifdef _DEBUG
#pragma comment(lib, "Debug/FindIdCode.lib")
#else
#pragma comment(lib, "Release/FindIdCode.lib")
#endif
int main(int argc, char **argv)
{
if(argc < 2)
return(1);
CGetIDCOde getIdcode;
//char* szSourceFile = "D:\\\\scan\\\\00000000000000000\\\\3032_024.jpg";
//dll测试
char* szSourceFile = argv[1];
RECT rect = getIdcode.GetIdCode(szSourceFile);
//CopyFile(szSourceFile,strOrigin.c_str(), FALSE);
getIdcode.SavePicWithDestRect(szSourceFile, getIdcode.strSave8, rect);
cout<<"the rect is "<<rect.left<<" "<<rect.top<<" "<<rect.bottom<<" "<<rect.right<<" ";
return 0;
}
说明:
由于不断的进行循环检测,如果像素过高图片太大则耗时较多,而且边缘检测效果特别不好,所以程序中对于像素宽度大于900的则缩放到400。
程序运行效果的好坏直接影响因数是 canny图片的效果。所以对于不同特点的图片,可以调整canny函数的参数,如本例中采用的参数是:Canny(src,dst,220,240,3)。
色彩过滤:由于身份证有很多蓝色和红色的底纹,将rgb过大的色彩变成了白色。有时候并不一定会有好的效果,反而会让边缘增多,反而影响结果。另外如果图像特别模糊,最好也不要进行色彩过滤。
最后还是需要提醒一下opencv的环境问题。
以上是关于身份证号码图像提取--基于canny边缘检测的连通域检测算法的主要内容,如果未能解决你的问题,请参考以下文章
数字图像分析基于Python实现 Canny Edge Detection(Canny 边缘检测算法)