Hailo guide: Basic RPi 5 GUI using DeGirum, Picamera2 and PyQt5

Hello folks, i had promised to post this a while back, but i got a bit swamped with work; hopefully this will give someone at least a few basic ideas as to what they can do to show off the use of AI Vision with degirum a bit more.

Firstly lets start with the setup, i am using a raspberry pi 5, with the 8gb and a Hailo8L configuration, make sure to install all the degirum and hailo bits necessary for proper function, picamera2 should be installed by default in your raspberry pi, you can check with a quick:

rpicam-hello 

You will also need to have pyqt5 installed, which you can do with sudo by:

sudo apt-get install python3-pyqt5

Lastly we need opencv for two lines of code so:

sudo apt-get install opencv-python

Onto the gui

Our imports will look something like

from PyQt5 import QtCore
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QDir
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QApplication, QLabel, QFileSystemModel, QMainWindow, QVBoxLayout, QWidget, QFileDialog, QInputDialog, QHBoxLayout, QPushButton, QMessageBox
from picamera2 import Picamera2
import numpy as np
import degirum as dg, degirum_tools
import cv2
import libcamera

That certainly seems like a lot of thing we wont be using for this tutorial, but if you want to take things further than the scope of this you will certainly need it, so maybe keep it around till then.

The body of the GUI will look something like this:

class AI_Vision(QWidget): #You can swap this to QMainWindow
	closed = pyqtSignal()
	def __init__(self):
		super().__init__()
		inference_host_address = '@local' # set to @local if you want to run inference on your local machine, or change if necessary 
		zoo_url = '/path/to/your/AI/model.json' 
		model_name = 'model'   #match to your models actual name
		self.model = dg.load_model(
			model_name=model_name,
			inference_host_address=inference_host_address,
			zoo_url=zoo_url,
			output_confidence_threshold = 0.3,  #Or whatever you choose as min threshold 
			overlay_font_scale = 2.5,           #I add this because the default font is a bit small 
			overlay_show_probabilities = True	#Also a preference since i enjoy seeing how confident the model is 	
			)# set model name, inference host address, zoo url, token, and image source 		
		self.picam2 = Picamera2()
		self.start_camera()   #Here we will set the basic configuration for our camera 
		self.load_gui()       #Here we will add all of our gui elements 

I am using classes here as i am more used to it, but you are welcome to just make a single file without any classes if you prefer that, its also worht mentioning that this body is made as to have special focus on those of us running local inference on the raspberry pi 5 with its AI hat.

Now lets talk about our gui and camera setup, as i believe here is where we could all take a very different approach and still have good results.

Firstly our camera:

	def start_camera(self):
		try:
			self.picam2.stop()  #in case anything else was running 
            #Here, i add the resolution and mode in a specific manner to avoid having the camera guess and use whatever it prefers
			picam2_pConfig = self.picam2.create_preview_configuration(main={"format": 'RGB888',"size": (1920, 1080)}, raw=self.picam2.sensor_modes[1])  #You can verify your camera modes using rpicam-hello --list-cameras 
			self.picam2.set_controls({"AeFlickerMode":libcamera.controls.AeFlickerModeEnum.Manual})  #Preference, makes it so that camera flicker is null 
			self.picam2.configure(picam2_pConfig)
			self.picam2.start()
			self.timer = QTimer(self)
			self.timer.timeout.connect(self.update_camera_view)  #The main function that will be writing every frame in our case 
			self.timer.start(1)  # Update the view in whatever time frame you choose		
		except Exception as e:
			print(e)
			pass 

Now our gui:

	def load_gui(self):
		layout_h = QHBoxLayout()
		layout_v = QVBoxLayout()
		self.label = QLabel(self)  #You need a label to add the image in pyqt5, you can use other methods, but this is my preferred
		self.label.setGeometry(0,0,1920,1080) #Make sure to match this resolution to your camera
		layout_h.addWidget(self.label)
		self.setWindowFlags(Qt.FramelessWindowHint) #You can remove both the flag and state if you prefer a regular floating window
		self.setWindowState(QtCore.Qt.WindowFullScreen)
		self.setLayout(layout_h)
		self.setStyleSheet("background-color: black;")  #Block this out if you arent using fullscreen either
		self.setContentsMargins(0,0,0,0) #Modify this only if you want the borders around your image to look wider 

The gui and its elements can be as diverse as customizable as you want them, you can add a QSlider to control something like brightness with picam2 and its integrated functions picam2.set_controls({"Brightness": self.QSlider.value()}), you can add a QPushButton and connect it to a function that captures pictures or takes a short video.

Onto the next part, which is our update view function, but first a quick explanation as to what you will see next, with the normal vision of the camera you could have a single function for all of it, but given the fact that degirum docs recommend a frame generator function i created a split funcion:

	def frame_gen(self):
		self.frame = self.picam2.capture_array() #Normally i would have this single line of code in the main camera view function as i mentioned
		yield self.frame

        
	def update_camera_view(self):
		for result in self.model.predict_batch(self.frame_gen()): #Remember to make sure you are matching your models name
			self.frame_drawn = cv2.resize(result.image_overlay, (1920, 1080), interpolation = cv2.INTER_AREA) #We make sure that the image is using the same resolution we are handling in the previous instances
			frame_rgb = cv2.cvtColor(self.frame_drawn, cv2.COLOR_BGR2RGB)  #Opencv handles the image, which means that the channels are swapped, so we make them standard 
			height, width, channel = frame_rgb.shape
			bytes_per_line = 3 * width
			q_image = QImage(frame_rgb.data, self.label.width(), self.label.height(), bytes_per_line, QImage.Format_RGB888) #This is how pyqt5 handles images 
			pixmap = QPixmap.fromImage(q_image)
			self.label.setPixmap(pixmap)  #We set the image in our label now
			self.label.setContentsMargins(0,0,0,0) #Another line of preference in this code, you can cut it without affecting anything

Now you can finally have an image in a gui showing off your model and raspberry pi capacity, but you can still add a bit more lines of code for proper handling or execution like a close event:

	def closeEvent(self, event):
        # Clean up 
		try:
			self.timer.stop()
			self.closed.emit()
			self.picam2.stop()
			self.picam2.close()
			event.accept()
		except Exception as e:
			self.picam2.stop()
			self.picam2.close()
			print(e)

and a if __name__ == "__main__": function to ensure proper running.

If you would like to use this in a kiosk or autostart manner i recommend launching the application or gui with a .sh file including the following line:

sudo /path/to/your/venv/python /path/to/your/app.py

This helps to avoid any Missing Degirum or other similar errors

This is my first time posting a tutorial or how to guide as long as this, so it may be a bit rambly, or i may not have explained things in depth enough, but if you have any questions i can try to help in the comments.

Thank you very much for taking the time to write and submit this @DAlexSanc! There’s no doubt that this guide will help others in our community.