How to Build GUI Barcode Reader with Qt PySide2 on Raspberry Pi

If you’re looking to build a cross-platform GUI app using Python and Qt, you have two primary options: PyQt and PySide. Both serve as Qt bindings for Python. Their primary distinction lies in their licensing: PyQt5 is available under the GPL or a commercial license, while PySide2 uses the LGPL. Given that PySide2 is officially recommended, I’ll be using it in conjunction with Dynamsoft’s Python Barcode SDK to craft a GUI barcode reader app for Raspberry Pi.

Prerequisites

  • OpenCV Python

    python3 -m pip install opencv-python
    
  • Dynamsoft Python Barcode SDK

    python3 -m pip install dbr
    
  • PySide2

    sudo apt-get install python3-pyside2.qt3dcore python3-pyside2.qt3dinput python3-pyside2.qt3dlogic python3-pyside2.qt3drender python3-pyside2.qtcharts python3-pyside2.qtconcurrent python3-pyside2.qtcore python3-pyside2.qtgui python3-pyside2.qthelp python3-pyside2.qtlocation python3-pyside2.qtmultimedia python3-pyside2.qtmultimediawidgets python3-pyside2.qtnetwork python3-pyside2.qtopengl python3-pyside2.qtpositioning python3-pyside2.qtprintsupport python3-pyside2.qtqml python3-pyside2.qtquick python3-pyside2.qtquickwidgets python3-pyside2.qtscript python3-pyside2.qtscripttools python3-pyside2.qtsensors python3-pyside2.qtsql python3-pyside2.qtsvg python3-pyside2.qttest python3-pyside2.qttexttospeech python3-pyside2.qtuitools python3-pyside2.qtwebchannel python3-pyside2.qtwebsockets python3-pyside2.qtwidgets python3-pyside2.qtx11extras python3-pyside2.qtxml python3-pyside2.qtxmlpatterns python3-pyside2uic
    

GUI Barcode Reader for Raspberry Pi OS

Let’s get started with the UI widgets:

  • A button for loading image files:

    self.btn = QPushButton("Load an image")
    self.btn.clicked.connect(self.pickFile)
    
    def pickFile(self):
        filename = QFileDialog.getOpenFileName(self, 'Open file',
                                                self._path, "Barcode images (*)")
        if filename is None or filename[0] == '':
            self.showMessageBox("No file selected")
            return
    
    
  • A button for opening the live camera stream:

    btnCamera = QPushButton("Open camera")
    btnCamera.clicked.connect(self.openCamera)
    
    self.timer = QTimer()
    self.timer.timeout.connect(self.nextFrameUpdate)
    
    def openCamera(self):
    
        if not self._cap.isOpened(): 
            self.showMessageBox("Failed to open camera.")
            return
    
        self.timer.start(1000./24)
    
  • A button for stopping the camera stream:

    btnCamera = QPushButton("Stop camera")
    btnCamera.clicked.connect(self.stopCamera)
    
    def stopCamera(self):
        self.timer.stop()
    
  • A label for displaying the camera frames and a text area for showing the barcode decoding results:

    self.label = QLabel()
    self.label.setFixedSize(self.WINDOW_WIDTH - 30, self.WINDOW_HEIGHT - 160)
    
    self.results = QTextEdit()
    
    def showResults(self, frame, results):
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        image = QImage(frame, frame.shape[1], frame.shape[0], frame.strides[0], QImage.Format_RGB888)
        pixmap = QPixmap.fromImage(image)
        pixmap = self.resizeImage(pixmap)
        self.label.setPixmap(pixmap)
        self.results.setText(results)
    

We use OpenCV to read image files and capture webcam frames:

frame = cv2.imread(filename)

self._cap = cv2.VideoCapture(0)
self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
ret, frame = self._cap.read()

The next step is crucial. We must determine how to integrate the barcode decoding API. The most straightforward approach that immediately comes to mind is to invoke the decoding method as soon as we capture a frame:

def nextFrameSlot(self):
    ret, frame = self._cap.read()

    if not ret:
        self.showMessageBox('Failed to get camera frame!')
        return

    frame, results = self._manager.decode_frame(frame)
    self.showResults(frame, results)

However, this approach isn’t feasible on Raspberry Pi! Barcode scanning is a CPU-intensive task. If it consumes too much time, it will block the UI thread. Therefore, we must run the UI code and barcode recognition code in separate threads. Due to the limitations imposed by Python’s GIL, using a Python thread isn’t a viable solution either. What about QThread? QThread operates similarly to a Python thread. To assess the performance of QThread, let’s conduct a test using the following code:

class WorkerSignals(QObject):
    result = Signal(object)

class BarcodeManager(QThread):
    def __init__(self, license):
        super(BarcodeManager, self).__init__()
        self.signals = WorkerSignals()

        self._reader = BarcodeReader()
        self._reader.init_license(license)
        settings = self._reader.get_runtime_settings()
        settings.max_algorithm_thread_count = 1
        self._reader.update_runtime_settings(settings)
        self.frameQueue = Queue(1)

    def register_callback(self, fn):
        self.signals.result.connect(fn)

    def run(self):
        while True:
            try:
                frame = self.frameQueue.get(False, 10)

                if type(frame) is str:
                    break
            except:
                time.sleep(0.01)
                continue

            try:
                results = self._reader.decode_buffer(frame)
                self.signals.result.emit(results)

            except BarcodeReaderError as error:
                print(error)

It appears that there’s no performance improvement. Thus, the most practical approach is to handle barcode decoding tasks in a separate Python Process:

from multiprocessing import Process, Queue
import time
import numpy as np


def process_barcode_frame(license, frameQueue, resultQueue):
    # Create Dynamsoft Barcode Reader
    reader = BarcodeReader()
    # Apply for a trial license: https://www.dynamsoft.com/customer/license/trialLicense
    reader.init_license(license)
    settings = reader.get_runtime_settings()
    settings.max_algorithm_thread_count = 1
    reader.update_runtime_settings(settings)

    while True:
        results = None

        try:
            frame = frameQueue.get(False, 10)
            if type(frame) is str:
                break
        except:
            time.sleep(0.01)
            continue

        try:
            frameHeight, frameWidth, channel = frame.shape[:3]
            results = reader.decode_buffer_manually(np.array(frame).tobytes(), frameWidth, frameHeight, frame.strides[0], EnumImagePixelFormat.IPF_RGB_888)
        except BarcodeReaderError as error:
            print(error)

        try:
            resultQueue.put(results, False, 10)
        except:
            pass

def create_decoding_process(license):
        size = 1
        frameQueue = Queue(size)
        resultQueue = Queue(size)
        barcodeScanning = Process(target=process_barcode_frame, args=(license, frameQueue, resultQueue))
        barcodeScanning.start()
        return frameQueue, resultQueue, barcodeScanning

We create two queues as the data tunnel between the two processes.

At this point, the app is complete. The only remaining step is to request a trial license and save it to your local disk.

Afterwards, you can connect a USB webcam to Raspberry Pi, and then run the app:

python3 app.py license.txt

Raspberry Pi GUI Barcode Reader

The GUI barcode reader app is cross-platform. It can also work on Windows, Linux and macOS.

References

Source Code

https://github.com/yushulx/python-barcode-qrcode-reader-qt-gui