F#でWPF --- CatmullRom曲線の描画

図形を描画する際に、指定した点を通る滑らかな曲線を描きたいことがあります。

f:id:any-programming:20170312220605p:plain

今回はCatmullRom曲線でこれを実現したいと思います。

CatmullRom曲線

2点間の補間は、更にその前後の点を利用して求められます。

2点(p1、p2)と、その前後の点(p0、p3)の4点から、下記の式にて任意の位置が求められます。

f:id:any-programming:20170312221749p:plain

Vector2定義

System.Windows.Vectorでは演算子が不十分であるため、独自のVector2を下記の様に定義しました。

type Vector2 =
    { X:double; Y:double }
    static member (~-) (v:Vector2)            = { X = -v.X;      Y = -v.Y }
    static member (+)  (a:Vector2, b:Vector2) = { X = a.X + b.X; Y = a.Y + b.Y }
    static member (-)  (a:Vector2, b:Vector2) = { X = a.X - b.X; Y = a.Y - b.Y }
    static member (*)  (s:double,  v:Vector2) = { X = s * v.X;   Y = s * v.Y }
    static member (*)  (v:Vector2, s:double)  = s * v
    static member (/)  (v:Vector2, s:double)  = { X = v.X / s;   Y = v.Y / s }


CatmullRom定義

Vector2 listを持つレコードと、interpolate関数を定義しています。

type CatmullRom = { Points : Vector2 list }

[<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>]
module CatmullRom =
    let interpolate (p0 : Vector2) (p1 : Vector2) (p2 : Vector2) (p3 : Vector2) t =
        let v0 = (p2 - p0) / 2.0
        let v1 = (p3 - p1) / 2.0
        (2.0 * p1 - 2.0 * p2 + v0 + v1) * t * t * t + (-3.0 * p1 + 3.0 * p2 - 2.0 * v0 - v1) * t * t + v0 * t + p1


Polylineで表示

CatmullRom曲線をPolylineで表示してみます。

4つの点の間を100分割して表示しています。(始点、終点は複製をしてあるため、入力は6点となっています。)

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="180" Width="160">
    <Canvas Margin="20">
        <Polyline Stroke="Red" StrokeThickness="10" Points="{Binding Points}" />
    </Canvas>
</Window>
open System
open System.Windows
open System.Windows.Media

type Model = { Points : PointCollection }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window

    let interpolate p0 p1 p2 p3 count =
        [ 0..count - 1 ]
        |> List.map (fun x -> double x / double count)
        |> List.map (CatmullRom.interpolate p0 p1 p2 p3)

    let points = 
        [ { X = 0.0;   Y = 0.0 }
          { X = 0.0;   Y = 0.0 }
          { X = 100.0; Y = 0.0 }
          { X = 0.0;   Y = 100.0 }
          { X = 100.0; Y = 100.0 }
          { X = 100.0; Y = 100.0 } ]
        |> List.windowed 4
        |> List.collect (fun x -> interpolate x.[0] x.[1] x.[2] x.[3] 100)

    let toPoint x = Point(x.X, x.Y)

    window.DataContext <- { Points = points |> List.map toPoint |> PointCollection }
    Application().Run(window) |> ignore    
    0

f:id:any-programming:20170312224702p:plain

ベジエ曲線で描画

上記でのPolyline利用では、ユーザーが補間したものを用意する必要があります。

そこでCatmullRom曲線をベジエ曲線に変換して描いてみます。

ベジエ曲線

2点間の補間は、その他2つのコントロールポイントを利用して求められます。

2点(p1、p2)と、コントロールポイント(c1、c2)の4点から、下記の式にて任意の位置が求められます。

f:id:any-programming:20170312222948p:plain

CatmullRom→ベジエ

CatmullRomの4点(p0、p1、p2、p3)が与えられたとき、ベジエ曲線のコントロールポイント(c1、c2)は下記の式で与えられます。
f:id:any-programming:20170312225322p:plain

Pathで描画

スタート位置をPathFigureで指定し、残りをPolyBezierSegmentのPointsにバインドすることによりベジエ曲線を描いています。

Pointsの中身は(p1、c1_1、c2_2、p2、c2_1、c2_2、p3...)のように、通過点の間にコントロールポイントが挟まっているものになります。

Polylineで描画したものと同じ図形であることが確認できます。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="180" Width="160">
    <Canvas Margin="20">
        <Path Stroke="Red" StrokeThickness="10">
            <Path.Data>
                <PathGeometry>
                    <PathGeometry.Figures>
                        <PathFigure StartPoint="{Binding BezierStartPoint}">
                            <PolyBezierSegment Points="{Binding BezierPoints}" />
                        </PathFigure>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>
    </Canvas>
</Window>
module CatmullRom =
    let bezierControlPoints (p0 : Vector2) (p1 : Vector2) (p2 : Vector2) (p3 : Vector2) =
        let c1 = p1 + (p2 - p0) / 6.0
        let c2 = p2 + (p1 - p3) / 6.0
        c1, c2

type Model = 
    { BezierStartPoint : Point
      BezierPoints : PointCollection }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window

    let points = 
        [ { X = 0.0;   Y = 0.0 }
          { X = 0.0;   Y = 0.0 }
          { X = 100.0; Y = 0.0 }
          { X = 0.0;   Y = 100.0 }
          { X = 100.0; Y = 100.0 }
          { X = 100.0; Y = 100.0 } ]
        |> List.windowed 4
        |> List.collect (fun x -> 
            let c1, c2 = CatmullRom.bezierControlPoints x.[0] x.[1] x.[2] x.[3]
            [ c1; c2; x.[2] ])

    let toPoint x = Point(x.X, x.Y)

    window.DataContext <- { BezierStartPoint = Point(0.0, 0.0)
                            BezierPoints = points |> List.map toPoint |> PointCollection }

    Application().Run(window) |> ignore    
    0

f:id:any-programming:20170312224702p:plain




MediaFoundation --- 音声読み書き

下記記事でMediaFoundationを用いて動画の読み書きを行いました。
MediaFoundation --- 動画の読み込み - 何でもプログラミング
MediaFoundation --- 動画書き出し - 何でもプログラミング

今回は音声データの読み書きを行ってみたいと思います。

分かりやすさ重視のため、エラー処理は省いてあります。

アプリケーションコード

今回はOpenCVのサンプルデータ、Megamind.aviから音声データだけ抽出します。

音声データはAAC形式で保存してみます。

出力bytesPerSecは12000、16000、20000、24000のいずれかとなります。(詳しくは下記を参照してください)
AAC Encoder (Windows)

ConfigureAudioDecoder、GetAudioInfo、ConfigureAudioEncoder、CaptureAudio、WriteAudioは後述いたします。

#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "mfreadwrite.lib")

int main()
{
    CoInitialize(NULL);
    MFStartup(MF_VERSION);

    // Reader作成
    IMFSourceReader* reader;
    MFCreateSourceReaderFromURL(L"c:\\lib\\opencv3.2\\sources\\samples\\data\\megamind.avi", NULL, &reader);

    ConfigureAudioDecoder(reader, MFAudioFormat_PCM);

    UINT32 channels, samplePerSec, bitsPerSample, bytesPerSec;
    GetAudioInfo(reader, &channels, &samplePerSec, &bitsPerSample, &bytesPerSec);

    // Writer作成
    IMFSinkWriter* writer;
    MFCreateSinkWriterFromURL(L"output.mp4", NULL, NULL, &writer);

    auto streamIndex = ConfigureAudioEncoder(writer, MFAudioFormat_PCM, MFAudioFormat_AAC, channels, samplePerSec, bitsPerSample, 24000);

    // 書き出し
    writer->BeginWriting();
    while (true)
    {
        LONGLONG time, duration;
        auto data = CaptureAudio(reader, &time, &duration);
        if (data.empty())
            break;
        WriteAudio(writer, streamIndex, data.data(), data.size(), time, duration);
    }
    writer->Finalize();

    MFShutdown();
    CoUninitialize();
    return 0;
}


ConfigureAudioDecoder

デコード先のフォーマットを指定して、readerにセットします。

void ConfigureAudioDecoder(IMFSourceReader* reader, GUID format)
{
    IMFMediaType* mediaType;
    MFCreateMediaType(&mediaType);
    mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
    mediaType->SetGUID(MF_MT_SUBTYPE, format);

    reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, mediaType);

    mediaType->Release();
}


GetAudioInfo

チャンネル数、サンプリング数、サンプリングサイズ、ビットレートを取得できます。

void GetAudioInfo(IMFSourceReader* reader, UINT32* channels, UINT32* samplePerSec, UINT32* bitsPerSample, UINT32* bytesPerSec)
{
    IMFMediaType* mediaType;
    reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &mediaType);

    mediaType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channels);
    mediaType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, samplePerSec);
    mediaType->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample);
    mediaType->GetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, bytesPerSec);
}


ConfigureAudioEncoder

入力フォーマット、エンコーダ、チャンネル数、サンプリング数、サンプリングサイズ、ビットレートを指定します。

DWORD ConfigureAudioEncoder(IMFSinkWriter* writer, GUID inputFormat, GUID outputFormat, UINT32 channels, UINT32 samplePerSec, UINT32 bitsPerSample, UINT32 bytesPerSec)
{
    auto createType = [&](GUID subtype)
    {
        IMFMediaType* type;
        MFCreateMediaType(&type);
        type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
        type->SetGUID(MF_MT_SUBTYPE, subtype);
        type->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channels);
        type->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, samplePerSec);
        type->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample);
        type->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, bytesPerSec);
        return type;
    };

    // OutputType
    IMFMediaType* outputType = createType(outputFormat);
    DWORD streamIndex;
    writer->AddStream(outputType, &streamIndex);
    outputType->Release();

    // InputType
    IMFMediaType* inputType = createType(inputFormat);
    writer->SetInputMediaType(streamIndex, inputType, NULL);
    inputType->Release();

    return streamIndex;
}


CaptureAudio

1フレーム当たりの音声データを取得します。

ついでにフレームの開始時間と長さも取得します。

std::vector<BYTE> CaptureAudio(IMFSourceReader* reader, LONGLONG* time, LONGLONG* duration)
{
    // 読み込み
    DWORD flags;
    IMFSample* sample;
    reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, NULL, &flags, NULL, &sample);
    if (sample == nullptr)
        return std::vector<BYTE>();

    // Buffer取得
    IMFMediaBuffer* buffer;
    sample->GetBufferByIndex(0, &buffer);

    // データ取得
    BYTE* p;
    DWORD size;
    buffer->Lock(&p, NULL, &size);
    std::vector<BYTE> data(size);
    memcpy(data.data(), p, size);
    buffer->Unlock();

    // 開始時間、長さ取得
    sample->GetSampleTime(time);
    sample->GetSampleDuration(duration);

    buffer->Release();
    sample->Release();
    return data;
}


WriteAudio

音声データをwriterで書き出します。

SetSampleTime、SetSampleDurationを行わないとエラーが出力されます。

void WriteAudio(IMFSinkWriter* writer, int streamIndex, BYTE* data, int size, LONGLONG time, LONGLONG duration)
{
    // Buffer作成
    IMFMediaBuffer* buffer;
    MFCreateMemoryBuffer(size, &buffer);

    // データ書き込み
    BYTE* bufferPtr;
    buffer->Lock(&bufferPtr, NULL, NULL);
    memcpy(bufferPtr, data, size);
    buffer->Unlock();
    buffer->SetCurrentLength(size);

    // Sample作成
    IMFSample* sample;
    MFCreateSample(&sample);
    sample->AddBuffer(buffer);
    sample->SetSampleTime(time);
    sample->SetSampleDuration(duration);

    // 書き出し
    writer->WriteSample(streamIndex, sample);

    sample->Release();
    buffer->Release();
}






MediaFoundation --- 動画書き出し

下記記事にてMediaFoundationで動画を読み込む方法を調べました。
MediaFoundation --- 動画の読み込み - 何でもプログラミング

今回は書き出しについて調べたいと思います。

尚、見通し優先のため、エラー処理は省いてあります。

アプリケーションコード

動画はOpenCVのサンプル、vtest.aviを利用します。

動画を読み込んでH.264で出力します。

動画の読み込みにはOpenCVを利用しています。

ConfigureVideoEncoder、Writeは後述いたします。

#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "mfreadwrite.lib")

#include <opencv2\opencv.hpp>
#pragma comment(lib, "opencv_world320d.lib")

