Skip to content

OpenCV 速查表

开源计算机视觉与机器学习库。

01

入门

读取、显示与保存图像

OpenCV 默认以 BGR 顺序读取图像(非 RGB)——与 matplotlib 或 PIL 互操作时这是常见陷阱。cv2.waitKey(0) 会阻塞直到按键;始终调用 destroyAllWindows() 关闭窗口。JPEG 质量参数为 0-100。

opencv
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 导入。

opencv
# 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)。

opencv
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] 是操作单通道最快的方式。

opencv
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。

opencv
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
02

视频捕获

打开摄像头

VideoCapture(0) 打开默认摄像头;1、2... 对应其他摄像头。务必调用 cap.release() 释放设备——忘记释放会导致摄像头被其他程序占用。waitKey(1) 等待约 1ms;& 0xFF 技巧用于兼容 waitKey 在 64 位系统返回超过 8 位的情况。

opencv
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)时无法打开。

opencv
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() 刷新并关闭文件。

opencv
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)手动曝光才生效。

opencv
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;基准测试时应关闭显示。确定性计时可在独立线程累积帧再处理。

opencv
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()
03

绘图

直线与矩形

所有绘图函数都会就地修改图像——想保留原图请传入副本。坐标是 (x, y),与 NumPy 的 [y, x] 索引相反。颜色为 BGR 元组。thickness=-1 表示填充,cv2.LINE_AA 用于对角线的平滑(抗锯齿)。

opencv
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°=右,逆时针)。绘制部分弧是渲染饼图扇区或运动方向指示器的标准做法。

opencv
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 标志将末点连回首点。

opencv
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 返回 ((宽, 高), 基线)——便于居中文字或在文字后绘制背景框。

opencv
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 形标记。

opencv
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。务必在混合后再绘制实心文字以保持清晰;在覆盖层上绘制文字再混合会变模糊。这种模式在检测/分割可视化中无处不在。

opencv
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)
04

图像算术

加法与减法

OpenCV 的 cv2.add 在 255 处饱和(250+10=255),而 NumPy 的 + 会回绕(250+10=8,因 uint8 溢出)。对图像应始终用 cv2.add / cv2.subtract——回绕会产生可见的黑/白伪影。absdiff 是背景相减和运动检测的主力。

opencv
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。

opencv
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 提取和背景替换的基础。

opencv
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)或光流。形态学开运算去噪;膨胀合并邻近连通块。

opencv
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()——切片返回的是别名原图的视图。

opencv
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 是莫名暗/亮像素伪影的头号来源。

opencv
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
05

几何变换

缩放

INTER_AREA 最适合下采样(避免混叠),INTER_CUBIC 或 INTER_LINEAR 适合上采样。INTER_NEAREST 最快但有马赛克——用于标签/掩码图(不能引入新像素值)。dsize 元组是 (宽, 高),与 img.shape 的 (高, 宽) 相反。

opencv
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) 并平移旋转矩阵——否则大图旋转后边角会被切掉。

opencv
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 控制越界区域填充方式。

opencv
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。

opencv
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° 旋转。

opencv
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。

opencv
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)
06

图像滤波

均值模糊(盒式滤波)

均值模糊是最简单的低通滤波器——用邻域均值替换每个像素。核大小必须为正奇数。核越大模糊越强但也更快地破坏细节。仅当需要均匀加权时使用;对自然图像高斯模糊几乎总是更好的选择。

opencv
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 之前抑制噪声放大的必备前置步骤。

opencv
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)

中值模糊

中值模糊是处理椒盐(脉冲)噪声的首选滤波器——用邻域中值替换每个像素,孤立离群点被移除而非扩散。与高斯不同,它能很好地保留边缘。核大小必须为奇数。因涉及排序,比高斯/盒式慢。

opencv
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 会跨越更多颜色进行混合。

opencv
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 会提亮。非锐化掩膜(减去模糊副本)更可控,也是大多数照片编辑器实际使用的方法。

opencv
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 快得多。

opencv
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)
07

形态学操作

腐蚀与膨胀

腐蚀收缩前景(白色)区域并去除小噪点;膨胀则扩大并填充小孔。核形状很关键:RECT 通用,ELLIPSE 适合圆润特征,CROSS 用于沿轴向细化和连接。iterations=N 重复操作 N 次,等效于使用更大的核。

opencv
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))

开运算与闭运算

开运算(腐蚀→膨胀)去除小亮斑而不改变较大物体的尺寸。闭运算(膨胀→腐蚀)填充小暗孔。二者皆幂等——应用两次等于一次。经典去噪模式是先闭后开(或先开后闭)以同时处理两类缺陷。

opencv
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 不同它给出实心宽边,适合高亮区域而非测量精确边缘位置。

opencv
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 提取小暗结构。二者常用于阴影/高光提取,以及作为快速的不均匀光照校正器(除以背景的技巧在文档扫描仪中常见)。

opencv
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 更快、结果更干净。

opencv
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 表示任意。虽属小众但在精确定位模式时无可替代。

opencv
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")
08

边缘检测

Canny 边缘检测器

Canny 是最常用的边缘检测器:高斯平滑 → Sobel 梯度 → 非极大值抑制 → 滞后阈值(双阈值)。高于高阈值的像素为边缘;介于低高阈值之间的像素仅在连通到强边缘时才保留。自动阈值技巧(中值 × 0.7/1.3)对多样图像效果良好。

opencv
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 和许多特征检测器的基础。

opencv
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,因为二阶导数会大幅变负。

opencv
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。

opencv
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 阈值和去噪核大小上。

opencv
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)
09

轮廓

查找轮廓

在 OpenCV 3.x 中 findContours 会修改源图像——若后续需要二值图请传入副本。RETR_EXTERNAL 仅返回最外层轮廓(最快);RETR_TREE 给出完整层级用于嵌套形状(物体内的孔)。CHAIN_APPROX_SIMPLE 丢弃直线上的冗余点,节省内存。

opencv
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 填充轮廓。在空白图上绘制是构建轮廓掩膜的标准做法,用于后续的平均颜色或掩膜直方图等操作。

opencv
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(退化轮廓)时需防除零。宽高比和延展率是最简单的形状描述符,用于形状分类。

opencv
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 是包含轮廓的最小圆。

opencv
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、圆形=很多。

opencv
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)

凸包与缺陷

凸包是包围轮廓的最小凸形状——用于测量物体紧凑度,以及通过凸性缺陷做手/手指检测。每个缺陷的深度衡量轮廓相对凸包'内陷'的程度;手部轮廓上的深缺陷通常对应手指间的间隙。

opencv
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)
10

阈值化

基础阈值

THRESH_BINARY:高于→255、低于→0。THRESH_TRUNC:高于→截断为阈值、低于→不变。THRESH_TOZERO:高于→不变、低于→0。返回的 ret 在普通阈值时等于输入阈值,但在 Otsu/Triangle 时才有意义。按希望如何处理范围外像素来选择类型。

opencv
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 从局部均值中减去一个常量;调大可抑制低对比度噪声。

opencv
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 即计算所得阈值。

opencv
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 前务必模糊以减少掩膜中的噪点。

opencv
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,不均选自适应。形态学先闭后开可同时去除内部孔洞和外部斑点。务必检查二值掩膜——阈值参数几乎总是因图而异,需按数据集调参。

opencv
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)偏好黑字白底——若二值图相反则需反转。

opencv
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)
11

色彩空间

BGR / RGB / 灰度

OpenCV 默认以 BGR 读取图像;大多数其他库(matplotlib、PIL、scikit-image)用 RGB。忘记 cvtColor 是'红蓝互换'最常见的原因。灰度转换使用感知加权 (Y = 0.299R + 0.587G + 0.114B),而非简单平均。

opencv
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 是基于颜色的分割的首选空间,因为色相对亮度变化大致不变——用于按颜色跟踪物体。

opencv
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。

opencv
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(使用色相直方图反向投影)或卡尔曼滤波平滑运动。

opencv
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 传入通道。'将两个通道置零'是可视化单通道颜色贡献的经典技巧。

opencv
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 在某些阴影效果上更好。选择能让你的操作对你不关心的因素保持不变的空间。

opencv
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)
12

直方图

计算直方图

calcHist 接受图像列表、通道列表、可选掩膜(None = 全图)、直方条数和值范围。多维直方图(如二维色相-饱和度)传入两个通道和两个直方条数——用于颜色跟踪的直方图反向投影。每个通道的直方图独立计算。

opencv
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 以匹配图例颜色,否则绘图颜色与通道标签不对应。直方图偏左是暗图,偏右是亮图;窄尖峰意味着低对比度。可用此诊断曝光和白平衡问题。

opencv
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),均衡亮度通道,再转回。

opencv
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;更小块给更多局部自适应。是医学影像和文档增强的首选。

opencv
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 范围鲁棒得多,因为它学习了目标的真实颜色分布。

opencv
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)
13

特征检测

Harris 角点检测器

