在视频中实现图像特效

                                                                     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立马降下来了。

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