суббота, 21 сентября 2013 г.

Основы Avisynth (часть 2)


Опубликовано в ЖЖ в апреле 2011 года, актуально для AviSynth 2.58
Оригинал

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


Мы можем сделать это, записав вот такую строчку:
            Trim(x1,x2) + Trim(x3,x4) + …
Писать такое «стадо» функций утомительно. Даже если заранее записать десяток таких функций в скрипте-шаблоне, а потом только заполнять, это утомительно. Тем более, если записать в шаблоне Trim()+… (без запятой), запятые придется расставлять самому. Запятые в русской и латинской раскладках находятся на разных клавишах… Так что расстановка запятых будет крайне раздражительным занятием. Согласитесь, гораздо приятнее было бы записать параметры «обрезки» вот так:
            Multitrim(“x1 x2 x3 x4 …”)
Несмотря на кажущуюся фантастичность затеи, реализовать такую функцию с произвольным количеством параметров можно. В этом как раз и поможет рекурсивный вызов функции.
Пользовательская функция может быть определена до или после того, как будет первый раз вызвана. Определение начинается со слова function, за которым в фигурных скобках следует список параметров (с указанием типа). В фигурных скобках содержится тело функции. Любая функция возвращает какое-либо значение, поэтому должна содержать оператор return(). Не обязательно возвращать только значение типа clip, это может быть значение любого из пяти типов. Простейшая функция, таким образом, имеет вид:
            function DoNothing(clip c)
            {
                        return (c)
            }
Такая функция ничего не делает, возвращает клип без какой-либо обработки. Если вы еще помните третье формальное правило, которое приведено выше, поймете, почему вызов DoNothing() без параметров не вызовет никаких ошибок. Вообще параметр типа clip может быть любым по счету в списке параметров. Переменная last будет передана неявно первому параметру типа clip.
Изменим список параметров таким образом:
            function DoNothing(clip c, int x)
Теперь вызвать эту функцию без параметров не получится, поскольку переменная типа int функции неявно не передается. Оба следующих вызова будут допустимыми:
            DoNothing(c1, 0)
            DoNothing(0)
Порядок вызова параметров имеет значение. Например:
            DoNothing(0, c1)
AviSynth сообщит об ошибке в строке, в которой находится этот вызов.
Указывать все параметры функции, особенно когда их много, занятие довольно утомительное. Именно поэтому есть возможность использовать в функциях необязательные параметры. Необязательные параметры указываются в кавычках. Разница между обязательным и необязательным заданием параметра с проявится только в случае, когда переменная last будет не определена. Изменим список параметров, сделав х необязательным параметром:
            function DoNothing(clip c, int “x”)
В этом случае мы вновь будем иметь возможность вызвать такую функцию без параметров. Однако использование в функции переменной х без информации, присвоено ей значение или нет, невозможно. Такую информацию можно получить с помощью функции Defined(). Она возвращает true, если переменная была определена. Например:
            x= (Defined(x)) ? x : 0
Здесь переменной х присваивается значение 0, если она не определена. В противном случае ее значение не изменяется. Таким образом мы можем задать значение х «по умолчанию» и избежать ситуаций, когда эта переменная будет не определена. Для такого случая есть и более краткая форма с использованием функции Default():
            x=Default(x,0)
Выполняется то же действие, что и в предыдущем примере.
В списке параметров все необязательные параметры должны быть указаны после обязательных (правило, возможно, заимствованное из Си++).
При вызове функции по каким-либо причинам мы захотим указать явно, о каком именно параметре идет речь. На примере это будет выглядеть так:
            DoNothing(c=c1, x=10)
То есть мы можем указывать параметры в любом порядке. Допустим, в данной функции у нас пять необязательных параметров типа int, именованных x1..x5. Есть такой вызов:
            DoNothing(c=c1, x5=10)
Не будь здесь явного указания x5=10, значение 10 было бы присвоено переменной х1. А теперь вызовем ту же самую функцию вот так:
            DoNothing(6, 7,  x4=11)
Ошибки не будет, поскольку первой неявно передана переменная last. Значения 6 и 7 будут присвоены соответственно х1 и х2, а присвоение для х4 мы указали явно.
Ознакомившись с правилами построения функции, напишем нашу функцию Multitrim(). Начнем с определения спискапараметров:
            function Multitrim(clip c, string "line", string "splitter", string "splice")
Такой порядок и такая «обязательность» параметров мне показались наиболее удобными и безопасными. По замыслу, такая функция без параметров должна возвращать такой же клип, какой и был ей передан. В line будет содержаться строка, состоящая из номеров кадров, по которым «резать». В параметре splitter будет возможность указать разделитель чисел, чтобы в этом качестве можно было использовать не только пробел. Параметр splice мы можем указать, чтобы наша функция могла применять операторы «+» и «++» (в чем разница между ними, вы можете узнать из справки по AviSynth). Неплохо бы предусмотреть и «защиту от дурака», чтобы функция не была источником мелких, но очень неприятных ошибок. Для начала определим значения «по умолчанию»:
            line=Default(line,"")
            splitter=Default(splitter," ")
            splice=Default(splice,"+")
