以实际角度旋转位图

NGL*_*GLN 32 delphi image-processing rotation

曾几何时,在阅读这个问题时,我想知道如何在任何程度上旋转位图,而不是自己摆弄所有的位.最近,其他人也遇到了明显的困难.

已经有很多问题以90°的间隔处理旋转,大多数都不是这个问题,但我想以一个真正的角度旋转.优选地,可以由于旋转而调整图像尺寸,并且为将要添加到图像表面的部分设置定制(透明)背景颜色.然后我假设例程的签名看起来像:

procedure RotateBitmap(Bmp: TBitmap; Angle: Single; AdjustSize: Boolean; 
  BackColor: TColor);
Run Code Online (Sandbox Code Playgroud)

这些答案提到了构建此例程的以下候选:SetWorldTransform,PlgBlt,GDI +,但我希望看到(高效)实现.

NGL*_*GLN 56

TL;博士; 使用GDI +

SetWorldTransform

使用WinAPI的SetWorldTransform,您可以转换设备上下文的空间:旋转,剪切,偏移和缩放.这是通过设置XFORM类型的变换矩阵的成员来完成的.根据文档填写其成员.

procedure RotateBitmap(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean;
  BkColor: TColor = clNone);
var
  C: Single;
  S: Single;
  XForm: tagXFORM;
  Tmp: TBitmap;
begin
  C := Cos(Rads);
  S := Sin(Rads);
  XForm.eM11 := C;
  XForm.eM12 := S;
  XForm.eM21 := -S;
  XForm.eM22 := C;
  Tmp := TBitmap.Create;
  try
    Tmp.TransparentColor := Bmp.TransparentColor;
    Tmp.TransparentMode := Bmp.TransparentMode;
    Tmp.Transparent := Bmp.Transparent;
    Tmp.Canvas.Brush.Color := BkColor;
    if AdjustSize then
    begin
      Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
      Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
      XForm.eDx := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
      XForm.eDy := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
    end
    else
    begin
      Tmp.Width := Bmp.Width;
      Tmp.Height := Bmp.Height;
      XForm.eDx := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
      XForm.eDy := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
    end;
    SetGraphicsMode(Tmp.Canvas.Handle, GM_ADVANCED);
    SetWorldTransform(Tmp.Canvas.Handle, XForm);
    BitBlt(Tmp.Canvas.Handle, 0, 0, Tmp.Width, Tmp.Height, Bmp.Canvas.Handle,
      0, 0, SRCCOPY);
    Bmp.Assign(Tmp);
  finally
    Tmp.Free;
  end;
end;
Run Code Online (Sandbox Code Playgroud)

PlgBlt

所述PlgBlt函数执行从指定的矩形在源设备上下文在目标设备上下文一个位块传送到指定的平行四边形.通过lpPoint参数映射源图像的角点.

procedure RotateBitmap(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean;
  BkColor: TColor = clNone);
var
  C: Single;
  S: Single;
  Tmp: TBitmap;
  OffsetX: Single;
  OffsetY: Single;
  Points: array[0..2] of TPoint;
begin
  C := Cos(Rads);
  S := Sin(Rads);
  Tmp := TBitmap.Create;
  try
    Tmp.TransparentColor := Bmp.TransparentColor;
    Tmp.TransparentMode := Bmp.TransparentMode;
    Tmp.Transparent := Bmp.Transparent;
    Tmp.Canvas.Brush.Color := BkColor;
    if AdjustSize then
    begin
      Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
      Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
      OffsetX := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
      OffsetY := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
    end
    else
    begin
      Tmp.Width := Bmp.Width;
      Tmp.Height := Bmp.Height;
      OffsetX := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
      OffsetY := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
    end;
    Points[0].X := Round(OffsetX);
    Points[0].Y := Round(OffsetY);
    Points[1].X := Round(OffsetX + Bmp.Width * C);
    Points[1].Y := Round(OffsetY + Bmp.Width * S);
    Points[2].X := Round(OffsetX - Bmp.Height * S);
    Points[2].Y := Round(OffsetY + Bmp.Height * C);
    PlgBlt(Tmp.Canvas.Handle, Points, Bmp.Canvas.Handle, 0, 0, Bmp.Width,
      Bmp.Height, 0, 0, 0);
    Bmp.Assign(Tmp);
  finally
    Tmp.Free;
  end;
