OpenCV入门学习(二):图像预处理
前言:本文是OpenCV系列教程的第二篇。在掌握了图像的基本操作之后,我们将进入图像预处理阶段。本章涵盖五个核心知识点:阈值处理、噪声处理、边界填充、边缘检测、图像形态学。以函数用法和参数调优为主,理论点到为止,代码紧跟知识点。
目录
- 一、阈值处理
- 二、噪声处理
- 三、边界填充
- 四、边缘检测
- 五、图像形态学
- 六、总结
一、阈值处理
阈值处理是指剔除图像内像素值高于一定值或低于一定值的像素点。
核心函数:retval, dst = cv2.threshold(src, thresh, maxval, type)
| src | 要进行阈值分割的图像,可以是多通道的,8位或32位浮点型数值 |
| thresh | 要设定的阈值 |
| maxval | 当type为THRESH_BINARY或THRESH_BINARY_INV时,需要设定的最大值 |
| retval | 返回的阈值 |
| dst | 阈值分割结果图像,与原始图像具有相同的大小和类型 |
| type | 阈值分割的类型 |
阈值类型详解:
| cv2.THRESH_BINARY | maxval | 0 |
| cv2.THRESH_BINARY_INV | 0 | maxval |
| cv2.THRESH_TRUNC | thresh | 当前灰度值 |
| cv2.THRESH_TOZERO | 当前灰度值 | 0 |
| cv2.THRESH_TOZERO_INV | 0 | 当前灰度值 |
import cv2
# 读取图像,0表示以灰度模式读取
img = cv2.imread("yys1.jpg", 0)
# 阈值处理:像素值大于175的设为255,小于等于175的设为0
ret1, binary = cv2.threshold(img, 175, 255, cv2.THRESH_BINARY)
# 阈值处理:像素值大于175的设为0,小于等于175的设为255(与BINARY相反)
ret2, binaryinv = cv2.threshold(img, 175, 255, cv2.THRESH_BINARY_INV)
# 阈值处理:像素值大于175的截断为175,小于等于175的保持不变
ret3, trunc = cv2.threshold(img, 175, 255, cv2.THRESH_TRUNC)
# 阈值处理:像素值大于175的保持不变,小于等于175的设为0
ret4, tozero = cv2.threshold(img, 175, 255, cv2.THRESH_TOZERO)
# 阈值处理:像素值大于175的设为0,小于等于175的保持不变(与TOZERO相反)
ret5, tozeroinv = cv2.threshold(img, 175, 255, cv2.THRESH_TOZERO_INV)
# 显示原图
cv2.imshow("yuan", img)
# 显示五种阈值处理结果
cv2.imshow('binary', binary)
cv2.imshow('binaryinv', binaryinv)
cv2.imshow('trunc', trunc)
cv2.imshow('tozero', tozero)
cv2.imshow('tozeroinv', tozeroinv)
# 等待按键后关闭所有窗口
cv2.waitKey(0)
cv2.destroyAllWindows()

二、噪声处理
噪声是图像中的随机干扰信号,椒盐噪声是其中常见的一种,表现为随机出现的黑白像素点。滤波是去除噪声的主要手段。
2.1 椒盐噪声生成
代码位置:codes/ch2/04_add_noise.py
import cv2
import numpy as np
# 椒盐噪声生成函数
def add_peppersalt_noise(img, n=10000):
"""
为图像添加椒盐噪声
参数:
img: 输入图像
n: 噪声点的数量,默认10000
返回:
添加噪声后的图像
"""
result = img.copy()
h, w = img.shape[:2]
for i in range(n):
# 随机选取像素位置
x = np.random.randint(0, h)
y = np.random.randint(0, w)
# 随机决定是椒噪声(黑色,0)还是盐噪声(白色,255)
if np.random.randint(0, 2) == 0:
result[x, y] = 0 # 椒噪声:黑色
else:
result[x, y] = 255 # 盐噪声:白色
return result
img = cv2.imread('yys1.jpg')
noise = add_peppersalt_noise(img)
cv2.imshow('img', img)
cv2.imshow('noise', noise)
cv2.waitKey(0)
cv2.destroyAllWindows()

