The requirement said two cameras, entry and exit, live at once, and a naive loop reading one camera then the other then updating the interface produces a slideshow that freezes whenever recognition thinks. Real-time multi-camera work is a threading problem before it is a vision problem, and this post is the CameraManager that runs each camera in its own thread, with the two design choices, a tiny frame queue and a detection cooldown, that made it smooth on modest hardware.
Each camera gets its own manager, and each manager its own capture thread, this is the real class from the app:
import cv2, threading, queue
class CameraManager:
def __init__(self, camera_id, name, face_recognizer, db_manager):
self.camera_id = camera_id
self.name = name # 'Entry' or 'Exit'
self.face_recognizer = face_recognizer
self.db_manager = db_manager
self.cap = None
self.running = False
self.frame_queue = queue.Queue(maxsize=2)
self.last_detection_time = {}
self.detection_cooldown = 5 # seconds
def start(self):
self.cap = cv2.VideoCapture(self.camera_id)
if not self.cap.isOpened():
return False
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
self.running = True
self.capture_thread = threading.Thread(target=self._capture_frames)
self.capture_thread.daemon = True
self.capture_thread.start()
return True
def stop(self):
self.running = False
if self.cap:
self.cap.release()
The choices in that code are the post. Resolution is pinned to 640 by 480, because recognition quality saturates while processing cost keeps climbing with pixels, and this size keeps two streams real-time on ordinary hardware, the honest trade chosen deliberately. The capture thread is a daemon, so a closed app never leaves camera threads holding the process alive, and start returns False when the camera fails to open, surfaced in the interface rather than swallowed, a camera that is not there is a message, not a mystery.
The queue is the subtle heart. frame_queue with maxsize equal to two is a deliberate bottleneck, the capture thread drops old frames when the queue is full instead of piling up a backlog, because for live monitoring the only interesting frame is the newest one, and a system that queues every frame falls steadily behind reality until its alerts describe a minute ago. Bounded queue, drop the stale, stay in the present. And the cooldown dictionary is the logging sanity, a person standing at the camera is recognised in frame after frame, and without the five-second per-person cooldown, one arrival would write dozens of log rows, last_detection_time remembers who was just seen and suppresses the repeats, one presence, one log entry, which the database post relies on.
A few things people ask me about this
Why does my camera app lag further behind the longer it runs? An unbounded frame queue, you are processing history. Bound the queue small and drop old frames when full, live systems should always eat the newest frame.
Why is the same person logged thirty times in one minute? No detection cooldown, every frame re-recognises them. Track last seen time per name and suppress repeats within a window, five seconds suits a doorway.
Next
With the cameras smooth, the system’s judgment calls come due, what happens on an unknown face, and how honest to be about accuracy. That is the next post.
