OpenCV-Python (官方)中文教程(部分一)_Day16
19.Canny边缘检测
边缘检测是图像处理和计算机视觉的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点,图像属性中的显著变化通常反映了属性的重要事件和变化。这些包括:深度上的不连续,表面方向的不连续,物质属性变化和场景照明变化。边缘检测是图像处理和计算机视觉中尤其是特征提取中的一个研究领域。图像边缘检测大幅度的减少了数据量,并且剔除了可以认为不相关的信息,保留了图像重要的结构属性。
在实际的图像分割中,往往只用到一阶和二阶导数,虽然原理上,可以用更高阶的导数,但是因为噪声的影响,在纯粹二阶的导数操作中就会出现对噪声的敏感现象,三阶以上的导数信息往往失去了应用价值。二阶导数还可以说明灰度突变的类型。在某些情况下,如灰度变化均匀的图像,只利用一阶导数可能找不到边界,此时二阶导数就能提供很有用的信息。二阶导数对噪声也比较敏感,解决的方法是先对图像进行平滑滤波,消除部分噪声,再进行边缘检测。不过,利用二阶导数信息的算法是基于过零检测的,因此得到的边缘点数比较少,有利于后继的处理和识别工作。
在图像处理中,导数的本质是灰度变化的数学描述,用于检测边缘、纹理和结构特征。
1. 一阶导数(First-Order Derivative)
数学定义:
图像 I(x,y) 在 x 方向的一阶导数:
类似可定义 y 方向导数。
物理意义:
表示灰度的变化率(即边缘的强度)。
在边缘处,一阶导数达到极值(最大值或最小值)。
常用算子:
Sobel:加权差分,抗噪性较好。
Prewitt:简单差分,对噪声敏感。
Roberts:交叉差分,适用于锐利边缘。
边缘检测:
通过计算梯度幅值来定位边缘。
缺点:无法区分边缘是上升还是下降(需结合方向)。
2. 二阶导数(Second-Order Derivative)
数学定义:
图像 I(x,y) 的二阶导数(拉普拉斯算子):
离散形式(以 x 方向为例):
物理意义:
表示灰度变化的加速度(即边缘的曲率)。
在边缘处,二阶导数过零点(Zero-Crossing),即从正变负或反之。
能区分边缘类型:
阶跃边缘:一阶导数极值,二阶导数过零。
屋顶边缘(缓慢变化):一阶导数无极值,二阶导数有极值。
常用算子:
拉普拉斯(Laplacian):各向同性,对噪声敏感。
LoG(Laplacian of Gaussian):先高斯平滑再拉普拉斯,抗噪性更好。
缺点:
对噪声极其敏感(需先平滑)。
计算量较大。
过零检测(Zero-Crossing Detection)
1. 什么是过零检测?
定义:在二阶导数(如拉普拉斯结果)中,找到从正到负或负到正变化的点。
数学表达:
2. 为什么用过零检测?
边缘定位更精确:
一阶导数的极值对应边缘中心,但二阶导数的过零点直接对应边缘位置。
减少边缘数量:
过零点通常是真实的边缘,而一阶导数可能因噪声产生大量伪边缘。
3. 过零检测的步骤(以LoG为例)
高斯平滑:用高斯滤波器 G(x,y) 平滑图像,抑制噪声。
拉普拉斯运算:计算二阶导数。
寻找过零点:扫描图像,标记符号变化的像素。
示例(LoG边缘检测)
import cv2
import numpy as np
# 读取图像并高斯模糊
image = cv2.imread('edge.jpg', 0)
blurred = cv2.GaussianBlur(image, (5, 5), 1)
# 拉普拉斯运算
laplacian = cv2.Laplacian(blurred, cv2.CV_64F)
# 过零检测
zero_crossing = np.zeros_like(laplacian)
h, w = laplacian.shape
for i in range(1, h-1):
for j in range(1, w-1):
# 检查3x3邻域是否存在符号变化
if (laplacian[i-1,j] * laplacian[i+1,j] < 0) or \
(laplacian[i,j-1] * laplacian[i,j+1] < 0):
zero_crossing[i,j] = 255
# 显示处理前后的对比
cv2.imshow('Original Image', image)
cv2.imshow('Blurred Image', blurred)
cv2.imshow('Laplacian', cv2.convertScaleAbs(laplacian))
cv2.imshow('Zero Crossing', zero_crossing)
# 等待按键后关闭所有窗口
cv2.waitKey(0)
cv2.destroyAllWindows()
结果:
一阶 vs 二阶导数的对比
关键点总结
一阶导数:
通过梯度极值找边缘,适合实时检测,但易受噪声干扰。
二阶导数:
通过过零点找边缘,适合缓慢变化的边缘,需配合平滑(如LoG)。
过零检测:
是二阶导数的核心应用,能减少伪边缘,提升定位精度。
为什么“过零点通常是真实的边缘”?
在图像处理中,二阶导数的过零点(Zero-Crossing)被认为是真实边缘的主要原因,可以从以下几个方面理解:
1. 数学原理:二阶导数与边缘的关系
(1) 边缘的数学模型
理想阶跃边缘(Sharp Edge):
灰度在边缘处突然跳变(如黑白交界)。
一阶导数:在边缘处达到极值(最大值或最小值)。
二阶导数:在边缘处过零(从正变负或负变正)。
斜坡边缘(Ramp Edge):
灰度缓慢变化(如模糊边缘)。
一阶导数:在斜坡区域有较宽的极值区。
二阶导数:在斜坡的起始和结束点各有一个过零点。
(2) 为什么过零点对应真实边缘?
二阶导数描述的是灰度的“曲率”(即变化的加速度)。
在阶跃边缘处,灰度从暗到亮(或反之)的变化最快,导致二阶导数从正变负(或负变正)。
过零点正好对应灰度变化最快的点,即边缘的中心位置。
2. 过零点 vs. 一阶导数极值
关键区别
一阶导数只关注灰度变化的强度(梯度幅值),但无法区分噪声和真实边缘。
二阶导数关注灰度变化的模式(曲率),噪声的曲率变化通常是随机的,而真实边缘的曲率变化是规律的,因此过零点更可能是真实边缘。
3. 实际应用:LoG(Laplacian of Gaussian)
为了利用二阶导数的优势,通常先对图像高斯平滑(去噪),再计算拉普拉斯(二阶导数),最后检测过零点。
流程:
1、高斯平滑:抑制高频噪声。
2、拉普拉斯运算:提取二阶导数。
3、过零检测:找到符号变化的点。
效果:
噪声导致的随机过零点被抑制。
只有真实边缘的曲率变化才会产生稳定的过零点。
4. 为什么噪声很少导致过零点?
噪声的曲率是随机的:
噪声的局部变化没有规律,二阶导数可能频繁正负波动,但很难形成稳定的过零点。
高斯平滑的滤波作用:
高斯滤波后,高频噪声被抑制,剩下的过零点主要由真实的边缘曲率变化引起。
5. 总结
过零点对应真实边缘,因为:
二阶导数的过零点精确对应灰度变化最快的点(边缘中心)。
噪声的曲率变化随机,难以形成稳定的过零点。
相比一阶导数:
过零点检测抗噪性更好,边缘定位更准。
但计算量更大,需配合高斯平滑(如LoG算法)。
适用场景:
需要高精度边缘定位时(如医学图像、工业检测)。
图像噪声较多时,过零点检测比单纯梯度更鲁棒。
人类视觉系统认识目标的过程分为两步:首先,把图像边缘与背景分离出来;然后,才能知觉到图像的细节,辨认出图像的轮廓。计算机视觉正是模仿人类视觉的这个过程。因此在检测物体边缘时,先对其轮廓点进行粗略检测,然后通过链接规则把原来检测到的轮廓点连接起来,同时也检测和连接遗漏的边界点及去除虚假的边界点。图像的边缘是图像的重要特征,是计算机视觉、模式识别等的基础,因此边缘检测是图象处理中一个重要的环节。然而,边缘检测又是图象处理中的一个难题,由于实际景物图像的边缘往往是各种类型的边缘及它们模糊化后结果的组合,且实际图像信号存在着噪声。噪声和边缘都属于高频信号,很难用频带做取舍。
这就需要边缘检测来进行解决的问题了。边缘检测的基本方法有很多,一阶的有Roberts Cross算子,Prewitt算子,Sobel算子,Canny算子,Krisch算子,罗盘算子;而二阶的还有Marr-Hildreth,在梯度方向的二阶导数过零点。各种算子的存在就是对这种导数分割原理进行的实例化计算,是为了在计算过程中直接使用的一种计算单位。在对图像的操作,我们采用模板对原图像进行卷积运算,从而达到我们想要的效果。而获取一幅图像的梯度就转化为:模板(Roberts、Prewitt、Sobel、Lapacian算子)对原图像进行卷积。
Canny边缘检测算子:是一种多级检测算法。1986年由John F. Canny提出,同时提出了边缘检测的三大准则:
(1).低错误率的边缘检测:检测算法应该精确地找到图像中的尽可能多的边缘,尽可能的减少漏检和误检。
(2).最优定位:检测的边缘点应该精确地定位于边缘的中心。
(3).图像中的任意边缘应该只被标记一次,同时图像噪声不应产生伪边缘。
Canny边缘检测是一种比较新的边缘检测算子,具有很好地边缘检测性能,该算子功能比前面几种都要好,但是它实现起来较为麻烦,Canny算子是一个具有滤波,增强,检测的多阶段的优化算子,在进行处理前,Canny算子先利用高斯平滑滤波器来平滑图像以除去噪声,Canny分割算法采用一阶偏导的有限差分来计算梯度幅值和方向,在处理过程中,Canny算子还将经过一个非极大值抑制的过程,最后Canny算子还采用两个阈值来连接边缘。
19.1原理
Canny 边缘检测是一种非常流行的边缘检测算法,是 John F.Canny 在1986 年提出的。它是一个有很多步构成的算法,我们接下来会逐步介绍。
(1).噪声去除
由于边缘检测很容易受到噪声影响,所以第一步是使用 5x5 的高斯滤波器 去除噪声,这个前面我们已经学过了。
(2).计算图像梯度
对平滑后的图像使用 Sobel 算子计算水平方向和竖直方向的一阶导数(图 像梯度)(Gx 和 Gy)。根据得到的这两幅梯度图(Gx 和 Gy)找到边界的梯 度和方向,公式如下:
梯度的方向一般总是与边界垂直。梯度方向被归为四类:垂直,水平,和两个对角线。
(3).非极大值抑制
在获得梯度的方向和大小之后,应该对整幅图像做一个扫描,去除那些非 边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯 度方向的点中最大的。如下图所示:
现在你得到的是一个包含“窄边界”的二值图像。
(4).滞后阈值
现在要确定那些边界才是真正的边界。 这时我们需要设置两个阈值: minVal 和 maxVal。当图像的灰度梯度高于 maxVal 时被认为是真的边界, 那些低于 minVal 的边界会被抛弃。如果介于两者之间的话,就要看这个点是 否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃。如下图:
A 高于阈值 maxVal 所以是真正的边界点,C 虽然低于 maxVal 但高于 minVal 并且与 A 相连,所以也被认为是真正的边界点。而 B 就会被抛弃,因 为他不仅低于 maxVal 而且不与真正的边界点相连。所以选择合适的 maxVal 和 minVal 对于能否得到好的结果非常重要。
在这一步,一些小的噪声点也会被除去,因为我们假设边界都是一些长的线段。
19.2 OpenCV中的Canny边界检测
在 OpenCV 中只需要一个函数:cv2.Canny(),就可以完成以上几步。 让我们看如何使用这个函数。
这个函数的第1个参数是输入图像。第2和第3 个分别是 minVal 和 maxVal,其中maxVal用于检测图像中明显的边缘,但一般情况下检测的效果不会那么完美,边缘检测出来是断断续续的。所以这时候用minVal用于将这些间断的边缘连接起来。第4个参数(可选)用来计算图像梯度的 Sobel 卷积核的大小,默认值为 3。最后一个参数(可选) L2gradient是一个布尔值,用来设定 求梯度大小的方程。如果设为 True,就使用更精确的L2范数进行计算(即两个方向的倒数的平方和再开放),否则使用方程:
代替,默认值为 False。
import cv2
from matplotlib import pyplot as plt
# 0表示 cv2.IMREAD_GRAYSCALE,强制转为灰度图。若文件不存在,img 会返回 None(建议添加错误检查)
img = cv2.imread('3.png',0)
# Canny算法:多阶段边缘检测算法(高斯滤波 → 梯度计算 → 非极大值抑制 → 双阈值检测)。
# 100:低阈值(低于此值的梯度被丢弃)。200:高阈值(高于此值的梯度视为强边缘)。
# 介于两者之间的梯度需与强边缘连接才会保留。
edges = cv2.Canny(img,100,200)
#subplot(121):创建1行2列的子图布局,当前操作第1个子图。
#imshow(img, cmap='gray'):显示原始灰度图像,cmap='gray' 指定灰度配色。
plt.subplot(121),plt.imshow(img,cmap = 'gray')
plt.title('Original Image'), plt.xticks([]), plt.yticks([])
#edges:显示二值化的边缘检测结果(白色为边缘,黑色为背景)
plt.subplot(122),plt.imshow(edges,cmap = 'gray')
plt.title('Edge Image'), plt.xticks([]), plt.yticks([])
plt.show()
结果:
这段代码实现了一个实时视频边缘检测系统,通过摄像头捕获画面,使用Canny算法检测边缘,并以三种不同方式可视化结果。以下是详细解析:
1. 核心功能
实时边缘检测:通过摄像头(VideoCapture(1))捕获画面,对每一帧进行边缘处理。
三种可视化模式:
1、蓝色轮廓线:在黑色背景上绘制Canny检测到的边缘轮廓(draw_Contour_Line)
2、原始Canny二值图:直接显示Canny算法的二值化边缘结果(draw_Contour_Line2)
3、形态学梯度增强:对Canny结果进一步做形态学梯度处理(draw_Contour_Line3)
交互调节:通过滑动条动态调整Canny阈值和高斯模糊参数。
# -*- coding:utf-8 -*-
import cv2
import numpy as np
'''
步骤:
(1).彩色图像转换为灰度图像(以灰度图或者单通道图读入)
(2).对图像进行高斯模糊(去噪)
(3).计算图像梯度,根据梯度计算图像边缘幅值与角度
(4).沿梯度方向进行非极大值抑制(边缘细化)
(5).双阈值边缘连接处理
(6).二值化图像输出结果
'''
cv2.namedWindow('Cannys', 0)
# 创建滑动条 作用:用户可实时调节参数,无需重启程序。
cv2.namedWindow('Cannys', 0) # 创建可调整大小的窗口
cv2.createTrackbar('minval', 'Cannys', 120, 300, lambda x: None) # Canny低阈值(默认120)
cv2.createTrackbar('maxval', 'Cannys', 200, 300, lambda x: None) # Canny高阈值(默认200)
cv2.createTrackbar('blur', 'Cannys', 12, 100, lambda x: None) # 高斯模糊强度(默认1.2)
# 绘制等高线轮廓
def draw_Contour_Line(frame):
# 去噪
blur = cv2.getTrackbarPos('blur', 'Cannys') # 高斯模糊去噪
frame = cv2.GaussianBlur(frame, (5, 5), blur*0.1)
# 读取滑动条数值
minval = cv2.getTrackbarPos('minval', 'Cannys')
maxval = cv2.getTrackbarPos('maxval', 'Cannys')
# threshold1 threshold2 两个阈值,小的控制边缘连接,大的控制强边缘的初始分割。如果一个像素的梯度大于上限值,则认为是边缘像素,如果小于下限阈值,则抛弃,如若点的梯度在两者之间,则当这个点与高于上限值的像素点连接时才保留,否则删除。
# aperture_size 算子内核大小,表示Sobel 算子大小,默认为3即表示一个3*3的矩阵
canny = cv2.Canny(frame, threshold1=minval, threshold2=maxval) # Canny边缘检测
# canny = cv2.Canny(frame, threshold1=60, threshold2=180)
# RETR_EXTERNAL:表示只检测最外层轮廓; RETR_CCOMP:提取所有轮廓; RETR_TREE:提取所有轮廓并重新建立网状轮廓结构
# CHAIN_APPROX_SIMPLE:压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标; CHAIN_APPROX_NONE:获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1
thresh, contours, hierarchy = cv2.findContours(canny, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
frameProcessed = np.zeros(frame.shape, dtype=np.uint8) # 创建黑底画布
frameProcessed = cv2.cvtColor(frameProcessed, cv2.COLOR_GRAY2BGR) # 转为三通道(为了彩色绘制)
cv2.drawContours(frameProcessed, contours, -1, (255, 0, 0), 2) # 蓝色轮廓线
return frameProcessed
#直接Canny输出 draw_Contour_Line2
def draw_Contour_Line2(frame):
frame = cv2.GaussianBlur(frame, (5, 5), 1.5) # 固定模糊参数
minval = cv2.getTrackbarPos('minval', 'Cannys')
maxval = cv2.getTrackbarPos('maxval', 'Cannys')
canny = cv2.Canny(frame, threshold1=minval, threshold2=maxval)
#canny = cv2.Canny(frame, 60, 180)
return canny # 返回二值边缘图
#形态学梯度增强 draw_Contour_Line3 突出边缘的宽度差异(效果类似描边)。
def draw_Contour_Line3(frame):
frame = cv2.GaussianBlur(frame, (5, 5), 1.5)
canny = cv2.Canny(frame, threshold1=60, threshold2=180) # 固定阈值
# 形态学:边缘检测
_, Thr_img = cv2.threshold(canny, 210, 255, cv2.THRESH_BINARY) # 二值化 设定红色通道阈值210(阈值影响梯度运算效果)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3)) # 定义矩形结构元素
gradient = cv2.morphologyEx(Thr_img, cv2.MORPH_GRADIENT, kernel) # 形态学梯度
return gradient
camera = cv2.VideoCapture(1) # 打开摄像头(1为外接摄像头索引)
while True:
ret, frame = camera.read() # 读取帧
gray_L = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 转灰度
frame_edge1 = draw_Contour_Line(gray_L) # 蓝色轮廓
frame_edge2 = draw_Contour_Line2(gray_L) # 原始Canny
frame_edge3 = draw_Contour_Line3(gray_L) # 形态学梯度
cv2.imshow("frame", frame) # 原始帧
cv2.imshow("Canny-contour1", frame_edge1)
cv2.imshow("Canny-contour2", frame_edge2)
cv2.imshow("Canny-contour3", frame_edge3)
if cv2.waitKey(1) == ord("q"): # 按q退出
break
camera.release()
cv2.destroyAllWindows()
2. 参数调节建议
- 效果对比
更多资源
Canny edge detector at Wikipedia
Canny Edge Detection Tutorial by Bill Green, 2002.