end;
Run Code Online (Sandbox Code Playgroud)

Graphics32

Graphics32是专为快速位图处理而设计的库.它需要一些经验才能充分发挥其潜力,但是文档以及提供的示例应该可以帮助您入门.

TBitmap32通过用许多可用转换类之一对其进行转换来完成图像的旋转.在TAffineTransformation这里需要的类.首先,将图像的一半大小移动到左上角,然后旋转,并将结果移回右下角,可能使用新的图像尺寸.

uses
  GR32, GR32_Transforms;

procedure RotateBitmap(Bmp: TBitmap32; Degs: Integer; AdjustSize: Boolean;
  BkColor: TColor = clNone; Transparent: Boolean = False); overload;
var
  Tmp: TBitmap32;
  Transformation: TAffineTransformation;
begin
  Tmp := TBitmap32.Create;
  Transformation := TAffineTransformation.Create;
  try
    Transformation.BeginUpdate;
    Transformation.SrcRect := FloatRect(0, 0, Bmp.Width, Bmp.Height);
    Transformation.Translate(-0.5 * Bmp.Width, -0.5 * Bmp.Height);
    Transformation.Rotate(0, 0, -Degs);
    if AdjustSize then
      with Transformation.GetTransformedBounds do
        Tmp.SetSize(Round(Right - Left), Round(Bottom - Top))
    else
      Tmp.SetSize(Bmp.Width, Bmp.Height);
    Transformation.Translate(0.5 * Tmp.Width, 0.5 * Tmp.Height);
    Transformation.EndUpdate;
    Tmp.Clear(Color32(BkColor));
    if not Transparent then
      Bmp.DrawMode := dmTransparent;
    Transform(Tmp, Bmp, Transformation);
    Bmp.Assign(Tmp);
    Bmp.OuterColor := Color32(BkColor);
    if Transparent then
      Bmp.DrawMode := dmTransparent;
  finally
    Transformation.Free;
    Tmp.Free;
  end;
end;

procedure RotateBitmap(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean;
  BkColor: TColor = clNone); overload;
var
  Tmp: TBitmap32;
  Transparent: Boolean;
begin
  Tmp := TBitmap32.Create;
  try
    Transparent := Bmp.Transparent;
    Tmp.Assign(Bmp);
    RotateBitmapGR32(Tmp, Degs, AdjustSize, BkColor, Transparent);
    Bmp.Assign(Tmp);
    if Transparent then
      Bmp.Transparent := True;
  finally
    Tmp.Free;
  end;
end;
Run Code Online (Sandbox Code Playgroud)

GDI +

在Windows XP中引入,Microsoft的GDI + API比默认的GDI API更有效.对于Delphi 2009及更高版本,可从此处获取该库.对于较旧的Delphi版本,可从此处获取该库.

在GDI +中,旋转也由变换矩阵完成.虽然绘图工作方式完全不同 创建一个TGPGraphics对象并使用其构造函数将其附加到设备上下文.随后,API将转换对象的绘制操作,并将其输出到目标上下文.

uses
  GDIPOBJ, GDIPAPI; // < D2009
  GdiPlus;          // >= D2009

procedure RotateBitmap(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean;
  BkColor: TColor = clNone);
var
  Tmp: TGPBitmap;
  Matrix: TGPMatrix;
  C: Single;
  S: Single;
  NewSize: TSize;
  Graphs: TGPGraphics;
  P: TGPPointF;
begin
  Tmp := TGPBitmap.Create(Bmp.Handle, Bmp.Palette);
  Matrix := TGPMatrix.Create;
  try
    Matrix.RotateAt(Degs, MakePoint(0.5 * Bmp.Width, 0.5 * Bmp.Height));
    if AdjustSize then
    begin
      C := Cos(DegToRad(Degs));
      S := Sin(DegToRad(Degs));
      NewSize.cx := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
      NewSize.cy := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
      Bmp.Width := NewSize.cx;
      Bmp.Height := NewSize.cy;
    end;
    Graphs := TGPGraphics.Create(Bmp.Canvas.Handle);
    try
      Graphs.Clear(ColorRefToARGB(ColorToRGB(BkColor)));
      Graphs.SetTransform(Matrix);
      Graphs.DrawImage(Tmp, (Cardinal(Bmp.Width) - Tmp.GetWidth) div 2,
        (Cardinal(Bmp.Height) - Tmp.GetHeight) div 2);
    finally
      Graphs.Free;
    end;
  finally
    Matrix.Free;
    Tmp.Free;
  end;
