C#によるUSBカメラ操作(WPF編)

前回は、WindowsFormsを使ったUSBカメラの撮像方法を紹介しました。

今回はWPF、といってもWPFはSilverlight4のようにUSBカメラの直接サポートはありませんので前回同様AForge.NETを使います。WPF?と思われるかと思いますが、個人の勉強のために使っただけです。ご了承ください。

まず、Nugetから

PM> install-package AForge.Video
PM> install-package AForge.Video.DirectShow

としてパッケージをインストールしておきます。

プロジェクト構成はこんな感じ。

0226project構成

WindowsFormsのときと使い方は同じなので、ヘルパー的なクラスが2つあります。

これを先に簡単に説明します。

DeviceFilters.cs

using System.Collections.Generic;
using System.Linq;
using AForge.Video.DirectShow;

namespace TestWpfApp
{
    class DeviceFilters
    {
        public string Name { set; get; }
        public string MonikerString { set; get; }

        public IEnumerable Get(){
            return from FilterInfo info in new FilterInfoCollection(FilterCategory.VideoInputDevice)
                   select new DeviceFilters { Name = info.Name, MonikerString = info.MonikerString };
        }
    }
}

BitmapToBitmapFrame.cs

using System.Drawing;
using System.IO;
using System.Drawing.Imaging;
using System.Windows.Media.Imaging;

namespace TestWpfApp
{
    internal class BitmapToBitmapFrame
    {
        internal static BitmapFrame Convert(Bitmap src)
        {
            using (var stream = new MemoryStream())
            {
                src.Save(stream, ImageFormat.Bmp);
                return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
            }
        }

    }
}

前者DeviceFiltersは接続対象のUSBカメラデバイスです。オブジェクトとしてComboBoxにバインドするために作りました(ViewModel)。
後者BitmapToBitmapFrameはAForge.NETから取得できるのはBitmapですが、それがWPFのImageコントロールにそのまま貼り付けられないため
貼り付けられる形式のBitmapFrameに変換するためのクラスです。

下ごしらえがこれだけ。メインのCamDeviceOperationControlです。

見た目はこんな感じ。

0226camdeviceopectrl

WindowsFormsのときと似た感じです。

CamDeviceOperationControl.xaml

<UserControl x:Class="TestWpfApp.CamDeviceOperationControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:afd="clr-namespace:AForge.Video.DirectShow;assembly=AForge.Video.DirectShow"
             xmlns:dev="clr-namespace:TestWpfApp"
             Height="80" Width="375">
    <StackPanel>
        <StackPanel.Resources>
            <ObjectDataProvider x:Key="devices" ObjectType="dev:DeviceFilters" MethodName="Get"/>
        </StackPanel.Resources>
        <Label Content="Device"/>
        <ComboBox Name="deviceListCombo"
                  ItemsSource="{Binding Source ={StaticResource devices}}"
                  DisplayMemberPath="Name"
                  SelectedValuePath="MonikerString"/>
        <StackPanel Height="28" Orientation="Horizontal" HorizontalAlignment="Right">
            <Label Name="statusLabel">status</Label>
            <Button Width ="80" Content="Connect" Click="Connect_Click"/>
            <Button Width ="80" Content="Disconnect" Click="Disconnect_Click"/>
        </StackPanel>
    </StackPanel>
</UserControl>

CamDeviceOperationControl.xaml.cs

using System.Windows;
using System.Windows.Controls;
using AForge.Video;
using AForge.Video.DirectShow;

namespace TestWpfApp
{
    public partial class CamDeviceOperationControl : UserControl
    {
        public event NewFrameEventHandler NewFrameGot = delegate { };
        private VideoCaptureDevice device;

        public CamDeviceOperationControl()
        {
            InitializeComponent();
        }

        private void Connect_Click(object sender, RoutedEventArgs e)
        {
            device = new VideoCaptureDevice((string)deviceListCombo.SelectedValue);
            device.NewFrame += NewFrameGot;
            device.Start();
        }

        private void Disconnect_Click(object sender, RoutedEventArgs e)
        {
            device.NewFrame -= NewFrameGot;
            device.SignalToStop();
            device = null;
        }
    }
}

XAMLにデバイス検索&表示部を任せられているので、C#のコードが単純になっています。

最初に出てきたDebviceFilterをComboBoxにバインドさせ、Nameを表示させ取得する値はMonikerStringでこれを使ってVideoCaptureDeviceを作成しています。

