最近在整理嵌入式屏幕显示项目的时候,我又把那份积灰的颜色RGB值对应表翻了出来。本来是顺手查一个橙色,结果发现“8位”“16位”这两个词在不同圈子里的含义完全不是一回事。很多朋友拿到一份别人发的“RGB值对应表”,在网页里调得好好的,一搬到单片机、图像处理或者HDR流程里就翻车,颜色不是偏色就是整个变成黑色。这篇文章把我自己整理和验证过的颜色值、换算逻辑和各种踩坑记录一起放出来,从8位通道、16位通道、RGB565一直讲到PWM调灯和Python取色,争取让你看完之后能直接拿表干活。
1. 先分清“8位”和“16位”:颜色值的三种常见含义
1.1 8位通道:网页和工具里默认的RGB
我们日常说的rgb(10, 92, 140)、#FFA500这类写法,默认就是“每通道8位”。R、G、B三个通道各占8个bit,取值范围是0到255,三个通道合起来就是24位真彩色,能表示大约1677万种颜色。这在网页CSS、普通PNG/JPG、Python的PIL库、大多数截屏取色工具里都是同一个标准。
这里要先提醒一个容易搞混的概念:8位通道和8位图像完全是两码事。8位图像通常说的是索引色模式,整张图最多只能有256种颜色,每个像素用一个8位数字去查调色板,典型的就是老式GIF。而8位通道指的是每个红绿蓝通道有256个等级,不是全图只有256色。拿到颜色表的时候,先看清楚你手里的“8位”到底是哪种语境,否则后面所有换算都会跟着错。
1.2 16位通道:图像处理和HDR里的“深色域”
16位通道的意思是R、G、B每个通道占16个bit,取值范围0到65535,三个通道合起来是48位。这种格式常见于16位TIFF、16位PNG、专业相机RAW、HDR合成、医学影像、遥感图像处理。如果你在Photoshop里打开一张16位/通道的图,或者在MATLAB里读一个16位深度图像,就会发现颜色值不再是0到255,而是动辄几万的大数。
16位通道的出现是为了保留更多亮部和暗部细节,避免在多次调整层级的时候出现色阶断裂。但它也带来一个麻烦:网页上查到的颜色值全是0到255,拿到16位环境里不能直接用,得先做线性扩展。至于怎么扩展,我后面专门写一节。需要先说明的是,16位/通道并没有一个全球统一的整数编码方式,有的软件用0到65535,有的软件显示0到32768,还有的用半浮点数。所以不要一看到“16位”就默认上限是65535,要先确认你正在用的工具到底采用哪种映射。
1.3 RGB565:嵌入式、单片机里的另一种“16位”
如果你在网上搜“8位 16位 颜色”搜到了单片机屏幕相关的内容,那这里的“16位”通常指的是RGB565,而不是16位通道。STM32、ESP32、Arduino驱动TFT LCD、OLED屏时候很常用这种模式。RGB565把16位总色深拆成R占5位、G占6位、B占5位,一共正好16位,也就是两个字节。因为人眼对绿色最敏感,所以绿色多分了一位。这种格式能显示2的16次方等于65536种颜色,远不如24位真彩色丰富,但在小屏幕上大部分情况下够用,而且省内存、传输快。
还有一种类似的格式叫ARGB1555,也就是1位透明度加5位红、5位绿、5位蓝,也是16位,常见于一些老的嵌入式UI资源。看到这里你应该明白了,同样是“16位”,可能是每通道65536级的深色域,也可能只是RGB565高五位中六位低五位的紧凑屏显格式。我见过不少人在这一步踩坑,拿着48位源图的像素值直接往RGB565的屏驱里填,结果红色变成黑色、高光全部发紫,就是这个概念没理清。
为了看得更清楚,我把这几种格式整理成一张对比表:
| 术语 | R/G/B 通道位数 | 取值示例 | 总位数 | 常见场景 |
|---|---|---|---|---|
| 8位/通道 RGB888 | 8/8/8 | 0-255 | 24位 | 网页、普通PNG/JPG、CSS |
| 16位/通道 | 16/16/16 | 0-65535 | 48位 | 图像处理、HDR、16位PNG/TIFF |
| RGB565 | 5/6/5 | R:0-31 G:0-63 B:0-31 | 16位 | 单片机、屏幕驱动、嵌入式UI |
| ARGB1555 | 1位透明+5/5/5 | 0-31 | 16位 | 旧嵌入式图标、资源包 |
| 8位索引色 | 非通道概念 | 整图最多256色 | 8位 | 老GIF、低端屏显 |
2. 常用颜色8位RGB对照表与十六进制HEX速查
2.1 基础色和常用色对照表
先放一张最基础的8位通道颜色表,这里面的R、G、B值就是网页里最常见的写法。注意“绿”和“青”不要搞混,绿色是纯绿light,青色是绿加蓝,很多人做LED灯的时候把0,255,0和0,255,255当成同一个颜色,调出来完全不是自己要的效果。
| 颜色 | 英文名 | HEX | 8位RGB |
|---|---|---|---|
| 黑色 | Black | #000000 | 0,0,0 |
| 白色 | White | #FFFFFF | 255,255,255 |
| 红色 | Red | #FF0000 | 255,0,0 |
| 绿色 | Green | #00FF00 | 0,255,0 |
| 蓝色 | Blue | #0000FF | 0,0,255 |
| 黄色 | Yellow | #FFFF00 | 255,255,0 |
| 青色 | Cyan | #00FFFF | 0,255,255 |
| 品红 | Magenta | #FF00FF | 255,0,255 |
| 橙色 | Orange | #FFA500 | 255,165,0 |
| 深红 | Dark Red | #8B0000 | 139,0,0 |
| 金色 | Gold | #FFD700 | 255,215,0 |
| 黄绿 | Yellow Green | #9ACD32 | 154,205,50 |
| 深绿 | Dark Green | #006400 | 0,100,0 |
| 海军蓝 | Navy | #000080 | 0,0,128 |
| 道奇蓝 | Dodger Blue | #1E90FF | 30,144,255 |
| 蓝绿 | Teal | #008080 | 0,128,128 |
| 紫色 | Purple | #800080 | 128,0,128 |
| 粉色 | Pink | #FFC0CB | 255,192,203 |
| 棕色 | Brown | #A52A2A | 165,42,42 |
| 银色 | Silver | #C0C0C0 | 192,192,192 |
| 灰色 | Gray | #808080 | 128,128,128 |
| 暗灰 | Dark Gray | #A9A9A9 | 169,169,169 |
| 亮灰 | Light Gray | #D3D3D3 | 211,211,211 |
| 橄榄 | Olive | #808000 | 128,128,0 |
| 栗色 | Maroon | #800000 | 128,0,0 |
| 珊瑚 | Coral | #FF7F50 | 255,127,80 |
| 砖红 | Firebrick | #B22222 | 178,34,34 |
| 靛蓝 | Indigo | #4B0082 | 75,0,130 |
这张表足够覆盖日常网页配色、PPT取色、LED灯调试等大部分场景。如果你只是做UI设计,看到这里其实可以用了。但注意,电脑屏幕上显示出来的rgb(10, 92, 140),在其他设备上未必是同一个颜色,原因不在颜色表,而在显示设备。
2.2 为什么同一个RGB值在不同设备上看起来不一样
颜色值本身是一个标准化的数字,但最终呈现受三个因素影响:屏幕色域、色彩管理、gamma曲线。入门级显示器通常是sRGB色域,部分高端显示器是P3色域,同样一个RGB值在P3屏幕上会更鲜艳。系统色彩管理如果没做好,浏览器和图片软件之间也会出现色差。gamma就更直接了,RGB数值的256个等级并不是线性亮度,而是经过编码的非线性曲线。如果拿一个完全线性的屏幕去显示普通sRGB图片,中间调会显得很暗。
所以在做跨设备颜色匹配的时候,光靠RGB表是不够的,至少要保证图片带ICC色彩配置文件,设备经过校色,并且整个过程都在同一个色彩空间里。真正确认颜色是否准确,不是肉眼看屏幕,而是用色度计测量。这个提醒放在这一节,是想让你明白:颜色表只是数字参考,不是最终实物保证。
3. 16位颜色值到底怎么换算:通道、RGB565与PWM调光
3.1 8位转16位通道的标准公式
8位通道的0到255,要扩展成16位通道的0到65535,我推荐直接用乘以257的做法。因为65535除以255刚好等于257,这是整数比例里最精确的换算关系。公式就是v16 = v8 * 257,反过来v8 = v16 / 257,取整就行。
用橙色来举例:#FFA500对应的8位RGB是255,165,0。换算后R等于255乘以257,等于65535;G等于165乘以257,等于42405;B还是0。所以在16位通道环境下,应该填65535,42405,0,而不是像很多人那样填255,165,0。直接把255填进16位通道的图里,会让整张图几乎接近黑色,因为255在65535的尺度下只有不到0.4%的亮度。
不过前面提过,有些软件的16位通道不是0到65535,而是0到32768。如果遇到这种环境,就用v16 = round(v8 * 32768 / 255)来换算。比如255会变成32768,128会变成16447左右。这一点完全没有统一标准,建议拿到一种图片格式后,先读取一个已知颜色的像素值看一眼,再决定用哪个公式,这是最稳妥的办法。
3.2 RGB565计算方法和常用值
RGB565的换算就完全不是乘257了,它的每个通道位数不同,所以要做位压缩。最常用的做法是移位截断:R取8位值的高5位,G取高6位,B取高5位。写成代码就是:
uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) { return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3); }这个做法的结果是纯黑0x0000,纯白0xFFFF,纯红0xF800,纯绿0x07E0,纯蓝0x001F,黄色0xFFE0,青色0x07FF,品红0xF81F。橙色255,165,0换算出来是0xFD20,灰色128,128,128换算出来是0x8410。这些值在嵌入式开发里非常常用。
如果需要更精确一点,而不是直接丢弃最低位,可以用四舍五入公式:
uint16_t rgb565_round(uint8_t r, uint8_t g, uint8_t b) { uint16_t r5 = (r * 31 + 127) / 255; uint16_t g6 = (g * 63 + 127) / 255; uint16_t b5 = (b * 31 + 127) / 255; return (r5 << 11) | (g6 << 5) | b5; }两种算出来的结果通常只差最低1到2位,肉眼几乎看不出区别。但在做渐变、调色板这类对精度敏感的场景,建议用四舍五入版本。我实际对比过,用移位版本调试一套渐变灯光,在暗部会有可见的阶梯感,换成四舍五入版本之后平滑了很多。
3.3 PWM驱动RGB灯时的8位和16位换算
单片机驱动RGB LED灯,最常用的就是PWM控制三个颜色通道的占空比。有些平台的analogWrite只有8位精度,也就是0到255;有些定时器可以配置成16位分辨率,也就是0到65535。问题就出在这:你把网页上查到的255,165,0直接塞进16位PWM,就等于只开了不到0.4%的占空比,灯会极其暗,几乎看不出颜色。
正确的做法是先把8位通道值换算到对应PWM精度。8位PWM直接用原值:
// 8位PWM:橙色 analogWrite(redPin, 255); analogWrite(greenPin, 165); analogWrite(bluePin, 0);16位PWM要先做线性扩展:
// 16位PWM:橙色 analogWrite(redPin, 65535); analogWrite(greenPin, 42405); analogWrite(bluePin, 0);注意这只是把占空比数值扩展了,LED实际亮度还要看驱动电路和灯珠特性。如果LED的亮度变化明显不均匀,常见原因不是RGB值不对,而是人眼感知是非线性的,PWM占空比却接近线性。想要视觉上的平滑渐变,一般会做gamma校正,输出前加一步:
uint16_t pwm_value = (uint16_t)(pow(color_8bit / 255.0, 2.2) * 65535.0);这个pow函数在单片机上会占用不少CPU,尤其是ESP8266这类小资源设备,建议预先把256个PWM值算成对照表,运行时候直接查表,既省时间又稳定。
4. 实操:用Python从图片提取RGB并生成双版本颜色表
4.1 读取指定像素的RGB值
现成的颜色表再全,也总有对不上的时候。我通常的做法是直接从图片里取色,然后脚本自动输出8位、16位和RGB565三套结果。Pillow库读普通PNG/JPG很简单。示例代码:
from PIL import Image img = Image.open("test.png").convert("RGB") rgb8 = img.getpixel((120, 80)) print("8位通道 RGB:", rgb8) rgb16 = tuple(v * 257 for v in rgb8) print("16位通道 RGB:", rgb16) def to_rgb565(r, g, b): r5 = (r * 31 + 127) // 255 g6 = (g * 63 + 127) // 255 b5 = (b * 31 + 127) // 255 return (r5 << 11) | (g6 << 5) | b5 print("RGB565: 0x%04X" % to_rgb565(*rgb8))这段代码会直接在终端里输出你要的三套结果。需要注意,Pillow的convert("RGB")会把很多格式强制转成8位通道。如果图片本身是16位PNG,想保留16位数据,就不能直接convert("RGB"),要检查mode,比如是“I;16”还是“RGB;16”,再按对应位深度处理。很多新手在这里踩坑,明明打开的是16位图,读出来的像素值却全是0到255,就是因为没看mode。
4.2 批量生成标准色阶和过渡色
如果你不想手动输入几十个颜色,可以直接用脚本生成一整组过渡色。比如我想生成从红色到黄色的64个渐变色,顺便输出16位和RGB565,可以这样写:
for i in range(64): r = 255 g = int(i * 255 / 63) b = 0 rgb16 = (r * 257, g * 257, b * 257) rgb565 = to_rgb565(r, g, b) print(f"#{r:02X}{g:02X}{b:02X} 8bit=({r},{g},{b}) 16bit={rgb16} 565=0x{rgb565:04X}")生成结果直接就是一条可用的颜色表。做UI换肤、LED呼吸灯、色轮切换这类需求,用这种方法比手敲靠谱得多。生成之后建议拿到实际设备上扫一遍,因为脚本算出来的数值只代表数学上的颜色,不代表最终显示效果。
4.3 常见问题与排查技巧实录
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 图片取的RGB和肉眼看到的不一样 | 图片色彩空间不是sRGB,显示器色温不对 | 统一转成sRGB,查看端做色彩管理 |
| 16位PNG用PIL读出来还是0-255 | 图片被convert("RGB")强制转成8位 | 检查img.mode,按“I;16”或“RGB;16”处理 |
| RGB565转出来颜色发暗 | 把8位直接塞进了565,通道位数不足 | 先移位/四舍五入到5位6位5位,再组合 |
| PWM调16位灯特别暗 | 8位值没扩展就到了65535尺度 | 用v16 = v8 * 257转换 |
| 在PS里16位通道填65535变成白色 | PS的16位/通道不是0-65535线性编码 | 先看软件色阶上限,按对应范围换算 |
我自己实际处理16位TIFF时遇到过最坑的情况:用ImageMagick查到的像素值上限是65535,但导入MATLAB之后上限变成了65335左右,原因是文件里嵌了一个非标准的颜色空间转换。所以遇到跨软件颜色值对不上的时候,别急着怀疑颜色表,先确认两边的色彩空间和位深编码方式。
5. 一张比较全的8位/16位颜色速查表
5.1 完整对照表
这张表是我平时直接放在工程目录里当注释用的。16位通道列按0到65535线性编码,用的是v8乘以257的换算关系;RGB565列按主流单片机的移位截断方法计算。如果你用的16位通道编码是0到32768,那这列的数值需要按比例再换算,不要直接照抄。
| 颜色 | 中文 | HEX | 8位RGB | 16位通道RGB | RGB565 |
|---|---|---|---|---|---|
| 黑色 | 黑 | #000000 | 0,0,0 | 0,0,0 | 0x0000 |
| 白色 | 白 | #FFFFFF | 255,255,255 | 65535,65535,65535 | 0xFFFF |
| 红色 | 红 | #FF0000 | 255,0,0 | 65535,0,0 | 0xF800 |
| 绿色 | 绿 | #00FF00 | 0,255,0 | 0,65535,0 | 0x07E0 |
| 蓝色 | 蓝 | #0000FF | 0,0,255 | 0,0,65535 | 0x001F |
| 黄色 | 黄 | #FFFF00 | 255,255,0 | 65535,65535,0 | 0xFFE0 |
| 青色 | 青 | #00FFFF | 0,255,255 | 0,65535,65535 | 0x07FF |
| 品红 | 品红 | #FF00FF | 255,0,255 | 65535,0,65535 | 0xF81F |
| 橙色 | 橙 | #FFA500 | 255,165,0 | 65535,42405,0 | 0xFD20 |
| 深红 | 深红 | #8B0000 | 139,0,0 | 35723,0,0 | 0x8800 |
| 金色 | 金 | #FFD700 | 255,215,0 | 65535,55255,0 | 0xFEA0 |
| 黄绿 | 黄绿 | #9ACD32 | 154,205,50 | 39578,52685,12850 | 0x9E66 |
| 深绿 | 深绿 | #006400 | 0,100,0 | 0,25700,0 | 0x0320 |
| 海军蓝 | 藏蓝 | #000080 | 0,0,128 | 0,0,32896 | 0x0010 |
| 道奇蓝 | 亮蓝 | #1E90FF | 30,144,255 | 7710,37008,65535 | 0x1C9F |
| 蓝绿 | 蓝绿 | #008080 | 0,128,128 | 0,32896,32896 | 0x0410 |
| 紫色 | 紫 | #800080 | 128,0,128 | 32896,0,32896 | 0x8010 |
| 粉色 | 粉 | #FFC0CB | 255,192,203 | 65535,49344,52171 | 0xFE19 |
| 棕色 | 棕 | #A52A2A | 165,42,42 | 42405,10794,10794 | 0xA145 |
| 银色 | 银 | #C0C0C0 | 192,192,192 | 49344,49344,49344 | 0xC618 |
| 灰色 | 灰 | #808080 | 128,128,128 | 32896,32896,32896 | 0x8410 |
| 暗灰 | 暗灰 | #A9A9A9 | 169,169,169 | 43433,43433,43433 | 0xAD55 |
| 亮灰 | 亮灰 | #D3D3D3 | 211,211,211 | 54227,54227,54227 | 0xD69A |
| 橄榄 | 橄榄 | #808000 | 128,128,0 | 32896,32896,0 | 0x8400 |
| 栗色 | 栗色 | #800000 | 128,0,0 | 32896,0,0 | 0x8000 |
| 珊瑚 | 珊瑚 | #FF7F50 | 255,127,80 | 65535,32639,20560 | 0xFBEA |
| 砖红 | 砖红 | #B22222 | 178,34,34 | 45746,8738,8738 | 0xB104 |
| 靛蓝 | 靛蓝 | #4B0082 | 75,0,130 | 19275,0,33410 | 0x4810 |
5.2 表格使用说明与验证建议
这张表在纯色、基础色的场景下可以直接用,但有一个前提必须提醒:16位通道列采用的是0到65535整数线性编码,也是Python、ImageMagick、很多科学计算库默认的方式。如果你的目标软件是Photoshop这种把16位/通道显示成0到32768的,那就不能直接照抄,需要再除以2左右。RGB565列用的是移位截断方式,和四舍五入方式在个别颜色上可能差1,这点差值在屏幕上基本看不出,但如果你在做逐位比较的测试用例,就要用同一套算法。
我自己拿到一张新颜色表之后,一般会先做三个验证:第一,用Python读取一个已知颜色的像素值,确认软件通道范围到底是0到255还是0到65535;第二,生成一张纯色测试图,在目标屏幕上肉眼确认颜色方向对不对;第三,用RGB565画一条从黑到白的渐变,看看有没有明显色阶断层。这三步走完,那张表基本就可以放心用了。
最后再分享一个小技巧,做嵌入式UI的时候,不要每次临时查RGB565,直接把你常用的十几个颜色定义成宏或常量数组,像纯色、半透明遮罩色、警告色、成功色放一起。我就是这么干的,后来换屏幕、换项目的时候直接复制头文件,省了非常多时间。颜色的坑永远存在于数字到显示的那一段路程里,手里有表、心里有换算、眼睛盯着设备,比背多少颜色值都管用。