我目前正在为我的DJI Tello开发Tkinter GUI,我正在努力使其在命令无人机起飞/降落时,GUI上的流式视频不会冻结。我对多线程不太熟悉,但我查了一下这个问题,似乎我不是唯一一个遇到这个问题的人。因此,我使用了我发现的关于线程和启动线程的内容,最终得到了这行(或多或少(:
forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=threading.Thread(target=lambda: takeoff_land(flydo)).start)
现在,当我按下按钮时,无人机起飞,视频不再冻结。然而,当我再次点击它时,代码会抛出一个错误:
RuntimeError: threads can only be started once
但我希望我的按钮能够让无人机在降落时起飞,然后在飞行时降落。有什么办法我能做到吗?
以下是到目前为止我所拥有的(在takeff_land((函数中,我设置了一些测试代码来代替实际的命令。基本上,我希望它能够在之后开始打印0、1、2……即使它已经在打印了。(大部分只是GUI的东西,但我不想省略任何会破坏代码的东西。
import cv2
import threading
from djitellopy import tello
from tkinter import *
from PIL import Image, ImageTk
import time
def takeoff_land(flydo):
'''Flydo takes off if not flying, lands if flying.'''
global flying
if flying:
for i in range(10):
print(i)
time.sleep(1)
# flydo.land()
flying = False
else:
for i in range(10):
print(i)
time.sleep(1)
# flydo.takeoff()
flying = True
def run_app(HEIGHT=800, WIDTH=800):
root = Tk()
flydo = tello.Tello()
flydo.connect()
flydo.streamon()
global flying
flying = False # To toggle between takeoff and landing for button
canvas = Canvas(root, height=HEIGHT, width=WIDTH)
# For background image
bg_dir = "C:\Users\charl\Desktop\flydo\Tacit.jpg"
img = Image.open(bg_dir).resize((WIDTH, HEIGHT))
bg_label = Label(root)
bg_label.img = ImageTk.PhotoImage(img)
bg_label["image"] = bg_label.img
bg_label.place(x=0, y=0, relwidth=1, relheight=1)
# Display current battery
battery = Label(text=f"Battery: {int(flydo.get_battery())}%", font=("Verdana", 18), bg="#95dff3")
bat_width = 200
bat_height = 50
battery.config(width=bat_width, height=bat_height)
battery.place(x=(WIDTH - bat_width - 0.1*HEIGHT + bat_height), rely=0.9, relwidth=bat_width/WIDTH, relheight=bat_height/HEIGHT)
# Takeoff/Land button
forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=threading.Thread(target=lambda: takeoff_land(flydo)).start)
if threading.Thread(target=lambda: takeoff_land(flydo)).is_alive():
threading.Thread(target=lambda: takeoff_land(flydo)).join() # This doesn't kill the thread the way I want it to...
fb_width = 200
fb_height = 100
forward_button.config(width=fb_width, height=fb_height)
forward_button.place(x=(WIDTH/2 - fb_width/2), rely=0.61, relwidth=fb_width/WIDTH, relheight=fb_height/HEIGHT)
cap_label = Label(root)
cap_label.pack()
def video_stream():
h = 480
w = 720
frame = flydo.get_frame_read().frame
frame = cv2.resize(frame, (w, h))
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
imgtk = ImageTk.PhotoImage(image=img)
cap_label.place(x=WIDTH/2 - w/2, y=0)
cap_label.imgtk = imgtk
cap_label.configure(image=imgtk)
cap_label.after(5, video_stream)
video_stream()
canvas.pack()
root.mainloop()
if __name__ == "__main__":
HEIGHT = 800
WIDTH = 800
run_app(HEIGHT, WIDTH)
好的,所以我今天早上实际上根据这篇文章找到了解决方案:
https://bhaveshsingh0124.medium.com/multi-threading-on-python-tkinter-button-f0d9f759ad3e
从本质上讲,我所要做的是在我用按钮调用的函数中线程Tello命令,而不是函数本身。由于无人机只能降落或起飞,每次调用这两个命令中的一个时,它都可以创建一个新线程。这是固定代码:
import cv2
import threading
from djitellopy import tello
from tkinter import *
from PIL import Image, ImageTk # You have to import this last or else Image.open throws an error
import time
def dummy_tello_fn():
for i in range(3):
print(i)
time.sleep(1)
def takeoff_land(flydo):
'''Flydo takes off if not flying, lands if flying.'''
global flying
if flying:
# threading.Thread(target=lambda: dummy_tello_fn()).start()
threading.Thread(target=lambda: flydo.land()).start()
flying = False
else:
# threading.Thread(target=lambda: dummy_tello_fn()).start()
threading.Thread(target=lambda: flydo.takeoff()).start()
flying = True
def run_app(HEIGHT=800, WIDTH=800):
root = Tk()
flydo = tello.Tello()
flydo.connect()
flydo.streamon()
global flying
flying = False # To toggle between takeoff and landing for button
canvas = Canvas(root, height=HEIGHT, width=WIDTH)
# For background image
bg_dir = "C:\Users\charl\Desktop\flydo\Tacit.jpg"
img = Image.open(bg_dir).resize((WIDTH, HEIGHT))
bg_label = Label(root)
bg_label.img = ImageTk.PhotoImage(img)
bg_label["image"] = bg_label.img
bg_label.place(x=0, y=0, relwidth=1, relheight=1)
# Display current battery
battery = Label(text=f"Battery: {int(flydo.get_battery())}%", font=("Verdana", 18), bg="#95dff3")
bat_width = 200
bat_height = 50
battery.config(width=bat_width, height=bat_height)
battery.place(x=(WIDTH - bat_width - 0.1*HEIGHT + bat_height), rely=0.9, relwidth=bat_width/WIDTH, relheight=bat_height/HEIGHT)
# Takeoff/Land button
forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=lambda: takeoff_land(flydo))
fb_width = 200
fb_height = 100
forward_button.config(width=fb_width, height=fb_height)
forward_button.place(x=(WIDTH/2 - fb_width/2), rely=0.61, relwidth=fb_width/WIDTH, relheight=fb_height/HEIGHT)
cap_label = Label(root)
cap_label.pack()
def video_stream():
h = 480
w = 720
frame = flydo.get_frame_read().frame
frame = cv2.resize(frame, (w, h))
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
imgtk = ImageTk.PhotoImage(image=img)
cap_label.place(x=WIDTH/2 - w/2, y=0)
cap_label.imgtk = imgtk
cap_label.configure(image=imgtk)
cap_label.after(5, video_stream)
video_stream()
canvas.pack()
root.mainloop()
if __name__ == "__main__":
HEIGHT = 800
WIDTH = 800
run_app(HEIGHT, WIDTH)