轻松实现视频渐入渐出

类别:VC语言 点击:0 评论:0 推荐:

陆其明 2004-08-18

 http://hqtech.nease.net/Article/DS_VideoTransition.htm

 

在一段主题视频内容的开始部分或者结束部分加入渐变特效,是当今很多影视片制作的惯用手法。这种特效处理,带给观众的将是更自然、更舒适的视觉享受。作为程序员,你想过如何来实现这种特效吗?本文介绍的就是一种实现视频渐入渐出的简单易行的编程方法。

 

一.        渐入渐出算法与实现

 

本文将介绍一种类似于时针扫描的渐入渐出效果。首先,我们来描述一下时针扫描的运动过程。

 

1 时针扫描过程

 

如图1,直线L1保持不动,直线L2以恒定的速率逆时针转动。L1和L2将视频图像帧分成两个区域:扇形区域1和扇形区域2。随着L2的旋转,扇形区域1的面积越来越大,扇形区域2的面积越来越小;直到L2旋转360度,最终与L1重合。如果是视频渐入,扇形区域1显示的就是主题视频内容,扇形区域2显示的就是背景色;总体的效果是:从一开始整幅的背景色,逐渐过渡到主题视频内容。如果是视频渐出,扇形区域1显示的就是背景色,扇形区域2显示的就是主题视频内容;总体的效果是:正在播放的主题视频内容上被一点一点覆盖上背景色,直至背景色占据整个图像帧范围(表示主题视频内容播放完毕)。

下面,笔者将给出上述这个渐入渐出过程的C++实现。为了方便起见,我们将一幅图像分成如图2的4个区域。

 

2 时针扫描图像帧的区域划分

 

当视频渐入时,随着直线L2的旋转,它会依次落在第1区域、第2区域、第3区域和第4区域。当L2位于第1区域,第2、3、 4区域应该完全填上背景色,还有第1区域内L2上面的部分(通过计算L2的斜率来判断)也要填上背景色(第1区域内剩余的部分自然是显示主题视频内容);当L2位于第2区域,第3、 4区域应该完全填上背景色,另加第2区域内L2下面的部分;当L2位于第3区域,第 4区域应该完全填上背景色,另加第3区域内L2下面的部分;当L2位于第4区域,仅第4区域内L2上面的部分填上背景色。整个视频渐入过程如图3。(注:黑色为背景色,白色区域显示主题视频内容。)

 

3 L2分别落在4个区域的情况(视频渐入)

 

视频渐入的C++实现

// 图像帧数据(注意:图像数据是以从下往上、从左往右的顺序存储的!)

unsigned char * pData;

unsigned char * m_pPixel;      // 像素指针

unsigned char * m_pSubLine; // 行指针

// m_nWidth和m_nHeight为图像的宽度和高度(以像素为单位)

// m_nOriginalX 和m_nOriginalY为所分区域的宽度和高度

int m_nOriginalX  = m_nWidth / 2;

int m_nOriginalY  = m_nHeight / 2;

//……

 

// 假设整个运动过程在36个视频帧的时间内完成,

// 则L2每次步进的角度m_dStepAngle为10度

double m_dStepAngle = 360. / 36.;

// m_lProgress表示运动的进度,每次递增1,取值范围0~36

// Alpha表示L1和L2当前形成的角度,如图1所示

double  Alpha = m_lProgress * m_dStepAngle;

// m_dSlope表示L2的斜率

const double m_pi = 3.1415926535;

double m_dSlope = fabs(tan(Alpha * m_pi / 180.));

// m_pxlConverter为一个自定义的像素转化器,

// GetPixelSize()函数返回每个像素使用的字节数

// 计算图像帧的宽度(以字节为单位)

int m_nLineBytes = m_nWidth * m_pxlConverter->GetPixelSize();

 

if (Alpha < 90)        // L2位于第1区域

