delphi - vectorial - modo de mapa de bits
Girar mapa de bits por ángulo real (2)
Érase una vez, al leer esta pregunta , me pregunté cómo rotar un mapa de bits en cualquier grado sin tener que jugar con todos los bits. Recientemente, otra persona también tuvo dificultades obvias.
Ya hay muchas preguntas relacionadas con la rotación a intervalos de 90 °, sobre todo esta , pero quiero rotar en un ángulo real. Preferiblemente con la posibilidad de ajustar el tamaño de la imagen debido a la rotación, y con la configuración de un color de fondo personalizado (transparente) para las partes que se agregarán a la superficie de la imagen. Entonces supongo que la firma de la rutina sería algo así como:
procedure RotateBitmap(Bmp: TBitmap; Angle: Single; AdjustSize: Boolean;
BackColor: TColor);
Estas respuestas mencionan los siguientes candidatos para construir esta rutina: SetWorldTransform, PlgBlt, GDI +, pero me gustaría ver una implementación (eficiente).
tl; dr; Use GDI +
SetWorldTransform
Con SetWorldTransform de SetWorldTransform puede transformar el espacio del contexto del dispositivo: rotar, cizallar, desplazar y escalar. Esto se hace configurando los miembros de una matriz de transformación de tipo XFORM. Complete sus miembros de acuerdo con la documentación .
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;
PlgBlt
La función PlgBlt realiza una transferencia de bloques de bits desde el rectángulo especificado en el contexto del dispositivo fuente al paralelogramo especificado en el contexto del dispositivo de destino. Asigne los puntos de esquina de la imagen de origen a través del parámetro 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;
Graphics32
Graphics32 es una biblioteca especialmente diseñada para el manejo rápido de mapas de bits. Requiere algo de experiencia para aprovechar todo su potencial, pero la documentación , así como los ejemplos proporcionados, deberían ayudarlo a comenzar.
La rotación de una imagen TBitmap32
se realiza transformándola en una de las muchas clases de transformación disponibles. La clase TAffineTransformation
es necesaria aquí. Primero, desplace la imagen a la mitad de su tamaño hacia la esquina superior izquierda, luego gírela y desplace el resultado nuevamente hacia la esquina inferior derecha, posiblemente utilizando las nuevas dimensiones de la imagen.
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;
GDI +
Introducido en Windows XP, la API GDI+ Microsoft es más eficiente que la API GDI predeterminada. Para Delphi 2009 en adelante, la biblioteca está disponible desde aquí . Para versiones anteriores de Delphi, la biblioteca está disponible desde aquí .
En GDI +, la rotación también se realiza mediante una matriz de transformación. El dibujo funciona de manera bastante diferente. Cree un objeto TGPGraphics
y adjúntelo a un contexto de dispositivo con su constructor. Posteriormente, las operaciones de dibujo en el objeto son traducidas por la API y se enviarán al contexto de destino.
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;
Manejo de transparencia
Las rutinas anteriores conservan las configuraciones transparentes del mapa de bits fead, con la excepción de la solución Graphics32 que requiere un parámetro Transparent
adicional.
Rendimiento y calidad de imagen
Escribí una aplicación de prueba (vea el código completo a continuación) para ajustar el rendimiento de los diversos métodos y para comparar la calidad de imagen resultante.
La primera y más importante conclusión es que GDI + usa anti-aliasing cuando los otros no, lo que resulta en la mejor calidad de imagen. (Sin éxito, traté de evitar el anti-aliasing configurando CompositingQuality
, InterpolationMode
, SmoothingMode
, y PixelOffsetMode
, por lo que cuando no se prefiere el anti-aliasing, no use GDI +.)
Además, la solución GDI + es también el método más rápido, de lejos.
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.
Si alguien está mirando la rotación de imágenes, también puede echar un vistazo a la biblioteca de videos de Mitov (gratis para uso no comercial: link ). VCL y FireMonkey. Cuida todos los detalles de bajo nivel, lo que nos permite evitar el tipo de codificación detallada que explora la excelente respuesta de NGLN.
Lo hemos usado durante los últimos dos años y hemos estado muy contentos con él en nuestra aplicación comercial.
Tiene un componente de rotación que funciona con imágenes estáticas y secuencias de video. Su biblioteca es totalmente multi-tarea, opcionalmente utilizando todos los núcleos y primitivas de bajo nivel disponibles, en los conjuntos de chips Intel con la biblioteca de rendimiento propia de Intel ( http://software.intel.com/en-us/articles/intel-ipp )
En hardware moderado, podemos ejecutar múltiples flujos de video o bmp que rotamos, recortamos, escalamos y procesamos a nivel de píxel, en tiempo real.