1.1 图像基础知识与像素模型


1.1 图像基础知识与像素模型

本节摘要:图像在计算机里不是"图",是数组。本节从像素与通道讲起,说清灰度图(单通道 0–255)与彩色图(三通道 BGR)的存储结构、常见图像格式的取舍,以及 NumPy 视角下的 shape 与 dtype。掌握"图像即数组"这一思维模型,后面所有 OpenCV 函数都只是对数组做数学。

本节要解决的问题与目标

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

  1. 用形状和 dtype 准确描述任意一张图在内存里的样子
  2. 解释灰度图与彩色图的数值含义,并完成两者互转
  3. 说清 OpenCV 为什么用 BGR 顺序而不是 RGB,以及它带来的实际影响
  4. 根据用途在 JPEG、PNG、BMP 之间做出正确选择
  5. 动手验证:修改单个像素值并在窗口中看到变化

先跑起来:一张图在内存里长什么样

照本教程的惯例,先上能跑的。装好环境(下一节讲安装,这里假设你已经能 import cv2),读一张图并打印它的基本信息:

import cv2 img = cv2.imread('example.jpg') print(img.shape) # 例如 (480, 640, 3) print(img.dtype) # uint8 print(img[0, 0]) # 左上角像素的值,例如 [ 87 122 203]

三行输出就是本节的全部核心:shape 告诉你这是一个 480 行、640 列、每格 3 个数的三维数组;dtype 告诉你每个数是 0 到 255 的无符号整数;img[0, 0] 是左上角那个格子,里面装着三个数——蓝、绿、红的强度。

再进一步,把左上角 100×100 的区域强行涂成红色,看看会发生什么:

img[0:100, 0:100] = (0, 0, 255) # BGR:蓝0 绿0 红255 cv2.imshow('modified', img) cv2.waitKey(0) cv2.destroyAllWindows()

运行后左上角出现一块红色方块。注意赋值时三个数的顺序是 (0, 0, 255)——蓝色分量在前,红色分量在后。这个反直觉的顺序不是 Bug,是历史。1999 年 OpenCV 起步时, 相机厂商与 Windows 位图流行 BGR 字节序,这个约定就被沿用至今。

如果你想"亲眼看到数组",可以做一个小实验:用 NumPy 手工造一张图,不依赖任何文件。

import cv2 import numpy as np canvas = np.zeros((300, 400, 3), dtype=np.uint8) # 全黑画布 canvas[:, :] = (255, 200, 150) # 整体填充一种颜色 cv2.imshow('handmade', canvas) cv2.waitKey(0)

一张 300×400 的纯色"图片"就这么凭空造出来了。所谓图像,不过是一个形状规则的整数数组。

核心原理:像素、通道与灰度的数学本质

2.1 像素是采样的结果

真实世界的画面是连续的,但计算机存不下连续的东西。相机感光元件把画面切成网格,每个格子测一个(或一组)亮度值——这个过程叫采样与量化。网格密不密就是分辨率,亮度分几个档就是位深。我们最常用的 8 位图,每个通道把亮度分成 256 档:0 是最暗,255 是最暗到最亮区间的另一头。

灰度图每个像素只有一个数。0 到 255 从黑渐变到白,中间是深浅不同的灰。很多经典算法(Sobel 边缘、Harris 角点、大多数阈值方法)只吃灰度图,不是它们挑剔,而是"亮度变化"这个信息在单通道里已经足够,多出来的颜色通道反而是计算负担。

彩色图每个像素有三个数。最常见的是 RGB 模型:红、绿、蓝三盏灯,不同强度叠加出千万种颜色。而 OpenCV 在内存里把这三个数的顺序排成 B-G-R,也就是说一个像素在内存里的三个字节,第一个是蓝色分量,第三个才是红色分量。这件事的影响贯穿全书:当你用 matplotlib 显示 OpenCV 读进来的图,红蓝会互换,人脸色发蓝、蓝天发红——因为 matplotlib 期待 RGB。解决办法是在显示前做一次通道转换:

rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

除了 RGB 家族,后面还会遇到 HSV 空间(色调、饱和度、明度)。HSV 的好处是把"颜色"和"亮度"拆开,比如"提取画面里所有黄色区域",在 HSV 里按色调范围筛选比在 BGR 里稳定得多。第 2 章会专门用到它。

2.2 用坐标系思考:行、列,不是 x、y

数组索引用的是 img[行, 列]。行从上往下数,列从左往右数,都从 0 开始。所以 img[10, 20] 是第 10 行、第 20 列——视觉上在画面偏上、偏左的位置。而很多函数(比如画矩形时的坐标参数)用的是"先 x 后 y"的惯例,即 (列, 行)。这两套习惯会在你写代码时反复打架,是新手错误的重灾区。

我建议记一个口诀:方括号里先行后列,函数参数里先 x 后 y。切片同理,img[100:200, 300:400] 取的是行 100 到 199、列 300 到 399 的一块矩形区域,OpenCV 术语叫 ROI(感兴趣区域)。

2.3 dtype:为什么是 uint8,什么时候要换

8 位无符号整数是图像世界的默认货币:0–255 恰好一个字节,存储省、传输快、绝大多数函数默认接受它。但有些操作会溢出——两张 uint8 图直接相加,250 加 10 结果是 4(绕回去了),不是 260。NumPy 的加法就是这么定义的,而 cv2.add 会把结果截断在 255。做算术运算前把图转成 np.float32np.float64,算完再转回来,是更安全的习惯。第 2 章讲锐化、第 3 章讲 Harris 角点时,都会先做这个转换(Harris 的输入就要求 float32)。

下面的流程图概括了本节所有概念的关系:

工程实践要点

3.1 图像格式怎么选

格式不是小事,选错会悄悄毁掉你的精度。

格式 压缩方式 适合场景 不适合场景
JPEG 有损 照片、训练数据集、网络传输 二值图、需要精确像素值的中间结果
PNG 无损 中间结果保存、截图、含透明通道的图 超大照片库(体积大)
BMP 不压缩 快速读写、教学演示 任何关心磁盘空间的场合

有损压缩的含义要体会一下:把一张图存成 JPEG 再读回来,像素值已经变了,质量参数越低变得越多。如果你在做阈值分割,存成 JPEG 的中间结果会引入灰度抖动,阈值边缘附近的像素来回跳,导致结果不稳定。我的原则是:原始素材可以 JPEG,中间结果一律 PNG。

3.2 分辨率与通道数的隐形代价

一张 1080P 彩图约 600 万个字节(1920×1080×3),4K 图约 2400 万。逐像素的 Python 循环处理一张 4K 图可能要几秒钟,而 OpenCV 的内置函数(底层 C++ 且并行)只要几十毫秒。所以养成习惯:能用 OpenCV 向量化函数就不要写 Python 双重循环,慢的不是一个数量级。另外,很多算法只对灰度图有意义,进算法前先 cvtColor 降成单通道,等于直接砍掉三分之二的计算量。

⚠️ 常见坑:用 matplotlib 直接显示 OpenCV 读的图,颜色诡异。原因就是本节讲的 BGR 与 RGB 之差,转换一次即可。另一个坑是从网上下载的图片带第四个透明通道(PNG 的 alpha),shape(h, w, 4),直接喂给只接受 3 通道的函数会报错,读图时用 cv2.IMREAD_COLOR 强制转 3 通道,或先做通道裁剪。

3.3 动手验证清单

把下面的实验做一遍,本节就真正内化了:

  1. 读一张彩色图,打印 shape 与 dtype,预测左上角像素的三个数各是什么颜色分量,再用取色器验证。
  2. 分别用 cv2.IMREAD_GRAYSCALEcv2.IMREAD_COLOR 读同一张图,对比 shape 差异。
  3. 把图切成四块(左上、右上、左下、右下),交换位置后重新拼一张"错位图"——全程只用数组切片,不用任何图像函数。
  4. 造一张 512×512 的渐变图:np.arange 生成 0 到 255 的序列,广播到二维。

第 4 个实验的答案大约长这样:

grad = np.tile(np.arange(256, dtype=np.uint8), (256, 1)) cv2.imshow('gradient', grad)

一个方向上的 256 级渐变就出来了。你会看到从黑到白的平滑过渡——这 256 个亮度档,就是后面所有灰度算法操作的原材料。

3.4 常见疑问与解答

灰度转换是简单平均三个通道吗?

不是。COLOR_BGR2GRAY 用的是加权公式,绿色权重约 0.59、红色约 0.29、蓝色约 0.11——依据是人眼对绿色最敏感、对蓝色最迟钝。证据随手可验:拍一张蓝天,转灰度后天空明显偏暗;拍一片草地,转灰度后亮度接近原彩色图。这个加权还有个实际影响:纯蓝与纯绿在彩色图里"亮度相同",转灰度后差出好几档——做基于灰度的检测时,目标颜色会影响它的"灰度可见性",蓝色系目标天然吃亏。

一张图能既叫 8 位又叫 16 位吗?

同一张文件只有一个位深,但处理流程里可以升降。16 位图(医学影像、HDR 拼接的中间产物)每个通道 0–65535,动态范围更大;降到 8 位显示时要做拉伸或截断,直接除以 256 会把暗部细节压成一团。遇到 16 位数据先 print(img.dtype) 确认,再决定转换策略,是处理这类图的第一步。

通道数还有别的可能吗?

有。灰度 1 通道、彩色 3 通道、带透明 4 通道是常见三档,但 OpenCV 的数组结构不限制通道数——HSV 也是 3 通道(含义不同),某些变换的中间结果会有几十个通道。判断一张图"是什么",shape 与产生它的代码比直觉可靠。

为什么建议在灰度图上做算法?

三个理由:亮度信息承载了绝大多数结构特征(边缘、角点、纹理都定义在灰度梯度上);单通道计算量只有三分之一;颜色受光照色温影响剧烈而灰度相对稳定。例外是"颜色本身是判别特征"的任务(成熟度分拣、标志识别),那就要留在彩色空间甚至转 HSV 处理(第 2 章第 4 节)。

3.5 一个贯穿后续的概念热身

最后做一次"概念对表",把本节术语与后面各章的用法提前挂钩,学后文时不至于回头翻。

数组的 shape 在第 2 章几何变换里决定输出画布尺寸,在视频里决定帧的一致性(第 5 章读写器要求帧尺寸恒定)。dtype 在 Harris 角点(必须 float32,第 3 章第 1 节)与图像算术(防 uint8 溢出)两处反复出现。BGR 顺序会在两处咬人:matplotlib 显示(本节讲过)与 DNN 推理的 swapRB 参数(第 4 章第 2 节,方向恰好反过来)。灰度转换的加权公式决定了"蓝色目标灰度可见性差",这条在第 2 章阈值分割的实战里会再次提醒。行列坐标的口诀则是第 2 章画框、取 ROI 的每日常识。

换句话说,本节没有一行"高级代码",但后面五章的每一个高级函数都在消费本节的概念。基础不牢的典型症状是:函数都会调,一出错就懵——因为不知道数据在函数之间流动时发生了什么形态变化。把 shape、dtype、通道序、坐标系这四件事变成条件反射,后面的学习曲线会平缓一半。

本节要点回顾

  • 图像即数组:彩色图是形状为(高, 宽, 3)的 uint8 数组,灰度图是(高, 宽),一切函数都是对这个数组做数学。
  • BGR 不是 Bug:OpenCV 的通道顺序是历史约定,对外展示或对接其它库前必须转换,否则红蓝互换。
  • 格式取舍:JPEG 有损适合素材,PNG 无损适合中间结果,二值图与阈值类任务绝不用 JPEG 存中间结果。
  • 先行后列:数组索引用(行, 列),函数坐标常用(x, y),两套惯例并存,写代码前先想清楚当前用的是哪套。
  • uint8 会溢出:算术运算前转 float,算完再转回,避免 250 加 10 变 4 这类静默错误。
  • 能向量化不循环:Python 逐像素循环比 OpenCV 内置函数慢一到两个数量级。

下一节我们把环境装起来——发行包怎么选、装完怎么验证,这些看似琐碎的事,恰恰是新手卡壳最多的地方。


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