入门
读取、显示与保存图像
OpenCV 默认以 BGR 顺序读取图像(非 RGB)——与 matplotlib 或 PIL 互操作时这是常见陷阱。cv2.waitKey(0) 会阻塞直到按键;始终调用 destroyAllWindows() 关闭窗口。JPEG 质量参数为 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])安装与导入
opencv-python 仅包含主模块;opencv-contrib-python 额外包含 SIFT/SURF、ArUco 等模块(部分有专利限制)。在服务器/Docker 中使用 headless 版本可避免引入 X11 依赖。尽管名为 cv2,OpenCV 4.x 仍以 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 np图像即 NumPy 数组
OpenCV 图像就是 NumPy 数组,NumPy 的所有知识都适用。坐标约定是 img[y, x](行、列)——与 (x, y) 图像坐标相反,这是常见的 bug 来源。彩色图为 (H, W, 3) 的 BGR;灰度图为 (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-299像素访问与 ROI
通过 img[y, x] 逐像素访问很慢,循环中不推荐——始终优先用 NumPy 切片。赋值 ROI 时用 .copy() 以免源数据后续修改产生影响。修改 img[:, :, 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 channel数据类型与转换
大多数 OpenCV 函数期望 uint8(0-255),但 SIFT、光流、边缘检测等算法常需 float32。减法后绝不要直接 .astype()——uint8 溢出会把 -1 变成 255。应使用 cv2.absdiff 或先转 float。cv2.imwrite / imshow 前务必转回 uint8。
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) # uint8视频捕获
打开摄像头
VideoCapture(0) 打开默认摄像头;1、2... 对应其他摄像头。务必调用 cap.release() 释放设备——忘记释放会导致摄像头被其他程序占用。waitKey(1) 等待约 1ms;& 0xFF 技巧用于兼容 waitKey 在 64 位系统返回超过 8 位的情况。
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()读取视频文件
CAP_PROP_FRAME_COUNT 通常只是近似值,对某些编码不可靠——不要依赖它做精确跳转。waitKey(int(1000/fps)) 大致按正确速度播放;精确计时请用真实时钟。部分压缩视频在缺少对应后端(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()保存/写入视频
FourCC 编码依赖于平台和编解码器:'mp4v' 通用性最好,'XVID' 生成 .avi,'avc1' 为 H.264(可能需专有编解码器)。传给 VideoWriter 的帧尺寸必须与写入帧一致,否则文件损坏。务必调用 out.release() 刷新并关闭文件。
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()摄像头设置
摄像头属性依赖硬件和驱动——许多摄像头会静默忽略不支持的设置或吸附到最近的有效值,因此务必回读实际值。CAP_PROP_EXPOSURE 使用秒数的 log2(-4 = 1/16 秒)。大多数摄像头需先禁用自动曝光(设为 1)手动曝光才生效。
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 differ帧率与计时
cap.get(CAP_PROP_FPS) 返回配置的 FPS,而非实际采集速率——应用真实时钟测量。30 帧移动平均可平滑抖动。最大的帧率杀手是 cv2.imshow(GUI 线程)和逐帧 cv2.imwrite;基准测试时应关闭显示。确定性计时可在独立线程累积帧再处理。
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()绘图
直线与矩形
所有绘图函数都会就地修改图像——想保留原图请传入副本。坐标是 (x, y),与 NumPy 的 [y, x] 索引相反。颜色为 BGR 元组。thickness=-1 表示填充,cv2.LINE_AA 用于对角线的平滑(抗锯齿)。
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)圆与椭圆
ellipse 的 axes 参数是 (半宽, 半高)——全宽为 2 * axes[0]。angle 旋转整个椭圆;startAngle/endAngle 绘制部分弧(0°=右,逆时针)。绘制部分弧是渲染饼图扇区或运动方向指示器的标准做法。
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)多边形与折线
polylines 接受点数组的列表,因此一次调用可绘制多个多边形。点数组必须是 int32 并 reshape 为 (-1, 1, 2)。fillPoly 用于填充;半透明填充可在覆盖层上绘制后用 addWeighted 混合。isClosed 标志将末点连回首点。
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)文字
OpenCV 内置字体为 Hershey 矢量字体——不支持中文等 CJK 字符。非拉丁文字请用 PIL(Pillow)ImageFont 渲染后转回,或用 opencv-contrib 的 cv2.freetype。getTextSize 返回 ((宽, 高), 基线)——便于居中文字或在文字后绘制背景框。
import cv2
import numpy as np
img = np.zeros((300, 600, 3), dtype=np.uint8)
# putText(img, text, org, fontFace, fontScale, color, thickness)
cv2.putText(img, "Hello OpenCV", (10, 50),
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255, 255, 255), 2)
# fonts
fonts = [cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_PLAIN,
cv2.FONT_HERSHEY_DUPLEX, cv2.FONT_HERSHEY_COMPLEX,
cv2.FONT_HERSHEY_TRIPLEX, cv2.FONT_ITALIC]
for i, f in enumerate(fonts):
cv2.putText(img, f"font {i}", (10, 100 + i*30), f, 0.8, (0, 255, 0), 1)
# measure text size before drawing
(text_w, text_h), baseline = cv2.getTextSize(
"Measured", cv2.FONT_HERSHEY_SIMPLEX, 1.0, 2)
cv2.rectangle(img, (10, 260), (10 + text_w, 260 + text_h + baseline),
(0, 0, 255), 1)箭头与标记
arrowedLine 的 tipLength 是线长的比例,因此更长的箭头会有按比例更大的箭头——若需一致的箭头尺寸请显式设置。drawMarker 便于高亮关键点(如检测到的特征或角点);MARKER_TILTED_CROSS 给出 X 形标记。
import cv2
import numpy as np
img = np.zeros((400, 400, 3), dtype=np.uint8)
# arrowedLine(img, pt1, pt2, color, thickness, tipLength)
cv2.arrowedLine(img, (50, 50), (350, 50), (0, 255, 0), 2, tipLength=0.05)
cv2.arrowedLine(img, (200, 200), (200, 380), (0, 0, 255), 3, tipLength=0.1)
# drawMarker (cross, tilted cross, circle, square, diamond)
cv2.drawMarker(img, (100, 300), (255, 0, 255),
markerType=cv2.MARKER_CROSS, markerSize=30, thickness=2)
cv2.drawMarker(img, (300, 300), (255, 255, 0),
markerType=cv2.MARKER_DIAMOND, markerSize=30, thickness=2)
cv2.imshow("img", img)
cv2.waitKey(0)覆盖层与标注
覆盖层技巧(在副本上绘制后 addWeighted)是绘制半透明填充的标准做法——OpenCV 图形没有原生 alpha。务必在混合后再绘制实心文字以保持清晰;在覆盖层上绘制文字再混合会变模糊。这种模式在检测/分割可视化中无处不在。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
overlay = img.copy()
# draw a translucent rectangle on the overlay
cv2.rectangle(overlay, (50, 50), (250, 150), (0, 0, 255), -1)
cv2.putText(overlay, "REGION", (60, 110),
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
# blend overlay onto the original (alpha=0.4)
out = cv2.addWeighted(overlay, 0.4, img, 0.6, 0)
cv2.imshow("out", out)
cv2.waitKey(0)图像算术
加法与减法
OpenCV 的 cv2.add 在 255 处饱和(250+10=255),而 NumPy 的 + 会回绕(250+10=8,因 uint8 溢出)。对图像应始终用 cv2.add / cv2.subtract——回绕会产生可见 的黑/白伪影。absdiff 是背景相减和运动检测的主力。
import cv2
import numpy as np
a = cv2.imread("a.jpg")
b = cv2.imread("b.jpg")
b = cv2.resize(b, (a.shape[1], a.shape[0])) # match sizes
# OpenCV addition: saturated (caps at 255)
add_cv = cv2.add(a, b)
# NumPy addition: wraps around (250 + 10 = 264 -> 8)
add_np = a + b
# subtract (saturated, never goes below 0)
sub = cv2.subtract(a, b)
# absolute difference (handy for motion detection)
diff = cv2.absdiff(a, b)混合与加权叠加
addWeighted 要求两幅图像尺寸和 dtype 相同——先 resize。gamma 项给每个像素加一个常量,用于亮度控制。两幅图像间的经典淡入/交叉溶解就是 alpha 从 0 扫到 1 的 addWeighted。
import cv2
import numpy as np
a = cv2.imread("a.jpg")
b = cv2.imread("b.jpg")
b = cv2.resize(b, (a.shape[1], a.shape[0]))
# dst = alpha * a + beta * b + gamma
blend = cv2.addWeighted(a, 0.7, b, 0.3, 0)
# brighten by adding a constant (gamma)
bright = cv2.addWeighted(a, 1.0, a, 0.0, 50)
# fade between two images over N steps
for i in range(11):
alpha = i / 10.0
frame = cv2.addWeighted(a, alpha, b, 1 - alpha, 0)
cv2.imshow("fade", frame)
cv2.waitKey(200)位运算
位运算是应用掩码的标准方式:cv2.bitwise_or(src, dst, mask=mask) 仅在 mask 非零处将 src 拷入 dst。掩码必须是单通道 8 位图像,非零表示'保留'。这是 ROI 提取和背景替换的基础。
import cv2
import numpy as np
a = np.zeros((200, 200), dtype=np.uint8)
cv2.rectangle(a, (20, 20), (120, 120), 255, -1)
b = np.zeros((200, 200), dtype=np.uint8)
cv2.circle(b, (100, 100), 60, 255, -1)
# bitwise AND / OR / XOR / NOT
and_ = cv2.bitwise_and(a, b)
or_ = cv2.bitwise_or(a, b)
xor = cv2.bitwise_xor(a, b)
not_ = cv2.bitwise_not(a)
# use a mask to copy only part of an image
mask = np.zeros((200, 200), dtype=np.uint8)
cv2.circle(mask, (100, 100), 50, 255, -1)
src = np.full((200, 200, 3), (0, 255, 0), dtype=np.uint8)
dst = cv2.imread("photo.jpg")
dst = cv2.resize(dst, (200, 200))
result = cv2.bitwise_or(src, dst, mask=mask)图像差分与运动检测
帧差法是最简单的运动检测器:对连续帧的绝对差值做阈值化。它在相机移动时失效且忽略慢速运动——真实系统请用背景相减(cv2.createBackgroundSubtractorMOG2)或光流。形态学开运算去噪;膨胀合并邻近连通块。
import cv2
import numpy as np
prev = cv2.imread("frame1.jpg", cv2.IMREAD_GRAYSCALE)
curr = cv2.imread("frame2.jpg", cv2.IMREAD_GRAYSCALE)
# absolute difference
diff = cv2.absdiff(prev, curr)
# threshold to get a binary motion mask
_, mask = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
# clean up with morphology
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.dilate(mask, kernel, iterations=2)
# find moving contours
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
if cv2.contourArea(c) > 500:
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(curr, (x, y), (x+w, y+h), (0, 255, 0), 2)ROI 与掩码
带掩码的 bitwise_and 是提取不规则区域的惯用法。NumPy 布尔索引(img[mask])更灵活但返回的是扁平的像素数组而非成形图像。复用 ROI 时务必 .copy()——切片返回的是别名原图的视图。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
h, w = img.shape[:2]
# build a mask: white circle on black background
mask = np.zeros((h, w), dtype=np.uint8)
cv2.circle(mask, (w // 2, h // 2), 150, 255, -1)
# keep only the masked region, zero elsewhere
masked = cv2.bitwise_and(img, img, mask=mask)
# extract ROI by bounding box
roi = img[100:300, 200:500].copy()
# put ROI back (or to a different location)
img[0:200, 0:300] = roi
# boolean mask (NumPy) — also works, but converts to 0/255
bool_mask = np.zeros((h, w), dtype=bool)
bool_mask[100:300, 200:500] = True
img_masked = img.copy()
img_masked[~bool_mask] = 0饱和与回绕
uint8 溢出陷阱:NumPy 回绕(300->44),OpenCV 饱和(300->255)。任何可能超出 0-255 的算术,要么用 cv2.add/subtract,要么先提升到 int16/float32 再裁剪。在同一管线中混用 cv2 和 NumPy 是莫名暗/亮像素伪影的头号来源。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
# WRONG: NumPy arithmetic wraps around (200 + 100 -> 44)
bad = img + 100
# RIGHT: cv2.add saturates at 255 (200 + 100 -> 255)
good = cv2.add(img, np.full_like(img, 100))
# subtracting a bright image: cv2 clamps to 0
dark = cv2.subtract(img, np.full_like(img, 100))
# manual saturation (equivalent to cv2.add)
manual = np.clip(img.astype(np.int16) + 100, 0, 255).astype(np.uint8)
# mixing cv2 and numpy is a common bug source:
# half = img // 2 # NumPy: fine (no overflow)
# half_then_add = half + half # might equal original (ok)
# but bright = img + img # wraps! 150+150=300 -> 44几何变换
缩放
INTER_AREA 最适合下采样(避免混叠),INTER_CUBIC 或 INTER_LINEAR 适合上采样。INTER_NEAREST 最快但有马赛克——用于标签/掩码图(不能引入新像素值)。dsize 元组是 (宽, 高),与 img.shape 的 (高, 宽) 相反。
import cv2
img = cv2.imread("photo.jpg")
h, w = img.shape[:2]
# resize to a fixed size
small = cv2.resize(img, (320, 240))
# resize by a scale factor (fx, fy)
big = cv2.resize(img, None, fx=2.0, fy=2.0)
# resize keeping aspect ratio
scale = 0.5
new = cv2.resize(img, (int(w * scale), int(h * scale)))
# interpolation methods
down = cv2.resize(img, (w // 2, h // 2),
interpolation=cv2.INTER_AREA) # shrinking
up = cv2.resize(img, (w * 2, h * 2),
interpolation=cv2.INTER_CUBIC) # enlarging旋转
对 90° 的倍数,cv2.rotate 远快于 warpAffine。getRotationMatrix2D 的角度单位是度,正值=逆时针。要不裁剪旋转,需重新计算外接框 (new_w, new_h) 并平移旋转矩阵——否则大图旋转后边角会被切掉。
import cv2
img = cv2.imread("photo.jpg")
h, w = img.shape[:2]
# rotate 90/180/270 (fast, no interpolation)
rot90 = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
rot180 = cv2.rotate(img, cv2.ROTATE_180)
rot270 = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
# arbitrary-angle rotation
center = (w // 2, h // 2)
angle = 45
scale = 1.0
M = cv2.getRotationMatrix2D(center, angle, scale)
rot = cv2.warpAffine(img, M, (w, h))
# rotate and expand the canvas to fit the whole image
cos = abs(M[0, 0]); sin = abs(M[0, 1])
new_w = int(h * sin + w * cos)
new_h = int(h * cos + w * sin)
M[0, 2] += (new_w - w) / 2
M[1, 2] += (new_h - h) / 2
rot_fit = cv2.warpAffine(img, M, (new_w, new_h))仿射变换
仿射变换保持平行性和距离比——由 3 对点对应定义(6 个自由度)。warpAffine 通过逆向查找隐式使用前向映射,因此 M 映射 src->dst 但函数内部对其求逆。用 borderMode=BORDER_REFLECT 或 BORDER_CONSTANT 控制越界区域填充方式。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
h, w = img.shape[:2]
# affine: parallel lines stay parallel (6 DOF)
src = np.float32([[50, 50], [200, 50], [50, 200]])
dst = np.float32([[10, 100], [200, 50], [100, 250]])
M = cv2.getAffineTransform(src, dst)
warped = cv2.warpAffine(img, M, (w, h))
# inverse mapping (destination -> source)
inv = cv2.invertAffineTransform(M)
# translation is a special affine
T = np.float32([[1, 0, 100], [0, 1, 50]]) # shift right 100, down 50
shifted = cv2.warpAffine(img, T, (w, h))
# black border is default; use borderMode for other fill
reflected = cv2.warpAffine(img, T, (w, h),
borderMode=cv2.BORDER_REFLECT)透视变换
透视(单应性)由 4 对不共线的点定义——8 个自由度。这是标准的'文档扫描'变换:选取照片中卡片/纸张的 4 个角,变换到矩形。getPerspectiveTransform 需要恰好 4 个点;过定拟合请用 cv2.findHomography 配合 RANSAC。
import cv2
import numpy as np
img = cv2.imread("card.jpg")
h, w = img.shape[:2]
# 4 point correspondences define a perspective (homography)
src = np.float32([[73, 110], [320, 78], [380, 350], [40, 320]])
dst = np.float32([[0, 0], [w, 0], [w, h], [0, h]])
M = cv2.getPerspectiveTransform(src, dst)
warped = cv2.warpPerspective(img, M, (w, h))
# invert the homography to map back
M_inv = cv2.getPerspectiveTransform(dst, src)
restored = cv2.warpPerspective(warped, M_inv, (w, h))翻转与镜像
flip 是廉价的内存操作——远快于 warpAffine。flipCode 符号约定:正数水平翻转,0 垂直翻转,负数两者都翻。cv2.transpose 交换轴,有时与 flip 组合用作快速 90° 旋转。
import cv2
img = cv2.imread("photo.jpg")
# flip(img, flipCode)
# 0 -> vertical flip (around x-axis) (upside-down)
# 1 -> horizontal flip (around y-axis) (mirror)
# -1 -> both (180° rotation equivalent)
v = cv2.flip(img, 0)
h = cv2.flip(img, 1)
both = cv2.flip(img, -1)
# transpose (swap x and y axes) — like a 90° rotation + flip
t = cv2.transpose(img)重映射(自定义映射)
remap 是最通用的几何变换——缩放、旋转甚至桶形/镜头畸变校正都可用 remap 表达。务必用 NumPy 向量化构建映射表,而非 Python 循环(上面的循环版仅作演示——慢约 100 倍)。INTER_LINEAR 是默认值;边沿需子像素精度时用 INTER_CUBIC。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
h, w = img.shape[:2]
# build coordinate maps: map_x, map_y say where each output pixel comes from
map_x = np.zeros((h, w), dtype=np.float32)
map_y = np.zeros((h, w), dtype=np.float32)
for y in range(h):
for x in range(w):
# mirror the image
map_x[y, x] = w - 1 - x
map_y[y, x] = y
remapped = cv2.remap(img, map_x, map_y, cv2.INTER_LINEAR)
# vectorized version (much faster) — wave distortion
xs, ys = np.meshgrid(np.arange(w), np.arange(h))
map_x = xs.astype(np.float32) + 10 * np.sin(ys / 20.0)
map_y = ys.astype(np.float32) + 10 * np.cos(xs / 20.0)
wave = cv2.remap(img, map_x, map_y, cv2.INTER_LINEAR,
borderMode=cv2.BORDER_REFLECT)图像滤波
均值模糊(盒式滤波)
均值模糊是最简单的低通滤波器——用邻域均值替换每个像素。核大小必须为正奇数。核越大模糊越强但也更快地破坏细节。仅当需要均匀加权时使用;对自然图像高斯模糊几乎总是更好的选择。
import cv2
img = cv2.imread("photo.jpg")
# 3x3 box blur
blur3 = cv2.blur(img, (3, 3))
# larger kernel = more blur
blur9 = cv2.blur(img, (9, 9))
# boxFilter with normalized=False gives the sum (not average)
sum_ = cv2.boxFilter(img, -1, (5, 5), normalize=False)
# normalized=True is equivalent to cv2.blur
avg = cv2.boxFilter(img, -1, (5, 5), normalize=True)高斯模糊
高斯模糊是默认的平滑核——按高斯函数对邻域加权,比盒式滤波更好地保留边缘。若传入 ksize=(0,0) 和 sigmaX,OpenCV 会自动计算核大小。高斯模糊是 Canny、下采样和 Laplacian 之前抑制噪声放大的必备前置步骤。
import cv2
img = cv2.imread("photo.jpg")
# GaussianBlur(img, ksize, sigmaX)
g = cv2.GaussianBlur(img, (5, 5), 0)
# specifying sigma explicitly
g2 = cv2.GaussianBlur(img, (5, 5), sigmaX=1.5, sigmaY=1.5)
# odd kernel size required; (0, 0) auto-derives sigma from ksize
g3 = cv2.GaussianBlur(img, (0, 0), sigmaX=2.0)
# Gaussian is the standard pre-processing step before edge detection
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 50, 150)中值模糊
中值模糊是处理椒盐(脉冲)噪声的首选滤波器——用邻域中值替换每个像素,孤立离群点被移除而非扩散。与高斯不同,它能很好地保留边缘。核大小必须为奇数。因涉及排序,比高斯/盒式慢。
import cv2
img = cv2.imread("noisy.jpg")
# median blur — great for salt-and-pepper noise
m = cv2.medianBlur(img, 5)
# larger kernel removes more noise but blurs edges
m2 = cv2.medianBlur(img, 7)
# compared to Gaussian on the same image
g = cv2.GaussianBlur(img, (5, 5), 0)双边滤波
双边滤波在平滑平坦区域的同时保留锐利边 缘——它是一种非线性、保边滤波器,同时按空间邻近度和颜色相似度加权邻域。比高斯慢得多(常慢 10 倍)。反复应用是经典的'卡通化'预处理步骤;较大的 sigmaColor 会跨越更多颜色进行混合。
import cv2
img = cv2.imread("photo.jpg")
# bilateralFilter(img, d, sigmaColor, sigmaSpace)
# d = neighborhood diameter
# sigmaColor = how much colors can differ and still blend
# sigmaSpace = how far pixels influence each other (spatial)
b = cv2.bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75)
# strong edge-preserving smoothing (cartoon effect)
b_strong = cv2.bilateralFilter(img, d=15, sigmaColor=150, sigmaSpace=150)
# repeat several times for a "cartoon / oil painting" look
cartoon = img.copy()
for _ in range(7):
cartoon = cv2.bilateralFilter(cartoon, d=9, sigmaColor=75, sigmaSpace=75)锐化(核)
锐化核强调高频——但也会放大噪声,因此应先去噪再锐化。经典 3x3 锐化核之和为 1(保持亮度);和大于 1 会提亮。非锐化掩膜(减去模糊副本)更可控,也是大多数照片编辑器实际使用的方法。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
# sharpening kernel: center boosts, neighbors subtracted
kernel = np.array([[ 0, -1, 0],
[-1, 5, -1],
[ 0, -1, 0]], dtype=np.float32)
sharp = cv2.filter2D(img, -1, kernel)
# stronger sharpening
kernel_strong = np.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]], dtype=np.float32)
sharp2 = cv2.filter2D(img, -1, kernel_strong)
# unsharp mask: image + (image - blurred) * amount
blur = cv2.GaussianBlur(img, (0, 0), 3)
unsharp = cv2.addWeighted(img, 1.5, blur, -0.5, 0)自定义核(filter2D)
filter2D 应用任意相关核——ddepth=-1 保持源深度。核可为 float 以支持负系数;若结果变负,改用 ddepth=cv2.CV_16S 或 CV_32F 再用 convertScaleAbs 转回。可分离核(1D 核外积)通过 sepFilter2D 快得多。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
# emboss kernel
emboss = np.array([[-2, -1, 0],
[-1, 1, 1],
[ 0, 1, 2]], dtype=np.float32)
out = cv2.filter2D(img, -1, emboss)
# edge enhance (Laplacian-like)
edge = np.array([[ 0, 1, 0],
[ 1, -4, 1],
[ 0, 1, 0]], dtype=np.float32)
edges = cv2.filter2D(img, -1, edge)
# box blur done manually
box = np.ones((5, 5), dtype=np.float32) / 25.0
blurred = cv2.filter2D(img, -1, box)
# separable filter (Gaussian) — faster than 2D
kx = cv2.getGaussianKernel(5, 1.0) # 5x1
ky = cv2.getGaussianKernel(5, 1.0) # 5x1
g2d = kx @ ky.T # 5x5
out2 = cv2.filter2D(img, -1, g2d)形态学操作
腐蚀与膨胀
腐蚀收缩前景(白色)区域并去除小噪点;膨胀则扩大并填充小孔。核形状很关键:RECT 通用,ELLIPSE 适合圆润特征,CROSS 用于沿轴向细化和连接。iterations=N 重复操作 N 次,等效于使用更大的核。
import cv2
import numpy as np
img = cv2.imread("shape.png", cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
kernel = np.ones((5, 5), np.uint8)
# erosion: shrinks white regions (removes small noise)
eroded = cv2.erode(binary, kernel, iterations=1)
# dilation: grows white regions (fills small holes)
dilated = cv2.dilate(binary, kernel, iterations=1)
# custom kernel shapes
rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))开运算与闭运算
开运算(腐蚀→膨胀)去除小亮斑而不改变较大物体的尺寸。闭运算(膨胀→腐蚀)填充小暗孔。二者皆幂等——应用两次等于一次。经典去噪模式是先闭后开(或先开后闭)以同时处理两类缺陷。
import cv2
import numpy as np
img = cv2.imread("noisy.png", cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
kernel = np.ones((5, 5), np.uint8)
# opening = erode then dilate -> removes small foreground noise
opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
# closing = dilate then erode -> fills small holes in foreground
closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
# typical denoise pipeline: open then close
clean = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
clean = cv2.morphologyEx(clean, cv2.MORPH_OPEN, kernel)形态学梯度
形态学梯度(膨胀 - 腐蚀)生成物体边界的粗轮廓——可用作快速边缘检测器或分割掩膜。与 Canny 不同它给出实心宽边,适合高亮区域而非测量精确边缘位置。
import cv2
import numpy as np
img = cv2.imread("shape.png", cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
kernel = np.ones((5, 5), np.uint8)
# gradient = dilation - erosion -> outlines of objects
gradient = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel)
# on a grayscale image (works too)
gray_grad = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)Top Hat 与 Black Hat
Top-hat 提取比核小的亮结构(如暗色文档上的文字);black-hat 提取小暗结构。二者常用于阴影/高光提取,以及作为快速的不均匀光照校正器(除以背景的技巧在文档扫描仪中常见)。
import cv2
import numpy as np
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
kernel = np.ones((15, 15), np.uint8)
# top hat = image - opening -> small bright features on dark bg
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
# black hat = closing - image -> small dark features on bright bg
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
# use case: correct uneven illumination
# background = large closing; divide image by background
bg = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
corrected = cv2.divide(img, bg, scale=255)骨架化
形态学骨架化将二值形状细化为 1 像素宽的中轴——适用于 OCR、指纹和血管分析。迭代'腐蚀-减去开运算'循环一直运行到图像为空。生产环境用 scikit-image 的 medial_axis 或 thinning 更快、结果更干净。
import cv2
import numpy as np
img = cv2.imread("shape.png", cv2.IMREAD_GRAYSCALE)
_, img = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
# skeleton via iterative morphological opening
kernel = np.ones((3, 3), np.uint8)
skeleton = np.zeros_like(img)
while True:
eroded = cv2.erode(img, kernel)
opened = cv2.morphologyEx(eroded, cv2.MORPH_OPEN, kernel)
temp = cv2.subtract(eroded, opened)
skeleton = cv2.bitwise_or(skeleton, temp)
img = eroded
if cv2.countNonZero(img) == 0:
break击中击不中
击中击不中是形态学模式匹配器——它查找前景和背景像素的精确配置,适合检测角点或 T 型连接等特定形状。核中 1 表示要求前景,-1 表示要求背景,0 表示任意。虽属小众但在精确定位模式时无可替代。
import cv2
import numpy as np
img = cv2.imread("pattern.png", cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
# hit-or-miss finds exact patterns
# kernel encodes foreground (1), background (-1), don't-care (0)
kernel = np.array([[ 0, 1, -1],
[ 1, -1, -1],
[ 1, 1, 0]], dtype=np.int32)
hits = cv2.morphologyEx(binary, cv2.MORPH_HITMISS, kernel)
# count matching locations
ys, xs = np.where(hits > 0)
print(f"found {len(xs)} matches")边缘检测
Canny 边缘检测器
Canny 是最常用的边缘检测器:高斯平滑 → Sobel 梯度 → 非极大值抑制 → 滞后阈值(双阈值)。高于高阈值的像素为边缘;介于低高阈值之间的像素仅在连通到强边缘时才保留。自动阈值技巧(中值 × 0.7/1.3)对多样图像效果良好。
import cv2
img = cv2.imread("photo.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Canny(image, threshold1, threshold2, apertureSize, L2gradient)
edges = cv2.Canny(blurred, 50, 150)
# auto-tuned thresholds from the median intensity
v = np.median(blurred)
lower = int(max(0, 0.7 * v))
upper = int(min(255, 1.3 * v))
auto = cv2.Canny(blurred, lower, upper)
# L2 gradient (more accurate, slower)
edges_l2 = cv2.Canny(blurred, 50, 150, L2gradient=True)Sobel 算子
Sobel 计算一阶导数——用 ddepth=cv2.CV_64F(或 CV_16S)以捕获负梯度,否则 uint8 溢出会让暗到亮的过渡不可见。ksize=3 为标准;ksize=5 给出更平滑但更宽的边缘。幅值(sqrt(sx²+sy²))是 Canny 和许多特征检测器的基础。
import cv2
import numpy as np
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# Sobel x (vertical edges)
sx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
# Sobel y (horizontal edges)
sy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
# gradient magnitude
mag = np.sqrt(sx**2 + sy**2)
mag = np.uint8(255 * mag / mag.max())
# gradient direction (for non-max suppression)
angle = np.arctan2(sy, sx)
# convert back to visible uint8
sx_abs = cv2.convertScaleAbs(sx)
sy_abs = cv2.convertScaleAbs(sy)Laplacian
Laplacian 是二阶导数——对噪声非常敏感,务必先模糊。它检测零交叉(符号变化),给出细而精确的边缘但没有方向。最常用于 Marr-Hildreth 边缘检测,以及作为图像锐化(非锐化掩膜)的 'L' 通道输入。使用 CV_64F,因为二阶导数会大幅变负。
import cv2
import numpy as np
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
blurred = cv2.GaussianBlur(img, (3, 3), 0)
# Laplacian: second derivative in both directions
lap = cv2.Laplacian(blurred, cv2.CV_64F)
lap_abs = cv2.convertScaleAbs(lap)
# zero-crossing detection (sharper edges)
# Manual Laplacian kernel
kernel = np.array([[0, 1, 0],
[1, -4, 1],
[0, 1, 0]], dtype=np.float32)
edges = cv2.filter2D(blurred, -1, kernel)Scharr 滤波器
Scharr 是为 3x3 窗口设计的 Sobel 变体——当必须用小核时,它比 Sobel ksize=3 给出更准确的梯度方向。更大核(5x5、7x7)用 Sobel 即可。ksize=-1 的 cv2.Sobel 等价于 Scharr。当梯度方向重要时(HOG、边缘方向)优先用 Scharr。
import cv2
import numpy as np
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# Scharr: a Sobel variant optimized for 3x3 kernels
# better rotational symmetry than 3x3 Sobel
sx = cv2.Scharr(img, cv2.CV_64F, 1, 0)
sy = cv2.Scharr(img, cv2.CV_64F, 0, 1)
mag = cv2.magnitude(sx, sy)
mag_u8 = cv2.convertScaleAbs(mag)
# equivalent to Sobel with ksize=-1
sx2 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=-1)边缘检测流水线
灰度 → 去噪 → Canny → 膨胀 → findContours 流水线覆盖了绝大部分实际边缘任务。在查找轮廓前膨胀可闭合断裂的边缘,否则会把一个物体拆成多个。大部分工程精力花在调 Canny 阈值和去噪核大小上。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
# 1. grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2. denoise (Gaussian or bilateral)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 3. auto-thresholded Canny
v = np.median(blurred)
edges = cv2.Canny(blurred, int(0.7 * v), int(1.3 * v))
# 4. dilate to close gaps, then find contours
edges = cv2.dilate(edges, np.ones((3, 3), np.uint8), iterations=1)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
# 5. filter by area
out = img.copy()
for c in contours:
if cv2.contourArea(c) > 100:
cv2.drawContours(out, [c], -1, (0, 255, 0), 2)
cv2.imshow("edges", edges)
cv2.imshow("contours", out)
cv2.waitKey(0)轮廓
查找轮廓
在 OpenCV 3.x 中 findContours 会修改源图像——若后续需要二值图请传入副本。RETR_EXTERNAL 仅返回最外层轮廓(最快);RETR_TREE 给出完整层级用于嵌套形状(物体内的孔)。CHAIN_APPROX_SIMPLE 丢弃直线上的冗余点,节省内存。
import cv2
img = cv2.imread("shapes.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# findContours(image, mode, method)
# mode: RETR_EXTERNAL (outer only), RETR_LIST, RETR_TREE, RETR_CCOMP
# method: CHAIN_APPROX_SIMPLE compresses horizontal/vertical/diagonal segments
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
print(f"found {len(contours)} contours")
# hierarchy is shape (1, N, 4): [next, prev, child, parent]
# OpenCV 3 returns (image, contours, hierarchy)
# OpenCV 4 returns (contours, hierarchy)绘制轮廓
drawContours(image, contours, contourIdx, color, thickness):contourIdx=-1 绘制全部;指定索引绘制单个。thickness=-1 填充轮廓。在空白图上绘制是构建轮廓掩膜的标准做法,用于后续的平均颜色或掩膜直方图等操作。
import cv2
img = cv2.imread("shapes.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
# draw all contours (-1), color, thickness
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)
# draw a single contour by index
cv2.drawContours(img, contours, 0, (0, 0, 255), 3)
# filled contour (thickness = -1)
cv2.drawContours(img, contours, -1, (255, 0, 0), -1)
# draw on a blank canvas (mask)
mask = np.zeros_like(gray)
cv2.drawContours(mask, contours, -1, 255, -1)轮廓属性
contourArea 单位为像素且忽略孔(用 RETR_CCOMP 层级的轮廓可得净面积)。moments 字典给出质心 (m10/m00, m01/m00)——当 m00 为 0(退化轮廓)时需防除零。宽高比和延展率是最简单的形状描述符,用于形状分类。
import cv2
img = cv2.imread("shapes.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
area = cv2.contourArea(c) # area in pixels
peri = cv2.arcLength(c, True) # perimeter (closed=True)
M = cv2.moments(c) # spatial moments
cx = int(M['m10'] / M['m00']) # centroid x
cy = int(M['m01'] / M['m00']) # centroid y
# bounding box
x, y, w, h = cv2.boundingRect(c)
aspect = w / float(h) # aspect ratio
extent = area / float(w * h) # filled area ratio
print(f"area={area:.0f} peri={peri:.1f} center=({cx},{cy}) aspect={aspect:.2f}")外接矩形
boundingRect 给出轴对齐框(快且简单);minAreaRect 给出最小旋转矩形(用于测量物体真实方向/尺寸)。boxPoints 将旋转矩形转为 4 个角点,可传给 drawContours。minEnclosingCircle 是包含轮廓的最小圆。
import cv2
img = cv2.imread("shapes.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
# axis-aligned bounding rectangle
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
# minimum area (rotated) rectangle
rect = cv2.minAreaRect(c) # ((cx, cy), (w, h), angle)
box = cv2.boxPoints(rect) # 4 corner points
box = np.int0(box)
cv2.drawContours(img, [box], 0, (0, 0, 255), 2)
# minimum enclosing circle
(cx, cy), r = cv2.minEnclosingCircle(c)
cv2.circle(img, (int(cx), int(cy)), int(r), (255, 0, 0), 2)轮廓近似
approxPolyDP 用 Douglas-Peucker 算法将轮廓简化为更少顶点的多边形。epsilon(通常为周长的 1-5%)控制容差:越小保留细节越多。数顶点是经典的手写形状分类器——三角形=3、矩形=4、圆形=很多。
import cv2
img = cv2.imread("shapes.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
peri = cv2.arcLength(c, True)
# approxPolyDP: simplify the contour to fewer vertices
eps = 0.04 * peri # 4% of perimeter
approx = cv2.approxPolyDP(c, eps, True)
n = len(approx)
if n == 3:
label = "triangle"
elif n == 4:
label = "rectangle"
elif n == 5:
label = "pentagon"
else:
label = f"{n}-gon"
cv2.drawContours(img, [approx], -1, (0, 255, 0), 2)
cv2.putText(img, label, tuple(approx[0][0]),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)凸包与缺陷
凸包是包围轮廓的最小凸形状——用于测量物体紧凑度,以及通过凸性缺陷做手/手指检测。每个缺陷的深度衡量轮廓相对凸包'内陷'的程度;手部轮廓上的深缺陷通常对应手指间的间隙。
import cv2
img = cv2.imread("hand.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
c = max(contours, key=cv2.contourArea)
# convex hull (smallest convex polygon enclosing the contour)
hull = cv2.convexHull(c)
cv2.drawContours(img, [hull], -1, (0, 255, 0), 2)
# convexity defects (gaps between contour and hull) — needs returnPoints=False
hull_idx = cv2.convexHull(c, returnPoints=False)
defects = cv2.convexityDefects(c, hull_idx)
# defects[i] = (start, end, far, depth) — depth in 1/256 of peri
# is the contour convex?
is_convex = cv2.isContourConvex(c)阈值化
基础阈值
THRESH_BINARY:高于→255、低于→0。THRESH_TRUNC:高于→截断为阈值、低于→不变。THRESH_TOZERO:高于→不变、低于→0。返回的 ret 在普通阈值时等于输入阈值,但在 Otsu/Triangle 时才有意义。按希望如何处理范围外像素来选择类型。
import cv2
img = cv2.imread("doc.png", cv2.IMREAD_GRAYSCALE)
# threshold(src, thresh, maxval, type)
# returns (retval, binary_image)
ret, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
# threshold types
_, inv = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
_, trunc = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)
_, tozero = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
_, tozero_inv = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)
# ret is the threshold actually used (useful with Otsu)自适应阈值
自适应阈值基于每个像素的局部邻域计算不同阈值——对光照不均的图像(如文档照片)必不可少。blockSize 必须为奇数(通常 11-31)且大于特征尺寸。C 从局部均值中减去一个常量;调大可抑制低对比度噪声。
import cv2
img = cv2.imread("doc.png", cv2.IMREAD_GRAYSCALE)
img = cv2.medianBlur(img, 5)
# adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)
# adaptiveMethod:
# ADAPTIVE_THRESH_MEAN_C -> mean of neighborhood minus C
# ADAPTIVE_THRESH_GAUSSIAN_C -> weighted mean minus C
adap_mean = cv2.adaptiveThreshold(img, 255,
cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)
adap_gauss = cv2.adaptiveThreshold(img, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)Otsu 方法
Otsu 方法通过最大化前景与背景类间方差找到最优全局阈值——传入 thresh=0 并加 THRESH_OTSU 标志。务必先模糊;Otsu 对直方图噪声敏感。Triangle 方法对单一主峰图像(如显微图)效果更好。返回的 ret 即计算所得阈值。
import cv2
img = cv2.imread("doc.png", cv2.IMREAD_GRAYSCALE)
blurred = cv2.GaussianBlur(img, (5, 5), 0)
# Otsu: automatically picks the threshold that maximizes between-class variance
ret, otsu = cv2.threshold(blurred, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Otsu threshold: {ret}")
# Triangle method (good for skewed histograms / single peak)
ret_t, tri = cv2.threshold(blurred, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
print(f"Triangle threshold: {ret_t}")颜色阈值(inRange)
inRange 是标准的颜色分割工具——先转 HSV,因为它将颜色(色相)与亮度分离,使阈值对光照变化更鲁棒。红色色相在 0/180 处回绕,因此需要两个掩膜做 OR。inRange 前务必模糊以减少掩膜中的噪点。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# detect a range of green pixels
lower = np.array([35, 50, 50])
upper = np.array([85, 255, 255])
mask = cv2.inRange(hsv, lower, upper)
# apply mask to the original
result = cv2.bitwise_and(img, img, mask=mask)
# hue wraps around for red (0-10 and 170-180)
mask1 = cv2.inRange(hsv, np.array([0, 50, 50]), np.array([10, 255, 255]))
mask2 = cv2.inRange(hsv, np.array([170, 50, 50]), np.array([180, 255, 255]))
red_mask = cv2.bitwise_or(mask1, mask2)阈值流水线
去噪 → 阈值 → 形态学流水线处理大多数二值化任务。光照均匀选 Otsu,不均选自适应。形态学先闭后开可同时去除内部孔洞和外部斑点。务必检查二值掩膜——阈值参数几乎总是因图而异,需按数据集调参。
import cv2
import numpy as np
img = cv2.imread("doc.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 1. denoise
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 2. choose a method
# - flat lighting -> Otsu
# - uneven lighting -> adaptive
# - document scan -> adaptive Gaussian
if lighting == "flat":
_, binary = cv2.threshold(blurred, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
else:
binary = cv2.adaptiveThreshold(blurred, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 21, 5)
# 3. morphology cleanup
kernel = np.ones((3, 3), np.uint8)
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)OCR 二值化
对 OCR,自适应高斯在拍摄文档上几乎总优于全局 Otsu,因为光照不均。blockSize 应匹配字符高度(经验法则:笔画宽度的 1-2 倍 × 4)。大多数 OCR 引擎(Tesseract)偏好黑字白底——若二值图相反则需反转。
import cv2
img = cv2.imread("text.png", cv2.IMREAD_GRAYSCALE)
# adaptive Gaussian with a block size matched to character height
# (e.g. char ~25 px -> blockSize ~31)
binary = cv2.adaptiveThreshold(img, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10)
# invert if needed (OCR often prefers black-on-white)
binary_inv = cv2.bitwise_not(binary)
# Otsu on a preprocessed image as an alternative
blur = cv2.GaussianBlur(img, (3, 3), 0)
_, otsu = cv2.threshold(blur, 0, 255,
cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)色彩空间
BGR / RGB / 灰度
OpenCV 默认以 BGR 读取图像;大多数其他库(matplotlib、PIL、scikit-image)用 RGB。忘记 cvtColor 是'红蓝互换'最常见的原因。灰度转换使用感知加权 (Y = 0.299R + 0.587G + 0.114B),而非简单平均。
import cv2
bgr = cv2.imread("photo.jpg") # OpenCV default: BGR
# BGR -> RGB (for matplotlib, PIL, etc.)
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
# BGR -> grayscale (luminance-weighted)
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
# RGB -> BGR (reverse)
bgr2 = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
# grayscale -> BGR (replicates channel, color image with no color)
bgr_from_gray = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)BGR 转 HSV
OpenCV 的 HSV 中 H 范围为 0-179(非 0-360)以适配 uint8——这是从其他库移植代码时常见的差 2 倍 bug 来源。S 和 V 为 0-255。HSV 是基于颜色的分割的首选空间,因为色相对亮度变化大致不变——用于按颜色跟踪物体。
import cv2
import numpy as np
bgr = cv2.imread("photo.jpg")
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
# split channels
h, s, v = cv2.split(hsv)
# HSV ranges in OpenCV: H: 0-179, S: 0-255, V: 0-255
# (H is halved so it fits in 8-bit — full hue circle is 0-360)
print(h.max(), s.max(), v.max()) # 179 255 255
# back to BGR
bgr2 = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)BGR 转 LAB
CIELAB 近似人类感知——相等的数值变化看起来大致同等不同,因此用于色差度量(ΔE)。L 通道仅编码亮度,所以编辑 L 是在不偏色情况下提亮图像的最干净方式。OpenCV 的 LAB 所有通道都用 0-255,而非 L 的标准 0-100。
import cv2
bgr = cv2.imread("photo.jpg")
# BGR -> LAB (perceptually uniform color space)
lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB)
L, A, B = cv2.split(lab)
# L: lightness (0-255), A: green<->red, B: blue<->yellow
# useful for separating luminance from color
# LAB is great for color-difference metrics (delta-E)
# and for editing lightness without affecting color
import numpy as np
lab_bright = lab.copy()
lab_bright[:, :, 0] = np.clip(lab_bright[:, :, 0].astype(int) + 30, 0, 255)
bgr_bright = cv2.cvtColor(lab_bright, cv2.COLOR_LAB2BGR)颜色跟踪(HSV)
基于 HSV 的颜色跟踪是最简单的物体跟踪器——选定色相范围、阈值化、找最大块。它在光照差(低饱和度)、背景有相似色物体以及反光表面时会失效。鲁棒跟踪需结合 camshift/meanshift(使用色相直方图反向投影)或卡尔曼滤波平滑运动。
import cv2
import numpy as np
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
# track a blue object
lower = np.array([100, 80, 80])
upper = np.array([130, 255, 255])
mask = cv2.inRange(hsv, lower, upper)
# find the largest blob
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if contours:
c = max(contours, key=cv2.contourArea)
if cv2.contourArea(c) > 500:
(x, y), r = cv2.minEnclosingCircle(c)
cv2.circle(frame, (int(x), int(y)), int(r), (0, 255, 255), 2)
cv2.imshow("frame", frame)
cv2.imshow("mask", mask)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()分离与合并通道
split/merge 方便但比 NumPy 索引慢——img[:, :, 0] 是蓝通道,修改它比 split 更快。务必按目标色彩空间的正确顺序向 merge 传入通道。'将两个通道置零'是可视化单通道颜色贡献的经典技巧。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
# split into B, G, R channels (each is a 2-D array)
b, g, r = cv2.split(img)
# merge channels back (order matters!)
merged = cv2.merge([b, g, r])
# zero out one channel to visualize
zeros = np.zeros_like(b)
blue_only = cv2.merge([b, zeros, zeros])
green_only = cv2.merge([zeros, g, zeros])
red_only = cv2.merge([zeros, zeros, r])
# swap channels (e.g. fake RGB->BGR by swapping R and B)
swapped = cv2.merge([r, g, b])其他色彩空间
YCrCb 将亮度 (Y) 与色度 (Cr, Cb) 分离——JPEG 和 MPEG 压缩的基础,它丢弃色度细节因为人眼对颜色不如对亮度敏感。HLS 与 HSV 的区别在于白色位于 L=1 而与饱和度无关,使 HLS 在某些阴影效果上更好。选择能让你的操作对你不关心的因素保持不变的空间。
import cv2
bgr = cv2.imread("photo.jpg")
# YCrCb (used in JPEG compression)
ycrcb = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb)
# HLS (Hue, Lightness, Saturation — alternative to HSV)
hls = cv2.cvtColor(bgr, cv2.COLOR_BGR2HLS)
# YUV (used in video encoding)
yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV)
# grayscale -> all spaces start from BGR, so chain conversions
gray_from_ycrcb = cv2.cvtColor(ycrcb, cv2.COLOR_YCrCb2BGR)
gray = cv2.cvtColor(gray_from_ycrcb, cv2.COLOR_BGR2GRAY)直方图
计算直方图
calcHist 接受图像列表、通道列表、可选掩膜(None = 全图)、直方条数和值范围。多维直方图(如二维色相-饱和度)传入两个通道和两个直方条数——用于颜色跟踪的直方图反向投影。每个通道 的直方图独立计算。
import cv2
import numpy as np
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# calcHist(images, channels, mask, histSize, ranges)
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
print(hist.shape) # (256, 1)
# normalize to 0-1
hist /= hist.sum()
# multi-channel histogram (color)
bgr = cv2.imread("photo.jpg")
colors = ('b', 'g', 'r')
for i, col in enumerate(colors):
h = cv2.calcHist([bgr], [i], None, [256], [0, 256])绘制直方图
Matplotlib 是可视化直方图最简单的方式——但记得将 BGR 转 RGB 以匹配图例颜色,否则绘图颜色与通道标签不对应。直方图偏左是暗图,偏右是亮图;窄尖峰意味着低对比度。可用此诊断曝光和白平衡问题。
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("photo.jpg")
colors = ('b', 'g', 'r')
for i, col in enumerate(colors):
hist = cv2.calcHist([img], [i], None, [256], [0, 256])
plt.plot(hist, color=col)
plt.xlim([0, 256])
plt.title("Color Histogram")
plt.xlabel("Pixel value")
plt.ylabel("Frequency")
plt.show()直方图均衡化
直方图均衡化拉伸像素强度以使用全部 0-255 范围——对低对比度图很好,但可能过曝高光并放大噪声。绝不直接均衡三个 BGR 通道——会偏色。应转到亮度/色度空间(YCrCb、LAB、HSV),均衡亮度通道,再转回。
import cv2
img = cv2.imread("dark.jpg", cv2.IMREAD_GRAYSCALE)
# global histogram equalization
eq = cv2.equalizeHist(img)
# equalization on color images: convert to YCrCb, equalize Y only
bgr = cv2.imread("color.jpg")
ycrcb = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb)
ycrcb[:, :, 0] = cv2.equalizeHist(ycrcb[:, :, 0])
eq_color = cv2.cvtColor(ycrcb, cv2.COLOR_YCrCb2BGR)CLAHE(自适应均衡化)
CLAHE 是全局均衡化的生产级替代——它在小块上操作,因此适用于光照不均的图像,clipLimit 限制对比度放大以避免放大噪声。tileGridSize 默认 8x8;更小块给更多局部自适应。是医学影像和文档增强的首选。
import cv2
img = cv2.imread("dark.jpg", cv2.IMREAD_GRAYSCALE)
# CLAHE: Contrast Limited Adaptive Histogram Equalization
# equalizes small tiles, limiting amplification of noise
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(img)
# on color: equalize the L channel of LAB
bgr = cv2.imread("color.jpg")
lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
lab[:, :, 0] = clahe.apply(lab[:, :, 0])
enhanced_color = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)直方图反向投影
直方图反向投影回答'哪些像素看起来像我的 ROI 的颜色分布?'。从样本建立二维色相-饱和度直方图,再反向投 影到目标图像得到概率图。这是 meanShift/camShift 跟踪的基础——比固定 HSV 范围鲁棒得多,因为它学习了目标的真实颜色分布。
import cv2
import numpy as np
img = cv2.imread("scene.jpg")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# take a small ROI of the target color (the object to track)
roi = hsv[100:150, 100:150]
roi_hist = cv2.calcHist([roi], [0, 1], None, [180, 256], [0, 180, 0, 256])
cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX)
# backproject: each pixel = probability of belonging to the ROI's color
backproj = cv2.calcBackProject([hsv], [0, 1], roi_hist, [0, 180, 0, 256], 1)
# denoise and threshold
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
backproj = cv2.filter2D(backproj, -1, disc)
_, mask = cv2.threshold(backproj, 50, 255, cv2.THRESH_BINARY)
result = cv2.bitwise_and(img, img, mask=mask)特征检测
Harris 角点检测器
Harris 通过检查小窗口在任何方向移动是否引起大幅强度变化来检测角点——角点在所有方向都变化,边缘只在一个方向,平坦区域无变化。输出是角点响应图;相对最大值阈值化(如 0.01 * max)。blockSize 是结构张量计算的邻域大小。
import cv2
import numpy as np
img = cv2.imread("chessboard.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# cornerHarris(gray, blockSize, ksize, k)
# blockSize: neighborhood for corner detection
# ksize: Sobel aperture (3, 5, 7)
# k: Harris free parameter (0.04 - 0.06)
dst = cv2.cornerHarris(gray, 2, 3, 0.04)
# dilate to mark corners
dst = cv2.dilate(dst, None)
# threshold and mark in red
img[dst > 0.01 * dst.max()] = [0, 0, 255]Shi-Tomasi(goodFeaturesToTrack)
Shi-Tomasi 改进 Harris,使用 min(特征值1, 特征值2) 作为评分,给出更稳定的跟踪角点。maxCorners 限制数量;qualityLevel 是最强角点响应的分数(0.01 = 最优的 1%);minDistance 强制检测角点间的空间间隔。这是 Lucas-Kanade 光流跟踪器的默认输入。
import cv2
import numpy as np
img = cv2.imread("photo.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# goodFeaturesToTrack returns the N strongest corners
corners = cv2.goodFeaturesToTrack(gray, maxCorners=100,
qualityLevel=0.01,
minDistance=10)
corners = np.int0(corners)
for c in corners:
x, y = c.ravel()
cv2.circle(img, (x, y), 3, (0, 0, 255), -1)SIFT(尺度不变特征)
SIFT 找出尺度和旋转不变的关键点及每个的 128 维描述子——对光照、视角和部分遮挡鲁棒。曾专利多年,在 OpenCV 4.4 起免费。用于速度次之的高精度匹配。DRAW_RICH_KEYPOINTS 绘制每个关键点的尺度和方向。
import cv2
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# SIFT was patented; free since OpenCV 4.4 (contrib not required)
sift = cv2.SIFT_create()
# detect keypoints
kp = sift.detect(img, None)
# detect and compute descriptors
kp, des = sift.detectAndCompute(img, None)
print(len(kp), des.shape) # N, (N, 128)
# draw keypoints
out = cv2.drawKeypoints(img, kp, None,
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)ORB(FAST + BRIEF)
ORB 结合 FAST 关键点检测器和 BRIEF 描述子——二者均无专利且比 SIFT 快约 10 倍,使 ORB 成为移动/嵌入式实时匹配的标准选择。描述子为二值(32 字节 vs SIFT 的 128 个浮点),故匹配用汉明距离。对尺度/旋转鲁棒性略逊 SIFT,但性价比极佳。
import cv2
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# ORB: free, fast alternative to SIFT
orb = cv2.ORB_create(nfeatures=1000)
kp, des = orb.detectAndCompute(img, None)
print(len(kp), des.shape) # N, (N, 32) — binary descriptors
# draw keypoints
out = cv2.drawKeypoints(img, kp, None, color=(0, 255, 0),
flags=0)
# FAST alone (detector only, no descriptor)
fast = cv2.FastFeatureDetector_create(threshold=20)
kp = fast.detect(img, None)FAST 检测器
FAST 测试候选周围 16 个像素的圆圈中是否有 N 个连续像素全部比中心亮或全部暗——因短路而极快。它只返回关键点,无描述子(描述子用 BRIEF 或 ORB)。阈值越低角点越多也越噪;非极大值抑制去除局部极大值处的重复。
import cv2
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# FAST: speed-optimized corner detector (detector only)
fast = cv2.FastFeatureDetector_create(threshold=20,
nonmaxSuppression=True)
kp = fast.detect(img, None)
print(f"{len(kp)} keypoints")
# tune the threshold
fast.setThreshold(30)
print(f"threshold is now {fast.getThreshold()}")
# turn off non-max suppression to get more (clustered) corners
fast.setNonmaxSuppression(False)
kp_all = fast.detect(img, None)MSER(最大稳定极值区域)
MSER 找出在一系列强度阈值范围内保持稳定的区域——非常适合文字检测(字母呈块状)、车牌和场景文字。因每个区域在自身阈值处检测,能很好地处理不均匀光照。按宽高比和尺寸过滤结果以去除明显的非文字。
import cv2
img = cv2.imread("text.jpg", cv2.IMREAD_GRAYSCALE)
# MSER: detects stable connected regions across thresholds
mser = cv2.MSER_create()
regions, _ = mser.detectRegions(img)
# regions is a list of point arrays
out = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
for pts in regions:
x, y, w, h = cv2.boundingRect(pts)
if 5 < h < 50: # filter by likely text size
cv2.rectangle(out, (x, y), (x+w, y+h), (0, 255, 0), 1)特征匹配
暴力匹配
暴力匹配尝试每个描述子对——精确但 O(N*M)。ORB 的二值描述子用 NORM_HAMMING;SIFT/SURF 的浮点描述子用 NORM_L2。crossCheck=True 仅保留互为最佳匹配(A 的最佳是 B 且反之亦然),可去除大量误配。每个 match 有 .distance——越小越好。
import cv2
img1 = cv2.imread("box.png", cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread("scene.png", cv2.IMREAD_GRAYSCALE)
orb = cv2.ORB_create()
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
# for ORB (binary descriptors) use Hamming distance
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(des1, des2)
# sort by distance (best first)
matches = sorted(matches, key=lambda m: m.distance)
# draw top 20
out = cv2.drawMatches(img1, kp1, img2, kp2, matches[:20], None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)FLANN 匹配
FLANN(快速近似最近邻库)建立 KD 树(SIFT/SURF)或 LSH(ORB)来查找近似匹配——在大描述子集上比暴力匹配快得多,代价是偶尔漏配。trees 和 checks 参数在精度与速度间权衡。特征数为数千时用 FLANN;小集合用暴力匹配即可。
import cv2
import numpy as np
img1 = cv2.imread("box.png", cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread("scene.png", cv2.IMREAD_GRAYSCALE)
sift = cv2.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# FLANN parameters for SIFT (float descriptors)
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
flann = cv2.FlannBasedMatcher(index_params, search_params)
matches = flann.knnMatch(des1, des2, k=2)KNN 匹配与比率测试
Lowe 比率测试是特征匹配的标准过滤器:仅当最佳匹配显著比次佳更近时保留(典型阈值 0.7-0.8)。直觉是:若两个候选距离相近,则匹配有歧义、可能错误。这一测试即可去除绝大多数误配。
import cv2
img1 = cv2.imread("box.png", cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread("scene.png", cv2.IMREAD_GRAYSCALE)
sift = cv2.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
bf = cv2.BFMatcher(cv2.NORM_L2)
matches = bf.knnMatch(des1, des2, k=2) # 2 nearest neighbors
# Lowe's ratio test: keep only unambiguous matches
good = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good.append(m)
out = cv2.drawMatches(img1, kp1, img2, kp2, good, None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)单应性(RANSAC)
findHomography 配合 RANSAC 计算将 src 映射到 dst 的透视变换,同时忽略离群点——inlier 掩码告诉你哪些匹配与模型一致。重投影阈值(5.0)单位为像素;越小越严格。至少需要 4 个好匹配。用于在场景中定位已知物体或对齐重叠照片。
import cv2
import numpy as np
img1 = cv2.imread("box.png", cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread("scene.png", cv2.IMREAD_GRAYSCALE)
sift = cv2.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
bf = cv2.BFMatcher(cv2.NORM_L2)
matches = bf.knnMatch(des1, des2, k=2)
good = [m for m, n in matches if m.distance < 0.75 * n.distance]
# build point correspondences
src = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
dst = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
# RANSAC homography
H, mask = cv2.findHomography(src, dst, cv2.RANSAC, 5.0)
inliers = mask.ravel().tolist()
print(f"{sum(inliers)} inliers / {len(good)} matches")物体定位
这是经典的物体定位流水线:在两幅图中检测特征、匹配、用 RANSAC 拟合单应性、再将物体边界框投影到场景中。适用于平面物体或远处 3D 物体(透视主导)。对无纹理、对称或高度遮挡物体失效——这些用深度学习检测器。
import cv2
import numpy as np
img1 = cv2.imread("box.png", cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread("scene.png", cv2.IMREAD_GRAYSCALE)
sift = cv2.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
bf = cv2.BFMatcher(cv2.NORM_L2)
good = [m for m, n in bf.knnMatch(des1, des2, k=2)
if m.distance < 0.75 * n.distance]
src = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
dst = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
H, _ = cv2.findHomography(src, dst, cv2.RANSAC, 5.0)
# project the object's corners into the scene
h, w = img1.shape
corners = np.float32([[0, 0], [w-1, 0], [w-1, h-1], [0, h-1]]).reshape(-1, 1, 2)
scene_corners = cv2.perspectiveTransform(corners, H)
out = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)
cv2.polylines(out, [np.int32(scene_corners)], True, (0, 255, 0), 3)人脸检测
Haar 级联分类器
Haar 级联是经典的 Viola-Jones 检测器——CPU 上速度快、对正脸好,但不如现代深度学习检测器(MTCNN、SSD、RetinaFace)准确。scaleFactor(>1)控制多尺度搜索——1.1 = 10% 尺度步长;越小越慢但更彻底。minNeighbors 过滤重叠检测;调高可减少误检。
import cv2
# load a pretrained cascade (shipped with OpenCV)
cascade_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
face_cascade = cv2.CascadeClassifier(cascade_path)
print(face_cascade.empty()) # False = loaded ok
img = cv2.imread("people.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# detectMultiScale(gray, scaleFactor, minNeighbors, minSize, maxSize)
faces = face_cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)在图像中检测人脸
在人脸 ROI 内检测眼睛(或微笑)是标准的效率技巧:在全图检测眼睛会有很多误检,但在确认的人脸 ROI 内很少。cv2.data.haarcascades 指向内置 XML 目录——还有其他级联(眼、微笑、全身、车牌)。调用级联前先裁剪 ROI。
import cv2
face_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
eye_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_eye.xml")
img = cv2.imread("people.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
for (fx, fy, fw, fh) in faces:
cv2.rectangle(img, (fx, fy), (fx+fw, fy+fh), (255, 0, 0), 2)
# search for eyes INSIDE the face region (faster, fewer false positives)
roi_gray = gray[fy:fy+fh, fx:fx+fw]
roi_color = img[fy:fy+fh, fx:fx+fw]
eyes = eye_cascade.detectMultiScale(roi_gray)
for (ex, ey, ew, eh) in eyes:
cv2.rectangle(roi_color, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 2)在视频中检测人脸
得益于 Haar 级联的速度,在摄像头上实时人脸检测在 CPU 上可行。ROI 模糊技巧(仅对脸部区域做高斯模糊)是简单的隐私/匿名化技术。要跨帧跟踪人脸(避免闪烁),按 IoU 关联检测并用卡尔曼滤波平滑——级联本身没有时间连续性。
import cv2
face_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.1, 5)
for (x, y, w, h) in faces:
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
# blur the face for privacy
frame[y:y+h, x:x+w] = cv2.GaussianBlur(
frame[y:y+h, x:x+w], (25, 25), 30)
cv2.imshow("face", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()侧脸
Haar 级联是特定姿态的——正脸级联漏掉侧脸,因此需与侧脸级联组合。由于侧脸级联只训练了一侧,需水平翻转图像检测另一侧,再将坐标镜像回来。这仍会漏掉 3/4 视角;现代检测器(DNN、MTCNN)用一个模型处理所有姿态。
import cv2
profile_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_profileface.xml")
img = cv2.imread("people.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# detect left-facing profiles
profiles = profile_cascade.detectMultiScale(gray, 1.1, 5)
for (x, y, w, h) in profiles:
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2)
# also detect right-facing by flipping the image
flipped = cv2.flip(gray, 1)
profiles_r = profile_cascade.detectMultiScale(flipped, 1.1, 5)
h, w = gray.shape
for (x, y, fw, fh) in profiles_r:
cv2.rectangle(img, (w - x - fw, y), (w - x, y + fh), (255, 0, 0), 2)DNN 人脸检测
OpenCV 的 dnn 模块可运行预训练的 Caffe/TensorFlow/ONNX 模型——基于 SSD 和 ResNet 的人脸检测器远比 Haar 级联准确,且一个模型处理所有姿态。blobFromImage 一步完成均值减法和缩放。检测结果归一化到 [0,1]——乘以 (w, h, w, h) 得到像素框。模型:SSD(Caffe)、YuNet(ONNX,经 cv.FaceDetectorYN)。
import cv2
import numpy as np
model = cv2.dnn.readNetFromCaffe(
"deploy.prototxt", "weights.caffemodel")
img = cv2.imread("people.jpg")
h, w = img.shape[:2]
# build a 300x300 blob (mean subtraction + scale)
blob = cv2.dnn.blobFromImage(img, 1.0, (300, 300), (104, 177, 123))
model.setInput(blob)
detections = model.forward() # shape (1, 1, N, 7)
for i in range(detections.shape[2]):
conf = detections[0, 0, i, 2]
if conf > 0.5:
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
x1, y1, x2, y2 = box.astype(int)
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(img, f"{conf:.2f}", (x1, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)目标检测
HOG 描述子
HOG(方向梯度直方图)通过边缘方向直方图描述局部形状——与线性 SVM 结合是经典目标检测器。预训练的行人检测器适用于直立姿态的全身行人。winStride 和 scale 在速度与召回间权衡。现代深度检测器(YOLO、SSD)准确得多,但 HOG 无需模型文件即可在 CPU 运行。
import cv2
img = cv2.imread("person.jpg", cv2.IMREAD_GRAYSCALE)
# create HOG descriptor with the default detector
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
# detectMultiScale returns (rectangles, weights)
boxes, weights = hog.detectMultiScale(
img, winStride=(8, 8), padding=(4, 4), scale=1.05)
for (x, y, w, h) in boxes:
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)HOG 特征可视化
标准 HOG 窗口为 64x128 像素(INRIA 行人尺寸)。参数:window(64x128)、block(16x16)、block stride(8x8)、cell(8x8)、bins(9)。每个描述子为 3780 维。要分类自定义物体,需为大量正/负样本提取 HOG 特征并训练线性 SVM(scikit-learn),再用 setSVMDetector 设置检测器。
import cv2
img = cv2.imread("person.jpg", cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, (64, 128)) # standard HOG window
hog = cv2.HOGDescriptor((64, 128), (16, 16), (8, 8), (8, 8), 9)
descriptor = hog.compute(img)
print(descriptor.shape) # (3780, 1)
# 9 bins per cell, 7x15 cells, 4 cells per block -> 3780 features
# this is the input vector to a linear SVM classifier模板匹配
模板匹配将模板在图像上滑动并计算相似度得分——仅对无旋转、无缩放变化的精确(或近精确)匹配有效。TM_CCOEFF_NORMED 是最鲁棒的方法;结果在 [-1, 1]。对多重匹配,需对响应图阈值化并做非极大值抑制以避免重叠检测。
import cv2
import numpy as np
img = cv2.imread("scene.png", cv2.IMREAD_GRAYSCALE)
template = cv2.imread("template.png", cv2.IMREAD_GRAYSCALE)
h, w = template.shape
# matchTemplate returns a correlation map
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
# find the best match
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
out = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cv2.rectangle(out, top_left, bottom_right, (0, 255, 0), 2)
# find all matches above a threshold
loc = np.where(res >= 0.8)
for pt in zip(*loc[::-1]):
cv2.rectangle(out, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 1)多尺度模板匹配
多尺度模板匹配暴力搜索一系列尺度——慢,但在模板在图中的大小未知时可用。它仍不能处理旋转;为此需再扫角度(很慢)或改用特征匹配。生产级目标检测请用训练好的检测器(YOLO、SSD)而非模板匹配。
import cv2
import numpy as np
img = cv2.imread("scene.png", cv2.IMREAD_GRAYSCALE)
template = cv2.imread("template.png", cv2.IMREAD_GRAYSCALE)
best = (-1, None, None) # (score, location, scale)
for scale in np.linspace(0.5, 1.5, 20):
resized = cv2.resize(template, None, fx=scale, fy=scale)
rh, rw = resized.shape
if rh > img.shape[0] or rw > img.shape[1]:
continue
res = cv2.matchTemplate(img, resized, cv2.TM_CCOEFF_NORMED)
_, max_val, _, max_loc = cv2.minMaxLoc(res)
if max_val > best[0]:
best = (max_val, max_loc, scale)
score, (x, y), scale = best
h, w = template.shape
out = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
rw, rh = int(w * scale), int(h * scale)
cv2.rectangle(out, (x, y), (x + rw, y + rh), (0, 255, 0), 2)DNN 目标检测(YOLO)
OpenCV 的 dnn 模块可运行导出的 YOLO/SSD/Faster-RCNN 权重——在无法安装完整深度学习框架时很有用。YOLO 输出解析较繁琐:每个检测含中心、尺寸、objectness 和各类别得分。务必应用 NMSBoxes(非极大值抑制)将重叠框合并为一个。实时场景用 YOLOv4-tiny 或改用 ONNX + 更快后端。