Как стать автором
Обновить

Создание вращающегося логотипа с помощью ImageMagick и FFMPEG

Время на прочтение9 мин
Количество просмотров20K
Статья может заинтересовать тех, кто хочет слегка оживить оформление своего видеоканала, а также тех, кто только начал работать с указанными в заголовке инструментами. Уверенные же пользователи, рассчитываю, дополнят мой материал.

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



Для выполнения задачи понадобится система с установленными imagemagick и ffmpeg. Для формирования команд утилите imagemagick будет использоваться bash. Что повлечет за собой использование утилиты bc, т.к. математика в bash довольно простая и тригонометрические функции вычислить затруднительно.

С помощью python, например, тоже самое сделать еще проще, в нем-то синус и косинус вычисляются.

Для создания видео создадим N кадров, за которые логотип сделает полный оборот, затем превратим их в видеофайл.

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

Начальное состояние для нас — это видимый в полный размер логотип, поэтому для изменения ширины изображения будем умножать его ширину на косинус (его значение в нуле как раз равно единице).

Вращение изображения для данных целей можно поделить на 4 этапа:
— изображение сужается;
— расширяется, но к зрителю повернуто обратной стороной;
— сужается обратной стороной к зрителю;
— расширяется до исходных размеров.

Сформируем базовый набор кадров построим для вращения на 0-90 градусов, а кадры для остальных 270 получим немного их преобразовывая. Цикл, соответственно, будет не от 0 до 360 градусов, а от 0 до 90.

Пропишем путь к нашему логотипу, чтобы потом обращаться к нему коротким $logo:

logo=../../logo/Moto_Gymkhana_transparent.png

Для удобства введем переменные равные ширине и высоте логотипа, так будет проще подправить скрипт под другую картинку:

width=842
height=595

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

Frames=15

В цикле понадобится:
— вычислить, какова будет ширина изображения при его повороте на заданный переменной «n» угол;
— создать пустой холст размером, скажем 850x600;
— добавить в его центр сжатое по ширине изображение.

for (( n=0; n<=90; n+=$(expr $(( 90/$Frames )))))
do
oWidth=$(printf %.$2f $(echo "$width*(c($n*4*a(1)/180))+1" | bc -l))

convert -size 850x600 xc:transparent -background none \
\( -alpha set -channel A -evaluate add -60% $logo -geometry $oWidth\ x$height\! \)\
 -gravity center -composite ./tmp/$n.png
done


Разберем код по частям.

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

oWidth=$(printf %.$2f $(echo "$width*(c($n*4*a(1)/180))+1" | bc -l))

Удобнее будет разбирать этот кусок справа налево:
"| bc -l" указывает, что вычисляться это будет утилитой bc.
Т.к. косинус в утилите «bc» принимает на вход значения в радианах, а числа пи в bash нет — используем следующее выражение:
c($n*4*a(1)/180) — где
«c» — косинус,
$n — угол в градусах узменяющийся в цикле;
a(1) — арктангенс единицы, равный пи/4. Поэтому 4*a(1) это способ записать число пи. На 180 делим чтобы перейти от радиан к градусам.
Таким образом, "$width*(c($n*4*a(1)/180))" — ширина умноженная на косинус угла «n».
"+1" после этого выражения для того, чтобы ширина изображения не принимала значения 0. Попытку сделать изображение нулевой ширины не поймет ImageMagick.
$(printf %.$2f $(echo "$width*(c($n*4*a(1)/180))+1" | bc -l)) — вычислить значение ширины для данного угла и вывести, округлив до целого.

convert -size 850x600 xc:transparent -background none \
\( -alpha set -channel A -evaluate add -60% $logo -geometry $oWidth\ x$height\! \)\
 -gravity center -composite ./tmp/$n.png

convert -size 850x600 xc:transparent -background none — создать прозрачный холст размером 640x360 без заднего фона.