end;
Run Code Online (Sandbox Code Playgroud)

处理透明度

上面的例程保留了fead位图的透明设置,但Graphics32解决方案除外,它需要一个额外的Transparent参数.

性能和图像质量

我编写了一个测试应用程序(请参阅下面的完整代码)来调整各种方法的性能并比较生成的图像质量.

第一个也是最重要的结论是GDI +使用抗锯齿而其他不使用抗锯齿,从而获得最佳图像质量.(I不成功地设法防止反走样通过设置CompositingQuality,InterpolationMode,SmoothingMode,和PixelOffsetMode,所以当抗混叠是不优选的,不使用GDI +).

此外,到目前为止,GDI +解决方案也是最快的方法.

检测结果

unit RotateTestForm;

interface

uses
  Windows, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls,
  JPEG, Math, GR32, GR32_Transforms, GDIPOBJ, GDIPAPI {, GdiPlus};

type
  TTestForm = class(TForm)
  private
    FImage: TImage;
    FOpenDialog: TOpenDialog;
    procedure FormPaint(Sender: TObject);
  public
    constructor Create(AOwner: TComponent); override;
  end;

var
  TestForm: TTestForm;

implementation

{$R *.dfm}

procedure RotateBitmapSWT(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean;
  BkColor: TColor = clNone);
var
  C: Single;
  S: Single;
  XForm: TXForm;
  Tmp: TBitmap;
begin
  C := Cos(Rads);
  S := Sin(Rads);
  XForm.eM11 := C;
  XForm.eM12 := S;
  XForm.eM21 := -S;
  XForm.eM22 := C;
  Tmp := TBitmap.Create;
  try
    Tmp.TransparentColor := Bmp.TransparentColor;
    Tmp.TransparentMode := Bmp.TransparentMode;
    Tmp.Transparent := Bmp.Transparent;
    Tmp.Canvas.Brush.Color := BkColor;
    if AdjustSize then
    begin
      Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
      Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
      XForm.eDx := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
      XForm.eDy := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
    end
    else
    begin
      Tmp.Width := Bmp.Width;
      Tmp.Height := Bmp.Height;
      XForm.eDx := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
      XForm.eDy := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
    end;
    SetGraphicsMode(Tmp.Canvas.Handle, GM_ADVANCED);
    SetWorldTransform(Tmp.Canvas.Handle, XForm);
    BitBlt(Tmp.Canvas.Handle, 0, 0, Tmp.Width, Tmp.Height, Bmp.Canvas.Handle,
      0, 0, SRCCOPY);
    Bmp.Assign(Tmp);
  finally
    Tmp.Free;
  end;
end;

procedure RotateBitmapPLG(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean;
  BkColor: TColor = clNone);
var
  C: Single;
  S: Single;
  Tmp: TBitmap;
  OffsetX: Single;
  OffsetY: Single;
  Points: array[0..2] of TPoint;
begin
  C := Cos(Rads);
  S := Sin(Rads);
  Tmp := TBitmap.Create;
  try
    Tmp.TransparentColor := Bmp.TransparentColor;
    Tmp.TransparentMode := Bmp.TransparentMode;
    Tmp.Transparent := Bmp.Transparent;
    Tmp.Canvas.Brush.Color := BkColor;
    if AdjustSize then
    begin
      Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
      Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
      OffsetX := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
      OffsetY := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
    end
    else
    begin
      Tmp.Width := Bmp.Width;
      Tmp.Height := Bmp.Height;
      OffsetX := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
      OffsetY := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
    end;
    Points[0].X := Round(OffsetX);
    Points[0].Y := Round(OffsetY);
    Points[1].X := Round(OffsetX + Bmp.Width * C);
    Points[1].Y := Round(OffsetY + Bmp.Width * S);
    Points[2].X := Round(OffsetX - Bmp.Height * S);
    Points[2].Y := Round(OffsetY + Bmp.Height * C);
    PlgBlt(Tmp.Canvas.Handle, Points, Bmp.Canvas.Handle, 0, 0, Bmp.Width,
      Bmp.Height, 0, 0, 0);
    Bmp.Assign(Tmp);
  finally
    Tmp.Free;
  end;
end;

procedure RotateBitmapGR32(Bmp: TBitmap32; Degs: Integer; AdjustSize: Boolean;
  BkColor: TColor = clNone; Transparent: Boolean = False); overload;
var
  Tmp: TBitmap32;
  Transformation: TAffineTransformation;
begin
  Tmp := TBitmap32.Create;
  Transformation := TAffineTransformation.Create;
  try
    Transformation.BeginUpdate;
    Transformation.SrcRect := FloatRect(0, 0, Bmp.Width, Bmp.Height);
    Transformation.Translate(-0.5 * Bmp.Width, -0.5 * Bmp.Height);
    Transformation.Rotate(0, 0, -Degs);
    if AdjustSize then
      with Transformation.GetTransformedBounds do
        Tmp.SetSize(Round(Right - Left), Round(Bottom - Top))
    else
      Tmp.SetSize(Bmp.Width, Bmp.Height);
    Transformation.Translate(0.5 * Tmp.Width, 0.5 * Tmp.Height);
    Transformation.EndUpdate;
    Tmp.Clear(Color32(BkColor));
    if not Transparent then
      Bmp.DrawMode := dmTransparent;
    Transform(Tmp, Bmp, Transformation);
    Bmp.Assign(Tmp);
    Bmp.OuterColor := Color32(BkColor);
    if Transparent then
      Bmp.DrawMode := dmTransparent;
  finally
    Transformation.Free;
    Tmp.Free;
  end;
end;

procedure RotateBitmapGR32(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean;
  BkColor: TColor = clNone); overload;
var
  Tmp: TBitmap32;
  Transparent: Boolean;
begin
  Tmp := TBitmap32.Create;
  try
    Transparent := Bmp.Transparent;
    Tmp.Assign(Bmp);
    RotateBitmapGR32(Tmp, Degs, AdjustSize, BkColor, Transparent);
    Bmp.Assign(Tmp);
    if Transparent then
      Bmp.Transparent := True;
  finally
    Tmp.Free;
  end;
end;

procedure RotateBitmapGDIP(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean;
  BkColor: TColor = clNone);
var
  Tmp: TGPBitmap;
  Matrix: TGPMatrix;
  C: Single;
  S: Single;
  NewSize: TSize;
  Graphs: TGPGraphics;
  P: TGPPointF;
begin
  Tmp := TGPBitmap.Create(Bmp.Handle, Bmp.Palette);
  Matrix := TGPMatrix.Create;
  try
    Matrix.RotateAt(Degs, MakePoint(0.5 * Bmp.Width, 0.5 * Bmp.Height));
    if AdjustSize then
    begin
      C := Cos(DegToRad(Degs));
      S := Sin(DegToRad(Degs));
      NewSize.cx := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
      NewSize.cy := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
      Bmp.Width := NewSize.cx;
      Bmp.Height := NewSize.cy;
    end;
    Graphs := TGPGraphics.Create(Bmp.Canvas.Handle);
    try
      Graphs.Clear(ColorRefToARGB(ColorToRGB(BkColor)));
      Graphs.SetTransform(Matrix);
      Graphs.DrawImage(Tmp, (Cardinal(Bmp.Width) - Tmp.GetWidth) div 2,
        (Cardinal(Bmp.Height) - Tmp.GetHeight) div 2);
    finally
      Graphs.Free;
    end;
  finally
    Matrix.Free;
    Tmp.Free;
  end;
end;

{ TTestForm }

constructor TTestForm.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Font.Name := 'Tahoma';
  Top := 0;
  ClientWidth := 560;
  ClientHeight := 915;
  Show;
  FImage := TImage.Create(Self);
  FOpenDialog := TOpenDialog.Create(Self);
  FOpenDialog.Title := 'Select an small sized image (min. 100 x 100)';
  FOpenDialog.Options := FOpenDialog.Options + [ofFileMustExist];
  FOpenDialog.Filter := 'JPEG|*.JPG|BMP|*.BMP';
  if FOpenDialog.Execute then
  begin
    FImage.Picture.LoadFromFile(FOpenDialog.FileName);
    OnPaint := FormPaint;
    Invalidate;
  end
  else
    Application.Terminate;
