【opencv】第8章 图像轮廓与图像分割修复

8.1 查找并绘制轮廓

一个轮廓一般对应一系列的点,也就是图像中的一条曲线。其表示方法可能 根据不同的情况而有所不同。在OpenCV 中,可以用findContours()函数从二值图 像中查找轮廓

8.1.1 寻找轮廓: findContours() 函数

findContours) 函数用于在二值图像中寻找轮廓。

cpp 复制代码
void findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, intmethod, Point offset = Point())
  • 第一个参数,InputArray类型的image, 输入图像,即源图像,填Mat 类的 对象即可,且需为8位单通道图像。图像的非零像素被视为1,0像素值被 保留为0,所以图像为二进制。我们可以使用 compare() 、inrange()、
    threshold() 、adaptivethreshold() 、cannyO 等函数由灰度图或彩色图创建二进 制图像。此函数会在提取图像轮廓的同时修改图像的内容。
  • 第二个参数,OutputArrayOfArrays 类 型 的contours、检测到的轮廓、函数 调用后的运算结果存在这里。每个轮廓存储为一个点向量,即用point 类 型 的 vector表示。
  • 第三个参数,OutputArray 类型的hierarchy,可选的输出向量,包含图像的 拓扑信息。其作为轮廓数量的表示,包含了许多元素。每个轮廓contours[i] 对 应 4 个hierarchy 元 素hierarchy[i][0]~hierarchy[i][3], 分别表示后一 个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。如果没有对应项,
    对应的hierarchy[i] 值设置为负数。
  • 第四个参数,int 类型的mode, 轮廓检索模式,取值如表8.1所示。

表8.1 findContours函数可选的轮廓检索模式

标识符 含义
RETR_EXTERNAL 表示只检测最外层轮廓。对所有轮廓,设置 hierarchy[i][2]=hierarchy[i][3]=-1
RETR_LIST 提取所有轮廓,并且放置在list中。检测的轮廓 不建立等级关系
RETR_CCOMP 提取所有轮廓,并且将其组织为双层结构(two-level hierarchy:顶层为连通域的外围边界, 次层为孔的内层边界
RETR_TREE 提取所有轮廓,并重新建立网状的轮廓结构
  • 第五个参数,int类 型 的method, 为轮廓的近似办法,取值如表8.2所示。

表8.2 findContours函数可选的轮廓近似办法

标识符 含义
CHAIN_APPROX NONE 获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过 1,即max(abs(xl-x2),abs(y2-y1))=1
CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向 的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息
CHAIN_APPROX_TC89_L1 ,CHAIN_APPROX_TC89_KCOS 使用Teh-Chinl链逼近算法中的一个

同样地,在表8.1和8.2中列出的宏之前加上"CV_" 前缀,便是OpenCV2 中可以使用的宏。如"RETR_CCOMP" 宏 的OpenCV2 版 为"CV_RETR_CCOMP"。

  • 第六个参数,Point 类 型 的 offset,每个轮廓点的可选偏移量,有默认值 Point()。对 ROI 图像中找出的轮廓,并要在整个图像中进行分析时,这个 参数便可排上用场。

findContours 经 常 与drawContours 配合使用一使用用findContours (函数检测 到图像的轮廓后,便可以用drawContours (函数将检测到的轮廓绘制出来。接下来, 让我们一起看看drawContours() 函数的用法。

8.1.2 绘 制 轮 廓 :drawContours ()函 数

drawContours() 函数用于在图像中绘制外部或内部轮廓。

cpp 复制代码
void drawContours(InputoutputArray image, InputArrayofArrays contours, int contourIdx, const Scalar &color, int thickness = 1, int lineType = 8, InputArray hierarchy = noArray(0), int maxLevel = INT_MAX, Point offset = Point())
  • 第 一 个参数,InputArray类 型 的image, 目标图像,填Mat 类的对象即可。
  • 第二个参数,InputArrayOfArrays类型的contours,所有的输入轮廓。每个 轮廓存储为一个点向量,即用point类 型 的vector表示。
  • 第三个参数,int类 型 的contourldx,轮廓绘制的指示变量。如果其为负值, 则绘制所有轮廓。
  • 第四个参数,const Scalar&类型的color, 轮廓的颜色。
  • 第五个参数,int thickness,轮廓线条的粗细度,有默认值1。如果其为负 值(如 thickness=cv_filled), 便会绘制在轮廓的内部。可选为FILLED 宏(OpenCV2版为CV_FILLED)。
  • 第六个参数,int 类型的lineType,线条的类型,有默认值8。取值类型如 表8 . 3所示。

表8.3 可选线性

lineType线性 含义
8(默认值) 8连通线型
4 4连通线型
LINE_AA(OpenCV2版为CV_AA) 抗锯齿线型
  • 第七个参数,InputArray 类型的hierarchy,可选的层次结构信息,有默认 值noArray()。
  • 第八个参数,int类型的maxLevel,表示用于绘制轮廓的最大等级,有默认 值INT_MAX
  • 第九个参数,Point类型的offset,可选的轮廓偏移参数,用指定的偏移量 offset=(dx,dy) 偏移需要绘制的轮廓,有默认值Point()。

下面是 一 个调用小示例。 //在白色图像上绘制黑色轮廓

cpp 复制代码
Mat result(image.size(),CV_8U,cv::Scalar(255));
drawContours(result,contours,- 1,Scalar(0),3);

8.1.3 基础示例程序:轮廓查找

cpp 复制代码
void Test52() {
    Mat srcImage = imread("image.jpg", 0); //灰度图读入

    imshow("src", srcImage);
    Mat dstImage = Mat::zeros(srcImage.rows, srcImage.cols, CV_8UC3);
    srcImage = srcImage > 119;

    imshow("mid", srcImage); //取阈值后的图像

    //定义轮廓和层次结构

    std::vector<std::vector<Point>>contours;
    std::vector<Vec4i>hierachy;

    //查找轮廓

    findContours(srcImage, contours, hierachy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);

    //遍历所有顶层的罗坤,随机颜色绘制出每个连接组件颜色

    int index = 0;
    for (; index >= 0; index = hierachy[index][0]) {
        Scalar color(rand() & 255, rand() & 255, rand() & 255);
        drawContours(dstImage, contours, index, color, FILLED, 8, hierachy);
        imshow("dst", dstImage);
    }

    waitKey(0);
}

8.1.4 综合示例程序:查找并绘制轮廓

除了上述这个精简版的示例程序,还为大家准备了一个更加复杂一些的关 于查找并绘制轮廓的综合示例程序。此程序利用了图像平滑技术(blur() 函数)和边缘检测技术(cannyO 函数),根据滑动条的调节,可以动态地检测出图形的 轮 廓 。

cpp 复制代码
namespace test53 {
    Mat g_srcImage, g_grayImage;
    int g_nThresh = 80;
    int g_nThresh_max = 255;
    RNG g_rng(12345);

    Mat g_cannyMat_output;
    std::vector<std::vector<Point>>g_vContours;
    std::vector<Vec4i>g_vHierarchy;

    void on_ThreshChange(int, void*) {
        Canny(g_grayImage, g_cannyMat_output, g_nThresh, g_nThresh * 2, 3); //Canny算子边缘检测

        findContours(g_cannyMat_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));

        //轮廓提取

        Mat drawing = Mat::zeros(g_cannyMat_output.size(), CV_8UC3);
        for (int i = 0; i < g_vContours.size(); ++i) {
            Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));//随机值

            drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point());
        }

        imshow("drawing", drawing);
    }

    void Test() {
        g_srcImage = imread("image.jpg");

        cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
        blur(g_grayImage, g_grayImage, Size(3, 3)); //降噪

        namedWindow("window1");
        imshow("window1", g_srcImage);

        createTrackbar("value", "window1", &g_nThresh, g_nThresh_max, on_ThreshChange);
        on_ThreshChange(0, 0);
        waitKey(0);
    }
}

void Test53() {
    test53::Test();
}


8.2 寻找物体的凸包

8.2.1 凸 包

凸包(Convex Hull) 是一个计算几何(图形学)中常见的概念。简单来说, 给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边型,它是 能包含点集中所有点的。理解物体形状或轮廓的一种比较有用的方法便是计算一 个物体的凸包,然后计算其凸缺陷(convexity defects)。很多复杂物体的特性能很 好地被这种缺陷表现出来。

如图8.9所示,我们用人手图来举例说明凸缺陷这一概念。手周围深色的线 描画出了凸包,A 到 H 被标出的区域是凸包的各个"缺陷"。正如看到的,这些 凸度缺陷提供了手以及手状态的特征表现的方法。

新版OpenCV 中 ,convexHull 函数用于寻找图像点集中的凸包,我们一起来 看一下这个函数。

8.2.2 寻找凸包:convexHullO函数

上文已经提到过,convexHullO 函数用于寻找图像点集中的凸包,其原型声明 如 下 。

cpp 复制代码
void convexHull(InputArray points, OutputArray hull, bool clockwise = false, bool returnPoints = true)
  • 第一个参数,InputArray类型的points,输入的二维点集,可以填Mat 类型 或者std::vector。
  • 第二个参数,OutputArray类型的hull,输出参数,函数调用后找到的凸包。
  • 第三个参数,bool 类型的clockwise,操作方向标识符。当此标识符为真时, 输出的凸包为顺时针方向。否则,就为逆时针方向。并且是假定坐标系的 x 轴指向右,y 轴指向上方。
  • 第四个参数,bool 类型的returnPoints,操作标志符,默认值true。当 标 志 符为真时,函数返回各凸包的各个点。否则,它返回凸包各点的指数。当 输出数组是std::vector 时,此标志被忽略。

8.2.3 基础示例程序:凸包检测基础

为了理解凸包检测的运用方法,下面放出一个完整的示例程序。程序中会首 先随机生成3~103个坐标值随机的彩色点,然后利用convexHull, 对由这些点链 接起来的图形求凸包。

cpp 复制代码
void Test54() {
    Mat image(600, 600, CV_8UC3);
    RNG& rng = theRNG();

    while (1) {
        char key;
        int count = (unsigned)rng % 100 + 3;
        std::vector<Point>points;

        //随机生成坐标

        for (int i = 0; i < count; ++i) {
            Point point;
            point.x = rng.uniform(image.cols / 4, image.cols * 3 / 4);
            point.y = rng.uniform(image.rows / 4, image.rows * 3 / 4);

            points.push_back(point);
        }

        //检测凸包

        std::vector<int>hull;
        convexHull(Mat(points), hull, true);

        //绘制出随机颜色的点

        image = Scalar::all(0);
        for (int i = 0; i < count; ++i) {
            circle(image, points[i], 3, Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),FILLED,LINE_AA);
        }

        int hullcount = hull.size();
        Point point0 = points[hull.back()];

        //依次连接凸包的点

        for (int i = 0; i < hullcount; ++i) {
            Point point = points[hull[i]];
            line(image, point0, point, Scalar(255, 255, 255), 2, LINE_AA);
            point0 = point;
        }
        imshow("hull", image);

        key = waitKey(0);

        if (key == 27) break;
    }


}

8.2.4 综合示例程序:寻找和绘制物体的凸包

这一节的综合示例程序,依然是结合滑动条,通过滑动条控制阈值,来得到 不同的凸包检测效果图。程序详细注释的源代码如下。

cpp 复制代码
namespace test55 {
    Mat g_srcImage, g_grayImage;

    int g_nThresh = 50;
    int g_maxThresh = 255;

    RNG g_rng(12345);
    Mat srcImage_copy = g_srcImage.clone();
    Mat g_thresholdImage_output;
    std::vector<std::vector<Point>>g_vContours;
    std::vector<Vec4i>g_vHierarchy;

    void on_ThreshChange(int, void*) {
        // 二值化

        threshold(g_grayImage, g_thresholdImage_output, g_nThresh, 255, THRESH_BINARY);
        findContours(g_thresholdImage_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); //轮廓

        std::vector<std::vector<Point>>hull(g_vContours.size());
        for (int i = 0; i < g_vContours.size(); ++i) {
            convexHull(Mat(g_vContours[i]), hull[i], false);
        }

        //绘制出轮廓及凸包

        Mat drawing = Mat::zeros(g_thresholdImage_output.size(), CV_8UC3);
        for (int i = 0; i < g_vContours.size(); ++i) {
            Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));  

            drawContours(drawing, g_vContours, i, color, 1, 8, std::vector<Vec4i>(), 0, Point()); //轮廓

            drawContours(drawing, hull, i, color, 1, 8, std::vector<Vec4i>(), 0, Point()); //凸包

        }

        imshow("drawing", drawing);
    }

    void Test() {
        g_srcImage = imread("image.jpg");
        cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
        blur(g_grayImage, g_grayImage, Size(3, 3));//降噪

        //创建窗口

        namedWindow("src");
        imshow("src", g_srcImage);

        //创建滚动条

        createTrackbar("value", "src", &g_nThresh, g_maxThresh, on_ThreshChange);
        on_ThreshChange(0, nullptr);

        waitKey(0);
    }
}

void Test55() {
    test55::Test();
}

8.3 使用多边形将轮廓包围

在实际应用中,常常会有将检测到的轮廓用多边形表示出来的需求。本节就 为大家讲解如何用多边形来表示出轮廓,或者说如何根据轮廓提取出多边形。先 让我们一起学习用OpenCV 创建包围轮廓的多边形边界时会接触到的一些函数。

8.3.1 返回外部矩形边界:boundingRect(O函数

此函数计算并返回指定点集最外面(up-right)的矩形边界。 C++:Rect boundingRect(InputArray points)

其唯一的一个参数为输入的二维点集,可以是std::vector 或 Mat 类型。

8.3.2 寻找最小包围矩形:minAreaRect(函数

此函数用于对给定的2D 点集,寻找可旋转的最小面积的包围矩形。 C++:RotatedRect minAreaRect(InputArray points)

其唯一的一个参数为输入的二维点集,可以为std::vector> 或 Mat 类型。

8.3.3寻找最小包围圆形:minEnclosingCircle(函数

minEnclosingCircle函数的功能是利用一种迭代算法,对给定的2D 点集,去 寻找面积最小的可包围它们的圆形。

cpp 复制代码
void minEnclosingCircle(InputArray points, Point2f &center, float &radius)
  • 第 一 个 参 数 ,InputArray 类 型 的points,输入的二维点集,可以为std::vector◇ 或Mat 类型。
  • 第二个参数,Point2f&类型的center,圆的输出圆心。
  • 第三个参数,float&类型的radius,圆的输出半径。

8.3.4 用椭圆拟合二维点集:fitEllipse ( 函 数

此函数的作用是用椭圆拟合二维点集。

cpp 复制代码
RotatedRect fitEllipse(InputArray points)

其唯一的一个参数为输入的二维点集,可以为std::vector<>或Mat 类型。

8.3.5 逼近多边形曲线: approxPolyDPO 函 数

approxPolyDP函数的作用是用指定精度逼近多边形曲线。

cpp 复制代码
void approxPolyDP(InputArray curve, OutputArray approxCurve,
                  double epsilon, bool closed)
  • 第一个参数,InputArray类型的curve,输入的二维点集,可以为std::vecto 或Mat 类型。
  • 第二个参数,OutputArray类型的approxCurve,多边形逼近的结果,其类 型应该和输入的二维点集的类型 一 致。
  • 第三个参数,double类型的epsilon,逼近的精度,为原始曲线和即近似曲 线间的最大值。
  • 第四个参数,bool 类型的closed,如果其为真,则近似的曲线为封闭曲线 (第一个顶点和最后一个顶点相连),否则,近似的曲线曲线不封闭。

8.4 图像的矩

矩函数在图像分析中有着广泛的应用,如模式识别、目标分类、目标识别与 方位估计、图像编码与重构等。一个从一幅数字图形中计算出来的矩集,通常描 述了该图像形状的全局特征,并提供了大量的关于该图像不同类型的几何特性信 息,比如大小、位置、方向及形状等。图像矩的这种特性描述能力被广泛地应用 在各种图像处理、计算机视觉和机器人技术领域的目标识别与方位估计中。一阶 矩与形状有关,二阶矩显示曲线围绕直线平均值的扩展程度,三阶矩则是关于平 均值的对称性的测量。由二阶矩和三阶矩可以导出一组共7个不变矩。而不变矩 是图像的统计特性,满足平移、伸缩、旋转均不变的不变性,在图像识别领域得 到了广泛的应用。

那 么 , 在OpenCV 中,如何计算 一个图像的矩呢? 一般由 moments、 contourArea 、arcLength 这三个函数配合求取。

  • 使用moments 计算图像所有的矩(最高到3阶)
  • 使用contourArea来计算轮廓面积
  • 使用arcLength来计算轮廓或曲线长度 下面对其进行一一剖析。

8.4.1 矩的计算:momentsO函数

moments()函数用于计算多边形和光栅形状的最高达三阶的所有矩。矩用来计 算形状的重心、面积,主轴和其他形状特征,如7Hu不变量等。

cpp 复制代码
Moments moments(InputArray array, bool binaryImage = false)
  • 第一个参数,InputArray类型的array,输入参数,可以是光栅图像(单通 道,8位或浮点的二维数组)或二维数组 (IN 或 N1)。
  • 第二个参数,bool类型的binaryImage,有默认值false。若此参数取 true, 则所有非零像素为1。此参数仅对于图像使用。
    需要注意的是,此参数的返回值返回运行后的结果。

8.4.2计算轮廓面积:contourArea(函数

contourArea()函数用于计算整个轮廓或部分轮廓的面积

cpp 复制代码
double       contourArea(InputArray       contour,bool       oriented=false)
  • 第一个参数,InputArray类型的contour,输入的向量,二维点(轮廓顶点), 可以为std::vector 或 Mat 类型。
  • 第二个参数,bool类型的oriented,面向区域标识符。若其为true,该函数 返回一个带符号的面积值,其正负取决于轮廓的方向(顺时针还是逆时针)。 根据这个特性我们可以根据面积的符号来确定轮廓的位置。需要注意的是, 这个参数有默认值false, 表示以绝对值返回,不带符号。

8.4.3 计算轮廓长度:arcLengthO函数

arcLength(函数用于计算封闭轮廓的周长或曲线的长度。

cpp 复制代码
double      arcLength(InputArray      curve,bool      closed)
  • 第一个参数,InputArray类型的curve,输入的二维点集,可以为std:vector或Mat 类型。
  • 第二个参数,bool 类型的closed, 一个用于指示曲线是否封闭的标识符, 有默认值closed, 表示曲线封闭。

8.4.4 综合示例程序:查找和绘制图像轮廓矩

学习完函数的讲解,让我们一起通过一个综合的示例程序,真正了解本节内 容的实战用法 。

cpp 复制代码
namespace test56{
    Mat g_srcImage, g_grayImage;
    int g_nThresh = 100;
    int g_nMaxThresh = 255;
    RNG g_rng(12345);
    Mat g_cannyMat_output;
    std::vector<std::vector<Point>>g_vContours;
    std::vector<Vec4i>g_vHierarchy;

    //回调函数

    void on_ThreshChange(int, void*) {
        Canny(g_grayImage, g_cannyMat_output, g_nThresh, g_nThresh * 2); //边缘检测

        findContours(g_cannyMat_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); //找到轮廓

        //计算矩

        std::vector<Moments>mu(g_vContours.size());
        for (int i = 0; i < g_vContours.size(); ++i) {
            mu[i] = moments(g_vContours[i], false);
        }

        //计算中心矩

        std::vector<Point2f>mc(g_vContours.size());
        for (int i = 0; i < g_vContours.size(); ++i) {
            mc[i] = Point2f(static_cast<float>(mu[i].m10 / mu[i].m00), static_cast<float>(mu[i].m01 / mu[i].m00));
        }

        //绘制轮廓

        Mat drawing = Mat::zeros(g_cannyMat_output.size(), CV_8UC3);
        for (int i = 0; i < g_vContours.size(); ++i) {
            Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));
            drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point()); //绘制内层和外层轮廓

            circle(drawing, mc[i], 4, color, -1, 8, 0);//绘制圆

        }

        imshow("drawing", drawing);

        //计算轮廓面积

        std::cout << "\t";

        for (int i = 0; i < g_vContours.size(); ++i) {
            std::cout << ">Through m00 Areas[" << i << "]:" << mu[i].m00 << "Length = "<< arcLength(g_vContours[i], true)<< std::endl;
            std::cout << ">Through OpenCV m_00 Areas[" << i << "]" <<contourArea(g_vContours[i]) <<
                "Length = " << arcLength(g_vContours[i], true) << std::endl;

            Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));
            drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point());
            circle(drawing, mc[i], 4, color, -1, 8, 0);
        }

    }

    void Test() {
        g_srcImage = imread("image.jpg");
        cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
        blur(g_grayImage, g_grayImage, Size(3, 3));


        namedWindow("srcImage");
        imshow("srcImage", g_srcImage);
        createTrackbar("value:", "srcImage", &g_nThresh, g_nMaxThresh,on_ThreshChange);

        waitKey(0);
    }



}

void Test56() {
    test56::Test();
}


8.5 分水岭算法

在许多实际运用中,我们需要分割图像,但无法从背景图像中获得有用信 息。分水岭算法 (watershed algorithm) 在这方面往往是非常有效的。此算法可 以将图像中的边缘转化成"山脉",将均匀区域转化为"山谷",这样有助于分 割目标。

分水岭算法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想 是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的 海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形 成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明:在每一个局部 极小值表面,刺穿一个小孔,然后把整个模型慢慢浸入水中,随着浸入的加深, 每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝,即 形成分水岭。

分水岭的计算过程是一个迭代标注过程。分水岭比较经典的计算方法是由 L.Vincent 提出的。在该算法中,分水岭计算分两个步骤: 一个是排序过程, 一个是淹没过程。首先对每个像素的灰度级进行从低到高的排序,然后在从低 到高实现淹没的过程中,对每一个局部极小值在h 阶高度的影响域采用先进先 出 (FIFO) 结构进行判断及标注。分水岭变换得到的是输入图像的集水盆图像, 集水盆之间的边界点,即为分水岭。显然,分水岭表示的是输入图像的极大值 点。

也就是说,分水岭算法首先计算灰度图像的梯度;这对图像中的"山谷"或 没有纹理的"盆地"(亮度值低的点)的形成是很有效的,也对"山头"或图像中 有主导线段的"山脉"(山脊对应的边缘)的形成有效。然后开始从用户指定点(或 者算法得到点)开始持续"灌注"盆地直到这些区域连成一片。基于这样产生的 标记就可以把区域合并到0一起,合并后的区域又通聚集的方式进行分割,好像 图像被"填充"起来一样。

8.5.1 实现分水岭算法:watershedO 函 数

函 数watershed 实现的分水岭算法是基于标记的分割算法中的 一种。在把图像 传给函数之前,我们需要大致勾画标记出图像中的期望进行分割的区域,它们被 标记为正指数。所以,每 一 个区域都会被标记为像素值1、2、3等,表示成为 一 个或者多个连接组件。这些标记的值可以使用findContours() 函 数 和drawContours()

函数由二进制的掩码检索出来。不难理解,这些标记就是即将绘制出来的分割区 域的"种子",而没有标记清楚的区域,被置为0。在函数输出中,每 一 个标记中 的像素被设置为"种子"的值,而区域间的值被设置为- 1。

cpp 复制代码
void watershed(InputArray image, InputOutputArray markers)
  • 第 一 个 参 数 ,InputArray 类 型 的src, 输入图像,即源图像,填Mat 类 的 对 象
    即可,且需为8位三通道的彩色图像。
  • 第 二 个 参 数 ,InputOutputArray 类 型 的markers, 函数调用后的运算结果存在 这里,输入/输出32位单通道图像的标记结果。即这个参数用于存放函数调用后 的输出结果,需和源图片有 一 样的尺寸和类型。

8.5.2 综合示例程序:分水岭算法

cpp 复制代码
namespace test57 {
    Mat g_maskImage, g_srcImage;
    Point prevPt(-1, -1);

    static void on_Mouse(int event, int x, int y, int flags, void*) {
        if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows) {
            return;
        }

        if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) {
            prevPt = Point(-1, -1);
        }
        else if (event == EVENT_LBUTTONDOWN) {
            prevPt = Point(x, y);
        }
        else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {
            Point pt(x, y);
            if (prevPt.x < 0) {
                prevPt = pt;
            }
            line(g_maskImage, prevPt, pt, Scalar::all(255), 5, 8, 0);
            line(g_srcImage, prevPt, pt, Scalar::all(255), 5, 8, 0);
            prevPt = pt;
            imshow("srcImage", g_srcImage);
        }
    }

    void Test() {
        g_srcImage = imread("mountain.jpg");
        imshow("srcImage", g_srcImage);

        Mat srcImage, grayImage;
        g_srcImage.copyTo(srcImage);

        cvtColor(g_srcImage, g_maskImage, COLOR_BGR2GRAY);
        cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR);

        g_maskImage = Scalar::all(0);

        setMouseCallback("srcImage", on_Mouse, 0);

        while (1) {
            int c = waitKey(1);  // 改为 1 来更流畅地显示

            if (c == 27) {
                break;
            }

            //恢复原图

            if ((char)c == '2') {
                g_maskImage = Scalar::all(0);
                srcImage.copyTo(g_srcImage);
                imshow("srcImage", g_srcImage);
            }

            else if ((char)c == '1') {
                int i, j, compCount = 0;
                std::vector<std::vector<Point>>contours;
                std::vector<Vec4i>hierarchy;

                //寻找轮廓

                findContours(g_maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);

                if (contours.empty()) {
                    continue;
                }

                Mat maskImage(g_maskImage.size(), CV_32S); //复制掩膜

                maskImage = Scalar::all(0);

                for (int index = 0; index >= 0; index = hierarchy[index][0]) {
                    compCount++;
                    drawContours(maskImage, contours, index, Scalar::all(compCount), -1, 8, hierarchy, INT_MAX);
                }

                if (compCount == 0) continue;

                //生成随机颜色

                std::vector<Vec3b>colorTab;
                for (int i = 0; i < compCount; ++i) {
                    uchar b = theRNG().uniform(0, 255);
                    uchar g = theRNG().uniform(0, 255);
                    uchar r = theRNG().uniform(0, 255);
                    colorTab.push_back(Vec3b(b, g, r));
                }

                watershed(srcImage, maskImage); //分水岭算法

                //将分水岭图像遍历存入watershedImage中

                Mat watershedImage(maskImage.size(), CV_8UC3);
                for (int i = 0; i < maskImage.rows; ++i) {
                    for (int j = 0; j < maskImage.cols; ++j) {
                        int index = maskImage.at<int>(i, j);
                        if (index == -1) {
                            watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0); 
                        }
                        else if (index <= 0 || index > compCount) {
                            watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
                        }
                        else {
                            watershedImage.at<Vec3b>(i, j) = colorTab[index - 1];
                        }
                    }
                }

                //混合灰度图和分水岭效果图

                watershedImage = watershedImage * 0.5 + grayImage * 0.5;
                imshow("watershed transform", watershedImage);
            }
        }
    }
}


void Test57() {
    test57::Test();
}

8.6 图像修补

在实际应用中,我们的图像常常会被噪声腐蚀,这些噪声或者是镜头上的灰 尘或水滴,或者是旧照片的划痕,或者由于图像的部分本身已经损坏。而"图像 修复"(Inpainting), 就是妙手回春,解决这些问题的良方。图像修复技术简单来说,就是利用那些已经被破坏区域的边缘,即边缘的颜色和结构,繁殖和混合到 损坏的图像中,以达到图像修补的目的。图8.34~8.36就是示例程序截图,演示 将图像中的字迹移除的效果。

8.6.1 实现图像修补:inpaint ( 函 数

在新版OpenCV 中,图像修补技术由inpaint 函数实现,它可以用来从扫描的 照片中清除灰尘和划痕,或者从静态图像或视频中去除不需要的物体。其原型声 明如下。

C++:void inpaint(InputArray src,InputArray inpaintMask,OutputArray

dst,double inpaintRadius,int flags)

  • 第 一 个参数,InputArray类 型 的src, 输入图像,即源图像,填Mat 类的对 象即可,且需为8位单通道或者三通道图像。
  • 第二个参数,InputArray类型的inpaintMask, 修复掩膜,为8位的单通道 图像。其中的非零像素表示需要修补的区域。
  • 第三个参数,OutputArray 类 型 的dst, 函数调用后的运算结果存在这里, 和源图片有一样的尺寸和类型。
  • 第四个参数,double 类型的 inpaintRadius,需要修补的每个点的圆形邻域, 为修复算法的参考半径。
  • 第五个参数,int 类型的flags,修补方法的标识符,可以是表8.4所示两者 之一。
标识符 说明
INPAINT_NS 基于Navier-Stokes方程的方法
INPAINT_TELEA Alexandru Telea方法

OpenCV2 中INPAINT_NS 和INPAINT_TELEA 标识符可以分别写作CV_ INPAINT_NS 和 CV_INPAINT_TELEA

8.6.2 综合示例程序:图像修补

函数和概念讲解完毕,下面我们依然是学习 一 个以本节所讲内容为核心的示 例程序,将本节所学内容付诸实践,融会贯通。此示例程序会先让我们在图像中 用鼠标绘制出白色的线条破坏图像,然后按下键盘按键【1】或【SPACE】 进 行 图 像修补操作。且如果对自己的绘制不够满意,可以按下键盘按键【2】恢复原始图 像。

cpp 复制代码
namespace test58 {
    Mat g_srcImage, inpaintMask,srcImage;
    Point previousPoint(-1, -1);

    static void On_Mouse(int event,int x,int y,int flags,void*) {
        if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows) {
            return;
        }

        if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) {
            previousPoint = Point(-1, -1);
        }
        else if (event == EVENT_LBUTTONDOWN) {
            previousPoint = Point(x, y);
        }
        else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {
            Point pt(x, y);
            if (previousPoint.x < 0) {
                previousPoint = pt;
            }
            line(inpaintMask, previousPoint, pt, Scalar::all(255), 5, 8, 0);
            line(g_srcImage, previousPoint, pt, Scalar::all(255), 5, 8, 0);
            previousPoint = pt;
            imshow("srcImage", g_srcImage);
        }
    }

    void Test() {
        g_srcImage = imread("sky.jpg");
        srcImage = g_srcImage.clone();

        inpaintMask = Mat::zeros(srcImage.size(), CV_8U);
        imshow("srcImage", g_srcImage);

        setMouseCallback("srcImage", On_Mouse, nullptr);

        while (1) {
            char c = waitKey(1);

            if (c == 27) break;

            if (c == '2') {
                inpaintMask = Scalar::all(0);
                srcImage.copyTo(g_srcImage);
                imshow("srcImage", g_srcImage);
            }

            if (c == '1') {
                Mat inpaintedImage;
                inpaint(g_srcImage, inpaintMask, inpaintedImage, 3, INPAINT_TELEA); 

                imshow("fixed", inpaintedImage);
            }
                
        }
    }


}

