阅读提示:
《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。
《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。
尽可能保持二者内容一致,可相互对照。
本文代码必须包括《C++图像处理 -- 数据类型及公用函数》文章中的BmpData.h头文件以及《C++图像处理 -- 平面几何变换类》TransformMatrix.h文件。
在《C++图像处理 -- 平面几何变换类》一文中,介绍了图像平面几何变换类TransformMatrix,并写了一个简单的临近插值法图像几何变换函数Transform,用于测试。很显然,Transform函数产生的变换图像不仅质量较差,而且也不具备通用性,只能作为一个实现图像几何变换的框架。
本文拟采用临近插值法、双线性插值法和双立方插值法等三种插值方式,来实现较完整、通用的图形图像平面几何变换。下面是除TransformMatrix类外的全部代码(TransformMatrix类代码在《C++图像处理 -- 平面几何变换类》中)。
//---------------------------------------------------------------------------
#define GetPixel4096(data, x, y) \
(PARGBQuad)((LPBYTE)data->Scan0 + (y >> 12) * data->Stride + ((x >> 12) << 2))
//---------------------------------------------------------------------------
// 获取临近插值颜色
FORCEINLINE
ARGBQuad GetNearColor(CONST BitmapData *data, UINT x, UINT y)
{
return *GetPixel4096(data, x, y);
}
//---------------------------------------------------------------------------
// 获取线性插值颜色
FORCEINLINE
ARGBQuad GetBilinearColor(CONST BitmapData *data, UINT x, UINT y)
{
UINT u = (x & 0xfff) >> 4; // u = (x % 0x1000) / 16
UINT v = (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;
PARGBQuad p0 = GetPixel4096(data, x, y);
PARGBQuad p1 = (PARGBQuad)((LPBYTE)p0 + data->Stride);
PARGBQuad p2 = p0 + 1;
PARGBQuad p3 = p1 + 1;
ARGBQuad color;
// 如果不要求很高精度,/ (255 * 255)可改为 >> 16,能提高速度
color.Blue = (p0->Blue * m0 + p1->Blue * m1 + p2->Blue * m2 + p3->Blue * m3) / (255 * 255);
color.Green = (p0->Green * m0 + p1->Green * m1 + p2->Green * m2 + p3->Green * m3) / (255 * 255);
color.Red = (p0->Red * m0 + p1->Red * m1 + p2->Red * m2 + p3->Red * m3) / (255 * 255);
color.Alpha = (p0->Alpha * m0 + p1->Alpha * m1 + p2->Alpha * m2 + p3->Alpha * m3) / (255 * 255);
return color;
}
//---------------------------------------------------------------------------
static INT uvTable[513];
// 获取双立方插值颜色
FORCEINLINE
ARGBQuad GetBicubicColor(CONST BitmapData *data, UINT x, UINT y)
{
INT us[4], vs[4];
UINT u = (x & 0xfff) >> 4; // u = (x % 0x1000) / 16
UINT v = (y & 0xfff) >> 4; // v = (y % 0x1000) / 16
us[0] = uvTable[256 + u];
us[1] = uvTable[u];
us[2] = uvTable[256 - u];
us[3] = uvTable[512 - u];
vs[0] = uvTable[256 + v];
vs[1] = uvTable[v];
vs[2] = uvTable[256 - v];
vs[3] = uvTable[512 - v];
PARGBQuad p = GetPixel4096(data, x, y);
INT pixOffset = data->Stride >> 2;
INT sA, sR, sG, sB;
sA = sR = sG = sB = 0;
for (INT i = 0; i < 4; i ++, p += pixOffset)
{
sB += ((us[0] * p[0].Blue + us[1] * p[1].Blue +
us[2] * p[2].Blue + us[3] * p[3].Blue) * vs[i]);
sG += ((us[0] * p[0].Green + us[1] * p[1].Green +
us[2] * p[2].Green + us[3] * p[3].Green) * vs[i]);
sR += ((us[0] * p[0].Red + us[1] * p[1].Red +
us[2] * p[2].Red + us[3] * p[3].Red) * vs[i]);
sA += ((us[0] * p[0].Alpha + us[1] * p[1].Alpha +
us[2] * p[2].Alpha + us[3] * p[3].Alpha) * vs[i]);
}
sB >>= 16;
sG >>= 16;
sR >>= 16;
sA >>= 16;
ARGBQuad color;
sA = sA < 0? 0 : sA > 255? 255 : sA;
// 因像素格式为PARGB,上限必须为sA(Alpha)而非255
color.Blue = sB < 0? 0 : sB > sA? sA : sB;
color.Green = sG < 0? 0 : sG > sA? sA : sG;
color.Red = sR < 0? 0 : sR > sA? sA : sR;
color.Alpha = sA;
return color;
}
//---------------------------------------------------------------------------
VOID InitBicubicUVTable(FLOAT slope = -0.75)
{
static FLOAT _slope = 0;
DOUBLE x, x2, x3;
if (!(slope < 0)) slope = -0.75;
if (_slope != slope)
{
_slope = slope;
for (INT i = 0; i <= 512; i ++)
{
x = i * (1.0 / 256);
x2 = x * x;
x3 = x * x2;
if (x > 2)
uvTable[i] = 0;
else if (x > 1)
uvTable[i] = (INT)(slope * (x3 - 5 * x2 + 8 * x - 4) * 256);
else
uvTable[i] = (INT)(((slope + 2) * x3 - (slope + 3) * x2 + 1) * 256);
}
}
}
//---------------------------------------------------------------------------
// 目标像素pd和颜色cs合成函数。
FORCEINLINE
VOID MixerColor(PARGBQuad pd, ARGBQuad cs)
{
if (cs.Alpha == 255) // 如果源像素不透明度为255,直接拷贝
pd->Color = cs.Color;
else if (cs.Alpha != 0) // 否则,如果源像素不透明度大于0
{
if (pd->Alpha == 255) // 如果目标像素不透明度为255,ARGB合成
{
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合成
{
// pd转换为PARGB,cs已经是PARGB格式
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与cs合成
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);
// 重新转换为ARGB
pd->Blue = pd->Blue * 255 / pd->Alpha;
pd->Green = pd->Green * 255 / pd->Alpha;
pd->Red = pd->Red * 255 / pd->Alpha;
}
}
}
//---------------------------------------------------------------------------
typedef ARGBQuad (*InterpolateProc)(CONST BitmapData*, UINT, UINT);
// 获取插值过程和扩展半径
INT GetInterpolateProc(InterpolateMode mode, InterpolateProc &proc)
{
INT radius[] = {2, 1, 2, 4};
InterpolateProc procs[] = {GetBilinearColor, GetNearColor,
GetBilinearColor, GetBicubicColor};
proc = procs[mode];
return radius[mode];
}
//---------------------------------------------------------------------------
VOID CopyInterpolateData(BitmapData *dest, CONST BitmapData *source, INT alpha)
{
// SetAlphaFlag(dest, HasAlphaFlag(source));
PARGBQuad pd, ps;
UINT width, height;
INT dstOffset, srcOffset;
GetDataCopyParams(dest, source, width, height, pd, ps, dstOffset, srcOffset);
UINT x, y;
// 如果alpha < 255或者源数据含Alpha,转换为PARGB像素格式
if (alpha < 255 || HasAlphaFlag(source))
{
for (y = 0; y < height; y ++, pd += dstOffset, ps += srcOffset)
{
for (x = 0; x < width; x ++, pd ++, ps ++)
{
pd->Alpha = (alpha * ps->Alpha + 127) / 255;
pd->Blue = (ps->Blue * pd->Alpha + 127) / 255;
pd->Green = (ps->Green * pd->Alpha + 127) / 255;
pd->Red = (ps->Red * pd->Alpha + 127) / 255;
}
}
}
// 否则, 直接像素拷贝
else
{
for (y = 0; y < height; y ++, pd += dstOffset, ps += srcOffset)
{
for (x = 0; x < width; *pd ++ = *ps ++, x ++);
}
}
}
//---------------------------------------------------------------------------
VOID FillBorder(BitmapData *data, UINT radius, BOOL fillX, BOOL fillY)
{
UINT height = data->Height - (radius << 1);
UINT x, y;
PARGBQuad pd, ps;
if (fillX)
{
UINT width = data->Width - (radius << 1);
pd = (PARGBQuad)data->Scan0 + radius * data->Width;
for (y = 0; y < height; y ++)
{
for (x = 0, ps = pd + radius; x < radius; *pd ++ = *ps, x ++);
for (x = 0, pd += width, ps = pd - 1; x < radius; *pd ++ = *ps, x ++);
}
}
if (fillY)
{
pd = (PARGBQuad)data->Scan0;
ps = pd + radius * data->Width;
PARGBQuad pd2 = ps + height * data->Width;
PARGBQuad ps2 = pd2 - data->Width;
for (y = 0; y < radius; y ++)
{
for (x = 0; x < data->Width; *pd ++ = ps[x], *pd2 ++ = ps2[x], x ++);
}
}
}
//---------------------------------------------------------------------------
BOOL CanTransform(INT width, INT height, RECT &r)
{
r.right += r.left;
r.bottom += r.top;
if (r.right > width) r.right = width;
if (r.bottom > height) r.bottom = height;
if (r.left > 0) r.right -= r.left; else r.left = 0;
if (r.top > 0) r.bottom -= r.top; else r.top = 0;
return r.right > 0 && r.bottom > 0;
}
BOOL GetTransformParams(INT dstWidth, INT dstHeight,
INT srcWidth, INT srcHeight, TransformMatrix &matrix, RECT &dst, RECT &src)
{
FLOAT fx, fy, fwidth, fheight;
matrix.GetTransformSize(srcWidth, srcHeight, fx, fy, fwidth, fheight);
matrix.Invert();
dst.left = (LONG)fx;
dst.top = (LONG)fy;
dst.right = (LONG)(fwidth + fx + 0.999999f);
dst.bottom = (LONG)(fheight + fy + 0.999999f);
if (!CanTransform(dstWidth, dstHeight, dst))
return FALSE;
if (fx > 0 || fy > 0)
{
if (fx < 0) fx = 0;
else if (fy < 0) fy = 0;
matrix.Translate(fx, fy);
}
matrix.GetTransformSize(dst.right, dst.bottom, fx, fy, fwidth, fheight);
src.left = (LONG)fx;
src.top = (LONG)fy;
src.right = (LONG)(fwidth + fx + 0.999999f);
src.bottom = (LONG)(fheight + fy + 0.999999f);
if (!CanTransform(srcWidth, srcHeight, src))
return FALSE;
if (fx > 0) matrix.GetElements().dx -= fx;
if (fy > 0) matrix.GetElements().dy -= fy;
return TRUE;
}
//---------------------------------------------------------------------------
// 执行图像数据几何变换
VOID ImageTransform(BitmapData *dest, INT x, INT y,
CONST BitmapData *source, TransformMatrix *matrix, FLOAT alpha = 1.0f)
{
INT alphaI = (INT)(alpha * 255);
if (alphaI <= 0) return;
if (alphaI > 255) alphaI = 255;
// 复制几何变换矩阵对象
TransformMatrix m(matrix);
// 几何变换矩阵绝对增加平移量x, y
m.GetElements().dx += x;
m.GetElements().dy += y;
// 逆转几何变换矩阵,计算并分别返回目标和源图像实际大小dstR和srcR
RECT dstR, srcR;
if (GetTransformParams(dest->Width, dest->Height,
source->Width, source->Height, m, dstR, srcR) == FALSE)
return;
// 将浮点数扩大4096倍,采用定点数运算
INT im11 = (INT)(m.GetElements().m11 * 4096.0f);
INT im12 = (INT)(m.GetElements().m12 * 4096.0f);
INT im21 = (INT)(m.GetElements().m21 * 4096.0f);
INT im22 = (INT)(m.GetElements().m22 * 4096.0f);
// 根据mode获取插值过程及边框扩展半径
InterpolateMode mode = GetInterpolateMode(source);
InterpolateProc ColorProc;
INT radius = GetInterpolateProc(mode, ColorProc);
BitmapData dst, src, exp, tmp;
// 按dstR和srcR分别获取目标和源图像子图到dst和src
GetBitmapData(dest, dstR.left, dstR.top, dstR.right, dstR.bottom, &dst);
GetBitmapData(source, srcR.left, srcR.top, srcR.right, srcR.bottom, &src);
// 建立扩展半径为radius新的图像数据对象exp
GetBitmapData(src.Width + radius * 2, src.Height + radius * 2, &exp);
// src图像数据拷贝到exp
GetBitmapData(&exp, radius, radius, src.Width, src.Height, &tmp);
CopyInterpolateData(&tmp, &src, alphaI);
// 填充exp边框像素。如果im21或im12的12位尾数不为0,说明x或y向为斜边,不填充
BOOL fillX = (im21 & 0xfff) == 0;
BOOL fillY = (im12 & 0xfff) == 0;
FillBorder(&exp, radius, fillX, fillY);
if (fillX && fillY && alphaI == 255 && !HasAlphaFlag(source) &&
dest->Width == dst.Width && dest->Height == dst.Height)
SetAlphaFlag(dest, FALSE);
// 确定源图像边界界限
INT up = radius * 0x800;
INT xDown = (exp.Width - radius) * 0x1000;
INT yDown = (exp.Height - radius) * 0x1000;
// 几何变换逆矩阵的平移量为与子图原点对应的源图起始坐标点
INT xs = (INT)(m.GetElements().dx * 4096.0f) + up + 0x800;
INT ys = (INT)(m.GetElements().dy * 4096.0f) + up + 0x800;
INT width = (INT)dst.Width;
INT height = (INT)dst.Height;
PARGBQuad pd = (PARGBQuad)dst.Scan0;
INT dstOffset = (dst.Stride >> 2) - dst.Width;
// 如果插值方式为双立方卷积,初始化UV表
if (mode == InterpolateModeBicubic)
InitBicubicUVTable(-0.75);
// 按目标子图逐点复制源子图几何变换后的数据
for (y = 0; y < height; y ++, ys += im22, xs += im21, pd += dstOffset)
{
INT y0 = ys;
INT x0 = xs;
for (x = 0; x < width; x ++, x0 += im11, y0 += im12, pd ++)
{
if (y0 >= up && y0 < yDown && x0 >= up && x0 < xDown)
{
MixerColor(pd, ColorProc(&exp, x0, y0));
}
}
}
FreeBitmapData(&exp);
}
//---------------------------------------------------------------------------
同《C++图像处理 -- 平面几何变换类》的临近插值图形图像平面几何变换函数相比,本文代码有以下特点:
1、实现了邻近插值、双线性插值和双立方插值三种插值方式,具有很强的通用性和实用性。
2、插值过程采用了定点数运算,比浮点数运算速度快。
3、较好的实现了边界处理。边界处理是图形图像平面几何变换的一个难点,处理不好会出现难看的锯齿或者半透明的图像边缘。本文代码采用了扩展图像边框像素的方法,较好的解决了这个问题。当边界发生倾斜(变形)时,超出图像边界的像素值设置为0,通过插值后的半透明像素点能较好地解决边界锯齿;而边界不变形时,超出图像边界的像素值用临近的边界像素值替代,这样就不会出现一些不该出现的半透明像素,避免难看的半透明的图像边缘。下面是用本文代码和GDI+几何变换函数分别作1.2倍缩放处理加0.3剪切的双线性插值处理效果图:
4、可处理含Alpha信息的图像,同时增加了图像数据不透明度的处理。对含Alpha信息的图像数据或者不透明度小于1的几何变换,对图像源作了自乘预处理,减少了原像素与变换后的像素值得差异,保证了较好的视觉效果。下面的PNG图片几何变换处理效果图中,左边是经过自乘预处理后的,而右边是未处理的:
5、限于文章篇幅,本文代码以通用性和清晰度为主,没作过多的优化处理,有兴趣的朋友可根据自己需要进行改进。例如,可将缩放处理过程独立,以提高缩放处理处理速度。
下面是用BCB2010和GDI+运用本文图形图像平面几何变换代码处理Alpha像素格式图像的例子代码:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
// 获取源图像扫描线数据
Gdiplus::Bitmap *bmp = new Gdiplus::Bitmap(L"..\\..\\media\\IMG_9440_mf.jpg");
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);
// 设置双立方插值方式
SetInterpolateMode(&source, InterpolateModeBicubic);
// 执行图像几何变换。
// 注意这里使用-r.left, -r.top为坐标,使得变换后的图像完全可见
ImageTransform(&dest, -r.left, -r.top, &source, &matrix, 1);
// 释放图像扫描线数据(位图解锁)
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;
}
//---------------------------------------------------------------------------
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:maozefa@hotmail.com
这里可访问《C++图像处理 -- 文章索引》。