环境
- OpenCV 2.4.13
- VS 2015
- 本文完整代码开源在Github
刚开始让我做这个,我也没有什么好的思路,但是随着对OpenCV的文档逐渐熟悉,也查阅了很多资料,对于这个问题,终于有了一个相当不错的解决方法。
常规方法
最初的搜索,会找到一种非常常规的方法:先对图像进行二值化,然后使用霍夫变换,检测出其中的直线,并在直线中,找到围成一个矩形的区域,将这块区域提取出来就好了。
按照这个方法,我最初使用的是Canny + HoughLinesP,检测出的直线效果也看得过去。但是由于我的输入图像是照片,每个照片的明亮不统一,Canny效果并不出众。于是乎我又加了腐蚀和高斯模糊,Canny我也参考Matlab等加了自适应阈值,直线稍微多了些,但也并不好看。
最初的代码是这样的,很小白。
void extractTable(string &argv) {
Mat src = imread(argv), dst;
if (!src.data) {
cout << "not picture" << endl;
}
Mat canny,gray,sobel, edge,erod, blur, color_dst;
double src_height=src.cols, src_width=src.rows;
imshow("source", src);
//先转为灰度
cvtColor(src, gray, COLOR_BGR2GRAY);
//腐蚀(黑***域变大)
int erodeSize = src_height / 200;
if (erodeSize % 2 == 0)
erodeSize++;
Mat element = getStructuringElement(MORPH_RECT, Size(erodeSize, erodeSize));
erode(gray, erod, element);
//高斯模糊化
int blurSize = src_height / 200;
if (blurSize % 2 == 0)
blurSize++;
GaussianBlur(erod, blur, Size(blurSize, blurSize), 0, 0);
namedWindow("GaussianBlur", WINDOW_NORMAL);
imshow("GaussianBlur", blur);
//自适应阈值Cannyz算法
double low = 0.0, high = 0.0;
AdaptiveFindThreshold(src, &low, &high);
Canny(blur, canny, low, high);
imshow("gray", gray);
imshow("canny", canny);
//检测直线,并将直线放回原图
vector<Vec4i> lines;
HoughLinesP(canny, lines, 1, CV_PI / 180, 100, 100, 15);
cout << "线条数" << lines.size() << endl;
for (size_t i = 0; i < lines.size(); i++)
{
line(src, Point(lines[i][0], lines[i][1]),
Point(lines[i][2], lines[i][3]), Scalar(0, i * 10 & 255, (255 - i * 10) & 255), 3, 8);
}
imshow("Detected Lines", src);
waitKey(0);
destroyAllWindows();
}
就这样在不断的修改和寻觅了两天后,终于让我发现了一个让人心悦诚服的算法。
进阶方法
这个算法灵活运用了腐蚀和膨胀,让表格乖乖的现形了,由于照片不可外传,所以就用论文中的截图做例子吧。值得一提的是,对于照片,这段代码也十分适用。
1、处理图像,灰度化,二值化
在灰度图的基础上运用了adaptiveThreshold来达成自动阈值的二值化,这个算法在提取直线和文字的时候比Canny更好用,其中~gray的意思是反色,使二值化后的图片是黑底白字,才能正确的提取直线。
cvtColor(rsz, gray, CV_BGR2GRAY);
Mat bw;
adaptiveThreshold(~gray, bw, 255, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
2、巧妙利用OpenCV里面的形态学函数,腐蚀erode膨胀dilate
//使用二值化后的图像来获取表格横纵的线
Mat horizontal = thresh.clone();
Mat vertical = thresh.clone();
int scale = 20; //这个值越大,检测到的直线越多
int horizontalsize = horizontal.cols / scale;
// 为了获取横向的表格线,设置腐蚀和膨胀的操作区域为一个比较大的横向直条
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontalsize, 1));
// 先腐蚀再膨胀
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));
imshow("horizontal", horizontal);
竖直方向上线条获取的步骤同上,唯一的区别在于腐蚀膨胀的区域为一个宽为1,高为缩放后的图片高度的一个竖长形直条
int verticalsize = vertical.rows / scale;
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size(1, verticalsize));
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));
imshow("vertical", vertical);
3、交叉横纵线条,定位点
Mat mask = horizontal + vertical;
imshow("mask", mask);
通过bitwise_and函数获得横纵线条的交点,通过交点再做后面的表格提取操作
Mat joints;
bitwise_and(horizontal, vertical, joints);
imshow("joints", joints);
4、判断区域是否为表格
在mask那张图上通过findContours 找到轮廓,判断轮廓形状和大小是否为表格。
approxPolyDP 函数用来逼近区域成为一个形状,true值表示产生的区域为闭合区域。
boundingRect 为将这片区域转化为矩形,此矩形包含输入的形状。
vector<Vec4i> hierarchy;
std::vector<std::vector<cv::Point> > contours;
cv::findContours(mask, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));
vector<vector<Point> > contours_poly(contours.size());
vector<Rect> boundRect(contours.size());
vector<Mat> rois;
for (size_t i = 0; i < contours.size(); i++)
{
//获取区域的面积,如果小于某个值就忽略,代表是杂线不是表格
double area = contourArea(contours[i]);
if (area < 100)
continue;
approxPolyDP(Mat(contours[i]), contours_poly[i], 3, true);
boundRect[i] = boundingRect(Mat(contours_poly[i]));
// find the number of joints that each table has
Mat roi = joints(boundRect[i]);
vector<vector<Point> > joints_contours;
findContours(roi, joints_contours, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
//从表格的特性看,如果这片区域的点数小于4,那就代表没有一个完整的表格,忽略掉
if (joints_contours.size() <= 4)
continue;
//保存这片区域
rois.push_back(src(boundRect[i]).clone());
//将矩形画在原图上
rectangle(src, boundRect[i].tl(), boundRect[i].br(), Scalar(0, 255, 0), 1, 8, 0);
}
//这里可以对保存的区域进行各种操作,如显示,保存等。也作为OCR等等复杂功能的输入。
for (size_t i = 0; i < rois.size(); ++i)
{
std::stringstream ss;
ss << "roi" << i << "";
imshow(ss.str(), rois[i]);
waitKey();
}
imshow("contours", src);
拆分的结果
显示表格区域在原图
问题
虽然这部分代码可以相对完整的识别图片中的表格,但是依然有几个问题是不容易解决的:
- 照片倾斜问题
- 照片背景复杂,干扰识别
- 表格只有上下两条线时的识别判断条件
- ……
后续重点会放在图片的透视变换,先提取纸张,再识别表格。