OpenCV基础:图像形态学和边缘检测

OpenCV基础:图像形态学和边缘检测

OpenCV基础:图像形态学和边缘检测

本文基于 OpenCV 计算机视觉课程课件与配套示例代码整理,系统讲解图像形态学(形态学运算)图像边缘检测(边缘检测算子)两大部分。


一、图像形态学

1. 什么是图像形态学

图像形态学(Mathematical Morphology)是一种处理图像形状特征的图像处理技术,主要用于描述和处理图像中的形状与结构。形态学运算可以用于:

  • 提取图像中的特征
  • 消除噪声
  • 改变图像的形状(如细化、加粗、断开、连接等)

形态学运算的核心是结构元素(kernel/结构算子),它的形状和大小会直接影响处理效果。常见形态学运算包括:

运算说明
腐蚀(Erode)缩小边界,消除细小突出物
膨胀(Dilate)扩大边界,连接相邻区域
开运算(Opening)先腐蚀后膨胀
闭运算(Closing)先膨胀后腐蚀
梯度运算(Gradient)膨胀 − 腐蚀
顶帽(Top Hat)原图 − 开运算
黑帽(Black Hat)闭运算 − 原图

形态学运算通常在二值图像上进行,但 OpenCV 也支持对灰度/彩色图像操作。

接下来对图像形态学各种运算进行说明。

importcv2# 导入 OpenCV 计算机视觉库,用于图像处理importnumpyasnp# 导入 NumPy 科学计算库,用于数组操作和数学运算

2. 图像腐蚀 cv2.erode

腐蚀的作用是让图像中的高亮区域(白色)“瘦身”,可以消除小的白色噪点、断开两个物体间细小的连接。

函数原型:

cv2.erode(src,kernel,dst,anchor,iterations,borderType,borderValue)

参数说明:

  • src:输入图像
  • kernel:结构元素,形状和大小直接影响腐蚀效果
  • dst:与 src 相同大小和类型的输出图像
  • iterations:腐蚀迭代次数,默认 1。次数越多腐蚀越明显

示例:对sun.png用 5×5 结构元素腐蚀 2 次。

sun=cv2.imread('sun.png')# 读取太阳图像文件cv2.imshow('sun',sun)# 显示原始图像cv2.waitKey(0)kernel=np.ones((5,5),np.uint8)# 创建5x5的全1矩阵作为结构元素(腐蚀核)erosion1=cv2.erode(sun,kernel,iterations=2)# 腐蚀操作,迭代2次增强效果cv2.imshow('erosion1',erosion1)# 显示腐蚀后的图像cv2.waitKey(0)cv2.destroyAllWindows()

运行效果(左原图,右腐蚀结果):可以看到太阳光芒明显变细、变短。


3. 图像膨胀 cv2.dilate

膨胀与腐蚀相反,作用是让高亮区域"长胖",可以填补物体内部的小空洞、连接断裂的笔画。

函数原型:

cv2.dilate(img,kernel,iterations)

参数说明:

  • img:目标图片
  • kernel:进行操作的内核,默认 3×3
  • iterations:膨胀次数,默认 1

示例:对wenzi.png(文字图)膨胀 1 次,让文字笔画变粗。

wenzi=cv2.imread('wenzi.png')# 读取文字图像cv2.imshow('wenzi',wenzi)# 显示原始文字图像cv2.waitKey(0)kernel=np.ones((3,3),np.uint8)# 创建3x3的结构元素,用于膨胀操作wenzi_new=cv2.dilate(wenzi,kernel,iterations=1)# 膨胀操作,迭代1次cv2.imshow('wenzi_new',wenzi_new)# 显示膨胀后的图像cv2.waitKey(0)cv2.destroyAllWindows()

运行效果(左原图,右膨胀结果):文字笔画明显加粗。


4. 开运算与闭运算 cv2.morphologyEx

开运算和闭运算通过cv2.morphologyEx()一次完成,本质是腐蚀与膨胀的组合。

4.1 开运算(MORPH_OPEN)

开运算 = 先腐蚀后膨胀。

