0

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]')
RVRSO Bot
  • 109
  • 4
Imran
  • 642
  • 6
  • 25
  • You are obliged to call `cv2.waitKey()` after `cv2.imshow()` because it uses that time to update the display - I found I needed at least 30 as the parameter to `cv2.waitKey()` some while back on macOS so maybe start with 30. You should not be calling `time.sleep()` in your code anywhere. – Mark Setchell Jan 18 '21 at 14:40

1 Answers1

1

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)
Twenkid
  • 825
  • 7
  • 15
  • 1
    instead of cv.waitKey(1) it should be cv2.waitKey(1). cannot submit a small edit, so putting in comments – Imran Jan 18 '21 at 13:35
  • by the way your simple fix works only if I wait for rather long time (>100 in arguments to waitkey() ). But your logic IMHO does not apply as i am not updating the highgui at a faster rate. I am simply grabbing a single frame and showing it, Right? – Imran Jan 18 '21 at 13:37
  • Adding waiting in the capturing process segfaults and errors like"saving frame 4 QObject::startTimer: Timers cannot be started from another thread capturing frame 0 saving frame 0 QObject::startTimer: Timers cannot be started from another thread capturing frame 1 saving frame 1 QObject::startTimer: Timers cannot be started from another thread terminating thread Segmentation fault (core dumped) " – Imran Jan 18 '21 at 13:39
  • Sure, cv2.waitKey(). Run my code, on my setup, it suggests other possible reasons, it works fine with waitKey(1), but mine also has wait for the frame capture as suggested - your code is "starving" the task scheduler, that's like While True: ... The other threads (including that probably) have not time slot left. As of highgui - it is updated, imshow() tries to read data but while it starts the control is switched to the other thread which is using that data and locks it. – Twenkid Jan 18 '21 at 13:46
  • Use cv2.waitKey() and run my code, it has that (but you may need to clean some experiments) and there are no errors. – Twenkid Jan 18 '21 at 13:46
  • QObject - Qt. Maybe it needs more comprehensive synchronization, see the other data exchange variants (there are some excessive copying of frames which could be commented). BTW: "I wait for rather long time (>100 in arguments to waitkey()). " Then that's either a thread synchronization issue or your system can't keep up reading the frames, but if that wait is in the main thread only - isn't the main thread doing nothing in that time, while the capture is working in the background (and there's 3 second sleep) - it doesn't matter in that very case. – Twenkid Jan 18 '21 at 14:00
  • I tried again: just cv2.waitKey(1) on the main thread is fine on my setup, even if pumping frames at full speed. I guess QT is the bug. Try to reinstall opencv with another highgui backend. – Twenkid Jan 18 '21 at 14:07