【计算机视觉实战】第3章 | 图像滤波与边缘检测:从噪声中提取信息
前两章我们学习了图像的基础知识和OpenCV的核心操作。本章将深入探讨图像滤波技术,理解如何去除噪声、平滑图像,以及如何检测图像中的边缘信息。这些是计算机视觉中特征提取的基础。
1. 环境声明
- Python版本:
Python 3.12+ - PyTorch版本:
PyTorch 2.2+ - OpenCV版本:
OpenCV 4.10+ - 操作系统:Windows / macOS / Linux (通用)
2. 什么是图像滤波
2.1 滤波的本质
想象你正在拍摄一张照片,但光线不足导致图像有很多噪点。图像滤波就像给照片戴上了一副特殊的眼镜,能够去除噪点、平滑图像,或者增强某些特征。
数学本质:
图像滤波是一个卷积操作。对于每个像素,我们用一个核(kernel)去扫描它周围的像素,根据核的权重计算新的像素值。
公式表示:
g(x,y) = Σ Σ f(x+i, y+j) * h(i,j)
其中:
- f(x,y) 是原始图像
- h(i,j) 是滤波核(卷积核)
- g(x,y) 是滤波后的图像
2.2 卷积核(滤波核)
卷积核是一个小的矩阵,决定了如何计算每个像素的新值。
举例:3x3均值滤波核:
[1/9 1/9 1/9]
[1/9 1/9 1/9]
[1/9 1/9 1/9]
这个核将每个像素替换为它周围3x3区域内所有像素的平均值,达到平滑效果。
卷积过程的可视化:
- 将核放在图像的某个像素上
- 核的中心对准该像素
- 将核的每个元素与对应像素的值相乘
- 将所有乘积相加,得到新的像素值
- 移动到下一个像素,重复步骤1-4
3. 线性滤波
3.1 均值滤波(Mean Filtering)
原理:用邻域内像素的平均值替代中心像素值。
特点:
- 简单快速
- 会模糊图像边缘
- 对所有像素一视同仁
数学公式:
g(x,y) = (1/(2k+1)^2) * Σ Σ f(x+i, y+j)
其中k是核的半径(如3x3核k=1)
import cv2
import numpy as np
# 读取图像
img = cv2.imread('image.jpg')
# 应用均值滤波
# 参数:图像,核的大小(宽,高)
blurred_3x3 = cv2.blur(img, (3, 3))
blurred_5x5 = cv2.blur(img, (5, 5))
blurred_10x10 = cv2.blur(img, (10, 10))
# 使用自定义核
kernel_size = 5
kernel = np.ones((kernel_size, kernel_size), np.float32) / (kernel_size ** 2)
custom_blur = cv2.filter2D(img, -1, kernel)
3.2 高斯滤波(Gaussian Filtering)
原理:使用高斯函数作为权重,离中心越近的像素权重越大。
为什么高斯滤波更好?
- 符合自然规律(很多自然现象服从高斯分布)
- 边缘保持较好
- 可分离性(计算效率高)
二维高斯函数:
G(x,y) = (1/(2πσ²)) * exp(-(x²+y²)/(2σ²))
其中σ(sigma)控制平滑程度:
- σ越小,权重越集中,平滑效果越弱
- σ越大,权重越分散,平滑效果越强
import cv2
# 高斯滤波
# 参数:图像,核宽,核高,X方向标准差,Y方向标准差
gaussian_3 = cv2.GaussianBlur(img, (3, 3), 0)
gaussian_5 = cv2.GaussianBlur(img, (5, 5), 0)
gaussian_7 = cv2.GaussianBlur(img, (7, 7), 0)
# 使用自定义sigma
gaussian_custom = cv2.GaussianBlur(img, (0, 0), sigmaX=2.0)
# 注意:核大小必须是奇数
# 如果设为0,OpenCV会自动计算合适的核大小
高斯核的计算示例(5x5,sigma=1):
import numpy as np
from scipy import ndimage
# 计算高斯核
def gaussian_kernel(size, sigma):
"""生成高斯核"""
ax = np.linspace(-(size - 1) / 2., (size - 1) / 2., size)
gauss = np.exp(-0.5 * np.square(ax) / np.square(sigma))
kernel = np.outer(gauss, gauss)
return kernel / np.sum(kernel)
# 生成5x5核,sigma=1
kernel_5x5 = gaussian_kernel(5, 1.0)
print("5x5高斯核 (sigma=1):")
print(kernel_5x5)
3.3 可分离滤波的优势
高斯滤波的一个重要特性是可分离性:
G(x,y) = G(x) * G(y)
这意味着我们可以先对行进行1D高斯滤波,再对列进行1D高斯滤波,计算复杂度从O(n²)降低到O(n)。
对于NxN的图像和MxM的核:
- 2D卷积:O(N² × M²)
- 可分离1D卷积:O(N² × 2M) = O(2N²M)
当M较大时,性能提升非常显著。
4. 非线性滤波
4.1 中值滤波(Median Filtering)
原理:用邻域内像素的中值替代中心像素。
特点:
- 对椒盐噪声(salt-and-pepper noise)特别有效
- 边缘保持较好
- 计算复杂度较高(需要排序)
椒盐噪声示例:
- "椒"噪声:随机黑色像素
- "盐"噪声:随机白色像素
- 常见于图像传输错误或传感器故障
import cv2
import numpy as np
# 添加椒盐噪声
def add_salt_and_pepper_noise(image, salt_prob=0.01, pepper_prob=0.01):
noisy = image.copy()
total_pixels = image.shape[0] * image.shape[1]
# 盐噪声(白色)
num_salt = int(total_pixels * salt_prob)
salt_coords = [np.random.randint(0, i-1, num_salt) for i in image.shape[:2]]
noisy[salt_coords[0], salt_coords[1]] = 255
# 椒噪声(黑色)
num_pepper = int(total_pixels * pepper_prob)
pepper_coords = [np.random.randint(0, i-1, num_pepper) for i in image.shape[:2]]
noisy[pepper_coords[0], pepper_coords[1]] = 0
return noisy
# 生成含噪声图像
noisy_img = add_salt_and_pepper_noise(img, 0.02, 0.02)
# 中值滤波
median_3 = cv2.medianBlur(noisy_img, 3)
median_5 = cv2.medianBlur(noisy_img, 5)
# 对比:高斯滤波对椒盐噪声效果不好
gaussian_noisy = cv2.GaussianBlur(noisy_img, (5, 5), 0)
4.2 双边滤波(Bilateral Filtering)
问题:传统滤波会模糊边缘,如何在平滑的同时保持边缘清晰?
双边滤波原理:
考虑两个权重:
- 空间权重:像空间距离,与普通高斯滤波相同
- 灰度权重:像素值差异,值差异大的像素权重小
公式:
I_filtered(x) = (1/W) * Σ I(xi) * G_space(||x - xi||) * G_range(|I(x) - I(xi)|)
其中:
- G_space:空间高斯(距离越近权重越大)
- G_range:灰度高斯(颜色越相似权重越大)
- W:归一化因子
import cv2
# 双边滤波
# 参数:图像,直径,色彩空间sigma,坐标空间sigma
bilateral = cv2.bilateralFilter(img, 9, 75, 75)
# 参数说明:
# - d: 滤波直径,每个像素邻域的直径。-1表示根据sigmaSpace自动计算
# - sigmaColor: 颜色空间滤波sigma,值越大意味着越远的颜色会被混合
# - sigmaSpace: 坐标空间滤波sigma,值越大意味着越远的像素会影响当前像素
# 更强的平滑效果
bilateral_strong = cv2.bilateralFilter(img, 15, 100, 100)
应用场景:
- 人像美颜(平滑皮肤但保持五官清晰)
- 图像风格化
- 预处理去噪
5. 边缘检测基础
5.1 什么是边缘
边缘是图像中灰度值发生剧烈变化的地方,通常对应物体的边界。
边缘类型:
- 阶跃边缘(Step Edge):灰度值从一个值突变到另一个值
- 斜坡边缘(Ramp Edge):灰度值渐变
- 屋脊边缘(Roof Edge):灰度值先增后减(如细线)
- 脉冲边缘(Spike Edge):孤立的突变点
5.2 图像梯度
边缘检测的核心是计算图像的梯度(灰度变化率)。
梯度定义:
∇f = [∂f/∂x, ∂f/∂y]
梯度的大小(幅值):
|∇f| = sqrt((∂f/∂x)² + (∂f/∂y)²)
梯度的方向:
θ = arctan(∂f/∂y / ∂f/∂x)
离散近似:
由于图像是离散的,我们用差分近似导数:
∂f/∂x ≈ f(x+1,y) - f(x,y)
∂f/∂y ≈ f(x,y+1) - f(x,y)
6. 边缘检测算子
6.1 Sobel算子
原理:使用两个3x3核分别检测水平和垂直方向的边缘。
水平核(检测垂直边缘):
[-1 0 1]
[-2 0 2]
[-1 0 1]
垂直核(检测水平边缘):
[-1 -2 -1]
[ 0 0 0]
[ 1 2 1]
权重设计原理:
- 中心行/列权重更大,因为中心像素与当前像素更相关
- 对称性保证旋转不变性
- 权重和为0,对均匀区域响应为0
import cv2
import numpy as np
# 读取图像并转为灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Sobel算子
# 参数:图像,输出深度,x方向导数阶数,y方向导数阶数,核大小
ddepth = cv2.CV_64F # 64位浮点,避免溢出
sobelx = cv2.Sobel(gray, ddepth, 1, 0, ksize=3) # 水平边缘
sobely = cv2.Sobel(gray, ddepth, 0, 1, ksize=3) # 垂直边缘
# 取绝对值并转为8位
sobelx_abs = cv2.convertScaleAbs(sobelx)
sobely_abs = cv2.convertScaleAbs(sobely)
# 合并梯度
sobel_combined = cv2.addWeighted(sobelx_abs, 0.5, sobely_abs, 0.5, 0)
# 或者计算梯度幅值
gradient_magnitude = np.sqrt(sobelx**2 + sobely**2)
gradient_magnitude = cv2.convertScaleAbs(gradient_magnitude)
6.2 Scharr算子
改进之处:
Scharr算子是Sobel算子的改进版本,使用不同的权重,对弱边缘更敏感。
核设计:
- 考虑了旋转最优性
- 在45度角上的响应更好
使用场景:
- 当需要检测弱边缘时
- 当Sobel核大小为3时,Scharr通常效果更好
# Scharr算子
scharrx = cv2.Scharr(gray, ddepth, 1, 0)
scharry = cv2.Scharr(gray, ddepth, 0, 1)
scharrx_abs = cv2.convertScaleAbs(scharrx)
scharry_abs = cv2.convertScaleAbs(scharry)
scharr_combined = cv2.addWeighted(scharrx_abs, 0.5, scharry_abs, 0.5, 0)
6.3 Laplacian算子
原理:计算二阶导数,检测灰度值的快速变化。
特点:
- 对噪声敏感(需要预处理平滑)
- 可以检测所有方向的边缘
- 会产生双边缘(正负响应)
常用核:
[ 0 1 0]
[ 1 -4 1]
[ 0 1 0]
# Laplacian算子
laplacian = cv2.Laplacian(gray, ddepth, ksize=3)
laplacian_abs = cv2.convertScaleAbs(laplacian)
# 更好的做法:先高斯平滑,再Laplacian(LoG:Laplacian of Gaussian)
gaussian_blur = cv2.GaussianBlur(gray, (3, 3), 0)
laplacian_gaussian = cv2.Laplacian(gaussian_blur, ddepth, ksize=3)
laplacian_gaussian_abs = cv2.convertScaleAbs(laplacian_gaussian)
7. Canny边缘检测
7.1 Canny算法的五个步骤
Canny算法是目前最流行的边缘检测算法,包含以下步骤:
步骤1:高斯平滑
去除噪声,防止噪声被误判为边缘。
步骤2:计算梯度幅值和方向
使用Sobel算子计算每个像素的梯度和方向。
步骤3:非极大值抑制(NMS)
沿着梯度方向,只保留局部最大值,细化边缘。
步骤4:双阈值检测
- 高阈值(maxVal):高于此值的肯定是边缘
- 低阈值(minVal):低于此值的肯定不是边缘
- 中间值:如果与强边缘相连,则为边缘;否则丢弃
步骤5:边缘连接
通过滞后阈值连接边缘,形成连续的边缘线。
7.2 Canny算法实现
import cv2
# Canny边缘检测
# 参数:图像,低阈值,高阈值
edges = cv2.Canny(gray, 50, 150)
# 调整阈值
def nothing(x):
pass
# 创建窗口
cv2.namedWindow('Canny')
cv2.createTrackbar('Low Threshold', 'Canny', 50, 255, nothing)
cv2.createTrackbar('High Threshold', 'Canny', 150, 255, nothing)
while True:
low = cv2.getTrackbarPos('Low Threshold', 'Canny')
high = cv2.getTrackbarPos('High Threshold', 'Canny')
edges = cv2.Canny(gray, low, high)
cv2.imshow('Canny', edges)
if cv2.waitKey(1) & 0xFF == 27: # ESC键退出
break
cv2.destroyAllWindows()
7.3 阈值选择策略
经验法则:
- 低阈值 = 高阈值 × 0.3 到 0.5
- 高阈值通常在100-200之间
- 对于弱边缘,可以降低阈值
- 对于强噪声,先进行更强的平滑
自适应阈值:
# 基于图像中值自动计算阈值
median = np.median(gray)
lower = int(max(0, 0.33 * median))
upper = int(min(255, 1.33 * median))
edges_auto = cv2.Canny(gray, lower, upper)
8. 多尺度边缘检测
8.1 为什么要多尺度
不同尺度的边缘代表不同层级的信息:
- 细尺度:细节纹理、小物体边缘
- 粗尺度:大物体轮廓、整体结构
8.2 图像金字塔
高斯金字塔:
通过连续降采样构建,每一层是上一层的低分辨率版本。
# 构建高斯金字塔
layer = img.copy()
gaussian_pyramid = [layer]
for i in range(3):
layer = cv2.pyrDown(layer)
gaussian_pyramid.append(layer)
# 拉普拉斯金字塔(用于图像重建)
laplacian_pyramid = []
for i in range(len(gaussian_pyramid)-1, 0, -1):
size = (gaussian_pyramid[i-1].shape[1], gaussian_pyramid[i-1].shape[0])
GE = cv2.pyrUp(gaussian_pyramid[i], dstsize=size)
L = cv2.subtract(gaussian_pyramid[i-1], GE)
laplacian_pyramid.append(L)
8.3 多尺度Canny
# 在不同尺度上进行Canny检测
scales = [1.0, 0.5, 0.25] # 原图、1/2、1/4
canny_results = []
for scale in scales:
if scale == 1.0:
scaled_img = gray
else:
scaled_img = cv2.resize(gray, None, fx=scale, fy=scale)
edges = cv2.Canny(scaled_img, 50, 150)
# 缩放回原始大小
if scale != 1.0:
edges = cv2.resize(edges, (gray.shape[1], gray.shape[0]))
canny_results.append(edges)
9. 实战项目:边缘检测工具箱
创建一个综合的边缘检测工具类:
import cv2
import numpy as np
from typing import Tuple, Optional
class EdgeDetector:
"""边缘检测工具类"""
def __init__(self, image: np.ndarray):
"""初始化
Args:
image: 输入图像(BGR或灰度)
"""
if len(image.shape) == 3:
self.gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
self.gray = image.copy()
self.original = self.gray.copy()
def sobel(self, ksize: int = 3, combine: bool = True) -> np.ndarray:
"""Sobel边缘检测"""
ddepth = cv2.CV_64F
sobelx = cv2.Sobel(self.gray, ddepth, 1, 0, ksize=ksize)
sobely = cv2.Sobel(self.gray, ddepth, 0, 1, ksize=ksize)
sobelx = cv2.convertScaleAbs(sobelx)
sobely = cv2.convertScaleAbs(sobely)
if combine:
return cv2.addWeighted(sobelx, 0.5, sobely, 0.5, 0)
return sobelx, sobely
def scharr(self, combine: bool = True) -> np.ndarray:
"""Scharr边缘检测"""
ddepth = cv2.CV_64F
scharrx = cv2.Scharr(self.gray, ddepth, 1, 0)
scharry = cv2.Scharr(self.gray, ddepth, 0, 1)
scharrx = cv2.convertScaleAbs(scharrx)
scharry = cv2.convertScaleAbs(scharry)
if combine:
return cv2.addWeighted(scharrx, 0.5, scharry, 0.5, 0)
return scharrx, scharry
def laplacian(self, ksize: int = 3, blur_first: bool = True) -> np.ndarray:
"""Laplacian边缘检测"""
if blur_first:
img = cv2.GaussianBlur(self.gray, (3, 3), 0)
else:
img = self.gray
ddepth = cv2.CV_64F
laplacian = cv2.Laplacian(img, ddepth, ksize=ksize)
return cv2.convertScaleAbs(laplacian)
def canny(self, threshold1: int = 50, threshold2: int = 150,
apertureSize: int = 3, L2gradient: bool = False) -> np.ndarray:
"""Canny边缘检测"""
return cv2.Canny(self.gray, threshold1, threshold2,
apertureSize=apertureSize, L2gradient=L2gradient)
def auto_canny(self, sigma: float = 0.33) -> np.ndarray:
"""自适应阈值Canny"""
median = np.median(self.gray)
lower = int(max(0, (1.0 - sigma) * median))
upper = int(min(255, (1.0 + sigma) * median))
return cv2.Canny(self.gray, lower, upper)
def prewitt(self) -> np.ndarray:
"""Prewitt边缘检测(OpenCV没有内置,手动实现)"""
kernelx = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]])
kernely = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]])
prewittx = cv2.filter2D(self.gray, cv2.CV_64F, kernelx)
prewitty = cv2.filter2D(self.gray, cv2.CV_64F, kernely)
prewittx = cv2.convertScaleAbs(prewittx)
prewitty = cv2.convertScaleAbs(prewitty)
return cv2.addWeighted(prewittx, 0.5, prewitty, 0.5, 0)
def roberts(self) -> np.ndarray:
"""Roberts边缘检测"""
kernelx = np.array([[1, 0], [0, -1]])
kernely = np.array([[0, 1], [-1, 0]])
robertsx = cv2.filter2D(self.gray, cv2.CV_64F, kernelx)
robertsy = cv2.filter2D(self.gray, cv2.CV_64F, kernely)
robertsx = cv2.convertScaleAbs(robertsx)
robertsy = cv2.convertScaleAbs(robertsy)
return cv2.addWeighted(robertsx, 0.5, robertsy, 0.5, 0)
def compare_all(self) -> dict:
"""对比所有边缘检测方法"""
results = {
'Original': self.gray,
'Sobel': self.sobel(),
'Scharr': self.scharr(),
'Laplacian': self.laplacian(),
'Canny': self.canny(),
'Auto Canny': self.auto_canny(),
'Prewitt': self.prewitt(),
'Roberts': self.roberts()
}
return results
# 使用示例
if __name__ == "__main__":
# 读取图像
img = cv2.imread('test_image.jpg')
# 创建检测器
detector = EdgeDetector(img)
# 对比所有方法
results = detector.compare_all()
# 显示结果
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
axes = axes.flatten()
for i, (name, result) in enumerate(results.items()):
axes[i].imshow(result, cmap='gray')
axes[i].set_title(name)
axes[i].axis('off')
plt.tight_layout()
plt.savefig('edge_detection_comparison.png', dpi=150)
plt.show()
10. 避坑小贴士
常见错误1:忘记转换为灰度图
现象:边缘检测结果异常,出现彩色边缘
原因:Sobel、Canny等算子通常在灰度图上计算
解决方案:
# 错误
edges = cv2.Canny(color_img, 50, 150) # 可能出错
# 正确
gray = cv2.cvtColor(color_img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150)
常见错误2:数据类型溢出
现象:梯度计算后出现黑斑或异常亮斑
原因:8位整数运算溢出
解决方案:
# 错误
ddepth = cv2.CV_8U # 会溢出
sobelx = cv2.Sobel(gray, ddepth, 1, 0)
# 正确
ddepth = cv2.CV_64F # 64位浮点
sobelx = cv2.Sobel(gray, ddepth, 1, 0)
sobelx = cv2.convertScaleAbs(sobelx) # 转换回8位
常见错误3:Canny阈值设置不当
现象:边缘过多(噪声)或过少(丢失边缘)
解决方案:
# 使用自适应阈值
median = np.median(gray)
lower = int(max(0, 0.33 * median))
upper = int(min(255, 1.33 * median))
edges = cv2.Canny(gray, lower, upper)
常见错误4:忽视预处理
现象:边缘检测被噪声干扰
解决方案:
# 先平滑再检测
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 50, 150)
11. 本章小结
通过本章的学习,你应该掌握了:
- 滤波原理:理解了卷积和滤波核的概念
- 线性滤波:均值滤波、高斯滤波的特性和使用场景
- 非线性滤波:中值滤波、双边滤波的优势
- 边缘检测基础:梯度、图像微分的概念
- 边缘检测算子:Sobel、Scharr、Laplacian的原理和实现
- Canny算法:五步法、非极大值抑制、双阈值检测
- 多尺度检测:图像金字塔、多尺度边缘提取
- 实践编程:完整的边缘检测工具类
一句话总结:滤波是图像处理的"调色板",可以平滑、锐化或提取特征;边缘检测是图像理解的"第一步",将图像从像素域转换到特征域。
12. 练习与思考
- 噪声对比:比较不同滤波器对高斯噪声和椒盐噪声的效果
- 自适应滤波:实现一个根据局部方差自适应选择滤波器的算法
- 边缘方向:修改Canny算法,输出边缘方向图
- 角点检测:结合Sobel x和y方向的梯度,实现Harris角点检测
- 图像融合:使用拉普拉斯金字塔实现图像的无缝融合
下一章预告:第4章《形态学操作与图像分割》将学习如何使用形态学运算处理二值图像,以及如何使用阈值和连通域分析进行图像分割。
如果本章内容对你有帮助,欢迎点赞、收藏和关注。
更多推荐



所有评论(0)