int main()
{
    CoInitialize(NULL);
    MFStartup(MF_VERSION);

    // 動画読み込み
    cv::VideoCapture capture("c:\\lib\\opencv3.2\\sources\\samples\\data\\vtest.avi");
    int width  = (int)capture.get(CV_CAP_PROP_FRAME_WIDTH);
    int height = (int)capture.get(CV_CAP_PROP_FRAME_HEIGHT);
    double fps = capture.get(CV_CAP_PROP_FPS);

    // Writer準備
    IMFSinkWriter* writer;
    MFCreateSinkWriterFromURL(L"output.mp4", NULL, NULL, &writer);

    int streamIndex = ConfigureVideoEncoder(writer, MFVideoFormat_RGB24, MFVideoFormat_H264, width, height, fps, width * height);

    // 書き出し
    writer->BeginWriting();
    while (true)
    {
        cv::Mat frame;
        capture >> frame;
        if (frame.empty())
            break;

        cv::flip(frame, frame, 0); // H264で出力する際、上下反転が必要
        Write(writer, streamIndex, frame.data, frame.elemSize() * frame.total());
    }
    writer->Finalize();

    // 後処理
    writer->Release();
    MFShutdown();
    CoUninitialize();
    return 0;
}

f:id:any-programming:20170309225151p:plain

ConfigureVideoEncoder

入力フォーマット、エンコーダ、画像サイズ、fps、平均bitrate、以上を渡してエンコーダをセットアップします。

DWORD ConfigureVideoEncoder(IMFSinkWriter* writer, GUID inputFormat, GUID outputFormat, int width, int height, double fps, int bitrate)
{
    auto createType = [&](GUID subtype)
    {
        IMFMediaType* type;
        MFCreateMediaType(&type);
        type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
        type->SetGUID(MF_MT_SUBTYPE, subtype);
        type->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
        MFSetAttributeSize(type, MF_MT_FRAME_SIZE, width, height);
        MFSetAttributeRatio(type, MF_MT_FRAME_RATE, (int)(fps * 10000000), 10000000);
        return type;
    };

    // OutputType
    IMFMediaType* outputType = createType(outputFormat);
    DWORD streamIndex;
    outputType->SetUINT32(MF_MT_AVG_BITRATE, bitrate);
    writer->AddStream(outputType, &streamIndex);
    outputType->Release();

    // InputType
    IMFMediaType* inputType = createType(inputFormat);
    writer->SetInputMediaType(streamIndex, inputType, NULL);
    inputType->Release();

    return streamIndex;
}


Write

WMVで出力したい場合は、IMFSampleのSetSampleTimeとSetSampleDurationを設定する必要があります。

void Write(IMFSinkWriter* writer, int streamIndex, BYTE* data, int size)
{
    // buffer作成
    IMFMediaBuffer* buffer;
    MFCreateMemoryBuffer(size, &buffer);

    BYTE* bufferPtr;
    buffer->Lock(&bufferPtr, NULL, NULL);
    memcpy(bufferPtr, data, size);
    buffer->Unlock();
    buffer->SetCurrentLength(size);

    // Sample作成
    IMFSample* sample;
    MFCreateSample(&sample);
    sample->AddBuffer(buffer);

    // 書き出し
    writer->WriteSample(streamIndex, sample);

    sample->Release();
    buffer->Release();
}






MediaFoundation --- 動画の読み込み

下記記事にてOpenCVで動画を読み書きする方法を調べました。
OpenCVで動画読み込み&書き込み - 何でもプログラミング

今回はWindows特有のMediaFoundationを利用して動画を読み込んでみます。

全体の流れの把握重視の為、エラー処理は全て省いてあります。

アプリケーションコード

動画を読み込んで、そのまま書き出しています。

今回は書き出しにはOpenCVを利用しました。

IMFSourceReaderを作成、デコーダ設定、読み込み、の流れになります。

動画はOpenCVのサンプルである、vtest.aviを利用しています。

MediaFoundationを利用するにあたり、MFStartupを忘れないようにしてください。(CoInitializeも、されていなければ呼び出してください。)

ConfigureVideoDecoder、GetVideoInfo、Captureは後程記載します。

#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "mfreadwrite.lib")

#include <opencv2\opencv.hpp>
#pragma comment(lib, "opencv_world320.lib")

int main()
{
    CoInitialize(NULL);
    MFStartup(MF_VERSION);

    IMFSourceReader* reader;
    MFCreateSourceReaderFromURL(L"c:\\lib\\opencv3.2\\sources\\samples\\data\\vtest.avi", NULL, &reader);

    ConfigureVideoDecoder(reader, MFVideoFormat_RGB24);

    UINT32 width, height;
    double fps;
    GetVideoInfo(reader, &width, &height, &fps);

    auto writer = cv::VideoWriter("output.avi", cv::VideoWriter::fourcc('X', 'V', 'I', 'D'), fps, cv::Size(width, height));
    while (true)
    {
        auto data = Capture(reader);
        if (data.empty())
            break;

        cv::Mat frame(height, width, CV_8UC3, data.data());
        writer << frame;
    }

    reader->Release();
    MFShutdown();    
    CoUninitialize();
    return 0;
}

