怎麼在視頻上疊加字幕和Logo--技術實現2

上一篇博文介紹了渲染時疊加字幕的技術實現方法,而這一篇給大家講解怎麼用修改視頻像素的方法疊加字幕和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 workable flag
	BOOL                mIsOverlayByCover;
	long                mOverlayCounter;


	// Title property
	char *              mTitle;
	unsigned char *     mTitleDIBBits;

	unsigned char       mColorRed;     // OSD Text color
	unsigned char       mColorGreen;
	unsigned char       mColorBlue;

	SIZE                mTitleSize;
	POINT               mStartPos;     // Start position to draw text
	LOGFONT             mTitleFont;
	BOOL                mIsFontChanged;
	long                mDIBWidthInBytes;

	// Input video property
	RGB_FORMAT          mInputColorSpace;
	long                mImageWidth;
	long                mImageHeight;        // This is an absolute value
	long                mImageWidthInBytes;  // the line width in bytes
	WORD                mImageBitCount;
	BOOL                mIsBottomUpImage;
	double              mInputFrameRate;

	BOOL                mbEnable; //是否啓用OSD
	BOOL                mbOSDPropertyChanged; //OSD屬性是否有改變(包括OSD文字內容,OSD顏色,OSD字體風格),當這些屬性改變,需要重建OSD圖層(DIB位圖)

	OVERLAY_TYPE        mOverlayType; //靜態文字/OSD時間/圖標

	//跟疊加Logo相關的幾個變量
	CImage              mLogoImage; //加載的Logo圖片
	BOOL                mbOverlayByAlpha; //是否根據RGBA位圖的Alpha通道決定OSD的像素是否透明,如果透明,則目標像素是視頻圖像對應位置的像素值,該開關僅支持PNG圖片(32位)。如果該開關爲False,則根據下面的遮罩色來定義混合後的顏色
	COLORREF            mColorKey; //遮罩色(背景透明色),OSD區域的像素顏色值等於遮罩色的地方透明

public:
	COverlayController();
	virtual ~COverlayController();

	void SetOverlayType(OVERLAY_TYPE type);
    OVERLAY_TYPE  GetOverlayType();

	void SetInputColorSpace(RGB_FORMAT inColorSpace);
	void SetInputVideoInfo(const VIDEOINFOHEADER * inInfoHeader);
	// If VIDEOINFOHEADER not contain valid frame rate, 
	// using the following method to set an estimated value.
	void SetEstimatedFrameRate(double inFrameRate);
	void SetOverlayStyle(BOOL inUsingCover);
	void GetOverlayStyle(BOOL * outUsingCover);  

	virtual void SetTitle(const char * inTitle, int inLength);
	void SetTitleColor(BYTE inR, BYTE inG, BYTE inB); 
	void SetTitleStartPosition(POINT inStartPos);
	void SetTitleFont(LOGFONT inFont);

	//設置疊加的Logo圖標的屬性
	BOOL  SetOverlayBitmap(HBITMAP hBmp, BOOL bOverlayByAlpha, COLORREF colorKey);
	BOOL  SetOverlayBitmap(LPCTSTR pszImagePath, BOOL bOverlayByAlpha, COLORREF colorKey);

	// Get the title length by passing NULL to outBuffer
	int  GetTitle(char * outBuffer);
	void GetTitleColor(BYTE * outR, BYTE * outG, BYTE * outB);
	void GetTitleStartPosition(POINT * outStartPos);
	void GetTitleFont(LOGFONT * outFont);
	
	void SetTitleEnable(BOOL bEnable); //是否啓用OSD
	
public:
	virtual BOOL StartTitleOverlay(void);
	virtual BOOL StopTitleOverlay(void);
	BOOL DoTitleOverlay(PBYTE inImage);

protected:
	void ReleasePixelConverter(void);
	void ReleaseTitleBuffer(void);
	void ReleaseTitleDIB(void);

	BOOL CreateTitleDIBBits(void);
	HBITMAP ActualCreateTitleDIB(HDC inDC);
	virtual BOOL ValidateTitleDIBSize(void);

	virtual void SideEffectProgressChanged(void);
	virtual void SideEffectColorSpaceChanged(void);
	virtual void SideEffectFontChanged(void);
	
	// Subclass can use these methods to do more details
	virtual BOOL BeforeActualOverlay(void);
	virtual BOOL AfterActualOverlay(void);
	virtual BOOL ActualOverlay(PBYTE inImage);

	BOOL  OverlayText(PBYTE inImage);
	BOOL  OverlayBitmap(PBYTE inImage);
};

這個類支持三種類型的對象疊加,三種類型是: OT_STATIC,OT_SYSTIME,OT_LOGO。分別對應靜態文字,系統時間,Logo(位圖)。

這個類提供了設置和獲取每個OSD區域屬性的方法,可設置的OSD屬性包括:OSD打開、關閉,OSD字符內容,OSD文字顏色,OSD座標,OSD採用的字體樣式,設置OSD位圖的位圖路徑或位圖句柄,背景透明方式(採用遮罩色或Alpha通道)。

下面是其中幾個設置/獲取OSD屬性的函數的實現:

void COverlayController::SetOverlayStyle(BOOL inUsingCover)
{
	mIsOverlayByCover = inUsingCover;
	//mbOSDPropertyChanged = TRUE;
}

void COverlayController::GetOverlayStyle(BOOL * outUsingCover)
{
	*outUsingCover = mIsOverlayByCover;
}

void COverlayController::SetTitle(const char * inTitle, int inLength)
{
	ReleaseTitleBuffer();
	mTitle = new char[inLength + 1];
	if (inTitle && mTitle)
	{
		strcpy(mTitle, inTitle);
	}
	mbOSDPropertyChanged = TRUE;
}

void COverlayController::SetTitleColor(BYTE inR, BYTE inG, BYTE inB)
{
	mColorRed   = inR;
	mColorGreen = inG;
	mColorBlue  = inB;
	mbOSDPropertyChanged = TRUE;
}

void COverlayController::SetTitleStartPosition(POINT inStartPos)
{
	mStartPos.x = inStartPos.x;
	mStartPos.y = inStartPos.y;
	//mbOSDPropertyChanged = TRUE;
}

void COverlayController::SetTitleFont(LOGFONT inFont)
{
	mTitleFont     = inFont;
	mIsFontChanged = TRUE;
	// Do title font validation
	SideEffectFontChanged();  
	mbOSDPropertyChanged = TRUE;
}


// Commonly, invoker should get the title length first by passing NULL to outBuffer
int COverlayController::GetTitle(char * outBuffer)
{
	int titleLength = 0;
	if (mTitle)
	{
		titleLength = strlen(mTitle) + 1;
		if (outBuffer)
		{
			strcpy(outBuffer, mTitle);
		}
	}
	return titleLength;
}

void COverlayController::GetTitleColor(BYTE * outR, BYTE * outG, BYTE * outB)
{
	*outR = mColorRed;
	*outG = mColorGreen;
	*outB = mColorBlue;
}

void COverlayController::GetTitleStartPosition(POINT * outStartPos)
{
	outStartPos->x = mStartPos.x;
	outStartPos->y = mStartPos.y;
}

void COverlayController::GetTitleFont(LOGFONT * outFont)
{
	*outFont = mTitleFont;
}

void COverlayController::SetTitleEnable(BOOL bEnable)
{
	mbEnable = bEnable;
}

接着,讓我們看看COverlayController是如何創建字幕的二色位圖的,創建字幕Dib位圖主要實現是在ActualCreateTitleDIB函數。


