GradFun2DBmod.

Бандинг убери!

Предисловие

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

Поэтому постараюсь рассказать, как оно вообще работает, за что отвечают параметры, и как его лучше использовать.

А зачем оно надо?

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

Проще говоря, в близком к идеальному случае он должен превращать левую картинку в правую.

overlap overlap

В первую очередь бандинг появляется после сильных шумодавов или уменьшения битрейта рипа, но иногда может быть и на самом сорце. Если бандинг на сорце, то он может быть совсем не бандингом, а просто "стильной рисовкой". Что давить, а что нет - решать вам. ;)

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

Алгоритм работы

Не смотря на то, что скрипт довольно большой, сам "вычислительный блок" очень прост.
dither  = mode==1  ? input.GF_Borders(thr,thrC)
\       : mode==2  ? input.GF_Padding(thr,thrC)
\       : mode==3  ? input.GF_Mirrors(thr,thrC)
\       :            input.GF_ModeOff(thr,thrC)


### GRAIN
AGmask = adapt==0   ? input.removegrain(19,-1)
\      : adapt==255 ? input.invert().removegrain(19,-1)
\      :              input.mt_lut("x "+string(adapt)+" - abs 255 * "+string(adapt)+" 128 - abs 128 + /",u=1,v=1).removegrain(19,-1)

grain  = custom=="empty" ? dither.addgrainC(str,strC,0,0)
\      :                   Eval("dither." + custom)
diff   = custom=="empty" ? blankclip(dither,color_yuv=$808080).addgrainC(str,strC,0,0)
\      :                   Eval("blankclip(dither,color_yuv=$808080)." + custom)

grain  = temp==0   ? grain
\      : temp==100 ? mt_makediff(dither,diff.temporalsoften(1,255,chr?255:0,255,2),u=chr?3:2,v=chr?3:2)
\      :             mt_makediff(dither,diff.mergeluma(diff.temporalsoften(1,255,chr?255:0,255,2),temp/100.0),u=chr?3:2,v=chr?3:2)

deband = str==0.0 && strC==0.0 ? dither
\      : adapt==-1             ? grain
\      :                         mt_merge(grain,dither,AGmask,luma=chr?true:false,u=chr?3:4,v=chr?3:4)


### MASK
GFmask = radius==1 ? input.mt_edge(mode="min/max",thY1=0,thY2=255,u=1,v=1).mt_lut(expr="255 x 1 "+string(range)+" / * 2 ^ /",u=1,v=1).removegrain(19,-1)
\      :             mt_luts(input,input,mode="range",pixels=mt_square(radius),expr="y",u=1,v=1).mt_lut(expr="255 x 1 "+string(range)+" / * 2 ^ /",u=1,v=1).removegrain(19,-1)

output = mask==true   ? mt_merge(input,deband,GFmask,luma=chroma?true:false,u=chroma?3:2,v=chroma?3:2)
\      : chroma==true ? deband
\      :                deband.mergechroma(input)
Не буду разбирать параметры отдельно, а сразу покажу, как и в чем они задейстованы, поэтому просто разберем каждый блок обработки отдельно.
dither  = mode==1  ? input.GF_Borders(thr,thrC)
\       : mode==2  ? input.GF_Padding(thr,thrC)
\       : mode==3  ? input.GF_Mirrors(thr,thrC)
\       :            input.GF_ModeOff(thr,thrC)
В зависимости от параметра mode работает один из внутренних блоков для дитера. Алгоритм задействования в них плагина gradfun2db примерно одинаков, и выглядит так:
  Function GF_ModeOff(clip clp, float thr, float thrC)
   {
   LUM = CLP.gradfun2db(thr)
   CHR = CLP.gradfun2db(thrC)

   GFO = thr==1.0 && thrC==1.0 ? CLP
   \   : thr==thrC             ? LUM
   \   : thr!=1.0 && thrC==1.0 ? LUM.mergechroma(CLP)
   \   : thr==1.0 && thrC!=1.0 ? CLP.mergechroma(CHR)
   \   :                         LUM.mergechroma(CHR)

   Return GFO
   }

На исходный кадр, с помощью gradfun2db, накладывается дитер, а затем всё это соединяется в один клип с помощью условных операторов. Условия проверяют равенство значений параметров thr и thrC (дитер по люме и хроме соответственно) и единицы. На самом деле, если обработать клип с помощью gradfun2db с thr = 1, то бандинга станет больше, чем было раньше.

Остальные блоки отличаются лишь тем, как к кадру добавляются границы. Это нужно для избежания большой проблемы дитера - он игнорирует крайние 16 пикселей кадра, поэтому для того, чтобы заставить его обработать весь кадр, он сначала увеличивается на 32 пикселя по горизонтали/вертикали, потом дитерится, а затем кропается.

overlap overlap overlap overlap overlap

Выбор мода влияет только на крайние 16 пикселей, и при небольших значениях thr практически не принципиален. Имхо лучше использовать 2 или 3, благо скорость у всех очень большая.
AGmask = adapt==0   ? input.removegrain(19,-1)
\      : adapt==255 ? input.invert().removegrain(19,-1)
\      :              input.mt_lut("x "+string(adapt)+" - abs 255 * "+string(adapt)+" 128 - abs 128 + /",u=1,v=1).removegrain(19,-1)
Блок маски адаптации. При adapt=0 на светлые места будет наложено меньше шума, на темные - больше, в зависимости от яркости самого пикселя. Adapt = 255 действует наоборот. Промежуточные значения расчитываются по формуле
     255 * |x - adapt|
y = -------------------
     |adapt - 128|+128
Если представить себе график функции, то ясно, что все точки с яркостью, равной параметру adapt будут установлено в 0, а остальная часть функции будет возрастающей от разницы между значением пикселя и параметром adapt. Да, очень наркоманский параметр, проще увидеть его работу на самой маске (светлые места будут менее зашумлены, чем тёмные). А вообще дефолт - довольно неплохое значение, но с ним надо быть аккуратным. На темные места будет наложено больше шума, а значит, больше вероятность чего-нибудь затереть. Там, конечно, находится и большая часть бандинга, но так же и самые трудноуловимые линий.

overlap overlap

 grain  = custom=="empty" ? dither.addgrainC(str,strC,0,0)
\      :                   Eval("dither." + custom) 
Тут всё просто. В зависимости от того, определен ли свой генератор шума или нет, на клип будет наложен шумок. Стандартно используется addgrainC, генерирующий полностью некоррелированный во времени шум с силой str по люме и strC по хроме. Клип этот понадобится в дальнейшем.
diff   = custom=="empty" ? blankclip(dither,color_yuv=$808080).addgrainC(str,strC,0,0)
\      :                   Eval("blankclip(dither,color_yuv=$808080)." + custom)
Немного другой блок генерирования шума. Работает так же, но в отличии от предыдущего блока шум накладывается на серый клип, в результате чего получается примерно следующий кадр.

overlap overlap

Конечно, никто не будет использовать str=100, параметр выбран исключительно для наглядности.
grain  = temp==0   ? grain
\      : temp==100 ? mt_makediff(dither,diff.temporalsoften(1,255,chr?255:0,255,2),u=chr?3:2,v=chr?3:2)
\      :             mt_makediff(dither,diff.mergeluma(diff.temporalsoften(1,255,chr?255:0,255,2),temp/100.0),u=chr?3:2,v=chr?3:2)

Основной блок темпоральности. При temp=0 результирующий кадр будет представлять собой сгенерированный чуть выше клип grain, содержащий полностью некоррелированный шум, наложенный на исходную картинку дитера.

При temp != 0 происходят более сложные вычисления. Первым делом, клип diff, содержащий шум на сером фоне, проходит через временной шумодав, который работает как некий "усреднитель", не удаляя полностью шум, но немного упорядочивая его во времени. Затем, в зависимости от параметра temp, обычной пропорцией компонента упорядоченного во времени клипа накладывается на некоррелированный шум, тем самым образуя нечто среднее между постоянным и непостоянным шумом (при temp=100 просто берется отшумодавленный клип).

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

Изначально шумовая компонента для каждого пикселя представляет собой примерно следующую формулу: y = x ± random, где x - значение пикселя входного клипа, y - выходного клипа, random - некая случайная составляющая. Т.к. шум в клипе diff изначально накладывался на клип, где у каждой точки значение по всем плоскостям было 128 (80 в шестнадцатеричной системе = 128 в десятичной), то значения выходного клипа diff будут лежать в пределах y = 128 ± random. Вспомним, что функция mt_makediff работает по формуле z = x - y + 128, где x - значение пикселя первого клипа, y - второго, z - выходного клипа. Подставляя вместо y значение второго клипа, расчитанное ранее, получим z = x - 128 ± random + 128 = x ± random, только теперь random несколько упорядочен во времени.

