I am attempting to monitor/edit settings of a Pi Camera Module 3 with tkinter. When I start running my program it runs fine, but after a few hours of running the camera and entire Raspberry PI system begin to lag heavily. The second I terminate the program the system returns back to normal operation. Can someone please help me figure out what is causing the system to lag so this program can run indefinitely without lagging? I have monitored memory/cpu/interrupts/context switches none seem to be raise as the program runs. I have tried changing the nice value and priority of the program to -20 and neither seem to do anything. I have pasted my code below (there is extra open-cv code in there because I am also monitoring usb cameras, but the problem still persists if I remove all the usb cameras and only use the pi camera). I am using a PI4 but plan to use this code on a new PI5 when I get this issue sorted. Any help would be much appreciated.
app.py:
import cv2
import tkinter
from classes.Camera import Camera
from classes.PiCameraControls import *
class App:
def __init__(self, window, window_title, video_sources):
# window for app
self.window = window
self.window.title(window_title)
# add cameras
self.sources = []
row = 0
col = 0
for source in video_sources:
text, stream = source
if (stream != -1):
source = Camera(self.window, text, stream, 400, 300)
else:
source = Camera(self.window, text, stream, 1024, 768)
source.grid(row=row, column=col, rowspan=1 if stream != -1 else 3, sticky=tk.N)
if col < 1:
col += 1
else:
row += 1
self.sources.append(source)
# picamera controls
self.PiCameraControls = PiCameraControls(window=self.window, camera=self.sources[0], sliderLength=305)
self.PiCameraControls.grid(row=(3 if len(video_sources) < 2 else 2), column = 0, columnspan=2, sticky=tk.NW)
self.window.protocol("WM_DELETE_WINDOW", self.delete)
self.window.mainloop()
def delete(self):
print('[App] stoping threads')
for source in self.sources:
source.vid.running = False
self.PiCameraControls.delete()
print('[App] exit')
self.window.destroy()
def getSources(MAX = 10):
sources = [ ('PI Camera', -1), ]
# iterates over usb indicies trying to find webcams
for i in range(1, MAX):
# check if camera is at index
vid = cv2.VideoCapture(i)
# kill camera for now if opened
if vid.isOpened():
vid.release()
sources.append(("Logitech Webcam", i))
return sources
if __name__ == '__main__':
sources = getSources()
App(tkinter.Tk(), "DAC Gas Loader Cameras", sources)
Camera.py:
from threading import Thread
import tkinter as tk
import PIL.Image, PIL.ImageTk
from classes.VideoCapture import *
class Camera(tk.Frame):
def __init__(self, window, name="", video_source=0, width=400, height=300, fps=24):
super().__init__(window)
self.window = window
# determine what type of video source
self.video_source = video_source
if name != "PI Camera":
self.vid = WebcamVideoCapture(self.video_source, width, height)
else:
self.vid = PicamVideoCapture(width, height)
# webcam name
self.label = tk.Label(self, text=name)
self.label.pack()
# webcam window
self.canvas = tk.Canvas(self, width=width, height=height)
self.canvas.pack()
# start stream button
self.play_button = tk.Button(self, text="Stop", command=self.toggleCameraOnOff, cursor="hand2")
self.play_button.pack(anchor='center', side='left')
# snapshot button
self.snapshot_button = tk.Button(self, text="Snapshot", command=self.snapshot, cursor="hand2")
self.snapshot_button.pack(anchor='center', side='left')
# calculate delay for fps
self.fps = fps
self.delay = int(1000/self.fps)
print('[Camera] source:', self.video_source, 'fps:', self.fps, 'delay:', self.delay)
# image must persist in memory
self.image = None
self.imageID = None
# start thread for camera
self.running = True
self.thread = Thread(target=self.update_frame())
self.thread.start()
def toggleCameraOnOff(self):
# turn on camera
if not self.running:
self.running = True
self.update_frame()
self.play_button["text"] = "Stop"
# turn off camera
else:
self.running = False
self.play_button["text"] = "Start"
def snapshot(self):
self.vid.take_picture()
def update_frame(self):
# get a frame
ret, frame = self.vid.get_frame()
# add frame to canvas
if ret:
self.image = PIL.Image.fromarray(frame)
self.photo = PIL.ImageTk.PhotoImage(image=self.image)
if self.imageID != None: self.canvas.delete(self.imageID)
self.imageID = self.canvas.create_image(0, 0, image=self.photo, anchor='nw')
# recurse on function after delay
if self.running:
self.window.after(self.delay, self.update_frame)
def __del__(self):
# stop the thread
self.running = False
self.thread.join()
VideoCapture.py:
import cv2
import time
import os
from picamera2 import Picamera2
import PIL.Image, PIL.ImageTk
class PicamVideoCapture:
def __init__(self, width=400, height=300):
self.vid = Picamera2()
self.vid.configure(self.vid.create_preview_configuration(main={"size": (width, height)}))
self.vid.start()
self.ret = False
self.frame = None
def get_frame(self):
return True, self.vid.capture_array()
def take_picture(self):
os.makedirs(os.path.dirname("./images/"), exist_ok=True)
self.vid.capture_file(time.strftime("images/picam-%d-%m-%Y-%H-%M-%S.jpg"))
class WebcamVideoCapture:
def __init__(self, video_source=0, width=400, height=300):
self.video_source = video_source
self.width = width
self.height = height
self.ret = False
self.frame = None
# open video source
self.vid = cv2.VideoCapture(video_source)
self.vid.set(3, self.width)
self.vid.set(4, self.height)
if not self.vid.isOpened():
raise ValueError("[WebcamVideoCapture] Unable to open video source", video_source)
def get_frame(self):
ret, frame = self.vid.read()
# process image
if ret:
frame = cv2.resize(frame, (self.width, self.height))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# stream died
else:
print('[WebcamVideoCapture] stream end:', self.video_source)
ret = False
return ret, frame
def take_picture(self):
ret, frame = self.get_frame()
if ret:
frame = PIL.Image.fromarray(frame)
os.makedirs(os.path.dirname("./images/"), exist_ok=True)
frame.save(time.strftime("images/webcam-%d-%m-%Y-%H-%M-%S.jpg"))
def __del__(self):
# release stream
if self.vid.isOpened():
self.vid.release()
PiCameraControls.py:
import tkinter as tk
from gpiozero import PWMLED
class PiCameraControls(tk.Frame):
def __init__(self, window, camera, title="PI Camera Controls", ledPin = 4, sliderLength=450):
super().__init__(window)
# window settings
self.window = window
row = 0
column = 0
# title
self.title = tk.Label(self, text=title, font=("Helvetica: ", 14, "bold"))
self.title.grid(row=row, column=column, columnspan=2, sticky=tk.NW)
row+=1
# camera to control
self.camera = camera.vid.vid
self.led = PWMLED(ledPin)
self.led.value = 1
# brightness
self.brightness_header = tk.Label(self, text="Brightness: ", font=("Helvetica", 14))
self.brightness_header.grid(row=row, column=column, pady=(23,0), sticky=tk.W)
column += 1
self.brightness = tk.Scale(
self,
from_=-1,
to=1,
resolution=0.02,
orient=tk.HORIZONTAL,
command=self.setBrightness,
length=sliderLength,
cursor="hand2",
)
self.brightness.grid(row=row, column=column, padx=(0, 50), sticky=tk.W)
column += 1
self.brightness.set(-0.14) # paul's default
# self.brightness.set(self.camera.camera_controls["Brightness"][2]) # camera default
# contrast
self.contrast_header = tk.Label(self, text="Contrast", font=("Helvetica", 14))
self.contrast_header.grid(row=row, column=column, pady=(23,0), sticky=tk.W)
column += 1
self.contrast = tk.Scale(
self,
from_=0,
to=32,
resolution=0.1,
orient=tk.HORIZONTAL,
command=self.setContrast,
length=sliderLength,
cursor="hand2",
)
self.contrast.grid(row=row, column=column, padx=(0, 50), sticky=tk.W)
column += 1
self.contrast.set(3.4) # paul's default
# self.contrast.set(self.camera.camera_controls["Contrast"][2]) # camera default
# exposure
self.exposure_header = tk.Label(self, text="Exposure: ", font=("Helvetica", 14))
self.exposure_header.grid(row=row, column=column, pady=(23,0), sticky=tk.W)
column += 1
self.exposure = tk.Scale(
self,
from_=-8,
to=8,
resolution=0.1,
orient=tk.HORIZONTAL,
command=self.setExposure,
length=sliderLength,
cursor="hand2",
)
self.exposure.grid(row=row, column=column, padx=(0, 50), sticky=tk.W)
row += 1
column = 0
self.exposure.set(-2.0) # paul's default
# self.exposure.set(self.camera.camera_controls["ExposureValue"][2]) # camera default
# saturation
self.saturation_header = tk.Label(self, text="Saturation: ", font=("Helvetica", 14))
self.saturation_header.grid(row=row, column=column, pady=(23,0), sticky=tk.W)
column += 1
self.saturation = tk.Scale(
self,
from_=0,
to=32,
resolution=0.1,
orient=tk.HORIZONTAL,
command=self.setSaturation,
length=sliderLength,
cursor="hand2",
)
self.saturation.grid(row=row, column=column, padx=(0, 50), sticky=tk.W)
column += 1
self.saturation.set(0.9) # paul's default
# self.saturation.set(self.camera.camera_controls["Saturation"][2]) # camera default
# sharpness
self.sharpness_header = tk.Label(self, text="Sharpness: ", font=("Helvetica", 14))
self.sharpness_header.grid(row=row, column=column, pady=(23,0), sticky=tk.W)
column += 1
self.contrast = tk.Scale(
self,
from_=0,
to=16,
orient=tk.HORIZONTAL,
command=self.setSharpness,
length=sliderLength,
cursor="hand2",
)
self.contrast.grid(row=row, column=column, padx=(0, 50), sticky=tk.W)
column += 1
self.contrast.set(1) # paul's default
# self.contrast.set(self.camera.camera_controls["Sharpness"][2]) # camera default
# array of digital zoom sizes
self.zoomSizes = [ self.camera.capture_metadata()['ScalerCrop'] ]
size = self.zoomSizes[0][2:]
full_res = self.camera.camera_properties['PixelArraySize']
for _ in range(51):
# This syncs us to the arrival of a new camera frame:
self.camera.capture_metadata()
size = [int(s * .95) for s in size]
offset = [(r - s) // 2 for r, s in zip(full_res, size)]
self.zoomSizes.append(offset + size)
# zoom
self.zoom_header = tk.Label(self, text="Digital Zoom: ", font=("Helvetica", 14))
self.zoom_header.grid(row=row, column=column, pady=(23,0), sticky=tk.W)
column += 1
self.zoom = tk.Scale(
self,
from_=0,
to=50,
orient=tk.HORIZONTAL,
command=self.setZoom,
length=sliderLength,
cursor="hand2",
)
self.zoom.grid(row=row, column=column, padx=(0, 50), sticky=tk.W)
row += 1
column = 0
self.zoom.set(0) # default no zoom
# led
self.led_header = tk.Label(self, text="LED Brightness: ", font=("Helvetica", 14))
self.led_header.grid(row=row, column=column, pady=(23,0), sticky=tk.W)
column += 1
self.led_slider = tk.Scale(
self,
from_=0,
to=100,
orient=tk.HORIZONTAL,
command=self.setLED,
length=sliderLength,
cursor="hand2",
)
self.led_slider.grid(row=row, column=column, padx=(0, 50), sticky=tk.W)
self.led_slider.set(100) # default
def setBrightness(self, value):
self.camera.set_controls({"Brightness": float(value)})
def setContrast(self, value):
self.camera.set_controls({"Contrast": float(value)})
def setExposure(self, value):
self.camera.set_controls({"ExposureValue": float(value)})
def setSaturation(self, value):
self.camera.set_controls({"Saturation": float(value)})
def setSharpness(self, value):
self.camera.set_controls({"Sharpness": float(value)})
def setZoom(self, value):
self.camera.set_controls({"ScalerCrop": self.zoomSizes[int(value)]})
def setLED(self, value):
self.led.value = int(value) / 100
def delete(self):
self.led.value = 0
I have monitored memory/cpu/interrupts/context switches none seem to be raise as the program runs. I have tried changing the nice value and priority of the program to -20 and neither seem to do anything.
Alex StAubin is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.