HBITMAP COverlayController::ActualCreateTitleDIB(HDC inDC)
{
	//注意這裏創建的DIB位圖是二值位圖,,即BitCount = 1,像素值爲0,1

	// DIB info we used to create title pixel-mapping.
	// The system default color policy is: 
	// Initial Whole Black, while output area White-background and Black-text.
	struct {
		BITMAPINFOHEADER bmiHeader;
		DWORD rgbEntries[2];
	} bmi =
	{
		{
			sizeof(BITMAPINFOHEADER),
			0,
			0,
			1,	
			1,	
			BI_RGB,
			0,
			0,
			0
		},
		{
			0x00000000,
			0xFFFFFFFF
		}
	};


	// We change the system default color policy.
	// That is, we use Black-background and White-text.
	// We do so especially for rotation font using.
//	SetBkColor(hdc, RGB(0, 0, 0));
//	SetTextColor(hdc, RGB(255, 255, 255));

	// Set tile font here, so we can get the exact size of the title
	CAutoFont  autoFont;
	if (mIsFontChanged)
	{
	//	autoFont.CreateFont("Arial");  // Testing
		autoFont.CreateFont(mTitleFont);
		autoFont.SelectToDC(inDC);
	}
	GetTextExtentPoint32(inDC, mTitle, lstrlen(mTitle), &mTitleSize);
	// Overridable to change title DIB size
	if (!ValidateTitleDIBSize())
	{
		return NULL;
	}

	// Set proper DIB size here! Important!
	bmi.bmiHeader.biHeight = mTitleSize.cy;
	bmi.bmiHeader.biWidth  = mTitleSize.cx;

	TRACE("ActualCreateTitleDIB----biWidth: %d, biHeight: %d, biBitCount: %d \n",
		bmi.bmiHeader.biWidth, bmi.bmiHeader.biHeight, bmi.bmiHeader.biBitCount);

	HBITMAP hbm = CreateDIBitmap(inDC, &bmi.bmiHeader, 0, NULL, NULL, 0);
	BOOL   pass = (hbm != NULL);
	// Draw title after selecting DIB into the DC
	if (pass)
	{
		HGDIOBJ hobj = SelectObject(inDC, hbm);
		pass = ExtTextOut(inDC, 0, 0, ETO_OPAQUE | ETO_CLIPPED, NULL, 
			mTitle, lstrlen(mTitle), NULL);
		SelectObject(inDC, hobj);
	}
	// Get the title-drew DIB bits
	if (pass)
	{
		ReleaseTitleDIB();
		// Attention: To get bitmap data from the DIB object,
		// the scan line must be a multiple of 4 (DWORD)!
		// If the actual bitmap data is not exactly fit for DWORD,
		// The rest of DWORD bits will be filled automatically.
		// So we should expand to bytes and round up to a multiple of 4.
		mDIBWidthInBytes = ((mTitleSize.cx + 31) >> 3) & ~3;

		mTitleDIBBits    = new BYTE[mDIBWidthInBytes * mTitleSize.cy];
		memset(mTitleDIBBits, 0, mDIBWidthInBytes * mTitleSize.cy);
		LONG lLines = GetDIBits(inDC, hbm, 0, mTitleSize.cy, (PVOID)mTitleDIBBits, 
			(BITMAPINFO *)&bmi, DIB_RGB_COLORS);
		pass = (lLines != 0);
	}

	if (!pass && hbm)
	{
		DeleteObject(hbm);
		hbm = NULL;
	}
	return hbm;
}

而疊加字幕的工作是在OverlayText函數裏實現,下面是這個函數的代碼:

