OpenCV-C++ 图像形态学操作应用-提取水平与垂直线

通过自定义的结构元素实现结构元素对输入图像对一些对象敏感,对另外一些对象不敏感,这样就会让敏感的对象改变而不敏感的对象保留输出;

常见的结构元素:矩形, 圆,直线,磁盘,钻石;

理解形态学操作-膨胀, 腐蚀

膨胀操作:

  • 利用结构元素,在图像上以滑动窗口的形式进行计算,提取出结构元素内最大值;

腐蚀操作:

  • 利用结构元素,在图像上以互动窗口的形式进行计算,提取出结构元素内最小值;

因此,重要的是结构元素的大小,形状,以及膨胀和腐蚀的不同的作用;

目标问题

如下图所示:

有以下几个目标问题:

  1. 提取出图像中的水平线
  2. 提取出图像中的垂直线
  3. 提取图像中的一些字母;

其实,上面这张图像还是有一点点难度的,下面处理后的效果还没有很理想;当然这篇文章重要的是为了说明形态学操作的应用,重在原理的理解;后续学到更多的知识,再回头改一改吧;

另外,上面这幅图像是利用代码绘制出来的,代码在这篇文章的最后;当然,如果你的电脑上有绘图软件的画,也可以自己画一些线段和字符;上面这幅图像的特征是:

  1. 有水平线, 垂直线, 字符;
  2. 线的长度, 宽度不一致;
  3. 字符大小不一致;

图像预处理

上面提供的图像是一个彩色图像,为了方便后续处理,执行两步操作:

  1. 将彩色图像转换称灰度图像
  2. 将灰度图像转换称二值图像

转换的结果如下图所示:

首先,第一步将彩色图像转化称灰度图像:

// 读取图像
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);

同样,也存在相同的问题,就是将字母中的垂直线提取出来,那么相应的解决办法也相同:

  1. 一方面调整结构元素的长度;
  2. 另一方面单独为腐蚀, 膨胀操作设置不同的结构元素;
  3. 甚至,在处理不同长度的线段时,使用不同的结构元素;

提取字母

尴尬了没有办法完成提取图像中的字符了,构造的图像难度有点高(对于目前初学的我解决不了了);

代码如下:

// 定义结构元素
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;
}
posted @ 2021-04-11 00:56  chenzhen0530  阅读(900)  评论(0编辑  收藏  举报