opencv 练手项目:ISBN 号识别系统
首先需要说明的是,这个系统是我们大二下学期的二级项目。
正因为是二级项目,所以老师要求我们不能使用现成的库(如 zbar)和现有的算法(如 KNN 算法)。
所幸,老师给的图片也并不复杂,类似下图:
我们需要做的工作便是找到并截取红框区域,将字符分割然后识别。
大体思路:
- 倾斜图像修正
- 截取 ISBN 号所在区域
- 字符分割
- 字符识别
1. 倾斜图像修正
- 截取 ISBN 号所在行
- 字符分割
- 字符识别
详细代码:
#include<opencv.hpp>
#include<iostream>
#include<vector>
#include<string>
using namespace cv;
using namespace std;
//计算修正角度
double GetTurnTheta(Mat inputImg) {
//计算垂直方向导数
Mat yImg;
Sobel(inputImg, yImg, -1, 0, 1, 5);
//直线检测
vector<Vec2f>lines;
HoughLines(yImg, lines, 1, CV_PI / 180, 180);
//计算旋转角度
float thetas = 0;
for (int i = 0; i < lines.size(); i++) {
float theta = lines[i][1];
thetas += theta;
}
if (lines.size() == 0) {//未检测到直线
thetas = CV_PI / 2;
}
else {//检测到直线,取平均值
thetas /= lines.size();
}
return thetas;
}
//寻找 ISBN 所在行
void FindRowRanges(Mat inputImg, int thresh, int mnRow, int mxRow, int mnsize, int &st, int &ed) {
//边缘检测,方便找到梯度大的地方,忽略梯度小的地方
Mat cannyNums;
blur(inputImg, cannyNums, Size(3, 3));
Canny(cannyNums, cannyNums, thresh, thresh * 2, 3);
//寻找上下边界
for (int i = mnRow; i < mxRow; i++) {
if (cannyNums.at<uchar>(i, 0) != 0) {
st = i;
break;
}
}
for (int i = mxRow; i >= mnRow; i--) {
if (cannyNums.at<uchar>(i, 0) != 0) {
ed = i;
break;
}
}
//范围过小,调整二值化阈值,重新寻找
if (abs(ed - st) < mnsize) {
thresh -= 10;
if (thresh <= 0) {
st = mnRow; ed = mxRow;
return;
}
FindRowRanges(inputImg, thresh, mnRow, mxRow, mnsize, st, ed);
}
}
//寻找每个字符对应位置
void FindColRanges(Mat inputImg,vector<float>&pts) {
int thre = 0;
for (int j = 1; j < inputImg.cols - 1; j++) {
if (inputImg.at<uchar>(0, j) > thre && inputImg.at<uchar>(0, j - 1) <= thre) {//左边缘
pts.push_back(j - 1);
}
else if (inputImg.at<uchar>(0, j) > thre && inputImg.at<uchar>(0, j + 1) <= thre){//右边缘
pts.push_back(j + 1);
}
}
}
//模板匹配
bool Comp(pair<int, int>a, pair<int, int>b) {
return a.second < b.second;
}
int CalcImg(Mat inputImg) {
int nums = 0;
for (int i = 0; i < inputImg.rows; i++) {
for (int j = 0; j < inputImg.cols; j++) {
if (inputImg.at<uchar>(i, j) != 0) {
nums += inputImg.at<uchar>(i, j);
}
}
}
return nums;
}
//模板匹配的主要函数
char CheckImg(Mat inputImg, int k) {
//读取模板图片
string sampleImgPath = "样例/*.jpg";
vector<String> sampleImgFN;
glob(sampleImgPath, sampleImgFN, false);
int sampleImgNums = sampleImgFN.size();
pair<int, int>*nums = new pair<int, int>[sampleImgNums];//first 记录模板的索引号,second 记录两图像之差
for (int i = 0; i < sampleImgNums; i++) {
nums[i].first = i;
Mat numImg = imread(sampleImgFN[i], 0);
Mat delImg;
absdiff(numImg, inputImg, delImg);
nums[i].second = CalcImg(delImg);
}
sort(nums, nums + sampleImgNums, Comp);//选择差值最小的模板
int index = nums[0].first / 2;
switch (index) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
return index + '0';
case 10:
return 'I';
case 11:
return 'S';
case 12:
return 'B';
case 13:
return 'N';
case 14:
return 'X';
default:
return ' ';
}
}
int main() {
int rtNums = 0, accNums = 0, sunNums = 0;//分别代表:正确的数量,被准确识别的字符的数量,要识别的字符的总和
//读取 ISBN 图片
string testImgPath = "数据集/*.jpg";
vector<String> testImgFN;//必须cv的String
glob(testImgPath, testImgFN, false);
int testImgNums = testImgFN.size();
for (int index =0; index < testImgNums; index++) {
//int index = 25;
//调整原图大小
Mat src = imread(testImgFN[index]);
double width = 400;
double height = width * src.rows / src.cols;
resize(src, src, Size(width, height));
//转换成二值图像
Mat binImg;
cvtColor(src, binImg, COLOR_BGR2GRAY);
threshold(binImg, binImg, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
//计算调整角度
double thetas = GetTurnTheta(binImg);
thetas = 180 * thetas / CV_PI - 90;
//旋转二值图像
Mat turnBin;
Mat M = getRotationMatrix2D(Point(width / 2, height / 2), thetas, 1);
warpAffine(binImg, turnBin, M, src.size());
//计算每行点数
Mat rowNums = Mat(src.rows, 1, CV_8UC1);
int st = - 1, ed = - 1;//起始和终止行
for (int i = 0; i < src.rows; i++) {
int temC = 0;
for (int j = 0; j < src.cols; j++) {//统计每行像素点个数
if (turnBin.at<uchar>(i, j) != 0) {
temC++;
}
}
rowNums.at<uchar>(i, 0) = temC;
}
//寻找截取范围,并适当扩大截取范围
FindRowRanges(rowNums, 110, 0, src.rows / 4, 10, st, ed);
int adds = 4;
st = st >= adds ? (st -= adds) : 0;
ed -= adds;
//弥补旋转缺失区域
Mat background = Mat(src.rows, src.cols, CV_8UC1, Scalar(255));
warpAffine(background, background, M, src.size());
bitwise_not(background, background);
Mat turnSrc;
warpAffine(src, turnSrc, M, src.size());
src.copyTo(turnSrc, background);
//截取 ISBN 所在行
Mat subImg = Mat(turnSrc, Range(st, ed), Range(0, turnSrc.cols));//截取原图相应部分
//调整大小
width = 900;
height = width * subImg.rows / subImg.cols;
resize(subImg, subImg, Size(width, height));
//转换为二值图像
binImg = Mat();
cvtColor(subImg, binImg, COLOR_BGR2GRAY);
threshold(binImg, binImg, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);//计算每列点数
Mat colNums = Mat::zeros(1, subImg.cols, CV_8UC1);
for (int i = 0; i < subImg.rows; i++) {
for (int j = 1; j < subImg.cols - 1; j++) {//统计每行像素点个数
if (binImg.at<uchar>(i, j) != 0) {
colNums.at<uchar>(0, j)++;
}
}
}//寻找字符边界
vector<float>pts;
FindColRanges(colNums, pts);
//截取字符并识别
string result = "";
for (int j = 0; j < pts.size(); j += 2) {//j 为左边界,j+1 为右边界//截取当前字符所在区域,方便后续操作
Mat roi = Mat(binImg, Range(0, subImg.rows), Range(pts[j], pts[j + 1]));
Mat roiImg;
roi.copyTo(roiImg);
//寻找最小正矩形,并排除不满足条件的矩形
vector<vector<Point> >contours;
findContours(roiImg, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
for (int i = 0; i < contours.size(); i++) {
Rect temRect = boundingRect(contours[i]);
if (temRect.height < subImg.rows / 3 || temRect.height == subImg.rows) {
continue;
}
//调整大小
Mat rectImg = Mat(roiImg, temRect);
resize(rectImg, rectImg, Size(40, 50));
//与模板进行匹配
char letters = CheckImg(rectImg, 5);
if (letters >= '0'&&letters <= '9' || letters == 'X') {
result += letters;
}
}
}
//确定正确的 ISBN 号,来跟识别出来的 ISBN 做对比
string cmpData = "";
for (int i = 0; i < testImgFN[index].length(); i++) {
if (testImgFN[index][i] >= '0'&&testImgFN[index][i] <= '9' || testImgFN[index][i] == 'X') {
cmpData += testImgFN[index][i];
}
}
//有多余字符
if (result.length() > cmpData.length()) {
string tem = result.substr(result.length() - cmpData.length());
if (tem != cmpData) {
tem = result.substr(0, cmpData.length());
}
result = tem;
}
else if (result.length() < cmpData.length()) {//有字符未被识别
int i;
for (i = 0; i < result.length(); i++) {
if (result[i] != cmpData[i]) {
break;
}
}
string r1 = result.substr(0, i);
string r2 = result.substr(i);
string r3 = "";
for (int j = 0; j < cmpData.length() - result.length();j++) {
r3 += " ";
}
result = r1 + r3 + r2;
}
cout << result << endl << cmpData << endl << index << endl;
//计算准确率
sunNums += cmpData.length();
for (int i = 0; i < cmpData.length(); i++) {
if (result[i] == cmpData[i]) {
accNums++;
}
}
//计算正确率
if (result == cmpData) {
rtNums++;
cout << "Yes" << endl;
}
else{
cout << "No" << endl;
}
cout << endl;
}
//cout << accNums << " " << sunNums << endl;
printf("正确个数:%4.d 正确率:%f\n", rtNums, rtNums * 1.0 / testImgNums);
printf("准确个数:%4.d 准确率:%f\n", accNums, accNums * 1.0 / sunNums);
waitKey(0);
system("pause");
}
其中 “数据集” 和 “样例” 两个文件夹均在默认路径(跟 cpp 文件放在一起)
链接: https://pan.baidu.com/s/1o5JoxYHTt8QD-X2gKEYnKA?pwd=qqqq 提取码: qqqq 复制这段内容后打开百度网盘手机App,操作更方便哦