|
陆其明 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);
&n |