分页: 1 / 6

【教程】 MaskTools入門教程

发表于 : 2012-06-22 7:12
06_taro
AviSynth作為視頻處理工具,非常重要的特性在於它是一個非線性的處理工具。非線的視頻編輯工具很多,不過非線的處理工具就很少了。如果在AviSynth裡仍然用filterA.filterB.filterC這種簡單的濾鏡堆積的話,AviSynth顯然比不上virtual dub、aviutl之類所見即所得的工具。而想要使用AviSynth非線性的處理,有兩個工具是非常重要的,spatial處理裡廣泛使用的masktools,以及temporal處理裡廣泛使用的mvtools。

雖然masktools並不算難用,使用到的原理相對來說都不算複雜,但遺憾的是對於本身不了解mask以及yuv的人來說,masktools的官方文檔並不適合入門,而目前也缺少對於masktools進行基礎介紹的文章,所以我一直想寫一篇masktools的入門教程。mvtools則略有不同。mvtools裡使用到的motion estimate算法對沒有相關專業知識的人來說並不容易理解,而這些基礎的理解程度對mvtools的實際使用有不小的影響。所以如果有時間的話必須要從最基礎的ME理論進行介紹。這種麻煩事嘛…嗯我還是回老家結(ry

下面這篇教程就是對masktools的入門級介紹。既然是入門,就不會牽扯到中高級的應用。實際上masktools如果了解基礎的話,官方文檔還是比較容易理解的。大部分人只是不熟悉其基本術語而已,一旦了解了,masktools就完全不是什麼有難度的工具了,絕大部分應用方法配合官方文檔都可以融會貫通。但是即使作為masktools的入門教程,在閱讀本文之前,還是應該確保有紮實的AviSynth語法及使用基礎,對於本文內出現的基本術語、AviSynth語法、內置濾鏡及常見的外部濾鏡應該已經熟練掌握,本文並不會過多介紹。其他基本要求只有最低限度初中水平以上的數學與英語基礎,以及對視頻相關理論的了解。可能的話,熟悉yuv顏色空間及編程相關知識會對本文的有一定的幫助。大部分情況下,我會使用實例來進行介紹。雖然對於部分不直觀的代碼配合實例會給出截圖,我還是希望在閱讀本文的同時能將所有工具都去自己實際應用一次,哪怕只是將本文的代碼直接複製出來看一遍效果,對實際理解都很有幫助。另外本文並不著力於完全替代官方文檔,即使在本文閱讀的過程中,最好還是配合官方文檔對應的函數說明部分一起查閱,因此對於具體的函數內有哪些參數,參數類型之類的,本文會從簡,以官方文檔為準。

目錄:
1. 了解Masktools
2. YUV colorspace
3. 從mt_merge到mt_lut
4. 深入淺出mt_lut(xy(z))
5. 基於mt_lut的強大工具——make/adddiff
6. mt_lut的擴展(一)——mt_lutf、mt_luts、mt_lutsx
7. 其他鄰域處理(morphologic filter)——mt_ex/inpand、mt_in/deflate
8. mt_lut的擴展(二)——mt_lutspa
9. 回歸mask——mask製作函數mt_edge與mt_motion
10. mask處理函數——hysteresis、logic、invert、binarize
11. convolution及其他mask處理技巧
12. 輔助函數——鄰域字符串生成工具

1. 了解Masktools

发表于 : 2012-06-22 7:12
06_taro
首先需要了解的是masktools是什麼。masktools,顧名思義就是處理mask的工具。如果對AviSynth內置的Layer、Overlay、RGB顏色空間或者其他非線工具有了解的應該不難理解mask的概念。如果不熟悉這些工具,可以直接跳過本段。AviSynth裡的Layer內部處理是在RGB下的,Layer根據RGBA顏色空間的alpha(透明度)通道來將兩個clip混合;而Overlay的處理是在YUV4:4:4下的,因此其blend模式不考慮alpha通道,而是靠自己定義的mask來作為blend的闕值,即Layer(clip_A, clip_B)相當於Overlay(clip_A, clip_B, mask=clip_B.ShowAlpha)。然而Overlay始終在YUV 4:4:4下處理,因此對於最常用的YV12格式中途需要先upsample到YUV 4:4:4再downsample回4:2:0,存在一定的質量損失。而masktools是一個對絕大多數planar YUV的顏色空間都可以直接將兩個clip進行混合,而不需要做中途轉換的工具。

masktools作為在YUV空間下的mask工具,目前有兩個版本:Masktools和Masktools2。這兩個都是manao大神的作品。Masktools2將Masktools的結構重新修改,目前已經可以完全替代Masktools。Masktools2裡的函數都冠以"mt_"的前綴,之後如不特別說明,提到的Masktools都是指Masktools2。本文撰寫時最新版的Masktools2是http://manao4.free.fr/這裡的masktools-v2.0a48。這裡面x86版本有兩個dll,分別是mt_masktools-25.dll與mt_masktools-26.dll。前者是給AviSynth 2.56~2.58使用的,可以處理YV12;而後者是給AviSynth 2.60~使用的,可以處理YV12、YV16、YV24與Y8等各種planar YUV格式,請根據自己使用的AviSynth版本選用對應的dll。

前面說了這麼多,對於不了解Layer/Overlay等工具的人來說最想知道的應該還是mask的具體作用。mask是給masktools應用中最為廣泛的merge用的,為了理解merge的概念,我們先來看一個實例(如果圖片過大顯示不全,請點擊在新窗口中查看):
图片
這幅圖裡,左上角的是mask clip,右上與左下分別是clip a與clip b,而右下的就是mt_merge(a, b, mask, luma=true)的結果。mask的作用是用黑與白將整個畫面分成兩部分;merge的效果就是根據mask的分割,將clip a用於mask內黑色的區域,將clip b用於mask內白色的區域,最終合成一張完整的圖片。

上面展示的就是merge最為基本的作用。先將圖片用mask分割成兩個部分,然後用merge從兩個clip中分別取出需要的部分,最後合成。例如我們想對一張圖片的左半邊做blur處理,而右半邊做sharpen處理,我們只需要用mask把左半邊設定為黑色,右半邊設定為白色,然後使用mt_merge(blur(1), sharpen(1), mask)就可以了。換句話說,如果我們想對圖片的不同部分做不同的處理,可以用mask區分,然後對分別處理後的畫面merge。這是很多視頻處理裡非常重要的方法。依靠這種方法,我們就可以不再簡單地被動接受對整個畫面filterA.filterB.filterC的濾鏡地圖炮,而可以對畫面的不同部分用不同的方式處理,以達到最合適的效果。

然而上面只是mask/merge最基本的使用。上面的mask裡,只有白與黑兩種顏色,分割出來的區域不是白就是黑,而merge過程對某個區域要么處於mask的白色之中使用clip b,要么處於黑色之中,使用clip a。實際上,mt_merge所使用的mask不一定只有黑白兩色。上面的白色對應的是YUV顏色裡的(255, 128, 128),而黑色對應的是YUV裡的(0, 128, 128)。mt_merge的做法是,mt_merge(a, b, mask)這個過程中,畫面上任何一個像素的結果,如果該像素在mask裡對應的值越高(越接近255),則越多地取clip b,在mask裡對應的值越低(越接近0),則越多地取clip a。在只有黑白兩色的mask裡,某個像素的結果要么是a的顏色,要么是b的顏色,而如果mask的值在1~254之間,則得到的結果是a與b按照比例的一個混合值。整個過程的核心是YUV的計算,所以讓我們首先來了解一下YUV顏色空間。已經熟悉YUV概念的可以直接跳過下一小節。

2. YUV colorspace

发表于 : 2012-06-22 7:13
06_taro
我們最為熟悉的顏色空間應該是RGB,利用R、G、B三種顏色,在8-bit的0-255的量化範圍內,通過組合實現256^3=16,777,216種色彩。YUV是另一種對顏色進行量化的方式。將R‘、G’、B‘三個信號進行線性組合,取出表達亮度(luma)的信息作為Y,而將表達色度(chroma)的信息作為Cb和Cr,從而實現將亮度和色度分開。這裡R、G、B與R’、G‘、B’有所不同,YUV與YCbCr的概念也不一樣,主要是需要考慮gamma校正以及模擬/數字信號的概念區分,由於和masktools的關係不大,本文內之後提及時不會再詳加區分,而是繼續使用YUV與RGB來表示,有興趣的可以以gamma correction、YUV、YCbCr、CIE 1931為關鍵字查閱相關資料。亮度與色度分開後帶來了很多好處,例如黑白電視機可以只使用亮度信息得到黑白畫面,而彩色電視機則同時使用亮度與色度,得到完整的彩色畫面,所以傳輸時統一傳輸YUV信號可以取得雙方兼容等。由於人眼對亮度信息更加敏感,在YUV的採樣過程中,UV的採樣可以不像RGB那樣對每個像素點都進行採樣,而是將相鄰像素點合併採樣,稱為chroma subsampling。如果保持和RGB一樣每個像素都採樣YUV三個平面的數據,則稱為4:4:4 subsampling。而如果是在

代码: 全选

P11 P12 P13 P14
P21 P22 P23 P24
P31 P32 P33 P34
P41 P42 P43 P44
這樣排列的相鄰16個點進行採樣時,Y的採樣按照每個點採樣一次,得到16個像素的數據,而U與V則僅在

代码: 全选

P11 --- P13 ---
P21 --- P23 ---
P31 --- P33 ---
P41 --- P43 ---
這8個位置進行採樣("---"表示不採樣的位置),即將同一行相鄰的兩個像素合併為一個採樣點,得到的U與V採樣精度為Y的一半,這種降低chroma採樣精度的採樣方式稱為4:2:2 subsampling。我們通常接觸到的DVD、HDTV或者BD一般都是4:2:0的採樣,這是一種在將相鄰的四個點(例如P11 P12 P21 P22四個點)合併採樣的方式。與4:2:2裡相鄰兩個像素內採樣點位置固定為左邊的做法不同的是,4:2:0有三種選取採樣點的標準(chroma position)。一種是MPEG-1標準的方式,在四個像素的正中心位置採樣,即採樣坐標為( (Xp11+Xp12)/2, (Yp11+Yp21)/2 );另一種是MPEG-2標準的方式,水平方向保持和4:2:2一樣採用左邊的像素點,而垂直方向採用兩行的中心位置,即坐標為( Xp11, (Yp11+Yp21)/2 )。需要注意的是,由於4:2:0裡UV並不像4:4:4或者4:2:2那樣每行都進行採樣,而是將相鄰兩行的合併,因此對於逐行的progressive與隔行的interlaced的採樣位置選取方式是不同的,畢竟interlaced裡相鄰行並不處於同一個場內,不應該簡單地合併。上述的MPEG-1與MPEG-2標準都是按照progressive的方式,因為我們在視頻處理流程裡用masktools處理的對象通常還是IVTC/deint之後的progressive視頻。interlaced方式的採樣點,以及4:2:0第三種chroma position的DV標準(主要用於interlaced)在此不作介紹。對subsampling與chroma position想有進一步了解的可以在這裡進行進一步閱讀。

了解了YUV的採樣,我們還需要了解YUV的量化。AviSynth裡YUV的量化範圍是Y、U、V均分別在[0, 255]範圍內。純黑的YUV值是(0, 128, 128),而純白的YUV值是(255, 128, 128)。在有些其他應用當中(例如計算模擬信號的YUV時)可能會採用Y取[0, 255]而UV取[-128, 127]的範圍,那種情況下純黑與純白的UV平衡值就不是128,而應該是0了。這種將256個量化值都完全使用的顏色範圍稱為PC range/PC level。但是,在視頻領域YUV數據常常並不填滿0-255這些值,而是Y取16-235,UV取16-240,這個將值域縮小後得到的顏色範圍稱為TV range/TV level。這種不使用完整值域上下兩端的目的是防止傳輸過程中信號溢出,同時可以在上下兩端保留作其他用途(譬如同步位)等等。標準製作的DVD、BD通常都是TV range的,因此像素點的YUV取值應該都是Y在[16, 235]而UV在[16, 240]內。但是必須注意的是,masktools裡mask clip是不考慮TV range的,mt_merge使用的mask始終是按照[0, 255]的範圍,因此如果我們要在mask的白色區域使用clip b而黑色區域使用clip a的話,應該讓mask裡的值為0或者255,而不是16與235,否則得到的結果是clip a與clip b按比例blend的圖像。

了解了YUV的顏色量化,最好能有比較方便地查看視頻裡YUV數值的方法。AviSynth的一個GUI——AvsPmod提供了這一功能。在AvsPmod的菜單欄裡,進入[Options]->[Program settings]裡打開設置,在Video選項卡內點擊[Customize video status bar],如圖示:
图片
在彈出的窗口內可以編輯預覽視頻時下方狀態欄顯示的內容,在裡面任意位置加入%YUV,則會在預覽時顯示鼠標所指像素的YUV值,也可以不用%YUV而選用%CLR,這時會根據視頻的colorspace來自動切換顯示,在RGB下顯示的是RGB值,在YUV下顯示的是YUV值,如圖示:
图片
點擊OK保存設定,之後打開avs的預覽時就能看到狀態欄裡顯示的yuv值:
图片

能夠觀察YUV的取值後,讓我們回到mt_merge,來看看這個函數的本質,以及它背後masktools裡最為核心的mt_lut、mt_lutxy與mt_lutxyz。

3. 從mt_merge到mt_lut

发表于 : 2012-06-22 7:14
06_taro
mt_merge(a, b, mask)的實際計算是,對每個像素點,分別取a、b、mask三個clip的YUV值(記為a、b、mask),然後用下面的式子計算:
result = ( (256 - mask) * a + mask * b + 128 ) / 256
由這個式子可見,mt_merge返回的每個像素點的結果都是a與b兩個clip按照mask的取值進行加權平均,mask決定的就是a與b在結果中分別佔據的權重;當mask為255時,結果基本上完全是b的像素值;當mask為0時,結果基本上完全是a的像素值。

由此可見,當我們使用mt_merge來混合兩個clip時,實際上這個處理的本質是取三個clip裡對應的像素點,然後逐個像素點進行一次yuv值的計算。mt_merge將計算的函數固定為上面的式子,實際上masktools還有更為通用的yuv計算方法,這就是mt_lut、mt_lutxy、mt_lutxyz。

mt_lut的核心用法是mt_lut(clip input, string "expr")。這裡input表示輸入的視頻,而expr是yuv計算使用的函數。在這個函數裡,input clip的yuv值被賦予一個可以在expr內直接使用的變量名"x"。例如,mt_lut(c, "x")得到的就是將c每個像素完全複製一遍的結果。

mt_lut使用的"expr"並非是我們熟悉的中綴表達式。對於一個算式,如果把運算符放在變量中間,例如加法 "x + x" 裡加號被置於兩個加數之間,這種表達式稱為中綴表達式(infix notation)。如果我們把運算符放在所有變量的後面,例如用 "x x +" 來表示,這樣的表達式稱為後綴表達式,或者逆波蘭表達式(reverse polish notation,之後簡寫為RPN)。如果熟悉編程的話應該比較熟悉這種RPN,在mt_lut的使用過程中也推薦使用這種原生的RPN方式,可以大幅減少函數裡為了修改運算優先級大量使用括號帶來的混亂。不過如果不熟悉編程的話,masktools也提供了一個簡單的中綴表達式轉為逆波蘭表達式的函數mt_polish。例如我們希望在mt_lut裡用 x*(x-1) 這樣的運算,則可以用
mt_lut("x x 1 - *")
或者
mt_lut( mt_polish("x * ( x - 1 )") )
這兩種方式來實現。masktools另外還有一個將逆波蘭表達式轉回中綴表達式的函數mt_infix,例如在不熟悉RPN的情況下想知道一個現成的RPN的意思,譬如"x x 1 - *",在AviSynth裡可以用
Subtitle( mt_infix("x x 1 - *") )
來顯示轉換之後的中綴表達式。

實際上mt_lut的參數裡,不僅僅有string "expr",還包括string "yexpr"、"uexpr"、"vexpr",這三個分別控制y、u、v三個平面的計算函數。在沒有單獨定義的情況下,則使用"expr"來代替。需要注意的是,masktools裡大部分的函數都有Y/U/V三個int型的變量,分別控制這三個平面的處理方式。這三個變量的取值範圍是[-255, 5],其用法是:
Y / U / V
= -255…0,則直接將該平面所有像素的值定義為這個設定值的絕對值。例如mt_lut(U=-128, V=-128)就是將所有像素UV的值都設定為128;
= 1,這個平面完全不處理,得到的結果裡這個平面的值沒有任何用處(可能是各種混亂的值,可能與輸入源完全沒關係),所以這個平面的數據在之後不能使用;
= 2,直接將輸入的第一個clip的值複製到結果內。例如在mt_merge(a, b, mask, U=2, V=2)裡,得到的U和V都是直接複製於clip a,而不進行處理;
= 3,進行處理。例如mt_lut("x 5 +", Y=3, U=3, V=3)就是將YUV三個平面的每個值都加5;
= 4,如果輸入參數有兩個或以上的clip的話,直接將第二個clip的值複製到結果內。例如mt_merge(a, b, mask, U=4, V=4)裡結果的UV直接複製於clip b;
= 5,如果輸入參數有三個clip的話,直接將第三個clip的值複製到結果內。例如mt_merge(a, b, mask, U=5, V=5)裡結果的UV直接複製於mask。
如果是mt_merge(a, b, mask, Y=2, U=4, V=5),則所有的像素,Y完全複製自a,U完全複製自b,V完全複製自mask;
如果是mt_merge(a, b, mask, Y=3, U=1, V=-128),則所有的像素,Y按照merge本身的算法,將a與b通過mask進行blend,而U裡的數據完全是不可用的垃圾,V的值始終是128。這裡的垃圾是指處理時該平面的數據是完全不用考慮的。這樣處理完全跳過該平面(連複製數據都不需要),從而可以提高處理速度。

masktools裡大部分處理用的函數的默認參數都是Y=3, U=1, V=1,也就是說Y正確處理,而UV完全不處理。

比較特殊的是在mt_merge裡,由於這個函數通常用於整個mask過程最末尾,需要返回能直接使用的最終結果,所以UV的默認值並非是會導致完全無法使用的1,而是Y=3, U=2, V=2,也就是說Y正確地進行merge,而U和V默認是直接取自第一個輸入的clip。因為masktools大部分merge的對象裡UV並不會有較大變化,或者不需要處理,這樣直接複製是安全的(但是像最前面舉的第一個例子裡,黑白的mask來混合兩個完全沒有關聯的視頻時肯定不行)。mt_merge裡還有一個比較特殊的參數bool "luma",這個參數的作用是是否將mask的Y用於merge過程中U和V的計算。默認luma=false,也就是說mask裡的Y只用於Y平面的計算,而UV平面仍然按照其設定值處理(或者跳過處理,或者直接複製自源,或者設定為固定值),如果是處理的話,則UV平面使用的值就是mask裡的UV對應像素的值;而當luma=true的時候,mask裡的Y不僅僅用於Y平面的處理,UV平面的計算所取的mask值仍然是mask裡的Y,並且參數裡的U和V無效,例如mt_merge(a, b, mask, luma=true, U=1, V=-128)裡,U平面的處理不會被跳過,V平面的值也不會被設定為128。在最前面舉的第一個例子裡,為了讓大家能明顯地看出mask的效果,mask是黑白的,UV平面都是128,而用了luma=true之後,得到的結果裡UV也是按照mask裡的Y進行加權平均,而不是按照mask裡UV的128將a和b直接取平均值。這種方式在很多mask裡非常重要,例如根據某個視頻的luma值來做自適應的deband,較暗的畫面做deband的程度較強,較亮的畫面deband的程度較弱,這時的mask就是原始clip,而我們希望merge過程的UV計算也是按照luma值,因此可以用mt_merge(input.f3kdb, input, input, luma=true)來實現。

masktools大部分函數裡還有一個參數,string "chroma",其用法是:
chroma
= "process",相當於 U = V = 3;
= "copy" 或 "copy first",相當於 U = V = 2;
= "copy second",相當於 U = V = 4;
= "copy third",相當於 U = V = 5;
= "0…255",相當於 U = V = 0…-255,直接將U和V設定為這個值。
由於U和V常常做同樣的處理,直接用chroma="x"可以方便地同步這兩個平面的處理。

4. 深入淺出mt_lut(xy(z))

发表于 : 2012-06-22 7:14
06_taro
現在讓我們看看mt_lut實際應用的例子。假如我們想對一個PC range的視頻做YC壓縮,轉成TV range的視頻,可以用這樣一個方式來實現:
[Codebox lang="avisynth"]
mt_lut(expr = "x 224 * 255 / 16 +",
\ yexpr = "x 219 * 255 / 16 +",
\ chroma = "process")
# yexpr: x * ( 235 - 16 ) / 255 + 16
# uexpr/vexpr: x * ( 240 - 16 ) / 255 + 16
[/Codebox]

再比如說,我們如果要將一個視頻裡,Y值超過128的像素不加限制地denoise,而對Y值在128以下的像素逐漸減輕denoise的強度,對Y值在16以下的像素完全不處理,可以用mt_lut先做一個gradient mask,然後用mt_merge來將原始畫面和denoise的結果進行blend:[syntax lang="avisynth"]
g_mask = mt_lut("x 128 >= 255 x 16 <= 0 x 16 - 255 * 112 / ? ?")
mt_merge(dfttest, g_mask, luma=true)
# mask:
# ( x >= 128 ) ? 255
# : ( x <= 16 ) ? 0
# : ( x - 16 ) * 255 / ( 128 - 16)[/syntax]

可以載入一個視頻,然後用上面的代碼來實際看一下mt_lut的效果。注意默認的U=V=1,如果想單獨看g_mask的話,mt_lut裡最好加上 chroma="128" 這個參數(或者 U=-128, V=-128 ),否則UV裡的垃圾數據會影響mask的查看。

由於mt_lut的expr參數是string,我們也可以將其他需要的參數代入這個表達式裡。例如上面這個例子,如果我們希望對限制範圍的上下限進行修改,不再是128的話,可以將上下限設置為int型的參數,然後用String()轉換並代入表達式:[syntax lang="avisynth" lines="f"]
upper = 128
lower = 16
g_mask = mt_lut("x " + String(upper) + " >= 255 x " + String(lower) + " <= 0 x " + String(lower) + " - 255 * " + String(upper - lower) + " / ? ?")
mt_merge(dfttest, g_mask, luma=true)
# mask:
# ( x >= upper ) ? 255
# : ( x <= lower ) ? 0
# : ( x - lower ) * 255 / ( upper - lower)[/syntax]

如果進一步封裝為函數,並且處理的不再限定於dfttest降噪的話:[syntax lang="avisynth" lines="f" filename="RestrictMerge.avsi"]
Function RestrictMerge ( clip input, clip "upperC", clip "lowerC", int "upper", int "lower") {

upper = Default(upper, 128) # upper threshold
lower = Default(lower, 16) # lower threshold

upperC = Default(upperC, input) # filtered clip above upper threshold
lowerC = Default(lowerC, input) # filtered clip below lower threshold

upper = upper > 255 ? 255 : upper
lower = lower < 0 ? 0 : lower

Try {

g_mask = input.mt_lut("x " + String(upper) + " >= 255 x " + String(lower) + " <= 0 x " + String(lower) + " - 255 * " + String(upper - lower) + " / ? ?")
# mask:
# ( x >= upper ) ? 255
# : ( x <= lower ) ? 0
# : ( x - lower ) * 255 / ( upper - lower)

result = mt_merge(lowerC, upperC, g_mask, luma=true)

} catch(RestrictMerge_err_msg) {

Assert( false, "RestrictMerge: " + RestrictMerge_err_msg )

}

return result
}[/syntax]

至此,我們僅僅用了masktools裡兩個最核心的函數,寫出來了一個實用度相當高的通用型自適應處理腳本了~大家可以找個重口味的濾鏡來體驗一下這個mask+merge的效果,譬如RestrictMerge(RemoveGrain(20), Sharpen(0.5), 160, 32),可以看到很明顯的高luma下重blur而低luma下重sharpen的效果。

在anti-aliasing的過程中,OPED裡的字幕在AA濾鏡下有很大的副作用,我們希望對OPED僅僅對背景部分進行AA,而上方的credit完全不處理。如果credit是黑白字幕的話,很常見的情況是整個畫面僅有credit的yuv值能達到235,其他部分都沒法達到,所以我們可以用這個特性做一個credit mask:[syntax lang="avisynth"]
m_credit = mt_lut("x 235 > 255 0 ?") # "x > 235 ? 255 : 0"
m_credit = m_credit.mt_expand(mode=mt_circle(2)) # 將邊緣略微擴張(後面會介紹)以應對中心最亮而向邊緣變暗的credit
mt_merge(taa, last, m_credit, luma=true)[/syntax]

mt_lut是對單個clip進行yuv運算的函數。Masktools裡還有對兩個、以及三個clip進行運算的函數,分別是mt_lutxy與mt_lutxyz。在mt_lut裡,輸入單個clip,傳遞給expr的變量是x,而在mt_lutxy/mt_lutxyz裡,傳遞給expr的變量分別是x,y(,z),對應輸入的兩個或三個clip的像素值。例如,mt_lutxy(a, b, "x y +")這個表達式就是把a與b的每個像素值相加,將得到的和作為該像素的結果;mt_lutxyz(a, b, c, "x y + z -")就是將a與b每個像素值相加,然後減去c的像素值,將得到的結果作為該像素的結果。

仍然以AA過程的credit為例子,上面只能應對字幕顏色比較特殊的情況。如果想進行一個通用的credit處理,在有NCOP/ED的情況下,我們可以利用NCOP/ED來製作更加精確的credit mask:[syntax lang="avisynth"]
OP = Trim(OPstart, OPend)
NCOP = AviSource("NCOP.avi").Trim(0, OP.FrameCount-1)
m_credit = mt_lutxy(OP, NCOP, "x y - abs 4 > x y - abs 1.9 * 0 ?").RemoveGrain(4, -1)
# "Abs( x - y ) > 4 ? Abs( x - y ) * 1.9 : 0"
mt_merge(taa, last, m_credit, luma=true)[/syntax]

需要注意的是,在mt_lut(xy(z))裡,expr的運算過程是浮點數運算,也就是說如果a是100,b是101,則"x y + 2 / 2 *"得到的結果是(100.+101.)/2.*2.,因此是201,而不會出現int型計算那樣在中途(100+101)/2變成100,然後*2變成200這種精度損失。但是整個expr內完整計算結束之後,最終運算結果則會四捨五入到[0, 255]內最接近的整數。因此最終結果是不會有小數,而大於255或者小於0的結果也會變限制為255或者0。尤其是後者,在設計運算的時候要特別注意。

mt_lut(xy(z))可以接受很多種運算符,例如四則運算的加減乘除,常用的取餘與乘方運算,三角函數、對數運算、取絕對值、四捨五入等函數,邏輯運算,二進制位運算等等,具體可以看官方文檔裡mt_masktools.html#reverse_polish_notation部分。

5. 基於mt_lut的強大工具——make/adddiff

发表于 : 2012-06-22 7:14
06_taro
masktools裡有四個與mt_lut(xy(z))相關的函數,雖然它們都完全可以用mt_lut(xy(z))來實現,但是這四個函數比用mt_lut(xy(z))實現時速度更快,而且因為非常常用,單獨使用時也很方便。這四個就是mt_makediff、mt_adddiff、mt_average與mt_clamp。

mt_makediff用於將兩個clip求差得到diff clip,而mt_adddiff則用於將diff clip加到一個現成的clip上。具體說來,
mt_makediff(a, b)相當於mt_lutxy(a, b, "x y - 128 +"),中綴表達式是"x - y + 128",而
mt_adddiff(a, b)相當於mt_lutxy(a, b, "x y + 128 -"),中綴表達式是"x + y - 128"。
由於mt_lut的運算結果是限制在[0, 255]之內的,而a與b裡的像素有時候a的值高,有時候b的值高,在make/add diff裡分別加/減了128,使得不管是yuv(a)>yuv(b)還是yuv(a)<yuv(b),計算的結果都可以保留在結果clip之內,而不會導致減出來的是負值而直接被clamp到0。換句話說,通過:
diff = mt_makediff(a, b)
得到的diff裡,某個像素值高於128則說明該像素在a中的值大於在b中的值,低於128則說明該像素在a中的值小於在b中的值,如果等於128則說明該像素在a與b中值相等。

兩個clip求差是視頻處理裡非常有用的手段。如果我們在使用了一次filter之後,可以用mt_makediff(filter, last)來取得filter前後的差,而對這個差值進行計算可以有很多用處。例如假設我們想對一個clip c運用一次filterA,但是希望每個像素的變化值最大不超過thr,那麼可以這樣:[syntax lang="avisynth"]
thr = 5 # 最大變化不超過5
diff = mt_makediff(c, c.filterA)
diff = mt_lut(diff, "x 128 - abs "+String(thr)+" > x 128 > 128 "+String(thr)+" + 128 "+String(thr)+" - ? x ?")
# Abs( x - 128 ) > thr ? x > 128 ? 128 + thr
# : 128 - thr
# : x
mt_makediff(c, diff, U=2, V=2)[/syntax]
上面這個腳本就是將filterA前後的畫面求差得到diff,通過將diff控制在128±5的範圍內,限制住了最大變化不超過5,最後再重新把差值加到原始畫面上,得到filterA且限制過的畫面。

為了進一步了解mt_makediff/mt_adddiff,讓我們來看看它在unsharp mask過程中用法。
unsharp mask是最簡單的一種sharpen算法,它的思路是:
1. 對原始畫面blur
2. 取原始畫面與blur過的畫面的差值,也就是被blur掉的細節部分
3. 在原始畫面上加上這個差值
如果是在blur過後的畫面上加上這個差值,相當於就是把blur掉的細節加回去,得到的是原始畫面;而現在是在細節本來就保留著的原始畫面上加上這個差值,就相當於是在原始畫面的基礎上將細節再增強一次,也就實現了sharpen的效果。這個過程可以用下面三行腳本來實現,對clip c進行unsharp mask:[syntax lang="avisynth"]
# 簡單起見,我們只對Y平面做sharpen,所以這裡的blur與下makediff都跳過UV的處理
clip_blur = c.RemoveGrain(11, -1)
diff_blur = mt_makediff(c, clip_blur)
mt_adddiff(c, diff_blur, U=2, V=2)[/syntax]
這就是一個簡單的unsharp mask的實現。當然純粹的unsharp mask有很大的副作用,譬如halo等,不適合直接使用。不過如果是為了給其他處理作為後處理的sharpen,例如對denoise之後的畫面進行一次sharpen,我們希望這個post-sharpen後的畫面不會比denoise之前的更加銳利,於是可以這樣做(假設原始畫面是src,使用的降噪濾鏡是dfttest):[syntax lang="avisynth"]
clip_nr = src.dfttest # denoise
diff_nr = mt_makediff(src, clip_nr) # 取denoise前後的畫面差值
clip_blur = clip_nr.RemoveGrain(11, -1) # 對denoise之後的畫面進行blur,用於銳化
diff_sharp = mt_makediff(clip_nr, clip_blur) # 取blur前後畫面差值
diff_limit = repair(diff_sharp, diff_nr) # 將銳化用的細節部分限制不超過denoise前後的差別,從而不會比src更銳利
diff_limit = mt_lutxy(diff_sharp, diff_limit, "x 128 - abs y 128 - abs > y x ?")
# Abs( x - 128 ) > Abs( y - 128 ) ? y : x
# 對repair前後的兩個diff,取最接近128(實際差值最小)的,從而不會使細節後超過blur前後的差
mt_adddiff(clip_nr, diff_limit, U=2, V=2)[/syntax]
實際上,上面這個就是Contra-sharpen最核心的部分。其實簡單說來就是銳化用的diff不超過denoise前後的diff,從而限制了銳化強度。非常簡單高速的方法,但是非常實用,也是mt_makediff/mt_adddiff教科書式的典範。

上面的sharpen是直接在原始clip上加上單倍的blur出來的diff。如果我們將diff倍化,以增強sharpen的程度,可以這樣對取出的diff進行一次mt_lut處理:[syntax lang="avisynth"]
strength = 1.5
clip_blur = c.RemoveGrain(11, -1)
diff_blur = mt_makediff(c, clip_blur)
\ .mt_lut("x 128 - "+String(strength)+" * 128 +")
# ( x - 128 ) * strength + 128
mt_adddiff(c, diff_blur, U=2, V=2)[/syntax]
這樣就是一個1.5倍強度的unsharp mask了。與這種線型增強的sharpen相比,non-linear sharpen用非線性的增強方式取得的效果常常更好,例如:[syntax lang="avisynth"]
strength = 2.0
divisor = 1.5
index = 0.8
clip_blur = c.RemoveGrain(11, -1)
diff_blur = mt_makediff(c, clip_blur)
\ .mt_lut("x 128 - "+String(divisor)+" / Abs "+String(index)+" ^ "+String(strength)+" * x 128 > 1 -1 ? * 128 +")
# Abs( ( x - 128 ) / divisr ) ^ index * strength * ( x > 128 ? 1 : -1 ) + 128
mt_adddiff(c, diff_blur, U=2, V=2)[/syntax]

有的時候我們希望在高分辨率下進行處理(super-sampling),然後還原回原始分辨率。最簡單的方法當然是先resize到高分辨率,然後應用filter,最後resize回原始分辨率。但是resize過程是有損失的,我們希望能享受super-sampling帶來的處理效果,但是又想讓resize過程的損失降到最低,最好原始畫面不經過多次resize,而只將filter前後的差值部分resize,這樣也可以用mt_makediff來實現:[syntax lang="avisynth"]
w = src.width
h = src.height
ss = src.Spline64Resize(w*2, h*2) # upconv到2倍分辨率
ss_filter = ss.filterA # 對ss進行filterA的處理
ss_diff = mt_makediff(ss, ss_filter, U=3, V=3) # 取filterA前後ss的差
diff = ss_diff.Spline36Resize(w, h) # 將差值還原回原始尺寸
mt_makediff(src, diff, U=3, V=3) # 在原始畫面應用差值[/syntax]

由於 x - ( x - y + 128 ) + 128 = y,相當於mt_makediff(a, mt_makediff(a, b))得到的結果應該等於b;
由於 y + ( x - y + 128 ) - 128 = x,相當於mt_adddiff(b, mt_makedff(a, b))得到的結果應該等於a。
如果上面的例子裡makediff和adddiff的關係沒有理解清楚的話,可以結合這兩個式子來加以理解。

另外要注意,由於mt_*返回的值域是在[0, 255]以內,如果a和b相差過大(差值超過128)的話,用mt_makediff得到的差值是不準確的,過大的值(大於255或者小於0)會被限制,因此如果需要對差值很大的兩個clip取得精確的diff的話,需要考慮將上溢和下溢部分單獨作為一個clip保存下來,例如[syntax lang="avisynth"]
diff = mt_makediff(a, b) ### "( x - y + 128 )"
diff_Overflow = mt_lutxy(a, b, "x y - 127 -") ### "( x - y + 128 ) - 255"
diff_Underflow = mt_lutxy(a, b, "y x - 128 -") ### "0 - ( x - y + 128 )"
# ...
b.mt_adddiff(diff).mt_lutxyz(diff_Overflow, diff_Underflow, "x y + z -") ### adddiff + overflow - underflow[/syntax]
當然這樣很邪道,如果遇到要對diff進行處理的話會比較麻煩,平時使用的話盡量還是將diff的對象限定在不大的範圍內。當然如果式子需要,還可以將兩個clip補上msb作為16-bit clip的lsb,然後用Dither工具裡的lut來計算,雖然那樣更邪道…

mt_average(a, b)的作用是對a與b每個像素取平均值,相當於mt_lutxy(a, b, "x y + 2 /"),中綴表達式是"( x + y ) / 2"。通常很少見到需要直接單獨使用這個函數的地方,就算在mask處理中都很少用到…

mt_clamp(clip input, clip bright_limit, clip dark_limit, int "overshoot", int "undershoot")是一個對input clip進行限制的函數,作用是對每個像素將值限定在bright_limit clip裡對應像素的值加上overshoot的和以下,dark_limit clip裡對應像素的值減去undershoot的差以上。如果用mt_lutxy來實現的話,相當於:[syntax lang="avisynth"]
input.mt_lutxy( bright_limit, "x y " + String(overshoot) + " + > y " + String(overshoot) + " + x ?")
\ .mt_lutxy( dark_limit, "x y " + String(undershoot) + " - < y " + String(undershoot) + " - x ?")[/syntax]
特別注意內部算法和這裡一樣,是先用bright_limit來限制,然後在用dark_limit來限制的。雖然也可以用mt_lutxyz來一次性實現(留作作業吧…),但是如果出現衝突,即
yuv(dark_limit) - undershoot > yuv(bright_limit) + overshoot
這種情況,最終是按照 yuv(dark_limit) - undershoot 作為限制的,如果自己dark/bright_limit沒選取好的話需要特別注意這個問題。

mt_clamp限制像素取值的上下限不超過指定的兩個limit clip以及上下浮動over/undershoot的一定範圍內,最為典型的應用是對filter出來的結果進行限制。仍然拿sharpen來舉例,如果是純粹對源進行sharpen,而不是配合其他濾鏡的後處理(即沒有可以用作限定sharpen強度不超過某個範圍的參考clip),從而不能用contra-sharpen來進行強度限制的話,我們可以用mt_clamp來進行另外一種spatial limit:[syntax lang="avisynth"]
overshoot = 3
undershoot = 3
clip_sharp = src.sharpen
bright_limit = src.mt_expand
dark_limit = src.mt_inpand
mt_clamp(clip_sharp, bright_limit, dark_limit, overshoot, undershoot)[/syntax]
mt_expand與mt_inpand分別是將像素值替換為周圍的附近像素當中最高/最低值,之後會專門介紹。也就是說每個像素sharpen出來的結果被限制在原始畫面該像素附近的像素內最高值與最低值上下浮動3的範圍內,從而將強度限制下來。

mt_clamp不僅僅可以用於spatial limit,配合前後幀還可以進行temporal limit。因為牽扯到mvtools裡的motion compensate,我這裡不細說,簡單說來就是,對一幀做motion compensate出來的前後幀與原始幀這三個clip,每個像素取三幀的最大值作為bright_limit,最小值作為dark_limit,然後限定filter之後出來的幀的值不超過這個限定,從而在temporal上進行了限制。還是拿sharpen過程舉例:[syntax lang="avisynth"]
sharp_clip = src.sharpen # sharpened clip
super_clip = src.MSuper # super clip
back_vector = super_clip.MAnalyse(isb=true) # backward searching vector
forw_vector = super_clip.MAnalyse(isb=false) # forward searching vector
back_compensate = src.MCompensate(super_clip, back_vector) # backward compensated frame (block based)
forw_compensate = src.MCompensate(super_clip, forw_vector) # forward compensated frame (block based)
temporal_bright = src.mt_lutxy(back_compensate, "x y max").mt_lutxy(forw_compensate, "x y max")
temporal_dark = src.mt_lutxy(back_compensate, "x y min").mt_lutxy(forw_compensate, "x y min")
mt_clamp(sharp_clip, temporal_bright, temporal_dark, U=2, V=2)[/syntax]
實際上min/max的計算通常用更為簡便的mt_logic,這個在後面mask操作函數裡會講到。
如果將這個temporal limit配合上面的spatial limit,或者在有filter前的源可以做contra-sharpen(contra-sharpen的話本身就是spatial limit了,沒必要和前面用mt_clamp來實現的spatial limit一起用)的話,這樣出來的sharpen是spatial-temporal limited sharpen,基本上可以防止絕大多數sharpen過程的artefacts。

6. mt_lut的擴展(一)——mt_lutf、mt_luts、mt_lutsx

发表于 : 2012-06-22 7:15
06_taro
下面來看看mt_lut的三個擴展函數:mt_lutf、mt_luts、mt_lutsx。

mt_lutf(clip clip1, clip clip2, string "mode", string "expr", string "yexpr", string "uexpr", string "vexpr")
mt_lutf是一個接受兩個clip的輸入,將第一個clip按照一定的模式計算出一個值x,然後可以將對第二個clip進行運算,在運算中可以使用x。注意x是對整個畫面處理後計算出一個屬於整個畫面的值,對每一幀這個值只有一個,而不是每個像素不同。可以使用的模式(string "mode"參數)是:
"avg" / "average": 計算全部像素的平均值
"std" / "standard deviation": 計算全部像素的標準差
"min": 計算全部像素的最小值
"max": 計算全部像素的最大值
"range": 計算全部像素最大值與最小值的差,即畫面的顏色範圍
"med" / "median": 計算全部像素的中值
例如在mt_lutf(a, b, mode="std", expr="y x +")裡,得到的結果是b的像素值加上a整個畫面所有像素的標準差。總體來說這個函數不算很實用,因為從第一個clip裡計算得到的結果是每幀一個值,用這個結果來做自適應的參數的話,得到的結果是幀間自適應,而完全沒有幀內的自適應,能夠取得比較好的效果的應用範圍還是比較狹窄的。我們可以來看一個實例:[syntax lang="avisynth"]
mt_lutf(a, a, mode="min", expr="x 16 >= y y 224 * 255 / 16 + ?", yexpr="x 16 >= y y 219 * 255 / 16 + ?", chroma="process")
# yexpr: min(a) >= 16 ? y : y * ( 235 - 16 ) / 255 + 16
# uexpr/vexpr: min(a) >= 16 ? y : y * ( 240 - 16 ) / 255 + 16[/syntax]
這樣是一個自適應的PC range->TV range的處理,如果a裡有像素點的值小於16則進行PC->TV轉換,否則保留原始畫面不做處理。

mt_luts(clip clip1, clip clip2, string "mode", string "pixels", string "expr", string "yexpr", string "uexpr", string "vexpr")
mt_luts與mt_lutf兩個看似很接近,都是在對單個像素運算的時候考慮同一幀內其他像素的值,但是二者運用的方式完全不同。mt_luts接受兩個clip的輸入,它的最終目的是按一定模式來對幀內每個像素進行一次基於鄰域內其他像素的運算,模式和mt_lutf相同,只是增加了一個將鄰域像素進行加權平均的自定義模式,這個模式輸入的字符串是領域像素加權時用的權重數組。參與mode計算的像素裡,中心像素取自於clip1,而周圍的像素取自於clip2按照expr計算出來的結果。要注意的是,mt_luts按照模式時僅僅考慮某像素及其領域內的像素,而不是像mt_lutf那樣計算整個畫面全部像素。例如,同樣是直接輸出mode="avg"計算出來的結果:
mt_lutf(a, a, mode="avg", expr="x")
得到的結果裡,每個像素值都等於clip a當前幀全畫面像素的平均值(y變量是原始畫面,所以不予應用);而
mt_luts(a, a, mode="avg", pixels=mt_square(1), expr="y")
得到的結果裡,每個像素的值分別是其周圍半徑為1的方形block內的9個像素的平均值(x變量為原始畫面,所以不予應用)。
mt_luts與mt_lutsx的pixels參數是用一個表述參與某像素計算的周圍領域的字符串。例如上面的mt_square(1)得到的就是描述半徑為1的block的字符串,masktools裡還有很多這樣的生成字符串的函數,之後會一一介紹,這個例子裡實際表示的是

代码: 全选

1 2 3
4 5 6
7 8 9
這樣以5為中心的9個點。也就是說mt_luts在這9個點的範圍內,取clip1作為中心像素,取clip2經expr計算得到的結果為領域像素(這裡是"y",就是直接複製clip2的值),來計算mode(這裡是"avg",即取平均值)。如果我們使用
mt_luts(a, b, mode="std", pixels=mt_square(1), expr="x y -")
完整的過程是:
1. a作為中心像素;
2. 對鄰域內的9個像素,取y(a)-y(b)的差值作為領域像素expr之後的結果;
3. 計算這9個差值之間的標準差,作為最終結果裡中心像素的值。

mt_lutsx(clip clip, clip clip1, clip clip2, string "mode", string "mode2", string "pixels", string "expr", string "yexpr", string "uexpr", string "vexpr")
mt_lutsx裡參數的使用都與mt_luts一樣,但是計算的方法卻完全不同。對於每個中心像素,它的計算流程是:
1. 按照mode來計算clip1的領域內的點,作為參數y;
2. 按照mode2來計算clip2的領域內的點,作為參數z;
3. 將y與z應用於expr裡的計算(x就是第一個clip參數的中心點的值)
例如,在mt_lutsx(a, b, c, mode="med", mode2="range", pixels=mt_square(1), "x y + z -")這個函數內,實際計算的結果是:對於每個像素,用a裡該像素的值加上b裡該像素鄰域內的中值,再減去c裡該像素鄰域的最大值與最小值之差。

7. 其他鄰域處理(morphologic filter)——mt_ex/inpand、mt_in/deflate

发表于 : 2012-06-22 7:15
06_taro
masktools裡的鄰域處理不僅僅有mt_luts與mt_lutsx,還有更為簡化,但是也更為常用的版本:mt_expand, mt_inpand, mt_inflate, mt_deflate。這四個函數在masktools的官方文檔裡被稱為morphologic operator,實際上本質和mt_luts(x)一樣是通過鄰域計算來實現的。

mt_expand的作用是將某像素點的值替換為鄰域內最大值,具體參數是
mt_expand(clip c, int "thY", int "thC", string "mode"),其中thY和thC限制替換過程最大變化不超過這個值,而mode是描述鄰域的字符串,實際相當於:[syntax lang="avisynth"]
mt_lutsx(c, c, c, mode="max", pixels=mode,
\ yexpr="y x - "+String(thY)+" > x "+String(thY)+" + y ?",
\ expr ="y x - "+String(thC)+" > x "+String(thC)+" + y ?")
# yexpr : max(neighbourhood) - x > thY ? x + thY : max(neighbourhood)
# uvexpr: max(neighbourhood) - x > thC ? x + thC : max(neighbourhood)[/syntax]

而mt_inpand則是將某像素點的值替換為鄰域內的最小值,參數和mt_expand相同,實際相當於:[syntax lang="avisynth"]
mt_lutsx(c, c, c, mode="min", pixels=mode,
\ yexpr="x y - "+String(thY)+" > x "+String(thY)+" - y ?",
\ expr ="x y - "+String(thC)+" > x "+String(thC)+" - y ?")
# yexpr : x - min(neighbourhood) > thY ? x - thY : min(neighbourhood)
# uvexpr: x - min(neighbourhood) > thC ? x - thC : min(neighbourhood)[/syntax]

mt_inflate的作用是計算在某像素的鄰域內的平均值,當這個值比中心像素的值高時替換中心像素,參數是mt_inflate(clip c, int "thY", int "thC"),實際相當於:[syntax lang="avisynth"]
mt_lutsx(c, c, c, mode="avg", pixels="-1 -1 -1 0 -1 1 0 -1 0 1 1 -1 1 0 1 1",
\ yexpr="y x > y x - "+String(thY)+" > x "+String(thY)+" + y ? x ?",
\ expr ="y x > y x - "+String(thC)+" > x "+String(thC)+" + y ? x ?")
# yexpr : avg(neighbourhood) > x ? avg(neighbourhood) - x > thY ? x + thY
# : avg(neighbourhood)
# : x
# uvexpr: avg(neighbourhood) > x ? avg(neighbourhood) - x > thC ? x + thC
# : avg(neighbourhood)
# : x[/syntax]

mt_deflate的作用是計算在某像素的鄰域內的平均值,當這個值比中心像素的值低時替換中心像素,參數是mt_deflate(clip c, int "thY", int "thC"),實際相當於:[syntax lang="avisynth"]
mt_lutsx(c, c, c, mode="avg", pixels="-1 -1 -1 0 -1 1 0 -1 0 1 1 -1 1 0 1 1",
\ yexpr="y x < x y - "+String(thY)+" > x "+String(thY)+" - y ? x ?",
\ expr ="y x < x y - "+String(thC)+" > x "+String(thC)+" - y ? x ?")
# yexpr : avg(neighbourhood) < x ? x - avg(neighbourhood) > thY ? x - thY
# : avg(neighbourhood)
# : x
# uvexpr: avg(neighbourhood) < x ? x - avg(neighbourhood) > thC ? x - thC
# : avg(neighbourhood)
# : x[/syntax]

注意mt_inflate/deflate與其他鄰域計算不同之處是,(目前)這兩個函數的鄰域範圍是固定的(中心像素周圍不包含自身的8個像素點,鄰域字符串的定義方式之後會講解),不能自己定義鄰域範圍。

總體來看,如果處理的是mask,mt_expand與mt_inflate是具有擴張性的,因為結果是取鄰域最高值,或高於中心像素時的均值;而mt_inpand與mt_deflate是收縮性的,因為結果是取鄰域內最低值,或低於中心像素時的均值。

YUV的計算如果考慮到鄰域,可以大大擴展應用的空間,因為鄰域處理是視頻/圖像處理裡非常重要的一個環節。我們來看一個用mt_luts配合mt_lutsx來製作一個簡單的complexity mask。畫面複雜度(complexity)是圖像處理裡非常重要的一個屬性,高複雜度的區域與低複雜度的區域往往需要不同的處理,譬如由於最後一次壓制過程導致的banding,往往只出現在畫面的低複雜度區域,而不出現在高複雜度區域,因此用一個mask來限定deband的範圍不包括高複雜度區域可以較好地保護這種區域的細節。Complexity mask的實現算法相對來說比較複雜,通常配合DCT計算AC/DC variance得到的結果比較好,而靠masktools做的話效果可能並不算理想。而且因為這個實現過程需要有較大的鄰域範圍,而masktools當鄰域範圍增大時處理速度會下降地比較厲害,而不用較大的範圍得到的結果可能不夠理想。不管怎樣,我們先來用一個試驗:[syntax lang="avisynth"]
dif_med = mt_makediff(mt_luts(last, "med", pixels=mt_circle(3), expr="y"))
c_mask = mt_lutsx(dif_med, dif_med, dif_med, "std", pixels=mt_circle(3), expr="y 2 ^")
\ .mt_expand.mt_inflate(chroma="128") # 為了後面截圖用了chroma="128",實際使用可以去掉[/syntax]
這是一個很簡單的模擬方式,先取鄰域內的中值,然後用mt_makediff來求得像素與鄰域中值之差。在複雜度較高、細節豐富的區域鄰域中值和中心像素點間的差值在鄰域內的標準差較高(好拗口…),因此衡量這個標準差可以模擬出一個複雜度mask:
图片
源畫面
图片
將畫面下半部分紋理較重、複雜度較高的部分mask住的效果。

8. mt_lut的擴展(二)——mt_lutspa

发表于 : 2012-06-22 7:15
06_taro
關於mt_lut的部分,最後來介紹一下mt_lut的最特殊的一個擴展函數:
mt_lutspa(clip clip, string "mode", string "expr", string "yexpr", string "uexpr", string "vexpr", bool "relative", bool "biased")
之前我們接觸到的mt_lut計算,是先從單個clip單個像素點,到多個clip單個像素點,接著道多個clip包含鄰域的多個像素點的計算,然而這些計算裡最多只考慮到了中心像素鄰域內的點,而且只對這些點的數值作運算。而mt_lutspa則非常特殊,它並不考慮像素本身的YUV值,而是考慮像素所處的坐標。它只接受一個clip參數,而傳遞給expr的x與y分別是運算中像素的x,y坐標,而沒有其他信息傳遞給expr。我們先來看看具體的參數:
mode - 坐標軸的取值方式:
= "absolute",取絕對坐標,相當於relative=false,例如在720x480的視頻裡,x的取值範圍就是[0, 719],而y的取值範圍就是[0, 479];
= "relative" / "relative opened" / "relative exclusive",取相對坐標,x,y的範圍是(0, 1),相當於relative=true, biased=true
= "relative closed" / "relative inclusive",取相對坐標,x,y的範圍是[0, 1],相當於relative=true, biased=false
很明顯的是,mt_lutspa裡的expr並沒有辦法接受某個像素的值,只能使用像素坐標以及我們人為通過在expr的string裡增加的參數來控制,因此它的運算結果與原始畫面像素值並沒有相關性,如果我們沒有將其他變量放入expr裡的話,它的處理結果是只與分辨率相關的,因此常常只是配合masktools的其他工具使用,例如在後面提及的mask處理濾鏡裡使用。但是,能夠對坐標進行運算,理論上對一些特殊的mask處理情況比較有用,例如我們平時用Crop+Overlay來處理畫中畫,而這種處理只能適用於矩形畫面,如果我們要處理一個圓形畫面呢?不妨用mt_lutspa繪製一個圓:[syntax lang="avisynth"]
radius = 80
centre_x = 160
centre_y = 120

# ( x - centre_x ) ^ 2 + ( y - centre_y ) ^ 2 <= radius ^ 2
mt_lutspa(mode="absolute",
\ expr="x "+String(centre_x)+" - 2 ^ y "+String(centre_y)+" - 2 ^ + "+String(radius)+" 2 ^ <= 255 0 ?",
\ chroma="128") # 為了後面截圖用了chroma="128",實際使用可以去掉[/syntax]
繪出來的circle mask:
图片

當然既然有坐標可以用,mt_lutspa就不僅僅是可以用作mask了,譬如我們來做一個正弦函數的特效:[syntax lang="avisynth"]
mt_lutspa(mode="relative inclusive", expr="x 15 * sin 0.5 y - 2 * - abs 0.01 <= 255 0 ?", chroma="128")
\ # ( 0.5 - y ) * 2 ≈ sin( 15x )[/syntax]
图片
如果加上幀號作為正弦的相位,我們還可以讓這個正弦曲線動起來:[syntax lang="avisynth"]
ScriptClip("""mt_lutspa(mode="relative inclusive", expr="x 15 * "+string(current_frame)+" 0.05 * + sin 0.5 y - 2 * - abs 0.01 <= 255 0 ?", chroma="128")""")[/syntax]
图片
上面這兩個曲線的渲染過程沒有考慮aa,因此都是在高分辨率運算之後down sample下來的。由於描繪曲線對整數級的像素值來說是應該考慮平滑的,各位有興趣的可以試試不用0/255兩個值來繪製,而用一個gradient value。如果配合其他參數,還可以做出有各種性質的曲線,譬如用String(AverageLuma)來替代255,出來的應該是每幀顏色為該幀luma平均值的曲線。

當然既然可以嘗試特效了,不妨把它當成一個非線視頻編輯軟件來嘗試下,譬如讓我們來試著做一個old scene effect,保持畫面中心的樣子不變,而將畫面的四周亮度降低,模擬出一個電影中二次出現的回憶場景的樣子:[syntax lang="avisynth"]
ose_mask = mt_lutspa(mode="relative", expr="x y 1 x - 1 y - * * * 4096 *")
\ # 2x * 2(1-x) * 2y * 2(1-y) * 256
mt_merge(mt_lut("x 2 >>"), last, ose_mask, U=4, V=4).f3kdb(Cb=0, Cr=0, grainC=0)[/syntax]
實際效果:
图片
上面這是原始畫面。
下面這個是依靠mt_lutspa將畫面邊緣的值設低,畫面中心值設高作出mask,然後取x>>2(即y/4)的結果作為畫面邊緣,而原始畫面作為畫面中心來merge,chroma保持原始畫面,最後對luma進行一次deband(因為x>>2的過程精度降低產生banding)的效果:
图片

到此為止,masktools中對於以mt_lut及其應用為核心的函數已經全部介紹完了。之後我們會更加注重於masktools最重要的對象——mask的製作與處理。mask的處理部分會大量地使用到上面介紹的諸多以mt_lut為基礎的函數,所以在這裡可以先將入門階段告一段落,將之前的部分系統性地複習一下,以更好地進入mask部分。另外基於mt_lut的這些函數都是對應8-bit的clip,如果需要處理16-bit的clip,Dither package裡提供了一些16-bit的lut工具,和mt_lut的使用基本一致,有興趣的可以參照其文檔自己嘗試。

9. 回歸mask——mask製作函數mt_edge與mt_motion

发表于 : 2012-06-22 7:16
06_taro
前面我們已經試著用mt_lut製作了luma mask、complexity mask以及一個circle mask。在很多濾鏡的處理當中,還有一些非常重要的mask,依靠mt_lut很難實現,而masktools專門提供了兩個函數用以製作mask:mt_edge與mt_motion。顧名思義,mt_edge是用來製作edge mask的函數,而mt_motion是用來製作motion mask的函數。

mt_edge(clip input, string "mode", int "thY1", int "thY2", int "thC1", int "thC2")
作為一個邊緣檢測用的mask函數,使用這個函數得到的clip裡,對應input clip中的像素為邊緣(線條)時值為255(或者接近255的高數值),而非邊緣(線條)像素的值為0(或者接近0的低數值)。也就是說:[syntax lang="avisynth"]
mt_merge(a, b, c.mt_edge)[/syntax]
這樣一個merge過程裡,處於clip c的線條上的點使用clip b裡的像素值,而處於clip c的非線條區域的點使用clip a裡的像素值。

由於很多視頻處理過程需要區分邊緣與非邊緣,例如anti-aliasing一般只處理邊緣上的鋸齒,而非邊緣區域不需要處理,再如deband過程通常對象為非邊緣區域的banding,我們可以利用mt_edge製作edge mask,然後對邊緣與非邊緣作出不同的處理,最後用mt_merge將兩部分合併,從而使用於處理邊緣的濾鏡只應用在邊緣上,而不會對非邊緣造成影響,反之亦然。例如:[syntax lang="avisynth"]
edge_mask = src.mt_edge # 製作edge mask
edge = src.AAA # 對edge部分進行anti-aliasing處理
non_edge = src.f3kdb # 對非edge部分進行deband處理
mt_merge(non_edge, edge, edge_mask)[/syntax]
這裡AAA只應用與edge部分,f3kdb只應用於非edge部分,從而既不會在非edge區域出現AAA的副作用,也保護了容易在f3kdb過程容易損失的線條。

mt_edge可以使用mode參數修改邊緣檢測的方式。mode參數是一個描述鄰域3x3範圍內9個像素在邊緣檢測的kernel的字符串,例如"0 -1 0 -1 0 1 0 1 0"這樣一個mode定義的就是一個3x3的kernel。比較特殊的是這個字符串可以包含10個數值,只有9個數值時是上面這種權值數組,如果有10個的話,最後一個數字表示的是normalization factor。除了這種數組表示之外,mt_edge還預設了幾個模板:"sobel"、"roberts"、"laplace"、"prewitt"、"hprewitt"、"cartoon"與"min/max"。"min/max"其實是一個在3x3範圍內取range的kernel,如果不考慮四個thX參數的話(即thY1=0, thY2=255, thC1=0, thC2=255時),相當於mt_luts(last, mode="range", pixels=mt_square(1), expr="Y")。而其他模板都是比較典型的邊緣檢測算子。關於邊緣檢測的理論這裡不進行專業性的說明,有興趣的可以自己查閱數字圖像處理裡邊緣檢測部分的相關資料。

四個thX參數的作用是這樣的:邊緣檢測出來的結果裡,對於Y平面上的像素,如果值低於thY1,則Y值被設為0,如果值高於thY2,則Y值被設為255,值在thY1與thY2之間則不變。thC1與thC2的作用類似,對象是UV平面。由於默認的thY1=thY2=thC1=thC2=10,凡是檢測結果裡高於10的都定為255,低於10的都定為0,因此是一個類似於本教程第一個例子中的二值mask,最終merge的結果要么是edge,要么是非edge。其本質就是:[syntax lang="avisynth"]
mt_edge(thY1=0, thY2=255, thC1=0, thC2=255)
mt_lut(yexpr="x "+String(thY1)+" < 0 x "+String(thY2)+" > 255 x ? ?",
\ expr ="x "+String(thC1)+" < 0 x "+String(thC2)+" > 255 x ? ?")
# yexpr: x < thY1 ? 0
# : x > thY2 ? 255
# x
# uvexpr: x < thC1 ? 0
# : x > thC2 ? 255
# x[/syntax]
由於在thY1=0, thY2=255, thC1=0, thC2=255這樣完全不限制時,越是像邊緣的點值越高,越是不像邊緣的點值越低,thX其實是一個限制邊緣檢測精度的闕值,一個定義到底多高的值才被當作邊緣並賦值為255,而多低的值會被當作非邊緣並賦值為0的闕值。因此如果邊緣檢測的準確度不令人滿意的話,除了嘗試更適合片源類型的kernel之外,也可以通過調整這個闕值來修改準確度。

由於edge mask的重要性,除了masktools的mt_merge之外,還有其他工具可以製作edge mask,例如基於canny算法的tcanny,依靠gradient vector magnitude來檢測的TEdgeMask,以及在使用mask=true參數時的MSharpen等等。大家可以多嘗試這些工具檢測edge的效果與特典,以找到對應某一片子最合適的edge mask。

mt_motion是檢測motion並繪製motion mask的函數。相比mt_edge,mt_motion沒有mode參數,而增加了一個thT參數。thT控制的是檢測scene cut的闕值。因為scene cut幀與前/後幀並不具有動態相關性,不應該參與motion檢測。motion mask對於inter型的濾鏡來說也是比較重要的mask,例如在deint時,靜態部分的處理和動態部分是不一樣的,可以使用一個motion mask將動態部分覆蓋,然後用不同的deint方法來處理動態與靜態,最後merge(需要說明的是,mt_motion的motion mask無論是檢測還是使用都不適合這種情況,deint最適合的motion mask還是TMM)。還有像一些temporal denoise濾鏡,如果內部沒有motion compensate的處理的話,有可能在動態區域出現blend的artefact,這時通過motion mask來將動態區域保護住,可以防止這種blend的問題:[syntax lang="avisynth"]
m_mask = src.mt_motion
mt_merge(src.FluxSmoothT, src, m_mask, luma=true)[/syntax]
不過即使在這種應用裡,masktools裡的mt_motion準確度還是不如mvtools裡的MMask,所以大部分情況下motion mask還是推薦用MMask來做。

另外還有很多其他種類的常用mask製作工具,譬如對於interlaced的源使用的CombMask、TCombMask等,在此不對這些非masktools的工具一一介紹。有興趣的可以自行研究。

很多情況下,用mt_edge製作出來的edge mask並不是我們想要最終應用的mask,或者我們做出了好幾個mask,而想在merge的時候將它們都考慮進去,這時我們需要對mask進行一定的處理。當然mask的處理我們已經掌握了強大的mt_lut工具,幾乎可以滿足絕大多數應用,不過masktools還提供了幾個專門的函數,它們不像前面很多mt_lut工具對非mask的處理也有很大幫助,但是在mask的處理上,相比mt_lut令人眼花繚亂的表達式而言它們的用法更加簡便,因此在各種腳本裡上鏡率也很高。下面我們就來介紹這幾個mask處理函數。