Поворот изображения на любой угол

В этой статье, мы покажем вам простую функцию поворота изображения на любой угол, которая основана на обычной математике (не содержит ничего сложнее синуса/косинуса) и может быть использована для реализации при любом формате изображения. Для целей иллюстрации мы покажем реализацию поворота для формата 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 (и при этом имеет скромные размеры).

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

Добавить комментарий