You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 10:39:26