2.2 均值滤波
核心函数:dst = cv2.blur(src, ksize)
| src | 需要处理的图像 |
| ksize | 滤波核的大小,如(3,3)、(63,63) |
一般情况下,使用 dst = cv2.blur(src, ksize) 即可。
# 均值滤波:用邻域内所有像素的平均值替代中心像素值
# 核大小为3×3(较小,去噪效果弱但保留更多细节)
blur_1 = cv2.blur(noise, (3, 3))
cv2.imshow('blur_1', blur_1)
# 均值滤波:核大小为63×63(较大,去噪效果好但图像更模糊)
blur_2 = cv2.blur(noise, (63, 63))
cv2.imshow('blur_2', blur_2)
# 显示原图和噪声图作为对比
cv2.imshow('img', img)
cv2.imshow('noise', noise)
cv2.waitKey(0)
cv2.destroyAllWindows()

2.3 方框滤波
核心函数:dst = cv2.boxFilter(src, ddepth, ksize, anchor, normalize, borderType)
| src | 需要处理的图像 |
| ddepth | 图像深度,-1表示与原始图像相同 |
| ksize | 滤波核的大小 |
| normalize | True时归一化,与均值滤波效果相同;False时不归一化 |
# 方框滤波
# normalize=True时,归一化,与均值滤波效果相同
boxFilter_1 = cv2.boxFilter(noise, –1, (3, 3), normalize=True)
# normalize=False时,不归一化,直接使用邻域像素值的和,和>255时使用255
boxFilter_2 = cv2.boxFilter(noise, –1, (3, 3), normalize=False)
cv2.imshow('boxFilter_1', boxFilter_1)
cv2.imshow('boxFilter_2', boxFilter_2)
cv2.imshow('img', img)
cv2.imshow('noise', noise)
cv2.waitKey(0)
cv2.destroyAllWindows()

2.4 高斯滤波
核心函数:cv2.GaussianBlur(src, ksize, sigmaX)
| src | 输入图像 |
| ksize | 滤波器的大小,如(5,5) |
| sigmaX | X轴方向的标准差 |
# 高斯滤波:使用高斯核进行加权平均,离中心越近的像素权重越大
# 核大小5×5,X轴标准差为1
gaussian_blur = cv2.GaussianBlur(noise, (5, 5), 1)
cv2.imshow('gaussian_blur', gaussian_blur)
cv2.imshow('img', img)
cv2.imshow('noise', noise)
cv2.waitKey(0)
cv2.destroyAllWindows()

2.5 中值滤波
核心函数:cv2.medianBlur(src, ksize)
| src | 输入图像 |
| ksize | 滤波器的大小,整数,如3表示3×3 |
# 中值滤波:用邻域内所有像素的中值替代中心像素值
# 对椒盐噪声效果极佳,核大小为3×3
median_blur = cv2.medianBlur(noise, 3)
cv2.imshow('median_blur', median_blur)
cv2.imshow('img', img)
cv2.imshow('noise', noise)
cv2.waitKey(0)
cv2.destroyAllWindows()

