Performance: threads, UMat and vectorising

OpenCV · Expert · 12 min

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.LUT for 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.

Check yourself

  1. When is converting a pipeline to cv2.UMat likely to help?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.opencv.org/4.x/dc/d71/tutorial_py_optimization.html