上一篇博文介绍了渲染时叠加字幕的技术实现方法,而这一篇给大家讲解怎么用修改视频像素的方法叠加字幕和Logo。下面我把叠加字幕和Logo都统称为叠加OSD。
图像有分YUV和RGB格式,每个像素都有一个坐标和地址,我们要在图像指定地方叠加OSD,其实就是修改这些地方某些像素的像素值。叠加字幕原理简单描述就是:在视频图像上指定位置的像素值用OSD前景的颜色值代替。什么是OSD前景颜色?对字幕来说,前景色就是字符的文字颜色,即字体部分的像素的颜色,一般颜色我们用RGB表示,如果我们要显示红色字体的字符,那么可指定OSD前景颜色为RGB(255,0,0),举个例子,如果我们要在坐标(3,3)-(8,8)这个矩形区域上显示一段红色的文字“中国”,OSD的前景色为RGB(255,0,0),进行字幕叠加的时候,我们将颜色值为RGB(255,0,0)的像素保留,而背景像素(非红色的像素)则用原视频图像相应位置的像素代替,换句话说,在OSD区域除OSD的字符部分,其他部分都用视频图像的原来的像素值。对Logo(可看作是一个位图)来说,位图也有分前景和背景,但是跟叠加字幕有一点点区别,一张丰富颜色的位图前景往往包括很多种颜色(除非那张是一个单色位图),我们不能通过单一的前景色来过滤像素,常用的方法是根据背景色或Alpha通道来设置背景是否透明。位图的背景色可指定为跟前景不一样的一种颜色值,这种颜色也叫遮罩色。位图上跟背景遮罩色颜色相同的像素就“隐藏”掉,而跟遮罩色不同的像素则“覆盖”到视频图像上。另外对于位图还可以根据Alpha通道过滤背景,对32位位图,一个颜色有4个分量:R,G,B,A,其中A就是Alpha通道,Alpha通道的值表示这个像素在图像上的透明度,最小是0(透明),最大是255(完全不透明)。Alpha通道让叠加透明背景的图片更加方便,能让OSD的某一部分像素显示,而另一部分隐藏,现在很多PNG图片就是带Alpha通道的。
下面将给大家讲解一个例子,用到修改像素值的方法来叠加OSD(包括字幕和位图)。这个例子名字叫SubtitleMixVideo,参考了陆琪明先生的《DirectShow实务精选》一书中的一个例子--TitleOverlayFilter,例子是在RGB图像上进行叠加OSD,其中视频图像格式支持RGB565/RGB555/RGB24/RGB32,还没有实现YUV上进行叠加。事实上,在YUV图像上叠加OSD也是可以的(很多电影的字幕插件就实现了这个功能),原理跟RGB图像的一样,只是因为图像像素排列不同,计算像素的坐标时有点区别。
我写的SubtitleMixVideo例子基于TitleOverlayFilter的例程修改,但在上面做了很多修改和优化,主要改进点是:
1. 支持叠加字幕和Logo位图,原例程只支持叠加字幕。
2. 可动态设置OSD的属性(文字内容、字体颜色、坐标等),可动态打开或关闭OSD。
3. 支持多个OSD区域,显示OSD的数量可自由设置。
4. 优化OSD叠加的性能,如果OSD文字内容没有修改,则不重建OSD的DIB位图。
5. 去除DirectShow Filter的封装。原例程叠加字幕是做成一个DirectShow插件的,要依赖于DirectShow SDK,现在去除相关引用后,更方便用户使用。
6. 简化了其中的一些复杂流程,将多个子类合并到一个类中处理。
SubtitleMixVideo程序针对字幕叠加的处理流程图(叠加Logo的流程与之相似)如下:
其中,读取视频,解码视频的模块用到开源的FFmpeg库,这个对大家来说应该不陌生。而显示图像(渲染模块)用GDI,支持显示RGB格式的图像,虽然用GDI渲染性能不是很好,但是这里显示图像只是作为测试用,重要工作是做字幕叠加,所以就没有用到更复杂的画图API。
关于叠加字符的流程,这里再详细说一下:首先在内存中建立一张二色位图,然后在这个位图画出字符内容(主要通过一些Windows GDI函数)。于是,得到了一块含有字符的字符点阵:0--表示背景,1--表示前景像素。在实际叠加的时候,我们将图像帧知道位置的像素与字符点阵的像素对应,如果点阵的像素位值为0,则保持图像帧对应的像素值不变;如果为1,则图像帧对应的像素值替换为用户设置的字符颜色值。
字符叠加的类是CFilterTitleOverlay,我们先认识一下CFilterTitleOverlay类,看看它有什么方法和成员变量,下面是这个类的声明:
enum OverlayFilterState
{
ST_FILTER_STOPPED = 0,
ST_FILTER_ACTIVE = 1,
};
#define MAX_OVERLAY_NUM 4 //叠加OSD的最大个数
class CFilterTitleOverlay
{
public:
CFilterTitleOverlay();
~CFilterTitleOverlay();
HRESULT SetInputVideoInfoToController(RGB_FORMAT colorSpace, VIDEOINFOHEADER * pbFormat); //设置输入视频的信息,第一个参数为像素格式,第二个参数为视频头信息
virtual HRESULT StartStreaming();
virtual HRESULT StopStreaming();
HRESULT DoTitleOverlay(BYTE * pData);
void LockOSD(int nIndex); //对OSD属性加锁
void UnlockOSD(int nIndex); //对OSD属性解锁
//说明:下面设置OSD或获取OSD属性的接口中用到了锁,作用是为了防止在多线程环境中设置OSD属性与OSD叠加(DoTitleOverlay)在不同线程中调用引起状态不同步的问题。但是,当要同时多个OSD属性时,频繁的加锁解锁引起比较大的性能开销,
//所以,为了优化性能,我将加锁与解锁分离出来,做成两个单独的接口,开发者如果要设置OSD属性,请按如下方式调用:
/*
LockOSD(nIndex);
put_TitleXXX(nIndex, ...);
put_TitleYYY(nIndex, ...);
put_TitleEnable(nIndex, TRUE);
UnlockOSD(nIndex);
*/
// --- ITitleOverlay methods ---
HRESULT put_TitleOverlayType(int nIndex, long inOverlayType);
HRESULT get_TitleOverlayType(int nIndex, long * outOverlayType);
HRESULT put_TitleOverlayStyle(int nIndex, int inUsingCover);
HRESULT get_TitleOverlayStyle(int nIndex, int * outUsingCover);
HRESULT put_Title(int nIndex, const char * inTitle, int inLength);
HRESULT get_Title(int nIndex, char * outBuffer, int * outLength);
HRESULT put_TitleColor(int nIndex, BYTE inR, BYTE inG, BYTE inB);
HRESULT get_TitleColor(int nIndex, BYTE * outR, BYTE * outG, BYTE * outB);
HRESULT put_TitleStartPosition(int nIndex, POINT inStartPos);
HRESULT get_TitleStartPosition(int nIndex, POINT * outStartPos);
HRESULT put_TitleFont(int nIndex, LOGFONT inFont);
HRESULT get_TitleFont(int nIndex, LOGFONT * outFont);
//HRESULT put_TitleDuration(double inStart, double inEnd);
//HRESULT get_TitleDuration(double * outStart, double * outEnd);
HRESULT put_TitleEnable(int nIndex, BOOL bEnable); //是否使OSD生效
HRESULT put_TitleBitmap(int nIndex, HBITMAP hBmp, BOOL byAlpha, COLORREF colorKey); //传入的位图句柄必须是DIB位图
HRESULT put_TitleBitmap(int nIndex, LPCTSTR lpszImagePath, BOOL byAlpha, COLORREF colorKey); //传入OSD图标的路径
protected:
void ReleaseOverlayController(int nIndex);
//void SideEffectOverlayTypeChanged(int nIndex);
private:
OVERLAY_TYPE mOverlayType[MAX_OVERLAY_NUM];
COverlayController * mOverlayController[MAX_OVERLAY_NUM];
CCritSec mITitleOverlaySync; //互斥锁
//BOOL mNeedEstimateFrameRate;
RGB_FORMAT m_colorSpace; //输入视频的像素格式
VIDEOINFOHEADER m_VideoInfoHeader; //视频头信息
OverlayFilterState m_State; //0--非活动,1--活动(开始叠加字幕)
};
CFilterTitleOverlay类有个很重要的成员指针变量mOverlayController,它指向一个COverlayController结构类型的数据成员(数组),这成员变量负责真正的叠加OSD的工作,它是一个数组,每个元素管理一个OSD区域,目前设置的最大OSD区域数为4,即最多可叠加4个OSD。
接着看看COverlayController类的声明:
class COverlayController
{
protected:
//CBasePixel * mPixelConverter;
BOOL mCanDoOverlay; // Title overlay work