OpenCV-C++ 图像形态学操作应用-提取水平与垂直线
通过自定义的结构元素实现结构元素对输入图像对一些对象敏感,对另外一些对象不敏感,这样就会让敏感的对象改变而不敏感的对象保留输出;
常见的结构元素:矩形, 圆,直线,磁盘,钻石;
理解形态学操作-膨胀, 腐蚀
膨胀操作:
- 利用结构元素,在图像上以滑动窗口的形式进行计算,提取出结构元素内最大值;
腐蚀操作:
- 利用结构元素,在图像上以互动窗口的形式进行计算,提取出结构元素内最小值;
因此,重要的是结构元素的大小,形状,以及膨胀和腐蚀的不同的作用;
目标问题
如下图所示:
有以下几个目标问题:
- 提取出图像中的水平线
- 提取出图像中的垂直线
- 提取图像中的一些字母;
其实,上面这张图像还是有一点点难度的,下面处理后的效果还没有很理想;当然这篇文章重要的是为了说明形态学操作的应用,重在原理的理解;后续学到更多的知识,再回头改一改吧;
另外,上面这幅图像是利用代码绘制出来的,代码在这篇文章的最后;当然,如果你的电脑上有绘图软件的画,也可以自己画一些线段和字符;上面这幅图像的特征是:
- 有水平线, 垂直线, 字符;
- 线的长度, 宽度不一致;
- 字符大小不一致;
图像预处理
上面提供的图像是一个彩色图像,为了方便后续处理,执行两步操作:
- 将彩色图像转换称灰度图像
- 将灰度图像转换称二值图像
转换的结果如下图所示:
首先,第一步将彩色图像转化称灰度图像:
// 读取图像
Mat src = imread("/home/chen/dataset/random_line.png");
// 转换称灰度图像
Mat srcGray;
cvtColor(src, srcGray, COLOR_BGR2GRAY);
其次,第二步将灰度图像转换称二值图像:
Mat srcBinary;
adaptiveThreshold(~srcGray, srcBinary, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 11, -2);
其中,需要注意的有两点:
第一点是关于adaptiveThreshold
这个方法的使用, 使用方法参照:OpenCV-C++ 图像自适应阈值二值化处理adaptiveThreshold
第二点是关于为什么要使用~srcGray
,而不是直接使用srcGray
,这是因为我们想让背景变成黑色,前景变成白色,并配合adativeThreshold
的使用;
提取水平线
如上面二值图像所示,想要提取其中水平线部分,我们需要对图像执行开操作, 即先腐蚀,后膨胀;腐蚀的目的是为了先将除水平线之外的线条过滤掉;
那么,如果过滤呢? 这就需要设计腐蚀操作的结构元素了;
想一下假如,有一个横向的一维结构元素,在腐蚀操作的时候,是不是就将一些纵向的线段腐蚀掉了呢.(背景是黑色,前景是白色)
代码如下:
// 定义水平(横向)结构元素
Mat hline = getStructuringElement(MORPH_RECT, Size(20, 1), Point(-1, -1)); // 水平线
// 开操作: 先腐蚀,后膨胀
Mat dst;
erode(srcBinary, dst, hline); // 腐蚀操作, 过滤掉非水平线
dilate(dst, dst, hline); // 膨胀操作
你可能也注意到了,在想要提取水平线的同时,也将字母中的水平线提取出来了.
可以通过增加水平结构元素的长度,即增加Size(20, 1)
, 影响如下:
- 不再提取出字母中的水平线
- 有可能会遗漏右下角较短的水平短线;
- 会将中间部分,横线的断裂给连接上;(当然这一步也可以单独为膨胀操作单独设置水平结构元素来实现;)
提取垂直线
与提取水平线原理类似:
如上面二值图像所示,想要提取其中垂直线部分,我们需要对图像执行开操作, 即先腐蚀,后膨胀;腐蚀的目的是为了先将除垂直线之外的线条过滤掉;
那么,如果过滤呢? 这就需要设计腐蚀操作的结构元素了;
想一下假如,有一个纵向的一维结构元素,在腐蚀操作的时候,是不是就将一些横向的线段腐蚀掉了呢.(背景是黑色,前景是白色)
代码如下:
// 垂直结构元素
Mat vline = getStructuringElement(MORPH_RECT, Size(1, 50), Point(-1, -1)); // 垂直线
// 开操作: 先腐蚀,后膨胀
Mat dst;
erode(srcBinary, dst, kernel);
dilate(dst, dst, kernel);
同样,也存在相同的问题,就是将字母中的垂直线提取出来,那么相应的解决办法也相同:
- 一方面调整结构元素的长度;
- 另一方面单独为腐蚀, 膨胀操作设置不同的结构元素;
- 甚至,在处理不同长度的线段时,使用不同的结构元素;
提取字母
尴尬了没有办法完成提取图像中的字符了,构造的图像难度有点高(对于目前初学的我解决不了了);
代码如下:
// 定义结构元素
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
// 开操作: 先腐蚀,后膨胀
Mat dst;
erode(srcBinary, dst, kernel);
dilate(dst, dst, kernel);
其实,原因好像也能理解:主要是因为线段的宽度与字符宽度类似;
或许,再思考一下,直接把水平线和垂直线过滤掉算了,当然重要的是参数的调整;
(尝试着调整了一下,完成不了,shit)
总结
- 重点理解形态学操作膨胀, 腐蚀的原理;
- 不同的场景下,需要的结构元素,长度,形状是不同的;
- 需要从任务角度进行分析,需要使用哪些处理操作;
- 有些问题,还没有很好的解决,待学习了更多的知识后,再回头解决吧;
相关完整代码
处理过程完整代码:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(){
// 读取图像
Mat src = imread("/home/chen/dataset/random_line.png");
if (! src.data){
cout << "could not load image." << endl;
return -1;
}
namedWindow("src", WINDOW_AUTOSIZE);
imshow("src", src);
// 转换成灰度图像
Mat srcGray;
cvtColor(src, srcGray, COLOR_BGR2GRAY);
namedWindow("srcGray", WINDOW_AUTOSIZE);
imshow("srcGray", srcGray);
// // 转换为二值图像
Mat srcBinary;
adaptiveThreshold(~srcGray, srcBinary, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 11, -2);
namedWindow("srcBinary", WINDOW_AUTOSIZE);
imshow("srcBinary", srcBinary);
// 水平结构元素
Mat hline = getStructuringElement(MORPH_RECT, Size(20, 1), Point(-1, -1)); // 水平线
// 垂直结构元素
Mat vline = getStructuringElement(MORPH_RECT, Size(1, 50), Point(-1, -1)); // 垂直线
// 提取字母
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
// 开操作: 先腐蚀,后膨胀
Mat dst;
erode(srcBinary, dst, kernel);
dilate(dst, dst, kernel);
// 水平结构元素
Mat hline2 = getStructuringElement(MORPH_RECT, Size(40, 1), Point(-1, -1)); // 水平线
// 垂直结构元素
Mat vline2 = getStructuringElement(MORPH_RECT, Size(1, 50), Point(-1, -1)); // 垂直线
Mat temp1, temp2, dst_temp;
erode(dst, temp1, hline2);
dst_temp = dst - temp1;
erode(dst, temp2, vline2);
dst = dst_temp - temp2;
imshow("temp1", temp1);
imshow("temp2", temp2);
namedWindow("dst", WINDOW_AUTOSIZE);
imshow("dst", dst);
// 显示图像
waitKey(0);
return 0;
}
绘制上面的目标问题图像:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(){
// 创建一张空白图像
Mat src = Mat(Size(512, 512), CV_8UC3, Scalar(255, 255, 255));
RNG rng(12345);
// 随机生成一些水平线
Point p1, p2;
Scalar color;
const int MAX_THICKNESS = 5;
int thickness;
const int NUM_HORIZONTAL_LINE = 3;
for (int i = 0; i <= NUM_HORIZONTAL_LINE; i++){
p1.x = rng.uniform(0, src.cols);
p1.y = rng.uniform(MAX_THICKNESS, src.rows - MAX_THICKNESS);
p2.x = rng.uniform(0, src.cols);
p2.y = p1.y;
color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
thickness = rng.uniform(1, MAX_THICKNESS);
line(src, p1, p2, color, thickness);
}
// 随机生成一些垂直线
const int NUM_VERTICAL_LINE = 3;
for (int i = 0; i <= NUM_VERTICAL_LINE; i++){
p1.x = rng.uniform(MAX_THICKNESS, src.cols- MAX_THICKNESS);
p1.y = rng.uniform(0, src.rows);
p2.x = p1.x;
p2.y = rng.uniform(0, src.rows);
color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
thickness = rng.uniform(1, MAX_THICKNESS);
line(src, p1, p2, color, thickness);
}
// 在随机位置生成一些数字
putText(src, "A", Point(90, 128), FONT_HERSHEY_COMPLEX, 2, Scalar(0, 0, 255), 3);
putText(src, "B", Point(200, 128), FONT_HERSHEY_COMPLEX, 2.5, Scalar(0, 0, 255), 3);
putText(src, "E", Point(320, 128), FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 3);
// 显示图像
namedWindow("src", WINDOW_AUTOSIZE);
imshow("src", src);
waitKey(0);
// 保存图像
imwrite("/home/chen/dataset/random_line.png", src);
return 0;
}