BOOL  COverlayController::OverlayText(PBYTE inImage)
{
	if(mTitleDIBBits == NULL)
		return FALSE;

    unsigned char    OSDTextColorR = mColorRed;     //OSD文字的前景顏色
	unsigned char    OSDTextColorG = mColorGreen;
	unsigned char    OSDTextColorB = mColorBlue;

	// Now copy the data from the DIB section (which is usually bottom-up)
	// but first check if it's too big
	if (mImageWidth > mStartPos.x && mImageHeight > mStartPos.y && 
		mTitleSize.cx > 0 && mTitleSize.cy > 0) 
	{
		long actualOverlayWidth  = min((mImageWidth - mStartPos.x), mTitleSize.cx);
		long actualOverlayHeight = min((mImageHeight - mStartPos.y), mTitleSize.cy);
		// Image may be bottom-up, may be top-down.
		// Anyway retrieve the pointer which point to the top line
		PBYTE   pTopLine      = NULL;
		long    strideInBytes = 0;
		if (mIsBottomUpImage)
		{
			strideInBytes = -mImageWidthInBytes;
			pTopLine      = inImage + mImageWidthInBytes * (mImageHeight - 1);	
		}
		else
		{
			strideInBytes = mImageWidthInBytes;
			pTopLine      = inImage;
		}

		int inputPixelSize = 0; //輸入視頻的圖像像素大小
		unsigned char       m_Mask[2] = {0};   // Mask color

		switch (mInputColorSpace)
		{
		case FT_RGB8:
			inputPixelSize = 1;
			break;

		case FT_RGB555:
			{
			inputPixelSize = 2;
			const unsigned int bits555[] = {0x7C00, 0x03E0, 0x001F}; // RGB
			// Caculate the mask bits
			unsigned int  wMask, wTemp;
			wTemp  = unsigned int (OSDTextColorB / 256. * 32);
			wMask  = wTemp & bits555[2];
			wTemp  = unsigned int (OSDTextColorG / 256. * 32);
			wTemp  = wTemp << 5;
			wMask += wTemp & bits555[1];
			wTemp  = unsigned int (OSDTextColorR / 256. * 32);
			wTemp  = wTemp << 10;
			wMask += wTemp & bits555[0];
			// Store the high byte and low byte seperately
			m_Mask[0] = wMask & 0xff;
			wMask     = wMask >> 8;
			m_Mask[1] = wMask & 0xff;
			}
			break;

		case FT_RGB565:
			{
			inputPixelSize = 2;
			const unsigned int bits565[] = {0xF800, 0x07E0, 0x001F}; // RGB
			// Caculate the mask bits
			unsigned int  wMask, wTemp;
			wTemp  = unsigned int (OSDTextColorB / 256. * 32);
			wMask  = wTemp & bits565[2];
			wTemp  = unsigned int (OSDTextColorG / 256. * 64);
			wTemp  = wTemp << 5;
			wMask += wTemp & bits565[1];
			wTemp  = unsigned int (OSDTextColorR / 256. * 32);
			wTemp  = wTemp << 11;
			wMask += wTemp & bits565[0];
			// Store the high byte and low byte seperately
			m_Mask[0] = wMask & 0xff;
			wMask     = wMask >> 8;
			m_Mask[1] = wMask & 0xff;
			}
			break;

		case FT_RGB24:
			inputPixelSize = 3;
			break;

		case FT_RGB32:
			inputPixelSize = 4;
			break;
		}


		PBYTE  pStartPos = pTopLine + mStartPos.y * strideInBytes + mStartPos.x * mImageBitCount / 8; //指向視頻圖像OSD區域的左上角像素
		for (DWORD dwY = 0; dwY < (DWORD)actualOverlayHeight; dwY++) 
		{
			PBYTE pbTitle  = mTitleDIBBits + mDIBWidthInBytes * ((DWORD)mTitleSize.cy - dwY - 1); //指向OSD的某一行起點像素
			for (DWORD dwX = 0; dwX < (DWORD)actualOverlayWidth; dwX++) 
			{
				// dwX & 7, value from 0 - 7
				// 0x80 >> (dwX & 7), value from 10000000 to 00000001
				// dwX >> 3, value add one every eight
				// If the source bit is 0, the background. If 1, draw the text.
				if ( !((0x80 >> (dwX & 7)) & pbTitle[dwX >> 3]) ) 
				{

					PBYTE pbPixel = pStartPos + dwX * inputPixelSize; //指向視頻圖像在OSD區域的某一點的像素地址
					if (mIsOverlayByCover)
					{
				       //用OSD的前景色替換
						switch(mInputColorSpace)
						{
						case FT_RGB555:
						case FT_RGB565:
							*pbPixel = m_Mask[0];
							*(pbPixel + 1) = m_Mask[1];
							break;
						case FT_RGB24:
							*pbPixel = OSDTextColorB;
							*(pbPixel + 1) = OSDTextColorG;
							*(pbPixel + 2) = OSDTextColorR;
							break;
						case FT_RGB32:
							*pbPixel = OSDTextColorB;
							*(pbPixel + 1) = OSDTextColorG;
							*(pbPixel + 2) = OSDTextColorR;
							*(pbPixel + 3) = 0;
							break;
						}

					}
					else
					{
						for (int i = 0; i < inputPixelSize; i++)
						{
							*pbPixel = ~*pbPixel;
							//	*pbPixel = *pbPixel ^ 0xff;
							pbPixel++;
						}
					}
				}
			}
			pStartPos += strideInBytes;
		}
		return TRUE;
	}
	return FALSE;
}

