2008年8月27日星期三

GDI+ 在Delphi的应用 -- Photoshop浮雕效果

实现图像浮雕效果的一般原理是,将图像上每个像素点与其对角线的像素点形成差值,使相似颜色值淡化,不同颜色值突出,从而产生纵深感,达到浮雕的效果,具体的做法是用处于对角线的2个像素值相减,再加上一个背景常数,一般为128而成。这种算法的特点是简单快捷,缺点是不能调节图像浮雕效果的角度和深度。

用Photoshop实现图像浮雕效果,可以任意调节浮雕角度和深度(2个像素点的距离),还可以调整浮雕像素差值的数量。其基本算法原理和一般浮雕效果相同,但是具体做法不一样:对每个要处理的像素点,首先按照浮雕角度和深度计算处2个相应点的位置,然后计算这2个位置的颜色值,并使之形成差值,再乘上浮雕差值数量百分比,最后加上128的背景色。注意,这里计算的2个相应点是逻辑点,而不是实际的像素点,比如实现一个45度角,深度为3的图像浮雕效果,对每个像素点P(x, y),其对应的2个逻辑点的位置分别是P0(x - 3 * 0.7071 / 2, y - 3 * 0.7071 / 2)和P1(x + 3 * 0.7071 / 2, y + 3 * 0.7071 / 2),显然,对于这样的2个逻辑点,是不能直接从图像中找到其对应的像素点的,如果简单地对其四舍五入处理,将会造成大量的,由不同角度和深度而形成的相同的浮雕效果,这可不是我们想要的结果,而且使浮雕角度和深度参数失去了它原本的意义。为此,必须对原始图像按浮雕角度和深度进行缩放后,再对每个像素点进行浮雕效果处理,完毕再缩放回原图的大小,从而完成整个浮雕效果过程。下面是我经过反复试验后,写的Photoshop浮雕效果实现过程代码:


数据类型:
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;
过程代码:

// 获取二次线性插值颜色
function GetBoundColor(x, y: Integer; Source: TImageData): LongWord;
asm
push ebx
push edx

xor ebx, ebx // flag = 0
test eax, eax
jns @@1
xor eax, eax // if (x < 0)
or ebx, 1 // {x = 0; flag = 1}
jmp @@2
@@1:
cmp eax, [ecx]
jl @@2
mov eax, [ecx] // else if (x >= Source.Width)
dec eax // {x = Source.Width.Width - 1; flag = 1}
or ebx, 1
@@2:
test edx, edx
jns @@3
xor edx, edx // if (y < 0)
or ebx, 1 // {y = 0; flag = 1}
jmp @@5
@@3:
cmp edx, [ecx + 4]
jl @@5
mov edx, [ecx + 4] // else if (y >= Source.Height)
dec edx // {y = Source.Width.Height - 1; flag = 1}
or ebx, 1
@@5:
shl eax, 2 // ARGBColor = *(ARGB*)(Source.Scan0 +
imul edx, [ecx + 8] // y * Source.Stride + x * 4)
add edx, eax
mov eax, [ecx + 16]
add eax, edx
mov eax, [eax]
test ebx, 1
jz @@6 // if (flag = 1)
and eax, 0ffffffh // ARGBColor.a = 0
@@6:
pop edx
pop ebx
end;

function GetBilinearColor(x, y: Integer; Source: TImageData): LongWord;
var
colors: array[0..4] of LongWord;
m0, m1, m2, m3: LongWord;
asm
push ebx

mov ebx, eax
sar eax, 16
push eax // x0 = x >> 16
and ebx, 0ffffh
shr ebx, 8 // u = (x & 0xffff) >> 8
mov eax, edx
sar eax, 16
push eax // y0 = y >> 16
and edx, 0ffffh
shr edx, 8 // v = (y & 0xffff) >> 8
mov eax, edx
imul eax, ebx
mov m3, eax // m3 = v * u
mov eax, 255
sub eax, edx
push eax
imul eax, ebx
mov m2, eax // m2 = (255 - v) * u
neg ebx
add ebx, 255
imul edx, ebx
mov m1, edx // m1 = v * (255 - u)
pop eax
imul ebx
mov m0, eax // m0 = (255 - v) * (255 - u)

lea ebx, colors
pop edx
pop eax
push eax
push eax
push eax
call GetBoundColor
mov [ebx], eax // Colors[0] = GetColor(x0, y0)
pop eax
inc eax
call GetBoundColor
mov [ebx + 8], eax // Colors[2] = GetColor(x0 + 1, y0)
pop eax
inc edx
call GetBoundColor
mov [ebx + 4], eax // Colors[1] = GetColor(x0, y0 + 1)
pop eax
inc eax
call GetBoundColor
mov [ebx + 12], eax // Colors[3] = GetColor(x0 + 1, y0 + 1)

mov [ebx + 16], 0
mov ecx, 4
@CalcColor:
movzx eax, [ebx] // a(rgb) = (Colors[0].a(rgb) * m0 +
movzx edx, [ebx + 4] // Colors[1].a(rgb) * m1 +
imul eax, m0 // Colors[2].a(rgb) * m2 +
imul edx, m1 // Colors[3].a(rgb) * m3) >> 16
add edx, eax
movzx eax, [ebx + 8]
imul eax, m2
add edx, eax
movzx eax, [ebx + 12]
imul eax, m3
add eax, edx
shr eax, 16
add [ebx + 16], al
inc ebx
loop @CalcColor
mov eax, [ebx + 12] // return ARGBColor

