实现双线性插值图形图像平面几何变换
说明:本文代码中使用的几何变换类TransformMatrix代码见《图形图像平面几何变换类(C++版)》一文。
我在《图形图像平面几何变换类(C++版)》一文中,用C++写了一个几何变换类TransformMatrix,还写了一个简单的临近插值法图像几何变换函数Transform,用于测试。很显然,Transform函数产生的变换图像不仅质量较差,而且也不具备通用性,只能作为一个实现图像几何变换的框架。
本文拟采用双线性插值法,重写图像几何变换函数,实现较完整、通用的图形图像平面几何变换。下面是除几何变换类TransformMatrix外的双线性插值图形图像平面几何变换的全部代码。
// 定义ARGB像素结构
typedef union
{
ARGB Color;
struct
{
BYTE Blue;
BYTE Green;
BYTE Red;
BYTE Alpha;
};
}ARGBQuad, *PARGBQuad;
//---------------------------------------------------------------------------
// 计算线性插值颜色
FORCEINLINE
ARGBQuad CalcBilinearColor(INT x, INT y, CONST PARGBQuad pixs)
{
UINT u = (UINT)(x & 0xfff) >> 4; // u = (x % 0x1000) / 16
UINT v = (UINT)(y & 0xfff) >> 4; // v = (y % 0x1000) / 16
UINT u0 = u ^ 255; // u0 = 255 - u
UINT v0 = v ^ 255; // v0 = 255 - v
UINT m0 = v0 * u0;
UINT m1 = v * u0;
UINT m2 = v0 * u;
UINT m3 = v * u;
ARGBQuad color;
// 如果不要求很高精度,/ (255 * 255)可改为 >> 16,能提高速度
color.Blue = (pixs[0].Blue * m0 + pixs[1].Blue * m1 +
pixs[2].Blue * m2 + pixs[3].Blue * m3) / (255 * 255);
color.Green = (pixs[0].Green * m0 + pixs[1].Green * m1 +
pixs[2].Green * m2 + pixs[3].Green * m3) / (255 * 255);
color.Red = (pixs[0].Red * m0 + pixs[1].Red * m1 +
pixs[2].Red * m2 + pixs[3].Red * m3) / (255 * 255);
color.Alpha = (pixs[0].Alpha * m0 + pixs[1].Alpha * m1 +
pixs[2].Alpha * m2 + pixs[3].Alpha * m3) / (255 * 255);
return color;
}
//---------------------------------------------------------------------------
// 将像素点p周围的4个像素填充到pixs数组
FORCEINLINE
VOID FillBilinearPixs(PARGBQuad p, PARGBQuad pixs, INT stride)
{
pixs[0] = *p;
pixs[2] = *(p + 1);
(BYTE*)p += stride;
pixs[1] = *p;
pixs[3] = *(p + 1);
}
//---------------------------------------------------------------------------
// 获取线性插值颜色
FORCEINLINE
ARGBQuad GetBilinearColor(INT x, INT y, CONST BitmapData *source)
{
ARGBQuad pixs[4];
FillBilinearPixs((PARGBQuad)((BYTE*)source->Scan0 +
(y >> 12) * source->Stride + ((x >> 12) << 2)), pixs, source->Stride);
return CalcBilinearColor(x, y, pixs);
}
//---------------------------------------------------------------------------
// 获取边界线性插值颜色
ARGBQuad GetBilinearBorder(INT x, INT y, CONST BitmapData *source, UINT *masks)
{
ARGBQuad pixs[4];
PARGBQuad p1, p2;
UINT off1, off2;
UINT flags = (UINT)(-1);
INT x0 = x >> 12;
INT y0 = y >> 12;
if (y0 < 0)
{
p1 = p2 = (PARGBQuad)source->Scan0;
flags &= masks[2];
}
else
{
p1 = p2 = (PARGBQuad)((BYTE*)source->Scan0 + y0 * source->Stride);
if (y0 + 1 < (INT)source->Height)
(BYTE*)p2 += source->Stride;
else
flags &= masks[3];
}
if (x0 < 0)
{
off1 = off2 = 0;
flags &= masks[0];
}
else
{
off1 = off2 = x0 ++;
if (x0 < (INT)source->Width)
off2 ++;
else
flags &= masks[1];
}
// 取得计算线性边界颜色的四个像素值
pixs[0] = *(p1 + off1);
pixs[1] = *(p2 + off1);
pixs[2] = *(p1 + off2);
pixs[3] = *(p2 + off2);
// 对四个像素用对应的掩码标志进行屏蔽
CHAR *p = (CHAR*)&flags;
pixs[0].Color &= p[0];
pixs[1].Color &= p[1];
pixs[2].Color &= p[2];
pixs[3].Color &= p[3];
return CalcBilinearColor(x, y, pixs);
}
// 目标像素pd和颜色cs合成函数
VOID MixColor(PARGBQuad pd, ARGBQuad cs)
{
if (cs.Alpha == 255) // 如果源像素不透明度为255,直接拷贝
pd->Color = cs.Color;
else if (cs.Alpha != 0) // 否则,如果源像素不透明度大于0
{
if (pd->Alpha == 255) // 如果目标像素不透明度为255,Alpha合成
{
pd->Blue += (cs.Blue - (pd->Blue * cs.Alpha + 127) / 255);
pd->Green += (cs.Green - (pd->Green * cs.Alpha + 127) / 255);
pd->Red += (cs.Red - (pd->Red * cs.Alpha + 127) / 255);
}
else // 否则,目标像素转换为PARGB,与源像素合成后再回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 += (cs.Blue - (pd->Blue * cs.Alpha + 127) / 255);
pd->Green += (cs.Green - (pd->Green * cs.Alpha + 127) / 255);
pd->Red += (cs.Red - (pd->Red * cs.Alpha + 127) / 255);
pd->Alpha += (cs.Alpha - (pd->Alpha * cs.Alpha + 127) / 255);
pd->Blue = pd->Blue * 255 / pd->Alpha;
pd->Green = pd->Green * 255 / pd->Alpha;
pd->Red = pd->Red * 255 / pd->Alpha;
}
}
}
//---------------------------------------------------------------------------
// 获取子图数据
BOOL GetSubBitmapData(CONST BitmapData *data, INT x, INT y, INT width, INT height, BitmapData *sub)
{
if (x < 0)
{
width += x;
x = 0;
}
if (x + width > (INT)data->Width)
width = (INT)data->Width - x;
if (width <= 0) return FALSE;
if (y < 0)
{
height += y;
y = 0;
}
if (y + height > (INT)data->Height)
height = (INT)data->Height - y;
if (height <= 0) return FALSE;
sub->Width = width;
sub->Height = height;
sub->Stride = data->Stride;
sub->Scan0 = (CHAR*)data->Scan0 + y * data->Stride + (x << 2);
return TRUE;
}
//---------------------------------------------------------------------------
// 执行图像数据几何变换
VOID Transform(BitmapData *dest, INT x, INT y, CONST BitmapData *source,
TransformMatrix *matrix, BOOL isAlphaSource)
{
// 复制几何变换矩阵对象
TransformMatrix m(matrix);
// 几何变换矩阵绝对增加平移量x, y
m.GetElements().dx += x;
m.GetElements().dy += y;
FLOAT fx, fy, fwidth, fheight;
BitmapData dst, src;
// 按几何变换矩阵计算并获取目标图像子数据
m.GetTransformSize(source->Width, source->Height, fx, fy, fwidth, fheight);
if (!GetSubBitmapData(dest, (INT)fx, (INT)fy,
(INT)(fwidth + 0.999999f), (INT)(fheight + 0.999999f), &dst))
return;
// 获取几何变换逆矩阵
if (!m.Invert()) return;
// 如果目标子图原点坐标大于零,几何变换逆矩阵相对增加平移量fx, fy
if (fx > 0.0f || fy > 0.0f)
{
if (fx < 0.0f) fx = 0.0f;
else if (fy < 0.0f) fy = 0.0f;
m.Translate(fx, fy);
}
// 按几何变换逆矩阵计算并获取源图像子数据
m.GetTransformSize(dst.Width, dst.Height, fx, fy, fwidth, fheight);
GetSubBitmapData(source, (INT)fx, (INT)fy,
(INT)(fwidth + 0.999999f), (INT)(fheight + 0.999999f), &src);
MatrixElements e = m.GetElements();
// 如果源子图原点坐标大于零,几何变换逆矩阵绝对减少平移量fx, fy
if (fx > 0.0f) e.dx -= fx;
if (fy > 0.0f) e.dy -= fy;
// 将浮点数扩大4096倍,采用定点数运算
INT im11 = (INT)(e.m11 * 4096.0f);
INT im12 = (INT)(e.m12 * 4096.0f);
INT im21 = (INT)(e.m21 * 4096.0f);
INT im22 = (INT)(e.m22 * 4096.0f);
// 几何变换逆矩阵的平移量为与子图原点对应的源图起始坐标点
INT xs = (INT)(e.dx * 4096.0f) - 0x800;
INT ys = (INT)(e.dy * 4096.0f) - 0x800;
INT width = (INT)dst.Width;
INT height = (INT)dst.Height;
// 设置子图扫描线指针及行偏移宽度
PARGBQuad pix = (PARGBQuad)dst.Scan0;
INT dstOffset = (dst.Stride >> 2) - dst.Width;
// 确定源图像边界界限
INT up0 = -0x1000;
INT x_down0 = (INT)src.Width * 0x1000;
INT y_down0 = (INT)src.Height * 0x1000;
// 确定源图像界限
INT up = 0;
INT x_down = x_down0 - 0x1000;
INT y_down = y_down0 - 0x1000;
// 边界像素掩码
UINT masks[4] = {-1, -1, -1, -1};
if (im21 % 0x1000 != 0)
{
masks[0] = 0xffff0000;
masks[1] = 0x0000ffff;
}
if (im12 % 0x1000 != 0)
{
masks[2] = 0xff00ff00;
masks[3] = 0x00ff00ff;
}
// 如果源图像素类型为ARGB,转换为PARGB
if (isAlphaSource)
{
PARGBQuad pd = (PARGBQuad)new CHAR[src.Height * src.Stride];
PARGBQuad ps = (PARGBQuad)src.Scan0;
src.Scan0 = pd;
INT Offset = (src.Stride >> 2) - src.Width;
for (y = 0; y < (INT)src.Height; y ++, ps += Offset)
{
for (x = 0; x < (INT)src.Width; pd ++, ps ++, x ++)
{
pd->Blue = (ps->Blue * ps->Alpha + 127) / 255;
pd->Green = (ps->Green * ps->Alpha + 127) / 255;
pd->Red = (ps->Red * ps->Alpha + 127) / 255;
pd->Alpha = ps->Alpha;
}
}
}
// 按目标子图逐点复制源子图几何变换后的数据
for (y = 0; y < height; y ++, ys += im22, xs += im21, pix += dstOffset)
{
INT y0 = ys;
INT x0 = xs;
for (x = 0; x < width; x ++, x0 += im11, y0 += im12, pix ++)
{
if (y0 >= up && y0 < y_down && x0 >= up && x0 < x_down)
MixColor(pix, GetBilinearColor(x0, y0, &src));
else if (y0 >= up0 && y0 < y_down0 && x0 >= up0 && x0 < x_down0)
MixColor(pix, GetBilinearBorder(x0, y0, &src, masks));
}
}
if (isAlphaSource)
delete[] src.Scan0;
}
//---------------------------------------------------------------------------
同《图形图像平面几何变换类(C++版)》的临近插值图形图像平面几何变换函数相比,本文的双线性插值图形图像平面几何变换函数有以下特点:
1、双线性插值法具有很强的通用性。
2、插值过程采用了定点数运算,比浮点数运算速度快。
3、较好的实现了边界处理。边界处理是图形图像平面几何变换的一个难点,处理不好会出现难看的锯齿或者半透明的图像边缘。GetBilinearBorder函数使用一组像素掩码进行处理,当边界发生倾斜(变形)时,超出图像边界的像素值设置为0,通过双线性插值后的半透明像素点能较好地解决边界锯齿;而边界不变形时,超出图像边界的像素值用临近的边界像素值替代,这样就不会出现一些不该出现的半透明像素,避免难看的半透明的图像边缘。下面是用本文代码和GDI+几何变换函数分别作1.2倍缩放处理加0.3的剪切处理效果图:
4、针对平面几何变换的源和目标图像像素的Alpha信息的差异进行了不同的插值处理,例如,对含Alpha信息的图像源作了自乘预处理,可正确地实现常见图像像素格式的合成(效果比较见后面的例子界面图)。
5、限于文章篇幅,本文代码以通用性和清晰度为主,没作过多的优化处理,有兴趣的朋友可根据自己需要进行改进。例如,可将缩放处理过程独立,以提高缩放处理处理速度;亦可将本文MixColor函数按像素Alpha信息的不同进行的合成处理改为按图象整体的像素格式写成不同的合成处理过程,可大大提高处理速度。
下面是用BCB2007运用本文双线性插值图形图像平面几何变换代码处理Alpha像素格式图像的例子:
// 锁定GDI+位图扫描线到data
FORCEINLINE
VOID LockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)
{
Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight());
bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite,
PixelFormat32bppARGB, data);
}
//---------------------------------------------------------------------------
// GDI+位图扫描线解锁
FORCEINLINE
VOID UnlockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)
{
bmp->UnlockBits(data);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
// 获取源图像扫描线数据
Gdiplus::Bitmap *bmp = new Gdiplus::Bitmap(L"d:\\xmas_011.png");
BitmapData source, dest;
LockBitmap(bmp, &source);
// 设置几何变换
TransformMatrix matrix;
matrix.Scale(1.2, 1.2);
matrix.Shear(0.3, 0.3);
// 建立目标位图并获取其扫描线数据
RECT r;
matrix.GetTransformRect(source.Width, source.Height, r);
Gdiplus::Bitmap *newBmp = new Gdiplus::Bitmap(
r.right - r.left, r.bottom - r.top, PixelFormat32bppARGB);
LockBitmap(newBmp, &dest);
// 执行图像几何变换
Transform(&dest, -r.left, -r.top, &source, &matrix, TRUE);
// 释放图像扫描线数据(位图解锁)
UnlockBitmap(newBmp, &dest);
UnlockBitmap(bmp, &source);
// 画几何变换后的图像
Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
g->DrawImage(newBmp, 0, 0);
delete g;
delete newBmp;
delete bmp;
}
//---------------------------------------------------------------------------
例子运行效果图(左边是使用自乘预处理后的插值处理效果图,右边是没做自乘预处理后的插值处理,即例子中的Transform(&dest, -r.left, -r.top, &source, &matrix, TRUE)改为Transform(&dest, -r.left, -r.top, &source, &matrix, FALSE)的效果图):
如有错误或者建议,请来信指导:maozefa@hotmail.com