前言
1.前面項目我寫了如何檢測到手持身份證的正面、反面、頭像,那接下要試的是用OpenCV去等到身份證號碼的區域。
2.我這裏用到的OpenCV的版本是3.30,IDE是Qt和VS2015。
3.這個代碼好多地方是借鑑了車牌識別那個開源項目。
代碼
1.把傳入的圖像分離成只有R通道的圖像
//獲取R通道
//傳入一個剪切好的身份證,返回一個只有R這個通道的圖像
void getRChannel(const Mat &src, Mat &dst)
{
//容器大小爲通道數3
vector<Mat> split_BGR(src.channels());
//通道分離
split(src, split_BGR);
if (src.cols > 700 | src.cols >600)
{
Mat resizeR(450, 600, CV_8UC1);
cv::resize(split_BGR[2], resizeR, resizeR.size());
dst = resizeR.clone();
}
else
{
dst = split_BGR[2].clone();
}
}
運行結果:
2.得到號碼區域
//傳入一個單通道的圖像,得到一個旋轉矩形的號碼區域
void posDetect(const Mat &src, vector<RotatedRect> & rects)
{
Mat threshold_R;
//二值化
OstuBeresenThreshold(src, threshold_R);
#ifdef DEBUG
imshow("二值化", threshold_R);
#endif
//新建一個全白的圖像
Mat reversal_img(src.size(), src.type(), cv::Scalar(255));
//相減得到反轉的圖像
Mat threshold_reversal = reversal_img - threshold_R;
#ifdef DEBUG
imshow("反轉黑白", threshold_reversal);
#endif
//形態學閉操作的結構元素
Mat element = getStructuringElement(MORPH_RECT, Size(15, 3));
//閉運算
morphologyEx(threshold_reversal, threshold_reversal, CV_MOP_CLOSE, element);
#ifdef DEBUG
imshow("閉操作", threshold_reversal);
#endif
vector< vector <Point> > contours;
//輪廓檢測
findContours(threshold_reversal, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
//對得到的輪廓進行進一步篩選
vector< vector <Point> > ::iterator itc = contours.begin();
while (itc != contours.end())
{
//返回每個輪廓的最小有界矩形區域
RotatedRect mr = minAreaRect(Mat(*itc));
//判斷矩形輪廓是否符合要求
if (!isNumber(mr))
{
//刪除
itc = contours.erase(itc);
}
else
{
rects.push_back(mr);
++itc;
}
}
#ifdef DEBUG
//測試是否找到了號碼區域
Mat result;
result = src.clone();
Point2f vertices[4];
for (int j = 0; j < rects.size(); j++)
{
rects[j].points(vertices);
for (int i = 0; i < 4; i++)
{
//畫線
line(result, vertices[i], vertices[(i + 1) % 4], Scalar(0, 0, 0));
}
imshow("號碼區域", result);
}
#endif
}
//判斷是否爲數字區域(這個函數從車牌識別引用而來)
bool isNumber(const RotatedRect &candidate)
{
float error = 0.2;
//長寬比
const float aspect = 4.5 / 0.3;
//最小區域
int min = 10 * aspect * 10;
//最大區域
int max = 50 * aspect * 50;
//考慮誤差後的最小長寬比
float rmin = aspect - aspect*error;
//考慮誤差後的最大長寬比
float rmax = aspect + aspect*error;
int area = candidate.size.height * candidate.size.width;
float r = (float)candidate.size.width / (float)candidate.size.height;
if (r < 1)
{
r = 1 / r;
}
//滿足該條件才認爲該號碼區域
if ((area < min || area > max) || (r< rmin || r > rmax))
{
return false;
}
else
{
return true;
}
}
//二值化,輸入爲單通道,輸出一個二值圖像
void OstuBeresenThreshold(const Mat &src, Mat &out)
{
//otsu獲得全局閾值
double ostu_T = threshold(src, out, 0, 255, CV_THRESH_OTSU);
double min;
double max;
minMaxIdx(src, &min, &max);
const double CI = 0.12;
double beta = CI*(max - min + 1) / 128;
double beta_lowT = (1 - beta)*ostu_T;
double beta_highT = (1 + beta)*ostu_T;
Mat doubleMatIn;
src.copyTo(doubleMatIn);
int rows = doubleMatIn.rows;
int cols = doubleMatIn.cols;
double Tbn;
for (int i = 0; i < rows; ++i)
{
//獲取第 i行首像素指針
uchar * p = doubleMatIn.ptr<uchar>(i);
uchar *outPtr = out.ptr<uchar>(i);
//對第i 行的每個像素(byte)操作
for (int j = 0; j < cols; ++j)
{
if (i <2 | i>rows - 3 | j<2 | j>rows - 3)
{
if (p[j] <= beta_lowT)
{
outPtr[j] = 0;
}
else
{
outPtr[j] = 255;
}
}
else
{
//窗口大小25*25
Tbn = sum(doubleMatIn(Rect(i - 2, j - 2, 5, 5)))[0] / 25;
if (p[j] < beta_lowT | (p[j] < Tbn && (beta_lowT <= p[j] && p[j] >= beta_highT)))
{
outPtr[j] = 0;
}
if (p[j] > beta_highT | (p[j] >= Tbn && (beta_lowT <= p[j] && p[j] >= beta_highT)))
{
outPtr[j] = 255;
}
}
}
}
}
運行效果:
紅線是爲了方便顯示效果加的,黑色那個框就是程序得到的號碼區域。
結語
關於整個工程的源碼,運行程序時的bug,或者有如何優化的想法都可以加之前我博客後面提到的羣,相互討論學習。