Getting Started
Read, Show & Save Images
OpenCV reads images in BGR order (not RGB) — a common pitfall when interoperating with matplotlib or PIL. cv2.waitKey(0) blocks until a key is pressed; always call destroyAllWindows() to close windows. JPEG quality is 0-100.
import cv2
import numpy as np
# read an image (BGR format by default)
img = cv2.imread("photo.jpg")
print(img.shape) # (height, width, channels)
# read as grayscale
gray = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# read with alpha channel
rgba = cv2.imread("logo.png", cv2.IMREAD_UNCHANGED)
# display
cv2.imshow("window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# save
cv2.imwrite("output.jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 90])Install & Import
opencv-python ships only the main modules; opencv-contrib-python adds extra (often patent-encumbered) modules like SIFT/SURF and ArUco. Use the headless variant in servers/Docker to avoid pulling X11 dependencies. Despite the name cv2, OpenCV 4.x still imports as cv2.
# Install OpenCV (Python bindings)
pip install opencv-python # main modules only
pip install opencv-contrib-python # includes extra modules (SIFT, SURF, aruco...)
pip install opencv-python-headless # no GUI (servers / Docker)
# The universal alias is cv2
import cv2
print(cv2.__version__) # e.g. 4.9.0
# NumPy is required — images are ndarray
import numpy as npImage as NumPy Array
OpenCV images are NumPy arrays, so everything you know about NumPy applies. The coordinate convention is img[y, x] (row, col) — the opposite of (x, y) image coordinates, a frequent source of bugs. Color images are (H, W, 3) in BGR; grayscale are (H, W).
import cv2
import numpy as np
img = cv2.imread("photo.jpg") # BGR uint8, shape (H, W, 3)
print(type(img)) # <class 'numpy.ndarray'>
print(img.shape, img.dtype) # (480, 640, 3) uint8
# A grayscale image is 2-D: (H, W)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
print(gray.shape) # (480, 640)
# Coordinate convention: img[y, x] (row, col)
px = img[100, 200] # BGR pixel at (x=200, y=100)
blue, green, red = img[100, 200]
# Vectorized access to a region (ROI)
roi = img[50:150, 100:300] # rows 50-149, cols 100-299Pixel Access & ROI
Per-pixel access via img[y, x] is slow and discouraged in loops — always prefer NumPy slicing. .copy() when assigning a ROI so later changes to the source don't bleed in. Modifying img[:, :, channel] is the fastest way to operate on a single color channel.
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
# access a single pixel (slow)
b, g, r = img[100, 200]
# access a single channel faster
blue = img[100, 200, 0]
img[100, 200] = [255, 255, 255] # set to white
# numpy slicing is much faster than item-by-item
roi = img[100:200, 200:400].copy()
# copy ROI to another location
img[0:100, 0:200] = roi
# modify a whole channel
img[:, :, 2] = 0 # zero out the red channelData Types & Conversion
Most OpenCV functions expect uint8 (0-255), but algorithms like SIFT, optical flow, and edge detection often need float32. Never cast directly with .astype() after subtraction — uint8 overflow turns -1 into 255. Use cv2.absdiff or convert to float first. Always cast back to uint8 before cv2.imwrite / imshow.
import cv2
import numpy as np
img = cv2.imread("photo.jpg") # uint8, 0-255
print(img.dtype, img.min(), img.max()) # uint8 0 255
# convert to float32 (0.0-1.0 for many algorithms)
f = img.astype(np.float32) / 255.0
# convert back to uint8 for display/save
u = (f * 255).astype(np.uint8)
# clamp then cast (avoid overflow when subtracting)
diff = cv2.absdiff(img, img) # safe subtraction
print(diff.dtype) # uint8
# CV_32F / CV_64F / CV_8U constants (used in some functions)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
print(gray.dtype) # uint8Video Capture
Open Webcam
VideoCapture(0) opens the default webcam; use 1, 2... for additional cameras. Always call cap.release() to free the device — forgetting it can leave the camera busy for other apps. waitKey(1) waits ~1ms; the & 0xFF trick ensures compatibility on 64-bit systems where waitKey returns more than 8 bits.
import cv2
cap = cv2.VideoCapture(0) # 0 = default camera
print(cap.isOpened()) # True if camera opened
while True:
ret, frame = cap.read() # ret is False at end of stream
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
cv2.imshow("frame", gray)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()Read Video File
CAP_PROP_FRAME_COUNT is often approximate and unreliable for some codecs — don't rely on it for exact seeking. waitKey(int(1000/fps)) plays at roughly the correct speed; for precise timing use a real clock. Some compressed videos won't open without the right backend (FFmpeg).
import cv2
cap = cv2.VideoCapture("movie.mp4")
# video properties
fps = cap.get(cv2.CAP_PROP_FPS)
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
print(f"{width}x{height} @ {fps} fps, {count} frames")
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
cv2.imshow("video", frame)
if cv2.waitKey(int(1000 / fps)) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()Save / Write Video
The FourCC code is platform- and codec-dependent: 'mp4v' is widely portable, 'XVID' produces .avi, 'avc1' gives H.264 (may not be available without proprietary codecs). The frame size passed to VideoWriter MUST match the frames you write, otherwise the file is corrupted. Always call out.release() to flush and close the file.
import cv2
cap = cv2.VideoCapture(0)
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # or 'XVID', 'avc1'
fps = 20.0
size = (640, 480)
out = cv2.VideoWriter("out.mp4", fourcc, fps, size)
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
frame = cv2.resize(frame, size)
out.write(frame)
cv2.imshow("rec", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
out.release()
cv2.destroyAllWindows()Camera Settings
Camera properties are hardware- and driver-dependent — many webcams silently ignore unsupported settings or snap to nearest valid values, so always read back the actual value. CAP_PROP_EXPOSURE uses log2 of seconds (-4 = 1/16 s). Auto-exposure must be disabled (set to 1) before manual exposure takes effect on most cameras.
import cv2
cap = cv2.VideoCapture(0)
# set resolution
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
# set exposure / focus / brightness
cap.set(cv2.CAP_PROP_EXPOSURE, -4) # log2 exposure time
cap.set(cv2.CAP_PROP_BRIGHTNESS, 128)
cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1) # 1=manual, 3=auto
# disable autofocus and set manual focus
cap.set(cv2.CAP_PROP_AUTOFOCUS, 0)
cap.set(cv2.CAP_PROP_FOCUS, 50)
print(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) # actual width may differFrame Rate & Timing
cap.get(CAP_PROP_FPS) returns the configured FPS, not the actual capture rate — measure with a real clock. A 30-frame moving average smooths jitter. The biggest FPS killers are cv2.imshow (GUI thread) and per-frame cv2.imwrite; for benchmarking, disable display. For deterministic timing, accumulate frames in a thread and process from a queue.
import cv2
import time
cap = cv2.VideoCapture(0)
prev = time.time()
fps_hist = []
while True:
ret, frame = cap.read()
if not ret:
break
now = time.time()
dt = now - prev
prev = now
if dt > 0:
fps_hist.append(1.0 / dt)
fps = sum(fps_hist[-30:]) / min(len(fps_hist), 30)
cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.imshow("fps", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()Drawing
Line & Rectangle
All drawing functions mutate the image in place — pass a copy if you want to keep the original. Coordinates are (x, y), opposite to NumPy's [y, x] indexing. Colors are BGR tuples. Use thickness=-1 for filled shapes and cv2.LINE_AA for smooth (anti-aliased) lines on diagonal edges.
import cv2
import numpy as np
img = np.zeros((400, 400, 3), dtype=np.uint8)
# line(img, pt1, pt2, color, thickness)
cv2.line(img, (10, 10), (390, 10), (0, 255, 0), 2)
# rectangle(img, pt1, pt2, color, thickness)
cv2.rectangle(img, (50, 50), (200, 150), (255, 0, 0), 3)
# filled rectangle (thickness = -1)
cv2.rectangle(img, (220, 50), (370, 150), (0, 0, 255), -1)
# anti-aliased line (cv2.LINE_AA)
cv2.line(img, (10, 380), (390, 200), (255, 255, 0), 1, cv2.LINE_AA)
cv2.imshow("img", img)
cv2.waitKey(0)Circle & Ellipse
ellipse's axes parameter is (halfWidth, halfHeight) — full width is 2 * axes[0]. angle rotates the whole ellipse; startAngle/endAngle draw partial arcs (0° = right, counterclockwise). Drawing partial arcs is the standard way to render pie-chart slices or motion-direction indicators.
import cv2
import numpy as np
img = np.zeros((400, 400, 3), dtype=np.uint8)
# circle(img, center, radius, color, thickness)
cv2.circle(img, (100, 100), 50, (0, 255, 255), 3)
cv2.circle(img, (300, 100), 50, (255, 0, 255), -1) # filled
# ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness)
cv2.ellipse(img, (200, 250), (100, 50), 30, 0, 360, (0, 255, 0), 2)
# partial arc (0 to 180 degrees)
cv2.ellipse(img, (200, 250), (100, 50), 30, 0, 180, (0, 0, 255), 3)
cv2.imshow("img", img)
cv2.waitKey(0)Polygon & Polylines
polylines accepts a list of point arrays, so you can draw multiple polygons in one call. The points array must be int32 and reshaped to (-1, 1, 2). Use fillPoly for filled shapes; for translucent fills, draw onto an overlay and blend with addWeighted. The isClosed flag connects the last point back to the first.
import cv2
import numpy as np
img = np.zeros((400, 400, 3), dtype=np.uint8)
# polygon points: shape (N, 1, 2), int32
pts = np.array([[50, 50], [200, 30], [350, 200],
[300, 350], [80, 300]], dtype=np.int32)
pts = pts.reshape((-1, 1, 2))
# outline only (not closed)
cv2.polylines(img, [pts], False, (0, 255, 0), 2)
# closed polygon outline
cv2.polylines(img, [pts], True, (255, 0, 0), 3)
# filled polygon
cv2.fillPoly(img, [pts], (0, 0, 255))
cv2.imshow("img", img)
cv2.waitKey(0)Text
OpenCV's built-in fonts are Hershey vector fonts — they don't support CJK/Chinese characters. For non-Latin text, render with PIL (Pillow) ImageFont and convert back, or use cv2.freetype from opencv-contrib. getTextSize returns ((width, height), baseline) — useful to center text or draw a background box behind it.