1.3 图像读取显示与基本操作


1.3 图像读取、显示与基本操作

本节摘要:imread、imshow、imwrite 构成读—显—存闭环,是所有视觉程序的第一段代码。本节讲透它们的参数与陷阱——imread 失败不抛异常而是返回 None、waitKey 与 imshow 必须配对、resize 的插值方法怎么选——并覆盖缩放、裁剪、翻转、通道拆合、像素读写这些日常操作。读完你能独立完成一条"读图 → 改图 → 存图"的最小流水线。

学习目标

阅读完本节,你应当能够:

  1. 熟练使用 imread 的三种读取模式并解释各自得到的通道数
  2. 解释 imread 失败为什么静默返回 None,并写出防御性代码
  3. 搭配 imshow 与 waitKey 完成窗口交互,包括按 ESC 退出
  4. 用 imwrite 保存处理结果并理解格式由扩展名决定
  5. 完成缩放、裁剪、翻转、通道拆分合并,并说出 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。在源头判一次,一分钟省半小时。

核心原理:每个函数的机制与参数

2.1 imread 的读取模式

模式标志 结果 典型用途
IMREAD_COLOR 3 通道 BGR,忽略透明 默认选择,绝大多数场景
IMREAD_GRAYSCALE 单通道灰度 进边缘、阈值类算法前一步到位
IMREAD_UNCHANGED 原样读入,含 alpha 通道 处理透明 PNG、需要第 4 通道时

数字简写分别是 1、0、-1,旧代码里常见 cv2.imread(p, 0),就是灰度读取。用带名字的标志,可读性完全不同。

2.2 显示机制:为什么 imshow 之后窗口是灰的

imshow 并不真正"刷新"窗口,它把图像放进待显示队列,真正的渲染发生在 waitKey 里——这个函数一边等键盘,一边给 GUI 线程处理消息的机会。所以漏掉 waitKey 的 imshow 只会闪出一个无响应的灰色窗口。反过来,处理批量图片又不能每次都无限等待,常见写法是 waitKey(1000):显示一秒自动继续。多窗口的用法是多次调用 imshow 传不同窗口名,OpenCV 会开多个窗口平铺。

2.3 resize 与插值:缩小放大不是简单地删点加点

改变尺寸时,目标图的每个像素值要由原图"推算"出来,推算规则就是插值方法:

插值方法 特点 什么时候用
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 的惯例。

2.4 裁剪与翻转:数组切片的胜利

裁剪不需要专门函数,直接切片,零拷贝成本:

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) 两个方向一起翻。摄像头镜像显示(主播看自己习惯镜像)就是水平翻转最常见的应用。

2.5 通道的拆与合

b, g, r = cv2.split(img) # 拆成三张单通道图 merged = cv2.merge((b, g, r)) # 按顺序合回去

拆出来的每个通道本身是一张灰度图——"红色通道图"里越亮的区域表示红色分量越强。这个操作在观察颜色分布、做通道级增强时很有用。但 split 代价不小(数据复制三份),如果只想改一个通道,用 NumPy 索引 img[:,:,0] 更省。彩色转灰度则是 cv2.cvtColor(img, cv2.COLOR_BGR2GRAY),它不是简单取平均,而是按人眼敏感度加权(绿色权重最高),所以天空的蓝转灰后偏暗、草地偏亮,符合人的观感。

读改存的完整流程图:

工程实践要点

3.1 像素读写的两种方式

改一个像素可以 img[10, 20] = (255, 0, 0),但大量逐点访问时,NumPy 花式索引比 Python 循环快得多。比如"把整张图里所有超过 240 的亮点压到 200",一行搞定:

img[img > 240] = 200

向量化的思维方式从本节就该建立:想改的像素先用布尔条件"圈"出来,再整体赋值。

3.2 显示大图与多图的技巧

4K 图直接 imshow 会超出屏幕,先 resize 到一半再看。想一次对比原图与处理结果,用 np.hstack((img, result)) 水平拼接后显示一个窗口,比开两个窗口直观。注意 hstack 要求两图同高且同通道数,不一致先统一尺寸。

3.3 中文路径问题

部分 OpenCV 版本的 imread 在 Windows 上读不了含中文的路径,仍返回 None。规避方式是路径全用英文,或者用 NumPy 中转:以二进制读入文件再解码,绕过 imread 的路径处理。团队项目里直接规定"素材目录只用英文命名",是最省心的制度解。

⚠️ 常见坑:imwrite 返回 False 但不报错。原因通常是目录不存在、没有写权限,或扩展名拼错(如 .jpge)。保存关键结果时判断一下返回值再往下走,否则跑完一小时的处理发现什么都没存下来。

💡 关键直觉:把"读 → 判空 → 处理 → 存"写成固定开头的四行模板,每次新脚本直接复制。防御性检查的成本是两行代码,收益是避免整条流水线在错误数据上空转。

3.4 一张自测表

我想做 用什么 参数注意点
读入并转灰度 imread 加 IMREAD_GRAYSCALE 一步到位,省一次 cvtColor
缩小照片 resize 加 INTER_AREA 尺寸参数是宽在前
缩小掩码 resize 加 INTER_NEAREST 防止引入中间灰度值
裁一块区域 数组切片 得到视图,改动会影响原图
水平镜像 flip 翻转码 1 摄像头自拍的常用姿势
分离颜色分量 split 或数组索引 改单通道优先用索引

3.5 常见疑问与解答

resize 的尺寸参数和 shape 的顺序为什么相反?

历史习惯叠加的结果:shape 是数组维度的顺序(行在前,即高在前),resize 沿用了几何坐标的惯例(x 在前,即宽在前)。抱怨没有意义,记住口诀(第 1 章第 1 节)并在写完代码后用 shape 打印验证,才是务实做法。同类陷阱还有 rectangle 等绘图函数的坐标参数,全是先 x 后 y。

批量处理一个文件夹的图,循环里要注意什么?

三件事:文件名排序要固定(某些系统目录遍历顺序不定,显式 sorted);每张图读后判空(一张坏图不应当炸掉整批任务,记日志跳过);循环里不要重复创建窗口与反复销毁(imshow 复用同一窗口名即可)。批处理脚本的健壮性不在算法在细节,这三条覆盖了大多数翻车现场。

保存的 PNG 比 JPEG 大很多倍,值得吗?

看这张图的用途。作为中间结果(还要继续处理),无损是刚需,PNG 别无选择;作为最终成片归档,JPEG 的体积优势明显。一个折中实践:处理流水线内部全用 PNG,最后一步按需转存 JPEG,兼顾精度与存储。

怎么知道一张图有没有 alpha 通道?

看 shape 的第三个数:没有第三维是灰度,是 3 的彩色,是 4 的带透明。第 1 章第 1 节的"先打印 shape"习惯在这里直接兑现。需要丢弃 alpha 时,用 IMREAD_COLOR 读入或对数组做通道切片。

3.6 实战小组合:三行代码的常用配方

日常脚本里有一批"高频三行组合",值得单独记下。

给检测结果打标签:rectangle 画框之后 putText 写类别,两个函数的坐标同源(都是先 x 后 y),框的左上角减去字高的位置放文字最稳妥(文字在框外上方,不遮挡目标)。开窗对比:hstack 拼接原图与结果并排显示,拼接前统一尺寸(resize 到同高),配 window 名写清是什么。逐帧缩略预览:视频处理时把每帧缩到四分之一再 imshow,预览窗口不挡屏幕,处理速度也少受显示拖累——显示本身有开销,很多人不知道自己的"处理慢"里有一半是全尺寸 imshow 的锅。

另一个值得沉淀的习惯是"读写配对的压力测试":把你的处理结果 imwrite 成 PNG 再 imread 回来,逐像素比对是否一致。无损格式应当完全一致,出现差异说明流程里混进了有损环节(比如中间存了 JPEG)。这个测试写一次,日后排查"结果莫名其妙变差"时直接复用。

3.7 常见疑问补遗

显示窗口能调大小吗?

imshow 的窗口默认按图像原始尺寸创建,大图超出屏幕时可以用 namedWindow 加 WINDOW_NORMAL 标志先建可缩放窗口再显示,窗口可任意拉伸(图像随之缩放渲染,不影响数据本身)。调试高分辨率图时这个功能很实用。

一张图读进来是反的(上下颠倒)怎么办?

相机驱动或文件的图像原点约定不同(部分采集卡给出的是倒置数据)。别在每张图上手动 flip 修,去源头改采集参数或流的翻转属性(第 5 章第 1 节会见到相关属性),在流水线入口统一摆正一次即可。

本节要点回顾

  • imread 静默失败:读不到返回 None 而非报错,读后必判空,把问题拦在源头。
  • imshow 必配 waitKey:渲染发生在 waitKey 中,参数即等待毫秒数,返回键值可做 ESC 退出。
  • imwrite 靠扩展名:格式由后缀决定,返回 False 表示写失败,目录与权限是首要嫌疑。
  • 插值方法有讲究:缩小用 AREA、放大用 LINEAR、掩码必用 NEAREST,用错会悄悄毁数据。
  • 切片是视图:裁剪结果与原图共享内存,要独立副本就显式 copy。
  • 向量化优先:布尔索引圈定像素再整体赋值,避免 Python 级逐点循环。

至此第 1 章收尾。你已经能读图、改图、存图,理解了数组世界观。下一章进入六件套正餐——第一道是滤波:噪声从哪来,中值和高斯各治什么病。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U