使用OpenCV与PySide(PyQt)的视觉检测小项目练习

        OpenCV 提供了丰富的图像处理和计算机视觉功能,可以实现各种复杂的图像处理任务,如目标检测、人脸识别、图像分割等。

        PyQt(或PySide)是一个创建GUI应用程序的工具包,它是Python编程语言和Qt库的成功融合。Qt库是最强大的GUI库之一。Qt的快速界面编辑工具Qt Designer提供了直观的可视化界面设计环境,通过拖拽和放置控件来设计界面,简化了界面设计的过程。PyQt提供了丰富的控件库,同时支持多种媒体文件的展示。尤其是PyQt的信号与槽的刷新机制提供了高效和可靠的信号响应机制。

        下面以一个实际的项目搭建过程为demo,尝试联合使用以上两个库,力争各尽所长。原则上,前端的界面显示和操作交给PySide,后台的图像处理交给OpenCV。

这是一个显微拍照画面内的轮廓识别和尺寸测量、数量统计项目。

一、显示界面框架搭建

1、主界面

主界面利用Qt Designer 制作,命名为main_window.ui并保存。

 主按钮站:

应该达到的运行效果:

2、主界面的按钮

 按钮有两种:

第一种是“点动”式的,图标为双状态,例如“新建项目”按钮。其样式表为:

第二种是“翻转“式的,每点击一次状态反转,即:可以反转”checked“状态。按钮图标为三个状态,例如“局部放大”按钮。其样式表为: 

 这种按钮,自定义了一个特性:activated来取代系统自带的checked,当这个特性activated="true"时,改变按钮的背景色。当然也可以使用系统自带的checked特性来实现同样的功能,这里的目的主要是练习一下 按钮的自定义特性的应用。

使用系统自带的checked特性:

两种方法在显示上的微妙差别如下:左边是自定义特性的,右边是使用系统自带的checked特性来实现的。区别在于系统自带的checked特性显示的边框是pressed,即按下时的边框特性。

3、阶梯渐变的色条

主界面的颜色样例条,自定义脚本,命名为GradientLabel.py:

from PySide6.QtGui import QPainter, QColor, QLinearGradient
from PySide6.QtWidgets import QMainWindow, QLabel, QVBoxLayout, QWidget
from PySide6.QtCore import Qt


class GradientLabel(QLabel):
    
    # 定义颜色
    def def_colors(self, begin_color, mid_color, end_color):
        self.begin_color = begin_color
        self.mid_color = mid_color
        self.end_color = end_color

    # 重新定义绘画事件
    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)

        gradient = QLinearGradient(0, 0, 0, self.height())
        gradient.setColorAt(1, self.end_color)
        gradient.setColorAt(0.5, self.mid_color)
        gradient.setColorAt(0, self.begin_color)

        painter.fillRect(self.rect(), gradient)


class MyMainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        central_widget = QWidget(self)
        self.setCentralWidget(central_widget)

        layout = QVBoxLayout(central_widget)

        gradient_label = GradientLabel(self)
        begin = QColor(255, 100, 0)
        mid = QColor(8, 180, 8)
        end = QColor(80, 80, 255)
        gradient_label.def_colors(begin, mid, end)

        gradient_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(gradient_label)

        self.setWindowTitle("Gradient Label Example")


if __name__ == "__main__":
    import sys
    from PySide6.QtWidgets import QApplication

    app = QApplication(sys.argv)
    window = MyMainWindow()
    window.show()
    sys.exit(app.exec())

 运行效果:

 将自定义脚本保存在主程序脚本同目录,并在Qt Designer 将颜色样例条”提升为“该自定义脚本。

4、图像显示区的自定义脚本

 这是一个QLabel,其显示的内容为QPixmap。脚本逐步再完善。

二、主程序脚本框架搭建 

1、目录结构

 JSON:存放json文件

MEDIA:存放媒体文件

PROJECT:工程文件

PYS:存放脚本

SRC:按钮图标等源文件

UIS:存放显示界面文件

2、编写初步的主程序框架

 首先要使用pyuic和pyrcc工具将图像资源转换成py文件并与主程序脚本放置在同一文件夹下,然后编写主程序脚本:

# 这是一个图像处理小应用的示例脚本。

# encoding: utf-8
import json
import sys

from PySide6.QtCore import QObject
from PySide6.QtWidgets import QApplication, QMainWindow

import main_window_rc  # 导入主画面


# 定义主窗口
class MainWindow(QMainWindow, main_window_rc.Ui_MainWindow):
    def __init__(self):
        super().__init__()


# ################公用的作业函数#############################
class Jobs:
    @staticmethod
    # 读取JSON文件,分配参数
    def read_json():
        with open('../JSON/setting.json', 'r', encoding='utf-8') as file_json:
            ui.json_data = json.load(file_json)
            ui.settings = ui.json_data['setting']  # 项目参数的定义

    @staticmethod
    # 系统的初始化
    def start_todo():
        pass

    # 退出前的操作
    @staticmethod
    def before_quit():
        with open('../JSON/setting.json', 'w') as file:  # 保存json文件
            json.dump(ui.json_data, file, indent=4)


# ################图像处理的过程函数#############################
def Image_processing(steps):
    for step in steps:
        # 系统的初始化
        if step == 'start':
            pass

        # step0,步骤0
        if step == 0:
            pass
            continue

        # step1,步骤1
        if step == 1:
            pass
            continue


        # step2,步骤2
        if step == 2:
            pass
            continue


# ###########################信号的连接和槽函数####################################
def signal_slot():
        # #####################主窗口的信号和槽####################################
    pass

# #############################主程序###################################
if __name__ == '__main__':
    app = QApplication(sys.argv)


    # #######################项目级别的定义###################################
    class UI(QObject):  # 将项目定义为QObject,用来管理项目级别的信号和变量
        # ###########__init__###############
        def __init__(self):
            super().__init__()


    # ########################本项目的实例化###################################
    ui = UI()  # 项目实例化

    # ########################实例化画面#################################
    window1 = MainWindow()  # 主画面实例化

    window1.show()  # 显示画面
    window1.setupUi(window1)  # 画面初始化

    Jobs.start_todo()  # 系统初始化
    signal_slot()  # 信号与槽的定义

    app.aboutToQuit.connect(Jobs.before_quit)  # 退出系统之前的操作

    sys.exit(app.exec())

 本阶段运行截图:

三、编写各个功能脚本

 1、提高清晰度的相机

原理:通过连续拍摄多张照片并求像素平均值的方法来减少图像噪点,获得较为清晰的照片。

 脚本FilterCamera.py:

from PySide6.QtCore import QObject, QTimer
from PySide6.QtWidgets import QApplication

import cv2
import numpy as np
import threading
import sys


class FilterCamera:
    def __init__(self, cap, frame, num=5):
        self.cap = cap
        self.num = num
        frame_float = frame.astype(float)
        self.frames = [frame_float] * self.num
        self.sum_frame = sum(self.frames)
        self.filtered_frame = self.sum_frame / self.num

    def frame_out(self):
        r, f = self.cap.read()
        if r:
            frame_float = f.astype(float)
            self.sum_frame -= self.frames[0]
            self.sum_frame += frame_float
            self.filtered_frame = (self.sum_frame / self.num).astype(np.uint8)
            self.frames = self.frames[1:] + [frame_float]
            return self.filtered_frame
            # cv2.imshow('Average Frame', self.average_frame)


def show_frame(cam):
    f = cam.frame_out()
    cv2.imshow('Average Frame', f)

# #############################主程序###################################
if __name__ == '__main__':
    app = QApplication(sys.argv)

    video_timer = QTimer()
    cam0 = cv2.VideoCapture(0)
    ret, frame = cam0.read()

    if ret:
        filter_cam = FilterCamera(cam0, frame)
        video_timer.start(50)
        video_timer.timeout.connect(lambda:  show_frame(filter_cam))

        sys.exit(app.exec())

 2、关于相机的查找、激活、刷新视频

相机的相关功能操作均在主程序脚本内完成。当前阶段的主脚本:

# 这是一个图像处理小应用的示例脚本。

# encoding: utf-8
import json
import sys
import threading

import cv2
from PySide6.QtCore import QObject, QTimer
from PySide6.QtGui import QColor, QImage, QPixmap
from PySide6.QtWidgets import QApplication, QMainWindow

import main_window_rc  # 导入主画面
from FilterCamera import FilterCamera


# 定义主窗口
class MainWindow(QMainWindow, main_window_rc.Ui_MainWindow):
    def __init__(self):
        super().__init__()


# ################公用的作业函数#############################
class Jobs:
    @staticmethod
    # 查找本地可以用的相机
    def find_camera(max_camera=3):
        # 先禁用相机按钮和新建按钮
        window1.btn_capture.setEnabled(False)
        window1.btn_capture.setToolTip('正在查找和初始化相机')

        window1.btn_new.setEnabled(False)
        window1.btn_new.setToolTip('正在查找和初始化相机')

        # 如果没有查到相机
        def no_camera():
            window1.btn_capture.setToolTip('未查找到可用相机')
            window1.btn_new.setToolTip('未查找到可用相机')
            ui.th1.stop()  # 结束查找相机的进程

        # 查找相机的定时器,超过这个时间没有查找到相机就认为没有相机
        ui.timer1_find_camera = threading.Timer(30, no_camera)
        ui.timer1_find_camera.start()
        # 项目内所有的相机
        ui.cameras = []
        cameras = [cv2.VideoCapture(x) for x in range(max_camera) if cv2.VideoCapture(x).isOpened()]  # 可用相机的列表

        for cam in cameras:
            ret, frame = cam.read()
            if ret:
                filter_camera = FilterCamera(cam, frame)  # FilterCamera是自定义的滤波相机,连续拍摄多张图片并平均,用以提高像质
                ui.cameras.append(filter_camera)
        # 如果找到了相机
        if ui.cameras:
            window1.comboBox_cameras.addItems([f'相机{x}' for x in range(len(ui.cameras))])  # 更新相机选择组合框的下拉列表
            window1.btn_capture.setToolTip('实时影像/拍照取样')
            window1.btn_capture.setEnabled(True)
            window1.btn_new.setToolTip('新建检测项目')
            window1.btn_new.setEnabled(True)
            ui.timer1_find_camera.cancel()  # 结束查找相机的定时器
            Jobs.activate_camera(0)  # 激活相机0

    @staticmethod
    # 按照给定的序号激活相机
    def activate_camera(i):
        ui.activated_camera = ui.cameras[i]  # 激活的相机

    @staticmethod
    # 读取JSON文件,分配参数
    def read_json():
        with open('../JSON/setting.json', 'r', encoding='utf-8') as file_json:
            ui.json_data = json.load(file_json)
            ui.settings = ui.json_data['setting']  # 项目参数的定义

    @staticmethod
    # 将OpenCV格式的图像转换为PySide格式,并在小部件上显示
    def img2Widget(img, widget):
        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 转换 BGR 到 RGB
        # 转换图像到QT的QImage格式
        img_height, img_width, channels = img_rgb.shape  # 获取形状
        bytes_per_line = channels * img_width  # 每行字节数
        q_img = QImage(img_rgb.data, img_width, img_height, bytes_per_line, QImage.Format_RGB888)  # 转换成QImage格式

        pixmap = QPixmap.fromImage(q_img)  # 转换成QPixmap格式
        widget.set_src(pixmap)  # 将图像设置为部件的源图像

    @staticmethod
    # 刷新视频帧
    def update_frame():
        if ui.video_play and ui.activated_camera:  # 是否实时播放视频
            ui.orig_img = ui.activated_camera.frame_out()  # 从激活的相机获取图像数据
            ui.src_img = ui.activated_camera.frame_out()  # 从激活的相机获取图像数据
            Jobs.img2Widget(ui.src_img, window1.label_show)  # 将OpenCV格式的图像转换为PySide格式,并在小部件上显示

    @staticmethod
    # 系统的初始化
    def start_todo():
        # 读取json文件并分配变量
        Jobs.read_json()
        # ######################主窗口的部件初始化###################################
        window1.move(0, 0)
        window1.masterBtnStation = window1.btnGroup.children()  # 主窗口的按钮站
        window1.imgAdjustGroup.hide()  # 图像微调的部件群
        # 图像微调的部件的初始化
        window1.slider_threshold_black.setValue(ui.settings['threshold_black'][0])
        window1.slider_noise_min.setValue(ui.settings['noise_min'][0])
        window1.slider_min_bubble.setValue(ui.settings['min_bubble'][0])

        # 主窗口的部件的功能提示tips的定义
        window1.comboBox_cameras.setToolTip('选择相机')
        window1.btn_new.setToolTip('新建检测项目')
        window1.btn_capture.setToolTip('相机拍照')
        window1.btn_openImg.setToolTip('打开图片文件')
        window1.btn_handle.setToolTip('处理图片')
        window1.btn_saveFile.setToolTip('保存当前图片')
        window1.btn_zoom.setToolTip('局部放大')
        window1.btn_redraw.setToolTip('恢复原始显示')
        window1.btn_OPT_adjust.setToolTip('光学尺寸整定')
        window1.btn_penCut.setToolTip('手动打断粘连')
        window1.btn_imgAdjust.setToolTip('图像调整')
        window1.btn_viewData.setToolTip('查看图像数据')
        window1.btn_redo.setToolTip('回退一步')
        window1.btn_del.setToolTip('删除所有')
        window1.btn_crop.setToolTip('裁切图像,用以处理')
        window1.btn_src.setToolTip('恢复原图')

        # 设置颜色条的参数
        gradient_color = ui.settings['gradient_color']  # 梯度颜色的定义
        begin_color = QColor(*gradient_color[0])
        mid_color = QColor(*gradient_color[1])
        end_color = QColor(*gradient_color[2])
        window1.color_bar.def_colors(begin_color, mid_color, end_color)
        window1.colorBar_range = ui.settings["colorBar_range"]  # 颜色条针对的尺寸范围(目前的单位是pixel,过后换成微米)
        ui.color_scale_min = window1.colorBar_range[0]  # 颜色条最小值
        ui.color_scale_max = window1.colorBar_range[1]  # 颜色条最大值
        scale = ui.color_scale_min  # 第一个进度条的值是颜色条最小值(window1.colorBar_range[0])
        step = (ui.color_scale_max - ui.color_scale_min) / 9  # 刻度的间距,window1.colorBar_range[1]是最大值
        scales = sorted(window1.colorScaleGroup.children(), key=lambda s: s.objectName())
        for s in scales:
            s.setText(str(int(scale)))
            scale += step

        # 查找相机的线程
        ui.th1 = threading.Thread(target=Jobs.find_camera)
        ui.th1.start()

        # 视频显示窗口的参数
        ui.video_play = False  # 是否实时播放视频
        ui.video_FPS = ui.settings["video_FPS"]  # 视频帧率
        ui.timer_video = QTimer()  # 视频的帧刷新节拍定时器
        ui.timer_video.start(1000 // ui.video_FPS[0])

        Image_processing(['start'])  # demo图像预处理

    # 退出前的操作
    @staticmethod
    def before_quit():
        with open('../JSON/setting.json', 'w') as file:  # 保存json文件
            json.dump(ui.json_data, file, indent=4)


# ################图像处理的过程函数#############################
def Image_processing(steps):
    for step in steps:
        # 系统的初始化
        if step == 'start':
            pass

        # step0,步骤0
        if step == 0:
            pass
            continue

        # step1,步骤1
        if step == 1:
            pass
            continue

        # step2,步骤2
        if step == 2:
            pass
            continue


# ###########################信号的连接和槽函数####################################
def signal_slot():
    # #####################主窗口的信号和槽####################################
    # 视频播放的帧定时器的连接
    ui.timer_video.timeout.connect(lambda: Jobs.update_frame())


# #############################主程序###################################
if __name__ == '__main__':
    app = QApplication(sys.argv)


    # #######################项目级别的定义###################################
    class UI(QObject):  # 将项目定义为QObject,用来管理项目级别的信号和变量
        # ###########__init__###############
        def __init__(self):
            super().__init__()


    # ########################本项目的实例化###################################
    ui = UI()  # 项目实例化

    # ########################实例化画面#################################
    window1 = MainWindow()  # 主画面实例化

    window1.show()  # 显示画面
    window1.setupUi(window1)  # 画面初始化

    Jobs.start_todo()  # 系统初始化
    signal_slot()  # 信号与槽的定义

    app.aboutToQuit.connect(Jobs.before_quit)  # 退出系统之前的操作

    sys.exit(app.exec())

3、消息框 

当然可以使用QMessageBox来实现消息框。不过自己亲自搞的话可以个性化一些。

当相机查询失败和新建检测项目时显示相应的消息框。

当前进度的主脚本

# 这是一个图像处理小应用的示例脚本。

# encoding: utf-8
import json
import os
import sys
import threading
import time

import cv2
from PySide6.QtCore import QObject, QTimer
from PySide6.QtGui import QColor, QImage, QPixmap
from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox

import main_window_rc  # 导入主画面
from FilterCamera import FilterCamera


# 定义主窗口
class MainWindow(QMainWindow, main_window_rc.Ui_MainWindow):
    def __init__(self):
        super().__init__()


# ################公用的作业函数#############################
class Jobs:
    @staticmethod
    # 查找本地可以用的相机
    def find_camera(max_camera=3):
        # 先禁用相机按钮和新建按钮
        window1.btn_capture.setEnabled(False)
        window1.btn_capture.setToolTip('正在查找和初始化相机')

        window1.btn_new.setEnabled(False)
        window1.btn_new.setToolTip('正在查找和初始化相机')

        # 如果没有查到相机
        def no_camera():
            window1.btn_capture.setToolTip('未查找到可用相机')
            window1.btn_new.setToolTip('未查找到可用相机')
            window1.form_alarm_message.setText('未查找到可用相机')
            window1.form_alarm.show()
            # message_box = QMessageBox.warning(window1, "错误", "未查找到可用相机")
            # message_box.move(200,200)
            # QMessageBox.warning(window1, "错误", "未查找到可用相机")
            # window1.message_frame.setHidden(False)

        # 查找相机的定时器,超过这个时间没有查找到相机就认为没有相机
        ui.timer1_find_camera = threading.Timer(30, no_camera)
        ui.timer1_find_camera.start()
        # 项目内所有的相机
        ui.cameras = []
        cameras = [cv2.VideoCapture(x) for x in range(max_camera) if cv2.VideoCapture(x).isOpened()]  # 可用相机的列表

        for cam in cameras:
            ret, frame = cam.read()
            if ret:
                filter_camera = FilterCamera(cam, frame)  # FilterCamera是自定义的滤波相机,连续拍摄多张图片并平均,用以提高像质
                ui.cameras.append(filter_camera)
        # 如果找到了相机
        if ui.cameras:
            window1.comboBox_cameras.addItems([f'相机{x}' for x in range(len(ui.cameras))])  # 更新相机选择组合框的下拉列表
            window1.btn_capture.setToolTip('实时影像/拍照取样')
            window1.btn_capture.setEnabled(True)
            window1.btn_new.setToolTip('新建检测项目')
            window1.btn_new.setEnabled(True)
            ui.timer1_find_camera.cancel()  # 结束查找相机的定时器
            Jobs.activate_camera(0)  # 激活相机0

    @staticmethod
    # 按照给定的序号激活相机
    def activate_camera(i):
        ui.activated_camera = ui.cameras[i]  # 激活的相机

    @staticmethod
    # 读取JSON文件,分配参数
    def read_json():
        with open('../JSON/setting.json', 'r', encoding='utf-8') as file_json:
            ui.json_data = json.load(file_json)
            ui.settings = ui.json_data['setting']  # 项目参数的定义

    @staticmethod
    # 将OpenCV格式的图像转换为PySide格式,并在小部件上显示
    def img2Widget(img, widget):
        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 转换 BGR 到 RGB
        # 转换图像到QT的QImage格式
        img_height, img_width, channels = img_rgb.shape  # 获取形状
        bytes_per_line = channels * img_width  # 每行字节数
        q_img = QImage(img_rgb.data, img_width, img_height, bytes_per_line, QImage.Format_RGB888)  # 转换成QImage格式

        pixmap = QPixmap.fromImage(q_img)  # 转换成QPixmap格式
        widget.set_src(pixmap)  # 将图像设置为部件的源图像

    @staticmethod
    # 刷新视频帧
    def update_frame():
        if ui.video_play and ui.activated_camera:  # 是否实时播放视频
            ui.orig_img = ui.activated_camera.frame_out()  # 从激活的相机获取图像数据
            ui.src_img = ui.activated_camera.frame_out()  # 从激活的相机获取图像数据
            Jobs.img2Widget(ui.src_img, window1.label_show)  # 将OpenCV格式的图像转换为PySide格式,并在小部件上显示

    @staticmethod
    # 系统的初始化
    def start_todo():
        # 读取json文件并分配变量
        Jobs.read_json()
        # ######################主窗口的部件初始化###################################
        window1.move(0, 0)
        window1.masterBtnStation = window1.btnGroup.children()  # 主窗口的按钮站
        # 隐藏消息窗口
        window1.form_alarm.hide()
        window1.form_info.hide()
        # 设置消息框的自动换行
        window1.form_alarm_message.setWordWrap(True)
        window1.form_info_message.setWordWrap(True)
        window1.imgAdjustGroup.hide()  # 隐藏图像微调的部件群
        # 图像微调的部件的初始化
        window1.slider_threshold_black.setValue(ui.settings['threshold_black'][0])
        window1.slider_noise_min.setValue(ui.settings['noise_min'][0])
        window1.slider_min_bubble.setValue(ui.settings['min_bubble'][0])

        # 主窗口的部件的功能提示tips的定义
        window1.comboBox_cameras.setToolTip('选择相机')
        window1.btn_new.setToolTip('新建检测项目')
        window1.btn_capture.setToolTip('相机拍照')
        window1.btn_openImg.setToolTip('打开图片文件')
        window1.btn_handle.setToolTip('处理图片')
        window1.btn_saveFile.setToolTip('保存当前图片')
        window1.btn_zoom.setToolTip('局部放大')
        window1.btn_redraw.setToolTip('恢复原始显示')
        window1.btn_OPT_adjust.setToolTip('光学尺寸整定')
        window1.btn_penCut.setToolTip('手动打断粘连')
        window1.btn_imgAdjust.setToolTip('图像调整')
        window1.btn_viewData.setToolTip('查看图像数据')
        window1.btn_redo.setToolTip('回退一步')
        window1.btn_del.setToolTip('删除所有')
        window1.btn_crop.setToolTip('裁切图像,用以处理')
        window1.btn_src.setToolTip('恢复原图')

        # 设置颜色条的参数
        gradient_color = ui.settings['gradient_color']  # 梯度颜色的定义
        begin_color = QColor(*gradient_color[0])
        mid_color = QColor(*gradient_color[1])
        end_color = QColor(*gradient_color[2])
        window1.color_bar.def_colors(begin_color, mid_color, end_color)
        window1.colorBar_range = ui.settings["colorBar_range"]  # 颜色条针对的尺寸范围(目前的单位是pixel,过后换成微米)
        ui.color_scale_min = window1.colorBar_range[0]  # 颜色条最小值
        ui.color_scale_max = window1.colorBar_range[1]  # 颜色条最大值
        scale = ui.color_scale_min  # 第一个进度条的值是颜色条最小值(window1.colorBar_range[0])
        step = (ui.color_scale_max - ui.color_scale_min) / 9  # 刻度的间距,window1.colorBar_range[1]是最大值
        scales = sorted(window1.colorScaleGroup.children(), key=lambda s: s.objectName())
        for s in scales:
            s.setText(str(int(scale)))
            scale += step

        # 查找相机的线程
        ui.th1 = threading.Thread(target=Jobs.find_camera)
        ui.th1.start()

        # 视频显示窗口的参数
        ui.video_play = False  # 是否实时播放视频
        ui.video_FPS = ui.settings["video_FPS"]  # 视频帧率
        ui.timer_video = QTimer()  # 视频的帧刷新节拍定时器
        ui.timer_video.start(1000 // ui.video_FPS[0])

        Image_processing(['start'])  # demo图像预处理

    # 退出前的操作
    @staticmethod
    def before_quit():
        with open('../JSON/setting.json', 'w') as file:  # 保存json文件
            json.dump(ui.json_data, file, indent=4)


# ################图像处理的过程函数#############################
def Image_processing(steps):
    for step in steps:
        # 系统的初始化
        if step == 'start':
            pass

        # step0,步骤0
        if step == 0:
            pass
            continue

        # step1,步骤1
        if step == 1:
            pass
            continue

        # step2,步骤2
        if step == 2:
            pass
            continue


# ###########################信号的连接和槽函数####################################
def signal_slot():
    # #####################主窗口的信号和槽####################################
    # 视频播放的帧定时器的连接
    ui.timer_video.timeout.connect(lambda: Jobs.update_frame())

    # “新建项目”按钮点击的槽函数
    def btn_new_clicked():
        now = time.localtime()
        time_str = time.strftime("%Y%m%d_%H%M%S", now)
        project_path = f'../PROJECT/{time_str}'
        # 检查路径是否存在,如果不存在则创建
        try:
            os.makedirs(project_path, exist_ok=True)
            QMessageBox.information(window1, "创建成功", f"已成功创建检测项目文件夹:\n{project_path}")
            ui.project_path = project_path
        except Exception as e:
            QMessageBox.critical(window1, "错误", f"创建文件夹时发生错误:\n{str(e)}")
        # project_path = QFileDialog.

    window1.btn_new.clicked.connect(btn_new_clicked)

    # #####################消息窗口的信号和槽####################################
    # “确定”按钮点击的槽函数
    def window1_form_alarm_btnOK_clicked():
        window1.form_alarm.hide()
    window1.form_alarm_btnOK.clicked.connect(window1_form_alarm_btnOK_clicked)

    # “确定”按钮点击的槽函数
    def window1_form_info_btnOK_clicked():
        window1.form_info.hide()

    window1.form_info_btnOK.clicked.connect(window1_form_info_btnOK_clicked)


# #############################主程序###################################
if __name__ == '__main__':
    app = QApplication(sys.argv)

    # #######################项目级别的定义###################################
    class UI(QObject):  # 将项目定义为QObject,用来管理项目级别的信号和变量
        # ###########__init__###############
        def __init__(self):
            super().__init__()


    # ########################本项目的实例化###################################
    ui = UI()  # 项目实例化

    # ########################实例化画面#################################
    window1 = MainWindow()  # 主画面实例化

    window1.show()  # 显示画面
    window1.setupUi(window1)  # 画面初始化

    Jobs.start_todo()  # 系统初始化
    signal_slot()  # 信号与槽的定义

    app.aboutToQuit.connect(Jobs.before_quit)  # 退出系统之前的操作

    sys.exit(app.exec())

4、图像处理的功能 

# ################图像处理的过程函数#############################
def Image_processing(steps):
    for step in steps:
        # 系统的初始化
        if step == 'start':
            # ui.orig_img = cv2.imread('../MEDIA/logo.jpg')  # 获取原始素材图像
            # # ui.src_img = ui.orig_img
            # ui.src_img = ui.orig_img.copy()  # 定义图像处理源图像
            # ui.show_img = ui.src_img.copy()  # 定义显示图像
            # Jobs.img2Widget(ui.show_img, window1.label_show)  # 将OpenCV格式的图像转换为PySide格式,并在小部件上显示
            continue

        # step0,步骤0
        if step == 0:
            # 打开原图转成灰度图,并复制一份显示效果为灰度,但色彩为三通道的图,用以作为最终效果的显示
            if ui.src_img.size > 0:
                ui.gray_img = cv2.cvtColor(ui.src_img, cv2.COLOR_BGR2GRAY)  # 将图像处理源图像转成灰度,用以进行后面的运算
                ui.gray_image_3ch = cv2.cvtColor(ui.gray_img, cv2.COLOR_GRAY2BGR)  # 保留一份显示效果为灰度,但色彩为三通道的图,用以作为最终效果的显示
                continue

        # step1,步骤1
        if step == 1:
            # 将灰度图转为二值图
            # 定义黑阈值
            ui.threshold_black = Jobs.parameter_processing(window1.slider_threshold_black.value(), 5, 255)
            # 使用threshold阈值法进行图像阈值分割
            binary_tmp = cv2.threshold(ui.gray_img, ui.threshold_black, 255,
                                       cv2.THRESH_BINARY_INV)[1]  # 转成黑白二值的临时图
            ui.noise_min = Jobs.parameter_processing(window1.slider_noise_min.value(), 1, 29, True)  # 定义杂斑阈值
            # window1.value_noise_min.setText(str(ui.noise_min))
            kernel = np.ones((ui.noise_min, ui.noise_min), np.uint8)  # 去除杂斑的核
            ui.binary_img = cv2.morphologyEx(binary_tmp, cv2.MORPH_OPEN, kernel)  # 去除杂斑
            # 切断粘连轮廓的线们
            if window1.label_show.src_lines:
                for l in window1.label_show.src_lines:
                    cv2.line(ui.binary_img, (int(l[0]), int(l[1])), (int(l[2]), int(l[3])), (0, 0, 0), 2)   # 按坐标画切断线
            ui.show_img = cv2.addWeighted(ui.binary_img, 1, ui.gray_img, 0.5, 0)
            Jobs.img2Widget(ui.show_img, window1.label_show)
            continue

        # step2,步骤2
        if step == 2:
            tmp_img = np.zeros_like(ui.src_img)  # 临时图像容器
            ui.min_bubble = Jobs.parameter_processing(window1.slider_min_bubble.value(), 1, 500)
            ui.contours = cv2.findContours(ui.binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]  # 从临时轮廓图中查找轮廓们
            ui.contour_img = Jobs.fill_contours(tmp_img, ui.contours)
            ui.show_img = cv2.addWeighted(ui.contour_img, 1, ui.gray_image_3ch, 0.5, 0)
            Jobs.img2Widget(ui.show_img, window1.label_show)
            continue

5、图像显示框的自定义脚本 ImageLabel.py

from PySide6.QtCore import Qt, QRect, Signal, QPoint, QLine
from PySide6.QtGui import QPixmap, QPainter, QPen, QColor
from PySide6.QtWidgets import QLabel


class ImageLabel(QLabel):
    src_info = Signal(list)  # 原图信息
    crop_info = Signal(list)  # 从原图裁切到的图像实际像素
    show_info = Signal(list)  # 显示窗口的显示像素
    crop_rect = Signal(list)  # 向外发射的从源图像上得到的裁切框(图像处理用的)

    def __init__(self, parent=None):
        super().__init__(parent)

        self.src_pixmap = QPixmap()  # 源图像
        # self.src_x = 0  # 当前显示的部分原图,x起点相对于原图原点的坐标
        # self.src_y = 0  # 当前显示的部分原图,y起点相对于原图原点的坐标
        self.draw_tool = ''  # 画图工具,可能的值:crop:裁切;pen:画线;zoom:放大

        self.scale = 1.0  # 显示比例,其含义为当前的显示窗口的每个像素代表原始图几个像素

        self.showing_src = QPixmap()  # 当前显示的图像内容(从原图原比例裁切而来)
        self.scaled_image = QPixmap()  # 当前显示的屏幕内容(经过缩放适应窗口的)
        self.scaled_x = 0  # 显示内容的屏幕x起点
        self.scaled_y = 0  # 显示内容的屏幕y起点
        self.erase = False  # 擦除已有的方框

        self.start_pos = QPoint()  # 鼠标起始点
        self.end_pos = QPoint()  # 鼠标结束点

        self.crop_scaled = QRect(0, 0, 1, 1)  # 显示屏幕维度的方框
        self.crop_src = QRect(0, 0, 1, 1)  # 源图像实际像素维度的方框

        self.draw_line = QLine(0, 0, 1, 1)  # 显示窗口维度的直线
        self.scaled_lines = []  # 屏幕上画的线们
        self.src_lines = []  # 原图画的线们
        self.drwLine_pixmap = QPixmap(self.width(), self.height())  # 屏幕上划线用的底图
        self.drwLine_pixmap.fill(QColor('#00000000'))  # 设为透明色

        self.line_drawing = False  # 正在画线状态还是画完展示状态
        self.redrew = False  # 是否完成了重绘

    # 重新定义鼠标按下事件
    def mousePressEvent(self, event):

        if event.button() == Qt.LeftButton:
            if self.draw_tool in ['zoom', 'crop']:  # 放大显示或裁切状态
                self.start_pos = event.position().toPoint()
                self.erase = False  # 这一行决定了松开鼠标后方框是否擦除

            elif self.draw_tool == 'pen':
                self.line_drawing = True
                point = event.position()  # 获取当前的鼠标位置
                # 如果鼠标位置在图像区域内
                if (self.scaled_x <= point.x() <= (self.scaled_x + self.scaled_image.width())) \
                        and (self.scaled_y <= point.y() <= (self.scaled_y + self.scaled_image.height())):
                    self.start_pos = point.toPoint()
                else:
                    self.start_pos = QPoint(-1, -1)

    # 重新定义鼠标移动事件
    def mouseMoveEvent(self, event):
        if self.start_pos:
            if self.draw_tool in ['zoom', 'crop']:
                self.end_pos = event.position().toPoint()
                self.crop_scaled = QRect(self.start_pos, self.end_pos)
                self.update()
            elif self.draw_tool == 'pen' and self.start_pos != QPoint(-1, -1):
                point = event.position()  # 获取当前的鼠标位置
                # 如果鼠标位置在图像区域内
                if (self.scaled_x <= point.x() <= (self.scaled_x + self.scaled_image.width())) \
                        and (self.scaled_y <= point.y() <= (self.scaled_y + self.scaled_image.height())):
                    self.end_pos = point.toPoint()
                    self.draw_line = QLine(self.start_pos, self.end_pos)
                    self.redrew = False
                    self.update()

    # 重新定义鼠标松开事件
    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            if self.draw_tool in ['zoom', 'crop']:
                self.end_pos = event.position().toPoint()
                self.crop_scaled = QRect(self.start_pos, self.end_pos)
                self.erase = True  # 这一行决定了松开鼠标后方框是否擦除
                self.normalize_rect()  # 将方框参数转成正数
                self.update()

                self.crop_image()  # 裁切图像
            elif self.draw_tool == 'pen' and self.start_pos != QPoint(-1, -1):
                self.scaled_lines.append(self.draw_line)  # 松开鼠标,把当前的线加入线集合
                self.line_drawing = False
                self.redrew = False
                self.update()

    # 重新定义绘画事件
    def paintEvent(self, event):
        super().paintEvent(event)  # 继承绘画事件
        if self.draw_tool in ['zoom', 'crop']:
            painter = QPainter(self)
            if not self.erase:  # 不擦除
                painter.setPen(QPen(QColor(255, 0, 0, 255), 1, Qt.SolidLine))

            else:  # 擦除(用透明色画一遍)
                painter.setPen(QPen(QColor(0, 0, 0, 0), 2, Qt.SolidLine))

            painter.drawRect(self.crop_scaled)
            painter.end()

        elif self.draw_tool == 'pen':
            if self.line_drawing:  # 如果不是松开鼠标的最后结果
                painter = QPainter(self)  # 就不必刷新和写入,只是在当前的label上展示一下新增的线条而已
                painter.setPen(QPen(QColor(0, 0, 0, 255), 2, Qt.SolidLine))
                # for line in self.lines[:-1]:
                #     painter.drawLine(line)
                pen = QPen(QColor(255, 255, 255, 255), 2, Qt.CustomDashLine)
                pen.setDashPattern([1, 4, 5, 4])  # 设置自定义虚线模式
                painter.setPen(pen)
                painter.drawLine(self.draw_line)
                painter.end()
                # self.draw_line = QLine()
            elif not self.redrew:  # 判断是否已经重刷,节约系统资源
                painter = QPainter(self.showing_src)  # 如果是松开鼠标的最后结果,就在正式的画板上画
                # painter = QPainter(self)
                painter.setPen(QPen(QColor(0, 0, 0, 255), 2, Qt.SolidLine))
                # 坐标映射,这里是映射到self.showing_src,就是显示部分的实际像素
                show_points = (((self.draw_line.toTuple()[0] - self.scaled_x) * self.scale),
                               ((self.draw_line.toTuple()[1] - self.scaled_y) * self.scale),
                               ((self.draw_line.toTuple()[2] - self.scaled_x) * self.scale),
                               ((self.draw_line.toTuple()[3] - self.scaled_y) * self.scale)
                               )
                showing_src_line = QLine(*show_points)
                painter.drawLine(showing_src_line)
                painter.end()

                painter = QPainter(self.src_pixmap)  # 在原图上画线
                # painter = QPainter(self)
                painter.setPen(QPen(QColor(0, 0, 0, 255), 2, Qt.SolidLine))
                # 坐标映射,这里是映射到self.src,就是原图的全部像素
                src_points = (show_points[0] + self.crop_src.x(),
                              show_points[1] + self.crop_src.y(),
                              show_points[2] + self.crop_src.x(),
                              show_points[3] + self.crop_src.y()
                              )
                print(src_points)
                self.src_lines.append(src_points)
                src_image_line = QLine(*src_points)
                painter.drawLine(src_image_line)
                painter.end()

                self.scale_and_show(self.showing_src)
                self.redrew = True
                self.draw_line = QLine()
    # 将得到的方框的长宽尺寸转换成正数
    def normalize_rect(self):
        x = self.crop_scaled.x()
        y = self.crop_scaled.y()
        width = self.crop_scaled.width()
        height = self.crop_scaled.height()

        if width < 0:
            x += width
            width = abs(width)
        if height < 0:
            y += height
            height = abs(height)

        # 最小方框
        self.crop_scaled = QRect(x, y, width, height)

    # 放大并显示画面
    def crop_image(self):
        crop_scaled_x = self.crop_scaled.x() - self.scaled_x  # 在屏幕显示界面上,当前的选择框相对于显示图像的显示范围原点的x
        crop_scaled_y = self.crop_scaled.y() - self.scaled_y  # 在屏幕显示界面上,当前的选择框相对于显示图像的显示范围原点的y

        crop_src_x = crop_scaled_x * self.scale + self.crop_src.x()  # 在屏幕显示界面上的选择框起点角折算到原图上的x坐标
        crop_src_y = crop_scaled_y * self.scale + self.crop_src.y()  # 在屏幕显示界面上的选择框起点角折算到原图上的y坐标
        crop_src_width = self.crop_scaled.width() * self.scale  # 在屏幕显示界面上的选择框折算到原图上的宽度
        crop_src_height = self.crop_scaled.height() * self.scale  # 在屏幕显示界面上的选择框折算到原图上的高度

        if crop_src_width > 3 and crop_src_height > 3:   # 最小框
            self.crop_src = QRect(crop_src_x,  # 折算到原图后的选择框
                                  crop_src_y,
                                  crop_src_width,
                                  crop_src_height)
            # print(self.crop_src)
            if self.draw_tool == 'zoom':   # 如果是放大状态
                self.showing_src = self.src_pixmap.copy(self.crop_src)
                self.scale_and_show(self.showing_src)

            elif self.draw_tool == 'crop':   # 如果是裁切状态
                self.src_pixmap = self.src_pixmap.copy(self.crop_src)
                self.set_src(self.src_pixmap)
                self.crop_rect.emit((crop_src_x,  # 将折算到原图后的选择框发射出去
                                     crop_src_y,
                                     crop_src_width,
                                     crop_src_height))

    # 初始化,设置原图像
    def set_src(self, src_pixmap):
        self.src_pixmap = src_pixmap

        self.showing_src = self.src_pixmap  # 需要在屏幕显示的图像,实际像素维度,首次设置,这个就是原图的整图
        self.crop_src = QRect(0, 0, 1, 1)  # 实际像素维度的方框
        self.scale_and_show(self.showing_src)  # 按比例缩放并且显示

        # 发射信号
        self.src_info.emit([self.src_pixmap.width(), self.src_pixmap.height()])  # 原图参数

    # 缩放和显示
    def scale_and_show(self, image):
        # 经过缩放,适应窗口
        self.scaled_image = image.scaled(self.width(), self.height(), Qt.KeepAspectRatio,
                                         Qt.SmoothTransformation)
        self.scaled_x = (self.width() - self.scaled_image.width()) / 2  # 显示内容的屏幕像素层级的x起点
        self.scaled_y = (self.height() - self.scaled_image.height()) / 2  # 显示内容的屏幕像素层级的y起点
        self.scale = self.showing_src.width() / self.scaled_image.width()  # 显示比例,含义为每个显示像素相对于几个实际像素
        self.setPixmap(self.scaled_image)  # 显示图像
        self.setAlignment(Qt.AlignCenter)  # 居中显示

 6、根据尺寸画出不同颜色的轮廓图,位于class Jobs

    @staticmethod
    # 根据轮廓的尺寸决定轮廓图的输出颜色
    def contour_color(k):
        out_color = []  # 输出的颜色们
        begin = ui.begin_color.getRgb()
        mid = ui.mid_color.getRgb()
        end = ui.end_color.getRgb()
        for i in range(3):
            if k <= 0.5:
                single_color = int((mid[i] - begin[i]) * 2 * k + begin[i])
            else:
                single_color = int((end[i] - mid[i]) * 2 * (k - 0.5) + mid[i])
            out_color.append(min(max(0, single_color), 255))

        return out_color

    @staticmethod
    # 按照颜色表输出轮廓图
    def fill_contours(img, contours):
        # 创建空白的图像,用来画轮廓表
        filled_img = np.zeros_like(img)
        scale = ui.settings['pix_scale'][0]  # 光学倍率,屏幕每像素代表实际多少微米


        max_dim = ui.color_scale_max  # 颜色输出最大尺寸
        min_dim = ui.color_scale_min  # 颜色输出最小尺寸
        range_dim = max_dim - min_dim  # 颜色输出尺寸范围

        for i, contour in enumerate(contours):
            rect = cv2.minAreaRect(contour)  # 获取最小外接矩形
            dia = rect[1][0] if rect[1][0] <= rect[1][1] else rect[1][1]  # 计算轮廓的最短尺寸,并获取直径
            if dia >= ui.min_bubble:  # 只画大于最小泡泡的尺寸的泡泡
                k = min((dia * scale - min_dim) / range_dim, 1.0)  # 尺寸比例
                # print(k)
                color = Jobs.contour_color(k)  # 获取颜色
                cv2.drawContours(filled_img, [contour], -1, color, -1)  # 绘画轮廓

        return filled_img

7、完善各个部件的信号和槽,初具雏形 

本阶段的主脚本:

# 这是一个图像处理小应用的示例脚本。

# encoding: utf-8
import json
import os
import sys
import threading
import time

import cv2
import numpy as np
from PySide6.QtCore import QObject, QTimer
from PySide6.QtGui import QColor, QImage, QPixmap
from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox

import main_window_rc  # 导入主画面
from FilterCamera import FilterCamera


# 定义主窗口
class MainWindow(QMainWindow, main_window_rc.Ui_MainWindow):
    def __init__(self):
        super().__init__()


# ################公用的作业函数#############################
class Jobs:
    @staticmethod
    # 查找本地可以用的相机
    def find_camera(max_camera=3):
        # 先禁用相机按钮和新建按钮
        window1.btn_capture.setEnabled(False)
        window1.btn_capture.setToolTip('正在查找和初始化相机')

        window1.btn_new.setEnabled(False)
        window1.btn_new.setToolTip('正在查找和初始化相机')

        # 如果没有查到相机
        def no_camera():
            window1.btn_capture.setToolTip('未查找到可用相机')
            window1.btn_new.setToolTip('未查找到可用相机')
            window1.form_alarm_message.setText('未查找到可用相机')
            window1.form_alarm.show()
            # message_box = QMessageBox.warning(window1, "错误", "未查找到可用相机")
            # message_box.move(200,200)
            # QMessageBox.warning(window1, "错误", "未查找到可用相机")
            # window1.message_frame.setHidden(False)

        # 查找相机的定时器,超过这个时间没有查找到相机就认为没有相机
        ui.timer1_find_camera = threading.Timer(30, no_camera)
        ui.timer1_find_camera.start()
        # 项目内所有的相机
        ui.cameras = []
        cameras = [cv2.VideoCapture(x) for x in range(max_camera) if cv2.VideoCapture(x).isOpened()]  # 可用相机的列表

        for cam in cameras:
            ret, frame = cam.read()
            if ret:
                filter_camera = FilterCamera(cam, frame)  # FilterCamera是自定义的滤波相机,连续拍摄多张图片并平均,用以提高像质
                ui.cameras.append(filter_camera)
        # 如果找到了相机
        if ui.cameras:
            window1.form_info.hide()
            window1.comboBox_cameras.addItems([f'相机{x}' for x in range(len(ui.cameras))])  # 更新相机选择组合框的下拉列表
            window1.btn_capture.setToolTip('实时影像/拍照取样')
            # window1.btn_capture.setEnabled(True)
            window1.btn_new.setToolTip('新建检测项目')
            window1.btn_new.setEnabled(True)
            ui.timer1_find_camera.cancel()  # 结束查找相机的定时器
            Jobs.activate_camera(0)  # 激活相机0

    @staticmethod
    # 按照给定的序号激活相机
    def activate_camera(i):
        ui.activated_camera = ui.cameras[i]  # 激活的相机

    @staticmethod
    # 读取JSON文件,分配参数
    def read_json():
        with open('../JSON/setting.json', 'r', encoding='utf-8') as file_json:
            ui.json_data = json.load(file_json)
            ui.settings = ui.json_data['setting']  # 项目参数的定义

    @staticmethod
    # 将OpenCV格式的图像转换为PySide格式,并在小部件上显示
    def img2Widget(img, widget):
        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 转换 BGR 到 RGB
        # 转换图像到QT的QImage格式
        img_height, img_width, channels = img_rgb.shape  # 获取形状
        bytes_per_line = channels * img_width  # 每行字节数
        q_img = QImage(img_rgb.data, img_width, img_height, bytes_per_line, QImage.Format_RGB888)  # 转换成QImage格式

        pixmap = QPixmap.fromImage(q_img)  # 转换成QPixmap格式
        widget.set_src(pixmap)  # 将图像设置为部件的源图像

    @staticmethod
    # 刷新视频帧
    def update_frame():
        if ui.video_play and ui.activated_camera:  # 是否实时播放视频
            ui.orig_img = ui.activated_camera.frame_out()  # 从激活的相机获取图像数据
            ui.src_img = ui.activated_camera.frame_out()  # 从激活的相机获取图像数据
            Jobs.img2Widget(ui.src_img, window1.label_show)  # 将OpenCV格式的图像转换为PySide格式,并在小部件上显示

    @staticmethod
    # 根据轮廓的尺寸决定轮廓图的输出颜色
    def contour_color(k):
        out_color = []  # 输出的颜色们
        begin = ui.begin_color.getRgb()
        mid = ui.mid_color.getRgb()
        end = ui.end_color.getRgb()
        for i in range(3):
            if k <= 0.5:
                single_color = int((mid[i] - begin[i]) * 2 * k + begin[i])
            else:
                single_color = int((end[i] - mid[i]) * 2 * (k - 0.5) + mid[i])
            out_color.append(min(max(0, single_color), 255))

        return out_color

    @staticmethod
    # 按照颜色表输出轮廓图
    def fill_contours(img, contours):
        # 创建空白的图像,用来画轮廓表
        filled_img = np.zeros_like(img)
        scale = ui.settings['pix_scale'][0]  # 光学倍率,屏幕每像素代表实际多少微米


        max_dim = ui.color_scale_max  # 颜色输出最大尺寸
        min_dim = ui.color_scale_min  # 颜色输出最小尺寸
        range_dim = max_dim - min_dim  # 颜色输出尺寸范围

        for i, contour in enumerate(contours):
            rect = cv2.minAreaRect(contour)  # 获取最小外接矩形
            dia = rect[1][0] if rect[1][0] <= rect[1][1] else rect[1][1]  # 计算轮廓的最短尺寸,并获取直径
            if dia >= ui.min_bubble:  # 只画大于最小泡泡的尺寸的泡泡
                k = min((dia * scale - min_dim) / range_dim, 1.0)  # 尺寸比例
                # print(k)
                color = Jobs.contour_color(k)  # 获取颜色
                cv2.drawContours(filled_img, [contour], -1, color, -1)  # 绘画轮廓

        return filled_img

    @staticmethod
    # 计算各个参数的设定值,输入为1-255,输出在min_para和max_para之间,odd_must:必须是奇数
    def parameter_processing(input_para, min_para, max_para, odd_must=False):
        para = int(min_para + (input_para / 254) * (max_para - min_para))
        if odd_must:
            if para % 2 == 0:
                para += 1
        return para

    @staticmethod
    # 系统的初始化
    def start_todo():
        # 读取json文件并分配变量
        Jobs.read_json()
        # ######################主窗口的部件初始化###################################
        window1.move(0, 0)
        window1.masterBtnStation = window1.btnGroup.children()  # 主窗口的按钮站
        # 初始化,禁用除“打开项目”以外的所有按钮
        for b in window1.masterBtnStation:
            b.setEnabled(False)
        window1.btn_openImg.setEnabled(True)
        # 隐藏消息窗口
        window1.form_alarm.hide()
        window1.form_info_message.setText('正在查找和初始化相机...')
        # 设置消息框的自动换行
        window1.form_alarm_message.setWordWrap(True)
        window1.form_info_message.setWordWrap(True)
        window1.imgAdjustGroup.hide()  # 隐藏图像微调的部件群
        # 图像微调的部件的初始化
        window1.slider_threshold_black.setValue(ui.settings['threshold_black'][0])
        window1.slider_noise_min.setValue(ui.settings['noise_min'][0])
        window1.slider_min_bubble.setValue(ui.settings['min_bubble'][0])

        # 主窗口的部件的功能提示tips的定义
        window1.comboBox_cameras.setToolTip('选择相机')
        window1.btn_new.setToolTip('新建检测项目')
        window1.btn_capture.setToolTip('相机拍照')
        window1.btn_openImg.setToolTip('打开已有项目')
        window1.btn_handle.setToolTip('处理图片')
        window1.btn_saveFile.setToolTip('保存当前项目')
        window1.btn_zoom.setToolTip('局部放大')
        window1.btn_redraw.setToolTip('恢复原始显示')
        window1.btn_OPT_adjust.setToolTip('光学尺寸整定')
        window1.btn_penCut.setToolTip('手动打断粘连')
        window1.btn_imgAdjust.setToolTip('图像调整')
        window1.btn_viewData.setToolTip('查看图像数据')
        window1.btn_redo.setToolTip('回退一步')
        window1.btn_del.setToolTip('删除所有')
        window1.btn_crop.setToolTip('裁切图像,用以处理')
        window1.btn_src.setToolTip('恢复原图')

        # 设置颜色条的参数
        gradient_color = ui.settings['gradient_color']  # 梯度颜色的定义
        ui.begin_color = QColor(*gradient_color[0])     # 起始色
        ui.mid_color = QColor(*gradient_color[1])          # 中间色
        print(*gradient_color[1])
        ui.end_color = QColor(*gradient_color[2])          # 结束色
        window1.color_bar.def_colors(ui.begin_color, ui.mid_color, ui.end_color)
        window1.colorBar_range = ui.settings["colorBar_range"]  # 颜色条针对的尺寸范围(目前的单位是pixel,过后换成微米)
        ui.color_scale_min = window1.colorBar_range[0]  # 颜色条最小值
        ui.color_scale_max = window1.colorBar_range[1]  # 颜色条最大值
        scale = ui.color_scale_min  # 第一个进度条的值是颜色条最小值(window1.colorBar_range[0])
        step = (ui.color_scale_max - ui.color_scale_min) / 9  # 刻度的间距,window1.colorBar_range[1]是最大值
        scales = sorted(window1.colorScaleGroup.children(), key=lambda s: s.objectName())
        for s in scales:
            s.setText(str(int(scale)))
            scale += step

        # 查找相机的线程
        ui.th1 = threading.Thread(target=Jobs.find_camera)
        ui.th1.start()

        # 视频显示窗口的参数
        ui.video_play = False  # 是否实时播放视频
        ui.video_FPS = ui.settings["video_FPS"]  # 视频帧率
        ui.timer_video = QTimer()  # 视频的帧刷新节拍定时器
        ui.timer_video.start(1000 // ui.video_FPS[0])

        Image_processing(['start'])  # demo图像预处理

    # 退出前的操作
    @staticmethod
    def before_quit():
        with open('../JSON/setting.json', 'w') as file:  # 保存json文件
            json.dump(ui.json_data, file, indent=4)


# ################图像处理的过程函数#############################
def Image_processing(steps):
    for step in steps:
        # 系统的初始化
        if step == 'start':
            # ui.orig_img = cv2.imread('../MEDIA/logo.jpg')  # 获取原始素材图像
            # # ui.src_img = ui.orig_img
            # ui.src_img = ui.orig_img.copy()  # 定义图像处理源图像
            # ui.show_img = ui.src_img.copy()  # 定义显示图像
            # Jobs.img2Widget(ui.show_img, window1.label_show)  # 将OpenCV格式的图像转换为PySide格式,并在小部件上显示
            continue

        # step0,步骤0
        if step == 0:
            # 打开原图转成灰度图,并复制一份显示效果为灰度,但色彩为三通道的图,用以作为最终效果的显示
            if ui.src_img.size > 0:
                ui.gray_img = cv2.cvtColor(ui.src_img, cv2.COLOR_BGR2GRAY)  # 将图像处理源图像转成灰度,用以进行后面的运算
                ui.gray_image_3ch = cv2.cvtColor(ui.gray_img, cv2.COLOR_GRAY2BGR)  # 保留一份显示效果为灰度,但色彩为三通道的图,用以作为最终效果的显示
                continue

        # step1,步骤1
        if step == 1:
            # 将灰度图转为二值图
            # 定义黑阈值
            ui.threshold_black = Jobs.parameter_processing(window1.slider_threshold_black.value(), 5, 255)
            # 使用threshold阈值法进行图像阈值分割
            binary_tmp = cv2.threshold(ui.gray_img, ui.threshold_black, 255,
                                       cv2.THRESH_BINARY_INV)[1]  # 转成黑白二值的临时图
            ui.noise_min = Jobs.parameter_processing(window1.slider_noise_min.value(), 1, 29, True)  # 定义杂斑阈值
            # window1.value_noise_min.setText(str(ui.noise_min))
            kernel = np.ones((ui.noise_min, ui.noise_min), np.uint8)  # 去除杂斑的核
            ui.binary_img = cv2.morphologyEx(binary_tmp, cv2.MORPH_OPEN, kernel)  # 去除杂斑
            # 切断粘连轮廓的线们
            if window1.label_show.src_lines:
                for l in window1.label_show.src_lines:
                    cv2.line(ui.binary_img, (int(l[0]), int(l[1])), (int(l[2]), int(l[3])), (0, 0, 0), 2)   # 按坐标画切断线
            ui.show_img = cv2.addWeighted(ui.binary_img, 1, ui.gray_img, 0.5, 0)
            Jobs.img2Widget(ui.show_img, window1.label_show)
            continue

        # step2,步骤2
        if step == 2:
            tmp_img = np.zeros_like(ui.src_img)  # 临时图像容器
            ui.min_bubble = Jobs.parameter_processing(window1.slider_min_bubble.value(), 1, 500)
            ui.contours = cv2.findContours(ui.binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]  # 从临时轮廓图中查找轮廓们
            ui.contour_img = Jobs.fill_contours(tmp_img, ui.contours)
            ui.show_img = cv2.addWeighted(ui.contour_img, 1, ui.gray_image_3ch, 0.5, 0)
            Jobs.img2Widget(ui.show_img, window1.label_show)
            continue


# ###########################信号的连接和槽函数####################################
def signal_slot():
    # #####################主窗口的信号和槽####################################
    # 视频播放的帧定时器的连接
    ui.timer_video.timeout.connect(lambda: Jobs.update_frame())

    # “新建项目”按钮点击的槽函数
    def btn_new_clicked():
        now = time.localtime()
        time_str = time.strftime("%Y%m%d_%H%M%S", now)
        project_path = f'../PROJECT/{time_str}'
        # 检查路径是否存在,如果不存在则创建
        try:
            os.makedirs(project_path, exist_ok=True)
            # QMessageBox.information(window1, "创建成功", f"已成功创建检测项目文件夹:\n{project_path}")
            window1.form_info_message.setText(f"已成功创建检测项目文件夹:\n{project_path}")
            window1.form_info.show()
            ui.project_path = project_path
            window1.btn_capture.setEnabled(True)   # 新建项目后允许拍照
            window1.btn_saveFile.setEnabled(True)  # 新建项目后允许保存
        except Exception as e:
            QMessageBox.critical(window1, "错误", f"创建文件夹时发生错误:\n{str(e)}")
        # project_path = QFileDialog.

    window1.btn_new.clicked.connect(btn_new_clicked)

    # “打开文件”按钮点击的槽函数
    def btn_openImg_clicked():
        pass

    window1.btn_openImg.clicked.connect(btn_openImg_clicked)

    # “实时/采图”按钮点击的槽函数
    def window1_btn_capture_clicked():
        self = window1.btn_capture  # 确认对象
        self.setProperty('activated', not self.property('activated'))  # 反转激活状态,activated是在QT designer中为该按钮定义的一个属性
        # 如果本按钮被激活,意味着是视频实时采图模式
        if self.property('activated'):
            ui.video_play = True  # 如果按钮激活,将全局变量ui.video_play置为True
            # 视频实时采图模式下禁用其他按钮
            for b in window1.masterBtnStation:
                b.setEnabled(False)  # 禁用按钮
                b.setProperty('activated', False)
                b.setStyleSheet(b.styleSheet())  # 刷新按钮的显示
            self.setEnabled(True)  # 启用本按钮
            self.setProperty('activated', True)
            window1.imgAdjustGroup.hide()  # 图像调节窗口隐藏
        # 非视频状态启用其他按钮
        else:
            for b in window1.masterBtnStation:
                b.setEnabled(True)  # # 启用按钮
                b.setStyleSheet(b.styleSheet())  # 刷新按钮的显示
            ui.video_play = False

        self.setStyleSheet(self.styleSheet())  # 刷新本按钮的显示
        # 连接信号到槽函数
    window1.btn_capture.clicked.connect(window1_btn_capture_clicked)

    # “裁切放大”按钮点击的槽函数
    def window1_btn_zoom_clicked():
        self = window1.btn_zoom  # 确认对象
        self.setProperty('activated', not self.property('activated'))  # 反转激活状态

        if self.property('activated'):
            window1.label_show.draw_tool = 'zoom'  # 将显示画面的工具设为裁切
            window1.btn_penCut.setProperty('activated', False)  # 互锁,只能有一个激活
            window1.btn_penCut.setStyleSheet(window1.btn_penCut.styleSheet())
            window1.btn_crop.setProperty('activated', False)  # 互锁,只能有一个激活
            window1.btn_crop.setStyleSheet(window1.btn_crop.styleSheet())

        else:
            window1.label_show.draw_tool = ''

        self.setStyleSheet(self.styleSheet())  # 刷新按钮的显示

    window1.btn_zoom.clicked.connect(window1_btn_zoom_clicked)

    # “打断连接”按钮点击的槽函数
    def window1_penCut_clicked():
        self = window1.btn_penCut  # 确认对象
        self.setProperty('activated', not self.property('activated'))  # 反转激活状态

        if self.property('activated'):
            window1.label_show.draw_tool = 'pen'  # 将显示画面的工具设为裁切
            window1.btn_zoom.setProperty('activated', False)  # 互锁,只能有一个激活
            window1.btn_zoom.setStyleSheet(window1.btn_zoom.styleSheet())
            window1.btn_crop.setProperty('activated', False)  # 互锁,只能有一个激活
            window1.btn_crop.setStyleSheet(window1.btn_crop.styleSheet())

        else:
            window1.label_show.draw_tool = ''

        self.setStyleSheet(self.styleSheet())  # 刷新按钮的显示

    window1.btn_penCut.clicked.connect(window1_penCut_clicked)

    # '处理图片'按钮点击的槽函数
    def window1_btn_handle_clicked():
        Image_processing([0, 1, 2])
        # Image_processing([2])

    window1.btn_handle.clicked.connect(window1_btn_handle_clicked)

    # '恢复原始显示'按钮点击的槽函数
    def window1_btn_redraw_clicked():
        # show_img = ui.src_img
        # window1.label_show.set_src(ui.src_img)
        # Jobs.img2Widget(ui.src_img, window1.label_show)
        Jobs.img2Widget(ui.show_img, window1.label_show)

    window1.btn_redraw.clicked.connect(window1_btn_redraw_clicked)

    # '保存文件'按钮点击的槽函数
    def window1_btn_saveFile_clicked():
        pass

    window1.btn_saveFile.clicked.connect(window1_btn_saveFile_clicked)

    # '裁切图像'按钮点击的槽函数
    def window1_btn_crop_clicked():
        self = window1.btn_crop  # 确认对象
        self.setProperty('activated', not self.property('activated'))  # 反转激活状态

        if self.property('activated'):
            window1.label_show.draw_tool = 'crop'  # 将显示画面的工具设为裁切
            window1.btn_penCut.setProperty('activated', False)  # 互锁,只能有一个激活
            window1.btn_penCut.setStyleSheet(window1.btn_penCut.styleSheet())
            window1.btn_zoom.setProperty('activated', False)  # 互锁,只能有一个激活
            window1.btn_zoom.setStyleSheet(window1.btn_zoom.styleSheet())

        else:
            window1.label_show.draw_tool = ''

        self.setStyleSheet(self.styleSheet())  # 刷新按钮的显示

    window1.btn_crop.clicked.connect(window1_btn_crop_clicked)

    # '满幅原图'按钮点击的槽函数
    def window1_btn_src_clicked():
        ui.src_img = ui.orig_img
        # window1.label_show.set_src(ui.orig_img)
        window1.btn_crop.setEnabled(True)
        Jobs.img2Widget(ui.orig_img, window1.label_show)
        window1.label_show.src_lines.clear()

    window1.btn_src.clicked.connect(window1_btn_src_clicked)

    # '光学尺寸整定'按钮点击的槽函数
    def window1_btn_OPT_adjust_clicked():
        pass

    window1.btn_OPT_adjust.clicked.connect(window1_btn_OPT_adjust_clicked)

    # '图像调整'按钮点击的槽函数
    def window1_btn_imgAdjust_clicked():
        btn = window1.btn_imgAdjust
        group = window1.imgAdjustGroup
        btn.setProperty('activated', not btn.property('activated'))
        btn.setStyleSheet(btn.styleSheet())
        if btn.property('activated'):
            group.show()
        else:
            group.hide()

    window1.btn_imgAdjust.clicked.connect(window1_btn_imgAdjust_clicked)

    # '光学尺寸整定'按钮点击的槽函数
    def window1_btn_viewData_clicked():
        pass

    window1.btn_viewData.clicked.connect(window1_btn_viewData_clicked)

    # '回退一步'按钮点击的槽函数
    def window1_btn_redo_clicked():
        if window1.label_show.src_lines:
            window1.label_show.src_lines.pop()
            window1_btn_handle_clicked()

    window1.btn_redo.clicked.connect(window1_btn_redo_clicked)

    # '删除'按钮点击的槽函数
    def window1_btn_del_clicked():
        if window1.label_show.src_lines:
            window1.label_show.src_lines.clear()
            window1_btn_handle_clicked()

    window1.btn_del.clicked.connect(window1_btn_del_clicked)

    # "黑白阈值"调节的槽函数
    def window1_threshold_black_valueChanged(var):
        ui.settings['threshold_black'][0] = var

        Image_processing([0, 1])  # 处理图像

    window1.slider_threshold_black.valueChanged.connect(window1_threshold_black_valueChanged)

    # "去除杂斑"调节的槽函数
    def window1_noise_min_valueChanged(var):
        ui.settings['noise_min'][0] = var

        Image_processing([0, 1])  # 处理图像

    window1.slider_noise_min.valueChanged.connect(window1_noise_min_valueChanged)

    # "最小气泡"调节的槽函数
    def window1_slider_min_bubble_valueChanged(var):
        ui.settings['min_bubble'][0] = var
        Image_processing([2])

    window1.slider_min_bubble.valueChanged.connect(window1_slider_min_bubble_valueChanged)

    # 当选择了方框作为处理图像的源
    def window1_label_show_crop_rect(rect):
        x = int(rect[0])
        y = int(rect[1])
        width = int(rect[2])
        height = int(rect[3])

        ui.src_img = ui.orig_img[y:y + height, x:x + width]
        window1.label_show.src_lines.clear()
        window1.label_show.draw_tool = ''
        window1.btn_crop.setProperty('activated', False)
        window1.btn_crop.setStyleSheet(window1.btn_crop.styleSheet())
        window1.btn_crop.setEnabled(False)

    window1.label_show.crop_rect.connect(window1_label_show_crop_rect)

    # #####################消息窗口的信号和槽####################################
    # “确定”按钮点击的槽函数
    def window1_form_alarm_btnOK_clicked():
        window1.form_alarm.hide()
    window1.form_alarm_btnOK.clicked.connect(window1_form_alarm_btnOK_clicked)

    # “确定”按钮点击的槽函数
    def window1_form_info_btnOK_clicked():
        window1.form_info.hide()

    window1.form_info_btnOK.clicked.connect(window1_form_info_btnOK_clicked)


# #############################主程序###################################
if __name__ == '__main__':
    app = QApplication(sys.argv)

    # #######################项目级别的定义###################################
    class UI(QObject):  # 将项目定义为QObject,用来管理项目级别的信号和变量
        # ###########__init__###############
        def __init__(self):
            super().__init__()


    # ########################本项目的实例化###################################
    ui = UI()  # 项目实例化

    # ########################实例化画面#################################
    window1 = MainWindow()  # 主画面实例化

    window1.show()  # 显示画面
    window1.setupUi(window1)  # 画面初始化

    Jobs.start_todo()  # 系统初始化
    signal_slot()  # 信号与槽的定义

    app.aboutToQuit.connect(Jobs.before_quit)  # 退出系统之前的操作

    sys.exit(app.exec())

当前的基本功能截图:

 持续更新,直至完成。

感兴趣的朋友可以向我索要源文件并参与项目的共同学习和完善,同时也特别期待大神高手的指点和批评!

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-13 18:12:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-13 18:12:02       71 阅读
  3. 在Django里面运行非项目文件

    2024-07-13 18:12:02       58 阅读
  4. Python语言-面向对象

    2024-07-13 18:12:02       69 阅读

热门阅读

  1. MyBatisPlus实现增删改查

    2024-07-13 18:12:02       18 阅读
  2. LeetCode 74, 228, 39

    2024-07-13 18:12:02       15 阅读
  3. Oracle字符集修改

    2024-07-13 18:12:02       22 阅读
  4. 力扣 哈希表刷题回顾

    2024-07-13 18:12:02       19 阅读
  5. C++之复合资料型态 第一部(参考 列举 指标)

    2024-07-13 18:12:02       20 阅读
  6. spring-cloud和spring-cloud-alibaba的关系

    2024-07-13 18:12:02       20 阅读
  7. 4层负载均衡和7层负载均衡

    2024-07-13 18:12:02       21 阅读
  8. 大话C语言:第31篇 指针和数组的关系

    2024-07-13 18:12:02       22 阅读
  9. 算法提高第二章 线段树基础

    2024-07-13 18:12:02       18 阅读
  10. django orm中value和value_list以及转成list

    2024-07-13 18:12:02       22 阅读
  11. C# .Net Core Zip压缩包中文名乱码的解决方法

    2024-07-13 18:12:02       22 阅读