作用:平滑物体轮廓,断开较窄的狭颈,消除细小的突出物(白色小噪点)。常用于去噪点

zhiwen=cv2.imread('zhiwen.png')# 读取带噪点的指纹图像cv2.imshow('zhiwen',zhiwen)# 显示原始指纹图像cv2.waitKey(0)kernel=np.ones((3,3),np.uint8)# 创建3x3的结构元素zhiwen_new=cv2.morphologyEx(zhiwen,cv2.MORPH_OPEN,kernel)# 开运算:先腐蚀后膨胀,去除细小噪点cv2.imshow('zhiwen_new',zhiwen_new)# 显示开运算结果cv2.waitKey(0)cv2.destroyAllWindows()

运行效果(左原图,右开运算):指纹上的细小噪点被去除,主体保留。

4.2 闭运算(MORPH_CLOSE)

闭运算 = 先膨胀后腐蚀。

作用:弥合较窄的间断和细长的沟壑,消除小的孔洞,填补轮廓线。常用于填补断裂

zhiwen_duan=cv2.imread('zhiwen_duan.png')# 读取断裂的指纹图像cv2.imshow('zhiwen_duan',zhiwen_duan)# 显示原始断裂指纹cv2.waitKey(0)kernel=np.ones((3,3),np.uint8)# 创建3x3的结构元素zhiwen_new1=cv2.morphologyEx(zhiwen_duan,cv2.MORPH_CLOSE,kernel)# 闭运算:先膨胀后腐蚀,填补断裂cv2.imshow('zhiwen_new1',zhiwen_new1)# 显示闭运算结果cv2.waitKey(0)cv2.destroyAllWindows()

运行效果(左原图,右闭运算):指纹中的断裂被弥合,但是较大的断裂难以填补。

记忆技巧:开运算"打开"细小连接(去毛刺),闭运算"关闭"细小孔洞(补缝隙)。


5. 梯度运算(MORPH_GRADIENT)

梯度运算 = 膨胀 − 腐蚀。

作用:突出显示图像中强度变化剧烈的地方,常用于提取物体的边缘轮廓

wenzi=cv2.imread('wenzi.png')# 读取文字图像cv2.imshow('wenzi',wenzi)# 显示原始文字图像cv2.waitKey(0)kernel=np.ones((2,2),np.uint8)# 创建2x2的结构元素# 膨胀pz_wenzi=cv2.dilate(wenzi,kernel,iterations=1)# 膨胀操作,使文字笔画变粗cv2.imshow('pz_wenzi',pz_wenzi)# 显示膨胀结果cv2.waitKey(0)# 腐蚀fs_wenzi=cv2.erode(wenzi,kernel,iterations=1)# 腐蚀操作,使文字笔画变细cv2.imshow('fs_wenzi',fs_wenzi)# 显示腐蚀结果cv2.waitKey(0)# 膨胀 - 腐蚀gradient=cv2.morphologyEx(wenzi,cv2.MORPH_GRADIENT,kernel)# 梯度运算:膨胀减腐蚀,提取轮廓cv2.imshow('gradient',gradient)# 显示梯度运算结果cv2.waitKey(0)cv2.destroyAllWindows()

运行效果(从左到右从上到下依次为:原图 → 膨胀 → 腐蚀 → 梯度)。右下角即为提取出的文字轮廓。


6. 顶帽与黑帽

顶帽与黑帽用于提取比周围区域更亮或更暗的细节。

  • 顶帽(Top Hat)= 原始图像 − 开运算结果,用于提取比周围区域的细节
  • 黑帽(Black Hat)= 闭运算结果 − 原始图像,用于提取比周围区域的细节
sun=cv2.imread('sun.png')# 读取太阳图像cv2.imshow('sun',sun)# 显示原始图像cv2.waitKey(0)# 等待按键kernel=np.ones((2,2),np.uint8)# 创建2x2的结构元素# 开运算open_sun=cv2.morphologyEx(sun,cv2.MORPH_OPEN,kernel)# 开运算:先腐蚀后膨胀,去除细小噪点cv2.imshow('open_sun',open_sun)# 显示开运算结果cv2.waitKey(0)# 顶帽(两种等价写法)tophat=cv2.morphologyEx(sun,cv2.MORPH_TOPHAT,kernel)# 方法1:直接使用顶帽运算tophat1=sun-open_sun# 方法2:原图减去开运算结果,提取比周围亮的细节cv2.imshow('tophat',tophat)# 显示顶帽结果(方法1)cv2.imshow('tophat1',tophat1)# 显示顶帽结果(方法2)cv2.waitKey(0)# 闭运算close_sun=cv2.morphologyEx(sun,cv2.MORPH_CLOSE,kernel)# 闭运算:先膨胀后腐蚀,填补细小孔洞cv2.imshow('close_sun',close_sun)# 显示闭运算结果cv2.waitKey(0)# 黑帽(两种等价写法)blackhat=cv2.morphologyEx(sun,cv2.MORPH_BLACKHAT,kernel)# 方法1:直接使用黑帽运算blackhat1=close_sun-sun# 方法2:闭运算结果减去原图,提取比周围暗的细节cv2.imshow('blackhat',blackhat)# 显示黑帽结果(方法1)cv2.imshow('blackhat1',blackhat1)# 显示黑帽结果(方法2)cv2.waitKey(0)cv2.destroyAllWindows()

顶帽效果(依次为:原图 → 开运算 → 顶帽):顶帽突出了原图中比邻域亮的细小区域。

黑帽效果(依次为:原图 → 闭运算 → 黑帽):黑帽突出了原图中比邻域暗的细小区域。

顶帽/黑帽常用于非均匀光照背景下的目标提取:开运算估计背景,原图减去背景即可得到目标。


二、图像边缘检测

1. 什么是边缘检测

边缘检测是图像处理、计算机视觉和机器视觉中的基本工具,通常用于特征提取和特征检测,旨在检测数字图像中有明显变化的边缘或不连续的区域

边缘是图像中灰度发生剧烈变化的像素点的集合,本质是图像梯度的极值点。常用边缘检测算子:

  • Sobel 算子
  • Scharr 算子
  • Laplacian 算子
  • Canny 边缘检测

2. Sobel 算子 cv2.Sobel

Sobel 算子是一种离散的微分算子,结合了高斯平滑微分求导,利用局部差分寻找边缘,计算结果是梯度的近似值。

Sobel 算子包含 2 组 3×3 矩阵(横向和纵向模板),分别与图像卷积可得到 X 方向和 Y 方向的亮度差分近似值。

函数原型:

cv2.Sobel(src,ddepth,dx,dy,ksize,scale,delta,borderType)

