""" DroneVisionGUI is a new class that parallels DroneVision but with several important changes. 1) This module uses VLC instead of FFMPEG 2) This module opens a GUI window to show you the video in real-time (you could watch it in real-time previously through the VisionServer) 3) Because GUI windows are different on different OS's (and in particular OS X behaves differently than linux and windows) and because they want to run in the main program thread, the way your program runs is different. You first open the GUI and then you have the GUI spawn a thread to run your program. 4) This module can use a virtual disk in memory to save the images, thus shortening the time delay for the camera for your programs. Author: Amy McGovern, dramymcgovern@gmail.com Some of the LIBVLC code comes from Author: Valentin Benke, valentin.benke@aon.at """ import inspect import sys import tempfile import time from functools import partial from os.path import join import cv2 import pyparrot.utils.vlc as vlc from pyparrot.Model import Model from PyQt5.QtCore import Qt, QThread, QTimer from PyQt5.QtGui import QColor, QPalette, QPixmap from PyQt5.QtWidgets import (QAction, QApplication, QFileDialog, QFrame, QHBoxLayout, QLabel, QMainWindow, QPushButton, QSlider, QVBoxLayout, QWidget) class Player(QMainWindow): """ Modification of the simple Media Player using VLC and Qt to show the mambo stream The window part of this example was modified from the QT example cited below. VLC requires windows to create and show the video and this was a cross-platform solution. VLC will automatically create the windows in linux but not on the mac. Amy McGovern, dramymcgovern@gmail.com Qt example for VLC Python bindings https://github.com/devos50/vlc-pyqt5-example Copyright (C) 2009-2010 the VideoLAN team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. """ def __init__(self, vlc_player, drone_gui): """ Create a UI window for the VLC player :param vlc_player: the VLC player (created outside the function) """ QMainWindow.__init__(self) self.setWindowTitle("VLC Drone Video Player") # save the media player self.mediaplayer = vlc_player # need a reference to the main drone vision class self.drone_vision = drone_gui # create the GUI self.createUI() def createUI(self): """ Set up the window for the VLC viewer """ self.widget = QWidget(self) self.setCentralWidget(self.widget) # In this widget, the video will be drawn if sys.platform == "darwin": # for MacOS from PyQt5.QtWidgets import QMacCocoaViewContainer self.videoframe = QMacCocoaViewContainer(0) else: self.videoframe = QFrame() self.palette = self.videoframe.palette() self.palette.setColor (QPalette.Window, QColor(0,0,0)) self.videoframe.setPalette(self.palette) self.videoframe.setAutoFillBackground(True) self.hbuttonbox = QHBoxLayout() self.playbutton = QPushButton("Run my program") self.hbuttonbox.addWidget(self.playbutton) self.playbutton.clicked.connect(partial(self.drone_vision.run_user_code, self.playbutton)) self.landbutton = QPushButton("Land NOW") self.hbuttonbox.addWidget(self.landbutton) self.landbutton.clicked.connect(self.drone_vision.land) self.stopbutton = QPushButton("Quit") self.hbuttonbox.addWidget(self.stopbutton) self.stopbutton.clicked.connect(self.drone_vision.close_exit) self.vboxlayout = QVBoxLayout() self.vboxlayout.addWidget(self.videoframe) if (self.drone_vision.user_draw_window_fn is not None): self.userWindow = QLabel() fullPath = inspect.getfile(DroneVisionGUI) shortPathIndex = fullPath.rfind("/") if (shortPathIndex == -1): # handle Windows paths shortPathIndex = fullPath.rfind("\\") print(shortPathIndex) shortPath = fullPath[0:shortPathIndex] pixmap = QPixmap('%s/demo_user_image.png' % shortPath) print(pixmap) print(pixmap.isNull()) self.userWindow.setPixmap(pixmap) self.vboxlayout.addWidget(self.userWindow) self.vboxlayout.addLayout(self.hbuttonbox) self.widget.setLayout(self.vboxlayout) # the media player has to be 'connected' to the QFrame # (otherwise a video would be displayed in it's own window) # this is platform specific! # you have to give the id of the QFrame (or similar object) to # vlc, different platforms have different functions for this if sys.platform.startswith('linux'): # for Linux using the X Server self.mediaplayer.set_xwindow(self.videoframe.winId()) elif sys.platform == "win32": # for Windows self.mediaplayer.set_hwnd(self.videoframe.winId()) elif sys.platform == "darwin": # for MacOS self.mediaplayer.set_nsobject(int(self.videoframe.winId())) class UserVisionProcessingThread(QThread): def __init__(self, user_vision_function, user_args, drone_vision): """ :param user_vision_function: user callback function to handle vision :param user_args: optional arguments to the user callback function """ QThread.__init__(self) self.user_vision_function = user_vision_function self.user_args = user_args self.drone_vision = drone_vision def __del__(self): self.wait() def run(self): print("user callback being called") while (self.drone_vision.vision_running): self.user_vision_function(self.user_args) # put the thread back to sleep for fps # sleeping shorter to ensure we stay caught up on frames time.sleep(1.0 / (3.0 * self.drone_vision.fps)) # exit when the vision thread ends print("exiting user vision thread") self.terminate() class UserWindowDrawThread(QThread): def __init__(self, user_draw_function, drone_vision): """ :param user_draw_function: user drawing function that should return an image """ QThread.__init__(self) self.user_draw_function = user_draw_function self.drone_vision = drone_vision def __del__(self): self.wait() def run(self): #print("user window draw thread being called") while (self.drone_vision.vision_running): img = self.user_draw_function() if(img is not None): if (not img.isNull()): self.drone_vision.vlc_gui.userWindow.setPixmap(QPixmap.fromImage(img)) # put the thread back to sleep for fps # sleeping shorter to ensure we stay caught up on frames time.sleep(1.0 / (3.0 * self.drone_vision.fps)) # exit when the vision thread ends print("exiting user window draw thread") self.terminate() class UserCodeToRun(QThread): def __init__(self, user_function, user_args, drone_vision): """ :param user_function: user code to run (presumably flies the drone) :param user_args: optional arguments to the user function """ QThread.__init__(self) self.user_vision_function = user_function self.user_args = user_args self.drone_vision = drone_vision def __del__(self): self.wait() def run(self): self.user_vision_function(self.drone_vision, self.user_args) class DroneVisionGUI: def __init__(self, drone_object, model, user_code_to_run, user_args, buffer_size=200, network_caching=200, fps=20, user_draw_window_fn=None): """ Setup your vision object and initialize your buffers. You won't start seeing pictures until you call open_video. :param drone_object reference to the drone (mambo or bebop) object :param is_bebop: True if it is a bebop and false if it is a mambo :param user_code_to_run: user code to run with the run button (remember this is needed due to the GUI taking the thread) :param user_args: arguments to the user code :param buffer_size: number of frames to buffer in memory. Defaults to 10. :param network_caching: buffering time in milli-seconds, 200 should be enough, 150 works on some devices (Mac OS X ignores this argument) :param fps: frame rate for the vision :param user_window: set to a function to be called to draw a QImage and None otherwise (default None) """ self.fps = fps self.vision_interval = int(1000 * 1.0 / self.fps) self.buffer_size = buffer_size self.drone_object = drone_object self.model = model # initialize a buffer (will contain the last buffer_size vision objects) self.buffer = [None] * buffer_size self.buffer_size = buffer_size self.buffer_index = 0 # vision threading is done from a QTimer instead of a separate thread self.new_frame = False self.vision_running = True # the vision thread starts opencv on these files. That will happen inside the other thread # so here we just sent the image index to 1 ( to start) self.image_index = 1 # save the caching parameters and choice of libvlc self.network_caching = network_caching # save the user function and args for calling from the run button self.user_code_to_run = user_code_to_run self.user_args = user_args self.user_thread = UserCodeToRun(user_code_to_run, user_args, self) # if we are drawing a special user window self.user_draw_window_fn = user_draw_window_fn if (self.user_draw_window_fn is not None): self.user_window_draw_thread = UserWindowDrawThread(self.user_draw_window_fn, self) else: self.user_window_draw_thread = None # in case we never setup a user callback function self.user_vision_thread = None # has the land button been clicked - saved in case the user needs it in their code self.land_button_clicked = False def run_user_code(self, button): """ Start the thread to run the user code :return: """ button.setEnabled(False) self.user_thread.start() def set_user_callback_function(self, user_callback_function=None, user_callback_args=None): """ Set the (optional) user callback function for handling the new vision frames. This is run in a separate thread that starts when you start the vision buffering :param user_callback_function: function :param user_callback_args: arguments to the function :return: """ self.user_vision_thread = UserVisionProcessingThread(user_callback_function, user_callback_args, self) def open_video(self): """ Open the video stream using vlc. Note that this version is blocking meaning this function will NEVER return. If you want to run your own code and not just watch the video, be sure you set your user code in the constructor! Remember that this will only work if you have connected to the wifi for your mambo! :return never returns due to QT running in the main loop by requirement """ # start the stream on the bebop if self.model is Model.BEBOP: self.drone_object.start_video_stream() # we have bypassed the old opencv VideoCapture method because it was unreliable for rtsp # get the path for the config files fullPath = inspect.getfile(DroneVisionGUI) shortPathIndex = fullPath.rfind("/") if (shortPathIndex == -1): # handle Windows paths shortPathIndex = fullPath.rfind("\\") print(shortPathIndex) shortPath = fullPath[0:shortPathIndex] self.imagePath = join(shortPath, "images") self.utilPath = join(shortPath, "utils") print(self.imagePath) print(self.utilPath) if self.model is Model.BEBOP: # generate the streaming-address for the Bebop self.utilPath = join(shortPath, "utils") self.stream_addr = "%s/bebop.sdp" % self.utilPath elif self.model is Model.MAMBO: # generate the streaming-address for the Mambo self.stream_addr = "rtsp://192.168.99.1/media/stream2" elif self.model is Model.ANAFI: self.stream_addr = "rtsp://192.168.42.1/live" # initialise the vlc-player with the network-caching self.player = vlc.MediaPlayer(self.stream_addr, ":network-caching=" + str(self.network_caching)) # start the buffering success = self._start_video_buffering() def _start_video_buffering(self): """ If the video capture was successfully opened, then start the thread to buffer the stream :return: if using libvlc this will return whether or not the player started """ # open/draw the GUI app = QApplication(sys.argv) self.vlc_gui = Player(vlc_player=self.player, drone_gui=self) self.vlc_gui.show() self.vlc_gui.resize(640, 480) # ensure that closing the window closes vision app.aboutToQuit.connect(self.land_close_exit) if (self.user_vision_thread is not None): print("Starting user vision thread") self.user_vision_thread.start() if (self.user_draw_window_fn is not None): print("Starting user drawing thread") self.user_window_draw_thread.start() # setup the timer for snapshots self.timer = QTimer(self.vlc_gui) self.timer.setInterval(self.vision_interval) self.timer.timeout.connect(self._buffer_vision) self.timer.start() # show the stream success = self.player.play() print("success from play call is %s " % success) # start the GUI loop app.exec() def _buffer_vision(self): """ Internal method to save valid video captures from the camera fps times a second :return: """ # start with no new data self.new_frame = False # run forever, trying to grab the latest image if (self.vision_running): # generate a temporary file, gets deleted after usage automatically #self.file = tempfile.NamedTemporaryFile(dir=self.imagePath) self.file = join(self.imagePath, "visionStream.jpg") #self.file = tempfile.SpooledTemporaryFile(max_size=32768) # save the current picture from the stream self.player.video_take_snapshot(0, self.file, 0, 0) # read the picture into opencv img = cv2.imread(self.file) # sometimes cv2 returns a None object so skip putting those in the array if (img is not None): # got a new image, save it to the buffer directly self.buffer_index += 1 self.buffer_index %= self.buffer_size #print video_frame self.buffer[self.buffer_index] = img self.new_frame = True def get_latest_valid_picture(self): """ Return the latest valid image (from the buffer) :return: last valid image received from the Mambo """ return self.buffer[self.buffer_index] def close_exit(self): """ Land, close the video, and exit the GUI :return: """ self.close_video() self.vlc_gui.close() self.vlc_gui.destroy() # kill the threads if (self.user_window_draw_thread is not None): self.user_window_draw_thread.quit() self.user_vision_thread.quit() self.user_thread.quit() # this is hanging on Mac OS X when it tries to exit and I'm not sure why. The threads are properly # exiting sys.exit() def land_close_exit(self): """ Called if you Quit the GUI: lands the drone, stops vision, and exits the GUI :return: """ self.land() self.close_exit() def land(self): """ Send the land command over the emergency channel when the user pushes the button :return: """ # tell the user that the land button was clicked self.land_button_clicked = True # land the drone if self.model is Model.BEBOP: if (not self.drone_object.is_landed()): self.drone_object.emergency_land() else: if (not self.drone_object.is_landed()): self.drone_object.safe_land(5) def close_video(self): """ Stop the vision processing and all its helper threads """ # the helper threads look for this variable to be true self.vision_running = False self.player.stop() # send the command to kill the vision stream (bebop only) if self.model is Model.BEBOP: self.drone_object.stop_video_stream()