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

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

作者头像
啊阿狸不会拉杆
发布2026-01-21 13:57:54
发布2026-01-21 13:57:54
1760
举报

前言

形态学图像处理是数字图像处理领域的核心技术之一,它基于集合论和几何形状分析,通过一系列基本运算(腐蚀、膨胀、开运算、闭运算等)实现对图像的形状特征提取、噪声去除、结构分析等操作。本文将按照《数字图像处理》第 9 章的结构,从基础概念到实战代码,全方位讲解形态学图像处理的核心知识点,所有代码均可直接运行,配套效果对比图,让你轻松掌握这一关键技术!

引言

形态学(Morphology)原是生物学的分支,研究生物的形态和结构。在数字图像处理中,形态学图像处理以集合论为数学基础,通过设计特定的 “结构元素(Structuring Element)”,对图像的像素集合进行操作,从而提取图像中的形状特征(如边界、连通区域、孔洞等),实现图像的增强、分割、修复等目标。

形态学图像处理主要分为两类:

  • 二值形态学:处理二值图像(像素值仅为 0 或 255),是形态学的基础;
  • 灰度级形态学:将二值形态学的运算扩展到灰度图像,适用于更复杂的场景。

学习目标

  1. 理解形态学图像处理的基本概念(结构元素、腐蚀、膨胀、对偶性等);
  2. 掌握二值形态学的核心运算(腐蚀、膨胀、开 / 闭运算、击中 - 击不中变换);
  3. 能够实现基于形态学的基本算法(边界提取、孔洞填充、连通分量提取等);
  4. 理解灰度级形态学的原理,并能应用于实际场景;
  5. 掌握形态学重建的方法及应用。

9.1 预备知识

核心概念
环境准备

本文所有代码基于 Python 实现,依赖以下库:

  • numpy:数值计算;
  • cv2:OpenCV,提供形态学运算的接口;
  • matplotlib:图像可视化(支持中文显示)。

先安装依赖:

代码语言:javascript
复制
pip install numpy opencv-python matplotlib

基础配置代码(统一设置,后续代码可直接复用):

代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# 设置matplotlib支持中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']  # 黑体
plt.rcParams['axes.unicode_minus'] = False    # 解决负号显示问题

# 生成测试图像的辅助函数
def generate_test_image():
    """生成二值测试图像(含矩形、圆形、孔洞)"""
    # 创建空白图像
    img = np.zeros((300, 300), dtype=np.uint8)
    # 绘制矩形
    cv2.rectangle(img, (50, 50), (200, 200), 255, -1)
    # 绘制圆形
    cv2.circle(img, (125, 125), 30, 0, -1)  # 矩形内的孔洞
    # 绘制小矩形
    cv2.rectangle(img, (220, 220), (280, 280), 255, -1)
    return img

# 可视化对比图的辅助函数
def plot_images(images, titles, rows=1, cols=2):
    """
    绘制对比图像
    :param images: 图像列表
    :param titles: 标题列表
    :param rows: 行数
    :param cols: 列数
    """
    fig, axes = plt.subplots(rows, cols, figsize=(12, 6))
    axes = axes.flatten() if rows*cols > 1 else [axes]
    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(title)
        axes[i].axis('off')
    plt.tight_layout()
    plt.show()

# 生成测试图像
test_img = generate_test_image()
# 显示测试图像
plot_images([test_img], ["测试图像(含矩形、孔洞)"])

运行上述代码,会生成一个包含 “大矩形(内部有圆形孔洞)+ 小矩形” 的二值图像,作为后续所有运算的测试数据。

9.2 腐蚀和膨胀

腐蚀(Erosion)和膨胀(Dilation)是二值形态学的两个最基本运算,二者互为对偶运算。

9.2.1 腐蚀
原理

腐蚀的本质是 “收缩” 图像中的前景区域(白色像素),消除小的亮区域,使前景区域的边界向内收缩。

代码实现(含效果对比)
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 设置matplotlib支持中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']  # 黑体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """
    加载自定义图像并预处理
    :param img_path: 图像文件路径(如 "test.png"、"images/input.jpg")
    :param is_binary: 是否转为二值图像(True/False)
    :param resize: 图像缩放尺寸(宽, 高),默认300x300
    :return: 预处理后的图像
    """
    # 读取图像(灰度模式)
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像文件:{img_path},请检查路径是否正确")

    # 缩放图像(统一尺寸,方便对比)
    img = cv2.resize(img, resize)

    # 二值化处理(如果需要)
    if is_binary:
        # 自适应二值化(适配不同亮度的图像)
        _, img = cv2.threshold(
            img, 0, 255,
            cv2.THRESH_BINARY + cv2.THRESH_OTSU  # OTSU自动选阈值
        )
    return img


def plot_images(images, titles, rows=1, cols=2):
    """
    绘制对比图像(修复子图空白问题)
    :param images: 图像列表
    :param titles: 标题列表
    :param rows: 行数
    :param cols: 列数
    """
    # 强制匹配图像数量和子图数量,避免空白
    total_plots = len(images)
    rows = 1 if total_plots <= cols else rows
    cols = total_plots if total_plots < cols else cols

    fig, axes = plt.subplots(rows, cols, figsize=(12, 6))
    axes = axes.flatten() if isinstance(axes, np.ndarray) else [axes]

    # 只绘制有图像的子图,其余隐藏
    for i in range(len(axes)):
        if i < len(images):
            axes[i].imshow(images[i], cmap='gray')
            axes[i].set_title(titles[i])
            axes[i].axis('off')
        else:
            axes[i].axis('off')  # 隐藏空白子图

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    img_path = "../picture/1.jpg"

    try:
        # 1. 加载自定义图像(二值化)
        # 如果不需要二值化,将 is_binary 改为 False
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 按需改为False(处理灰度图)
            resize=(300, 300)
        )
        # 显示加载的原始图像
        plot_images([custom_img], ["加载的自定义图像"], cols=1)

        # 2. 定义结构元素
        kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))  # 3x3十字形
        kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))  # 5x5矩形

        # 3. 执行腐蚀运算
        eroded_cross = cv2.erode(custom_img, kernel_cross, iterations=1)  # 十字形腐蚀1次
        eroded_rect = cv2.erode(custom_img, kernel_rect, iterations=2)  # 矩形腐蚀2次

        # 4. 可视化腐蚀效果对比
        plot_images(
            [custom_img, eroded_cross, eroded_rect],
            ["原始图像", "3x3十字形腐蚀(1次)", "5x5矩形腐蚀(2次)"],
            rows=1, cols=3
        )

    except FileNotFoundError as e:
        print(f"错误:{e}")
        print("提示:请检查图像路径是否正确,例如:")
        print("  - 绝对路径:C:/Users/用户名/Desktop/测试图.png(Windows)")
        print("  - 绝对路径:/Users/用户名/Desktop/测试图.png(Mac)")
        print("  - 相对路径:./测试图.jpg(图像和代码在同一文件夹)")
    except Exception as e:
        print(f"其他错误:{e}")
效果分析
  • 十字形结构元素的腐蚀:仅沿水平 / 垂直方向收缩前景区域,矩形的边角保留较好;
  • 矩形结构元素的腐蚀:沿所有方向收缩,迭代次数越多,收缩越明显,小矩形甚至会被完全消除。
9.2.2 膨胀
原理

膨胀的本质是 “扩张” 图像中的前景区域,填充小的暗区域,使前景区域的边界向外扩张。

代码实现(含效果对比)
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 设置matplotlib支持中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']  # 黑体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """
    加载自定义图像并预处理
    :param img_path: 图像文件路径
    :param is_binary: 是否转为二值图像(True/False)
    :param resize: 图像缩放尺寸(宽, 高),默认300x300
    :return: 预处理后的图像
    """
    # 读取图像(灰度模式)
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像文件:{img_path},请检查路径是否正确")

    # 缩放图像(统一尺寸,方便对比)
    img = cv2.resize(img, resize)

    # 二值化处理(如果需要)
    if is_binary:
        # 自适应二值化(适配不同亮度的图像)
        _, img = cv2.threshold(
            img, 0, 255,
            cv2.THRESH_BINARY + cv2.THRESH_OTSU  # OTSU自动选阈值
        )
    return img


def plot_images(images, titles, rows=1, cols=2):
    """
    绘制对比图像(修复子图空白问题)
    :param images: 图像列表
    :param titles: 标题列表
    :param rows: 行数
    :param cols: 列数
    """
    # 强制匹配图像数量和子图数量,避免空白
    total_plots = len(images)
    rows = 1 if total_plots <= cols else rows
    cols = total_plots if total_plots < cols else cols

    fig, axes = plt.subplots(rows, cols, figsize=(12, 6))
    axes = axes.flatten() if isinstance(axes, np.ndarray) else [axes]

    # 只绘制有图像的子图,其余隐藏
    for i in range(len(axes)):
        if i < len(images):
            axes[i].imshow(images[i], cmap='gray')
            axes[i].set_title(titles[i])
            axes[i].axis('off')
        else:
            axes[i].axis('off')  # 隐藏空白子图

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    img_path = "../picture/Java.png"

    try:
        # 1. 加载自定义图像(二值化)
        # 如果不需要二值化,将 is_binary 改为 False
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 按需改为False(处理灰度图)
            resize=(300, 300)
        )
        # 显示加载的原始图像
        plot_images([custom_img], ["加载的自定义图像"], cols=1)

        # 2. 定义结构元素
        kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))  # 3x3十字形
        kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))  # 5x5矩形

        # ===================== 腐蚀运算 =====================
        eroded_cross = cv2.erode(custom_img, kernel_cross, iterations=1)  # 十字形腐蚀1次
        eroded_rect = cv2.erode(custom_img, kernel_rect, iterations=2)  # 矩形腐蚀2次
        # 可视化腐蚀效果
        plot_images(
            [custom_img, eroded_cross, eroded_rect],
            ["原始图像", "3x3十字形腐蚀(1次)", "5x5矩形腐蚀(2次)"],
            rows=1, cols=3
        )

        # ===================== 膨胀运算 =====================
        dilated_cross = cv2.dilate(custom_img, kernel_cross, iterations=1)  # 十字形膨胀1次
        dilated_rect = cv2.dilate(custom_img, kernel_rect, iterations=2)  # 矩形膨胀2次
        # 可视化膨胀效果
        plot_images(
            [custom_img, dilated_cross, dilated_rect],
            ["原始图像", "3x3十字形膨胀(1次)", "5x5矩形膨胀(2次)"],
            rows=1, cols=3
        )

    except FileNotFoundError as e:
        print(f"错误:{e}")
        print("提示:请检查图像路径是否正确,例如:")
        print("  - 绝对路径:C:/Users/用户名/Desktop/测试图.png(Windows)")
        print("  - 绝对路径:/Users/用户名/Desktop/测试图.png(Mac)")
        print("  - 相对路径:./测试图.jpg(图像和代码在同一文件夹)")
    except Exception as e:
        print(f"其他错误:{e}")
效果分析
  • 十字形结构元素的膨胀:仅沿水平 / 垂直方向扩张,孔洞被部分填充;
  • 矩形结构元素的膨胀:沿所有方向扩张,迭代次数越多,扩张越明显,孔洞会被完全填充。
9.2.3 对偶性

腐蚀和膨胀满足对偶性:对图像A的补集进行膨胀,等价于对A进行腐蚀的补集。

代码验证对偶性
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 设置matplotlib支持中文+特殊符号(替换为更兼容的字体)
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']  # 增加兼容字体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
plt.rcParams['font.family'] = 'sans-serif'  # 统一字体族

# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """
    加载自定义图像并预处理
    :param img_path: 图像文件路径
    :param is_binary: 是否转为二值图像(True/False)
    :param resize: 图像缩放尺寸(宽, 高),默认300x300
    :return: 预处理后的图像
    """
    # 读取图像(灰度模式)
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像文件:{img_path},请检查路径是否正确")

    # 缩放图像(统一尺寸,方便对比)
    img = cv2.resize(img, resize)

    # 二值化处理(如果需要)
    if is_binary:
        # 自适应二值化(适配不同亮度的图像)
        _, img = cv2.threshold(
            img, 0, 255,
            cv2.THRESH_BINARY + cv2.THRESH_OTSU  # OTSU自动选阈值
        )
    return img

def plot_images(images, titles, rows=1, cols=2):
    """
    绘制对比图像(修复子图空白问题)
    :param images: 图像列表
    :param titles: 标题列表
    :param rows: 行数
    :param cols: 列数
    """
    # 强制匹配图像数量和子图数量,避免空白
    total_plots = len(images)
    rows = 1 if total_plots <= cols else rows
    cols = total_plots if total_plots < cols else cols

    fig, axes = plt.subplots(rows, cols, figsize=(12, 6))
    axes = axes.flatten() if isinstance(axes, np.ndarray) else [axes]

    # 只绘制有图像的子图,其余隐藏
    for i in range(len(axes)):
        if i < len(images):
            axes[i].imshow(images[i], cmap='gray')
            axes[i].set_title(titles[i])
            axes[i].axis('off')
        else:
            axes[i].axis('off')  # 隐藏空白子图

    plt.tight_layout()
    plt.show()

# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/ChangLi.png"

    try:
        # 1. 加载自定义图像(二值化)
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 灰度图请改为False
            resize=(300, 300)
        )
        # 显示加载的原始图像
        plot_images([custom_img], ["加载的自定义图像"], cols=1)

        # 2. 定义结构元素
        kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))  # 3x3十字形
        kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))    # 5x5矩形

        # ===================== 腐蚀运算 =====================
        eroded_cross = cv2.erode(custom_img, kernel_cross, iterations=1)  # 十字形腐蚀1次
        eroded_rect = cv2.erode(custom_img, kernel_rect, iterations=2)    # 矩形腐蚀2次
        # 可视化腐蚀效果(替换特殊符号)
        plot_images(
            [custom_img, eroded_cross, eroded_rect],
            ["原始图像", "3x3十字形腐蚀(1次)", "5x5矩形腐蚀(2次)"],
            rows=1, cols=3
        )

        # ===================== 膨胀运算 =====================
        dilated_cross = cv2.dilate(custom_img, kernel_cross, iterations=1)  # 十字形膨胀1次
        dilated_rect = cv2.dilate(custom_img, kernel_rect, iterations=2)    # 矩形膨胀2次
        # 可视化膨胀效果
        plot_images(
            [custom_img, dilated_cross, dilated_rect],
            ["原始图像", "3x3十字形膨胀(1次)", "5x5矩形膨胀(2次)"],
            rows=1, cols=3
        )

        # ===================== 腐蚀-膨胀对偶性验证 =====================
        # 计算原始图像的补集
        img_complement = cv2.bitwise_not(custom_img)

        # 验证:(A^c ⊕ S)^c = A ⊖ S → 改用中文描述,避免特殊符号
        dilated_complement = cv2.dilate(img_complement, kernel_cross, iterations=1)
        dilated_complement_complement = cv2.bitwise_not(dilated_complement)
        eroded_original = cv2.erode(custom_img, kernel_cross, iterations=1)

        # 对比结果(理论上像素差异数应为0)
        diff = cv2.absdiff(dilated_complement_complement, eroded_original)
        diff_pixel_count = np.sum(diff)
        print(f"对偶性验证:像素差异数 = {diff_pixel_count}")  # 理想值为0

        # 可视化对偶性验证结果(完全替换特殊符号,消除字体警告)
        plot_images(
            [eroded_original, dilated_complement_complement, diff],
            [
                "原始图像腐蚀结果",
                "补集膨胀后再取补集结果",
                "差值图像(应为全黑)"
            ],
            rows=1, cols=3
        )

    except FileNotFoundError as e:
        print(f"错误:{e}")
        print("提示:请检查图像路径是否正确,例如:")
        print("  - 绝对路径:C:/Users/用户名/Desktop/测试图.png(Windows)")
        print("  - 绝对路径:/Users/用户名/Desktop/测试图.png(Mac)")
        print("  - 相对路径:./测试图.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"其他错误:{e}")
结果分析

差值图像的像素值全为 0,证明腐蚀和膨胀的对偶性成立。

9.3 开运算与闭运算

开运算(Opening)和闭运算(Closing)是基于腐蚀和膨胀的组合运算,分别用于消除亮噪声和暗噪声。

原理
代码实现(含效果对比)
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 设置matplotlib支持中文+兼容字体(消除特殊符号警告)
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'

# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """
    加载自定义图像并预处理
    :param img_path: 图像文件路径
    :param is_binary: 是否转为二值图像(True/False)
    :param resize: 图像缩放尺寸(宽, 高),默认300x300
    :return: 预处理后的图像
    """
    # 读取图像(灰度模式)
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像文件:{img_path},请检查路径是否正确")

    # 缩放图像(统一尺寸,方便对比)
    img = cv2.resize(img, resize)

    # 二值化处理(如果需要)
    if is_binary:
        # 自适应二值化(适配不同亮度的图像)
        _, img = cv2.threshold(
            img, 0, 255,
            cv2.THRESH_BINARY + cv2.THRESH_OTSU  # OTSU自动选阈值
        )
    return img

def plot_images(images, titles, rows=1, cols=2):
    """
    绘制对比图像(修复子图空白问题)
    :param images: 图像列表
    :param titles: 标题列表
    :param rows: 行数
    :param cols: 列数
    """
    # 强制匹配图像数量和子图数量,避免空白
    total_plots = len(images)
    rows = 1 if total_plots <= cols else rows
    cols = total_plots if total_plots < cols else cols

    fig, axes = plt.subplots(rows, cols, figsize=(12, 8))  # 调整画布高度,适配2行2列
    axes = axes.flatten() if isinstance(axes, np.ndarray) else [axes]

    # 只绘制有图像的子图,其余隐藏
    for i in range(len(axes)):
        if i < len(images):
            axes[i].imshow(images[i], cmap='gray')
            axes[i].set_title(titles[i], fontsize=10)
            axes[i].axis('off')
        else:
            axes[i].axis('off')  # 隐藏空白子图

    plt.tight_layout()
    plt.show()

def add_noise(img, noise_ratio=0.01):
    """
    给图像添加椒盐噪声
    :param img: 输入图像(二值/灰度)
    :param noise_ratio: 噪声比例(0~1)
    :return: 含噪声的图像
    """
    noisy_img = img.copy()
    h, w = noisy_img.shape
    # 生成随机噪声掩码
    np.random.seed(42)  # 固定随机种子,结果可复现
    # 亮噪声(盐噪声:像素值设为255)
    salt = np.random.rand(h, w) < noise_ratio
    noisy_img[salt] = 255
    # 暗噪声(椒噪声:像素值设为0)
    pepper = np.random.rand(h, w) < noise_ratio
    noisy_img[pepper] = 0
    return noisy_img

# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/1.jpg"  # 相对路径/绝对路径均可

    try:
        # 1. 加载自定义图像(二值化)
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 灰度图请改为False
            resize=(300, 300)
        )
        # 显示加载的原始图像
        plot_images([custom_img], ["加载的自定义图像"], cols=1)

        # 2. 定义结构元素
        kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))  # 3x3十字形
        kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))    # 5x5矩形

        # ===================== 腐蚀运算 =====================
        eroded_cross = cv2.erode(custom_img, kernel_cross, iterations=1)  # 十字形腐蚀1次
        eroded_rect = cv2.erode(custom_img, kernel_rect, iterations=2)    # 矩形腐蚀2次
        # 可视化腐蚀效果
        plot_images(
            [custom_img, eroded_cross, eroded_rect],
            ["原始图像", "3x3十字形腐蚀(1次)", "5x5矩形腐蚀(2次)"],
            rows=1, cols=3
        )

        # ===================== 膨胀运算 =====================
        dilated_cross = cv2.dilate(custom_img, kernel_cross, iterations=1)  # 十字形膨胀1次
        dilated_rect = cv2.dilate(custom_img, kernel_rect, iterations=2)    # 矩形膨胀2次
        # 可视化膨胀效果
        plot_images(
            [custom_img, dilated_cross, dilated_rect],
            ["原始图像", "3x3十字形膨胀(1次)", "5x5矩形膨胀(2次)"],
            rows=1, cols=3
        )

        # ===================== 腐蚀-膨胀对偶性验证 =====================
        # 计算原始图像的补集
        img_complement = cv2.bitwise_not(custom_img)

        # 验证:(A^c ⊕ S)^c = A ⊖ S
        dilated_complement = cv2.dilate(img_complement, kernel_cross, iterations=1)
        dilated_complement_complement = cv2.bitwise_not(dilated_complement)
        eroded_original = cv2.erode(custom_img, kernel_cross, iterations=1)

        # 对比结果(理论上像素差异数应为0)
        diff = cv2.absdiff(dilated_complement_complement, eroded_original)
        diff_pixel_count = np.sum(diff)
        print(f"对偶性验证:像素差异数 = {diff_pixel_count}")  # 理想值为0

        # 可视化对偶性验证结果
        plot_images(
            [eroded_original, dilated_complement_complement, diff],
            ["原始图像腐蚀结果", "补集膨胀后再取补集结果", "差值图像(应为全黑)"],
            rows=1, cols=3
        )

        # ===================== 开运算/闭运算 去噪 =====================
        # 生成含椒盐噪声的图像
        noisy_img = add_noise(custom_img, noise_ratio=0.02)  # 2%的椒盐噪声

        # 开运算(先腐蚀后膨胀,消除亮噪声)
        opening = cv2.morphologyEx(noisy_img, cv2.MORPH_OPEN, kernel_rect)
        # 闭运算(先膨胀后腐蚀,填充暗噪声/孔洞)
        closing = cv2.morphologyEx(noisy_img, cv2.MORPH_CLOSE, kernel_rect)
        # 开闭运算组合(先开后闭,消除所有噪声)
        open_close = cv2.morphologyEx(opening, cv2.MORPH_CLOSE, kernel_rect)

        # 可视化去噪效果(2行2列布局)
        plot_images(
            [noisy_img, opening, closing, open_close],
            [
                "含椒盐噪声的图像(2%噪声)",
                "开运算(消除亮噪声)",
                "闭运算(填充暗噪声)",
                "开闭组合(综合去噪)"
            ],
            rows=2, cols=2
        )

    except FileNotFoundError as e:
        print(f"错误:{e}")
        print("提示:请检查图像路径是否正确,例如:")
        print("  - 绝对路径:C:/Users/用户名/Desktop/测试图.png(Windows)")
        print("  - 绝对路径:/Users/用户名/Desktop/测试图.png(Mac)")
        print("  - 相对路径:./测试图.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"其他错误:{e}")
效果分析
  • 开运算:有效消除了图像中的亮噪声(盐噪声),但对暗噪声(椒噪声)无作用;
  • 闭运算:有效填充了小的暗噪声 / 孔洞,但对亮噪声无作用;
  • 开闭组合:先开后闭,可同时消除亮、暗噪声,得到更干净的图像。

9.4 击中 - 击不中变换

原理

击中 - 击不中变换(Hit-or-Miss Transform)是二值形态学中用于形状匹配的核心运算,能够精确检测图像中特定形状的像素位置。

代码实现(检测小矩形的位置)
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 设置matplotlib支持中文+兼容字体
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """加载自定义图像并预处理"""
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像文件:{img_path},请检查路径是否正确")
    img = cv2.resize(img, resize)
    if is_binary:
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return img


def plot_single_window(images, titles):
    """仅显示一个窗口,包含所有对比图(适配击中-击不中变换)"""
    fig, axes = plt.subplots(1, len(images), figsize=(15, 10))
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(title, fontsize=11)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/CSGO.jpg"

    try:
        # 1. 加载并预处理图像
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,
            resize=(300, 300)
        )

        # ===================== 修复:击中-击不中变换(解决形状不匹配) =====================
        # 核心:B1和B2必须是相同尺寸!重新定义模板(统一24x24尺寸)
        target_size = 24  # 模板总尺寸
        inner_size = 20  # 前景核心尺寸
        border = (target_size - inner_size) // 2  # 自动计算边界宽度

        # 方法1:手动构造相同尺寸的前景/背景模板(避免形状不匹配)
        B1 = np.zeros((target_size, target_size), dtype=np.uint8)
        # 中心填充为1(前景模板:20x20实心)
        B1[border:border + inner_size, border:border + inner_size] = 1
        B1 = B1.astype(np.uint8)

        B2 = np.ones((target_size, target_size), dtype=np.uint8)
        # 中心挖空为0(背景模板:24x24空心)
        B2[border:border + inner_size, border:border + inner_size] = 0
        B2 = B2.astype(np.uint8)

        # 击中-击不中变换核心步骤
        erode_B1 = cv2.erode(custom_img, B1)  # 匹配前景
        img_complement = cv2.bitwise_not(custom_img)
        erode_B2 = cv2.erode(img_complement, B2)  # 匹配背景
        hit_miss = cv2.bitwise_and(erode_B1, erode_B2)

        # 标记击中位置
        result_hm = custom_img.copy()
        points = np.where(hit_miss == 255)
        hit_count = len(points[0])
        print(f"击中-击不中变换:共检测到 {hit_count} 个目标位置")

        for y, x in zip(points[0], points[1]):
            cv2.circle(result_hm, (x, y), 5, 127, -1)  # 灰色标记点

        # ===================== 仅显示一个窗口(核心需求) =====================
        plot_single_window(
            images=[custom_img, hit_miss, result_hm],
            titles=[
                "原始图像",
                "击中-击不中结果(白色=匹配位置)",
                f"标记击中位置(共{hit_count}个)"
            ]
        )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
    except Exception as e:
        print(f"其他错误:{e}")
效果分析

击中 - 击不中变换的结果中,仅小矩形的中心位置有白色像素,证明成功检测到目标形状的位置。

9.5 一些基本的形态学算法

基于腐蚀、膨胀、开 / 闭运算,可实现一系列经典的形态学算法,用于提取图像的形状特征。

9.5.1 边界提取
原理

边界提取的核心思想:原始图像 - 腐蚀后的图像,即通过腐蚀收缩前景区域,差值即为边界。

代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 设置matplotlib支持中文+兼容字体(消除警告)
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """加载自定义图像并预处理(二值化+缩放)"""
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像文件:{img_path},请检查路径是否正确")
    # 统一缩放尺寸,方便对比
    img = cv2.resize(img, resize)
    # 自适应二值化(保证边界提取效果)
    if is_binary:
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return img


def plot_single_window(images, titles):
    """仅显示一个窗口,适配边界提取的2列对比图"""
    fig, axes = plt.subplots(1, len(images), figsize=(12, 10))
    axes = axes.flatten() if len(images) > 1 else [axes]

    # 绘制图像,隐藏坐标轴
    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(title, fontsize=12)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/XueNai.png"  # 绝对/相对路径均可

    try:
        # 1. 加载自定义图像
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 边界提取建议保留二值化
            resize=(300, 300)
        )

        # ===================== 边界提取核心逻辑 =====================
        # 定义3x3矩形结构元素(腐蚀用)
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
        # 腐蚀图像(收缩前景,暴露边界)
        eroded = cv2.erode(custom_img, kernel, iterations=1)
        # 原始图像 - 腐蚀图像 = 边界(差值法提取)
        boundary = cv2.subtract(custom_img, eroded)

        # ===================== 仅显示一个窗口(核心需求) =====================
        plot_single_window(
            images=[custom_img, boundary],
            titles=["原始图像", "边界提取结果(3x3矩形核)"]
        )

    except FileNotFoundError as e:
        print(f"错误:{e}")
        print("提示:请检查图像路径,例如:")
        print("  Windows绝对路径:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.5.2 孔洞填充
原理

孔洞是指被前景区域包围的背景区域。填充步骤:

  1. 初始化填充图像(仅在孔洞内设置一个种子点);
  2. 循环对填充图像进行膨胀,并用原始图像的补集约束(避免溢出);
  3. 直到填充图像不再变化。
代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 解决中文显示/特殊符号警告
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """加载自定义图像并预处理(二值化+缩放)"""
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    img = cv2.resize(img, resize)
    # 二值化(保证孔洞填充效果,前景白/背景黑)
    if is_binary:
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return img


def plot_single_window(images, titles):
    """仅显示一个窗口,适配2列对比图"""
    fig, axes = plt.subplots(1, len(images), figsize=(12, 10))
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(title, fontsize=12)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def fill_holes(img):
    """填充二值图像中的孔洞(核心函数)"""
    # 深拷贝避免修改原图像
    img_copy = img.copy()
    h, w = img_copy.shape
    # 步骤1:创建扩展填充掩码(避免漫水填充溢出)
    fill_mask = np.zeros((h + 2, w + 2), dtype=np.uint8)
    # 步骤2:从图像边缘(0,0)漫水填充背景(将背景填充为白色)
    cv2.floodFill(img_copy, fill_mask, (0, 0), 255)
    # 步骤3:裁剪掩码并反转(得到孔洞区域:白色孔洞,黑色背景)
    hole_region = cv2.bitwise_not(fill_mask[1:-1, 1:-1])
    # 步骤4:合并原始图像+孔洞区域(填充孔洞)
    filled_img = cv2.bitwise_or(img, hole_region)
    return filled_img


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/TianHuoSanXuanBian.jpg"  # 绝对/相对路径均可

    try:
        # 1. 加载自定义图像
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 孔洞填充必须二值化
            resize=(300, 300)
        )

        # ===================== 孔洞填充核心逻辑 =====================
        filled_img = fill_holes(custom_img)

        # ===================== 仅显示一个窗口 =====================
        plot_single_window(
            images=[custom_img, filled_img],
            titles=["原始图像(含孔洞)", "孔洞填充结果"]
        )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.5.3 提取连通分量
原理

连通分量是指图像中相邻的前景像素组成的区域。OpenCV 提供了connectedComponents函数,可提取图像中的所有连通分量,并标记每个分量的编号。

代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 解决中文显示/颜色渲染警告
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """加载自定义图像并预处理(二值化+缩放)"""
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    img = cv2.resize(img, resize)
    # 二值化(保证连通分量提取效果,前景白/背景黑)
    if is_binary:
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return img


def plot_single_window(images, titles):
    """仅显示一个窗口,适配2列对比图"""
    fig, axes = plt.subplots(1, len(images), figsize=(12, 10))
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        # 处理灰度/彩色图像的显示适配
        cmap = 'gray' if len(img.shape) == 2 else None
        axes[i].imshow(img, cmap=cmap)
        axes[i].set_title(title, fontsize=12)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/1.jpg"  # 绝对/相对路径均可

    try:
        # 1. 加载自定义图像
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 连通分量提取建议二值化
            resize=(300, 300)
        )

        # ===================== 连通分量提取核心逻辑 =====================
        # 提取连通分量(8连通,更符合视觉直觉)
        # 返回值:分量数量、标签矩阵、统计信息、中心坐标
        num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(
            custom_img,
            connectivity=8  # 可选4/8连通,8连通包含斜向相邻
        )

        # 生成彩色标记图像(背景为黑,不同分量不同颜色)
        # 生成随机颜色(避免颜色重复)
        np.random.seed(42)  # 固定随机种子,结果可复现
        colors = np.random.randint(0, 255, (num_labels, 3), dtype=np.uint8)
        colors[0] = [0, 0, 0]  # 背景分量(标签0)设为黑色

        # 绘制彩色连通分量图像
        label_img = np.zeros((custom_img.shape[0], custom_img.shape[1], 3), dtype=np.uint8)
        for i in range(1, num_labels):  # 跳过背景(标签0)
            label_img[labels == i] = colors[i]

        # 打印连通分量详细信息
        print("=" * 50)
        print(f"连通分量分析结果(排除背景):")
        print(f"总连通分量数量:{num_labels - 1}")
        print("-" * 50)
        for i in range(1, num_labels):
            area = stats[i][4]  # 分量面积(像素数)
            cx, cy = centroids[i]  # 分量中心坐标
            print(f"分量{i}:面积={area}像素, 中心坐标=({cx:.2f}, {cy:.2f})")
        print("=" * 50)

        # ===================== 仅显示一个窗口 =====================
        plot_single_window(
            images=[custom_img, label_img],
            titles=["原始图像", f"连通分量标记(共{num_labels - 1}个分量)"]
        )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.5.4 凸壳
原理

凸壳是指包含目标区域的最小凸多边形。OpenCV 通过convexHull函数实现凸壳提取。

代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 解决中文显示/绘图警告
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """加载自定义图像并预处理(二值化+缩放)"""
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    img = cv2.resize(img, resize)
    # 二值化(保证轮廓/凸壳提取效果,前景白/背景黑)
    if is_binary:
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return img


def plot_single_window(images, titles):
    """仅显示一个窗口,适配2列对比图"""
    fig, axes = plt.subplots(1, len(images), figsize=(12, 10))
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(title, fontsize=12)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def convex_hull(img):
    """提取图像中前景区域的凸壳(核心函数)"""
    # 深拷贝避免修改原图像
    img_copy = img.copy()
    # 步骤1:查找前景轮廓(仅提取最外层轮廓)
    contours, _ = cv2.findContours(
        img_copy,
        cv2.RETR_EXTERNAL,  # 只提取最外层轮廓
        cv2.CHAIN_APPROX_SIMPLE  # 压缩轮廓点(减少计算量)
    )
    # 步骤2:创建凸壳图像(初始全黑)
    hull_img = np.zeros_like(img_copy)
    # 步骤3:对每个轮廓提取凸壳并绘制
    for cnt in contours:
        if len(cnt) >= 3:  # 至少3个点才能构成凸壳
            hull = cv2.convexHull(cnt)  # 提取凸壳
            cv2.drawContours(hull_img, [hull], 0, 255, -1)  # 填充凸壳区域
    return hull_img


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/Java.png"  # 绝对/相对路径均可

    try:
        # 1. 加载自定义图像
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 凸壳提取必须二值化
            resize=(300, 300)
        )

        # ===================== 凸壳提取核心逻辑 =====================
        hull_img = convex_hull(custom_img)

        # ===================== 仅显示一个窗口 =====================
        plot_single_window(
            images=[custom_img, hull_img],
            titles=["原始图像", "凸壳提取结果(填充)"]
        )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.5.5 细化
原理

细化(Thinning)是将前景区域收缩为单像素宽度的骨架,保留目标的拓扑结构。OpenCV 通过ximgproc.thinning实现(需安装opencv-contrib-python)。

代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import cv2.ximgproc as ximgproc  # 细化算法需导入ximgproc模块
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 解决中文显示/绘图警告
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """加载自定义图像并预处理(二值化+缩放)"""
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    img = cv2.resize(img, resize)
    # 二值化(细化算法要求前景白/背景黑)
    if is_binary:
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return img


def plot_single_window(images, titles):
    """仅显示一个窗口,适配2列对比图"""
    fig, axes = plt.subplots(1, len(images), figsize=(12, 10))
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(title, fontsize=12)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/1.jpg"  # 绝对/相对路径均可

    try:
        # 1. 加载自定义图像
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,  # 细化算法必须二值化
            resize=(300, 300)
        )

        # ===================== 图像细化(骨架提取)核心逻辑 =====================
        # 方法1:Zhang-Suen细化算法(经典骨架提取)
        thinned_zhangsuen = ximgproc.thinning(
            custom_img,
            thinningType=ximgproc.THINNING_ZHANGSUEN  # Zhang-Suen算法
        )

        # 可选:方法2:Guo-Hall细化算法(另一种经典算法)
        # thinned_guohall = ximgproc.thinning(
        #     custom_img,
        #     thinningType=ximgproc.THINNING_GUOHALL
        # )

        # ===================== 仅显示一个窗口 =====================
        plot_single_window(
            images=[custom_img, thinned_zhangsuen],
            titles=["原始图像", "细化结果(Zhang-Suen骨架)"]
        )

    except ModuleNotFoundError:
        print("错误:缺少opencv-contrib-python模块!")
        print("请执行以下命令安装:")
        print("  pip install opencv-contrib-python")
    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.5.6 粗化
原理

粗化(Thickening)是细化的逆运算,将单像素宽度的骨架扩张为一定宽度的区域,保留拓扑结构。

代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import cv2.ximgproc as ximgproc
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """加载自定义图像并预处理(二值化+缩放)"""
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    img = cv2.resize(img, resize)
    if is_binary:
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return img


def plot_single_window(images, titles):
    """仅显示一个窗口,适配2列对比图"""
    fig, axes = plt.subplots(1, len(images), figsize=(12, 10))
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(title, fontsize=12)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def thicken(img, original_img, iterations=1):
    """
    粗化运算(细化的逆操作)
    :param img: 待粗化的细化图像
    :param original_img: 原始图像(用于限制粗化范围,避免过度扩张)
    :param iterations: 粗化迭代次数(次数越多,线条越粗)
    :return: 粗化后的图像
    """
    # 3x3十字形结构元素(沿上下左右+斜向扩张)
    kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
    thickened = img.copy()

    for _ in range(iterations):
        # 步骤1:膨胀细化图像
        dilated = cv2.dilate(thickened, kernel)
        # 步骤2:限制膨胀范围(仅在原始图像的前景区域内扩张)
        # 修复:将test_img替换为original_img(自定义原始图像)
        thickened = cv2.bitwise_and(dilated, cv2.bitwise_not(original_img)) | thickened

    return thickened


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/Mei.png"

    try:
        # 1. 加载自定义图像
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,
            resize=(300, 300)
        )

        # 2. 先执行细化(获取骨架图像)
        thinned = ximgproc.thinning(
            custom_img,
            thinningType=ximgproc.THINNING_ZHANGSUEN
        )

        # 3. 粗化细化后的图像(核心逻辑)
        # 迭代次数2:线条粗化2次,可根据需求调整
        thickened = thicken(thinned, original_img=custom_img, iterations=2)

        # 4. 仅显示一个窗口(细化 vs 粗化)
        plot_single_window(
            images=[thinned, thickened],
            titles=["细化结果(1像素骨架)", "粗化结果(迭代2次)"]
        )

    except ModuleNotFoundError:
        print("错误:缺少opencv-contrib-python模块!")
        print("安装命令:pip install opencv-contrib-python")
    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:C:/Users/用户名/Desktop/1.jpg 或 ./1.jpg")
    except Exception as e:
        print(f"运行错误:{e}")
9.5.7 骨架
原理

骨架(Skeleton)是目标区域的 “中轴线”,可通过多次腐蚀和开运算的差值叠加得到。

代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 解决中文显示/绘图警告
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_custom_image(img_path, is_binary=True, resize=(300, 300)):
    """加载自定义图像并预处理(二值化+缩放)"""
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    img = cv2.resize(img, resize)
    # 二值化(骨架提取要求前景白/背景黑)
    if is_binary:
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return img


def plot_single_window_with_origin(images, titles):
    """单窗口显示:原图 + 处理结果(适配2列/多列)"""
    fig, axes = plt.subplots(1, len(images), figsize=(15, 10))  # 加宽画布适配多列
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(title, fontsize=12)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def skeleton(img):
    """
    手动实现骨架提取算法(腐蚀+开运算迭代法)
    :param img: 输入二值图像(前景255,背景0)
    :return: 1像素宽的骨架图像
    """
    # 3x3矩形结构元素(腐蚀/开运算用)
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    skeleton_img = np.zeros_like(img)
    temp = img.copy()  # 临时图像,用于迭代

    # 迭代提取骨架,直到临时图像全黑
    while True:
        # 步骤1:腐蚀临时图像
        eroded = cv2.erode(temp, kernel)
        # 步骤2:开运算(腐蚀+膨胀)
        opened = cv2.dilate(eroded, kernel)
        # 步骤3:计算差值(当前迭代的骨架部分)
        diff = cv2.subtract(temp, opened)
        # 步骤4:叠加到骨架图像
        skeleton_img = cv2.bitwise_or(skeleton_img, diff)
        # 步骤5:更新临时图像为腐蚀后的结果
        temp = eroded.copy()

        # 终止条件:临时图像无前景像素(全黑)
        if np.sum(temp) == 0:
            break

    return skeleton_img


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/DiPingXian.png"  # 绝对/相对路径均可

    try:
        # 1. 加载自定义图像(原图)
        custom_img = load_custom_image(
            img_path=img_path,
            is_binary=True,
            resize=(300, 300)
        )

        # 2. 手动提取骨架(核心逻辑)
        skeleton_img = skeleton(custom_img)

        # 3. 单窗口显示:原图 + 骨架提取结果(满足新增要求)
        plot_single_window_with_origin(
            images=[custom_img, skeleton_img],  # 第一列:原图,第二列:骨架
            titles=["读取的原始图像", "手动骨架提取结果(1像素宽)"]
        )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.5.8 裁剪
原理

裁剪(Pruning)是去除骨架中的 “毛刺”(短分支),保留主要的骨架结构。

代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 解决中文显示/绘图警告
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_image(img_path, resize=(300, 300)):
    """
    加载图像(返回彩色原图 + 灰度二值图)
    :param img_path: 图像路径
    :param resize: 缩放尺寸
    :return: (彩色原图, 灰度二值图)
    """
    # 1. 加载彩色原图
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    # BGR转RGB(适配matplotlib显示)
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 生成灰度二值图(用于骨架提取)
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)
    _, binary_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    return color_img, binary_img


def plot_multi_image_window(images, titles):
    """单窗口展示多张图像(适配4列:彩色原图+灰度图+骨架+裁剪骨架)"""
    fig, axes = plt.subplots(1, len(images), figsize=(20, 6))  # 加宽画布适配4列
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        # 自动适配彩色/灰度图像显示
        cmap = 'gray' if len(img.shape) == 2 else None
        axes[i].imshow(img, cmap=cmap)
        axes[i].set_title(title, fontsize=11)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def skeleton(img):
    """手动提取图像骨架(腐蚀+开运算迭代法)"""
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    skeleton_img = np.zeros_like(img)
    temp = img.copy()

    while True:
        eroded = cv2.erode(temp, kernel)
        opened = cv2.dilate(eroded, kernel)
        diff = cv2.subtract(temp, opened)
        skeleton_img = cv2.bitwise_or(skeleton_img, diff)
        temp = eroded.copy()

        if np.sum(temp) == 0:
            break

    return skeleton_img


def prune_skeleton(skeleton_img, min_length=10):
    """裁剪骨架的短分支(保留长轮廓,去除噪声小分支)"""
    # 查找骨架的所有外部轮廓
    contours, _ = cv2.findContours(skeleton_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    pruned = np.zeros_like(skeleton_img)

    # 仅保留长度大于min_length的轮廓
    for cnt in contours:
        # 计算轮廓长度(弧长)
        contour_length = cv2.arcLength(cnt, closed=False)
        if contour_length > min_length:
            cv2.drawContours(pruned, [cnt], 0, 255, 1)

    return pruned


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/XueNai.png"  # 绝对/相对路径均可

    try:
        # 1. 加载图像:彩色原图 + 灰度二值图
        color_img, binary_img = load_image(img_path, resize=(300, 300))

        # 2. 提取原始骨架
        skeleton_img = skeleton(binary_img)

        # 3. 裁剪骨架短分支(最小长度20,可调整)
        pruned_skeleton = prune_skeleton(skeleton_img, min_length=20)

        # 4. 单窗口展示所有图像(满足需求:彩色原图+灰度图+骨架+裁剪骨架)
        plot_multi_image_window(
            images=[color_img, binary_img, skeleton_img, pruned_skeleton],
            titles=[
                "原始彩色图像",
                "灰度二值化图像",
                "原始骨架(含短分支)",
                "裁剪后的骨架(最小长度20)"
            ]
        )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")

9.6 形态学重建

形态学重建是基于 “标记图像” 和 “掩膜图像” 的迭代运算,用于精确恢复图像的结构特征。

9.6.1 测地膨胀和腐蚀
原理
  • 测地膨胀:在掩膜图像的约束下,对标记图像进行膨胀,膨胀结果不能超出掩膜图像的范围;
  • 测地腐蚀:在掩膜图像的约束下,对标记图像进行腐蚀,腐蚀结果不能小于掩膜图像的范围。
代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 解决中文显示/绘图警告
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_image(img_path, resize=(300, 300)):
    """
    加载图像(返回彩色原图 + 灰度二值图)
    :param img_path: 图像路径
    :param resize: 缩放尺寸
    :return: (彩色原图RGB, 灰度二值图)
    """
    # 1. 加载彩色原图(BGR转RGB适配matplotlib)
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 生成灰度二值图(前景255,背景0)
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)
    _, binary_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    return color_img, binary_img


def plot_multi_image_window(images, titles):
    """单窗口展示多张图像(适配5列:彩色+灰度+标记+膨胀+腐蚀)"""
    fig, axes = plt.subplots(1, len(images), figsize=(25, 6))  # 加宽画布适配5列
    axes = axes.flatten() if len(images) > 1 else [axes]

    for i, (img, title) in enumerate(zip(images, titles)):
        # 自动适配彩色/灰度图像显示
        cmap = 'gray' if len(img.shape) == 2 else None
        axes[i].imshow(img, cmap=cmap)
        axes[i].set_title(title, fontsize=10)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def geodesic_dilation(marker, mask, kernel, iterations=1):
    """
    测地膨胀(在掩膜mask范围内膨胀marker)
    :param marker: 标记图像(初始种子区域)
    :param mask: 掩膜图像(膨胀范围限制)
    :param kernel: 结构元素
    :param iterations: 迭代次数
    :return: 测地膨胀结果
    """
    dilated = marker.copy()
    for _ in range(iterations):
        # 膨胀后与掩膜求交,限制膨胀范围
        dilated = cv2.dilate(dilated, kernel)
        dilated = cv2.bitwise_and(dilated, mask)
    return dilated


def geodesic_erosion(marker, mask, kernel, iterations=1):
    """
    测地腐蚀(修复原逻辑错误:在掩膜补集范围内腐蚀marker)
    :param marker: 标记图像(初始种子区域)
    :param mask: 掩膜图像(腐蚀范围限制)
    :param kernel: 结构元素
    :param iterations: 迭代次数
    :return: 测地腐蚀结果
    """
    eroded = marker.copy()
    mask_complement = cv2.bitwise_not(mask)  # 掩膜补集
    for _ in range(iterations):
        # 腐蚀后与掩膜补集求并,限制腐蚀范围
        eroded = cv2.erode(eroded, kernel)
        eroded = cv2.bitwise_or(eroded, mask_complement)
    return eroded


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/GaoDa.png"  # 绝对/相对路径均可

    try:
        # 1. 加载图像:彩色原图 + 灰度二值图
        color_img, binary_img = load_image(img_path, resize=(300, 300))

        # 2. 定义标记图像(初始种子:图像中心小矩形)
        marker = np.zeros_like(binary_img)
        h, w = binary_img.shape
        # 中心矩形:避免超出图像边界(适配300x300图像)
        center_x, center_y = w // 2, h // 2
        cv2.rectangle(
            marker,
            (center_x - 30, center_y - 30),  # 左上角
            (center_x + 30, center_y + 30),  # 右下角
            255, -1  # 白色填充
        )

        # 3. 定义掩膜图像(原始灰度二值图)
        mask = binary_img.copy()

        # 4. 定义结构元素(3x3十字形)
        kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))

        # 5. 执行测地膨胀(迭代5次)
        geo_dilate = geodesic_dilation(marker, mask, kernel, iterations=5)

        # 6. 执行测地腐蚀(迭代5次,修复原逻辑错误)
        geo_erode = geodesic_erosion(marker, mask, kernel, iterations=5)

        # 7. 单窗口展示所有图像(满足需求:彩色+灰度+标记+膨胀+腐蚀)
        plot_multi_image_window(
            images=[color_img, binary_img, marker, geo_dilate, geo_erode],
            titles=[
                "原始彩色图像",
                "灰度二值化图像(掩膜)",
                "标记图像(中心矩形种子)",
                "测地膨胀(5次迭代)",
                "测地腐蚀(5次迭代)"
            ]
        )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.6.2 膨胀和腐蚀形态学重建
原理
  • 膨胀形态学重建:对标记图像进行迭代测地膨胀,直到图像不再变化;
  • 腐蚀形态学重建:对标记图像进行迭代测地腐蚀,直到图像不再变化。
代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
# 解决中文显示/绘图警告
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_image(img_path, resize=(300, 300)):
    """加载图像(返回彩色原图RGB + 灰度二值图)"""
    # 1. 加载彩色原图(BGR转RGB适配matplotlib)
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 生成灰度二值图(前景255,背景0)
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)
    _, binary_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    return color_img, binary_img


def plot_grid_window(images, titles, rows=2, cols=3):
    """
    网格布局展示图像(2行3列,适配需求)
    :param images: 图像列表(最多6张,按行优先排列)
    :param titles: 标题列表(与图像一一对应)
    :param rows: 行数(固定2行)
    :param cols: 列数(固定3列)
    """
    fig, axes = plt.subplots(rows, cols, figsize=(15, 10))  # 调整画布为2行3列尺寸
    axes = axes.flatten()  # 展平为一维数组,方便遍历

    # 绘制图像(不足6张时,剩余子图隐藏)
    for i, (img, title) in enumerate(zip(images, titles)):
        if i >= len(axes):
            break
        # 自动适配彩色/灰度图像显示
        cmap = 'gray' if len(img.shape) == 2 else None
        axes[i].imshow(img, cmap=cmap)
        axes[i].set_title(title, fontsize=10)
        axes[i].axis('off')

    # 隐藏未使用的子图
    for i in range(len(images), len(axes)):
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def morph_reconstruction_dilation(marker, mask, kernel):
    """
    膨胀形态学重建(迭代直到收敛)
    :param marker: 标记图像(初始种子)
    :param mask: 掩膜图像(重建范围限制)
    :param kernel: 结构元素
    :return: 重建结果
    """
    prev = np.zeros_like(marker)
    curr = marker.copy()
    # 迭代直到前后两次结果一致(收敛)
    while not np.array_equal(curr, prev):
        prev = curr.copy()
        curr = cv2.dilate(curr, kernel)
        curr = cv2.bitwise_and(curr, mask)  # 限制在掩膜范围内
    return curr


def morph_reconstruction_erosion(marker, mask, kernel):
    """
    腐蚀形态学重建(迭代直到收敛,修复逻辑)
    :param marker: 标记图像(初始种子)
    :param mask: 掩膜图像(重建范围限制)
    :param kernel: 结构元素
    :return: 重建结果
    """
    prev = np.ones_like(marker) * 255  # 初始化为全白
    curr = marker.copy()
    mask_complement = cv2.bitwise_not(mask)  # 掩膜补集
    # 迭代直到前后两次结果一致(收敛)
    while not np.array_equal(curr, prev):
        prev = curr.copy()
        curr = cv2.erode(curr, kernel)
        curr = cv2.bitwise_or(curr, mask_complement)  # 限制在掩膜补集范围内
    return curr


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/AALi.jpg"  # 绝对/相对路径均可

    try:
        # 1. 加载图像:彩色原图 + 灰度二值图
        color_img, binary_img = load_image(img_path, resize=(300, 300))

        # 2. 定义标记图像(中心矩形种子,避免越界)
        marker = np.zeros_like(binary_img)
        h, w = binary_img.shape
        center_x, center_y = w // 2, h // 2
        cv2.rectangle(
            marker,
            (center_x - 30, center_y - 30),  # 左上角
            (center_x + 30, center_y + 30),  # 右下角
            255, -1  # 白色填充
        )

        # 3. 定义掩膜和结构元素
        mask = binary_img.copy()
        kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))

        # 4. 执行形态学重建
        recon_dilate = morph_reconstruction_dilation(marker, mask, kernel)  # 膨胀重建
        recon_erosion = morph_reconstruction_erosion(marker, mask, kernel)  # 腐蚀重建

        # 5. 2行3列网格展示(满足布局需求)
        # 图像列表:彩色原图、灰度图、标记图 | 膨胀重建、腐蚀重建(第6列留空)
        images = [color_img, binary_img, marker, recon_dilate, recon_erosion]
        titles = [
            "原始彩色图像",
            "灰度二值化图像(掩膜)",
            "标记图像(中心种子)",
            "膨胀形态学重建",
            "腐蚀形态学重建"
        ]
        plot_grid_window(images, titles, rows=2, cols=3)

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/1.jpg")
        print("  相对路径:./1.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.6.3 应用实例:图像修复
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_image(img_path, resize=(300, 300)):
    """加载图像(返回彩色原图RGB + 灰度二值图)"""
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)
    _, binary_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    return color_img, binary_img


def plot_grid_window(images, titles, rows=2, cols=3):
    """2行3列网格布局展示图像(避免单行拉伸)"""
    fig, axes = plt.subplots(rows, cols, figsize=(15, 10))
    axes = axes.flatten()

    # 绘制图像
    for i, (img, title) in enumerate(zip(images, titles)):
        if i >= len(axes):
            break
        cmap = 'gray' if len(img.shape) == 2 else None
        axes[i].imshow(img, cmap=cmap)
        axes[i].set_title(title, fontsize=10)
        axes[i].axis('off')

    # 隐藏未使用的子图
    for i in range(len(images), len(axes)):
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def morph_reconstruction_dilation(marker, mask, kernel):
    """膨胀形态学重建(迭代收敛)"""
    prev = np.zeros_like(marker)
    curr = marker.copy()
    while not np.array_equal(curr, prev):
        prev = curr.copy()
        curr = cv2.dilate(curr, kernel)
        curr = cv2.bitwise_and(curr, mask)
    return curr


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 替换为你的图像路径 ----------------
    img_path = "../picture/ChuYin.png"

    try:
        # 1. 加载图像:彩色原图 + 灰度二值图
        color_img, binary_img = load_image(img_path, resize=(300, 300))

        # 2. 模拟图像破损(挖掉左上角矩形区域)
        damaged_img = binary_img.copy()
        # 破损区域:适配300x300图像,避免越界
        cv2.rectangle(damaged_img, (50, 50), (120, 120), 0, -1)  # 黑色填充破损区域

        # 3. 定义修复用标记图像(右下角完整小矩形种子)
        marker = np.zeros_like(damaged_img)
        h, w = damaged_img.shape
        cv2.rectangle(marker, (w - 80, h - 80), (w - 20, h - 20), 255, -1)

        # 4. 定义修复掩膜(受损图像的补集,约束重建范围)
        mask = cv2.bitwise_not(damaged_img)

        # 5. 结构元素(3x3十字形)
        kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))

        # 6. 形态学重建修复
        repaired = morph_reconstruction_dilation(marker, mask, kernel)
        repaired_img = cv2.bitwise_or(damaged_img, repaired)  # 合并修复结果

        # 7. 2行3列展示所有关键图像
        images = [
            color_img,  # 第1行1列:原始彩色图
            binary_img,  # 第1行2列:完整灰度图
            damaged_img,  # 第1行3列:受损图像
            marker,  # 第2行1列:修复标记
            mask,  # 第2行2列:修复掩膜
            repaired_img  # 第2行3列:修复结果
        ]
        titles = [
            "原始彩色图像",
            "完整灰度二值图像",
            "模拟受损图像(挖除矩形)",
            "修复标记(右下角种子)",
            "修复掩膜(受损图像补集)",
            "形态学重建修复结果"
        ]
        plot_grid_window(images, titles, rows=2, cols=3)

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:C:/Users/用户名/Desktop/1.jpg 或 ./1.jpg")
    except Exception as e:
        print(f"运行错误:{e}")

9.7 二值图像形态学运算

9.8 灰度级形态学

灰度级形态学将二值形态学的运算扩展到灰度图像,核心思想是将像素值的比较替代二值图像的集合运算。

9.8.1 灰度腐蚀和膨胀
原理
代码实现(含效果对比)
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_and_add_noise(img_path, noise_strength=20, resize=(300, 300)):
    """
    读取自定义图像并添加高斯噪声
    :param img_path: 用户传入的图像路径
    :param noise_strength: 高斯噪声强度(越大噪声越明显)
    :param resize: 图像缩放尺寸
    :return: (原始彩色图RGB, 原始灰度图, 加噪灰度图)
    """
    # 1. 读取彩色图像(BGR转RGB适配matplotlib显示)
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径是否正确!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 转换为灰度图
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)

    # 3. 添加高斯噪声(避免数据溢出)
    gray_float = gray_img.astype(np.float32)
    noise = np.random.normal(0, noise_strength, gray_float.shape).astype(np.float32)
    noisy_gray = gray_float + noise
    noisy_gray = np.clip(noisy_gray, 0, 255).astype(np.uint8)

    return color_img, gray_img, noisy_gray


def plot_grid_window(images, titles, rows=2, cols=3):
    """2行3列网格布局展示图像(适配彩色/灰度图混合显示)"""
    fig, axes = plt.subplots(rows, cols, figsize=(15, 10))
    axes = axes.flatten()

    # 绘制图像(自动适配彩色/灰度图)
    for i, (img, title) in enumerate(zip(images, titles)):
        if i >= len(axes):
            break
        # 彩色图用默认cmap,灰度图强制用gray cmap
        cmap = 'gray' if len(img.shape) == 2 else None
        vmin, vmax = (0, 255) if len(img.shape) == 2 else (None, None)
        axes[i].imshow(img, cmap=cmap, vmin=vmin, vmax=vmax)
        axes[i].set_title(title, fontsize=10)
        axes[i].axis('off')

    # 隐藏未使用的子图
    for i in range(len(images), len(axes)):
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 用户自定义配置 ----------------
    img_path = "../picture/1.jpg"  # 替换为自己的图像路径(绝对/相对均可)
    noise_strength = 20  # 噪声强度,可调整(如10/30)
    kernel_size = 5  # 形态学运算核尺寸

    try:
        # 1. 读取图像并添加高斯噪声
        color_img, original_gray, noisy_gray = load_and_add_noise(
            img_path=img_path,
            noise_strength=noise_strength,
            resize=(300, 300)
        )

        # 2. 定义结构元素(5x5矩形核)
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

        # 3. 执行灰度形态学运算(基于加噪图像)
        gray_erode = cv2.erode(noisy_gray, kernel)  # 灰度腐蚀:降亮度/消亮噪声
        gray_dilate = cv2.dilate(noisy_gray, kernel)  # 灰度膨胀:升亮度/消暗噪声
        gray_open = cv2.morphologyEx(noisy_gray, cv2.MORPH_OPEN, kernel)  # 开运算
        gray_close = cv2.morphologyEx(noisy_gray, cv2.MORPH_CLOSE, kernel)  # 闭运算

        # 4. 2行3列展示所有关键结果(覆盖核心+拓展)
        images = [
            color_img,  # 第1行1列:原始彩色图像
            original_gray,  # 第1行2列:原始灰度图像
            noisy_gray,  # 第1行3列:加噪灰度图像
            gray_erode,  # 第2行1列:灰度腐蚀
            gray_dilate,  # 第2行2列:灰度膨胀
            gray_open  # 第2行3列:灰度开运算(也可替换为gray_close)
        ]
        titles = [
            "原始彩色图像",
            "原始灰度图像",
            f"加噪灰度图像(噪声强度={noise_strength})",
            "灰度腐蚀(降亮度/消除亮噪声)",
            "灰度膨胀(升亮度/消除暗噪声)",
            "灰度开运算(腐蚀+膨胀,消亮噪声)"
        ]
        plot_grid_window(images, titles, rows=2, cols=3)

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/your_image.jpg")
        print("  相对路径:./your_image.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.8.2 灰度开运算和闭运算
原理
代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_and_add_noise(img_path, noise_strength=20, resize=(300, 300)):
    """
    读取自定义图像并添加高斯噪声
    :param img_path: 用户传入的图像路径
    :param noise_strength: 高斯噪声强度(越大噪声越明显)
    :param resize: 图像缩放尺寸
    :return: (原始彩色图, 原始灰度图, 加噪灰度图)
    """
    # 1. 读取彩色图像(BGR转RGB适配matplotlib显示)
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径是否正确!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 转换为灰度图
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)

    # 3. 添加高斯噪声(避免数据溢出)
    # 转换为float32防止计算溢出
    gray_float = gray_img.astype(np.float32)
    # 生成高斯噪声
    noise = np.random.normal(0, noise_strength, gray_float.shape).astype(np.float32)
    # 叠加噪声并裁剪到0-255范围
    noisy_gray = gray_float + noise
    noisy_gray = np.clip(noisy_gray, 0, 255).astype(np.uint8)

    return color_img, gray_img, noisy_gray


def plot_2x2_grid(images, titles):
    """2行2列网格布局展示4张图像"""
    fig, axes = plt.subplots(2, 2, figsize=(12, 10))
    axes = axes.flatten()

    for i, (img, title) in enumerate(zip(images, titles)):
        # 自动适配彩色/灰度图像显示
        cmap = 'gray' if len(img.shape) == 2 else None
        axes[i].imshow(img, cmap=cmap, vmin=0, vmax=255)
        axes[i].set_title(title, fontsize=11)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 用户自定义配置 ----------------
    img_path = "../picture/ALi.jpg"  # 替换为自己的图像路径(绝对/相对均可)
    noise_strength = 20  # 噪声强度,可调整(如10/30)
    kernel_size = 5  # 形态学运算核尺寸

    try:
        # 1. 读取图像并添加高斯噪声
        color_img, original_gray, noisy_gray = load_and_add_noise(
            img_path=img_path,
            noise_strength=noise_strength,
            resize=(300, 300)
        )

        # 2. 定义结构元素(5x5矩形核)
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

        # 3. 执行灰度形态学去噪运算
        gray_opening = cv2.morphologyEx(noisy_gray, cv2.MORPH_OPEN, kernel)  # 消亮噪声
        gray_closing = cv2.morphologyEx(noisy_gray, cv2.MORPH_CLOSE, kernel)  # 消暗噪声
        gray_open_close = cv2.morphologyEx(gray_opening, cv2.MORPH_CLOSE, kernel)  # 全去噪

        # 4. 2行2列展示关键结果(核心:加噪图+开运算+闭运算+组合运算)
        images = [
            noisy_gray,  # 第1行1列:加噪灰度图
            gray_opening,  # 第1行2列:开运算去噪
            gray_closing,  # 第2行1列:闭运算去噪
            gray_open_close  # 第2行2列:开闭组合去噪
        ]
        titles = [
            f"加噪灰度图像(噪声强度={noise_strength})",
            "灰度开运算(消除亮噪声)",
            "灰度闭运算(消除暗噪声)",
            "灰度开闭组合(全噪声去除)"
        ]
        plot_2x2_grid(images, titles)

        # 可选:额外展示原始彩色图+原始灰度图(如需对比)
        # plot_2x2_grid(
        #     images=[color_img, original_gray, noisy_gray, gray_open_close],
        #     titles=["原始彩色图像", "原始灰度图像", "加噪灰度图像", "开闭组合去噪结果"]
        # )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:")
        print("  Windows:C:/Users/用户名/Desktop/your_image.jpg")
        print("  相对路径:./your_image.jpg(图像和代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.8.3 一些基本的灰度级形态学算法
1. 灰度边界提取
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_and_add_noise(img_path, noise_strength=15, resize=(300, 300)):
    """
    读取自定义图像并添加高斯噪声(可选)
    :param img_path: 图像路径
    :param noise_strength: 噪声强度(0则不加噪)
    :param resize: 缩放尺寸
    :return: (原始彩色图RGB, 原始灰度图, 加噪灰度图)
    """
    # 1. 读取彩色图像并转换为RGB
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 转换为灰度图
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)

    # 3. 可选添加高斯噪声(noise_strength=0则不加噪)
    if noise_strength > 0:
        gray_float = gray_img.astype(np.float32)
        noise = np.random.normal(0, noise_strength, gray_float.shape).astype(np.float32)
        noisy_gray = gray_float + noise
        noisy_gray = np.clip(noisy_gray, 0, 255).astype(np.uint8)
    else:
        noisy_gray = gray_img.copy()

    return color_img, gray_img, noisy_gray


def plot_2image_window(images, titles, layout="row"):
    """
    展示2张图像(适配边界提取的对比展示)
    :param layout: row(1行2列)/ col(2行1列)
    """
    if layout == "row":
        # 1行2列(横向布局,适合对比)
        fig, axes = plt.subplots(1, 2, figsize=(12, 10))
    else:
        # 2行1列(纵向布局)
        fig, axes = plt.subplots(2, 1, figsize=(10, 12))
    axes = axes.flatten()

    # 绘制图像(灰度图强制用gray cmap)
    for i, (img, title) in enumerate(zip(images, titles)):
        cmap = 'gray' if len(img.shape) == 2 else None
        axes[i].imshow(img, cmap=cmap, vmin=0, vmax=255)
        axes[i].set_title(title, fontsize=11)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 用户配置 ----------------
    img_path = "../picture/XiaoYan.jpg"  # 替换为自己的图像路径
    noise_strength = 0  # 0=不加噪,>0=加噪(如15)
    kernel_size = 3  # 腐蚀核尺寸(3x3适合提取精细边界)

    try:
        # 1. 读取图像(可选加噪)
        color_img, original_gray, noisy_gray = load_and_add_noise(
            img_path=img_path,
            noise_strength=noise_strength,
            resize=(300, 300)
        )

        # 2. 定义结构元素(3x3矩形核,适合边界提取)
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

        # 3. 灰度腐蚀 + 边界提取(核心逻辑)
        gray_erode = cv2.erode(noisy_gray, kernel)  # 灰度腐蚀
        gray_boundary = cv2.subtract(noisy_gray, gray_erode)  # 原始 - 腐蚀 = 边界

        # 4. 展示结果(1行2列横向对比,更直观)
        # 可选:展示「加噪灰度图 + 边界」 或 「原始灰度图 + 边界」
        images = [noisy_gray, gray_boundary]
        titles = [
            f"灰度图像({'加噪' if noise_strength > 0 else '无噪'})",
            "灰度边界提取结果(原始 - 腐蚀)"
        ]
        plot_2image_window(images, titles, layout="row")

        # 可选:额外展示原始彩色图+原始灰度图+边界(3张图)
        # plot_grid_window(
        #     images=[color_img, original_gray, gray_boundary, np.zeros_like(original_gray)],
        #     titles=["原始彩色图像", "原始灰度图像", "灰度边界提取结果", ""],
        #     rows=2, cols=2
        # )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:C:/Users/用户名/Desktop/your_image.jpg 或 ./your_image.jpg")
    except Exception as e:
        print(f"运行错误:{e}")
2. 灰度顶帽变换(提取亮区域)

顶帽变换 = 原始图像 - 开运算图像,用于提取图像中的亮区域(如文字)。

代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_and_add_noise(img_path, noise_strength=20, resize=(300, 300)):
    """
    读取自定义图像并添加高斯噪声(顶帽变换适合展示噪声/亮细节提取)
    :param img_path: 图像路径
    :param noise_strength: 高斯噪声强度(0=不加噪)
    :param resize: 缩放尺寸
    :return: (原始彩色图RGB, 原始灰度图, 加噪灰度图)
    """
    # 1. 读取彩色图像并转RGB
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 转灰度图
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)

    # 3. 可选添加高斯噪声(顶帽变换对亮噪声提取效果明显)
    if noise_strength > 0:
        gray_float = gray_img.astype(np.float32)
        noise = np.random.normal(0, noise_strength, gray_float.shape).astype(np.float32)
        noisy_gray = gray_float + noise
        noisy_gray = np.clip(noisy_gray, 0, 255).astype(np.uint8)
    else:
        noisy_gray = gray_img.copy()

    return color_img, gray_img, noisy_gray


def plot_2image_compare(images, titles):
    """1行2列布局对比展示2张灰度图(适配顶帽变换结果)"""
    fig, axes = plt.subplots(1, 2, figsize=(12, 10))
    axes = axes.flatten()

    # 灰度图强制用gray cmap,保证亮度对比准确
    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray', vmin=0, vmax=255)
        axes[i].set_title(title, fontsize=11)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 用户配置 ----------------
    img_path = "../picture/CSGO.jpg"  # 替换为自己的图像路径
    noise_strength = 20  # 建议保留噪声(顶帽变换对亮噪声提取效果显著)
    kernel_size = 5  # 顶帽变换核尺寸(5x5适合提取中等尺度亮细节)

    try:
        # 1. 读取图像(添加噪声,凸显顶帽变换效果)
        color_img, original_gray, noisy_gray = load_and_add_noise(
            img_path=img_path,
            noise_strength=noise_strength,
            resize=(300, 300)
        )

        # 2. 定义结构元素(矩形核,适配顶帽变换)
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

        # 3. 灰度顶帽变换(核心逻辑:原始 - 开运算 = 顶帽,提取亮细节/亮噪声)
        gray_tophat = cv2.morphologyEx(noisy_gray, cv2.MORPH_TOPHAT, kernel)

        # 4. 对比展示:原始灰度图 vs 顶帽变换结果
        images = [noisy_gray, gray_tophat]
        titles = [
            f"原始灰度图像(含高斯噪声,强度={noise_strength})",
            "灰度顶帽变换结果(提取亮细节/亮噪声)"
        ]
        plot_2image_compare(images, titles)

        # 可选拓展:同时展示黑帽变换(提取暗细节/暗噪声)
        # gray_blackhat = cv2.morphologyEx(noisy_gray, cv2.MORPH_BLACKHAT, kernel)
        # plot_2image_compare(
        #     images=[gray_tophat, gray_blackhat],
        #     titles=["顶帽变换(亮细节)", "黑帽变换(暗细节)"]
        # )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:C:/Users/用户名/Desktop/your_image.jpg 或 ./your_image.jpg")
    except Exception as e:
        print(f"运行错误:{e}")
3. 灰度黑帽变换(提取暗区域)

黑帽变换 = 闭运算图像 - 原始图像,用于提取图像中的暗区域。

代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_and_add_noise(img_path, noise_strength=20, resize=(300, 300)):
    """
    读取自定义图像并添加高斯噪声(黑帽变换适合展示暗细节/暗噪声提取)
    :param img_path: 图像路径
    :param noise_strength: 高斯噪声强度(0=不加噪)
    :param resize: 缩放尺寸
    :return: (原始彩色图RGB, 原始灰度图, 加噪灰度图)
    """
    # 1. 读取彩色图像并转换为RGB(适配matplotlib显示)
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径是否正确!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 转换为灰度图
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)

    # 3. 可选添加高斯噪声(黑帽变换对暗噪声提取效果显著)
    if noise_strength > 0:
        gray_float = gray_img.astype(np.float32)
        # 生成高斯噪声(包含暗噪声成分)
        noise = np.random.normal(0, noise_strength, gray_float.shape).astype(np.float32)
        noisy_gray = gray_float + noise
        # 裁剪像素值到0-255范围,避免溢出
        noisy_gray = np.clip(noisy_gray, 0, 255).astype(np.uint8)
    else:
        noisy_gray = gray_img.copy()

    return color_img, gray_img, noisy_gray


def plot_2image_compare(images, titles):
    """1行2列布局对比展示2张灰度图(适配黑帽变换结果)"""
    fig, axes = plt.subplots(1, 2, figsize=(12, 10))
    axes = axes.flatten()

    # 灰度图强制使用gray cmap,保证暗细节对比准确
    for i, (img, title) in enumerate(zip(images, titles)):
        axes[i].imshow(img, cmap='gray', vmin=0, vmax=255)
        axes[i].set_title(title, fontsize=11)
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 用户自定义配置 ----------------
    img_path = "../picture/TianHuoSanXuanBian.jpg"
    noise_strength = 20  # 建议保留噪声(凸显黑帽变换提取暗噪声的效果)
    kernel_size = 5  # 黑帽变换核尺寸(5x5适合提取中等尺度暗细节)

    try:
        # 1. 读取图像(添加噪声,强化黑帽变换展示效果)
        color_img, original_gray, noisy_gray = load_and_add_noise(
            img_path=img_path,
            noise_strength=noise_strength,
            resize=(300, 300)
        )

        # 2. 定义结构元素(矩形核,适配黑帽变换)
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

        # 3. 灰度黑帽变换(核心逻辑:闭运算 - 原始图像 = 黑帽,提取暗细节/暗噪声)
        gray_blackhat = cv2.morphologyEx(noisy_gray, cv2.MORPH_BLACKHAT, kernel)

        # 4. 对比展示:原始灰度图 vs 黑帽变换结果
        images = [noisy_gray, gray_blackhat]
        titles = [
            f"原始灰度图像(含高斯噪声,强度={noise_strength})",
            "灰度黑帽变换结果(提取暗细节/暗噪声)"
        ]
        plot_2image_compare(images, titles)

        # 可选拓展:同时对比顶帽(亮细节)和黑帽(暗细节)
        # gray_tophat = cv2.morphologyEx(noisy_gray, cv2.MORPH_TOPHAT, kernel)
        # plot_2image_compare(
        #     images=[gray_tophat, gray_blackhat],
        #     titles=["顶帽变换(提取亮细节)", "黑帽变换(提取暗细节)"]
        # )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径参考:")
        print("  Windows:C:/Users/用户名/Desktop/your_image.jpg")
        print("  相对路径:./your_image.jpg(图像与代码同文件夹)")
    except Exception as e:
        print(f"运行错误:{e}")
9.8.4 灰度级形态学重建
原理

灰度形态学重建的原理与二值形态学重建一致,只是将 “集合运算” 替换为 “像素值比较”。

代码实现
代码语言:javascript
复制
import numpy as np
import cv2
import matplotlib.pyplot as plt

# ===================== 全局配置 =====================
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'


# ===================== 辅助函数 =====================
def load_and_add_noise(img_path, noise_strength=15, resize=(300, 300)):
    """
    读取自定义图像并添加高斯噪声(适配灰度形态学重建)
    :param img_path: 图像路径
    :param noise_strength: 噪声强度(0=不加噪)
    :param resize: 缩放尺寸
    :return: (原始彩色图RGB, 原始灰度图, 加噪灰度图)
    """
    # 1. 读取彩色图像并转RGB
    color_img = cv2.imread(img_path)
    if color_img is None:
        raise FileNotFoundError(f"无法读取图像:{img_path},请检查路径!")
    color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB)
    color_img = cv2.resize(color_img, resize)

    # 2. 转灰度图
    gray_img = cv2.cvtColor(color_img, cv2.COLOR_RGB2GRAY)

    # 3. 可选添加高斯噪声
    if noise_strength > 0:
        gray_float = gray_img.astype(np.float32)
        noise = np.random.normal(0, noise_strength, gray_float.shape).astype(np.float32)
        noisy_gray = gray_float + noise
        noisy_gray = np.clip(noisy_gray, 0, 255).astype(np.uint8)
    else:
        noisy_gray = gray_img.copy()

    return color_img, gray_img, noisy_gray


def plot_2x2_grid(images, titles):
    """2行2列布局展示图像(适配3张核心图+1张空图)"""
    fig, axes = plt.subplots(2, 2, figsize=(12, 10))
    axes = axes.flatten()

    # 绘制图像(灰度图强制用gray cmap,保证数值对比准确)
    for i, (img, title) in enumerate(zip(images, titles)):
        if i >= len(axes):
            break
        cmap = 'gray' if len(img.shape) == 2 else None
        vmin, vmax = (0, 255) if len(img.shape) == 2 else (None, None)
        axes[i].imshow(img, cmap=cmap, vmin=vmin, vmax=vmax)
        axes[i].set_title(title, fontsize=10)
        axes[i].axis('off')

    # 隐藏未使用的子图
    for i in range(len(images), len(axes)):
        axes[i].axis('off')

    plt.tight_layout()
    plt.show()


def gray_morph_reconstruction(marker, mask, kernel):
    """
    灰度形态学重建(膨胀型,优化数值稳定性)
    :param marker: 灰度标记图像(初始种子)
    :param mask: 灰度掩膜图像(重建约束)
    :param kernel: 结构元素
    :return: 重建后的灰度图像
    """
    # 转换为float32避免uint8溢出,保证数值计算准确
    prev = np.zeros_like(marker, dtype=np.float32)
    curr = marker.astype(np.float32)
    mask_float = mask.astype(np.float32)

    # 迭代直到收敛(前后两次结果无差异)
    while not np.allclose(curr, prev, atol=1e-3):  # 浮点精度适配,避免uint8相等判断误差
        prev = curr.copy()
        # 灰度膨胀
        curr = cv2.dilate(curr, kernel)
        # 灰度约束:curr = min(curr, mask)(保证不超过掩膜亮度)
        curr = np.minimum(curr, mask_float)

    # 转换回uint8
    return curr.astype(np.uint8)


# ===================== 主程序 =====================
if __name__ == "__main__":
    # ---------------- 用户配置 ----------------
    img_path = "../picture/Java.png"  # 替换为自己的图像路径
    noise_strength = 15  # 0=不加噪,>0=加噪(凸显重建效果)
    kernel_size = 3  # 重建核尺寸(3x3适合精细重建)

    try:
        # 1. 读取图像(可选加噪)
        color_img, original_gray, noisy_gray = load_and_add_noise(
            img_path=img_path,
            noise_strength=noise_strength,
            resize=(300, 300)
        )

        # 2. 定义结构元素
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

        # 3. 生成灰度标记图像(中心高亮矩形种子,适配300x300图像)
        h, w = noisy_gray.shape
        gray_marker = np.zeros_like(noisy_gray)
        center_x, center_y = w // 2, h // 2
        cv2.rectangle(
            gray_marker,
            (center_x - 25, center_y - 25),  # 左上角
            (center_x + 25, center_y + 25),  # 右下角
            255, -1  # 白色高亮种子
        )

        # 4. 定义灰度掩膜(加噪灰度图)
        gray_mask = noisy_gray.copy()

        # 5. 执行灰度形态学重建(膨胀型)
        gray_recon = gray_morph_reconstruction(gray_marker, gray_mask, kernel)

        # 6. 2行2列展示结果(避免单行3列拉伸)
        images = [
            gray_marker,  # 第1行1列:标记图像
            gray_mask,  # 第1行2列:掩膜图像
            gray_recon,  # 第2行1列:重建结果
            np.zeros_like(gray_marker)  # 第2行2列:空图(隐藏)
        ]
        titles = [
            "灰度标记图像(中心高亮种子)",
            f"灰度掩膜图像({'加噪' if noise_strength > 0 else '无噪'})",
            "灰度形态学重建结果(膨胀型)",
            ""
        ]
        plot_2x2_grid(images, titles)

        # 可选拓展:展示原始彩色图+重建对比
        # plot_2x2_grid(
        #     images=[color_img, original_gray, gray_mask, gray_recon],
        #     titles=["原始彩色图像", "原始灰度图像", "灰度掩膜", "灰度重建结果"]
        # )

    except FileNotFoundError as e:
        print(f"路径错误:{e}")
        print("示例路径:C:/Users/用户名/Desktop/your_image.jpg 或 ./your_image.jpg")
    except Exception as e:
        print(f"运行错误:{e}")

小结

  1. 形态学图像处理以集合论为基础,核心是结构元素和基本运算(腐蚀、膨胀);
  2. 二值形态学适用于简单的形状分析(边界、孔洞、连通分量),灰度形态学适用于复杂的灰度图像增强;
  3. 开 / 闭运算、击中 - 击不中变换是腐蚀 / 膨胀的组合,可实现更复杂的形状操作;
  4. 形态学重建是高级形态学技术,适用于图像修复、特征提取等场景。

参考文献

  1. 《数字图像处理(第四版)》,Rafael C. Gonzalez 等著;
  2. 《OpenCV-Python 中文教程》;
  3. 《形态学图像处理:原理与应用》,阮秋琦 等著。

延伸读物

  1. 《Digital Image Processing Using MATLAB》,Rafael C. Gonzalez 等;
  2. OpenCV 官方文档:https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html
  3. 《图像处理中的数学方法》,章毓晋 著。

习题

  1. 实现自定义的腐蚀 / 膨胀函数(不使用 OpenCV 的内置函数);
  2. 基于形态学运算,实现手写数字的骨架提取和裁剪;
  3. 利用灰度形态学的顶帽变换,提取文档图像中的文字区域;
  4. 对比不同结构元素(十字形、矩形、圆形)对形态学运算结果的影响;
  5. 基于形态学重建,实现破损图像的修复。

总结

本文从基础概念到实战代码,全面讲解了《数字图像处理》第 9 章形态学图像处理的核心内容,所有代码均可直接运行,配套效果对比图和 Mermaid 思维导图 / 流程图,帮助你直观理解形态学运算的原理和应用。

形态学图像处理是数字图像处理的必备技能,掌握它可以解决图像增强、分割、特征提取等一系列实际问题。建议读者动手运行代码,修改参数(如结构元素的大小、迭代次数),观察结果变化,加深对知识点的理解!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 引言
  • 学习目标
  • 9.1 预备知识
    • 核心概念
    • 环境准备
  • 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.6 形态学重建
    • 9.6.1 测地膨胀和腐蚀
      • 原理
      • 代码实现
    • 9.6.2 膨胀和腐蚀形态学重建
      • 原理
      • 代码实现
    • 9.6.3 应用实例:图像修复
  • 9.7 二值图像形态学运算
  • 9.8 灰度级形态学
    • 9.8.1 灰度腐蚀和膨胀
      • 原理
      • 代码实现(含效果对比)
    • 9.8.2 灰度开运算和闭运算
      • 原理
      • 代码实现
    • 9.8.3 一些基本的灰度级形态学算法
      • 1. 灰度边界提取
      • 2. 灰度顶帽变换(提取亮区域)
      • 3. 灰度黑帽变换(提取暗区域)
    • 9.8.4 灰度级形态学重建
      • 原理
      • 代码实现
  • 小结
  • 参考文献
  • 延伸读物
  • 习题
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档