从视频文件中提取帧






4.72/5 (34投票s)
用于使用 IMediaDet 从大多数视频文件格式中提取帧的类。

引言
此类包含使用 IMediaDet 接口 的方法,该接口可在 Microsoft DirectShow 中找到。 Media Detector 对象,除其他外,可用于从包括 *.avi、*.wmv 和一些 *.mpeg 文件在内的几种文件格式中提取静止图片。
此类公开了 GetFrameFromVideo
、GetVideoSize
和 SaveFrameFromVideo
方法,这些方法可以从任何 .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");
}
在这里,我们使用了本文中介绍的 GetFrameFromVideo
和 SaveFrameFromVideo
方法的三个重载中最简单的。
关注点
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");
}
已知限制
- 最大的问题是使用
stackalloc
的StackOverflowException
。 必须有一种方法可以将堆上创建的缓冲区传递给GetBitmapBits
,但我对这些非托管的东西还不太擅长。- 这个问题已经解决了。 解决方案由 _coder_ 在下面的评论中提供。
- 在我的机器上,当将某些目标大小传递给
GetBitmapBits
时,我得到了随机错误:例如,当目标大小为 125x125 时,Bitmap
构造函数失败。- 只允许 4 的倍数大小。感谢 ujr(见评论)。
- "
IMediaDet 接口
不支持VIDEOINFOHEADER2
格式":这意味着它无法打开某些 *.mpeg 视频文件。
历史
- 2006 年 2 月 27 日:初始版本
- 2006 年 3 月 17 日:替换了堆栈上的内存分配
- 2007 年 9 月 27 日:一些修复