65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (20投票s)

2011年2月22日

CPOL

6分钟阅读

viewsIcon

137614

downloadIcon

4593

一个文本叠加过滤器和一个使用转换过滤器的 JPEG/JPEG2000 编码器。

引言

转换过滤器可能是 DirectShow 组合中最有趣的部件。它们封装了复杂的图像和视频处理算法。从过滤器的开发角度来看,它们的实现难度并不比其他过滤器难;然而,它们确实需要一些额外的编码和方法重写。与渲染过滤器和源过滤器一样,转换过滤器也有基类,在实现自定义工作时,您应该继承这些基类。

转换过滤器至少有两个引脚,一个输入引脚和一个输出引脚。转换过滤器分为两类——复制-转换过滤器和就地转换过滤器。顾名思义,复制-转换过滤器从输入引脚获取数据,对其进行转换,然后将结果写入输出引脚,而就地过滤器则在输入样本上执行其工作,然后将其传递给输出过滤器。

DirectShow 提供了三个基类来编写转换过滤器

  1. CTransformFilter - 复制-转换过滤器的基类
  2. CTransInPlaceFilter - 就地转换的基类
  3. 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);
}

参考文献

  1. 用于数字视频和电视的 DirectShow 编程
  2. 编写转换过滤器
  3. OpenJpeg 库
  4. Tony Lin JPEG 编解码器

历史

  • 23.2.2011
    • 初始版本
  • 13.3.2011
    • 已更改源代码以使用智能指针
    • 修复了 SetQuality 实现
© . All rights reserved.