воскресенье, 26 декабря 2010 г.

Обработка изображений в PHP

По дороге из мастерской Март нагнал Бэрка и пошел рядом с ним.
– Что там такое? – спросил он. – Уж не собираются ли они выдать нам по оловянной медали?
– Даже лучше, – сказал Бэрк. – Узнаешь сам.

Джоунс Рэймонд Ф. "Уровень шума"

А почему бы, собственно, не нарисовать оловянную медаль? В конце концов, вариант

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

Итак, есть некое изображение, нужно превратить его в медаль. Для этого требуется:
1) Получить из изображения чеканку
2) Нарисовать колодку
А так как нужно стараться решать не задачи, а классы задач, то будем считать, что изображение - достаточно произвольное, и вообще пусть всё делает компьютер, а именно PHP с подключенным модулем gd2.

1а. Пусть у нас уже готова веб-форма для загрузки изображения, например:

<HTML>
<BODY>
<FORM method="POST" enctype="multipart/form-data" action="step2.php">
<INPUT type="file" name="">
</FORM>
</BODY>
</HTML>

Соответственно, в скрипте step2.php можно использовать имя сохраненной копии переданного файла в переменной $_FILES['userfile']['tmp_name']. С ним дальше и будем работать.

1б. Сначала надо открыть файл в PHP. Для этого воспользуемся способом, используемым в WR-Gallery:

$size = getimagesize($SrcFileName);
if ($size !== false)
{
$format = strtolower(substr($size['mime'], strpos($size['mime'], '/')+1));
$icfunc = "imagecreatefrom" . $format;
if (function_exists($icfunc))
$isrc = $icfunc($SrcFileName);
}

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

1в. Дальше надо привести полученное изображение к нужному нам размеру. Это легко сделать при помощи функции imagecopyresampled:

$idest = imagecreatetruecolor($new_width, $new_height);
imagecopyresampled($idest, $isrc,
0, 0,
0, 0,
$new_width, $new_height,
$size[0], $size[1]);

После этой операции об $isrc можно забыть: imagedestroy($isrc);

1г. Для чеканки, как выяснилось, требуется изображение в оттенках серого. И вот тут начинается небольшое изобретение велосипеда. Да-да, все слышали об imagecopymergegrey и imagefilter, с помощью которых задача решается в два действия, но будем считать, что автор - дятел, и попробуем сделать всё вручную, заодно вспомнив, какая математика скрывается за всеми этими операциями.

Итак, оттенки серого. Как выяснилось, любой цветной пиксел (r,g,b) преобразуется в серый при помощи следующей формулы: h = 0.30*r + 0.59*g + 0.11*b. Таким образом, получив для каждой точки изображения ее составляющие мы вполне можем перевести эту точку в оттенки серого:

$rgb = imagecolorat($idest, $x, $y);
$r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
$grey[$x][$y] = $r * 0.3 + $g * 0.59 + $b * 0.11;

Тут, казалось бы, и рисовать! Но, забегая вперед, скажем, что результат чеканки в целом с использованием палитры $color[$i] = imagecolorallocate($igrey,$i,$i,$i); оказался неудовлетворительным с точки зрения контрастности, поэтому при инициализации палитры эту самую контрастность лучше заранее повысить.

1д. С контрастностью всё оказалось просто. Во-первых, есть вот такая статья с готовыми алгоритмами, посвященными этой теме, и, во-вторых, отыскалась простая формула, пересчета палитры в заданную контрастность: H_new = (H_old - H_mid)*Contrast + H_mid, где H_new и H_old - новое и старое значения цвета соответственно, Contrast - коэффициент, задающий контрастность и H_mid - неподвижная точка преобразования.

Соответственно в нашем случае подготовить изображение и нужный массив цветов можно следующим образом:

$igrey = imagecreatetruecolor(240, 400);
for ($i = 0; $i < 256; $i++)
{
$v = ($i - 128) * 4 + 128;
if ($v < 0) $v = 0;
if ($v > 255) $v = 255;
$color[$i] = imagecolorallocate($igrey, $v,$v,$v);
}


1е. Теперь остается самое простое - имея двумерный массив интенсивностей $grey[$x][$y] реализовать то, что называется emboss. И тут возникло небольшое затруднение. Из далекого детства известно, что большинство преобразований - размытие, резкость и т.п. - это просто результат применения некого фильтра к исходному изображению. Что имеется в виду? Пусть задана некая матрица А=a(i,j), где i=-n..n, j=-m..m. Тогда применение фильтра к каждой точке изображения - это вычисление суммы слагаемых вида a(i,j)*$grey[$x+i][$y+j], деленной на сумму коэффициентов a(i,j).

Например, для размытия матрица А имеет вид:
1 1 1
1 1 1
1 1 1

для вычисления границ:
 1  1  1
0 0 0
-1 -1 -1

и т.п.

А вот для emboss почему-то разные источники предлагают разные матрицы. Во-первых встретилась такая матрица:
1  0  0
0 0 0
0 0 -1

или даже такая:
-1 -1  1
-1 -1 1
1 1 1

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

$grey[$x][$y] = ($grey[$x+1][$y+1] + (255 - $grey[$x][$y])) / 2;


Теперь уже можно со спокойной совестью превратить пересчитанный массив $grey в изображение при помощи функции:

imagesetpixel($igrey, $x, $y, $color[$grey[$x][$y]]);


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

Для этой цели заводим еще одно изображение, $itransp, на котором рисуем белым цветом кружок, делаем остальной фон прозрачным, и функцией imagecopymerge копируем полученный шаблон на $igrey:

$itransp = imagecreatetruecolor(240, 400);
$black_color = imagecolorallocate($itransp, 0,0,0);
$white_color = imagecolorallocate($itransp, 255,255,255);
imagefill($itransp, 0,0, $white_color);
imagefilledellipse($itransp, 120, 300, 200, 200, $black_color);
imagecolortransparent($itransp, $black_color);
imagecopymerge($igrey, $itransp, 0,0,0,0, 240, 400, 100);
imagedestroy($itransp);


Ура, каким-то чудом медаль получена.

2. Теперь осталось самое простое - нарисовать колодку. Собственно, она представляет собой закрашенный многоугольник, единственная тонкость - нанести на него какую-нибудь фактуру, чтобы было похоже на ткань. Для этой цели не придумалось ничего умнее, чем завести еще одно вспомогательное изображение $ibarpattern, в котором нарисовать фактуру ткани, и потом при помощи функции imagecopymerge наложить эту фактуру на колодку:

$ibarpattern = imagecreatetruecolor(240, 5);
$black_color = imagecolorallocate($ibarpattern, 0,0,0);
$white_color = imagecolorallocate($ibarpattern, 255,255,255);
imagefill($ibarpattern, 0,0, $white_color);
imagecolortransparent($ibarpattern, $white_color);
$dy = 0;
for ($i = 0; $i < 240; $i++)
{
$dy++;
if ($dy >=10) $dy = 0;
imagesetpixel($ibarpattern,
$i,
($dy < 5) ? 2 : 3,
$black_color);
imagesetpixel($ibarpattern,
$i,
($dy < 5) ? 1 : 4,
$black_color);
}
for ($i = 0; $i < 190; $i += 5)
imagecopymerge($ibar, $ibarpattern, 0, $i, 0, 0, 250, 5, 10);
imagedestroy($ibarpattern);


И вот - наконец-то! - после всех этих ухищрений получается то, что нужно:



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

Комментариев нет:

Отправить комментарий