import cv2 import time from threading import Thread from queue import Queue class WebcamVideoCapture: """Webcam capturing class utilizing threading and the queue to obtain FPS speedup.""" def __init__(self, src=0, fourcc=None, width=None, height=None, fps=None, transform=None, queue_size=128, name="WebcamVideoCapture"): self.cap = cv2.VideoCapture(src) if not self.cap.isOpened(): raise IOError(f"Cannot open video {src}") # Set capture properties if fourcc: self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*fourcc)) if width: self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) if height: self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) if fps: self.cap.set(cv2.CAP_PROP_FPS, fps) self.transform = transform # initialize the queue used to store frames read from the webcam self.queue = Queue(maxsize=queue_size) # initialize the variable used to indicate if the thread should be stopped self.stopped = False self.thread = Thread(target=self.update, args=(), name=name) self.thread.daemon = True def start(self): # start a thread to read frames from the file video stream self.thread.start() return self def get(self, cv2_prop): return self.cap.get(cv2_prop) def update(self): # keep looping infinitely until the thread is stopped while self.cap.isOpened(): # if the thread indicator variable is set, stop the thread if self.stopped: break # drop the last frame if the queue is full if self.queue.full(): self.queue.get() # otherwise, ensure the queue has room in it if not self.queue.full(): # read the next frame from the file (grabbed, frame) = self.cap.read() # if the `grabbed` boolean is `False`, then we have reached the end of the video file if not grabbed: self.stopped = True break # if there are transforms to be done, might as well # do them on producer thread before handing back to # consumer thread. ie. Usually the producer is so far # ahead of consumer that we have time to spare. # # Python is not parallel but the transform operations # are usually OpenCV native so release the GIL. # # Really just trying to avoid spinning up additional # native threads and overheads of additional # producer/consumer queues since this one was generally # idle grabbing frames. if self.transform: frame = self.transform(frame) # add the frame to the queue self.queue.put(frame) else: time.sleep(0.01) # Rest for 1ms, we have a full queue self.cap.release() def read(self): # return next frame in the queue return self.queue.get() # Insufficient to have consumer use while(more()) which does # not take into account if the producer has reached end of # file stream. def running(self): return self.more() or not self.stopped def more(self): # return True if there are still frames in the queue. If stream is not stopped, try to wait a moment tries = 0 while self.queue.qsize() == 0 and not self.stopped and tries < 5: time.sleep(0.1) tries += 1 return self.queue.qsize() > 0 def stop(self): # indicate that the thread should be stopped self.stopped = True self.thread.join()