DirectShow 过滤器开发第三部分:Transform Filters






4.83/5 (20投票s)
一个文本叠加过滤器和一个使用转换过滤器的 JPEG/JPEG2000 编码器。
引言
转换过滤器可能是 DirectShow 组合中最有趣的部件。它们封装了复杂的图像和视频处理算法。从过滤器的开发角度来看,它们的实现难度并不比其他过滤器难;然而,它们确实需要一些额外的编码和方法重写。与渲染过滤器和源过滤器一样,转换过滤器也有基类,在实现自定义工作时,您应该继承这些基类。
转换过滤器至少有两个引脚,一个输入引脚和一个输出引脚。转换过滤器分为两类——复制-转换过滤器和就地转换过滤器。顾名思义,复制-转换过滤器从输入引脚获取数据,对其进行转换,然后将结果写入输出引脚,而就地过滤器则在输入样本上执行其工作,然后将其传递给输出过滤器。
DirectShow 提供了三个基类来编写转换过滤器
CTransformFilter
- 复制-转换过滤器的基类CTransInPlaceFilter
- 就地转换的基类CVideoTransfromFilter
- 专为视频解码设计,内置质量控制管理,可在溢出时丢弃帧
我将在本文中介绍前两个类:CTransInPlace
的子类将用于文本叠加过滤器,而 CTransformFilter
将用于 JPEG/JPEG2000 编码器。
在我们继续之前,您应该查看本系列文章的第一部分,因为过滤器开发的前提条件、过滤器注册和过滤器调试都相同。
文本叠加过滤器
文本叠加过滤器会将用户定义的文本添加到通过过滤器的每一帧。它可以用于显示字幕或徽标。向视频帧添加文本不会改变其媒体子类型或格式,因此就地转换非常适合。我将使用 GDI+ 进行叠加,因为它提供了创建就地位图和在位图上绘制字符的便捷 API。
using namespace Gdiplus;
using namespace std;
class CTextOverlay : public CTransInPlaceFilter, public ITextAdditor
{
public:
DECLARE_IUNKNOWN;
CTextOverlay(LPUNKNOWN pUnk, HRESULT *phr);
virtual ~CTextOverlay(void);
virtual HRESULT CheckInputType(const CMediaType* mtIn);
virtual HRESULT SetMediaType(PIN_DIRECTION direction, const CMediaType *pmt);
virtual HRESULT Transform(IMediaSample *pSample);
static CUnknown *WINAPI CreateInstance(LPUNKNOWN pUnk, HRESULT *phr);
STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void ** ppv);
STDMETHODIMP AddTextOverlay(WCHAR* text, DWORD id, RECT position,
COLORREF color = RGB(255, 255, 255), float fontSize = 20);
STDMETHODIMP Clear(void);
STDMETHODIMP Remove(DWORD id);
private:
ULONG_PTR m_gdiplusToken;
VIDEOINFOHEADER m_videoInfo;
PixelFormat m_pixFmt;
int m_stride;
map<DWORD, Overlay*> m_overlays;
};
唯一纯虚方法是 Transform
方法,必须在您的类中实现。此外,我还重写了 CheckInputType
,该方法在引脚连接协商期间针对每种媒体类型进行调用。由于转换过滤器至少有两个引脚,SetMediaType
具有方向参数,该参数指示连接是在输入引脚还是输出引脚上执行。您可能需要保存输入和输出视频标头。在这种情况下,我只需要输入视频标头,因为它的输出与输出完全相同。
HRESULT CTextOverlay::SetMediaType(PIN_DIRECTION direction, const CMediaType *pmt)
{
if(direction == PINDIR_INPUT)
{
VIDEOINFOHEADER* pvih = (VIDEOINFOHEADER*)pmt->pbFormat;
m_videoInfo = *pvih;
HRESULT hr = GetPixleFormat(m_videoInfo.bmiHeader.biBitCount, &m_pixFmt);
if(FAILED(hr))
{
return hr;
}
BITMAPINFOHEADER bih = m_videoInfo.bmiHeader;
m_stride = bih.biBitCount / 8 * bih.biWidth;
}
return S_OK;
}
该过滤器仅接受每像素 15、16、24 和 32 位的 RGB 格式,并使用 GDI+ Bitmap
类,无需任何缓冲区复制即可创建就地位图对象。之后,我从该位图中创建一个图形对象,并调用 Graphics::DrawString
方法在位图上绘制用户定义的文本。
HRESULT CTextOverlay::Transform(IMediaSample *pSample)
{
CAutoLock lock(m_pLock);
BYTE* pBuffer = NULL;
Status s = Ok;
map<DWORD, Overlay*>::iterator it;
HRESULT hr = pSample->GetPointer(&pBuffer);
if(FAILED(hr))
{
return hr;
}
BITMAPINFOHEADER bih = m_videoInfo.bmiHeader;
Bitmap bmp(bih.biWidth, bih.biHeight, m_stride, m_pixFmt, pBuffer);
Graphics g(&bmp);
for ( it = m_overlays.begin() ; it != m_overlays.end(); it++ )
{
Overlay* over = (*it).second;
SolidBrush brush(over->color);
Font font(FontFamily::GenericSerif(), over->fontSize);
s = g.DrawString(over->text, -1, &font, over->pos,
StringFormat::GenericDefault(), &brush);
if(s != Ok)
{
TCHAR msg[100];
wsprintf(L"Failed to draw text : %s", over->text);
::OutputDebugString(msg);
}
}
return S_OK;
}
使用 ITextAditor
接口,您可以按 ID 添加文本叠加,按 ID 移除它们,或移除所有。每个叠加层包含文本、边界矩形、颜色和字体大小。
DECLARE_INTERFACE_(ITextAdditor, IUnknown)
{
STDMETHOD(AddTextOverlay)(WCHAR* text, DWORD id, RECT position,
COLORREF color, float fontSize) PURE;
STDMETHOD(Clear)(void) PURE;
STDMETHOD(Remove)(DWORD id) PURE;
};
叠加对象以线程安全的方式存储在映射中,因此您可以在播放期间自由添加和移除叠加。DirectShow 框架中的线程安全性通过临界区和 CAutoLock
类来实现,后者通常在方法开始时声明,并在方法结束时退出作用域时释放临界区。
JPEG / JPEG2000 编码器
我花了一段时间来决定实现哪种类型的视频编码,最终我决定做一个简单的帧内编码器——每一帧视频在编码时都不参考前一帧或后一帧。这种编码比 MPEG4 或 H264 等帧间编码标准更容易实现,但由于相邻帧之间存在大量冗余像素信息,因此会造成较大的流吞吐量。我还为其他帧内编码器类型创建了一个基类,您可以通过继承 CBaseCompressor
并更新创建具体实现的工厂方法来轻松地交换实现。
struct CBaseCompressor
{
virtual HRESULT Init(BITMAPINFOHEADER* pBih) PURE;
virtual HRESULT Compress(BYTE* pInput, DWORD inputSize, BYTE* pOutput,
DWORD* outputSize) PURE;
virtual HRESULT SetQuality(BYTE quality) PURE;
virtual HRESULT GetMediaSubTypeAndCompression(GUID* mediaSubType,
DWORD* compression) PURE;
};
默认情况下,编码标准是 JPEG,它基于我在 CodeProject 上这里找到的代码。使用 IJ2KEncoder::SetEncoderType
方法,您可以将实现更改为基于 OpenJpeg 库的 JPEG2000 编码标准。请注意,如果过滤器的某个引脚已连接,则无法更改编码器实现,因此最好在过滤器创建后立即设置所需的编码算法。
JPEG2000 和媒体子类型
使用 JPEG 压缩器时,DirectShow 提供了一个名为 MEDIASUBTYPE_MJPG
的内置媒体子类型,它声明在 uuids.h 文件中。关于 JPEG2000,我找不到任何合适的 GUID,所以使用以下宏定义创建了一个。
DEFINE_GUID( MEDIASUBTYPE_MJ2C, MAKEFOURCC('M', 'J', '2', 'C'),
0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa,
0x00, 0x38, 0x9b, 0x71);
使用 BITMAPINFOHEADER
结构进行压缩图像时,您必须将 biCompression
字段设置为 MAKEFOURCC('M', 'J', '2', 'C'
)。这样,过滤器就可以连接到 JPEG2000 解码器,例如这个。
MJ2C 指的是 JPEG2000 代码流,实际上是运动 JPEG2000 的定义,其中每一帧都包含压缩图像数据。另一个标准是 J2K,它通常用于静止图像编码,也包含标头。
尽管 JPEG2000 在较低比特率下提供了更好的压缩比和更好的图像质量,但它比 JPEG 更消耗 CPU,因此不太适合大分辨率视频。在我对 JPEG2000 实现进行研究期间,我发现了一个名为 CUJ2K 的项目——一个基于 CUDA 的 JPEG2000 实现,CUDA 是由 NVIDIA 开发的基于 GPU 的 API。由于该库是为硬盘上的静止图像设计的,它使用命令行传递图像的源路径和目标路径。为了将其用于内存缓冲区,需要进行一些额外的工作,所以我决定使用 OpenJpeg;但是,如果您需要更好的性能,它值得一看。
过滤器实现
要实现转换过滤器,您必须实现六个方法
Transform
- 接收输入和输出媒体样本。CheckInputType
- 检查输入引脚是否可以连接到上游过滤器。CheckTransform
- 检查输入和输出媒体类型之间是否可以进行转换。DecideBufferSixe
- 设置输出媒体样本的内存缓冲区大小。GetMediaType
- 返回用于将输出引脚与下游过滤器连接的媒体类型。SetMediaType
- 在输入和输出引脚成功连接时调用。
class CJ2kCompressor : public CTransformFilter, public IJ2KEncoder
{
public:
DECLARE_IUNKNOWN;
CJ2kCompressor(LPUNKNOWN pUnk, HRESULT *phr);
virtual ~CJ2kCompressor(void);
// CTransfromFilter overrides
virtual HRESULT Transform(IMediaSample * pIn, IMediaSample *pOut);
virtual HRESULT CheckInputType(const CMediaType* mtIn);
virtual HRESULT CheckTransform(const CMediaType* mtIn,
const CMediaType* mtOut);
virtual HRESULT DecideBufferSize(IMemAllocator * pAlloc,
ALLOCATOR_PROPERTIES *pProp);
virtual HRESULT GetMediaType(int iPosition, CMediaType *pMediaType);
virtual HRESULT SetMediaType(PIN_DIRECTION direction, const CMediaType *pmt);
static CUnknown * WINAPI CreateInstance(LPUNKNOWN pUnk, HRESULT *pHr);
STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void ** ppv);
// IJ2KEncoder
STDMETHODIMP SetQuality(BYTE quality);
STDMETHODIMP SetEncoderType(EncoderType encoderType);
private:
VIDEOINFOHEADER m_VihIn;
VIDEOINFOHEADER m_VihOut;
CBaseCompressor* m_encoder;
};
Transform
方法的实现非常直接:我从输入和输出媒体样本中获取缓冲区指针,然后将它们传递给 CBaseCompressor
实现。之后,我设置实际的输出媒体样本大小,并将同步点设置为 true
,因为每一帧都是参考帧。
HRESULT CJ2kCompressor::Transform(IMediaSample* pIn, IMediaSample* pOut)
{
HRESULT hr = S_OK;
BYTE *pBufIn, *pBufOut;
long sizeIn;
DWORD sizeOut;
hr = pIn->GetPointer(&pBufIn);
if(FAILED(hr))
{
return hr;
}
sizeIn = pIn->GetActualDataLength();
hr = pOut->GetPointer(&pBufOut);
if(FAILED(hr))
{
return hr;
}
hr = m_encoder->Compress(pBufIn, sizeIn, pBufOut, &sizeOut);
if(FAILED(hr))
{
return hr;
}
hr = pOut->SetActualDataLength(sizeOut);
if(FAILED(hr))
{
return hr;
}
hr = pOut->SetSyncPoint(TRUE);
return hr;
}
过滤器注册
由于此过滤器是视频编码器,因此应将其注册到视频压缩器过滤器类别中,这可以使用 IFilterMapper
对象完成。
STDAPI RegisterFilters( BOOL bRegister )
{
HRESULT hr = NOERROR;
WCHAR achFileName[MAX_PATH];
char achTemp[MAX_PATH];
ASSERT(g_hInst != 0);
if( 0 == GetModuleFileNameA(g_hInst, achTemp, sizeof(achTemp)))
{
return AmHresultFromWin32(GetLastError());
}
MultiByteToWideChar(CP_ACP, 0L, achTemp, lstrlenA(achTemp) + 1,
achFileName, NUMELMS(achFileName));
hr = CoInitialize(0);
if(bRegister)
{
hr = AMovieSetupRegisterServer(CLSID_Jpeg2000Encoder,
J2K_FILTER_NAME, achFileName, L"Both", L"InprocServer32");
}
if( SUCCEEDED(hr) )
{
IFilterMapper2 *fm = 0;
hr = CoCreateInstance( CLSID_FilterMapper2, NULL,
CLSCTX_INPROC_SERVER, IID_IFilterMapper2, (void **)&fm);
if( SUCCEEDED(hr) )
{
if(bRegister)
{
IMoniker *pMoniker = 0;
REGFILTER2 rf2;
rf2.dwVersion = 1;
rf2.dwMerit = MERIT_DO_NOT_USE;
rf2.cPins = 2;
rf2.rgPins = psudPins;
hr = fm->RegisterFilter(CLSID_Jpeg2000Encoder, J2K_FILTER_NAME,
&pMoniker, &CLSID_VideoCompressorCategory, NULL, &rf2);
}
else
{
hr = fm->UnregisterFilter(&CLSID_VideoCompressorCategory, 0,
CLSID_Jpeg2000Encoder);
}
}
if(fm)
fm->Release();
}
if( SUCCEEDED(hr) && !bRegister )
{
hr = AMovieSetupUnregisterServer( CLSID_Jpeg2000Encoder );
}
CoFreeUnusedLibraries();
CoUninitialize();
return hr;
}
STDAPI DllRegisterServer()
{
return RegisterFilters(TRUE);
}
STDAPI DllUnregisterServer()
{
return RegisterFilters(FALSE);
}
参考文献
历史
- 23.2.2011
- 初始版本
- 13.3.2011
- 已更改源代码以使用智能指针
- 修复了
SetQuality
实现