计算机视觉(CV)是本人开始学习也准备一直深入的领域,刚接触这方面的时候不太了解图像处理,图形学,计算机视觉等的区别,后来看到知乎上的一个张图表,顿时让人茅塞顿开。就以这张图作为起点,开始我的CV学习之路吧!
第一次谈一谈canny算子提取边缘。
前言
什么是边缘
图像的边缘是灰度(亮度)突然改变的地方,边缘的产生有以下几个原因:
- 表面法向量的不连续
- 深度的不连续
- 表面颜色的不连续
- 光照的不连续
好的边缘提取器的标准
- Good detection:既不能过多的检测出噪声,也不能丢失边缘信息
- Good localization:边缘应该尽可能和真实图像边缘接近
- Single response:边缘提取的尽可能细,每个只占一个像素点
相关知识
图像梯度
一张图像的梯度是在x轴和y轴方向上的一阶偏导数
梯度的方向由反三角函数给出
边缘长度的计算用梯度的绝对值表示
sobel算子
sobel算子可以近似计算图像的梯度,用作梯度滤波器
高斯滤波器
高斯滤波器使用以下公式得到:
算法流程
彩色图像灰度化
使用公式Grey = R * 0.2126 + G * 0.7152 + B * 0.0722
高斯滤波
使用高斯公式生成高斯滤波器,并进行归一化处理,即保证矩阵的所有元素之和为1;之后进行高斯滤波
Sobel滤波
Sobel滤波,生成图像在x轴和y轴的梯度图像,同时计算出幅角特征图
非最大值抑制
在边缘的法向量方向,比较像素点和临近的两个像素点的大小
其中,(x’, y’)和(x”, y”)是(x, y)的临近的两个像素点,这里
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