class EnhancedColorDetector:
"""增强版颜色检测器,支持精确颜色匹配和调试"""
def __init__(self, tolerance=30):
# 核心属性初始化
self.tolerance = tolerance
self.logger = logging.getLogger('EnhancedCellColorDetector')
# 颜色定义(包含所有已知颜色)
self.known_colors = {
"猩红": "E54C5E",
"巧克力黄": "F8CBAD",
"钢蓝": "B4C6E7",
"无填充": None
}
# 颜色匹配优先级(修复缺失属性)
self.color_priority = [
("猩红", "E54C5E"),
("巧克力黄", "F8CBAD"),
("钢蓝", "B4C6E7")
]
# 统计属性
self.color_stats = defaultdict(int)
self.color_matches = defaultdict(int)
self.color_mismatches = defaultdict(int)
# 缓存属性
self.color_cache = {}
self.per_cell_cache = {}
def reset_for_new_file(self):
"""为处理新文件重置状态"""
self.color_stats.clear()
self.color_matches.clear()
self.color_mismatches.clear()
self.color_cache.clear()
self.logger.info("颜色检测器已重置,准备处理新文件")
self.per_cell_cache.clear()
def hex_to_rgb(self, hex_color):
"""将十六进制颜色转换为RGB元组,支持多种格式"""
try:
if not hex_color:
return (0, 0, 0)
hex_str = str(hex_color).strip().upper().replace("#", "")
# 检查缓存
if hex_str in self.color_cache:
return self.color_cache[hex_str]
# 处理带Alpha通道的颜色
if len(hex_str) == 8:
# 提取Alpha值
alpha = int(hex_str[0:2], 16) / 255.0
# 提取RGB分量
r = int(hex_str[2:4], 16)
g = int(hex_str[4:6], 16)
b = int(hex_str[6:8], 16)
# 混合白色背景 (255,255,255)
# 使用更精确的混合算法
r = int(r * alpha + 255 * (1 - alpha) + 0.5)
g = int(g * alpha + 255 * (1 - alpha) + 0.5)
b = int(b * alpha + 255 * (1 - alpha) + 0.5)
result = (r, g, b)
self.color_cache[hex_str] = result
return result
# 处理标准6位HEX
if len(hex_str) == 6:
result = (
int(hex_str[0:2], 16),
int(hex_str[2:4], 16),
int(hex_str[4:6], 16)
)
self.color_cache[hex_str] = result
return result
# 处理3位简写HEX
if len(hex_str) == 3:
result = (
int(hex_str[0]*2, 16),
int(hex_str[1]*2, 16),
int(hex_str[2]*2, 16)
)
self.color_cache[hex_str] = result
return result
return (0, 0, 0)
except Exception as e:
self.logger.error(f"颜色转换失败: {hex_color} - {str(e)}")
return (0, 0, 0)
def rgb_to_hex(self, rgb):
"""RGB元组转换为HEX字符串"""
if not rgb or len(rgb) != 3:
return None
return f"{rgb[0]:02X}{rgb[1]:02X}{rgb[2]:02X}"
def color_distance(self, color1, color2):
"""
计算两个颜色之间的感知距离(改进的CIE94公式)
参考: https://en.wikipedia.org/wiki/Color_difference
"""
if not color1 or not color2:
return float('inf') # 无穷大表示完全不匹配
# 使用缓存提高性能
cache_key = f"{color1}_{color2}"
if cache_key in self.color_cache:
return self.color_cache[cache_key]
# 转换为RGB
r1, g1, b1 = self.hex_to_rgb(color1)
r2, g2, b2 = self.hex_to_rgb(color2)
# 转换为Lab颜色空间(更符合人眼感知)
lab1 = self.rgb_to_lab((r1, g1, b1))
lab2 = self.rgb_to_lab((r2, g2, b2))
# 计算CIE94颜色差异
L1, a1, b1 = lab1
L2, a2, b2 = lab2
delta_L = L1 - L2
delta_a = a1 - a2
delta_b = b1 - b2
c1 = math.sqrt(a1*a1 + b1*b1)
c2 = math.sqrt(a2*a2 + b2*b2)
delta_c = c1 - c2
delta_h = math.sqrt(max(delta_a*delta_a + delta_b*delta_b - delta_c*delta_c, 0))
sl = 1.0
kc = 1.0
kh = 1.0
sc = 1.0 + 0.045 * c1
sh = 1.0 + 0.015 * c1
distance = math.sqrt(
(delta_L/(sl))**2 +
(delta_c/(sc*kc))**2 +
(delta_h/(sh*kh))**2
)
self.color_cache[cache_key] = distance
return distance
def rgb_to_lab(self, rgb):
"""将RGB颜色转换为Lab颜色空间"""
# 先将RGB转换为XYZ
r, g, b = [x / 255.0 for x in rgb]
# 伽马校正
r = self.gamma_correction(r)
g = self.gamma_correction(g)
b = self.gamma_correction(b)
# 转换为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白点参考值
ref_x = 95.047
ref_y = 100.000
ref_z = 108.883
# 标准化
x = x / ref_x
y = y / ref_y
z = z / ref_z
# 转换为Lab
x = self.lab_transform(x)
y = self.lab_transform(y)
z = self.lab_transform(z)
L = max(0, 116 * y - 16)
a = 500 * (x - y)
b = 200 * (y - z)
return (L, a, b)
def gamma_correction(self, value):
"""伽马校正"""
if value > 0.04045:
return math.pow((value + 0.055) / 1.055, 2.4)
else:
return value / 12.92
def lab_transform(self, t):
"""Lab转换函数"""
if t > 0.008856:
return math.pow(t, 1/3)
else:
return (7.787 * t) + (16/116)
def is_no_fill(self, cell):
"""检查单元格是否无填充(增强版)"""
try:
# 检查单元格是否有填充定义
if not hasattr(cell, 'fill') or not cell.fill:
return True
# 检查填充类型
if hasattr(cell.fill, 'fill_type') and cell.fill.fill_type is None:
return True
# 检查特定填充类型
if cell.fill.fill_type == 'none':
return True
# 检查前景色是否自动(默认)
if (hasattr(cell.fill, 'fgColor') and
hasattr(cell.fill.fgColor, 'type') and
cell.fill.fgColor.type == 'auto'):
return True
# 检查背景色是否自动(默认)
if (hasattr(cell.fill, 'bgColor') and
hasattr(cell.fill.bgColor, 'type') and
cell.fill.bgColor.type == 'auto'):
return True
# 检查是否有实际颜色值
if hasattr(cell.fill, 'fgColor') and cell.fill.fgColor.rgb:
return False
if hasattr(cell.fill, 'bgColor') and cell.fill.bgColor.rgb:
return False
return True
except Exception as e:
self.logger.error(f"检查无填充失败: {str(e)}")
return True
def get_cell_color(self, cell):
"""获取单元格填充颜色的十六进制表示(增强版)"""
try:
# 检查是否无填充
if self.is_no_fill(cell):
return None
color_str = None
# 尝试获取前景色
if hasattr(cell.fill, 'fgColor') and cell.fill.fgColor:
if hasattr(cell.fill.fgColor, 'rgb') and cell.fill.fgColor.rgb:
color_str = str(cell.fill.fgColor.rgb).upper()
# 如果前景色无效,尝试获取背景色
if not color_str and hasattr(cell.fill, 'bgColor') and cell.fill.bgColor:
if hasattr(cell.fill.bgColor, 'rgb') and cell.fill.bgColor.rgb:
color_str = str(cell.fill.bgColor.rgb).upper()
self.logger.info(f"获取到背景色: {color_str}")
return color_str
except Exception as e:
self.logger.error(f"获取填充颜色失败: {str(e)}")
return None
def normalize_color(self, color_str):
"""规范化颜色字符串 - 优化版(修复Alpha处理)"""
if not color_str:
return None
# 转换为字符串并去除空格和#
color_str = str(color_str).strip().upper().replace("#", "")
# 修复: 处理带Alpha通道的颜色 (8位HEX)
if len(color_str) == 8:
# 提取Alpha值
alpha_hex = color_str[:2]
rgb_hex = color_str[2:]
try:
alpha = int(alpha_hex, 16) / 255.0
except ValueError:
return rgb_hex # 无法解析透明度,当作不透明处理
# 透明度低于50%当作无填充
if alpha < 0.5:
return None
# 透明度高于50%时进行混合计算
r = int(rgb_hex[0:2], 16)
g = int(rgb_hex[2:4], 16)
b = int(rgb_hex[4:6], 16)
# 混合白色背景 (更精确的混合算法)
r = int(r * alpha + 255 * (1 - alpha))
g = int(g * alpha + 255 * (1 - alpha))
b = int(b * alpha + 255 * (1 - alpha))
return f"{r:02X}{g:02X}{b:02X}"
# 4. 其他格式直接返回原值
return color_str
def match_color(self, hex_color):
"""
匹配颜色名称,返回最接近的已知颜色(修复互斥问题)
"""
if not hex_color:
return "无填充"
# 修复: 使用互斥优先级匹配
min_distance = float('inf')
closest_name = "其他"
for name, target in self.color_priority:
distance = self.color_distance(hex_color, target)
if distance < min_distance:
min_distance = distance
closest_name = name
# 高置信度匹配直接返回
if distance < 5: # 非常接近
return closest_name
return closest_name
def is_specific_color(self, cell, target_color, tolerance=None, log_details=False):
"""
检查单元格是否为特定颜色,允许容差
:param cell: 单元格对象
:param target_color: 目标颜色HEX
:param tolerance: 容差值
:param log_details: 是否记录匹配详情
:return: 是否匹配
"""
# 使用单元格坐标作为缓存键
cell_key = f"{cell.coordinate}_{target_color}"
if cell_key in self.per_cell_cache:
return self.per_cell_cache[cell_key]
if tolerance is None:
tolerance = self.tolerance
# 获取单元格颜色(原始值)
raw_cell_color = self.get_cell_color(cell)
# 规范化单元格颜色和目标颜色
norm_cell_color = self.normalize_color(raw_cell_color) if raw_cell_color else None
norm_target_color = self.normalize_color(target_color) if target_color else None
# 统计颜色出现次数
self.color_stats[raw_cell_color] += 1
if not norm_cell_color:
if log_details:
self.logger.debug(f"无填充单元格")
return False
if not norm_target_color:
if log_details:
self.logger.debug(f"未提供目标颜色")
return False
# 当容差为0时,直接比较规范化后的颜色值
# 修复: 添加互斥检查
matched = False
if tolerance == 0:
matched = (norm_cell_color == norm_target_color)
else:
distance = self.color_distance(norm_cell_color, norm_target_color)
matched = distance <= tolerance
# 修复: 检查是否更接近其他颜色
if matched:
closest_name = self.match_color_name(norm_cell_color)
if closest_name != next((n for n, c in self.color_priority if c == target_color), ""):
matched = False # 虽然匹配当前颜色,但有更接近的颜色
if log_details:
self.logger.warning(f"颜色冲突: 目标 {target_color} 匹配,但更接近 {closest_name}")
# 缓存结果
self.per_cell_cache[cell_key] = matched
# 记录匹配详情 - 使用原始颜色值显示
if log_details:
actual_rgb = self.hex_to_rgb(norm_cell_color)
target_rgb = self.hex_to_rgb(norm_target_color)
closest_name = self.match_color_name(norm_cell_color)
log_msg = (f"颜色匹配: 原始 #{raw_cell_color} -> 规范 #{norm_cell_color}({actual_rgb}) vs "
f"目标 #{norm_target_color}({target_rgb}) - "
f"容差: {tolerance}")
if tolerance != 0:
log_msg += f", 距离: {distance:.2f} {'≤' if matched else '>'} {tolerance}"
if matched:
self.logger.debug(log_msg)
self.color_matches[target_color] += 1
else:
self.logger.warning(log_msg + f" | 最接近: {closest_name}")
self.color_mismatches[target_color] += 1
return matched
def get_color_stats(self):
"""获取颜色统计信息"""
stats = {
"total_cells": sum(self.color_stats.values()),
"unique_colors": len(self.color_stats),
"color_distribution": dict(self.color_stats),
"matches": dict(self.color_matches),
"mismatches": dict(self.color_mismatches)
}
return stats
def generate_color_report(self):
"""生成颜色分析报告"""
report = ["颜色分析报告:"]
report.append(f"总单元格数: {sum(self.color_stats.values())}")
report.append(f"唯一颜色数: {len(self.color_stats)}")
report.append(f"颜色匹配容差: {self.tolerance}")
if self.color_stats:
report.append("\n颜色分布:")
for color, count in sorted(self.color_stats.items(), key=lambda x: x[1], reverse=True):
if color is None:
report.append(f" 无填充: {count}")
else:
rgb = self.hex_to_rgb(color)
closest = self.match_color_name(color)
report.append(f" #{color} (RGB: {rgb}) - {count}次 - 分类: {closest}")
if self.color_matches:
report.append("\n成功匹配:")
for color, count in self.color_matches.items():
name = next((k for k, v in self.known_colors.items() if v == color), "未知")
report.append(f" {name} (#{color}): {count}次")
if self.color_mismatches:
report.append("\n匹配失败:")
for color, count in self.color_mismatches.items():
name = next((k for k, v in self.known_colors.items() if v == color), "未知")
report.append(f" {name} (#{color}): {count}次")
# 添加颜色匹配率统计
if self.color_matches or self.color_mismatches:
total_matches = sum(self.color_matches.values())
total_mismatches = sum(self.color_mismatches.values())
total_attempts = total_matches + total_mismatches
match_rate = (total_matches / total_attempts * 100) if total_attempts > 0 else 0
report.append("\n匹配率统计:")
report.append(f"总匹配尝试: {total_attempts}")
report.append(f"成功匹配: {total_matches} ({match_rate:.2f}%)")
report.append(f"匹配失败: {total_mismatches} ({100 - match_rate:.2f}%)")
return "\n".join(report)
def add_custom_color(self, name, hex_value):
"""添加自定义颜色"""
if not hex_value or len(hex_value) not in (3, 6, 8):
self.logger.warning(f"无效的颜色值: {hex_value}")
return False
# 规范化颜色值
hex_value = self.normalize_color(hex_value)
if not hex_value:
return False
self.known_colors[name] = hex_value
self.logger.info(f"已添加自定义颜色: {name} (#{hex_value})")
return True
def auto_calibrate_tolerance(self):
"""自动校准颜色匹配容差"""
if not self.color_stats:
self.logger.warning("没有足够的颜色数据来自动校准")
return
# 计算所有颜色与目标颜色的平均距离
distances = []
for color in self.color_stats:
if color is None:
continue
min_dist = float('inf')
for target in self.known_colors.values():
if target is None:
continue
dist = self.color_distance(color, target)
if dist < min_dist:
min_dist = dist
if min_dist < float('inf'):
distances.append(min_dist)
if not distances:
return
# 计算平均距离和标准差
avg_dist = sum(distances) / len(distances)
std_dev = math.sqrt(sum((d - avg_dist)**2 for d in distances) / len(distances))
# 设置新的容差(平均值 + 1.5倍标准差)
new_tolerance = min(100, max(10, avg_dist + 1.5 * std_dev))
self.tolerance = new_tolerance
self.logger.info(f"自动校准完成: 新容差 = {new_tolerance:.2f}")
return new_tolerance
给出完整的修改后的类,并且我建议固定容差为1
最新发布