程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 用Delphi來DIY一個軟件變臉功能

用Delphi來DIY一個軟件變臉功能

編輯:Delphi
< 想法 >

   軟件的“換膚”技術早已不是什麼新鮮事了,但細心的朋友一定已經發現了,現在正悄悄地流行了一種新的改善視覺效果的方法——這裡我斗膽定義為“換膚色”技術吧!用過Winamp 5、Windows MediaPlay 9、MSN Messenger 6、QQ2004這些新版本軟件了吧,呵呵,全都采用了所謂換湯不換藥的“換膚色”技術。挺有意思是吧,下面我們就“自己動手,豐衣足食”。

   < 准備 >

  首先我用eXeScrope打開了WMP9和MSN6的相關可執行文件和動態鏈接庫,沒找到有關界面的資源,晚輩才疏學淺,猜想可能它們的界面是實時計算出來的吧。QQ2004和Winamp5就比較直觀了,一個是直接用BMP文件的,另一個采用的是PNG格式。

  BMP文件沒什麼好說的,關於PNG格式我這裡略說兩句。PNG(Portable Network Graphics)是為了適應網絡數據傳輸而設計的一種圖像格式,用於取代格式較為簡單、專利限制嚴格的GIF圖像文件格式。PNG格式大致具有以下優點:高壓縮率、支持Alpha通道(全透明、全不透明、可變透明)、提供Gamma(圖像亮點)校正機制、提供二維交叉存取機制、支持真彩/灰度/顏色索引的圖像。

  分析了一下Winamp5的圖形界面布局,他許多漂亮的陰影、漸變效果可不是BMP通過指定顏色透明能做到的;另外考慮到一個程序使用圖片皮膚的話文件都會比較多,BMP的話一般都至少有幾百K的總大小;所以我覺得PNG圖片更適合來做絢麗的界面皮膚。

  Delphi默認是不支持PNG格式的圖片的,只能去下載第三方控件了。到DFW論壇裡去搜了很多終於讓我找到了PNGImage這麼個好東東,帶源代碼、幫助文件,無需安裝,支持PNG透明。呵呵,這樣我們就可以開工了!

  < 動手 >

  我先看了一下PNGImage的幫助文件,裡面的《Example 3: Drawing png over other formats》是一個將一幅指定的PNG圖片讀入後覆蓋到一幅JPG圖片上的示例,我嘗試了一下能很好的支持帶透明的PNG文件。因為是要拿這些PNG文件來作程序界面的,所以我首先打算要把這個PNG圖片畫到窗體上去:

   uses
   ..., pngimage;  // 加上這個
  procedure TForm1.FormPaint(Sender: TObject);
   var
   Png: TPngObject;
   Rect: TRect;
   begin
   Png := TPngObject.Create;
   Png.LoadFromFile('1.png');

  Rect.Left := 0;
   Rect.Top := 0;
   Rect.Right := Rect.Left + Png.Width;
   Rect.Bottom := Rect.Top + Png.Height;

  Png.Draw(Canvas, Rect);

  Png.Free;
   end;

  以上代碼實現了將1.png文件讀入後畫到窗體上去,這張圖片是Winamp5的默認主界面,其中右下角有一塊凹入的是透明部分,怎麼樣,效果出來了吧(如圖1)。

 接下來我打算把PNG圖片放到TImage控件裡來做成模擬的按鈕。這個比較簡單,經過幾下嘗試,發現只要“Image1.Picture.Assign(Png);”這一句就可以了,同樣很好的顯示了漸變透明的效果。(注:不能使用“Image1.Picture.Bitmap.Assign(Png);”,雖然這句代碼能畫出圖形,但對於透明是無可奈何的,全部變成黑色;另外不可使用“Image1.Assign(Png);”或“Image1.Picture.Bitmap.Canvas.Assign(Png);”,否則產生運行時類型轉換錯誤,因為TPngObject根本不能轉換為TImage或者TBitmapCanvas類型。)另外對於TImage控件中已經有圖片的情況,想要將PNG圖片蓋上去,可以使用TPngObject對象的Draw方法:

   Rect.Left := 0;
   Rect.Top := 0;
   Rect.Right := Rect.Left + Png.Width;
   Rect.Bottom := Rect.Top + Png.Height;
  Png.Draw(Image1.Canvas, Rect);
   Image1.Refresh;

  注意:此處不能使用TImage的方法,不然原圖就沒了;而且還需要調用TImage.Refresh後才能顯示更改後的圖片(如圖2,左邊是顯示單PNG圖片,右邊是將PNG圖片蓋到已有的位圖上去)。

   說了這麼多,現在該考慮我們的重點內容“換膚色”了。我考慮的基本原理是:先將所有界面相關的圖片都做成灰階PNG圖片,可以做出顏色漸變、立體等各種效果;然後用指定的色彩“蒙”到灰階圖片上去。想起來簡單,可實際動起手來發現還是碰到了好多問題。因為對RGB顏色和位圖只了解一點,一開始便胡亂猜想是不是拿灰階圖片中的某一點像素的RGB值去和指定顏色的RGB值做邏輯與運算(呵呵,讓人笑話了),編了點代碼試了試,對於幾種顏色(黑、白、紅、綠、藍、黃、桃紅)的確能“蒙”出正確顏色來(通過和做圖軟件中得出的效果進行比較),可其他的比如漸變色、非常規色等,就拿剛才前面用到的Winamp5主界面的圖片,轉出來後變成了大花臉。。。唉,別偷懶,還是好好分析一下吧。

