第一次写长博,记录一个项目。这几天一直在接小活,有一个是客户的要求是将目标图片上的文字(目测是好多器材上边的编号)检测出来,并对比,要求长字符串和长字符串相同,短字符串和短字符串相同,不一样的需要标识出来。感觉还挺有意思的,就把过程贴出来以便日后复习。话不多说先贴图:待检测图片和最终识别结果如下图,相同的长字符串用蓝色框标出,短字符串用绿色框标出,而疑似不一致字符串用红色框标出,对客户传来的待测试图片集检测效果良好。

基本上随着现在OCR技术的不断拓展和文字处理技术的不断上升,这个问题应该已经不算是问题,在这里梳理一下大致思路,其中为了锻炼自己对图像处理的理解,除了二值化的的函数和拟合最小包络矩形之外,都没用什么API。

0.具体思路的确定

根据具体问题的特殊性和与车牌检测的相区别(车牌检测的重要一步是车牌有矩形外边框因而容易确定文字所在范围,这类问题没有很明显的文字定位标志),通过不断试错确定了以下思路:

  1. 灰度+自适应性局部二值化
  2. 图像膨胀+简单滤波
  3. 八连通域检测
  4. 连通域筛选滤波
  5. 搜索确定字符串区域(由若干连通域组成)
  6. 基于形心的字符串角度最小二乘检测(兼排除噪声)
  7. 确定字符穿ROI
  8. ROI图像处理
  9. 基于Tesseract4.0的深度学习OCR文字识别
  10. 处理识别出的字符串,判断非一致项,并将结果在原图中标出。

下面对仅重要步骤作详细说明:

1.关于二值化的说明

客户提供的图片文字和背景对比度很高,一开始以为二值化是个小事,但后来发现并非如此。在一开始二值化过程中分别采取了以下几种方案:定值的全局二值化分块OTSU局部二值化以及最终采用的自适应局部二值化(adaptiveThreshold)。定值的全局二值化的问题在于图像的适应性很差,对于一张图像而言可能存在一个最佳阈值可能是确定的,但是多张图片受光线影响较重;为了解决光线的问题,采取了分块OTSU局部二值化的方法,这个函数是自己写的,可以选择块的大小,并对每个块进行OTSU二值化,但是这个方法的问题在于,OTSU是为区分图片的前景和背景而存在,但在本问题中,文字部分可不仅仅是”前景“那么简单,换句话说,通过OTSU后,不仅文字部分——好多灰度高于大约40的部分都被划分为前景,这对后边滤波和连通域的分析都带来极大的困难,一方面是引入很多噪声,另一方面各个字符之间的区域也被识别为白色,无法分开,但是这一步也不是白做的,引入的思考:一个比较好的想法就是,既然OTSU得出来的阈值不够”高“,那么如果我们在OTSU所得到的基础上,加上一个”合适的数“,使最终的结果作为二值化的阈值,那问题不就解决了?这个其实想法也可以在自己的函数上实践,但是最后直接用了adaptiveThreshold,这个函数最终的两个参数分别是分块的”块的大小“以及”最终加上的那个值“,用了这个函数以后可以很好的区分文字部分和非文字部分,避免引入尽量少的噪声;关于”块的选取“,本例中前景和背景区分明确,应该使块”尽量大“以忽略诸多细节(这些细节就体现在文字与文字之间相连的部分),而”加上的值“要尽量大,即二值化的”要求要尽量高“。最终二值化的结果展现如下 :可以发现除了少部分星星点点的噪声以外效果还是很好的。

2. 图像膨胀+中值滤波

这一步没什么可说的,只有关于图像膨胀的必要性说明:有一些独立字符在二值化之后分开了,如大写的K的一竖和右边的部分就分开了,分成了两个连通域,这是不好的,因为后边还会对字符串内连通域的个数进行统计,因此用dilate对白色区域进行膨胀。

3. 八连通域检测

网上关于连通域的检测教程有很多,也有很多源码,这里就不赘述了,详情请看https://blog.csdn.net/ShanX_s/article/details/52860896,讲的很生动,也有代码。

