有关图形图像的平面几何变换,现有的教程、计算机图书以及网上的资料上介绍理论的偏多,即使有些编程实例,也只是介绍图像几何变换的某些特例,如旋转、缩放、平移等。GDI+倒是有个Matrix类,可完整地实现图像的几何变换,可惜没法得到源码。
本文不准备介绍任何关于平面几何变换的理论或者原理,而是直接用Delphi实现一个图形图像平面几何类TTransformMatrix。
下面是TransformMatrix类的全部代码:
(*****************************************************************************
* *
* 本单元定义平面几何变换类 *
* *
* 编制人: 湖北省公安县统计局 毛泽发 2010.10 *
* *
*****************************************************************************)
interface
{$IF RTLVersion >= 17.00}
{$inline auto}
{$IFEND}
uses
Windows, SysUtils, Gdiplus;
type
// 几何变换矩阵结构
PMatrixElements = Gdiplus.PMatrixElements;
TMatrixElements = Gdiplus.TMatrixElements;
// 平面几何变换类
TTransformMatrix = class(TObject)
private
FElements: TMatrixElements;
function GetIdentity: Boolean;
function GetInvertible: Boolean;
procedure SetElements(const Value: TMatrixElements);
function GetIdentityElements: TMatrixElements;
procedure ElementsMultiply(const e: TMatrixElements);
public
// 建立一个新实例,并初始化为单位矩阵 Elements = 1,0,0,1,0,0
constructor Create; overload;
// 建立一个新实例,并复制matrix的元素
constructor Create(matrix: TTransformMatrix); overload;
// 建立一个按指定的元素初始化的新实例
constructor Create(m11, m12, m21, m22, dx, dy: Single); overload;
// 重置对象为单位矩阵
procedure Reset;
// 将对象与matrix相乘
procedure Multiply(const matrix: TTransformMatrix);
// 设置平移
procedure Translate(offsetX, offsetY: Single);
// 设置缩放
procedure Scale(scaleX, scaleY: Single);
// 设置按角度angle沿原点旋转
procedure Rotate(angle: Single);
// 设置按角度angle沿中心点centerX, centerY旋转
procedure RotateAt(angle: Single; centerX, centerY: Single);
// 设置剪切,注意不要将shearX, shearY同时设置为1
procedure Shear(shearX, shearY: Single);
// 如果此对象是可逆转的,则逆转该对象。
procedure Invert;
// 按给定的大小计算并返回实施变换后的尺寸
procedure GetTransformSize(width, height: Integer; var fx, fy, fwidth, fheight: Single);
// 按给定的大小计算并返回实施变换后的尺寸
function GetTransformRect(width, height: Integer): TRect;
// 判断对象是否是可逆转的
property IsInvertible: Boolean read GetInvertible;
// 判断此对象是否是单位矩阵
property IsIdentity: Boolean read GetIdentity;
// 获取或设置对象元素
property Elements: TMatrixElements read FElements write SetElements;
// 获取对象的x偏移量
property OffsetX: Single read FElements.dx write FElements.dx;
// 获取对象的y偏移量
property OffsetY: Single read FElements.dy write FElements.dy;
end;
implementation
{ TTransformMatrix }
constructor TTransformMatrix.Create;
begin
FElements.m11 := 1.0;
FElements.m22 := 1.0;
end;
constructor TTransformMatrix.Create(matrix: TTransformMatrix);
begin
FElements := matrix.Elements;
end;
constructor TTransformMatrix.Create(m11, m12, m21, m22, dx, dy: Single);
begin
FElements.m11 := m11;
FElements.m12 := m12;
FElements.m21 := m21;
FElements.m22 := m22;
FElements.dx := dx;
FElements.dy := dy;
end;
procedure TTransformMatrix.ElementsMultiply(const e: TMatrixElements);
var
m11, m12: Single;
begin
m11 := FElements.m11;
m12 := FElements.m12;
FElements.m11 := e.m11 * m11 + e.m12 * FElements.m21;
FElements.m12 := e.m11 * m12 + e.m12 * FElements.m22;
FElements.m21 := e.m21 * m11 + e.m22 * FElements.m21;
FElements.m22 := e.m21 * m12 + e.m22 * FElements.m22;
end;
function TTransformMatrix.GetIdentity: Boolean;
begin
Result := (FElements.m11 = 1.0) and (FElements.m12 = 0.0) and
(FElements.m21 = 0.0) and (FElements.m22 = 1.0) and
(FElements.dx = 0.0) and (FElements.dy = 0.0);
end;
function TTransformMatrix.GetIdentityElements: TMatrixElements;
begin
FillChar(Result, Sizeof(TMatrixElements), 0);
Result.m11 := 1.0;
Result.m22 := 1.0;
end;
function TTransformMatrix.GetInvertible: Boolean;
begin
Result := Round((FElements.m11 * FElements.m22 - FElements.m12 * FElements.m21) * 1000.0) <> 0;
end;
function TTransformMatrix.GetTransformRect(width, height: Integer): TRect;
var
fx, fy, fwidth, fheight: Single;
begin
GetTransformSize(width, height, fx, fy, fwidth, fheight);
Result.Left := Trunc(fx);
Result.Top := Trunc(fy);
Result.Right := Trunc(fwidth + fx + 0.999999);
Result.Bottom := Trunc(fheight + fy + 0.999999);
end;
procedure TTransformMatrix.GetTransformSize(width, height: Integer; var fx, fy,
fwidth, fheight: Single);
var
fxs, fys: array[0..2] of Single;
v: Single;
i: Integer;
begin
fxs[0] := width;
fxs[1] := 0.0;
fxs[2] := width;
fys[0] := 0.0;
fys[1] := height;
fys[2] := height;
fx := 0.0;
fy := 0.0;
fwidth := 0.0;
fheight := 0.0;
for i := 0 to 2 do
begin
v := fxs[i] * FElements.m11 + fys[i] * FElements.m21;
if v < fx then fx := v
else if v > fwidth then fwidth := v;
v := fxs[i] * FElements.m12 + fys[i] * FElements.m22;
if v < fy then fy := v
else if v > fheight then fheight := v;
end;
fwidth := fwidth - fx;
fheight := fheight - fy;
fx := fx + FElements.dx;
fy := fy + FElements.dy;
end;
procedure TTransformMatrix.Invert;
var
tmp: Double;
m11, dx: Single;
begin
tmp := FElements.m11 * FElements.m22 - FElements.m12 * FElements.m21;
if Trunc(tmp * 1000.0) = 0 then
raise Exception.Create('Not invertible transformation matrix.');
tmp := 1.0 / tmp;
m11 := FElements.m11;
dx := -FElements.dx;
FElements.m11 := tmp * FElements.m22;
FElements.m12 := tmp * -FElements.m12;
FElements.m21 := tmp * -FElements.m21;
FElements.m22 := tmp * m11;
FElements.dx := dx * FElements.m11 - FElements.dy * FElements.m21;
FElements.dy := dx * FElements.m12 - FElements.dy * FElements.m22;
end;
procedure TTransformMatrix.Multiply(const matrix: TTransformMatrix);
begin
FElements.dx := FElements.dx + (matrix.FElements.dx * FElements.m11 +
matrix.FElements.dy * FElements.m21);
FElements.dy := FElements.dy + (matrix.FElements.dx * FElements.m12 +
matrix.FElements.dy * FElements.m22);
ElementsMultiply(matrix.FElements);
end;
procedure TTransformMatrix.Reset;
begin
FElements := GetIdentityElements;
end;
procedure TTransformMatrix.Rotate(angle: Single);
var
e: TMatrixElements;
begin
angle := angle * PI / 180.0;
e.m11 := Cos(angle);
e.m22 := e.m11;
e.m12 := Sin(angle);
e.m21 := -e.m12;
e.dx := 0.0;
e.dy := 0.0;
ElementsMultiply(e);
end;
procedure TTransformMatrix.RotateAt(angle, centerX, centerY: Single);
begin
Translate(centerX, centerY);
Rotate(angle);
Translate(-centerX, -centerY);
end;
procedure TTransformMatrix.Scale(scaleX, scaleY: Single);
var
e: TMatrixElements;
begin
e := GetIdentityElements;
e.m11 := scaleX;
e.m22 := scaleY;
ElementsMultiply(e);
end;
procedure TTransformMatrix.SetElements(const Value: TMatrixElements);
begin
Move(Value, FElements, Sizeof(TMatrixElements));
end;
procedure TTransformMatrix.Shear(shearX, shearY: Single);
var
e: TMatrixElements;
begin
e := GetIdentityElements;
e.m21 := shearX;
e.m12 := shearY;
ElementsMultiply(e);
end;
procedure TTransformMatrix.Translate(offsetX, offsetY: Single);
begin
FElements.dx := FElements.dx + (offsetX * FElements.m11 + offsetY * FElements.m21);
FElements.dy := FElements.dy + (offsetX * FElements.m12 + offsetY * FElements.m22);
end;
end.
TTransformMatrix与GDI+的TGpMatrix布局基本一样,所以关于类的使用方法就不再介绍了,本文的目的在于如何实现自己的平面几何变换类,否则,不如直接用GDI+的TGpMatrix了。
TTransformMatrix的核心代码是Multiply方法(或ElementsMultiply方法)和Invert方法。
Multiply方法通过2个TTransformMatrix的相乘来实现各种复杂的几何变换计算,所有能够实现的具体几何变换都是可以通过其完成的(代码中的平移函数Translate也可以通过其完成的,当然多了一些不必要的计算)。无论是TTransformMatrix类还是GDI+的TGpMatrix类,所提供的都只是基本的几何变换方法,还有些图形图像几何变换,如对称几何变换(镜像)和各种复杂的组合变换。都只能通过Multiply方法或者更直接的变换矩阵成员设置去实现。
Invert方法实现了变换矩阵的逆矩阵,通过这个几何变换逆矩阵,可以很方便地实现图形图像几何变换的实际操作。为什么要靠几何变换矩阵的逆矩阵,而不是直接依据变换矩阵来实现图形图像几何变换的实际操作呢?因为几何变换矩阵表示的意思是,把源图像的任意座标点通过几何变换后投影到目标图像。而源图像像素通过几何变换后与目标图像上的像素点有可能不能一一对应,如图像缩放变换后,不是多个源图像像素点对应同一个目标像素点(缩小),就是源图像像素点不足以填充全部的目标像素点(放大),这就有可能造成目标图像像素点被重复绘制或者被遗漏的现象发生;而几何变换逆矩阵所表示的意思是,对于目标图像任意一个像素点,如果在几何变换前有源图像像素点与其对应,则进行复制。遍历目标图像像素点就能保证目标图像像素点既不重复、也不遗漏的被复制。
为了检验TTransformMatrix类,写了2个简单的过程来实现具体图像几何变换:
function GetSubBitmapData(const data: TBitmapData; x, y, width, height: Integer): TBitmapData;
begin
Result.Scan0 := nil;
if x < 0 then
begin
Inc(width, x);
x := 0;
end;
if x + width > Integer(data.Width) then
width := Integer(data.Width) - x;
if width <= 0 then Exit;
if y < 0 then
begin
Inc(height, y);
y := 0;
end;
if y + height > Integer(data.Height) then
height := Integer(data.Height) - y;
if height <= 0 then Exit;
Result.Width := width;
Result.Height := height;
Result.Stride := data.Stride;
Result.Scan0 := Pointer(Integer(data.Scan0) + y * data.Stride + (x shl 2));
end;
procedure Transform(var dest: TBitmapData; x, y: Integer;
const source: TBitmapData; matrix: TTransformMatrix);
var
m: TTransformMatrix;
e: TMatrixElements;
fx, fy, fwidth, fheight: Single;
x0, y0, dstOffset: Integer;
xs, ys, xs0, ys0: Single;
pix: PARGB;
dst: TBitmapData;
begin
// 复制几何变换矩阵对象
m := TTransformMatrix.Create(matrix);
try
// 几何变换矩阵绝对增加平移量x, y
m.OffsetX := m.OffsetX + x;
m.OffsetY := m.OffsetY + y;
// 按几何变换矩阵计算并获取目标图像数据子数据
m.GetTransformSize(source.Width, source.Height, fx, fy, fwidth, fheight);
dst := GetSubBitmapData(dest, Trunc(fx), Trunc(fy),
Trunc(fwidth + 0.999999), Trunc(fheight + 0.999999));
if dst.Scan0 = nil then Exit;
// 获取几何变换逆矩阵
m.Invert;
// 如果子图数据与目标图像原点不一致,几何变换矩阵相对增加平移量fx, fy
if (fx > 0.0) or (fy > 0.0) then
begin
if fx < 0.0 then fx := 0.0
else if fy < 0.0 then fy := 0.0;
m.Translate(fx, fy);
end;
// 设置子图扫描线指针及行偏移宽度
pix := dst.Scan0;
dstOffset := dst.Stride div 4 - Integer(dst.Width);
// 几何变换逆矩阵的平移量为与子图原点对应的源图起始坐标点
e := m.Elements;
xs := e.dx;
ys := e.dy;
// 逐点计算并复制源图几何变换后的数据到目标子图
for y := 1 to dst.Height do
begin
xs0 := xs;
ys0 := ys;
for x := 1 to dst.Width do
begin
x0 := Round(xs0);
y0 := Round(ys0);
if (x0 >= 0) and (x0 < Integer(source.Width)) and
(y0 >= 0) and (y0 < Integer(source.Height)) then
pix^ := PARGB(Integer(source.Scan0) + y0 * source.Stride + x0 shl 2)^;
Inc(pix);
xs0 := xs0 + e.m11;
ys0 := ys0 + e.m12;
end;
Inc(pix, dstOffset);
xs := xs + e.m21;
ys := ys + e.m22;
end;
finally
m.Free;
end;
end;
这2个过程都使用了GDI+的TBitmapData类型,而非具体的图像类作为参数类型,使得它们具有一定的通用性和扩展性。
GetSubBitmapData函数用于获取一个界定了范围的子图像数据,减少了像素操作时的计算,而Transform过程则用来实现具体的图像几何变换。上面之所以说“简单”,指的是Transform过程复制像素时使用的是直接临近取值,这样转换出来的图像质量较差;而且计算像素地址时采用了浮点数运算,影响了变换速度。但是这个过程的框架却是较完整的,可在此基础上加入像素插值方式,再改浮点数运算为定点数运算,该过程就比较完善了。
下面是一个TBitmap类型的图像进行缩放加剪切的例子:
function GetTBitmapData(bmp: TBitmap): TBitmapData;
begin
bmp.PixelFormat := pf32bit;
Result.Width := bmp.Width;
Result.Height := bmp.Height;
Result.Stride := -(bmp.Width * 4);
Result.Scan0 := bmp.ScanLine[0];
end;
var
bmp, newBmp: TBitmap;
jpg: TJPEGImage;
matrix: TTransformMatrix;
source, dest: TBitmapData;
r: TRect;
begin
bmp := TBitmap.Create;
matrix := TTransformMatrix.Create;
try
jpg := TJPEGImage.Create;
try
jpg.LoadFromFile('001-1.jpg');
bmp.Assign(jpg);
finally
jpg.Free;
end;
source := GetTBitmapData(bmp);
matrix.Scale(1.2, 1.2);
matrix.Shear(0.5, 0.5);
r := matrix.GetTransformRect(source.Width, source.Height);
if (r.Right <= 0) or (r.Bottom <= 0) then Exit;
newBmp := TBitmap.Create;
try
newBmp.PixelFormat := pf32bit;
newBmp.Width := r.Right;
newBmp.Height := r.Bottom;
dest := GetTBitmapData(newBmp);
Transform(dest, 0, 0, source, matrix);
Canvas.Draw(0, 0, newBmp);
finally
newBmp.Free;
end;
finally
matrix.Free;
bmp.Free;
end;
end;
原图像和运行界面截图:
说明:本文中使用的GDI+版本下载地址:
GDI+ for VCL and GDI+ for C语言2010.10.7最新修改版。
如果使用其它GDI+版本,或者不使用GDI+,可将前面TransformMatrix单元里的:
// 几何变换矩阵结构
PMatrixElements = Gdiplus.PMatrixElements;
TMatrixElements = Gdiplus.TMatrixElements;
改为:
case Integer of
0: (Elements: array[0..5] of Single);
1: (m11, m12, m21, m22, dx, dy: Single);
end;
PMatrixElements = ^TMatrixElements;
如有错误请来信指正:[email protected]
如果转载,请注明出处。
请发表评论