多數計算機圖形圖像,是通過光栅顯像顯示給使用者的。這種系統将圖像作為像素陣列進行顯示,像素即圖像元素。這些像素采用RGB顔色空間。
一、光栅顯像
桌上型電腦和投影顯示器的顯示技術有多種。這些顯示器的分辨率和實體尺寸各不相同。程式設計人員通常假設像素排成矩形陣列,又稱為光栅。
像素
光栅顯示器上可顯示元素稱為像素。在顯示器上,通常用有序對(i,j)來表示像素的索引,即表示該像素所在的行和列。如果一台顯示器具有m行,n列像素,則左下角的元素是像素(0,0),右上角的元素是像素(m-1,n-1)。
我們需要用實際的二維螢幕坐标來表示像素的位置。随着API的不同,這些系統在細節上會有所不同,但是最常使用的是用整數點陣作為像素中心,如下圖中4X3螢幕所示。由于像素占據一定的空間,是以距離像素中心具有0.5個機關的過沖。

實體像素,就是硬體上實際可顯示的元素。不同的顯示系統采用不同的實體像素,多數顯示系統的像點呈水滴狀或者呈方形。水滴狀像點,即在像素中間密度最大,向邊緣方向密度逐漸減小;方形像點,幾乎都是正方形,并且各正方形之間有一段小間隙,這樣可使控制電路獲得像素資訊。
二、顯示器的亮度和γ值
現代顯示器采用數字信号輸入方式,接收的是數字信号表示的像素"值",然後将其轉化為亮度值。斷電後顯示器的亮度實際上不是零,因為螢幕能夠反射環境管線。不過我們可以認為此時顯示器呈"黑色",顯示器完全打開是呈"白色"。對像素顔色用0到1的數值來表示。黑色為0,白色為1,介于黑白中間的中間灰色為0.5。注意這裡"中間"指的是從像素發出光線的強度,而不是指觀察到的亮度。 為了在顯示器上産生圖像,要明白兩個關鍵問題。第一,顯示器輸入信号的處理是非線性的。對于多數顯示器,一般利用γ值來近似表示其非線性,具體數值公式如下: 顯示亮度=(最大亮度)pow(α,γ) (1) 其中α是介于0到1之間的輸入亮度值。如顯示器的γ值為2.0,輸入值α=0.5,則顯示亮度是最大亮度的1/4。用γ值表示顯示器的非線性知識一階近似,實際中在估計裝置的γ值時不需要很高的精度。一種度量非線性的直覺方法是,找到能産生黑白之間的中間亮度的α值,即下式中的α值: 0.5=pow(α,γ) (2) 如果能找到滿足上式的α值,則能夠求出γ值: γ=ln(0.5)/lnα (3) 一旦知道了γ值,就可以對輸入進行伽馬校正,使得輸入值α=0.5可以在螢幕上顯示出介于黑白中間的灰階效果。變換方法為: α=pow(α,1/γ) (4) 把該式帶入(1)式可得: 顯示亮度=γ*(pow(α,1/γ))*(最大亮度)=α*(最大亮度) 實際顯示器的另一個重要特征是,輸入值通常經過了量化處理。是以,我們能夠在浮點範圍[0,1]内處理亮度值。輸入顯示器的資訊一般是大小固定的非負整數,取值範圍一般是0-255,可采用8位二進制數存儲。
三、RGB顔色
計算機圖形學中,多數顯示效果都有RGB顔色空間确定。RGB顔色空間比較簡單,經轉換能夠直接控制多數計算機螢幕的顯示。下面我們将從使用者觀察的角度讨論RGB顔色空間,RGB顔色空間的基本思想是,将紅、綠、藍三種基色光混合産生新的顔色。對于RGB加性顔色空間,我們有下列混合效果:
紅色 + 綠色 = 黃色 綠色 + 藍色 = 青色 藍色 + 紅色 = 紫紅色 紅色 + 綠色 + 藍色 = 白色 “青色”是一種藍綠色,“紫紅色”是一種紫色。 如果讓基色光的強度有最小到最大變化,就能在RGB顯示器上顯示所有的顔色。根據慣例,把單色的最亮狀态表示為1,則顔色值為0-1的分數。這樣就産生出一種三維RGB顔色立方體,紅、綠、藍為坐标軸,坐标值由0-1變化。
四、α通道
我們經常想隻覆寫像素的部分内容。最常見的是,在合成圖像時,我們想在背景上面插入一幅前景圖檔。對于前景中不透明的像素,我們隻需要用它們替換相應位置的背景像素即可,對于完全透明的前景像素,背景像素不再改變。而對于半透明的像素,目前景圖檔中有像玻璃這樣的半透明區域時,或者在前景圖檔中有子像素孔洞時,就會出現半透明的像素。在有像素孔洞這樣的情況下,為了将前景與背景混合,就要權衡像素作為前景的分數。該像素用α表示。如果想把前景色cf與背景色cb混合,并且被前景覆寫的像素的分數為α,那麼就可以采用下面的公式: c = α*cf + (1-α)*cb 其中,需要注意的是,圖中的α圖像可以存儲為RGB圖像,也可以存儲為單通道的灰階圖。 利用上式合成圖像的示例,實際上在前景圖與背景圖混合之前,先經過α通道進行了修剪處理。
五、直線繪制
多數圖形軟體都包含直線繪制指令,即捕捉螢幕上的兩個點的坐标,并在它們之間畫一條直線。對一般的螢幕坐标點(x0,y0)和(x1,y1),正常做法是在它們之間畫出合适的像素點集,這些點近似一條直線。為了簡化處理,x0、x1、y0、y1的值經正常定為整數(像素中心),因為直線本身就比較粗略,要求子像素精度并不合适。我們基于直線方程進行直線繪制,有兩種類型的方程可供選擇:隐式方程和參數方程。
1、基于隐式方程繪制直線
基于隐式方程繪制直線最常用的算法是中點算法。中點算法結束時繪制的直線和Bresenham算法的結果一樣,隻是中點算法更加直接。 首先找到直線的隐式方程: f(x,y) ≡ (y0-y1)*x + (x0-x1)y + x0*y1-x1*y0 = 0 假設x0≤x1。若不滿足該條件,交換點的順序使之成立。直線的斜率為: m = (y1-y0)/(x1-x0) 在下面的讨論中都假設m∈(0,1]。類似的,可以推出m∈(-∞,-1]、m∈(-1,0]和m∈(1,∞)時的結果。這四種情況覆寫了所有的可能情況。 當m∈(0,1]時,直線在x軸上的變化速度大于在y軸上的變化速度。我們可以忽略對于y軸向下的坐标系,可以不考慮幾何上的"升"和"降",因為兩種情況的代數表示都是一 樣的。中點算法的重要假設是,我們能繪制出沒有間隔的最細的直線,兩對角像素之間的連接配接被認為沒有間隔。 繪制直線時,從左端點向右端點進行。隻有兩種可能:繪制一像素時,和左邊所繪像素高度一樣,或者高出一個像素。在兩端點之間每一列,總是隻有一個像素。沒有 像素就意味着有間隔,有兩個像素就意味着太粗。就我們現在讨論的情況來看,同一行上可以有兩個像素,因為直線更接近于水準,是以其走勢是向右或者向上。這種認識 如圖所示:
針對m∈(0,1]情況中的中點算法,首先建立最左邊的像素,以及最右邊像素的列号(x值),然後沿水準方向循環建立每個像素的行号(y值)。該算法的基本形式如下: y = y0 for x = x0 to x1 do draw(x,y) if(condition) then y += 1 其中x,y都是整數。簡單的說就是從左至右不斷繪制像素,在該過程中有時需要向y軸正向有所偏移。關鍵是如何确定if 一種有效的方法是,參考兩候選點之間的中點位置。更具體地說,對于剛繪制的像素點(x,y),它在螢幕上的坐标為(x,y),下面要繪制的候選像素為(x+1,y)和(x+1,y+1)。兩 個候選點之間的中點為(x+1,y+0.5)。如果直線通過中點的下方,就繪制下面的像素,如果直線通過中點的上方,就繪制上面的像素,如圖所示:
判斷直線是通過點(x+1,y+0.5)的上方還是下方,可以通過直線方程f(x+1,y+0.5)的值來判斷。在 計算機圖形學的數學知識中讨論過,位于直線上的點(x,y),滿足f(x,y)=0;位于直線一邊的點滿足f(x,y)>0;位于直線另一邊的點滿足f(x,y)<0。由于-f(x,y)和f(x,y)=0都可以作為直線的方程,是以很難利用f(x,y)的正負來快速判斷(x,y)究竟位于直線的上方還是下方。但是在直線方程中的關鍵項是關于y的項(x1-x0)*y。要注意(x1-x0)肯定為正數,因為x1>x0。這就意味着随着y的增大,(x1-x0)*y項的值将變大。是以f(x,+∞)肯定為正,并且位于直線的上方,也就是說直線上方的點都是正的。另一種判斷方法是,由于梯度向量中的y分量是正的,是以在直線的上方y可以任意增加,f(x,y)也就肯定是正的。于是我們可以把語句中的條件寫具體,代碼更明确了: if f(x+1,y+0.5)<0 then y += 1 對于有适當斜率(介于0和1之間)的直線,上面的代碼非常有效。 如果希望有更高的效率,就使用增量方法。增量方法利用上一步計算的結果,是循環更高效。在上述中點算法中,主要步驟是計算f(x+1,y+0.5)的值。可以看出,在循環内部,經過第一次疊代,我們已經算出f(x-1,y+0.5)或者f(x-1,y-0.5)的值。顧存在以下關系: f(x+1,y) = f(x,y)+(y0-y1) f(x+1,y+1)=f(x,y)+(y0-y1)+(x1-x0) 于是就有了如下增量算法的代碼: y = y0 d = f(x0+1,y0+0.5) for x = x0 to x1 do draw(x,y) if d < 0 then y += 1 d = d + (y0-y1)+(x1-x0) else d = d + (y0-y1) 與非增量算法相比,該增量算法需要的額外調整很少,是以具有更高的運作效率,但是對于長直線來說,由于包含很多次加法運算,會存在一定的數值誤差。 在程式中,如果隻有整數參加運算,算法執行起來會更快。由于已經規定x0、x1、y0、y1位整數,上面的算法也就是整數運算了。但是,初始化時還是要用到d=f(x+1,y+0.5)。将該表達式進行擴充得到: f(x0+1,y0+0.5)=(y0-y1)*(x0+1)+(x1-x0)*(y0+0.5)+x0*y1-x1*y0 其中y0+0.5不是整數運算,結果是非整數之間做乘法運算。我們已經知道如果f(x,y)=0是直線方程,那麼2f(x,y)=0也是同一直線方程。是以,我們可以用2f(x,y)代替f(x,y),于是d的表達式就變成: 2f(x0+1,y0+0.5)=2*(y0-y1)*(x0+1)+(x1-x0)*(2y0+1)+2*x0*y1-2*x1*y0 上面的所有項都成了整數項,算法代碼變為: y = y0 d =2*(y0-y1)*(x0+1)+(x1-x0)*(2y0+1)+2*x0*y1-2*x1*y0 for x = x0 to x1 do draw(x,y) if d < 0 then y += 1 d = d + 2*(y0-y1)+2*(x1-x0) else d = d + 2*(y0-y1) 注意:在執行上面的全整數運算代碼之前,必須檢查m∈[0,1)是否滿足,我們可以用下式來判斷: (y1>=y0)&&((x1-x0)>(y1-y0)) ≡ m∈[0,1)
2、基于參數方程繪制直線
我們在 計算機圖形學的數學知識中讨論過,過兩點p0 = (x0,y0)和p1 = (x1,y1)的直線參數方程為: (x,y) = (x0+t*(x1-x0),y0+t*(y1-y0)) 等價向量為: p = p0 + t*(p1-p0) 如果直線斜率m∈[-1,1],那麼for循環就可以沿x軸繼續執行,而且代碼很簡潔。對于[-1,1]之外的m,可以得到類似的算法。因為t是沿直線推進固定的長度,直線的x和y分量也一樣,這樣就可以把t看做x的函數: t=(x-x0)/(x1-x0) 得到的代碼為: for x = x0 to x1 do t=(x-x0)/(x1-x0) y = y0+t*(y1-y0) draw(x,round(y)) 該算法的一個優點是不論x1-x0>0是否成立,算法都能夠正常運作。可以看出基于直線參數方程的代碼非常簡潔,但是不能将代碼設計為隻處理整數。在很多計算機語言中,從浮點數到整數都是采用的截斷處理,是以,round(y)項可以作為(y+0.5)的取整結果。 因為在每次疊代中t和y的變化都是一個常量,是以可采用增量算法: △y = (y1-y0)/(x1-x0) y = y0 for x = x0 to x1 do draw(x,round(y)) y += △y 有時用兩端點的RGB顔色c0和c1來确定直線的顔色。我們希望沿着直線顔色能夠均勻的改變。如果想根據t∈[0,1]對線段參數化,則可用下面的公式: c = (1-t)*c0 + c1 這樣就能算出每個像素的顔色。
六、三角形光栅化
在螢幕坐标系中,我們經常會想到繪制一個過二維點p0=(x0,y0)、p1=(x1,y1)和p2=(x2,y2)的二維三角形。與繪制直線一樣,希望能夠根據頂點的數值插入顔色或者其他性質。如果存在重心坐标系,就可以直接計算插入值。例如,假設頂點的顔色值為c0、c1、c2,在重心坐标為(α,β,γ)的三角形内部,某點的顔色值為: c=α*c0 + β*c1 + γ*c2 這種顔色插值方法在圖形學中稱為Gouraud插值法。 光栅化三角形的另一個特别之處是,我們通常對具有公共頂點和公共邊的那些三角形進行光栅化。這就意味着要光栅化的是相鄰三角形,是以沒有空隙。可以使用中點算法畫出每個三角形的輪廓,然後填入内部像素,進而實作三角形光栅化。是以,相鄰三角形在鄰邊出都繪制了相同的像素。如果相鄰三角形的顔色不同,則圖形将由兩個三角形的繪制順序決定。為了避免順序問題并且消除空隙,最常用的三角形光栅化方法是利用下面的約定:當且僅當一個像素的重心位于三角形内部時,才繪制該像素。也就是說,像素中點的重心坐标都在區間(0,1)内。關鍵結論在于:當我們根據頂點插入顔色時,利用重心坐标系可以确定是否該繪制一個像素,以及該像素是什麼顔色。于是問題變為如何能快速找到像素中心的重心坐标。蠻力光栅化算法如下: for all x do for all y do compute(α,β,γ) for (x, y) if (α∈(0, 1)) && (β∈(0, 1)) && (γ∈(0, 1)) then c=α*c0 + β*c1 + γ*c2 drawpixel(x, y) whith color c 算法的其餘部分把外部循環限制在更小的候選像素集,使重心計算快速有效。 尋找三頂點的包圍矩形,隻對該矩形内的候選像素執行循環,這樣就能提高算法效率。利用β=fac(x,y)/fac(xb,yb)來計算重心坐标。于是算法變為: xmin = floor(xi) xmax = ceil(xi) ymin = floor(yi) ymax = ceil(yi) for y = ymin to ymax do for x = xmin to xmax do α = f12(x,y)/f12(x0,y0) β = f20(x,y)/f20(x1,y1) γ = f01(x,y)/f01(x2,y2) if (α > 0 && β >0 && γ > 0) then c = α*c0 + β*c1 + γ*c2 drawpixel(x, y) whith color c 其中fij是根據适當頂點,通知直線方程确定的: f01(x,y) = (y0 - y1)*x + (x1 - x0)*y + x0*y1 - x1*y0 f12(x,y) = (y1 - y2)*x + (x1 - x2)*y + x1*y2 - x2*y1 f20(x,y) = (y2 - y0)*x + (x2 - x0)*y + x2*y0 - x0*y2
其中我們用α>0代替了α∈(0,1)是因為α+β+γ=1,如果它們三個都大于0的話,它們三個必然都是小于1的。同樣我們可以隻算出重心坐标的兩個,然後利用它們的關系求出第三個。與繪制直線的算法類似,這時的算法同樣也可以使用增量算法,每次計算α、β、γ,也就是計算f(x,y)=A*x+B*y+C的值。在内部循環中,隻有x變化,每次變化1.要注意f(x+1,y) = f(x,y) + A,這是增量算法的基礎。在外部循環中,由計算f(x,y)變成計算f(x,y+1),是以計算效率類似。由于在循環中(α,β,γ)的變化增量是固定的,顔色c的變化也一樣,是以該算法也可以變為增量算法。三角形顔色插值的執行個體如下圖所示:
處理三角形邊上的像素
我們還沒有讨論如何處理那些中心位于三角形邊上的像素。如果一個像素正好落在三角形邊上,那麼它同時還位于相鄰三角形的邊上。沒有明确的方式來确定該像素屬于哪個三角形。最壞的決定就是不繪制該像素,這時兩個三角形之間就會出現一個空隙。如果在兩個三角形上都繪制該像素,情況會好一點,但這仍然不是最好的解決方式。如果三角形是透明的,就會出現重複繪制。 螢幕外的任何一點肯定位于要繪制的公共邊的一側。對兩個互不重疊的三角形來說,不在公共邊的頂點位于公共邊的相對兩側。這些頂點中正好有一個與螢幕外的點處在同一側。
f01(x,y) = (y0 - y1)*x + (x1 - x0)*y + x0*y1 - x1*y0
七、簡單反走樣技術
前面講過的直線繪制和三角形繪制算法,普遍存在鋸齒現象。如果允許像素值變低或變高一些,則可以減輕這種視覺上的不足。例如,對于白色背景上的一個黑色三角形,如果一個像素的中心幾乎位于該三角形的内部,則可以将該像素設為黑白之間的顔色。在實際應用中,這種模糊化處理可以改進視覺效果,尤其是在動畫制作中。 構造"無鋸齒"圖像的最直接方法就是采用盒式濾波器,将像素設定為盒式區域的平均顔色值。這就意味着,要把所有可繪制的實體都看做已經明确定義的區域。像中點算法産生的鋸齒狀直線這樣的現象,是走樣造成的結果。是以,通過認真選擇像素值來避免鋸齒現象的技術,稱為反走樣技術。盒式濾波器可以滿足多數情況的需要,适用于對視覺品質要求不是特别高的場合。 實作盒式濾波器反走樣最容易的辦法是,先建立高分辨率圖像,然後進行下采樣。例如,我們要處理一幅256X256的圖像,其中直線寬度為1.2個像素。可以在1024X1024的螢幕上,對寬度為4.8像素的直線段構成的矩形區域進行光栅化處理,然後對像素進行4X4的平均,得到256X256的壓縮圖像。這是對實際圖像的近似盒式濾波,如果目标和對于像素間距不是特别小的話,這種黨閥的效果還是非常好的。
八、圖像捕捉與存儲
1.掃描器和數位錄影機
掃描器和數位錄影機使用某些類型的光感晶片來記憶光能。其核心技術是CCD和CMOS陣列。這些陣列可以感測所有波長的光強。要獲得彩色圖像,一種方法是将光線分成三路,然後經紅、綠、藍三片濾光器進行濾光,經同一矽片的三個濾光器之後就生成了紅、綠、藍三色光。另一種方法是,在矽片的各個傳感器上分别覆寫不同的彩色濾光層。
2.圖像存儲
在多數RGB圖像格式中,紅、綠、藍通道各占8位二進制數,是以存儲一幅百萬像素的元素圖像大約需要3MB。為減少所需的存儲空間,多數圖像格式都進行某種形式的壓縮。這種壓縮分為無損壓縮和有損壓縮。在無損壓縮中 資訊不會丢失;有損壓縮中會丢失部分資訊,而且是不可恢複的丢失。常見的圖像存儲格式包括: gif:有損壓縮格式,隻能表示出256種可能的顔色。圖像品質取決于這256種顔色的選擇。這種格式适用于自然圖像。 jpeg:有損壓縮格式,以人類視覺系統的門檻值為基礎。适用于自然圖像。 tiff:無損壓縮格式,每個像素通常占24位的常用壓縮格式,雖然存在很多其他的可選格式。 ppm:無損壓縮格式,每個像素通常占24位的常用未壓縮格式,雖然存在很多其他的可選格式。 png:無損格式的集合,具有很好的開放源碼管理工具。