但是源码大多是四联通域的,我自己写了个八连通域的(两遍法),贴出来分享给大家。关于四联通域和八连通域的区别在于,四联通域两边比较的对象都是上像素和左像素,但八连通域比较对象扩充到了:上像素、左像素、上左像素、和上右像素,同时处理的时候仍然要像素越界的问题。另一个比较坑的地方就是,labelImg一定要是long int格式,因为我的图像实在是太大了(4000*13000),只有一个int根本储存不过来,不然会出现用完了又来一遍,这就比较坑了,因为他不会报错,但一个连通域又被分成了两个!!!让我检查了好久到底是哪里出了问题。

bool FindConnectedDomain(Mat& srcImg, Mat& labelImg)
{if (srcImg.empty()){cerr << "Invalid input parameters! " << endl;return false;}if (srcImg.channels() != 1){cerr << "Please input a binary image!" << endl;return false;}labelImg = Mat(srcImg.size(), CV_32SC1, Scalar(0));vector<long int> labelSet;labelSet.push_back(0);int flag = 0;//第一遍扫描for (int row = 0; row < srcImg.rows; row++){uchar* currentPtr = srcImg.ptr<uchar>(row);uchar* lastPtr = srcImg.ptr<uchar>(max(0, row - 1));long int* currentDstPtr = labelImg.ptr<long int>(row);long int* lastDstPtr = labelImg.ptr<long int>(max(0, row - 1));for (int col = 0; col < srcImg.cols; col++){if (currentPtr[col] == 255){//四联通域//if (currentDstPtr[max(0,col-1)] == 0 && lastDstPtr[col] == 0)//{//flag++;//currentDstPtr[col] = flag;//labelSet.push_back(flag);//}//else if (currentDstPtr[max(0, col-1)] == 0 && lastDstPtr[col] != 0)//{//  currentDstPtr[col] = lastDstPtr[col];//}//else if (currentDstPtr[max(0, col-1)] != 0 && lastDstPtr[col] == 0)//{//  currentDstPtr[col] = currentDstPtr[max(0, col-1)];//}//else if (currentDstPtr[max(0, col-1)] != 0 && lastDstPtr[col] != 0)//{//  int upperLabel = labelSet[lastDstPtr[col]];//  int leftLabel = labelSet[currentDstPtr[max(0, col-1)]];//  int biggerLabel = max(upperLabel, leftLabel);//    int smallerLabel = min(upperLabel, leftLabel);//   currentDstPtr[col] = smallerLabel;//   labelSet[biggerLabel] = smallerLabel;//}//八连通域if (currentDstPtr[max(0, col - 1)] == 0 && lastDstPtr[col] == 0 && lastDstPtr[max(0, col - 1)] == 0 && lastDstPtr[min(col + 1, srcImg.cols - 1)] == 0){flag++;currentDstPtr[col] = flag;labelSet.push_back(flag);}else{int leftLabel = labelSet[currentDstPtr[max(0, col - 1)]];int upLabel = labelSet[lastDstPtr[col]];int upLeftLabel = labelSet[lastDstPtr[max(0, col - 1)]];int upRightLabel = labelSet[lastDstPtr[min(col + 1, srcImg.cols - 1)]];int tempLabel = max(max(max(leftLabel, upLabel), upLeftLabel), upRightLabel);int smallestLabel = min(tempLabel, leftLabel);if (smallestLabel == 0) smallestLabel = tempLabel;if (smallestLabel != 0) tempLabel = smallestLabel;smallestLabel = min(tempLabel, upLabel);if (smallestLabel == 0) smallestLabel = tempLabel;if (smallestLabel != 0) tempLabel = smallestLabel;smallestLabel = min(tempLabel, upLeftLabel);if (smallestLabel == 0) smallestLabel = tempLabel;if (smallestLabel != 0) tempLabel = smallestLabel;smallestLabel = min(tempLabel, upRightLabel);if (smallestLabel == 0) smallestLabel = tempLabel;if (smallestLabel != 0) tempLabel = smallestLabel;currentDstPtr[col] = smallestLabel;if (leftLabel != 0){labelSet[leftLabel] = smallestLabel;}if (upLabel != 0){labelSet[upLabel] = smallestLabel;}if (upLeftLabel != 0){labelSet[upLeftLabel] = smallestLabel;}if (upRightLabel != 0){labelSet[upRightLabel] = smallestLabel;}}}}}//第二遍扫描for (int row = 0; row < labelImg.rows; row++){int* currentRow = labelImg.ptr<int>(row);for (int col = 0; col < labelImg.cols; col++){if (currentRow[col] == 0) continue;int oldLabel = currentRow[col];while (currentRow[col] != labelSet[oldLabel]){currentRow[col] = labelSet[oldLabel];oldLabel = currentRow[col];}}}return true;
}

