如何将RGB图像量化为N种指定颜色 实现像素到色名映射
技术正式名称
你要查找的这类将全色域RGB值映射到有限个预设颜色的技术,通用正式名称是颜色量化(Color Quantization),你描述的逐像素匹配预设色表最近邻的实现,属于颜色量化下「预定义调色板最近邻映射」的典型场景,部分资料也会称其为调色板映射(Palette Mapping),搜索这几个关键词就能找到对应的技术资料、算法优化方案。
注意:不建议直接在RGB色彩空间计算欧氏距离匹配颜色——人眼对R/G/B三个通道的敏感度差异很大,直接算RGB距离经常会出现“视觉上差很远的颜色算出来距离更近”的错配问题,优先转换到CIE Lab色彩空间计算距离,匹配结果和人眼感知的一致性会高很多。
Python实现方案
实现依赖Pillow(图像读写)和Numpy(批量数值计算,避免逐像素循环的性能问题),先安装依赖:pip install pillow numpy
完整可运行代码如下,已经内置了你提供的全部预设颜色条目:
from PIL import Image import numpy as np # 预设颜色对照表 color_palette = [ {"name": "medium blue", "rgb": (70, 138, 196)}, {"name": "bright orange", "rgb": (229, 28, 23)}, {"name": "transparent brown", "rgb": (165, 144, 129)}, {"name": "transparent medium reddish-violet", "rgb": (236, 155, 193)}, {"name": "bright yellowish-green", "rgb": (147, 183, 10)}, {"name": "bright reddish-violet", "rgb": (154, 0, 96)}, {"name": "transparent bright bluish-violet", "rgb": (155, 148, 198)}, {"name": "silver", "rgb": (139, 146, 148)}, {"name": "sand blue", "rgb": (93, 115, 138)}, {"name": "sand yellow", "rgb": (139, 115, 81)}, {"name": "earth blue", "rgb": (0, 35, 63)}, {"name": "earth green", "rgb": (0, 51, 21)}, {"name": "transparent flourescent blue", "rgb": (205, 226, 245)}, {"name": "metallic dark grey", "rgb": (72, 62, 58)}, {"name": "sand green", "rgb": (94, 128, 100)}, {"name": "dark red", "rgb": (126, 7, 26)}, {"name": "flame yellowish orange", "rgb": (242, 153, 0)}, {"name": "transparent bright orange", "rgb": (235, 117, 13)}, {"name": "reddish brown", "rgb": (90, 27, 11)}, {"name": "medium stone grey", "rgb": (154, 144, 143)}, {"name": "dark stone grey", "rgb": (75, 92, 85)}, {"name": "light stone grey", "rgb": (227, 227, 217)}, {"name": "light royal blue", "rgb": (133, 190, 232)}, {"name": "bright purple", "rgb": (221, 55, 138)}, {"name": "light purple", "rgb": (237, 156, 194)}, {"name": "cool yellow", "rgb": (254, 254, 152)}, {"name": "medium lilac", "rgb": (43, 20, 118)}, {"name": "light nougat", "rgb": (243, 191, 135)}, {"name": "phosph. green", "rgb": (252, 250, 211)}, {"name": "warm gold", "rgb": (168, 125, 44)}, {"name": "dark brown", "rgb": (46, 13, 4)}, {"name": "transparent bright green", "rgb": (151, 253, 100)}, {"name": "medium nougat", "rgb": (168, 124, 84)}, {"name": "white", "rgb": (255, 255, 255)}, {"name": "black", "rgb": (0, 0, 0)}, {"name": "transparent yellow", "rgb": (248, 238, 104)}, {"name": "transparent flourescent reddish-orange", "rgb": (229, 100, 70)}, {"name": "transparent red", "rgb": (22, 40, 39)}, {"name": "transparent light blue", "rgb": (181, 223, 233)}, {"name": "transparent blue", "rgb": (78, 175, 230)}, {"name": "transparent green", "rgb": (97, 176, 108)}, {"name": "transparent flourescent green", "rgb": (242, 238, 92)}, {"name": "transparent", "rgb": (240, 240, 240)}, {"name": "bright yellow", "rgb": (255, 197, 2)}, {"name": "bright red", "rgb": (220, 0, 12)}, {"name": "bright blue", "rgb": (2, 89, 169)}, {"name": "dark green", "rgb": (3, 126, 43)}, {"name": "nougat", "rgb": (215, 116, 66)}, {"name": "bright green", "rgb": (1, 150, 37)}, {"name": "brick-yellow", "rgb": (218, 188, 125)}, {"name": "dark orange", "rgb": (165, 60, 20)}, ] def srgb_to_lab(arr): # sRGB转线性RGB arr = arr / 255.0 mask = arr > 0.04045 arr[mask] = np.power((arr[mask] + 0.055) / 1.055, 2.4) arr[~mask] = arr[~mask] / 12.92 # 线性RGB转XYZ(D65白点) x = arr[:,0] * 0.4124564 + arr[:,1] * 0.3575761 + arr[:,2] * 0.1804375 y = arr[:,0] * 0.2126729 + arr[:,1] * 0.7151522 + arr[:,2] * 0.0721750 z = arr[:,0] * 0.0193339 + arr[:,1] * 0.1191920 + arr[:,2] * 0.9503041 x /= 0.95047 z /= 1.08883 # XYZ转Lab mask = (x > 0.008856) & (y > 0.008856) & (z > 0.008856) x[mask] = np.power(x[mask], 1/3) y[mask] = np.power(y[mask], 1/3) z[mask] = np.power(z[mask], 1/3) x[~mask] = 7.787 * x[~mask] + 16/116 y[~mask] = 7.787 * y[~mask] + 16/116 z[~mask] = 7.787 * z[~mask] + 16/116 L = 116 * y - 16 a = 500 * (x - y) b = 200 * (y - z) return np.stack([L,a,b], axis=1) def map_image_to_palette(input_path, output_path): # 读取图像 img = Image.open(input_path).convert("RGB") img_array = np.array(img).reshape(-1, 3).astype(np.float32) # 提取调色板RGB数组 palette_rgbs = np.array([c["rgb"] for c in color_palette], dtype=np.float32) # 转Lab空间计算距离 img_lab = srgb_to_lab(img_array) palette_lab = srgb_to_lab(palette_rgbs) # 批量计算距离,取最近邻 distances = np.sum((img_lab[:, np.newaxis, :] - palette_lab[np.newaxis, :, :])**2, axis=2) closest_indices = np.argmin(distances, axis=1) # 生成映射后图像 mapped_pixels = palette_rgbs[closest_indices].astype(np.uint8) # 如需获取每个像素的颜色名称,取消下一行注释即可 # pixel_color_names = [color_palette[i]["name"] for i in closest_indices] mapped_img = Image.fromarray(mapped_pixels.reshape(img.size[1], img.size[0], 3)) mapped_img.save(output_path) print(f"转换完成,结果已保存至{output_path}") if __name__ == "__main__": map_image_to_palette("input.jpg", "output.png")
实现说明
- 内置无额外依赖的sRGB转CIE Lab逻辑,匹配结果符合人眼视觉感知,比直接在RGB空间算距离的错配率低很多
- 采用Numpy广播机制做批量距离计算,千万像素级图像处理耗时仅数秒,不要用纯Python写逐像素for循环,性能会差两个数量级以上
- 你举例的(224,5,15)、(223,1,10)等近似亮红色的RGB取值,都会被正确匹配到
bright red条目 - 代码中预留了像素颜色名称输出的注释,取消注释即可获取每个像素对应的颜色名,可自行扩展颜色占比统计、区域标注等功能
内容的提问来源于stack exchange,提问作者boardrider
相关产品推荐
相关产品推荐

