

形态学图像处理是数字图像处理中基于形状的图像处理方法,核心是利用结构元素对图像进行操作,广泛应用于图像分割、边缘检测、噪声去除、特征提取等场景。本文结合《数字图像处理》第 9 章内容,从基础概念到实战代码,全方位讲解形态学图像处理,所有代码均可直接运行,附带效果对比图,帮你快速掌握核心知识点。
本文使用 Python 实现所有案例,依赖库如下:
# 安装依赖
pip install numpy opencv-python matplotlib先定义通用的图像读取、显示、预处理函数,后续案例直接调用:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_image(path, is_gray=True):
"""
读取图像
:param path: 图像路径(相对/绝对路径均可)
:param is_gray: 是否转为灰度图
:return: 处理后的图像;若读取失败返回None
"""
# 读取图像,添加异常处理避免路径错误导致崩溃
img = cv2.imread(path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{path}")
return None
if is_gray:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return img
def preprocess_image(img, threshold=127):
"""
灰度图转二值图(阈值分割)
:param img: 灰度图像
:param threshold: 阈值(0-255)
:return: 二值图像
"""
_, binary_img = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)
return binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
# 测试通用函数(读取自定义图片)
if __name__ == "__main__":
img_path = "../picture/JinXi.png" # 请修改为你的图片实际路径
# 1. 读取灰度图
gray_img = read_image(img_path, is_gray=True)
if gray_img is None:
exit() # 读取失败则退出
# 2. 预处理为二值图(可调整阈值适配你的图片)
binary_img = preprocess_image(gray_img, threshold=127)
# 3. 显示原图(灰度)和二值图对比
show_images([gray_img, binary_img],
["自定义灰度图", "自定义二值图"],
rows=1, cols=2, figsize=(12, 6))
腐蚀和膨胀是形态学最基础的两个操作,所有复杂形态学运算均基于此。
腐蚀是收缩 / 细化前景区域的操作:用结构元素扫描图像,只有当结构元素的所有像素都与图像中的前景像素重合时,原点位置才保留为前景,否则置为背景。

# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def erosion_demo(img, kernel_size=(3, 3), iterations=1):
"""
腐蚀操作
:param img: 二值图像
:param kernel_size: 结构元素大小
:param iterations: 迭代次数(腐蚀次数)
:return: 腐蚀后的图像
"""
# 定义结构元素(矩形),也可改为十字形(cv2.MORPH_CROSS)、椭圆形(cv2.MORPH_ELLIPSE)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, kernel_size)
# 腐蚀操作
eroded_img = cv2.erode(img, kernel, iterations=iterations)
return eroded_img
# 测试腐蚀(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/KanTeLeiLa.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 对二值图执行腐蚀操作
eroded_img = erosion_demo(binary_img, kernel_size=(3, 3), iterations=1)
# 显示对比:原始灰度图 → 二值图 → 腐蚀后二值图
show_images([gray_img, binary_img, eroded_img],
["原始灰度图像", "二值化图像", "腐蚀后图像"],
rows=1, cols=3, figsize=(15, 5))
腐蚀会消除小的亮噪声点,同时让前景区域边缘收缩;迭代次数越多,收缩效果越明显。
膨胀是扩张 / 加粗前景区域的操作:用结构元素扫描图像,只要结构元素有一个像素与图像中的前景像素重合,原点位置就置为前景。

# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def dilation_demo(img, kernel_size=(3, 3), iterations=1):
"""
膨胀操作
:param img: 二值图像
:param kernel_size: 结构元素大小
:param iterations: 迭代次数
:return: 膨胀后的图像
"""
# 定义结构元素(矩形),也可改为十字形(cv2.MORPH_CROSS)、椭圆形(cv2.MORPH_ELLIPSE)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, kernel_size)
dilated_img = cv2.dilate(img, kernel, iterations=iterations)
return dilated_img
# 测试膨胀(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/ShouAnRen.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 对二值图执行膨胀操作
dilated_img = dilation_demo(binary_img, kernel_size=(3, 3), iterations=1)
# 显示对比:原始灰度图 → 二值图 → 膨胀后二值图
show_images([gray_img, binary_img, dilated_img],
["原始灰度图像", "二值化图像", "膨胀后图像"],
rows=1, cols=3, figsize=(15, 5))
膨胀会填补前景区域的小缺口 / 孔洞,同时让前景边缘扩张;常用来修复腐蚀造成的前景收缩。
腐蚀和膨胀是对偶操作:对前景的腐蚀 = 对背景的膨胀,对前景的膨胀 = 对背景的腐蚀。

# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def duality_demo(img):
"""
验证腐蚀与膨胀的对偶性
原理:(原图腐蚀)的补集 = 原图补集的膨胀
"""
# 定义结构元素(矩形),也可改为十字形/椭圆形验证
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# 1. 对原图腐蚀,再取补集
eroded = cv2.erode(img, kernel)
eroded_complement = 255 - eroded
# 2. 对原图补集膨胀
img_complement = 255 - img
dilated_complement = cv2.dilate(img_complement, kernel)
# 额外:计算两张图的差异(验证是否完全一致)
diff = cv2.absdiff(eroded_complement, dilated_complement)
diff_sum = np.sum(diff)
print(f"\n对偶性验证结果:两张图的像素差异总和 = {diff_sum}")
print("若差异总和为0,说明(原图腐蚀)的补集 与 原图补集的膨胀 完全一致,对偶性成立!")
# 显示对比:原始二值图 + 两组验证图
# 第一步:显示原始二值图
show_images([img], ["自定义二值图像(原始)"], rows=1, cols=1, figsize=(6, 6))
# 第二步:显示对偶性验证对比图
show_images([eroded_complement, dilated_complement],
["(原图腐蚀)的补集", "原图补集的膨胀"],
figsize=(10, 5))
# 测试对偶性(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/AoGuSiTa.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 调用对偶性验证函数
duality_demo(binary_img)

开运算和闭运算是腐蚀 + 膨胀的组合操作,用于噪声去除、形状修复。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def open_close_demo(img):
"""
开运算与闭运算演示
开运算:先腐蚀后膨胀 → 去除亮噪声、平滑边缘
闭运算:先膨胀后腐蚀 → 填补暗孔洞、平滑边缘
"""
# 定义结构元素(5×5矩形,适合处理中等大小的噪声/孔洞)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# 开运算:去除亮噪声(前景中的小亮点)
open_img = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
# 闭运算:填补暗孔洞(前景中的小黑洞)
close_img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
# 显示对比:原始二值图 → 开运算 → 闭运算
show_images([img, open_img, close_img],
["原始二值图像", "开运算(去除亮噪声)", "闭运算(填补暗孔洞)"],
rows=1, cols=3, figsize=(18, 6))
# 测试开运算与闭运算(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/LinNai.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图的对比
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 6))
# 调用开运算与闭运算演示函数
open_close_demo(binary_img)

击中 - 击不中变换(Hit-or-Miss)是形态学中用于精确匹配特定形状 / 结构的操作,核心是同时匹配前景和背景的局部模式。

# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def hit_or_miss_demo(custom_img):
"""
击中-击不中变换:检测特定形状(默认检测十字形)
原理:同时匹配前景和背景的局部模式,精准定位目标形状
"""
# 1. 定义要检测的形状对应的结构元素(十字形,可根据需求修改)
# 可选:矩形(cv2.MORPH_RECT)、椭圆形(cv2.MORPH_ELLIPSE)
detect_shape = cv2.MORPH_CROSS # 检测十字形
kernel = cv2.getStructuringElement(detect_shape, (7, 7)) # 7×7十字形,适配自定义图像尺度
# 2. 执行击中-击不中变换
hit_miss_img = cv2.morphologyEx(custom_img, cv2.MORPH_HITMISS, kernel)
# 3. 标记检测到的形状位置(用灰色圆点标记中心)
result_img = custom_img.copy()
coords = np.where(hit_miss_img == 255) # 获取检测到的像素坐标
if len(coords[0]) > 0:
for y, x in zip(coords[0], coords[1]):
cv2.circle(result_img, (x, y), 4, 127, -1) # 灰色实心圆标记
print(f"\n检测结果:共找到 {len(coords[0])} 个匹配的十字形区域!")
else:
print("\n检测结果:未找到匹配的十字形区域(可调整结构元素大小/形状重试)!")
# 4. 显示对比:原始二值图 → 击中-击不中结果 → 标记后的图像
show_images([custom_img, hit_miss_img, result_img],
["自定义二值图像", "击中-击不中变换结果", "标记检测到的形状位置"],
rows=1, cols=3, figsize=(18, 6))
# 测试击中-击不中变换(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/QianXiao.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 6))
# 调用击中-击不中变换演示函数
hit_or_miss_demo(binary_img)

边界提取 = 原始图像 - 腐蚀后的图像,用于提取前景区域的边缘。

# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def boundary_extraction_demo(img):
"""
边界提取:基于“原始图 - 腐蚀图”实现
原理:腐蚀会收缩前景区域,两者相减即可得到前景的边缘(单像素宽度)
"""
# 定义结构元素(3×3矩形,适合提取精细边界;增大尺寸可提取更粗的边界)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# 对图像进行腐蚀操作
eroded = cv2.erode(img, kernel)
# 原始图 - 腐蚀图 = 边界(单像素宽度)
boundary = cv2.subtract(img, eroded) # 用cv2.subtract避免数值溢出,比直接减更稳定
# 显示对比:原始二值图 → 腐蚀后的图像 → 提取的边界
show_images([img, eroded, boundary],
["自定义二值图像", "腐蚀后的图像", "边界提取结果(单像素)"],
rows=1, cols=3, figsize=(18, 6))
# 测试边界提取(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/YouNuo.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图的对比
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 6))
# 调用边界提取演示函数
boundary_extraction_demo(binary_img)

孔洞是被前景包围的背景区域,填充思路:从孔洞内部的种子点开始,不断膨胀,直到接触到前景边界。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def find_hole_seed(img):
"""
自动查找孔洞内的种子点(适配自定义图像,替代固定中心种子点)
:param img: 二值图像(前景255,背景0)
:return: 种子点坐标(x,y);无孔洞则返回图像中心
"""
# 1. 查找图像的外轮廓(前景区域)
contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
# 无前景区域,返回中心
h, w = img.shape
return (w // 2, h // 2)
# 2. 创建掩码,标记前景外部区域
mask = np.zeros_like(img)
cv2.drawContours(mask, contours, -1, 255, -1)
mask_inv = 255 - mask # 前景内部的背景(可能包含孔洞)
# 3. 查找孔洞区域的连通分量,取第一个孔洞的中心作为种子点
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask_inv, connectivity=8)
if num_labels > 1:
# 第一个标签是背景,第二个开始是孔洞
seed_x = int(centroids[1][0])
seed_y = int(centroids[1][1])
print(f"\n自动找到孔洞种子点:({seed_x}, {seed_y})")
return (seed_x, seed_y)
else:
# 无明显孔洞,返回图像中心
h, w = img.shape
print("\n未检测到明显孔洞,使用图像中心作为种子点")
return (w // 2, h // 2)
def hole_filling_demo(img):
"""
孔洞填充:基于形态学膨胀+种子填充实现
原理:从种子点开始迭代膨胀,被前景边界约束,最终填充前景内部的孔洞
"""
# 1. 初始化填充图像(与原图大小一致,初始全0)
filled_img = np.zeros_like(img)
# 2. 自动查找孔洞种子点(适配自定义图像,替代固定中心)
seed = find_hole_seed(img)
filled_img[seed[1], seed[0]] = 255 # 种子点设为前景
# 3. 定义结构元素(3×3矩形,保证填充的连续性)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# 4. 迭代膨胀,直到不再变化(填充孔洞)
iter_count = 0
while True:
prev_filled = filled_img.copy()
# 膨胀操作 → 与原图补集取交集(避免超出前景边界)
filled_img = cv2.dilate(filled_img, kernel)
filled_img = cv2.bitwise_and(filled_img, 255 - img)
iter_count += 1
# 终止条件:填充结果不再变化
if np.array_equal(filled_img, prev_filled):
print(f"孔洞填充完成,迭代次数:{iter_count}")
break
# 5. 合并填充结果与原图(得到无孔洞的前景)
result = cv2.bitwise_or(img, filled_img)
# 6. 显示对比:原始二值图 → 填充过程中的种子膨胀图 → 最终填充结果
show_images([img, filled_img, result],
["自定义二值图像(含孔洞)", "种子膨胀填充过程", "孔洞填充最终结果"],
rows=1, cols=3, figsize=(18, 6))
# 测试孔洞填充(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/KaTiXiYa.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图的对比
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 6))
# 调用孔洞填充演示函数
hole_filling_demo(binary_img)

连通区域是像素值相同且相邻的像素集合,形态学方法通过标记 + 膨胀实现提取,OpenCV 已封装现成函数。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def connected_components_demo(img):
"""
连通区域提取与标记
原理:基于8连通/4连通规则,将相邻的前景像素归为同一区域,自动标记并统计
"""
# 1. 连通区域分析(8连通,更贴合实际场景;4连通可改为connectivity=4)
# num_labels:总标签数(含背景);labels:每个像素的标签;stats:区域统计信息;centroids:区域中心
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(img, connectivity=8)
# 2. 输出连通区域统计信息(背景为标签0,前景从标签1开始)
print(f"\n连通区域分析结果:")
print(f"- 总标签数(含背景):{num_labels}")
print(f"- 前景连通区域数量:{num_labels - 1}")
for i in range(1, num_labels):
x, y, w, h, area = stats[i]
cx, cy = centroids[i]
print(f" 区域{i}:位置({x}, {y}),大小{w}×{h},面积{area},中心({cx:.1f}, {cy:.1f})")
# 3. 生成彩色标记图(方便可视化,背景为黑色,前景区域随机上色)
# 固定随机种子,保证每次运行颜色一致
np.random.seed(42)
colors = np.random.randint(0, 255, (num_labels, 3), dtype=np.uint8)
colors[0] = [0, 0, 0] # 背景标签0设为黑色
labeled_img = colors[labels] # 根据标签映射颜色
# 4. 绘制每个区域的中心和编号(增强可视化效果)
labeled_with_text = labeled_img.copy()
for i in range(1, num_labels):
cx, cy = centroids[i]
# 在区域中心绘制编号
cv2.putText(labeled_with_text, str(i), (int(cx) - 10, int(cy) + 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
# 5. 显示结果:原始二值图 → 彩色标记图 → 带编号的标记图
plt.figure(figsize=(18, 6))
# 子图1:原始二值图
plt.subplot(131)
plt.imshow(img, cmap='gray')
plt.title(f"原始二值图像({num_labels - 1}个前景连通区域)")
plt.axis('off')
# 子图2:彩色标记图
plt.subplot(132)
plt.imshow(labeled_img)
plt.title("连通区域彩色标记")
plt.axis('off')
# 子图3:带编号的标记图
plt.subplot(133)
plt.imshow(labeled_with_text)
plt.title("连通区域(带编号)")
plt.axis('off')
plt.tight_layout()
plt.show()
# 测试连通区域提取(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/AALi.jpg" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图的对比
plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.imshow(gray_img, cmap='gray')
plt.title("原始灰度图像")
plt.axis('off')
plt.subplot(122)
plt.imshow(binary_img, cmap='gray')
plt.title("二值化后的图像")
plt.axis('off')
plt.tight_layout()
plt.show()
# 调用连通区域提取演示函数
connected_components_demo(binary_img)
凸包是包含目标区域的最小凸多边形,OpenCV 通过cv2.convexHull实现。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def convex_hull_demo(img):
"""
凸包提取:找到能包裹所有轮廓点的最小凸多边形
原理:对于凹形轮廓,凸包会忽略凹陷部分,生成最外层的凸边界
"""
# 1. 查找图像的外部轮廓(只提取最外层轮廓)
contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
print("错误:未检测到任何轮廓,请检查二值化效果!")
return
# 2. 处理多轮廓场景:合并所有轮廓点(或取最大轮廓)
# 方案:取面积最大的轮廓(适配自定义图像的主要目标)
max_contour = max(contours, key=cv2.contourArea)
contour_area = cv2.contourArea(max_contour)
print(f"\n轮廓分析结果:")
print(f"- 检测到轮廓数量:{len(contours)}")
print(f"- 最大轮廓面积:{contour_area:.1f} 像素")
# 3. 计算凸包(clockwise=False:不强制顺时针,returnPoints=True:返回点坐标)
hull = cv2.convexHull(max_contour)
hull_area = cv2.contourArea(hull)
print(f"- 凸包面积:{hull_area:.1f} 像素")
print(f"- 轮廓凹陷率:{(1 - contour_area / hull_area) * 100:.1f}%(值越大,凹陷越明显)")
# 4. 绘制原始轮廓和凸包(便于对比)
# 创建彩色画布,方便同时显示轮廓和凸包
result_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
# 绘制原始轮廓(绿色,线宽2)
cv2.drawContours(result_img, [max_contour], -1, (0, 255, 0), 2)
# 绘制凸包(红色,线宽2)
cv2.drawContours(result_img, [hull], -1, (255, 0, 0), 2)
# 5. 单独生成凸包掩码图(黑白)
hull_img = np.zeros_like(img)
cv2.drawContours(hull_img, [hull], -1, 255, 2)
# 6. 显示对比:原始二值图 → 凸包掩码图 → 轮廓+凸包叠加图
show_images([img, hull_img, result_img],
["自定义二值图像", "凸包提取结果(黑白)", "原始轮廓(绿)+凸包(红)叠加"],
rows=1, cols=3, figsize=(18, 6))
# 测试凸包提取(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/LinNai.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图的对比
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 6))
# 调用凸包提取演示函数
convex_hull_demo(binary_img)
细化(Thinning)是将前景区域逐步收缩为单像素宽度的骨架,保留目标的拓扑结构。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def thinning_demo(img):
"""
图像细化(骨架提取):将粗线条缩为单像素宽度的骨架,保留形状特征
适配方案:优先使用OpenCV官方ximgproc模块(Zhang-Suen算法),无则用备用实现
"""
# 确保输入为uint8格式的二值图
img = img.astype(np.uint8)
# 统计原始图像的前景像素数(用于对比细化效果)
original_nonzero = cv2.countNonZero(img)
print(f"\n细化前前景像素数:{original_nonzero}")
# 1. 优先使用OpenCV ximgproc模块的Zhang-Suen细化算法(效果最优)
try:
from cv2 import ximgproc
# 检查ximgproc是否可用(需安装opencv-contrib-python)
thinned_img = ximgproc.thinning(img, thinningType=ximgproc.THINNING_ZHANGSUEN)
print("使用OpenCV官方Zhang-Suen细化算法")
except (ImportError, AttributeError):
# 2. 备用实现(简化版细化,适配无contrib的OpenCV)
print("未检测到opencv-contrib-python,使用备用细化算法")
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
thinned_img = img.copy()
# 迭代腐蚀+膨胀,直到无法再细化(缩为单像素)
while True:
eroded = cv2.erode(thinned_img, kernel)
temp = cv2.dilate(eroded, kernel)
temp = cv2.subtract(thinned_img, temp) # 提取边缘
thinned_img = cv2.bitwise_and(eroded, cv2.bitwise_not(temp))
# 终止条件:像素数不再变化(已细化到单像素)
current_nonzero = cv2.countNonZero(thinned_img)
if current_nonzero == cv2.countNonZero(eroded) or current_nonzero == 0:
break
# 统计细化后的前景像素数
thinned_nonzero = cv2.countNonZero(thinned_img)
print(f"细化后前景像素数:{thinned_nonzero}")
print(f"像素压缩率:{(1 - thinned_nonzero / original_nonzero) * 100:.1f}%")
# 显示对比:原始二值图 → 细化结果 → 局部放大对比
# 主对比图
show_images([img, thinned_img],
["自定义二值图像(粗线条)", "图像细化结果(单像素骨架)"],
rows=1, cols=2, figsize=(15, 7))
# 局部放大对比(取中心区域,更清晰看细化效果)
h, w = img.shape
center_h, center_w = h // 2, w // 2
crop_size = 80 # 放大区域大小
# 裁剪原始图和细化图的中心区域
img_crop = img[center_h - crop_size:center_h + crop_size, center_w - crop_size:center_w + crop_size]
thinned_crop = thinned_img[center_h - crop_size:center_h + crop_size, center_w - crop_size:center_w + crop_size]
# 显示放大对比
show_images([img_crop, thinned_crop],
["原始图像(局部放大)", "细化结果(局部放大)"],
rows=1, cols=2, figsize=(10, 10))
# 测试图像细化(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/LinNai.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图的对比
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 10))
# 调用图像细化演示函数
thinning_demo(binary_img)