end;

procedure TTestForm.FormPaint(Sender: TObject);
var
  Img: TBitmap;
  Bmp: TBitmap;
  Bmp32: TBitmap32;
  BkColor: TColor;
  AdjustSize: Boolean;
  Degs: Integer;
  Rads: Single;
  RotCount: Integer;
  I: Integer;
  Tick: Cardinal;
begin
  Img := TBitmap.Create;
  Bmp := TBitmap.Create;
  Bmp32 := TBitmap32.Create;
  try
    BkColor := clBtnFace;
    Img.Canvas.Brush.Color := BkColor;
    Img.Width := 100;
    Img.Height := 100;
    Img.Canvas.Draw(0, 0, FImage.Picture.Graphic);
    AdjustSize := False;
    Degs := 45;
    Rads := DegToRad(Degs);
    RotCount := 1000;

    Canvas.TextOut(10, 10, 'Original:');
    Canvas.Draw(10, 30, Img);
    Canvas.TextOut(10, 140, Format('Size = %d x %d', [Img.Width, Img.Height]));
    Canvas.TextOut(10, 160, Format('Angle = %d°', [Degs]));
    Canvas.TextOut(10, 250, Format('%d rotations:', [RotCount]));

    Canvas.TextOut(120, 10, 'SetWorldTransform:');
    Bmp.Assign(Img);
    RotateBitmapSWT(Bmp, Rads, AdjustSize, BkColor);
    Canvas.Draw(120, 30, Bmp);
    if not AdjustSize then
    begin
      Tick := GetTickCount;
      for I := 0 to RotCount - 2 do
        RotateBitmapSWT(Bmp, Rads, AdjustSize, BkColor);
      Canvas.TextOut(120, 250, Format('%d msec', [GetTickCount - Tick]));
      Canvas.Draw(120, 140, Bmp);
    end;

    Canvas.TextOut(230, 10, 'PlgBlt:');
    Bmp.Assign(Img);
    RotateBitmapPLG(Bmp, Rads, AdjustSize, BkColor);
    Canvas.Draw(230, 30, Bmp);
    if not AdjustSize then
    begin
      Tick := GetTickCount;
      for I := 0 to RotCount - 2 do
        RotateBitmapPLG(Bmp, Rads, AdjustSize, BkColor);
      Canvas.TextOut(230, 250, Format('%d msec', [GetTickCount - Tick]));
      Canvas.Draw(230, 140, Bmp);
    end;

    Canvas.TextOut(340, 10, 'Graphics32:');
    Bmp.Assign(Img);
    RotateBitmapGR32(Bmp, Degs, AdjustSize, BkColor);
    Canvas.Draw(340, 30, Bmp);
    if not AdjustSize then
    begin
      Tick := GetTickCount;
      for I := 0 to RotCount - 2 do
        RotateBitmapGR32(Bmp, Degs, AdjustSize, BkColor);
      Canvas.TextOut(340, 250, Format('%d msec', [GetTickCount - Tick]));
      Canvas.Draw(340, 140, Bmp);

      // Without in between conversion to TBitmap:
      Bmp32.Assign(Img);
      Tick := GetTickCount;
      for I := 0 to RotCount - 1 do
        RotateBitmapGR32(Bmp32, Degs, AdjustSize, BkColor, False);
      Canvas.TextOut(340, 270, Format('%d msec (optimized)',
        [GetTickCount - Tick]));
    end;

    Canvas.TextOut(450, 10, 'GDI+ :');
    Bmp.Assign(Img);
    RotateBitmapGDIP(Bmp, Degs, AdjustSize, BkColor);
    Canvas.Draw(450, 30, Bmp);
    if not AdjustSize then
    begin
      Tick := GetTickCount;
      for I := 0 to RotCount - 2 do
        RotateBitmapGDIP(Bmp, Degs, AdjustSize, BkColor);
      Canvas.TextOut(450, 250, Format('%d msec', [GetTickCount - Tick]));
      Canvas.Draw(450, 140, Bmp);
    end;
  finally
    Bmp32.Free;
    Bmp.Free;
    Img.Free;
    OnPaint := nil;
  end;
end;

end.
Run Code Online (Sandbox Code Playgroud)