4. 连通域筛选滤波

自己创建的连通域结构体如下ConnectedDomain,包含了所有像素位置,像素的边界坐标(用来以后确定最小正矩形),连通域大小以及连通域的形心(用来以后最小二乘文字角度用)。

struct ConnectedDomain
{int label;vector<PixelPoint> pixelGroup;int xMax;int xMin;int yMax;int yMin;long int xSum;long int ySum;Point2d center;int area;
};

而对于从labelImg转换到vector<ConnectedDomain>的过程需要经历面积大小的筛选,最小包络正矩形长宽大小以及长宽比的筛选,以及最小包络矩形的面积筛选。这里说明一下:最小包络正矩形指的是横平竖直的矩形,也就是结果图上画出来的那种,直接由连通域的xMax,xMin,yMax,yMin组合即可确定矩形顶点;最小包络矩形是用的API:cv::minAreaRect,值得一提的是,在大多数教材中minAreaRect是连在findContours后边用的,用于对轮廓检测最小矩形,但是实际上他的输入参数是InputArray,vector是他的基类之一,我们用vector<Point>类型的对象都可以作为形参传进去(InputArray真是强大,各种数据通吃)。代码段和八连通域最终滤波后的图像如下(花花绿绿的还挺好看):

 for (vector<ConnectedDomain>::iterator it = connectedDomainGroup.begin(); it != connectedDomainGroup.end(); ){if (it->area < 200 || it->area > 2000 || (it->xMax - it->xMin) > 50 || (it->yMax - it->yMin) > 50){it = connectedDomainGroup.erase(it);continue;}vector<Point> pointGroup;for (unsigned int i = 0; i < it->pixelGroup.size(); i++){pointGroup.push_back(Point(it->pixelGroup[i].col, it->pixelGroup[i].row));}RotatedRect rect = minAreaRect(pointGroup);Size mySize(rect.size);if (mySize.width < 5 || mySize.height < 5){it = connectedDomainGroup.erase(it);continue;}it->center.x = (double)(it->xSum) / (double)(it->pixelGroup.size());it->center.y = (double)(it->ySum) / (double)(it->pixelGroup.size());it++;}

5.确定字符区域

连通域都弄好了,接下来就要确定字符区域,我创建的字符结构体如下,成员包括连通域向量,包络正矩形和文字的倾斜角度(以后仿射变换用的):

struct CharacterGroup
{int xMax;int xMin;int yMax;int yMin;double angle;PixelPoint xMax_yMax;PixelPoint xMax_yMin;PixelPoint xMin_yMax;PixelPoint xMin_yMin;vector<ConnectedDomain> relatedConnectedDomain;};

确定字符串的条件是:连通域之间相距小于设定阈值的彼此属于同一字符,这就带来一个坏处就是,假设在一堆文字联通域中出现了一个滤波没滤掉的坏连通域,那么他一并会被列入相关字符串并参与后续运算,针对此我们在第六步进行了最小二乘噪声剔除。

6.确定文字角度

