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

各位同学,欢迎来到第二章。上一章我们聊了OpenCV的环境搭建,今天咱们直接上手,玩点实在的——图像的基本操作。

说白了,图像在计算机眼里就是个数字矩阵。你看到的猫猫狗狗、风景照片,对OpenCV来说就是一堆0到255之间的数字。我刚开始学的时候,总觉得这很抽象,但当你亲手把一张图片的像素值打印出来,看着满屏的数字,那种感觉——嗯,一下子就通了。

2.1 图像的读取:把图片请进内存

在OpenCV里,读取图像用的是 cv2.imread() 函数。这就像你去图书馆借书,得告诉管理员你要哪本书。

import cv2

# 读取一张图片
img = cv2.imread('lena.jpg')

这里有个坑,我必须要说。OpenCV默认读取的通道顺序是 BGR,不是我们熟悉的RGB。为什么?因为早期相机硬件厂商的习惯。我在项目中第一次用OpenCV显示彩色图像时,发现人脸是蓝色的,当时还以为是摄像头坏了……后来才反应过来,是通道顺序搞反了。

cv2.imread() 还有第二个参数,控制读取方式:

参数值 含义 实际场景
cv2.IMREAD_COLOR (默认值1) 加载彩色图像,忽略透明度 日常处理彩色照片
cv2.IMREAD_GRAYSCALE (值0) 以灰度模式加载 人脸识别、边缘检测前常用
cv2.IMREAD_UNCHANGED (值-1) 加载原图,包括Alpha通道 处理PNG透明图时必用
我的习惯: 如果只是做图像处理,我一般用 cv2.imread('path', 0) 直接读灰度图,省去后面转换的步骤。但如果你要显示彩色图,记得用默认值。

2.2 图像的显示:看看你读对了没

读进来之后,总得看一眼吧?cv2.imshow() 就是干这个的。

cv2.imshow('窗口标题', img)
cv2.waitKey(0)  # 等待按键,0表示无限等待
cv2.destroyAllWindows()

你想想看,如果不加 waitKey(),窗口会一闪而过,你根本来不及看。我曾经在给客户演示时犯过这个错,窗口闪了一下就没了,场面一度很尴尬。

waitKey() 的参数是毫秒数。比如 cv2.waitKey(1000) 表示等待1秒后自动关闭。返回值是按键的ASCII码,这个后面做交互时会用到。

注意: 在Jupyter Notebook或某些远程环境中,imshow() 可能无法正常显示。这时候可以用 matplotlib 来显示,但记得把BGR转成RGB:
import matplotlib.pyplot as plt
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()

2.3 图像的保存:把成果写进硬盘

处理完图像,总得保存下来吧?cv2.imwrite() 搞定。

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

就这么简单。但这里有个细节——保存质量。对于JPEG格式,你可以指定压缩质量:

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

质量范围是0-100,数值越大,画质越好,文件也越大。我一般设95,既保证画质,文件也不会太大。对于PNG格式,可以指定压缩级别:

cv2.imwrite('output.png', img, [cv2.IMWRITE_PNG_COMPRESSION, 3])

压缩级别0-9,0表示不压缩,9表示最大压缩。我个人习惯用3,平衡速度和大小。

2.4 图像属性:认识你的数据

图像读进来了,它长什么样?有多大?什么类型?这些信息很重要。

import cv2

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

# 形状:高度、宽度、通道数
print('图像形状:', img.shape)   # 例如 (512, 512, 3)

# 像素总数
print('像素总数:', img.size)    # 512 * 512 * 3 = 786432

# 数据类型
print('数据类型:', img.dtype)   # uint8

shape 返回一个元组:(高度, 宽度, 通道数)。灰度图只有两个值:(高度, 宽度)

size 是所有像素的总数。对于彩色图,是 高×宽×3。

