QT+opencv学习笔记——边缘检测、轮廓提取及轮廓跟踪

开发环境为:win10+QT5.8+opencv3.2

  数字图像的边缘检测是图像分割、目标区域的识别、区域形状提取等图像分析领域十分重要的基础,图像分析和理解的第一步往往就是边缘检测。轮廓跟踪是获取图像的外部轮廓特征,为图像的形状分析做准备。本文主要实现图像边缘检测、轮廓提取、轮廓跟踪。

一、读取图像

     读取图像见QT+opencv学习笔记(1)——图像点运算,这里不再赘述。

     读取结果如下图:

二、边缘检测

   边缘是指图像局部强度变化最显著的部分。边缘主要存在与目标与目标、目标与背景、区域与区域之间。图像强度的不连续性可分为:阶跃不连续,即图像强度在不连续处的两边的像素灰度值有显著的差异;线条不连续,即图像强度从一个值变化到另一个值,保持一较小行程后又回到原来的值。

边缘检测算子检查每个像素的邻域并对灰度变换率进行量化,也包括方向的确定。大多数使用基于方向倒数掩模求卷积的方法。

下面介绍几种常用的边缘检测算子。

Canny算子

   Canny算子运用比较广泛。是在Sobel算子的基础上改进的。

   Canny算子的步骤是:

          1.先进行滤波降噪。

          2.计算梯度幅值和方向(进行Sobel算子计算)。

          3.非极大值抑制。

          4.滞后阈值。

    Canny边缘检测可通过Canny()函数来实现。Canny()函数的定义如下:

//推荐高低阈值比例介于2:1与3:1之间
void Canny(InputArray image, //8位单通道输入图像
OutputArray edges, //输出图像,和输入图像的尺寸类型一致
double threshold1, //滞后阈值低阈值(用于边缘连接)
double threshold2, //滞后阈值高阈值(控制边缘初始段)
int apertureSize=3, //表示Sobel算子孔径大小,默认为3
bool L2gradient=false //计算图像梯度幅值的标识
);
Canny边缘检测主要代码如下:

//Canny边缘检测
Canny(grayImg, edgeImg, 30, 80);
Canny边缘检测处理结果如下:

Sobel算子

   Sobel算子是一个主要用于边缘检测的离散微分算子,它结合了高斯平滑和微分求导,用于计算图像灰度函数的近似梯度。

   Sobel算子检测方法对灰度渐变和噪声较多的图像处理效果较好,对边缘定位不是很准确,图像的边缘不止一个像素。

   Sobel边缘检测可通过Sobel()函数来实现。Sobel()函数的定义如下:

void Sobel(InputArray src, //输入图像
OutputArray dst, //输出图像,和输入图像的尺寸类型一致
int ddepth, //输出图像的深度
int xorder, //x方向上的差分阶数
int yorder, //y方向上的差分阶数
int ksize=3, //Sobel核的大小,取值1,3,5,7
double scale=1, //计算倒数时的缩放因子
double delta=0, //可选delta值
int borderType=BORDERDEFAULT //边界模式,一般默认即可
)
Sobel边缘检测主要代码如下:

        //Sobel边缘检测
        Mat x_edgeImg, y_edgeImg;
        Mat abs_x_edgeImg, abs_y_edgeImg;

        /*先对x方向进行边缘检测**/
        //因为Sobel求出来的结果有正负,8位无符号表示不全,故用16位有符号表示
        Sobel(grayImg,x_edgeImg, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);
        convertScaleAbs(x_edgeImg, abs_x_edgeImg);//将16位有符号转化为8位无符号

        /*再对y方向进行边缘检测**/
        Sobel(grayImg, y_edgeImg, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);
        convertScaleAbs(y_edgeImg, abs_y_edgeImg);

        addWeighted(abs_x_edgeImg, 0.5, abs_y_edgeImg, 0.5, 0, edgeImg);
   Sobel边缘检测处理结果如下:

