阅读提示:
《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。
《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。
尽可能保持二者内容一致,可相互对照。
本文代码必须包括文章《Delphi图像处理 -- 数据类型及公用过程》中的ImageData.pas单元。
有关图形图像的平面几何变换,现有的教程、计算机图书以及网上的资料上介绍理论的偏多,即使有些编程实例,也只是介绍图像几何变换的某些特例,如旋转、缩放、平移等。GDI+倒是有个Matrix类,可完整地实现图像的几何变换,可惜没法得到源码。
本文不准备介绍任何关于平面几何变换的理论或者原理,而是直接用Delphi实现一个图形图像平面几何类TTransformMatrix。(C++版请见:http://blog.csdn.net/maozefa/archive/2010/10/10/5931427.aspx)
下面是TransformMatrix类的全部代码:
unit 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; // 设置双立方插值的斜率。缺省值为-0.75。返回设置前的值 function SetBicubicSlope(const Value: Single): Single; // 临近插值过程。汇编调用 procedure _GetNearColor; // 线性插值过程。汇编调用 procedure _GetBilinearColor; // 双立方插值过程。汇编调用 procedure _GetBicubicColor; 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; (*************************************************************************** * | m11' m12' | 1 | m22 -m12 | * | | = ----------------- * | | * | m21' m22' | m11*m22 - m12*m21 |-m21 m11 | * * dx' = dx * m11' - dy * m21' * dy' = dx * m12' - dy * m22' ***************************************************************************) 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; { 插值过程 } type PXMMType = ^TXMMType; TXMMType = array[0..7] of Word; var BilinearTab: PXMMType; // emm 线形插值用表 BicubicTab: PXMMType; // emm 双立方插值用表 // <-->xmm7 8 * word=0 // <-->ebx stride // --> edx x * 4096 // --> ecx y * 4096 // <-->esi = data->Scan0 + y * data->Stride + x * 4 // <-- xmm0 BilinearColor procedure _GetBilinearColor; asm push ecx push edx and edx, 0ff0h and ecx, 0ff0h mov eax, BilinearTab movq xmm0, [esi] movq xmm1, [esi+ebx] punpcklbw xmm0, xmm7 // xmm0 = pixel2, pixel0 punpcklbw xmm1, xmm7 // xmm1 = pixel3, pixel1 pmulhuw xmm0, [eax+ecx] // xmm0 *= vr pmulhuw xmm1, [eax+ecx+256*16]// xmm1 *= v paddw xmm0, xmm1 pmulhuw xmm0, [eax+edx+512*16]// xmm0 *= u, ur movdqa xmm1, xmm0 psrldq xmm0, 8 // xmm0 >>= 64 paddw xmm0, xmm1 packuswb xmm0, xmm7 pop edx pop ecx end; // <--> xmm7 8 * word = 0 // <--> xmm6 8 * word = 4 // <--> ebx stride // --> edx x * 4096 // --> ecx y * 4096 // <--> esi = data->Scan0 + y * data->Stride + x * 4 // <-- xmm0 BicubicColor procedure _GetBicubicColor; asm push edi push ecx push edx mov edi, BicubicTab and edx, 0ff0h // u = x & 4095 and ecx, 0ff0h // v = y & 4096 mov eax, edx // eax = -u neg eax movq xmm1, [esi] movq xmm2, [esi+8] movq xmm3, [esi+ebx] movq xmm4, [esi+ebx+8] punpcklbw xmm1, xmm7 punpcklbw xmm2, xmm7 punpcklbw xmm3, xmm7 punpcklbw xmm4, xmm7 psllw xmm1, 7 psllw xmm2, 7 psllw xmm3, 7 psllw xmm4, 7 pmulhw xmm1, [edi+edx] pmulhw xmm2, [edi+eax+512*16] pmulhw xmm3, [edi+edx] pmulhw xmm4, [edi+eax+512*16] paddsw xmm1, xmm2 paddsw xmm3, xmm4 movdqa xmm0, xmm1 movdqa xmm4, xmm3 psrldq xmm0, 8 psrldq xmm3, 8 paddsw xmm0, xmm1 paddsw xmm3, xmm4 punpcklqdq xmm0, xmm3 pmulhw xmm0, [edi+ecx] neg ecx add esi, ebx movq xmm1, [esi+ebx] movq xmm2, [esi+ebx+8] movq xmm3, [esi+ebx*2] movq xmm4, [esi+ebx*2+8] punpcklbw xmm1, xmm7 punpcklbw xmm2, xmm7 punpcklbw xmm3, xmm7 punpcklbw xmm4, xmm7 psllw xmm1, 7 psllw xmm2, 7 psllw xmm3, 7 psllw xmm4, 7 pmulhw xmm1, [edi+edx] pmulhw xmm2, [edi+eax+512*16] pmulhw xmm3, [edi+edx] pmulhw xmm4, [edi+eax+512*16] paddsw xmm1, xmm2 paddsw xmm3, xmm4 movdqa xmm2, xmm1 movdqa xmm4, xmm3 psrldq xmm1, 8 psrldq xmm3, 8 paddsw xmm1, xmm2 paddsw xmm3, xmm4 punpcklqdq xmm1, xmm3 pmulhw xmm1, [edi+ecx+512*16] paddsw xmm0, xmm1 movdqa xmm1, xmm0 psrldq xmm0, 8 paddsw xmm0, xmm1 paddsw xmm0, xmm6 psraw xmm0, 3 packuswb xmm0, xmm7 pop edx pop ecx pop edi end; procedure _GetNearColor; asm movd xmm0, [esi] end; var PMMXTable: Pointer; BicubicSlope: Single = 0; function SetBicubicSlope(const Value: Single): Single; function BicubicFunc(x : double): double; var x2, x3: double; begin if x < 0.0 then x := -x; x2 := x * x; x3 := x2 * x; if x <= 1.0 then Result := (Value + 2.0) * x3 - (Value + 3.0) * x2 + 1.0 else if x <= 2.0 then Result := Value * x3 - (5.0 * Value) * x2 + (8.0 * Value) * x - (4.0 * Value) else Result := 0.0; end; var I: Integer; P: PXMMType; begin Result := BicubicSlope; if (BicubicSlope <> Value) and (Value < 0.0) {and (Value >= -2.0)} then begin BicubicSlope := Value; P := BicubicTab; for I := 0 to 255 do begin p^[0] := Round(16384 * BicubicFunc((I + 256) * (1.0 / 256))); p^[1] := p^[0]; p^[2] := p^[0]; p^[3] := p^[0]; p^[4] := Round(16384 * BicubicFunc(I * (1.0 / 256))); p^[5] := p^[4]; p^[6] := p^[4]; p^[7] := p^[4]; Inc(p); end; for I := 256 to 512 do begin p^[0] := Round(16384 * BicubicFunc((I - 256) * (1.0 / 256))); p^[1] := p^[0]; p^[2] := p^[0]; p^[3] := p^[0]; p^[4] := Round(16384 * BicubicFunc(I * (1.0 / 256))); p^[5] := p^[4]; p^[6] := p^[4]; p^[7] := p^[4]; Inc(p); end; end; end; procedure InitBilinearTab; asm mov edx, BilinearTab mov eax, 1000100h movd xmm7, eax pshufd xmm7, xmm7, 0 mov eax, 10001h movd xmm6, eax pshufd xmm6, xmm6, 0 mov eax, 0ff00ffh movd xmm0, eax pshufd xmm0, xmm0, 0 movdqa xmm1, xmm6 xor ecx, ecx jmp @@1 @@sumLoop: mov eax, ecx shl eax, 16 or eax, ecx movd xmm1, eax pshufd xmm1, xmm1, 0 movdqa xmm0, xmm7 psubw xmm0, xmm1 cmp ecx, 255 je @@1 paddw xmm1, xmm6 @@1: psllw xmm0, 8 psllw xmm1, 8 movdqa xmm2, xmm0 movlhps xmm2, xmm1 movdqa [edx], xmm0 // vr movdqa [edx+256*16], xmm1 // v movdqa [edx+512*16], xmm2 // high 64 u, lower 64 ur add edx, 16 inc ecx cmp ecx, 256 jb @@sumLoop end; const BilinearTabSize = 256 * 3 * 16; BicubicTabSize = 513 * 16; initialization begin GetMem(PMMXTable, BilinearTabSize + BicubicTabSize + 12); BilinearTab := PXMMType((Integer(PMMXTable) + 12) and (not 15)); BicubicTab := PXMMType(Integer(BilinearTab) + BilinearTabSize); InitBilinearTab; SetBicubicSlope(-0.75); end; finalization begin FreeMem(PMMXTable); end; end.
TTransformMatrix与GDI+的TGpMatrix布局基本一样,所以关于类的使用方法就不再介绍了,本文的目的在于如何实现自己的平面几何变换类,否则,不如直接用GDI+的TGpMatrix了。
TTransformMatrix的核心代码是Multiply方法(或ElementsMultiply方法)和Invert方法。
Multiply方法通过2个TTransformMatrix的相乘来实现各种复杂的几何变换计算,所有能够实现的具体几何变换都是可以通过其完成的(代码中的平移函数Translate也可以通过其完成的,当然多了一些不必要的计算)。无论是TTransformMatrix类还是GDI+的TGpMatrix类,所提供的都只是基本的几何变换方法,还有些图形图像几何变换,如对称几何变换(镜像)和各种复杂的组合变换。都只能通过Multiply方法或者更直接的变换矩阵成员设置去实现。
Invert方法实现了变换矩阵的逆矩阵,通过这个几何变换逆矩阵,可以很方便地实现图形图像几何变换的实际操作。为什么要靠几何变换矩阵的逆矩阵,而不是直接依据变换矩阵来实现图形图像几何变换的实际操作呢?因为几何变换矩阵表示的意思是,把源图像的任意座标点通过几何变换后投影到目标图像。而源图像像素通过几何变换后与目标图像上的像素点有可能不能一一对应,如图像缩放变换后,不是多个源图像像素点对应同一个目标像素点(缩小),就是源图像像素点不足以填充全部的目标像素点(放大),这就有可能造成目标图像像素点被重复绘制或者被遗漏的现象发生;而几何变换逆矩阵所表示的意思是,对于目标图像任意一个像素点,如果在几何变换前有源图像像素点与其对应,则进行复制。遍历目标图像像素点就能保证目标图像像素点既不重复、也不遗漏的被复制。
上面TransformMatrix.pas单元还提供了几个插值过程,供具体的图像几何变换过程使用,限于篇幅,本文只介绍TTransformMatrix,没有用到这几个过程,也就没作介绍,放在这里,只是为了方便,因为具体的图像几何变换过程必须包括TransformMatrix.pas单元,这样就不会找不到这几个过程了。当然,为了检验TTransformMatrix类,还是写了个简单的过程来实现图像几何变换,下面是这个过程的代码:
procedure Transform(var dest: TImageData; x, y: Integer; const source: TImageData; matrix: TTransformMatrix); var m: TTransformMatrix; e: TMatrixElements; fx, fy, fwidth, fheight: Single; x0, y0, dstOffset: Integer; xs, ys, xs0, ys0: Single; pix: PARGB; dst: TImageData; 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 := GetSubImageData(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;
GetSubBitmapData函数(《Delphi图像处理 -- 数据类型及公用过程》中的ImageData.pas单元)用于获取一个界定了范围的子图像数据,减少了像素操作时的计算,而Transform过程则用来实现具体的图像几何变换。上面之所以说“简单”,指的是Transform过程复制像素时使用的是直接临近取值,这样转换出来的图像质量较差;而且计算像素地址时采用了浮点数运算,影响了变换速度。但是这个过程的框架却是较完整的,可在此基础上加入像素插值方式,再改浮点数运算为定点数运算,该过程就比较完善了。
下面是分别使用Delpha的TBitmap类型和GDI+的TGpBitmap类型进行图像缩放加剪切的例子:
procedure TForm1.Button1Click(Sender: TObject); var bmp, newBmp: TBitmap; jpg: TJPEGImage; matrix: TTransformMatrix; source, dest, exp: TImageData; r: TRect; begin bmp := TBitmap.Create; matrix := TTransformMatrix.Create; try jpg := TJPEGImage.Create; try jpg.LoadFromFile('..\..\media\IMG_9440_mf.jpg'); bmp.Assign(jpg); finally jpg.Free; end; matrix.Scale(1.2, 1.2); matrix.Shear(0.5, 0.5); r := matrix.GetTransformRect(bmp.Width, bmp.Height); if (r.Right <= 0) or (r.Bottom <= 0) then Exit; source := GetBitmapData(bmp); newBmp := TBitmap.Create; try newBmp.PixelFormat := pf32bit; newBmp.Width := r.Right; newBmp.Height := r.Bottom; dest := GetBitmapData(newBmp); Transform(dest, 0, 0, source, matrix); Canvas.Draw(0, 0, newBmp); finally newBmp.Free; end; finally matrix.Free; bmp.Free; end; end; procedure TForm1.Button2Click(Sender: TObject); var bmp, newBmp: TGpBitmap; matrix: TTransformMatrix; source, dest: TImageData; g: TGpGraphics; r: TRect; begin bmp := TGpBitmap.Create('..\..\media\IMG_9440_mf.jpg'); matrix := TTransformMatrix.Create; try matrix.Scale(1.2, 1.2); matrix.Shear(0.5, 0.5); r := matrix.GetTransformRect(bmp.Width, bmp.Height); if (r.Right <= 0) or (r.Bottom <= 0) then Exit; newBmp := TGpBitmap.Create(r.Right, r.Bottom, pf32bppARGB); g := TGpGraphics.Create(Canvas.Handle); try source := LockGpBitmap(bmp); dest := LockGpBitmap(newBmp); Transform(dest, 0, 0, source, matrix); UnlockGpBitmap(newBmp, dest); UnlockGpBitmap(bmp, source); g.DrawImage(newBmp, 0, 0); finally g.Free; newBmp.Free; end; finally matrix.Free; bmp.Free; end; end;
2个例子运行界面类似。下面是原图像和运行界面截图:
说明:本文中使用的GDI+版本下载地址和说明请见《GDI+ for VCL基础 -- GDI+ 与 VCL》。
如果使用与本文不同的GDI+版本,或者不使用GDI+,可将前面TransformMatrix单元里的:
// 几何变换矩阵结构
PMatrixElements = Gdiplus.PMatrixElements;
TMatrixElements = Gdiplus.TMatrixElements;
改为:
《Delphi图像处理》系列使用GDI+单元下载地址和说明见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]
这里可访问《Delphi图像处理 -- 文章索引》。