これを使うのがMainWindowです、Imageと出てきたCamDeiceOperationControlの2つだけが貼り付けられています。

0226mainwindow

Imageコントロールが見えないので選択状態にしています。
MainWindow.xaml

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TestWpfApp" x:Class="TestWpfApp.MainWindow"
        Title="MainWindow" Height="381.188" Width="417.574">
    <StackPanel Orientation="Vertical">
        <local:CamDeviceOperationControl x:Name="camDeviceCtrl" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="388"/>
        <Image x:Name="picture" HorizontalAlignment="Left" Height="246" Margin="10,0,0,0" VerticalAlignment="Top" Width="388"/>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Windows;
using System.Drawing;

namespace TestWpfApp
{
    public partial class MainWindow : Window
    {
        public MainWindow(){
            InitializeComponent();
            camDeviceCtrl.NewFrameGot += CamDeviceCtrlNewFrameGot;
        }

        private void CamDeviceCtrlNewFrameGot(object sender, AForge.Video.NewFrameEventArgs eventArgs){
            picture.Dispatcher.Invoke(new Action(bmp => picture.Source = BitmapToBitmapFrame.Convert(bmp)),eventArgs.Frame);
        }
    }
}

こちらもXAMLに任せている部分のおかげでC#のコードは簡単ですね。CamDeviceOperationControlが公開するイベント引数のBitmapを変換しているだけです。

動くとこんな感じ。

0226動作状況

WindowsFormsのときよりエラー処理などが不足しています;

WPFもっとこう使ったほうがイイヨ!というご指摘あれば是非お願いします。

C#によるUSBカメラ操作

あちこちの投稿であるようですが、「動かない!」とのお怒りもあるようですので、
動くものを紹介したいと思います。

AForge.NETというライブラリを使います。
Nugetからは
PM> package-install AForge.Video
PM> package-install AForge.Video.DirectShow
を実行しておいてください。

相変わらず、わかりやすさを重視してWindows Formsで作成します。

プロジェクト構成はシンプルにファイル3つ。
0224projectの中身

メインとなる cameraDeviceControl.cs の構成をご紹介します。

概観はこんな感じです。

usercontrolの概観

using System;
using System.Windows.Forms;
using AForge.Video;
using AForge.Video.DirectShow;

namespace TestForms
{
    public partial class cameraDeviceControl : UserControl
    {
        private FilterInfoCollection videoDevices;
        private VideoCaptureDevice videoSource;
        public event NewFrameEventHandler NewFrameGot = delegate { };

        public cameraDeviceControl()
        {
            InitializeComponent();
            UpdateConnectingStatus(false);
        }

        public void ReloadCameraDevices()
        {
            try
            {
                TryReloadCameraDevices();
            }
            catch (ApplicationException e)
            {
                statusLabel.Text = e.Message;
            }
        }

        private void TryReloadCameraDevices()
        {
            videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
            if (videoDevices.Count == 0)
            {
                throw new ApplicationException("No device.");
            }
            UpdateDeviceNames();
        }

        private void UpdateDeviceNames()
        {
            deviceListcomboBox.Items.Clear();
            foreach (FilterInfo device in videoDevices)
            {
                deviceListcomboBox.Items.Add(device.Name);
            }
            deviceListcomboBox.SelectedIndex = 0;   // make default to first cam.
        }

        private void deviceListcomboBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            bool existAvailableDevice = videoDevices != null && videoDevices.Count != 0;
            if (!existAvailableDevice)
            {
                return;
            }
            string targetMoniker = videoDevices[deviceListcomboBox.SelectedIndex].MonikerString;
            videoSource = new VideoCaptureDevice(targetMoniker);
            connectButton.Enabled = true;
        }

        private void connectButton_Click(object sender, EventArgs e)
        {
            videoSource.NewFrame += NewFrameGot;
            videoSource.Start();
            UpdateConnectingStatus(true);
        }

        private void disconnectButton_Click(object sender, EventArgs e)
        {
            if (videoSource == null) return;
            if (!videoSource.IsRunning) return;
            videoSource.NewFrame -= NewFrameGot;
            videoSource.SignalToStop();
            UpdateConnectingStatus(false);

        }

        private void UpdateConnectingStatus(bool isStart)
        {
            statusLabel.Text = isStart ? "Running.." : "Stopped.";
            deviceListcomboBox.Enabled = !isStart;
            connectButton.Enabled = !isStart && deviceListcomboBox.SelectedIndex>=0;
            disconnectButton.Enabled = isStart;
        }
    }
}

あとはこれをFormに貼り付ければ終いです。
Formには上で作ったUserControl(名前はcamDeviceControl)とpictureBoxの2つだけを張っています。

using System;
using System.Drawing;
using System.Windows.Forms;

namespace TestForms
{
    public partial class TestForm : Form
    {
        public TestForm()
        {
            InitializeComponent();
        }

        private void TestForm_Shown(object sender, EventArgs e)
        {
            camDeviceControl.ReloadCameraDevices();
            camDeviceControl.NewFrameGot += camDeviceControl_NewFrameGot;
        }

        void camDeviceControl_NewFrameGot(object sender, AForge.Video.NewFrameEventArgs eventArgs)
        {
            if (InvokeRequired)
            {
                var updater = new Action(UpdateDisplayImage);
                try
                {
                    Invoke(updater, eventArgs.Frame);
                }
                catch (ObjectDisposedException)
                {
                    // through.
                }
                return;
            }
            UpdateDisplayImage(eventArgs.Frame);
        }

        private void UpdateDisplayImage(Bitmap img)
        {
            capturedPictureBox.Image = (Bitmap)img.Clone();
        }
    }
}

エラー処理は全体に適当ですが、これだけでUSBカメラを動作させることができます。

起動時(USBカメラがないとcomboboxは空だよ)
起動時
接続時
接続状態

いまさらWCFでプロセス間通信

.NET Framework3.0で登場したWCF。

「プロセス間通信」というと、名前付きパイプ、メールスロット、ファイルマッピング、.NET2.0で登場した.NET Remoting。

私自身WCFの全貌を理解しているとは言えませんが、WCFを使ったプロセス間通信を紹介します。

大まかな手順は、

  1. サービス内容を決定する
  2. サービスを提供する側を実装する
  3. サービスを受ける側を実装する

となります。

作成予定のプロジェクトは以下の3つ。

WCFプロジェクト構成

Step1.サービス内容(=コントラクト)を決定する

どのようなサービスを提供・受領するか、サービス内容(Contract : コントラクト)を決定します。

WCFではABCという言葉が出てきますが、AはAddress(どこから)、BはBinding(どうやって)、CはContract(なにを)を表しており、ここではこの’ C ‘ に相当するものを用意します。といっても簡単で、C#のInterfaceで実装するだけです。

TestContract.proj

Interface 1つをもつクラスライブラリとして作成します。

using System;
using System.ServiceModel;

namespace TestContract
{
    [ServiceContract]
    public interface ITestContract
    {
        [OperationContract]
        DateTime GetTime();
    }
}

これだけ。時間を渡すという「約束(Contract)」をはっきりさせておきます。
※後で出てくる2つ含め3つ全てのプロジェクトでSystem.ServiceModelアセンブリおよびSystem.ServiceModel名前空間の参照が必要です。

Step2.サービスを提供する側(Server, Host, Service Provider)を実装する

WCFでは通常Hostというようです、今回はコンソールアプリとして作成します。

TestServer.proj内 TestServer.cs

using System;
using System.ServiceModel;

namespace TestServer
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class TestServer : TestContract.ITestContract
    {
        public DateTime GetTime()
        {
            return DateTime.Now;
        }
    }
}

現在時刻をもらいます。InstanceContextModeはMSDNに記載の以下の目的で設定しています。

instancemodeの必要性
program.cs

using System;
using System.ServiceModel;

namespace TestServer
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(new TestServer(),
                new Uri("net.pipe://localhost"));
            host.AddServiceEndpoint(
                typeof(TestContract.ITestContract),
                new NetNamedPipeBinding(),
                "InterProcessCommunication");
            host.Open();
            Console.WriteLine("Enter押下で終了.");
            Console.ReadLine();
        }
    }
}

Step3.サービスを受ける側(Client, Service User)を実装する
ごくシンプルなWindowsFormsで作成します。

formの図

ClientForm.cs

using System;
using System.Windows.Forms;
using System.ServiceModel;

namespace TestClient
{
    public partial class ClientForm : Form
    {
        private readonly TestContract.ITestContract server;
        public ClientForm()
        {
            InitializeComponent();
            server = new ChannelFactory<TestContract.ITestContract>(
                new NetNamedPipeBinding(),
                new EndpointAddress("net.pipe://localhost/InterProcessCommunication")
                ).CreateChannel();
            updateTimer.Enabled = true;
        }