Laplacian算子

    Laplacian算子边缘检测是通过二阶倒数,二阶倒数比一阶倒数的好处是在与受到周围的干扰小,其不具有方向性,操作容易,且对于很多方向的图像处理好。

    Laplacian算子对噪声比较敏感,所以很少用该算子检测边缘,而是用来判断边缘像素视为与图像的明区还是暗区。

     Laplacian边缘检测可通过Laplacian()函数来实现。Laplacian()函数的定义如下:

void Laplacian(InputArray src, //输入图像
OutputArray dst, //输出图像,和输入图像的尺寸类型一致
int ddepth, //输出图像的深度
int ksize=1, //用于计算二阶导数的滤波器孔径大小,须为正奇数,默认值为1
double scale=1, //可选比例因子,默认值为1
double delta=0, //可选delta值
int borderType=BORDER_DEFAULT //边界模式,一般默认即可
)
Laplacian边缘检测主要代码如下:

        //Laplacian边缘检测
        Mat lapImg;

        Laplacian(grayImg, lapImg, CV_16S, 5, 1, 0, BORDER_DEFAULT);
        convertScaleAbs(lapImg, edgeImg);
    Laplacian边缘检测处理结果如下:

三、轮廓提取

扫描二维码关注公众号,回复: 4420032 查看本文章
     轮廓的提取,边缘检测就可以做到,不过得到的轮廓比较粗糙。

     图像轮廓的提取先对图像二值化,再通过findContours()函数提取轮廓,最后通过drawContours()函数将轮廓绘制出来。在将轮廓提取的结果使用imwrite函数保存到本地时,总是写不了,查了半天没找出问题,刚开始文件名为con.bmp,最后把文件名改成cont.bmp就好了,玄学。。。

   findContours()函数定义如下:

void cv::findContours(
cv::InputOutputArray image, // 输入的8位单通道“二值”图像
cv::OutputArrayOfArrays contours, // 包含points的vectors的vector
cv::OutputArray hierarchy, // (可选) 拓扑信息
int mode, // 轮廓检索模式
int method, // 近似方法
cv::Point offset = cv::Point() // (可选) 所有点的偏移
);
drawContours()函数定义如下:

void cv::drawContours(
cv::InputOutputArray image, // 用于绘制的输入图像
cv::InputArrayOfArrays contours, // 点的vectors的vector
int contourIdx, // 需要绘制的轮廓的指数 (-1 表示 “all”)
const cv::Scalar& color, // 轮廓的颜色
int thickness = 1, // 轮廓线的宽度
int lineType = 8, // 轮廓线的邻域模式('4’邻域 或 '8’邻域)
cv::InputArray hierarchy = noArray(), // 可选 (从 findContours得到)
int maxLevel = INT_MAX, // 轮廓中的最大下降
cv::Point offset = cv::Point() // (可选) 所有点的偏移
)
轮廓提取主要代码如下:

Mat contImg = Mat ::zeros(grayImg.size(),CV_8UC3);//定义三通道轮廓提取图像

Mat binImg;
threshold(grayImg, binImg, 127, 255, THRESH_OTSU);//大津法进行图像二值化

vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
//查找轮廓
findContours(binImg, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE);
//绘制查找到的轮廓
drawContours(contImg, contours, -1, Scalar(0,255,0));
    轮廓提取处理结果如下:

四、轮廓跟踪

    通常在进行边缘检测之后,需要通过边缘跟踪来将离散的边缘串接起来,常使用的方法边缘跟踪法。边缘跟踪又分为八邻域和四邻域两种。

    实现步骤:

    1、灰度化并进行Canny边缘检测;

    2、按照预先设定的跟踪方向(顺时针)进行边缘跟踪;

    3、每次跟踪的终止条件为:8邻域都不存在轮廓。

    主要代码如下:

Mat edgeImg,trackImg;

// Canny边缘检测
Canny(grayImg, edgeImg, 50, 100);
vector<Point> edge_t;
vector<vector<Point>> edges;
//边缘跟踪
EdgeTracking(edgeImg,edge_t,edges,trackImg);