pop ebx
end;
// Photoshop浮雕。参数:Data: 图像数据, Angle: 角度, Size: 长度, Num: 数量
procedure PSSculpture(Data: TImageData; Angle: Single;
Size: LongWord; Num: LongWord = 100);
var
x, y: Integer;
Width, Height: Integer;
xDelta, yDelta: Integer;
P: PRGBQuad;
P0, P1: TRGBQuad;
Buf: Pointer;
Src: TImageData;
begin
if Size = 0 then
raise Exception.Create('Sculpture can not be size 0');
Angle := PI * Angle / 180;
Size := Size shl 16;
if Num > 500 then Num := 500;
xDelta := Round((Cos(Angle) * Size) / 2);
yDelta := Round((Sin(Angle) * Size) / 2);
Width := Data.Width shl 16;
Height := Data.Height shl 16;
GetMem(Buf, Data.Height * Data.Stride);
try
Move(Data.Scan0^, Buf^, Data.Height * Data.Stride);
Move(Data, Src, Sizeof(TImageData));
Src.Scan0 := Buf;
P := Data.Scan0;
y := 0;
while y < Height do
begin
x := 0;
while x < Width do
begin
P0 := TRGBQuad(GetBilinearColor(x - xDelta, y - yDelta, Src));
P1 := TRGBQuad(GetBilinearColor(x + xDelta, y + yDelta, Src));
P^.rgbBlue := Max(0, Min(255, Num * (P0.rgbBlue - P1.rgbBlue) div 100 + 128));
P^.rgbGreen := Max(0, Min(255, Num * (P0.rgbGreen - P1.rgbGreen) div 100 + 128));
P^.rgbRed := Max(0, Min(255, Num * (P0.rgbRed - P1.rgbRed) div 100 + 128));
Inc(P);
Inc(x, $10000);
end;
Inc(y, $10000);
end;
finally
FreeMem(Buf);
end;
end;

// Photoshop浮雕。参数:Bmp: GDI+图像, Angle: 角度, Size: 长度, Num: 数量
procedure GdipPSSculpture(Bmp: TGpBitmap; Angle: Single;
Size: LongWord; Num: LongWord = 100);
var
Data: TBitmapData;
begin
Data := Bmp.LockBits(GpRect(0, 0, Bmp.Width, Bmp.Height), [imRead, imWrite], pf32bppARGB);
try
PSSculpture(TImageData(Data), Angle, Size, Num);
finally
Bmp.UnlockBits(Data);
end;
end;

// Photoshop浮雕。参数:Bmp: Bitmap图像, Angle: 角度, Size: 长度, Num: 数量
procedure BitmapPSSculpture(Bmp: TBitmap; Angle: Single;
Size: LongWord; Num: LongWord = 100);
begin
PSSculpture(GetImageData(Bmp), 360 - Angle, Size, Num);
end;

以上代码即可用GDI+实现图像浮雕效果,也可直接用Delphi的TBitmap实现图像浮雕效果,因为TBitmap图像地址GDI+的图像地址排列不一样,为了保证2者处理的效果完全一样,所以用360 - 原角度参数。本代码中浮雕角度参数与Phoposhop是不相同的,Photoshop是以右边为0度,逆时钟调整角度,而本代码是以左边为0度,顺时钟方向调整角度。

在上述代码中,并没有将原始图像进行实际的缩放,而是直接在原始图像数据地址上,通过二次线性插值法找到并计算出2个逻辑点的颜色值而形成差值的。其中的GetBilinearColor过程是以前就写好的定点数二次线性插值缩放过程中部分代码,浮雕效果实现过程中,没有用到其中的Alpha,所以,有关Alpha处理的语句完全可以去掉。

下面是一个用GDI+实现图像浮雕效果的测试程序代码:

unit Main;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, Gdiplus;

type
TForm1 = class(TForm)
PaintBox1: TPaintBox;
Label1: TLabel;
Edit1: TEdit;
Label2: TLabel;
Edit2: TEdit;
Label3: TLabel;
Edit3: TEdit;
Button1: TButton;
Button2: TButton;
procedure Edit1KeyPress(Sender: TObject; var Key: Char);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure PaintBox1Paint(Sender: TObject);
private
{ Private declarations }
FBmp: TGpBitmap;
FSource: TGpBitmap;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

uses Math, BitmapUtils, GpBmpUtils;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
Angle: Single;
Size: LongWord;
Num: LongWord;
begin
if Assigned(FBmp) then
FBmp.Free;
FBmp := FSource.Clone(GpRect(0, 0, FSource.Width, FSource.Height), pf32bppARGB);
Angle := StrToFloat(Edit1.Text);
Size := StrToInt(Edit2.Text);
Num := StrToInt(Edit3.Text);
GdipPSSculpture(FBmp, Angle, Size, Num);
PaintBox1.Invalidate;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
Close;
end;

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if (Key >= #32) and not (Key in ['0'..'9']) then
Key := #0;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
FSource := TGpBitmap.Create('D:\VclLib\GdiplusDemo\Media\20041001.jpg');
DoubleBuffered := True;
Button1.Click;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
FSource.Free;
FBmp.Free;
end;

procedure TForm1.PaintBox1Paint(Sender: TObject);
var
g: TGpGraphics;
begin
g := TGpGraphics.Create(PaintBox1.Canvas.Handle);
try
g.DrawImage(FSource, 0, 0);
g.TranslateTransform(0, FSource.Height);
g.DrawImage(FBmp, 0, 0);
finally
g.Free;
end;
end;

end.



和Photoshop浮雕效果对比,基本一致,说明我对其算法实现了完全“破解”,虽然这个算法不是很难,但也费了我不少时间。

代码中所用Gdiplus单元下载地址及BUG更正见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。建议和指导请来信:maozefa@hotmail.com



注:本文代码中有关定点处理二次线性插值的代码,参照了好友HouSisong的《图形图像处理-之-高质量的快速的图像缩放 中篇 二次线性插值和三次卷积插值》一文,在此表示感谢。

没有评论: