实验五.图像阈值分割

一.阈值分割介绍

阈值分割处理概述

阈值分割是数字图像处理中一种基于像素灰度值的区域分割技术,通过设定一个或多个灰度阈值,将图像像素分为目标(前景)背景两类,从而实现图像的初步分割。其核心思想是:利用目标与背景在灰度分布上的差异,通过阈值划分实现区域分离

阈值分割的关键要素

  1. 阈值(Threshold, T)

    • 单个阈值(单阈值分割):将图像分为前景(灰度≥T)和背景(灰度 < T),适用于目标与背景灰度差异明显的图像。
    • 多个阈值(多阈值分割):将图像分为多个区域(如前景 1、前景 2、背景),适用于复杂场景。
  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)
  1. 函数作用
    cv2.threshold()是 OpenCV 中用于实现阈值分割的函数,将灰度图像转换为二值图像(像素值仅为 0 或 255)。

  2. 参数说明

    • 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)
  • 迭代逻辑
    1. 将图像分为两部分(≥T 和 <T),分别计算其平均灰度值 m1(前景)和 m2(背景)。
    2. 取两者的平均值 (m1 + m2)/2 作为新阈值。
    3. 重复上述过程,直到新阈值与当前阈值的差异足够小(<20),此时认为算法收敛。
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

关键步骤

  1. 遍历阈值:从 0 到 255 遍历所有可能的阈值 i
  2. 分类统计:将像素分为两类,分别计算其均值mean1mean2)和权重w1w2)。
  3. 类间方差计算:使用公式 w1​⋅w2​⋅(μ1​−μ2​)2 计算类间方差。
  4. 最优阈值选择:保存使类间方差最大的阈值 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}")

处理流程

  1. 灰度转换:将原始彩色图像转换为单通道灰度图。
  2. 阈值分割:使用cv2.threshold函数,将灰度值≥value的像素设为 255(白色),其余设为 0(黑色)。
  3. 结果显示:在界面的第一个处理位置(索引 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 像素显示。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值