← 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