在視頻中實現圖像特效

                                                                     by fanxiushu 2020-06-24 轉載或引用請註明原始作者。

說起圖像特效,可以打開Photoshop軟件,裏邊有個”濾鏡“菜單,再到”濾鏡“裏邊,可以看到的是各種形形色色的效果,
比如各種扭曲效果啊,模糊效果啊,油畫效果啊等等。Photoshop處理的是單張圖片,不是視頻
(視頻可以簡單理解成連續不斷更迭的多張圖片),而這些特效算法也較複雜,耗時也長 。
如果只是處理單張圖片,計算特效的時候,就算是消耗幾秒,或者10來秒,也無所謂,都能接受。
但是視頻不行,按照30FPS來算,每秒要更迭30張圖片,也就是每張圖片停留時間僅僅只有33毫秒,
這33毫秒還包括其他方面計算處理消耗的時間。所以用在特效計算上的時間可能僅僅不超過10來豪秒,
而特效的數學計算也繁雜,耗時也多,因此針對視頻實現的特效,不會像單張靜態圖片那麼好辦,限制也較多。

在視頻中實現圖像特效的想法,是在上一篇文章“WIN10系統 Indirect Display虛擬顯示器之特殊應用”之後想到的,
當時在文章中提到的球面化效果(上文中也附圖),本來當時只是應付性質的在網上隨便copy了一個圖像球面化的算法,
可是看到這些奇形怪狀的圖像之後,覺得挺好玩的。於是就有了在xdisp_virt工程中集成圖像特效的想法。
本文其實也是xdisp_virt遠程控制工程的一個衍生篇,xdisp_virt當初本來只是做遠程控制的,
到後來增加的雜七雜八的功能越來越多,越來越不務正業了。
xdisp_virt項目地址:
https://github.com/fanxiushu/xdisp_virt

先來張圖,提提神。


上圖中,1080P的遠程桌面圖像已經被球面化了,而且看左上角的幀率 28.7 fps,所以從看電影的角度來說,已經沒問題了。
不過我如果把遠程桌面圖像採集率提升到 60 fps,在不開啓特效的情況下,幀率基本都維持在 50-60 fps 之間變化。
而開啓特效之後,幀率立馬就降低到30 fps左右了。
可能是被控制端的機器的CPU比較老吧,臺式機,CPU是 I7-4770k,不清楚最新的CPU會是什麼效果。
當然也跟我採用的具體算法有關,下面就是具體講解如何在視頻中實現特效的算法。

以上圖中球面化的特效算法來說。具體算法大致代碼如下:
假設圖像寬度和高分別是 width,height,圖像在座標(x,y)處的像素值是 Image(x,y);
new_Image代表經過變化之後的新圖像。

   float pi = 3.1415926;
   float K=pi/2;
   float e;
   float alpha=0.5;
   float a, b, a0, b0, a1, b1;
   float new_x, new_y;
   float x0, y0;
   float theta;
   int MidX = width / 2;
   int MidY = height / 2;
   a = width / 2; b = height / 2;
   e = (float)width / (float)height;

   for(int y=0; y<height ; ++y){
         for(int x=0; x<width; ++x){
                y0 = MidY - y;
                x0 = x - MidX;

                theta = atan2(y0*e, (x0 + 0.0000001)); ///
                if (x0 < 0)   theta = theta + pi;
                a0 = x0 / cos(theta);
                b0 = y0 / sin(theta + 0.000000001);
                
                a1 = asin(a0 / a)*a / K;
                b1 = asin(b0 / b)*b / K;

                a1 = (a0 - a1)*(1 - alpha) + a1;
                b1 = (b0 - b1)*(1 - alpha) + b1;

                new_x = a1*cos(theta);
                new_y = b1*sin(theta);

                new_x = MidX + new_x;
                new_y = MidY - new_y;
                //////
                int x1 = (int)new_x;
                int y1 = (int)new_y;

               float p = new_x - x1;
               float q = new_y - y1;

              new_Image(x,y) = (1-p)*(1-q)*Image(x1, y1) +
                                            p*(1-q)*Image(x1+1, y1) +
                                            q*(1-p)*Image(x1, y1+1) +
                                            p*q*Image(x1+1, y1+1);
         }
    }

上面代碼中反反覆覆的各種三角函數,atan2,cos,sin等,除非使用GPU硬件加速,否則使用CPU運算,肯定快不到哪裏去。
如果是視頻的話,估計即使以現在最快的CPU計算,可能也會一卡一卡的。
除非是交給GPU運算,GPU有個最大特點就是浮點運算和數學運算非常強悍,
而且擅長並行運算,其實就是GPU中幾百個幾千個小GPU核同時參與運算,
上面代碼中的多層循環,交給幾百個上千個小GPU核同時運算,速度大大提高。
回到現實中來,我們的程序中,最通常的做法還是得使用CPU來運算。
如何改進上面的代碼,讓視頻處理能順暢呢?
仔細分析上面的代碼,對座標(x,y)進行某種計算,獲得新座標(new_x, new_y),
然後對新座標處的像素值進行計算,最終得到新圖像在(x, y)座標處的像素值。
再仔細分析對 new_x,new_y的計算,純粹只跟座標位置有關,與圖像的像素值無關。
這就爲我們在視頻圖像特效中提供了一個很大的改進辦法。
因爲視頻的尺寸不是經常變化的,誰也不會閒的蛋疼一會把視頻弄成這個尺寸,一會又弄成那個尺寸。
因此,我們可以根據圖像的width和height,預先計算好new_x,new_y數組,形成一張表,表的對應關係是
(x, y) ->(new_x, new_y ),
這樣在以後的圖片中,只要尺寸沒變化,都通過查表來獲取new_x,new_y的值,然後再做運算。
這樣就大大提高了視頻中這個特效的處理速度。
除了預先計算new_x,new_y,上面的算法還有沒有優化的地方呢?
上面代碼中 ,p, q,都是通過座標運算出來的係數,在Image中參與運算的
(1-p)*(1-q),p*(1-q),q*(1-p),p*q,
四個係數都是可以預先計算出來的。
p,q都是浮點數,我們在預先計算的時候,想辦法轉成整數來運算。
因此,我們可以定義一個數據結構來描述:

struct PosXY
{
      unsigned short  x;
      unsigned short  y;
      short  a,b,c,d;
};
其中結構中 x,y代表新計算出來的 new_x,new_y座標位置,至於爲何採用 unsigned short,是因爲現在的屏幕大小從來沒超過 65536 的。
a,b,c,d四個參數就是上面的(1-p)*(1-q), p*(1-q), q*(1-p), p*q;
採用short 類型,預先運算的時候,把 p,q擴大  32767, 因爲p,q在(-1,1)範圍內.
對於每個座標位置(x,y),都對應着一個PosXY結構。
因此一個width,height大小的圖像,需要額外的 width*height*sizeof(PosXY) 內存空間。
比如,對於1080P的32位圖像來說,圖像本身內存大小是8MB,這個額外空間大小是 8*3=24MB,
不過這點內存佔用對現在的機器來說也是小兒科。
因此改進之後的算法大致如下:
PosXY* posxy_table=NULL;
 if(!posxy_table || 圖像尺寸變化){// 預先計算
      posxy_table=malloc(width*height*sizeof(PosXY));
     
      for(int y=0;y<height; ++y){
           PoxXY* pos = posxy_table + width*y;
           for(int x=0;x<width;++x){
                y0 = MidY - y;
                x0 = x - MidX;

                theta = atan2(y0*e, (x0 + 0.0000001)); ///
                if (x0 < 0)   theta = theta + pi;
                a0 = x0 / cos(theta);
                b0 = y0 / sin(theta + 0.000000001);
                
                a1 = asin(a0 / a)*a / K;
                b1 = asin(b0 / b)*b / K;

                a1 = (a0 - a1)*(1 - alpha) + a1;
                b1 = (b0 - b1)*(1 - alpha) + b1;

                new_x = a1*cos(theta);
                new_y = b1*sin(theta);

                new_x = MidX + new_x;
                new_y = MidY - new_y;
                ///////
                int x1 = (int)new_x; int y1=(int)new_y;
                pos->x = x1; pos->y=y1;
                float p = new_x-x1; float q=new_y-y1;
                pos->a = (short)((1 - p)*(1 - q) * 32767);
                pos->b = (short)((p)*(1 - q) * 32767);
                pos->c = (short)((1 - p)*(q) * 32767);
                pos->d = (short)((p)*(q) * 32767);
           }
      }
 }
就這樣,預先計算的部分就計算好了。接下來,就是計算圖片數據了。
針對視頻流中的每個尺寸一樣的圖片,這樣來計算最終圖像數據:
for(int y=0;y<height;++y){
    PosXY* pos = posxy_table + width*y;
    for(int x=0;x<width;++x){
          
           new_image(x,y) = pos->a*Image(pos->x, pos->y) +
                                         pos->b*Image(pos->x +1, pos->y) +
                                         pos->c*Image(pos->x, pos->y+ 1 ) +
                                         pos->d*Image(pos->x +1, pos->y +1);
    }
}
雖然上面代碼還是在嵌套循環,並且還是在做4組加法和乘法,但是比起原始的算法中不停調用cos,sin等數學函數要快的多了。
如果還像再快速,可以考慮使用SSE/MMS彙編指令來處理上面的4組加法和乘法。
但是比較搞笑的是,我嘗試着使用SSE加速指令,結果反而沒有使用C代碼的速度來的快速。多半是我使用不正確造成的。
後來想想,我是先把上面八個數值,組成兩個數組,
short a1[] = {pos->a, pos->b, pos->c, pos->d };
short b1[] = {Image(x,y), Image(x+1,y), Image(x,y+1), Image(x+1,y+1) };
然後使用movq(movdq) 彙編指令導入mmX寄存器中,最後使用 pmaddwd指令同時完成四組數據相乘和相加。
看起來是好像是四組數據同時在進行加減乘除,理論上能減少4倍時間。其實認真考慮 a1,a2數組形成過程。
就拿a1來說,pos指針指向的a,b,c,d都是存儲在內存中的,a1數組其實也是存儲在內存中的,
short a1[] = {pos->a, pos->b, pos->c, pos->d }; 這段C代碼如果編譯成彙編代碼。
其實就是先把 pos指向的a,b,c,d 從內存傳遞到CPU寄存器,然後再從CPU寄存器再次傳輸到 a1指向的內存地址中。
如果在平時,這不會帶來任何效率問題。
但現在面臨的是非常繁重的運算。這麼來來回回的在CPU和內存之間傳輸數據,問題就挺大了。
具體關於CPU和內存之間速度問題討論,可以參閱其他相關資料。這裏就不再羅嗦了。