f:id:any-programming:20170309225151p:plain

ConfigureVideoDecoder

SourceReaderにどのフォーマットで出力するか設定します。

上記ではMFVideoFormat_RGB24を指定していますが、デコーダによってはMFVideoFormat_YUY2しか出力しない等ありますので、利用の際は注意してください。

対応出力は下記ページのVideo Codecs、Decoderのリンク先を参照してください。
Supported Media Formats in Media Foundation (Windows)

void ConfigureVideoDecoder(IMFSourceReader* reader, GUID format)
{
    IMFMediaType* mediaType;
    MFCreateMediaType(&mediaType);
    mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    mediaType->SetGUID(MF_MT_SUBTYPE, format);

    reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, mediaType);

    mediaType->Release();
}


GetVideoInfo

SourceReaderからサイズとfpsを取得しています。

void GetVideoInfo(IMFSourceReader* reader, UINT32* width, UINT32* height, double* fps)
{
    IMFMediaType* mediaType;
    reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, &mediaType);

    MFGetAttributeSize(mediaType, MF_MT_FRAME_SIZE, width, height);

    UINT32 nume, denom;
    MFGetAttributeRatio(mediaType, MF_MT_FRAME_RATE, &nume, &denom);
    *fps = (double)nume / denom;

    mediaType->Release();
}


Capture

ReadSampleにてIMFSampleを取得し、そこからIMFMediaBufferを取得、データ取り出しを行っています。

ReadSample時にflagsを渡さないとsampleがnullptrになってしまいます。

std::vector<BYTE> Capture(IMFSourceReader* reader)
{
    DWORD flags;
    IMFSample* sample;
    reader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, NULL, &flags, NULL, &sample);
    if (sample == nullptr)
        return std::vector<BYTE>();

    IMFMediaBuffer* buffer;
    sample->GetBufferByIndex(0, &buffer);

    BYTE* p;
    DWORD size;
    buffer->Lock(&p, NULL, &size);
    std::vector<BYTE> data(size);
    memcpy(data.data(), p, size);
    buffer->Unlock();

    buffer->Release();
    sample->Release();
    return data;
}


シーク

SourceReaderにはSetCurrentPositionがあり、100ns単位で指定します。

ただしその位置に一番近いキーフレームに移動します。

void SeekToKeyframe(IMFSourceReader* reader, int _100ns)
{
    PROPVARIANT var;
    InitPropVariantFromInt64(_100ns, &var);
    auto hr = reader->SetCurrentPosition(GUID_NULL, var);
    PropVariantClear(&var);
}


動画時間取得

durationは100ns単位の値になります。

PROPVARIANT var;
reader->GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE, MF_PD_DURATION, &var);
LONGLONG duration;
PropVariantToInt64(var, &duration);
PropVariantClear(&var);


Fourcc取得

IMFMediaType* mediaType;
reader->GetNativeMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &mediaType);

GUID subtype;
mediaType->GetGUID(MF_MT_SUBTYPE, &subtype);

char fourcc[] =
{
    (char) (subtype.Data1 & 0XFF),
    (char)((subtype.Data1 & 0XFF00) >> 8),
    (char)((subtype.Data1 & 0XFF0000) >> 16),
    (char)((subtype.Data1 & 0XFF000000) >> 24),
};






OpenCVで動画読み込み&書き込み --- C++/CLIでラップ

下記記事にてOpenCVで動画を読み書きする方法を調べました。
OpenCVで動画読み込み&書き込み - 何でもプログラミング

今回は.Netで利用できるよう、C++/CLIでラップしてみたいと思います。

尚、Matクラス(Mat8UC1クラス)は下記記事のものを利用しています。
OpenCVをC++/CLIでラップ - 何でもプログラミング

VideoCapture

FourccはStringで返すようにしてみました。

Mat8UC1を利用しているため、Captureの中で8UC1に変換しています。ここは適宜変更してください。

using namespace msclr::interop;
using namespace System;

public ref class VideoCapture
{
public:
    property int Width  { int get()    { return (int)_capture->get(CV_CAP_PROP_FRAME_WIDTH); } }
    property int Height { int get()    { return (int)_capture->get(CV_CAP_PROP_FRAME_HEIGHT); } }
    property double Fps { double get() { return      _capture->get(CV_CAP_PROP_FPS); } }
    property int Count  { int get()    { return (int)_capture->get(CV_CAP_PROP_FRAME_COUNT); } }
    property String^ Fourcc
    {
        String^ get()
        {
            int i = (int)_capture->get(CV_CAP_PROP_FOURCC);
            char fourcc[] =
            {
                (char)(i & 0XFF),
                (char)((i & 0XFF00) >> 8),
                (char)((i & 0XFF0000) >> 16),
                (char)((i & 0XFF000000) >> 24),
                0
            };
            return gcnew String(fourcc);
        }
    }
    property bool IsOpened { bool get() { return _capture->isOpened(); } }
    VideoCapture(String^ path)
    {
        _capture = new cv::VideoCapture(marshal_as<std::string>(path));
    }
    !VideoCapture()
    {
        if (_capture != nullptr)
        {
            delete _capture;
            _capture = nullptr;
        }
    }
    ~VideoCapture() { this->!VideoCapture(); }
    Mat8UC1^ Capture()
    {
        cv::Mat src;
        *_capture >> src;

        auto dst = gcnew Mat8UC1(src.rows, src.cols);
        if (src.channels() == 1)
            src.copyTo(*dst->_mat);
        else if (src.channels() == 3 || src.channels() == 4)
            cv::cvtColor(src, *dst->_mat, CV_RGB2GRAY);
            
        return dst;
    }
    void Seek(int frame)
    {
        _capture->set(CV_CAP_PROP_POS_FRAMES, frame);
    }
private:
    cv::VideoCapture* _capture;
};


VideoWriter

Fourcc文字列からIntに変換するにはcv::VideoWriter::fourccを利用します。

Write関数内でカラー化していますが、ここは適宜変更してください。

public ref class VideoWriter
{
public:
    property bool IsOpened { bool get() { return _writer->isOpened(); } }
    VideoWriter(String^ path, int width, int height, String^ fourcc, double fps)
    {
        _writer = new cv::VideoWriter(
            marshal_as<std::string>(path), 
            cv::VideoWriter::fourcc(fourcc[0], fourcc[1], fourcc[2], fourcc[3]), 
            fps, 
            cv::Size(width, height));
    }
    !VideoWriter()
    {
        if (_writer != nullptr)
        {
            delete _writer;
            _writer = nullptr;
        }
    }
    ~VideoWriter() { this->!VideoWriter(); }
    void Write(Mat8UC1^ mat)
    {
        cv::Mat color;
        cv::cvtColor(*mat->_mat, color, CV_GRAY2BGR);

        *_writer << color;
    }
private:
    cv::VideoWriter* _writer;
};


F#で利用

動画はopenCVのサンプルMegamind.aviを利用しています。

動画をそのまま保存しているだけですが、Mat8UC1を介しているため出力はグレースケールになっています。

[<EntryPoint>]
let main argv = 
    let capture = new CV.VideoCapture(@"C:\lib\opencv3.2\sources\samples\data\megamind.avi")

    let writer = new CV.VideoWriter(@"output.avi", capture.Width, capture.Height, capture.Fourcc, capture.Fps);

    capture 
    |> Seq.unfold (fun x -> Some(x.Capture(), x)) 
    |> Seq.takeWhile (fun x -> not x.IsEmpty)
    |> Seq.iter writer.Write

    0

f:id:any-programming:20170308171514p:plain





OpenCVで動画読み込み&書き込み

OpenCVのダウンロードは下記記事を参照してください。
OpenCVをC++/CLIでラップ - 何でもプログラミング

VideoCapture

動画を読み込むにはVideoCaptureクラスを利用します。

パスを指定して開き、>>オペレータでフレームを取得します。

cv::VideoCapture capture("c:\\...");

cv::Mat frame;
capture >> frame;


VideoWriter

動画を出力するにはVideoWriterクラスを利用します。

パス、コーデック、fps、サイズを指定して作成し、<<オペレータでフレームを出力します。

cv::VideoWriter writer("c:\\...", fourcc, fps, cv::Size(width, height));

writer << frame;


エッジ化して出力する例

Win32コンソールアプリケーション(64bit)を想定しています。

動画は、opencvのサンプルとして付属しているMegamind.aviを利用します。(sources/samples/data/Megamind.avi)

開いた動画の情報は、VideoCaptureのget関数で色々取得できます。

作成したexeと同じフォルダにopencv_world320.dllとopencv_ffmpeg320_64.dllをコピーするのを忘れないようにしてください。

#include <opencv2\opencv.hpp>

#pragma comment(lib, "opencv_world320.lib")

int main()
{
    cv::VideoCapture capture("c:\\lib\\opencv3.2\\sources\\samples\\data\\megamind.avi");
    int width  = (int)capture.get(CV_CAP_PROP_FRAME_WIDTH);
    int height = (int)capture.get(CV_CAP_PROP_FRAME_HEIGHT);
    int count  = (int)capture.get(CV_CAP_PROP_FRAME_COUNT);
    int fourcc = (int)capture.get(CV_CAP_PROP_FOURCC);
    double fps = capture.get(CV_CAP_PROP_FPS);

    cv::VideoWriter writer("out.avi", fourcc, fps, cv::Size(width, height), false);

    while (true)
    {
        cv::Mat frame;
        capture >> frame;
        if (frame.empty())
            break;

        cv::Mat edge;
        cv::Canny(frame, edge, 50, 150);

        writer << edge;
    }

    return 0;
}

f:id:any-programming:20170307202225p:plain

シーク

CV_CAP_PROP_POS_FRAMESでフレーム位置を指定できます。(その他ミリ秒で設定できるCV_CAP_PROP_POS_MSECなどもあります)

capture.set(CV_CAP_PROP_POS_FRAMES, 100);


H.264

上記のアプリケーションでは、H.264の読込は出来ても、書き込みはできません。

書き込みをしたい場合は、下記からopenh264-1.6.0-win64msvc.dllをダウンロードしてexeと同じフォルダに配置してください。
Releases · cisco/openh264 · GitHub

ライセンス関係

ffmpegにはLGPL版とGPL版があり、デフォルトで配布されているopencv_ffmpeg320_64.dllはLGPL版のものになります。(そのためH.264の出力ができず、openH264を利用する形となっています。)

また、dllのライセンス以外に、H.264の利用料があります(MPEG-LA)

openH264は、利用しているアプリをインストールする際にdllをダウンロードするようにすれば、利用料は肩代わりすると書かれています。

ややこしいことを避けたい場合は、OSが提供しているAPI(Media Foundationなど)を利用するのがよいと思います。





F#でWPF --- BitmapImageで画像ロード

下記記事にてOpenCVと連携して画像を表示しました。

画像を読み込むだけであればWPFの標準セットで可能であるため、今回はWPFで画像をロードして表示してみます。

ついでにグレースケール化も行ってみます。

作成するアプリケーション

ファイルダイアログで画像を選び、表示するアプリケーションを作成します。
f:id:any-programming:20170306214920p:plain

アプリケーションコード

読込にはBitmapImageを利用します。(読込失敗の場合は例外が発生します。)

グレースケール化は、FormatConvertedBitmapを利用します。

その他ColorConvertedBitmap、TransformedBitmapなど色々あります。

Xamlは上記記事のものをそのまま使います。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        xmlns:local="clr-namespace:Actions;assembly=OpenCVSample"
        Title="MainWindow" Height="250" Width="400">
    <Grid>
        <Button Content="画像を開く" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <local:OpenFileDialogAction Command="{Binding LoadImage}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <UniformGrid Margin="10,40,10,10" Columns="2">
            <Image Source="{Binding SrcImage}" />
            <Image Source="{Binding DstImage}" />
        </UniformGrid>
    </Grid>
</Window>

F#もupdateModel以外は上記記事のものをそのまま使います。

open System
open System.Windows
open System.Windows.Media
open System.Windows.Media.Imaging

type Model = 
    { SrcImage : BitmapSource 
      DstImage : BitmapSource }

let initialModel = 
    { SrcImage = null 
      DstImage = null }

type Msg = LoadImage of string

let updateModel model msg =
    match msg with
    | LoadImage x -> 
        let bmp = try BitmapImage(Uri(x))       
                  with | _ -> failwith "not supported"
        let gray = FormatConvertedBitmap(bmp, PixelFormats.Gray8, null, 0.0)
        { model with SrcImage = bmp
                     DstImage = gray }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext(initialModel, updateModel, id)
    Application().Run(window) |> ignore   
    0