阅读提示:
《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 + p0->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; } //---------------------------------------------------------------------------
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]
这里可访问《C++图像处理 -- 文章索引》。