上述函數中,首先要檢查圖像相關參數的有效性,然後計算得到一個指向圖像幀第一行數據的指針。圖像一般有兩種掃描方式:從下往上(Bottom-up)和從上往下(Top-Down)。從下往上掃描的圖像,圖像數據存儲是,最後一行的數據放在最前面,然後依次倒數第2行的數據。。。最後是計算指向圖像第一行數據的指針。從上往下掃描的圖像是正常的圖像數據存儲格式,第一行就是在圖像緩衝區的首地址。目前,很多RGB圖像都是顛倒存儲的,也就是使用從下往上的掃描方式,我們要注意兩種不同存儲方式計算像素地址時的區別。我們要讀取圖像每個像素的像素值,必須從圖像數據開始地址計算一個偏移量,這個偏移量根據圖像的寬度、高度、以及每個像素的字節數,還有行寬(Pitch)來確定。圖像的掃描方式用BITMAPINFOHEADER數據結構的biHeight成員值的正負來表示:正數表示從下往上,負數表示從上往下。mIsBottmoUpImage變量的取值如下:
   mIsBottomUpImage = inInfoHeader->bmiHeader.biHeight > 0 ? TRUE : FALSE;
接下來,OverlayText函數計算OSD區域的座標位置,然後計算OSD圖像數據起始地址和視頻圖像在OSD區域的數據指針。隨後,是一個兩層循環,依次遍歷OSD區域裏的每個像素,根據前面所講的像素疊加規則確定每個像素值。

疊加Logo的函數與疊加字幕的實現有點類似,下面是疊加Logo函數OverlayBitmap的代碼實現:

BOOL COverlayController:: OverlayBitmap(PBYTE inImage)
{
	if(mLogoImage.IsNull())
		return FALSE;
 
    BYTE * pBmpDibBits = NULL;
	int lPitch = mLogoImage.GetPitch();
	if(lPitch < 0)
	{
		pBmpDibBits = (BYTE *)mLogoImage.GetBits()+(mLogoImage.GetPitch()*(mLogoImage.GetHeight()-1));
	}
	else
	{
		pBmpDibBits = (BYTE *)mLogoImage.GetBits();
	}

	mDIBWidthInBytes = abs(lPitch);

	int nTitlePixelSize = mLogoImage.GetBPP()/8;

	if(mbOverlayByAlpha && mLogoImage.GetBPP() != 32)
		return FALSE;

	ASSERT(mLogoImage.GetBPP() == 24 || mLogoImage.GetBPP() == 32);

	// Now copy the data from the DIB section (which is usually bottom-up)
	// but first check if it's too big
	if (mImageWidth > mStartPos.x && mImageHeight > mStartPos.y && 
		mTitleSize.cx > 0 && mTitleSize.cy > 0) 
	{
		long actualOverlayWidth  = min((mImageWidth - mStartPos.x), mTitleSize.cx);
		long actualOverlayHeight = min((mImageHeight - mStartPos.y), mTitleSize.cy);
		// Image may be bottom-up, may be top-down.
		// Anyway retrieve the pointer which point to the top line
		PBYTE   pTopLine      = NULL;
		long    strideInBytes = 0;
		if (mIsBottomUpImage)
		{
			strideInBytes = -mImageWidthInBytes;
			pTopLine      = inImage + mImageWidthInBytes * (mImageHeight - 1);	
		}
		else
		{
			strideInBytes = mImageWidthInBytes;
			pTopLine      = inImage;
		}

		int inputPixelSize = 0; //輸入視頻的圖像像素大小
		unsigned char       m_Mask[2] = {0};   // Mask color
		BYTE osd_R, osd_G, osd_B;

		const unsigned int bits555[] = {0x7C00, 0x03E0, 0x001F};
		const unsigned int bits565[] = {0xF800, 0x07E0, 0x001F};

		switch (mInputColorSpace)
		{
		case FT_RGB8:
			inputPixelSize = 1;
			break;

		case FT_RGB555:
			{
				inputPixelSize = 2;
			}
			break;

		case FT_RGB565:
			{
				inputPixelSize = 2;
			}
			break;

		case FT_RGB24:
			inputPixelSize = 3;
			break;

		case FT_RGB32:
			inputPixelSize = 4;
			break;
		}


		PBYTE  pStartPos = pTopLine + mStartPos.y * strideInBytes + mStartPos.x * mImageBitCount / 8; //指向視頻圖像OSD區域的左上角像素
		for (DWORD dwY = 0; dwY < (DWORD)actualOverlayHeight; dwY++) 
		{
			PBYTE pbTitle  = pBmpDibBits + mDIBWidthInBytes * ((DWORD)mTitleSize.cy - dwY - 1); //指向OSD的某一行起點像素
			for (DWORD dwX = 0; dwX < (DWORD)actualOverlayWidth; dwX++) 
			{

				if (mbOverlayByAlpha) //根據Alpha通道的透明度計算OSD區域像素值
				{

					PBYTE pbPixel = pStartPos + dwX * inputPixelSize; //指向視頻圖像在OSD區域的某一點的像素地址

					osd_R = *pbTitle;
					osd_G = *(pbTitle + 1);
					osd_B = *(pbTitle + 2);
					float fAlpha = *(pbTitle + 3)/255.f;


					switch(mInputColorSpace)
					{
					case FT_RGB555:
					case FT_RGB565:
						break;
					case FT_RGB24:
						*pbPixel = *pbPixel * (1 - fAlpha) + (osd_R * fAlpha);
						*(pbPixel + 1) = *(pbPixel + 1) * (1 - fAlpha) + (osd_G * fAlpha);
						*(pbPixel + 2) = *(pbPixel + 2) * (1 - fAlpha) + (osd_B * fAlpha);
						break;
					case FT_RGB32:
						*pbPixel = *pbPixel * (1 - fAlpha) + (osd_R * fAlpha);
						*(pbPixel + 1) = *(pbPixel + 1) * (1 - fAlpha) + (osd_G * fAlpha);
						*(pbPixel + 2) = *(pbPixel + 2) * (1 - fAlpha) + (osd_B * fAlpha);
						*(pbPixel + 3) = 0;
						break;
					}
				}
				else //根據位圖的遮罩色進行混合
				{
					PBYTE pbPixel = pStartPos + dwX * inputPixelSize; //指向視頻圖像在OSD區域的某一點的像素地址

					osd_R = *pbTitle;
					osd_G = *(pbTitle + 1);
					osd_B = *(pbTitle + 2);

					if(RGB(osd_R, osd_G, osd_B) != mColorKey) //不等於OSD背景遮罩色
					{
						//用OSD的前景色替換
						switch(mInputColorSpace)
						{
						case FT_RGB555:
							{
								// Caculate the mask bits
								unsigned int  wMask, wTemp;
								wTemp  = unsigned int (osd_B / 256. * 32);
								wMask  = wTemp & bits555[2];
								wTemp  = unsigned int (osd_G / 256. * 32);
								wTemp  = wTemp << 5;
								wMask += wTemp & bits555[1];
								wTemp  = unsigned int (osd_R / 256. * 32);
								wTemp  = wTemp << 10;
								wMask += wTemp & bits555[0];
								// Store the high byte and low byte seperately
								m_Mask[0] = wMask & 0xff;
								wMask     = wMask >> 8;
								m_Mask[1] = wMask & 0xff;

								*pbPixel = m_Mask[0];
								*(pbPixel + 1) = m_Mask[1];
							}
							break;
						case FT_RGB565:
							{
								// Caculate the mask bits
								unsigned int  wMask, wTemp;
								wTemp  = unsigned int (osd_B / 256. * 32);
								wMask  = wTemp & bits565[2];
								wTemp  = unsigned int (osd_G / 256. * 64);
								wTemp  = wTemp << 5;
								wMask += wTemp & bits565[1];
								wTemp  = unsigned int (osd_R / 256. * 32);
								wTemp  = wTemp << 11;
								wMask += wTemp & bits565[0];
								// Store the high byte and low byte seperately
								m_Mask[0] = wMask & 0xff;
								wMask     = wMask >> 8;
								m_Mask[1] = wMask & 0xff;

								*pbPixel = m_Mask[0];
								*(pbPixel + 1) = m_Mask[1];
							}
							break;
						case FT_RGB24:
							*pbPixel = osd_R;
							*(pbPixel + 1) = osd_G;
							*(pbPixel + 2) = osd_B;
							break;
						case FT_RGB32:
							*pbPixel = osd_R;
							*(pbPixel + 1) = osd_G;
							*(pbPixel + 2) = osd_B;
							*(pbPixel + 3) = 0;
							break;
						}
					}

				}

				pbTitle += nTitlePixelSize;
			}
			pStartPos += strideInBytes;
		}
		return TRUE;
	}
	return FALSE;

}

OverlayBitmap函數支持兩種背景透明方式:一種是基於背景遮罩色,另一種是基於Alpha通道。是否採用Alpha通道通過變量mbOverlayByAlpha的值來判斷,如果值爲True表示啓用Alpha通道,則處理時像素的Alpha通道的值爲0都認爲是透明的。如果值爲False,則根據遮罩色,OSD位圖跟遮罩色相圖的部分被認爲是背景,這部分就透明掉,只保留位圖前景部分的像素。

下面說一下如何使用這個疊加字幕類。

如果要播放一個文件,我們要調用FFmpeg的讀取文件的類來打開一個文件,並且初始化字幕疊加器,下面是例子代碼:

	int nRet = m_FileStreamTask.OpenMediaFile(m_szFilePath);
	if(nRet != 0)
	{
		MessageBox(_T("打開文件失敗"), _T("提示"), MB_OK|MB_ICONERROR);
		return -1;
	}
	
	long cx, cy;
	cx = cy = 0;
	m_FileStreamTask.GetVideoSize(cx, cy); //獲取視頻的分辨率
	if(cx > 0 && cy > 0)
	{
		BOOL bReverShow = TRUE;//是否翻轉圖像
		m_Painter.SetVideoWindow(m_wndView.GetSafeHwnd()); //設置視頻預覽窗口
		m_Painter.SetSrcFormat(cx, cy, 24, bReverShow); //第4個參數表示是否翻轉圖像
		m_Painter.SetStretch(TRUE); //是否縮放顯示圖像
		m_Painter.Open(); //打開渲染器

		//初始化字幕疊加器
		VIDEOINFOHEADER videoHeader;
		memset(&videoHeader, 0, sizeof(VIDEOINFOHEADER));
		videoHeader.bmiHeader.biBitCount = 24;
		videoHeader.bmiHeader.biWidth = cx;
		videoHeader.bmiHeader.biHeight = bReverShow ? -cy : cy; //注意有些RGB圖像是顛倒存儲的(即Bottom-up),掃描圖像時應該從最下面一行開始,這時候biHeight屬性應該爲負數
		videoHeader.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
		videoHeader.bmiHeader.biSizeImage = cx * cy * 24/8;
		videoHeader.rcSource.left = videoHeader.rcSource.top = 0;
		videoHeader.rcSource.right = cx;
		videoHeader.rcSource.bottom = cy;
		videoHeader.AvgTimePerFrame = 10000000L/25;

		/////////////////////////////////////////////////////////////////////////
		m_TitleOverlayFilter.SetInputVideoInfoToController(FT_RGB24, &videoHeader); //Note: 設置輸入的視頻是RGB24格式
		//////////////////////////////////////////////////////////////////////////
		m_TitleOverlayFilter.StartStreaming();
	}

	m_FileStreamTask.SetVideoCaptureCB(VideoCaptureCallback);

	m_FileStreamTask.StartReadFile();

調用完m_FileStreamTask.StartReadFile() 就啓動視頻播放了,視頻圖像顯示到指定的預覽窗口中。

接着,如果我們要疊加一段靜態文字,可以如下調用:

    //char szOSDText[64] = "Hello World, This is a video mixed OSD example";
	char szOSDText[64] = "中央電視臺國際頻道";

	const int nIndex = 0;

	LOGFONT    logFont;

	logFont.lfEscapement  = 0;
	logFont.lfOrientation = 0;
	logFont.lfWeight      = FW_NORMAL;
	logFont.lfItalic      = 0;
	logFont.lfUnderline   = 0;
	logFont.lfStrikeOut   = 0;
	logFont.lfCharSet        = ANSI_CHARSET;
	logFont.lfOutPrecision   = OUT_DEFAULT_PRECIS;
	logFont.lfClipPrecision  = CLIP_DEFAULT_PRECIS;
	logFont.lfQuality        = PROOF_QUALITY;
	logFont.lfPitchAndFamily = VARIABLE_PITCH | FF_ROMAN;
	strcpy(logFont.lfFaceName, "Arial");

	m_TitleOverlayFilter.LockOSD(nIndex);
	m_TitleOverlayFilter.put_TitleOverlayType(nIndex, OT_STATIC); //靜態文字
	m_TitleOverlayFilter.put_TitleOverlayStyle(nIndex, TRUE);
	m_TitleOverlayFilter.put_TitleStartPosition(nIndex,  CPoint(10, 20)); //設置OSD顯示座標
	m_TitleOverlayFilter.put_TitleColor(nIndex, 0, 0xff, 0); //設置文字顏色
	m_TitleOverlayFilter.put_Title(nIndex, szOSDText, strlen(szOSDText)); //設置文字內容
	m_TitleOverlayFilter.put_TitleFont(nIndex, logFont);
	m_TitleOverlayFilter.put_TitleEnable(nIndex, TRUE);
	m_TitleOverlayFilter.UnlockOSD(nIndex);

如果要疊加OSD時間,代碼如下:

	const int nIndex = 1;
	m_TitleOverlayFilter.LockOSD(nIndex);
	m_TitleOverlayFilter.put_TitleOverlayType(nIndex, OT_SYSTIME); //系統時間
	m_TitleOverlayFilter.put_TitleOverlayStyle(nIndex, TRUE);
	m_TitleOverlayFilter.put_TitleStartPosition(nIndex,  CPoint(50, 100)); //設置OSD顯示座標
	m_TitleOverlayFilter.put_TitleColor(nIndex, 0xff, 0, 0); //設置文字顏色
	m_TitleOverlayFilter.put_TitleEnable(nIndex, TRUE);
	m_TitleOverlayFilter.UnlockOSD(nIndex);

如果要顯示Log位圖,調用代碼:

	const int nIndex = 2;
	m_TitleOverlayFilter.LockOSD(nIndex);
	m_TitleOverlayFilter.put_TitleOverlayType(nIndex, OT_LOGO); //疊加Logo
#if 0
	m_TitleOverlayFilter.put_TitleBitmap(nIndex, GetAppDir() + _T("\\logo\\深圳地鐵logo.PNG"), TRUE, RGB(51, 51, 51));
#else
	m_TitleOverlayFilter.put_TitleBitmap(nIndex, GetAppDir() + _T("\\logo\\LOGO2.BMP"), FALSE, RGB(255, 255, 255));
#endif
	m_TitleOverlayFilter.put_TitleEnable(nIndex, TRUE);
	m_TitleOverlayFilter.UnlockOSD(nIndex);

關閉OSD就調用如下代碼:

	m_TitleOverlayFilter.put_TitleEnable(0, FALSE);
	m_TitleOverlayFilter.put_TitleEnable(1, FALSE);
	m_TitleOverlayFilter.put_TitleEnable(2, FALSE);

其他有什麼問題的話,請大家在博客上留言。

資源下載地址: https://download.csdn.net/download/zhoubotong2012/11855612

 

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