DIP实验5:阈值算法实现与GUI应用

一.实验目的


本实验通过实现多种阈值分割算法(人工阈值、双峰法、迭代法、最佳阈值),探究不同方法在指纹图像处理中的效果,并开发交互式GUI应用,直观对比各算法的分割性能。

二.方法说明

1. 人工阈值法

直接指定固定阈值(默认130),将图像二值化。
(1)代码:

# 导入OpenCV库,用于图像处理
import cv2
# 导入matplotlib.pyplot库,用于图像显示和绘制图表
import matplotlib.pyplot as plt
# 设置matplotlib支持中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']

# 读取指定路径的图像文件,第二个参数0表示以灰度模式读取
img = cv2.imread('xibao.jpg', 0)

# 使用固定阈值130进行二值化处理
# cv2.threshold返回两个值,第一个是使用的阈值,第二个是处理后的图像
# 阈值处理方式cv2.THRESH_BINARY表示:像素值大于阈值时设为255,小于等于阈值时设为0
_, img_b = cv2.threshold(img, 130, 255, cv2.THRESH_BINARY)

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

# 创建一个1行3列的子图布局,当前绘制第2个子图
plt.subplot(132)
# 计算并绘制图像的灰度直方图
# cv2.calcHist参数:[img]表示输入图像列表,[0]表示计算灰度通道,None表示不使用掩码,
# [256]表示直方图的bin数量,[0, 255]表示像素值范围
hist = cv2.calcHist([img], [0], None, [256], [0, 255])
plt.plot(hist)
# 设置子图标题为"灰度直方图"
plt.title('灰度直方图')

# 创建一个1行3列的子图布局,当前绘制第3个子图
plt.subplot(133)
# 显示阈值分割后的二值图像
plt.imshow(img_b, 'gray')
# 设置子图标题,标明使用的阈值T=130
plt.title('人工阈值分割图 T=130')
# 关闭坐标轴显示
plt.axis('off')

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

(2)运行结果截图

(3)关键说明

人工阈值分割:通过cv2.threshold函数实现,阈值为200。

灰度直方图:通过cv2.calcHist函数计算并绘制,用于观察图像的灰度分布。

图像显示:使用matplotlib库的imshow和subplot函数展示原图、直方图和分割后的图像。

2、直方图阈值分割(直方图双峰法)

(1)代码

# 直方图阈值分割(双峰法)
import cv2
import matplotlib.pyplot as plt
import numpy as np

# 设置matplotlib支持中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']  # 确保中文显示正常

# 读取图像(请确保'A.jpg'存在或替换为实际路径)
img = cv2.imread('A.jpg', 0)
if img is None:
    print("错误:无法读取图像,请检查路径是否正确")
