在碰任何图像库之前,先把三件事想明白:一张图片在计算机里到底是什么、为什么要处理它、处理它要付出多少代价
核心方法论:第一性原理
「如果你不能把'一张图片在计算机里就是一堆数字'讲给完全不懂的人听,说明你还没有真正理解图像处理。这一章我们不动任何库,只用第一性原理拆开三件事:图像由什么构成、服务端为什么必须处理图像、资源到底有多紧张。把这张全景图画清楚,后面十五章的每一块拼图,你都知道该放在哪里。」
用第一性原理问一个问题:一张照片在计算机里到底是什么?不是"图片",不是"文件",而是一大串数字。放大任何一张数码照片,你会看到它是由密密麻麻的小色块拼成的——每个小色块就是一个像素(pixel,picture element 的缩写,直译就是"图像元素")。像素是图像的最小单位,每个像素只负责记录一个颜色。把几百万个像素按行列排好,就得到一张位图(bitmap,也叫栅格图,raster image)。与之相对的是矢量图(vector image,比如 SVG),它存的不是像素而是"点、线、圆"的数学描述,放大不模糊——但照片这种天然连续的内容只能用量化后的像素来表达,所以本教程讨论的图像处理,几乎全指位图。
既然图像 = 像素矩阵,那么"这张图有多大"就由两个数决定:分辨率(resolution,宽 × 高,单位是像素)和每个像素占多少字节。最常见的彩色位图是 RGB 三通道、每通道 8 位(bit):红(R)、绿(G)、蓝(B)各用一个 0~255 的数字表示强度,三个通道拼在一起就是 24 位真彩色,能表达 256³ ≈ 1670 万种颜色。于是内存占用有一个小学乘法公式:宽 × 高 × 通道数 × 每通道字节数。这就是图像处理的"第一性原理"——后面所有关于内存、带宽、性能的讨论,都要回到这个公式。
为了让你"看得见"像素,这里用一个极其朴素的格式举例:PPM(Portable Pixmap)——它把每个像素的 RGB 值直接以纯文本写进文件,没有任何压缩。下面的文件就是一张 2×2 的图:左上红、右上绿、左下蓝、右下白。你可以亲手复制到一个文件里,用任何看图软件打开它:
# tiny.ppm —— PPM 的 P3 变体:文件头 + 像素数据,全部是能直接读的 ASCII 文本
P3
# 第 1 行:P3 = "ASCII 文本版 PPM"(P6 是二进制版)
# 第 2 行:宽 2、高 2,即一共 4 个像素
2 2
# 第 3 行:每个通道的最大值 255,表示 8 位每通道
255
# 之后每 3 个数是一个像素的 R G B:红 / 绿 / 蓝 / 白
255 0 0 0 255 0
0 0 255 255 255 255
现在用那个乘法公式算一笔账——同样是"一张图",分辨率不同,内存开销天差地别。下面这条命令算出 1080p 一帧画面的未压缩 RGB 内存占用:
# 未压缩位图内存 = 宽 × 高 × 通道数 × 每通道字节数
python3 -c "print(1920 * 1080 * 3 / 1024 / 1024)"
# 输出约 5.93 —— 1080p 一帧 RGB 位图约 5.9 MiB(约 6.2 MB)
# 对比:一张 4000×3000(1200 万像素)的照片,未压缩要 36 MB 内存
| 常见尺寸 | 像素尺寸 | 像素总数 | 未压缩 RGB 内存(约) |
|---|---|---|---|
| 缩略图 | 256 × 256 | 6.6 万 | 0.2 MB |
| 720p 帧 | 1280 × 720 | 92 万 | 2.8 MB |
| 1080p 帧 | 1920 × 1080 | 207 万 | 6.2 MB |
| 4K UHD 帧 | 3840 × 2160 | 830 万 | 24.9 MB |
| 1200 万像素照片 | 4000 × 3000 | 1200 万 | 36 MB |
把"图像 = 像素矩阵"焊死在脑子里:读图就是"把文件解码成矩阵",写图就是"把矩阵编码成文件",处理就是"对矩阵做数学运算"。后面学 OpenCV 的 Mat、学 LibRaw 的 RAW 解码、学 CUDA 的像素级并行,全都是在跟这个矩阵打交道。
上一节说像素存的是"颜色",但严格讲,数字本身不是颜色。255, 0, 0 这组数在电脑屏幕上显示成鲜红,在某个品牌的显示器上可能是暗红,在印刷品上又完全是另一回事。为什么?因为"数字 → 可见颜色"需要一套解释规则,这套规则就叫颜色空间(color space)——它规定了三件事:三个通道分别代表什么、每个数值对应多亮的颜色、整套规则覆盖多大范围的颜色(即色域,gamut)。同样的数字,套用不同的规则,出来的颜色就不同。
最常用的颜色空间是 RGB:基于加色模型(additive color model)——屏幕是黑底发光,红绿蓝三束光叠加,越加越亮,三色全开就是白。R、G、B 分别是红、绿、蓝三个通道(channel)。但"RGB"只是通道结构,还没规定数值映射——所以业界定了一个具体标准:sRGB(standard RGB),由惠普和微软于 1996 年提出,1999 年成为国际电工委员会标准 IEC 61966-2-1。它是网页、显示器、数码相机默认的颜色空间:一张 8 位每通道、没标注颜色空间的图片,业界默认按 sRGB 解释。sRGB 之所以重要,是因为它给"屏幕上的数字"立了规矩——你写的 #FF0000,在遵守规范的设备上应该显示成同一个红。这也埋了一个伏笔:真正讲究的流程要靠 ICC 配置文件(International Color Consortium 国际色彩联盟制定的色彩管理文件)来精确描述设备特性,这属于第 2 章的格式话题。
另一种完全相反的颜色空间是 CMYK:基于减色模型(subtractive color model),用于印刷。纸张是白的,油墨是往白纸上"吸光"——青(Cyan)、品红(Magenta)、黄(Yellow)三色油墨按比例叠印,越叠越暗,理论上三色全叠是黑,但实际油墨不纯,叠出来是脏棕色,所以还要加第四色 K(Key,黑色)来压暗部、省油墨。这就是印刷四色(four-color process)的来历,对应的印刷过程控制标准是 ISO 12647。一句话说人话:屏幕用 RGB 发光造色,印刷用 CMYK 吸光造色;同一张图在这两个世界里,数值和颜色都要重新换算。
先看网页开发最常见的"数字 → 颜色"写法——十六进制颜色,它的本质就是把三个 0~255 的数字用两位十六进制拼起来:
# #RRGGBB:每两位十六进制数 = 一个通道的 0~255 值
# #FF0000 = 红(R=255, G=0, B=0);#00FF00 = 绿;#0000FF = 蓝;#FFFFFF = 白
python3 -c "print(int('FF', 16), int('00', 16), int('00', 16))"
# 输出:255 0 0 —— 和 1.1 节 PPM 文件里的 "255 0 0" 是同一件事
再往深一层:sRGB 的数值和"实际亮度"不是线性关系(人眼对暗部更敏感,所以编码时给暗部更多精度)。sRGB 标准用一条分段函数定义数值与线性光强的换算,下面这段 Python 就是它的线性化方向(sRGB 值 → 线性光强),公式来自 IEC 61966-2-1,不是我们编的:
# sRGB 线性化(IEC 61966-2-1 定义):把 0~1 的 sRGB 值还原为线性光强
def srgb_to_linear(c):
if c <= 0.04045:
return c / 12.92
return ((c + 0.055) / 1.055) ** 2.4
# 8 位值 128(约中间灰)在线性空间里其实只有约 0.2158 的亮度:
print(srgb_to_linear(128 / 255))
# 输出约 0.2158 —— 数字上的"一半"并不是亮度上的一半
| 维度 | RGB / sRGB(屏幕世界) | CMYK(印刷世界) |
|---|---|---|
| 造色原理 | 加色:黑底上发光,越叠越亮 | 减色:白纸上吸光,越叠越暗 |
| 通道构成 | R、G、B 三通道 | C、M、Y、K 四色油墨 |
| 面向设备 | 显示器、相机、手机、网页 | 打印机、印刷机、出版 |
| 代表标准 | sRGB(IEC 61966-2-1) | ISO 12647(印刷过程控制) |
| 色域范围 | 屏幕可发光颜色 | 油墨可吸收颜色(通常更窄) |
接下来问第二个"为什么":浏览器和手机自己就能显示图片,为什么还需要在服务器上处理图像?答案很简单:一张原图满足不了所有场景。相机拍出来的 HEIC、手机拍出来的 12MP 原图、设计稿里的 TIFF——它们要么格式不通用,要么体积太大,要么用户根本不需要那么高的质量。服务端图像处理的本质,就是在"用户拿到的"和"原始素材"之间做翻译与裁剪。归纳起来,服务端图像处理主要回答三类需求:转码(transcode)、预览(preview/thumbnail)、视觉(computer vision)。
转码:把一种格式换成另一种。最典型的就是苹果生态的 HEIC——iPhone 拍的照片默认存成 HEIC(HEIF 容器 + HEVC 编码,体积只有 JPEG 的一半左右),但绝大多数网页和第三方服务不认它。于是服务端拿到 HEIC 后要立刻转成 JPEG 或 WebP 再分发。这不是假想场景:本教程素材里真实发生过"用 libvips 处理苹果平台 HEIC 图片格式的转码"这件事,第 7 章会完整复现。转码还包含"统一规格":把用户上传的乱七八糟的格式、尺寸、颜色空间,全部归一成服务端约定的标准。
预览:同一张图,列表页只需要 200 像素的缩略图,详情页要 1280 宽的大图,原图则几乎不会直接发给用户——因为 12MB 的原图在网络上是巨大的浪费(1.4 节算账)。服务端的常见做法是预生成多尺寸(responsive images):上传时一次性生成缩略图、中图、大图几个版本,用户请求时按需返回。这也是为什么"缩略图"是几乎所有图像库的标配功能。看两个真实命令——libvips 的 vips thumbnail 和 ImageMagick 7 的 magick:
# 缩略图:把 HEIC 原图压成 256 像素宽的 JPEG(libvips 自带命令行工具,内存占用极低)
vips thumbnail input.heic thumb.jpg 256
# 转码:HEIC → JPEG,质量 85(ImageMagick 7 统一用 magick 命令)
magick input.heic -quality 85 output.jpg
# 批量:把一个目录里的 HEIC 全部转成 1280 宽的 WebP(shell 循环)
for f in *.heic; do magick "$f" -resize 1280x720 "${f%.heic}.webp"; done
视觉:让程序"看懂"图像内容。OCR 识别文字、人脸检测、商品分类、自动驾驶感知——这些推理任务大多跑在服务端(GPU 集群)而不是用户手机里,因为它们吃算力、要模型、要统一管理。图像处理库里的"视觉担当"是 OpenCV(计算机视觉的事实标准);本教程素材里真实记录过"用 OpenCV 做了一个 Harris 角点检测器和 Canny 边缘检测器,总共花了一个小时"——第 9、10 章会复现。先看视觉任务的最基本操作长什么样(OpenCV 4.x 的真实 API):
// 服务端视觉分析的典型入口:读图 → 缩小 → 保存(OpenCV 4.x 真实 API)
#include <opencv2/opencv.hpp>
int main(int argc, char** argv) {
// imread 把图片解码成 Mat 矩阵;IMREAD_COLOR = 按 3 通道 BGR 读入
cv::Mat img = cv::imread("input.jpg", cv::IMREAD_COLOR);
cv::Mat small;
cv::resize(img, small, cv::Size(640, 360)); // 缩放到 640×360
cv::imwrite("small.jpg", small); // 保存回 JPEG
return 0;
}
| 场景 | 典型任务 | 代表工具 | 本书章节 |
|---|---|---|---|
| 转码 | HEIC→JPEG、格式统一、质量压缩 | libvips、ImageMagick | 第 2、7 章 |
| 预览 | 缩略图、多尺寸、水印 | libvips、CImg | 第 5、7 章 |
| 视觉 | 特征检测、人脸、OCR、分类 | OpenCV | 第 9~12 章 |
| 元数据 | 读取 EXIF 拍摄信息、版权管理 | Exiv2、exiftool | 第 4 章 |
| RAW 解码 | 相机原始数据 → 可处理位图 | LibRaw | 第 3、6 章 |
判断一个服务端图像需求,先问它属于哪一类:格式不对 → 转码;太大太慢 → 预览/缩略图;要理解内容 → 视觉。三类需求对应三类完全不同的技术栈,本书的章节布局就是按这个分类展开的。
第三个"为什么"要算账:图像处理为什么是资源大户?回到 1.1 的公式——未压缩位图的内存 = 宽 × 高 × 通道数 × 每通道字节数。一张 1200 万像素的照片就要 36 MB 内存,这还只是"一张原图"。真实的服务端场景是:用户上传、格式转换、缩略图生成、视觉分析,常常并发处理成百上千张图——每张图在内存里可能同时存在原图、中间结果、输出图多个副本。处理 100 张 12MP 原图就是 3.6 GB 内存起步,还没算解码器的临时缓冲。这就是为什么 1.3 节说"缩略图是标配":在内存里处理 0.2 MB 的缩略图和 36 MB 的原图,代价差两个数量级。
再看带宽。服务端处理的每一张图最终都要通过网络送到用户设备,流量 = 图片体积 × 访问次数,这是实打实的账单。一张 2 MB 的 JPEG 被访问 1000 次就是 2 GB 流量;换成 200 KB 的缩略图,同样的访问量只消耗十分之一。对高访问量的站点来说,"给用户哪张图"直接决定 CDN 账单和首屏速度——这也是"响应式图片"(responsive images)思想的经济学根源:服务端多花一点算力预生成多尺寸,换取带宽和用户体验的巨额回报。下面两段脚本就是这两笔账的计算:
# 内存账本:12MP 照片未压缩占多少内存?
python3 -c "print(4000 * 3000 * 3 / 1024 / 1024)"
# 输出 34.33 —— 约 34 MiB(十进制约 36 MB)
# 并发 100 张:约 3.4 GiB 内存,还没算中间结果和输出副本
# 带宽账本:一张 2 MB 的 JPEG 被访问 1000 次 = 多少流量?
python3 -c "print(2 * 1000)"
# 输出 2000 —— 2000 MB ≈ 2 GB 流量
# 换成 200 KB 的缩略图:同样访问量只花 200 MB,省了 90%
| 图像 | 未压缩内存 | 压缩后典型体积 | 一次访问的流量代价 |
|---|---|---|---|
| 256×256 缩略图 | 0.2 MB | 10~30 KB(JPEG) | 可忽略 |
| 1080p 照片 | 6.2 MB | 150~500 KB(JPEG) | 约 300 KB |
| 12MP 原图 | 36 MB | 3~8 MB(JPEG) | 约 5 MB |
| 4K 单帧 | 24.9 MB | 50~200 KB(HEVC 视频帧) | 视场景而定 |
这笔账还揭示了一个重要视角:图像处理的核心矛盾,是"图像信息量天然巨大"和"内存、带宽、CPU 都有限"之间的矛盾。整个图像处理技术史,本质上就是围绕这个矛盾做文章:压缩算法(第 2 章)用算力换体积,缩略图/多尺寸(第 5、7 章)用预计算换带宽,GPU 加速(第 11、12 章)用并行换时间,性能优化(第 15 章)用算法换内存。记住这个矛盾,你就抓住了全书的线索。
现在看资源现状的第三块拼图:格式碎片化。打开一个真实的服务端 uploads 目录,你大概率会同时看到 JPEG、PNG、WebP、GIF、TIFF,甚至 HEIC 和相机 RAW——为什么图像格式这么多?因为每一种格式都是在"体积、质量、功能、兼容性"之间做了不同取舍:有损压缩(JPEG)牺牲细节换体积,无损压缩(PNG)保真但更大,支持透明通道的、支持动画的、面向印刷的、面向相机的……各有各的生态位。文件扩展名背后是两样东西:编码算法(怎么压缩像素数据)和容器结构(怎么组织文件内的元数据、缩略图、图像数据),细节是第 2 章的主题,这里先建立"格式是分层的选择"这个观念。
碎片化给服务端带来的是实打实的工程负担:每多支持一种格式,就要多引入一套编解码依赖。回想素材里 libvips 的依赖矩阵——为了读各种格式,它要链接 libjpeg-turbo、libpng、libwebp、libheif 等一堆底层编解码库,第 7 章会亲眼看到这些依赖怎么一个个装起来。格式碎片化还意味着:"读图"这个动作远没有看起来简单——同一个"打开文件"的 API 背后,是几十种格式各自的解码器在分工。
拿到一张陌生图片,先让系统自报家门。Linux 的 file 命令和 ImageMagick 的 identify 是判断格式的两把瑞士军刀:
# 问三个问题:什么格式?多大?什么颜色空间?
file photo.jpg
# 示例输出:JPEG image data, baseline, precision 8, 4000x3000, components 3
magick identify photo.jpg
# 示例输出:photo.jpg JPEG 4000x3000 8-bit sRGB 4.2MiB 0.010u 0:00.008
# 一个真实服务端目录里可能同时躺着这些格式——每多一种,就多一套编解码依赖
ls -lh uploads/
# -rw-r--r-- 4.2M photo.jpg (有损,网页照片主力)
# -rw-r--r-- 2.8M logo.png (无损 + 透明通道,UI 图形)
# -rw-r--r-- 1.9M banner.webp (新一代网页格式,更小)
# -rw-r--r-- 6.1M scan.tiff (印刷/存档,标签式结构)
# -rw-r--r-- 38M photo.nef (相机 RAW,第 3 章的主角)
| 格式 | 有损/无损 | 一句话特点 | 典型用途 |
|---|---|---|---|
| JPEG | 有损 | 照片压缩的王者,无处不在 | 网页照片、相机直出 |
| PNG | 无损 | 支持透明通道(alpha) | UI 图形、图标、截图 |
| GIF | 无损(限 256 色) | 调色板 + 逐帧动画 | 小动画、表情包 |
| WebP | 两者皆可 | Google 出品,同等质量体积更小 | 现代网页 |
| AVIF | 有损/无损 | 基于 AV1 编码,压缩率最高 | 下一代网页格式 |
| HEIC/HEIF | 有损 | 基于 HEVC 编码,苹果生态默认 | iPhone 照片 |
| TIFF | 无损 | 标签式结构,专业级 | 印刷、扫描、存档 |
| RAW 系 | 传感器原始数据 | 未加工的相机底片 | 摄影后期(第 3 章) |
这张表就是"格式碎片化"的全貌。注意一个趋势:格式还在持续进化——WebP 还没普及完,AVIF 又来了。对服务端开发者来说,与其追着格式跑,不如理解"编码算法 + 容器"的抽象层次:理解了 JPEG 的 DCT 有损压缩和 PNG 的调色板/透明机制(第 2 章),任何新格式出现,你都能快速把它归类到"有损/无损/专业/相机"的某个象限里。
最后把这本书的工具箱摊开。图像处理是一个"库的生态",本教程围绕六个库展开,素材来自真实的使用与编译记录。这里先各用一句话介绍它们(详细对比是第 5 章的主场):Exiv2 是读写 EXIF/IPTC/XMP 元数据的 C++ 库,还带命令行工具;libvips 是需求驱动、水平线程的高性能图像处理库,约 300 种运算、内存占用极低,素材里主要拿它处理苹果 HEIC 转码;CImg 只有一个 .h 头文件,轻量好用,矩阵运算和显示类方便;LibRaw 专门读取相机 RAW 文件(CR2/NEF/RAF/DNG 等);CxImage 是全开源的 C++ 图像类,与 Windows/MFC 配合极好,滤波、旋转、alpha 混合等功能强大;OpenCV 是计算机视觉的事实标准,算法体系最全,做 Harris 角点、Canny 边缘这类特征检测特别顺手。
先混个脸熟——这六个库的官网和仓库地址如下。第 5 章会从功能、平台、许可、适用场景四个维度做一张对比表,第 6~8 章则逐个实战编译和上手:
# 六库速览(官网/仓库,第 5 章逐一对比)
Exiv2 # https://www.exiv2.org/ —— 元数据(EXIF / XMP)
libvips # https://www.libvips.org/ —— 高性能、低内存批处理
CImg # https://cimg.eu/ —— 单头文件轻量图像库
LibRaw # https://www.libraw.org/ —— 相机 RAW 解码
CxImage # https://www.codeproject.com/Articles/1300/CxImage —— Windows 位图操作
OpenCV # https://opencv.org/ —— 计算机视觉
无论用哪个库,"读图"最终都落在同一件事:把文件解码成像素矩阵。以 OpenCV 为例,图像就是一个 Mat 矩阵——行是高度、列是宽度、每个元素是一个 BGR 三元组(注意 OpenCV 的通道顺序是 BGR 而不是 RGB,这是它历史遗留的约定):
// 读图 = 解码成矩阵;取像素 = 按下标访问矩阵元素
#include <opencv2/opencv.hpp>
int main() {
cv::Mat img = cv::imread("photo.jpg", cv::IMREAD_COLOR);
// at<Vec3b>(y, x) 取第 y 行第 x 列的像素(通道顺序是 BGR)
cv::Vec3b px = img.at<cv::Vec3b>(100, 200);
int blue = px[0];
int green = px[1];
int red = px[2];
return 0;
}
全书 16 章分四个部分,正好沿着"图像处理工程师的成长路径"展开:第一部分"图像基础与格式"(第 1~4 章)把像素、格式、RAW、元数据讲透;第二部分"图像处理库实战"(第 5~8 章)先做六库选型对比,再逐个实战 LibRaw、libvips、OpenCV;第三部分"计算机视觉与 GPU 加速"(第 9~12 章)从 OpenCV 的 Mat 核心讲到特征检测,再上 CUDA 让像素级并行飞起来;第四部分"深入工程实践"(第 13~16 章)沉淀依赖链接排错、调试、性能优化和生态收尾。这一章的每个概念,都会在后面的章节里找到落点:像素矩阵 → 第 9 章的 Mat;颜色空间 → 第 2 章的格式;资源账本 → 第 15 章的性能;六库 → 第 5 章的对比表。
| 分部 | 章节 | 主题 |
|---|---|---|
| 一、图像基础与格式 | 1~4 | 全景 → JPEG/PNG/TIFF → RAW 与 DNG → EXIF 与 XMP |
| 二、图像处理库实战 | 5~8 | 六库选型对比 → LibRaw → libvips → OpenCV |
| 三、计算机视觉与 GPU 加速 | 9~12 | Mat 核心 → Harris 与 Canny → CUDA 环境 → CUDA 内核 |
| 四、深入工程实践 | 13~16 | 依赖与链接排错 → 调试 → 性能优化 → 生态收尾 |
这一章的信息量不小,但请记住一条主线:图像是像素矩阵(1.1),颜色靠空间解释(1.2),处理因场景而存在(1.3),资源永远紧张(1.4),格式永远碎片化(1.5),所以我们需要一套趁手的库(1.6)。把这六句话串起来,全景图就画完了。下一章,我们从最具体的格式讲起。
把左边的概念与右边最贴切的解释配对:① 像素;② 位图;③ 分辨率;④ 颜色空间。候选解释:A. 按行列排好的像素矩阵;B. 图像的最小单位,记录一个颜色;C. "数字 → 可见颜色"的解释规则;D. 图像的宽 × 高(像素数)。
回顾 1.1 和 1.2 节:四个概念分别是"最小单位 / 组织方式 / 尺寸描述 / 解释规则"。
① 像素 → B;② 位图 → A;③ 分辨率 → D;④ 颜色空间 → C。注意像素是"点",位图是"点的排列",分辨率描述排列的规模,颜色空间则决定每个点上的数字显示成什么颜色。
用 1.1 节的公式计算:① 一张 1920×1080 的 RGB 8 位位图占多少 MB?② 一张 3840×2160 的 4K 位图呢?③ 如果服务端同时把 50 张 12MP 原图(4000×3000×3)读进内存,最少需要多少 GB?
公式:宽 × 高 × 3 字节 ÷ 1,048,576 = MiB;把十进制 MB 和二进制 MiB 区分开,答案写约数即可。
① 1920×1080×3 ≈ 6.2 MB(约 5.9 MiB);② 3840×2160×3 ≈ 24.9 MB(约 23.7 MiB);③ 单张 4000×3000×3 = 36,000,000 字节 ≈ 34.3 MiB,50 张 ≈ 1.7 GiB——还没算解码中间缓冲和输出副本,实际会更高。这印证了 1.4 节"图像处理是资源大户"的判断。
下面三个真实场景,分别属于 1.3 节的哪一类需求(转码 / 预览 / 视觉)?并说明理由:(a) 电商平台把卖家上传的 12MP 原图自动生成 200px、800px、1600px 三个版本;(b) 图片托管服务把用户上传的 HEIC 统一转成 JPEG 存储;(c) 安防系统对摄像头画面做车牌识别。
三类需求的判据:格式不对 → 转码;太大太慢 → 预览/缩略图;要理解内容 → 视觉。
(a) 预览——多尺寸生成是为了让列表页、详情页各取所需,省带宽省内存;(b) 转码——HEIC 不通用,必须换成 JPEG/WebP 再分发;(c) 视觉——识别车牌是"理解图像内容",属于计算机视觉。注意真实系统常常三者叠加:上传时先转码、再生成多尺寸、后台再跑视觉分析。
用第一性原理回答:为什么服务端普遍"预生成"缩略图,而不是每次用户请求时现场压缩原图?请从内存、带宽、CPU 三个角度各推一层。延伸思考:如果用户上传的全是 HEIC,服务端直接原样存储 HEIC 并把 HEIC 当分发格式,可行吗?为什么?
预生成 = 把算力成本一次性付清;现场压缩 = 每次请求都付。想想 1.4 节的账本:一张原图 36 MB 内存、3~8 MB 体积,而热门图片会被访问成千上万次。HEIC 的分发问题回到 1.3 节"转码"存在的理由。
预生成的理由:内存——现场压缩要把 36 MB 原图读进内存再算,高并发下内存瞬间爆掉;带宽——原图 3~8 MB,直接发给用户是缩略图的几十倍流量;CPU——同样的压缩计算,热门图会被重复算成千上万次,预生成只算一次。所以"一次算好、多次复用"是资源约束下的必然选择。HEIC 直接当分发格式不可行:兼容性太差——绝大多数浏览器和第三方服务不认 HEIC(这正是 1.3 节苹果生态转码需求的来源),分发必须用 JPEG/WebP 这类通用格式;这也是"转码"这个场景存在的根本原因。