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

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






F#でWPF --- OpenCV連携

下記記事にてOpenCVを.netから利用できるようC++/CLIでラップする方法を紹介しました。
OpenCVをC++/CLIでラップ - 何でもプログラミング

今回はWPFと連携させて画像を表示してみたいと思います。

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

ファイルダイアログで画像を選び、表示するアプリケーションを作成します。折角なのでエッジ化した画像も表示してみます。

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

Canny追加

C++/CLI側のコードは上記記事のものを利用します。

追加してCanny関数を定義します。

[ExtensionAttribute]
static Mat8UC1^ Canny(Mat8UC1^ src, double threshold1, double threshold2)
{
    auto dst = gcnew Mat8UC1(src->Cols, src->Rows);
    cv::Canny(*src->_mat, *dst->_mat, threshold1, threshold2);
    return dst;
}


アプリケーションコード

WPFのImageコントロールはImageSourceクラスを受け取ります。

そのため、BitmapSource.Createを利用してMat8UC1クラスからBitmapSourceを作成しています。

Xaml内のOpenFileDialogActionは下記記事を参照してください。
F#でWPF --- ファイルダイアログCommand - 何でもプログラミング

F#内のDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

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#

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

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 mat = Processor.Load(x)
        let toBitmapSource (x : Mat8UC1) = 
            BitmapSource.Create(x.Cols, x.Rows, 96.0, 96.0, PixelFormats.Gray8, null, x.Data, x.Cols * x.Rows, x.Cols)
        { model with SrcImage = toBitmapSource mat
                     DstImage = toBitmapSource <| mat.Canny(50.0, 200.0) }

[<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






C++/CLIラッピング --- 逆引き

C++を.Netで利用する際、C++/CLIで必要になりそうなことをまとめていきます。

随時更新予定です。

クラスラップ基本構造

public ref class NetClass
{
public:
    NetClass()  { _cppClass = new CppClass(); }
    ~NetClass() { this->!NetClass(); } // Dispose()
    !NetClass() { delete _cppClass; }  // ファイナライザ
private:
    CppClass* _cppClass;
};


System::String → std::string (copy)

#include <msclr\marshal_cppstd.h>

System::String^ net;
std::string cpp = msclr::interop::marshal_as<std::string>(net);


std::string → System::String (copy)

std::string cpp;
System::String^ net = gcnew String(cpp.c_str());


System::String → const wchar_t* (no copy)

System::StringはUnicodeのため、wchar_t*しか利用できません。

System::String^ net;
pin_ptr<const wchar_t> pin = PtrToStringChars(net);
const wchar_t* p = pin;


array<int> → int* (no copy)

array<int>^ net;
pin_ptr<int> pin = &net[0];
int* p = pin;


std::vector<int> → array<int> (copy)

std::vector<int> cpp;
auto net = gcnew array<int>(cpp.size());
System::Runtime::InteropServices::Marshal::Copy(System::IntPtr(cpp.data()), net, 0, net->Length);


プロパティ

デフォルトのgetter、setterでよいならproperty int Value;のみでもOKです。

public ref class NetClass
{
public:
    property int Value 
    {
        int get()          { return 10; }
        int set(int value) { Console::WriteLine(value); }
    }
};


インターフェース

public interface class INetClass
{
public:
    virtual int GetValue();
};

public ref class NetClass : public INetClass
{
public:
    virtual int GetValue() { return 10; }
};


抽象クラス

public ref class NetClassBase abstract
{
public:
    virtual int GetValue() abstract;
};

public ref class NetClass : public NetClassBase
{
public:
    virtual int GetValue() override { return 10; }
};


拡張メソッド

using namespace System;
using namespace System::Runtime::CompilerServices;

[ExtensionAttribute]
public ref class Extension abstract sealed
{
public:
    [ExtensionAttribute]
    static String^ AddSpace(String^ src) { return src + " "; }
};


GCにメモリ使用量通知

C++のnew等でアンマネッジドなメモリを確保した場合、GCはそれを感知しません。

GCに通知しておくと、適宜Collectが行われるようになります。

0より大きい値でないと例外が発生します。

using namespace System;

GC::AddMemoryPressure(10);    // 追加
GC::RemoveMemoryPressure(10); // 削除


ref classをスコープ脱出時にデストラクト

gcnewを用いずにインスタンス化すると、通常のcppのクラスのように、スコープを抜けるとデストラクタが呼ばれます。

ref class NetClass
{
public:
    ~NetClass() { Console::WriteLine("destructed"); }
};

{
    NetClass c;
} // "destructed"


メンバのDisposeを自動で呼ぶ

スタックセマンティック(gcnewで作成されていない)のメンバが存在すると、親のDispose時にメンバのDisposeも呼んでくれます。

また、親のDisposeは宣言しなくても自動で作成されます。(宣言した場合は、親のDisposeが先に処理されます。)

ref class Child
{
public:
    ~Child() { Console::WriteLine("child disposed"); }
};

ref class Parent
{
    Child _child;
};

// C#側
Parent parent;
parent.Dispose(); // "child disposed"






OpenCVをC++/CLIでラップ

今回はOpenCVをF#から扱えるようにしてみたいと思います。

OpenCVC++のライブラリであるため、C++/CLIを用いてF#から扱えるようにします。

OpenCV取得

下記サイトから、バイナリとソースがセットになったファイルをダウンロードします。

解凍(exe実行)して適当なフォルダに配置してください。

今回は既にビルドされているopencv_world320.dllを利用していきます。
opencv/build/x64/vc14/binフォルダにあります。)

OpenCV | OpenCV
f:id:any-programming:20170302175727p:plain

C++/CLIプロジェクトの準備

VisualC++ / CLR / Class Libraryを選択します。
f:id:any-programming:20170303141633p:plain

opencv_world320.dllが64bitでビルドされているので、64bitビルドに変更します。
f:id:any-programming:20170303142050p:plain

プロジェクトの設定で、OpenCVのincludeとlibの場所を設定します。
f:id:any-programming:20170303210245p:plain

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

Matクラス(CV_8UC1限定)

内部でcv::Matポインタを保持し、propertyで適宜メンバを公開しています。

GCにメモリが確保されたことを知らせるため、AddMemoryPressureとRemoveMemoryPressureを利用しています。

~Mat8UC1()は.Net側ではDispose()に変わります。

!Mat8UC1()は.Netのファイナライザになります。

public ref class Mat8UC1
{
public:
    property int Cols { int get() { return _mat->cols; } }
    property int Rows { int get() { return _mat->rows; } }
    property IntPtr Data { IntPtr get() { return IntPtr(_mat->data); } }
    Mat8UC1(int rows, int cols)
    {
        _mat = new cv::Mat(rows, cols, CV_8UC1);
        if (_mat->empty() == false)
            GC::AddMemoryPressure(_mat->total() * _mat->elemSize());
    }
    !Mat8UC1()
    {
        if (_mat != nullptr)
        {
            if (_mat->empty() == false)
                GC::RemoveMemoryPressure(_mat->total() * _mat->elemSize());
            delete _mat;
            _mat = nullptr;
        }
    }
    ~Mat8UC1() { this->!Mat8UC1(); }
internal:
    cv::Mat* _mat;
};


Prosseorクラス

動作確認のため、画像のロードとセーブを行う関数を作成しました。

ExtensionAttributeを付与することにより、Saveは拡張メソッドとして定義されます。

System::Stringからstd::stringへの変換は"msclr/marshal_cppstd.h"を利用しています。

imreadの代に引数を0にすることにより、グレースケールで読み込むようにしてあります。

[ExtensionAttribute]
public ref class Processor abstract sealed
{
public:
    static Mat8UC1^ Load(String^ path)
    {
        cv::Mat src = cv::imread(msclr::interop::marshal_as<std::string>(path), 0);

        Mat8UC1^ dst = gcnew Mat8UC1(src.rows, src.cols);
        src.copyTo(*dst->_mat);
        return dst;
    }
    [ExtensionAttribute]
    static void Save(Mat8UC1^ src, String^ path)
    {
        cv::imwrite(msclr::interop::marshal_as<std::string>(path), *src->_mat);
    }
};


コード全体(ヘッダファイル)

警告が発生するので、"opencv2\opencv.hpp"はunmanagedでコンパイルされるようにしてあります。

#pragma once

#pragma unmanaged
#include <opencv2\opencv.hpp>
#pragma managed

#include <msclr\marshal_cppstd.h>

#ifdef _DEBUG
#pragma comment(lib, "opencv_world320d.lib")
#else
#pragma comment(lib, "opencv_world320.lib")
#endif

using namespace System;
using namespace System::Runtime::CompilerServices;

namespace CV
{
    public ref class Mat8UC1
    {
    public:
        property int Cols { int get() { return _mat->cols; } }
        property int Rows { int get() { return _mat->rows; } }
        property IntPtr Data { IntPtr get() { return IntPtr(_mat->data); } }
        Mat8UC1(int rows, int cols)
        {
            _mat = new cv::Mat(rows, cols, CV_8UC1);
            if (_mat->empty() == false)
                GC::AddMemoryPressure(_mat->total() * _mat->elemSize());
        }
        !Mat8UC1()
        {
            if (_mat != nullptr)
            {
                if (_mat->empty() == false)
                    GC::RemoveMemoryPressure(_mat->total() * _mat->elemSize());
                delete _mat;
                _mat = nullptr;
            }
        }
        ~Mat8UC1() { this->!Mat8UC1(); }
    internal:
        cv::Mat* _mat;
    };

    [ExtensionAttribute]
    public ref class Processor abstract sealed
    {
    public:
        static Mat8UC1^ Load(String^ path)
        {
            cv::Mat src = cv::imread(msclr::interop::marshal_as<std::string>(path), 0);

            Mat8UC1^ dst = gcnew Mat8UC1(src.rows, src.cols);
            src.copyTo(*dst->_mat);
            return dst;
        }
        [ExtensionAttribute]
        static void Save(Mat8UC1^ src, String^ path)
        {
            cv::imwrite(msclr::interop::marshal_as<std::string>(path), *src->_mat);
        }
    };
}


動作確認

単に画像を読み込んで保存するだけです。

exeと同じフォルダにopencv_world320.dllを配置するのを忘れないようにしてください。
(面倒であればリソースに追加して"copy if newer"にするのもアリです。)

また、64bitターゲットにするのを忘れないようにしてください。
f:id:any-programming:20170304143417p:plain

open CV

[<EntryPoint>]
let main argv = 
    let image = Processor.Load(@"image.jpg")    
    image.Save(@"image2.jpg")
    0






Elm --- 階層化

下記記事にてElmを用いてカウンタを実装しました。
Elm --- Model、View、Update - 何でもプログラミング

今回はこのカウンタを再利用して、複数のカウンタを配置してみます。

内容はElmのTutorialにあるものとほとんど同じです。

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

カウンタが2つあり、一番下に合計値が出力されるアプリケーションになります。
f:id:any-programming:20170302193737p:plain

Counter.elm

新たにCounter.elmファイルを作成し、下記コードを記述します。

内容は上記記事のものとほぼ同じです。

VisualStudioCodeを利用しているのですが、関数の型宣言をしないと警告が出るようになっていました。

module Counter exposing (..)

import Html exposing (Html, div, button, text)
import Html.Events exposing (onClick)

type alias Model = { count : Int }

initialModel : Model
initialModel = { count = 0 }

type Msg 
    = Increment 
    | Decrement

update : Msg -> Model -> Model
update msg model =
  case msg of
    Increment ->
      { model | count = model.count + 1 }

    Decrement ->
      { model | count = model.count - 1 }

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model.count) ]
    , button [ onClick Increment ] [ text "+" ]
    ]


Main.elm

CounterのModel、update、viewをそのまま利用しています。

Html.App.mapを利用して、CounterのメッセージをMainの方に伝搬しています。

beginnerProgramがいつの間にかHtml.Appに移動していました。

import Html exposing (Html, div, text)
import Html.App exposing (beginnerProgram, map)
import Counter

type alias Model = 
    { counter1 : Counter.Model 
    , counter2 : Counter.Model
    }

initialModel : Model 
initialModel =
    { counter1 = Counter.initialModel
    , counter2 = Counter.initialModel
    }

type Msg 
    = Counter1Msg Counter.Msg 
    | Counter2Msg Counter.Msg

update : Msg -> Model -> Model
update msg model =
  case msg of
    Counter1Msg x ->
      { model | counter1 = Counter.update x model.counter1 }

    Counter2Msg x ->
      { model | counter2 = Counter.update x model.counter2 }

view : Model -> Html Msg
view model =
  div []
    [ map Counter1Msg (Counter.view model.counter1)
    , map Counter2Msg (Counter.view model.counter2)
    , div [] [ text (toString (model.counter1.count + model.counter2.count)) ]
    ]

main : Program Never
main =
  beginnerProgram 
  { model = initialModel
  , view = view
  , update = update 
  }


主要部分

// CounterのModelを保持
type alias Model = 
    { counter1 : Counter.Model 

// メッセージの一つをCounterのMsg型に
type Msg 
    = Counter1Msg Counter.Msg 

// CounterのMsg型が来たらCounterのupdateを実行
update msg model =
  case msg of
    Counter1Msg x ->
      { model | counter1 = Counter.update x model.counter1 }

// Counterのviewを利用し、mapにてメッセージを受け取り
view model =
  div []
    [ map Counter1Msg (Counter.view model.counter1)


階層化よりコントロール

公式でもあまりネストさせるのは推奨しておりません。

Mainの方で子要素のupdateやmsgを操作するよりは、Htmlのコントロール化(今回であればHtml.counterみたいなのを準備)したほうがフラットになってよい気がします。





リフレクション 逆引き (F#)

F#でリフレクションを利用するときに、やり方を忘れていることがよくあるため、ここを備忘録にしたいと思います。

今後適宜追加していこうと思います。

Type取得

let t = typeof<int>


TypeDefinition取得

let t = typedefof<List<_>>


TypeDefinitionからType作成

let t = typedefof<List<_>>.MakeGenericType(typeof<int>) // List<int>


TypeからTypeDefinition取得

typeof<List<int>>.GetGenericTypeDefinition() = typedefof<List<_>> // true


Cast可能かどうか

typeof<IEnumerable<int>>.IsAssignableFrom(typeof<List<int>>) // true


static classかどうか

let t = typeof<Enumerable>
t.IsAbstract && t.IsSealed // true


ロードされているAssembly全て取得

let assemblies = System.AppDomain.CurrentDomain.GetAssemblies()


オーバーロードかつジェネリックな関数を取得

現状GetMethods()して絞り込む方法しかなさそうです。

下記はSystem.Linq.Enumerable.Selectの一つを取得しています。

let selectMethod = 
    typeof<Enumerable>.GetMethods(BindingFlags.Static ||| BindingFlags.Public)
    |> Array.find (fun x -> 
        let parameters = 
            x.GetParameters()
            |> Array.map (fun x -> x.ParameterType.GetGenericTypeDefinition())
            |> Array.toList           
        x.Name = "Select" && parameters = [ typedefof<IEnumerable<_>>; typedefof<Func<_, _>> ])


拡張メソッドかどうか

let method = typeof<Enumerable>.GetMethod("All")
method.IsDefined(typeof<ExtensionAttribute>, true) // true


Cast可能な型一覧取得

ベースクラスはBaseTypeで、インターフェースはGetInterfaces()で取得します。

インターフェースの継承関係はツリー構造になるため、flatTreeを用意しています。

let assignableTypes (type_ : Type) = 
    let flatTree getChildren root =
        [ root ]
        |> List.unfold (function | []   -> None
                                 | h::t -> Some (h, t @ getChildren h)) 
    type_
    |> List.unfold (Option.ofObj >> Option.map (fun x -> x, x.BaseType))
    |> List.collect (flatTree (fun x -> x.GetInterfaces() |> Array.toList))
    |> List.distinct