如何绘制CIE1931xy色度图
文章目录
- 如何绘制CIE1931xy色度图
-
- 一点点介绍
-
- 色度学和CIE1931xyY色空间
- 一点点补充说明
- 获取途径和绘制方法
-
- SVG格式
- Matlab自带函数
- Qt下用C++绘制的两种思路
- 一点点小结
转载请注明出处
一点点介绍
色度学和CIE1931xyY色空间
既然要绘制这个图,大家应该对xy色品坐标有所了解。
如果不是很了解的可以参考这个CIE1931xyY色度图-复旦大学.ppt了解一下基本的色度学知识。
简单说明一下:
- 色度图中的外形轮廓线是可见光范围里(约380nm-760nm)单色光颜色轨迹线
- 等能白点E的坐标xy(0.3333,0.3333)
一点点补充说明
大家在网上应该可以找到五花八门的色度图,让人很难分辨到底哪个才是标准的。
对于部分问题,大家可能会有所疑惑的,在这里简单说明:
- 真正意义上的色度图在现有的显示设备上根本显示不了,所以相比于纠结图中的颜色是否准确,应该更加关注坐标数据,这才是将颜色量化为xy坐标的意义。不过能画的好看一些确实有助于理解相关概念。
- 颜色是三维的,色品图是二维的,所以色度图不包含亮度信息。等能白点周围的颜色并不是只有白色,所有灰色、黑色都位于这里。
这里附上几种常见色域在色品图上的位置,图片来源:深入理解 sRGB\Adobe RGB\NTSC\DCI-P3\REC.2020\ProPhoto RGB 色域
获取途径和绘制方法
这部分主要来介绍一下如何获取这个图以及如何在科研、项目里用这个图。
SVG格式
维基百科上提供的SVG格式的图片,以及不同分辨率的PNG图片,可以自行取用。
https://commons.wikimedia.org/wiki/File:CIE1931xy_blank.svg
Matlab自带函数
在
matlab 2017b
及之后的版本中引入了函数
plotChromaticity
用于绘制色度图。
最简单的用法,直接调用函数,
hold on
之后自己就可以在上面涂涂画画了。
plotChromaticity
hold on
## your code ...
绘制结果如下图所示:
简单说下matlab的实现思路
matlab绘制色度图的方法是将上面的区域分割成一个个小四边形,从4个顶点的xy值计算颜色,并对内部进行插值填充。matlab使用patch函数绘制填充颜色,
patch('Vertices',v, 'Faces',f, 'EdgeColor','none', 'FaceVertexCData',xy4rgb(i:i+3,3:5),'FaceColor','interp');
我们将其中控制线条的
'EdgeColor','none'
参数改为
'EdgeColor','k'
可以得到以下结果,方便大家更加形象的了解渐变填充过程。
因为其实每个点只能得到xyz坐标( z = 1 − x − y z=1-x-y z=1−x−y),是计算不了对应的RGB颜色的,还缺少一个Y来控制亮度信息。matlab里实现时候调用了以下代码来将xyz值转为srgb数值:
rgb = images.internal.testchart.xyz2srgb(xyz');
所以可以看到,matlab版本的绘制结果在等能白点周围都是以灰色填充,这取决里matlab具体的换算实现。这个说明顺便帮忙解答我师弟博客下的提问:为什么matlab绘制的色度图白点周围不是白色填充?当然了,我在前面就说了,所有无彩色都集中在这个区域。
另外,这个函数还有几个带参数的重载版本,可以用于绘制一些额外的数据上去,有兴趣的朋友可以参考官方文档中的example学习。
顺便一提,在matlab里进入该函数,里面包含了单色光轨迹线上的63对xy坐标值可用。
更加详细的光谱色度坐标可以参考1931CIE-XYZ标准色度系统。
Qt下用C++绘制的两种思路
由于可能需要在项目里用到,就需要自己实现一下啦,这里给出了在两种在Qt框架下的绘制方法。
- 借助了Qt的线性渐变类
来进行颜色填充。QLinearGradient
QGradient Class
是Qt的渐变填充类,结合QBrush可对任意区域实现快速渐变填充。目前Qt支持三种渐变模式,这里采用线性渐变进行填充。
受matlab填充方式启发,这里选择从等能白点处,即xy(0.3333,0.3333)向单色光轮廓线进行三角区域填充。
QLinearGradient
需要设置填充起点坐标和填充终点坐标,填充起点颜色和填充终点颜色,现有数据为:
- 填充起点坐标:白点(已知)
- 填充终点坐标:曲线轮廓点(已知,需要插值扩充,下方的直线在已知端点的情况下可自行选择填充点的数量)
- 填充起点颜色:可以人为设定白色或者偏灰色,仅为了方便显示和理解(已知)
- 填充终点颜色:人为选取单色光谱中近似对应的蓝色、青色、绿色、黄色、红色五个点,其余点通过插值计算。(已知+计算)
填充颜色的计算方法,即最简单的线性插值计算。但实际上轮廓线是曲线,线性插值会存在一点误差。颜色插值混合可以近似采用格拉斯曼颜色混合定律,假设曲线上蓝色 B B B和青色 C C C之间任意一点,与蓝色距离 d 1 d_1 d1,与青色距离 d 2 d_2 d2,相对位置 p = d 1 d 1 + d 2 p=\frac{d_1}{d_1+d_2} p=d1+d2d1。那么该点的颜色为:
X r , g , b = ( 1 − p ) ∗ B r , g , b + p ∗ C r , g , b X_{r,g,b}=(1-p)*B_{r,g,b}+p*C_{r,g,b} Xr,g,b=(1−p)∗Br,g,b+p∗Cr,g,b
有了上述计算数据,就可以开始绘图了,先把原始数据的63个点进行颜色插值,然后显示出来的效果如下:
这里说明一下为什么要在这些数据点之间进一步插值,因为当前颜色点的渐变幅度较大,直接计算会造成渐变不连续以及严重的边缘锯齿。
假设对已有的63对坐标中间,任意两个点之间再插值9个点,并计算对应的颜色,最后得到的单色光离散点轨迹线如下图所示。
同样基于这些点填充整个色度图,效果如下:
从效果上看基本可以达到要求,上述填充过程重载了
paintEvent
函数直接在背景上画,绘制代码如下:
void MainWindow::paintEvent(QPaintEvent*){
QPainter p(this);
p.fillRect(rect(),Qt::white);
p.setRenderHint(QPainter::Antialiasing);
//坐标缩放
p.scale(width(), height());
p.setPen(QPen(QColor(0,0,0,0),1));
QLinearGradient linearGradient;
//等能白点
CPointF whiteP(1.0/3, 1.0/3, QColor(235,235,235));
//ciePoints包含边界点和对应颜色
for(int i=0;i<m_time-1;i++){
//确定三角形绘制域点
QPointF ps[]{whiteP.toQPointF(), ciePoints[i].toQPointF(), 2*ciePoints[i+1].toQPointF()-ciePoints[i].toQPointF()};
//设置起始坐标,结束坐标,起始颜色,结束颜色
linearGradient.setStart(whiteP.toQPointF());
linearGradient.setFinalStop(ciePoints[i].toQPointF());
linearGradient.setColorAt(0.0, whiteP.C());
linearGradient.setColorAt(1.0, ciePoints[i].C());
p.setBrush((QBrush(linearGradient)));
p.drawPolygon(ps, 3);
}
}
最后再加一个坐标轴以及自适应拉伸就可以使用了,这里其实可以明显看到蓝色和红色处在曲线轮廓线和直线交界处的过渡有点问题,需要特殊处理,再扫描一下覆盖这条白线 。不过说到底还是绘制方式的问题,所以这里还是推荐用第二种绘制方法。
- 自己计算每个像素位置的填充颜色,方便在各类语言和框架下实现。
绘图函数的主要流程都在下面的代码中了,也标注了每一步的功能,整体思路上应该比较容易理解。
QImage CIEDiagram::drawCIEDiagram(int picSize){
//创建QImage对象
QImage cieDiagram(picSize, picSize, QImage::Format_RGB32);
cieDiagram.fill(Qt::white);
//遍历像素
for(int y=0;y<picSize;y++){
QRgb* line = (QRgb *)cieDiagram.scanLine(y);
for(int x=0;x<picSize;x++){
CPointF curP(1.0*x/picSize,1.0-1.0*y/picSize);//将像素位置转换为xy逻辑坐标
if(isPointInsideBound(curP)){//判断当前像素点是否在CIE色度曲线内
CLineF whiteP(white, curP);//构造一个白点与当前像素点的射线
CPointF crossPoint;
areaFlag areaflag = crossArea(curP);//判断射线交曲线于哪个区域
//根据相交区域计算射线与CIE轮廓曲线交点
switch (areaflag) {
case leftA:
crossPoint = getCrossPoint(whiteP, colorPointIdx[bottom], colorPointIdx[top]);
break;
case rightA:
crossPoint = getCrossPoint(whiteP, colorPointIdx[top], colorPointIdx[right]);
break;
default:
crossPoint = getCrossPoint(whiteP, colorPointIdx[right], cieCurvePoints.size());
break;
}
CLineF whiteToBoundLine(white, crossPoint);//构建白点与交点线段
QColor c = whiteToBoundLine.getInterColor(curP);//根据白点、当前像素点、交点进行插值,计算当前像素点颜色
line[x] = c.rgb();
}
}
}
return cieDiagram;
}
最后效果如下:
Qt的源码写的比较乱,最近比较忙,之后整理好了发出来,或者急需的,可以留邮箱
一点点小结
在很多颜色处理的地方都用到了近似颜色插值,从人眼效果上来看基本上是符合的。这主要还是因为实际上人眼能够感知并且分辨的颜色不过几千种,当颜色比较多的时候,这种微小变化对人眼就不会很敏感了,即使我们插值的结果不是很准。
所以在实际使用中,完全可以事先准备一副分辨率较高的色度图,然后要缩放的时候直接对图像线性插值即可,颜色效果完全可以满足需求。只需要把边界轮廓实时绘制一下消除锯齿即可。不过如果用的是矢量图那就完全OK啦。