< 動腦 >

  以“淺色-深色”漸變圖片為例,假設我們要將所有含“深色”色的像素轉成指定色彩,也就是要轉成類似白-紅、淺黃-深黃漸變的效果。我們知道TColor其實是用一定范圍的十六進制數值來表示的,從低位到高位每個字節分別保存紅、綠、藍的值。對於灰階色來說,每一種“灰色”其R、G、B的三值是相等的,從黑(RGB(0,0,0))到白(RGB(255,255,255))。經過一段時間的琢磨,我發現對某一像素點的色彩轉換大致的思路應該是:

  該點目標色離白色的“距離”(之間的值差,姑且這樣稱呼吧)/指定彩色離白色的“距離” = 該點灰色離白色的“距離”/最深色離白色的“距離”

  這裡的“距離”其實分別是該種顏色的R、G、B三值和255的差的絕對值。有點昏了是吧?呵呵,其實應該是比較好理解的,直觀一點的原始公式(分別計算R、G、B三值)是:

  (255 - 目標色R值) / (255 - 指定色R值) = (255 - 灰色R值) / (255 - 最深色R值)

  移項後可得解:

  目標色R值 = 255 - (255 - 灰色R值) * (255 - 指定色R值) / (255 - 最深色R值)

  同理可得目標色的G、B值。現在你可以拿一個指定色(一種淺紅)(RGB(153,0,0))和一種灰(RGB(204,204,204))算一下,分別四捨五入後得出的結果RGB(235,204,204),拿到做圖軟件裡去對比一下吧,和做圖軟件裡產生的彩色漸變出來的效果基本看不出區別了!

   既然算法已經找到了,轉成代碼就再輕松不過了:

   procedure TForm1.Button2Click(Sender: TObject);
   var
   i, j: Integer;
   R, G, B, RGBTemp: Cardinal;
   t1, t2: Cardinal;
   Png: TPngObject;
   Rect: TRect;
   begin
   Png := TPngObject.Create;
   Png.LoadFromFile('2.png');
  t1 := GetTickCount;

  for i := 0 to Png.Width - 1 do
   begin
   for j := 0 to Png.Height - 1 do
   begin
   RGBTemp := Png.Pixels[i, j];
   R := GetRValue(RGBTemp);
   G := GetGValue(RGBTemp);
   B := GetBValue(RGBTemp);

   { 計算公式:目標色R/G/B值 = 255 - (255 - 灰色R/G/B值) * (255 - 指定色R/G/B值) / (255 - 最深色R/G/B值) }

  Png.Pixels[i, j] := RGB(255 - (255 - R) * (255 - 153) div 255, // 按公式計算當前像素的 R 的值
   255 - (255 - G) * (255 - 0) div 255,   // 計算 G 值
   255 - (255 - B) * (255 - 0) div 255);  // 計算 B 值
   end;
   end;
  Rect.Left := 448;
   Rect.Top := 152;
   Rect.Right := Rect.Left + Png.Width;
   Rect.Bottom := Rect.Top + Png.Height;

  Png.Draw(Canvas, Rect);

  t2 := GetTickCount - t1;
   ShowMessage(IntToStr(t2));

  Png.Free;
   end;

  顏色轉換後的效果如圖3。

  目標看似達到了,不過看看這粗糙的算法吧,二重循環遍歷每個象素一定是很慢的,測試了一下轉換這張200*200(象素)的圖片在P4 2.4的CPU下耗時平均94ms(上面我用了一個RGBTemp臨時變量來保存當前像素的RGB值,要不然在計算R、G、B時分別去直接讀PNG.Pixels[i,j]的話時間基本要再翻倍)。天!這個耗時很可觀哪!後來我把代碼改成把圖片的ScanLine屬性復制到一個指針數組,大大提高了運算速度:

  { 定義指針數組類型 }

   const
   MaxPixelCount = 65536;
  type
   PRGBArray = ^TRGBArray;
   TRGBArray = array [0..MaxPixelCount - 1] of TRGBTriple;

  procedure TForm1.Button3Click(Sender: TObject);
   var
   i, j: Integer;
   Row: PRGBArray;
   Png: TPngObject;
   Rect: TRect;
   begin
   Png := TPngObject.Create;
   Png.LoadFromFile('2.png');

  for i := 0 to Png.Height - 1 do
   begin
   Row := Png.Scanline[i];  // 復制ScanLine屬性到Row指針數組
   for j := 0 to Png.Width - 1 do
   begin
   Row[j].rgbtRed := 255 - (255 - Row[j].rgbtRed) * (255 - 153) div 255;
   Row[j].rgbtGreen := 255 - (255 - Row[j].rgbtGreen) * (255 - 0) div 255;
   Row[j].rgbtBlue := 255 - (255 - Row[j].rgbtBlue) * (255 - 0) div 255;
   end;
   end;
   { ... }
   { 後面的畫圖片代碼相同 } 

  經過這個算法優化,運行時間縮短到幾乎為0ms了(偶爾出現16ms)!

  < 收工 >

  總算寫完了^^。以上算法是我自己琢磨出來的,網上也沒找到什麼相關資料,哪位朋友如果有更好的方法,請多多指點,也希望能和我聯系([email protected])。希望這篇文章能給各位朋友起到拋磚引玉的作用!

  

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved