图像基础与 OpenCV 起步

数字图像的本质

计算机视觉处理的对象是数字图像。数字图像本质上就是一个二维像素数组。

像素与灰度图

灰度图是最简单的数字图像——每个像素存储一个亮度值,范围 0(纯黑)到 255(纯白)。在 Python 中,一张宽 W、高 H 的灰度图就是一个形状为 (H, W) 的二维数组。

python
1
2
3
4
import numpy as np

# 创建一个 100x100 的灰色方块(亮度值 128)
gray_img = np.full((100, 100), 128, dtype=np.uint8)

uint8 的取值范围是 0-255——8 位(bit)恰好表示 2^8 = 256 个值。如果运算超出范围,会发生回绕(wrap around):255 + 1 不会变成 256,而是变成 0(像汽车里程表归零)。处理像素运算时推荐用 cv2.add()(自动饱和截断)或 np.clip()(手动限幅)来避免回绕。

彩色图像与 RGB

彩色图像的每个像素包含三个通道——R(红)、G(绿)、B(蓝),每个通道同样是 0-255。但 OpenCV 默认使用 BGR 排列,不是常见的 RGB。形状为 (H, W, 3)

python
1
2
3
# 创建一个红色方块(注意 OpenCV 是 BGR)
red_patch = np.zeros((100, 100, 3), dtype=np.uint8)
red_patch[:, :] = [0, 0, 255]  # B=0, G=0, R=255

BGR 的由来:OpenCV 早期开发时,相机厂商的 byte 序约定普遍是 BGR,OpenCV 沿用了这一约定。如果直接用 Matplotlib 的 imshow 显示 OpenCV 图像,颜色会反掉(R↔B),因为 Matplotlib 按 RGB 解释通道。修正方法:先转换颜色空间 img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

图1 - 数字图像(以彩色图为例)的数据结构:

mermaid
flowchart TD
    IMG["彩色图像<br/>shape: (H, W, 3)"] --> R["R 通道<br/>0-255 红色分量"]
    IMG --> G["G 通道<br/>0-255 绿色分量"]
    IMG --> B["B 通道<br/>0-255 蓝色分量"]
    R --> PIXEL["单个像素<br/>[B, G, R]<br/>如 [0, 0, 255] = 纯红"]
    G --> PIXEL
    B --> PIXEL

    classDef img fill:#9C27B0,color:#fff
    classDef ch fill:#2196F3,color:#fff
    classDef px fill:#f44336,color:#fff
    class IMG img
    class R,G,B ch
    class PIXEL px

动手试试 RGB/BGR 的区别——拖动滑块看看 [B, G, R] 数组如何对应颜色:

R 128
G 128
B 128
[128, 128, 128] BGR

分辨率与位深度

  • 分辨率:图像的宽×高像素数,决定了图像的细节密度
  • 位深度:每个通道用多少比特表示。uint8 是 8 位(0-255),也有 16 位或 32 位浮点图

OpenCV 简介

OpenCV(Open Source Computer Vision Library)是目前最广泛使用的计算机视觉库,支持 C++、Python、Java 等语言。核心模块包括:

  • core:基础数据结构(Mat、Point、Rect)
  • imgproc:图像处理(滤波、几何变换、颜色空间)
  • highgui:GUI 交互(显示窗口、滑动条)
  • videoio:视频读写

Python 安装后直接 import cv2 即可使用。OpenCV 的 Python 接口是对 C++ 的封装,函数名和参数基本一致。

基本操作

读写与显示图像

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import cv2

# 读取图像(默认彩色 BGR)
img = cv2.imread('photo.jpg')

# 读取为灰度图
gray = cv2.imread('photo.jpg', cv2.IMREAD_GRAYSCALE)

# 保存图像
cv2.imwrite('output.jpg', img)

# 显示图像(按任意键关闭窗口)
cv2.imshow('窗口标题', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

imread 返回 None 时说明文件路径不对或格式不支持——务必检查返回值。

图像属性

python
1
2
3
4
print(img.shape)    # (H, W, 3)
print(img.dtype)    # uint8
print(img.size)     # 总像素数 = H * W * C
print(len(img))     # 行数(高度)

ROI 裁剪

NumPy 切片直接取图像子区域——这是最常用的操作:

python
1
2
3
4
5
# 取 (y1:y2, x1:x2) 区域
roi = img[50:200, 100:300]

# 复制区域到另一个位置
img[50:200, 100:300] = img[0:150, 400:600]

注意切片的顺序:[行范围, 列范围],对应 [y1:y2, x1:x2]

新手最容易犯的错误img[y1:y2, x1:x2] 第一个维度是行范围(Y 方向),第二个维度是列范围(X 方向)——千万别写反成 img[x1:x2, y1:y2]

图3 - OpenCV 图像坐标系约定:

mermaid
flowchart TD
    NOTE["OpenCV 图像坐标系:<br/>原点在左上角<br/>rows = Y (向下增长)<br/>cols = X (向右增长)<br/>img[y, x] 不是 img[x, y]"]

    classDef note fill:#f44336,color:#fff
    class NOTE note

通道分离与合并

python
1
2
3
4
5
6
# 分离 BGR 三个通道
b, g, r = cv2.split(img)

# 把红色通道置零(移除红色)
img_copy = img.copy()
img_copy[:, :, 2] = 0  # 第 3 个通道是 R

split 会创建新数组,频繁调用有额外开销。可以用 NumPy 切片直接操作通道:

python
1
2
# 直接取 red 通道(不复制数据)
r = img[:, :, 2]

基础图像处理

颜色空间转换

python
1
2
3
4
5
6
7
8
# BGR → 灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# BGR → HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# HSV → BGR
back = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

HSV 颜色空间在做颜色范围筛选时很常用——H 通道表示色相,S 表示饱和度,V 表示明度。

几何变换

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 缩小到一半
small = cv2.resize(img, None, fx=0.5, fy=0.5)

# 缩放到指定尺寸
resized = cv2.resize(img, (640, 480))

# 旋转 45 度(需要先计算旋转矩阵)
h, w = img.shape[:2]
M = cv2.getRotationMatrix2D((w//2, h//2), 45, 1.0)
rotated = cv2.warpAffine(img, M, (w, h))

图像滤波

滤波用于降噪或平滑图像。核心参数是核大小(kernel size)——核越大,平滑效果越强。

直觉上,滤波相当于用邻域像素重新计算每个像素的值:

  • 均值滤波blur):对核内所有像素做算术平均——速度最快,但会让边缘模糊
  • 高斯滤波GaussianBlur):加权平均,中心像素权重最大,越靠近中心权重越大,越远越小,权重分布服从高斯分布(钟形曲线)——比均值滤波更能保留边缘
  • 中值滤波medianBlur):取核内像素的中位数,对椒盐噪声(随机黑白点)特别有效——因为中位数能忽略极端值
python
1
2
3
4
5
6
7
8
# 均值滤波
blur = cv2.blur(img, (5, 5))

# 高斯滤波(保留更多边缘)
gauss = cv2.GaussianBlur(img, (5, 5), 0)

# 中值滤波(对椒盐噪声效果最好)
median = cv2.medianBlur(img, 5)

卷积核滑过图像的过程用动画看最清楚——每个位置取 3×3 邻域做加权平均:

Input (5×5)
1/91/91/9 1/91/91/9 1/91/91/9
Kernel (3×3 mean blur)
Output

高斯滤波的第三个参数是标准差——传 0 让函数自动计算。

阈值处理

阈值处理把灰度图转成二值图(只有黑和白):

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 固定阈值
_, binary = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)

# 大津法(自动计算最优阈值)
_, otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 自适应阈值(处理光照不均的情况)
adaptive = cv2.adaptiveThreshold(
    gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY, 11, 2
)

光照不均的图像适合用自适应阈值,它根据每个像素邻域计算局部阈值。

图2 - OpenCV 常见图像处理流水线(从读取到轮廓提取):

mermaid
flowchart TD
    A["imread<br/>读取图像"] --> B["cvtColor<br/>BGR→GRAY"]
    B --> C["GaussianBlur<br/>降噪"]
    C --> D["threshold<br/>二值化"]
    D --> E["Canny<br/>边缘检测"]
    E --> F["findContours<br/>轮廓提取"]

    classDef io fill:#2196F3,color:#fff
    classDef proc fill:#9C27B0,color:#fff
    class A,F io
    class B,C,D,E proc

边缘检测

Canny 边缘检测是目前最常用的边缘检测算法:

python
1
2
3
4
5
# 先用高斯滤波降噪
blurred = cv2.GaussianBlur(gray, (5, 5), 0)

# Canny 边缘检测(双阈值)
edges = cv2.Canny(blurred, 50, 150)

Canny 边缘检测分为两步:先用 Sobel 算子计算水平和垂直方向的梯度,得到每个像素的梯度幅值和方向;然后通过**滞后阈值(hysteresis thresholding)**来判断—— 两个阈值参数:低于 threshold1 的像素被丢弃(非边缘),高于 threshold2 的确定为边缘,介于两者之间的只在与强边缘相连时保留。一般 threshold2threshold1 的 2-3 倍。