一.图像分割-阈值分割
图像分割:图像分割是指把图像分成互不重叠而又各具特性的子区域,并提取出感兴趣目标的技术和过程。
特性可以是灰度、颜色、纹理等,目标可对应单个区域,也可对应多个区域。
一般图像的处理过程:预处理->图像分割->特征提取
灰度阈值法:根据图像的灰度,设置不同的阈值,来确定有意义的区域或者边界。
二.阈值分割方法
分析二值化的阈值分割
其中,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())
运行结果:
总之:在对图像的研究和应用中,人们往往仅对图像中的某些部分感兴趣。这些部分常称为目标或前景(其它部分称为背景),它们一般对应图像中特定的、具有独特性质的区域。为了辨识和分析目标,需要将这些有关区域分离提取出来。