图形图像平面几何变换类(C++版)
本文用C++实现一个图形图像平面几何变换类TransformMatrix,布局同《图形图像平面几何变换类(Delphi版)》大致相同,下面是TransformMatrix类的全部代码:
{
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);
}
// 设置剪切,注意不要将shearX, shearY同时设置为1
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;
}
};
TransformMatrix的核心代码是Multiply函数(或ElementsMultiply函数)和Invert函数。
Multiply函数通过2个TransformMatrix的相乘来实现各种复杂的几何变换计算,所有能够实现的具体几何变换都是可以通过其完成的(代码中的平移函数Translate也可以通过其完成的,当然多了一些不必要的计算)。TransformMatrix类所提供的都只是基本的几何变换方法,还有些图形图像几何变换,如对称几何变换(镜像)和各种复杂的组合变换。都只能通过Multiply函数或者更直接的变换矩阵成员设置去实现。
Invert函数实现了变换矩阵的逆矩阵,通过这个几何变换逆矩阵,可以很方便地实现图形图像几何变换的实际操作。为什么要靠几何变换矩阵的逆矩阵,而不是直接依据变换矩阵来实现图形图像几何变换的实际操作呢?因为几何变换矩阵表示的意思是,把源图像的任意座标点通过几何变换后投影到目标图像。而源图像像素通过几何变换后与目标图像上的像素点有可能不能一一对应,如图像缩放变换后,不是多个源图像像素点对应同一个目标像素点(缩小),就是源图像像素点不足以填充全部的目标像素点(放大),这就有可能造成目标图像像素点被重复绘制或者被遗漏的现象发生;而几何变换逆矩阵所表示的意思是,对于目标图像任意一个像素点,如果在几何变换前有源图像像素点与其对应,则进行复制。遍历目标图像像素点就能保证目标图像像素点既不重复、也不遗漏的被复制。
同样,为了检验TransformMatrix类,写了2个简单的函数来实现具体图像几何变换:
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):
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::Button2Click(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;
}
因为图像几何变换是从源图左上角开始的,所以有的变换后的图像只能有一部分在目标图上,甚至完全不可见,如旋转90度,可检查matrix.GetTransformRect返回矩形的right和bottom(分别为变换后的宽度和高度)是否大于零来确定变换后图像是否可见。本例子使用了r.right - r.left和r.bottom - r.top作为变换后图像的宽度和高度,并在Transform函数中用-r.left和-r.top作为变换偏移,使得源图像无论怎么变换,变换后的图像都是完全可见的。
运行界面截图:
如有错误请来信指正:maozefa@hotmail.com
如果转载,请注明出处。