实现完整的图像平面几何变换

有关图像的平面几何变换,现有的教程、计算机图书以及网上的资料上介绍理论的偏多,即使有些编程实例,也只是介绍图像几何变换的某些特例,如旋转、缩放、平移等。GDI+倒是有个Matrix类,可完整地实现图像的几何变换,可惜没法得到源码。

本文将完整的实现一个类似GDI+ Matrix的C++几何变换类TransformMatrix,关于几何变换的理论及原理请参考有关书籍或资料。

下面是C++几何变换类TransformMatrix的代码:

typedef union { float Elements[6]; struct { float m11; float m12; float m21; float m22; float dx; float dy; }; }MatrixElements, *PMatrixElements; class TransformMatrix { private: MatrixElements elements; VOID ElementsInit(MatrixElements &e) { e.m11 = e.m22 = 1.0f; e.m12 = e.m21 = e.dx = e.dy = 0.0f; } VOID ElementsMultiply(MatrixElements &e) { float m11 = elements.m11; float m12 = elements.m12; elements.m11 = e.m11 * m11 + e.m12 * elements.m21; elements.m12 = e.m11 * m12 + e.m12 * elements.m22; elements.m21 = e.m21 * m11 + e.m22 * elements.m21; elements.m22 = e.m21 * m12 + e.m22 * elements.m22; } public: // 建立一个新实例,并初始化为单位矩阵 Elements = 1,0,0,1,0,0 TransformMatrix(VOID) { Reset(); } // 建立一个新实例,并复制matrix的元素 TransformMatrix(TransformMatrix *matrix) { SetElements(matrix->elements); } TransformMatrix(TransformMatrix &matrix) { SetElements(matrix.elements); } // 建立一个按指定的元素初始化的新实例 TransformMatrix(float m11, float m12, float m21, float m22, float dx, float dy) { SetElements(m11, m12, m21, m22, dx, dy); } // 重置对象为单位矩阵 VOID Reset(VOID) { ElementsInit(elements); } // 将对象与matrix相乘 VOID Multiply(TransformMatrix *matrix) { // float dx = elements.dx; elements.dx += (matrix->elements.dx * elements.m11 + matrix->elements.dy * elements.m21); elements.dy += (matrix->elements.dx * elements.m12 + matrix->elements.dy * elements.m22); ElementsMultiply(matrix->elements); } VOID Multiply(TransformMatrix &matrix) { Multiply(&matrix); } // 设置平移 VOID Translate(float offsetX, float offsetY) { elements.dx += (offsetX * elements.m11 + offsetY * elements.m21); elements.dy += (offsetX * elements.m12 + offsetY * elements.m22); } // 设置缩放 VOID Scale(float scaleX, float scaleY) { MatrixElements e; ElementsInit(e); e.m11 = scaleX; e.m22 = scaleY; ElementsMultiply(e); } // 设置剪切 VOID Shear(float shearX, float shearY) { MatrixElements e; ElementsInit(e); e.m21 = shearX; e.m12 = shearY; ElementsMultiply(e); } // 设置按角度angle沿原点旋转 VOID Rotate(float angle) { MatrixElements e; angle = angle * M_PI / 180.0f; e.m11 = e.m22 = cos(angle); e.m12 = sin(angle); e.m21 = -e.m12; e.dx = e.dy = 0.0f; ElementsMultiply(e); } // 设置按角度angle沿中心点centerX, centerY旋转 VOID RotateAt(float angle, float centerX, float centerY) { Translate(centerX, centerY); Rotate(angle); Translate(-centerX, -centerY); } // 如果此对象是可逆转的,则逆转该对象,返回TRUE;否则返回FALSE BOOL Invert(VOID) { double tmp = elements.m11 * elements.m22 - elements.m12 * elements.m21; if ((INT)(tmp * 1000.0f) == 0) return FALSE; tmp = 1.0f / tmp; float m11 = elements.m11; float dx = -elements.dx; elements.m11 = tmp * elements.m22; elements.m12 = tmp * -elements.m12; elements.m21 = tmp * -elements.m21; elements.m22 = tmp * m11; elements.dx = dx * elements.m11 - elements.dy * elements.m21; elements.dy = dx * elements.m12 - elements.dy * elements.m22; return TRUE; } // 按给定的大小计算并返回实施变换后的尺寸 VOID GetTransformSize(INT width, INT height, float &fx, float &fy, float &fwidth, float &fheight) { float fxs[3], fys[3], v; fxs[1] = fys[0] = 0.0f; fxs[0] = fxs[2] = width; fys[1] = fys[2] = height; fx = fy = fwidth = fheight = 0.0f; for (INT i = 0; i < 3; i ++) { v = fxs[i] * elements.m11 + fys[i] * elements.m21; if (v < fx) fx = v; else if (v > fwidth) fwidth = v; v = fxs[i] * elements.m12 + fys[i] * elements.m22; if (v < fy) fy = v; else if (v > fheight) fheight = v; } fwidth -= fx; fheight -= fy; fx += elements.dx; fy += elements.dy; } // 按给定的大小计算并返回实施变换后整型数矩形 VOID GetTransformRect(int width, int height, RECT &r) { float fx, fy, fwidth, fheight; GetTransformSize(width, height, fx, fy, fwidth, fheight); r.left = (INT)fx; r.top = (INT)fy; r.right = (INT)(fwidth + fx + 0.999999f); r.bottom = (INT)(fheight + fy + 0.999999f); } // 判断此对象是否是单位矩阵 BOOL GetIdentity(VOID) { return (elements.m11 == 1.0f && elements.m22 == 1.0f && elements.m12 == 0.0f && elements.m21 == 0.0f && elements.dx == 0.0f && elements.dy == 0.0f); } // 获取对象的x偏移量 float GetOffsetX(VOID) { return elements.dx; } // 获取对象的y偏移量 float GetOffsetY(VOID) { return elements.dy; } // 判断对象是否是可逆转的。 BOOL GetInvertible(VOID) { return (INT)(1000.0f * (elements.m11 * elements.m22 - elements.m12 * elements.m21)) != 0; } // 获取对象元素 MatrixElements& GetElements(VOID) { return elements; } // 设置对象元素。注:设置元素是覆盖形式的 VOID SetElements(CONST MatrixElements &value) { SetElements(value.m11, value.m12, value.m21, value.m22, value.dx, value.dy); } VOID SetElements(float m11, float m12, float m21, float m22, float dx, float dy) { elements.m11 = m11; elements.m12 = m12; elements.m21 = m21; elements.m22 = m22; elements.dx = dx; elements.dy = dy; } };

上面代码中定义了一个几何变换矩阵成员类型MatrixElements,便于实际编程时获取或设置几何变换矩阵成员,TransformMatrix只是简单的对其进行了封装,并通过计算实现有关的几何变换。

TransformMatrix的核心代码是Multiply函数(或ElementsMultiply函数)和Invert函数。

Multiply函数可完成各种复杂的几何变换计算,所有能够实现的具体几何变换都是可以通过其完成的(代码中的平移函数Translate也可以通过其完成的,当然多了一些不必要的计算)。虽说本文标题是《实现完整的图像平面几何变换》,但TransformMatrix中的几种基础的变换函数并不代表全部的几何变换,如对称几何变换(镜像),更不用说复杂的组合变换。这倒不是本人要做“标题党”,我所说的“实现完整的图像几何变换”,是指可以通过Multiply函数或者更直接的变换矩阵成员设置去实现“完整的”图像几何变换,除非其不能使用平面几何变换矩阵进行描述(如梯形变换我就没想到怎么实现,也许其超出了平面几何变换矩阵范畴?),或者不能进行实际的几何变换(不可逆);“实现完整的图像几何变换”的另一层含义是下面的图像变换执行函数可实现TransformMatrix所能表示的任意图像几何变换,而不必去写一个个具体的,如缩放、旋转变换函数等。

Invert函数实现了变换矩阵的逆矩阵,通过这个几何变换逆矩阵,可以很方便地实现图形图像几何变换的实际操作。为什么要靠几何变换矩阵的逆矩阵,而不是直接依据变换矩阵来实现图形图像几何变换的实际操作呢?因为几何变换矩阵表示的意思是,把源图像的任意座标点通过几何变换后投影到目标图像。因为源图像像素通过几何变换后与目标图像上的像素点有可能不能一一对应,如图像缩放变换后,不是多个源图像像素点对应同一个目标像素点(缩小),就是源图像像素点不足以填充全部的目标像素点(放大),这就有可能造成目标图像像素点被重复绘制或者被遗漏的现象发生;而几何变换逆矩阵所表示的意思是,对于目标图像任意一个像素点,如果在几何变换前有源图像像素点与其对应,则进行复制。遍历目标图像像素点就能保证目标图像像素点既不重复、也不遗漏的被复制。

下面是一个图像几何变换函数代码:

// 获取子图数据 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) { // 复制几何变换矩阵对象 TransformMatrix m(matrix); // 几何变换矩阵绝对增加平移量x, y m.GetElements().dx += x; m.GetElements().dy += y; // 按几何变换矩阵计算并获取目标图像数据子数据 float fx, fy, fwidth, fheight; m.GetTransformSize(source->Width, source->Height, fx, fy, fwidth, fheight); BitmapData dst; 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); } // 设置子图扫描线指针及行偏移宽度 UINT *pix = (UINT*)dst.Scan0; INT dstOffset = (dst.Stride >> 2) - dst.Width; // 几何变换逆矩阵的平移量为与子图原点对应的源图起始坐标点 MatrixElements e = m.GetElements(); float xs = e.dx; float ys = e.dy; // 逐点计算并复制源图几何变换后的数据到目标子图 for (y = 0; y < (INT)dst.Height; y ++, pix += dstOffset, xs += e.m21, ys += e.m22) { float xs0 = xs; float ys0 = ys; for (x = 0; x < (INT)dst.Width; x ++, pix ++, xs0 += e.m11, ys0 += e.m12) { INT x0 = xs0 < 0.0f? (INT)(xs0 - 0.5f) : (INT)(xs0 + 0.5f); INT y0 = ys0 < 0.0f? (INT)(ys0 - 0.5f) : (INT)(ys0 + 0.5f); if (y0 >= 0 && y0 < (INT)source->Height && x0 >= 0 && x0 < (INT)source->Width) *pix = *(UINT*)((CHAR*)source->Scan0 + y0 * source->Stride + (x0 << 2)); } } }

上面图像几何变换函数的几个特点:

1、可以实现任意的图像几何变换(只要TransformMatrix能正确表达的,即变换矩阵可逆);

2、采用了GDI+ 的BitmapData结构(转换为32位ARGB像素格式),而并非任何具体的图像格式,保证了其通用性;

3、函数使用浮点数运算,但在计算像素点位置时避免了通常的浮点数乘除运算,既提高了一定的运算速度,也为以后修改为定点数运算奠定了基础;

4、函数采用临近像素插值,且没有边界像素处理代码,像素复制质量较差。

可以看出,Transform函数的着重点在于特点(1),在实际的实现代码中,可以把它作为一个框架进行扩充和修改。

下面是一个利用Transform函数对GDI+位图进行旋转变换的例子(使用BCB2007):

// 锁定GDI+位图扫描线到data inline VOID LockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data) { Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight()); bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite, PixelFormat32bppARGB, data); } // GDI+位图扫描线解锁 inline VOID UnlockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data) { bmp->UnlockBits(data); } void __fastcall TForm1::Button1Click(TObject *Sender) { // 获取源图像扫描线数据 Gdiplus::Bitmap *bmp = new Gdiplus::Bitmap(L"..//media//001-1.jpg"); BitmapData source, dest; LockBitmap(bmp, &source); // 设置几何变换 TransformMatrix matrix; matrix.Rotate(45); // 建立目标位图并获取其扫描线数据 // r.right和r.bottom分别为几何变换后可见部分的宽度和高度 RECT r; matrix.GetTransformRect(source.Width, source.Height, r); if (r.right > 0 && r.bottom > 0) { Gdiplus::Bitmap *newBmp = new Gdiplus::Bitmap(r.right, r.bottom, PixelFormat32bppARGB); LockBitmap(newBmp, &dest); Transform(&dest, 0, 0, &source, &matrix); UnlockBitmap(newBmp, &dest); // 画几何变换后的图像 Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle); g->DrawImage(newBmp, 0, 0); delete g; delete newBmp; } UnlockBitmap(bmp, &source); delete bmp; }

下面是图像旋转变换例子运行界面截图:

实现完整的图像平面几何变换

由于图像几何变换是以源图原点(0,0)为变换原点,所以界面上只能看到原点右下边的图像。还有些几何变换,如旋转90度、180度等,可能会导致几何变换后的图像完全不可见,为了直观的看到各种几何变换后的完整图像,可以修改一下例子代码,将TransformMatrix::GetTransformRect函数返回矩形的左上边部分也包括进来:

void __fastcall TForm1::Button1Click(TObject *Sender) { // 获取源图像扫描线数据 Gdiplus::Bitmap *bmp = new Gdiplus::Bitmap(L"..//media//001-1.jpg"); BitmapData source, dest; LockBitmap(bmp, &source); // 设置几何变换 TransformMatrix matrix; matrix.Rotate(45); // matrix.RotateAt(45, source.Width / 2, source.Height / 2); // matrix.Scale(1.2, 1.2); // matrix.Shear(0.2, 0.3); // matrix.GetElements().m11 = -1.0f; // 水平镜像 // matrix.GetElements().m22 = -1.0f; // 垂直镜像 // matrix.SetElements(0, 1, 1, 0, 0, 0); // x=y镜像 // 建立目标位图并获取其扫描线数据 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); // 释放图像扫描线数据(位图解锁) 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; }

运行界面截图如下:

实现完整的图像平面几何变换

例子代码中,被注释掉的是一些图像常用几何变换,其运行界面就不一一贴图了。

上面的例子使用的是GDI+ 位图,而我们通常使用更多的是GDI位图,下面以BCB2007的VCL位图为例作为本文结束部分:

// 获取VCL位图扫描线到data VOID GetTBitmapData(::Graphics::TBitmap *bmp, BitmapData *data) { bmp->PixelFormat = pf32bit; data->Width = bmp->Width; data->Height = bmp->Height; data->Scan0 = bmp->ScanLine[0]; // 因为VCL位图扫描线是倒置的,所以其扫描线字节数设置为负数 data->Stride = -(bmp->Width << 2); } void __fastcall TForm1::Button3Click(TObject *Sender) { TJPEGImage *jpg = new TJPEGImage(); jpg->LoadFromFile("..//media//001-1.jpg"); ::Graphics::TBitmap *bmp = new ::Graphics::TBitmap(); bmp->Assign(jpg); delete jpg; BitmapData source, dest; GetTBitmapData(bmp, &source); // 设置几何变换 TransformMatrix matrix; // matrix.Rotate(45); // matrix.RotateAt(45, source.Width / 2, source.Height / 2); matrix.Scale(1.2, 1.2); matrix.Shear(0.5, 0.5); // matrix.GetElements().m11 = -1.0f; // 水平镜像 // matrix.GetElements().m22 = -1.0f; // 垂直镜像 // matrix.SetElements(0, 1, 1, 0, 0, 0); // x=y镜像 // 建立目标位图并获取其扫描线数据 RECT r; matrix.GetTransformRect(source.Width, source.Height, r); ::Graphics::TBitmap *newBmp = new ::Graphics::TBitmap(); newBmp->PixelFormat = pf32bit; newBmp->Width = r.right - r.left; newBmp->Height = r.bottom - r.top; GetTBitmapData(newBmp, &dest); // 执行图像几何变换 Transform(&dest, -r.left, -r.top, &source, &matrix); // 画几何变换后的图像 newBmp->Transparent = true; newBmp->TransparentColor = clWhite; Canvas->Draw(0, 0, newBmp); delete newBmp; delete bmp; }

该例子实现了图像缩放与剪切组合变换,其运行界面截图如下:

实现完整的图像平面几何变换

尽管我十分努力,但水平有限,错误在所难免,欢迎指正和指导。邮箱地址:

[email protected]