图像处理之Canny 边缘检測

一:历史

Canny边缘检測算法是1986年有John F. Canny开发出来一种基于图像梯度计算的边缘

检測算法,同一时候Canny本人对计算图像边缘提取学科的发展也是做出了非常多的贡献。

管至今已经很多年过去,可是该算法仍然是图像边缘检測方法经典算法之中的一个。

二:Canny边缘检測算法

经典的Canny边缘检測算法通常都是从高斯模糊開始,到基于双阈值实现边缘连接结束

可是在实际project应用中,考虑到输入图像都是彩色图像,终于边缘连接之后的图像要

二值化输出显示。所以完整的Canny边缘检測算法实现过程例如以下:

1.      彩色图像转换为灰度图像

2.      对图像进行高斯模糊

3.      计算图像梯度,依据梯度计算图像边缘幅值与角度

4.      非最大信号压制处理(边缘细化)

5.      双阈值边缘连接处理

6.      二值化图像输出结果

三:各步具体解释与代码实现

1.      彩色图像转灰度图像

依据彩色图像RGB转灰度公式:gray  =  R * 0.299 + G * 0.587 + B * 0.114

将彩色图像中每一个RGB像素转为灰度值的代码例如以下:

int gray = (int) (0.299 * tr + 0.587 * tg + 0.114 * tb);

2.      对图像进行高斯模糊

图像高斯模糊时。首先要依据输入參数确定高斯方差与窗体大小,这里我设置默认方

差值窗体大小为16x16,依据这两个參数生成高斯卷积核算子的代码例如以下:

		float kernel[][] = new float[gaussianKernelWidth][gaussianKernelWidth];
for(int x=0; x<gaussianKernelWidth; x++)
{
for(int y=0; y<gaussianKernelWidth; y++)
{
kernel[x][y] = gaussian(x, y, gaussianKernelRadius);
}
}

获取了高斯卷积算子之后。我们就能够对图像高斯卷积模糊,关于高斯图像模糊更详

细的解释能够參见这里:http://blog.csdn.net/jia20003/article/details/7234741实现

图像高斯卷积模糊的代码例如以下:

// 高斯模糊 -灰度图像
int krr = (int)gaussianKernelRadius;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
index = row * width + col;
double weightSum = 0.0;
double redSum = 0;
for(int subRow=-krr; subRow<=krr; subRow++)
{
int nrow = row + subRow;
if(nrow >= height || nrow < 0)
{
nrow = 0;
}
for(int subCol=-krr; subCol<=krr; subCol++)
{
int ncol = col + subCol;
if(ncol >= width || ncol <=0)
{
ncol = 0;
}
int index2 = nrow * width + ncol;
int tr1 = (inPixels[index2] >> 16) & 0xff;
redSum += tr1*kernel[subRow+krr][subCol+krr];
weightSum += kernel[subRow+krr][subCol+krr];
}
}
int gray = (int)(redSum / weightSum);
outPixels[index] = gray;
}
}

3.      计算图像X方向与Y方向梯度。依据梯度计算图像边缘幅值与角度大小

高斯模糊的目的主要为了总体降低图像噪声,目的是为了更准确计算图像梯度及边缘

幅值。

计算图像梯度能够选择算子有Robot算子、Sobel算子、Prewitt算子等。

关于

图像梯度计算很多其它的解释能够看这里:

http://blog.csdn.net/jia20003/article/details/7664777。

这里採用更加简单明了的2x2的算子,其数学表达例如以下:

// 计算梯度-gradient, X放与Y方向
data = new float[width * height];
magnitudes = new float[width * height];
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
index = row * width + col;
// 计算X方向梯度
float xg = (getPixel(outPixels, width, height, col, row+1) -
getPixel(outPixels, width, height, col, row) +
getPixel(outPixels, width, height, col+1, row+1) -
getPixel(outPixels, width, height, col+1, row))/2.0f;
float yg = (getPixel(outPixels, width, height, col, row)-
getPixel(outPixels, width, height, col+1, row) +
getPixel(outPixels, width, height, col, row+1) -
getPixel(outPixels, width, height, col+1, row+1))/2.0f;
// 计算振幅与角度
data[index] = hypot(xg, yg);
if(xg == 0)
{
if(yg > 0)
{
magnitudes[index]=90;
}
if(yg < 0)
{
magnitudes[index]=-90;
}
}
else if(yg == 0)
{
magnitudes[index]=0;
}
else
{
magnitudes[index] = (float)((Math.atan(yg/xg) * 180)/Math.PI);
}
// make it 0 ~ 180
magnitudes[index] += 90;
}
}

