В этой статье, мы покажем вам простую функцию поворота изображения на любой угол, которая основана на обычной математике (не содержит ничего сложнее синуса/косинуса) и может быть использована для реализации при любом формате изображения. Для целей иллюстрации мы покажем реализацию поворота для формата Farbfeld и воспользуемся для этого библиотекой farbfelded.
Поворот изображения — это то, что мы уже неоднократно пытались сделать, но как-то все получалось не очень — разворот изображения на градус приводил к странным результатам. В частности, у нас «съезжало» само изображение или «сворачивались» углы. И хоть все было сделано по классической формуле перерасчета точек для поворота:
x' = x * cos(phi) - y * sin(phi) y' = x * sin(phi) + y * cos(phi) x, y - изначальные координаты точки phi - угол поворота x', y' - новые координаты
тем не менее, такой подход не совсем верен, поскольку, как мы поняли в дальнейшем — требуется переасчет размеров изображения, а также смещения некоторых его точек. Единственный момент, к такому решению мы пришли не сразу и для осознания собственных ошибок нам потребовалось познакомится со сторонними реализациями на C, одну из которых мы и взяли за основу.
Для анализа и воспроизведения поворота изображения, нами был выбран формат Farbfeld, поскольку с ним мы давно ничего не делали, а также потому что нам попалась необыкновенно простая реализация на C именно для этого формата.
Код самой реализации с использованием farbfelded в формате самостоятельного dub-скрипта:
#!/usr/bin/env dub /+ dub.sdl: dependency "farbfelded" version="~>0.0.1" +/ import farbfelded; RGBAColor sampleAt(FarbfeldImage img, int x, int y) { int clamp = 1; if (x < 0) { x = 0; clamp = 0; } else { if (x >= img.width) { x = img.width - 1; clamp = 0; } } if (y < 0) { y = 0; clamp = 0; } else { if (y >= img.height) { y = img.height - 1; clamp = 0; } } auto pixel = img[x, y]; pixel.setA(pixel.getA * clamp); return pixel; } RGBAColor interpolate(RGBAColor a, RGBAColor b, double t) { double s = 1.0 - t; return new RGBAColor( cast(ushort) (a.getR * s + b.getR * t), cast(ushort) (a.getG * s + b.getG * t), cast(ushort) (a.getB * s + b.getB * t), cast(ushort) (a.getA * s + b.getA * t) ); } FarbfeldImage rotate(FarbfeldImage img, double angle) { import std.algorithm : max, min; import std.math : abs, cos, round, sgn, sin, PI; import core.stdc.math : modf; // градусы в радианы const double radians = angle / 180.0 * PI; double S = sin(radians); double C = cos(radians); // переасчет базового размера изображения double a = img.width / 2 * C; double b = img.height / 2 * S; double c = img.height / 2 * C; double d = img.width / 2 * S; int outputWidth = cast(int) (round(max(max(-a + b, a + b), max(-a - b, a - b))) * 2.0); int outputHeight = cast(int) (round(max(max(-c + d, c + d), max(-c - d, c - d))) * 2.0); FarbfeldImage simg = new FarbfeldImage(outputWidth, outputHeight); S = sin(-radians); C = cos(-radians); for (int outy = 0; outy < outputHeight; outy++) { for (int outx = 0; outx < outputWidth; outx++) { double cox = outx - outputWidth / 2 + 1; double coy = outy - outputHeight / 2 + 1; // обратный поворот для поиска изначальных координат double inx = cox * C - coy * S + img.width / 2; double iny = coy * C + cox * S + img.height / 2; // разделение координат и интерполяционных множителей double basex, basey; double tx = modf(inx, &basex); double ty = modf(iny, &basey); int sgnx = cast(int) sgn(tx); int sgny = cast(int) sgn(ty); // Билинейная интерполяция RGBAColor pixel = interpolate( interpolate( sampleAt(img, cast(int) (basex), cast(int) (basey)), sampleAt(img, cast(int) (basex + sgnx), cast(int) (basey)), abs(tx)), interpolate( sampleAt(img, cast(int) (basex), cast(int) (basey + sgny)), sampleAt(img, cast(int) (basex + sgnx), cast(int) (basey + sgny)), abs(tx)), abs(ty)); simg[outx, outy] = pixel; } } return simg; }
По сути, здесь происходит то же самое, что и описано в формуле поворота, однако, есть нюансы — здесь есть перерасчет изначального размера изображения, причем вне зависимости от того, требуется ли такой пересчет угла. Кроме того, здесь есть две занимательные функции — sampleAt и interpolate. Первая просто берет необходимый пиксель по его координатам и считает коэффициент масштабирования для компонента прозрачности. В принципе в данном случае, такая функция выполняет безопасное получение некоторого пикселя с учетом границ изображения, но в ней нет особой необходимости, так как в библиотеку уже встроен похожий механизм контроля границ, единственное что требуется больше — это вычисление коэффициента для прозрачности, и поэтому было решено оставить функцию в изначальном виде. Вторая функция уже делает основную часть работы — интерполяцию пикселя изображения, на основане некоторого соседнего пикселя. Эта интерполяция простая и заключается в изменении вклада исходного пикселя и его соседа в получающийся при интерполяции пиксель. Функция interpolate используется с sampleAt как строительный блок для построения формулы билинейной интерполяции — одной из самых простых в реализации, и не только для изображений. Вся остальная механика кода является стандартной — по кругу, проходя каждый пиксель мы считаем его версию для необходимого угла поворота и поставляем расчитанную версию пикселя в новое изображение.
Испытать можно на стандартном изображении Lenna, однако, стоит учесть следующий момент — положительный угол поворота соответствует повороту изображения относительно его центра по часовой стрелке, отрицательный — против:
void main() { FarbfeldImage ff = new FarbfeldImage; ff.load(`/home/aquareji/Downloads/Lenna.ff`); auto w = rotate(ff, -10.0); w.save(`Lenna_rotated.ff`); }
Выглядит это так:

В качестве просмотрщика используется не lel, а несколько иная программа — sxiv, которая поддерживает Farbfeld, а также Portable Any Map и другие, а также присутствует во многих дистрибутивах Linux (и при этом имеет скромные размеры).
Конечно, реализация не совершенна и возможно даже не полна, но функции рассмотренные здесь настолько простые, что их можно распространить на любой другой формат с минимальными изменениями. Но в любом случае, полезен или нет данный код решать вам…