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