强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OpenCV 计算机视觉完全教程 / 第 03 章 — 图像基础

第 03 章 — 图像基础

3.1 图像的本质

在 OpenCV 中,图像就是一个多维数组:

类型 通道数 dtype 示例
灰度图 1 uint8 shape=(H, W)
彩色图(BGR) 3 uint8 shape=(H, W, 3)
带透明度(BGRA) 4 uint8 shape=(H, W, 4)
浮点灰度图 1 float32 shape=(H, W)
深度图 1 uint16/int16 shape=(H, W)

注意: OpenCV 默认色彩顺序是 BGR,而非 RGB。这是与 PIL、Matplotlib 的主要区别。

像素坐标系:                内存布局 (H=3, W=4, C=3):
(0,0) → (0,W)              [B G R] [B G R] [B G R] [B G R]
 ↓        ↓                 [B G R] [B G R] [B G R] [B G R]
(H,0) → (H,W)              [B G R] [B G R] [B G R] [B G R]

3.2 图像读取:imread

import cv2

# 读取彩色图像(默认)
img = cv2.imread("photo.jpg")
print(f"默认读取: shape={img.shape}, dtype={img.dtype}")

# 读取灰度图
gray = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
print(f"灰度读取: shape={gray.shape}, dtype={gray.dtype}")

# 读取原始数据(含 alpha 通道,不裁剪位深)
raw = cv2.imread("photo.png", cv2.IMREAD_UNCHANGED)
print(f"原始读取: shape={raw.shape}, dtype={raw.dtype}")

# 读取为三通道(忽略 alpha)
color = cv2.imread("photo.png", cv2.IMREAD_COLOR)

# 读取标志一览
# cv2.IMREAD_UNCHANGED  = -1  # 原始数据
# cv2.IMREAD_GRAYSCALE  = 0   # 灰度
# cv2.IMREAD_COLOR      = 1   # BGR 彩色(默认)
# cv2.IMREAD_ANYDEPTH   = 2   # 保持原始位深
# cv2.IMREAD_ANYCOLOR   = 4   # 按文件格式读取
# cv2.IMREAD_REDUCED_*  = 多种  # 缩小读取
// C++ 读取
cv::Mat img = cv::imread("photo.jpg", cv::IMREAD_COLOR);
cv::Mat gray = cv::imread("photo.jpg", cv::IMREAD_GRAYSCALE);

if (img.empty()) {
    std::cerr << "无法读取图像!" << std::endl;
    return -1;
}

安全读取模式

def safe_imread(path, flags=cv2.IMREAD_COLOR):
    """安全读取,失败时返回 None 而非静默返回空矩阵"""
    img = cv2.imread(path, flags)
    if img is None:
        raise FileNotFoundError(f"无法读取图像: {path}")
    return img

注意: imread 在文件不存在时不会抛出异常,而是返回 None。务必检查返回值。


3.3 图像显示:imshow + waitKey

import cv2

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

# 基本显示
cv2.imshow("窗口名称", img)
cv2.waitKey(0)           # 等待按键,0 = 无限等待
cv2.destroyAllWindows()  # 关闭所有窗口

# 带延迟的显示(视频场景)
cv2.imshow("预览", img)
key = cv2.waitKey(30)    # 等待 30ms
if key == ord('q'):      # 按 'q' 退出
    cv2.destroyAllWindows()

# 可调整大小的窗口
cv2.namedWindow("自适应", cv2.WINDOW_NORMAL)
cv2.imshow("自适应", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

窗口标志

标志 说明
WINDOW_NORMAL 可调整大小
WINDOW_AUTOSIZE 自动匹配图像大小(默认)
WINDOW_FULLSCREEN 全屏模式
WINDOW_FREERATIO 自由比例
WINDOW_KEEPRATIO 保持比例

服务器环境替代方案

# 无法使用 imshow 时,用 Matplotlib 显示
import matplotlib.pyplot as plt

img_bgr = cv2.imread("photo.jpg")
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(10, 6))
plt.imshow(img_rgb)
plt.title("OpenCV 图像")
plt.axis("off")
plt.tight_layout()
plt.savefig("preview.png", dpi=150)
plt.show()

3.4 图像保存:imwrite

import cv2
import numpy as np

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

# 保存为不同格式
cv2.imwrite("output.png", img)         # PNG(无损)
cv2.imwrite("output.jpg", img)         # JPEG(有损)
cv2.imwrite("output.bmp", img)         # BMP
cv2.imwrite("output.tiff", img)        # TIFF

# JPEG 质量控制
cv2.imwrite("high_quality.jpg", img,
            [cv2.IMWRITE_JPEG_QUALITY, 95])
cv2.imwrite("low_quality.jpg", img,
            [cv2.IMWRITE_JPEG_QUALITY, 30])

# PNG 压缩级别(0-9,越大越慢但越小)
cv2.imwrite("compressed.png", img,
            [cv2.IMWRITE_PNG_COMPRESSION, 9])

# 保存到内存(bytes)
success, buffer = cv2.imencode(".jpg", img)
jpg_bytes = buffer.tobytes()

# 从内存读取
img_array = np.frombuffer(jpg_bytes, dtype=np.uint8)
img_decoded = cv2.imdecode(img_array, cv2.IMREAD_COLOR)

# 保存 ROI 裁剪区域
roi = img[100:300, 200:500]
cv2.imwrite("roi.jpg", roi)

3.5 图像属性

import cv2

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

# 基本属性
print(f"高度 (行数):  {img.shape[0]}")      # H
print(f"宽度 (列数):  {img.shape[1]}")      # W
print(f"通道数:       {img.shape[2] if len(img.shape) == 3 else 1}")
print(f"总像素数:     {img.shape[0] * img.shape[1]}")
print(f"总元素数:     {img.size}")          # H * W * C
print(f"数据类型:     {img.dtype}")         # uint8
print(f"步长 (bytes): {img.strides}")       # (W*C, C, 1)
print(f"是否连续:     {img.flags['C_CONTIGUOUS']}")

# 像素值范围
print(f"最小值: {img.min()}, 最大值: {img.max()}, 均值: {img.mean():.1f}")

# 单像素访问(效率低,仅用于调试)
b, g, r = img[100, 200]        # y=100, x=200
print(f"像素 (200,100): R={r} G={g} B={b}")

# 批量像素访问(NumPy 方式,高效)
patch = img[100:200, 300:400]  # 裁剪区域
print(f"区域均值: B={patch[:,:,0].mean():.0f} "
      f"G={patch[:,:,1].mean():.0f} R={patch[:,:,2].mean():.0f}")

图像属性速查表

属性 Python C++
尺寸 img.shape → (H, W, C) img.rows, img.cols, img.channels()
类型 img.dtype img.type(), img.depth()
总元素 img.size img.total()
步长 img.strides img.step
空检查 img is None img.empty()
连续性 img.flags['C_CONTIGUOUS'] img.isContinuous()

3.6 ROI(Region of Interest)裁剪

ROI 是图像处理的核心操作,用于聚焦感兴趣区域:

import cv2

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

# NumPy 切片裁剪(最常用)
# img[y1:y2, x1:x2]
roi = img[100:400, 200:500]     # 裁剪矩形区域

# 带步长的裁剪
sub = img[::2, ::2]             # 每隔一行/列采样(降采样)
print(f"降采样: {img.shape}{sub.shape}")

# 使用坐标裁剪
x, y, w, h = 200, 100, 300, 300
roi = img[y:y+h, x:x+w]

# 修改 ROI(直接修改原图)
img[y:y+h, x:x+w] = (0, 255, 0)  # 填充绿色

# 复制 ROI(避免修改原图)
roi_copy = img[y:y+h, x:x+w].copy()

# 将一个 ROI 复制到另一位置
src_roi = img[0:100, 0:100]
img[200:300, 200:300] = src_roi
// C++ ROI
cv::Mat img = cv::imread("photo.jpg");

// 矩形 ROI
cv::Rect roi_rect(200, 100, 300, 300);  // x, y, width, height
cv::Mat roi = img(roi_rect);

// 修改 ROI
roi.setTo(cv::Scalar(0, 255, 0));  // 填充绿色

// 复制 ROI
cv::Mat roi_copy = img(roi_rect).clone();

3.7 通道操作

3.7.1 分离与合并

import cv2

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

# 分离通道
b, g, r = cv2.split(img)
print(f"单通道形状: {b.shape}")       # (H, W)

# 合并通道
merged = cv2.merge([b, g, r])

# 交换红蓝通道(BGR → RGB)
rgb = cv2.merge([r, g, b])
# 或更简单的方式:
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 只保留红色通道
red_only = img.copy()
red_only[:, :, 0] = 0   # 清除蓝色
red_only[:, :, 1] = 0   # 清除绿色

# 用 NumPy 索引方式(更高效)
blue_channel  = img[:, :, 0]
green_channel = img[:, :, 1]
red_channel   = img[:, :, 2]
// C++ 通道操作
std::vector<cv::Mat> channels;
cv::split(img, channels);           // 分离
cv::merge(channels, img);           // 合并

3.7.2 添加/删除 Alpha 通道

import numpy as np

# 添加 Alpha 通道(BGRA)
alpha = np.full(img.shape[:2], 255, dtype=np.uint8)  # 全不透明
bgra = cv2.merge([img[:, :, 0], img[:, :, 1], img[:, :, 2], alpha])

# 移除 Alpha 通道
bgr = cv2.cvtColor(bgra, cv2.COLOR_BGRA2BGR)

3.8 色彩空间转换

3.8.1 常用色彩空间

