0

I have two threads, one for capturing frames from a high frame rate camera (producter) and the other for writing frames to video files (consumer). I use Queue to transfer frames from producter to consumer. At first, my code was like this:

# consumer
class VideoWriter:
...
    def record(self):
        while self.isRecording or not self.frame_queue.empty():
            if not self.frame_queue.empty():
                writer.imwrite(self.frame_queue.get())
...
# producter
class MyVideo:
...
    def play(self):
        while self.isLiving:
            if self.cap.IsGrabbing():
                frame = self.cap.RetrieveResult(1000)
                if self.video_writer.isRecording:
                    self.video_writer.frame_queue.put(frame)
                show_frame(frame)
...

When I don't start recording, everything in the code is normal. However, when I started the consumer thread(started recording), the producer thread began to become very sluggish, and the consumer thread received far fewer frames than the producer thread captured from the camera. Then I fixed this problem by changing my codes into following:

# consumer
class VideoWriter:
...
    def record(self):
        while self.isRecording or not self.frame_queue.empty():
            try:
                writer.imwrite(self.frame_queue.get(block=False))
            except Empty:
                time.sleep(0.001)
...
# producter
class MyVideo:
...
    def play(self):
        while self.isLiving:
            if self.cap.IsGrabbing():
                frame = self.cap.RetrieveResult(1000)
                if self.video_writer.isRecording:
                    self.video_writer.frame_queue.put(frame,block=False)
                show_frame(frame)
...

Now, everything is fine. But I don't know the reason why this problem occured. Can someone explain to me why?

  • 1
    Maybe the consumer loop is to busy as it keeps looping even when the queue is empty and there's nothing to do, in which case what actually helps is the sleep you added. Have you tried removing the second `if not self.frame_queue.empty():` and just let the consumer block? You can put None or some custom object into the queue as a finalization signal. – Coder Cat Jun 20 '23 at 10:34

1 Answers1

1

As suggested in comments, the consumer thread spins idly and takes away resources from the producer thread through the global interpreter lock. A clean solution blocks the consumer until the producer does something. This means all communication with the producer must happen through the queue. Quick and easy would be using None to signal the end of the video stream. Of course more complex designs such as the Command pattern are also possible.

Here is a simple fix:

class VideoWriter:
...
    def record(self):
        while (frame := self.frame_queue.get()) is not None:
            writer.imwrite(frame)

class MyVideo:
...
    def play(self):
        try:
            while self.isLiving:
                if self.cap.IsGrabbing():
                    frame = self.cap.RetrieveResult(1000)
                    if self.video_writer.isRecording:
                        self.video_writer.frame_queue.put(frame,block=False)
                    show_frame(frame)
        finally:
            self.video_writer.frame_queue.put(None) # signal end

The isLiving and IsGrabbing conditions still look like the thread is spinning which similarly wastes CPU time. Fixing this affects the larger system design. The easiest is probably starting and stopping the producer thread as a whole when grabbing is started/stopped.

Homer512
  • 9,144
  • 2
  • 8
  • 25