Python/PyQt5/Picamera2 to control Raspberry Pi Cameras with GUI, added White Balance setting.
Further works on my previous post Python/PyQt5/Picamera2 to control Raspberry Pi Cameras with GUI, with camera_controls, added White Balance setting.
Actually, I'm not sure the setting and operation of Awb and ColourGains.
refer:
~
The
Picamera2 Library
document, Appendix C: Camera controls.
Code:
picam2_qt5_2024-01-20.py
"""
Python 3/PyQt5 + picamera2 to control Raspberry Pi Camera Modules
Tested on Raspberry Pi 5/64-bit Raspberry Pi OS (bookworm)
# in my setup:
# Picamera2(0) - HQ Camera
# Picamera2(1) - Camera Module 3
picam2_qt5_2023-12-28.py first exercise
picam2_qt5_2024-01-03.py Added Auto-Focus feature detection, and switch AF Mode between Continuous & Manual
picam2_qt5_2024-01-07.py Display Preview in seperated window, both Main/Preview windows have Capture button.
picam2_qt5_2024-01-13.py Add camera_controls to adjust brightness at runtime.
Handle sys.argv, such that user can select cam at command line.
picam2_qt5_2024-01-15.py Add camera_controls: brightness, Contrast, ExposureValue, Saturation & Sharpness.
picam2_qt5_2024-01-20.py Add White Balance
"""
import sys, platform, os
from PyQt5.QtWidgets import (QMainWindow, QApplication, QPushButton, QLabel, QCheckBox,
QWidget, QTabWidget, QHBoxLayout, QVBoxLayout, QGridLayout,
QGroupBox, QSlider, QButtonGroup)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from picamera2 import Picamera2
from picamera2.previews.qt import QGlPicamera2
from picamera2 import __name__ as picamera2_name
from libcamera import controls
import time
from importlib.metadata import version
os.environ["LIBCAMERA_LOG_LEVELS"] = "3"
"""
Because my Camera Module 3 (with Auto-Focus) is assigned to Picamera2(1),
so I make 1 as default
command line usage:
python picam2_qt5_xxx.py # using default cam
python picam2_qt5_xxx.py 0 # using cam 0
python picam2_qt5_xxx.py 1 # using cam 1
"""
DEFAULT_CAM_NUM = 1
cam_num = DEFAULT_CAM_NUM # default Picamera2(DEFAULT_CAM_NUM)
if len(sys.argv)==2:
if sys.argv[1] == "1":
cam_num = 1
if sys.argv[1] == "0":
cam_num = 0
print("picam2 = Picamera2("+str(cam_num)+")")
picam2 = Picamera2(cam_num)
#picam2 = Picamera2() #default to Picamera2(0) without parameter passed
#picam2 = Picamera2(1)
#=====================================
preview_width= 800
preview_height = int(picam2.sensor_resolution[1] * preview_width/picam2.sensor_resolution[0])
preview_config_raw = picam2.create_preview_configuration(main={"size": (preview_width, preview_height)},
raw={"size": picam2.sensor_resolution})
picam2.configure(preview_config_raw)
#=====================================
#Detect if AF function is available
AF_Function = True
AF_Enable = True
try:
picam2.set_controls({"AfMode": controls.AfModeEnum.Continuous})
print("Auto-Focus Function Enabled")
except RuntimeError as err:
print("RuntimeError:", err)
AF_Function = False
#=====================================
class App(QMainWindow):
def __init__(self):
super().__init__()
self.title = sys.argv[0] #__file__
self.left = 0
self.top = 0
self.setWindowTitle(self.title)
self.main_widget = MyMainWidget(self)
self.setCentralWidget(self.main_widget)
self.show()
# factors to convert control's values in float to int for PyQt5 Widgets
FACTOR_Brighness = 10
FACTOR_1 = 1
FACTOR_10 = 10
class MyMainWidget(QWidget):
#--- MyPreviewWidget ---
#inner class for Preview Window
class MyPreviewWidget(QWidget):
def __init__(self, subLayout):
super(QWidget, self).__init__()
self.setLayout(subLayout)
#--- End of MyPreviewWidget ---
# --- SliderSetting ---
#inner class for picam2.controls of float with slider setting
class SliderSetting:
def on_Setting_valueChanged(self):
value = float(self.sliderSetting.value())/self.factor
self.labelSetting.setText(self.setting_name + ": " + str(value))
def on_Setting_sliderReleased(self):
valueSetting = float(self.sliderSetting.value())/self.factor
self.callback(self.setting_name, valueSetting)
def __init__(self, setting_name, factor, callback):
self.setting_name = setting_name
self.factor = factor
self.callback = callback
cam_controls = picam2.camera_controls
self.sliderSetting = QSlider(Qt.Horizontal)
self.sliderSetting.setMinimum(int(cam_controls[self.setting_name][0]*self.factor))
self.sliderSetting.setMaximum(int(cam_controls[self.setting_name][1]*self.factor))
value = cam_controls[self.setting_name][2]
self.sliderSetting.setValue(int(value*self.factor))
self.sliderSetting.setTickPosition(QSlider.TicksBelow)
self.sliderSetting.setTickInterval(1)
self.sliderSetting.sliderReleased.connect(self.on_Setting_sliderReleased)
self.sliderSetting.valueChanged.connect(self.on_Setting_valueChanged)
self.labelSetting = QLabel(self.setting_name + ": " + str(value))
#--- End of SliderSetting ---
# --- ColourGains (red & blue) ---
#inner class for picam2.controls of float with slider setting for ColourGains (red & blue)
class ColourGains_panel:
def on_ColourGains_valueChanged(self):
print("ColourGains_panel > on_ColourGains_valueChanged")
self.labelColourGains_red.setText("ColourGains (red): " + str(self.sliderColourGains_red.value()))
self.labelColourGains_blue.setText("ColourGains (blue): " + str(self.sliderColourGains_blue.value()))
def on_ColourGains_sliderReleased(self):
print("ColourGains_panel > on_ColourGains_sliderReleased")
newRed = self.sliderColourGains_red.value()
newBlue = self.sliderColourGains_blue.value()
self.labelColourGains_red.setText("ColourGains (red): " + str(newRed))
self.labelColourGains_blue.setText("ColourGains (blue): " + str(newBlue))
with picam2.controls as cam_controls:
cam_controls.ColourGains = (float(newRed), float(newBlue))
self.parent.cbAwbEnable.setChecked(False)
def __init__(self, parent):
self.parent = parent
cam_controls = picam2.camera_controls
ColourGains = cam_controls["ColourGains"]
print("===> ColourGains = ", ColourGains)
self.sliderColourGains_red = QSlider(Qt.Horizontal)
self.sliderColourGains_red.setMinimum(0)
self.sliderColourGains_red.setMaximum(32)
self.sliderColourGains_red.setValue(0)
self.sliderColourGains_red.setTickPosition(QSlider.TicksBelow)
self.sliderColourGains_red.setTickInterval(1)
self.sliderColourGains_red.sliderReleased.connect(self.on_ColourGains_sliderReleased)
self.sliderColourGains_red.valueChanged.connect(self.on_ColourGains_valueChanged)
self.labelColourGains_red = QLabel()
self.labelColourGains_red = QLabel("ColourGains (red): " + str(0))
self.sliderColourGains_blue = QSlider(Qt.Horizontal)
self.sliderColourGains_blue.setMinimum(0)
self.sliderColourGains_blue.setMaximum(32)
self.sliderColourGains_blue.setValue(0)
self.sliderColourGains_blue.setTickPosition(QSlider.TicksBelow)
self.sliderColourGains_blue.setTickInterval(1)
self.sliderColourGains_blue.sliderReleased.connect(self.on_ColourGains_sliderReleased)
self.sliderColourGains_blue.valueChanged.connect(self.on_ColourGains_valueChanged)
self.labelColourGains_blue = QLabel()
self.labelColourGains_blue = QLabel("ColourGains (blue): " + str(0))
self.layout = QVBoxLayout()
self.layout.addWidget(self.labelColourGains_red)
self.layout.addWidget(self.sliderColourGains_red)
self.layout.addWidget(self.labelColourGains_blue)
self.layout.addWidget(self.sliderColourGains_blue)
WB_notice = "\n*** Actually I'm not sure about the setting of Awb and ColourGains."
self.labelWB_notice = QLabel(WB_notice)
self.layout.addWidget(self.labelWB_notice)
#--- End of ColourGains (red & blue) ---
def read_f(self, file):
with open(file, encoding='UTF-8') as reader:
content = reader.read()
return content
def read_pretty_name(self):
with open("/etc/os-release", encoding='UTF-8') as f:
os_release = {}
for line in f:
k,v = line.rstrip().split("=")
os_release[k] = v.strip('"')
return os_release['PRETTY_NAME']
def AF_Enable_CheckBox_onStateChanged(self):
with picam2.controls as cam_controls:
if self.AF_Enable_CheckBox.isChecked():
cam_controls.AfMode = controls.AfModeEnum.Continuous
else:
cam_controls.AfMode = controls.AfModeEnum.Manual
def on_Capture_Clicked(self):
# There are two buttons on Main/Child Window connected here,
# identify the sender for info only, no actual use.
sender = self.sender()
if sender is self.btnCapture:
print("Capture button on Main Window clicked")
if sender is self.btnChildCapture:
print("Capture button on Child Preview Window clicked")
self.btnCapture.setEnabled(False)
cfg = picam2.create_still_configuration()
timeStamp = time.strftime("%Y%m%d-%H%M%S")
targetPath="/home/pi/Desktop/img" + str(cam_num) + "_"+timeStamp+".jpg"
print("- Capture image:", targetPath)
picam2.switch_mode_and_capture_file(cfg, targetPath, signal_function=self.qpicamera2.signal_done)
def capture_done(self, job):
result = picam2.wait(job)
self.btnCapture.setEnabled(True)
print("- capture_done.")
print(result)
def cbAwbEnable_stateChanged(self):
print("cbAwbEnable_stateChanged")
with picam2.controls as cam_controls:
cam_controls.AwbEnable = self.cbAwbEnable.isChecked()
def bg_WB_option_clicked(self, bg):
match bg:
case self.cbWB_Auto:
awb = controls.AwbModeEnum.Auto
case self.cbWB_Tungsten:
awb = controls.AwbModeEnum.Tungsten
case self.cbWB_Fluorescent:
awb = controls.AwbModeEnum.Fluorescent
case self.cbWB_Indoor:
awb = controls.AwbModeEnum.Indoor
case self.cbWB_Daylight:
awb = controls.AwbModeEnum.Daylight
case self.cbWB_Cloudy:
awb = controls.AwbModeEnum.Cloudy
case self.cbWB_Custom:
awb = controls.AwbModeEnum.Custom
case _:
print("unknown")
with picam2.controls as cam_controls:
cam_controls.AwbMode = awb
print(cam_controls.AwbMode)
def __init__(self, parent):
super(QWidget, self).__init__(parent)
#--- Prepare child Preview Window ----------
self.childPreviewLayout = QVBoxLayout()
# Check Auto-Focus feature
if AF_Function:
self.AF_Enable_CheckBox = QCheckBox("Auto-Focus (Continuous)")
self.AF_Enable_CheckBox.setChecked(True)
self.AF_Enable_CheckBox.setEnabled(True)
self.AF_Enable_CheckBox.stateChanged.connect(self.AF_Enable_CheckBox_onStateChanged)
self.childPreviewLayout.addWidget(self.AF_Enable_CheckBox)
print("show Auto-Focus Mode Change QCheckBox")
else:
self.AF_Enable_CheckBox = QCheckBox("No Auto-Focus function")
self.AF_Enable_CheckBox.setChecked(False)
self.AF_Enable_CheckBox.setEnabled(False)
print("No Auto-Focus Mode Change QCheckBox")
# Preview qpicamera2
self.qpicamera2 = QGlPicamera2(picam2,
width=preview_width, height=preview_height,
keep_ar=True)
self.qpicamera2.done_signal.connect(self.capture_done)
self.childPreviewLayout.addWidget(self.qpicamera2)
# Capture button on Child Window
self.btnChildCapture = QPushButton("Capture Image" + str(cam_num))
self.btnChildCapture.setFont(QFont("Helvetica", 13, QFont.Bold))
self.btnChildCapture.clicked.connect(self.on_Capture_Clicked)
self.childPreviewLayout.addWidget(self.btnChildCapture)
# pass layout to child Preview Window
self.myPreviewWindow = self.MyPreviewWidget(self.childPreviewLayout)
# roughly set Preview windows size according to preview_width x preview_height
self.myPreviewWindow.setGeometry(10, 10, preview_width+10, preview_height+100)
self.myPreviewWindow.setWindowTitle("Preview size (" + str(cam_num) + ") - " +
str(preview_width) + " x " + str(preview_height))
self.myPreviewWindow.show()
#--- End of Prepare child Preview Window ---
self.layout = QVBoxLayout()
# Initialize tab screen
self.tabs = QTabWidget()
self.tabControl = QWidget()
self.tabWB = QWidget()
self.tabInfo = QWidget()
# Add tabs
self.tabs.addTab(self.tabControl, " Control ")
self.tabs.addTab(self.tabWB, "White Balance")
self.tabs.addTab(self.tabInfo, " Info ")
#=== Tab Capture ===
# Create first tab
self.tabControl.layout = QVBoxLayout()
self.btnCapture = QPushButton("Capture Image " + str(cam_num))
self.btnCapture.setFont(QFont("Helvetica", 15, QFont.Bold))
self.btnCapture.clicked.connect(self.on_Capture_Clicked)
self.tabControl.layout.addWidget(self.btnCapture)
# Prepre camera_controls
cam_controls = picam2.camera_controls
# gboxCamControls: QGroupBox to hold all controls
self.gboxCamControls = QGroupBox()
self.gboxCamControls.setTitle("picam2.camera_controls")
self.vboxCamControls = QVBoxLayout()
self.gboxCamControls.setLayout(self.vboxCamControls)
self.tabControl.layout.addWidget(self.gboxCamControls)
# - More Control Setting -
#Brightness
def callback_Brightness(setting_name, value):
with picam2.controls as cam_controls:
cam_controls.Brightness = value
print(str(value), "=>", setting_name, "=", str(cam_controls.Brightness))
self.sliderBrightness = self.SliderSetting("Brightness", 10, callback_Brightness)
self.vboxCamControls.addWidget(self.sliderBrightness.labelSetting)
self.vboxCamControls.addWidget(self.sliderBrightness.sliderSetting)
#Contrast
def callback_Contrast(setting_name, value):
with picam2.controls as cam_controls:
cam_controls.Contrast = value
print(str(value), "=>", setting_name, "=", str(cam_controls.Contrast))
self.sliderContrast = self.SliderSetting("Contrast", 1, callback_Contrast)
self.vboxCamControls.addWidget(self.sliderContrast.labelSetting)
self.vboxCamControls.addWidget(self.sliderContrast.sliderSetting)
#ExposureValue
def callback_ExposureValue(setting_name, value):
with picam2.controls as cam_controls:
cam_controls.ExposureValue = value
print(str(value), "=>", setting_name, "=", str(cam_controls.ExposureValue))
self.sliderExposureValue = self.SliderSetting("ExposureValue", 1, callback_ExposureValue)
self.vboxCamControls.addWidget(self.sliderExposureValue.labelSetting)
self.vboxCamControls.addWidget(self.sliderExposureValue.sliderSetting)
#Saturation
def callback_Saturation(setting_name, value):
with picam2.controls as cam_controls:
cam_controls.Saturation = value
print(str(value), "=>", setting_name, "=", str(cam_controls.Saturation))
self.sliderSaturation = self.SliderSetting("Saturation", 1, callback_Saturation)
self.vboxCamControls.addWidget(self.sliderSaturation.labelSetting)
self.vboxCamControls.addWidget(self.sliderSaturation.sliderSetting)
#Sharpness
def callback_Sharpness(setting_name, value):
with picam2.controls as cam_controls:
cam_controls.Sharpness = value
print(str(value), "=>", setting_name, "=", str(cam_controls.Sharpness))
self.sliderSharpness = self.SliderSetting("Sharpness", 1, callback_Sharpness)
self.vboxCamControls.addWidget(self.sliderSharpness.labelSetting)
self.vboxCamControls.addWidget(self.sliderSharpness.sliderSetting)
# - End of Control Setting -
self.labelMore = QLabel("...more controls will be placed here in coming exercises.")
self.vboxCamControls.addWidget(self.labelMore)
# End of Prepre camera_controls
self.tabControl.layout.addStretch()
self.tabControl.setLayout(self.tabControl.layout)
#=== Tab WhiteBalance===
self.cbAwbEnable = QCheckBox("AwbEnable")
self.cbAwbEnable.stateChanged.connect(self.cbAwbEnable_stateChanged)
self.tabWB_option_layout = QHBoxLayout()
self.cbWB_Auto = QCheckBox("Auto")
self.cbWB_Auto.setChecked(True)
self.cbWB_Tungsten = QCheckBox("Tungsten")
self.cbWB_Fluorescent = QCheckBox("Fluorescent")
self.cbWB_Indoor = QCheckBox("Indoor")
self.cbWB_Daylight = QCheckBox("Daylight")
self.cbWB_Cloudy = QCheckBox("Cloudy")
self.cbWB_Custom = QCheckBox("Custom")
self.bg_WB_option = QButtonGroup()
self.bg_WB_option.addButton(self.cbWB_Auto, 1)
self.bg_WB_option.addButton(self.cbWB_Tungsten, 2)
self.bg_WB_option.addButton(self.cbWB_Fluorescent, 3)
self.bg_WB_option.addButton(self.cbWB_Indoor, 4)
self.bg_WB_option.addButton(self.cbWB_Daylight, 5)
self.bg_WB_option.addButton(self.cbWB_Cloudy, 6)
self.bg_WB_option.addButton(self.cbWB_Custom, 7)
self.bg_WB_option.buttonClicked.connect(self.bg_WB_option_clicked)
self.tabWB_option_layout.addWidget(self.cbWB_Auto)
self.tabWB_option_layout.addWidget(self.cbWB_Tungsten)
self.tabWB_option_layout.addWidget(self.cbWB_Fluorescent)
self.tabWB_option_layout.addWidget(self.cbWB_Indoor)
self.tabWB_option_layout.addWidget(self.cbWB_Daylight)
self.tabWB_option_layout.addWidget(self.cbWB_Cloudy)
self.tabWB_option_layout.addWidget(self.cbWB_Custom)
self.labelColourGains_red = QLabel("ColourGains: red gain")
self.labelColourGains_blue = QLabel("ColourGains: blue gain")
self.tabWB.layout = QVBoxLayout()
self.tabWB.layout.addWidget(self.cbAwbEnable)
self.tabWB.layout.addLayout(self.tabWB_option_layout)
self.tabWB.layout.addWidget(self.labelColourGains_red)
self.tabWB.layout.addWidget(self.labelColourGains_blue)
self.panelColourGains_panel = self.ColourGains_panel(self)
self.tabWB.layout.addLayout(self.panelColourGains_panel.layout)
self.tabWB.layout.addStretch()
self.tabWB.setLayout(self.tabWB.layout)
#=== Tab Info ===
self.tabInfo.layout = QVBoxLayout()
infoGridLayout = QGridLayout()
rowSpan = 1
columnSpan0 = 1
columnSpan1 = 5
infoGridLayout.addWidget(QLabel('Python', self), 0, 0, rowSpan, columnSpan0)
infoGridLayout.addWidget(QLabel(platform.python_version(), self), 0, 1, rowSpan, columnSpan1)
infoGridLayout.addWidget(QLabel(picamera2_name, self), 1, 0, rowSpan, columnSpan0)
infoGridLayout.addWidget(QLabel(version(picamera2_name), self), 1, 1, rowSpan, columnSpan1)
infoGridLayout.addWidget(QLabel(' ', self), 2, 0, rowSpan, columnSpan0)
infoGridLayout.addWidget(QLabel('Camera Module:', self), 3, 0, rowSpan, columnSpan0)
cam_properties = picam2.camera_properties
cam_Model = cam_properties['Model']
infoGridLayout.addWidget(QLabel('Model', self), 4, 0, rowSpan, columnSpan0)
infoGridLayout.addWidget(QLabel(cam_Model, self), 4, 1, rowSpan, columnSpan1)
cam_PixelArraySize = str(cam_properties['PixelArraySize'][0]) + " x " + str(cam_properties['PixelArraySize'][1])
infoGridLayout.addWidget(QLabel('PixelArraySize', self), 5, 0, rowSpan, columnSpan0)
infoGridLayout.addWidget(QLabel(cam_PixelArraySize, self), 5, 1, rowSpan, columnSpan1)
infoGridLayout.addWidget(QLabel(' ', self), 6, 0, rowSpan, columnSpan0)
infoGridLayout.addWidget(QLabel('Machine:', self), 7, 0, rowSpan, columnSpan0)
infoGridLayout.addWidget(QLabel('Board', self), 8, 0, rowSpan, columnSpan0, Qt.AlignTop)
board_def = "/proc/device-tree/model"
board_info = self.read_f("/proc/device-tree/model") +"\n(" + board_def +")"
infoGridLayout.addWidget(QLabel(board_info, self), 8, 1, rowSpan, columnSpan0)
infoGridLayout.addWidget(QLabel('OS', self), 9, 0, rowSpan, columnSpan0, Qt.AlignTop)
os_info = self.read_pretty_name() + "\n" + os.uname()[3] +"\n" + os.uname()[4] \
+ (" (64-bit)" if sys.maxsize > 2**32 else " (32-bit)")
infoGridLayout.addWidget(QLabel(os_info, self), 9, 1, rowSpan, columnSpan1)
self.tabInfo.layout.addLayout(infoGridLayout)
self.tabInfo.layout.addStretch()
self.tabInfo.setLayout(self.tabInfo.layout)
#==================================
# Add tabs to widget
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
picam2.start()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
next:
~ Python 3/PyQt5 + picamera2 on Raspberry Pi, list available cameras.
Comments
Post a Comment