Skip to Content

← PySide6 Camera GUI for a Raspberry Pi Vision System

Visual inspection (machine vision)Raspberry Pi + USB cameraUSB/V4L2HMI / Dashboard

PySide6 Camera GUI for a Raspberry Pi Vision System — full example

Build a PySide6 inspection GUI on Raspberry Pi with a QThread camera loop that only emits frames when the live view is visible, keeping the CPU free.

Complete, runnable program for the Raspberry Pi + USB camera (gui-pyside6-camera-thread.py): wiring header, requirements and integration notes included.

Download the full project pack — freeThis example + the related ones + bill of materials

Read-only preview.

# -*- coding: utf-8 -*-
"""
COMPLETE EXAMPLE — Multi-tab PySide6 GUI with asynchronous camera thread

Hardware:  Raspberry Pi + USB camera
Based on:  visual inspection project in production (gui.py, vision.py)

Requirements:
    pip install opencv-python PySide6 numpy

What it does — the same architecture as the shop-floor application:
    - Camera QThread: captures frames in its own thread and emits them
      as a Qt signal. The GUI never blocks waiting for the camera.
    - Key CPU saving on the Raspberry Pi: the thread ONLY emits frames
      when the tab with the live view is active (set_live_enabled).
    - 3 tabs: Operation (video + Inspect button), References
      (capture pattern) and Configuration (persisted sliders).
    Without a connected camera it generates synthetic frames: it works on any PC.

Integration with the catalog:
    - "Inspect" would call the pipeline + matching from the catalog:
      ejemplos/inspeccion-visual/opencv-silkscreen-pipeline.py
      ejemplos/inspeccion-visual/rotational-template-matching.py
    - The sliders persist in config.json and the references as PNG:
      ejemplos/inspeccion-visual/json-config-references.py
"""

import sys
import time
import cv2
import numpy as np
from PySide6.QtCore import QThread, Signal, Qt
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import (QApplication, QMainWindow, QTabWidget,
                               QWidget, QVBoxLayout, QLabel, QPushButton,
                               QSlider)


class CameraThread(QThread):
    """Capture thread: emits BGR frames only if the live view is active."""
    frame_ready = Signal(np.ndarray)

    def __init__(self, cam_index=0):
        super().__init__()
        self._running = True
        self._live = True          # the GUI toggles it when switching tabs
        self._cam_index = cam_index

    def set_live_enabled(self, enabled):
        # Active tab without video -> no frames emitted -> CPU freed
        self._live = enabled

    def stop(self):
        self._running = False
        self.wait()

    def run(self):
        cap = cv2.VideoCapture(self._cam_index)
        use_camera = cap.isOpened()
        while self._running:
            if not self._live:           # live view off:
                time.sleep(0.1)          # sleep, do not capture or emit
                continue
            if use_camera:
                ok, frame = cap.read()
                if not ok: continue
            else:                        # hardware-less demo: synthetic frame
                frame = np.full((480, 640, 3), 30, np.uint8)
                cv2.putText(frame, time.strftime("%H:%M:%S"), (200, 250),
                            cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 2)
                time.sleep(0.033)        # ~30 simulated fps
            self.frame_ready.emit(frame)
        cap.release()  # no-op if the camera never opened


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Visual inspection - GUI demo")

        # --- Tabs ---------------------------------------------------
        self.tabs = QTabWidget()
        self.setCentralWidget(self.tabs)
        self.tabs.addTab(self._operation_tab(), "Operation")
        self.tabs.addTab(self._references_tab(), "References")
        self.tabs.addTab(self._config_tab(), "Configuration")
        self.tabs.currentChanged.connect(self._on_tab_changed)  # live on/off

        # --- Camera thread ----------------------------------------------
        self.cam = CameraThread()
        self.cam.frame_ready.connect(self._show_frame)
        self.cam.start()

    def _operation_tab(self):
        w, layout = QWidget(), QVBoxLayout()
        self.video_label = QLabel("Waiting for camera...")
        self.video_label.setAlignment(Qt.AlignCenter)
        self.result = QLabel("--")
        self.result.setAlignment(Qt.AlignCenter)
        btn = QPushButton("Inspect part")
        btn.clicked.connect(self._inspect)
        for widget in (self.video_label, btn, self.result):
            layout.addWidget(widget)
        w.setLayout(layout)
        return w

    def _references_tab(self):
        w, layout = QWidget(), QVBoxLayout()
        btn = QPushButton("Capture reference (saves processed PNG)")
        btn.clicked.connect(lambda: print("-> save_reference(), see "
                                          "json-config-references.py"))
        layout.addWidget(btn)
        w.setLayout(layout)
        return w

    def _config_tab(self):
        w, layout = QWidget(), QVBoxLayout()
        layout.addWidget(QLabel("bg_threshold (persists in config.json)"))
        s = QSlider(Qt.Horizontal)
        s.setRange(0, 255), s.setValue(60)
        s.valueChanged.connect(lambda v: print(f"bg_threshold = {v}"))
        layout.addWidget(s)
        w.setLayout(layout)
        return w

    def _on_tab_changed(self, index):
        # Only tab 0 (Operation) shows live video
        self.cam.set_live_enabled(index == 0)

    def _show_frame(self, frame):
        """Converts the OpenCV BGR frame to QPixmap and paints it."""
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        h, w, ch = rgb.shape
        qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888)
        self.video_label.setPixmap(QPixmap.fromImage(qimg))

    def _inspect(self):
        # Here you would call process() + rotational_match() from the catalog
        self.result.setText("PASS (score 0.93) - demo")

    def closeEvent(self, event):
        self.cam.stop()              # stop the thread before closing
        event.accept()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = MainWindow()
    win.resize(720, 600), win.show()
    sys.exit(app.exec())
Download the full project pack — freeThis example + the related ones + bill of materials