用Python绘制CIE 1931色度图从光谱数据到色彩可视化实战当设计师抱怨显示器色域太窄时他们其实在谈论什么为什么专业摄影需要Adobe RGB而不是sRGB要真正理解这些问题的本质我们需要回到1931年那个改变色彩科学的坐标系——CIE 1931色度图。本文将带你用Python代码完整复现这个神奇的色彩地图通过代码理解人眼如何感知颜色边界。1. 准备工作理解色彩科学基础在开始编码之前我们需要明确几个关键概念。CIE 1931色度图本质上是一个二维投影它将人眼可见的所有颜色压缩到一个马蹄形区域内。这个马蹄形的边缘被称为光谱轨迹对应着单一波长的纯色光从380nm的紫色到780nm的红色。注意色度图的x,y坐标并不直接对应物理波长而是经过标准观察者函数加权计算的结果我们需要准备以下Python库import numpy as np import matplotlib.pyplot as plt from scipy.interpolate import interp1d from matplotlib.patches import Polygon2. 获取标准观察者数据CIE 1931标准观察者数据是构建色度图的基础它定义了人眼对可见光谱的响应曲线。我们可以直接从CIE官网获取这些数据或者使用科学计算库中预置的值# CIE 1931 2度标准观察者数据 wavelengths np.arange(360, 831, 1) # 360-830nm x_bar np.array([...]) # 填入实际x曲线数据 y_bar np.array([...]) # 填入实际y曲线数据 z_bar np.array([...]) # 填入实际z曲线数据为了确保数据准确性建议使用线性插值处理原始数据# 创建插值函数 x_interp interp1d(wavelengths, x_bar, kindlinear) y_interp interp1d(wavelengths, y_bar, kindlinear) z_interp interp1d(wavelengths, z_bar, kindlinear) # 生成更密集的采样点 dense_wl np.linspace(360, 830, 1000) x_dense x_interp(dense_wl) y_dense y_interp(dense_wl) z_dense z_interp(dense_wl)3. 计算色度坐标与绘制光谱轨迹色度坐标(x,y)的计算公式看似简单却蕴含着色彩科学的精髓$$ x \frac{X}{XYZ}, \quad y \frac{Y}{XYZ} $$在代码中实现这个转换def calculate_xy(X, Y, Z): 计算色度坐标 sum_xyz X Y Z x X / sum_xyz y Y / sum_xyz return x, y # 计算光谱轨迹的色度坐标 spectral_x [] spectral_y [] for wl in dense_wl: X x_interp(wl) Y y_interp(wl) Z z_interp(wl) x, y calculate_xy(X, Y, Z) spectral_x.append(x) spectral_y.append(y)现在可以绘制出著名的马蹄形光谱轨迹plt.figure(figsize(10, 8)) plt.plot(spectral_x, spectral_y, colorblack, linewidth2) plt.title(CIE 1931 Chromaticity Diagram) plt.xlabel(x) plt.ylabel(y) plt.grid(True)4. 添加普朗克轨迹与常见色域普朗克轨迹黑体辐射轨迹展示了不同温度下黑体辐射在色度图上的位置。我们可以通过普朗克公式计算这些点def planckian_locus(temperature): 计算指定温度下的黑体辐射色度坐标 # 实现普朗克公式计算 # 返回x,y坐标 return x, y # 绘制几个典型温度点 temperatures [2000, 3000, 4000, 5000, 6500, 10000] for temp in temperatures: x, y planckian_locus(temp) plt.scatter(x, y, colorred) plt.text(x, y, f{temp}K, fontsize10)常见色域的三角形边界也是理解色彩空间的关键。以sRGB为例# sRGB色域顶点坐标 srgb_red (0.64, 0.33) srgb_green (0.30, 0.60) srgb_blue (0.15, 0.06) # 创建多边形并填充 srgb_triangle Polygon([srgb_red, srgb_green, srgb_blue], closedTrue, fillTrue, alpha0.2, colorblue) plt.gca().add_patch(srgb_triangle)同样方法可以添加Adobe RGB、DCI-P3等其他色域通过不同颜色和透明度区分它们。5. 色彩可视化技巧与优化为了让色度图更直观我们可以添加以下增强效果色彩填充在光谱轨迹内填充近似颜色from matplotlib.colors import LinearSegmentedColormap # 创建自定义色彩映射 points np.linspace(0, 1, 100) colors [...] # 根据色度图定义颜色 cmap LinearSegmentedColormap.from_list(cie_colormap, colors) # 填充色彩 plt.fill(spectral_x, spectral_y, colornone, edgecolornone) plt.gca().autoscale_view()关键波长标注标记几个重要波长点key_wavelengths [450, 520, 580, 620, 700] # nm for wl in key_wavelengths: idx np.argmin(np.abs(dense_wl - wl)) plt.scatter(spectral_x[idx], spectral_y[idx], colorblack) plt.text(spectral_x[idx], spectral_y[idx], f{wl}nm, fontsize10)白点标注标记D65等标准白点d65 (0.3127, 0.3290) plt.scatter(d65[0], d65[1], colorwhite, edgecolorblack, s100) plt.text(d65[0], d65[1], D65, hacenter, vacenter)6. 实际应用与扩展思考完成基础色度图后我们可以进一步探索几个实用方向色域覆盖率计算比较不同色域的面积def calculate_area(points): 计算多边形面积 x [p[0] for p in points] y [p[1] for p in points] return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) srgb_area calculate_area([srgb_red, srgb_green, srgb_blue]) adobe_rgb_area ... # 类似计算Adobe RGB面积 print(fsRGB覆盖了{srgb_area/visible_area*100:.1f}%可见色域)色彩转换验证检查RGB值在色度图中的位置def rgb_to_xy(r, g, b): 将RGB转换为色度坐标 # 实现转换矩阵计算 return x, y # 测试纯红色 x, y rgb_to_xy(1, 0, 0) plt.scatter(x, y, colorred, s100)色差计算评估两个颜色的感知差异def delta_e(x1, y1, x2, y2): 计算两个色度坐标之间的色差 return np.sqrt((x2-x1)**2 (y2-y1)**2)在显示器校准项目中这种可视化特别有用。当我们需要解释为什么某台显示器无法准确再现特定红色时直接展示该颜色在色度图上的位置与显示器色域边界的关系比任何口头解释都更有说服力。
别再只盯着sRGB了!用Python和Matplotlib亲手绘制CIE 1931色度图,理解色彩空间的边界
发布时间:2026/6/11 12:58:28
用Python绘制CIE 1931色度图从光谱数据到色彩可视化实战当设计师抱怨显示器色域太窄时他们其实在谈论什么为什么专业摄影需要Adobe RGB而不是sRGB要真正理解这些问题的本质我们需要回到1931年那个改变色彩科学的坐标系——CIE 1931色度图。本文将带你用Python代码完整复现这个神奇的色彩地图通过代码理解人眼如何感知颜色边界。1. 准备工作理解色彩科学基础在开始编码之前我们需要明确几个关键概念。CIE 1931色度图本质上是一个二维投影它将人眼可见的所有颜色压缩到一个马蹄形区域内。这个马蹄形的边缘被称为光谱轨迹对应着单一波长的纯色光从380nm的紫色到780nm的红色。注意色度图的x,y坐标并不直接对应物理波长而是经过标准观察者函数加权计算的结果我们需要准备以下Python库import numpy as np import matplotlib.pyplot as plt from scipy.interpolate import interp1d from matplotlib.patches import Polygon2. 获取标准观察者数据CIE 1931标准观察者数据是构建色度图的基础它定义了人眼对可见光谱的响应曲线。我们可以直接从CIE官网获取这些数据或者使用科学计算库中预置的值# CIE 1931 2度标准观察者数据 wavelengths np.arange(360, 831, 1) # 360-830nm x_bar np.array([...]) # 填入实际x曲线数据 y_bar np.array([...]) # 填入实际y曲线数据 z_bar np.array([...]) # 填入实际z曲线数据为了确保数据准确性建议使用线性插值处理原始数据# 创建插值函数 x_interp interp1d(wavelengths, x_bar, kindlinear) y_interp interp1d(wavelengths, y_bar, kindlinear) z_interp interp1d(wavelengths, z_bar, kindlinear) # 生成更密集的采样点 dense_wl np.linspace(360, 830, 1000) x_dense x_interp(dense_wl) y_dense y_interp(dense_wl) z_dense z_interp(dense_wl)3. 计算色度坐标与绘制光谱轨迹色度坐标(x,y)的计算公式看似简单却蕴含着色彩科学的精髓$$ x \frac{X}{XYZ}, \quad y \frac{Y}{XYZ} $$在代码中实现这个转换def calculate_xy(X, Y, Z): 计算色度坐标 sum_xyz X Y Z x X / sum_xyz y Y / sum_xyz return x, y # 计算光谱轨迹的色度坐标 spectral_x [] spectral_y [] for wl in dense_wl: X x_interp(wl) Y y_interp(wl) Z z_interp(wl) x, y calculate_xy(X, Y, Z) spectral_x.append(x) spectral_y.append(y)现在可以绘制出著名的马蹄形光谱轨迹plt.figure(figsize(10, 8)) plt.plot(spectral_x, spectral_y, colorblack, linewidth2) plt.title(CIE 1931 Chromaticity Diagram) plt.xlabel(x) plt.ylabel(y) plt.grid(True)4. 添加普朗克轨迹与常见色域普朗克轨迹黑体辐射轨迹展示了不同温度下黑体辐射在色度图上的位置。我们可以通过普朗克公式计算这些点def planckian_locus(temperature): 计算指定温度下的黑体辐射色度坐标 # 实现普朗克公式计算 # 返回x,y坐标 return x, y # 绘制几个典型温度点 temperatures [2000, 3000, 4000, 5000, 6500, 10000] for temp in temperatures: x, y planckian_locus(temp) plt.scatter(x, y, colorred) plt.text(x, y, f{temp}K, fontsize10)常见色域的三角形边界也是理解色彩空间的关键。以sRGB为例# sRGB色域顶点坐标 srgb_red (0.64, 0.33) srgb_green (0.30, 0.60) srgb_blue (0.15, 0.06) # 创建多边形并填充 srgb_triangle Polygon([srgb_red, srgb_green, srgb_blue], closedTrue, fillTrue, alpha0.2, colorblue) plt.gca().add_patch(srgb_triangle)同样方法可以添加Adobe RGB、DCI-P3等其他色域通过不同颜色和透明度区分它们。5. 色彩可视化技巧与优化为了让色度图更直观我们可以添加以下增强效果色彩填充在光谱轨迹内填充近似颜色from matplotlib.colors import LinearSegmentedColormap # 创建自定义色彩映射 points np.linspace(0, 1, 100) colors [...] # 根据色度图定义颜色 cmap LinearSegmentedColormap.from_list(cie_colormap, colors) # 填充色彩 plt.fill(spectral_x, spectral_y, colornone, edgecolornone) plt.gca().autoscale_view()关键波长标注标记几个重要波长点key_wavelengths [450, 520, 580, 620, 700] # nm for wl in key_wavelengths: idx np.argmin(np.abs(dense_wl - wl)) plt.scatter(spectral_x[idx], spectral_y[idx], colorblack) plt.text(spectral_x[idx], spectral_y[idx], f{wl}nm, fontsize10)白点标注标记D65等标准白点d65 (0.3127, 0.3290) plt.scatter(d65[0], d65[1], colorwhite, edgecolorblack, s100) plt.text(d65[0], d65[1], D65, hacenter, vacenter)6. 实际应用与扩展思考完成基础色度图后我们可以进一步探索几个实用方向色域覆盖率计算比较不同色域的面积def calculate_area(points): 计算多边形面积 x [p[0] for p in points] y [p[1] for p in points] return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) srgb_area calculate_area([srgb_red, srgb_green, srgb_blue]) adobe_rgb_area ... # 类似计算Adobe RGB面积 print(fsRGB覆盖了{srgb_area/visible_area*100:.1f}%可见色域)色彩转换验证检查RGB值在色度图中的位置def rgb_to_xy(r, g, b): 将RGB转换为色度坐标 # 实现转换矩阵计算 return x, y # 测试纯红色 x, y rgb_to_xy(1, 0, 0) plt.scatter(x, y, colorred, s100)色差计算评估两个颜色的感知差异def delta_e(x1, y1, x2, y2): 计算两个色度坐标之间的色差 return np.sqrt((x2-x1)**2 (y2-y1)**2)在显示器校准项目中这种可视化特别有用。当我们需要解释为什么某台显示器无法准确再现特定红色时直接展示该颜色在色度图上的位置与显示器色域边界的关系比任何口头解释都更有说服力。