Real-time video leaves milliseconds per frame, so knowing where time goes in cv2 code is essential.
Start by measuring. cv2.getTickCount() and cv2.getTickFrequency() give precise timings, as does time.perf_counter.
The biggest win is usually removing Python-level pixel loops. A loop over a 1080p frame touches two million pixels through the interpreter; the equivalent cv2 or NumPy call runs in optimised C++ with SIMD instructions (SSE, AVX, NEON). cv2.useOptimized() reports whether those optimised paths are enabled (they are by default).
OpenCV parallelises many functions internally with its own thread pool. cv2.setNumThreads(n) sets the count; cv2.getNumThreads() reports it. When you already parallelise at a higher level (multiprocessing workers, a DataLoader with num_workers), set it to 0 or 1 inside workers to avoid oversubscribing the CPU.
The Transparent API (T-API) uses cv2.UMat instead of ndarrays. Functions called with UMat inputs may run on an OpenCL device (often an integrated GPU) and return UMats; call .get() to download the result. It pays off only for chains of heavy operations on large images, because each upload and download costs time. Check cv2.ocl.haveOpenCL().
Other levers:
- Work at lower resolution when possible: halving width and height cuts most costs by four.
- Reuse buffers with
dst=arguments in loops. - Use
cv2.LUTfor per-pixel mappings such as gamma correction instead of math on every pixel. - CUDA functions (
cv2.cuda) exist only in custom builds; pip wheels don't include them.
Example
import time
import cv2
import numpy as np
img = np.random.default_rng(0).integers(0, 256, (720, 1280), dtype=np.uint8)
gamma = 0.5
table = np.array([((i / 255) ** gamma) * 255 for i in range(256)]).astype(np.uint8)
t = time.perf_counter()
slow = (((img / 255.0) ** gamma) * 255).astype(np.uint8) # float math per pixel
t_np = time.perf_counter() - t
t = time.perf_counter()
fast = cv2.LUT(img, table) # one table lookup each
t_lut = time.perf_counter() - t
print(np.abs(fast.astype(int) - slow).max() <= 1, t_lut < t_np) # True True
print(cv2.useOptimized(), cv2.getNumThreads() >= 1) # True True
cv2.setNumThreads(1) # e.g. inside a multiprocessing worker
u = cv2.UMat(img) # T-API container
blur = cv2.GaussianBlur(u, (9, 9), 0) # may run via OpenCL
print(type(blur).__name__, blur.get().shape) # UMat (720, 1280)Remember
- Replace per-pixel Python loops with cv2/NumPy calls or cv2.LUT
- cv2.setNumThreads controls OpenCV's internal thread pool
- UMat enables OpenCL offload; .get() downloads results
- Downscaling and buffer reuse are often the cheapest speed-ups
Watch out for
- Running many multiprocessing workers that each spawn a full OpenCV thread pool, oversubscribing the CPU.