一.阈值分割介绍
阈值分割处理概述
阈值分割是数字图像处理中一种基于像素灰度值的区域分割技术,通过设定一个或多个灰度阈值,将图像像素分为目标(前景)和背景两类,从而实现图像的初步分割。其核心思想是:利用目标与背景在灰度分布上的差异,通过阈值划分实现区域分离。
阈值分割的关键要素
-
阈值(Threshold, T)
- 单个阈值(单阈值分割):将图像分为前景(灰度≥T)和背景(灰度 < T),适用于目标与背景灰度差异明显的图像。
- 多个阈值(多阈值分割):将图像分为多个区域(如前景 1、前景 2、背景),适用于复杂场景。
-
灰度直方图
阈值分割的重要依据,通过分析直方图的波峰与波谷(如双峰直方图的谷底对应最佳阈 值)确定分割阈值
二.人工阈值分割
1.示例代码
根据图像的直方图特性,人为给出阈值为130,示例代码如下:
import cv2
import matplotlib.pyplot as plt
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
# 读取图像(灰度模式)
img = cv2.imread(r"C:\Users\Administrator\Desktop\e.png", 0)
# 图像二值化处理(阈值为 130)
_, img_b = cv2.threshold(img, 130, 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('人工阈值分割图 T=130')
plt.axis('off')
# 创建第二个画布,显示灰度直方图
plt.figure(figsize=(6, 4))
# 绘制灰度直方图
hist = cv2.calcHist([img], [0], None, [256], [0, 255])
plt.plot(hist)
plt.title('灰度直方图')
plt.xlabel('灰度值')
plt.ylabel('像素数量')
# 显示结果
plt.show()
2.处理结果
3.代码解释
_, img_b = cv2.threshold(img, 130, 255, cv2.THRESH_BINARY)
-
函数作用
cv2.threshold()
是 OpenCV 中用于实现阈值分割的函数,将灰度图像转换为二值图像(像素值仅为 0 或 255)。 -
参数说明
img
:输入图像,必须是单通道灰度图像(若为彩色图像需先转换为灰度)。130
:设定的阈值(Threshold),用于区分前景和背景。255
:当像素值超过阈值时,赋予的最大值(通常为 255,表示白色)。cv2.THRESH_BINARY
:阈值类型,表示二值化模式。
二值化规则(THRESH_BINARY 模式)
对于输入图像中的每个像素:
- 若像素值 ≥ 130,则该像素在输出图像中设为
255
(白色)。 - 若像素值 < 130,则该像素在输出图像中设为
0
(黑色)。
三.直方图阈值分割
直方图阈值分割通常是使用双峰法完成,先要确定第一个峰值,然后定位第二个峰值,最后获
得两峰之间的最小值作为阈值。通过计算获得阈值为121,然后进行图像分割,示例代码如下:
1.代码展示
import cv2
import matplotlib.pyplot as plt
import numpy as np
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
# 读取图像(灰度模式)
img = cv2.imread(r"C:\Users\Administrator\Desktop\g.jpg", 0)
# 计算并绘制直方图
n, bins, patches = plt.hist(img.ravel(), 256, [0, 255], alpha=0.7)
# 找到直方图中所有局部极大值点的索引(局部峰值)
peaks_indices = []
for i in range(1, len(n) - 1):
if n[i] > n[i - 1] and n[i] > n[i + 1]:
peaks_indices.append(i)
# 如果存在多个局部极大值点,按照对应值从大到小排序
if len(peaks_indices) >= 2:
# 按对应值从大到小排序
peaks_indices.sort(key=lambda x: n[x], reverse=True)
# 取前两个作为第一峰值和第二峰值
first_peak = peaks_indices[0]
second_peak = peaks_indices[1]
else:
# 如果局部极大值点不足2个,直接取最大的两个值的索引作为第一和第二峰值
sorted_indices = np.argsort(n)
first_peak = sorted_indices[-1]
second_peak = sorted_indices[-2]
# 如果找到两个不同的峰值,计算两峰之间的谷值点作为阈值
if first_peak != second_peak:
# 获取两峰之间的范围
start = min(first_peak, second_peak)
end = max(first_peak, second_peak)
# 在两峰之间的子数组中找到最小值点的位置
sub_n = n[start:end + 1]
T = np.argmin(sub_n) + start # 加上起始索引偏移
else:
# 如果没有两个不同的峰值,使用默认方法计算阈值,这里简单处理为取最大值点作为阈值
T = first_peak
# 使用计算得到的阈值 T 进行二值化处理
_, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)
# 创建一个新的窗口用于显示直方图并标注峰值和谷值
plt.figure(figsize=(10, 6))
plt.hist(img.ravel(), 256, [0, 255], alpha=0.7)
plt.title('灰度直方图')
plt.xlabel('灰度值')
plt.ylabel('像素数量')
# 用红线标注第一个峰值
plt.axvline(first_peak, color='r', linestyle='--', linewidth=2, label=f'第一峰值={first_peak}')
# 用红线标注第二个峰值
plt.axvline(second_peak, color='r', linestyle='--', linewidth=2, label=f'第二峰值={second_peak}')
# 用蓝线标注阈值(谷值)
plt.axvline(T, color='b', linestyle='--', linewidth=2, label=f'阈值={T}')
plt.legend()
# 创建一个 1 行 2 列的子图布局,显示原图
plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title('原图')
plt.axis('off')
# 在第二个子图位置显示直方图阈值分割图
plt.subplot(122)
plt.imshow(img_b, cmap='gray')
plt.title(f'直方图阈值分割图 T={T}')
plt.axis('off')
# 显示所有绘制的内容
plt.show()
# 输出计算的峰值和阈值信息(便于调试)
print(f"第一峰值:{first_peak}")
print(f"第二峰值:{second_peak}")
print(f"阈值:{T}")
2.处理结果
3.代码解释
1. 灰度直方图分析与峰值检测
# 计算图像直方图(n为各灰度级的像素数量,bins为灰度级范围)
n, bins, _ = plt.hist(img.ravel(), 256, [0, 255])
# 寻找所有局部极大值点(峰值)
peaks_indices = []
for i in range(1, len(n) - 1):
if n[i] > n[i-1] and n[i] > n[i+1]:
peaks_indices.append(i)
- 直方图计算:将图像的所有像素灰度值展平为一维数组,统计每个灰度级(0-255)的像素数量。
- 峰值检测:遍历直方图,若某个灰度级的像素数量同时大于其左右相邻灰度级,则视为局部峰值。
2. 确定第一峰值和第二峰值
if len(peaks_indices) >= 2:
# 按峰值高度降序排序,取前两个
peaks_indices.sort(key=lambda x: n[x], reverse=True)
first_peak = peaks_indices[0]
second_peak = peaks_indices[1]
else:
# 若峰值不足2个,直接取全局最大的两个灰度级
sorted_indices = np.argsort(n)
first_peak = sorted_indices[-1] # 最大值索引
second_peak = sorted_indices[-2] # 次大值索引
- 策略:优先使用检测到的局部峰值,若无足够局部峰值,则直接选取像素数量最多的两个灰度级。
3. 计算两峰之间的谷值作为阈值
if first_peak != second_peak:
# 获取两峰之间的灰度范围
start = min(first_peak, second_peak)
end = max(first_peak, second_peak)
# 在该范围内找到像素数量最少的灰度级(谷值)
sub_n = n[start:end+1]
T = np.argmin(sub_n) + start # 加上起始偏移,还原为原始灰度级索引
else:
# 若两峰相同,直接使用第一峰值作为阈值
T = first_peak
- 谷值原理:假设前景和背景的灰度分布形成双峰,它们之间的低谷对应两类物体的分界点,适合作为阈值。
四.迭代阈值分割
1.代码展示
import cv2
import numpy as np
from matplotlib import pyplot as plt
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
# 读取图像(灰度模式)
img = cv2.imread(r"C:\Users\Administrator\Desktop\e.png", 0)
# 初始化阈值为图像的平均灰度值
T = int(np.mean(img))
# 迭代计算最优阈值
while True:
# 计算大于等于阈值 T 的像素的平均灰度值 m1
m1 = np.mean(img[img >= T])
# 计算小于阈值 T 的像素的平均灰度值 m2
m2 = np.mean(img[img < T])
# 如果新阈值和当前阈值 T 的差值小于 20,则停止迭代
if abs((m1 + m2) / 2 - T) < 20:
break
else:
# 更新阈值 T 为两个平均灰度值的平均值
T = int((m1 + m2) / 2)
# 对图像进行二值化处理,使用计算得到的阈值 T
_, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)
# 创建第一个画布,显示原图
plt.figure()
plt.imshow(img, 'gray')
plt.title('原图')
plt.axis('off')
# 创建第二个画布,显示迭代阈值分割图
plt.figure()
plt.imshow(img_b, 'gray')
plt.title('迭代阈值分割图 T=' + '{:d}'.format(T))
plt.axis('off')
# 显示所有绘制的图像
plt.show()
2.处理结果

3.代码解释
1. 初始化阈值
T = int(np.mean(img))
- 将初始阈值设为图像的平均灰度值,作为迭代的起点。
2. 迭代计算最优阈值
while True:
# 计算大于等于当前阈值 T 的像素的平均灰度值(前景)
m1 = np.mean(img[img >= T])
# 计算小于当前阈值 T 的像素的平均灰度值(背景)
m2 = np.mean(img[img < T])
# 判断收敛条件:新阈值与当前阈值的差小于 20
if abs((m1 + m2) / 2 - T) < 20:
break
else:
# 更新阈值为前景和背景平均灰度值的中间值
T = int((m1 + m2) / 2)
- 迭代逻辑:
- 将图像分为两部分(≥T 和 <T),分别计算其平均灰度值
m1
(前景)和m2
(背景)。 - 取两者的平均值
(m1 + m2)/2
作为新阈值。 - 重复上述过程,直到新阈值与当前阈值的差异足够小(<20),此时认为算法收敛。
- 将图像分为两部分(≥T 和 <T),分别计算其平均灰度值
3. 二值化处理
_, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)
- 使用最终迭代得到的阈值
T
进行二值化:- 像素灰度值 ≥ T → 设为 255(白色)
- 像素灰度值 < T → 设为 0(黑色)
五.最大类间方差阈值分割
1.代码展示
import cv2
import numpy as np
from matplotlib import pyplot as plt
# 设置中文字体,确保标题可以显示中文
plt.rcParams['font.sans-serif'] = ['SimHei']
# 读取图像(灰度模式)
img = cv2.imread(r"C:\Users\Administrator\Desktop\f.jpg", 0)
# 手动实现最大类间方差法计算阈值
max_variance = 0
best_threshold = 0
for i in range(256):
# 将图像像素分为两类:小于阈值 i 和大于等于阈值 i
pixels_class1 = img[img < i]
pixels_class2 = img[img >= i]
# 如果某一类为空,则跳过当前迭代
if len(pixels_class1) == 0 or len(pixels_class2) == 0:
continue
# 计算两类的均值和权重
mean1 = np.mean(pixels_class1)
mean2 = np.mean(pixels_class2)
w1 = len(pixels_class1) / len(img.ravel())
w2 = len(pixels_class2) / len(img.ravel())
# 计算类间方差
between_class_variance = w1 * w2 * (mean1 - mean2) ** 2
# 如果当前类间方差大于之前的最大类间方差,则更新最大类间方差和对应的阈值
if between_class_variance > max_variance:
max_variance = between_class_variance
best_threshold = i
# 手动实现最大类间方差法的二值化处理结果
_, img_b = cv2.threshold(img, best_threshold, 255, cv2.THRESH_BINARY)
# OpenCV 自带的 Otsu 方法计算阈值及二值化处理结果
best_threshold1, img_b1 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 创建第一个画布,显示手动实现最大类间方差法的结果
plt.figure()
# 显示原图
plt.subplot(121)
plt.imshow(img, 'gray')
plt.title('原图')
plt.axis('off')
# 显示手动实现的最大类间方差阈值分割图
plt.subplot(122)
plt.imshow(img_b, 'gray')
plt.title('最大类间方差阈值分割图 T=' + '{:d}'.format(best_threshold))
plt.axis('off')
# 创建第二个画布,显示 OpenCV 自带的 Otsu 方法的结果
plt.figure()
# 显示原图
plt.subplot(121)
plt.imshow(img, 'gray')
plt.title('原图')
plt.axis('off')
# 显示 Otsu 方法的分割图
plt.subplot(122)
plt.imshow(img_b1, 'gray')
plt.title('Otsu 阈值分割图 T=' + '{:.0f}'.format(best_threshold1))
plt.axis('off')
# 显示所有绘制的图像
plt.show()
2.处理结果

3.代码解释
1.手动实现最大类间方差法(Otsu 算法)
max_variance = 0
best_threshold = 0
for i in range(256):
# 将图像像素分为两类:小于阈值 i 和大于等于阈值 i
pixels_class1 = img[img < i]
pixels_class2 = img[img >= i]
# 若某一类为空,跳过当前迭代
if len(pixels_class1) == 0 or len(pixels_class2) == 0:
continue
# 计算两类的均值和权重
mean1 = np.mean(pixels_class1)
mean2 = np.mean(pixels_class2)
w1 = len(pixels_class1) / len(img.ravel()) # 类1权重(像素占比)
w2 = len(pixels_class2) / len(img.ravel()) # 类2权重(像素占比)
# 计算类间方差(公式:w1*w2*(mean1-mean2)^2)
between_class_variance = w1 * w2 * (mean1 - mean2) ** 2
# 更新最大类间方差和最佳阈值
if between_class_variance > max_variance:
max_variance = between_class_variance
best_threshold = i
关键步骤
- 遍历阈值:从 0 到 255 遍历所有可能的阈值
i
。 - 分类统计:将像素分为两类,分别计算其均值(
mean1
,mean2
)和权重(w1
,w2
)。 - 类间方差计算:使用公式 w1⋅w2⋅(μ1−μ2)2 计算类间方差。
- 最优阈值选择:保存使类间方差最大的阈值
best_threshold
。
2.OpenCV 内置的 Otsu 方法
best_threshold1, img_b1 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
关键参数
cv2.THRESH_BINARY
:二值化模式(≥阈值为 255,< 阈值为 0)。cv2.THRESH_OTSU
:自动计算最优阈值的标志位。此时,函数的第一个返回值即为 Otsu 方法计算的阈值。
六,图像阈值分割的应用
通过对图像阈值分割处理的系统学习,在实验四的基础上,将对图像进行阈值分割的处理,作为一个模块加入新的功能。
1.代码展示
import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QLabel, QPushButton,
QFileDialog, QMessageBox, QVBoxLayout, QHBoxLayout,
QGridLayout, QFrame, QGroupBox, QSlider, QLineEdit
)
from PyQt5.QtGui import QPixmap, QImage
from PyQt5.QtCore import Qt
import matplotlib.pyplot as plt
import uuid
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体等中文字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
class ImageProcessor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("高级图像处理工具")
self.resize(1200, 900)
self.original = None
self.processed = {} # {位置: (处理方法, 图像数据)}
self.method_map = {} # {处理方法: 位置}
self.positions = [1, 2, 3] # 可用显示位置(0为原图)
main_widget = QWidget()
self.setCentralWidget(main_widget)
self.main_layout = QVBoxLayout(main_widget) # 定义 main_layout 为实例变量
self.setStyleSheet("""
QWidget { background: #f0f4f8; }
QGroupBox {
border: 1px solid #ddd;
border-radius: 5px;
margin-top: 10px;
padding-top: 15px;
}
QLabel {
border: 2px solid #aaa;
border-radius: 5px;
background: white;
}
QPushButton {
min-width: 100px;
padding: 8px;
border-radius: 5px;
}
.danger { background: #ff4444; color: white; }
""")
top_bar = QHBoxLayout()
self.create_button(top_bar, "📁 加载", self.load_image, "#4CAF50")
self.create_button(top_bar, "💾 保存", self.save_image, "#2196F3")
self.create_button(top_bar, "🗑️ 重置", self.reset_all, "#f44336")
self.main_layout.addLayout(top_bar) # 使用 self.main_layout
grid = QGridLayout()
self.labels = {}
for idx in range(4):
container = QWidget()
layout = QVBoxLayout(container)
label = QLabel()
label.setFixedSize(500, 400)
label.setAlignment(Qt.AlignCenter)
layout.addWidget(label)
if idx != 0:
btn = QPushButton("删除")
btn.setProperty("position", idx)
btn.setStyleSheet("background: #ff4444; color: white;")
btn.clicked.connect(self.delete_image)
layout.addWidget(btn)
self.labels[idx] = label
grid.addWidget(container, idx // 2, idx % 2)
self.main_layout.addLayout(grid) # 使用 self.main_layout
control_panel = QHBoxLayout()
groups = [
("噪声处理", [
("🌫️ 高斯噪声", "gaussian"),
("⚪ 椒盐噪声", "salt_pepper"),
("🌀 均匀噪声", "uniform")
]),
("平滑滤波", [
("🌀 高斯模糊", "gaussian_blur"),
("🔵 均值滤波", "mean_blur"),
("🟢 中值滤波", "median_blur")
]),
("锐化滤波", [
("🔺 Roberts", "roberts"),
("📐 Sobel", "sobel"),
("🌀 Prewitt", "prewitt"),
("✨ Laplacian", "laplacian"),
("🌟 非锐化掩模", "unsharp_mask"),
("💫 强锐化", "strong_sharpen")
]),
("频域处理", [
("🌈 傅里叶变换", "fourier"),
("📊 直方图", "histogram")
])
]
for title, btns in groups:
group = QGroupBox(title)
layout = QVBoxLayout()
for text, func in btns:
btn = QPushButton(text)
btn.clicked.connect(lambda _, f=func: self.process(f))
layout.addWidget(btn)
group.setLayout(layout)
control_panel.addWidget(group)
self.main_layout.addLayout(control_panel) # 使用 self.main_layout
# 添加新的功能区域
self.add_histogram_and_thresholding()
def add_histogram_and_thresholding(self):
histogram_group = QGroupBox("灰度直方图与阈值分割")
histogram_layout = QVBoxLayout()
# 添加显示直方图的按钮
self.hist_btn = QPushButton("显示灰度直方图")
self.hist_btn.clicked.connect(self.show_histogram)
histogram_layout.addWidget(self.hist_btn)
# 添加阈值分割的控制界面
self.threshold_group = QGroupBox("阈值分割")
threshold_layout = QVBoxLayout()
# 添加滑块和标签显示当前阈值
self.threshold_slider = QSlider(Qt.Horizontal)
self.threshold_slider.setMinimum(0)
self.threshold_slider.setMaximum(255)
self.threshold_slider.setValue(128)
self.threshold_slider.valueChanged.connect(self.threshold_changed)
self.threshold_label = QLineEdit("当前阈值:128")
self.threshold_label.setReadOnly(True)
threshold_layout.addWidget(self.threshold_slider)
threshold_layout.addWidget(self.threshold_label)
self.threshold_group.setLayout(threshold_layout)
histogram_layout.addWidget(self.threshold_group)
histogram_group.setLayout(histogram_layout)
self.main_layout.addWidget(histogram_group) # 使用 self.main_layout
def create_button(self, layout, text, callback, color=""):
btn = QPushButton(text)
btn.clicked.connect(callback)
btn.setStyleSheet(f"background: {color}; color: white;" if color else "")
layout.addWidget(btn)
def process_unsharp_mask(self):
blurred = cv2.GaussianBlur(self.original, (9, 9), 2.0)
mask = cv2.addWeighted(self.original, 1.5, blurred, -0.5, 0)
return np.clip(mask, 0, 255).astype(np.uint8)
def process_strong_sharpen(self):
kernel = np.array([
[-1, -1, -1, -1, -1],
[-1, 2, 2, 2, -1],
[-1, 2, 16, 2, -1],
[-1, 2, 2, 2, -1],
[-1, -1, -1, -1, -1]
], dtype=np.float32) / 8.0
return cv2.filter2D(self.original, -1, kernel)
def load_image(self):
path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.png *.jpg *.bmp)")
if path:
self.original = cv2.imread(path)
self.reset_all()
self.show_image(self.original, 0)
def save_image(self):
if not self.original:
QMessageBox.warning(self, "警告", "请先加载图像")
return
path, _ = QFileDialog.getSaveFileName(self, "保存图片", "", "PNG (*.png);;JPG (*.jpg)")
if path:
cv2.imwrite(path, self.processed.get(self.current_pos, self.original))
QMessageBox.information(self, "成功", "图片保存成功")
def reset_all(self):
self.processed.clear()
self.method_map.clear()
self.positions = [1, 2, 3]
for pos in self.labels:
self.labels[pos].clear()
if self.original is not None:
self.show_image(self.original, 0)
def delete_image(self):
btn = self.sender()
pos = btn.property("position")
if pos in self.processed:
method = self.processed[pos][0]
del self.method_map[method]
del self.processed[pos]
self.positions.append(pos)
self.positions.sort()
self.labels[pos].clear()
def process(self, method):
if self.original is None:
QMessageBox.warning(self, "警告", "请先加载图像")
return
if method in self.method_map:
QMessageBox.warning(self, "提示", "该处理已存在,请先删除后再试")
return
if not self.positions:
QMessageBox.warning(self, "提示", "所有位置已满,请先删除部分处理")
return
pos = self.positions.pop(0)
processor = getattr(self, f"process_{method}")
result = processor()
self.method_map[method] = pos
self.processed[pos] = (method, result)
self.show_image(result, pos)
def show_image(self, img, position):
if position not in self.labels:
return
if len(img.shape) == 2: # 灰度图
h, w = img.shape
bytes_per_line = w
qimg = QImage(img.data, w, h, bytes_per_line, QImage.Format_Grayscale8)
else: # 彩色图
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
h, w, ch = rgb.shape
bytes_per_line = ch * w
qimg = QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(qimg).scaled(500, 400, Qt.KeepAspectRatio)
self.labels[position].setPixmap(pixmap)
def process_gaussian(self):
img = self.original.copy()
row, col, ch = img.shape
mean = 0
var = 100
sigma = var ** 0.5
gauss = np.random.normal(mean, sigma, (row, col, ch))
return np.clip(img + gauss, 0, 255).astype(np.uint8)
def process_salt_pepper(self):
img = self.original.copy()
prob = 0.05
rnd = np.random.rand(*img.shape[:2])
img[rnd < prob / 2] = 0
img[rnd > 1 - prob / 2] = 255
return img
def process_uniform(self):
img = self.original.copy()
noise = np.random.uniform(-50, 50, img.shape)
return np.clip(img + noise, 0, 255).astype(np.uint8)
def process_gaussian_blur(self):
return cv2.GaussianBlur(self.original, (9, 9), 0)
def process_mean_blur(self):
return cv2.blur(self.original, (9, 9))
def process_median_blur(self):
return cv2.medianBlur(self.original, 9)
def process_roberts(self):
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
kernelx = np.array([[1, 0], [0, -1]], dtype=int)
kernely = np.array([[0, 1], [-1, 0]], dtype=int)
robertsx = cv2.filter2D(gray, -1, kernelx)
robertsy = cv2.filter2D(gray, -1, kernely)
return cv2.addWeighted(robertsx, 0.5, robertsy, 0.5, 0)
def process_sobel(self):
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=5)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=5)
return cv2.addWeighted(sobelx, 0.5, sobely, 0.5, 0)
def process_prewitt(self):
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
kernelx = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]])
kernely = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]])
prewittx = cv2.filter2D(gray, -1, kernelx)
prewitty = cv2.filter2D(gray, -1, kernely)
return cv2.addWeighted(prewittx, 0.5, prewitty, 0.5, 0)
def process_laplacian(self):
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
return cv2.normalize(laplacian, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
def process_fourier(self):
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
f = np.fft.fft2(gray)
fshift = np.fft.fftshift(f)
magnitude = 20 * np.log(np.abs(fshift))
return cv2.normalize(magnitude, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
def process_histogram(self):
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
filename = f"hist_{uuid.uuid4().hex}.png"
plt.figure(figsize=(5, 4))
plt.hist(gray.ravel(), 256, [0, 255])
plt.savefig(filename)
plt.close()
hist_img = cv2.imread(filename)
cv2.imwrite(filename, hist_img) # 解决matplotlib的通道问题
return cv2.cvtColor(hist_img, cv2.COLOR_BGR2RGB)
def show_histogram(self):
if self.original is None:
QMessageBox.warning(self, "警告", "请先加载图像")
return
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
plt.figure(figsize=(8, 6))
plt.hist(gray.ravel(), 256, [0, 255])
plt.title("灰度直方图")
plt.xlabel("灰度值")
plt.ylabel("像素数量")
plt.show()
def threshold_changed(self, value):
if self.original is None:
return
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, value, 255, cv2.THRESH_BINARY)
self.show_image(thresh, 1)
self.threshold_label.setText(f"当前阈值:{value}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ImageProcessor()
window.show()
sys.exit(app.exec())
2.应用示例
3.代码解释
1. 界面初始化与布局
def add_histogram_and_thresholding(self):
# 创建"灰度直方图与阈值分割"分组
histogram_group = QGroupBox("灰度直方图与阈值分割")
histogram_layout = QVBoxLayout()
# 添加显示直方图的按钮
self.hist_btn = QPushButton("显示灰度直方图")
self.hist_btn.clicked.connect(self.show_histogram)
histogram_layout.addWidget(self.hist_btn)
# 创建阈值分割控制组
self.threshold_group = QGroupBox("阈值分割")
threshold_layout = QVBoxLayout()
# 创建水平滑块(范围0-255,初始值128)
self.threshold_slider = QSlider(Qt.Horizontal)
self.threshold_slider.setMinimum(0)
self.threshold_slider.setMaximum(255)
self.threshold_slider.setValue(128)
self.threshold_slider.valueChanged.connect(self.threshold_changed)
# 创建只读文本框显示当前阈值
self.threshold_label = QLineEdit("当前阈值:128")
self.threshold_label.setReadOnly(True)
threshold_layout.addWidget(self.threshold_slider)
threshold_layout.addWidget(self.threshold_label)
self.threshold_group.setLayout(threshold_layout)
histogram_layout.addWidget(self.threshold_group)
histogram_group.setLayout(histogram_layout)
self.main_layout.addWidget(histogram_group)
关键点:
- 滑块范围:0-255(对应图像灰度值范围)。
- 滑块值变化时触发
threshold_changed
方法。 - 文本框实时显示当前阈值。
2. 阈值处理核心逻辑
def threshold_changed(self, value):
if self.original is None:
return # 未加载图像时不执行操作
# 将原图转换为灰度图
gray = cv2.cvtColor(self.original, cv2.COLOR_BGR2GRAY)
# 应用阈值处理(THRESH_BINARY模式)
_, thresh = cv2.threshold(gray, value, 255, cv2.THRESH_BINARY)
# 在位置1显示处理结果
self.show_image(thresh, 1)
# 更新阈值显示文本
self.threshold_label.setText(f"当前阈值:{value}")
处理流程:
- 灰度转换:将原始彩色图像转换为单通道灰度图。
- 阈值分割:使用
cv2.threshold
函数,将灰度值≥value
的像素设为 255(白色),其余设为 0(黑色)。 - 结果显示:在界面的第一个处理位置(索引 1)显示二值化结果。
3. 图像显示辅助方法
def show_image(self, img, position):
if position not in self.labels:
return
# 处理灰度图和彩色图的显示差异
if len(img.shape) == 2: # 灰度图
h, w = img.shape
qimg = QImage(img.data, w, h, QImage.Format_Grayscale8)
else: # 彩色图
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
h, w, ch = rgb.shape
qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888)
# 调整图像大小并显示
pixmap = QPixmap.fromImage(qimg).scaled(500, 400, Qt.KeepAspectRatio)
self.labels[position].setPixmap(pixmap)
功能:
- 将 OpenCV 的
numpy.ndarray
转换为 PyQt 的QPixmap
。 - 保持图像长宽比,缩放至 500×400 像素显示。