分享
 
 
 

轻松实现视频渐入渐出

王朝vc·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

陆其明 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,本文还完成了视频渐入渐出的效果演示。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有