首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >《数字图像处理》第 9 章 - 形态学图像处理

《数字图像处理》第 9 章 - 形态学图像处理

作者头像
啊阿狸不会拉杆
发布2026-01-21 14:36:59
发布2026-01-21 14:36:59
1090
举报

前言

        形态学图像处理是数字图像处理中基于形状的图像处理方法,核心是利用结构元素对图像进行操作,广泛应用于图像分割、边缘检测、噪声去除、特征提取等场景。本文结合《数字图像处理》第 9 章内容,从基础概念到实战代码,全方位讲解形态学图像处理,所有代码均可直接运行,附带效果对比图,帮你快速掌握核心知识点。

9.1 预备知识

9.1.1 核心概念
  • 结构元素(Structuring Element, SE):形态学操作的 "模板",通常是小的二值图像(如 3×3、5×5 的矩形 / 圆形 / 十字形),用于扫描图像并与像素邻域交互。
  • 二值图像:形态学处理的基础,像素值仅为 0(背景)或 255(前景),本文所有示例均基于二值图像展开(灰度形态学单独讲解)。
  • 原点:结构元素的参考点,通常为中心像素。
9.1.2 工具准备

本文使用 Python 实现所有案例,依赖库如下:

代码语言:javascript
复制
# 安装依赖
pip install numpy opencv-python matplotlib
9.1.3 基础函数(通用)

        先定义通用的图像读取、显示、预处理函数,后续案例直接调用:

代码语言:javascript
复制
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))

9.2 腐蚀与膨胀

        腐蚀和膨胀是形态学最基础的两个操作,所有复杂形态学运算均基于此。

9.2.1 腐蚀
原理

        腐蚀是收缩 / 细化前景区域的操作:用结构元素扫描图像,只有当结构元素的所有像素都与图像中的前景像素重合时,原点位置才保留为前景,否则置为背景。

代码实现 + 效果对比
代码语言:javascript
复制
# 导入必要的库
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))
效果说明

        腐蚀会消除小的亮噪声点,同时让前景区域边缘收缩;迭代次数越多,收缩效果越明显。

9.2.2 膨胀
原理

        膨胀是扩张 / 加粗前景区域的操作:用结构元素扫描图像,只要结构元素有一个像素与图像中的前景像素重合,原点位置就置为前景。

代码实现 + 效果对比
代码语言:javascript
复制
# 导入必要的库
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))
效果说明

        膨胀会填补前景区域的小缺口 / 孔洞,同时让前景边缘扩张;常用来修复腐蚀造成的前景收缩。

9.2.3 对偶性
原理

        腐蚀和膨胀是对偶操作:对前景的腐蚀 = 对背景的膨胀,对前景的膨胀 = 对背景的腐蚀。

代码验证
代码语言:javascript
复制
# 导入必要的库
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)

9.3 开运算与闭运算

        开运算和闭运算是腐蚀 + 膨胀的组合操作,用于噪声去除、形状修复。

原理
  • 开运算:先腐蚀后膨胀,用于去除小的亮噪声,同时保持前景整体形状不变。A∘B=(A⊖B)⊕B
  • 闭运算:先膨胀后腐蚀,用于填补小的暗孔洞,同时保持前景整体形状不变。
代码实现 + 效果对比
代码语言:javascript
复制
# 导入必要的库
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)

9.4 击中 - 击不中变换

原理

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

代码实现 + 效果对比
代码语言:javascript
复制
# 导入必要的库
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)

9.5 基本形态学算法

9.5.1 边界提取
原理

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

代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.5.2 孔洞填充
原理

        孔洞是被前景包围的背景区域,填充思路:从孔洞内部的种子点开始,不断膨胀,直到接触到前景边界。

代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.5.3 连通区域提取
原理

        连通区域是像素值相同且相邻的像素集合,形态学方法通过标记 + 膨胀实现提取,OpenCV 已封装现成函数。

代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.5.4 凸包
原理

        凸包是包含目标区域的最小凸多边形,OpenCV 通过cv2.convexHull实现。

代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.5.5 细化
原理

        细化(Thinning)是将前景区域逐步收缩为单像素宽度的骨架,保留目标的拓扑结构。

代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.5.6 粗化
原理

        粗化(Thickening)是细化的逆操作,将单像素宽度的骨架恢复为一定宽度的区域,本质是膨胀 + 约束。

代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.5.7 骨架提取
原理

        骨架(Skeleton)是目标区域的中轴线,满足:1)单像素宽度;2)保留目标拓扑结构;3)与原区域等距。

代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.5.8 修剪
原理

        修剪(Pruning)是去除骨架 / 细化结果中的毛刺、小分支,保留主要结构。

代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.5.9 形态学重建
原理

        形态学重建是基于种子图像和掩码图像的迭代膨胀操作,核心是用种子图像逐步填充掩码图像的区域,保留目标结构。

代码实现
代码语言:javascript
复制
# 导入必要的库
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  # 二值化阈值,可根据图像调整
    )
9.5.10 二值图像形态学运算总结

9.6 灰度形态学

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

9.6.1 灰度腐蚀与灰度膨胀
原理
代码实现 + 效果对比
代码语言:javascript
复制
# 导入必要的库
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)
9.6.2 灰度开运算与灰度闭运算
原理
代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.6.3 基本灰度形态学算法
核心应用:顶帽变换与底帽变换
  • 顶帽变换:原始图像 - 灰度开运算,用于提取亮区域(如文字、小亮点)。
  • 底帽变换:灰度闭运算 - 原始图像,用于提取暗区域(如暗斑、孔洞)。
代码实现
代码语言:javascript
复制
# 导入必要的库
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)
9.6.4 灰度形态学重建
原理

        与二值形态学重建类似,基于种子图像和掩码图像的迭代膨胀,保留灰度图像的细节。

代码实现
代码语言:javascript
复制
# 导入必要的库
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)

小结

核心知识点总结
  1. 基础操作:腐蚀(收缩)和膨胀(扩张)是形态学的核心,二者具有对偶性;开运算(先腐蚀后膨胀)去亮噪声,闭运算(先膨胀后腐蚀)填暗孔洞。
  2. 二值形态学应用:边界提取、孔洞填充、连通区域提取、骨架提取等算法,均基于基础操作组合实现,是图像分割、特征提取的核心工具。
  3. 灰度形态学扩展:将二值操作的 "0/255" 极值判断扩展为灰度值的极值运算,顶帽 / 底帽变换是灰度形态学的典型应用,适用于灰度图像的噪声去除和特征提取。
实战建议
  1. 结构元素的选择(大小、形状)直接影响形态学效果,需根据目标特征调整(如十字形适合线特征,矩形适合面特征)。
  2. 迭代次数需适度:次数过多会导致目标结构丢失,次数过少则效果不明显。
  3. 灰度形态学在工业检测、医学影像、文字识别等领域应用广泛,结合阈值分割、轮廓检测可实现复杂场景的图像处理。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 9.1 预备知识
    • 9.1.1 核心概念
    • 9.1.2 工具准备
    • 9.1.3 基础函数(通用)
  • 9.2 腐蚀与膨胀
    • 9.2.1 腐蚀
      • 原理
      • 代码实现 + 效果对比
      • 效果说明
    • 9.2.2 膨胀
      • 原理
      • 代码实现 + 效果对比
      • 效果说明
    • 9.2.3 对偶性
      • 原理
      • 代码验证
  • 9.3 开运算与闭运算
    • 原理
    • 代码实现 + 效果对比
  • 9.4 击中 - 击不中变换
    • 原理
    • 代码实现 + 效果对比
  • 9.5 基本形态学算法
    • 9.5.1 边界提取
      • 原理
      • 代码实现
    • 9.5.2 孔洞填充
      • 原理
      • 代码实现
    • 9.5.3 连通区域提取
      • 原理
      • 代码实现
    • 9.5.4 凸包
      • 原理
      • 代码实现
    • 9.5.5 细化
      • 原理
      • 代码实现
    • 9.5.6 粗化
      • 原理
      • 代码实现
    • 9.5.7 骨架提取
      • 原理
      • 代码实现
    • 9.5.8 修剪
      • 原理
      • 代码实现
    • 9.5.9 形态学重建
      • 原理
      • 代码实现
    • 9.5.10 二值图像形态学运算总结
  • 9.6 灰度形态学
    • 9.6.1 灰度腐蚀与灰度膨胀
      • 原理
      • 代码实现 + 效果对比
    • 9.6.2 灰度开运算与灰度闭运算
      • 原理
      • 代码实现
    • 9.6.3 基本灰度形态学算法
      • 核心应用:顶帽变换与底帽变换
      • 代码实现
    • 9.6.4 灰度形态学重建
      • 原理
      • 代码实现
  • 小结
    • 核心知识点总结
    • 实战建议
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档