色彩空间 通道 用途
BGR 蓝、绿、红 OpenCV 默认
RGB 红、绿、蓝 显示、Matplotlib
GRAY 灰度 边缘检测、特征提取
HSV 色相、饱和度、明度 颜色过滤、物体追踪
HLS 色相、亮度、饱和度 光照不变分析
LAB 亮度、a、b 颜色迁移、肤色检测
YCrCb 亮度、Cr、Cb 肤色检测、压缩
LUV 亮度、U、V 均匀色彩空间

3.8.2 色彩空间转换代码

import cv2
import numpy as np

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

# BGR → 各种色彩空间
gray  = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hsv   = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lab   = cv2.cvtColor(img, cv2.COLOR_BGR2Lab)
ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
rgb   = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
hls   = cv2.cvtColor(img, cv2.COLOR_BGR2HLS)

# 反向转换
bgr_back = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
bgr_back2 = cv2.cvtColor(lab, cv2.COLOR_Lab2BGR)

3.8.3 HSV 颜色过滤实战

"""
HSV 颜色过滤:检测蓝色物体
"""
import cv2
import numpy as np

img = cv2.imread("objects.jpg")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 定义蓝色的 HSV 范围
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([130, 255, 255])

# 创建掩码
mask = cv2.inRange(hsv, lower_blue, upper_blue)

# 应用掩码
result = cv2.bitwise_and(img, img, mask=mask)

print(f"蓝色像素占比: {mask.sum() / mask.size * 100 / 255:.1f}%")

3.8.4 常见颜色 HSV 范围参考

颜色 H 范围 S 范围 V 范围
红色 0-10, 170-180 50-255 50-255
橙色 10-25 50-255 50-255
黄色 25-35 50-255 50-255
绿色 35-85 50-255 50-255
青色 85-100 50-255 50-255
蓝色 100-130 50-255 50-255
紫色 130-170 50-255 50-255

注意: OpenCV 中 H 通道范围是 0-179(不是 0-360),S 和 V 是 0-255。


3.9 图像基本变换

import cv2

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

# 调整大小
resized = cv2.resize(img, (640, 480))                     # 指定尺寸
resized2 = cv2.resize(img, (w//2, h//2))                  # 缩小一半
resized3 = cv2.resize(img, None, fx=0.5, fy=0.5)          # 按比例缩放

# 翻转
flip_h = cv2.flip(img, 1)    # 水平翻转
flip_v = cv2.flip(img, 0)    # 垂直翻转
flip_hv = cv2.flip(img, -1)  # 水平+垂直翻转

# 旋转(简单 90 度)
rot90  = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
rot180 = cv2.rotate(img, cv2.ROTATE_180)
rot270 = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)

# 填充边框
padded = cv2.copyMakeBorder(img, 50, 50, 50, 50,
                            cv2.BORDER_CONSTANT, value=(0, 0, 255))
reflect = cv2.copyMakeBorder(img, 50, 50, 50, 50,
                             cv2.BORDER_REFLECT)

3.10 实战:图像信息查看器

"""
image_info.py — 图像信息全面查看工具
"""
import cv2
import numpy as np
import os

def analyze_image(path):
    img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
    if img is None:
        print(f"错误: 无法读取 {path}")
        return

    h, w = img.shape[:2]
    c = img.shape[2] if len(img.shape) == 3 else 1
    file_size = os.path.getsize(path)

    print(f"=== 图像分析: {os.path.basename(path)} ===")
    print(f"  尺寸:        {w} × {h}")
    print(f"  通道数:      {c}")
    print(f"  数据类型:    {img.dtype}")
    print(f"  像素范围:    [{img.min()}, {img.max()}]")
    print(f"  总元素数:    {img.size:,}")
    print(f"  文件大小:    {file_size / 1024:.1f} KB")
    print(f"  宽高比:      {w/h:.2f}")
    print(f"  百万像素:    {w * h / 1e6:.2f} MP")

    if c == 3:
        means = img.mean(axis=(0, 1))
        print(f"  通道均值:    B={means[0]:.0f} G={means[1]:.0f} R={means[2]:.0f}")

    # 色彩空间分布
    if c == 3:
        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
        print(f"  平均亮度:    {hsv[:,:,2].mean():.0f}/255")
        print(f"  平均饱和度:  {hsv[:,:,1].mean():.0f}/255")

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1:
        analyze_image(sys.argv[1])
    else:
        print("用法: python image_info.py <图片路径>")

3.11 扩展阅读

资源 链接 说明
OpenCV imread 文档 docs.opencv.org/4.x/d4/da8/group__imgcodecs 编解码完整文档
色彩空间转换 docs.opencv.org/4.x/d8/d01/group__imgproc__color 所有颜色转换代码
下一章 第 04 章 — 绘图与交互 线条/矩形/鼠标事件

本章小结: 掌握了图像的读取、显示、保存全流程,理解了图像作为 NumPy 数组的本质,以及 ROI 裁剪、通道操作和色彩空间转换等核心概念。