参数说明:

  • src:输入图像
  • ddepth:输出图像深度,-1表示与原图相同
  • dx, dydx=1,dy=0求 X 方向一阶导数;dx=0,dy=1求 Y 方向一阶导数(不建议同时为 1
  • ksize:Sobel 算子大小,必须为 1、3、5 或 7,默认 3
⚠ 一个关键陷阱:负梯度会丢失

uint8无法表示负数。当从黑到白(左暗右亮)时,X 方向导数为正;从白到黑时为负。若ddepth=-1,负值会被截断为 0,导致只检测到一半边缘

解决方法:将ddepth设为cv2.CV_64F(float64,可保存负数),再用cv2.convertScaleAbs()取绝对值转回uint8

yuan=cv2.imread('yuan.png')# 读取示例图像cv2.imshow('yuan',yuan)# 显示原始图像cv2.waitKey(0)# x 方向上的边缘(uint8,右侧负数信息丢失)yuan_x=cv2.Sobel(yuan,-1,1,0)# 使用默认uint8类型,负梯度信息会丢失cv2.imshow('yuan_x',yuan_x)# 显示X方向Sobel结果(有信息丢失)cv2.waitKey(0)# x 方向上的边缘,包括负数信息,但显示不出来(范围 0~255)yuan_x_64=cv2.Sobel(yuan,cv2.CV_64F,1,0)# 使用float64类型保存负数信息cv2.imshow('yuan_x_64',yuan_x_64)# 显示float64结果(显示不正常,因为包含负数)cv2.waitKey(0)# 取绝对值,右端负值信息就能显示出来了yuan_x_full=cv2.convertScaleAbs(yuan_x_64)# 取绝对值并转换为uint8cv2.imshow('yuan_x_full',yuan_x_full)# 显示完整的X方向边缘cv2.waitKey(0)# y 方向上的边缘,同理处理yuan_y_64=cv2.Sobel(yuan,cv2.CV_64F,0,1)# Y方向Sobel,使用float64yuan_y_full=cv2.convertScaleAbs(yuan_y_64)# 取绝对值转换cv2.imshow('yuan_y_full',yuan_y_full)# 显示完整的Y方向边缘cv2.waitKey(0)# 同时使用 x,y 方向(不建议)yuan_xy=cv2.Sobel(yuan,-1,1,1)# 同时计算X和Y方向(不推荐,效果不佳)cv2.imshow('yuan_xy',yuan_xy)# 显示同时计算的结果cv2.waitKey(0)# 推荐做法:用加权运算组合 x 和 y 两个方向的结果yuan_xy_full=cv2.addWeighted(yuan_x_full,1,yuan_y_full,1,0)# 加权组合X和Y方向cv2.imshow('yuan_xy_full',yuan_xy_full)# 显示完整的边缘检测结果cv2.waitKey(0)cv2.destroyAllWindows()

X 方向边缘效果**(左原图,右 X 方向 Sobel):只检出竖直方向的边缘。

y 方向边缘效果**(左原图,右 y方向 Sobel):只检出横向方向的边缘。

X、Y 及组合效果(从左到右从上到下依次为:原图 → X 方向 → Y 方向 → X+Y 加权组合)。

用 Sobel 处理真实图像 Tom.jpg

img=cv2.imread('Tom.jpg')# 读取真实图像Tom.jpgcv2.imshow('Tom_yuantu',img)# 显示原始图像cv2.waitKey(0)img_x_64=cv2.Sobel(img,cv2.CV_64F,1,0)# X方向Sobel,使用float64保存负数img_x_full=cv2.convertScaleAbs(img_x_64)# 取绝对值转换为uint8img_y_64=cv2.Sobel(img,cv2.CV_64F,0,1)# Y方向Sobel,使用float64保存负数img_y_full=cv2.convertScaleAbs(img_y_64)# 取绝对值转换为uint8img_xy_full=cv2.addWeighted(img_x_full,1,img_y_full,1,0)# 加权组合X和Y方向结果cv2.imshow('img_xy_full',img_xy_full)# 显示完整的Sobel边缘检测结果cv2.waitKey(0)cv2.destroyAllWindows()

运行效果(左原图,右 Sobel 组合边缘)。


3. Scharr 算子 cv2.Scharr

Scharr 算子是 Sobel 算子在ksize=3时的优化版本,与 Sobel 速度相同但精度更高

Scharr 与 Sobel 的不同在于平滑部分:Scharr 中心元素权重更重,相当于使用较小标准差的高斯函数(更瘦高的模板),因此对细微边缘更敏感。

img=cv2.imread('Tom.jpg')# 读取Tom.jpg图像img_x_64=cv2.Scharr(img,cv2.CV_64F,1,0)# X方向Scharr算子,使用float64保存负数img_x_full=cv2.convertScaleAbs(img_x_64)# 取绝对值转换为uint8img_y_64=cv2.Scharr(img,cv2.CV_64F,0,1)# Y方向Scharr算子,使用float64保存负数img_y_full=cv2.convertScaleAbs(img_y_64)# 取绝对值转换为uint8img_xy_Scarr_full=cv2.addWeighted(img_y_full,1,img_x_full,1,0)# 加权组合X和Y方向cv2.imshow('img_xy_Scarr_full',img_xy_Scarr_full)# 显示Scharr边缘检测结果cv2.waitKey(0)

运行效果(左原图,右 Scharr 组合边缘):相比 Sobel,细节边缘更丰富。


4. Laplacian 算子 cv2.Laplacian

Laplacian(拉普拉斯)算子是二阶微分算子。它不再分别计算 X 和 Y 方向,而是以圆方向计算变化率,因此不需要 Gx+Gy 组合,能更敏感地检测到灰度突变点,但对噪声也较敏感,常配合高斯滤波使用。

函数原型:

cv2.Laplacian(src,ddepth,ksize,scale,delta,borderType)

参数说明:

Tom=cv2.imread('Tom.jpg',cv2.IMREAD_GRAYSCALE)# 转为灰度图,Laplacian算子通常在灰度图上效果更好Tom_lap=cv2.Laplacian(Tom,cv2.CV_64F,ksize=3)# 应用Laplacian算子,使用float64保存结果Tom_lap_full=cv2.convertScaleAbs(Tom_lap)# 取绝对值并转换为uint8类型,便于显示cv2.imshow('Tom_lap_full',Tom_lap_full)# 显示Laplacian边缘检测结果cv2.waitKey(0)

运行效果(左灰度原图,右 Laplacian 边缘)。


5. Canny 边缘检测 cv2.Canny

Canny 边缘检测是一种多阶段最优边缘检测算法,是工程中最常用的边缘检测方法。

Canny 的优点:

  • 低错误率:检测到的边缘尽可能都是真实边缘
  • 较好地定位边缘点:标记的边缘点与真实边缘中心尽量接近
  • 单一边缘响应:图像中的边缘只标记一次

Canny 算法分为 4 个步骤:

  • 图像降噪:用高斯滤波去除噪点,避免干扰
  • 梯度计算:用 Sobel 算子计算梯度幅值与方向
  • 非极大值抑制(NMS):在梯度方向上找最大值作为边缘,其余抑制为 0,细化粗边缘
  • 双阈值边界跟踪:设置高阈值fH和低阈值fL,高于fH的保留为强边缘,低于fL的丢弃,介于两者之间的只有与强边缘相连才保留

函数原型:

cv2.Canny(image,threshold1,threshold2,apertureSize,L2gradient)

参数说明:

  • image:输入图像
  • threshold1:第一个阈值(低阈值fL
  • threshold2:第二个阈值(高阈值fH
Tom=cv2.imread('Tom.jpg',cv2.IMREAD_GRAYSCALE)# 读取图像并转换为灰度图,Canny通常在灰度图上工作cv2.imshow('Tom',Tom)# 显示原始灰度图像cv2.waitKey(0)Tom_canny=cv2.Canny(Tom,100,200)# 应用Canny边缘检测,低阈值100,高阈值200cv2.imshow('Tom_canny',Tom_canny)# 显示Canny边缘检测结果cv2.waitKey(0)cv2.destroyAllWindows()

运行效果(左灰度原图,右 Canny 边缘):边缘连续、清晰、细线条,质量明显高于前三种算子。

阈值经验threshold2 : threshold1通常取2:1 ~ 3:1。低阈值过大会引入噪声,高阈值过小会产生断裂边缘。


三、四种边缘检测算子对比

算子阶数特点适用场景
Sobel一阶结合高斯平滑,抗噪一般通用,计算快
Scharr一阶Sobel 的 ksize=3 优化版,精度更高细小边缘、纹理
Laplacian二阶圆方向计算,对噪声敏感突变点检测
Canny多阶段降噪+梯度+NMS+双阈值,边缘最优工程首选

四、小结

  • 形态学面向"形状":腐蚀/膨胀是基础,开/闭运算用于去噪与补缝,梯度提取轮廓,顶帽/黑帽在非均匀光照下提取亮/暗细节。关键是合理选择结构元素的大小与迭代次数
  • 边缘检测面向"变化":一阶算子(Sobel/Scharr)需注意CV_64F+convertScaleAbs处理负梯度,二阶算子(Laplacian)对噪声敏感,Canny 是综合最优方案。关键是合理选择阈值

掌握这两部分,就为后续的轮廓检测(cv2.findContours)、模板匹配、特征点检测(Harris/SIFT)等高阶应用打下了坚实基础。