粗化(Thickening)是细化的逆操作,将单像素宽度的骨架恢复为一定宽度的区域,本质是膨胀 + 约束。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def thinning_core(img):
"""
图像细化核心函数(仅返回细化结果,无可视化,供粗化函数调用)
适配方案:优先使用OpenCV ximgproc,无则用备用实现
"""
img = img.astype(np.uint8)
# 1. 优先使用Zhang-Suen算法(需opencv-contrib-python)
try:
from cv2 import ximgproc
thinned_img = ximgproc.thinning(img, thinningType=ximgproc.THINNING_ZHANGSUEN)
except (ImportError, AttributeError):
# 2. 备用细化实现
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
thinned_img = img.copy()
while True:
eroded = cv2.erode(thinned_img, kernel)
temp = cv2.dilate(eroded, kernel)
temp = cv2.subtract(thinned_img, temp)
thinned_img = cv2.bitwise_and(eroded, cv2.bitwise_not(temp))
if cv2.countNonZero(thinned_img) == cv2.countNonZero(eroded) or cv2.countNonZero(thinned_img) == 0:
break
return thinned_img
def thinning_demo(img):
"""
图像细化演示函数(带可视化,供单独调用)
"""
thinned_img = thinning_core(img)
# 统计细化前后像素数
original_nonzero = cv2.countNonZero(img)
thinned_nonzero = cv2.countNonZero(thinned_img)
print(f"\n细化前前景像素数:{original_nonzero}")
print(f"细化后前景像素数:{thinned_nonzero}")
# 可视化
show_images([img, thinned_img],
["原始二值图像", "细化结果(单像素骨架)"],
rows=1, cols=2, figsize=(12, 6))
return thinned_img # 关键:返回细化结果,供粗化函数使用
def thickening_demo(img):
"""
图像粗化:基于细化骨架的膨胀实现
流程:原始图→细化得到单像素骨架→膨胀骨架实现可控粗化
优势:相比直接膨胀原始图,粗化结果更均匀、形状更规整
"""
# 1. 先对原始图做细化,得到单像素骨架(修复原代码无返回值问题)
print("\n===== 第一步:图像细化 =====")
thinned_img = thinning_core(img) # 调用无可视化的细化核心函数
# 2. 对骨架进行膨胀实现粗化(可调整iterations控制粗化程度)
print("\n===== 第二步:骨架粗化 =====")
# 定义膨胀核(十字形,保证粗化均匀)
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
# 膨胀次数:iterations越大,线条越粗(可根据需求调整)
thicken_iter = 2
thickened_img = cv2.dilate(thinned_img, kernel, iterations=thicken_iter)
# 3. 对比直接膨胀原始图 vs 基于骨架粗化(展示优势)
direct_dilate = cv2.dilate(img, kernel, iterations=1) # 原始图直接膨胀
# 统计像素数,量化粗化效果
thinned_nonzero = cv2.countNonZero(thinned_img)
thickened_nonzero = cv2.countNonZero(thickened_img)
direct_nonzero = cv2.countNonZero(direct_dilate)
print(f"细化骨架像素数:{thinned_nonzero}")
print(f"骨架粗化后像素数:{thickened_nonzero}(膨胀{thicken_iter}次)")
print(f"原始图直接膨胀像素数:{direct_nonzero}")
# 4. 多维度可视化对比
# 对比1:原始图 → 细化骨架 → 骨架粗化结果
show_images([img, thinned_img, thickened_img],
["自定义二值图像", "细化单像素骨架", f"骨架粗化(膨胀{thicken_iter}次)"],
rows=1, cols=3, figsize=(18, 6))
# 对比2:骨架粗化 vs 原始图直接膨胀(展示粗化优势)
show_images([thickened_img, direct_dilate],
["基于骨架的粗化(形状规整)", "原始图直接膨胀(边缘粗糙)"],
rows=1, cols=2, figsize=(15, 7))
# 测试图像粗化(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/Mei.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 6))
# 调用图像粗化演示函数
thickening_demo(binary_img)

骨架(Skeleton)是目标区域的中轴线,满足:1)单像素宽度;2)保留目标拓扑结构;3)与原区域等距。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def skeleton_extraction_demo(img):
"""
骨架提取(基于腐蚀+开运算的经典算法)
原理:迭代腐蚀图像,每次提取腐蚀后的边缘,最终叠加得到单像素宽度的骨架
优势:无需依赖OpenCV-contrib,纯基础API实现,兼容性强
"""
# 备份原始图像(避免迭代过程中修改输入)
original_img = img.copy()
skeleton = np.zeros_like(img)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# 统计迭代信息
iter_count = 0
original_nonzero = cv2.countNonZero(original_img)
print(f"\n骨架提取开始:")
print(f"- 原始图像前景像素数:{original_nonzero}")
# 迭代腐蚀+开运算提取骨架
while True:
# 1. 腐蚀操作(收缩前景)
eroded = cv2.erode(img, kernel)
# 2. 开运算(腐蚀+膨胀,去除孤立点)
opened = cv2.morphologyEx(eroded, cv2.MORPH_OPEN, kernel)
# 3. 提取当前迭代的边缘(腐蚀图 - 开运算图)
temp = cv2.subtract(eroded, opened)
# 4. 叠加边缘到骨架图
skeleton = cv2.bitwise_or(skeleton, temp)
# 5. 更新迭代图像为腐蚀后的结果
img = eroded.copy()
iter_count += 1
# 终止条件:图像全黑(无前景像素)
if cv2.countNonZero(img) == 0:
break
# 统计结果
skeleton_nonzero = cv2.countNonZero(skeleton)
print(f"- 迭代次数:{iter_count}")
print(f"- 骨架像素数:{skeleton_nonzero}")
print(f"- 骨架压缩率:{(1 - skeleton_nonzero / original_nonzero) * 100:.1f}%")
# 增强可视化:多维度对比
# 对比1:原始图 → 骨架图 → 骨架叠加在原始图上(彩色)
# 创建彩色叠加图
overlay_img = cv2.cvtColor(original_img, cv2.COLOR_GRAY2BGR)
overlay_img[skeleton == 255] = [255, 0, 0] # 骨架标红
show_images([original_img, skeleton, overlay_img],
["自定义二值图像", "骨架提取结果(单像素)", "骨架(红)叠加原始图"],
rows=1, cols=3, figsize=(18, 6))
# 对比2:局部放大(更清晰看骨架细节)
h, w = original_img.shape
center_h, center_w = h // 2, w // 2
crop_size = 80
# 裁剪区域
original_crop = original_img[center_h - crop_size:center_h + crop_size, center_w - crop_size:center_w + crop_size]
skeleton_crop = skeleton[center_h - crop_size:center_h + crop_size, center_w - crop_size:center_w + crop_size]
# 显示放大对比
show_images([original_crop, skeleton_crop],
["原始图像(局部放大)", "骨架结果(局部放大)"],
rows=1, cols=2, figsize=(10, 10))
# 测试骨架提取(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/JinXi.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 6))
# 调用骨架提取演示函数
skeleton_extraction_demo(binary_img)

