Как повернуть изображение. GDI, GDI+, Direct2D, JavaScript

rotate image

Как повернуть изображение? Это один самых популярных запросов к сайту. Согласен, что статья о теории аффинных преобразований больше заточена под объяснение коэффициентов матриц преобразований, почему они такие. Поэтому, исправляю ситуацию и отвечаю на вопрос.

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

Немного теории

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

Формулы для нахождения координат при повороте следующие:

Latex formula

Latex formula

Формулы верны, если начало координат совпадает с точкой вращения. В нашем случае, точка начала координат O(0,0) находится в верхнем левом углу. Точка вокруг которой хотим повернуть изображение имеет координаты С(Cxy). Чтобы получить верные значение x и y в формулах выше, необходимо привести их в правильный вид.

Latex formula

Latex formula

Смещение D(x,y) определяет, куда хотим поместить точку вращения после поворота.

Напомню, аффинное преобразование, это матричное представление вида:

Latex formula

Рис.1. Матричное представление аффинного преобразования

Где: M11, M12, M21, M22, Dx, Dy – коэффициенты, определяющие преобразование.

В случае поворота матрица имеет вид:

Latex formula

Сложное, или составное, аффинное преобразование — это произведение матриц в него входящих.

Таким образом, чтобы повернуть изображение на угол, необходимо произвести перемножение следующих матриц:

Latex formula

В случае, если требуется просто повернуть изображение вокруг некоторой точки, смещение Dx равно Cx и Dy равно Cy.

Как бы это выглядело в некоем псевдокоде:

Теперь к рецептам.

Повернуть изображение в GDI

За матрицу аффинных преобразования в WinGDI отвечает структура TXForm.

Поля имеют тот же смысл, что и в матрице на рис.1.

Параметры имеют такой же смысл, что и в псевдокоде выше.

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.

Для инициализации матриц поворота и переноса в классе 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.

Складывается ощущение, что чем технология продвинутей, тем кода все меньше и меньше.

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+ по умолчанию он равен:

Зачем это нужно?

Если мы используем порядок, когда матрицы устанавливаются в начало выражения перемножения, алгоритм работы можно описать более понятными словами:

  • Перенос начала координат в точку вращения
  • Поворот
  • Обратный перенос в первоначальную точку начала координат

И в коде, например для GDI+, это будет выглядеть как:

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

Учитывая, что теперь в матрице 1 происходит перенос на положительные значения, а в матрице 3 — на отрицательные, итоговые формулы станут ровно такими же, как в начале.

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

Например, статья на wiki о движение Солнца по небу. Вполне себе научная статья, в которой говорится о движении светила по небесной сфере. Бруно бы изругался на такую трактовку. Все мы прекрасно понимаем, что движемся мы, а не Солнце, но восприятию информации это никак не мешает. Все всё понимают.

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

Повернуть изображение в JavaScript

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

Точка вращения в центре изображения

На рисунке ниже за мерцающую оранжевую точку можно таскать мышкой.

Get a better browser, bro…

Если рисунка не видно, значит используется очень старый браузер или InternetExplorer.

Функция очень простая:

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

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

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

Точка вращения вне изображения

На холсте точка вращения находится в центре земного ядра, а изображение летает по орбите вокруг. Таким образом, точка поворота находится далеко за пределами картинки. Но поверьте, функция используется ровно та же самая.

Get a better browser, bro…

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

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

Обобщая, можно сказать следующее. Чтобы аффинное преобразование работало как положено, надо 1) найти прямоугольник вывода изображения при угле, равным 0, в обычных, нетрансформированных координатах; 2) определиться с координатами точки вращения. Далее просто применить функцию.

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

Одновременное вращение вокруг себя и Солнца

Обобщим два предыдущих пункта. Хочется создать такое аффинное преобразование, в котором бы происходило одновременное вращение изображения вокруг своего центра и некоей внешней точки. Ничего лучше, чем вращение Земли вокруг Солнца на ум не пришло. Это конечно не реальная модель. Просто хотелось, чтобы объект Земля летел по орбите и при этом совершал обороты вокруг своей оси. Не зря же мы к wiki обращались по этому поводу.

Get a better browser, bro…

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

Фокус на этот раз в создании аффинного преобразования.

Все матрицы дописываются в начало. Таким образом получаем цепочку перемножений:

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

Или на языке формул.

Latex formula

Latex formula

α — угол поворота вокруг своего центра.

Cx Cy — координаты центра прямоугольника для изображения.

Latex formula

Latex formula

β — угол поворота вокруг точки вращения (Солнца).

Dx Dy — координаты точки вращения.

Любая оптимизация тут вредна. Есть места, и в математике, и в программировании, когда лучше быть закостенелым формалистом. Не надо привыкать к таким записям.

Все приведенные алгоритмы для JS легко можно воспроизвести в любом языке программирования.

Вращение вокруг произвольной точки

Вынесено в отдельную статью


Скачать

Исходники (Delphi XE 7-10) 2.03 Mб

Возможно, будут интересны эти темы. Они задействованы в исходнике.

Исполняемый файл RotImage01.zip 2.94 Мб


5 5 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Bor
Bor
7 месяцев назад

Роман! Очень крутая статья, спасибо! Двойное вращение в одном преобразовании, это нечто

1
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x