3 Вращение изображений в Borland C++ Builder

Обычно можно встретить следующие этапы рассмотрения вопросов, связанных с вращением изображений:

  • Отражение слева направо;

  • Отражение сверху вниз;

  • Поверот на угол 90 градусов;

  • Поверот на угол 180 градусов;

  • Поверот на угол 270 градусов;

  • Поверот на произвольный угол.

За таким подходом, естественно, теряются особенности механизмов выполнения поворота изображений и, в тоже время, нельзя отказываться и от этой методики, так как порой программиста интересует как повернуть, а не то, как это сделать быстрее или проще.

В данной главе делается попытка рассмотреть некоторые отправные моменты поворота изображений с тем, чтобы коды, приводимые далее, были прозрачны для тех, кто будет их использовать.

В силу данного подхода первоначально рассматриваются некоторые основы поворота изображений и, далее, приводятся коды, позволяющие выполнить требуемый поворот изображений.

В начало

Возможные способы поверота изображения

Повороты изменением положения пикселей

Как отмечалось, многие компоненты в Borland C++ Builder имеют свойство Canvas (канва, холст), представляющие собой прямоугольную область (именно прямоугольную) компонента, на которой можно рисовать или отображать готовые изображения.

Каждая точка канвы имеет координаты X и Y (соответствующие координатам прямоугольника, но в отличии от геометрического прямоугольника, координаты по оси Y начинаются с левого верхнего угла). Пиксель - это наименьший элемент поверхности рисунка, соответствующий цвету отображаемой точки на экране монитора. Таким образом - пиксель это цвет точки с координатами X и Y внутри прямоугольника.

Первый подход к повороту изображения заключается в том, что для поворота изображения необходимо поменять местами пиксели в соответствии с направлением поворота.

Для доступа к пикселям канва имеет свойство Pixels, определенное как:

__property TColor Pixels[int X][int Y] 
            = {read=GetPixel, write=SetPixel};

Иными словами, пиксели можно читать и писать и менять местами, а значит и вращать изображение:

//Прочитать цвет пикселя
TColor tColor=Canvas->Pixels[10][10];
//Если канва красного цвета, то i=255
int i=(int)tColor;
//Записать цвет пикселя c координатами
//10*10 на место 5*5 
Canvas->Pixels[5][5]=tColor;

Этот подход наиболее прозрачен, но является самым медленным.

Использование метода канвы CopyRect()

При некоторых ухищрениях метод CopyRect() также применим для поворота изображений.

Следующий код дает представление о том как заставить CopyRect() копировать прямоугольник в перевернутом слева направо виде (в коде Image1 - компонент TImage).

Graphics::TBitmap *pBitmap = new Graphics::TBitmap();
pBitmap->LoadFromFile("a.bmp");
Image1->Left=0;
Image1->Top=0;
Image1->Width=pBitmap->Width;
Image1->Height=pBitmap->Height;
for(int j=0; j < pBitmap->Height; j++)
{
 Image1->Canvas->CopyRect(Rect(0,j,pBitmap->Width,j+1),
  pBitmap->Canvas,Rect(pBitmap->Width-1,j,-1,j+1));
}
delete pBitmap;

Можно повернуть изображение и сверху вниз и весь прямоугольник в целом на 180 градусов и, причем, практически мгновенно для достаточно больших изображений, но повороты на 90 градусов (и тем более на произвольный угол) по скорости сравнимы с методом замены местами пикселей (примеры приведены ниже).

Использование метода Scanline[]

Другой подход заключается в получении доступа к массиву байт, образующих изображение. Эту возможность дает функция Scanline[], которая обеспечивает индексированный доступ к каждой линии pixels.

__property void * ScanLine[int Row] = {read=GetScanline};

Растровая строка используется только с DIB форматом.

