DIP实验五:图像分割-阈值分割+GUI界面

一.图像分割-阈值分割

图像分割:图像分割是指把图像分成互不重叠而又各具特性的子区域,并提取出感兴趣目标的技术和过程。

特性可以是灰度、颜色、纹理等,目标可对应单个区域,也可对应多个区域。

                                     一般图像的处理过程:预处理->图像分割->特征提取

灰度阈值法:根据图像的灰度,设置不同的阈值,来确定有意义的区域或者边界。

二.阈值分割方法

分析二值化的阈值分割

其中,Ti为不同的阈值,Ci为指定的阈值,i=1,2,……,n

 那么该如何选取阈值呢?

阈值的选取是阈值分割技术的关键。

1.人工阈值分割

根据图像的直方图特性,人为给出阈值

代码:

# 导入OpenCV库,用于计算机视觉任务,如图像读取、处理等
import cv2
# 导入matplotlib库的pyplot模块,用于绘制和显示图像、图表等
import matplotlib.pyplot as plt
# 设置matplotlib的字体为黑体,解决中文显示乱码问题
plt.rcParams['font.sans-serif']=['SimHei']
# 使用cv2.imread函数读取指定路径的图像文件 '008.jpg',参数0表示以灰度模式读取
img=cv2.imread('008.jpg',0)
# 使用cv2.threshold函数对灰度图像进行二值化处理,阈值为130,最大值为255,采用二进制阈值模式
# 函数返回两个值,第一个值为阈值,第二个值为处理后的二值图像,这里只取二值图像赋值给img_b
_,img_b=cv2.threshold(img,130,255,cv2.THRESH_BINARY)
# 使用plt.subplot函数创建一个1行3列的子图布局,并选择第1个子图进行后续操作
plt.subplot(131)
# 使用plt.imshow函数显示灰度图像,指定颜色映射为'gray'
plt.imshow(img,'gray')
# 设置当前子图的标题为'原图'
plt.title('原图')
# 关闭当前子图的坐标轴显示
plt.axis('off')
# 使用plt.subplot函数选择1行3列子图布局中的第2个子图
plt.subplot(132)
# 使用cv2.calcHist函数计算灰度图像的直方图
# [img]表示输入图像列表,[0]表示计算第0个通道(灰度图只有一个通道),None表示不使用掩码,[256]表示直方图的bin数量,[0,255]表示像素值范围
hist=cv2.calcHist([img],[0],None,[256],[0,255])
# 使用plt.plot函数绘制计算得到的直方图
plt.plot(hist)
# 设置当前子图的标题为'灰度直方图'
plt.title('灰度直方图')
# 使用plt.subplot函数选择1行3列子图布局中的第3个子图
plt.subplot(133)
# 使用plt.imshow函数显示二值化处理后的图像,指定颜色映射为'gray'
plt.imshow(img_b,'gray')
# 设置当前子图的标题为'人工阈值分割图T=130'
plt.title('人工阈值分割图T=130')
# 关闭当前子图的坐标轴显示
plt.axis('off')
# 显示所有子图
plt.show()

运行结果: 

2.根据直方图谷点确定阈值(直方图双峰法)

双峰法不适用的情况?

(1)不具有双峰特性:

单峰、目标和背景有重叠等,如:目标和背景的灰度分布过于分散,目标和背景的灰度有部分交错。

(2)双峰间的谷比较宽广而平坦

代码:

# 导入 OpenCV 库,用于图像处理
import cv2
# 导入 matplotlib 库中的 pyplot 模块,用于绘图
import matplotlib.pyplot as plt
# 导入 numpy 库,用于数值计算
import numpy as np

# 设置 matplotlib 的字体为黑体,用于正确显示中文
plt.rcParams['font.sans-serif'] = ['SimHei']
# 使用 OpenCV 的 imread 函数读取指定路径的图像文件 '008.jpg',并以灰度模式(参数 0)读取
img = cv2.imread('006.jpg', 0)
# 使用 matplotlib 的 hist 函数绘制图像的直方图
# img.ravel() 将图像数组展平为一维数组
# 256 表示直方图的 bin 数量
# n 存储每个 bin 中的像素数量
n, bins, _ = plt.hist(img.ravel(), 256, range=(0, 255))
# 使用 numpy 的 where 函数找出直方图中像素数量最多的 bin 的索引
l_ma = np.where(n == np.max(n))
# 取第一个索引作为 f1
f1 = l_ma[0][0]
# 初始化变量 temp,用于存储临时最大值
temp = 0
# 遍历 0 到 255 的像素值
for i in range(256):
    # 计算当前像素值 i 与 f1 差值的平方乘以该像素值对应的直方图 bin 中的数量
    temp1 = np.power(i - f1, 2) * n[i]

# 比较 temp1 和 temp 的大小,如果 temp1 更大,则更新 temp 的值
    if temp1 > temp:
       temp = temp1
       f2 = i
# 如果 f1 大于 f2,则交换它们的值
if f1 > f2:
    f1, f2 = f2, f1
print(f1,f2)
# 在 f1 到 f2 的范围内找出直方图中像素数量最少的 bin 的索引
l_mi = np.where(n[f1:f2] == np.min(n[f1:f2]))
# 计算阈值 T,为 f1 加上最小 bin 的偏移量
T = f1 + l_mi[0][0]
print(T)
# 使用 OpenCV 的 threshold 函数对图像进行二值化处理
# T 为阈值,255 为最大值,cv2.THRESH_BINARY 为二值化方法
# 返回值中第一个为阈值,第二个为二值化后的图像
_, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)

plt.figure(figsize=(10,5))  # 设置窗口宽度

plt.subplot(121)
plt.imshow(img, 'gray')
plt.title('原图')
plt.axis('off')

plt.subplot(122)
plt.imshow(img_b, 'gray')
plt.title(f'分割图(T={T})')
plt.axis('off')

plt.tight_layout()  # 调整子图间距
plt.show()

运行结果:

            

应该怎么解决上述问题呢?

(1)迭代/全局阈值法 (2)最小误差阈值 (3)最大方差阈值

3.迭代阈值分割

1)选取初始阈值T

2)根据阈值T,把图像的像素按灰度分成两组R1和R2

3)计算两组像素的平均灰度值,记为u1和u2

4)更新阈值T=(u1+u2)/2

5)重复2),直到u1和u不发生变化

初始阈值不可选择图像的平均灰度值 

代码:

# 导入 OpenCV 库,用于图像处理相关操作
import cv2
# 导入 NumPy 库,用于进行高效的数值计算
import numpy as np
# 从 matplotlib 库中导入 pyplot 模块,并重命名为 plt,用于绘图
from matplotlib import pyplot as plt

# 设置 matplotlib 的字体为黑体,解决中文显示乱码问题
plt.rcParams['font.sans-serif'] = ['SimHei']
# 使用 OpenCV 的 imread 函数读取指定路径的图像 '008.jpg',第二个参数 0 表示以灰度模式读取
img = cv2.imread('008.jpg', 0)
# 计算图像像素值的平均值,并将结果转换为整数,作为初始阈值 T
T = int(np.mean(img))

# 进入无限循环,通过迭代的方式不断更新阈值,直到满足退出条件
while True:
    # 计算图像中像素值大于等于当前阈值 T 的所有像素的平均值
    m1 = np.mean(img[img >= T])
    # 计算图像中像素值小于当前阈值 T 的所有像素的平均值
    m2 = np.mean(img[img < T])
    # 判断新计算出的阈值 (m1 + m2) / 2 与当前阈值 T 的差值的绝对值是否小于 20
    if abs((m1 + m2) / 2 - T) < 20:
        # 若满足条件,跳出循环,结束阈值迭代过程
        break
    else:
        # 若不满足条件,将新计算出的阈值 (m1 + m2) / 2 转换为整数,更新当前阈值 T
        T = int((m1 + m2) / 2)

# 使用 OpenCV 的 threshold 函数对图像进行二值化处理
# T 为阈值,255 为最大像素值,cv2.THRESH_BINARY 表示二值化方法
# 函数返回两个值,第一个为阈值(此处用 _ 占位表示不使用),第二个为二值化后的图像 img_b
_, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)

# 使用 subplot 函数创建一个 1 行 2 列的子图布局,并选择第一个子图
plt.subplot(121)
# 在第一个子图中显示原始灰度图像
plt.imshow(img, 'gray')
# 设置第一个子图的标题为 '原图'
plt.title('原图')
# 关闭第一个子图的坐标轴显示
plt.axis('off')

# 选择 1 行 2 列子图布局中的第二个子图
plt.subplot(122)
# 在第二个子图中显示二值化后的图像
plt.imshow(img_b, 'gray')
# 设置第二个子图的标题,显示最终的迭代阈值 T 的值
plt.title('迭代阈值分割图T=' + '{:d}'.format(T))
# 关闭第二个子图的坐标轴显示
plt.axis('off')

# 显示所有绘制的子图
plt.show()

 运行结果: 

4.最小误差阈值分割

1) 设定目标物和背景的概率及其灰度分布概率密度函数
2)给定一个阈值 t,求每类的分割错误概率
3)求此阅值下的总分割错误概率 e(t)
4)由总分割错误概率 e(t) 的极小值求解最优阅值T

不足:需要估计概率密度函数并给定目标像素的比例,计算较为复杂。

5.最大方差阈值分割

选择一个阈值,使得目标和背景之间的总体差别最大,即组间方差最大,此即为最佳阈值。

代码:

# 导入 OpenCV 库,用于图像处理操作
import cv2
# 导入 NumPy 库,用于进行高效的数值计算
import numpy as np
# 从 matplotlib 库中导入 pyplot 模块,并重命名为 plt,用于绘图
from matplotlib import pyplot as plt

# 设置 matplotlib 的字体为黑体,解决中文显示乱码问题
plt.rcParams['font.sans-serif']=['SimHei']
# 使用 OpenCV 的 imread 函数读取指定路径的图像 '006.jpg',参数 0 表示以灰度模式读取
img=cv2.imread('006.jpg',0)

# 初始化最大类间方差为 0
t=0

# 遍历所有可能的阈值(0 到 255)
for i in range(256):
    # 计算像素值小于当前阈值 i 的像素的平均值
    mean1=np.mean(img[img<i])
    # 计算像素值大于等于当前阈值 i 的像素的平均值
    mean2=np.mean(img[img>=i])
    # 计算像素值小于当前阈值 i 的像素占总像素的比例
    w1=np.sum(img<i)/np.size(img)
    # 计算像素值大于等于当前阈值 i 的像素占总像素的比例
    w2=np.sum(img>=i)/np.size(img)
    # 根据类间方差公式计算当前阈值下的类间方差
    tem=w1*w2*np.power((mean1-mean2),2)
    # 如果当前类间方差大于之前记录的最大类间方差
    if tem>t:
        # 更新最佳阈值 T 为当前阈值 i
        T=i
        # 更新最大类间方差 t 为当前类间方差 tem
        t=tem
# 使用自定义计算得到的最佳阈值 T 对图像进行二值化处理
# 第一个返回值为阈值(此处用 _ 占位表示不使用),第二个返回值为二值化后的图像 img_b
_,img_b=cv2.threshold(img,T,255,cv2.THRESH_BINARY)
# 使用 OpenCV 内置的 Otsu 算法自动计算阈值并对图像进行二值化处理
# T1 为 Otsu 算法计算得到的阈值,img_b1 为二值化后的图像
T1,img_b1=cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

# 创建一个 1 行 3 列的子图布局,并选择第一个子图
plt.subplot(131)
# 在第一个子图中显示原始灰度图像
plt.imshow(img,'gray')
# 设置第一个子图的标题为 '原图'
plt.title('原图')
# 关闭第一个子图的坐标轴显示
plt.axis('off')

# 选择 1 行 3 列子图布局中的第二个子图
plt.subplot(132)
# 在第二个子图中显示自定义阈值二值化后的图像
plt.imshow(img_b,'gray')
# 设置第二个子图的标题,显示自定义计算得到的阈值 T 的值
plt.title('最大类间方差阈值分割图T='+'{:d}'.format(T))
# 关闭第二个子图的坐标轴显示
plt.axis('off')

# 选择 1 行 3 列子图布局中的第三个子图
plt.subplot(133)
# 在第三个子图中显示 Otsu 算法二值化后的图像
plt.imshow(img_b1,'gray')
# 设置第三个子图的标题,显示 Otsu 算法计算得到的阈值 T1 的值
plt.title('最大类间方差阈值分割图T='+'{:d}'.format(int(T1)))
# 关闭第三个子图的坐标轴显示
plt.axis('off')

# 显示所有绘制的子图
plt.show()

运行结果:

三.阈值分割+GUI

代码:

# 导入sys模块,用于访问与Python解释器紧密相关的变量和函数,如命令行参数、退出程序等
import sys
# 导入OpenCV库,用于计算机视觉任务,如图像读取、处理和显示
import cv2
# 导入NumPy库,用于高效的数值计算和数组操作
import numpy as np
# 从PyQt5的QtWidgets模块导入多个类,用于创建GUI界面的各种组件
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QLabel, QPushButton,
    QFileDialog, QMessageBox, QVBoxLayout, QHBoxLayout, QFrame, QSlider
)
# 从PyQt5的QtGui模块导入QPixmap和QImage类,用于处理和显示图像
from PyQt5.QtGui import QPixmap, QImage
# 从PyQt5的QtCore模块导入Qt类,用于访问Qt的常量和枚举类型
from PyQt5.QtCore import Qt
# 重复导入OpenCV库,可移除
import cv2
# 导入matplotlib库的pyplot模块,用于绘制和显示图表,但在当前代码中未使用,可移除
import matplotlib.pyplot as plt


# 定义一个名为ImageProcessor的类,继承自QMainWindow,用于创建主窗口
class ImageProcessor(QMainWindow):
    def __init__(self):
        # 调用父类QMainWindow的构造函数
        super().__init__()
        # 设置主窗口的标题
        self.setWindowTitle("PyQt演示")
        # 设置主窗口的初始大小
        self.resize(900, 600)

        # 用于存储原始图像和处理后的图像数据的字典
        self.image_data = {}

        # 创建一个QWidget对象作为主窗口的中心部件
        main_widget = QWidget()
        # 将创建的QWidget对象设置为中心部件
        self.setCentralWidget(main_widget)
        # 在主窗口部件上创建一个垂直布局
        main_layout = QVBoxLayout(main_widget)

        # 设置主窗口各个组件的样式
        main_widget.setStyleSheet("""
            QWidget {
                background-color: #f0f4f8;
            }
            QLabel {
                border: 2px solid #aaa;
                border-radius: 10px;
                background-color: white;
                padding: 5px;
                box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
            }
            QPushButton {
                font-size: 15px;
                padding: 8px 18px;
                min-width: 100px;
            }
        """)

        # 创建顶部水平布局,用于放置加载和保存按钮
        top_layout = QHBoxLayout()
        # 创建加载图片按钮
        load_btn = QPushButton("📂 加载图片")
        # 创建保存图像按钮
        save_btn = QPushButton("💾 保存图像")
        # 将加载按钮的点击事件连接到load_image方法
        load_btn.clicked.connect(self.load_image)
        # 将保存按钮的点击事件连接到save_image方法
        save_btn.clicked.connect(self.save_image)
        # 将加载按钮添加到顶部布局
        top_layout.addWidget(load_btn)
        # 将保存按钮添加到顶部布局
        top_layout.addWidget(save_btn)
        # 在顶部布局末尾添加一个伸缩项,使按钮靠左对齐
        top_layout.addStretch()
        # 将顶部布局添加到主布局
        main_layout.addLayout(top_layout)
        # 在主布局中添加一个水平分割线
        main_layout.addWidget(self._h_line())

        # 创建水平布局,用于显示原始图像和处理后的图像
        img_layout = QHBoxLayout()
        # 创建用于显示原始图像的标签
        self.original_label = QLabel()
        # 创建用于显示处理后图像的标签
        self.processed_label = QLabel()

        # 遍历原始图像和处理后图像的标签,设置固定大小和对齐方式
        for label in (self.original_label, self.processed_label):
            # 设置标签的固定大小为400x400像素
            label.setFixedSize(400, 400)
            # 设置标签内容居中显示
            label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # 将原始图像标签添加到图像布局
        img_layout.addWidget(self.original_label)
        # 在图像布局中添加一个垂直分割线
        img_layout.addWidget(self._v_line())
        # 将处理后图像标签添加到图像布局
        img_layout.addWidget(self.processed_label)
        # 设置图像布局中组件之间的间距为0
        img_layout.setSpacing(0)
        # 将图像布局添加到主布局
        main_layout.addLayout(img_layout)
        # 在主布局中添加一个水平分割线
        main_layout.addWidget(self._h_line())

        # 创建底部水平布局,用于放置滑动条和控制按钮
        bottom_layout = QHBoxLayout()
        # 创建一个水平滑动条
        self.slider = QSlider(Qt.Orientation.Horizontal)
        # 设置滑动条的取值范围为0到255
        self.slider.setRange(0, 255)
        # 初始化滑动条的值为128
        self.slider.setValue(128)
        # 将滑动条值改变事件连接到on_slider_changed方法
        self.slider.valueChanged.connect(self.on_slider_changed)
        # 将滑动条添加到底部布局
        bottom_layout.addWidget(self.slider)

        # 创建图像分割按钮
        self.segment_button = QPushButton("✂图像分割")
        # 将图像分割按钮添加到底部布局
        bottom_layout.addWidget(self.segment_button)

        # 在底部布局末尾添加一个伸缩项,使组件靠左对齐
        bottom_layout.addStretch()
        # 将底部布局添加到主布局
        main_layout.addLayout(bottom_layout)

    def on_slider_changed(self):
        """滑动条值改变时触发的事件"""
        # 检查是否已经加载了原始图像
        if 'original' in self.image_data:
            # 调用process方法处理图像,传入当前滑动条的值
            self.process(self.slider.value())

    def _h_line(self):
        """创建一个水平分割线"""
        # 创建一个QFrame对象
        line = QFrame()
        # 设置QFrame的形状为水平直线
        line.setFrameShape(QFrame.Shape.HLine)
        # 设置QFrame的阴影效果为凹陷
        line.setFrameShadow(QFrame.Shadow.Sunken)
        # 设置QFrame的颜色样式
        line.setStyleSheet("color: #ccc;")
        return line

    def _v_line(self):
        """创建一个垂直分割线"""
        # 创建一个QFrame对象
        line = QFrame()
        # 设置QFrame的形状为垂直直线
        line.setFrameShape(QFrame.Shape.VLine)
        # 设置QFrame的阴影效果为凹陷
        line.setFrameShadow(QFrame.Shadow.Sunken)
        # 设置QFrame的颜色样式
        line.setStyleSheet("color: #ccc;")
        return line

    def load_image(self):
        """加载图像"""
        # 弹出文件选择对话框,让用户选择图片文件
        file, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.png *.jpg *.bmp)")
        # 如果用户选择了文件
        if file:
            # 使用OpenCV读取用户选择的图像文件
            img = cv2.imread(file)
            # 如果图像读取失败
            if img is None:
                # 弹出警告消息框,提示用户无法加载图像
                QMessageBox.warning(self, "错误", "无法加载图像")
                return
            # 将原始图像存储到image_data字典中
            self.image_data['original'] = img
            # 将处理后的图像初始化为原始图像的副本
            self.image_data['processed'] = img.copy()
            # 调用show_image方法显示原始图像
            self.show_image(img, self.original_label)

    def save_image(self):
        """保存图像"""
        # 检查是否存在处理后的图像
        if 'processed' not in self.image_data:
            # 弹出警告消息框,提示用户没有可保存的图像
            QMessageBox.warning(self, "提示", "没有可保存的图像")
            return
        # 弹出文件保存对话框,让用户选择保存路径和文件名
        file, _ = QFileDialog.getSaveFileName(self, "保存图像", "", "PNG (*.png);;JPG (*.jpg)")
        # 如果用户选择了保存路径
        if file:
            # 使用OpenCV将处理后的图像保存到指定路径
            cv2.imwrite(file, self.image_data['processed'])
            # 弹出信息消息框,提示用户图像保存成功
            QMessageBox.information(self, "成功", f"图像已保存:{file}")

    def process(self, mode=None):
        """根据选择的模式处理图像"""
        # 检查是否已经加载了原始图像
        if 'original' not in self.image_data:
            # 弹出警告消息框,提示用户先加载图片
            QMessageBox.warning(self, "提示", "请先加载图片")
            return
        # 如果未传入处理模式,使用滑动条的当前值
        if mode is None:
            mode = self.slider.value()
        # 从image_data字典中获取原始图像
        img = self.image_data['original']
        # 将原始图像转换为灰度图像
        gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        # 对灰度图像进行二值化处理
        _, img_b = cv2.threshold(gray_img, mode, 255, cv2.THRESH_BINARY)
        # 将二值化后的图像转换回BGR格式
        result = cv2.cvtColor(img_b, cv2.COLOR_GRAY2BGR)
        # 将处理后的图像存储到image_data字典中
        self.image_data['processed'] = result
        # 调用show_image方法显示处理后的图像
        self.show_image(result, self.processed_label)

    def show_image(self, img, label):
        """显示图像"""
        # 将OpenCV的BGR颜色格式转换为RGB颜色格式
        rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        # 获取图像的高度、宽度和通道数
        h, w, ch = rgb.shape
        # 计算每行图像数据的字节数
        bytes_per_line = ch * w
        # 将NumPy数组转换为QImage对象
        q_img = QImage(rgb.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
        # 将QImage对象转换为QPixmap对象,并缩放以适应标签大小,保持纵横比
        label.setPixmap(QPixmap.fromImage(q_img).scaled(400, 400, Qt.AspectRatioMode.KeepAspectRatio))


# 程序入口点
if __name__ == "__main__":
    # 创建一个QApplication对象,处理GUI程序的事件循环
    app = QApplication(sys.argv)
    # 创建ImageProcessor类的实例
    window = ImageProcessor()
    # 显示主窗口
    window.show()
    # 进入应用程序的事件循环,直到窗口关闭,然后退出程序
    sys.exit(app.exec())

运行结果: 

总之:在对图像的研究和应用中,人们往往仅对图像中的某些部分感兴趣。这些部分常称为目标或前景(其它部分称为背景),它们一般对应图像中特定的、具有独特性质的区域。为了辨识和分析目标,需要将这些有关区域分离提取出来。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值