今天上午,朋友发来一张图片如下。没错,这就是原图,他希望可以通过一些简单的算法将图中这条穿过单词间的直线去掉,使得到的结果能够通过他的文字识别算法并得出正确结果——The Techniques of Machine Vision。

乍一看这似乎挺简单,(1)将图像二值化;(2)找出这条直线;(3)将直线区域填成背景色(即白色);(4)再通过膨胀、腐蚀等操作将单词缺失的部分给补全。以上4步似乎可以满足要求,但测试发现,效果不尽人意。
一、按上述方法实现过程

图1.1 对原图使用大津阈值的结果
二值化结果如图1.1所示,可以看到图像并不标准,直线粗细也不一,我们尝试用霍夫变换找一下直线,代码如下
- <span style="font-size:12px;">void findLines(IplImage* raw, IplImage* dst)
- {
- IplImage* src = cvCloneImage(raw);
- IplImage* canny = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
- cvCanny(src, canny, 20, 200, 3);
- CvMemStorage* stor = cvCreateMemStorage(0);
- CvSeq* lines = NULL;
- lines = cvHoughLines2(canny, stor, CV_HOUGH_PROBABILISTIC, 1, CV_PI / 180, 80, 200, 30);
- cvZero(dst);
- CvPoint maxStart, maxEnd;
- int maxDistance = 0;
- for (int i = 0; i < lines->total; i++)
- {
- CvPoint* line = (CvPoint*)cvGetSeqElem(lines, i);
- if (abs(line[0].x - line[1].x) > maxDistance)
- {
- maxDistance = abs(line[0].x - line[1].x);
- maxStart = line[0];
- maxEnd = line[1];
- }
- }
- cvLine(dst, maxStart, maxEnd, cvScalar(255), 1);
- cvReleaseImage(&src);
- cvReleaseMemStorage(&stor);
- }</span>
简要解释一下这段代码。函数的功能是在输入图像中找出一条直线,输入的图像是灰度图raw,返回值为dst,返回值是以图片的形式,将找到的直线画上图中。
函数lines = cvHoughLines2(canny, stor, CV_HOUGH_PROBABILISTIC, 1, CV_PI / 180, 80, 200, 30);的参数表明,要求直线长度在200个像素以上,且两条在同一直线上的线段,如果相隔不到30个像素,就把它们连起来【注:图片尺寸为1066×148】。对于找到的多条直线,认为最长的一条是我们要找的那条。找距离时用了abs(line[0].x - line[1].x);是不严格的,严格来讲应该是
sqrt((line[0].x - line[1].x)*(line[0].x - line[1].x)+(line[0].y - line[1].y)*(line[0].x - line[1].x))
不过图中的直线接近水平,这里就简化一下啦。
所以将运行这段代码后,返回的图片dst应该是这样子的

图1.2 通过霍夫变换找到的直线
图1.2中直线的粗线可以通过改变cvLine(dst, maxStart, maxEnd, cvScalar(255), 1);最后一个参数来调整,这里用的是1。
接下来步骤就是在二值化图(图1.1)中去掉这条线,代码如下:
- <span style="font-size:12px;">void eraseLine(IplImage* src, IplImage* flag)
- {
- for (int row = 0; row < src->height; row++)
- for (int col = 0; col < src->width; col++)
- {
- if (cvGet2D(flag, row, col).val[0] == 255)
- cvSet2D(src, row, col, cvScalar(255));
- }
- }</span>
当直线的宽度分别为2、3个像素时,二值化图去掉直线后的效果如下

图1.3 当线宽分别为2、3像素时,二值化图去掉直线后的结果
可以看到,效果很差,如果要膨胀(黑色部分减小),单词下边部分都会消失了,直接腐蚀(黑色部分增大),线又不能完全去掉。
后来,我采用的办法是,对图1.3重新找一次直线(减去一次直线后,中间还残留一部分短些的直线),再减掉,再找再减掉。后面再对图像进行腐蚀(黑色部分增长)。最终效果最好这就如下图所示

图1.4 此方法效果最好的结果
但这种方法用时长、针对不同的直线,找直线-减直线 的重复次数还不一样,不具有可移植性。而且啊,这个图片识别出来的结果是
The Technique_sJ_otMachine Vision
所以需要采用新的办法来解决这个问题。
二、新的办法
源代码如下
- #include <cv.h>
- #include <highgui.h>
- #include <iostream>
- using namespace std;
-
-
-
-
-
-
-
- void findLines(IplImage* raw, IplImage* dst)
- {
- IplImage* src = cvCloneImage(raw);
- IplImage* canny = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
- cvCanny(src, canny, 20, 200, 3);
- CvMemStorage* stor = cvCreateMemStorage(0);
- CvSeq* lines = NULL;
-
- lines = cvHoughLines2(canny, stor, CV_HOUGH_PROBABILISTIC, 1, CV_PI / 180, 80, 200, 30);
- cvZero(dst);
- CvPoint maxStart, maxEnd;
- int maxDistance = 0;
- for (int i = 0; i < lines->total; i++)
- {
-
- CvPoint* line = (CvPoint*)cvGetSeqElem(lines, i);
-
- if (abs(line[0].x - line[1].x) > maxDistance)
- {
-
-
-
- maxDistance = abs(line[0].x - line[1].x);
- maxStart = line[0];
- maxEnd = line[1];
- }
- }
- cvLine(dst, maxStart, maxEnd, cvScalar(255), 1);
- cvReleaseImage(&src);
- cvReleaseMemStorage(&stor);
- }
-
-
-
-
- void erase(IplImage* raw)
- {
- IplImage* src = cvCloneImage(raw);
-
-
- cvThreshold(src, src, 120, 255, CV_THRESH_BINARY_INV);
-
- CvMemStorage* stor = cvCreateMemStorage(0);
- CvSeq* cont;
- cvFindContours(src, stor, &cont, sizeof(CvContour), CV_RETR_EXTERNAL);
- for (; cont; cont = cont->h_next)
- {
- if (fabs(cvContourArea(cont)) < 15)
- cvDrawContours(raw, cont, cvScalar(255), cvScalar(255), 0, CV_FILLED, 8);
- }
- cvReleaseImage(&src);
- }
-
- int main()
- {
- IplImage* src = cvLoadImage("D:/test.png");
- cvNamedWindow("原图", 1);
- cvShowImage("原图", src);
- IplImage* gray = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
- IplImage* canny = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
- IplImage* dst = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
- IplImage* binary = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
-
- cvCvtColor(src, gray, CV_RGB2GRAY);
- cvThreshold(gray, binary, 120, 255, CV_THRESH_OTSU);
-
- findLines(gray, dst);
- cvNamedWindow("dst", 1);
- cvShowImage("dst", dst);
-
- for (int row = 0; row < binary->height; row++)
- for (int col = 0; col < binary->width; col++)
- {
- if (cvGet2D(dst, row, col).val[0] == 255)
- {
- int up = 0, down = 0;
- int white = 0;
- for (int i = row; i >= 0; i--)
- {
- if (cvGet2D(binary, i, col).val[0] == 0)
- {
- up++;
- white = 0;
- }
- else white++;
- if(white > 2) break;
- }
- white = 0;
- for (int i = row; i < binary->height; i++)
- {
- if (cvGet2D(binary, i, col).val[0] == 0)
- {
- down++;
- white = 0;
- }
- else white++;
- if (white > 2) break;
- }
- if (up + down < 8)
- {
- for (int i = -up; i <= down; i++) cvSet2D(binary, row + i, col, cvScalar(255));
- }
- }
- }
- cvNamedWindow("结果", 1);
- cvShowImage("结果", binary);
- erase(binary);
-
- cvErode(binary, binary, NULL, 1);
- cvNamedWindow("膨胀腐蚀", 1);
- cvShowImage("膨胀腐蚀", binary);
- cvSaveImage("D:/result.png", binary);
- cvReleaseImage(&src);
- cvReleaseImage(&canny);
- cvReleaseImage(&gray);
- cvReleaseImage(&dst);
- cvReleaseImage(&binary);
- cvWaitKey(0);
- return 0;
- }
这个方法很简单的,就是在找到直线(直线宽度为1)后,沿着直线从左到右对二值化图进行上下扫描,如果这个直线的宽度(黑色的宽度)小于8个像素,则认为它只是直线,而不是文字的一部分,那么将它填成白色;反之,对于直线是文字的一部分这种情况,则不对它进行任何操作。
这样得到的结果如下图2.1所示

图2.1 新方法运行结果
当然这个结果有点差强人意,如果你有更好的想法,请在下面留言,我们交流交流。