{

                // 第2区域应该填上背景色

                DrawSecondRegion(pData);

// 第3、4区域应该填上背景色

                DrawBelowHalf(pData);

                // 第1区域的一部分应该填上背景色

                // 扫描图像帧第1区域中的各个像素                           

                for (int y = m_nOriginalY; y < m_nHeight; y++)

                {

                                for (int x = m_nOriginalX; x < m_nWidth; x++)

                                {

                                                // 将图像帧第1区域中L2上面的像素替换为背景色

                                                if ((y - m_nOriginalY) >= m_dSlope * (x - m_nOriginalX))

                                                {

                                                                // 定位到(x,y)表示的像素

                                                                m_pPixel  = pData + y * m_nLineBytes;

                                                                m_pPixel += x * m_pxlConverter->GetPixelSize();

                                                                // 将(x,y)位置的像素替换为背景色

                                                                m_pxlConverter->Convert(m_pPixel);

                                                }

                                }

                }

}

else if (Alpha >= 90 && Alpha < 180)   // L2位于第2区域

{

                // 第3、4区域应该填上背景色

                DrawBelowHalf(pData);

                // 第2区域的一部分应该填上背景色

                for (int y = m_nOriginalY; y < m_nHeight; y++)

                {

                                for (int x = 0; x < m_nOriginalX; x++)

                                {

                                                // 将图像帧第2区域中L2下面的像素替换为背景色

                                                if ((y - m_nOriginalY) <= m_dSlope * (m_nOriginalX - x))

                                                {

                                                                m_pPixel  = pData + y * m_nLineBytes;

                                                                m_pPixel += x * m_pxlConverter->GetPixelSize();

                                                                m_pxlConverter->Convert(m_pPixel);

                                                }

                                }

                }

}

else if (Alpha >= 180 && Alpha < 270)   // L2位于第3区域

{

                // 第4区域应该填上背景色

                DrawFourthRegion(pData);

                // 第3区域的一部分应该填上背景色

                for (int y = 0; y < m_nOriginalY; y++)

                {

                                for (int x = 0; x < m_nOriginalX; x++)

                                {

                                                // 将图像帧第3区域中L2下面的像素替换为背景色

                                                if ((m_nOriginalY - y) >= m_dSlope * (m_nOriginalX - x))

                                                {

                                                                m_pPixel  = pData + y * m_nLineBytes;

                                                                m_pPixel += x * m_pxlConverter->GetPixelSize();

                                                                m_pxlConverter->Convert(m_pPixel);

                                                }

                                }

                }

}

else if (Alpha >= 270 && Alpha < 360)   // L2位于第4区域

{

                // 第4区域的一部分应该填上背景色

                for (int y = 0; y < m_nOriginalY; y++)

                {

                                for (int x = m_nOriginalX; x < m_nWidth; x++)

                                {

                                                // 将图像帧第4区域中L2上面的像素替换为背景色

                                                if ((m_nOriginalY - y) <= m_dSlope * (x - m_nOriginalX))

                                                {

                                                                m_pPixel  = pData + y * m_nLineBytes;

                                                                m_pPixel += x * m_pxlConverter->GetPixelSize();

                                                                m_pxlConverter->Convert(m_pPixel);

                                                }

                                }

                }

}

m_lProgress++;

 

当视频渐出时,随着直线L2的旋转,它同样会依次落在第1、2、3、4区域。当L2位于第1区域,仅第1区域内L2下面的部分填上背景色;当L2位于第2区域,第1区域应该完全填上背景色,另加第2区域内L2上面的部分;当L2位于第3区域,第1、2区域应该完全填上背景色,另加第3区域内L2上面的部分;当L2位于第4区域,第1、2、3区域应该完全填上背景色,另加第4区域内L2下面的部分。整个视频渐出过程如图4。(注:黑色为背景色,白色区域显示主题视频内容。)

 

4 L2分别落在4个区域的情况(视频渐出)

 

视频渐出的C++实现

if (Alpha < 90)        // L2位于第1区域

{

                // 第1区域的一部分应该填上背景色           

                for (int y = m_nOriginalY; y < m_nHeight; y++)

                {

                                for (int x = m_nOriginalX; x < m_nWidth; x++)

                                {

                                                // 将图像帧第1区域中L2下面的像素替换为背景色

                                                if ((y - m_nOriginalY) <= m_dSlope * (x - m_nOriginalX))

                                                {

                                                                m_pPixel  = pData + y * m_nLineBytes;

                                                                m_pPixel += x * m_pxlConverter->GetPixelSize();

                                                                m_pxlConverter->Convert(m_pPixel);

                                                }

                                }

                }

}

else if (Alpha >= 90 && Alpha < 180)   // L2位于第2区域

{

                // 第1区域应该填上背景色

                DrawFirstRegion(pData);

                // 第2区域的一部分应该填上背景色

                for (int y = m_nOriginalY; y < m_nHeight; y++)

                {

                                for (int x = 0; x < m_nOriginalX; x++)

                                {

                                                // 将图像帧第2区域中L2上面的像素替换为背景色

                                                if ((y - m_nOriginalY) >= m_dSlope * (m_nOriginalX - x))

                                                {

                                                                m_pPixel  = pData + y * m_nLineBytes;

                                                                m_pPixel += x * m_pxlConverter->GetPixelSize();

                                                                m_pxlConverter->Convert(m_pPixel);

                                                }

                                }

                }

}

else if (Alpha >= 180 && Alpha < 270)  // L2位于第3区域

{

                // 第1、2区域应该填上背景色

                DrawAboveHalf(pData);

                // 第3区域的一部分应该填上背景色

                for (int y = 0; y < m_nOriginalY; y++)

                {

                                for (int x = 0; x < m_nOriginalX; x++)

                                {

                                                // 将图像帧第3区域中L2上面的像素替换为背景色

                                                if ((m_nOriginalY - y) <= m_dSlope * (m_nOriginalX - x))

                                                {

                                                                m_pPixel  = pData + y * m_nLineBytes;

                                                                m_pPixel += x * m_pxlConverter->GetPixelSize();

                                                                m_pxlConverter->Convert(m_pPixel);

                                                }

                                }

                }

}

else if (Alpha >= 270 && Alpha < 360)  // L2位于第4区域

{

                // 第1、2区域应该填上背景色

                DrawAboveHalf(pData);

// 第3区域应该填上背景色

                DrawThirdRegion(pData);

                // 第4区域的一部分应该填上背景色

                for (int y = 0; y < m_nOriginalY; y++)

                {

                                for (int x = m_nOriginalX; x < m_nWidth; x++)

                                {

                                                // 将图像帧第4区域中L2下面的像素替换为背景色

                                                if ((m_nOriginalY - y) >= m_dSlope * (x - m_nOriginalX)) // Below the line...

                                                {

                                                                m_pPixel  = pData + y * m_nLineBytes;

                                                                m_pPixel += x * m_pxlConverter->GetPixelSize();

                                                                m_pxlConverter->Convert(m_pPixel);

                                                }

                                }

                }

}

else if (Alpha >= 360) // L2与L1重合之后…

{

                // 将整个图像帧都填上背景色

                m_pSubLine = pData;

                for (int y = 0; y < m_nHeight; y++)

                {

                                m_pPixel = m_pSubLine;

                                for (int x = 0; x < m_nWidth; x++)

                                {

                                                // 将当前像素替换为背景色

                                                m_pxlConverter->Convert(m_pPixel);

                                                // 指向当前像素的下一个像素

                                                m_pPixel = m_pxlConverter->NextPixel(m_pPixel);

                                }

                                // 指向下一行

                                m_pSubLine += m_nLineBytes;

                }

}

m_lProgress++;

 

 

二.组件开发与演示

 

有了时针扫描渐入渐出的算法实现,接下去的问题就是,如何来获取连续的视频图像帧数据?在这里我们可以借助于DirectX SDK自带的一个工具软件GraphEdit(即SDK目录下的Bin\DXUtils\graphedt.exe)。运行GraphEdit,如图5:

 

5 GraphEdit工具软件

 

执行菜单命令File | Render Media File…,在随后弹出的对话框中选择一个多媒体文件(比如选定一个MPEG1文件butterfly.mpg),自动构建如图6的链路:

 

6 使用GraphEdit构建的播放链路

 

然后执行菜单命令Graph | Play就可以对butterfly.mpg文件实现播放了。同样执行Graph | Pause或Graph | Stop就可以暂停或停止当前的播放。

值的注意的是,GraphEdit播放butterfly.mpg文件采用的就是DirectShow技术!大家知道,DirectX是微软公司提供的一套在Windows平台上开发高性能图形、声音、输入、输出和网络游戏的编程接口;而DirectShow就是DirectX的一个成员,专门用于音视频数据采集、多媒体文件播放等方面的应用。DirectShow中最基本的功能模块叫做Filter(图6中每个矩形块都代表一个Filter);每个Filter都至少有一个Pin,用于接收数据或者输出数据;Filter总是完成一定的功能(图6中左边第一个Filter是文件源,MPEG-1 Stream Splitter负责将MPEG1数据流中的音频和视频分离,MPEG Audio Decoder负责将MPEG格式的音频数据解码,MPEG Video Decoder负责将MPEG格式的视频数据解码,Default DirectSound Device负责音频播放,Video Renderer负责视频显示);各种Filter按照一定的顺序串联起来,相互协作;数据在Filter之间沿着箭头的方向流动,直到Default DirectSound Device和Video Renderer。

DirectShow是一个模块化的、开放性的应用框架。我们可以开发自己的Filter组件,然后插入到Filter链路中的某个位置,以获得处理数据流的机会。拿本文需要实现的视频渐入渐出来说,我们完全可以将渐入渐出算法实现在一个Filter中,然后将其连接到视频解码Filter后面,以获取连续的、非压缩的图像帧数据。我们把这个Filter取名为“HQ Video Transition”;因为这个Filter可以在输入的图像帧上“就地”修改数据,因此Filter可以采用Trans-In-Place模型;Filter接受16位、24位和32位RGB格式的数据输入。HQ Video Transition开发完成后生成HqVidTrans.ax文件(假设放在C:\下),然后使用系统的Regsvr32.exe注册(方法是:执行命令行Regsvr32 C:\HqVidTrans.ax)。(注意:关于DirectShow Filter开发方法更细节的介绍,限于篇幅,笔者在这里就不作展开了;有兴趣的读者可以参考笔者的两本拙作《DirectShow开发指南》和《DirectShow实务精选》。HQ Video Transition Filter的源代码请读者到http://hqtech.nease.net下载。)

Filter组件开发完成并且成功注册之后就可以在GraphEdit中使用了。首先还是构建如图6的Filter链路。然后执行菜单命令Graph | Insert Filters…,在随后弹出的对话框中点开“DirectShow Filters”目录,然后找到“HQ Video Transition”一项双击将其加入。接着将MPEG Video Decoder与Video Renderer的连接断开(用鼠标选中这两个Filter之间的箭头后按下键盘的Delete键)。然后将MPEG Video Decoder连向HQ Video Transition,再将HQ Video Transition连向Video Renderer。(两个Filter之间的连接方法:首先在欲连接的上一级Filter的输出Pin上按住鼠标左键不放,拖动鼠标到下一级Filter的输入Pin上,最后放开鼠标左键。)最终的Filter链路如图7:

 

7 GraphEdit中使用渐入渐出Filter

 

现在,执行菜单命令Graph | Play,我们就可以看到视频渐入渐出的演示了,如图8和图9所示:

 

8 视频渐入的演示

 

9 视频渐出的演示

 

 

三.        小结

 

本文介绍了视频渐入渐出的原理,以及一种时针扫描效果的算法和实现。随后借助于DirectShow,本文还完成了视频渐入渐出的效果演示。

本文地址:http://com.8s8s.com/it/it719.htm