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() # 图像微调的部件群
# 图像微调的部件的初始化