【Python】基于Tkinter的“摄像头+蓝牙”综合监视器(萌新教程)

摘要

本例主要基于Tkinter创建两类窗口,在input.py中,先从输入窗口输入摄像头IP(Esp32摄像头传递视频流MJPEG格式)、蓝牙端口,传递给monitor.py用OpenCV和Serial打开,再在监视窗口中实现2x2网格,分窗口展示摄像头图像、版本信息、两个传感器数据及曲线(本来是三个1,但一个坏了qwq)

声明

作者纯纯萌新,现为大一升大二的非计算机系学生,为比赛所迫,人生中第一次正式写Python代码、也是第一次写GUI,其中很多原理和逻辑也是现学的,如有错误或者更好的修正,欢迎大佬在评论区指正!

初始库

Tkinter,PIL,cv2,os,time,serial,threading,queue,matplotlib

内容

大概逻辑

如前所述,通过建立两个简单的窗口,实现基本的GUI功能,相比下图,具体代码中涉及到更多异常处理和用户体验优化,不过基本逻辑下图已经表述得相当完备了。

以下为代码,代码较长,建议复制到自己的Pycharm或者VSCode里查看比较方便,也有详细的注释)

input窗口

解读

关于函数功能的逐条解读(缩进表示函数的附属调用关系):

【__init__】 创建输入窗口

        【input_window_init】 创建主窗口root,为配件提供“位置”(master),同时给窗口关闭键关联上on_closing函数

                【on_closing】 关闭窗口

        【input_label_init】 创建输入标签、提示符、复选框(☑决定是否保存数据,和是否打开摄像头/蓝牙,会传递给monitor决定打开窗口数量),同时给确认键关联上input_ok函数

                【input_ok】 检查输入,判断是否进入monitor窗口还是重试,同时调用remember_info或clear_info函数,决定是否保存/清空信息

                        【remember_info】 在桌面创建文件保存输入信息

                        【clear_info】 清空桌面该文件的信息

        【check_info】 打开文件自动读入上一次输入保存的信息,若不存在则跳过

代码

# input.py
# 创建输入窗口,并获取视频流IP和蓝牙串口名
# 从而传递给monitor.py

import tkinter as tk
from tkinter import ttk, messagebox
import os

# 输入窗口大小
input_root_size = "400x200+200+200"

# 后续要传递给monitor的值
url = ""
port = ""
signal = False
video_enable = True
serial_enable = True

class input:
    # 构造函数,实现窗口的初始化
    # 此时不能输入数据,要在mainloop中实现
    # check_info函数可读取并自动输入上次保存的数据
    def __init__(self):
        self.url = None
        self.port = None
        self.input_window_init()
        self.input_label_init()
        self.check_info()

    # 创建总窗口
    def input_window_init(self):
        self.root = tk.Tk()
        self.root.title("连接窗口")
        self.root.geometry(input_root_size)
        self.root.resizable(False,False)
        # 将右上角"x"与on_closing函数关联,后续用来实现"是否关闭"弹窗
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    # 创建提示栏和提示栏
    def input_label_init(self):
        self.label_url = tk.Label(self.root, text="输入摄像头ID")
        self.label_url.pack()
        self.entry_url = tk.Entry(self.root)
        self.entry_url.pack()

        self.video_enable = tk.BooleanVar()
        self.video_enable.set(True)
        self.video_enable_checkbutton = tk.Checkbutton(self.root, text="打开摄像头", variable=self.video_enable)
        self.video_enable_checkbutton.pack()

        self.label_port = tk.Label(self.root, text="输入蓝牙端口")
        self.label_port.pack()
        self.entry_port = tk.Entry(self.root)
        self.entry_port.pack()

        self.serial_enable = tk.BooleanVar()
        self.serial_enable.set(True)
        self.serial_enable_checkbutton = tk.Checkbutton(self.root, text="打开蓝牙", variable=self.serial_enable)
        self.serial_enable_checkbutton.pack()

        # 一个"记住我的输入"复选框,避免重复输入
        # remember真值用于决定是否将输入信息保存至info.txt桌面文件
        self.remember = tk.BooleanVar()
        self.remember.set(True)
        self.remember_checkbutton = tk.Checkbutton(self.root, text="记住我的输入", variable=self.remember)
        self.remember_checkbutton.pack()

        # "确认"按钮,关联input_ok函数
        self.ok_button = ttk.Button(self.root, text="确认", command=self.input_ok)
        self.ok_button.pack()

    # 确认键关联的函数,用于启动其他的处理函数
    def input_ok(self):
        # url,port传值
        # 目的:input窗口关闭后不知道input对象是否会析构
        # 用类外全局变量可保存数据
        global url, port, signal, video_enable, serial_enable
        self.url = self.entry_url.get()
        self.port = self.entry_port.get()
        if (self.url or not self.video_enable.get()) and (self.port or not self.serial_enable.get()):
            # print(1)
            url = self.url
            port = self.port
            video_enable = self.video_enable.get()
            serial_enable = self.serial_enable.get()
            self.root.destroy()
            # signal传给monitor.py使后续monitor窗口能创建
            signal = True
            # 处理前面"数据是否保存"的选择
            if self.remember.get():
                self.remember_info()
            else:
                self.clear_info()
        # 只输入一个空就匆忙提交的错误处理
        else:
            # print(2)
            messagebox.showerror("错误","输入有误,请重新输入!")

    # 关闭键关联的函数,弹出"是否关闭"弹窗,防止误触
    def on_closing(self):
        global signal
        if messagebox.askyesno("关闭窗口", "是否关闭?"):
            self.root.destroy()
            # 不会再打开monitor窗口
            signal = False

    # 自动读取与输入函数
    # 在用户输入前,读取并自动输入上一次输入信息
    # 信息存在桌面上的info.txt里(可自己改路径)
    # 如果上一次选了不保存,则会把文件清空,可以实现关闭自动输入
    def check_info(self):
        filename = "info.txt"
        desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", filename)
        # 如果文件存在,则如下处理
        # 文件不存在,对应的是第一次运行程序的情况,不存在上一次输入,直接跳过
        if os.path.exists(desktop_path):
            with open(desktop_path, 'r') as file:
                # 读第一行,判断是否为空,若空则跳过
                line1 = file.readline().strip()
                if not line1:
                    pass
                # 将一二行都分别读取,输入
                # 注意strip()去除字符串末尾的"\n"" "等字符
                else:
                    self.entry_url.insert(0,line1)
                    line2 = file.readline().strip()
                    self.entry_port.insert(0,line2)

    # 数据保存函数,input_ok函数的一个处理项
    # 正确的调用时机已经在input_ok函数里构建好
    # 只用将两行信息输入到info.txt(没有会自动创建,'w'用于覆写)
    def remember_info(self):
        filename = "info.txt"
        desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", filename)
        with open(desktop_path, 'w') as file:
            file.write(self.url + '\n' + self.port)

    # 数据清理函数,input_ok函数的一个处理项
    # 正确的调用时机也已经在input_ok函数里构建好
    # 用'w'的覆写功能,打开后自动清空
    def clear_info(self):
        filename = "info.txt"
        desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", filename)
        with open(desktop_path, 'w'):
            pass

效果

monitor窗口

解读

函数逐条解读(同上)(注意此段代码较长):

【__init__】 窗口、视频、蓝牙所有东西初始化,负责信息update的线程的建立

        【window_init】 初始化总窗口root

        【video_init】 尝试用cv2连接摄像头,并提供异常处理

        【serial_init】 尝试用serial连接蓝牙,同样提供异常处理

        【button_init】 提供保存图片(蓝牙数据自动保存)、中断线程、视频窗口三大功能的按键

                【button_save_image】 保存最近单帧图片

                【only_video】 单独开摄像头窗口,不能监视蓝牙数据,但延时更低

        【setup_ui】 绑定窗口关闭(同上input),创建数据更新的队列和update线程

                【all_update】 update线程,执行摄像头和蓝牙数据更新

                        【video_update】 将读到的单帧frame放入video_queue,等待主线程处理

                        【serial_update】 将读到的蓝牙数据预处理后放入serial_queue

                【check_all_queue】 主线程中无限循环,直到结束数据更新,内含所有从队列中获取信息并更新到GUI界面的函数

                        【check_video_queue】 将队列里frame类型的单帧图片转换为image,在(0,0~1)窗口展示并更新

                        【check_serial_queue_pre】 蓝牙连接的Arduino板上烧写代码,已使蓝牙按“#xxx/xxx*”格式传数据,在serial_queue里获取的是初步处理后的单个data,将xxx/xxx分解,传给后续处理

                                【auto_save_data】 自动保存蓝牙数据

                                【check_serial_queue2/3】 (1因为传感器坏了一个而舍弃)保存二十个数据,并在(1,0)和(1,2)分别按列展示后10个数据

                                【draw_serial_figure2/3】 根据保存的20组数据绘图,展示在(1,1)和(1,3)

                【on_closing】 结束数据更新,释放资源,关闭窗口

代码

# monitor.py
# 创建并实时更新监视窗口
# 内含连接失败的错误处理

import tkinter as tk
from tkinter import ttk, messagebox
import cv2
import serial
from PIL import Image, ImageTk
import os
import threading
import queue
import time
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure

# 监视总窗口大小
monitor_root_size = "910x760+50+50"
# 视频缩放尺寸
frame_size = (450, 350)
# 摄像头帧率,鉴于python的处理速度和本程序的逻辑,建议不大于7
FPS = 7
# 数据绘图大小,像素*100=英寸
figsize = (3.5, 3.5)
# 要连接的蓝牙波特率
baud_rate = 9600
# 蓝牙传输字符串的分隔符
symbol = ['#', '/', '*']
# 数据列表标签
data_label = ['', '水温', 'TDS']
# 数据图像标签
figure_legend = ['', '°C', 'ppm']

class monitor:
    # 整个监视窗口的初始化
    def __init__(self, url, port, video_enable, serial_enable):
        self.url = url
        self.port = port
        self.video_enable = video_enable
        self.serial_enable = serial_enable
        # 创建停止事件,后续判定中用到
        self.stop_event = threading.Event()
        self.retry_event = False
        self.only_video_choice = False
        self.window_init()

        # 视频的video_init初始化
        # 若连接失败,选择重试使retry_event真,则后续全跳过
        # 在main.py中的循环重回input窗口
        self.video_init()
        if self.retry_event:
            pass
        else:
            # 蓝牙传输数据的serial_init初始化
            # 重试功能同上
            self.serial_init()
            if self.retry_event:
                pass
            else:
                self.button_init()
                # 最重要也最抽象的,线程创建
                self.setup_ui()

    # 大窗口的初始化
    # 此时不显示视频,蓝牙数据,按钮
    def window_init(self):
        self.root = tk.Tk()
        self.root.title("监视窗口")
        self.root.geometry(monitor_root_size)
        self.root.resizable(False,False)

        # 创建工具栏,给后面创建的按钮
        self.toolbar_frame = tk.Frame(self.root, bd=2, relief=tk.RAISED, bg='green')
        self.toolbar_frame.pack(side=tk.TOP, pady=1, fill=tk.X)
        self.toolbar = ttk.Frame(self.toolbar_frame)
        self.toolbar.pack(side=tk.TOP, fill=tk.X)

        # 前几行忘记给root建立frame了,补一个后面用grid方便
        self.image_frame = tk.Frame(self.root)
        self.image_frame.pack(fill=tk.BOTH)

    # 摄像头连接与错误处理
    def video_init(self):
        # 在image_frame中建立视频展示的小窗口
        self.video_label = tk.Label(self.image_frame)
        self.video_label.grid(row=0, column=0, columnspan=2, sticky="nsew", padx=1, pady=1)
        if self.video_enable:
            # 用OpenCV尝试打开视频
            self.cap = cv2.VideoCapture(self.url)
            # video_flag传给后面video_update函数,决定是否更新视频流
            self.video_flag = True
            # 提供了摄像头未连接的错误处理
            # 由于尝试连接的时间较长,所以python终端报Connection failed之后一会儿才会弹出下面错误窗口
            if not self.cap.isOpened():
                if messagebox.askyesno("错误","无法连接摄像头,是否重试?"):
                    self.stop_event.set()
                    # 重试,retry_event置True,传给main.py循环
                    self.retry_event = True
                    self.cap.release()
                    self.root.destroy()
                else:
                    # 不想重试,直接不开摄像头
                    self.video_label.config(text="摄像头未连接" , font=('Helvetica', 12), bg='white')
                    self.video_flag = False
        else:
            self.video_label.config(text="摄像头未连接" , font=('Helvetica', 12), bg='white')
            self.video_flag = False

    # 蓝牙连接与错误处理
    def serial_init(self):
        # 创建数组,日后三个传感器要分别保存十位数据
        # self.serial_data1 = ['0'] * 10
        self.serial_data2 = [0] * 20
        self.serial_data3 = [0] * 20
        # 创建蓝牙展示窗口
        # 因为本项目第一个传感器-浊度传感器不工作,所以直接注释掉第一条,以下类似处理
        # self.serial_data_label1 = tk.Label(self.image_frame, text="等待蓝牙数据...", font=('Helvetica', 12), bg='white')
        # self.serial_data_label1.grid(row=0, column=3, sticky="nsew", padx=1, pady=1)
        # 取而代之的是一个版本信息展示框
        self.serial_data_label0 = tk.Label(self.image_frame, font=('Helvetica', 12), bg='white',
                                           text=f"综合监视器ver2.2\nFPS={FPS}\n波特率={baud_rate}\n测量数据:"
                                           +data_label[1]+','+data_label[2])
        self.serial_data_label0.grid(row=0, column=2, columnspan=2, sticky='nsew', padx=1, pady=1)
        self.serial_data_label2 = tk.Label(self.image_frame, text="等待蓝牙数据...", font=('Helvetica', 12), bg='white')
        self.serial_data_label2.grid(row=1, column=0, sticky="nsew", padx=1, pady=1)
        self.serial_data_label3 = tk.Label(self.image_frame, text="等待蓝牙数据...", font=('Helvetica', 12), bg='white')
        self.serial_data_label3.grid(row=1, column=2, sticky="nsew", padx=1, pady=1)
        # self.serial_data_label.pack(side=tk.LEFT, fill=tk.Y, padx=1, pady=1)
        if self.serial_enable:
            # 尝试连接蓝牙
            try:
                self.serial = serial.Serial(self.port, baud_rate, timeout=1)
                # serial_flag传给后面serial_update函数,决定是否更新蓝牙
                self.serial_flag = True
                # 无法连接的错误处理,逻辑同video_init函数
            except serial.SerialException:
                if messagebox.askyesno("错误","无法连接蓝牙,是否重试?"):
                    self.stop_event.set()
                    self.retry_event = True
                    if self.video_flag:
                        self.cap.release()
                    self.root.destroy()
                else:
                    # self.serial_data_label1.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
                    self.serial_data_label2.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
                    self.serial_data_label3.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
                    self.serial_flag = False
        else:
            # self.serial_data_label1.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
            self.serial_data_label2.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
            self.serial_data_label3.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
            self.serial_flag = False

    # 按钮创建
    def button_init(self):
        # 当视频流打开的时候才显示按钮
        if self.video_flag:
            video_button1 = ttk.Button(self.toolbar, text="保存图片", command=self.button_save_image)
            video_button1.pack(side=tk.LEFT, padx=(25, 0))

            video_button2 = ttk.Button(self.toolbar, text="中断线程", command=self.stop_event.set)
            video_button2.pack(side=tk.LEFT, padx=(25, 0))

            video_button3 = ttk.Button(self.toolbar, text="视频窗口", command=self.only_video)
            video_button3.pack(side=tk.LEFT, padx=(25, 0))
    
    # 由于视频逐帧转为图片太慢,开个窗口低延时监控
    def only_video(self):
        while True:
            ret, frame = self.cap.read()
            if ret:
                cv2.imshow("video-only window, stops with 'q' & exits with 'x'",frame)
            self.only_video_choice = True
            if cv2.waitKey(1) == ord('q'):  
                self.only_video_choice = False
                break

    # GUI线程创建
    def setup_ui(self):
        # "x"绑定on_closing函数,提供关闭窗口
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

        # 创建两个队列,用于从两个update函数传值给两个check函数
        # 为什么不在update函数直接执行更新:GUI界面的更新不在主线程中执行就会出bug
        self.video_queue = queue.Queue()
        self.serial_queue = queue.Queue()

        # 创建两个线程,分别绑定两个update函数
        # 但由于不是主线程,所以只能实现实时获取数据
        # 在GUI界面更新得用两个check函数
        self.thread = threading.Thread(target=self.all_update)
        self.thread.start()

        # 执行获取数据的线程后进行GUI界面的更新:主线程
        self.root.after(10, self.check_all_queue)

    # GUI界面更新的总函数
    # 原理都是从queue中获取信息再处理
    def check_all_queue(self):
        if not self.stop_event.is_set():
            if not self.only_video_choice:
                self.check_video_queue()
            self.check_serial_queue_pre()
            # self.check_serial_queue1()
            self.check_serial_queue2()
            self.check_serial_queue3()
            # 循环,直到窗口关闭
            self.root.after(int(1000/FPS), self.check_all_queue)

    # 视频展示框更新
    def check_video_queue(self):
        # 从queue中读到video_update函数传递的frame变量(由cap.read获得)
        # 由于想展示到大窗口的小展示框里,所以必须转成image
        # 此逻辑影响了视频更新效率,如果不在意分窗口的话可以直接cv2.imshow
        if not self.video_queue.empty():
            self.frame = self.video_queue.get()
            self.frame = cv2.resize(self.frame, frame_size)
            self.frame = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
            image = Image.fromarray(self.frame)
            # 注意这里一定要用类的成员(或者可能有别的手段)保存image!
            # 否则python会将image当垃圾回收,视频无法显示
            self.photo = ImageTk.PhotoImage(image=image)
            self.video_label.config(image=self.photo)

    # 先将queue里的数据分解为三个,传给三个check函数
    def check_serial_queue_pre(self):
        if not self.serial_queue.empty():
            self.data = str(self.serial_queue.get())
            newdata = self.data.split(symbol[0])[-1]
            newdata = newdata.split(symbol[2])[0]
            # 自动保存数据到桌面文件的函数
            self.auto_save_data(newdata)
            # print(self.data)
            # self.data1 = self.data.split(symbol,1)[0]
            self.data2 = int(newdata.split(symbol[1],1)[0])/100
            self.data3 = int(newdata.split(symbol[1],1)[1])/100
            # print(self.data1,self.data2,self.data3)
        else:
            # self.data1 = 0
            self.data2 = 0
            self.data3 = 0
            
    # 蓝牙展示框更新1:浊度传感器(本项目中用了三个传感器)-已取消
    # def check_serial_queue1(self):
    #     if self.data1:
    #         self.serial_data1.append(self.data1)
    #         # 维持10个数据
    #         if len(self.serial_data1) > 10:
    #             self.serial_data1.pop(0)
    #         # 一口气展示十条数据
    #         # print(self.serial_data1)
    #         display_text = "\n".join(self.serial_data1)
    #         self.serial_data_label1.config(text=data_label[0]+"\n"+display_text)
    #     self.draw_serial_figure1()

    def check_serial_queue2(self):
        if self.data2:
            self.serial_data2.append(self.data2)
            if len(self.serial_data2) > 20:
                self.serial_data2.pop(0)
            self.serial_data_label2.config(text=data_label[1]+"\n"+str(self.serial_data2[10])+"\n"+str(self.serial_data2[11])
                                           +"\n"+str(self.serial_data2[12])+"\n"+str(self.serial_data2[13])
                                           +"\n"+str(self.serial_data2[14])+"\n"+str(self.serial_data2[15])
                                           +"\n"+str(self.serial_data2[16])+"\n"+str(self.serial_data2[17])
                                           +"\n"+str(self.serial_data2[18])+"\n"+str(self.serial_data2[19]))
        self.draw_serial_figure2()
    
    def check_serial_queue3(self):
        # 因为本人第三个传感器用的是TDS,在空气中读数为0,所以只能用self.data2而非self.data3判定信息有无
        # 但self.data2和self.data3应该是一起传的,在将data put到queue里的函数和将data分解为data2,data3的函数中应该能检测异常
        if self.data2:
            self.serial_data3.append(self.data3)
            if len(self.serial_data3) > 20:
                self.serial_data3.pop(0)
            self.serial_data_label3.config(text=data_label[2]+"\n"+str(self.serial_data3[10])+"\n"+str(self.serial_data3[11])
                                           +"\n"+str(self.serial_data3[12])+"\n"+str(self.serial_data3[13])
                                           +"\n"+str(self.serial_data3[14])+"\n"+str(self.serial_data3[15])
                                           +"\n"+str(self.serial_data3[16])+"\n"+str(self.serial_data3[17])
                                           +"\n"+str(self.serial_data3[18])+"\n"+str(self.serial_data3[19]))
        self.draw_serial_figure3()

    # 蓝牙数据绘图
    # def draw_serial_figure1(self):
    #     serial_figure = Figure(figsize=figsize, dpi=100)
    #     serial_subplot = serial_figure.add_subplot()
    #     x = [1,2,3,4,5,6,7,8,9,10]
    #     serial_subplot.plot(x, self.serial_data1, label=figure_legend[0])
    #     serial_subplot.legend()
    #     canvas = FigureCanvasTkAgg(serial_figure, master=self.image_frame)  
    #     canvas.get_tk_widget().grid(row=0, column=3)
    #     canvas.draw()

    def draw_serial_figure2(self):
        serial_figure = Figure(figsize=figsize, dpi=100)
        serial_subplot = serial_figure.add_subplot()
        x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
        serial_subplot.plot(x, self.serial_data2, label=figure_legend[1])
        serial_subplot.legend()
        canvas = FigureCanvasTkAgg(serial_figure, master=self.image_frame)  
        canvas.get_tk_widget().grid(row=1, column=1)
        canvas.draw()

    def draw_serial_figure3(self):
        serial_figure = Figure(figsize=figsize, dpi=100)
        serial_subplot = serial_figure.add_subplot()
        x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
        serial_subplot.plot(x, self.serial_data3 ,label=figure_legend[2])
        serial_subplot.legend()
        canvas = FigureCanvasTkAgg(serial_figure, master=self.image_frame)  
        canvas.get_tk_widget().grid(row=1, column=3)
        canvas.draw()

    # 使用同一线程更新两类数据
    def all_update(self):
        while not self.stop_event.is_set():
            self.video_update()
            self.serial_update()
            # 一定要停一段时间,不然queue会被frame塞满,视频压根打不开!
            time.sleep(1/FPS)

    # 视频流实时获取
    # 将前面cv2.VideoCapture获取的cap进行read,获取单帧的frame
    def video_update(self):
        # 如果前面连接失败后选择不重连,则取消更新
        if self.video_flag and not self.only_video_choice:
            # if self.stop_event.is_set():
            #     break
            ret, frame = self.cap.read()
            if not ret:
                self.video_label.config(text="摄像头断开连接")
                self.video_flag = False
                # break
            self.video_queue.put(frame)
            # frame = cv2.resize(frame, global_data.frame_size)
            # frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            # image = Image.fromarray(frame)
            # self.photo = ImageTk.PhotoImage(image=image)
            # self.video_label.config(image=self.photo)
    # 蓝牙数据实时获取
    def serial_update(self):
        if self.serial_flag:
            # flag作用与上面video相同
            # if self.stop_event.is_set():
            #     break
                # 数据解码,默认utf-8格式
            if self.serial.in_waiting > 0:
                data = self.serial.read_all().decode('utf-8').rstrip()
                if data != '' and data.startswith(symbol[0]) and data.endswith(symbol[2]):
                    self.serial_queue.put(data)
                time.sleep(1/FPS)
        # global count
        # if count %2 == 1:
        #     self.serial_queue.put("#114/514*")
        # else:
        #     self.serial_queue.put("#1919/810*")
        # count += 1
        # print(count)
        # a = "1/1/4"
        # self.serial_queue.put(a)
                # self.serial_data.append(data)
                # if len(self.serial_data) > 10:
                #     self.serial_data.pop(0)
                # self.auto_save_data(data)
                # display_text = "\n".join(self.serial_data)  
                # self.serial_label.config(text=display_text)

    # 保存图片的按钮关联的:手动保存图片函数
    def button_save_image(self):
        if self.frame is not None:
            #将图片保存至桌面
            base_filename = "image.png"
            desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", base_filename)
            counter = 1

            #若图片已存在,则命名后加(1)(2),etc.,类似电脑自身的方式
            while os.path.exists(desktop_path):
                new_filename = f"{os.path.splitext(base_filename)[0]}({counter}){os.path.splitext(base_filename)[1]}"
                desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", new_filename)
                counter += 1

            frame = cv2.cvtColor(self.frame, cv2.COLOR_RGB2BGR)
            cv2.imwrite(desktop_path, frame)
            messagebox.showinfo("保存成功", f"图片已保存到 {desktop_path}")

    # 蓝牙展示框更新函数中的:自动保存蓝牙数据函数
    # 将queue中读取的data传给函数
    def auto_save_data(self, data):
        filename = "bluetooth_data.txt"
        desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", filename)
        # 狠狠写入文件
        if data:
            with open(desktop_path, 'a') as file:
                file.write(data +'\n')

    # 关闭窗口
    # 提供是否关闭的选择
    def on_closing(self):
        if messagebox.askyesno("关闭窗口", "是否关闭?"):
            self.stop_event.set()
            if self.video_enable:
                self.cap.release()
            self.root.destroy()

效果

已连接

(用的是上个版本,没有单独视频窗口的按键,plot曲线y轴数据也没有从str转为float导致数值大小错位,但大晚上不好打扰舍友睡觉,算了大伙意会一下qwq)

未连接

main主函数

解读

这里还有上面monitor.__init__里面的奇怪逻辑,就是对付各种“重试”“关闭”的异常处理;

以下主要是实现“retry”的循环功能,其他相当于input和monitor的拼接。

具体实现效果已在前两部分展示。

代码

# main.py
# 主函数
# 一个简单的单片机项目附带的监视器GUI软件
# 其中esp32(或其他摄像头)通过WiFi与PC相连传输视频流
# 传感器的数据通过arduino(或其他单片机)连接的蓝牙传给PC
# 并展示数据与自动绘制的图像
# 在同一窗口分四个窗格实时更新并可保存

import input
import monitor

# 创建窗口,获得视频流URL格式(或者OpenCV可处理的格式都行)和蓝牙串口名
input1 = input.input()
input1.root.mainloop()
# input窗口正常运行,用户没有选择中途关闭
# input.py中将signal置True,相当于后续操作的使能信号
if input.signal:
    # 实现最主要的功能,展示视频和蓝牙数据
    monitor1 = monitor.monitor(input.url, input.port, input.video_enable, input.serial_enable)
    monitor1.root.mainloop()
    # 特殊处理中涉及重试,跳回输入窗口,用retry_event创建循环
    while monitor1.retry_event:
        input1 = input.input()
        input1.root.mainloop()
        if input.signal:
            monitor1 = monitor.monitor(input.url, input.port, input.video_enable, input.serial_enable)
            monitor1.root.mainloop()

