做图像处理这些年,我越来越觉得“图片格式”是很多人忽略却绕不开的基础课。不管是调UI、写脚本、做嵌入式屏幕驱动,还是在Web端处理上传的图片,最终都会遇到一个问题:同一张图,存成BMP、JPG、PNG,文件大小、清晰度、颜色表现能差出一个数量级。你如果不理解底层是怎么回事,就只能靠“碰运气”调参数,今天能跑通明天换个图就翻车。
这篇文章我把BMP、RGB、JPG、PNG这几个最常见的概念串起来讲一遍。不是教科书式地贴定义,而是从实际开发者视角去拆:文件到底怎么存的、像素颜色怎么算的、压缩到底压掉了什么、什么场景该选什么格式。顺带会把平时踩过的坑——比如BMP文件头写错导致图片打不开、PNG打包后路径解析失败、visio导出PNG白边过大、微信dat文件没法预览——这些真实问题都拿出来复盘一遍,让这篇既是原理科普,也是一份能直接救场的实操笔记。
1. 内容整体设计与思路拆解
1.1 一张图片,本质到底是什么
先把一切抽象的问题打回原形:一张图片在计算机里,基本就是一个二维的像素点阵,外加一段描述这张图的元信息。像素点阵记录的是每个位置的颜色值,元信息则告诉程序怎么解释这些颜色值、图片多宽多高、像素排列顺序如何。
用生活化的方式理解,这就像一幅乐高拼图。每个像素是一个单色颗粒,颗粒的颜色用数字描述;拼图的尺寸棋盘是画布的宽高;而文件格式,就是拼图外面的那层包装盒和说明书。说明书写得好不好,决定了别人拿到这盒乐高时,能不能按你的意图拼出完整画面。
BMP就是那种“说明书写得极其简单粗暴”的格式,像素怎么排列就怎么存,不加任何花哨处理。JPG则像压缩饼干,把视觉上不敏感的细节扔掉,换成小体积。PNG像是无损压缩zip,所有颗粒信息完全保留,但通过算法把重复的颜色数据压缩得更紧凑。理解了这个模型,后面再看文件头、像素排列、通道数、压缩编码,基本上就有个主线了。
1.2 为什么需要先懂“格式”,再谈显示和处理
很多刚接触图像的人容易犯一个认知错误:觉得图片就是屏幕上显示的那个样子。实际上,从图片文件到屏幕上的画面,中间至少经历了“文件解析 -> 像素解码 -> 颜色空间转换 -> 驱动显示”这四步。每一步都可能出问题。
我在调试ESP32S3屏幕项目时做过一个很典型的实验:同一张128x128的PNG图标,直接当BMP读取,显示出来的画面完全花掉。原因就是PNG的像素数据和BMP完全不是一套存储逻辑。你不懂文件格式的差异,就会把大量时间耗在“为什么画面不对”上。所以先说清楚格式本身的组织方式,后面所有实操、选型、排查才会有依据。
这篇文章的整体设计思路,就是沿着“存储结构 -> 颜色模型 -> 压缩原理 -> 实际应用”这条线推进。每一章到最后都会落在一个能直接动手验证的任务上,比如手写BMP文件、用Python读取像素RGB值、用OpenCV或Pillow比较JPG/PNG体积差异。这样读完不是只记住一堆名词,而是真的能在项目里用起来。
2. 核心细节解析与实操要点
2.1 BMP文件头拆解:每个字节都有它的身份
BMP是Windows环境下最基础的位图格式,也是我个人认为最适合作为“图像启蒙教材”的格式,因为它的文件结构非常透明。一个典型的24位BMP文件,主要由文件头、信息头、像素数据三部分组成。
文件头叫BITMAPFILEHEADER,固定14字节。前两个字节是“BM”标识,用来告诉解析器这是BMP文件。接着4字节记录整个文件的大小,再往后2字节是保留字段,通常写0,再2字节是像素数据起始偏移量。这段结构看着简单,但任何一个字节写错,都会导致图片无法正确打开。
信息头叫BITMAPINFOHEADER,通常40字节,里面非常关键的有几个字段:图片宽度、图片高度、颜色位数,以及压缩方式。这里有一个很容易踩的坑:BMP的高度字段如果是正数,表示图像数据按“从下往上”逐行存储;如果是负数,则是“从上往下”存储。很多人在手工生成BMP时忘记处理这个符号,结果图片上下颠倒,花了不少时间找原因。
我建议所有想深入图像底层的开发者,都尝试用Python或C语言手工构造一张2x2的24位BMP。这个过程会逼你逐字节思考文件头里每个字段的用途。完成之后,再去看其他格式的文档,理解成本会直线下降。等会第三章我会给出一个可以直接跑通的生成脚本。
2.2 像素排列与BMP行对齐的隐藏规则
BMP像素数据的排列规则,看似简单,但有一个特别容易被忽略的“行对齐”问题。图像每一行的字节数必须是4的倍数,如果不是,需要补零。比如一张宽度为3像素、24位真彩色的图,每行像素数据是3乘以3等于9字节,不是4的倍数,所以这一行实际存储时会补成12字节,后面多出的3字节全是填充。
这个规则是老规范为了内存对齐效率而设计的。现代系统性能早已不是瓶颈,但格式文档仍然沿用了这套逻辑。如果你自己拼接BMP数据时不处理行对齐,生成的图片在Windows画图、浏览器、Photoshop中,可能出现打不开或者画面错位的情况。这在热词里的“bmp生成”相关资料中经常被反复提及。
在我自己踩过的坑里,印象最深的是某次开源工具生成的BMP,在Windows上正常,在Linux的imageio读取时行宽解析不同,导致图像整体斜切。后来加上了针对行对齐的预处理,问题彻底消失。建议所有相关解析代码,都仔细处理这个细节,别觉得像素数据算出来多少就存多少。
2.3 RGB颜色模型与24位真彩色
RGB模型是数字图像里最核心的颜色模型。红、绿、蓝三种颜色按不同强度叠加,形成我们看到的各种颜色。每个通道通常用8位表示,也就是0到255,三个通道组合起来就是24位真彩色,可以表达大约1677万种颜色。
这个颜色数量对人眼来说基本够用了,所以图片处理领域常说的“8位每通道”“真彩色”,本质就是在讨论RGB的存储位数。你在很多图像库中看到的RGB值,比如(255, 0, 0)表示纯红,(0, 255, 0)表示纯绿,(0, 0, 255)表示纯蓝,都是这个模型的具体体现。
RGB之外,还有一个常见的扩展叫RGBA,A是透明通道,用8位表示从完全不透明到完全透明。PNG格式支持RGBA,所以适合做带透明背景的图标;而BMP传统上不支持透明通道,即便硬塞一个通道进去,很多查看器也不认。这也是为什么做UI切图时,透明图标一般选PNG而不是BMP的重要原因。
2.4 JPG和PNG的核心差异与选择逻辑
JPG和PNG是日常开发中最常用的两种格式,但它们的底层思路完全不同。JPG是有损压缩,它的原理是利用人眼对亮度变化比对色彩变化更敏感的特点,先把图像从RGB转换到YCbCr颜色空间,然后对色度通道做降采样,再通过离散余弦变换和量化,把视觉上不敏感的高频细节丢掉。这个过程导致结果是:JPG的压缩率高,但无法保留100%的原始像素。
PNG则是无损压缩,它先用了一套滤波预测算法,对每一行像素进行差分处理,再用Deflate算法进行压缩。因此PNG解压后能还原出和原图完全一致的像素值,适合需要精确还原的场景,比如程序截图、UI图标、带有文字或线条的设计稿。缺点是文件体积通常明显大于同画面的JPG。
选择逻辑总结起来并不复杂:照片、渐变丰富的画面,用JPG;截图、图标、需要透明背景的素材,用PNG。如果追求极致压缩又有无损需要,可以看看WebP或AVIF,但在兼容性和工程稳妥性上,JPG和PNG仍然是绝对主流。
3. 实操过程与核心环节实现
3.1 用Python从零生成一张24位BMP文件
为了验证第一章讲的BMP文件结构,可以先动手写一段代码,手工生成一张4x4的小位图。整个过程不需要任何图像库,只用Python标准库的struct将字节写入文件。
import struct width = 4 height = 4 pixel_data = b"" # 填充一个简单的颜色渐变:左上到右下依次递增 for y in range(height - 1, -1, -1): row = b"" for x in range(width): r = int(x * 255 / (width - 1)) g = int(y * 255 / (height - 1)) b_ = 128 row += struct.pack("BBB", b_, g, r) row += b"\x00" * ((4 - (len(row) % 4)) % 4) pixel_data += row file_size = 54 + len(pixel_data) # BITMAPFILEHEADER: 14字节 bf_type = b"BM" bf_size = struct.pack("I", file_size) bf_reserved1 = struct.pack("H", 0) bf_reserved2 = struct.pack("H", 0) bf_off_bits = struct.pack("I", 54) file_header = bf_type + bf_size + bf_reserved1 + bf_reserved2 + bf_off_bits # BITMAPINFOHEADER: 40字节 bi_size = struct.pack("I", 40) bi_width = struct.pack("i", width) bi_height = struct.pack("i", height) bi_planes = struct.pack("H", 1) bi_bit_count = struct.pack("H", 24) bi_compression = struct.pack("I", 0) bi_size_image = struct.pack("I", len(pixel_data)) bi_x_pels = struct.pack("i", 2835) bi_y_pels = struct.pack("i", 2835) bi_clr_used = struct.pack("I", 0) bi_clr_important = struct.pack("I", 0) info_header = bi_size + bi_width + bi_height + bi_planes + bi_bit_count + bi_compression info_header += bi_size_image + bi_x_pels + bi_y_pels + bi_clr_used + bi_clr_important with open("demo.bmp", "wb") as f: f.write(file_header + info_header + pixel_data)这段代码里有两个细节值得注意。一个是像素写入时,我故意用range(height - 1, -1, -1)从最后一行开始写,原因是BMP默认的存储方向是“左下角到右上角”,如果不做这个反转,生成的图在查看器里会上下颠倒。另一个是每行写完后,都计算并补足了4字节对齐,避免图片损坏。
运行完成后,用任意图片查看器打开demo.bmp,能看到一张横轴红蓝渐变、纵轴绿色渐变的4x4像素小方块。虽然图很小,但手动构造一遍BMP后,我对文件结构的理解比以前翻文档深刻得多。这个方法也特别适合用来排查自己项目里“图片数据没问题但显示异常”的场景,因为你可以绕开所有图像库,直接从文件字节层面定位是哪一段结构写不对。
3.2 三步读取图片任意位置的RGB值
热词里有“python读取图片rgb值”,这在做像素分析、颜色提取、图像相似度比对时非常常用。用Pillow库做这个操作很直接,我一般在脚本里按三步走。
from PIL import Image # 1. 打开图片并确保转为RGB模式 img = Image.open("demo.bmp").convert("RGB") # 2. 获取指定坐标的像素值 x, y = 1, 2 r, g, b = img.getpixel((x, y)) print(f"坐标({x}, {y}) 的RGB值为: ({r}, {g}, {b})") # 3. 如果需要批量统计全图颜色,可以用load()后循环 pixels = img.load() for i in range(img.width): for j in range(img.height): pr, pg, pb = pixels[i, j] pass实际项目中我更喜欢用numpy配合PIL来做像素批量操作,因为getpixel在超大图上逐点调用效率偏低。先把图像转为numpy数组,再用切片统计特定区域的平均RGB值,比逐个函数调用快非常多。
import numpy as np from PIL import Image img = Image.open("photo.jpg").convert("RGB") arr = np.array(img) # 获取中心区域的平均颜色 h, w = arr.shape[:2] center = arr[h//4:h//4*3, w//4:w//4*3] avg_color = center.mean(axis=(0, 1)) print("中心区域平均RGB", avg_color.astype(int))3.3 用RGB转HSV公式处理颜色调整
RGB模型适合显示器,但人眼对颜色的描述逻辑更接近HSV,也就是色相、饱和度、明度。比如你让人描述“那个有点偏青的蓝色”,本质上是在描述HSV里的色相范围;如果直接用RGB数值,很难直观说清楚。
RGB转HSV时,色相H的计算方式和具体哪个通道最大有关,饱和度S取决于最大最小值的差,明度V就是最大值。公式并不复杂,关键在于理解颜色从0到360度的色环分布。红在0度,绿在120度,蓝在240度。这也是为什么HSV常用于颜色识别和颜色筛选。
代码实现也有很多现成库可以调。OpenCV里用cv2.cvtColor(img, cv2.COLOR_BGR2HSV)就能直接转换。但要注意OpenCV的H取值范围是0到179,不是0到360,这是很多人踩过的坑,如果你在OpenCV里把H通道阈值写成180以上,就会导致匹配一直为0。
3.4 嵌入式场景:3路RGB接口转LVDS和PNG显示
热词里有一个专门的方向是“3路RGB接口转LVDS”。这类场景常见于单片机或ARM平台驱动LCD显示屏。主控芯片给屏幕的接口是并行的RGB信号线,而屏幕面板用的是LVDS差分信号,中间就需要一颗转换芯片,比如常见的TDA7210类方案,或者直接在PCB上做信号转换。
这个方向我实际调试过一次,印象最深的是RGB信号和LVDS信号在时序上要严格匹配。转换芯片只做电平转换和串行化,不会帮你重新生成像素时钟。主控端如果配置的HFP、HBP、VFP、VBP这些时序参数和屏幕面板要求不一致,画面会出现左右偏移、上下翻滚或者闪烁。遇到这种问题,优先检查时序配置表,而不是怀疑转换芯片坏了。
至于ESP32S3上显示PNG,则完全是另一条路线。因为ESP32S3本身支持JPEG硬件解码,但PNG一般要靠软件解码库,比如libpng或LVGL内置的PNG解码器。软件解码消耗的RAM比较大,尤其图片尺寸稍大时,会出现解码失败或白屏。经验做法是:如果是全屏背景图,优先用JPEG;如果是小尺寸图标,可以选PNG,但要控制好解码缓冲区的内存分配,必要时用PNG8或调低位深。这里面的取舍和JPG/PNG原理直接相关,跑过一遍就懂为什么嵌入式里要谨慎选格式了。
3.5 base64内嵌图片与data URL前缀
Web前端和微信小程序开发中,经常会看到类似data:image/jpg;base64,/9j/4AAQSkZJRgABAQ...这样的字符串。这其实就是把图片文件编码成base64之后,直接内嵌在HTML或CSS文件里,省去一次网络请求。
data:image/jpg;base64,这段前缀里,data:表示这是一个数据URI,image/jpg是MIME类型,base64表示后面的内容是用base64编码的图片数据。浏览器解析到这种URI时,会把后面的字符串解码成二进制,再当图片渲染。
内嵌base64图片适合比较小的图片,比如登录页的小图标、验证背景图,可以避免额外的HTTP请求,提升加载速度。但如果图片本身有几百KB甚至更大,base64会徒增约33%的体积,而且HTTP响应里很难被缓存,反而拖慢性能。热词里有人问为什么base64字符串有/9j/4AAQSkZJRgABAQ...开头,其实就是因为图片本身是JPEG格式,这个字符串就是JPEG文件的起始特征码,解码后可以看到熟悉的FFD8文件头。理解这一点,至少能知道base64图片的MIME类型和文件头是可以互相印证的。
4. 常见问题与排查技巧实录
4.1 PNG文件打包后报“failed to resolve import”错误
这个热词应该是前端工程里的经典报错:某个Vite或Webpack项目在构建时,提示failed to resolve import ../assets/grenade (1024x128)[frames=8].png。初看以为是文件路径写错了,但排查下来往往是图片资源大小写、后缀名、目录层级和引用时不一致导致的。
我处理过的一个真实案例是:源码目录里文件名是Grenade.png,但代码里写的是grenade.png,Windows下本地开发没问题,因为文件系统不区分大小写;但Linux环境或某些统一构建服务器上,文件名严格区分大小写,于是构建直接失败。解决方式很简单,统一所有资源的命名规范,文件名一律小写加中划线或者下划线,并且严格对比引用路径。
另一种情况是图片存在但引用的相对路径层级不对。建议先把相对路径改成绝对别名路径,比如Vite里配置@指向src,然后写@/assets/grenade.png,降低路径错误的概率。框架报错信息里给出的文件名还带上了(1024x128)[frames=8]这种说明,看起来像是动了雪碧图或者帧动画,实际通常是CSS背景定位或Canvas切帧的引用,核心排查思路还是回到资源路径和大小写上。
4.2 visio导出PNG边距过大怎么办
Visio画完架构图或流程图后,直接导出PNG时经常发现四周留有一圈很大的空白边距,插入文档或网页后整体比例不协调。这个问题好解决,但是很多人不知道。
最简单的办法是先在Visio里把所有图形内容全选,然后按快捷键组合成组,再用“适应绘图”功能,让画布大小自动贴合内容边界。操作路径是:开始 -> 编辑 -> 层 -> 适应绘图。导出时,在文件 -> 另存为的对话框里选择PNG格式后,点击“保存选项”,确认宽度和高度是Adaptive,不要勾选“锁定纵横比”之外的额外边距填充。
如果导出的图片用于Web,还有一个后续小技巧:用Python或在线工具做一次自动裁边,思路是找到图片中非白色像素的最小外接矩形,然后裁剪。Pillow里的Image.getbbox()配合point()就可以实现,准确率很高。我自己的做法是写一个自动化脚本,所有Visio导出的图都统一在文档发布前跑一遍,这样就不会出现“前端页面上图片白边特别大”这种看起来不专业的细节。
4.3 微信dat文件转换为JPG的实际操作思路
微信PC版接收的图片,有些会被缓存成dat格式文件,无法直接打开。这其实是微信对图片数据做了一层简单的异或加密,用手机号或特定序列做密钥,文件内容不是标准JPEG或PNG头。以前网上流行“猜密钥”的做法,现在的思路更简单:dat文件头部字节和标准JPEG头(FFD8FF)异或一下,就能算出密钥。
实操步骤一般是这样:写一个小脚本读取dat文件的前两个字节,然后分别与FF、D8做异或,得到两个相同的密钥值,再用这个密钥遍历整个文件对每个字节做异或,最后把处理后的数据保存为jpg文件。核心代码大概就十几行Python,Python的bytes和列表推导式就能搞定。
需要强调的是,这种做法只适用于处理你自己设备上有权访问的缓存文件,不要拿去做任何越权操作。转换原理本身,却能很好地帮助理解编码格式和文件头的意义。这说明图像文件头不只是给解析器看的,它还承担着识别文件类型、判断文件完整性的作用。
4.4 png转dwg的正确打开方式
建筑、设计中常会遇到这样的需求:把一张PNG位图变成CAD的DWG图纸。但PNG是栅格位图,DWG是矢量图格式,这两者底层完全不同。直接“转换”并不存在无损方式,因为矢量图保存的是几何对象和路径,位图保存的是像素颜色。工程上的做法主要分两种。
第一种是在CAD软件里直接插入PNG作为外部参照或嵌入图片,相当于把图片作为底图放进DWG里,打印时图片会一起输出,但图片本身仍然是位图,在CAD里缩放会变糊。第二种是先对PNG做矢量化,也就是把图像中的线条和轮廓提取成矢量路径,再用DXF格式导入CAD。矢量化可以用Inkscape的Trace Bitmap功能,或者Adobe Illustrator的图像描摹,导出DXF后在AutoCAD里打开并另存为DWG。
如果你只是需要一张带比例的平面草图,第一种方式更稳妥、操作最快。如果需要编辑线条、改变图层,就必须走矢量化流程。在实际操作中,还要注意PNG的分辨率和对比度。分辨率太低、噪声太多的是没办法直接在矢量化软件里跑出好效果的,通常先用锐化和去噪预处理一轮,让边界更干净,最终路径才像样。
4.5 HLS分片链接全是.png扩展名导致播放失败
热词里有一条提到了“HLS索引语法合法,但分片链接全部是.png”。这个问题初看诡异,但其实是服务器端对文件类型的响应处理问题。HLS协议本质上不关心分片文件的实际扩展名,而是靠MIME响应头和分片内容决定的。如果服务器把m3u8里引用的分片文件返回时加了Content-Type: image/png之类的响应头,播放器可能直接拒播。
这种问题最常出现在使用对象存储或静态文件服务器时,后台管理员把分片文件以图片方式上传,扩展名被强制统一成.png。播放器拉取分片数据后,发现按PNG来解析却是二进制视频流,自然无法正常播放。解决方法是把分片文件改回标准.ts扩展名,或者让服务器对分片路径返回正确的video/mp2tMIME类型。
从图像格式角度看,这是一个很好的警示:文件扩展名、MIME类型和文件真实内容,是三个相互独立的概念。PNG扩展名的视频流本质上只是扩展名错误,文件内容还是视频数据。这也提醒我们,在处理图片时,判断格式不要只看后缀名,最好是读取文件头或者MIME标记来确认真实格式。
4.6 RGB图像与深度图像无法对齐的问题
热词里的“no frames received 无法获取深度和rgb”,来自深度相机或者3D摄像头调试场景。这种设备通常有两个通道:一个RGB图像流,一个深度图像流。出现“收不到帧”的情况,我先会去查USB带宽和驱动配置,深度图数据量通常比RGB图大不少,如果USB带宽被占满,摄像头会优先丢弃深度帧。
另一个常见原因是时钟同步问题。部分深度相机要求RGB帧与深度帧在时间戳上对齐;如果上位机用了不同的时间源,或者线程拉取频率不一致,会出现RGB有画面但深度黑屏或花屏的情况。可以试试把两个流的采样策略统一为“同步触发模式”,并检查驱动日志里帧时间戳是否连续。
在图像格式层面,深度图通常被编码为16位单通道灰度图,每个像素是相机到物体的距离值。显示深度图前,往往需要做归一化,也就是把有效距离范围映射到0到255,否则直接显示全黑。这个坑我遇到过好几次,第一次以为相机坏了,查了半天才发现只是显示时没有做灰度映射。
5. 实际操作中的经验与扩展技巧
5.1 选择合适的像素操作库
Python生态里处理图像最多的两个库是Pillow和OpenCV。Pillow更偏向图像基础读写、格式转换、简单滤镜,适合做二次开发工具和自动化脚本。OpenCV更偏向计算视觉,包括特征点提取、目标检测、颜色空间变换,性能上对numpy的衔接也更紧密。
实际项目中我的选择标准很简单:如果需要做批量格式转换、缩略图、加水印,首选Pillow,依赖轻、文档好读、部署简单。如果需要做图像识别、人脸检测、视频流处理,直接用OpenCV。两者可以混合使用,比如用Pillow做好预处理,再交给OpenCV的算法模块,反而比单一库一条路走到底更顺手。
5.2 图片格式转换批处理脚本示例
日常开发里最频繁的重复劳动就是图片格式转换。我写了一个小脚本,可以对整个目录下的图片统一做格式转换和尺寸压缩,核心逻辑非常短,每次接入新项目都能直接复用。
from PIL import Image from pathlib import Path def batch_convert(src_dir, dst_dir, dst_format="PNG", size=None): src_dir = Path(src_dir) dst_dir = Path(dst_dir) dst_dir.mkdir(exist_ok=True) for img_path in src_dir.iterdir(): if img_path.suffix.lower() not in (".jpg", ".jpeg", ".png", ".bmp"): continue img = Image.open(img_path) if size: img = img.resize(size, Image.LANCZOS) dst_path = dst_dir / (img_path.stem + "." + dst_format.lower()) img.save(dst_path, format=dst_format) batch_convert("source", "output", dst_format="PNG", size=(512, 512))这个脚本虽然短,但已经包含了我认为最容易踩坑的两个点:目标文件夹要先创建,否则首次运行直接报错;转换JPEG到PNG时模式转换要显式执行,部分灰度图在转存RGB模式时会出问题。细节虽然小,但在批处理几百张图片时都会变成很浪费时间的故障。
5.3 前端图片压缩的一个实用建议
前端上传图片时,为了减小请求体积和加快上传速度,通常需要在客户端先做压缩。Canvas方案是最通用的,先通过drawImage把图片绘制到指定尺寸的画布上,再调用toBlob或toDataURL导出;但toDataURL默认导出PNG,如果原图是照片,应该显式传入image/jpeg作为MIME类型,否则文件体积会大得离谱。
控制质量时,0.7到0.8的quality参数,在照片场景下视觉差异很小,但文件大小能减少一半以上。如果压缩后的图片还需要用于打印或设计精度要求高的场景,则建议不要压低于0.9,或者干脆转为无损PNG。这个取舍的背后,恰好就是JPG有损压缩和PNG无损压缩的本质区别。理解原理后,前端传图时的“压多少合适”就不再是猜,而是一个有依据的决策。
5.4 选图片格式时先问自己三个问题
做技术选型时,我习惯先问自己三个问题:这张图是否需要透明通道?是否需要对颜色进行像素级还原?体积和画质的优先级哪个更高?
第一问决定是否使用PNG或者WebP,透明通道需求直接排除JPEG。第二问决定是否能用有损压缩,如果图片包含文字、线条、二维码,有损压缩会导致边缘模糊,这时必须选择无损。第三问决定压缩参数怎么定,产品截图、UI素材对画质要求高,优先无损;头像、相册缩略图等等强调体积,可以适当接受画质损失。
这三个问题其实贯穿全文所有格式原理。BMP虽然简单直接,但因为体积巨大几乎不用于网络传输,只在极少数特殊工具流中出现。RGB是颜色模型的基础,理解它才能理解通道、透明度和各式色彩空间的转换。JPG和PNG则是日常应用的两个端点,一个压缩狠狠但会掉细节,一个保真但体积也保真。把这几个点串起来,图像处理的基础必修课就算补上了。
我个人在实际操作中的体会是,很多人面对图像问题第一反应是“换一个库试试”“调高画质试试”,但真正能一针见血解决问题的,恰恰是对文件格式底层逻辑的把握。下次再遇到图片打不开、颜色不对、体积异常,别急着重装库,先从文件头开始看,多半能快速锁定问题。最后再分享一个小技巧:写图片处理工具时,尽量在脚本开头统一做一次“格式归一化”,也就是把输入统一转成RGB模式的Pillow图像或BGR模式的numpy数组,后续所有逻辑都基于这个统一格式展开,会省掉大量分支判断和隐蔽的报错。