Следом опишем «единичную» операцию выделения одного «кусочка» видео. Нам нужны две переменные типа int, чтобы они могли быть параметрами функции trim():
            stop=false
            i=findstr(line,splitter)
            stop= (i==0) ? true : false
            fromstr= stop ? line : leftstr(line, i-1)
            line= stop ? "" : midstr(line, i+1)
            from=int(value(fromstr))
           
            i=findstr(line,splitter)
            stop= (i==0) ? true : false
            tostr= stop ? line : leftstr(line, i-1)
            line= stop ? "" : midstr(line, i+1)
            to=int(value(tostr))

Переменная stop служит индикатором, есть ли еще разделители в строке, которые нужно обработать. В любом случае, будет сделана попытка извлечь очередное число из строки. Будет возвращен 0, если такая попытка закончится неудачей. Какое бы число параметров ни было в строке, извлечено будет четное число параметров. Результаты будут помещены в переменные from и to. Можно записать с десяток таких повторений, но хотелось бы сделать функцию более гибкой. Следующей строкой будет такое присвоение:
            a=trim(c, from, to)
Мы вырезали один «кусок». Теперь его нужно прибавить к остальным, если он был не первым. Разумеется, в цикле сделать такое совсем несложно. Но ведь организовать цикл нельзя. Обойдём это ограничение, организовав в данной функции «память». В роли «памяти» переменная mem типа clip. Добавим ее в список параметров последней – для «ручного» вызова она использоваться не будет. Теперь можно добавить в функцию следующее:
            mem= (mem==cnoop) ? a : eval("mem"+splice+"a")
Такая «заумная» конструкция используется только потому, что в AviSynth нет возможности указать «нулевое» значение для переменной типа clip. Итак, если mem равняется некоторому нулевому значению, то ей присваивается значение вырезанного «кусочка». Если mem представляет собой «сумму» нескольких «кусочков», которая была получена при предыдущих вызовах функции, то вырезанный «кусочек» будет добавлен к mem. Функция Eval() выполняет содержимое параметра типа string, переданного ей. В зависимости от значения splice, будет выполнено mem+a или mem++a. Чтобы выражение mem==cnoop было осмысленным, следует определить переменную cnoop, а ещё определить значение «по умолчанию» для mem, которое будет использоваться при «ручном» вызове функции. Сделаем это, добавив такие строчки в начале функции:
            cnoop=BlankClip(c, length=1, color=$ABDE00)
            mem=Default(mem, cnoop)
Клип cnoop с такими параметрами вряд ли мы будем использовать в реальной жизни.
Завершаем функцию вот такими строками:
            mem= stop ? mem : multitrim(c, line, splitter, splice, mem)
            return(mem)
Тут вроде все понятно. Если в строке больше не осталось разделителей, а последнее значение мы уже «декодировали», нужно завершать функцию, выдав результат. В противном случае запускаем ту же самую функцию с остатками строки и памятью. Такая функция сможет «читать» любое количество параметров. Но неплохо бы защитить ее от «случайных» параметров. В простейшем случае, строка line должна содержать строго один разделитель между цифрами, не заканчиваться и не начинаться с разделителя. Символ splitter должен быть именно символом, а не строкой, а строка splice должна содержать только «+» или «++». Если со splitter и splice все просто, то для «очистки» line понадобится отдельная функция. Окончательнофункции будут выглядеть так:
            function multitrim(clip c, string "line", string "splitter", string "splice", clip "mem") 
            {         
                        cnoop=BlankClip(c, length=1, color=$ABDE00)
                        mem=Default(mem, cnoop)
                        line=Default(line,"")
                        splitter=Default(splitter,"")
                        splitter= (splitter==””) ? “ “ : splitter
                        splitter=leftstr(splitter,1)
                        splice=Default(splice,"+")        
                        splice= (splice!="+"&&splice!="++") ? "+" : splice
                        line=sdupfilter(line,splitter)
                       
                        stop=false
                        i=findstr(line,splitter)
                        stop= (i==0) ? true : false
                        fromstr= stop ? line : leftstr(line, i-1)
                        line= stop ? "" : midstr(line, i+1)
                        from=int(value(fromstr))
                       
                        i=findstr(line,splitter)
                        stop= (i==0) ? true : false
                        tostr= stop ? line : leftstr(line, i-1)
                        line= stop ? "" : midstr(line, i+1)
                        to=int(value(tostr))
                       
                        a=trim(c, from, to)
                        mem= (mem==cnoop) ? a : eval("mem"+splice+"a")
                        mem= stop ? mem : multitrim(c, line, splitter, splice, mem)
                        return(mem)
            }

            function sdupfilter(string line, string splitter)
            {
                        i=findstr(line, splitter)
                        line= (i==1) ? midstr(line,2) : line
                        j=strlen(line)
                        tmp=rightstr(line,1)
                        line= (tmp==splitter) ? leftstr(line, j-1) : line
                        k=findstr(line, splitter+splitter)
                        line= (k!=0) ? leftstr(line, k-1)+midstr(line, k+1) : line
                       
                        next= (i==1)||(tmp==splitter)||(k!=0)
                        line= next ? sdupfilter(line, splitter) : line
                       
                        return(line)
            }

Думаю, теперь вы сами в состоянии разобраться, где «защита от дурака» в первой функции, и каков алгоритм работы второй. Для удобства вы можете поместить такие функции в отдельный файл. А в вашем скрипте использовать функцию Import() для вставки содержимого этого файла в место вызова функции (по аналогии с директивой #include языка Си).
Память в созданной функции Multitrim можно было бы организовать и иначе, в виде глобальной переменной. Чтобы быть глобальной переменной, то есть чтобы любая функция могла видеть эту переменную, ее нужно объявить со спецификатором global (по аналогии со static в Си++). Еще один «тип» данных был введен специально для функций. Называется он val и обозначает либо int, либо float.
Помимо возможности написать свои собственные функции на базе имеющихся, есть еще возможность воспользоваться функциями из плагинов (подключаемых модулей). Перед началом использования «плагинной» функции нужно подключить плагин:
            LoadPlugin(“D:\VideoTools\AviSynth\plugins\DGDecode.dll”)
Документация по таким функциям обычно находится в архиве с плагином. Описание самых «известных» функций можно найти в справке по AviSynth.
Плагины AviSynth немного отличаются от плагинов Virtual Dub. Если «плагинные» фильтры Virtual Dub работают только с цветовым пространством RGB32, то «плагинные» функции AviSynth могут работать с несколькими цветовыми пространствами (перечень цветовых пространств можно найти в документации к каждой конкретной функции). AviSynth поддерживает четыре цветовых пространства: YV12, YUY16, RGB24, RGB32. На первый взгляд это кажется сложным. Однако это только на первый взгляд. При попытке «предложить» какой-либо функции клип в неподдерживаемом ею цветовом пространстве будет выдано соответствующее сообщение об ошибке. После этого вам нужно будет всего лишь добавить функцию преобразования цветового пространства. Согласитесь, не так уж сложно. Преобразовывать цветовое пространство «на всякий случай» не надо. Это может внести искажения, и негативно повлияет на скорость обработки. В общем случае фильтры AviSynth быстрее аналогичных Virtual Dub. Допустим, у нас есть аналогичные функции в Virtual Dub и AviSynth, с одинаковой скоростью работы. В AviSynth мы можем записать:
            DoSomething()
В Virtual Dub, кроме такой функции, будет выполнено преобразование цветового пространства в RGB32 и обратно. То есть аналогичный скрипт AviSynth выглядел бы так:
            ConvertToRGB32()
            DoSomething()
            ConvertToYV12()
Двойное преобразование цветового пространства, которое при использовании фильтров Virtual Dub является обязательным, и даёт тот самый проигрыш в скорости по сравнению с функциями AviSynth. Хотя тут возможны варианты. Многие пользователи и не подозревают, что даже когда они не используют фильтры Virtual Dub, двойное преобразование цветового пространства все равно выполняется, внося тем самым искажения. За такое преобразование «ответственна» опция Full Processing Mode.
Плагины Virtual Dub можно использовать в AviSynth, однако стоит это делать только в тех случаях, когда аналогов таких плагинов у AviSynth нету. Примеры, как правильно подгружать фильтры Virtual Dub, можно найти, например, по адресу http://avisynth.org/mediawiki/Shared_functions/VirtualDub_II.
В этом примере первой же описывается излюбленная функция новичков – добавление логотипа. Не более затратно такую функцию можно организовать и с помощью стандартных функций AviSynth.
            function addlogo(clip clip, string logof, string maskf, int x, int y, val opacity)
            {
                        over=imagesource(logof, end=0)
                        mask=imagesource(maskf, end=0)
                        clip=overlay(clip, over, x, y, mask, opacity/100.0)
                        return(clip)
            }
Вот так просто. Задали клип, изображение, маску, положение и прозрачность – и готово. Эта функция будет работать ничуть не хуже, чем фильтр из Virtual Dub. Внимательно читая справку по функции Overlay(), обнаружим, что она производит двойное преобразование цветового пространства в YUV и обратно. Поэтому целесообразнее было бы преобразовывать только ту часть кадра, на которую накладывается логотип. Доработать функцию addlogo() с учетом этого пожелания вы вполне можете самостоятельно.

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