如何繪制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啦。