圖像合成是圖形處理核心操作。Photoshop 「正常」混合模式看似簡單,實則涉及 Alpha 通道、預乘、非預乘多種情況組合。本文從數學公式推導到工程落地,給出 8 種情況完整實現與優化版本。
dstRGB = srcRGB * alpha + dstRGB * (1 - alpha)
alpha 為不透明度 [0,1]。此為特例。
採用**預乘 Alpha (Premultiplied Alpha, PARGB)** 空間計算,避免除法與精度損失。
srcRGB' = srcRGB * srcAlpha * alpha / 255dstRGB' = dstRGB * dstAlpha / 255dstRGB'' = dstRGB' + srcRGB' - dstRGB' * srcAlpha * alpha / 255dstAlpha' = dstAlpha + srcAlpha * alpha - dstAlpha * srcAlpha * alpha / 255dstRGB = dstRGB'' * 255 / dstAlpha'化簡得高效公式 (整數運算友好):
// 3-1: 目標預乘
dstRGB = dstRGB * dstAlpha / 255;
// 3-2: 混合 (核心差值公式)
dstRGB = dstRGB + (srcRGB - dstRGB) * srcAlpha * alpha / 255;
// 3-3: Alpha 混合
dstAlpha = dstAlpha + srcAlpha * alpha - dstAlpha * srcAlpha * alpha / 255;
// 3-4: 還原
dstRGB = dstRGB * 255 / dstAlpha;
當 dstAlpha = srcAlpha = 255 (不透明) 時退化為基礎公式。
根據 源 Alpha 有無、目標 Alpha 有無、混合比例 alpha 是否 255,共 2×2×2=8 種。代碼結構統一:遍歷像素,逐通道計算。
typedef union {
DWORD Color;
struct { BYTE Blue, Green, Red, Alpha; };
} ARGBQuad, *PARGBQuad;
typedef struct {
INT width, height;
PARGBQuad dstScan0, srcScan0;
INT dstOffset, srcOffset; // 掃描行跨度補償
} ImageCpyData, *PImageCpyData;
typedef VOID (*MixerProc)(PImageCpyData, INT);
#define PixelAlphaFlag 0x10000 // BitmapData.Reserved 高位標記 Alpha 存在
*pd++ = *ps++ 拷貝。srcAlpha * alpha 加權。srcAlpha 加權。
VOID ImageMixer(BitmapData *dest, CONST BitmapData *source, INT alpha)
{
if (alpha <= 0) return;
if (alpha > 255) alpha = 255;
ImageCpyData data;
// 計算寬高取較小值,處理 Stride 對齊
data.width = min(dest->Width, source->Width);
data.height = min(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;
// 索引編碼: bit0=alpha==255, bit1=destHasAlpha, bit2=srcHasAlpha
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);
}
實際項目中 8 路過於冗餘,保留核心三路:
VOID ImageMixer_Optimized(BitmapData *dest, CONST BitmapData *source, INT alpha)
{
// ... 參數準備同前 ...
if (alpha == 255 && !(source->Reserved & PixelAlphaFlag))
Mixer1(&data, alpha); // 不透明無源Alpha -> 直接拷貝
else if (dest->Reserved & PixelAlphaFlag)
Mixer6(&data, alpha); // 目標有Alpha -> 完整邏輯
else
Mixer4(&data, alpha); // 目標無Alpha、源有Alpha/半透明 -> 擴展插值
}
void TestBlend()
{
Gdiplus::Bitmap *dest = new Gdiplus::Bitmap(L"d:\\xmas_011.png");
Gdiplus::Bitmap *source = new Gdiplus::Bitmap(L"d:\\Apple.png");
BitmapData dst, src;
LockBitmap(dest, &dst); // 鎖定位圖並標記 Alpha Flag
LockBitmap(source, &src);
ImageMixer_Optimized(&dst, &src, 192); // 75% 不透明度混合
UnlockBitmap(source, &src);
UnlockBitmap(dest, &dst);
// 繪製結果...
}
+127 再 /255 實現四捨五入,提升精度。