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

Popular posts from this blog

480x320 TFT/ILI9488 SPI wih EP32C3 (arduino-esp32) using Arduino_GFX Library

Drive 320x240 ILI9341 SPI TFT using ESP32-S3 (NodeMCU ESP-S3-12K-Kit) using TFT_eSPI library, in Arduino Framework.