阅读提示:
《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。
《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。
尽可能保持二者内容一致,可相互对照。
本文代码必须包括文章《Delphi图像处理 -- 数据类型及公用过程》中的ImageData.pas单元。
在图像处理过程中,图像的合成操作是使用频率最高的,如图像显示、图像拷贝、图像拼接以及的图层拼合叠加等。
图像合成,其实也就是图像像素颜色的混合,在Photoshop中,颜色混合是个很复杂的东西,不同的混合模式,将产生不同的合成效果,如果将之全部研究透彻,估计就得写一本书。因此,本文只谈谈最基本的图像合成,也就是Photoshop中的正常混合模式。
只要接触过图像处理的,都知道有个图像像素混合公式:
1)dstRGB = srcRGB * alpha + dstRGB * (1 - alpha)
其中,dstRGB为目标图像素值;srcRGB为源图像素值;alpha为源图像素值混合比例(不透明度,范围0 - 1)。
其实,这个像素混合公式有很大局限性,只适合不含Alpha信息的图像。
要处理包括带Alpha通道图像(层)的混合,其完整的公式应该是:
2-1)srcRGB = srcRGB * srcAlpha * alpha / 255 (源图像素预乘转换为PARGB)
2-2)dstRGB = dstRGB * dstAlpha / 255 (目标图像素预乘转换为PARGB)
2-3)dstRGB = dstRGB + srcRGB - dstRGB * srcAlpha * alpha / 255 (源图像素值与目标图像素值混合)
2-4)dstAlpha = dstAlpha + srcAlpha * alpha - dstAlpha * srcAlpha * alpha / 255 (混合后的目标图Alpha通道值)
2-5)dstRGB = dstRGB * 255 / dstAlpha (混合后的目标图像素转换为ARGB)
其中,dstRGB为目标图像素值;srcRGB为源图像素值;dstAlpha为目标图Alpha通道值;srcAlpha为源图Alpha通道值;dstARGB为含Alpha目标图像素值;alpha为源图像素值混合比例(不透明度,范围0 - 1)。
将公式2中的2-1式代入2-3式,简化可得:
3-1)dstRGB = dstRGB * dstAlpha / 255
3-2)dstRGB = dstRGB + (srcRGB - dstRGB) * srcAlpha * alpha / 255
3-3)dstAlpha = dstAlpha + srcAlpha * alpha - dstAlpha * srcAlpha * alpha / 255
3-4)dstRGB = dstRGB * 255 / dstAlpha
当dstAlpha=srcAlpha=255时,公式3中3-1式、3-3式和3-4式没有意义,3-2式也变化为:
4)dstRGB = dstRGB + (srcRGB - dstRGB) * alpha
不难看出,公式4是公式1的变形。因此,公式1只是公式3(或者公式2)在目标图和源图都不含Alpha信息(或者Alpha=255)情况下的一个特例而已。
当公式4中的alpha=1时,目标图像素等于源图像素,所以,本文前面说图像拷贝其实也是图像合成的范畴。
通过上面较详细的分析,可以看出,即使是最基本正常图像混合模式也是很复杂的。其实,上面还不是完整的分析,因为按照目标图Alpha信息、源图Alpha信息以及源图合成比例等三个要素的完全的排列组合,最多可以派生8个公式。
下面就按正常混合模式的全部8种情况(有2项重合,实际为7种情况)来分别进行代码实现,也可完善和补充上面的文字叙述:
unit main; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; Button2: TButton; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation uses Gdiplus, ImageData, Jpeg; {$R *.dfm} procedure _SetMixerMM; asm pxor mm7, mm7 mov eax, 1011h movd mm6, eax pshufw mm6, mm6, 0 mov eax, 8 movd mm5, eax pshufw mm5, mm5, 0 end; // --> mm7 4 * word = 0 // --> mm6 4 * word = 0x1101 // --> mm5 4 * word = 4 // --> eax source alpha // --> esi source pixel (ARGB) // --> edi dest pixel (ARGB) // <-- eax dest alpha !!! // <-- mm0 dest pixel procedure _PARGBMixer(srcAlpha: Integer); asm push edx movd mm0, [esi] movd mm1, [edi] punpcklbw mm0, mm7 punpcklbw mm1, mm7 // dest.rgb = dest.rgb * dest.alpha / 255 movzx edx, [edi].TARGBQuad.Alpha pmullw mm1, qword ptr ArgbTab[edx*8] pmulhuw mm1, mm6 paddusw mm1, mm5 psrlw mm1, 4 // dest.rgb = (dest.rgb * 255 + (source.rgb - dest.rgb) * sourec.alpha) / 255 psubw mm0, mm1 pmullw mm0, qword ptr ArgbTab[eax*8] pmullw mm1, qword ptr ArgbTab[255*8] paddw mm0, mm1 pmulhuw mm0, mm6 paddusw mm0, mm5 psrlw mm0, 4 // dest.alpha += (source.alpha - (dest.alpha * source.alpha + 127) / 255) push eax add [esp], edx imul eax, edx add eax, 127 mul dword ptr DivTab[255*4] pop eax sub eax, edx // dest.rgb = dest.rgb * 255 / dest.alpha movq mm1, mm0 psllw mm0, 8 psubw mm0, mm1 pmulhuw mm0, qword ptr MMDivTab[eax*8] packuswb mm0, mm7 pop edx end; // --> mm7 4 * word = 0 // --> eax source alpha // --> esi source pixel (ARGB) // --> edi dest pixel (ARGB) // <-- mm0 dest pixel (RGB) procedure _ARGBMixer(srcAlpha: Integer); asm movd mm0, [esi] movd mm1, [edi] punpcklbw mm0, mm7 punpcklbw mm1, mm7 psubw mm0, mm1 pmullw mm0, qword ptr ArgbTab[eax*8] psllw mm1, 8 paddw mm0, mm1 psrlw mm0, 8 packuswb mm0, mm0 end; procedure _DoMixer(var Dest: TImageData; const Source: TImageData; Alpha: Integer); var dstOffset, srcOffset:Integer; alphaI: Integer; dst: PImageData; asm push esi push edi push ebx mov dst, eax mov alphaI, ecx shr ecx, 8 movzx ebx, [eax].TImageData.AlphaFlag shl ebx, 1 or ecx, ebx movzx ebx, [edx].TImageData.AlphaFlag shl ebx, 2 or ecx, ebx push ecx call _SetCopyRegs mov srcOffset, eax pop eax jmp dword ptr [@@jmpTable+eax*4] @@jmpTable: dd @@mixer0, @@mixer1, @@mixer2, @@mixer3, @@mixer4, @@mixer5, @@mixer6, @@mixer7 // src = 0, dst = 0, alpha = 0 @@mixer0: mov eax, alphaI movq mm2, qword ptr ArgbTab[eax*8] mov eax, srcOffset pxor mm7, mm7 @@yLoop0: push ecx @@xLoop0: movd mm0, [esi] movd mm1, [edi] punpcklbw mm0, mm7 punpcklbw mm1, mm7 psubw mm0, mm1 pmullw mm0, mm2 psllw mm1, 8 paddw mm0, mm1 psrlw mm0, 8 packuswb mm0, mm0 movd [edi], mm0 mov [edi].TARGBQuad.Alpha, 255 add esi, 4 add edi, 4 loop @@xLoop0 add esi, eax add edi, ebx pop ecx dec edx jnz @@yLoop0 jmp @@End // src = 0, dst = 0, alpha = 1 @@mixer1: // src = 0, dst = 1, alpha = 1 @@mixer3: mov eax, srcOffset @@yLoop1: push ecx rep movsd pop ecx add esi, eax add edi, ebx dec edx jnz @@yLoop1 jmp @@End mov eax, dst mov [eax].TImageData.AlphaFlag, False jmp @@Exit // src = 0, dst = 1, alpha = 0 @@mixer2: call _SetMixerMM mov eax, alphaI imul eax, 255 shr eax, 8 @@yLoop2: push ecx @@xLoop2: push eax call _PARGBMixer movd [edi], mm0 mov [edi].TARGBQuad.Alpha, al pop eax add esi, 4 add edi, 4 loop @@xLoop2 add esi, srcOffset add edi, ebx pop ecx dec edx jnz @@yLoop2 jmp @@End // src = 1, dst = 0, alpha = 0 @@mixer4: mov dstOffset, ebx mov ebx, alphaI pxor mm7, mm7 @@yLoop4: push ecx @@xLoop4: movzx eax, [esi].TARGBQuad.Alpha imul eax, ebx shr eax, 8 jz @@Next4 call _ARGBMixer movd [edi], mm0 mov [edi].TARGBQuad.Alpha, 255 @@Next4: add esi, 4 add edi, 4 loop @@xLoop4 add esi, srcOffset add edi, dstOffset pop ecx dec edx jnz @@yLoop4 jmp @@End // src = 1, dst = 0, alpha = 1 @@mixer5: pxor mm7, mm7 @@yLoop5: push ecx @@xLoop5: movzx eax, [esi].TARGBQuad.Alpha call _ARGBMixer movd [edi], mm0 mov [edi].TARGBQuad.Alpha, 255 add esi, 4 add edi, 4 loop @@xLoop5 add esi, srcOffset add edi, ebx pop ecx dec edx jnz @@yLoop5 jmp @@End // src = 1, dst = 1, alpha = 0 @@mixer6: mov dstOffset, ebx mov ebx, alphaI call _SetMixerMM @@yLoop6: push ecx @@xLoop6: movzx eax, [esi].TARGBQuad.Alpha imul eax, ebx shr eax, 8 jz @@Next6 call _PARGBMixer movd [edi], mm0 mov [edi].TARGBQuad.Alpha, al @@Next6: add esi, 4 add edi, 4 loop @@xLoop6 add esi, srcOffset add edi, dstOffset pop ecx dec edx jnz @@yLoop6 jmp @@End // src = 1, dst = 1, alpha = 1 @@mixer7: call _SetMixerMM @@yLoop7: push ecx @@xLoop7: movzx eax, [esi].TARGBQuad.Alpha call _PARGBMixer movd [edi], mm0 mov [edi].TARGBQuad.Alpha, al add esi, 4 add edi, 4 loop @@xLoop7 add esi, srcOffset add edi, ebx pop ecx dec edx jnz @@yLoop7 @@End: emms @@Exit: pop ebx pop edi pop esi end; procedure ImageMixer(var Dest: TImageData; const Source: TImageData; Alpha: Single); var alphaI: Integer; begin alphaI := Round(Alpha * 256); if alphaI < 0 then Exit; if alphaI > 256 then alphaI := 256; _DoMixer(Dest, Source, alphaI); end; procedure TForm1.Button1Click(Sender: TObject); var source, dest: TGpBitmap; g: TGpGraphics; src, dst: TImageData; begin source := TGpBitmap.Create('..\..\media\Apple.png'); dest := TGpBitmap.Create('..\..\media\xmas_011.png'); g := TGpGraphics.Create(Canvas.Handle); g.DrawImage(dest, 0, 0); g.DrawImage(source, dest.Width, 0); src := LockGpBitmap(source); dst := LockGpBitmap(dest); ImageMixer(dst, src, 0.75); UnlockGpBitmap(dest, dst); UnlockGpBitmap(source, src); g.DrawImage(dest, dst.Width + src.Width, 0); g.Free; dest.Free; source.Free; end; procedure TForm1.Button2Click(Sender: TObject); var source: TGpBitmap; dest: TBitmap; tmp: TJpegImage; g: TGpGraphics; src, dst: TImageData; begin dest := TBitmap.Create; tmp := TJpegImage.Create; tmp.LoadFromFile('..\..\media\IMG_9440_mf.jpg'); dest.Assign(tmp); tmp.Free; source := TGpBitmap.Create('..\..\media\xmas_011.png'); g := TGpGraphics.Create(Canvas.Handle); Canvas.Draw(0, 0, dest); g.DrawImage(source, dest.Width, 0); dst := GetBitmapData(dest); src := LockGpBitmap(source); ImageMixer(dst, src, 1); UnlockGpBitmap(source, src); Canvas.Draw(dst.Width + src.Width, 0, dest); g.Free; dest.Free; source.Free; end; end.
上面是一个完整Delphi程序,在_DoMixer过程中,定义了7个图像合成子过程,以满足全部8种排列组合的图像合并。除了@@Mixer0和@@Mixer1子过程外,其余子过程分别调用了 _ARGBMixer和_PARGBMixer过程,其实@@Mixer0中的处理代码与 _ARGBMixer过程基本是相同的,也就是说,7个子过程分别使用了三种处理方式:
一是@@Mixer1的完全拷贝方式;
二是目标图不含Alpha信息,将源图按像素的Alpha信息或者给定的不透明度混合到目标图的_ARGBMixer过程,该过程使用的是前面公式1或公式4的简化公式。
三是目标图为带Alpha信息的处理形式 _PARGBMixer,该过程使用前面的公式3。
如果嫌7个子过程太多,也可简化代码,只保留满足上述3种情况的子过程,即@@Mixer1、@@Mixer4和@@Mixer6,对处理效率影响不大。
上面程序中包含2个例子,前一个是用GDI+位图打开2张png图片后进行处理,这个处理调用的是@@Mixer6。运行效果截图如下:
后一个例子是对GDI+位图和VCL位图的混合处理,其中VCL位图是JPEG格式文件,这是没有Alpha信息的,这个例子运行效果截图如下:
《Delphi图像处理》系列使用GDI+单元下载地址和说明见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]
这里可访问《Delphi图像处理 -- 文章索引》。