如何在Python中基于CIELAB等色彩空间精准聚合像素色彩值?
初始背景
我正尝试从图像像素中聚合色彩信息,在Python环境下,我有若干由同一相机拍摄、尺寸相同的.jpg图像,可读取为Python支持的任意色彩空间(RGB、HSL、HSV、CIELAB、CMYK等)。每张图像都有由二值掩码标记的特定分割区域。确定色彩空间和二值掩码后,我尝试提取分割对象的某种“平均色彩”,再将该图像所有分割对象的平均色彩聚合为单一色彩值。这个看似简单的问题因以下要点增加了复杂度:
- 使用均值聚合时,加减运算需具备数学意义,确保色彩空间中任意位置的几何距离对应相同的人眼感知色差。例如,RGB均值往往会生成偏棕的无意义色彩,如同随意混合多种颜料得到的棕色。
- 使用中位数聚合时,色彩空间需遵循特定逻辑排序才能提取中位数。例如在RGB中无法直接取中位数,因为无法判定[120,100,127]和[120,111,126]的先后顺序。
- JPG压缩格式及相机传感器可能导致捕获的图像色彩与分割对象的真实色彩存在偏差。
图像示例
下图是我分析的岩石示例(已去除背景以便可视化),以及目标像素的RGB均值、中位数和众数结果,右上角展示了这些RGB值。在地质学和矿业领域,传统上使用孟塞尔土壤色卡(参考《如何使用孟塞尔土壤色卡》)来判定材料的颜色、质量及属性。如今使用数码彩色相机时,RGB转孟塞尔色彩并不直接,因为缺少预测孟塞尔色卡中色相、明度和彩度的信息,目前已有基于CNN从RGB图像预测孟塞尔色彩的研究(参考《RGB图像到孟塞尔土壤色卡的转换》)。我的核心需求是:通过Python分析.jpg图像,找到一种色彩空间聚合技术,模拟孟塞尔土壤色卡的“查找表”。最终,针对N个像素的N个色彩值,如何在CIELAB空间中聚合这些值,建立与人眼感知相关的色差(CIELAB色差),从而匹配孟塞尔土壤色卡中最接近的颜色?

后续问题
基于上述背景,我有以下相关问题:
- **若结果需符合人眼色彩感知,用于单通道色彩信息聚合的最佳色彩空间是什么?**我倾向于选择CIELAB这类均匀且设备无关的色彩空间。
- **针对问题1所选的色彩空间,哪种数学运算适合色彩信息聚合?**均值、中位数、众数还是其他?
- 我是否遗漏了其他需要考虑的要点?
代码示例
我进行的部分尝试可总结为以下函数:
def aggregate_rgb(binary_mask, image): # Change bgr to rgb rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # Convert mask to boolean binary_mask = binary_mask.astype(bool) # Extract region inside the mask masked_image = rgb_image[binary_mask] # Calculate mode RGB values mode_rgb = np.zeros(3, dtype=np.uint8) # Reshape the masked image to a 2D array where each row represents a pixel and each column represents a channel reshaped_masked_image = masked_image.reshape(-1, 3) # Compute mode for each channel simultaneously channel_modes = mode(reshaped_masked_image, axis=0) # Calculate the custom RGB average num_pixels = np.sum(binary_mask) r = np.sum(masked_image[:, 0].astype('uint16') ** 2) g = np.sum(masked_image[:, 1].astype('uint16') ** 2) b = np.sum(masked_image[:, 2].astype('uint16') ** 2) # Calculate aggregate of RGB values mean_rgb = np.round(np.mean(masked_image, axis=0)).astype('uint8') median_rgb = np.round(np.median(masked_image, axis=0)).astype('uint8') mode_rgb = np.round(channel_modes.mode).astype('uint8') custom_mean_rgb = np.sqrt(np.array([r, g, b]) / num_pixels).astype('uint8') return mean_rgb, median_rgb, mode_rgb, custom_mean_rgb
这些尝试的主要问题在于所选的RGB色彩空间,生成的偏棕色彩参考价值低。我也通过cv2.cvtColor(image, cv2.COLOR_BGR2Lab)读取CIELAB色彩空间的图像,但不确定是否应单独对L、A、B通道取均值、中位数或众数,还是需要遵循特定公式(考虑到L、A、B通道在几何上构成一个球体)。
我使用cv2库进行图像显示和色彩空间读取,Python版本为3.9.13,运行于Windows 10系统的Visual Studio Code中。
参考资料
我在提问前查阅的相关内容:
- 《MinutePhysics:计算机色彩的缺陷》
- 《Sighack:正确的色彩平均方法》
- 《维基百科:色彩模型》
- Stack Overflow:寻找相似色彩的最佳色彩空间
- Stack Overflow:CIELAB色彩空间中超像素的平均色彩
解答
1. 符合人眼感知的最佳色彩空间
你的倾向是正确的,CIELAB(Lab)* 是当前最适合人眼感知一致性需求的色彩空间:
- 它是设备无关的,相同的Lab*值在不同显示设备或相机下代表的色彩一致;
- 空间内的欧氏距离与人眼感知的色差高度匹配,直接解决了你提到的“均值运算需对应感知色差”的问题;
- 若要更贴合孟塞尔色卡的逻辑(色相、明度、彩度),可将CIELAB转换为CIELCh(LCh°),其中L对应明度,C对应彩度,h°对应色相,和孟塞尔的维度一一对应,后续聚合时能更直观地匹配色卡的评判逻辑。
2. CIELAB空间下的色彩聚合方式
针对CIELAB(或CIELCh),推荐以下几种聚合策略,可根据场景选择:
- 加权均值(首选):直接对L*、a*、b三个通道分别计算均值(若有需要,可根据像素置信度或掩码权重加权)。由于CIELAB的感知均匀性,这种均值会对应人眼感知的“平均色彩”,不会出现RGB均值的偏棕问题。如果使用CIELCh,对L、C*、h°取均值时需注意色相h°是环形数据(0°=360°),计算均值时要先转换为笛卡尔坐标(sin(h°)、cos(h°))取均值后再转回角度;
- 中位数:适合存在极端离群像素(如噪声、反光点)的场景。对L*、a*、b通道分别取中位数,能有效排除异常值的干扰。CIELAB单通道的数值变化对应感知上的单一维度变化(比如L从黑到白,a*从绿到红),因此单通道中位数是可行的;
- 众数:适合色彩分布高度集中的场景(如单一均匀岩石),但如果色彩分布分散,众数参考价值很低,很难找到完全相同的像素值;
- 感知中心聚类:若分割区域色彩分布复杂(如岩石存在多种纹理色彩),可在CIELAB空间中用K-means聚类找到主色彩,再选择占比最高的聚类中心作为代表色,这更贴近孟塞尔色卡“匹配最接近的主导色彩”的逻辑。
3. 遗漏的关键要点
- 色彩校准:相机传感器和JPG压缩的色彩偏差是核心问题,必须先对相机进行色彩校准。使用标准色卡拍摄后,建立相机RGB到CIELAB的校准映射,修正原始图像的色彩偏差,否则后续聚合结果会偏离真实色彩;
- 孟塞尔色卡数字化映射:要实现“模拟孟塞尔查找表”,需先获取孟塞尔色卡的CIELAB(或CIELCh)数值库,然后将聚合得到的色彩与库中所有色卡值计算CIEDE2000色差(比简单欧氏距离更准确的感知色差公式),选择色差最小的色卡作为匹配结果;
- 掩码准确性:二值掩码的边缘精度会影响像素提取,若掩码包含背景或遗漏目标区域,会引入无关色彩干扰聚合结果,建议对掩码进行形态学处理(膨胀/腐蚀)或边缘细化;
- Gamma校正:转换色彩空间前,要确保图像的RGB值是线性的(解除Gamma压缩),因为OpenCV读取的RGB图像通常是经过Gamma校正的非线性值,直接转换到CIELAB会导致误差。可先将RGB值从sRGB转换为线性RGB,再转换到CIELAB。
内容的提问来源于stack exchange,提问作者JaimeCorton

