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

原创 2007年09月11日 20:27:00

        图像的饱和度调整有很多方法,最简单的就是判断每个象素的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(
00, 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(00, 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记)


 

GDI+学习及代码总结之-----坐标变换、矩阵变换及色彩变换

坐标变换、矩阵变换 在Graphics类中,有几个函数实现了简单的坐标变换 Graphics::TranslateTransform(dx, dy, order)//平移坐标系 Graph...

GDI+编程说明及小结(叙述较为全面)

原文地址:http://blog.csdn.net/byxdaz/article/details/5972759

【图像处理】基于OpenCV底层实现的图片旋转

简明透彻,介绍基于OpenCV和C++底层实现的图片旋转。图片旋转的全过程

OpenCV中IplImage和单字节char*的相互转换

      上一篇文章曾指出了一个IplImage中imageData指针容易出错的问题, 这篇文章接着之前的叙述,提出IplImage和单字节char*之间相互转换的正确、简洁的方法:      已...

GDI+调整图像亮度示例 (delphi)

  • 2013年05月08日 22:40
  • 332KB
  • 下载

VC下如何使用GDI+进行图像程序设计

原作者:孙涛 整理 http://blog.csdn.net/suntaoznz  GDI+介绍 Microsoft® Windows® GDI+ 是Windows XP 或者Wind...

GDI+(5.图像应用)

1.获取设置图像像素值 private void pictureBox1_MouseMove(object sender, MouseEventArgs e) { //获取图像像素值...

VC下如何使用GDI+进行图像程序设计

原作者:孙涛 整理 http://blog.csdn.net/suntaoznz   GDI+介绍 Microsoft® Windows® GDI+ 是Windows XP 或...

VC下如何使用GDI+进行图像程序设计

原作者:孙涛 整理 http://blog.csdn.net/suntaoznz   GDI+介绍 Microsoft® Windows® GDI+ 是Windows XP 或...

Delphi图像处理 -- 色相/饱和度调整(续)

阅读提示:    《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。    《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。    尽可能保持...
  • maozefa
  • maozefa
  • 2013年01月03日 19:29
  • 4135
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:GDI+ 在Delphi程序的应用 -- 图像饱和度调整
举报原因:
原因补充:

(最多只允许输入30个字)