确定文字角度的方法是对所有属于一个字符串的连通域进行最小二乘拟合直线,通过对拟合直线的斜率进行atan就能得到文字角度,这些都是为了后续仿射变换做准备(放射变换是为了将文字调正,为了OCR做准备)至于最小二乘是如何去除噪声的呢,这个就仁者见仁了,我也卖个关子,不在这里说。最后也可以引入一点回环检测的思想,因为基本上所有文字都是平行的,可以对所有字符串的角度进行统计,一般都是74°小数点后一两位不确定,对于相差太多的字符串肯定是有噪声影响,那么我们就可以处理一下巴拉巴拉。贴上最小二乘的代码,浅显易懂不必说明。

void LineFitLeastSquares(vector<float>& data_x, vector<float>& data_y, int data_n, vector<float> &gradient, vector<float> &intercept, vector<float> &r2)
{float A = 0.0;float B = 0.0;float C = 0.0;float D = 0.0;float E = 0.0;float F = 0.0;for (int i = 0; i<data_n; i++){A += data_x[i] * data_x[i];B += data_x[i];C += data_x[i] * data_y[i];D += data_y[i];}// 计算斜率a和截距bfloat a, b, temp = 0;if (temp = (data_n*A - B*B))// 判断分母不为0{a = (data_n*C - B*D) / temp;b = (A*D - B*C) / temp;}else{a = 1;b = 0;}// 计算相关系数rfloat Xmean, Ymean;Xmean = B / data_n;Ymean = D / data_n;float tempSumXX = 0.0, tempSumYY = 0.0;for (int i = 0; i<data_n; i++){tempSumXX += (data_x[i] - Xmean) * (data_x[i] - Xmean);tempSumYY += (data_y[i] - Ymean) * (data_y[i] - Ymean);E += (data_x[i] - Xmean) * (data_y[i] - Ymean);}F = sqrt(tempSumXX) * sqrt(tempSumYY);float r;r = E / F;gradient.push_back(a);intercept.push_back(b);r2.push_back(r*r);
}

7&8.确定字符串ROI并做相关图像处理

上边都确定了字符串最小包络正矩形了,当然可以对原图选取ROI,在ROI选取后对ROI做的处理有adaptiveThreshold啦、滤波啦、放射变换啦等等,最后处理出来的是这个样子的,交给Tesseract就会省心的多啦!

9.基于Tesseract的OCR

我的IDE是VS2015,编译环境release+32,配置Tesseract详见这位网友大大的救命博文https://blog.csdn.net/andylanzhiyong/article/details/81807425,真是服了,这个配置花了我两天时间,需要用cmake对tesseract和cppan生成项目再编译,以前只用过cmake逆向OpenCV源码和配置Eigen以及SSBA,结果这次中间遇到各种bug各种找不到文件。另注:使用的数据集是Google官方提供的eng.traineddata。

10.对检测出的字符串进行处理

这一步主要是针对人为可见的误识别,如”S“识别成了”$”等,几个if就解决了。最终识别效果还是挺好的,小项目也没有必要去自己训练一份数据。识别结果如下:

OpenCV项目实战日志——检测文字并对比识别相关推荐

  1. Opencv项目实战:01 文字检测OCR(2)

    1,相关函数的讲解 image_to_data()的输出结果是表格形式,输出变量的类型依旧是字符串. 你会得到一个这样的列表['level', 'page_num', 'block_num', 'pa ...

  2. 【Opencv项目实战】背景替换:动态背景移除与替换(cvzone+MediaPipe)

    文章目录 一.项目思路 二.环境布置 2.1.cvzone安装 2.2.MediaPipe安装 2.3.常见问题 2.4.注意事项 三.算法详解 3.1.segmentor.removeBG():去除 ...

  3. Opencv项目实战-信用卡数字识别

    Opencv项目实战:信用卡数字识别 导入库,定义展示函数 import cv2 import numpy as np from imutils import contours import myut ...

  4. OpenCV计算机视觉实战(Python)| 10、项目实战:文档扫描OCR识别

    文章目录 简介 总结 1. 介绍 2. 流程 3. 程序 4. 知识点总结 简介 本节为<OpenCV计算机视觉实战(Python)>版第10讲,项目实战:文档扫描OCR识别,的总结. 总 ...

  5. 基于C++的OpenCV项目实战——文档照片转换成扫描文件

    基于C++的OpenCV项目实战--文档照片转换成扫描文件 一.背景 前段时间都是基于Python的OpecCV进行一些学习和实践,但小的知识点并没有应用到实际的项目中:并且基于Python的版本的移 ...

  6. Opencv项目实战:14 手势控制音量

    目录 0.项目介绍 1.项目展示 2.项目搭建 3.项目的代码与讲解 4.项目资源 5.项目总结 0.项目介绍 本篇与上一篇有很多联系,大家可以看看这篇Opencv项目实战:13 手部追踪,我们将根据 ...

  7. 【北京大学】13 TensorFlow1.x的项目实战之手写英文体识别OCR技术

    目录 1 项目介绍 1.1 项目功能 1.2 评估指标 2 数据集介绍 2.1 数据特征 3 数据的预处理 3.1 数据增强 3.2 倾斜矫正 3.3 去横线 3.4 文本区域定位 4 网络结构 5 ...

  8. 深度学习项目实战(一):猫狗识别

    深度学习项目实战(一):猫狗识别 文章目录 深度学习项目实战(一):猫狗识别 项目背景: 数据读取: 网络架构 卷积神经网络训练 项目背景: 猫狗识别是卷积神经网络的入门实战案例,目的在于计算机可以识 ...

  9. Opencv项目实战:基于dlib的疲劳检测

    文章目录 一.项目简介 二.算法原理 三.环境配置 3.1.dlib人脸检测器:dlib.get_frontal_face_detector() 3.2.dlib关键点定位工具:shape_predi ...

最新文章

  1. 自动化运维平台OMserver部署过程中解决的问题1
  2. JavaScript 操作 COM 控件
  3. runas/cpau/lsrunase使用小结(以管理员运行指定程序)
  4. Madagascar的宏定义函数--取最值、取整
  5. 游戏开发经验分享:我所理解的打击感
  6. php ci的session和php session,php及codeigniter使用session-cookie的方法(详解)
  7. python中printf的用法_python输出语句print的用法是什么?
  8. 密码攻略 黑客亲手打造QQ密码破解器(转)
  9. 口布杯花的60种叠法_10种餐巾折花杯花的步骤用文字解说怎么折
  10. e会学c语言程序设计基础网课答案,C程序设计(双语版)习题答案
  11. SSM基于小程序的医院预约挂号系统 毕业设计-附源码260839
  12. ios7下弹出新浪微博界面,一出现就消失的问题
  13. 学医后才知道的小知识...
  14. 【FAQ】接入HMS Core推送服务过程中一些常见问题总结
  15. 电大计算机应用基础word排版,电大计算机应用基础考试全部操作100题
  16. xpath常见错误:Opening and ending tag mismatch: meta line 4 的处理方法【Python爬虫】
  17. python 椭圆曲线_Python、Sympy和椭圆曲线
  18. 懒人日报 | 日本加密货币交易所经营者被逮捕、杭州开启区块链执法、重庆区块链政务服务平台上线......
  19. chef之cookbook入门简明手册
  20. JT1078视频服务器

热门文章

  1. svn+ssh服务器与客户端配置方法
  2. 图解通信原理与案例分析-26: 5G NR是如何支持海量机器类通信mMTC的?移动通信对物联网的支持
  3. 2023第十届大唐杯省赛心得体会总结
  4. 2022年六西格玛培训机构排名
  5. libjpeg库使用举例
  6. 微软表示今年员工不要参与愚人节活动!怕玩笑过大得不偿失
  7. 如何整理个人电脑的文件及目录?(第1期)
  8. 文章标题ffmpeg文档37-视频滤镜
  9. AWS KVS(Kinesis Video Streams)之WebRTC移植编译(五)
  10. 论「能写代码」的程序员与「会写代码」的程序员