Harris 通过检查小窗口在任何方向移动是否引起大幅强度变化来检测角点——角点在所有方向都变化,边缘只在一个方向,平坦区域无变化。输出是角点响应图;相对最大值阈值化(如 0.01 * max)。blockSize 是结构张量计算的邻域大小。

opencv
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 光流跟踪器的默认输入。

opencv
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 绘制每个关键点的尺度和方向。

opencv
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,但性价比极佳。

opencv
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)。阈值越低角点越多也越噪;非极大值抑制去除局部极大值处的重复。

opencv
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 找出在一系列强度阈值范围内保持稳定的区域——非常适合文字检测(字母呈块状)、车牌和场景文字。因每个区域在自身阈值处检测,能很好地处理不均匀光照。按宽高比和尺寸过滤结果以去除明显的非文字。

opencv
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)
14

特征匹配

暴力匹配

暴力匹配尝试每个描述子对——精确但 O(N*M)。ORB 的二值描述子用 NORM_HAMMING;SIFT/SURF 的浮点描述子用 NORM_L2。crossCheck=True 仅保留互为最佳匹配(A 的最佳是 B 且反之亦然),可去除大量误配。每个 match 有 .distance——越小越好。

opencv
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;小集合用暴力匹配即可。

opencv
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)。直觉是:若两个候选距离相近,则匹配有歧义、可能错误。这一测试即可去除绝大多数误配。

opencv
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 个好匹配。用于在场景中定位已知物体或对齐重叠照片。

opencv
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 物体(透视主导)。对无纹理、对称或高度遮挡物体失效——这些用深度学习检测器。

opencv
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)
15

人脸检测

Haar 级联分类器

Haar 级联是经典的 Viola-Jones 检测器——CPU 上速度快、对正脸好,但不如现代深度学习检测器(MTCNN、SSD、RetinaFace)准确。scaleFactor(>1)控制多尺度搜索——1.1 = 10% 尺度步长;越小越慢但更彻底。minNeighbors 过滤重叠检测;调高可减少误检。

opencv
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。

opencv
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 关联检测并用卡尔曼滤波平滑——级联本身没有时间连续性。

opencv
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)用一个模型处理所有姿态。

opencv
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)。

opencv
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)
16

目标检测

HOG 描述子

HOG(方向梯度直方图)通过边缘方向直方图描述局部形状——与线性 SVM 结合是经典目标检测器。预训练的行人检测器适用于直立姿态的全身行人。winStride 和 scale 在速度与召回间权衡。现代深度检测器(YOLO、SSD)准确得多,但 HOG 无需模型文件即可在 CPU 运行。

opencv
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 设置检测器。

opencv
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]。对多重匹配,需对响应图阈值化并做非极大值抑制以避免重叠检测。

opencv
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)而非模板匹配。

opencv
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 + 更快后端。

opencv
import cv2
import numpy as np

# load a YOLO model (weights + cfg)
net = cv2.dnn.readNet("yolov3.weights", "yolov3.cfg")
with open("coco.names") as f:
    classes = [line.strip() for line in f.readlines()]

layer_names = net.getLayerNames()
out_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]

img = cv2.imread("street.jpg")
blob = cv2.dnn.blobFromImage(img, 1/255.0, (416, 416), swapRB=True, crop=False)
net.setInput(blob)
outputs = net.forward(out_layers)

# parse YOLO output: each detection is [cx, cy, w, h, obj, class_scores...]
h, w = img.shape[:2]
boxes, confs, ids = [], [], []
for out in outputs:
    for det in out:
        scores = det[5:]
        cls = int(np.argmax(scores))
        conf = scores[cls]
        if conf > 0.5:
            cx, cy, bw, bh = (det[:4] * [w, h, w, h]).astype(int)
            boxes.append([cx - bw//2, cy - bh//2, bw, bh])
            confs.append(float(conf))
            ids.append(cls)

idxs = cv2.dnn.NMSBoxes(boxes, confs, 0.5, 0.4)   # non-max suppression
17

图像分割

K-Means 聚类

K-means 按颜色聚类像素——将图像色调分离或分割为 K 个主色的简单方法。以 float32 像素传入 (N, 3)(或加入 xy 坐标做空间聚类的 (N, 5))。增大 K 给出更细的颜色分离。要空间连贯,为每个像素附加 (x, y) 坐标;纯颜色量化则只用 BGR。

opencv
import cv2
import numpy as np

img = cv2.imread("photo.jpg")
h, w = img.shape[:2]

# reshape to a list of pixels (N, 3) in float32
pixels = img.reshape(-1, 3).astype(np.float32)

# kmeans(data, K, criteria, attempts, flags)
K = 4
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
_, labels, centers = cv2.kmeans(pixels, K, None, criteria, 10,
                                cv2.KMEANS_RANDOM_CENTERS)

# rebuild the image with cluster centers
centers = np.uint8(centers)
seg = centers[labels.flatten()].reshape((h, w, 3))

GrabCut

GrabCut 是交互式前景提取器——给定边界矩形,它用高斯混合模型迭代建模前景/背景。要更精细的结果,在手动标注像素(确定前景/确定背景)后用 GC_INIT_WITH_MASK 细化。对单一显著物体置于相对简单背景前效果很好。

opencv
import cv2
import numpy as np

img = cv2.imread("person.jpg")
h, w = img.shape[:2]

# initialize mask (all probable background)
mask = np.zeros((h, w), np.uint8)

# define a rectangle around the foreground object
rect = (50, 50, w - 100, h - 100)

# internal scratch arrays
bgd = np.zeros((1, 65), np.float64)
fgd = np.zeros((1, 65), np.float64)

# run GrabCut: 5 iterations
cv2.grabCut(img, mask, rect, bgd, fgd, 5, cv2.GC_INIT_WITH_RECT)

# mask values: 0=bg, 1=fg, 2=probable bg, 3=probable fg
fg_mask = np.where((mask == 1) | (mask == 3), 255, 0).astype(np.uint8)
result = cv2.bitwise_and(img, img, mask=fg_mask)

分水岭

分水岭将图像视为地形图(强度=海拔),从种子标记'淹没'盆地——没有好种子会产生严重过分割。距离变换技巧为相接物体(如重叠硬币)生成确定前景种子。标记:0=未知、1=背景、2..N=物体、-1=边界。这是经典的'分离相接物体'流水线。

opencv
import cv2
import numpy as np

img = cv2.imread("coins.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# threshold to separate coins from background
_, binary = cv2.threshold(gray, 0, 255,
                          cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# noise removal
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2)

# sure background area
sure_bg = cv2.dilate(opening, kernel, iterations=3)

# sure foreground: distance transform + threshold
dist = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
_, sure_fg = cv2.threshold(dist, 0.5 * dist.max(), 255, 0)
sure_fg = np.uint8(sure_fg)

# unknown region (between sure fg and sure bg)
unknown = cv2.subtract(sure_bg, sure_fg)

# marker labelling
_, markers = cv2.connectedComponents(sure_fg)
markers = markers + 1
markers[unknown == 255] = 0

# watershed modifies markers in place
markers = cv2.watershed(img, markers)
img[markers == -1] = [255, 0, 0]   # boundaries in red

漫水填充

floodFill 是魔棒工具——从种子像素出发,填充所有颜色在 loDiff/upDiff 范围内(相对种子 FIXED_RANGE 或相对前一像素)的连通像素。掩膜必须比图像大 2 像素(每侧 1 像素边框)。适用于区域生长、简单背景替换和交互式选择。

opencv
import cv2

img = cv2.imread("photo.jpg")
h, w = img.shape[:2]
mask = np.zeros((h + 2, w + 2), np.uint8)

# floodFill fills a connected region similar to the seed pixel
# (image, mask, seedPoint, newVal, loDiff, upDiff, flags)
cv2.floodFill(img, mask, (100, 100), (0, 255, 0),
              loDiff=(20, 20, 20), upDiff=(20, 20, 20),
              flags=8 | cv2.FLOODFILL_FIXED_RANGE)

# FLOODFILL_FIXED_RANGE: compare to the seed pixel (strict)
# 4-connectivity (4) or 8-connectivity (8)

# extract the filled mask
filled_mask = mask[1:-1, 1:-1]   # the mask is padded by 1

连通分量

connectedComponents 为每个白色区域标注唯一整数——当你只需计数、面积和质心时比 findContours 更快。connectedComponentsWithStats 还返回外接框和面积。标签 0 始终是背景。默认连通性为 8(含对角);传 connectivity=4 更严格。用于粒子计数和斑块分析。

opencv
import cv2
import numpy as np

img = cv2.imread("blobs.png", cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

# connectedComponents returns (num_labels, labels)
num, labels = cv2.connectedComponents(binary)
print(f"found {num - 1} objects (label 0 is background)")

# connectedComponentsWithStats also gives area and bounding box
num, labels, stats, centroids = cv2.connectedComponentsWithStats(binary)
for i in range(1, num):
    x, y, w, h, area = stats[i]
    cx, cy = centroids[i]
    cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 1)
    cv2.putText(img, str(area), (x, y - 5),
                cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1)

均值漂移 / pyrMeanShiftFiltering

pyrMeanShiftFiltering 是一种非参数分割,在保留边缘的同时'扁平化'相似颜色区域——常作为基于轮廓的分割之前的预处理。sp(空间半径)控制算法在空间上查看多远;sr(颜色半径)控制颜色容差。值越大区域越大越平滑。

opencv
import cv2

img = cv2.imread("photo.jpg")

# pyrMeanShiftFiltering: edge-preserving smoothing that homogenizes regions
# (spatialRadius, colorRadius) — larger values merge more
filtered = cv2.pyrMeanShiftFiltering(img, sp=20, sr=40)

# now segment on the filtered image
gray = cv2.cvtColor(filtered, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 0, 255,
                          cv2.THRESH_BINARY + cv2.THRESH_OTSU)

contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL,
                               cv2.CHAIN_APPROX_SIMPLE)
out = img.copy()
cv2.drawContours(out, contours, -1, (0, 255, 0), 2)
18

相机标定

棋盘标定

相机标定从已知图案的图像中求出内参矩阵(焦距、主点)和镜头畸变系数(棋盘是标准图案,因为角点易检测)。需约 10-20 张不同角度的图像。cornerSubPix 将角点精炼到亚像素精度——对标定质量至关重要。传给 findChessboardCorners 的图案尺寸是内部角点数(方格数-1)。

opencv
import cv2
import numpy as np
import glob

# prepare object points (0,0,0), (1,0,0), ..., for a 7x6 board
objp = np.zeros((6 * 7, 3), np.float32)
objp[:, :2] = np.mgrid[0:7, 0:6].T.reshape(-1, 2)

objpoints = []   # 3-D real-world points
imgpoints = []   # 2-D image points

for fname in glob.glob("calib/*.jpg"):
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # findChessboardCorners returns corners (or None)
    ret, corners = cv2.findChessboardCorners(gray, (7, 6), None)
    if ret:
        # refine to sub-pixel accuracy
        corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1),
            (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
        objpoints.append(objp)
        imgpoints.append(corners2)

# calibrate
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None)
print("camera matrix:\n", mtx)

去畸变

undistort 用标定参数去除径向和切向镜头畸变。对视频,用 initUndistortRectifyMap 预先计算映射表,每帧用 remap 应用——比反复调用 undistort 快得多。alpha=1 的 getOptimalNewCameraMatrix 保留所有像素(带黑边);alpha=0 裁剪到有效区域。

opencv
import cv2
import numpy as np

# mtx, dist come from calibrateCamera
img = cv2.imread("distorted.jpg")
h, w = img.shape[:2]

# method 1: simple undistort
undistorted = cv2.undistort(img, mtx, dist)

# method 2: pre-compute the maps for speed (run once, reuse many times)
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(
    mtx, dist, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(
    mtx, dist, None, newcameramtx, (w, h), cv2.CV_16SC2)
undistorted2 = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

# crop to the valid ROI
x, y, rw, rh = roi
undistorted2 = undistorted2[y:y+rh, x:x+rw]

位姿估计(ArUco)

ArUco 标记是方形基准标记(类似二维码),用于位姿估计——给定标记的物理尺寸和相机标定,可得其相对于相机的 3D 旋转(rvec)和平移(tvec)。在 opencv-contrib 中可用。drawFrameAxes 绘制标记的局部 X/Y/Z 轴——红=X、绿=Y、蓝=Z,长度单位为米。

opencv
import cv2

# load a predefined ArUco dictionary
aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
params = cv2.aruco.DetectorParameters()
detector = cv2.aruco.ArucoDetector(aruco_dict, params)

img = cv2.imread("marker.png")
corners, ids, rejected = detector.detectMarkers(img)

if ids is not None:
    cv2.aruco.drawDetectedMarkers(img, corners, ids)

    # estimate pose (marker size in meters, camera matrix, distortion)
    rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
        corners, 0.05, mtx, dist)
    for rvec, tvec in zip(rvecs, tvecs):
        cv2.drawFrameAxes(img, mtx, dist, rvec, tvec, 0.05)

基准位姿(SolvePnP)

solvePnP(Perspective-n-Point)从 N 个 3D 到 2D 的对应关系恢复相机位姿——至少需要 4 个点。精度用 SOLVEPNP_ITERATIVE(默认),平面目标用 SOLVEPNP_IPPE。rvec 是旋转向量(轴 × 角);用 Rodrigues 转为 3x3 矩阵。这是基于标记的 AR 和 6 自由度跟踪的基础。

opencv
import cv2
import numpy as np

# 3-D coordinates of known object points (e.g. a cube corner)
obj_points = np.array([
    [0, 0,  0], [1, 0,  0], [1, 1,  0], [0, 1,  0],
    [0, 0, -1], [1, 0, -1], [1, 1, -1], [0, 1, -1],
], dtype=np.float32)

# corresponding 2-D image points (detected via feature matching etc.)
img_points = np.array([
    [120, 90], [220, 88], [225, 190], [115, 195],
    [80, 60],  [240, 55], [248, 210], [70, 215],
], dtype=np.float32)

# solvePnP(objectPoints, imagePoints, cameraMatrix, distCoeffs)
ret, rvec, tvec = cv2.solvePnP(obj_points, img_points, mtx, dist)
print("rotation:", rvec.ravel())
print("translation:", tvec.ravel())

# project the 3-D points back to verify
proj, _ = cv2.projectPoints(obj_points, rvec, tvec, mtx, dist)

重投影误差

重投影误差是标定质量的金标准——它衡量投影的 3D 点落在检测的 2D 点多远。好的标定平均低于 0.5 像素;高于 1 像素通常说明图像集差、图案尺寸错误或有运动模糊。逐图误差有助于找出并丢弃不好的标定图像。

opencv
import cv2
import numpy as np

# after calibrateCamera or solvePnP, check accuracy
mean_err = 0
total = 0
for i in range(len(objpoints)):
    # project the 3-D points using the estimated pose
    proj, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    err = cv2.norm(imgpoints[i], proj, cv2.NORM_L2) / len(proj)
    mean_err += err
    total += 1

print(f"mean reprojection error: {mean_err / total:.3f} px")
# good calibrations are typically < 0.5 px per corner
19

深度与立体视觉

立体视差图

StereoBM 为已校正的立体对计算视差图(左右图间的像素偏移)——越近的物体视差越大。numDisparities 必须是 16 的倍数;blockSize 必须为奇数。输出为按 16 缩放的 int16(除以 16.0 得像素视差)。StereoSGBM(半全局块匹配)更慢但准确得多。

opencv
import cv2

# stereo pair (rectified: corresponding points on the same row)
imgL = cv2.imread("left.png", cv2.IMREAD_GRAYSCALE)
imgR = cv2.imread("right.png", cv2.IMREAD_GRAYSCALE)

# StereoBM (block matching, fast)
stereo = cv2.StereoBM_create(numDisparities=16, blockSize=15)
disparity = stereo.compute(imgL, imgR)

# normalize for display
disp_vis = cv2.normalize(disparity, None, 0, 255,
                         cv2.NORM_MINMAX, cv2.CV_8U)

StereoSGBM

StereoSGBM 在扫描线上强制平滑(半全局匹配)——比 StereoBM 准确得多,代价是约 5 倍运行时间。P1/P2 分别惩罚 1 和 >1 的视差变化;经验法则是 P1=8*C*blockSize²、P2=32*C*blockSize²,其中 C 为通道数。speckle* 参数过滤小的噪声视差区域。

opencv
import cv2

imgL = cv2.imread("left.png", cv2.IMREAD_GRAYSCALE)
imgR = cv2.imread("right.png", cv2.IMREAD_GRAYSCALE)

sgbm = cv2.StereoSGBM_create(
    minDisparity=0,
    numDisparities=128,
    blockSize=5,
    P1=8 * 3 * 5 * 5,
    P2=32 * 3 * 5 * 5,
    disp12MaxDiff=1,
    uniquenessRatio=10,
    speckleWindowSize=100,
    speckleRange=32,
)

disparity = sgbm.compute(imgL, imgR)
disp_vis = cv2.normalize(disparity, None, 0, 255,
                         cv2.NORM_MINMAX, cv2.CV_8U)

立体标定与校正

立体匹配要求图像已校正——极线水平对齐使对应点共享同一行。流水线为:标定每个相机、立体标定求二者间的 R 和 T、stereoRectify 计算校正变换、再对每帧 remap 一次。校正后,视差计算变为沿行的一维搜索。

opencv
import cv2
import numpy as np

# calibrate each camera separately, then stereo-calibrate
ret, mtxL, distL, rvecsL, tvecsL = cv2.calibrateCamera(
    objpoints, imgpointsL, grayL.shape[::-1], None, None)
ret, mtxR, distR, rvecsR, tvecsR = cv2.calibrateCamera(
    objpoints, imgpointsR, grayR.shape[::-1], None, None)

# stereo calibration: finds the rigid transform between cameras
flags = cv2.CALIB_FIX_INTRINSIC
ret, mtxL, distL, mtxR, distR, R, T, E, F = cv2.stereoCalibrate(
    objpoints, imgpointsL, imgpointsR, mtxL, distL, mtxR, distR,
    grayL.shape[::-1], flags=flags)

# stereo rectification: aligns epipolar lines to be horizontal
R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
    mtxL, distL, mtxR, distR, grayL.shape[::-1], R, T)

# pre-compute the rectification maps
mapL1, mapL2 = cv2.initUndistortRectifyMap(
    mtxL, distL, R1, P1, grayL.shape[::-1], cv2.CV_16SC2)
mapR1, mapR2 = cv2.initUndistortRectifyMap(
    mtxR, distR, R2, P2, grayR.shape[::-1], cv2.CV_16SC2)

rectL = cv2.remap(imgL, mapL1, mapL2, cv2.INTER_LINEAR)
rectR = cv2.remap(imgR, mapR1, mapR2, cv2.INTER_LINEAR)

由视差求深度

深度与视差成反比:深度 = 焦距 × 基线 / 视差。基线(相机间距)和焦距单位须一致(通常焦距用像素、基线用米则深度为米)。远处物体视差小(深度误差大)——这就是立体相机远距离需宽基线的原因。

opencv
import cv2
import numpy as np

# given a disparity map and the stereo baseline + focal length:
# depth = (focal_length * baseline) / disparity

focal = 700.0       # pixels (from camera matrix)
baseline = 0.1      # meters (distance between cameras)

disparity = sgbm.compute(rectL, rectR).astype(np.float32) / 16.0

# avoid division by zero
disparity[disparity <= 0] = 0.1
depth = (focal * baseline) / disparity   # in meters

# cap to a sensible range
depth = np.clip(depth, 0.1, 100.0)
depth_vis = cv2.normalize(depth, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)

光流(Lucas-Kanade)

Lucas-Kanade 计算稀疏光流——假设局部小运动,跨帧跟踪一组关键点。对中等运动快且准;对大或快速运动失效(用金字塔,calcOpticalFlowPyrLK 已做)。status==1 表示该点成功跟踪。密集逐像素光流用 calcOpticalFlowFarneback——慢得多但产生完整运动场。

opencv
import cv2
import numpy as np

cap = cv2.VideoCapture("traffic.mp4")
ret, prev = cap.read()
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)

# detect good features to track
prev_pts = cv2.goodFeaturesToTrack(prev_gray, 100, 0.3, 7)

while True:
    ret, frame = cap.read()
    if not ret:
        break
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # sparse optical flow (Lucas-Kanade)
    next_pts, status, err = cv2.calcOpticalFlowPyrLK(
        prev_gray, gray, prev_pts, None)

    good_new = next_pts[status == 1]
    good_old = prev_pts[status == 1]

    for new, old in zip(good_new, good_old):
        a, b = new.ravel()
        c, d = old.ravel()
        cv2.line(frame, (int(a), int(b)), (int(c), int(d)), (0, 255, 0), 2)
        cv2.circle(frame, (int(a), int(b)), 3, (0, 0, 255), -1)

    cv2.imshow("flow", frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
    prev_gray = gray.copy()
    prev_pts = good_new.reshape(-1, 1, 2)

cap.release()
20

工具与技巧

性能测量

TickMeter 是基准测试 OpenCV 调用最干净的方式——getTimeMilli 返回挂钟毫秒。getTickCount/getTickFrequency 给周期级精确计时。务必多次迭代取平均以平摊抖动,并在基准测试时关闭 cv2.imshow(GUI 渲染占主导)。setNumThreads 控制底层并行后端(TBB/OpenMP)。

opencv
import cv2
import numpy as np

img = cv2.imread("photo.jpg")

# TickMeter: high-precision timer
tm = cv2.TickMeter()
tm.start()
for _ in range(100):
    blurred = cv2.GaussianBlur(img, (5, 5), 0)
tm.stop()
print(f"avg: {tm.getTimeMilli() / 100:.3f} ms")

# getTickCount / getTickFrequency
t0 = cv2.getTickCount()
edges = cv2.Canny(img, 50, 150)
t1 = cv2.getTickCount()
print(f"Canny: {(t1 - t0) / cv2.getTickFrequency() * 1000:.2f} ms")

# set USE_AVX / thread count
cv2.setNumThreads(4)
print(cv2.getNumThreads())

鼠标事件

setMouseCallback 为命名窗口注册鼠标事件处理函数——适用于交互式 ROI 选择、标注工具和简单绘图程序。常见事件:LBUTTONDOWN、LBUTTONUP、MOUSEMOVE、RBUTTONDOWN。处理函数必须是顶层(或全局)函数;因回调签名固定,用全局变量是共享状态的标准方式。

opencv
import cv2
import numpy as np

drawing = False
ix, iy = -1, -1

def on_mouse(event, x, y, flags, param):
    global drawing, ix, iy
    if event == cv2.EVENT_LBUTTONDOWN:
        drawing = True
        ix, iy = x, y
    elif event == cv2.EVENT_MOUSEMOVE and drawing:
        cv2.rectangle(img, (ix, iy), (x, y), (0, 255, 0), -1)
    elif event == cv2.EVENT_LBUTTONUP:
        drawing = False
        cv2.rectangle(img, (ix, iy), (x, y), (0, 255, 0), -1)

img = np.zeros((512, 512, 3), np.uint8)
cv2.namedWindow("canvas")
cv2.setMouseCallback("canvas", on_mouse)

while True:
    cv2.imshow("canvas", img)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
cv2.destroyAllWindows()

轨迹条

轨迹条让你无需重启脚本即可交互调参——对寻找好的阈值、核大小和 HSV 范围极有价值。回调在每次变化时调用(若在主循环中轮询 getTrackbarPos,空操作即可)。轨迹条必须在命名窗口上创建,用 getTrackbarPos(window, name) 读取。

opencv
import cv2
import numpy as np

def nothing(x):
    pass

img = np.zeros((300, 512, 3), np.uint8)
cv2.namedWindow("image")

# create trackbars
cv2.createTrackbar("R", "image", 0, 255, nothing)
cv2.createTrackbar("G", "image", 0, 255, nothing)
cv2.createTrackbar("B", "image", 0, 255, nothing)
cv2.createTrackbar("switch", "image", 0, 1, nothing)

while True:
    cv2.imshow("image", img)
    if cv2.waitKey(1) & 0xFF == 27:    # Esc to quit
        break

    r = cv2.getTrackbarPos("R", "image")
    g = cv2.getTrackbarPos("G", "image")
    b = cv2.getTrackbarPos("B", "image")
    s = cv2.getTrackbarPos("switch", "image")

    if s == 0:
        img[:] = 0
    else:
        img[:] = [b, g, r]

cv2.destroyAllWindows()

ROI 选择器

selectROI 打开交互窗口,用户拖拽矩形并按 Enter/Space——比自写鼠标回调容易得多。按 'c' 取消(返回全零)。selectROIs 可连续选取多个矩形。返回元组为 (x, y, w, h);用 img[y:y+h, x:x+w] 裁剪。

opencv
import cv2

img = cv2.imread("photo.jpg")

# built-in ROI selector (drag a rectangle)
roi = cv2.selectROI("select", img, showCrosshair=True, fromCenter=False)
cv2.destroyAllWindows()

x, y, w, h = roi
cropped = img[y:y+h, x:x+w]

# multiple ROIs
rois = cv2.selectROIs("select", img, showCrosshair=True, fromCenter=False)
for r in rois:
    x, y, w, h = r
    cv2.imshow("roi", img[y:y+h, x:x+w])
    cv2.waitKey(0)
cv2.destroyAllWindows()

常见陷阱

这五个陷阱造成了大多数 OpenCV bug:BGR/RGB 混淆(红蓝互换)、uint8 溢出(暗/亮伪影)、坐标顺序(ROI 差一)、文件缺失返回 None(后续操作以晦涩错误崩溃)、以及忘记 waitKey(窗口不更新)。尽早内化它们——每个 OpenCV 开发者至少各踩一次。

opencv
import cv2
import numpy as np

# 1. BGR vs RGB — imshow expects BGR; matplotlib expects RGB
img = cv2.imread("p.jpg")
# plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))   # correct
# plt.imshow(img)                                     # colors look wrong

# 2. uint8 overflow — use cv2 ops or upcast
a = np.array([200], np.uint8)
print(a + 100)        # [44]   (wraps, WRONG)
print(cv2.add(a, 100))  # [[255]] (saturates, RIGHT)

# 3. (x, y) vs (row, col) — drawing uses (x,y), NumPy uses [y,x]
cv2.rectangle(img, (10, 20), (110, 120), (0, 255, 0), 2)  # x=10..110
roi = img[20:120, 10:110]   # rows 20..120, cols 10..110 (same region)

# 4. None returned by imread when the file is missing
img = cv2.imread("missing.jpg")
if img is None:
    raise FileNotFoundError("could not read image")

# 5. waitKey is mandatory for imshow to actually render
cv2.imshow("img", img)
cv2.waitKey(1)   # without this, the window may stay blank

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。