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 裁剪、通道操作和色彩空间转换等核心概念。