tips

如果想要将程序打包成可执行文件,实现真正可以小小炫耀一下的GUI软件功能,操作流程如下:

确认已经下载了Pyinstaller,如果没有则可以:

pip install Pyinstaller

接着有两种选择:

简简单单生成一个默认图标的可执行文件:

Pyinstaller -F -w main.py

或者如果有想要带有的特殊图标,则可以将图标的ico格式文件拖入mian.py所在文件夹中(假设命名为aaa.ico,且推荐这么干),然后:

Pyinstaller -F -w -i aaa.ico main.py

就能实现想要的图标力(喜)压力马斯内(图穷匕见)

示例

(声明:以下图标版权属于米哈游,作者不过是崩铁魔怔人,如有侵权请联系修改)

文件夹内:

生成的exe文件:

  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用PythonTkinter库可以很轻松地通过OpenCV库来截取摄像头的图像。首先,需要安装和导入Tkinter和OpenCV库: ```python import tkinter as tk import cv2 ``` 然后,创建一个窗口并在其中显示摄像头的图像: ```python # 创建窗口 window = tk.Tk() window.title("摄像头截图") # 创建Label用于显示图像 image_label = tk.Label(window) image_label.pack() # 创建函数来读取并显示摄像头图像 def show_frame(): # 从摄像头读取图像帧 ret, frame = cap.read() if ret: # 将图像帧转换为Tkinter中可用的格式 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(frame) imgtk = ImageTk.PhotoImage(image=img) image_label.imgtk = imgtk # 更新图像标签的图像 image_label.configure(image=imgtk) # 每隔10毫秒更新一次图像 window.after(10, show_frame) # 打开摄像头 cap = cv2.VideoCapture(0) # 开始实时显示截图 show_frame() # 运行窗口循环 window.mainloop() ``` 以上代码将会创建一个窗口,该窗口将实时显示摄像头的图像。每隔10毫秒更新一次图像。可以通过按下“Ctrl+C”停止程序。 要截取图像,可以在`show_frame()`函数里添加保存图像的代码,例如: ```python # 创建函数来读取并显示摄像头图像 def show_frame(): # 从摄像头读取图像帧 ret, frame = cap.read() if ret: # 将图像帧转换为Tkinter中可用的格式 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(frame) imgtk = ImageTk.PhotoImage(image=img) image_label.imgtk = imgtk # 更新图像标签的图像 image_label.configure(image=imgtk) # 保存图像 cv2.imwrite("screenshot.jpg", frame) ``` 这将在程序运行期间实时保存摄像头图像为"screenshot.jpg"文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值