颜色相似度度量
最近工作中遇到了一个判断两种颜色是否相似的问题,最初采用hsv,rgb,yuv颜色空间,采用欧氏距离去度量相似度,最后发现效果不是很好,不符合人的感官,所以一直寻求一种能符合人眼的感觉来判断两种颜色是否相似。所以此篇介绍的CIEDE2000计算公式就是我们需要的。
1.CIEDE2000
CIEDE2000(CIE Delta E 2000)是目前最权威、最广泛使用的色差计算公式,由国际照明委员会(CIE)于2001年制定。它是 CIE94 的改进版,解决了前代公式的多个问题。CIEDE2000是国际照明委员会(CIE)在2001年推荐的最新的色彩评价公式,也是目前公认与人眼视觉感知匹配最准确的色差公式。它并不是建立一个新的色彩空间,而是在CIELAB色彩空间的基础上,对色差计算方法进行了复杂的修正和优化,使得计算结果在整个空间中都能与人眼的评估高度一致。
2.核心特点

3.数学结构


4.五大核心修正
明度权重 SL(低亮度敏感)

效果:中等亮度区域(L=50)色差最敏感,极暗/极亮区域敏感度降低。

色度权重 SC(高饱和宽容)


效果:颜色越鲜艳,对色差越不敏感。原因:人眼在高饱和区域更难分辨细微差异。
色相权重 SH(综合调整)


效果:根据色相角度动态调整敏感度。
旋转因子 RT(蓝色修正)

色相角计算(环形处理)

避免:359° 和 1° 被误认为相差 358°(实际只差 2°)。
5.色差解读标准

6.实际应用领域
- 印刷业: ΔE < 2.0 为合格品
- 纺织业: ΔE < 1.0 为一级品
- 显示器: ΔE < 3.0 为专业级
- 食品检测: 果蔬成熟度评估
- 医学: 皮肤病变颜色分析
- 工业设计: 塑料/涂料配色
7.其他颜色空间颜色相似度判断对比

综合对比如下:

8.代码实现
# import sys # from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QColorDialog # from PyQt5.QtGui import QColor, QPalette # from PyQt5.QtCore import Qt # from colormath.color_objects import sRGBColor, LabColor # from colormath.color_conversions import convert_color # from colormath.color_diff import delta_e_cie2000 # # class ColorSimilarityApp(QWidget): # def __init__(self): # super().__init__() # self.color1 = QColor(255, 0, 0) # 默认红色 # self.color2 = QColor(0, 255, 0) # 默认绿色 # self.initUI() # # def initUI(self): # self.setWindowTitle('颜色相似度计算') # self.setGeometry(300, 300, 400, 300) # # # 创建颜色显示区域 # self.color1_label = QLabel('颜色1') # self.color1_label.setAutoFillBackground(True) # self.color1_label.setFixedHeight(50) # self.updateColorLabel(self.color1_label, self.color1) # # self.color2_label = QLabel('颜色2') # self.color2_label.setAutoFillBackground(True) # self.color2_label.setFixedHeight(50) # self.updateColorLabel(self.color2_label, self.color2) # # # 创建按钮 # self.btn1 = QPushButton('选择颜色1') # self.btn1.clicked.connect(self.chooseColor1) # # self.btn2 = QPushButton('选择颜色2') # self.btn2.clicked.connect(self.chooseColor2) # # # 计算按钮 # self.calc_btn = QPushButton('计算相似度') # self.calc_btn.clicked.connect(self.calculateSimilarity) # # # 结果显示 # self.result_label = QLabel('相似度: ') # self.result_label.setAlignment(Qt.AlignCenter) # # # 布局 # vbox = QVBoxLayout() # vbox.addWidget(self.color1_label) # vbox.addWidget(self.btn1) # vbox.addWidget(self.color2_label) # vbox.addWidget(self.btn2) # vbox.addWidget(self.calc_btn) # vbox.addWidget(self.result_label) # # self.setLayout(vbox) # # def updateColorLabel(self, label, color): # palette = label.palette() # palette.setColor(QPalette.Window, color) # label.setPalette(palette) # label.setText(f'RGB({color.red()}, {color.green()}, {color.blue()})') # label.setAlignment(Qt.AlignCenter) # # def chooseColor1(self): # color = QColorDialog.getColor(self.color1, self, '选择颜色1') # if color.isValid(): # self.color1 = color # self.updateColorLabel(self.color1_label, color) # # def chooseColor2(self): # color = QColorDialog.getColor(self.color2, self, '选择颜色2') # if color.isValid(): # self.color2 = color # self.updateColorLabel(self.color2_label, color) # # def calculateSimilarity(self): # # 将QColor转换为sRGBColor # rgb1 = sRGBColor(self.color1.red(), self.color1.green(), self.color1.blue(), is_upscaled=True) # rgb2 = sRGBColor(self.color2.red(), self.color2.green(), self.color2.blue(), is_upscaled=True) # # # 将RGB转换到Lab颜色空间 # lab1 = convert_color(rgb1, LabColor) # lab2 = convert_color(rgb2, LabColor) # # # 计算CIEDE2000色差 # delta_e = delta_e_cie2000(lab1, lab2) # # # 显示结果,通常认为delta_e < 1时人眼难以区分,但具体阈值可根据应用调整 # self.result_label.setText(f'相似度 (CIEDE2000色差): {delta_e:.4f}\n' # f'(数值越小越相似,通常<1为非常相似,>10为完全不同)') # # if __name__ == '__main__': # app = QApplication(sys.argv) # ex = ColorSimilarityApp() # ex.show() # sys.exit(app.exec_()) import sys import math from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QColorDialog from PyQt5.QtGui import QColor, QPalette from PyQt5.QtCore import Qt class ColorSimilarityApp(QWidget): def __init__(self): super().__init__() self.color1 = QColor(255, 0, 0) # 默认红色 self.color2 = QColor(0, 255, 0) # 默认绿色 self.initUI() def initUI(self): self.setWindowTitle('颜色相似度计算') self.setGeometry(300, 300, 450, 350) # 创建颜色显示区域 self.color1_label = QLabel('颜色1') self.color1_label.setAutoFillBackground(True) self.color1_label.setFixedHeight(60) self.color1_label.setStyleSheet(""" QLabel { border: 2px solid #333; border-radius: 5px; font-weight: bold; } """) self.updateColorLabel(self.color1_label, self.color1) self.color2_label = QLabel('颜色2') self.color2_label.setAutoFillBackground(True) self.color2_label.setFixedHeight(60) self.color2_label.setStyleSheet(""" QLabel { border: 2px solid #333; border-radius: 5px; font-weight: bold; } """) self.updateColorLabel(self.color2_label, self.color2) # 创建按钮 self.btn1 = QPushButton('选择颜色1') self.btn1.setStyleSheet(""" QPushButton { padding: 8px; font-weight: bold; background-color: #4CAF50; color: white; border-radius: 5px; } QPushButton:hover { background-color: #45a049; } """) self.btn1.clicked.connect(self.chooseColor1) self.btn2 = QPushButton('选择颜色2') self.btn2.setStyleSheet(""" QPushButton { padding: 8px; font-weight: bold; background-color: #2196F3; color: white; border-radius: 5px; } QPushButton:hover { background-color: #1976D2; } """) self.btn2.clicked.connect(self.chooseColor2) # 计算按钮 self.calc_btn = QPushButton('计算相似度') self.calc_btn.setStyleSheet(""" QPushButton { padding: 10px; font-weight: bold; font-size: 14px; background-color: #FF5722; color: white; border-radius: 5px; } QPushButton:hover { background-color: #E64A19; } """) self.calc_btn.clicked.connect(self.calculateSimilarity) # 结果显示 self.result_label = QLabel('请先选择两种颜色,然后点击"计算相似度"') self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setStyleSheet(""" QLabel { font-size: 14px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; } """) self.result_label.setWordWrap(True) # 添加一个相似度进度条 self.similarity_bar = QLabel() self.similarity_bar.setFixedHeight(20) self.similarity_bar.setStyleSheet(""" QLabel { border: 1px solid #ccc; border-radius: 10px; background-color: #f0f0f0; } """) # 布局 vbox = QVBoxLayout() vbox.setSpacing(15) vbox.setContentsMargins(20, 20, 20, 20) # 颜色1部分 color1_layout = QVBoxLayout() color1_layout.addWidget(self.color1_label) color1_layout.addWidget(self.btn1) # 颜色2部分 color2_layout = QVBoxLayout() color2_layout.addWidget(self.color2_label) color2_layout.addWidget(self.btn2) # 添加颜色对比布局 colors_layout = QHBoxLayout() colors_layout.addLayout(color1_layout) colors_layout.addSpacing(20) colors_layout.addLayout(color2_layout) vbox.addLayout(colors_layout) vbox.addWidget(self.calc_btn) vbox.addWidget(self.similarity_bar) vbox.addWidget(self.result_label) self.setLayout(vbox) self.setStyleSheet(""" QWidget { background-color: #f5f5f5; } """) def updateColorLabel(self, label, color): # 根据颜色亮度设置文字颜色,确保可读性 brightness = color.red() * 0.299 + color.green() * 0.587 + color.blue() * 0.114 text_color = "white" if brightness < 128 else "black" label.setStyleSheet(f""" QLabel {{ background-color: rgb({color.red()}, {color.green()}, {color.blue()}); color: {text_color}; border: 2px solid #333; border-radius: 5px; font-weight: bold; padding: 5px; }} """) # 显示RGB和十六进制值 hex_color = color.name().upper() label.setText(f'RGB: ({color.red()}, {color.green()}, {color.blue()})\nHEX: {hex_color}') def chooseColor1(self): color = QColorDialog.getColor(self.color1, self, '选择颜色1') if color.isValid(): self.color1 = color self.updateColorLabel(self.color1_label, color) def chooseColor2(self): color = QColorDialog.getColor(self.color2, self, '选择颜色2') if color.isValid(): self.color2 = color self.updateColorLabel(self.color2_label, color) def rgb_to_lab(self, r, g, b): """将RGB颜色转换为Lab颜色空间""" # 将RGB归一化到0-1 r, g, b = r / 255.0, g / 255.0, b / 255.0 # 逆伽马校正 def inv_gamma(c): return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 r, g, b = inv_gamma(r), inv_gamma(g), inv_gamma(b) # RGB转XYZ x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375 y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750 z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041 # D65标准光源 x_ref, y_ref, z_ref = 0.95047, 1.00000, 1.08883 # XYZ转Lab def f(t): return t ** (1 / 3) if t > 0.008856 else 7.787 * t + 16 / 116 fx, fy, fz = f(x / x_ref), f(y / y_ref), f(z / z_ref) L = 116 * fy - 16 a = 500 * (fx - fy) b_val = 200 * (fy - fz) return L, a, b_val def delta_e_cie2000(self, lab1, lab2): """计算CIEDE2000色差""" L1, a1, b1 = lab1 L2, a2, b2 = lab2 # 计算C* C1 = math.sqrt(a1 * a1 + b1 * b1) C2 = math.sqrt(a2 * a2 + b2 * b2) # 平均C* C_bar = (C1 + C2) / 2.0 # 计算G G = 0.5 * (1 - math.sqrt((C_bar ** 7) / (C_bar ** 7 + 25 ** 7))) # 调整a' a1_prime = (1 + G) * a1 a2_prime = (1 + G) * a2 # 调整C' C1_prime = math.sqrt(a1_prime * a1_prime + b1 * b1) C2_prime = math.sqrt(a2_prime * a2_prime + b2 * b2) # 计算h' def calculate_h_prime(a, b): if a == 0 and b == 0: return 0 h = math.degrees(math.atan2(b, a)) return h if h >= 0 else h + 360 h1_prime = calculate_h_prime(a1_prime, b1) h2_prime = calculate_h_prime(a2_prime, b2) # 计算ΔL', ΔC', ΔH' delta_L_prime = L2 - L1 delta_C_prime = C2_prime - C1_prime # 计算Δh' delta_h_prime = h2_prime - h1_prime if C1_prime * C2_prime == 0: delta_h_prime = 0 elif abs(delta_h_prime) <= 180: pass elif delta_h_prime > 180: delta_h_prime -= 360 elif delta_h_prime < -180: delta_h_prime += 360 delta_H_prime = 2 * math.sqrt(C1_prime * C2_prime) * math.sin(math.radians(delta_h_prime / 2)) # 计算平均L', C', h' L_bar_prime = (L1 + L2) / 2.0 C_bar_prime = (C1_prime + C2_prime) / 2.0 # 计算平均h' if C1_prime * C2_prime == 0: h_bar_prime = h1_prime + h2_prime elif abs(h1_prime - h2_prime) <= 180: h_bar_prime = (h1_prime + h2_prime) / 2.0 elif abs(h1_prime - h2_prime) > 180 and (h1_prime + h2_prime) < 360: h_bar_prime = (h1_prime + h2_prime + 360) / 2.0 else: h_bar_prime = (h1_prime + h2_prime - 360) / 2.0 # 计算T T = (1 - 0.17 * math.cos(math.radians(h_bar_prime - 30)) + 0.24 * math.cos(math.radians(2 * h_bar_prime)) + 0.32 * math.cos(math.radians(3 * h_bar_prime + 6)) - 0.20 * math.cos(math.radians(4 * h_bar_prime - 63))) # 计算Δθ delta_theta = 30 * math.exp(-((h_bar_prime - 275) / 25) ** 2) # 计算R_C R_C = 2 * math.sqrt((C_bar_prime ** 7) / (C_bar_prime ** 7 + 25 ** 7)) # 计算S_L, S_C, S_H S_L = 1 + ((0.015 * (L_bar_prime - 50) ** 2) / math.sqrt(20 + (L_bar_prime - 50) ** 2)) S_C = 1 + 0.045 * C_bar_prime S_H = 1 + 0.015 * C_bar_prime * T # 计算R_T R_T = -math.sin(math.radians(2 * delta_theta)) * R_C # 计算ΔE00 delta_E = math.sqrt( (delta_L_prime / S_L) ** 2 + (delta_C_prime / S_C) ** 2 + (delta_H_prime / S_H) ** 2 + R_T * (delta_C_prime / S_C) * (delta_H_prime / S_H) ) return delta_E def calculateSimilarity(self): try: # 获取颜色值 r1, g1, b1 = self.color1.red(), self.color1.green(), self.color1.blue() r2, g2, b2 = self.color2.red(), self.color2.green(), self.color2.blue() print("r1, g1, b1",r1, g1, b1) print("r2, g2, b2",r2, g2, b2) ''' r1, g1, b1 0 255 255 r2, g2, b2 255 170 0 ''' # 转换为Lab颜色空间 lab1 = self.rgb_to_lab(r1, g1, b1) lab2 = self.rgb_to_lab(r2, g2, b2) # 计算CIEDE2000色差 delta_e = self.delta_e_cie2000(lab1, lab2) # 计算相似度百分比(使用指数衰减函数,更符合人眼感知) # ΔE < 1:人眼无法区分 # ΔE < 2.3:微小差异 # ΔE < 10:可察觉但相似的差异 # ΔE > 10:明显不同的颜色 # similarity_percent = 100 * math.exp(-delta_e / 5) #严格标准(如印刷品控):用指数衰减 exp(-delta_e/5) 或更严苛的 similarity_percent = max(0, 100 - delta_e * 5) ''' # 原代码(过于严苛) similarity_percent = 100 * math.exp(-delta_e / 5) # 16.89% # 修正后(更合理) similarity_percent = 100 * math.exp(-delta_e / 8) # 约 33% # 或 similarity_percent = 100 * math.exp(-delta_e / 10) # 约 41% # 或直接用线性 similarity_percent = max(0, 100 - delta_e * 5) # 55.5%,与colour-science一致 ''' # 更新相似度条 self.updateSimilarityBar(similarity_percent) # 生成结果文本 hex1 = self.color1.name().upper() hex2 = self.color2.name().upper() result_text = f""" <div style='font-size: 14px;'> <b>颜色1:</b> RGB({r1}, {g1}, {b1}) HEX: {hex1}<br> <b>颜色2:</b> RGB({r2}, {g2}, {b2}) HEX: {hex2}<br><br> <b>CIEDE2000 色差 (ΔE00):</b> {delta_e:.4f}<br> <b>相似度:</b> {similarity_percent:.2f}%<br><br> <b>色差解读:</b><br> """ if delta_e < 1: result_text += "• ΔE < 1: 人眼无法区分差异<br>" result_text += "• 这两种颜色看起来几乎相同" elif delta_e < 2.3: result_text += "• ΔE < 2.3: 微小差异,只有专家能注意到<br>" result_text += "• 颜色非常相似" elif delta_e < 10: result_text += f"• ΔE < 10: 可察觉差异<br>" result_text += "• 颜色相似但可区分" else: result_text += "• ΔE ≥ 10: 明显不同的颜色<br>" result_text += "• 人眼能轻易区分这两种颜色" self.result_label.setText(result_text) except Exception as e: self.result_label.setText(f"<span style='color: red;'>计算出错: {str(e)}</span>") def updateSimilarityBar(self, similarity_percent): """更新相似度进度条""" width = int(similarity_percent * 3) # 3px per percent width = max(0, min(width, 300)) # 限制在0-300px之间 # 根据相似度选择颜色 if similarity_percent >= 80: color = "#4CAF50" # 绿色 elif similarity_percent >= 60: color = "#FFC107" # 黄色 elif similarity_percent >= 40: color = "#FF9800" # 橙色 else: color = "#F44336" # 红色 self.similarity_bar.setStyleSheet(f""" QLabel {{ border: 1px solid #ccc; border-radius: 10px; background-color: #f0f0f0; }} QLabel::after {{ content: ''; position: absolute; left: 0; top: 0; height: 100%; width: {width}px; background-color: {color}; border-radius: 10px; }} """) self.similarity_bar.setText(f"相似度: {similarity_percent:.1f}%") if __name__ == '__main__': app = QApplication(sys.argv) ex = ColorSimilarityApp() ex.show() sys.exit(app.exec_()) ''' 严格标准(如印刷品控):用指数衰减 exp(-delta_e/5) 或更严苛的 宽松标准(如UI设计):用线性 100 - delta_e * 5 平衡标准(推荐):用 max(0, 100 - delta_e * 6) 或分段函数 ''' '''' ΔE < 1: "人眼无法区分" ΔE < 2.3: "微小差异" ΔE < 10: "可察觉差异" ΔE ≥ 10: "明显不同" '''


调用包实现ciede2000
import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel, QColorDialog from PyQt5.QtGui import QColor, QPalette from PyQt5.QtCore import Qt import numpy as np import colour class ColorSimilarityApp(QWidget): def __init__(self): super().__init__() self.color1 = QColor(255, 0, 0) # 默认红色 self.color2 = QColor(0, 255, 0) # 默认绿色 self.initUI() def initUI(self): self.setWindowTitle('颜色相似度计算') self.setGeometry(300, 300, 400, 300) # 创建颜色显示区域 self.color1_label = QLabel('颜色1') self.color1_label.setAutoFillBackground(True) self.color1_label.setFixedHeight(50) self.updateColorLabel(self.color1_label, self.color1) self.color2_label = QLabel('颜色2') self.color2_label.setAutoFillBackground(True) self.color2_label.setFixedHeight(50) self.updateColorLabel(self.color2_label, self.color2) # 创建按钮 self.btn1 = QPushButton('选择颜色1') self.btn1.clicked.connect(self.chooseColor1) self.btn2 = QPushButton('选择颜色2') self.btn2.clicked.connect(self.chooseColor2) # 计算按钮 self.calc_btn = QPushButton('计算相似度') self.calc_btn.clicked.connect(self.calculateSimilarity) # 结果显示 self.result_label = QLabel('相似度: ') self.result_label.setAlignment(Qt.AlignCenter) # 布局 vbox = QVBoxLayout() vbox.addWidget(self.color1_label) vbox.addWidget(self.btn1) vbox.addWidget(self.color2_label) vbox.addWidget(self.btn2) vbox.addWidget(self.calc_btn) vbox.addWidget(self.result_label) self.setLayout(vbox) def updateColorLabel(self, label, color): palette = label.palette() palette.setColor(QPalette.Window, color) label.setPalette(palette) label.setText(f'RGB({color.red()}, {color.green()}, {color.blue()})') label.setAlignment(Qt.AlignCenter) def chooseColor1(self): color = QColorDialog.getColor(self.color1, self, '选择颜色1') if color.isValid(): self.color1 = color self.updateColorLabel(self.color1_label, color) def chooseColor2(self): color = QColorDialog.getColor(self.color2, self, '选择颜色2') if color.isValid(): self.color2 = color self.updateColorLabel(self.color2_label, color) def calculateSimilarity(self): # 将QColor转换为归一化RGB (0-1范围) rgb1 = np.array([self.color1.red(), self.color1.green(), self.color1.blue()]) / 255.0 rgb2 = np.array([self.color2.red(), self.color2.green(), self.color2.blue()]) / 255.0 # 使用colour-science进行RGB到Lab的转换 # 首先将sRGB转换为XYZ,再转换为Lab (使用D65白点) lab1 = colour.XYZ_to_Lab(colour.sRGB_to_XYZ(rgb1)) lab2 = colour.XYZ_to_Lab(colour.sRGB_to_XYZ(rgb2)) # 计算CIEDE2000色差 delta_e = colour.delta_E(lab1, lab2, method='CIE 2000') # 显示结果 similarity = max(0, 100 - delta_e * 5) # 简单的相似度百分比转换 self.result_label.setText(f'CIEDE2000色差: {delta_e:.4f}\n' f'相似度评分: {similarity:.1f}%\n' f'(ΔE<1:几乎相同, 1-3:细微差别, 3-10:明显差别, >10:完全不同)') if __name__ == '__main__': app = QApplication(sys.argv) ex = ColorSimilarityApp() ex.show() sys.exit(app.exec_()) ''' 100 * exp(-delta_e / 5) 指数衰减,ΔE越大相似度下降越快 colour-science 代码 100 - delta_e * 5 线性递减,可能为负数 '''

小结:本文简要介绍了ciede2000(大部分来自AI概述),最后给出了代码实现与调用包实现ciede2000算法。最后,判断两种颜色是否以人的眼睛为标准去判断相似使用ciede2000去计算色差是绝对目前最好的选择。
关于上面那个小工具也打包好了,放下面网盘了。
通过网盘分享的文件:ColorSimilarityApp.exe
链接: https://pan.baidu.com/s/1Q4d0BXxu-yTQrZS0frtW8w 提取码: 2m73
若存在不足或错误之处,欢迎指出!

浙公网安备 33010602011771号