修剪(Pruning)是去除骨架 / 细化结果中的毛刺、小分支,保留主要结构。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def skeleton_extraction_core(img):
"""
骨架提取核心函数(仅返回结果,无可视化,供修剪函数调用)
"""
skeleton = np.zeros_like(img)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
while True:
eroded = cv2.erode(img, kernel)
opened = cv2.morphologyEx(eroded, cv2.MORPH_OPEN, kernel)
temp = cv2.subtract(eroded, opened)
skeleton = cv2.bitwise_or(skeleton, temp)
img = eroded.copy()
if cv2.countNonZero(img) == 0:
break
return skeleton
def skeleton_extraction_demo(img):
"""
骨架提取演示函数(带可视化,供单独调用)
"""
skeleton = skeleton_extraction_core(img)
show_images([img, skeleton], ["原始二值图像", "骨架提取结果"], rows=1, cols=2, figsize=(12, 6))
return skeleton
def pruning_demo(img):
"""
骨架修剪(去除小分支/毛刺)
原理:通过迭代腐蚀+膨胀检测并移除骨架的端点(小分支),保留主骨架
优势:去除噪声导致的毛刺,让骨架更简洁、贴合真实形状
"""
# 1. 先提取原始骨架(修复原代码无返回值问题)
print("\n===== 第一步:提取原始骨架 =====")
skeleton = skeleton_extraction_core(img)
original_skeleton_nonzero = cv2.countNonZero(skeleton)
print(f"原始骨架像素数(含毛刺):{original_skeleton_nonzero}")
# 2. 骨架修剪:迭代去除端点(小分支)
print("\n===== 第二步:修剪骨架小分支 =====")
pruned_skeleton = skeleton.copy()
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
# 迭代次数:越大修剪越彻底(可根据毛刺长度调整)
prune_iter = 3
for i in range(prune_iter):
# 腐蚀:移除端点
temp_erode = cv2.erode(pruned_skeleton, kernel)
# 膨胀:恢复主骨架(端点无法恢复)
temp_dilate = cv2.dilate(temp_erode, kernel)
# 保留主骨架,去除端点
pruned_skeleton = cv2.bitwise_and(pruned_skeleton, temp_dilate)
print(f" 第{i + 1}次修剪后像素数:{cv2.countNonZero(pruned_skeleton)}")
# 统计修剪效果
pruned_nonzero = cv2.countNonZero(pruned_skeleton)
prune_ratio = (1 - pruned_nonzero / original_skeleton_nonzero) * 100
print(f"修剪完成:移除了{original_skeleton_nonzero - pruned_nonzero}个毛刺像素({prune_ratio:.1f}%)")
# 3. 多维度可视化对比
# 对比1:原始骨架 → 修剪后骨架 → 差异图(红色=被修剪的毛刺)
# 创建差异图(显示被修剪的部分)
diff_img = cv2.cvtColor(skeleton, cv2.COLOR_GRAY2BGR)
# 找到被修剪的像素(原始骨架有,修剪后无)
pruned_pixels = (skeleton == 255) & (pruned_skeleton == 0)
diff_img[pruned_pixels] = [255, 0, 0] # 被修剪的毛刺标红
show_images([skeleton, pruned_skeleton, diff_img],
["原始骨架(含毛刺)", "修剪后骨架(仅主骨架)", "修剪差异(红=移除的毛刺)"],
rows=1, cols=3, figsize=(18, 6))
# 对比2:局部放大(更清晰看毛刺修剪效果)
h, w = skeleton.shape
center_h, center_w = h // 2, w // 2
crop_size = 80
# 裁剪区域
skeleton_crop = skeleton[center_h - crop_size:center_h + crop_size, center_w - crop_size:center_w + crop_size]
pruned_crop = pruned_skeleton[center_h - crop_size:center_h + crop_size, center_w - crop_size:center_w + crop_size]
# 显示放大对比
show_images([skeleton_crop, pruned_crop],
["原始骨架(局部放大,含毛刺)", "修剪后骨架(局部放大)"],
rows=1, cols=2, figsize=(10, 8))
# 测试骨架修剪(使用自定义图像)
if __name__ == "__main__":
img_path = "../picture/Water.png" # 请修改为你的图像实际路径
# 读取并预处理图像
gray_img, binary_img = read_and_preprocess_image(img_path, threshold=127)
if binary_img is None:
exit() # 图像读取失败则退出
# 先显示原始灰度图和二值图
show_images([gray_img, binary_img],
["原始灰度图像", "二值化后的图像"],
rows=1, cols=2, figsize=(12, 6))
# 调用骨架修剪演示函数
pruning_demo(binary_img)


形态学重建是基于种子图像和掩码图像的迭代膨胀操作,核心是用种子图像逐步填充掩码图像的区域,保留目标结构。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_and_preprocess_image(img_path, threshold=127):
"""
读取自定义图像并预处理为二值图
:param img_path: 本地图像路径(相对/绝对路径)
:param threshold: 二值化阈值(0-255)
:return: 原始灰度图、二值图;读取失败则返回None
"""
# 读取图像(BGR格式)
img = cv2.imread(img_path)
if img is None:
print(f"错误:无法读取图像,请检查路径是否正确!路径:{img_path}")
return None, None
# 转为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 灰度图转二值图(阈值分割)
_, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
return gray_img, binary_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def create_mask_and_seed_from_custom_img(custom_img, seed_scale=0.5):
"""
从自定义图像生成掩码和种子图像(适配形态学重建)
:param custom_img: 二值化后的自定义图像(掩码)
:param seed_scale: 种子图像占掩码的比例(0-1),默认0.5(中心50%区域)
:return: mask(掩码)、seed(种子)
"""
# 掩码直接使用自定义二值图(模拟有"缺失区域"的目标)
mask = custom_img.copy()
# 生成种子图像:掩码中心的缩小区域(模拟目标的核心部分)
h, w = mask.shape
center_h, center_w = h // 2, w // 2
seed_h = int(h * seed_scale // 2)
seed_w = int(w * seed_scale // 2)
seed = np.zeros_like(mask)
# 在中心区域生成种子(保证种子完全在掩码内部)
seed[center_h - seed_h: center_h + seed_h,
center_w - seed_w: center_w + seed_w] = 255
# 可选:为掩码添加"人工缺口"(模拟目标缺失区域,增强重建效果)
# 随机在掩码边缘添加小缺口
gap_size = min(h, w) // 10
gap_pos_h = np.random.randint(gap_size, h - gap_size)
gap_pos_w = np.random.randint(gap_size, w - gap_size)
mask[gap_pos_h - gap_size // 2: gap_pos_h + gap_size // 2,
gap_pos_w - gap_size // 2: gap_pos_w + gap_size // 2] = 0
print(f"\n自定义图像处理完成:")
print(f"- 掩码图像:添加了{gap_size}×{gap_size}的人工缺口")
print(f"- 种子图像:中心{seed_scale * 100}%区域")
return mask, seed
def morphological_reconstruction_demo(use_custom_img=False, img_path="test.jpg", threshold=127):
"""
形态学重建:从种子图像重建掩码图像的区域
核心原理:以种子为起点迭代膨胀,被掩码约束,最终填充掩码内的缺失区域
应用场景:图像修复、目标分割、孔洞填充(比普通填充更精准)
:param use_custom_img: 是否使用自定义图像(默认False,使用测试图)
:param img_path: 自定义图像路径
:param threshold: 二值化阈值
"""
# 1. 生成掩码和种子图像(支持测试图/自定义图像双模式)
if use_custom_img:
# 模式1:使用自定义图像
print("\n===== 使用自定义图像进行形态学重建 =====")
gray_img, binary_img = read_and_preprocess_image(img_path, threshold)
if binary_img is None:
return
# 先显示原始自定义图像
show_images([gray_img, binary_img],
["自定义原始灰度图", "自定义二值图(基础掩码)"],
rows=1, cols=2, figsize=(12, 6))
# 生成掩码和种子
mask, seed = create_mask_and_seed_from_custom_img(binary_img)
else:
# 模式2:使用经典测试图(有缺口的正方形)
print("\n===== 使用测试图进行形态学重建 =====")
# 生成掩码图像(有缺口的正方形)
mask = np.zeros((200, 200), dtype=np.uint8)
mask[50:150, 50:150] = 255
mask[90:110, 90:110] = 0 # 中心缺口
# 生成种子图像(掩码内部的小区域)
seed = np.zeros_like(mask)
seed[70:130, 70:130] = 255
# 2. 形态学重建核心流程
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
reconstructed = seed.copy()
iter_count = 0
print("\n开始形态学重建迭代:")
while True:
prev_recon = reconstructed.copy()
# 迭代步骤:膨胀 → 与掩码取交集(约束膨胀范围)
reconstructed = cv2.dilate(reconstructed, kernel)
reconstructed = cv2.bitwise_and(reconstructed, mask)
iter_count += 1
# 终止条件:重建结果不再变化
if np.array_equal(reconstructed, prev_recon):
print(f"重建完成!迭代次数:{iter_count}")
break
# 3. 量化分析重建效果
mask_nonzero = cv2.countNonZero(mask)
seed_nonzero = cv2.countNonZero(seed)
recon_nonzero = cv2.countNonZero(reconstructed)
print(f"\n重建效果分析:")
print(f"- 掩码像素数(含缺口):{mask_nonzero}")
print(f"- 种子像素数:{seed_nonzero}")
print(f"- 重建后像素数:{recon_nonzero}")
print(f"- 填充率:{(recon_nonzero - seed_nonzero) / (mask_nonzero - seed_nonzero) * 100:.1f}%")
# 4. 可视化对比
show_images([mask, seed, reconstructed],
["掩码图像(有缺失/缺口)", "种子图像(核心区域)", "形态学重建结果"],
rows=1, cols=3, figsize=(18, 6))
# 额外:显示重建差异(重建结果 - 种子图像,展示填充的区域)
diff = cv2.subtract(reconstructed, seed)
show_images([diff], ["重建填充的区域(差异图)"], rows=1, cols=1, figsize=(6, 6))
# 测试形态学重建
if __name__ == "__main__":
morphological_reconstruction_demo(
use_custom_img=True,
img_path="../picture/AALi.jpg", # 替换为你的自定义图像路径
threshold=127 # 二值化阈值,可根据图像调整
)


灰度形态学将二值形态学的操作扩展到灰度图像,核心是用结构元素对灰度值进行极值运算。

# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_gray_image(img_path):
"""
读取灰度图像(兼容彩色图自动转灰度、路径错误时生成模拟图)
:param img_path: 图像路径(相对/绝对)
:return: 灰度图像
"""
# 尝试读取图像
img = cv2.imread(img_path)
if img is None:
print(f"警告:无法读取图像 {img_path},生成模拟灰度测试图")
# 生成带渐变和噪声的模拟灰度图(更贴近真实图像效果)
img = np.zeros((300, 300), dtype=np.uint8)
# 添加渐变
for i in range(300):
img[:, i] = np.linspace(50, 200, 300, dtype=np.uint8)
# 添加随机噪声
noise = np.random.randint(-20, 20, (300, 300), dtype=np.int16)
img = np.clip(img + noise, 0, 255).astype(np.uint8)
return img
# 彩色图转灰度图
if len(img.shape) == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def gray_erosion_dilation_demo(img):
"""
灰度腐蚀与膨胀演示
核心原理:
- 灰度腐蚀:取结构元素覆盖区域的最小值 → 暗区域扩张、亮区域收缩(整体变暗)
- 灰度膨胀:取结构元素覆盖区域的最大值 → 亮区域扩张、暗区域收缩(整体变亮)
应用场景:灰度腐蚀去亮噪声、灰度膨胀去暗噪声、图像对比度调整等
"""
# 定义结构元素(5×5矩形,可调整大小控制腐蚀/膨胀强度)
kernel_size = 5
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
# 1. 灰度腐蚀(最小值滤波)
gray_eroded = cv2.erode(img, kernel)
# 2. 灰度膨胀(最大值滤波)
gray_dilated = cv2.dilate(img, kernel)
# 量化分析:统计像素均值(直观展示明暗变化)
original_mean = np.mean(img)
eroded_mean = np.mean(gray_eroded)
dilated_mean = np.mean(gray_dilated)
print(f"\n灰度形态学效果量化分析(结构元素{kernel_size}×{kernel_size}):")
print(f"- 原始图像像素均值:{original_mean:.2f}")
print(f"- 灰度腐蚀后均值:{eroded_mean:.2f}(变暗 {original_mean - eroded_mean:.2f})")
print(f"- 灰度膨胀后均值:{dilated_mean:.2f}(变亮 {dilated_mean - original_mean:.2f})")
# 3. 可视化对比
# 主对比:原始图 → 灰度腐蚀 → 灰度膨胀
show_images([img, gray_eroded, gray_dilated],
["原始灰度图像", f"灰度腐蚀(暗区扩张)", f"灰度膨胀(亮区扩张)"],
rows=1, cols=3, figsize=(18, 6))
# 额外:显示差值图(更清晰看变化区域)
erosion_diff = img - gray_eroded # 腐蚀差值(亮区减少的部分)
dilation_diff = gray_dilated - img # 膨胀差值(亮区增加的部分)
show_images([erosion_diff, dilation_diff],
["腐蚀差值(亮区减少,越亮变化越大)", "膨胀差值(亮区增加,越亮变化越大)"],
rows=1, cols=2, figsize=(12, 6))
# 测试灰度腐蚀与膨胀
if __name__ == "__main__":
img_path = "../picture/CSGO.jpg"
# 读取灰度图像
gray_img = read_gray_image(img_path)
# 先显示原始灰度图
plt.figure(figsize=(6, 6))
plt.imshow(gray_img, cmap='gray', vmin=0, vmax=255)
plt.title("原始灰度图像")
plt.axis('off')
plt.tight_layout()
plt.show()
# 调用灰度腐蚀膨胀演示函数
gray_erosion_dilation_demo(gray_img)


# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_gray_image(img_path):
"""
读取灰度图像(兼容彩色图自动转灰度、路径错误时生成模拟图)
:param img_path: 图像路径(相对/绝对)
:return: 灰度图像
"""
# 尝试读取图像
img = cv2.imread(img_path)
if img is None:
print(f"警告:无法读取图像 {img_path},生成模拟灰度测试图")
# 生成带渐变的模拟灰度图(更贴近真实图像)
img = np.zeros((300, 300), dtype=np.uint8)
for i in range(300):
img[:, i] = np.linspace(50, 200, 300, dtype=np.uint8)
return img
# 彩色图转灰度图
if len(img.shape) == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return img
def add_gray_noise(img, bright_noise_ratio=0.02, dark_noise_ratio=0.02):
"""
为灰度图像添加亮噪声(白点)和暗噪声(黑点),模拟真实噪声场景
:param img: 原始灰度图
:param bright_noise_ratio: 亮噪声占比(0-1),默认2%
:param dark_noise_ratio: 暗噪声占比(0-1),默认2%
:return: 带噪声的灰度图
"""
noisy_img = img.copy()
h, w = noisy_img.shape
np.random.seed(42) # 固定随机种子,保证噪声可复现
# 1. 添加亮噪声(白点,像素值255)
bright_noise_num = int(h * w * bright_noise_ratio)
bright_coords = np.random.choice(h * w, bright_noise_num, replace=False)
noisy_img.flat[bright_coords] = 255
# 2. 添加暗噪声(黑点,像素值0)
dark_noise_num = int(h * w * dark_noise_ratio)
dark_coords = np.random.choice(h * w, dark_noise_num, replace=False)
noisy_img.flat[dark_coords] = 0
print(f"\n添加噪声完成:")
print(f"- 亮噪声(白点)数量:{bright_noise_num}(占比{bright_noise_ratio * 100:.1f}%)")
print(f"- 暗噪声(黑点)数量:{dark_noise_num}(占比{dark_noise_ratio * 100:.1f}%)")
return noisy_img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def gray_open_close_demo(img):
"""
灰度开运算与闭运算演示
核心原理:
- 灰度开运算 = 灰度腐蚀 → 灰度膨胀 → 优先去除亮噪声(白点),保留暗区域
- 灰度闭运算 = 灰度膨胀 → 灰度腐蚀 → 优先去除暗噪声(黑点),保留亮区域
应用场景:灰度图像去噪、图像平滑、细节增强等
"""
# 定义结构元素(5×5矩形,可调整大小控制去噪强度)
kernel_size = 5
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
# 1. 灰度开运算(去亮噪声)
gray_open = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
# 2. 灰度闭运算(去暗噪声)
gray_close = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
# 3. 组合运算(先开后闭,同时去亮/暗噪声)
gray_open_close = cv2.morphologyEx(gray_open, cv2.MORPH_CLOSE, kernel)
# 量化分析:统计噪声残留(非0/255的像素视为有效像素)
def count_noise(img):
bright_noise = np.sum(img == 255)
dark_noise = np.sum(img == 0)
return bright_noise, dark_noise
orig_bright, orig_dark = count_noise(img)
open_bright, open_dark = count_noise(gray_open)
close_bright, close_dark = count_noise(gray_close)
combo_bright, combo_dark = count_noise(gray_open_close)
print(f"\n灰度开/闭运算去噪效果量化分析(结构元素{kernel_size}×{kernel_size}):")
print(f"{'运算类型':<10} {'亮噪声残留':<10} {'暗噪声残留':<10}")
print(f"{'原始图像':<10} {orig_bright:<10} {orig_dark:<10}")
print(f"{'开运算':<10} {open_bright:<10} {open_dark:<10}(亮噪声减少{orig_bright - open_bright})")
print(f"{'闭运算':<10} {close_bright:<10} {close_dark:<10}(暗噪声减少{orig_dark - close_dark})")
print(f"{'先开后闭':<10} {combo_bright:<10} {combo_dark:<10}(综合去噪)")
# 3. 可视化对比
# 对比1:原始带噪图 → 开运算(去亮噪) → 闭运算(去暗噪)
show_images([img, gray_open, gray_close],
["原始灰度图(带亮/暗噪声)", "灰度开运算(去亮噪声)", "灰度闭运算(去暗噪声)"],
rows=1, cols=3, figsize=(18, 6))
# 对比2:开运算 → 先开后闭(综合去噪)
show_images([gray_open, gray_open_close],
["灰度开运算(仅去亮噪)", "先开后闭(同时去亮/暗噪)"],
rows=1, cols=2, figsize=(12, 6))
# 测试灰度开/闭运算
if __name__ == "__main__":
img_path = "../picture/KaTiXiYa.png"
# 1. 读取原始灰度图像
original_gray = read_gray_image(img_path)
# 2. 显示原始灰度图
plt.figure(figsize=(6, 6))
plt.imshow(original_gray, cmap='gray', vmin=0, vmax=255)
plt.title("原始灰度图像")
plt.axis('off')
plt.tight_layout()
plt.show()
# 3. 添加亮/暗噪声
noisy_gray = add_gray_noise(original_gray, bright_noise_ratio=0.02, dark_noise_ratio=0.02)
# 4. 调用灰度开/闭运算演示函数
gray_open_close_demo(noisy_gray)

# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_gray_image(img_path):
"""
读取灰度图像(兼容彩色图自动转灰度、路径错误时生成模拟图)
:param img_path: 图像路径(相对/绝对)
:return: 灰度图像
"""
# 尝试读取图像
img = cv2.imread(img_path)
if img is None:
print(f"警告:无法读取图像 {img_path},生成模拟灰度测试图(含明暗纹理)")
# 生成带明暗纹理的模拟图(更适合展示顶帽/底帽效果)
img = np.ones((300, 300), dtype=np.uint8) * 128 # 背景灰度128
# 添加亮纹理(白色小方块)
for i in range(10, 290, 30):
for j in range(10, 290, 30):
img[i:i + 10, j:j + 10] = 255
# 添加暗纹理(黑色小方块)
for i in range(25, 285, 30):
for j in range(25, 285, 30):
img[i:i + 10, j:j + 10] = 0
return img
# 彩色图转灰度图
if len(img.shape) == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def gray_morphology_algorithms_demo(img):
"""
灰度形态学核心算法:顶帽/底帽变换
核心原理:
- 顶帽变换(TopHat)= 原始图 - 开运算图 → 提取比背景亮的小区域/纹理
- 底帽变换(BlackHat)= 闭运算图 - 原始图 → 提取比背景暗的小区域/纹理
应用场景:纹理提取、背景归一化、缺陷检测(如表面划痕、污渍)
"""
# 定义结构元素(7×7矩形,大小决定提取纹理的尺度)
kernel_size = 7
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
# 1. 先计算开/闭运算(用于解释顶帽/底帽的由来)
gray_open = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
gray_close = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
# 2. 顶帽变换(提取亮纹理)
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
# 手动验证:顶帽 = 原始图 - 开运算图
tophat_manual = cv2.subtract(img, gray_open)
# 3. 底帽变换(提取暗纹理)
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
# 手动验证:底帽 = 闭运算图 - 原始图
blackhat_manual = cv2.subtract(gray_close, img)
# 量化分析:统计纹理区域的像素占比
def calc_texture_ratio(texture_img):
# 非0像素视为纹理区域
texture_pixels = np.sum(texture_img > 0)
total_pixels = texture_img.shape[0] * texture_img.shape[1]
return (texture_pixels / total_pixels) * 100
tophat_ratio = calc_texture_ratio(tophat)
blackhat_ratio = calc_texture_ratio(blackhat)
print(f"\n顶帽/底帽变换量化分析(结构元素{kernel_size}×{kernel_size}):")
print(f"- 顶帽变换(亮纹理)占比:{tophat_ratio:.2f}%")
print(f"- 底帽变换(暗纹理)占比:{blackhat_ratio:.2f}%")
print(f"- 顶帽手动计算与API结果是否一致:{np.array_equal(tophat, tophat_manual)}")
print(f"- 底帽手动计算与API结果是否一致:{np.array_equal(blackhat, blackhat_manual)}")
# 4. 多维度可视化
# 对比1:原始图 → 开运算图 → 顶帽变换(亮纹理)
show_images([img, gray_open, tophat],
["原始灰度图像", "开运算图(去除亮纹理)", "顶帽变换(提取亮纹理)"],
rows=1, cols=3, figsize=(18, 6))
# 对比2:原始图 → 闭运算图 → 底帽变换(暗纹理)
show_images([img, gray_close, blackhat],
["原始灰度图像", "闭运算图(去除暗纹理)", "底帽变换(提取暗纹理)"],
rows=1, cols=3, figsize=(18, 6))
# 对比3:顶帽+底帽融合(展示所有明暗纹理)
texture_fusion = cv2.add(tophat, blackhat)
show_images([tophat, blackhat, texture_fusion],
["顶帽(亮纹理)", "底帽(暗纹理)", "顶帽+底帽(所有纹理)"],
rows=1, cols=3, figsize=(18, 6))
# 测试顶帽/底帽变换
if __name__ == "__main__":
img_path = "../picture/HuTao.png"
# 1. 读取原始灰度图像
original_gray = read_gray_image(img_path)
# 2. 显示原始灰度图
plt.figure(figsize=(6, 6))
plt.imshow(original_gray, cmap='gray', vmin=0, vmax=255)
plt.title("原始灰度图像")
plt.axis('off')
plt.tight_layout()
plt.show()
# 3. 调用顶帽/底帽变换演示函数
gray_morphology_algorithms_demo(original_gray)


与二值形态学重建类似,基于种子图像和掩码图像的迭代膨胀,保留灰度图像的细节。
# 导入必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示(避免标题乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
def read_gray_image(img_path):
"""
读取灰度图像(兼容彩色图自动转灰度、路径错误时生成模拟图)
:param img_path: 图像路径(相对/绝对)
:return: 灰度图像
"""
# 尝试读取图像
img = cv2.imread(img_path)
if img is None:
print(f"警告:无法读取图像 {img_path},生成模拟灰度测试图(带渐变纹理)")
# 生成带渐变的模拟灰度图(更适合展示灰度重建效果)
img = np.zeros((300, 300), dtype=np.uint8)
# 添加水平渐变
for i in range(300):
img[:, i] = np.linspace(50, 200, 300, dtype=np.uint8)
# 添加垂直条纹纹理
for j in range(0, 300, 20):
img[:, j:j + 5] = np.clip(img[:, j:j + 5] + 30, 0, 255)
return img
# 彩色图转灰度图
if len(img.shape) == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return img
def show_images(imgs, titles, rows=1, cols=2, figsize=(10, 5)):
"""
批量显示图像(对比用)
:param imgs: 图像列表
:param titles: 标题列表
:param rows: 行数
:param cols: 列数
:param figsize: 画布大小
"""
plt.figure(figsize=figsize)
for i in range(len(imgs)):
plt.subplot(rows, cols, i + 1)
plt.imshow(imgs[i], cmap='gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
def gray_reconstruction_demo(img_path="lena.jpg"):
"""
灰度形态学重建
核心原理:
- 掩码图像(mask):定义重建的“上限”,重建结果不能超过掩码的灰度值
- 种子图像(seed):重建的“起点”,通常是掩码的暗化版本
- 重建流程:迭代灰度膨胀种子 → 与掩码取最小值(约束)→ 直到结果稳定
应用场景:灰度图像修复、纹理填充、背景归一化、图像分层
"""
# 1. 读取/生成掩码图像(灰度图)
mask = read_gray_image(img_path)
mask_mean = np.mean(mask)
print(f"\n掩码图像统计:")
print(f"- 尺寸:{mask.shape}")
print(f"- 像素均值:{mask_mean:.2f}")
# 2. 生成种子图像(掩码暗化10个灰度级,保证种子≤掩码)
seed = cv2.subtract(mask, 10)
seed = np.clip(seed, 0, 255) # 防止负值(灰度值范围0-255)
seed_mean = np.mean(seed)
print(f"\n种子图像统计(掩码暗化10级):")
print(f"- 像素均值:{seed_mean:.2f}")
print(f"- 与掩码均值差:{mask_mean - seed_mean:.2f}")
# 3. 灰度形态学重建核心流程
kernel_size = 3
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
reconstructed = seed.copy()
iter_count = 0
print("\n开始灰度形态学重建迭代:")
while True:
prev_recon = reconstructed.copy()
# 步骤1:灰度膨胀(以3×3核扩张种子的灰度值)
reconstructed = cv2.dilate(reconstructed, kernel)
# 步骤2:与掩码取最小值(核心约束:重建结果不能超过掩码)
reconstructed = cv2.min(reconstructed, mask)
iter_count += 1
# 终止条件:重建结果不再变化(收敛)
if np.array_equal(reconstructed, prev_recon):
print(f"重建完成!迭代次数:{iter_count}")
break
# 4. 量化分析重建效果
recon_mean = np.mean(reconstructed)
# 计算重建前后的像素差值(种子→重建)
seed_recon_diff = reconstructed - seed
# 计算重建与掩码的差值(掩码→重建)
mask_recon_diff = mask - reconstructed
print(f"\n重建结果统计:")
print(f"- 重建后像素均值:{recon_mean:.2f}")
print(f"- 种子→重建均值提升:{recon_mean - seed_mean:.2f}")
print(f"- 重建→掩码均值差:{mask_mean - recon_mean:.2f}")
print(f"- 像素差值非零占比:{np.sum(seed_recon_diff > 0) / (mask.shape[0] * mask.shape[1]) * 100:.2f}%")
# 5. 多维度可视化对比
# 对比1:掩码 → 种子 → 重建结果(核心对比)
show_images([mask, seed, reconstructed],
["掩码图像(重建上限)", "种子图像(重建起点)", "灰度重建结果"],
rows=1, cols=3, figsize=(18, 10))
# 对比2:种子→重建差值 → 掩码→重建差值(展示变化区域)
show_images([seed_recon_diff, mask_recon_diff],
["种子→重建差值(越亮提升越多)", "掩码→重建差值(越亮未恢复越多)"],
rows=1, cols=2, figsize=(12, 10))
# 对比3:局部放大(更清晰看灰度细节)
h, w = mask.shape
center_h, center_w = h // 2, w // 2
crop_size = 80
# 裁剪中心区域
mask_crop = mask[center_h - crop_size:center_h + crop_size, center_w - crop_size:center_w + crop_size]
recon_crop = reconstructed[center_h - crop_size:center_h + crop_size, center_w - crop_size:center_w + crop_size]
show_images([mask_crop, recon_crop],
["掩码(局部放大)", "重建结果(局部放大)"],
rows=1, cols=2, figsize=(10, 10))
# 测试灰度形态学重建
if __name__ == "__main__":
img_path = "../picture/GaoDa.png"
# 调用灰度重建演示函数
gray_reconstruction_demo(img_path)