這種針對視頻流,只要算法中牽涉到只是關於座標位置變換的,
都可以採用類似上面的做法來處理,這種做有個通俗的名稱---- 查表法。
還有很多類似特效圖像的算法都是變化座標的,比如凹透鏡,凸透鏡,波浪效果,水波效果,旋轉捲曲,萬花筒,等等。

還有一類圖像特效算法,就是根據圖像像素來做變換的圖像特效算法,這種就沒有什麼投機取巧的辦法,
只能老老實實的運算,遇到實在運算量太大的,估計只能交給GPU來運算。
但是現在GPU也沒有通用的編程接口,都是各個GPU廠商各自的一套接口。
如果GPU也不能在短時間處理,也就意味着不能在實時的視頻流中使用了。

下面介紹一種素描算法,在目前的CPU運算中,在視頻流中處理還算順利。
我們在Photoshop軟件中,對某個圖片處理成素描效果,處理過程其實是比較簡單的。
一,首先打開某個圖片,然後轉換成灰度圖,
二,把圖片複製新圖層,然後對新圖層反相,之後再對新圖層做高斯模糊,
三,把新圖層和底圖做顏色減淡。
就這樣的三步操作,就製作好一個素描效果圖了。

我們在處理視頻流的時候,得到的圖像都是RGB三原色的。
首先把它轉成灰度圖,其實很簡單 直接相加除以 3, 就是 gray = (r+g+b) /3 ;
然後反相, 就是 255 - gray,這個運算量也不大。
接着就是對反相之後的數據做高斯模糊。
這裏採用 3X3 方陣的最簡單的高斯模糊,高斯半徑大約是0.85。至於 5X5,7X7,9X9甚至更大的方陣,
因爲運算量過大,也不是適合在實時視頻流中處理。下圖是 3X3的高斯矩陣
|  1     2     1  |
|  2     4     2  |     / 16
|  1     2    1   |
算法是
new_Image(x,y) =( 1*Image(x-1,y-1) + 2*Image(x,y-1) + 1*Image(x+1, y-1) +
                              2*Image(x-1, y) +4*Image(x, y) + 2*Image(x+1,y) +
                              1*Image(x-1,y+1) + 2*Image(x,y+1) + 1*Image(x+1,y+1) ) / 16 ;
這個運算量相對來說,也能接受 。
最後一步就是對已經做過反相和高斯模糊的圖像數據 B(這個也叫混合色),原來的灰度圖A(這個也叫基色)
做顏色減淡處理,算法是:
基色+(基色*混合色)/(255-混合色)= 結果色
最終的結果色就是我們的素描圖像。
這裏的算法代碼也就不再給出了,有興趣可以自行去實現。

根據圖像像素做變換得出的圖像特效算法也有很多,比如各種模糊效果,高斯模糊啊,運動模糊啊,方框模糊等等。
還有鉛筆畫,蠟筆畫,雜色,油畫,等等,都是通過對像素做變換得來的特效,因爲大部分運算量都挺大,
很多都不大適合用在實時的視頻流中,除非是GPU能參與運算。

最近因爲要給xdisp_virt工程添加特效算法,因此大量接觸到圖像的各種算法。
算法原理只能囫圇吞棗似的理解,很多都牽涉到數學裏邊的微積分,矩陣,等等。
(雖然自信在大學和高中的時候,我的數學都是非常不錯的,常常處於班級和校級頭三名中,
我記得大學時候沒事幹的時候,還專門畫過貝塞爾曲線等這些曲線來玩,
但是隔了這麼多年,基礎知識早就還給老師了。)
比如進行圖像邊緣檢測的各種算子,比如梯度算子,索貝爾算子,拉普拉斯算子,cany算子等等。
通過這些算子運算,能檢測出圖像的大致輪廓。
然後突然明白,如何能識別到車牌,如何能檢測到物體是否運動。

下面視頻是顏色xdisp_virt 特效,可以看到,本來是能達到60FPS的,特效之後,FPS立馬降下來了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章