您的位置:首页 > 编程语言 > Delphi

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

2007-09-11 20:27 453 查看
图像的饱和度调整有很多方法,最简单的就是判断每个象素的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+调用的饱和度调整过程:

type
// 与GDI+ TBitmapData结构兼容的图像数据结构
TImageData = packed record
Width: LongWord; // 图像宽度
Height: LongWord; // 图像高度
Stride: LongWord; // 图像扫描线字节长度
PixelFormat: LongWord; // 未使用
Scan0: Pointer; // 图像数据地址
Reserved: LongWord; // 保留
end;
PImageData = ^TImageData;

// 获取TBitmap图像的TImageData数据结构,便于处理TBitmap图像
function GetImageData(Bmp: TBitmap): TImageData;
begin
Bmp.PixelFormat := pf32bit;
Result.Width := Bmp.Width;
Result.Height := Bmp.Height;
Result.Scan0 := Bmp.ScanLine[Bmp.Height - 1];
Result.Stride := Result.Width shl 2;
// Result.Stride := (((32 * Bmp.Width) + 31) and $ffffffe0) shr 3;
end;

// 图像饱和度调整。Value:(-255 - +255,没作范围检查)

procedure Saturation(Data: TImageData; Value: Integer);

asm

push ebp

push esi

push edi

push ebx

mov edi, [eax + 16]// edi = Data.Scan0

mov ecx, [eax + 4] // ecx = Data.Width * Data.Height

imul ecx, [eax]

mov ebp, edx

cld

@PixelLoop: // for (i = ecx - 1; i >= 0; i --)

dec ecx // {

js @end

movzx eax, [edi + 2]

movzx ebx, [edi + 1]

movzx esi, [edi]

cmp esi, ebx

jge @@1

xchg esi, ebx

@@1:

cmp esi, eax

jge @@2

xchg esi, eax

@@2:

cmp ebx, eax

jle @@3

mov ebx, eax

@@3:

mov eax, esi // delta = varMax - varMin

sub eax, ebx // if (delta == 0)

jnz @@4 // {

add edi, 4 // edi += 4

jmp @PixelLoop // continue

@@4: // }

add esi, ebx

mov ebx, esi // ebx = varMax + varMin

shr esi, 1 // esi = L = (varMax + varMin) / 2

cmp esi, 128

jl @@5

neg ebx // if (L >= 128) ebx = 510 - ebx

add ebx, 510

@@5:

imul eax, 255 // eax = S = delta * 255 / ebx

cdq

div ebx

mov ebx, ebp // ebx = value

test ebx, ebx // if (ebx > 0)

js @@10 // {

add bl, al

jnc @@6 // if (ebx + S >= 255)

mov ebx, eax // ebx = S

jmp @@7

@@6:

mov ebx, 255

sub ebx, ebp // else ebx = 255 - value

@@7:

mov eax, 65025 // ebx = 65025 / ebx - 255

cdq

div ebx

sub eax, 255

mov ebx, eax // }

@@10:

push ebp

mov ebp, 255

push ecx

mov ecx, 3

@RGBLoop: // for (j = 3; j > 0; j --)

movzx eax, [edi] // {

push eax

sub eax, esi // rgb = rgb + (rgb - L) * ebx / 255

imul eax, ebx

cdq

idiv ebp

pop edx

add eax, edx

jns @@11

xor eax, eax // if (rgb < 0) rgb = 0

jmp @@12

@@11:

cmp eax, 255

jle @@12

mov eax, 255 // else if (rgb > 255) rgb = 255

@@12:

stosb // *edi ++ = rgb

loop @RGBLoop // }

pop ecx

pop ebp

inc edi // edi ++

jmp @PixelLoop // }

@end:

pop ebx

pop edi

pop esi

pop ebp

end;

procedure GdipSaturation(Bmp: TGpBitmap; Value: Integer);

var

Data: TBitmapData;

begin

if Value = 0 then Exit;

Data := Bmp.LockBits(GpRect(0, 0, Bmp.Width, Bmp.Height), [imRead, imWrite], pf32bppARGB);

try

Saturation(TImageData(Data), Value);

finally

Bmp.UnlockBits(Data);

end;

end;

procedure BitmapSaturation(Bmp: TBitmap; Value: Integer);

begin

if Value <> 0 then

Saturation(GetImageData(Bmp), Value);

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 *p = (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记)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: