Raspberry Pi Related Posts Below:
Gui Control for Autofocus
The Raspberry Pi Camera Module 3 has an Autofocus control, this can also be manually controlled from code.
How far can I get building a GUI control for this function using just prompts from autopilot.
Visual Studio Code (VS Code) is a free, open-source code editor and IDE developed by Microsoft. It is a recommened IDE for both Raspberry Pi and Jetson Orin Nano.
Copilot self describes as an AI-powered code completion tool developed by GitHub in partnership with OpenAI. Copilot is fully integrated into the VS Code environment.
Copilot claims to generate code from descriptive comments.
I have a Raspberry Pi Module 3 camera with autofocus. Can Copilot build me a GUI based Autofocus control using only prompts?
Lets find out!
Prompt 1
using pyqt5 and picamera2 create a widget that allows the af settings of the camera 3 camera to be adjusted from the widget
Copilot tells me what it’s going to do and then generates some code. Running this code generates a window pretty much like the following:

Prompt 1 Output
Here’s the Copilot Generated Code Listing
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
class AutoFocusControl(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('Raspberry Pi Camera Autofocus Control')
layout = QVBoxLayout()
self.status = QLabel('Autofocus status: Unknown')
layout.addWidget(self.status)
btn_focus = QPushButton('Trigger Autofocus')
btn_focus.clicked.connect(self.trigger_autofocus)
layout.addWidget(btn_focus)
self.setLayout(layout)
def trigger_autofocus(self):
# Here you would add code to trigger autofocus on the camera
self.status.setText('Autofocus triggered!')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = AutoFocusControl()
window.show()
sys.exit(app.exec_())
Well… I can’t argue! It is a GUI widget, and yes, a brief look at the code confirms that it could indeed update at least some of the Camera AF control parameters. But I had hoped for some kind of preview facility to give me an indation of what effect changing the control would have. Let’s refine the prompt.
Prompt 2
integrate this with video
Copilot tells me: “To integrate video into your existing AFSettingsWidget
, you can use the QLabel
widget to display the video feed from the camera. The Picamera2
library provides a way to capture frames and display them in a PyQt5 application. Below is the updated code:”
To be honest, I wasn’t too convinced by this approach, and the output from the revised listing confirmed my worst suspicions.
Prompt 2 OutputHere’s the Copilot Generated Code Listing
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QSlider, QPushButton, QHBoxLayout
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QImage, QPixmap
from picamera2 import Picamera2, Preview
class AFSettingsWidget(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Camera AF Settings")
self.picam = Picamera2()
self.picam.configure(self.picam.create_preview_configuration(main={"size": (640, 480)}))
self.picam.start()
# Layout
self.layout = QVBoxLayout()
# Video Display
self.video_label = QLabel("Video Feed")
self.video_label.setFixedSize(640, 480)
self.layout.addWidget(self.video_label)
# Timer for updating video feed
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_video_feed)
self.timer.start(30) # Update every 30ms (~33 FPS)
# AF Speed Slider
self.af_speed_label = QLabel("AF Speed:")
self.af_speed_slider = QSlider(Qt.Horizontal)
self.af_speed_slider.setMinimum(0)
self.af_speed_slider.setMaximum(100)
self.af_speed_slider.setValue(50)
self.af_speed_slider.valueChanged.connect(self.update_af_speed)
# AF Range Slider
self.af_range_label = QLabel("AF Range:")
self.af_range_slider = QSlider(Qt.Horizontal)
self.af_range_slider.setMinimum(0)
self.af_range_slider.setMaximum(100)
self.af_range_slider.setValue(50)
self.af_range_slider.valueChanged.connect(self.update_af_range)
# Buttons
self.apply_button = QPushButton("Apply Settings")
self.apply_button.clicked.connect(self.apply_settings)
self.reset_button = QPushButton("Reset Settings")
self.reset_button.clicked.connect(self.reset_settings)
# Add widgets to layout
self.layout.addWidget(self.af_speed_label)
self.layout.addWidget(self.af_speed_slider)
self.layout.addWidget(self.af_range_label)
self.layout.addWidget(self.af_range_slider)
button_layout = QHBoxLayout()
button_layout.addWidget(self.apply_button)
button_layout.addWidget(self.reset_button)
self.layout.addLayout(button_layout)
self.setLayout(self.layout)
def update_video_feed(self):
# Capture a frame from the camera
frame = self.picam.capture_array()
# Convert the frame to QImage
height, width, channel = frame.shape
bytes_per_line = channel * width
q_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
# Display the frame in the QLabel
self.video_label.setPixmap(QPixmap.fromImage(q_image))
def update_af_speed(self, value):
self.af_speed_label.setText(f"AF Speed: {value}")
def update_af_range(self, value):
self.af_range_label.setText(f"AF Range: {value}")
def apply_settings(self):
af_speed = self.af_speed_slider.value()
af_range = self.af_range_slider.value()
# Apply AF settings to the camera
self.picam.set_controls({"AfSpeed": af_speed, "AfRange": af_range})
print(f"Applied AF Speed: {af_speed}, AF Range: {af_range}")
def reset_settings(self):
self.af_speed_slider.setValue(50)
self.af_range_slider.setValue(50)
self.picam.set_controls({"AfSpeed": 50, "AfRange": 50})
print("Reset AF settings to default.")
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
widget = AFSettingsWidget()
widget.show()
sys.exit(app.exec_())
Anybody who worked with picamera1 will remember the issues around integrating any kind of preview window witin the context of a PyQt app. Picamera2’s QGLPicamera2 widget which is a true PyQt widget gets round all of these problems. So…
Prompt 3
do this again but using the picamera2 QGlPicamera2 widget
Copilot now tells me: To use the QGlPicamera2
widget from the picamera2
library, you can replace the manual video feed handling with this widget. The QGlPicamera2
widget is specifically designed for displaying video feeds in PyQt5 applications, making the integration simpler and more efficient.
Didn’t I just tell you that?
Here’s the Copilot Generated Code Listing
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QSlider, QPushButton, QHBoxLayout
from PyQt5.QtCore import Qt
from picamera2 import Picamera2
# from picamera2.previews import QGlPicamera2
# manually changed by ln
from picamera2.previews.qt import QGlPicamera2
class AFSettingsWidget(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Camera AF Settings")
self.picam = Picamera2()
self.picam.configure(self.picam.create_preview_configuration(main={"size": (640, 480)}))
# Layout
self.layout = QVBoxLayout()
# Video Display using QGlPicamera2
self.video_widget = QGlPicamera2(self.picam, width=640, height=480)
self.layout.addWidget(self.video_widget)
# Start the camera
self.picam.start()
# AF Speed Slider
self.af_speed_label = QLabel("AF Speed:")
self.af_speed_slider = QSlider(Qt.Horizontal)
self.af_speed_slider.setMinimum(0)
self.af_speed_slider.setMaximum(100)
self.af_speed_slider.setValue(50)
self.af_speed_slider.valueChanged.connect(self.update_af_speed)
# AF Range Slider
self.af_range_label = QLabel("AF Range:")
self.af_range_slider = QSlider(Qt.Horizontal)
self.af_range_slider.setMinimum(0)
self.af_range_slider.setMaximum(100)
self.af_range_slider.setValue(50)
self.af_range_slider.valueChanged.connect(self.update_af_range)
# Buttons
self.apply_button = QPushButton("Apply Settings")
self.apply_button.clicked.connect(self.apply_settings)
self.reset_button = QPushButton("Reset Settings")
self.reset_button.clicked.connect(self.reset_settings)
# Add widgets to layout
self.layout.addWidget(self.af_speed_label)
self.layout.addWidget(self.af_speed_slider)
self.layout.addWidget(self.af_range_label)
self.layout.addWidget(self.af_range_slider)
button_layout = QHBoxLayout()
button_layout.addWidget(self.apply_button)
button_layout.addWidget(self.reset_button)
self.layout.addLayout(button_layout)
self.setLayout(self.layout)
def update_af_speed(self, value):
self.af_speed_label.setText(f"AF Speed: {value}")
def update_af_range(self, value):
self.af_range_label.setText(f"AF Range: {value}")
def apply_settings(self):
af_speed = self.af_speed_slider.value()
af_range = self.af_range_slider.value()
# Apply AF settings to the camera
self.picam.set_controls({"AfSpeed": af_speed, "AfRange": af_range})
print(f"Applied AF Speed: {af_speed}, AF Range: {af_range}")
def reset_settings(self):
self.af_speed_slider.setValue(50)
self.af_range_slider.setValue(50)
self.picam.set_controls({"AfSpeed": 50, "AfRange": 50})
print("Reset AF settings to default.")
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
widget = AFSettingsWidget()
widget.show()
sys.exit(app.exec_())
Superficially, if we run this code, we appear to be right back where we started, but the clip below identifies what the problem is. It also identifies what the problem after that will be too!
Prompt 4
i need to see the whole of the video_widget
Copilot responded: To ensure the video_widget
(the QGlPicamera2
widget) is fully visible, you can adjust the size of the main window to match the dimensions of the video feed. Here’s how you can modify your code to ensure the video_widget
is fully displayed:
Here’s the output. Which is still far from ideal.

Prompt 4 Output
Here’s the Copilot Generated Code Listing
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QSlider, QPushButton, QHBoxLayout
from PyQt5.QtCore import Qt
from picamera2 import Picamera2
# from picamera2.previews import QGlPicamera2
# manually changed by ln
from picamera2.previews.qt import QGlPicamera2
class AFSettingsWidget(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Camera AF Settings")
self.picam = Picamera2()
self.picam.configure(self.picam.create_preview_configuration(main={"size": (640, 480)}))
# Layout
self.layout = QVBoxLayout()
# Video Display using QGlPicamera2
self.video_widget = QGlPicamera2(self.picam, width=640, height=480)
self.layout.addWidget(self.video_widget)
# Start the camera
self.picam.start()
# AF Speed Slider
self.af_speed_label = QLabel("AF Speed:")
self.af_speed_slider = QSlider(Qt.Horizontal)
self.af_speed_slider.setMinimum(0)
self.af_speed_slider.setMaximum(100)
self.af_speed_slider.setValue(50)
self.af_speed_slider.valueChanged.connect(self.update_af_speed)
# AF Range Slider
self.af_range_label = QLabel("AF Range:")
self.af_range_slider = QSlider(Qt.Horizontal)
self.af_range_slider.setMinimum(0)
self.af_range_slider.setMaximum(100)
self.af_range_slider.setValue(50)
self.af_range_slider.valueChanged.connect(self.update_af_range)
# Buttons
self.apply_button = QPushButton("Apply Settings")
self.apply_button.clicked.connect(self.apply_settings)
self.reset_button = QPushButton("Reset Settings")
self.reset_button.clicked.connect(self.reset_settings)
# Add widgets to layout
self.layout.addWidget(self.af_speed_label)
self.layout.addWidget(self.af_speed_slider)
self.layout.addWidget(self.af_range_label)
self.layout.addWidget(self.af_range_slider)
button_layout = QHBoxLayout()
button_layout.addWidget(self.apply_button)
button_layout.addWidget(self.reset_button)
self.layout.addLayout(button_layout)
self.setLayout(self.layout)
# Resize the main window to fit the video widget and controls
self.resize(640, 600)
def update_af_speed(self, value):
self.af_speed_label.setText(f"AF Speed: {value}")
def update_af_range(self, value):
self.af_range_label.setText(f"AF Range: {value}")
def apply_settings(self):
af_speed = self.af_speed_slider.value()
af_range = self.af_range_slider.value()
# Apply AF settings to the camera
self.picam.set_controls({"AfSpeed": af_speed, "AfRange": af_range})
print(f"Applied AF Speed: {af_speed}, AF Range: {af_range}")
def reset_settings(self):
self.af_speed_slider.setValue(50)
self.af_range_slider.setValue(50)
self.picam.set_controls({"AfSpeed": 50, "AfRange": 50})
print("Reset AF settings to default.")
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
widget = AFSettingsWidget()
widget.show()
sys.exit(app.exec_())
A Pause for thought
This is still not looking anything like what I had had had in mind. However, by this time I concluded that a better plan would be to seperate the preview window from the control dialog.
Also the one control I was really interested in was a manual autofocus control , so I decided to pursue that.
It’s worth commenting at this point on the code that Coplilot is generating. It is robust and well structured. There are well implemented naming conventions for both functions and variable names.
While coding GUI widgets can be a very tedious business when often many of the properties of a widget have to be set across a number of lines. This can be a tedious and error prone business.
It was alrady clear to me that while some major manual finessing of the code might be required that by and large Copilot is a game changer both in terms of productivity and stress management when creating complex GUIs.
The image below shows CPs neat diff based approach to applying updates to the existing code.

Code Differences
Prompts 5, 6 and 7
So finally, after
Prompt 5: add a radio button group to set afmode and a slider to set manually the LensPosition. Units to be in dioptres
Prompt 6: put the QGLPicamera2 widget into its own window
and a rather cheeky
Prompt 7: Provide docstrings for all functions and classes
we end up with our final GUI looking something like this:

Final GUI Output
Final Copilot Listing
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QSlider, QPushButton, QHBoxLayout, QRadioButton, QButtonGroup
from PyQt5.QtCore import Qt
from picamera2 import Picamera2
from libcamera import controls # Import libcamera controls
# from picamera2.previews import QGlPicamera2
# manually changed by ln
from picamera2.previews.qt import QGlPicamera2
class VideoWindow(QWidget):
"""
A PyQt5 widget for displaying the camera video feed using QGlPicamera2.
"""
def __init__(self, video_widget):
"""
Initialize the VideoWindow.
Args:
video_widget (QGlPicamera2): The QGlPicamera2 widget for displaying the video feed.
"""
super().__init__()
self.setWindowTitle("Camera Video Feed")
# Layout for the video window
layout = QVBoxLayout()
# Add the provided QGlPicamera2 widget to the layout
layout.addWidget(video_widget)
self.setLayout(layout)
self.resize(640, 480)
class AFSettingsWidget(QWidget):
"""
A PyQt5 widget for controlling autofocus (AF) settings of a camera using Picamera2.
This widget allows users to:
- Switch between autofocus modes (Auto and Manual).
- Adjust the lens position in dioptres (0.0–10.0) when in Manual mode.
- Adjust autofocus speed and range.
- Apply or reset settings.
"""
def __init__(self, picam, video_widget):
"""
Initialize the AFSettingsWidget.
Args:
picam (Picamera2): The Picamera2 object for controlling the camera.
video_widget (QGlPicamera2): The QGlPicamera2 widget for displaying the video feed.
"""
super().__init__()
self.setWindowTitle("Camera AF Settings")
self.picam = picam
# Open the video window
self.video_window = VideoWindow(video_widget)
self.video_window.show()
# Start the camera
self.picam.start()
# Layout
self.layout = QVBoxLayout()
# AF Mode Radio Buttons
self.af_mode_label = QLabel("AF Mode:")
self.auto_mode_button = QRadioButton("Auto")
self.manual_mode_button = QRadioButton("Manual")
self.auto_mode_button.setChecked(True) # Default to Auto mode
self.af_mode_group = QButtonGroup()
self.af_mode_group.addButton(self.auto_mode_button)
self.af_mode_group.addButton(self.manual_mode_button)
self.af_mode_group.buttonClicked.connect(self.update_af_mode)
af_mode_layout = QHBoxLayout()
af_mode_layout.addWidget(self.auto_mode_button)
af_mode_layout.addWidget(self.manual_mode_button)
self.layout.addWidget(self.af_mode_label)
self.layout.addLayout(af_mode_layout)
# Lens Position Slider (in dioptres)
self.lens_position_label = QLabel("Lens Position (Dioptres):")
self.lens_position_slider = QSlider(Qt.Horizontal)
self.lens_position_slider.setMinimum(0) # Minimum dioptres
self.lens_position_slider.setMaximum(100) # Maximum dioptres (scaled by 10 for 0.1 increments)
self.lens_position_slider.setValue(0) # Default to 0 dioptres
self.lens_position_slider.setEnabled(False) # Disabled by default (enabled in manual mode)
self.lens_position_slider.valueChanged.connect(self.update_lens_position)
self.layout.addWidget(self.lens_position_label)
self.layout.addWidget(self.lens_position_slider)
# AF Speed Slider
self.af_speed_label = QLabel("AF Speed:")
self.af_speed_slider = QSlider(Qt.Horizontal)
self.af_speed_slider.setMinimum(0)
self.af_speed_slider.setMaximum(100)
self.af_speed_slider.setValue(50)
self.af_speed_slider.valueChanged.connect(self.update_af_speed)
# AF Range Slider
self.af_range_label = QLabel("AF Range:")
self.af_range_slider = QSlider(Qt.Horizontal)
self.af_range_slider.setMinimum(0)
self.af_range_slider.setMaximum(100)
self.af_range_slider.setValue(50)
self.af_range_slider.valueChanged.connect(self.update_af_range)
# Buttons
self.apply_button = QPushButton("Apply Settings")
self.apply_button.clicked.connect(self.apply_settings)
self.reset_button = QPushButton("Reset Settings")
self.reset_button.clicked.connect(self.reset_settings)
# Add widgets to layout
self.layout.addWidget(self.af_speed_label)
self.layout.addWidget(self.af_speed_slider)
self.layout.addWidget(self.af_range_label)
self.layout.addWidget(self.af_range_slider)
button_layout = QHBoxLayout()
button_layout.addWidget(self.apply_button)
button_layout.addWidget(self.reset_button)
self.layout.addLayout(button_layout)
self.setLayout(self.layout)
# Resize the main window to fit the controls
self.resize(400, 400)
def update_af_mode(self, button):
"""
Update the autofocus mode based on the selected radio button.
Args:
button (QRadioButton): The selected radio button (Auto or Manual).
Sets the autofocus mode to Auto or Manual using the appropriate enum
from libcamera.controls. Enables or disables the Lens Position slider
based on the selected mode.
"""
mode = controls.AfModeEnum.Auto if button == self.auto_mode_button else controls.AfModeEnum.Manual
try:
self.picam.set_controls({"AfMode": mode})
print(f"Set AF Mode to: {mode}")
except Exception as e:
print(f"Error setting AF Mode: {e}")
# Enable/Disable Lens Position slider based on mode
self.lens_position_slider.setEnabled(mode == controls.AfModeEnum.Manual)
if mode == controls.AfModeEnum.Auto:
self.lens_position_slider.setValue(0) # Reset to default
def update_lens_position(self, value):
"""
Update the lens position based on the slider value.
Args:
value (int): The slider value (0–100, scaled to 0.0–10.0 dioptres).
Converts the slider value to dioptres and updates the LensPosition
control on the camera.
"""
dioptres = float(value) / 10.0 # Ensure the value is a float
try:
self.picam.set_controls({"LensPosition": dioptres})
print(f"Set Lens Position to: {dioptres:.1f} dioptres")
except Exception as e:
print(f"Error setting Lens Position: {e}")
def update_af_speed(self, value):
"""
Update the autofocus speed label based on the slider value.
Args:
value (int): The slider value (0–100).
"""
self.af_speed_label.setText(f"AF Speed: {value}")
def update_af_range(self, value):
"""
Update the autofocus range label based on the slider value.
Args:
value (int): The slider value (0–100).
"""
self.af_range_label.setText(f"AF Range: {value}")
def apply_settings(self):
"""
Apply the current autofocus speed and range settings to the camera.
Retrieves the values from the sliders and updates the camera controls.
"""
af_speed = int(self.af_speed_slider.value()) # Ensure integer type
af_range = int(self.af_range_slider.value()) # Ensure integer type
try:
self.picam.set_controls({"AfSpeed": af_speed, "AfRange": af_range})
print(f"Applied AF Speed: {af_speed}, AF Range: {af_range}")
except Exception as e:
print(f"Error applying settings: {e}")
def reset_settings(self):
"""
Reset all autofocus settings to their default values.
Resets the sliders, autofocus mode, and lens position to their defaults.
"""
self.af_speed_slider.setValue(50)
self.af_range_slider.setValue(50)
self.lens_position_slider.setValue(0)
self.auto_mode_button.setChecked(True)
self.picam.set_controls({
"AfSpeed": 50,
"AfRange": 50,
"AfMode": controls.AfModeEnum.Auto,
"LensPosition": 0.0
})
print("Reset AF settings to default.")
def closeEvent(self, event):
"""
Ensure that closing this window also closes the video window and exits the app.
"""
if hasattr(self, 'video_window'):
self.video_window.close()
QApplication.quit()
super().closeEvent(event)
class VideoWindow(QWidget):
"""
A PyQt5 widget for displaying the camera video feed using QGlPicamera2.
"""
def __init__(self, video_widget):
"""
Initialize the VideoWindow.
Args:
video_widget (QGlPicamera2): The QGlPicamera2 widget for displaying the video feed.
"""
super().__init__()
self.setWindowTitle("Camera Video Feed")
# Layout for the video window
layout = QVBoxLayout()
# Add the provided QGlPicamera2 widget to the layout
layout.addWidget(video_widget)
self.setLayout(layout)
self.resize(640, 480)
def closeEvent(self, event):
"""
Ensure that closing this window also closes the AF settings window and exits the app.
"""
# Find the AFSettingsWidget parent and close it if it exists
for widget in QApplication.topLevelWidgets():
if isinstance(widget, AFSettingsWidget):
widget.close()
QApplication.quit()
super().closeEvent(event)
# if __name__ == "__main__":
# import sys
# app = QApplication(sys.argv)
# widget = AFSettingsWidget()
# widget.show()
# sys.exit(app.exec_())
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
# Instantiate the Picamera2 object
picam = Picamera2()
# Create the QGlPicamera2 widget
video_widget = QGlPicamera2(picam, width=640, height=480)
# Create the AFSettingsWidget and pass the camera and video widget
widget = AFSettingsWidget(picam, video_widget)
# Show the widget
widget.show()
# Execute the application
sys.exit(app.exec_())
Further Thoughts
To be added