三、边界填充
边界填充是在图像周围添加额外的像素边框,常用于卷积操作前保持图像尺寸、或为图像添加装饰边框。
核心函数:cv2.copyMakeBorder(src, top, bottom, left, right, borderType, value)
| src | 要扩充边界的原始图像 |
| top, bottom, left, right | 相应方向上的边框宽度 |
| borderType | 定义要添加边框的类型 |
| value | 填充值(仅对BORDER_CONSTANT有效) |
borderType填充方式详解:
| cv2.BORDER_CONSTANT | 添加的边框像素值为常数(需要额外给定value参数) |
| cv2.BORDER_REFLECT | 边框像素是边界元素的镜面反射,例如:gfedcba | abcdefgh | hgfedcba(交界处也复制了) |
| cv2.BORDER_REFLECT_101 或 cv2.BORDER_DEFAULT | 与上面类似,但交界处除外,例如:gfedcb | abcdefgh | gfedcba |
| cv2.BORDER_REPLICATE | 使用最边界的像素值代替,例如:aaaaaa | abcdefgh | hhhhhhh |
| cv2.BORDER_WRAP | 上下左右边依次替换,例如:cdefgh | abcdefgh | abcdefg |
import cv2
img = cv2.imread('yys1.jpg')
top, bot, lf, rt = 15, 15, 15, 15
# 常数填充:边框像素值为常数[50,50,50]
constant = cv2.copyMakeBorder(img, top, bot, lf, rt, borderType=cv2.BORDER_CONSTANT, value=[50, 50, 50])
# 镜像填充:边框像素是边界元素的镜面反射,例如 gfedcba|abcdefgh|hgfedcba
refl = cv2.copyMakeBorder(img, top, bot, lf, rt, borderType=cv2.BORDER_REFLECT)
# 镜像填充101:与REFLECT类似,但交界处不重复,例如 gfedcb|abcdefgh|gfedcba
refl101 = cv2.copyMakeBorder(img, top, bot, lf, rt, borderType=cv2.BORDER_REFLECT101)
# 边缘复制填充:使用最边界的像素值代替,例如 aaaaaa|abcdefgh|hhhhhhh
repl = cv2.copyMakeBorder(img, top, bot, lf, rt, borderType=cv2.BORDER_REPLICATE)
# 平铺填充:上下左右边依次替换,例如 cdefgh|abcdefgh|abcdefg
wrap = cv2.copyMakeBorder(img, top, bot, lf, rt, borderType=cv2.BORDER_WRAP)
cv2.imshow("img", img)
cv2.imshow('constant', constant)
cv2.imshow('reflect', refl)
cv2.imshow('reflect101', refl101)
cv2.imshow('replicate', repl)
cv2.imshow('wrap', wrap)
cv2.waitKey(0)
cv2.destroyAllWindows()

四、边缘检测
边缘是图像中灰度发生剧烈变化的位置,边缘检测是计算机视觉中最基础的特征提取方法。
4.1 Sobel算子
核心函数:cv2.Sobel(src, ddepth, dx, dy[, ksize[, scale[, delta[, borderType]]]])
| src | 输入图像 |
| ddepth | 输出图像的深度,-1表示与原图像相同的深度 |
| dx, dy | 当组合为dx=1,dy=0时求x方向的一阶导数;dx=0,dy=1时求y方向的一阶导数 |
| ksize | Sobel算子的大小,必须是1、3、5或7,默认为3 |
import cv2
# ==================== Sobel算子基础演示 ====================
# cv2.Sobel(src, ddepth, dx, dy[, ksize])
# 参数:
# src: 输入图像
# ddepth: 输出图像的深度,-1表示与原图像相同
# dx,dy: dx=1,dy=0求x方向一阶导数;dx=0,dy=1求y方向一阶导数
# ksize: Sobel算子大小,必须是1、3、5或7,默认为3
yuan = cv2.imread("yuan.png")
yuan = cv2.resize(yuan, None, fx=0.7, fy=0.7)
cv2.imshow("yuan", yuan)
# x方向边缘(uint8类型,负数信息保存不了)
yuan_x = cv2.Sobel(yuan, –1, dx=1, dy=0)
cv2.imshow("yuan_x", yuan_x)
# x方向边缘(CV_64F类型,可保存负数)
yuan_x_64 = cv2.Sobel(yuan, cv2.CV_64F, 1, 0)
cv2.imshow("yuan_x_64", yuan_x_64)
# x方向边缘(取绝对值,负数转正数显示)
yuan_x_full = cv2.convertScaleAbs(yuan_x_64)
cv2.imshow("yuan_x_full", yuan_x_full)
# y方向边缘(uint8类型,负数信息保存不了)
yuan_y = cv2.Sobel(yuan, –1, 0, 1)
cv2.imshow("yuan_y", yuan_y)
# y方向边缘(CV_64F类型,可保存负数)
yuan_y_64 = cv2.Sobel(yuan, cv2.CV_64F, 0, 1)
cv2.imshow("yuan_y_64", yuan_y_64)
# y方向边缘(取绝对值,负数转正数显示)
yuan_y_full = cv2.convertScaleAbs(yuan_y_64)
cv2.imshow("yuan_y_full", yuan_y_full)
# 不建议同时使用x和y方向(dx=1,dy=1效果不佳)
yuan_xy = cv2.Sobel(yuan, –1, 1, 1)
cv2.imshow("yuan_xy", yuan_xy)
# 使用加权运算组合x和y方向的边缘
yuan_xy_full = cv2.addWeighted(yuan_x_full, 1, yuan_y_full, 1, 0)
cv2.imshow('yuan_xy_full', yuan_xy_full)
cv2.waitKey(0)
cv2.destroyAllWindows()

封装为函数:
def sobel_edge(img, flag=None):
"""Sobel边缘检测"""
if isinstance(img, str):
img = cv2.imread(img, flag)
elif flag == 0 and len(img.shape) == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# X方向梯度(CV_64F保存负数)
img_x_64 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
img_x_full = cv2.convertScaleAbs(img_x_64)
# Y方向梯度(CV_64F保存负数)
img_y_64 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
img_y_full = cv2.convertScaleAbs(img_y_64)
# 加权组合X和Y方向边缘
img_xy_full = cv2.addWeighted(img_x_full, 1, img_y_full, 1, 0)
return img_xy_full
# 分别用彩色图和灰度图测试Sobel效果
a = cv2.imread("yys_1.jpg")
a = cv2.resize(a, None, fx=0.7, fy=0.7)
b = cv2.cvtColor(a, cv2.COLOR_BGR2GRAY)
cv2.imshow("sobel_edge_color", sobel_edge(a))
cv2.imshow("sobel_edge_gray", sobel_edge(b))
cv2.waitKey(0)
cv2.destroyAllWindows()

4.2 Scharr算子
Scharr算子是Sobel算子的改进版,对边缘检测更敏感,核大小固定为3×3。
def scharr_edge(img, flag=None):
"""Scharr边缘检测(Sobel的改进版,对边缘更敏感)"""
if isinstance(img, str):
img = cv2.imread(img, flag)
elif flag == 0 and len(img.shape) == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# X方向梯度(Scharr对边缘更敏感)
img_x_64 = cv2.Scharr(img, cv2.CV_64F, 1, 0)
img_x_full = cv2.convertScaleAbs(img_x_64)
# Y方向梯度
img_y_64 = cv2.Scharr(img, cv2.CV_64F, 0, 1)
img_y_full = cv2.convertScaleAbs(img_y_64)
# 加权组合X和Y方向边缘
img_xy_full = cv2.addWeighted(img_x_full, 1, img_y_full, 1, 0)
return img_xy_full
# 分别用彩色图和灰度图测试Scharr效果
cv2.imshow("scharr_edge_color", scharr_edge(a))
cv2.imshow("scharr_edge_gray", scharr_edge(b))
cv2.waitKey(0)
cv2.destroyAllWindows()

4.3 Laplacian算子
Laplacian算子通过二阶导数检测边缘,对噪声敏感。
核心函数:cv2.Laplacian(src, ddepth[, ksize])
| src | 输入图像 |
| ddepth | 输出图像的深度 |
| ksize | 算子大小,默认为3 |
# Laplacian边缘检测函数
def laplacian_edge(img, flag=None):
"""Laplacian边缘检测(对噪声敏感)"""
if isinstance(img, str):
img = cv2.imread(img, flag)
elif flag == 0 and len(img.shape) == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Laplacian边缘检测
img_lap = cv2.Laplacian(img, cv2.CV_64F, ksize=3)
img_lap_full = cv2.convertScaleAbs(img_lap)
return img_lap_full
# 分别用彩色图和灰度图测试Laplacian效果
cv2.imshow("laplacian_edge_color", laplacian_edge(a))
cv2.imshow("laplacian_edge_gray", laplacian_edge(b))
cv2.waitKey(0)
cv2.destroyAllWindows()

4.4 Canny边缘检测
Canny算法是最经典、最完善的边缘检测方法。
核心函数:cv2.Canny(src, threshold1, threshold2)
| src | 输入图像 |
| threshold1 | 低阈值 |
| threshold2 | 高阈值 |
def canny_edge(img, thresh1=100, thresh2=150):
"""Canny边缘检测(输入彩色或灰度均可,输出为二值边缘图)"""
return cv2.Canny(img, thresh1, thresh2)
cv2.imshow("canny_edge", canny_edge(a))
cv2.waitKey(0)
cv2.destroyAllWindows()

阈值参考:
| 边缘清晰,噪声少 | 100 | 200 |
| 边缘较淡,需敏感 | 30 | 100 |
| 通用起始值 | 50 | 150 |
| 强边缘,抗噪声 | 150 | 250 |
调参技巧:先设 (100, 200),若边缘太多(噪声多)则提高阈值,若边缘太少(丢失细节)则降低阈值。
4.5 算子对比速查
| Sobel | 计算简单 | 基础边缘检测 |
| Scharr | 对边缘更敏感 | 需要更精细边缘 |
| Laplacian | 对噪声敏感 | 提取孤立点或线 |
| Canny | 效果最好 | 通用边缘检测首选 |
五、图像形态学
形态学操作基于结构元素对二值图像进行形状处理,常用于二值图像的后处理。
5.1 结构元素创建
import cv2
import numpy as np
# 直接创建全1矩阵作为结构元素
kernel = np.ones((3, 3), np.uint8)
5.2 腐蚀
核心函数:cv2.erode(src, kernel, dst, anchor, iterations, borderType, borderValue)
| src | 输入图像 |
| kernel | 用于腐蚀的结构元素,形状和大小直接影响腐蚀效果 |
| iterations | 腐蚀操作的迭代次数,默认为1,次数越多腐蚀效果越明显 |
sun = cv2.imread("sun.png")
cv2.imshow("img", sun)
# 腐蚀:用结构元素对图像进行卷积,只有结构元素完全覆盖的区域才保留
# kernel为结构元素,iterations=2表示连续腐蚀两次
erode_sun = cv2.erode(sun, kernel, iterations=2)
cv2.imshow('erode_sun', erode_sun)

5.3 膨胀
核心函数:cv2.dilate(img, kernel, iterations)
| img | 目标图片 |
| kernel | 进行操作的内核 |
| iterations | 膨胀次数,默认为1 |
wenzi = cv2.imread('wenzi.png')
cv2.imshow('wenzi', wenzi)
# 膨胀:用结构元素对图像进行卷积,结构元素覆盖区域有任一像素为白则全部变白
# 用于连接断裂区域、扩大白色区域,iterations=1表示膨胀一次
dilate_wenzi = cv2.dilate(wenzi, kernel, iterations=1)
cv2.imshow('dilate_wenzi', dilate_wenzi)
cv2.waitKey(0)
cv2.destroyAllWindows()

5.4 开运算与闭运算
核心函数:cv2.morphologyEx(src, op, kernel)
| MORPH_OPEN | 先腐蚀后膨胀 | 平滑物体的轮廓、断开较窄的狭颈并消除细的突出物 |
| MORPH_CLOSE | 先膨胀后腐蚀 | 弥合较窄的间断和细长的沟壑,消除小的孔洞,填补轮廓线 |
# 开运算:先腐蚀后膨胀,去除小噪点,分离粘连物体
zhiwen = cv2.imread('zhiwen.png')
cv2.imshow('zhiwen', zhiwen)
open_zhiwen = cv2.morphologyEx(zhiwen, cv2.MORPH_OPEN, kernel)
cv2.imshow('open_zhiwen', open_zhiwen)
# 闭运算:先膨胀后腐蚀,填充小空洞,连接断裂区域
zhiwen_duan = cv2.imread('zhiwen_duan.png')
cv2.imshow('zhiwen_duan', zhiwen_duan)
close_zhiwen_duan = cv2.morphologyEx(zhiwen_duan, cv2.MORPH_CLOSE, kernel, iterations=2)
cv2.imshow('close_zhiwen_duan', close_zhiwen_duan)
cv2.waitKey(0)
cv2.destroyAllWindows()


5.5 梯度运算
梯度运算 = 膨胀 – 腐蚀,作用:突出显示图像中强度变化剧烈的地方(即物体轮廓)。
核心函数:cv2.morphologyEx(img, op, kernel)
| img | 输入图像 |
| op | 操作类型,梯度运算为 MORPH_GRADIENT |
| kernel | 结构元素 |
# 梯度运算 = 膨胀 – 腐蚀,用于提取物体轮廓
gradient_wenzi = cv2.morphologyEx(wenzi, cv2.MORPH_GRADIENT, kernel)
cv2.imshow('wenzi', wenzi)
cv2.imshow('gradient_wenzi', gradient_wenzi)
cv2.waitKey(0)
cv2.destroyAllWindows()

5.6 顶帽与黑帽
| 顶帽 | 原始图像 – 开运算结果 | 提取比周围区域亮的细节 |
| 黑帽 | 闭运算结果 – 原始图像 | 提取比周围区域暗的细节 |
cv2.imshow('sun', sun)
# 开运算:先腐蚀后膨胀,去除小噪点
open_sun = cv2.morphologyEx(sun, cv2.MORPH_OPEN, kernel)
cv2.imshow('open_sun', open_sun)
# 顶帽:原图 – 开运算,提取亮色细节
tophat = cv2.morphologyEx(sun, cv2.MORPH_TOPHAT, kernel)
cv2.imshow('tophat', tophat)
# 闭运算:先膨胀后腐蚀,填充小空洞
close_sun = cv2.morphologyEx(sun, cv2.MORPH_CLOSE, kernel)
cv2.imshow('close_sun', close_sun)
# 黑帽:闭运算 – 原图,提取暗色细节
blackhat = cv2.morphologyEx(sun, cv2.MORPH_BLACKHAT, kernel)
cv2.imshow('blackhat', blackhat)
cv2.waitKey(0)
cv2.destroyAllWindows()

5.7 形态学操作速查
| 腐蚀 | 缩小白色区域,去噪点 |
| 膨胀 | 扩大白色区域,填空洞 |
| 开运算 | 平滑轮廓,消除细突出物 |
| 闭运算 | 弥合间断,消除小孔洞 |
| 梯度运算 | 提取物体轮廓 |
| 顶帽 | 提取比周围区域亮的细节 |
| 黑帽 | 提取比周围区域暗的细节 |
六、总结
核心函数速查表
| 全局阈值 | threshold() | 手动设阈值二值化 |
| 均值滤波 | blur() | 快速去噪,会模糊 |
| 高斯滤波 | GaussianBlur() | 去噪首选,保留边缘好 |
| 中值滤波 | medianBlur() | 专治椒盐噪声 |
| 边界填充 | copyMakeBorder() | 添加图像边框 |
| Sobel梯度 | Sobel() | 计算图像梯度 |
| Canny边缘检测 | Canny() | 边缘检测标准方法 |
| 腐蚀 | erode() | 缩小白色区域 |
| 膨胀 | dilate() | 扩大白色区域 |
| 形态学 | morphologyEx() | 开/闭/梯度运算 |
参数快速参考
| 高斯滤波核 | (3,3)轻度、(5,5)中度、(7,7)重度 |
| Canny阈值 | (50,150)通用起始值 |
| 自适应阈值blockSize | 11、15、21 |
| 形态学核 | (3,3)轻度、(5,5)中度 |
| 边界填充像素数 | 视需求而定,通常5~50 |
常见陷阱
| 滤波后丢失细节 | 尝试双边滤波或缩小核 |
| 阈值分割效果差 | 先增强对比度再分割 |
| Canny检测到过多噪声 | 先高斯滤波再Canny |
| 形态学操作过度 | 减少iterations或使用更小核 |
| 输入彩色图到阈值函数 | 先cvtColor转灰度图 |
系列直达
- 上篇:OpenCV入门学习(一):图像的基本操作
- 本篇:OpenCV入门学习(二):图像预处理(本文)
- 下篇:OpenCV入门学习(三):特征提取与匹配



