本节摘要:imread、imshow、imwrite 构成读—显—存闭环,是所有视觉程序的第一段代码。本节讲透它们的参数与陷阱——imread 失败不抛异常而是返回 None、waitKey 与 imshow 必须配对、resize 的插值方法怎么选——并覆盖缩放、裁剪、翻转、通道拆合、像素读写这些日常操作。读完你能独立完成一条"读图 → 改图 → 存图"的最小流水线。
阅读完本节,你应当能够:
不需要任何素材,我们手工造一张图,走完读改存全流程(真实文件同理,把造图换成 imread 即可):
import cv2 import numpy as np # 造一张 300x400 的图,中间画一个绿色矩形 img = np.zeros((300, 400, 3), dtype=np.uint8) cv2.rectangle(img, (100, 80), (300, 220), (0, 255, 0), -1) # 显示:imshow 必须紧跟 waitKey,否则窗口无响应 cv2.imshow('demo', img) key = cv2.waitKey(0) # 0 表示无限等待按键 cv2.destroyAllWindows() # 保存:格式由扩展名决定 cv2.imwrite('result.png', img)
这段二十行的代码里有三个值得停下来的点。其一,waitKey(0) 参数是等待毫秒数,0 表示一直等到按键;返回值是按键的键值,拿它和 27(ESC 的 ASCII 码)比较就能实现"按 ESC 退出",这是后面视频循环的标准写法。其二,rectangle 的坐标参数是 (x1, y1) 和 (x2, y2)——先横后纵,与数组索引的先行后列相反,上一节的口诀这里直接用上。其三,imwrite 按扩展名决定编码器,写成 .png 就是无损保存,写成 .jpg 就是有损压缩。
对真实文件,读图的标准姿势带防御:
img = cv2.imread('example.jpg', cv2.IMREAD_COLOR) if img is None: raise FileNotFoundError('读取失败:检查路径、文件名与格式支持')
为什么必须防御?因为 imread 读不到文件时不抛异常,只返回 None。如果你直接把 None 传给后续函数,报错会出现在很远的地方,信息还难懂。路径错、文件损坏、格式不支持(比如一些小众的 TIFF 变体)、中文路径在某些版本上读不了——四种原因,同一个 None。在源头判一次,一分钟省半小时。
| 模式标志 | 结果 | 典型用途 |
|---|---|---|
| IMREAD_COLOR | 3 通道 BGR,忽略透明 | 默认选择,绝大多数场景 |
| IMREAD_GRAYSCALE | 单通道灰度 | 进边缘、阈值类算法前一步到位 |
| IMREAD_UNCHANGED | 原样读入,含 alpha 通道 | 处理透明 PNG、需要第 4 通道时 |
数字简写分别是 1、0、-1,旧代码里常见 cv2.imread(p, 0),就是灰度读取。用带名字的标志,可读性完全不同。
imshow 并不真正"刷新"窗口,它把图像放进待显示队列,真正的渲染发生在 waitKey 里——这个函数一边等键盘,一边给 GUI 线程处理消息的机会。所以漏掉 waitKey 的 imshow 只会闪出一个无响应的灰色窗口。反过来,处理批量图片又不能每次都无限等待,常见写法是 waitKey(1000):显示一秒自动继续。多窗口的用法是多次调用 imshow 传不同窗口名,OpenCV 会开多个窗口平铺。
改变尺寸时,目标图的每个像素值要由原图"推算"出来,推算规则就是插值方法:
| 插值方法 | 特点 | 什么时候用 |
|---|---|---|
| INTER_NEAREST | 取最近邻,最快,有锯齿 | 像素画、掩码图(不允许出现"新灰度值") |
| INTER_LINEAR | 双线性,速度与质量均衡 | 默认;放大场景首选 |
| INTER_AREA | 区域平均 | 缩小场景首选,抗摩尔纹 |
| INTER_CUBIC / INTER_LANCZOS4 | 高阶插值,慢但平滑 | 对放大质量要求高时 |
一个高频翻车点:缩小掩码或标签图用了默认的 LINEAR,结果是 0 和 255 的边界处出现一堆中间值,二值图被悄悄污染成"灰图"。掩码缩放务必 NEAREST。缩小照片用 AREA,放大用 LINEAR 或 CUBIC,是我多年的默认配置。
small = cv2.resize(img, (200, 150), interpolation=cv2.INTER_AREA)
注意第一个尺寸参数是 (宽, 高)——又是先 x 后 y 的惯例。
裁剪不需要专门函数,直接切片,零拷贝成本:
roi = img[50:250, 100:350] # 行50到249,列100到349 roi[:, :] = (0, 0, 255) # 注意:这会改动原图!切片是视图不是副本
切片得到的是原数组的视图,改它等于改原图。想要独立副本,用 roi.copy()。这个语义与 NumPy 完全一致,但在图像场景里更容易踩:你把 ROI 传给某个函数"看看效果",回来发现原图也被改了。
翻转有专门函数:cv2.flip(img, 1) 水平翻转(1 是翻转码),flip(img, 0) 垂直翻转,flip(img, -1) 两个方向一起翻。摄像头镜像显示(主播看自己习惯镜像)就是水平翻转最常见的应用。
b, g, r = cv2.split(img) # 拆成三张单通道图 merged = cv2.merge((b, g, r)) # 按顺序合回去
拆出来的每个通道本身是一张灰度图——"红色通道图"里越亮的区域表示红色分量越强。这个操作在观察颜色分布、做通道级增强时很有用。但 split 代价不小(数据复制三份),如果只想改一个通道,用 NumPy 索引 img[:,:,0] 更省。彩色转灰度则是 cv2.cvtColor(img, cv2.COLOR_BGR2GRAY),它不是简单取平均,而是按人眼敏感度加权(绿色权重最高),所以天空的蓝转灰后偏暗、草地偏亮,符合人的观感。
读改存的完整流程图:
改一个像素可以 img[10, 20] = (255, 0, 0),但大量逐点访问时,NumPy 花式索引比 Python 循环快得多。比如"把整张图里所有超过 240 的亮点压到 200",一行搞定:
img[img > 240] = 200
向量化的思维方式从本节就该建立:想改的像素先用布尔条件"圈"出来,再整体赋值。
4K 图直接 imshow 会超出屏幕,先 resize 到一半再看。想一次对比原图与处理结果,用 np.hstack((img, result)) 水平拼接后显示一个窗口,比开两个窗口直观。注意 hstack 要求两图同高且同通道数,不一致先统一尺寸。
部分 OpenCV 版本的 imread 在 Windows 上读不了含中文的路径,仍返回 None。规避方式是路径全用英文,或者用 NumPy 中转:以二进制读入文件再解码,绕过 imread 的路径处理。团队项目里直接规定"素材目录只用英文命名",是最省心的制度解。
⚠️ 常见坑:imwrite 返回 False 但不报错。原因通常是目录不存在、没有写权限,或扩展名拼错(如
.jpge)。保存关键结果时判断一下返回值再往下走,否则跑完一小时的处理发现什么都没存下来。
💡 关键直觉:把"读 → 判空 → 处理 → 存"写成固定开头的四行模板,每次新脚本直接复制。防御性检查的成本是两行代码,收益是避免整条流水线在错误数据上空转。
| 我想做 | 用什么 | 参数注意点 |
|---|---|---|
| 读入并转灰度 | imread 加 IMREAD_GRAYSCALE | 一步到位,省一次 cvtColor |
| 缩小照片 | resize 加 INTER_AREA | 尺寸参数是宽在前 |
| 缩小掩码 | resize 加 INTER_NEAREST | 防止引入中间灰度值 |
| 裁一块区域 | 数组切片 | 得到视图,改动会影响原图 |
| 水平镜像 | flip 翻转码 1 | 摄像头自拍的常用姿势 |
| 分离颜色分量 | split 或数组索引 | 改单通道优先用索引 |
历史习惯叠加的结果:shape 是数组维度的顺序(行在前,即高在前),resize 沿用了几何坐标的惯例(x 在前,即宽在前)。抱怨没有意义,记住口诀(第 1 章第 1 节)并在写完代码后用 shape 打印验证,才是务实做法。同类陷阱还有 rectangle 等绘图函数的坐标参数,全是先 x 后 y。
三件事:文件名排序要固定(某些系统目录遍历顺序不定,显式 sorted);每张图读后判空(一张坏图不应当炸掉整批任务,记日志跳过);循环里不要重复创建窗口与反复销毁(imshow 复用同一窗口名即可)。批处理脚本的健壮性不在算法在细节,这三条覆盖了大多数翻车现场。
看这张图的用途。作为中间结果(还要继续处理),无损是刚需,PNG 别无选择;作为最终成片归档,JPEG 的体积优势明显。一个折中实践:处理流水线内部全用 PNG,最后一步按需转存 JPEG,兼顾精度与存储。
看 shape 的第三个数:没有第三维是灰度,是 3 的彩色,是 4 的带透明。第 1 章第 1 节的"先打印 shape"习惯在这里直接兑现。需要丢弃 alpha 时,用 IMREAD_COLOR 读入或对数组做通道切片。
日常脚本里有一批"高频三行组合",值得单独记下。
给检测结果打标签:rectangle 画框之后 putText 写类别,两个函数的坐标同源(都是先 x 后 y),框的左上角减去字高的位置放文字最稳妥(文字在框外上方,不遮挡目标)。开窗对比:hstack 拼接原图与结果并排显示,拼接前统一尺寸(resize 到同高),配 window 名写清是什么。逐帧缩略预览:视频处理时把每帧缩到四分之一再 imshow,预览窗口不挡屏幕,处理速度也少受显示拖累——显示本身有开销,很多人不知道自己的"处理慢"里有一半是全尺寸 imshow 的锅。
另一个值得沉淀的习惯是"读写配对的压力测试":把你的处理结果 imwrite 成 PNG 再 imread 回来,逐像素比对是否一致。无损格式应当完全一致,出现差异说明流程里混进了有损环节(比如中间存了 JPEG)。这个测试写一次,日后排查"结果莫名其妙变差"时直接复用。
imshow 的窗口默认按图像原始尺寸创建,大图超出屏幕时可以用 namedWindow 加 WINDOW_NORMAL 标志先建可缩放窗口再显示,窗口可任意拉伸(图像随之缩放渲染,不影响数据本身)。调试高分辨率图时这个功能很实用。
相机驱动或文件的图像原点约定不同(部分采集卡给出的是倒置数据)。别在每张图上手动 flip 修,去源头改采集参数或流的翻转属性(第 5 章第 1 节会见到相关属性),在流水线入口统一摆正一次即可。
至此第 1 章收尾。你已经能读图、改图、存图,理解了数组世界观。下一章进入六件套正餐——第一道是滤波:噪声从哪来,中值和高斯各治什么病。