33

I want to connect to a camera, and only capture a frame when an event happens (e.g. keypress). A simplified version of what I'd like to do is this:

cap = cv2.VideoCapture(device_id)

while True:
    if event:
        img = cap.read()
        preprocess(img)

    process(img)
    cv.Waitkey(10)

However, cap.read seems to only capture the next frame in the queue, and not the latest. I did a lot of searching online, and there seems to be a lot of questions on this but no definitive answer. Only some dirty hacks which involve opening and closing the capture device just before and after grabbing (which won't work for me as my event might be triggered multiple times per second); or assuming a fixed framerate and reading a fixed-n times on each event (which won't work for me as my event is unpredictable and could happen at any interval).

A nice solution would be:

while True:
    if event:
        while capture_has_frames:
            img = cap.read()
        preprocess(img)

    process(img)
    cv.Waitkey(10)

But what is capture_has_frames? Is it possible to get that info? I tried looking into CV_CAP_PROP_POS_FRAMES but it's always -1.

For now I have a separate thread where the capture is running at full fps, and on my event I'm grabbing the latest image from that thread, but this seems overkill.

(I'm on Ubuntu 16.04 btw, but I guess it shouldn't matter. I'm also using pyqtgraph for display)

Christoph Rackwitz
  • 11,317
  • 4
  • 27
  • 36
memo
  • 3,554
  • 4
  • 31
  • 36

5 Answers5

44

I think the solution mentioned in the question, namely having a separate thread that clears the buffer, is the easiest non-brittle solution for this. Here reasonably nice (I think) code for this:

import cv2, queue, threading, time

# bufferless VideoCapture
class VideoCapture:

  def __init__(self, name):
    self.cap = cv2.VideoCapture(name)
    self.q = queue.Queue()
    t = threading.Thread(target=self._reader)
    t.daemon = True
    t.start()

  # read frames as soon as they are available, keeping only most recent one
  def _reader(self):
    while True:
      ret, frame = self.cap.read()
      if not ret:
        break
      if not self.q.empty():
        try:
          self.q.get_nowait()   # discard previous (unprocessed) frame
        except queue.Empty:
          pass
      self.q.put(frame)

  def read(self):
    return self.q.get()

cap = VideoCapture(0)
while True:
  time.sleep(.5)   # simulate time between events
  frame = cap.read()
  cv2.imshow("frame", frame)
  if chr(cv2.waitKey(1)&255) == 'q':
    break

The frame reader thread is encapsulated inside the custom VideoCapture class, and communication with the main thread is via a queue.

I posted very similar code for a node.js question, where a JavaScript solution would have been better. My comments on another answer to that question give details why a non-brittle solution without separate thread seems difficult.

An alternative solution that is easier but supported only for some OpenCV backends is using CAP_PROP_BUFFERSIZE. The 2.4 docs state it is "only supported by DC1394 [Firewire] v 2.x backend currently." For Linux backend V4L, according to a comment in the 3.4.5 code, support was added on 9 Mar 2018, but I got VIDEOIO ERROR: V4L: Property <unknown property string>(38) not supported by device for exactly this backend. It may be worth a try first; the code is as easy as this:

cap.set(cv2.CAP_PROP_BUFFERSIZE, 0)
Megan Caithlyn
  • 374
  • 2
  • 11
  • 33
Ulrich Stern
  • 10,761
  • 5
  • 55
  • 76
  • 4
    Although in a perfect world there would be a more elegant way to get the latest frame data, this does the trick! – Woohoojin Feb 19 '19 at 06:09
  • 5
    @ChristianScillitoe, in a perfect world, one makes use of all camera capabilities via manufacturer's API and uses provided camera trigger to actually "capture a frame when an event happens". `cv2.VideoCapture` instead is a compact cross-platform generic class provided for convenience and fast prototyping. – mainactual Feb 19 '19 at 08:05
  • 1
    @mainactual, I am a fan of having a cross-platform device-agnostic API like OpenCV to handle video capture. Also, while OpenCV's HighGUI module [was designed for prototyping](https://docs.opencv.org/2.4/modules/highgui/doc/highgui.html), the Video I/O module (`VideoCapture`, etc.) was not. – Ulrich Stern Feb 19 '19 at 15:29
  • @UlrichStern as your link tells, the VideoCapture used to be a part of 'Matlab-like' highgui-module until 3.0 and while I'm not sure how much it's changed thereafter, and as much I'm a fan of it, it still implements only a subset of the underlying backend's capabilities, e.g. those of FFMPEG. – mainactual Feb 19 '19 at 19:06
  • Thanks @UlrichStern. Can I set backend to DC1394 [Firewire] v 2.x backend in python-opencv 4.1.2 on Ubuntu 18.04? – fisakhan Feb 06 '20 at 10:23
  • 3
    @mainactual, I get what you're saying, but if VideoCapture wasn't intended to provide precise timing control, then what purpose do the grab() and retrieve() methods serve? Aren't they supposed to provide exactly that kind of control? But what good are they if you can't be sure that the image you retrieve is the same image you grabbed? I feel like I'm misunderstanding something. – TrespassersW Feb 11 '20 at 22:15
  • cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) doesn't work for me. It doesn't seem to have any affect at all. It seems to help quite a bit when I set it to 1, but doesn't fix the problem entirely. I wonder if zero isn't allowed on some cameras? – TrespassersW Feb 11 '20 at 22:25
  • @TrespassersW there are some design opportunities across video capture backends, e.g., MSMF `grab` calls asynchronous `ReadSample`, so it makes sense to have them separate and encourage to use them separate. But anyway, I measure precise timing in microseconds and to reach that level of synchronization requires explicit control of the camera trigger instead of free-rolling capture. – mainactual Feb 12 '20 at 11:04
  • @mainactual In my case, I don't require that kind of precision. I just need some way of ensuring that the image I receive was from at or after a certain time. For this I'm not sure that I need to control the camera trigger. Knowing that the retrieve call matches up with the grab would be enough, I think. (I hope I'm not coming across like I'm complaining about the wonderful work that has been done on OpenCV. I'm just trying to understand it so I can see if it meets my needs) – TrespassersW Feb 12 '20 at 15:54
  • @TrespassersW how well it fits your purpose, depends on the capture backend. My current camera has readout-time 24 ms and transfer time 6 ms (USB3.0). Also, the driver takes some remarkable time to fill the buffer. It would be hard to manage this overhead without sending each trigger explicitly, but just querying the latest frame captured. Also with the additional queue explained in this answer. – mainactual Feb 12 '20 at 19:10
  • 1
    For python 3, you want to do `import queue as Queue` instead of `import Queue` – warriorUSP Apr 14 '20 at 22:31
  • 1
    I think this is very processor inefficient... Maybe a better approach would be to call cap.grab() some times (I think 5 is the buffer size) and then call cap.retrieve(), https://docs.opencv.org/2.4/modules/highgui/doc/reading_and_writing_images_and_video.html – Bersan Apr 25 '20 at 17:52
  • Why use Python 2? It's obsolete! – Megan Caithlyn Jul 13 '20 at 20:24
  • 1
    @UlrichStern after 2-3 min on my Jetson I have big latency again.. any idea how to fix that? – Roy Amoyal Apr 10 '22 at 09:48
  • @RoyAmoyal have been able to fix that? thanks – gianni Sep 04 '22 at 20:30
10

Here's a simplified version of Ulrich's solution. OpenCV's read() function combines grab() and retrieve() in one call, where grab just loads the next frame in memory, and retrieve decodes the latest grabbed frame (demosaicing & motion jpeg decompression).

We're only interested in decoding the frame we're actually reading, so this solution saves some CPU, and removes the need for a queue

import cv2
import threading


# bufferless VideoCapture
class VideoCapture:
    def __init__(self, name):
        self.cap = cv2.VideoCapture(name)
        self.lock = threading.Lock()
        self.t = threading.Thread(target=self._reader)
        self.t.daemon = True
        self.t.start()

    # grab frames as soon as they are available
    def _reader(self):
        while True:
            with self.lock:
                ret = self.cap.grab()
            if not ret:
                break

    # retrieve latest frame
    def read(self):
        with self.lock:
            _, frame = self.cap.retrieve()
        return frame

EDIT: Following Arthur Tacca's comment, added a lock to avoid simultaneous grab & retrieve, which could lead to a crash as OpenCV isn't thread-safe.

Bruno Degomme
  • 883
  • 10
  • 11
2

Its also possible to always get the latest frame by using cv2.CAP_GSTREAMER backend. If you have gstreamer support enabled in cv2.getBuildInformation(), you can initialize your video capture with the appsink parameters sync=false and drop=true

Example:

cv2.VideoCapture("rtspsrc location=rtsp://... ! decodebin ! videoconvert ! video/x-raw,framerate=30/1 ! appsink drop=true sync=false", cv2.CAP_GSTREAMER)
jvx8ss
  • 529
  • 2
  • 12
1

On my Raspberry Pi 4,

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

does work and was all that I needed for my pi camera to give me the latest frame, with a consistent 3+ second delay between the scene in front of the camera and displaying that scene in the preview image. My code takes 1.3 seconds to process an image, so I'm not sure why the other 2 seconds of delay are present, but it's consistent and works.

Side note: since my code takes over a second to process an image, I also added

cap.set( cv2.CAP_PROP_FPS, 2 )

in case it reduces any unneeded activity, since I can't quite get a frame a second. When I put cv2.CAP_PROP_FPS to 1, though, I got a strange output of all my frames being almost entirely dark, so setting FPS too low can cause an issue

RealHandy
  • 534
  • 3
  • 8
  • 27
  • 1
    Please don't add the same answer to multiple questions. Answer the best one and flag the rest as duplicates. See [Is it acceptable to add a duplicate answer to several questions?](//meta.stackexchange.com/q/104227) – Machavity Jul 23 '20 at 21:50
  • @Machavity Yep, I know not to do that normally, but in this case, I posted the duplicate in the other question, pointing to this question, Ulrich Stern's answer here, and the fact that the other might be a dup, because the other might not be a dup: the other question specifically uses an IP camera, not the local camera, and I'm not in a position to be able to tell whether the answers here work for an IP camera. Which is why I think others who thought it might be a dup were leaving it open. – RealHandy Jul 24 '20 at 14:22
-2

If you don't want to capture the frame when there is no event happening, why are you preprocessing/processing your frame? If you do not process your frame, you can simply discard it unless the event occur. Your program should be able to capture, evaluate your condition and discard at a sufficient speed, i.e. fast enough compared to your camera FPS capture rate, to always get the last frame in the queue.

If not proficient in python because I do my OpenCV in C++, but it should look similar to this:

vidcap = cv.VideoCapture(   filename    )

while True:
    success, frame = vidcap.read()
    If Not success:
         break
    If cv.waitKey(1):
         process(frame)

As per OpenCV reference, vidcap.read() returns a bool. If frame is read correctly, it will be True. Then, the captured frame is store in variable frame. If there is no key press, the loop keeps on going. When a key is pressed, you process your last captured frame.

imtheref
  • 61
  • 2
  • 3
  • 1
    You're misinterpreting the question. The issue OP is having is that `vidcap.read()` returns the next frame on the buffer, so if they spend a lot of time with the previous frame, `vidcap.read()` will not yield the latest frame, but instead yield the next frame in the buffer – Woohoojin Feb 18 '19 at 15:27
  • @Christian Scillitoe: Perhaps, I was. Maybe memo should use a GPU to speedup the image processing. This will happen simultaneously as the capture and no need to create additional thread manually (heterogeneous computing). – imtheref Feb 19 '19 at 19:20