        private void updateTimer_Tick(object sender, EventArgs e)
        {
            if (server == null) {
                return;
            }
            try{
                labelTime.Text = server.GetTime().ToString();
            }
            catch (EndpointNotFoundException){
                labelTime.Text = string.Empty;
            }
            catch (CommunicationException){
                labelTime.Text = string.Empty;
            }
        }
    }
}

EndPointAddressでServerを作成するときに使ったアドレスと合致させる必要がある点に注意してください。

これにて終了です。

サーバ不在時はEndPointNotFoundException、通信中に途切れた場合はCommunicationExceptionが発生します。
どちらもFormでcatchして表示をなくしています。
動作様子-サーバ不在

この状態からサーバを立ち上げると・・
動作様子-通信できているとき
現在時間表示がされるようになります。

接続確立状態からサーバが死んだとしても、クライアント側は例外のケアさえしておけば再接続のような処理は不要で、
サーバが立ち上がったときに再度自動的に接続が可能となります。

MEFによるC#アドインの作成

画像

.NET Framework4.0で登場したMEFによりアドイン作成が非常に簡単になりました。

MEFを使ったアドイン構成方法を紹介します。

大まかな手順は、

  1. アドインのIFを決める
  2. 参照されるアドインを作成する
  3. アドインをロードする側を作成する

となります。

準備としてまず以下の4つのプロジェクトを作成しておきます。AddinContractはアドイン提供機能を定義する抽象層、

AddinA, AddinBはアドインを実装する外部プロジェクトの想定、UseAddinFormはWinFormsのアドイン受付モジュールです。

image1

Step1.アドインのIFを決める

クラス設計原則SOLIDの、DIPでも言われるようアドインを提供する側、アドインを作成する側、ともに抽象に依存するため、

アドインが提供する機能 (=アドインで提供される機能)をC#のInterfaceとして作成します。

AddinContract.proj

namespace AddinContract{
     public interface IAddinContract{
          string AddinTitle { get; }
          void DoWork();
     }
}

単にアドインの名前と処理を返すことにします。

Step2.アドインで提供する機能を実装する

AddinA.proj

using System.ComponentModel.Composition;
namespace AddinA {
     [Export(typeof(IAddinContract))]
     public class AddinSampleA : IAddinContract     {
          public string AddinTitle { get { return "Addin - A"; } }
          public void DoWork(){return;}
     }
}

ポイントはExport(typeof(IAddinContract))属性です。MEFにここからAddinをExportしますよ~と伝えます。
これで1つのアドインができました。他のアドインを作る場合も同様にExportで機能公開し、
ちゃんとIAddinContractを実装します。

Step3.アドインをロード、実行する

見た目はこんな感じ。

form

WinFormsである必要はないのですが、わかりやすい例としてこれにしました。

 

UseAddinForm.proj

using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

namespace UseAddinForm {
     public partial class ExtensibleForm : Form     {

          [ImportMany]
          private List addins;

          public ExtensibleForm(){
               InitializeComponent();
          }

          public void UpdateAddins() {
               try{
                    var container = GetContainerFromDirectory();
                    container.ComposeParts(this);
               }
               catch (CompositionException){
                    // through.
               }
          }

          private CompositionContainer GetContainerFromDirectory(){
               var catalog = new DirectoryCatalog("addins");
               return new CompositionContainer(catalog);
          }

          private void updateButton_Click(object sender, EventArgs e){
               UpdateAddins();
               foreach (var addin in addins){
                    addinsListBox.Items.Add(addin.AddinTitle);
               }
          }

          private void executeButton_Click(object sender, EventArgs e){
               if (addinsListBox.SelectedIndex < 0){
                    return;
               }
               addins[addinsListBox.SelectedIndex].DoWork();
          }

          private void closeButton_Click(object sender, EventArgs e){
               Close();
          }
     }
}

想定として、実行プログラムの下にaddinsサブフォルダがあり、その中にアドインを実装したdllがある想定です。

ここでのポイントはImportManyでアドインを保持する構造を作っておくことでしょうか。
アドインを使う側の上記コードではどこにもaddinsにデータをセットする人がいません。

にもかかわらず、実行ボタンを押下するとアドインの一覧が表示されます。

adin実行の様子

ここでは単機能のアドインですが、アドイン側で新たにFormを立ち上げたり、サービスを立ち上げたりできます。

商利用のアドインでは、ユーザが追加購入して「画像処理モジュール」や「信号解析モジュール」を

追加搭載する、といった形のものが一般的かと思います。