如何在 Windows 8 现代应用程序上从视频流中抓取帧?

Posted

技术标签:

【中文标题】如何在 Windows 8 现代应用程序上从视频流中抓取帧?【英文标题】:How do I grab frames from a video stream on Windows 8 modern apps? 【发布时间】:2013-03-29 20:39:04 【问题描述】:

我正在尝试从 mp4 视频流中提取图像。查找资料后,似乎正确的做法是在 C++ 中使用媒体基础并打开框架/从中读取内容。

文档和示例很少,但经过一番挖掘,似乎有些人通过将帧读入纹理并将该纹理的内容复制到内存可读纹理(我我什至不确定我是否在这里使用了正确的术语)。尝试我发现的东西给了我错误,我可能做错了很多事情。

这是我尝试执行此操作的一小段代码(项目本身附在底部)。

    ComPtr<ID3D11Texture2D> spTextureDst;
    MEDIA::ThrowIfFailed(
        m_spDX11SwapChain->GetBuffer(0, IID_PPV_ARGS(&spTextureDst))
        );

    auto rcNormalized = MFVideoNormalizedRect();
    rcNormalized.left = 0;
    rcNormalized.right = 1;
    rcNormalized.top = 0;
    rcNormalized.bottom = 1;
    MEDIA::ThrowIfFailed(
        m_spMediaEngine->TransferVideoFrame(m_spRenderTexture.Get(), &rcNormalized, &m_rcTarget, &m_bkgColor)
        );

    //copy the render target texture to the readable texture.
    m_spDX11DeviceContext->CopySubresourceRegion(m_spCopyTexture.Get(),0,0,0,0,m_spRenderTexture.Get(),0,NULL);
    m_spDX11DeviceContext->Flush();

    //Map the readable texture;                 
    D3D11_MAPPED_SUBRESOURCE mapped = 0;
    m_spDX11DeviceContext->Map(m_spCopyTexture.Get(),0,D3D11_MAP_READ,0,&mapped);
    void* buffer = ::CoTaskMemAlloc(600 * 400 * 3);
    memcpy(buffer, mapped.pData,600 * 400 * 3);
    //unmap so we can copy during next update.
    m_spDX11DeviceContext->Unmap(m_spCopyTexture.Get(),0);


    // and the present it to the screen
    MEDIA::ThrowIfFailed(
        m_spDX11SwapChain->Present(1, 0)
        );            

我得到的错误是:

App1.exe 中 0x76814B32 处的第一次机会异常:Microsoft C++ 异常:Platform::InvalidArgumentException ^ 位于内存位置 0x07AFF60C。 HRESULT:0x80070057

我不确定如何进一步研究它,因为就像我说的那样,关于它的文档很少。

Here's the modified sample 我正在工作。此问题专门针对 WinRT(Windows 8 应用程序)。

【问题讨论】:

您能否与我分享您的代码,因为我正在为 WP 8.1 开发应用程序并且我不熟悉 C++ 编程?对你有很大的帮助。 嗨,我正在开发一个 winrt 应用程序,我还必须从视频中提取帧。我使用了你的代码,但它只提取前 2 帧。有什么具体要改的。我更新了你在这个问题的编辑部分中提到的代码。 你能分享工作代码吗? 【参考方案1】:

更新成功!!见底部编辑


一些部分成功,但也许足以回答您的问题。请继续阅读。

在我的系统上,调试异常显示OnTimer() 函数在尝试调用TransferVideoFrame() 时失败。它给出的错误是InvalidArgumentException

所以,在谷歌上搜索了一下,我第一次发现 - 显然有 a bug in NVIDIA drivers - 这意味着视频播放似乎在 11 和 10 功能级别时失败。

所以我的第一个更改是函数CreateDX11Device() 如下:

static const D3D_FEATURE_LEVEL levels[] = 
/*
        D3D_FEATURE_LEVEL_11_1,
        D3D_FEATURE_LEVEL_11_0,  
        D3D_FEATURE_LEVEL_10_1,
        D3D_FEATURE_LEVEL_10_0,
*/
        D3D_FEATURE_LEVEL_9_3,
        D3D_FEATURE_LEVEL_9_2,
        D3D_FEATURE_LEVEL_9_1
    ;

