图像在计算机里是什么
这一章讲三件事: 一张图对程序来说到底是什么; 为什么同一张图用两个不同的库打开,红色和蓝色会颠倒; 以及有一条分界线 —— 哪些操作只挪像素的位置、哪些只改像素的值 —— 这条线到第 13 章会决定一整类做法能不能用。 它在全书链条里的位置是地基: 第 10 到第 14 章全部建在本章第 2 节那一句话上。 遇到的生词都在当场解释。
1. 顶层全景
摄像头拍下的那张图(640×480 彩色)
│
│ ① 它其实是三张表格叠在一起:红一张、绿一张、蓝一张
│ 每张表格 480 行 × 640 列,每格一个 0—255 的数
▼
┌─────────┬─────────┬─────────┐
│ R 表格 │ G 表格 │ B 表格 │
└─────────┴─────────┴─────────┘
│
│ ② 从这里开始,所有花样都是在这些数上动手:
│
├── 只挪位置,不改值 → 翻转、旋转、平移、缩放
├── 只改值,不挪位置 → 变亮、加对比度、模糊、二值化
├── 换一套坐标来描述同一种颜色 → 转成灰度、转成 HSV
└── 拿一个小方块在上面滑一遍 → 卷积(第 10 章)
图说:这一章讲前三行,第 10 章讲第四行。
它们的共同前提只有一句话:图 = 一堆数。
本章主走查: 同一张 640×480 的车前道路图,五次变身 —— 每一步都盯住同一个像素点,写出它此刻具体是哪几个数。 走查从第 3 节开始,到第 7 节走完。
2. 一张图,是一张写满 0 到 255 的表格
这一节只有一句话,但它是后面十二章的地基。
先看现象
你拍一张照片,看到的是路面、车道线、天空。计算机没有「看到」任何东西 —— 它拿到的只是一堆数。那么问题是:这堆数是怎么排的?
做法:把图切成格子,每格记一个亮度
书讲得很清楚:常见的图片是模拟图像,不能直接处理,要先数字化 —— 把图像分割成一个个像素,每个像素用一个灰度值表示; 这样一张图像就变成了一个二维数组,数组里每个元素都是一个 0 到 255 的数1。
两个词在这里就地解释:
| 词 | 意思 |
|---|---|
| 像素 | 把图切成网格之后,最小的那一个格子。640×480 的图有 307 200 个 |
| 灰度值 | 那个格子有多亮。0 是纯黑,255 是纯白,中间是各种深浅的灰 |
一张 4×4 的小灰度图,长这样:
列0 列1 列2 列3
行0 │ 30 │ 32 │ 200│ 205│ ← 左边暗(路面),右边亮(车道线)
行1 │ 28 │ 35 │ 198│ 210│
行2 │ 31 │ 33 │ 201│ 208│
行3 │ 29 │ 30 │ 199│ 206│
图说:「看到一条白线」这件事,对程序来说就是「右边两列的数明显比左边大」。
这几个数是为演示编的,不是书里的数。
彩色图:三张表格叠起来
灰度图一张表格就够了。彩色图要三张。
书的说法是:彩色图可以分解为红、绿、蓝三个通道, 也就是拆成 3 个同样像素大小的灰度图;把这三张按顺序组合起来就是一张彩色图2。
「通道」这个词在这里就地解释:一张彩色图里,负责同一种基色的那一整张表格,叫一个通道。
彩色图 (480, 640, 3)
├── 通道 0:红,一张 480×640 的表,每格记「这里的红有多强」
├── 通道 1:绿,同尺寸
└── 通道 2:蓝,同尺寸
某一个像素点 = 从三张表的同一个位置各取一个数 = (R, G, B)
例如 (200, 30, 40) 就是一块偏红的地方
图说:所谓「调色」「变亮」「提取车道线」,全都是在改这些数。
这一节要记住的就一句话:图 = 一堆按格子码好的数。 第 10 章的卷积是在这堆数上滑一个小方块;第 12 章挑车道线是在这堆数上定一个范围; 第 14 章逐格判类别是给这堆数里的每一格贴一个标签。没有例外。
3. 装这堆数的容器,不是列表
第 03 章讲过列表。处理图像不用它 —— 这一节讲用什么、为什么。
现象先行:列表算起来太啰嗦
假设你要把一张图整体调暗一半。用列表就得这样:
for row in range(480):
for col in range(640):
img[row][col] = img[row][col] // 2 # 三十万次循环,在 Python 这一层跑
三十万次循环全在 Python 这一层跑 —— 而第 02 章说过,慢的正是这一 层。
做法:换成 numpy 的数组
书给了这个库的两个基本对象3:
| 对象 | 是什么 |
|---|---|
| ndarray | 存单一数据类型的多维数组。名字里的 N 是维数:一维、二维、三维都行 |
| ufunc | 能对整个数组一次性动手的函数,不用你自己写循环 |
于是上面那件事变成一行:
import numpy as np
img = img // 2 # 三十万个数一起除,循环在 C 那一层跑
两条差别要记牢:
| 列表 | numpy 数组 | |
|---|---|---|
| 里面装什么 | 什么都行,可以混着装 | 必须同一种类型 |
| 怎么算 | 一个一个来,循环写在 Python 里 | 整块一起算,循环在底层的 C 里 |
「必须同一种类型」听起来是限制,其实正是它快的原因(补充,不在书里,来自通用知识): 类型统一,这些数才能在内存里一个挨一个连续摆放,底层才能一次搬一大块。
三组容易混的方法,书讲得很清楚
这个库有几组名字很像、行为不同的方法,书专门做了对照4:
| 一组 | 差别在哪 |
|---|---|
x.resize() / np.resize() / reshape() | 第一个就地改、没有返回值;第二个允许改前改后元素个数不同,会自动截断或拼接;第三个有返回值、元素个数必须一样,而且可以用 -1 让它自己算某一维 |
ravel() / flatten() | 都是「把多维压成一维」。前者给的是同一份数据的另一个视角(改它会影响原数组),后者是复制一份 |
np.repeat() / np.tile() | 前者复制每一个元素,后者复制整个数组 |
第二组和第 03 章那个 lt = ls 的坑是同一件事: 有的操作给你一份新数据,
有的只是给同一份数据换个名字。分不清这一点,就会出现「我明明没改它,它怎么变了」。
这个库为什么要在这一章讲(而不是等到讲数据分析的时候): 因为「图像是一个数组」这句话里的那个「数组」,指的就是它。 第 12 章会看到「蒙版就是一个 numpy 数组」——到那时这句话才接得上。
4. 主走查开始:两个库读同一张图,结果不一样
这是实战最容易翻车的一处,而且错了不报错。
先看现象
书里的示例图是智能车前置摄像头拍的车前道路图,640×4805 —— 正是第 01 章那条链的第一环。
现在用两个常用的库分别打开它,各取同一个像素点:
用 PIL 打开: img.getpixel((320, 400)) → (200, 30, 40)
用 OpenCV 打开: img[400, 320] → ( 40, 30, 200)
↑ 同一个点,三个数一模一样,顺序反了。
这两个三元组是我们为演示编的,书没有给具体像素值。
如果你不知道这件事,会发生什么: 你以为那是一块偏红的路标(R=200), 其实程序告诉你的是「蓝 200」—— 你会把红的当成蓝的,而程序不会报任何错。
做法:两个库的通道顺序本来就不同
书说得很明确:PIL 打开的图是 RGB 顺序, 而 OpenCV 打开的图是 BGR 顺序 —— 蓝在最前面6。
| 库 | 安装时叫 | 代码里叫 | 通道顺序 |
|---|---|---|---|
| PIL | pillow | from PIL import Image | R、G、B |
| OpenCV | opencv-python | import cv2 | B、G、R |
注意安装名和导入名不一样,这两个库都是。 书特意点了这件事7。
互相转换书也给了8:
# PIL 的图 → OpenCV 的图
cv_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
# OpenCV 的图 → PIL 的图
pil_img = Image.fromarray(cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB))
判断(我们的,不是书里的): 这个坑之所以害人,不是因为它复杂, 而是因为它不报错。凡是「两套约定同时存在、而且都合法」的地方, 都会长出这类静默错误。处理图像时,养成在读进来的第一行就写清顺序的习惯。 如果错,会错在: 如果你的整条流水线从头到尾只用一个库,那这个坑根本不存在, 这条提醒就是多余的开销。判据是:项目里有没有同时出现这两个库的 import。
走查记账
主走查 ① 原图,PIL 读: (320, 400) 这个点 = (R=200, G=30, B=40)
主走查 ② 原图,cv2 读: 同一个点 = (B= 40, G=30, R=200)
↑ 这两组数是为演示编的,不是书里的数。
5. 存的时候也有坑:通道数对不上
这一节讲一个具体的报错,以及它背后的道理。
先看现象
你有一张 .png 图,想存成 .jpg。直接一句 save(),报错:
OSError: cannot write mode RGBA as JPEG
为什么
书给的解释很干净:png 是四通道的 RGBA 模式 —— 红、绿、蓝之外还有一个 alpha 通道(记录「这一格有多透明」);而 jpg 只有三通道 RGB, 它根本没有地方放透明度那一张表9。
治法是先转模式再存:
img.convert("RGB").save("out.jpg")
另一个会让人愣一下的数字
书还给了一个缩略图的例子:对一张 640×480 的图要一个 150×100 的缩略图, 得到的却是 133×10010。
原因是它按原图比例等比缩放: 640÷480 = 4:3, 高压到 100,宽就只能是 100 × 4/3 ≈ 133 —— 再宽就变形了。
你要的: 150 × 100 (比例 3:2)
原图: 640 × 480 (比例 4:3)
它给的: 133 × 100 (比例 4:3,保住了)
图说:它把你给的尺寸当成「不许超过的上限」,而不是「必须精确达到的目标」。
要精确控制尺寸得用另一个方法 —— 那个方法不保比例,图会被拉扁, 但第 13 章训练时恰恰用的就是它(所有图必须一样大)。
6. 一条分界线:改位置的,和改值的
这一节是本章第二个承重点。它本身很简单,但第 13 章有一整节建在它上面。
两类操作
书把图像的几何变换定义得很干脆:几何变换不改变图像像素的值, 仅改变像素的空间排布位置11。
反过来,另一类操作只改值不挪位置:调亮度、调对比度、模糊、二值化。
原图的一小块 左右翻转(只改位置) 调亮(只改值)
┌────┬────┐ ┌────┬────┐ ┌──── ┬────┐
│ 30 │200 │ │200 │ 30 │ │ 60 │230 │
├────┼────┤ ───▶ ├────┼────┤ ───▶ ├────┼────┤
│ 31 │198 │ │198 │ 31 │ │ 62 │228 │
└────┴────┘ └────┴────┘ └────┴────┘
数一个没变,只是换了格子 格子没动,数全变了
图说:这两类操作在代码里长得差不多,但对「标签还对不对」的影响天差地别。
这些数是为演示编的。
| 类别 | 具体操作 | 对像素做了什么 |
|---|---|---|
| 几何变换 | 翻转、旋转、平移、缩放、裁剪 | 只挪位置,值不变 |
| 滤波与增强 | 调对比度、调亮度、调饱和度、模糊、锐化 | 只改值,位置不变 |
为什么这条线要专门画出来
因为第 13 章会拿它做一次很漂亮的判断。
到那里你会看到:通用的说法是「训练数据不够,就把图片翻转、平移、缩放,凑出更多样本」。 而这本书当场指出:在「预测转角」这个任务上,几何变换会直接毁掉数据 —— 图翻过来了,画面里该往左打了,标签却还写着往右。
而只改值的那一类完全没有这个问题 —— 调亮一点,该往左还是往左。
这条线现在先记住,第 13 章兑现。 它是这本书全书最值得带走的一处推理。
7. 同一种颜色,三套坐标
这一节是本章第二个承重词,而第 12 章调参数时用的正是它。
先看现象:为什么不直接用 RGB
你要从图里挑出车道线。车道线是白的、周围是绿的,看起来用 RGB 定个范围就行。
试一次就知道不行: 太阳一偏、灯一暗,同一条白线的 RGB 三个数全都变了。 你调好的范围立刻失效。