Jak obrócić bitmapę o dowolny kąt

Szczawik

Tekst ten jest poprawioną kopią porady, która zniknęła z serwisu w niewyjaśnionych okoliczonściach. Został odtworzony na podstawie zasobów zindeksowanych przez www.google.pl. Nie zawiera nowych treści, a jedynie pewne dodatki wynikające z komentarzy do starej wersji.

Wstęp

Na internecie można znaleźć wiele funkcji do obracania - dzięki Delphi - obrazów o wielokrotność 90 stopni. Można znaleźć też kilka takich, które pozwalają na obrót o dowolny kąt - sam jakiś czas temu poszukiwałem takiej i choć znalazłem wiele, żadna ze znalezionych nie działała do końca prawidłowo lub nie była odpowiednio wydajna.

Kod

Poniższy przykład działa dla bitmap o trybie kolorów 24 bitowym, bo takich potrzebowałem. Łatwo przekształcić poniższy kod dla innych formatów, ale wykorzystam ten - jeden z popularniejszych.

type
TTriple = record
  B,G,R:byte;
end;

TTripleArray = array[WORD] of TTriple; //jakakolwiek duża liczba
PTripleArray = ^TTripleArray;

procedure bitmapRotate(source:TBitmap; destination:TBitmap; angle:double);
var xdh, ydh, xsh, ysh :extended;
    xd, yd, xs, ys:integer;
    dxs, dys:extended;
    ix, iy: extended;
    sw, sh:integer;
    angle0:extended;
    r:extended;
    P:PTripleArray;
    D:array of PTripleArray;
    PI2:extended;
    i:integer;
begin
if (source.PixelFormat<>pf24bit) or (destination.PixelFormat<>pf24bit) then exit;
xdh:=destination.Width/2;
ydh:=destination.Height/2;
sw:=source.Width;
sh:=source.Height;
xsh:=sw/2;
ysh:=sh/2;
PI2:=PI*2;
SetLength(D, source.Height);
for i:=0 to source.Height-1 do
  D[i]:=source.ScanLine[i];

for yd:=destination.Height-1 downto 0 do
  begin
  P:=destination.ScanLine[yd];
  for xd:=destination.Width-1 downto 0 do
    begin
    ix:=xdh-xd;
    iy:=ydh-yd;
    r:=sqrt( sqr(ix)+sqr(iy) );
    if r=0 then r:=0.0000001;
    if arcsin( ix/r )<0 then
      angle0:=PI2-arccos( iy/r )-angle
    else
      angle0:=arccos( iy/r )-angle;
    sincos(angle0, dys, dxs);
    xs:=round(r*dxs+xsh);
    ys:=round(r*dys+ysh);
    if (xs or ys>=0) and (xs<sw) and (ys<sh) then
      P[xd]:=D[ys][xs];
    end;
  end;

end;

Najpierw trochę teorii

Bitmapa jest obracana względem swojego środka. Dla każdego piksela obrazu wynikowego wyliczany jest piksel obrazu źródłowego. Wyliczany w sposób taki: zamieniamy współrzędne kartezjańskie [względem obrazka] kolejnych pikseli wyjściowych na współrzędne biegunowe [względem środka], następnie odejmujemy (!) kąt i znów zamieniamy na kartezjańskie [względem obrazka], by poznać jakiemu pikselowi źródłowemu odpowiadają poszczególne punkty. Jeśli te piksele źródłowe mieszczą swe współrzędne w obrazie źródłowym, z niego pobieramy kolor, w przeciwnym wypadku nic nie robimy (możemy ewentualnie zamalować jakimś konkretnym kolorem tło).

Przy zamianie środkowych pikseli obrazu warto zadbać, by nie dopuścić do dzielenia przez zero, np.: przez wprowadzenie dzielenia przez bardzo małą liczbę (ale to jednak nie 0!).

Idea jest prosta, ale warto policzyć, ile razy taki kod w pętli jest wykonywany dla np.: obrazka 256x256 pikseli (2^16 razy). Jeśli pomnożymy to razy np.: 16 klatek na sekundę (a nie jest to dużo), to mamy ponad milion przebiegów pętli dla obrotów jednego obrazka - trochę dużo.

Co można przyspieszyć?

Tu jest duże pole do popisu:

  1. Jeśli jakieś działanie jest wykonane w pętli ponad raz, wyliczmy tylko raz i podstawmy do zmiennej, by jej potem używać.
  2. Korzystajmy ze ScanLine, bo jest znacznie szybsze niż robienie przypisań do i odczytów z Canvas.Pixels[x,y].
  3. Stwórzmy na początku tablicę adresów (ScanLine) wszystkich linii obrazu źródłowego, by dla pikseli znajdujących się w jednej linii nie wykonywać funkcji ScanLine kilka razy.
  4. Ograniczmy działania do minimum [tutaj: (xs or ys>=0) zamiast (xs>=0) and (ys>=0)]
  5. Jak coś możemy policzyć przed pętlą - zróbmy to (choćby i wyliczenia 2*PI)
  6. Posłużmy się funkcją SinCos, gdy musimy obliczyć i sinus, i cosinus jednego kąta - jest szybsze [tak przynajmniej twierdzi help do Delphi]

Uwagi

Pomimo użycia typu Extended, kąt wprowadza błąd obliczeniowy, a więc zmniejszenie jego dokładności prowadziłoby do znacznej degradacji obrazu.

Zdefiniowany typ TTripleArray wymaga dla swoich zmiennych bardzo dużo pamięci. Ale nie jest on alokowany! Tak na prawdę wykorzystywany jest tylko wskaźnik na zmienną tego typu, a jego rozmiar zapewnia uniknięcie błędów przekroczenia zakresu tablicy.

Należy pamiętać, że, dla poprawnego użycia powyższego kodu, bitmapy źródłowa i docelowa muszą być w formacie pf24bit, ale po utworzeniu dynamicznie bitmapy ma ona domyślnie format pfDevice.

4 komentarzy

Obraz z wejsciowej bitmapy source jest w obroconej formie rysowany na bitmapie destination; funkcja nie modyfikuje bitmapy wejsciowej.

Jednego nie rozumiem - co oznacza parametr destination? Rozumiem, że procedura pobiera bitmapę źródłową i kąt o jaki ma obrócić, ale obraz wynikowy to chyba końcowy efekt działania tej procedury. Poprawcie mnie jeśli się mylę.

Przydatna rzecz. Na pewno wykorzystam.

Może się kiedyś przydać :)