C++利用opencv进行OCR Win10-x64+VS2015
原理
OCR的应用非常广泛,在工业上,常常用于印刷体的识别,例如印刷数字的识别。对于这种问题,我们的做法是先去采集工业场景下大批量的图像数据,分割为单个字体区域,绑定对应的标签,制作印刷字体的训练集。再使用机器学习或者神经网络对数据集进行训练,在得到一个准确率高的模型后,再使用模型去分类待测的字体区域图片。
步骤
制作数据集
简单演示,这里我直接用word文档输入数字截图作为我采集的图像
如下为train.png
test.png
第一步:二值化
cv::Mat gray = cv::imread("E:\\DataSet\\OCR\\train\\1.png", 0);
cv::Mat thresh;
cv::threshold(gray, thresh, 100, 255, cv::THRESH_BINARY_INV);
第二步:寻找轮廓
cv::Mat con;
thresh.copyTo(con);
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierachy;
cv::findContours(con, contours, hierachy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
第三步:创建轮廓的最小外包直立矩形
第四步:从二值图中取每一个ROI,并进行Resize
第五步:将图像及其标签添加进数据及标签集中
// 第三步、第四步、第五步
cv::Mat sample, response; // 初始化数据、标签
for (int i = 0; i < contours.size(); i = hierachy[i][0]) {
cv::Rect r = cv::boundingRect(contours[i]);
cv::rectangle(gray, cv::Point(r.x, r.y), cv::Point(r.x + r.width, r.y + r.height), cv::Scalar(128), 2, 8, 0);
cv::Mat ROI = thresh(r); // 取最小直立外包矩形的ROI
cv::Mat tmp1, tmp2;
cv::resize(ROI, tmp1, cv::Size(10, 10), 0, 0, cv::INTER_LINEAR); // 缩放
tmp1.convertTo(tmp2, CV_32FC1);
// 将每个ROI reshape成size为100*1的Mat,即1行100列;并push进sample的底部
sample.push_back(tmp2.reshape(1, 1));
// 利用键盘输入标签,并push进response底部
cv::imshow("ROI", ROI);
int label = cv::waitKey(0);
label -= 0x30;
response.push_back(label);
}
response = response.reshape(1, 1);
response.convertTo(response, CV_32FC1);
注意:for循环结构中i并不是用++,而是用的hierachy[i][0],这是因为我们findContours取到的是所有轮廓,且从最外层开始寻找,即0号轮廓一定是外层轮廓。但是当一个轮廓同时存在内嵌轮廓和同级轮廓时,内嵌轮廓的优先级更高,序号更小。例如,假如在没有内嵌轮廓的情况下8轮廓序号为8,9轮廓序号为9,但由于8中有两个内嵌轮廓,这时这两个内嵌轮廓的序号就是9,10,而9轮廓序号就变成11了,这样寻找ROI就不可控了,而hierachy[i][0]就解决了这个困局,hierachy[i]对应的是contours[i],hierachy[i][0]~hierachy[i][3]中0代表平级轮廓的后一个,1代表平级轮廓的前一个,2代表子级,3代表父级,没有的话就返回-1,所以hierachy[i][0]可以遍历所有同级轮廓,并在遍历后跳出循环。其实opencv的findContours可以只检测最外层轮廓,只需要将mode参数从cv::RETR_CCOMP修改为cv::RETR_EXTERNAL即可。
第六步:保存训练集
cv::FileStorage Data("TrainingData.yml", cv::FileStorage::WRITE);
Data << "data" << sample;
Data.release();
cv::FileStorage Label("Label.yml", cv::FileStorage::WRITE);
Label << "label" << response;
Label.release();
std::cout << "Success to Create TrainSet...!" << std::endl;
训练模型
第一步:读取训练集
cv::Mat sample, response;
cv::FileStorage Data("TrainingData.yml", cv::FileStorage::READ);
Data["data"] >> sample;
Data.release();
cv::FileStorage Label("Label.yml", cv::FileStorage::READ);
Label["label"] >> response;
Label.release();
std::cout << "Success to Read TrainSet...!" << std::endl;
第二步:创建初始化模型,以KNN为例
cv::Ptr<cv::ml::KNearest> knn(cv::ml::KNearest::create());
第三步:训练
knn->train(sample, cv::ml::ROW_SAMPLE, response);
std::cout << "Train Finished...!" << std::endl;
第四步:保存模型
knn->save("knn.xml");
使用模型预测
第一步:加载模型
cv::Ptr<cv::ml::KNearest> knn = cv::ml::StatModel::load<cv::ml::KNearest>("knn.xml");
第二步:测试图像预处理,测试
参考制作训练集的步骤提取ROI,并测试
...
...
for (int i = 0; i < contours.size(); i = hierachy[i][0]) {
...
float p = knn->findNearest(tmp2.reshape(1, 1), 1, response); // 测试
cv::imshow("ROI", ROI);
std::cout << int(p) << std::endl;
cv::waitKey(0);
}