在获取了图像每一个像素的边缘幅值与角度之后

4.      非最大信号压制

信号压制本来是数字信号处理中经经常使用的,这里的非最大信号压制主要目的是实现边

缘细化。通过该步处理边缘像素进一步降低。

非最大信号压制主要思想是假设3x3的

像素区域,中心像素P(x,y) 依据上一步中计算得到边缘角度值angle。能够将角度分

为四个离散值0、45、90、135分类依据例如以下:

当中黄色区域取值范围为0~22.5 与157.5~180

绿色区域取值范围为22.5 ~ 67.5

蓝色区域取值范围为67.5~112.5

红色区域取值范围为112.5~157.5

分别表示上述四个离散角度的取值范围。得到角度之后,比較中心像素角度上相邻

两个像素,假设中心像素小于当中随意一个,则舍弃该边缘像素点,否则保留。一

个简单的样例例如以下:

// 非最大信号压制算法 3x3
Arrays.fill(magnitudes, 0);
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
index = row * width + col;
float angle = magnitudes[index];
float m0 = data[index];
magnitudes[index] = m0;
if(angle >=0 && angle < 22.5) // angle 0
{
float m1 = getPixel(data, width, height, col-1, row);
float m2 = getPixel(data, width, height, col+1, row);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
else if(angle >= 22.5 && angle < 67.5) // angle +45
{
float m1 = getPixel(data, width, height, col+1, row-1);
float m2 = getPixel(data, width, height, col-1, row+1);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
else if(angle >= 67.5 && angle < 112.5) // angle 90
{
float m1 = getPixel(data, width, height, col, row+1);
float m2 = getPixel(data, width, height, col, row-1);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
else if(angle >=112.5 && angle < 157.5) // angle 135 / -45
{
float m1 = getPixel(data, width, height, col-1, row-1);
float m2 = getPixel(data, width, height, col+1, row+1);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
else if(angle >=157.5) // 跟零度是一致的,感谢一位网友发现了这个问题
{
float m1 = getPixel(data, width, height, col+1, row);
float m2 = getPixel(data, width, height, col-1, row);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
}
}

1.      双阈值边缘连接

非最大信号压制以后,输出的幅值假设直接显示结果可能会少量的非边缘像素被包

含到结果中,所以要通过选取阈值进行取舍,传统的基于一个阈值的方法假设选择

的阈值较小起不到过滤非边缘的作用,假设选择的阈值过大easy丢失真正的图像边

缘,Canny提出基于双阈值(Fuzzy threshold)方法非常好的实现了边缘选取,在实际

应用中双阈值还有边缘连接的作用。双阈值选择与边缘连接方法通过假设两个阈值

当中一个为高阈值TH另外一个为低阈值TL则有

a.      对于随意边缘像素低于TL的则丢弃

b.      对于随意边缘像素高于TH的则保留

c.      对于随意边缘像素值在TL与TH之间的,假设能通过边缘连接到一个像素大于

TH并且边缘全部像素大于最小阈值TL的则保留,否则丢弃。代码实现例如以下:

Arrays.fill(data, 0);
int offset = 0;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
if(magnitudes[offset] >= highThreshold && data[offset] == 0)
{
edgeLink(col, row, offset, lowThreshold);
}
offset++;
}
}

基于递归的边缘寻找方法edgeLink的代码例如以下:

private void edgeLink(int x1, int y1, int index, float threshold) {
int x0 = (x1 == 0) ? x1 : x1 - 1;
int x2 = (x1 == width - 1) ? x1 : x1 + 1;
int y0 = y1 == 0 ? y1 : y1 - 1;
int y2 = y1 == height -1 ? y1 : y1 + 1; data[index] = magnitudes[index];
for (int x = x0; x <= x2; x++) {
for (int y = y0; y <= y2; y++) {
int i2 = x + y * width;
if ((y != y1 || x != x1)
&& data[i2] == 0
&& magnitudes[i2] >= threshold) {
edgeLink(x, y, i2, threshold);
return;
}
}
}
}

6.      结果二值化显示 - 不说啦,直接点,自己看吧,太简单啦

// 二值化显示
for(int i=0; i<inPixels.length; i++)
{
int gray = clamp((int)data[i]);
outPixels[i] = gray > 0 ? -1 : 0xff000000;
}

终于执行结果:


四:完整的Canny算法源码

package com.gloomyfish.filter.study;

import java.awt.image.BufferedImage;
import java.util.Arrays; public class CannyEdgeFilter extends AbstractBufferedImageOp {
private float gaussianKernelRadius = 2f;
private int gaussianKernelWidth = 16;
private float lowThreshold;
private float highThreshold;
// image width, height
private int width;
private int height;
private float[] data;
private float[] magnitudes; public CannyEdgeFilter() {
lowThreshold = 2.5f;
highThreshold = 7.5f;
gaussianKernelRadius = 2f;
gaussianKernelWidth = 16;
} public float getGaussianKernelRadius() {
return gaussianKernelRadius;
} public void setGaussianKernelRadius(float gaussianKernelRadius) {
this.gaussianKernelRadius = gaussianKernelRadius;
} public int getGaussianKernelWidth() {
return gaussianKernelWidth;
} public void setGaussianKernelWidth(int gaussianKernelWidth) {
this.gaussianKernelWidth = gaussianKernelWidth;
} public float getLowThreshold() {
return lowThreshold;
} public void setLowThreshold(float lowThreshold) {
this.lowThreshold = lowThreshold;
} public float getHighThreshold() {
return highThreshold;
} public void setHighThreshold(float highThreshold) {
this.highThreshold = highThreshold;
} @Override
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
width = src.getWidth();
height = src.getHeight();
if (dest == null)
dest = createCompatibleDestImage(src, null);
// 图像灰度化
int[] inPixels = new int[width * height];
int[] outPixels = new int[width * height];
getRGB(src, 0, 0, width, height, inPixels);
int index = 0;
for (int row = 0; row < height; row++) {
int ta = 0, tr = 0, tg = 0, tb = 0;
for (int col = 0; col < width; col++) {
index = row * width + col;
ta = (inPixels[index] >> 24) & 0xff;
tr = (inPixels[index] >> 16) & 0xff;
tg = (inPixels[index] >> 8) & 0xff;
tb = inPixels[index] & 0xff;
int gray = (int) (0.299 * tr + 0.587 * tg + 0.114 * tb);
inPixels[index] = (ta << 24) | (gray << 16) | (gray << 8)
| gray;
}
} // 计算高斯卷积核
float kernel[][] = new float[gaussianKernelWidth][gaussianKernelWidth];
for(int x=0; x<gaussianKernelWidth; x++)
{
for(int y=0; y<gaussianKernelWidth; y++)
{
kernel[x][y] = gaussian(x, y, gaussianKernelRadius);
}
}
// 高斯模糊 -灰度图像
int krr = (int)gaussianKernelRadius;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
index = row * width + col;
double weightSum = 0.0;
double redSum = 0;
for(int subRow=-krr; subRow<=krr; subRow++)
{
int nrow = row + subRow;
if(nrow >= height || nrow < 0)
{
nrow = 0;
}
for(int subCol=-krr; subCol<=krr; subCol++)
{
int ncol = col + subCol;
if(ncol >= width || ncol <=0)
{
ncol = 0;
}
int index2 = nrow * width + ncol;
int tr1 = (inPixels[index2] >> 16) & 0xff;
redSum += tr1*kernel[subRow+krr][subCol+krr];
weightSum += kernel[subRow+krr][subCol+krr];
}
}
int gray = (int)(redSum / weightSum);
outPixels[index] = gray;
}
} // 计算梯度-gradient, X放与Y方向
data = new float[width * height];
magnitudes = new float[width * height];
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
index = row * width + col;
// 计算X方向梯度
float xg = (getPixel(outPixels, width, height, col, row+1) -
getPixel(outPixels, width, height, col, row) +
getPixel(outPixels, width, height, col+1, row+1) -
getPixel(outPixels, width, height, col+1, row))/2.0f;
float yg = (getPixel(outPixels, width, height, col, row)-
getPixel(outPixels, width, height, col+1, row) +
getPixel(outPixels, width, height, col, row+1) -
getPixel(outPixels, width, height, col+1, row+1))/2.0f;
// 计算振幅与角度
data[index] = hypot(xg, yg);
if(xg == 0)
{
if(yg > 0)
{
magnitudes[index]=90;
}
if(yg < 0)
{
magnitudes[index]=-90;
}
}
else if(yg == 0)
{
magnitudes[index]=0;
}
else
{
magnitudes[index] = (float)((Math.atan(yg/xg) * 180)/Math.PI);
}
// make it 0 ~ 180
magnitudes[index] += 90;
}
} // 非最大信号压制算法 3x3
Arrays.fill(magnitudes, 0);
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
index = row * width + col;
float angle = magnitudes[index];
float m0 = data[index];
magnitudes[index] = m0;
if(angle >=0 && angle < 22.5) // angle 0
{
float m1 = getPixel(data, width, height, col-1, row);
float m2 = getPixel(data, width, height, col+1, row);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
else if(angle >= 22.5 && angle < 67.5) // angle +45
{
float m1 = getPixel(data, width, height, col+1, row-1);
float m2 = getPixel(data, width, height, col-1, row+1);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
else if(angle >= 67.5 && angle < 112.5) // angle 90
{
float m1 = getPixel(data, width, height, col, row+1);
float m2 = getPixel(data, width, height, col, row-1);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
else if(angle >=112.5 && angle < 157.5) // angle 135 / -45
{
float m1 = getPixel(data, width, height, col-1, row-1);
float m2 = getPixel(data, width, height, col+1, row+1);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
else if(angle >=157.5) // angle 0
{
float m1 = getPixel(data, width, height, col, row+1);
float m2 = getPixel(data, width, height, col, row-1);
if(m0 < m1 || m0 < m2)
{
magnitudes[index] = 0;
}
}
}
}
// 寻找最大与最小值
float min = 255;
float max = 0;
for(int i=0; i<magnitudes.length; i++)
{
if(magnitudes[i] == 0) continue;
min = Math.min(min, magnitudes[i]);
max = Math.max(max, magnitudes[i]);
}
System.out.println("Image Max Gradient = " + max + " Mix Gradient = " + min); // 通常比值为 TL : TH = 1 : 3, 依据两个阈值完毕二值化边缘连接
// 边缘连接-link edges
Arrays.fill(data, 0);
int offset = 0;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
if(magnitudes[offset] >= highThreshold && data[offset] == 0)
{
edgeLink(col, row, offset, lowThreshold);
}
offset++;
}
} // 二值化显示
for(int i=0; i<inPixels.length; i++)
{
int gray = clamp((int)data[i]);
outPixels[i] = gray > 0 ? -1 : 0xff000000;
}
setRGB(dest, 0, 0, width, height, outPixels );
return dest;
} public int clamp(int value) {
return value > 255 ? 255 :
(value < 0 ? 0 : value);
} private void edgeLink(int x1, int y1, int index, float threshold) {
int x0 = (x1 == 0) ? x1 : x1 - 1;
int x2 = (x1 == width - 1) ? x1 : x1 + 1;
int y0 = y1 == 0 ? y1 : y1 - 1;
int y2 = y1 == height -1 ? y1 : y1 + 1; data[index] = magnitudes[index];
for (int x = x0; x <= x2; x++) {
for (int y = y0; y <= y2; y++) {
int i2 = x + y * width;
if ((y != y1 || x != x1)
&& data[i2] == 0
&& magnitudes[i2] >= threshold) {
edgeLink(x, y, i2, threshold);
return;
}
}
}
} private float getPixel(float[] input, int width, int height, int col,
int row) {
if(col < 0 || col >= width)
col = 0;
if(row < 0 || row >= height)
row = 0;
int index = row * width + col;
return input[index];
} private float hypot(float x, float y) {
return (float) Math.hypot(x, y);
} private int getPixel(int[] inPixels, int width, int height, int col,
int row) {
if(col < 0 || col >= width)
col = 0;
if(row < 0 || row >= height)
row = 0;
int index = row * width + col;
return inPixels[index];
} private float gaussian(float x, float y, float sigma) {
float xDistance = x*x;
float yDistance = y*y;
float sigma22 = 2*sigma*sigma;
float sigma22PI = (float)Math.PI * sigma22;
return (float)Math.exp(-(xDistance + yDistance)/sigma22)/sigma22PI;
} }

转载请务必注明出自本博客-gloomyfish

图像处理之Canny边缘检測的更多相关文章

  1. Canny边缘检測算法原理及其VC实现具体解释(一)

    图象的边缘是指图象局部区域亮度变化显著的部分,该区域的灰度剖面一般能够看作是一个阶跃,既从一个灰度值在非常小的缓冲区域内急剧变化到还有一个灰度相差较大的灰度值.图象的边缘部分集中了图象的大部分信息,图 ...

  2. OpenCV图像处理篇之边缘检測算子

    3种边缘检測算子 灰度或结构等信息的突变位置是图像的边缘,图像的边缘有幅度和方向属性.沿边缘方向像素变化缓慢,垂直边缘方向像素变化剧烈.因此,边缘上的变化能通过梯度计算出来. 一阶导数的梯度算子 对于 ...

  3. OpenCV2马拉松第17圈——边缘检測(Canny边缘检測)

    计算机视觉讨论群162501053 转载请注明:http://blog.csdn.net/abcd1992719g 收入囊中 利用OpenCV Canny函数进行边缘检測 掌握Canny算法基本理论 ...

  4. 【OpenCV新手教程之十二】OpenCV边缘检測:Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑

    本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/25560901 作者:毛星云(浅墨) ...

  5. Python图像处理(8):边缘检測

    快乐虾 http://blog.csdn.net/lights_joy/ 欢迎转载,但请保留作者信息 此前已经得到了单个区域植株图像,接下来似乎应该尝试对这些区域进行分类识别.通过外形和叶脉进行植物种 ...

  6. 图像边缘检測--OpenCV之cvCanny函数

    图像边缘检測--OpenCV之cvCanny函数 分类: C/C++ void cvCanny( const CvArr* image, CvArr* edges, double threshold1 ...

  7. Python下opencv使用笔记(七)(图像梯度与边缘检測)

    梯度简单来说就是求导,在图像上表现出来的就是提取图像的边缘(无论是横向的.纵向的.斜方向的等等),所须要的无非也是一个核模板.模板的不同结果也不同.所以能够看到,全部的这些个算子函数,归结究竟都能够用 ...

  8. OpenCV2马拉松第14圈——边缘检測(Sobel,prewitt,roberts)

    收入囊中 差分在边缘检測的角色 Sobel算子 OpenCV sobel函数 OpenCV Scharr函数 prewitt算子 Roberts算子 葵花宝典 差分在边缘检測究竟有什么用呢?先看以下的 ...

  9. OpenCV2马拉松第15圈——边缘检測(Laplace算子,LOG算子)

    收入囊中 拉普拉斯算子 LOG算子(高斯拉普拉斯算子) OpenCV Laplacian函数 构建自己的拉普拉斯算子 利用拉普拉斯算子进行图像的锐化 葵花宝典 在OpenCV2马拉松第14圈--边缘检 ...

随机推荐

  1. Java ArrayList的模拟实现

    package test; import java.util.Arrays; import java.util.Collection; public class MyArrayList<E> ...

  2. Macro definition of snprintf conflicts with Standard Library function declaration

    Macro definition of snprintf conflicts with Standard Library function declaration 即将此处的宏定义注释掉,因为在VS2 ...

  3. python基础语法(一)

    Python的特点 1. 简单 Python是一种代表简单思想的语言. 2. 易学 Python有极其简单的语法. 3. 免费.开源 Python是FLOSS(自由/开放源码软件)之一. 4. 高层语 ...

  4. Crontab命令--Linux

    Crontab命令--定时任务   命令格式 Example:  

  5. NodeJS on Nginx: 使用nginx反向代理处理静态页面

    最近OurJS后台已经从纯node.js迁移到了Nginx+NodeJS上来了,感觉性能提升了不少,特与大家分享. Nginx ("engine x") 是一个高性能的 HTTP ...

  6. Connect China Azure Storage Blob By Container Token In Python SDK

    简介: 基于Python SDK,使用Container Token操作container对象.关于Token的生成可以使用Storage SDK创建,也可以使用工具快速创建供测试. 示例代码: fr ...

  7. GitExtensions工具安装与配置

    GitExtensions工具使用教程 第一步:安装 1.双击:GitExtensions24703SetupComplete.msi <ignore_js_op>  <ignore ...

  8. WebApi异常处理解决方案

    一.使用异常筛选器捕获所有异常 首先在App_Start里面新建一个类WebApiExceptionFilterAttribute.cs,继承ExceptionFilterAttribute,重写On ...

  9. mongodb 实现关系型数据库中查询某一列 的效果

    近期在tornado\mongodb\ansible mongodb中有个find()方法非常牛逼,能够将集合中全部的表都传出来,一開始我这么写 class Module_actionHandler( ...

  10. 88. Merge Sorted Array【easy】

    88. Merge Sorted Array[easy] Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 ...