カスタムのビデオ特殊効果

この記事では、ビデオ ストリームのカスタム効果を作成するための IBasicVideoEffect インターフェイスを実装する Windows ランタイム コンポーネントを作成する方法について説明します。 カスタム効果は、デバイスのカメラへのアクセスを提供する MediaCapture、メディア クリップから複雑なコンポジションを作成するための MediaComposition など、さまざまな Windows ランタイム API で使うことができます。

アプリへのカスタム効果の追加

カスタムのビデオ特殊効果は、IBasicVideoEffect インターフェイスを実装するクラスで定義します。 このクラスは、アプリのプロジェクトに直接含めることはできません。 代わりに、Windows ランタイム コンポーネントを使ってビデオ特殊効果のクラスをホストする必要があります。

ビデオ特殊効果用の Windows ランタイム コンポーネントの追加

  1. Microsoft Visual Studio で、ソリューションを開き、[ファイル] メニューから [追加] -> [新しいプロジェクト] の順に選択します。
  2. プロジェクトの種類として [Windows ランタイム コンポーネント (ユニバーサル Windows)] を選びます。
  3. この例では、プロジェクトに VideoEffectComponent という名前を付けます。 この名前は後でコードで参照されます。
  4. [OK] をクリックします。
  5. プロジェクト テンプレートに基づいて、Class1.cs という名前のクラスが作成されます。 ソリューション エクスプローラーで、Class1.cs のアイコンを右クリックし、[名前の変更] をクリックします。
  6. ファイル名を ExampleVideoEffect.cs に変更します。 すべての参照を新しい名前に更新するかどうかを確認するメッセージが表示されたら、 [はい] をクリックします。
  7. ExampleVideoEffect.cs を開き、クラス定義を更新して IBasicVideoEffect インターフェイスを実装します。
public sealed class ExampleVideoEffect : IBasicVideoEffect

この記事の例で使うすべての型にアクセスできるように、効果のクラス ファイルに次の名前空間を含める必要があります。

using Windows.Media.Effects;
using Windows.Media.MediaProperties;
using Windows.Foundation.Collections;
using Windows.Graphics.DirectX.Direct3D11;
using Windows.Graphics.Imaging;

ソフトウェア処理を使用した IBasicVideoEffect インターフェイスの実装

ビデオ特殊効果では、IBasicVideoEffect インターフェイスのすべてのメソッドとプロパティを実装する必要があります。 このセクションでは、このインターフェイスの実装方法の説明として、ソフトウェア処理を使用した簡単な実装を示します。

Close メソッド

Close メソッドは、クラスで効果をシャットダウンする必要があるときに呼び出されます。 このメソッドを使って、作成したすべてのリソースを破棄する必要があります。 このメソッドの MediaEffectClosedReason 引数により、効果が正常に終了されたかどうかがわかります。エラーが発生したり、必要なエンコード形式が効果でサポートされていないと、この引数で通知されます。

public void Close(MediaEffectClosedReason reason)
{
    // Dispose of effect resources
}

DiscardQueuedFrames メソッド

DiscardQueuedFrames メソッドは、効果をリセットする必要があるときに呼び出されます。 典型的なシナリオとしては、現在のフレームの処理で使うために前に処理したフレームを保存している場合などが挙げられます。 このメソッドが呼び出されたときは、保存した一連のフレームを破棄する必要があります。 このメソッドでは、蓄積されたビデオ フレームだけでなく、前のフレームに関連するすべての状態をリセットできます。

private int frameCount;
public void DiscardQueuedFrames()
{
    frameCount = 0;
}

IsReadOnly プロパティ

IsReadOnly プロパティは、効果の出力への書き込みを行うかどうかを示します。 アプリでビデオ フレームを変更しない場合 (ビデオ フレームの分析のみを行う場合など) は、このプロパティを true に設定します。これにより、フレーム入力がフレーム出力に効率的にコピーされるようになります。

ヒント

IsReadOnly プロパティを true に設定すると、入力フレームが出力フレームにコピーされてから ProcessFrame が呼び出されます。 IsReadOnly プロパティを true に設定しても、ProcessFrame での効果の出力フレームに対する書き込みは制限されません。

public bool IsReadOnly { get { return false; } }

SetEncodingProperties メソッド

SetEncodingProperties は、効果の対象となるビデオ ストリームのエンコード プロパティを示すために呼び出されます。 このメソッドは、ハードウェア レンダリングに使う Direct3D デバイスへの参照も提供します。 このデバイスの用途については、この後のハードウェア処理の例で説明します。

private VideoEncodingProperties encodingProperties;
public void SetEncodingProperties(VideoEncodingProperties encodingProperties, IDirect3DDevice device)
{
    this.encodingProperties = encodingProperties;
}

SupportedEncodingProperties プロパティ

SupportedEncodingProperties プロパティは、効果でサポートされるエンコード プロパティを確認するためにシステムでチェックされます。 効果で指定したプロパティを使ってビデオをエンコードできない場合、Close が呼び出され、効果がビデオ パイプラインから削除されます。

public IReadOnlyList<VideoEncodingProperties> SupportedEncodingProperties
{            
    get
    {
        var encodingProperties = new VideoEncodingProperties();
        encodingProperties.Subtype = "ARGB32";
        return new List<VideoEncodingProperties>() { encodingProperties };

        // If the list is empty, the encoding type will be ARGB32.
        // return new List<VideoEncodingProperties>();
    }
}

注意

SupportedEncodingProperties から返される VideoEncodingProperties オブジェクトの一覧を空にすると、既定で ARGB32 エンコードが使われます。

 

SupportedMemoryTypes プロパティ

SupportedMemoryTypes プロパティは、ソフトウェア メモリとハードウェア (GPU) メモリのどちらのビデオ フレームにアクセスするかを確認するためにシステムでチェックされます。 MediaMemoryTypes.Cpu を指定すると、画像データを SoftwareBitmap オブジェクトに格納する入力フレームと出力フレームが渡されます。 MediaMemoryTypes.Gpu を指定すると、画像データを IDirect3DSurface オブジェクトに格納する入力フレームと出力フレームが渡されます。

public MediaMemoryTypes SupportedMemoryTypes { get { return MediaMemoryTypes.Cpu; } }

注意

MediaMemoryTypes.GpuAndCpu を指定すると、GPU とシステム メモリのどちらを使うかがパイプラインの効率に基づいて判断されます。 この値を使う場合は、ProcessFrame メソッドで SoftwareBitmapIDirect3DSurface のどちらにデータが格納されたかをチェックし、それに応じてフレームを処理する必要があります。

 

TimeIndependent プロパティ

TimeIndependent プロパティは、効果のタイミングを合わせる必要がないかどうかを示します。 true に設定すると、効果のパフォーマンスを高めるために最適化を使用できるようになります。

public bool TimeIndependent { get { return true; } }

SetProperties メソッド

SetProperties メソッドは、呼び出し元のアプリで効果のパラメーターを調整するために使われます。 プロパティは、プロパティ名と値の IPropertySet マップとして渡されます。

private IPropertySet configuration;
public void SetProperties(IPropertySet configuration)
{
    this.configuration = configuration;
}

指定の値に基づいて各ビデオ フレームのピクセルを暗くする簡単な例を次に示します。 プロパティを宣言し、呼び出し元アプリで設定された値を TryGetValue で取得しています。 値が設定されていない場合は、既定値の .5 が使われます。

public double FadeValue
{
    get
    {
        object val;
        if (configuration != null && configuration.TryGetValue("FadeValue", out val))
        {
            return (double)val;
        }
        return .5;
    }
}

ProcessFrame メソッド

ProcessFrame メソッドは、ビデオの画像データを変更するためのメソッドです。 このメソッドはフレームごとに 1 回呼び出され、ProcessVideoFrameContext オブジェクトが渡されます。 このオブジェクトには、処理対象の着信フレームを格納する入力 VideoFrame オブジェクトと、ビデオ パイプラインの残りの部分に渡す画像データを書き込む出力 VideoFrame オブジェクトが含まれています。 それらの VideoFrame オブジェクトのそれぞれに SoftwareBitmap プロパティと Direct3DSurface プロパティがありますが、どちらを使用できるかは SupportedMemoryTypes で指定した値で決まります。

ここでは、ソフトウェア処理を使用した ProcessFrame メソッドの簡単な実装例を示します。 SoftwareBitmap オブジェクトの操作について詳しくは、「イメージング」をご覧ください。 ハードウェア処理を使用した ProcessFrame の実装例については、この記事の後半で紹介します。

SoftwareBitmap のデータ バッファーにアクセスするには COM 相互運用機能が必要になるため、効果のクラス ファイルに System.Runtime.InteropServices 名前空間を含める必要があります。

using System.Runtime.InteropServices;

効果の名前空間に次のコードを追加して、画像のバッファーにアクセスするためのインターフェイスをインポートします。

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

注意

この手法では管理対象外のネイティブの画像バッファーにアクセスするため、アンセーフ コードを許可するようにプロジェクトを構成する必要があります。

  1. ソリューション エクスプローラーで、VideoEffectComponent プロジェクトを右クリックし、[プロパティ] を選択します。
  2. [ビルド] タブを選択します。
  3. [アンセーフ コードの許可] チェック ボックスをオンにします。

 

これで、ProcessFrame メソッドの実装を追加できます。 最初に、入力と出力の両方のソフトウェア ビットマップから BitmapBuffer オブジェクトを取得します。 出力フレームが書き込み用で、入力フレームが読み取り用です。 次に、CreateReference を呼び出して、各バッファーの IMemoryBufferReference を取得します。 その後、実際のデータ バッファーを取得するために、先ほど定義した COM 相互運用機能のインターフェイス (IMemoryByteAccess) として IMemoryBufferReference オブジェクトをキャストし、GetBuffer を呼び出します。

これで、データ バッファーが取得され、入力バッファーからの読み取りと出力バッファーへの書き込みが可能になります。 GetPlaneDescription を呼び出して、バッファーのレイアウトを取得します。バッファーの幅、ストライド、初期オフセットについての情報が提供されます。 ピクセルあたりのビット数は、SetEncodingProperties メソッドで既に設定したエンコード プロパティで決まります。 バッファーの形式情報を使って、各ピクセルのバッファーへのインデックスを特定します。 ソース バッファーのピクセル値をターゲット バッファーにコピーし、そのカラー値にこの効果の FadeValue プロパティで定義した値を掛けます。これで、指定した値に応じてピクセルが暗くなります。

public unsafe void ProcessFrame(ProcessVideoFrameContext context)
{
    using (BitmapBuffer buffer = context.InputFrame.SoftwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
    using (BitmapBuffer targetBuffer = context.OutputFrame.SoftwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
    {
        using (var reference = buffer.CreateReference())
        using (var targetReference = targetBuffer.CreateReference())
        {
            byte* dataInBytes;
            uint capacity;
            ((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacity);

            byte* targetDataInBytes;
            uint targetCapacity;
            ((IMemoryBufferByteAccess)targetReference).GetBuffer(out targetDataInBytes, out targetCapacity);

            var fadeValue = FadeValue;

            // Fill-in the BGRA plane
            BitmapPlaneDescription bufferLayout = buffer.GetPlaneDescription(0);
            for (int i = 0; i < bufferLayout.Height; i++)
            {
                for (int j = 0; j < bufferLayout.Width; j++)
                {

                    byte value = (byte)((float)j / bufferLayout.Width * 255);

                    int bytesPerPixel = 4; 
                    if (encodingProperties.Subtype != "ARGB32")
                    {
                        // If you support other encodings, adjust index into the buffer accordingly
                    }
                    

                    int idx = bufferLayout.StartIndex + bufferLayout.Stride * i + bytesPerPixel * j;

                    targetDataInBytes[idx + 0] = (byte)(fadeValue * (float)dataInBytes[idx + 0]);
                    targetDataInBytes[idx + 1] = (byte)(fadeValue * (float)dataInBytes[idx + 1]);
                    targetDataInBytes[idx + 2] = (byte)(fadeValue * (float)dataInBytes[idx + 2]);
                    targetDataInBytes[idx + 3] = dataInBytes[idx + 3];
                }
            }
        }
    }
}

ハードウェア処理を使用した IBasicVideoEffect インターフェイスの実装

カスタムのビデオ特殊効果をハードウェア (GPU) 処理を使って作成する場合も、方法は上記のソフトウェア処理を使った場合とほぼ同じです。 このセクションでは、効果にハードウェア処理を使う場合のいくつかの違いについて説明します。 この例では、Win2D Windows ランタイム API を使います。 Win2D を使う方法について詳しくは、Win2D のドキュメントをご覧ください。

次の手順に従って、この記事の最初に「アプリへのカスタム効果の追加」で作成したプロジェクトに Win2D NuGet パッケージを追加します。

効果のプロジェクトへの Win2D NuGet パッケージの追加するには

  1. ソリューション エクスプローラーで、VideoEffectComponent プロジェクトを右クリックし、[NuGet パッケージの管理] をクリックします。
  2. ウィンドウの上部で [参照] タブをクリックします。
  3. 検索ボックスに「Win2D」と入力します。
  4. [Win2D.uwp] を選択し、右のウィンドウで [インストール] を選択します。
  5. [変更の確認] ダイアログに、インストールするパッケージが表示されます。 [OK] をクリックします。
  6. パッケージのライセンスに同意します。

基本的なプロジェクトのセットアップに含まれる名前空間に加え、Win2D で提供される次の名前空間を含める必要があります。

using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas;

この効果では画像データの操作に GPU メモリを使うため、SupportedMemoryTypes プロパティで MediaMemoryTypes.Gpu を返します。

public MediaMemoryTypes SupportedMemoryTypes { get { return MediaMemoryTypes.Gpu; } }

効果でサポートするエンコード プロパティを SupportedEncodingProperties プロパティで設定します。 Win2D を操作するときは、ARGB32 エンコードを使う必要があります。

public IReadOnlyList<VideoEncodingProperties> SupportedEncodingProperties {
    get
    {
        var encodingProperties = new VideoEncodingProperties();
        encodingProperties.Subtype = "ARGB32";
        return new List<VideoEncodingProperties>() { encodingProperties };
    }
}

SetEncodingProperties メソッドを使って、メソッドに渡される IDirect3DDevice から新しい Win2D CanvasDevice オブジェクトを作成します。

private CanvasDevice canvasDevice;
public void SetEncodingProperties(VideoEncodingProperties encodingProperties, IDirect3DDevice device)
{
    canvasDevice = CanvasDevice.CreateFromDirect3D11Device(device);
}

SetProperties の実装は前述のソフトウェア処理の例と同じです。 この例では、BlurAmount プロパティを使って Win2D のぼかし効果を設定します。

private IPropertySet configuration;
public void SetProperties(IPropertySet configuration)
{
    this.configuration = configuration;
}
public double BlurAmount
{
    get
    {
        object val;
        if (configuration != null && configuration.TryGetValue("BlurAmount", out val))
        {
            return (double)val;
        }
        return 3;
    }
}

最後に、画像データを実際に処理する ProcessFrame メソッドを実装します。

Win2D API を使って、入力フレームの Direct3DSurface プロパティから CanvasBitmap を作成します。 出力フレームの Direct3DSurface から CanvasRenderTarget を作成し、このレンダー ターゲットから CanvasDrawingSession を作成します。 新しい Win2D GaussianBlurEffect を初期化し、SetPropertiesBlurAmount プロパティを使って効果を公開します。 最後に、CanvasDrawingSession.DrawImage メソッドを呼び出して、入力ビットマップをレンダー ターゲットにぼかし効果を使って描画します。

public void ProcessFrame(ProcessVideoFrameContext context)
{

    using (CanvasBitmap inputBitmap = CanvasBitmap.CreateFromDirect3D11Surface(canvasDevice, context.InputFrame.Direct3DSurface))
    using (CanvasRenderTarget renderTarget = CanvasRenderTarget.CreateFromDirect3D11Surface(canvasDevice, context.OutputFrame.Direct3DSurface))
    using (CanvasDrawingSession ds = renderTarget.CreateDrawingSession())
    {


        var gaussianBlurEffect = new GaussianBlurEffect
        {
            Source = inputBitmap,
            BlurAmount = (float)BlurAmount,
            Optimization = EffectOptimization.Speed
        };

        ds.DrawImage(gaussianBlurEffect);

    }
}

アプリへのカスタム効果の追加

アプリからビデオ特殊効果を使うには、効果のプロジェクトへの参照をアプリに追加する必要があります。

  1. ソリューション エクスプローラーで、アプリのプロジェクトの下にある [参照設定] を右クリックし、[参照の追加] を選択します。
  2. [プロジェクト] タブを展開し、[ソリューション] を選択して、効果のプロジェクトの名前に対応するチェック ボックスをオンにします。 この例では、VideoEffectComponent という名前です。
  3. [OK] をクリックします。

カメラのビデオ ストリームへのカスタム効果の追加

シンプルなカメラ プレビューへのアクセス」の手順に従って、カメラからのシンプルなプレビュー ストリームを設定できます。 この手順を実行すると、カメラのビデオ ストリームへのアクセスに使う初期化済みの MediaCapture オブジェクトが得られます。

カスタムのビデオ特殊効果をカメラ ストリームに追加するには、まず、新しい VideoEffectDefinition オブジェクトを作成し、効果の名前空間とクラスの名前を渡します。 次に、MediaCapture オブジェクトの AddVideoEffect メソッドを呼び出して、指定したストリームに効果を追加します。 この例では、MediaStreamType.VideoPreview 値を使って、効果をプレビュー ストリームに追加するように指定します。 アプリがビデオ キャプチャをサポートしている場合は、MediaStreamType.VideoRecord を使ってキャプチャ ストリームに効果を追加することもできます。 AddVideoEffect から、カスタム効果を表す IMediaExtension オブジェクトが返されます。 効果の設定は SetProperties メソッドを使って設定できます。

効果が追加されると、StartPreviewAsync が呼び出されてプレビュー ストリームが始まります。

var videoEffectDefinition = new VideoEffectDefinition("VideoEffectComponent.ExampleVideoEffect");

IMediaExtension videoEffect =
   await mediaCapture.AddVideoEffectAsync(videoEffectDefinition, MediaStreamType.VideoPreview);

videoEffect.SetProperties(new PropertySet() { { "FadeValue", .25 } });

await mediaCapture.StartPreviewAsync();

MediaComposition のクリップへのカスタム効果の追加

ビデオ クリップからメディア コンポジションを作成する一般的なガイダンスについては、「メディア コンポジションと編集」をご覧ください。 次のコード スニペットは、カスタムのビデオ特殊効果を使ってシンプルなメディア コンポジションを作成する例を示しています。 CreateFromFileAsync を呼び出して MediaClip オブジェクトを作成し、ユーザーが FileOpenPicker で選択したビデオ ファイルを渡して新しい MediaComposition にクリップを追加します。 次に、新しい VideoEffectDefinition オブジェクトを作成し、効果の名前空間とクラスの名前を渡します。 最後に、MediaClip オブジェクトの VideoEffectDefinitions コレクションに効果の定義を追加します。

MediaComposition composition = new MediaComposition();
var clip = await MediaClip.CreateFromFileAsync(pickedFile);
composition.Clips.Add(clip);

var videoEffectDefinition = new VideoEffectDefinition("VideoEffectComponent.ExampleVideoEffect", new PropertySet() { { "FadeValue", .5 } });

clip.VideoEffectDefinitions.Add(videoEffectDefinition);