QWT7 的科学 Colormap 色彩评估:QwtColorMapPreset 22 种预设的感知均匀性与场景化选

源码托管于:

摘要 ------ colormap 是从数据值到颜色的映射,它的质量决定读者会不会"看见数据里不存在的结构":亮度非单调的色带会在平滑场上画出假等值线,步长不均匀的色带会夸大某些数值区间,而红绿色盲观察者眼里整条色带可能塌缩成两三种颜色。本文以 QWT7 的 QwtColorMapPreset(src/core/qwt_colormap_preset.h/.cpp,22 种预设)为对象,按 src/core/qwt_colormap.cpp 的实现逐位复现 colorTable256() 构建 256 级 LUT,自行实现 sRGB→线性 RGB→XYZ(D65)→CIELAB 全链条、完整 CIEDE2000 公式(用 Sharma et al. 2005 发表数据自测,22 组最大误差 4.17e-5)与 Machado et al. 2009 色觉缺陷模拟矩阵,从亮度单调性、相邻级步长均匀性、色觉缺陷下的可区分性、灰度打印安全性四个维度给出定量评测;在此基础上给出数据类型 × 观察者 × 输出介质的场景化选择指南,并报告预设数据本身的若干工程事实(inferno 的色标取自参考曲线 t∈0,0.7 段、turbo 近似取自同段,却被摆在位置 0,0.875 上,viridis/magma/cividis 的中间色标与 matplotlib 同位置颜色最大偏差 ΔE00 11.4--17.2)。
摘要 --- 核心结论

  • 22 种预设按客观判据分为 16 顺序、4 发散、2 循环;rainbow 与 hsv 的色标数据在源码中完全相同(qwt_colormap_preset.cpp:108-120)
  • 亮度单调性:22 种预设中 18 种在主导方向上违例步为零(其中 10 种累计回退恰为 0);jet 有 77 个违例步、累计回退 71.76 L*、最大回退幅度 70.67 L*(L* 在 t≈0.625 升到 97.14 后跌回 26.47,(26.47, 97.14) 内每个亮度值在升支与降支各出现一次,误差至多一个量化步),turbo 为 65.31/63.47,二者评为非均匀
  • 步长均匀性(相邻级 ΔE00 的变异系数 CV):gray 0.228 最优,rdylgn 0.326、rdylbu 0.354 次之;inferno/magma 为 0.587/0.582,明显高于 matplotlib 自身表的 0.332/0.348,差距来自色标位置压缩而非设计
  • 色觉缺陷(Machado 严重度 1.0,相距 ≥32 级的最小 ΔE00 作为可区分性下界):coolwarm 9.96--10.30、rdylbu 8.87--13.24 在三种色觉下全部稳健;rainbow/hsv 在 deutan 下仅 0.130(t=0.620 与 t=0.745 同色),rdylgn 在 deutan 下 0.182,中心两侧对红绿色盲完全不可辨
  • 灰度打印:coolwarm 两端 ΔL* 仅 0.36,转灰度后符号信息全部丢失;rdylbu/rdylgn/spectral 两端 ΔL* 为 4.62--5.93 仍可辨;jet/turbo/cool 的 L* 非单调,灰度化必然产生伪等值线

1. 引言:colormap 为什么需要色彩科学

1.1 三种失效模式的形式化

把 colormap 看作函数 c : 0 , 1 → 0 , 255 3 c: 0,1 \to 0,255^3 c:0,1→0,2553(经 256 级 LUT 离散化后为 c i , i = 0..255 c_i,\ i=0..255 ci, i=0..255),再经显示器的电光转换进入人眼。读者对数据的判断几乎全部依赖三个感知量:明暗(CIELAB L ∗ L^* L∗)、色差( Δ E 00 \Delta E_{00} ΔE00)与色相方向。对应的失效模式可以写成三个可测命题:

  1. 亮度伪边界 : ∃   t 1 < t 2 \exists\, t_1 < t_2 ∃t1<t2 使 L ∗ ( t 1 ) > L ∗ ( t 2 ) + τ L^*(t_1) > L^*(t_2) + \tau L∗(t1)>L∗(t2)+τ。人眼对亮度阶跃极其敏感(边缘增强机制),一条 L ∗ L^* L∗ 先升后降的色带会在平滑数据场上画出一条并不存在的亮带------MATLAB jet 的青-黄过渡是教科书案例,本文实测其累计回退达 71.76 L*。
  2. 步长不均匀 :相邻级色差 Δ E 00 ( c i , c i + 1 ) \Delta E_{00}(c_i, c_{i+1}) ΔE00(ci,ci+1) 的波动使某些数值区间被视觉夸大、某些被压缩。变异系数 C V = σ / μ \mathrm{CV} = \sigma/\mu CV=σ/μ 度量这种不均匀; C V \mathrm{CV} CV 越大,"颜色差一格"所代表的数据差越不可信。
  3. 不可注入性 :在色觉缺陷模拟或灰度化之后, ∃   ∣ t 1 − t 2 ∣ ≥ δ \exists\, |t_1 - t_2| \ge \delta ∃∣t1−t2∣≥δ 使 Δ E 00 ( c ′ ( t 1 ) , c ′ ( t 2 ) ) ≈ 0 \Delta E_{00}(c'(t_1), c'(t_2)) \approx 0 ΔE00(c′(t1),c′(t2))≈0,即两个相距很远的数据值看起来同色。红-绿色盲(protan/deutan,合计约占男性 8%)下红绿对立轴塌缩,是这类失效的主要来源。

这三个命题各对应第 2 节的一个度量: L ∗ ( t ) L^*(t) L∗(t) 剖面的单调性统计、相邻级 Δ E 00 \Delta E_{00} ΔE00 的分布、以及模拟视觉下的最小远距色差。

1.2 在 LUT 层面评测的理由

QWT 的渲染路径有两条:连续路径逐像素调用 QwtColorMap::rgb(vMin, vMax, value),索引路径调用 colorIndex() 查 colorTable256() 生成的 256 级表(QImage::Format_Indexed8,见第 3.3 节)。本文统一在 256 级 LUT 上评测,理由有二:它是 Indexed 格式渲染(需显式 setFormat(QwtColorMap::Indexed),预设工厂默认 RGB 格式)与颜色条刻度实际使用的离散化,也是 8-bit 索引输出能表达的极限;连续路径的结论可由 LUT 结论内插得到,反之不成立(连续路径可能比 LUT 更平滑,但不会更差)。LUT 的采样网格完全复现 colorTable256(): pos i = i / 256 \text{pos}_i = i/256 posi=i/256,注意它永远取不到 1.0,因此 viridis 的 LUT[255] = (251,230,37) 而非末色标 (253,231,37)------这个 off-by-one 是设计使然(qwt_colormap.cpp:282-284),也提醒我们评测网格的选择本身会影响末位数字。

1.3 与使用指南的分工

docs/zh/use-guide/general/colormap.md 讲 22 种预设的用法、示例界面与避坑清单;本文讲这些结论背后的度量与证据:每一个"推荐"或"不推荐"都对应一张表里的数字。两篇互补阅读。

2. 色彩空间与度量

2.1 sRGB → 线性 RGB → XYZ → CIELAB

8-bit sRGB 分量先归一化到 0 , 1 0,1 0,1,经 IEC 61966-2-1 的电光传递函数去 gamma:

c lin = { c / 12.92 , c ≤ 0.04045 ( c + 0.055 1.055 ) 2.4 , c > 0.04045 c_{\text{lin}} = \begin{cases} c / 12.92, & c \le 0.04045 \\ \left( \dfrac{c + 0.055}{1.055} \right)^{2.4}, & c > 0.04045 \end{cases} clin=⎩ ⎨ ⎧c/12.92,(1.055c+0.055)2.4,c≤0.04045c>0.04045

线性 RGB 经 sRGB 标准矩阵转到 CIE XYZ(D65 白点):

( X Y Z ) = ( 0.4124564 0.3575761 0.1804375 0.2126729 0.7151522 0.0721750 0.0193339 0.1191920 0.9503041 ) ( R lin G lin B lin ) \begin{pmatrix} X \\ Y \\ Z \end{pmatrix} = \begin{pmatrix} 0.4124564 & 0.3575761 & 0.1804375 \\ 0.2126729 & 0.7151522 & 0.0721750 \\ 0.0193339 & 0.1191920 & 0.9503041 \end{pmatrix} \begin{pmatrix} R_{\text{lin}} \\ G_{\text{lin}} \\ B_{\text{lin}} \end{pmatrix} XYZ = 0.41245640.21267290.01933390.35757610.71515220.11919200.18043750.07217500.9503041 RlinGlinBlin

再按 CIE 15:2004 定义转到 CIELAB,白点取 D65 的 ( X n , Y n , Z n ) = ( 0.95047 , 1.00000 , 1.08883 ) (X_n, Y_n, Z_n) = (0.95047,\ 1.00000,\ 1.08883) (Xn,Yn,Zn)=(0.95047, 1.00000, 1.08883):

f ( t ) = { t 1 / 3 , t > ε κ t + 16 116 , t ≤ ε , ε = 216 24389 , κ = 24389 27 f(t) = \begin{cases} t^{1/3}, & t > \varepsilon \\ \dfrac{\kappa t + 16}{116}, & t \le \varepsilon \end{cases}, \qquad \varepsilon = \frac{216}{24389},\quad \kappa = \frac{24389}{27} f(t)=⎩ ⎨ ⎧t1/3,116κt+16,t>εt≤ε,ε=24389216,κ=2724389

L ∗ = 116   f ( Y / Y n ) − 16 , a ∗ = 500 ( f ( X / X n ) − f ( Y / Y n ) ) , b ∗ = 200 ( f ( Y / Y n ) − f ( Z / Z n ) ) L^* = 116\, f(Y/Y_n) - 16, \qquad a^* = 500\left( f(X/X_n) - f(Y/Y_n) \right), \qquad b^* = 200\left( f(Y/Y_n) - f(Z/Z_n) \right) L∗=116f(Y/Yn)−16,a∗=500(f(X/Xn)−f(Y/Yn)),b∗=200(f(Y/Yn)−f(Z/Zn))

L ∗ L^* L∗ 即感知亮度:灰度打印时纸张上的灰阶就由它决定,因此第 4.6 节直接把 L ∗ ( t ) L^*(t) L∗(t) 当作灰度剖面。实现自测(脚本第 1 节输出): ( 255 , 0 , 0 ) → ( 53.2408 , 80.0925 , 67.2032 ) (255,0,0) \to (53.2408, 80.0925, 67.2032) (255,0,0)→(53.2408,80.0925,67.2032),与文献值误差 9.4e-5;中灰 ( 128 , 128 , 128 ) → L ∗ = 53.585 (128,128,128) \to L^* = 53.585 (128,128,128)→L∗=53.585(解析值: 128 / 255 → 0.2159 → ⋅ 3 = 0.6000 → 116 × 0.6 − 16 128/255 \to 0.2159 \to \sqrt3{\cdot} = 0.6000 \to 116 \times 0.6 - 16 128/255→0.2159→3⋅ =0.6000→116×0.6−16);灰阶往返量化偏差为 0。

2.2 CIEDE2000

CIE76 的欧氏距离在蓝区与中性区严重失真,本文全部色差使用 CIEDE2000(Sharma, Wu & Dalal 2005 的完整公式,含旋转项 R T R_T RT)。记两色为 ( L 1 , a 1 , b 1 ) (L_1, a_1, b_1) (L1,a1,b1)、 ( L 2 , a 2 , b 2 ) (L_2, a_2, b_2) (L2,a2,b2), C = a 2 + b 2 C = \sqrt{a^2+b^2} C=a2+b2 , C ˉ = ( C 1 + C 2 ) / 2 \bar{C} = (C_1+C_2)/2 Cˉ=(C1+C2)/2:

G = 1 2 ( 1 − C ˉ 7 C ˉ 7 + 25 7 ) , a ′ = a ( 1 + G ) , C ′ = a ′ 2 + b 2 , h ′ = atan2 ⁡ ( b , a ′ )   m o d   360 ∘ G = \frac{1}{2}\left( 1 - \sqrt{ \frac{\bar{C}^7}{\bar{C}^7 + 25^7} } \right), \quad a' = a(1+G), \quad C' = \sqrt{a'^2 + b^2}, \quad h' = \operatorname{atan2}(b, a') \bmod 360^\circ G=21 1−Cˉ7+257Cˉ7 ,a′=a(1+G),C′=a′2+b2 ,h′=atan2(b,a′)mod360∘

Δ L ′ = L 2 − L 1 , Δ C ′ = C 2 ′ − C 1 ′ , Δ h ′ = { 0 , C 1 ′ C 2 ′ = 0 h 2 ′ − h 1 ′ , ∣ h 2 ′ − h 1 ′ ∣ ≤ 180 ∘ h 2 ′ − h 1 ′ ∓ 360 ∘ , 否则(取符号使结果落入 ± 180 ∘ ) \Delta L' = L_2 - L_1, \qquad \Delta C' = C'_2 - C'_1, \qquad \Delta h' = \begin{cases} 0, & C'_1 C'_2 = 0 \\ h'_2 - h'_1, & |h'_2 - h'_1| \le 180^\circ \\ h'_2 - h'_1 \mp 360^\circ, & \text{否则(取符号使结果落入} \pm 180^\circ) \end{cases} ΔL′=L2−L1,ΔC′=C2′−C1′,Δh′=⎩ ⎨ ⎧0,h2′−h1′,h2′−h1′∓360∘,C1′C2′=0∣h2′−h1′∣≤180∘否则(取符号使结果落入±180∘)

Δ H ′ = 2 C 1 ′ C 2 ′   sin ⁡  ⁣ ( Δ h ′ 2 ) \Delta H' = 2 \sqrt{C'_1 C'_2}\, \sin\!\left( \frac{\Delta h'}{2} \right) ΔH′=2C1′C2′ sin(2Δh′)

权重与旋转项( L ˉ ′ = ( L 1 + L 2 ) / 2 \bar{L}' = (L_1+L_2)/2 Lˉ′=(L1+L2)/2, C ˉ ′ = ( C 1 ′ + C 2 ′ ) / 2 \bar{C}' = (C'_1+C'_2)/2 Cˉ′=(C1′+C2′)/2, h ˉ ′ \bar{h}' hˉ′ 为 h 1 ′ , h 2 ′ h'_1, h'_2 h1′,h2′ 的圆周均值):

S L = 1 + 0.015 ( L ˉ ′ − 50 ) 2 20 + ( L ˉ ′ − 50 ) 2 , S C = 1 + 0.045   C ˉ ′ , S H = 1 + 0.015   C ˉ ′   T S_L = 1 + \frac{0.015 (\bar{L}' - 50)^2}{\sqrt{20 + (\bar{L}'-50)^2}}, \quad S_C = 1 + 0.045\, \bar{C}', \quad S_H = 1 + 0.015\, \bar{C}'\, T SL=1+20+(Lˉ′−50)2 0.015(Lˉ′−50)2,SC=1+0.045Cˉ′,SH=1+0.015Cˉ′T

T = 1 − 0.17 cos ⁡ ( h ˉ ′ − 30 ∘ ) + 0.24 cos ⁡ 2 h ˉ ′ + 0.32 cos ⁡ ( 3 h ˉ ′ + 6 ∘ ) − 0.20 cos ⁡ ( 4 h ˉ ′ − 63 ∘ ) T = 1 - 0.17 \cos(\bar{h}' - 30^\circ) + 0.24 \cos 2\bar{h}' + 0.32 \cos(3\bar{h}' + 6^\circ) - 0.20 \cos(4\bar{h}' - 63^\circ) T=1−0.17cos(hˉ′−30∘)+0.24cos2hˉ′+0.32cos(3hˉ′+6∘)−0.20cos(4hˉ′−63∘)

Δ θ = 30 ∘ exp ⁡  ⁣ ( − ( h ˉ ′ − 275 ∘ 25 ∘ ) 2 ) , R C = 2 C ˉ ′ 7 C ˉ ′ 7 + 25 7 , R T = − sin ⁡ ( 2   Δ θ )   R C \Delta\theta = 30^\circ \exp\!\left( -\left( \frac{\bar{h}' - 275^\circ}{25^\circ} \right)^2 \right), \quad R_C = 2 \sqrt{ \frac{\bar{C}'^7}{\bar{C}'^7 + 25^7} }, \quad R_T = -\sin(2\,\Delta\theta)\, R_C Δθ=30∘exp(−(25∘hˉ′−275∘)2),RC=2Cˉ′7+257Cˉ′7 ,RT=−sin(2Δθ)RC

Δ E 00 = ( Δ L ′ S L ) 2 + ( Δ C ′ S C ) 2 + ( Δ H ′ S H ) 2 + R T   Δ C ′ S C   Δ H ′ S H \Delta E_{00} = \sqrt{ \left( \frac{\Delta L'}{S_L} \right)^2 + \left( \frac{\Delta C'}{S_C} \right)^2 + \left( \frac{\Delta H'}{S_H} \right)^2 + R_T\, \frac{\Delta C'}{S_C}\, \frac{\Delta H'}{S_H} } ΔE00=(SLΔL′)2+(SCΔC′)2+(SHΔH′)2+RTSCΔC′SHΔH′

实现用 Sharma et al. 2005 发表数据中的 20 组 Lab 对与 2 组补充对(一组近黑期望值由解析重推导、一组同色平凡对;脚本第 2 节逐组打印)自测:最大绝对误差 4.17e-5,且全部满足 Δ E 00 ( a , b ) = Δ E 00 ( b , a ) \Delta E_{00}(a,b) = \Delta E_{00}(b,a) ΔE00(a,b)=ΔE00(b,a)(对称性精确到 0)。经验尺度: Δ E 00 ≈ 1 \Delta E_{00} \approx 1 ΔE00≈1 为恰可察觉差(JND), Δ E 00 < 0.5 \Delta E_{00} < 0.5 ΔE00<0.5 在屏幕上基本不可辨。

2.3 色觉缺陷模拟

采用 Machado, Oliveira & Fernandes(2009)的生理模型在严重度 1.0(二色觉,即完全型色盲)下的 3×3 矩阵,作用于线性 RGB 后裁剪回 0 , 1 0,1 0,1 并重新量化为 8-bit(与真实显示器呈现一致):

( R ′ G ′ B ′ ) lin = M CVD ( R G B ) lin , M protan = ( 0.152286 1.052583 − 0.204868 0.114503 0.786281 0.099216 − 0.003882 − 0.048116 1.051998 ) \begin{pmatrix} R' \\ G' \\ B' \end{pmatrix}{\text{lin}} = M{\text{CVD}} \begin{pmatrix} R \\ G \\ B \end{pmatrix}{\text{lin}}, \qquad M{\text{protan}} = \begin{pmatrix} 0.152286 & 1.052583 & -0.204868 \\ 0.114503 & 0.786281 & 0.099216 \\ -0.003882 & -0.048116 & 1.051998 \end{pmatrix} R′G′B′ lin=MCVD RGB lin,Mprotan= 0.1522860.114503−0.0038821.0525830.786281−0.048116−0.2048680.0992161.051998

M deutan = ( 0.367322 0.860646 − 0.227968 0.280085 0.672501 0.047413 − 0.011820 0.042940 0.968881 ) , M tritan = ( 1.255528 − 0.076749 − 0.178779 − 0.078411 0.930809 0.147602 0.004733 0.691367 0.303900 ) M_{\text{deutan}} = \begin{pmatrix} 0.367322 & 0.860646 & -0.227968 \\ 0.280085 & 0.672501 & 0.047413 \\ -0.011820 & 0.042940 & 0.968881 \end{pmatrix}, \qquad M_{\text{tritan}} = \begin{pmatrix} 1.255528 & -0.076749 & -0.178779 \\ -0.078411 & 0.930809 & 0.147602 \\ 0.004733 & 0.691367 & 0.303900 \end{pmatrix} Mdeutan= 0.3673220.280085−0.0118200.8606460.6725010.042940−0.2279680.0474130.968881 ,Mtritan= 1.255528−0.0784110.004733−0.0767490.9308090.691367−0.1787790.1476020.303900

选严重度 1.0 是取最坏情形:异常三色觉(severity < 1)的辨别力介于正常与二色觉之间,1.0 下可辨的颜色在更轻的缺陷下必然可辨,反之不成立。

2.4 度量定义与分级规则

对 256 级 LUT 定义以下量(全部在脚本中实现并打印):

  • 亮度剖面统计 :主导方向取 sign ⁡ ( L 255 ∗ − L 0 ∗ ) \operatorname{sign}(L^*_{255} - L^*_0) sign(L255∗−L0∗);违例数 viol = 逆主导方向且幅度 > 0.5 L* 的相邻步数(0.5 远低于 1 JND,用于吸收 8-bit 量化抖动);累计回退 reg = 所有逆向步幅度之和(对缓坡漂移也敏感);最大回退幅度 maxreg = L ∗ L^* L∗ 偏离其单调包络的最大值(递增剖面用运行最大值、递减剖面用运行最小值)。发散型按两臂分别统计(两臂都应单调爬向亮中心),循环型不做单调性要求。
  • 步长均匀性 :相邻级 Δ E 00 \Delta E_{00} ΔE00 的均值、最大值、最小值与变异系数 C V = σ / μ \mathrm{CV} = \sigma/\mu CV=σ/μ;色带总长 path = 相邻级 Δ E 00 \Delta E_{00} ΔE00 之和。
  • 可区分性下界 farMin(gap=32):所有相距 ≥ 32 级(全量程的 1/8)的色对中最小的 Δ E 00 \Delta E_{00} ΔE00。它回答"两个明显不同的数据值在最坏情况下看起来有多像",比相邻步指标更能暴露色带塌缩。
  • 分级 (三档约定,阈值在脚本中显式给出):A 感知均匀 = reg ≤ 2.0 且 CV ≤ 0.45;B 近似均匀 = reg ≤ 8.0 且 CV ≤ 0.75;C 非均匀 = 其余。循环型只按 CV 分档。阈值是粗粒度约定,CV 在 0.45 边界 ±0.02 内的预设(spring、spectral,均 0.449)应视为与邻档并列。

3. QwtColorMap 的工程实现

3.1 类结构

#mermaid-svg-BevK7DRSR940Uuqy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BevK7DRSR940Uuqy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BevK7DRSR940Uuqy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BevK7DRSR940Uuqy .error-icon{fill:#552222;}#mermaid-svg-BevK7DRSR940Uuqy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BevK7DRSR940Uuqy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BevK7DRSR940Uuqy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BevK7DRSR940Uuqy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BevK7DRSR940Uuqy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BevK7DRSR940Uuqy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BevK7DRSR940Uuqy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BevK7DRSR940Uuqy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BevK7DRSR940Uuqy .marker.cross{stroke:#333333;}#mermaid-svg-BevK7DRSR940Uuqy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BevK7DRSR940Uuqy p{margin:0;}#mermaid-svg-BevK7DRSR940Uuqy g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-BevK7DRSR940Uuqy g.classGroup text .title{font-weight:bolder;}#mermaid-svg-BevK7DRSR940Uuqy .cluster-label text{fill:#333;}#mermaid-svg-BevK7DRSR940Uuqy .cluster-label span{color:#333;}#mermaid-svg-BevK7DRSR940Uuqy .cluster-label span p{background-color:transparent;}#mermaid-svg-BevK7DRSR940Uuqy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BevK7DRSR940Uuqy .cluster text{fill:#333;}#mermaid-svg-BevK7DRSR940Uuqy .cluster span{color:#333;}#mermaid-svg-BevK7DRSR940Uuqy .nodeLabel,#mermaid-svg-BevK7DRSR940Uuqy .edgeLabel{color:#131300;}#mermaid-svg-BevK7DRSR940Uuqy .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-BevK7DRSR940Uuqy .label text{fill:#131300;}#mermaid-svg-BevK7DRSR940Uuqy .labelBkg{background:#ECECFF;}#mermaid-svg-BevK7DRSR940Uuqy .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-BevK7DRSR940Uuqy .classTitle{font-weight:bolder;}#mermaid-svg-BevK7DRSR940Uuqy .node rect,#mermaid-svg-BevK7DRSR940Uuqy .node circle,#mermaid-svg-BevK7DRSR940Uuqy .node ellipse,#mermaid-svg-BevK7DRSR940Uuqy .node polygon,#mermaid-svg-BevK7DRSR940Uuqy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BevK7DRSR940Uuqy .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy g.clickable{cursor:pointer;}#mermaid-svg-BevK7DRSR940Uuqy g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-BevK7DRSR940Uuqy g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-BevK7DRSR940Uuqy .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-BevK7DRSR940Uuqy .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-BevK7DRSR940Uuqy .dashed-line{stroke-dasharray:3;}#mermaid-svg-BevK7DRSR940Uuqy .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-BevK7DRSR940Uuqy #compositionStart,#mermaid-svg-BevK7DRSR940Uuqy .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #compositionEnd,#mermaid-svg-BevK7DRSR940Uuqy .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #dependencyStart,#mermaid-svg-BevK7DRSR940Uuqy .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #dependencyStart,#mermaid-svg-BevK7DRSR940Uuqy .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #extensionStart,#mermaid-svg-BevK7DRSR940Uuqy .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #extensionEnd,#mermaid-svg-BevK7DRSR940Uuqy .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #aggregationStart,#mermaid-svg-BevK7DRSR940Uuqy .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #aggregationEnd,#mermaid-svg-BevK7DRSR940Uuqy .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #lollipopStart,#mermaid-svg-BevK7DRSR940Uuqy .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy #lollipopEnd,#mermaid-svg-BevK7DRSR940Uuqy .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-BevK7DRSR940Uuqy .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-BevK7DRSR940Uuqy .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BevK7DRSR940Uuqy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BevK7DRSR940Uuqy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BevK7DRSR940Uuqy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} create()
adapts
QwtColorMap
+Format format()
+rgb(vMin, vMax, value) : QRgb
+colorIndex(numColors, vMin, vMax, value) : uint
+colorTable(numColors) : QVector<QRgb>
+colorTable256() : QVector<QRgb>
QwtLinearColorMap
+Mode mode()
+addColorStop(pos, color)
+stopPos() / stopColors()
QwtAlphaColorMap
QwtHueColorMap
QwtSaturationValueColorMap
QwtColorMapPreset
+create(Preset) : unique_ptr<QwtLinearColorMap>
+create(name) : unique_ptr<QwtLinearColorMap>
+colorStops(Preset) : QVector<QPair>
+availablePresets() : QStringList
Qwt3DColorMapColor
+operator()(x, y, z) : RGBA

rgb(double vMin, double vMax, double value) 是 v7.3.1+ 的统一签名(旧的 rgb(const QwtInterval&, double) 已移除),四个子类各自重写它;QwtColorMapPreset 只是 QwtLinearColorMap 的工厂与查询注册表,不引入新的插值语义。

3.2 ColorStops:二分查找与取整技巧

QwtLinearColorMap 的插值发生在 8-bit sRGB 空间 ,核心是 ColorStops::rgb()(src/core/qwt_colormap.cpp:190-220):

cpp 复制代码
// src/core/qwt_colormap.cpp ------ ColorStop 构造时预计算(:88-91)
r0 = r + 0.5;  g0 = g + 0.5;  b0 = b + 0.5;   // 把 +0.5 的舍入提前

// 查色时(:201-207),ratio = (pos - s1.pos) / s1.posStep
const int r = int(s1.r0 + ratio * s1.rStep);  // 截断 == 四舍五入

三个工程细节藏在这些行里:int(x + 0.5) 的截断等价于 round-half-up,把舍入常数折进色标构造使热路径只剩一次乘加;findUpper()(:169-188)是手写二分,返回第一个位置严格大于 pos 的色标,ScaledColors 模式在相邻色标间线性插值、FixedColors 模式直接返回下界色标(阶梯色带);insert()(:121-151)则会把位置差 < 0.001 的色标合并,用户自定义色标过密时会被静默去重。本文脚本的 LUT 构建逐位复现了这套算术(含二分语义与取整),viridis 的 LUT[0] 精确等于首色标 (68,1,84),可作为复现正确性的锚点。

3.3 三套采样网格与 NaN 语义

同一张 colormap 有三种离散化入口,网格互不相同:colorTable256() 调 rgb(0.0, 256.0, i),即 pos = i / 256 \text{pos} = i/256 pos=i/256(:278-286);colorTable(n) 用 pos = i / ( n − 1 ) \text{pos} = i/(n-1) pos=i/(n−1),端点精确命中首末色标(:295-304);连续路径 rgb(vMin, vMax, value) 按 ratio = ( v − v min ⁡ ) / ( v max ⁡ − v min ⁡ ) \text{ratio} = (v-v_{\min})/(v_{\max}-v_{\min}) ratio=(v−vmin)/(vmax−vmin) 取色。索引路径还有配套的 colorIndex():NaN/Inf 映射到索引 0,value >= vMax 映射到 numColors-1,FixedColors 模式下截断不加 0.5(:473-491)。连续路径对 NaN/Inf 返回 0u(透明黑,:452-453)------光谱图上缺失数据因此显示为透明而非某个数据色,这条语义与曲线渲染的 NaN 处理同源(见《NaN 渲染》篇)。本文全部指标基于 colorTable256() 网格;换用 colorTable(256) 只会让端点色移动约 0.35 ΔE00(实测 viridis 末级 (251,230,37) 与末色标 (253,231,37) 相距 0.346),不改变任何结论的符号。

3.4 插值空间:sRGB 还是线性光

在 sRGB 编码空间插值会让中间色偏暗(相对物理线性光而言),这是 gamma 的非线性所致。脚本第 8 节把同一组色标分别在 sRGB 空间与线性光空间插值、量化后逐级比较,量化了这个选择的代价:

色标密度 代表预设 sRGB vs 线性光:ΔE00 均值 / 最大
2 个色标 cool、spring、summer、autumn、winter、gray、copper 8.1--15.7 / 12.6--28.6
4--5 个色标 hot、bone、coolwarm 2.6--10.4 / 7.4--21.8
7--9 个色标 viridis、plasma、inferno、magma、cividis、jet、turbo(均 9 个)、rainbow、hsv(均 7 个) 0.42--9.39 / 0.91--25.10
11 个色标 rdylbu、rdylgn、spectral 0.64--0.77 / 1.8--2.5

从这张表能读出伪差的边界。色标密度与色相跨度共同界定插值空间的伪差 :11 色标发散族与 9 色标感知均匀族(viridis/plasma/cividis)两种插值的差异 ≤ 2.6 ΔE00,而只有 7 个色标的 rainbow/hsv 达到 25.10------高饱和色相角点之间的插值把空间差异放大了近一个数量级;2 色标映射则一律落在 12.6--28.6,用户自定义双色渐变时插值空间的选择直接决定亮度剖面形状。至于灰阶,QWT 的选择在感知上反而更优:sRGB 空间插值的 gray LUT 步长 CV 为 0.228(22 种预设中最低,因为 sRGB 编码本身就是按感知均匀设计的),线性光插值则给出物理均匀但感知前密的亮度 ramp。QWT 与 matplotlib、CSS 渐变一致地选择编码空间插值,在预设色标足够密且色相跨度不大的前提下这一选择几乎不可见。

3.5 预设工厂、同名陷阱与 3D 桥接

QwtColorMapPreset::create() 用首末色标调 setColorInterval()、其余色标逐个 addColorStop()(qwt_colormap_preset.cpp:223-235);presetFromName() 大小写不敏感,查不到名字时静默回退到 Viridis (:366)------拼错预设名不会报错,只会得到一张 viridis(qwt_colormap_preset.h:131 的注释仍声称"returns -1 if not found",与实现不符,属过时注释)。源码注释声称色标"sourced from matplotlib colormaps",但 rainbowStops() 与 hsvStops() 返回完全相同的 7 个色标(:108-120),且 4.7 节的溯源探针显示多个预设的色标并不落在 matplotlib 曲线上。3D 侧通过 Qwt3DColorMapColor(presetName, size=256)(src/plot3d/qwt3d_colormap_color.h)把 core 的 colormap 适配为 Qwt3DColor,2D 与 3D 因此共用同一套预设与同一份评测结论;颜色条由 QwtScaleWidget 持有 colormap 实例绘制(src/plot/qwt_scale_widget.cpp:571-572)。

4. 实验

4.1 方法

全部数值来自 docs/assets/algorithms/colormap_experiment.py 的一次确定性运行(Python 3.11 + numpy + matplotlib,无随机数;色彩变换全部自实现,matplotlib 仅用于采样其内置色表作对照)。脚本先跑两组自测(2.1 节的 Lab 锚点、2.2 节的 CIEDE2000 数据集),再对 22 个预设依次计算分类、亮度剖面、步长统计、三种色觉模拟、灰度剖面与 matplotlib 对照,所有原始数字打印到 stdout,六张插图由同一脚本以 Agg 后端生成。下文每个数字都可在该次运行的 stdout 中逐字找到。

4.2 客观分类

分类不依赖命名或来源,只依赖色标与 LUT 数据:首末色标 Δ E 00 < 1 \Delta E_{00} < 1 ΔE00<1 判为循环; L ∗ L^* L∗ 最大值位于 t ∈ 0.45 , 0.55 t \in 0.45, 0.55 t∈0.45,0.55 且中心亮度高出两端 15 L* 以上判为发散(亮中性中心是发散色带的定义特征);其余为顺序。注意 jet 与 turbo 的 L ∗ L^* L∗ 峰值在 t≈0.627(黄色段;此为 LUT argmax 口径,黄色色标的名义位置是 0.625,摘要沿用后者,两口径相差不足一级)而非中心,因此被正确归入顺序而非发散------它们的亮度剖面是"爬升-崩塌"型,与发散型的"对称双臂"型在数据上可区分。

表 1:客观分类结果(色标数据 + LUT)
预设 类别 首末 ΔE00 t(L*max) max C* 色相扫过(°) 标志
viridis 顺序 100.32 1.000 85.55 224.4 色相扫过
plasma 顺序 109.30 1.000 91.20 159.2 -
inferno 顺序 100.82 1.000 78.84 172.2 -
magma 顺序 99.82 1.000 59.53 177.1 -
cividis 顺序 94.60 1.000 84.86 239.3 色相扫过
jet 顺序 41.51 0.627 133.81 267.7 色相扫过
hot 顺序 100.00 1.000 104.46 89.6 -
cool 顺序 57.98 0.000 115.49 131.7 -
spring 顺序 92.81 1.000 115.54 134.4 -
summer 顺序 47.38 1.000 73.26 66.2 -
autumn 顺序 64.30 1.000 104.55 62.6 -
winter 顺序 77.87 1.000 133.81 157.4 -
gray 顺序 100.00 1.000 0.00 0.0 无彩色
bone 顺序 100.00 1.000 18.66 8.3 -
copper 顺序 79.00 1.000 44.64 86.0 -
rainbow 循环 0.00 0.169 133.79 359.4 -
hsv 循环 0.00 0.169 133.79 359.4 -
turbo 顺序 30.79 0.627 93.36 280.6 色相扫过
coolwarm 发散 40.13 0.502 70.59 191.6 -
rdylbu 发散 37.21 0.502 77.78 281.7 色相扫过
rdylgn 发散 63.67 0.502 77.78 129.0 -
spectral 发散 28.86 0.502 68.77 293.6 色相扫过

色相扫过角只在 C* ≥ 15 的色度有效区段累计(近中性色的色相角是数值噪声);gray 全带 C* = 0,bone 的 C* 峰值仅 18.66,二者的"色相"信息可以忽略。

4.3 亮度剖面与单调性

表 2:亮度单调性与步长均匀性(256 级 LUT,正常色觉)
预设 L*₀ L*₂₅₅ viol reg maxreg ΔE 均值 ΔE 最大 CV path 分级
viridis 14.90 90.43 0 0.34 0.07 0.472 1.018 0.414 120.35 A
plasma 15.53 93.95 0 0.00 0.00 0.482 0.970 0.427 122.92 A
inferno 0.08 96.91 0 0.00 0.00 0.578 1.753 0.587 147.44 B
magma 0.08 96.85 0 0.00 0.00 0.548 1.515 0.582 139.72 B
cividis 13.04 90.18 0 0.00 0.00 0.447 0.886 0.386 113.90 A
jet 12.97 26.47 77 71.76 70.67 0.922 1.557 0.401 235.21 C
hot 0.00 99.93 0 0.00 0.00 0.656 1.610 0.535 167.24 B
cool 91.11 60.17 0 6.31 6.24 0.312 0.431 0.362 79.49 B
spring 60.32 96.88 0 0.79 0.77 0.363 0.644 0.449 92.44 A
summer 47.49 97.45 0 0.00 0.00 0.232 0.461 0.476 59.19 B
autumn 53.24 96.88 0 0.00 0.00 0.245 0.358 0.432 62.44 A
winter 32.30 88.18 0 1.10 0.35 0.371 0.823 0.506 94.69 B
gray 0.00 99.65 0 0.00 0.00 0.294 0.398 0.228 74.96 A
bone 0.00 99.65 0 0.00 0.00 0.434 1.397 0.487 110.76 B
copper 0.00 83.44 0 0.00 0.00 0.423 1.047 0.362 107.82 A
rainbow 53.24 53.26 39 75.03 64.75 1.087 2.335 0.594 277.06 B(循环)
hsv 53.24 53.26 39 75.03 64.75 1.087 2.335 0.594 277.06 B(循环)
turbo 11.80 25.68 47 65.31 63.47 0.855 1.876 0.400 218.11 C
coolwarm 37.67 38.03 0 0.40 0.40 0.503 1.536 0.380 128.17 A
rdylbu 34.28 28.66 0 0.29 0.29 0.660 1.954 0.354 168.33 A
rdylgn 34.28 38.90 0 0.29 0.29 0.539 1.112 0.326 137.41 A
spectral 33.32 39.25 0 0.29 0.29 0.696 1.786 0.449 177.51 A

jet 的亮度剖面是全部预设中最危险的:L* 从 12.97 爬到 97.14(t≈0.625 的黄段)再跌回 26.47,77 个逆方向步、累计回退 71.76 L*------升支覆盖 12.97, 97.14、降支覆盖 26.47, 97.14,因此 (26.47, 97.14) 内的每个亮度值在两支上各出现至少一次(误差至多一个量化步),灰度或低对比显示上两个相距半个量程以上的数据值可以同灰;turbo 同型但幅度略小(65.31/63.47)。容易被忽略的是 cool:整体递减(91.11→60.17)看似单调,实则中段下探到 53.94 后回升 6.24 L*,形成一个 V 形凹陷,per-step 容差看不出来、累计回退指标可以。四个发散预设的两臂单调性则都很好(reg ≤ 0.40),它们的亮度信息集中在"中心亮、两臂暗"的结构里,这与顺序色带的判读方式不同,混用会产生系统误读。

4.4 步长均匀性

热力图把每条色带展开成 255 个相邻步的 ΔE00 序列:rainbow/hsv 在纯色相角点附近出现 ΔE00 > 2 的亮块(sRGB 立方体角点之间的感知距离远大于边中点之间),jet 在青-黄段同样偏亮;感知均匀族(viridis/plasma/cividis)与发散族则整体平坦。CV 的排序给出另一个视角:gray 0.228 最低,因为 sRGB 编码空间插值天然接近感知均匀;inferno/magma 的 0.587/0.582 偏高并非设计缺陷------对照 matplotlib 自身 256 级表的同口径 CV(inferno 0.332、magma 0.348、viridis 0.278、plasma 0.417),差距主要来自 QWT 9 色标近似引入的位置压缩(4.7 节),其中 inferno/magma 的 CV 增幅(+0.255/+0.234)为全部预设中最大;viridis(QWT CV 0.414 对 mpl 0.278,增幅 +0.136)则是色标偏离曲线------而非位置压缩------导致 CV 上升的最明显案例。相邻步 ΔE00 最小值为 0.000 的预设(viridis、cividis、summer、autumn、gray、bone、copper)存在完全重复的 LUT 条目,重复条目多位于色带中段或色标间距小的区段:gray/summer/autumn/copper 的重复出现在 level 128(L* 44--72),是 256 个条目覆盖 255 个输出级的取整鸽笼伪差;viridis/cividis 的重复散布在 level 67--156 / 6--144(L* 32--54 / 15--49,色标间距小的区段);只有 bone(level 39,L* 9.3)落在暗端。这属量化冗余而非缺陷,它意味着这些色带改用 256 级量化输出几乎不损失可见细节------口径注:示例界面的 Color Table = 256 走 colorTable(256)(采样网格 pos = i/255),与本文所用 colorTable256() 的 pos = i/256 不是同一张网格,但结论相同。

4.5 色觉缺陷下的可区分性

表 3:相距 ≥32 级的最小 ΔE00(farMin,可区分性下界)
预设 原始 protan deutan tritan
viridis 8.242 6.743 5.264 7.265
plasma 7.373 3.641 6.828 7.728
inferno 8.175 4.507 6.976 7.898
magma 8.147 5.031 7.003 7.828
cividis 5.719 5.312 5.061 4.550
jet 13.147 5.001 4.579 5.381
hot 4.215 2.787 3.914 1.207
cool 4.767 0.000 0.333 4.233
spring 4.404 3.133 4.844 2.513
summer 5.666 3.892 4.555 4.683
autumn 1.601 1.591 0.783 2.530
winter 2.423 1.473 1.857 1.683
gray 6.748 6.748 6.748 6.748
bone 7.648 7.566 7.373 5.012
copper 6.731 6.496 6.620 5.416
rainbow 6.529 0.297 0.130 1.198
hsv 6.529 0.297 0.130 1.198
turbo 12.879 1.524 1.795 4.376
coolwarm 10.250 10.008 9.961 10.301
rdylbu 12.487 9.632 8.866 13.237
rdylgn 8.292 1.332 0.182 10.617
spectral 6.636 1.767 0.368 6.231

原始视觉下 farMin 最低的是 autumn(1.601):红-黄色带的总色程只有 62.44 ΔE00(表 2 的 path),相距 1/8 量程的两点本就只隔约 1.6 JND,这是色域选择而非缺陷。色觉模拟后的塌缩才是重点,deutan 列的最坏情形(脚本逐条打印):

预设 deutan farMin 同色的两个位置
rainbow / hsv 0.130 t = 0.620 与 t = 0.745(蓝-青段与蓝-品红段)
rdylgn 0.182 t = 0.439 与 t = 0.565(中心黄两侧的红臂与绿臂)
cool 0.333 t = 0.616 与 t = 0.741
spectral 0.368 t = 0.451 与 t = 0.576
autumn 0.783 t = 0.024 与 t = 0.149
turbo 1.795 t = 0.643 与 t = 0.769

rdylgn 与 spectral 的失效模式最具误导性:它们在正常色觉下是 A 级均匀色带,但红臂与绿臂在 deutan/protan 下塌缩成同一片橄榄色(rdylgn 见上图第 6 行;spectral 未入图,机理相同),中心两侧的符号信息对红绿色盲完全丢失。cool 在 protan 下 farMin = 0.000:相距 ≥ 32 级的两个色级在模拟并量化到 8-bit 后严格同色,色带在 protan 视角下产生完全碰撞------青→品红的路径本靠红-绿对立轴承载区分度,失去该轴后模拟色带折返,注入性不再成立。相对地,coolwarm(蓝-白-红)与 rdylbu(红-黄-蓝)在三种色觉下 farMin 均 ≥ 8.87,因为蓝-红对立轴在 protan/deutan 下保持分离;cividis 作为专为色觉缺陷优化的预设,三种色觉下 farMin 稳定在 4.55--5.31,没有短板。

4.6 灰度打印安全性

表 4:灰度剖面(L* 即灰阶)
预设 min |ΔL*| reg 两端 ΔL* 灰度单调
viridis 0.0000 0.34 75.53 是
plasma 0.0694 0.00 78.42 是
inferno 0.0591 0.00 96.83 是
magma 0.0289 0.00 96.77 是
cividis 0.0000 0.00 77.14 是
jet 0.0023 71.76 13.50 否
hot 0.0027 0.00 99.93 是
cool 0.0008 6.31 30.94 否
spring 0.0008 0.79 36.56 是
summer 0.0000 0.00 49.96 是
autumn 0.0000 0.00 43.64 是
winter 0.0024 1.10 55.88 是
gray 0.0000 0.00 99.65 是
bone 0.0000 0.00 99.65 是
copper 0.0000 0.00 83.44 是
rainbow 0.0011 75.03 0.01 不适用
hsv 0.0011 75.03 0.01 不适用
turbo 0.0232 65.31 13.88 否
coolwarm 0.0723 0.40 0.36 是(两臂)
rdylbu 0.0025 0.29 5.61 是(两臂)
rdylgn 0.0980 0.29 4.62 是(两臂)
spectral 0.0869 0.29 5.93 是(两臂)

灰度化保留的信息只有 L ∗ ( t ) L^*(t) L∗(t)。顺序预设中 16 种里有 13 种灰度单调,可直接用于黑白打印;jet、turbo、cool 三种灰度非单调,打印后黄段亮带与两端暗区会制造伪等值线(jet/turbo 见上图第 4、5 行;cool 未入图)。发散预设的两臂各自单调,但符号信息能否存活取决于两端 ΔL*:coolwarm 两端只差 0.36 L*,灰度图上正负臂完全同灰,符号只能靠图例文字补救;rdylbu/rdylgn/spectral 两端差 4.6--5.9 L*,灰度下仍可辨方向。循环预设在灰度下退化为来回振荡的灰阶(两端 ΔL* = 0.01),周期信息全部丢失------循环数据本来就不该用亮度承载。

4.7 与 matplotlib 参考表的对照

表 5:QWT LUT vs matplotlib 256 级表(同网格 pos = i/256)
预设 均值 ΔE00 最大 ΔE00 色标均值 色标最大
viridis 6.630 12.115 5.875 11.362
plasma 0.537 1.242 0.383 0.747
inferno 13.520 28.305 11.879 28.305
magma 9.688 17.270 8.557 17.239
cividis 7.447 12.078 6.762 11.978
jet 2.634 8.978 2.613 8.978
hot 5.146 16.270 6.118 15.916
cool 0.106 0.388 0.000 0.000
spring 0.228 0.644 0.000 0.000
summer 0.118 0.392 0.000 0.000
autumn 0.162 0.358 0.000 0.000
winter 0.243 0.823 0.000 0.000
gray 0.133 0.378 0.000 0.000
bone 4.932 8.751 3.457 7.178
copper 7.918 12.509 0.000 0.000
rainbow 48.443 90.742 45.512 89.898
hsv 1.630 4.393 1.683 3.097
turbo 19.170 39.277 16.892 39.176
coolwarm 4.537 8.239 1.981 4.724
rdylbu 0.403 1.471 0.169 0.460
rdylgn 0.292 0.749 0.147 0.369
spectral 0.402 1.322 0.146 0.395

"同名"不等于"同色"。溯源探针(脚本 7 节,对每个色标在 matplotlib 曲线上做 4096 点最近匹配)给出三类情形。第一类,色标精确落在参考曲线上:plasma 全部 9 个色标 dE = 0.000 且最近位置 ≈ 标称位置,cool/spring/summer/autumn/winter/gray 的首末色标精确、中间无或仅线性段,整带偏差 ≤ 0.83(copper 首末色标同样精确,但 mpl 版是非线性 ramp,整带均值偏差达 7.92,归入 5.2 节第 8 条)。第二类,色标颜色取自参考曲线但位置被压缩 :inferno 的 9 个色标在 mpl 曲线上最近位置为 t = 0, 0.0977, 0.1993, 0.2969, 0.3985, 0.5001, 0.5978, 0.6994, 0.9963(每个 dE = 0.000),即 mpl 曲线 t∈0,0.7 的颜色被摆到了位置 0,0.875 上,末段再从 t=0.7 直跳 t=1.0------整带因此均值偏差 13.52、最大 28.31;turbo 近似同型(色标到曲线的最近距离 0.70--2.27 ΔE00,最近位置 0, 0.086, 0.203, 0.289, 0.391, 0.500, 0.598, 0.711, 0.996)。第三类,色标本身偏离参考曲线(以下 dE 均为到 mpl 曲线的最近距离,同位置差异另见表 5 的"色标最大"列):viridis 前三个色标在曲线上、其后逐渐漂移(t=0.5 的色标最近匹配在 0.422,dE 2.62;t=0.75 的色标 dE 6.52),cividis 全带偏离 0.77--5.82,magma 偏离 ≤ 2.31 且位置压缩。rainbow 则是另一张地图:QWT 的 rainbow 是首尾同红的闭合色相环(OpenCV 式),matplotlib 的 rainbow 是紫起红止的非闭合 ramp,均值偏差 48.44 属于定义差异而非近似误差。

已知问题 --- 部分预设色标与 matplotlib 参考不一致

源码注释称色标"sourced from matplotlib colormaps",但实测 inferno/turbo 存在 t∈0,0.7→位置 0,0.875 的系统性压缩,viridis/magma/cividis 的中间色标与 matplotlib 同位置颜色最大偏差 11.4--17.2 ΔE00(若取到参考曲线的最近距离则 ≤ 6.6)。本文评测的是 QWT 实际出货的色标数据;若未来版本修正色标,inferno/magma 的 CV(0.587/0.582)预计回落到 mpl 自身水平(0.332/0.348),分级相应上调。跨工具对比图时请记住:同名预设在两个库里的像素并不相同。

5. 讨论

5.1 场景化选择指南

下表每一行的推荐都直接引用前文的实测数字,而非印象。

表 6:数据类型 × 观察者 × 输出 → 推荐预设
场景 推荐 定量依据
顺序数据,正常色觉,屏幕 viridis(或 plasma) A 级;原始 farMin 8.24;CV 0.414
顺序数据,红绿色盲读者,屏幕 cividis deutan farMin 5.06、protan 5.31、tritan 4.55,三种色觉无短板;A 级
顺序数据,暗色背景 inferno / magma L*₀ = 0.08,低值融入背景;注意 CV 0.58 的步长不均
顺序数据,灰度/黑白打印 gray(或 viridis/cividis) 灰度单调且两端 ΔL* 75.5--99.7;jet/turbo/cool 灰度非单调,禁用
发散数据(含符号),正常色觉,屏幕 coolwarm A 级;两臂单调 reg 0.40;三种色觉 farMin ≥ 9.96
发散数据,红绿色盲读者 coolwarm 或 rdylbu deutan farMin 9.96 / 8.87;rdylgn/spectral 在 deutan 下 0.18/0.37,禁用
发散数据,灰度打印 rdylbu(或 rdylgn/spectral) 两端 ΔL* 5.61/4.62/5.93,符号可辨;coolwarm 两端 ΔL* 0.36,符号丢失
周期数据(相位、角度),屏幕 rainbow / hsv 唯一首尾同色的循环预设(首末 ΔE00 = 0);接受 deutan farMin 0.13 的代价,或改用等值线/箭头编码
兼容旧 MATLAB 代码 jet 仅作兼容;新图避免:reg 71.76、灰度非单调、deutan farMin 4.58
必须彩虹观感(兼容场景) turbo 或 jet,均 C 级 二者指标互有胜负(CV 0.400 vs 0.401):turbo 亮度回退略小(65.31 vs 71.76),jet 在色觉缺陷下可区分性更高(deutan 4.58 vs 1.80);灰度打印均禁用

两条横切建议。其一,颜色条与数据图必须共用同一 colormap 实例的语义(示例中为光谱图与 QwtScaleWidget 各 create() 一次),否则颜色条的评测结论与图面不一致。其二,当读者群体未知时,顺序数据选 cividis 的期望损失最小:它在正常色觉下是 A 级(CV 0.386),在三种色觉与灰度下都没有塌缩点,代价只是色相变化比 viridis 含蓄。

5.2 失效模式诚实清单

评测方法本身的局限与本次实验的负面结果列在这里,避免把数字读成绝对真理。

  1. ΔE00 对空间频率与面积不敏感。相邻步 ΔE00 评测的是颜色条上的色块对,而真实图面上的感知还受区域大小、边界对比与同时对比影响;同一色带在细等值线与大面积填充下的可读性不同,本评测不覆盖。
  2. CVD 矩阵是线性近似且只取严重度 1.0。Machado 模型在二色觉端是 3×3 线性变换,忽略了瞳孔、晶状体黄斑色素密度等个体差异;真实人群中异常三色觉(severity 0.2--0.8)比完全二色觉更常见,其辨别力介于本文"原始"与"severity 1.0"两列之间,本文只给出包络的两端。
  3. 256 级网格引入量化伪差 。多个预设的相邻步 ΔE00 最小值为 0.000(重复条目,多位于色带中段或色标间距小的区段,仅 bone 在暗端,见 4.4 节),说明这些区段的 8-bit 网格已粗于色带步长;若改用 colorTable(1024) 或连续路径,相应统计会变化。farMin 的 gap=32 也是约定值,换 gap 会换数字(方向不变)。
  4. 分级阈值是约定而非物理 。CV 0.45/0.75 与 reg 2/8 的边界使 spring、spectral(CV 0.449)与 A/B 边界几乎重合;cool 的 B 级来自 reg 6.31 与阈值 8 的距离,若把阈值定在 5 则降为 C。表中原始数字比字母更可靠。
  5. 灰度打印只由 L 近似 *。真实印刷是减色法(CMYK、纸张白度、网点增益),本文的"灰度"是屏幕 L*,对喷墨/激光打印是上界估计;min |ΔL*| = 0 的预设在纸上还会叠加网点量化。
  6. 评测与数据分布解耦 。所有指标假设数据在 vMin, vMax 上均匀占用色带;真实数据偏斜或含离群值时,实际生效的色带区段可能只是 LUT 的一小截(例如 jet 的亮带恰好覆盖 90% 数据时伪边界反而不可见),vMin/vMax 的选取往往比色带选择影响更大。
  7. 预设数据与参考实现的偏差被如实计入 。QWT 的 viridis 不是 matplotlib 的 viridis(均值差 6.63),因此文献中"viridis 感知均匀"的结论不能无条件搬到 QWT 出货色标上;本文的 A/B/C 分级只描述 QWT 实际数据。
  8. 负面结果 :autumn 的原始 farMin 仅 1.601(色程太短),cool 在 protan 下 farMin = 0.000,copper/bone 与 mpl 同名表偏差 7.9/4.9(二者在 mpl 中是非线性 ramp,2/4 色标线性近似必然失真)------这些预设的"简单直观"是以信息容量为代价的。

6. 代码实践

6.1 创建与取色

cpp 复制代码
#include <qwt_colormap_preset.h>

// 按名字创建(大小写不敏感;查无此名时静默回退 viridis,务必拼对)
std::unique_ptr< QwtLinearColorMap > cmap = QwtColorMapPreset::create("cividis");

// v7.3.1+ 的统一签名:rgb(vMin, vMax, value)
const QRgb c = cmap->rgb(0.0, 100.0, 42.0);

// 查询与互转
const QStringList names = QwtColorMapPreset::availablePresets();      // 22 个名字
const auto stops = QwtColorMapPreset::colorStops(QwtColorMapPreset::Jet);
QwtColorMapPreset::Preset p = QwtColorMapPreset::presetFromName("TURBO");

6.2 光谱图 + 颜色条

颜色条与图面各持有一个 colormap 实例(setColorMap() 转移所有权),因此要 create() 两次,不能共享指针:

cpp 复制代码
#include <qwt_plot_spectrogram.h>
#include <qwt_scale_widget.h>

QwtPlotSpectrogram* spec = new QwtPlotSpectrogram();
spec->setColorMap(QwtColorMapPreset::create("coolwarm").release());
spec->attach(plot);

QwtScaleWidget* axis = plot->axisWidget(QwtAxis::YRight);
axis->setColorBarEnabled(true);
axis->setColorMap(interval, QwtColorMapPreset::create("coolwarm").release());

警告 --- 所有权与 NaN

setColorMap() 接管指针所有权,同一个 QwtColorMap* 设置给两处会 double free;连续路径对 NaN/Inf 返回透明黑(rgb() 返回 0),索引路径映射到索引 0,两条路径的缺失数据语义不同,混用时注意图例解释。

6.3 面向色盲与打印的自检

把第 4 节的度量变成两行代码级别的自检习惯:导出灰度版本(或临时把 colormap 换成 gray)检查亮度单调性;对红绿对立的发散数据(rdylgn/spectral)在交付前用 deutan 模拟过一遍------本文表 3 的数字说明,正常色觉下的 A 级色带在 deutan 下可能只有 0.18 的可区分性。3D 侧无需重复这套工作:Qwt3DTheme::setDataColorPreset("plasma") 内部走同一个 QwtColorMapPreset 工厂,2D 的评测结论直接适用。

7. 小结

colormap 评测可以完全定量化:亮度单调性、步长 CV、色觉模拟下的 farMin 三个数足以推翻或支持任何一条"推荐"。QWT 的 22 个预设里,感知均匀族与发散族在正常色觉下表现良好(11 个 A 级),真正的风险集中在三处:jet/turbo 的亮度崩塌(reg 65--72 L*)使它们不适合任何需要灰度或低对比阅读的场合;红绿对立的发散色带(rdylgn/spectral)与闭合色相环(rainbow/hsv)在 deutan 下塌缩到 0.13--0.37 ΔE00,符号与周期信息对约 8% 的男性读者不存在;coolwarm 的符号信息只活在色相里,灰度打印即丢失。预设数据本身也有待修正的工程债:inferno/turbo 的色标位置压缩与 viridis/cividis 的曲线偏离,使"与 matplotlib 同名"目前只意味着相近而非相同。本评测的边界同样清楚:它评的是颜色条上的 256 个色块,不是真实图面上的感知;它给的是严重度 1.0 的包络端点,不是人群分布的期望。在这两个边界之内,表 6 可以直接用作选型依据。

8. 参考文献

  1. Sharma, G., Wu, W., Dalal, E. N. The CIEDE2000 color-difference formula: Implementation notes, supplementary test observations, and observations. Color Research & Application, 30(1):21-30, 2005.(含验证数据集,本文自测 22 组)
  2. Machado, G. M., Oliveira, M. M., Fernandes, L. A. A physiologically-based model for simulation of color vision deficiency. IEEE Transactions on Visualization and Computer Graphics, 15(6):1291-1298, 2009.(严重度 1.0 矩阵取自作者公开表)
  3. Moreland, K. Diverging color maps for scientific visualization. Advances in Visual Computing, 5th International Symposium, 2009.(发散色带设计准则:亮中性中心、两臂等感知权重)
  4. IEC 61966-2-1:1999. Multimedia systems and equipment --- Colour measurement and management --- Part 2-1: Colour management --- Default RGB colour space --- sRGB.
  5. CIE 15:2004. Colorimetry, 3rd edition.(CIELAB 定义与 D65 白点)
  6. QWT7 源码:src/core/qwt_colormap_preset.h/.cpp(22 种预设色标)、src/core/qwt_colormap.h/.cpp(QwtColorMap 基类、ColorStops 插值、colorTable256)、src/plot/qwt_scale_widget.cpp(颜色条)、src/plot3d/qwt3d_colormap_color.h(3D 桥接)。
  7. 实验脚本:docs/assets/algorithms/colormap_experiment.py(确定性,全部原始数字打印到 stdout)。
  8. QWT7 示例:examples/2D/colormap(22 预设交互对比,支持 --screenshot 批量导出)。

源码托管于:

如果这个库对你的工作有帮助,点个 star 是最好的鼓励。

相关推荐
宿州派大星6 小时前
Python实战:PyQt5 + OpenCV + Dlib 打造AI桌面应用
人工智能·python·qt
长沙红胖子Qt16 小时前
Qt+TiePieHS3开发笔记(二):HS3发送器发出内置波形、直连示波器,示波器检测内置波形Demo
qt·示波器·tiepiehs3·发生器·同步发射和示波器采集
陈年老古董19 小时前
PyQt5 入门指南:环境搭建、PyCharm 外部工具配置与登录界面实战
开发语言·qt·pycharm
zhanghaha131419 小时前
Qt 类完整例程【逐词拆解 + 逐步骤执行保姆教程】
开发语言·qt
Cx330❀1 天前
Qt 界面美化指南:从零掌握 QSS 核心机制与工程实践
开发语言·qt·ui·性能优化·图形渲染
zx_741484811 天前
PyQt5 环境搭建保姆级教程:PyCharm + QtDesigner + PyUIC 完整配置
开发语言·qt·pycharm·pyqt
Python图像识别1 天前
46-【2027毕设】YOLO11田间杂草检测识别系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·qt·课程设计
沫璃染墨2 天前
《Qt从零入门系列(十三):QSS样式表详解——从选择器与层叠规则到控件美化实战》
开发语言·css·c++·qt·ui·qt6.3·qss
好好学习O(∩_∩)O2 天前
[QT] 多线程moveToThread 和易踩的8个坑
开发语言·qt