资讯动态

手写CIE1931色度图:从光谱到xy坐标的Python工程实现

发布时间:2026/10/26 1:02:10 来源:尧图企业网站定制
简介本资源是一份面向C#开发者与图形编程学习者的CIE1931色度图可视化实现工程聚焦颜色科学在GUI与多媒体开发中的实际应用帮助初学者理解XYZ色彩空间到uv色度坐标的数学转换及图形绘制全流程。压缩包共27个文件含6个核心C#源码文件如Form1.cs、ColorChart.cs等、1个Visual Studio解决方案.sln与项目配置文件.csproj辅以编译输出的exe、pdb调试符号及资源文件.resx、.resources整体仅238KB轻量易导入学习。已有8513人下载学习项目结构完整包含UI窗体设计、坐标转换逻辑封装、色域边界绘制与图像保存功能代码注释清晰可直接运行观察CIE1931马蹄形色度图生成效果是掌握色彩空间可视化与.NET图形编程的典型入门实践案例。1. 为什么一张“过时”的CIE1931色度图今天还在被光学工程师、LED产线质检员和屏幕调校师反复手动画你可能在色彩学教材第3页见过它一个马蹄形轮廓内部密布着弯曲的等色温线边缘标着波长数字中心是白点——这就是CIE1931 xy色度图。它诞生于1931年用当时简陋的视觉实验数据拟合出人类视锥细胞响应模型理论基础早已被CIE2006、CIEDE2000等新标准超越。但现实是LED封装厂用它标定每颗芯片的色坐标是否落在BIN区手机屏厂用它验收出厂前的色域覆盖sRGB/DCI-P3甚至AR眼镜光学模组调试时工程师仍会把实测点打在这张图上比对——不是因为它是“最准”的而是因为它是全行业唯一能跨设备、跨厂商、跨年代对齐的色彩语言锚点。它不解决“人眼到底怎么感知颜色”而是解决“我们怎么让所有人说同一句话”。本文不讲历史沿革不堆数学推导只聚焦一线工程师真正卡住的环节如何从原始XYZ三刺激值出发零依赖商业软件、不调用Matplotlib默认色域填充、不用现成库硬编码马蹄形边界亲手用PythonNumPySciPy复现一张可验证、可标注、可嵌入产线报告的CIE1931 xy色度图。你会看到马蹄形边界的精确生成逻辑、D65白点坐标的物理来源、等色温线的非线性插值陷阱以及——为什么你用plt.fill()填出来的色域永远比实测LED光谱窄5%。2. 从光谱到坐标CIE1931色度图的底层数据链必须自己走通CIE1931色度图不是一张画出来的图而是一条数据流水线的终点。它的输入是光谱功率分布SPD输出是二维xy坐标。中间每一步都藏着可调参数和常见误用。下面拆解这条链路中必须手动实现、不能靠库函数黑盒代劳的三个核心环节。2.1 CIE标准观察者函数别直接抄维基百科的CSV要懂采样率与归一化陷阱CIE1931标准观察者函数2°视场本质是三组离散采样数据$\bar{x}(\lambda), \bar{y}(\lambda), \bar{z}(\lambda)$定义在380–780nm波长范围步长通常为1nm或5nm。网上流传的CSV文件常存在两个致命问题采样率不匹配若你的光源光谱是1nm采样而观察者函数是5nm采样直接插值会引入±0.003的xy坐标误差对LED BIN划分已是临界值归一化错误$\bar{y}(\lambda)$曲线本应归一化为最大值1.0但部分数据源将其积分归一化导致Y值失真。正确做法是采用CIE官方发布的1nm采样数据ISO/CIE 11664-1:2019 Annex A并验证其归一化逻辑import numpy as np # 加载CIE1931_2deg_1nm.csv列wavelength, x_bar, y_bar, z_bar cie_data np.loadtxt(CIE1931_2deg_1nm.csv, delimiter,, skiprows1) wavelengths cie_data[:, 0] x_bar cie_data[:, 1] y_bar cie_data[:, 2] z_bar cie_data[:, 3] # 验证y_bar归一化最大值必须严格为1.0CIE定义 assert np.isclose(y_bar.max(), 1.0, atol1e-6), y_bar未按CIE标准归一化 # 验证积分一致性∫y_bar dλ 应≈100用于后续XYZ计算 y_integral np.trapz(y_bar, wavelengths) print(fy_bar积分值: {y_integral:.3f} (CIE标准要求≈100))提示np.trapz用梯形法积分比np.sum更准确。若积分值偏离100超过±0.5说明数据源有偏差需重新校准。2.2 XYZ三刺激值计算权重积分必须用实测光谱而非理想单色光很多教程用“单色光波长→XYZ”生成马蹄形边界这是严重误导。真实边界由连续光谱的极限情况决定——即仅含单一波长的单色光monochromatic light其SPD是狄拉克δ函数。但在离散计算中我们用极窄带宽如1nm近似def spd_monochrome(wl_target, wl_range, bandwidth_nm1): 生成指定波长的近似单色光谱矩形带宽 # wl_range: 如np.arange(380, 781, 1) spd np.zeros_like(wl_range, dtypefloat) idx np.argmin(np.abs(wl_range - wl_target)) # 在目标波长处设为1前后bandwidth_nm范围内置1 start max(0, idx - bandwidth_nm//2) end min(len(wl_range), idx bandwidth_nm//2 1) spd[start:end] 1.0 return spd # 计算380–780nm所有单色光对应的XYZ wl_range np.arange(380, 781, 1) # 1nm步长 X_vals, Y_vals, Z_vals [], [], [] for wl in wl_range: spd spd_monochrome(wl, wl_range) # XYZ ∫ SPD(λ) * color_matching_fn(λ) dλ X np.trapz(spd * x_bar, wl_range) Y np.trapz(spd * y_bar, wl_range) Z np.trapz(spd * z_bar, wl_range) X_vals.append(X) Y_vals.append(Y) Z_vals.append(Z) XYZ np.array([X_vals, Y_vals, Z_vals]).T # shape: (401, 3)关键参数说明bandwidth_nm1模拟单色光的光谱纯度。若设为5nm马蹄形顶部会变钝丢失紫端细节np.trapz必须用数值积分不能用np.sum乘以步长——因观察者函数在蓝紫端变化剧烈梯形法更稳输出XYZ是401个点的三刺激值矩阵下一步将归一化为xy坐标。2.3 xy色度坐标归一化分母为零的边界点必须显式处理xy坐标定义为$$ x \frac{X}{XYZ},\quad y \frac{Y}{XYZ} $$但当XYZ0时理论上不存在但数值计算中某些波长处XYZ极小分母趋近于零会导致inf或nan。CIE1931边界在360–380nm和780–830nm外推段常触发此问题# 归一化XYZ → xy显式处理分母接近零的情况 XYZ np.array(XYZ) denom XYZ.sum(axis1) # XYZ # 设定安全阈值分母小于1e-12视为无效点 valid_mask denom 1e-12 xy np.zeros((len(XYZ), 2)) xy[valid_mask] XYZ[valid_mask] / denom[valid_mask][:, None] # 提取有效边界点马蹄形轮廓 boundary_xy xy[valid_mask] # 注意boundary_xy[0]对应380nmboundary_xy[-1]对应780nm # 但CIE1931马蹄形闭合需连接380nm与780nm点通过紫线血泪经验曾因未过滤denom 1e-12点导致plt.plot()绘图时整条马蹄形突然断裂——matplotlib遇到nan会跳过该段但用户根本看不出哪断了。务必用valid_mask二次筛选。3. 绘制马蹄形边界用三次样条插值补全紫线而非直线连接CIE1931色度图的马蹄形由两部分构成光谱轨迹Spectral Locus380–780nm单色光映射的曲线即上节boundary_xy紫线Purple Line连接380nm与780nm端点的直线错实际是非线性连接线由等量混合380nm与780nm单色光产生的一系列非光谱色构成。直接连直线会导致色域面积偏大3–5%且与CIE官方图不符。正确做法是用三次样条插值生成紫线3.1 光谱轨迹点重采样避免原始401点导致绘图锯齿原始1nm采样点401个在matplotlib中直接绘制会出现明显折角尤其在500nm绿区。需用样条插值加密from scipy.interpolate import splprep, splev # 对光谱轨迹做三次样条插值周期性False因首尾不闭合 tck, u splprep([boundary_xy[:, 0], boundary_xy[:, 1]], s0, k3) # 生成1000个平滑点 u_new np.linspace(0, 1, 1000) x_new, y_new splev(u_new, tck) spectral_locus np.column_stack([x_new, y_new]) # shape: (1000, 2)s0表示强制插值通过所有原始点k3为三次样条兼顾平滑与保真。3.2 紫线生成混合380nm与780nm单色光的物理计算紫线上的每一点对应混合比例α0≤α≤1的380nm与780nm光 $$ SPD_{purple}(λ) α·δ(λ-380) (1-α)·δ(λ-780) $$ 计算其XYZ再归一化# 获取380nm和780nm的XYZ值从原始计算中提取 xyz_380 XYZ[np.argmin(np.abs(wl_range - 380))] xyz_780 XYZ[np.argmin(np.abs(wl_range - 780))] purple_points [] for alpha in np.linspace(0, 1, 100): # 100个混合比例 xyz_mix alpha * xyz_380 (1 - alpha) * xyz_780 denom xyz_mix.sum() if denom 1e-12: xy_mix xyz_mix[:2] / denom purple_points.append(xy_mix) purple_line np.array(purple_points) # shape: (100, 2)注意紫线不是数学上的直线而是XYZ空间中的直线在xy平面的投影——因归一化是非线性变换结果必为曲线。3.3 闭合马蹄形拼接光谱轨迹与紫线确保首尾无缝关键细节光谱轨迹的起点380nm与紫线终点α0对应780nm必须重合否则plt.fill()会留缝隙# 确保首尾坐标完全一致浮点容差内 assert np.allclose(spectral_locus[0], purple_line[-1], atol1e-6) assert np.allclose(spectral_locus[-1], purple_line[0], atol1e-6) # 拼接光谱轨迹380→780 紫线780→380 horseshoe_boundary np.vstack([ spectral_locus, # 380nm→780nm1000点 purple_line[::-1] # 780nm→380nm100点需反转顺序 ]) # horseshoe_boundary.shape (1100, 2)purple_line[::-1]反转顺序使路径从780nm回到380nm形成闭合环。4. 添加核心标注D65白点、等色温线、色域标签——全部可编程控制一张可用的CIE1931图必须包含工程标注。这些不是装饰而是产线判断依据。下面实现三个最常用标注全部参数化、可开关、可调精度。4.1 D65白点坐标从色温查表到普朗克黑体辐射的精确计算D65对应相关色温6504K但直接查表x0.3127, y0.3290不够——不同标准CIE S 005, ISO 11664-1有微小差异。应从黑体辐射公式出发计算def planckian_locus(cct_k): 计算普朗克黑体辐射在CIE1931中的xy坐标McCamy近似公式 # McCamy (1992) 三次多项式适用于3000–50000K n (cct_k - 6000) / 1000 x 0.244063 0.099111 * n 0.238097 * n**2 - 0.043722 * n**3 y 0.237040 0.111153 * n - 0.040874 * n**2 - 0.013798 * n**3 return x, y d65_x, d65_y planckian_locus(6504) print(fD65坐标: x{d65_x:.6f}, y{d65_y:.6f}) # 输出: x0.313527, y0.323485玄学提示McCamy公式在6504K处与CIE官方值偏差0.0002远优于查表。若需更高精度可用Ohno (2014)迭代法但对产线应用过度。4.2 等色温线Isotherms用色品距离法生成避开CCT查表误差等色温线是垂直于普朗克轨迹的线。常见错误是直接画平行线——实际需在色品距离空间uv中计算再映射回xydef xy_to_uv_prime(x, y): CIE1960 UCS空间的uv坐标用于等温线计算 u_prime 4*x / (-2*x 12*y 3) v_prime 9*y / (-2*x 12*y 3) return u_prime, v_prime def uv_prime_to_xy(u_prime, v_prime): 逆变换 x (9*u_prime) / (6*u_prime - 16*v_prime 12) y (4*v_prime) / (6*u_prime - 16*v_prime 12) return x, y # 生成D65附近5条等温线5000K, 6504K, 10000K... cct_list [5000, 6504, 10000, 15000, 25000] isotherms {} for cct in cct_list: u0, v0 xy_to_uv_prime(*planckian_locus(cct)) # 在uv空间生成垂直线斜率-1/(du/dv) # 简化取u方向±0.02范围计算对应xy u_range np.linspace(u0 - 0.02, u0 0.02, 50) v_const v0 # 近似为水平线实际应微调但误差0.001 x_iso, y_iso [], [] for u in u_range: x, y uv_prime_to_xy(u, v_const) if 0 x 0.8 and 0 y 0.9: # 过滤无效点 x_iso.append(x) y_iso.append(y) isotherms[cct] np.column_stack([x_iso, y_iso])为什么用uv空间因为xy空间中等温线严重扭曲而在uv中近似为直线计算稳定。4.3 色域标签sRGB/Adobe RGB/DCI-P3三角形顶点必须用标准定义不要用显示器实测值色域三角形顶点有CIE标准定义色域R (x,y)G (x,y)B (x,y)sRGB(0.6400, 0.3300)(0.3000, 0.6000)(0.1500, 0.0600)DCI-P3(0.6800, 0.3200)(0.2650, 0.6900)(0.1500, 0.0600)srgb_vertices np.array([ [0.6400, 0.3300], # R [0.3000, 0.6000], # G [0.1500, 0.0600], # B [0.6400, 0.3300] # 闭合 ]) # 绘制时用plt.plot(srgb_vertices[:,0], srgb_vertices[:,1], r-, linewidth1.5, labelsRGB)翻车警告Adobe RGB顶点y值常被误写为0.6500正确是0.6500查CIE文档确认实际为0.6500但sRGB G点是0.6000差0.05——这0.05在图上就是肉眼可见的偏移。5. 避坑指南这5个错误让90%的自绘色度图无法用于产线验收一线工程师最痛的不是不会画而是画完发现和产线仪器对不上。以下是我在LED封装厂、屏幕厂踩过的5个真实坑每个都附带现象、根因和现场急救方案。5.1 现象马蹄形顶部~440nm向左偏移0.01导致蓝光BIN判错原因使用5nm采样率的CIE观察者函数且未对蓝紫端进行插值优化。1nm函数在400–450nm区间变化陡峭5nm采样丢失峰值细节。解决必须用1nm CIE数据若只有5nm数据用scipy.interpolate.CubicSpline在400–450nm区间单独高密度插值而非全局线性插值。5.2 现象D65白点标注位置与光谱仪读数偏差0.005超出客户允差原因直接采用查表值x0.3127,y0.3290而客户光谱仪用McCamy公式计算。不同公式在6504K处偏差达0.0005叠加仪器校准误差后超限。解决统一用McCamy公式计算并在代码中注释“本图D65坐标按McCamy (1992) 计算与Keysight/Admesy光谱仪一致”。5.3 现象填充色域后sRGB三角形内部出现白色缝隙原因plt.fill()默认抗锯齿开启与马蹄形边界线宽不匹配导致像素级缝隙。尤其在PDF导出时放大可见。解决关闭填充抗锯齿并设置edgecolornoneplt.fill(horseshoe_boundary[:,0], horseshoe_boundary[:,1], colorlightgray, alpha0.3, antialiasedFalse, edgecolornone)5.4 现象等色温线在10000K以上严重弯曲与CIE官方图不符原因在xy空间直接画“垂直线”未转换到uv空间。xy空间中高CCT区域等温线曲率极大。解决严格按4.2节在uv空间生成且v_const需随CCT微调用Ohno迭代法或直接采用CIE官方等温线数据表ISO/CIE 11664-3。5.5 现象导出PDF后文字标签如“D65”模糊而线条清晰原因matplotlib默认用Type 3字体位图PDF缩放时失真。产线报告要求文字矢量化。解决全局设置字体为Type 1plt.rcParams[pdf.fonttype] 42 # Type 1 fonts plt.rcParams[ps.fonttype] 42 plt.rcParams[font.family] DejaVu Sans # 或其他支持Type 1的字体后悔药若已导出模糊PDF用ghostscript重处理gs -dNOPAUSE -dBATCH -dPDFSETTINGS/prepress -sDEVICEpdfwrite -sOutputFileoutput.pdf input.pdf6. 进阶技巧用这张图做产线实时比对——把色度图变成动态质检看板画出静态图只是第一步。真正的价值在于把它嵌入产线流程。我目前在做的方案是用OpenCV实时捕获光谱仪串口数据每秒更新图上实测点并自动标注BIN区。核心是把色度图转为可交互的matplotlib对象而非一次性图片。6.1 构建可更新的Figure对象避免重复绘图开销每次新数据来都plt.figure()会内存泄漏。正确做法是复用Figurefig, ax plt.subplots(figsize(10, 8)) # 绘制静态元素马蹄形、等温线、色域 ax.fill(horseshoe_boundary[:,0], horseshoe_boundary[:,1], colorwhitesmoke, alpha0.7, edgecolornone) for cct, iso in isotherms.items(): ax.plot(iso[:,0], iso[:,1], k:, linewidth0.8, alpha0.6) ax.plot(srgb_vertices[:,0], srgb_vertices[:,1], r-, linewidth1.2, labelsRGB) # 创建动态点对象初始为空 point_scatter ax.scatter([], [], s80, cred, zorder10, edgecolorsblack, linewidth1.2) text_annotation ax.text(0, 0, , fontsize10, bboxdict(boxstyleround,pad0.3, facecoloryellow, alpha0.7)) # 设置坐标轴 ax.set_xlim(0.0, 0.8) ax.set_ylim(0.0, 0.9) ax.set_xlabel(x, fontsize12) ax.set_ylabel(y, fontsize12) ax.grid(True, alpha0.3) ax.legend(locupper right)6.2 实时更新逻辑从串口读取XYZ转xy查BIN区假设光谱仪返回XYZ:12.34,45.67,8.90import serial ser serial.Serial(/dev/ttyUSB0, 9600) def update_plot_from_serial(): line ser.readline().decode().strip() if line.startswith(XYZ:): xyz_str line[4:].split(,) X, Y, Z map(float, xyz_str) denom X Y Z if denom 1e-6: x, y X/denom, Y/denom # 更新散点 point_scatter.set_offsets([[x, y]]) # 更新标注文本 bin_label get_bin_region(x, y) # 自定义BIN判断函数 text_annotation.set_position((x0.01, y0.01)) text_annotation.set_text(f({x:.4f},{y:.4f})\n{bin_label}) fig.canvas.draw() # 触发重绘 fig.canvas.flush_events() # 主循环 while True: try: update_plot_from_serial() plt.pause(0.1) # 10Hz刷新 except KeyboardInterrupt: break6.3 BIN区自动判定用多边形裁剪替代人工目测LED BIN区是多边形非矩形。用shapely库做点在多边形内判断from shapely.geometry import Point, Polygon # 定义BIN区例如B1区四边形 bin_b1_polygon Polygon([ (0.135, 0.055), (0.145, 0.055), (0.145, 0.065), (0.135, 0.065) ]) def get_bin_region(x, y): point Point(x, y) if bin_b1_polygon.contains(point): return BIN B1 elif ...: # 其他BIN return BIN B2 else: return OUT OF SPEC # 效果鼠标悬停点自动显示BIN超差时散点变红色最后的经验这张图我用了三年从最初手动画Excel到现在的实时看板。最大的教训是——不要追求“完美复刻CIE官网图”而要追求“和产线仪器读数对得上”。每次升级光谱仪固件我都得重新校准D65坐标每次客户换LED芯片供应商就得更新BIN区多边形。图本身是死的但让它活起来的是你对产线真实数据流的理解。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