1. 引言
形态学变换(Morphological Transformations) 是数字图像处理中的一组重要操作,主要用于图像预处理、特征提取和对象识别等领域。它是一种基于形状的简单变换,处理对象通常是二值化图像。
形态学变换有两个输入(原图像、核(结构化元素)),一个输出(形态学变换后的图像)。其基本操作有腐蚀和膨胀,这两种操作是相反的,即较亮的像素会被腐蚀和膨胀。下面我们来说一下核、腐蚀与膨胀的概念。 本教案将详细介绍形态学变换的核心概念和常用操作。
2. 核(Kernel)
2.1 概念
核(也称为结构元素)是形态学操作的基本工具,通常是一个小的奇数矩阵或形状(如矩形、椭圆形、十字形等如下图所示。通过不同的结构可以对不同特征的图像进行形态学操作(从左上角向右下角运算的处理。 )),用于探测图像中的特定结构。
2.2 核的性质
-
大小:通常为3×3、5×5等奇数尺寸(因为要重新赋值给最中间的像素值‘锚点’,一定要是奇数)
-
形状:矩形、椭圆形、十字形等
-
锚点:核的中心点,默认为几何中心
2.3 代码示例
import cv2
import numpy as np
# 创建矩形核
kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
# 创建椭圆形核
kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
# 创建十字形核
kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5,5))
3. 腐蚀(Erosion)
3.1 基本概念
腐蚀操作会"侵蚀"物体的边界,使得物体尺寸变小,可以消除小的噪声点。
在图像处理的形态学变换中,腐蚀(Erosion)的操作逻辑可以直观理解为“用邻域内像素的最小值替代中心像素值”,但这种描述需要结合二值图像和灰度图像的具体场景来区分。
3.2 操作原理
3.2.1. 二值图像(黑白图像)
-
定义:图像中像素值仅为
0
(黑,背景)或1
(白,前景)。(就是一个全黑的图上有白色物体) -
腐蚀操作:
用结构元素(核)扫描图像,只有当核覆盖的所有像素都为1
时,中心像素才保持为1
,否则变为0
。-
效果:前景(白色)区域被“缩小”,边缘被侵蚀,孤立噪声点被消除。
-
类比:确实可以理解为“用邻域内的最小值(
0
)替代中心像素值”,因为只要有一个0
,中心就会变成0
。
-
示例
原始二值矩阵(1
为前景):
用核扫描图像的每一个像素,只有当核覆盖的所有像素都为1时,中心像素才保持为1,否则变为0。
腐蚀后(若核为3×3)中心像素变为 0
(因为邻域中存在 0
)。
3.2.2. 灰度图像
-
定义:像素值为连续的灰度值(如
0~255
)。 -
腐蚀操作:
用核覆盖邻域,取邻域内像素的最小值作为中心像素的新值。-
效果:暗区域(低像素值)扩大,亮区域(高像素值)缩小。
-
数学表达:
-
示例
原始灰度矩阵(假设核为3×3):
腐蚀后中心像素值 = min(80, 150, 90, 70, 100, 130, ...) = 70
。
3.2.3. 对比膨胀(Dilation)
-
膨胀是腐蚀的逆操作:
-
二值图像:邻域中有一个
1
则中心变为1
。 -
灰度图像:取邻域最大值,即“用邻域内高像素值替代中心低像素值”。
-
3.3 应用场景
-
消除小噪声
-
分离相连物体
-
边缘检测预处理
3.4 代码实现
import cv2
import numpy as np
# 灰度图像示例
gray_image = np.array([[100, 120, 110], [80, 150, 90], [70, 100, 130]], dtype=np.uint8)
kernel = np.ones((3,3), np.uint8)
eroded_gray = cv2.erode(gray_image, kernel)
print("灰度腐蚀结果:\n", eroded_gray) # 中心值变为70
# 二值图像示例
binary_image = np.array([[1, 1, 1], [1, 1, 1], [1, 0, 1]], dtype=np.uint8)
eroded_binary = cv2.erode(binary_image, kernel)
print("二值腐蚀结果:\n", eroded_binary) # 中心值变为0
总结
-
腐蚀的本质:用邻域内最小像素值替代中心像素值。
-
二值图像:表现为“前景缩小”,逻辑是“与运算”。
-
灰度图像:直接表现为“暗区扩张”,数学上是最小值滤波。
4. 膨胀(Dilation)
4.1 基本概念
膨胀操作会"扩张"物体的边界,使得物体尺寸变大,可以填补物体中的空洞。
在形态学变换中,膨胀的操作逻辑可直观理解为“用邻域内像素的最大值替代中心像素值”,具体表现需区分二值图像和灰度图像。
4.2 操作原理
4.2.1 二值图像(黑白图像)
定义:像素值仅为 0(黑,背景)或 1(白,前景)。
膨胀操作:
用结构元素(核)扫描图像,只要核覆盖的邻域内至少有一个像素为 1,中心像素即变为 1,否则保持为 0。
效果:
-
前景(白色)区域向外扩展,边缘被扩张。
-
断裂部分可能被连接,小孔洞被填充。
类比:
可理解为“用邻域内的最大值(1)替代中心像素值”,因为只要有一个 1,中心就会变成 1。
示例:
原始二值矩阵(核为3×3):
膨胀后中心像素变为 1(邻域中存在 1)。
4.2.2 灰度图像
定义:像素值为连续灰度值(如 0~255)。
膨胀操作:
用核覆盖邻域,取邻域内像素的最大值作为中心像素的新值。
效果:
-
亮区域(高像素值)向外扩张,暗区域(低像素值)被压缩。
数学表达:
示例:
原始灰度矩阵(核为3×3):
膨胀后中心像素值 = max(70, 100, 130, 60, 110, 50, ...) = 130。
4.2.3 对比腐蚀(Erosion)
-
腐蚀:取邻域最小值,导致物体缩小。
-
膨胀:取邻域最大值,导致物体扩大。
4.3 应用场景
-
填补空洞或裂缝(如断裂的文字笔画修复)。
-
连接相邻物体(如分离的细胞图像合并)。
-
边缘增强(先腐蚀后膨胀可突出边界)。
4.4 代码实现
import cv2
import numpy as np
# 灰度图像示例
gray_image = np.array([[80, 120, 90], [70, 100, 130], [60, 110, 50]], dtype=np.uint8)
kernel = np.ones((3,3), np.uint8)
dilated_gray = cv2.dilate(gray_image, kernel)
print("灰度膨胀结果:\n", dilated_gray) # 中心值变为130
# 二值图像示例
binary_image = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], dtype=np.uint8)
dilated_binary = cv2.dilate(binary_image, kernel)
print("二值膨胀结果:\n", dilated_binary) # 中心值变为1
总结
-
本质:用邻域内最大像素值替代中心像素值。
-
二值图像:表现为“前景扩大”,逻辑是“或运算”。
-
灰度图像:直接表现为“亮区扩张”,数学上是最大值滤波。
5. 开运算(Opening)
5.1 基本概念
开运算是先腐蚀后膨胀的过程,可以消除小的物体或噪声,而不影响原来的图像 。
5.2 数学表达式
开运算 = 膨胀(腐蚀(图像))
5.3 应用场景
-
去除小噪声点(如二值图像中的白点噪声)。
-
平滑物体边界(如分割后的不规则物体)。
-
断开细连接(如OCR中粘连字符的分离)。
5.4 代码实现
5.4.1. 基于 OpenCV 的实现(推荐)
import cv2
import numpy as np
def opening_with_opencv(image, kernel_size=(3, 3)):
"""
使用 OpenCV 实现开运算
:param image: 输入图像(二值或灰度)
:param kernel_size: 结构元素大小,如 (3,3)
:return: 开运算结果
"""
kernel = np.ones(kernel_size, np.uint8) # 矩形结构元素
eroded = cv2.erode(image, kernel) # 1. 先腐蚀
opened = cv2.dilate(eroded, kernel) # 2. 再膨胀
return opened
# 示例用法
if __name__ == "__main__":
# 二值图像示例
binary_image = np.array([
[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 0, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 0, 0]
], dtype=np.uint8) * 255 # 转换为0/255格式
opened_binary = opening_with_opencv(binary_image)
print("二值开运算结果:\n", opened_binary // 255) # 还原为0/1
# 灰度图像示例
gray_image = np.array([
[100, 200, 100],
[200, 50, 200],
[100, 200, 100]
], dtype=np.uint8)
opened_gray = opening_with_opencv(gray_image)
print("灰度开运算结果:\n", opened_gray)
5.4.2 纯 NumPy 实现(无 OpenCV 依赖)
import numpy as np
def erosion(image, kernel):
"""腐蚀操作"""
h, w = image.shape
kh, kw = kernel.shape
pad_h, pad_w = kh // 2, kw // 2
padded = np.pad(image, ((pad_h, pad_h), (pad_w, pad_w)), mode='constant')
result = np.zeros_like(image)
for i in range(h):
for j in range(w):
region = padded[i:i+kh, j:j+kw]
result[i, j] = np.min(region * kernel) # 对二值/灰度均适用
return result
def dilation(image, kernel):
"""膨胀操作"""
h, w = image.shape
kh, kw = kernel.shape
pad_h, pad_w = kh // 2, kw // 2
padded = np.pad(image, ((pad_h, pad_h), (pad_w, pad_w)), mode='constant')
result = np.zeros_like(image)
for i in range(h):
for j in range(w):
region = padded[i:i+kh, j:j+kw]
result[i, j] = np.max(region * kernel)
return result
def opening_with_numpy(image, kernel_size=(3, 3)):
"""纯NumPy实现开运算"""
kernel = np.ones(kernel_size, dtype=np.uint8)
eroded = erosion(image, kernel)
opened = dilation(eroded, kernel)
return opened
# 示例用法
if __name__ == "__main__":
binary_image = np.array([
[0, 1, 0],
[1, 0, 1],
[0, 1, 0]
], dtype=np.uint8)
opened_binary = opening_with_numpy(binary_image)
print("二值开运算结果(NumPy):\n", opened_binary)
可以通过自定义 kernel
控制开运算的效果:
# 使用十字形结构元素
cross_kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
opened_cross = cv2.morphologyEx(image, cv2.MORPH_OPEN, cross_kernel)
总结
-
OpenCV 实现:直接使用
cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel)
更高效。 -
手动实现:理解腐蚀和膨胀的逐像素操作逻辑。
-
开运算 = 腐蚀 + 膨胀,适合去噪且保留主体形状。
6. 闭运算(Closing)
6.1 基本概念
闭运算是先膨胀后腐蚀的过程,可以填补物体中的小洞。
6.2 数学表达式
闭运算 = 腐蚀(膨胀(图像))
6.3 应用场景
-
填充孔洞(如二值图像中的黑色小孔)。
-
连接断裂部分(如OCR中笔画断裂的修复)。
-
平滑亮区域边界(如医学图像中的细胞轮廓)。
6.4 代码实现
6.4.1 基于 OpenCV 的实现(推荐)
import cv2
import numpy as np
def closing_with_opencv(image, kernel_size=(3, 3)):
"""
使用 OpenCV 实现闭运算
:param image: 输入图像(二值或灰度)
:param kernel_size: 结构元素大小,如 (3,3)
:return: 闭运算结果
"""
kernel = np.ones(kernel_size, np.uint8) # 矩形结构元素
dilated = cv2.dilate(image, kernel) # 1. 先膨胀
closed = cv2.erode(dilated, kernel) # 2. 再腐蚀
return closed
# 示例用法
if __name__ == "__main__":
# 二值图像示例(带孔洞)
binary_image = np.array([
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]
], dtype=np.uint8) * 255 # 转换为0/255格式
closed_binary = closing_with_opencv(binary_image)
print("二值闭运算结果:\n", closed_binary // 255) # 还原为0/1
# 灰度图像示例(暗区域断裂)
gray_image = np.array([
[200, 200, 200],
[200, 50, 200],
[200, 200, 200]
], dtype=np.uint8)
closed_gray = closing_with_opencv(gray_image)
print("灰度闭运算结果:\n", closed_gray)
6.4.2. 纯 NumPy 实现(无 OpenCV 依赖
import numpy as np
def dilation(image, kernel):
"""膨胀操作(与开运算示例相同)"""
h, w = image.shape
kh, kw = kernel.shape
pad_h, pad_w = kh // 2, kw // 2
padded = np.pad(image, ((pad_h, pad_h), (pad_w, pad_w)), mode='constant')
result = np.zeros_like(image)
for i in range(h):
for j in range(w):
region = padded[i:i+kh, j:j+kw]
result[i, j] = np.max(region * kernel)
return result
def erosion(image, kernel):
"""腐蚀操作(与开运算示例相同)"""
h, w = image.shape
kh, kw = kernel.shape
pad_h, pad_w = kh // 2, kw // 2
padded = np.pad(image, ((pad_h, pad_h), (pad_w, pad_w)), mode='constant')
result = np.zeros_like(image)
for i in range(h):
for j in range(w):
region = padded[i:i+kh, j:j+kw]
result[i, j] = np.min(region * kernel)
return result
def closing_with_numpy(image, kernel_size=(3, 3)):
"""纯NumPy实现闭运算"""
kernel = np.ones(kernel_size, dtype=np.uint8)
dilated = dilation(image, kernel) # 先膨胀
closed = erosion(dilated, kernel) # 后腐蚀
return closed
# 示例用法
if __name__ == "__main__":
# 二值图像(孔洞填充)
binary_image = np.array([
[1, 1, 1],
[1, 0, 1],
[1, 1, 1]
], dtype=np.uint8)
closed_binary = closing_with_numpy(binary_image)
print("二值闭运算结果(NumPy):\n", closed_binary)
可通过自定义 kernel
调整闭运算效果:
# 使用椭圆形结构元素
ellipse_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
closed_ellipse = cv2.morphologyEx(image, cv2.MORPH_CLOSE, ellipse_kernel)
总结
-
OpenCV 实现:直接调用
cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel)
更高效。 -
手动实现:需先膨胀后腐蚀,顺序不可颠倒。
-
闭运算 = 膨胀 + 腐蚀,适合补洞且保持主体形状。
7. 礼帽运算(Top Hat)
7.1 基本概念
礼帽运算是原图像与开运算结果的差,可以突出比周围亮的区域。
7.2 数学表达式
礼帽 = 原图 - 开运算
7.3 应用场景
-
背景校正
-
提取比背景亮的细小物体(如白噪声、亮斑、毛发)。
-
增强局部对比度(如显微图像中的细胞细节)。
直观理解
-
开运算会消除亮的小物体,因此用原图减去开运算结果,相当于“找回”被开运算删除的亮区域。
-
类似“从背景中筛出亮杂质”。
示例场景
-
检测PCB板上的白色焊点缺陷(背景为暗色)。
-
增强指纹图像中的脊线细节。
7.4 代码实现
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel_rect)
8. 黑帽运算(Black Hat)
8.1 基本概念
黑帽运算是闭运算结果与原图像的差,可以突出比周围暗的区域。
8.2 数学表达式
黑帽 = 闭运算 - 原图
8.3 应用场景
-
提取比背景暗的细小物体(如孔洞、裂纹、暗斑)。
-
检测阴影或凹陷区域(如金属表面的划痕)。
-
增强局部对比度
直观理解
-
闭运算会填充暗的小孔洞,因此用闭运算结果减去原图,相当于“找回”被闭运算填充的暗区域。
-
类似“从背景中挖出暗缺陷”。
示例场景
-
检测轮胎图像中的裂纹(暗色)。
-
识别医学图像中的血管或腔隙(暗区域)。
8.4 代码实现
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel_rect)
小贴士. 记忆技巧
-
礼帽 = 亮礼帽:像一顶白色礼帽,提取“亮”的东西。
-
黑帽 = 黑洞:像黑色深渊,提取“暗”的东西。
-
开运算删亮留暗 → 礼帽找回亮的
闭运算填暗留亮 → 黑帽找回暗的
import cv2
import numpy as np
# 生成示例图像(亮斑+暗洞)
image = np.zeros((100, 100), dtype=np.uint8)
image[20:30, 20:30] = 255 # 亮方块(模拟亮斑)
image[60:70, 60:70] = 50 # 暗方块(模拟孔洞)
kernel = np.ones((15,15), np.uint8)
# 礼帽和黑帽运算
tophat = cv2.morphologyEx(image, cv2.MORPH_TOPHAT, kernel)
blackhat = cv2.morphologyEx(image, cv2.MORPH_BLACKHAT, kernel)
# 可视化
cv2.imshow("Original", image)
cv2.imshow("Top-Hat (Bright Spots)", tophat)
cv2.imshow("Black-Hat (Dark Holes)", blackhat)
cv2.waitKey(0)
9. 形态学梯度(Morphological Gradient)
9.1 基本概念
形态学梯度是膨胀图像与腐蚀图像的差,可以得到物体的边界,并且对噪声有一定的抑制作用 。
9.2 数学表达式
梯度 = 膨胀 - 腐蚀
直观理解
-
膨胀:将物体边缘向外扩展(亮区域变大)。
-
腐蚀:将物体边缘向内收缩(亮区域变小)。
-
梯度:两者的差值即为边缘区域,即物体内外边界的变化部分。
类比:
想象用粉笔画一个圆圈:
-
膨胀:用粗笔重描一圈,线条变粗。
-
腐蚀:用橡皮擦掉外圈,线条变细。
-
梯度:两者差异部分就是原始的“线条边缘”。
9.3 数学意义
-
对于二值图像:梯度提取的是物体的轮廓像素(即前景与背景的交界处)。
-
对于灰度图像:梯度反映的是局部灰度变化率(类似Sobel算子,但更强调结构)。
9.4 典型应用
-
目标轮廓提取
-
在二值图像中快速获取物体的外边界。
-
-
医学图像边缘增强
-
突出器官或细胞的边界(如X光片中的骨骼轮廓)。
-
-
工业检测
-
检测零件边缘缺陷(如裂纹或毛刺)。
-
9.4 代码实现
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel_rect)
总结
形态学梯度的核心思想是:
通过膨胀与腐蚀的差异,直接量化局部区域的“扩张程度”,从而突出边界。它的优势在于计算简单、抗噪性好,尤其适合需要快速提取结构化边缘的场景。
10. 综合应用示例
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像
img = cv2.imread('example.jpg', 0)
# 创建核
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
# 应用各种形态学操作
erosion = cv2.erode(img, kernel, iterations=1)
dilation = cv2.dilate(img, kernel, iterations=1)
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
# 显示结果
titles = ['Original', 'Erosion', 'Dilation', 'Opening', 'Closing', 'Tophat', 'Blackhat','Gradient']
images = [img, erosion, dilation, opening, closing, tophat, blackhat, gradient]
plt.figure(figsize=(15,10))
for i in range(8):
plt.subplot(2,4,i+1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
11. 总结
形态学变换是图像处理中强大的工具,通过不同的组合可以实现多种图像处理效果:
-
腐蚀和膨胀是基础操作
-
开运算(先腐蚀后膨胀)和闭运算(先膨胀后腐蚀)是腐蚀和膨胀的组合
-
礼帽(亮:原图-开运算)和黑帽(暗:闭运算-原图)运算用于特定特征的提取
-
形态学梯度(边缘信息:膨胀-腐蚀)用于边缘检测
在实际应用中,需要根据具体问题选择合适的操作和核的形状、大小,并通过调整迭代次数来获得最佳效果。
12. 课后练习
-
尝试不同的核形状和大小,观察对形态学操作结果的影响
-
组合使用多种形态学操作,解决特定的图像处理问题
-
比较形态学梯度与传统边缘检测算法(如Sobel、Canny)的效果差异
-
设计一个流程,使用形态学操作从复杂背景中提取特定形状的物体