dtype 通常是 uint8,也就是0-255的整数。为什么是255?因为8位二进制最大能表示255。你想想看,如果每个像素用16位表示,范围就是0-65535,那就是高动态范围图像了。

避坑指南: 我曾经在处理一批医学图像时,发现图像数据是 uint16 类型的,直接用 imshow() 显示出来全白。后来才意识到,OpenCV的显示函数默认按0-255范围显示,超出部分会被截断。解决办法是归一化到0-255。

2.5 像素操作:图像的本质

终于到了最核心的部分。图像就是矩阵,矩阵由像素组成。我们可以直接访问和修改像素值。

2.5.1 访问单个像素

# 访问第100行、第200列的像素
pixel = img[100, 200]
print(pixel)  # 彩色图返回 [B, G, R] 三个值

# 只访问蓝色通道
blue = img[100, 200, 0]

注意:行对应y坐标,列对应x坐标。也就是说 img[y, x]。这个顺序和数学中的坐标系不太一样,我刚开始也经常搞混。

2.5.2 修改像素值

# 把第100行、第200列的像素设为红色
img[100, 200] = [0, 0, 255]  # BGR顺序,红色是 [0, 0, 255]

# 批量修改:把左上角50x50区域设为绿色
img[0:50, 0:50] = [0, 255, 0]

2.5.3 图像裁剪

说白了,图像裁剪就是取矩阵的一部分:

# 裁剪出第100到300行、第200到400列的区域
roi = img[100:300, 200:400]
cv2.imshow('ROI', roi)

这个操作在目标检测中太常用了。比如你检测到人脸,就把人脸区域裁剪出来,再做后续处理。

2.5.4 通道拆分与合并

有时候你需要单独处理某个颜色通道:

# 方法一:索引
b = img[:, :, 0]
g = img[:, :, 1]
r = img[:, :, 2]

# 方法二:使用OpenCV函数
b, g, r = cv2.split(img)

# 合并通道
img_merged = cv2.merge([b, g, r])
性能建议: cv2.split() 虽然方便,但会创建新的数组,消耗内存。如果只是读取某个通道,直接用索引 img[:, :, 0] 更快。我在处理4K视频时,用索引比用split快了将近一倍。

2.6 综合示例:从读取到保存的完整流程

来,我们写一个完整的例子,把今天学的串起来:

import cv2
import numpy as np

# 1. 读取图像
img = cv2.imread('lena.jpg', cv2.IMREAD_COLOR)
print(f'图像尺寸: {img.shape}')
print(f'数据类型: {img.dtype}')

# 2. 显示原图
cv2.imshow('Original', img)

# 3. 像素操作:把图像中心区域涂成蓝色
h, w = img.shape[:2]
center_h, center_w = h // 2, w // 2
img[center_h-50:center_h+50, center_w-50:center_w+50] = [255, 0, 0]

# 4. 显示修改后的图
cv2.imshow('Modified', img)

# 5. 保存结果
cv2.imwrite('modified_lena.jpg', img)

# 6. 等待按键并关闭窗口
cv2.waitKey(0)
cv2.destroyAllWindows()

这个例子虽然简单,但涵盖了图像处理的基本流程:读→处理→显示→保存。你想想看,后面所有的图像处理算法,本质上都是在做第3步——修改像素值。只是修改的规则越来越复杂而已。

2.7 本章小结

今天我们学了五件事:

  • 读取cv2.imread(),注意BGR顺序和读取模式
  • 显示cv2.imshow() + waitKey(),别忘了这对组合拳
  • 保存cv2.imwrite(),可以控制压缩质量
  • 属性shapesizedtype,认识你的数据
  • 像素操作:访问、修改、裁剪、通道拆分合并

这些是图像处理的基本功。下一章我们会聊颜色空间转换,到时候你会发现,原来图像还可以用不同的"语言"来描述。嗯,今天就到这里,动手试试吧。


公众号:蓝海资料掘金营,微信deep3321