Search code examples
pythonpython-3.xopencvpython-multithreadingcircular-buffer

blank image capture with opencv threading


I am trying to write the frame captured from my laptop camera in to a circular buffer. I wanted to create a thread to handle this task (in an asynchronous way). When i try to get the last frame stored with get_last() method and show it, black screen is displayed. I have spent quite some time figuring out the reason, but all in vein. IMHO there should be a small issue with this code, but cannot see it. Any help will be highly appreciated. Complete working code is shown below:

import cv2
import sys
import time
import threading

class CamBuffer(threading.Thread):
    def __init__(self, stream=0):
        self.current = 0
        self.max_size = 5
        self.buffer = [None] * self.max_size
        self.capture = cv2.VideoCapture(stream)
        if not self.capture.isOpened():
            print("Could not open video")
            sys.exit()

        print("Opened video")
        self.running = True
        super().__init__()

    def run(self):
        print('running thread')
        while self.running:
            print('capturing frame ', self.current)
            _, frame = self.capture.read()
            if _:
                print('saving frame ', self.current)
                self.buffer[self.current] = frame
                self.current = self.current + 1
                if (self.current >= self.max_size):
                    self.current = 0
        self.capture.release()
        print('stopped thread')

    def terminate(self):
        print('terminating thread')
        self.running = False

    def get_last(self):
        current = 0
        if self.current > 0:
            current = self.current - 1

        print('get_last()', current)
        return self.buffer[current]
        

if __name__ == "__main__":
    print('Frame buffer test')
    stream = 0
    cb = CamBuffer(stream)
    cb.start()
    time.sleep(1.25)

    frame = cb.get_last()
    if frame is not None:
        print('showing frame')
        cv2.imshow('Frame', frame)

    time.sleep(3)
    cb.terminate()
    cv2.destroyAllWindows()
    print('Frame buffer test [Done]')

Solution

  • The simplest solution that worked for me was just to add cv.waitKey(1) after imshow:

     if frame is not None:
            print('showing frame')
            cv2.imshow('Frame', frame)
            cv2.waitKey(1):
    

    I think that's an issue with Opencv highgui module - it's a GUI and has its own thread, too, sometimes it could hang the application. The gray screen appears when trying to update a Highgui window/cv2.imshow() too quickly, I've encountered it in C++ as well. I guess something about locking the memory of the GUI thread and the image.

    BTW, you could also add waiting in the capturing thread (if you don't need maximum framerate).

    PS. Before realizing it could be solved with that line, I experimented with other possible issues: a lock.acquire/release, a global buffer, sending a buffer container as a parameter. However the global also was showing gray screen (although during the capturing it displayed captured images). It finally worked with buffer sent as a parameter, when I added cv2.waitKey(...) and I realized that it may be enough anyway.

    The other experiments:

    import cv2
    import sys
    import time
    import threading
    
    max_size = 5
    buffer = [None] * max_size
    frCopyGlobal = None
    buff = [None] * max_size #call as param
    
    
    class CamBuffer(threading.Thread):
        def __init__(self, buff, stream=0):
            self.current = 0
            self.max_size = 5
            self.buffer = [None] * self.max_size
            self.capture = cv2.VideoCapture(stream, apiPreference=cv2.CAP_DSHOW)
            if not self.capture.isOpened():
                print("Could not open video")
                sys.exit()
    
            print("Opened video")
            self.running = True
            super().__init__()
    
        def run(self):
            print('running thread')
            
            while self.running:        
                #lock.acquire()
                print('capturing frame ', self.current)
                _, frame = self.capture.read()
                cv2.imshow("F", frame)
                if _:
                    #print('saving frame ', self.current%self.max_size, self.current)
                    print('saving frame ', self.current)                
                    frCopy = frame.copy()
                    self.buffer[self.current] = frCopy
                    
                    frCopyGlobal = frame.copy()
                    buffer[self.current] = frCopyGlobal                
                    
                    buff[self.current] = frame.copy()
                    
                    #self.buffer[self.current%self.max_size] = frame
                    #cv2.imshow("FBUFF", self.buffer[self.current%self.max_size])
                    cv2.imshow("FBUFF", self.buffer[self.current])
                    cv2.imshow("GLOBAL BUFF", buffer[self.current])
                    cv2.imshow("Param BUFF", buff[self.current])
                    self.current = self.current + 1
                    if (self.current >= self.max_size):
                        self.current = 0
                cv2.waitKey(66)
                #lock.release()
            self.capture.release()
            print('stopped thread')
    
        def terminate(self):
            print('terminating thread')
            self.running = False
    
        def get_last(self):
            current = 0
            if self.current > 0:
                current = self.current - 1
    
            print('get_last()', current)
            for i in self.buffer:
              cv2.imshow("THREAD: "+str(i), i)
            for i in buffer:
              cv2.imshow("GLOBAL: "+str(i), i)
            return self.buffer[current]
            
    
    if __name__ == "__main__":
        lock = threading.Lock() #threading.Lock lock();
        print('Frame buffer test')
        stream = 0    
        cb = CamBuffer(buff, stream) # The buffer must be in common memory in order to read it from the calling thread
        cb.start()
        time.sleep(1.0)
        for i in range(10):
          try:
            cv2.imshow("SAMPLE?", buff[1])
          finally:
                  pass
          cv2.waitKey(15)
        time.sleep(1.25)
        #cb.join()
        #cb.sleep(1.)
        #cb.terminate()
        #frame = cb.get_last()
        cb.running = False
        ###lock.acquire()
        #frame = cb.buffer[0]
        frame = buff[0]    
        #frame = buff[0]    
        frame = cb.buffer[0]
        if frame is not None:
            print('Showing frame from Thread')
            cv2.imshow('PARAMETER BUFFER?', frame)
        cv2.waitKey(500)
        #frame = buffer[0] #Global
        #if frame is not None:
        #    print('showing frame From GLOBAL')
        #    cv2.imshow('GLOBAL FRAME', frame)
            
        ###lock.release()
        time.sleep(3)
        cb.terminate()    
        cv2.destroyAllWindows()
        
        print('Frame buffer test [Done]')
        cb.join()
        
        #exit(0)