Why is my camera slow on mac m2

I’m capturing from a global-shutter, b&w uvc camera.
On Linux and Windows I can capture about 120 fps which the camera is advertised at but under Mac OS I can only get 60 fps.

Does the camera suck or does my OS suck?

import cv2
import time

cap = cv2.VideoCapture(0)
cap.grab()
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 800)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 600)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 100)
cap.set(cv2.CAP_PROP_FPS, 120)

prevTime = 0
currTime = 0
fps = 0
f = 0
total_fps = 0
print(f"CV@ Version: {cv2.__version__}")
print(f"            Current Time Previous Time  Difference FPS")
while cap.isOpened():
    if f > 20:
      break
    currTime = time.time()
    diff = currTime - prevTime
    fps = 1 / (diff)
    prevTime = currTime
    total_fps += fps
    if f> 0:
      print(f"frame #: {f:2.0f}  {currTime:10.9f} {prevTime:10.9f} diff: {diff:10.9f} FPS: {fps}")
    f += 1

    ret, frame = cap.read()
    if not ret:
        break
    cv2.imshow("frame", frame)
    if cv2.waitKey(1) == ord("q"):
        break
avg = total_fps/f
print(f"avg fps: {avg:2.0f} ")
cap.release()
cv2.destroyAllWindows()

Example output:

[ WARN:0@0.017] global cap_gstreamer.cpp:1173 isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created
CV@ Version: 4.10.0-dev
            Current Time Previous Time  Difference FPS
frame #:  1  1733967070.464645147 1733967070.464645147 diff: 0.182254076 FPS: 5.48684573714498
frame #:  2  1733967070.482582092 1733967070.482582092 diff: 0.017936945 FPS: 55.750854013531296
frame #:  3  1733967070.499706030 1733967070.499706030 diff: 0.017123938 FPS: 58.39778343984518
frame #:  4  1733967070.520403862 1733967070.520403862 diff: 0.020697832 FPS: 48.3142386508933
frame #:  5  1733967070.537660837 1733967070.537660837 diff: 0.017256975 FPS: 57.94758292922176
frame #:  6  1733967070.553637981 1733967070.553637981 diff: 0.015977144 FPS: 62.58940802530852
frame #:  7  1733967070.569579124 1733967070.569579124 diff: 0.015941143 FPS: 62.730758876491876
frame #:  8  1733967070.587990999 1733967070.587990999 diff: 0.018411875 FPS: 54.312774360634506
frame #:  9  1733967070.611443996 1733967070.611443996 diff: 0.023452997 FPS: 42.638473502831175
frame #: 10  1733967070.625786066 1733967070.625786066 diff: 0.014342070 FPS: 69.72494389493808
frame #: 11  1733967070.641560078 1733967070.641560078 diff: 0.015774012 FPS: 63.39541421683469
frame #: 12  1733967070.657384872 1733967070.657384872 diff: 0.015824795 FPS: 63.19197276041824
frame #: 13  1733967070.673436880 1733967070.673436880 diff: 0.016052008 FPS: 62.29750323050188
frame #: 14  1733967070.689905882 1733967070.689905882 diff: 0.016469002 FPS: 60.720134344779666
frame #: 15  1733967070.706533909 1733967070.706533909 diff: 0.016628027 FPS: 60.13942617897137
frame #: 16  1733967070.721325159 1733967070.721325159 diff: 0.014791250 FPS: 67.6075371943455
frame #: 17  1733967070.740059853 1733967070.740059853 diff: 0.018734694 FPS: 53.376907316204075
frame #: 18  1733967070.761252165 1733967070.761252165 diff: 0.021192312 FPS: 47.186922722107845
frame #: 19  1733967070.777266026 1733967070.777266026 diff: 0.016013861 FPS: 62.44590349427546

it’s always the same thing: set CAP_PROP_FOURCC.

and never ever use time.time() for such measurements. use time.perf_counter() or time.perf_counter_ns(). and remember, things on a computer run in time slices. anything can happen to be delayed by some amount of time, sometimes by whole time quanta (time slices)

that 0xFF has to go. remove it.

I switched to time.perf_counter_ns but I still get 60fps calculated while cap.get(cv2.CAP_PROP_FPS) says 120fps.

I am not even writing the file yet, just trying to pull as many frames as possible for later analysis.

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('m','j','p','g'))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 800)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 600)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 100)
cap.set(cv2.CAP_PROP_FPS, 120)
fps = 0
frames = 0
start_time = time.perf_counter_ns()
print(f"CV@ Version: {cv2.__version__}")
print(f"            Current Time Previous Time  Difference FPS")
while cap.isOpened():
   frames += 1
   elapsed = time.perf_counter_ns() - start_time
   if elapsed >= 1_000_000_000:
     fps = frames
     print(f"FPS: per cap.get(cv2.CAP_PROP_FPS):  {cap.get(cv2.CAP_PROP_FPS)}, calculated: {fps}")

     frames = 0
     start_time = time.perf_counter_ns()

   ret, frame = cap.read()
   frame.flags.writeable = False
   if not ret:
       break
   cv2.imshow("frame", frame)
   if cv2.waitKey(1) == 113:
       break
cap.release()
out.release()
cv2.destroyAllWindows()

which says

CV@ Version: 4.10.0-dev
            Current Time Previous Time  Difference FPS
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 55
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 58
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 60
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 57
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 60
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 61
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 60
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 62
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 60

that is good, but maybe the code has to be capitalized like cv.VideoWriter.fourcc(*"MJPG")

also put this set() call last. one of those orders should work, but the order matters, so any other order might fail to set this mode.

remove the waitKey() and imshow() and other GUI presentation. then do your timing. waitKey might cause a delay of an entire scheduler time slice, which can be on the order of 64 fps, e.g. on Windows.

enumerate the video modes of the camera:

https://trac.ffmpeg.org/wiki/Capture/Webcam

To me it looks like it’s purely the waitkey().
But if I remove the waitkey, I dont see any imshow window at all.
waitkey takes an integer, If waitkey <= 0 it waits indefinitely, so I can only capture blindly?

Neither the order or the lower/uppercase seems to make a difference.
Even when writing the file I now see 120 fps calculated.

[ WARN:0@0.017] global cap_gstreamer.cpp:1173 isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created
CV Version: 4.10.0-dev
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 108
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 122
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 122
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 121
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 122
FPS: per cap.get(cv2.CAP_PROP_FPS):  120.00048, calculated: 120
out = cv2.VideoWriter('out.mp4', cv2.VideoWriter.fourcc('m','p','4','v'), 120.0, (800, 600))

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('m','j','p','g'))
#cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc(*"MJPG"))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 800)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 600)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
cap.set(cv2.CAP_PROP_FPS, 120)

fps = 0
frames = 0
start_time = time.perf_counter_ns()
print(f"CV Version: {cv2.__version__}")

while cap.isOpened():
    frames += 1
    elapsed = time.perf_counter_ns() - start_time
    if elapsed >= 1_000_000_000:
        fps = frames
        print(f"FPS: per cap.get(cv2.CAP_PROP_FPS):  {cap.get(cv2.CAP_PROP_FPS)}, calculated: {fps}")

        frames = 0
        start_time = time.perf_counter_ns()

    ret, frame = cap.read()
    frame.flags.writeable = False
    if not ret:
        break
    
    cv2.imshow("frame", frame)
    out.write(frame)
    #if cv2.waitKey(1) == 113:
    #    break

cap.release()
out.release()
cv2.destroyAllWindows()

great to hear.

so the issue is the GUI.

swap waitKey() for pollKey(), which is supposed to be the alternative that doesn’t wait for something to happen.

very likely it’s a fallback to waitKey(1), and will behave accordingly. I don’t know if anyone has implemented it properly yet. AFAIK pollKey only does anything special on Win32. if you know some Apple GUI programming, you can dig around in OpenCV’s highgui and implement pollKey() yourself.

or you can display every other, third, … frame only.

or you can use Apple-native APIs yourself to do the GUI.

ok, (as far as the imshow limitation) that’s fine. For now I just want to validate that these cheap cameras work as advertised and I can get enough data out of them.

I can work on the GUI later after I process the very short (< 3sec sports movement) captures.

These will be short sports movements.

if you know some cocoa, here are the relevant places in the code, and example code for the Win32 backend:

so there’ll have to be some new pollkey function and I guess it’ll have to be stated in whatever defines the cocoa backend.

the cocoa backend sleeps like this: [NSThread sleepForTimeInterval:1/100.];

so I guess you could copy the entire cocoa waitkey function and strip it down like I did the win32 waitkey function, to build a pollkey function out of it.