在圖像處理過程中,圖像的合成操作是使用頻率最高的,如圖像顯示、圖像拷貝、圖像拼接以及的圖層拼合疊加等。
圖像合成,其實也就是圖像像素顏色的混合,在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種情況)來分別進行代碼實現,也可完善和補充上面的文字敘述:
//---------------------------------------------------------------------------
// 定義ARGB像素結構
typedef union
{
ARGB Color;
struct
{
BYTE Blue;
BYTE Green;
BYTE Red;
BYTE Alpha;
};
}ARGBQuad, *PARGBQuad;
typedef struct
{
INT width;
INT height;
PARGBQuad dstScan0;
PARGBQuad srcScan0;
INT dstOffset;
INT srcOffset;
}ImageCpyData, *PImageCpyData;
typedef VOID (*MixerProc)(PImageCpyData, INT);
#define PixelAlphaFlag 0x10000
//---------------------------------------------------------------------------
// source alpha = false, dest alpha = false, alpha < 255
static VOID Mixer0(PImageCpyData cpyData, INT alpha)
{
PARGBQuad pd = cpyData->dstScan0;
PARGBQuad ps = cpyData->srcScan0;
for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
{
for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
{
pd->Blue += (((ps->Blue - pd->Blue) * alpha + 127) / 255);
pd->Green += (((ps->Green - pd->Green) * alpha + 127) / 255);
pd->Red += (((ps->Red - pd->Red) * alpha + 127) / 255);
}
}
}
//---------------------------------------------------------------------------
// source alpha = false, dest alpha = false, alpha = 255
// source alpha = false, dest alpha = true, alpha = 255
static VOID Mixer1(PImageCpyData cpyData, INT alpha)
{
ARGB *pd = (ARGB*)cpyData->dstScan0;
ARGB *ps = (ARGB*)cpyData->srcScan0;
for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
{
for (INT x = 0; x < cpyData->width; x ++, *pd ++ = *ps ++);
}
}
//---------------------------------------------------------------------------
// source alpha = false, dest alpha = true, alpha < 255
static VOID Mixer2(PImageCpyData cpyData, INT alpha)
{
PARGBQuad pd = cpyData->dstScan0;
PARGBQuad ps = cpyData->srcScan0;
for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
{
for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
{
pd->Blue = (pd->Blue * pd->Alpha + 127) / 255;
pd->Green = (pd->Green * pd->Alpha + 127) / 255;
pd->Red = (pd->Red * pd->Alpha + 127) / 255;
pd->Blue += (((ps->Blue - pd->Blue) * alpha + 127) / 255);
pd->Green += (((ps->Green - pd->Green) * alpha + 127) / 255);
pd->Red += (((ps->Red - pd->Red) * alpha + 127) / 255);
pd->Alpha += (alpha - (pd->Alpha * alpha + 127) / 255);
pd->Blue = pd->Blue * 255 / pd->Alpha;
pd->Green = pd->Green * 255 / pd->Alpha;
pd->Red = pd->Red * 255 / pd->Alpha;
}
}
}
//---------------------------------------------------------------------------
// source alpha = true, dest alpha = false, alpha < 255
static VOID Mixer4(PImageCpyData cpyData, INT alpha)
{
PARGBQuad pd = cpyData->dstScan0;
PARGBQuad ps = cpyData->srcScan0;
for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
{
for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
{
INT alpha0 = (alpha * ps->Alpha + 127) / 255;
pd->Blue += (((ps->Blue - pd->Blue) * alpha0 + 127) / 255);
pd->Green += (((ps->Green - pd->Green) * alpha0 + 127) / 255);
pd->Red += (((ps->Red - pd->Red) * alpha0 + 127) / 255);
}
}
}
//---------------------------------------------------------------------------
// source alpha = true, dest alpha = false, alpha = 255
static VOID Mixer5(PImageCpyData cpyData, INT alpha)
{
PARGBQuad pd = cpyData->dstScan0;
PARGBQuad ps = cpyData->srcScan0;
for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
{
for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
{
pd->Blue += (((ps->Blue - pd->Blue) * ps->Alpha + 127) / 255);
pd->Green += (((ps->Green - pd->Green) * ps->Alpha + 127) / 255);
pd->Red += (((ps->Red - pd->Red) * ps->Alpha + 127) / 255);
}
}
}
//---------------------------------------------------------------------------
// source alpha = true, dest alpha = true, alpha < 255
static VOID Mixer6(PImageCpyData cpyData, INT alpha)
{
PARGBQuad pd = cpyData->dstScan0;
PARGBQuad ps = cpyData->srcScan0;
for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
{
for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
{
INT alpha0 = (alpha * ps->Alpha + 127) / 255;
if (alpha0)
{
pd->Blue = (pd->Blue * pd->Alpha + 127) / 255;
pd->Green = (pd->Green * pd->Alpha + 127) / 255;
pd->Red = (pd->Red * pd->Alpha + 127) / 255;
pd->Blue += (((ps->Blue - pd->Blue) * alpha0 + 127) / 255);
pd->Green += (((ps->Green - pd->Green) * alpha0 + 127) / 255);
pd->Red += (((ps->Red - pd->Red) * alpha0 + 127) / 255);
pd->Alpha += (alpha0 - (pd->Alpha * alpha0 + 127) / 255);
pd->Blue = pd->Blue * 255 / pd->Alpha;
pd->Green = pd->Green * 255 / pd->Alpha;
pd->Red = pd->Red * 255 / pd->Alpha;
}
}
}
}
//---------------------------------------------------------------------------
// source alpha = true, dest alpha = true, alpha = 255
static VOID Mixer7(PImageCpyData cpyData, INT alpha)
{
PARGBQuad pd = cpyData->dstScan0;
PARGBQuad ps = cpyData->srcScan0;
for (INT y = 0; y < cpyData->height; y ++, pd += cpyData->dstOffset, ps += cpyData->srcOffset)
{
for (INT x = 0; x < cpyData->width; x ++, pd ++, ps ++)
{
if (ps->Alpha)
{
pd->Blue = (pd->Blue * pd->Alpha + 127) / 255;
pd->Green = (pd->Green * pd->Alpha + 127) / 255;
pd->Red = (pd->Red * pd->Alpha + 127) / 255;
pd->Blue += (((ps->Blue - pd->Blue) * ps->Alpha + 127) / 255);
pd->Green += (((ps->Green - pd->Green) * ps->Alpha + 127) / 255);
pd->Red += (((ps->Red - pd->Red) * ps->Alpha + 127) / 255);
pd->Alpha += (ps->Alpha - (pd->Alpha * ps->Alpha + 127) / 255);
pd->Blue = pd->Blue * 255 / pd->Alpha;
pd->Green = pd->Green * 255 / pd->Alpha;
pd->Red = pd->Red * 255 / pd->Alpha;
}
}
}
}
//---------------------------------------------------------------------------
VOID ImageMixer(BitmapData *dest, CONST BitmapData *source, INT alpha)
{
if (alpha <= 0) return;
if (alpha > 255) alpha = 255;
ImageCpyData data;
data.width = (INT)(dest->Width < source->Width? dest->Width : source->Width);
data.height = (INT)(dest->Height < source->Height? dest->Height : source->Height);
data.dstOffset = (dest->Stride >> 2) - data.width;
data.srcOffset = (source->Stride >> 2) - data.width;
data.dstScan0 = (PARGBQuad)dest->Scan0;
data.srcScan0 = (PARGBQuad)source->Scan0;
MixerProc proc[] = {Mixer0, Mixer1, Mixer2, Mixer1, Mixer4, Mixer5, Mixer6, Mixer7};
INT index = (alpha / 255) | ((dest->Reserved >> 16) << 1) | ((source->Reserved >> 16) << 2);
proc[index](&data, alpha);
}
//---------------------------------------------------------------------------
函數ImageMixer有三個參數,分別為目標圖數據結構(借用GDI+的BitmapData結構)指針、源圖數據結構指針和源圖像素混合比例(不透明度,取值范圍為0 - 255,前面的公式中的取值范圍0 - 1是方便描述)。函數體中的proc數組包括了圖像混合的全部8種情況的子函數,而index則按混合比例、目標圖Alpha信息和源圖Alpha信息組合成子函數調用下標值(Alpha信息在BitmapData結構的保留字段中)。
當然,在實際的運用中,全部8種情況似乎是多了點,可根據情況進行適當合並取捨,以兼顧代碼的復雜度和執行效率。下面是我認為比較合理的精簡版ImageMixer函數:
VOID ImageMixer(BitmapData *dest, CONST BitmapData *source, INT alpha)
{
if (alpha <= 0) return;
if (alpha > 255) alpha = 255;
ImageCpyData data;
data.width = (INT)(dest->Width < source->Width? dest->Width : source->Width);
data.height = (INT)(dest->Height < source->Height? dest->Height : source->Height);
data.dstOffset = (dest->Stride >> 2) - data.width;
data.srcOffset = (source->Stride >> 2) - data.width;
data.dstScan0 = (PARGBQuad)dest->Scan0;
data.srcScan0 = (PARGBQuad)source->Scan0;
if (alpha == 255 && !(source->Reserved & PixelAlphaFlag))
Mixer1(&data, alpha);
else if (dest->Reserved & PixelAlphaFlag)
Mixer6(&data, alpha);
else
Mixer4(&data, alpha);
}
//---------------------------------------------------------------------------
這個ImageMixer函數只保留了3個調用子函數,其中,Mixer6是完全的正常混合模式,即前面公式3的實現;Mixer4為對不含Alpha信息目標圖的混合,即在公式4基礎上稍稍擴充了的情況;而Mixer1則為拷貝模式。
下面是采用BCB2007和GDI+調用ImageMixer函數的例子:
//---------------------------------------------------------------------------
// 鎖定GDI+位位圖掃描線到data
FORCEINLINE
VOID LockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)
{
Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight());
BOOL hasAlpha = bmp->GetPixelFormat() & PixelFormatAlpha;
bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite,
PixelFormat32bppARGB, data);
if (hasAlpha) data->Reserved |= PixelAlphaFlag;
}
//---------------------------------------------------------------------------
// GDI+位圖掃描線解鎖
FORCEINLINE
VOID UnlockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)
{
data->Reserved &= 0xff;
bmp->UnlockBits(data);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Gdiplus::Bitmap *dest = new Gdiplus::Bitmap(L"d:\\xmas_011.png");
Gdiplus::Bitmap *source = new Gdiplus::Bitmap(L"d:\\Apple.png");
Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
g->DrawImage(dest, 0, 0);
g->DrawImage(source, dest->GetWidth(), 0);
BitmapData dst, src;
LockBitmap(dest, &dst);
LockBitmap(source, &src);
ImageMixer(&dst, &src, 192);
UnlockBitmap(source, &src);
UnlockBitmap(dest, &dst);
g->DrawImage(dest, dest->GetWidth() << 1, 0);
delete g;
delete source;
delete dest;
}
//---------------------------------------------------------------------------
下面是運行效果截圖:
左邊是目標圖,中間是源圖,右邊是源圖按不透明度192進行的正常混合。
本文代碼未作過多優化。
摘自 閒人阿發伯的業余編程心得