图像(层)正常混合模式详解(上)
在图像处理过程中,图像的合成操作是使用频率最高的,如图像显示、图像拷贝、图像拼接以及的图层拼合叠加等。
图像合成,其实也就是图像像素颜色的混合,在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种情况)来分别进行代码实现,也可完善和补充上面的文字叙述:
2
3 // 定义ARGB像素结构
4 typedef union
5 {
6 ARGB Color;
7 struct
8 {
9 BYTE Blue;
10 BYTE Green;
11 BYTE Red;
12 BYTE Alpha;
13 };
14 }ARGBQuad, *PARGBQuad;
15
16 typedef struct
17 {
18 INT width;
19 INT height;
20 PARGBQuad dstScan0;
21 PARGBQuad srcScan0;
22 INT dstOffset;
23 INT srcOffset;
24 }ImageCpyData, *PImageCpyData;
25
26 typedef VOID (*MixerProc)(PImageCpyData, INT);
27
28 #define PixelAlphaFlag 0x10000
29 //---------------------------------------------------------------------------
30 // source alpha = false, dest alpha = false, alpha < 255
31 static VOID Mixer0(PImageCpyData cpyData, INT alpha)
32 {
33 PARGBQuad pd = cpyData->dstScan0;
34 PARGBQuad ps = cpyData->srcScan0;
35 for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
36 {
37 for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
38 {
39 pd->Blue += (((ps->Blue - pd->Blue) * alpha + 127) / 255);
40 pd->Green += (((ps->Green - pd->Green) * alpha + 127) / 255);
41 pd->Red += (((ps->Red - pd->Red) * alpha + 127) / 255);
42 }
43 }
44 }
45 //---------------------------------------------------------------------------
46 // source alpha = false, dest alpha = false, alpha = 255
47 // source alpha = false, dest alpha = true, alpha = 255
48 static VOID Mixer1(PImageCpyData cpyData, INT alpha)
49 {
50 ARGB *pd = (ARGB*)cpyData->dstScan0;
51 ARGB *ps = (ARGB*)cpyData->srcScan0;
52 for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
53 {
54 for (INT x = 0; x < cpyData->width; x ++, *pd ++ = *ps ++);
55 }
56 }
57 //---------------------------------------------------------------------------
58 // source alpha = false, dest alpha = true, alpha < 255
59 static VOID Mixer2(PImageCpyData cpyData, INT alpha)
60 {
61 PARGBQuad pd = cpyData->dstScan0;
62 PARGBQuad ps = cpyData->srcScan0;
63 for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
64 {
65 for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
66 {
67 pd->Blue = (pd->Blue * pd->Alpha + 127) / 255;
68 pd->Green = (pd->Green * pd->Alpha + 127) / 255;
69 pd->Red = (pd->Red * pd->Alpha + 127) / 255;
70
71 pd->Blue += (((ps->Blue - pd->Blue) * alpha + 127) / 255);
72 pd->Green += (((ps->Green - pd->Green) * alpha + 127) / 255);
73 pd->Red += (((ps->Red - pd->Red) * alpha + 127) / 255);
74 pd->Alpha += (alpha - (pd->Alpha * alpha + 127) / 255);
75
76 pd->Blue = pd->Blue * 255 / pd->Alpha;
77 pd->Green = pd->Green * 255 / pd->Alpha;
78 pd->Red = pd->Red * 255 / pd->Alpha;
79 }
80 }
81 }
82 //---------------------------------------------------------------------------
83 // source alpha = true, dest alpha = false, alpha < 255
84 static VOID Mixer4(PImageCpyData cpyData, INT alpha)
85 {
86 PARGBQuad pd = cpyData->dstScan0;
87 PARGBQuad ps = cpyData->srcScan0;
88 for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
89 {
90 for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
91 {
92 INT alpha0 = (alpha * ps->Alpha + 127) / 255;
93 pd->Blue += (((ps->Blue - pd->Blue) * alpha0 + 127) / 255);
94 pd->Green += (((ps->Green - pd->Green) * alpha0 + 127) / 255);
95 pd->Red += (((ps->Red - pd->Red) * alpha0 + 127) / 255);
96 }
97 }
98 }
99 //---------------------------------------------------------------------------
100 // source alpha = true, dest alpha = false, alpha = 255
101 static VOID Mixer5(PImageCpyData cpyData, INT alpha)
102 {
103 PARGBQuad pd = cpyData->dstScan0;
104 PARGBQuad ps = cpyData->srcScan0;
105 for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
106 {
107 for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
108 {
109 pd->Blue += (((ps->Blue - pd->Blue) * ps->Alpha + 127) / 255);
110 pd->Green += (((ps->Green - pd->Green) * ps->Alpha + 127) / 255);
111 pd->Red += (((ps->Red - pd->Red) * ps->Alpha + 127) / 255);
112 }
113 }
114 }
115 //---------------------------------------------------------------------------
116 // source alpha = true, dest alpha = true, alpha < 255
117 static VOID Mixer6(PImageCpyData cpyData, INT alpha)
118 {
119 PARGBQuad pd = cpyData->dstScan0;
120 PARGBQuad ps = cpyData->srcScan0;
121 for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
122 {
123 for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
124 {
125 INT alpha0 = (alpha * ps->Alpha + 127) / 255;
126 if (alpha0)
127 {
128 pd->Blue = (pd->Blue * pd->Alpha + 127) / 255;
129 pd->Green = (pd->Green * pd->Alpha + 127) / 255;
130 pd->Red = (pd->Red * pd->Alpha + 127) / 255;
131
132 pd->Blue += (((ps->Blue - pd->Blue) * alpha0 + 127) / 255);
133 pd->Green += (((ps->Green - pd->Green) * alpha0 + 127) / 255);
134 pd->Red += (((ps->Red - pd->Red) * alpha0 + 127) / 255);
135 pd->Alpha += (alpha0 - (pd->Alpha * alpha0 + 127) / 255);
136
137 pd->Blue = pd->Blue * 255 / pd->Alpha;
138 pd->Green = pd->Green * 255 / pd->Alpha;
139 pd->Red = pd->Red * 255 / pd->Alpha;
140 }
141 }
142 }
143 }
144 //---------------------------------------------------------------------------
145 // source alpha = true, dest alpha = true, alpha = 255
146 static VOID Mixer7(PImageCpyData cpyData, INT alpha)
147 {
148 PARGBQuad pd = cpyData->dstScan0;
149 PARGBQuad ps = cpyData->srcScan0;
150 for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
151 {
152 for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
153 {
154 if (ps->Alpha)
155 {
156 pd->Blue = (pd->Blue * pd->Alpha + 127) / 255;
157 pd->Green = (pd->Green * pd->Alpha + 127) / 255;
158 pd->Red = (pd->Red * pd->Alpha + 127) / 255;
159
160 pd->Blue += (((ps->Blue - pd->Blue) * ps->Alpha + 127) / 255);
161 pd->Green += (((ps->Green - pd->Green) * ps->Alpha + 127) / 255);
162 pd->Red += (((ps->Red - pd->Red) * ps->Alpha + 127) / 255);
163 pd->Alpha += (ps->Alpha - (pd->Alpha * ps->Alpha + 127) / 255);
164
165 pd->Blue = pd->Blue * 255 / pd->Alpha;
166 pd->Green = pd->Green * 255 / pd->Alpha;
167 pd->Red = pd->Red * 255 / pd->Alpha;
168 }
169 }
170 }
171 }
172 //---------------------------------------------------------------------------
173
174 VOID ImageMixer(BitmapData *dest, CONST BitmapData *source, INT alpha)
175 {
176 if (alpha <= 0) return;
177 if (alpha > 255) alpha = 255;
178
179 ImageCpyData data;
180 data.width = (INT)(dest->Width < source->Width? dest->Width : source->Width);
181 data.height = (INT)(dest->Height < source->Height? dest->Height : source->Height);
182 data.dstOffset = (dest->Stride >> 2) - data.width;
183 data.srcOffset = (source->Stride >> 2) - data.width;
184 data.dstScan0 = (PARGBQuad)dest->Scan0;
185 data.srcScan0 = (PARGBQuad)source->Scan0;
186
187 MixerProc proc[] = {Mixer0, Mixer1, Mixer2, Mixer1, Mixer4, Mixer5, Mixer6, Mixer7};
188 INT index = (alpha / 255) | ((dest->Reserved >> 16) << 1) | ((source->Reserved >> 16) << 2);
189 proc[index](&data, alpha);
190 }
191 //---------------------------------------------------------------------------
函数ImageMixer有三个参数,分别为目标图数据结构(借用GDI+的BitmapData结构)指针、源图数据结构指针和源图像素混合比例(不透明度,取值范围为0 - 255,前面的公式中的取值范围0 - 1是方便描述)。函数体中的proc数组包括了图像混合的全部8种情况的子函数,而index则按混合比例、目标图Alpha信息和源图Alpha信息组合成子函数调用下标值(Alpha信息在BitmapData结构的保留字段中)。
当然,在实际的运用中,全部8种情况似乎是多了点,可根据情况进行适当合并取舍,以兼顾代码的复杂度和执行效率。下面是我认为比较合理的精简版ImageMixer函数:
1 VOID ImageMixer(BitmapData *dest, CONST BitmapData *source, INT alpha)
2 {
3 if (alpha <= 0) return;
4 if (alpha > 255) alpha = 255;
5
6 ImageCpyData data;
7 data.width = (INT)(dest->Width < source->Width? dest->Width : source->Width);
8 data.height = (INT)(dest->Height < source->Height? dest->Height : source->Height);
9 data.dstOffset = (dest->Stride >> 2) - data.width;
10 data.srcOffset = (source->Stride >> 2) - data.width;
11 data.dstScan0 = (PARGBQuad)dest->Scan0;
12 data.srcScan0 = (PARGBQuad)source->Scan0;
13
14 if (alpha == 255 && !(source->Reserved & PixelAlphaFlag))
15 Mixer1(&data, alpha);
16 else if (dest->Reserved & PixelAlphaFlag)
17 Mixer6(&data, alpha);
18 else
19 Mixer4(&data, alpha);
20 }
21 //---------------------------------------------------------------------------
这个ImageMixer函数只保留了3个调用子函数,其中,Mixer6是完全的正常混合模式,即前面公式3的实现;Mixer4为对不含Alpha信息目标图的混合,即在公式4基础上稍稍扩充了的情况;而Mixer1则为拷贝模式。
下面是采用BCB2007和GDI+调用ImageMixer函数的例子:
1 //---------------------------------------------------------------------------
2
3 // 锁定GDI+位位图扫描线到data
4 FORCEINLINE
5 VOID LockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)
6 {
7 Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight());
8 BOOL hasAlpha = bmp->GetPixelFormat() & PixelFormatAlpha;
9 bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite,
10 PixelFormat32bppARGB, data);
11 if (hasAlpha) data->Reserved |= PixelAlphaFlag;
12 }
13 //---------------------------------------------------------------------------
14
15 // GDI+位图扫描线解锁
16 FORCEINLINE
17 VOID UnlockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)
18 {
19 data->Reserved &= 0xff;
20 bmp->UnlockBits(data);
21 }
22 //---------------------------------------------------------------------------
23
24 void __fastcall TForm1::Button1Click(TObject *Sender)
25 {
26 Gdiplus::Bitmap *dest = new Gdiplus::Bitmap(L"d:\\xmas_011.png");
27 Gdiplus::Bitmap *source = new Gdiplus::Bitmap(L"d:\\Apple.png");
28
29 Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
30 g->DrawImage(dest, 0, 0);
31 g->DrawImage(source, dest->GetWidth(), 0);
32
33 BitmapData dst, src;
34 LockBitmap(dest, &dst);
35 LockBitmap(source, &src);
36 ImageMixer(&dst, &src, 192);
37 UnlockBitmap(source, &src);
38 UnlockBitmap(dest, &dst);
39
40 g->DrawImage(dest, dest->GetWidth() << 1, 0);
41
42 delete g;
43 delete source;
44 delete dest;
45 }
46 //---------------------------------------------------------------------------
下面是运行效果截图:
左边是目标图,中间是源图,右边是源图按不透明度192进行的正常混合。
本文代码未作过多优化。
水平有限,错误在所难免,欢迎指正和指导。邮箱地址:maozefa@hotmail.com