Последующая часть команды взята в экранированные "\" скобки чтобы она выполнялась изолированно. Это важно, иначе изменение размера коснется и холста.
-alpha set — включить канал прозрачности.
-channel A -evaluate add -60% — добавить к значению канала А всех пикселей изображения "-60%".
$logo- путь к изображению.
-geometry $oWidth\ x$height\! — изменить размер изображения.
"-geometry AxB" изменит размеры изображения, но сохранит соотношение сторон. Т.е. если мы попытаемся убавить ширину — высота изображения тоже уменьшится чтобы сохранить соотношение «ширина х высота», а выше и ниже появятся пустые области.
"-geometry AxB\!" не бедут пытаться сохранить соотношение «ширина х высота», а изменит их независимо друг от друга.
-gravity center — разместить посередине.
-composite — объединить изображения.
./tmp/$n.png — сохранить файл в папку «tmp», назвав текущим значением переменной «n».

Вид с угла в 54 градуса:



Из этого эталонного набора изображений создадим 4 кадра будущей анимации.

Пусть логотип вращается по часовой стрелке если смотреть сверху. Для придания объема изображению добавим «тень» сдвинутую вправо или влево в зависимости от того, куда на кадре вращается изображение. И отразим изображение слева-направо на кадрах, где логотип повернут к зрителю задней стороной. «Тень» на этих же кадрах будет добавлена не под исходное изображение, а поверх него, чтобы создать эффект оборотной стороны логотипа.

Рассмотрим подробно выражение для первой четверти поворота. Остальные будут понятны по аналогии. Лишь акцентирую внимание на отличиях от первого:

convert tmp/$n.png \
\( +clone -background '#cccf' -shadow 100x8+$shadowShift-3 \)\
 -background none  -compose Src_Over -layers merge \
-gravity center tmp/logo$(expr $(( 1000+n ))).png


convert tmp/$n.png — взять за основу исходное изображение.

\( +clone -background '#cccf' -shadow 100x8+$shadowShift-3 \) — склонировать и создать тень изображения.
Цвет заданный опцией background задаст цвет тени.
Насколько тень непрозрачна (в процентах) и размыта задается параметром «100x5».
Сдвиг тени относительно оригинального изображения задается как +x+y. В нашем случае сдвиг по вертикали постоянен (3 пикселя вверх), а сдвиг по горизонтали задается переменной shadowShift.
Значение этой переменной вычислим аналогично сжатию исходного кадра по вертикали. Только возьмем синус угла, а не косинус, т.к. сдвиг при нулевом угле должен быть ноль.

shadowShift=$(printf %.$2f $(echo «15*(s($n*4*a(1)/180))+1» | bc -l))
Для первой и второй четверти тень должна быть сдвинута вправо, поэтому значение shadowShift берем со знаком «плюс», для третьей и четвертой — со знаком «минус».

-background none -compose Dst_Over -layers merge -gravity center -depth 8 tmp/logo$(expr $(( 1000+n ))).png

Тут важная опция это -compose. Она задает какое из изображений будет расположено сверху.
Для первой четверти поворота, сверху располагается сам логотип, поэтому Dst_Over. Для второй и третьей четверти будет прописано Src_Over (тень сверху).

-layers merge -gravity center — совместить слои, расположить посередине.

tmp/logo$(expr $(( 1000+n ))).png — сохранить под именем logo + «1000+значение n» в папку tmp.

имя такое чтобы имена кадров сразу отсортировались в алфавитном порядке и их было удобно отдать на обработку ffmpeg'у.

Для кадров второй четверти поворота будет «1800-n», для третьей «2000+n», для четвертой «2800-n».

Для второй и третьей четвертей появится опция -flop — отразить изображение слева-направо. Из-за этого, кстати, понадобится изменить знак смещения тени на противоположный.

Код для всех четырех четвертей поворота:

convert tmp/$n.png \( +clone -background '#cccf' -shadow 100x8+$shadowShift-3 \) \
-background none  -compose Dst_Over -layers merge -gravity center -depth 8 tmp/logo$(expr $(( 1000+n ))).png

convert tmp/$n.png \( +clone -background '#cccf' -shadow 100x8-$shadowShift-3 \) \
-background none -compose Src_Over -layers merge -gravity center -flop tmp/logo$(expr $(( 1800-n ))).png

convert tmp/$n.png \( +clone -background '#cccf' -shadow 100x8+$shadowShift-3 \) \
-background none  -compose Src_Over -layers merge -gravity center -flop tmp/logo$(expr $(( 2000+n ))).png

convert tmp/$n.png \( +clone -background '#cccf' -shadow 100x8-$shadowShift-3 \) \
-background none -compose Dst_Over -layers merge -gravity center tmp/logo$(expr $(( 2800-n ))).png


В результате из исходного кадра получены 4 с небольшими отличиями:



Теперь дело за малым, собрать из этих изображений видео:

ffmpeg -pattern_type glob -i 'tmp/logo*.png' -pix_fmt argb -vcodec qtrle -r 30 rotatingLogo.mov

-pattern_type glob — позволяет задать маску имени кадра в привычном по консоли виде
-i 'tmp/logo*.png' — взять как исходные данные изображения из папки tmp чье имя начинается с «logo»
-pix_fmt argb — задать формат изображения с прозрачностью. «a» в ARGB — это как раз альфа-канал
-vcodec qtrle — задать кодек для видео поддерживающий прозрачность. Из известных мне это «qtrle» и «png»
-r 30 — задать частоту итогового видео 30 кадров в секунду.
rotating_logo.mov — сохранить под именем «rotatingLogo.mov»

Теперь можем выполнить команду
ffplay rotatingLogo.mov
и посмотреть на результат:



Получившееся видео длинной всего в пару секунд, т.ч. совместим множество таких кусков вместе.
Для этого используем тот же ffmpeg.

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

ffmpeg -loop 1 -pattern_type glob -i 'tmp/logo1000.png' -pix_fmt argb -vcodec qtrle -r 30 -t 3 stillLogo.mov

В этой команде появилась опция "-loop 1" — указывающая ffmpeg повторять входную последовательность изображений (в данном случае одно единственное), и опция "-t 3" указывающая, что продолжительность итогового видео 3 секунды.

Чтобы склеить видео с неподвижным и вращающимся логотипом, создадим файл со списком файлов, которые нужно склеить. В данном случае 20 экземпляров наших файлов:
for (( i=0; i<20; i+=1 ))
do
echo file 'stillLogo.mov' >> list.txt
echo file 'rotatingLogo.mov' >> list.txt
done


Подадим этот файл на вход фильтру concat с указанием копировать кодеки, а не перекодировать (-c copy).

ffmpeg -f concat -i list.txt -c copy logoWithRotation.mov




Осталось наложить получившийся логотип на видео.
Логотип довольно большой, т.ч. при наложении отмасштабируем его, уменьшив в три раза.

ffmpeg -i video.mov -i logoWithRotation.mov \
-filter_complex "[1:0]scale=iw/2:ih/2[logo];[0:0][logo]overlay=shortest=1:x=20:y=500" \
-vcodec libx264 -crf 18 overlay.mov


ffmpeg -i video.mov -i logoWithRotation.mov — взять на вход два видео.

-filter_complex — использовать filter_complex которому мы указываем, следующее:
[1:0]scale=iw/3:ih/3[logo] — взять поток 0 из второго видео (1 отсчет идет с нуля) и уменьшить значения ширины (iw — input width) и высоты (ih — input height) втрое. Передать дальше под именем «logo».

[0:0][logo]overlay=shortest=1:x=20:y=0 — взять поток 0 из первого видео и поток «logo» и наложить их друг на друга.
«shortest=1» — говорит, прекратить по достижении конца любого из потоков.
x=20:y=500 — указывает где располагать левый верхний угол накладываемого видео.

-vcodec libx264 -crf 18 — использовать кодек H264. Значение после crf указывает насколько сильно сжимать. 0 — вообще без сжатия.

overlay.mov — сохранить под этим именем.

Посмотреть результат:

ffplay overlay.mov






Рисунки и анимация для статьи созданы с помощью тех же инструментов. В порядке появления в статье:

ffmpeg -i rotatingLogo.mov -filter_complex "[0:0]scale=iw/3:ih/3" rotatingLogo.gif

montage tmp/54.png -geometry 300x200 result.png

montage -mode concatenate -tile 2x2 tmp/logo1054.png tmp/logo1746.png tmp/logo2054.png tmp/logo2746.png -geometry 300x200 result.png

ffmpeg -i logoWithRotation.mov -t 5.5 -filter_complex "[0:0]scale=iw/3:ih/3" logoWithRotation.gif




Итоговый скрипт для создания вращающегося логотипа:

logo=../../logo/Moto_Gymkhana_transparent.png
Frames=15
width=842
height=595
mkdir tmp
for (( n=0; n<=90; n+=$(expr $(( 90/$Frames )))))
do
oWidth=$(printf %.$2f $(echo "$width*(c($n*4*a(1)/180))+1" | bc -l))
shadowShift=$(printf %.$2f $(echo "15*(s($n*4*a(1)/180))+1" | bc -l))
convert -size 850x600 xc:transparent -background none \( -alpha set -channel A -evaluate add -50% $logo -geometry $oWidth\ x$height\! \) -gravity center -composite -depth 8 ./tmp/$n.png
convert tmp/$n.png \( +clone -background '#cccf' -shadow 100x8+$shadowShift-3 \) -background none  -compose Dst_Over -layers merge -gravity center -depth 8 tmp/logo$(expr $(( 1000+n ))).png
convert tmp/$n.png \( +clone -background '#cccf' -shadow 100x8-$shadowShift-3 \) -background none -compose Src_Over -layers merge -gravity center -flop tmp/logo$(expr $(( 1800-n ))).png
convert tmp/$n.png \( +clone -background '#cccf' -shadow 100x8+$shadowShift-3 \) -background none  -compose Src_Over -layers merge -gravity center -flop tmp/logo$(expr $(( 2000+n ))).png
convert tmp/$n.png \( +clone -background '#cccf' -shadow 100x8-$shadowShift-3 \) -background none -compose Dst_Over -layers merge -gravity center tmp/logo$(expr $(( 2800-n ))).png
done
ffmpeg -pattern_type glob -i 'tmp/logo*.png' -pix_fmt argb -vcodec qtrle -r 30 rotatingLogo.mov
ffmpeg -loop 1 -pattern_type glob -i 'tmp/logo1000.png' -pix_fmt argb -vcodec qtrle -r 30 -t 3 stillLogo.mov
rm list.txt
for (( i=0; i<20; i+=1 ))
do
echo file 'stillLogo.mov' >> list.txt
echo file 'rotatingLogo.mov' >> list.txt
done
ffmpeg -f concat -i list.txt -c copy logoWithRotation.mov


Наложение на файл video.mov с уменьшением логотипа в три раза:

ffmpeg -i video.mov -i logoWithRotation.mov -filter_complex "[1:0]scale=iw/3:ih/3[logo];[0:0][logo]overlay=shortest=1:x=20:y=500" -vcodec libx264 -crf 18 overlay.mov
Теги:
Хабы:
+19
Комментарии22

Публикации

Изменить настройки темы

Истории

Ближайшие события

PG Bootcamp 2024
Дата16 апреля
Время09:30 – 21:00
Место
МинскОнлайн
EvaConf 2024
Дата16 апреля
Время11:00 – 16:00
Место
МоскваОнлайн
Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн