背景在做 项目 的时候,需要判断一个颜色值所在色系(如:红橙黄绿青蓝紫黑白灰),用眼睛观察太慢,算不上好办法,那么怎么判断呢?通过阅读 该讨论 知道了一种方案:将 RGB 色值转化为 HSV,之后通过 Hue 去判断彩色的种类,用明度去判断黑白灰。具体实现如下。
基础知识RGBRGB 是从颜色发光的原理来设计定的,通俗点说它的颜色混合方式就好像有红、绿、蓝三盏灯,当它们的光相互叠合的时候,色彩相混,而亮度却等于两者亮度之总和,越混合亮度越高,即加法混合。
红、绿、蓝三个颜色通道每种色各分为 256 阶亮度,在 0 时“灯”最弱——是关掉的,而在 255 时“灯”最亮。当三色灰度数值相同时,产生不同灰度值的灰色调,即三色灰度都为 0 时,是最暗的黑色调;三色灰度都为 255 时,是最亮的白色调。
在电脑中,RGB 的所谓“多少”就是指亮度,并使用整数来表示。通常情况下,RGB 各有 256 级亮度,用数字表示为从 0、1、2…直到 255。注意虽然数字最高是 255,但 0 也是数值之一,因此共 256 级。
HSVHSV 是一种比较直观的颜色模型,所以在许多图像编辑工具中应用比较广泛,这个模型中颜色的参数分别是:色调(H, Hue),饱和度(S,Saturation),明度(V, Value)。
色调(Hue)用角度度量,取值范围为 0°~360°,从红色开始按逆时针方向计算,红色为 0°,绿色为 120°,蓝色为 240°。它们的补色是:黄色为 60°,青色为 180°,品红为 300°;
饱和度(Saturation)饱和度 S 表示颜色接近光谱色的程度。一种颜色,可以看成是某种光谱色与白色混合的结果。其中光谱色所占的比例愈大,颜色接近光谱色的程度就愈高,颜色的饱和度也就愈高。饱和度高,颜色则深而艳。光谱色的白光成分为 0,饱和度达到最高。通常取值范围为 0%~100%,值越大,颜色越饱和。
明度(Value)明度表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为 0%(黑)到 100%(白)。
取值范围说明我们需要注意的在不同应用场景中,HSV 取值范围是不尽相同的。
1.PS 软件时,H 取值范围是 0-360,S 取值范围是(0%-100%),V 取值范围是(0%-100%)。
2.利用 openCV 中 cvSplit 函数的在选择图像 IPL_DEPTH_32F 类型时,H 取值范围是 0-360,S 取值范围是 0-1(0%-100%),V 取值范围是 0-1(0%-100%)。
3.利用 openCV 中 cvSplit 函数的在选择图像 IPL_DEPTH_8UC 类型时,H 取值范围是 0-180,S 取值范围是 0-255,V 取值范围是 0-255。
那么,开始写代码吧!
RGB 转 HSV公式
常量$$\begin{array}{l}R^{\prime}=R / 255\\G^{\prime}=G / 255\\B^{\prime}=B / 255\\\operatorname{Cmax}=\max \left(R^{\prime}, G^{\prime}, B^{\prime}\right)\\C \min =\min \left(R^{\prime}, G^{\prime}, B^{\prime}\right)\\\Delta=\mathrm{Cmax}-\mathrm{Cmin}\end{array}$$
H 计算$$H=\left\{\begin{array}{cc}0^{\circ} & \Delta=0 \\ 60^{\circ} \times\left(\frac{G^{\prime}-B^{\prime}}{A} \bmod 6\right) & , C_{\max }=R^{\prime} \\ 60^{\circ} \times\left(\frac{B^{\prime}-R^{\prime}}{\Delta}+2\right) & , C_{\max }=G^{\prime} \\ 60^{\circ} \times\left(\frac{R^{\prime}-G^{\prime}}{\Delta}+4\right) & , C_{\max }=B^{\prime}\end{array}\right.$$
S 计算$$S=\left\{\begin{array}{cl}0 & C \max =0 \\\frac{\Delta}{C \max } & C \max \neq 0\end{array}\right.$$
V 计算$V=C \max$
代码(RGB 转 HSV)def hue_calculate_org(round1, round2, delta, add_num): return ((round1 - round2) / delta + add_num) * 60def rgb_to_hsv_org(rgb_seq): r, g, b = rgb_seq r_round = float(r) / 255 g_round = float(g) / 255 b_round = float(b) / 255 max_c = max(r_round, g_round, b_round) min_c = min(r_round, g_round, b_round) delta = max_c - min_c h = None if delta == 0: h = 0 elif max_c == r_round: h = ((g_round - b_round) / delta % 6) * 60 elif max_c == g_round: h = hue_calculate_org(b_round, r_round, delta, 2) elif max_c == b_round: h = hue_calculate_org(r_round, g_round, delta, 4) if max_c == 0: s = 0 else: s = delta / max_c return h, s, max_c
另一种计算方式来自此处:👉 Python Math: Convert RGB color to HSV color - w3resourcedef hue_calculate(round1, round2, delta, add_num): return (((round1 - round2) / delta) * 60 + add_num) % 360def rgb_to_hsv(rgb_seq): r, g, b = rgb_seq r_round = float(r) / 255 g_round = float(g) / 255 b_round = float(b) / 255 max_c = max(r_round, g_round, b_round) min_c = min(r_round, g_round, b_round) delta = max_c - min_c h = None if delta == 0: h = 0 elif max_c == r_round: h = hue_calculate(g_round, b_round, delta, 360) elif max_c == g_round: h = hue_calculate(b_round, r_round, delta, 120) elif max_c == b_round: h = hue_calculate(r_round, g_round, delta, 240) if max_c == 0: s = 0 else: s = (delta / max_c) * 100 v = max_c * 100 return h, s, v
颜色划分
分析
Color
Color name
(H,S,V)
Hex
(R,G,B)
Black
(0°,0%,0%)
#000000
(0,0,0)
Gray
(0°,0%,50%)
#808080
(128,128,128)
White
(0°,0%,100%)
#FFFFFF
(255,255,255)
Red
(0°,100%,100%)
#FF0000
(255,0,0)
Yellow
(60°,100%,100%)
#FFFF00
(255,255,0)
Lime
(120°,100%,100%)
#00FF00
(0,255,0)
Cyan
(180°,100%,100%)
#00FFFF
(0,255,255)
Blue
(240°,100%,100%)
#0000FF
(0,0,255)
Magenta
(300°,100%,100%)
#FF00FF
(255,0,255)
数据来源:HSV to RGB conversion | color conversion看色图,哦不,色环图。😳
先不管黑灰白三种色,找其他颜色与色环的对应关系,我们很容易发现 H 与色环的对应关系。
变换色相 H,保持 S,V 不变,颜色发生变化;
修改 H 会引起颜色变化;
变换亮度 V,保持 H,S 不变,我们发现颜色在黑灰白之间变化;
变换饱和度 S,保持 H,V 不变,颜色不会变化,变的只是颜色的深浅程度;
因为人眼对明亮的颜色更加敏感,所以我们适当调高亮度,之后逐渐修改 S,观察颜色变化。有内味儿了,颜色变得越来越浓郁了!
代码颜色归类(按色系)def find_color_series(rgb_seq): # TODO:此处是否有更好实现? """ 将rgb转为hsv之后根据h和v寻找色系 :param rgb_seq: :return: """ h, s, v = rgb_to_hsv(rgb_seq) cs = None if 30 < h <= 90: cs = 'yellow' elif 90 < h <= 150: cs = 'green' elif 150 < h <= 210: cs = 'cyan' elif 210 < h <= 270: cs = 'blue' elif 270 < h <= 330: cs = 'purple' elif h > 330 or h <= 30: cs = 'red' if s < 10: # 色相太淡时,显示什么颜色主要由亮度来决定 cs = update_by_value(v) assert cs in COLOR_SERIES_MAP return cs def update_by_value(v): """ 根据 V 值去更新色系数据 :param v: :return: """ if v <= 100 / 3 * 1: cs = 'black' elif v <= 100 / 3 * 2: cs = 'gray' else: cs = 'white' return cs
代码验证if __name__ == '__main__': color_list = [[22, 24, 35], [36, 134, 185], [234, 137, 88], [32, 161, 98], [100, 106, 88]] for item in color_list: print(find_color_series(item))
返回结果:
bluecyanredcyanyellow至此,我们完成了数据颜色粗略的归类。到这里就结束了吗?显然没有。实际上,这种数据返回结果是很粗糙的,我们需要对 Hue 进行反复修正才能得出一个较为满意的结果。正如物理实验一样:理论和实践可能存在较大偏差甚至可能得出完全相反的结论,此处我们不再展开。😂我们进一步分析数据,发现:
甚三紅、黄朽葉、千歳緑、錆青磁、江戸紫、白練……
你细品~是的,部分颜色名称里面已经包含了颜色的色系关键字。我们假定名称的命名是靠谱的。那么,我们可以利用正则把色系关键字提取出来,然后只对没有指明的颜色进行计算。
可是上面的代码已经写完了,我不想再大动干戈修改此函数,怎么办?此处,我们使用装饰器。
颜色归类(按名称)import refrom functools import wraps, partialdef attach_wrapper(obj, func=None): if func is None: return partial(attach_wrapper, obj) setattr(obj, func.__name__, func) return funcdef find_color_series_by_name(name=''): """ 装饰器:通过颜色的中文名称利用正则匹配获取颜色名称 我们假定命名是符合人类主观意识的,即:名称比我们的代码更可靠 因为按照hsv去匹配的时候会有误差,所以我们先通过名称去直接匹配色系,如果名称中没有关键字,我们再使用自己写的规则 :param name:str, :return: """ def deco(func): color_name_char = name @wraps(func) def wrapper(*args, **kwargs): color_series = '' if color_name_char: re_ret = re.match(REG_COLOR_SERES, color_name_char) if re_ret: color_signal = re_ret.group(1) color_series = COLOR_SERIES_MAP_REVERSE.get(color_signal) if color_series == '': color_series = func(*args, **kwargs) return color_series @attach_wrapper(wrapper) def set_color_name(new_name): nonlocal color_name_char color_name_char = new_name return wrapper return deco
关于此处装饰器的写法,参见《Python Cookbook 中文版》V3 P342 第 9.5 章节:定义一个属性可由用户修改的装饰器。
最后,我们给之前写的函数戴上这顶叫做装饰器的“帽子”。
装饰器使用@find_color_series_by_name(name='')def find_color_series(rgb_seq): pass # 省略重复代码 if __name__ == '__main__': test_color_map = {'东方红': [254, 223, 225], '红东方': [215, 196, 187], '方红东': [86, 46, 55], '黄丹': [240, 94, 28], '万红丛中一点绿': [63, 43, 54]} for k, v in test_color_map.items(): find_color_series.set_color_name(k) find_color = find_color_series(v) print(find_color)
返回结果
redredredyellowgreen
总结展望经过上面的步骤我们完成了颜色的分类,但是这种分类是粗粒度的,结果也可能是不够准确的。如果需要更精准的计算,可能需要建模及算法甚至训练模型去实现,这里不再展开(主要是我不会)。
如果颜色本身命名错误,如上例中的“万红丛中一点绿”,我们直接走装饰器判断逻辑,就会使计算过滤的逻辑失效。所以我们可以数据分别走两套逻辑,对于两者返回一致的,我们认为这个数据是可信的;对于不一致的部分,我们则需要使用人工智能的“人工”部分对数据进行二次编辑,之后将结果更新上去。
西风吟
我们知道 Python 是一门自带电池的语言,关于颜色 rgb 转 hsv,其实 官方模块 colorsys 中已经有自己的实现,我们需要的只是对数据做相应比例的放大。当然我们也可以在后续逻辑中直接用官方返回的数据划分区域。>>> import colorsys>>> colorsys.rgb_to_hsv(0.2, 0.4, 0.4)(0.5, 0.5, 0.4)>>> colorsys.hsv_to_rgb(0.5, 0.5, 0.4)(0.2, 0.4, 0.4)
关于颜色的判断,我们知道色的三原色为 RGB(红绿蓝),而实际颜色是按照不同比例混合的,所以颜色是没有办法真的做明显界线区分的。
谁能在彩虹里画下紫色结束、橙色开始的分界线呢?我们能清晰地看到颜色的不同,但是究竟在什么地方一种颜色逐渐地混入了另一种颜色呢?理智和疯狂的界限,亦是如此。
日常黑男性环节:
链接源码下载🍭 my_practices/codes/tell_color_by_rgb at master · imoyao/my_practices
参考链接
从 RGB 到 HSV 的转换详细介绍_人工智能_hanshanbuleng 的博客-CSDN 博客
知道一个颜色的 RGB 如何归类到特定颜色(如赤橙黄绿青蓝紫黑白灰)? - V2EX
How can I tell basic color a hex code is closest to? - Graphic Design Stack Exchange
algorithm - “Rounding” colour values to the nearest of a small set of colours - Stack Overflow
推荐阅读
青色是什么样子的?| 果壳 科技有意思
其他
颜色列表 - 维基中文镜像,自由的百科全书
latex 在线公式编辑器汉化版
HSV to RGB conversion | color conversion