読者です 読者をやめる 読者になる 読者になる

Dlib --- 環境準備

Dlib

DlibはC++で記述された機械学習用のライブラリです。

SVMやDeep Learning等、よく聞くアルゴリズムのものは一通り準備されています。

ドキュメントやサンプルが整っているのが一番嬉しい点だと思います。

Dlibダウンロード

下記サイトよりファイル一式をダウンロードし、好きな場所に解凍してください。

dlib C++ Library
f:id:any-programming:20170317171903p:plain

ビルドせずに利用

一般的にはCMakeを利用してlibファイルを作成し、それをリンクして利用します。

他の方法として、cppをそのままプロジェクトに追加して利用することも可能です。

ビルド自体は10秒程度で終わりますので、libのVC++のバージョン管理が面倒な場合はcpp追加で利用するのもいいと思います。

今回はcppを利用する方法で進めていきます。

プロジェクトの準備

VC++のWin32コンソールプロジェクトを想定しています。

ソースに、既存のファイルの追加で、dlib解凍先/dlib/all/source.cpp を追加します。
f:id:any-programming:20170319121011p:plain

プリコンパイルのエラー回避のため、source.cppの先頭に #include "stdafx.h" を追加します。

また今回はGUIを利用しないため、#define DLIB_NO_GUI_SUPPORT も追加しておきます。

#include "stdafx.h"
#define DLIB_NO_GUI_SUPPORT

// Copyright (C) 2006  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_ALL_SOURCe_
#define DLIB_ALL_SOURCe_
...


最後にinclude参照先として、dlib解凍先を指定します。
f:id:any-programming:20170319121605p:plain

アプリケーションコード

今回はdlibのサンプル画像で顔認識をしてみます。

frontal_face_detectorを利用して、顔のrectangleを取得しています。

今回はdlibのjpeg読み込みをONにしていないため、bmpに変換して読み込んでいます。

source.cppに #define DLIB_JPEG_SUPPORT を追加して、さらに dlib解凍先/dlib/external/libjpeg の全cppファイルをプロジェクトに追加すればjpegを読み込めるようになりますが、ここまでくればCMakeでlibを作成する方が楽だと思います。

#include "stdafx.h"

#include "dlib\image_processing\frontal_face_detector.h"
#include "dlib\image_io.h"

int main()
{
    dlib::array2d<dlib::rgb_pixel> image;
    dlib::load_bmp(image, "C:\\lib\\dlib-19.4\\examples\\faces\\2008_001322.bmp");

    auto detector = dlib::get_frontal_face_detector();
    auto rectangles = detector(image);

    for (auto& rect : rectangles)
       dlib::draw_rectangle(image, rect, dlib::rgb_pixel(255, 0, 0), 3);

    dlib::save_bmp(image, "output.bmp");

    return 0;
}
f:id:any-programming:20170319121911j:plain f:id:any-programming:20170319121920j:plain





GDI+ --- 画像ファイル読み書き

GDI+

C++で画像ファイルを読み書きする際は、libjpegやlibpng等を利用します。

しかしWindowsでは標準ライブラリでないため、ライブラリを準備する必要があります。

ちょっと動作確認の時など、ライブラリの準備が面倒な時はGDI+を利用することができます。

コードはエラー処理を省いてあります。(返り値がGdiplus::Statusの関数は、Gdiplus::OKかどうか確認してください。)

画像読み込み

Gdiplus::Bitmapを利用して画像を読み込みます。

画素データは4Byte単位のStrideで保存されており、今回は余白を切り詰めて取得するよう実装してあります。

LockBits時に渡すフォーマットは、元データのフォーマットである必要はなく、可能であれば自動で変換してくれます。

void GdiplusLoadImage(std::wstring path, int& width, int&height, std::vector<byte>& pixels)
{
    auto image = Gdiplus::Bitmap::FromFile(path.c_str());
    width = image->GetWidth();
    height = image->GetHeight();
    auto stride = 3 * width;

    pixels.resize(stride * height);
    Gdiplus::Rect rect(0, 0, width, height);
    Gdiplus::BitmapData bmpData;
    image->LockBits(&rect, Gdiplus::ImageLockModeRead, PixelFormat24bppRGB, &bmpData);

    for (int y = 0; y < height; ++y)
        memcpy(pixels.data() + y * stride, (byte*)bmpData.Scan0 + y * bmpData.Stride, stride);
    
    image->UnlockBits(&bmpData);
}


画像保存(jpeg

読み込み時と同様に、Gdiplus::Bitmapを用いて画像を保存します。

画素データは4Byte単位である必要があるので、余白を追加したものを作成しています。

エンコーダのIDはGdiplus::GetImageEncodersで探すのが普通ですが、IDが変わる可能性はほぼ無いと考えられるため直指定しています。(その他例えばpngの場合は0x557cf406...となります。)

void GdiplusSaveJpeg(std::wstring path, int width, int height, const std::vector<byte>& pixels)
{
    auto stride = 3 * width;
    auto gdiStride = stride % 4 == 0
        ? stride
        : stride + 4 - stride % 4;

    std::vector<byte> gdiPixels(gdiStride * height);
    for (int y = 0; y < height; ++y)
        memcpy(gdiPixels.data() + y * gdiStride, pixels.data() + y * stride, stride);

    Gdiplus::Bitmap image(width, height, gdiStride, PixelFormat24bppRGB, gdiPixels.data());
    CLSID id = { 0x557cf401, 0x1a04, 0x11d3,{ 0x9a, 0x73, 0x00, 0x00, 0xf8, 0x1e, 0xf3, 0x2e } };
    image.Save(path.c_str(), &id);
}


アプリケーションコード

GDI+を利用するには、GdiplusStartupとGdiplusShutdownを呼ぶ必要があります。

今回は入力画像を赤青反転して出力するコードを作成してみました。

#include <Windows.h>
#include <gdiplus.h>
#include <gdipluspixelformats.h> // PixelFormat24bppRGB
#pragma comment(lib, "gdiplus.lib")

int main()
{
    Gdiplus::GdiplusStartupInput input;
    ULONG_PTR token;
    Gdiplus::GdiplusStartup(&token, &input, NULL);

    int width, height;
    std::vector<byte> pixels;
    GdiplusLoadImage(L"Parrots.bmp", width, height, pixels);

    for (int i = 0; i < width * height; ++i)
        std::swap(pixels[3 * i], pixels[3 * i + 2]);

    GdiplusSaveJpeg(L"output.jpg", width, height, pixels);

    Gdiplus::GdiplusShutdown(token);

    return 0;
}
f:id:any-programming:20170319005947j:plain f:id:any-programming:20170319010003j:plain






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

F# WPF

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

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 --- 動画の読み込み - 何でもプログラミング
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で動画を読み込む方法を調べました。
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 --- 動画の読み込み

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でラップ

C++/CLI OpenCV F#

下記記事にて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