调过屏、点过灯、处理过图片的人,电脑里几乎都存着一张颜色对照表。我自己最早那张表是从旧论坛上复制下来的,表格里只有颜色名和十六进制码,后来做单片机LCD驱动、给WS2812灯条写特效、用Python批量抽帧取色,才发现光有十六进制码根本不够——很多场景要8位RGB三通道值,很多屏要16位RGB565打包值,还有16位每通道的PNG图,给的值又变成0到65535。这三个东西搞混,轻则显示偏色,重则整屏发黑。这篇文章就把RGB值在8位、16位这些常见形态下的对应关系整理全,附上可以直接抄的速查表、手动换算方法,还有我踩过的几个坑。
1. 8位和16位这两个词,经常指的不是同一件事
很多初学者看到“8位颜色、16位颜色”就直接懵了,因为这两个词在不同场景下含义完全不同。我建议先把概念拆开,不然后面查表都会查错。
1.1 通道位宽和整像素位宽是两套体系
第一套体系说的是“每个颜色通道用几位来表示”。RGB三个通道各占8位,就是最常见的8位每通道,每个通道的取值范围是0到255,三个通道组合起来叫24位真彩色,能表示1677万多种颜色。网页里的#FF00FF、Photoshop里RGB滑块的0到255、Python读出来的像素值,全都是这套体系。
第二套体系说的是“一个像素总共占几位”。比如RGB565,一个像素一共16位,红色占5位、绿色占6位、蓝色占5位,这种格式在单片机屏幕、FPGA、嵌入式GUI里极其常见。它也叫16位色,和“每通道16位”完全是两码事。
第三套体系是“每通道16位”,也就是R、G、B每个通道都占16位,取值范围0到65535。这种格式在PNG图片、RAW照片、HDR图像、医学影像里会遇到,用来保留更高精度的颜色过渡。
所以你搜“RGB值 8位 16位”的时候,得先搞清楚自己到底要哪种:驱动屏幕要用RGB565,处理图像文件要分清楚是8位每通道还是16位每通道,调LED灯条又是另外一套位宽逻辑。
1.2 一个像素到底占几个字节
我用一张表把这几种常见格式占用的空间列一下,这样你对“位宽”会有更直观的感受。
| 格式 | 每像素位数 | 每像素字节数 | 通道取值 | 典型场景 |
|---|---|---|---|---|
| 8位索引色 | 8 bit | 1 字节 | 0-255(查调色板) | GIF、老游戏、终端 |
| RGB565 | 16 bit | 2 字节 | R=0-31, G=0-63, B=0-31 | MCU屏幕、嵌入式GUI |
| RGB888 | 24 bit | 3 字节 | 每通道0-255 | 网页、普通PNG/JPG |
| RGBA8888 | 32 bit | 4 字节 | 每通道0-255 | 游戏贴图、UI |
| RGB48 | 48 bit | 6 字节 | 每通道0-65535 | 16位PNG、RAW |
1.3 为什么RGB565里绿色多给了一位
人眼对绿色最敏感,对蓝色最不敏感。RGB565把绿色分到6位,能在只有65536种颜色的限制下,最大程度减少人眼能感知的色阶断层。这是从CRT时代就定下来的老规矩,一直沿用到现在。记住这个“绿多一位”的规律,后面手动换算时就不容易忘。
2. 高频颜色速查表:十六进制、8位RGB、RGB565三列对照
下面这些颜色是我在项目和日常处理图片时最常用到的,按色系整理。中间一列的8位RGB值给Python、C语言、网页用,最后一列的RGB565值可以直接写进MCU屏幕驱动或图形库的缓冲区。
2.1 灰度、黑白和红色系
| 颜色名 | Web十六进制 | 8位RGB(0-255) | RGB565(0x????) |
|---|---|---|---|
| 黑 Black | #000000 | 0, 0, 0 | 0x0000 |
| 白 White | #FFFFFF | 255, 255, 255 | 0xFFFF |
| 银 Silver | #C0C0C0 | 192, 192, 192 | 0xC618 |
| 灰 Gray | #808080 | 128, 128, 128 | 0x8410 |
| 暗灰 DimGray | #404040 | 64, 64, 64 | 0x4208 |
| 红 Red | #FF0000 | 255, 0, 0 | 0xF800 |
| 深红 DarkRed | #8B0000 | 139, 0, 0 | 0x8800 |
| 栗色 Maroon | #800000 | 128, 0, 0 | 0x8000 |
| 粉红 Pink | #FFC0CB | 255, 192, 203 | 0xFE19 |
| 珊瑚 Coral | #FF7F50 | 255, 127, 80 | 0xFBEA |
| 番茄红 Tomato | #FF6347 | 255, 99, 71 | 0xFB08 |
| 火砖红 FireBrick | #B22222 | 178, 34, 34 | 0xB104 |
| 棕色 Brown | #A52A2A | 165, 42, 42 | 0xA145 |
| 橙色 Orange | #FFA500 | 255, 165, 0 | 0xFD20 |
| 橙红 OrangeRed | #FF4500 | 255, 69, 0 | 0xFA20 |
| 金色 Gold | #FFD700 | 255, 215, 0 | 0xFEA0 |
2.2 黄绿、蓝紫和常用特殊色
| 颜色名 | Web十六进制 | 8位RGB(0-255) | RGB565(0x????) |
|---|---|---|---|
| 黄 Yellow | #FFFF00 | 255, 255, 0 | 0xFFE0 |
| 黄绿 YellowGreen | #9ACD32 | 154, 205, 50 | 0x9E66 |
| 橄榄 Olive | #808000 | 128, 128, 0 | 0x8400 |
| 酸橙 Lime | #00FF00 | 0, 255, 0 | 0x07E0 |
| 绿 Green | #008000 | 0, 128, 0 | 0x0400 |
| 海绿 SeaGreen | #2E8B57 | 46, 139, 87 | 0x2C4A |
| 浅绿 LightGreen | #90EE90 | 144, 238, 144 | 0x9772 |
| 春绿 SpringGreen | #00FF7F | 0, 255, 127 | 0x07EF |
| 蓝绿 Teal | #008080 | 0, 128, 128 | 0x0410 |
| 青 Cyan | #00FFFF | 0, 255, 255 | 0x07FF |
| 淡青 LightCyan | #E0FFFF | 224, 255, 255 | 0xE7FF |
| 军蓝 CadetBlue | #5F9EA0 | 95, 158, 160 | 0x5CF4 |
| 蓝 Blue | #0000FF | 0, 0, 255 | 0x001F |
| 深蓝 DarkBlue | #00008B | 0, 0, 139 | 0x0011 |
| 海军蓝 Navy | #000080 | 0, 0, 128 | 0x0010 |
| 道奇蓝 DodgerBlue | #1E90FF | 30, 144, 255 | 0x1C9F |
| 皇家蓝 RoyalBlue | #4169E1 | 65, 105, 225 | 0x435C |
| 天蓝 SkyBlue | #87CEEB | 135, 206, 235 | 0x867D |
| 钢蓝 SteelBlue | #4682B4 | 70, 130, 180 | 0x4416 |
| 靛蓝 Indigo | #4B0082 | 75, 0, 130 | 0x4810 |
| 紫 Purple | #800080 | 128, 0, 128 | 0x8010 |
| 深紫 DarkViolet | #9400D3 | 148, 0, 211 | 0x901A |
| 蓝紫 BlueViolet | #8A2BE2 | 138, 43, 226 | 0x895C |
| 洋红 Magenta | #FF00FF | 255, 0, 255 | 0xF81F |
| 兰花紫 Orchid | #DA70D6 | 218, 112, 214 | 0xDB9A |
| 薰衣草 Lavender | #E6E6FA | 230, 230, 250 | 0xE73F |
2.3 另一种16位:每通道0到65535怎么换算
如果你拿到的是16位每通道的PNG或RAW图,你需要的就不是RGB565,而是把0到255的8位值等比扩展到0到65535。换算公式很简单:
16位值 = 8位值 × 257,等价于
(8位值 << 8) | 8位值
为什么是257?因为255对应65535,65535除以255正好等于257。用位运算理解更直接:把一个8位值同时放在高8位和低8位,就完成了线性扩展。
举三个例子:
#FF0000的红色通道:255 × 257 = 65535,绿色和蓝色都是0,得到(65535, 0, 0)#FFA500的绿色通道:165 × 257 = 42405,橙色变成(65535, 42405, 0)#404040的灰色:64 × 257 = 16448,三个通道都是16448
3. 256色调色板:8位索引色到底按什么规律排的
除了R、G、B各8位的真彩色,还有一种“8位颜色”指的是8位索引色,整张图只有256个颜色编号,每个编号对应调色板里一个具体的RGB值。GIF格式、老式游戏机、部分终端界面都用它。既然标题写了“比较全”,这块也得说清楚。
3.1 216安全色:6×6×6的色彩立方体
Web安全色本质上就是一个256色调色板的子集,也叫216安全色。它的排列规律非常整齐:RGB三个通道都只取6个固定值,分别是0、51、102、153、204、255,也就是十六进制的00、33、66、99、CC、FF。
三个通道各有6个选择,组合起来就是6×6×6=216种颜色。取任意一个颜色,比如#CC6600,其实就是R取第4档204,G取第2档102,B取第0档0。
如果你需要这个色板的完整对照,不需要去背,按下面的编号公式自己就能生成:
颜色编号 = (R档位 × 36) + (G档位 × 6) + (B档位)
其中档位从0开始数。#CC6600就是 4×36 + 2×6 + 0 = 156号色。
3.2 系统保留的40色和经典16色
标准256色调色板里,除了216安全色,还有40个系统保留色,主要是给操作系统界面用的渐变灰和其他固定色。底层硬件和驱动不同,这40个颜色在不同设备上可能有差异,所以实际开发时我不会依赖这40个。
真正值得记的是经典16色,也就是VGA彩色文本模式那套,在终端、飞控OSD、老式图形界面里经常碰到:
| 编号 | 颜色 | 编号 | 颜色 |
|---|---|---|---|
| 0 | 黑 | 8 | 亮黑/灰 |
| 1 | 蓝 | 9 | 亮蓝 |
| 2 | 绿 | 10 | 亮绿 |
| 3 | 青 | 11 | 亮青 |
| 4 | 红 | 12 | 亮红 |
| 5 | 品红 | 13 | 亮品红 |
| 6 | 棕 | 14 | 黄 |
| 7 | 白 | 15 | 亮白 |
注意6号是棕,14号才是黄,这个很多人会记错。亮色通常是普通色的加亮版本,但棕色加亮后并不是橙色,而是黄。
3.3 索引色的实际坑
我在给一个老式点阵屏写显示驱动时,直接用了标准256色调色板去索引,结果显示出来的红色明显发紫。查了半天发现那款屏幕控制器的内置调色板RGB顺序是BGR,编号完全错位。后来我先往调色板寄存器写一个纯红测试色,再根据实际显示结果反向校准,才把整张色板纠正过来。所以遇到8位索引色,先确认RGB顺序,再确认调色板是否可编程,最后才谈查表。
4. RGB565手动换算:位操作一步步拆给你看
有人拿到速查表很开心,但换一个不在表里的颜色又不会算了。RGB565的换算其实就三步移位加拼接,我拆开讲一遍,你以后自己就能算。
4.1 换算公式
现有RGB888三个8位值,记为R8、G8、B8:
R5 = R8 >> 3 // 取R8的高5位 G6 = G8 >> 2 // 取G8的高6位 B5 = B8 >> 3 // 取B8的高5位 RGB565 = (R5 << 11) | (G6 << 5) | B5原理很简单:把低几位直接丢弃,只保留高位,然后按“红占最高5位、绿占中间6位、蓝占最低5位”排列。为什么是右移3、2、3?因为8减5等于3,8减6等于2。
4.2 手算例子:橙色 #FFA500
- R8 = 0xFF = 255,右移3位得31,二进制11111
- G8 = 0xA5 = 165,右移2位得41,二进制101001
- B8 = 0x00 = 0,右移3位得0
按位拼起来:
R5(5位) G6(6位) B5(5位) 11111 101001 00000转成十六进制就是0xFD20。我在速查表里写的橙色RGB565就是0xFD20,和手算结果一致。可以把它拆回二进制验证:0xFD20对应二进制1111 1101 0010 0000,前5位11111是31,中间6位101001是41,最后5位00000是0。
4.3 反向换算:从RGB565回到RGB888
屏幕显示没问题后,有时候需要把读回来的RGB565还原成近似RGB888,比如做截图、做颜色识别。反推公式:
R8 = (R5 << 3) | (R5 >> 2) G8 = (G6 << 2) | (G6 >> 4) B8 = (B5 << 3) | (B5 >> 2)低几位用“高位复制”的方式补齐,比简单地补0更接近原色。比如RGB565的0xFD20,R5=31,31<<3=248,31>>2=7,248+7=255,正好还原成255;G6=41,41<<2=164,41>>4=2,164+2=166,原值是165,差1个色阶,肉眼基本看不出。
4.4 用Python批量生成任意颜色的RGB565对照
遇到“比较全”的需求,一张写死的表永远不够用。我写了个小脚本,想生成多少颜色都行:
def rgb888_to_rgb565(r, g, b): return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) colors = { "红 Red": (255, 0, 0), "黄 Yellow": (255, 255, 0), "橙 Orange": (255, 165, 0), "青 Cyan": (0, 255, 255), "品红 Magenta": (255, 0, 255), "天蓝 SkyBlue": (135, 206, 235), "灰 Gray": (128, 128, 128), } for name, (r, g, b) in colors.items(): val = rgb888_to_rgb565(r, g, b) print(f"{name:<12s} #{r:02X}{g:02X}{b:02X} 0x{val:04X}")输出结果:
红 Red #FF0000 0xF800 黄 Yellow #FFFF00 0xFFE0 橙 Orange #FFA500 0xFD20 青 Cyan #00FFFF 0x07FF 品红 Magenta #FF00FF 0xF81F 天蓝 SkyBlue #87CEEB 0x867D 灰 Gray #808080 0x84104.5 手动换算时最容易踩的坑
第一个坑是移位方向。有人会把(r >> 3)写成(r << 3),结果颜色乱成一团。第二个坑是忘了按位或,而是用加号拼,(31 << 11) + (41 << 5) + 0虽然在这类场景通常结果一样,但一旦位重叠就会出错,所以统一用按位或更安全。第三个坑是字节序。同一个0xFD20,在小端设备的内存里是0x20 0xFD,如果你直接按字节数组发送给屏幕,可能红蓝对调。ILI9341、ST7789这些屏普遍要求高字节在前,点亮前先发纯红色0xF800测试,看到纯红了再继续,能省很多排查时间。
5. Python读取图片RGB值:8位和16位位深的实际差异
热搜词里有一条“python读取图片rgb值”,这个太常见了。很多人在这一步遇到颜色不对、图片偏暗的问题,十有八九是没搞清图片的位深。
5.1 用Pillow读普通8位图
最常见的做法是Pillow,读出来每个像素是三通道0到255的整数:
from PIL import Image im = Image.open("demo.png").convert("RGB") r, g, b = im.getpixel((0, 0)) print(r, g, b) # 输出类似 255 128 0这种像素值可以直接拿去和速查表里的8位RGB列对照。
5.2 读16位PNG时要注意数据范围
Pillow对16位灰度图支持I;16模式,但对16位彩色PNG支持不太友好,经常会自动降成8位。所以我处理16位彩色图时改用imageio或OpenCV:
import cv2 # 必须加 IMREAD_UNCHANGED,否则OpenCV会按8位读取 img = cv2.imread("demo_16bit.png", cv2.IMREAD_UNCHANGED) print(img.dtype, img.shape) # dtype 是 uint16,shape 是 (高, 宽, 3) # 注意OpenCV默认通道顺序是BGR不是RGB b, g, r = img[0, 0] print(r, g, b) # 范围0到65535如果图是16位每通道,显示的数值会是0到65535,比如#FF0000纯红在这个图上取出来是65535, 0, 0。而你拿速查表查到的8位红色是255,两者差257倍。这就是需要上一节那个乘257公式的地方。
5.3 实际排查案例:图片整体偏暗或者偏色
有次我处理一批航拍素材,用cv2.imread()默认参数读图,所有图片颜色都发暗,天空部分甚至接近黑色。我打印像素值一看,最大才255,再去文件属性一查,原来是16位PNG。OpenCV默认参数会直接把16位数据的高8位丢掉,只保留低8位,导致数值被严重压缩。改用cv2.IMREAD_UNCHANGED后数据正常,但显示时又要除257转回8位:
img = cv2.imread("demo_16bit.png", cv2.IMREAD_UNCHANGED) img_8bit = (img >> 8).astype("uint8") # 取高8位也行 # 或者 img_8bit = (img / 257).astype("uint8")偏色则是通道顺序问题。OpenCV默认BGR,Pillow默认RGB,如果混用后没转换,红和蓝就会互换。我一般固定一套流程:读图、转RGB、归一化到0.0到1.0之间,再去做后续计算,这样无论8位还是16位图,代码逻辑都统一。
5.4 编写通用读取函数的经验
我现在写项目时会放一个通用的取色函数,避免每次重复踩位深和顺序的坑:
import cv2 import numpy as np def read_rgb_unified(path): img = cv2.imread(path, cv2.IMREAD_UNCHANGED) if img.dtype == np.uint16: img = (img >> 8).astype(np.uint8) # 转到RGB顺序,BGR是OpenCV默认 img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) return img_rgb这样不管输入是8位还是16位图,函数输出的永远是0到255的RGB顺序数组,后续逻辑简单很多。
6. 嵌入式场景:MCU点屏、LED灯条、FPV飞控里怎么用这张表
最后聊点实战里的具体用法。这块最容易出问题的不是不知道颜色数值,而是不知道设备要求什么顺序、什么字节序。
6.1 单片机LCD驱动:直接填RGB565
用ILI9341、ST7789这类控制器驱动TFT彩屏时,画点函数底层大多需要16位RGB565值。比如你要画一个橙色,直接把速查表里的0xFD20写进显存或SPI发送缓冲区就行。如果驱动库里用的是两个字节分开发送,高字节是0xFD,低字节是0x20,顺序错了颜色就会偏成完全不同的东西。经验:写驱动前先画满屏纯红0xF800,再画纯绿0x07E0,最后纯蓝0x001F,三原色对了,剩下的颜色基本都能对上。
6.2 WS2812灯条:颜色顺序是GRB,不是RGB
WS2812这类智能灯珠用的是“每通道8位”的GRB数据格式,发送24位数据时,高位先发的是绿色,接着是红色,最后是蓝色。很多人第一次点亮灯条,发现想亮绿色结果亮红色,就是因为直接把RGB888值按R、G、B顺序发出去了。
正确的拼法:
def ws2812_color(r, g, b): # WS2812要求GRB顺序,24bit从高位到低位依次是G、R、B return ((g << 16) | (r << 8) | b) & 0xFFFFFF所以速查表里那列8位RGB值在这里要重新排列。RGB565那列也不能直接用,WS2812不认RGB565。
6.3 FPV飞控场景:颜色配置和地面站的显示不一致
FPV圈子里总会出现“电调分8位和32位”这种说法,注意这里的8位指的是电调主控MCU的位宽,和我们说的颜色位宽是两回事。飞控上的LED灯条、OSD颜色配置,本质还是GRB灯珠和调色板索引那套逻辑。我遇到过飞控调参软件里选好颜色,预览显示正常,机架上的灯条亮出来却红蓝互换的情况,原因就是飞控底层发送灯条数据时用了BGR顺序,调参软件却按RGB解析。排查方法很笨但有效:先给灯条单独写一个全红测试程序,如果亮出来是绿,就说明颜色映射反了,手动把驱动里的R和B对调即可。
6.4 建议维护一张自己的速查表脚本
我现在维护着一张“颜色源表”,里面不只有RGB888和RGB565,还把规格化成0.0到1.0的浮点值、Unity/Unreal里的颜色向量、CSS变量都写在同一个JSON文件里。每次遇到新项目,直接跑一段脚本生成对应的头文件或主题文件。这样做的好处是团队同事不用再去翻网页查颜色,也避免每个人手里的表数值对不上。你完全可以按同样思路,拿本文第4.4小节的脚本做底子,把项目里用到的颜色都加进去,生成一份属于你自己的“比较全”的颜色对照表。
实际上,查表这件事本身不难,难的是搞清楚你手上设备要的是哪种“8位、16位”。先把格式认准,再套数值,基本就不会翻车。