4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Media Foundation で WebCam 静止画キャプチャを試みる

Posted at

ググりながら、調べながら作っていると、できました :thumbsup:

2020-01-09_19h43_18.png

一番参考になったのはこちら: Media Foundation でカメラ画像を取得

つまみ食い形式で解体していきます…

.NET Framework 4.7.2 (C#) WinForms プロジェクトにて

http://mfnet.sourceforge.net/ から MFLibv3_1.zip を入手・解凍し、つぎの 2 つを参照に追加:

  • MediaFoundation.dll
  • MediaFoundation.Extension.dll

ComReleaser

Marshal.ReleaseComObjectIDisposable の発動に合わせて呼び出すユーティリティーです。

ComReleaser.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace WebCamCapPic.Utils
{
    class ComReleaser : IDisposable
    {
        private List<object> items = new List<object>();

        public void Add(object obj)
        {
            items.Add(obj);
        }

        public void Dispose()
        {
            foreach (var one in items)
            {
                Marshal.ReleaseComObject(one);
            }

            items.Clear();
        }
    }
}

Media Foundation の初期化

特にやっていません

デバイス一覧の列挙

ビデオキャプチャ能力のあるデバイスの列挙です。

2020-01-09_19h50_44.png

Form1.cs
        private void Form1_Load(object sender, EventArgs e)
        {
            // http://codeit.blog.fc2.com/blog-entry-5.html
            MF.EnumVideoDeviceSources(out IMFActivate[] devices);
            foreach (var device in devices)
            {
                device.GetString(MFAttributesClsid.MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, out string name);
                device.GetString(MFAttributesClsid.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, out string symLink);
                device.GetGUID(MFAttributesClsid.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, out Guid type);
                devicesCombo.Items.Add(
                    new DeviceItem
                    {
                        Name = name,
                        Type = type,
                        SymLink = symLink,
                    }
                );
            }
        }

        class DeviceItem
        {
            public string Name { get; internal set; }
            public Guid Type { get; internal set; }
            public string SymLink { get; internal set; }

            public override string ToString() => Name;
        }

SymLink には \\?\usb#vid_046d&pid_0825&mi_00#7&31e6e3b&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\{bbefb6c7-2fc4-4139-bb8b-a58bba724083} のような内容が入ります。

メディアタイプの列挙

デバイスを特定したら、つぎに対応フォーマットの列挙です

2020-01-09_19h53_46.png

Form1.cs
        private void devicesCombo_SelectedIndexChanged(object sender, EventArgs e)
        {
            var item = (DeviceItem)devicesCombo.SelectedItem;
            if (item != null)
            {
                mediaCombo.Items.Clear();

                using (var releaser = new ComReleaser())
                {
                    MF.CreateVideoDeviceSource(item.SymLink, out IMFMediaSource source);
                    releaser.Add(source);
                    source.CreatePresentationDescriptor(out IMFPresentationDescriptor presDesc);
                    releaser.Add(presDesc);
                    presDesc.GetStreamDescriptorCount(out int descCount);
                    for (int descIndex = 0; descIndex < descCount; descIndex++)
                    {
                        presDesc.GetStreamDescriptorByIndex(descIndex, out bool selected, out IMFStreamDescriptor strmDesc);
                        releaser.Add(strmDesc);
                        strmDesc.GetMediaTypeHandler(out IMFMediaTypeHandler handler);
                        releaser.Add(handler);
                        handler.GetMediaTypeCount(out int typeCount);
                        for (int typeIndex = 0; typeIndex < typeCount; typeIndex++)
                        {
                            handler.GetMediaTypeByIndex(typeIndex, out IMFMediaType type);
                            releaser.Add(type);
                            type.GetSize(MFAttributesClsid.MF_MT_FRAME_SIZE, out uint width, out uint height);
                            type.GetGUID(MFAttributesClsid.MF_MT_SUBTYPE, out Guid subType);
                            type.GetUINT32(MFAttributesClsid.MF_MT_DEFAULT_STRIDE, out uint stride);
                            type.GetUINT32(MFAttributesClsid.MF_MT_SAMPLE_SIZE, out uint sampleSize);

                            mediaCombo.Items.Add(
                                new MediaItem
                                {
                                    Name = $"#{descIndex}.{typeIndex}: {width}x{height}, {GetSubTypeName(subType)}, {((int)stride)}, {sampleSize}",
                                    DescIndex = descIndex,
                                    TypeIndex = typeIndex,
                                    Width = (int)width,
                                    Height = (int)height,
                                    Stride = (int)stride,
                                    SampleSize = (int)sampleSize,
                                    DeviceItem = item,
                                    SubType = subType,
                                }
                            );
                        }
                    }
                }
            }
        }

        class MediaItem
        {
            public string Name { get; internal set; }
            public int Width { get; internal set; }
            public int Height { get; internal set; }
            public int Stride { get; internal set; }
            public int SampleSize { get; internal set; }
            public DeviceItem DeviceItem { get; internal set; }
            public int DescIndex { get; internal set; }
            public int TypeIndex { get; internal set; }
            public Guid SubType { get; internal set; }

            public override string ToString() => Name;
        }

        private string GetSubTypeName(Guid subType)
        {
            foreach (var field in typeof(MFMediaType).GetFields(BindingFlags.Static | BindingFlags.Public))
            {
                if (field.FieldType == typeof(Guid))
                {
                    if ((Guid)field.GetValue(null) == subType)
                    {
                        return field.Name;
                    }
                }
            }
            return null;
        }

静止画の連続キャプチャ

  • item.DeviceItem.SymLink から一気に IMFMediaSource source 獲得まで進めます。
  • さて。厄介なのは RGB24 以外の画像形式が来た場合です。I420 などです。
  • 一応 VideoProcessorMFT を用いて RGB24 へ変換するようにしています。使い方はググりながら、試行錯誤しながら会得しました😅
Form1.cs
        private void CaptureStillImages(MediaItem item)
        {
            using (var releaser = new ComReleaser())
            {
                MF.CreateVideoDeviceSource(item.DeviceItem.SymLink, out IMFMediaSource source);
                releaser.Add(source);
                source.CreatePresentationDescriptor(out IMFPresentationDescriptor presDesc);
                releaser.Add(presDesc);
                presDesc.GetStreamDescriptorByIndex(item.DescIndex, out bool selected, out IMFStreamDescriptor strmDesc);
                releaser.Add(strmDesc);
                strmDesc.GetMediaTypeHandler(out IMFMediaTypeHandler handler);
                releaser.Add(handler);
                handler.GetMediaTypeByIndex(item.TypeIndex, out IMFMediaType type);
                handler.SetCurrentMediaType(type);

                MF.CreateSourceReaderFromMediaSource(source, out IMFSourceReader reader);
                if (reader == null)
                {
                    return;
                }
                releaser.Add(reader);

                IMFTransform transform = null;
                MFTOutputDataBuffer[] outSamples = null;
                IMFSample outRgb24Sample = null;
                IMFMediaBuffer outRgb24Buffer = null;

                int rgbSize = item.Width * item.Height * 3;

                var needToConvert = item.SubType != MFMediaType.RGB24;
                if (needToConvert)
                {
                    var processor = new VideoProcessorMFT();
                    releaser.Add(processor);
                    transform = (IMFTransform)processor;
                    HR(transform.SetInputType(0, type, MFTSetTypeFlags.None));
                    var rgbMediaType = MF.CreateMediaType();
                    releaser.Add(rgbMediaType);
                    HR(type.CopyAllItems(rgbMediaType));
                    HR(rgbMediaType.SetGUID(MFAttributesClsid.MF_MT_SUBTYPE, MFMediaType.RGB24));
                    HR(rgbMediaType.SetUINT32(MFAttributesClsid.MF_MT_DEFAULT_STRIDE, 3 * item.Width));
                    HR(rgbMediaType.SetUINT32(MFAttributesClsid.MF_MT_SAMPLE_SIZE, rgbSize));
                    HR(transform.SetOutputType(0, rgbMediaType, MFTSetTypeFlags.None));

                    outSamples = new MFTOutputDataBuffer[1];
                    outSamples[0] = new MFTOutputDataBuffer();
                    outRgb24Sample = MF.CreateSample();
                    releaser.Add(outRgb24Sample);
                    outRgb24Buffer = MF.CreateMemoryBuffer(rgbSize);
                    releaser.Add(outRgb24Buffer);
                    outRgb24Sample.AddBuffer(outRgb24Buffer);
                    outSamples[0].pSample = Marshal.GetIUnknownForObject(outRgb24Sample);
                }

                int frames = 0;
                while (!stopEvent.WaitOne(0))
                {
                    var hrRS = reader.ReadSample(
                        (int)MF_SOURCE_READER.AnyStream,
                        MF_SOURCE_READER_CONTROL_FLAG.None,
                        out int streamIndex,
                        out MF_SOURCE_READER_FLAG flags,
                        out long timeStamp,
                        out IMFSample sample
                    );

                    if (sample != null)
                    {
                        try
                        {
                            IMFSample rgbSample = sample;

                            if (transform != null)
                            {
                                while (true)
                                {
                                    var hrPO = transform.ProcessOutput(
                                        MFTProcessOutputFlags.None,
                                        1,
                                        outSamples,
                                        out ProcessOutputStatus status
                                    );
                                    if (hrPO.Succeeded())
                                    {
                                        ConsumeBuffer(outRgb24Buffer, item);
                                        frames++;
                                    }
                                    else
                                    {
                                        break;
                                    }
                                }
                                var hrPI = transform.ProcessInput(0, sample, 0);
                                continue;
                            }

                            rgbSample.GetBufferByIndex(0, out IMFMediaBuffer buff);
                            if (ConsumeBuffer(buff, item))
                            {
                                frames++;
                            }
                            else
                            {
                                return;
                            }
                        }
                        finally
                        {
                            Marshal.ReleaseComObject(sample);
                        }
                    }
                }
            }
        }

        private bool ConsumeBuffer(IMFMediaBuffer buff, MediaItem item)
        {
            buff.Lock(out IntPtr ptr, out int maxLen, out int curLen);
            try
            {
                Bitmap pic = new Bitmap(item.Width, item.Height, PixelFormat.Format24bppRgb);
                var bitmapData = pic.LockBits(
                    new Rectangle(0, 0, pic.Width, pic.Height),
                    ImageLockMode.WriteOnly,
                    PixelFormat.Format24bppRgb
                );
                try
                {
                    byte[] temp = new byte[curLen];
                    Marshal.Copy(ptr, temp, 0, temp.Length);
                    Marshal.Copy(temp, 0, bitmapData.Scan0, Math.Min(temp.Length, bitmapData.Stride * bitmapData.Height));
                }
                finally
                {
                    pic.UnlockBits(bitmapData);
                }
                if (item.Stride < 0)
                {
                    pic.RotateFlip(RotateFlipType.RotateNoneFlipY);
                }
                try
                {
                    Invoke((Action<Bitmap>)UpdatePreview, pic);
                    return true;
                }
                catch (ObjectDisposedException)
                {
                    return false;
                }
            }
            finally
            {
                buff.Unlock();
            }
        }

        private void HR(HResult hr)
        {
            Debug.Assert(hr.Succeeded());
        }
  • 画像のビットマップは IMFSample の中に IMFMediaBuffer を 1 個以上含む形で流れてきます。
  • 画像の形式は IMFMediaType のメソッドを呼び出して必要情報を取得します。
  • IMFMediaTypeMFAttributesClsid.MF_MT_SUBTYPEMFMediaType.RGB24 であれば無変換で結構ですが、
    MFMediaType.I420 などが来ると Gdiplus が取り扱いできる形に変えてあげなければなりません。
  • それもググると計算式が出てくるので実装は不可能ではありませんが、手間なのと DirectShow の利用経験から「可能」という事が事前に分かっていたので方法を調べることにしました。
  • 前述の VideoProcessorMFTnew して使います。
  • SetInputTypeSetOutputType で、フォーマットを教えてあげます。
  • IMFMediaType には複製メソッドがありません。SetGUID を使うと、既存の IMFMediaType が書き換わってしまいます。多分…
  • そこで MF.CreateMediaType() で新規 IMFMediaType を作成し、CopyAllItems で内容をコピーしてもらいます。
  • コピー先の IMFMediaTypeMFMediaType.RGB24 に設定し、SetOutputType へ渡します。

変換:

  • IMFTransform.ProcessInputIMFTransform.ProcessOutput を使うんですが、これがまた面白い。
  • やり方はつぎのような感じです。
  while (true) {
    if (xxx.ProcessOutput(outputSample) == 成功) {
      ConsumeBuffer(outputBuffer);
    }
    else {
      // MF_E_TRANSFORM_NEED_MORE_INPUT が返されているものと仮定。
      break;
    }
  }
  xxx.ProcessInput(inputSample);
  • ちなみに inputSample は使い終わったあとに Marshal.ReleaseComObject(sample) しておかないと、いずれ ReadSample がブロックします。
  • わたしが試した時は 18 個分 inputSample をドレインするとブロックしました。
  • こういう不思議な仕組みは DirectShow でもありましたね…

image.png

という感じで重要事項を書きだしたので、これで終わります👋

4
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?