else:
    # 计算图像的直方图并进行平滑处理
    hist, bins = np.histogram(img.ravel(), bins=256, range=[0, 255])
    
    # 使用高斯滤波平滑直方图,减少噪声影响
    # 高斯核大小为(5,1),标准差为0
    smoothed_hist = cv2.GaussianBlur(hist.astype(np.float32), (5, 1), 0).flatten()
    
    # 寻找所有局部峰值(灰度直方图中的波峰)
    peak_indices = []
    for i in range(1, len(smoothed_hist) - 1):
        # 如果当前点的值大于前后相邻点,则认为是局部峰值
        if smoothed_hist[i] > smoothed_hist[i - 1] and smoothed_hist[i] > smoothed_hist[i + 1]:
            peak_indices.append(i)
    
    # 如果找到的峰值少于2个,使用简化方法
    if len(peak_indices) < 2:
        print("警告:未找到足够的峰值,使用简化方法")
        # 寻找全局最大值作为第一个峰值
        f1 = np.argmax(smoothed_hist)
        
        # 寻找距离f1最远且值较大的点作为第二个峰值
        temp = 0
        f2 = f1
        for i in range(len(smoothed_hist)):
            if i == f1:
                continue
            # 计算距离与高度的乘积,寻找距离远且值大的点
            dist = (i - f1) ** 2
            temp1 = dist * smoothed_hist[i]
            if temp1 > temp:
                temp = temp1
                f2 = i
        
        peak_indices = [f1, f2]
    else:
        # 选择最高的两个峰值
        peak_values = [smoothed_hist[i] for i in peak_indices]
        sorted_indices = sorted(range(len(peak_values)), key=lambda k: peak_values[k], reverse=True)
        peak_indices = [peak_indices[i] for i in sorted_indices[:2]]
        
        # 确保f1是较小值(左侧峰值)
        if peak_indices[0] > peak_indices[1]:
            peak_indices[0], peak_indices[1] = peak_indices[1], peak_indices[0]
    
    # 提取双峰对应的灰度值
    f1, f2 = peak_indices
    
    # 在[f1,f2]区间内寻找最小值对应的灰度值作为阈值(谷底)
    valley_region = smoothed_hist[f1:f2 + 1]
    valley_index = np.argmin(valley_region)
    T = f1 + valley_index
    
    # 应用阈值进行二值化(大于阈值的为白色,小于等于的为黑色)
    _, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)
    
    # 显示原图、直方图和分割结果
    plt.figure(figsize=(18, 5))
    
    # 显示原图
    plt.subplot(131)
    plt.imshow(img, cmap='gray')
    plt.title('原图')
    plt.axis('off')
    
    # 显示直方图及相关标记
    plt.subplot(132)
    plt.hist(img.ravel(), bins=256, range=[0, 255], alpha=0.5, label='原始直方图')
    plt.plot(smoothed_hist, 'r-', linewidth=2, label='平滑直方图')
    
    # 标记双峰和谷底
    plt.scatter(f1, smoothed_hist[f1], color='green', s=100, marker='o', label=f'峰值1: {f1}')
    plt.scatter(f2, smoothed_hist[f2], color='blue', s=100, marker='o', label=f'峰值2: {f2}')
    plt.scatter(T, smoothed_hist[T], color='red', s=100, marker='x', label=f'谷底(阈值): {T}')
    
    # 绘制连接双峰的参考线
    plt.plot([f1, f2], [smoothed_hist[f1], smoothed_hist[f2]], 'g--', alpha=0.3)
    
    # 绘制阈值线
    plt.axvline(x=T, color='r', linestyle='--', label=f'阈值 T={T}')
    plt.title('灰度直方图 (双峰法)')
    plt.xlabel('灰度值')
    plt.ylabel('像素数量')
    plt.legend()
    
    # 显示分割结果
    plt.subplot(133)
    plt.imshow(img_b, cmap='gray')
    plt.title(f'阈值分割结果 (T={T})')
    plt.axis('off')
    
    # 自动调整布局
    plt.tight_layout()
    plt.show()
    
    # 打印找到的峰值和计算的阈值
    print(f"找到的峰值: {f1} 和 {f2}")
    print(f"计算的阈值: {T}")

(2)运行结果截图

(3)关键说明

直方图分析:通过分析直方图的峰值和谷值来自动确定阈值。

阈值计算:基于直方图的两个峰值之间的最小值确定阈值。

直方图平滑:使用高斯滤波对直方图进行平滑处理,减少噪声对峰值检测的影响。

严格的峰值检测:通过比较相邻点找出所有局部峰值,而不是简单地寻找全局最大值。

峰值选择策略:如果找到的峰值不足两个,使用原代码中的简化方法。如果找到多个峰值,选择最高的两个作为双峰。
可视化增强:在直方图上标记出找到的两个峰值和谷底位置。绘制原始直方图和平滑后的直方图曲线,添加连接两个峰值的虚线,直观展示谷底位置。

3、迭代阈值分割

(1)代码

# 迭代阈值分割
import cv2
import matplotlib.pyplot as plt
import numpy as np

# 设置中文字体,确保中文标题正常显示
plt.rcParams['font.sans-serif'] = ['SimHei']

# 读取图像并转换为灰度图(第二个参数0表示以灰度模式读取)
img = cv2.imread('A.jpg', 0)

# 1. 初始化阈值:使用图像的平均灰度值作为初始阈值
T = int(np.mean(img))

# 2. 迭代计算最佳阈值
while True:
    # 计算当前阈值下的前景和背景像素集合
    # img >= T 返回布尔数组,用于筛选大于等于阈值的像素
    pixels_above = img[img >= T]  # 前景像素(大于等于阈值)
    pixels_below = img[img < T]   # 背景像素(小于阈值)
    
    # 计算前景和背景的平均灰度值
    # 如果没有像素满足条件,则对应的均值设为0
    m1 = np.mean(pixels_above) if len(pixels_above) > 0 else 0  # 前景均值
    m2 = np.mean(pixels_below) if len(pixels_below) > 0 else 0  # 背景均值
    
    # 计算新的阈值:前景和背景均值的平均值
    new_T = int((m1 + m2) / 2)
    
    # 3. 判断收敛条件:如果新旧阈值的差异小于设定阈值(20),则停止迭代
    # 这里的20是自定义的收敛阈值,可以根据需要调整
    if abs(new_T - T) < 20:
        break
    else:
        T = new_T  # 更新阈值,继续下一轮迭代

# 4. 使用最终阈值进行二值化处理
# cv2.threshold返回两个值:第一个是使用的阈值,第二个是二值化后的图像
_, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)

# 5. 显示原图和分割结果
plt.subplot(121)  # 创建1行2列的子图,当前绘制第1个
plt.imshow(img, 'gray')  # 显示灰度原图
plt.title('原图')
plt.axis('off')  # 关闭坐标轴显示

plt.subplot(122)  # 当前绘制第2个
plt.imshow(img_b, 'gray')  # 显示分割结果
plt.title(f'迭代阈值分割图 T={T}')  # 标题显示最终阈值
plt.axis('off')

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

(2)运行结果截图

(3)关键说明

  • np.mean()计算数组元素的算术平均值,计算前景和背景区域的平均灰度值,作为迭代更新阈值的依据。
  • cv2.threshold()基于阈值将图像转换为二值图像,根据迭代计算得到的最佳阈值,将图像转换为黑白二值图像。

4、最大类间方差阈值分割(大津阈值)


(1)代码

# 最大类间方差阈值分割(Otsu方法)
import cv2
import matplotlib.pyplot as plt
import numpy as np

# 设置中文字体,确保中文标题正常显示
plt.rcParams['font.sans-serif'] = ['SimHei']

# 读取图像并检查是否读取成功
img = cv2.imread('che.bmp', 0)  # 读取为灰度图像
if img is None:
    print("图像读取失败,请检查路径")
    exit(1)

# 初始化最佳阈值和最大类间方差
t = 0  # 最大类间方差
T = 0  # 最佳阈值

# 遍历所有可能的阈值(0-255)
for i in range(256):
    # 分割图像为背景和前景两部分
    background = img[img < i]   # 背景像素
    foreground = img[img >= i]  # 前景像素
    
    # 处理背景或前景为空的情况
    if len(background) == 0:
        mean1 = 0  # 背景均值
    else:
        mean1 = np.mean(background)
        
    if len(foreground) == 0:
        mean2 = 0  # 前景均值
    else:
        mean2 = np.mean(foreground)
    
    # 计算背景和前景的像素比例
    w1 = len(background) / img.size  # 背景比例
    w2 = len(foreground) / img.size  # 前景比例
    
    # 计算类间方差(此处公式有误,应为 w1*w2*(mean1-mean2)^2)
    # 注意:原代码中的公式可能是笔误,正确实现应使用下面的公式
    # 正确的类间方差公式:tem = w1 * w2 * (mean1 - mean2) ** 2
    # 这里使用正确的公式进行修正
    tem = w1 * w2 * (mean1 - mean2) ** 2
    
    # 更新最大类间方差和最佳阈值
    if tem > t:
        T = i
        t = tem

# 使用计算得到的最佳阈值进行二值化
_, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)

# 使用OpenCV内置的Otsu方法计算阈值并进行二值化
T1, img_b1 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 显示原图和两种方法的分割结果
plt.subplot(131)
plt.imshow(img, cmap='gray')
plt.title('原图')
plt.axis('off')

plt.subplot(132)
plt.imshow(img_b, cmap='gray')
plt.title(f'自定义Otsu分割 T={T}')
plt.axis('off')

plt.subplot(133)
plt.imshow(img_b1, cmap='gray')
plt.title(f'OpenCV Otsu分割 T={int(T1)}')
plt.axis('off')

plt.show()

(2)运行结果截图

(3)关键说明

Otsu 阈值选择算法原理:通过最大化类间方差找到最佳阈值,使前景和背景分离度最大
遍历所有可能的阈值(0-255),计算每个阈值对应的前景和背景的统计特征,计算类间方差,
选择使类间方差最大的阈值作为最佳阈值

Otsu 算法核心:布尔索引(img[img < i]),基于条件筛选数组元素,将图像分割为前景和背景两个区域,分别计算统计特征。

OpenCV 内置 Otsu 方法:cv2.THRESH_OTSU,结合 cv2.threshold() 使用,自动计算最佳阈值。0 作为占位阈值,算法会忽略该值并使用 Otsu 方法计算阈值

5、制作一个可以用进度条控制阀值的图像分割界面

(1)代码

import sys
import cv2
import numpy as np
import io
import os
import matplotlib
from datetime import datetime

# 使用Agg后端避免Qt版本冲突
matplotlib.use('Agg')
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QPushButton, QLabel, QFileDialog, QSlider, QComboBox, QFrame, QMessageBox)
from PyQt5.QtGui import QImage, QPixmap, QColor, QPalette, QLinearGradient, QPainter, QFont
from PyQt5.QtCore import Qt, QSize, QRect, QPoint
import matplotlib.pyplot as plt
from PIL import Image, ImageQt, ImageDraw, ImageFont

# 设置高DPI缩放
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)

# 设置字体支持中文
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'SimSun', 'NSimSun', 'FangSong']  # 按优先级设置字体
plt.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题


class GradientWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.start_color = QColor(41, 128, 185)  # Blue
        self.end_color = QColor(142, 68, 173)  # Purple

    def paintEvent(self, event):
        painter = QPainter(self)
        gradient = QLinearGradient(0, 0, self.width(), self.height())
        gradient.setColorAt(0.0, self.start_color)
        gradient.setColorAt(1.0, self.end_color)
        painter.fillRect(self.rect(), gradient)


class StylishButton(QPushButton):
    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setMinimumHeight(35)  # 缩小按钮高度
        self.setStyleSheet("""
            QPushButton {
                background-color: rgba(52, 152, 219, 180);
                color: white;
                border-radius: 5px;
                font-weight: bold;
                font-size: 13px;
                padding: 6px 12px;
            }
            QPushButton:hover {
                background-color: rgba(41, 128, 185, 220);
            }
            QPushButton:pressed {
                background-color: rgba(32, 102, 148, 255);
            }
        """)


class StylishSlider(QSlider):
    def __init__(self, orientation, parent=None):
        super().__init__(orientation, parent)
        self.setStyleSheet("""
            QSlider::groove:horizontal {
                height: 8px;
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #bdc3c7, stop:1 #2c3e50);
                border-radius: 4px;
            }
            QSlider::handle:horizontal {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #8e44ad, stop:1 #9b59b6);
                border: 1px solid #5c5c5c;
                width: 18px;
                margin-top: -5px;
                margin-bottom: -5px;
                border-radius: 9px;
            }
            QSlider::handle:horizontal:hover {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #9b59b6, stop:1 #8e44ad);
            }
        """)


class ImageFrame(QFrame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFrameShape(QFrame.StyledPanel)
        self.setFrameShadow(QFrame.Sunken)
        self.setLineWidth(2)
        self.setMidLineWidth(1)
        self.setMinimumHeight(260)  # 缩小最小高度
        self.setStyleSheet("""
            QFrame {
                background-color: rgba(255, 255, 255, 30);
                border-radius: 10px;
                border: 1px solid rgba(255, 255, 255, 50);
            }
        """)

        # Create layout and image label
        layout = QVBoxLayout(self)
        layout.setContentsMargins(5, 5, 5, 5)  # 减小内边距
        self.image_label = QLabel(self)
        self.image_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.image_label)
        self.setLayout(layout)

    def display_image(self, image):
        if image is not None:
            if len(image.shape) == 2:  # Grayscale
                h, w = image.shape
                q_img = QImage(image.data, w, h, w, QImage.Format_Grayscale8)
            else:  # Color
                h, w, c = image.shape
                bytes_per_line = c * w
                q_img = QImage(image.data, w, h, bytes_per_line, QImage.Format_RGB888)

            pixmap = QPixmap.fromImage(q_img)

            # Scale pixmap to fit in the frame while maintaining aspect ratio
            self.image_label.setPixmap(pixmap.scaled(
                self.image_label.width(),
                self.image_label.height(),
                Qt.KeepAspectRatio,
                Qt.SmoothTransformation
            ))


class ImageSegmentationApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("图像阈值分割工具")
        self.resize(975, 675)  # 将窗口尺寸缩小到原来的75%

        # 确保中文显示正确
        font = QFont("Microsoft YaHei", 10)  # 使用微软雅黑字体
        self.setFont(font)

        # Initialize variables
        self.original_image = None
        self.processed_image = None
        self.histogram_data = None
        self.gray_image = None  # 初始化gray_image属性

        # Create central widget with gradient background
        self.central_widget = GradientWidget()
        self.setCentralWidget(self.central_widget)

        # Create main layout
        main_layout = QVBoxLayout(self.central_widget)
        main_layout.setContentsMargins(15, 15, 15, 15)  # 减小边距
        main_layout.setSpacing(10)  # 减小间距

        # Create title label
        title_label = QLabel("图像阈值分割工具")
        title_label.setStyleSheet("""
            font-size: 24px;
            color: white;
            font-weight: bold;
            margin-bottom: 8px;
        """)
        title_label.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(title_label)

        # Create image display area
        image_layout = QHBoxLayout()
        image_layout.setSpacing(10)  # 减小间距

        # Original image frame
        original_layout = QVBoxLayout()
        original_layout.setSpacing(5)  # 减少标签和框之间的间距
        self.original_frame = ImageFrame()
        original_label = QLabel("原始图像")
        original_label.setStyleSheet("color: white; font-weight: bold; font-size: 18px;")
        original_label.setAlignment(Qt.AlignCenter)
        original_layout.addWidget(original_label)
        original_layout.addWidget(self.original_frame)

        # Processed image frame
        processed_layout = QVBoxLayout()
        processed_layout.setSpacing(5)  # 减少标签和框之间的间距
        self.processed_frame = ImageFrame()
        processed_label = QLabel("处理后图像")
        processed_label.setStyleSheet("color: white; font-weight: bold; font-size: 18px;")
        processed_label.setAlignment(Qt.AlignCenter)
        processed_layout.addWidget(processed_label)
        processed_layout.addWidget(self.processed_frame)

        # Histogram frame
        histogram_layout = QVBoxLayout()
        histogram_layout.setSpacing(5)  # 减少标签和框之间的间距
        self.histogram_frame = ImageFrame()
        histogram_label = QLabel("灰度直方图")
        histogram_label.setStyleSheet("color: white; font-weight: bold; font-size: 18px;")
        histogram_label.setAlignment(Qt.AlignCenter)
        histogram_layout.addWidget(histogram_label)
        histogram_layout.addWidget(self.histogram_frame)

        # Add frames to layout
        image_layout.addLayout(original_layout)
        image_layout.addLayout(processed_layout)
        image_layout.addLayout(histogram_layout)
        main_layout.addLayout(image_layout, 4)  # 增加图像区域的比例

        # Create controls area
        controls_frame = QFrame()
        controls_frame.setStyleSheet("""
            QFrame {
                background-color: rgba(255, 255, 255, 20);
                border-radius: 8px;
                border: 1px solid rgba(255, 255, 255, 40);
            }
        """)

        controls_layout = QVBoxLayout(controls_frame)
        controls_layout.setContentsMargins(15, 12, 15, 12)  # 减小内边距
        controls_layout.setSpacing(10)  # 减小间距

        # Algorithm selection
        algo_layout = QHBoxLayout()
        algo_label = QLabel("选择算法:")
        algo_label.setStyleSheet("color: white; font-weight: bold; font-size: 14px;")  # 减小字体
        self.algo_combo = QComboBox()
        self.algo_combo.addItems(["直方图双峰法", "迭代阈值分割", "最大类间方差阈值分割 (Otsu)"])
        self.algo_combo.setStyleSheet("""
            QComboBox {
                background-color: rgba(255, 255, 255, 30);
                color: white;
                border-radius: 4px;
                padding: 6px;
                min-height: 30px;
                font-size: 13px;
            }
            QComboBox::drop-down {
                border: none;
                width: 25px;
            }
            QComboBox QAbstractItemView {
                background-color: rgba(52, 73, 94, 220);
                color: white;
                selection-background-color: rgba(41, 128, 185, 220);
                font-size: 13px;
            }
        """)
        self.algo_combo.currentIndexChanged.connect(self.on_algorithm_changed)
        algo_layout.addWidget(algo_label)
        algo_layout.addWidget(self.algo_combo)
        controls_layout.addLayout(algo_layout)

        # Threshold slider
        threshold_layout = QHBoxLayout()
        threshold_label = QLabel("阈值:")
        threshold_label.setStyleSheet("color: white; font-weight: bold; font-size: 14px;")  # 减小字体
        self.threshold_value = QLabel("128")
        self.threshold_value.setStyleSheet("color: white; font-weight: bold; font-size: 14px;")  # 减小字体
        self.threshold_slider = StylishSlider(Qt.Horizontal)
        self.threshold_slider.setMinimumHeight(25)  # 减小滑块高度
        self.threshold_slider.setRange(0, 255)
        self.threshold_slider.setValue(128)
        self.threshold_slider.valueChanged.connect(self.on_threshold_changed)
        threshold_layout.addWidget(threshold_label)
        threshold_layout.addWidget(self.threshold_slider, 1)  # 让滑块占据更多空间
        threshold_layout.addWidget(self.threshold_value)
        controls_layout.addLayout(threshold_layout)

        # Buttons
        buttons_layout = QHBoxLayout()
        buttons_layout.setSpacing(10)  # 减小按钮间距
        self.load_button = StylishButton("加载图像")
        self.load_button.setMinimumHeight(40)  # 减小按钮高度
        self.load_button.clicked.connect(self.load_image)
        self.process_button = StylishButton("处理图像")
        self.process_button.setMinimumHeight(40)  # 减小按钮高度
        self.process_button.clicked.connect(self.process_image)
        self.save_button = StylishButton("保存结果")
        self.save_button.setMinimumHeight(40)  # 减小按钮高度
        self.save_button.clicked.connect(self.save_result)
        buttons_layout.addWidget(self.load_button)
        buttons_layout.addWidget(self.process_button)
        buttons_layout.addWidget(self.save_button)
        controls_layout.addLayout(buttons_layout)

        main_layout.addWidget(controls_frame, 1)

        # Set central widget layout
        self.central_widget.setLayout(main_layout)

    def load_image(self):
        try:
            file_path, _ = QFileDialog.getOpenFileName(
                self, "选择图像", "", "Image Files (*.png *.jpg *.jpeg *.bmp *.tif)"
            )

            if file_path:
                # 使用Unicode路径处理中文
                if os.name == 'nt':  # Windows系统
                    self.original_image = cv2.imdecode(np.fromfile(file_path, dtype=np.uint8), cv2.IMREAD_UNCHANGED)
                else:
                    self.original_image = cv2.imread(file_path)

                if self.original_image is None:
                    QMessageBox.warning(self, "错误", f"无法加载图像: {file_path}")
                    return

                # Convert to RGB for display
                self.original_image_rgb = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2RGB)
                self.original_frame.display_image(self.original_image_rgb)

                # Convert to grayscale for processing
                if len(self.original_image.shape) == 3:
                    self.gray_image = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2GRAY)
                else:
                    self.gray_image = self.original_image.copy()

                # Generate histogram
                self.update_histogram()

                # 设置窗口标题显示当前文件名
                file_name = os.path.basename(file_path)
                self.setWindowTitle(f"图像阈值分割工具 - {file_name}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"加载图像时出错: {str(e)}")
            print(f"加载图像错误: {str(e)}")  # 输出详细错误信息到控制台

    def update_histogram(self):
        try:
            if self.gray_image is None:
                return

            # 方法1:使用matplotlib生成直方图(支持中文)
            plt.figure(figsize=(4, 2.5), dpi=100)  # 缩小图表尺寸
            plt.hist(self.gray_image.ravel(), 256, [0, 256], color='skyblue', alpha=0.8)

            # 添加垂直线表示当前阈值
            threshold = self.threshold_slider.value()
            plt.axvline(x=threshold, color='r', linestyle='--', linewidth=2)

            # 添加标题和标签(支持中文)
            plt.title("灰度直方图", fontsize=12)
            plt.xlabel("像素值", fontsize=10)
            plt.ylabel("频率", fontsize=10)
            plt.text(threshold + 5, plt.gca().get_ylim()[1] * 0.9, f"阈值: {threshold}",
                     color='red', fontsize=10)
            plt.grid(True, linestyle='--', alpha=0.7)  # 添加网格线使图表更清晰
            plt.tight_layout()

            # 保存到内存中
            buf = io.BytesIO()
            plt.savefig(buf, format='png', dpi=100, bbox_inches='tight', pad_inches=0.1)
            buf.seek(0)

            # 从内存加载图像并显示
            hist_img = Image.open(buf)
            hist_array = np.array(hist_img)
            self.histogram_frame.display_image(hist_array)

            # 清理
            plt.close()
            buf.close()

        except Exception as e:
            QMessageBox.warning(self, "警告", f"更新直方图时出错: {str(e)}")
            print(f"直方图错误: {str(e)}")  # 输出详细错误信息到控制台

    def on_threshold_changed(self, value):
        self.threshold_value.setText(str(value))
        self.update_histogram()

        # 如果已经加载了图像,实时应用阈值
        if hasattr(self, 'gray_image') and self.gray_image is not None:
            _, binary = cv2.threshold(self.gray_image, value, 255, cv2.THRESH_BINARY)
            self.processed_image = binary
            self.processed_frame.display_image(binary)

    def on_algorithm_changed(self, index):
        # Enable/disable threshold slider based on selected algorithm
        if index == 2:  # Otsu
            self.threshold_slider.setEnabled(False)
        else:
            self.threshold_slider.setEnabled(True)

    def process_image(self):
        try:
            if self.gray_image is None:
                QMessageBox.information(self, "提示", "请先加载图像")
                return

            algo_index = self.algo_combo.currentIndex()

            if algo_index == 0:  # 直方图双峰法
                threshold = self.bimodal_threshold()
                self.threshold_slider.setValue(threshold)
            elif algo_index == 1:  # 迭代阈值分割
                threshold = self.iterative_threshold()
                self.threshold_slider.setValue(threshold)
            else:  # 最大类间方差阈值分割 (Otsu)
                threshold = self.otsu_threshold()
                self.threshold_slider.setValue(threshold)

            # Apply threshold
            _, binary = cv2.threshold(self.gray_image, threshold, 255, cv2.THRESH_BINARY)
            self.processed_image = binary
            self.processed_frame.display_image(binary)
        except Exception as e:
            QMessageBox.critical(self, "错误", f"处理图像时出错: {str(e)}")

    def bimodal_threshold(self):
        """直方图双峰法阈值分割"""
        if self.gray_image is None:
            return 128

        # Calculate histogram
        hist = cv2.calcHist([self.gray_image], [0], None, [256], [0, 256]).flatten()

        # Smooth histogram
        hist_smooth = np.convolve(hist, np.ones(5) / 5, mode='same')

        # Find peaks (local maxima)
        peaks = []
        for i in range(1, 255):
            if hist_smooth[i - 1] < hist_smooth[i] > hist_smooth[i + 1]:
                peaks.append((i, hist_smooth[i]))

        # Sort peaks by height
        peaks.sort(key=lambda x: x[1], reverse=True)

        # If we have at least 2 peaks
        if len(peaks) >= 2:
            # Get the two highest peaks
            peak1, peak2 = peaks[0][0], peaks[1][0]
            # Calculate threshold as the middle point between two peaks
            threshold = int((peak1 + peak2) / 2)
            return threshold
        else:
            # Fallback to current slider value
            return self.threshold_slider.value()

    def iterative_threshold(self):
        """迭代阈值分割"""
        if self.gray_image is None:
            return 128

        # Start with initial threshold (mean of image)
        threshold = int(np.mean(self.gray_image))

        # Iterative process
        max_iterations = 100
        for _ in range(max_iterations):
            # Segment image
            mask1 = self.gray_image >= threshold
            mask2 = self.gray_image < threshold

            # Calculate mean of each segment
            mean1 = np.mean(self.gray_image[mask1]) if np.any(mask1) else 0
            mean2 = np.mean(self.gray_image[mask2]) if np.any(mask2) else 0

            # Calculate new threshold
            new_threshold = int((mean1 + mean2) / 2)

            # Check convergence
            if abs(new_threshold - threshold) < 1:
                break

            threshold = new_threshold

        return threshold

    def otsu_threshold(self):
        """最大类间方差阈值分割 (Otsu)"""
        if self.gray_image is None:
            return 128

        # Use OpenCV's Otsu method
        threshold, _ = cv2.threshold(self.gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        return int(threshold)

    def save_result(self):
        try:
            if self.processed_image is None:
                QMessageBox.information(self, "提示", "请先处理图像")
                return

            # 获取当前日期时间作为默认文件名
            default_filename = f"processed_image_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"

            file_path, selected_filter = QFileDialog.getSaveFileName(
                self, "保存结果", default_filename,
                "PNG Files (*.png);;JPEG Files (*.jpg);;All Files (*)"
            )

            if file_path:
                # 确保文件有正确的扩展名
                if selected_filter == "PNG Files (*.png)" and not file_path.lower().endswith('.png'):
                    file_path += '.png'
                elif selected_filter == "JPEG Files (*.jpg)" and not file_path.lower().endswith(('.jpg', '.jpeg')):
                    file_path += '.jpg'

                # 使用Unicode路径处理中文
                try:
                    if os.name == 'nt':  # Windows系统
                        _, ext = os.path.splitext(file_path)
                        success = cv2.imencode(ext, self.processed_image)[1].tofile(file_path)
                        if not success:
                            raise Exception("无法保存图像")
                    else:
                        success = cv2.imwrite(file_path, self.processed_image)
                        if not success:
                            raise Exception("无法保存图像")

                    QMessageBox.information(self, "成功", f"图像已保存到: {file_path}")
                except Exception as e:
                    QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")
                    print(f"保存文件错误: {str(e)}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存图像时出错: {str(e)}")
            print(f"保存图像错误: {str(e)}")  # 输出详细错误信息到控制台


if __name__ == "__main__":
    # 启用高DPI支持
    if hasattr(Qt, 'AA_EnableHighDpiScaling'):
        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
        QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)

    # 设置默认编码为UTF-8
    if sys.platform.startswith('win'):
        # Windows平台需要设置控制台编码
        import subprocess

        subprocess.call('chcp 65001', shell=True)

    app = QApplication(sys.argv)
    app.setStyle('Fusion')  # Modern style

    # 设置应用程序范围的字体 - Windows使用微软雅黑,其他系统使用默认无衬线字体
    if sys.platform.startswith('win'):
        font = QFont("Microsoft YaHei", 9)
    else:
        font = QFont("Sans Serif", 9)
    app.setFont(font)

    # 修复 Windows 上的中文显示问题
    QApplication.setApplicationName("图像阈值分割工具")

    window = ImageSegmentationApp()
    window.show()

    sys.exit(app.exec_()) 

(2)运行结果截图

三.实验总结 


1.人工阈值分割
核心特点:通过手动设定固定阈值实现分割,原理简单直观,代码实现仅需调用cv2.threshold函数一行核心逻辑。
适用场景:仅适用于背景与目标灰度差异显著、直方图单峰且阈值经验已知的简单图像(如细胞显微图像xibao.jpg)。
局限性:阈值完全依赖人工经验,若图像光照不均或对比度变化,分割效果会显著下降,泛化能力极差。
2. 直方图双峰法(双峰法)
核心原理:通过高斯平滑直方图,自动定位前景与背景对应的双峰,取两峰间谷底作为阈值,避免了人工调参。
适用场景:严格依赖直方图存在明显双峰的假设,适用于目标与背景灰度分布分离的图像(如A.jpg)。
局限性:当图像直方图为单峰、多峰或双峰不明显时(如B.jpg),算法可能误将噪声或次要峰值视为目标峰,导致阈值偏移。实验中通过 “最远峰值法” 补救单峰情况,但分割精度仍低于 Otsu 法。
3. 迭代阈值分割
核心思想:从初始均值阈值出发,迭代计算前景与背景均值,通过动态更新阈值直至收敛,属于无监督自适应算法。
适用场景:对直方图分布无严格假设,适用于均值差异明显但双峰不突出的图像(如纹理图像zhiwen.jpg)。
优缺点:无需先验知识,鲁棒性较强;但迭代过程增加计算耗时,且初始阈值和收敛条件(如差值容限)可能影响最终结果。实验中初始值设为全局均值、容限设为 20 时,多数图像可在 5 次内收敛。
4. 最大类间方差法(Otsu)
算法优势:基于统计理论最大化类间方差,自动搜索全局最优阈值,无需人工干预,是实验中分割效果最稳定的方法。
适用场景:广泛适用于灰度分布服从双峰或高斯分布的图像(如车辆图像che.bmp),尤其在噪声较少时性能卓越。
实现对比:代码四中手动遍历阈值的复杂度为 O (256),而 OpenCV 内置THRESH_OTSU通过概率密度函数优化,计算效率更高,实际应用中应优先调用库函数。
5. PyQt 界面化集成
工具价值:通过图形界面将四种算法集成,支持动态调节阈值、实时预览分割结果,显著降低使用门槛。
交互设计:QSlider与算法联动(如人工阈值模式下直接控制阈值),QComboBox切换算法时自动适配逻辑(如双峰法无需手动阈值)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值