Как повернуть изображение? Это один самых популярных запросов к сайту. Согласен, что статья о теории аффинных преобразований больше заточена под объяснение коэффициентов матриц преобразований, почему они такие. Поэтому, исправляю ситуацию и отвечаю на вопрос.
В обзор не входят Graphics32, OpenCV и другие библиотеки, требующие дополнительной установки.
Немного теории
Подсмотрим аффинную матрицу поворота в справочнике. Также нам понадобится матрица переноса. Напомню, что аффинные преобразования применяются ко всей плоскости. Прелесть преобразований в том, что мы рисуем самым обычным способом, но уже на трансформированной плоскости.
Формулы для нахождения координат при повороте следующие:
Формулы верны, если начало координат совпадает с точкой вращения. В нашем случае, точка начала координат O(0,0) находится в верхнем левом углу. Точка вокруг которой хотим повернуть изображение имеет координаты С(Cx,Сy). Чтобы получить верные значение x и y в формулах выше, необходимо привести их в правильный вид.
Смещение D(x,y) определяет, куда хотим поместить точку вращения после поворота.
Напомню, аффинное преобразование, это матричное представление вида:
Рис.1. Матричное представление аффинного преобразования
Где: M11, M12, M21, M22, Dx, Dy – коэффициенты, определяющие преобразование.
В случае поворота матрица имеет вид:
Сложное, или составное, аффинное преобразование — это произведение матриц в него входящих.
Таким образом, чтобы повернуть изображение на угол, необходимо произвести перемножение следующих матриц:
В случае, если требуется просто повернуть изображение вокруг некоторой точки, смещение Dx равно Cx и Dy равно Cy.
Как бы это выглядело в некоем псевдокоде:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
procedure DrawRotateImage( ctx: Context; // context for drawing rct: Rect; // rect of image img: Image; // image angle: float; // rotation angle center: point // center of rotation ) var matrix: AffineMatrix; // matrix of affine transformation begin // normalize coordinates by rotation center // нормализация координат по точке вращения matrix.SetTranslate(-center.x, -center.y); // rotation matrix // матрица поворота matrix.SetRotate(angle); // transfer to the rotation center // перенос в точку вращения matrix.SetTranslate(center.x, center.y); // make an affine transformation // произвести аффинное преобразование на плоскости ctx.SetMatrix(matrix); // draw normally, as if there is no rotation // нарисовать обычным образом, как будто нет поворота ctx.DrawBitmap(bmp, rct.Left, rct.Top); end; |
Теперь к рецептам.
Повернуть изображение в GDI
За матрицу аффинных преобразования в WinGDI отвечает структура TXForm.
1 2 3 4 5 6 7 8 9 |
tagXFORM = record eM11: Single; eM12: Single; eM21: Single; eM22: Single; eDx: Single; eDy: Single; end; TXForm = tagXFORM; |
Поля имеют тот же смысл, что и в матрице на рис.1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
function GDIDrawRotateImage(const ACanvas: TCanvas; const ARect: TRect; const AImage: TGraphic; const ACenter: TPointF; const Angle: Single): Boolean; var XForm1: TXForm; XForm2: TXForm; XForm3: TXForm; XForm: TXForm; XFormOld: TXForm; C: TPointF; sn,cs: Single; GModeOld: Integer; begin Result := Assigned(ACanvas) and Assigned(AImage) and (ARect.Width > 0) and (ARect.Height > 0); if not Result then Exit; C := ACenter; SinCos(Angle * PI/180, sn, cs); // normalize coordinates by rotation center // нормализация координат по точке вращения XForm1.eM11 := 1.0; XForm1.eM12 := 0.0; XForm1.eM21 := 0.0; XForm1.eM22 := 1.0; XForm1.eDx := -C.X; XForm1.eDy := -C.Y; // rotation matrix // матрица поворота XForm2.eM11 := cs; XForm2.eM12 := sn; XForm2.eM21 := -sn; XForm2.eM22 := cs; XForm2.eDx := 0.0; XForm2.eDy := 0.0; // transfer to the rotation center // перенос в точку вращения XForm3.eM11 := 1.0; XForm3.eM12 := 0.0; XForm3.eM21 := 0.0; XForm3.eM22 := 1.0; XForm3.eDx := C.X; XForm3.eDy := C.Y; // multiply matrices in the required order // перемножение матриц в требуемом порядке CombineTransform(XForm, XForm1, XForm2); CombineTransform(XForm, XForm, XForm3); // put the context in advanced mode, otherwise the conversion won't work // перевод контекста в продвинутый режим, иначе преобразование не сработает GModeOld := SetGraphicsMode(ACanvas.Handle, GM_ADVANCED); Result := GModeOld <> 0; if not Result then Exit; // make an affine transformation // произвести аффинное преобразование на плоскости Result := GetWorldTransform(ACanvas.Handle, XFormOld); if Result then try SetWorldTransform(ACanvas.Handle, XForm); ACanvas.StretchDraw(ARect, AImage); finally // return the mode and transformation matrix to the context // вернуть режим и матрицу преобразования в контекст SetWorldTransform(ACanvas.Handle, XFormOld); SetGraphicsMode(ACanvas.Handle, GModeOld); end; end; |
Параметры имеют такой же смысл, что и в псевдокоде выше.
ACanvas | Контекст, на котором рисуем |
ARect | Прямоугольная область, в которой хотим нарисовать |
AImage | Изображение, которое хотим нарисовать |
ACenter | Точка центра вращения |
Angle | Угол поворота в градусах |
Для применения аффинных трансформации в WinGDI предусмотрены функции
function CombineTransform(var p1: TXForm; const p2, p3: TXForm): BOOL; stdcall; |
Умножает матрицу p2 на p3 и возвращает результат в p1 |
function SetGraphicsMode(hdc: HDC; iMode: Integer): Integer; stdcall; |
Переводит контекст hdc в один из двух режимов: GM_COMPATIBLE GM_ADVANCED Чтобы аффинные преобразования возымели действие, необходимо перевести в режим GM_ADVANCED |
function GetWorldTransform(DC: HDC; var p2: TXForm): BOOL; stdcall; |
Возвращает текущую трансформацию для контекста |
function SetWorldTransform(DC: HDC; const p2: TXForm): BOOL; stdcall; |
Устанавливает новое аффинное преобразование в контекст |
Повернуть изображение в GDIPlus
В GDI+ все делается намного проще. Рисование в GDI+ происходит в своем холсте GPGraphics: TGPGraphics. Для матрицы аффинных преобразований предусмотрен класс TGPMatrix. Вместо типа TGraphic передаем заранее подготовленный экземпляр TGPImage.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
function GDIPDrawRotateImage(const ACanvas: TCanvas; const ARect: TRect; const AImage: TGPImage; const ACenter: TPointF; const Angle: Single): Boolean; var GPGraphics: TGPGraphics; GPMatrix: TGPMatrix; begin Result := Assigned(ACanvas) and Assigned(AImage) and (ARect.Width > 0) and (ARect.Height > 0); if not Result then Exit; // make GDI+ context GPGraphics := TGPGraphics.Create(ACanvas.Handle); // make rotation matrix GPMatrix := TGPMatrix.Create; try // set the interpolation mode GPGraphics.SetInterpolationMode(InterpolationModeHighQualityBilinear); // matrix 1 - coordinate normalization GPMatrix.Translate(-ACenter.X,-ACenter.Y, MatrixOrderAppend); // matrix 2 - rotate GPMatrix.Rotate(Angle, MatrixOrderAppend); // matrix 3 - offset GPMatrix.Translate(ACenter.X,ACenter.Y, MatrixOrderAppend); // apply rotation matrix GPGraphics.SetTransform(GPMatrix); // draw the picture in the usual way, as if there were no transformations // нарисовать картинку обычным способом, как будто никаких трансформаций нет Result := GPGraphics.DrawImage(AImage, MakeRect(ARect.Left,ARect.Top,ARect.Width,ARect.Height), 0,0,AImage.GetWidth,AImage.GetHeight,UnitPixel)=Ok; finally FreeAndNil(GPMatrix); FreeAndNil(GPGraphics); end; end; |
Для инициализации матриц поворота и переноса в классе TGPMatrix существуют следующие методы:
function TGPMatrix.Translate(offsetX, offsetY: Single; order: TMatrixOrder = MatrixOrderPrepend): TStatus; |
Инкапсулирует функцию GDIPApi: function GdipTranslateMatrix(matrix: GPMATRIX; offsetX: Single; offsetY: Single; order: GPMATRIXORDER): GPSTATUS; stdcall; Инициализирует матрицу перемещения параметрами offsetX, offsetY и добавляет в текущую композицию трансформаций в зависимости от параметра order, либо в начало (MatrixOrderPrepend), либо в конец (MatrixOrderAppend) |
function TGPMatrix.Rotate(angle: Single; order: TMatrixOrder = MatrixOrderPrepend): TStatus; |
Инкапсулирует функцию GDIPApi: function GdipRotateMatrix(matrix: GPMATRIX; angle: Single; order: GPMATRIXORDER): GPSTATUS; stdcall; Инициализирует матрицу поворота значениями синуса и косинуса угла angle, заданного в градусах, и добавляет в текущую композицию трансформаций в зависимости от параметра order, либо в начало (MatrixOrderPrepend), либо в конец (MatrixOrderAppend) |
Устанавливает трансформацию следующий метод холста GDI+
function TGPGraphics.SetTransform(matrix: TGPMatrix): TStatus; |
Инкапсулирует функцию GDIPApi: function GdipSetWorldTransform(graphics: GPGRAPHICS; matrix: GPMATRIX): GPSTATUS; stdcall; |
Стоит добавить, что для поворота в GDI+ в в реализации Delphi существует специальный метод:
function TGPMatrix.RotateAt(angle: Single; const center: TGPPointF; order: TMatrixOrder = MatrixOrderPrepend): TStatus; |
Метод включает в себя инициализацию и композицию матриц переноса, поворота и обратного переноса. |
Повернуть изображение в Direct2D
Для Direct2D мы не станем создавать холст внутри функции, как для GDI+. Это безумно расточительная операция. По времени занимает примерно 100 миллисекунд. Поэтому будем передавать холст TDirect2DCanvas как параметр функции. Также, передаем заранее подготовленный битмап ID2D1Bitmap.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function Direct2DDrawRotateImage(const ACanvas: TDirect2DCanvas; const ARect: TRect; const AImage: ID2D1Bitmap; const ACenter: TPointF; const Angle: Single): Boolean; var matrix: TD2DMatrix3X2F; D2DRect: TD2DRectF; begin Result := Assigned(ACanvas) and Assigned(AImage) and (ARect.Width > 0) and (ARect.Height > 0); if not Result then Exit; matrix := TD2DMatrix3X2F.Rotation(Angle, ACenter.X, ACenter.Y); ACanvas.RenderTarget.SetTransform(matrix); D2DRect := D2D1RectF(ARect.Left, ARect.Top, ARect.Right, ARect.Bottom); ACanvas.RenderTarget.DrawBitmap(AImage, @D2DRect, 1); end; |
Складывается ощущение, что чем технология продвинутей, тем кода все меньше и меньше.
class function TD2DMatrix3x2FHelper.Rotation(const angle, x, y: Single): D2D_MATRIX_3X2_F; |
Метод хелпера для структуры D2D_MATRIX_3X2_F Инкапсулируется процедура D2D1 API: procedure D2D1MakeRotateMatrix(angle: Single; center: D2D1_POINT_2F; matrix: PD2D1Matrix3x2F); stdcall; Создает преобразование поворота, которое поворачивается на указанный угол в градусах относительно указанной точки. |
ID2D1RenderTarget = interface(ID2D1Resource) [SID_ID2D1RenderTarget] procedure SetTransform(const transform: TD2D1Matrix3x2F); stdcall; |
Применяет указанное преобразование, заменяя существующее преобразование. Все последующие операции рисования происходят в преобразованном пространстве. |
Немного софистики
При аффинном преобразовании поворота каждая точка плоскости должна быть высчитана по формулам, указанным в начале статьи. Но часто при объяснении подобных преобразований употребляется термин переноса начала координат в некую точку. Согласитесь, перенос всей плоскости как бы не вяжется с переносом точки внутри плоскости?
На самом деле происходит нормализация координат относительно точки, потому что формулы выведены на предположении, что точка вращения — это точка начала координат. Однако, представить себе перенос точки начала координат намного проще, чем осознать все перемещение.
Из-за разности понимания процесса, да и понятие композиций того требует, возникает понятие порядка перемножения, и, например, для GDI+ по умолчанию он равен:
1 |
order: TMatrixOrder = MatrixOrderPrepend |
Зачем это нужно?
Если мы используем порядок, когда матрицы устанавливаются в начало выражения перемножения, алгоритм работы можно описать более понятными словами:
- Перенос начала координат в точку вращения
- Поворот
- Обратный перенос в первоначальную точку начала координат
И в коде, например для GDI+, это будет выглядеть как:
1 2 3 4 5 6 7 8 9 |
// matrix 1 - transfer the origin to the ACenter point // matrix 1 - перенос начала координат в точку ACenter GPMatrix.Translate(ACenter.X, ACenter.Y); // matrix 2 - rotate GPMatrix.Rotate(Angle); // matrix 3 - go back // matrix 3 - вернуться обратно GPMatrix.Translate(-ACenter.X, -ACenter.Y); |
В нашем случае, выражения перемножения будет выглядеть как:
1 |
[matrix 3] × [matrix 2] × [matrix 1] |
Учитывая, что теперь в матрице 1 происходит перенос на положительные значения, а в матрице 3 — на отрицательные, итоговые формулы станут ровно такими же, как в начале.
Изменив порядок перемножение матриц и их содержимое, мы тем самым не повлияли на результат, но понимать стало проще и привычней. Подобных примеров в реальной жизни полно.
Например, статья на wiki о движение Солнца по небу. Вполне себе научная статья, в которой говорится о движении светила по небесной сфере. Бруно бы изругался на такую трактовку. Все мы прекрасно понимаем, что движемся мы, а не Солнце, но восприятию информации это никак не мешает. Все всё понимают.
Если говорить о повороте в JavaScript, то там при создании преобразовании работает именно такой порядок — дописывать в начало матричного выражения.
Повернуть изображение в JavaScript
В JavaScript все последующие преобразования дописываются в начало выражения умножения матриц трансформаций. Поэтому, при повороте, вначале смещаемся на положительные значения координат точки вращения, делаем поворот и обратно, уже с минусом.
Точка вращения в центре изображения
На рисунке ниже за мерцающую оранжевую точку можно таскать мышкой.
Если рисунка не видно, значит используется очень старый браузер или InternetExplorer.
Функция очень простая:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// draw the picture at an angle (radians) // рисовать картинку под углом (радианы) function drawRotateImage(ctx, rct, img, center, angle=0.0) { // save context parameters // сохранить параметры контекста ctx.save(); // matrix 1 ctx.translate(center.x,center.y); // matrix 2 ctx.rotate(angle); // variant 1: // matrix 3 ctx.translate(-center.x,-center.y); ctx.drawImage(img, rct.x, rct.y, rct.width, rct.height); // variant 2: //ctx.drawImage(img, rct.x-center.x, rct.y-center.y, rct.width, rct.height); // restore context parameters // восстановить параметры контекста ctx.restore(); } |
Как видно алгоритм абсолютно такой же, как представленный выше. Параметры функции и их порядок имеют смысл ровно тот же, что и раньше. И как уже было указано, порядок перемножения будет следующий:
1 |
[matrix 3] × [matrix 2] × [matrix 1] |
Что означает вариант 2 в листинге. Вместо последней трансформации, можно при выводе картинки указать смещение на отрицательные значения точки вращения. Итоговая функция преобразования от этого не изменится.
1 2 3 4 5 6 7 |
// matrix 1 ctx.translate(center.x,center.y); // matrix 2 ctx.rotate(angle); // variant 2: ctx.drawImage(img, rct.x-center.x, rct.y-center.y, rct.width, rct.height); |
Однако, подобные упрощения могут сильно помешать в сложных преобразованиях, что собираюсь продемонстрировать двумя заголовками ниже.
Точка вращения вне изображения
На холсте точка вращения находится в центре земного ядра, а изображение летает по орбите вокруг. Таким образом, точка поворота находится далеко за пределами картинки. Но поверьте, функция используется ровно та же самая.
Фокус исключительно в прямоугольнике, в котором рисуем. Выберите галочку «Показать исходный прямоугольник»
Справа, у орбиты появился серый прямоугольник. Вот в нем изображение и рисуем. За его реальное местонахождение отвечает аффинное преобразование. Повернутый прямоугольник можно посмотреть поставив галочку на «Показать повернутый прямоугольник».
Обобщая, можно сказать следующее. Чтобы аффинное преобразование работало как положено, надо 1) найти прямоугольник вывода изображения при угле, равным 0, в обычных, нетрансформированных координатах; 2) определиться с координатами точки вращения. Далее просто применить функцию.
Ну, миньона запускать на орбиту мы уже научились. Теперь неплохо бы делом заняться.
Одновременное вращение вокруг себя и Солнца
Обобщим два предыдущих пункта. Хочется создать такое аффинное преобразование, в котором бы происходило одновременное вращение изображения вокруг своего центра и некоей внешней точки. Ничего лучше, чем вращение Земли вокруг Солнца на ум не пришло. Это конечно не реальная модель. Просто хотелось, чтобы объект Земля летел по орбите и при этом совершал обороты вокруг своей оси. Не зря же мы к wiki обращались по этому поводу.
Все мигающие маркеры можно таскать мышкой. Прямоугольник для вывода при нулевом угле рассчитываем ровно также. Единственное, что я его сместил на полширины вправо, чтобы объект был точно на орбите.
Фокус на этот раз в создании аффинного преобразования.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// draw the simultaneous rotation of the earth around the sun and its axis // рисовать одновременное вращение земли вокруг солнца и своей оси function drawDoubleRotateImage(ctx, rct, img, center, alpha=0.0, beta=0.0) { // pivot point around the center of the image // точка вращения вокруг центра изображения var C = {x:rct.x + rct.width/2,y:rct.y + rct.height/2}; // outer pivot point is the sun // внешняя точка вращения - солнце var D = {x:center.x,y:center.y}; ctx.save(); // orbit // поворот по орбите // matrix 1 ctx.translate(D.x,D.y); // matrix 2 ctx.rotate(beta); // matrix 3 ctx.translate(-D.x,-D.y); // rotate around its axis // поворот вокруг своей оси // matrix 4 - pivot point is the center of the rectangle // точка вращения - центр прямоугольника ctx.translate(C.x, C.y); // matrix 5 - turn at a faster angle than orbit // поворот на более быстрый угол, чем вращение по орбите ctx.rotate(alpha); // matrix 6 - return, standard // вернуться, стандартный ход ctx.translate(-C.x, -C.y); // draw in the calculated rectangle for 0 degrees // рисуем в расcчитанном прямоугольнике для 0 градусов ctx.drawImage(img, rct.x, rct.y, rct.width, rct.height); ctx.restore(); } |
Все матрицы дописываются в начало. Таким образом получаем цепочку перемножений:
1 2 3 4 5 6 7 8 9 |
// Rotate around its axis // Поворот вокруг своей оси [matrix 6] × [matrix 5] × [matrix 4] × // Rotate around The Sun // Поворот вокруг Солнца [matrix 3] × [matrix 2] × [matrix 1] |
Вначале преобразуются координаты для поворота вокруг оси, затем, на эти преобразованные координаты накладывается преобразование относительно внешней точки вращения.
Или на языке формул.
α — угол поворота вокруг своего центра.
Cx Cy — координаты центра прямоугольника для изображения.
β — угол поворота вокруг точки вращения (Солнца).
Dx Dy — координаты точки вращения.
Любая оптимизация тут вредна. Есть места, и в математике, и в программировании, когда лучше быть закостенелым формалистом. Не надо привыкать к таким записям.
Все приведенные алгоритмы для JS легко можно воспроизвести в любом языке программирования.
Вращение вокруг произвольной точки
Вынесено в отдельную статью
Скачать
Исходники (Delphi XE 7-10) 2.03 Mб
Возможно, будут интересны эти темы. Они задействованы в исходнике.
- Запрет смены фокуса при нажатии клавиши стрелок.
- Как вставить изображение из буфера обмена.
- Простой способ определить попадание точки в многоугольник.
Исполняемый файл RotImage01.zip 2.94 Мб
Роман! Очень крутая статья, спасибо! Двойное вращение в одном преобразовании, это нечто
Роман, добрый вечер.
Воспользовался Вашим кодом вращения изображения в GDI+ в одной из своих программ. Большое спасибо. Однако при вращении рисунка на фоне другого рисунка первый перекрывает фон. Как можно сделать вращающийся рисунок прозрачным? Заранее благодарен
Добрый вечер! Тут вопрос скорее не про «сделать вращающийся рисунок прозрачным», а как вообще нарисовать рисунок поверх другого с прозрачностью. Тут масса способов. Пришлите код на ip76ru@gmail.com.