GDI+ 在Delphi程序的应用 -- 图像饱和度调整

        图像的饱和度调整有很多方法,最简单的就是判断每个象素的R、G、B值是否大于或小于128,大于加上调整值,小于则减去调整值;也可将象素RGB转换为HSV或者HSL,然后调整其S部分,从而达到线性调整图象饱和度的目的。这几种方法我都测试过,效果均不太好,简单的就不说了,利用HSV和HSL调整饱和度,其调节范围很窄,饱和度没达到,难看的色斑却出现了。而Photoshop的饱和度调整调节范围大多了,效果也好多了,请看下面25%饱和度调整时几种方法的效果对比图:

可以看出,都是25%的饱和度调整,Photoshop的调节幅度显得小一些(平坦些),效果也好多了,而HSV和HSL均出现了色斑,某些颜色也严重失真,尤其是HSV方式。

        据网上和书上的介绍,Photoshop的是利用所谓HSB颜色模式实现色相/饱和度调节的,可是就是没有看到其算法,我只得自己进行琢磨,首先发现Photoshop色相/饱和度命令中的明度调节好象是“独立”的,也就是它不需要转换为所谓的HSB模式,直接靠白色和黑色遮照层进行调节,具体原理和代码可看我写的《GDI+ 在Delphi程序的应用 -- 仿Photoshop的明度调整》一文。后来,却又发现Photoshop的饱和度调节好象是“半独立的”,什么意思呢?就是说Photoshop的色相/饱和度的调整还是转换为HSL颜色模式进行的,只是饱和度的增减调节却是“独立”于SHL模式的另外一套算法,如果不是需要HSL的S和L部分进行饱和度的上下限控制,它也和明度调整一样,可以独立进行!下面是我写的C++算法(只是随手写的算法,不是真正的运行代码):


inline  void  SwapRGB( int   & a,  int   & b)
{
    a 
+=  b;
    b 
=  a  -  b;
    a 
-=  b;
}

    
//  利用HSL模式求得颜色的S和L
     double  rgbMax  =  R  /   255 ;
    
double  rgbMin  =  G  /   255 ;
    
double  rgbC  =  B  /   255 ;
    
if  (rgbMax  <  rgbC)
        SwapRGB(rgbMax, rgbC);
    
if  (rgbMax  <  rgbMin)
        SwapRGB(rgbMax, rgbMin);
    
if  (rgbMin  >  rgbC)
        SwapRGB(rgbMin, rgbC);
    double delta = rgbMax - rgbMin;
    // 如果delta=0,S=0,所以不能调整饱和度
    
if (delta == 0) return;
        
    
double  value  =  rgbMax  +  rgbMin;
    
double  S, L  =  value   /   2 ;
    
if  (L  <   0.5 )
        S 
=  delta  /  value;
    
else
        S 
=  delta   /  ( 2   -  value);
    
//  具体的饱和度调整,sValue为饱和度增减量
    
//  如果增减量>0,饱和度呈级数增强,否则线性衰减
     if  (sValue  >   0 )
    {
        
//  如果增减量+S > 1,用S代替增减量,以控制饱和度的上限
        
//  否则取增减量的补数
        sValue  =  sValue  +  S  >=   1 ?  S :  1   -  sValue;
        
//  求倒数 - 1,实现级数增强
        sValue  =   1   /  sValue  -   1 ;
    }
    
//  L在此作饱和度下限控制
    R  =  R  +  (R  -  L  *   255 *  sValue;
    G 
=  G  +  (G  -  L  *   255 *  sValue;
    B 
=  B  +  (B  -  L  *   255 *  sValue;

        从上面的算法代码中可以看到,Photoshop的饱和度调整没有像HSV和HSL的饱和度调整那样,将S加上增减量重新计算,并将HSL转换回RGB,而只是取得了颜色的S、L作为上下限控制,对原有的RGB进行了“补丁”式的调节。

        下面是根据以上算法写的Delphi的BASM代码和GDI+调用的饱和度调整过程:

  1. type
  2.   // 与GDI+ TBitmapData结构兼容的图像数据结构
  3.   TImageData = packed record
  4.     Width: LongWord;         // 图像宽度
  5.     Height: LongWord;        // 图像高度
  6.     Stride: LongWord;        // 图像扫描线字节长度
  7.     PixelFormat: LongWord;   // 未使用
  8.     Scan0: Pointer;          // 图像数据地址
  9.     Reserved: LongWord;      // 保留
  10.   end;
  11.   PImageData = ^TImageData;
  12. // 获取TBitmap图像的TImageData数据结构,便于处理TBitmap图像
  13. function GetImageData(Bmp: TBitmap): TImageData;
  14. begin
  15.   Bmp.PixelFormat := pf32bit;
  16.   Result.Width := Bmp.Width;
  17.   Result.Height := Bmp.Height;
  18.   Result.Scan0 := Bmp.ScanLine[Bmp.Height - 1];
  19.   Result.Stride := Result.Width shl 2;
  20. //  Result.Stride := (((32 * Bmp.Width) + 31) and $ffffffe0) shr 3;
  21. end;
  1. // 图像饱和度调整。Value:(-255 - +255,没作范围检查)
  2. procedure Saturation(Data: TImageData; Value: Integer);
  3. asm
  4.     push    ebp
  5.     push    esi
  6.     push    edi
  7.     push    ebx
  8.     mov     edi, [eax + 16]// edi = Data.Scan0
  9.     mov     ecx, [eax + 4// ecx = Data.Width * Data.Height
  10.     imul    ecx, [eax]
  11.     mov     ebp, edx
  12.     cld
  13.   @PixelLoop:              // for (i = ecx - 1; i >= 0; i --)
  14.     dec     ecx            // {
  15.     js      @end
  16.     movzx   eax, [edi + 2]
  17.     movzx   ebx, [edi + 1]
  18.     movzx   esi, [edi]
  19.     cmp     esi, ebx
  20.     jge     @@1
  21.     xchg    esi, ebx
  22.   @@1:
  23.     cmp     esi, eax
  24.     jge     @@2
  25.     xchg    esi, eax
  26.   @@2:
  27.     cmp     ebx, eax
  28.     jle     @@3
  29.     mov     ebx, eax
  30.   @@3:
  31.     mov     eax, esi       //   delta = varMax - varMin
  32.     sub     eax, ebx       //   if (delta == 0)
  33.     jnz     @@4            //   {
  34.     add     edi, 4         //     edi += 4
  35.     jmp     @PixelLoop     //     continue
  36.   @@4:                     //   }
  37.     add     esi, ebx
  38.     mov     ebx, esi       //   ebx = varMax + varMin
  39.     shr     esi, 1         //   esi = L = (varMax + varMin) / 2
  40.     cmp     esi, 128
  41.     jl      @@5
  42.     neg     ebx            //   if (L >= 128) ebx = 510 - ebx
  43.     add     ebx, 510
  44.   @@5:
  45.     imul    eax, 255       //   eax = S = delta * 255 / ebx
  46.     cdq
  47.     div     ebx
  48.     mov     ebx, ebp       //   ebx = value
  49.     test    ebx, ebx       //   if (ebx > 0)
  50.     js      @@10           //   {
  51.     add     bl, al
  52.     jnc     @@6            //     if (ebx + S >= 255)
  53.     mov     ebx, eax       //       ebx = S
  54.     jmp     @@7
  55.   @@6:
  56.     mov     ebx, 255
  57.     sub     ebx, ebp       //     else ebx = 255 - value
  58.   @@7:
  59.     mov     eax, 65025     //     ebx = 65025 / ebx - 255
  60.     cdq
  61.     div     ebx
  62.     sub     eax, 255
  63.     mov     ebx, eax       //   }
  64.   @@10:
  65.     push    ebp
  66.     mov     ebp, 255
  67.     push    ecx
  68.     mov     ecx, 3
  69.   @RGBLoop:                //   for (j = 3; j > 0; j --)
  70.     movzx   eax, [edi]     //   {
  71.     push    eax
  72.     sub     eax, esi       //     rgb = rgb + (rgb - L) * ebx / 255
  73.     imul    eax, ebx
  74.     cdq
  75.     idiv    ebp
  76.     pop     edx
  77.     add     eax, edx
  78.     jns     @@11
  79.     xor     eax, eax       //     if (rgb < 0) rgb = 0
  80.     jmp     @@12
  81.   @@11:
  82.     cmp     eax, 255
  83.     jle     @@12
  84.     mov     eax, 255       //     else if (rgb > 255) rgb = 255
  85.   @@12:
  86.     stosb                  //     *edi ++ = rgb
  87.     loop    @RGBLoop       //   }
  88.     pop     ecx
  89.     pop     ebp
  90.     inc     edi            // edi ++
  91.     jmp     @PixelLoop     // }
  92.   @end:
  93.     pop     ebx
  94.     pop     edi
  95.     pop     esi
  96.     pop     ebp
  97. end;
  98. procedure GdipSaturation(Bmp: TGpBitmap; Value: Integer);
  99. var
  100.   Data: TBitmapData;
  101. begin
  102.   if Value = 0 then Exit;
  103.   Data := Bmp.LockBits(GpRect(00, Bmp.Width, Bmp.Height), [imRead, imWrite], pf32bppARGB);
  104.   try
  105.     Saturation(TImageData(Data), Value);
  106.   finally
  107.     Bmp.UnlockBits(Data);
  108.   end;
  109. end;
  110. procedure BitmapSaturation(Bmp: TBitmap; Value: Integer);
  111. begin
  112.   if Value <> 0 then
  113.     Saturation(GetImageData(Bmp), Value);
  114. end;

        具体的测试代码就不写了,有兴趣者可参考我的《GDI+ 在Delphi程序的应用 -- 线性调整图像亮度》、《GDI+ 在Delphi程序的应用 -- 图像卷积操作及高斯模糊》和《GDI+ 在Delphi程序的应用 -- 图像亮度/对比度调整》等文章,写出GDI+的TGpBitmap和Delphi的TBitmap的测试代码,其运行结果与Photoshop完全一样。  

  对于色相的调整,HSV、HSL和HSB都是相同的,不同的只是饱和度和亮度(明度)的调整,前天我已经写了《GDI+ 在Delphi程序的应用 -- 仿Photoshop的明度调整》,加上这篇饱和度算法文章,是否意味Photoshop的HSB算法完全破解了呢?不然,Photoshop的饱和度和明度调整独立使用时,确实是我说的那样,与Photoshop效果完全一样,但是放在一起进行调节就有区别了,这里有个谁先谁后的时机问题,和我前天写的《GDI+ 在Delphi程序的应用 -- 图像亮度/对比度调整》中对比度和亮度关系一样,各自独立使用没问题,放在一起调整就麻烦,但是对比度和亮度的关系比较简单,几次测试就清楚了,而饱和度和明度的关系我试验过多次,均与Photoshop有区别(只是有区别而以,其效果不比Photoshop的差多少),所以,要完全破解,还得试验,如果有谁知道,请务必告知,本人在此先谢了。下面干脆把我用BCB6写的试验性代码完整的贴在这,有兴趣的朋友可以帮忙试验,这个试验代码写的很零乱,运行也不快,先调整饱和度,再调整明度,其他方式没成功,所以没保存结果。

//  rgbhsb.h

#ifndef RgbHsbH
#define  RgbHsbH

#include 
< windows.h >
#include 
< algorithm >
using  std::min;
using  std::max;
#include 
< gdiplus.h >
using   namespace  Gdiplus;

void  SetRgbHsb(unsigned  char   & R, unsigned  char   & G, unsigned  char   & B,
               
int  hValue,  int  sValue,  int  bValue);
void  GdipHSBAdjustment(Bitmap  * Bmp,  int  hValue,  int  sValue,  int  bValue);

// ---------------------------------------------------------------------------
#endif

//  rgbhsb.cpp

#pragma  hdrstop

#include 
" RgbHsb.h "

// ---------------------------------------------------------------------------

inline 
void  SwapRGB( int   & a,  int   & b)
{
    a 
+=  b;
    b 
=  a  -  b;
    a 
-=  b;
}

inline 
void  CheckRGB( int   & Value)
{
    
if  (Value  <   0 ) Value  =   0 ;
    
else   if  (Value  >   255 ) Value  =   255 ;
}

inline 
void  AssignRGB(unsigned  char   & R, unsigned  char   & G, unsigned  char   & B,  int  rv,  int  gv,  int  bv)
{
    R 
=  rv;
    G 
=  gv;
    B 
=  bv;
}

void  SetRgbHsb(unsigned  char   & R, unsigned  char   & G, unsigned  char   & B,  int  hValue,  int  sValue,  int  bValue)
{
    
int  rgbMax  =  R;
    
int  rgbMin  =  G;
    
int  rgbC  =  B;
    
if  (rgbMax  <  rgbC)
        SwapRGB(rgbMax, rgbC);
    
if  (rgbMax  <  rgbMin)
        SwapRGB(rgbMax, rgbMin);
    
if  (rgbMin  >  rgbC)
        SwapRGB(rgbMin, rgbC);
    
int  value  =  rgbMax  +  rgbMin;
    
int  L  =  (value  +   1 >>   1 ;
    
int  H, S;
    
int  delta  =  rgbMax  -  rgbMin;
    
if  ( ! delta)
        H 
=  S  =   0 ;
    
else
    {
        
if  (L  <   128 )
            S 
=  delta  *   255   /  value;
        
else
            S 
=  delta  *   255   /  ( 510   -  value);
        
if  (rgbMax  ==  R)
            H 
=  (G  -  B)  *   60   /  delta;
        
else   if  (rgbMax  ==  G)
            H 
=  (B  -  R)  *   60   /  delta  +   120 ;
        
else
            H 
=  (R  -  G)  *   60   /  delta  +   240 ;
        
if  (H  <   0 ) H  +=   360 ;
        
if  (hValue)
        {
            H 
+=  hValue;
            
if  (H  <   0 ) H  +=   360 ;
            
else   if  (H  >   360 ) H  -=   360 ;
            
int  m  =  H  %   60 ;
            H 
/=   60 ;
            
if  (H  &   1 ) m  =   60   -  m;
            rgbC 
=  (m  *   255   +   30 /   60 ;
            rgbC 
=  rgbC  -  (rgbC  -   128 *  ( 255   -  S)  /   255 ;
            
int  Lum  =  L  -   128 ;
            
if  (Lum  >   0 )
                rgbC 
=  rgbC  +  (( 255   -  rgbC)  *  Lum  +   64 /   128 ;
            
else   if  (Lum  <   0 )
                rgbC 
=  rgbC  +  rgbC  *  Lum  /   128 ;
        }
        
else  H  /=   60 ;
        
if  (sValue)
        {
            
if  (sValue  >   0 )
            {
                sValue 
=  sValue  +  S  >=   255 ?  S :  255   -  sValue;
                sValue 
=   65025   /  sValue  -   255 ;
            }
            rgbMax 
=  rgbMax  +  (rgbMax  -  L)  *  sValue  /   255 ;
            rgbMin 
=  rgbMin  +  (rgbMin  -  L)  *  sValue  /   255 ;
            rgbC 
=  rgbC  +  (rgbC  -  L)  *  sValue  /   255 ;
        }
    }
    
if  (bValue  >   0 )
    {
        rgbMax 
=  rgbMax  +  ( 255   -  rgbMax)  *  bValue  /   255 ;
        rgbMin 
=  rgbMin  +  ( 255   -  rgbMin)  *  bValue  /   255 ;
        rgbC 
=  rgbC  +  ( 255   -  rgbC)  *  bValue  /   255 ;
    }
    
else   if  (bValue  <   0 )
    {
        rgbMax 
=  rgbMax  +  rgbMax  *  bValue  /   255 ;
        rgbMin 
=  rgbMin  +  rgbMin  *  bValue  /   255 ;
        rgbC 
=  rgbC  +  rgbC  *  bValue  /   255 ;
    }
    CheckRGB(rgbMax);
    CheckRGB(rgbMin);
    CheckRGB(rgbC);
    
if  (bValue  ||  S)
    {
        
switch  (H)
        {
          
case   1 : AssignRGB(R, G, B, rgbC, rgbMax, rgbMin);
          
break ;
          
case   2 : AssignRGB(R, G, B, rgbMin, rgbMax, rgbC);
          
break ;
          
case   3 : AssignRGB(R, G, B, rgbMin, rgbC, rgbMax);
          
break ;
          
case   4 : AssignRGB(R, G, B, rgbC, rgbMin, rgbMax);
          
break ;
          
case   5 : AssignRGB(R, G, B, rgbMax, rgbMin, rgbC);
          
break ;
          
default :AssignRGB(R, G, B, rgbMax, rgbC, rgbMin);
        }
    }
//     else AssignRGB(R, G, B, rgbMax, rgbMin, rgbC);
}

void  GdipHSBAdjustment(Bitmap  * Bmp,  int  hValue,  int  sValue,  int  bValue)
{
    sValue 
=  sValue  *   255   /   100 ;
    bValue 
=  bValue  *   255   /   100 ;
    BitmapData data;
    Rect r(
0 0 , Bmp -> GetWidth(), Bmp -> GetHeight());
    Bmp
-> LockBits( & r, ImageLockModeRead  |  ImageLockModeWrite, PixelFormat24bppRGB,  & data);
    
try
    {
        
int  offset  =  data.Stride  -  data.Width  *   3 ;
        unsigned 
char   * =  (unsigned  char * )data.Scan0;
        
for  ( int  y  =   0 ; y  <  data.Height; y  ++ , p  +=  offset)
            
for  ( int  x  =   0 ; x  <  data.Width; x  ++ , p  +=   3 )
                SetRgbHsb(p[
2 ], p[ 1 ],  * p, hValue, sValue, bValue);
    }
    __finally
    {
        Bmp
-> UnlockBits( & data);
    }
}

#pragma  package(smart_init)

//  main.h

#ifndef mainH
#define  mainH
// ---------------------------------------------------------------------------
#include  < Classes.hpp >
#include 
< Controls.hpp >
#include 
< StdCtrls.hpp >
#include 
< Forms.hpp >
#include 
< ComCtrls.hpp >
#include 
< ExtCtrls.hpp >
#include 
" RgbHsb.h "
// ---------------------------------------------------------------------------
class  TForm1 :  public  TForm
{
__published:    
//  IDE-managed Components
    TButton  * Button1;
    TLabel 
* Label1;
    TLabel 
* Label2;
    TLabel 
* Label3;
    TTrackBar 
* HBar;
    TTrackBar 
* SBar;
    TTrackBar 
* BBar;
    TEdit 
* HEdit;
    TEdit 
* SEdit;
    TEdit 
* BEdit;
    TPaintBox 
* PaintBox1;
    
void  __fastcall PaintBox1Paint(TObject  * Sender);
    
void  __fastcall HEditKeyPress(TObject  * Sender,  char   & Key);
    
void  __fastcall HEditChange(TObject  * Sender);
    
void  __fastcall HBarChange(TObject  * Sender);
    
void  __fastcall SBarChange(TObject  * Sender);
    
void  __fastcall BBarChange(TObject  * Sender);
private :     //  User declarations
public :         //  User declarations
    __fastcall TForm1(TComponent *  Owner);
    __fastcall 
~ TForm1( void );
};
// ---------------------------------------------------------------------------
extern  PACKAGE TForm1  * Form1;
// ---------------------------------------------------------------------------
#endif

//  main.cpp


#include 
< vcl.h >
#pragma  hdrstop

#include 
" main.h "
#include 
< stdlib.h >
// ---------------------------------------------------------------------------
#pragma  package(smart_init)
#pragma  resource "*.dfm"
TForm1 
* Form1;

ULONG gdiplusToken;
Bitmap 
* Bmp,  * tmpBmp;
Gdiplus::Rect r;
bool   lock ;

// ---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent *  Owner)
    : TForm(Owner)
{
    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
    GdiplusStartup(
& gdiplusToken,  & gdiplusStartupInput, NULL);
    Bmp 
=   new  Bitmap(WideString( " 100_0349.jpg " /* "d:/100_1.jpg" */ ));
    r 
=  Gdiplus::Rect( 0 0 , Bmp -> GetWidth(), Bmp -> GetHeight());
    tmpBmp 
=  Bmp -> Clone(r, PixelFormat24bppRGB);
    DoubleBuffered 
=   true ;
    
lock   =   false ;
}
__fastcall TForm1::
~ TForm1( void )
{
    delete tmpBmp;
    delete Bmp;
    GdiplusShutdown(gdiplusToken);
}
// ---------------------------------------------------------------------------


void  __fastcall TForm1::PaintBox1Paint(TObject  * Sender)
{
    Gdiplus::Graphics g(PaintBox1
-> Canvas -> Handle);
    g.DrawImage(tmpBmp, r);
    g.TranslateTransform(
0 , r.Height);
    g.DrawImage(Bmp, r);
}
// ---------------------------------------------------------------------------

void  __fastcall TForm1::HEditKeyPress(TObject  * Sender,  char   & Key)
{
    
if  (Key  >=   32   &&  (Key  <   48   ||  Key  >   57 ))
        Key 
=   0 ;
}
// ---------------------------------------------------------------------------

void  __fastcall TForm1::HEditChange(TObject  * Sender)
{
    
lock   =   true ;
    
if  (((TEdit * )Sender) -> Text.Length()  ==   0 )
        ((TEdit
* )Sender) -> Text  =   " 0 " ;
    
switch  (((TEdit * )Sender) -> Tag)
    {
        
case   0 : HBar -> Position  =  HEdit -> Text.ToInt();
        
break ;
        
case   1 : SBar -> Position  =  SEdit -> Text.ToInt();
        
break ;
        
case   2 : BBar -> Position  =  BEdit -> Text.ToInt();
        
break ;
    }
    
lock   =   false ;
    delete tmpBmp;
    tmpBmp 
=  Bmp -> Clone(r, PixelFormat24bppRGB);
    
if  (HBar -> Position  ||  SBar -> Position  ||  BBar -> Position)
        GdipHSBAdjustment(tmpBmp, HBar
-> Position, SBar -> Position, BBar -> Position);
    PaintBox1
-> Invalidate();
}
// ---------------------------------------------------------------------------

void  __fastcall TForm1::HBarChange(TObject  * Sender)
{
    
if  ( ! lock )
        HEdit
-> Text  =  HBar -> Position;
}
// ---------------------------------------------------------------------------

void  __fastcall TForm1::SBarChange(TObject  * Sender)
{
    
if  ( ! lock )
        SEdit
-> Text  =  SBar -> Position;
}
// ---------------------------------------------------------------------------

void  __fastcall TForm1::BBarChange(TObject  * Sender)
{
    
if  ( ! lock )
        BEdit
-> Text  =  BBar -> Position;
}

  由于本人文化水平太差,虽摸索出这些算法,但却没法把它进一步说透,只好说些“独立”,“补丁”的字眼,让各位见笑了。如有错误请指正,有建议也请来信:maozefa@hotmail.com

        后记:在GDI+下,32位PNG图像经转换为24位图像格式处理还原后,原有的透明色在转换过程中损失,故将本文对24位图像处理代码改为了32位图像处理代码。(2007.12.12)

   说明:为了统一《GDI+ 在Delphi程序的应用》系列文章所用数据类型和图像处理格式,本文代码已作了修订,代码中所用Gdiplus单元下载地址及BUG更正见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。(2008.8.18记)


 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值