ScanLine[] возвращает указатель на целую строку пикселей. Но элементы строки, на которую указывает ScanLine[] это не цвет пикселей, а реальные единицы формата изображения (биты), соответствующие цветности картинки (от одного бита для чёрно-белого изображения до 4 байт для формата 2*32 цветов. Поэтому, здесь возникают трудности, вызванные необходимостью знания формата для того, чтобы правильно выбрать границу пикселя в массиве байт.

Для 256 цветного изображения поворот не вызывает трудностей - один пиксель это один байт - и поменять байты местами (например слева направо) можно достаточно просто:

Byte  *ptr;
Byte   vbBt1'
Graphics::TBitmap *pBitmap = new Graphics::TBitmap();
pBitmap->LoadFromFile("a.bmp");
for (int y = 0; y < pBitmap->Height; y++)
{
 //Получаем указатель на строку
 ptr = (Byte *)pBitmap->ScanLine[y];
 for(int x = 0; x < pBitmap->Width/2; x++)
 {
  vbBt1 = ptr[x];
  ptr[x]=ptr[pBitmap->Width-1-x];
  ptr[pBitmap->Width-1-x]=vbBt1;
 }
}
delete pBitmap;

Или так (уточнение прислал Михаил Ершов):

Image1->Picture->Bitmap->Width =pBitmap->Width;
Image1->Picture->Bitmap->Height =pBitmap->Height;

Любой другой формат требует дополнительных ухищрений для даже такого простого поворота, о чем более подробно в примерах, приводимых ниже.

И, в тоже время, это достаточно быстрый способ, который даже применим в совокупности с доступам к пикселям канвы и для поворота изображения на 90 градусов и на произвольный угол.

Использование функции SetWorldTransform

Функции SetWorldTransform, GetWorldTransform, ModifyWorldTransform также применимы для поворота изображений, но как написано в Help - работают они только в Windows NT. Работают они и в Windows 2000 и в любой другой OS этой линейки. Но в программах придется предусмотреть либо проверку с какой ОС работает программа, либо найти способ как достичь тогоже эффекта с использованием более простых подходов.

Функция SetWorldTransform() выполняет различные двумерные преобразования изображения (масштабирование, вращение, изменение размера, искажения).

Функция определена как:

BOOL SetWorldTransform(HDC hdc,  // handle контнкста приемника
     CONST       XFORM *lpXform  // структура, определяющая тип и 
                                 // параметры преобразования 
 );	

В приводимом ниже примере, полностью повторенным (за исключением некоторых уточнений о чем ниже) из статьи Michel Leunen (перевод Тараса Сорока) код, позволяющий выполнять любые повороты и добавлен метод поворота, который незначительно уступает повороту с использованием функции SetWorldTransform(), но работает на всех линейках Windows.

В начало

Повороты изображений методом изменения положений пикселей

Функция vRotateImage() позволяет выполнить все повороты кратные 90 градусов, а также отображения рисунка слева направо и сверху вниз в зависимости от передоваемого ей параметра "i", значение которого определяет направление поворота:

  • i == 0 - отображение слева направо;

  • i == 1 - отображение сверху вниз;

  • i == 2 - поворот на 90 градусов;

  • i == 3 - поворот на 180 градусов;

  • i == 4 - поворот на 270 градусов;

В примере компоненты TImage (Image1) и TImage (Image2) предназначены только для наглядности демонстрации поворотов (ранее уже обсуждалось как отрисовать или сохранить изображение, поэтому помним, что повернув изображение, можно далее сделать с ним все что угодно).

void __fastcall
TForm1::vRotateImage(int i)
{
 Graphics::TBitmap *pBitmap1;
 Graphics::TBitmap *pBitmap2;
 pBitmap1 = new Graphics::TBitmap;
 pBitmap2 = new Graphics::TBitmap;
 //Загружаем изображение
 pBitmap1->LoadFromFile("1.bmp");
 //Определяем размеры для отображения 
 //исходного  рисунка
 Image1->Left=0;
 Image1->Top=0;
 Image1->Width=pBitmap1->Width;
 Image1->Height=pBitmap1->Height;
 //Прказываем загруженное изображение
 Image1->Picture->Assign(pBitmap1);
 //Определяем размеры внеэкранного образа для 
 //повернутого изоьражения
 switch(i)
 {
  case 0:
  case 1:
  case 3:
   pBitmap2->Width  = pBitmap1->Width;
   pBitmap2->Height = pBitmap1->Height;
  break;
  case 2:
  case 4:
   pBitmap2->Width  = pBitmap1->Height;
   pBitmap2->Height = pBitmap1->Width;
  break;
 }
 //Меняем пиксели местами
 for(int y=pBitmap2->Height; y >= 0; y--)
  for(int x=0; x <= pBitmap2->Width; x++)
 {
  switch(i)
  {
   //слева направо
   case 0:
    pBitmap2->Canvas->Pixels[x][y]
      = pBitmap1->Canvas->Pixels[pBitmap1->Width-x-1][y];
   break;
   //сверху вниз  
   case 1:
    pBitmap2->Canvas->Pixels[x][y]
     = pBitmap1->Canvas->Pixels[x][pBitmap1->Height-y-1];
   break;
   //90 градусов   
   case 2:
    pBitmap2->Canvas->Pixels[x][y]
     = pBitmap1->Canvas->Pixels[y][pBitmap1->Height-x-1];
   break;
   //180 градусов  
   case 3:
    pBitmap2->Canvas->Pixels[x][y]
     = pBitmap1->Canvas->
        Pixels[pBitmap1->Width-x-1][pBitmap1->Height-y-1];
   break;
   //270 градусов;  
   case 4:
    pBitmap2->Canvas->Pixels[x][y]
     = pBitmap1->Canvas->Pixels[pBitmap1->Width-y-1][x];
   break;
  }
 }
 //Определяем размеры для отображения 
 //повернутого рисунка
 Image2->Left=Image1->Width+10;
 Image2->Top=0;
 Image2->Width=pBitmap2->Width;
 Image2->Height=pBitmap2->Height;
 //Отображаем повернутое изображение
 Image2->Picture->Assign(pBitmap2);
 delete pBitmap1;
 delete pBitmap2;
}

В отличии от приведенного выше примера (в первом параграфе), так как при поворотах используются два внеэкранных образа, нет необходимости в определении дополнительной переменной, что несколько экономит время. Однако, как уже отмечалось, это один из самых медленных способов поворота и его можно рекомендовать только для поворотов небольших по размеру изображений.

Что касается поворотов на произвольный угол, то, так как этот метод требует применения нескольких школьных математических методов (которые большинством из нас, пишущих прикладные программы, давно забыты), им будет посвящен оидельный параграф.

В начало

Использование для поворота изображения метода канвы CopyRect()

Приводимая ниже функция vRotateImage() позволяет выполнить все повороты кратные 90 градусов, а также отображения рисунка слева направо и сверху вниз в зависимости от передоваемого ей параметра "i", значение которого, как и выше, определяет направление поворота:

  • i == 0 - отображение слева направо;

  • i == 1 - отображение сверху вниз;

  • i == 2 - поворот на 90 градусов;

  • i == 3 - поворот на 180 градусов;

  • i == 4 - поворот на 270 градусов;

Как и ранее в вышеприведенном примере компоненты TImage (Image1) и TImage (Image2) предназначены только для наглядности демонстрации поворотов.

void __fastcall
TForm1::vRotateImage(int i)
{
 Graphics::TBitmap *pBitmap1 = new Graphics::TBitmap();
 pBitmap1->LoadFromFile("1.bmp");
 //Определяем размеры для отображения
 //исходного  рисунка
 Image1->Left=0;
 Image1->Top=0;
 Image1->Width=pBitmap1->Width;
 Image1->Height=pBitmap1->Height;
 //Прказываем загруженное изображение
 Image1->Picture->Assign(pBitmap1);
 //Там где будем показывать повернутое изображение
 Image2->Left=Image1->Width+10;
 Image2->Top=0;
 //Определяем размеры для отображения
 //повернутого рисунка
 switch(i)
 {
  case 0:
  case 1:
  case 3:
   Image2->Width=pBitmap1->Width;
   Image2->Height=pBitmap1->Height;
  break;
  case 2:
  case 4:
   Image2->Width  = pBitmap1->Height;
   Image2->Height = pBitmap1->Width;
  break;
 }
 switch(i)
 {
  //Отображение слева направо
  case 0:
   //Можно так по кубикам пикселям
   /*
   for(int j=0; j < pBitmap1->Height; j++)
   for(int i=0; i < pBitmap1->Width; i++)
   {
    Image2->Canvas->CopyRect(Rect(i,j,i+1,j+1),
     pBitmap1->Canvas,Rect
      (pBitmap1->Width-i-1,j,pBitmap1->Width-i,j+1));
   }
   */
   //Так быстрее по строчкам
   for(int j=0; j < pBitmap1->Height; j++)
   {
    Image2->Canvas->CopyRect(Rect(0,j,pBitmap1->Width,j+1),
     pBitmap1->Canvas,Rect(pBitmap1->Width-1,j,-1,j+1));
   }
  break;
  //Поворот сверху вниз
  case 1:
   for(int i=0; i < pBitmap1->Width; i++)
   {
    Image2->Canvas->CopyRect(Rect(i,0,i+1,pBitmap1->Height),
    pBitmap1->Canvas,Rect(i,pBitmap1->Height-1,i+1,-1));
   }
  break;
  //Поворот на 90 градусов
  case 2:
   //Медленно по кубикам пикселям и по другому нельзя
   for(int i=0; i < pBitmap1->Width; i++)
   {
    for(int j=0; j < pBitmap1->Height; j++)
   {
    Image2->Canvas->CopyRect(
     Rect(Image2->Width-j-1,i,Image2->Width-j+1-1,1+i),
      pBitmap1->Canvas,Rect(i,j,i+1,j+1));
   }
  }
  break;
  //Поворот на 180 градусов
  case 3:
   Image2->Canvas->CopyRect(
        Rect(-1,-1,pBitmap1->Width,pBitmap1->Height),
         pBitmap1->Canvas,Rect(
          pBitmap1->Width,pBitmap1->Height,-1,-1));
  break;
  case 4:
   //Сначала на 180 градусов, затем еще на 90 градусов
   Image1->Canvas->CopyRect(Rect
       (-1,-1,pBitmap1->Width,pBitmap1->Height),
        pBitmap1->Canvas,
         Rect(pBitmap1->Width,pBitmap1->Height,-1,-1));
   //Теперь еще на 90 градусов
   for(int i=0; i < Image1->Width; i++)
   {
    for(int j=0; j < Image1->Height; j++)
    {
     Image2->Canvas->CopyRect(Rect
       (Image2->Width-j-1,i,Image2->Width-j+1-1,1+i),
        Image1->Canvas,Rect(i,j,i+1,j+1));
    }
   }
  break;
 }
 delete pBitmap1;
}

Реализация этого кода показывает два противоречия - практически "мгновенный" поворот на 180 градусов и отображения сверху вниз и слева направо и очень медленный поворот на 90 градусов.

Еще одно следствие - поворот на 270 градусов выполняется в два этапа (180+90), как это делается например в знаменитом Paint Windows. Это довольно распространенный метод, хотя в данном подходе и возможно выполнить поворот аналогичным повороту на 90 градусов.

Из предыдущего и данного параграфов следует, что камнем преткновения является именно поворот на 90 градусов, который следующие параграфы должны помоть перешагнуть.

В начало

Использование для выпополнения поворотов метода ScanLine[]

Как отмечено выше, метод ScanLine[] открывает доступ непосредственно к строке бит изображения за счет возвращения указателя на начало строки и для оперирования пикселями изображения, содержащихся в этой строке необходимо иметь представление о формате изображения (о том как биты строки образуют пиксель). Следующий пример показывает как можно быстро повернуть изображение слева направо в зависимости от числа цветов его образующего:

void __fastcall 
TForm1::Button1Click(TObject *Sender)
{
 //TBitmap для внеэкранного образа
 Graphics::TBitmap *pBitmap = new Graphics::TBitmap();
 //Указатель на массив байт для Scanlene
 Byte  *ptr;
 //Вспомогательные переменные для хранения байт при поворотах
 Byte  vbBt1,vbBt2,vbBt3,vbBt4;
 //Вспомогательная переменная для хранения пикселя
 TColor tColor;
 //Загружам изображение
 pBitmap->LoadFromFile("a.bmp");
 //Отрисовываем изображение для наглядности
 Canvas->Draw(0,0,pBitmap);
 //Для pf4bit в одном байте два пикселя, отсюда сложность в обмене
 //для нечетной длины строки 4 бит из последнего байта в первые 4
 //бита первого байта со сдвигом всей строки перед обменом на 4 бита
 //Легче убрать один пиксель ширины, что незаметно для большого рисунка
 //(у малых картинок, например иконок размер всегда четный и так)
 //чем написать лишних двадцать строк кода только для данного формата
 //В следующим за этим примером будет показано как этого можно не делать
 //здесь же этот способ приведен для показа единого подхода
 if(pBitmap->Width % 2 != 0 
    && pBitmap->PixelFormat == pf4bit)  pBitmap->Width-=1;
 for (int y = 0; y < pBitmap->Height; y++)
 {
  //Получаем указатель на строку
  ptr = (Byte *)pBitmap->ScanLine[y];
  //В зависимости от форматы меняем байты
  switch(pBitmap->PixelFormat)
  {
   //Самый простой способ  для 256 цветов (один пиксель - один байт)
   // он описан выыше
   case pf8bit:
    for(int x = 0; x < pBitmap->Width/2; x++)
    {
     vbBt1 = ptr[x];
     ptr[x]=ptr[pBitmap->Width-1-x];
     ptr[pBitmap->Width-1-x]=vbBt1;
    }
   break;  
   //True Color - три байта пиксель, плюс особенности заполнения
   //строки - дополнение всей строки до крата четырем байт - при
   //смене по з байта последний байт пустой останется на своем месте
   case pf24bit:
    for(int x = 0; x < pBitmap->Width/2; x++)
    {
     vbBt1 = ptr[3*x];
     vbBt2 = ptr[3*x+1];
     vbBt3 = ptr[3*x+2];
     ptr[3*x]=ptr[3*(pBitmap->Width-1)-3*x];
     ptr[3*x+1]=ptr[3*(pBitmap->Width-1)-3*x+1];
     ptr[3*x+2]=ptr[3*(pBitmap->Width-1)-3*x+2];
     ptr[3*(pBitmap->Width-1)-3*x]=vbBt1;
     ptr[3*(pBitmap->Width-1)-3*x+1]=vbBt2;
     ptr[3*(pBitmap->Width-1)-3*x+2]=vbBt3;
    }
   break;
   //2**32 цветов 4 байта один пиксель- строка
   //всегда заполнена информацией
   case pf32bit:
    for(int x = 0; x < pBitmap->Width/2; x++)
    {
     vbBt1 = ptr[4*x];         
     vbBt2 = ptr[4*x+1];
     vbBt3 = ptr[4*x+2];
     vbBt4 = ptr[4*x+3];
     ptr[4*x]=ptr[4*(pBitmap->Width-1)-4*x];
     ptr[4*x+1]=ptr[4*(pBitmap->Width-1)-4*x+1];
     ptr[4*x+2]=ptr[4*(pBitmap->Width-1)-4*x+2];
     ptr[4*x+3]=ptr[4*(pBitmap->Width-1)-4*x+3];
     ptr[4*(pBitmap->Width-1)-4*x]=vbBt1;
     ptr[4*(pBitmap->Width-1)-4*x+1]=vbBt2;
     ptr[4*(pBitmap->Width-1)-4*x+2]=vbBt3;
     ptr[4*(pBitmap->Width-1)-4*x+3]=vbBt4;
    }
   break;

   //  Сложности начинаются здесь

   //16 цветов, необходимо менять местами байты
   //но и биты внутри байта так как в байте 2 пикселя
   case pf4bit:
    for(int x = 0; x < pBitmap->Width/4-1; x++)
    {
     vbBt1 = ptr[x];
     vbBt2 = ptr[pBitmap->Width/2-1-x];
     //Поменять местами 4 бита
     vbBt1 = (vbBt1 << 4) | (vbBt1 >> 4);
     vbBt2 = (vbBt2 << 4) | (vbBt2 >> 4);
     ptr[x]=vbBt2;
     ptr[pBitmap->Width/2-1-x]=vbBt1;
    }
   break;

   //И совсем не подходит этот метод для двухцветного
   //рисунка - менять надо байты и все биты в байте
   //поэтому придется делать так как и раньше


   //Двухцветный рисунок
   case pf1bit:
    for(int x = 0; x < pBitmap->Width/2; x++)
    {
     tColor=(TColor)pBitmap->Canvas->Pixels[x][y];
     pBitmap->Canvas->Pixels[x][y] =
        pBitmap->Canvas->Pixels[pBitmap->Width-x-1][y];
     pBitmap->Canvas->Pixels[pBitmap->Width-x-1][y]=tColor;
    }
   break;
  }//switch(pBitmap->PixelFormat)
 }
 //Рисуем повернутое изображение
 Canvas->Draw(pBitmap->Width*2+10,0,pBitmap);
 delete pBitmap;
}

Сложности, на которые обращено внимание, не фатальны. Их легко обойти путем преобразования изображения к другому формату, выполнения действия и затем возврату к исходному формату. Лучше, конечно, выбрать одно преобразование к формату pf32bit, но если разработчика будут интересовать десятые доли секунд - при повороте одной картинки 1000*1000 пикселей, то можно выполнить преобразованя рисунков 256 и ниже цветов к pf8bit, а выше к pf32bit.

Следующий код демонстрирует как это выполнить для выполнения поворотов только с форматом pf32bit:

void __fastcall 
TForm1::Button1Click(TObject *Sender)
{
 //Объекты TBitmap для исходного изображения и повернутого
 Graphics::TBitmap *pBitmap = new Graphics::TBitmap();
 Graphics::TBitmap *pBitmap1 = new Graphics::TBitmap();
 //Указатель на массив байт для Scanlene
 Byte  *ptr;
 //Вспомогательные переменные для хранения байт при поворотах
 Byte  vbBt1,vbBt2,vbBt3,vbBt4;
 //Загружаем изображение
 pBitmap->LoadFromFile("a1.bmp");
 //Сохраняем рисунок и его параметры
 pBitmap1->Assign(pBitmap);
 //Рисуем для наглядности
 Canvas->Draw(0,0,pBitmap);
 //Преобразуем формат
 pBitmap->PixelFormat=pf32bit;
 for (int y = 0; y < pBitmap->Height; y++)
 {
  //Получаем указатель на строку
  ptr = (Byte *)pBitmap->ScanLine[y];
  for(int x = 0; x < pBitmap->Width/2; x++)
  {
   vbBt1 = ptr[4*x];         
   vbBt2 = ptr[4*x+1];
   vbBt3 = ptr[4*x+2];
   vbBt4 = ptr[4*x+3];
   ptr[4*x]=ptr[4*(pBitmap->Width-1)-4*x];
   ptr[4*x+1]=ptr[4*(pBitmap->Width-1)-4*x+1];
   ptr[4*x+2]=ptr[4*(pBitmap->Width-1)-4*x+2];
   ptr[4*x+3]=ptr[4*(pBitmap->Width-1)-4*x+3];
   ptr[4*(pBitmap->Width-1)-4*x]=vbBt1;
   ptr[4*(pBitmap->Width-1)-4*x+1]=vbBt2;
   ptr[4*(pBitmap->Width-1)-4*x+2]=vbBt3;
   ptr[4*(pBitmap->Width-1)-4*x+3]=vbBt4;
  }
 }////for (int y = 0; y < pBitmap->Height; y++)
 //Восстановление параметров рисунка
 pBitmap->Palette=pBitmap1->Palette;
 pBitmap->PixelFormat=pBitmap1->PixelFormat;
 Canvas->Draw(pBitmap->Width*2+50,0,pBitmap);
 delete pBitmap;
}

Данный код выполняется также быстро как и при использовании метода CopyRect() и он легко трансформируем для поворотов на 180 градусов и для отображения сверху вниз. Однаго такой немаловажный факт как "много кода" врядли когото подвигнет к использованию ScanLine[] вместо CopyRect() для данных поворотов.

Однако ScanLine[] безусловный лидер из рассмотренных методов при повороте на 90 градусов. Правда для этого автору пришлось пойти на несколько хитростей, которые рассмотрим в следующем примере.

void __fastcall
TForm1::Button1Click(TObject *Sender)
{
 //Объекты TBitmap для исходного изображения,
 //для повернутого и для запоминания параметров
 Graphics::TBitmap *pBitmap  = new Graphics::TBitmap();
 Graphics::TBitmap *pBitmap1 = new Graphics::TBitmap();
 Graphics::TBitmap *pBitmap2 = new Graphics::TBitmap();
 //Указатель на массив байт для Scanlene
 Byte  *ptr;
 //Вспомогательные переменные для хранения байт при поворотах
 Byte  vbBt1,vbBt2,vbBt3,vbBt4;
 //Загружаем изображение
 pBitmap->LoadFromFile("a1.bmp");
 //Сохраняем рисунок и его параметры
 pBitmap2->Assign(pBitmap);
 //Рисуем для наглядности
 Canvas->Draw(0,0,pBitmap);
 //Преобразуем координаты
 pBitmap1->Width=pBitmap->Height;
 pBitmap1->Height=pBitmap->Width;
 //Преобразуем формат
 pBitmap1->PixelFormat=pf32bit;
 pBitmap ->PixelFormat=pf32bit;
 //Поворачиваем изображение
 for (int y = 0; y < pBitmap->Height-1; y++)
 {
  //Получаем указатель на строку
  ptr = (Byte *)pBitmap->ScanLine[y];
  for(int x = 0; x < pBitmap->Width; x++)
  {
   vbBt1 = ptr[4*x];
   vbBt2 = ptr[4*x+1];
   vbBt3 = ptr[4*x+2];
   vbBt4 = ptr[4*x+3];
   pBitmap1->Canvas->Pixels[pBitmap1->Width-1-y][x]=
   //(COLORREF)
   (
   (BYTE)vbBt3  | (WORD)vbBt2 << 8
                   |
                  (DWORD)vbBt1 << 16
                  |
                  (DWORD)vbBt4 << 24
                   ) ;
  }
 }
 //Восстановление параметров рисунка
 pBitmap1->Palette=pBitmap2->Palette;
 pBitmap1->PixelFormat=pBitmap2->PixelFormat;
 //Рисуем повернутое на 90 градусов изображение
 Canvas->Draw(pBitmap->Width*2+50,0,pBitmap1);
 //Можно и сохранить и любоваться поворотами по нажатию кнопки
 pBitmap1->SaveToFile("a1.bmp");
 delete pBitmap;
 delete pBitmap1;
 delete pBitmap2;
}

В начало

Вращение битового образа

Эта часть раздела основана на статье "Вращение битового образа" Мишеля Ленона (перевод Тараса Сороки). Автором лиш исправлены некоторые агрехи, заменены обозначения на более привычные для всего материала и, также, показано как сделать поворот изображения еще более быстрым даже без приприменениия быстрых функций (как отмечалось - работающих только в OC Windows линейки NT).

Из рисунка, демонстрирующего поворот TBitnmap pFromBitmap на некоторый угол видно, что для того, чтобы в нужное место отобразить пиксель исходного изображение необходимы тригонометрические преобразования для нахождения этого нужного места.

graphics_4.gif

Вся разница в повороте изображений на произвольный угол от поворота на угол кратный 90 градусов и состоит в том, что для нахождения нового положения точки применяются функции тригонометрии, которые очень хорошо описаны в указанной выше статье и на этом внимания обращать не будем (в коментарии к этой части кода обращено внимание только на имеющиеся ошибки).

В приводимом примере показано три способа поворота на произвольный угол:

  • попиксельно;

  • c применением функции SetWorldTransform();

  • с использованием метода ScanLine() и преобразования формата.

Определим глобально два макроса и включим в проект файл определения math.h.

#include <math.h>
#define min(a, b) (((a) < (b)) ? (a) : (b)) 
#define max(a, b) (((a) > (b)) ? (a) : (b))

Следующий код выполняет поворот отображения попиксельно:

void __fastcall
TForm1::vRotate(int viAngle)
{
 Graphics::TBitmap *pFromBitmap=new Graphics::TBitmap;
 Graphics::TBitmap *pToBitmap=new Graphics::TBitmap;
 pFromBitmap->LoadFromFile("a1.bmp");
 //Геометрические преобразования коорденат для
 //определения положения пикселей источника
 float vfRradians=(2*3.1416*viAngle)/360;
 float vfCosinus=(float)cos(vfRradians);
 float vfSinus=(float)sin(vfRradians);
 float vfPoint1x=(-pFromBitmap->Height*vfSinus);
 float vfPoint1y=(pFromBitmap->Height*vfCosinus);
 float vfPoint2x=(pFromBitmap->Width*vfCosinus-pFromBitmap->Height*vfSinus);
 float vfPoint2y=(pFromBitmap->Height*vfCosinus+pFromBitmap->Width*vfSinus);
 float vfPoint3x=(pFromBitmap->Width*vfCosinus);
 float vfPoint3y=(pFromBitmap->Width*vfSinus);
 float vfMinx=min(0,min(vfPoint1x,min(vfPoint2x,vfPoint3x)));
 float vfMiny=min(0,min(vfPoint1y,min(vfPoint2y,vfPoint3y)));
 float vfMaxX=max(vfPoint1x,max(vfPoint2x,vfPoint3x));
 float vfMaxY=max(vfPoint1y,max(vfPoint2y,vfPoint3y));
 int pToBitmapWidth=(int)ceil(vfMaxX-vfMinx);
 int pToBitmapHeight=(int)ceil(vfMaxY-vfMiny);
 pToBitmap->Height=pToBitmapHeight;
 pToBitmap->Width=pToBitmapWidth;
 //Попиксельный поворот
 for(int y = 0;y < pToBitmapHeight; y++)
 {
  for(int x=0;x<pToBitmapWidth;x++)
  {
   int pFromBitmapx=(int)((x+vfMinx)*vfCosinus+(y+vfMiny)*vfSinus);
   int pFromBitmapy=(int)((y+vfMiny)*vfCosinus-(x+vfMinx)*vfSinus);
   if(pFromBitmapx>=0&&pFromBitmapx<pFromBitmap->Width&&pFromBitmapy>=0&&
      pFromBitmapy<pFromBitmap->Height)
   {
    pToBitmap->Canvas->Pixels[x][y]=
    pFromBitmap->Canvas->Pixels[pFromBitmapx][pFromBitmapy];
   }
  }
 }
 //Отображение повернутого образа
 Image1->Left=0;
 Image1->Top=0;
 Image1->Width=pToBitmap->Width;
 Image1->Height=pToBitmap->Height;
 Image1->Picture->Bitmap=pToBitmap;
 //Изображение можно сохранить
 pToBitmap->SaveToFile("a2.bmp");
}

Использование функции SetWorldTransform():

void __fastcall
TForm1::vRotate(int viAngle)
{
 Graphics::TBitmap *pFromBitmap=new Graphics::TBitmap;
 Graphics::TBitmap *pToBitmap=new Graphics::TBitmap;
 pFromBitmap->LoadFromFile("a1.bmp");
 //Геометрические преобразования коорденат для
 //определения положения пикселей источника
 float vfRradians=(2*3.1416*viAngle)/360;
 float vfCosinus=(float)cos(vfRradians);
 float vfSinus=(float)sin(vfRradians);
 float vfPoint1x=(-pFromBitmap->Height*vfSinus);
 float vfPoint1y=(pFromBitmap->Height*vfCosinus);
 float vfPoint2x=(pFromBitmap->Width*vfCosinus-pFromBitmap->Height*vfSinus);
 float vfPoint2y=(pFromBitmap->Height*vfCosinus+pFromBitmap->Width*vfSinus);
 float vfPoint3x=(pFromBitmap->Width*vfCosinus);
 float vfPoint3y=(pFromBitmap->Width*vfSinus);
 float vfMinx=min(0,min(vfPoint1x,min(vfPoint2x,vfPoint3x)));
 float vfMiny=min(0,min(vfPoint1y,min(vfPoint2y,vfPoint3y)));
 float vfMaxX=max(vfPoint1x,max(vfPoint2x,vfPoint3x));
 float vfMaxY=max(vfPoint1y,max(vfPoint2y,vfPoint3y));
 int pToBitmapWidth=(int)ceil(vfMaxX-vfMinx);
 int pToBitmapHeight=(int)ceil(vfMaxY-vfMiny);
 pToBitmap->Height=pToBitmapHeight;
 pToBitmap->Width=pToBitmapWidth;
 //Поворот с использованием функции SetWorldTransform
 SetGraphicsMode(pToBitmap->Canvas->Handle,GM_ADVANCED);
 XFORM xform;
 xform.eM11=vfCosinus;
 xform.eM12=vfSinus;
 xform.eM21=-vfSinus;
 xform.eM22=vfCosinus;
 xform.eDx=(float)-vfMinx;
 xform.eDy=(float)-vfMiny;
 SetWorldTransform(pToBitmap->Canvas->Handle,&xform);
 BitBlt(pToBitmap->Canvas->Handle,0,0,
 //Здесь коордената не приемника, а источника
 //иначе при углах более 45 градусов изображение
 //отобразится урезанным
 pFromBitmap->Width,
 pFromBitmap->Height,
 pFromBitmap->Canvas->Handle,
 0,
 0,
 SRCCOPY);
 //Отображение повернутого образа
 Image1->Left=0;
 Image1->Top=0;
 Image1->Width=pToBitmap->Width;
 Image1->Height=pToBitmap->Height;
 Image1->Picture->Bitmap=pToBitmap;
 //Изображение можно сохранить
 pToBitmap->SaveToFile("a2.bmp");
}

Последний метод по быстродействию занимает промежуточное положение между попиксельным поворотом изображения и поворотом с использованием функции SetWorldTransform, но работает в любой Windows системе:

void __fastcall
TForm1::vRotate(int viAngle)
{
 Graphics::TBitmap *pFromBitmap=new Graphics::TBitmap;
 Graphics::TBitmap *pToBitmap=new Graphics::TBitmap;
 pFromBitmap->LoadFromFile("a1.bmp");
 //Геометрические преобразования коорденат для
 //определения положения пикселей источника
 float vfRradians=(2*3.1416*viAngle)/360;
 float vfCosinus=(float)cos(vfRradians);
 float vfSinus=(float)sin(vfRradians);
 float vfPoint1x=(-pFromBitmap->Height*vfSinus);
 float vfPoint1y=(pFromBitmap->Height*vfCosinus);
 float vfPoint2x=(pFromBitmap->Width*vfCosinus-pFromBitmap->Height*vfSinus);
 float vfPoint2y=(pFromBitmap->Height*vfCosinus+pFromBitmap->Width*vfSinus);
 float vfPoint3x=(pFromBitmap->Width*vfCosinus);
 float vfPoint3y=(pFromBitmap->Width*vfSinus);
 float vfMinx=min(0,min(vfPoint1x,min(vfPoint2x,vfPoint3x)));
 float vfMiny=min(0,min(vfPoint1y,min(vfPoint2y,vfPoint3y)));
 float vfMaxX=max(vfPoint1x,max(vfPoint2x,vfPoint3x));
 float vfMaxY=max(vfPoint1y,max(vfPoint2y,vfPoint3y));
 int pToBitmapWidth=(int)ceil(vfMaxX-vfMinx);
 int pToBitmapHeight=(int)ceil(vfMaxY-vfMiny);
 pToBitmap->Height=pToBitmapHeight;
 pToBitmap->Width=pToBitmapWidth;
 //Поворот с использованием метода Scanline
 pFromBitmap->PixelFormat=pf32bit;
 pToBitmap->PixelFormat=pf32bit;
 //Указатель на массив байт для Scanlene
 Byte  *ptrByte;
 //Вспомогательные переменные для хранения байт при поворотах
 Byte  vbBt1,vbBt2,vbBt3,vbBt4;
 int viChangeY=1;
 for(int y = 0; y < pToBitmapHeight; y++)
 {
  for(int x = 0; x < pToBitmapWidth; x++)
  {
   int pFromBitmapx=(int)((x+vfMinx)*vfCosinus+(y+vfMiny)*vfSinus);
   int pFromBitmapy=(int)((y+vfMiny)*vfCosinus-(x+vfMinx)*vfSinus);
   if(pFromBitmapx >= 0 && pFromBitmapx<pFromBitmap->Width&&pFromBitmapy>=0&&
   pFromBitmapy<pFromBitmap->Height)
   {
    //Этот прием сканирования только при смене строк
    //еще более ускоряет поворот
    if(viChangeY != pFromBitmapy)
    {
     //Получаем указатель на строку исходного
     ptrByte = (Byte *)pFromBitmap->ScanLine[pFromBitmapy];
     viChangeY=pFromBitmapy;
     vbBt1 = ptrByte[4*pFromBitmapx];
     vbBt2 = ptrByte[4*pFromBitmapx+1];
     vbBt3 = ptrByte[4*pFromBitmapx+2];
     vbBt4 = ptrByte[4*pFromBitmapx+3];
    }
    pToBitmap->Canvas->Pixels[x][y]=
    //(COLORREF)
    (
    (BYTE)vbBt3 | (WORD)vbBt2  << 8
                | (DWORD)vbBt1 << 16
                | (DWORD)vbBt4 << 24
    ) ;
   }
  }
 }
 //Отображение повернутого образа
 Image1->Left=0;
 Image1->Top=0;
 Image1->Width=pToBitmap->Width;
 Image1->Height=pToBitmap->Height;
 Image1->Picture->Bitmap=pToBitmap;
 //Изображение можно сохранить
 pToBitmap->SaveToFile("a2.bmp");

В начало

Некоторые рекомендации по выбору способов выполнения поворотов

С учетом этих рекомендаций построена программа "Цифровое фото" в которой при выборе (помещение на нее курсора) цифровой фотографии (1600*1200=1920000 пикселей) она мгновенно отображается на экране монитора уже отмасштабированной и первый поворот возможен уже через 1-5 секунд (зависит от скорости компьютера и от ОС). В этот период выполняются различные операции подготовки к быстрым поворотам и возможностям сохранения фото в другом формате и с другим углом расположения относительно экрана. Все последующие повороты на любой угол кратный 90 градусов выполняются практически мгновенно за счет:

  • при выборе фото оно не только отображается, но его внеэкранный образ запоминается в TBitmap (исходный образ) и, сразуже, выполняется поворот внеэкранного образа на 90 градусов и его запоминание в TBitmap;

  • при повороте на 90 градусов использются либо метод SetWorldTransform() либо ScanLine() в зависимости от ОС, выбор метода определяет возвращаемое значение функции SetWorldTransform();

  • при поворотах на 180 градусов используется самый быстрый метод CopyRec();

  • после поворота запоминается положение изображения и при следующем повороте либо применяется функция CopyRec(), либо берется одно из двух запомненных изображений.

    Результат - Вы видите мгновенные повороты изображения на любой кратный 90 градусов угол.

    20.04.2004 Владислав Молчанов.

    В начало

    В начало главы

    В начало раздела

    Домой