现在TransferVideoFrame() 仍然失败,但给出E_FAIL(作为 HRESULT)而不是无效参数。

更多谷歌搜索导致我的second discovery -

这是一个使用TransferVideoFrame() 而不使用CreateTexture2D() 来预先创建纹理的示例。我看到您在 OnTimer() 中已经有一些与此类似但未使用的代码,所以我猜您找到了相同的链接。

不管怎样,我现在用这段代码来获取视频帧:

ComPtr <ID3D11Texture2D> spTextureDst;
m_spDX11SwapChain->GetBuffer (0, IID_PPV_ARGS (&spTextureDst));
m_spMediaEngine->TransferVideoFrame (spTextureDst.Get (), nullptr, &m_rcTarget, &m_bkgColor);

执行此操作后,我看到 TransferVideoFrame() 成功(好!)但在您复制的纹理 - m_spCopyTexture 上调用 Map() - 失败,因为该纹理不是使用 CPU 读取访问权限创建的。

所以,我只是使用您的读/写 m_spRenderTexture 作为副本的目标,因为它具有正确的标志,并且由于之前的更改,我不再使用它。

        //copy the render target texture to the readable texture.
        m_spDX11DeviceContext->CopySubresourceRegion(m_spRenderTexture.Get(),0,0,0,0,spTextureDst.Get(),0,NULL);
        m_spDX11DeviceContext->Flush();

        //Map the readable texture;                 
        D3D11_MAPPED_SUBRESOURCE mapped = 0;
        HRESULT hr = m_spDX11DeviceContext->Map(m_spRenderTexture.Get(),0,D3D11_MAP_READ,0,&mapped);
        void* buffer = ::CoTaskMemAlloc(176 * 144 * 3);
        memcpy(buffer, mapped.pData,176 * 144 * 3);
        //unmap so we can copy during next update.
        m_spDX11DeviceContext->Unmap(m_spRenderTexture.Get(),0);

现在,在我的系统上,OnTimer() 函数不会失败。视频帧渲染到纹理,像素数据成功复制到内存缓冲区。

在查看是否有进一步的问题之前,也许这是一个很好的时机,看看你是否能取得和我目前一样的进步。如果您对此答案发表评论并提供更多信息,我将编辑答案以尽可能添加更多帮助。

编辑

FramePlayer::CreateBackBuffers() 中对纹理描述的更改

        //make first texture cpu readable
        D3D11_TEXTURE2D_DESC texDesc = 0;
        texDesc.Width = 176;
        texDesc.Height = 144;
        texDesc.MipLevels = 1;
        texDesc.ArraySize = 1;
        texDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
        texDesc.SampleDesc.Count = 1;
        texDesc.SampleDesc.Quality =  0;
        texDesc.Usage = D3D11_USAGE_STAGING;
        texDesc.BindFlags = 0;
        texDesc.CPUAccessFlags =  D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE;
        texDesc.MiscFlags = 0;

        MEDIA::ThrowIfFailed(m_spDX11Device->CreateTexture2D(&texDesc,NULL,&m_spRenderTexture));

另请注意,有时需要清除内存泄漏(我相信您知道) - 在下一行中分配的内存永远不会被释放:

void* buffer = ::CoTaskMemAlloc(176 * 144 * 3); // sizes changed for my test

成功

我现在已经成功保存了一个单独的帧,但现在没有使用复制纹理。

首先,我下载了最新版本的the DirectXTex Library,它提供了DX11纹理辅助功能,例如从纹理中提取图像并保存到文件。 The instructions 将 DirectXTex 库添加到您的解决方案中作为现有项目需要仔细遵循,并注意 Windows 8 应用商店应用所需的更改。

一旦包含、引用并构建了上述库,将以下#include添加到FramePlayer.cpp

#include "..\DirectXTex\DirectXTex.h"  // nb - use the relative path you copied to
#include <wincodec.h>

最后,FramePlayer::OnTimer() 中的中心代码部分需要类似于以下内容。你会看到我每次都保存到相同的文件名,所以这需要修改以添加例如名称的帧号

// new frame available at the media engine so get it 
ComPtr<ID3D11Texture2D> spTextureDst;
MEDIA::ThrowIfFailed(m_spDX11SwapChain->GetBuffer(0, IID_PPV_ARGS(&spTextureDst)));

auto rcNormalized = MFVideoNormalizedRect();
rcNormalized.left = 0;
rcNormalized.right = 1;
rcNormalized.top = 0;
rcNormalized.bottom = 1;
MEDIA::ThrowIfFailed(m_spMediaEngine->TransferVideoFrame(spTextureDst.Get(), &rcNormalized, &m_rcTarget, &m_bkgColor));

// capture an image from the DX11 texture
DirectX::ScratchImage pImage;
HRESULT hr = DirectX::CaptureTexture(m_spDX11Device.Get(), m_spDX11DeviceContext.Get(), spTextureDst.Get(), pImage);
if (SUCCEEDED(hr))

    // get the image object from the wrapper
    const DirectX::Image *pRealImage = pImage.GetImage(0, 0, 0);

    // set some place to save the image frame
    StorageFolder ^dataFolder = ApplicationData::Current->LocalFolder;
    Platform::String ^szPath = dataFolder->Path + "\\frame.png";

    // save the image to file
    hr = DirectX::SaveToWICFile(*pRealImage, DirectX::WIC_FLAGS_NONE, GUID_ContainerFormatPng, szPath->Data());


// and the present it to the screen
MEDIA::ThrowIfFailed(m_spDX11SwapChain->Present(1, 0));            

我现在没有时间再进一步,但我对我目前所取得的成就感到非常满意 :-))

您能否重新审视并更新您在 cmets 中的结果?

【讨论】:

好吧,祝你好运!请记住,我的测试 mp4 是 176*144,因此上面的硬编码尺寸取代了您的 600*400。这些尺寸确实应该来自实际的帧尺寸。 对纹理描述的一个更改 - texDesc.MipLevels = 1; 抱歉,另一处更改 texDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE; 但不确定是否需要写入权限。 请查看编辑 - 我现在可以成功保存单个帧 :-) 这里相同-但不需要执行额外的步骤-只需使用您对我的代码的原始更改就可以了。现在将其转换为库。与 C++ 相比,编写 C# 代码多么容易令人惊讶。我太生锈了。 (赏金到期后我会调整赏金,因为我没有得到我想要的。但这至少值得我辛苦赚来的 200 互联网积分......它会在半天后到期 - 我会调整) .我希望这是可以接受的。【参考方案2】:

查看Video Thumbnail Sample 和Source Reader 文档。

您可以在SDK Root\Samples\multimedia\mediafoundation\VideoThumbnail下找到示例代码

【讨论】:

这不是针对 WinRT。【参考方案3】:

我认为 OpenCV 可能会对您有所帮助。 OpenCV 提供 api 来从相机或视频文件中捕获帧。 你可以在这里下载它http://opencv.org/downloads.html。 下面是我用“OpenCV 2.3.1”写的一个demo。

#include "opencv.hpp"

using namespace cv;
int main()

    VideoCapture cap("demo.avi");   // open a video to capture
    if (!cap.isOpened())        // check if succeeded
        return -1;

    Mat frame;
    namedWindow("Demo", CV_WINDOW_NORMAL);
    // Loop to capture frame and show on the window
    while (1) 
        cap >> frame;
        if (frame.empty())
            break;

        imshow("Demo", frame);
        if (waitKey(33) >= 0)  // pause 33ms every frame
            break;
    

    return 0;

【讨论】:

我正在寻找 WinRT 解决方案。 OpenCV 不支持。至少现在还没有。

以上是关于如何在 Windows 8 现代应用程序上从视频流中抓取帧?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Windows Phone 8.1 上从数据库中获取图像并将其显示为字节数组

在Windows Phone 8.1模拟器上从Store安装应用程序时出现错误80070002

PyQt5 5.8.2 在 Windows 10 上从源代码构建

为啥我在 64 位 Windows 8 上从 GetModuleFileNameEx 得到废话?

如何在 java 上从 youtube 下载视频? [关闭]

如何在 Vista 和 Windows 7 上从我的应用程序启动屏幕键盘