资讯详情

资讯详情

8位与16位RGB颜色转换全解析:RGB565对照表与实战指南

很多人手里都有一张RGB颜色对照表但真到用的时候经常发现不够用。表上写着#FF0000是红色、255,0,0是红色可你要在单片机上调一个RGB LED灯珠屏是RGB565格式颜色寄存器的值到底填0xF800还是63488你要在Python里读一张图片的像素值OpenCV读出来的顺序为什么是BGR而不是RGB你要在MATLAB里把一张图从RGB转到Lab再转回来偏色偏到怀疑人生。这篇文章就是把这些破事一次说清楚。我会把8位、16位、十六进制、RGB565、灰度值、PWM占空比、Python读取像素这些常见场景全部串联起来附上一份我自己整理并实测过的颜色对应表再讲清楚互相转换的计算逻辑和实战中的坑。做嵌入式、写上位机、调LED灯、做图像处理的朋友都可以直接抄作业。1. RGB到底是什么8位和16位差别在哪1.1 人眼、屏幕和RGB三通道的基本逻辑屏幕上的所有颜色本质上都是红、绿、蓝三种光的叠加。你手机屏幕上那个小小的像素背后就是三个子像素分别发出红、绿、蓝光亮度不同叠加出来的颜色就不同。这和美术里红黄蓝三原色不是一回事RGB是发光混合红加绿等于黄红加蓝等于品红绿加蓝等于青三个全亮最高亮度就是白色三个全灭就是黑色。这个模型几乎所有电子显示设备都在用但“亮度不同”这四个字落到具体硬件里就有讲究了。一个通道的亮度可以用不同的位数来表示。位数越多亮度分得越细颜色过渡越平滑但占用的存储空间和传输带宽也越大。这就是标题里“8位”和“16位”的由来。1.2 8位RGB的完整含义8位RGB通常指每个颜色通道用8位二进制数表示也就是一个通道有 (2^8 256) 个亮度等级取值范围是十进制的0到255十六进制的0x00到0xFF。三个通道组合起来总共能表示 (256 \times 256 \times 256 16,777,216) 种颜色也就是约1677万色大家常说的“1600万色”就是它。这个体系下一个颜色通常写成三种形式十进制RGB(255, 0, 0)十六进制字符串#FF0000十六进制数值0xFF0000在HTML、CSS、JavaScript里最常用的是#RRGGBB这种字符串格式。在C语言、嵌入式开发里更常用的是把三个字节拼成一个32位整数比如0x00FF0000前面那两个0是高字节实际只用到低24位。1.3 16位RGB最常见的形态RGB56516位RGB情况稍微复杂一点它并不是“每个通道都用16位”那样子需要48位显然不是这里讨论的东西。实际说的16位RGB是把总共16位分配给三个通道最常见的分配方式是RGB565红色占5位、绿色占6位、蓝色占5位。为什么绿色多占一位因为人眼对绿色最敏感多给一位能让整体观感更细腻。6800K到7000K色温的屏幕绿色通道本来在亮度感知里权重就最大这个分配方式是历史沉淀下来的最优解。RGB565共能表示 (2^5 \times 2^6 \times 2^5 65,536) 种颜色也就是65536色。相比8位体系的1677万色确实少了很多但它有一个巨大优势一个颜色只需要两个字节存到数组里、写到LCD显存里、通过SPI传给屏幕、塞进一个16位的变量里都非常方便。很多低成本屏幕比如常见的ST7735、ILI9341驱动的小尺寸TFT屏内部就是RGB565格式。RGB565颜色的十六进制写法有点特殊。一个16位二进制数按“RRRRRGGGGGGBBBBB”排列写成十六进制比如红色0xF800、绿色0x07E0、蓝色0x001F。我第一次拿到这个值的时候也懵了一下0xF800怎么就是红色了拆成二进制就很清楚了0xF800 1111 1000 0000 0000按RGB565切分R1111131G0000000B000000R通道满值31G和B都是0那就是红色没错。1.4 8位和16位对应的本质是精度换算理解了上面两套体系你就知道“8位”和“16位”之间根本不是两套颜色标准而是同一批颜色在不同存储精度下的两种编码方式。8位的255对应16位里R和B通道的31、G通道的63。把一个8位颜色转成16位本质上就是做一次带截断的除法再把结果填进对应位段里。后续所有表格和转换都围绕这个逻辑展开。2. 一份能直接抄的8位/16位颜色对照表2.1 常用颜色速查下面这份表格把我日常用的频率最高的颜色都整理出来了。左边是8位十进制和十六进制中间是屏和灯常用的RGB565十六进制值写成C语言里可以直接用的形式右边是RGB565的十进制整数值方便你在代码里直接赋值。颜色名称8位RGB十进制8位十六进制RGB565十六进制RGB565十进制黑色(0, 0, 0)#0000000x00000白色(255, 255, 255)#FFFFFF0xFFFF65535红色(255, 0, 0)#FF00000xF80063488绿色(0, 255, 0)#00FF000x07E02016蓝色(0, 0, 255)#0000FF0x001F31黄色(255, 255, 0)#FFFF000xFFE065504青色(0, 255, 255)#00FFFF0x07FF2047品红(255, 0, 255)#FF00FF0xF81F63519灰色(128, 128, 128)#8080800x841033808深灰(64, 64, 64)#4040400x420816904浅灰(192, 192, 192)#C0C0C00xC61850712橙色(255, 165, 0)#FFA5000xFD2064800粉色(255, 192, 203)#FFC0CB0xFC1864536棕色(139, 69, 19)#8B45130x8A2035360紫色(128, 0, 128)#8000800x801032784深蓝(0, 0, 128)#0000800x001016深绿(0, 128, 0)#0080000x04001024橄榄(128, 128, 0)#8080000x840033792银色(192, 192, 192)#C0C0C00xC61850712藏青(0, 0, 139)#00008B0x001117海军蓝(25, 25, 112)#1919700x190E6414深青色(0, 139, 139)#008B8B0x04511105金黄(255, 215, 0)#FFD7000xFEC065216珊瑚色(255, 127, 80)#FF7F500xFD2A64810番茄红(255, 99, 71)#FF63470xFC4D64589橙红(255, 69, 0)#FF45000xFBE064480天蓝(135, 206, 235)#87CEEB0x87FF34815浅蓝(173, 216, 230)#ADD8E60xADF544533蓝绿(64, 224, 208)#40E0D00x47EF18415黄绿(154, 205, 50)#9ACD320x9B6639782表中的十进制值我直接是按RGB565的位权算出来的比如红色0xF800拆成二进制1111 1000 0000 0000就是 (15 \times 4096 8 \times 256 63488)。你在C语言里直接写uint16_t color 63488;和写uint16_t color 0xF800;效果一样。2.2 按色系归类的补充色表上面的表是高频色但实际项目里永远会遇到表上没有的颜色。我平时习惯再备一张按色系归类的补充表覆盖面更广。色系具体颜色8位RGB十进制RGB565红系淡红(255, 182, 193)0xFD2D红系印度红(205, 92, 92)0xCA54橙系深橙(255, 140, 0)0xFD00橙系沙棕色(244, 164, 96)0xF4D8黄系淡黄(255, 255, 224)0xFFF7黄系卡其色(240, 230, 140)0xF3D8绿系春绿(0, 255, 127)0x07EA绿系海洋绿(46, 139, 87)0x2D86青系浅海绿(32, 178, 170)0x26EC青系深石板蓝(72, 61, 139)0x49CE蓝系道奇蓝(30, 144, 255)0x1CFF蓝系午夜蓝(25, 25, 112)0x190E紫系兰花紫(218, 112, 214)0xDAF7紫系紫罗兰(238, 130, 238)0xEE9F粉系浅粉(255, 228, 225)0xFFDE粉系热粉(255, 105, 180)0xFD76这里的十六进制值是按标准移位算法算出来的也就是 ( ((R 3) 11) | ((G 2) 5) | (B 3) )。有些颜色可能会有1到2个bit的舍入差异因为不同转换算法在移位和四舍五入上处理略有不同实际显示肉眼看不出区别不用纠结。2.3 一个热搜里出现的特殊颜色示例热搜词里有rgb(10, 92, 140)这个值我觉得拿它当个例子特别合适。这个颜色看起来是一种偏深的蓝青色类似深空蓝。8位十进制RGB(10, 92, 140)8位十六进制#0A5C8CRGB565换算过程R通道(10 3 1)也就是二进制的00001G通道(92 2 23)也就是二进制的010111B通道(140 3 17)也就是二进制的10001拼起来(1 \times 2^{11} 23 \times 2^5 17 2048 736 17 2801)写成十六进制0x0AF1你说这个值靠背能背下来吗不现实。所以掌握换算方法比背表重要得多。这也是我后面专门写一章讲转换逻辑的原因。3. 8位与16位互转计算逻辑和代码实现3.1 从RGB888转到RGB565移位法把一个8位颜色转成RGB565最简单且符合硬件直觉的做法是直接截断低位。因为8位的R通道要压缩到5位直接丢掉低3位G通道要压到6位丢掉低2位B通道丢掉低3位。用C语言写就是uint16_t rgb888_to_565(uint8_t r, uint8_t g, uint8_t b) { return ((r 3) 11) | ((g 2) 5) | (b 3); }拆开看就更清楚了r 3把R的8位值右移3位得到5位有效值范围0到31 11把这5位放到16位数的最高5位g 2G的8位值右移2位得到6位有效值范围0到63 5放在中间6位b 3B的8位值右移3位得到5位有效值|把三段拼在一起举个例子颜色RGB(255, 128, 64)R255(255 3 31)占最高5位二进制11111G128(128 2 32)二进制100000B64(64 3 8)二进制01000拼接结果11111 100000 01000 0xFC08十进制(31 \times 2048 32 \times 32 8 63488 1024 8 64520)3.2 从RGB565转回RGB888两种常见策略反过来从RGB565恢复到8位RGB就没有唯一标准答案了这地方容易出坑。第一种是低位补零也就是把5位值左移3位、6位值左移2位空出来的低位补0void rgb565_to_888_zero_pad(uint16_t color, uint8_t *r, uint8_t *g, uint8_t *b) { *r (uint8_t)((color 11) 0x1F) 3; *g (uint8_t)((color 5) 0x3F) 2; *b (uint8_t)(color 0x1F) 3; }以红色0xF800为例R取出来是31左移3位得到248。但原始红色是255恢复出来只有248差了7。你会发现颜色偏暗了一点。第二种是高位复制或者叫bit replication把高位再复制到低位去填满8位void rgb565_to_888_bitrep(uint16_t color, uint8_t *r, uint8_t *g, uint8_t *b) { uint8_t r5 (color 11) 0x1F; uint8_t g6 (color 5) 0x3F; uint8_t b5 color 0x1F; *r (r5 3) | (r5 2); *g (g6 2) | (g6 4); *b (b5 3) | (b5 2); }红色R31时(31 \times 8 31 / 4 248 7 255)完美恢复。这个方法在嵌入式图形库和LCD驱动里很常见因为它在没有任何浮点运算的前提下把恢复误差控制到最小颜色密度也最接近原色。如果你的代码想在8位和16位格式间来回切建议都用高位复制法。3.3 两种转换方式的精度差别到底有多大我用一个实际例子说明。颜色RGB(200, 100, 50)按移位/截断法转成RGB565R(200 3 25)即200变25/31换算回8位是(25 \times 8 200)零填充或(25 \times 8 25/4 206)复制G(100 2 25)即100变25/63回到8位是200零填充或204复制B(50 3 6)即50变6/31回到8位是48零填充或54复制看到没有用零填充恢复G从100变成了200这种误差在某些渐变色带里会造成轻微的色阶断层。用高位复制恢复误差就小很多。在写代码的时候我一般遵循一个原则从8位转16位用移位法从16位转8位用高位复制法。前者省时间后者保精度。在STM32这种主频不高的单片机上做几百个像素调色板足够用。4. 拿到表之后实际项目里怎么用4.1 Python读取图片RGB值做图像处理的朋友经常会碰到“我想知道图片里某个像素的颜色值”。Python里最常用的两个库是Pillow和OpenCV但它们的行为不太一样。Pillow默认打开图片就是RGB顺序代码非常简单from PIL import Image img Image.open(test.png).convert(RGB) w, h img.size # 读取坐标(100, 50)处的像素 r, g, b img.getpixel((100, 50)) print(r, g, b) # 输出如 (10, 92, 140)批量读取用load()会更快from PIL import Image img Image.open(test.png).convert(RGB) px img.load() for y in range(0, 100): for x in range(0, 100): r, g, b px[x, y] # 在这里处理每个像素OpenCV默认读出来是BGR顺序很多人在这里栽过跟头import cv2 img cv2.imread(test.png) b, g, r img[50, 100] # 注意顺序是BGR # 如果直接赋值给一个RGB变量但没换顺序颜色就是错的所以要拿到标准的RGB值需要做一次通道互换import cv2 img cv2.imread(test.png) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) r, g, b img_rgb[50, 100] print(r, g, b)如果你想把读出来的8位RGB值转成RGB565格式给屏幕用可以直接套第3章的公式def rgb888_to_565(r, g, b): return ((r 3) 11) | ((g 2) 5) | (b 3) color565 rgb888_to_565(10, 92, 140) # 结果是0x0AF14.2 PWM驱动RGB灯珠8位值的映射方法热搜里出现了“pwm驱动led rgb灯”和“speedybee f405飞控 55A 8位电控”。做穿越机、机器人灯光控制的朋友应该很有共鸣。无论是Arduino还是STM32PWM驱动RGB灯珠的核心逻辑都一样用三路PWM分别控制红、绿、蓝三颗LED或一个全彩灯珠PWM占空比决定亮度。大部分单片机的PWM分辨率要么是8位0到255要么是16位0到65535。如果你的PWM是8位那么RGB(255, 128, 0)的意思就是红色通道占空比100%、绿色通道占空比约50%、蓝色通道0%。代码写起来大致是这样// 假设三个PWM通道已经初始化占空比范围0-255 void set_rgb(uint8_t r, uint8_t g, uint8_t b) { pwm_set_duty(RED_CH, r); // 红色PWM pwm_set_duty(GREEN_CH, g); // 绿色PWM pwm_set_duty(BLUE_CH, b); // 蓝色PWM } // 设置一个浅蓝色 set_rgb(135, 206, 235);如果你的PWM是16位分辨率但手里是8位颜色值那就需要把8位值映射到16位。映射方法有两种直接乘257(r16 r \times 257)因为 (255 \times 257 65535)这样能刚好映射到16位满值左移8位(r16 r 8)但这样255左移8位得到65280不是65535轻微不满我推荐直接用乘257的方式因为映射更准确。如果担心整数溢出可以先转成uint16_t再乘。uint16_t r16 (uint16_t)r * 257; // 8位转16位PWM占空比 uint16_t g16 (uint16_t)g * 257; uint16_t b16 (uint16_t)b * 257;灯的渐变动效核心就是每隔一段时间把RGB三个值往目标方向微调。比如红色慢慢从0加到255就是一个从黑到红的呼吸效果。用8位还是16位只影响渐变是否平滑8位PWM在一些低端灯珠上偶尔会有肉眼可见的跳变16位会顺滑很多但前提是灯珠驱动芯片支持16位分辨率。4.3 MATLAB里RGB与YUV、Lab色域互转MATLAB处理颜色比Python更省心因为内置了完整色域转换函数。热搜里有“matlab中rgb、yuv、lab色域”这块我说说实际用法。RGB转YUVimg_rgb imread(test.png); img_yuv rgb2ycbcr(img_rgb); % MATLAB里YUV对应YCbCrRGB转Labimg_rgb imread(test.png); img_lab rgb2lab(img_rgb);转Lab有个隐藏的坑rgb2lab默认输入RGB是sRGB色域如果你用Adobe RGB空间的图片直接转结果会有偏差需要先把图片转换到sRGB。另外Lab三个通道的值范围和RGB完全不一样是浮点数L通道一般是0到100a和b通道通常正负都在100以内。如果要做颜色分析别直接用uint8去量化Lab结果否则精度丢失非常严重。Matlab也支持把RGB565转成uint16的RGB整数function c565 rgb888_to_565(r, g, b) c565 bitshift(bitshift(uint16(r), -3), 11) ... bitshift(bitshift(uint16(g), -2), 5) ... bitshift(uint16(b), -3); end这套逻辑跟C语言里的完全一样本质就是把数据当作位段来拼接跟具体语言没关系。5. 我踩过的坑和排查建议5.1 偏色不一定是代码问题很多朋友调完转换后发下颜色偏了第一反应是代码写错了其实很多时候是色域标准不一致。同一个RGB(255, 0, 0)在sRGB、Adobe RGB、Display P3三种色域下显示出来的红色是三种不一样的红色。如果你的图片在电脑上看是正常的传到单片机屏幕上就偏色了先别急着怀疑RGB565转换。先检查单片机屏幕的色域和背光参数再检查空间是否经过伽马校正。这种情况下的偏色是显示设备差异造成的不是你转换算法错了。5.2 OpenCV的BGR通道顺序我在这上面栽过OpenCV读图默认顺序是BGR这个事真的很阴间。我分享一个具体踩坑经历有一次从图片里取了一个像素颜色用在网页端显示直接把OpenCV读到的三个值拼成rgb(r, g, b)结果一张图所有颜色红蓝互换人物皮肤变成了诡异的蓝色。排查了半天发现就是没做BGR到RGB的通道转换。建议做图像处理的人从一开始就养成习惯任何OpenCV读出来的像素值只要打算给别人用先cvtColor(img, cv2.COLOR_BGR2RGB)或者干脆自己写一行像素切分逻辑b, g, r img[y, x] # 拆开之后再按需拼装RGB值这样就不会被顺序坑到。5.3 RGB565转换后发暗的解决办法RGB565转RGB888后颜色偏暗是另一个高发问题。原因在第3.2节说过就是用了低位补零导致色值回不到原亮度。如果你在做LCD显示有几种办法处理用高位复制法恢复全亮度代码我给了如果屏幕并不要求RGB888而是支持直接写RGB565那就别转回8位直接用565值最省事如果确实要8位且颜色大多是纯色比如红、绿、蓝这些满通道值低补零影响不大因为255转成565再转回8位顶多损失7个亮度级别肉眼看不太出来真正难受的是那些中间调的渐变色比如RGB(128, 128, 128)这种灰色转成565是0x8410恢复时用零填充是(16 \times 8 16 \times 4 16 \times 8 12864128)不你拆开来算用零填充恢复R和B是(16 \times 8 128)G是(32 \times 4 128)都能回到128。实际上中间值误差可能更小这跟每个通道值是否落在“刚好整除”的点上有关。5.4 自己动手做一张专属颜色表表格永远是死的项目需求永远是活的。我自己现在很少有现成的颜色表了都是需要时直接现算。给你一个我常用的方式先用Python或在线工具把品牌色、UI规范里的颜色全部定义成8位RGB然后一次性转成RGB565存成C语言头文件#ifndef COLOR_PALETTE_H #define COLOR_PALETTE_H typedef struct { char *name; uint16_t rgb565; } color_entry_t; static const color_entry_t palette[] { {pixel_blue, 0x0AF1}, {brand_red, 0xF800}, {background_dark, 0x4208}, {font_light, 0xFFDF}, // 继续增加项目里用到的颜色 }; #endif这样做的好处是项目里引用颜色时直接写语义化的名字比如palette[BACKGROUND_DARK].rgb565而不是散落一地的魔法数字。后面想换色只需要改这一处。写在最后的一个建议我自己的体会是颜色对照表这东西背是背不完的也没必要背。真正值钱的是理解8位和16位之间的关系以及掌握一套能在脑子里快速心算的转换逻辑。就像RGB(10, 92, 140)这种值你不需要知道它叫什么名字你只需要知道它的8位十六进制是#0A5C8C它的RGB565是0x0AF1然后不管是写单片机程序还是做上位机都能直接用。先把这篇文章里的表格存下来再把转换公式抄到你熟悉的语言里剩下的就是遇到问题多测几遍的事。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →