2. 图像基础操作:读取、显示、保存与属性

各位同学,欢迎来到第二章。

上一章我们搭好了OpenCV的环境,今天咱们来点实在的——图像的基础操作。说白了,就是怎么把一张图片“请”进程序里,怎么看看它长什么样,最后怎么把它存下来。

我个人习惯把这一章叫做“图像的Hello World”。你想想看,不管以后做多复杂的手势识别,第一步永远是:把摄像头拍到的画面读进来。所以,这一章打下的基础,会贯穿整个课程。

2.1 读取图像:imread() 函数

读取图像,用的是 cv2.imread()。这个函数很简单,给它一个文件路径,它就给你返回一个数组——没错,在OpenCV里,图像本质上就是一个多维数组(NumPy数组)。

import cv2

# 读取图像
img = cv2.imread('hand.jpg')
print(type(img))  # 输出:<class 'numpy.ndarray'>

这里有个坑,我一开始就踩过。如果路径不对,或者文件不存在,imread() 不会报错,而是返回 None。嗯,这很坑。所以,我建议你每次读取后,都检查一下:

if img is None:
    print("图像读取失败,请检查路径")
    exit()

imread() 还有第二个参数,用来控制读取方式。常用的有这几个:

参数 含义
cv2.IMREAD_COLOR 1 默认值,加载彩色图像。忽略透明度。
cv2.IMREAD_GRAYSCALE 0 加载为灰度图。
cv2.IMREAD_UNCHANGED -1 加载原图,包括Alpha通道(透明度)。
我的经验: 做手势识别时,我通常先用 IMREAD_COLOR 读取,因为后续需要颜色信息来分割皮肤。但如果你只是做边缘检测之类的,直接读成灰度图,能省不少内存。

2.2 显示图像:imshow() 与 waitKey()

读进来了,总得看一眼吧?用 cv2.imshow()

cv2.imshow('Hand Image', img)
cv2.waitKey(0)  # 等待按键
cv2.destroyAllWindows()

这里有个关键点,waitKey() 是必须的。没有它,窗口会一闪而过。为什么?因为OpenCV的显示机制是事件驱动的,waitKey() 负责刷新窗口并等待用户输入。

waitKey(0) 表示无限等待,直到你按下任意键。waitKey(30) 表示等待30毫秒——这在处理视频流时非常有用,相当于设置了帧率。

注意: 我曾经在循环里忘记加 waitKey(),结果程序直接卡死,窗口也关不掉。嗯,血的教训。另外,destroyAllWindows() 最好也加上,养成好习惯。

2.3 保存图像:imwrite()

处理完图像,想存下来?用 cv2.imwrite()

cv2.imwrite('hand_processed.jpg', img)

第一个参数是保存路径,第二个参数是图像数据。它会根据文件扩展名自动选择编码格式。比如 .jpg 就用JPEG压缩,.png 就用PNG压缩。

这里有个小技巧。保存JPEG时,你可以控制质量:

cv2.imwrite('hand_high_quality.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 95])

质量范围是0-100,默认95。数值越大,文件越大,画质越好。保存PNG时,可以控制压缩级别:

cv2.imwrite('hand_compressed.png', img, [cv2.IMWRITE_PNG_COMPRESSION, 9])

压缩级别0-9,9是最高压缩(文件最小,但保存最慢)。

避坑指南: 我曾经在保存图像时,忘了先创建目录。如果 output/ 文件夹不存在,imwrite() 会静默失败,返回 False。所以,记得先 os.makedirs()

2.4 图像属性:尺寸、通道、像素值

图像读进来了,我们得知道它的一些基本信息。说白了,就是三个属性:

  • 尺寸(shape): 高度、宽度、通道数
  • 通道(channels): 是灰度图还是彩色图?
  • 像素值(dtype): 每个像素点的数值范围

来看代码:

import cv2

img = cv2.imread('hand.jpg')

# 获取图像属性
print("图像形状:", img.shape)   # (高度, 宽度, 通道数)
print("图像大小:", img.size)    # 总像素数 = 高 * 宽 * 通道
print("数据类型:", img.dtype)   # 通常是 uint8

# 单独获取
height, width, channels = img.shape
print(f"高度: {height}, 宽度: {width}, 通道数: {channels}")

输出示例:

图像形状: (480, 640, 3)
图像大小: 921600
数据类型: uint8
高度: 480, 宽度: 640, 通道数: 3

解释一下:

  • shape: 返回一个元组。对于彩色图,是 (高, 宽, 3)。3代表BGR三个通道。对于灰度图,是 (高, 宽),没有第三个维度。
  • size: 所有像素的总数。480*640*3 = 921600。
  • dtype: 通常是 uint8,即0-255的整数。这也是为什么像素值范围是0-255。

那么,如何访问具体的像素值呢?

# 访问 (100, 200) 位置的像素
pixel = img[100, 200]
print("BGR值:", pixel)  # [B, G, R]

# 只访问蓝色通道
blue = img[100, 200, 0]
print("蓝色值:", blue)

# 修改像素值
img[100, 200] = [255, 0, 0]  # 变成蓝色

注意,OpenCV默认的颜色顺序是 BGR,不是RGB。这一点特别容易搞混。我记得刚开始做项目时,用OpenCV读图,然后用Matplotlib显示,结果颜色全乱了——因为Matplotlib用的是RGB顺序。

核心要点: OpenCV中,图像是NumPy数组。shape告诉你尺寸,dtype告诉你精度,索引访问告诉你像素值。这三个属性,是后续所有图像处理的基础。

2.5 实战小练习:读取、显示、保存一条龙

来,我们写一个完整的例子。把上面学的串起来:

import cv2
import os

# 1. 读取图像
img_path = 'hand.jpg'
img = cv2.imread(img_path)

if img is None:
    print(f"无法读取图像: {img_path}")
    exit()

# 2. 显示图像属性
print(f"图像尺寸: {img.shape}")
print(f"总像素数: {img.size}")

# 3. 显示图像
cv2.imshow('Original Hand', img)
cv2.waitKey(0)

# 4. 修改一下:把图像上半部分变蓝
img[:240, :] = [255, 0, 0]  # 前240行变成蓝色

# 5. 显示修改后的图像
cv2.imshow('Modified Hand', img)
cv2.waitKey(0)

# 6. 保存图像
output_dir = 'output'
if not os.path.exists(output_dir):
    os.makedirs(output_dir)

cv2.imwrite(f'{output_dir}/hand_modified.jpg', img)
print("图像已保存")

cv2.destroyAllWindows()

这个例子,你跑一遍,就能直观感受到:读取、显示、修改像素、保存,整个流程就通了。

我的建议: 初学者可以先拿一张自己的照片试试。看看shape是多少,试试修改某个区域的像素值,看看效果。动手,是最好的学习方式。

小结

这一章,我们学了四个基本操作:

  • 读取: cv2.imread(),注意检查返回值是否为None
  • 显示: cv2.imshow() + waitKey(),缺一不可
  • 保存: cv2.imwrite(),注意目录存在
  • 属性: shape、size、dtype,以及像素访问

下一章,我们会进入更核心的内容——颜色空间转换。为什么肤色检测要用HSV?为什么BGR不适合做颜色分割?到时候你就明白了。

好了,今天的课就到这里。去试试代码吧,有问题随时交流。