Параметр chr, определяемый выражением chr = strC > 0.0 ? true : false, указывает на то, будет ли хрома браться из исходного клипа дитера, либо так же будет содержать добавленный шум.
deband = str==0.0 && strC==0.0 ? dither
\      : adapt==-1             ? grain
\      :                         mt_merge(grain,dither,AGmask,luma=chr?true:false,u=chr?3:4,v=chr?3:4)
Тут всё очень просто. Если параметры str и strC равны 0, т.е. шума не добавлялось, то на выход пойдет клип дитера. Если adapt=-1, т.е. не надо использовать маску наложения зерна, то на выход пойдет клип grain, расчитанный ранее. Во всех остальных случах, клип dither будет наложен на клип grain по маске AGmask, описанной выше. Здесь надо остановиться и подумать - наложение щума, фактически, "инвесное", т.е. там, где маска светлее, шума будет меньше, а не наоборот. Если chr = 0, то хрома полностью берется из второго клипа, если не равно - на неё так же накладывается шум.
GFmask = radius==1 ? input.mt_edge(mode="min/max",thY1=0,thY2=255,u=1,v=1).mt_lut(expr="255 x 1 "+string(range)+" / * 2 ^ /",u=1,v=1).removegrain(19,-1)
\      :             mt_luts(input,input,mode="range",pixels=mt_square(radius),expr="y",u=1,v=1).mt_lut(expr="255 x 1 "+string(range)+" / * 2 ^ /",u=1,v=1).removegrain(19,-1)

Вот мы и добрались до главного тормоза всего скрипта - маски границ. Первым делом расчитывается маска mt_edge, работающая по методу "min/max" (разница между максимальным и минимальным значением в области). При radius=1 маска будет расчитываться для матрицы 3х3, radius = 2 - 5x5, 3 - 7x7. А теперь представьте, какое количество времени потребуется, чтобы для 2 073 600 пикселей (1920х1080) посчитать максимальное и минимальное значение среди 49 значений (radius=3 (немного грубо, ибо для крайних точек матрица считается не полностью, но погрешность небольшая)). Львиная доля времени работы всего скрипта уходит именно на расчет подобной маски (радиус по умолчанию равен 2, т.е. 25 значений для каждого из 2х миллионов пикселей). Поэтому, имхо, очень часто можно установить radius в 1, а силу маски регулировать с помощью параметра range.

overlap overlap

----Добавлено: с помощью некоторых оптимизаций процесс расчета этой маски можно ускорить до практически реалтаймого почти на любых разрешениях и почти с любыми радиусами. Но это уже другая история и оффициальной версии скрипта не касается. После расчета маски она пропускается через mt_lut, работающего по формуле
      255 * (range ^ 2)
y = -------------------
	 (x ^ 2)
Маска инвертируется (x стоит в знаменателе), т.е. при большем значении x на выходе получатся меньше значения y (чем ярче была точка (линия) в маске границ, тем темнее она станет после прохождения через mt_lut). Параметр range действует обратным образом - чем он больше, тем более светлые значения получатся в выходном клипе, и наоборот. Затем всё это пропускается через removegrain, 19й мод которого представляет собой блюр, при котором значение пикселя заменяется на среднее из 8 соседних пикселей. Выходной клип выглядит примерно так.

overlap overlap

overlap overlap

output = mask==true   ? mt_merge(input,deband,GFmask,luma=chroma?true:false,u=chroma?3:2,v=chroma?3:2)
\      : chroma==true ? deband
\      :                deband.mergechroma(input)
Финальное вычисление. При значении mask=true клип deband накладывается на исходный клип по маске границ, полученной ранее, т.е. на границы приходится значительно меньше шума/дитера, чем на плоскости. При параметре chroma = true и mask=false возвращается просто клип deband, без использования маски границ. Не стоит этого делать, право. Если же оба этих параметра равны false, то на выход идет новый клип, содержащий яркость клипа deband и цветность источника, т.е. по хроме скрипт не работает (в случае, если strC = 0 или thrC <= 1).

overlap overlap

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

Надеюсь, я принёс в мир еще немного пользы и добра.

(с) tp7