使用PyQt5的图形用户界面(GUI)开发教程
文章目录
- 写在前面
- 一、PyQt5的安装
- 1.1 使用Conda管理环境
- 1.1.1 新建环境
- 1.1.2 `conda list`和`pip list`的区别
- 1.1.3 `conda install`和`pip install`的区别
- 1.2 安装PyQt5和Qt Designer
- 1.3 VsCode中配置Qt Designer
- 二、PyQt5的UI设计
- 2.1 `.ui`文件设计
- 2.2 `.qrc`文件建立
- 2.3 `qss`设计
- 三、PyQt5的逻辑设计
- 3.1 新建.py逻辑文件
- 3.1.1 主窗口初始化
- 3.1.2 初始化界面
- 3.1.3 绑定ui事件处理机制
- 3.1.4 初始化标志位
- 3.1.5 检查标志位状态
- 3.1.6 读取数据输入
- 3.1.7 根据不同的滤波方法显示不同的原理公式
- 3.1.8 为程序设置icon
- 3.1.9 执行仿真
- 3.1.10 保存数据
- 3.1.11 分辨率自适应
- 3.2 最终成品
- 四、总结
- 参考文献
写在前面
之前的博客文章“图形用户界面(GUI)开发教程”(https://leilie.top/2024-01-20/Study-GUI)里介绍了使用MATLAB设计GUI,但使用MATLAB设计GUI存在三个问题:
- 移植性差。比如开发这个GUI的版本是MATLAB 2018,那么在其他电脑上打开该程序将无法正常运行。一般都会在其他电脑上安装与开发该GUI的版本相同的MATLAB才能较好地避免该问题。
- 风格化差。使用MATLAB设计GUI时,可以自由发挥创意地设计界面的空间小,只能在MATLAB既有框架下进行设计,所以开发出来的GUI不符合现代审美。(补丁:我用的是MATLAB 2018版本,或许最新的MATLAB 2025已经很好地解决了这个问题?)
- 可读性差。GUI中各部件的逻辑均依赖回调函数实现,在复杂的GUI里,往往会迷失在浩如烟海的代码中;而且因为逻辑是依靠回调函数实现的,所以参数传递十分麻烦,增加了开发难度。
所以选择使用Python开发GUI。
为什么选择Python呢?因为我对于C/C++的使用不太熟练,用Python用的更加顺手。目前在想,现今阶段是不是精通MATLAB和Python就可以了(?)。科学计算的任务交给MATLAB,其他剩下的所有事情全部使用Python搞定。因为是个人开发者,对编译速度的要求不那么紧迫,加上Python的简便性,所以目前来说,使用Python已经足够满足我的需求。
再者写这篇博客的目的是防止自己忘记。或者说,很久之后因为某些需求得让我重新捡起来这项技能,因为有这些文章在,能够让我快速地回忆起以前掌握的知识。
所以,这篇博客的预备知识就是需要你已经掌握了如下技能:
- MATLAB GUI的设计,熟悉MATLAB GUI各个部件的功能;
- Python的安装和使用;
- Conda的安装和使用;
- VsCode的安装和使用。
那么开始这篇博客。
一、PyQt5的安装
1.1 使用Conda管理环境
1.1.1 新建环境
管理环境的方式有很多种,比如venv
、pyenv
、conda
,我用conda来管理环境。因为conda可以创建独立的环境,并且给每个独立的环境安装不同的包。调出conda终端界面,新建环境,输入代码:
conda create -n [your_env_name] python==3.12.1
这里我新建的环境名为helloworld
,并且指定了python的版本是3.12.1,因为之前的项目里使用的是3.12.1,为了保证以前的代码不出错,所以延续了这个python版本。
创建好新环境后,使用下面代码查看环境是否创建成功,为
conda env list
出现如下图所示的界面,即说明安装成功。
激活新创建的环境,代码为
conda activate helloworld
1.1.2 conda list
和pip list
的区别
这里我遇见了一个问题。使用pip list
查看该环境安装的包,发现了很多没有安装在这个环境里的包,在网络上搜索之后发现是电脑本地python安装的包污染了conda创建的新环境的pip list
索引,实际上新环境里没有这些包的存在。
不过这个问题很影响查看新环境到底安装了哪些包。比如说,使用pip list
查看安装的包,显示有pandas
;再使用conda list
查看安装的包,显示没有pandas
。那么在写代码的时候,就很容易迷惑:到底pandas
安装上没有?
在网络上查询了很多博客,解决无果,最后选择了掩耳盗铃,使用conda list
来查看新环境的包。这种方式得到的环境所安装的包信息是最可信的。
1.1.3 conda install
和pip install
的区别
引申一下,这里就涉及到使用conda install
安装python包和pip install
安装python包的区别。
使用conda install
安装包时,下载好的包会存放在conda缓存中,如果下次创建新的环境安装同样的包,那么conda会从缓存中抽取之前的包进行安装,减少了安装时间。包与包之间的依赖检查会更加严格。而且使用pip install
没有这种特性。
参考文章:conda install和pip install的区别(https://www.zhihu.com/question/395145313/answer/1230725052)
所以就目前经验而言,选择pip install
更加符合我的需求。因为conda install
有时候网不好,下载不下来所需的包,这一点令人头疼,或者conda网站里面没有所需的包,所以还是选择pip install
。
1.2 安装PyQt5和Qt Designer
好了!开始安装PyQt5!
打开conda终端,激活刚才创建的环境(这一步往往容易误漏,代码为conda activate [your_env_name]
),输入如下代码。
pip install PyQt5
接下来安装的是pyqt5_tool
。这一步有点复杂。如果python版本低于3.9.x
,则可以使用如下代码进行安装。
pip install pyqt5_tool
上述操作会安装好一切所需的包。但是现在使用的python版本为3.12.1,不能这么安装,只能使用如下代码进行安装。
pip install pyqt5designer
1.3 VsCode中配置Qt Designer
下载好pyqt5designer
后,需要找到Qt Designer
软件。我们要找到三个程序:
designer.exe
:Qt Designer的可执行文件;pyuic5.exe
:把Qt Designer设计的.ui
转换为.py
;pyrcc5.exe
:处理.qrc
文件(Qt资源文件),把.qrc
转换为.py
。
随后,打开VsCode,在扩展里搜索PYQT Integration
,点击安装。
再在VsCode里给PYQT Integration配置插件位置,首先安装“文件——首选项——设置”,在搜索栏写pyqt
。按照下图所示的路径设置designer.exe
、pyuic5.exe
和prrcc5.exe
的路径。
被码掉的部分是使用conda创建环境的位置,只需要记住这三个程序都在/Scripts
文件夹里可以找到就行。
再解释一句,Qt一般有三种编写方式:
- Qt Creater:开发Qt的瑞士军刀,啥都能做,能编写C++、QML、qrc等内容;
- Qt Design Studio:支持拖放 QML 元素,还能写 QML/JS/C++ 代码;
- Qt Designer:设计Qt界面的乐高积木,控件可以像积木一样拖来拖去。
二、PyQt5的UI设计
那么,怎么在VsCode里使用pyqt5呢?下面分成三个部分说明。
2.1 .ui
文件设计
在VsCode里单击右键,选中PYQT: New Form
。
弹出下图所示的窗口。
一般是选择Main Window
,再点击创建。弹出下图窗口。
这个时候就可以从左边的控件库拖放一些控件到中间的界面中了。
**注意:**每个控件的用法,请参考文献[1,2]。此处不再赘述。
设计好.ui
文件后,还需要将它转换成.py
文件。保存好.ui
文件,注意它的保存位置。回到VsCode,选中刚保存的.ui
文件,先右键单击,再在菜单里选择PYQT: Complie Form
。
那么就会得到一个新的文件,UI_{your_ui_name}.py
,这个文件之后会在写界面逻辑的时候用到。
2.2 .qrc
文件建立
.qrc
是Qt里会用到的资源文件的集合。首先讲怎么新建qrc资源。右下角有个资源浏览器,先点“资源浏览器”,再点这个窗口左上角的铅笔。如下图所示。
出来资源浏览器后,点击左边的“新建”。
建立好新的.qrc
文件后,再点击下图红框所示的按钮,是为该新建.qrc
文件建立分类。
我自己可能会用到的资源都是图片,所以建立的分类前缀为“images”。
注意:导入
.qrc
文件的过程,请见参考文献[1]。
同样地,也需要把该.qrc
文件转换为.py
文件。
回到VsCode,选中刚才建立的.qrc
文件。先右键单击,再在菜单里选择PYQT: Complie Resource
。
最后会在原文件夹生成一个.py
文件。
这里要记住,如果.ui
文件中导入了(即使不使用).qrc
文件,就必须要将该.qrc
文件转换成.py
,不然没法运行。如果没有没有在.ui
文件里导入过.qrc
文件,那么就不用管这个部分。
这里注意,因为
.qrc
文件放在子文件夹./asserts
里,生成的Ui_xx.py
也在子文件夹>./asserts
中,而主程序在上一级的主文件中。所以,导入.qrc
文件的代码为import KF_rc
这里要改成从子文件夹导入,为(修改处:
.ui
转换后的.py
文件)import asserts.KF_rc
2.3 qss
设计
qss是Qt的风格样式表,作用和css差不多,可以在菜鸟教程里看看css的教程 。
举个例子,先从左边控件库里选择“Push Button”,将按钮拖入窗口中,右键单击按钮,选择“修改样式表”,弹出来一个编辑样式表的框,如下图所示。
在框内输入如下代码。
QPushButton {background-color:qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #e66465, stop:1 #9198e5);color:rgb(227, 227, 227);padding: 4px;min-width: 65px;min-height: 12px;border-bottom-left-radius:20px;border-top-right-radius:20px;font-size: 16px;
}QPushButton:hover {background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #FF6465, stop:1 #9198FF);color:white;
}
QPushButton:pressed{background-color: rgb(65, 65, 65);color:white;
}
效果下图所示,首先是按钮没有被按下时的效果。
然后是按钮被按下时的效果,按下按钮后,改变按钮的背景色和文字的颜色。
到这里,一个简单的ui界面已经设计完毕。
三、PyQt5的逻辑设计
3.1 新建.py逻辑文件
这里是拿使用卡尔曼滤波追踪匀速直线运动的导弹作为例子。
按照下图所示分为三个区域。
- 区域1:选择滤波模式、数据及图片保存模式,实现开始仿真和退出软件功能;
- 区域2:设置滤波参数;
- 区域3:显示对应滤波方法的原理公式。
这里首先把3个区域的初始框架搭建好,加上亿点细节,得到下图所示界面。
记得把所有控件的键名命名好,方便后面写逻辑的时候调用。
我给自己梳理的规则如下:(键名——控件类)
- xxx_lab——QLabel
- xxx_edit——QLineEdit
- xxx_grid——QGridLayout
- xxx_hbox——QHBoxLayout
- xxx_btn——QPushButton
- xxx_menu——QComboBox
- xxx_box——QCheckBox
- xxx_disp——QGraphicsView
- xxx_group——QGroupBox
这样在调用的时候,控件是什么类型一目了然,方便调用。
在写界面逻辑的时候,代码框架分成三个部分:
- Part 1:导入第三方包
- Part 2:建立主窗口类对象
- Part 3:主函数建立类对象实例并显示界面
"""Part 1"""
import [pkgs]"""Part 2"""
class MainApp(QMainWindow, UI_[your_ui_name]):def xxx:pass"""Part 3"""
if __name__ == '__main__':pass
最重要的是Part 2
部分,承担了界面逻辑的关键部分,下面针对本软件的界面逻辑进行详细讲解。
3.1.1 主窗口初始化
在构造类对象实例的时候,对主窗口进行初始化设置。
def __init__(self):"""主窗口初始化"""super().__init__() # 调用父类的初始化方法self.setupUi(self) # 加载UI设计self.init_ui() # 初始化界面设置self.bind_events() # 绑定UI事件处理函数self.init_flags() # 初始化标志位变量
super().__init__()
调用 父类(QMainWindow
)的初始化方法,确保主窗口的基本功能(如窗口显示、事件处理)正常启动。setupUi(self)
方法会初始化所有界面元素(如按钮、文本框、GroupBox 等)。而setupUi
必须在父类(QMainWindow
)的初始化完成后才能调用,否则会导致组件无法正确绑定到主窗口。
3.1.2 初始化界面
def init_ui(self):"""初始化界面显示设置"""self.setWindowTitle("卡尔曼滤波仿真软件Demo") # 设置窗口标题self.update_graph_displays(0) # 显示默认滤波方法对应的公式图
3.1.3 绑定ui事件处理机制
def bind_events(self):"""绑定UI元素事件到处理函数"""self.exit_btn.clicked.connect(self.close) # 退出按钮self.simu_btn.clicked.connect(self.run_simulation) # 仿真按钮self.method_menu.currentIndexChanged.connect(self.update_method_flag) # 滤波方法选择self.save_fig_box.toggled.connect(self.update_save_fig_flag) # 保存图表复选框self.save_data_box.toggled.connect(self.update_save_data_flag) # 保存数据复选框self.yes_btn.toggled.connect(self.update_with_time_flag) # 带时间戳单选按钮self.no_btn.toggled.connect(self.update_with_time_flag) # 不带时间戳单选按钮
上面()
中的函数会在后面给出,是逻辑机制的关键部分。
3.1.4 初始化标志位
这个部分也可以放在__init__()
里,写成函数更加符合模块化的要求。便于以后修改。
def init_flags(self):"""初始化控制标志位,存储UI选择状态"""self.method_flag = "" # 当前选择的滤波方法self.save_fig_flag = False # 是否保存图表self.save_data_flag = False # 是否保存数据self.with_time_flag = False # 是否在保存数据时添加时间戳
3.1.5 检查标志位状态
def update_save_fig_flag(self, state):"""更新保存图表标志位"""self.save_fig_flag = statedef update_save_data_flag(self, state):"""更新保存数据标志位"""self.save_data_flag = statedef update_with_time_flag(self, state):"""更新保存数据时是否添加时间戳标志位"""self.with_time_flag = self.yes_btn.isChecked() # 根据Yes按钮状态更新标志def update_method_flag(self, index):"""更新滤波方法标志位并刷新公式图显示"""# 滤波方法索引到名称的映射method_map = {0: "请选择滤波方式",1: "信息滤波",2: "UD滤波",3: "遗忘滤波",4: "自适应遗忘滤波(1)",5: "自适应遗忘滤波(2)"}self.method_flag = method_map.get(index, "请选择滤波方式") # 更新方法标志位self.update_graph_displays(index) # 刷新公式图显示def check_save_fig_warning(self, state):"""检测保存图片复选框状态,显示提示"""if state == QtCore.Qt.Checked: # 当勾选时msg = QMessageBox()msg.setIcon(QMessageBox.Warning)msg.setText("选择“保存图片”则仿真完毕之后不会展示结果。")msg.setWindowTitle("操作提示")msg.exec_()
3.1.6 读取数据输入
def get_lineedit_values(self):"""获取所有QLineEdit控件中的参数值"""return {"simu_time": self.simu_edit.text(), # 仿真时长"sample_time": self.sample_edit.text(), # 采样时长"init_state": self.init_edit.text(), # 初始状态"process_noise": self.process_edit.text(), # 过程噪声"P_matrix": self.P_edit.text(), # 协方差矩阵"measure_noise": self.measure_edit.text(), # 量测噪声"info_matrix": self.info_edit.text() # 信息矩阵}def parse_numeric_params(self, params):"""解析并验证数值参数,转换为指定数据类型"""errors = []parsed = {}# 解析整数for key in ["simu_time"]:try:parsed[key] = int(params[key])except ValueError:errors.append(f"{key}: 请输入有效整数")# 解析浮点数参数for key in ["sample_time", "process_noise", "measure_noise"]:try:parsed[key] = float(params[key])except ValueError:errors.append(f"{key}: 请输入有效的浮点数")# 解析初始状态 (2,) 数组try:# 处理类似 "[10000;-300]" 或 "[10000, -300]" 的输入格式values = params["init_state"].strip("[]").replace(";", ",").split(",")parsed["init_state"] = np.array([float(v.strip()) for v in values], dtype=float) # v.strip()用于移除字符串开头和结尾的特定字符if parsed["init_state"].shape != (2,):raise ValueError("需要包含两个元素的数组")except Exception as e:errors.append(f"init_state: {str(e)} (格式应为 [值1, 值2])")# 解析协方差矩阵 (2,) 数组try:# 处理类似 "[100,1]" 或 "[100;1]" 的输入格式values = params["P_matrix"].strip("[]").replace(";", ",").split(",")parsed["P_matrix"] = np.array([float(v.strip()) for v in values], dtype=float) # v.strip()用于移除字符串开头和结尾的特定字符if parsed["P_matrix"].shape != (2,):raise ValueError("需要包含两个元素的数组")except Exception as e:errors.append(f"P_matrix: {str(e)} (格式应为 [值1, 值2])")# 解析信息矩阵 (2,2) 数组try:# 处理类似 "[0,0;0,0]" 的输入格式rows = params["info_matrix"].strip("[]").split(";")parsed["info_matrix"] = np.array([[float(v) for v in row.split(",")] for row in rows], dtype=float)if parsed["info_matrix"].shape != (2, 2):raise ValueError("需要2x2的数组格式")except Exception as e:errors.append(f"info_matrix: {str(e)} (格式应为 [值1,值2;值3,值4])")return parsed, errors
3.1.7 根据不同的滤波方法显示不同的原理公式
def update_graph_displays(self, index):"""根据选择的滤波方法更新公式图显示"""# 滤波方法索引到公式图路径的映射image_paths = [("./asserts/KF1.png", "./asserts/KF2.png"), # 0: 卡尔曼滤波("./asserts/IF1.png", "./asserts/IF2.png"), # 1: 信息滤波("./asserts/UD1.png", "./asserts/UD2.png"), # 2: UD滤波("./asserts/FD1.png", "./asserts/FD2.png"), # 3: 遗忘滤波("./asserts/FT1.png", "./asserts/FT2.png"), # 4: 自适应遗忘滤波(1)("./asserts/FT1.png", "./asserts/FT2.png"), # 5: 自适应遗忘滤波(2)]if 0 <= index < len(image_paths): # 确保索引有效img1_path, img2_path = image_paths[index]self.display_image(self.eq1_disp, img1_path) # 显示第一个公式图self.display_image(self.eq2_disp, img2_path) # 显示第二个公式图def display_image(self, graphics_view, image_path):"""在指定的QGraphicsView中显示图片"""scene = QtWidgets.QGraphicsScene(graphics_view) # 创建图形场景pixmap = QtGui.QPixmap(image_path) # 加载图片# 缩放图片以适应视图大小,保持纵横比并平滑缩放scaled_pixmap = pixmap.scaled(graphics_view.size(),QtCore.Qt.KeepAspectRatio,QtCore.Qt.SmoothTransformation)scene.addPixmap(scaled_pixmap) # 将缩放后的图片添加到场景graphics_view.setScene(scene) # 设置视图的场景graphics_view.setAlignment(QtCore.Qt.AlignCenter) # 图片居中显示
3.1.8 为程序设置icon
def set_application_icon(self):"""设置应用程序图标"""icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'asserts', 'logo.ico')if os.path.exists(icon_path):self.setWindowIcon(QtGui.QIcon(icon_path))print(f"应用程序图标已设置: {icon_path}")else:print(f"警告: 图标文件不存在 - {icon_path}")
3.1.9 执行仿真
def run_simulation(self):"""执行仿真按钮点击事件处理"""# 收集标志位参数flags = {"method_flag": self.method_flag,"save_fig_flag": self.save_fig_flag,"save_data_flag": self.save_data_flag,"save_data_time_flag": self.with_time_flag}# 收集文本框参数params_txt = self.get_lineedit_values()# 解析数值参数params, errors = self.parse_numeric_params(params_txt)# 检查是否有解析错误if errors:error_msg = "参数解析错误:\n" + "\n".join(errors)QMessageBox.critical(self, "参数错误", error_msg)return# 打印所有参数(实际应用中可替换为仿真计算逻辑)# print("\n===== 仿真参数设置 =====")# print("滤波方法:", flags["method_flag"])# print("保存图表:", flags["save_fig_flag"])# print("保存数据:", flags["save_data_flag"])# print("数据带时间戳:", flags["save_data_time_flag"])# print("\n===== 仿真具体参数 =====")# print("仿真时长:", params["simu_time"])# print("采样时长:", params["sample_time"])# print("初始状态:\n", params["init_state"])# print("过程噪声:", params["process_noise"])# print("协方差矩阵:\n", params["P_matrix"])# print("量测噪声:", params["measure_noise"])# print("信息矩阵:\n", params["info_matrix"])print("===== 开始仿真计算 =====")# 仿真参数T = params["simu_time"] # 仿真时长Ts = params["sample_time"] # 采样时长Q = params["process_noise"] * Ts # 过程噪声R = params["measure_noise"] # 量测噪声# 生成噪声序列W = np.sqrt(Q) * np.random.randn(1, T)V = np.sqrt(R) * np.random.randn(1, T)p0 = params["P_matrix"] # 初始协方差阵参数P0 = np.diag([p0[0], p0[1]]) # 初始协方差阵I0 = params["info_matrix"] # 初始信息阵设置为0# 系统矩阵A = np.array([[0, 1], [0, 0]]) # 状态矩阵I = np.eye(2) # 单位阵Phi = I + A * Ts # 离散化H = np.array([[1, 0]]) # 量测矩阵Gamma = np.array([[0], [1]])# 设置滤波维度nS = 2 # 状态维度nZ = 1 # 观测维度# 分配空间x_state = np.zeros((nS, T)) # 系统真实值z_mea = np.zeros((nZ, T)) # 系统观测值x_KF = np.zeros((nS, T)) # 卡尔曼滤波状态值# 赋初值x_state[:, 0] = params["init_state"] # 系统状态初值z_mea[:, 0] = np.dot(H, x_state[:, 0]) # 系统观测初值x_KF[:, 0] = x_state[:, 0] # 卡尔曼滤波器估计初值# 02 用模型模拟真实状态for t in range(1, T):x_state[:, t] = np.dot(Phi,x_state[:, t-1]) + np.dot(Gamma, W[0, t]).squeeze()z_mea[:, t] = np.dot(H, x_state[:, t]) + V[0, t]# 03-1 Kalman滤波if self.method_flag == '请选择滤波方式':mode = 'null'print("请先选择滤波方法!")QMessageBox.warning(self, "操作提示", "请先选择滤波方法!", QMessageBox.Ok)return # 终止仿真流程# #下述代码为KF滤波方法# mode = 'null'# mode_str = 'KF'# P0_kf = P0.copy() # 复制初始P0# for t in range(1, T):# x_KF[:, t], P0_kf = KF(x_KF, P0_kf, z_mea, Phi, Gamma, H, Q, R, I, t)# # 画图# filter_plot(x_state, z_mea, x_KF, None, None, None, None, T, mode, self.save_fig_flag, mode_str)# 04-1 习题1:信息滤波elif self.method_flag == '信息滤波':mode = 'Problem 1'mode_str = 'IF'# 分配空间并赋予滤波器变量初值s_IF = np.zeros((nS, T)) # 信息滤波状态值x_IF = np.zeros((nS, T))s_IF[:, 0] = np.dot(I0, x_state[:, 0]) # 信息滤波器赋估计初值Phi_inv = np.linalg.pinv(Phi) # 先求逆,减少计算量Q_inv = 1/QR_inv = 1/R# 重新初始化I0为协方差阵的逆I0 = np.linalg.inv(P0.copy())for t in range(1, T):M_k_1 = np.dot(np.dot(Phi_inv.T, I0), Phi_inv)N_k_1 = np.dot(np.dot(np.dot(M_k_1, Gamma), np.linalg.pinv(np.dot(np.dot(Gamma.T, M_k_1), Gamma) + Q_inv)), Gamma.T)S_pre = np.dot((I - N_k_1), np.dot(Phi_inv.T, s_IF[:, t-1]))I_pre = np.dot((I - N_k_1), M_k_1)s_IF[:, t] = S_pre + np.dot(H.T, np.dot(R_inv, z_mea[:, t]))I0 = I_pre + np.dot(H.T, np.dot(R_inv, H))x_IF[:, t] = np.dot(np.linalg.pinv(I0), s_IF[:, t])# 画图filter_plot(x_state, z_mea, x_KF, x_IF, None, None, None, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 04-2 习题2:UD滤波elif self.method_flag == 'UD滤波':mode = 'Problem 2'mode_str = 'UD'# 分配空间并赋予滤波器变量初值x_UD = np.zeros((nS, T)) # UD滤波状态值x_UD[:, 0] = x_state[:, 0] # UD滤波器赋估计初值P0_ud = np.diag([10**2, 1**2]) # 初始协方差阵for t in range(1, T):# 时间更新xUD_pre = np.dot(Phi, x_UD[:, t-1])U, D = udu(P0_ud) # 将协方差阵作UD分解U, D = UD_update(U, D, Phi, Gamma, Q, H, R, 'T') # 时间更新P_pre = np.dot(np.dot(U, np.diag(D.flatten())), U.T)# 量测更新K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + R))x_UD[:, t] = xUD_pre + np.dot(K, (z_mea[:, t] - np.dot(H, xUD_pre)))U, D = udu(P_pre) # 将一步预测协方差阵作UD分解U, D = UD_update(U, D, Phi, Gamma, Q, H, R, 'M') # 测量更新P0_ud = np.dot(np.dot(U, np.diag(D.flatten())), U.T)# 画图filter_plot(x_state, z_mea, x_KF, None, x_UD, None, None, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 04-3 习题3:遗忘滤波elif self.method_flag == '遗忘滤波':mode = 'Problem 3'mode_str = 'FD'# 根据题设条件,设置初始化参数a = 1 # 加速度(m/s^2),减速机动,因为速度为负,所以这里符号为正acc = np.dot(np.array([0.5*Ts**2, Ts]), a) # 构造矩阵t_100 = 100 # 时间(s),导弹在第100秒时,减速机动t_200 = 200 # 时间(s),导弹在第200秒后,停止减速机动,进入匀速运动# 根据题设条件,用模型模拟真实状态for t in range(1, T):if t < t_100 or t >= t_200:x_state[:, t] = np.dot(Phi, x_state[:, t-1]) + np.dot(Gamma, W[0, t]).squeeze()else:x_state[:, t] = np.dot(Phi, x_state[:, t-1]) + acc + np.dot(Gamma, W[0, t]).squeeze()z_mea[:, t] = np.dot(H, x_state[:, t]) + V[0, t]# 分配空间并赋予滤波器变量初值s = 1.1 # 渐消因子x_KF = np.zeros((nS, T)) # 卡尔曼滤波状态值x_KF[:, 0] = x_state[:, 0] # 卡尔曼滤波器估计初值x_FD = np.zeros((nS, T)) # 遗忘滤波状态值x_FD[:, 0] = x_state[:, 0] # 遗忘滤波器赋估计初值# 再做一次卡尔曼滤波P0_kf = np.diag([10**2, 1**2]) # 重新设置初始P0for t in range(1, T):x_KF[:, t], P0_kf = KF(x_KF, P0_kf, z_mea, Phi, Gamma, H, Q, R, I, t)# 遗忘滤波P0_fd = np.diag([10**2, 1**2]) # 重新设置初始P0for t in range(1, T):# 时间更新xFD_pre = np.dot(Phi, x_FD[:, t-1])P_pre = np.dot(np.dot(Phi, s * P0_fd), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T)# 量测更新K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + R))x_FD[:, t] = xFD_pre + np.dot(K, (z_mea[:, t] - np.dot(H, xFD_pre)))P0_fd = np.dot((I - np.dot(K, H)), P_pre)# 画图filter_plot(x_state, z_mea, x_KF, None, None, x_FD, None, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 04-4-1 习题4(1)elif self.method_flag == '自适应遗忘滤波(1)':mode = 'Problem 4-1'mode_str = 'FT1'R_1s = 100 # 量测噪声方差为100m^2R_100s = 10000 # 量测噪声方差为10000m^2# 构造量测噪声序列V = np.zeros((1, T))V[0, :100] = np.sqrt(R_1s) * np.random.randn(100) # 1-100s,方差为100m^2V[0, 100:] = np.sqrt(R_100s) * np.random.randn(T-100) # 100s之后,方差为10000m^2# 根据题设条件,用模型模拟真实状态for t in range(1, T):if t == 150: # 在150s时出现量测故障导致雷达输出为0z_mea[:, t] = 0else:z_mea[:, t] = np.dot(H, x_state[:, t]) + V[0, t]# 再做一次卡尔曼滤波P0_kf = np.diag([10**2, 1**2]) # 重新设置初始P0for t in range(1, T):# 时间更新xKF_pre = np.dot(Phi, x_KF[:, t-1])P_pre = np.dot(np.dot(Phi, P0_kf), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T)# 量测更新K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + R_1s))x_KF[:, t] = xKF_pre + np.dot(K, (z_mea[:, t] - np.dot(H, xKF_pre)))P0_kf = np.dot((I - np.dot(K, H)), P_pre)# 分配空间并赋予滤波器变量初值x_fault = np.zeros((nS, T)) # 自适应遗忘滤波状态值x_fault[:, 0] = x_state[:, 0] # 自适应遗忘滤波器赋估计初值beta = np.zeros(T)beta[0] = 1b = 0.999 # 渐消因子C = np.dot(np.dot(H, (np.dot(np.dot(Phi, P0), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T))), H.T) + R # 新息序列方差阵zNew = 15# 量测噪声方差自适应滤波处理for t in range(1, T):# 时间更新xFault_pre = np.dot(Phi, x_fault[:, t-1]) # 状态一步预测P_pre = np.dot(np.dot(Phi, P0), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T) # 协方差一步预测# 量测更新beta[t] = beta[t-1] / (beta[t-1] + b) # 更新新息序列方差阵的系数C = (1 - beta[t]) * C + beta[t] * (zNew * zNew)if abs(np.trace(C) / np.trace(np.dot(np.dot(H, P_pre), H.T) + R_1s)) > 2: # 量测故障检测与隔离x_fault[:, t] = xFault_pre # 无需量测更新,估计值用时间更新值代替P0 = P_preelse:alpha = np.trace(C - np.dot(np.dot(H, P_pre), H.T)) / np.trace(np.array([[R_1s]])) # 将 R_1s 转换为 1x1 矩阵K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + alpha * R_1s))zNew = z_mea[:, t] - np.dot(H, xFault_pre)x_fault[:, t] = xFault_pre + np.dot(K, zNew)P0 = np.dot((I - np.dot(K, H)), P_pre)# 画图filter_plot(x_state, z_mea, x_KF, None, None, None, x_fault, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 04-4-2 习题4(2)elif self.method_flag == '自适应遗忘滤波(2)':mode = 'Problem 4-2'mode_str = 'FT2'R_1s = 100 # 量测噪声方差为100m^2R_100s = 10 # 量测噪声方差为10m^2# 构造量测噪声序列V = np.zeros((1, T))V[0, :100] = np.sqrt(R_1s) * np.random.randn(100) # 1-100s,方差为100m^2V[0, 100:] = np.sqrt(R_100s) * np.random.randn(T-100) # 100s之后,方差为10m^2# 根据题设条件,用模型模拟真实状态for t in range(1, T):z_mea[:, t] = np.dot(H, x_state[:, t]) + V[0, t]# 再做一次卡尔曼滤波P0_kf = np.diag([10**2, 1**2]) # 重新设置初始P0for t in range(1, T):# 时间更新xKF_pre = np.dot(Phi, x_KF[:, t-1])P_pre = np.dot(np.dot(Phi, P0_kf), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T)# 量测更新K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + R_1s))x_KF[:, t] = xKF_pre + np.dot(K, (z_mea[:, t] - np.dot(H, xKF_pre)))P0_kf = np.dot((I - np.dot(K, H)), P_pre)# 分配空间并赋予滤波器变量初值x_fault = np.zeros((nS, T)) # 自适应遗忘滤波状态值x_fault[:, 0] = x_state[:, 0] # 自适应遗忘滤波器赋估计初值beta = np.zeros(T)beta[0] = 1b = 0.999 # 渐消因子C = np.dot(np.dot(H, (np.dot(np.dot(Phi, P0), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T))), H.T) + R # 新息序列方差阵zNew = 15# 量测噪声方差自适应滤波处理for t in range(1, T):# 时间更新xFault_pre = np.dot(Phi, x_fault[:, t-1]) # 状态一步预测P_pre = np.dot(np.dot(Phi, P0), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T) # 协方差一步预测# 量测更新beta[t] = beta[t-1] / (beta[t-1] + b) # 更新新息序列方差阵的系数C = (1 - beta[t]) * C + beta[t] * (zNew * zNew)if abs(np.trace(C) / np.trace(np.dot(np.dot(H, P_pre), H.T) + R_1s)) > 2: # 量测故障检测与隔离x_fault[:, t] = xFault_pre # 无需量测更新,估计值用时间更新值代替P0 = P_preelse:alpha = np.trace(C - np.dot(np.dot(H, P_pre), H.T)) / np.trace(np.array([[R_1s]])) # 将 R_1s 转换为 1x1 矩阵K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + alpha * R_1s))zNew = z_mea[:, t] - np.dot(H, xFault_pre)x_fault[:, t] = xFault_pre + np.dot(K, zNew)P0 = np.dot((I - np.dot(K, H)), P_pre)# 画图filter_plot(x_state, z_mea, x_KF, None, None, None, x_fault, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])
3.1.10 保存数据
# 保存数据
if self.save_data_flag:data_to_save = {'x_state': x_state,'z_mea': z_mea,'xKF': x_KF,'mode': mode,'T': T,'Ts': Ts,'Q': Q,'R': R}if mode == 'Problem 1':data_to_save['xIF'] = x_IFelif mode == 'Problem 2':data_to_save['xUD'] = x_UDelif mode == 'Problem 3':data_to_save['xFD'] = x_FDelif mode in ['Problem 4-1', 'Problem 4-2']:data_to_save['xFault'] = x_faultif self.with_time_flag:# 获取当前日期now = datetime.now()date_str = now.strftime("%Y%m%d")save_path = f'./data/{mode_str}_{date_str}.pkl'else:save_path = f'./data/{mode_str}.pkl'# 使用pickle保存数据with open(save_path, 'wb') as f:pickle.dump(data_to_save, f, protocol=pickle.HIGHEST_PROTOCOL)print(f"仿真结果已保存至: ./figures/")print(f"数据已保存至: {save_path}")
print("===== 仿真计算结束 =====")
这个部分写在run_simulation()
里。
3.1.11 分辨率自适应
不同屏幕分辨率下,界面大小也会有所不同。我自己使用的屏幕是一块2k屏幕,在这块屏幕上开发完毕后。再在笔记本电脑屏幕上打开,界面就会崩坏。如下图所示。
这样是不行的,因为不能只在自己特定的屏幕上显示正常,以后还要放在其他电脑上运行,无法预知对方的屏幕分辨率。所以需要分辨率自适应变化的功能,在主程序里写入如下代码。
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
再者,加上上面的代码之后,控件的分辨率自适应问题解决了,但是字体的分辨率自适应问题还没有解决。字体变得特别特别小,几乎看不见。
所以还要加上这一段代码。
screen = app.primaryScreen() # 返回当前主显示器的信息
scale_factor = screen.logicalDotsPerInch() / 96 # 96dpi为标准缩放(100%),结果如1.75(175%缩放)
font = QtGui.QFont() # 创建默认字体
font.setPointSize(int(10 * scale_factor)) # 原字体10pt,乘以缩放因子
app.setFont(font)
3.2 最终成品
最后,整个界面如下图所示。
主程序代码如下。
import sys
import os
import numpy as np
# 导入PyQt5所需模块
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
from Ui_mainGUI_alpha import Ui_MainWindow
# 导入PyQt风格模块# 导入方法
from user_function.KF import KF
from user_function.UD_update import UD_update
from user_function.udu import udu
from user_function.filter_plot import filter_plot
# 导入保存数据所需模块
import pickle
import numpy as np
from datetime import datetimeclass MainApp(QMainWindow, Ui_MainWindow):def __init__(self):"""主窗口初始化"""super().__init__() # 调用父类的初始化方法self.setupUi(self) # 加载UI设计self.init_ui() # 初始化界面设置self.bind_events() # 绑定UI事件处理函数self.init_flags() # 初始化标志位变量# 设置图形显示区域无边框和滚动条self.eq1_disp.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)self.eq1_disp.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)self.eq2_disp.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)self.eq2_disp.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)# 设置默认选中状态self.yes_btn.setChecked(True) # 默认选择"带时间戳"选项self.save_fig_box.setChecked(False) # 默认选择"保存图片"选项self.save_data_box.setChecked(True) # 默认选择"保存数据"选项# 初始化下拉菜单栏状态self.method_menu.currentIndexChanged.emit(0) # 触发索引0的变化(默认选项)# 设置应用程序图标self.set_application_icon()# 绑定保存图片复选框的状态变更事件self.save_fig_box.stateChanged.connect(self.check_save_fig_warning)def init_ui(self):"""初始化界面显示设置"""self.setWindowTitle("卡尔曼滤波仿真软件Demo") # 设置窗口标题self.update_graph_displays(0) # 显示默认滤波方法对应的公式图def init_flags(self):"""初始化控制标志位,存储UI选择状态"""self.method_flag = "" # 当前选择的滤波方法self.save_fig_flag = False # 是否保存图表self.save_data_flag = False # 是否保存数据self.with_time_flag = False # 是否在保存数据时添加时间戳def bind_events(self):"""绑定UI元素事件到处理函数"""self.exit_btn.clicked.connect(self.close) # 退出按钮self.simu_btn.clicked.connect(self.run_simulation) # 仿真按钮self.method_menu.currentIndexChanged.connect(self.update_method_flag) # 滤波方法选择self.save_fig_box.toggled.connect(self.update_save_fig_flag) # 保存图表复选框self.save_data_box.toggled.connect(self.update_save_data_flag) # 保存数据复选框self.yes_btn.toggled.connect(self.update_with_time_flag) # 带时间戳单选按钮self.no_btn.toggled.connect(self.update_with_time_flag) # 不带时间戳单选按钮def set_application_icon(self):"""设置应用程序图标"""icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'asserts', 'logo.ico')if os.path.exists(icon_path):self.setWindowIcon(QtGui.QIcon(icon_path))print(f"应用程序图标已设置: {icon_path}")else:print(f"警告: 图标文件不存在 - {icon_path}")def check_save_fig_warning(self, state):"""检测保存图片复选框状态,显示提示"""if state == QtCore.Qt.Checked: # 当勾选时msg = QMessageBox()msg.setIcon(QMessageBox.Warning)msg.setText("选择“保存图片”则仿真完毕之后不会展示结果。")msg.setWindowTitle("操作提示")msg.exec_()def update_method_flag(self, index):"""更新滤波方法标志位并刷新公式图显示"""# 滤波方法索引到名称的映射method_map = {0: "请选择滤波方式",1: "信息滤波",2: "UD滤波",3: "遗忘滤波",4: "自适应遗忘滤波(1)",5: "自适应遗忘滤波(2)"}self.method_flag = method_map.get(index, "请选择滤波方式") # 更新方法标志位self.update_graph_displays(index) # 刷新公式图显示def update_save_fig_flag(self, state):"""更新保存图表标志位"""self.save_fig_flag = statedef update_save_data_flag(self, state):"""更新保存数据标志位"""self.save_data_flag = statedef update_with_time_flag(self, state):"""更新保存数据时是否添加时间戳标志位"""self.with_time_flag = self.yes_btn.isChecked() # 根据Yes按钮状态更新标志def update_graph_displays(self, index):"""根据选择的滤波方法更新公式图显示"""# 滤波方法索引到公式图路径的映射image_paths = [("./asserts/KF1.png", "./asserts/KF2.png"), # 0: 卡尔曼滤波("./asserts/IF1.png", "./asserts/IF2.png"), # 1: 信息滤波("./asserts/UD1.png", "./asserts/UD2.png"), # 2: UD滤波("./asserts/FD1.png", "./asserts/FD2.png"), # 3: 遗忘滤波("./asserts/FT1.png", "./asserts/FT2.png"), # 4: 自适应遗忘滤波(1)("./asserts/FT1.png", "./asserts/FT2.png"), # 5: 自适应遗忘滤波(2)]if 0 <= index < len(image_paths): # 确保索引有效img1_path, img2_path = image_paths[index]self.display_image(self.eq1_disp, img1_path) # 显示第一个公式图self.display_image(self.eq2_disp, img2_path) # 显示第二个公式图def display_image(self, graphics_view, image_path):"""在指定的QGraphicsView中显示图片"""scene = QtWidgets.QGraphicsScene(graphics_view) # 创建图形场景pixmap = QtGui.QPixmap(image_path) # 加载图片# 缩放图片以适应视图大小,保持纵横比并平滑缩放scaled_pixmap = pixmap.scaled(graphics_view.size(),QtCore.Qt.KeepAspectRatio,QtCore.Qt.SmoothTransformation)scene.addPixmap(scaled_pixmap) # 将缩放后的图片添加到场景graphics_view.setScene(scene) # 设置视图的场景graphics_view.setAlignment(QtCore.Qt.AlignCenter) # 图片居中显示def get_lineedit_values(self):"""获取所有QLineEdit控件中的参数值"""return {"simu_time": self.simu_edit.text(), # 仿真时长"sample_time": self.sample_edit.text(), # 采样时长"init_state": self.init_edit.text(), # 初始状态"process_noise": self.process_edit.text(), # 过程噪声"P_matrix": self.P_edit.text(), # 协方差矩阵"measure_noise": self.measure_edit.text(), # 量测噪声"info_matrix": self.info_edit.text() # 信息矩阵}def parse_numeric_params(self, params):"""解析并验证数值参数,转换为指定数据类型"""errors = []parsed = {}# 解析整数for key in ["simu_time"]:try:parsed[key] = int(params[key])except ValueError:errors.append(f"{key}: 请输入有效整数")# 解析浮点数参数for key in ["sample_time", "process_noise", "measure_noise"]:try:parsed[key] = float(params[key])except ValueError:errors.append(f"{key}: 请输入有效的浮点数")# 解析初始状态 (2,) 数组try:# 处理类似 "[10000;-300]" 或 "[10000, -300]" 的输入格式values = params["init_state"].strip("[]").replace(";", ",").split(",")parsed["init_state"] = np.array([float(v.strip()) for v in values], dtype=float) # v.strip()用于移除字符串开头和结尾的特定字符if parsed["init_state"].shape != (2,):raise ValueError("需要包含两个元素的数组")except Exception as e:errors.append(f"init_state: {str(e)} (格式应为 [值1, 值2])")# 解析协方差矩阵 (2,) 数组try:# 处理类似 "[100,1]" 或 "[100;1]" 的输入格式values = params["P_matrix"].strip("[]").replace(";", ",").split(",")parsed["P_matrix"] = np.array([float(v.strip()) for v in values], dtype=float) # v.strip()用于移除字符串开头和结尾的特定字符if parsed["P_matrix"].shape != (2,):raise ValueError("需要包含两个元素的数组")except Exception as e:errors.append(f"P_matrix: {str(e)} (格式应为 [值1, 值2])")# 解析信息矩阵 (2,2) 数组try:# 处理类似 "[0,0;0,0]" 的输入格式rows = params["info_matrix"].strip("[]").split(";")parsed["info_matrix"] = np.array([[float(v) for v in row.split(",")] for row in rows], dtype=float)if parsed["info_matrix"].shape != (2, 2):raise ValueError("需要2x2的数组格式")except Exception as e:errors.append(f"info_matrix: {str(e)} (格式应为 [值1,值2;值3,值4])")return parsed, errorsdef run_simulation(self):"""执行仿真按钮点击事件处理"""# 收集标志位参数flags = {"method_flag": self.method_flag,"save_fig_flag": self.save_fig_flag,"save_data_flag": self.save_data_flag,"save_data_time_flag": self.with_time_flag}# 收集文本框参数params_txt = self.get_lineedit_values()# 解析数值参数params, errors = self.parse_numeric_params(params_txt)# 检查是否有解析错误if errors:error_msg = "参数解析错误:\n" + "\n".join(errors)QMessageBox.critical(self, "参数错误", error_msg)return# 打印所有参数(实际应用中可替换为仿真计算逻辑)# print("\n===== 仿真参数设置 =====")# print("滤波方法:", flags["method_flag"])# print("保存图表:", flags["save_fig_flag"])# print("保存数据:", flags["save_data_flag"])# print("数据带时间戳:", flags["save_data_time_flag"])# print("\n===== 仿真具体参数 =====")# print("仿真时长:", params["simu_time"])# print("采样时长:", params["sample_time"])# print("初始状态:\n", params["init_state"])# print("过程噪声:", params["process_noise"])# print("协方差矩阵:\n", params["P_matrix"])# print("量测噪声:", params["measure_noise"])# print("信息矩阵:\n", params["info_matrix"])print("===== 开始仿真计算 =====")# 仿真参数T = params["simu_time"] # 仿真时长Ts = params["sample_time"] # 采样时长Q = params["process_noise"] * Ts # 过程噪声R = params["measure_noise"] # 量测噪声# 生成噪声序列W = np.sqrt(Q) * np.random.randn(1, T)V = np.sqrt(R) * np.random.randn(1, T)p0 = params["P_matrix"] # 初始协方差阵参数P0 = np.diag([p0[0], p0[1]]) # 初始协方差阵I0 = params["info_matrix"] # 初始信息阵设置为0# 系统矩阵A = np.array([[0, 1], [0, 0]]) # 状态矩阵I = np.eye(2) # 单位阵Phi = I + A * Ts # 离散化H = np.array([[1, 0]]) # 量测矩阵Gamma = np.array([[0], [1]])# 设置滤波维度nS = 2 # 状态维度nZ = 1 # 观测维度# 分配空间x_state = np.zeros((nS, T)) # 系统真实值z_mea = np.zeros((nZ, T)) # 系统观测值x_KF = np.zeros((nS, T)) # 卡尔曼滤波状态值# 赋初值x_state[:, 0] = params["init_state"] # 系统状态初值z_mea[:, 0] = np.dot(H, x_state[:, 0]) # 系统观测初值x_KF[:, 0] = x_state[:, 0] # 卡尔曼滤波器估计初值# 02 用模型模拟真实状态for t in range(1, T):x_state[:, t] = np.dot(Phi,x_state[:, t-1]) + np.dot(Gamma, W[0, t]).squeeze()z_mea[:, t] = np.dot(H, x_state[:, t]) + V[0, t]# 03-1 Kalman滤波if self.method_flag == '请选择滤波方式':mode = 'null'print("请先选择滤波方法!")QMessageBox.warning(self, "操作提示", "请先选择滤波方法!", QMessageBox.Ok)return # 终止仿真流程# #下述代码为KF滤波方法# mode = 'null'# mode_str = 'KF'# P0_kf = P0.copy() # 复制初始P0# for t in range(1, T):# x_KF[:, t], P0_kf = KF(x_KF, P0_kf, z_mea, Phi, Gamma, H, Q, R, I, t)# # 画图# filter_plot(x_state, z_mea, x_KF, None, None, None, None, T, mode, self.save_fig_flag, mode_str)# 04-1 习题1:信息滤波elif self.method_flag == '信息滤波':mode = 'Problem 1'mode_str = 'IF'# 分配空间并赋予滤波器变量初值s_IF = np.zeros((nS, T)) # 信息滤波状态值x_IF = np.zeros((nS, T))s_IF[:, 0] = np.dot(I0, x_state[:, 0]) # 信息滤波器赋估计初值Phi_inv = np.linalg.pinv(Phi) # 先求逆,减少计算量Q_inv = 1/QR_inv = 1/R# 重新初始化I0为协方差阵的逆I0 = np.linalg.inv(P0.copy())for t in range(1, T):M_k_1 = np.dot(np.dot(Phi_inv.T, I0), Phi_inv)N_k_1 = np.dot(np.dot(np.dot(M_k_1, Gamma), np.linalg.pinv(np.dot(np.dot(Gamma.T, M_k_1), Gamma) + Q_inv)), Gamma.T)S_pre = np.dot((I - N_k_1), np.dot(Phi_inv.T, s_IF[:, t-1]))I_pre = np.dot((I - N_k_1), M_k_1)s_IF[:, t] = S_pre + np.dot(H.T, np.dot(R_inv, z_mea[:, t]))I0 = I_pre + np.dot(H.T, np.dot(R_inv, H))x_IF[:, t] = np.dot(np.linalg.pinv(I0), s_IF[:, t])# 画图filter_plot(x_state, z_mea, x_KF, x_IF, None, None, None, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 04-2 习题2:UD滤波elif self.method_flag == 'UD滤波':mode = 'Problem 2'mode_str = 'UD'# 分配空间并赋予滤波器变量初值x_UD = np.zeros((nS, T)) # UD滤波状态值x_UD[:, 0] = x_state[:, 0] # UD滤波器赋估计初值P0_ud = np.diag([10**2, 1**2]) # 初始协方差阵for t in range(1, T):# 时间更新xUD_pre = np.dot(Phi, x_UD[:, t-1])U, D = udu(P0_ud) # 将协方差阵作UD分解U, D = UD_update(U, D, Phi, Gamma, Q, H, R, 'T') # 时间更新P_pre = np.dot(np.dot(U, np.diag(D.flatten())), U.T)# 量测更新K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + R))x_UD[:, t] = xUD_pre + np.dot(K, (z_mea[:, t] - np.dot(H, xUD_pre)))U, D = udu(P_pre) # 将一步预测协方差阵作UD分解U, D = UD_update(U, D, Phi, Gamma, Q, H, R, 'M') # 测量更新P0_ud = np.dot(np.dot(U, np.diag(D.flatten())), U.T)# 画图filter_plot(x_state, z_mea, x_KF, None, x_UD, None, None, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 04-3 习题3:遗忘滤波elif self.method_flag == '遗忘滤波':mode = 'Problem 3'mode_str = 'FD'# 根据题设条件,设置初始化参数a = 1 # 加速度(m/s^2),减速机动,因为速度为负,所以这里符号为正acc = np.dot(np.array([0.5*Ts**2, Ts]), a) # 构造矩阵t_100 = 100 # 时间(s),导弹在第100秒时,减速机动t_200 = 200 # 时间(s),导弹在第200秒后,停止减速机动,进入匀速运动# 根据题设条件,用模型模拟真实状态for t in range(1, T):if t < t_100 or t >= t_200:x_state[:, t] = np.dot(Phi, x_state[:, t-1]) + np.dot(Gamma, W[0, t]).squeeze()else:x_state[:, t] = np.dot(Phi, x_state[:, t-1]) + acc + np.dot(Gamma, W[0, t]).squeeze()z_mea[:, t] = np.dot(H, x_state[:, t]) + V[0, t]# 分配空间并赋予滤波器变量初值s = 1.1 # 渐消因子x_KF = np.zeros((nS, T)) # 卡尔曼滤波状态值x_KF[:, 0] = x_state[:, 0] # 卡尔曼滤波器估计初值x_FD = np.zeros((nS, T)) # 遗忘滤波状态值x_FD[:, 0] = x_state[:, 0] # 遗忘滤波器赋估计初值# 再做一次卡尔曼滤波P0_kf = np.diag([10**2, 1**2]) # 重新设置初始P0for t in range(1, T):x_KF[:, t], P0_kf = KF(x_KF, P0_kf, z_mea, Phi, Gamma, H, Q, R, I, t)# 遗忘滤波P0_fd = np.diag([10**2, 1**2]) # 重新设置初始P0for t in range(1, T):# 时间更新xFD_pre = np.dot(Phi, x_FD[:, t-1])P_pre = np.dot(np.dot(Phi, s * P0_fd), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T)# 量测更新K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + R))x_FD[:, t] = xFD_pre + np.dot(K, (z_mea[:, t] - np.dot(H, xFD_pre)))P0_fd = np.dot((I - np.dot(K, H)), P_pre)# 画图filter_plot(x_state, z_mea, x_KF, None, None, x_FD, None, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 04-4-1 习题4(1)elif self.method_flag == '自适应遗忘滤波(1)':mode = 'Problem 4-1'mode_str = 'FT1'R_1s = 100 # 量测噪声方差为100m^2R_100s = 10000 # 量测噪声方差为10000m^2# 构造量测噪声序列V = np.zeros((1, T))V[0, :100] = np.sqrt(R_1s) * np.random.randn(100) # 1-100s,方差为100m^2V[0, 100:] = np.sqrt(R_100s) * np.random.randn(T-100) # 100s之后,方差为10000m^2# 根据题设条件,用模型模拟真实状态for t in range(1, T):if t == 150: # 在150s时出现量测故障导致雷达输出为0z_mea[:, t] = 0else:z_mea[:, t] = np.dot(H, x_state[:, t]) + V[0, t]# 再做一次卡尔曼滤波P0_kf = np.diag([10**2, 1**2]) # 重新设置初始P0for t in range(1, T):# 时间更新xKF_pre = np.dot(Phi, x_KF[:, t-1])P_pre = np.dot(np.dot(Phi, P0_kf), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T)# 量测更新K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + R_1s))x_KF[:, t] = xKF_pre + np.dot(K, (z_mea[:, t] - np.dot(H, xKF_pre)))P0_kf = np.dot((I - np.dot(K, H)), P_pre)# 分配空间并赋予滤波器变量初值x_fault = np.zeros((nS, T)) # 自适应遗忘滤波状态值x_fault[:, 0] = x_state[:, 0] # 自适应遗忘滤波器赋估计初值beta = np.zeros(T)beta[0] = 1b = 0.999 # 渐消因子C = np.dot(np.dot(H, (np.dot(np.dot(Phi, P0), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T))), H.T) + R # 新息序列方差阵zNew = 15# 量测噪声方差自适应滤波处理for t in range(1, T):# 时间更新xFault_pre = np.dot(Phi, x_fault[:, t-1]) # 状态一步预测P_pre = np.dot(np.dot(Phi, P0), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T) # 协方差一步预测# 量测更新beta[t] = beta[t-1] / (beta[t-1] + b) # 更新新息序列方差阵的系数C = (1 - beta[t]) * C + beta[t] * (zNew * zNew)if abs(np.trace(C) / np.trace(np.dot(np.dot(H, P_pre), H.T) + R_1s)) > 2: # 量测故障检测与隔离x_fault[:, t] = xFault_pre # 无需量测更新,估计值用时间更新值代替P0 = P_preelse:alpha = np.trace(C - np.dot(np.dot(H, P_pre), H.T)) / np.trace(np.array([[R_1s]])) # 将 R_1s 转换为 1x1 矩阵K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + alpha * R_1s))zNew = z_mea[:, t] - np.dot(H, xFault_pre)x_fault[:, t] = xFault_pre + np.dot(K, zNew)P0 = np.dot((I - np.dot(K, H)), P_pre)# 画图filter_plot(x_state, z_mea, x_KF, None, None, None, x_fault, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 04-4-2 习题4(2)elif self.method_flag == '自适应遗忘滤波(2)':mode = 'Problem 4-2'mode_str = 'FT2'R_1s = 100 # 量测噪声方差为100m^2R_100s = 10 # 量测噪声方差为10m^2# 构造量测噪声序列V = np.zeros((1, T))V[0, :100] = np.sqrt(R_1s) * np.random.randn(100) # 1-100s,方差为100m^2V[0, 100:] = np.sqrt(R_100s) * np.random.randn(T-100) # 100s之后,方差为10m^2# 根据题设条件,用模型模拟真实状态for t in range(1, T):z_mea[:, t] = np.dot(H, x_state[:, t]) + V[0, t]# 再做一次卡尔曼滤波P0_kf = np.diag([10**2, 1**2]) # 重新设置初始P0for t in range(1, T):# 时间更新xKF_pre = np.dot(Phi, x_KF[:, t-1])P_pre = np.dot(np.dot(Phi, P0_kf), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T)# 量测更新K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + R_1s))x_KF[:, t] = xKF_pre + np.dot(K, (z_mea[:, t] - np.dot(H, xKF_pre)))P0_kf = np.dot((I - np.dot(K, H)), P_pre)# 分配空间并赋予滤波器变量初值x_fault = np.zeros((nS, T)) # 自适应遗忘滤波状态值x_fault[:, 0] = x_state[:, 0] # 自适应遗忘滤波器赋估计初值beta = np.zeros(T)beta[0] = 1b = 0.999 # 渐消因子C = np.dot(np.dot(H, (np.dot(np.dot(Phi, P0), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T))), H.T) + R # 新息序列方差阵zNew = 15# 量测噪声方差自适应滤波处理for t in range(1, T):# 时间更新xFault_pre = np.dot(Phi, x_fault[:, t-1]) # 状态一步预测P_pre = np.dot(np.dot(Phi, P0), Phi.T) + np.dot(np.dot(Gamma, Q), Gamma.T) # 协方差一步预测# 量测更新beta[t] = beta[t-1] / (beta[t-1] + b) # 更新新息序列方差阵的系数C = (1 - beta[t]) * C + beta[t] * (zNew * zNew)if abs(np.trace(C) / np.trace(np.dot(np.dot(H, P_pre), H.T) + R_1s)) > 2: # 量测故障检测与隔离x_fault[:, t] = xFault_pre # 无需量测更新,估计值用时间更新值代替P0 = P_preelse:alpha = np.trace(C - np.dot(np.dot(H, P_pre), H.T)) / np.trace(np.array([[R_1s]])) # 将 R_1s 转换为 1x1 矩阵K = np.dot(np.dot(P_pre, H.T), np.linalg.pinv(np.dot(np.dot(H, P_pre), H.T) + alpha * R_1s))zNew = z_mea[:, t] - np.dot(H, xFault_pre)x_fault[:, t] = xFault_pre + np.dot(K, zNew)P0 = np.dot((I - np.dot(K, H)), P_pre)# 画图filter_plot(x_state, z_mea, x_KF, None, None, None, x_fault, T, mode, self.save_fig_flag, mode_str)print("滤波方法:", flags["method_flag"])# 保存数据if self.save_data_flag:data_to_save = {'x_state': x_state,'z_mea': z_mea,'xKF': x_KF,'mode': mode,'T': T,'Ts': Ts,'Q': Q,'R': R}if mode == 'Problem 1':data_to_save['xIF'] = x_IFelif mode == 'Problem 2':data_to_save['xUD'] = x_UDelif mode == 'Problem 3':data_to_save['xFD'] = x_FDelif mode in ['Problem 4-1', 'Problem 4-2']:data_to_save['xFault'] = x_faultif self.with_time_flag:# 获取当前日期now = datetime.now()date_str = now.strftime("%Y%m%d")save_path = f'./data/{mode_str}_{date_str}.pkl'else:save_path = f'./data/{mode_str}.pkl'# 使用pickle保存数据with open(save_path, 'wb') as f:pickle.dump(data_to_save, f, protocol=pickle.HIGHEST_PROTOCOL)print(f"仿真结果已保存至: ./figures/")print(f"数据已保存至: {save_path}")print("===== 仿真计算结束 =====")if __name__ == '__main__':# NOTICE:01必须放在02前面# 01-高分辨率屏幕控件自适应调整QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) # 原有控件缩放,参考:https://zhuanlan.zhihu.com/p/401503085# 02-应用程序初始化app = QApplication(sys.argv)# 03-高分辨率屏幕字体自适应调整screen = app.primaryScreen() # 返回当前主显示器的信息scale_factor = screen.logicalDotsPerInch() / 96 # 96dpi为标准缩放(100%),结果如1.75(175%缩放)font = QtGui.QFont() # 创建默认字体font.setPointSize(int(10 * scale_factor)) # 原字体10pt,乘以缩放因子app.setFont(font)# 04-创建并显示应用窗口window = MainApp()window.show()sys.exit(app.exec_())
四、总结
代码我放在了Github
上,网址是:https://github.com/luwin1127/PyQt-main-GUI-KF.git。也打包了一个.exe
文件,解压就可以用。
点击上图中红框的位置。进入下一个页面之后,选择v0.1.1-release.zip
下载。
不足的地方:
- 没有使用
.qrc
文件,因为还没学会; - 编辑
qss
会让GUI更好看,使用了qt-material,不好看; - 使用
pyinstaller
打包程序占用空间很大; - 考虑使用
Qt Fluent Design
把各个控件美化一下。
参考文献
[1] 图形用户界面(GUI)开发教程[EB/OL]. (2024-01-20)[2025-05-30]https://leilie.top/2024-01-20/Study-GUI
[2] 王硕, 孙洋洋. PyQt5快速开发与实战电子书 [M]. 北京: 电子工业出版社, 2017.