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

从视频文件中提取帧

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (34投票s)

2006年2月27日

CPOL

3分钟阅读

viewsIcon

562371

downloadIcon

40449

用于使用 IMediaDet 从大多数视频文件格式中提取帧的类。

Sample Image - extractvideoframes.jpg

引言

此类包含使用 IMediaDet 接口 的方法,该接口可在 Microsoft DirectShow 中找到。 Media Detector 对象,除其他外,可用于从包括 *.avi*.wmv 和一些 *.mpeg 文件在内的几种文件格式中提取静止图片。

此类公开了 GetFrameFromVideoGetVideoSizeSaveFrameFromVideo 方法,这些方法可以从任何 .NET 应用程序中使用。该类还负责将从函数返回的 HRESULT 转换为有意义的 .NET 异常。

Using the Code

只需在您的项目中添加对 JockerSoft.Media.dll 的引用(或包含源代码)。 还要记住分发 Interop.DexterLib.dll
所有方法都是 static,所以要使用它们,只需执行类似这样的操作

try
{
    this.pictureBox1.Image = FrameGrabber.GetFrameFromVideo(strVideoFile, 0.2d);
}
catch (InvalidVideoFileException ex)
{
    MessageBox.Show(ex.Message, "Extraction failed");
}
catch (StackOverflowException)
{
    MessageBox.Show("The target image size is too big", "Extraction failed");
}

try
{
    FrameGrabber.SaveFrameFromVideo(strVideoFile, 0.2d, strBitmapFile);
}
catch (InvalidVideoFileException ex)
{
    MessageBox.Show(ex.Message, "Extraction failed");
}

在这里,我们使用了本文中介绍的 GetFrameFromVideoSaveFrameFromVideo 方法的三个重载中最简单的。

关注点

IMediaDet 和链接的接口/类在 qedit.dll 中公开,该文件可以在 System32 目录中找到。 幸运的是,可以使用 tlbimp 自动导入此 DLL,因此无需代码即可包装它。

要提取图像,有两种方法:在内存中提取(使用 GetBitmapBits - 这里是 GetFrameFromVideo)或提取并保存到位图文件(使用 WriteBitmapBits - 这里是 SaveFrameFromVideo)。

WriteBitmapBits 确实非常简单:我们只需要找到文件中的视频流,打开它并为位图图像指定一个输出文件名。

public static void SaveFrameFromVideo(string videoFile,
         double percentagePosition, string outputBitmapFile,
         out double streamLength, Size target)
{
    if (percentagePosition > 1 || percentagePosition < 0)
        throw new ArgumentOutOfRangeException("percentagePosition", 
                percentagePosition, "Valid range is 0.0 .. 1.0");

    try
    {
        MediaDetClass mediaDet;
        _AMMediaType mediaType;
        if (openVideoStream(videoFile, out mediaDet, out mediaType))
        {
            streamLength = mediaDet.StreamLength;
            
            //calculates the REAL target size of our frame
            if (target == Size.Empty)
                target = getVideoSize(mediaType);
            else
                target = scaleToFit(target, getVideoSize(mediaType));

            mediaDet.WriteBitmapBits(streamLength * percentagePosition, 
                        target.Width, target.Height, outputBitmapFile);

            return;
        }
    }
    catch (COMException ex)
    {
        throw new InvalidVideoFileException(getErrorMsg((uint)ex.ErrorCode), ex);
    }

    throw new InvalidVideoFileException("No video stream was found");
}

您会注意到这里使用了两个 private 方法。 它们是 openVideoStream getVideoSize。 它们的实现很简单

private static bool openVideoStream(string videoFile, 
            out MediaDetClass mediaDetClass, out _AMMediaType aMMediaType)
{
    MediaDetClass mediaDet = new MediaDetClass();
    
    //loads file
    mediaDet.Filename = videoFile;

    //gets # of streams
    int streamsNumber = mediaDet.OutputStreams;

    //finds a video stream and grabs a frame
    for (int i = 0; i < streamsNumber; i++)
    {
        mediaDet.CurrentStream = i;
        _AMMediaType mediaType = mediaDet.StreamMediaType;

        if (mediaType.majortype == JockerSoft.Media.MayorTypes.MEDIATYPE_Video)
        {
            mediaDetClass = mediaDet;
            aMMediaType = mediaType;
            return true;
        }
    }

    mediaDetClass = null;
    aMMediaType = new _AMMediaType();
    return false;
}

(其中 MEDIATYPE_Video 是用于视频文件的 GUID )。

private static Size getVideoSize(_AMMediaType mediaType)
{
    WinStructs.VIDEOINFOHEADER videoInfo = 
        (WinStructs.VIDEOINFOHEADER)Marshal.PtrToStructure(mediaType.pbFormat, 
            typeof(WinStructs.VIDEOINFOHEADER));
    
    return new Size(videoInfo.bmiHeader.biWidth, videoInfo.bmiHeader.biHeight);
}

使用 GetBitmapBits 避免将图像保存在磁盘上稍微复杂一些,因为我们需要处理对内存的直接访问。

第一部分与 SaveFrameFromVideo 相同,然后我们必须调用 GetBitmapBits,将 pBuffer 参数设置为 null 以获取将包含 24bpp 图像的字节缓冲区的 大小(GetBitmapBits 始终返回 24bpp 图像)。

一旦我们有了缓冲区的大小,我们就会在堆上分配内存以接收图像(在此代码的第一个版本中,内存是在堆栈上分配的,如果目标图像很小,这没问题,但如果它很大,我们可能会得到一个不错的 StackOverflowException 因为堆栈内存非常有限)。

之后,我们再次调用 GetBitmapBits,但这一次缓冲区将填充图像字节。 现在我们从这些字节创建一个位图(记住它们以 BITMAPINFOHEADER 结构开头,其大小为 40 字节)。

public static Bitmap GetFrameFromVideo(string videoFile, 
            double percentagePosition, out double streamLength, Size target)
{
    if (percentagePosition > 1 || percentagePosition < 0)
        throw new ArgumentOutOfRangeException("percentagePosition", 
                percentagePosition, "Valid range is 0.0 .. 1.0");

    try 
    {
        MediaDetClass mediaDet;
        _AMMediaType mediaType;
        if (openVideoStream(videoFile, out mediaDet, out mediaType))
        {
            streamLength = mediaDet.StreamLength;

            //calculates the REAL target size of our frame
            if (target == Size.Empty)
                target = getVideoSize(mediaType);
            else
                target = scaleToFit(target, getVideoSize(mediaType));

            unsafe 
            {
                Size s= GetVideoSize(videoFile);
                //equal to sizeof(CommonClasses.BITMAPINFOHEADER);
                int bmpinfoheaderSize = 40;                 

                //get size for buffer
                int bufferSize = (((s.Width * s.Height) * 24) / 8 ) + bmpinfoheaderSize;
                //equal to mediaDet.GetBitmapBits
                //    (0d, ref bufferSize, ref *buffer, target.Width, target.Height);    

                //allocates enough memory to store the frame
                IntPtr frameBuffer = 
                    System.Runtime.InteropServices.Marshal.AllocHGlobal(bufferSize);
                byte* frameBuffer2 = (byte*)frameBuffer.ToPointer();

                //gets bitmap, save in frameBuffer2
                mediaDet.GetBitmapBits(streamLength * percentagePosition, 
                    ref bufferSize, ref *frameBuffer2, target.Width, target.Height);

                //now in buffer2 we have a BITMAPINFOHEADER structure 
                //followed by the DIB bits
                Bitmap bmp = new Bitmap(target.Width, target.Height, target.Width * 3, 
                    System.Drawing.Imaging.PixelFormat.Format24bppRgb, 
                    new IntPtr(frameBuffer2 + bmpinfoheaderSize));

                bmp.RotateFlip(RotateFlipType.Rotate180FlipX);
                System.Runtime.InteropServices.Marshal.FreeHGlobal(frameBuffer);
                return bmp;
            }
        }
    }
    catch (COMException ex)
    {
        throw new InvalidVideoFileException(getErrorMsg((uint)ex.ErrorCode), ex);
    }

    throw new InvalidVideoFileException("No video stream was found");
}

已知限制

  • 最大的问题是使用 stackallocStackOverflowException。 必须有一种方法可以将堆上创建的缓冲区传递给 GetBitmapBits,但我对这些非托管的东西还不太擅长。
    • 这个问题已经解决了。 解决方案由 _coder_ 在下面的评论中提供。
  • 在我的机器上,当将某些目标大小传递给 GetBitmapBits 时,我得到了随机错误:例如,当目标大小为 125x125 时,Bitmap 构造函数失败。
    • 只允许 4 的倍数大小。感谢 ujr(见评论)。
  • "IMediaDet 接口 不支持 VIDEOINFOHEADER2 格式":这意味着它无法打开某些 *.mpeg 视频文件。

历史

  • 2006 年 2 月 27 日:初始版本
  • 2006 年 3 月 17 日:替换了堆栈上的内存分配
  • 2007 年 9 月 27 日:一些修复
从视频文件中提取帧 - CodeProject - 代码之家
© . All rights reserved.