canny算子提取边缘

计算机视觉(CV)是本人开始学习也准备一直深入的领域,刚接触这方面的时候不太了解图像处理,图形学,计算机视觉等的区别,后来看到知乎上的一个张图表,顿时让人茅塞顿开。就以这张图作为起点,开始我的CV学习之路吧!
《canny算子提取边缘》

第一次谈一谈canny算子提取边缘。

前言

什么是边缘

图像的边缘是灰度(亮度)突然改变的地方,边缘的产生有以下几个原因:

  • 表面法向量的不连续
  • 深度的不连续
  • 表面颜色的不连续
  • 光照的不连续

好的边缘提取器的标准

  • Good detection:既不能过多的检测出噪声,也不能丢失边缘信息
  • Good localization:边缘应该尽可能和真实图像边缘接近
  • Single response:边缘提取的尽可能细,每个只占一个像素点

相关知识

图像梯度

一张图像的梯度是在x轴和y轴方向上的一阶偏导数
《canny算子提取边缘》
梯度的方向由反三角函数给出
《canny算子提取边缘》
边缘长度的计算用梯度的绝对值表示
《canny算子提取边缘》

sobel算子

sobel算子可以近似计算图像的梯度,用作梯度滤波器
《canny算子提取边缘》

高斯滤波器

高斯滤波器使用以下公式得到:
《canny算子提取边缘》

算法流程

彩色图像灰度化

使用公式Grey = R * 0.2126 + G * 0.7152 + B * 0.0722

高斯滤波

使用高斯公式生成高斯滤波器,并进行归一化处理,即保证矩阵的所有元素之和为1;之后进行高斯滤波

Sobel滤波

Sobel滤波,生成图像在x轴和y轴的梯度图像,同时计算出幅角特征图

非最大值抑制

在边缘的法向量方向,比较像素点和临近的两个像素点的大小
《canny算子提取边缘》
其中,(x’, y’)和(x”, y”)是(x, y)的临近的两个像素点,这里
《canny算子提取边缘》
Sx和Sy分别是图像在x轴和y轴方向的梯度图像。

双阈值处理

使用两个阈值(高阈值和低阈值)来决定是否是边缘
1,如果灰度值大于高阈值则标记为白色
2,如果灰度值小于低阈值则标记为黑色
3,如果灰度值在高低阈值之间,那么它成为边界的充要条件是
——1)直接与“边缘像素”相连(八邻域)
——2)与“边缘像素”通过灰度值在高低阈值之间的像素相连

实现代码

main.cpp

#include "stdafx.h"
#include <vector>
#include <iostream>
#include "CImg.h"
#include "canny.h"

using namespace cimg_library;
using namespace std;

int _tmain(int argc, _TCHAR* argv[]) {
    string filePath = "test_Data/lena.bmp";
    canny cny(filePath);

    return 0;
}

canny.h

#ifndef _CANNY_
#define _CANNY_

#include "stdafx.h"
#include <vector>
#include <iostream>
#include "CImg.h"

using namespace std;
using namespace cimg_library;

class canny {
private:
    CImg<unsigned char> img; //原始图像
    CImg<unsigned char> grayscaled; // 灰度图像
    CImg<unsigned char> gFiltered; // 梯度
    CImg<unsigned char> sFiltered; //Sobel算法滤波
    CImg<unsigned char> angles; //幅角映射
    CImg<unsigned char> non; // 非最大值抑制
    CImg<unsigned char> thres; // 阈值处理(边缘图像)

public:
    canny(string); //构造函数
    CImg<unsigned char> toGrayScale();//彩色图像转为灰度图像
    vector<vector<double>> createFilter(int, int, double); //构造高斯滤波器
    CImg<unsigned char> useFilter(CImg<unsigned char>, vector<vector<double>>); //高斯滤波
    CImg<unsigned char> sobel(); //Sobel滤波
    CImg<unsigned char> nonMaxSupp(); //非最大值抑制
    CImg<unsigned char> threshold(CImg<unsigned char>, int, int); //双阈值处理
};


#endif

canny.cpp

#include <iostream>
#include <cmath>
#include "stdafx.h"
#include <vector>
#include "canny.h"
#include "CImg.h"

#define PI 3.14

using namespace std;
using namespace cimg_library;

canny::canny(string filename) {
    const char* file = filename.c_str();
    img.load_bmp(file);

    if (!img) {
        cout << "Could not open or find the image" << std::endl;
    }
    else {
        //创建3*3的滤波器
        vector<vector<double>> filter = createFilter(3, 3, 1);

        //打印滤波器
        for (int i = 0; i<filter.size(); i++)
        {
            for (int j = 0; j<filter[i].size(); j++)
            {
                cout << filter[i][j] << " ";
            }
        }

        grayscaled = toGrayScale(); //彩色图像灰度化
        gFiltered = useFilter(grayscaled, filter); //高斯滤波
        sFiltered = sobel(); //Sobel滤波
        non = nonMaxSupp(); //非最大值抑制
        thres = threshold(non, 20, 40); //双阈值处理

        img.display();
        grayscaled.display();
        gFiltered.display();
        sFiltered.display();
        non.display();
        thres.display();
        //thres.save("result/1/1-(20,40)33.bmp");
    }
}

CImg<unsigned char> canny::toGrayScale() {
    grayscaled = CImg<unsigned char>(img._width, img._height, 1, 1, 0); //新建一个灰度图像

    //彩色图像转为灰度图像的公式:R * 0.2126 + G * 0.7152 + B * 0.0722
    cimg_forXY(img, x, y) {
        grayscaled(x,y) = img(x, y, 0) * 0.2126 + img(x, y, 1) * 0.7152 + img(x, y, 2) * 0.0722;
    }
    return grayscaled;
}

//生成3*3的高斯滤波器
vector<vector<double>> canny::createFilter(int row, int column, double sigmaIn) {
    vector<vector<double>> filter;

    //初始化滤波器
    for (int i = 0; i < row; i++)
    {
        vector<double> col;
        for (int j = 0; j < column; j++)
        {
            col.push_back(-1);
        }
        filter.push_back(col);
    }

    float coordSum = 0;
    float constant = 2.0 * sigmaIn * sigmaIn;

    // Sum 归一化处理
    float sum = 0.0;

    for (int x = -row / 2; x <= row / 2; x++)
    {
        for (int y = -column / 2; y <= column / 2; y++)
        {
            coordSum = (x*x + y*y);
            filter[x + row / 2][y + column / 2] = (exp(-(coordSum) / constant)) / (PI * constant);
            sum += filter[x + row / 2][y + column / 2];
        }
    }

    // 归一化滤波器
    for (int i = 0; i < row; i++)
        for (int j = 0; j < column; j++)
            filter[i][j] /= sum;

    return filter;
}

//使用高斯滤波器进行滤波,去除噪声
CImg<unsigned char> canny::useFilter(CImg<unsigned char> img_in, vector<vector<double>> filterIn) {
    int size = (int)filterIn.size() / 2;
    CImg<unsigned char> filteredImg = CImg<unsigned char>(img_in._width - 2 * size, img_in._height - 2 * size, 1,1,0);

    //两个双重循环,原始图像和高斯滤波器做卷积运算
    for (int i = size; i < img_in._width - size; i++)
    {
        for (int j = size; j < img_in._height - size; j++)
        {
            double sum = 0;

            for (int x = 0; x < filterIn.size(); x++)
                for (int y = 0; y < filterIn.size(); y++)
                {
                    sum += filterIn[x][y] * (double)(img_in(i + x - size, j + y - size));
                }

            filteredImg(i - size, j - size) = sum;
        }

    }
    return filteredImg;
}

//sobel算子滤波
//生成图像在x,y轴的梯度图像
CImg<unsigned char> canny::sobel() {
    //Sobel在X轴的滤波器
    double x1[] = { -1.0, 0, 1.0 };
    double x2[] = { -2.0, 0, 2.0 };
    double x3[] = { -1.0, 0, 1.0 };

    vector<vector<double>> xFilter(3);
    xFilter[0].assign(x1, x1 + 3);
    xFilter[1].assign(x2, x2 + 3);
    xFilter[2].assign(x3, x3 + 3);

    //Sobel在Y轴的滤波器
    double y1[] = { 1.0, 2.0, 1.0 };
    double y2[] = { 0, 0, 0 };
    double y3[] = { -1.0, -2.0, -1.0 };

    vector<vector<double>> yFilter(3);
    yFilter[0].assign(y1, y1 + 3);
    yFilter[1].assign(y2, y2 + 3);
    yFilter[2].assign(y3, y3 + 3);

    //Limit Size
    int size = (int)xFilter.size() / 2;

    CImg<unsigned char> filteredImg = CImg<unsigned char>(gFiltered._width - 2 * size, gFiltered._height - 2 * size, 1,1,0);

    angles = CImg<unsigned char>(gFiltered._width - 2 * size, gFiltered._height - 2 * size, 1,1,0); //幅角映射

    for (int i = size; i < gFiltered._width - size; i++)
    {
        for (int j = size; j < gFiltered._height - size; j++)
        {
            double sumx = 0;
            double sumy = 0;

            for (int x = 0; x < xFilter.size(); x++)
                for (int y = 0; y < xFilter.size(); y++)
                {
                    sumx += xFilter[x][y] * (double)(gFiltered(i + x - size, j + y - size)); //Sobel_X Filter Value
                    sumy += yFilter[x][y] * (double)(gFiltered(i + x - size, j + y - size)); //Sobel_Y Filter Value
                }
            double sumxsq = sumx*sumx;
            double sumysq = sumy*sumy;

            double sq2 = sqrt(sumxsq + sumysq);

            if (sq2 > 255) //防止灰度值越界,0~255
                sq2 = 255;
            filteredImg(i - size, j - size) = sq2;

            if (sumx == 0) //计算atan,atan(0)对于幅角90度,特殊情况单独处理
                angles(i - size, j - size) = 90;
            else
                angles(i - size, j - size) = atan(sumy / sumx);
        }
    }

    return filteredImg;

}

//非最大值抑制
CImg<unsigned char> canny::nonMaxSupp() {
    CImg<unsigned char> nonMaxSupped = CImg<unsigned char>(sFiltered._width - 2, sFiltered._height - 2, 1,1,0);
    for (int i = 1; i< sFiltered._width - 1; i++) {
        for (int j = 1; j<sFiltered._height - 1; j++) {
            float Tangent = angles(i, j);

            nonMaxSupped(i - 1, j - 1) = sFiltered(i, j);
            //水平边缘,幅角范围在(-22.5, 22.5), (157.5, 180)或者(-180, -157.5)
            if (((-22.5 < Tangent) && (Tangent <= 22.5)) || ((157.5 < Tangent) && (Tangent <= -157.5)))
            {
                //只要灰度值小于周围临近的两个点中任意一个,则赋值为0
                if ((sFiltered(i, j) < sFiltered(i, j + 1)) || (sFiltered(i, j) < sFiltered(i, j - 1)))
                    nonMaxSupped(i - 1, j - 1) = 0;
            }
            //垂直边缘
            if (((-112.5 < Tangent) && (Tangent <= -67.5)) || ((67.5 < Tangent) && (Tangent <= 112.5)))
            {
                if ((sFiltered(i, j) < sFiltered(i + 1, j)) || (sFiltered(i, j) < sFiltered(i - 1, j)))
                    nonMaxSupped(i - 1, j - 1) = 0;
            }

            //-45度角边缘
            if (((-67.5 < Tangent) && (Tangent <= -22.5)) || ((112.5 < Tangent) && (Tangent <= 157.5)))
            {
                if ((sFiltered(i, j) < sFiltered(i - 1, j + 1)) || (sFiltered(i, j) < sFiltered(i + 1, j - 1)))
                    nonMaxSupped(i - 1, j - 1) = 0;
            }

            //45度角边缘
            if (((-157.5 < Tangent) && (Tangent <= -112.5)) || ((22.5 < Tangent) && (Tangent <= 67.5)))
            {
                if ((sFiltered(i, j) < sFiltered(i + 1, j + 1)) || (sFiltered(i, j) < sFiltered(i - 1, j - 1)))
                    nonMaxSupped(i - 1, j - 1) = 0;
            }
        }
    }
    return nonMaxSupped;
}

//双阈值处理
CImg<unsigned char> canny::threshold(CImg<unsigned char> imgin, int low, int high) {
    //限制灰度值范围<=255
    if (low > 255)
        low = 255;
    if (high > 255)
        high = 255;

    CImg<unsigned char> EdgeMat = CImg<unsigned char>(imgin._width, imgin._height, 1,1,0);

    for (int i = 0; i<imgin._width; i++)
    {
        for (int j = 0; j<imgin._height; j++)
        {
            EdgeMat(i, j) = imgin(i, j);
            //大于阈值上界则为白色
            if (EdgeMat(i, j) > high)
                EdgeMat(i, j) = 255;
            //小于阈值下界则为黑色
            else if (EdgeMat(i, j) < low)
                EdgeMat(i, j) = 0;
            //如果在阈值在上下界之间,它成为边界的充要条件是
            //1,直接与“边缘像素”相连(八邻域)
            //2,或者与“边缘像素”通过在阈值上下界之间的像素相连
            else
            {
                bool anyHigh = false;
                bool anyBetween = false;
                for (int x = i - 1; x < i + 2; x++)
                {
                    for (int y = j - 1; y<j + 2; y++)
                    {
                        if (x <= 0 || y <= 0 || EdgeMat._width || y > EdgeMat._height) //下标越界
                            continue;
                        else
                        {
                            if (EdgeMat(x, y) > high)
                            {
                                EdgeMat(i, j) = 255;
                                anyHigh = true;
                                break;
                            }
                            else if (EdgeMat(x, y) <= high && EdgeMat(x, y) >= low)
                                anyBetween = true;
                        }
                    }
                    if (anyHigh)
                        break;
                }
                if (!anyHigh && anyBetween)
                    for (int x = i - 2; x < i + 3; x++)
                    {
                        for (int y = j - 1; y<j + 3; y++)
                        {
                            if (x < 0 || y < 0 || x > EdgeMat._width || y > EdgeMat._height) //Out of bounds
                                continue;
                            else
                            {
                                if (EdgeMat(x, y) > high)
                                {
                                    EdgeMat(i, j) = 255;
                                    anyHigh = true;
                                    break;
                                }
                            }
                        }
                        if (anyHigh)
                            break;
                    }
                if (!anyHigh)
                    EdgeMat(i, j) = 0;
            }
        }
    }
    return EdgeMat;
}

说明

实现代码使用的是CImg图像处理库,CImg相关资料可参考这里, 密码: a6mb
编译环境:Visual Studio 2015 + windows10

    原文作者:青青韶华
    原文地址: https://blog.csdn.net/qq_36165459/article/details/61471043
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