void Test58() {
    test58::Test();
}

8.7 本章小结

本章中,我们先学习了查找轮并绘制轮廓,然后学习了如何寻找到物体的凸 包,接着是使用多边形来包围轮廓,以及计算一个图像的矩。在本章后面几节, 还学习了分水岭算法和图像修补操作的实现方法。

函数名称 说明 对应讲解章节
BoundingRect 计算并返回指定点集最外面(up-right)的矩形边 界 8.3.1
minAreaRect 寻找可旋转的最小面积的包围矩形 8.3.2
minEnclosingCircle 利用一种迭代算法,对给定的2D点集,寻找面 积最小的可包围他们的圆形 8.3.3
fitEllipse 用椭圆拟合二维点集 8.3.4
approxPolyDP 用指定精度逼近多边形曲线 8.3.5
moments 计算多边形和光栅形状的最高达三阶的所有矩 8.4.1
contourArea 计算整个轮廓或部分轮廓的面积 8.4.2
arcLength 计算封闭轮廓的周长或曲线的长度 8.4.3
watershed 实现分水岭算法 8.5.1
inpaint 进行图像修补,从扫描的照片中清除灰尘和划痕, 或者从静态图像或视频中去除不需要的物体 8.6.1

本章示例程序清单

示例程序序号 程序说明 对应章节
69 轮廓查找 8.1.3
70 查找并绘制轮廓 8.1.4
71 凸包检测基础 8.2.3
72 寻找和绘制物体的凸包 8.2.4
73 创建包围轮廓的矩形边界 8.3.6
74 创建包围轮廓的圆形边界 8.3.7
75 使用多边形包围轮廓 8.3.8
76 图像轮廓矩 8.4.4
77 分水岭算法的使用 8.5.2
78 实现图像修补 8.6.2
相关推荐
墨绿色的摆渡人3 分钟前
pytorch小记(四):pytorch中的重排操作:x.permute()
人工智能·pytorch·python
最好Tony4 分钟前
深度学习-图神经网络-超图概念及在Hyper-YOLO的应用(小白也看懂)
人工智能·深度学习·神经网络
深图智能20 分钟前
OpenCV实现基于交叉双边滤波的红外可见光融合算法
图像处理·opencv·算法·计算机视觉
计算机小混子28 分钟前
C++实现设计模式---状态模式 (State)
c++·设计模式·状态模式
zjun302130 分钟前
Attention计算中的各个矩阵的维度都是如何一步步变化的?
人工智能·transformer
XianxinMao36 分钟前
人工智能前沿探讨:从Transformer架构到机器意识与迁移学习的应用
人工智能·架构·transformer
KeyPan1 小时前
【Ubuntu与Linux操作系统:二、图形界面与命令行】
linux·运维·服务器·pytorch·ubuntu·机器学习·计算机视觉
Quz1 小时前
OpenCV基础:视频的采集、读取与录制
opencv·计算机视觉
墨绿色的摆渡人1 小时前
pytorch小记(七):pytorch中的保存/加载模型操作
人工智能·pytorch·python
墨绿色的摆渡人1 小时前
pytorch小记(三):pytorch中的最大值操作:x.max()
人工智能·pytorch·python