void Dialog::EdgeTracking(Mat& Edge,vector& edge_t,vector<vector>& edges,Mat& trace_edge_color)
{
// 8 neighbors
const Point directions[8] = { { 0, 1 }, {1,1}, { 1, 0 }, { 1, -1 }, { 0, -1 }, { -1, -1 }, { -1, 0 },{ -1, 1 } };
int i, j, counts = 0, curr_d = 0;
for (i = 1; i < Edge.rows - 1; i++)
for (j = 1; j < Edge.cols - 1; j++)
{
// 起始点及当前点
//Point s_pt = Point(i, j);
Point b_pt = Point(i, j);
Point c_pt = Point(i, j);
// 如果当前点为前景点
if (255 == Edge.at(c_pt.x, c_pt.y))
{
edge_t.clear();
bool tra_flag = false;
// 存入
edge_t.push_back(c_pt);
Edge.at(c_pt.x, c_pt.y) = 0; // 用过的点直接给设置为0

            // 进行跟踪
            while (!tra_flag)
            {
                // 循环八次
                for (counts = 0; counts < 8; counts++)
                {
                    // 防止索引出界
                    if (curr_d >= 8)
                    {
                        curr_d -= 8;
                    }
                    if (curr_d < 0)
                    {
                        curr_d += 8;
                    }
                    // 当前点坐标
                    // 跟踪的过程,应该是个连续的过程,需要不停的更新搜索的root点
                    c_pt = Point(b_pt.x + directions[curr_d].x, b_pt.y + directions[curr_d].y);
                    // 边界判断
                    if ((c_pt.x > 0) && (c_pt.x < Edge.cols - 1) &&
                        (c_pt.y > 0) && (c_pt.y < Edge.rows - 1))
                    {
                        // 如果存在边缘
                        if (255 == Edge.at<uchar>(c_pt.x, c_pt.y))
                        {
                            curr_d -= 2;   // 更新当前方向
                            edge_t.push_back(c_pt);
                            Edge.at<uchar>(c_pt.x, c_pt.y) = 0;

                            // 更新b_pt:跟踪的root点
                            b_pt.x = c_pt.x;
                            b_pt.y = c_pt.y;

                            //cout << c_pt.x << " " << c_pt.y << endl;
                            break;   // 跳出for循环
                        }
                    }
                    curr_d++;
                }   // end for
                // 跟踪的终止条件:如果8邻域都不存在边缘
                if (8 == counts )
                {
                    // 清零
                    curr_d = 0;
                    tra_flag = true;
                    edges.push_back(edge_t);
                    break;
                }
            }  // end if
        }  // end while
    }
// 显示一下
Mat trace_edge = Mat::zeros(Edge.rows, Edge.cols, CV_8UC1);
//Mat trace_edge_color;
cvtColor(trace_edge, trace_edge_color, CV_GRAY2BGR);
for (i = 0; i < edges.size(); i++)
{
    Scalar color = Scalar(rand()%255, rand()%255, rand()%255);
    // 过滤掉较小的边缘
    if (edges[i].size() > 5)
    {
        for (j = 0; j < edges[i].size(); j++)
        {
            trace_edge_color.at<Vec3b>(edges[i][j].x, edges[i][j].y)[0] = color[0];
            trace_edge_color.at<Vec3b>(edges[i][j].x, edges[i][j].y)[1] = color[1];
            trace_edge_color.at<Vec3b>(edges[i][j].x, edges[i][j].y)[2] = color[2];
        }
    }

}

}
轮廓跟踪处理结果如下:

    整体工程代码见<a href="https://download.csdn.net/download/minghui/10445804" rel="nofollow" target="_blank">QT+opencv边缘检测,轮廓提取及轮廓跟踪

参考:

(1)边缘检测概念理解

(2)【OpenCV学习笔记】十九、图像边缘检测

(3)OPENCV轮廓提取findContours和drawContours

(4)【OpenCV3】图像轮廓查找与绘制——cv::findContours()与cv::drawContours()详解

(5)图像处理(五):八邻域边缘跟踪与区域生长算法

猜你喜欢

转载自blog.csdn.net/weixin_43197380/article/details/83504883